Skip to content

Commit 4c8eb1d

Browse files
t3dotggclaude
andauthored
fix(server): background command results reach a running Codex goal (#33268)
Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent 2b65ffb commit 4c8eb1d

2 files changed

Lines changed: 98 additions & 62 deletions

File tree

‎apps/server/src/orchestration-v2/Orchestrator.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6063,18 +6063,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
60636063
taskIds: delivery.taskIds,
60646064
};
60656065
}
6066-
// Route durable mailbox deliveries and delegated task questions under the
6067-
// thread lock, using the live session's capabilities. A question queued
6068-
// behind a long parent turn is never seen. Never interrupt/restart a turn
6069-
// for a notification.
6066+
// Steer mailbox deliveries, delegated task questions, and command notices
6067+
// into an active goal under the thread lock: queued behind a long turn or a
6068+
// goal run, they are never seen. Never interrupt/restart a turn for one.
6069+
const noticeKind = command.notification?.source.kind;
6070+
const commandNotice = noticeKind === "command" && command.creationSource === "server";
60706071
if (
60716072
!freshContext &&
60726073
(delegatedCompletion !== undefined
60736074
? delegatedCompletion.taskIds.every(
60746075
(id) =>
60756076
projection.subagents.find((task) => task.id === id)?.completionWake === "always",
60766077
)
6077-
: command.notification?.source.kind === "delegated_task")
6078+
: noticeKind === "delegated_task" || commandNotice)
60786079
) {
60796080
const active = projection.runs.find((run) => run.status === "running");
60806081
const providerThread = projection.providerThreads.find(
@@ -6090,6 +6091,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
60906091
active !== undefined &&
60916092
activeTurn !== undefined &&
60926093
providerThread?.providerSessionId != null &&
6094+
(!commandNotice || providerThread.goal?.status === "active") &&
60936095
(activeMessage === undefined || !isNativeMaintenanceCommand(activeMessage))
60946096
) {
60956097
const session = yield* providerSessions

‎apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts‎

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -651,63 +651,97 @@ const nextTurnSelectionHarness = Effect.fn("nextTurnSelectionHarness")(function*
651651
return { cwd, events, started, steered, interrupted, layer, startFirstTurn };
652652
});
653653

654-
it.effect("steers a delegated task question notice into the running parent turn", () =>
655-
Effect.scoped(
656-
Effect.gen(function* () {
657-
const { started, steered, layer, startFirstTurn } = yield* nextTurnSelectionHarness(
658-
"delegated-question-steer",
659-
);
660-
yield* Effect.gen(function* () {
661-
const orchestrator = yield* Orchestrator.OrchestratorV2;
662-
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
663-
const threadId = yield* startFirstTurn;
664-
const { thread } = yield* orchestrator.getThreadProjection(threadId);
665-
yield* (yield* EventSink.EventSinkV2).write({
666-
events: [
667-
{
668-
id: EventId.make("event:composer-selection"),
669-
type: "thread.model-selection-updated",
670-
threadId,
671-
providerInstanceId: instanceId,
672-
occurredAt: yield* DateTime.now,
673-
payload: { ...thread, modelSelection: composerSelection },
674-
},
675-
],
676-
});
677-
const messageId = MessageId.make("message:delegated-task-question:child-question");
678-
const notification = {
679-
source: {
680-
kind: "delegated_task" as const,
681-
taskIds: [NodeId.make("task:question")],
682-
childThreadId: ThreadId.make("thread:question-child"),
683-
},
684-
outcome: "updated" as const,
685-
summary: "Delegated task has a question",
686-
};
687-
yield* orchestrator.dispatch({
688-
type: "message.dispatch",
689-
commandId: CommandId.make("question-wake"),
690-
threadId,
691-
messageId,
692-
text: "Delegated task asked a question",
693-
notification,
694-
attachments: [],
695-
dispatchMode: { type: "queue_after_active" },
696-
createdBy: "agent",
697-
creationSource: "server",
698-
});
699-
yield* worker.drain();
700-
const after = yield* orchestrator.getThreadProjection(threadId);
701-
assert.deepEqual(steered, ["Delegated task asked a question"]);
702-
assert.lengthOf(started, 1);
703-
assert.isFalse(after.runs.some((run) => run.status === "queued"));
704-
const message = after.messages.find((candidate) => candidate.id === messageId);
705-
assert.equal(message?.runId, started[0]?.runId);
706-
assert.deepEqual(message?.notification, notification);
707-
assert.deepEqual(after.thread.modelSelection, composerSelection);
708-
}).pipe(Effect.provide(layer));
709-
}),
710-
),
654+
it.effect.each([
655+
{ notice: "delegated task question", goal: null, steers: true },
656+
{ notice: "background command", goal: "active", steers: true },
657+
{ notice: "background command", goal: null, steers: false },
658+
] as const)(
659+
"a $notice notice steers into the running turn: $steers (goal $goal)",
660+
({ notice, goal, steers }) =>
661+
Effect.scoped(
662+
Effect.gen(function* () {
663+
const { started, steered, layer, startFirstTurn } = yield* nextTurnSelectionHarness(
664+
`notice-steer-${notice.replaceAll(" ", "-")}-${goal ?? "none"}`,
665+
);
666+
yield* Effect.gen(function* () {
667+
const orchestrator = yield* Orchestrator.OrchestratorV2;
668+
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
669+
const threadId = yield* startFirstTurn;
670+
const { thread } = yield* orchestrator.getThreadProjection(threadId);
671+
yield* (yield* EventSink.EventSinkV2).write({
672+
events: [
673+
{
674+
id: EventId.make("event:composer-selection"),
675+
type: "thread.model-selection-updated",
676+
threadId,
677+
providerInstanceId: instanceId,
678+
occurredAt: yield* DateTime.now,
679+
payload: { ...thread, modelSelection: composerSelection },
680+
},
681+
],
682+
});
683+
if (goal !== null) {
684+
const providerThread = (yield* orchestrator.getThreadProjection(threadId))
685+
.providerThreads[0]!;
686+
yield* (yield* EventSink.EventSinkV2).write({
687+
events: [
688+
{
689+
id: EventId.make("event:goal-active"),
690+
type: "provider-thread.updated",
691+
threadId,
692+
occurredAt: yield* DateTime.now,
693+
payload: { ...providerThread, goal: { objective: "Ship it", status: goal } },
694+
},
695+
],
696+
});
697+
}
698+
const messageId = MessageId.make("message:notice");
699+
const notification =
700+
notice === "delegated task question"
701+
? {
702+
source: {
703+
kind: "delegated_task" as const,
704+
taskIds: [NodeId.make("task:question")],
705+
childThreadId: ThreadId.make("thread:question-child"),
706+
},
707+
outcome: "updated" as const,
708+
summary: "Delegated task has a question",
709+
}
710+
: {
711+
source: { kind: "command" as const },
712+
outcome: "completed" as const,
713+
summary: "Background command finished",
714+
};
715+
yield* orchestrator.dispatch({
716+
type: "message.dispatch",
717+
commandId: CommandId.make("notice-wake"),
718+
threadId,
719+
messageId,
720+
text: `${notification.summary}\nexit 0: built 12 files`,
721+
notification,
722+
attachments: [],
723+
dispatchMode: { type: "queue_after_active" },
724+
createdBy: "agent",
725+
creationSource: "server",
726+
});
727+
yield* worker.drain();
728+
const after = yield* orchestrator.getThreadProjection(threadId);
729+
assert.deepEqual(
730+
steered,
731+
steers ? [`${notification.summary}\nexit 0: built 12 files`] : [],
732+
);
733+
assert.lengthOf(started, 1);
734+
assert.equal(
735+
after.runs.some((run) => run.status === "queued"),
736+
!steers,
737+
);
738+
const message = after.messages.find((candidate) => candidate.id === messageId);
739+
assert.deepEqual(message?.notification, notification);
740+
if (steers) assert.equal(message?.runId, started[0]?.runId);
741+
assert.deepEqual(after.thread.modelSelection, composerSelection);
742+
}).pipe(Effect.provide(layer));
743+
}),
744+
),
711745
);
712746

713747
it.effect("sends a selected held message once and preserves the remaining queue order", () =>

0 commit comments

Comments
 (0)