| author | |
| committer | |
| log | 8ef653fcc0270855a7e39140664e584083e759b4 |
| tree | 4b109cf58599e2baaa20c060855fe0d93048fe0a |
| parent | fb8d66b8d321ce58a52768560cdc3349d7db878d |
- 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 tests19 files changed, 106 insertions(+), 36 deletions(-)
apps/server/integration/orchestrationEngine.integration.test.ts+2| ... | @@ -136,6 +136,8 @@ const startTurn = (input: { | ... | @@ -136,6 +136,8 @@ const startTurn = (input: { |
| 136 | text: input.text, | 136 | text: input.text, |
| 137 | attachments: [], | 137 | attachments: [], |
| 138 | }, | 138 | }, |
| 139 | approvalPolicy: "on-request", | ||
| 140 | sandboxMode: "workspace-write", | ||
| 139 | createdAt: nowIso(), | 141 | createdAt: nowIso(), |
| 140 | }); | 142 | }); |
| 141 | 143 |
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+16| ... | @@ -317,6 +317,8 @@ describe("CheckpointReactor", () => { | ... | @@ -317,6 +317,8 @@ describe("CheckpointReactor", () => { |
| 317 | providerName: "codex", | 317 | providerName: "codex", |
| 318 | providerSessionId: asSessionId("sess-1"), | 318 | providerSessionId: asSessionId("sess-1"), |
| 319 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 319 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 320 | approvalPolicy: "on-request", | ||
| 321 | sandboxMode: "workspace-write", | ||
| 320 | activeTurnId: null, | 322 | activeTurnId: null, |
| 321 | lastError: null, | 323 | lastError: null, |
| 322 | updatedAt: createdAt, | 324 | updatedAt: createdAt, |
| ... | @@ -394,6 +396,8 @@ describe("CheckpointReactor", () => { | ... | @@ -394,6 +396,8 @@ describe("CheckpointReactor", () => { |
| 394 | providerName: "codex", | 396 | providerName: "codex", |
| 395 | providerSessionId: asSessionId("sess-1"), | 397 | providerSessionId: asSessionId("sess-1"), |
| 396 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 398 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 399 | approvalPolicy: "on-request", | ||
| 400 | sandboxMode: "workspace-write", | ||
| 397 | activeTurnId: null, | 401 | activeTurnId: null, |
| 398 | lastError: null, | 402 | lastError: null, |
| 399 | updatedAt: createdAt, | 403 | updatedAt: createdAt, |
| ... | @@ -445,6 +449,8 @@ describe("CheckpointReactor", () => { | ... | @@ -445,6 +449,8 @@ describe("CheckpointReactor", () => { |
| 445 | text: "start turn", | 449 | text: "start turn", |
| 446 | attachments: [], | 450 | attachments: [], |
| 447 | }, | 451 | }, |
| 452 | approvalPolicy: "on-request", | ||
| 453 | sandboxMode: "workspace-write", | ||
| 448 | createdAt: new Date().toISOString(), | 454 | createdAt: new Date().toISOString(), |
| 449 | }), | 455 | }), |
| 450 | ); | 456 | ); |
| ... | @@ -481,6 +487,8 @@ describe("CheckpointReactor", () => { | ... | @@ -481,6 +487,8 @@ describe("CheckpointReactor", () => { |
| 481 | providerName: "codex", | 487 | providerName: "codex", |
| 482 | providerSessionId: asSessionId("sess-missing"), | 488 | providerSessionId: asSessionId("sess-missing"), |
| 483 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), | 489 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-missing"), |
| 490 | approvalPolicy: "on-request", | ||
| 491 | sandboxMode: "workspace-write", | ||
| 484 | activeTurnId: asTurnId("turn-missing-cwd"), | 492 | activeTurnId: asTurnId("turn-missing-cwd"), |
| 485 | lastError: null, | 493 | lastError: null, |
| 486 | updatedAt: createdAt, | 494 | updatedAt: createdAt, |
| ... | @@ -529,6 +537,8 @@ describe("CheckpointReactor", () => { | ... | @@ -529,6 +537,8 @@ describe("CheckpointReactor", () => { |
| 529 | providerName: "codex", | 537 | providerName: "codex", |
| 530 | providerSessionId: asSessionId("sess-1"), | 538 | providerSessionId: asSessionId("sess-1"), |
| 531 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 539 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 540 | approvalPolicy: "on-request", | ||
| 541 | sandboxMode: "workspace-write", | ||
| 532 | activeTurnId: null, | 542 | activeTurnId: null, |
| 533 | lastError: null, | 543 | lastError: null, |
| 534 | updatedAt: createdAt, | 544 | updatedAt: createdAt, |
| ... | @@ -580,6 +590,8 @@ describe("CheckpointReactor", () => { | ... | @@ -580,6 +590,8 @@ describe("CheckpointReactor", () => { |
| 580 | providerName: "codex", | 590 | providerName: "codex", |
| 581 | providerSessionId: asSessionId("sess-1"), | 591 | providerSessionId: asSessionId("sess-1"), |
| 582 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 592 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 593 | approvalPolicy: "on-request", | ||
| 594 | sandboxMode: "workspace-write", | ||
| 583 | activeTurnId: null, | 595 | activeTurnId: null, |
| 584 | lastError: null, | 596 | lastError: null, |
| 585 | updatedAt: createdAt, | 597 | updatedAt: createdAt, |
| ... | @@ -634,6 +646,8 @@ describe("CheckpointReactor", () => { | ... | @@ -634,6 +646,8 @@ describe("CheckpointReactor", () => { |
| 634 | providerName: "codex", | 646 | providerName: "codex", |
| 635 | providerSessionId: asSessionId("sess-1"), | 647 | providerSessionId: asSessionId("sess-1"), |
| 636 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 648 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 649 | approvalPolicy: "on-request", | ||
| 650 | sandboxMode: "workspace-write", | ||
| 637 | activeTurnId: null, | 651 | activeTurnId: null, |
| 638 | lastError: null, | 652 | lastError: null, |
| 639 | updatedAt: createdAt, | 653 | updatedAt: createdAt, |
| ... | @@ -713,6 +727,8 @@ describe("CheckpointReactor", () => { | ... | @@ -713,6 +727,8 @@ describe("CheckpointReactor", () => { |
| 713 | providerName: "codex", | 727 | providerName: "codex", |
| 714 | providerSessionId: asSessionId("sess-1"), | 728 | providerSessionId: asSessionId("sess-1"), |
| 715 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 729 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 730 | approvalPolicy: "on-request", | ||
| 731 | sandboxMode: "workspace-write", | ||
| 716 | activeTurnId: null, | 732 | activeTurnId: null, |
| 717 | lastError: null, | 733 | lastError: null, |
| 718 | updatedAt: createdAt, | 734 | updatedAt: createdAt, |
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+6| ... | @@ -92,6 +92,8 @@ describe("OrchestrationEngine", () => { | ... | @@ -92,6 +92,8 @@ describe("OrchestrationEngine", () => { |
| 92 | text: "hello", | 92 | text: "hello", |
| 93 | attachments: [], | 93 | attachments: [], |
| 94 | }, | 94 | }, |
| 95 | approvalPolicy: "on-request", | ||
| 96 | sandboxMode: "workspace-write", | ||
| 95 | createdAt, | 97 | createdAt, |
| 96 | }), | 98 | }), |
| 97 | ); | 99 | ); |
| ... | @@ -428,6 +430,8 @@ describe("OrchestrationEngine", () => { | ... | @@ -428,6 +430,8 @@ describe("OrchestrationEngine", () => { |
| 428 | text: "hello", | 430 | text: "hello", |
| 429 | attachments: [], | 431 | attachments: [], |
| 430 | }, | 432 | }, |
| 433 | approvalPolicy: "on-request" as const, | ||
| 434 | sandboxMode: "workspace-write" as const, | ||
| 431 | createdAt, | 435 | createdAt, |
| 432 | }; | 436 | }; |
| 433 | 437 | ||
| ... | @@ -586,6 +590,8 @@ describe("OrchestrationEngine", () => { | ... | @@ -586,6 +590,8 @@ describe("OrchestrationEngine", () => { |
| 586 | text: "hello", | 590 | text: "hello", |
| 587 | attachments: [], | 591 | attachments: [], |
| 588 | }, | 592 | }, |
| 593 | approvalPolicy: "on-request", | ||
| 594 | sandboxMode: "workspace-write", | ||
| 589 | createdAt: now(), | 595 | createdAt: now(), |
| 590 | }), | 596 | }), |
| 591 | ), | 597 | ), |
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+4| ... | @@ -794,6 +794,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta | ... | @@ -794,6 +794,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta |
| 794 | payload: { | 794 | payload: { |
| 795 | threadId, | 795 | threadId, |
| 796 | messageId, | 796 | messageId, |
| 797 | approvalPolicy: "on-request", | ||
| 798 | sandboxMode: "workspace-write", | ||
| 797 | createdAt: turnStartedAt, | 799 | createdAt: turnStartedAt, |
| 798 | }, | 800 | }, |
| 799 | }); | 801 | }); |
| ... | @@ -827,6 +829,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta | ... | @@ -827,6 +829,8 @@ it.effect("restores pending turn-start metadata across projection pipeline resta |
| 827 | providerName: "codex", | 829 | providerName: "codex", |
| 828 | providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"), | 830 | providerSessionId: ProviderSessionId.makeUnsafe("provider-session-restart"), |
| 829 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"), | 831 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-restart"), |
| 832 | approvalPolicy: "on-request", | ||
| 833 | sandboxMode: "workspace-write", | ||
| 830 | activeTurnId: turnId, | 834 | activeTurnId: turnId, |
| 831 | lastError: null, | 835 | lastError: null, |
| 832 | updatedAt: sessionSetAt, | 836 | updatedAt: sessionSetAt, |
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+2| ... | @@ -473,6 +473,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { | ... | @@ -473,6 +473,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { |
| 473 | providerName: event.payload.session.providerName, | 473 | providerName: event.payload.session.providerName, |
| 474 | providerSessionId: event.payload.session.providerSessionId, | 474 | providerSessionId: event.payload.session.providerSessionId, |
| 475 | providerThreadId: event.payload.session.providerThreadId, | 475 | providerThreadId: event.payload.session.providerThreadId, |
| 476 | approvalPolicy: event.payload.session.approvalPolicy, | ||
| 477 | sandboxMode: event.payload.session.sandboxMode, | ||
| 476 | activeTurnId: event.payload.session.activeTurnId, | 478 | activeTurnId: event.payload.session.activeTurnId, |
| 477 | lastError: event.payload.session.lastError, | 479 | lastError: event.payload.session.lastError, |
| 478 | updatedAt: event.payload.session.updatedAt, | 480 | updatedAt: event.payload.session.updatedAt, |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+6| ... | @@ -143,6 +143,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { | ... | @@ -143,6 +143,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { |
| 143 | provider_name, | 143 | provider_name, |
| 144 | provider_session_id, | 144 | provider_session_id, |
| 145 | provider_thread_id, | 145 | provider_thread_id, |
| 146 | approval_policy, | ||
| 147 | sandbox_mode, | ||
| 146 | active_turn_id, | 148 | active_turn_id, |
| 147 | last_error, | 149 | last_error, |
| 148 | updated_at | 150 | updated_at |
| ... | @@ -153,6 +155,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { | ... | @@ -153,6 +155,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { |
| 153 | 'codex', | 155 | 'codex', |
| 154 | 'provider-session-1', | 156 | 'provider-session-1', |
| 155 | 'provider-thread-1', | 157 | 'provider-thread-1', |
| 158 | 'on-request', | ||
| 159 | 'workspace-write', | ||
| 156 | 'turn-1', | 160 | 'turn-1', |
| 157 | NULL, | 161 | NULL, |
| 158 | '2026-02-24T00:00:07.000Z' | 162 | '2026-02-24T00:00:07.000Z' |
| ... | @@ -282,6 +286,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { | ... | @@ -282,6 +286,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { |
| 282 | providerName: "codex", | 286 | providerName: "codex", |
| 283 | providerSessionId: asProviderSessionId("provider-session-1"), | 287 | providerSessionId: asProviderSessionId("provider-session-1"), |
| 284 | providerThreadId: asProviderThreadId("provider-thread-1"), | 288 | providerThreadId: asProviderThreadId("provider-thread-1"), |
| 289 | approvalPolicy: "on-request", | ||
| 290 | sandboxMode: "workspace-write", | ||
| 285 | activeTurnId: asTurnId("turn-1"), | 291 | activeTurnId: asTurnId("turn-1"), |
| 286 | lastError: null, | 292 | lastError: null, |
| 287 | updatedAt: "2026-02-24T00:00:07.000Z", | 293 | updatedAt: "2026-02-24T00:00:07.000Z", |
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+4| ... | @@ -196,6 +196,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -196,6 +196,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 196 | provider_name AS "providerName", | 196 | provider_name AS "providerName", |
| 197 | provider_session_id AS "providerSessionId", | 197 | provider_session_id AS "providerSessionId", |
| 198 | provider_thread_id AS "providerThreadId", | 198 | provider_thread_id AS "providerThreadId", |
| 199 | approval_policy AS "approvalPolicy", | ||
| 200 | sandbox_mode AS "sandboxMode", | ||
| 199 | active_turn_id AS "activeTurnId", | 201 | active_turn_id AS "activeTurnId", |
| 200 | last_error AS "lastError", | 202 | last_error AS "lastError", |
| 201 | updated_at AS "updatedAt" | 203 | updated_at AS "updatedAt" |
| ... | @@ -378,6 +380,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -378,6 +380,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 378 | providerName: row.providerName, | 380 | providerName: row.providerName, |
| 379 | providerSessionId: row.providerSessionId, | 381 | providerSessionId: row.providerSessionId, |
| 380 | providerThreadId: row.providerThreadId, | 382 | providerThreadId: row.providerThreadId, |
| 383 | approvalPolicy: row.approvalPolicy, | ||
| 384 | sandboxMode: row.sandboxMode, | ||
| 381 | activeTurnId: row.activeTurnId, | 385 | activeTurnId: row.activeTurnId, |
| 382 | lastError: row.lastError, | 386 | lastError: row.lastError, |
| 383 | updatedAt: row.updatedAt, | 387 | updatedAt: row.updatedAt, |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+6| ... | @@ -319,6 +319,8 @@ describe("ProviderCommandReactor", () => { | ... | @@ -319,6 +319,8 @@ describe("ProviderCommandReactor", () => { |
| 319 | providerName: "codex", | 319 | providerName: "codex", |
| 320 | providerSessionId: asSessionId("sess-1"), | 320 | providerSessionId: asSessionId("sess-1"), |
| 321 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 321 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 322 | approvalPolicy: "on-request", | ||
| 323 | sandboxMode: "workspace-write", | ||
| 322 | activeTurnId: asTurnId("turn-1"), | 324 | activeTurnId: asTurnId("turn-1"), |
| 323 | lastError: null, | 325 | lastError: null, |
| 324 | updatedAt: now, | 326 | updatedAt: now, |
| ... | @@ -358,6 +360,8 @@ describe("ProviderCommandReactor", () => { | ... | @@ -358,6 +360,8 @@ describe("ProviderCommandReactor", () => { |
| 358 | providerName: "codex", | 360 | providerName: "codex", |
| 359 | providerSessionId: asSessionId("sess-1"), | 361 | providerSessionId: asSessionId("sess-1"), |
| 360 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 362 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 363 | approvalPolicy: "on-request", | ||
| 364 | sandboxMode: "workspace-write", | ||
| 361 | activeTurnId: null, | 365 | activeTurnId: null, |
| 362 | lastError: null, | 366 | lastError: null, |
| 363 | updatedAt: now, | 367 | updatedAt: now, |
| ... | @@ -400,6 +404,8 @@ describe("ProviderCommandReactor", () => { | ... | @@ -400,6 +404,8 @@ describe("ProviderCommandReactor", () => { |
| 400 | providerName: "codex", | 404 | providerName: "codex", |
| 401 | providerSessionId: asSessionId("sess-1"), | 405 | providerSessionId: asSessionId("sess-1"), |
| 402 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 406 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 407 | approvalPolicy: "on-request", | ||
| 408 | sandboxMode: "workspace-write", | ||
| 403 | activeTurnId: null, | 409 | activeTurnId: null, |
| 404 | lastError: null, | 410 | lastError: null, |
| 405 | updatedAt: now, | 411 | updatedAt: now, |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+18-14| ... | @@ -64,6 +64,8 @@ const serverCommandId = (tag: string): CommandId => | ... | @@ -64,6 +64,8 @@ const serverCommandId = (tag: string): CommandId => |
| 64 | 64 | ||
| 65 | const HANDLED_TURN_START_KEY_MAX = 10_000; | 65 | const HANDLED_TURN_START_KEY_MAX = 10_000; |
| 66 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); | 66 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); |
| 67 | const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never"; | ||
| 68 | const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write"; | ||
| 67 | 69 | ||
| 68 | const make = Effect.gen(function* () { | 70 | const make = Effect.gen(function* () { |
| 69 | const orchestrationEngine = yield* OrchestrationEngineService; | 71 | const orchestrationEngine = yield* OrchestrationEngineService; |
| ... | @@ -143,8 +145,10 @@ const make = Effect.gen(function* () { | ... | @@ -143,8 +145,10 @@ const make = Effect.gen(function* () { |
| 143 | return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); | 145 | return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); |
| 144 | } | 146 | } |
| 145 | 147 | ||
| 146 | const desiredApprovalPolicy = options?.approvalPolicy ?? thread.session?.approvalPolicy; | 148 | const desiredApprovalPolicy = |
| 147 | const desiredSandboxMode = options?.sandboxMode ?? thread.session?.sandboxMode; | 149 | options?.approvalPolicy ?? thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY; |
| 150 | const desiredSandboxMode = | ||
| 151 | options?.sandboxMode ?? thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE; | ||
| 148 | const preferredProvider: ProviderKind | undefined = | 152 | const preferredProvider: ProviderKind | undefined = |
| 149 | thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode" | 153 | thread.session?.providerName === "codex" || thread.session?.providerName === "claudeCode" |
| 150 | ? thread.session.providerName | 154 | ? thread.session.providerName |
| ... | @@ -160,8 +164,8 @@ const make = Effect.gen(function* () { | ... | @@ -160,8 +164,8 @@ const make = Effect.gen(function* () { |
| 160 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), | 164 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), |
| 161 | ...(thread.model ? { model: thread.model } : {}), | 165 | ...(thread.model ? { model: thread.model } : {}), |
| 162 | ...(resumeThreadId ? { resumeThreadId } : {}), | 166 | ...(resumeThreadId ? { resumeThreadId } : {}), |
| 163 | ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}), | 167 | approvalPolicy: desiredApprovalPolicy, |
| 164 | ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}), | 168 | sandboxMode: desiredSandboxMode, |
| 165 | }); | 169 | }); |
| 166 | 170 | ||
| 167 | const bindSessionToThread = (session: ProviderSession) => | 171 | const bindSessionToThread = (session: ProviderSession) => |
| ... | @@ -173,8 +177,8 @@ const make = Effect.gen(function* () { | ... | @@ -173,8 +177,8 @@ const make = Effect.gen(function* () { |
| 173 | providerName: session.provider, | 177 | providerName: session.provider, |
| 174 | providerSessionId: session.sessionId, | 178 | providerSessionId: session.sessionId, |
| 175 | providerThreadId: session.threadId ?? null, | 179 | providerThreadId: session.threadId ?? null, |
| 176 | ...(desiredApprovalPolicy !== undefined ? { approvalPolicy: desiredApprovalPolicy } : {}), | 180 | approvalPolicy: desiredApprovalPolicy, |
| 177 | ...(desiredSandboxMode !== undefined ? { sandboxMode: desiredSandboxMode } : {}), | 181 | sandboxMode: desiredSandboxMode, |
| 178 | // Provider turn ids are not orchestration turn ids. | 182 | // Provider turn ids are not orchestration turn ids. |
| 179 | activeTurnId: null, | 183 | activeTurnId: null, |
| 180 | lastError: session.lastError ?? null, | 184 | lastError: session.lastError ?? null, |
| ... | @@ -211,8 +215,8 @@ const make = Effect.gen(function* () { | ... | @@ -211,8 +215,8 @@ const make = Effect.gen(function* () { |
| 211 | readonly attachments?: ReadonlyArray<ChatAttachment>; | 215 | readonly attachments?: ReadonlyArray<ChatAttachment>; |
| 212 | readonly model?: string; | 216 | readonly model?: string; |
| 213 | readonly effort?: string; | 217 | readonly effort?: string; |
| 214 | readonly approvalPolicy?: ProviderApprovalPolicy; | 218 | readonly approvalPolicy: ProviderApprovalPolicy; |
| 215 | readonly sandboxMode?: ProviderSandboxMode; | 219 | readonly sandboxMode: ProviderSandboxMode; |
| 216 | readonly createdAt: string; | 220 | readonly createdAt: string; |
| 217 | }) { | 221 | }) { |
| 218 | const thread = yield* resolveThread(input.threadId); | 222 | const thread = yield* resolveThread(input.threadId); |
| ... | @@ -220,8 +224,8 @@ const make = Effect.gen(function* () { | ... | @@ -220,8 +224,8 @@ const make = Effect.gen(function* () { |
| 220 | return; | 224 | return; |
| 221 | } | 225 | } |
| 222 | const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, { | 226 | const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt, { |
| 223 | ...(input.approvalPolicy !== undefined ? { approvalPolicy: input.approvalPolicy } : {}), | 227 | approvalPolicy: input.approvalPolicy, |
| 224 | ...(input.sandboxMode !== undefined ? { sandboxMode: input.sandboxMode } : {}), | 228 | sandboxMode: input.sandboxMode, |
| 225 | }); | 229 | }); |
| 226 | const normalizedInput = toNonEmptyProviderInput(input.messageText); | 230 | const normalizedInput = toNonEmptyProviderInput(input.messageText); |
| 227 | const normalizedAttachments = input.attachments ?? []; | 231 | const normalizedAttachments = input.attachments ?? []; |
| ... | @@ -267,10 +271,8 @@ const make = Effect.gen(function* () { | ... | @@ -267,10 +271,8 @@ const make = Effect.gen(function* () { |
| 267 | ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), | 271 | ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), |
| 268 | ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), | 272 | ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), |
| 269 | ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), | 273 | ...(event.payload.effort !== undefined ? { effort: event.payload.effort } : {}), |
| 270 | ...(event.payload.approvalPolicy !== undefined | 274 | approvalPolicy: event.payload.approvalPolicy, |
| 271 | ? { approvalPolicy: event.payload.approvalPolicy } | 275 | sandboxMode: event.payload.sandboxMode, |
| 272 | : {}), | ||
| 273 | ...(event.payload.sandboxMode !== undefined ? { sandboxMode: event.payload.sandboxMode } : {}), | ||
| 274 | createdAt: event.payload.createdAt, | 276 | createdAt: event.payload.createdAt, |
| 275 | }); | 277 | }); |
| 276 | }); | 278 | }); |
| ... | @@ -347,6 +349,8 @@ const make = Effect.gen(function* () { | ... | @@ -347,6 +349,8 @@ const make = Effect.gen(function* () { |
| 347 | providerName: thread.session?.providerName ?? null, | 349 | providerName: thread.session?.providerName ?? null, |
| 348 | providerSessionId: null, | 350 | providerSessionId: null, |
| 349 | providerThreadId: null, | 351 | providerThreadId: null, |
| 352 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, | ||
| 353 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, | ||
| 350 | activeTurnId: null, | 354 | activeTurnId: null, |
| 351 | lastError: thread.session?.lastError ?? null, | 355 | lastError: thread.session?.lastError ?? null, |
| 352 | updatedAt: now, | 356 | updatedAt: now, |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+4| ... | @@ -164,6 +164,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -164,6 +164,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 164 | providerName: "codex", | 164 | providerName: "codex", |
| 165 | providerSessionId: asSessionId("sess-1"), | 165 | providerSessionId: asSessionId("sess-1"), |
| 166 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), | 166 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 167 | approvalPolicy: "on-request", | ||
| 168 | sandboxMode: "workspace-write", | ||
| 167 | activeTurnId: null, | 169 | activeTurnId: null, |
| 168 | updatedAt: createdAt, | 170 | updatedAt: createdAt, |
| 169 | lastError: null, | 171 | lastError: null, |
| ... | @@ -332,6 +334,8 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -332,6 +334,8 @@ describe("ProviderRuntimeIngestion", () => { |
| 332 | attachments: [], | 334 | attachments: [], |
| 333 | }, | 335 | }, |
| 334 | assistantDeliveryMode: "streaming", | 336 | assistantDeliveryMode: "streaming", |
| 337 | approvalPolicy: "on-request", | ||
| 338 | sandboxMode: "workspace-write", | ||
| 335 | createdAt: now, | 339 | createdAt: now, |
| 336 | }), | 340 | }), |
| 337 | ); | 341 | ); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+8-12| ... | @@ -3,6 +3,8 @@ import { | ... | @@ -3,6 +3,8 @@ import { |
| 3 | CommandId, | 3 | CommandId, |
| 4 | MessageId, | 4 | MessageId, |
| 5 | type OrchestrationEvent, | 5 | type OrchestrationEvent, |
| 6 | type ProviderApprovalPolicy, | ||
| 7 | type ProviderSandboxMode, | ||
| 6 | ProviderThreadId, | 8 | ProviderThreadId, |
| 7 | type ThreadId, | 9 | type ThreadId, |
| 8 | TurnId, | 10 | TurnId, |
| ... | @@ -24,6 +26,8 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId | ... | @@ -24,6 +26,8 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId |
| 24 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); | 26 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); |
| 25 | 27 | ||
| 26 | const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered"; | 28 | const DEFAULT_ASSISTANT_DELIVERY_MODE: AssistantDeliveryMode = "buffered"; |
| 29 | const DEFAULT_APPROVAL_POLICY: ProviderApprovalPolicy = "never"; | ||
| 30 | const DEFAULT_SANDBOX_MODE: ProviderSandboxMode = "workspace-write"; | ||
| 27 | const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; | 31 | const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; |
| 28 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); | 32 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); |
| 29 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; | 33 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; |
| ... | @@ -371,12 +375,8 @@ const make = Effect.gen(function* () { | ... | @@ -371,12 +375,8 @@ const make = Effect.gen(function* () { |
| 371 | providerName: event.provider, | 375 | providerName: event.provider, |
| 372 | providerSessionId: event.sessionId, | 376 | providerSessionId: event.sessionId, |
| 373 | providerThreadId, | 377 | providerThreadId, |
| 374 | ...(thread.session?.approvalPolicy !== undefined | 378 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, |
| 375 | ? { approvalPolicy: thread.session.approvalPolicy } | 379 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, |
| 376 | : {}), | ||
| 377 | ...(thread.session?.sandboxMode !== undefined | ||
| 378 | ? { sandboxMode: thread.session.sandboxMode } | ||
| 379 | : {}), | ||
| 380 | activeTurnId, | 380 | activeTurnId, |
| 381 | lastError, | 381 | lastError, |
| 382 | updatedAt: now, | 382 | updatedAt: now, |
| ... | @@ -485,12 +485,8 @@ const make = Effect.gen(function* () { | ... | @@ -485,12 +485,8 @@ const make = Effect.gen(function* () { |
| 485 | providerName: event.provider, | 485 | providerName: event.provider, |
| 486 | providerSessionId: event.sessionId, | 486 | providerSessionId: event.sessionId, |
| 487 | providerThreadId, | 487 | providerThreadId, |
| 488 | ...(thread.session?.approvalPolicy !== undefined | 488 | approvalPolicy: thread.session?.approvalPolicy ?? DEFAULT_APPROVAL_POLICY, |
| 489 | ? { approvalPolicy: thread.session.approvalPolicy } | 489 | sandboxMode: thread.session?.sandboxMode ?? DEFAULT_SANDBOX_MODE, |
| 490 | : {}), | ||
| 491 | ...(thread.session?.sandboxMode !== undefined | ||
| 492 | ? { sandboxMode: thread.session.sandboxMode } | ||
| 493 | : {}), | ||
| 494 | activeTurnId: toTurnId(event.turnId) ?? null, | 490 | activeTurnId: toTurnId(event.turnId) ?? null, |
| 495 | lastError: event.message, | 491 | lastError: event.message, |
| 496 | updatedAt: now, | 492 | updatedAt: now, |
apps/server/src/orchestration/commandInvariants.test.ts+2| ... | @@ -90,6 +90,8 @@ const messageSendCommand: OrchestrationCommand = { | ... | @@ -90,6 +90,8 @@ const messageSendCommand: OrchestrationCommand = { |
| 90 | text: "hello", | 90 | text: "hello", |
| 91 | attachments: [], | 91 | attachments: [], |
| 92 | }, | 92 | }, |
| 93 | approvalPolicy: "on-request", | ||
| 94 | sandboxMode: "workspace-write", | ||
| 93 | createdAt: now, | 95 | createdAt: now, |
| 94 | }; | 96 | }; |
| 95 | 97 |
apps/server/src/orchestration/decider.ts+2-4| ... | @@ -254,10 +254,8 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -254,10 +254,8 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 254 | ...(command.model !== undefined ? { model: command.model } : {}), | 254 | ...(command.model !== undefined ? { model: command.model } : {}), |
| 255 | ...(command.effort !== undefined ? { effort: command.effort } : {}), | 255 | ...(command.effort !== undefined ? { effort: command.effort } : {}), |
| 256 | assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, | 256 | assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, |
| 257 | ...(command.approvalPolicy !== undefined | 257 | approvalPolicy: command.approvalPolicy, |
| 258 | ? { approvalPolicy: command.approvalPolicy } | 258 | sandboxMode: command.sandboxMode, |
| 259 | : {}), | ||
| 260 | ...(command.sandboxMode !== undefined ? { sandboxMode: command.sandboxMode } : {}), | ||
| 261 | createdAt: command.createdAt, | 259 | createdAt: command.createdAt, |
| 262 | }, | 260 | }, |
| 263 | }; | 261 | }; |
apps/server/src/orchestration/projector.test.ts+4| ... | @@ -135,6 +135,8 @@ describe("orchestration projector", () => { | ... | @@ -135,6 +135,8 @@ describe("orchestration projector", () => { |
| 135 | payload: { | 135 | payload: { |
| 136 | threadId: "thread-1", | 136 | threadId: "thread-1", |
| 137 | messageId: "message-1", | 137 | messageId: "message-1", |
| 138 | approvalPolicy: "on-request", | ||
| 139 | sandboxMode: "workspace-write", | ||
| 138 | createdAt: "2026-01-01T00:00:00.000Z", | 140 | createdAt: "2026-01-01T00:00:00.000Z", |
| 139 | }, | 141 | }, |
| 140 | }), | 142 | }), |
| ... | @@ -193,6 +195,8 @@ describe("orchestration projector", () => { | ... | @@ -193,6 +195,8 @@ describe("orchestration projector", () => { |
| 193 | providerName: "codex", | 195 | providerName: "codex", |
| 194 | providerSessionId: "session-1", | 196 | providerSessionId: "session-1", |
| 195 | providerThreadId: "provider-thread-1", | 197 | providerThreadId: "provider-thread-1", |
| 198 | approvalPolicy: "on-request", | ||
| 199 | sandboxMode: "workspace-write", | ||
| 196 | activeTurnId: "turn-1", | 200 | activeTurnId: "turn-1", |
| 197 | lastError: null, | 201 | lastError: null, |
| 198 | updatedAt: startedAt, | 202 | updatedAt: startedAt, |
apps/server/src/persistence/Layers/ProjectionThreadSessions.ts+8| ... | @@ -25,6 +25,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -25,6 +25,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 25 | provider_name, | 25 | provider_name, |
| 26 | provider_session_id, | 26 | provider_session_id, |
| 27 | provider_thread_id, | 27 | provider_thread_id, |
| 28 | approval_policy, | ||
| 29 | sandbox_mode, | ||
| 28 | active_turn_id, | 30 | active_turn_id, |
| 29 | last_error, | 31 | last_error, |
| 30 | updated_at | 32 | updated_at |
| ... | @@ -35,6 +37,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -35,6 +37,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 35 | ${row.providerName}, | 37 | ${row.providerName}, |
| 36 | ${row.providerSessionId}, | 38 | ${row.providerSessionId}, |
| 37 | ${row.providerThreadId}, | 39 | ${row.providerThreadId}, |
| 40 | ${row.approvalPolicy}, | ||
| 41 | ${row.sandboxMode}, | ||
| 38 | ${row.activeTurnId}, | 42 | ${row.activeTurnId}, |
| 39 | ${row.lastError}, | 43 | ${row.lastError}, |
| 40 | ${row.updatedAt} | 44 | ${row.updatedAt} |
| ... | @@ -45,6 +49,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -45,6 +49,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 45 | provider_name = excluded.provider_name, | 49 | provider_name = excluded.provider_name, |
| 46 | provider_session_id = excluded.provider_session_id, | 50 | provider_session_id = excluded.provider_session_id, |
| 47 | provider_thread_id = excluded.provider_thread_id, | 51 | provider_thread_id = excluded.provider_thread_id, |
| 52 | approval_policy = excluded.approval_policy, | ||
| 53 | sandbox_mode = excluded.sandbox_mode, | ||
| 48 | active_turn_id = excluded.active_turn_id, | 54 | active_turn_id = excluded.active_turn_id, |
| 49 | last_error = excluded.last_error, | 55 | last_error = excluded.last_error, |
| 50 | updated_at = excluded.updated_at | 56 | updated_at = excluded.updated_at |
| ... | @@ -62,6 +68,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { | ... | @@ -62,6 +68,8 @@ const makeProjectionThreadSessionRepository = Effect.gen(function* () { |
| 62 | provider_name AS "providerName", | 68 | provider_name AS "providerName", |
| 63 | provider_session_id AS "providerSessionId", | 69 | provider_session_id AS "providerSessionId", |
| 64 | provider_thread_id AS "providerThreadId", | 70 | provider_thread_id AS "providerThreadId", |
| 71 | approval_policy AS "approvalPolicy", | ||
| 72 | sandbox_mode AS "sandboxMode", | ||
| 65 | active_turn_id AS "activeTurnId", | 73 | active_turn_id AS "activeTurnId", |
| 66 | last_error AS "lastError", | 74 | last_error AS "lastError", |
| 67 | updated_at AS "updatedAt" | 75 | updated_at AS "updatedAt" |
apps/server/src/persistence/Migrations/005_Projections.ts+2| ... | @@ -65,6 +65,8 @@ export default Effect.gen(function* () { | ... | @@ -65,6 +65,8 @@ export default Effect.gen(function* () { |
| 65 | provider_name TEXT, | 65 | provider_name TEXT, |
| 66 | provider_session_id TEXT, | 66 | provider_session_id TEXT, |
| 67 | provider_thread_id TEXT, | 67 | provider_thread_id TEXT, |
| 68 | approval_policy TEXT NOT NULL, | ||
| 69 | sandbox_mode TEXT NOT NULL, | ||
| 68 | active_turn_id TEXT, | 70 | active_turn_id TEXT, |
| 69 | last_error TEXT, | 71 | last_error TEXT, |
| 70 | updated_at TEXT NOT NULL | 72 | updated_at TEXT NOT NULL |
apps/server/src/persistence/Services/ProjectionThreadSessions.ts+4| ... | @@ -9,6 +9,8 @@ | ... | @@ -9,6 +9,8 @@ |
| 9 | import { | 9 | import { |
| 10 | IsoDateTime, | 10 | IsoDateTime, |
| 11 | OrchestrationSessionStatus, | 11 | OrchestrationSessionStatus, |
| 12 | ProviderApprovalPolicy, | ||
| 13 | ProviderSandboxMode, | ||
| 12 | ProviderSessionId, | 14 | ProviderSessionId, |
| 13 | ProviderThreadId, | 15 | ProviderThreadId, |
| 14 | ThreadId, | 16 | ThreadId, |
| ... | @@ -25,6 +27,8 @@ export const ProjectionThreadSession = Schema.Struct({ | ... | @@ -25,6 +27,8 @@ export const ProjectionThreadSession = Schema.Struct({ |
| 25 | providerName: Schema.NullOr(Schema.String), | 27 | providerName: Schema.NullOr(Schema.String), |
| 26 | providerSessionId: Schema.NullOr(ProviderSessionId), | 28 | providerSessionId: Schema.NullOr(ProviderSessionId), |
| 27 | providerThreadId: Schema.NullOr(ProviderThreadId), | 29 | providerThreadId: Schema.NullOr(ProviderThreadId), |
| 30 | approvalPolicy: ProviderApprovalPolicy, | ||
| 31 | sandboxMode: ProviderSandboxMode, | ||
| 28 | activeTurnId: Schema.NullOr(TurnId), | 32 | activeTurnId: Schema.NullOr(TurnId), |
| 29 | lastError: Schema.NullOr(Schema.String), | 33 | lastError: Schema.NullOr(Schema.String), |
| 30 | updatedAt: IsoDateTime, | 34 | updatedAt: IsoDateTime, |
apps/server/src/wsServer.test.ts+2| ... | @@ -1055,6 +1055,8 @@ describe("WebSocket Server", () => { | ... | @@ -1055,6 +1055,8 @@ describe("WebSocket Server", () => { |
| 1055 | attachments: [], | 1055 | attachments: [], |
| 1056 | }, | 1056 | }, |
| 1057 | assistantDeliveryMode: "streaming", | 1057 | assistantDeliveryMode: "streaming", |
| 1058 | approvalPolicy: "on-request", | ||
| 1059 | sandboxMode: "workspace-write", | ||
| 1058 | createdAt, | 1060 | createdAt, |
| 1059 | }); | 1061 | }); |
| 1060 | expect(startTurnResponse.error).toBeUndefined(); | 1062 | expect(startTurnResponse.error).toBeUndefined(); |
packages/contracts/src/orchestration.ts+6-6| ... | @@ -142,8 +142,8 @@ export const OrchestrationSession = Schema.Struct({ | ... | @@ -142,8 +142,8 @@ export const OrchestrationSession = Schema.Struct({ |
| 142 | providerName: Schema.NullOr(TrimmedNonEmptyString), | 142 | providerName: Schema.NullOr(TrimmedNonEmptyString), |
| 143 | providerSessionId: Schema.NullOr(ProviderSessionId), | 143 | providerSessionId: Schema.NullOr(ProviderSessionId), |
| 144 | providerThreadId: Schema.NullOr(ProviderThreadId), | 144 | providerThreadId: Schema.NullOr(ProviderThreadId), |
| 145 | approvalPolicy: Schema.optional(ProviderApprovalPolicy), | 145 | approvalPolicy: ProviderApprovalPolicy, |
| 146 | sandboxMode: Schema.optional(ProviderSandboxMode), | 146 | sandboxMode: ProviderSandboxMode, |
| 147 | activeTurnId: Schema.NullOr(TurnId), | 147 | activeTurnId: Schema.NullOr(TurnId), |
| 148 | lastError: Schema.NullOr(TrimmedNonEmptyString), | 148 | lastError: Schema.NullOr(TrimmedNonEmptyString), |
| 149 | updatedAt: IsoDateTime, | 149 | updatedAt: IsoDateTime, |
| ... | @@ -284,8 +284,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ | ... | @@ -284,8 +284,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ |
| 284 | model: Schema.optional(TrimmedNonEmptyString), | 284 | model: Schema.optional(TrimmedNonEmptyString), |
| 285 | effort: Schema.optional(TrimmedNonEmptyString), | 285 | effort: Schema.optional(TrimmedNonEmptyString), |
| 286 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), | 286 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 287 | approvalPolicy: Schema.optional(ProviderApprovalPolicy), | 287 | approvalPolicy: ProviderApprovalPolicy, |
| 288 | sandboxMode: Schema.optional(ProviderSandboxMode), | 288 | sandboxMode: ProviderSandboxMode, |
| 289 | createdAt: IsoDateTime, | 289 | createdAt: IsoDateTime, |
| 290 | }); | 290 | }); |
| 291 | 291 | ||
| ... | @@ -500,8 +500,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ | ... | @@ -500,8 +500,8 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ |
| 500 | model: Schema.optional(TrimmedNonEmptyString), | 500 | model: Schema.optional(TrimmedNonEmptyString), |
| 501 | effort: Schema.optional(TrimmedNonEmptyString), | 501 | effort: Schema.optional(TrimmedNonEmptyString), |
| 502 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), | 502 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 503 | approvalPolicy: Schema.optional(ProviderApprovalPolicy), | 503 | approvalPolicy: ProviderApprovalPolicy, |
| 504 | sandboxMode: Schema.optional(ProviderSandboxMode), | 504 | sandboxMode: ProviderSandboxMode, |
| 505 | createdAt: IsoDateTime, | 505 | createdAt: IsoDateTime, |
| 506 | }); | 506 | }); |
| 507 | 507 |