From 5d64138a279310463fd1d4abdf4880d022acce0d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 18:39:28 -0800 Subject: [PATCH 01/10] Propagate runtime approval and sandbox settings to turn startup - Add `approvalPolicy` and `sandboxMode` to turn-start command and requested payload contracts - Pass runtime mode-derived values from `ChatView` when dispatching `thread.turn.start` - Thread provider options through decider/reactor into session startup, with tests updated --- .../Layers/ProviderCommandReactor.test.ts | 4 ++++ .../Layers/ProviderCommandReactor.ts | 17 ++++++++++++++++- .../decider.projectScripts.test.ts | 10 ++++++++++ apps/server/src/orchestration/decider.ts | 4 ++++ apps/web/src/components/ChatView.tsx | 5 +++++ packages/contracts/src/orchestration.ts | 4 ++++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 3c59e54cba86..c41ce614aea8 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -171,6 +171,8 @@ describe("ProviderCommandReactor", () => { text: "hello reactor", attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now, }), ); @@ -181,6 +183,8 @@ describe("ProviderCommandReactor", () => { expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ cwd: "/tmp/provider-project", model: "gpt-5-codex", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", }); const readModel = await Effect.runPromise(harness.engine.getReadModel()); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index dc08566aefd9..ecba45185183 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -3,7 +3,9 @@ import { CommandId, EventId, type OrchestrationEvent, + type ProviderApprovalPolicy, type ProviderKind, + type ProviderSandboxMode, type OrchestrationSession, type ThreadId, type TurnId, @@ -128,6 +130,10 @@ const make = Effect.gen(function* () { const ensureSessionForThread = Effect.fnUntraced(function* ( threadId: ThreadId, createdAt: string, + options?: { + readonly approvalPolicy?: ProviderApprovalPolicy; + readonly sandboxMode?: ProviderSandboxMode; + }, ) { const readModel = yield* orchestrationEngine.getReadModel(); const thread = readModel.threads.find((entry) => entry.id === threadId); @@ -153,6 +159,8 @@ const make = Effect.gen(function* () { ...(preferredProvider ? { provider: preferredProvider } : {}), ...(effectiveCwd ? { cwd: effectiveCwd } : {}), ...(thread.model ? { model: thread.model } : {}), + ...(options?.approvalPolicy !== undefined ? { approvalPolicy: options.approvalPolicy } : {}), + ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}), }); yield* setThreadSession({ @@ -180,13 +188,18 @@ const make = Effect.gen(function* () { readonly attachments?: ReadonlyArray; readonly model?: string; readonly effort?: string; + readonly approvalPolicy?: ProviderApprovalPolicy; + readonly sandboxMode?: ProviderSandboxMode; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); if (!thread) { return; } - const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt); + const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, { + ...(input.approvalPolicy !== undefined ? { approvalPolicy: input.approvalPolicy } : {}), + ...(input.sandboxMode !== undefined ? { sandboxMode: input.sandboxMode } : {}), + }); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; @@ -232,6 +245,8 @@ const make = Effect.gen(function* () { ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), createdAt: event.payload.createdAt, + approvalPolicy: event.payload.approvalPolicy, + sandboxMode: event.payload.sandboxMode, }); }); diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index 9973219f629f..6d528614de5d 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -152,6 +152,8 @@ describe("decider project scripts", () => { }, model: "gpt-5", effort: "high", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now, }, readModel, @@ -169,5 +171,13 @@ describe("decider project scripts", () => { return; } expect(turnStartEvent.payload.assistantDeliveryMode).toBe("buffered"); + expect(turnStartEvent.payload).toMatchObject({ + threadId: ThreadId.makeUnsafe("thread-1"), + messageId: asMessageId("message-user-1"), + model: "gpt-5", + effort: "high", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + }); }); }); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 91e7d756e9e8..9599b4c05f80 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -254,6 +254,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ...(command.model !== undefined ? { model: command.model } : {}), ...(command.effort !== undefined ? { effort: command.effort } : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, + ...(command.approvalPolicy !== undefined + ? { approvalPolicy: command.approvalPolicy } + : {}), + ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}), createdAt: command.createdAt, }, }; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 95ab08d6c8f2..80dac2351509 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1496,6 +1496,9 @@ export default function ChatView({ threadId }: ChatViewProps) { ), ); attemptedTurnStart = true; + const approvalPolicy = state.runtimeMode === "full-access" ? "never" : "on-request"; + const sandboxMode = + state.runtimeMode === "full-access" ? "danger-full-access" : "workspace-write"; await api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), @@ -1509,6 +1512,8 @@ export default function ChatView({ threadId }: ChatViewProps) { model: selectedModel || undefined, effort: selectedEffort || undefined, assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", + approvalPolicy, + sandboxMode, createdAt: messageCreatedAt, }); } catch (err) { diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index c2f25d4e02b2..cf69a8db844f 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -282,6 +282,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), effort: Schema.optional(TrimmedNonEmptyString), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), + approvalPolicy: Schema.optional(ProviderApprovalPolicy), + sandboxMode: Schema.optional(ProviderSandboxMode), createdAt: IsoDateTime, }); @@ -496,6 +498,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), effort: Schema.optional(TrimmedNonEmptyString), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), + approvalPolicy: Schema.optional(ProviderApprovalPolicy), + sandboxMode: Schema.optional(ProviderSandboxMode), createdAt: IsoDateTime, }); From 6f2181dfaabe81a5fbd09df1b2e175730642045b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 19:12:09 -0800 Subject: [PATCH 02/10] Restart provider sessions when runtime mode changes - Persist `approvalPolicy` and `sandboxMode` on orchestration sessions - Reuse existing sessions when runtime mode is unchanged - Stop and recreate provider sessions when runtime mode settings change - Add reactor tests for reuse vs restart behavior --- .../Layers/ProviderCommandReactor.test.ts | 124 +++++++++++++++++- .../Layers/ProviderCommandReactor.ts | 75 +++++++---- .../Layers/ProviderRuntimeIngestion.ts | 12 ++ packages/contracts/src/orchestration.ts | 2 + 4 files changed, 181 insertions(+), 32 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index c41ce614aea8..bacb78607b45 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -71,16 +71,18 @@ describe("ProviderCommandReactor", () => { async function createHarness() { const now = new Date().toISOString(); const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); - const startSession = vi.fn((_: unknown, __: unknown) => - Effect.succeed({ - sessionId: asSessionId("sess-1"), + let nextSessionIndex = 1; + const startSession = vi.fn((_: unknown, __: unknown) => { + const sessionIndex = nextSessionIndex++; + return Effect.succeed({ + sessionId: asSessionId(`sess-${sessionIndex}`), provider: "codex" as const, status: "ready" as const, - threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + threadId: ProviderThreadId.makeUnsafe(`provider-thread-${sessionIndex}`), createdAt: now, updatedAt: now, - }), - ); + }); + }); const sendTurn = vi.fn((_: unknown) => Effect.succeed({ threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), @@ -190,6 +192,116 @@ describe("ProviderCommandReactor", () => { const readModel = await Effect.runPromise(harness.engine.getReadModel()); const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); expect(thread?.session?.providerSessionId).toBe("sess-1"); + expect(thread?.session?.approvalPolicy).toBe("on-request"); + expect(thread?.session?.sandboxMode).toBe("workspace-write"); + }); + + it("reuses the same provider session when runtime mode is unchanged", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-unchanged-1"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-unchanged-1"), + role: "user", + text: "first", + attachments: [], + }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-unchanged-2"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-unchanged-2"), + role: "user", + text: "second", + attachments: [], + }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + createdAt: now, + }), + ); + + await waitFor(() => harness.sendTurn.mock.calls.length === 2); + expect(harness.startSession.mock.calls.length).toBe(1); + expect(harness.stopSession.mock.calls.length).toBe(0); + }); + + it("restarts the provider session when runtime mode changes", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-runtime-mode-1"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-runtime-mode-1"), + role: "user", + text: "first", + attachments: [], + }, + approvalPolicy: "never", + sandboxMode: "danger-full-access", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-runtime-mode-2"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-runtime-mode-2"), + role: "user", + text: "second", + attachments: [], + }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + createdAt: now, + }), + ); + + await waitFor(() => harness.stopSession.mock.calls.length === 1); + await waitFor(() => harness.startSession.mock.calls.length === 2); + await waitFor(() => harness.sendTurn.mock.calls.length === 2); + + expect(harness.stopSession.mock.calls[0]?.[0]).toEqual({ sessionId: asSessionId("sess-1") }); + expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({ + resumeThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + }); + expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ + sessionId: asSessionId("sess-2"), + }); + + const readModel = await Effect.runPromise(harness.engine.getReadModel()); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + expect(thread?.session?.providerSessionId).toBe("sess-2"); + expect(thread?.session?.approvalPolicy).toBe("on-request"); + expect(thread?.session?.sandboxMode).toBe("workspace-write"); }); it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => { diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index ecba45185183..bee328e47938 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -8,6 +8,8 @@ import { type ProviderSandboxMode, type OrchestrationSession, type ThreadId, + type ProviderSession, + type ProviderThreadId, type TurnId, } from "@t3tools/contracts"; import { Cache, Cause, Duration, Effect, Layer, Option, Queue, Stream } from "effect"; @@ -141,11 +143,8 @@ const make = Effect.gen(function* () { return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); } - const existingSessionId = thread.session?.providerSessionId; - if (existingSessionId) { - return existingSessionId; - } - + const desiredApprovalPolicy = options?.approvalPolicy ?? thread.session?.approvalPolicy; + const desiredSandboxMode = options?.sandboxMode ?? thread.session?.sandboxMode; const preferredProvider: ProviderKind | undefined = thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode" ? thread.session.providerName @@ -155,30 +154,54 @@ const make = Effect.gen(function* () { projects: readModel.projects, }); - const startedSession = yield* providerService.startSession(threadId, { - ...(preferredProvider ? { provider: preferredProvider } : {}), - ...(effectiveCwd ? { cwd: effectiveCwd } : {}), - ...(thread.model ? { model: thread.model } : {}), - ...(options?.approvalPolicy !== undefined ? { approvalPolicy: options.approvalPolicy } : {}), - ...(options?.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}), - }); + const startProviderSession = (resumeThreadId?: ProviderThreadId | null) => + providerService.startSession(threadId, { + ...(preferredProvider ? { provider: preferredProvider } : {}), + ...(effectiveCwd ? { cwd: effectiveCwd } : {}), + ...(thread.model ? { model: thread.model } : {}), + ...(resumeThreadId ? { resumeThreadId } : {}), + ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}), + ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}), + }); - yield* setThreadSession({ - threadId, - session: { + const bindSessionToThread = (session: ProviderSession) => + setThreadSession({ threadId, - status: mapProviderSessionStatusToOrchestrationStatus(startedSession.status), - providerName: startedSession.provider, - providerSessionId: startedSession.sessionId, - providerThreadId: startedSession.threadId ?? null, - // Provider turn ids are not orchestration turn ids. - activeTurnId: null, - lastError: startedSession.lastError ?? null, - updatedAt: startedSession.updatedAt, - }, - createdAt, - }); + session: { + threadId, + status: mapProviderSessionStatusToOrchestrationStatus(session.status), + providerName: session.provider, + providerSessionId: session.sessionId, + providerThreadId: session.threadId ?? null, + ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}), + ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}), + // Provider turn ids are not orchestration turn ids. + activeTurnId: null, + lastError: session.lastError ?? null, + updatedAt: session.updatedAt, + }, + createdAt, + }); + + const existingSessionId = thread.session?.providerSessionId; + if (existingSessionId) { + const approvalPolicyChanged = + options?.approvalPolicy !== undefined && options.approvalPolicy !== thread.session?.approvalPolicy; + const sandboxModeChanged = + options?.sandboxMode !== undefined && options.sandboxMode !== thread.session?.sandboxMode; + + if (!approvalPolicyChanged && !sandboxModeChanged) { + return existingSessionId; + } + + yield* providerService.stopSession({ sessionId: existingSessionId }); + const restartedSession = yield* startProviderSession(thread.session?.providerThreadId ?? null); + yield* bindSessionToThread(restartedSession); + return restartedSession.sessionId; + } + const startedSession = yield* startProviderSession(); + yield* bindSessionToThread(startedSession); return startedSession.sessionId; }); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 84e2a936dbf3..889a8903e0a6 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -371,6 +371,12 @@ const make = Effect.gen(function* () { providerName: event.provider, providerSessionId: event.sessionId, providerThreadId, + ...(thread.session?.approvalPolicy !== undefined + ? { approvalPolicy: thread.session.approvalPolicy } + : {}), + ...(thread.session?.sandboxMode !== undefined + ? { sandboxMode: thread.session.sandboxMode } + : {}), activeTurnId, lastError, updatedAt: now, @@ -479,6 +485,12 @@ const make = Effect.gen(function* () { providerName: event.provider, providerSessionId: event.sessionId, providerThreadId, + ...(thread.session?.approvalPolicy !== undefined + ? { approvalPolicy: thread.session.approvalPolicy } + : {}), + ...(thread.session?.sandboxMode !== undefined + ? { sandboxMode: thread.session.sandboxMode } + : {}), activeTurnId: toTurnId(event.turnId) ?? null, lastError: event.message, updatedAt: now, diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index cf69a8db844f..cdf32a02f157 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -142,6 +142,8 @@ export const OrchestrationSession = Schema.Struct({ providerName: Schema.NullOr(TrimmedNonEmptyString), providerSessionId: Schema.NullOr(ProviderSessionId), providerThreadId: Schema.NullOr(ProviderThreadId), + approvalPolicy: Schema.optional(ProviderApprovalPolicy), + sandboxMode: Schema.optional(ProviderSandboxMode), activeTurnId: Schema.NullOr(TurnId), lastError: Schema.NullOr(TrimmedNonEmptyString), updatedAt: IsoDateTime, From 454ddeea57e16befdd3a893420ca46bbf291e7b6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 19:30:24 -0800 Subject: [PATCH 03/10] Add .t3 keybindings and initial local runtime state files - add default `.t3/keybindings.json` for terminal/chat/editor/diff shortcuts - include initial provider log files and SQLite state artifacts under `apps/server/.t3` --- apps/server/.t3/keybindings.json | 45 ++++++++++++++++++ .../logs/providers/provider-canonical.ndjson | 0 .../.t3/logs/providers/provider-native.ndjson | 0 apps/server/.t3/state.sqlite | Bin 0 -> 200704 bytes apps/server/.t3/state.sqlite-shm | Bin 0 -> 32768 bytes apps/server/.t3/state.sqlite-wal | 0 6 files changed, 45 insertions(+) create mode 100644 apps/server/.t3/keybindings.json create mode 100644 apps/server/.t3/logs/providers/provider-canonical.ndjson create mode 100644 apps/server/.t3/logs/providers/provider-native.ndjson create mode 100644 apps/server/.t3/state.sqlite create mode 100644 apps/server/.t3/state.sqlite-shm create mode 100644 apps/server/.t3/state.sqlite-wal diff --git a/apps/server/.t3/keybindings.json b/apps/server/.t3/keybindings.json new file mode 100644 index 000000000000..2d1f06cd49ea --- /dev/null +++ b/apps/server/.t3/keybindings.json @@ -0,0 +1,45 @@ +[ + { + "key": "mod+j", + "command": "terminal.toggle" + }, + { + "key": "mod+d", + "command": "terminal.split", + "when": "terminalFocus" + }, + { + "key": "mod+n", + "command": "terminal.new", + "when": "terminalFocus" + }, + { + "key": "mod+w", + "command": "terminal.close", + "when": "terminalFocus" + }, + { + "key": "mod+n", + "command": "chat.new", + "when": "!terminalFocus" + }, + { + "key": "mod+shift+o", + "command": "chat.new", + "when": "!terminalFocus" + }, + { + "key": "mod+shift+n", + "command": "chat.newLocal", + "when": "!terminalFocus" + }, + { + "key": "mod+o", + "command": "editor.openFavorite" + }, + { + "key": "mod+d", + "command": "diff.toggle", + "when": "!terminalFocus" + } +] diff --git a/apps/server/.t3/logs/providers/provider-canonical.ndjson b/apps/server/.t3/logs/providers/provider-canonical.ndjson new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/apps/server/.t3/logs/providers/provider-native.ndjson b/apps/server/.t3/logs/providers/provider-native.ndjson new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/apps/server/.t3/state.sqlite b/apps/server/.t3/state.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..2faad4ed67e452aacac0a6576e3e43d1bc8143c8 GIT binary patch literal 200704 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 literal 0 HcmV?d00001 diff --git a/apps/server/.t3/state.sqlite-shm b/apps/server/.t3/state.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..936a7c6bc62035ab45aeb45eef3b7cbf7776fd30 GIT binary patch literal 32768 zcmeI*Nluhe6vgpNsq~#7T409?NM~Pm*kBXqF2lG1ci|FPgfm8N!I5#y0$6~FfpaPn zYj{bl)c0QgCpoDmH&yTZE#SWP!JRnCP{izeeR!b$`Mvw)YO^SUQid*i|Qq{tuCrd>SgtcdR4uqURRgZ8|qEB{LKM7Lh-3u3 z)rn*ThU0N&tN}CLs6{dY-e*QK0weJxGuD8ahYdJG8AtFYKavp`ji;Hh2FyHaz!}Oo zf_GSwjKEkt%ZxEokbt+hl8nH3JkN|VQ;@)NL1!K8Q72+&um|AZ|GR)Uc9V?2WW30X zF;kF$ch!@Oz*M}w2KRJ&o@_C3OjG#=E*6 nsBcf>n}11N0$bgCr0aU7{ydHEye4%C_`Y$H5%8_*BqMMP9;`R( literal 0 HcmV?d00001 diff --git a/apps/server/.t3/state.sqlite-wal b/apps/server/.t3/state.sqlite-wal new file mode 100644 index 000000000000..e69de29bb2d1 From 6158b2ad5f63aa68debca2c1e899541a8421ba9e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 19:53:11 -0800 Subject: [PATCH 04/10] Fix type error with optional approvalPolicy and sandboxMode Use conditional spreading for these properties to handle undefined values correctly with exactOptionalPropertyTypes. Co-Authored-By: Claude Opus 4.5 --- .../src/orchestration/Layers/ProviderCommandReactor.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index bee328e47938..bebd7b8202dd 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -267,9 +267,11 @@ const make = Effect.gen(function* () { ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), + ...(event.payload.approvalPolicy !== undefined + ? { approvalPolicy: event.payload.approvalPolicy } + : {}), + ...(event.payload.sandboxMode !== undefined ? { sandboxMode: event.payload.sandboxMode } : {}), createdAt: event.payload.createdAt, - approvalPolicy: event.payload.approvalPolicy, - sandboxMode: event.payload.sandboxMode, }); }); From 422160a4203269e6cbc3769a05219bc634aa9e97 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 20:59:40 -0800 Subject: [PATCH 05/10] rm --- apps/server/.t3/keybindings.json | 45 ------------------ .../logs/providers/provider-canonical.ndjson | 0 .../.t3/logs/providers/provider-native.ndjson | 0 apps/server/.t3/state.sqlite | Bin 200704 -> 0 bytes apps/server/.t3/state.sqlite-shm | Bin 32768 -> 0 bytes apps/server/.t3/state.sqlite-wal | 0 6 files changed, 45 deletions(-) delete mode 100644 apps/server/.t3/keybindings.json delete mode 100644 apps/server/.t3/logs/providers/provider-canonical.ndjson delete mode 100644 apps/server/.t3/logs/providers/provider-native.ndjson delete mode 100644 apps/server/.t3/state.sqlite delete mode 100644 apps/server/.t3/state.sqlite-shm delete mode 100644 apps/server/.t3/state.sqlite-wal diff --git a/apps/server/.t3/keybindings.json b/apps/server/.t3/keybindings.json deleted file mode 100644 index 2d1f06cd49ea..000000000000 --- a/apps/server/.t3/keybindings.json +++ /dev/null @@ -1,45 +0,0 @@ -[ - { - "key": "mod+j", - "command": "terminal.toggle" - }, - { - "key": "mod+d", - "command": "terminal.split", - "when": "terminalFocus" - }, - { - "key": "mod+n", - "command": "terminal.new", - "when": "terminalFocus" - }, - { - "key": "mod+w", - "command": "terminal.close", - "when": "terminalFocus" - }, - { - "key": "mod+n", - "command": "chat.new", - "when": "!terminalFocus" - }, - { - "key": "mod+shift+o", - "command": "chat.new", - "when": "!terminalFocus" - }, - { - "key": "mod+shift+n", - "command": "chat.newLocal", - "when": "!terminalFocus" - }, - { - "key": "mod+o", - "command": "editor.openFavorite" - }, - { - "key": "mod+d", - "command": "diff.toggle", - "when": "!terminalFocus" - } -] diff --git a/apps/server/.t3/logs/providers/provider-canonical.ndjson b/apps/server/.t3/logs/providers/provider-canonical.ndjson deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/apps/server/.t3/logs/providers/provider-native.ndjson b/apps/server/.t3/logs/providers/provider-native.ndjson deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/apps/server/.t3/state.sqlite b/apps/server/.t3/state.sqlite deleted file mode 100644 index 2faad4ed67e452aacac0a6576e3e43d1bc8143c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200704 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 diff --git a/apps/server/.t3/state.sqlite-shm b/apps/server/.t3/state.sqlite-shm deleted file mode 100644 index 936a7c6bc62035ab45aeb45eef3b7cbf7776fd30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*Nluhe6vgpNsq~#7T409?NM~Pm*kBXqF2lG1ci|FPgfm8N!I5#y0$6~FfpaPn zYj{bl)c0QgCpoDmH&yTZE#SWP!JRnCP{izeeR!b$`Mvw)YO^SUQid*i|Qq{tuCrd>SgtcdR4uqURRgZ8|qEB{LKM7Lh-3u3 z)rn*ThU0N&tN}CLs6{dY-e*QK0weJxGuD8ahYdJG8AtFYKavp`ji;Hh2FyHaz!}Oo zf_GSwjKEkt%ZxEokbt+hl8nH3JkN|VQ;@)NL1!K8Q72+&um|AZ|GR)Uc9V?2WW30X zF;kF$ch!@Oz*M}w2KRJ&o@_C3OjG#=E*6 nsBcf>n}11N0$bgCr0aU7{ydHEye4%C_`Y$H5%8_*BqMMP9;`R( diff --git a/apps/server/.t3/state.sqlite-wal b/apps/server/.t3/state.sqlite-wal deleted file mode 100644 index e69de29bb2d1..000000000000 From 4d791fe27ce02675dac4b26fc9c2cc0550e09bc1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 21:08:38 -0800 Subject: [PATCH 06/10] Persist required approval policy and sandbox mode for sessions - Make `approvalPolicy` and `sandboxMode` required in orchestration contracts and turn-start flow - Add defaults in provider reactors/ingestion to guarantee values are always set - Store and hydrate both fields in projection persistence, snapshots, and related tests --- .../orchestrationEngine.integration.test.ts | 2 ++ .../Layers/CheckpointReactor.test.ts | 16 ++++++++++ .../Layers/OrchestrationEngine.test.ts | 6 ++++ .../Layers/ProjectionPipeline.test.ts | 4 +++ .../Layers/ProjectionPipeline.ts | 2 ++ .../Layers/ProjectionSnapshotQuery.test.ts | 6 ++++ .../Layers/ProjectionSnapshotQuery.ts | 4 +++ .../Layers/ProviderCommandReactor.test.ts | 6 ++++ .../Layers/ProviderCommandReactor.ts | 32 +++++++++++-------- .../Layers/ProviderRuntimeIngestion.test.ts | 4 +++ .../Layers/ProviderRuntimeIngestion.ts | 20 +++++------- .../orchestration/commandInvariants.test.ts | 2 ++ apps/server/src/orchestration/decider.ts | 6 ++-- .../src/orchestration/projector.test.ts | 4 +++ .../Layers/ProjectionThreadSessions.ts | 8 +++++ .../persistence/Migrations/005_Projections.ts | 2 ++ .../Services/ProjectionThreadSessions.ts | 4 +++ apps/server/src/wsServer.test.ts | 2 ++ packages/contracts/src/orchestration.ts | 12 +++---- 19 files changed, 106 insertions(+), 36 deletions(-) diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts index 3ccb19ebd34b..5572b6e3eed9 100644 --- a/apps/server/integration/orchestrationEngine.integration.test.ts +++ b/apps/server/integration/orchestrationEngine.integration.test.ts @@ -136,6 +136,8 @@ const startTurn = (input: { text: input.text, attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: nowIso(), }); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 8be890c5998e..769310380cca 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -317,6 +317,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: createdAt, @@ -394,6 +396,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: createdAt, @@ -445,6 +449,8 @@ describe("CheckpointReactor", () => { text: "start turn", attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: new Date().toISOString(), }), ); @@ -481,6 +487,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-missing"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: asTurnId("turn-missing-cwd"), lastError: null, updatedAt: createdAt, @@ -529,6 +537,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: createdAt, @@ -580,6 +590,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: createdAt, @@ -634,6 +646,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: createdAt, @@ -713,6 +727,8 @@ describe("CheckpointReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: createdAt, diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts index 6680402a8d63..925826bb1b43 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts @@ -92,6 +92,8 @@ describe("OrchestrationEngine", () => { text: "hello", attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt, }), ); @@ -428,6 +430,8 @@ describe("OrchestrationEngine", () => { text: "hello", attachments: [], }, + approvalPolicy: "on-request" as const, + sandboxMode: "workspace-write" as const, createdAt, }; @@ -586,6 +590,8 @@ describe("OrchestrationEngine", () => { text: "hello", attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now(), }), ), diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts index 9cc29bcd8eac..83fcb579579d 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts @@ -794,6 +794,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta payload: { threadId, messageId, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: turnStartedAt, }, }); @@ -827,6 +829,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta providerName: "codex", providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: turnId, lastError: null, updatedAt: sessionSetAt, diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 71ad8db405d8..262d4545a4a1 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -473,6 +473,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { providerName: event.payload.session.providerName, providerSessionId: event.payload.session.providerSessionId, providerThreadId: event.payload.session.providerThreadId, + approvalPolicy: event.payload.session.approvalPolicy, + sandboxMode: event.payload.session.sandboxMode, activeTurnId: event.payload.session.activeTurnId, lastError: event.payload.session.lastError, updatedAt: event.payload.session.updatedAt, diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index 824f9a7cfd67..97dadf01cb5a 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -143,6 +143,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { provider_name, provider_session_id, provider_thread_id, + approval_policy, + sandbox_mode, active_turn_id, last_error, updated_at @@ -153,6 +155,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { 'codex', 'provider-session-1', 'provider-thread-1', + 'on-request', + 'workspace-write', 'turn-1', NULL, '2026-02-24T00:00:07.000Z' @@ -282,6 +286,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { providerName: "codex", providerSessionId: asProviderSessionId("provider-session-1"), providerThreadId: asProviderThreadId("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: asTurnId("turn-1"), lastError: null, updatedAt: "2026-02-24T00:00:07.000Z", diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index babf83171a63..fe2f96615cb1 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -196,6 +196,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { provider_name AS "providerName", provider_session_id AS "providerSessionId", provider_thread_id AS "providerThreadId", + approval_policy AS "approvalPolicy", + sandbox_mode AS "sandboxMode", active_turn_id AS "activeTurnId", last_error AS "lastError", updated_at AS "updatedAt" @@ -378,6 +380,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { providerName: row.providerName, providerSessionId: row.providerSessionId, providerThreadId: row.providerThreadId, + approvalPolicy: row.approvalPolicy, + sandboxMode: row.sandboxMode, activeTurnId: row.activeTurnId, lastError: row.lastError, updatedAt: row.updatedAt, diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index bacb78607b45..eba9836a3c50 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -319,6 +319,8 @@ describe("ProviderCommandReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: asTurnId("turn-1"), lastError: null, updatedAt: now, @@ -358,6 +360,8 @@ describe("ProviderCommandReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: now, @@ -400,6 +404,8 @@ describe("ProviderCommandReactor", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, lastError: null, updatedAt: now, diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index bebd7b8202dd..3ae4dc99b5f5 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -64,6 +64,8 @@ const serverCommandId = (tag: string): CommandId => const HANDLED_TURN_START_KEY_MAX = 10_000; const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); +const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never"; +const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write"; const make = Effect.gen(function* () { const orchestrationEngine = yield* OrchestrationEngineService; @@ -143,8 +145,10 @@ const make = Effect.gen(function* () { return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); } - const desiredApprovalPolicy = options?.approvalPolicy ?? thread.session?.approvalPolicy; - const desiredSandboxMode = options?.sandboxMode ?? thread.session?.sandboxMode; + const desiredApprovalPolicy = + options?.approvalPolicy ?? thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY; + const desiredSandboxMode = + options?.sandboxMode ?? thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE; const preferredProvider: ProviderKind | undefined = thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode" ? thread.session.providerName @@ -160,8 +164,8 @@ const make = Effect.gen(function* () { ...(effectiveCwd ? { cwd: effectiveCwd } : {}), ...(thread.model ? { model: thread.model } : {}), ...(resumeThreadId ? { resumeThreadId } : {}), - ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}), - ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}), + approvalPolicy: desiredApprovalPolicy, + sandboxMode: desiredSandboxMode, }); const bindSessionToThread = (session: ProviderSession) => @@ -173,8 +177,8 @@ const make = Effect.gen(function* () { providerName: session.provider, providerSessionId: session.sessionId, providerThreadId: session.threadId ?? null, - ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}), - ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}), + approvalPolicy: desiredApprovalPolicy, + sandboxMode: desiredSandboxMode, // Provider turn ids are not orchestration turn ids. activeTurnId: null, lastError: session.lastError ?? null, @@ -211,8 +215,8 @@ const make = Effect.gen(function* () { readonly attachments?: ReadonlyArray; readonly model?: string; readonly effort?: string; - readonly approvalPolicy?: ProviderApprovalPolicy; - readonly sandboxMode?: ProviderSandboxMode; + readonly approvalPolicy: ProviderApprovalPolicy; + readonly sandboxMode: ProviderSandboxMode; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); @@ -220,8 +224,8 @@ const make = Effect.gen(function* () { return; } const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, { - ...(input.approvalPolicy !== undefined ? { approvalPolicy: input.approvalPolicy } : {}), - ...(input.sandboxMode !== undefined ? { sandboxMode: input.sandboxMode } : {}), + approvalPolicy: input.approvalPolicy, + sandboxMode: input.sandboxMode, }); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; @@ -267,10 +271,8 @@ const make = Effect.gen(function* () { ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), - ...(event.payload.approvalPolicy !== undefined - ? { approvalPolicy: event.payload.approvalPolicy } - : {}), - ...(event.payload.sandboxMode !== undefined ? { sandboxMode: event.payload.sandboxMode } : {}), + approvalPolicy: event.payload.approvalPolicy, + sandboxMode: event.payload.sandboxMode, createdAt: event.payload.createdAt, }); }); @@ -347,6 +349,8 @@ const make = Effect.gen(function* () { providerName: thread.session?.providerName ?? null, providerSessionId: null, providerThreadId: null, + approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, + sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, activeTurnId: null, lastError: thread.session?.lastError ?? null, updatedAt: now, diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 42247506410b..7a3a029e89a0 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -164,6 +164,8 @@ describe("ProviderRuntimeIngestion", () => { providerName: "codex", providerSessionId: asSessionId("sess-1"), providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: null, updatedAt: createdAt, lastError: null, @@ -332,6 +334,8 @@ describe("ProviderRuntimeIngestion", () => { attachments: [], }, assistantDeliveryMode: "streaming", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now, }), ); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 889a8903e0a6..8c763381cdd9 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -3,6 +3,8 @@ import { CommandId, MessageId, type OrchestrationEvent, + type ProviderApprovalPolicy, + type ProviderSandboxMode, ProviderThreadId, type ThreadId, TurnId, @@ -24,6 +26,8 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered"; +const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never"; +const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write"; const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; @@ -371,12 +375,8 @@ const make = Effect.gen(function* () { providerName: event.provider, providerSessionId: event.sessionId, providerThreadId, - ...(thread.session?.approvalPolicy !== undefined - ? { approvalPolicy: thread.session.approvalPolicy } - : {}), - ...(thread.session?.sandboxMode !== undefined - ? { sandboxMode: thread.session.sandboxMode } - : {}), + approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, + sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, activeTurnId, lastError, updatedAt: now, @@ -485,12 +485,8 @@ const make = Effect.gen(function* () { providerName: event.provider, providerSessionId: event.sessionId, providerThreadId, - ...(thread.session?.approvalPolicy !== undefined - ? { approvalPolicy: thread.session.approvalPolicy } - : {}), - ...(thread.session?.sandboxMode !== undefined - ? { sandboxMode: thread.session.sandboxMode } - : {}), + approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, + sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, activeTurnId: toTurnId(event.turnId) ?? null, lastError: event.message, updatedAt: now, diff --git a/apps/server/src/orchestration/commandInvariants.test.ts b/apps/server/src/orchestration/commandInvariants.test.ts index d02f80e30074..9ad953d5b7fb 100644 --- a/apps/server/src/orchestration/commandInvariants.test.ts +++ b/apps/server/src/orchestration/commandInvariants.test.ts @@ -90,6 +90,8 @@ const messageSendCommand: OrchestrationCommand = { text: "hello", attachments: [], }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: now, }; diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 9599b4c05f80..45268a113209 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -254,10 +254,8 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ...(command.model !== undefined ? { model: command.model } : {}), ...(command.effort !== undefined ? { effort: command.effort } : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, - ...(command.approvalPolicy !== undefined - ? { approvalPolicy: command.approvalPolicy } - : {}), - ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}), + approvalPolicy: command.approvalPolicy, + sandboxMode: command.sandboxMode, createdAt: command.createdAt, }, }; diff --git a/apps/server/src/orchestration/projector.test.ts b/apps/server/src/orchestration/projector.test.ts index ee4f7e872f98..56c0c0bed92a 100644 --- a/apps/server/src/orchestration/projector.test.ts +++ b/apps/server/src/orchestration/projector.test.ts @@ -135,6 +135,8 @@ describe("orchestration projector", () => { payload: { threadId: "thread-1", messageId: "message-1", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt: "2026-01-01T00:00:00.000Z", }, }), @@ -193,6 +195,8 @@ describe("orchestration projector", () => { providerName: "codex", providerSessionId: "session-1", providerThreadId: "provider-thread-1", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", activeTurnId: "turn-1", lastError: null, updatedAt: startedAt, diff --git a/apps/server/src/persistence/Layers/ProjectionThreadSessions.ts b/apps/server/src/persistence/Layers/ProjectionThreadSessions.ts index 2a3b0806ee33..0793ef388dba 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreadSessions.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreadSessions.ts @@ -25,6 +25,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { provider_name, provider_session_id, provider_thread_id, + approval_policy, + sandbox_mode, active_turn_id, last_error, updated_at @@ -35,6 +37,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { ${row.providerName}, ${row.providerSessionId}, ${row.providerThreadId}, + ${row.approvalPolicy}, + ${row.sandboxMode}, ${row.activeTurnId}, ${row.lastError}, ${row.updatedAt} @@ -45,6 +49,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { provider_name = excluded.provider_name, provider_session_id = excluded.provider_session_id, provider_thread_id = excluded.provider_thread_id, + approval_policy = excluded.approval_policy, + sandbox_mode = excluded.sandbox_mode, active_turn_id = excluded.active_turn_id, last_error = excluded.last_error, updated_at = excluded.updated_at @@ -62,6 +68,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { provider_name AS "providerName", provider_session_id AS "providerSessionId", provider_thread_id AS "providerThreadId", + approval_policy AS "approvalPolicy", + sandbox_mode AS "sandboxMode", active_turn_id AS "activeTurnId", last_error AS "lastError", updated_at AS "updatedAt" diff --git a/apps/server/src/persistence/Migrations/005_Projections.ts b/apps/server/src/persistence/Migrations/005_Projections.ts index c950da76a18e..ae5e7af07d2c 100644 --- a/apps/server/src/persistence/Migrations/005_Projections.ts +++ b/apps/server/src/persistence/Migrations/005_Projections.ts @@ -65,6 +65,8 @@ export default Effect.gen(function* () { provider_name TEXT, provider_session_id TEXT, provider_thread_id TEXT, + approval_policy TEXT NOT NULL, + sandbox_mode TEXT NOT NULL, active_turn_id TEXT, last_error TEXT, updated_at TEXT NOT NULL diff --git a/apps/server/src/persistence/Services/ProjectionThreadSessions.ts b/apps/server/src/persistence/Services/ProjectionThreadSessions.ts index a769cae89693..2789e2481e3b 100644 --- a/apps/server/src/persistence/Services/ProjectionThreadSessions.ts +++ b/apps/server/src/persistence/Services/ProjectionThreadSessions.ts @@ -9,6 +9,8 @@ import { IsoDateTime, OrchestrationSessionStatus, + ProviderApprovalPolicy, + ProviderSandboxMode, ProviderSessionId, ProviderThreadId, ThreadId, @@ -25,6 +27,8 @@ export const ProjectionThreadSession = Schema.Struct({ providerName: Schema.NullOr(Schema.String), providerSessionId: Schema.NullOr(ProviderSessionId), providerThreadId: Schema.NullOr(ProviderThreadId), + approvalPolicy: ProviderApprovalPolicy, + sandboxMode: ProviderSandboxMode, activeTurnId: Schema.NullOr(TurnId), lastError: Schema.NullOr(Schema.String), updatedAt: IsoDateTime, diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 78394af58557..fcb93a71d5ed 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -960,6 +960,8 @@ describe("WebSocket Server", () => { attachments: [], }, assistantDeliveryMode: "streaming", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", createdAt, }); expect(startTurnResponse.error).toBeUndefined(); diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index cdf32a02f157..619ea31bb4db 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -142,8 +142,8 @@ export const OrchestrationSession = Schema.Struct({ providerName: Schema.NullOr(TrimmedNonEmptyString), providerSessionId: Schema.NullOr(ProviderSessionId), providerThreadId: Schema.NullOr(ProviderThreadId), - approvalPolicy: Schema.optional(ProviderApprovalPolicy), - sandboxMode: Schema.optional(ProviderSandboxMode), + approvalPolicy: ProviderApprovalPolicy, + sandboxMode: ProviderSandboxMode, activeTurnId: Schema.NullOr(TurnId), lastError: Schema.NullOr(TrimmedNonEmptyString), updatedAt: IsoDateTime, @@ -284,8 +284,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), effort: Schema.optional(TrimmedNonEmptyString), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), - approvalPolicy: Schema.optional(ProviderApprovalPolicy), - sandboxMode: Schema.optional(ProviderSandboxMode), + approvalPolicy: ProviderApprovalPolicy, + sandboxMode: ProviderSandboxMode, createdAt: IsoDateTime, }); @@ -500,8 +500,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), effort: Schema.optional(TrimmedNonEmptyString), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), - approvalPolicy: Schema.optional(ProviderApprovalPolicy), - sandboxMode: Schema.optional(ProviderSandboxMode), + approvalPolicy: ProviderApprovalPolicy, + sandboxMode: ProviderSandboxMode, createdAt: IsoDateTime, }); From 4b80ad2a358fc47fff96c0c1cad9a710b60a6b03 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 21:13:00 -0800 Subject: [PATCH 07/10] Handle session restart failures without dropping active thread session - Reorder restart flow to bind the new provider session before stopping the old one - Log (and swallow) stop-session failures for superseded sessions - Add regression test ensuring failed restart does not stop the active session --- .../Layers/ProviderCommandReactor.test.ts | 58 +++++++++++++++++++ .../Layers/ProviderCommandReactor.ts | 10 +++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index eba9836a3c50..6bbf951b7eaf 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -304,6 +304,64 @@ describe("ProviderCommandReactor", () => { expect(thread?.session?.sandboxMode).toBe("workspace-write"); }); + it("does not stop the active session when restart fails before rebind", async () => { + const harness = await createHarness(); + const now = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-restart-failure-1"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-restart-failure-1"), + role: "user", + text: "first", + attachments: [], + }, + approvalPolicy: "never", + sandboxMode: "danger-full-access", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 1); + await waitFor(() => harness.sendTurn.mock.calls.length === 1); + + harness.startSession.mockImplementationOnce((_: unknown, __: unknown) => + Effect.fail(new Error("simulated restart failure")) as never, + ); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-restart-failure-2"), + threadId: ThreadId.makeUnsafe("thread-1"), + message: { + messageId: asMessageId("user-message-restart-failure-2"), + role: "user", + text: "second", + attachments: [], + }, + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + createdAt: now, + }), + ); + + await waitFor(() => harness.startSession.mock.calls.length === 2); + await Effect.runPromise(Effect.sleep("30 millis")); + + expect(harness.stopSession.mock.calls.length).toBe(0); + expect(harness.sendTurn.mock.calls.length).toBe(1); + + const readModel = await Effect.runPromise(harness.engine.getReadModel()); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + expect(thread?.session?.providerSessionId).toBe("sess-1"); + expect(thread?.session?.approvalPolicy).toBe("never"); + expect(thread?.session?.sandboxMode).toBe("danger-full-access"); + }); + it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 3ae4dc99b5f5..1a206ff64615 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -198,9 +198,17 @@ const make = Effect.gen(function* () { return existingSessionId; } - yield* providerService.stopSession({ sessionId: existingSessionId }); const restartedSession = yield* startProviderSession(thread.session?.providerThreadId ?? null); yield* bindSessionToThread(restartedSession); + yield* providerService.stopSession({ sessionId: existingSessionId }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("provider command reactor failed to stop superseded provider session", { + threadId, + sessionId: existingSessionId, + cause: Cause.pretty(cause), + }), + ), + ); return restartedSession.sessionId; } From caafd2113b539ef731c9424999375c3416e447bb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 21:18:00 -0800 Subject: [PATCH 08/10] Disable two Effect lint rules in server tsconfig - Turn off `preferSchemaOverJson` - Turn off `globalErrorInEffectFailure` --- apps/server/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index dd544c7f15ad..6173f99e44ba 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -14,7 +14,9 @@ "importFromBarrel": "error", "anyUnknownInErrorContext": "warning", "instanceOfSchema": "warning", - "deterministicKeys": "warning" + "deterministicKeys": "warning", + "preferSchemaOverJson": "off", + "globalErrorInEffectFailure": "off" } } ] From 21c62c5a4d0e5e3696fa86cbb4ecb4a1ff6641df Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 21:18:49 -0800 Subject: [PATCH 09/10] tone down defautls --- .../server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 8c763381cdd9..9da54b2a2636 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -26,7 +26,7 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered"; -const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never"; +const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "on-request"; const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write"; const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); From 7d0226ce6afa7ab4d312e309526fe9c97b1e8435 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 26 Feb 2026 21:23:23 -0800 Subject: [PATCH 10/10] migr --- apps/server/src/persistence/Migrations.ts | 2 ++ .../persistence/Migrations/005_Projections.ts | 2 -- ...ojectionThreadSessionRuntimeModeColumns.ts | 30 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 3779c16d4f13..12873ca29d2a 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -17,6 +17,7 @@ import Migration0002 from "./Migrations/002_OrchestrationCommandReceipts.ts"; import Migration0003 from "./Migrations/003_CheckpointDiffBlobs.ts"; import Migration0004 from "./Migrations/004_ProviderSessionRuntime.ts"; import Migration0005 from "./Migrations/005_Projections.ts"; +import Migration0006 from "./Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts"; /** * Migration loader with all migrations defined inline. @@ -34,6 +35,7 @@ const loader = Migrator.fromRecord({ "3_CheckpointDiffBlobs": Migration0003, "4_ProviderSessionRuntime": Migration0004, "5_Projections": Migration0005, + "6_ProjectionThreadSessionRuntimeModeColumns": Migration0006, }); /** diff --git a/apps/server/src/persistence/Migrations/005_Projections.ts b/apps/server/src/persistence/Migrations/005_Projections.ts index ae5e7af07d2c..c950da76a18e 100644 --- a/apps/server/src/persistence/Migrations/005_Projections.ts +++ b/apps/server/src/persistence/Migrations/005_Projections.ts @@ -65,8 +65,6 @@ export default Effect.gen(function* () { provider_name TEXT, provider_session_id TEXT, provider_thread_id TEXT, - approval_policy TEXT NOT NULL, - sandbox_mode TEXT NOT NULL, active_turn_id TEXT, last_error TEXT, updated_at TEXT NOT NULL diff --git a/apps/server/src/persistence/Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts b/apps/server/src/persistence/Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts new file mode 100644 index 000000000000..bcdef2801633 --- /dev/null +++ b/apps/server/src/persistence/Migrations/006_ProjectionThreadSessionRuntimeModeColumns.ts @@ -0,0 +1,30 @@ +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as Effect from "effect/Effect"; + +const DEFAULT_APPROVAL_POLICY = "never"; +const DEFAULT_SANDBOX_MODE = "workspace-write"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + ALTER TABLE projection_thread_sessions + ADD COLUMN approval_policy TEXT NOT NULL DEFAULT 'never' + `; + + yield* sql` + ALTER TABLE projection_thread_sessions + ADD COLUMN sandbox_mode TEXT NOT NULL DEFAULT 'workspace-write' + `; + + yield* sql` + UPDATE projection_thread_sessions + SET approval_policy = ${DEFAULT_APPROVAL_POLICY} + WHERE approval_policy IS NULL + `; + yield* sql` + UPDATE projection_thread_sessions + SET sandbox_mode = ${DEFAULT_SANDBOX_MODE} + WHERE sandbox_mode IS NULL + `; +});