v1.11.2 — What Cannot Be Rebuilt Cannot Be Deleted
A data-integrity patch. One near-miss set the theme: the container-team reaper - shipped two days before token ledgers landed on agent rows - was deleting rows that carried the only copy of their token attribution. This release stops that deletion (twice, at two different gates), restores byte-exact what one purge had already destroyed, pins dispatch identity so ledgers cannot be overwritten by a same-name dispatch, and brings the roster tools back under the MCP result cap so damage of this kind is at least visible. Surfaces stay put: MCP tools 113, REST endpoints 207, machine checks I1-I14. Tests 2,307 -> 2,493.
Added
- Purge restore tool (
0faacd8) -scripts/restore_purged_container_team.pyrebuilds a purged container team from three independent sources: an identity snapshot, the token-backfill journal, and a third backup used purely for corroboration (8,557 units compared, zero contradictions). Its clock-regime gate readsPRAGMA user_versionon every source - the forensics-recommended backup turned out to predate the UTC migration, and restoring it verbatim would have injected 843 wall-clock timestamps into a UTC database, indistinguishable from truth afterwards. Rows and their ledgers land in a single transaction, so at no instant does a restored team look like an unaccounted husk to the reaper. Used in production to restore 1 team / 7 agents / 835 activities and 325,075,503 tokens of attribution (4.74% of everything measured), verified to the digit; idempotent re-run plans zero writes. - Branch-ownership assertion at commit time (
399c078) - the 2026-07-10 incident was a commit silently landing on another session's branch, so the guard now lives where the harm happens instead of where the preparation happens. Each agent's first commit in a checkout records its branch in hook-local state (no API dependency - live-agent counts measured unreliable in both directions); committing on a branch another agent recorded within 24h exits 2; switching your own branch warns loudly; probe failures are loud rather than silently permissive. - Governance-lease takeover observation (
9148423) -governance.lease_taken_overfires when an expired lease is claimed over a previous holder. Deliberately observation-only: the lease table keeps no history, so whether takeover ever actually happens was unknowable. Two weeks of data decide whether a fence epoch is warranted; zero events means it is not. - Dispatch-identity predicate (
0202f24) -services/agent_identity.py: an agent row may be reused only by the same CC dispatch (cc_tool_use_id) or if it never was a dispatch and carries no ledger. All three name-matching rungs ofSubagentStartdedup, the pre-create race check and_resolve_agentlate binding now go through it; a reuse never blanks an existing binding (an empty payload id is "unknown", not "none"). - Template registry + launch-call validation (
fdecaa6) -services/agent_template_registry.pyis shared by both spawn-plan construction sites. Unknown template names pass through untouched with a top-leveltemplate_warningsentry carrying nearest-match suggestions - templates are an accelerator, not an allowlist: the legal set is CC's actual registry (templates plus built-ins, and the built-ins cannot be enumerated), so validation reveals, never rejects, and never silently swaps a type. An AST-level test pins every present and futurelaunch_callconstruction site to the validation path - both previous incidents took exactly the shape of a new construction site nobody thought to validate. - Memory v2.1 (
7a75886) - direction-layer storage cap equals the injection budget (buckets: global 1200 / project 1500 / user 300 chars). An over-quota write is rejected with the complete bucket inventory and an in-turn reorganize-then-retry protocol, so the budget is enforced at write time instead of by silent truncation at injection time. Write-side threat scan (invisible-Unicode ranges, injection patterns, credential shapes - credentials are never echoed back);memory_invalidateaccepts acontent_matchsubstring; meeting conclusions stopped auto-writing to the team memory scope. - Release-process closure (
b4b7fe2) -scripts/release_notes.pygenerates Release notes offline from the CHANGELOG slice and byte-compares against the live entry (--check); the/os-releasechecklist skill records who presses which button; both READMEs link the CHANGELOGs. Written after two identical misses in which tags were pushed weeks before their Release entries existed.
Fixed
- Reaper could destroy irreplaceable attribution (
a6ccb67,6da695a) - the container-team purge shipped in v1.11.0 (07-27), token columns landed on agent rows in v1.11.1 (07-29), and the retention gate was never revisited: it asked "does anything still reference this team" (six tables) while the ledger lives on the member rows themselves. Two gates now stand: any member carrying token usage - or merely atokens_measured_at, since a measured zero is a measurement - blocks the purge forever; and any member still pointing at an unharvested transcript on disk blocks it too, because the row is the only pointer to that file and deleting it closes the backfill window silently. Verified against production with a paired dry-run: gate off would have deleted 9 more teams; gate on excluded exactly those 9. - Same-name dispatch overwrote token ledgers (
0202f24) -find_agent_by_sessionwaslimit(1)with noORDER BY, and SubagentStart's name-based dedup could weld a new dispatch onto an old offline row, whereupon SubagentStop overwrote that row's four token columns in place. Row identity now equals dispatch identity (see Added);find_agent_by_sessionreturns the newest match. Historic fold-ins cannot be reconstructed and are disclosed in the coverage note instead of being recomputed. - Roster tools were effectively dead on real teams (
b2d1e21,57418c8,b543edf) -agent_listmeasured 67,766 chars andteam_status69,660 on a 51-member team, both rejected by CC's MCP result cap (25,000 tokens; the per-toolmaxResultSizeCharsmeta only governs disk spill and does not lift it). Seven tools moved to compact projections (51-memberagent_list-> 1,982 chars; 173-member workflow-teamteam_status170,331 -> 1,509;workflow_get268,753 -> 15,909). Offline rows fold into a count plus digest withinclude_offline=Trueas the explicit escape hatch;fieldscontrols row width andinclude_offlinecontrols row selection - orthogonal by contract, because an escape hatch that exceeds the cap is no escape hatch.event_list's compact window caps at 60 rows. - Template identity split between filename and frontmatter name (
fd177b5,fcf33ab) - 15 of 25 templates had stem != name; dedup and grouping keyed on the stem while CC resolves the name, so cross-source duplicates listed twice and 15 templates could be listed but not opened. Identity now keys on frontmatter name (stem fallback),get_templateresolves names too, and grouping keys on shared stem-prefix families (6 buckets, not 21) - grouping and identity are orthogonal. - Leader-usage capture was unfalsifiable (
f316f66) - five distinct outcomes collapsed into oneNonewith debug-only logging that rotated away within minutes, so "did capture run" could not be answered after the fact (the chain itself measured healthy end-to-end: 53MB transcript parses in 0.209s, 44 concurrent writers, zero losses). Outcomes now carry reasons, forced-capture failures log at WARNING and emitLEADER_USAGE_CAPTURE_FAILED(the red test caught the latentValueErrorof an unregisteredEventTypemember on the spot), andcwdjoinedsend_event's essential fields so the transcript-slug fallback works in the very payload-stripping scenario it was written for. - Backfill Job D idempotence (
6b01045) - measured rows now claim their transcript's representation slot up front. Previously a re-run promoted ghost rows sharing an already-harvested main transcript into write candidates, and a second--applywould have double-counted the same session's usage - caught live on the production re-run check. - fastmcp supply chain (
0b34faf) - pinned>=3.4.5,<4across all three requirement surfaces (install.pywas entirely unpinned); the two private-API touchpoints replaced with public APIs; MCPserverInfonow reports the OS version instead of the library default. - preflight's eslint step skips when
node_modulesis absent, same semantics as the ruff step (4af4169).
Changed
- worktree isolation extended from 12 to 20 of 25 agent templates (
1ad471f,1c7d771) - criterion: does the template's own description promise files landing in the repo.code-reviewerdeliberately stays out - isolation would degrade branch review from direct reads togit show. - Chinese CHANGELOG backfilled for 1.10.0 -> 1.11.1 under the mirror-translation contract (
fa4e61d) - six versions, section structure aligned line-for-line, numbers machine-compared against the English source. - CLAUDE.md gained an engineering-traps section (
7b33b68), since extended with two production receipts: delete paths must ask "can this be rebuilt" (5d17904), and restoring from an old backup must check the clock regime first (8e42a73). The workflow skill gained a structured-output size discipline (7b33b68).
Upgrade notes
- Run
python3 install.py --updateafter upgrading: the branch-ownership assertion lives in the hook copies under~/.claude/hooks/, which only refresh on install/update. - No schema migration. The reaper gates and the takeover event activate on API restart.