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
18 changes: 18 additions & 0 deletions changes/unreleased/4485.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- changes-log: packages/coding-agent/src/modes/rpc/changes.md -->
## 2026-10-27 - Settled terminals on the paged compact index (part of #4272)

### What changed

- `packages/coding-agent/src/modes/rpc/compact-settled-messages.ts`: keep S05's exact settled membership in a resident key set instead of the removed `has()` lookup on the compact index; `session-acceptance-lane.ts` reads the settled root through the paged index shape.

### Why

- #4374 and #4398 merged without a textual conflict, but together main failed to typecheck.

### Why an extension could not handle it

- Internal compact recovery state below extension delivery.

### Expected merge conflict zones

- `compact-settled-messages.ts` membership checks. Rollout remains off.
30 changes: 19 additions & 11 deletions packages/coding-agent/src/modes/rpc/compact-settled-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ export interface OwnerRow {
}

type Row = TerminalRow | OwnerRow | { readonly kind: "ended" };
export type SettledRoot = FrozenIndex<Row>;
export type SettledRoot = FrozenIndex;

/** One spillable collection owns payloads, end markers and both directions of the durable join. */
export class CompactSettledMessages {
private readonly store: CompactContentStore;
private readonly rows: CompactContentIndex<Row>;
/** Resident membership of the paged rows, the synchronous check acceptance needs (pruning: carried item 9). */
private readonly keys = new Set<string>();
private readonly epoch: string;
private readonly diagnose: (reason: SettledDiagnostic) => void;

Expand All @@ -58,13 +60,13 @@ export class CompactSettledMessages {
end(record: object, carrier: RecoveryCarrier | undefined, sequence: number, turnId: string | null): void {
if (!this.valid(carrier)) return this.diagnose("unknown_message");
const id = carrier.messageId;
if (this.rows.has(`end:${id}`)) return this.diagnose("duplicate_end");
if (carrier.durableEntryId && !this.rows.has(`owner:${id}`)) {
if (this.rows.has(`entry:${carrier.durableEntryId}`)) return this.diagnose("conflicting_owner");
if (this.keys.has(`end:${id}`)) return this.diagnose("duplicate_end");
if (carrier.durableEntryId && !this.keys.has(`owner:${id}`)) {
if (this.keys.has(`entry:${carrier.durableEntryId}`)) return this.diagnose("conflicting_owner");
this.join(id, carrier.durableEntryId);
}
this.rows.set(`end:${id}`, { kind: "ended" });
if (this.rows.has(`owner:${id}`)) return;
this.put(`end:${id}`, { kind: "ended" });
if (this.keys.has(`owner:${id}`)) return;
const message: unknown = Reflect.get(record, "message");
const role = String(Reflect.get(Object(message), "role"));
const payload: LeafId[] = [];
Expand All @@ -78,7 +80,7 @@ export class CompactSettledMessages {
});
for (let offset = 0; offset < text.length; offset += COMPACT_LEAF_CODE_UNITS)
payload.push(this.store.createLeaf(text.slice(offset, offset + COMPACT_LEAF_CODE_UNITS)));
this.rows.set(`terminal:${id}`, {
this.put(`terminal:${id}`, {
kind: "terminal",
messageId: id,
turnId,
Expand All @@ -95,7 +97,7 @@ export class CompactSettledMessages {
}

fail(carrier: RecoveryCarrier | undefined, errorMessage: string): void {
if (!this.valid(carrier) || !this.rows.has(`terminal:${carrier.messageId}`))
if (!this.valid(carrier) || !this.keys.has(`terminal:${carrier.messageId}`))
return this.diagnose("unknown_message");
this.rows.patch(`terminal:${carrier.messageId}`, { status: "failed", error: { errorMessage } });
}
Expand All @@ -104,16 +106,22 @@ export class CompactSettledMessages {
const id = transition.owningMessageId;
if (id === undefined) return;
if (transition.recoveryEpoch !== this.epoch) return this.diagnose("retired_owner");
if (this.rows.has(`owner:${id}`) || this.rows.has(`entry:${transition.id}`))
if (this.keys.has(`owner:${id}`) || this.keys.has(`entry:${transition.id}`))
return this.diagnose("conflicting_owner");
this.join(id, transition.id);
}

private join(messageId: string, entryId: string): void {
const owner: OwnerRow = { kind: "owner", messageId, entryId };
this.rows.set(`owner:${messageId}`, owner);
this.rows.set(`entry:${entryId}`, owner);
this.put(`owner:${messageId}`, owner);
this.put(`entry:${entryId}`, owner);
this.rows.delete(`terminal:${messageId}`);
this.keys.delete(`terminal:${messageId}`);
}

private put(key: string, row: Row): void {
this.rows.set(key, row);
this.keys.add(key);
}

freeze(): SettledRoot {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface SessionBootstrap {
readonly epochSignal?: SharedArrayBuffer;
}

const RETIRED_SETTLED: SettledRoot = Object.freeze({ base: undefined, overrides: undefined });

/** Recovery kinds take a sequence; responses and every other record keep FIFO order with none. */
const SEQUENCED = new Set([
"agent_start",
Expand Down Expand Up @@ -174,8 +176,10 @@ export class SessionAcceptanceLane {
turnId: this.turnId,
correlation: this.correlation,
transcript: this.transcript.freeze(),
settled: this.settled.freeze(),
// A retired lane's paged settled store refuses new cuts; its root is never live or readable.
settled: this.retired ? RETIRED_SETTLED : this.settled.freeze(),
});
if (this.retired) return root;
this.rootOwners.set(root, this.settled);
this.live.add(root);
return root;
Expand Down
Loading