Skip to content

feat: pause at a human gate — PausingInterviewer, checkpointed gate record, tracker.Answer - #734

Open
clintecker wants to merge 12 commits into
mainfrom
feat/pause-gate-core
Open

clintecker wants to merge 12 commits into
mainfrom
feat/pause-gate-core

Conversation

@clintecker

@clintecker clintecker commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Second of three PRs for #732. With PR 1's pause plumbing in place, nothing yet pauses a human gate, and a library caller has no way to answer one. Running from an agent's shell, CI or a script still means stdin, --auto-approve, --autopilot or a webhook.

Fix

  • handlers.PausingInterviewer. Records exactly what each gate asked and pauses the run with pipeline.NewPausedGateError. The record (pipeline.PausedGate) holds:

    • the Ask* arguments as passed;
    • the full prompt;
    • the scope path;
    • interview questions, including context;
    • a fingerprint (GateFingerprint) and a random nonce.

    Alternatively it returns an in-memory PendingAnswer once, to the gate with the same node, scope and fingerprint. If the question changed, the answer is dropped and the gate pauses again with a note.

  • Engine. Stores the records on Checkpoint.PausedGates (secure state dir, authoritative) and Checkpoint.PriorUsage. It mirrors them to <run dir>/gate.json (0600, symlink refused, atomic, never read back) and puts the gate on gate_paused. Records clear when the paused node completes; a run start with no waiting gate removes a stale mirror.

  • Per-gate context. handlers.WithGateInfo / GateInfoFrom carry the gate's identity, full prompt, ScopePath and branch index on every Ask*Context call. Subgraph and manager_loop children run under pipeline.WithSubgraphScope, and branch contexts get internal _branch_scope / _branch_index keys.

  • Pause mode inside a subgraph or manager_loop child fails closed (routable OutcomeFail). A paused gate's gate_resolved carries resolution: "paused" and no error.

  • Parallel. Every waiting branch gate is recorded, lowest branch first. A billing pause outranks the gates and carries no gate records.

  • Resume of a gate-paused run. No context compaction (the question must re-render identically), and the budget baseline is seeded from PriorUsage, so caps span every answer.

  • Library.

    • Config.GateMode (pause requires ArtifactDir; console; lowest priority).
    • tracker.Answer, ValidateAnswer, PendingGates, WaitingRuns.
    • Result.PausedGate / RunSummary.PausedGate.
    • Per-run lock: pipeline.AcquireRunLock, taken by every ResumeRunID run, stale locks replaced. Answer re-checks the nonce under the lock.
  • Wire. gate_resolution / gate_fingerprint on the activity log and NDJSON; GateQuestion.context.

  • Docs. embedding.md, transport-boundary.md, handlers/human.md (pause mode, plus the not-tamper-proof residual), engine.md (pausing), api-stability.md, site transport page, CHANGELOG.

  • Size-ratchet moves. interviewer_callback.go, parallel_events.go, parallel_pause.go, tracker_resume.go; terminal results moved to terminal_emit.go. The baseline only shrinks.

Verification

  • New tests, each watched failing first (the run-lock test was checked with a mutation instead):
    • fingerprint, checkpoint record, mirror, stale-mirror removal, clearing on completion, gate_paused detail;
    • scope and GateInfo on context, including inside parallel branches;
    • PausingInterviewer in every mode: apply-once, changed question, other scope, interview answers and cancel;
    • handler pause event and the subgraph fail-closed path;
    • parallel merge and billing precedence;
    • resume without compaction (RF1), plain resume re-pauses / interactive answer clears (RF2), budget across answers;
    • the ValidateAnswer table (incl. " Approve ", RF3), GateMode precedence, Answer round trip / not waiting / two gates / planted files ignored / nonce changed;
    • WaitingRuns, run lock (concurrent + stale + released after a canceled answer, RF4).
  • go test ./... -short green; API surface and wire envelope goldens updated; activity schema regenerated; pre-commit gates green.

PR 1 (#733) is merged; rebased onto main.

Part of #732

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-20T22:47:02.468840Z 0e73409 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 25 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b8ac3da6-4044-4b8a-8a82-faec56ec575f
📥 Commits

Reviewing files that changed from the base of the PR and between 17d4d6e and ec0347e.

⛔ Files ignored due to path filters: 1
  • scripts/gen/activitylog/schema.go is excluded by !**/gen/**
📒 Files selected for processing: 56
  • CHANGELOG.md
  • docs/api-stability.md
  • docs/architecture/embedding.md
  • docs/architecture/engine.md
  • docs/architecture/handlers/human.md
  • docs/architecture/transport-boundary.md
  • pipeline/budget.go
  • pipeline/checkpoint.go
  • pipeline/engine.go
  • pipeline/engine_options.go
  • pipeline/engine_resume_paused.go
  • pipeline/engine_resume_paused_gate_test.go
  • pipeline/engine_run.go
  • pipeline/engine_run_identity.go
  • pipeline/events_gate.go
  • pipeline/events_jsonl_entry.go
  • pipeline/events_jsonl_gate.go
  • pipeline/handlers/human.go
  • pipeline/handlers/human_gate_aware.go
  • pipeline/handlers/human_gate_ctx_test.go
  • pipeline/handlers/human_gate_events.go
  • pipeline/handlers/human_gate_pause.go
  • pipeline/handlers/interviewer_callback.go
  • pipeline/handlers/interviewer_pausing.go
  • pipeline/handlers/interviewer_pausing_test.go
  • pipeline/handlers/manager_loop.go
  • pipeline/handlers/parallel.go
  • pipeline/handlers/parallel_branch_test.go
  • pipeline/handlers/parallel_pause.go
  • pipeline/handlers/pause_resume_test.go
  • pipeline/pause.go
  • pipeline/paused_gate.go
  • pipeline/paused_gate_answer.go
  • pipeline/paused_gate_test.go
  • pipeline/run_lock.go
  • pipeline/run_lock_test.go
  • pipeline/run_lock_unix.go
  • pipeline/run_lock_windows.go
  • pipeline/scope.go
  • pipeline/scope_test.go
  • pipeline/subgraph.go
  • pipeline/terminal_emit.go
  • scripts/complexity/baseline.txt
  • site/content/transport.html
  • testdata/api_surface.golden
  • tracker.go
  • tracker_activity_entry_gen.go
  • tracker_activity_raw_gen.go
  • tracker_answer.go
  • tracker_answer_test.go
  • tracker_audit.go
  • tracker_events.go
  • tracker_events_payload.go
  • tracker_interviewer.go
  • tracker_resume.go
  • tracker_wireshape_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

clintecker added a commit that referenced this pull request Oct 20, 2026
…ed payload, stale records, OS run lock

- Several gates waiting in one parallel node looped forever when answered
  one at a time: each answer re-ran the node, the unanswered sibling paused
  again and the given answer was discarded. The pausing interviewer now holds
  one answer per gate (NewPausingInterviewerFor), and tracker.ValidateAnswers
  requires an answer for every waiting gate (AnswerInput.Gates, keyed by node
  ID or <scope>/<node>); a single answer with several waiting is refused,
  naming them. A branch gate that is its branch's target no longer repeats
  its node ID in its scope path. AnswerInput.Node is gone (unreleased).
- gate_paused carries the waiting gate's node and scope, how many gates
  wait, and the tracker answer command (pipeline.AnswerCommand), in
  GateDetail, the event message, the activity log and the NDJSON stream.
- A run that ends any way but paused at a gate drops its waiting-gate record
  and gate.json (a failure at the gate, a --from resume around it).
- The run lock is an OS file lock (flock / LockFileEx), released by the OS
  when the holder exits, so no stale-lock takeover (and no race in it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
clintecker added a commit that referenced this pull request Oct 20, 2026
…ed payload, stale records, OS run lock

- Several gates waiting in one parallel node looped forever when answered
  one at a time: each answer re-ran the node, the unanswered sibling paused
  again and the given answer was discarded. The pausing interviewer now holds
  one answer per gate (NewPausingInterviewerFor), and tracker.ValidateAnswers
  requires an answer for every waiting gate (AnswerInput.Gates, keyed by node
  ID or <scope>/<node>); a single answer with several waiting is refused,
  naming them. A branch gate that is its branch's target no longer repeats
  its node ID in its scope path. AnswerInput.Node is gone (unreleased).
- gate_paused carries the waiting gate's node and scope, how many gates
  wait, and the tracker answer command (pipeline.AnswerCommand), in
  GateDetail, the event message, the activity log and the NDJSON stream.
- A run that ends any way but paused at a gate drops its waiting-gate record
  and gate.json (a failure at the gate, a --from resume around it).
- The run lock is an OS file lock (flock / LockFileEx), released by the OS
  when the holder exits, so no stale-lock takeover (and no race in it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@clintecker
clintecker force-pushed the feat/pause-gate-core branch from 266e1a8 to 961513d Compare October 20, 2026 23:45
clintecker added a commit that referenced this pull request Oct 21, 2026
…ly gate reads, answer command flags, interrupted resume keeps the gate

- A parallel node whose answered gates re-pause one at a time (a changed
  question) alternated between its gates forever: the re-pause recorded only
  the gate that asked again. It now records the node's other gates again too
  (withAnsweredSiblings) — their answers were used, and the node re-runs.
- PendingGates / WaitingRuns / Answer read only the secure checkpoint: a
  checkpoint planted in the tool-writable run dir is never listed or resumed.
- The gate_paused answer command carries the run's -w / --artifact-dir
  (WithAnswerCommandFlags, set by the library from WorkingDir / ArtifactDir)
  and no shell comment; AnswerAlternatives lists the other answers.
- A canceled resume (Ctrl+C during an interactive tracker -r) keeps the
  waiting-gate record instead of dropping it with the fail terminal.
- Size-ratchet moves: generateRunID to engine_run_identity.go, the budget
  usage helpers to budget.go (baseline shrinks).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01647BKi372JDvjUGxQeYU9g
@clintecker
clintecker deleted the branch feat/pause-gate-engine October 21, 2026 19:50
@clintecker clintecker closed this Oct 21, 2026
clintecker and others added 12 commits October 21, 2026 14:50
… mirror

A PauseError can carry the waiting gate(s) (PausedGate: the gate's exact
interviewer arguments, full prompt, fingerprint and nonce). The engine
stores them on Checkpoint.PausedGates before saving, stamps the cumulative
usage on PriorUsage, writes a 0600 gate.json mirror in the run dir (symlink
refused, atomic rename; never read back), and puts the gate on the
gate_paused event (GateDetail.Resolution "paused" + Fingerprint, also on
the activity log and the NDJSON stream). The record and mirror clear when
the paused node completes; a run start with no waiting gate removes a stale
mirror. buildSuccessResult moves to terminal_emit.go for the size ratchet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The human handler builds the gate's GateInfo once (now with the full prompt,
ScopePath and BranchIndex) and passes it to the interviewer on the gate
call's context (WithGateInfo / GateInfoFrom) as well as to BeginGate, so an
interviewer serving concurrent branch gates reads the right gate from the
call. Subgraph and manager_loop children run under WithSubgraphScope; the
parallel handler marks each branch context with internal branch scope and
index keys (internal, so they never leak past the join).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nswer once

PausingInterviewer records exactly what each gate asked (its Ask* arguments,
the GateInfo from the call's context, interview questions incl. context, a
fingerprint and a fresh nonce) and pauses with NewPausedGateError, or returns
the in-memory PendingAnswer once for the gate with the same node, scope and
fingerprint. A changed question drops the answer and re-pauses with a note.
In pause mode a gate inside a subgraph / manager_loop child fails closed
(routable OutcomeFail); a paused gate's gate_resolved carries Resolution
"paused" and no Error. GateQuestion gains Context. CallbackInterviewer and
QueueInterviewer move to interviewer_callback.go for the size ratchet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…first

A parallel node whose branches pause at gates returns one paused_gate pause
carrying every branch's gate record, ordered by branch index (it used to keep
only the first branch's pause). A billing pause in any branch outranks gate
pauses and carries no gate record: the resume re-runs the node and its gates
ask again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resuming a run whose checkpoint has PausedGates skips resume compaction (the
gate must re-render the question it recorded, or the answer never matches)
and seeds the budget guard's baseline from PriorUsage, so the cost and token
caps bound the whole run across answers. Other resumes are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Config.GateMode ("pause" / "console", after Interviewer / AutoApprove /
WebhookGate / Autopilot; pause requires ArtifactDir). tracker.Answer resumes a
paused_gate run with an answer validated against the checkpoint's record
(ValidateAnswer: per-mode rules, case-insensitive labels, errors list the valid
answers), re-checking the record's nonce under the run lock; WaitingRuns and
PendingGates read the authoritative checkpoint. Every ResumeRunID run holds a
per-run lock in the secure dir (pipeline.AcquireRunLock: O_EXCL + PID, stale
locks replaced). Result / RunSummary gain PausedGate. Docs: embedding,
transport boundary, human handler (pause mode), engine (pausing), API
stability, site transport page. Size-ratchet moves: resume setup to
tracker_resume.go, terminal result builders to terminal_emit.go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rprint

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CLI's -r resolves the run to Config.CheckpointDir rather than setting
ResumeRunID, so the lock keyed only on ResumeRunID missed it. Any existing
checkpoint the run resumes from now takes the lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed payload, stale records, OS run lock

- Several gates waiting in one parallel node looped forever when answered
  one at a time: each answer re-ran the node, the unanswered sibling paused
  again and the given answer was discarded. The pausing interviewer now holds
  one answer per gate (NewPausingInterviewerFor), and tracker.ValidateAnswers
  requires an answer for every waiting gate (AnswerInput.Gates, keyed by node
  ID or <scope>/<node>); a single answer with several waiting is refused,
  naming them. A branch gate that is its branch's target no longer repeats
  its node ID in its scope path. AnswerInput.Node is gone (unreleased).
- gate_paused carries the waiting gate's node and scope, how many gates
  wait, and the tracker answer command (pipeline.AnswerCommand), in
  GateDetail, the event message, the activity log and the NDJSON stream.
- A run that ends any way but paused at a gate drops its waiting-gate record
  and gate.json (a failure at the gate, a --from resume around it).
- The run lock is an OS file lock (flock / LockFileEx), released by the OS
  when the holder exits, so no stale-lock takeover (and no race in it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ly gate reads, answer command flags, interrupted resume keeps the gate

- A parallel node whose answered gates re-pause one at a time (a changed
  question) alternated between its gates forever: the re-pause recorded only
  the gate that asked again. It now records the node's other gates again too
  (withAnsweredSiblings) — their answers were used, and the node re-runs.
- PendingGates / WaitingRuns / Answer read only the secure checkpoint: a
  checkpoint planted in the tool-writable run dir is never listed or resumed.
- The gate_paused answer command carries the run's -w / --artifact-dir
  (WithAnswerCommandFlags, set by the library from WorkingDir / ArtifactDir)
  and no shell comment; AnswerAlternatives lists the other answers.
- A canceled resume (Ctrl+C during an interactive tracker -r) keeps the
  waiting-gate record instead of dropping it with the fail terminal.
- Size-ratchet moves: generateRunID to engine_run_identity.go, the budget
  usage helpers to budget.go (baseline shrinks).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01647BKi372JDvjUGxQeYU9g
…only

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01647BKi372JDvjUGxQeYU9g
@clintecker clintecker reopened this Oct 21, 2026
@clintecker
clintecker changed the base branch from feat/pause-gate-engine to main October 21, 2026 19:50
@clintecker
clintecker force-pushed the feat/pause-gate-core branch from 5ed4be0 to ec0347e Compare October 21, 2026 19:52

This branch has not been deployed

No deployments
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.

1 participant