stitchkit 0.95.3 → 0.96.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 (52) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/application/grammy-bot.d.ts +44 -0
  3. package/dist/application/grammy-bot.d.ts.map +1 -0
  4. package/dist/application/grammy-update-admission.d.ts +35 -0
  5. package/dist/application/grammy-update-admission.d.ts.map +1 -0
  6. package/dist/application/grammy.d.ts +23 -4
  7. package/dist/application/grammy.d.ts.map +1 -1
  8. package/dist/application/kernel-admission.d.ts +11 -0
  9. package/dist/application/kernel-admission.d.ts.map +1 -1
  10. package/dist/application/kernel-resource-context.d.ts.map +1 -1
  11. package/dist/application/resource.d.ts +22 -0
  12. package/dist/application/resource.d.ts.map +1 -1
  13. package/dist/entrypoints/application/grammy.d.ts +2 -1
  14. package/dist/entrypoints/application/grammy.d.ts.map +1 -1
  15. package/dist/entrypoints/application/grammy.js +139 -5
  16. package/dist/entrypoints/application.d.ts +1 -1
  17. package/dist/entrypoints/application.d.ts.map +1 -1
  18. package/dist/entrypoints/application.js +1 -1
  19. package/dist/entrypoints/observability.d.ts +1 -0
  20. package/dist/entrypoints/observability.d.ts.map +1 -1
  21. package/dist/entrypoints/observability.js +46 -4
  22. package/dist/entrypoints/telegram.d.ts +4 -0
  23. package/dist/entrypoints/telegram.d.ts.map +1 -1
  24. package/dist/entrypoints/telegram.js +693 -88
  25. package/dist/entrypoints/testing.js +1 -1
  26. package/dist/{index-w906953s.js → index-5qn1kx34.js} +48 -0
  27. package/dist/observability/json-logger.d.ts +38 -0
  28. package/dist/observability/json-logger.d.ts.map +1 -0
  29. package/dist/telegram/bot-api.d.ts +50 -0
  30. package/dist/telegram/bot-api.d.ts.map +1 -0
  31. package/dist/telegram/broadcast-state.d.ts +67 -0
  32. package/dist/telegram/broadcast-state.d.ts.map +1 -0
  33. package/dist/telegram/broadcast.d.ts +93 -0
  34. package/dist/telegram/broadcast.d.ts.map +1 -0
  35. package/dist/telegram/local-files.d.ts +63 -0
  36. package/dist/telegram/local-files.d.ts.map +1 -0
  37. package/dist/telegram/operator-channel.d.ts +78 -0
  38. package/dist/telegram/operator-channel.d.ts.map +1 -0
  39. package/llms/application-grammy.txt +5 -0
  40. package/llms/application.2.txt +1 -0
  41. package/llms/application.txt +51 -13
  42. package/llms/observability.txt +5 -0
  43. package/llms/server.4.txt +3 -0
  44. package/llms/stitchkit.txt +5 -5
  45. package/llms/telegram.txt +162 -3
  46. package/llms/{upgrading-0.37.0-0.53.0.txt → upgrading-0.37.0-0.56.0.txt} +130 -3
  47. package/llms/{upgrading-0.55.0-0.68.0.txt → upgrading-0.57.0-0.69.0.txt} +44 -130
  48. package/llms/{upgrading-0.69.0-0.95.0.txt → upgrading-0.70.0-0.96.0.txt} +40 -44
  49. package/llms/upgrading.txt +3 -3
  50. package/llms-full.txt +268 -23
  51. package/llms.txt +10 -9
  52. package/package.json +1 -1
package/llms/server.4.txt CHANGED
@@ -386,6 +386,9 @@ and our payload is wrong, which is the case a list of substrings quietly counts
386
386
  against the user. An unrecognised refusal leaves the recipient reachable: losing
387
387
  a working subscriber forever costs more than one wasted send.
388
388
 
389
+ The rest of what a bot is assembled from — its resources, journal, operator
390
+ channel, broadcasts and local Bot API files — is in [Telegram bots](telegram.md).
391
+
389
392
  ## Cookies
390
393
 
391
394
  ```ts
@@ -69,7 +69,7 @@ own, recorded as an ADR.
69
69
  | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
70
70
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
71
71
  | `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
72
- | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification and Bot API send-failure classification |
72
+ | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification, send-failure classification, resumable broadcasts, the operator channel and local Bot API files |
73
73
  | `stitchkit/tracking` | browser **and** server | evolving | visitor-tracking mechanics — `createTrackingClient`, the tab-shared outbox, the page-leave beacon, attribution, the contract factory; no event vocabulary, no React |
74
74
  | `stitchkit/tracking/server` | server (Bun or Node) | evolving | the decisions a tracking backend makes — dispositions, visit lease over an application-owned store, active intervals, presence; no database |
75
75
  | `stitchkit/release` | browser **and** server | evolving | a page follows the release it was built for — `createReleaseMarker` on the server, `createReleaseWatcher` in the browser, the `X-Build-Id` header and a socket event between them |
@@ -77,11 +77,11 @@ own, recorded as an ADR.
77
77
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
78
78
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
79
79
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
80
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 40 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
80
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 41 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
81
81
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
82
82
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
83
83
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
84
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 21 of the 40 minors since 0.56.2, most recently 0.94.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
84
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 21 of the 41 minors since 0.56.2, most recently 0.94.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
85
85
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
86
86
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
87
87
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -91,8 +91,8 @@ own, recorded as an ADR.
91
91
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
92
92
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
93
93
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
94
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 8 of the 40 minors since 0.56.2, most recently 0.95.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
95
- | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
94
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 9 of the 41 minors since 0.56.2, most recently 0.96.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
95
+ | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
96
96
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
97
97
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
98
98
  | `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
package/llms/telegram.txt CHANGED
@@ -3,7 +3,155 @@
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: API reference: `stitchkit/telegram` (docs/api/reference.md)
6
+ Contains: Guide: Telegram bots (docs/guide/telegram.md) · API reference: `stitchkit/telegram` (docs/api/reference.md)
7
+
8
+ ==============================================================================
9
+ # Guide: Telegram bots (docs/guide/telegram.md)
10
+ ==============================================================================
11
+
12
+ # Telegram bots
13
+
14
+ `stitchkit/telegram` holds what the Bot API itself asks of every bot, with no
15
+ bot library; `stitchkit/application/grammy` makes a grammY bot a set of
16
+ application resources. Together they are what a bot is assembled from, so that
17
+ a bot's own code is its product and nothing else. A generated bot to start from:
18
+ `bun create stitchkit my-bot --template telegram-bot`. Why it is shaped this
19
+ way: [ADR 0201](../decisions/0201-a-telegram-bot-is-assembled-from-primitives.md).
20
+
21
+ | Job | Where |
22
+ |---|---|
23
+ | the bot as resources: command menu, then long polling with updates admitted by batch | `grammyBotResources` — [application kernel](application-kernel.md#optional-grammy-adapter) |
24
+ | the process journal | `createJsonLogger` from `stitchkit/observability` |
25
+ | the operator's chat: product events, apart from the journal | `createTelegramOperatorChannel` |
26
+ | a broadcast that survives crashes, deploys and Ctrl-C | `runTelegramBroadcast` |
27
+ | a file the local Bot API server wrote to disk | `createTelegramLocalFiles` |
28
+ | why a send was refused | `classifyTelegramSendFailure` — [auth and errors](auth-and-errors.md#telegram-mini-apps) |
29
+ | Mini App `initData` | `verifyTelegramInitData` — [auth and errors](auth-and-errors.md#telegram-mini-apps) |
30
+ | one Bot API call without a bot library | `callTelegramBotApi` |
31
+
32
+ ## The journal
33
+
34
+ A bot's journal is `createJsonLogger`: one JSON object per line on standard
35
+ output, in pino's shape — numeric `level` (`debug` 20, `info` 30, `warn` 40,
36
+ `error` 50), `time` in epoch milliseconds, `msg` — so `pino-pretty` reads it on
37
+ a terminal and a supervisor matching `"level":50` sees errors. Unlike pino, an
38
+ `Error` under *any* key is written with its name, message, stack and cause; an
39
+ error logged as `{ error }` does not become `"error":{}`.
40
+
41
+ ```ts
42
+ import { createJsonLogger } from 'stitchkit/observability'
43
+ import { TELEGRAM_BOT_TOKEN_PATTERN } from 'stitchkit/telegram'
44
+
45
+ export const logger = createJsonLogger({
46
+ level: env.LOG_LEVEL,
47
+ fields: { service: 'my-bot' },
48
+ sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
49
+ })
50
+
51
+ logger.error('Application startup failed', { error })
52
+ ```
53
+
54
+ The error line contract, for whatever recognises a failure from the journal: a
55
+ line whose `level` is `50` or more is an error. There is no separate `fatal`;
56
+ an unrecoverable end is an `error` line followed by a non-zero exit.
57
+
58
+ ## The operator channel
59
+
60
+ The chat where people read about new users, payments and failures is not the
61
+ journal. The journal must not lose a line to a slow network; the chat must not
62
+ slow the bot down. `post` returns at once and never throws; sends are paced
63
+ below Telegram's per-chat limit (one every 3 s by default), 429s wait the time
64
+ Telegram named, and when the chat cannot keep up the oldest message is dropped
65
+ and reported.
66
+
67
+ ```ts
68
+ import { createTelegramOperatorChannel, telegramOperatorSender } from 'stitchkit/telegram'
69
+
70
+ const operators = createTelegramOperatorChannel<'users' | 'payments' | 'errors'>({
71
+ chatId: env.OPERATOR_CHAT_ID,
72
+ topics: { users: 2, payments: 3, errors: 4 }, // forum message_thread_id
73
+ send: telegramOperatorSender({ token: env.BOT_TOKEN }),
74
+ onDropped: (drop) => logger.warn('Operator message dropped', { ...drop }),
75
+ })
76
+
77
+ operators.post(`New user ${user.id}`, 'users')
78
+ ```
79
+
80
+ Every message is masked before it leaves: a bot token always — bots post their
81
+ errors here, and an error about a local Bot API file carries the token in its
82
+ path — plus any `sensitivePatterns` the bot adds; `telegramOperatorSender` also
83
+ masks its own exact token. What to post stays the bot's. On the way down, drain it within the grace period
84
+ and close it: `drain: (context) => operators.drain(context.signal)`,
85
+ `close: () => operators.close()` in a resource.
86
+
87
+ ## Broadcasts
88
+
89
+ A broadcast is a job with an end, so it is a function, not a resource:
90
+
91
+ ```ts
92
+ import { runTelegramBroadcast, telegramBroadcastSender } from 'stitchkit/telegram'
93
+
94
+ const report = await runTelegramBroadcast({
95
+ name: '2026-04-28-price-lock',
96
+ directory: env.STATE_DIRECTORY, // a state root, never the release tree
97
+ recipients: () => audience.activeSubscribers(),
98
+ send: telegramBroadcastSender({
99
+ token: env.BOT_TOKEN,
100
+ message: { copyFrom: { chatId: env.DRAFTS_CHAT_ID, messageId: 42 } },
101
+ }),
102
+ signal: stop.signal,
103
+ onProgress: (progress) => logger.info('Broadcast', { ...progress }),
104
+ })
105
+ ```
106
+
107
+ - **Resumable by name.** The audience is written once on the first run; running
108
+ the same name again continues with that list. Progress is an append-only
109
+ journal beside it, one line before and one after each send.
110
+ - **Nobody twice.** A send that was in flight when the process died is recorded
111
+ `uncertain` and not repeated. A signal stops between sends, after the one in
112
+ flight is recorded.
113
+ - **Refusals by meaning.** A blocked, deactivated or never-started recipient is
114
+ `unreachable` and never addressed again. A 429 waits Telegram's
115
+ `retry_after` and sends the same recipient again; a server error is retried
116
+ with backoff up to `maxAttempts`, then `failed`. A message Telegram cannot
117
+ parse is not the recipient's fault: the run **halts** with them still pending,
118
+ so the run after the fix reaches them. So does Telegram being unreachable.
119
+ - **Pacing** is `ratePerSecond`, 25 by default. `dryRun` counts and writes
120
+ nothing. One runner per name: a second concurrent run is refused.
121
+
122
+ The same call works from a script (the process's signal) and inside a live
123
+ application (the application's signal, with admission held by the caller).
124
+ The recipient state contains Telegram ids: keep it in the state directory, out
125
+ of git.
126
+
127
+ ## Files from a local Bot API server
128
+
129
+ A local `telegram-bot-api` keeps downloaded files under `<dir>/<bot token>/`,
130
+ and a bot sharing that directory reads them from disk and deletes them when
131
+ done. `file_path` comes from a server over the network, so it is resolved only
132
+ inside the bot's directory — `../`, an absolute path elsewhere and a link
133
+ pointing out are refused, judged on real paths.
134
+
135
+ ```ts
136
+ import { createTelegramLocalFiles } from 'stitchkit/telegram'
137
+
138
+ const files = createTelegramLocalFiles({ root: env.BOT_API_FILES_ROOT, token: env.BOT_TOKEN })
139
+
140
+ const { file_path } = await ctx.getFile()
141
+ const path = await files.resolve(file_path) // throws TelegramLocalFileError
142
+ // … process it …
143
+ await files.remove(file_path)
144
+ ```
145
+
146
+ The recommended variable for the root is **`BOT_API_FILES_ROOT`** — the
147
+ server's `--dir`, absolute, as the bot's process sees it. A relative `file_path`
148
+ (a server without `--local`) and an absolute one inside the bot's directory (a
149
+ server with `--local`) both resolve. A refusal carries a reason
150
+ (`root-not-absolute`, `bot-directory-unavailable`, `outside-bot-directory`,
151
+ `missing`, `not-a-file`) and never the path or the token. `files.check()`
152
+ answers whether the directory is there to read and delete from; a resource that
153
+ throws on `{ ready: false }` in `start` keeps the bot from answering before it
154
+ can read its media.
7
155
 
8
156
  ==============================================================================
9
157
  # API reference: `stitchkit/telegram` (docs/api/reference.md)
@@ -11,9 +159,10 @@ Contains: API reference: `stitchkit/telegram` (docs/api/reference.md)
11
159
 
12
160
  ## `stitchkit/telegram`
13
161
 
14
- Peer-free server-only Telegram platform primitives. Importing this resolves no
162
+ Peer-free server-only Telegram platform primitives — the Bot API's own
163
+ protocols and the jobs every bot rewrote around them. Importing this resolves no
15
164
  bot library; `stitchkit/application/grammy` remains the lifecycle adapter for an
16
- injected grammY bot. → ADR 0143
165
+ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
17
166
 
18
167
  | Export | Kind | Summary |
19
168
  |--------|------|---------|
@@ -26,5 +175,15 @@ injected grammY bot. → ADR 0143
26
175
  | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
27
176
  | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
28
177
  | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
178
+ | `runTelegramBroadcast` | function | a resumable broadcast by `name` under a state `directory`: audience written once, an append-only journal, pacing at `ratePerSecond` (25), 429 waits `retry_after`, unreachable recipients never addressed again, a `message-invalid` or unreachable Telegram halts without charging the recipient, a send in flight at a crash is `uncertain` and not repeated, `dryRun`, one runner per name |
179
+ | `TelegramBroadcastConfig` / `TelegramBroadcastSend` / `TelegramBroadcastReport` / `TelegramBroadcastRunOutcome` / `TelegramBroadcastOutcome` / `TelegramBroadcastRecipient` | _type_ | `name`, `directory`, `recipients()`, `send({ recipient, attempt })`, `maxAttempts`, `signal`, `onProgress`; counts `delivered` / `unreachable` / `failed` / `uncertain` / `pending` and `finished` / `stopped` / `halted` / `dry-run` |
180
+ | `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
181
+ | `createTelegramOperatorChannel` | function | the operator chat, apart from the journal: `post(text, topic?)` returns at once and never throws; paced sends into forum topics, 429 honoured, oldest dropped on overflow, `drain(signal)` and `close()` for the way down |
182
+ | `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`, `sensitivePatterns` (a bot token is always masked); a drop is `overflow` / `refused` / `attempts` / `closed` with its classification |
183
+ | `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode` |
184
+ | `createTelegramLocalFiles` | function | files the local Bot API server wrote under `<root>/<token>/`: `resolve(file_path)` and `remove(file_path)` only inside the bot's directory, judged on real paths so a link is judged by where it leads; `check()` for readiness. Recommended root variable: `BOT_API_FILES_ROOT` |
185
+ | `TelegramLocalFilesConfig` / `TelegramLocalFiles` / `TelegramLocalFilesCheck` / `TelegramLocalFileError` / `TelegramLocalFileRefusal` | _type_ / class | `root`, `token`; refusals `root-not-absolute` / `bot-directory-unavailable` / `outside-bot-directory` / `missing` / `not-a-file`, never carrying the path or the token |
186
+ | `callTelegramBotApi` / `TelegramBotApiCall` / `TelegramBotApiError` | function / _type_ / class | one Bot API method over `fetch`; a refusal keeps `error_code`, `description` and `parameters` for the classifier, and no error names the URL |
187
+ | `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
29
188
 
30
189
  ---
@@ -1,14 +1,141 @@
1
- # Upgrading 0.37.0 → 0.53.0 — stitchkit docs slice
1
+ # Upgrading 0.37.0 → 0.56.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.37.0 → 0.53.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.37.0 → 0.56.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.37.0 → 0.53.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.37.0 → 0.56.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.56.0
13
+
14
+ ### Surface manifests are version 2
15
+
16
+ `operation.tools` could not describe a role-selected MCP surface, a different
17
+ Agent or CLI selection, or an advertised `extend` schema, so projections moved
18
+ out of the canonical operation:
19
+
20
+ ```ts
21
+ // before
22
+ manifest.operations[0].tools.MCP
23
+ // after
24
+ manifest.toolSurfaces.find((s) => s.transport === 'MCP' && s.surface === null)?.tools
25
+
26
+ // before
27
+ buildSurfaceManifest({ mcpSurfaces: { admin: { services, extend } } })
28
+ // after
29
+ buildSurfaceManifest({ mcpSurfaces: { admin: { services } }, mcpPreparation: { extend } })
30
+ ```
31
+
32
+ A committed snapshot is regenerated once, deliberately: `manifestVersion` is
33
+ `2`, and `ConformanceTransport` gained `REALTIME`, so any exhaustive
34
+ `Record<ConformanceTransport, …>` must handle it.
35
+
36
+ ### `FILE_*` codes joined the error registry
37
+
38
+ `StitchErrorCode` gained `FILE_INVALID_PATH`, `FILE_OUTSIDE_ROOT`,
39
+ `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_INSPECTION_REJECTED`,
40
+ `FILE_TOO_LARGE` and `FILE_EXISTS`. Only exhaustive maps break:
41
+
42
+ ```ts
43
+ // before — compiled while the registry had no file codes
44
+ const copy: Record<StitchErrorCode, string> = { …, RATE_LIMITED: '…' }
45
+ // after — add the seven managed-file codes (or use Partial<Record<…>>)
46
+ ```
47
+
48
+ Unexpected IO stays scrubbed as `INTERNAL_SERVER_ERROR`: the new codes are the
49
+ caller-safe ones only.
50
+
51
+ ### `ScopedAuthHook` is nominal
52
+
53
+ A hand-written function shaped like an auth hook is no longer assignable —
54
+ identity now comes from the factory, so scope ownership and the inferred
55
+ context cannot drift apart:
56
+
57
+ ```ts
58
+ // before — a structural stand-in
59
+ const auth: ScopedAuthHook<Scopes> = async (ctx, endpoint) => { … }
60
+ // after — create it, then compose domains
61
+ const auth = createAuthHook({ resolve, rules })
62
+ const composed = composeAuthHooks({ hooks: [auth, billingAuth], defaultScope: 'public' })
63
+ ```
64
+
65
+ ### Managed-file inspectors also run on reads
66
+
67
+ An inspector is no longer write-only, and it has a finite default deadline
68
+ (15 s). Make it read-aware and idempotent — a read carries no
69
+ `declaredMediaType`:
70
+
71
+ ```ts
72
+ // before
73
+ inspect: ({ declaredMediaType }) => inspectDeclaredType(declaredMediaType!)
74
+ // after
75
+ inspect: ({ prefix, declaredMediaType, signal }) =>
76
+ inspectBytes(prefix, { declaredMediaType, signal })
77
+ ```
78
+
79
+ Set `inspectionTimeoutMs` explicitly when 15 seconds is the wrong budget.
80
+
81
+ ### Direct async-operation binding needs a wire-stable ID
82
+
83
+ A direct binding reuses the start output as the follow-up wire input, so the ID
84
+ schema must parse to itself (`z.input` equals `z.output`, no transform,
85
+ coercion, default or overwrite). Anything else is parsed twice:
86
+
87
+ ```ts
88
+ // before — a transform silently ran on start and again on every follow-up
89
+ defineAsyncOperationContract({ binding: 'direct', id: z.string().transform(Number) })
90
+ // after — keep the wire shape, adapt explicitly
91
+ defineAsyncOperationContract({
92
+ binding: 'adapted',
93
+ id,
94
+ adapters: { idFromStart, inputFor },
95
+ })
96
+ ```
97
+
98
+ ## Released migration: 0.55.0
99
+
100
+ ### Peer-free `implementRemote`
101
+
102
+ `implementRemote` now has one canonical, optional-peer-free owner. This keeps
103
+ MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
104
+
105
+ ```ts
106
+ // before
107
+ import { implementRemote } from 'stitchkit/tools'
108
+ // after
109
+ import { implementRemote } from 'stitchkit/remote'
110
+ ```
111
+
112
+ ### Managed file boundary and strict auth returns
113
+
114
+ Create one boundary during application bootstrap and pass the capability, never
115
+ a per-call directory or host path:
116
+
117
+ ```ts
118
+ import { createManagedFileBoundary } from 'stitchkit/files'
119
+
120
+ const files = await createManagedFileBoundary({ root: '/srv/app-files' })
121
+
122
+ // before
123
+ defineDownloadTool({ defaultDir: '/srv/app-files', resolveUrl, ...common })
124
+ defineUploadTool({ upload: (path) => provider.uploadFile(path), ...common })
125
+ defineViewFileTool({ baseDir: '/srv/app-files', ...common })
126
+
127
+ // after
128
+ defineDownloadTool({ files, resolveUrl, ...common })
129
+ defineUploadTool({ files, upload: ({ bytes }) => provider.upload(bytes), ...common })
130
+ defineViewFileTool({ files, ...common })
131
+ ```
132
+
133
+ Downloaded `path` is now relative to the boundary and MIME metadata is named
134
+ `mediaType`. Update raw `mountDownload`/`mountUpload`/`mountViewFile` configs the
135
+ same way. Auth predicates must explicitly return `true`, `false`, or a plain
136
+ object of context fields; replace accidental `undefined` fallthroughs with the
137
+ intended boolean.
138
+
12
139
  ## Released migration: 0.53.0
13
140
 
14
141
  ### Realtime `emit` returns `boolean` instead of `void`
@@ -1,14 +1,55 @@
1
- # Upgrading 0.55.0 → 0.68.0 — stitchkit docs slice
1
+ # Upgrading 0.57.0 → 0.69.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.55.0 → 0.68.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.57.0 → 0.69.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.55.0 → 0.68.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.57.0 → 0.69.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.69.0
13
+
14
+ ### Direct coding-tool operation names
15
+
16
+ `createAgentCodingTools` no longer prefixes durable operation identity with `coding_`, and the
17
+ unguarded exact-string edit tool is gone. Update approval maps, presenters and tests together:
18
+
19
+ ```ts
20
+ // before
21
+ { coding_read_file: 'approved', coding_search: 'approved', coding_patch_file: 'user-approval' }
22
+
23
+ // after
24
+ { read_file: 'approved', search_files: 'approved', apply_patch: 'user-approval' }
25
+ ```
26
+
27
+ The full mapping is `coding_read_file → read_file`, `coding_write_file → write_file`,
28
+ `coding_search → search_files`, `coding_patch_file → apply_patch`, `coding_shell → run_command`,
29
+ `coding_read_artifact → read_output` and `harness_read_resource → read_resource`. Replace
30
+ `coding_edit_file` with `read_file` followed by guarded `apply_patch`; `read_file` now returns the
31
+ required SHA-256. If the host declares no executable aliases, expect `run_command` to be absent.
32
+
33
+ ### Durable Agent approval message variants
34
+
35
+ If a renderer, store adapter or export pipeline exhaustively switches over
36
+ `AgentMessage.role`, add the `tool` branch. If it switches over
37
+ `AgentMessagePart.type`, add `tool-approval-request` and `tool-approval-response`.
38
+ They are durable provider-continuation evidence: preserve them in storage and provider history;
39
+ a UI may render them as approval state or intentionally omit their visual row.
40
+
41
+ ```ts
42
+ // before
43
+ const unreachable: never = message.role
44
+
45
+ // after
46
+ if (message.role === 'tool') renderToolContinuation(message.parts)
47
+ else renderExistingRole(message)
48
+ ```
49
+
50
+ No data migration is needed. The schema still accepts all earlier records, and applications that
51
+ do not exhaustively branch over these unions compile unchanged.
52
+
12
53
  ## Released migration: 0.68.0
13
54
 
14
55
  ### Make Unix transport selection explicit outside Bun
@@ -1073,130 +1114,3 @@ The consequence for an exhaustive switch written without a `default` branch is a
1073
1114
  compile error, which is the point. The consequence for a switch that *has* a
1074
1115
  `default` is worse and silent: post-commit admissions fall into it and are
1075
1116
  projected as an unknown event. Grep for publisher switches before upgrading.
1076
-
1077
- ## Released migration: 0.56.0
1078
-
1079
- ### Surface manifests are version 2
1080
-
1081
- `operation.tools` could not describe a role-selected MCP surface, a different
1082
- Agent or CLI selection, or an advertised `extend` schema, so projections moved
1083
- out of the canonical operation:
1084
-
1085
- ```ts
1086
- // before
1087
- manifest.operations[0].tools.MCP
1088
- // after
1089
- manifest.toolSurfaces.find((s) => s.transport === 'MCP' && s.surface === null)?.tools
1090
-
1091
- // before
1092
- buildSurfaceManifest({ mcpSurfaces: { admin: { services, extend } } })
1093
- // after
1094
- buildSurfaceManifest({ mcpSurfaces: { admin: { services } }, mcpPreparation: { extend } })
1095
- ```
1096
-
1097
- A committed snapshot is regenerated once, deliberately: `manifestVersion` is
1098
- `2`, and `ConformanceTransport` gained `REALTIME`, so any exhaustive
1099
- `Record<ConformanceTransport, …>` must handle it.
1100
-
1101
- ### `FILE_*` codes joined the error registry
1102
-
1103
- `StitchErrorCode` gained `FILE_INVALID_PATH`, `FILE_OUTSIDE_ROOT`,
1104
- `FILE_NOT_FOUND`, `FILE_NOT_REGULAR`, `FILE_INSPECTION_REJECTED`,
1105
- `FILE_TOO_LARGE` and `FILE_EXISTS`. Only exhaustive maps break:
1106
-
1107
- ```ts
1108
- // before — compiled while the registry had no file codes
1109
- const copy: Record<StitchErrorCode, string> = { …, RATE_LIMITED: '…' }
1110
- // after — add the seven managed-file codes (or use Partial<Record<…>>)
1111
- ```
1112
-
1113
- Unexpected IO stays scrubbed as `INTERNAL_SERVER_ERROR`: the new codes are the
1114
- caller-safe ones only.
1115
-
1116
- ### `ScopedAuthHook` is nominal
1117
-
1118
- A hand-written function shaped like an auth hook is no longer assignable —
1119
- identity now comes from the factory, so scope ownership and the inferred
1120
- context cannot drift apart:
1121
-
1122
- ```ts
1123
- // before — a structural stand-in
1124
- const auth: ScopedAuthHook<Scopes> = async (ctx, endpoint) => { … }
1125
- // after — create it, then compose domains
1126
- const auth = createAuthHook({ resolve, rules })
1127
- const composed = composeAuthHooks({ hooks: [auth, billingAuth], defaultScope: 'public' })
1128
- ```
1129
-
1130
- ### Managed-file inspectors also run on reads
1131
-
1132
- An inspector is no longer write-only, and it has a finite default deadline
1133
- (15 s). Make it read-aware and idempotent — a read carries no
1134
- `declaredMediaType`:
1135
-
1136
- ```ts
1137
- // before
1138
- inspect: ({ declaredMediaType }) => inspectDeclaredType(declaredMediaType!)
1139
- // after
1140
- inspect: ({ prefix, declaredMediaType, signal }) =>
1141
- inspectBytes(prefix, { declaredMediaType, signal })
1142
- ```
1143
-
1144
- Set `inspectionTimeoutMs` explicitly when 15 seconds is the wrong budget.
1145
-
1146
- ### Direct async-operation binding needs a wire-stable ID
1147
-
1148
- A direct binding reuses the start output as the follow-up wire input, so the ID
1149
- schema must parse to itself (`z.input` equals `z.output`, no transform,
1150
- coercion, default or overwrite). Anything else is parsed twice:
1151
-
1152
- ```ts
1153
- // before — a transform silently ran on start and again on every follow-up
1154
- defineAsyncOperationContract({ binding: 'direct', id: z.string().transform(Number) })
1155
- // after — keep the wire shape, adapt explicitly
1156
- defineAsyncOperationContract({
1157
- binding: 'adapted',
1158
- id,
1159
- adapters: { idFromStart, inputFor },
1160
- })
1161
- ```
1162
-
1163
- ## Released migration: 0.55.0
1164
-
1165
- ### Peer-free `implementRemote`
1166
-
1167
- `implementRemote` now has one canonical, optional-peer-free owner. This keeps
1168
- MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
1169
-
1170
- ```ts
1171
- // before
1172
- import { implementRemote } from 'stitchkit/tools'
1173
- // after
1174
- import { implementRemote } from 'stitchkit/remote'
1175
- ```
1176
-
1177
- ### Managed file boundary and strict auth returns
1178
-
1179
- Create one boundary during application bootstrap and pass the capability, never
1180
- a per-call directory or host path:
1181
-
1182
- ```ts
1183
- import { createManagedFileBoundary } from 'stitchkit/files'
1184
-
1185
- const files = await createManagedFileBoundary({ root: '/srv/app-files' })
1186
-
1187
- // before
1188
- defineDownloadTool({ defaultDir: '/srv/app-files', resolveUrl, ...common })
1189
- defineUploadTool({ upload: (path) => provider.uploadFile(path), ...common })
1190
- defineViewFileTool({ baseDir: '/srv/app-files', ...common })
1191
-
1192
- // after
1193
- defineDownloadTool({ files, resolveUrl, ...common })
1194
- defineUploadTool({ files, upload: ({ bytes }) => provider.upload(bytes), ...common })
1195
- defineViewFileTool({ files, ...common })
1196
- ```
1197
-
1198
- Downloaded `path` is now relative to the boundary and MIME metadata is named
1199
- `mediaType`. Update raw `mountDownload`/`mountUpload`/`mountViewFile` configs the
1200
- same way. Auth predicates must explicitly return `true`, `false`, or a plain
1201
- object of context fields; replace accidental `undefined` fallthroughs with the
1202
- intended boolean.
@@ -1,14 +1,51 @@
1
- # Upgrading 0.69.0 → 0.95.0 — stitchkit docs slice
1
+ # Upgrading 0.70.0 → 0.96.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.69.0 → 0.95.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.70.0 → 0.96.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.69.0 → 0.95.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.70.0 → 0.96.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.96.0
13
+
14
+ ### resource-context-admission
15
+
16
+ One breaking change in `stitchkit/application`: `ManagedResourceContext` gains a
17
+ required `admission`. Every context the kernel hands a resource already carries
18
+ it; only a context built by hand needs it.
19
+
20
+ 1. **Give a hand-built resource context an `admission`.** **Who must act:** a
21
+ project whose tests call a resource's phase directly with a context object
22
+ they wrote themselves (`resource.close?.({ applicationId, signal, … })`).
23
+ The type checker names each one:
24
+
25
+ ```ts
26
+ // before
27
+ await resource.close?.({ applicationId: 'test', signal, now, reportHealth, use })
28
+ // after
29
+ await resource.close?.({
30
+ applicationId: 'test',
31
+ signal,
32
+ now,
33
+ reportHealth,
34
+ use,
35
+ admission: { acquire: () => null, acquireWhenAccepting: () => new Promise(() => {}) },
36
+ })
37
+ ```
38
+
39
+ A test that needs real admission starts the resource inside
40
+ `createApplication` instead.
41
+
42
+ 2. **Nothing to do for grammY polling, but know what changed.** **Who must
43
+ act:** nobody. `grammyPollingResource` now admits updates by batch: it does
44
+ not fetch while the application is not accepting, and finishes the batch in
45
+ hand before `bot.stop()`. A project that wrote its own `getUpdates`
46
+ transformer for this (waiting on `app.admission` before returning a batch)
47
+ deletes it; keeping both waits twice and is harmless.
48
+
12
49
  ## Released migration: 0.95.0
13
50
 
14
51
  One breaking change in `stitchkit/application`: the process lifecycle ledger
@@ -1108,44 +1145,3 @@ boundary, move the built-in filesystem operations to a supported worker or repla
1108
1145
  application-owned tools backed by an equivalent native handle API. They fail closed rather than
1109
1146
  falling back to path spelling. `run_command` remains separately available under its explicit
1110
1147
  executable alias and authorization policy; this change does not claim an executable sandbox.
1111
-
1112
- ## Released migration: 0.69.0
1113
-
1114
- ### Direct coding-tool operation names
1115
-
1116
- `createAgentCodingTools` no longer prefixes durable operation identity with `coding_`, and the
1117
- unguarded exact-string edit tool is gone. Update approval maps, presenters and tests together:
1118
-
1119
- ```ts
1120
- // before
1121
- { coding_read_file: 'approved', coding_search: 'approved', coding_patch_file: 'user-approval' }
1122
-
1123
- // after
1124
- { read_file: 'approved', search_files: 'approved', apply_patch: 'user-approval' }
1125
- ```
1126
-
1127
- The full mapping is `coding_read_file → read_file`, `coding_write_file → write_file`,
1128
- `coding_search → search_files`, `coding_patch_file → apply_patch`, `coding_shell → run_command`,
1129
- `coding_read_artifact → read_output` and `harness_read_resource → read_resource`. Replace
1130
- `coding_edit_file` with `read_file` followed by guarded `apply_patch`; `read_file` now returns the
1131
- required SHA-256. If the host declares no executable aliases, expect `run_command` to be absent.
1132
-
1133
- ### Durable Agent approval message variants
1134
-
1135
- If a renderer, store adapter or export pipeline exhaustively switches over
1136
- `AgentMessage.role`, add the `tool` branch. If it switches over
1137
- `AgentMessagePart.type`, add `tool-approval-request` and `tool-approval-response`.
1138
- They are durable provider-continuation evidence: preserve them in storage and provider history;
1139
- a UI may render them as approval state or intentionally omit their visual row.
1140
-
1141
- ```ts
1142
- // before
1143
- const unreachable: never = message.role
1144
-
1145
- // after
1146
- if (message.role === 'tool') renderToolContinuation(message.parts)
1147
- else renderExistingRole(message)
1148
- ```
1149
-
1150
- No data migration is needed. The schema still accepts all earlier records, and applications that
1151
- do not exhaustively branch over these unions compile unchanged.