stitchkit 0.103.13 → 0.104.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 (223) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/darwin-native.cjs +1 -3
  3. package/dist/agent-runtime/store-conformance-lifecycle.d.ts +10 -10
  4. package/dist/agent-runtime/store-conformance-support.d.ts +5 -5
  5. package/dist/application/directory-inbox-accept.d.ts +13 -0
  6. package/dist/application/directory-inbox-accept.d.ts.map +1 -0
  7. package/dist/application/directory-inbox-contract.d.ts +42 -5
  8. package/dist/application/directory-inbox-contract.d.ts.map +1 -1
  9. package/dist/application/directory-inbox-delivery.d.ts +14 -0
  10. package/dist/application/directory-inbox-delivery.d.ts.map +1 -0
  11. package/dist/application/directory-inbox-files.d.ts +28 -0
  12. package/dist/application/directory-inbox-files.d.ts.map +1 -0
  13. package/dist/application/directory-inbox-state.d.ts +14 -0
  14. package/dist/application/directory-inbox-state.d.ts.map +1 -0
  15. package/dist/application/directory-inbox.d.ts +2 -1
  16. package/dist/application/directory-inbox.d.ts.map +1 -1
  17. package/dist/application/file-state-store.d.ts +2 -8
  18. package/dist/application/file-state-store.d.ts.map +1 -1
  19. package/dist/application/state-store.d.ts +6 -1
  20. package/dist/application/state-store.d.ts.map +1 -1
  21. package/dist/browser/client-arguments.d.ts +4 -0
  22. package/dist/browser/client-arguments.d.ts.map +1 -0
  23. package/dist/browser/client-executors.d.ts.map +1 -1
  24. package/dist/browser/client.d.ts.map +1 -1
  25. package/dist/browser/contract-stream.d.ts.map +1 -1
  26. package/dist/browser/http.d.ts +2 -1
  27. package/dist/browser/http.d.ts.map +1 -1
  28. package/dist/browser/native-fetch.d.ts +8 -0
  29. package/dist/browser/native-fetch.d.ts.map +1 -0
  30. package/dist/contract/errors.d.ts +16 -12
  31. package/dist/contract/errors.d.ts.map +1 -1
  32. package/dist/contract/normalize.d.ts +5 -1
  33. package/dist/contract/normalize.d.ts.map +1 -1
  34. package/dist/contract/stream.d.ts +1 -0
  35. package/dist/contract/stream.d.ts.map +1 -1
  36. package/dist/entrypoints/agent-runtime/browser.js +3 -3
  37. package/dist/entrypoints/agent-runtime/coding-tools.js +6 -6
  38. package/dist/entrypoints/agent-runtime/harness-tools.js +8 -8
  39. package/dist/entrypoints/agent-runtime/harness.js +11 -11
  40. package/dist/entrypoints/agent-runtime/realtime.js +3 -3
  41. package/dist/entrypoints/agent-runtime/sandbox.js +6 -6
  42. package/dist/entrypoints/agent-runtime.js +13 -13
  43. package/dist/entrypoints/application/diagnostic-journal.js +1 -1
  44. package/dist/entrypoints/application/directory-inbox.js +424 -119
  45. package/dist/entrypoints/application/grammy.js +1 -1
  46. package/dist/entrypoints/application.d.ts +2 -2
  47. package/dist/entrypoints/application.d.ts.map +1 -1
  48. package/dist/entrypoints/application.js +7 -3
  49. package/dist/entrypoints/cli.d.ts +2 -0
  50. package/dist/entrypoints/cli.d.ts.map +1 -1
  51. package/dist/entrypoints/cli.js +757 -136
  52. package/dist/entrypoints/contract.js +3 -3
  53. package/dist/entrypoints/files/packaging.d.ts +3 -0
  54. package/dist/entrypoints/files/packaging.d.ts.map +1 -0
  55. package/dist/entrypoints/files/packaging.js +133 -0
  56. package/dist/entrypoints/files.js +10 -107
  57. package/dist/entrypoints/index.js +7 -7
  58. package/dist/entrypoints/node.js +8 -8
  59. package/dist/entrypoints/observability.js +2 -2
  60. package/dist/entrypoints/react.js +1 -1
  61. package/dist/entrypoints/remote.js +19 -6
  62. package/dist/entrypoints/server.d.ts +1 -1
  63. package/dist/entrypoints/server.d.ts.map +1 -1
  64. package/dist/entrypoints/server.js +23 -17
  65. package/dist/entrypoints/telegram.d.ts +1 -0
  66. package/dist/entrypoints/telegram.d.ts.map +1 -1
  67. package/dist/entrypoints/telegram.js +214 -98
  68. package/dist/entrypoints/testing.js +7 -7
  69. package/dist/entrypoints/tools/connections.d.ts +2 -2
  70. package/dist/entrypoints/tools/connections.d.ts.map +1 -1
  71. package/dist/entrypoints/tools/connections.js +427 -294
  72. package/dist/entrypoints/tools/invoker.js +6 -6
  73. package/dist/entrypoints/tools/mcp.d.ts +12 -0
  74. package/dist/entrypoints/tools/mcp.d.ts.map +1 -0
  75. package/dist/entrypoints/tools/mcp.js +45 -0
  76. package/dist/entrypoints/tools.d.ts +1 -0
  77. package/dist/entrypoints/tools.d.ts.map +1 -1
  78. package/dist/entrypoints/tools.js +82 -1147
  79. package/dist/entrypoints/tracking.js +1 -1
  80. package/dist/files/boundary.d.ts +2 -8
  81. package/dist/files/boundary.d.ts.map +1 -1
  82. package/dist/files/file-io.d.ts +0 -5
  83. package/dist/files/file-io.d.ts.map +1 -1
  84. package/dist/files/native-packaging-layout.d.ts +12 -0
  85. package/dist/files/native-packaging-layout.d.ts.map +1 -0
  86. package/dist/files/native-packaging.d.ts +72 -0
  87. package/dist/files/native-packaging.d.ts.map +1 -0
  88. package/dist/files/read.d.ts +2 -2
  89. package/dist/files/read.d.ts.map +1 -1
  90. package/dist/index-1a6jtnd9.js +88 -0
  91. package/dist/index-269zqj8m.js +77 -0
  92. package/dist/{index-r34mvse1.js → index-27a2pj09.js} +1 -1
  93. package/dist/{index-r956rndn.js → index-2hyavjjj.js} +3 -3
  94. package/dist/{index-m2xqkv55.js → index-2vnh05w4.js} +83 -14
  95. package/dist/{index-pw99k9h5.js → index-43p08g7t.js} +1 -1
  96. package/dist/{index-xwq9j1sf.js → index-4e0ah203.js} +9 -7
  97. package/dist/{index-qapzv4p5.js → index-5z6jn897.js} +1 -1
  98. package/dist/{index-ahvsksw5.js → index-6d9xaxw1.js} +1 -1
  99. package/dist/{index-8zfsgar0.js → index-6drst9sg.js} +11 -5
  100. package/dist/{index-x87j7csh.js → index-77nxk6ay.js} +2 -2
  101. package/dist/{index-dnn9z96q.js → index-a0zzh8ak.js} +3 -3
  102. package/dist/{index-kmpzj1cq.js → index-a3wma1hk.js} +1 -1
  103. package/dist/{index-j8aw2c0e.js → index-bakgedh8.js} +35 -7
  104. package/dist/index-bhb950qs.js +1145 -0
  105. package/dist/{index-tgzhq07n.js → index-cvda079a.js} +1 -1
  106. package/dist/{index-n2gre124.js → index-fa4wg312.js} +4 -4
  107. package/dist/{index-zp4jpkp0.js → index-fk18k56k.js} +2 -2
  108. package/dist/{index-afjvjyck.js → index-g2j7pnak.js} +2 -134
  109. package/dist/index-g95fk660.js +116 -0
  110. package/dist/{index-r5c4fedv.js → index-h2fhjqy7.js} +2 -1
  111. package/dist/{index-sybedc8b.js → index-hbd32c6a.js} +23 -94
  112. package/dist/{index-fyphpknh.js → index-hp9d77wk.js} +1 -1
  113. package/dist/index-jd470k6q.js +109 -0
  114. package/dist/{index-b2bz0kdm.js → index-kb8styz3.js} +4 -3
  115. package/dist/{index-0c419yx7.js → index-kemm0ej5.js} +12 -9
  116. package/dist/{index-db2spkv1.js → index-ptn0g0gj.js} +3 -3
  117. package/dist/{index-w575evhq.js → index-r4r70zmh.js} +1 -1
  118. package/dist/{index-12m2v2a9.js → index-s4evrmp5.js} +1 -1
  119. package/dist/{index-1khy8y7n.js → index-t1sr4aks.js} +3 -3
  120. package/dist/{index-45tdvqa3.js → index-wh0ecvyk.js} +1 -1
  121. package/dist/{index-n53mswt3.js → index-x928baz2.js} +8 -1
  122. package/dist/index-yk4g8805.js +133 -0
  123. package/dist/{index-ygkka6zn.js → index-ywppsaby.js} +28 -5
  124. package/dist/{index-9853ymwb.js → index-za6j7nwy.js} +18 -4
  125. package/dist/internal/bounded-file-read.d.ts +41 -0
  126. package/dist/internal/bounded-file-read.d.ts.map +1 -0
  127. package/dist/internal/exclusive-lock.d.ts +2 -0
  128. package/dist/internal/exclusive-lock.d.ts.map +1 -1
  129. package/dist/internal/with-exclusive-lock.d.ts +4 -2
  130. package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
  131. package/dist/telegram/broadcast-delivery.d.ts +23 -0
  132. package/dist/telegram/broadcast-delivery.d.ts.map +1 -0
  133. package/dist/telegram/broadcast-failure.d.ts +22 -0
  134. package/dist/telegram/broadcast-failure.d.ts.map +1 -0
  135. package/dist/telegram/broadcast-state.d.ts +6 -3
  136. package/dist/telegram/broadcast-state.d.ts.map +1 -1
  137. package/dist/telegram/broadcast.d.ts +10 -5
  138. package/dist/telegram/broadcast.d.ts.map +1 -1
  139. package/dist/tools/cli/format.d.ts.map +1 -1
  140. package/dist/tools/cli/invoke.d.ts +2 -2
  141. package/dist/tools/cli/invoke.d.ts.map +1 -1
  142. package/dist/tools/cli/publication-assets.d.ts +10 -0
  143. package/dist/tools/cli/publication-assets.d.ts.map +1 -0
  144. package/dist/tools/cli/publication-contract.d.ts +75 -0
  145. package/dist/tools/cli/publication-contract.d.ts.map +1 -0
  146. package/dist/tools/cli/publication-control.d.ts +4 -0
  147. package/dist/tools/cli/publication-control.d.ts.map +1 -0
  148. package/dist/tools/cli/publication-storage.d.ts +32 -0
  149. package/dist/tools/cli/publication-storage.d.ts.map +1 -0
  150. package/dist/tools/cli/publication.d.ts +35 -0
  151. package/dist/tools/cli/publication.d.ts.map +1 -0
  152. package/dist/tools/cli/update.d.ts.map +1 -1
  153. package/dist/tools/connections/define.d.ts.map +1 -1
  154. package/dist/tools/connections/error-projection.d.ts +4 -0
  155. package/dist/tools/connections/error-projection.d.ts.map +1 -0
  156. package/dist/tools/connections/errors.d.ts +24 -8
  157. package/dist/tools/connections/errors.d.ts.map +1 -1
  158. package/dist/tools/connections/limits.d.ts +11 -31
  159. package/dist/tools/connections/limits.d.ts.map +1 -1
  160. package/dist/tools/connections/mcp-client.d.ts +6 -4
  161. package/dist/tools/connections/mcp-client.d.ts.map +1 -1
  162. package/dist/tools/connections/mcp.d.ts.map +1 -1
  163. package/dist/tools/connections/openapi.d.ts.map +1 -1
  164. package/dist/tools/connections/operation-limits.d.ts +25 -0
  165. package/dist/tools/connections/operation-limits.d.ts.map +1 -0
  166. package/dist/tools/connections/sse-frames.d.ts +11 -2
  167. package/dist/tools/connections/sse-frames.d.ts.map +1 -1
  168. package/dist/tools/connections/sse-session.d.ts +15 -2
  169. package/dist/tools/connections/sse-session.d.ts.map +1 -1
  170. package/dist/tools/connections/types.d.ts +19 -1
  171. package/dist/tools/connections/types.d.ts.map +1 -1
  172. package/dist/tools/execute-result.d.ts.map +1 -1
  173. package/dist/tools/execute.d.ts.map +1 -1
  174. package/dist/tools/internal/observe-hook.d.ts +3 -0
  175. package/dist/tools/internal/observe-hook.d.ts.map +1 -0
  176. package/dist/tools/internal/surface-projector.d.ts +2 -1
  177. package/dist/tools/internal/surface-projector.d.ts.map +1 -1
  178. package/dist/tools/mcp/mount.d.ts +2 -2
  179. package/dist/tools/mcp/mount.d.ts.map +1 -1
  180. package/dist/tools/mcp/prepare.d.ts +4 -5
  181. package/dist/tools/mcp/prepare.d.ts.map +1 -1
  182. package/dist/tools/mcp/register.d.ts +4 -1
  183. package/dist/tools/mcp/register.d.ts.map +1 -1
  184. package/dist/tools/mcp/runtime-tools.d.ts.map +1 -1
  185. package/dist/tools/remote.d.ts.map +1 -1
  186. package/dist/tools/runtime-tool-execution.d.ts +65 -0
  187. package/dist/tools/runtime-tool-execution.d.ts.map +1 -0
  188. package/dist/tools/runtime-tool-mcp.d.ts +27 -0
  189. package/dist/tools/runtime-tool-mcp.d.ts.map +1 -0
  190. package/dist/tools/runtime-tool.d.ts +9 -60
  191. package/dist/tools/runtime-tool.d.ts.map +1 -1
  192. package/dist/tools/surface.d.ts +12 -14
  193. package/dist/tools/surface.d.ts.map +1 -1
  194. package/llms/application.2.txt +221 -44
  195. package/llms/application.3.txt +3 -1
  196. package/llms/application.txt +1 -1
  197. package/llms/cli.2.txt +144 -1
  198. package/llms/cli.txt +123 -104
  199. package/llms/contract.txt +1 -1
  200. package/llms/files-packaging.txt +27 -0
  201. package/llms/process.txt +63 -1
  202. package/llms/server.4.txt +4 -1
  203. package/llms/server.5.txt +1 -0
  204. package/llms/stitchkit.2.txt +14 -0
  205. package/llms/stitchkit.txt +6 -4
  206. package/llms/telegram.txt +64 -2
  207. package/llms/tools-connections.txt +3 -0
  208. package/llms/tools-mcp.txt +29 -0
  209. package/llms/tools.2.txt +59 -0
  210. package/llms/tools.3.txt +1 -0
  211. package/llms/tools.txt +11 -52
  212. package/llms/upgrading-0.37.0-0.44.0.txt +444 -0
  213. package/llms/{upgrading-0.44.0-0.60.0.txt → upgrading-0.44.0-0.61.0.txt} +140 -271
  214. package/llms/{upgrading-0.61.0-0.74.0.txt → upgrading-0.62.0-0.80.0.txt} +217 -140
  215. package/llms/{upgrading-0.75.0-0.103.0.txt → upgrading-0.81.0-0.104.0.txt} +154 -217
  216. package/llms/upgrading.txt +4 -4
  217. package/llms-full.txt +745 -60
  218. package/llms.txt +14 -12
  219. package/native-assets.json +8 -0
  220. package/package.json +12 -3
  221. package/dist/index-wykebpn9.js +0 -221
  222. package/llms/upgrading-0.37.0.txt +0 -176
  223. package/dist/{index-69nvze1q.js → index-pptyv13k.js} +3 -3
@@ -122,49 +122,101 @@ deadline holds `stop()` — and so the resource's `force()` — for that one cal
122
122
  so give the transport a timeout. Superseding a key that is being sent right now
123
123
  does not recall it: the send completes, and only its receipt is not written.
124
124
 
125
- `createDirectoryInbox` is the receiving side for another program on the same
126
- host: it drops entries — a release transition, say — into a directory, and
127
- the application takes each one at least once. It is a managed resource of its
128
- own (`createDirectoryInbox({ id, directory, schema, handle })`, imported from
129
- `stitchkit/application/directory-inbox` because it touches files; its types
130
- and schemas are in `stitchkit/application`) and publishes a handle with
131
- `flush()` and `state()`. The producer writes each entry
132
- atomically as `<name>.json` — a temporary name, then a rename; names that start
133
- with a dot are never read. Entries are delivered in name order, and only after
134
- `activate`: none reaches the application before it is ready. `stopAdmission`
135
- stops taking new ones, `drain` waits for the one in flight, and `force` aborts
136
- the `signal` its handler was given.
137
-
138
- Taken and done are separate durable records. Before `handle` runs, the entry
139
- is claimed with a lease (`leaseMs`, default five minutes); after it returns,
140
- a receipt replaces the claim and only then is the file removed. A process that
141
- died mid-delivery leaves a claim, and the entry is taken again once the lease
142
- runs out — at least once, not exactly once, so a handler that sends somewhere
143
- should carry the entry's `key` as its idempotency key. A process that died
144
- after the receipt leaves only the file, which the next pass removes without
145
- delivering it again. A handler that throws is retried after a backoff from one
146
- second doubling to five minutes; an entry that fails `schema`, is over
147
- `maxEntryBytes` or was taken `maxAttempts` times (default 20 — the count
148
- survives restarts, so an entry whose handling kills the process is not taken
149
- forever) is moved to `rejected/` with its reason and reported to `onRejected`,
150
- and the entries after it keep flowing. The state lives in
151
- `<directory>/.inbox-state.json` under `createFileStateStore`'s inter-process
152
- lock unless a `store` is supplied.
153
-
154
- These primitives depend on the structural `StateStore`. On a server,
155
- `createFileStateStore` supplies the shared Zod-validated JSON adapter with an
156
- inter-process lock, unique temporary file, fsync and atomic rename. The lock
157
- is a file with a heartbeat: the holder refreshes its mtime every third of
158
- `staleLockMs` (default 3 s), a contender waits up to `lockTimeoutMs` (default
159
- 10 s), and the stale bound must sit inside the timeout — otherwise a crashed
160
- holder blocks every update until the lock ages out, which the constructor
161
- refuses. A lock whose heartbeat is stale is reclaimed once its recorded pid is
162
- gone; a live or unverifiable pid (a reused number, another user's process)
163
- keeps it for ten stale bounds — and never fewer than thirty missed heartbeats,
164
- so a scheduler stall on a loaded host cannot pull a live lock from under its
165
- holder — after which the heartbeat wins and the lock is abandoned. Temporary files a crashed writer left beside the state are swept on
166
- the store's first update. Ledger corruption may be declared reconstructable;
167
- an outbox must fail closed rather than silently discard pending delivery.
125
+ `createDirectoryInbox` receives entries from another program on the same host,
126
+ or directly from a transport adapter. Import the resource from
127
+ `stitchkit/application/directory-inbox`; its contracts live in
128
+ `stitchkit/application`. Its published handle has `accept({ source, key, entry })`,
129
+ `flush()` and `state()`:
130
+
131
+ ```ts
132
+ const inbox = createDirectoryInbox({
133
+ id: 'updates',
134
+ directory: env.INBOX_DIRECTORY,
135
+ schema: UpdateSchema,
136
+ handle: ({ identity, entry, signal }) => processUpdate(identity, entry, signal),
137
+ })
138
+ const ingress = defineManagedResource({
139
+ id: 'ingress',
140
+ dependsOn: [inbox],
141
+ start(context) {
142
+ const intake = context.use(inbox)
143
+ return { value: (key: string, entry: Update) =>
144
+ intake.accept({ source: 'client-updates', key, entry }) }
145
+ },
146
+ })
147
+ ```
148
+
149
+ `accept` is available after the resource starts, including before activation.
150
+ It validates the entry and full producer identity, writes a lossless JSON envelope,
151
+ syncs its file and parent directory, then returns
152
+ `{ status: 'accepted' | 'duplicate', filename }`. Automatic delivery starts at
153
+ `activate`; an explicit `flush()` is available before activation. A producer
154
+ should acknowledge its provider only after acceptance resolves. Failure after
155
+ publication can mean the entry was accepted but its acknowledgement was lost:
156
+ retry the same identity, and `duplicate` keeps the original payload.
157
+
158
+ The identity is the pair `source` (1–128 characters) and `key` (1–1024 characters).
159
+ A hash of the pair names the file; the full pair is retained inside its envelope
160
+ and checked against existing files and records. Two sources may use the same key.
161
+ The handler's legacy `key` remains the filename; programmatic entries additionally
162
+ carry `identity`. No Telegram numeric update id or MTProto event shape is invented.
163
+
164
+ The default `maxPendingEntries: 1000` refuses new programmatic entries when the
165
+ pending directory is full, without publishing another file; duplicate acceptance
166
+ still succeeds. `maxEntryBytes` defaults to 1 MiB and counts the complete UTF-8
167
+ envelope. A completed identity is deduplicated while its receipt is retained
168
+ (`retain`, default 1000); a rejected envelope also deduplicates while its file
169
+ remains in `rejected/`. Choose the retention and rejected-file cleanup policy to
170
+ match the provider's replay window. Provider cursors, identity extraction and
171
+ external-effect idempotency remain application responsibilities.
172
+
173
+ The original file-producer path also remains: atomically publish
174
+ `<name>.json`, using a temporary name followed by a rename. Names starting with
175
+ a dot are ignored; `intake-<64 hex characters>.json` is reserved for programmatic
176
+ envelopes. Entries are delivered in filename order. The reader refuses unsafe
177
+ leaf symlinks, multiple links and files changed during reading.
178
+ `stopAdmission` stops new acceptance and delivery, `drain` waits for accepted
179
+ storage operations and the handler in flight, and `force` aborts its handler
180
+ signal. A handler ignoring that signal may still finish an external effect;
181
+ the inbox leaves its entry unsettled rather than claiming it was cancelled.
182
+
183
+ Taken and done are separate durable records. Before `handle`, the entry is
184
+ claimed with a unique expiring lease (`leaseMs`, default five minutes). A
185
+ long handler renews that lease; an expired or replaced lease cannot complete,
186
+ reject, retry or remove the successor's file. Success first commits a receipt,
187
+ then removes only that file generation under a new protected transaction.
188
+ A crash after receipt commit is recovered by cleanup without redelivery. A crash
189
+ before receipt commit may redeliver: the guarantee is at least once, so use the
190
+ full producer identity as the external idempotency key.
191
+
192
+ A throwing handler retries after a backoff from one second to five minutes.
193
+ Schema failures, oversize files and `maxAttempts` exhaustion (default 20) are
194
+ recorded before their files move to `rejected/`, with `onRejected` reporting the
195
+ reason. The state defaults to `<directory>/.inbox-state.json`, or uses the supplied
196
+ `store`. Manual file producers are responsible for atomic publication and capacity.
197
+
198
+ These primitives share the structural `StateStore`. `update` owns one atomic
199
+ read/modify/write transaction and passes a `StateStoreUpdateContext` as the
200
+ transition's second argument. Its `await context.assertHeld()` checks that the
201
+ transaction is still active and its exclusive generation is still owned. A custom
202
+ store must implement both real serialization/atomic commit and this guard;
203
+ a no-op guard does not satisfy the contract. Ordinary one-argument transition
204
+ callbacks remain valid.
205
+
206
+ On a server, `createFileStateStore` uses the same process-instance lock as
207
+ `withExclusiveLock`, unique staging files and atomic directory-durable publication.
208
+ `lockTimeoutMs` (default 10 s, zero means one attempt) bounds acquisition only.
209
+ A live or unattributable owner is never replaced by age, including after an event
210
+ loop stall or `SIGSTOP`; only a provably dead local process instance is reclaimed.
211
+ Legacy readable pid/token records without a host are unknown owners and need
212
+ operator recovery after the old writer is verified stopped.
213
+
214
+ `staleLockMs` and `retryMs` are removed and are refused at runtime. The current
215
+ write removes its own staging file; unknown historical temporary files are
216
+ preserved, without an age sweep. Directory durability requires a filesystem
217
+ supporting directory sync (Linux and macOS); Windows is refused explicitly.
218
+ Ledger corruption may be declared reconstructable; an outbox must fail closed
219
+ rather than discard pending delivery.
168
220
 
169
221
  ## Recovering change subscriptions
170
222
 
@@ -232,7 +284,7 @@ description: Executable patterns for moving database, poller, queue and operatio
232
284
  type: architecture
233
285
  status: active
234
286
  created: 2026-08-24
235
- updated: 2026-08-24
287
+ updated: 2026-10-04 13:28 +07:00
236
288
  ---
237
289
 
238
290
  # Application migration recipes
@@ -266,6 +318,131 @@ The application still owns the client, ORM configuration, transactions,
266
318
  migrations and reconnect policy. The executable recipe deliberately fails after
267
319
  allocation and proves one rollback close.
268
320
 
321
+ ## Stateful clients, including MTProto
322
+
323
+ An application-owned client is an ordinary managed resource. Keep SDK creation,
324
+ credentials, authorization and reconnect policy in the application; no Telegram
325
+ client SDK is required by Stitchkit. Create a fresh client inside each `start`,
326
+ publish it after connection/readiness succeeds, and share one memoized destroy
327
+ promise between partial-start cleanup, `close` and `force`:
328
+
329
+ ```ts
330
+ let generation: { destroy(): Promise<void> } | undefined
331
+
332
+ const client = defineManagedResource({
333
+ id: 'client',
334
+ async start({ signal }) {
335
+ const connection = createClient() // Your SDK and configuration.
336
+ let destruction: Promise<void> | undefined
337
+ const destroy = () => destruction ??= Promise.resolve()
338
+ .then(() => connection.destroy())
339
+ generation = { destroy }
340
+ let ready = false
341
+ let startFailure: unknown
342
+ let startFailed = false
343
+ let cleanupFailure: unknown
344
+ let cleanupFailed = false
345
+ try {
346
+ await connection.connect(signal)
347
+ await connection.assertReady()
348
+ signal.throwIfAborted()
349
+ ready = true
350
+ } catch (error) {
351
+ startFailure = error
352
+ startFailed = true
353
+ } finally {
354
+ if (!ready) {
355
+ try { await destroy() }
356
+ catch (cleanup) {
357
+ cleanupFailure = cleanup
358
+ cleanupFailed = true
359
+ }
360
+ }
361
+ }
362
+ if (startFailed) {
363
+ if (cleanupFailed) throw new AggregateError([startFailure, cleanupFailure],
364
+ 'Client startup and cleanup failed', { cause: startFailure })
365
+ throw startFailure
366
+ }
367
+ return { value: connection }
368
+ },
369
+ close: () => generation?.destroy(),
370
+ force: () => generation?.destroy(),
371
+ })
372
+
373
+ const api = defineManagedResource({
374
+ id: 'api',
375
+ dependsOn: [client],
376
+ start(context) {
377
+ const connection = context.use(client)
378
+ return { value: async () => {
379
+ const lease = context.admission.acquire()
380
+ if (!lease) throw new ApplicationAdmissionError()
381
+ try { return await connection.invoke() }
382
+ finally { lease.release() }
383
+ } }
384
+ },
385
+ })
386
+ ```
387
+
388
+ `createClient` supplies a client whose `destroy` safely cleans partial connection
389
+ setup; its adapter must honor cancellation or otherwise reconcile late SDK effects.
390
+ The destroy cache belongs to one generation and is reset by the next `start`.
391
+ Successful startup keeps the connection alive. Dependants start after readiness,
392
+ requests acquire admission before invoking the client, and dependent HTTP resources
393
+ close before the client. A subtree restart builds a new client and new dependants.
394
+
395
+ The packed-consumer fixture
396
+ [`managed-client-composition.mjs`](../../packages/core/scripts/consumer-lane/fixtures/minimal/src/managed-client-composition.mjs)
397
+ proves readiness/admission, partial startup, close order, forced cleanup and restart
398
+ with a fake client on Bun and Node. It does not claim a live provider acceptance test.
399
+
400
+ ## Custom StateStore transaction guards
401
+
402
+ Transitions now receive `(current, context)`. Keep ordinary callbacks that only use
403
+ `current`; update custom store implementations to pass a real protected context.
404
+ For an in-memory test store, queue transactions and invalidate the context when
405
+ the transaction finishes:
406
+
407
+ ```ts
408
+ function memoryStore<T>(): StateStore<T> {
409
+ let state: T | null = null
410
+ let tail: Promise<unknown> = Promise.resolve()
411
+ return {
412
+ read: async () => state,
413
+ update(transition) {
414
+ const work = tail.then(async () => {
415
+ let active = true
416
+ const context: StateStoreUpdateContext = {
417
+ async assertHeld() {
418
+ if (!active) throw new Error('Transaction is no longer active')
419
+ },
420
+ }
421
+ try {
422
+ const next = await transition(state, context)
423
+ await context.assertHeld()
424
+ state = next.state
425
+ return next.result
426
+ } finally { active = false }
427
+ })
428
+ tail = work.catch(() => undefined)
429
+ return work
430
+ },
431
+ }
432
+ }
433
+ ```
434
+
435
+ This recipe is process-local and has no restart durability. A database store must
436
+ guard its real transaction/generation and commit atomically; an escaped or lost
437
+ guard must refuse. Filesystem mutations made inside inbox transactions call
438
+ `assertHeld` immediately before publication or cleanup.
439
+
440
+ For `createFileStateStore`, remove `staleLockMs` and `retryMs` from configuration.
441
+ `lockTimeoutMs` remains the acquisition budget. Timestamp age no longer revokes
442
+ a live or unknown writer. Stop and verify old writers before recovering a legacy
443
+ pid-only lock; do not invent owner metadata. Historical temporary files are no
444
+ longer deleted by age. See the [durable intake contract](application-kernel.md#durable-process-facts-and-owner-notifications).
445
+
269
446
  ## Long-running poller
270
447
 
271
448
  Return separate readiness and completion promises. Readiness says dependants may
@@ -36,7 +36,9 @@ cutovers are covered by the executable
36
36
  | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
37
37
  | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
38
38
  | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
39
- | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | managed resource delivering each `<name>.json` entry another program drops into a directory to the application at least once, after readiness; separate durable taken/done records, leases, bounded retry |
39
+ | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | one managed directory inbox for files or durable `accept({ source, key, entry })`; first payload wins, bounded capacity/bytes/receipts and renewable fenced leases; delivery begins after readiness and effects remain at least once |
40
+ | `DirectoryInboxIdentitySchema` / `DirectoryInboxIdentity` / `DirectoryInboxAccept` / `DirectoryInboxAcceptResultSchema` / `DirectoryInboxAcceptResult` | schema / _type_ | validated source/key, typed entry and durable accepted/duplicate result with filename; duplicates do not replace the first payload |
41
+ | `StateStoreUpdateContext` | _type_ | real active protected transaction: `assertHeld()` refuses after ownership loss or completion; custom adapters supply this context to their transition |
40
42
  | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
41
43
  | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
42
44
  | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
@@ -19,7 +19,7 @@ description: Compose process-local resources, schedules, readiness, drain and op
19
19
  type: architecture
20
20
  status: active
21
21
  created: 2026-08-23
22
- updated: 2026-09-30 17:41 +07:00
22
+ updated: 2026-10-04 13:28 +07:00
23
23
  ---
24
24
 
25
25
  # Managed application kernel
package/llms/cli.2.txt CHANGED
@@ -5,7 +5,146 @@
5
5
 
6
6
  Parts of this slice:
7
7
  - `llms/cli.txt`: Guide: CLI (docs/guide/cli.md)
8
- - `llms/cli.2.txt`: API reference: `stitchkit/cli` (docs/api/reference.md)
8
+ - `llms/cli.2.txt`: Guide: CLI (docs/guide/cli.md) · API reference: `stitchkit/cli` (docs/api/reference.md)
9
+
10
+ ==============================================================================
11
+ # Guide: CLI (docs/guide/cli.md) (continued)
12
+ ==============================================================================
13
+
14
+ ## Commands discovered from a running server
15
+
16
+ A CLI compiled from contracts carries the surface of the build it was compiled
17
+ from. One built from discovery carries the surface the server has *right now* —
18
+ which matters, because a long-lived MCP client freezes schemas at connect time
19
+ and then refuses the server's own newer fields:
20
+
21
+ ```ts
22
+ const discovered = await mountConnections([
23
+ defineMcpClientConnection({
24
+ name: 'api',
25
+ transport: { url },
26
+ token: () => key,
27
+ transports: ['CLI'], // the opt-in: this server's tools are commands
28
+ limits: {
29
+ discovery: { timeoutMs: 30_000, maxResponseBytes: 4 * 1024 * 1024 },
30
+ call: { timeoutMs: 120_000 },
31
+ },
32
+ }),
33
+ ])
34
+ await createCli({ name: 'myapp', version, runtimeTools: discovered, commands: [...] })
35
+ ```
36
+
37
+ `transports` is where the opt-in belongs: a whole server becomes a set of
38
+ commands, and a connection without it still contributes nothing to the CLI —
39
+ exposure stays explicit, as it is everywhere else in the framework.
40
+
41
+ One unconvertible schema no longer takes the connection down with it. The tool
42
+ is skipped and **named** (`onSkippedTool`, or a stderr line by default), so a
43
+ surface of two hundred tools is not lost to one.
44
+
45
+ MCP connection limits follow the operation phase. `initialize`, the initialized
46
+ notification, `tools/list` and legacy endpoint discovery use `limits.discovery`;
47
+ `tools/call` uses `limits.call`. A mounted call's handshake still uses discovery
48
+ limits. Each logical operation has one deadline, including initial transport
49
+ negotiation and response-body reading. Legacy endpoint readiness has a discovery
50
+ deadline; the lifetime of an established SSE stream does not.
51
+
52
+ Omitted phase fields inherit the existing connection-level `timeoutMs` and
53
+ `maxResponseBytes`, whose defaults are 30 seconds and 1 MiB. Values must be positive
54
+ safe integers; `timeoutMs` must also fit the runtime timer range. The byte ceiling
55
+ counts raw UTF-8 body bytes, including SSE framing, regardless of `Content-Length`.
56
+ Pending legacy calls also count comments and wrong-id frames. `budget.maxTools`
57
+ and `budget.maxSchemaBytes` remain separate mount limits.
58
+
59
+ Only the initial `initialize` can negotiate HTTP-to-legacy fallback on 400, 404
60
+ or 405. A failed `tools/call` is not replayed. Caller cancellation preserves its
61
+ original reason for in-process observers.
62
+
63
+ Expected local failures produce safe tool errors: `CONNECTION_TIMEOUT` (exit 7),
64
+ `CONNECTION_RESPONSE_TOO_LARGE` (exit 1), `UNAUTHORIZED` (exit 2), `FORBIDDEN`
65
+ (exit 3) or `CONNECTION_REQUEST_FAILED` (exit 1). Deadline and response-limit
66
+ failures include fixed `operation`/`phase`, the applicable bound and measured
67
+ `observedReadBytes`. The public `ConnectionTimeoutError` and
68
+ `ConnectionResponseTooLargeError` classes carry the same diagnostics. Raw URL,
69
+ credential, body, identity and stack stay with the original in-process cause in
70
+ `onToolError` / `afterToolCall`; tool errors produce no process console dump.
71
+ Unknown throws remain `INTERNAL_SERVER_ERROR`. A timeout is not proof that a
72
+ write did not happen: verify the remote outcome before repeating an operation.
73
+
74
+ **A remote refusal keeps its code.** A failed `tools/call` used to become a
75
+ one-sentence `Error` with the result discarded, which cost three things at once:
76
+ the code (so `exitCodes` had nothing to map and every remote failure exited `1`),
77
+ the message the operator needed, and the error's own class — a plain `Error` is
78
+ an *unexpected* error to the runner, so it printed a code frame of the framework
79
+ bundle before the JSON failure and was then scrubbed to `INTERNAL_SERVER_ERROR`.
80
+ A structured `{ error, details }` body is now relayed as the contract error it
81
+ is, on every transport; anything else fails as `UPSTREAM_TOOL_ERROR` carrying
82
+ what the server did send. Not `INTERNAL_SERVER_ERROR`, because nothing of ours
83
+ broke.
84
+
85
+ **On the CLI a discovered command prints the answer, not the envelope.**
86
+ `tools/call` returns `{ content: [...], structuredContent? }`, and an agent mount
87
+ needs exactly that — the parts are what a model is shown. The CLI is different in
88
+ kind, because the handler's value is what gets printed, piped and aggregated:
89
+ handed the envelope, `--count-by status` groups the *content parts* and answers
90
+ `no record carries the field "status" — available: text, type`. So the CLI
91
+ transport unwraps, and only it: `structuredContent` when the server sent one, a
92
+ lone text part when it parses as JSON, its text when it does not, and `null` when
93
+ there are no parts at all — a tool without an output contract answers that way,
94
+ and the `_meta` beside an empty `content` is protocol metadata, never the answer.
95
+ Several parts, an image or audio pass through whole — picking one of many would
96
+ be inventing an answer.
97
+
98
+ ## Auth parity
99
+
100
+ A scoped command is guarded by the same `createAuthHook` your HTTP server uses —
101
+ pass it as `lifecycle`, and inject the identity through `context` so
102
+ `resolveFromContext` can read it:
103
+
104
+ ```ts
105
+ const authHook = createAuthHook({ /* resolve, resolveFromContext, rules */ })
106
+
107
+ await createCli({
108
+ name: 'myapp',
109
+ version: '1.0.0',
110
+ resolveAuth: () => resolveIdentityFromToken(process.env.MYAPP_TOKEN),
111
+ context: (identity) => ({ user: identity }), // resolveFromContext reads this
112
+ lifecycle: { beforeHandle: authHook }, // same policy; HTTP wires it as authorize
113
+ services,
114
+ })
115
+ ```
116
+
117
+ Without `lifecycle`, a scoped command runs **unguarded** — the scope check lives
118
+ entirely inside the `createAuthHook` result, so with no hook wired in there is
119
+ nothing to enforce a method's `scope`. This matches the MCP / agent surfaces
120
+ exactly ([ADR 0014](../decisions/0014-tool-http-parity.md)): on every tool
121
+ transport the auth gate is opt-in, so a contract with scoped methods **must** be
122
+ given a `lifecycle` (and `context` identity) to be protected. The gate only
123
+ *fails closed* once the hook **is** present but `resolveFromContext` is missing —
124
+ then a scoped call has no identity and is rejected.
125
+
126
+ ## Typed context
127
+
128
+ Use `createToolkit<AppContext>()` to type the injected `context` against your
129
+ app's context shape — the tool-side mirror of `createImplement`
130
+ ([ADR 0017](../decisions/0017-typed-tool-context.md)):
131
+
132
+ ```ts
133
+ const tools = createToolkit<{ user: User }>()
134
+ await tools.createCli({
135
+ name: 'myapp',
136
+ version: '1.0.0',
137
+ services,
138
+ context: (identity) => ({ user: identity }), // checked against { user: User }
139
+ })
140
+ ```
141
+
142
+ ## Remaining boundary
143
+
144
+ File-upload (`multipart`) contract endpoints remain CLI-invisible, the same as
145
+ on MCP/Agent: their wire body is not a JSON tool form. Model file-oriented
146
+ application behavior as a managed pathless command, or binary-only behavior as
147
+ a native command. Streaming (SSE) output is not yet piped to stdout.
9
148
 
10
149
  ==============================================================================
11
150
  # API reference: `stitchkit/cli` (docs/api/reference.md)
@@ -33,6 +172,7 @@ SDK nor the `ai` peer.
33
172
  | `checkCliUpdate` | function | bounded, interval-limited, never-throwing check for a newer published build |
34
173
  | `applyCliUpdate` | function | download, verify the decompressed digest and replace the binary by rename |
35
174
  | `assertCliPublishable` | function | refuse republishing one version from a different commit — takes one manifest or every manifest already published, because the promise a version makes does not stop at a channel boundary |
175
+ | `publishCli` / `CliPublicationOptions` / `CliPublicationPhase` / `CliPublicationResult` | function / _type_ | optional bounded builder/admission over one immutable name/version/commit stamp; commit verified assets before the manifest pointer, recover exact completed versions, retain verified history; existing signatures/installer/updater own the wire contract |
36
176
  | `rollbackCliUpdate` / `CliRollbackConfig` / `RolledBackCliUpdate` | function / _types_ | put the kept previous build back, digest-checked first and written by the same atomic rename the update uses |
37
177
  | `signCliManifest` / `verifyCliManifest` / `cliManifestSigningPayload` | function | Ed25519 over `{name, version, commit, builtAt, assets[]}` — every asset's digest included, so the chain closes on the file that executes |
38
178
  | `cliSignatureAccepted` / `CliSignatureVerdict` | function / _type_ | `valid` \| `unenforced` \| `missing` \| `unknown-key` \| `invalid`; `unenforced` keeps an unpinned build updating while making the absence of a check visible |
@@ -64,6 +204,9 @@ SDK nor the `ai` peer.
64
204
  | `CliConfig` | _type_ | config for `createCli`; `defaultCommand`, `globalOptions`, `optionAliases` and `positionals` define the shared command presentation policy |
65
205
  | `CliPresentationPolicyConfig` | _type_ | shared command presentation-policy subset of `CliConfig` |
66
206
  | `CliSurfaceSource` | _type_ | static service/runtime array or identity-dependent factory |
207
+ | `RuntimeToolExecution` | _type_ | neutral heterogeneous runtime-tool registration; handlers execute through the schema-validating managed runner |
208
+ | `RuntimeToolExecutionWithOutput` | _type_ | strict neutral managed definition with schema-derived handler input/output; construct before adding to CLI `runtimeTools` |
209
+ | `RuntimeToolDefinitionWithoutOutput` | _type_ | strict neutral managed void definition without an output contract or transport presenters |
67
210
  | `CliCommandDefinition` | _type_ | native command definition union |
68
211
  | `CliCommandDefinitionBase` | _type_ | native command name, description and input schema |
69
212
  | `CliCommandDefinitionWithOutput` | _type_ | native command with validated declared output and typed optional `present` / successful `exitCode` callbacks |