History

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

Julius Marminge committed 7 months ago

Commit a60daa11 parent bc1024c15 files · +448 −450Browse files

Unified · Split

15 files
  1. apps/server/integration/OrchestrationEngineHarness.integration.ts +1 −1
  2. apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +1 −1
  3. apps/server/src/orchestration/Layers/CheckpointReactor.ts +1 −1
  4. apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts +10 −7
  5. apps/server/src/orchestration/Layers/OrchestrationReactor.ts +4 −4
  6. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +1 −1
  7. apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +14 −12
  8. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +1 −1
  9. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +403 −410
  10. apps/server/src/orchestration/Services/CheckpointReactor.ts +1 −1
  11. apps/server/src/orchestration/Services/OrchestrationReactor.ts +1 −1
  12. apps/server/src/orchestration/Services/ProviderCommandReactor.ts +1 −1
  13. apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts +1 −1
  14. apps/server/src/wsServer.ts +1 −1
  15. docs/effect-fn-checklist.md +7 −7
apps/server/integration/OrchestrationEngineHarness.integration.ts+1−1
@@ -360,7 +360,7 @@ export const makeOrchestrationIntegrationHarness = ( · 359 unmodified lines
360360
361361 const scope = yield* Scope.make("sequential");
362362 yield* tryRuntimePromise("start OrchestrationReactor", () =>
363− runtime.runPromise(reactor.start.pipe(Scope.provide(scope))),
363+ runtime.runPromise(reactor.start().pipe(Scope.provide(scope))),
364364 ).pipe(Effect.orDie);
365365 const receiptHistory = yield* Ref.make<ReadonlyArray<OrchestrationRuntimeReceipt>>([]);
366366 yield* Stream.runForEach(runtimeReceiptBus.stream, (receipt) =>
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+1−1
@@ -271,7 +271,7 @@ describe("CheckpointReactor", () => { · 270 unmodified lines
271271 const reactor = await runtime.runPromise(Effect.service(CheckpointReactor));
272272 const checkpointStore = await runtime.runPromise(Effect.service(CheckpointStore));
273273 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)));
275275 const drain = () => Effect.runPromise(reactor.drain);
276276
277277 const createdAt = new Date().toISOString();
apps/server/src/orchestration/Layers/CheckpointReactor.ts+1−1
@@ -766,7 +766,7 @@ const make = Effect.gen(function* () { · 765 unmodified lines
766766
767767 const worker = yield* makeDrainableWorker(processInputSafely);
768768
769− const start: CheckpointReactorShape["start"] = Effect.gen(function* () {
769+ const start: CheckpointReactorShape["start"] = Effect.fn("start")(function* () {
770770 yield* Effect.forkScoped(
771771 Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => {
772772 if (
apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts+10−7
@@ -24,25 +24,28 @@ describe("OrchestrationReactor", () => { · 23 unmodified lines
2424 Layer.effect(OrchestrationReactor, makeOrchestrationReactor).pipe(
2525 Layer.provideMerge(
2626 Layer.succeed(ProviderRuntimeIngestionService, {
27− start: Effect.sync(() => {
27+ start: () => {
2828 started.push("provider-runtime-ingestion");
29− }),
29+ return Effect.void;
30+ },
3031 drain: Effect.void,
3132 }),
3233 ),
3334 Layer.provideMerge(
3435 Layer.succeed(ProviderCommandReactor, {
35− start: Effect.sync(() => {
36+ start: () => {
3637 started.push("provider-command-reactor");
37− }),
38+ return Effect.void;
39+ },
3840 drain: Effect.void,
3941 }),
4042 ),
4143 Layer.provideMerge(
4244 Layer.succeed(CheckpointReactor, {
43− start: Effect.sync(() => {
45+ start: () => {
4446 started.push("checkpoint-reactor");
45− }),
47+ return Effect.void;
48+ },
4649 drain: Effect.void,
4750 }),
4851 ),
@@ -51,7 +54,7 @@ describe("OrchestrationReactor", () => { · 2 unmodified lines
5154
5255 const reactor = await runtime.runPromise(Effect.service(OrchestrationReactor));
5356 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
5659 expect(started).toEqual([
5760 "provider-runtime-ingestion",
apps/server/src/orchestration/Layers/OrchestrationReactor.ts+4−4
@@ -13,10 +13,10 @@ export const makeOrchestrationReactor = Effect.gen(function* () { · 12 unmodified lines
1313 const providerCommandReactor = yield* ProviderCommandReactor;
1414 const checkpointReactor = yield* CheckpointReactor;
1515
16− const start: OrchestrationReactorShape["start"] = Effect.gen(function* () {
17− yield* providerRuntimeIngestion.start;
18− yield* providerCommandReactor.start;
19− yield* checkpointReactor.start;
16+ const start: OrchestrationReactorShape["start"] = Effect.fn("start")(function* () {
17+ yield* providerRuntimeIngestion.start();
18+ yield* providerCommandReactor.start();
19+ yield* checkpointReactor.start();
2020 });
2121
2222 return {
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+1−1
@@ -235,7 +235,7 @@ describe("ProviderCommandReactor", () => { · 234 unmodified lines
235235 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
236236 const reactor = await runtime.runPromise(Effect.service(ProviderCommandReactor));
237237 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)));
239239 const drain = () => Effect.runPromise(reactor.drain);
240240
241241 await Effect.runPromise(
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+14−12
@@ -773,22 +773,24 @@ const make = Effect.gen(function* () { · 772 unmodified lines
773773
774774 const worker = yield* makeDrainableWorker(processDomainEventSafely);
775775
776− const start: ProviderCommandReactorShape["start"] = Effect.forkScoped(
777− Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => {
776+ const start: ProviderCommandReactorShape["start"] = Effect.fn("start")(function* () {
777+ const processEvent = Effect.fn("processEvent")(function* (event: OrchestrationEvent) {
778778 if (
779− event.type !== "thread.runtime-mode-set" &&
780− event.type !== "thread.turn-start-requested" &&
781− event.type !== "thread.turn-interrupt-requested" &&
782− event.type !== "thread.approval-response-requested" &&
783− event.type !== "thread.user-input-response-requested" &&
784− event.type !== "thread.session-stop-requested"
779+ event.type === "thread.runtime-mode-set" ||
780+ event.type === "thread.turn-start-requested" ||
781+ event.type === "thread.turn-interrupt-requested" ||
782+ event.type === "thread.approval-response-requested" ||
783+ event.type === "thread.user-input-response-requested" ||
784+ event.type === "thread.session-stop-requested"
785785 ) {
786− return Effect.void;
786+ return yield* worker.enqueue(event);
787787 }
788+ });
788789
789− return worker.enqueue(event);
790− }),
791− ).pipe(Effect.asVoid);
790+ yield* Effect.forkScoped(
791+ Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent),
792+ );
793+ });
792794
793795 return {
794796 start,
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+1−1
@@ -214,7 +214,7 @@ describe("ProviderRuntimeIngestion", () => { · 213 unmodified lines
214214 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
215215 const ingestion = await runtime.runPromise(Effect.service(ProviderRuntimeIngestionService));
216216 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)));
218218 const drain = () => Effect.runPromise(ingestion.drain);
219219
220220 const createdAt = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+403−410
@@ -500,7 +500,7 @@ function runtimeEventToActivities( · 499 unmodified lines
500500 return [];
501501 }
502502
503−const make = Effect.gen(function* () {
503+const make = Effect.fn("make")(function* () {
504504 const orchestrationEngine = yield* OrchestrationEngineService;
505505 const providerService = yield* ProviderService;
506506 const projectionTurnRepository = yield* ProjectionTurnRepository;
@@ -587,8 +587,8 @@ const make = Effect.gen(function* () { · 80 unmodified lines
587587
588588 const appendBufferedAssistantText = (messageId: MessageId, delta: string) =>
589589 Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe(
590− Effect.flatMap((existingText) =>
591− Effect.gen(function* () {
590+ Effect.flatMap(
591+ Effect.fn("appendBufferedAssistantText")(function* (existingText) {
592592 const nextText = Option.match(existingText, {
593593 onNone: () => delta,
594594 onSome: (text) => `${text}${delta}`,
@@ -644,7 +644,7 @@ const make = Effect.gen(function* () { · 49 unmodified lines
644644 const clearAssistantMessageState = (messageId: MessageId) =>
645645 clearBufferedAssistantText(messageId);
646646
647− const finalizeAssistantMessage = (input: {
647+ const finalizeAssistantMessage = Effect.fn("finalizeAssistantMessage")(function* (input: {
648648 event: ProviderRuntimeEvent;
649649 threadId: ThreadId;
650650 messageId: MessageId;
@@ -653,40 +653,39 @@ const make = Effect.gen(function* () { · 2 unmodified lines
653653 commandTag: string;
654654 finalDeltaCommandTag: string;
655655 fallbackText?: string;
656− }) =>
657− Effect.gen(function* () {
658− const bufferedText = yield* takeBufferedAssistantText(input.messageId);
659− const text =
660− bufferedText.length > 0
661− ? bufferedText
662− : (input.fallbackText?.trim().length ?? 0) > 0
663− ? input.fallbackText!
664− : "";
665−
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−
656+ }) {
657+ const bufferedText = yield* takeBufferedAssistantText(input.messageId);
658+ const text =
659+ bufferedText.length > 0
660+ ? bufferedText
661+ : (input.fallbackText?.trim().length ?? 0) > 0
662+ ? input.fallbackText!
663+ : "";
664+
665+ if (text.length > 0) {
678666 yield* orchestrationEngine.dispatch({
679− type: "thread.message.assistant.complete",
680− commandId: providerCommandId(input.event, input.commandTag),
667+ type: "thread.message.assistant.delta",
668+ commandId: providerCommandId(input.event, input.finalDeltaCommandTag),
681669 threadId: input.threadId,
682670 messageId: input.messageId,
671+ delta: text,
683672 ...(input.turnId ? { turnId: input.turnId } : {}),
684673 createdAt: input.createdAt,
685674 });
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,
687684 });
685+ yield* clearAssistantMessageState(input.messageId);
686+ });
688687
689− const upsertProposedPlan = (input: {
688+ const upsertProposedPlan = Effect.fn("upsertProposedPlan")(function* (input: {
690689 event: ProviderRuntimeEvent;
691690 threadId: ThreadId;
692691 threadProposedPlans: ReadonlyArray<{
@@ -700,32 +699,31 @@ const make = Effect.gen(function* () { · 7 unmodified lines
700699 planMarkdown: string | undefined;
701700 createdAt: string;
702701 updatedAt: string;
703− }) =>
704− Effect.gen(function* () {
705− const planMarkdown = normalizeProposedPlanMarkdown(input.planMarkdown);
706− if (!planMarkdown) {
707− return;
708− }
702+ }) {
703+ const planMarkdown = normalizeProposedPlanMarkdown(input.planMarkdown);
704+ if (!planMarkdown) {
705+ return;
706+ }
709707
710− const existingPlan = input.threadProposedPlans.find((entry) => entry.id === input.planId);
711− yield* orchestrationEngine.dispatch({
712− type: "thread.proposed-plan.upsert",
713− commandId: providerCommandId(input.event, "proposed-plan-upsert"),
714− threadId: input.threadId,
715− proposedPlan: {
716− id: input.planId,
717− turnId: input.turnId ?? null,
718− planMarkdown,
719− implementedAt: existingPlan?.implementedAt ?? null,
720− implementationThreadId: existingPlan?.implementationThreadId ?? null,
721− createdAt: existingPlan?.createdAt ?? input.createdAt,
722− updatedAt: input.updatedAt,
723− },
724− createdAt: input.updatedAt,
725− });
708+ const existingPlan = input.threadProposedPlans.find((entry) => entry.id === input.planId);
709+ yield* orchestrationEngine.dispatch({
710+ type: "thread.proposed-plan.upsert",
711+ commandId: providerCommandId(input.event, "proposed-plan-upsert"),
712+ threadId: input.threadId,
713+ proposedPlan: {
714+ id: input.planId,
715+ turnId: input.turnId ?? null,
716+ planMarkdown,
717+ implementedAt: existingPlan?.implementedAt ?? null,
718+ implementationThreadId: existingPlan?.implementationThreadId ?? null,
719+ createdAt: existingPlan?.createdAt ?? input.createdAt,
720+ updatedAt: input.updatedAt,
721+ },
722+ createdAt: input.updatedAt,
726723 });
724+ });
727725
728− const finalizeBufferedProposedPlan = (input: {
726+ const finalizeBufferedProposedPlan = Effect.fn("finalizeBufferedProposedPlan")(function* (input: {
729727 event: ProviderRuntimeEvent;
730728 threadId: ThreadId;
731729 threadProposedPlans: ReadonlyArray<{
@@ -738,66 +736,65 @@ const make = Effect.gen(function* () { · 6 unmodified lines
738736 turnId?: TurnId;
739737 fallbackMarkdown?: string;
740738 updatedAt: string;
741− }) =>
742− Effect.gen(function* () {
743− const bufferedPlan = yield* takeBufferedProposedPlan(input.planId);
744− const bufferedMarkdown = normalizeProposedPlanMarkdown(bufferedPlan?.text);
745− const fallbackMarkdown = normalizeProposedPlanMarkdown(input.fallbackMarkdown);
746− const planMarkdown = bufferedMarkdown ?? fallbackMarkdown;
747− if (!planMarkdown) {
748− return;
749− }
739+ }) {
740+ const bufferedPlan = yield* takeBufferedProposedPlan(input.planId);
741+ const bufferedMarkdown = normalizeProposedPlanMarkdown(bufferedPlan?.text);
742+ const fallbackMarkdown = normalizeProposedPlanMarkdown(input.fallbackMarkdown);
743+ const planMarkdown = bufferedMarkdown ?? fallbackMarkdown;
744+ if (!planMarkdown) {
745+ return;
746+ }
750747
751− yield* upsertProposedPlan({
752− event: input.event,
753− threadId: input.threadId,
754− threadProposedPlans: input.threadProposedPlans,
755− planId: input.planId,
756− ...(input.turnId ? { turnId: input.turnId } : {}),
757− planMarkdown,
758− createdAt:
759− bufferedPlan?.createdAt && bufferedPlan.createdAt.length > 0
760− ? bufferedPlan.createdAt
761− : input.updatedAt,
762− updatedAt: input.updatedAt,
763− });
764− yield* clearBufferedProposedPlan(input.planId);
748+ yield* upsertProposedPlan({
749+ event: input.event,
750+ threadId: input.threadId,
751+ threadProposedPlans: input.threadProposedPlans,
752+ planId: input.planId,
753+ ...(input.turnId ? { turnId: input.turnId } : {}),
754+ planMarkdown,
755+ createdAt:
756+ bufferedPlan?.createdAt && bufferedPlan.createdAt.length > 0
757+ ? bufferedPlan.createdAt
758+ : input.updatedAt,
759+ updatedAt: input.updatedAt,
765760 });
761+ yield* clearBufferedProposedPlan(input.planId);
762+ });
766763
767− const clearTurnStateForSession = (threadId: ThreadId) =>
768− Effect.gen(function* () {
769− const prefix = `${threadId}:`;
770− const proposedPlanPrefix = `plan:${threadId}:`;
771− const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
772− const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
773− yield* Effect.forEach(
774− turnKeys,
775− (key) =>
776− Effect.gen(function* () {
777− if (!key.startsWith(prefix)) {
778− return;
779− }
764+ const clearTurnStateForSession = Effect.fn("clearTurnStateForSession")(function* (
765+ threadId: ThreadId,
766+ ) {
767+ const prefix = `${threadId}:`;
768+ const proposedPlanPrefix = `plan:${threadId}:`;
769+ const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey));
770+ const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById));
771+ yield* Effect.forEach(
772+ turnKeys,
773+ Effect.fn(function* (key) {
774+ if (!key.startsWith(prefix)) {
775+ return;
776+ }
780777
781− const messageIds = yield* Cache.getOption(turnMessageIdsByTurnKey, key);
782− if (Option.isSome(messageIds)) {
783− yield* Effect.forEach(messageIds.value, clearAssistantMessageState, {
784− concurrency: 1,
785− }).pipe(Effect.asVoid);
786− }
778+ const messageIds = yield* Cache.getOption(turnMessageIdsByTurnKey, key);
779+ if (Option.isSome(messageIds)) {
780+ yield* Effect.forEach(messageIds.value, clearAssistantMessageState, {
781+ concurrency: 1,
782+ }).pipe(Effect.asVoid);
783+ }
787784
788− yield* Cache.invalidate(turnMessageIdsByTurnKey, key);
789− }),
790− { concurrency: 1 },
791− ).pipe(Effect.asVoid);
792− yield* Effect.forEach(
793− proposedPlanKeys,
794− (key) =>
795− key.startsWith(proposedPlanPrefix)
796− ? Cache.invalidate(bufferedProposedPlanById, key)
797− : Effect.void,
798− { concurrency: 1 },
799− ).pipe(Effect.asVoid);
800− });
785+ yield* Cache.invalidate(turnMessageIdsByTurnKey, key);
786+ }),
787+ { concurrency: 1 },
788+ ).pipe(Effect.asVoid);
789+ yield* Effect.forEach(
790+ proposedPlanKeys,
791+ (key) =>
792+ key.startsWith(proposedPlanPrefix)
793+ ? Cache.invalidate(bufferedProposedPlanById, key)
794+ : Effect.void,
795+ { concurrency: 1 },
796+ ).pipe(Effect.asVoid);
797+ });
801798
802799 const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fnUntraced(function* (
803800 threadId: ThreadId,
@@ -872,359 +869,355 @@ const make = Effect.gen(function* () { · 68 unmodified lines
872869 });
873870 });
874871
875− const processRuntimeEvent = (event: ProviderRuntimeEvent) =>
876− Effect.gen(function* () {
877− const readModel = yield* orchestrationEngine.getReadModel();
878− const thread = readModel.threads.find((entry) => entry.id === event.threadId);
879− if (!thread) return;
872+ const processRuntimeEvent = Effect.fn("processRuntimeEvent")(function* (
873+ event: ProviderRuntimeEvent,
874+ ) {
875+ const readModel = yield* orchestrationEngine.getReadModel();
876+ const thread = readModel.threads.find((entry) => entry.id === event.threadId);
877+ if (!thread) return;
880878
881− const now = event.createdAt;
882− const eventTurnId = toTurnId(event.turnId);
883− const activeTurnId = thread.session?.activeTurnId ?? null;
879+ const now = event.createdAt;
880+ const eventTurnId = toTurnId(event.turnId);
881+ const activeTurnId = thread.session?.activeTurnId ?? null;
884882
885− const conflictsWithActiveTurn =
886− activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId);
887− const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined;
883+ const conflictsWithActiveTurn =
884+ activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId);
885+ const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined;
888886
889− const shouldApplyThreadLifecycle = (() => {
890− if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {
887+ const shouldApplyThreadLifecycle = (() => {
888+ if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {
889+ return true;
890+ }
891+ switch (event.type) {
892+ case "session.exited":
891893 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 = (() => {
893933 switch (event.type) {
934+ case "session.state.changed":
935+ return orchestrationSessionStatusFromRuntimeState(event.payload.state);
936+ case "turn.started":
937+ return "running";
894938 case "session.exited":
895− return true;
939+ return "stopped";
940+ case "turn.completed":
941+ return normalizeRuntimeTurnState(event.payload.state) === "failed" ? "error" : "ready";
896942 case "session.started":
897943 case "thread.started":
898− return true;
899− case "turn.started":
900− return !conflictsWithActiveTurn;
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;
944+ // Provider thread/session start notifications can arrive during an
945+ // active turn; preserve turn-running state in that case.
946+ return activeTurnId !== null ? "running" : "ready";
913947 }
914948 })();
915− const acceptedTurnStartedSourcePlan =
916− event.type === "turn.started" && shouldApplyThreadLifecycle
917− ? yield* getSourceProposedPlanReferenceForAcceptedTurnStart(thread.id, eventTurnId)
918− : null;
919−
920− if (
921− event.type === "session.started" ||
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"
949+ const lastError =
950+ event.type === "session.state.changed" && event.payload.state === "error"
951+ ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error")
952+ : event.type === "turn.completed" &&
953+ normalizeRuntimeTurnState(event.payload.state) === "failed"
954+ ? (event.payload.errorMessage ?? thread.session?.lastError ?? "Turn failed")
955+ : status === "ready"
932956 ? null
933− : activeTurnId;
934− const status = (() => {
935− switch (event.type) {
936− case "session.state.changed":
937− return orchestrationSessionStatusFromRuntimeState(event.payload.state);
938− case "turn.started":
939− return "running";
940− case "session.exited":
941− return "stopped";
942− case "turn.completed":
943− return normalizeRuntimeTurnState(event.payload.state) === "failed"
944− ? "error"
945− : "ready";
946− case "session.started":
947− case "thread.started":
948− // Provider thread/session start notifications can arrive during an
949− // active turn; preserve turn-running state in that case.
950− return activeTurnId !== null ? "running" : "ready";
951− }
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− }
957+ : (thread.session?.lastError ?? null);
958+
959+ if (shouldApplyThreadLifecycle) {
960+ if (event.type === "turn.started" && acceptedTurnStartedSourcePlan !== null) {
961+ yield* markSourceProposedPlanImplemented(
962+ acceptedTurnStartedSourcePlan.sourceThreadId,
963+ acceptedTurnStartedSourcePlan.sourcePlanId,
964+ thread.id,
965+ now,
966+ ).pipe(
967+ Effect.catchCause((cause) =>
968+ Effect.logWarning("provider runtime ingestion failed to mark source proposed plan", {
969+ eventId: event.eventId,
970+ eventType: event.type,
971+ cause: Cause.pretty(cause),
972+ }),
973+ ),
974+ );
975+ }
983976
984− yield* orchestrationEngine.dispatch({
985− type: "thread.session.set",
986− commandId: providerCommandId(event, "thread-session-set"),
977+ yield* orchestrationEngine.dispatch({
978+ type: "thread.session.set",
979+ commandId: providerCommandId(event, "thread-session-set"),
980+ threadId: thread.id,
981+ session: {
987982 threadId: thread.id,
988− session: {
989− threadId: thread.id,
990− status,
991− providerName: event.provider,
992− runtimeMode: thread.session?.runtimeMode ?? "full-access",
993− activeTurnId: nextActiveTurnId,
994− lastError,
995− updatedAt: now,
996− },
997− createdAt: now,
998− });
999− }
983+ status,
984+ providerName: event.provider,
985+ runtimeMode: thread.session?.runtimeMode ?? "full-access",
986+ activeTurnId: nextActiveTurnId,
987+ lastError,
988+ updatedAt: now,
989+ },
990+ createdAt: now,
991+ });
1000992 }
993+ }
1001994
1002− const assistantDelta =
1003− event.type === "content.delta" && event.payload.streamKind === "assistant_text"
1004− ? event.payload.delta
1005− : undefined;
1006− const proposedPlanDelta =
1007− event.type === "turn.proposed.delta" ? event.payload.delta : undefined;
1008−
1009− if (assistantDelta && assistantDelta.length > 0) {
1010− const assistantMessageId = MessageId.makeUnsafe(
1011− `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1012− );
1013− const turnId = toTurnId(event.turnId);
1014− if (turnId) {
1015− yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);
1016− }
995+ const assistantDelta =
996+ event.type === "content.delta" && event.payload.streamKind === "assistant_text"
997+ ? event.payload.delta
998+ : undefined;
999+ const proposedPlanDelta =
1000+ event.type === "turn.proposed.delta" ? event.payload.delta : undefined;
1001+
1002+ if (assistantDelta && assistantDelta.length > 0) {
1003+ const assistantMessageId = MessageId.makeUnsafe(
1004+ `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1005+ );
1006+ const turnId = toTurnId(event.turnId);
1007+ if (turnId) {
1008+ yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);
1009+ }
10171010
1018− const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
1019− serverSettingsService.getSettings,
1020− (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),
1021− );
1022− if (assistantDeliveryMode === "buffered") {
1023− const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);
1024− 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 {
1011+ const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
1012+ serverSettingsService.getSettings,
1013+ (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),
1014+ );
1015+ if (assistantDeliveryMode === "buffered") {
1016+ const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);
1017+ if (spillChunk.length > 0) {
10361018 yield* orchestrationEngine.dispatch({
10371019 type: "thread.message.assistant.delta",
1038− commandId: providerCommandId(event, "assistant-delta"),
1020+ commandId: providerCommandId(event, "assistant-delta-buffer-spill"),
10391021 threadId: thread.id,
10401022 messageId: assistantMessageId,
1041− delta: assistantDelta,
1023+ delta: spillChunk,
10421024 ...(turnId ? { turnId } : {}),
10431025 createdAt: now,
10441026 });
10451027 }
1046− }
1047−
1048− if (proposedPlanDelta && proposedPlanDelta.length > 0) {
1049− const planId = proposedPlanIdFromEvent(event, thread.id);
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,
1028+ } else {
1029+ yield* orchestrationEngine.dispatch({
1030+ type: "thread.message.assistant.delta",
1031+ commandId: providerCommandId(event, "assistant-delta"),
10851032 threadId: thread.id,
10861033 messageId: assistantMessageId,
1034+ delta: assistantDelta,
10871035 ...(turnId ? { turnId } : {}),
10881036 createdAt: now,
1089− commandTag: "assistant-complete",
1090− finalDeltaCommandTag: "assistant-delta-finalize",
1091− ...(assistantCompletion.fallbackText !== undefined && shouldApplyFallbackCompletionText
1092− ? { fallbackText: assistantCompletion.fallbackText }
1093− : {}),
10941037 });
1038+ }
1039+ }
10951040
1096− if (turnId) {
1097− yield* forgetAssistantMessageId(thread.id, turnId, assistantMessageId);
1098− }
1041+ if (proposedPlanDelta && proposedPlanDelta.length > 0) {
1042+ const planId = proposedPlanIdFromEvent(event, thread.id);
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);
10991091 }
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) {
11021126 yield* finalizeBufferedProposedPlan({
11031127 event,
11041128 threadId: thread.id,
11051129 threadProposedPlans: thread.proposedPlans,
1106− planId: proposedPlanCompletion.planId,
1107− ...(proposedPlanCompletion.turnId ? { turnId: proposedPlanCompletion.turnId } : {}),
1108− fallbackMarkdown: proposedPlanCompletion.planMarkdown,
1130+ planId: proposedPlanIdForTurn(thread.id, turnId),
1131+ turnId,
11091132 updatedAt: now,
11101133 });
11111134 }
1135+ }
11121136
1113− if (event.type === "turn.completed") {
1114− const turnId = toTurnId(event.turnId);
1115− if (turnId) {
1116− const assistantMessageIds = yield* getAssistantMessageIdsForTurn(thread.id, turnId);
1117− yield* Effect.forEach(
1118− assistantMessageIds,
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);
1137+ if (event.type === "session.exited") {
1138+ yield* clearTurnStateForSession(thread.id);
1139+ }
1140+
1141+ if (event.type === "runtime.error") {
1142+ const runtimeErrorMessage = event.payload.message;
11321143
1133− yield* finalizeBufferedProposedPlan({
1134− event,
1144+ const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD
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: {
11351154 threadId: thread.id,
1136− threadProposedPlans: thread.proposedPlans,
1137− planId: proposedPlanIdForTurn(thread.id, turnId),
1138− turnId,
1155+ status: "error",
1156+ providerName: event.provider,
1157+ runtimeMode: thread.session?.runtimeMode ?? "full-access",
1158+ activeTurnId: eventTurnId ?? null,
1159+ lastError: runtimeErrorMessage,
11391160 updatedAt: now,
1140− });
1141− }
1142− }
1143−
1144− if (event.type === "session.exited") {
1145− yield* clearTurnStateForSession(thread.id);
1161+ },
1162+ createdAt: now,
1163+ });
11461164 }
1165+ }
11471166
1148− if (event.type === "runtime.error") {
1149− const runtimeErrorMessage = event.payload.message;
1150−
1151− const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD
1152− ? true
1153− : activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId);
1167+ if (event.type === "thread.metadata.updated" && event.payload.name) {
1168+ yield* orchestrationEngine.dispatch({
1169+ type: "thread.meta.update",
1170+ commandId: providerCommandId(event, "thread-meta-update"),
1171+ threadId: thread.id,
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+ );
11561193 yield* orchestrationEngine.dispatch({
1157− type: "thread.session.set",
1158− commandId: providerCommandId(event, "runtime-error-session-set"),
1194+ type: "thread.turn.diff.complete",
1195+ commandId: providerCommandId(event, "thread-turn-diff-complete"),
11591196 threadId: thread.id,
1160− session: {
1161− threadId: thread.id,
1162− status: "error",
1163− providerName: event.provider,
1164− runtimeMode: thread.session?.runtimeMode ?? "full-access",
1165− activeTurnId: eventTurnId ?? null,
1166− lastError: runtimeErrorMessage,
1167− updatedAt: now,
1168− },
1197+ turnId,
1198+ completedAt: now,
1199+ checkpointRef: CheckpointRef.makeUnsafe(`provider-diff:${event.eventId}`),
1200+ status: "missing",
1201+ files: [],
1202+ assistantMessageId,
1203+ checkpointTurnCount: maxTurnCount + 1,
11691204 createdAt: now,
11701205 });
11711206 }
11721207 }
1208+ }
11731209
1174− if (event.type === "thread.metadata.updated" && event.payload.name) {
1175− yield* orchestrationEngine.dispatch({
1176− type: "thread.meta.update",
1177− commandId: providerCommandId(event, "thread-meta-update"),
1178− threadId: thread.id,
1179− title: event.payload.name,
1180− });
1181− }
1182−
1183− if (event.type === "turn.diff.updated") {
1184− const turnId = toTurnId(event.turnId);
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− });
1210+ const activities = runtimeEventToActivities(event);
1211+ yield* Effect.forEach(activities, (activity) =>
1212+ orchestrationEngine.dispatch({
1213+ type: "thread.activity.append",
1214+ commandId: providerCommandId(event, "thread-activity-append"),
1215+ threadId: thread.id,
1216+ activity,
1217+ createdAt: activity.createdAt,
1218+ }),
1219+ ).pipe(Effect.asVoid);
1220+ });
12281221
12291222 const processDomainEvent = (_event: TurnStartRequestedDomainEvent) => Effect.void;
12301223
@@ -1248,7 +1241,7 @@ const make = Effect.gen(function* () { · 17 unmodified lines
12481241
12491242 const worker = yield* makeDrainableWorker(processInputSafely);
12501243
1251− const start: ProviderRuntimeIngestionShape["start"] = Effect.gen(function* () {
1244+ const start: ProviderRuntimeIngestionShape["start"] = Effect.fn("start")(function* () {
12521245 yield* Effect.forkScoped(
12531246 Stream.runForEach(providerService.streamEvents, (event) =>
12541247 worker.enqueue({ source: "runtime", event }),
@@ -1272,5 +1265,5 @@ const make = Effect.gen(function* () { · 17 unmodified lines
12721265
12731266 export const ProviderRuntimeIngestionLive = Layer.effect(
12741267 ProviderRuntimeIngestionService,
1275− make,
1268+ make(),
12761269 ).pipe(Layer.provide(ProjectionTurnRepositoryLive));
apps/server/src/orchestration/Services/CheckpointReactor.ts+1−1
@@ -22,7 +22,7 @@ export interface CheckpointReactorShape { · 21 unmodified lines
2222 * Consumes both orchestration-domain and provider-runtime events via an
2323 * internal queue.
2424 */
25− readonly start: Effect.Effect<void, never, Scope.Scope>;
25+ readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2626
2727 /**
2828 * 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 { · 18 unmodified lines
1919 * The returned effect must be run in a scope so all worker fibers can be
2020 * finalized on shutdown.
2121 */
22− readonly start: Effect.Effect<void, never, Scope.Scope>;
22+ readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2323 }
2424
2525 /**
apps/server/src/orchestration/Services/ProviderCommandReactor.ts+1−1
@@ -22,7 +22,7 @@ export interface ProviderCommandReactorShape { · 21 unmodified lines
2222 * Filters orchestration domain events to provider-intent types before
2323 * processing.
2424 */
25− readonly start: Effect.Effect<void, never, Scope.Scope>;
25+ readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2626
2727 /**
2828 * 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 { · 21 unmodified lines
2222 * Uses an internal queue and continues after non-interrupt failures by
2323 * logging warnings.
2424 */
25− readonly start: Effect.Effect<void, never, Scope.Scope>;
25+ readonly start: () => Effect.Effect<void, never, Scope.Scope>;
2626
2727 /**
2828 * 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< · 645 unmodified lines
646646 }),
647647 ).pipe(Effect.forkIn(subscriptionsScope));
648648
649− yield* Scope.provide(orchestrationReactor.start, subscriptionsScope);
649+ yield* Scope.provide(orchestrationReactor.start(), subscriptionsScope);
650650 yield* readiness.markOrchestrationSubscriptionsReady;
651651
652652 let welcomeBootstrapProjectId: ProjectId | undefined;
docs/effect-fn-checklist.md+7−7
@@ -46,7 +46,7 @@ Effect.fn("name")( · 45 unmodified lines
4646 - [x] `apps/server/src/provider/Layers/CodexAdapter.ts`
4747 - [x] `apps/server/src/git/Layers/GitCore.ts`
4848 - [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`
5050 - [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`
5151 - [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts`
5252 - [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts`
@@ -115,12 +115,12 @@ Effect.fn("name")( · 62 unmodified lines
115115
116116 ### `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)
119−- [ ] [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)
121−- [ ] [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)
123−- [ ] Nested callback wrappers in this file
118+- [x] [finalizeAssistantMessage](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680)
119+- [x] [upsertProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722)
120+- [x] [finalizeBufferedProposedPlan](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761)
121+- [x] [clearTurnStateForSession](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800)
122+- [x] [processRuntimeEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908)
123+- [x] Nested callback wrappers in this file
124124
125125 ### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`)
126126