refactor: remove draft agent routes and consolidate terminal output into session model

This commit is contained in:
Mohamed Boudra
2026-03-04 14:17:39 +07:00
parent b21764fd40
commit 92991ffdb8
25 changed files with 634 additions and 317 deletions
@@ -74,6 +74,14 @@ export class TerminalOutputDeliveryQueue {
consume(input: { sequence: number }): void {
if (this.inFlightChunk?.sequence !== input.sequence) {
terminalDebugLog({
scope: "output-delivery-queue",
event: "consume:ignored",
details: {
sequence: input.sequence,
inFlightSequence: this.inFlightChunk?.sequence ?? null,
},
});
return;
}
@@ -0,0 +1,97 @@
import { describe, expect, it } from "vitest";
import {
getTerminalOutputSession,
releaseTerminalOutputSession,
retainTerminalOutputSession,
} from "./terminal-output-session";
describe("terminal-output-session", () => {
async function flushAsyncWork(): Promise<void> {
await Promise.resolve();
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 0);
});
await Promise.resolve();
}
it("returns the same session instance for the same scope key", () => {
const a = getTerminalOutputSession({
scopeKey: "scope-a",
maxOutputChars: 1_000,
});
const b = getTerminalOutputSession({
scopeKey: "scope-a",
maxOutputChars: 50,
});
expect(a).toBe(b);
});
it("evicts a scope session when the retain count returns to zero", () => {
const scopeKey = "scope-retain-release";
const first = getTerminalOutputSession({
scopeKey,
maxOutputChars: 1_000,
});
retainTerminalOutputSession({ scopeKey });
releaseTerminalOutputSession({ scopeKey });
const second = getTerminalOutputSession({
scopeKey,
maxOutputChars: 1_000,
});
expect(second).not.toBe(first);
});
it("publishes selected terminal snapshot and chunk state via delivery/consume", async () => {
const session = getTerminalOutputSession({
scopeKey: "scope-session-state",
maxOutputChars: 1_000,
});
session.setSelectedTerminal({ terminalId: null });
session.setSelectedTerminal({ terminalId: "term-1" });
session.append({
terminalId: "term-1",
text: "abc",
replay: false,
});
await flushAsyncWork();
const stateAfterAppend = session.getState();
expect(stateAfterAppend.selectedTerminalId).toBe("term-1");
expect(stateAfterAppend.chunkSequence).toBeGreaterThan(0);
expect(stateAfterAppend.chunkText).toBe("abc");
expect(stateAfterAppend.snapshotText).toBe("abc");
expect(stateAfterAppend.snapshotSequence).toBe(stateAfterAppend.chunkSequence);
session.consume({ sequence: stateAfterAppend.chunkSequence });
const stateAfterConsume = session.getState();
expect(stateAfterConsume.snapshotText).toBe("abc");
});
it("keeps chunks for non-selected terminals out of selected state", async () => {
const session = getTerminalOutputSession({
scopeKey: "scope-other-terminal",
maxOutputChars: 1_000,
});
session.setSelectedTerminal({ terminalId: "term-1" });
session.append({
terminalId: "term-2",
text: "hidden",
replay: false,
});
await flushAsyncWork();
const state = session.getState();
expect(state.selectedTerminalId).toBe("term-1");
expect(state.chunkText).toBe("");
expect(state.snapshotText).toBe("");
});
});
@@ -0,0 +1,215 @@
import {
TerminalOutputPump,
type TerminalOutputChunk,
} from "./terminal-output-pump";
import {
TerminalOutputDeliveryQueue,
type TerminalOutputDeliveryChunk,
} from "./terminal-output-delivery-queue";
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
type TerminalOutputState = {
selectedTerminalId: string | null;
snapshotText: string;
snapshotSequence: number;
chunkText: string;
chunkSequence: number;
chunkReplay: boolean;
};
const EMPTY_STATE: TerminalOutputState = {
selectedTerminalId: null,
snapshotText: "",
snapshotSequence: 0,
chunkText: "",
chunkSequence: 0,
chunkReplay: false,
};
export type TerminalOutputSessionAppendInput = {
terminalId: string;
text: string;
replay: boolean;
};
export type TerminalOutputSessionSetSelectedInput = {
terminalId: string | null;
};
export type TerminalOutputSessionPruneInput = {
terminalIds: string[];
};
export type TerminalOutputSessionConsumeInput = {
sequence: number;
};
export class TerminalOutputSession {
private readonly listeners = new Set<() => void>();
private readonly outputPump: TerminalOutputPump;
private readonly deliveryQueue: TerminalOutputDeliveryQueue;
private state: TerminalOutputState = EMPTY_STATE;
constructor(input: { maxOutputChars: number }) {
this.outputPump = new TerminalOutputPump({
maxOutputChars: input.maxOutputChars,
onSelectedOutputChunk: (chunk: TerminalOutputChunk) => {
this.deliveryQueue.enqueue(chunk);
},
});
this.deliveryQueue = new TerminalOutputDeliveryQueue({
onDeliver: (chunk: TerminalOutputDeliveryChunk) => {
const snapshotText = this.outputPump.readSnapshot({
terminalId: this.state.selectedTerminalId,
});
this.state = {
...this.state,
snapshotText,
snapshotSequence: chunk.sequence,
chunkText: chunk.text,
chunkSequence: chunk.sequence,
chunkReplay: chunk.replay,
};
terminalDebugLog({
scope: "output-session",
event: "deliver",
details: {
selectedTerminalId: this.state.selectedTerminalId,
sequence: chunk.sequence,
replay: chunk.replay,
chunkLength: chunk.text.length,
snapshotLength: snapshotText.length,
preview: summarizeTerminalText({ text: chunk.text, maxChars: 80 }),
},
});
this.emit();
},
});
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
getState(): TerminalOutputState {
return this.state;
}
setSelectedTerminal(input: TerminalOutputSessionSetSelectedInput): void {
if (this.state.selectedTerminalId === input.terminalId) {
return;
}
this.deliveryQueue.reset();
this.outputPump.setSelectedTerminal({ terminalId: input.terminalId });
const snapshotText = this.outputPump.readSnapshot({
terminalId: input.terminalId,
});
this.state = {
selectedTerminalId: input.terminalId,
snapshotText,
snapshotSequence: this.state.snapshotSequence,
chunkText: "",
chunkSequence: 0,
chunkReplay: false,
};
terminalDebugLog({
scope: "output-session",
event: "selected-terminal:set",
details: {
selectedTerminalId: input.terminalId,
snapshotLength: snapshotText.length,
},
});
this.emit();
}
append(input: TerminalOutputSessionAppendInput): void {
this.outputPump.append(input);
}
clearTerminal(input: { terminalId: string }): void {
this.outputPump.clearTerminal(input);
if (this.state.selectedTerminalId !== input.terminalId) {
return;
}
this.deliveryQueue.reset();
this.state = {
...this.state,
snapshotText: "",
chunkText: "",
chunkSequence: 0,
chunkReplay: false,
};
terminalDebugLog({
scope: "output-session",
event: "selected-terminal:clear",
details: {
terminalId: input.terminalId,
},
});
this.emit();
}
prune(input: TerminalOutputSessionPruneInput): void {
this.outputPump.prune(input);
}
consume(input: TerminalOutputSessionConsumeInput): void {
this.deliveryQueue.consume(input);
}
private emit(): void {
for (const listener of this.listeners) {
listener();
}
}
}
const sessionsByScopeKey = new Map<string, TerminalOutputSession>();
const sessionRefCountByScopeKey = new Map<string, number>();
export function getTerminalOutputSession(input: {
scopeKey: string;
maxOutputChars: number;
}): TerminalOutputSession {
const existing = sessionsByScopeKey.get(input.scopeKey);
if (existing) {
return existing;
}
const session = new TerminalOutputSession({
maxOutputChars: input.maxOutputChars,
});
sessionsByScopeKey.set(input.scopeKey, session);
return session;
}
export function retainTerminalOutputSession(input: { scopeKey: string }): void {
const current = sessionRefCountByScopeKey.get(input.scopeKey) ?? 0;
sessionRefCountByScopeKey.set(input.scopeKey, current + 1);
}
export function releaseTerminalOutputSession(input: { scopeKey: string }): void {
const current = sessionRefCountByScopeKey.get(input.scopeKey) ?? 0;
if (current <= 1) {
sessionRefCountByScopeKey.delete(input.scopeKey);
sessionsByScopeKey.delete(input.scopeKey);
terminalDebugLog({
scope: "output-session",
event: "scope:release",
details: {
scopeKey: input.scopeKey,
released: true,
},
});
return;
}
sessionRefCountByScopeKey.set(input.scopeKey, current - 1);
}