Skip to content

fix(decopilot): drop the ?-in-text heuristic from thread status resolution - #5585

Merged
guitavano merged 3 commits into
mainfrom
fix/thread-status-drop-question-mark-heuristic
Aug 3, 2026
Merged

fix(decopilot): drop the ?-in-text heuristic from thread status resolution#5585
guitavano merged 3 commits into
mainfrom
fix/thread-status-drop-question-mark-heuristic

Conversation

@guitavano

@guitavano guitavano commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes the fragile heuristic in resolveThreadStatus (apps/api/src/api/routes/decopilot/status.ts) that flipped a cleanly-finished (stop) turn to requires_action whenever its final text contained a ?.

The heuristic only stripped https://-scheme URLs before the .includes("?") check, so it false-positived on:

  • schemeless URLs with a query string — e.g. a review agent writing `fonts.googleapis.com/css2?...` in its summary, and
  • any rhetorical question in normal prose.

Real-world impact (the bug that surfaced this)

A QA / Code Reviewer thread that had already recorded its decision (TASK_BOARD_REVIEW_DECISION → task correctly moved to in_progress) was nonetheless marked requires_action ("Requer entrada") because its summary mentioned fonts.googleapis.com/css2?....

That non-terminal thread status then wedged the task board:

  • shouldAdvanceToReview (apps/api/src/storage/task-board.ts) requires every linked thread to be terminal → the task never moved back to In Review after the Super Agent's fix.
  • decideStallAction (stall-recovery.ts) deliberately leaves requires_action alone, assuming a human owns a user_ask that never existed.

Net result: the card sat in In Progress indefinitely, waiting on input nobody ever needed to give.

Fix

A clean stop is a finished turn. An agent that needs input signals it structurally — a pending user_ask or an approval-requested tool part, both already handled by the tool-calls branch. So stop now always maps to completed; the ? text inference is gone (and the now-unused text field dropped from the local ResponsePart type).

Testing

  • bun test apps/api/.../status.test.ts — 14/14 pass.
  • Inverted the two unit tests that encoded the old ? behavior (stop + trailing question, stop + question-with-URL) to assert completed.
  • Added a regression test for the real schemeless-URL case (fonts.googleapis.com/css2?...completed).
  • Updated the e2e skip note in packages/e2e/tests/decopilot-projection.spec.ts that referenced the removed stop+? path.
  • tsc --noEmit clean in both apps/api and packages/e2e.

🤖 Generated with Claude Code


Summary by cubic

Dropped the ?-in-text heuristic in server resolveThreadStatus and web deriveTerminalThreadStatus so a stop finish always maps to completed. Fixes false requires_action from schemeless URLs and rhetorical questions, and keeps client/server status in sync.

  • Bug Fixes

    • Server: rely only on structured signals (user_ask, approval-requested under tool-calls); remove text from part type; invert tests; add a schemeless-URL regression; update the e2e skip note and a stale project-chunks.ts comment.
    • Web: mirror stop -> completed; drop text from the local type; invert the unit test; add a cross-reference comment to keep server/web logic aligned.
  • Refactors

    • Trimmed overlong rationale comments across status and thread-status files per comment-cop.

Written for commit 185a930. Summary will update on new commits.

Review in cubic

…olution

A `stop`-finished turn was classified as `requires_action` whenever its
final text contained a `?` (after stripping only `https://`-scheme URLs).
That false-positived on any summary mentioning a URL query string — e.g. a
review agent writing `fonts.googleapis.com/css2?...` (schemeless, so not
stripped) — or asking a rhetorical question.

The consequence on the task board: a QA/Code Reviewer thread that had
cleanly recorded its decision was left in `requires_action`. Since
`shouldAdvanceToReview` requires every linked thread to be terminal, the
task never moved back to In Review after the Super Agent's fix, and
stall-recovery deliberately leaves `requires_action` alone (assuming a
human owns a `user_ask`) — so the card wedged in In Progress forever.

A clean stop is a finished turn. An agent that needs input signals it
structurally (a pending `user_ask` or `approval-requested` tool part, both
under the `tool-calls` branch), so `stop` now always maps to `completed`.

- Invert the two tests that encoded the old `?` behavior; add a regression
  test for the real schemeless-URL case.
- Update the e2e skip note that referenced the removed `stop`+`?` path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mirror too

Review of #5585 found the same heuristic living as a byte-identical copy in
`deriveTerminalThreadStatus` (apps/web/.../chat/store/thread-status.ts), used
to optimistically patch a thread's status in the client store on every chat
turn (chat-context onFinish). Leaving it re-introduced the exact false positive
the server fix removes — any assistant reply containing a `?` (including a
schemeless URL like `fonts.googleapis.com/css2?...`) optimistically flipped the
thread to `requires_action`, diverging from the server's `completed` and
mis-grouping the chat as "needs input".

- Web `stop` branch now returns `completed`; drop the now-unused `text?` field;
  add a cross-reference comment (apps/web can't import apps/api/src, so the two
  mirrors are kept in lockstep by hand).
- Invert the web unit test that asserted the old `stop`+`?` -> requires_action.
- Fix a stale comment in project-chunks.ts that still described the removed
  "question ending" path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +15 to +17
// A clean stop is a finished turn. Ending prose with a question no longer
// implies the agent is waiting on input — that must be signalled
// structurally (user_ask / approval-requested, both under `tool-calls`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +26 to +29
// Real case: a review agent's summary mentioned `fonts.googleapis.com/css2?...`
// (no scheme). The old `?`-substring heuristic only stripped `https://`-scheme
// URLs, so this query-string `?` survived and the completed review thread was
// wrongly flipped to requires_action — wedging the task board.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +27 to +34
// A clean stop is a finished turn. An agent that needs input signals it
// structurally — a pending `user_ask` or an `approval-requested` tool part,
// both handled by the `tool-calls` branch below — never by ending normal
// prose with a question. We used to infer `requires_action` from a `?` in
// the final text; that false-positived on any summary mentioning a URL
// query string (e.g. `fonts.googleapis.com/css2?...`) or a rhetorical
// question, wedging completed review threads in `requires_action` and
// blocking the task board from advancing them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +7 to +9
// A clean stop is completed even when its text ends with a question — the
// `?` heuristic is gone; "needs input" comes only from the structural
// `tool-calls` signals below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +8 to +11
// Client-side mirror of the server's `resolveThreadStatus`
// (apps/api/src/api/routes/decopilot/status.ts). `apps/web` can't import from
// `apps/api/src` (ban-web-server-imports), so the two are kept in lockstep by
// hand — change both together.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +17 to +21
// A clean stop is a finished turn. An agent that needs input signals it
// structurally — a pending `user_ask` or `approval-requested` part, both
// handled by the `tool-calls` branch below — never by ending prose with a
// question. (We used to infer `requires_action` from a `?` in the text,
// which false-positived on any URL query string or rhetorical question.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

The comment-cop bot flagged six paragraph-length justification comments across
status.ts / status.test.ts / thread-status.ts / thread-status.test.ts
(CLAUDE.md: a paragraph justifying code is a smell). The code is already the
clean end-state; condense each to a one/two-line "why", leaning on the
descriptive test names for the rest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +27 to +28
// Finished turn. "Needs input" comes only from the structured `tool-calls`
// signals below (user_ask / approval), never from a `?` in the prose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +14 to +15
// Finished turn. "Needs input" comes only from the structured `tool-calls`
// signals below (user_ask / approval), never from a `?` in the prose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

@guitavano
guitavano merged commit 747c9d9 into main Aug 3, 2026
25 checks passed
@guitavano
guitavano deleted the fix/thread-status-drop-question-mark-heuristic branch August 3, 2026 14:23
decocms Bot pushed a commit that referenced this pull request Aug 3, 2026
PR: #5585 fix(decopilot): drop the `?`-in-text heuristic from thread status resolution
Bump type: patch

- decocms (apps/api/package.json): 4.163.0 -> 4.163.1
- @decocms/native (apps/native/package.json): 4.163.0 -> 4.163.1
- @decocms/e2e (packages/e2e/package.json): 1.28.0 -> 1.28.1

Deploy-Scope: both
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR authored by a coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants