stitchkit 0.118.1 → 0.119.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 (144) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/contract/client-types.d.ts +6 -3
  4. package/dist/contract/client-types.d.ts.map +1 -1
  5. package/dist/contract/define-validation.d.ts.map +1 -1
  6. package/dist/contract/define.d.ts +18 -12
  7. package/dist/contract/define.d.ts.map +1 -1
  8. package/dist/contract/endpoint-capabilities.d.ts +28 -0
  9. package/dist/contract/endpoint-capabilities.d.ts.map +1 -0
  10. package/dist/contract/factory.d.ts +49 -19
  11. package/dist/contract/factory.d.ts.map +1 -1
  12. package/dist/contract/tool-view.d.ts +1 -1
  13. package/dist/contract/tool-view.d.ts.map +1 -1
  14. package/dist/contract/transports.d.ts +5 -0
  15. package/dist/contract/transports.d.ts.map +1 -0
  16. package/dist/entrypoints/agent-runtime/coding-tools.js +6 -5
  17. package/dist/entrypoints/agent-runtime/harness.js +6 -5
  18. package/dist/entrypoints/agent-runtime/sandbox.js +6 -5
  19. package/dist/entrypoints/agent-runtime/sqlite/bun.js +9 -8
  20. package/dist/entrypoints/agent-runtime/sqlite/node.js +9 -8
  21. package/dist/entrypoints/agent-runtime/testing.js +9 -8
  22. package/dist/entrypoints/agent-runtime.js +14 -13
  23. package/dist/entrypoints/application/diagnostic-journal.js +3 -3
  24. package/dist/entrypoints/application/directory-inbox.js +4 -4
  25. package/dist/entrypoints/application/opentelemetry.js +1 -1
  26. package/dist/entrypoints/application.js +5 -5
  27. package/dist/entrypoints/cli/publish.js +4 -4
  28. package/dist/entrypoints/cli.js +8 -7
  29. package/dist/entrypoints/contract.d.ts +1 -1
  30. package/dist/entrypoints/contract.d.ts.map +1 -1
  31. package/dist/entrypoints/contract.js +6 -4
  32. package/dist/entrypoints/files.js +3 -3
  33. package/dist/entrypoints/geo/contract.d.ts +7 -0
  34. package/dist/entrypoints/geo/contract.d.ts.map +1 -0
  35. package/dist/entrypoints/geo/contract.js +6 -0
  36. package/dist/entrypoints/geo.js +60 -23
  37. package/dist/entrypoints/http.js +2 -2
  38. package/dist/entrypoints/index.js +8 -6
  39. package/dist/entrypoints/live.js +1 -1
  40. package/dist/entrypoints/node.js +7 -6
  41. package/dist/entrypoints/primitives.js +1 -1
  42. package/dist/entrypoints/process/owner-loss.js +1 -1
  43. package/dist/entrypoints/process.js +3 -3
  44. package/dist/entrypoints/react.js +1 -1
  45. package/dist/entrypoints/remote.js +4 -3
  46. package/dist/entrypoints/server.js +11 -10
  47. package/dist/entrypoints/telegram.js +2 -2
  48. package/dist/entrypoints/testing.js +7 -6
  49. package/dist/entrypoints/tools/connections.js +3 -2
  50. package/dist/entrypoints/tools/contract.js +1 -1
  51. package/dist/entrypoints/tools/invoker.js +5 -4
  52. package/dist/entrypoints/tools/mcp.js +7 -6
  53. package/dist/entrypoints/tools.js +17 -15
  54. package/dist/entrypoints/tracking/contract.js +5 -3
  55. package/dist/entrypoints/tracking/performance.js +3 -3
  56. package/dist/entrypoints/tracking.d.ts +1 -1
  57. package/dist/entrypoints/tracking.d.ts.map +1 -1
  58. package/dist/entrypoints/tracking.js +63 -83
  59. package/dist/entrypoints/xlsx.js +1 -1
  60. package/dist/geo/index.d.ts +2 -1
  61. package/dist/geo/index.d.ts.map +1 -1
  62. package/dist/geo/record.d.ts +8 -0
  63. package/dist/geo/record.d.ts.map +1 -0
  64. package/dist/geo/resolver.d.ts +1 -3
  65. package/dist/geo/resolver.d.ts.map +1 -1
  66. package/dist/geo/schema.d.ts +44 -0
  67. package/dist/geo/schema.d.ts.map +1 -0
  68. package/dist/geo/types.d.ts +2 -13
  69. package/dist/geo/types.d.ts.map +1 -1
  70. package/dist/{index-n5sv4z0w.js → index-1k18qzqa.js} +13 -15
  71. package/dist/{index-tsngj2b5.js → index-2dfvv1jg.js} +206 -206
  72. package/dist/{index-d9hq291n.js → index-4jpykzvw.js} +70 -81
  73. package/dist/{index-ghtpfz4z.js → index-57gqaq8b.js} +2 -2
  74. package/dist/index-69t14s4a.js +39 -0
  75. package/dist/{index-6p3frkv5.js → index-6a69xzrx.js} +4 -4
  76. package/dist/{index-dky0dcmf.js → index-72r9k0av.js} +6 -2
  77. package/dist/{index-vv1sch8a.js → index-8vzh5wpv.js} +13 -8
  78. package/dist/{index-xx1j310k.js → index-8yxgcyt6.js} +171 -169
  79. package/dist/{index-p6apg0jg.js → index-9frf01t9.js} +16 -9
  80. package/dist/{index-q0th8q0z.js → index-9mjrnqxs.js} +1 -3
  81. package/dist/{index-ega3yz4c.js → index-aqwg2dm5.js} +1 -1
  82. package/dist/{index-073nw9qr.js → index-ba7are04.js} +524 -522
  83. package/dist/{index-095ya5hm.js → index-bbvc59gw.js} +13 -11
  84. package/dist/{index-txg1npmg.js → index-cyxp8rbf.js} +1 -1
  85. package/dist/index-d3xdyjt6.js +38 -0
  86. package/dist/{index-n8z3rvzw.js → index-f4njz9j0.js} +3 -5
  87. package/dist/{index-90p34rts.js → index-fcqj44mb.js} +3 -1
  88. package/dist/{index-86nbfysb.js → index-ff39f9dr.js} +3 -3
  89. package/dist/{index-75ns2ybj.js → index-fqd4yqwd.js} +1 -1
  90. package/dist/{index-nqxrm176.js → index-g444vfz2.js} +1 -1
  91. package/dist/{index-vwhrnp67.js → index-kz9734jc.js} +3 -3
  92. package/dist/{index-dr3t5n1v.js → index-nn9qvkww.js} +46 -46
  93. package/dist/{index-qezhmp51.js → index-nz06qw5a.js} +3 -1
  94. package/dist/{index-1bx1xv1k.js → index-pfvde2fz.js} +5 -2
  95. package/dist/{index-s80awwsw.js → index-qch1vcaa.js} +3 -1
  96. package/dist/{index-cycp3r3e.js → index-rxcgp4b9.js} +2 -2
  97. package/dist/{index-w7jq3fz9.js → index-s0qjs6ee.js} +1198 -1196
  98. package/dist/{index-p0rf7sq9.js → index-s20s2mad.js} +209 -209
  99. package/dist/{index-v6vb5qx8.js → index-s3yxw6kt.js} +1 -1
  100. package/dist/{index-cqmqyjax.js → index-sa99p98w.js} +1 -1
  101. package/dist/{index-0gxvns6s.js → index-sba3e6m4.js} +1 -0
  102. package/dist/{index-f24bdt13.js → index-v1swr26p.js} +3 -1
  103. package/dist/{index-rhzkndr5.js → index-x4p3kw1p.js} +533 -530
  104. package/dist/{index-hdjmdwsj.js → index-x8d9fdeg.js} +513 -507
  105. package/dist/{index-htxh46ej.js → index-ycqhyars.js} +2 -2
  106. package/dist/{index-9bwcthxw.js → index-yj6saztf.js} +31 -13
  107. package/dist/{index-tdpmfks8.js → index-ynrf908v.js} +1 -1
  108. package/dist/{index-f6m4c545.js → index-zrdcmg6j.js} +4 -6
  109. package/dist/process/bun-child.d.ts +3 -2
  110. package/dist/process/bun-child.d.ts.map +1 -1
  111. package/dist/server/contract-method.d.ts.map +1 -1
  112. package/dist/tools/internal/surface-projector.d.ts.map +1 -1
  113. package/dist/tools/operations/async-operation-canonical-types.d.ts +1 -1
  114. package/dist/tracking/attribution.d.ts +21 -25
  115. package/dist/tracking/attribution.d.ts.map +1 -1
  116. package/dist/tracking/client-visit.d.ts +5 -7
  117. package/dist/tracking/client-visit.d.ts.map +1 -1
  118. package/dist/tracking/client.d.ts +2 -2
  119. package/dist/tracking/client.d.ts.map +1 -1
  120. package/dist/tracking/contract.d.ts +1 -1
  121. package/dist/tracking/host.d.ts +0 -1
  122. package/dist/tracking/host.d.ts.map +1 -1
  123. package/dist/tracking/privacy.d.ts +6 -0
  124. package/dist/tracking/privacy.d.ts.map +1 -1
  125. package/dist/tracking/schemas.d.ts +2 -2
  126. package/dist/tracking/schemas.d.ts.map +1 -1
  127. package/llms/contract.txt +46 -24
  128. package/llms/geo-contract.txt +23 -0
  129. package/llms/geo.txt +40 -4
  130. package/llms/stitchkit.txt +4 -3
  131. package/llms/tools.2.txt +2 -2
  132. package/llms/tools.3.txt +1 -1
  133. package/llms/tracking-contract.txt +1 -1
  134. package/llms/tracking.txt +27 -5
  135. package/llms/{upgrading-0.105.0-0.118.0.txt → upgrading-0.106.0-0.119.0.txt} +121 -410
  136. package/llms/upgrading-0.37.0-0.53.0.txt +969 -0
  137. package/llms/{upgrading-0.44.0-0.62.0.txt → upgrading-0.55.0-0.68.0.txt} +509 -528
  138. package/llms/{upgrading-0.63.0-0.83.0.txt → upgrading-0.69.0-0.95.0.txt} +506 -509
  139. package/llms/{upgrading-0.84.0-0.104.0.txt → upgrading-0.96.0-0.105.0.txt} +410 -506
  140. package/llms/upgrading.txt +5 -5
  141. package/llms-full.txt +247 -38
  142. package/llms.txt +10 -9
  143. package/package.json +23 -19
  144. package/llms/upgrading-0.37.0-0.44.0.txt +0 -444
@@ -1,14 +1,421 @@
1
- # Upgrading 0.84.0 → 0.104.0 — stitchkit docs slice
1
+ # Upgrading 0.96.0 → 0.105.0 — stitchkit docs slice
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
5
 
6
- Contains: Guide: Upgrading, 0.84.0 → 0.104.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.96.0 → 0.105.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.84.0 → 0.104.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.96.0 → 0.105.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.105.0
13
+
14
+ ### CLI publication has its own entrypoint
15
+
16
+ **Who must act:** code that imports `publishCli`, `CliPublicationOptions`, `CliPublicationPhase` or
17
+ `CliPublicationResult` from `stitchkit/cli`. Everything else in `stitchkit/cli` — `createCli`, the
18
+ manifest schemas, signatures, `renderCliInstaller` and the updater — is unchanged.
19
+
20
+ `publishCli` is release infrastructure, so it now ships from the evolving entrypoint
21
+ `stitchkit/cli/publish` instead of the stable `stitchkit/cli`
22
+ ([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). Its options, result and
23
+ behaviour are the same.
24
+
25
+ 1. Move the publication names to the new specifier; keep every other CLI import where it is:
26
+
27
+ ```ts
28
+ // before
29
+ import { publishCli, type CliPublicationOptions, verifyCliManifest } from 'stitchkit/cli'
30
+ // after
31
+ import { verifyCliManifest } from 'stitchkit/cli'
32
+ import { publishCli, type CliPublicationOptions } from 'stitchkit/cli/publish'
33
+ ```
34
+
35
+ Search pattern: `\b(publishCli|CliPublication(Options|Phase|Result))\b` in files that import
36
+ from `'stitchkit/cli'`.
37
+ 2. Type-check: an import left on `stitchkit/cli` fails with "has no exported member".
38
+ 3. `stitchkit/cli/publish` is evolving: read its breaking entries in each minor you upgrade across.
39
+
40
+ ### a command stops by its `stop` policy
41
+
42
+ **Who must act:** callers of `runNativeCommand` (`stitchkit/process`) that pass `killGraceMs`.
43
+ Callers that omit it need no change: the default stop is the same TERM to the group, 100 ms, KILL.
44
+
45
+ 1. Replace `killGraceMs: N` with a group stop of the same grace:
46
+
47
+ ```ts
48
+ // before
49
+ await runNativeCommand({ executable, args, signal, killGraceMs: 500 })
50
+ // after
51
+ await runNativeCommand({ executable, args, signal, stop: { target: 'group', graceMs: 500 } })
52
+ ```
53
+
54
+ Search pattern: `killGraceMs:\s*(\d[\d_]*)` → `stop: { target: 'group', graceMs: $1 }`.
55
+ 2. If your own wrapper signals only the leader and waits a long grace (cooperative cancellation),
56
+ replace it with `stop: { target: 'leader', signal: 'SIGINT', graceMs }` (up to one hour). Pass
57
+ `killOn` with your shutdown signal so an application that stops sooner kills the group first.
58
+ 3. The result of a stopped command rejects once the group is gone, up to `graceMs` plus
59
+ `cleanupTimeoutMs` after the abort; size your caller deadlines on that sum.
60
+
61
+ ### a command stops what its leader left
62
+
63
+ **Who must act:** callers of `runNativeCommand` (`stitchkit/process`) whose command starts a helper
64
+ in its own process group that must keep running after the command returns. A daemon that detaches
65
+ with `setsid` (as `daemon(3)` does) is not in the group and needs
66
+ no change.
67
+
68
+ `descendants` now defaults to `'terminate-after-leader'`
69
+ ([ADR 0246](../decisions/0246-a-command-stops-its-descendants-by-default.md)): when the leader
70
+ exits, the members it left in the group are stopped by the command's `stop` policy before the
71
+ pipes drain.
72
+
73
+ 1. Find the commands that start a background helper on purpose (a `&` in a shell script, a spawned
74
+ child the leader does not wait for) and declare `'leave'` on them:
75
+
76
+ ```ts
77
+ // before
78
+ await runNativeCommand({ executable, args, timeoutMs })
79
+ // after
80
+ await runNativeCommand({ executable, args, timeoutMs, descendants: 'leave' })
81
+ ```
82
+
83
+ 2. Remove an explicit `descendants: 'terminate-after-leader'`, or keep it: it is now the default.
84
+ 3. A command whose helper inherited stdout or stderr used to wait for that helper until its
85
+ deadline. It now completes when the leader exits; drop a workaround that killed the helper from
86
+ `onLeaderSettled`.
87
+
88
+ ### native packaging verifies published digests
89
+
90
+ **Who must act:** build scripts that call `createNativePackaging` (`stitchkit/files/packaging`) and
91
+ read `asset.sourcePath`, hash the addon themselves, switch over the refusal `state`/`code`, pass a
92
+ `platform` other than the literal `'darwin'`, or parse `node_modules/stitchkit/native-assets.json`.
93
+
94
+ The package now publishes the `size` and SHA256 of each Darwin addon, computed when Stitchkit is
95
+ built, in `native-assets.json` at `formatVersion: 2`. `createNativePackaging` reads each selected
96
+ addon once, compares it with that digest and returns the verified bytes. An installed addon that
97
+ differs refuses with `NATIVE_ASSET_DIGEST_MISMATCH`.
98
+
99
+ 1. Replace every read of `asset.sourcePath` with `asset.bytes`, and delete your own re-hash of the
100
+ addon: the result's `sha256` is the published digest and `bytes` already match it.
101
+
102
+ ```ts
103
+ // before
104
+ const bytes = readFileSync(asset.sourcePath)
105
+ if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256) throw …
106
+ copyFileSync(asset.sourcePath, destination)
107
+ // after
108
+ writeFileSync(destination, asset.bytes)
109
+ ```
110
+
111
+ 2. Handle the third refusal. A `switch` over `result.state` gains `'mismatch'`
112
+ (`code: 'NATIVE_ASSET_DIGEST_MISMATCH'`); treat it as a failed build, never as a missing addon.
113
+ It carries the differing `architecture` with `expected` (published) and `actual` (installed)
114
+ `{ size, sha256 }`; log both pairs.
115
+ 3. Pass the target platform as the literal `'darwin'`. `platform` is a closed enum, so a free
116
+ string (a target name read from configuration, `process.platform`) no longer type-checks, and
117
+ another platform throws instead of returning `unsupported`. Skip packaging for non-Darwin
118
+ targets before the call:
119
+
120
+ ```ts
121
+ // before
122
+ const native = createNativePackaging({ platform: target.platform, architecture, … })
123
+ if (native.state === 'unsupported') return
124
+ // after
125
+ if (target.platform !== 'darwin') return
126
+ const native = createNativePackaging({ platform: target.platform, architecture, … })
127
+ ```
128
+
129
+ `unsupported` now means only an architecture the package has no addon for.
130
+ 4. Call `createNativePackaging` once per build and pass the result (its `assets` and `plugin`)
131
+ where it is needed. Each call reads and hashes the selected addons again.
132
+ 5. If you read `native-assets.json` directly, read `assets.<arch>.path`, `.size` and `.sha256`
133
+ instead of a bare path per architecture; an architecture with no entry is not published.
134
+
135
+ ### exclusive locks publish their owner first
136
+
137
+ **Who must act:** callers of `withExclusiveLock` (`stitchkit/files`) that rely on a lock file with no
138
+ readable owner being taken by age, and anything that puts lock files on a filesystem without hard
139
+ links. Every other caller needs no change.
140
+
141
+ A lock is now written to a private `.lock-<id>.tmp` file in the lock's directory, then hard-linked to
142
+ the lock name. The name never exists without a complete owner record, so `ownerlessGraceMs` defaults
143
+ to `null` instead of 5 000 ms: a lock file with no readable owner is never taken because of its age.
144
+
145
+ 1. If you depended on age-based reclaim of an empty lock file left by an older writer, opt in:
146
+
147
+ ```ts
148
+ // before: an empty lock older than 5 s was taken by default
149
+ await withExclusiveLock(path, run)
150
+ // after: say so; only legacy empty files need it
151
+ await withExclusiveLock(path, run, { ownerlessGraceMs: 5_000 })
152
+ ```
153
+
154
+ 2. Keep lock directories on a filesystem that supports hard links (local ext4, xfs, APFS and the
155
+ like). `link` failing with `EPERM` or `ENOTSUP` fails the acquisition.
156
+ 3. Expect a `.lock-*.tmp` sibling only after a holder was killed between writing its record and
157
+ publishing it. It is not a lock; the next reclaim in that directory removes the ones whose owner
158
+ is gone on this machine. Exclude the pattern from directory-size caps or backups if you have any.
159
+
160
+ ### one runtime-tool declaration
161
+
162
+ **Who must act:** code that names `RuntimeToolExecution`, `RuntimeToolExecutionWithOutput`,
163
+ `RuntimeMcpToolDefinition`, `RuntimeMcpToolDefinitionWithOutput` or `ToolSurfaceDefinition`; code
164
+ that annotates a definition carrying `present` as `RuntimeToolDefinitionWithOutput<I, O>` from
165
+ `stitchkit/tools`; imports of `stitchkit/agent-runtime/harness-tools`; code that compares agent
166
+ control error codes; and callers that send a JSON array as text to a string-or-array tool field.
167
+ Definitions returned by `defineRuntimeTool` / `createRuntimeToolFactory`, native CLI `commands` and
168
+ contract tools need no change.
169
+
170
+ A runtime tool now has one declaration, `RuntimeToolDefinitionWithOutput` /
171
+ `RuntimeToolDefinitionWithoutOutput`, exported with the same name from `stitchkit/cli`,
172
+ `stitchkit/tools` and `stitchkit/tools/mcp`. It carries no SDK type: the presenters are the adapter's
173
+ extension, named as the fourth type argument. `RuntimeToolDefinition` is the one registration type.
174
+
175
+ 1. Replace each removed name.
176
+
177
+ | before | after |
178
+ |---|---|
179
+ | `RuntimeToolExecutionWithOutput<I, O>` (`stitchkit/cli`) | `RuntimeToolDefinitionWithOutput<I, O>` |
180
+ | `RuntimeToolExecution` (`stitchkit/cli`) | `RuntimeToolDefinition` |
181
+ | `RuntimeMcpToolDefinitionWithOutput<I, O>` (`stitchkit/tools/mcp`) | `RuntimeToolDefinitionWithOutput<I, O, undefined, RuntimeMcpToolPresenters<z.output<O>>>` |
182
+ | `RuntimeMcpToolDefinition` (`stitchkit/tools/mcp`) | `RuntimeToolDefinition` |
183
+ | `ToolSurfaceDefinition` (`stitchkit/tools`) | `ToolSurfaceProjection<RuntimeToolDefinition>` |
184
+
185
+ 2. In `stitchkit/tools`, a type that names a definition with presenters passes the presenter type:
186
+
187
+ ```ts
188
+ // before
189
+ const tool: RuntimeToolDefinitionWithOutput<typeof input, typeof output> = defineRuntimeTool({ …, present })
190
+ // after: let defineRuntimeTool infer it, or name the presenters
191
+ const tool = defineRuntimeTool({ …, present })
192
+ const named: RuntimeToolDefinitionWithOutput<typeof input, typeof output, undefined, RuntimeToolPresenters<z.output<typeof output>>> = tool
193
+ ```
194
+
195
+ 3. An inline handler in `runtimeTools: [...]` needs no wrapper: it is typed by the parsed object.
196
+ To type `input` from the schema, construct with `defineRuntimeTool` (or
197
+ `satisfies RuntimeToolDefinitionWithOutput<typeof input, typeof output>`) before registering.
198
+
199
+ 4. Replace `createAgentHarnessTools` (`stitchkit/agent-runtime/harness-tools`) with the call it made:
200
+
201
+ ```ts
202
+ // before
203
+ tools: createAgentHarnessTools(({ context }) => ({ services: [svc], context, lifecycle: auth }))
204
+ // after
205
+ import { composeToolLifecycle, mountAgent } from 'stitchkit/tools'
206
+ tools: ({ context, toolFenceLifecycle }) =>
207
+ mountAgent([svc], { context, lifecycle: composeToolLifecycle(auth, toolFenceLifecycle) })
208
+ ```
209
+
210
+ 5. Agent control codes: `ACCESS_DENIED` is `FORBIDDEN`, and `CONNECTION_CLOSED` is returned for a closed
211
+ connection. `AgentControlErrorCode` lists every code. `authorize` now runs once per request; after you
212
+ withdraw someone's access, call `binding.revoke(conversationId, matches?)` so an open stream is
213
+ re-authorized and ends with an `access-denied` delivery.
214
+
215
+ 6. A string-or-array field that receives `'["a","b"]'` now fails with `VALIDATION_ERROR` ("`paths` is a
216
+ list written as text"). Send the array itself. A string that is not a list the array member accepts
217
+ (`[preview].png`) is still one plain string.
218
+
219
+ ### flattened unions keep declaration order
220
+
221
+ **Who must act:** consumers that use `flattenUnionInput: true` and keep snapshot tests, golden
222
+ files or provider prompt caches of flattened tool schemas.
223
+
224
+ A flattened union now lists its properties, enum values and branch labels in the order they first
225
+ appear across the variants, not alphabetically. Declaration order often encodes priority, so the
226
+ model now reads the order you wrote. Alternatives of different kinds (`anyOf`) keep their
227
+ canonical order, independent of variant order.
228
+
229
+ 1. Regenerate every snapshot or golden file of a flattened tool listing (`buildToolManifest`,
230
+ `listToolNames`, MCP `tools/list`, Agent tool schemas) and review the diff: only ordering moves.
231
+ 2. Expect one provider prompt-cache miss per flattened tool after the upgrade.
232
+ 3. To keep an order you prefer, declare the variants and fields in that order.
233
+
234
+ ### connection limits are validated
235
+
236
+ **Who must act:** callers that pass `timeoutMs` or `maxResponseBytes` to a connection and relied
237
+ on an invalid value silently becoming the default.
238
+
239
+ `stitchkit/tools/connections` throws a `RangeError` when `timeoutMs` is not a positive integer
240
+ within timer range (at most 2 147 483 647) or `maxResponseBytes` is not a positive safe integer.
241
+ Before 0.104.0 a value of 0, a negative number, a fraction or an out-of-range number fell back to
242
+ the default without a message.
243
+
244
+ ```ts
245
+ // Before: { timeoutMs: 0 } meant "use the default" by accident.
246
+ // After: omit the option to take the default, or pass a real limit.
247
+ { timeoutMs: 30_000 }
248
+ ```
249
+
250
+ ### one canonical JSON serializer
251
+
252
+ **Who must act:** tests that call `serializeSurfaceValue` from `stitchkit/testing`, and code that
253
+ puts a value nested deeper than 100 levels into agent store events, archives or projections
254
+ through `canonicalAgentJson`.
255
+
256
+ `canonicalJson` from `stitchkit/primitives` is the package's one canonical serializer. It
257
+ admits dense arrays, plain objects, strings, booleans, `null` and finite numbers; an `undefined`
258
+ object member is omitted. It refuses everything else with a `CanonicalJsonError`, a `TypeError`
259
+ whose `reason` says why: `'depth'`, `'nodes'` and `'bytes'` are the limits you set, while
260
+ `'cycle'`, `'negative-zero'` and `'not-json'` say the value is not JSON data.
261
+
262
+ `serializeSurfaceValue` is removed. It produced the same bytes without limits:
263
+
264
+ ```ts
265
+ // Before
266
+ import { serializeSurfaceValue } from 'stitchkit/testing'
267
+ const text = serializeSurfaceValue(manifest)
268
+ // After: the limits default to 100 levels, 100 000 nodes and 1 MiB; raise them for a large manifest
269
+ import { canonicalJson } from 'stitchkit/primitives'
270
+ const text = canonicalJson(manifest, { maxBytes: 16 * 1024 * 1024 })
271
+ ```
272
+
273
+ `canonicalAgentJson` in `stitchkit/agent-runtime` is `canonicalJson` with the node and byte
274
+ limits at their maximum. A value both accepted has the same bytes as before, so stored digests
275
+ and archives still match: like `z.json()` before it, the store hashes `-0` as `0` and drops an
276
+ own `__proto__` member (the public `canonicalJson` refuses `-0` and keeps `__proto__`). What
277
+ changed is the refusal table: an `undefined` object member used to fail and is now omitted, and
278
+ a value nested deeper than 100 levels is now refused.
279
+
280
+ ```ts
281
+ // Before: canonicalAgentJson({ a: undefined }) threw a ZodError
282
+ // After: canonicalAgentJson({ a: undefined }) === '{}'
283
+ // canonicalAgentJson(nestedBeyond100Levels) throws CanonicalJsonError (reason 'depth')
284
+ ```
285
+
286
+ ### directory inbox entries
287
+
288
+ **Who must act:** producers that drop hard links into an inbox directory, and callers that pass
289
+ `accept` the output of a transforming schema.
290
+
291
+ A file dropped into the inbox directory with more than one link is set aside as `invalid`
292
+ (detail `managed file has multiple links`). Another name can still rewrite such a file after it
293
+ was validated, so it is never trusted. Write each entry as its own regular file: a temporary
294
+ name in the same directory, then a rename onto `<name>.json`.
295
+
296
+ `accept` takes the schema's input type, stores that input as given and parses it with the
297
+ schema on delivery. A schema without a transform has the same input and output type and needs
298
+ no change. With a transform, pass the input:
299
+
300
+ ```ts
301
+ const schema = z.object({ amount: z.string().transform(Number) })
302
+ // Before: accept({ source, key, entry: { amount: 42 } }) // typed as the output; `z.string()` refused it
303
+ // After: accept({ source, key, entry: { amount: '42' } }) // delivered as { amount: 42 }
304
+ ```
305
+
306
+ A name that is already in `rejected/` no longer stops the pass: identical bytes are the entry
307
+ already set aside, different bytes are kept as `rejected/<name>.<16 hex digits>`.
308
+
309
+ ### journal startup scan
310
+
311
+ **Who must act:** operators that read `status.recovery` right after `createDiagnosticJournal` to
312
+ find damaged rows in the middle of a file.
313
+
314
+ Opening a journal reads only the last line of the active file and of every retained
315
+ generation (`startupScan: 'tails'`, the default). A crash tears the end of a file, so a torn tail
316
+ is still reported and still rotated away, and opening no longer parses and validates every
317
+ line of every generation. It still reads each file once in large blocks to count lines, so the time
318
+ grows with the bytes retained, at read speed. Pass `startupScan: 'full'` to read and validate every line as before:
319
+
320
+ ```ts
321
+ // Before: every open parsed every line of every generation.
322
+ // After: the same full scan, on request.
323
+ const journal = await createDiagnosticJournal({ path, eventSchema, limits, startupScan: 'full' })
324
+ ```
325
+
326
+ ### journal startup quarantine
327
+
328
+ **Who must act:** operators of a journal that is a source of truth and must not start over
329
+ damaged retained files; code that constructs `DiagnosticJournalRecoveryError` or reads its
330
+ `recovery`; protocols that embed `DiagnosticJournalRecoveryStatusSchema`, and their readers.
331
+ A diagnostic journal needs no change.
332
+
333
+ Opening a journal no longer refuses to start over a torn active file with `maxFiles: 1`, a
334
+ generation name that is not a regular file or a retained file it cannot read. It renames the file
335
+ to `<file>.quarantined-<epoch>` beside the journal, starts, and lists the move in
336
+ `getStatus().recovery.quarantined`. Quarantined files are outside retention and never deleted;
337
+ every later open lists them again, without a `reason`, until you remove them.
338
+
339
+ 1. A journal whose damage must stop the process declares the old behaviour:
340
+
341
+ ```ts
342
+ // Before: refusing was the only behaviour.
343
+ await createDiagnosticJournal({ path, eventSchema, limits })
344
+ // After:
345
+ await createDiagnosticJournal({ path, eventSchema, limits, onStartupRefusal: 'fail' })
346
+ ```
347
+
348
+ 2. Branch on the refusal's `reason` instead of its message or error class. Every startup refusal
349
+ about retained evidence is a `DiagnosticJournalRecoveryError`, including the former plain
350
+ `Error` for a non-file generation and the raw `EACCES` for an unreadable one (now in `cause`):
351
+
352
+ ```ts
353
+ // Before: catch (error) { if (error instanceof DiagnosticJournalRecoveryError) use(error.recovery) }
354
+ // After:
355
+ catch (error) {
356
+ if (error instanceof DiagnosticJournalRecoveryError && error.reason === 'torn-without-retention-slot') use(error.recovery)
357
+ }
358
+ ```
359
+
360
+ `recovery` is present only for `torn-without-retention-slot`. The constructor takes one
361
+ options object: `new DiagnosticJournalRecoveryError({ reason, file, recovery?, cause? })`.
362
+
363
+ 3. A protocol that embeds `DiagnosticJournalRecoveryStatusSchema` carries the new optional
364
+ `quarantined` array (at most 32 entries, `reason` optional) and `quarantinedUnlisted` count.
365
+ The schema is `.strict()`: upgrade every reader that parses it with its own copy before a
366
+ writer on this version sends a status with either field, or bump your protocol version.
367
+
368
+ ### error constructors take named options
369
+
370
+ **Who must act:** code that constructs an `AppError` or an `ApiError` with anything beyond its
371
+ code, and classes that extend `AppError` and call `super(code, message, status, …)`. Code that
372
+ only throws the `notFound`, `badRequest`, `unauthorized`, `forbidden`, `conflict` and
373
+ `rateLimited` helpers, or only reads `code`, `status`, `details`, `hint`, `traceId`,
374
+ `retryable` and `cause` from a caught error, needs no change.
375
+
376
+ `AppError` (`stitchkit/contract`, `stitchkit/server`, `stitchkit/node`) and `ApiError`
377
+ (`stitchkit`) keep the code as the one positional argument. Every other value is a named
378
+ option, so a call site reads without the signature open. `new AppError(code)` and
379
+ `new ApiError(code)` are unchanged. The compiler finds every old call: a string or number
380
+ in the second position is a type error.
381
+
382
+ 1. `AppError`: move `message`, `status`, `details`, `hint`, `traceId` and `retryable` into one
383
+ object after the code. `status` defaults to 500 and `message` to the code.
384
+
385
+ ```ts
386
+ // before
387
+ throw new AppError('CONFLICT', 'Selection is stale', 409, { revision: 7 })
388
+ throw new AppError('BUSY', 'Try later', 503, undefined, undefined, undefined, true)
389
+ // after
390
+ throw new AppError('CONFLICT', { message: 'Selection is stale', status: 409, details: { revision: 7 } })
391
+ throw new AppError('BUSY', { message: 'Try later', status: 503, retryable: true })
392
+ ```
393
+
394
+ 2. A subclass passes the same object to `super`:
395
+
396
+ ```ts
397
+ // before
398
+ super('FEATURE_LOCKED', 'locked', 403)
399
+ // after
400
+ super('FEATURE_LOCKED', { message: 'locked', status: 403 })
401
+ ```
402
+
403
+ 3. `ApiError`: the old order was `code, status, details, message, hint, traceId, errorOptions,
404
+ retryable`. Name each value; `status` defaults to 0 and the former `ErrorOptions` argument
405
+ is now the `cause` option.
406
+
407
+ ```ts
408
+ // before
409
+ new ApiError('RATE_LIMITED', 429, undefined, 'slow down', 'wait', 'trace-1', { cause }, true)
410
+ new ApiError('VALIDATION_ERROR', 0, { issues }, 'bad input')
411
+ // after
412
+ new ApiError('RATE_LIMITED', { status: 429, message: 'slow down', hint: 'wait', traceId: 'trace-1', retryable: true, cause })
413
+ new ApiError('VALIDATION_ERROR', { status: 0, details: { issues }, message: 'bad input' })
414
+ ```
415
+
416
+ 4. Both classes accept the standard `cause`, which `error.cause` returns; omit the option to
417
+ leave the property absent.
418
+
12
419
  ## Released migration: 0.104.0
13
420
 
14
421
  JS bundles carrying Darwin native capabilities also need the
@@ -502,506 +909,3 @@ it; only a context built by hand needs it.
502
909
  hand before `bot.stop()`. A project that wrote its own `getUpdates`
503
910
  transformer for this (waiting on `app.admission` before returning a batch)
504
911
  deletes it; keeping both waits twice and is harmless.
505
-
506
- ## Released migration: 0.95.0
507
-
508
- One breaking change in `stitchkit/application`: the process lifecycle ledger
509
- counts downtime as the window nobody answered in, and gains the `draining`
510
- phase and the `startup-failed` outcome. A ledger file written by an earlier
511
- version loads unchanged — the new `unavailableAt` reads as `null`.
512
-
513
- 1. **Read downtime from the ready fact.** **Who must act:** a project that
514
- reads `downtimeMs` from a `started` fact. It is gone from `StartFact`; the
515
- number it carried is `processGapMs`. The downtime now arrives with the new
516
- run's readiness:
517
-
518
- ```ts
519
- // before
520
- ledger.subscribe((fact) => {
521
- if (fact.type === 'started') report(fact.previousExit, fact.downtimeMs);
522
- });
523
- // after
524
- ledger.subscribe((fact) => {
525
- if (fact.type === 'started') previousExit = fact.previousExit;
526
- if (fact.type === 'ready') report(previousExit, fact.downtimeMs);
527
- });
528
- ```
529
-
530
- 2. **Handle the new values.** **Who must act:** a project that switches
531
- exhaustively over `LifecycleTermination` (new: `draining`,
532
- `startup-failed`), `PreviousExit` (new: `startup-failed`) or
533
- `ProcessLifecycleFact` (new: `DrainingFact`, `type: 'draining'`). The type
534
- checker names each switch. An open run is `active` or `draining`; a check
535
- written as `termination === 'active'` to mean "still running" becomes
536
- `termination === 'active' || termination === 'draining'`.
537
- `TransitionReadyInput` is renamed `TransitionRunInput` — the same fields,
538
- now also taken by `transitionProcessDraining`; rename the import.
539
-
540
- 3. **Record the drain and the readiness when driving the ledger by hand.**
541
- **Who must act:** only a project that calls `createProcessLifecycleLedger`
542
- methods itself instead of mounting `lifecycleLedgerResource`. Call
543
- `recordDraining()` where the application stops admitting work, before it
544
- drains; without it downtime is counted from the recorded stop. Call
545
- `recordReady()` when the run serves; a run stopped without it now ends
546
- `startup-failed`, not `clean`.
547
-
548
- ## Released migration: 0.94.0
549
-
550
- One breaking minor that consolidates: tool options move into one group, and
551
- names that were exported from two entrypoints keep only their owner. Every step
552
- is an edit a type checker or `defineContract` points at; the few behaviour
553
- changes that need no edit are under Changed and Fixed in the changelog.
554
-
555
- 1. **Move endpoint tool options into `tool`.** **Who must act:** every project
556
- with an endpoint that sets `toolName`, `ui`, `annotations` or `mcp`. Run the
557
- codemod from a checkout of this repository, then typecheck:
558
-
559
- ```bash
560
- bun packages/core/scripts/codemod-tool-group.ts --check path/to/your/src # list
561
- bun packages/core/scripts/codemod-tool-group.ts path/to/your/src # rewrite
562
- ```
563
-
564
- ```ts
565
- // before
566
- { method: 'GET', path: '/', desc: 'List', toolName: 'user_list', mcp: { inputRequired } }
567
- // after
568
- { method: 'GET', path: '/', desc: 'List', tool: { name: 'user_list', mcp: { inputRequired } } }
569
- ```
570
-
571
- The codemod leaves two kinds of endpoint for a person and prints where: one
572
- built with a spread (`{ ...base, toolName }`), and one exposed on HTTP only,
573
- whose tool options never reached a tool — delete them there. Anything left
574
- over fails at startup, by name — a key moved into the group under its old
575
- name (`tool: { toolName }`) included:
576
-
577
- ```text
578
- Contract "users": endpoint "list" sets `toolName` — tool options live in `tool` since 0.94.0: use tool.name
579
- ```
580
-
581
- `expose` does not move. `MethodDef` — what hooks and runtime tools see — keeps
582
- `toolName`, `ui`, `annotations` and `mcp` at the top level, so hook code does
583
- not change. A tool view is declared only with `withToolView`, which now puts
584
- it in `tool.view`.
585
-
586
- 2. **Change imports of moved names.** **Who must act:** projects importing any
587
- of these; the compiler lists every site.
588
-
589
- | Name | From | To |
590
- |---|---|---|
591
- | `RuntimeToolTransport`, `ToolSurfaceTransport`, `ToolInvokerTransport` | `stitchkit/tools`, `stitchkit/tools/invoker`, `stitchkit/tools/connections` | `ToolTransport` from `stitchkit/contract` |
592
- | `createCli`, `defineCliCommand`, `CliConfig`, `CliSurfaceSource`, `CliPresentationPolicyConfig`, `CliWaitConfig`, `ExitCodeMap`, `CliCommand*` | `stitchkit/tools` | `stitchkit/cli` |
593
- | `parseSSE`, `ParseSSEOptions` | `stitchkit/server` | `stitchkit` |
594
- | `runAgentStoreConformance`, `AgentStoreConformance*`, `createAgentRace*`, `AgentRace*` | `stitchkit/testing` | `stitchkit/agent-runtime/testing` |
595
- | `createLocalStepDurability`, `LocalStepDurability`, `LocalStepDurabilityOptions`, `StepDurabilityLedger` | `stitchkit/agent-runtime` | `stitchkit/tools` |
596
-
597
- 3. **Regenerate a surface snapshot only if it reports drift.** **Who must act:**
598
- a project whose committed snapshot has an object with integer-like keys
599
- (`"9"`, `"10"`) — they are now sorted like every other key. Every other
600
- snapshot, CLI manifest signature and agent-store archive is byte-identical.
601
-
602
- ## Released migration: 0.93.0
603
-
604
- 1. **Regenerate every committed surface snapshot, once.** `manifestVersion` is
605
- `3` and each operation row gained `mcp` — the rounds it declares before it
606
- runs. Most rows get `null`, so the first diff is large and mechanical.
607
- **Who must act:** anyone with a committed snapshot. A snapshot on the older
608
- version is refused by name rather than failing a schema literal:
609
-
610
- ```text
611
- Surface snapshot is manifestVersion 2; this build writes 3. Regenerate it
612
- and review the diff — the format changed, so the first regeneration is
613
- expected to be large.
614
- ```
615
-
616
- ```ts
617
- // before — a declared round left no trace, so declaring or removing one
618
- // passed review invisibly
619
- manifest.operations[0]
620
- // after
621
- manifest.operations[0].mcp
622
- // null | { inputRequired: [{ key, message, schema }] }
623
- // | { inputRequired: 'resolved-per-call' }
624
- ```
625
-
626
- A project that kept its own test for "declared `inputRequired` implies a
627
- configured state key" can drop it: the snapshot holds that link now.
628
-
629
- 2. **Nothing else to do.** Native commands that declare `output` became
630
- invokable in process; a name that answered `NOT_FOUND` from
631
- `createCliInvoker` in 0.92.0 may now resolve. If an application relied on
632
- native commands being unreachable there, stop passing them to the invoker.
633
-
634
- ## Released migration: 0.92.0
635
-
636
- 1. **`RequestEvent.method`, `path` and `statusCode` are optional.** Work that did
637
- not arrive over a transport now records their **absence** instead of inventing
638
- values — a background loop used to write `method: 'AGENT'`, which is not a
639
- verb. **Who must act:** anyone whose `RequestEventSinkConfig.write` puts these
640
- into NOT NULL columns, or whose filter reads `event.method`.
641
-
642
- ```ts
643
- // before
644
- write: (event) => db.audit.create({ data: { method: event.method, path: event.path, status: event.statusCode } })
645
- // after — a job has none of the three; `kind` says which kind of row it is
646
- write: (event) => db.audit.create({ data: {
647
- kind: event.kind,
648
- name: event.name ?? null,
649
- method: event.method ?? null,
650
- path: event.path ?? null,
651
- status: event.statusCode ?? null,
652
- } })
653
- ```
654
-
655
- A filter written as `event.method !== 'GET'` keeps jobs (`undefined !== 'GET'`),
656
- which is usually what you want. To exclude them deliberately:
657
- `event.kind === 'request' && event.method !== 'GET'`.
658
-
659
- 2. **`applyCliUpdate` needs the manifest when given a trust root.** The signature
660
- covers the manifest and every asset digest together, so an asset alone cannot
661
- be verified. **Who must act:** only callers adopting signature verification;
662
- the unsigned call is unchanged.
663
-
664
- ```ts
665
- // before
666
- await applyCliUpdate({ asset })
667
- // after
668
- await applyCliUpdate({ asset, manifest: check.manifest, trust })
669
- ```
670
-
671
- 3. **An MCP continuation minted before this release is refused once.** The signed
672
- round state now carries a fingerprint of the question plan, checked on every
673
- round, so a plan that moved between rounds is a refusal rather than a
674
- different question asked under the previous one's key. A conversation already
675
- in flight at deploy time has no fingerprint and gets
676
- `INVALID_REQUEST_STATE`; the host re-asks and the second attempt succeeds.
677
- **Who must act:** nobody — but expect a small number of these in the minutes
678
- after a deploy, and do not read them as tampering.
679
-
680
- ## Released migration: 0.91.0
681
-
682
- 1. `STITCH_ERROR_STATUS` gained `NOT_IMPLEMENTED` (501). An **exhaustive** error
683
- vocabulary must name it — without the row `defineErrors({ mapping: { exhaustive:
684
- true } })` stops compiling and then throws at startup. Nothing else in this
685
- release requires action.
686
-
687
- ```ts
688
- // before
689
- map: { …, WAIT_TIMEOUT: 'timeout' }
690
- // after
691
- map: { …, NOT_IMPLEMENTED: 'not_implemented', WAIT_TIMEOUT: 'timeout' }
692
- ```
693
-
694
- The code is thrown only by the new dev-only `onMissingHandler: 'stub'` policy;
695
- a vocabulary that is not exhaustive is unaffected.
696
-
697
- ## Released migration: 0.90.0
698
-
699
- 1. `stitchkit.watch.value` is a discriminated union on `kind` — `full`, `delta`,
700
- `unchanged` — and every frame carries `fingerprint`. **Both ends must come from
701
- the same major.** A client older than this release reads a `delta` frame as a
702
- value of `undefined`, silently; upgrade the hub and the pages that talk to it
703
- together, or set `deltaMemoryBytes: 0` on the hub until they are.
704
-
705
- Applications using `createWatchClient` need no code change: it rebuilds the
706
- value and hands the listener the whole answer as before. Code that reads
707
- `WatchValueFrame.value` directly — a hand-written subscriber, a test double —
708
- narrows on `kind` first.
709
-
710
- ```ts
711
- // before: frame.value
712
- // after:
713
- if (frame.kind === 'full') hold(frame.value);
714
- else if (frame.kind === 'delta') hold(applyWatchDelta(held, frame.delta));
715
- // 'unchanged' leaves what you hold standing
716
- ```
717
-
718
- 2. `AttachedWatcher.open` takes an optional third argument, `have`. A custom
719
- server binding that forwards `stitchkit.watch.open` should pass the payload's
720
- `have` through; omitting it costs a whole value on every reconnection and is
721
- otherwise harmless.
722
-
723
- ## Released migration: 0.89.0
724
-
725
- 1. Custom `AgentRuntimeStore` adapters implement `seedConversationInput`; custom
726
- `AgentRuntimeStoreDriver` adapters implement `seeds.load` and `seeds.create` together
727
- with atomic, ordered history insertion. Use the maintained Prisma example and run
728
- `runAgentStoreConformance` against the adapter. Built-in memory and SQLite
729
- stores already implement the contract; SQLite migrates to schema version 3.
730
- 2. OpenRouter usage normalization consumes each completed SDK step's `usage`, including
731
- `raw`, rather than aggregate usage. Missing counters stay `unavailable`.
732
-
733
- ```ts
734
- // before: normalizeOpenRouterUsage(await result.usage, metadata)
735
- // after:
736
- for (const step of await result.steps) {
737
- const usage = normalizeOpenRouterUsage(step.usage, step.providerMetadata);
738
- // Persist the step's measured usage with its provenance.
739
- }
740
- ```
741
-
742
- 3. Durable step bodies return lossless JSON. Convert dates explicitly, reject invalid
743
- numbers upstream, and return `null` after an effect-only operation.
744
-
745
- ```ts
746
- await context.step('send', async () => {
747
- await sendIdempotently();
748
- return null;
749
- });
750
- ```
751
-
752
- First execution, replay and concurrent callers receive detached values. Persistence
753
- does not make external effects transactional; the host retains idempotency and recovery.
754
-
755
-
756
- How to move a consuming project from one stitchkit version to another — including
757
- across many versions at once (a project frozen on an old version, then jumped
758
- forward). The process is mechanical: stitchkit marks every breaking change in one
759
- place and one format, so you can recover the full migration from the version diff.
760
-
761
- ## Released migration: 0.88.0
762
-
763
- Two mechanical edits, and only if your project builds these values rather than only reading them.
764
-
765
- A completed step event now carries the provider's response identity, and the field is required:
766
-
767
- ```ts
768
- // before
769
- { type: 'step-finished', step: 0, usage }
770
-
771
- // after
772
- { type: 'step-finished', step: 0, usage, response: { id: generationId } }
773
- ```
774
-
775
- Every event the runtime emits already has it, so a sink that only reads them needs no change. A
776
- fixture, a test double or a pipeline that re-validates events persisted by an earlier version does:
777
- give the older records a response identity, or keep parsing them with the schema they were written
778
- under.
779
-
780
- If you implement the whole `AgentRuntime` interface — usually a mock — add the new method:
781
-
782
- ```ts
783
- // before
784
- const fake: AgentRuntime = { submit, interrupt, recover, close }
785
-
786
- // after
787
- const fake: AgentRuntime = { submit, interrupt, abandon, recover, close }
788
- ```
789
-
790
- No data migration is involved, and nothing in the store changes shape.
791
-
792
- ## Released migration: 0.87.0
793
-
794
- Only if your project implements `AgentConversationReader` itself. A message page
795
- now names which of its messages compaction removed:
796
-
797
- ```ts
798
- // before
799
- return { items, ...(nextCursor && { nextCursor }) }
800
-
801
- // after
802
- return { items, compacted: [], ...(nextCursor && { nextCursor }) }
803
- ```
804
-
805
- An empty array is the honest answer for a reader that pages only the active
806
- history; it is what the SQLite reader returns unless the caller asks for
807
- `includeCompacted`. Calling the reader needs no change, and no data migration is
808
- involved.
809
-
810
- ## Released migration: 0.86.0
811
-
812
- Only if you branch on `AgentTerminalReason` or show it to a person. Three
813
- failures that used to arrive as `provider_failure` now arrive as themselves:
814
-
815
- ```ts
816
- // before
817
- if (run.terminalReason === 'provider_failure') showOutage()
818
-
819
- // after
820
- switch (run.terminalReason) {
821
- case 'provider_failure':
822
- showOutage()
823
- break
824
- case 'storage_conflict':
825
- // Another writer took this run's revision, or the store refused the write.
826
- retryOrReport()
827
- break
828
- case 'output_rejected':
829
- // Your own `protocol.acceptTerminal` refused the finished message.
830
- reportRejectedAnswer()
831
- break
832
- case 'runtime_failure':
833
- // The runtime, or a check the SDK ran on what it was handed, failed
834
- // before the provider was reached.
835
- reportInternal()
836
- break
837
- }
838
- ```
839
-
840
- A run that never reached the provider can no longer report `provider_failure`,
841
- so an alert keyed on that value stops firing for causes that were never the
842
- provider's.
843
-
844
- If you implement `AgentRuntimeStore` directly, add the event-ledger surface:
845
-
846
- ```ts
847
- const store: AgentRuntimeStore = {
848
- // existing normalized operations
849
- appendEvent,
850
- readEvents,
851
- exportConversation,
852
- importConversation,
853
- }
854
- ```
855
-
856
- If you implement an `AgentRuntimeStoreDriver` (Prisma, Postgres, any external
857
- database), the ledger is yours to keep: `events` is a required member, and it
858
- needs a table.
859
-
860
- ```ts
861
- // before
862
- const driver: AgentRuntimeStoreDriver<Tx> = { transaction, head, runs, admissions, history, scanRecoverable }
863
-
864
- // after
865
- const driver: AgentRuntimeStoreDriver<Tx> = {
866
- transaction, head, runs, admissions, history, scanRecoverable,
867
- events: {
868
- // One row per event; `seq` is per conversation, starts at 1, and is
869
- // assigned inside the caller's transaction so concurrent appends cannot
870
- // share or skip a number.
871
- append: async (tx, draft) => {
872
- // Two transactions that both read max(seq) and both insert max + 1
873
- // collide on the primary key; under SERIALIZABLE the snapshot predates
874
- // any lock you take here. Treat a unique violation on this table as a
875
- // serialization conflict and re-run the whole transaction — see
876
- // `runTransaction` in examples/agent-store-prisma/adapter.ts.
877
- const seq = (await tx.event.aggregate({ _max: { seq: true }, where: { conversationId: draft.conversationId } }))._max.seq ?? 0
878
- await tx.event.create({ data: { ...draft, seq: seq + 1, payload: JSON.stringify(draft.payload) } })
879
- return { ...draft, seq: seq + 1 }
880
- },
881
- // Ordered by `seq`, bounded by `fromSeq`/`toSeq`/`limit`; `nextSeq` when
882
- // more remain.
883
- list: async (tx, input) => { /* see examples/agent-store-prisma/adapter.ts */ },
884
- },
885
- }
886
- ```
887
-
888
- The table carries `eventId` (unique), `conversationId`, `seq`, `schemaVersion`,
889
- `kind`, `occurredAt`, `ignorable` and `payload`. `runAgentStoreConformance`
890
- from `stitchkit/testing` now exercises it — contiguous `seq` under twenty
891
- concurrent appends, bounded reads, the `ignorable` flag, a byte-stable export
892
- — so run it against your driver before deploying. Expect one more insert per
893
- run operation (`runtime/transition`) and one per model step
894
- (`provider/request`) on the hot path; there is no opt-out.
895
-
896
- If you operate a built-in SQLite store file:
897
-
898
- 1. Back the file up. The first open by 0.86.0 migrates it to schema 2 in one
899
- transaction and there is no way back: a 0.85.x package refuses a migrated
900
- file with `Unsupported Stitchkit agent-runtime SQLite schema version 2`.
901
- 2. Check the build for FTS5 before deploying:
902
-
903
- ```sql
904
- SELECT sqlite_compileoption_used('ENABLE_FTS5');
905
- ```
906
-
907
- A build without it is refused at open with `requires FTS5 support`. On the
908
- machine this was verified, Bun 1.3 `bun:sqlite` and Node 24.18 `node:sqlite`
909
- both carry it; verify your own build rather than assuming — the table of
910
- supported runtimes says Node ≥ 22.5, and FTS5 there is not verified here.
911
- 3. Know what the migration wrote. Each existing conversation becomes one
912
- `runtime/baseline` event at `seq 1`, dated when the migration ran, with
913
- `asOf` inside the payload naming the last message it describes. Event
914
- search and projections address that conversation's pre-migration history as
915
- one snapshot at `seq 1`; event-level precision begins with the first event
916
- after the migration.
917
-
918
- If you subscribe to `AgentRuntimeEvent` and switch over `event.type`
919
- exhaustively, add the transient `attempt-reset` case — drop the run's
920
- transient text — or the reducer stops compiling; a reducer that ignores it
921
- keeps a failed attempt's partial text on screen.
922
-
923
- New in this release, not a migration: child runs cascade only when the manager
924
- is given to the runtime —
925
- `createAgentRuntime({ ...config, children: createSqliteAgentChildManager({ sqlite, spawn }) })`
926
- — and the SQLite companions take the store handle (`{ sqlite }`), whose
927
- `database` and `transaction` they share.
928
-
929
- ## Released migration: 0.85.0
930
-
931
- Only if you implement `AgentRuntimeStore` directly. Add the new durable
932
- operation mutation beside the existing run mutations:
933
-
934
- ```ts
935
- // before
936
- const store: AgentRuntimeStore = {
937
- checkpointRunAssistant,
938
- commitRunTerminal,
939
- // ...
940
- }
941
-
942
- // after
943
- const store: AgentRuntimeStore = {
944
- checkpointRunAssistant,
945
- recordRunOperation,
946
- commitRunTerminal,
947
- // ...
948
- }
949
- ```
950
-
951
- It must atomically replace `AgentRun.lastOperation`, increment the run revision
952
- and conversation snapshot version, and enforce the supplied owner, fencing
953
- token and expected revision. Adapters built with `createAgentRuntimeStore`
954
- already receive the reference implementation; no driver method is added.
955
-
956
- ## Released migration: 0.84.0
957
-
958
- Only if you call `createAgentCodingTools`.
959
-
960
- ```bash
961
- rg -n "createAgentCodingTools"
962
- ```
963
-
964
- **1. Backslashes are refused.** If anything in your system hands the tools a Windows-style path,
965
- it now gets `FORBIDDEN`. Convert to `/` at the boundary:
966
-
967
- ```ts
968
- // before: read_file({ path: 'src\\index.ts' }) // reached openat as two segments
969
- // after: read_file({ path: 'src/index.ts' })
970
- ```
971
-
972
- This is the half that matters most: until now a backslash bypassed your **required**
973
- `authorize({ operation, path })` callback, not just `authorizePath`. A rule denying
974
- `credentials/token.txt` refused that spelling and served — and overwrote — the same file spelled
975
- `credentials\token.txt`. If your policy denies specific files, re-read your audit logs for
976
- backslash spellings.
977
-
978
- **2. Denying a directory now denies everything under it — and a policy must admit every directory
979
- on the way to a file**, the way POSIX needs `+x` on each directory in a path.
980
-
981
- If your policy is a DENY-list, it now does what it read as:
982
-
983
- ```ts
984
- authorizePath: ({ path }) => path !== 'credentials'
985
- // before: hid `credentials` from glob, served `credentials/token.txt` to read_file
986
- // after: refuses `credentials/token.txt`, `credentials/nested/deep.txt` and writes under it
987
- ```
988
-
989
- If you relied on the old behaviour — a directory hidden from discovery but readable directly — you
990
- were relying on a defect, and the two halves of one callback disagreeing. Name the paths you
991
- actually want reachable instead.
992
-
993
- If your policy is an ALLOW-list, widen it to the directories it leads through, or nothing under
994
- them is reachable:
995
-
996
- ```ts
997
- // before: allowed src/index.ts, because only the leaf was ever asked
998
- authorizePath: ({ path }) => path.startsWith('src/')
999
- // after: `src` itself is asked first and refused, so the file is refused too
1000
- authorizePath: ({ path }) => path === 'src' || path.startsWith('src/')
1001
- ```
1002
-
1003
- The workspace root is not asked about — it is the boundary the tools already own.
1004
-
1005
- **3. The policy is called more often.** A path of depth N costs N+1 calls (`.`, then each ancestor,
1006
- then the leaf), outermost first, stopping at the first refusal. A policy that logs or meters will
1007
- see a different sequence, and will no longer see the leaf once an ancestor has refused.