stitchkit 0.95.3 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +99 -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 +680 -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 +72 -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 +159 -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 +265 -23
  51. package/llms.txt +10 -9
  52. package/package.json +1 -1
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,152 @@ 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
+ What to post stays the bot's. On the way down, drain it within the grace period
12833
+ and close it: `drain: (context) => operators.drain(context.signal)`,
12834
+ `close: () => operators.close()` in a resource.
12835
+
12836
+ ## Broadcasts
12837
+
12838
+ A broadcast is a job with an end, so it is a function, not a resource:
12839
+
12840
+ ```ts
12841
+ import { runTelegramBroadcast, telegramBroadcastSender } from 'stitchkit/telegram'
12842
+
12843
+ const report = await runTelegramBroadcast({
12844
+ name: '2026-04-28-price-lock',
12845
+ directory: env.STATE_DIRECTORY, // a state root, never the release tree
12846
+ recipients: () => audience.activeSubscribers(),
12847
+ send: telegramBroadcastSender({
12848
+ token: env.BOT_TOKEN,
12849
+ message: { copyFrom: { chatId: env.DRAFTS_CHAT_ID, messageId: 42 } },
12850
+ }),
12851
+ signal: stop.signal,
12852
+ onProgress: (progress) => logger.info('Broadcast', { ...progress }),
12853
+ })
12854
+ ```
12855
+
12856
+ - **Resumable by name.** The audience is written once on the first run; running
12857
+ the same name again continues with that list. Progress is an append-only
12858
+ journal beside it, one line before and one after each send.
12859
+ - **Nobody twice.** A send that was in flight when the process died is recorded
12860
+ `uncertain` and not repeated. A signal stops between sends, after the one in
12861
+ flight is recorded.
12862
+ - **Refusals by meaning.** A blocked, deactivated or never-started recipient is
12863
+ `unreachable` and never addressed again. A 429 waits Telegram's
12864
+ `retry_after` and sends the same recipient again; a server error is retried
12865
+ with backoff up to `maxAttempts`, then `failed`. A message Telegram cannot
12866
+ parse is not the recipient's fault: the run **halts** with them still pending,
12867
+ so the run after the fix reaches them. So does Telegram being unreachable.
12868
+ - **Pacing** is `ratePerSecond`, 25 by default. `dryRun` counts and writes
12869
+ nothing. One runner per name: a second concurrent run is refused.
12870
+
12871
+ The same call works from a script (the process's signal) and inside a live
12872
+ application (the application's signal, with admission held by the caller).
12873
+ The recipient state contains Telegram ids: keep it in the state directory, out
12874
+ of git.
12875
+
12876
+ ## Files from a local Bot API server
12877
+
12878
+ A local `telegram-bot-api` keeps downloaded files under `<dir>/<bot token>/`,
12879
+ and a bot sharing that directory reads them from disk and deletes them when
12880
+ done. `file_path` comes from a server over the network, so it is resolved only
12881
+ inside the bot's directory — `../`, an absolute path elsewhere and a link
12882
+ pointing out are refused, judged on real paths.
12883
+
12884
+ ```ts
12885
+ import { createTelegramLocalFiles } from 'stitchkit/telegram'
12886
+
12887
+ const files = createTelegramLocalFiles({ root: env.BOT_API_FILES_ROOT, token: env.BOT_TOKEN })
12888
+
12889
+ const { file_path } = await ctx.getFile()
12890
+ const path = await files.resolve(file_path) // throws TelegramLocalFileError
12891
+ // … process it …
12892
+ await files.remove(file_path)
12893
+ ```
12894
+
12895
+ The recommended variable for the root is **`BOT_API_FILES_ROOT`** — the
12896
+ server's `--dir`, absolute, as the bot's process sees it. A relative `file_path`
12897
+ (a server without `--local`) and an absolute one inside the bot's directory (a
12898
+ server with `--local`) both resolve. A refusal carries a reason
12899
+ (`root-not-absolute`, `bot-directory-unavailable`, `outside-bot-directory`,
12900
+ `missing`, `not-a-file`) and never the path or the token. `files.check()`
12901
+ answers whether the directory is there to read and delete from; a resource that
12902
+ throws on `{ ready: false }` in `start` keeps the bot from answering before it
12903
+ can read its media.
12904
+
12905
+
12719
12906
  ==============================================================================
12720
12907
  # Guide: GeoIP (docs/guide/geo.md)
12721
12908
  ==============================================================================
@@ -12812,6 +12999,43 @@ analytics policy belong to the application.
12812
12999
 
12813
13000
  # Upgrading stitchkit
12814
13001
 
13002
+ ## Released migration: 0.96.0
13003
+
13004
+ ### resource-context-admission
13005
+
13006
+ One breaking change in `stitchkit/application`: `ManagedResourceContext` gains a
13007
+ required `admission`. Every context the kernel hands a resource already carries
13008
+ it; only a context built by hand needs it.
13009
+
13010
+ 1. **Give a hand-built resource context an `admission`.** **Who must act:** a
13011
+ project whose tests call a resource's phase directly with a context object
13012
+ they wrote themselves (`resource.close?.({ applicationId, signal, … })`).
13013
+ The type checker names each one:
13014
+
13015
+ ```ts
13016
+ // before
13017
+ await resource.close?.({ applicationId: 'test', signal, now, reportHealth, use })
13018
+ // after
13019
+ await resource.close?.({
13020
+ applicationId: 'test',
13021
+ signal,
13022
+ now,
13023
+ reportHealth,
13024
+ use,
13025
+ admission: { acquire: () => null, acquireWhenAccepting: () => new Promise(() => {}) },
13026
+ })
13027
+ ```
13028
+
13029
+ A test that needs real admission starts the resource inside
13030
+ `createApplication` instead.
13031
+
13032
+ 2. **Nothing to do for grammY polling, but know what changed.** **Who must
13033
+ act:** nobody. `grammyPollingResource` now admits updates by batch: it does
13034
+ not fetch while the application is not accepting, and finishes the batch in
13035
+ hand before `bot.stop()`. A project that wrote its own `getUpdates`
13036
+ transformer for this (waiting on `app.admission` before returning a batch)
13037
+ deletes it; keeping both waits twice and is harmless.
13038
+
12815
13039
  ## Released migration: 0.95.0
12816
13040
 
12817
13041
  One breaking change in `stitchkit/application`: the process lifecycle ledger
@@ -17111,6 +17335,7 @@ cutovers are covered by the executable
17111
17335
  | `ApplicationConfig` / `ApplicationHandle` | _type_ | application declaration and its start/snapshot/subscription/admission/shutdown handle |
17112
17336
  | `ApplicationAdmission` / `ApplicationOperationLease` | _type_ | atomic process-local admission and idempotent release primitive |
17113
17337
  | `ManagedResource` / `ManagedResourceContext` / `ManagedResourceStartResult` | _type_ | resource lifecycle callbacks, shared deadlines, health reporting, separate readiness/completion promises and the value a resource publishes to its dependants |
17338
+ | `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
17339
  | `ManagedResourceDependency` | _type_ | a dependency named by id or given as the resource itself — the second form is what `context.use(...)` can type |
17115
17340
  | `ManagedResourcePublished` | _type_ | the value type `context.use(resource)` returns, recovered from that resource's own `start` |
17116
17341
  | `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 +17513,10 @@ does not resolve grammY.
17288
17513
  | Export | Kind | Summary |
17289
17514
  |--------|------|---------|
17290
17515
  | `grammyPollingResource` | function | adapt an injected bot's long polling with distinct `onStart` readiness, observed completion and one stop chain |
17516
+ | `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` |
17517
+ | `GrammyBotResourcesConfig` / `GrammyBotResources` | _type_ | injected bot, `dependsOn`, `configure`, polling options, `onStart` / `onError` / `onEnded`; `{ configuration?, polling, resources }` in start order |
17518
+ | `TELEGRAM_CONFIGURATION_RESOURCE_ID` / `TELEGRAM_POLLING_RESOURCE_ID` | constant | the two stable ids a monitor, a test or a restart addresses |
17519
+ | `GrammyPollingEnd` | _type_ | `{ error? }` — how a poller that was ready ended on its own; never reported for a requested stop |
17291
17520
  | `createGrammyWebhookResource` | function | return a managed resource plus admission-guarded `handleUpdate`; HTTP hosting and webhook registration stay application-owned |
17292
17521
  | `GrammyWebhookUnavailableError` | class | stable `GRAMMY_WEBHOOK_NOT_ACCEPTING` rejection after webhook admission closes |
17293
17522
  | `GrammyPollingResourceConfig` | _type_ | injected bot, polling options, resource graph policy and isolated error observer |
@@ -17817,6 +18046,8 @@ audit event. See the [Observability guide](../guide/observability.md).
17817
18046
  | `DimensionCollision` / `SetRequestDimensionsOptions` | _type_ | explicit overwrite, preserve or error policy for dimension keys |
17818
18047
  | `createBoundedLogger` | function | decorate a `StitchLogger` with request context, shared sanitisation, redaction and total bounds |
17819
18048
  | `DEFAULT_REDACT_PATHS` | constant | baseline credential/token paths added to the sanitizer's sensitive-key policy |
18049
+ | `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` |
18050
+ | `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
18051
  | `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
18052
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
17822
18053
  | `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 +18520,10 @@ available from `stitchkit/contract`.
18289
18520
 
18290
18521
  ## `stitchkit/telegram`
18291
18522
 
18292
- Peer-free server-only Telegram platform primitives. Importing this resolves no
18523
+ Peer-free server-only Telegram platform primitives — the Bot API's own
18524
+ protocols and the jobs every bot rewrote around them. Importing this resolves no
18293
18525
  bot library; `stitchkit/application/grammy` remains the lifecycle adapter for an
18294
- injected grammY bot. → ADR 0143
18526
+ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
18295
18527
 
18296
18528
  | Export | Kind | Summary |
18297
18529
  |--------|------|---------|
@@ -18304,6 +18536,16 @@ injected grammY bot. → ADR 0143
18304
18536
  | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
18305
18537
  | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
18306
18538
  | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
18539
+ | `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 |
18540
+ | `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` |
18541
+ | `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
18542
+ | `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 |
18543
+ | `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`; a drop is `overflow` / `refused` / `attempts` / `closed` with its classification |
18544
+ | `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode` |
18545
+ | `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` |
18546
+ | `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 |
18547
+ | `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 |
18548
+ | `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
18307
18549
 
18308
18550
  ---
18309
18551
 
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) 11.7 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.0",
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",