Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
export interface ToolResultDedupEntry {
|
|
fingerprint: string;
|
|
toolName: string;
|
|
turnNumber: number;
|
|
preview: string;
|
|
}
|
|
|
|
export class ToolResultDedup {
|
|
private turnNumber = 0;
|
|
private readonly cache = new Map<string, ToolResultDedupEntry>();
|
|
private readonly consumedBudgets = new Map<string, number>();
|
|
private readonly completedWrites = new Map<string, unknown[]>();
|
|
private replayableWrites = new Map<string, unknown[]>();
|
|
private readonly terminalJobSessions = new Map<string, string>();
|
|
private writeReplayEnabled = false;
|
|
|
|
beginTurn(): void {
|
|
this.turnNumber += 1;
|
|
this.consumedBudgets.clear();
|
|
this.completedWrites.clear();
|
|
this.replayableWrites.clear();
|
|
this.writeReplayEnabled = false;
|
|
}
|
|
|
|
reset(): void {
|
|
this.cache.clear();
|
|
this.turnNumber = 0;
|
|
this.consumedBudgets.clear();
|
|
this.completedWrites.clear();
|
|
this.replayableWrites.clear();
|
|
this.terminalJobSessions.clear();
|
|
this.writeReplayEnabled = false;
|
|
}
|
|
|
|
rememberCompletedWrite(fingerprint: string, result: unknown): void {
|
|
const results = this.completedWrites.get(fingerprint) ?? [];
|
|
results.push(result);
|
|
this.completedWrites.set(fingerprint, results);
|
|
}
|
|
|
|
rememberTerminalJobSession(jobId: string, sessionId: string): void {
|
|
this.terminalJobSessions.set(jobId, sessionId);
|
|
}
|
|
|
|
terminalSessionForJob(jobId: string): string | undefined {
|
|
return this.terminalJobSessions.get(jobId);
|
|
}
|
|
|
|
enableWriteReplay(preservedFingerprints: Iterable<string> = []): void {
|
|
this.replayableWrites = new Map(
|
|
Array.from(this.completedWrites, ([fingerprint, results]) => [fingerprint, [...results]]),
|
|
);
|
|
this.writeReplayEnabled = true;
|
|
for (const fingerprint of preservedFingerprints) {
|
|
this.consumeReplay(fingerprint);
|
|
}
|
|
}
|
|
|
|
replayCompletedWrite(fingerprint: string): unknown | undefined {
|
|
if (!this.writeReplayEnabled) return undefined;
|
|
return this.consumeReplay(fingerprint);
|
|
}
|
|
|
|
private consumeReplay(fingerprint: string): unknown | undefined {
|
|
const results = this.replayableWrites.get(fingerprint);
|
|
const result = results?.shift();
|
|
if (results?.length === 0) this.replayableWrites.delete(fingerprint);
|
|
return result;
|
|
}
|
|
|
|
takeBudget(key: string, requested: number, limit: number): number {
|
|
const consumed = this.consumedBudgets.get(key) ?? 0;
|
|
const granted = Math.max(0, Math.min(requested, limit - consumed));
|
|
this.consumedBudgets.set(key, consumed + granted);
|
|
return granted;
|
|
}
|
|
|
|
fingerprintFor(toolName: string, key: string): string {
|
|
return `${toolName}:${key}`;
|
|
}
|
|
|
|
check(fingerprint: string): ToolResultDedupEntry | undefined {
|
|
return this.cache.get(fingerprint);
|
|
}
|
|
|
|
remember(toolName: string, fingerprint: string, preview: string): void {
|
|
this.cache.set(fingerprint, {
|
|
fingerprint,
|
|
toolName,
|
|
turnNumber: this.turnNumber,
|
|
preview: preview.slice(0, 160),
|
|
});
|
|
}
|
|
|
|
buildCachedNotice(entry: ToolResultDedupEntry): string {
|
|
return `[cached] same as turn ${entry.turnNumber} for ${entry.toolName}`;
|
|
}
|
|
}
|
|
|
|
export function hashScopeKey(parts: Array<string | undefined>): string {
|
|
return parts.filter(Boolean).join('|');
|
|
}
|
|
|
|
export function buildTerminalWriteFingerprint(
|
|
toolName: 'terminal_execute' | 'terminal_start',
|
|
chatSessionId: string | undefined,
|
|
args: { sessionId?: unknown; command?: unknown },
|
|
): string | undefined {
|
|
if (typeof args.sessionId !== 'string' || typeof args.command !== 'string') return undefined;
|
|
const fingerprintToolName = toolName === 'terminal_start' ? 'terminal.start:write' : toolName;
|
|
return `${fingerprintToolName}:${hashScopeKey([chatSessionId, args.sessionId, args.command])}`;
|
|
}
|
|
|
|
export function previewToolResult(result: unknown): string {
|
|
if (typeof result === 'string') return result.slice(0, 160);
|
|
try {
|
|
return JSON.stringify(result).slice(0, 160);
|
|
} catch {
|
|
return String(result).slice(0, 160);
|
|
}
|
|
}
|
|
|
|
export function hashToolResult(result: unknown): string {
|
|
let value: string;
|
|
try {
|
|
value = typeof result === 'string' ? result : JSON.stringify(result);
|
|
} catch {
|
|
value = String(result);
|
|
}
|
|
let hash = 0x811c9dc5;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
hash ^= value.charCodeAt(index);
|
|
hash = Math.imul(hash, 0x01000193);
|
|
}
|
|
return (hash >>> 0).toString(36);
|
|
}
|