authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-28 22:57:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-29 05:57:32+00:00
loga60daa1c764a97e88f2eef63bbac960b79274ed6
treec5f5c89b0c0d456fed78b8ccd2ed1578d34a543a
parentbc1024caf09aa349f18dd72aaf823588f3ff1fec
signature Signed by PGP key B5690EEEBB952194

Wrap orchestration reactor starts in Effect.fn (#1513)


15 files changed, 448 insertions(+), 450 deletions(-)

apps/server/integration/OrchestrationEngineHarness.integration.ts+1-1
...@@ -360,7 +360,7 @@ export const makeOrchestrationIntegrationHarness = (...@@ -360,7 +360,7 @@ export const makeOrchestrationIntegrationHarness = (
360360
361 const scope = yield* Scope.make("sequential");361 const scope = yield* Scope.make("sequential");
362 yield* tryRuntimePromise("start OrchestrationReactor", () =>362 yield* tryRuntimePromise("start OrchestrationReactor", () =>
363 runtime.runPromise(reactor.start.pipe(Scope.provide(scope))),363 runtime.runPromise(reactor.start().pipe(Scope.provide(scope))),
364 ).pipe(Effect.orDie);364 ).pipe(Effect.orDie);
365 const receiptHistory = yield* Ref.make<ReadonlyArray<OrchestrationRuntimeReceipt>>([]);365 const receiptHistory = yield* Ref.make<ReadonlyArray<OrchestrationRuntimeReceipt>>([]);
366 yield* Stream.runForEach(runtimeReceiptBus.stream, (receipt) =>366 yield* Stream.runForEach(runtimeReceiptBus.stream, (receipt) =>
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+1-1
...@@ -271,7 +271,7 @@ describe("CheckpointReactor", () => {...@@ -271,7 +271,7 @@ describe("CheckpointReactor", () => {
271 const reactor = await runtime.runPromise(Effect.service(CheckpointReactor));271 const reactor = await runtime.runPromise(Effect.service(CheckpointReactor));
272 const checkpointStore = await runtime.runPromise(Effect.service(CheckpointStore));272 const checkpointStore = await runtime.runPromise(Effect.service(CheckpointStore));
273 scope = await Effect.runPromise(Scope.make("sequential"));273 scope = await Effect.runPromise(Scope.make("sequential"));
274 await Effect.runPromise(reactor.start.pipe(Scope.provide(scope)));274 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
275 const drain = () => Effect.runPromise(reactor.drain);275 const drain = () => Effect.runPromise(reactor.drain);
276276
277 const createdAt = new Date().toISOString();277 const createdAt = new Date().toISOString();
apps/server/src/orchestration/Layers/CheckpointReactor.ts+1-1
...@@ -766,7 +766,7 @@ const make = Effect.gen(function* () {...@@ -766,7 +766,7 @@ const make = Effect.gen(function* () {
766766
767 const worker = yield* makeDrainableWorker(processInputSafely);767 const worker = yield* makeDrainableWorker(processInputSafely);
768768
769 const start: CheckpointReactorShape["start"] = Effect.gen(function* () {769 const start: CheckpointReactorShape["start"] = Effect.fn("start")(function* () {
770 yield* Effect.forkScoped(770 yield* Effect.forkScoped(
771 Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => {771 Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => {
772 if (772 if (
apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts+10-7
...@@ -24,25 +24,28 @@ describe("OrchestrationReactor", () => {...@@ -24,25 +24,28 @@ describe("OrchestrationReactor", () => {
24 Layer.effect(OrchestrationReactor, makeOrchestrationReactor).pipe(24 Layer.effect(OrchestrationReactor, makeOrchestrationReactor).pipe(
25 Layer.provideMerge(25 Layer.provideMerge(
26 Layer.succeed(ProviderRuntimeIngestionService, {26 Layer.succeed(ProviderRuntimeIngestionService, {
27 start: Effect.sync(() => {27 start: () => {
28 started.push("provider-runtime-ingestion");28 started.push("provider-runtime-ingestion");
29 }),29 return Effect.void;
30 },
30 drain: Effect.void,31 drain: Effect.void,
31 }),32 }),
32 ),33 ),
33 Layer.provideMerge(34 Layer.provideMerge(
34 Layer.succeed(ProviderCommandReactor, {35 Layer.succeed(ProviderCommandReactor, {
35 start: Effect.sync(() => {36 start: () => {
36 started.push("provider-command-reactor");37 started.push("provider-command-reactor");
37 }),38 return Effect.void;
39 },
38 drain: Effect.void,40 drain: Effect.void,
39 }),41 }),
40 ),42 ),
41 Layer.provideMerge(43 Layer.provideMerge(
42 Layer.succeed(CheckpointReactor, {44 Layer.succeed(CheckpointReactor, {
43 start: Effect.sync(() => {45 start: () => {
44 started.push("checkpoint-reactor");46 started.push("checkpoint-reactor");
45 }),47 return Effect.void;
48 },
46 drain: Effect.void,49 drain: Effect.void,
47 }),50 }),
48 ),51 ),
...@@ -51,7 +54,7 @@ describe("OrchestrationReactor", () => {...@@ -51,7 +54,7 @@ describe("OrchestrationReactor", () => {
5154
52 const reactor = await runtime.runPromise(Effect.service(OrchestrationReactor));55 const reactor = await runtime.runPromise(Effect.service(OrchestrationReactor));
53 const scope = await Effect.runPromise(Scope.make("sequential"));56 const scope = await Effect.runPromise(Scope.make("sequential"));
54 await Effect.runPromise(reactor.start.pipe(Scope.provide(scope)));57 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
5558
56 expect(started).toEqual([59 expect(started).toEqual([
57 "provider-runtime-ingestion",60 "provider-runtime-ingestion",
apps/server/src/orchestration/Layers/OrchestrationReactor.ts+4-4
...@@ -13,10 +13,10 @@ export const makeOrchestrationReactor = Effect.gen(function* () {...@@ -13,10 +13,10 @@ export const makeOrchestrationReactor = Effect.gen(function* () {
13 const providerCommandReactor = yield* ProviderCommandReactor;13 const providerCommandReactor = yield* ProviderCommandReactor;
14 const checkpointReactor = yield* CheckpointReactor;14 const checkpointReactor = yield* CheckpointReactor;
1515
16 const start: OrchestrationReactorShape["start"] = Effect.gen(function* () {16 const start: OrchestrationReactorShape["start"] = Effect.fn("start")(function* () {
17 yield* providerRuntimeIngestion.start;17 yield* providerRuntimeIngestion.start();
18 yield* providerCommandReactor.start;18 yield* providerCommandReactor.start();
19 yield* checkpointReactor.start;19 yield* checkpointReactor.start();
20 });20 });
2121
22 return {22 return {
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+1-1
...@@ -235,7 +235,7 @@ describe("ProviderCommandReactor", () => {...@@ -235,7 +235,7 @@ describe("ProviderCommandReactor", () => {
235 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));235 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
236 const reactor = await runtime.runPromise(Effect.service(ProviderCommandReactor));236 const reactor = await runtime.runPromise(Effect.service(ProviderCommandReactor));
237 scope = await Effect.runPromise(Scope.make("sequential"));237 scope = await Effect.runPromise(Scope.make("sequential"));
238 await Effect.runPromise(reactor.start.pipe(Scope.provide(scope)));238 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
239 const drain = () => Effect.runPromise(reactor.drain);239 const drain = () => Effect.runPromise(reactor.drain);
240240
241 await Effect.runPromise(241 await Effect.runPromise(
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+14-12
...@@ -773,22 +773,24 @@ const make = Effect.gen(function* () {...@@ -773,22 +773,24 @@ const make = Effect.gen(function* () {
773773
774 const worker = yield* makeDrainableWorker(processDomainEventSafely);774 const worker = yield* makeDrainableWorker(processDomainEventSafely);
775775
776 const start: ProviderCommandReactorShape["start"] = Effect.forkScoped(776 const start: ProviderCommandReactorShape["start"] = Effect.fn("start")(function* () {
777 Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => {777 const processEvent = Effect.fn("processEvent")(function* (event: OrchestrationEvent) {
778 if (778 if (
779 event.type !== "thread.runtime-mode-set" &&779 event.type === "thread.runtime-mode-set" ||
780 event.type !== "thread.turn-start-requested" &&780 event.type === "thread.turn-start-requested" ||
781 event.type !== "thread.turn-interrupt-requested" &&781 event.type === "thread.turn-interrupt-requested" ||
782 event.type !== "thread.approval-response-requested" &&782 event.type === "thread.approval-response-requested" ||
783 event.type !== "thread.user-input-response-requested" &&783 event.type === "thread.user-input-response-requested" ||
784 event.type !== "thread.session-stop-requested"784 event.type === "thread.session-stop-requested"
785 ) {785 ) {
786 return Effect.void;786 return yield* worker.enqueue(event);
787 }787 }
788 });
788789
789 return worker.enqueue(event);790 yield* Effect.forkScoped(
790 }),791 Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent),
791 ).pipe(Effect.asVoid);792 );
793 });
792794
793 return {795 return {
794 start,796 start,
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+1-1
...@@ -214,7 +214,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -214,7 +214,7 @@ describe("ProviderRuntimeIngestion", () => {
214 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));214 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
215 const ingestion = await runtime.runPromise(Effect.service(ProviderRuntimeIngestionService));215 const ingestion = await runtime.runPromise(Effect.service(ProviderRuntimeIngestionService));
216 scope = await Effect.runPromise(Scope.make("sequential"));216 scope = await Effect.runPromise(Scope.make("sequential"));
217 await Effect.runPromise(ingestion.start.pipe(Scope.provide(scope)));217 await Effect.runPromise(ingestion.start().pipe(Scope.provide(scope)));
218 const drain = () => Effect.runPromise(ingestion.drain);218 const drain = () => Effect.runPromise(ingestion.drain);
219219
220 const createdAt = new Date().toISOString();220 const createdAt = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+403-410
...@@ -500,7 +500,7 @@ function runtimeEventToActivities(...@@ -500,7 +500,7 @@ function runtimeEventToActivities(
500 return [];500 return [];
501}501}
502502
503const make = Effect.gen(function* () {503const make = Effect.fn("make")(function* () {
504 const orchestrationEngine = yield* OrchestrationEngineService;504 const orchestrationEngine = yield* OrchestrationEngineService;
505 const providerService = yield* ProviderService;505 const providerService = yield* ProviderService;
506 const projectionTurnRepository = yield* ProjectionTurnRepository;506 const projectionTurnRepository = yield* ProjectionTurnRepository;
...@@ -587,8 +587,8 @@ const make = Effect.gen(function* () {...@@ -587,8 +587,8 @@ const make = Effect.gen(function* () {
587587
588 const appendBufferedAssistantText = (messageId: MessageId, delta: string) =>588 const appendBufferedAssistantText = (messageId: MessageId, delta: string) =>
589 Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe(589 Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe(
590 Effect.flatMap((existingText) =>590 Effect.flatMap(
591 Effect.gen(function* () {591 Effect.fn("appendBufferedAssistantText")(function* (existingText) {
592 const nextText = Option.match(existingText, {592 const nextText = Option.match(existingText, {
593 onNone: () => delta,593 onNone: () => delta,
594 onSome: (text) => `${text}${delta}`,594 onSome: (text) => `${text}${delta}`,
...@@ -644,7 +644,7 @@ const make = Effect.gen(function* () {...@@ -644,7 +644,7 @@ const make = Effect.gen(function* () {
644 const clearAssistantMessageState = (messageId: MessageId) =>644 const clearAssistantMessageState = (messageId: MessageId) =>
645 clearBufferedAssistantText(messageId);645 clearBufferedAssistantText(messageId);
646646
647 const finalizeAssistantMessage = (input: {647 const finalizeAssistantMessage = Effect.fn("finalizeAssistantMessage")(function* (input: {
648 event: ProviderRuntimeEvent;648 event: ProviderRuntimeEvent;
649 threadId: ThreadId;649 threadId: ThreadId;
650 messageId: MessageId;650 messageId: MessageId;
...@@ -653,40 +653,39 @@ const make = Effect.gen(function* () {...@@ -653,40 +653,39 @@ const make = Effect.gen(function* () {
653 commandTag: string;653 commandTag: string;
654 finalDeltaCommandTag: string;654 finalDeltaCommandTag: string;
655 fallbackText?: string;655 fallbackText?: string;
656 }) =>656 }) {
657 Effect.gen(function* () {657 const bufferedText = yield* takeBufferedAssistantText(input.messageId);
658 const bufferedText = yield* takeBufferedAssistantText(input.messageId);658 const text =
659 const text =659 bufferedText.length > 0
660 bufferedText.length > 0660 ? bufferedText
661 ? bufferedText661 : (input.fallbackText?.trim().length ?? 0) > 0
662 : (input.fallbackText?.trim().length ?? 0) > 0662 ? input.fallbackText!
663 ? input.fallbackText!663 : "";
664 : "";664
665665 if (text.length > 0) {
666 if (text.length > 0) {
667 yield* orchestrationEngine.dispatch({
668 type: "thread.message.assistant.delta",
669 commandId: providerCommandId(input.event, input.finalDeltaCommandTag),
670 threadId: input.threadId,
671 messageId: input.messageId,
672 delta: text,
673 ...(input.turnId ? { turnId: input.turnId } : {}),
674 createdAt: input.createdAt,
675 });
676 }
677
678 yield* orchestrationEngine.dispatch({666 yield* orchestrationEngine.dispatch({
679 type: "thread.message.assistant.complete",667 type: "thread.message.assistant.delta",
680 commandId: providerCommandId(input.event, input.commandTag),668 commandId: providerCommandId(input.event, input.finalDeltaCommandTag),
681 threadId: input.threadId,669 threadId: input.threadId,
682 messageId: input.messageId,670 messageId: input.messageId,
671 delta: text,
683 ...(input.turnId ? { turnId: input.turnId } : {}),672 ...(input.turnId ? { turnId: input.turnId } : {}),
684 createdAt: input.createdAt,673 createdAt: input.createdAt,
685 });674 });
686 yield* clearAssistantMessageState(input.messageId);675 }
676
677 yield* orchestrationEngine.dispatch({
678 type: "thread.message.assistant.complete",
679 commandId: providerCommandId(input.event, input.commandTag),
680 threadId: input.threadId,
681 messageId: input.messageId,
682 ...(input.turnId ? { turnId: input.turnId } : {}),
683 createdAt: input.createdAt,
687 });684 });
685 yield* clearAssistantMessageState(input.messageId);
686 });
688687
689 const upsertProposedPlan = (input: {688 const upsertProposedPlan = Effect.fn("upsertProposedPlan")(function* (input: {
690 event: ProviderRuntimeEvent;689 event: ProviderRuntimeEvent;
691 threadId: ThreadId;690 threadId: ThreadId;
692 threadProposedPlans: ReadonlyArray<{691 threadProposedPlans: ReadonlyArray<{
...@@ -700,32 +699,31 @@ const make = Effect.gen(function* () {...@@ -700,32 +699,31 @@ const make = Effect.gen(function* () {
700 planMarkdown: string | undefined;699 planMarkdown: string | undefined;
701 createdAt: string;700 createdAt: string;
702 updatedAt: string;701 updatedAt: string;
703 }) =>702 }) {
704 Effect.gen(function* () {703 const planMarkdown = normalizeProposedPlanMarkdown(input.planMarkdown);
705 const planMarkdown = normalizeProposedPlanMarkdown(input.planMarkdown);704 if (!planMarkdown) {
706 if (!planMarkdown) {705 return;
707 return;706 }
708 }
709707
710 const existingPlan = input.threadProposedPlans.find((entry) => entry.id === input.planId);708 const existingPlan = input.threadProposedPlans.find((entry) => entry.id === input.planId);
711 yield* orchestrationEngine.dispatch({709 yield* orchestrationEngine.dispatch({
712 type: "thread.proposed-plan.upsert",710 type: "thread.proposed-plan.upsert",
713 commandId: providerCommandId(input.event, "proposed-plan-upsert"),711 commandId: providerCommandId(input.event, "proposed-plan-upsert"),
714 threadId: input.threadId,712 threadId: input.threadId,
715 proposedPlan: {713 proposedPlan: {
716 id: input.planId,714 id: input.planId,
717 turnId: input.turnId ?? null,715 turnId: input.turnId ?? null,
718 planMarkdown,716 planMarkdown,
719 implementedAt: existingPlan?.implementedAt ?? null,717 implementedAt: existingPlan?.implementedAt ?? null,
720 implementationThreadId: existingPlan?.implementationThreadId ?? null,718 implementationThreadId: existingPlan?.implementationThreadId ?? null,
721 createdAt: existingPlan?.createdAt ?? input.createdAt,719 createdAt: existingPlan?.createdAt ?? input.createdAt,
722 updatedAt: input.updatedAt,720 updatedAt: input.updatedAt,
723 },721 },
724 createdAt: input.updatedAt,722 createdAt: input.updatedAt,
725 });
726 });723 });
724 });
727725
728 const finalizeBufferedProposedPlan = (input: {726 const finalizeBufferedProposedPlan = Effect.fn("finalizeBufferedProposedPlan")(function* (input: {
729 event: ProviderRuntimeEvent;727 event: ProviderRuntimeEvent;
730 threadId: ThreadId;728 threadId: ThreadId;
731 threadProposedPlans: ReadonlyArray<{729 threadProposedPlans: ReadonlyArray<{
...@@ -738,66 +736,65 @@ const make = Effect.gen(function* () {...@@ -738,66 +736,65 @@ const make = Effect.gen(function* () {
738 turnId?: TurnId;736 turnId?: TurnId;
739 fallbackMarkdown?: string;737 fallbackMarkdown?: string;
740 updatedAt: string;738 updatedAt: string;
741 }) =>739 }) {
742 Effect.gen(function* () {740 const bufferedPlan = yield* takeBufferedProposedPlan(input.planId);
743 const bufferedPlan = yield* takeBufferedProposedPlan(input.planId);741 const bufferedMarkdown = normalizeProposedPlanMarkdown(bufferedPlan?.text);
744 const bufferedMarkdown = normalizeProposedPlanMarkdown(bufferedPlan?.text);742 const fallbackMarkdown = normalizeProposedPlanMarkdown(input.fallbackMarkdown);
745 const fallbackMarkdown = normalizeProposedPlanMarkdown(input.fallbackMarkdown);743 const planMarkdown = bufferedMarkdown ?? fallbackMarkdown;
746 const planMarkdown = bufferedMarkdown ?? fallbackMarkdown;744 if (!planMarkdown) {
747 if (!planMarkdown) {745 return;
748 return;746 }
749 }
750747
751 yield* upsertProposedPlan({748 yield* upsertProposedPlan({
752 event: input.event,749 event: input.event,
753 threadId: input.threadId,750 threadId: input.threadId,
754 threadProposedPlans: input.threadProposedPlans,751 threadProposedPlans: input.threadProposedPlans,
755 planId: input.planId,752 planId: input.planId,
756 ...(input.turnId ? { turnId: input.turnId } : {}),753 ...(input.turnId ? { turnId: input.turnId } : {}),
757 planMarkdown,754 planMarkdown,
758 createdAt:755 createdAt:
759 bufferedPlan?.createdAt && bufferedPlan.createdAt.length > 0756 bufferedPlan?.createdAt && bufferedPlan.createdAt.length > 0
760 ? bufferedPlan.createdAt757 ? bufferedPlan.createdAt
761 : input.updatedAt,758 : input.updatedAt,
762 updatedAt: input.updatedAt,759 updatedAt: input.updatedAt,
763 });
764 yield* clearBufferedProposedPlan(input.planId);
765 });760 });
761 yield* clearBufferedProposedPlan(input.planId);
762 });
766763
767 const clearTurnStateForSession = (threadId: ThreadId) =>764 const clearTurnStateForSession = Effect.fn("clearTurnStateForSession")(function* (
768 Effect.gen(function* () {765 threadId: ThreadId,
769 const prefix = `${threadId}:`;766 ) {
770 const proposedPlanPrefix = `plan:${threadId}:`;767 const prefix = `${threadId}:`;
771 const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));768 const proposedPlanPrefix = `plan:${threadId}:`;
772 const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));769 const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
773 yield* Effect.forEach(770 const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
774 turnKeys,771 yield* Effect.forEach(
775 (key) =>772 turnKeys,
776 Effect.gen(function* () {773 Effect.fn(function* (key) {
777 if (!key.startsWith(prefix)) {774 if (!key.startsWith(prefix)) {
778 return;775 return;
779 }776 }
780777
781 const messageIds = yield* Cache.getOption(turnMessageIdsByTurnKey, key);778 const messageIds = yield* Cache.getOption(turnMessageIdsByTurnKey, key);
782 if (Option.isSome(messageIds)) {779 if (Option.isSome(messageIds)) {
783 yield* Effect.forEach(messageIds.value, clearAssistantMessageState, {780 yield* Effect.forEach(messageIds.value, clearAssistantMessageState, {
784 concurrency: 1,781 concurrency: 1,
785 }).pipe(Effect.asVoid);782 }).pipe(Effect.asVoid);
786 }783 }
787784
788 yield* Cache.invalidate(turnMessageIdsByTurnKey, key);785 yield* Cache.invalidate(turnMessageIdsByTurnKey, key);
789 }),786 }),
790 { concurrency: 1 },787 { concurrency: 1 },
791 ).pipe(Effect.asVoid);788 ).pipe(Effect.asVoid);
792 yield* Effect.forEach(789 yield* Effect.forEach(
793 proposedPlanKeys,790 proposedPlanKeys,
794 (key) =>791 (key) =>
795 key.startsWith(proposedPlanPrefix)792 key.startsWith(proposedPlanPrefix)
796 ? Cache.invalidate(bufferedProposedPlanById, key)793 ? Cache.invalidate(bufferedProposedPlanById, key)
797 : Effect.void,794 : Effect.void,
798 { concurrency: 1 },795 { concurrency: 1 },
799 ).pipe(Effect.asVoid);796 ).pipe(Effect.asVoid);
800 });797 });
801798
802 const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fnUntraced(function* (799 const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fnUntraced(function* (
803 threadId: ThreadId,800 threadId: ThreadId,
...@@ -872,359 +869,355 @@ const make = Effect.gen(function* () {...@@ -872,359 +869,355 @@ const make = Effect.gen(function* () {
872 });869 });
873 });870 });
874871
875 const processRuntimeEvent = (event: ProviderRuntimeEvent) =>872 const processRuntimeEvent = Effect.fn("processRuntimeEvent")(function* (
876 Effect.gen(function* () {873 event: ProviderRuntimeEvent,
877 const readModel = yield* orchestrationEngine.getReadModel();874 ) {
878 const thread = readModel.threads.find((entry) => entry.id === event.threadId);875 const readModel = yield* orchestrationEngine.getReadModel();
879 if (!thread) return;876 const thread = readModel.threads.find((entry) => entry.id === event.threadId);
877 if (!thread) return;
880878
881 const now = event.createdAt;879 const now = event.createdAt;
882 const eventTurnId = toTurnId(event.turnId);880 const eventTurnId = toTurnId(event.turnId);
883 const activeTurnId = thread.session?.activeTurnId ?? null;881 const activeTurnId = thread.session?.activeTurnId ?? null;
884882
885 const conflictsWithActiveTurn =883 const conflictsWithActiveTurn =
886 activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId);884 activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId);
887 const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined;885 const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined;
888886
889 const shouldApplyThreadLifecycle = (() => {887 const shouldApplyThreadLifecycle = (() => {
890 if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {888 if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {
889 return true;
890 }
891 switch (event.type) {
892 case "session.exited":
891 return true;893 return true;
892 }894 case "session.started":
895 case "thread.started":
896 return true;
897 case "turn.started":
898 return !conflictsWithActiveTurn;
899 case "turn.completed":
900 if (conflictsWithActiveTurn || missingTurnForActiveTurn) {
901 return false;
902 }
903 // Only the active turn may close the lifecycle state.
904 if (activeTurnId !== null && eventTurnId !== undefined) {
905 return sameId(activeTurnId, eventTurnId);
906 }
907 // If no active turn is tracked, accept completion scoped to this thread.
908 return true;
909 default:
910 return true;
911 }
912 })();
913 const acceptedTurnStartedSourcePlan =
914 event.type === "turn.started" && shouldApplyThreadLifecycle
915 ? yield* getSourceProposedPlanReferenceForAcceptedTurnStart(thread.id, eventTurnId)
916 : null;
917
918 if (
919 event.type === "session.started" ||
920 event.type === "session.state.changed" ||
921 event.type === "session.exited" ||
922 event.type === "thread.started" ||
923 event.type === "turn.started" ||
924 event.type === "turn.completed"
925 ) {
926 const nextActiveTurnId =
927 event.type === "turn.started"
928 ? (eventTurnId ?? null)
929 : event.type === "turn.completed" || event.type === "session.exited"
930 ? null
931 : activeTurnId;
932 const status = (() => {
893 switch (event.type) {933 switch (event.type) {
934 case "session.state.changed":
935 return orchestrationSessionStatusFromRuntimeState(event.payload.state);
936 case "turn.started":
937 return "running";
894 case "session.exited":938 case "session.exited":
895 return true;939 return "stopped";
940 case "turn.completed":
941 return normalizeRuntimeTurnState(event.payload.state) === "failed" ? "error" : "ready";
896 case "session.started":942 case "session.started":
897 case "thread.started":943 case "thread.started":
898 return true;944 // Provider thread/session start notifications can arrive during an
899 case "turn.started":945 // active turn; preserve turn-running state in that case.
900 return !conflictsWithActiveTurn;946 return activeTurnId !== null ? "running" : "ready";
901 case "turn.completed":
902 if (conflictsWithActiveTurn || missingTurnForActiveTurn) {
903 return false;
904 }
905 // Only the active turn may close the lifecycle state.
906 if (activeTurnId !== null && eventTurnId !== undefined) {
907 return sameId(activeTurnId, eventTurnId);
908 }
909 // If no active turn is tracked, accept completion scoped to this thread.
910 return true;
911 default:
912 return true;
913 }947 }
914 })();948 })();
915 const acceptedTurnStartedSourcePlan =949 const lastError =
916 event.type === "turn.started" && shouldApplyThreadLifecycle950 event.type === "session.state.changed" && event.payload.state === "error"
917 ? yield* getSourceProposedPlanReferenceForAcceptedTurnStart(thread.id, eventTurnId)951 ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error")
918 : null;952 : event.type === "turn.completed" &&
919953 normalizeRuntimeTurnState(event.payload.state) === "failed"
920 if (954 ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed")
921 event.type === "session.started" ||955 : status === "ready"
922 event.type === "session.state.changed" ||
923 event.type === "session.exited" ||
924 event.type === "thread.started" ||
925 event.type === "turn.started" ||
926 event.type === "turn.completed"
927 ) {
928 const nextActiveTurnId =
929 event.type === "turn.started"
930 ? (eventTurnId ?? null)
931 : event.type === "turn.completed" || event.type === "session.exited"
932 ? null956 ? null
933 : activeTurnId;957 : (thread.session?.lastError ?? null);
934 const status = (() => {958
935 switch (event.type) {959 if (shouldApplyThreadLifecycle) {
936 case "session.state.changed":960 if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) {
937 return orchestrationSessionStatusFromRuntimeState(event.payload.state);961 yield* markSourceProposedPlanImplemented(
938 case "turn.started":962 acceptedTurnStartedSourcePlan.sourceThreadId,
939 return "running";963 acceptedTurnStartedSourcePlan.sourcePlanId,
940 case "session.exited":964 thread.id,
941 return "stopped";965 now,
942 case "turn.completed":966 ).pipe(
943 return normalizeRuntimeTurnState(event.payload.state) === "failed"967 Effect.catchCause((cause) =>
944 ? "error"968 Effect.logWarning("provider runtime ingestion failed to mark source proposed plan", {
945 : "ready";969 eventId: event.eventId,
946 case "session.started":970 eventType: event.type,
947 case "thread.started":971 cause: Cause.pretty(cause),
948 // Provider thread/session start notifications can arrive during an972 }),
949 // active turn; preserve turn-running state in that case.973 ),
950 return activeTurnId !== null ? "running" : "ready";974 );
951 }975 }
952 })();
953 const lastError =
954 event.type === "session.state.changed" && event.payload.state === "error"
955 ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error")
956 : event.type === "turn.completed" &&
957 normalizeRuntimeTurnState(event.payload.state) === "failed"
958 ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed")
959 : status === "ready"
960 ? null
961 : (thread.session?.lastError ?? null);
962
963 if (shouldApplyThreadLifecycle) {
964 if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) {
965 yield* markSourceProposedPlanImplemented(
966 acceptedTurnStartedSourcePlan.sourceThreadId,
967 acceptedTurnStartedSourcePlan.sourcePlanId,
968 thread.id,
969 now,
970 ).pipe(
971 Effect.catchCause((cause) =>
972 Effect.logWarning(
973 "provider runtime ingestion failed to mark source proposed plan",
974 {
975 eventId: event.eventId,
976 eventType: event.type,
977 cause: Cause.pretty(cause),
978 },
979 ),
980 ),
981 );
982 }
983976
984 yield* orchestrationEngine.dispatch({977 yield* orchestrationEngine.dispatch({
985 type: "thread.session.set",978 type: "thread.session.set",
986 commandId: providerCommandId(event, "thread-session-set"),979 commandId: providerCommandId(event, "thread-session-set"),
980 threadId: thread.id,
981 session: {
987 threadId: thread.id,982 threadId: thread.id,
988 session: {983 status,
989 threadId: thread.id,984 providerName: event.provider,
990 status,985 runtimeMode: thread.session?.runtimeMode ?? "full-access",
991 providerName: event.provider,986 activeTurnId: nextActiveTurnId,
992 runtimeMode: thread.session?.runtimeMode ?? "full-access",987 lastError,
993 activeTurnId: nextActiveTurnId,988 updatedAt: now,
994 lastError,989 },
995 updatedAt: now,990 createdAt: now,
996 },991 });
997 createdAt: now,
998 });
999 }
1000 }992 }
993 }
1001994
1002 const assistantDelta =995 const assistantDelta =
1003 event.type === "content.delta" && event.payload.streamKind === "assistant_text"996 event.type === "content.delta" && event.payload.streamKind === "assistant_text"
1004 ? event.payload.delta997 ? event.payload.delta
1005 : undefined;998 : undefined;
1006 const proposedPlanDelta =999 const proposedPlanDelta =
1007 event.type === "turn.proposed.delta" ? event.payload.delta : undefined;1000 event.type === "turn.proposed.delta" ? event.payload.delta : undefined;
10081001
1009 if (assistantDelta && assistantDelta.length > 0) {1002 if (assistantDelta && assistantDelta.length > 0) {
1010 const assistantMessageId = MessageId.makeUnsafe(1003 const assistantMessageId = MessageId.makeUnsafe(
1011 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,1004 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1012 );1005 );
1013 const turnId = toTurnId(event.turnId);1006 const turnId = toTurnId(event.turnId);
1014 if (turnId) {1007 if (turnId) {
1015 yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);1008 yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);
1016 }1009 }
10171010
1018 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(1011 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
1019 serverSettingsService.getSettings,1012 serverSettingsService.getSettings,
1020 (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),1013 (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),
1021 );1014 );
1022 if (assistantDeliveryMode === "buffered") {1015 if (assistantDeliveryMode === "buffered") {
1023 const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);1016 const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);
1024 if (spillChunk.length > 0) {1017 if (spillChunk.length > 0) {
1025 yield* orchestrationEngine.dispatch({
1026 type: "thread.message.assistant.delta",
1027 commandId: providerCommandId(event, "assistant-delta-buffer-spill"),
1028 threadId: thread.id,
1029 messageId: assistantMessageId,
1030 delta: spillChunk,
1031 ...(turnId ? { turnId } : {}),
1032 createdAt: now,
1033 });
1034 }
1035 } else {
1036 yield* orchestrationEngine.dispatch({1018 yield* orchestrationEngine.dispatch({
1037 type: "thread.message.assistant.delta",1019 type: "thread.message.assistant.delta",
1038 commandId: providerCommandId(event, "assistant-delta"),1020 commandId: providerCommandId(event, "assistant-delta-buffer-spill"),
1039 threadId: thread.id,1021 threadId: thread.id,
1040 messageId: assistantMessageId,1022 messageId: assistantMessageId,
1041 delta: assistantDelta,1023 delta: spillChunk,
1042 ...(turnId ? { turnId } : {}),1024 ...(turnId ? { turnId } : {}),
1043 createdAt: now,1025 createdAt: now,
1044 });1026 });
1045 }1027 }
1046 }1028 } else {
10471029 yield* orchestrationEngine.dispatch({
1048 if (proposedPlanDelta && proposedPlanDelta.length > 0) {1030 type: "thread.message.assistant.delta",
1049 const planId = proposedPlanIdFromEvent(event, thread.id);1031 commandId: providerCommandId(event, "assistant-delta"),
1050 yield* appendBufferedProposedPlan(planId, proposedPlanDelta, now);
1051 }
1052
1053 const assistantCompletion =
1054 event.type === "item.completed" && event.payload.itemType === "assistant_message"
1055 ? {
1056 messageId: MessageId.makeUnsafe(
1057 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1058 ),
1059 fallbackText: event.payload.detail,
1060 }
1061 : undefined;
1062 const proposedPlanCompletion =
1063 event.type === "turn.proposed.completed"
1064 ? {
1065 planId: proposedPlanIdFromEvent(event, thread.id),
1066 turnId: toTurnId(event.turnId),
1067 planMarkdown: event.payload.planMarkdown,
1068 }
1069 : undefined;
1070
1071 if (assistantCompletion) {
1072 const assistantMessageId = assistantCompletion.messageId;
1073 const turnId = toTurnId(event.turnId);
1074 const existingAssistantMessage = thread.messages.find(
1075 (entry) => entry.id === assistantMessageId,
1076 );
1077 const shouldApplyFallbackCompletionText =
1078 !existingAssistantMessage || existingAssistantMessage.text.length === 0;
1079 if (turnId) {
1080 yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);
1081 }
1082
1083 yield* finalizeAssistantMessage({
1084 event,
1085 threadId: thread.id,1032 threadId: thread.id,
1086 messageId: assistantMessageId,1033 messageId: assistantMessageId,
1034 delta: assistantDelta,
1087 ...(turnId ? { turnId } : {}),1035 ...(turnId ? { turnId } : {}),
1088 createdAt: now,1036 createdAt: now,
1089 commandTag: "assistant-complete",
1090 finalDeltaCommandTag: "assistant-delta-finalize",
1091 ...(assistantCompletion.fallbackText !== undefined && shouldApplyFallbackCompletionText
1092 ? { fallbackText: assistantCompletion.fallbackText }
1093 : {}),
1094 });1037 });
1038 }
1039 }
10951040
1096 if (turnId) {1041 if (proposedPlanDelta && proposedPlanDelta.length > 0) {
1097 yield* forgetAssistantMessageId(thread.id, turnId, assistantMessageId);1042 const planId = proposedPlanIdFromEvent(event, thread.id);
1098 }1043 yield* appendBufferedProposedPlan(planId, proposedPlanDelta, now);
1044 }
1045
1046 const assistantCompletion =
1047 event.type === "item.completed" && event.payload.itemType === "assistant_message"
1048 ? {
1049 messageId: MessageId.makeUnsafe(
1050 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1051 ),
1052 fallbackText: event.payload.detail,
1053 }
1054 : undefined;
1055 const proposedPlanCompletion =
1056 event.type === "turn.proposed.completed"
1057 ? {
1058 planId: proposedPlanIdFromEvent(event, thread.id),
1059 turnId: toTurnId(event.turnId),
1060 planMarkdown: event.payload.planMarkdown,
1061 }
1062 : undefined;
1063
1064 if (assistantCompletion) {
1065 const assistantMessageId = assistantCompletion.messageId;
1066 const turnId = toTurnId(event.turnId);
1067 const existingAssistantMessage = thread.messages.find(
1068 (entry) => entry.id === assistantMessageId,
1069 );
1070 const shouldApplyFallbackCompletionText =
1071 !existingAssistantMessage || existingAssistantMessage.text.length === 0;
1072 if (turnId) {
1073 yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);
1074 }
1075
1076 yield* finalizeAssistantMessage({
1077 event,
1078 threadId: thread.id,
1079 messageId: assistantMessageId,
1080 ...(turnId ? { turnId } : {}),
1081 createdAt: now,
1082 commandTag: "assistant-complete",
1083 finalDeltaCommandTag: "assistant-delta-finalize",
1084 ...(assistantCompletion.fallbackText !== undefined && shouldApplyFallbackCompletionText
1085 ? { fallbackText: assistantCompletion.fallbackText }
1086 : {}),
1087 });
1088
1089 if (turnId) {
1090 yield* forgetAssistantMessageId(thread.id, turnId, assistantMessageId);
1099 }1091 }
1092 }
1093
1094 if (proposedPlanCompletion) {
1095 yield* finalizeBufferedProposedPlan({
1096 event,
1097 threadId: thread.id,
1098 threadProposedPlans: thread.proposedPlans,
1099 planId: proposedPlanCompletion.planId,
1100 ...(proposedPlanCompletion.turnId ? { turnId: proposedPlanCompletion.turnId } : {}),
1101 fallbackMarkdown: proposedPlanCompletion.planMarkdown,
1102 updatedAt: now,
1103 });
1104 }
1105
1106 if (event.type === "turn.completed") {
1107 const turnId = toTurnId(event.turnId);
1108 if (turnId) {
1109 const assistantMessageIds = yield* getAssistantMessageIdsForTurn(thread.id, turnId);
1110 yield* Effect.forEach(
1111 assistantMessageIds,
1112 (assistantMessageId) =>
1113 finalizeAssistantMessage({
1114 event,
1115 threadId: thread.id,
1116 messageId: assistantMessageId,
1117 turnId,
1118 createdAt: now,
1119 commandTag: "assistant-complete-finalize",
1120 finalDeltaCommandTag: "assistant-delta-finalize-fallback",
1121 }),
1122 { concurrency: 1 },
1123 ).pipe(Effect.asVoid);
1124 yield* clearAssistantMessageIdsForTurn(thread.id, turnId);
11001125
1101 if (proposedPlanCompletion) {
1102 yield* finalizeBufferedProposedPlan({1126 yield* finalizeBufferedProposedPlan({
1103 event,1127 event,
1104 threadId: thread.id,1128 threadId: thread.id,
1105 threadProposedPlans: thread.proposedPlans,1129 threadProposedPlans: thread.proposedPlans,
1106 planId: proposedPlanCompletion.planId,1130 planId: proposedPlanIdForTurn(thread.id, turnId),
1107 ...(proposedPlanCompletion.turnId ? { turnId: proposedPlanCompletion.turnId } : {}),1131 turnId,
1108 fallbackMarkdown: proposedPlanCompletion.planMarkdown,
1109 updatedAt: now,1132 updatedAt: now,
1110 });1133 });
1111 }1134 }
1135 }
11121136
1113 if (event.type === "turn.completed") {1137 if (event.type === "session.exited") {
1114 const turnId = toTurnId(event.turnId);1138 yield* clearTurnStateForSession(thread.id);
1115 if (turnId) {1139 }
1116 const assistantMessageIds = yield* getAssistantMessageIdsForTurn(thread.id, turnId);1140
1117 yield* Effect.forEach(1141 if (event.type === "runtime.error") {
1118 assistantMessageIds,1142 const runtimeErrorMessage = event.payload.message;
1119 (assistantMessageId) =>
1120 finalizeAssistantMessage({
1121 event,
1122 threadId: thread.id,
1123 messageId: assistantMessageId,
1124 turnId,
1125 createdAt: now,
1126 commandTag: "assistant-complete-finalize",
1127 finalDeltaCommandTag: "assistant-delta-finalize-fallback",
1128 }),
1129 { concurrency: 1 },
1130 ).pipe(Effect.asVoid);
1131 yield* clearAssistantMessageIdsForTurn(thread.id, turnId);
11321143
1133 yield* finalizeBufferedProposedPlan({1144 const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD
1134 event,1145 ? true
1146 : activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId);
1147
1148 if (shouldApplyRuntimeError) {
1149 yield* orchestrationEngine.dispatch({
1150 type: "thread.session.set",
1151 commandId: providerCommandId(event, "runtime-error-session-set"),
1152 threadId: thread.id,
1153 session: {
1135 threadId: thread.id,1154 threadId: thread.id,
1136 threadProposedPlans: thread.proposedPlans,1155 status: "error",
1137 planId: proposedPlanIdForTurn(thread.id, turnId),1156 providerName: event.provider,
1138 turnId,1157 runtimeMode: thread.session?.runtimeMode ?? "full-access",
1158 activeTurnId: eventTurnId ?? null,
1159 lastError: runtimeErrorMessage,
1139 updatedAt: now,1160 updatedAt: now,
1140 });1161 },
1141 }1162 createdAt: now,
1142 }1163 });
1143
1144 if (event.type === "session.exited") {
1145 yield* clearTurnStateForSession(thread.id);
1146 }1164 }
1165 }
11471166
1148 if (event.type === "runtime.error") {1167 if (event.type === "thread.metadata.updated" && event.payload.name) {
1149 const runtimeErrorMessage = event.payload.message;1168 yield* orchestrationEngine.dispatch({
11501169 type: "thread.meta.update",
1151 const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD1170 commandId: providerCommandId(event, "thread-meta-update"),
1152 ? true1171 threadId: thread.id,
1153 : activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId);1172 title: event.payload.name,
1173 });
1174 }
11541175
1155 if (shouldApplyRuntimeError) {1176 if (event.type === "turn.diff.updated") {
1177 const turnId = toTurnId(event.turnId);
1178 if (turnId && (yield* isGitRepoForThread(thread.id))) {
1179 // Skip if a checkpoint already exists for this turn. A real
1180 // (non-placeholder) capture from CheckpointReactor should not
1181 // be clobbered, and dispatching a duplicate placeholder for the
1182 // same turnId would produce an unstable checkpointTurnCount.
1183 if (thread.checkpoints.some((c) => c.turnId === turnId)) {
1184 // Already tracked; no-op.
1185 } else {
1186 const assistantMessageId = MessageId.makeUnsafe(
1187 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1188 );
1189 const maxTurnCount = thread.checkpoints.reduce(
1190 (max, c) => Math.max(max, c.checkpointTurnCount),
1191 0,
1192 );
1156 yield* orchestrationEngine.dispatch({1193 yield* orchestrationEngine.dispatch({
1157 type: "thread.session.set",1194 type: "thread.turn.diff.complete",
1158 commandId: providerCommandId(event, "runtime-error-session-set"),1195 commandId: providerCommandId(event, "thread-turn-diff-complete"),
1159 threadId: thread.id,1196 threadId: thread.id,
1160 session: {1197 turnId,
1161 threadId: thread.id,1198 completedAt: now,
1162 status: "error",1199 checkpointRef: CheckpointRef.makeUnsafe(`provider-diff:${event.eventId}`),
1163 providerName: event.provider,1200 status: "missing",
1164 runtimeMode: thread.session?.runtimeMode ?? "full-access",1201 files: [],
1165 activeTurnId: eventTurnId ?? null,1202 assistantMessageId,
1166 lastError: runtimeErrorMessage,1203 checkpointTurnCount: maxTurnCount + 1,
1167 updatedAt: now,
1168 },
1169 createdAt: now,1204 createdAt: now,
1170 });1205 });
1171 }1206 }
1172 }1207 }
1208 }
11731209
1174 if (event.type === "thread.metadata.updated" && event.payload.name) {1210 const activities = runtimeEventToActivities(event);
1175 yield* orchestrationEngine.dispatch({1211 yield* Effect.forEach(activities, (activity) =>
1176 type: "thread.meta.update",1212 orchestrationEngine.dispatch({
1177 commandId: providerCommandId(event, "thread-meta-update"),1213 type: "thread.activity.append",
1178 threadId: thread.id,1214 commandId: providerCommandId(event, "thread-activity-append"),
1179 title: event.payload.name,1215 threadId: thread.id,
1180 });1216 activity,
1181 }1217 createdAt: activity.createdAt,
11821218 }),
1183 if (event.type === "turn.diff.updated") {1219 ).pipe(Effect.asVoid);
1184 const turnId = toTurnId(event.turnId);1220 });
1185 if (turnId && (yield* isGitRepoForThread(thread.id))) {
1186 // Skip if a checkpoint already exists for this turn. A real
1187 // (non-placeholder) capture from CheckpointReactor should not
1188 // be clobbered, and dispatching a duplicate placeholder for the
1189 // same turnId would produce an unstable checkpointTurnCount.
1190 if (thread.checkpoints.some((c) => c.turnId === turnId)) {
1191 // Already tracked; no-op.
1192 } else {
1193 const assistantMessageId = MessageId.makeUnsafe(
1194 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1195 );
1196 const maxTurnCount = thread.checkpoints.reduce(
1197 (max, c) => Math.max(max, c.checkpointTurnCount),
1198 0,
1199 );
1200 yield* orchestrationEngine.dispatch({
1201 type: "thread.turn.diff.complete",
1202 commandId: providerCommandId(event, "thread-turn-diff-complete"),
1203 threadId: thread.id,
1204 turnId,
1205 completedAt: now,
1206 checkpointRef: CheckpointRef.makeUnsafe(`provider-diff:${event.eventId}`),
1207 status: "missing",
1208 files: [],
1209 assistantMessageId,
1210 checkpointTurnCount: maxTurnCount + 1,
1211 createdAt: now,
1212 });
1213 }
1214 }
1215 }
1216
1217 const activities = runtimeEventToActivities(event);
1218 yield* Effect.forEach(activities, (activity) =>
1219 orchestrationEngine.dispatch({
1220 type: "thread.activity.append",
1221 commandId: providerCommandId(event, "thread-activity-append"),
1222 threadId: thread.id,
1223 activity,
1224 createdAt: activity.createdAt,
1225 }),
1226 ).pipe(Effect.asVoid);
1227 });
12281221
1229 const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;1222 const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
12301223
...@@ -1248,7 +1241,7 @@ const make = Effect.gen(function* () {...@@ -1248,7 +1241,7 @@ const make = Effect.gen(function* () {
12481241
1249 const worker = yield* makeDrainableWorker(processInputSafely);1242 const worker = yield* makeDrainableWorker(processInputSafely);
12501243
1251 const start: ProviderRuntimeIngestionShape["start"] = Effect.gen(function* () {1244 const start: ProviderRuntimeIngestionShape["start"] = Effect.fn("start")(function* () {
1252 yield* Effect.forkScoped(1245 yield* Effect.forkScoped(
1253 Stream.runForEach(providerService.streamEvents, (event) =>1246 Stream.runForEach(providerService.streamEvents, (event) =>
1254 worker.enqueue({ source: "runtime", event }),1247 worker.enqueue({ source: "runtime", event }),
...@@ -1272,5 +1265,5 @@ const make = Effect.gen(function* () {...@@ -1272,5 +1265,5 @@ const make = Effect.gen(function* () {
12721265
1273export const ProviderRuntimeIngestionLive = Layer.effect(1266export const ProviderRuntimeIngestionLive = Layer.effect(
1274 ProviderRuntimeIngestionService,1267 ProviderRuntimeIngestionService,
1275 make,1268 make(),
1276).pipe(Layer.provide(ProjectionTurnRepositoryLive));1269).pipe(Layer.provide(ProjectionTurnRepositoryLive));
apps/server/src/orchestration/Services/CheckpointReactor.ts+1-1
...@@ -22,7 +22,7 @@ export interface CheckpointReactorShape {...@@ -22,7 +22,7 @@ export interface CheckpointReactorShape {
22 * Consumes both orchestration-domain and provider-runtime events via an22 * Consumes both orchestration-domain and provider-runtime events via an
23 * internal queue.23 * internal queue.
24 */24 */
25 readonly start: Effect.Effect<void, never, Scope.Scope>;25 readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2626
27 /**27 /**
28 * Resolves when the internal processing queue is empty and idle.28 * Resolves when the internal processing queue is empty and idle.
apps/server/src/orchestration/Services/OrchestrationReactor.ts+1-1
...@@ -19,7 +19,7 @@ export interface OrchestrationReactorShape {...@@ -19,7 +19,7 @@ export interface OrchestrationReactorShape {
19 * The returned effect must be run in a scope so all worker fibers can be19 * The returned effect must be run in a scope so all worker fibers can be
20 * finalized on shutdown.20 * finalized on shutdown.
21 */21 */
22 readonly start: Effect.Effect<void, never, Scope.Scope>;22 readonly start: () => Effect.Effect<void, never, Scope.Scope>;
23}23}
2424
25/**25/**
apps/server/src/orchestration/Services/ProviderCommandReactor.ts+1-1
...@@ -22,7 +22,7 @@ export interface ProviderCommandReactorShape {...@@ -22,7 +22,7 @@ export interface ProviderCommandReactorShape {
22 * Filters orchestration domain events to provider-intent types before22 * Filters orchestration domain events to provider-intent types before
23 * processing.23 * processing.
24 */24 */
25 readonly start: Effect.Effect<void, never, Scope.Scope>;25 readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2626
27 /**27 /**
28 * Resolves when the internal processing queue is empty and idle.28 * Resolves when the internal processing queue is empty and idle.
apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts+1-1
...@@ -22,7 +22,7 @@ export interface ProviderRuntimeIngestionShape {...@@ -22,7 +22,7 @@ export interface ProviderRuntimeIngestionShape {
22 * Uses an internal queue and continues after non-interrupt failures by22 * Uses an internal queue and continues after non-interrupt failures by
23 * logging warnings.23 * logging warnings.
24 */24 */
25 readonly start: Effect.Effect<void, never, Scope.Scope>;25 readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2626
27 /**27 /**
28 * Resolves when the internal processing queue is empty and idle.28 * Resolves when the internal processing queue is empty and idle.
apps/server/src/wsServer.ts+1-1
...@@ -646,7 +646,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<...@@ -646,7 +646,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
646 }),646 }),
647 ).pipe(Effect.forkIn(subscriptionsScope));647 ).pipe(Effect.forkIn(subscriptionsScope));
648648
649 yield* Scope.provide(orchestrationReactor.start, subscriptionsScope);649 yield* Scope.provide(orchestrationReactor.start(), subscriptionsScope);
650 yield* readiness.markOrchestrationSubscriptionsReady;650 yield* readiness.markOrchestrationSubscriptionsReady;
651651
652 let welcomeBootstrapProjectId: ProjectId | undefined;652 let welcomeBootstrapProjectId: ProjectId | undefined;
docs/effect-fn-checklist.md+7-7
...@@ -46,7 +46,7 @@ Effect.fn("name")(...@@ -46,7 +46,7 @@ Effect.fn("name")(
46- [x] `apps/server/src/provider/Layers/CodexAdapter.ts`46- [x] `apps/server/src/provider/Layers/CodexAdapter.ts`
47- [x] `apps/server/src/git/Layers/GitCore.ts`47- [x] `apps/server/src/git/Layers/GitCore.ts`
48- [x] `apps/server/src/git/Layers/GitManager.ts`48- [x] `apps/server/src/git/Layers/GitManager.ts`
49- [ ] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`49- [x] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`
50- [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`50- [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`
51- [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts`51- [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts`
52- [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts`52- [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts`
...@@ -115,12 +115,12 @@ Effect.fn("name")(...@@ -115,12 +115,12 @@ Effect.fn("name")(
115115
116### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)116### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`)
117117
118- [ ] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680)118- [x] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680)
119- [ ] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722)119- [x] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722)
120- [ ] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761)120- [x] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761)
121- [ ] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800)121- [x] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800)
122- [ ] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908)122- [x] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908)
123- [ ] Nested callback wrappers in this file123- [x] Nested callback wrappers in this file
124124
125### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`)125### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`)
126126