Skip to content

Wire live task updates and simplify createChatDirector's options - #408

Merged
TheGreatAxios merged 5 commits into
mainfrom
cl-5708-wire-live-task-updates
Aug 8, 2026
Merged

Wire live task updates and simplify createChatDirector's options#408
TheGreatAxios merged 5 commits into
mainfrom
cl-5708-wire-live-task-updates

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Collapses createChatDirector's 8 optional positional parameters into a single ChatDirectorOptions object, shared between the factory function and the ChatDirectorImpl class constructor. All call sites (TUI runner, exec runner, integration harness, all test files) updated; no back-compat overload since this is fully internal. onTasksChange is required, not optional — both production call sites already pass it, and an omitted required field is a visible gap in a caller's diff.
  • Wires onTasksChange into the TUI's live chrome panel (mirrors the existing goalGovernor emit/pull pattern) so a running session's task list updates live instead of only appearing on resume. Exec mode wires the same callback to debug logging, documented inline as a placeholder since exec has no equivalent live task-rendering surface today.
  • Removes the duplicate manage_tasks re-parsing logic in turns-to-blocks.ts. Both the live decide() loop and hydrateTasksFromTurns() (used for session-resume hydration) now share one applyManageTasksToolCall helper. manage_tasks's tool handler has no side effect of its own — the task list is mutated by the director at the tool_call event, before the tool executes — so applying (and stripping resumed rows) on the tool_call itself, not gated on the tool_result, is the correct invariant, not just parity with old behavior.
  • Keeps the two resume surfaces consistent: manage_tasks rows are stripped from the resumed transcript regardless of whether their tool_result errored or is missing entirely, and the derived list is restored into the director so the chrome panel doesn't read an empty list until the model next calls manage_tasks.
  • manage_tasks no longer prompts the operator for approval, in or out of auto mode. Reviewed and confirmed safe by Greybeard beforehand: the tool's handler has no side effect to deny — denying it cannot undo state that already changed in the director's decide() loop.

Test plan

  • bun run typecheck
  • bun run build
  • bun run test (4184/4185 pass; the one failure, src/agent/lsp-availability.test.ts, is a pre-existing environment-dependent check unrelated to this change — it asserts a language server binary is installed in the sandbox)
  • Live-wiring test: a manage_tasks tool call through a real ChatDirectorImpl invokes onTasksChange with the updated list (src/director.test.ts)
  • restoreTasks seeds a resumed session's task list and notifies the consumer (src/director.test.ts)
  • turnsToContentBlocks no longer derives a tasks block on its own, and strips manage_tasks rows regardless of tool_result outcome — success, error, or missing entirely (src/tui/turns-to-blocks.test.ts)
  • End-to-end test composing turnsToContentBlocks + hydrateTasksFromTurns the way runner.ts does: an errored or missing manage_tasks result still shows the task exactly once (src/tui/turns-to-blocks.test.ts)
  • manage_tasks auto-allows outside auto mode too (src/permission/permission.test.ts)

Ten positional parameters made call sites error-prone — several same-typed
optional callbacks sat next to each other with nothing but position to
distinguish them. A single ChatDirectorOptions object, shared between the
factory function and the class constructor, makes each call site self-
describing and immune to accidental argument swaps.
…hydrate parser

The TUI's chrome panel now pulls task state through onTasksChange, the same
callback path onActivateTools already used, so a running session's task list
updates live instead of only appearing after a resume. Exec mode wires the
same callback to debug logging, since it has no equivalent live task surface
today.

Resumed sessions previously re-derived their task list by independently
replaying manage_tasks tool calls in turns-to-blocks.ts. That replay is gone;
hydrate and the live decide() loop now share one function for applying a
manage_tasks call to a task list, so there is a single definition of what a
transcript's task state means. The live loop already applied a call as soon
as it saw the tool_call, without waiting for its tool_result — hydrate now
matches that instead of gating on a successful result.
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CL-5708

Two surfaces disagreed with each other after a resume. The transcript
dropped its manage_tasks rows in favor of one aggregated task block, but
only when the rows were stripped alongside submit_plan's — without that,
every call reappeared as its own row beneath the summary. And the chrome
panel reads the director's in-memory list, which a freshly constructed
director leaves empty until the model happens to call manage_tasks again,
so a resumed session showed tasks in its scrollback and none in its panel.

Restoring the derived list into the director closes both: one source of
task state, painted the same way everywhere.
Resumed transcripts stripped a manage_tasks call's raw rows only when it
had a successful tool_result, but hydrateTasksFromTurns already applies the
call unconditionally on the tool_call itself — a call with an errored or
missing result kept its raw rows next to the aggregated task block instead
of being replaced by it. The strip now matches the apply: manage_tasks rows
come out regardless of the result's outcome, because the tool_call is what
the underlying tool's side-effect-free handler makes authoritative, not
whatever result eventually shows up.

onTasksChange moves from optional to required on ChatDirectorOptions, same
motivation as CL-5709: an omitted required field is a visible gap in a
caller's diff, not an invisible one.
manage_tasks's handler has no side effect of its own — it parses its
arguments and returns a fixed string. The task list it appears to control
is actually mutated earlier, by the director's decide() loop at the
tool_call event, before this tool ever executes. By the time an approval
prompt for it would reach the operator, there is nothing left for a denial
to undo, the same reasoning that already exempts read-only tools like lsp
from approval.
@TheGreatAxios
TheGreatAxios merged commit 336cfd5 into main Aug 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant