Skip to content

feat(PTYCAP-04): lifetime inject counter + kill switch + 60% alarm + 20M cap (stacked on #505) - #506

Merged
finedesignz merged 1 commit into
mainfrom
phase-ptycap-04-lifetime-counter-kill-switch
Oct 14, 2026
Merged

finedesignz merged 1 commit into
mainfrom
phase-ptycap-04-lifetime-counter-kill-switch

Conversation

@finedesignz

@finedesignz finedesignz commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

PTYCAP Phase 4: Lifetime Inject Counter + Kill Switch (DRAFT, QC BLOCKED)

Stacked on #505 (Phase 3). Merge #505 first. The base is main instead of the Phase 3 branch because Woodpecker qc.yaml only runs for PRs into main, and the postgres:16 service is the only place the new SQL is proven.

What ships

  • SC-1 lifetime counter.
    • Append-only automation_inject_ledger with a server-set counterKey:
      • task:<id> for orchestrator and scheduler
      • triage:<application_uuid>
      • heal:<userId>:<repo>
    • The reserve is atomic: it runs under an advisory lock just before send, is idempotent per dispatch token, and is released when a send fails.
    • Rate and lifetime are checked together over a de-duplicated union of the ledger and the run log.
    • Default cap 1000 (REMO_AUTOMATION_LIFETIME_INJECT_CAP). A one-time 80% warning page and an exhaustion page go out. The owner reset needs step-up.
  • SC-2 kill switch.
    • Scopes are global, user and session. It is read fresh on every check, a DB error counts as engaged, and the env break-glass is REMO_AUTOMATION_KILL_SWITCH.
    • Routes are under /api/automation: engage needs no step-up, while release and reset do.
    • The kill gate is first in every non-human gates list, and a coverage test denies by default.
    • Fresh halt checks run right before every machine session.start or run command, on the autospawn, scheduler, TEAB, orphan-resume, auto-launch and /api/sessions/heal paths.
    • A halted one-time task is re-armed instead of burned.
    • Human paths are never gated: web start, Telegram, cloud send and the human PTY.
  • SC-3: an email alarm fires at 60% of the daily token ceiling (REMO_TOKEN_CAP_ALARM_PCT). It is event-driven at token ingest, uses a durable claim with a delivered flag, and sends at most once per user per local day.
  • SC-4: the default daily token cap drops from 50M to 20M.
  • G-1: the supervisor PTY usage emitter now dedupes on message.id, emitting only positive per-bucket deltas. Supervisor is bumped to 0.15.1.
  • Phase 3's governor slots are now real: the kill switch runs first, and the inject-rate check counts actual turns.

Owner notes before deploy

  • The 20M default only applies where REMO_ORCHESTRATOR_DAILY_TOKEN_CAP is unset. Human PTY usage counts toward it. Check the prod env before deploying; this PR changes no prod env.
  • The G-1 fix reaches hosts only with a signed supervisor release (supervisor-v0.15.1 tag).
  • The lifetime cap is literal: a 15-minute cron task exhausts 1000 in about 10 days and then needs an owner reset or a higher env value. This follows SC-1's own text ("4/hr forever is still 35,000/year").

Gates

  • check-baseline: pass=2701, skip=259, fail=0. No new skips.
  • tsc: 427 errors, all pre-existing; no new errors.
  • docs:sync: clean.
  • Supervisor guard tests pass.

QC

/qc: three seats ran: Sonnet adversary, Sonnet correctness/SQL and Codex.

  • Round 1: 1 BLOCKER, 5 MAJOR and 4 MINOR findings, all fixed.
  • Round 2: 3 remaining, fixed.
  • Status: BLOCKED. The 2-round cap was reached with two open HIGH races, both confirmed by the independent verifier:
    1. hub/src/orchestrator/orphan-resume.ts reopenOrphanIfNoReplacement uses UPDATE ... NOT EXISTS, which does not serialize against a concurrent replacement INSERT. Fix: take the same per-user advisory lock that launchOrchestrator holds (auto-launch.ts:~160) around the reopen.
    2. hub/src/scheduler/senders/{supervisor,teab,work}.ts re-arm a one-time task on schedule_kind alone, so a manual Run Now on a completed once task that hits a late halt gets re-enabled. Fix: pass onceClaimed into the run context and re-arm only when it is true, plus a regression test.

Verifier and CI

  • Verifier (04-VERIFICATION.md): SC-1 through SC-4 PASS. NO-SHIP at 8447cdf because CI was red from two e2e fixture defects.
  • CI: those defects are fixed in bf3f51d (unique project_dir, machine launch arg). The CI re-run is pending.

🤖 Generated with Claude Code

@agentautofix-fixer

agentautofix-fixer Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

AgentAutofix AI Review

claude

PASSED

  • warning hub/src/dispatch/gates.ts: Default daily token cap halved (50M → 20M) with no prod env change -- DEFAULT_DAILY_TOKEN_CAP now applies to any deployment that leaves REMO_ORCHESTRATOR_DAILY_TOKEN_CAP unset. This also gates human interactive PTY usage (checked on every Enter per CLAUDE.md), so a deployment relying on the default could suddenly lock the owner out of their own terminal for the rest of the day. The PR's own docs flag this ('THE OWNER MUST CHECK THE PROD ENV VALUE BEFORE DEPLOY') but it's a real operational risk worth surfacing before merge, not just a doc note.
  • info hub/src/dispatch/pipeline.ts: Machine-path detection relies on string-literal duplication across modules -- pipeline.ts defines its own KILL_SWITCH_GATE_NAME / SESSION_INJECT_RATE_GATE_NAME constants that must stay textually equal to AUTOMATION_KILL_SWITCH_GATE_NAME (automation-controls.ts) and the real gate name in gates.ts. Equality is only enforced by a test (automation-controls-config.test.ts) rather than a shared import/const. If that test is ever skipped or the names drift, dispatch() would silently stop treating a gate list as 'machine', disabling the pre-send halt re-check, final halt check, and lifetime reservation for that path while still looking like a no-op (the standalone kill-switch gate in the list would still run, masking the regression in normal testing).
  • info hub/src/api/automation.ts: Kill-switch engage/release/reset audit trail is console.log only -- audit() just writes to stdout; there's no durable audit row for who engaged/released the kill switch or reset a lifetime counter beyond engaged_by/released_by columns on the kill-switch row itself (lifetime resets have no actor column read back). Acceptable for an initial cut given the ledger's reset marker does record the actor, but worth confirming this meets the intended operational audit bar for an emergency-stop control.

codex

PASSED

No findings.

agy

advisory -- not part of the gate decision

PASSED

No findings.

Result: Both AI reviewers passed

Reviewer models (requested pin, not independently confirmed):

  • claude: claude-haiku-4-5-20251001 (requested pin, not independently confirmed)
  • codex: gpt-5.5 (requested pin, not independently confirmed)

…in after #505 squash); releaseInject retries and parks failures

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqBYNFyRHsavnRSJZihaZ6
@finedesignz
finedesignz force-pushed the phase-ptycap-04-lifetime-counter-kill-switch branch from ec2237e to ec8908f Compare October 14, 2026 17:05

Copy link
Copy Markdown
Owner Author

Rebased onto main after #505's squash (clean re-apply of the Phase 4 diff) and fixed the codex blocking finding: releaseInject now retries 3x, returns whether it stamped, parks failures in a bounded queue and drains them on the next release/reserve (test added; check-baseline pass=2708 skip=259 fail=0).

Full QC panel (3 fresh-context reviewers) on ec8908f:

  • Security: no blocking. Minors: truthyEnv fails open on typos (e.g. enabled); /heal counter key embeds caller-supplied repo.
  • Tenancy/authz/data: no blocking. Minors: orphaned session-scope switch after soft-delete; counter reads rely on key-format invariant; thin real-SQL cross-user tests.
  • Correctness: one item flagged blocking-to-confirm: the 1000-inject lifetime cap applies to scheduler agent/triage keys regardless of the PTY flag. This is the locked design (04-CONTEXT D-04/D-05: the hard ceiling that would have stopped the 2,192-turn incident, pages once, owner reset via POST /api/automation/lifetime/reset), so accepted, not a defect. Minors: orphan-resume reopens on non-halt reasons; idempotent reserve/release needs unique per-turn tokens in Phase 5.

Minors are tracked for follow-up; none widen a spend path. Merging when Woodpecker qc + docs-drift are green.


Generated by Claude Code

@finedesignz
finedesignz merged commit ab69c42 into main Oct 14, 2026
4 of 5 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