stitchkit 0.96.0 → 0.97.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 (35) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agent-runtime/schedule-contract.d.ts +52 -0
  3. package/dist/agent-runtime/schedule-contract.d.ts.map +1 -0
  4. package/dist/agent-runtime/schedule-dispatch.d.ts +26 -0
  5. package/dist/agent-runtime/schedule-dispatch.d.ts.map +1 -0
  6. package/dist/agent-runtime/schedule-records.d.ts +8 -14
  7. package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
  8. package/dist/agent-runtime/schedule-tools.d.ts +1 -1
  9. package/dist/agent-runtime/schedule-tools.d.ts.map +1 -1
  10. package/dist/agent-runtime/schedules.d.ts +7 -92
  11. package/dist/agent-runtime/schedules.d.ts.map +1 -1
  12. package/dist/agent-runtime/sqlite-schema.d.ts.map +1 -1
  13. package/dist/agent-runtime/store-migrations/v3-to-v4.d.ts +4 -0
  14. package/dist/agent-runtime/store-migrations/v3-to-v4.d.ts.map +1 -0
  15. package/dist/entrypoints/agent-runtime/sqlite/bun.js +1 -1
  16. package/dist/entrypoints/agent-runtime/sqlite/node.js +1 -1
  17. package/dist/entrypoints/agent-runtime.d.ts +2 -1
  18. package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
  19. package/dist/entrypoints/agent-runtime.js +289 -214
  20. package/dist/entrypoints/telegram.js +15 -2
  21. package/dist/{index-p9e0w8hs.js → index-1m2thnq0.js} +25 -1
  22. package/dist/telegram/operator-channel.d.ts +6 -0
  23. package/dist/telegram/operator-channel.d.ts.map +1 -1
  24. package/llms/agent-runtime.2.txt +224 -0
  25. package/llms/agent-runtime.3.txt +1 -1
  26. package/llms/agent-runtime.txt +18 -225
  27. package/llms/stitchkit.txt +3 -3
  28. package/llms/telegram.txt +5 -2
  29. package/llms/{upgrading-0.37.0-0.56.0.txt → upgrading-0.37.0-0.58.0.txt} +82 -3
  30. package/llms/{upgrading-0.57.0-0.69.0.txt → upgrading-0.59.0-0.71.0.txt} +108 -82
  31. package/llms/{upgrading-0.70.0-0.96.0.txt → upgrading-0.72.0-0.97.0.txt} +39 -108
  32. package/llms/upgrading.txt +3 -3
  33. package/llms-full.txt +63 -7
  34. package/llms.txt +5 -5
  35. package/package.json +1 -1
@@ -636,6 +636,17 @@ function createTelegramLocalFiles(config) {
636
636
  }
637
637
  // src/telegram/operator-channel.ts
638
638
  var TEXT_LIMIT = 4096;
639
+ var MASK = "[redacted]";
640
+ function masked(text, patterns) {
641
+ let output = text;
642
+ for (const pattern of patterns) {
643
+ output = output.replace(new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`), MASK);
644
+ }
645
+ return output;
646
+ }
647
+ function literal(value) {
648
+ return new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
649
+ }
639
650
  function timer(milliseconds, signal) {
640
651
  return new Promise((resolve2, reject) => {
641
652
  if (signal.aborted)
@@ -660,6 +671,7 @@ function createTelegramOperatorChannel(config) {
660
671
  const minIntervalMs = config.minIntervalMs ?? 3000;
661
672
  const maxAttempts = config.maxAttempts ?? 3;
662
673
  const sleep = config.sleep ?? timer;
674
+ const patterns = [TELEGRAM_BOT_TOKEN_PATTERN, ...config.sensitivePatterns ?? []];
663
675
  const queue = [];
664
676
  const idle = new Set;
665
677
  let lifetime = new AbortController;
@@ -740,7 +752,7 @@ function createTelegramOperatorChannel(config) {
740
752
  try {
741
753
  const item = {
742
754
  ...topic !== undefined && { topic },
743
- text,
755
+ text: masked(text, patterns),
744
756
  attempts: 0
745
757
  };
746
758
  if (closed) {
@@ -786,12 +798,13 @@ function createTelegramOperatorChannel(config) {
786
798
  };
787
799
  }
788
800
  function telegramOperatorSender(config) {
801
+ const token = literal(config.token);
789
802
  return (message, signal) => callTelegramBotApi({
790
803
  token: config.token,
791
804
  method: "sendMessage",
792
805
  params: {
793
806
  chat_id: message.chatId,
794
- text: message.text,
807
+ text: masked(message.text, [token]),
795
808
  ...message.threadId !== undefined && { message_thread_id: message.threadId },
796
809
  ...config.parseMode && { parse_mode: config.parseMode },
797
810
  link_preview_options: { is_disabled: true }
@@ -337,10 +337,25 @@ function migrateAgentRuntimeSqliteV2ToV3(database) {
337
337
  database.prepare("UPDATE stitchkit_agent_runtime_meta SET value = '3' WHERE key = 'schema_version'").run();
338
338
  }
339
339
 
340
+ // src/agent-runtime/store-migrations/v3-to-v4.ts
341
+ function migrateAgentRuntimeSqliteV3ToV4(database) {
342
+ database.exec(`
343
+ ALTER TABLE stitchkit_agent_runtime_schedules ADD COLUMN retry_at TEXT;
344
+ ALTER TABLE stitchkit_agent_runtime_schedules ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0;
345
+ ALTER TABLE stitchkit_agent_runtime_schedules ADD COLUMN last_error TEXT;
346
+ ALTER TABLE stitchkit_agent_runtime_schedules ADD COLUMN eligible_at TEXT
347
+ GENERATED ALWAYS AS (MAX(next_at, COALESCE(retry_at, next_at), COALESCE(claim_until, next_at))) VIRTUAL;
348
+ DROP INDEX stitchkit_agent_runtime_schedules_due;
349
+ CREATE INDEX stitchkit_agent_runtime_schedules_due
350
+ ON stitchkit_agent_runtime_schedules (eligible_at, id) WHERE state = 'scheduled';
351
+ UPDATE stitchkit_agent_runtime_meta SET value = '4' WHERE key = 'schema_version';
352
+ `);
353
+ }
354
+
340
355
  // src/agent-runtime/sqlite-schema.ts
341
356
  var MetaRowSchema = z3.object({ value: z3.string() });
342
357
  var TableRowSchema = z3.object({ name: z3.string() });
343
- var SCHEMA_VERSION = 3;
358
+ var SCHEMA_VERSION = 4;
344
359
  var TABLES = [
345
360
  "stitchkit_agent_runtime_heads",
346
361
  "stitchkit_agent_runtime_runs",
@@ -373,12 +388,20 @@ function initializeAgentRuntimeSqlite(database) {
373
388
  if (version === 1) {
374
389
  migrateAgentRuntimeSqliteV1ToV2(database);
375
390
  migrateAgentRuntimeSqliteV2ToV3(database);
391
+ migrateAgentRuntimeSqliteV3ToV4(database);
376
392
  initializeSqliteConversationPurge(database);
377
393
  database.exec("COMMIT");
378
394
  return;
379
395
  }
380
396
  if (version === 2) {
381
397
  migrateAgentRuntimeSqliteV2ToV3(database);
398
+ migrateAgentRuntimeSqliteV3ToV4(database);
399
+ initializeSqliteConversationPurge(database);
400
+ database.exec("COMMIT");
401
+ return;
402
+ }
403
+ if (version === 3) {
404
+ migrateAgentRuntimeSqliteV3ToV4(database);
382
405
  initializeSqliteConversationPurge(database);
383
406
  database.exec("COMMIT");
384
407
  return;
@@ -437,6 +460,7 @@ function initializeAgentRuntimeSqlite(database) {
437
460
  createAgentRuntimeSqliteV2Tables(database);
438
461
  createAgentRuntimeSqliteV3Tables(database);
439
462
  database.prepare("INSERT INTO stitchkit_agent_runtime_meta (key, value) VALUES ('schema_version', '3')").run();
463
+ migrateAgentRuntimeSqliteV3ToV4(database);
440
464
  initializeSqliteConversationPurge(database);
441
465
  database.exec("COMMIT");
442
466
  } catch (error) {
@@ -48,6 +48,12 @@ export interface TelegramOperatorChannelConfig<TTopic extends string> {
48
48
  readonly onDropped?: (drop: TelegramOperatorDrop<TTopic>) => void;
49
49
  /** Default: a timer. Rejects when `signal` aborts. */
50
50
  readonly sleep?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
51
+ /**
52
+ * More secrets to mask in every message before it leaves. A bot token is
53
+ * always masked: bots post their errors here, and an error about a local Bot
54
+ * API file carries `<root>/<token>/…` in its path.
55
+ */
56
+ readonly sensitivePatterns?: readonly RegExp[];
51
57
  }
52
58
  export interface TelegramOperatorChannel<TTopic extends string> {
53
59
  /** Queue one message. Returns at once and never throws. */
@@ -1 +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"}
1
+ {"version":3,"file":"operator-channel.d.ts","sourceRoot":"","sources":["../../src/telegram/operator-channel.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,230 @@ Parts of this slice:
13
13
  # Guide: Agent application runtime (docs/guide/agent-runtime.md) (continued)
14
14
  ==============================================================================
15
15
 
16
+ ## What happens to a run when new input arrives
17
+
18
+ `runs.inputPolicy` decides. It takes five values, or a function returning one:
19
+
20
+ | policy | the run in flight | what it already produced |
21
+ |--------|-------------------|--------------------------|
22
+ | `queue` (default) | finishes first | kept |
23
+ | `inject` | continues, and answers the new input too | kept, and built on |
24
+ | `interrupt` | ends | kept, and marked as cut off |
25
+ | `interrupt-next` | ends; this input runs before ordinary queued work | kept, and marked as cut off |
26
+ | `supersede` | ends | discarded from the prompt, kept in the record |
27
+
28
+ `interrupt-next` is the explicit priority path for an urgent input. If A is
29
+ running, ordinary B is queued, and urgent C arrives, the coordinator aborts A,
30
+ waits for A to settle and then runs C before B. B keeps its durable identity and
31
+ eventually runs. Urgent inputs remain FIFO among themselves. They do not
32
+ coalesce into an ordinary pending run: that would erase the priority boundary.
33
+
34
+ `interrupt` and `supersede` differ in exactly one thing, and the question that
35
+ picks between them is **not** "was the run interrupted" but **"did anyone see
36
+ what it produced"**:
37
+
38
+ - The user pressed **stop**. The partial answer was streamed to their screen and
39
+ they read it. It belongs in the conversation — dropping it makes the history
40
+ lie to the model about what the human has seen. That is `interrupt`.
41
+ - A newer message **superseded** the run. Whether the partial reached anyone
42
+ depends on the delivery surface: a token stream shows it as it is produced, a
43
+ surface that sends nothing until the run is done never showed it at all. When
44
+ it reached nobody, it is not part of the conversation. That is `supersede`.
45
+
46
+ **Stitchkit cannot answer that question for you** — delivery belongs to the
47
+ transport, and the runtime sees an abort, not a screen. Hence a declared policy
48
+ (→ ADR 0108), and hence `inputPolicy` accepting a function, so one application
49
+ can hold two surfaces with different rules without the core learning which is
50
+ which:
51
+
52
+ ```ts
53
+ runs: {
54
+ inputPolicy: (input) =>
55
+ protocol.parseContext(input.context).surface === 'operator' ? 'queue' : 'supersede',
56
+ }
57
+ ```
58
+
59
+ `input.context` is the **raw** context here — admission runs before the runtime
60
+ parses it, so the callback narrows it itself with the protocol it already has.
61
+
62
+ A superseded run ends with `terminalReason: 'superseded'`, run state
63
+ `'superseded'` and an assistant message of status `'superseded'`. **The record
64
+ is kept** — excluded from the projection, not deleted — so an operator can see
65
+ what was thrown away, and run identity, admission receipts and the terminal CAS
66
+ keep the row they depend on. Compaction leaves it alone for the same reason: a
67
+ turn whose answer is never spoken is not a turn that may be summarised into one,
68
+ because that would both feed the discarded text to the summariser and drop the
69
+ record in `replacedMessageIds`.
70
+
71
+ It is also outside the token budget. `selectAgentHistory` removes it with reason
72
+ `'superseded'` and does not count it, so an abandoned fragment cannot push a
73
+ real turn out of a context it never occupies.
74
+
75
+ ### How an interrupted turn reaches the model
76
+
77
+ An interrupted turn is projected, and says so:
78
+
79
+ ```text
80
+ { role: 'assistant', content: [
81
+ { type: 'text', text: 'We are the team, where would you like' },
82
+ { type: 'text', text: '[interrupted: this turn was cut off before it finished]' },
83
+ ]}
84
+ ```
85
+
86
+ `history.interruptedAssistant` chooses the form, and the difference between the
87
+ first two is structural rather than cosmetic. **An assistant turn in provider
88
+ history is a commitment**: the model reads its own previous turn as something it
89
+ said and stays consistent with it. A system line is context.
90
+
91
+ | value | form | right when |
92
+ |-------|------|-----------|
93
+ | `assistant-marked` (default) | assistant turn plus a marker | the human read the text |
94
+ | `system-note` | `[interrupted] partial response: …` as a system line | the fragment reached nobody |
95
+ | `omit` | not projected at all | you want it gone from the request |
96
+
97
+ There is deliberately no value that reproduces what the projection used to do,
98
+ which was to send the partial as an ordinary assistant turn and drop its
99
+ `control` marker on the way. That was the defect, not a behaviour to stay
100
+ compatible with.
101
+
102
+ `projectAgentHistoryDetailed` reports what reached the provider, including part
103
+ types that no projected content stands for:
104
+
105
+ ```ts
106
+ const { decisions } = await projectAgentHistoryDetailed(snapshot.messages)
107
+ // → { messageId: 'assistant-2', action: 'projected', reason: 'projected',
108
+ // omittedParts: ['source', 'provider'] }
109
+ // → { messageId: 'assistant-1', action: 'omitted', reason: 'superseded' }
110
+ ```
111
+
112
+ `runtime.stop(key, 'supersede')` is the same decision taken by hand: the
113
+ process-local escape hatch chooses the reason, so a caller that knows the answer
114
+ was never delivered can discard it without a newer input arriving.
115
+
116
+ ### An input that joins a run in flight
117
+
118
+ `inject` is right when the new input **refines** rather than redirects, and the
119
+ steps already taken are still worth something: *"summarise this thread… actually,
120
+ in bullet points"* should not throw away the reading the first message paid for.
121
+
122
+ What happens:
123
+
124
+ 1. The input is admitted exactly like any other — a committed user message and a
125
+ **queued run**, durable before anything else happens.
126
+ 2. At the running loop's next step boundary, that input joins its prompt. Only
127
+ that input: an unrelated queued submission is never carried in.
128
+ 3. When the run finishes, its terminal commit — **one transaction** — records
129
+ the input as one the run answered and settles the queued successor with
130
+ `terminalReason: 'absorbed'` and `absorbedIntoRunId` naming the run that
131
+ answered it. Every ticket for that successor resolves to the same answer.
132
+
133
+ Nothing durable happens between 1 and 3, and that is the design (→ ADR 0113).
134
+ A run that crashes, is closed, or is interrupted after taking an input on
135
+ commits no absorption at all, so what is left behind is an ordinary queued
136
+ successor — the state every other policy already produces and recovery already
137
+ handles. **There is no ordering in which an accepted input becomes
138
+ unanswerable.** Only a run that *completes* may absorb; an interrupted one took
139
+ the input into its prompt and then stopped, and does not get to say it answered
140
+ it.
141
+
142
+ An absorbed run has **no assistant message of its own** — it produced none, and
143
+ writing an empty one would be a record claiming otherwise. Its answer is
144
+ reachable through `absorbedIntoRunId`, and the store follows that pointer itself
145
+ when a submission arrives on the absorbed run's idempotency key, so a retry
146
+ after a restart returns the answer rather than an empty terminal record.
147
+
148
+ It does publish one more `run-state` event, carrying `'superseded'`. It never
149
+ enters the run executor, so that event is the only thing that tells a delivery
150
+ surface following its `runId` that it is no longer queued.
151
+
152
+ With `coalescePending`, one successor can carry several inputs, and an
153
+ absorption covers a successor **whole or not at all** — a partial one would
154
+ leave a terminal run with inputs nobody answered. Inputs that coalesce before
155
+ the absorbing run's last step boundary join the same absorption. One that
156
+ arrives after it cancels the absorption: the successor then answers all of its
157
+ inputs itself, and the absorbing run has answered one of them too. A duplicate
158
+ answer, never a missing one.
159
+
160
+ (0.63.0 shipped a version of this that committed the absorption at the step
161
+ boundary, before the answer existed, and it was withdrawn in 0.65.0. ADR 0113
162
+ records what that ordering broke.)
163
+
164
+ With `runs.coalescePending: true`, an active lane has at most one queued
165
+ successor. Every later accepted input is atomically appended to that successor;
166
+ its `AgentRun.inputMessageIds` records the whole batch and every input ticket
167
+ resolves to the same terminal run. Coalescing never mutates the active run.
168
+
169
+ `AbortSignal.aborted` is only a cooperation request. A successor does not begin
170
+ while the predecessor still owns managed callbacks. A hung predecessor blocks
171
+ the lane in the first version.
172
+
173
+ Shutdown is two-phase, and both budgets carry the names they carry everywhere
174
+ else in Stitchkit:
175
+
176
+ ```ts
177
+ const closed = await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
178
+ if (!closed.settled) {
179
+ console.warn(`exiting with ${closed.remaining} run(s) still in flight`)
180
+ }
181
+ ```
182
+
183
+ `close` first rejects new process-local admissions and gives active runs
184
+ `gracePeriodMs` to finish on their own. Only after that budget expires does it
185
+ abort them with reason `shutdown`; `forceTimeoutMs` then bounds the settlement
186
+ wait for a non-cooperative model or tool, measured from the abort — the two
187
+ budgets add up rather than overlapping.
188
+
189
+ **`close()` reports what it achieved rather than promising an outcome it cannot
190
+ reach.** The result is `{ settled, timedOut, remaining }`: `settled` when every
191
+ in-flight run finished, `timedOut` with a `remaining` count when the force
192
+ budget expired first. There is no combination of budgets that is both bounded
193
+ and guaranteed to leave nothing in flight — that is the trade the budgets exist
194
+ to make, and the result is where you read which side of it you got:
195
+
196
+ | budgets | behaviour | can return with a run in flight |
197
+ |---------|-----------|---------------------------------|
198
+ | neither | aborts immediately, waits for settlement | no — unbounded wait |
199
+ | `gracePeriodMs` only | waits, aborts, then waits for settlement | no — unbounded wait |
200
+ | `forceTimeoutMs` only | aborts immediately, waits at most that long | **yes** — `timedOut` |
201
+ | both | waits, aborts, waits at most that long | **yes** — `timedOut` |
202
+
203
+ A caller that wants the old shape still writes `await runtime.close(…)` and
204
+ ignores the result. A caller deciding whether to exit the process reads it.
205
+ Durably queued records rejected from the local queue remain recoverable through
206
+ `scanRecoverable`; close never marks them terminal on its own.
207
+
208
+ Use `await runtime.interrupt({ conversationId, runId })` when the interruption
209
+ must be durable: it first commits `interrupt_requested`, then aborts the local
210
+ coordinator signal. If provider completion races that revision change, the terminal path reloads
211
+ the canonical snapshot. An already-terminal winner settles the ticket directly; a still-owned
212
+ `interrupt_requested` run is committed as `interrupted`, and unrelated aggregate-head conflicts
213
+ remain retriable while the run is active with the same owner and fencing token. A stale owner or
214
+ fencing token remains a conflict. Only the execution that applies the terminal mutation publishes the
215
+ **delivery** `terminal` event — a loser settles from canonical state without republishing the turn,
216
+ and its `AgentRuntimeResult.metrics` is `undefined`. The **operator** `run-terminal` event is not
217
+ gated that way: a losing execution still ran, and still spent whatever it spent, so it reports its
218
+ own usage. The two channels answer to different readers — delivering a turn twice is a user's
219
+ problem, and omitting a run's cost is an operator's.
220
+ `runtime.stop(key)` is the process-local signal-only escape hatch.
221
+
222
+ When an operator has positive evidence that another process owning one known
223
+ run is gone, close that run through the runtime boundary rather than updating
224
+ an adapter table or JSON payload directly:
225
+
226
+ ```ts
227
+ await runtime.abandon({
228
+ conversationId,
229
+ runId,
230
+ expectedRevision: run.revision,
231
+ staleOwner: true,
232
+ })
233
+ ```
234
+
235
+ The revision protects against stale operator evidence. The mutation updates the
236
+ canonical run and failed assistant atomically, removes the run from recovery
237
+ indexes, and publishes the resulting `run-state`. A run that moved since it was
238
+ read returns a conflict; `staleOwner: true` is mandatory evidence, not a default.
239
+
16
240
  ## Store operations
17
241
 
18
242
  `AgentRuntimeStore` remains the runtime-facing aggregate. Application adapters
@@ -111,7 +111,7 @@ Server-only optional application runtime. See the
111
111
  | `AgentSandboxProcess` / `AgentSandboxOutputStream` | _types_ | structural child/output interface for optional lifecycle-owned `AgentProcessSandbox.spawn`, without Node ambient type dependencies |
112
112
  | `AgentEventSearchResultSchema` / `AgentEventSearchResult` / `createSqliteAgentEventSearch` / `createAgentEventSearchTools` | schema / _type_ / functions | authorized FTS5 search with exact event addresses and `session_*` tools |
113
113
  | `createSqliteAgentSpillStore` | function | durable content-address-checked artifact storage, bounded read/search, retention facts and archive participation |
114
- | `AgentScheduleSchema` / `AgentSchedule` / `AgentScheduleService` / `createAgentScheduleService` / `createAgentScheduleTools` | schema / _types_ / functions | durable `at`/`after`/timezone-explicit `every`, stable dispatch identity and Agent tools |
114
+ | `AgentScheduleSchema` / `AgentSchedule` / `AgentScheduleService` / `AgentScheduleDeliveryOutcomeSchema` / `AgentScheduleDeliveryOutcome` / `createAgentScheduleService` / `createAgentScheduleTools` | schemas / _types_ / functions | durable `at`/`after`/timezone-explicit `every`, indexed retry eligibility, typed retry/terminal failure, bounded dispatch and fenced stable occurrence identity (ADR 0202) |
115
115
  | `AgentChildBudgetSchema` / `AgentChildBudget` / `AgentChildStateSchema` / `AgentChildState` / `AgentChildRecordSchema` / `AgentChildRecord` | schema / _type_ | durable child graph, bounded seed and measured budget state |
116
116
  | `AgentChildBlockingKindSchema` / `AgentChildBlockingKind` / `AgentChildBlockingSourceSchema` / `AgentChildBlockingSource` / `AgentChildBlockingEventSchema` / `AgentChildBlockingEvent` / `AgentChildBlockingDecision` | schemas / _types_ | parent-owned request identities; responses discriminate approval decisions from JSON input values |
117
117
  | `AgentChildHandle` / `AgentChildManager` / `SqliteAgentChildManagerConfig` / `createSqliteAgentChildManager` / `createAgentChildTools` / `agentChildBudgetStopPolicy` | _type_ / functions | host execution port, child lifecycle, cascade (given to `createAgentRuntime` as `children`), messaging, Agent tools, and the child runtime's own budget stop policy — `recordStepUsage` at every step boundary, `policy_stop` as `child-budget` when spent |
@@ -19,7 +19,7 @@ description: Configure Stitchkit's optional durable history, stream loop, run co
19
19
  type: architecture
20
20
  status: active
21
21
  created: 2026-08-22
22
- updated: 2026-08-29
22
+ updated: 2026-09-26 16:00 +07:00
23
23
  ---
24
24
 
25
25
  # Agent application runtime
@@ -636,6 +636,23 @@ has invoked a tool is never automatically replayed.
636
636
  `createAgentScheduleService` persists `at`, `after` and `every` input. Repeating
637
637
  schedules require an explicit IANA time zone. Dispatch uses a stable
638
638
  `schedule:<id>:<occurrence>` idempotency key, and restart lateness is recorded.
639
+ Resolve `void` after durable admission; return `{ status: 'retry', reason }` for a
640
+ transient refusal or `{ status: 'terminal', reason }` to stop the entire schedule in
641
+ `failed` state. Throws retry too. Backoff is 1, 2, 4, 8, 16, 32, then 60 seconds,
642
+ capped at 60 seconds and persisted across restart. `retryAt`, `attempts` and `lastError`
643
+ show retry diagnostics; success resets them. `nextAt` and `lateByMs` retain the original
644
+ occurrence time. Recurring schedules fire once after an idle stretch and advance to the
645
+ next future slot on their original cadence.
646
+
647
+ Dispatch receives an `AbortSignal`, has a 30-second deadline and holds a unique
648
+ 60-second lease. A late or cancelled attempt cannot settle or emit firing events.
649
+ Honor the signal and durably deduplicate the key: a crash after admission but before
650
+ settlement can replay delivery. This is at-least-once, not exactly-once.
651
+ Due selection is indexed, limited to 32 parallel deliveries per tick; one hung batch
652
+ can delay newly due work by at most its deadline. The service keeps one nearest-due
653
+ timer plus per-attempt deadline timers. Storage failures use a bounded recovery timer;
654
+ idle ticks do not write. SQLite v4 migration preserves due times and claims; stop older
655
+ scheduler processes before migration. See [ADR 0202](../decisions/0202-schedule-retries-preserve-occurrence-identity.md).
639
656
  `createAgentScheduleTools` exposes the exact `schedule_after`, `schedule_at`,
640
657
  `schedule_every`, `schedule_list` and `schedule_cancel` surface.
641
658
 
@@ -667,227 +684,3 @@ a successor input into the run already in flight.
667
684
  ```text
668
685
  input + queued run → running → execution settled → terminal CAS → successor
669
686
  ```
670
-
671
- ## What happens to a run when new input arrives
672
-
673
- `runs.inputPolicy` decides. It takes five values, or a function returning one:
674
-
675
- | policy | the run in flight | what it already produced |
676
- |--------|-------------------|--------------------------|
677
- | `queue` (default) | finishes first | kept |
678
- | `inject` | continues, and answers the new input too | kept, and built on |
679
- | `interrupt` | ends | kept, and marked as cut off |
680
- | `interrupt-next` | ends; this input runs before ordinary queued work | kept, and marked as cut off |
681
- | `supersede` | ends | discarded from the prompt, kept in the record |
682
-
683
- `interrupt-next` is the explicit priority path for an urgent input. If A is
684
- running, ordinary B is queued, and urgent C arrives, the coordinator aborts A,
685
- waits for A to settle and then runs C before B. B keeps its durable identity and
686
- eventually runs. Urgent inputs remain FIFO among themselves. They do not
687
- coalesce into an ordinary pending run: that would erase the priority boundary.
688
-
689
- `interrupt` and `supersede` differ in exactly one thing, and the question that
690
- picks between them is **not** "was the run interrupted" but **"did anyone see
691
- what it produced"**:
692
-
693
- - The user pressed **stop**. The partial answer was streamed to their screen and
694
- they read it. It belongs in the conversation — dropping it makes the history
695
- lie to the model about what the human has seen. That is `interrupt`.
696
- - A newer message **superseded** the run. Whether the partial reached anyone
697
- depends on the delivery surface: a token stream shows it as it is produced, a
698
- surface that sends nothing until the run is done never showed it at all. When
699
- it reached nobody, it is not part of the conversation. That is `supersede`.
700
-
701
- **Stitchkit cannot answer that question for you** — delivery belongs to the
702
- transport, and the runtime sees an abort, not a screen. Hence a declared policy
703
- (→ ADR 0108), and hence `inputPolicy` accepting a function, so one application
704
- can hold two surfaces with different rules without the core learning which is
705
- which:
706
-
707
- ```ts
708
- runs: {
709
- inputPolicy: (input) =>
710
- protocol.parseContext(input.context).surface === 'operator' ? 'queue' : 'supersede',
711
- }
712
- ```
713
-
714
- `input.context` is the **raw** context here — admission runs before the runtime
715
- parses it, so the callback narrows it itself with the protocol it already has.
716
-
717
- A superseded run ends with `terminalReason: 'superseded'`, run state
718
- `'superseded'` and an assistant message of status `'superseded'`. **The record
719
- is kept** — excluded from the projection, not deleted — so an operator can see
720
- what was thrown away, and run identity, admission receipts and the terminal CAS
721
- keep the row they depend on. Compaction leaves it alone for the same reason: a
722
- turn whose answer is never spoken is not a turn that may be summarised into one,
723
- because that would both feed the discarded text to the summariser and drop the
724
- record in `replacedMessageIds`.
725
-
726
- It is also outside the token budget. `selectAgentHistory` removes it with reason
727
- `'superseded'` and does not count it, so an abandoned fragment cannot push a
728
- real turn out of a context it never occupies.
729
-
730
- ### How an interrupted turn reaches the model
731
-
732
- An interrupted turn is projected, and says so:
733
-
734
- ```text
735
- { role: 'assistant', content: [
736
- { type: 'text', text: 'We are the team, where would you like' },
737
- { type: 'text', text: '[interrupted: this turn was cut off before it finished]' },
738
- ]}
739
- ```
740
-
741
- `history.interruptedAssistant` chooses the form, and the difference between the
742
- first two is structural rather than cosmetic. **An assistant turn in provider
743
- history is a commitment**: the model reads its own previous turn as something it
744
- said and stays consistent with it. A system line is context.
745
-
746
- | value | form | right when |
747
- |-------|------|-----------|
748
- | `assistant-marked` (default) | assistant turn plus a marker | the human read the text |
749
- | `system-note` | `[interrupted] partial response: …` as a system line | the fragment reached nobody |
750
- | `omit` | not projected at all | you want it gone from the request |
751
-
752
- There is deliberately no value that reproduces what the projection used to do,
753
- which was to send the partial as an ordinary assistant turn and drop its
754
- `control` marker on the way. That was the defect, not a behaviour to stay
755
- compatible with.
756
-
757
- `projectAgentHistoryDetailed` reports what reached the provider, including part
758
- types that no projected content stands for:
759
-
760
- ```ts
761
- const { decisions } = await projectAgentHistoryDetailed(snapshot.messages)
762
- // → { messageId: 'assistant-2', action: 'projected', reason: 'projected',
763
- // omittedParts: ['source', 'provider'] }
764
- // → { messageId: 'assistant-1', action: 'omitted', reason: 'superseded' }
765
- ```
766
-
767
- `runtime.stop(key, 'supersede')` is the same decision taken by hand: the
768
- process-local escape hatch chooses the reason, so a caller that knows the answer
769
- was never delivered can discard it without a newer input arriving.
770
-
771
- ### An input that joins a run in flight
772
-
773
- `inject` is right when the new input **refines** rather than redirects, and the
774
- steps already taken are still worth something: *"summarise this thread… actually,
775
- in bullet points"* should not throw away the reading the first message paid for.
776
-
777
- What happens:
778
-
779
- 1. The input is admitted exactly like any other — a committed user message and a
780
- **queued run**, durable before anything else happens.
781
- 2. At the running loop's next step boundary, that input joins its prompt. Only
782
- that input: an unrelated queued submission is never carried in.
783
- 3. When the run finishes, its terminal commit — **one transaction** — records
784
- the input as one the run answered and settles the queued successor with
785
- `terminalReason: 'absorbed'` and `absorbedIntoRunId` naming the run that
786
- answered it. Every ticket for that successor resolves to the same answer.
787
-
788
- Nothing durable happens between 1 and 3, and that is the design (→ ADR 0113).
789
- A run that crashes, is closed, or is interrupted after taking an input on
790
- commits no absorption at all, so what is left behind is an ordinary queued
791
- successor — the state every other policy already produces and recovery already
792
- handles. **There is no ordering in which an accepted input becomes
793
- unanswerable.** Only a run that *completes* may absorb; an interrupted one took
794
- the input into its prompt and then stopped, and does not get to say it answered
795
- it.
796
-
797
- An absorbed run has **no assistant message of its own** — it produced none, and
798
- writing an empty one would be a record claiming otherwise. Its answer is
799
- reachable through `absorbedIntoRunId`, and the store follows that pointer itself
800
- when a submission arrives on the absorbed run's idempotency key, so a retry
801
- after a restart returns the answer rather than an empty terminal record.
802
-
803
- It does publish one more `run-state` event, carrying `'superseded'`. It never
804
- enters the run executor, so that event is the only thing that tells a delivery
805
- surface following its `runId` that it is no longer queued.
806
-
807
- With `coalescePending`, one successor can carry several inputs, and an
808
- absorption covers a successor **whole or not at all** — a partial one would
809
- leave a terminal run with inputs nobody answered. Inputs that coalesce before
810
- the absorbing run's last step boundary join the same absorption. One that
811
- arrives after it cancels the absorption: the successor then answers all of its
812
- inputs itself, and the absorbing run has answered one of them too. A duplicate
813
- answer, never a missing one.
814
-
815
- (0.63.0 shipped a version of this that committed the absorption at the step
816
- boundary, before the answer existed, and it was withdrawn in 0.65.0. ADR 0113
817
- records what that ordering broke.)
818
-
819
- With `runs.coalescePending: true`, an active lane has at most one queued
820
- successor. Every later accepted input is atomically appended to that successor;
821
- its `AgentRun.inputMessageIds` records the whole batch and every input ticket
822
- resolves to the same terminal run. Coalescing never mutates the active run.
823
-
824
- `AbortSignal.aborted` is only a cooperation request. A successor does not begin
825
- while the predecessor still owns managed callbacks. A hung predecessor blocks
826
- the lane in the first version.
827
-
828
- Shutdown is two-phase, and both budgets carry the names they carry everywhere
829
- else in Stitchkit:
830
-
831
- ```ts
832
- const closed = await runtime.close({ gracePeriodMs: 30_000, forceTimeoutMs: 5_000 })
833
- if (!closed.settled) {
834
- console.warn(`exiting with ${closed.remaining} run(s) still in flight`)
835
- }
836
- ```
837
-
838
- `close` first rejects new process-local admissions and gives active runs
839
- `gracePeriodMs` to finish on their own. Only after that budget expires does it
840
- abort them with reason `shutdown`; `forceTimeoutMs` then bounds the settlement
841
- wait for a non-cooperative model or tool, measured from the abort — the two
842
- budgets add up rather than overlapping.
843
-
844
- **`close()` reports what it achieved rather than promising an outcome it cannot
845
- reach.** The result is `{ settled, timedOut, remaining }`: `settled` when every
846
- in-flight run finished, `timedOut` with a `remaining` count when the force
847
- budget expired first. There is no combination of budgets that is both bounded
848
- and guaranteed to leave nothing in flight — that is the trade the budgets exist
849
- to make, and the result is where you read which side of it you got:
850
-
851
- | budgets | behaviour | can return with a run in flight |
852
- |---------|-----------|---------------------------------|
853
- | neither | aborts immediately, waits for settlement | no — unbounded wait |
854
- | `gracePeriodMs` only | waits, aborts, then waits for settlement | no — unbounded wait |
855
- | `forceTimeoutMs` only | aborts immediately, waits at most that long | **yes** — `timedOut` |
856
- | both | waits, aborts, waits at most that long | **yes** — `timedOut` |
857
-
858
- A caller that wants the old shape still writes `await runtime.close(…)` and
859
- ignores the result. A caller deciding whether to exit the process reads it.
860
- Durably queued records rejected from the local queue remain recoverable through
861
- `scanRecoverable`; close never marks them terminal on its own.
862
-
863
- Use `await runtime.interrupt({ conversationId, runId })` when the interruption
864
- must be durable: it first commits `interrupt_requested`, then aborts the local
865
- coordinator signal. If provider completion races that revision change, the terminal path reloads
866
- the canonical snapshot. An already-terminal winner settles the ticket directly; a still-owned
867
- `interrupt_requested` run is committed as `interrupted`, and unrelated aggregate-head conflicts
868
- remain retriable while the run is active with the same owner and fencing token. A stale owner or
869
- fencing token remains a conflict. Only the execution that applies the terminal mutation publishes the
870
- **delivery** `terminal` event — a loser settles from canonical state without republishing the turn,
871
- and its `AgentRuntimeResult.metrics` is `undefined`. The **operator** `run-terminal` event is not
872
- gated that way: a losing execution still ran, and still spent whatever it spent, so it reports its
873
- own usage. The two channels answer to different readers — delivering a turn twice is a user's
874
- problem, and omitting a run's cost is an operator's.
875
- `runtime.stop(key)` is the process-local signal-only escape hatch.
876
-
877
- When an operator has positive evidence that another process owning one known
878
- run is gone, close that run through the runtime boundary rather than updating
879
- an adapter table or JSON payload directly:
880
-
881
- ```ts
882
- await runtime.abandon({
883
- conversationId,
884
- runId,
885
- expectedRevision: run.revision,
886
- staleOwner: true,
887
- })
888
- ```
889
-
890
- The revision protects against stale operator evidence. The mutation updates the
891
- canonical run and failed assistant atomically, removes the run from recovery
892
- indexes, and publishes the resulting `run-state`. A run that moved since it was
893
- read returns a conflict; `staleOwner: true` is mandatory evidence, not a default.
@@ -77,11 +77,11 @@ own, recorded as an ADR.
77
77
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
78
78
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
79
79
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
80
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 41 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
80
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 42 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
81
81
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
82
82
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
83
83
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
84
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 21 of the 41 minors since 0.56.2, most recently 0.94.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
84
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 42 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
85
85
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
86
86
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
87
87
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -91,7 +91,7 @@ own, recorded as an ADR.
91
91
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
92
92
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
93
93
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
94
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 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 |
94
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 9 of the 42 minors since 0.56.2, most recently 0.96.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
95
95
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
96
96
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
97
97
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |