Skip to content

docs(agent): document the default pause deadline across the SDK and examples [SAP-3207] - #850

Open
antoine-berger wants to merge 12 commits into
mainfrom
antoineberger/sap-3207-engine-paused-runs-without-a-deadline-never-expire-23-stuck
Open

antoine-berger wants to merge 12 commits into
mainfrom
antoineberger/sap-3207-engine-paused-runs-without-a-deadline-never-expire-23-stuck

Conversation

@antoine-berger

@antoine-berger antoine-berger commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Primary change type

  • Bug fix
  • Documentation
  • Feature
  • Tests
  • Dependency update
  • Maintenance or refactor

Problem and motivation

pauseUntilSignal accepts an optional timeoutMs, but nothing in the SDK said
what 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' with paused_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 be
discoverable at the call site, and authors who genuinely need a longer wait need
to know that timeoutMs is 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 on PauseUntilSignalDirective.timeoutMs
    (the wire interface), on Pause<Resume>.timeoutMs (the branded constructor
    type), and a paragraph in the pauseUntilSignal doc block covering the default,
    the PauseTimeoutError outcome, and when overriding is meaningful.
  • packages/agent/README.md: a deadline bullet in the Pausing on a long-running
    capability
    section. It scopes the claim to the hosted engine, notes that
    raising timeoutMs past the default buys nothing on a capability pause (the
    resume 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 a
    behavior 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-chain stated in three places (index.ts header and the
    present step, README.md, AGENTS.md) that its gates deliberately carry no
    timeoutMs, precisely because a lapsed deadline terminates the run instead of
    resuming it and would skip the graceful escalate step. Omitting timeoutMs no
    longer 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 and
    finite enough that an abandoned chain still reaches a terminal state. The
    reminder and escalation cadence still comes from the approval.decision signal,
    never from the deadline.
  • examples/wait-for-webhook advertised an indefinite wait by default across
    README.md, the index.ts header, the parseTimeoutMs doc, AGENTS.md and
    template.json. All now describe the 7-day fallback and keep
    CALLBACK_TIMEOUT_MS as 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 silently
inherited 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 the
default and now say why on the page: 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). After this PR no pauseUntilSignal call in examples/ relies on an
unstated deadline.

Out of scope, and deliberately untouched:

  • The engine change itself (default in pauseExecution, the legacy-row sweep
    backstop, the paused-age gauge) lives in apps/workflows-engine in the
    sapiom repo, tasks 1 to 5 of the SAP-3207 plan.
  • manifest.spec.ts's "accepts null timeoutMs (engine default)" case: that is
    the per-step execution timeout, a different field with a different default.
  • packages/agent-runtime carries the same defect as the engine
    (in-memory-store.ts:365 writes pausedUntil = null with no timeoutMs,
    runner-core.ts:415 bails on !row.pausedUntil, and nothing in this repo calls
    expirePausedExecution). It is a parallel implementation the engine does not
    depend on, so run_local never expires a pause at all. Left for a separate
    decision 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.result directly, so a child can report back long after seven days. The
first draft of these docs told authors that raising timeoutMs on a capability
pause 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
sapiom PR. Merging this first is harmless for anyone reading the docs to write
new code (passing an explicit timeoutMs already works today), but the "7-day
default" 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 sapiom repo; monitoring follow-up in SAP-3209). Lost-resume
producers behind the stuck runs: SAP-2716, SAP-3165, SAP-2777.

Validation

pnpm --filter @sapiom/agent test: 10 suites / 175 tests passed
pnpm --filter @sapiom/agent typecheck: passed, no output
pnpm --filter @sapiom/agent lint: passed, no output
pnpm --filter @sapiom/agent build: cjs + esm emitted, no errors
pnpm examples:check: OK, 12 templates, 12 manifests schema-valid, 54 assets terminology-checked
pnpm examples:check:test: 165 tests passed, 0 failed
pnpm examples:test: 22 passed, 0 failed
tsc --noEmit per touched example: clean for human-in-the-loop, proposal-generator, scheduled-compliance-audit, pr-review-bot, durable-backfill
pnpm terminology:check: passed (512 files, no stale allowlist entries)
pnpm provider-copy:check: passed (129 audited files)
npx changeset status: @sapiom/agent listed for a patch bump

Known-red on CI, inherited from main, not from this diff: the `examples` job runs
pnpm terminology:check, which fails on main at 8495be29 with "Stale terminology
allowlist entries found: agent-map-pane-icon-identifier [workflow] ...
AgentMapPane.tsx (expected 2, matched 3)". PR #844 changed that file without
updating the allowlist. The same check passes locally against this branch's base.

Tests and documentation

Documentation is most of the change: three JSDoc sites, the @sapiom/agent
README section, and the two examples' prose.

The behavioral edits are the pause deadlines on five examples. No test added for
them: run_local auto-resumes every pause immediately and never enforces a
deadline, so a test could only assert each constant against itself rather than any
behavior, and approval-chain ships without a suite at all (pnpm examples:test
reports it under "without a suite"). The repo's own gates were run instead:
examples:check validates the manifests and authoring assets, examples:check:test
and examples:test stay green including the suites of all five touched examples,
each touched example typechecks clean, and the @sapiom/agent suite is unchanged
and green, which is the regression signal for the JSDoc edits.

Compatibility and release impact

  • Breaking or externally visible changes: None from the @sapiom/agent package,
    which changes only comments. The behavior being documented (a 7-day default
    deadline, PauseTimeoutError on expiry) is externally visible and ships from the
    engine side, where a pause that previously hung forever will now fail. Existing
    code that already passes an explicit timeoutMs is unaffected. Five examples
    change shape: their human gates go from no timeoutMs to an explicit one year,
    which preserves the wait the engine default would otherwise cap at a week.
  • Changeset: Added .changeset/pause-deadline-docs.md, patch for @sapiom/agent.

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. I
    will follow the
    Security Policy for
    private reporting.

AI assistance

  • I did not use AI assistance for this change.
  • I used AI assistance and have described it below.

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-chain documents a design the engine default
silently 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

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained
    any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

Summary by CodeRabbit

  • Documentation
    • Clarified that hosted pauses default to a 7-day deadline and expire as failed with a pause-timeout error.
    • Documented explicit deadlines for long-running human approval gates, including one-year examples.
    • Clarified that dispatched child-agent pauses are exempt while the child remains active, while scheduled child agents may require explicit deadlines.
    • Documented that local runs resume pauses immediately without enforcing deadlines.
    • Updated webhook, approval-chain, backfill, review-bot, proposal, scheduled-audit, and human-in-the-loop examples.

Devin Review

@sapiom sapiom deleted a comment from github-actions Bot Sep 6, 2026
@antoine-berger antoine-berger reopened this Sep 8, 2026
@antoine-berger antoine-berger changed the title docs(agent): document the engine's default pause deadline [SAP-3207] docs(agent): document the default pause deadline across the SDK and examples [SAP-3207] Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review — PR #850

1. The run_local sentence is wrong, and it ships in the changeset

Three surfaces state that run_local "records the deadline but never sweeps for it, so a
local run parks on a pause whatever timeoutMs says" (packages/agent/README.md:199-201,
packages/agent/src/directives.ts:83-85 and :261-262), and the changeset repeats it as
"the in-memory host records the deadline but never sweeps for it".

Both halves are inaccurate:

  • A local run never parks. runLocal loops on execution status and immediately resumes any
    paused row with the registered signal result or {}
    (packages/agent-core/src/local/run-local.ts:152-155). That is the documented behaviour
    of the examples themselves ("run_local auto-resumes each pause with no payload",
    examples/approval-chain/template.json:15). An author who reads the new README will
    expect a local run to sit at the gate and will file a bug when it walks straight through.
  • With timeoutMs omitted — the case the paragraph is about — the in-memory store writes
    pausedUntil = null (packages/agent-runtime/src/in-memory-store.ts:365). Nothing is
    recorded, and no 7-day default exists locally.

The changeset is the one surface that cannot be edited after publish. Suggested wording:
"run_local never applies or enforces a pause deadline — it auto-resumes every pause
immediately. Expiry only exists on the hosted engine."

2. Changeset asserts engine behaviour that has not shipped

.changeset/pause-deadline-docs.md says "the hosted engine now gives it a 7-day
deadline … a run that previously hung forever will surface as a failure". The PR body states
the opposite of that tense: "the '7-day default' sentence is only literally true once the
engine change deploys. Land this alongside or after it."

That ordering constraint is invisible to whoever runs the release. If @sapiom/agent
publishes before the engine deploy, the CHANGELOG in the tarball tells consumers their
pauses now fail at 7 days while they in fact still hang forever — permanently, since
changelog entries cannot be retracted. Either gate the merge/publish on the engine deploy
explicitly (not in prose the releaser will not read), or word it as a forthcoming change
with the engine version/date that carries it.

3. Five other examples still use the pattern this PR just declared broken

The PR fixes approval-chain because "omitting timeoutMs no longer avoids" a terminal
sweep. The same construct — a plain signal pause, no timeoutMs, waiting on a human or an
external scheduler — is untouched in:

  • examples/human-in-the-loop/index.ts:416 and :499 (approval gate and confirm gate)
  • examples/proposal-generator/index.ts:611
  • examples/scheduled-compliance-audit/index.ts:396
  • examples/pr-review-bot/index.ts:386
  • examples/durable-backfill/index.ts:517 (cron heartbeat — dies if the schedule stalls a
    week)

Under the documented default each of these hard-fails with PauseTimeoutError at day 7: a
two-week approval loses the run, which is exactly the damage the approval-chain edit
exists to prevent. If approval-chain needed an explicit deadline, these do too. Fix them
or state in the PR why a shipped template whose whole point is a human gate is safe with a
one-week ceiling — "docs only" is not an answer for a behaviour change that lands under
these examples.

4. Published docs pin an engine constant and a backend coupling with no citable guarantee

packages/agent/README.md:184-186 and directives.ts:78-80 do not just state the default,
they teach a rule: 7 days is "the capability resume-token TTL", therefore "raising it buys
nothing" on a capability pause. That is two backend invariants asserted in a published API
doc, neither verifiable from this repo, and no test in packages/agent fails if the engine
changes either one. The first is a number that will drift; the second will make authors of
long-running capability pauses (e.g. scene-to-video, research-to-microsite) not pass a
timeoutMs they may need. Scope it — "the hosted engine's current default is 7 days" —
and drop the "buys nothing" instruction unless a backend guarantee can be cited.

Nit

  • PauseTimeoutError is named across the README, both JSDoc blocks and the changeset but is
    not exported from @sapiom/agent (it lives in @sapiom/agent-runtime/src/errors.ts:72).
    Say where it comes from, or describe the observable outcome (a failed run whose error name
    is PauseTimeoutError) so consumers know what to match on.

Verdict

Request changes: the run_local claim is factually wrong on the one surface that cannot be
retracted after publish (1), the changeset's tense outruns the engine deploy (2), and the
example fix stops one template short of the five that carry the same defect (3). No
confidentiality issues in the diff.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Follow-up review — PR #850

Delta reviewed: d7677d5f..5a7644f0 (one commit, three files: changeset, packages/agent/README.md, packages/agent/src/directives.ts).

Fixed

Finding 1 (the run_local claim) is resolved and now accurate: the new wording matches run-local.ts:152-155, and "the registered capability result or an empty payload" is right — the map is filled by the stub client (local/dispatcher.ts:157).

Earlier findings the push did NOT fix

  • Finding 3 — still open. human-in-the-loop/index.ts:416,499, proposal-generator/index.ts:611, scheduled-compliance-audit/index.ts:396, pr-review-bot/index.ts:386, durable-backfill/index.ts:517 still pause with no timeoutMs. Under the documented default each hard-fails at day 7 — the exact damage the approval-chain edit exists to prevent. Fix them or state why a shipped human-gate template is safe with a one-week ceiling.
  • Finding 4 — still open. packages/agent/README.md:180-186 and directives.ts:77-80,253-258 still assert two uncitable backend invariants: that 7 days is the capability resume-token TTL, and that "raising it buys nothing" on a capability pause. Nothing in this repo fails if the engine changes either. Scope to "the hosted engine's current default" and drop the instruction.
  • Nit — still open. PauseTimeoutError is named on all three surfaces but is not exported from @sapiom/agent (it is in agent-runtime/src/errors.ts:72). Consumers have nothing to match on.

New finding

  • Finding 2 fixed on the changeset only. The changeset now reads "Forthcoming behavior change … once the engine change for SAP-3207 is deployed", but README.md:181 and directives.ts:77 / :253 still say the engine applies a 7-day default in the present tense. Those ship in the same npm tarball as the changelog and, by the PR body's own admission, are not yet true. Align the tense, or gate publish on the engine deploy so both are true at once.

No confidentiality issues in the delta; SAP-3207 in the changeset matches existing published CHANGELOG.md precedent.

Verdict: request changes — two substantive earlier findings (3, 4) untouched, and the tense fix stopped at the changeset.

@gwitwer

gwitwer commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Holding off on approval for now, for two reasons that both trace to SAP-3207 moving after this branch's last push:

  1. The head is behind your own decision. Your 09-09 comment on the ticket records the revised rule (7 days for machine waits, 365 days for approval gates via isApprovalGateSchema) and says every "7-day default" surface here has to change, with the rewrite sitting locally as 213b693a pending the gate-predicate decision. Approving 5fe97d79 would stamp a version you've already superseded. Ping me when the rewrite is pushed.
  2. The child-agent guidance now contradicts the engine. The README bullet and the pauseUntilSignal JSDoc tell authors to size timeoutMs to a child run's worst case, "otherwise the parent fails while the child is still working". #4919 commit 22fbfa25 (pushed after this PR's last commit) waives the deadline while a dispatched child agent is non-terminal, so an author following these docs adds a timeout they don't need. Suggest dropping that instruction in the rewrite.

Smaller: PauseTimeoutError is referenced by name but not exported from @sapiom/agent (the open bot nit). Also, the cancelled CI check was a duplicate "Classify pull request" job superseded by a passing rerun, so no action needed there.

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.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: c87627d4-6e7a-4bb8-9ddb-9cd8441b2ea9

📥 Commits

Reviewing files that changed from the base of the PR and between 20e904c and f0cfa65.

📒 Files selected for processing (2)
  • examples/approval-chain/AGENTS.md
  • examples/wait-for-webhook/AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/wait-for-webhook/AGENTS.md

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The pull request updates pause-deadline documentation and approval examples. It documents the hosted seven-day default, terminal timeout behavior, child-agent waivers, and unchanged run_local behavior. It also adds explicit one-year deadlines to human approval pauses.

Changes: Pause Deadline Documentation

Layer / File(s) Summary
Public pause deadline contract
packages/agent/src/directives.ts, packages/agent/README.md, .changeset/pause-deadline-docs.md
Documents the hosted seven-day default, timeout state, dispatched child-agent waiver, scheduled-child behavior, and run_local behavior.
Human approval gate deadlines
examples/approval-chain/*, examples/human-in-the-loop/index.ts, examples/proposal-generator/index.ts, examples/scheduled-compliance-audit/index.ts
Adds explicit one-year deadlines to approval pauses and documents terminal timeout behavior and signal-driven reminders.
Webhook timeout guidance
examples/wait-for-webhook/*
Documents the seven-day default, explicit callback deadlines, timeout failures, and invalid timeout handling.
Intentional default timeout examples
examples/durable-backfill/index.ts, examples/pr-review-bot/index.ts
Documents intentional use of the seven-day default for stalled heartbeat and webhook waits.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Other

Merge Risk: ⚪ Minimal · up to f0cfa

The documentation and examples consistently describe the hosted pause deadline and signal-driven candidate advancement, with no remaining merge-blocking risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
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.

❤️ Share

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.

coderabbitai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Both fixed in 1f124433.

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 timeoutMs, no distinction by kind. You were reviewing on the opposite assumption, so tell me if that reasoning holds for you or if you would rather see the detection widened than the rule dropped.

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.

PauseTimeoutError is gone from the docs: engine-side, not exported here.

coderabbitai[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@antoine-berger
antoine-berger force-pushed the antoineberger/sap-3207-engine-paused-runs-without-a-deadline-never-expire-23-stuck branch from c87b86f to 40023ef Compare September 29, 2026 21:57

@gwitwer gwitwer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

antoine-berger and others added 12 commits October 20, 2026 20:00
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>
@antoine-berger
antoine-berger force-pushed the antoineberger/sap-3207-engine-paused-runs-without-a-deadline-never-expire-23-stuck branch from abde176 to f399c15 Compare October 21, 2026 01:00
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