fix(task-board): set task context on spawned sessions - #429
Conversation
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
LGTM — no issues found.
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 1 issue(s).
server/task-orchestrator.ts
Clean, well-tested fix that correctly sets task context on spawned sessions. The optional dependency is backward-compatible, the implementation mirrors the existing setTaskContext pattern, and both success and fallback paths are covered by tests. One pre-existing timing gap noted (MCP tools not wired up for spawned sessions) but not introduced by this PR.
- 🔵 unsafe_assumptions (L370): Pre-existing timing gap (not introduced by this PR):
setTaskContextForClientruns in the.then()afterspawnSessionresolves, butstartChat(fire-and-forget insidespawnSession) readstaskContextsynchronously inbuildMcpAllowedTools(chat.ts:838) andbuildTaskMcpServer(chat.ts:918) — both before the firstawait. So spawned sessions get the task prompt in the system prompt (chat.ts:962, after await — works correctly), but the task-board MCP server and its tool allowlist are not wired up. The agent is told about its task but can't callTaskComplete/TaskStatustools. A future fix could passtaskContextas astartChatoption so it's available during synchronous setup.[fixable]
|
|
||
| if (clientId) { | ||
| this.deps.store.setSessionId(next.id, clientId); | ||
| this.deps.setTaskContextForClient?.(clientId, next.id, capturedGoalId); |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Pre-existing timing gap (not introduced by this PR): setTaskContextForClient runs in the .then() after spawnSession resolves, but startChat (fire-and-forget inside spawnSession) reads taskContext synchronously in buildMcpAllowedTools (chat.ts:838) and buildTaskMcpServer (chat.ts:918) — both before the first await. So spawned sessions get the task prompt in the system prompt (chat.ts:962, after await — works correctly), but the task-board MCP server and its tool allowlist are not wired up. The agent is told about its task but can't call TaskComplete/TaskStatus tools. A future fix could pass taskContext as a startChat option so it's available during synchronous setup. [fixable]
Centaur ReviewFound 2 issue(s) (1 warning).
|
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 2 issue(s).
server/index.ts
Clean, well-tested fix. The only concern is a redundant double-set of taskContext (once via startChat options, once via setTaskContextForClient) — harmless but worth consolidating for clarity.
- 🔵 style (L261): Redundant double-set:
taskContextis passed tostartChat(line 261), which setssession.taskContextsynchronously during registration. ThensetTaskContextForClient(called from the orchestrator's.then()callback) sets the same value again. Both paths always set identical values. Consider removing thetaskContextoption from thestartChatcall and relying solely onsetTaskContextForClient, or vice versa, to make the ownership clear.[fixable]
server/__tests__/task-orchestrator.test.ts
Clean, well-tested fix. The only concern is a redundant double-set of taskContext (once via startChat options, once via setTaskContextForClient) — harmless but worth consolidating for clarity.
- 🔵 missing_tests (L883): The fallback test asserts
setTaskContextwas called but doesn't verify the arguments (task.id,goal.id). AddingtoHaveBeenCalledWith(task.id, goal.id)would strengthen the assertion and guard against argument-order regressions.[fixable]
| mode: 'agent', | ||
| isolation: true, | ||
| telosTaskId: goalId, | ||
| taskContext: { currentTaskId: taskId, goalId }, |
There was a problem hiding this comment.
🔵 style: Redundant double-set: taskContext is passed to startChat (line 261), which sets session.taskContext synchronously during registration. Then setTaskContextForClient (called from the orchestrator's .then() callback) sets the same value again. Both paths always set identical values. Consider removing the taskContext option from the startChat call and relying solely on setTaskContextForClient, or vice versa, to make the ownership clear. [fixable]
|
|
||
| orch.start(goal.id); | ||
|
|
||
| await vi.waitFor(() => { |
There was a problem hiding this comment.
🔵 missing_tests: The fallback test asserts setTaskContext was called but doesn't verify the arguments (task.id, goal.id). Adding toHaveBeenCalledWith(task.id, goal.id) would strengthen the assertion and guard against argument-order regressions. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 1 issue(s).
server/task-orchestrator.ts
Clean, well-tested fix that correctly propagates task context to spawned sessions via startChat options. The setTaskContextForClient dep is redundant with the startChat path but harmless.
- 🔵 style (L370): Redundant task context assignment.
spawnSessionin index.ts already passestaskContexttostartChat(line 261), which setssession.taskContextsynchronously before_startChatInner's firstawait(line 1059). By the time this.then()runs,session.taskContextis already set to the identical value. The two mechanisms are defensible as belt-and-suspenders, but a comment explaining the intentional redundancy would help future readers understand which is the primary mechanism (thestartChatoption, since it's set before the system prompt is built at line 967).
|
|
||
| if (clientId) { | ||
| this.deps.store.setSessionId(next.id, clientId); | ||
| this.deps.setTaskContextForClient?.(clientId, next.id, capturedGoalId); |
There was a problem hiding this comment.
🔵 style: Redundant task context assignment. spawnSession in index.ts already passes taskContext to startChat (line 261), which sets session.taskContext synchronously before _startChatInner's first await (line 1059). By the time this .then() runs, session.taskContext is already set to the identical value. The two mechanisms are defensible as belt-and-suspenders, but a comment explaining the intentional redundancy would help future readers understand which is the primary mechanism (the startChat option, since it's set before the system prompt is built at line 967).
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
LGTM — no issues found.
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
LGTM — no issues found.
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 3 issue(s) (1 warning).
server/chat.ts
Clean, well-scoped fix. The taskContext assignment ordering relative to _onSessionChange is worth checking; test coverage is good for the orchestrator but doesn't verify the chat.ts integration end of the change.
- 🟡 regressions (L867): The
taskContextassignment happens after_onSessionChange?.(clientId, 'start')is emitted in the diff's line ordering. If any_onSessionChangelistener readssession.taskContext(e.g. to broadcast session info), it will seenullfor spawned sessions. Consider moving thetaskContextassignment before the_onSessionChangecall to match the pattern ofsession.modelandsession.inputQueuewhich are also set before the notification.[fixable]
server/__tests__/task-orchestrator.test.ts
Clean, well-scoped fix. The taskContext assignment ordering relative to _onSessionChange is worth checking; test coverage is good for the orchestrator but doesn't verify the chat.ts integration end of the change.
- 🔵 missing_tests: No test verifies that
startChatactually receives thetaskContextoption from thespawnSessioncallback. The orchestrator tests mockspawnSessionentirely, and there's no integration test or unit test onchat.tsconfirming thatoptions.taskContextis written tosession.taskContext. A unit test on_startChatInner(or at minimum a test inindex.tsverifying thespawnSessionclosure passestaskContext) would close this gap.[fixable] - 🔵 missing_tests: No test covers the spawn-throws path (
.catchhandler in orchestrator) to verify thatsetTaskContextis NOT called whenspawnSessionrejects. The existing tests cover success and null-return, but not the rejection case.[fixable]
| const session = registry.get(clientId)!; | ||
| session.model = options.model ?? session.model; | ||
| session.inputQueue = inputQueue as { push: (msg: unknown) => void; close: () => void }; | ||
| if (options.taskContext) { |
There was a problem hiding this comment.
🟡 regressions: The taskContext assignment happens after _onSessionChange?.(clientId, 'start') is emitted in the diff's line ordering. If any _onSessionChange listener reads session.taskContext (e.g. to broadcast session info), it will see null for spawned sessions. Consider moving the taskContext assignment before the _onSessionChange call to match the pattern of session.model and session.inputQueue which are also set before the notification. [fixable]
Spawned task sessions never had taskContext set on their registry entry, so TaskComplete calls failed with 400 "No active task context" and tasks stayed stuck on "running" forever. Add setTaskContextForClient dep to OrchestratorDeps and call it in the spawn success path, so spawned agents can complete tasks and trigger the next step. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Centaur finding: buildTaskMcpServer runs synchronously during startChat before setTaskContextForClient could be called, so spawned sessions never got the task-board MCP tools wired up. Pass taskContext as a startChat option so it's set on the session during registration, before MCP server construction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove setTaskContextForClient — startChat options are the single owner of taskContext for spawned sessions. Removes redundant double-set found by Centaur review. Strengthen fallback test assertion with exact args. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7171257 to
f11ab31
Compare
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
LGTM — no issues found.
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 2 issue(s) (1 warning).
server/__tests__/task-orchestrator.test.ts
Correct fix — spawned sessions will now get task context, MCP server, and system prompt. The two new orchestrator tests are near-duplicates of existing tests; the actual new behavior (startChat setting session.taskContext from options) lacks test coverage.
- 🟡 missing_tests: Both new orchestrator tests duplicate existing tests. 'does not call setTaskContext (pinned) for spawned sessions' duplicates 'auto policy (store default) spawns instead of reusing' (line 824) — both assert
setTaskContextis not called on successful spawn. 'falls back to setTaskContext (pinned) when spawn returns null' duplicates 'falls back to pinned session when spawnSession returns null' (line 656) — both mockspawnSessionreturning null and assertsetTaskContextis called. The actual new behavior —startChatpropagatingoptions.taskContextontosession.taskContextso thatbuildTaskMcpServerandbuildTaskPromptForSessionpick it up — has no test coverage anywhere.[fixable]
server/chat.ts
Correct fix — spawned sessions will now get task context, MCP server, and system prompt. The two new orchestrator tests are near-duplicates of existing tests; the actual new behavior (startChat setting session.taskContext from options) lacks test coverage.
- 🔵 style (L865): The placement between
session.inputQueueand_onSessionChangeis fine functionally but groups a task-orchestration concern with low-level session plumbing. Consider placing it near thesession.modelassignment (line 863) or after_onSessionChangewith a brief comment linking it to the task-board MCP server and system prompt that read it downstream (lines 918, 962). Minor — the current placement works correctly.[fixable]
| @@ -862,6 +864,9 @@ async function _startChatInner( | |||
| const session = registry.get(clientId)!; | |||
| session.model = options.model ?? session.model; | |||
There was a problem hiding this comment.
🔵 style: The placement between session.inputQueue and _onSessionChange is fine functionally but groups a task-orchestration concern with low-level session plumbing. Consider placing it near the session.model assignment (line 863) or after _onSessionChange with a brief comment linking it to the task-board MCP server and system prompt that read it downstream (lines 918, 962). Minor — the current placement works correctly. [fixable]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 4 issue(s) (1 warning).
server/chat.ts
The wiring fix is correct and ordering in _startChatInner is sound, but the actual behavior change (startChat propagating taskContext to the session) lacks direct test coverage — the two new orchestrator tests duplicate existing mocked assertions rather than testing the real integration point.
- 🟡 missing_tests (L866): The core fix —
startChatacceptingtaskContextand settingsession.taskContext— has no test. The new orchestrator tests only verify mockedsetTaskContextcalls, but nothing validates thatstartChatactually propagatestaskContextto the session object, or thatbuildTaskMcpServer/buildTaskPromptForSessionsee it. An integration test (or a focused unit test of_startChatInnerwith a fake registry) would cover the real behavior change.[fixable]
server/__tests__/task-orchestrator.test.ts
The wiring fix is correct and ordering in _startChatInner is sound, but the actual behavior change (startChat propagating taskContext to the session) lacks direct test coverage — the two new orchestrator tests duplicate existing mocked assertions rather than testing the real integration point.
- 🔵 missing_tests (L841): The new test 'does not call setTaskContext (pinned) for spawned sessions' duplicates existing coverage: the test at line 824 ('auto policy spawns instead of reusing') already asserts
expect(deps.setTaskContext).not.toHaveBeenCalled()with an identical mock setup (spawnSession returns a clientId). The only difference is explicitsessionPolicy: 'spawn'vs. the default 'auto', but both resolve to the same code path (line 346:const policy = next.sessionPolicy === 'reuse' ? 'reuse' : 'spawn').[fixable] - 🔵 missing_tests (L864): The new test 'falls back to setTaskContext (pinned) when spawn returns null' duplicates the existing test at line 656 ('falls back to pinned session when spawnSession returns null'), which also mocks
spawnSessionreturningnulland assertsexpect(deps.setTaskContext).toHaveBeenCalledWith(task.id, goal.id). The existing test additionally checksactiveTaskId, making the new one strictly a subset.[fixable]
server/index.ts
The wiring fix is correct and ordering in _startChatInner is sound, but the actual behavior change (startChat propagating taskContext to the session) lacks direct test coverage — the two new orchestrator tests duplicate existing mocked assertions rather than testing the real integration point.
- 🔵 style (L258): Both
telosTaskId: goalId(line 257) andtaskContext: { currentTaskId: taskId, goalId }(line 258) now passgoalIdtostartChatvia two separate paths.telosTaskIdis used for event-store metadata whiletaskContextdrives MCP tools and system prompt. The overlap is intentional but worth a brief inline note to prevent a future reader from consolidating them.
| @@ -862,6 +864,9 @@ async function _startChatInner( | |||
| const session = registry.get(clientId)!; | |||
| session.model = options.model ?? session.model; | |||
| session.inputQueue = inputQueue as { push: (msg: unknown) => void; close: () => void }; | |||
There was a problem hiding this comment.
🟡 missing_tests: The core fix — startChat accepting taskContext and setting session.taskContext — has no test. The new orchestrator tests only verify mocked setTaskContext calls, but nothing validates that startChat actually propagates taskContext to the session object, or that buildTaskMcpServer/buildTaskPromptForSession see it. An integration test (or a focused unit test of _startChatInner with a fake registry) would cover the real behavior change. [fixable]
| @@ -839,6 +839,49 @@ describe('TaskOrchestrator', () => { | |||
| expect(deps.setTaskContext).not.toHaveBeenCalled(); | |||
| }); | |||
|
|
|||
There was a problem hiding this comment.
🔵 missing_tests: The new test 'does not call setTaskContext (pinned) for spawned sessions' duplicates existing coverage: the test at line 824 ('auto policy spawns instead of reusing') already asserts expect(deps.setTaskContext).not.toHaveBeenCalled() with an identical mock setup (spawnSession returns a clientId). The only difference is explicit sessionPolicy: 'spawn' vs. the default 'auto', but both resolve to the same code path (line 346: const policy = next.sessionPolicy === 'reuse' ? 'reuse' : 'spawn'). [fixable]
| // Spawned sessions get taskContext via startChat options, not setTaskContext | ||
| expect(deps.setTaskContext).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
There was a problem hiding this comment.
🔵 missing_tests: The new test 'falls back to setTaskContext (pinned) when spawn returns null' duplicates the existing test at line 656 ('falls back to pinned session when spawnSession returns null'), which also mocks spawnSession returning null and asserts expect(deps.setTaskContext).toHaveBeenCalledWith(task.id, goal.id). The existing test additionally checks activeTaskId, making the new one strictly a subset. [fixable]
| mode: 'agent', | ||
| isolation: true, | ||
| telosTaskId: goalId, | ||
| taskContext: { currentTaskId: taskId, goalId }, |
There was a problem hiding this comment.
🔵 style: Both telosTaskId: goalId (line 257) and taskContext: { currentTaskId: taskId, goalId } (line 258) now pass goalId to startChat via two separate paths. telosTaskId is used for event-store metadata while taskContext drives MCP tools and system prompt. The overlap is intentional but worth a brief inline note to prevent a future reader from consolidating them.
Summary
taskContextset on their registry entry, soTaskCompletecalls from the agent hit a 400 error and tasks stayed stuck on "running" foreversetTaskContextForClienttoOrchestratorDeps— sets task context on a specific clientId (not just the pinned client)setSessionId, so spawned agents can complete tasks and trigger the orchestrator to advanceTest plan
sets task context on spawned session via setTaskContextForClientdoes not set task context when spawn returns null (fallback path)🤖 Generated with Claude Code