Skip to content

feat(sx): invariant D — cross-cycle extension ceilings; the last self-extend audit GAP closes - #321

Merged
IanFrelinger merged 1 commit into
masterfrom
feat/sx-invariant-d-extension-ceiling
Aug 16, 2026
Merged

feat(sx): invariant D — cross-cycle extension ceilings; the last self-extend audit GAP closes#321
IanFrelinger merged 1 commit into
masterfrom
feat/sx-invariant-d-extension-ceiling

Conversation

@IanFrelinger

Copy link
Copy Markdown
Owner

Summary

Closes the last open item in the self-extend safety audit. docs/SELF-EXTEND-AUDIT.md enforced invariants A, B, C on the live extender path and left D — "cross-cycle extender recursion depth is not capped" — as "the primary deferred safety item before treating unattended multi-cycle self-extension as production-safe." Its rejection test was the last [Fact(Skip = "GAP…")] in the whole suite (and, incidentally, the test the readiness gate tripped over in #317).

Per-cycle caps (ReAct iterations, build/test budget) were always there; nothing bounded the extender across cycles.

ExtensionCeiling — three ceilings, each a refusal on its own

CeilingDefaultWhat it bounds
MaxLineageDepth1Recursion proper.ParentId hops below a human-authored root an extender may sit and still extend: roots and their direct children may, machine-spawned grandchildren may not. An unresolvable parent still counts as a hop.
MaxUnattendedCycles8Runaway. Extend cycles since a human last armed the agent, then hold.
MaxCyclesPerHour4Rate. Extend cycles in any trailing hour.

Posture mirrors the certified loop's RecursionDiscipline: the environment (NEXO_EXTENSION_MAX_LINEAGE_DEPTH / …_UNATTENDED_CYCLES / …_CYCLES_PER_HOUR) and an agent's own Parameters (MaxLineageDepth / MaxUnattendedCycles / MaxCyclesPerHour) may only lower a ceiling, never raise it — so no configuration an agent can write extends its own authority (I-1). Raising a default is a code change. A host may register a stricter ExtensionCeiling in DI.

Enforcement

In BackgroundAgentRegistry, at the same point as invariant C — after the mode gate, before ISelfExtendRunner.RunAsync. A refusal is visible: Warning in the agent log, Warn observation with stopped_reason=extension_ceiling, cycle telemetry. Only cycles that actually reach the runner consume budget (a Passive skip or approval denial does not).

The one design decision that matters: an agent cannot re-arm itself. Ledgers (ExtensionLedger) live on the registry, not the agent instance, so re-registration — which an agent can do to itself via UpdateAgentConfigTool (registers as Authored) — does not reset them. Re-arm is RearmExtension(agentId) (an operator surface that must never be registered as a tool) or a process restart, and it clears the unattended count but not the hour's rate. A CLI verb for re-arm is a follow-up on an application/* branch.

Tests (SelfExtendInvariantDRecursionCeilingTests, 10)

  • the rejection test un-skipped (was the last GAP marker); default ceiling bounds 12 back-to-back cycles at 4 with 8 observable refusals; unattended ceiling binds when rate does not; re-registration does not re-arm and RearmExtension does; lineage grandchild refused; env and Parameters only lower (int / JSON / string shapes); ledger window slides and re-arm does not forgive the rate.
  • BackgroundAgents suite 486/486, 0 skipped.

Docs: audit D → ENFORCED (trace, verdict table, index updated; REORDER note now names the real remaining question — one self-extension path, not two); Configuration.md gains the three env rows.

Test plan

  • Nexo.Tests.BackgroundAgents 486/486 locally (net9.0), 0 skipped
  • CI gates green

🤖 Generated with Claude Code

…-extend audit GAP closes
The self-extend audit enforced invariants A, B and C on the live extender path and left D —
"cross-cycle extender recursion depth is not capped" — as the primary deferred safety item
before treating unattended multi-cycle self-extension as production-safe. Per-cycle caps
(ReAct iterations, build/test budget) were always there; nothing bounded the extender ACROSS
cycles.
ExtensionCeiling (Nexo.BackgroundAgents.Extending) — three ceilings, each a refusal on its own:
- MaxLineageDepth (default 1): recursion proper. ParentId hops below a human-authored root an
extender may sit and still extend; roots and their direct children may, machine-spawned
grandchildren may not. An unresolvable parent still counts as a hop.
- MaxUnattendedCycles (default 8): runaway. Extend cycles since a human last armed the agent,
then hold.
- MaxCyclesPerHour (default 4): rate, trailing hour.
Posture mirrors the certified loop's RecursionDiscipline: the environment
(NEXO_EXTENSION_MAX_*) and an agent's own Parameters may only LOWER a ceiling, never raise it,
so no configuration an agent can write extends its own authority (I-1). Raising a default is a
code change.
Enforced in BackgroundAgentRegistry at the same point as invariant C — after the mode gate,
before ISelfExtendRunner.RunAsync. Refusals are visible: Warning in the agent log, Warn
observation with stopped_reason=extension_ceiling, cycle telemetry. Only cycles that reach the
runner consume budget. Ledgers (ExtensionLedger) live on the registry, NOT the agent instance,
so re-registration — which an agent can do to itself via UpdateAgentConfigTool — cannot reset
them; re-arm is RearmExtension(agentId) (operator surface, never a tool) or a restart, and it
clears the unattended count but not the hour's rate. A host may register a stricter
ExtensionCeiling in DI.
Tests: the invariant D rejection test is un-skipped (was the last GAP marker in the suite);
default ceiling bounds 12 back-to-back cycles at 4 with 8 observable refusals; unattended
ceiling binds when rate does not; re-registration does not re-arm and RearmExtension does;
lineage grandchild refused; env and Parameters only lower (int / JSON / string shapes);
ledger window slides and re-arm does not forgive the rate. BackgroundAgents suite 486/486,
0 skipped. Audit doc: D → ENFORCED, trace and index updated; Configuration.md gains the three
env rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursorBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@IanFrelinger
IanFrelinger merged commit efceb74 into masterAug 16, 2026
5 checks passed
@IanFrelinger
IanFrelinger deleted the feat/sx-invariant-d-extension-ceiling branch August 16, 2026 17:43
Sign up for freeto 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

@IanFrelinger@PlzTouchGrass