fix(workspace): attach the engine for a message sent while the assistant is busy

RA
21h ago
+477-29

Issue for this PR

Closes #1460

Type of change

  • Bug fix

What does this PR do?

A message sent while the assistant is busy joins the running prompt loop instead of starting one. The engine's turn boundary (reconcile the engine, then catalog and pin the tools) ran only on a loop's first catalog, so that message kept the run's tools and an engine installed meanwhile never attached for it.

The boundary now runs on the first catalog of each user request in the loop, not of each user message. The loop writes user messages itself: the summary after a subtask, the validator retry, the compaction marker and the continue message. Re-running the boundary on those would re-pin engine tools mid-task, which the pin exists to prevent.

Precedent. What counts as a user request isn't new here. Lesson retrieval in the same loop already decides it (the lessons block before the tool catalog in prompt.ts): a user message with a text part that is neither synthetic nor ignored, or a file part; a compaction replay stamped with metadata.learnOriginalMessage stays the request it replays. This PR moves that rule unchanged into UserRequest.requestID and both callers use it, so the two can't drift apart.

TurnBoundary.tracker() replaces the catalogued flag. It runs the boundary on the loop's first catalog and on the first catalog after a request it hasn't seen. It checks every request in history, not just the newest, so neither a loop-written message nor a replay arriving after a queued one hides it.

Claims

  • C1 A message sent while the assistant is busy gets its own turn boundary in the same loop, so an engine installed meanwhile attaches on it.
  • C2 The messages the loop writes itself never run the boundary again.
  • C3 Lesson retrieval selects as before: same rule, now shared.
  • C4 A loop with one request is unchanged: one boundary, on its first catalog.

Residuals

  • R1 Compaction stamps a replay only while lessons are delivered for the session (projects with .altimate-code/learn). Elsewhere, a replay after an overflow counts as a new request and runs the boundary once more, where the request restarts from the summary. With the engine unchanged, that reconcile changes nothing.
  • R2 The turn that raises the install offer still runs without integration tools. That's a separate design question.
  • R3 A client that supplies its own message ids out of send order: a message given a lower id than the one running sorts before it, and the running loop ends without answering it (on main too).

Overlap: #1431 adds if (firstCatalog) workspaceNotice.settle(sessionID) on a line this PR renames. Whichever merges second resolves it as if (boundary) …, so the linked-project notice also re-settles for a queued request.

How did you verify your code works?

  • The rule, over each writer's message shape: user text, a file, subtask summary, validator retry, compaction marker, continue message, stamped and unstamped replays.
  • The tracker, over loop histories, including a queued request followed by a loop-written message.
  • The real prompt loop (test LLM server, boundary spied): a message sent while the first answer is on hold reaches the model in the same run and gets a second boundary. On main this test fails (1 boundary, expected 2). An overflow that compacts and continues gets one.
  • Live before/after (E2E), install mid-run then a queued message: main gets no engine tools; here the engine starts with its 20 tools.
  • Typecheck clean; learn, workspace and session suites pass (4183, 45 todo). Marker Guard clean.

Codex rounds against these claims: review log.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

šŸ¤– Generated with Claude Code

https://claude.ai/code/session_01HRUhVhg6XwxSKJs1iTufiA


Summary by cubic

Fixes the workspace engine's turn boundary so a message sent while the assistant is busy attaches the engine on that message, instead of keeping the tools from the previous request.

  • The boundary now runs on the first catalog of each user request in the loop, not just the loop's first catalog.
  • The rule for identifying user requests is now shared with lesson retrieval via UserRequest.requestID.
  • Any request not seen before in the history triggers a boundary, so a stamped compaction replay or out-of-order history can't hide a message queued during compaction.
  • The loop's own messages (subtask summary, validator retry, compaction marker, continue message) still don't trigger a boundary.

Summary by CodeRabbit

  • Bug Fixes
    • Workspace updates and notices now run at each new user-request boundary, including requests submitted while the assistant is busy.
    • Compaction and continuation remain part of the same request turn, avoiding duplicate boundary handling.