From 4f4691676acad990c6a51221e2522d4ffb131415 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 21 Feb 2026 19:04:59 -0800 Subject: [PATCH] Persist turn diffs and checkpoint reverts in orchestration - map `checkpoint/captured` to `thread.turnDiff.complete` and record turn status/id - handle `thread.revert` to truncate messages and turn summaries in read model - migrate event store from `better-sqlite3` to `node:sqlite` and add safe close/idempotent stop tests --- apps/server/package.json | 2 - apps/server/src/orchestration/engine.test.ts | 194 +++++++++++++++++++ apps/server/src/orchestration/engine.ts | 56 +++++- apps/server/src/orchestration/eventStore.ts | 86 ++++++-- apps/server/src/orchestration/reducer.ts | 105 ++++++++++ apps/server/src/providerManager.test.ts | 15 +- apps/server/src/providerManager.ts | 34 +++- apps/server/src/wsServer.ts | 70 ++++++- bun.lock | 66 ------- packages/contracts/src/orchestration.ts | 24 +++ 10 files changed, 553 insertions(+), 99 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index e8e6a978eb17293dbb64c8281577a5a527958752..5571a4a7cb488b8d10c8399266f0c84cfa77475f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -17,7 +17,6 @@ "test": "vitest run" }, "dependencies": { - "better-sqlite3": "^12.6.2", "effect": "^3.19.19", "node-pty": "^1.1.0", "open": "^10.1.0", @@ -26,7 +25,6 @@ "devDependencies": { "@t3tools/contracts": "workspace:*", "@t3tools/web": "workspace:*", - "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.2", "@types/ws": "^8.5.13", "tsdown": "^0.20.3", diff --git a/apps/server/src/orchestration/engine.test.ts b/apps/server/src/orchestration/engine.test.ts index 527ff57f6aeedc1e3fcc77406c9ac3213594f89e..d4a0123169c3665ba1d1ee69482473aed6e88420 100644 --- a/apps/server/src/orchestration/engine.test.ts +++ b/apps/server/src/orchestration/engine.test.ts @@ -116,4 +116,198 @@ describe("OrchestrationEngine", () => { expect(events[1]?.type).toBe("project.deleted"); await engine.stop(); }); + + it("stores completed turn summaries even when no files changed", async () => { + const stateDir = makeTempDir("t3code-orchestration-turn-diff-"); + const engine = new OrchestrationEngine(stateDir); + const completedAt = new Date().toISOString(); + await engine.start(); + await engine.dispatch({ + type: "project.create", + commandId: "cmd-project-turn-diff", + projectId: "project-turn-diff", + name: "turn-diff", + cwd: "/tmp/turn-diff", + model: "gpt-5-codex", + createdAt: completedAt, + }); + await engine.dispatch({ + type: "thread.create", + commandId: "cmd-thread-turn-diff", + threadId: "thread-turn-diff", + projectId: "project-turn-diff", + title: "Turn diff thread", + model: "gpt-5-codex", + branch: null, + worktreePath: null, + createdAt: completedAt, + }); + await engine.dispatch({ + type: "thread.turnDiff.complete", + commandId: "cmd-turn-diff-complete", + threadId: "thread-turn-diff", + turnId: "turn-1", + completedAt, + status: "completed", + files: [], + checkpointTurnCount: 1, + createdAt: completedAt, + }); + + const firstThread = engine.getSnapshot().threads.find((thread) => thread.id === "thread-turn-diff"); + expect(firstThread?.turnDiffSummaries).toEqual([ + { + turnId: "turn-1", + completedAt, + status: "completed", + files: [], + checkpointTurnCount: 1, + }, + ]); + await engine.stop(); + + const restartedEngine = new OrchestrationEngine(stateDir); + await restartedEngine.start(); + const restartedThread = restartedEngine + .getSnapshot() + .threads.find((thread) => thread.id === "thread-turn-diff"); + expect(restartedThread?.turnDiffSummaries).toEqual([ + { + turnId: "turn-1", + completedAt, + status: "completed", + files: [], + checkpointTurnCount: 1, + }, + ]); + await restartedEngine.stop(); + }); + + it("reverts thread messages and turn summaries to a checkpoint", async () => { + const stateDir = makeTempDir("t3code-orchestration-revert-"); + const engine = new OrchestrationEngine(stateDir); + const createdAt = new Date().toISOString(); + await engine.start(); + await engine.dispatch({ + type: "project.create", + commandId: "cmd-project-revert", + projectId: "project-revert", + name: "revert", + cwd: "/tmp/revert", + model: "gpt-5-codex", + createdAt, + }); + await engine.dispatch({ + type: "thread.create", + commandId: "cmd-thread-revert", + threadId: "thread-revert", + projectId: "project-revert", + title: "Revert thread", + model: "gpt-5-codex", + branch: null, + worktreePath: null, + createdAt, + }); + await engine.dispatch({ + type: "message.send", + commandId: "cmd-msg-1-user", + threadId: "thread-revert", + messageId: "user-1", + role: "user", + text: "first", + streaming: false, + createdAt, + }); + await engine.dispatch({ + type: "message.send", + commandId: "cmd-msg-1-assistant", + threadId: "thread-revert", + messageId: "assistant:turn-1", + role: "assistant", + text: "first-response", + streaming: false, + createdAt, + }); + await engine.dispatch({ + type: "thread.turnDiff.complete", + commandId: "cmd-turn-1-complete", + threadId: "thread-revert", + turnId: "turn-1", + completedAt: createdAt, + status: "completed", + files: [], + assistantMessageId: "assistant:turn-1", + checkpointTurnCount: 1, + createdAt, + }); + + const createdAtSecond = new Date(Date.now() + 1_000).toISOString(); + await engine.dispatch({ + type: "message.send", + commandId: "cmd-msg-2-user", + threadId: "thread-revert", + messageId: "user-2", + role: "user", + text: "second", + streaming: false, + createdAt: createdAtSecond, + }); + await engine.dispatch({ + type: "message.send", + commandId: "cmd-msg-2-assistant", + threadId: "thread-revert", + messageId: "assistant:turn-2", + role: "assistant", + text: "second-response", + streaming: false, + createdAt: createdAtSecond, + }); + await engine.dispatch({ + type: "thread.turnDiff.complete", + commandId: "cmd-turn-2-complete", + threadId: "thread-revert", + turnId: "turn-2", + completedAt: createdAtSecond, + status: "completed", + files: [], + assistantMessageId: "assistant:turn-2", + checkpointTurnCount: 2, + createdAt: createdAtSecond, + }); + + await engine.dispatch({ + type: "thread.revert", + commandId: "cmd-thread-revert-apply", + threadId: "thread-revert", + turnCount: 1, + messageCount: 2, + createdAt: createdAtSecond, + }); + + const thread = engine.getSnapshot().threads.find((entry) => entry.id === "thread-revert"); + expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]); + expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]); + expect(thread?.latestTurnId).toBe("turn-1"); + await engine.stop(); + + const restarted = new OrchestrationEngine(stateDir); + await restarted.start(); + const restartedThread = restarted + .getSnapshot() + .threads.find((entry) => entry.id === "thread-revert"); + expect(restartedThread?.messages.map((message) => message.id)).toEqual([ + "user-1", + "assistant:turn-1", + ]); + expect(restartedThread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]); + await restarted.stop(); + }); + + it("allows stop to be called multiple times", async () => { + const stateDir = makeTempDir("t3code-orchestration-stop-"); + const engine = new OrchestrationEngine(stateDir); + await engine.start(); + await engine.stop(); + await expect(engine.stop()).resolves.toBeUndefined(); + }); }); diff --git a/apps/server/src/orchestration/engine.ts b/apps/server/src/orchestration/engine.ts index 5b93c507a3f944a0f55dad7826b028181d29664d..28ef5e976e45741f193047d5eaaf2bbdebebf52b 100644 --- a/apps/server/src/orchestration/engine.ts +++ b/apps/server/src/orchestration/engine.ts @@ -148,6 +148,42 @@ function mapCommandToEvent(command: OrchestrationCommand): Omit()); - this.readModelPubSub = Runtime.runSync(this.runtime)(PubSub.unbounded()); + this.readModelPubSub = Runtime.runSync(this.runtime)( + PubSub.unbounded(), + ); this.eventPubSub = Runtime.runSync(this.runtime)(PubSub.unbounded()); } @@ -184,11 +222,15 @@ export class OrchestrationEngine { Effect.tryPromise({ try: async () => { const eventBase = mapCommandToEvent(envelope.command); - const savedEvent = await Runtime.runPromise(this.runtime)(this.eventStore.append(eventBase)); + const savedEvent = await Runtime.runPromise(this.runtime)( + this.eventStore.append(eventBase), + ); this.readModel = reduceEvent(this.readModel, savedEvent); await Promise.all([ Runtime.runPromise(this.runtime)(PubSub.publish(this.eventPubSub, savedEvent)), - Runtime.runPromise(this.runtime)(PubSub.publish(this.readModelPubSub, this.readModel)), + Runtime.runPromise(this.runtime)( + PubSub.publish(this.readModelPubSub, this.readModel), + ), ]); for (const listener of this.domainEventListeners) { listener(savedEvent); @@ -199,7 +241,8 @@ export class OrchestrationEngine { envelope.resolve({ sequence: savedEvent.sequence }); }, catch: (error) => { - const message = error instanceof Error ? error.message : "Unknown command processing error"; + const message = + error instanceof Error ? error.message : "Unknown command processing error"; envelope.reject(new Error(message)); return undefined; }, @@ -219,6 +262,7 @@ export class OrchestrationEngine { await Runtime.runPromise(this.runtime)(Fiber.interrupt(this.workerFiber)); this.workerFiber = null; } + this.eventStore.close(); } getSnapshot(): OrchestrationReadModel { @@ -226,7 +270,9 @@ export class OrchestrationEngine { } async replayEvents(fromSequenceExclusive: number): Promise { - return Runtime.runPromise(this.runtime)(this.eventStore.readFromSequence(fromSequenceExclusive)); + return Runtime.runPromise(this.runtime)( + this.eventStore.readFromSequence(fromSequenceExclusive), + ); } async dispatchUnknown(command: unknown): Promise<{ sequence: number }> { diff --git a/apps/server/src/orchestration/eventStore.ts b/apps/server/src/orchestration/eventStore.ts index bc54c9767d68107dde454f52cdc817ca64bf4694..636879785c1f258d7b0dbf04829b80376f090620 100644 --- a/apps/server/src/orchestration/eventStore.ts +++ b/apps/server/src/orchestration/eventStore.ts @@ -1,8 +1,9 @@ import fs from "node:fs"; import path from "node:path"; +import { DatabaseSync, StatementSync } from "node:sqlite"; +import type { SQLOutputValue } from "node:sqlite"; import type { OrchestrationEvent } from "@t3tools/contracts"; -import Database from "better-sqlite3"; import { Effect } from "effect"; interface EventRow { @@ -16,15 +17,56 @@ interface EventRow { payload_json: string; } +function readRequiredString( + row: Record, + column: keyof EventRow, +): string { + const value = row[column]; + if (typeof value !== "string") { + throw new Error(`Invalid SQLite value for ${String(column)}: expected string`); + } + return value; +} + +function readOptionalString( + row: Record, + column: keyof EventRow, +): string | null { + const value = row[column]; + if (value === null) return null; + if (typeof value !== "string") { + throw new Error(`Invalid SQLite value for ${String(column)}: expected string or null`); + } + return value; +} + +function readRequiredNumber( + row: Record, + column: keyof EventRow, +): number { + const value = row[column]; + if (typeof value === "number") return value; + if (typeof value === "bigint") return Number(value); + throw new Error(`Invalid SQLite value for ${String(column)}: expected number`); +} + export class SqliteEventStore { - private readonly db: Database.Database; + private readonly db: DatabaseSync; + private readonly insertStatement: StatementSync; + private closed = false; constructor(dbPath: string) { fs.mkdirSync(path.dirname(dbPath), { recursive: true }); - this.db = new Database(dbPath); - this.db.pragma("journal_mode = WAL"); - this.db.pragma("foreign_keys = ON"); + this.db = new DatabaseSync(dbPath); + this.db.exec("PRAGMA journal_mode = WAL;"); + this.db.exec("PRAGMA foreign_keys = ON;"); this.migrate(); + this.insertStatement = this.db.prepare(` + INSERT INTO orchestration_events ( + event_id, event_type, aggregate_type, aggregate_id, occurred_at, command_id, payload_json + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); } private migrate(): void { @@ -47,13 +89,7 @@ export class SqliteEventStore { append(event: Omit): Effect.Effect { return Effect.sync(() => { - const stmt = this.db.prepare(` - INSERT INTO orchestration_events ( - event_id, event_type, aggregate_type, aggregate_id, occurred_at, command_id, payload_json - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - `); - const result = stmt.run( + const result = this.insertStatement.run( event.eventId, event.type, event.aggregateType, @@ -82,16 +118,16 @@ export class SqliteEventStore { LIMIT ? `, ) - .all(sequenceExclusive, limit) as EventRow[]; + .all(sequenceExclusive, limit); return rows.map((row) => ({ - sequence: row.sequence, - eventId: row.event_id, - type: row.event_type, - aggregateType: row.aggregate_type, - aggregateId: row.aggregate_id, - occurredAt: row.occurred_at, - commandId: row.command_id, - payload: JSON.parse(row.payload_json) as unknown, + sequence: readRequiredNumber(row, "sequence"), + eventId: readRequiredString(row, "event_id"), + type: readRequiredString(row, "event_type"), + aggregateType: readRequiredString(row, "aggregate_type"), + aggregateId: readRequiredString(row, "aggregate_id"), + occurredAt: readRequiredString(row, "occurred_at"), + commandId: readOptionalString(row, "command_id"), + payload: JSON.parse(readRequiredString(row, "payload_json")) as unknown, })); }); } @@ -99,4 +135,12 @@ export class SqliteEventStore { readAll(): Effect.Effect { return this.readFromSequence(0, Number.MAX_SAFE_INTEGER); } + + close(): void { + if (this.closed) { + return; + } + this.closed = true; + this.db.close(); + } } diff --git a/apps/server/src/orchestration/reducer.ts b/apps/server/src/orchestration/reducer.ts index 0aafdc2646bc3c0e36e3890418d95452f7abc940..14e2c237fb86c6ca1eb497aa10ef7e63c41c5335 100644 --- a/apps/server/src/orchestration/reducer.ts +++ b/apps/server/src/orchestration/reducer.ts @@ -31,6 +31,10 @@ function asNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } +function asArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : []; +} + export function createEmptyReadModel(nowIso: string): OrchestrationReadModel { return { sequence: 0, @@ -192,6 +196,107 @@ export function reduceEvent( }), }; } + case "thread.turn-diff-completed": { + if (!payload) return nextBase; + const threadId = asString(payload.threadId) ?? event.aggregateId; + const thread = nextBase.threads.find((entry) => entry.id === threadId); + const turnId = asString(payload.turnId); + if (!thread || !turnId) return nextBase; + + const files = asArray(payload.files).flatMap((filePayload) => { + const file = asObject(filePayload); + const filePath = asString(file?.path); + if (!filePath) { + return []; + } + + const nextFile: { + path: string; + kind?: string; + additions?: number; + deletions?: number; + } = { path: filePath }; + const kind = asString(file?.kind); + if (kind !== null) { + nextFile.kind = kind; + } + const additions = asNumber(file?.additions); + if (additions !== null) { + nextFile.additions = additions; + } + const deletions = asNumber(file?.deletions); + if (deletions !== null) { + nextFile.deletions = deletions; + } + return [nextFile]; + }); + + const nextSummary = { + turnId, + completedAt: asString(payload.completedAt) ?? event.occurredAt, + ...(asString(payload.status) !== null ? { status: asString(payload.status) ?? undefined } : {}), + files, + ...(asString(payload.assistantMessageId) !== null + ? { assistantMessageId: asString(payload.assistantMessageId) ?? undefined } + : {}), + ...(asNumber(payload.checkpointTurnCount) !== null + ? { checkpointTurnCount: asNumber(payload.checkpointTurnCount) ?? undefined } + : {}), + }; + const turnDiffSummaries = [...thread.turnDiffSummaries.filter((summary) => summary.turnId !== turnId), nextSummary] + .toSorted((left, right) => left.completedAt.localeCompare(right.completedAt)); + + return { + ...nextBase, + threads: updateThread(nextBase.threads, threadId, { + latestTurnId: turnId, + latestTurnCompletedAt: nextSummary.completedAt, + turnDiffSummaries, + updatedAt: event.occurredAt, + }), + }; + } + case "thread.reverted": { + if (!payload) return nextBase; + const threadId = asString(payload.threadId) ?? event.aggregateId; + const thread = nextBase.threads.find((entry) => entry.id === threadId); + if (!thread) return nextBase; + + const targetTurnCount = Math.max(0, Math.floor(asNumber(payload.turnCount) ?? 0)); + const targetMessageCount = Math.max(0, Math.floor(asNumber(payload.messageCount) ?? 0)); + const sortedSummaries = [...thread.turnDiffSummaries].toSorted((left, right) => + left.completedAt.localeCompare(right.completedAt), + ); + const inferredTurnCountByTurnId = new Map(); + for (let index = 0; index < sortedSummaries.length; index += 1) { + const summary = sortedSummaries[index]; + if (!summary) continue; + inferredTurnCountByTurnId.set(summary.turnId, index + 1); + } + + const turnDiffSummaries = thread.turnDiffSummaries + .filter((summary) => { + const checkpointTurnCount = + summary.checkpointTurnCount ?? inferredTurnCountByTurnId.get(summary.turnId) ?? 0; + return checkpointTurnCount <= targetTurnCount; + }) + .toSorted((left, right) => left.completedAt.localeCompare(right.completedAt)); + const latestSummary = + turnDiffSummaries.length > 0 ? turnDiffSummaries[turnDiffSummaries.length - 1] : null; + + return { + ...nextBase, + threads: updateThread(nextBase.threads, threadId, { + messages: thread.messages.slice(0, Math.min(targetMessageCount, thread.messages.length)), + turnDiffSummaries, + latestTurnId: latestSummary?.turnId ?? null, + latestTurnStartedAt: null, + latestTurnCompletedAt: latestSummary?.completedAt ?? null, + latestTurnDurationMs: null, + updatedAt: event.occurredAt, + }), + }; + } case "git.read-model-upsert": { if (!payload) return nextBase; const projectId = asString(payload.projectId) ?? event.aggregateId; diff --git a/apps/server/src/providerManager.test.ts b/apps/server/src/providerManager.test.ts index c26f34cb6ae55ba5c536dba0a86bd866971b57da..08281bdee200323a2727085f68089e60208c3fe3 100644 --- a/apps/server/src/providerManager.test.ts +++ b/apps/server/src/providerManager.test.ts @@ -298,12 +298,14 @@ describe("ProviderManager", () => { it("emits checkpoint/captured after turn completion checkpoint capture", async () => { const manager = new ProviderManager(); - const events: Array<{ method: string; payload?: unknown; threadId?: string }> = []; + const events: Array<{ method: string; payload?: unknown; threadId?: string; turnId?: string }> = + []; manager.on("event", (event) => { events.push({ method: event.method, ...(event.payload !== undefined ? { payload: event.payload } : {}), ...(event.threadId ? { threadId: event.threadId } : {}), + ...(event.turnId ? { turnId: event.turnId } : {}), }); }); @@ -316,6 +318,8 @@ describe("ProviderManager", () => { createdAt: string; method: string; threadId: string; + turnId?: string; + payload?: unknown; }) => void; codex: { hasSession: (sessionId: string) => boolean; @@ -351,6 +355,12 @@ describe("ProviderManager", () => { createdAt: "2026-02-19T00:00:00.000Z", method: "turn/completed", threadId: "thr_1", + turnId: "turn_1", + payload: { + turn: { + status: "completed", + }, + }, }); await vi.waitFor(() => { @@ -364,9 +374,12 @@ describe("ProviderManager", () => { const checkpointCapturedEvent = events.find((event) => event.method === "checkpoint/captured"); expect(checkpointCapturedEvent?.threadId).toBe("thr_1"); + expect(checkpointCapturedEvent?.turnId).toBe("turn_1"); expect(checkpointCapturedEvent?.payload).toEqual({ threadId: "thr_1", turnCount: 1, + turnId: "turn_1", + status: "completed", }); manager.dispose(); diff --git a/apps/server/src/providerManager.ts b/apps/server/src/providerManager.ts index d8442e6c7dee4aaf52d907eec6f69e730677fec5..5844b172a39c5bf25ef2b1742cc576928279371c 100644 --- a/apps/server/src/providerManager.ts +++ b/apps/server/src/providerManager.ts @@ -130,6 +130,12 @@ function summarizeTurn(turn: CodexThreadTurnSnapshot): { }; } +function readTurnCompletionStatus(event: ProviderEvent): string | undefined { + const payload = asObject(event.payload); + const turn = asObject(payload?.turn); + return asString(turn?.status); +} + function buildCheckpoints(turns: CodexThreadTurnSnapshot[]): ProviderCheckpoint[] { const checkpoints: ProviderCheckpoint[] = []; let messageCount = 0; @@ -475,6 +481,7 @@ export class ProviderManager extends EventEmitter { return; } + const turnStatus = readTurnCompletionStatus(event); const checkpointCwd = this.sessionCheckpointCwds.get(event.sessionId); if (!checkpointCwd) { void this.getOrInitializeFilesystemCheckpointCwd(event.sessionId) @@ -483,7 +490,13 @@ export class ProviderManager extends EventEmitter { return; } const snapshot = await this.codex.readThread(event.sessionId); - this.emitCheckpointCaptured(event.sessionId, snapshot.threadId, snapshot.turns.length); + this.emitCheckpointCaptured( + event.sessionId, + snapshot.threadId, + snapshot.turns.length, + event.turnId, + turnStatus, + ); }) .catch((error) => { const message = @@ -504,14 +517,26 @@ export class ProviderManager extends EventEmitter { threadId: snapshot.threadId, turnCount: snapshot.turns.length, }); - this.emitCheckpointCaptured(event.sessionId, snapshot.threadId, snapshot.turns.length); + this.emitCheckpointCaptured( + event.sessionId, + snapshot.threadId, + snapshot.turns.length, + event.turnId, + turnStatus, + ); }).catch((error) => { const message = error instanceof Error ? error.message : "Failed to capture checkpoint."; this.emitFilesystemCheckpointError(event.sessionId, message, event.threadId); }); } - private emitCheckpointCaptured(sessionId: string, threadId: string, turnCount: number): void { + private emitCheckpointCaptured( + sessionId: string, + threadId: string, + turnCount: number, + turnId?: string, + status?: string, + ): void { this.emit("event", { id: randomUUID(), kind: "notification", @@ -520,9 +545,12 @@ export class ProviderManager extends EventEmitter { createdAt: new Date().toISOString(), method: "checkpoint/captured", threadId, + ...(turnId ? { turnId } : {}), payload: { threadId, turnCount, + ...(turnId ? { turnId } : {}), + ...(status ? { status } : {}), }, }); } diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index a4cfef7dad2d1c30d83f63b1b73329912e99af9c..e0e811c7b698016ad6de63b9902d5aea995dd018 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -75,6 +75,19 @@ function parseBooleanEnv(value: string | undefined): boolean | undefined { return undefined; } +function asObject(value: unknown): Record | null { + if (!value || typeof value !== "object") return null; + return value as Record; +} + +function asString(value: unknown): string | null { + return typeof value === "string" ? value : null; +} + +function asNumber(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + export function createServer(options: ServerOptions) { const { port, @@ -200,6 +213,26 @@ export function createServer(options: ServerOptions) { createdAt: now, }); } + if (event.method === "checkpoint/captured") { + const payload = asObject(event.payload); + const turnId = event.turnId ?? asString(payload?.turnId); + if (turnId) { + await orchestrationEngine.dispatch({ + type: "thread.turnDiff.complete", + commandId: crypto.randomUUID(), + threadId: thread.id, + turnId, + completedAt: now, + ...(asString(payload?.status) !== null ? { status: asString(payload?.status) ?? undefined } : {}), + files: [], + assistantMessageId: `assistant:${turnId}`, + ...(asNumber(payload?.turnCount) !== null + ? { checkpointTurnCount: asNumber(payload?.turnCount) ?? undefined } + : {}), + createdAt: now, + }); + } + } if (event.kind === "error" && event.message) { await orchestrationEngine.dispatch({ type: "thread.session", @@ -425,7 +458,42 @@ export function createServer(options: ServerOptions) { return providerManager.getCheckpointDiff(request.params as never); case WS_METHODS.providersRevertToCheckpoint: - return providerManager.revertToCheckpoint(request.params as never); + { + const result = await providerManager.revertToCheckpoint(request.params as never); + const params = request.params as { sessionId?: string }; + const sessionId = params.sessionId; + if (typeof sessionId === "string") { + const snapshot = orchestrationEngine.getSnapshot(); + const thread = snapshot.threads.find((entry) => entry.session?.sessionId === sessionId); + if (thread) { + const now = new Date().toISOString(); + await orchestrationEngine.dispatch({ + type: "thread.revert", + commandId: crypto.randomUUID(), + threadId: thread.id, + turnCount: result.turnCount, + messageCount: result.messageCount, + createdAt: now, + }); + if (thread.session) { + await orchestrationEngine.dispatch({ + type: "thread.session", + commandId: crypto.randomUUID(), + threadId: thread.id, + session: { + ...thread.session, + status: "ready", + activeTurnId: null, + updatedAt: now, + lastError: null, + }, + createdAt: now, + }); + } + } + } + return result; + } case WS_METHODS.projectsList: return projectRegistry.list(); diff --git a/bun.lock b/bun.lock index ae9ab554c3c5013a4aee88b909b4d5a17bf7f401..56b5a99dbead1e0abfd3cc940a71564d54c4f685 100644 --- a/bun.lock +++ b/bun.lock @@ -34,7 +34,6 @@ "t3": "./dist/index.mjs", }, "dependencies": { - "better-sqlite3": "^12.6.2", "effect": "^3.19.19", "node-pty": "^1.1.0", "open": "^10.1.0", @@ -43,7 +42,6 @@ "devDependencies": { "@t3tools/contracts": "workspace:*", "@t3tools/web": "workspace:*", - "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.2", "@types/ws": "^8.5.13", "tsdown": "^0.20.3", @@ -498,8 +496,6 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], - "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -588,28 +584,18 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], - "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], - "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -640,8 +626,6 @@ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], @@ -674,8 +658,6 @@ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -748,8 +730,6 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -762,16 +742,12 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -790,8 +766,6 @@ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], @@ -840,16 +814,10 @@ "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="], "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], @@ -1044,8 +1012,6 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="], @@ -1054,10 +1020,6 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - - "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], - "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], @@ -1104,8 +1066,6 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], @@ -1122,8 +1082,6 @@ "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], @@ -1132,8 +1090,6 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], @@ -1184,8 +1140,6 @@ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -1204,10 +1158,6 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1226,14 +1176,10 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1254,10 +1200,6 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], @@ -1292,8 +1234,6 @@ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.8.7", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.7", "turbo-darwin-arm64": "2.8.7", "turbo-linux-64": "2.8.7", "turbo-linux-arm64": "2.8.7", "turbo-windows-64": "2.8.7", "turbo-windows-arm64": "2.8.7" }, "bin": { "turbo": "bin/turbo" } }, "sha512-RBLh5caMAu1kFdTK1jgH2gH/z+jFsvX5rGbhgJ9nlIAWXSvxlzwId05uDlBA1+pBd3wO/UaKYzaQZQBXDd7kcA=="], "turbo-darwin-64": ["turbo-darwin-64@2.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A=="], @@ -1344,8 +1284,6 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], @@ -1436,8 +1374,6 @@ "@types/babel__template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "@types/better-sqlite3/@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], - "@vitejs/plugin-react/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1494,8 +1430,6 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - "@types/better-sqlite3/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 2e1ee674cca40ffdaa12c48d111ded1071b3b28e..e9f53c19db9476b5f149348c3ef1e6efa6a8db79 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -189,6 +189,28 @@ export const UpsertGitReadModelCommandSchema = Schema.Struct({ createdAt: IsoDateTimeSchema, }); +export const CompleteThreadTurnDiffCommandSchema = Schema.Struct({ + type: Schema.Literal("thread.turnDiff.complete"), + commandId: IdSchema, + threadId: IdSchema, + turnId: IdSchema, + completedAt: IsoDateTimeSchema, + status: Schema.optional(Schema.String), + files: Schema.Array(OrchestrationTurnDiffFileSchema), + assistantMessageId: Schema.optional(Schema.String), + checkpointTurnCount: Schema.optional(Schema.Number), + createdAt: IsoDateTimeSchema, +}); + +export const RevertThreadCommandSchema = Schema.Struct({ + type: Schema.Literal("thread.revert"), + commandId: IdSchema, + threadId: IdSchema, + turnCount: Schema.Number, + messageCount: Schema.Number, + createdAt: IsoDateTimeSchema, +}); + export const OrchestrationCommandSchema = Schema.Union( CreateProjectCommandSchema, DeleteProjectCommandSchema, @@ -198,6 +220,8 @@ export const OrchestrationCommandSchema = Schema.Union( SendMessageCommandSchema, SetThreadSessionSchema, UpsertGitReadModelCommandSchema, + CompleteThreadTurnDiffCommandSchema, + RevertThreadCommandSchema, ); export type OrchestrationCommand = Schema.Schema.Type; -- 2.54.0