Repository navigation
docs(agent): document the default pause deadline across the SDK and examples [SAP-3207] - #850
Conversation
Review — PR #8501. The
|
Follow-up review — PR #850Delta reviewed: FixedFinding 1 (the Earlier findings the push did NOT fix
New finding
No confidentiality issues in the delta; Verdict: request changes — two substantive earlier findings (3, 4) untouched, and the tense fix stopped at the changeset. |
|
Holding off on approval for now, for two reasons that both trace to SAP-3207 moving after this branch's last push:
Smaller: The example-side changes and the "forthcoming" wording on the changeset look right. Once the docs match whatever shape #4919 lands in, this should be a quick approve. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | The title clearly identifies the documentation change, the affected agent SDK, and the related SAP-3207 work. |
| Description check | ✅ Passed | The description is complete and follows the repository template. It identifies documentation as the primary change, explains the motivation and scope, records related work, validation results, compati… |
| Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 8 files. (2 skipped: 2 … |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✨ Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create PR with unit tests
- Commit unit tests in branch
antoineberger/sap-3207-engine-paused-runs-without-a-deadline-never-expire-23-stuck
-
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.
A rabbit reads the deadline scroll,
Seven days now guard the goal.
Gates may wait a year in flight,
Signals guide them day and night.
Dead webhooks fail, not sleep away.
Local runs resume without delay.
Comment @coderabbitai help to get the list of available commands.
|
Both fixed in Point 1 is moot: the human-gate rule was cancelled, so there is no rewrite coming and the head is reverted. Its detection needed the resume step to declare a schema shape that real gates do not declare, so it would have covered the rigorous authors and missed the ones it was written for. The default is 7 days for every pause with no Point 2 was worse than a stale sentence. The platform now holds a parent's pause open while its dispatched child is still running, so the docs were making authors set a timeout they do not need. Both JSDoc sites and the README now say a child pause needs none, with the limits of that guarantee rather than an unqualified promise.
|
c87b86f to
40023ef
Compare
gwitwer
left a comment
There was a problem hiding this comment.
LGTM as docs for the engine default. Follow-up I'd like a ticket for: a lapsed pause deadline terminates the run, so authors cannot handle it (approval-chain had to dodge it, and now every human gate carries a one-year deadline). A pause timeout that resumes the step with a timed-out result the author can branch on would make the 7-day default safe without these workarounds. Separately, local runs not enforcing deadlines means local and hosted behaviour diverge on exactly this path.
A pause that omits `timeoutMs` had no documented deadline, so authors assumed a pause waits indefinitely. The engine is gaining a 7-day default (the capability resume-token TTL) after which the run is finalized as failed with `PauseTimeoutError` instead of hanging silently. Document it on `PauseUntilSignalDirective.timeoutMs`, `Pause.timeoutMs`, the `pauseUntilSignal` doc block, and the README's pausing section, with an example of overriding it for a longer human gate. Docs only: no SDK behavior changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first pass overstated the new default and left the repo contradicting itself. Three corrections to the SDK copy: - `run_local` never sweeps for a pause deadline, so the expiry only fires against the hosted engine. Say so instead of "every pause has a deadline". - Raising `timeoutMs` past the default buys nothing on a capability pause, since the resume token expires on the same horizon. The README example now shows a human gate, which is the case where a longer wait is meaningful. - The changeset stated the engine behavior in the present tense and closed on "no behavior changes". It now leads with the behavior notice: a run that previously hung forever will surface as a failure. Two shipped examples documented the opposite of the new default: - `examples/approval-chain` stated in three places that its gates deliberately carry no `timeoutMs`, because a lapsed deadline terminates the run rather than resuming it and would skip the graceful `escalate` step. Omission is no longer an escape hatch, it inherits the 7-day default and does the same damage on a one-week horizon. The gates now pass an explicit one-year `GATE_PAUSE_TIMEOUT_MS`: long enough that a slow approver never loses the run, finite enough that an abandoned chain still reaches a terminal state. - `examples/wait-for-webhook` advertised an indefinite wait by default across its README, header comment, `parseTimeoutMs` doc, AGENTS.md and template notes. All now describe the 7-day fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two surfaces were wrong about the local runner. `runLocal` does not park on a pause: its loop resumes any paused row immediately with the registered signal result or an empty payload (`run-local.ts:152-155`), and with `timeoutMs` omitted the in-memory store writes `pausedUntil = null`, so nothing is recorded either. An author reading "a local run parks on a pause" would file a bug the first time a gate walked straight through. Say what actually happens: no deadline is applied or enforced locally. The changeset also stated the engine default in the present tense, on the one surface that cannot be edited after publish. If `@sapiom/agent` ships before the engine deploy, the tarball CHANGELOG tells consumers their pauses fail at 7 days while they still hang forever. Reworded as a forthcoming change tied to the SAP-3207 engine deploy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`approval-chain` was fixed because omitting `timeoutMs` no longer means an unbounded wait, it means inheriting the engine's 7-day default. The same construct was left untouched in six other pause sites, so a two-week approval would still lose its run in templates whose entire point is a human gate. Human gates now carry the same explicit one-year ceiling as `approval-chain`: `human-in-the-loop` (approval and candidate confirm), `proposal-generator`, `scheduled-compliance-audit`. The two machine waits keep the default and say why, so the choice is on the page rather than inherited by accident: `pr-review-bot` (a PR event that has not arrived in a week is not coming) and `durable-backfill` (a schedule that has not ticked in a week has stopped, and failing loudly beats a backfill parked mid-cursor). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three surfaces justified the default with the capability resume-token TTL and concluded that raising `timeoutMs` on a capability pause buys nothing, because a later result could not be accepted. That holds for the sandboxed coding path and not for a child agent. The TTL is the `exp` of a JWT minted at dispatch and injected into the sandbox as `SAPIOM_CAPABILITY_RESUME_TOKEN`; it is verified only on the gateway's HTTP callbacks. A dispatched child returns through the orchestration-resume path, which verifies no token at all: it resolves the durable dispatch row and fires `agents.result` directly. A child can therefore report back long after seven days, and the advice not to raise `timeoutMs` would fail the parent while the child is still working. Nothing cascades that failure to the child, so it keeps running and its result is dropped against a parent that is no longer paused. Reported on SAP-3207 for the engine side. Here, say what actually bounds what, and tell authors to size a child pause on the child's worst case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine recognizes a run parked on a human approval gate and gives it a one-year deadline, applying the 7-day default only to machine waits. The hardcoded one-year `GATE_PAUSE_TIMEOUT_MS` added to five templates was working around a default that does not apply to them, so it goes: `approval-chain` (both the initial gate and the reminder re-pause), `human-in-the-loop` (approval and candidate confirm), `proposal-generator`, `scheduled-compliance-audit`. Their gates carry no `timeoutMs` again, and the prose says why omitting it is safe rather than why an explicit value was required. The SDK surfaces now state the rule as the engine applies it: a default chosen from what the pause is waiting on, 7 days for a machine wait and one year for a recognized human gate. The child-agent carve-out stands and is now the one case that still needs an explicit `timeoutMs`, since a child's result returns through parent linkage rather than a resume token; the README example moves to that case. The two machine waits are untouched: `pr-review-bot` and `durable-backfill` keep the 7-day default and the reason already written beside them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This reverts commit 213b693. The rule it documented, a 365-day default for a pause the engine recognizes as a human approval gate, was decided and then cancelled. The engine applies 7 days to every pause that omits `timeoutMs`, with no distinction by kind. The cancellation matches the code. On the engine branch, `pause-deadline.ts` exports only `DEFAULT_PAUSE_TIMEOUT_MS`; there is no human-gate constant, and `isApprovalGateSchema` is referenced only by the RUN_WAITING alerting path and the outbox listener, never by `pauseExecution`. The detection would also have rested on an optional author declaration on the resume step that none of the production human gates makes, so the rule would have protected the rigorous authors and failed exactly the ones it was meant to cover. Restored by this revert: the single 7-day default with no exception by kind, and the explicit `GATE_PAUSE_TIMEOUT_MS` on all five templates, justified as an author choosing to wait longer than the default rather than as engine recognition. The child-agent carve-out is untouched: it is factually true and implemented engine-side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review findings, one of which had the docs telling authors to work around behavior the engine now provides. `expirePausedExecution` waives the pause deadline while a dispatched child agent is alive (`runner.service.ts`, `awaitsLiveChildAgent`). The docs still told authors to size `timeoutMs` to a child run's worst case, "otherwise the parent fails while the child is still working", which is no longer true and makes them write a timeout they do not need. Both JSDoc sites and the README now say a child pause needs none, and state the waiver's edges: it covers a dispatch still pending or waiting on the parent, and lapses when the child goes terminal or its run no longer exists. `PauseTimeoutError` was named four times as though it were importable. It is an engine-side class, not an export of this package, so the docs describe the failure instead of naming a symbol an author cannot reach. `approval-chain` claimed a slow approver "never loses the run". The one-year value is still a terminal deadline: an approval that outlives it is failed by the sweep like any other. The header, the constant comment and the README now say the year is picked so nobody realistic reaches it, not that it is unbounded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second review pass caught the three templates the first one missed.
`human-in-the-loop`, `proposal-generator` and `scheduled-compliance-audit`
still promised that a slow approver "never loses the run". The one-year value
is a terminal deadline like any other; the year is picked so nobody realistic
reaches it, not so that nobody can.
`PauseTimeoutError` was still named in `approval-chain` and `pr-review-bot`
after being removed from the SDK docs for the same reason: it is an engine
class, not something an author can import or catch from `@sapiom/agent`.
The candidate gate in `human-in-the-loop` read as though its deadline were the
fallback that moves on to the next candidate. It is not: expiry fails the run
rather than delivering `{ decision: "timeout" }` to `resolve`. The comment now
says the signal drives that branch and the deadline is only the backstop. The
flow itself is unchanged: wiring a real timeout producer is the template's own
design question, not this PR's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docs told authors that a pause on a dispatched child needs no `timeoutMs`. That holds for a child launched now and not for one launched with `at`. The waiver is found through the dispatch ledger, whose row is keyed on the child's execution id. A scheduled launch pauses the parent on the schedule's correlation instead, minted before any child exists, so the two never match and the waiver never fires for it. The parent's deadline is anchored to the scheduled time, which covers the wait until the child starts but not the child's own run: a scheduled child that opens a gate of its own can outlive it. Both JSDoc sites and the README now split the two cases and tell authors to pass an explicit `timeoutMs` on a scheduled child that can take more than a week once it begins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…opts out An independent review pass caught that the child-agent guidance had gone stale and inverted, in the one direction that hurts. The engine branch moved after these docs were written. It now reaches a child launched with `at` through the child's own parent linkage, precisely because no dispatch-ledger row exists for a delayed dispatch, and pushes the parent's deadline while that child is alive exactly as it does for an immediate launch. The docs said the opposite, that a scheduled child gets no waiver. Worse, the remedy they prescribed is the one action that disables it: a deadline the author set is taken literally and never extended, so passing `timeoutMs` on a child pause trades a deadline that follows the child for one that can expire while the child is still working. All three surfaces now state the rule once, for both kinds of launch, and name the opt-out. Four smaller defects from the earlier revert rounds, same review: - `approval-chain` shipped two sentences with a duplicated fragment, in the load-bearing explanation of why its gates carry a year. - `wait-for-webhook`'s `parseTimeoutMs` justified rejecting a bad value by a forever-park three lines after saying an absent value inherits the 7-day default. The justification is now the real one: a dropped cap leaves the run on the default while the operator believes their own window applies. Same in its README. - Six templates justified their offline bypass with "pausing here would suspend the run forever". Nothing suspends forever after this change; the reason to skip the pause is that nothing will ever fire the signal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rules A second pass against the current engine head found the rules themselves stated correctly and five surfaces lagging behind them. - The changeset now says what happens to runs already paused when the engine change deploys: they are held to the same 7 days from their last activity, and a new `timeoutMs` only reaches pauses recorded after a redeploy. Its child-agent clause now matches the README and JSDoc: it covers a child launched with `at`, and an explicit `timeoutMs` on a child pause turns the extension off. - The README still told authors to give a human gate a `timeoutMs` "so the failure is recorded rather than sat on", which only made sense while an omitted deadline meant waiting forever. It now says to size the value to how long a human may take, and the example uses a value that actually differs from the default. - `pr-review-bot` promised a wait "for as long as it takes". Its pause carries no `timeoutMs`, so it waits up to the 7-day default and then fails. - `human-in-the-loop`, `proposal-generator` and `scheduled-compliance-audit` showed their gate directive without the `timeoutMs` the code passes, in the AGENTS.md files a coding agent reads before editing the template. They now show it and say why, as `approval-chain` already did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
abde176 to
f399c15
Compare
Primary change type
Problem and motivation
pauseUntilSignalaccepts an optionaltimeoutMs, but nothing in the SDK saidwhat happens when you omit it. Authors reasonably read the omission as "wait as
long as it takes", so almost nobody passes a value. In production that turned
into 23 customer runs parked at
status='paused'withpaused_until IS NULL,some for over two months: a lost capability result or a dropped webhook left the
run waiting forever, with no error, no terminal event and no notification.
SAP-3207 fixes that in the engine by giving every pause a default deadline of
7 days (the capability resume-token TTL, so no dispatched result can arrive
after it) and having the existing sweep finalize anything past its deadline with
PauseTimeoutError. This PR is the SDK-facing half: the default has to bediscoverable at the call site, and authors who genuinely need a longer wait need
to know that
timeoutMsis how they ask for one.Summary and scope
Two halves: document the deadline on the SDK surface, then fix every place in the
repo that documented the opposite.
SDK docs. No type, signature, or runtime change to the package.
packages/agent/src/directives.ts: JSDoc onPauseUntilSignalDirective.timeoutMs(the wire interface), on
Pause<Resume>.timeoutMs(the branded constructortype), and a paragraph in the
pauseUntilSignaldoc block covering the default,the
PauseTimeoutErroroutcome, and when overriding is meaningful.packages/agent/README.md: a deadline bullet in the Pausing on a long-runningcapability section. It scopes the claim to the hosted engine, notes that
raising
timeoutMspast the default buys nothing on a capability pause (theresume token expires on the same horizon), and gives a human-gate example, which
is the case where a longer wait is actually meaningful.
.changeset/pause-deadline-docs.md: patch bump for@sapiom/agent, worded as abehavior notice for the hosted engine rather than a claim about this package.
Examples that asserted the opposite. Both would otherwise ship contradicting
the engine change.
examples/approval-chainstated in three places (index.tsheader and thepresentstep,README.md,AGENTS.md) that its gates deliberately carry notimeoutMs, precisely because a lapsed deadline terminates the run instead ofresuming it and would skip the graceful
escalatestep. OmittingtimeoutMsnolonger avoids that: the pause inherits the 7-day default and does the same damage
on a one-week horizon. The gates now pass an explicit one-year
GATE_PAUSE_TIMEOUT_MS, long enough that a slow approver never loses the run andfinite enough that an abandoned chain still reaches a terminal state. The
reminder and escalation cadence still comes from the
approval.decisionsignal,never from the deadline.
examples/wait-for-webhookadvertised an indefinite wait by default acrossREADME.md, theindex.tsheader, theparseTimeoutMsdoc,AGENTS.mdandtemplate.json. All now describe the 7-day fallback and keepCALLBACK_TIMEOUT_MSas the way to size the wait to a real callback window.Every other pause site is now an explicit decision. Six more sites used the
same construct, a plain signal pause with no
timeoutMs, and would have silentlyinherited a one-week ceiling. The human gates get the same one-year deadline as
approval-chain:human-in-the-loop(approval gate and candidate confirm),proposal-generator,scheduled-compliance-audit. The two machine waits keep thedefault and now say why on the page:
pr-review-bot(a PR event that has notarrived in a week is not coming) and
durable-backfill(a schedule that has notticked in a week has stopped, and failing loudly beats a backfill parked
mid-cursor). After this PR no
pauseUntilSignalcall inexamples/relies on anunstated deadline.
Out of scope, and deliberately untouched:
pauseExecution, the legacy-row sweepbackstop, the paused-age gauge) lives in
apps/workflows-enginein thesapiomrepo, tasks 1 to 5 of the SAP-3207 plan.manifest.spec.ts's "accepts null timeoutMs (engine default)" case: that isthe per-step execution timeout, a different field with a different default.
packages/agent-runtimecarries the same defect as the engine(
in-memory-store.ts:365writespausedUntil = nullwith notimeoutMs,runner-core.ts:415bails on!row.pausedUntil, and nothing in this repo callsexpirePausedExecution). It is a parallel implementation the engine does notdepend on, so
run_localnever expires a pause at all. Left for a separatedecision rather than folded in here; the docs added by this PR say so explicitly.
The default does not bound a child agent. The 7-day figure is the sandboxed
capability's resume-token TTL, which is verified only on the gateway's HTTP
callbacks. A dispatched child agent returns through the orchestration-resume path,
which verifies no token: it resolves the durable dispatch row and fires
agents.resultdirectly, so a child can report back long after seven days. Thefirst draft of these docs told authors that raising
timeoutMson a capabilitypause buys nothing, which would fail a parent while its child was still working,
and nothing cascades that failure to the child. All three surfaces now say what
actually bounds what. The engine-side consequence (an orphaned child whose result
is dropped against a no-longer-paused parent) is reported on SAP-3207.
Merge ordering. This documents behavior the engine gains in the companion
sapiomPR. Merging this first is harmless for anyone reading the docs to writenew code (passing an explicit
timeoutMsalready works today), but the "7-daydefault" sentence is only literally true once the engine change deploys. Land
this alongside or after it.
Related work
Related issue or discussion: https://linear.app/sapiom/issue/SAP-3207 (engine
half tracked in the
sapiomrepo; monitoring follow-up in SAP-3209). Lost-resumeproducers behind the stuck runs: SAP-2716, SAP-3165, SAP-2777.
Validation
Tests and documentation
Documentation is most of the change: three JSDoc sites, the
@sapiom/agentREADME section, and the two examples' prose.
The behavioral edits are the pause deadlines on five examples. No test added for
them:
run_localauto-resumes every pause immediately and never enforces adeadline, so a test could only assert each constant against itself rather than any
behavior, and
approval-chainships without a suite at all (pnpm examples:testreports it under "without a suite"). The repo's own gates were run instead:
examples:checkvalidates the manifests and authoring assets,examples:check:testand
examples:teststay green including the suites of all five touched examples,each touched example typechecks clean, and the
@sapiom/agentsuite is unchangedand green, which is the regression signal for the JSDoc edits.
Compatibility and release impact
@sapiom/agentpackage,which changes only comments. The behavior being documented (a 7-day default
deadline,
PauseTimeoutErroron expiry) is externally visible and ships from theengine side, where a pause that previously hung forever will now fail. Existing
code that already passes an explicit
timeoutMsis unaffected. Five exampleschange shape: their human gates go from no
timeoutMsto an explicit one year,which preserves the wait the engine default would otherwise cap at a week.
.changeset/pause-deadline-docs.md, patch for@sapiom/agent.Security
will follow the
Security Policy for
private reporting.
AI assistance
Claude Code (Opus 5) drafted the JSDoc and README wording from the SAP-3207 plan
and the production triage of the 23 stuck runs, then ran every command listed under
Validation. The second commit responds to this repo's automated review of the first,
which caught that
examples/approval-chaindocuments a design the engine defaultsilently breaks; that finding was verified against the three files before acting on
it. I reviewed the final diff line by line: comments, markdown, a changeset, and one
example constant.
Checklist
CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.any N/A checks above.
Summary by CodeRabbit