From 840ea7b0557490c4eef9a4a833cd99ec086af7fb Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 16:20:28 +0530 Subject: [PATCH 01/13] fix(orchestration): persist plan implementation state --- .../Layers/ProjectionPipeline.ts | 2 + .../Layers/ProjectionSnapshotQuery.test.ts | 36 ++- .../Layers/ProjectionSnapshotQuery.ts | 4 + .../Layers/ProviderRuntimeIngestion.ts | 6 + .../decider.projectScripts.test.ts | 251 ++++++++++++++++++ apps/server/src/orchestration/decider.ts | 53 +++- .../Layers/ProjectionThreadProposedPlans.ts | 8 + apps/server/src/persistence/Migrations.ts | 2 + ...jectionThreadProposedPlanImplementation.ts | 16 ++ .../Services/ProjectionThreadProposedPlans.ts | 2 + packages/contracts/src/orchestration.test.ts | 57 ++++ packages/contracts/src/orchestration.ts | 9 + 12 files changed, 437 insertions(+), 9 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/014_ProjectionThreadProposedPlanImplementation.ts diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 6ae94105a..4120bfe71 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -653,6 +653,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { threadId: event.payload.threadId, turnId: event.payload.proposedPlan.turnId, planMarkdown: event.payload.proposedPlan.planMarkdown, + implementedAt: event.payload.proposedPlan.implementedAt, + implementationThreadId: event.payload.proposedPlan.implementationThreadId, createdAt: event.payload.proposedPlan.createdAt, updatedAt: event.payload.proposedPlan.updatedAt, }); diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index fc7db5480..9bfe581de 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -26,6 +26,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { yield* sql`DELETE FROM projection_projects`; yield* sql`DELETE FROM projection_state`; + yield* sql`DELETE FROM projection_thread_proposed_plans`; yield* sql`DELETE FROM projection_turns`; yield* sql` @@ -101,6 +102,29 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { ) `; + yield* sql` + INSERT INTO projection_thread_proposed_plans ( + plan_id, + thread_id, + turn_id, + plan_markdown, + implemented_at, + implementation_thread_id, + created_at, + updated_at + ) + VALUES ( + 'plan-1', + 'thread-1', + 'turn-1', + '# Ship it', + '2026-02-24T00:00:05.500Z', + 'thread-2', + '2026-02-24T00:00:05.000Z', + '2026-02-24T00:00:05.500Z' + ) + `; + yield* sql` INSERT INTO projection_thread_activities ( activity_id, @@ -253,7 +277,17 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { updatedAt: "2026-02-24T00:00:05.000Z", }, ], - proposedPlans: [], + proposedPlans: [ + { + id: "plan-1", + turnId: asTurnId("turn-1"), + planMarkdown: "# Ship it", + implementedAt: "2026-02-24T00:00:05.500Z", + implementationThreadId: ThreadId.makeUnsafe("thread-2"), + createdAt: "2026-02-24T00:00:05.000Z", + updatedAt: "2026-02-24T00:00:05.500Z", + }, + ], activities: [ { id: asEventId("activity-1"), diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 5fd38a540..1b5134fbc 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -200,6 +200,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { thread_id AS "threadId", turn_id AS "turnId", plan_markdown AS "planMarkdown", + implemented_at AS "implementedAt", + implementation_thread_id AS "implementationThreadId", created_at AS "createdAt", updated_at AS "updatedAt" FROM projection_thread_proposed_plans @@ -435,6 +437,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { id: row.planId, turnId: row.turnId, planMarkdown: row.planMarkdown, + implementedAt: row.implementedAt, + implementationThreadId: row.implementationThreadId, createdAt: row.createdAt, updatedAt: row.updatedAt, }); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 417e93c8d..60db85400 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -671,6 +671,8 @@ const make = Effect.gen(function* () { threadProposedPlans: ReadonlyArray<{ id: string; createdAt: string; + implementedAt: string | null; + implementationThreadId: ThreadId | null; }>; planId: string; turnId?: TurnId; @@ -693,6 +695,8 @@ const make = Effect.gen(function* () { id: input.planId, turnId: input.turnId ?? null, planMarkdown, + implementedAt: existingPlan?.implementedAt ?? null, + implementationThreadId: existingPlan?.implementationThreadId ?? null, createdAt: existingPlan?.createdAt ?? input.createdAt, updatedAt: input.updatedAt, }, @@ -706,6 +710,8 @@ const make = Effect.gen(function* () { threadProposedPlans: ReadonlyArray<{ id: string; createdAt: string; + implementedAt: string | null; + implementationThreadId: ThreadId | null; }>; planId: string; turnId?: TurnId; diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index 516d8b2a2..d0bc7d752 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -5,6 +5,7 @@ import { MessageId, ProjectId, ThreadId, + TurnId, } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; import { Effect } from "effect"; @@ -201,6 +202,256 @@ describe("decider project scripts", () => { }); }); + it("marks the source proposed plan implemented when starting a new implementation thread", async () => { + const now = new Date().toISOString(); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const withSourceThread = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create-source"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-plan"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-source"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-plan"), + projectId: asProjectId("project-1"), + title: "Plan Thread", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + const withTargetThread = await Effect.runPromise( + projectEvent(withSourceThread, { + sequence: 3, + eventId: asEventId("evt-thread-create-target"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-implement"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-target"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-implement"), + projectId: asProjectId("project-1"), + title: "Implementation Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withTargetThread, { + sequence: 4, + eventId: asEventId("evt-plan-upsert"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-plan"), + type: "thread.proposed-plan-upserted", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-plan-upsert"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-plan-upsert"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-plan"), + proposedPlan: { + id: "plan-1", + turnId: TurnId.makeUnsafe("turn-1"), + planMarkdown: "# Plan", + implementedAt: null, + implementationThreadId: null, + createdAt: now, + updatedAt: now, + }, + }, + }), + ); + + const result = await Effect.runPromise( + decideOrchestrationCommand({ + command: { + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-source-plan"), + threadId: ThreadId.makeUnsafe("thread-implement"), + message: { + messageId: asMessageId("message-user-2"), + role: "user", + text: "PLEASE IMPLEMENT THIS PLAN:\n# Plan", + attachments: [], + }, + sourceProposedPlan: { + threadId: ThreadId.makeUnsafe("thread-plan"), + planId: "plan-1", + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }, + readModel, + }), + ); + + expect(Array.isArray(result)).toBe(true); + const events = Array.isArray(result) ? result : [result]; + expect(events).toHaveLength(3); + expect(events[2]?.type).toBe("thread.proposed-plan-upserted"); + expect(events[2]?.aggregateId).toBe("thread-plan"); + if (events[2]?.type !== "thread.proposed-plan-upserted") { + return; + } + expect(events[2].payload.proposedPlan).toMatchObject({ + id: "plan-1", + implementedAt: now, + implementationThreadId: "thread-implement", + }); + }); + + it("rejects thread.turn.start when the source proposed plan is missing", async () => { + const now = new Date().toISOString(); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const withSourceThread = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create-source"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-plan"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-source"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-plan"), + projectId: asProjectId("project-1"), + title: "Plan Thread", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withSourceThread, { + sequence: 3, + eventId: asEventId("evt-thread-create-target"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-implement"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-target"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-implement"), + projectId: asProjectId("project-1"), + title: "Implementation Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + + await expect( + Effect.runPromise( + decideOrchestrationCommand({ + command: { + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-missing-source-plan"), + threadId: ThreadId.makeUnsafe("thread-implement"), + message: { + messageId: asMessageId("message-user-3"), + role: "user", + text: "PLEASE IMPLEMENT THIS PLAN:\n# Missing", + attachments: [], + }, + sourceProposedPlan: { + threadId: ThreadId.makeUnsafe("thread-plan"), + planId: "plan-missing", + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }, + readModel, + }), + ), + ).rejects.toThrow("Proposed plan 'plan-missing' does not exist on thread 'thread-plan'."); + }); + it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => { const now = new Date().toISOString(); const initial = createEmptyReadModel(now); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index eea41a2b3..677290c2c 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -262,11 +262,29 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } case "thread.turn.start": { - yield* requireThread({ + const targetThread = yield* requireThread({ readModel, command, threadId: command.threadId, }); + const sourceProposedPlan = command.sourceProposedPlan; + const sourceThread = sourceProposedPlan + ? yield* requireThread({ + readModel, + command, + threadId: sourceProposedPlan.threadId, + }) + : null; + const sourcePlan = + sourceProposedPlan && sourceThread + ? sourceThread.proposedPlans.find((entry) => entry.id === sourceProposedPlan.planId) + : null; + if (sourceProposedPlan && !sourcePlan) { + return yield* new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Proposed plan '${sourceProposedPlan.planId}' does not exist on thread '${sourceProposedPlan.threadId}'.`, + }); + } const userMessageEvent: Omit = { ...withEventBase({ aggregateKind: "thread", @@ -306,16 +324,35 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ? { providerOptions: command.providerOptions } : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, - runtimeMode: - readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ?? - command.runtimeMode, - interactionMode: - readModel.threads.find((entry) => entry.id === command.threadId)?.interactionMode ?? - command.interactionMode, + runtimeMode: targetThread.runtimeMode, + interactionMode: targetThread.interactionMode, createdAt: command.createdAt, }, }; - return [userMessageEvent, turnStartRequestedEvent]; + if (!sourcePlan || !sourceThread) { + return [userMessageEvent, turnStartRequestedEvent]; + } + + const sourcePlanUpsertEvent: Omit = { + ...withEventBase({ + aggregateKind: "thread", + aggregateId: sourceThread.id, + occurredAt: command.createdAt, + commandId: command.commandId, + }), + causationEventId: turnStartRequestedEvent.eventId, + type: "thread.proposed-plan-upserted", + payload: { + threadId: sourceThread.id, + proposedPlan: { + ...sourcePlan, + implementedAt: command.createdAt, + implementationThreadId: command.threadId, + updatedAt: command.createdAt, + }, + }, + }; + return [userMessageEvent, turnStartRequestedEvent, sourcePlanUpsertEvent]; } case "thread.turn.interrupt": { diff --git a/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts b/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts index 3d103592f..ccd322feb 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts @@ -22,6 +22,8 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () { thread_id, turn_id, plan_markdown, + implemented_at, + implementation_thread_id, created_at, updated_at ) @@ -30,6 +32,8 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () { ${row.threadId}, ${row.turnId}, ${row.planMarkdown}, + ${row.implementedAt}, + ${row.implementationThreadId}, ${row.createdAt}, ${row.updatedAt} ) @@ -38,6 +42,8 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () { thread_id = excluded.thread_id, turn_id = excluded.turn_id, plan_markdown = excluded.plan_markdown, + implemented_at = excluded.implemented_at, + implementation_thread_id = excluded.implementation_thread_id, created_at = excluded.created_at, updated_at = excluded.updated_at `, @@ -52,6 +58,8 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () { thread_id AS "threadId", turn_id AS "turnId", plan_markdown AS "planMarkdown", + implemented_at AS "implementedAt", + implementation_thread_id AS "implementationThreadId", created_at AS "createdAt", updated_at AS "updatedAt" FROM projection_thread_proposed_plans diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 7deb890dd..90c52289a 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -25,6 +25,7 @@ import Migration0010 from "./Migrations/010_ProjectionThreadsRuntimeMode.ts"; import Migration0011 from "./Migrations/011_OrchestrationThreadCreatedRuntimeMode.ts"; import Migration0012 from "./Migrations/012_ProjectionThreadsInteractionMode.ts"; import Migration0013 from "./Migrations/013_ProjectionThreadProposedPlans.ts"; +import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplementation.ts"; import { Effect } from "effect"; /** @@ -51,6 +52,7 @@ const loader = Migrator.fromRecord({ "11_OrchestrationThreadCreatedRuntimeMode": Migration0011, "12_ProjectionThreadsInteractionMode": Migration0012, "13_ProjectionThreadProposedPlans": Migration0013, + "14_ProjectionThreadProposedPlanImplementation": Migration0014, }); /** diff --git a/apps/server/src/persistence/Migrations/014_ProjectionThreadProposedPlanImplementation.ts b/apps/server/src/persistence/Migrations/014_ProjectionThreadProposedPlanImplementation.ts new file mode 100644 index 000000000..c7a82bfb3 --- /dev/null +++ b/apps/server/src/persistence/Migrations/014_ProjectionThreadProposedPlanImplementation.ts @@ -0,0 +1,16 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + ALTER TABLE projection_thread_proposed_plans + ADD COLUMN implemented_at TEXT + `; + + yield* sql` + ALTER TABLE projection_thread_proposed_plans + ADD COLUMN implementation_thread_id TEXT + `; +}); diff --git a/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts b/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts index ee662d52b..d141a11bb 100644 --- a/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts +++ b/apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts @@ -15,6 +15,8 @@ export const ProjectionThreadProposedPlan = Schema.Struct({ threadId: ThreadId, turnId: Schema.NullOr(TurnId), planMarkdown: TrimmedNonEmptyString, + implementedAt: Schema.NullOr(IsoDateTime), + implementationThreadId: Schema.NullOr(ThreadId), createdAt: IsoDateTime, updatedAt: IsoDateTime, }); diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index 25a641edb..158ddd856 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -6,6 +6,7 @@ import { DEFAULT_PROVIDER_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, OrchestrationGetTurnDiffInput, + OrchestrationProposedPlan, OrchestrationSession, ProjectCreateCommand, ThreadTurnStartCommand, @@ -21,6 +22,7 @@ const decodeThreadTurnStartCommand = Schema.decodeUnknownEffect(ThreadTurnStartC const decodeThreadTurnStartRequestedPayload = Schema.decodeUnknownEffect( ThreadTurnStartRequestedPayload, ); +const decodeOrchestrationProposedPlan = Schema.decodeUnknownEffect(OrchestrationProposedPlan); const decodeOrchestrationSession = Schema.decodeUnknownEffect(OrchestrationSession); const decodeThreadCreatedPayload = Schema.decodeUnknownEffect(ThreadCreatedPayload); @@ -186,6 +188,31 @@ it.effect("accepts provider-scoped model options in thread.turn.start", () => }), ); +it.effect("accepts a source proposed plan reference in thread.turn.start", () => + Effect.gen(function* () { + const parsed = yield* decodeThreadTurnStartCommand({ + type: "thread.turn.start", + commandId: "cmd-turn-source-plan", + threadId: "thread-2", + message: { + messageId: "msg-source-plan", + role: "user", + text: "implement this", + attachments: [], + }, + sourceProposedPlan: { + threadId: "thread-1", + planId: "plan-1", + }, + createdAt: "2026-01-01T00:00:00.000Z", + }); + assert.deepStrictEqual(parsed.sourceProposedPlan, { + threadId: "thread-1", + planId: "plan-1", + }); + }), +); + it.effect( "decodes thread.turn-start-requested defaults for provider, runtime mode, and interaction mode", () => @@ -216,3 +243,33 @@ it.effect("decodes orchestration session runtime mode defaults", () => assert.strictEqual(parsed.runtimeMode, DEFAULT_RUNTIME_MODE); }), ); + +it.effect("defaults proposed plan implementation metadata for historical rows", () => + Effect.gen(function* () { + const parsed = yield* decodeOrchestrationProposedPlan({ + id: "plan-1", + turnId: "turn-1", + planMarkdown: "# Plan", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }); + assert.strictEqual(parsed.implementedAt, null); + assert.strictEqual(parsed.implementationThreadId, null); + }), +); + +it.effect("preserves proposed plan implementation metadata when present", () => + Effect.gen(function* () { + const parsed = yield* decodeOrchestrationProposedPlan({ + id: "plan-2", + turnId: "turn-2", + planMarkdown: "# Plan", + implementedAt: "2026-01-02T00:00:00.000Z", + implementationThreadId: "thread-2", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-02T00:00:00.000Z", + }); + assert.strictEqual(parsed.implementedAt, "2026-01-02T00:00:00.000Z"); + assert.strictEqual(parsed.implementationThreadId, "thread-2"); + }), +); diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 17c5eb21d..7ae60af3e 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -163,11 +163,18 @@ export const OrchestrationProposedPlan = Schema.Struct({ id: OrchestrationProposedPlanId, turnId: Schema.NullOr(TurnId), planMarkdown: TrimmedNonEmptyString, + implementedAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(() => null)), + implementationThreadId: Schema.NullOr(ThreadId).pipe(Schema.withDecodingDefault(() => null)), createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export type OrchestrationProposedPlan = typeof OrchestrationProposedPlan.Type; +const SourceProposedPlanReference = Schema.Struct({ + threadId: ThreadId, + planId: OrchestrationProposedPlanId, +}); + export const OrchestrationSessionStatus = Schema.Literals([ "idle", "starting", @@ -374,6 +381,7 @@ export const ThreadTurnStartCommand = Schema.Struct({ interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(() => DEFAULT_PROVIDER_INTERACTION_MODE), ), + sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, }); @@ -394,6 +402,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({ assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, + sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, }); From 6efed9b7361da0edab62c69c59e2e154f74c6a35 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 16:20:38 +0530 Subject: [PATCH 02/13] fix(web): clear plan ready after cross-thread implementation --- apps/web/src/components/ChatView.browser.tsx | 63 +++++++++++++++++++ apps/web/src/components/ChatView.tsx | 7 ++- apps/web/src/components/Sidebar.logic.test.ts | 31 +++++++++ apps/web/src/components/Sidebar.logic.ts | 10 ++- apps/web/src/session-logic.test.ts | 55 +++++++++++++++- apps/web/src/session-logic.ts | 13 ++++ apps/web/src/store.ts | 2 + apps/web/src/types.ts | 2 + 8 files changed, 179 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index faecc7f51..df0c7d49d 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -8,6 +8,7 @@ import { type ProjectId, type ServerConfig, type ThreadId, + type TurnId, type WsWelcomePayload, WS_CHANNELS, WS_METHODS, @@ -345,6 +346,8 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { id: "plan-browser-test", turnId: null, planMarkdown, + implementedAt: null, + implementationThreadId: null, createdAt: isoAt(1_000), updatedAt: isoAt(1_001), }, @@ -356,6 +359,44 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { }; } +function createSnapshotWithImplementedProposedPlan(): OrchestrationReadModel { + const snapshot = createSnapshotForTargetUser({ + targetMessageId: "msg-user-plan-implemented" as MessageId, + targetText: "implemented plan thread", + }); + + return { + ...snapshot, + threads: snapshot.threads.map((thread) => + thread.id === THREAD_ID + ? Object.assign({}, thread, { + interactionMode: "plan" as const, + latestTurn: { + turnId: "turn-plan-implemented" as TurnId, + state: "completed" as const, + assistantMessageId: null, + requestedAt: isoAt(900), + startedAt: isoAt(901), + completedAt: isoAt(902), + }, + proposedPlans: [ + { + id: "plan-browser-implemented", + turnId: "turn-plan-implemented" as TurnId, + planMarkdown: "# Already implemented", + implementedAt: isoAt(903), + implementationThreadId: "thread-implement-copy" as ThreadId, + createdAt: isoAt(900), + updatedAt: isoAt(903), + }, + ], + updatedAt: isoAt(903), + }) + : thread, + ), + }; +} + function resolveWsRpc(body: WsRequestEnvelope["body"]): unknown { const tag = body._tag; if (tag === ORCHESTRATION_WS_METHODS.getSnapshot) { @@ -1247,4 +1288,26 @@ describe("ChatView timeline estimator parity (full app)", () => { await mounted.cleanup(); } }); + + it("hides plan follow-up actions after the proposed plan was implemented in another thread", async () => { + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotWithImplementedProposedPlan(), + }); + + try { + await vi.waitFor( + () => { + const buttonLabels = Array.from(document.querySelectorAll("button")).map((button) => + button.textContent?.trim(), + ); + expect(buttonLabels).not.toContain("Implement in new thread"); + expect(buttonLabels).not.toContain("Refine"); + }, + { timeout: 8_000, interval: 16 }, + ); + } finally { + await mounted.cleanup(); + } + }); }); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 52637695e..2d646a4cf 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -55,6 +55,7 @@ import { deriveActivePlanState, findLatestProposedPlan, deriveWorkLogEntries, + hasActionableProposedPlan, hasToolActivityForTurn, isLatestTurnSettled, formatElapsed, @@ -643,7 +644,7 @@ export default function ChatView({ threadId }: ChatViewProps) { pendingUserInputs.length === 0 && interactionMode === "plan" && latestTurnSettled && - activeProposedPlan !== null; + hasActionableProposedPlan(activeProposedPlan); const activePendingApproval = pendingApprovals[0] ?? null; const isComposerApprovalState = activePendingApproval !== null; const hasComposerHeader = @@ -2818,6 +2819,10 @@ export default function ChatView({ threadId }: ChatViewProps) { assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: "default", + sourceProposedPlan: { + threadId: activeThread.id, + planId: activeProposedPlan.id, + }, createdAt, }); }) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index a8f84d564..8c3b47010 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -141,6 +141,8 @@ describe("resolveThreadStatusPill", () => { createdAt: "2026-03-09T10:00:00.000Z", updatedAt: "2026-03-09T10:05:00.000Z", planMarkdown: "# Plan", + implementedAt: null, + implementationThreadId: null, }, ], session: { @@ -155,6 +157,35 @@ describe("resolveThreadStatusPill", () => { ).toMatchObject({ label: "Plan Ready", pulse: false }); }); + it("does not show plan ready after the proposed plan was implemented elsewhere", () => { + expect( + resolveThreadStatusPill({ + thread: { + ...baseThread, + latestTurn: makeLatestTurn(), + proposedPlans: [ + { + id: "plan-1" as never, + turnId: "turn-1" as never, + createdAt: "2026-03-09T10:00:00.000Z", + updatedAt: "2026-03-09T10:05:00.000Z", + planMarkdown: "# Plan", + implementedAt: "2026-03-09T10:06:00.000Z", + implementationThreadId: "thread-implement" as never, + }, + ], + session: { + ...baseThread.session, + status: "ready", + orchestrationStatus: "ready", + }, + }, + hasPendingApprovals: false, + hasPendingUserInput: false, + }), + ).toMatchObject({ label: "Completed", pulse: false }); + }); + it("shows completed when there is an unseen completion and no active blocker", () => { expect( resolveThreadStatusPill({ diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index f08ed212a..5c4a4cc95 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1,6 +1,10 @@ import type { Thread } from "../types"; import { cn } from "../lib/utils"; -import { findLatestProposedPlan, isLatestTurnSettled } from "../session-logic"; +import { + findLatestProposedPlan, + hasActionableProposedPlan, + isLatestTurnSettled, +} from "../session-logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; export type SidebarNewThreadEnvMode = "local" | "worktree"; @@ -124,7 +128,9 @@ export function resolveThreadStatusPill(input: { !hasPendingUserInput && thread.interactionMode === "plan" && isLatestTurnSettled(thread.latestTurn, thread.session) && - findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null) !== null; + hasActionableProposedPlan( + findLatestProposedPlan(thread.proposedPlans, thread.latestTurn?.turnId ?? null), + ); if (hasPlanReadyPrompt) { return { label: "Plan Ready", diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 74ba3a814..36c5b8619 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -1,4 +1,10 @@ -import { EventId, MessageId, TurnId, type OrchestrationThreadActivity } from "@t3tools/contracts"; +import { + EventId, + MessageId, + ThreadId, + TurnId, + type OrchestrationThreadActivity, +} from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; import { @@ -10,6 +16,7 @@ import { deriveTimelineEntries, deriveWorkLogEntries, findLatestProposedPlan, + hasActionableProposedPlan, hasToolActivityForTurn, isLatestTurnSettled, } from "./session-logic"; @@ -269,6 +276,8 @@ describe("findLatestProposedPlan", () => { id: "plan:thread-1:turn:turn-1", turnId: TurnId.makeUnsafe("turn-1"), planMarkdown: "# Older", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:01.000Z", updatedAt: "2026-02-23T00:00:01.000Z", }, @@ -276,6 +285,8 @@ describe("findLatestProposedPlan", () => { id: "plan:thread-1:turn:turn-1", turnId: TurnId.makeUnsafe("turn-1"), planMarkdown: "# Latest", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:01.000Z", updatedAt: "2026-02-23T00:00:02.000Z", }, @@ -283,6 +294,8 @@ describe("findLatestProposedPlan", () => { id: "plan:thread-1:turn:turn-2", turnId: TurnId.makeUnsafe("turn-2"), planMarkdown: "# Different turn", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:03.000Z", updatedAt: "2026-02-23T00:00:03.000Z", }, @@ -293,6 +306,8 @@ describe("findLatestProposedPlan", () => { id: "plan:thread-1:turn:turn-1", turnId: "turn-1", planMarkdown: "# Latest", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:01.000Z", updatedAt: "2026-02-23T00:00:02.000Z", }); @@ -305,6 +320,8 @@ describe("findLatestProposedPlan", () => { id: "plan:thread-1:turn:turn-1", turnId: TurnId.makeUnsafe("turn-1"), planMarkdown: "# First", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:01.000Z", updatedAt: "2026-02-23T00:00:01.000Z", }, @@ -312,6 +329,8 @@ describe("findLatestProposedPlan", () => { id: "plan:thread-1:turn:turn-2", turnId: TurnId.makeUnsafe("turn-2"), planMarkdown: "# Latest", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:02.000Z", updatedAt: "2026-02-23T00:00:03.000Z", }, @@ -323,6 +342,36 @@ describe("findLatestProposedPlan", () => { }); }); +describe("hasActionableProposedPlan", () => { + it("returns true for an unimplemented proposed plan", () => { + expect( + hasActionableProposedPlan({ + id: "plan-1", + turnId: TurnId.makeUnsafe("turn-1"), + planMarkdown: "# Plan", + implementedAt: null, + implementationThreadId: null, + createdAt: "2026-02-23T00:00:00.000Z", + updatedAt: "2026-02-23T00:00:01.000Z", + }), + ).toBe(true); + }); + + it("returns false for a proposed plan already implemented elsewhere", () => { + expect( + hasActionableProposedPlan({ + id: "plan-1", + turnId: TurnId.makeUnsafe("turn-1"), + planMarkdown: "# Plan", + implementedAt: "2026-02-23T00:00:02.000Z", + implementationThreadId: ThreadId.makeUnsafe("thread-implement"), + createdAt: "2026-02-23T00:00:00.000Z", + updatedAt: "2026-02-23T00:00:02.000Z", + }), + ).toBe(false); + }); +}); + describe("deriveWorkLogEntries", () => { it("omits tool started entries and keeps completed entries", () => { const activities: OrchestrationThreadActivity[] = [ @@ -531,6 +580,8 @@ describe("deriveTimelineEntries", () => { id: "plan:thread-1:turn:turn-1", turnId: TurnId.makeUnsafe("turn-1"), planMarkdown: "# Ship it", + implementedAt: null, + implementationThreadId: null, createdAt: "2026-02-23T00:00:02.000Z", updatedAt: "2026-02-23T00:00:02.000Z", }, @@ -550,6 +601,8 @@ describe("deriveTimelineEntries", () => { kind: "proposed-plan", proposedPlan: { planMarkdown: "# Ship it", + implementedAt: null, + implementationThreadId: null, }, }); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index e389f10e2..02042acd9 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -7,6 +7,7 @@ import { type ProviderKind, type ToolLifecycleItemType, type UserInputQuestion, + type ThreadId, type TurnId, } from "@t3tools/contracts"; @@ -72,6 +73,8 @@ export interface LatestProposedPlanState { updatedAt: string; turnId: TurnId | null; planMarkdown: string; + implementedAt: string | null; + implementationThreadId: ThreadId | null; } export type TimelineEntry = @@ -380,6 +383,8 @@ export function findLatestProposedPlan( updatedAt: matchingTurnPlan.updatedAt, turnId: matchingTurnPlan.turnId, planMarkdown: matchingTurnPlan.planMarkdown, + implementedAt: matchingTurnPlan.implementedAt, + implementationThreadId: matchingTurnPlan.implementationThreadId, }; } } @@ -400,9 +405,17 @@ export function findLatestProposedPlan( updatedAt: latestPlan.updatedAt, turnId: latestPlan.turnId, planMarkdown: latestPlan.planMarkdown, + implementedAt: latestPlan.implementedAt, + implementationThreadId: latestPlan.implementationThreadId, }; } +export function hasActionableProposedPlan( + proposedPlan: LatestProposedPlanState | Pick | null, +): boolean { + return proposedPlan !== null && proposedPlan.implementedAt === null; +} + export function deriveWorkLogEntries( activities: ReadonlyArray, latestTurnId: TurnId | undefined, diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index faebe4b0f..73f113313 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -304,6 +304,8 @@ export function syncServerReadModel(state: AppState, readModel: OrchestrationRea id: proposedPlan.id, turnId: proposedPlan.turnId, planMarkdown: proposedPlan.planMarkdown, + implementedAt: proposedPlan.implementedAt, + implementationThreadId: proposedPlan.implementationThreadId, createdAt: proposedPlan.createdAt, updatedAt: proposedPlan.updatedAt, })), diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index c071fb3f6..32a7fe02b 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -53,6 +53,8 @@ export interface ProposedPlan { id: OrchestrationProposedPlanId; turnId: TurnId | null; planMarkdown: string; + implementedAt: string | null; + implementationThreadId: ThreadId | null; createdAt: string; updatedAt: string; } From daee140d7ea3c36db55219277133ea66b08916a2 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 18:58:10 +0530 Subject: [PATCH 03/13] Track source plan refs and mark implementation on turn start - Carry `sourceProposedPlan` metadata in `thread.turn-start-requested` - Persist source plan thread/plan IDs on pending projection turns via migration 015 - Mark proposed plans implemented in runtime ingestion only after `turn.started` - Update decider, contracts, and ingestion tests to match new lifecycle --- .../Layers/ProjectionPipeline.ts | 24 +++ .../Layers/ProviderRuntimeIngestion.test.ts | 175 +++++++++++++++++- .../Layers/ProviderRuntimeIngestion.ts | 52 +++++- .../decider.projectScripts.test.ts | 16 +- apps/server/src/orchestration/decider.ts | 26 +-- .../src/persistence/Layers/ProjectionTurns.ts | 16 ++ apps/server/src/persistence/Migrations.ts | 2 + .../015_ProjectionTurnsSourceProposedPlan.ts | 16 ++ .../persistence/Services/ProjectionTurns.ts | 7 + packages/contracts/src/orchestration.test.ts | 19 ++ packages/contracts/src/orchestration.ts | 1 + 11 files changed, 319 insertions(+), 35 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/015_ProjectionTurnsSourceProposedPlan.ts diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 4120bfe71..d46764cc8 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -777,6 +777,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { yield* projectionTurnRepository.replacePendingTurnStart({ threadId: event.payload.threadId, messageId: event.payload.messageId, + sourceProposedPlanThreadId: event.payload.sourceProposedPlan?.threadId ?? null, + sourceProposedPlanId: event.payload.sourceProposedPlan?.planId ?? null, requestedAt: event.payload.createdAt, }); return; @@ -806,6 +808,16 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { pendingMessageId: existingTurn.value.pendingMessageId ?? (Option.isSome(pendingTurnStart) ? pendingTurnStart.value.messageId : null), + sourceProposedPlanThreadId: + existingTurn.value.sourceProposedPlanThreadId ?? + (Option.isSome(pendingTurnStart) + ? pendingTurnStart.value.sourceProposedPlanThreadId + : null), + sourceProposedPlanId: + existingTurn.value.sourceProposedPlanId ?? + (Option.isSome(pendingTurnStart) + ? pendingTurnStart.value.sourceProposedPlanId + : null), startedAt: existingTurn.value.startedAt ?? (Option.isSome(pendingTurnStart) @@ -824,6 +836,12 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { pendingMessageId: Option.isSome(pendingTurnStart) ? pendingTurnStart.value.messageId : null, + sourceProposedPlanThreadId: Option.isSome(pendingTurnStart) + ? pendingTurnStart.value.sourceProposedPlanThreadId + : null, + sourceProposedPlanId: Option.isSome(pendingTurnStart) + ? pendingTurnStart.value.sourceProposedPlanId + : null, assistantMessageId: null, state: "running", requestedAt: Option.isSome(pendingTurnStart) @@ -877,6 +895,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { turnId: event.payload.turnId, threadId: event.payload.threadId, pendingMessageId: null, + sourceProposedPlanThreadId: null, + sourceProposedPlanId: null, assistantMessageId: event.payload.messageId, state: event.payload.streaming ? "running" : "completed", requestedAt: event.payload.createdAt, @@ -912,6 +932,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { turnId: event.payload.turnId, threadId: event.payload.threadId, pendingMessageId: null, + sourceProposedPlanThreadId: null, + sourceProposedPlanId: null, assistantMessageId: null, state: "interrupted", requestedAt: event.payload.createdAt, @@ -956,6 +978,8 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { turnId: event.payload.turnId, threadId: event.payload.threadId, pendingMessageId: null, + sourceProposedPlanThreadId: null, + sourceProposedPlanId: null, assistantMessageId: event.payload.assistantMessageId, state: nextState, requestedAt: event.payload.completedAt, diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index b6b48c7ed..99d330dba 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -86,11 +86,12 @@ async function waitForThread( engine: OrchestrationEngineShape, predicate: (thread: ProviderRuntimeTestThread) => boolean, timeoutMs = 2000, + threadId: ThreadId = asThreadId("thread-1"), ) { const deadline = Date.now() + timeoutMs; const poll = async (): Promise => { const readModel = await Effect.runPromise(engine.getReadModel()); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + const thread = readModel.threads.find((entry) => entry.id === threadId); if (thread && predicate(thread)) { return thread; } @@ -150,6 +151,7 @@ describe("ProviderRuntimeIngestion", () => { ); const layer = ProviderRuntimeIngestionLive.pipe( Layer.provideMerge(orchestrationLayer), + Layer.provideMerge(SqlitePersistenceMemory), Layer.provideMerge(Layer.succeed(ProviderService, provider.service)), Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), Layer.provideMerge(NodeServices.layer), @@ -628,6 +630,177 @@ describe("ProviderRuntimeIngestion", () => { ); }); + it("marks the source proposed plan implemented only after the target turn starts", async () => { + const harness = await createHarness(); + const sourceThreadId = asThreadId("thread-plan"); + const targetThreadId = asThreadId("thread-implement"); + const sourceTurnId = asTurnId("turn-plan-source"); + const targetTurnId = asTurnId("turn-plan-implement"); + const createdAt = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-plan-source"), + threadId: sourceThreadId, + projectId: asProjectId("project-1"), + title: "Plan Source", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-plan-source"), + threadId: sourceThreadId, + session: { + threadId: sourceThreadId, + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: createdAt, + lastError: null, + }, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-plan-target"), + threadId: targetThreadId, + projectId: asProjectId("project-1"), + title: "Plan Target", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-plan-target"), + threadId: targetThreadId, + session: { + threadId: targetThreadId, + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: createdAt, + lastError: null, + }, + createdAt, + }), + ); + + harness.emit({ + type: "turn.proposed.completed", + eventId: asEventId("evt-plan-source-completed"), + provider: "codex", + createdAt, + threadId: sourceThreadId, + turnId: sourceTurnId, + payload: { + planMarkdown: "# Source plan", + }, + }); + + const sourceThreadWithPlan = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && + proposedPlan.implementedAt === null, + ), + 2_000, + sourceThreadId, + ); + const sourcePlan = sourceThreadWithPlan.proposedPlans.find( + (entry: ProviderRuntimeTestProposedPlan) => + entry.id === "plan:thread-plan:turn:turn-plan-source", + ); + expect(sourcePlan).toBeDefined(); + if (!sourcePlan) { + throw new Error("Expected source plan to exist."); + } + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target"), + threadId: targetThreadId, + message: { + messageId: asMessageId("msg-plan-target"), + role: "user", + text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", + attachments: [], + }, + sourceProposedPlan: { + threadId: sourceThreadId, + planId: sourcePlan.id, + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: new Date().toISOString(), + }), + ); + + const sourceThreadBeforeStart = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === sourcePlan.id && proposedPlan.implementedAt === null, + ), + 2_000, + sourceThreadId, + ); + expect( + sourceThreadBeforeStart.proposedPlans.find((entry) => entry.id === sourcePlan.id), + ).toMatchObject({ + implementedAt: null, + implementationThreadId: null, + }); + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-plan-target-started"), + provider: "codex", + createdAt: new Date().toISOString(), + threadId: targetThreadId, + turnId: targetTurnId, + }); + + const sourceThreadAfterStart = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === sourcePlan.id && + proposedPlan.implementedAt !== null && + proposedPlan.implementationThreadId === targetThreadId, + ), + 2_000, + sourceThreadId, + ); + expect( + sourceThreadAfterStart.proposedPlans.find((entry) => entry.id === sourcePlan.id), + ).toMatchObject({ + implementationThreadId: "thread-implement", + }); + }); + it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 60db85400..4d4333f51 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -15,6 +15,8 @@ import { Cache, Cause, Duration, Effect, Layer, Option, Ref, Stream } from "effe import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; import { ProviderService } from "../../provider/Services/ProviderService.ts"; +import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts"; +import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; import { isGitRepository } from "../../git/isRepo.ts"; import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; @@ -480,6 +482,7 @@ function runtimeEventToActivities( const make = Effect.gen(function* () { const orchestrationEngine = yield* OrchestrationEngineService; const providerService = yield* ProviderService; + const projectionTurnRepository = yield* ProjectionTurnRepository; const assistantDeliveryModeRef = yield* Ref.make( DEFAULT_ASSISTANT_DELIVERY_MODE, @@ -778,6 +781,46 @@ const make = Effect.gen(function* () { ).pipe(Effect.asVoid); }); + const markSourceProposedPlanImplementedForStartedTurn = Effect.fnUntraced(function* ( + threadId: ThreadId, + implementedAt: string, + ) { + const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId({ + threadId, + }); + if (Option.isNone(pendingTurnStart)) { + return; + } + + const sourceThreadId = pendingTurnStart.value.sourceProposedPlanThreadId; + const sourcePlanId = pendingTurnStart.value.sourceProposedPlanId; + if (sourceThreadId === null || sourcePlanId === null) { + return; + } + + const readModel = yield* orchestrationEngine.getReadModel(); + const sourceThread = readModel.threads.find((entry) => entry.id === sourceThreadId); + const sourcePlan = sourceThread?.proposedPlans.find((entry) => entry.id === sourcePlanId); + if (!sourceThread || !sourcePlan || sourcePlan.implementedAt !== null) { + return; + } + + yield* orchestrationEngine.dispatch({ + type: "thread.proposed-plan.upsert", + commandId: CommandId.makeUnsafe( + `provider:source-proposed-plan-implemented:${threadId}:${crypto.randomUUID()}`, + ), + threadId: sourceThread.id, + proposedPlan: { + ...sourcePlan, + implementedAt, + implementationThreadId: threadId, + updatedAt: implementedAt, + }, + createdAt: implementedAt, + }); + }); + const processRuntimeEvent = (event: ProviderRuntimeEvent) => Effect.gen(function* () { const readModel = yield* orchestrationEngine.getReadModel(); @@ -788,6 +831,10 @@ const make = Effect.gen(function* () { const eventTurnId = toTurnId(event.turnId); const activeTurnId = thread.session?.activeTurnId ?? null; + if (event.type === "turn.started") { + yield* markSourceProposedPlanImplementedForStartedTurn(thread.id, now); + } + const conflictsWithActiveTurn = activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId); const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined; @@ -1150,4 +1197,7 @@ const make = Effect.gen(function* () { } satisfies ProviderRuntimeIngestionShape; }); -export const ProviderRuntimeIngestionLive = Layer.effect(ProviderRuntimeIngestionService, make); +export const ProviderRuntimeIngestionLive = Layer.effect( + ProviderRuntimeIngestionService, + make, +).pipe(Layer.provide(ProjectionTurnRepositoryLive)); diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index d0bc7d752..09ce61f5e 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -202,7 +202,7 @@ describe("decider project scripts", () => { }); }); - it("marks the source proposed plan implemented when starting a new implementation thread", async () => { + it("carries the source proposed plan reference in turn-start-requested", async () => { const now = new Date().toISOString(); const initial = createEmptyReadModel(now); const withProject = await Effect.runPromise( @@ -333,16 +333,14 @@ describe("decider project scripts", () => { expect(Array.isArray(result)).toBe(true); const events = Array.isArray(result) ? result : [result]; - expect(events).toHaveLength(3); - expect(events[2]?.type).toBe("thread.proposed-plan-upserted"); - expect(events[2]?.aggregateId).toBe("thread-plan"); - if (events[2]?.type !== "thread.proposed-plan-upserted") { + expect(events).toHaveLength(2); + expect(events[1]?.type).toBe("thread.turn-start-requested"); + if (events[1]?.type !== "thread.turn-start-requested") { return; } - expect(events[2].payload.proposedPlan).toMatchObject({ - id: "plan-1", - implementedAt: now, - implementationThreadId: "thread-implement", + expect(events[1].payload.sourceProposedPlan).toMatchObject({ + threadId: "thread-plan", + planId: "plan-1", }); }); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 677290c2c..d15920ed9 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -326,33 +326,11 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, runtimeMode: targetThread.runtimeMode, interactionMode: targetThread.interactionMode, + ...(sourceProposedPlan !== undefined ? { sourceProposedPlan } : {}), createdAt: command.createdAt, }, }; - if (!sourcePlan || !sourceThread) { - return [userMessageEvent, turnStartRequestedEvent]; - } - - const sourcePlanUpsertEvent: Omit = { - ...withEventBase({ - aggregateKind: "thread", - aggregateId: sourceThread.id, - occurredAt: command.createdAt, - commandId: command.commandId, - }), - causationEventId: turnStartRequestedEvent.eventId, - type: "thread.proposed-plan-upserted", - payload: { - threadId: sourceThread.id, - proposedPlan: { - ...sourcePlan, - implementedAt: command.createdAt, - implementationThreadId: command.threadId, - updatedAt: command.createdAt, - }, - }, - }; - return [userMessageEvent, turnStartRequestedEvent, sourcePlanUpsertEvent]; + return [userMessageEvent, turnStartRequestedEvent]; } case "thread.turn.interrupt": { diff --git a/apps/server/src/persistence/Layers/ProjectionTurns.ts b/apps/server/src/persistence/Layers/ProjectionTurns.ts index 8330661e3..9b6c9c577 100644 --- a/apps/server/src/persistence/Layers/ProjectionTurns.ts +++ b/apps/server/src/persistence/Layers/ProjectionTurns.ts @@ -47,6 +47,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id, turn_id, pending_message_id, + source_proposed_plan_thread_id, + source_proposed_plan_id, assistant_message_id, state, requested_at, @@ -61,6 +63,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { ${row.threadId}, ${row.turnId}, ${row.pendingMessageId}, + ${row.sourceProposedPlanThreadId}, + ${row.sourceProposedPlanId}, ${row.assistantMessageId}, ${row.state}, ${row.requestedAt}, @@ -74,6 +78,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { ON CONFLICT (thread_id, turn_id) DO UPDATE SET pending_message_id = excluded.pending_message_id, + source_proposed_plan_thread_id = excluded.source_proposed_plan_thread_id, + source_proposed_plan_id = excluded.source_proposed_plan_id, assistant_message_id = excluded.assistant_message_id, state = excluded.state, requested_at = excluded.requested_at, @@ -106,6 +112,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id, turn_id, pending_message_id, + source_proposed_plan_thread_id, + source_proposed_plan_id, assistant_message_id, state, requested_at, @@ -120,6 +128,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { ${row.threadId}, NULL, ${row.messageId}, + ${row.sourceProposedPlanThreadId}, + ${row.sourceProposedPlanId}, NULL, 'pending', ${row.requestedAt}, @@ -141,6 +151,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { SELECT thread_id AS "threadId", pending_message_id AS "messageId", + source_proposed_plan_thread_id AS "sourceProposedPlanThreadId", + source_proposed_plan_id AS "sourceProposedPlanId", requested_at AS "requestedAt" FROM projection_turns WHERE thread_id = ${threadId} @@ -162,6 +174,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id AS "threadId", turn_id AS "turnId", pending_message_id AS "pendingMessageId", + source_proposed_plan_thread_id AS "sourceProposedPlanThreadId", + source_proposed_plan_id AS "sourceProposedPlanId", assistant_message_id AS "assistantMessageId", state, requested_at AS "requestedAt", @@ -193,6 +207,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id AS "threadId", turn_id AS "turnId", pending_message_id AS "pendingMessageId", + source_proposed_plan_thread_id AS "sourceProposedPlanThreadId", + source_proposed_plan_id AS "sourceProposedPlanId", assistant_message_id AS "assistantMessageId", state, requested_at AS "requestedAt", diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 90c52289a..ea1821014 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -26,6 +26,7 @@ import Migration0011 from "./Migrations/011_OrchestrationThreadCreatedRuntimeMod import Migration0012 from "./Migrations/012_ProjectionThreadsInteractionMode.ts"; import Migration0013 from "./Migrations/013_ProjectionThreadProposedPlans.ts"; import Migration0014 from "./Migrations/014_ProjectionThreadProposedPlanImplementation.ts"; +import Migration0015 from "./Migrations/015_ProjectionTurnsSourceProposedPlan.ts"; import { Effect } from "effect"; /** @@ -53,6 +54,7 @@ const loader = Migrator.fromRecord({ "12_ProjectionThreadsInteractionMode": Migration0012, "13_ProjectionThreadProposedPlans": Migration0013, "14_ProjectionThreadProposedPlanImplementation": Migration0014, + "15_ProjectionTurnsSourceProposedPlan": Migration0015, }); /** diff --git a/apps/server/src/persistence/Migrations/015_ProjectionTurnsSourceProposedPlan.ts b/apps/server/src/persistence/Migrations/015_ProjectionTurnsSourceProposedPlan.ts new file mode 100644 index 000000000..57a266187 --- /dev/null +++ b/apps/server/src/persistence/Migrations/015_ProjectionTurnsSourceProposedPlan.ts @@ -0,0 +1,16 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* sql` + ALTER TABLE projection_turns + ADD COLUMN source_proposed_plan_thread_id TEXT + `; + + yield* sql` + ALTER TABLE projection_turns + ADD COLUMN source_proposed_plan_id TEXT + `; +}); diff --git a/apps/server/src/persistence/Services/ProjectionTurns.ts b/apps/server/src/persistence/Services/ProjectionTurns.ts index 1c791342f..95dab450b 100644 --- a/apps/server/src/persistence/Services/ProjectionTurns.ts +++ b/apps/server/src/persistence/Services/ProjectionTurns.ts @@ -11,6 +11,7 @@ import { IsoDateTime, MessageId, NonNegativeInt, + OrchestrationProposedPlanId, OrchestrationCheckpointFile, OrchestrationCheckpointStatus, ThreadId, @@ -34,6 +35,8 @@ export const ProjectionTurn = Schema.Struct({ threadId: ThreadId, turnId: Schema.NullOr(TurnId), pendingMessageId: Schema.NullOr(MessageId), + sourceProposedPlanThreadId: Schema.NullOr(ThreadId), + sourceProposedPlanId: Schema.NullOr(OrchestrationProposedPlanId), assistantMessageId: Schema.NullOr(MessageId), state: ProjectionTurnState, requestedAt: IsoDateTime, @@ -50,6 +53,8 @@ export const ProjectionTurnById = Schema.Struct({ threadId: ThreadId, turnId: TurnId, pendingMessageId: Schema.NullOr(MessageId), + sourceProposedPlanThreadId: Schema.NullOr(ThreadId), + sourceProposedPlanId: Schema.NullOr(OrchestrationProposedPlanId), assistantMessageId: Schema.NullOr(MessageId), state: ProjectionTurnState, requestedAt: IsoDateTime, @@ -65,6 +70,8 @@ export type ProjectionTurnById = typeof ProjectionTurnById.Type; export const ProjectionPendingTurnStart = Schema.Struct({ threadId: ThreadId, messageId: MessageId, + sourceProposedPlanThreadId: Schema.NullOr(ThreadId), + sourceProposedPlanId: Schema.NullOr(OrchestrationProposedPlanId), requestedAt: IsoDateTime, }); export type ProjectionPendingTurnStart = typeof ProjectionPendingTurnStart.Type; diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index 158ddd856..136c5a849 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -225,9 +225,28 @@ it.effect( assert.strictEqual(parsed.provider, undefined); assert.strictEqual(parsed.runtimeMode, DEFAULT_RUNTIME_MODE); assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE); + assert.strictEqual(parsed.sourceProposedPlan, undefined); }), ); +it.effect("decodes thread.turn-start-requested source proposed plan metadata when present", () => + Effect.gen(function* () { + const parsed = yield* decodeThreadTurnStartRequestedPayload({ + threadId: "thread-2", + messageId: "msg-2", + sourceProposedPlan: { + threadId: "thread-1", + planId: "plan-1", + }, + createdAt: "2026-01-01T00:00:00.000Z", + }); + assert.deepStrictEqual(parsed.sourceProposedPlan, { + threadId: "thread-1", + planId: "plan-1", + }); + }), +); + it.effect("decodes orchestration session runtime mode defaults", () => Effect.gen(function* () { const parsed = yield* decodeOrchestrationSession({ diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 7ae60af3e..4ee53c276 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -685,6 +685,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(() => DEFAULT_PROVIDER_INTERACTION_MODE), ), + sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, }); From 44f8430f66fdf7bf1787a869aaa0e0874fe3c1db Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 19:44:02 +0530 Subject: [PATCH 04/13] fix(orchestration): gate plan consumption on accepted turn starts --- .../Layers/ProviderRuntimeIngestion.test.ts | 141 ++++++++++++++++++ .../Layers/ProviderRuntimeIngestion.ts | 41 +++-- 2 files changed, 172 insertions(+), 10 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 99d330dba..5da57530c 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -801,6 +801,147 @@ describe("ProviderRuntimeIngestion", () => { }); }); + it("does not mark the source proposed plan implemented for a rejected turn.started event", async () => { + const harness = await createHarness(); + const sourceThreadId = asThreadId("thread-plan"); + const targetThreadId = asThreadId("thread-1"); + const sourceTurnId = asTurnId("turn-plan-source"); + const activeTurnId = asTurnId("turn-already-running"); + const staleTurnId = asTurnId("turn-stale-start"); + const createdAt = new Date().toISOString(); + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-plan-source-guarded"), + threadId: sourceThreadId, + projectId: asProjectId("project-1"), + title: "Plan Source", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-plan-source-guarded"), + threadId: sourceThreadId, + session: { + threadId: sourceThreadId, + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: createdAt, + lastError: null, + }, + createdAt, + }), + ); + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-turn-started-already-running"), + provider: "codex", + createdAt, + threadId: targetThreadId, + turnId: activeTurnId, + }); + + await waitForThread( + harness.engine, + (thread) => + thread.session?.status === "running" && thread.session?.activeTurnId === activeTurnId, + 2_000, + targetThreadId, + ); + + harness.emit({ + type: "turn.proposed.completed", + eventId: asEventId("evt-plan-source-completed-guarded"), + provider: "codex", + createdAt, + threadId: sourceThreadId, + turnId: sourceTurnId, + payload: { + planMarkdown: "# Source plan", + }, + }); + + const sourceThreadWithPlan = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && + proposedPlan.implementedAt === null, + ), + 2_000, + sourceThreadId, + ); + const sourcePlan = sourceThreadWithPlan.proposedPlans.find( + (entry: ProviderRuntimeTestProposedPlan) => + entry.id === "plan:thread-plan:turn:turn-plan-source", + ); + expect(sourcePlan).toBeDefined(); + if (!sourcePlan) { + throw new Error("Expected source plan to exist."); + } + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target-guarded"), + threadId: targetThreadId, + message: { + messageId: asMessageId("msg-plan-target-guarded"), + role: "user", + text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", + attachments: [], + }, + sourceProposedPlan: { + threadId: sourceThreadId, + planId: sourcePlan.id, + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: new Date().toISOString(), + }), + ); + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-turn-started-stale-plan-implementation"), + provider: "codex", + createdAt: new Date().toISOString(), + threadId: targetThreadId, + turnId: staleTurnId, + }); + + await harness.drain(); + + const readModel = await Effect.runPromise(harness.engine.getReadModel()); + const sourceThreadAfterRejectedStart = readModel.threads.find( + (entry) => entry.id === sourceThreadId, + ); + expect( + sourceThreadAfterRejectedStart?.proposedPlans.find((entry) => entry.id === sourcePlan.id), + ).toMatchObject({ + implementedAt: null, + implementationThreadId: null, + }); + + const targetThreadAfterRejectedStart = readModel.threads.find( + (entry) => entry.id === targetThreadId, + ); + expect(targetThreadAfterRejectedStart?.session?.status).toBe("running"); + expect(targetThreadAfterRejectedStart?.session?.activeTurnId).toBe(activeTurnId); + }); + it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => { const harness = await createHarness(); const now = new Date().toISOString(); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 4d4333f51..fb7685cd3 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -4,6 +4,7 @@ import { CommandId, MessageId, type OrchestrationEvent, + type OrchestrationProposedPlanId, CheckpointRef, isToolLifecycleItemType, ThreadId, @@ -781,23 +782,34 @@ const make = Effect.gen(function* () { ).pipe(Effect.asVoid); }); - const markSourceProposedPlanImplementedForStartedTurn = Effect.fnUntraced(function* ( + const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fnUntraced(function* ( threadId: ThreadId, - implementedAt: string, ) { const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId({ threadId, }); if (Option.isNone(pendingTurnStart)) { - return; + return null; } const sourceThreadId = pendingTurnStart.value.sourceProposedPlanThreadId; const sourcePlanId = pendingTurnStart.value.sourceProposedPlanId; if (sourceThreadId === null || sourcePlanId === null) { - return; + return null; } + return { + sourceThreadId, + sourcePlanId, + } as const; + }); + + const markSourceProposedPlanImplemented = Effect.fnUntraced(function* ( + sourceThreadId: ThreadId, + sourcePlanId: OrchestrationProposedPlanId, + implementationThreadId: ThreadId, + implementedAt: string, + ) { const readModel = yield* orchestrationEngine.getReadModel(); const sourceThread = readModel.threads.find((entry) => entry.id === sourceThreadId); const sourcePlan = sourceThread?.proposedPlans.find((entry) => entry.id === sourcePlanId); @@ -808,13 +820,13 @@ const make = Effect.gen(function* () { yield* orchestrationEngine.dispatch({ type: "thread.proposed-plan.upsert", commandId: CommandId.makeUnsafe( - `provider:source-proposed-plan-implemented:${threadId}:${crypto.randomUUID()}`, + `provider:source-proposed-plan-implemented:${implementationThreadId}:${crypto.randomUUID()}`, ), threadId: sourceThread.id, proposedPlan: { ...sourcePlan, implementedAt, - implementationThreadId: threadId, + implementationThreadId, updatedAt: implementedAt, }, createdAt: implementedAt, @@ -831,10 +843,6 @@ const make = Effect.gen(function* () { const eventTurnId = toTurnId(event.turnId); const activeTurnId = thread.session?.activeTurnId ?? null; - if (event.type === "turn.started") { - yield* markSourceProposedPlanImplementedForStartedTurn(thread.id, now); - } - const conflictsWithActiveTurn = activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId); const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined; @@ -865,6 +873,10 @@ const make = Effect.gen(function* () { return true; } })(); + const acceptedTurnStartedSourcePlan = + event.type === "turn.started" && shouldApplyThreadLifecycle + ? yield* getSourceProposedPlanReferenceForPendingTurnStart(thread.id) + : null; if ( event.type === "session.started" || @@ -922,6 +934,15 @@ const make = Effect.gen(function* () { }, createdAt: now, }); + + if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) { + yield* markSourceProposedPlanImplemented( + acceptedTurnStartedSourcePlan.sourceThreadId, + acceptedTurnStartedSourcePlan.sourcePlanId, + thread.id, + now, + ); + } } } From 0e4354bc9ab8a5042871b330ad343d9593091fb9 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 19:57:04 +0530 Subject: [PATCH 05/13] refactor(orchestration): drop unused concrete turn source-plan refs --- .../Layers/ProjectionPipeline.ts | 22 ------------------- .../src/persistence/Layers/ProjectionTurns.ts | 10 --------- .../persistence/Services/ProjectionTurns.ts | 4 ---- 3 files changed, 36 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index d46764cc8..ce633aeca 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -808,16 +808,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { pendingMessageId: existingTurn.value.pendingMessageId ?? (Option.isSome(pendingTurnStart) ? pendingTurnStart.value.messageId : null), - sourceProposedPlanThreadId: - existingTurn.value.sourceProposedPlanThreadId ?? - (Option.isSome(pendingTurnStart) - ? pendingTurnStart.value.sourceProposedPlanThreadId - : null), - sourceProposedPlanId: - existingTurn.value.sourceProposedPlanId ?? - (Option.isSome(pendingTurnStart) - ? pendingTurnStart.value.sourceProposedPlanId - : null), startedAt: existingTurn.value.startedAt ?? (Option.isSome(pendingTurnStart) @@ -836,12 +826,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { pendingMessageId: Option.isSome(pendingTurnStart) ? pendingTurnStart.value.messageId : null, - sourceProposedPlanThreadId: Option.isSome(pendingTurnStart) - ? pendingTurnStart.value.sourceProposedPlanThreadId - : null, - sourceProposedPlanId: Option.isSome(pendingTurnStart) - ? pendingTurnStart.value.sourceProposedPlanId - : null, assistantMessageId: null, state: "running", requestedAt: Option.isSome(pendingTurnStart) @@ -895,8 +879,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { turnId: event.payload.turnId, threadId: event.payload.threadId, pendingMessageId: null, - sourceProposedPlanThreadId: null, - sourceProposedPlanId: null, assistantMessageId: event.payload.messageId, state: event.payload.streaming ? "running" : "completed", requestedAt: event.payload.createdAt, @@ -932,8 +914,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { turnId: event.payload.turnId, threadId: event.payload.threadId, pendingMessageId: null, - sourceProposedPlanThreadId: null, - sourceProposedPlanId: null, assistantMessageId: null, state: "interrupted", requestedAt: event.payload.createdAt, @@ -978,8 +958,6 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { turnId: event.payload.turnId, threadId: event.payload.threadId, pendingMessageId: null, - sourceProposedPlanThreadId: null, - sourceProposedPlanId: null, assistantMessageId: event.payload.assistantMessageId, state: nextState, requestedAt: event.payload.completedAt, diff --git a/apps/server/src/persistence/Layers/ProjectionTurns.ts b/apps/server/src/persistence/Layers/ProjectionTurns.ts index 9b6c9c577..72c6889c5 100644 --- a/apps/server/src/persistence/Layers/ProjectionTurns.ts +++ b/apps/server/src/persistence/Layers/ProjectionTurns.ts @@ -47,8 +47,6 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id, turn_id, pending_message_id, - source_proposed_plan_thread_id, - source_proposed_plan_id, assistant_message_id, state, requested_at, @@ -63,8 +61,6 @@ const makeProjectionTurnRepository = Effect.gen(function* () { ${row.threadId}, ${row.turnId}, ${row.pendingMessageId}, - ${row.sourceProposedPlanThreadId}, - ${row.sourceProposedPlanId}, ${row.assistantMessageId}, ${row.state}, ${row.requestedAt}, @@ -78,8 +74,6 @@ const makeProjectionTurnRepository = Effect.gen(function* () { ON CONFLICT (thread_id, turn_id) DO UPDATE SET pending_message_id = excluded.pending_message_id, - source_proposed_plan_thread_id = excluded.source_proposed_plan_thread_id, - source_proposed_plan_id = excluded.source_proposed_plan_id, assistant_message_id = excluded.assistant_message_id, state = excluded.state, requested_at = excluded.requested_at, @@ -174,8 +168,6 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id AS "threadId", turn_id AS "turnId", pending_message_id AS "pendingMessageId", - source_proposed_plan_thread_id AS "sourceProposedPlanThreadId", - source_proposed_plan_id AS "sourceProposedPlanId", assistant_message_id AS "assistantMessageId", state, requested_at AS "requestedAt", @@ -207,8 +199,6 @@ const makeProjectionTurnRepository = Effect.gen(function* () { thread_id AS "threadId", turn_id AS "turnId", pending_message_id AS "pendingMessageId", - source_proposed_plan_thread_id AS "sourceProposedPlanThreadId", - source_proposed_plan_id AS "sourceProposedPlanId", assistant_message_id AS "assistantMessageId", state, requested_at AS "requestedAt", diff --git a/apps/server/src/persistence/Services/ProjectionTurns.ts b/apps/server/src/persistence/Services/ProjectionTurns.ts index 95dab450b..b5888fdce 100644 --- a/apps/server/src/persistence/Services/ProjectionTurns.ts +++ b/apps/server/src/persistence/Services/ProjectionTurns.ts @@ -35,8 +35,6 @@ export const ProjectionTurn = Schema.Struct({ threadId: ThreadId, turnId: Schema.NullOr(TurnId), pendingMessageId: Schema.NullOr(MessageId), - sourceProposedPlanThreadId: Schema.NullOr(ThreadId), - sourceProposedPlanId: Schema.NullOr(OrchestrationProposedPlanId), assistantMessageId: Schema.NullOr(MessageId), state: ProjectionTurnState, requestedAt: IsoDateTime, @@ -53,8 +51,6 @@ export const ProjectionTurnById = Schema.Struct({ threadId: ThreadId, turnId: TurnId, pendingMessageId: Schema.NullOr(MessageId), - sourceProposedPlanThreadId: Schema.NullOr(ThreadId), - sourceProposedPlanId: Schema.NullOr(OrchestrationProposedPlanId), assistantMessageId: Schema.NullOr(MessageId), state: ProjectionTurnState, requestedAt: IsoDateTime, From da910077fea76c5ff9fb59564eca420cd3a3c252 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 20:20:13 +0530 Subject: [PATCH 06/13] refactor(test): compact orchestration plan setup fixtures --- .../Layers/ProviderRuntimeIngestion.test.ts | 364 ++++++------ .../decider.projectScripts.test.ts | 517 +++++++----------- 2 files changed, 361 insertions(+), 520 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 5da57530c..7ce2d7b73 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -104,6 +104,138 @@ async function waitForThread( return poll(); } +type ProviderRuntimeTestHarness = { + engine: OrchestrationEngineShape; + emit: (event: LegacyProviderRuntimeEvent) => void; + drain: () => Promise; +}; + +async function seedThreadWithReadySession( + engine: OrchestrationEngineShape, + input: { + threadId: ThreadId; + title: string; + createdAt: string; + commandKey: string; + interactionMode?: "default" | "plan"; + runtimeMode?: "approval-required" | "full-access"; + }, +) { + const interactionMode = input.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; + const runtimeMode = input.runtimeMode ?? "approval-required"; + + await Effect.runPromise( + engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe(`cmd-thread-create-${input.commandKey}`), + threadId: input.threadId, + projectId: asProjectId("project-1"), + title: input.title, + model: "gpt-5-codex", + interactionMode, + runtimeMode, + branch: null, + worktreePath: null, + createdAt: input.createdAt, + }), + ); + await Effect.runPromise( + engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe(`cmd-session-set-${input.commandKey}`), + threadId: input.threadId, + session: { + threadId: input.threadId, + status: "ready", + providerName: "codex", + runtimeMode, + activeTurnId: null, + updatedAt: input.createdAt, + lastError: null, + }, + createdAt: input.createdAt, + }), + ); +} + +async function seedSourcePlan( + harness: ProviderRuntimeTestHarness, + input: { + sourceThreadId: ThreadId; + sourceTurnId: TurnId; + createdAt: string; + eventId: string; + planMarkdown?: string; + }, +) { + harness.emit({ + type: "turn.proposed.completed", + eventId: asEventId(input.eventId), + provider: "codex", + createdAt: input.createdAt, + threadId: input.sourceThreadId, + turnId: input.sourceTurnId, + payload: { + planMarkdown: input.planMarkdown ?? "# Source plan", + }, + }); + + const sourceThreadWithPlan = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === `plan:${input.sourceThreadId}:turn:${input.sourceTurnId}` && + proposedPlan.implementedAt === null, + ), + 2_000, + input.sourceThreadId, + ); + const sourcePlan = sourceThreadWithPlan.proposedPlans.find( + (entry: ProviderRuntimeTestProposedPlan) => + entry.id === `plan:${input.sourceThreadId}:turn:${input.sourceTurnId}`, + ); + expect(sourcePlan).toBeDefined(); + if (!sourcePlan) { + throw new Error("Expected source plan to exist."); + } + + return sourcePlan; +} + +async function dispatchImplementPlanTurn( + harness: ProviderRuntimeTestHarness, + input: { + targetThreadId: ThreadId; + sourceThreadId: ThreadId; + sourcePlanId: string; + commandId: string; + messageId: string; + text?: string; + }, +) { + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe(input.commandId), + threadId: input.targetThreadId, + message: { + messageId: asMessageId(input.messageId), + role: "user", + text: input.text ?? "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", + attachments: [], + }, + sourceProposedPlan: { + threadId: input.sourceThreadId, + planId: input.sourcePlanId, + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: new Date().toISOString(), + }), + ); +} + type ProviderRuntimeTestReadModel = OrchestrationReadModel; type ProviderRuntimeTestThread = ProviderRuntimeTestReadModel["threads"][number]; type ProviderRuntimeTestMessage = ProviderRuntimeTestThread["messages"][number]; @@ -638,123 +770,34 @@ describe("ProviderRuntimeIngestion", () => { const targetTurnId = asTurnId("turn-plan-implement"); const createdAt = new Date().toISOString(); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.makeUnsafe("cmd-thread-create-plan-source"), - threadId: sourceThreadId, - projectId: asProjectId("project-1"), - title: "Plan Source", - model: "gpt-5-codex", - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.makeUnsafe("cmd-session-set-plan-source"), - threadId: sourceThreadId, - session: { - threadId: sourceThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.makeUnsafe("cmd-thread-create-plan-target"), - threadId: targetThreadId, - projectId: asProjectId("project-1"), - title: "Plan Target", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.makeUnsafe("cmd-session-set-plan-target"), - threadId: targetThreadId, - session: { - threadId: targetThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); - - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-plan-source-completed"), - provider: "codex", - createdAt, + await seedThreadWithReadySession(harness.engine, { threadId: sourceThreadId, - turnId: sourceTurnId, - payload: { - planMarkdown: "# Source plan", - }, + title: "Plan Source", + createdAt, + commandKey: "plan-source", + interactionMode: "plan", + }); + await seedThreadWithReadySession(harness.engine, { + threadId: targetThreadId, + title: "Plan Target", + createdAt, + commandKey: "plan-target", }); - const sourceThreadWithPlan = await waitForThread( - harness.engine, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && - proposedPlan.implementedAt === null, - ), - 2_000, + const sourcePlan = await seedSourcePlan(harness, { sourceThreadId, - ); - const sourcePlan = sourceThreadWithPlan.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === "plan:thread-plan:turn:turn-plan-source", - ); - expect(sourcePlan).toBeDefined(); - if (!sourcePlan) { - throw new Error("Expected source plan to exist."); - } + sourceTurnId, + createdAt, + eventId: "evt-plan-source-completed", + }); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target"), - threadId: targetThreadId, - message: { - messageId: asMessageId("msg-plan-target"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: sourceThreadId, - planId: sourcePlan.id, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: new Date().toISOString(), - }), - ); + await dispatchImplementPlanTurn(harness, { + targetThreadId, + sourceThreadId, + sourcePlanId: sourcePlan.id, + commandId: "cmd-turn-start-plan-target", + messageId: "msg-plan-target", + }); const sourceThreadBeforeStart = await waitForThread( harness.engine, @@ -810,38 +853,13 @@ describe("ProviderRuntimeIngestion", () => { const staleTurnId = asTurnId("turn-stale-start"); const createdAt = new Date().toISOString(); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.create", - commandId: CommandId.makeUnsafe("cmd-thread-create-plan-source-guarded"), - threadId: sourceThreadId, - projectId: asProjectId("project-1"), - title: "Plan Source", - model: "gpt-5-codex", - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt, - }), - ); - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.makeUnsafe("cmd-session-set-plan-source-guarded"), - threadId: sourceThreadId, - session: { - threadId: sourceThreadId, - status: "ready", - providerName: "codex", - runtimeMode: "approval-required", - activeTurnId: null, - updatedAt: createdAt, - lastError: null, - }, - createdAt, - }), - ); + await seedThreadWithReadySession(harness.engine, { + threadId: sourceThreadId, + title: "Plan Source", + createdAt, + commandKey: "plan-source-guarded", + interactionMode: "plan", + }); harness.emit({ type: "turn.started", @@ -860,58 +878,20 @@ describe("ProviderRuntimeIngestion", () => { targetThreadId, ); - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId("evt-plan-source-completed-guarded"), - provider: "codex", + const sourcePlan = await seedSourcePlan(harness, { + sourceThreadId, + sourceTurnId, createdAt, - threadId: sourceThreadId, - turnId: sourceTurnId, - payload: { - planMarkdown: "# Source plan", - }, + eventId: "evt-plan-source-completed-guarded", }); - const sourceThreadWithPlan = await waitForThread( - harness.engine, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && - proposedPlan.implementedAt === null, - ), - 2_000, + await dispatchImplementPlanTurn(harness, { + targetThreadId, sourceThreadId, - ); - const sourcePlan = sourceThreadWithPlan.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === "plan:thread-plan:turn:turn-plan-source", - ); - expect(sourcePlan).toBeDefined(); - if (!sourcePlan) { - throw new Error("Expected source plan to exist."); - } - - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target-guarded"), - threadId: targetThreadId, - message: { - messageId: asMessageId("msg-plan-target-guarded"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: sourceThreadId, - planId: sourcePlan.id, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: new Date().toISOString(), - }), - ); + sourcePlanId: sourcePlan.id, + commandId: "cmd-turn-start-plan-target-guarded", + messageId: "msg-plan-target-guarded", + }); harness.emit({ type: "turn.started", diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index 09ce61f5e..cf434d2be 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_PROVIDER_INTERACTION_MODE, EventId, MessageId, + type OrchestrationReadModel, ProjectId, ThreadId, TurnId, @@ -17,6 +18,114 @@ const asEventId = (value: string): EventId => EventId.makeUnsafe(value); const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); +async function seedProjectReadModel(now: string): Promise { + return Effect.runPromise( + projectEvent(createEmptyReadModel(now), { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); +} + +async function appendThreadToReadModel( + readModel: OrchestrationReadModel, + input: { + now: string; + sequence: number; + eventId: string; + commandId: string; + threadId: string; + title: string; + interactionMode: "default" | "plan"; + runtimeMode: "approval-required" | "full-access"; + }, +): Promise { + return Effect.runPromise( + projectEvent(readModel, { + sequence: input.sequence, + eventId: asEventId(input.eventId), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe(input.threadId), + type: "thread.created", + occurredAt: input.now, + commandId: CommandId.makeUnsafe(input.commandId), + causationEventId: null, + correlationId: CommandId.makeUnsafe(input.commandId), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe(input.threadId), + projectId: asProjectId("project-1"), + title: input.title, + model: "gpt-5-codex", + interactionMode: input.interactionMode, + runtimeMode: input.runtimeMode, + branch: null, + worktreePath: null, + createdAt: input.now, + updatedAt: input.now, + }, + }), + ); +} + +async function appendProposedPlanToReadModel( + readModel: OrchestrationReadModel, + input: { + now: string; + sequence: number; + eventId: string; + commandId: string; + threadId: string; + planId: string; + turnId: string; + planMarkdown: string; + }, +): Promise { + return Effect.runPromise( + projectEvent(readModel, { + sequence: input.sequence, + eventId: asEventId(input.eventId), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe(input.threadId), + type: "thread.proposed-plan-upserted", + occurredAt: input.now, + commandId: CommandId.makeUnsafe(input.commandId), + causationEventId: null, + correlationId: CommandId.makeUnsafe(input.commandId), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe(input.threadId), + proposedPlan: { + id: input.planId, + turnId: TurnId.makeUnsafe(input.turnId), + planMarkdown: input.planMarkdown, + implementedAt: null, + implementationThreadId: null, + createdAt: input.now, + updatedAt: input.now, + }, + }, + }), + ); +} + describe("decider project scripts", () => { it("emits empty scripts on project.create", async () => { const now = new Date().toISOString(); @@ -97,56 +206,16 @@ describe("decider project scripts", () => { it("emits user message and turn-start-requested events for thread.turn.start", async () => { const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-1"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); + const readModel = await appendThreadToReadModel(await seedProjectReadModel(now), { + now, + sequence: 2, + eventId: "evt-thread-create", + commandId: "cmd-thread-create", + threadId: "thread-1", + title: "Thread", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + }); const result = await Effect.runPromise( decideOrchestrationCommand({ @@ -204,108 +273,36 @@ describe("decider project scripts", () => { it("carries the source proposed plan reference in turn-start-requested", async () => { const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const withSourceThread = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create-source"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-plan"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-source"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-plan"), - projectId: asProjectId("project-1"), - title: "Plan Thread", - model: "gpt-5-codex", - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - const withTargetThread = await Effect.runPromise( - projectEvent(withSourceThread, { - sequence: 3, - eventId: asEventId("evt-thread-create-target"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-implement"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-target"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-implement"), - projectId: asProjectId("project-1"), - title: "Implementation Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withTargetThread, { - sequence: 4, - eventId: asEventId("evt-plan-upsert"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-plan"), - type: "thread.proposed-plan-upserted", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-plan-upsert"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-plan-upsert"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-plan"), - proposedPlan: { - id: "plan-1", - turnId: TurnId.makeUnsafe("turn-1"), - planMarkdown: "# Plan", - implementedAt: null, - implementationThreadId: null, - createdAt: now, - updatedAt: now, - }, - }, - }), - ); + const withSourceThread = await appendThreadToReadModel(await seedProjectReadModel(now), { + now, + sequence: 2, + eventId: "evt-thread-create-source", + commandId: "cmd-thread-create-source", + threadId: "thread-plan", + title: "Plan Thread", + interactionMode: "plan", + runtimeMode: "approval-required", + }); + const withTargetThread = await appendThreadToReadModel(withSourceThread, { + now, + sequence: 3, + eventId: "evt-thread-create-target", + commandId: "cmd-thread-create-target", + threadId: "thread-implement", + title: "Implementation Thread", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + }); + const readModel = await appendProposedPlanToReadModel(withTargetThread, { + now, + sequence: 4, + eventId: "evt-plan-upsert", + commandId: "cmd-plan-upsert", + threadId: "thread-plan", + planId: "plan-1", + turnId: "turn-1", + planMarkdown: "# Plan", + }); const result = await Effect.runPromise( decideOrchestrationCommand({ @@ -346,82 +343,26 @@ describe("decider project scripts", () => { it("rejects thread.turn.start when the source proposed plan is missing", async () => { const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const withSourceThread = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create-source"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-plan"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-source"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-plan"), - projectId: asProjectId("project-1"), - title: "Plan Thread", - model: "gpt-5-codex", - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withSourceThread, { - sequence: 3, - eventId: asEventId("evt-thread-create-target"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-implement"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-target"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-implement"), - projectId: asProjectId("project-1"), - title: "Implementation Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); + const withSourceThread = await appendThreadToReadModel(await seedProjectReadModel(now), { + now, + sequence: 2, + eventId: "evt-thread-create-source", + commandId: "cmd-thread-create-source", + threadId: "thread-plan", + title: "Plan Thread", + interactionMode: "plan", + runtimeMode: "approval-required", + }); + const readModel = await appendThreadToReadModel(withSourceThread, { + now, + sequence: 3, + eventId: "evt-thread-create-target", + commandId: "cmd-thread-create-target", + threadId: "thread-implement", + title: "Implementation Thread", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + }); await expect( Effect.runPromise( @@ -452,56 +393,16 @@ describe("decider project scripts", () => { it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => { const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-1"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); + const readModel = await appendThreadToReadModel(await seedProjectReadModel(now), { + now, + sequence: 2, + eventId: "evt-thread-create", + commandId: "cmd-thread-create", + threadId: "thread-1", + title: "Thread", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "full-access", + }); const result = await Effect.runPromise( decideOrchestrationCommand({ @@ -531,56 +432,16 @@ describe("decider project scripts", () => { it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => { const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-1"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-1"), - projectId: asProjectId("project-1"), - title: "Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); + const readModel = await appendThreadToReadModel(await seedProjectReadModel(now), { + now, + sequence: 2, + eventId: "evt-thread-create", + commandId: "cmd-thread-create", + threadId: "thread-1", + title: "Thread", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + }); const result = await Effect.runPromise( decideOrchestrationCommand({ From 2a535deb99234933214fc8e133b2446c00256b0c Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 20:29:54 +0530 Subject: [PATCH 07/13] Revert "refactor(test): compact orchestration plan setup fixtures" This reverts commit da910077fea76c5ff9fb59564eca420cd3a3c252. --- .../Layers/ProviderRuntimeIngestion.test.ts | 364 ++++++------ .../decider.projectScripts.test.ts | 517 +++++++++++------- 2 files changed, 520 insertions(+), 361 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 7ce2d7b73..5da57530c 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -104,138 +104,6 @@ async function waitForThread( return poll(); } -type ProviderRuntimeTestHarness = { - engine: OrchestrationEngineShape; - emit: (event: LegacyProviderRuntimeEvent) => void; - drain: () => Promise; -}; - -async function seedThreadWithReadySession( - engine: OrchestrationEngineShape, - input: { - threadId: ThreadId; - title: string; - createdAt: string; - commandKey: string; - interactionMode?: "default" | "plan"; - runtimeMode?: "approval-required" | "full-access"; - }, -) { - const interactionMode = input.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; - const runtimeMode = input.runtimeMode ?? "approval-required"; - - await Effect.runPromise( - engine.dispatch({ - type: "thread.create", - commandId: CommandId.makeUnsafe(`cmd-thread-create-${input.commandKey}`), - threadId: input.threadId, - projectId: asProjectId("project-1"), - title: input.title, - model: "gpt-5-codex", - interactionMode, - runtimeMode, - branch: null, - worktreePath: null, - createdAt: input.createdAt, - }), - ); - await Effect.runPromise( - engine.dispatch({ - type: "thread.session.set", - commandId: CommandId.makeUnsafe(`cmd-session-set-${input.commandKey}`), - threadId: input.threadId, - session: { - threadId: input.threadId, - status: "ready", - providerName: "codex", - runtimeMode, - activeTurnId: null, - updatedAt: input.createdAt, - lastError: null, - }, - createdAt: input.createdAt, - }), - ); -} - -async function seedSourcePlan( - harness: ProviderRuntimeTestHarness, - input: { - sourceThreadId: ThreadId; - sourceTurnId: TurnId; - createdAt: string; - eventId: string; - planMarkdown?: string; - }, -) { - harness.emit({ - type: "turn.proposed.completed", - eventId: asEventId(input.eventId), - provider: "codex", - createdAt: input.createdAt, - threadId: input.sourceThreadId, - turnId: input.sourceTurnId, - payload: { - planMarkdown: input.planMarkdown ?? "# Source plan", - }, - }); - - const sourceThreadWithPlan = await waitForThread( - harness.engine, - (thread) => - thread.proposedPlans.some( - (proposedPlan: ProviderRuntimeTestProposedPlan) => - proposedPlan.id === `plan:${input.sourceThreadId}:turn:${input.sourceTurnId}` && - proposedPlan.implementedAt === null, - ), - 2_000, - input.sourceThreadId, - ); - const sourcePlan = sourceThreadWithPlan.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => - entry.id === `plan:${input.sourceThreadId}:turn:${input.sourceTurnId}`, - ); - expect(sourcePlan).toBeDefined(); - if (!sourcePlan) { - throw new Error("Expected source plan to exist."); - } - - return sourcePlan; -} - -async function dispatchImplementPlanTurn( - harness: ProviderRuntimeTestHarness, - input: { - targetThreadId: ThreadId; - sourceThreadId: ThreadId; - sourcePlanId: string; - commandId: string; - messageId: string; - text?: string; - }, -) { - await Effect.runPromise( - harness.engine.dispatch({ - type: "thread.turn.start", - commandId: CommandId.makeUnsafe(input.commandId), - threadId: input.targetThreadId, - message: { - messageId: asMessageId(input.messageId), - role: "user", - text: input.text ?? "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: input.sourceThreadId, - planId: input.sourcePlanId, - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: new Date().toISOString(), - }), - ); -} - type ProviderRuntimeTestReadModel = OrchestrationReadModel; type ProviderRuntimeTestThread = ProviderRuntimeTestReadModel["threads"][number]; type ProviderRuntimeTestMessage = ProviderRuntimeTestThread["messages"][number]; @@ -770,34 +638,123 @@ describe("ProviderRuntimeIngestion", () => { const targetTurnId = asTurnId("turn-plan-implement"); const createdAt = new Date().toISOString(); - await seedThreadWithReadySession(harness.engine, { - threadId: sourceThreadId, - title: "Plan Source", - createdAt, - commandKey: "plan-source", - interactionMode: "plan", - }); - await seedThreadWithReadySession(harness.engine, { - threadId: targetThreadId, - title: "Plan Target", - createdAt, - commandKey: "plan-target", - }); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-plan-source"), + threadId: sourceThreadId, + projectId: asProjectId("project-1"), + title: "Plan Source", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-plan-source"), + threadId: sourceThreadId, + session: { + threadId: sourceThreadId, + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: createdAt, + lastError: null, + }, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-plan-target"), + threadId: targetThreadId, + projectId: asProjectId("project-1"), + title: "Plan Target", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-plan-target"), + threadId: targetThreadId, + session: { + threadId: targetThreadId, + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: createdAt, + lastError: null, + }, + createdAt, + }), + ); - const sourcePlan = await seedSourcePlan(harness, { - sourceThreadId, - sourceTurnId, + harness.emit({ + type: "turn.proposed.completed", + eventId: asEventId("evt-plan-source-completed"), + provider: "codex", createdAt, - eventId: "evt-plan-source-completed", + threadId: sourceThreadId, + turnId: sourceTurnId, + payload: { + planMarkdown: "# Source plan", + }, }); - await dispatchImplementPlanTurn(harness, { - targetThreadId, + const sourceThreadWithPlan = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && + proposedPlan.implementedAt === null, + ), + 2_000, sourceThreadId, - sourcePlanId: sourcePlan.id, - commandId: "cmd-turn-start-plan-target", - messageId: "msg-plan-target", - }); + ); + const sourcePlan = sourceThreadWithPlan.proposedPlans.find( + (entry: ProviderRuntimeTestProposedPlan) => + entry.id === "plan:thread-plan:turn:turn-plan-source", + ); + expect(sourcePlan).toBeDefined(); + if (!sourcePlan) { + throw new Error("Expected source plan to exist."); + } + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target"), + threadId: targetThreadId, + message: { + messageId: asMessageId("msg-plan-target"), + role: "user", + text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", + attachments: [], + }, + sourceProposedPlan: { + threadId: sourceThreadId, + planId: sourcePlan.id, + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: new Date().toISOString(), + }), + ); const sourceThreadBeforeStart = await waitForThread( harness.engine, @@ -853,13 +810,38 @@ describe("ProviderRuntimeIngestion", () => { const staleTurnId = asTurnId("turn-stale-start"); const createdAt = new Date().toISOString(); - await seedThreadWithReadySession(harness.engine, { - threadId: sourceThreadId, - title: "Plan Source", - createdAt, - commandKey: "plan-source-guarded", - interactionMode: "plan", - }); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.create", + commandId: CommandId.makeUnsafe("cmd-thread-create-plan-source-guarded"), + threadId: sourceThreadId, + projectId: asProjectId("project-1"), + title: "Plan Source", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.session.set", + commandId: CommandId.makeUnsafe("cmd-session-set-plan-source-guarded"), + threadId: sourceThreadId, + session: { + threadId: sourceThreadId, + status: "ready", + providerName: "codex", + runtimeMode: "approval-required", + activeTurnId: null, + updatedAt: createdAt, + lastError: null, + }, + createdAt, + }), + ); harness.emit({ type: "turn.started", @@ -878,20 +860,58 @@ describe("ProviderRuntimeIngestion", () => { targetThreadId, ); - const sourcePlan = await seedSourcePlan(harness, { - sourceThreadId, - sourceTurnId, + harness.emit({ + type: "turn.proposed.completed", + eventId: asEventId("evt-plan-source-completed-guarded"), + provider: "codex", createdAt, - eventId: "evt-plan-source-completed-guarded", + threadId: sourceThreadId, + turnId: sourceTurnId, + payload: { + planMarkdown: "# Source plan", + }, }); - await dispatchImplementPlanTurn(harness, { - targetThreadId, + const sourceThreadWithPlan = await waitForThread( + harness.engine, + (thread) => + thread.proposedPlans.some( + (proposedPlan: ProviderRuntimeTestProposedPlan) => + proposedPlan.id === "plan:thread-plan:turn:turn-plan-source" && + proposedPlan.implementedAt === null, + ), + 2_000, sourceThreadId, - sourcePlanId: sourcePlan.id, - commandId: "cmd-turn-start-plan-target-guarded", - messageId: "msg-plan-target-guarded", - }); + ); + const sourcePlan = sourceThreadWithPlan.proposedPlans.find( + (entry: ProviderRuntimeTestProposedPlan) => + entry.id === "plan:thread-plan:turn:turn-plan-source", + ); + expect(sourcePlan).toBeDefined(); + if (!sourcePlan) { + throw new Error("Expected source plan to exist."); + } + + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target-guarded"), + threadId: targetThreadId, + message: { + messageId: asMessageId("msg-plan-target-guarded"), + role: "user", + text: "PLEASE IMPLEMENT THIS PLAN:\n# Source plan", + attachments: [], + }, + sourceProposedPlan: { + threadId: sourceThreadId, + planId: sourcePlan.id, + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: new Date().toISOString(), + }), + ); harness.emit({ type: "turn.started", diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index cf434d2be..09ce61f5e 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -3,7 +3,6 @@ import { DEFAULT_PROVIDER_INTERACTION_MODE, EventId, MessageId, - type OrchestrationReadModel, ProjectId, ThreadId, TurnId, @@ -18,114 +17,6 @@ const asEventId = (value: string): EventId => EventId.makeUnsafe(value); const asProjectId = (value: string): ProjectId => ProjectId.makeUnsafe(value); const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); -async function seedProjectReadModel(now: string): Promise { - return Effect.runPromise( - projectEvent(createEmptyReadModel(now), { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); -} - -async function appendThreadToReadModel( - readModel: OrchestrationReadModel, - input: { - now: string; - sequence: number; - eventId: string; - commandId: string; - threadId: string; - title: string; - interactionMode: "default" | "plan"; - runtimeMode: "approval-required" | "full-access"; - }, -): Promise { - return Effect.runPromise( - projectEvent(readModel, { - sequence: input.sequence, - eventId: asEventId(input.eventId), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe(input.threadId), - type: "thread.created", - occurredAt: input.now, - commandId: CommandId.makeUnsafe(input.commandId), - causationEventId: null, - correlationId: CommandId.makeUnsafe(input.commandId), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe(input.threadId), - projectId: asProjectId("project-1"), - title: input.title, - model: "gpt-5-codex", - interactionMode: input.interactionMode, - runtimeMode: input.runtimeMode, - branch: null, - worktreePath: null, - createdAt: input.now, - updatedAt: input.now, - }, - }), - ); -} - -async function appendProposedPlanToReadModel( - readModel: OrchestrationReadModel, - input: { - now: string; - sequence: number; - eventId: string; - commandId: string; - threadId: string; - planId: string; - turnId: string; - planMarkdown: string; - }, -): Promise { - return Effect.runPromise( - projectEvent(readModel, { - sequence: input.sequence, - eventId: asEventId(input.eventId), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe(input.threadId), - type: "thread.proposed-plan-upserted", - occurredAt: input.now, - commandId: CommandId.makeUnsafe(input.commandId), - causationEventId: null, - correlationId: CommandId.makeUnsafe(input.commandId), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe(input.threadId), - proposedPlan: { - id: input.planId, - turnId: TurnId.makeUnsafe(input.turnId), - planMarkdown: input.planMarkdown, - implementedAt: null, - implementationThreadId: null, - createdAt: input.now, - updatedAt: input.now, - }, - }, - }), - ); -} - describe("decider project scripts", () => { it("emits empty scripts on project.create", async () => { const now = new Date().toISOString(); @@ -206,16 +97,56 @@ describe("decider project scripts", () => { it("emits user message and turn-start-requested events for thread.turn.start", async () => { const now = new Date().toISOString(); - const readModel = await appendThreadToReadModel(await seedProjectReadModel(now), { - now, - sequence: 2, - eventId: "evt-thread-create", - commandId: "cmd-thread-create", - threadId: "thread-1", - title: "Thread", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - }); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-1"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-1"), + projectId: asProjectId("project-1"), + title: "Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); const result = await Effect.runPromise( decideOrchestrationCommand({ @@ -273,36 +204,108 @@ describe("decider project scripts", () => { it("carries the source proposed plan reference in turn-start-requested", async () => { const now = new Date().toISOString(); - const withSourceThread = await appendThreadToReadModel(await seedProjectReadModel(now), { - now, - sequence: 2, - eventId: "evt-thread-create-source", - commandId: "cmd-thread-create-source", - threadId: "thread-plan", - title: "Plan Thread", - interactionMode: "plan", - runtimeMode: "approval-required", - }); - const withTargetThread = await appendThreadToReadModel(withSourceThread, { - now, - sequence: 3, - eventId: "evt-thread-create-target", - commandId: "cmd-thread-create-target", - threadId: "thread-implement", - title: "Implementation Thread", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - }); - const readModel = await appendProposedPlanToReadModel(withTargetThread, { - now, - sequence: 4, - eventId: "evt-plan-upsert", - commandId: "cmd-plan-upsert", - threadId: "thread-plan", - planId: "plan-1", - turnId: "turn-1", - planMarkdown: "# Plan", - }); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const withSourceThread = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create-source"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-plan"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-source"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-plan"), + projectId: asProjectId("project-1"), + title: "Plan Thread", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + const withTargetThread = await Effect.runPromise( + projectEvent(withSourceThread, { + sequence: 3, + eventId: asEventId("evt-thread-create-target"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-implement"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-target"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-implement"), + projectId: asProjectId("project-1"), + title: "Implementation Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withTargetThread, { + sequence: 4, + eventId: asEventId("evt-plan-upsert"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-plan"), + type: "thread.proposed-plan-upserted", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-plan-upsert"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-plan-upsert"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-plan"), + proposedPlan: { + id: "plan-1", + turnId: TurnId.makeUnsafe("turn-1"), + planMarkdown: "# Plan", + implementedAt: null, + implementationThreadId: null, + createdAt: now, + updatedAt: now, + }, + }, + }), + ); const result = await Effect.runPromise( decideOrchestrationCommand({ @@ -343,26 +346,82 @@ describe("decider project scripts", () => { it("rejects thread.turn.start when the source proposed plan is missing", async () => { const now = new Date().toISOString(); - const withSourceThread = await appendThreadToReadModel(await seedProjectReadModel(now), { - now, - sequence: 2, - eventId: "evt-thread-create-source", - commandId: "cmd-thread-create-source", - threadId: "thread-plan", - title: "Plan Thread", - interactionMode: "plan", - runtimeMode: "approval-required", - }); - const readModel = await appendThreadToReadModel(withSourceThread, { - now, - sequence: 3, - eventId: "evt-thread-create-target", - commandId: "cmd-thread-create-target", - threadId: "thread-implement", - title: "Implementation Thread", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - }); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const withSourceThread = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create-source"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-plan"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-source"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-plan"), + projectId: asProjectId("project-1"), + title: "Plan Thread", + model: "gpt-5-codex", + interactionMode: "plan", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withSourceThread, { + sequence: 3, + eventId: asEventId("evt-thread-create-target"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-implement"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create-target"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-implement"), + projectId: asProjectId("project-1"), + title: "Implementation Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); await expect( Effect.runPromise( @@ -393,16 +452,56 @@ describe("decider project scripts", () => { it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => { const now = new Date().toISOString(); - const readModel = await appendThreadToReadModel(await seedProjectReadModel(now), { - now, - sequence: 2, - eventId: "evt-thread-create", - commandId: "cmd-thread-create", - threadId: "thread-1", - title: "Thread", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "full-access", - }); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-1"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-1"), + projectId: asProjectId("project-1"), + title: "Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "full-access", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); const result = await Effect.runPromise( decideOrchestrationCommand({ @@ -432,16 +531,56 @@ describe("decider project scripts", () => { it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => { const now = new Date().toISOString(); - const readModel = await appendThreadToReadModel(await seedProjectReadModel(now), { - now, - sequence: 2, - eventId: "evt-thread-create", - commandId: "cmd-thread-create", - threadId: "thread-1", - title: "Thread", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - }); + const initial = createEmptyReadModel(now); + const withProject = await Effect.runPromise( + projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-1"), + type: "project.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-project-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-1"), + title: "Project", + workspaceRoot: "/tmp/project", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }), + ); + const readModel = await Effect.runPromise( + projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-1"), + type: "thread.created", + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-thread-create"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-thread-create"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-1"), + projectId: asProjectId("project-1"), + title: "Thread", + model: "gpt-5-codex", + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }), + ); const result = await Effect.runPromise( decideOrchestrationCommand({ From 444f4e9363a8c1ae998607445dccc9206036f089 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 20:31:21 +0530 Subject: [PATCH 08/13] test(web): drop redundant plan implementation browser regression --- apps/web/src/components/ChatView.browser.tsx | 61 -------------------- 1 file changed, 61 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index df0c7d49d..e576e18e3 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -8,7 +8,6 @@ import { type ProjectId, type ServerConfig, type ThreadId, - type TurnId, type WsWelcomePayload, WS_CHANNELS, WS_METHODS, @@ -359,44 +358,6 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { }; } -function createSnapshotWithImplementedProposedPlan(): OrchestrationReadModel { - const snapshot = createSnapshotForTargetUser({ - targetMessageId: "msg-user-plan-implemented" as MessageId, - targetText: "implemented plan thread", - }); - - return { - ...snapshot, - threads: snapshot.threads.map((thread) => - thread.id === THREAD_ID - ? Object.assign({}, thread, { - interactionMode: "plan" as const, - latestTurn: { - turnId: "turn-plan-implemented" as TurnId, - state: "completed" as const, - assistantMessageId: null, - requestedAt: isoAt(900), - startedAt: isoAt(901), - completedAt: isoAt(902), - }, - proposedPlans: [ - { - id: "plan-browser-implemented", - turnId: "turn-plan-implemented" as TurnId, - planMarkdown: "# Already implemented", - implementedAt: isoAt(903), - implementationThreadId: "thread-implement-copy" as ThreadId, - createdAt: isoAt(900), - updatedAt: isoAt(903), - }, - ], - updatedAt: isoAt(903), - }) - : thread, - ), - }; -} - function resolveWsRpc(body: WsRequestEnvelope["body"]): unknown { const tag = body._tag; if (tag === ORCHESTRATION_WS_METHODS.getSnapshot) { @@ -1288,26 +1249,4 @@ describe("ChatView timeline estimator parity (full app)", () => { await mounted.cleanup(); } }); - - it("hides plan follow-up actions after the proposed plan was implemented in another thread", async () => { - const mounted = await mountChatView({ - viewport: DEFAULT_VIEWPORT, - snapshot: createSnapshotWithImplementedProposedPlan(), - }); - - try { - await vi.waitFor( - () => { - const buttonLabels = Array.from(document.querySelectorAll("button")).map((button) => - button.textContent?.trim(), - ); - expect(buttonLabels).not.toContain("Implement in new thread"); - expect(buttonLabels).not.toContain("Refine"); - }, - { timeout: 8_000, interval: 16 }, - ); - } finally { - await mounted.cleanup(); - } - }); }); From d4bd52fa2c4c6a7934ab7776035d1ea015179c91 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 20:40:39 +0530 Subject: [PATCH 09/13] fix(orchestration): persist plan consumption before clearing pending turn --- .../Layers/ProviderRuntimeIngestion.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index fb7685cd3..2c06d433d 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -919,6 +919,15 @@ const make = Effect.gen(function* () { : (thread.session?.lastError ?? null); if (shouldApplyThreadLifecycle) { + if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) { + yield* markSourceProposedPlanImplemented( + acceptedTurnStartedSourcePlan.sourceThreadId, + acceptedTurnStartedSourcePlan.sourcePlanId, + thread.id, + now, + ); + } + yield* orchestrationEngine.dispatch({ type: "thread.session.set", commandId: providerCommandId(event, "thread-session-set"), @@ -934,15 +943,6 @@ const make = Effect.gen(function* () { }, createdAt: now, }); - - if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) { - yield* markSourceProposedPlanImplemented( - acceptedTurnStartedSourcePlan.sourceThreadId, - acceptedTurnStartedSourcePlan.sourcePlanId, - thread.id, - now, - ); - } } } From 86a20d1b73c5e99ccc721f2997755534d01b6d7e Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 21:18:24 +0530 Subject: [PATCH 10/13] fix(orchestration): reject cross-project source plans --- apps/server/src/orchestration/decider.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index d15920ed9..6ea4c5175 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -285,6 +285,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" detail: `Proposed plan '${sourceProposedPlan.planId}' does not exist on thread '${sourceProposedPlan.threadId}'.`, }); } + if (sourceThread && sourceThread.projectId !== targetThread.projectId) { + return yield* new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Proposed plan '${sourceProposedPlan?.planId}' belongs to thread '${sourceThread.id}' in a different project.`, + }); + } const userMessageEvent: Omit = { ...withEventBase({ aggregateKind: "thread", From d7a5164f6622df6090f67df1bfbb6b8fec58152d Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 15 Mar 2026 23:06:33 +0530 Subject: [PATCH 11/13] fix(orchestration): isolate source plan mark failures --- .../orchestration/Layers/ProviderRuntimeIngestion.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 2c06d433d..8dacb93cf 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -925,6 +925,17 @@ const make = Effect.gen(function* () { acceptedTurnStartedSourcePlan.sourcePlanId, thread.id, now, + ).pipe( + Effect.catchCause((cause) => + Effect.logWarning( + "provider runtime ingestion failed to mark source proposed plan", + { + eventId: event.eventId, + eventType: event.type, + cause: Cause.pretty(cause), + }, + ), + ), ); } From b8e97534d2957e3275e50963f1cd579c29114cb9 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Mon, 16 Mar 2026 00:22:40 +0530 Subject: [PATCH 12/13] Queue pending turn starts and clear oldest on start failure - stop overwriting pending turn-start placeholders; append and consume oldest first - handle `provider.turn.start.failed` by deleting only the oldest pending placeholder row - extend ingestion test coverage for overlapping turn-start requests on a target thread --- .../Layers/ProjectionPipeline.ts | 10 ++++++ .../Layers/ProviderRuntimeIngestion.test.ts | 16 +++++++++ .../src/persistence/Layers/ProjectionTurns.ts | 36 +++++++++---------- .../persistence/Services/ProjectionTurns.ts | 6 ++-- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index ce633aeca..2b9d397ff 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -784,6 +784,16 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () { return; } + case "thread.activity-appended": { + if (event.payload.activity.kind !== "provider.turn.start.failed") { + return; + } + yield* projectionTurnRepository.deletePendingTurnStartByThreadId({ + threadId: event.payload.threadId, + }); + return; + } + case "thread.session-set": { const turnId = event.payload.session.activeTurnId; if (turnId === null || event.payload.session.status !== "running") { diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 5da57530c..7ca090a5c 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -755,6 +755,22 @@ describe("ProviderRuntimeIngestion", () => { createdAt: new Date().toISOString(), }), ); + await Effect.runPromise( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.makeUnsafe("cmd-turn-start-plan-target-overlap"), + threadId: targetThreadId, + message: { + messageId: asMessageId("msg-plan-target-overlap"), + role: "user", + text: "follow-up before the first turn starts", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: new Date().toISOString(), + }), + ); const sourceThreadBeforeStart = await waitForThread( harness.engine, diff --git a/apps/server/src/persistence/Layers/ProjectionTurns.ts b/apps/server/src/persistence/Layers/ProjectionTurns.ts index 72c6889c5..041cf783d 100644 --- a/apps/server/src/persistence/Layers/ProjectionTurns.ts +++ b/apps/server/src/persistence/Layers/ProjectionTurns.ts @@ -91,10 +91,16 @@ const makeProjectionTurnRepository = Effect.gen(function* () { execute: ({ threadId }) => sql` DELETE FROM projection_turns - WHERE thread_id = ${threadId} - AND turn_id IS NULL - AND state = 'pending' - AND checkpoint_turn_count IS NULL + WHERE row_id IN ( + SELECT row_id + FROM projection_turns + WHERE thread_id = ${threadId} + AND turn_id IS NULL + AND state = 'pending' + AND checkpoint_turn_count IS NULL + ORDER BY requested_at ASC, row_id ASC + LIMIT 1 + ) `, }); @@ -154,7 +160,7 @@ const makeProjectionTurnRepository = Effect.gen(function* () { AND state = 'pending' AND pending_message_id IS NOT NULL AND checkpoint_turn_count IS NULL - ORDER BY requested_at DESC + ORDER BY requested_at ASC, row_id ASC LIMIT 1 `, }); @@ -251,20 +257,14 @@ const makeProjectionTurnRepository = Effect.gen(function* () { ); const replacePendingTurnStart: ProjectionTurnRepositoryShape["replacePendingTurnStart"] = (row) => - sql - .withTransaction( - clearPendingProjectionTurnsByThread({ threadId: row.threadId }).pipe( - Effect.flatMap(() => insertPendingProjectionTurn(row)), - ), - ) - .pipe( - Effect.mapError( - toPersistenceSqlOrDecodeError( - "ProjectionTurnRepository.replacePendingTurnStart:query", - "ProjectionTurnRepository.replacePendingTurnStart:encodeRequest", - ), + insertPendingProjectionTurn(row).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionTurnRepository.replacePendingTurnStart:query", + "ProjectionTurnRepository.replacePendingTurnStart:encodeRequest", ), - ); + ), + ); const getPendingTurnStartByThreadId: ProjectionTurnRepositoryShape["getPendingTurnStartByThreadId"] = (input) => diff --git a/apps/server/src/persistence/Services/ProjectionTurns.ts b/apps/server/src/persistence/Services/ProjectionTurns.ts index b5888fdce..f90c6d6a3 100644 --- a/apps/server/src/persistence/Services/ProjectionTurns.ts +++ b/apps/server/src/persistence/Services/ProjectionTurns.ts @@ -109,21 +109,21 @@ export interface ProjectionTurnRepositoryShape { ) => Effect.Effect; /** - * Replaces any existing pending-start placeholder rows for a thread with exactly one latest pending-start row. + * Stores a pending-start placeholder row for a thread. */ readonly replacePendingTurnStart: ( row: ProjectionPendingTurnStart, ) => Effect.Effect; /** - * Returns the newest pending-start placeholder for a thread; this is expected to be at most one row after replacement writes. + * Returns the oldest pending-start placeholder for a thread. */ readonly getPendingTurnStartByThreadId: ( input: GetProjectionPendingTurnStartInput, ) => Effect.Effect, ProjectionRepositoryError>; /** - * Deletes only pending-start placeholder rows (`turnId = null`) for a thread and leaves concrete turn rows untouched. + * Deletes the oldest pending-start placeholder row for a thread and leaves concrete turn rows untouched. */ readonly deletePendingTurnStartByThreadId: ( input: GetProjectionPendingTurnStartInput, From 7af586146e5e06657c309c2c2aa5ccf3a5a72cb9 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Mon, 16 Mar 2026 00:32:01 +0530 Subject: [PATCH 13/13] test(orchestration): drop duplicate source plan decider coverage --- .../decider.projectScripts.test.ts | 249 ------------------ 1 file changed, 249 deletions(-) diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index 09ce61f5e..516d8b2a2 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -5,7 +5,6 @@ import { MessageId, ProjectId, ThreadId, - TurnId, } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; import { Effect } from "effect"; @@ -202,254 +201,6 @@ describe("decider project scripts", () => { }); }); - it("carries the source proposed plan reference in turn-start-requested", async () => { - const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const withSourceThread = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create-source"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-plan"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-source"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-plan"), - projectId: asProjectId("project-1"), - title: "Plan Thread", - model: "gpt-5-codex", - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - const withTargetThread = await Effect.runPromise( - projectEvent(withSourceThread, { - sequence: 3, - eventId: asEventId("evt-thread-create-target"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-implement"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-target"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-implement"), - projectId: asProjectId("project-1"), - title: "Implementation Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withTargetThread, { - sequence: 4, - eventId: asEventId("evt-plan-upsert"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-plan"), - type: "thread.proposed-plan-upserted", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-plan-upsert"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-plan-upsert"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-plan"), - proposedPlan: { - id: "plan-1", - turnId: TurnId.makeUnsafe("turn-1"), - planMarkdown: "# Plan", - implementedAt: null, - implementationThreadId: null, - createdAt: now, - updatedAt: now, - }, - }, - }), - ); - - const result = await Effect.runPromise( - decideOrchestrationCommand({ - command: { - type: "thread.turn.start", - commandId: CommandId.makeUnsafe("cmd-turn-start-source-plan"), - threadId: ThreadId.makeUnsafe("thread-implement"), - message: { - messageId: asMessageId("message-user-2"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Plan", - attachments: [], - }, - sourceProposedPlan: { - threadId: ThreadId.makeUnsafe("thread-plan"), - planId: "plan-1", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }, - readModel, - }), - ); - - expect(Array.isArray(result)).toBe(true); - const events = Array.isArray(result) ? result : [result]; - expect(events).toHaveLength(2); - expect(events[1]?.type).toBe("thread.turn-start-requested"); - if (events[1]?.type !== "thread.turn-start-requested") { - return; - } - expect(events[1].payload.sourceProposedPlan).toMatchObject({ - threadId: "thread-plan", - planId: "plan-1", - }); - }); - - it("rejects thread.turn.start when the source proposed plan is missing", async () => { - const now = new Date().toISOString(); - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-1"), - type: "project.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-project-create"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-1"), - title: "Project", - workspaceRoot: "/tmp/project", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); - const withSourceThread = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create-source"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-plan"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-source"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-source"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-plan"), - projectId: asProjectId("project-1"), - title: "Plan Thread", - model: "gpt-5-codex", - interactionMode: "plan", - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withSourceThread, { - sequence: 3, - eventId: asEventId("evt-thread-create-target"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-implement"), - type: "thread.created", - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-thread-create-target"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-thread-create-target"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-implement"), - projectId: asProjectId("project-1"), - title: "Implementation Thread", - model: "gpt-5-codex", - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }), - ); - - await expect( - Effect.runPromise( - decideOrchestrationCommand({ - command: { - type: "thread.turn.start", - commandId: CommandId.makeUnsafe("cmd-turn-start-missing-source-plan"), - threadId: ThreadId.makeUnsafe("thread-implement"), - message: { - messageId: asMessageId("message-user-3"), - role: "user", - text: "PLEASE IMPLEMENT THIS PLAN:\n# Missing", - attachments: [], - }, - sourceProposedPlan: { - threadId: ThreadId.makeUnsafe("thread-plan"), - planId: "plan-missing", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - createdAt: now, - }, - readModel, - }), - ), - ).rejects.toThrow("Proposed plan 'plan-missing' does not exist on thread 'thread-plan'."); - }); - it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => { const now = new Date().toISOString(); const initial = createEmptyReadModel(now);