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
@@ -11,9 +11,9 @@ Contains: Guide: Upgrading, general (docs/guide/upgrading.md)
11
11
 
12
12
  Load this slice, then every range slice between your installed version and the target:
13
13
 
14
- - 0.69.0 → 0.95.0: `llms/upgrading-0.69.0-0.95.0.txt`
15
- - 0.55.0 → 0.68.0: `llms/upgrading-0.55.0-0.68.0.txt`
16
- - 0.37.0 → 0.53.0: `llms/upgrading-0.37.0-0.53.0.txt`
14
+ - 0.70.0 → 0.96.0: `llms/upgrading-0.70.0-0.96.0.txt`
15
+ - 0.57.0 → 0.69.0: `llms/upgrading-0.57.0-0.69.0.txt`
16
+ - 0.37.0 → 0.56.0: `llms/upgrading-0.37.0-0.56.0.txt`
17
17
 
18
18
  # Upgrading stitchkit
19
19
 
package/llms-full.txt CHANGED
@@ -60,7 +60,7 @@ own, recorded as an ADR.
60
60
  | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
61
61
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
62
62
  | `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
63
- | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification and Bot API send-failure classification |
63
+ | `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 |
64
64
  | `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 |
65
65
  | `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 |
66
66
  | `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 |
@@ -68,11 +68,11 @@ own, recorded as an ADR.
68
68
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
69
69
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
70
70
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
71
- | `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 |
71
+ | `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 |
72
72
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
73
73
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
74
74
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
75
- | `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 |
75
+ | `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 |
76
76
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
77
77
  | `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 |
78
78
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -82,8 +82,8 @@ own, recorded as an ADR.
82
82
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
83
83
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
84
84
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
85
- | `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 |
86
- | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
85
+ | `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 |
86
+ | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
87
87
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
88
88
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
89
89
  | `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
@@ -7003,40 +7003,78 @@ Install grammY only in an application that imports the adapter:
7003
7003
 
7004
7004
  ```ts
7005
7005
  import { Bot } from 'grammy'
7006
- import {
7007
- createGrammyWebhookResource,
7008
- grammyPollingResource,
7009
- } from 'stitchkit/application/grammy'
7006
+ import { grammyBotResources } from 'stitchkit/application/grammy'
7010
7007
  ```
7011
7008
 
7012
- For simple long polling, pass an already-configured bot:
7009
+ A long-polling bot is two resources, and one call gives both:
7013
7010
 
7014
7011
  ```ts
7015
7012
  const bot = new Bot(env.BOT_TOKEN)
7016
7013
  bot.command('start', (ctx) => ctx.reply('Hello'))
7017
- bot.catch(({ error }) => internalLogger.error(error))
7014
+ bot.catch(({ error }) => logger.error('Telegram handler failed', { error }))
7018
7015
 
7019
- const telegram = grammyPollingResource({
7020
- id: 'telegram',
7016
+ const telegram = grammyBotResources({
7021
7017
  bot,
7022
- required: true,
7023
- })
7024
- ```
7025
-
7026
- Readiness follows grammY's `onStart`. Shutdown calls `bot.stop()` once and then
7027
- awaits the retained `bot.start()` promise, because grammY documents that
7028
- `stop()` alone does not wait for middleware completion.
7018
+ dependsOn: [database, http], // what handlers need before the first update
7019
+ configure: () => bot.api.setMyCommands([{ command: 'start', description: 'Start' }]),
7020
+ onEnded: ({ error }) => stopProcess(error), // see below
7021
+ })
7022
+
7023
+ const app = createApplication({ id: 'bot', resources: [database, http, ...telegram.resources] })
7024
+ ```
7025
+
7026
+ The ids are stable — `telegram-configuration`, then `telegram-polling` — so a
7027
+ monitor, a test or `app.restart()` can name them. `configure` runs on every
7028
+ start, before polling, so a command menu that failed to publish fails the start
7029
+ instead of hiding behind a bot that answers.
7030
+
7031
+ **Updates are admitted by batch.** grammY confirms a batch to Telegram by
7032
+ asking for the next one, and it asks as soon as the batch went through
7033
+ middleware — whatever the middleware did. An update that arrived while the
7034
+ application was starting, degraded or stopping was refused by its handler and
7035
+ then confirmed as handled. The polling resource now waits for admission before
7036
+ asking for a batch, holds one admission lease for the batch it returns to
7037
+ grammY, and releases it when grammY asks for the next. On shutdown the batch in
7038
+ hand is finished before `bot.stop()` confirms the offset, so a stop neither
7039
+ loses an update nor hands the next process one this process already handled; a
7040
+ batch that arrives once the application will not admit it is never returned, and
7041
+ Telegram keeps it for the next process.
7042
+
7043
+ **When polling ends on its own** — Telegram refused the token (401), another
7044
+ process took the bot (409), or `bot.catch` rethrew — grammY's poller does not
7045
+ recover in-process. The resource becomes `failed`, the application stops being
7046
+ ready, and `onEnded` is called once; it is never called for a stop the
7047
+ application asked for. The framework does not exit processes (ADR 0074): the
7048
+ entry point turns `onEnded` into its own shutdown and a non-zero exit so the
7049
+ supervisor starts a fresh process. The Telegram bot template shows the one way.
7050
+
7051
+ **429s** are the application's for its own replies: install
7052
+ `@grammyjs/auto-retry` on `bot.api.config` with a bounded delay. Polling itself
7053
+ already waits Telegram's `retry_after`, and the broadcast and operator channel
7054
+ in `stitchkit/telegram` handle theirs.
7055
+
7056
+ `grammyPollingResource` is the polling half on its own, for a bot that needs no
7057
+ configuration step; it admits updates the same way.
7029
7058
 
7030
7059
  For webhook ingress, `createGrammyWebhookResource` wraps provider-owned update
7031
7060
  handling with admission and drain. The application still mounts the HTTP route,
7032
7061
  verifies/configures its webhook and chooses the grammY framework adapter. An
7033
7062
  update accepted before shutdown may finish; a later update is rejected.
7034
7063
 
7035
- The adapter never reads the token/env, installs commands, changes `bot.catch`,
7064
+ The adapter never reads the token or the environment, changes `bot.catch`,
7036
7065
  chooses retry plugins, drops pending updates, sends outbound messages or stores
7037
7066
  updates. A durable inbox/retry worker remains an application component, not a
7038
7067
  mode of this adapter.
7039
7068
 
7069
+ ### A resource that fetches work
7070
+
7071
+ `context.admission` is the application's admission as a resource sees it.
7072
+ `acquire()` answers now; `acquireWhenAccepting(signal)` waits until the
7073
+ application admits and then holds one operation — the right move for a poller or
7074
+ a queue consumer, which should not fetch at all while the application is not
7075
+ ready. It rejects with the signal's reason on abort, and with
7076
+ `ApplicationAdmissionError` once the application is shutting down or failed.
7077
+
7040
7078
  ## What disappears from an application
7041
7079
 
7042
7080
  A typical small service can replace roughly 120–220 lines of generic signal,
@@ -10463,6 +10501,9 @@ and our payload is wrong, which is the case a list of substrings quietly counts
10463
10501
  against the user. An unrecognised refusal leaves the recipient reachable: losing
10464
10502
  a working subscriber forever costs more than one wasted send.
10465
10503
 
10504
+ The rest of what a bot is assembled from — its resources, journal, operator
10505
+ channel, broadcasts and local Bot API files — is in [Telegram bots](telegram.md).
10506
+
10466
10507
  ## Cookies
10467
10508
 
10468
10509
  ```ts
@@ -12716,6 +12757,155 @@ One vocabulary for every side of the conversation — the server that runs the a
12716
12757
  page that shows the microphone — so a phase sent over the wire means the same on both ends.
12717
12758
 
12718
12759
 
12760
+ ==============================================================================
12761
+ # Guide: Telegram bots (docs/guide/telegram.md)
12762
+ ==============================================================================
12763
+
12764
+ # Telegram bots
12765
+
12766
+ `stitchkit/telegram` holds what the Bot API itself asks of every bot, with no
12767
+ bot library; `stitchkit/application/grammy` makes a grammY bot a set of
12768
+ application resources. Together they are what a bot is assembled from, so that
12769
+ a bot's own code is its product and nothing else. A generated bot to start from:
12770
+ `bun create stitchkit my-bot --template telegram-bot`. Why it is shaped this
12771
+ way: [ADR 0201](../decisions/0201-a-telegram-bot-is-assembled-from-primitives.md).
12772
+
12773
+ | Job | Where |
12774
+ |---|---|
12775
+ | the bot as resources: command menu, then long polling with updates admitted by batch | `grammyBotResources` — [application kernel](application-kernel.md#optional-grammy-adapter) |
12776
+ | the process journal | `createJsonLogger` from `stitchkit/observability` |
12777
+ | the operator's chat: product events, apart from the journal | `createTelegramOperatorChannel` |
12778
+ | a broadcast that survives crashes, deploys and Ctrl-C | `runTelegramBroadcast` |
12779
+ | a file the local Bot API server wrote to disk | `createTelegramLocalFiles` |
12780
+ | why a send was refused | `classifyTelegramSendFailure` — [auth and errors](auth-and-errors.md#telegram-mini-apps) |
12781
+ | Mini App `initData` | `verifyTelegramInitData` — [auth and errors](auth-and-errors.md#telegram-mini-apps) |
12782
+ | one Bot API call without a bot library | `callTelegramBotApi` |
12783
+
12784
+ ## The journal
12785
+
12786
+ A bot's journal is `createJsonLogger`: one JSON object per line on standard
12787
+ output, in pino's shape — numeric `level` (`debug` 20, `info` 30, `warn` 40,
12788
+ `error` 50), `time` in epoch milliseconds, `msg` — so `pino-pretty` reads it on
12789
+ a terminal and a supervisor matching `"level":50` sees errors. Unlike pino, an
12790
+ `Error` under *any* key is written with its name, message, stack and cause; an
12791
+ error logged as `{ error }` does not become `"error":{}`.
12792
+
12793
+ ```ts
12794
+ import { createJsonLogger } from 'stitchkit/observability'
12795
+ import { TELEGRAM_BOT_TOKEN_PATTERN } from 'stitchkit/telegram'
12796
+
12797
+ export const logger = createJsonLogger({
12798
+ level: env.LOG_LEVEL,
12799
+ fields: { service: 'my-bot' },
12800
+ sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
12801
+ })
12802
+
12803
+ logger.error('Application startup failed', { error })
12804
+ ```
12805
+
12806
+ The error line contract, for whatever recognises a failure from the journal: a
12807
+ line whose `level` is `50` or more is an error. There is no separate `fatal`;
12808
+ an unrecoverable end is an `error` line followed by a non-zero exit.
12809
+
12810
+ ## The operator channel
12811
+
12812
+ The chat where people read about new users, payments and failures is not the
12813
+ journal. The journal must not lose a line to a slow network; the chat must not
12814
+ slow the bot down. `post` returns at once and never throws; sends are paced
12815
+ below Telegram's per-chat limit (one every 3 s by default), 429s wait the time
12816
+ Telegram named, and when the chat cannot keep up the oldest message is dropped
12817
+ and reported.
12818
+
12819
+ ```ts
12820
+ import { createTelegramOperatorChannel, telegramOperatorSender } from 'stitchkit/telegram'
12821
+
12822
+ const operators = createTelegramOperatorChannel<'users' | 'payments' | 'errors'>({
12823
+ chatId: env.OPERATOR_CHAT_ID,
12824
+ topics: { users: 2, payments: 3, errors: 4 }, // forum message_thread_id
12825
+ send: telegramOperatorSender({ token: env.BOT_TOKEN }),
12826
+ onDropped: (drop) => logger.warn('Operator message dropped', { ...drop }),
12827
+ })
12828
+
12829
+ operators.post(`New user ${user.id}`, 'users')
12830
+ ```
12831
+
12832
+ Every message is masked before it leaves: a bot token always — bots post their
12833
+ errors here, and an error about a local Bot API file carries the token in its
12834
+ path — plus any `sensitivePatterns` the bot adds; `telegramOperatorSender` also
12835
+ masks its own exact token. What to post stays the bot's. On the way down, drain it within the grace period
12836
+ and close it: `drain: (context) => operators.drain(context.signal)`,
12837
+ `close: () => operators.close()` in a resource.
12838
+
12839
+ ## Broadcasts
12840
+
12841
+ A broadcast is a job with an end, so it is a function, not a resource:
12842
+
12843
+ ```ts
12844
+ import { runTelegramBroadcast, telegramBroadcastSender } from 'stitchkit/telegram'
12845
+
12846
+ const report = await runTelegramBroadcast({
12847
+ name: '2026-04-28-price-lock',
12848
+ directory: env.STATE_DIRECTORY, // a state root, never the release tree
12849
+ recipients: () => audience.activeSubscribers(),
12850
+ send: telegramBroadcastSender({
12851
+ token: env.BOT_TOKEN,
12852
+ message: { copyFrom: { chatId: env.DRAFTS_CHAT_ID, messageId: 42 } },
12853
+ }),
12854
+ signal: stop.signal,
12855
+ onProgress: (progress) => logger.info('Broadcast', { ...progress }),
12856
+ })
12857
+ ```
12858
+
12859
+ - **Resumable by name.** The audience is written once on the first run; running
12860
+ the same name again continues with that list. Progress is an append-only
12861
+ journal beside it, one line before and one after each send.
12862
+ - **Nobody twice.** A send that was in flight when the process died is recorded
12863
+ `uncertain` and not repeated. A signal stops between sends, after the one in
12864
+ flight is recorded.
12865
+ - **Refusals by meaning.** A blocked, deactivated or never-started recipient is
12866
+ `unreachable` and never addressed again. A 429 waits Telegram's
12867
+ `retry_after` and sends the same recipient again; a server error is retried
12868
+ with backoff up to `maxAttempts`, then `failed`. A message Telegram cannot
12869
+ parse is not the recipient's fault: the run **halts** with them still pending,
12870
+ so the run after the fix reaches them. So does Telegram being unreachable.
12871
+ - **Pacing** is `ratePerSecond`, 25 by default. `dryRun` counts and writes
12872
+ nothing. One runner per name: a second concurrent run is refused.
12873
+
12874
+ The same call works from a script (the process's signal) and inside a live
12875
+ application (the application's signal, with admission held by the caller).
12876
+ The recipient state contains Telegram ids: keep it in the state directory, out
12877
+ of git.
12878
+
12879
+ ## Files from a local Bot API server
12880
+
12881
+ A local `telegram-bot-api` keeps downloaded files under `<dir>/<bot token>/`,
12882
+ and a bot sharing that directory reads them from disk and deletes them when
12883
+ done. `file_path` comes from a server over the network, so it is resolved only
12884
+ inside the bot's directory — `../`, an absolute path elsewhere and a link
12885
+ pointing out are refused, judged on real paths.
12886
+
12887
+ ```ts
12888
+ import { createTelegramLocalFiles } from 'stitchkit/telegram'
12889
+
12890
+ const files = createTelegramLocalFiles({ root: env.BOT_API_FILES_ROOT, token: env.BOT_TOKEN })
12891
+
12892
+ const { file_path } = await ctx.getFile()
12893
+ const path = await files.resolve(file_path) // throws TelegramLocalFileError
12894
+ // … process it …
12895
+ await files.remove(file_path)
12896
+ ```
12897
+
12898
+ The recommended variable for the root is **`BOT_API_FILES_ROOT`** — the
12899
+ server's `--dir`, absolute, as the bot's process sees it. A relative `file_path`
12900
+ (a server without `--local`) and an absolute one inside the bot's directory (a
12901
+ server with `--local`) both resolve. A refusal carries a reason
12902
+ (`root-not-absolute`, `bot-directory-unavailable`, `outside-bot-directory`,
12903
+ `missing`, `not-a-file`) and never the path or the token. `files.check()`
12904
+ answers whether the directory is there to read and delete from; a resource that
12905
+ throws on `{ ready: false }` in `start` keeps the bot from answering before it
12906
+ can read its media.
12907
+
12908
+
12719
12909
  ==============================================================================
12720
12910
  # Guide: GeoIP (docs/guide/geo.md)
12721
12911
  ==============================================================================
@@ -12812,6 +13002,43 @@ analytics policy belong to the application.
12812
13002
 
12813
13003
  # Upgrading stitchkit
12814
13004
 
13005
+ ## Released migration: 0.96.0
13006
+
13007
+ ### resource-context-admission
13008
+
13009
+ One breaking change in `stitchkit/application`: `ManagedResourceContext` gains a
13010
+ required `admission`. Every context the kernel hands a resource already carries
13011
+ it; only a context built by hand needs it.
13012
+
13013
+ 1. **Give a hand-built resource context an `admission`.** **Who must act:** a
13014
+ project whose tests call a resource's phase directly with a context object
13015
+ they wrote themselves (`resource.close?.({ applicationId, signal, … })`).
13016
+ The type checker names each one:
13017
+
13018
+ ```ts
13019
+ // before
13020
+ await resource.close?.({ applicationId: 'test', signal, now, reportHealth, use })
13021
+ // after
13022
+ await resource.close?.({
13023
+ applicationId: 'test',
13024
+ signal,
13025
+ now,
13026
+ reportHealth,
13027
+ use,
13028
+ admission: { acquire: () => null, acquireWhenAccepting: () => new Promise(() => {}) },
13029
+ })
13030
+ ```
13031
+
13032
+ A test that needs real admission starts the resource inside
13033
+ `createApplication` instead.
13034
+
13035
+ 2. **Nothing to do for grammY polling, but know what changed.** **Who must
13036
+ act:** nobody. `grammyPollingResource` now admits updates by batch: it does
13037
+ not fetch while the application is not accepting, and finishes the batch in
13038
+ hand before `bot.stop()`. A project that wrote its own `getUpdates`
13039
+ transformer for this (waiting on `app.admission` before returning a batch)
13040
+ deletes it; keeping both waits twice and is harmless.
13041
+
12815
13042
  ## Released migration: 0.95.0
12816
13043
 
12817
13044
  One breaking change in `stitchkit/application`: the process lifecycle ledger
@@ -17111,6 +17338,7 @@ cutovers are covered by the executable
17111
17338
  | `ApplicationConfig` / `ApplicationHandle` | _type_ | application declaration and its start/snapshot/subscription/admission/shutdown handle |
17112
17339
  | `ApplicationAdmission` / `ApplicationOperationLease` | _type_ | atomic process-local admission and idempotent release primitive |
17113
17340
  | `ManagedResource` / `ManagedResourceContext` / `ManagedResourceStartResult` | _type_ | resource lifecycle callbacks, shared deadlines, health reporting, separate readiness/completion promises and the value a resource publishes to its dependants |
17341
+ | `ManagedResourceAdmission` | _type_ | `context.admission`: `acquire()` now or `null`, and `acquireWhenAccepting(signal)` for a resource that fetches work — a poller waits instead of fetching while the application is starting or degraded; rejects on abort and once the application can no longer admit |
17114
17342
  | `ManagedResourceDependency` | _type_ | a dependency named by id or given as the resource itself — the second form is what `context.use(...)` can type |
17115
17343
  | `ManagedResourcePublished` | _type_ | the value type `context.use(resource)` returns, recovered from that resource's own `start` |
17116
17344
  | `ManagedResourcePublishesNoValue` | _type_ | what `context.use(...)` returns for a resource that publishes nothing — a branded refusal rather than `never`, so reading it does not silently compile |
@@ -17288,6 +17516,10 @@ does not resolve grammY.
17288
17516
  | Export | Kind | Summary |
17289
17517
  |--------|------|---------|
17290
17518
  | `grammyPollingResource` | function | adapt an injected bot's long polling with distinct `onStart` readiness, observed completion and one stop chain |
17519
+ | `grammyBotResources` | function | a long-polling bot as `telegram-configuration` (optional `configure`, e.g. `setMyCommands`) then `telegram-polling`, both after the bot's `dependsOn`; updates admitted by batch and the end of polling reported to `onEnded` |
17520
+ | `GrammyBotResourcesConfig` / `GrammyBotResources` | _type_ | injected bot, `dependsOn`, `configure`, polling options, `onStart` / `onError` / `onEnded`; `{ configuration?, polling, resources }` in start order |
17521
+ | `TELEGRAM_CONFIGURATION_RESOURCE_ID` / `TELEGRAM_POLLING_RESOURCE_ID` | constant | the two stable ids a monitor, a test or a restart addresses |
17522
+ | `GrammyPollingEnd` | _type_ | `{ error? }` — how a poller that was ready ended on its own; never reported for a requested stop |
17291
17523
  | `createGrammyWebhookResource` | function | return a managed resource plus admission-guarded `handleUpdate`; HTTP hosting and webhook registration stay application-owned |
17292
17524
  | `GrammyWebhookUnavailableError` | class | stable `GRAMMY_WEBHOOK_NOT_ACCEPTING` rejection after webhook admission closes |
17293
17525
  | `GrammyPollingResourceConfig` | _type_ | injected bot, polling options, resource graph policy and isolated error observer |
@@ -17817,6 +18049,8 @@ audit event. See the [Observability guide](../guide/observability.md).
17817
18049
  | `DimensionCollision` / `SetRequestDimensionsOptions` | _type_ | explicit overwrite, preserve or error policy for dimension keys |
17818
18050
  | `createBoundedLogger` | function | decorate a `StitchLogger` with request context, shared sanitisation, redaction and total bounds |
17819
18051
  | `DEFAULT_REDACT_PATHS` | constant | baseline credential/token paths added to the sanitizer's sensitive-key policy |
18052
+ | `createJsonLogger` | function | the process journal: one JSON line per call in pino's shape (numeric `level`, epoch `time`, `msg`) on standard output, through `createBoundedLogger` — an `Error` in any field keeps name, message, stack and cause; secrets masked; `level` threshold, static `fields`, injectable `write` |
18053
+ | `JsonLoggerOptions` / `JsonLogLevel` / `JSON_LOG_LEVELS` | _type_ / constant | the bounded-logger options plus `level`, `fields`, `write`, `now`; pino's numbers `debug` 20 … `error` 50, which a line reader matches on |
17820
18054
  | `BoundedLoggerBounds` / `BoundedLoggerOptions` | _type_ | per-value and total record ceilings (`stringLengthByKey` raises the string bound for named keys such as `stack`) plus sink/redaction configuration |
17821
18055
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
17822
18056
  | `runUnitOfWork` / `RunUnitOfWorkOptions` | function / _type_ | run work that did not arrive over a transport — an agent loop, a scheduled broadcast — inside one context, with the same completion record a request gets |
@@ -18289,9 +18523,10 @@ available from `stitchkit/contract`.
18289
18523
 
18290
18524
  ## `stitchkit/telegram`
18291
18525
 
18292
- Peer-free server-only Telegram platform primitives. Importing this resolves no
18526
+ Peer-free server-only Telegram platform primitives — the Bot API's own
18527
+ protocols and the jobs every bot rewrote around them. Importing this resolves no
18293
18528
  bot library; `stitchkit/application/grammy` remains the lifecycle adapter for an
18294
- injected grammY bot. → ADR 0143
18529
+ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
18295
18530
 
18296
18531
  | Export | Kind | Summary |
18297
18532
  |--------|------|---------|
@@ -18304,6 +18539,16 @@ injected grammY bot. → ADR 0143
18304
18539
  | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
18305
18540
  | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
18306
18541
  | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
18542
+ | `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 |
18543
+ | `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` |
18544
+ | `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
18545
+ | `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 |
18546
+ | `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 |
18547
+ | `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode` |
18548
+ | `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` |
18549
+ | `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 |
18550
+ | `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 |
18551
+ | `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
18307
18552
 
18308
18553
  ---
18309
18554
 
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
9
9
  Load the slice for each entrypoint your code imports — `import … from 'stitchkit/server'` → `llms/server.txt`; the root `stitchkit` import → `llms/stitchkit.txt`. A slice holds that entrypoint's API reference section and the guide pages whose home it is; every guide lives in exactly one slice, and a slice that needs another's guide names it under "Also read". Each file is at most 50.0 KB; a longer one continues in numbered parts, each naming the next at its top. Moving across versions: load `llms/upgrading.txt` and the range slices it lists between your version and the target. `llms-full.txt` inlines everything; it is far larger than a context window, so do not read it whole.
10
10
 
11
11
  ## Entrypoint slices
12
- - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 15.5 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 35.0 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 14.2 KB
12
+ - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 15.6 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 35.0 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 14.2 KB
13
13
  - **stitchkit/live**: [llms/live.txt](llms/live.txt) 21.2 KB
14
14
  - **stitchkit/react**: [llms/react.txt](llms/react.txt) 8.6 KB
15
15
  - **stitchkit/tools**: [llms/tools.txt](llms/tools.txt) 44.9 KB → [llms/tools.2.txt](llms/tools.2.txt) 26.3 KB → [llms/tools.3.txt](llms/tools.3.txt) 25.4 KB
@@ -20,8 +20,8 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
20
20
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
21
21
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
22
22
  - **stitchkit/primitives**: [llms/primitives.txt](llms/primitives.txt) 9.3 KB
23
- - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 42.7 KB → [llms/server.4.txt](llms/server.4.txt) 37.3 KB → [llms/server.5.txt](llms/server.5.txt) 21.8 KB
24
- - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 42.8 KB
23
+ - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 42.7 KB → [llms/server.4.txt](llms/server.4.txt) 37.5 KB → [llms/server.5.txt](llms/server.5.txt) 21.8 KB
24
+ - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 43.4 KB
25
25
  - **stitchkit/agent-runtime**: [llms/agent-runtime.txt](llms/agent-runtime.txt) 46.6 KB → [llms/agent-runtime.2.txt](llms/agent-runtime.2.txt) 29.8 KB → [llms/agent-runtime.3.txt](llms/agent-runtime.3.txt) 31.8 KB
26
26
  - **stitchkit/agent-runtime/testing**: [llms/agent-runtime-testing.txt](llms/agent-runtime-testing.txt) 2.2 KB
27
27
  - **stitchkit/agent-runtime/harness**: [llms/agent-runtime-harness.txt](llms/agent-runtime-harness.txt) 4.1 KB
@@ -30,8 +30,8 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
30
30
  - **stitchkit/agent-runtime/openrouter**: [llms/agent-runtime-openrouter.txt](llms/agent-runtime-openrouter.txt) 1.1 KB
31
31
  - **stitchkit/agent-runtime/sqlite/bun**: [llms/agent-runtime-sqlite-bun.txt](llms/agent-runtime-sqlite-bun.txt) 1.8 KB
32
32
  - **stitchkit/agent-runtime/sqlite/node**: [llms/agent-runtime-sqlite-node.txt](llms/agent-runtime-sqlite-node.txt) 1.7 KB
33
- - **stitchkit/application**: [llms/application.txt](llms/application.txt) 42.1 KB → [llms/application.2.txt](llms/application.2.txt) 32.6 KB
34
- - **stitchkit/application/grammy**: [llms/application-grammy.txt](llms/application-grammy.txt) 1.5 KB
33
+ - **stitchkit/application**: [llms/application.txt](llms/application.txt) 44.6 KB → [llms/application.2.txt](llms/application.2.txt) 32.9 KB
34
+ - **stitchkit/application/grammy**: [llms/application-grammy.txt](llms/application-grammy.txt) 2.3 KB
35
35
  - **stitchkit/application/opentelemetry**: [llms/application-opentelemetry.txt](llms/application-opentelemetry.txt) 1.6 KB
36
36
  - **stitchkit/application/directory-inbox**: [llms/application-directory-inbox.txt](llms/application-directory-inbox.txt) 0.3 KB
37
37
  - **stitchkit/application/diagnostic-journal**: [llms/application-diagnostic-journal.txt](llms/application-diagnostic-journal.txt) 1.4 KB
@@ -39,7 +39,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
39
39
  - **stitchkit/testing**: [llms/testing.txt](llms/testing.txt) 30.8 KB
40
40
  - **stitchkit/agent-runtime/sandbox**: [llms/agent-runtime-sandbox.txt](llms/agent-runtime-sandbox.txt) 8.1 KB
41
41
  - **stitchkit/files**: [llms/files.txt](llms/files.txt) 2.9 KB
42
- - **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 2.0 KB
42
+ - **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 12.0 KB
43
43
  - **stitchkit/tracking**: [llms/tracking.txt](llms/tracking.txt) 16.8 KB
44
44
  - **stitchkit/release**: [llms/release.txt](llms/release.txt) 8.9 KB
45
45
  - **stitchkit/tracking/server**: [llms/tracking-server.txt](llms/tracking-server.txt) 2.5 KB
@@ -52,9 +52,9 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
52
52
 
53
53
  ## Upgrading slices
54
54
  - **Upgrading — how to move across versions**: [llms/upgrading.txt](llms/upgrading.txt) 10.3 KB
55
- - **Upgrading 0.69.0 → 0.95.0**: [llms/upgrading-0.69.0-0.95.0.txt](llms/upgrading-0.69.0-0.95.0.txt) 46.6 KB
56
- - **Upgrading 0.55.0 → 0.68.0**: [llms/upgrading-0.55.0-0.68.0.txt](llms/upgrading-0.55.0-0.68.0.txt) 47.0 KB
57
- - **Upgrading 0.37.0 → 0.53.0**: [llms/upgrading-0.37.0-0.53.0.txt](llms/upgrading-0.37.0-0.53.0.txt) 32.6 KB
55
+ - **Upgrading 0.70.0 → 0.96.0**: [llms/upgrading-0.70.0-0.96.0.txt](llms/upgrading-0.70.0-0.96.0.txt) 46.3 KB
56
+ - **Upgrading 0.57.0 → 0.69.0**: [llms/upgrading-0.57.0-0.69.0.txt](llms/upgrading-0.57.0-0.69.0.txt) 44.4 KB
57
+ - **Upgrading 0.37.0 → 0.56.0**: [llms/upgrading-0.37.0-0.56.0.txt](llms/upgrading-0.37.0-0.56.0.txt) 36.9 KB
58
58
 
59
59
  ## Guide (web)
60
60
  - [Getting started](https://github.com/max-listov/stitchkit/blob/master/docs/guide/getting-started.md): install, entrypoints, and a first contract → server → client app
@@ -80,6 +80,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
80
80
  - [Frontend integrations](https://github.com/max-listov/stitchkit/blob/master/docs/guide/frontend-integrations.md): React Router resource routes and a separate Vite development proxy
81
81
  - [React Query policy](https://github.com/max-listov/stitchkit/blob/master/docs/guide/react.md): request-local SSR clients, a browser singleton and ApiError retry rules
82
82
  - [Voice](https://github.com/max-listov/stitchkit/blob/master/docs/guide/voice.md): stitchkit/voice — sentences cut while a reply streams, the text a voice reads, a speech queue that synthesises ahead and stops on interruption
83
+ - [Telegram bots](https://github.com/max-listov/stitchkit/blob/master/docs/guide/telegram.md): stitchkit/telegram — the journal, the operator channel, resumable broadcasts, local Bot API files; the bot as application resources
83
84
  - [GeoIP](https://github.com/max-listov/stitchkit/blob/master/docs/guide/geo.md): stitchkit/geo — a server-only managed reader, three observable states and last-known-good generation reloads
84
85
  - [Upgrading](https://github.com/max-listov/stitchkit/blob/master/docs/guide/upgrading.md): moving a project across stitchkit versions; how breaking changes are marked
85
86
  - [Project declaration](https://github.com/max-listov/stitchkit/blob/master/docs/guide/declaration.md): the optional machine-readable statement a repository makes about itself — identity, roles, build, requirements, release steps and the names of the values a deployment supplies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.95.3",
3
+ "version": "0.96.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",