Skip to content

Robustness fixes from review (area 1a) - #302

Merged
adamjohnwright merged 1 commit into
mainfrom
review-1a-robustness
Oct 3, 2026
Merged

adamjohnwright merged 1 commit into
mainfrom
review-1a-robustness

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

These are the second group of fixes from the max-level code review of src/api and src/handoff.

Finding Fix Test
/api/answer left each one-shot thread's checkpoints in memory for the life of the process. The reviewer measured RSS growing 22 MiB over 600 answers. AgentGraph.forget_thread is called in a finally, as a task so it is safe during cancellation Answered and crashing streams both delete the thread. Sabotaged.
verify() let InvalidKeyError through. A header naming the allowed algorithm that doesn't match the key gave an unauthenticated 500 on three routes. Reproduced. Also catches PyJWTError, ValueError, TypeError and RecursionError. Tokens over 4,096 characters are refused before parsing. Forged-header and oversized-token tests. Sabotaged.
Every distinct handoff id posted to the window channel was redeemed: any page can post, and nothing rate-limits it. One claim per session. Later ids are acknowledged and otherwise ignored. The claimed id is stored as a string, not a set. Browser
A message sent while a handoff's data was loading ran on the same thread, and the seed was lost. The claim runs as a task (run_as_task), so sending is blocked and Stop is shown while it loads Browser: the analysis handoff end to end
The claim-id regex accepted a trailing newline fullmatch Unit test. Sabotaged.
Handoff refusals logged only "handoff refused", because the formatter drops extra= The reason goes in the message Seen in the log: handoff refused: no_summary (no stored summary at tier identifiers)

Verified

  • ./checks.sh passes.
  • Browser runs against a local instance:
    • The search handoff opens, and markup still renders as text.
    • The analysis handoff passes end to end: minted, tier enforced, same summary, follow-up answered from it, unknown id reported. The script's expected text for an unknown id was out of date and has been updated.

🤖 Generated with Claude Code

- /api/answer deletes its one-shot graph thread afterwards. Each answer
  left ~15 KiB of checkpoints for the life of the process, which also
  serves the chat (measured: 600 answers grew RSS by 22 MiB).
- verify() refuses every malformed token. InvalidKeyError -- a header
  naming the allowed algorithm that does not match the key -- is not an
  InvalidTokenError and became an unauthenticated 500 on three routes.
  Tokens over 4,096 characters are refused before parsing.
- One handoff claim per session. The window-message channel accepts posts
  from any page and has no rate limit; every distinct id was redeemed,
  each costing a message, a log line and state. The claimed id is kept as
  a string: a set in user_session does not survive being saved as JSON.
- A claim runs as a task, like a message, so sending is blocked while the
  analysis data loads. A question asked meanwhile ran on the same thread
  and the seed was lost, while the chat said it was continuing.
- Claim ids are fullmatched; `^...$` with match() accepted a newline.
- Handoff refusals and claims log their reason in the message: the only
  formatter does not print `extra=`, so every refusal read the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit 65aaa7f into main Oct 3, 2026
10 checks passed
@adamjohnwright
adamjohnwright deleted the review-1a-robustness branch October 3, 2026 12:12
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