stitchkit 0.96.1 → 0.98.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 (40) hide show
  1. package/CHANGELOG.md +50 -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/cli.js +1 -1
  21. package/dist/entrypoints/tools.js +1 -1
  22. package/dist/{index-p9e0w8hs.js → index-1m2thnq0.js} +25 -1
  23. package/dist/{index-t9rhjr9f.js → index-f1nb70ae.js} +45 -9
  24. package/dist/tools/cli/config.d.ts +5 -1
  25. package/dist/tools/cli/config.d.ts.map +1 -1
  26. package/dist/tools/cli/io.d.ts.map +1 -1
  27. package/dist/tools/cli/stdin.d.ts +6 -0
  28. package/dist/tools/cli/stdin.d.ts.map +1 -0
  29. package/llms/agent-runtime.2.txt +224 -0
  30. package/llms/agent-runtime.3.txt +1 -1
  31. package/llms/agent-runtime.txt +18 -225
  32. package/llms/cli.txt +15 -0
  33. package/llms/stitchkit.txt +3 -3
  34. package/llms/{upgrading-0.37.0-0.56.0.txt → upgrading-0.37.0-0.58.0.txt} +82 -3
  35. package/llms/{upgrading-0.57.0-0.69.0.txt → upgrading-0.59.0-0.71.0.txt} +108 -82
  36. package/llms/{upgrading-0.70.0-0.96.0.txt → upgrading-0.72.0-0.98.0.txt} +64 -108
  37. package/llms/upgrading.txt +3 -3
  38. package/llms-full.txt +98 -5
  39. package/llms.txt +5 -5
  40. package/package.json +1 -1
@@ -18,7 +18,7 @@ import {
18
18
  readCapped,
19
19
  renderCliView,
20
20
  routeCliArgv
21
- } from "../index-t9rhjr9f.js";
21
+ } from "../index-f1nb70ae.js";
22
22
  import"../index-sbdmyz75.js";
23
23
  import"../index-hmp4800j.js";
24
24
  import {
@@ -60,7 +60,7 @@ import {
60
60
  fetchPinnedDocument,
61
61
  readCapped,
62
62
  runWaitOperation
63
- } from "../index-t9rhjr9f.js";
63
+ } from "../index-f1nb70ae.js";
64
64
  import"../index-sbdmyz75.js";
65
65
  import {
66
66
  collectToolSurface
@@ -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) {
@@ -1336,16 +1336,52 @@ async function pollUntilDone(params) {
1336
1336
 
1337
1337
  // src/tools/cli/io.ts
1338
1338
  import { writeSync } from "node:fs";
1339
- async function readPipedStdin() {
1340
- if (process.stdin.isTTY)
1341
- return null;
1342
- const chunks = [];
1343
- for await (const chunk of process.stdin) {
1344
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1345
- }
1346
- const text = Buffer.concat(chunks).toString("utf8").trim();
1347
- return text.length > 0 ? text : null;
1339
+
1340
+ // src/tools/cli/stdin.ts
1341
+ function readPipedStdin(input = process.stdin) {
1342
+ if (input.isTTY || input.readableEnded)
1343
+ return Promise.resolve(null);
1344
+ if (input.destroyed)
1345
+ return Promise.reject(new Error("stdin closed before EOF"));
1346
+ return new Promise((resolve, reject) => {
1347
+ const chunks = [];
1348
+ const cleanup = () => {
1349
+ clearTimeout(timer);
1350
+ input.pause();
1351
+ input.off("data", onData);
1352
+ input.off("end", onEnd);
1353
+ input.off("error", onError);
1354
+ input.off("close", onClose);
1355
+ };
1356
+ const onData = (chunk) => {
1357
+ if (chunk.length === 0)
1358
+ return;
1359
+ clearTimeout(timer);
1360
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1361
+ };
1362
+ const onEnd = () => {
1363
+ cleanup();
1364
+ const text = Buffer.concat(chunks).toString("utf8").trim();
1365
+ resolve(text.length > 0 ? text : null);
1366
+ };
1367
+ const onError = (error) => {
1368
+ cleanup();
1369
+ reject(error);
1370
+ };
1371
+ const onClose = () => onError(new Error("stdin closed before EOF"));
1372
+ const timer = setTimeout(() => {
1373
+ cleanup();
1374
+ resolve(null);
1375
+ }, 250);
1376
+ input.on("end", onEnd);
1377
+ input.on("error", onError);
1378
+ input.on("close", onClose);
1379
+ input.on("data", onData);
1380
+ input.resume();
1381
+ });
1348
1382
  }
1383
+
1384
+ // src/tools/cli/io.ts
1349
1385
  function createCliIo(config) {
1350
1386
  let pendingWrite;
1351
1387
  let writeFailed = false;
@@ -84,7 +84,11 @@ export interface CliConfig<TAuth = unknown, TContext extends Record<string, unkn
84
84
  stderr?: (text: string) => void;
85
85
  /** Exit hook — default `process.exit`; injectable for tests. */
86
86
  exit?: (code: number) => void;
87
- /** Read piped stdin — default reads when not a TTY; injectable for tests. */
87
+ /**
88
+ * Read stdin for the first required unset field. The default skips TTYs,
89
+ * waits up to 250 ms for the first byte, then reads to EOF without a deadline.
90
+ * Override to explicitly wait for a delayed producer; no probe wraps this hook.
91
+ */
88
92
  stdin?: () => Promise<string | null>;
89
93
  }
90
94
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,WAAW,SAAS,CACxB,KAAK,GAAG,OAAO,EACf,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClE,QAAQ,SAAS,SAAS,GAAG,SAAS,CACtC,SAAQ,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACnD,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACtE;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC;IACtF,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;+DAC2D;IAC3D,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kEAAkE;IAClE,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACrC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE;;;OAGG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,oEAAoE;IACpE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,oEAAoE;IACpE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,gEAAgE;IAChE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,WAAW,SAAS,CACxB,KAAK,GAAG,OAAO,EACf,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClE,QAAQ,SAAS,SAAS,GAAG,SAAS,CACtC,SAAQ,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACnD,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IACtE;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC;IACtF,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;+DAC2D;IAC3D,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kEAAkE;IAClE,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACrC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrE;;;OAGG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,oEAAoE;IACpE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,oEAAoE;IACpE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,gEAAgE;IAChE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACtC"}
@@ -1 +1 @@
1
- {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/io.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAa1C,yFAAyF;AACzF,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,GAC9D,KAAK,CAqEP"}
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/io.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C,yFAAyF;AACzF,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,GAC9D,KAAK,CAqEP"}
@@ -0,0 +1,6 @@
1
+ import type { Readable } from 'node:stream';
2
+ /** Probe automatic stdin routing briefly; once data starts, only EOF completes it. */
3
+ export declare function readPipedStdin(input?: Readable & {
4
+ isTTY?: boolean;
5
+ }): Promise<string | null>;
6
+ //# sourceMappingURL=stdin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdin.d.ts","sourceRoot":"","sources":["../../../src/tools/cli/stdin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,sFAAsF;AACtF,wBAAgB,cAAc,CAC5B,KAAK,GAAE,QAAQ,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAkB,GACpD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwCxB"}
@@ -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 |