stitchkit 0.59.4 → 0.60.1

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 (110) hide show
  1. package/README.md +7 -3
  2. package/dist/agent-runtime/admission-lanes.d.ts +35 -0
  3. package/dist/agent-runtime/admission-lanes.d.ts.map +1 -0
  4. package/dist/agent-runtime/coordinator.d.ts +46 -2
  5. package/dist/agent-runtime/coordinator.d.ts.map +1 -1
  6. package/dist/agent-runtime/events.d.ts +7 -0
  7. package/dist/agent-runtime/events.d.ts.map +1 -1
  8. package/dist/agent-runtime/history.d.ts +7 -0
  9. package/dist/agent-runtime/history.d.ts.map +1 -1
  10. package/dist/agent-runtime/models.d.ts +2 -3
  11. package/dist/agent-runtime/models.d.ts.map +1 -1
  12. package/dist/agent-runtime/observability.d.ts.map +1 -1
  13. package/dist/agent-runtime/run-execution.d.ts +31 -0
  14. package/dist/agent-runtime/run-execution.d.ts.map +1 -0
  15. package/dist/agent-runtime/runtime-internals.d.ts +25 -0
  16. package/dist/agent-runtime/runtime-internals.d.ts.map +1 -0
  17. package/dist/agent-runtime/runtime-result.d.ts +11 -0
  18. package/dist/agent-runtime/runtime-result.d.ts.map +1 -0
  19. package/dist/agent-runtime/runtime.d.ts +16 -15
  20. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  21. package/dist/agent-runtime/schemas.d.ts.map +1 -1
  22. package/dist/agent-runtime/store-driver.d.ts.map +1 -1
  23. package/dist/agent-runtime/store.d.ts +9 -2
  24. package/dist/agent-runtime/store.d.ts.map +1 -1
  25. package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
  26. package/dist/agent-runtime/terminal-status.d.ts +13 -0
  27. package/dist/agent-runtime/terminal-status.d.ts.map +1 -0
  28. package/dist/agent-runtime.d.ts +5 -3
  29. package/dist/agent-runtime.d.ts.map +1 -1
  30. package/dist/agent-runtime.js +303 -163
  31. package/dist/application/activity.d.ts +7 -2
  32. package/dist/application/activity.d.ts.map +1 -1
  33. package/dist/application/grammy.d.ts +14 -2
  34. package/dist/application/grammy.d.ts.map +1 -1
  35. package/dist/application/health.d.ts +9 -2
  36. package/dist/application/health.d.ts.map +1 -1
  37. package/dist/application/kernel.d.ts +61 -5
  38. package/dist/application/kernel.d.ts.map +1 -1
  39. package/dist/application/schemas.d.ts +42 -0
  40. package/dist/application/schemas.d.ts.map +1 -1
  41. package/dist/application-grammy.js +5 -3
  42. package/dist/application-opentelemetry.js +2 -2
  43. package/dist/application.d.ts +3 -3
  44. package/dist/application.d.ts.map +1 -1
  45. package/dist/application.js +17 -8
  46. package/dist/cli.js +7 -6
  47. package/dist/contract/errors.d.ts +7 -0
  48. package/dist/contract/errors.d.ts.map +1 -1
  49. package/dist/contract/index.js +1 -1
  50. package/dist/declaration.d.ts +345 -0
  51. package/dist/declaration.d.ts.map +1 -0
  52. package/dist/declaration.js +183 -0
  53. package/dist/{index-3nwng7hs.js → index-1pgeyyee.js} +4 -4
  54. package/dist/{index-s8w2y7kr.js → index-2cgbdckv.js} +26 -18
  55. package/dist/{index-gnvzyw0k.js → index-6taryy00.js} +2 -2
  56. package/dist/{index-5c8n2123.js → index-82e74yfx.js} +69 -33
  57. package/dist/{index-mgf6cdkm.js → index-8eywc9zv.js} +35 -1
  58. package/dist/{index-0nc0cddp.js → index-a59da114.js} +17 -28
  59. package/dist/{index-f6pymtqe.js → index-mfw1pec7.js} +7 -5
  60. package/dist/{index-y2rb7dwx.js → index-pzyt11ch.js} +5 -1
  61. package/dist/{index-mr617n62.js → index-svqhcrrj.js} +1 -1
  62. package/dist/{index-28cqssm3.js → index-t8xrqc9g.js} +7 -0
  63. package/dist/{index-7c0gkyvj.js → index-tvwcrx2d.js} +5 -3
  64. package/dist/{index-1tkyng3g.js → index-vkk06pv1.js} +1 -9
  65. package/dist/{index-ps5pxp3h.js → index-wd8g8z6e.js} +2 -79
  66. package/dist/{index-sa2mbwa7.js → index-wrhf06ak.js} +7 -5
  67. package/dist/{index-2nzpcfbp.js → index-x8v8b7rc.js} +2 -2
  68. package/dist/index-xxye8j3k.js +85 -0
  69. package/dist/{index-41wm56v0.js → index-y91zd0ch.js} +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/internal/observability-sink.d.ts.map +1 -1
  72. package/dist/internal/route-pattern.d.ts +9 -0
  73. package/dist/internal/route-pattern.d.ts.map +1 -1
  74. package/dist/node.d.ts +1 -1
  75. package/dist/node.d.ts.map +1 -1
  76. package/dist/node.js +8 -7
  77. package/dist/observability/index.js +6 -5
  78. package/dist/observability/sanitize.d.ts +3 -1
  79. package/dist/observability/sanitize.d.ts.map +1 -1
  80. package/dist/remote.js +6 -4
  81. package/dist/server/error-hook.d.ts +10 -3
  82. package/dist/server/error-hook.d.ts.map +1 -1
  83. package/dist/server/index.d.ts +1 -1
  84. package/dist/server/index.d.ts.map +1 -1
  85. package/dist/server/index.js +16 -18
  86. package/dist/server/middleware/cors.d.ts +5 -2
  87. package/dist/server/middleware/cors.d.ts.map +1 -1
  88. package/dist/server/openapi.d.ts.map +1 -1
  89. package/dist/server/router.d.ts.map +1 -1
  90. package/dist/server/socket-io-config.d.ts +68 -3
  91. package/dist/server/socket-io-config.d.ts.map +1 -1
  92. package/dist/server/socket-io-node.d.ts +1 -1
  93. package/dist/server/socket-io-node.d.ts.map +1 -1
  94. package/dist/server/socket-io.d.ts +1 -1
  95. package/dist/server/socket-io.d.ts.map +1 -1
  96. package/dist/testing/surface-manifest.d.ts.map +1 -1
  97. package/dist/testing.js +9 -10
  98. package/dist/tools/flatten-join.d.ts +2 -0
  99. package/dist/tools/flatten-join.d.ts.map +1 -1
  100. package/dist/tools/flatten.d.ts.map +1 -1
  101. package/dist/tools/mcp-prepare.d.ts +0 -2
  102. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  103. package/dist/tools/runtime-tool.d.ts +0 -3
  104. package/dist/tools/runtime-tool.d.ts.map +1 -1
  105. package/dist/tools/schema.d.ts +0 -4
  106. package/dist/tools/schema.d.ts.map +1 -1
  107. package/dist/tools.js +13 -11
  108. package/llms-full.txt +926 -68
  109. package/llms.txt +1 -0
  110. package/package.json +10 -6
package/llms-full.txt CHANGED
@@ -33,24 +33,36 @@ realtime, `@tanstack/react-query` + `react-query-kit` for React). See
33
33
 
34
34
  ## Entrypoints
35
35
 
36
- stitchkit ships eleven entrypoints. Each is import-safe for one environment —
37
- keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
38
-
39
- | Import | Use in | Holds |
40
- |--------|--------|-------|
41
- | `stitchkit` | browser **and** server | `defineContract`, `createClient`, `createHttpClient`, `createSocketIOClient`, `parseSSE`, the error model |
42
- | `stitchkit/contract` | browser **and** server | the contract layer alone — `defineContract`, errors, pagination |
43
- | `stitchkit/server` | server (Bun) | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
44
- | `stitchkit/node` | server (Node 22) | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
45
- | `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
46
- | `stitchkit/cli` | server | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
47
- | `stitchkit/remote` | browser **and** server | peer-free `implementRemote` for thin HTTP proxy processes |
48
- | `stitchkit/files` | server (Bun or Node) | peer-free managed local-file boundary |
49
- | `stitchkit/observability` | server | request/tool event projections `createObservability`, trace context, sanitisation |
50
- | `stitchkit/agent-runtime` | server | optional durable conversation/run loop, history, models, prompts, fencing and events |
51
- | `stitchkit/agent-runtime/openrouter` | server | isolated OpenRouter language-model adapter |
52
- | `stitchkit/testing` | tests on Bun or Node | in-process generated clients over a real Fetch handler, without a TCP port |
53
- | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
36
+ Every entrypoint is import-safe for one environment — keeping server-only code
37
+ (`Bun.serve`, the MCP SDK) out of browser bundles.
38
+
39
+ Each also declares how settled it is. **Stable** means the shape changes rarely
40
+ and only with a reason worth a migration. **Evolving** means the shape is still
41
+ being found and may be redefined in any minor always with a
42
+ `### ⚠️ Breaking changes` entry and a migration section, never silently. Both
43
+ are legitimate choices; the difference is how often you should expect to read
44
+ the changelog. Moving an entrypoint from evolving to stable is a decision on its
45
+ own, recorded as an ADR.
46
+
47
+ | Import | Use in | Maturity | Holds |
48
+ |--------|--------|----------|-------|
49
+ | `stitchkit` | browser **and** server | stable | `defineContract`, `createClient`, `createHttpClient`, `createSocketIOClient`, `parseSSE`, the error model |
50
+ | `stitchkit/contract` | browser **and** server | stable | the contract layer alone `defineContract`, errors, pagination |
51
+ | `stitchkit/server` | server (Bun) | stable | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
52
+ | `stitchkit/node` | server (Node 22) | stable | `serveNode` + the runtime-agnostic core the Node mirror of `/server` |
53
+ | `stitchkit/tools` | server | stable | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
54
+ | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
55
+ | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
56
+ | `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
57
+ | `stitchkit/observability` | server | stable | request/tool event projections — `createObservability`, trace context, sanitisation |
58
+ | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
59
+ | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
60
+ | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
61
+ | `stitchkit/agent-runtime` | server | evolving | optional durable conversation/run loop, history, models, prompts, fencing and events |
62
+ | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
63
+ | `stitchkit/application` | server | evolving | managed resource graph, readiness, admission, schedules and bounded shutdown |
64
+ | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
65
+ | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
54
66
 
55
67
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
56
68
  code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
@@ -155,7 +167,9 @@ map — feature → packages:
155
167
  | React data layer (`stitchkit/react`) | `@tanstack/react-query` `react-query-kit` |
156
168
  | **Socket.IO server on Bun** | `socket.io` `@socket.io/bun-engine` |
157
169
  | **Socket.IO server on Node** | `socket.io` |
158
- | Socket.IO client | `socket.io-client` |
170
+ | Socket.IO client | `socket.io-client` (brings `@socket.io/component-emitter`, which the browser declarations reference for types only) |
171
+ | grammY lifecycle adapters (`stitchkit/application/grammy`) | `grammy` |
172
+ | OpenTelemetry gauges (`stitchkit/application/opentelemetry`) | `@opentelemetry/api` |
159
173
 
160
174
  ```bash
161
175
  bun add socket.io @socket.io/bun-engine # e.g. the Socket.IO server on Bun
@@ -375,12 +389,13 @@ With this policy, omitting `expose` materializes `['HTTP']` on the returned
375
389
  endpoint. MCP, Agent and CLI then require an explicit endpoint array. The plain
376
390
  factory and `defineContract` keep the default-on behaviour above.
377
391
 
378
- Tool transports (`MCP`, `AGENT`) skip three kinds of endpoint automatically:
379
- `multipart` (a file upload is not a tool call) and
392
+ Tool transports (`MCP`, `AGENT`) skip four kinds of endpoint automatically:
393
+ `multipart` (a file upload is not a tool call),
380
394
  [`rawResponse`](./server.md#raw-response-endpoints) (its answer is bytes, which
381
- a tool result cannot carry — it would serialize to `{}`), plus
395
+ a tool result cannot carry — it would serialize to `{}`),
382
396
  [`responseMeta`](./server.md#typed-json-response-metadata) (outbound HTTP
383
- headers have no meaning on a tool call).
397
+ headers have no meaning on a tool call) and `rawBody` (HTTP-only by
398
+ construction).
384
399
 
385
400
  ## `toolName`
386
401
 
@@ -395,11 +410,13 @@ derivation from the method key + prefix (`users` + `create` ⇒ `create_user`,
395
410
  **Every tool name — derived or explicit — must match `[a-zA-Z0-9_-]` and be at
396
411
  most 64 characters**, the character class every major provider accepts.
397
412
 
398
- Note the two classes differ. What is *accepted* includes the hyphen; what
399
- derivation *keeps* does not a derived name normalises everything outside
400
- `[a-zA-Z0-9_]` to `_`, the hyphen included, so `bot-status` + `get` ⇒
401
- `get_bot_status` and `admin/analytics` + `get` `get_admin_analytics`. A hyphen
402
- survives only in an explicit `toolName`, which is taken verbatim.
413
+ Derivation normalises **per half**, and the halves differ (→ ADR 0035). The
414
+ *service* half collapses everything outside `[a-zA-Z0-9_]` to `_`, hyphen
415
+ included: `bot-status` + `get` `get_bot_status`, `admin/analytics` + `get` ⇒
416
+ `get_admin_analytics`. The *method* half keeps its hyphen and normalises only
417
+ what no provider accepts (`[^a-zA-Z0-9_-]`), because a hyphenated method key has
418
+ always shipped a legal name that a client config may already pin: `notes` +
419
+ `get-user` ⇒ `get-user_notes`. An explicit `toolName` is taken verbatim.
403
420
 
404
421
  A prefix with *no* usable character at all (`'///'`, `'_'`, a fully non-ASCII
405
422
  prefix) and any explicit `toolName` outside the accepted class **throw at
@@ -1740,14 +1757,21 @@ without repeating transport configuration.
1740
1757
  `headers` as a function is the hook for runtime tokens — a bearer token or any
1741
1758
  short-lived credential — re-evaluated on every request.
1742
1759
 
1743
- Expected 401 policy is explicit and contract-driven. Select individual
1744
- operations with `contractEndpointMatchers(contract, ['login'])`, or omit the
1745
- second argument to select every HTTP operation in that contract. Pass the same
1760
+ Expected 401 policy is explicit and contract-driven. Name the operations you
1761
+ mean with `contractEndpointMatchers(contract, ['login'])`, or omit the second
1762
+ argument to select every HTTP operation in that contract. Pass the same
1746
1763
  `ContractClientConfig` as the typed client when routes use a static or dynamic
1747
1764
  `pathPrefix`; dynamic matchers require `stripPrefixKeys`, so the helper can
1748
- compile the prefix structure without a concrete tenant id. Matching is exact by
1749
- path segments, including params and trailing wildcards — a shared prefix never
1750
- suppresses a neighbouring protected endpoint.
1765
+ compile the prefix structure without a concrete tenant id.
1766
+
1767
+ **Matching is by PATH, not by operation.** Each named operation compiles to its
1768
+ path pattern, matched exactly by segments including params and trailing
1769
+ wildcards — so a shared *prefix* never suppresses a neighbour, but two
1770
+ operations on the **same path** are one matcher. If `login` is `POST /session`
1771
+ and `session` is `GET /session`, naming `login` suppresses the expected-401
1772
+ signal for both, and a real expired session stops raising `unauthorized`. Give
1773
+ an operation you must distinguish a path of its own, or narrow the policy with
1774
+ your own `(pathname) => boolean`.
1751
1775
 
1752
1776
  ### Unix domain sockets
1753
1777
 
@@ -2179,8 +2203,10 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
2179
2203
  { method: 'GET', path: '/lookup', desc: 'Look up a price', expose: ['MCP'] } // MCP tool only
2180
2204
  ```
2181
2205
 
2182
- Two kinds of endpoint are **never** tools, whatever `expose` says: a `multipart`
2183
- upload (not a tool call), and a
2206
+ Four kinds of endpoint are **never** tools, whatever `expose` says: a
2207
+ `multipart` upload (not a tool call), a `rawBody` endpoint (HTTP-only by
2208
+ construction), a [`responseMeta`](./server.md#typed-json-response-metadata)
2209
+ endpoint (outbound HTTP headers mean nothing on a tool call), and a
2184
2210
  [`rawResponse`](./server.md#raw-response-endpoints) endpoint (its answer is
2185
2211
  bytes — a tool result cannot carry them, and it would reach the model as `{}`).
2186
2212
  Pin the full list with `listToolNames` in a snapshot test.
@@ -2188,10 +2214,12 @@ Pin the full list with `listToolNames` in a snapshot test.
2188
2214
  `desc` is the tool description the model reads — write it for the model, not
2189
2215
  just for a human. The tool name defaults
2190
2216
  to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
2191
- `get_widget`); set `toolName` for an explicit one. Derivation normalises every
2192
- character outside `[a-zA-Z0-9_]` to `_` — the hyphen included, so `bot-status`
2193
- derives `get_bot_status` while a name is *accepted* if it matches
2194
- `[a-zA-Z0-9_-]`, so a hyphen survives in an explicit `toolName`. A name that
2217
+ `get_widget`); set `toolName` for an explicit one. Derivation normalises **per half**
2218
+ (→ ADR 0035): the service half turns everything outside `[a-zA-Z0-9_]` into `_`,
2219
+ hyphen included (`bot-status` ⇒ `get_bot_status`), while the method half keeps
2220
+ its hyphen (`get-user` `get-user_notes`) because such a name has always been
2221
+ legal and may already be pinned in a client config. A name is *accepted* if it
2222
+ matches `[a-zA-Z0-9_-]`. A name that
2195
2223
  still cannot be delivered (illegal explicit `toolName`, over 64 characters, or a
2196
2224
  prefix with no usable character) throws at mount rather than at the first model
2197
2225
  call —
@@ -3363,6 +3391,11 @@ updated: 2026-08-22
3363
3391
 
3364
3392
  # Agent application runtime
3365
3393
 
3394
+ > **Maturity: evolving.** This surface is still finding its shape and may be
3395
+ > redefined in any minor release — always with a `### ⚠️ Breaking changes` entry
3396
+ > and a migration section, never silently. If your application already owns the
3397
+ > conversation loop, `mountAgent` from `stitchkit/tools` is the stable path.
3398
+
3366
3399
  `stitchkit/agent-runtime` is the server-only, opinionated layer above
3367
3400
  `mountAgent`. Use it when the application wants Stitchkit to own conversation
3368
3401
  mechanics: durable acceptance, history projection, the AI SDK stream loop,
@@ -3554,17 +3587,38 @@ resolves to the same terminal run. Coalescing never mutates the active run.
3554
3587
  while the predecessor still owns managed callbacks. A hung predecessor blocks
3555
3588
  the lane in the first version.
3556
3589
 
3557
- Shutdown is two-phase when a drain budget is supplied:
3590
+ Shutdown is two-phase, and both budgets carry the names they carry everywhere
3591
+ else in Stitchkit:
3558
3592
 
3559
3593
  ```ts
3560
- await runtime.close({ drainTimeoutMs: 30_000, forceTimeoutMs: 5_000 })
3594
+ const closed = await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
3595
+ if (!closed.settled) {
3596
+ console.warn(`exiting with ${closed.remaining} run(s) still in flight`)
3597
+ }
3561
3598
  ```
3562
3599
 
3563
- `close` first rejects new process-local admissions and gives active runs the
3564
- natural drain budget. Only after that budget expires does it abort them with
3565
- reason `shutdown`; `forceTimeoutMs` bounds the final settlement wait for a
3566
- non-cooperative model or tool. Calling `close()` without `drainTimeoutMs` keeps
3567
- the immediate-shutdown form: abort active runs, then wait for their settlement.
3600
+ `close` first rejects new process-local admissions and gives active runs
3601
+ `gracePeriodMs` to finish on their own. Only after that budget expires does it
3602
+ abort them with reason `shutdown`; `forceTimeoutMs` then bounds the settlement
3603
+ wait for a non-cooperative model or tool, measured from the abort — the two
3604
+ budgets add up rather than overlapping.
3605
+
3606
+ **`close()` reports what it achieved rather than promising an outcome it cannot
3607
+ reach.** The result is `{ settled, timedOut, remaining }`: `settled` when every
3608
+ in-flight run finished, `timedOut` with a `remaining` count when the force
3609
+ budget expired first. There is no combination of budgets that is both bounded
3610
+ and guaranteed to leave nothing in flight — that is the trade the budgets exist
3611
+ to make, and the result is where you read which side of it you got:
3612
+
3613
+ | budgets | behaviour | can return with a run in flight |
3614
+ |---------|-----------|---------------------------------|
3615
+ | neither | aborts immediately, waits for settlement | no — unbounded wait |
3616
+ | `gracePeriodMs` only | waits, aborts, then waits for settlement | no — unbounded wait |
3617
+ | `forceTimeoutMs` only | aborts immediately, waits at most that long | **yes** — `timedOut` |
3618
+ | both | waits, aborts, waits at most that long | **yes** — `timedOut` |
3619
+
3620
+ A caller that wants the old shape still writes `await runtime.close(…)` and
3621
+ ignores the result. A caller deciding whether to exit the process reads it.
3568
3622
  Durably queued records rejected from the local queue remain recoverable through
3569
3623
  `scanRecoverable`; close never marks them terminal on its own.
3570
3624
 
@@ -3581,7 +3635,7 @@ terminal event and operator metrics; a loser settles from canonical state withou
3581
3635
  ## Store operations
3582
3636
 
3583
3637
  `AgentRuntimeStore` remains the runtime-facing aggregate. Application adapters
3584
- implement the smaller `AgentRuntimeStoreDriver`, not these eight transitions:
3638
+ implement the smaller `AgentRuntimeStoreDriver` rather than these nine members:
3585
3639
 
3586
3640
  - `acceptInputAndAssignRun`
3587
3641
  - `acquireRun`
@@ -3590,7 +3644,10 @@ implement the smaller `AgentRuntimeStoreDriver`, not these eight transitions:
3590
3644
  - `recoverRun`
3591
3645
  - `commitRunTerminal`
3592
3646
  - `replaceCompactedRange`
3593
- - `loadSnapshot` and `scanRecoverable`
3647
+ - `loadSnapshot`
3648
+ - `scanRecoverable` — one **bounded page** of recoverable runs; `recover()`
3649
+ calls this and nothing else, so an adapter that implements the interface has
3650
+ everything recovery needs
3594
3651
 
3595
3652
  Every mutation carries an expected run revision or snapshot version. Input
3596
3653
  assignment additionally carries an idempotency identity. A conflict is a
@@ -3702,9 +3759,13 @@ base64 blobs into the neutral history store.
3702
3759
 
3703
3760
  To send a stored attachment back to a multimodal model, configure
3704
3761
  `history.resolveFile`. It maps the neutral file reference to AI SDK file data
3705
- (URL, bytes, provider reference or text). Without a resolver the explicit
3706
- `unresolvedFile` policy is `text` by default; choose `omit` or `error` when a
3707
- placeholder would be incorrect.
3762
+ (URL, bytes, provider reference or text). Without a resolver the
3763
+ `unresolvedFile` policy applies, and it is **`omit`** by default: the file part
3764
+ simply does not reach the provider. Choose `text` for a describing placeholder —
3765
+ the filename or media type, never the storage reference, because that string is
3766
+ an address inside your infrastructure and this content travels upstream — or
3767
+ `error` to fail loudly. The `error` message does name the reference: it is
3768
+ thrown into your process, where you are owed the whole story.
3708
3769
 
3709
3770
  ## Compaction
3710
3771
 
@@ -3759,6 +3820,11 @@ updated: 2026-08-23
3759
3820
 
3760
3821
  # Managed application kernel
3761
3822
 
3823
+ > **Maturity: evolving.** This surface is still finding its shape and may be
3824
+ > redefined in any minor release — always with a `### ⚠️ Breaking changes` entry
3825
+ > and a migration section, never silently. The server, contract and client
3826
+ > surfaces it composes are stable.
3827
+
3762
3828
  Use `stitchkit/application` when several process-local resources must become
3763
3829
  ready and shut down as one application. Keep using the lower-level server and
3764
3830
  signal APIs when one managed server is already the complete lifecycle boundary.
@@ -3877,10 +3943,35 @@ A required long-lived completion that rejects after startup makes readiness
3877
3943
  false and health unhealthy. Stitchkit records the failure but does not restart
3878
3944
  the resource or process.
3879
3945
 
3880
- `createApplicationHealthHandler` exposes the neutral lifecycle/health snapshot
3946
+ `ApplicationResourceShutdown.failures` names the phase that failed — `start`,
3947
+ `ready`, `completion`, `admission`, `drain`, `close`, `force` — and nothing
3948
+ else. To learn *why*, pass `onResourceFailure`:
3949
+
3950
+ ```ts
3951
+ createApplication({
3952
+ id: 'app',
3953
+ resources,
3954
+ onResourceFailure: ({ resourceId, phase, error }) => {
3955
+ log.error({ resourceId, phase, err: error }, 'managed resource failed')
3956
+ },
3957
+ })
3958
+ ```
3959
+
3960
+ It fires for every phase, including the one place the cause would otherwise be
3961
+ lost entirely: an **optional** resource failing to start. A required one
3962
+ rethrows, so its cause reaches you anyway; an optional one is swallowed on
3963
+ purpose, because the application keeps running. A throwing observer cannot
3964
+ break the lifecycle it observes.
3965
+
3966
+ `createApplicationHealthHandler` publishes an `ApplicationStatusProjection`
3881
3967
  through a Fetch-compatible handler, suitable for a raw route on Bun or Node.
3882
- Product-specific probes may be composed beside it; do not put secrets or raw
3883
- provider failures in the response.
3968
+ The projection carries the verdict the application's own `id`, `lifecycle`,
3969
+ `health`, `ready`, `capturedAt` and resource **counts** — and never the internal
3970
+ topology: no per-resource ids, no `dependsOn` edges, no process `epoch`, no
3971
+ admission counters.
3972
+ Those stay in `getSnapshot()`, which never leaves the process, and in whatever
3973
+ telemetry you wire to it. Product-specific probes may be composed beside it; do
3974
+ not put secrets or raw provider failures in the response.
3884
3975
 
3885
3976
  For the conventional three-route surface, reuse the same semantics instead of
3886
3977
  copying them into the application:
@@ -3895,8 +3986,11 @@ const rawRoutes = [
3895
3986
  ]
3896
3987
  ```
3897
3988
 
3898
- `status` always returns the current sanitized snapshot with HTTP 200, including
3899
- while starting, draining or stopped. The two probes retain the existing
3989
+ `status` always returns the current published projection with HTTP 200,
3990
+ including while starting, draining or stopped. To read the full snapshot — every
3991
+ resource, its dependency edges and the live counters — call `app.getSnapshot()`
3992
+ in-process; there is no option that publishes it over HTTP, because these routes
3993
+ are meant to be reachable. The two probes retain the existing
3900
3994
  readiness/liveness status and `Retry-After` policy.
3901
3995
 
3902
3996
  Applications that already own an OpenTelemetry SDK may inject its `Meter` into
@@ -4733,6 +4827,12 @@ createServer({
4733
4827
  realtime.emit('note:created', note)
4734
4828
  ```
4735
4829
 
4830
+ `socket.io` and, on Bun, `@socket.io/bun-engine` are optional peers, resolved
4831
+ lazily so a project that never opens a socket does not have to install them. If
4832
+ you ship **one self-contained file** to a machine with no `node_modules`, tell
4833
+ the framework how to load them so your bundler can put them inside — see
4834
+ [shipping one self-contained artifact](./testing-and-deployment.md#shipping-one-self-contained-artifact).
4835
+
4736
4836
  The canonical room-broadcast example below is executed by the test suite. Its
4737
4837
  body is kept byte-identical to `packages/core/examples/realtime-room.ts`.
4738
4838
 
@@ -4768,6 +4868,12 @@ await createSocketIOServer({
4768
4868
  })
4769
4869
  ```
4770
4870
 
4871
+ `cors` is **optional**. Omit it when the browser reaches this server on its
4872
+ own origin — Socket.IO then emits no CORS headers, which is same-origin only.
4873
+ Supply it only for a genuinely cross-origin browser: naming a foreign origin is
4874
+ naming where the code will run, and a repository that does not have to know
4875
+ should not say.
4876
+
4771
4877
  The wrapper-owned fields (`cors` / `path` / `transports` / `ping*` /
4772
4878
  `allowRequest`) take
4773
4879
  precedence over the same keys in `serverOptions`. On Bun the engine-level options
@@ -5676,11 +5782,19 @@ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
5676
5782
  `CONFLICT`, `RATE_LIMITED`, `VALIDATION_ERROR`, `FILE_INVALID_PATH`,
5677
5783
  `FILE_OUTSIDE_ROOT`, `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`,
5678
5784
  `FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
5679
- `REALTIME_CONTRACT_VIOLATION`, `INTERNAL_SERVER_ERROR` — a set that grows in
5785
+ `REALTIME_CONTRACT_VIOLATION`, `APPLICATION_NOT_ACCEPTING`, `WAIT_TIMEOUT`,
5786
+ `WAIT_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
5787
+ `OPERATION_NOT_SUCCEEDED`, `INTERNAL_SERVER_ERROR` — a set that grows in
5680
5788
  ordinary releases, each addition named in the changelog. They are
5681
5789
  published as **`STITCH_ERROR_STATUS`** (the `code → status` map) and
5682
5790
  **`StitchErrorCode`** (its `keyof`), with **`isStitchErrorCode()`** (→ ADR 0026).
5683
5791
 
5792
+ > This list is enumerated here for reading, and the enumeration is what goes
5793
+ > stale — it once dropped `APPLICATION_NOT_ACCEPTING`, which made the map below
5794
+ > stop compiling. `Object.keys(STITCH_ERROR_STATUS)` is the answer that cannot;
5795
+ > `packages/core/tests/error-registry-completeness.test.ts` keeps the registry
5796
+ > itself complete against the source.
5797
+
5684
5798
  If you translate stitch's framework errors into your own wire codes in an
5685
5799
  `onError` hook, you choose how the set's growth reaches you. Keying the map by
5686
5800
  `StitchErrorCode` makes it exhaustive, so a code stitch adds or renames becomes
@@ -5691,7 +5805,10 @@ envelope is a published contract, at the cost of an edit on those releases:
5691
5805
  // Exhaustive on purpose: the annotation is what turns a new stitch code into a
5692
5806
  // compile error here. Drop it (or use `satisfies` on a partial map) to let an
5693
5807
  // unmapped code travel as itself instead.
5694
- const STITCH_TO_APP: Record<StitchErrorCode, AppCode> = {
5808
+ // `Partial`, not an exhaustive `Record`: the set grows in ordinary releases, and
5809
+ // a map written exhaustively stops compiling on every addition. Codes you leave
5810
+ // out fall through to `unmappedCode`.
5811
+ const STITCH_TO_APP: Partial<Record<StitchErrorCode, AppCode>> = {
5695
5812
  NOT_FOUND: 'NOT_FOUND', METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED',
5696
5813
  BAD_REQUEST: 'VALIDATION_ERROR', VALIDATION_ERROR: 'VALIDATION_ERROR',
5697
5814
  UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', CONFLICT: 'CONFLICT',
@@ -6114,10 +6231,18 @@ want the row to say something other than what the framework derived (a domain
6114
6231
  code, a curated message, structured issues). The framework writes only when the
6115
6232
  context carries nothing yet, so your value always wins.
6116
6233
 
6117
- **Endpoint identity is automatic.** The framework writes the matched operation's
6118
- `(serviceName, action)` into the context at route-match, *before* validation — so
6119
- `event.serviceName` / `event.action` are present on every event, including a
6120
- pre-handler 400. Nothing to wire.
6234
+ **Endpoint identity is automatic for contract operations.** The framework
6235
+ writes the matched operation's `(serviceName, action)` into the context at
6236
+ route-match, *before* validation, so both are present on every event for a
6237
+ request that matched a contract route, including a pre-handler 400. Nothing to
6238
+ wire.
6239
+
6240
+ They are **absent** on events that never reached a contract route: a raw route
6241
+ (`openApiRoute`, an MCP mount, any `rawRoutes` entry), a request short-circuited
6242
+ by `onRequest`, and an unmatched 404 or 405. Both fields are optional in
6243
+ `RequestEvent` for exactly that reason — a sink with a `NOT NULL` column on
6244
+ either, or a dashboard grouping by `(serviceName, action)`, has to allow for
6245
+ it.
6121
6246
 
6122
6247
  When failure attribution itself is asynchronous, use `createErrorHook`'s
6123
6248
  `onError(error, info, ctx, endpoint)` observer. The framework awaits it before
@@ -6269,7 +6394,9 @@ A payload goes into an audit row only after `sanitizePayload`:
6269
6394
  - **secret-named keys are masked** — a key whose words include a secret term is
6270
6395
  redacted (`password`, `sessionToken`, `X-Api-Key`, `authorization`, `cookie`,
6271
6396
  …), while identifiers that merely contain one (`authorId`, `sessionCount`,
6272
- `tokenizer`) survive;
6397
+ `tokenizer`) survive. This applies wherever a key exists — an object field and
6398
+ a `Map` entry alike, at any depth. A `Set` member has no key, so nothing there
6399
+ is masked by name;
6273
6400
  - **binary blobs** (`Uint8Array`, `Blob`, `FormData`) collapse to metadata —
6274
6401
  never the bytes;
6275
6402
  - the result is **capped** — anything over the byte limit becomes a preview.
@@ -6712,6 +6839,41 @@ runtime or type-only package outside a case's budget fails with both the case id
6712
6839
  and package name, so an accidental eager import cannot hide behind another
6713
6840
  fixture's transitive dependency.
6714
6841
 
6842
+ Beside the matrix, one case is about the ARTIFACT rather than the inventory: a
6843
+ program that injects the Socket.IO peer loaders is bundled and started in a
6844
+ directory with no `node_modules`, under Node and under Bun with auto-install
6845
+ off. It is checked as a pair — the same program without the loaders must fail
6846
+ there — because a positive result alone would only show that the machine
6847
+ running the check happens to have the package, which is precisely how this class
6848
+ of defect stays invisible in development.
6849
+
6850
+ ### Shipping one self-contained artifact
6851
+
6852
+ Stitchkit resolves the optional Socket.IO peers through a variable, so a
6853
+ consumer bundling an unrelated `stitchkit/server` export never has to resolve
6854
+ them. A bundler cannot follow that, which matters if you ship **one file** to a
6855
+ machine with no `node_modules`: the package never enters the artifact and the
6856
+ failure arrives at start-up, not at build time.
6857
+
6858
+ Pass the loaders, and the literal lives in your source where your bundler sees
6859
+ it:
6860
+
6861
+ ```ts
6862
+ const socket = await createSocketIOServer({
6863
+ cors: { origin: env.CORS_ORIGIN },
6864
+ peers: {
6865
+ server: () => import('socket.io'),
6866
+ // Bun only — on Node the engine is never asked for, and bundling a
6867
+ // Bun-only package for Node asks the bundler to carry `bun:` imports.
6868
+ bunEngine: () => import('@socket.io/bun-engine'),
6869
+ },
6870
+ })
6871
+ ```
6872
+
6873
+ Omit `peers` and nothing changes: the peers stay lazy and are resolved from the
6874
+ machine. Both peers must be listed as dependencies of the package you bundle,
6875
+ because they now genuinely are.
6876
+
6715
6877
  ## Deployment
6716
6878
 
6717
6879
  ### Build
@@ -6855,6 +7017,43 @@ domain run-state remain application resources and close explicitly after server
6855
7017
  drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
6856
7018
  `shutdown()`.
6857
7019
 
7020
+ ### Let the supervisor start the role, not a launcher
7021
+
7022
+ A shutdown chain treats a **second** signal as "stop waiting, force it now".
7023
+ That is deliberate — it is how an operator escalates. It also means an extra
7024
+ process between the supervisor and your role can escalate for you, by
7025
+ accident.
7026
+
7027
+ Point the supervisor at the process that serves, not at a script runner that
7028
+ starts it:
7029
+
7030
+ ```js
7031
+ // supervisor entry — the role's own process
7032
+ script: 'bun', args: ['dist/index.js']
7033
+
7034
+ // NOT this: a launcher in between
7035
+ script: 'bun', args: ['run', 'start']
7036
+ ```
7037
+
7038
+ Measured under PM2 with the launcher form: the stop signal reaches both
7039
+ processes, the launcher forwards its copy, and the server sees two presses in
7040
+ two turns. A declared 15 s grace period ended in 1.3 ms with
7041
+ `outcome: "forced"`, `reason: "signal"` — no requests drained, and the only
7042
+ visible trace was a non-zero exit code. With the direct form, the same stop is
7043
+ `outcome: "clean"`.
7044
+
7045
+ A workspace-filtering launcher (`bun run --filter <pkg> start`) fails harder:
7046
+ the signal never reaches the role at all, so no shutdown runs and the process
7047
+ is torn down when the launcher dies.
7048
+
7049
+ Two habits make this visible instead of silent:
7050
+
7051
+ - log the outcome in `onComplete`, not just the exit code —
7052
+ `result.outcome`, `result.reason` and `result.durationMs` are the difference
7053
+ between "drained" and "was cut off";
7054
+ - keep the supervisor's kill timeout **at or above** the grace period you
7055
+ declared, so a slow drain is not racing its own supervisor.
7056
+
6858
7057
  ### Stdio process signals
6859
7058
 
6860
7059
  An MCP stdio handle has `close()`, not managed HTTP `shutdown()` with a force
@@ -7208,6 +7407,12 @@ current one *up to* your target, and apply each snippet.
7208
7407
  change at each call site. There are no deprecation shims to lean on; the old
7209
7408
  shape is gone, so every site must move.
7210
7409
 
7410
+ Then read this file's **`## Released migration: X.Y.Z`** section for each of
7411
+ those versions. That is the half the changelog does not carry: what else
7412
+ stops working because of the change, and what changes *silently* rather than
7413
+ at compile time — projected history returning fewer messages, a dashboard
7414
+ field going blank. `bun run check` in step 6 cannot see either.
7415
+
7211
7416
  5. **Bump and install.** `bun add stitchkit@<target>` (or update the range), then
7212
7417
  `bun install`. Note the caret: `^0.7.0` is `< 0.8.0`, so crossing a breaking
7213
7418
  minor is always an explicit version bump, never automatic.
@@ -7217,7 +7422,195 @@ current one *up to* your target, and apply each snippet.
7217
7422
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
7218
7423
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
7219
7424
 
7220
- ## Unreleased migration: normalized agent runtime persistence
7425
+ ## Released migration: 0.60.0
7426
+
7427
+ ### close() says what it achieved
7428
+
7429
+ `AgentRuntime.close()` returned `Promise<void>` and its contract promised three
7430
+ things that cannot hold together. It now returns
7431
+ `{ settled, timedOut, remaining }`.
7432
+
7433
+ Nothing breaks if you ignore the value:
7434
+
7435
+ ```ts
7436
+ await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
7437
+ ```
7438
+
7439
+ What DOES change is any code that relied on the old sentence "`close()` never
7440
+ returns while a run is still in flight" while naming a `forceTimeoutMs`. That
7441
+ was never true — the force budget exists precisely to stop waiting — so if your
7442
+ process exits straight after `close()`, read the result:
7443
+
7444
+ ```ts
7445
+ const closed = await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
7446
+ if (!closed.settled) {
7447
+ // `closed.remaining` runs were still in flight. They stay recoverable
7448
+ // through `scanRecoverable`; close never marks them terminal on its own.
7449
+ logger.warn({ remaining: closed.remaining }, 'exiting with runs in flight')
7450
+ }
7451
+ ```
7452
+
7453
+ If you want the old guarantee, omit `forceTimeoutMs`: that is the one
7454
+ combination in which `close()` cannot return with a run in flight — and its wait
7455
+ is unbounded, which is the trade.
7456
+
7457
+ A TypeScript consumer that wrote `const done: void = await runtime.close()` is
7458
+ the only shape that stops compiling.
7459
+
7460
+ ### one name per concept
7461
+
7462
+ `AgentModelDeclaration` was a bare alias of `AgentModelDescriptor`, so the same
7463
+ type reached consumers under two exported names:
7464
+
7465
+ ```ts
7466
+ // before
7467
+ defineModelRegistry<Record<string, AgentModelDeclaration>>({ … })
7468
+ // after
7469
+ defineModelRegistry<Record<string, AgentModelDescriptor>>({ … })
7470
+ ```
7471
+
7472
+ A find-and-replace covers it; there is no behavioural change. Kept name is
7473
+ `AgentModelDescriptor`, the one paired with `AgentModelDescriptorSchema` and
7474
+ returned by `registry.descriptor()`.
7475
+
7476
+ Two names that look like the same case and are **not** being merged:
7477
+ `SocketEventMap` aliases Socket.IO's `EventsMap` so a vendor's name stays out of
7478
+ our signatures, and `AgentRun.ownerId` holds the same value a runtime publishes
7479
+ as `runtimeEpoch` on its events. The latter is one identity in two roles — "who
7480
+ owns this run" and "which runtime emitted this event" — and both declarations
7481
+ now say so. Renaming either would make the fencing comparison read worse.
7482
+
7483
+ ### a reachable public surface
7484
+
7485
+ Three exports that the public API required and did not provide, plus one option
7486
+ it accepted and ignored.
7487
+
7488
+ ```ts
7489
+ // before — identified by string, because the class was exported nowhere
7490
+ if (error instanceof Error && error.name === 'AgentRuntimeConflictError') …
7491
+ // after
7492
+ import { AgentRuntimeConflictError } from 'stitchkit/agent-runtime'
7493
+ if (error instanceof AgentRuntimeConflictError) …
7494
+ ```
7495
+
7496
+ `ActivityTokenBrand` is exported, so `ActivityProjection` can be implemented by
7497
+ a test double. `STITCH_ERROR_STATUS` gained `APPLICATION_NOT_ACCEPTING` (503) —
7498
+ only an exhaustive `satisfies Record<StitchErrorCode, …>` map stops compiling,
7499
+ and the fix is one line. (`GRAMMY_WEBHOOK_NOT_ACCEPTING` joined it in 0.60.1,
7500
+ under the same rule — see ADR 0105. This section briefly said the opposite: that
7501
+ an adapter's code stays out of the registry. It does not, and leaving it out was
7502
+ the worse of the two, because `isStitchErrorCode` then answered `false` and the
7503
+ code reached the wire spelled stitchkit's way, past both `codeMap` and
7504
+ `unmappedCode`.)
7505
+
7506
+ `application.shutdown()` no longer accepts `retryAfterSeconds`. Delete it from
7507
+ the call — the kernel never read it. If you meant the HTTP `Retry-After` a
7508
+ draining server sends, that lives on `managedServerResource({ retryAfterSeconds })`,
7509
+ where it always did the work.
7510
+
7511
+ ### one shutdown vocabulary
7512
+
7513
+ ```ts
7514
+ // before
7515
+ await runtime.close({ drainTimeoutMs: 30_000, forceTimeoutMs: 5_000 })
7516
+ // after
7517
+ await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
7518
+ ```
7519
+
7520
+ The rename is mechanical. The behaviour change under it is not, and it is the
7521
+ reason the rename waited: two combinations were traps.
7522
+ `close({ drainTimeoutMs })` with no force budget aborted the runs and returned
7523
+ **without waiting for them to settle** — so naming a budget gave a weaker
7524
+ guarantee than naming none — and `close({ forceTimeoutMs })` with no drain
7525
+ budget never read the force budget, leaving an unbounded wait. Both now behave
7526
+ as their names say.
7527
+
7528
+ > **Superseded.** This section once ended "and `close()` never returns while a
7529
+ > run is in flight". That was never true with a force budget — the budget exists
7530
+ > to stop waiting — and `close()` now returns `{ settled, timedOut, remaining }`
7531
+ > instead of promising it. See *Released migration: 0.60.0 → close() says what
7532
+ > it achieved*, above.
7533
+
7534
+ If your shutdown path measured how long `close()` took, expect it to take
7535
+ longer in exactly the case where it used to return early — that is the fix, not
7536
+ a regression. Defaults stay per-surface: `ShutdownOptions.gracePeriodMs`
7537
+ defaults to 30 seconds, and the runtime's omitted budget still means "abort
7538
+ immediately", the behaviour `close()` has always had.
7539
+
7540
+ ### unresolved attachments are omitted
7541
+
7542
+ `history.unresolvedFile` defaulted to `text`, and the placeholder it produced
7543
+ carried the storage reference:
7544
+
7545
+ ```ts
7546
+ // before — the provider received your object key
7547
+ // "[attachment: s3://bucket/tenants/42/invoice.pdf]"
7548
+ history: {}
7549
+ // after — omitted entirely by default; ask for a placeholder explicitly
7550
+ history: { unresolvedFile: 'text' } // "[attachment: invoice.pdf]"
7551
+ ```
7552
+
7553
+ Two things to check. If a prompt relied on the model seeing *something* where an
7554
+ unresolved file was, set `unresolvedFile: 'text'` — behaviour otherwise changes
7555
+ silently, since an omitted part produces no error. And if any stored transcript
7556
+ or provider log contains the old placeholder, it contains your storage layout;
7557
+ treat those as disclosed. The `error` policy still names the reference, because
7558
+ it is thrown into your process rather than sent upstream.
7559
+
7560
+ ### one bounded recoverable scan
7561
+
7562
+ `AgentRuntimeStore` had two scans: a mandatory unbounded one and an optional
7563
+ paged one. The runtime only ever called the optional one, so implementing the
7564
+ interface as written produced a store that threw on its first `recover()`:
7565
+
7566
+ ```ts
7567
+ // before — the mandatory member was dead, the needed one was optional
7568
+ {
7569
+ scanRecoverable: () => loadEveryRecoverableSnapshot(),
7570
+ scanRecoverablePage: ({ cursor, limit }) => page(cursor, limit),
7571
+ }
7572
+ // after — one member, the bounded signature the driver already used
7573
+ { scanRecoverable: ({ cursor, limit }) => page(cursor, limit) }
7574
+ ```
7575
+
7576
+ Delete the unbounded implementation rather than porting it: loading every
7577
+ recoverable conversation to start is the shape 0.59.0 and ADR 0101 moved away
7578
+ from, and nothing calls it now. If you built on `createAgentRuntimeStore()` you
7579
+ have nothing to do **if you only pass it to the runtime** — it implements the
7580
+ bounded page for you from the same driver member. If you CALL it yourself, the
7581
+ member changed shape: `scanRecoverable()` took no argument and returned
7582
+ snapshots; it now takes `{ cursor?, limit }` — `limit` is required — and returns
7583
+ one page of descriptors.
7584
+ ```ts
7585
+ // before: const stale = await store.scanRecoverable()
7586
+ // after: const { items, nextCursor } = await store.scanRecoverable({ limit: 100 })
7587
+ ```
7588
+
7589
+ ### published application status
7590
+
7591
+ `createApplicationHealthHandler` and `createApplicationOperationalHandlers` no
7592
+ longer serialise the whole `ApplicationSnapshot`. They publish
7593
+ `ApplicationStatusProjection` — the verdict plus resource counts:
7594
+
7595
+ ```ts
7596
+ // before — the response named every resource and its dependency edges
7597
+ const { resources } = await fetch('/status').then((r) => r.json())
7598
+ resources[0].dependsOn
7599
+ // after — the topology is read in-process, where it always belonged
7600
+ app.getSnapshot().resources[0].dependsOn
7601
+ ```
7602
+
7603
+ The consequence to check is not compilation — it is whatever already consumes
7604
+ these routes. A dashboard that drew the dependency graph from `/status`, or an
7605
+ alert keyed on `admission.pending`, goes blank rather than red: the fields are
7606
+ absent, not zero. Both are available from `getSnapshot()`, so the fix is to read
7607
+ them in the process that owns the application and publish them on a channel you
7608
+ control. If a route was reachable from outside your network, treat the previous
7609
+ payload as disclosed and rotate nothing but assume the topology is known.
7610
+
7611
+ ## Released migration: 0.59.0
7612
+
7613
+ ### Normalized agent runtime persistence
7221
7614
 
7222
7615
  `AgentRuntimeStoreDriver` no longer reads and rewrites a lifetime `AgentStoredState` JSON
7223
7616
  aggregate. Migrate that row and its recoverable/archive projections once:
@@ -7255,6 +7648,85 @@ createAgentRuntimeStore({
7255
7648
  If an application implements `AgentRuntimeStore` directly, its duplicate result must also include
7256
7649
  the canonical `run` and the retained `assistant` for a terminal run.
7257
7650
 
7651
+ ## Released migration: 0.58.0
7652
+
7653
+ ### The default history projection rejects invalid chronology
7654
+
7655
+ `projectAgentHistory` no longer forwards records a provider contract cannot
7656
+ accept. A completed assistant record before the first user message, and an
7657
+ assistant record whose tool calls have no matching results, are omitted with an
7658
+ inspectable decision instead of being sent upstream:
7659
+
7660
+ ```ts
7661
+ // before — a leading assistant record was forwarded as-is
7662
+ projectAgentHistory(messages)
7663
+ // after — opt in explicitly, and only where the provider contract permits it
7664
+ projectAgentHistory(messages, { leadingAssistant: 'allow' })
7665
+ ```
7666
+
7667
+ Two consequences the changelog does not spell out. First, the projection can now
7668
+ return **fewer** messages than the history holds, so any assertion or metric
7669
+ that compared projected length against stored length will move; read the
7670
+ detailed projection instead — it reports what was omitted and why, which is the
7671
+ supported way to see the difference. Second, the omission is silent to the
7672
+ provider but not to you: if a conversation suddenly loses its leading context,
7673
+ the decision record is where that shows up, not the transcript.
7674
+
7675
+ ### Operator events redact `internalCause` by default
7676
+
7677
+ Raw provider and tool failures no longer travel in operator events unless the
7678
+ sink asks for them:
7679
+
7680
+ ```ts
7681
+ // before — internalCause was present
7682
+ createAgentObservability({ write })
7683
+ // after — an explicit operator-only opt-in
7684
+ createAgentObservability({ write, includeInternalCause: true })
7685
+ ```
7686
+
7687
+ The consequence to check before upgrading: any dashboard, alert or log
7688
+ processor keyed on `internalCause` goes blind the moment you upgrade, and it
7689
+ goes blind quietly — the field is absent, not empty. Set the flag on the
7690
+ operator sink you own. Product delivery stays redacted regardless of the flag;
7691
+ this option cannot widen what reaches a user.
7692
+
7693
+ ## Released migration: 0.57.0
7694
+
7695
+ ### Duplicate admission results carry the complete identity
7696
+
7697
+ Custom `AgentRuntimeStore` adapters must persist and return the input and
7698
+ assistant identities associated with an idempotency key:
7699
+
7700
+ ```ts
7701
+ // before
7702
+ return { outcome: 'duplicate', runId, snapshot }
7703
+
7704
+ // after
7705
+ return { outcome: 'duplicate', input, inputMessageId, runId, assistantMessageId, snapshot }
7706
+ ```
7707
+
7708
+ Prefer replacing the custom aggregate reducer with `createAgentRuntimeStore()`;
7709
+ its admission record and transaction driver implement this contract
7710
+ automatically. Historical note: 0.59.0 reshaped this driver again, so an adapter
7711
+ crossing both versions should read that section first and migrate once.
7712
+
7713
+ ### `AgentRuntimeEvent` adds a post-commit `admission` variant
7714
+
7715
+ Add it to any exhaustive publisher switch. Its `assistant` is either the pending
7716
+ placeholder for a new assignment or the canonical persisted assistant for a
7717
+ duplicate:
7718
+
7719
+ ```ts
7720
+ case 'admission':
7721
+ await persistProductProjection(event.input, event.run, event.assistant)
7722
+ break
7723
+ ```
7724
+
7725
+ The consequence for an exhaustive switch written without a `default` branch is a
7726
+ compile error, which is the point. The consequence for a switch that *has* a
7727
+ `default` is worse and silent: post-commit admissions fall into it and are
7728
+ projected as an unknown event. Grep for publisher switches before upgrading.
7729
+
7258
7730
  ## Released migration: 0.56.0
7259
7731
 
7260
7732
  ### Surface manifests are version 2
@@ -7467,6 +7939,64 @@ export const implementFor = createScopedImplement<{
7467
7939
  endpoints inline in the contract literal: an endpoint hoisted into a variable
7468
7940
  widens its `scope` to `string` and is reported as undeclared.
7469
7941
 
7942
+ ## Released migration: 0.49.0
7943
+
7944
+ ### The server handle became managed
7945
+
7946
+ `createServer()` and `serveNode()` return a handle that owns admission, HTTP
7947
+ drain, realtime closure and one deadline-bounded runtime stop:
7948
+
7949
+ ```ts
7950
+ // before
7951
+ server.stop()
7952
+ await socket.io.close()
7953
+ // after
7954
+ await server.shutdown({ gracePeriodMs: 30_000 })
7955
+ ```
7956
+
7957
+ The runtime-specific instance stays reachable at `.runtime`, so an escape hatch
7958
+ you already rely on does not disappear — but code that closed transports itself,
7959
+ in its own order, is now racing the handle. Delete the manual closes rather than
7960
+ keeping both; the handle's result tells you what it drained and what it forced.
7961
+
7962
+ ### Socket.IO mounts through the whole handle
7963
+
7964
+ ```ts
7965
+ // before
7966
+ createServer({ websocket: socket.websocket, rawRoutes: [socket.route] })
7967
+ // after
7968
+ createServer({ socket })
7969
+ ```
7970
+
7971
+ One owner for the route, the WebSocket attachment and the closure. For a raw Bun
7972
+ lane, keep the composed `websocket` handler and pass `socket` beside it.
7973
+
7974
+ ### Bun native `routes` are gone
7975
+
7976
+ Native routes run before the Fetch handler, so they bypassed managed admission —
7977
+ which means they also bypassed shutdown, logging and observability, and that is
7978
+ why they had to go rather than be wired up:
7979
+
7980
+ ```ts
7981
+ // before
7982
+ createServer({ routes: { '/health': () => Response.json({ ok: true }) } })
7983
+ // after
7984
+ createServer({ rawRoutes: [{ method: 'GET', path: '/health', handler: () => Response.json({ ok: true }) }] })
7985
+ ```
7986
+
7987
+ ### The handshake policy takes a Web `Request`
7988
+
7989
+ ```ts
7990
+ // before
7991
+ createSocketIOServer({ serverOptions: { allowRequest: (req, done) => done(null, allowed(req)) } })
7992
+ // after
7993
+ createSocketIOServer({ allowRequest: (request) => allowed(request) })
7994
+ ```
7995
+
7996
+ The Node-shaped callback is gone, and the policy is now composed with shutdown
7997
+ admission on both runtimes: a handshake arriving during drain is refused for
7998
+ you.
7999
+
7470
8000
  ## Released migration: 0.48.0
7471
8001
 
7472
8002
  ### Typed-client request options move to `.withOptions`
@@ -7592,6 +8122,58 @@ contract shape.
7592
8122
  (`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
7593
8123
  `MethodDef`, `maxUploadBytes`) are available to adopt, not required.
7594
8124
 
8125
+ ## Released migration: 0.46.0
8126
+
8127
+ ### `REALTIME_CONTRACT_VIOLATION` joined the error registry
8128
+
8129
+ Realtime contract failures use the framework error model instead of a bare
8130
+ `ZodError`, so an exhaustive map stops compiling until the code is added:
8131
+
8132
+ ```ts
8133
+ // before
8134
+ { …, INTERNAL_SERVER_ERROR: 'internal' } satisfies Record<StitchErrorCode, string>
8135
+ // after
8136
+ { …, INTERNAL_SERVER_ERROR: 'internal', REALTIME_CONTRACT_VIOLATION: 'internal' } satisfies Record<StitchErrorCode, string>
8137
+ ```
8138
+
8139
+ Only an exhaustive map breaks. Since 0.56.1 `codeMap` itself is partial, so a
8140
+ map without the `satisfies` keeps compiling and lets the code travel as itself.
8141
+
8142
+ ### `RealtimeRejectedEvent.error` is an `AppError`
8143
+
8144
+ ```ts
8145
+ // before
8146
+ onRejected: ({ error }) => error.issues
8147
+ // after
8148
+ onRejected: ({ error }) => error.details?.issues // the ZodError moves to error.cause
8149
+ ```
8150
+
8151
+ The envelope gained `reason` and `fault`. The consequence worth checking: code
8152
+ reading `.issues` directly does not fail to compile if the handler is loosely
8153
+ typed — it silently reads `undefined`. Grep for `.issues` on rejection handlers.
8154
+
8155
+ ### CLI construction refuses reserved names
8156
+
8157
+ A contract field or tool named `json`, `wait`, `quiet`, `dry-run`, `help`,
8158
+ `version`, `wait-timeout` or `output-dir` now **throws while the CLI is built**,
8159
+ instead of being silently shadowed:
8160
+
8161
+ ```ts
8162
+ // before: app schedule_job --wait 2h → {"path":"2h"}, exit 0
8163
+ // after: building a CLI over a contract with a "wait" field throws
8164
+ ```
8165
+
8166
+ This one fires at startup, not at call time, so an application shipping such a
8167
+ field crashes on boot after the upgrade. That is deliberate — the old behaviour
8168
+ corrupted arguments silently — but it means the upgrade is not safe to deploy
8169
+ without building the CLI once locally.
8170
+
8171
+ ### `createToolLogger` writes to stderr
8172
+
8173
+ stdout is the JSON-RPC channel of a stdio MCP server, and the previous
8174
+ `console.info` default corrupted it. Pass `log` to redirect if your process
8175
+ collected tool logs from stdout.
8176
+
7595
8177
  ## Released migration: 0.44.0
7596
8178
 
7597
8179
  ### MCP TypeScript SDK v2 and protocol `2026-07-28`
@@ -8286,6 +8868,203 @@ You are on the other side of this flow — see
8286
8868
  allowed; write the `### ⚠️ Breaking changes` block with a before → after snippet,
8287
8869
  bump the minor (pre-1.0), and migrate the controlled consumers in the same pass.
8288
8870
 
8871
+ ### Where the migration section goes while the version has no number
8872
+
8873
+ Write it here, immediately under the flow above, as
8874
+ **`## Unreleased migration: <short slug>`**. The slug matters: several unreleased
8875
+ migrations may sit side by side, and each one belongs to whoever wrote it. Do
8876
+ **not** reuse an existing `Unreleased migration` heading for a different change —
8877
+ that is how the 0.57.0 migration was lost, overwritten by the next author before
8878
+ anyone promoted it.
8879
+
8880
+ At release, the release commit promotes every `Unreleased migration` heading into
8881
+ one `## Released migration: X.Y.Z`, each former heading becoming a `###`
8882
+ subsection under it. This is the same move the changelog makes when `[Unreleased]`
8883
+ becomes `## [X.Y.Z]`, and it happens in the same commit.
8884
+
8885
+ A release carrying `### ⚠️ Breaking changes` and no matching
8886
+ `## Released migration: X.Y.Z` is refused by `bun scripts/release-plan.ts` — in
8887
+ `pre-push` and again in the publishing workflow. The check starts at `0.44.0`;
8888
+ breaking versions older than that are covered by the summary sections near the
8889
+ end of this file.
8890
+
8891
+
8892
+ ==============================================================================
8893
+ # Guide: Project declaration (docs/guide/declaration.md)
8894
+ ==============================================================================
8895
+
8896
+ ---
8897
+ title: Project declaration
8898
+ description: One machine-readable statement a repository makes about itself — identity, roles, build, requirements, release steps and the names of the values a deployment supplies.
8899
+ type: architecture
8900
+ status: active
8901
+ created: 2026-08-25
8902
+ updated: 2026-08-25
8903
+ ---
8904
+
8905
+ # Project declaration
8906
+
8907
+ > **Maturity: evolving.** This surface is still finding its shape and may be
8908
+ > redefined in any minor release — always with a `### ⚠️ Breaking changes` entry
8909
+ > and a migration section, never silently.
8910
+
8911
+ > **Declaring yourself is optional.** A project with no `project.json` is a
8912
+ > complete stitchkit project: nothing in `stitchkit`, `stitchkit/server`,
8913
+ > `stitchkit/node`, `stitchkit/tools` or `stitchkit/cli` reads a declaration,
8914
+ > and no build, test or start path looks for one. Read this page as an offer,
8915
+ > not a requirement — it describes what you get if you decide to say these
8916
+ > things in a machine-readable way instead of in a README.
8917
+
8918
+ `stitchkit/declaration` is the schema for what a repository says about itself:
8919
+ what it is, the roles it runs, what it builds, what has to exist before it
8920
+ starts, what must happen once on release, and the **names** of the environment
8921
+ variables a deployment has to supply.
8922
+
8923
+ ```ts
8924
+ import { parseProjectDeclaration } from 'stitchkit/declaration';
8925
+ import declaration from '../project.json';
8926
+
8927
+ export const appDeclaration = parseProjectDeclaration(declaration);
8928
+ ```
8929
+
8930
+ `parseProjectDeclaration` checks `schemaVersion` **before** it reads any field.
8931
+ A reader that does not recognise the version refuses the repository instead of
8932
+ interpreting it partially — a half-understood declaration is the one failure
8933
+ mode that produces a running, wrong deployment rather than an error.
8934
+
8935
+ ## Why declare yourself
8936
+
8937
+ Because the statements exist either way, and without a schema they exist three
8938
+ times. A repository already says how many roles it runs (in a process file),
8939
+ which variables it needs (in a Zod schema), what it builds (in a script) and
8940
+ what has to happen on release (in a README paragraph). Those four copies drift
8941
+ independently, and nothing fails when they do.
8942
+
8943
+ What a declaration buys:
8944
+
8945
+ - **One reader can be outside the tree.** Whatever builds a source into an
8946
+ artifact and binds that artifact into a deployment can validate the project
8947
+ with the same schema the project validates itself with, without being told
8948
+ anything by the author.
8949
+ - **The tool is not the format.** The schema ships from the framework and is
8950
+ plain published TypeScript plus JSON. Any side may read `project.json`
8951
+ through `stitchkit/declaration` — or generate a JSON Schema from it with
8952
+ `z.toJSONSchema` — and serve the project without asking the format's author
8953
+ for anything. A repository that only one specific tool can bring up is a fork,
8954
+ not a dependency.
8955
+ - **Generated instead of hand-kept.** The starter renders its supervision files
8956
+ and its client-safe identity module from the declaration, so a role added in
8957
+ one place cannot be missing in another.
8958
+
8959
+ What it does not buy: nothing here starts, supervises or deploys anything.
8960
+ The declaration is a statement; acting on it belongs to whatever brings a
8961
+ deployment to a source.
8962
+
8963
+ ## The boundary rule
8964
+
8965
+ > A declaration must be complete and meaningful **when no machine exists**. A
8966
+ > field that cannot be filled in without knowing where the code will run is a
8967
+ > binding supplied by the deployment, not a declaration made by the repository.
8968
+
8969
+ So ports, hosts, absolute URLs, connection strings, machine paths, routing
8970
+ shape, supervision policy and secrets are never *values* in a declaration. A
8971
+ binding is **named** — by the variable that will carry it — and never valued:
8972
+
8973
+ ```jsonc
8974
+ {
8975
+ "listener": { "portVariable": "API_PORT", "bindVariable": "BIND_HOST", "readinessPath": "/health" }
8976
+ }
8977
+ ```
8978
+
8979
+ **What is guaranteed is structure**: there is nowhere in the declaration that a
8980
+ value of the place must go. A command is `executable` plus an `args` array — no
8981
+ shell string, no pipe — and no part may be an absolute path or carry an inline
8982
+ value, so `--port=8080` must be written `['--port', '8080']`, where it is
8983
+ refused as a port. A listener's variables must exist in `env.variables` with the
8984
+ right shapes. Unknown keys are refused rather than stripped.
8985
+
8986
+ **What is filtered is the rest**: every remaining free string is checked against
8987
+ `namesAMachine` — a scheme, a protocol-relative host, an absolute or
8988
+ home-relative path, a Windows drive, a `host:port` pair, a bare IPv4 literal.
8989
+
8990
+ The second half is hygiene, not a proof, and it is worth knowing where it stops:
8991
+ a secret written as its own argument and a hostname written as a plain word look
8992
+ like any other argument. The schema will not catch them, and it is not trying
8993
+ to — the point is that a complete declaration can be written before any machine
8994
+ exists, not that nobody can type a value into one.
8995
+
8996
+ ## Three kinds of input, not two
8997
+
8998
+ The boundary rule separates code from the values of a place. There is a third
8999
+ kind that is neither: **data read while building**. Pages prerendered from a
9000
+ database depend on bytes that are not in the source and are not a binding, and
9001
+ moving environment variables around does not make such a build portable.
9002
+
9003
+ Decide it **per route**, not per project:
9004
+
9005
+ | what the bytes depend on | what is legitimate |
9006
+ | --- | --- |
9007
+ | only the source | static — built once, and portable |
9008
+ | the place (domain, port) | never static; read it from the request |
9009
+ | data | render at runtime · a declared snapshot with a digest · generate it as a release step |
9010
+
9011
+ The default is **render at runtime**: the build needs no database, and the page
9012
+ reads its data where the data lives. Choose a declared snapshot when you need
9013
+ static bytes — the build then reads a frozen export whose digest is a declared
9014
+ input, not a live database. Choose a release step when the bytes must be
9015
+ produced on the way to the deployment; that is the same kind of step as a
9016
+ migration, not a new concept.
9017
+
9018
+ A build that reads data it has not declared is not hermetic, and the fact is
9019
+ invisible: it succeeds on the machine that happens to have the database.
9020
+
9021
+ ## What it carries
9022
+
9023
+ | key | what it says |
9024
+ | --- | --- |
9025
+ | `schemaVersion` | which format this is; checked before anything else |
9026
+ | `kind` | `application` or `library` |
9027
+ | `identity` | `slug`, `name`, `version`, `description` per locale |
9028
+ | `roles` | each with its own working directory, per-mode argv commands, optional listener and `drainFloorMs` |
9029
+ | `build` | the build command and the artifact paths it produces |
9030
+ | `requires` | what the code needs and does not provide, per phase (`release`, `start`) |
9031
+ | `release` | what must happen once before any role starts — migrations declared as bytes |
9032
+ | `env` | the variables a deployment must supply, by name and shape |
9033
+
9034
+ Two details are load-bearing:
9035
+
9036
+ - **A role may declare no listener at all.** A queue consumer, a bot or a
9037
+ scheduler is a role like any other; readiness belongs to a role rather than to
9038
+ the application.
9039
+ - **Migrations are declared as bytes** — `engine`, `root`, `lockfile` — not as a
9040
+ command to run. The side that can see the deployment decides what to do with
9041
+ them: exact contents, admission verdict, whether a preflight can be skipped
9042
+ because nothing touches the database.
9043
+
9044
+ ## Narrowing it for your project
9045
+
9046
+ A project that speaks exactly two locales narrows the shipped schema; it does
9047
+ not write its own copy of the rest.
9048
+
9049
+ ```ts
9050
+ import { z } from 'zod';
9051
+ import { ProjectDeclarationSchema, parseProjectDeclaration } from 'stitchkit/declaration';
9052
+
9053
+ const Narrowed = ProjectDeclarationSchema.safeExtend({
9054
+ identity: ProjectIdentityNarrowedSchema,
9055
+ });
9056
+ ```
9057
+
9058
+ `safeExtend` keeps the refinements the shipped schema carries. Extending the
9059
+ declaration itself is a breaking change to a published schema and moves the
9060
+ minor, with a migration section like any other.
9061
+
9062
+ ## Related
9063
+
9064
+ - [ADR 0104](../decisions/0104-the-project-declaration-ships-from-the-framework.md)
9065
+ — why the schema ships from the framework rather than from each project.
9066
+ - [API reference](../api/reference.md#stitchkitdeclaration) — every export.
9067
+
8289
9068
 
8290
9069
  ==============================================================================
8291
9070
  # API reference (docs/api/reference.md)
@@ -8619,6 +9398,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
8619
9398
  | `RealtimeServerHandle` | _type_ | minimal Socket.IO server handle accepted by `bindRealtimeServer`; carries the handshake identity type through to `connection.raw.data` |
8620
9399
  | `SocketIORequestPolicy` | _type_ | runtime-neutral async-capable Web `Request` handshake admission policy |
8621
9400
  | `SocketIOServerConfig` | _type_ | config for `createSocketIOServer`; `handshake` is the typed identity gate — [guide](../guide/realtime.md#handshake-auth--cookie-or-token) |
9401
+ | `SocketIOPeerLoaders` | _type_ | `peers: { server, bunEngine }` — literal dynamic imports written in YOUR source, so a bundler puts the optional peers inside a self-contained artifact. Omit for the lazy default |
8622
9402
  | `SocketIOHandshakeConfig` | _type_ | the `handshake` gate — Zod `schema` over `handshake.auth` plus optional async `verify`; the result lands typed in `socket.data` |
8623
9403
  | `SocketIOServerHandle` | _type_ | typed Socket.IO server plus Bun mount fields and idempotent lifecycle |
8624
9404
  | `SocketIOServerLifecycle` | _type_ | non-generic Bun mount/shutdown portion accepted by `createServer` |
@@ -8696,6 +9476,10 @@ cutovers are covered by the executable
8696
9476
  | Export | Kind | Summary |
8697
9477
  |--------|------|---------|
8698
9478
  | `createApplication` | function | compose a validated resource DAG into one non-restartable startup, readiness, admission and shutdown state machine |
9479
+ | `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
9480
+ | `ApplicationResourcePhase` | _type_ | the phase a managed resource failed in — the vocabulary of `ApplicationResourceShutdown.failures` |
9481
+ | `ApplicationShutdownOptionsSchema` / `ApplicationShutdownOptions` | schema / _type_ | the two shutdown budgets and an abort signal — without the HTTP-only `retryAfterSeconds` |
9482
+ | `ActivityTokenBrand` | const | the brand symbol `ActivityToken` carries, exported so `ActivityProjection` is implementable |
8699
9483
  | `defineManagedResource` | function | retain the exact typed resource declaration; every invoked start is rollback-eligible |
8700
9484
  | `managedServerResource` | function | adapt an existing managed server without copying its HTTP/WebSocket shutdown machine |
8701
9485
  | `createApplicationHealthHandler` | function | build a Fetch-clean liveness or readiness response from the sanitized application snapshot |
@@ -8754,6 +9538,9 @@ The entrypoint exports each Zod schema beside its inferred type:
8754
9538
  `ApplicationAdmissionSnapshotSchema` / `ApplicationAdmissionSnapshot`,
8755
9539
  `ManagedResourceSnapshotSchema` / `ManagedResourceSnapshot`,
8756
9540
  `ApplicationSnapshotSchema` / `ApplicationSnapshot`,
9541
+ `ApplicationStatusProjectionSchema` / `ApplicationStatusProjection` with
9542
+ `projectApplicationStatus` — what a status or probe endpoint may publish, and
9543
+ the function that derives it from a snapshot,
8757
9544
  `ApplicationResourceShutdownSchema` / `ApplicationResourceShutdown`, and
8758
9545
  `ApplicationShutdownResultSchema` / `ApplicationShutdownResult`.
8759
9546
 
@@ -8818,7 +9605,9 @@ Server-only optional application runtime. See the
8818
9605
  | `AgentAdmissionEventSchema` | schema | post-commit admission projection; removes store rereads but does not imply exactly-once delivery |
8819
9606
  | `AgentRunMetricsSchema` | schema | optional provenance-aware usage and timings; `partial` distinguishes checkpoint from terminal totals |
8820
9607
  | `AgentRuntimeRecoverOptions` | _type_ | bounded paged startup recovery with context resolver and explicit evidence policy |
8821
- | `AgentSessionCloseOptions` | _type_ | natural `drainTimeoutMs` followed by shutdown abort and optional bounded `forceTimeoutMs` settlement wait |
9608
+ | `AgentRuntimeConflictError` | class | thrown when a store mutation loses to a concurrent writer — catchable by type from `stitchkit/agent-runtime` |
9609
+ | `AgentSessionCloseOptions` | _type_ | `gracePeriodMs` for natural settlement, then abort, then `forceTimeoutMs` for bounded settlement after it |
9610
+ | `AgentSessionCloseResult` | _type_ | what `close()` achieved: `settled`, or `timedOut` with `remaining` runs still in flight. Only omitting `forceTimeoutMs` guarantees nothing is in flight on return |
8822
9611
  | `AgentHistoryProjectionOptions` | _type_ | storage-neutral file resolver and explicit unresolved-file behavior |
8823
9612
  | `createAgentToolFenceLifecycle` | function | pre-effect and post-effect run ownership fence for `mountAgent` |
8824
9613
  | `AgentRuntimeEventSchema` | schema | transient stream lifecycle plus post-commit admission/checkpoint/run-state/terminal projections |
@@ -8866,7 +9655,7 @@ selection never splits a tool chronology and reports why every canonical record
8866
9655
  removed.
8867
9656
 
8868
9657
  Model exports are `AgentLanguageModelProvider`, `AgentModelCapability`,
8869
- `AgentModelCapabilitySchema`, `AgentModelDeclaration`, `AgentModelDescriptor`,
9658
+ `AgentModelCapabilitySchema`, `AgentModelDescriptor`,
8870
9659
  `AgentModelDescriptorSchema`, `AgentModelRegistry`, `AgentModelRegistryConfig`,
8871
9660
  `AgentModelRegistrySnapshot`, `AgentModelRegistrySnapshotSchema`, `AgentModelSnapshotPolicy`,
8872
9661
  `AgentResolvedModel` and `validateAgentModelSnapshot`. Registry `preflight` validates availability,
@@ -9303,6 +10092,75 @@ available from `stitchkit/contract`.
9303
10092
 
9304
10093
  ---
9305
10094
 
10095
+ ## `stitchkit/declaration`
10096
+
10097
+ Zod-only, dependency-free. The **project declaration**: the single
10098
+ machine-readable statement a repository makes about itself, read by the project,
10099
+ by the scaffolder that writes the first copy, and by whatever builds a source and
10100
+ binds the artifact into a deployment. It ships from the framework so those
10101
+ readers cannot hold different copies of the same schema.
10102
+
10103
+ **Declaring yourself is optional.** A project with no `project.json` is a
10104
+ complete project: nothing else in the framework imports this entrypoint, no
10105
+ build, test or start path looks for a declaration, and its absence is never an
10106
+ error or a warning.
10107
+
10108
+ The rule the schema exists to hold: **a declaration must be complete and
10109
+ meaningful when no machine exists**. A field that cannot be filled in without
10110
+ knowing where the code will run is a binding supplied by the deployment, not a
10111
+ declaration made by the repository.
10112
+
10113
+ **Structure is the guarantee**: nothing here requires a value of the place. A
10114
+ command is `executable` plus an `args` array, no part may be an absolute path or
10115
+ carry an inline value (`--port=8080` must be `['--port', '8080']`), paths are
10116
+ repository-relative, bindings are named by variable and never valued, and a
10117
+ listener's variables must exist in `env.variables` with matching shapes.
10118
+
10119
+ **Hygiene is the filter**: every remaining free string is checked against
10120
+ `namesAMachine` — a scheme, a protocol-relative host, an absolute or
10121
+ home-relative path, a Windows drive, a `host:port` pair, a bare IPv4 literal —
10122
+ and a number after a port flag is refused. It catches known shapes; it is not a
10123
+ secret scanner, and a secret or hostname written as a plain argument passes.
10124
+
10125
+ Unknown keys are **refused**, not stripped. A key one reader does not recognise
10126
+ is a disagreement between programs that never meet, and discarding it silently
10127
+ is how a partially understood declaration becomes a running, wrong deployment.
10128
+
10129
+ | Export | Kind | Summary |
10130
+ |--------|------|---------|
10131
+ | `ProjectDeclarationSchema` / `ProjectDeclaration` | schema / _type_ | the declaration: `schemaVersion`, `kind`, `identity`, `roles`, `build`, `requires`, `release`, `env` |
10132
+ | `parseProjectDeclaration` | function | parse a declaration, refusing an unrecognised `schemaVersion` **before** any field is read |
10133
+ | `PROJECT_DECLARATION_SCHEMA_VERSION` | constant | the declaration format this build understands — the number a reader refuses on |
10134
+ | `namesAMachine` | function | why a string names a particular machine, or `undefined` — the one predicate every free string is checked against |
10135
+ | `findProjectRole` | function | the role with a given name, or `undefined` |
10136
+ | `ProjectIdentitySchema` / `ProjectIdentity` | schema / _type_ | `slug`, `name`, `version` and a per-locale `description` |
10137
+ | `ProjectRoleSchema` / `ProjectRole` | schema / _type_ | one runnable role: a `workingDirectory` inside the source, per-mode `commands`, an optional `listener`, and `drainFloorMs` |
10138
+ | `ProjectCommandSchema` / `ProjectCommand` | schema / _type_ | `executable` plus `args` — argv, never a shell string |
10139
+ | `ProjectRoleCommandSchema` / `ProjectRoleCommand` | schema / _type_ | a command run under a supervisor: additionally never a script runner, because a launcher duplicates the shutdown signal |
10140
+ | `ProjectListenerSchema` | schema | `portVariable`, `bindVariable` and `readinessPath` — **absent means the role has no listener at all** |
10141
+ | `ProjectRunModeSchema` | schema | the run modes a role declares commands for |
10142
+ | `ProjectBuildSchema` | schema | build `command`, the set of `artifacts` it produces — more than one path is normal — and any declared data `inputs` |
10143
+ | `ProjectBuildInputSchema` / `ProjectBuildInput` | schema / _type_ | data the build may read: a `name`, a frozen export `path` inside the source, and the `sha256:` `digest` that pins its bytes. **Absent `inputs` means the build reads no data** — an answer, not a gap |
10144
+ | `ProjectRequirementSchema` / `ProjectRequirementPhaseSchema` | schema | something the code needs but does not provide, and the `phases` — `release`, `start` — it is needed in |
10145
+ | `ProjectReleaseSchema` / `ProjectMigrationsSchema` | schema | what must happen once before roles start; migrations are declared as `engine`, `root` and `lockfile` — **bytes, not a command to run** |
10146
+ | `ProjectEnvVariableSchema` / `ProjectEnvVariable` / `ProjectEnvShapeSchema` | schema / _type_ | a variable a deployment must supply: `name`, `shape`, `required`, and `members` for an enum |
10147
+ | `ProjectSlugSchema` | schema | lowercase hyphen-separated identity everything else is named after |
10148
+ | `ProjectDescriptionSchema` | schema | description per locale tag |
10149
+ | `RepositoryPathSchema` | schema | a path inside the source — absolute, `..`, `~` and Windows paths are refused |
10150
+ | `BindingVariableSchema` | schema | the NAME of an environment variable a deployment fills in |
10151
+
10152
+ Version handling is **fail-closed**: an unrecognised `schemaVersion` is refused,
10153
+ never assumed compatible, and refused before any field is read so a newer
10154
+ declaration reports as a version this build cannot serve rather than as a list of
10155
+ unrecognised keys.
10156
+
10157
+ To compose a stricter declaration, build on the exported member schemas — for
10158
+ example `ProjectDeclarationSchema.safeExtend({ identity: … })`, which keeps the
10159
+ boundary refinements in force. `ProjectDescriptionSchema` is a record, so narrow
10160
+ it by replacing the field rather than extending it.
10161
+
10162
+ ---
10163
+
9306
10164
  ## `stitchkit/node`
9307
10165
 
9308
10166
  Server-only, for Node ≥ 22 (Bun uses `stitchkit/server`). The runtime-agnostic
@@ -9321,7 +10179,7 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
9321
10179
  | `NodeRuntimeServer` | _type_ | concrete `srvx/node` runtime escape hatch |
9322
10180
  | `NodeSocketLifecycle` | _type_ | Bun-free Socket.IO lifecycle accepted by `serveNode` |
9323
10181
  | `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` | _type_ | runtime-neutral handler types; raw routes default their host server to `unknown` |
9324
- | `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config and Bun-free Node handle with `io`, `attach` and lifecycle |
10182
+ | `SocketIORequestPolicy` / `SocketIOServerConfig` / `SocketIOPeerLoaders` / `SocketIOServerHandle` | _type_ | runtime-neutral handshake policy, config, optional-peer loaders and the Bun-free Node handle with `io`, `attach` and lifecycle |
9325
10183
  | `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
9326
10184
 
9327
10185
  ---