stitchkit 0.70.6 → 0.71.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 (61) hide show
  1. package/dist/agent-runtime/coding-tool-contract.d.ts +11 -1
  2. package/dist/agent-runtime/coding-tool-contract.d.ts.map +1 -1
  3. package/dist/agent-runtime/coding-tool-files.d.ts.map +1 -1
  4. package/dist/agent-runtime/coding-tool-listing.d.ts +15 -0
  5. package/dist/agent-runtime/coding-tool-listing.d.ts.map +1 -0
  6. package/dist/agent-runtime/coding-tool-paths.d.ts.map +1 -1
  7. package/dist/agent-runtime/coding-tool-refusals.d.ts +59 -0
  8. package/dist/agent-runtime/coding-tool-refusals.d.ts.map +1 -0
  9. package/dist/agent-runtime/coding-tool-search-patch.d.ts.map +1 -1
  10. package/dist/agent-runtime/coding-tools.d.ts +3 -1
  11. package/dist/agent-runtime/coding-tools.d.ts.map +1 -1
  12. package/dist/agent-runtime/compaction.d.ts +35 -0
  13. package/dist/agent-runtime/compaction.d.ts.map +1 -1
  14. package/dist/agent-runtime/contained-files.d.ts +21 -2
  15. package/dist/agent-runtime/contained-files.d.ts.map +1 -1
  16. package/dist/agent-runtime/provider-failure.d.ts +66 -0
  17. package/dist/agent-runtime/provider-failure.d.ts.map +1 -0
  18. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  19. package/dist/agent-runtime/runtime.d.ts +31 -1
  20. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  21. package/dist/agent-runtime/schemas.d.ts +1 -0
  22. package/dist/agent-runtime/schemas.d.ts.map +1 -1
  23. package/dist/agent-runtime-coding-tools.js +438 -124
  24. package/dist/agent-runtime-harness.js +4 -4
  25. package/dist/agent-runtime-openrouter.d.ts +19 -0
  26. package/dist/agent-runtime-openrouter.d.ts.map +1 -1
  27. package/dist/agent-runtime-openrouter.js +2 -1
  28. package/dist/agent-runtime.d.ts +3 -2
  29. package/dist/agent-runtime.d.ts.map +1 -1
  30. package/dist/agent-runtime.js +118 -14
  31. package/dist/cli.js +5 -5
  32. package/dist/{index-cx84zg25.js → index-118v0z41.js} +1 -1
  33. package/dist/{index-ejv31h14.js → index-7qy2ex0m.js} +1 -1
  34. package/dist/{index-trrf7nch.js → index-ezmn6ac6.js} +3 -3
  35. package/dist/{index-ekkt4gy9.js → index-jqtsc9mj.js} +2 -2
  36. package/dist/{index-hxtm2xr2.js → index-k2zczx1g.js} +1 -1
  37. package/dist/{index-by57nwhc.js → index-m668wzyc.js} +10 -2
  38. package/dist/{index-w9m1wznn.js → index-nemjkxjp.js} +118 -1
  39. package/dist/{index-tnazr5tc.js → index-pz7ytdga.js} +3 -3
  40. package/dist/{index-1z5xxfst.js → index-rqpdar1e.js} +102 -5
  41. package/dist/{index-qm87g51s.js → index-s4c8wy8m.js} +1 -1
  42. package/dist/node.js +2 -2
  43. package/dist/observability/index.js +1 -1
  44. package/dist/server/index.d.ts +1 -1
  45. package/dist/server/index.d.ts.map +1 -1
  46. package/dist/server/index.js +4 -2
  47. package/dist/server/request.d.ts +19 -0
  48. package/dist/server/request.d.ts.map +1 -1
  49. package/dist/telegram/init-data.d.ts +134 -0
  50. package/dist/telegram/init-data.d.ts.map +1 -0
  51. package/dist/telegram/send-failure.d.ts +73 -0
  52. package/dist/telegram/send-failure.d.ts.map +1 -0
  53. package/dist/telegram.d.ts +3 -0
  54. package/dist/telegram.d.ts.map +1 -0
  55. package/dist/telegram.js +224 -0
  56. package/dist/tool-invoker.js +4 -4
  57. package/dist/tools.js +7 -7
  58. package/llms-full.txt +266 -9
  59. package/native/darwin-arm64.node +0 -0
  60. package/native/darwin-x64.node +0 -0
  61. package/package.json +6 -2
@@ -0,0 +1,224 @@
1
+ import {
2
+ isRecord
3
+ } from "./index-qyrqwr4c.js";
4
+
5
+ // src/telegram/init-data.ts
6
+ import { z } from "zod";
7
+ var TelegramInitDataUserSchema = z.object({
8
+ id: z.int(),
9
+ is_bot: z.boolean().optional(),
10
+ first_name: z.string(),
11
+ last_name: z.string().optional(),
12
+ username: z.string().optional(),
13
+ language_code: z.string().optional(),
14
+ is_premium: z.boolean().optional(),
15
+ allows_write_to_pm: z.boolean().optional(),
16
+ photo_url: z.string().optional()
17
+ }).transform((user) => ({
18
+ id: user.id,
19
+ firstName: user.first_name,
20
+ ...user.is_bot !== undefined && { isBot: user.is_bot },
21
+ ...user.last_name !== undefined && { lastName: user.last_name },
22
+ ...user.username !== undefined && { username: user.username },
23
+ ...user.language_code !== undefined && { languageCode: user.language_code },
24
+ ...user.is_premium !== undefined && { isPremium: user.is_premium },
25
+ ...user.allows_write_to_pm !== undefined && { allowsWriteToPm: user.allows_write_to_pm },
26
+ ...user.photo_url !== undefined && { photoUrl: user.photo_url }
27
+ }));
28
+ var encoder = new TextEncoder;
29
+ async function hmacSha256(key, message) {
30
+ const imported = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
31
+ return crypto.subtle.sign("HMAC", imported, encoder.encode(message));
32
+ }
33
+ function toHex(buffer) {
34
+ return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
35
+ }
36
+ function digestsEqual(a, b) {
37
+ if (a.length !== b.length)
38
+ return false;
39
+ let difference = 0;
40
+ for (let index = 0;index < a.length; index += 1) {
41
+ difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
42
+ }
43
+ return difference === 0;
44
+ }
45
+ function optionalUser(raw, key) {
46
+ const encoded = raw[key];
47
+ if (encoded === undefined)
48
+ return;
49
+ return TelegramInitDataUserSchema.parse(JSON.parse(encoded));
50
+ }
51
+ async function verifyTelegramInitData(options) {
52
+ if (!options.botToken)
53
+ throw new TypeError("botToken is required to verify initData");
54
+ if (options.maxAgeSeconds !== undefined && (!Number.isFinite(options.maxAgeSeconds) || options.maxAgeSeconds < 0)) {
55
+ throw new TypeError("maxAgeSeconds must be a non-negative finite number");
56
+ }
57
+ const params = new URLSearchParams(options.initData);
58
+ const hash = params.get("hash");
59
+ if (!hash)
60
+ return { valid: false, reason: "missing-hash" };
61
+ const signed = {};
62
+ for (const [key, value] of params) {
63
+ if (key === "hash")
64
+ continue;
65
+ signed[key] = value;
66
+ }
67
+ const dataCheckString = Object.keys(signed).sort().map((key) => `${key}=${signed[key]}`).join(`
68
+ `);
69
+ const secretKey = await hmacSha256(encoder.encode("WebAppData"), options.botToken);
70
+ const expected = toHex(await hmacSha256(secretKey, dataCheckString));
71
+ if (!digestsEqual(expected, hash.toLowerCase())) {
72
+ return { valid: false, reason: "signature-mismatch" };
73
+ }
74
+ const authDateSeconds = Number(signed.auth_date);
75
+ if (!Number.isSafeInteger(authDateSeconds) || authDateSeconds <= 0) {
76
+ return { valid: false, reason: "malformed" };
77
+ }
78
+ const nowMs = options.now?.() ?? Date.now();
79
+ const ageSeconds = Math.floor(nowMs / 1000) - authDateSeconds;
80
+ if (options.maxAgeSeconds !== undefined && ageSeconds > options.maxAgeSeconds) {
81
+ return { valid: false, reason: "expired", ageSeconds };
82
+ }
83
+ let user;
84
+ let receiver;
85
+ try {
86
+ user = optionalUser(signed, "user");
87
+ receiver = optionalUser(signed, "receiver");
88
+ } catch {
89
+ return { valid: false, reason: "malformed" };
90
+ }
91
+ return {
92
+ valid: true,
93
+ data: {
94
+ ...user !== undefined && { user },
95
+ ...receiver !== undefined && { receiver },
96
+ authDate: new Date(authDateSeconds * 1000),
97
+ ageSeconds,
98
+ ...signed.query_id !== undefined && { queryId: signed.query_id },
99
+ ...signed.start_param !== undefined && { startParam: signed.start_param },
100
+ ...signed.chat_type !== undefined && { chatType: signed.chat_type },
101
+ ...signed.chat_instance !== undefined && { chatInstance: signed.chat_instance },
102
+ raw: Object.freeze({ ...signed })
103
+ }
104
+ };
105
+ }
106
+ // src/telegram/send-failure.ts
107
+ var BY_DESCRIPTION = [
108
+ [/bot was blocked by the user|bot was kicked/i, "blocked-by-user"],
109
+ [/user is deactivated|account is deactivated/i, "user-deactivated"],
110
+ [/can'?t initiate conversation|need to start a conversation/i, "not-started"],
111
+ [/chat not found|peer_id_invalid|user not found/i, "chat-not-found"],
112
+ [/too many requests|retry later|flood/i, "rate-limited"],
113
+ [
114
+ /message is too long|can'?t parse entities|message text is empty|wrong file identifier|button_url_invalid/i,
115
+ "message-invalid"
116
+ ]
117
+ ];
118
+ var RETRYABLE = new Set([
119
+ "rate-limited",
120
+ "server-error"
121
+ ]);
122
+ var UNREACHABLE = new Set([
123
+ "blocked-by-user",
124
+ "user-deactivated",
125
+ "chat-not-found",
126
+ "not-started"
127
+ ]);
128
+ function numberAt(value, keys) {
129
+ if (!isRecord(value))
130
+ return;
131
+ for (const key of keys) {
132
+ const candidate = value[key];
133
+ if (typeof candidate === "number" && Number.isFinite(candidate))
134
+ return candidate;
135
+ }
136
+ return;
137
+ }
138
+ function statusOf(value, depth = 0) {
139
+ if (!isRecord(value) || depth > 4)
140
+ return;
141
+ const direct = numberAt(value, ["error_code", "statusCode", "status"]);
142
+ if (direct !== undefined && direct >= 400 && direct < 600)
143
+ return direct;
144
+ const nested = value.cause ?? value.response ?? value.error;
145
+ return nested === value ? undefined : statusOf(nested, depth + 1);
146
+ }
147
+ function retryAfterOf(value, depth = 0) {
148
+ if (!isRecord(value) || depth > 4)
149
+ return;
150
+ const direct = numberAt(value.parameters, ["retry_after"]);
151
+ if (direct !== undefined)
152
+ return direct;
153
+ const nested = value.cause ?? value.response ?? value.error;
154
+ return nested === value ? undefined : retryAfterOf(nested, depth + 1);
155
+ }
156
+ function descriptionOf(value, depth = 0) {
157
+ if (typeof value === "string")
158
+ return value;
159
+ if (value instanceof Error) {
160
+ const nested2 = isRecord(value) ? descriptionOf(value.cause, depth + 1) : "";
161
+ return `${value.message} ${nested2}`.trim();
162
+ }
163
+ if (!isRecord(value) || depth > 4)
164
+ return "";
165
+ const own = typeof value.description === "string" ? value.description : "";
166
+ const message = typeof value.message === "string" ? value.message : "";
167
+ const nested = descriptionOf(value.cause ?? value.response ?? value.error, depth + 1);
168
+ return [own, message, nested].filter(Boolean).join(" ");
169
+ }
170
+ function classifyTelegramSendFailure(error) {
171
+ const status = statusOf(error);
172
+ const retryAfterSeconds = retryAfterOf(error);
173
+ const description = descriptionOf(error);
174
+ if (retryAfterSeconds !== undefined) {
175
+ return {
176
+ reason: "rate-limited",
177
+ ...status !== undefined && { status },
178
+ retryAfterSeconds,
179
+ retryable: true,
180
+ recipientUnreachable: false,
181
+ evidence: "parameters"
182
+ };
183
+ }
184
+ for (const [pattern, reason] of BY_DESCRIPTION) {
185
+ if (pattern.test(description)) {
186
+ return {
187
+ reason,
188
+ ...status !== undefined && { status },
189
+ retryable: RETRYABLE.has(reason),
190
+ recipientUnreachable: UNREACHABLE.has(reason),
191
+ evidence: "description"
192
+ };
193
+ }
194
+ }
195
+ if (status === 429) {
196
+ return {
197
+ reason: "rate-limited",
198
+ status,
199
+ retryable: true,
200
+ recipientUnreachable: false,
201
+ evidence: "status"
202
+ };
203
+ }
204
+ if (status !== undefined && status >= 500) {
205
+ return {
206
+ reason: "server-error",
207
+ status,
208
+ retryable: true,
209
+ recipientUnreachable: false,
210
+ evidence: "status"
211
+ };
212
+ }
213
+ return {
214
+ reason: "unknown",
215
+ ...status !== undefined && { status },
216
+ retryable: false,
217
+ recipientUnreachable: false,
218
+ evidence: "none"
219
+ };
220
+ }
221
+ export {
222
+ verifyTelegramInitData,
223
+ classifyTelegramSendFailure
224
+ };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createToolInvoker
3
- } from "./index-ekkt4gy9.js";
4
- import"./index-ejv31h14.js";
5
- import"./index-qm87g51s.js";
6
- import"./index-w9m1wznn.js";
3
+ } from "./index-jqtsc9mj.js";
4
+ import"./index-7qy2ex0m.js";
5
+ import"./index-s4c8wy8m.js";
6
+ import"./index-nemjkxjp.js";
7
7
  import"./index-22by16v6.js";
8
8
  import"./index-cby4ar3v.js";
9
9
  import"./index-nt1mp8km.js";
package/dist/tools.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  buildToolManifest,
6
6
  mountAgent
7
- } from "./index-trrf7nch.js";
7
+ } from "./index-ezmn6ac6.js";
8
8
  import"./index-3xnq72rz.js";
9
9
  import {
10
10
  signJwt,
@@ -20,7 +20,7 @@ import {
20
20
  } from "./index-vbf2p6me.js";
21
21
  import {
22
22
  createToolInvoker
23
- } from "./index-ekkt4gy9.js";
23
+ } from "./index-jqtsc9mj.js";
24
24
  import {
25
25
  WaitTimeoutError,
26
26
  createCli,
@@ -29,10 +29,10 @@ import {
29
29
  fetchPinnedDocument,
30
30
  readCapped,
31
31
  runWaitOperation
32
- } from "./index-tnazr5tc.js";
32
+ } from "./index-pz7ytdga.js";
33
33
  import {
34
34
  collectToolSurface
35
- } from "./index-hxtm2xr2.js";
35
+ } from "./index-k2zczx1g.js";
36
36
  import {
37
37
  createRuntimeToolFactory,
38
38
  defineRuntimeTool
@@ -41,19 +41,19 @@ import {
41
41
  collectTools,
42
42
  createToolRunner,
43
43
  formatToolError
44
- } from "./index-ejv31h14.js";
44
+ } from "./index-7qy2ex0m.js";
45
45
  import {
46
46
  ToolExecutionControlError,
47
47
  coerceJsonArgs,
48
48
  executeToolMethod,
49
49
  isToolExecutionControlError,
50
50
  toolResultFromError
51
- } from "./index-qm87g51s.js";
51
+ } from "./index-s4c8wy8m.js";
52
52
  import {
53
53
  getRequestContext,
54
54
  getTraceId,
55
55
  runWithRequestContext
56
- } from "./index-w9m1wznn.js";
56
+ } from "./index-nemjkxjp.js";
57
57
  import {
58
58
  ManagedFileError
59
59
  } from "./index-bfcpjw20.js";
package/llms-full.txt CHANGED
@@ -55,11 +55,12 @@ own, recorded as an ADR.
55
55
  | `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
56
56
  | `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
57
57
  | `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
58
+ | `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification and Bot API send-failure classification |
58
59
  | `stitchkit/observability` | server | stable | request/tool event projections — `createObservability`, trace context, sanitisation |
59
60
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
60
61
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
61
62
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
62
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 10 of the 15 minors since 0.56.2, most recently 0.69.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
63
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 10 of the 16 minors since 0.56.2, most recently 0.69.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
63
64
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
64
65
  | `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
65
66
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -67,7 +68,7 @@ own, recorded as an ADR.
67
68
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
68
69
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
69
70
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
70
- | `stitchkit/application` | server | evolving<br>_redefined in 3 of the 15 minors since 0.56.2, most recently 0.67.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
71
+ | `stitchkit/application` | server | evolving<br>_redefined in 3 of the 16 minors since 0.56.2, most recently 0.67.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
71
72
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
72
73
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
73
74
 
@@ -180,6 +181,7 @@ map — feature → packages:
180
181
  | **Socket.IO server on Node** | `socket.io` |
181
182
  | Socket.IO client | `socket.io-client` (runtime peer; unrelated root declarations remain peer-free) |
182
183
  | grammY lifecycle adapters (`stitchkit/application/grammy`) | `grammy` |
184
+ | Telegram platform primitives (`stitchkit/telegram`) | — (peer-free) |
183
185
  | OpenTelemetry gauges (`stitchkit/application/opentelemetry`) | `@opentelemetry/api` |
184
186
 
185
187
  ```bash
@@ -4101,7 +4103,8 @@ responses and results with a different call or tool name are invalid; dropping a
4101
4103
  approval input fails the run with a private diagnostic rather than starting a fresh model turn.
4102
4104
 
4103
4105
  `stitchkit/agent-runtime/coding-tools` returns ordinary direct runtime tools named `read_file`,
4104
- `write_file`, `search_files`, `apply_patch`, `run_command` and optional `read_output`. Every call passes a
4106
+ `write_file`, `edit_file`, `list_directory`, `glob`, `search_files`, `run_command` and optional
4107
+ `read_output`. Every call passes a
4105
4108
  required host authorization callback. File paths are relative, bounded and contained after
4106
4109
  descriptor-relative resolution: each ancestor is opened without following symlinks and remains
4107
4110
  pinned through authorization and the filesystem effect. Reads revalidate the pinned file identity;
@@ -4117,10 +4120,32 @@ Arguments, output and time are bounded, while cancellation terminates the child.
4117
4120
  root and cwd are path boundaries, not a security sandbox: isolate the process when an executable
4118
4121
  must not access the rest of the machine.
4119
4122
 
4120
- `apply_patch` binds the exact source to `baseSha256`, supports dry-run, authorizes the exact
4121
- replacement count, result digest and byte size, then rechecks the base under a per-target lock
4122
- before same-directory atomic replacement. It deliberately does not claim
4123
- multi-file atomicity. With an optional `AgentCodingArtifactStore`, shell output beyond the inline
4123
+ `edit_file` replaces one exact snippet. `oldText` is itself the freshness guard for the region it
4124
+ changes, so the digest is the optional `expectedSha256` and an edit is one call; pass the digest
4125
+ `read_file` returned when you want the whole-file guarantee, and read the `sha256` it returns to
4126
+ chain the next edit without re-reading. The read, the occurrence count and the construction of the
4127
+ new content all happen inside one per-target lock, so two concurrent edits of different snippets in
4128
+ one file cannot each build a file from the same base and have the second erase the first — the lock
4129
+ is process-local, which covers many agents in one process and not two processes over one workspace.
4130
+ It authorizes the exact replacement count, result digest and byte size, and deliberately does not
4131
+ claim multi-file atomicity.
4132
+
4133
+ `write_file` creates missing parent directories inside the root and names them in
4134
+ `createdDirectories`; the walk that finds them runs before authorization and reports them in the
4135
+ authorization payload, so no directory is created before a host approves it.
4136
+
4137
+ **Every ordinary outcome is a refusal a model can act on** — a missing file is `NOT_FOUND`, an
4138
+ existing file without `overwrite` is `CONFLICT`, an ambiguous snippet is `CONFLICT` carrying its
4139
+ occurrence count, a path outside the root is `FORBIDDEN` — each with a `hint` naming the next move.
4140
+ Host-level causes stay scrubbed to `INTERNAL_SERVER_ERROR` and name nothing outside the workspace.
4141
+ → ADR 0139
4142
+
4143
+ `list_directory` marks excluded directories rather than hiding them, and `glob` reports
4144
+ `skippedDirectories` beside its matches: an empty result from a tree whose files all live under an
4145
+ excluded directory is not "no files", and a model told only "nothing found" concludes the wrong
4146
+ thing. `search_files` takes `regex`, `context` lines and an `include` pattern; regex is bounded by
4147
+ refusing backreferences and lookaround and capping line length rather than by a timeout, because a
4148
+ JavaScript `RegExp` cannot be interrupted once it starts backtracking. With an optional `AgentCodingArtifactStore`, shell output beyond the inline
4124
4149
  preview continues into an opaque bounded artifact and `read_output` reads slices without
4125
4150
  exposing a host path. Without a store, the previous finite output-limit behavior is unchanged.
4126
4151
 
@@ -4807,6 +4832,40 @@ replace a leading summary on the next compaction.
4807
4832
  Set `maxAttempts` to allow bounded conflict recovery. Every retry reloads the snapshot, reselects the
4808
4833
  eligible range and recomputes the summary; the stale summary is never retried.
4809
4834
 
4835
+ ### Compacting without the store
4836
+
4837
+ The *selection* inside compaction needs no store, no version and no runtime, so
4838
+ it is published on its own as `selectCompactableHistory` — for an application
4839
+ that drives the model itself and keeps its own history (→ ADR 0142):
4840
+
4841
+ ```ts
4842
+ import { selectCompactableHistory } from 'stitchkit/agent-runtime'
4843
+
4844
+ const { leadingSummary, compactable, retained } = selectCompactableHistory({
4845
+ messages,
4846
+ keepRecentTurns: 3,
4847
+ })
4848
+ if (compactable.length > 0) {
4849
+ const summary = await summarize(compactable, leadingSummary)
4850
+ messages = [summary, ...retained] // replace the old summary, never stack one
4851
+ }
4852
+ ```
4853
+
4854
+ `compactable` is the oldest **whole complete** turns and `retained` is
4855
+ everything the model must still hear; together they are the input, in order. A
4856
+ turn holding a tool call whose result never arrived is never eligible, and
4857
+ neither is anything after it: half a turn hands the provider a call with no
4858
+ result, which most of them refuse outright.
4859
+
4860
+ `structuredCompaction` calls this same function, so the published selection and
4861
+ the runtime's cannot drift into two behaviours. What it adds is the part that
4862
+ does need the store — writing the result back under a version check.
4863
+
4864
+ Deciding *when* to compact is yours: `AgentRuntimeRunContext.contextUsage`
4865
+ carries the last step's prompt size beside the model's window, and dividing them
4866
+ is one line where the threshold is decided. The core does not own that ratio, on
4867
+ purpose — see ADR 0142 for what it publishes and what it declines to.
4868
+
4810
4869
  ## Observability
4811
4870
 
4812
4871
  `createAgentObservability` emits a separate operator-only `AgentRunEvent`. It
@@ -7445,6 +7504,68 @@ const payload = await verifyJwt(token, env.JWT_SECRET)
7445
7504
  `extractToken(req, cookieName?)` reads a bearer token from the `Authorization`
7446
7505
  header, or from the named cookie.
7447
7506
 
7507
+ ## Telegram Mini Apps
7508
+
7509
+ A Mini App hands its backend an `initData` query string that Telegram signed
7510
+ with a key derived from the bot token. `verifyTelegramInitData` checks that
7511
+ signature and reports what the string says — and refuses with a **reason**,
7512
+ because an application answers a stale string differently from a forged one.
7513
+
7514
+ ```ts
7515
+ import { verifyTelegramInitData } from 'stitchkit/telegram'
7516
+ import { unauthorized } from 'stitchkit/contract'
7517
+ import { env } from './env'
7518
+
7519
+ const result = await verifyTelegramInitData({
7520
+ initData,
7521
+ botToken: env.BOT_TOKEN,
7522
+ maxAgeSeconds: 3600, // a signed string is otherwise valid forever
7523
+ })
7524
+ if (!result.valid) {
7525
+ // 'missing-hash' | 'signature-mismatch' | 'malformed' | 'expired'
7526
+ throw unauthorized(result.reason === 'expired' ? 'Reopen the app' : 'Invalid session')
7527
+ }
7528
+ const telegramId = result.data.user?.id
7529
+ ```
7530
+
7531
+ Three things it does that a hand-written check usually does not:
7532
+
7533
+ - the **signature is checked first**, before `auth_date` or anything else is
7534
+ read — an expiry inside an unverified payload is a number the sender chose;
7535
+ - digests are compared **without an early exit**, so the time a rejection takes
7536
+ is not a measurement of how much of the digest was right;
7537
+ - `raw` keeps every signed pair, so a field Telegram adds after this release
7538
+ does not need a release to reach.
7539
+
7540
+ `maxAgeSeconds` is optional and there is no default: only the application knows
7541
+ how long its own session is worth. Omitting it means the string never expires,
7542
+ which is a decision rather than an oversight.
7543
+
7544
+ The module is server-only and pulls in no bot library — the token never belongs
7545
+ in a browser bundle. `stitchkit/application/grammy` remains the lifecycle
7546
+ adapter for an injected grammY bot. Its companion,
7547
+ `classifyTelegramSendFailure`, names why a send was refused and separates *retry
7548
+ this send* from *stop addressing this recipient*:
7549
+
7550
+ ```ts
7551
+ import { classifyTelegramSendFailure } from 'stitchkit/telegram'
7552
+
7553
+ try {
7554
+ await bot.api.sendMessage(chatId, text)
7555
+ } catch (error) {
7556
+ const failure = classifyTelegramSendFailure(error)
7557
+ if (failure.recipientUnreachable) await markUnreachable(chatId, failure.reason)
7558
+ else if (failure.retryable) await requeue(chatId, failure.retryAfterSeconds)
7559
+ }
7560
+ ```
7561
+
7562
+ The two flags are separate because one answer cannot serve both. A rate limit is
7563
+ retryable and implicates nobody; a blocked user is unreachable and no retry
7564
+ helps; a message Telegram could not parse is *neither* — the recipient is fine
7565
+ and our payload is wrong, which is the case a list of substrings quietly counts
7566
+ against the user. An unrecognised refusal leaves the recipient reachable: losing
7567
+ a working subscriber forever costs more than one wasted send.
7568
+
7448
7569
  ## Cookies
7449
7570
 
7450
7571
  ```ts
@@ -9172,6 +9293,94 @@ additive** — adopting it changes nothing in your code. (See
9172
9293
  So upgrading is: read the `### ⚠️ Breaking changes` of every version *above* your
9173
9294
  current one *up to* your target, and apply each snippet.
9174
9295
 
9296
+ ## Released migration: 0.71.0
9297
+
9298
+ The Agent coding tools. Two of the three changes are visible to the compiler;
9299
+ the third is the one to read carefully, because nothing will point at it.
9300
+
9301
+ ### If you implement `authorize`
9302
+
9303
+ The operation union gained `edit`, `list` and `glob` and lost `patch`. An
9304
+ exhaustive matcher stops compiling and the compiler shows you every arm. **A
9305
+ matcher with a default branch does not**, and that is the dangerous case:
9306
+
9307
+ ```ts
9308
+ // before — and after this upgrade, silently wrong in both directions
9309
+ if (request.operation === 'patch') return reviewPatch(request)
9310
+ return true // now also authorizes edit, list and glob
9311
+ // …or: return false // now also kills edit_file
9312
+ ```
9313
+
9314
+ ```ts
9315
+ // after
9316
+ switch (request.operation) {
9317
+ case 'edit': return reviewEdit(request) // the old `patch` payload, unchanged
9318
+ case 'list':
9319
+ case 'glob': return true // or your own policy
9320
+ // …existing read / write / search / shell / artifact-read arms
9321
+ }
9322
+ ```
9323
+
9324
+ `write` gained `createsDirectories` — the workspace-relative directories the
9325
+ call would create, outermost first, reported **before** anything is created. A
9326
+ host that wants to refuse implicit directory creation now can.
9327
+
9328
+ ### If you call `apply_patch`
9329
+
9330
+ It is `edit_file`, and it is one call:
9331
+
9332
+ ```ts
9333
+ // before
9334
+ const read = await readFile({ path })
9335
+ await applyPatch({ path, baseSha256: read.sha256, oldText, newText, dryRun: true })
9336
+ await applyPatch({ path, baseSha256: read.sha256, oldText, newText, dryRun: false })
9337
+
9338
+ // after
9339
+ await editFile({ path, oldText, newText })
9340
+ ```
9341
+
9342
+ `expectedSha256` is optional and still refuses a stale base with `CONFLICT` when
9343
+ you pass it; `edit_file` returns the resulting `sha256`, so a chain of edits
9344
+ never needs to re-read. If you key an approval policy or a UI label on the string
9345
+ `apply_patch`, update the key — nothing will fail loudly.
9346
+
9347
+ ### If you match on `INTERNAL_SERVER_ERROR` from a coding tool
9348
+
9349
+ Ordinary outcomes no longer arrive that way. A missing file is `NOT_FOUND`, an
9350
+ existing file without `overwrite` is `CONFLICT`, an ambiguous snippet is
9351
+ `CONFLICT` with the occurrence count, a path outside the root is `FORBIDDEN`.
9352
+ Code that treated any coding-tool failure as an internal fault will now see
9353
+ codes it did not before; code that showed the model an empty error now has a
9354
+ sentence and a `hint` to show it. Host-level causes are unchanged and still
9355
+ scrubbed.
9356
+
9357
+ ### If you write files into new directories
9358
+
9359
+ Nothing to change: `write_file` creates missing parents inside the root. Read
9360
+ `createdDirectories` in the result if you want to notice a typo — a path that
9361
+ was a failure before is now a successful write into a new tree.
9362
+
9363
+ ### If you want a step to know its context budget
9364
+
9365
+ Opt in by reading it; nothing is injected for you:
9366
+
9367
+ ```ts
9368
+ loop: {
9369
+ prepareStep: ({ contextUsage }) => {
9370
+ const used = contextUsage?.usedTokens
9371
+ if (used?.provenance === 'unavailable') return {} // no step has landed yet
9372
+ const fraction = (used?.value ?? 0) / (contextUsage?.contextWindow ?? 1)
9373
+ // …render it wherever you put it
9374
+ return {}
9375
+ },
9376
+ }
9377
+ ```
9378
+
9379
+ Put it at the **tail** of the conversation rather than in the system
9380
+ instructions unless you have a reason: changing the system prompt on every step
9381
+ invalidates the provider's prefix cache for the whole conversation, and on a long
9382
+ run that is a multiple of the input cost.
9383
+
9175
9384
  ## Released migration: 0.70.0
9176
9385
 
9177
9386
  ### Descriptor-backed Agent filesystem containment
@@ -12143,6 +12352,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
12143
12352
  | `generateTraceId` | function | a fresh trace id |
12144
12353
  | `resolveTraceId` | function | the default per-request trace-id resolver |
12145
12354
  | `extractIp` | function | the caller IP from a request |
12355
+ | `isPublicIp` | function | whether an address belongs to the public internet; anything unparseable is not |
12146
12356
  | `resolveSocketIp` | function | the caller IP for a Socket.IO handshake (proxy-aware) |
12147
12357
  | `getClientInfo` | function | caller IP + user-agent |
12148
12358
  | `EventBus` | _type_ | the `createEventBus` handle |
@@ -12397,6 +12607,8 @@ Server-only optional application runtime. See the
12397
12607
  | `AgentConversationMessagePageSchema` / `AgentConversationMessagePage` | schema / _type_ | cursor-paged durable message history |
12398
12608
  | `composeAgentPrompt` | function | ordered prompt contributions and provenance-aware signed context budget; irreducible reservation deficits are `oversized`, not compactable history |
12399
12609
  | `structuredCompaction` | function | summarize a provider-valid snapshot range and replace it through CAS |
12610
+ | `selectCompactableHistory` | function | which oldest whole complete turns may be summarised away — the half of compaction that needs no store (→ ADR 0142) |
12611
+ | `SelectCompactableHistoryOptions` / `CompactableHistory` | _type_ | message list, retained-turn count and evidence policy in; `leadingSummary`, `compactable` and `retained` out |
12400
12612
  | `createAgentSessionCoordinator` | function | strict process-local queue/interrupt/supersede lifecycle |
12401
12613
  | `AgentRuntimeStopPolicy` | _type_ | named custom AI SDK stop condition persisted and published on policy stop |
12402
12614
  | `AgentRuntimePrepareStep` | _type_ | per-run controlled step callback with typed domain context and managed run signal/fence |
@@ -12425,6 +12637,29 @@ transport adapters validate the same records. Runtime composition types are `Age
12425
12637
  `AgentSessionCoordinator`, `AgentCompactionContext`, `AgentCompactionResult` and
12426
12638
  `StructuredCompactionConfig`.
12427
12639
 
12640
+ A provider refusal is classified rather than phrased. `classifyProviderFailure` returns an
12641
+ `AgentProviderFailure` — an `AgentProviderFailureReason` (`insufficient-credits`, `rate-limited`,
12642
+ `model-unavailable`, `context-overflow`, `timeout`, `cancelled`, `unknown`), the provider's `status`
12643
+ when it supplied one, whether the same request is `retryable` unchanged, and the `evidence` the
12644
+ answer rests on: `status` is the provider stating its own answer, `message` is us reading its prose,
12645
+ and `none` is an honest refusal to guess. The sentence a user reads stays with the application —
12646
+ its tone and its decision about what to admit are not the core's to make. `isToolResultFailure`
12647
+ recognises a failure carried inside a *successful* tool result, in both the bare and the
12648
+ `{ value: … }` envelope. Both are plain functions and need no runtime. → ADR 0141
12649
+
12650
+ `normalizeOpenRouterUsage` is the same normalisation `openRouterProvider`
12651
+ applies, exported so an application calling the SDK directly gets provenance-correct numbers
12652
+ without adopting the runtime.
12653
+
12654
+ `AgentContextUsage` reaches every step through `AgentRuntimeRunContext.contextUsage`: how full the
12655
+ model's context is, as `usedTokens` (an `AgentUsageValue`, so it carries the provenance that says
12656
+ where the number came from) beside the model's declared `contextWindow`. It is the **last completed
12657
+ step's prompt size**, not the run's cumulative input tokens — cumulative counts every step's prompt
12658
+ again and is a multiple of the real fill. Before the first step lands there is no provider-reported
12659
+ number and the provenance is `unavailable`, which is a different fact from zero. No fraction is
12660
+ exposed: dividing is one line where it is rendered, and the output reserve belongs to the
12661
+ consumer's prompt budget rather than to this layer.
12662
+
12428
12663
  Canonical protocol exports are `AgentProtocol`, `AgentProtocolConfig`, `AgentTerminalAcceptance`,
12429
12664
  `AgentTerminalAcceptanceInput`, `hasAgentTerminalOutput`, `AgentRecordIdSchema`, `AgentRecordVersionSchema`,
12430
12665
  `AgentTimestampSchema`, `AgentJsonObjectSchema`, `AgentProviderEnvelopeSchema`,
@@ -12566,12 +12801,14 @@ explicit policy; use the Bun or Node SQLite leaf for durable reopen.
12566
12801
  ## `stitchkit/agent-runtime/coding-tools`
12567
12802
 
12568
12803
  Server-only evolving, peer-free direct runtime tools. `createAgentCodingTools(config)` returns
12569
- `read_file`, `write_file`, `search_files`, `apply_patch`, optional `run_command` and, when an
12804
+ `read_file`, `write_file`, `edit_file`, `list_directory`, `glob`, `search_files`, optional
12805
+ `run_command` and, when an
12570
12806
  artifact store is supplied, `read_output`.
12571
12807
 
12572
12808
  | Export | Kind | Summary |
12573
12809
  |--------|------|---------|
12574
- | `createAgentCodingTools` | function | construct direct host-authorized bounded file, search, guarded patch, shell and artifact runtime-tool definitions; filesystem operations use Linux `/proc/self/fd` or the packaged macOS Node-API backend and otherwise fail closed |
12810
+ | `createAgentCodingTools` | function | construct direct host-authorized bounded file, listing, glob, search, exact-snippet edit, shell and artifact runtime-tool definitions; every ordinary refusal is a typed code with an instructive `hint`, and filesystem operations use Linux `/proc/self/fd` or the packaged macOS Node-API backend and otherwise fail closed |
12811
+ | `AGENT_CODING_TOOL_NAMES` | const | the mounted tool names — `read_file`, `write_file`, `edit_file`, `list_directory`, `glob`, `search_files`, `run_command`, `read_output` |
12575
12812
  | `AgentCodingToolDefinition` | _type_ | peer-free structural direct-tool shape accepted by the canonical runtime-tool surface |
12576
12813
  | `AgentCodingToolConfig` | _type_ | absolute root, required authorization callback, finite executable alias map, exact child environment and optional limits |
12577
12814
  | `AgentCodingToolAuthorizationSchema` / `AgentCodingToolAuthorization` | schema / _type_ | discriminated read/write/search/patch/shell/artifact decision presented to host policy before effect |
@@ -13094,6 +13331,26 @@ available from `stitchkit/contract`.
13094
13331
 
13095
13332
  ---
13096
13333
 
13334
+ ## `stitchkit/telegram`
13335
+
13336
+ Peer-free server-only Telegram platform primitives. Importing this resolves no
13337
+ bot library; `stitchkit/application/grammy` remains the lifecycle adapter for an
13338
+ injected grammY bot. → ADR 0143
13339
+
13340
+ | Export | Kind | Summary |
13341
+ |--------|------|---------|
13342
+ | `verifyTelegramInitData` | function | verify a Mini App `initData` signature against the bot token in constant time, before reading anything out of it |
13343
+ | `VerifyTelegramInitDataOptions` | _type_ | raw `initData`, bot token, optional `maxAgeSeconds` bound and injected clock |
13344
+ | `TelegramInitDataVerification` | _type_ | `{ valid: true, data }` or a refusal carrying its reason |
13345
+ | `TelegramInitData` | _type_ | verified `user`/`receiver`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
13346
+ | `TelegramInitDataUser` | _type_ | camelCase user record inferred from Telegram's signed `user` payload |
13347
+ | `TelegramInitDataRefusal` | _type_ | `missing-hash` / `signature-mismatch` / `malformed` / `expired` — an expired string is not a forged one |
13348
+ | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
13349
+ | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
13350
+ | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
13351
+
13352
+ ---
13353
+
13097
13354
  ## `stitchkit/declaration`
13098
13355
 
13099
13356
  Zod-only, dependency-free. The **project declaration**: the single
Binary file
Binary file