Files
ironclaw/tools
Henry Park cbc3c4fac4 feat(turns): subagent activation provenance, activate() primitive, and derived autonomous-wake cap (slice 1) (#7752)
* docs(subagent): record background-enable recon, shape decision, and slice 1-2 plan

Recon of the landed PR1 subagent path, the shape decision to ship the design's
PR2-PR6 before clearing the production deny-filter, and the TDD plan for
slices 1-2 (activation provenance + background completion delivery).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(turns): add ActivationProvenance vocabulary for subagent activation tagging

Set once at run creation and immutable thereafter, so the derived activation
streak caps (design section 6 and 8.3) can read bounded windows of run history
instead of maintaining a stored counter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(turns): persist subagent activation provenance on the run record

Threads ActivationProvenance from a submission into durable agent-turn process
metadata and back out onto TurnRunRecord, so the derived streak caps can read
it. Additive and serde-defaulted: rows written before the field stay readable
as None, which is also the value every ordinary human-initiated submission
carries. A fresh child run is a spawn rather than a re-activation, so it
records no provenance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(turns): add provenance-tagged activate() re-activation primitive

activate() is the single re-activation primitive for an existing thread. It is
not a second admission path: it builds an ordinary SubmitTurnRequest, so
one-active-run exclusivity, idempotency replay, and busy rejection behave as
they do for any other submission, and the only thing it adds is the provenance
stamp the derived streak caps read.

The trait method carries a fail-closed default so the many test doubles of
TurnCoordinator need not each restate it, and so a coordinator that has not
opted into activation refuses rather than silently creating an untagged run
that the streak cap could not see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(processes): add bounded newest-first agent-turn query for a thread scope

The derived activation-streak caps need a fixed recent window of a thread's own
runs. Nothing returned that today: children_of is parent-keyed and unbounded,
and process_snapshots enumerates a whole scope.

Reuses the existing process_scope_v3 (scope_key, created_at, process_id) index
and the already-implemented Descending sort, so this needs no new index and no
backfilling migration. Because that index is deliberately not keyed on
process_kind, and a thread's scope also holds capability-invocation processes,
a flat LIMIT could come back holding no runs at all -- so this walks the
descending keyset a page at a time and filters by kind, bounded by a page
budget. The enumeration lives in rows.rs and reaches storage through
query_ordered, which keeps it outside the storage-scan gate gate's reach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(turns): bound autonomous System activations with a derived streak cap

Nothing else bounds the cumulative spawn -> settle -> wake -> spawn cycle: a
parent that spawns a fresh child on every background completion would loop
indefinitely under every existing cap, with no human in it. The cap is derived
from a bounded window of the thread's own run records rather than a stored
counter, so it adds no new component and no new persistence.

Refusing costs nothing durable -- a settled await-edge stays settled and drains
via the run-start sweep or the boot pass -- so this gates the reactive wake
only, never delivery. Human activations reset the streak and are never capped;
ParentAgent runs sit outside this window entirely so the two caps stay
independent.

recent_runs_for_thread moves onto the base AgentTurnRuntimePort with a
fail-closed default: an empty window reads as 'streak not established', so a
runtime that cannot answer must refuse rather than silently disable the cap.

Re-pins the host_api contracts size ceiling for ActivationProvenance, which is
turn vocabulary and has no lower crate that may own it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(subagent): mark slice 1 complete in the background-delivery plan

Records the five landed commits with their evidence, the gates run at slice
close, and the two gates this environment could not run (clippy component
absent; WebUI frontend build broken via corepack).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(turns): forward activate() through the production coordinator decorator and exclude ParentAgent from the streak fetch

Two defects found by code review of slice 1, each with a regression test that
fails before its fix.

1. CancelReconcilingTurnCoordinator is the one TurnCoordinator production
   composes, and its doc says every other method forwards -- but activate()
   did not, so it inherited the trait's fail-closed default and every
   production activation would have been refused. Slice 2's background
   delivery would have been dead on arrival, in the integration harness too,
   which mirrors this wiring deliberately.

2. The System-wake streak cap fetched exactly K records and only then filtered
   out ParentAgent, so interleaved ParentAgent runs shrank the window below K.
   A short window reads as 'streak not established' and admits, which disabled
   the cap entirely on exactly the human-free interleaved sequences it exists
   to bound -- while the code comment claimed the opposite. The design's
   section 8.3 requires ParentAgent be excluded from the fetch and names this
   interleaving case as a required test; both are now honored, with the
   over-fetch factor and its fail-open residual documented.

Also fixes a dead guard in the new bounded read: ResourceScope::system() mints
a fresh invocation_id per call, so comparing a scope against it by equality can
never match. The new code now uses is_system(). The pre-existing
process_snapshots guard one screen up has the same dead comparison and is
reported as a follow-up rather than changed here, since tightening it would
alter behavior for existing callers outside this slice.

Adds the multi-page keyset walk and system-scope coverage the review found
missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(subagent): move the slice 1-2 plan under docs/internal

AGENTS.md requires every internal engineering doc to live under docs/internal/
and nowhere else under docs/; docs/.mintignore is frozen, so a plan left at
docs/superpowers/plans/ would have been published to the public docs site.
scripts/ci/docs_publication_boundary.py failed on it and now passes. Placed
beside the existing superpowers plans, matching that convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address PR review feedback (#7752)

- Preserve activation provenance across terminal metadata rewrites. loop_exit
  writes the agent-turn envelope via agent_turn_metadata_from_claimed on every
  terminal transition, and from_claimed restored subagent_depth and
  spawn_tree_descendant_cap but not provenance -- so every completed run read
  back untagged and the wake cap could never fire in production. Provenance now
  rides on ClaimedTurnRun and is restored alongside its sibling lineage fields.
  The original tests missed this because the helper drove runs terminal
  directly, bypassing the rewrite; the regression test drives the real function
  loop_exit calls.

- Fail closed when the wake window cannot be established. A full raw fetch that
  still cannot yield a cap-sized non-ParentAgent window means the streak is
  unknown, not absent; admitting there was the fail-open residual the
  over-fetch alone left behind. A genuinely short fetch is still a young thread
  and still admits.

- Keep activate()'s advertised submission idempotency true at the cap boundary
  by excluding the caller's own accepted message from the window, so a retry of
  an accepted activation reaches the journal's operation-id replay instead of
  being refused by the run it already created.

- Give the cap refusal its own identity: AdmissionRejectionReason::SystemWakeStreak
  rather than a third indistinguishable InvalidRequest. Both downstream match
  sites audited and classified as retryable capacity, not caller error.

- Assert the cap refusal creates no run, add backend parity legs (libSQL and
  PostgreSQL) for the descending keyset window walk, and correct the stale
  slice number and plan snippet in the design docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Henry Park <16583448+henrypark133@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 00:26:34 +00:00
..