Gateway: keep whole T3 history pages, and show a command's first line as its activity
Gateway / check (push) Successful in 8s
Gateway / image (push) Successful in 19s

This commit is contained in:
gib committed 2026-11-02 12:44:08 -05:00
1 parent 52210d0223
commit abd2fd0e13
3 files changed
+13 -7

No files matched your search

+3 -2
View File
@@ -94,7 +94,7 @@ when the gateway cannot find its parent, or the parent already shows a request.
for a moment after a thread changes.
`activity` is the newest step the agent has in progress. `kind` is `command`, `edit`, `search`,
`web`, `subagent`, `thinking` or `tool`, and `text` is the command line, the file name (without its
`web`, `subagent`, `thinking` or `tool`, and `text` is the command's first line, the file name (without its
directory), the search, the subagent's title or the tool's name, trimmed to about 80 characters.
Monitors are left out, since they run for hours. It is null when no step is in progress. The gateway
rereads a running thread at most every 5 seconds, so it can lag that much.
@@ -136,7 +136,8 @@ GET /v1/threads/:hostId/:threadId/history?cursor=<earlierCursor>
200 { "messages": [Message], "earlierCursor": "..." | null }
```
The 20 messages before the ones the watch has, oldest first. Pass the returned `earlierCursor` for
The messages before the ones the watch has, oldest first: 20 at a time at first, then one T3 history
page at a time, which can hold more. Pass the returned `earlierCursor` for
the page before that. It is null at the start of the thread. Cursors are opaque.
```
+8 -3
View File
@@ -93,7 +93,10 @@ function windowCursor(start: number, bounded: BoundedThread): string | null {
return bounded.hasMoreHistory && bounded.historyCursor ? `h${bounded.historyCursor}` : null;
}
/** Up to 20 messages before `cursor`. T3 pages without messages are skipped, three at most. */
/**
* The messages before `cursor`: 20 from the bounded window, or one T3 page, which can hold more.
* T3 pages without messages are skipped, three at most.
*/
export async function loadHistory(host: T3Host, thread: ThreadShell, cursor: string): Promise<HistoryResponse> {
if (cursor.startsWith("w")) {
const end = Number(cursor.slice(1));
@@ -120,7 +123,7 @@ export async function loadHistory(host: T3Host, thread: ThreadShell, cursor: str
createdAt: item.startedAt ?? item.updatedAt ?? "",
}),
);
if (messages.length > 0) return { messages: messages.slice(-MESSAGES_PER_THREAD), earlierCursor: next && `h${next}` };
if (messages.length > 0) return { messages, earlierCursor: next && `h${next}` };
}
return { messages: [], earlierCursor: next && `h${next}` };
}
@@ -139,7 +142,9 @@ export function currentActivity(turnItems: TurnItem[]): Activity | null {
function describeStep(item: TurnItem): Omit<Activity, "startedAt"> | null {
switch (item.type) {
case "command_execution":
return item.monitor || typeof item.input !== "string" ? null : { kind: "command", text: item.input };
return item.monitor || typeof item.input !== "string"
? null
: { kind: "command", text: item.input.split("\n").find((line) => line.trim()) ?? item.input };
case "file_change":
return item.fileName ? { kind: "edit", text: item.fileName.split("/").pop()! } : null;
case "file_search":
+2 -2
View File
@@ -239,11 +239,11 @@ describe("currentActivity", () => {
expect(
currentActivity([
item("file_change", "running", { fileName: "gateway/src/host.ts" }),
item("command_execution", "running", { input: "bun test" }),
item("command_execution", "running", { input: "\nbun test \\\n --bail" }),
item("command_execution", "running", { input: "tail -f log", monitor: true }),
item("assistant_message", "running"),
]),
).toEqual({ kind: "command", text: "bun test", startedAt: "2026-11-01T09:00:00.000Z" });
).toEqual({ kind: "command", text: "bun test \\", startedAt: "2026-11-01T09:00:00.000Z" });
expect(currentActivity([item("file_change", "running", { fileName: "gateway/src/host.ts" })])?.text).toBe("host.ts");
});