@fungi.computer/shiitake 0.5.0 → 0.7.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 (88) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +94 -2
  3. package/dist/agent-control.js +147 -26
  4. package/dist/client-entry.d.ts +20 -0
  5. package/dist/client-entry.js +5 -0
  6. package/dist/client-visitor.d.ts +33 -0
  7. package/dist/client-visitor.js +231 -0
  8. package/dist/client-wire.d.ts +9 -0
  9. package/dist/client-wire.js +24 -3
  10. package/dist/client.d.ts +27 -6
  11. package/dist/client.js +23 -0
  12. package/dist/cloudflare.d.ts +20 -0
  13. package/dist/cloudflare.js +4 -1
  14. package/dist/delegation.d.ts +79 -53
  15. package/dist/delegation.js +26 -21
  16. package/dist/effect.d.ts +26 -1
  17. package/dist/effect.js +14 -1
  18. package/dist/errors.d.ts +2 -2
  19. package/dist/errors.js +2 -0
  20. package/dist/extensions.d.ts +22 -3
  21. package/dist/extensions.js +1 -1
  22. package/dist/fleet.d.ts +25 -2
  23. package/dist/fleet.js +108 -24
  24. package/dist/hosted.js +3 -0
  25. package/dist/http-routes.d.ts +23 -9
  26. package/dist/http.d.ts +1 -0
  27. package/dist/http.js +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/internal/accepted-run-executor.js +2 -1
  30. package/dist/internal/admission-door.d.ts +6 -1
  31. package/dist/internal/admission-door.js +33 -3
  32. package/dist/internal/contracts.d.ts +19 -1
  33. package/dist/internal/coordinator.js +115 -52
  34. package/dist/internal/effect-client.js +1 -1
  35. package/dist/internal/effect-contracts.d.ts +9 -2
  36. package/dist/internal/extension-command-registry.d.ts +2 -1
  37. package/dist/internal/extension-command-registry.js +3 -1
  38. package/dist/internal/extension-runtime.d.ts +3 -1
  39. package/dist/internal/extension-runtime.js +18 -8
  40. package/dist/internal/model-acquisition.js +2 -0
  41. package/dist/internal/observation-kernel.d.ts +2 -0
  42. package/dist/internal/observation-kernel.js +17 -6
  43. package/dist/internal/promise-facade.js +2 -0
  44. package/dist/internal/run-scope.d.ts +14 -0
  45. package/dist/internal/run-scope.js +58 -15
  46. package/dist/internal/runtime-coordinator.d.ts +14 -0
  47. package/dist/internal/runtime-coordinator.js +60 -11
  48. package/dist/internal/runtime-core.d.ts +3 -0
  49. package/dist/internal/runtime-core.js +1 -0
  50. package/dist/internal/runtime-reconciliation.js +123 -25
  51. package/dist/internal/session-context.d.ts +5 -5
  52. package/dist/internal/session-context.js +80 -83
  53. package/dist/internal/session-status.d.ts +2 -2
  54. package/dist/internal/session-status.js +5 -3
  55. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  56. package/dist/internal/woodstock/agent-reply.js +3 -0
  57. package/dist/internal/woodstock/client-projection.js +13 -11
  58. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  59. package/dist/internal/woodstock/fleet-store.js +24 -4
  60. package/dist/internal/woodstock/index.d.ts +7 -3
  61. package/dist/internal/woodstock/index.js +23 -6
  62. package/dist/internal/woodstock/progress.d.ts +11 -1
  63. package/dist/internal/woodstock/progress.js +126 -25
  64. package/dist/internal/woodstock/session-delegator.d.ts +13 -0
  65. package/dist/internal/woodstock/session-delegator.js +44 -0
  66. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  67. package/dist/internal/woodstock/session-store.d.ts +16 -2
  68. package/dist/internal/woodstock/session-store.js +349 -66
  69. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  70. package/dist/internal/woodstock/work-senders.js +70 -0
  71. package/dist/node.js +11 -2
  72. package/dist/remote-client.js +7 -2
  73. package/dist/server-visitor-stream.d.ts +13 -0
  74. package/dist/server-visitor-stream.js +124 -0
  75. package/dist/session-attention.d.ts +30 -0
  76. package/dist/session-attention.js +14 -0
  77. package/docs/api/agent-control.md +212 -15
  78. package/docs/api/client-entry.md +3 -0
  79. package/docs/api/client.md +209 -5
  80. package/docs/api/cloudflare.md +23 -1
  81. package/docs/api/delegation.md +284 -77
  82. package/docs/api/effect.md +100 -12
  83. package/docs/api/extensions.md +73 -12
  84. package/docs/api/fleet.md +138 -25
  85. package/docs/api/http.md +28 -0
  86. package/docs/api/index.md +43 -17
  87. package/docs/api/testing.md +9 -7
  88. package/package.json +10 -6
@@ -7,6 +7,8 @@ import { compactionSummaryMessage } from "../compaction/utils.js";
7
7
  import { SessionTitleSchema, sessionTitleLimit } from "../client-boundary.js";
8
8
  import { commandTimestamp, decodeStoredSessionEntry, encodeStoredSessionEntry, isCompactCommand, commandContextMessage, operatorContextMessage, promptCommand, } from "./session-entry.js";
9
9
  import { prepareSessionAttention } from "./session-attention.js";
10
+ import { prepareWorkSenders } from "./work-senders.js";
11
+ import { prepareSessionDelegators } from "./session-delegator.js";
10
12
  import { copySessionGrants, prepareSessionGrants } from "./session-grants.js";
11
13
  import { prepareSessionInbox, touchSessionInbox } from "./session-inbox.js";
12
14
  import { sessionIds } from "./identities.js";
@@ -268,9 +270,9 @@ function selectedLineageFirstPromptId(owner, sessionId, leafId) {
268
270
  UNION
269
271
  SELECT parent.id, parent.parent_id, parent.sequence,
270
272
  parent.type, parent.role, parent.state, parent.work_id
271
- FROM session_entries AS parent
272
- JOIN branch AS child ON parent.id = child.parent_id
273
- WHERE parent.session_id = ?
273
+ FROM branch AS child
274
+ CROSS JOIN session_entries AS parent
275
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
274
276
  )
275
277
  SELECT
276
278
  (SELECT command.id FROM branch AS command
@@ -534,6 +536,12 @@ const pendingAdmissions = `FROM session_command_admissions AS a
534
536
  WHERE e.session_id = a.session_id AND e.work_id = a.work_id
535
537
  AND e.type IN ('command', 'cancellation', 'run_terminal', 'work_terminal')
536
538
  )`;
539
+ /** A written run terminal closes the window for attaching another steer. */
540
+ export function hasRecordedRunTerminal(owner, sessionId, workId) {
541
+ return (owner.sql
542
+ .exec("SELECT 1 FROM session_entries WHERE session_id = ? AND work_id = ? AND type = 'run_terminal' LIMIT 1", sessionId, workId)
543
+ .toArray().length > 0);
544
+ }
537
545
  const decodeIdentity = Schema.decodeUnknownSync(Schema.Struct({ identity: Schema.NullOr(Schema.String) }));
538
546
  /** Extract the host-selected principal without giving the engine domain grammar. */
539
547
  export function readAdmissionIdentity(owner, path, meta) {
@@ -576,7 +584,8 @@ export function readPendingSessionCommands(owner, sessionId, limit, only = "all"
576
584
  .one()).total;
577
585
  const commands = owner.sql
578
586
  .exec(`SELECT ${admissionColumns("a.")} ${pendingAdmissions} ${filter}
579
- ORDER BY a.sequence ASC LIMIT ?`, sessionId, limit)
587
+ ORDER BY (json_extract(a.data_json, '$.delivery') = 'steer') DESC,
588
+ a.sequence ASC LIMIT ?`, sessionId, limit)
580
589
  .toArray()
581
590
  .map(parseCommandAdmissionRow);
582
591
  return { commands, total };
@@ -645,6 +654,7 @@ export function settleSessionControls(owner, input) {
645
654
  jobId: row.entry.workId,
646
655
  sessionId: input.sessionId,
647
656
  commandFactId: row.id,
657
+ delivery: "steer",
648
658
  });
649
659
  continue;
650
660
  }
@@ -680,6 +690,18 @@ function readSessionCommandAdmissionByWorkId(owner, sessionId, workId) {
680
690
  export function readSessionCommandAdmissionByRequest(owner, sessionId, requestId) {
681
691
  return readCommandAdmission(owner, sessionId, { requestId });
682
692
  }
693
+ /** Probe the indexed terminal facts without materializing a live work's body. */
694
+ export function hasSessionWorkTerminal(owner, sessionId, workId) {
695
+ // Keep the readability check even when the work has no terminal rows.
696
+ readSessionRow(owner, sessionId);
697
+ return (owner.sql
698
+ .exec(`SELECT 1 AS present FROM session_entries
699
+ INDEXED BY idx_session_entries_work_terminal
700
+ WHERE session_id = ? AND work_id = ?
701
+ AND type IN ('run_terminal', 'work_terminal')
702
+ LIMIT 1`, sessionId, workId)
703
+ .toArray().length > 0);
704
+ }
683
705
  /** Read entries correlated to one accepted work identity in Session order. */
684
706
  export function readSessionEntriesForWork(owner, sessionId, workId) {
685
707
  // Refuse the Session before selecting entries: a damaged work may have no
@@ -832,6 +854,7 @@ export function deleteStoredSession(owner, input) {
832
854
  "DELETE FROM session_command_admissions WHERE session_id = ?",
833
855
  // The inbox row references the Session; it goes in the same transaction.
834
856
  "DELETE FROM session_attention WHERE session_id = ?",
857
+ "DELETE FROM session_work_senders WHERE session_id = ?",
835
858
  "DELETE FROM session_tool_grants WHERE session_id = ?",
836
859
  "DELETE FROM session_inbox WHERE session_id = ?",
837
860
  "DELETE FROM session_creation_receipts WHERE session_id = ?",
@@ -1128,6 +1151,8 @@ export function prepareSessionStore(owner) {
1128
1151
  createSessionEntryTable(owner);
1129
1152
  prepareSessionDocuments(owner);
1130
1153
  prepareSessionAttention(owner);
1154
+ prepareWorkSenders(owner);
1155
+ prepareSessionDelegators(owner);
1131
1156
  prepareSessionGrants(owner);
1132
1157
  prepareSessionInbox(owner);
1133
1158
  owner.sql.exec(`
@@ -1136,13 +1161,28 @@ export function prepareSessionStore(owner) {
1136
1161
  session_id TEXT NOT NULL UNIQUE REFERENCES sessions(id)
1137
1162
  )
1138
1163
  `);
1164
+ // Preview admissions could have mistaken a later worker for the creator.
1165
+ owner.sql.exec(`DELETE FROM session_delegators
1166
+ WHERE NOT EXISTS (
1167
+ SELECT 1 FROM session_creation_receipts AS creation
1168
+ WHERE creation.session_id = session_delegators.session_id
1169
+ AND creation.request_id = session_delegators.mail_key
1170
+ )`);
1139
1171
  owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_parent ON session_entries(parent_id)");
1172
+ // Replace the preview index, not its payload-length expression: preparing
1173
+ // identity lookup must never read every stored message's payload.
1174
+ owner.sql.exec("DROP INDEX IF EXISTS idx_session_entries_lineage");
1175
+ owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_ancestry
1176
+ ON session_entries(id, session_id, parent_id, sequence, state, type)`);
1140
1177
  owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_session ON session_entries(session_id, sequence)");
1141
1178
  owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_streaming ON session_entries(session_id, state)");
1142
1179
  owner.sql.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_session_entries_request
1143
1180
  ON session_entries(session_id, request_id)
1144
1181
  WHERE request_id IS NOT NULL`);
1145
1182
  owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_work ON session_entries(work_id, sequence)");
1183
+ owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_work_terminal
1184
+ ON session_entries(session_id, work_id)
1185
+ WHERE type IN ('run_terminal', 'work_terminal')`);
1146
1186
  // Context and preference reads look up these rare older facts directly
1147
1187
  // rather than walking the history behind them.
1148
1188
  owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_model_preference
@@ -1339,6 +1379,8 @@ const lineageByteLimit = 4 * 1_048_576;
1339
1379
  * selected walk steps only to an older parent, so it ends without
1340
1380
  * deduplicating; the fork walk keeps every link for its whole-lineage check.
1341
1381
  */
1382
+ // Keep each recursive step child-first: SQLite must look up that child's
1383
+ // parent, not scan every entry of the Session before joining the walk.
1342
1384
  const lineageWalks = {
1343
1385
  selected: `WITH RECURSIVE branch (id, parent_id, sequence, state) AS (
1344
1386
  SELECT id, parent_id, sequence, state
@@ -1346,9 +1388,10 @@ const lineageWalks = {
1346
1388
  WHERE session_id = ? AND id = ?
1347
1389
  UNION ALL
1348
1390
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state
1349
- FROM session_entries AS parent
1350
- JOIN branch AS child ON parent.id = child.parent_id
1351
- WHERE parent.session_id = ? AND child.sequence > ?
1391
+ FROM branch AS child
1392
+ CROSS JOIN session_entries AS parent
1393
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
1394
+ AND child.sequence > ?
1352
1395
  AND +parent.sequence < child.sequence
1353
1396
  )`,
1354
1397
  fork: `WITH RECURSIVE branch (id, parent_id, sequence, state) AS (
@@ -1357,9 +1400,10 @@ const lineageWalks = {
1357
1400
  WHERE session_id = ? AND id = ? AND state != 'streaming'
1358
1401
  UNION
1359
1402
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state
1360
- FROM session_entries AS parent
1361
- JOIN branch AS child ON parent.id = child.parent_id
1362
- WHERE parent.session_id = ? AND child.sequence > ?
1403
+ FROM branch AS child
1404
+ CROSS JOIN session_entries AS parent
1405
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
1406
+ AND child.sequence > ?
1363
1407
  AND parent.state != 'streaming'
1364
1408
  )`,
1365
1409
  };
@@ -1375,6 +1419,130 @@ const lineageFailures = {
1375
1419
  invalid: "Fork path is invalid",
1376
1420
  },
1377
1421
  };
1422
+ const decodeAncestryRow = Schema.decodeUnknownResult(Schema.Struct({
1423
+ id: boundedId,
1424
+ parent_id: Schema.NullOr(Schema.String),
1425
+ sequence,
1426
+ state: Schema.Literals(["complete", "streaming"]),
1427
+ }), closed);
1428
+ const sessionReads = new WeakMap();
1429
+ /**
1430
+ * One synchronous, transaction-consistent Session baseline. Activity, timeline,
1431
+ * extension projections and context figures share its identity-only ancestry;
1432
+ * asynchronous model description runs after this scope has ended. Nothing is
1433
+ * retained between reads, and payload windows keep their row and byte bounds.
1434
+ */
1435
+ export function withSessionRead(owner, sessionId, read, before = Number.MAX_SAFE_INTEGER) {
1436
+ const previous = sessionReads.get(owner);
1437
+ sessionReads.set(owner, { sessionId, before, encoded: new Map() });
1438
+ try {
1439
+ return owner.transactionSync(read);
1440
+ }
1441
+ finally {
1442
+ if (previous === undefined)
1443
+ sessionReads.delete(owner);
1444
+ else
1445
+ sessionReads.set(owner, previous);
1446
+ }
1447
+ }
1448
+ /** Join the shared Session baseline, or keep a direct projection atomic. */
1449
+ export function withSessionReadTransaction(owner, sessionId, read) {
1450
+ return sessionReads.get(owner)?.sessionId === sessionId
1451
+ ? read()
1452
+ : owner.transactionSync(read);
1453
+ }
1454
+ /**
1455
+ * Walk identities once to the kept context boundary and the requested page's
1456
+ * row lookahead, whichever needs the older floor. Only compactions need their
1457
+ * payload to identify that boundary. A historical page's intervening gap is
1458
+ * traversed in SQL, not materialized into this request's identity cache.
1459
+ *
1460
+ * A context's kept entry can reuse the suffix starting at that ancestor. A later
1461
+ * tip may share only if it is the seed or an ancestor retained in that suffix;
1462
+ * another valid leaf uses its own walk instead of becoming an unavailable read.
1463
+ * A query crossing an omitted gap or asking below the cached floor also uses
1464
+ * its existing bounded walk. Nothing is retained between requests.
1465
+ */
1466
+ function readAncestry(owner, sessionId, tipId, range) {
1467
+ const read = sessionReads.get(owner);
1468
+ if (read?.sessionId !== sessionId)
1469
+ return undefined;
1470
+ read.ancestry ??= owner.sql
1471
+ .exec(`WITH RECURSIVE read_window (before_sequence, rows) AS (VALUES (?, ?)),
1472
+ ancestry (id, parent_id, sequence, state, head, context_closed, taken) AS (
1473
+ SELECT id, parent_id, sequence, state,
1474
+ ${compactionHead("session_entries")}, 0,
1475
+ CASE WHEN state != 'streaming'
1476
+ AND sequence < (SELECT before_sequence FROM read_window)
1477
+ THEN 1 ELSE 0 END
1478
+ FROM session_entries INDEXED BY idx_session_entries_ancestry
1479
+ WHERE session_id = ? AND id = ?
1480
+ UNION ALL
1481
+ SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
1482
+ COALESCE(child.head, ${compactionHead("parent")}),
1483
+ child.context_closed OR
1484
+ (child.head IS child.id AND child.state != 'streaming'),
1485
+ child.taken + CASE WHEN parent.state != 'streaming'
1486
+ AND parent.sequence < (SELECT before_sequence FROM read_window)
1487
+ THEN 1 ELSE 0 END
1488
+ FROM ancestry AS child
1489
+ CROSS JOIN session_entries AS parent INDEXED BY idx_session_entries_ancestry
1490
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
1491
+ AND +parent.sequence < child.sequence
1492
+ AND (child.taken < (SELECT rows FROM read_window)
1493
+ OR (child.context_closed = 0
1494
+ AND (child.head IS NOT child.id OR child.state = 'streaming')))
1495
+ )
1496
+ SELECT id, parent_id, sequence, state FROM ancestry
1497
+ WHERE context_closed = 0
1498
+ OR sequence <= (SELECT before_sequence FROM read_window)
1499
+ ORDER BY sequence DESC`, read.before, lineageRowLimit + 1, sessionId, tipId, sessionId)
1500
+ .toArray()
1501
+ .map((raw) => {
1502
+ const decoded = decodeAncestryRow(raw);
1503
+ if (decoded._tag === "Failure")
1504
+ throw unavailable("Stored Session ancestry is invalid");
1505
+ return decoded.success;
1506
+ });
1507
+ const start = read.ancestry.findIndex(({ id }) => id === tipId);
1508
+ if (start === -1)
1509
+ return undefined;
1510
+ const rows = read.ancestry.slice(start);
1511
+ if (range !== undefined) {
1512
+ const deepest = rows.at(-1);
1513
+ if (deepest !== undefined &&
1514
+ deepest.parent_id !== null &&
1515
+ deepest.sequence > range.floor + 1)
1516
+ return undefined;
1517
+ const ceiling = range.ceiling ?? Number.MAX_SAFE_INTEGER;
1518
+ for (let index = 1; index < rows.length; index += 1) {
1519
+ const child = rows[index - 1];
1520
+ const parent = rows[index];
1521
+ if (child !== undefined &&
1522
+ parent !== undefined &&
1523
+ child.parent_id !== parent.id &&
1524
+ range.floor < child.sequence - 1 &&
1525
+ ceiling > parent.sequence)
1526
+ return undefined;
1527
+ }
1528
+ }
1529
+ const encoded = read.encoded.get(tipId);
1530
+ if (encoded !== undefined)
1531
+ return encoded;
1532
+ const value = JSON.stringify(rows);
1533
+ read.encoded.set(tipId, value);
1534
+ return value;
1535
+ }
1536
+ /** A table over already-walked identities, decoded by SQLite's JSON reader. */
1537
+ function ancestryTable(name) {
1538
+ return `${name} AS MATERIALIZED (
1539
+ SELECT json_extract(value, '$.id') AS id,
1540
+ json_extract(value, '$.parent_id') AS parent_id,
1541
+ json_extract(value, '$.sequence') AS sequence,
1542
+ json_extract(value, '$.state') AS state
1543
+ FROM json_each(?)
1544
+ )`;
1545
+ }
1378
1546
  const count = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)));
1379
1547
  const decodeLineageCheck = Schema.decodeUnknownResult(Schema.Struct({
1380
1548
  length: count,
@@ -1392,9 +1560,16 @@ const decodeLineageDiagnostic = Schema.decodeUnknownResult(Schema.Struct({ work_
1392
1560
  * the tip while its rows are above it, so only those rows, and the first at
1393
1561
  * or below it, are visited.
1394
1562
  */
1395
- function lineageQuery(owner, lineage, floor, query, ...bindings) {
1563
+ function lineageQuery(owner, lineage, range, query, ...bindings) {
1564
+ const ancestry = lineage.walk === "selected"
1565
+ ? readAncestry(owner, lineage.sessionId, lineage.tipId, range)
1566
+ : undefined;
1567
+ if (ancestry !== undefined)
1568
+ return owner.sql
1569
+ .exec(`WITH ${ancestryTable("branch")} ${query}`, ancestry, ...bindings)
1570
+ .toArray();
1396
1571
  return owner.sql
1397
- .exec(`${lineageWalks[lineage.walk]} ${query}`, lineage.sessionId, lineage.tipId, lineage.sessionId, floor, ...bindings)
1572
+ .exec(`${lineageWalks[lineage.walk]} ${query}`, lineage.sessionId, lineage.tipId, lineage.sessionId, range.floor, ...bindings)
1398
1573
  .toArray();
1399
1574
  }
1400
1575
  /**
@@ -1493,7 +1668,7 @@ export function readSessionLineage(owner, input) {
1493
1668
  });
1494
1669
  }
1495
1670
  function readLineageRange(owner, lineage, oldest, newest) {
1496
- const rows = Object.freeze(lineageQuery(owner, lineage, oldest - 1, `SELECT ${entryColumns}
1671
+ const rows = Object.freeze(lineageQuery(owner, lineage, { floor: oldest - 1, ceiling: newest }, `SELECT ${entryColumns}
1497
1672
  FROM session_entries
1498
1673
  WHERE session_id = ? AND state != 'streaming'
1499
1674
  AND sequence BETWEEN ? AND ?
@@ -1526,18 +1701,38 @@ const decodeWindowSizes = Schema.decodeUnknownResult(Schema.Struct({
1526
1701
  * walk visits the rows from the tip down to the window, never the history
1527
1702
  * below it. The deepest walked row comes back with the sizes, so a walk that
1528
1703
  * ended at a missing parent above where it meant to stop is refused, never
1529
- * read as the end of history.
1704
+ * read as the end of history. When a read already owns the ancestry, choose
1705
+ * the same bounded candidate identities before consulting their record sizes.
1706
+ * SQLite's octet_length(column) reads the byte count from record metadata,
1707
+ * without loading the text's overflow pages or a payload-length index.
1530
1708
  */
1531
1709
  function readWindowSizes(owner, lineage, from, after, before, rows) {
1710
+ let ancestry = lineage.walk === "selected"
1711
+ ? readAncestry(owner, lineage.sessionId, lineage.tipId)
1712
+ : undefined;
1713
+ if (ancestry !== undefined) {
1714
+ const candidates = sessionReads
1715
+ .get(owner)
1716
+ ?.ancestry?.filter((row) => row.sequence <= lineage.newestSequence &&
1717
+ row.state !== "streaming" &&
1718
+ row.sequence > after &&
1719
+ row.sequence < before);
1720
+ const last = from === "newest" ? candidates?.[rows - 1] : undefined;
1721
+ ancestry = readAncestry(owner, lineage.sessionId, lineage.tipId, {
1722
+ floor: last === undefined ? after : last.sequence - 1,
1723
+ ceiling: before - 1,
1724
+ });
1725
+ }
1726
+ const direction = from === "newest" ? "DESC" : "ASC";
1532
1727
  const ends = `SELECT
1533
1728
  (SELECT json_group_array(json_array(sequence, bytes)) FROM (
1534
- SELECT sequence, LENGTH(CAST(data_json AS BLOB)) AS bytes
1729
+ SELECT sequence, octet_length(data_json) AS bytes
1535
1730
  FROM session_entries
1536
1731
  WHERE ${from === "newest" ? "" : "session_id = ? AND"}
1537
1732
  state != 'streaming'
1538
1733
  AND sequence > ? AND sequence < ?
1539
- AND id IN (SELECT id FROM branch)
1540
- ORDER BY sequence ${from === "newest" ? "DESC" : "ASC"}
1734
+ AND id IN (SELECT id FROM ${ancestry === undefined ? "branch" : "candidates"})
1735
+ ORDER BY sequence ${direction}
1541
1736
  LIMIT ?)) AS sizes,
1542
1737
  (SELECT parent_id FROM branch ORDER BY sequence ASC LIMIT 1)
1543
1738
  AS deepest_parent,
@@ -1546,9 +1741,18 @@ function readWindowSizes(owner, lineage, from, after, before, rows) {
1546
1741
  const counts = (row) => `CASE WHEN ${row}.state != 'streaming'
1547
1742
  AND ${row}.sequence < ? AND ${row}.sequence > ?
1548
1743
  THEN 1 ELSE 0 END`;
1549
- const raw = from === "newest"
1744
+ const raw = ancestry !== undefined
1550
1745
  ? owner.sql
1551
- .exec(`WITH RECURSIVE branch (id, parent_id, sequence, state, taken) AS (
1746
+ .exec(`WITH ${ancestryTable("branch")},
1747
+ candidates AS MATERIALIZED (
1748
+ SELECT id FROM branch
1749
+ WHERE state != 'streaming' AND sequence > ? AND sequence < ?
1750
+ ORDER BY sequence ${direction} LIMIT ?
1751
+ ) ${ends}`, ancestry, after, before, rows, ...(from === "newest" ? [] : [lineage.sessionId]), after, before, rows)
1752
+ .toArray()
1753
+ : from === "newest"
1754
+ ? owner.sql
1755
+ .exec(`WITH RECURSIVE branch (id, parent_id, sequence, state, taken) AS (
1552
1756
  SELECT id, parent_id, sequence, state,
1553
1757
  ${counts("session_entries")}
1554
1758
  FROM session_entries
@@ -1556,14 +1760,15 @@ function readWindowSizes(owner, lineage, from, after, before, rows) {
1556
1760
  UNION ALL
1557
1761
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
1558
1762
  child.taken + ${counts("parent")}
1559
- FROM session_entries AS parent
1560
- JOIN branch AS child ON parent.id = child.parent_id
1561
- WHERE parent.session_id = ? AND +parent.sequence < child.sequence
1763
+ FROM branch AS child
1764
+ CROSS JOIN session_entries AS parent
1765
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
1766
+ AND +parent.sequence < child.sequence
1562
1767
  AND child.taken < ? AND child.sequence > ?
1563
1768
  )
1564
1769
  ${ends}`, before, after, lineage.sessionId, lineage.tipId, before, after, lineage.sessionId, rows, after, after, before, rows)
1565
- .toArray()
1566
- : lineageQuery(owner, lineage, after, ends, lineage.sessionId, after, before, rows);
1770
+ .toArray()
1771
+ : lineageQuery(owner, lineage, { floor: after }, ends, lineage.sessionId, after, before, rows);
1567
1772
  const decoded = decodeWindowSizes(raw[0]);
1568
1773
  if (decoded._tag === "Failure")
1569
1774
  throw unavailable("Stored Session entry size is invalid");
@@ -1607,7 +1812,7 @@ export function readLineageWindow(owner, lineage, input) {
1607
1812
  }
1608
1813
  /** Whether a complete row with this sequence lies on the lineage. */
1609
1814
  export function lineageHasSequence(owner, lineage, rowSequence) {
1610
- return (lineageQuery(owner, lineage, rowSequence - 1, `SELECT 1 AS present
1815
+ return (lineageQuery(owner, lineage, { floor: rowSequence - 1, ceiling: rowSequence }, `SELECT 1 AS present
1611
1816
  FROM session_entries
1612
1817
  WHERE session_id = ? AND state != 'streaming' AND sequence = ?
1613
1818
  AND id IN (SELECT id FROM branch)`, lineage.sessionId, rowSequence).length > 0);
@@ -1637,15 +1842,9 @@ export function lineageEntrySequence(owner, lineage, id) {
1637
1842
  function newestLineagePreference(owner, sessionId, tipId) {
1638
1843
  const continues = (row) => `${row}.id NOT IN (SELECT id FROM targets)
1639
1844
  AND ${row}.sequence > (SELECT MIN(sequence) FROM targets)`;
1640
- const [newest] = walkedTargets(owner.sql
1641
- .exec(`WITH RECURSIVE targets AS MATERIALIZED (
1642
- SELECT id, sequence FROM session_entries
1845
+ const [newest] = readTargetIds(owner, sessionId, tipId, `SELECT id, sequence FROM session_entries
1643
1846
  WHERE session_id = ? AND type = 'model_preference'
1644
- AND state != 'streaming'
1645
- ),
1646
- ${targetWalk(continues("child"))}
1647
- ${walkSteps(continues("walk"))}`, sessionId, sessionId, tipId, sessionId)
1648
- .toArray());
1847
+ AND state != 'streaming'`, [sessionId], continues);
1649
1848
  return newest === undefined
1650
1849
  ? undefined
1651
1850
  : readEntryRow(owner, sessionId, newest);
@@ -1659,7 +1858,7 @@ function readLineageDocument(owner, lineage, extensionId, name) {
1659
1858
  if (lineage === undefined)
1660
1859
  return undefined;
1661
1860
  // No index finds documents yet, so this still walks the whole lineage.
1662
- const raw = lineageQuery(owner, lineage, 0, `SELECT ${entryColumns}
1861
+ const raw = lineageQuery(owner, lineage, { floor: 0 }, `SELECT ${entryColumns}
1663
1862
  FROM session_entries
1664
1863
  WHERE session_id = ? AND state != 'streaming' AND type = 'document'
1665
1864
  AND json_extract(data_json, '$.extensionId') = ?
@@ -1676,7 +1875,10 @@ function readLineageDocument(owner, lineage, extensionId, name) {
1676
1875
  throw unavailable("Stored Session document disagrees with its row");
1677
1876
  return entry.valueJson;
1678
1877
  }
1679
- const decodeOldestWorkRow = Schema.decodeUnknownResult(Schema.Struct({ sequence: Schema.NullOr(sequence) }), closed);
1878
+ const decodeWorkBounds = Schema.decodeUnknownResult(Schema.Struct({
1879
+ oldest: Schema.NullOr(sequence),
1880
+ newest: Schema.NullOr(sequence),
1881
+ }), closed);
1680
1882
  /**
1681
1883
  * Look up, on the lineage, only the facts of the given works. Diagnostics come
1682
1884
  * only from owned terminal fields, never provider message or retry text. The works'
@@ -1691,30 +1893,31 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
1691
1893
  cancellations: new Map(),
1692
1894
  });
1693
1895
  const works = JSON.stringify([...workIds]);
1694
- const oldest = decodeOldestWorkRow(owner.sql
1695
- .exec(`SELECT MIN(sequence) AS sequence FROM (
1696
- SELECT MIN(sequence) AS sequence
1896
+ const bounds = decodeWorkBounds(owner.sql
1897
+ .exec(`SELECT MIN(oldest) AS oldest, MAX(newest) AS newest FROM (
1898
+ SELECT MIN(sequence) AS oldest, MAX(sequence) AS newest
1697
1899
  FROM session_entries INDEXED BY idx_session_entries_work
1698
1900
  WHERE work_id IN (SELECT value FROM json_each(?))
1699
1901
  AND session_id = ?
1700
1902
  UNION ALL
1701
- SELECT MIN(sequence) FROM session_entries
1903
+ SELECT MIN(sequence), MAX(sequence) FROM session_entries
1702
1904
  INDEXED BY idx_session_entries_cancellation_target
1703
1905
  WHERE session_id = ? AND type = 'cancellation'
1704
1906
  AND json_extract(data_json, '$.targetWorkId')
1705
1907
  IN (SELECT value FROM json_each(?))
1706
1908
  )`, works, lineage.sessionId, lineage.sessionId, works)
1707
1909
  .one());
1708
- if (oldest._tag === "Failure")
1910
+ if (bounds._tag === "Failure")
1709
1911
  throw unavailable("Stored Session work order is invalid");
1710
- if (oldest.success.sequence === null)
1912
+ if (bounds.success.oldest === null || bounds.success.newest === null)
1711
1913
  return Object.freeze({
1712
1914
  commands: new Set(),
1713
1915
  diagnostics: new Map(),
1714
1916
  cancellations: new Map(),
1715
1917
  });
1716
- const floor = oldest.success.sequence - 1;
1717
- const commands = new Set(lineageQuery(owner, lineage, floor, `SELECT DISTINCT work_id
1918
+ const floor = bounds.success.oldest - 1;
1919
+ const range = { floor, ceiling: bounds.success.newest };
1920
+ const commands = new Set(lineageQuery(owner, lineage, range, `SELECT DISTINCT work_id
1718
1921
  FROM session_entries
1719
1922
  WHERE session_id = ? AND sequence > ? AND state != 'streaming' AND type = 'command'
1720
1923
  AND work_id IN (SELECT value FROM json_each(?))
@@ -1725,7 +1928,7 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
1725
1928
  return decoded.success.work_id;
1726
1929
  }));
1727
1930
  const diagnostics = new Map();
1728
- for (const raw of lineageQuery(owner, lineage, floor, `SELECT work_id, diagnostic FROM (
1931
+ for (const raw of lineageQuery(owner, lineage, range, `SELECT work_id, diagnostic FROM (
1729
1932
  SELECT work_id, sequence,
1730
1933
  json_extract(data_json, '$.diagnostic') AS diagnostic
1731
1934
  FROM session_entries
@@ -1742,7 +1945,7 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
1742
1945
  diagnostics.set(decoded.success.work_id, decoded.success.diagnostic);
1743
1946
  }
1744
1947
  const cancellations = new Map();
1745
- for (const { entry } of lineageQuery(owner, lineage, floor, `SELECT ${entryColumns}
1948
+ for (const { entry } of lineageQuery(owner, lineage, range, `SELECT ${entryColumns}
1746
1949
  FROM session_entries
1747
1950
  WHERE session_id = ? AND sequence > ? AND state != 'streaming' AND type = 'cancellation'
1748
1951
  AND json_extract(data_json, '$.targetWorkId')
@@ -1771,25 +1974,80 @@ export function readSelectedPath(owner, input) {
1771
1974
  * steps past it; a statement without targets reads only its starting row,
1772
1975
  * so a missing one is still refused.
1773
1976
  */
1774
- function targetWalk(continues) {
1775
- return `walk (id, parent_id, sequence, state) AS (
1977
+ function targetWalk(sessionId, tipId, continues, ancestry) {
1978
+ if (ancestry !== undefined)
1979
+ return {
1980
+ sql: `${ancestryTable("ancestors")},
1981
+ stopped AS (
1982
+ SELECT child.*,
1983
+ COALESCE(SUM(CASE WHEN ${continues} THEN 0 ELSE 1 END)
1984
+ OVER (ORDER BY child.sequence DESC
1985
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) AS stops
1986
+ FROM ancestors AS child
1987
+ ),
1988
+ walk AS (
1989
+ SELECT id, parent_id, sequence, state FROM stopped WHERE stops = 0
1990
+ )`,
1991
+ bindings: [ancestry],
1992
+ recursive: false,
1993
+ };
1994
+ return {
1995
+ sql: `walk (id, parent_id, sequence, state) AS (
1776
1996
  SELECT id, parent_id, sequence, state
1777
1997
  FROM session_entries
1778
1998
  WHERE session_id = ? AND id = ?
1779
1999
  UNION ALL
1780
2000
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state
1781
- FROM session_entries AS parent
1782
- JOIN walk AS child ON parent.id = child.parent_id
1783
- WHERE parent.session_id = ? AND +parent.sequence < child.sequence
2001
+ FROM walk AS child
2002
+ CROSS JOIN session_entries AS parent
2003
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
2004
+ AND +parent.sequence < child.sequence
1784
2005
  AND ${continues}
1785
- )`;
2006
+ )`,
2007
+ bindings: [sessionId, tipId, sessionId],
2008
+ recursive: true,
2009
+ };
2010
+ }
2011
+ /**
2012
+ * A shared prefix can answer a target read only if it reaches that read's
2013
+ * stopping point without crossing an omitted gap. Otherwise run the original
2014
+ * target-bounded walk; a cache horizon is not evidence of damaged history.
2015
+ */
2016
+ function readTargetIds(owner, sessionId, tipId, targets, bindings, continues) {
2017
+ const ancestry = readAncestry(owner, sessionId, tipId);
2018
+ const run = (shared) => {
2019
+ const walk = targetWalk(sessionId, tipId, continues("child"), shared);
2020
+ return owner.sql
2021
+ .exec(`WITH ${walk.recursive ? "RECURSIVE " : ""}targets AS MATERIALIZED (
2022
+ ${targets}
2023
+ ),
2024
+ ${walk.sql}
2025
+ ${walkSteps(continues("walk"))}`, ...bindings, ...walk.bindings)
2026
+ .toArray();
2027
+ };
2028
+ let raw = run(ancestry);
2029
+ if (ancestry !== undefined) {
2030
+ const last = raw.at(-1);
2031
+ if (last !== undefined) {
2032
+ const deepest = decodeWalkStep(last);
2033
+ if (deepest._tag === "Failure")
2034
+ throw unavailable("Stored Session walk row is invalid");
2035
+ if ((deepest.success.continues === 1 &&
2036
+ deepest.success.parent_id !== null) ||
2037
+ readAncestry(owner, sessionId, tipId, {
2038
+ floor: deepest.success.sequence - 1,
2039
+ }) === undefined)
2040
+ raw = run(undefined);
2041
+ }
2042
+ }
2043
+ return walkedTargets(raw);
1786
2044
  }
1787
2045
  /**
1788
2046
  * The walked targets, newest first, then the deepest walked row, each with
1789
2047
  * whether the walk meant to pass it. Only these rows reach JavaScript.
1790
2048
  */
1791
2049
  function walkSteps(continues) {
1792
- return `SELECT id, parent_id, target, continues FROM (
2050
+ return `SELECT id, parent_id, sequence, target, continues FROM (
1793
2051
  SELECT id, parent_id, sequence,
1794
2052
  id IN (SELECT id FROM targets) AS target,
1795
2053
  COALESCE((${continues}), 0) AS continues
@@ -1802,6 +2060,7 @@ const flag = Schema.Literals([0, 1]);
1802
2060
  const decodeWalkStep = Schema.decodeUnknownResult(Schema.Struct({
1803
2061
  id: boundedId,
1804
2062
  parent_id: Schema.NullOr(Schema.String),
2063
+ sequence,
1805
2064
  target: flag,
1806
2065
  continues: flag,
1807
2066
  }), closed);
@@ -1846,9 +2105,10 @@ const contextWalk = `WITH RECURSIVE walk (id, parent_id, sequence, state, head)
1846
2105
  UNION ALL
1847
2106
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
1848
2107
  COALESCE(child.head, ${compactionHead("parent")})
1849
- FROM session_entries AS parent
1850
- JOIN walk AS child ON parent.id = child.parent_id
1851
- WHERE parent.session_id = ? AND +parent.sequence < child.sequence
2108
+ FROM walk AS child
2109
+ CROSS JOIN session_entries AS parent
2110
+ WHERE parent.id = child.parent_id AND parent.session_id = ?
2111
+ AND +parent.sequence < child.sequence
1852
2112
  AND (child.head IS NOT child.id OR child.state = 'streaming')
1853
2113
  )`;
1854
2114
  /**
@@ -1860,9 +2120,7 @@ const contextWalk = `WITH RECURSIVE walk (id, parent_id, sequence, state, head)
1860
2120
  function readDecidingFacts(owner, sessionId, kept, window, workId) {
1861
2121
  const commands = JSON.stringify(window.flatMap(({ entry }) => entry.kind === "command" ? [entry.workId] : []));
1862
2122
  const continues = (row) => `${row}.sequence > (SELECT MIN(sequence) FROM targets)`;
1863
- const facts = walkedTargets(owner.sql
1864
- .exec(`WITH RECURSIVE targets AS MATERIALIZED (
1865
- SELECT id, sequence FROM session_entries
2123
+ const facts = readTargetIds(owner, sessionId, kept.id, `SELECT id, sequence FROM session_entries
1866
2124
  WHERE session_id = ? AND type = 'compaction' AND work_id = ?
1867
2125
  AND sequence < ? AND state != 'streaming'
1868
2126
  UNION ALL
@@ -1872,11 +2130,7 @@ function readDecidingFacts(owner, sessionId, kept, window, workId) {
1872
2130
  WHERE session_id = ? AND type = 'cancellation'
1873
2131
  AND json_extract(data_json, '$.targetWorkId')
1874
2132
  IN (SELECT value FROM json_each(?))
1875
- AND sequence < ? AND state != 'streaming'
1876
- ),
1877
- ${targetWalk(continues("child"))}
1878
- ${walkSteps(continues("walk"))}`, sessionId, workId, kept.sequence, sessionId, commands, kept.sequence, sessionId, kept.id, sessionId)
1879
- .toArray());
2133
+ AND sequence < ? AND state != 'streaming'`, [sessionId, workId, kept.sequence, sessionId, commands, kept.sequence], continues);
1880
2134
  if (facts.length === 0)
1881
2135
  return Object.freeze([]);
1882
2136
  return Object.freeze(owner.sql
@@ -1895,12 +2149,40 @@ function readDecidingFacts(owner, sessionId, kept, window, workId) {
1895
2149
  */
1896
2150
  function readContextWindow(owner, sessionId, tipId) {
1897
2151
  const failures = lineageFailures.selected;
2152
+ let ancestry = readAncestry(owner, sessionId, tipId);
2153
+ // The seed owns this cache's compaction boundary. An ancestor tip can have an
2154
+ // older boundary of its own (including one in an omitted historical gap).
2155
+ if (sessionReads.get(owner)?.ancestry?.[0]?.id !== tipId)
2156
+ ancestry = undefined;
2157
+ const source = ancestry === undefined
2158
+ ? { sql: contextWalk, bindings: [sessionId, tipId, sessionId] }
2159
+ : {
2160
+ sql: `WITH ${ancestryTable("ancestors")},
2161
+ boundary AS (
2162
+ SELECT sequence, json_extract(data_json, '$.head') AS head
2163
+ FROM session_entries
2164
+ WHERE session_id = ? AND type = 'compaction'
2165
+ AND state != 'streaming'
2166
+ AND id IN (SELECT id FROM ancestors)
2167
+ ORDER BY sequence DESC LIMIT 1
2168
+ ),
2169
+ walk AS (
2170
+ SELECT * FROM ancestors
2171
+ WHERE sequence >= COALESCE((
2172
+ SELECT ancestors.sequence FROM ancestors, boundary
2173
+ WHERE ancestors.id = boundary.head
2174
+ AND ancestors.state != 'streaming'
2175
+ AND ancestors.sequence <= boundary.sequence
2176
+ ), 0)
2177
+ )`,
2178
+ bindings: [ancestry, sessionId],
2179
+ };
1898
2180
  const rows = owner.sql
1899
- .exec(`${contextWalk}
2181
+ .exec(`${source.sql}
1900
2182
  SELECT ${entryColumns}
1901
2183
  FROM session_entries
1902
2184
  WHERE id IN (SELECT id FROM walk) AND state != 'streaming'
1903
- ORDER BY sequence ASC`, sessionId, tipId, sessionId)
2185
+ ORDER BY sequence ASC`, ...source.bindings)
1904
2186
  .toArray()
1905
2187
  .map((raw) => parseEntryRow(owner, raw));
1906
2188
  const kept = rows[0];
@@ -1961,7 +2243,7 @@ function readRunContextRows(owner, sessionId, command, current, workId) {
1961
2243
  throw unavailable("Accepted Session command lineage is unavailable");
1962
2244
  const tail = current === undefined
1963
2245
  ? []
1964
- : lineageQuery(owner, current, command.sequence, `SELECT ${entryColumns}
2246
+ : lineageQuery(owner, current, { floor: command.sequence }, `SELECT ${entryColumns}
1965
2247
  FROM session_entries
1966
2248
  WHERE session_id = ? AND sequence > ? AND state != 'streaming'
1967
2249
  AND id IN (SELECT id FROM branch)
@@ -2325,6 +2607,7 @@ export function acceptSessionCommandOwned(owner, sessionId, input, meta, origin,
2325
2607
  jobId: commandFactId,
2326
2608
  sessionId,
2327
2609
  commandFactId,
2610
+ delivery: input.type === "prompt" ? input.delivery : "followUp",
2328
2611
  });
2329
2612
  return acceptanceFromAdmission(command, "new");
2330
2613
  }