@arnilo/prism 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +15 -2
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-event-source.d.ts +9 -1
  6. package/dist/agent-event-source.js +10 -3
  7. package/dist/agent-loops.js +7 -4
  8. package/dist/agent-run-lifecycle.d.ts +15 -1
  9. package/dist/agent-run-lifecycle.js +91 -10
  10. package/dist/agent-run-state.d.ts +34 -2
  11. package/dist/agent-run-state.js +68 -6
  12. package/dist/agent-session/helpers.js +20 -1
  13. package/dist/agent-session/session/assemble.js +250 -27
  14. package/dist/agent-session/session/persist.d.ts +27 -0
  15. package/dist/agent-session/session/persist.js +94 -12
  16. package/dist/agent-session/session/provider-round.d.ts +14 -4
  17. package/dist/agent-session/session/provider-round.js +197 -25
  18. package/dist/agent-session/session/tool-round.js +24 -2
  19. package/dist/agent-session/session/types.d.ts +36 -2
  20. package/dist/agent-session/session.d.ts +40 -4
  21. package/dist/agent-session/session.js +78 -5
  22. package/dist/attention-compiler.d.ts +51 -2
  23. package/dist/attention-compiler.js +282 -21
  24. package/dist/cache-helpers.d.ts +4 -2
  25. package/dist/cache-helpers.js +8 -6
  26. package/dist/checkpoint-restore.d.ts +45 -0
  27. package/dist/checkpoint-restore.js +54 -0
  28. package/dist/checkpoints.js +7 -11
  29. package/dist/context-budget.d.ts +2 -1
  30. package/dist/context-budget.js +24 -2
  31. package/dist/contracts-core/agent.d.ts +30 -0
  32. package/dist/contracts-core/attention.d.ts +95 -0
  33. package/dist/contracts-core/content.d.ts +15 -0
  34. package/dist/contracts-core/guardrail-packs.d.ts +41 -0
  35. package/dist/contracts-core/guardrail-packs.js +2 -0
  36. package/dist/contracts-core/loop.d.ts +42 -0
  37. package/dist/contracts-core/provider.d.ts +25 -0
  38. package/dist/contracts-core/run-limits.d.ts +21 -0
  39. package/dist/contracts-core/session.d.ts +23 -5
  40. package/dist/contracts-core/session.js +21 -2
  41. package/dist/contracts-core/usage.d.ts +40 -0
  42. package/dist/contracts-core/usage.js +8 -0
  43. package/dist/contracts-core.d.ts +2 -0
  44. package/dist/contracts-core.js +2 -0
  45. package/dist/contracts-protocol.d.ts +90 -4
  46. package/dist/contracts-run-state.d.ts +82 -6
  47. package/dist/evidence-grounding.d.ts +29 -0
  48. package/dist/evidence-grounding.js +162 -0
  49. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  50. package/dist/guardrail-packs/coding-standard.js +63 -0
  51. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  52. package/dist/guardrail-packs/destructive-commands.js +46 -0
  53. package/dist/guardrail-packs/errors.d.ts +7 -0
  54. package/dist/guardrail-packs/errors.js +9 -0
  55. package/dist/guardrail-packs/index.d.ts +4 -0
  56. package/dist/guardrail-packs/index.js +15 -0
  57. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  58. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  59. package/dist/guardrail-packs/types.d.ts +16 -0
  60. package/dist/guardrail-packs/types.js +2 -0
  61. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  62. package/dist/guardrail-packs/validation-respect.js +53 -0
  63. package/dist/guardrails.d.ts +20 -1
  64. package/dist/guardrails.js +268 -0
  65. package/dist/host-composition.d.ts +13 -0
  66. package/dist/host-composition.js +33 -2
  67. package/dist/index.d.ts +19 -10
  68. package/dist/index.js +11 -6
  69. package/dist/input.d.ts +8 -1
  70. package/dist/input.js +68 -6
  71. package/dist/middleware.d.ts +37 -2
  72. package/dist/middleware.js +41 -0
  73. package/dist/node/session-store-jsonl.js +18 -3
  74. package/dist/observability.js +6 -0
  75. package/dist/provider-events.d.ts +11 -3
  76. package/dist/provider-events.js +62 -4
  77. package/dist/providers/openai-compatible.js +6 -3
  78. package/dist/providers/transport.d.ts +3 -1
  79. package/dist/providers/transport.js +36 -0
  80. package/dist/redaction.js +18 -2
  81. package/dist/run-bundle.d.ts +89 -0
  82. package/dist/run-bundle.js +150 -0
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +46 -0
  85. package/dist/session-stores.d.ts +12 -1
  86. package/dist/session-stores.js +21 -4
  87. package/dist/testing/agent-event-source-conformance.js +41 -2
  88. package/dist/testing/prefix-stability-conformance.d.ts +30 -0
  89. package/dist/testing/prefix-stability-conformance.js +104 -0
  90. package/dist/testing/session-store-conformance.d.ts +3 -2
  91. package/dist/testing/session-store-conformance.js +48 -0
  92. package/dist/testing/state-concurrency-conformance.js +5 -12
  93. package/dist/tools.d.ts +5 -0
  94. package/dist/tools.js +11 -3
  95. package/dist/usage-estimation.d.ts +29 -0
  96. package/dist/usage-estimation.js +79 -0
  97. package/docs/ag-ui.md +5 -0
  98. package/docs/agent-events.md +68 -1
  99. package/docs/agent-loops.md +33 -0
  100. package/docs/agent-session-runtime.md +5 -3
  101. package/docs/attention-compiler.md +89 -8
  102. package/docs/coding-agent-tools.md +1 -1
  103. package/docs/coding-security.md +1 -0
  104. package/docs/coding-tools.md +0 -1
  105. package/docs/compaction-and-retry.md +1 -1
  106. package/docs/compaction-observational-memory.md +34 -7
  107. package/docs/connected-apps.md +116 -0
  108. package/docs/context-and-skills.md +13 -0
  109. package/docs/core.md +1 -1
  110. package/docs/diagrams.md +6 -6
  111. package/docs/document-reader.md +9 -9
  112. package/docs/documents.md +32 -11
  113. package/docs/durable-runs.md +129 -0
  114. package/docs/embeddings.md +5 -0
  115. package/docs/enterprise-postgres-state.md +4 -0
  116. package/docs/evaluations.md +5 -0
  117. package/docs/execution-timeline.md +84 -1
  118. package/docs/guardrails.md +71 -2
  119. package/docs/history/079-messaging-primitive-review.md +391 -0
  120. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  121. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  122. package/docs/history/083-prism-work-primitive-review.md +84 -0
  123. package/docs/history/084-primitive-review.md +96 -0
  124. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  125. package/docs/history/README.md +5 -0
  126. package/docs/history/release-handoffs.md +38 -0
  127. package/docs/host-compositions.md +8 -6
  128. package/docs/host-security.md +2 -2
  129. package/docs/index.md +66 -29
  130. package/docs/input-and-prompt-assembly.md +3 -3
  131. package/docs/knowledge-sync.md +4 -0
  132. package/docs/live-testing.md +5 -3
  133. package/docs/mcp-tools.md +1 -0
  134. package/docs/messaging-channel-operations.md +166 -0
  135. package/docs/messaging-channels.md +150 -0
  136. package/docs/middleware-hooks.md +38 -2
  137. package/docs/migrate-to-0.8.md +124 -0
  138. package/docs/migrate-to-0.9.md +210 -0
  139. package/docs/migration.md +43 -0
  140. package/docs/model-registry.md +12 -2
  141. package/docs/multi-agent-patterns.md +25 -2
  142. package/docs/node-jsonl-session-store.md +7 -1
  143. package/docs/observability.md +7 -3
  144. package/docs/openapi-tools.md +1 -1
  145. package/docs/operations.md +1 -3
  146. package/docs/options-index.md +36 -3
  147. package/docs/peer-dependencies.md +6 -6
  148. package/docs/policy-and-audit.md +13 -1
  149. package/docs/postgres-persistence.md +1 -1
  150. package/docs/prefix-stability-conformance.md +93 -0
  151. package/docs/provider-caching.md +4 -4
  152. package/docs/provider-conformance.md +16 -0
  153. package/docs/provider-layer.md +2 -2
  154. package/docs/provider-packages.md +20 -20
  155. package/docs/providers/neuralwatt.md +5 -1
  156. package/docs/public-contracts.md +2 -2
  157. package/docs/rag.md +102 -4
  158. package/docs/release-and-install.md +55 -47
  159. package/docs/run-bundle.md +92 -0
  160. package/docs/runs-and-usage.md +57 -6
  161. package/docs/scoped-agent-memory.md +262 -0
  162. package/docs/server.md +2 -0
  163. package/docs/session-store-conformance.md +1 -2
  164. package/docs/session-stores.md +17 -17
  165. package/docs/sheets.md +9 -9
  166. package/docs/signal-channel.md +112 -0
  167. package/docs/speech.md +5 -1
  168. package/docs/sqlite-persistence.md +1 -1
  169. package/docs/supervisors.md +32 -12
  170. package/docs/telegram-channel.md +157 -0
  171. package/docs/testing.md +2 -2
  172. package/docs/tools.md +17 -0
  173. package/docs/wiki.md +1 -1
  174. package/docs/work-artifacts-and-review.md +1 -1
  175. package/docs/work-connectors.md +9 -9
  176. package/docs/work-sandbox.md +115 -0
  177. package/docs/work-tools.md +38 -16
  178. package/docs/workflows.md +5 -0
  179. package/package.json +9 -3
  180. package/templates/business-worker/manifest.json +2 -1
  181. package/templates/business-worker/src/agent.ts.tmpl +1 -1
  182. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,76 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.9.0] - 2026-09-20 (attention budget axes, turn traces, cache-stable disclosure, tool narrowing, guardrail packs, background agents, session search, deterministic turns, shared scopes)
4
+
5
+ > **Eleven publishable packages.** All 086–098 features ship behind their documented options, joined by the live-stream terminal fix (plan 100). Predecessor published line is **0.8.0**. Registry/tag writes stay operator-authorized.
6
+
7
+ ### Added
8
+ - **Attention budget axes and durable folding.** `attentionCompiler.trigger` accepts `input_ratio` (the legacy axis), `run_input_ratio` (fires against `RunLimits.maxInputTokens` instead of the model window), `token_floor`, and a host predicate, alone or as an any-of array; `attentionCompiler.durable: true` writes the fold ledger and sticky frontier into the checkpoint so a resumed run keeps its stubs. See [docs/attention-compiler.md](docs/attention-compiler.md).
9
+ - **Turn traces and exhaustion attribution.** `provider_turn_finished` carries a closed `stopReason` taxonomy, a `budgets` snapshot, the effective tool menu (`tools.count` / `tools.idsHash`), and provider-reported cache counts with a per-turn `hitRate`; `agent_finished` carries `finishReason` / `stopDetail`, `AgentRunResult.stopReason` names host-policy and loop-ceiling stops, and the execution timeline adds `turns[i].stopReason` plus `timeline.exhaustion`. See [docs/agent-events.md](docs/agent-events.md), [docs/execution-timeline.md](docs/execution-timeline.md).
10
+ - **Cache-stable progressive disclosure and cache metrics.** Skill bodies, deferred tool schemas, and loaded references land at cache-stable positions (append-only tail or explicit invalidation); cache read/write tokens and per-turn hit rate are recorded on usage records and turn events. See [docs/provider-caching.md](docs/provider-caching.md), [docs/prefix-stability-conformance.md](docs/prefix-stability-conformance.md).
11
+ - **Per-turn tool narrowing.** `AgentConfig.toolNarrowing` / `RunOptions.toolNarrowing` returns the effective tool subset before each provider turn; out-of-grant names are dropped and reported on `tool_narrowing_clamped`. See [docs/tools.md](docs/tools.md).
12
+ - **Usage estimation and the context meter.** `estimateMessageTokens` / `MODEL_FAMILY_TOKENS` / `resolveModelFamily` produce labeled `TokenEstimate`s, `AgentConfig.usageEstimation: "fallback"` (default) charges one labeled estimate for usage-less providers, and `session.contextMeter()` exposes `{ inputTokens, source, inputCap?, runInputBudget?, usedRatio? }`. See [docs/runs-and-usage.md](docs/runs-and-usage.md).
13
+ - **Execution guardrail packs.** `guardrailPacks` compiles four built-in restrictive rule sets (`coding-standard`, `destructive-commands`, `validation-respect`, `secrets-hygiene`) onto existing tool stages, each with a trajectory scorer (`createGuardrailPackScorer`). See [docs/guardrails.md](docs/guardrails.md).
14
+ - **Background child agents and child-event passthrough.** `lifetime: "session"`, `report: "on-complete" | "milestones" | "stream"`, `milestone`, and `budgetShare` on `delegate` / `delegateAsync` / `spawn_agent`, with `child_milestone`, `child_failed`, and rate-coalesced child events. See [docs/supervisors.md](docs/supervisors.md), [docs/multi-agent-patterns.md](docs/multi-agent-patterns.md).
15
+ - **Checkpoint sidecar metadata and restore hooks.** A redacted ≤4 KB `checkpointMetadata` map rides every checkpoint record without charging `maxStateBytes`, and `restoreHooks` revert external layers before a resume claims the run. See [docs/durable-runs.md](docs/durable-runs.md).
16
+ - **Bounded workspace session search.** `SessionStore.searchSessions?(query)` over workspace/time/provider/label/kind/ownership filters with an indexed full-text path (SQLite FTS5, Postgres `tsvector`, migration 004) and a shared linear matcher for memory and JSONL stores. See [docs/session-stores.md](docs/session-stores.md).
17
+ - **Deterministic no-model turns.** The `beforeProviderTurn` middleware hook answers a turn from host data (`BeforeProviderTurnPayload` → `DeterministicTurnAnswer`) with no provider request, recorded as `deterministic` on the timeline and graded by `createDeterministicTurnScorer`. See [docs/middleware-hooks.md](docs/middleware-hooks.md).
18
+ - **Shared work scopes for observational memory.** `sharedScopes` at attach time plus owner-branch grants (`grant` / `revoke`) let granted sessions contribute to and read one scope, with deny-by-default rechecks, `onScopeAccess` auditing, and revocation at the next read. See [docs/compaction-observational-memory.md](docs/compaction-observational-memory.md).
19
+ - **Retrieval revocation and a zero-service default reranker.** `createDeletionPropagator` (+ `collectInvalidationIds` / `listInvalidatedIds`, `repointSource`, `retireWikiSources`, RAG/wiki handlers) propagates deletion and revocation through derived artifacts, `createAccessRecheck` rechecks governed sources per query, and `resolveReranker({ kind: "local" })` / `createLocalReranker()` run an in-process cross-encoder behind the `LocalRerankRuntime` seam with no declared inference dependency. See [docs/rag.md](docs/rag.md), [docs/embeddings.md](docs/embeddings.md).
20
+ - **Examples.** `examples/attention-budget-axes.ts`, `examples/tool-narrowing-planes.ts`, `examples/session-search.ts`.
21
+
22
+ ### Changed
23
+ - **Lockstep `0.8.0` → `0.9.0`.** All eleven publishable manifests move together with `^0.9.0` internal ranges; the lockfile, the `src/index.ts` version constant, the docs index banner, the release-workflow tag lists, and the generated package-truth artifact agree (enforced by `scripts/version-literal-gate.test.mjs`).
24
+ - **A limit death no longer ends a stream early.** The terminal set is exactly `agent_finished` / `agent_denied` / `error`, decided by one exported `isTerminalAgentEventType` predicate shared by the memory, NATS, and Postgres sources, AG-UI replay, the A2A stream break, AG-UI `filterRun`, and conversation replay; `run_limit_exceeded` → `budget_exhausted` → `error` now reaches page, subscribe, and replay consumers in order. See [docs/migrate-to-0.9.md](docs/migrate-to-0.9.md).
25
+ - **Usage-less providers are charged a labeled estimate by default.** `AgentConfig.usageEstimation: "fallback"` records one `TokenEstimate` (`estimated: true` + `confidence`) at the existing usage seam; reported usage always wins, and `"off"` keeps the previous zero-for-no-usage behavior.
26
+ - **Compat baselines regenerated at 0.9.0**: `+119` public names with **zero removals** and zero renames (`@arnilo/prism` +49, `@arnilo/prism-memory` +60, `@arnilo/prism-core` +10). One type-level consumer change: the `recordUsage` callback of `generateProviderTurn` / `generateWithRetry` returns `Promise<Usage | undefined>` instead of `Promise<void>`.
27
+ - **Budgets rebaselined with recorded reasons**: root packed/unpacked/file count, per-package export ceilings, and the non-null assertion ratchet carry the measured 0.9.0 values and the plans that moved them.
28
+ - **Migration guide for 0.8.0 hosts**: [docs/migrate-to-0.9.md](docs/migrate-to-0.9.md) — four behavior changes inside existing surfaces, every new option with defaults and sizing lines, upgrade steps, and rollback — indexed from [docs/migration.md](docs/migration.md) and [docs/index.md](docs/index.md).
29
+
30
+ ### Fixed
31
+ - **Streams ended one record early on a run-limit death.** In-memory, NATS, Postgres, and AG-UI replay consumers treated `run_limit_exceeded` as terminal, so a subscriber that stopped at the first breach record never saw the `budget_exhausted` attribution or the terminal `error` (`assertAgentEventSourceConforms` now asserts the delivery order against every source implementation).
32
+ - **Attention folding was inert when the run input cap sat below the model window.** The new `run_input_ratio` axis evaluates cumulative run spend plus the assembled request, so a 500k run cap under a 1M-window model folds instead of dying at the cap.
33
+ - **Unindexed session search could scan an unbounded store.** Memory and JSONL stores share `searchLinearSessions` with contract linear caps, overridable only within their hard bounds.
34
+
35
+ ### Security
36
+ - **Guardrail packs fail closed on secrets and destructive commands.** `secrets-hygiene` scans tool arguments for credential shapes, `destructive-commands` matches shell commands, and `coding-standard` keeps edits inside `options.roots`; every pack is restrictive-only, so a pack can never widen a host policy. Cap: 8 packs, 64 rules per pack.
37
+ - **Shared-scope access is deny-by-default and audited.** Absent, unknown, revoked, unreachable, or not-opened-locally scope state denies the read, every decision reaches `onScopeAccess`, revocation lands on the next read, and the host `entries(sessionId)` callback stays inside one `OwnershipScope`.
38
+ - **Checkpoint metadata and restore hooks are redacted and bounded.** The sidecar map is redacted unconditionally (no public-key exemption) and capped at 4 KiB; a failing or timed-out restore hook aborts the restore rather than claiming the run.
39
+ - **Estimates can never masquerade as provider truth.** Every estimated token count carries `estimated: true` (and `confidence`), reported usage is never overwritten, and estimates never produce a price — a configured `maxCost` stays the fail-closed envelope.
40
+
41
+ ## [0.8.0] - 2026-09-18 (messaging channels, connected apps, work family, durable runs, honesty gates)
42
+
43
+ > **Eleven publishable packages.** `@arnilo/prism-channels` is new; `@arnilo/prism-work` replaces `@arnilo/prism-office`. Predecessor published line is **0.7.0**. Registry/tag writes stay operator-authorized.
44
+
45
+ ### Added
46
+ - **Messaging channels.** `@arnilo/prism-channels` ships the transport-neutral runtime (deny-by-default sender authorization, owned session binding, serialized turns, current-run replies, one-use durable approvals, bounded attachment refs), official Telegram (private DMs, opt-in granted groups/topics, private-chat drafts, bounded media, optional voice transcription/synthesis, opt-in notices to one already-bound pair), and experimental pinned signal-cli Signal. See [docs/messaging-channels.md](docs/messaging-channels.md), [docs/telegram-channel.md](docs/telegram-channel.md), [docs/signal-channel.md](docs/signal-channel.md), [docs/messaging-channel-operations.md](docs/messaging-channel-operations.md).
47
+ - **Connected apps and work HTTP.** Identity-bound MCP connected-app sessions admit host-selected transports and register prefixed tools. Google Workspace and Microsoft 365 HTTP adapters live under `@arnilo/prism-work/connectors`. Slack MCP wrap and Open Connector sidecar stay examples. See [docs/connected-apps.md](docs/connected-apps.md), [docs/work-connectors.md](docs/work-connectors.md).
48
+ - **Durable runs, turn-boundary stops, and run-bundle snapshots.** `AgentRunStateOptions.checkpointPolicy: "every-turn"` checkpoints a run at the provider-turn boundary so a crashed worker resumes with the host-only `decision: "continue"` action (never reachable from AG-UI or the server boundary, and rejected while any approval or ready tool call is pending). `RunOptions.turnPolicy` (`TurnPolicyOptions`) stops a run synchronously at a turn boundary and reports `stopReason: "host_policy"` with a redacted, bounded `stopDetail` on the result, the ledger row, the `agent_finished` event, and the execution timeline; a host-policy stop stays resumable. `snapshotRunBundle` returns a `RunBundleSnapshot` — a frozen, redacted digest projection of the effective run bundle with zero store or network reads. `createClaimGroundingGuardrail` (stage `"output"`) blocks or flags numeric claims that no tool result or host evidence supports. `ErrorInfo.failureClass` (`ProviderFailureClass`) types provider failures as `quota` / `rate_limited` / `auth` / `transient` / `permanent` / `unknown`; `ModelCapabilities.toolCallStrictness` adds advisory tool-call reliability. See [docs/durable-runs.md](docs/durable-runs.md), [docs/run-bundle.md](docs/run-bundle.md), [docs/guardrails.md](docs/guardrails.md).
49
+ - **Work sandbox and vendored skills.** `@arnilo/prism-work/sandbox` plus `createWorkComposition` run office/exec in an injected Docker sandbox; connectors stay on the host. The package ships `docx`, `xlsx`, `powerpoint`, `pdf` skills.
50
+
51
+ ### Changed
52
+ - **Lockstep `0.7.0` → `0.8.0`.** All eleven publishable manifests move together with `^0.8.0` internal ranges; the lockfile, the `src/index.ts` version constant, the docs index banner, the release-workflow tag lists, and the generated package-truth artifact agree (enforced by `scripts/version-literal-gate.test.mjs`).
53
+ - **Work family rename.** `@arnilo/prism-office` is replaced by `@arnilo/prism-work` (connectors, documents, sheets, diagrams, document-reader, sandbox, skills, tools). No pre-1.0 shim. See [docs/migrate-to-0.8.md](docs/migrate-to-0.8.md).
54
+ - **AG-UI input authority is opt-in server-side.** `CreateAgUiHandlerOptions.inputPolicy.clientState: "ignore"` validates then discards client-supplied AG-UI state and tools before projection, and stops advertising client-provided tools; the default `"honor"` path is byte-identical to 0.7.0.
55
+ - **Observational-memory workers are tool-only.** Text/thinking/done-only turns are successful no-ops. Limit and unknown-tool failures throw `MemoryError` / `MemoryLimitError` rather than matching an English message prefix.
56
+ - **Channel lease release is fail-closed.** In-memory `route.lease` clears only after the store acknowledges; a failed release retries on idle/`stop`. TTL remains the cross-process backstop.
57
+ - **This-tree Postgres evidence.** `release:gate` reports `test:postgres` as pass only when `scripts/postgres-evidence.json` matches current `git rev-parse HEAD`. A stale phase baseline is blocked.
58
+ - **Compat baselines regenerated** at 0.8.0. Inherited 083 `@arnilo/prism-office` → `@arnilo/prism-work` removals (and `prism-core` / `prism-coding-tools` moves) are listed separately from 085 additions. This cut's own Tasks 1–6 add no public removals.
59
+ - **Migration guide for 0.7.0 hosts**: [docs/migrate-to-0.8.md](docs/migrate-to-0.8.md), indexed from [docs/migration.md](docs/migration.md) and [docs/index.md](docs/index.md).
60
+
61
+ ### Fixed
62
+ - **Portable work-idempotency error codes survived the work-family move.** The enterprise PostgreSQL `IdempotencyStore` adapter keeps `ERR_PRISM_WORK_IDEMPOTENCY` / `ERR_PRISM_WORK_IDEMPOTENCY_CONFLICT`; only the error class changed (`EnterprisePostgresError`).
63
+ - **Stale ownership assertions in the protected PostgreSQL leg:** a foreign checkpoint scope is a miss plus a generic CAS conflict, and a foreign agent-run status read is indistinguishable from a missing run (`ERR_PRISM_AGENT_RUN_STATE`).
64
+ - **Wiki isolation nested-runner flake.** The wiki scratch gate spawns `node --test --test-isolation=none` (still strips `NODE_TEST_CONTEXT` / `NODE_TEST_WORKER_ID`) so worker IPC deserialization cannot fail the gate under `npm test` load.
65
+ - **Coverage artifact names.** Package keys in `scripts/coverage-summary.json` must equal live workspace manifests (`@arnilo/prism-work`, not `@arnilo/prism-office`).
66
+ - **Alibaba video `fetchUrl`.** Declared `fetchUrl` now downloads generated video bytes; unused OpenAI speech `_bearerHeaders` deleted.
67
+
68
+ ### Security
69
+ - **Numeric claims must be grounded or they fail closed.** The claim-grounding guardrail blocks by default, bounds every reported span, caps evidence collection (4,096 figures, 16 levels, 128 KiB), and treats a missing evidence set as ungrounded rather than passing silently.
70
+ - **Crash recovery cannot bypass approval gates.** `"continue"` resumes only a running checkpoint with no pending decision or ready call, and keeps the recorded fingerprint, revision, ownership/fencing, and CAS-version gates.
71
+ - **Failed channel lease release is not success.** This process does not treat the binding as free until the store acknowledges; TTL is the other-process backstop.
72
+ - **Postgres release pass cannot be inherited.** Missing or stale this-tree evidence is blocked, never a pass from a previous commit's counts.
73
+
1
74
  ## [0.7.0] - 2026-09-15 (extended line: plans 072, 073, 074, 075, 077, 078)
2
75
 
3
76
  > **Channels are not in this cut.** Plan 079 (Telegram/Signal adapters) was reassigned to **0.8.0** so the 0.7.0 cut stops waiting on it; nothing in this release mentions or ships a channel adapter.
package/README.md CHANGED
@@ -158,20 +158,21 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
158
158
  ## Packages
159
159
 
160
160
  <!-- generated:package-truth:inventory begin -->
161
- **10 publishable manifests** — root `@arnilo/prism` plus 9 workspace packages (3 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
161
+ **11 publishable manifests** — root `@arnilo/prism` plus 10 workspace packages (4 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
162
162
 
163
163
  | package | version | notes |
164
164
  | --- | --- | --- |
165
- | `@arnilo/prism` | 0.7.0 | core — runtime, CLI/RPC, templates, docs |
166
- | `@arnilo/prism-coding-tools` | 0.7.0 | family — /agent, /security, /document-reader, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
167
- | `@arnilo/prism-core` | 0.7.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /work, /validation subpaths |
168
- | `@arnilo/prism-providers` | 0.7.0 | family — all provider adapters as `/<adapter>` subpaths |
169
- | `@arnilo/prism-acp-agent` | 0.7.0 | capability — ACP adapter |
170
- | `@arnilo/prism-ag-ui` | 0.7.0 | capability — AG-UI/A2A/A2UI adapter |
171
- | `@arnilo/prism-mcp` | 0.7.0 | capability — MCP client/server/OAuth interop |
172
- | `@arnilo/prism-memory` | 0.7.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
173
- | `@arnilo/prism-office` | 0.7.0 | capability — /documents, /sheets, /diagrams subpaths |
174
- | `@arnilo/prism-web-tools` | 0.7.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
165
+ | `@arnilo/prism` | 0.9.0 | core — runtime, CLI/RPC, templates, docs |
166
+ | `@arnilo/prism-channels` | 0.9.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
167
+ | `@arnilo/prism-coding-tools` | 0.9.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
168
+ | `@arnilo/prism-core` | 0.9.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
169
+ | `@arnilo/prism-providers` | 0.9.0 | family — all provider adapters as `/<adapter>` subpaths |
170
+ | `@arnilo/prism-acp-agent` | 0.9.0 | capability — ACP adapter |
171
+ | `@arnilo/prism-ag-ui` | 0.9.0 | capability — AG-UI/A2A/A2UI adapter |
172
+ | `@arnilo/prism-mcp` | 0.9.0 | capability — MCP client/server/OAuth interop |
173
+ | `@arnilo/prism-memory` | 0.9.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
174
+ | `@arnilo/prism-web-tools` | 0.9.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
175
+ | `@arnilo/prism-work` | 0.9.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
175
176
  <!-- generated:package-truth:inventory end -->
176
177
 
177
178
  ## Scripts
@@ -1,5 +1,6 @@
1
+ import type { CheckpointRestoreAudit } from "./checkpoint-restore.js";
1
2
  import type { StoredAgentRunState } from "./agent-run-state.js";
2
- import { Agent, AgentRunResume, AgentRunState, AgentRunStateOptions, DecisionScope, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
3
+ import { Agent, AgentRunCheckpointMetadata, AgentRunResume, AgentRunState, AgentRunStateOptions, DecisionScope, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
3
4
  import type { AgentIdentity } from "./identity.js";
4
5
  /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
5
6
  export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly PendingDecision[] | undefined;
@@ -10,6 +11,10 @@ export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly
10
11
  * crash with raw TypeErrors. State-dependent checks (foreign/stale/duplicate ids, scope,
11
12
  * schema, policy) stay in {@link resolveRunDecisions}. Errors never include tool arguments,
12
13
  * elicitation payloads, credentials, or foreign approval details.
14
+ *
15
+ * The legacy `decision` accepts `continue` (plan 084 Task 1) in addition to `approve`/`deny`;
16
+ * it is a crash-recovery action for running-state checkpoints and is resolved in
17
+ * `prepareAgentRunResume`, never as an approval outcome.
13
18
  */
14
19
  export declare function assertValidAgentRunResume(resume: AgentRunResume): void;
15
20
  interface ResolvedRunDecisions {
@@ -46,7 +51,15 @@ export declare function decisionScopesEqual(a: DecisionScope, b: DecisionScope):
46
51
  export declare function nestedOutcomeToolResult(outcome: Exclude<NestedRunOutcome, {
47
52
  status: "suspended";
48
53
  }>, toolCallId: string, name: string): ToolResult;
49
- export interface ActiveDurableRun {
54
+ /**
55
+ * Non-state extras carried into a resumed run: the sidecar metadata seed (so later checkpoint
56
+ * writes preserve the record's map) and the restore audit emitted on `agent_resumed`.
57
+ */
58
+ export interface ActiveDurableRunExtras {
59
+ readonly checkpointMetadata?: AgentRunCheckpointMetadata;
60
+ readonly restore?: CheckpointRestoreAudit;
61
+ }
62
+ export interface ActiveDurableRun extends ActiveDurableRunExtras {
50
63
  readonly options: AgentRunStateOptions;
51
64
  state?: StoredAgentRunState;
52
65
  version: number;
@@ -28,6 +28,10 @@ export function pendingDecisionsOf(state) {
28
28
  * crash with raw TypeErrors. State-dependent checks (foreign/stale/duplicate ids, scope,
29
29
  * schema, policy) stay in {@link resolveRunDecisions}. Errors never include tool arguments,
30
30
  * elicitation payloads, credentials, or foreign approval details.
31
+ *
32
+ * The legacy `decision` accepts `continue` (plan 084 Task 1) in addition to `approve`/`deny`;
33
+ * it is a crash-recovery action for running-state checkpoints and is resolved in
34
+ * `prepareAgentRunResume`, never as an approval outcome.
31
35
  */
32
36
  export function assertValidAgentRunResume(resume) {
33
37
  const invalid = (message) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message);
@@ -45,7 +49,7 @@ export function assertValidAgentRunResume(resume) {
45
49
  if (!hasDecision && !hasDecisions)
46
50
  throw invalid("Resume requires a decision or decisions");
47
51
  if (hasDecision) {
48
- if (resume.decision !== "approve" && resume.decision !== "deny") {
52
+ if (resume.decision !== "approve" && resume.decision !== "deny" && resume.decision !== "continue") {
49
53
  throw invalid("Unknown legacy decision");
50
54
  }
51
55
  return;
@@ -1,4 +1,4 @@
1
- import type { AgentEventSource, AgentEventSourceOptions } from "./contracts.js";
1
+ import type { AgentEvent, AgentEventSource, AgentEventSourceOptions } from "./contracts.js";
2
2
  export type AgentEventSourceErrorCode = "ERR_PRISM_AGENT_EVENT_SOURCE_INPUT" | "ERR_PRISM_AGENT_EVENT_SOURCE_CURSOR" | "ERR_PRISM_AGENT_EVENT_SOURCE_RETENTION" | "ERR_PRISM_AGENT_EVENT_SOURCE_OVERFLOW" | "ERR_PRISM_AGENT_EVENT_SOURCE_CLOSED";
3
3
  export declare class AgentEventSourceError extends Error {
4
4
  readonly code: AgentEventSourceErrorCode;
@@ -9,3 +9,11 @@ export declare class AgentEventSourceError extends Error {
9
9
  * wakeup or persistence; use a database-backed source for distributed delivery.
10
10
  */
11
11
  export declare function createMemoryAgentEventSource(options?: AgentEventSourceOptions): AgentEventSource;
12
+ /**
13
+ * Terminal-set membership for agent event streams: a run's stream ends on its outcome record.
14
+ *
15
+ * Attribution records — `run_limit_exceeded` and `budget_exhausted` — are delivered *before* that
16
+ * outcome (the `error` that follows a run-limit death), so a page, subscription, or replay must
17
+ * keep reading past them. Every stream-ending site routes through this one predicate.
18
+ */
19
+ export declare function isTerminalAgentEventType(type: AgentEvent["type"]): boolean;
@@ -439,12 +439,19 @@ function positionAfter(records, cursor, stream) {
439
439
  }
440
440
  function terminalAt(records, after) {
441
441
  const last = records.at(-1);
442
- if (!last || !isTerminal(last))
442
+ if (!last || !isTerminalAgentEventType(last.type))
443
443
  return false;
444
444
  return after === undefined || after.sequence <= last.sequence;
445
445
  }
446
- function isTerminal(record) {
447
- return (record.type === "agent_finished" || record.type === "agent_denied" || record.type === "run_limit_exceeded" || record.type === "error");
446
+ /**
447
+ * Terminal-set membership for agent event streams: a run's stream ends on its outcome record.
448
+ *
449
+ * Attribution records — `run_limit_exceeded` and `budget_exhausted` — are delivered *before* that
450
+ * outcome (the `error` that follows a run-limit death), so a page, subscription, or replay must
451
+ * keep reading past them. Every stream-ending site routes through this one predicate.
452
+ */
453
+ export function isTerminalAgentEventType(type) {
454
+ return type === "agent_finished" || type === "agent_denied" || type === "error";
448
455
  }
449
456
  function sameRecord(existing, input) {
450
457
  const { sequence: _existingSequence, ...existingWithoutSequence } = existing;
@@ -25,12 +25,14 @@ export const singleShotLoop = {
25
25
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
26
26
  const request = await ctx.assemble(nextInput, undefined, turn);
27
27
  throwIfAborted(ctx.signal);
28
- const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
28
+ const { content, calls, messageId, started, usage: turnUsage, metadata: turnMetadata } = await ctx.generate(request);
29
29
  usage = turnUsage ?? usage;
30
30
  if (turn === 1)
31
31
  ctx.history.push(...ctx.inputMessages);
32
32
  if (started) {
33
- const message = { id: messageId, role: "assistant", content };
33
+ // Turn provenance (plan 096): a deterministic answer carries `{ deterministic: { middleware } }`
34
+ // so the persisted transcript proves the turn had no model behind it.
35
+ const message = { id: messageId, role: "assistant", content, ...(turnMetadata ? { metadata: turnMetadata } : {}) };
34
36
  ctx.history.push(message);
35
37
  await ctx.appendMessage(message);
36
38
  ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
@@ -140,7 +142,7 @@ export function generateValidateReviseLoop(opts) {
140
142
  }
141
143
  }
142
144
  throwIfAborted(ctx.signal);
143
- const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
145
+ const { content, calls, messageId, started, usage: turnUsage, metadata: turnMetadata } = await ctx.generate(request);
144
146
  usage = turnUsage ?? usage;
145
147
  if (pendingHistory.length > 0) {
146
148
  ctx.history.push(...pendingHistory);
@@ -149,7 +151,8 @@ export function generateValidateReviseLoop(opts) {
149
151
  if (turn === 1)
150
152
  ctx.history.push(...ctx.inputMessages);
151
153
  if (started) {
152
- const message = { id: messageId, role: "assistant", content };
154
+ // Same provenance contract as singleShotLoop (plan 096).
155
+ const message = { id: messageId, role: "assistant", content, ...(turnMetadata ? { metadata: turnMetadata } : {}) };
153
156
  ctx.history.push(message);
154
157
  await ctx.appendMessage(message);
155
158
  ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
1
+ import type { Agent, AgentCheckpointRestoreHook, AgentEvent, AgentRunCheckpointMetadataSource, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
2
2
  export interface AgentRunLifecycleAgent {
3
3
  readonly agent: Agent;
4
4
  /** Current host-authored revision; it must match the stored revision. */
@@ -12,6 +12,14 @@ export interface AgentRunLifecycleOptions {
12
12
  readonly signal?: AbortSignal;
13
13
  }) => AgentRunLifecycleAgent | Promise<AgentRunLifecycleAgent>;
14
14
  readonly fencingToken?: number;
15
+ /**
16
+ * Plan 094 Task 3: external-state restore hooks, run on every claiming resume before the
17
+ * checkpoint is claimed. Registered once here because a resume builds its session from the
18
+ * stored state (there is no live session to register against beforehand).
19
+ */
20
+ readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
21
+ /** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
22
+ readonly restoreHookTimeoutMs?: number;
15
23
  }
16
24
  export interface AgentRunLifecycleRequest {
17
25
  readonly ownership?: OwnershipScope;
@@ -22,6 +30,12 @@ export interface AgentRunLifecycleRequest {
22
30
  readonly persistSessionState?: boolean;
23
31
  /** Opt-in (plan 018 Task 6): restore persisted loaded-skill bodies on resume (requires `persistSessionState` too). */
24
32
  readonly includeSkillBodies?: boolean;
33
+ /** Checkpoint sidecar metadata applied on resume (and to the resumed run's later checkpoints). */
34
+ readonly checkpointMetadata?: AgentRunCheckpointMetadataSource;
35
+ /** Plan 094 Task 3: restore hooks for this resume; the lifecycle's own hooks are used when omitted. */
36
+ readonly restoreHooks?: readonly AgentCheckpointRestoreHook[];
37
+ /** Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
38
+ readonly restoreHookTimeoutMs?: number;
25
39
  }
26
40
  /** Bounded live-event options for a durable lifecycle resume. */
27
41
  export interface AgentRunLifecycleStreamRequest extends AgentRunLifecycleRequest, SubscribeOptions {
@@ -1,21 +1,31 @@
1
1
  import { assertValidAgentRunResume, pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
2
- import { parseAttentionStickyFrontier } from "./attention-compiler.js";
3
- import { agentFingerprint, loadAgentRunState, publicState, saveAgentRunState } from "./agent-run-state.js";
2
+ import { agentFingerprint, loadAgentRunState, publicState, resolveCheckpointMetadata, saveAgentRunState } from "./agent-run-state.js";
4
3
  import { RuntimeAgentSession, throwIfAbortedSignal } from "./agent-session.js";
4
+ import { parseAttentionStickyFrontier, restoreAttentionFoldLedger } from "./attention-compiler.js";
5
+ import { runCheckpointRestoreHooks } from "./checkpoint-restore.js";
5
6
  import { AgentRunStateError } from "./contracts.js";
6
7
  function assertAgentId(actual, expected) {
7
8
  if (expected !== undefined && actual !== expected)
8
9
  throw new AgentRunStateError("Agent run capability mismatch");
9
10
  }
11
+ /** Lifecycle-registered hooks run first, then per-request ones; either timeout setting wins for both. */
12
+ function restoreHookOptions(lifecycle, request) {
13
+ const hooks = [...(lifecycle.restoreHooks ?? []), ...(request.restoreHooks ?? [])];
14
+ const timeoutMs = request.restoreHookTimeoutMs ?? lifecycle.restoreHookTimeoutMs;
15
+ return {
16
+ ...(hooks.length > 0 ? { restoreHooks: hooks } : {}),
17
+ ...(timeoutMs === undefined ? {} : { restoreHookTimeoutMs: timeoutMs }),
18
+ };
19
+ }
10
20
  /** Host capability for durable agent status/resume. Adapters supply authorized ownership only. */
11
21
  export function createAgentRunLifecycle(options) {
12
22
  return {
13
23
  async status(ref, request = {}) {
14
24
  request.signal?.throwIfAborted();
15
- const { state, record } = await loadAgentRunState(options.checkpoints, ref, request.ownership);
25
+ const { state, record, metadata } = await loadAgentRunState(options.checkpoints, ref, request.ownership);
16
26
  assertAgentId(state.agentId, request.agentId);
17
27
  request.signal?.throwIfAborted();
18
- return { state: publicState({ ...state, version: record.version }), version: record.version };
28
+ return { state: publicState({ ...state, version: record.version }), version: record.version, ...(metadata ? { metadata } : {}) };
19
29
  },
20
30
  async resume(ref, resume, request = {}) {
21
31
  request.signal?.throwIfAborted();
@@ -28,8 +38,11 @@ export function createAgentRunLifecycle(options) {
28
38
  ownership: request.ownership,
29
39
  fencingToken: options.fencingToken,
30
40
  definitionRevision: resolved.definitionRevision,
41
+ signal: request.signal,
31
42
  persistSessionState: request.persistSessionState,
32
43
  includeSkillBodies: request.includeSkillBodies,
44
+ ...(request.checkpointMetadata === undefined ? {} : { checkpointMetadata: request.checkpointMetadata }),
45
+ ...restoreHookOptions(options, request),
33
46
  });
34
47
  },
35
48
  async *resumeStream(ref, resume, request = {}) {
@@ -48,6 +61,8 @@ export function createAgentRunLifecycle(options) {
48
61
  overflow: request.overflow,
49
62
  persistSessionState: request.persistSessionState,
50
63
  includeSkillBodies: request.includeSkillBodies,
64
+ ...(request.checkpointMetadata === undefined ? {} : { checkpointMetadata: request.checkpointMetadata }),
65
+ ...restoreHookOptions(options, request),
51
66
  });
52
67
  },
53
68
  };
@@ -55,7 +70,8 @@ export function createAgentRunLifecycle(options) {
55
70
  // Resume free functions moved from agents.ts at 0.1.4 (verbatim; the barrel re-exports the two public ones).
56
71
  /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
57
72
  export async function resumeAgentRun(agent, ref, resume, options) {
58
- return executePreparedAgentRunResume(await prepareAgentRunResume(agent, ref, resume, options));
73
+ throwIfAbortedSignal(options.signal);
74
+ return executePreparedAgentRunResume(await prepareAgentRunResume(agent, ref, resume, options, options.signal), options.signal);
59
75
  }
60
76
  /** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
61
77
  export async function* resumeAgentRunStream(agent, ref, resume, options) {
@@ -81,20 +97,42 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
81
97
  }
82
98
  }
83
99
  }
100
+ /**
101
+ * A `continue` resume needs a run whose frontier is intact: a crash-recovery checkpoint
102
+ * (`status: "running"`) or a turn-policy stop, which writes a terminal state that still carries
103
+ * the frontier (plan 084 Task 2). Every other terminal state is final — a naturally finished run
104
+ * must never be resurrected.
105
+ */
106
+ function isContinuableState(state) {
107
+ return state.status === "running" || (state.status === "succeeded" && state.stopReason === "host_policy");
108
+ }
84
109
  async function prepareAgentRunResume(agent, ref, resume, options, signal) {
85
110
  throwIfAbortedSignal(signal);
86
111
  // Plan 020 Task 2: one shared shape assertion before any checkpoint read/write, agent
87
112
  // resolution, subscription, or tool execution. Unknown legacy decisions (e.g. "sideways")
88
113
  // and malformed untyped batches fail closed here instead of falling through to approval.
89
114
  assertValidAgentRunResume(resume);
90
- const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
115
+ const continuing = resume.decision === "continue";
116
+ const { record, state, metadata: recordMetadata } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
117
+ // Sidecar metadata: a resume-time source wins; otherwise the record's existing map is
118
+ // preserved on every write below, so a non-durable resume cannot wipe it.
119
+ const checkpointMetadata = resolveCheckpointMetadata(options.checkpointMetadata) ?? recordMetadata;
91
120
  if (state.definitionRevision !== options.definitionRevision ||
92
121
  state.agentId !== (agent.config.id ?? agent.config.name) ||
93
122
  state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
94
123
  throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
95
124
  }
96
- if (record.version !== resume.expectedVersion || state.status !== "suspended") {
97
- throw new AgentRunStateError("Stale or non-suspended agent run resume");
125
+ if (record.version !== resume.expectedVersion || !(continuing ? isContinuableState(state) : state.status === "suspended")) {
126
+ throw new AgentRunStateError(continuing ? "Stale or non-running agent run resume" : "Stale or non-suspended agent run resume");
127
+ }
128
+ // Crash recovery never bypasses a gate: only a running checkpoint with no unresolved work may
129
+ // continue. A suspended state (tool approval, elicitation, input guardrail) requires a decision.
130
+ if (continuing) {
131
+ const pending = pendingDecisionsOf(state);
132
+ const awaitingDispatch = state.pending?.status === "ready" || state.pendingCalls?.some((entry) => entry.status === "ready") === true;
133
+ if (state.interruption !== undefined || (pending?.length ?? 0) > 0 || awaitingDispatch) {
134
+ throw new AgentRunStateError("Continue resume requires a running checkpoint with no pending decisions");
135
+ }
98
136
  }
99
137
  const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
100
138
  // Plan 078 Task 7: hand the reconstructed session to an observer (supervisor child-event pump)
@@ -116,6 +154,21 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
116
154
  if (frontier)
117
155
  session.restoreAttentionSticky(frontier);
118
156
  }
157
+ // Plan 086 T3: durable folding writes its own ledger (with the frontier it belongs to), so it
158
+ // is restored whenever the checkpoint carries one — the `durable` opt-in was the host's
159
+ // consent, and a run without it never has this key. Without the frontier the ledger's rows
160
+ // would not be re-applied on an under-ratio turn, so the two ride together.
161
+ if (state.sessionState?.attentionFold) {
162
+ const ledger = restoreAttentionFoldLedger(state.sessionState.attentionFold);
163
+ if (ledger) {
164
+ session.restoreAttentionFold(ledger);
165
+ if (!options.persistSessionState && state.sessionState.attentionSticky) {
166
+ const frontier = parseAttentionStickyFrontier(state.sessionState.attentionSticky);
167
+ if (frontier)
168
+ session.restoreAttentionSticky(frontier);
169
+ }
170
+ }
171
+ }
119
172
  // Plan 018 Task 6 (closeout `checkpoint-bodies`): restore exact instructions so the
120
173
  // resumed session renders them registry-independently (no load_skill round-trip).
121
174
  if (options.persistSessionState && options.includeSkillBodies && state.sessionState?.loadedSkillBodies) {
@@ -210,6 +263,7 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
210
263
  expectedVersion: record.version,
211
264
  ownership: options.ownership,
212
265
  fencingToken: options.fencingToken,
266
+ ...(checkpointMetadata ? { metadata: checkpointMetadata } : {}),
213
267
  });
214
268
  return {
215
269
  kind: "deny",
@@ -237,6 +291,21 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
237
291
  throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
238
292
  }
239
293
  throwIfAbortedSignal(signal);
294
+ // Plan 094 Task 3: restore external state (git commit, document versions) before the claim
295
+ // write. Every hook must succeed — a throw here leaves the checkpoint exactly as it was, and
296
+ // the conversation restore below never runs, so no half-restored world is claimed as resumed.
297
+ const restoreHooks = options.restoreHooks ?? [];
298
+ const restore = restoreHooks.length
299
+ ? await runCheckpointRestoreHooks(restoreHooks, {
300
+ runId: state.runId,
301
+ sessionId: state.sessionId,
302
+ version: record.version,
303
+ status: state.status,
304
+ ...(recordMetadata ? { metadata: recordMetadata } : {}),
305
+ checkpoint: record,
306
+ }, { timeoutMs: options.restoreHookTimeoutMs, signal })
307
+ : undefined;
308
+ throwIfAbortedSignal(signal);
240
309
  const claimed = await saveAgentRunState({
241
310
  checkpoints: options.checkpoints,
242
311
  state: {
@@ -248,20 +317,29 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
248
317
  expectedVersion: record.version,
249
318
  ownership: options.ownership,
250
319
  fencingToken: options.fencingToken,
320
+ ...(checkpointMetadata ? { metadata: checkpointMetadata } : {}),
251
321
  });
252
322
  return {
253
- kind: "approve",
323
+ kind: "claim",
254
324
  session,
255
325
  state: claimed.state,
256
326
  decisions: resolved?.decisionsById,
257
327
  ownership: options.ownership,
328
+ // The configured object must be passed by identity (agent-session assemble rejects a
329
+ // replaced config); its own `checkpointMetadata` provider wins, and the resolved map rides
330
+ // the session as a seed so later writes preserve a record's existing sidecar.
258
331
  runState: configured ?? {
259
332
  checkpoints: options.checkpoints,
260
333
  definitionRevision: options.definitionRevision,
261
334
  interruptBeforeTool: state.interruptBeforeTool,
262
335
  fencingToken: options.fencingToken,
263
336
  resumeNestedRun: options.resumeNestedRun,
337
+ // The checkpoint records its own cadence (plan 084 Task 1), so a continued run keeps writing
338
+ // turn checkpoints without the host repeating the option on resume.
339
+ ...(state.checkpointPolicy ? { checkpointPolicy: state.checkpointPolicy } : {}),
264
340
  },
341
+ ...(checkpointMetadata ? { checkpointMetadata } : {}),
342
+ ...(restore ? { restore } : {}),
265
343
  };
266
344
  }
267
345
  /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
@@ -274,6 +352,9 @@ async function executePreparedAgentRunResume(prepared, signal) {
274
352
  await prepared.session.recordDurableResumption(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
275
353
  return prepared.result;
276
354
  }
277
- return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal, prepared.decisions);
355
+ return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal, prepared.decisions, {
356
+ ...(prepared.checkpointMetadata ? { checkpointMetadata: prepared.checkpointMetadata } : {}),
357
+ ...(prepared.restore ? { restore: prepared.restore } : {}),
358
+ });
278
359
  }
279
360
  //# sourceMappingURL=agent-run-lifecycle.js.map
@@ -1,11 +1,13 @@
1
- import type { Agent, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
2
- import { type PersistedAttentionStickyFrontier } from "./attention-compiler.js";
1
+ import { type PersistedAttentionFoldLedger, type PersistedAttentionStickyFrontier } from "./attention-compiler.js";
2
+ import type { Agent, AgentRunCheckpointMetadata, AgentRunCheckpointMetadataSource, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
3
3
  import type { SecretRedactor } from "./redaction.js";
4
4
  import { type LoadedSkillBodiesEntry } from "./skill-load.js";
5
5
  export declare const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
6
6
  export declare const AGENT_RUN_STATE_SCHEMA_VERSION: 1;
7
7
  export declare const DEFAULT_MAX_AGENT_RUN_STATE_BYTES: number;
8
8
  export declare const HARD_MAX_AGENT_RUN_STATE_BYTES: number;
9
+ /** Sidecar metadata ceiling per checkpoint record (not the run-state value). */
10
+ export declare const MAX_AGENT_RUN_METADATA_BYTES: number;
9
11
  /** One gated tool call awaiting or holding a decision inside a suspended durable run. */
10
12
  export interface PendingToolCall {
11
13
  readonly call: ToolCallContent;
@@ -50,9 +52,25 @@ export interface StoredAgentRunState extends AgentRunState {
50
52
  /** Plan 074 P3: sticky attention mutations (thinking hashes + tool-call ids), so a durable
51
53
  * resume keeps its stubs instead of re-deciding on the first turn. Validated on load. */
52
54
  readonly attentionSticky?: PersistedAttentionStickyFrontier;
55
+ /** Plan 086 T3: folded bodies (`attention.compiler.durable`), so a resumed fold re-applies
56
+ * the same stub bytes instead of re-summarizing. Written and restored independently of
57
+ * `persistSessionState`. Validated on load. */
58
+ readonly attentionFold?: PersistedAttentionFoldLedger;
53
59
  };
54
60
  /** Per-run allow-list (Task 21). Absent = full registered set (legacy checkpoints). */
55
61
  readonly toolNames?: readonly string[];
62
+ /**
63
+ * Recorded checkpoint cadence (plan 084 Task 1). Present only for `"every-turn"` runs, so
64
+ * default checkpoints stay byte-identical. A resume of such a state keeps checkpointing each
65
+ * turn without the host repeating the option.
66
+ */
67
+ readonly checkpointPolicy?: "every-turn";
68
+ /**
69
+ * Set when a terminal state was written by a `RunOptions.turnPolicy` stop (plan 084 Task 2):
70
+ * the run succeeded cleanly but its frontier is intact, so `decision: "continue"` may resume
71
+ * it. Absent on every other state — a naturally finished run is never continuable.
72
+ */
73
+ readonly stopReason?: "host_policy";
56
74
  }
57
75
  /** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
58
76
  export declare const MAX_PERSISTED_SKILL_NAMES = 64;
@@ -69,7 +87,20 @@ export declare function validateRunStateOptions(options: AgentRunStateOptions):
69
87
  export declare function loadAgentRunState(checkpoints: CheckpointStore, ref: AgentRunRef, ownership?: OwnershipScope): Promise<{
70
88
  readonly record: CheckpointRecord;
71
89
  readonly state: StoredAgentRunState;
90
+ readonly metadata?: AgentRunCheckpointMetadata;
72
91
  }>;
92
+ /** Resolve a host metadata source. A throwing provider fails the checkpoint write (fail closed). */
93
+ export declare function resolveCheckpointMetadata(source: AgentRunCheckpointMetadataSource | undefined): AgentRunCheckpointMetadata | undefined;
94
+ /**
95
+ * Redact + bound a sidecar metadata map for a checkpoint write. Values must be strings;
96
+ * redaction runs first so a replacement marker is still charged against the 4 KiB ceiling.
97
+ */
98
+ export declare function boundCheckpointMetadata(metadata: AgentRunCheckpointMetadata, redactor?: SecretRedactor): AgentRunCheckpointMetadata;
99
+ /**
100
+ * Read-side normalization (legacy tolerance): absent, oversize, or non-string entries are
101
+ * dropped, never thrown — a malformed sidecar must not block a resume.
102
+ */
103
+ export declare function readCheckpointMetadata(metadata: unknown): AgentRunCheckpointMetadata | undefined;
73
104
  export declare function saveAgentRunState(input: {
74
105
  readonly checkpoints: CheckpointStore;
75
106
  readonly state: StoredAgentRunState;
@@ -78,6 +109,7 @@ export declare function saveAgentRunState(input: {
78
109
  readonly fencingToken?: number;
79
110
  readonly redactor?: SecretRedactor;
80
111
  readonly maxStateBytes?: number;
112
+ readonly metadata?: AgentRunCheckpointMetadata;
81
113
  }): Promise<{
82
114
  readonly record: CheckpointRecord;
83
115
  readonly state: StoredAgentRunState;