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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApplication
3
- } from "../index-w906953s.js";
3
+ } from "../index-5qn1kx34.js";
4
4
  import"../index-3gye9wzb.js";
5
5
  import"../index-2k4yrqkc.js";
6
6
  import"../index-3z73fh2c.js";
@@ -288,6 +288,50 @@ function waitForPending(state) {
288
288
  return Promise.resolve();
289
289
  return new Promise((resolve) => state.pendingWaiters.add(resolve));
290
290
  }
291
+ function acquireWhenAccepting(state, signal) {
292
+ return new Promise((resolve, reject) => {
293
+ let scheduled = false;
294
+ let settled = false;
295
+ const finish = () => {
296
+ settled = true;
297
+ state.listeners.delete(onChange);
298
+ signal.removeEventListener("abort", attempt);
299
+ };
300
+ function attempt() {
301
+ if (settled)
302
+ return true;
303
+ if (signal.aborted) {
304
+ finish();
305
+ reject(signal.reason);
306
+ return true;
307
+ }
308
+ if (state.shutdownRequested || state.lifecycle === "failed" || state.lifecycle === "stopped") {
309
+ finish();
310
+ reject(new ApplicationAdmissionError);
311
+ return true;
312
+ }
313
+ const lease = acquire(state);
314
+ if (!lease)
315
+ return false;
316
+ finish();
317
+ resolve(lease);
318
+ return true;
319
+ }
320
+ function onChange() {
321
+ if (scheduled)
322
+ return;
323
+ scheduled = true;
324
+ queueMicrotask(() => {
325
+ scheduled = false;
326
+ attempt();
327
+ });
328
+ }
329
+ if (attempt())
330
+ return;
331
+ state.listeners.add(onChange);
332
+ signal.addEventListener("abort", attempt, { once: true });
333
+ });
334
+ }
291
335
 
292
336
  // src/application/kernel-resource-context.ts
293
337
  function contextFor(state, record, options = {}) {
@@ -299,6 +343,10 @@ function contextFor(state, record, options = {}) {
299
343
  forceDeadlineAt: options.forceDeadlineAt
300
344
  },
301
345
  now: () => performance.now(),
346
+ admission: {
347
+ acquire: () => acquire(state),
348
+ acquireWhenAccepting: (signal) => acquireWhenAccepting(state, signal)
349
+ },
302
350
  use(resource) {
303
351
  const dependencyId = resource?.id;
304
352
  if (typeof dependencyId !== "string" || dependencyId.length === 0) {
@@ -0,0 +1,38 @@
1
+ import type { StitchLogger } from '../internal/logger.js';
2
+ import { type BoundedLoggerOptions } from './bounded-logger.js';
3
+ /**
4
+ * A process journal: one JSON object per line on standard output.
5
+ *
6
+ * A long-running process without a request — a bot, a worker — had no journal
7
+ * of its own here, so each one chose: a hand-written ndjson writer that cut the
8
+ * stack, `console.log` strings, or pino, where an error logged under any key but
9
+ * `err` became `"error":{}` and a production startup failure had to be
10
+ * reproduced by hand to learn its cause.
11
+ *
12
+ * The line is pino's shape on purpose — numeric `level`, `time` in epoch
13
+ * milliseconds, `msg` — so a supervisor that already recognises
14
+ * `"level":50` as an error, and `pino-pretty` on a developer's terminal, read
15
+ * it unchanged. What pino does not do, this does: every value passes through
16
+ * `createBoundedLogger`, so an `Error` in *any* field is written with its name,
17
+ * message, stack and cause, secrets are masked, and one line stays bounded.
18
+ */
19
+ /** pino's numbers, which is the contract a line reader matches on. */
20
+ export declare const JSON_LOG_LEVELS: {
21
+ readonly debug: 20;
22
+ readonly info: 30;
23
+ readonly warn: 40;
24
+ readonly error: 50;
25
+ };
26
+ export type JsonLogLevel = keyof typeof JSON_LOG_LEVELS;
27
+ export interface JsonLoggerOptions extends Omit<BoundedLoggerOptions, 'sink'> {
28
+ /** The lowest level written. Default `info`. */
29
+ readonly level?: JsonLogLevel;
30
+ /** Fields on every line — the service name, the release. A call's own fields win. */
31
+ readonly fields?: Readonly<Record<string, unknown>>;
32
+ /** Where a finished line goes. Default: standard output, newline-terminated. */
33
+ readonly write?: (line: string) => void;
34
+ /** Default `Date.now`. */
35
+ readonly now?: () => number;
36
+ }
37
+ export declare function createJsonLogger(options?: JsonLoggerOptions): StitchLogger;
38
+ //# sourceMappingURL=json-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-logger.d.ts","sourceRoot":"","sources":["../../src/observability/json-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,KAAK,oBAAoB,EAAuB,MAAM,kBAAkB,CAAC;AAElF;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,eAAO,MAAM,eAAe;aAAK,KAAK,EAAE,EAAE;aAAE,IAAI,EAAE,EAAE;aAAE,IAAI,EAAE,EAAE;aAAE,KAAK,EAAE,EAAE;CAAW,CAAC;AACrF,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,eAAe,CAAC;AAExD,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC;IAC3E,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAeD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,YAAY,CAgC9E"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One Bot API call over `fetch`, with no bot library.
3
+ *
4
+ * The operator channel and the broadcast send outside any update handler — from
5
+ * a script, a schedule, a failure observer — where a whole grammY instance is
6
+ * more than the job needs. What they need is one request whose refusal keeps
7
+ * Telegram's own structured answer, because `classifyTelegramSendFailure`
8
+ * reads `error_code` and `parameters.retry_after` before it ever reads prose.
9
+ */
10
+ /**
11
+ * The shape of a bot token: the bot's numeric id, a colon, the secret.
12
+ *
13
+ * For masking, not for validation — hand it to a logger's
14
+ * `sensitiveUrlPatterns` and a token inside a URL or an error message is not
15
+ * written to the journal.
16
+ */
17
+ export declare const TELEGRAM_BOT_TOKEN_PATTERN: RegExp;
18
+ /** Telegram refused the call. Carries its answer as fields, never the token. */
19
+ export declare class TelegramBotApiError extends Error {
20
+ readonly method: string;
21
+ readonly error_code: number;
22
+ readonly description: string;
23
+ readonly parameters: {
24
+ readonly retry_after?: number;
25
+ readonly migrate_to_chat_id?: number;
26
+ };
27
+ constructor(method: string, answer: {
28
+ error_code: number;
29
+ description: string;
30
+ parameters?: TelegramBotApiError['parameters'];
31
+ });
32
+ }
33
+ export interface TelegramBotApiCall {
34
+ readonly token: string;
35
+ readonly method: string;
36
+ readonly params?: Readonly<Record<string, unknown>>;
37
+ /** A local Bot API server, e.g. `http://127.0.0.1:8081`. Default: Telegram's. */
38
+ readonly apiRoot?: string;
39
+ readonly signal?: AbortSignal;
40
+ readonly fetch?: typeof fetch;
41
+ }
42
+ /**
43
+ * Call one Bot API method and return its `result`.
44
+ *
45
+ * A refusal throws `TelegramBotApiError`; a transport failure throws an error
46
+ * whose message names the method and never the URL, since the URL holds the
47
+ * token.
48
+ */
49
+ export declare function callTelegramBotApi(call: TelegramBotApiCall): Promise<unknown>;
50
+ //# sourceMappingURL=bot-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot-api.d.ts","sourceRoot":"","sources":["../../src/telegram/bot-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,QAA8B,CAAC;AAItE,gFAAgF;AAChF,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE7F,YACE,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;KAChD,EAQF;CACF;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CAC/B;AAQD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAuCnF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The durable half of a broadcast: who it is for, and what happened to each.
3
+ *
4
+ * Two files per broadcast name. The recipients are written once, atomically,
5
+ * the first time the broadcast runs — the audience of a resumed broadcast is
6
+ * the audience it started with, not whoever matches the query today. Progress
7
+ * is an append-only journal: a line before each send and a line after it, so
8
+ * recording one send costs one short append instead of rewriting a state file
9
+ * that grows with the audience.
10
+ *
11
+ * The line before the send is what makes a crash honest. A recipient whose
12
+ * last line says `sending` may or may not have received the message; a resume
13
+ * records them as `uncertain` and does not send again, because a second copy
14
+ * of a broadcast is the failure a subscriber notices.
15
+ */
16
+ import { z } from 'zod';
17
+ export declare const TelegramBroadcastOutcomeSchema: z.ZodEnum<{
18
+ delivered: "delivered";
19
+ failed: "failed";
20
+ uncertain: "uncertain";
21
+ unreachable: "unreachable";
22
+ }>;
23
+ export type TelegramBroadcastOutcome = z.infer<typeof TelegramBroadcastOutcomeSchema>;
24
+ declare const ChatIdSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
25
+ export type TelegramBroadcastRecipient = z.infer<typeof ChatIdSchema>;
26
+ declare const JournalLineSchema: z.ZodUnion<readonly [z.ZodObject<{
27
+ i: z.ZodNumber;
28
+ s: z.ZodEnum<{
29
+ released: "released";
30
+ sending: "sending";
31
+ }>;
32
+ }, z.core.$strict>, z.ZodObject<{
33
+ i: z.ZodNumber;
34
+ o: z.ZodEnum<{
35
+ delivered: "delivered";
36
+ failed: "failed";
37
+ uncertain: "uncertain";
38
+ unreachable: "unreachable";
39
+ }>;
40
+ r: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strict>]>;
42
+ type JournalLine = z.infer<typeof JournalLineSchema>;
43
+ /** Per recipient index: the outcome, or `sending` for a send whose end is unknown. */
44
+ export type BroadcastProgress = Map<number, TelegramBroadcastOutcome | 'sending'>;
45
+ export declare function assertBroadcastName(name: string): void;
46
+ export interface BroadcastFiles {
47
+ readonly recipients: string;
48
+ readonly journal: string;
49
+ readonly lock: string;
50
+ }
51
+ export declare function broadcastFiles(directory: string, name: string): BroadcastFiles;
52
+ export declare function readRecipients(files: BroadcastFiles): Promise<TelegramBroadcastRecipient[] | null>;
53
+ export declare function writeRecipients(files: BroadcastFiles, recipients: readonly TelegramBroadcastRecipient[]): Promise<void>;
54
+ /**
55
+ * Replay the journal. A torn last line — the process died mid-append — is
56
+ * skipped; every complete line before it stands.
57
+ */
58
+ export declare function readProgress(files: BroadcastFiles): Promise<BroadcastProgress>;
59
+ export declare function appendJournal(files: BroadcastFiles, line: JournalLine): Promise<void>;
60
+ /**
61
+ * One runner per broadcast name. Two would each read the same pending
62
+ * recipients and send them twice. A lock left by a process that is gone is
63
+ * taken over.
64
+ */
65
+ export declare function lockBroadcast(directory: string, files: BroadcastFiles): Promise<() => Promise<void>>;
66
+ export {};
67
+ //# sourceMappingURL=broadcast-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast-state.d.ts","sourceRoot":"","sources":["../../src/telegram/broadcast-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,8BAA8B;;;;;EAKzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF,QAAA,MAAM,YAAY,iDAAyD,CAAC;AAC5E,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAItE,QAAA,MAAM,iBAAiB;;;;;;;;;;;;;;;oBASrB,CAAC;AACH,KAAK,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAErD,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC,CAAC;AAIlF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMtD;AAKD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,CAM9E;AAED,wBAAsB,cAAc,CAClC,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAO9C;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,cAAc,EACrB,UAAU,EAAE,SAAS,0BAA0B,EAAE,GAChD,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsBpF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAErF;AAWD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAkB9B"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * A broadcast that survives its own process.
3
+ *
4
+ * Two bots carried the same broadcast subsystem — three files byte-identical —
5
+ * and inside it their own list of refusal phrases and their own single retry
6
+ * on 429. This is that subsystem once: the application says who and how one
7
+ * message is sent; the runner owns the pacing, the retries Telegram asks for,
8
+ * the difference between "not this recipient" and "not this message", and the
9
+ * state that lets a broadcast stopped by a crash, a deploy or Ctrl-C continue
10
+ * where it was without writing twice to anyone.
11
+ *
12
+ * It is a function, not a resource. A broadcast is a job with an end: run from
13
+ * a script it takes the process's signal; run inside a live application it
14
+ * takes the application's, and admission is the caller's to hold around it.
15
+ */
16
+ import { type TelegramBroadcastRecipient } from './broadcast-state.js';
17
+ import { type TelegramSendFailure } from './send-failure.js';
18
+ export type { TelegramBroadcastOutcome, TelegramBroadcastRecipient } from './broadcast-state.js';
19
+ export interface TelegramBroadcastSend {
20
+ readonly recipient: TelegramBroadcastRecipient;
21
+ /** 1 for the first try of this recipient in this run. */
22
+ readonly attempt: number;
23
+ }
24
+ export interface TelegramBroadcastConfig {
25
+ /** The broadcast's identity: running the same name again resumes it. `[A-Za-z0-9._-]`. */
26
+ readonly name: string;
27
+ /** Where its state lives — a state root, never the release tree. Absolute. */
28
+ readonly directory: string;
29
+ /**
30
+ * The audience, read once: on the first run it is written down, and every
31
+ * resumed run sends to that list. Duplicates are sent once.
32
+ */
33
+ readonly recipients: () => Iterable<TelegramBroadcastRecipient> | AsyncIterable<TelegramBroadcastRecipient> | Promise<Iterable<TelegramBroadcastRecipient>>;
34
+ /** Send the message to one recipient; throw Telegram's refusal as it came. */
35
+ readonly send: (send: TelegramBroadcastSend) => Promise<unknown>;
36
+ /** Sends started per second. Default 25, under Telegram's 30 for one bot. */
37
+ readonly ratePerSecond?: number;
38
+ /** Tries of one recipient on a retryable refusal before it is `failed`. Default 5. */
39
+ readonly maxAttempts?: number;
40
+ /** Count what would be sent and send nothing; no state is written. */
41
+ readonly dryRun?: boolean;
42
+ /** Stops between sends — the send in flight is finished and recorded first. */
43
+ readonly signal?: AbortSignal;
44
+ /** At the start, every `progressEveryMs`, and at the end. */
45
+ readonly onProgress?: (report: TelegramBroadcastReport) => void;
46
+ /** Default 10 000. */
47
+ readonly progressEveryMs?: number;
48
+ readonly sleep?: (milliseconds: number) => Promise<void>;
49
+ readonly now?: () => number;
50
+ }
51
+ /**
52
+ * - `finished` — nobody is left.
53
+ * - `stopped` — the signal stopped it; run the same name again to continue.
54
+ * - `halted` — the message itself was refused (`message-invalid`) or Telegram
55
+ * was unreachable; nobody was charged with it. Fix the cause and run again.
56
+ * - `dry-run` — counted, not sent.
57
+ */
58
+ export type TelegramBroadcastRunOutcome = 'finished' | 'stopped' | 'halted' | 'dry-run';
59
+ export interface TelegramBroadcastReport {
60
+ readonly name: string;
61
+ readonly outcome: TelegramBroadcastRunOutcome | 'running';
62
+ readonly total: number;
63
+ readonly delivered: number;
64
+ /** The recipient blocked the bot, is gone, or never started it — do not address them again. */
65
+ readonly unreachable: number;
66
+ /** Refused for a reason about neither the recipient nor the message, or retries ran out. */
67
+ readonly failed: number;
68
+ /** A send that was in flight when a previous run died — not repeated. */
69
+ readonly uncertain: number;
70
+ readonly pending: number;
71
+ /** Why a `halted` run stopped. */
72
+ readonly halt?: TelegramSendFailure;
73
+ }
74
+ export declare function runTelegramBroadcast(config: TelegramBroadcastConfig): Promise<TelegramBroadcastReport>;
75
+ /** What the standard sender delivers: a text, or a copy of a prepared message with its media. */
76
+ export type TelegramBroadcastMessage = {
77
+ readonly text: string;
78
+ readonly parseMode?: 'HTML' | 'MarkdownV2';
79
+ } | {
80
+ readonly copyFrom: {
81
+ readonly chatId: TelegramBroadcastRecipient;
82
+ readonly messageId: number;
83
+ };
84
+ };
85
+ export interface TelegramBroadcastSenderConfig {
86
+ readonly token: string;
87
+ readonly message: TelegramBroadcastMessage;
88
+ readonly apiRoot?: string;
89
+ readonly fetch?: typeof fetch;
90
+ }
91
+ /** The standard `send`: `sendMessage` or `copyMessage` through the Bot API, no bot library. */
92
+ export declare function telegramBroadcastSender(config: TelegramBroadcastSenderConfig): TelegramBroadcastConfig['send'];
93
+ //# sourceMappingURL=broadcast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/telegram/broadcast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A file the local Bot API server already wrote to disk.
3
+ *
4
+ * `telegram-bot-api` keeps what it downloads under `<dir>/<bot token>/`, and a
5
+ * bot sharing that directory reads the file straight from disk instead of
6
+ * downloading it again, then deletes it once handled. Two bots did this two
7
+ * ways: one refused anything leaving the bot's directory, the other joined
8
+ * `file_path` onto the root and read whatever that named. `file_path` comes
9
+ * from a server over the network; `../` in it, or a link inside the directory
10
+ * pointing out of it, turned the second into a read — and a delete — of any
11
+ * file the bot's user could reach.
12
+ *
13
+ * The containment is decided on real paths, after links are resolved, so a
14
+ * link inside the directory is judged by where it leads. A refusal is a
15
+ * reason, and its message never carries the path or the token: both are
16
+ * secrets in a journal, the token literally so.
17
+ */
18
+ /**
19
+ * Why a file will not be handed out.
20
+ *
21
+ * - `root-not-absolute` — the configured root is relative, so it would mean
22
+ * something different in every working directory.
23
+ * - `bot-directory-unavailable` — the root has no readable, writable directory
24
+ * for this bot: the server is not sharing it, or the process may not delete.
25
+ * - `outside-bot-directory` — `file_path` leaves the bot's directory, by `../`,
26
+ * by an absolute path elsewhere, or through a link.
27
+ * - `missing` — nothing is there (already handled, or never finished).
28
+ * - `not-a-file` — something is there, and it is not a regular file.
29
+ */
30
+ export type TelegramLocalFileRefusal = 'root-not-absolute' | 'bot-directory-unavailable' | 'outside-bot-directory' | 'missing' | 'not-a-file';
31
+ export declare class TelegramLocalFileError extends Error {
32
+ readonly reason: TelegramLocalFileRefusal;
33
+ constructor(reason: TelegramLocalFileRefusal);
34
+ }
35
+ export interface TelegramLocalFilesConfig {
36
+ /**
37
+ * The directory the Bot API server was started with (`--dir`), absolute, as
38
+ * this process sees it. The recommended variable is `BOT_API_FILES_ROOT`.
39
+ */
40
+ readonly root: string;
41
+ /** The bot's token: the server names the bot's directory after it. */
42
+ readonly token: string;
43
+ }
44
+ export type TelegramLocalFilesCheck = {
45
+ readonly ready: true;
46
+ } | {
47
+ readonly ready: false;
48
+ readonly reason: TelegramLocalFileRefusal;
49
+ };
50
+ export interface TelegramLocalFiles {
51
+ /**
52
+ * The absolute path of `file_path` as `getFile` returned it — relative, or
53
+ * absolute inside the bot's directory as a `--local` server answers.
54
+ * Throws `TelegramLocalFileError`.
55
+ */
56
+ resolve(filePath: string): Promise<string>;
57
+ /** Delete a handled file, within the same boundary. Already gone is not an error. */
58
+ remove(filePath: string): Promise<void>;
59
+ /** Whether the bot's directory is there to read and delete from — for readiness. */
60
+ check(): Promise<TelegramLocalFilesCheck>;
61
+ }
62
+ export declare function createTelegramLocalFiles(config: TelegramLocalFilesConfig): TelegramLocalFiles;
63
+ //# sourceMappingURL=local-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-files.d.ts","sourceRoot":"","sources":["../../src/telegram/local-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,wBAAwB,GAChC,mBAAmB,GACnB,2BAA2B,GAC3B,uBAAuB,GACvB,SAAS,GACT,YAAY,CAAC;AAEjB,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;IAE1C,YAAY,MAAM,EAAE,wBAAwB,EAI3C;CACF;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,uBAAuB,GAC/B;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3C,qFAAqF;IACrF,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,oFAAoF;IACpF,KAAK,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC3C;AAYD,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,kBAAkB,CAoEpB"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The operator's Telegram chat: product events for people, not the journal.
3
+ *
4
+ * Two bots each carried a 250-line static logger posting new users, payments
5
+ * and errors into forum topics of one chat, and each mixed it with the process
6
+ * journal. They answer different readers. The journal is for the machine and
7
+ * the engineer and must never lose a line to a slow network; the chat is for a
8
+ * person and must never slow the bot down. So this is a separate thing, with
9
+ * the opposite guarantee: `post` returns at once and never throws, sending is
10
+ * paced below Telegram's per-chat limit, and when the chat cannot keep up the
11
+ * oldest message is dropped and counted rather than the bot waiting.
12
+ *
13
+ * What to post stays the application's. This owns the queue, the pacing, the
14
+ * topics, 429s and the bounded way down.
15
+ */
16
+ import { type TelegramSendFailure } from './send-failure.js';
17
+ export type TelegramChatId = number | string;
18
+ export interface TelegramOperatorMessage {
19
+ readonly chatId: TelegramChatId;
20
+ /** The forum topic, when the chat has topics. */
21
+ readonly threadId?: number;
22
+ readonly text: string;
23
+ }
24
+ export interface TelegramOperatorDrop<TTopic extends string> {
25
+ readonly topic?: TTopic;
26
+ readonly text: string;
27
+ /**
28
+ * `overflow` — the queue was full and this was the oldest; `refused` —
29
+ * Telegram said no in a way repeating will not fix; `attempts` — it kept
30
+ * failing; `closed` — the channel closed before it was sent.
31
+ */
32
+ readonly reason: 'overflow' | 'refused' | 'attempts' | 'closed';
33
+ readonly failure?: TelegramSendFailure;
34
+ }
35
+ export interface TelegramOperatorChannelConfig<TTopic extends string> {
36
+ readonly chatId: TelegramChatId;
37
+ /** Topic name → forum `message_thread_id`. A topic not listed posts to the chat itself. */
38
+ readonly topics?: Readonly<Partial<Record<TTopic, number>>>;
39
+ /** Deliver one message — `telegramOperatorSender` or the application's own bot. */
40
+ readonly send: (message: TelegramOperatorMessage, signal: AbortSignal) => Promise<unknown>;
41
+ /** Default 200. The oldest message is dropped beyond it. */
42
+ readonly maxQueued?: number;
43
+ /** Pause between two sends. Default 3000 — Telegram allows about 20 per minute into a group. */
44
+ readonly minIntervalMs?: number;
45
+ /** Sends of one message before it is dropped. Default 3. */
46
+ readonly maxAttempts?: number;
47
+ /** Every message that will not arrive, with why. */
48
+ readonly onDropped?: (drop: TelegramOperatorDrop<TTopic>) => void;
49
+ /** Default: a timer. Rejects when `signal` aborts. */
50
+ readonly sleep?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
51
+ }
52
+ export interface TelegramOperatorChannel<TTopic extends string> {
53
+ /** Queue one message. Returns at once and never throws. */
54
+ post(text: string, topic?: TTopic): void;
55
+ /** Messages waiting, the one in flight included. */
56
+ readonly pending: number;
57
+ /** Resolves once everything queued was sent or dropped, or when `signal` aborts. */
58
+ drain(signal?: AbortSignal): Promise<void>;
59
+ /** Stop: the message in flight is abandoned and the queue dropped as `closed`. */
60
+ close(): void;
61
+ }
62
+ export declare function createTelegramOperatorChannel<TTopic extends string = never>(config: TelegramOperatorChannelConfig<TTopic>): TelegramOperatorChannel<TTopic>;
63
+ export interface TelegramOperatorSenderConfig {
64
+ readonly token: string;
65
+ readonly apiRoot?: string;
66
+ /** Default: plain text, so an operator message never fails on entity parsing. */
67
+ readonly parseMode?: 'HTML' | 'MarkdownV2';
68
+ readonly fetch?: typeof fetch;
69
+ }
70
+ /** The standard `send`: `sendMessage` through the Bot API, no bot library. */
71
+ export declare function telegramOperatorSender(config: TelegramOperatorSenderConfig): TelegramOperatorChannelConfig<string>['send'];
72
+ //# sourceMappingURL=operator-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator-channel.d.ts","sourceRoot":"","sources":["../../src/telegram/operator-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAA+B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAEvF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAK7C,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB,CAAC,MAAM,SAAS,MAAM;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;CACxC;AAED,MAAM,WAAW,6BAA6B,CAAC,MAAM,SAAS,MAAM;IAClE,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAC5D,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,uBAAuB,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3F,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,oBAAoB,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IAClE,sDAAsD;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,uBAAuB,CAAC,MAAM,SAAS,MAAM;IAC5D,2DAA2D;IAC3D,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,kFAAkF;IAClF,KAAK,IAAI,IAAI,CAAC;CACf;AA4BD,wBAAgB,6BAA6B,CAAC,MAAM,SAAS,MAAM,GAAG,KAAK,EACzE,MAAM,EAAE,6BAA6B,CAAC,MAAM,CAAC,GAC5C,uBAAuB,CAAC,MAAM,CAAC,CA8IjC;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CAC/B;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,6BAA6B,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAgB/C"}
@@ -7,6 +7,7 @@ Contains: API reference: `stitchkit/application/grammy` (docs/api/reference.md)
7
7
 
8
8
  Also read:
9
9
  - see `llms/application.txt` for the Managed application kernel guide
10
+ - see `llms/telegram.txt` for the Telegram bots guide
10
11
 
11
12
  ==============================================================================
12
13
  # API reference: `stitchkit/application/grammy` (docs/api/reference.md)
@@ -20,6 +21,10 @@ does not resolve grammY.
20
21
  | Export | Kind | Summary |
21
22
  |--------|------|---------|
22
23
  | `grammyPollingResource` | function | adapt an injected bot's long polling with distinct `onStart` readiness, observed completion and one stop chain |
24
+ | `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` |
25
+ | `GrammyBotResourcesConfig` / `GrammyBotResources` | _type_ | injected bot, `dependsOn`, `configure`, polling options, `onStart` / `onError` / `onEnded`; `{ configuration?, polling, resources }` in start order |
26
+ | `TELEGRAM_CONFIGURATION_RESOURCE_ID` / `TELEGRAM_POLLING_RESOURCE_ID` | constant | the two stable ids a monitor, a test or a restart addresses |
27
+ | `GrammyPollingEnd` | _type_ | `{ error? }` — how a poller that was ready ended on its own; never reported for a requested stop |
23
28
  | `createGrammyWebhookResource` | function | return a managed resource plus admission-guarded `handleUpdate`; HTTP hosting and webhook registration stay application-owned |
24
29
  | `GrammyWebhookUnavailableError` | class | stable `GRAMMY_WEBHOOK_NOT_ACCEPTING` rejection after webhook admission closes |
25
30
  | `GrammyPollingResourceConfig` | _type_ | injected bot, polling options, resource graph policy and isolated error observer |
@@ -319,6 +319,7 @@ cutovers are covered by the executable
319
319
  | `ApplicationConfig` / `ApplicationHandle` | _type_ | application declaration and its start/snapshot/subscription/admission/shutdown handle |
320
320
  | `ApplicationAdmission` / `ApplicationOperationLease` | _type_ | atomic process-local admission and idempotent release primitive |
321
321
  | `ManagedResource` / `ManagedResourceContext` / `ManagedResourceStartResult` | _type_ | resource lifecycle callbacks, shared deadlines, health reporting, separate readiness/completion promises and the value a resource publishes to its dependants |
322
+ | `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 |
322
323
  | `ManagedResourceDependency` | _type_ | a dependency named by id or given as the resource itself — the second form is what `context.use(...)` can type |
323
324
  | `ManagedResourcePublished` | _type_ | the value type `context.use(resource)` returns, recovered from that resource's own `start` |
324
325
  | `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 |
@@ -755,40 +755,78 @@ Install grammY only in an application that imports the adapter:
755
755
 
756
756
  ```ts
757
757
  import { Bot } from 'grammy'
758
- import {
759
- createGrammyWebhookResource,
760
- grammyPollingResource,
761
- } from 'stitchkit/application/grammy'
758
+ import { grammyBotResources } from 'stitchkit/application/grammy'
762
759
  ```
763
760
 
764
- For simple long polling, pass an already-configured bot:
761
+ A long-polling bot is two resources, and one call gives both:
765
762
 
766
763
  ```ts
767
764
  const bot = new Bot(env.BOT_TOKEN)
768
765
  bot.command('start', (ctx) => ctx.reply('Hello'))
769
- bot.catch(({ error }) => internalLogger.error(error))
766
+ bot.catch(({ error }) => logger.error('Telegram handler failed', { error }))
770
767
 
771
- const telegram = grammyPollingResource({
772
- id: 'telegram',
768
+ const telegram = grammyBotResources({
773
769
  bot,
774
- required: true,
770
+ dependsOn: [database, http], // what handlers need before the first update
771
+ configure: () => bot.api.setMyCommands([{ command: 'start', description: 'Start' }]),
772
+ onEnded: ({ error }) => stopProcess(error), // see below
775
773
  })
774
+
775
+ const app = createApplication({ id: 'bot', resources: [database, http, ...telegram.resources] })
776
776
  ```
777
777
 
778
- Readiness follows grammY's `onStart`. Shutdown calls `bot.stop()` once and then
779
- awaits the retained `bot.start()` promise, because grammY documents that
780
- `stop()` alone does not wait for middleware completion.
778
+ The ids are stable — `telegram-configuration`, then `telegram-polling` — so a
779
+ monitor, a test or `app.restart()` can name them. `configure` runs on every
780
+ start, before polling, so a command menu that failed to publish fails the start
781
+ instead of hiding behind a bot that answers.
782
+
783
+ **Updates are admitted by batch.** grammY confirms a batch to Telegram by
784
+ asking for the next one, and it asks as soon as the batch went through
785
+ middleware — whatever the middleware did. An update that arrived while the
786
+ application was starting, degraded or stopping was refused by its handler and
787
+ then confirmed as handled. The polling resource now waits for admission before
788
+ asking for a batch, holds one admission lease for the batch it returns to
789
+ grammY, and releases it when grammY asks for the next. On shutdown the batch in
790
+ hand is finished before `bot.stop()` confirms the offset, so a stop neither
791
+ loses an update nor hands the next process one this process already handled; a
792
+ batch that arrives once the application will not admit it is never returned, and
793
+ Telegram keeps it for the next process.
794
+
795
+ **When polling ends on its own** — Telegram refused the token (401), another
796
+ process took the bot (409), or `bot.catch` rethrew — grammY's poller does not
797
+ recover in-process. The resource becomes `failed`, the application stops being
798
+ ready, and `onEnded` is called once; it is never called for a stop the
799
+ application asked for. The framework does not exit processes (ADR 0074): the
800
+ entry point turns `onEnded` into its own shutdown and a non-zero exit so the
801
+ supervisor starts a fresh process. The Telegram bot template shows the one way.
802
+
803
+ **429s** are the application's for its own replies: install
804
+ `@grammyjs/auto-retry` on `bot.api.config` with a bounded delay. Polling itself
805
+ already waits Telegram's `retry_after`, and the broadcast and operator channel
806
+ in `stitchkit/telegram` handle theirs.
807
+
808
+ `grammyPollingResource` is the polling half on its own, for a bot that needs no
809
+ configuration step; it admits updates the same way.
781
810
 
782
811
  For webhook ingress, `createGrammyWebhookResource` wraps provider-owned update
783
812
  handling with admission and drain. The application still mounts the HTTP route,
784
813
  verifies/configures its webhook and chooses the grammY framework adapter. An
785
814
  update accepted before shutdown may finish; a later update is rejected.
786
815
 
787
- The adapter never reads the token/env, installs commands, changes `bot.catch`,
816
+ The adapter never reads the token or the environment, changes `bot.catch`,
788
817
  chooses retry plugins, drops pending updates, sends outbound messages or stores
789
818
  updates. A durable inbox/retry worker remains an application component, not a
790
819
  mode of this adapter.
791
820
 
821
+ ### A resource that fetches work
822
+
823
+ `context.admission` is the application's admission as a resource sees it.
824
+ `acquire()` answers now; `acquireWhenAccepting(signal)` waits until the
825
+ application admits and then holds one operation — the right move for a poller or
826
+ a queue consumer, which should not fetch at all while the application is not
827
+ ready. It rejects with the signal's reason on abort, and with
828
+ `ApplicationAdmissionError` once the application is shutting down or failed.
829
+
792
830
  ## What disappears from an application
793
831
 
794
832
  A typical small service can replace roughly 120–220 lines of generic signal,
@@ -5,6 +5,9 @@
5
5
 
6
6
  Contains: Guide: Observability (docs/guide/observability.md) · API reference: `stitchkit/observability` (docs/api/reference.md)
7
7
 
8
+ Also read:
9
+ - see `llms/telegram.txt` for the Telegram bots guide
10
+
8
11
  ==============================================================================
9
12
  # Guide: Observability (docs/guide/observability.md)
10
13
  ==============================================================================
@@ -831,6 +834,8 @@ audit event. See the [Observability guide](../guide/observability.md).
831
834
  | `DimensionCollision` / `SetRequestDimensionsOptions` | _type_ | explicit overwrite, preserve or error policy for dimension keys |
832
835
  | `createBoundedLogger` | function | decorate a `StitchLogger` with request context, shared sanitisation, redaction and total bounds |
833
836
  | `DEFAULT_REDACT_PATHS` | constant | baseline credential/token paths added to the sanitizer's sensitive-key policy |
837
+ | `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` |
838
+ | `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 |
834
839
  | `BoundedLoggerBounds` / `BoundedLoggerOptions` | _type_ | per-value and total record ceilings (`stringLengthByKey` raises the string bound for named keys such as `stack`) plus sink/redaction configuration |
835
840
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
836
841
  | `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 |
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