Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions src/tui/product-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { buildModelsFirstCatalog, modelOptionId } from "./model-catalog.js";
import { hydrateHistoryRows } from "./history-hydrate.js";
import { MAX_RETAINED_STREAM_ROWS } from "./long-log.js";
import { enterSubagentObserve } from "./shell/observe.js";
import { transcriptMarker } from "./shell/transcript.js";

function makeFakeSessionPort(): {
readonly sends: string[];
Expand Down Expand Up @@ -146,9 +147,9 @@ describe("mountProductHost", () => {
role: "assistant",
text: `row-${total - 1}`,
});
// Pre-sliced to the cap, so the append loop never trips retention
// eviction: no churn, no dropped-rows marker on resume.
expect(host.shell.streamLogBase).toBe(0);
const dropped = total - MAX_RETAINED_STREAM_ROWS;
expect(host.shell.streamLogBase).toBe(dropped);
expect(transcriptMarker(host.shell)).toBeDefined();
} finally {
host.dispose();
}
Expand All @@ -173,6 +174,7 @@ describe("mountProductHost", () => {
text: `small-${total - 1}`,
});
expect(host.shell.streamLogBase).toBe(0);
expect(transcriptMarker(host.shell)).toBeUndefined();
} finally {
host.dispose();
}
Expand Down Expand Up @@ -200,12 +202,12 @@ describe("mountProductHost", () => {
},
];
emitter.emit("history.hydrate", blocks);
// Folding merges the pair inside hydration, so the row-level slice keeps
// and drops whole merged rows: 605 texts + 1 merged row → newest 600.
// A block-level slice would split the pair and paint 599 rows instead.
const expected = hydrateHistoryRows(blocks).slice(
-MAX_RETAINED_STREAM_ROWS,
);
// Folding merges the pair inside hydration, so retention evicts whole
// merged rows: 605 texts + 1 merged row → newest 600. A block-level
// slice would split the pair and paint 599 rows instead.
const allRows = hydrateHistoryRows(blocks);
const expected = allRows.slice(-MAX_RETAINED_STREAM_ROWS);
const dropped = allRows.length - expected.length;
expect(expected.length).toBe(MAX_RETAINED_STREAM_ROWS);
expect(host.shell.streamLog).toEqual(expected);
expect(host.shell.streamLog[0]).toEqual({
Expand All @@ -215,7 +217,9 @@ describe("mountProductHost", () => {
const last = host.shell.streamLog[host.shell.streamLog.length - 1];
expect(last?.pending).not.toBe(true);
expect(last?.text).toBe("done cut-1");
expect(host.shell.streamLogBase).toBe(0);
expect(dropped).toBeGreaterThan(0);
expect(host.shell.streamLogBase).toBe(dropped);
expect(transcriptMarker(host.shell)).toBeDefined();
} finally {
host.dispose();
}
Expand Down Expand Up @@ -246,7 +250,11 @@ describe("mountProductHost", () => {
);
expect(host.shell.parentStreamLog).toEqual(expected);
expect(host.shell.parentStreamLog?.length).toBe(MAX_RETAINED_STREAM_ROWS);
expect(host.shell.parentStreamLogBase).toBe(0);
expect(host.shell.parentStreamLogBase).toBe(
total - MAX_RETAINED_STREAM_ROWS,
);
expect(host.shell.streamLogBase).toBe(0);
expect(transcriptMarker(host.shell)).toBeUndefined();
} finally {
host.dispose();
}
Expand Down
5 changes: 1 addition & 4 deletions src/tui/product-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ import {
import { surfaceSystemNotice } from "./shell/prompt.js";
import type { DeliverySettle, QueueKind } from "./delivery-queue.js";
import { hydrateHistoryRows } from "./history-hydrate.js";
import { MAX_RETAINED_STREAM_ROWS } from "./long-log.js";
import type { StreamRow } from "./stream.js";

import type { PendingImageAttachment } from "./image-attachments.js";
Expand Down Expand Up @@ -542,9 +541,7 @@ export async function mountProductHost(

function onHistory(blocks: unknown): void {
if (disposed) return;
const rows = hydrateHistoryRows(blocks);
const tail = rows.slice(-MAX_RETAINED_STREAM_ROWS);
for (const row of tail) {
for (const row of hydrateHistoryRows(blocks)) {
appendStreamRow(shell, row);
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/tui/runner/wiring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,10 @@ export function wirePostStartup(
// Hydrate a resumed session's transcript after first paint. Reading history and
// mapping it to content blocks is pure I/O with no bearing on the shell, so the
// App renders empty immediately and fills in the past turns once they are ready.
// Only the tail needed to fill RESUME_TRANSCRIPT_BLOCK_LIMIT blocks is read from
// disk — a long session's full history is not needed just to paint a transcript
// that itself caps how much it displays.
// Only the retained transcript tail is read from disk — a long session's
// full history is not needed just to paint a display that itself caps how
// much it keeps. Agent conversation state still loads in full via
// ContextStore.load(); this path is display-only.
void loadRecentTurns(state.workdir, RESUME_TRANSCRIPT_BLOCK_LIMIT)
.then((turns) => {
const blocks = turnsToContentBlocks(turns, {
Expand Down
12 changes: 11 additions & 1 deletion src/tui/turns-to-blocks.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, test, expect } from "bun:test";
import type { ConversationTurn } from "@intx/types/runtime";
import { buildMailboxMailPrompt } from "../subagent/mailbox-mail-drive.js";
import { turnsToContentBlocks } from "./turns-to-blocks.js";
import {
RESUME_TRANSCRIPT_BLOCK_LIMIT,
turnsToContentBlocks,
} from "./turns-to-blocks.js";
import { MAX_RETAINED_STREAM_ROWS } from "./long-log.js";
import { hydrateTasksFromTurns } from "../agent/director.js";

function manageTasksTurn(
Expand Down Expand Up @@ -115,6 +119,12 @@ describe("turnsToContentBlocks marks occupancy wakes with system origin", () =>
});
});

describe("RESUME_TRANSCRIPT_BLOCK_LIMIT", () => {
test("matches the retained stream tail, not a larger pre-slice", () => {
expect(RESUME_TRANSCRIPT_BLOCK_LIMIT).toBe(MAX_RETAINED_STREAM_ROWS);
});
});

describe("hydrateTasksFromTurns", () => {
test("derives the task list from manage_tasks tool calls in a transcript", () => {
const turns = [manageTasksTurn("m1", "doing"), toolResultTurn("m1", false)];
Expand Down
5 changes: 4 additions & 1 deletion src/tui/turns-to-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
} from "@intx/types/runtime";

import { isPersistedOccupancyWakeText } from "../subagent/mailbox-mail-drive.js";
import { MAX_RETAINED_STREAM_ROWS } from "./long-log.js";
import { validateView } from "./view/validate.js";
import type { ViewNode } from "./view/spec.js";

Expand Down Expand Up @@ -209,7 +210,9 @@ function finalizeResumeToolBlocks(
return out;
}

export const RESUME_TRANSCRIPT_BLOCK_LIMIT = 2000;
// Resume paints into the same retained log live turns use, so the disk/block
// window is the retention cap rather than a larger pre-slice.
export const RESUME_TRANSCRIPT_BLOCK_LIMIT = MAX_RETAINED_STREAM_ROWS;

interface TurnsToContentBlocksOptions {
maxBlocks?: number;
Expand Down
Loading