Skip to content

fix: let long team tasks accept follow-ups past 32 journal entries - #28

Merged
SergioTCG merged 1 commit into
mainfrom
fix/team-task-journal-limits
Oct 8, 2026
Merged

SergioTCG merged 1 commit into
mainfrom
fix/team-task-journal-limits

Conversation

@SergioTCG

Copy link
Copy Markdown
Owner

Summary

A long two-phase lane hit both per-task journal walls at once:

  • Coordinator side: its 33rd follow-up was refused with "This task reached its bounded control-operation limit."
  • Worker side: after 31 replies, only the slot reserved for a final gate-clearing checkpoint was left, so the worker could no longer report gate progress either.

This PR raises both journals to a lifetime-sized bound. It keeps their complete history, and bounds the task views so responses do not grow with that history.

Root cause

  • TASK_CONTROL_MAX = 32 caps a task's control journal: the request-ID records for message, replace, cancel and release. Every coordinator follow-up adds one.
  • TEAM_MAX_REPLIES = 32 caps worker replies and checkpoints, and reserves the last slot for the checkpoint that clears the final gate.
  • The affected lane took 32 follow-ups in about 34 hours. A task's lifetime is seven days.
  • Reproduced on main with a task of the live shape (32 delivered follow-ups, 31 replies, 6 pending gates, awaiting_release):
    • follow-up 33 → task_control_limit;
    • a progress checkpoint → reply_limit ("reserved its final reply slot").

Fix

  • Raised caps. TEAM_TASK_CONTROL_MAX (now exported) and TEAM_MAX_REPLIES go from 32 to 256. Nothing is evicted or compacted, so the following behave exactly as before:
    • request-ID idempotency, including the oldest operation;
    • conflict detection;
    • sab team mutation receipts;
    • delivery recovery;
    • the reserved final gate-clearing slot;
    • the cancel and release safety valves.
  • Bounded views. Every sab team message, reply and checkpoint response embeds publicTeamTask(), which for this lane is already about 200 KB. Views now show the latest 32 replies and messages (TEAM_TASK_VIEW_ENTRIES) plus replyCount and messageCount. Response size therefore stays within today's envelope. For tasks with 32 or fewer entries the view is unchanged apart from the two counts.
  • Exact single-entry responses. These now use publicTeamReply() and publicTeamMessage(), so an idempotent retry of an old operation still returns its exact entry.
  • No migration. A refused operation was never journaled. Existing tasks become unblocked as soon as the daemon restarts, and a coordinator can retry with the same request ID.

Tests

  • a long task accepts follow-ups and checkpoints past the former 32-entry walls:
    • follow-up 33 resumes the same task, with ownership, gates and the earlier message/reply history unchanged;
    • retries of the oldest and newest request IDs are idempotent;
    • reusing the oldest ID for different content is a conflict, and its mutation receipt resolves;
    • the worker checkpoints gate progress and then clears the final gate.
  • task journals stay bounded at their lifetime caps.
  • task views show the latest entries with totals while journals keep full history, including the exact single-entry mappers.
  • single-entry team responses never depend on the bounded task view, a daemon contract.
  • The existing cap tests now use the exported constants, so they keep testing the full-journal and reserved-slot cases at the new bounds.

Each mutation was applied on its own, then the files were restored byte-identical:

Mutation Result
Restore the 32 control cap 3 tests red, including the live-shaped regression
Restore the 32 reply cap 3 tests red
Remove the view window 1 red
Remove the bound entirely 1 red
Reintroduce a view lookup in a response 1 red

Validation

  • npm test: 585 pass, 0 fail. The focused team suites pass 91 of 91.
  • npm run check, npm run audit (0 vulnerabilities), node --check, Pi extension --list-models and shellcheck all pass. Every daemon named import resolves.

Deployment notes

  • Daemon restart needed: yes. The change is daemon code only, with no dependency or state change.
  • Running sessions survive the restart. The blocked task stays bound to its worker; nothing is cancelled, released or recreated.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Xs8AVBVrTGfpzUMiCQp2R

A two-phase lane hit both per-task journal walls at once, at the same time.
Its 32 delivered coordinator follow-ups filled the control journal, so the
next `sab team message` was refused with "This task reached its bounded
control-operation limit". Its 31 worker replies left only the slot reserved
for a final gate-clearing checkpoint, so the worker could no longer report
gate progress. At that lane's pace (32 follow-ups in about 34 hours), a
task's seven-day lifetime needs far more entries.

- TEAM_TASK_CONTROL_MAX (previously the private TASK_CONTROL_MAX) and
  TEAM_MAX_REPLIES rise from 32 to 256. Both journals keep their full
  history, so these all work exactly as before:
  - request-ID idempotency, including the oldest operations;
  - conflict detection;
  - mutation receipts;
  - delivery recovery;
  - the reserved final gate-clearing reply slot;
  - the cancellation and release safety valves.
- Every message and reply response embeds the task view. That view now
  windows replies and messages to the latest TEAM_TASK_VIEW_ENTRIES (32)
  and reports replyCount and messageCount, so responses stay within today's
  size envelope instead of growing with a long task's history.
- Single-entry responses map their exact reply or message with the new
  publicTeamReply() and publicTeamMessage() helpers, so an idempotent retry
  of an old operation still returns that entry.

A refused operation was never journaled, so no migration is needed. A
coordinator can retry with the same request ID after deployment.

Tests:
- A live-shaped regression: 32 delivered follow-ups, 31 replies, 6 pending
  gates, awaiting release. It verifies that follow-up 33 resumes the same
  task with ownership, gates and earlier history intact; that idempotency
  and conflict checks still cover the oldest request; that the mutation
  receipt for it resolves; and that the worker can checkpoint gate progress
  and still clear the final gate.
- The new bounds are enforced.
- Views are windowed, with exact single-entry mappers.
- Daemon responses never look entries up in the bounded view.

Restoring either old cap, removing the window, removing the bound, or
reintroducing a view lookup each turns these tests red. Full suite: 585
pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Xs8AVBVrTGfpzUMiCQp2R
@SergioTCG
SergioTCG merged commit 622eb19 into main Oct 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.

2 participants