@fungi.computer/shiitake 0.6.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 (63) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +1 -1
  3. package/dist/agent-control.js +40 -21
  4. package/dist/client-visitor.d.ts +2 -0
  5. package/dist/client-visitor.js +6 -1
  6. package/dist/client-wire.d.ts +4 -0
  7. package/dist/client-wire.js +18 -3
  8. package/dist/client.d.ts +24 -4
  9. package/dist/client.js +22 -0
  10. package/dist/cloudflare.d.ts +20 -0
  11. package/dist/cloudflare.js +4 -1
  12. package/dist/delegation.d.ts +5 -0
  13. package/dist/delegation.js +24 -8
  14. package/dist/effect.d.ts +1 -1
  15. package/dist/errors.d.ts +2 -2
  16. package/dist/errors.js +2 -0
  17. package/dist/extensions.d.ts +1 -1
  18. package/dist/fleet.d.ts +10 -2
  19. package/dist/fleet.js +77 -19
  20. package/dist/http-routes.d.ts +12 -9
  21. package/dist/http.d.ts +1 -0
  22. package/dist/http.js +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/internal/admission-door.d.ts +1 -1
  25. package/dist/internal/admission-door.js +8 -4
  26. package/dist/internal/contracts.d.ts +5 -0
  27. package/dist/internal/coordinator.js +107 -50
  28. package/dist/internal/effect-contracts.d.ts +5 -0
  29. package/dist/internal/extension-runtime.js +5 -3
  30. package/dist/internal/promise-facade.js +2 -0
  31. package/dist/internal/run-scope.js +27 -7
  32. package/dist/internal/runtime-coordinator.d.ts +14 -0
  33. package/dist/internal/runtime-coordinator.js +52 -10
  34. package/dist/internal/runtime-core.d.ts +3 -0
  35. package/dist/internal/runtime-core.js +1 -0
  36. package/dist/internal/runtime-reconciliation.js +4 -1
  37. package/dist/internal/session-context.d.ts +5 -5
  38. package/dist/internal/session-context.js +80 -83
  39. package/dist/internal/session-status.d.ts +2 -2
  40. package/dist/internal/session-status.js +5 -3
  41. package/dist/internal/woodstock/client-projection.js +5 -11
  42. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  43. package/dist/internal/woodstock/fleet-store.js +24 -4
  44. package/dist/internal/woodstock/index.d.ts +3 -3
  45. package/dist/internal/woodstock/index.js +16 -6
  46. package/dist/internal/woodstock/progress.d.ts +11 -1
  47. package/dist/internal/woodstock/progress.js +126 -25
  48. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  49. package/dist/internal/woodstock/session-store.d.ts +14 -2
  50. package/dist/internal/woodstock/session-store.js +329 -66
  51. package/dist/node.js +11 -2
  52. package/dist/server-visitor-stream.js +33 -24
  53. package/docs/api/agent-control.md +3 -2
  54. package/docs/api/client.md +77 -8
  55. package/docs/api/cloudflare.md +16 -0
  56. package/docs/api/delegation.md +84 -50
  57. package/docs/api/effect.md +85 -7
  58. package/docs/api/extensions.md +8 -8
  59. package/docs/api/fleet.md +87 -27
  60. package/docs/api/http.md +28 -0
  61. package/docs/api/index.md +39 -15
  62. package/docs/api/testing.md +9 -7
  63. package/package.json +5 -5
@@ -270,9 +270,9 @@ function selectedLineageFirstPromptId(owner, sessionId, leafId) {
270
270
  UNION
271
271
  SELECT parent.id, parent.parent_id, parent.sequence,
272
272
  parent.type, parent.role, parent.state, parent.work_id
273
- FROM session_entries AS parent
274
- JOIN branch AS child ON parent.id = child.parent_id
275
- 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 = ?
276
276
  )
277
277
  SELECT
278
278
  (SELECT command.id FROM branch AS command
@@ -536,6 +536,12 @@ const pendingAdmissions = `FROM session_command_admissions AS a
536
536
  WHERE e.session_id = a.session_id AND e.work_id = a.work_id
537
537
  AND e.type IN ('command', 'cancellation', 'run_terminal', 'work_terminal')
538
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
+ }
539
545
  const decodeIdentity = Schema.decodeUnknownSync(Schema.Struct({ identity: Schema.NullOr(Schema.String) }));
540
546
  /** Extract the host-selected principal without giving the engine domain grammar. */
541
547
  export function readAdmissionIdentity(owner, path, meta) {
@@ -578,7 +584,8 @@ export function readPendingSessionCommands(owner, sessionId, limit, only = "all"
578
584
  .one()).total;
579
585
  const commands = owner.sql
580
586
  .exec(`SELECT ${admissionColumns("a.")} ${pendingAdmissions} ${filter}
581
- 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)
582
589
  .toArray()
583
590
  .map(parseCommandAdmissionRow);
584
591
  return { commands, total };
@@ -647,6 +654,7 @@ export function settleSessionControls(owner, input) {
647
654
  jobId: row.entry.workId,
648
655
  sessionId: input.sessionId,
649
656
  commandFactId: row.id,
657
+ delivery: "steer",
650
658
  });
651
659
  continue;
652
660
  }
@@ -1153,7 +1161,19 @@ export function prepareSessionStore(owner) {
1153
1161
  session_id TEXT NOT NULL UNIQUE REFERENCES sessions(id)
1154
1162
  )
1155
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
+ )`);
1156
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)`);
1157
1177
  owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_session ON session_entries(session_id, sequence)");
1158
1178
  owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_streaming ON session_entries(session_id, state)");
1159
1179
  owner.sql.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_session_entries_request
@@ -1359,6 +1379,8 @@ const lineageByteLimit = 4 * 1_048_576;
1359
1379
  * selected walk steps only to an older parent, so it ends without
1360
1380
  * deduplicating; the fork walk keeps every link for its whole-lineage check.
1361
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.
1362
1384
  const lineageWalks = {
1363
1385
  selected: `WITH RECURSIVE branch (id, parent_id, sequence, state) AS (
1364
1386
  SELECT id, parent_id, sequence, state
@@ -1366,9 +1388,10 @@ const lineageWalks = {
1366
1388
  WHERE session_id = ? AND id = ?
1367
1389
  UNION ALL
1368
1390
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state
1369
- FROM session_entries AS parent
1370
- JOIN branch AS child ON parent.id = child.parent_id
1371
- 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 > ?
1372
1395
  AND +parent.sequence < child.sequence
1373
1396
  )`,
1374
1397
  fork: `WITH RECURSIVE branch (id, parent_id, sequence, state) AS (
@@ -1377,9 +1400,10 @@ const lineageWalks = {
1377
1400
  WHERE session_id = ? AND id = ? AND state != 'streaming'
1378
1401
  UNION
1379
1402
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state
1380
- FROM session_entries AS parent
1381
- JOIN branch AS child ON parent.id = child.parent_id
1382
- 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 > ?
1383
1407
  AND parent.state != 'streaming'
1384
1408
  )`,
1385
1409
  };
@@ -1395,6 +1419,130 @@ const lineageFailures = {
1395
1419
  invalid: "Fork path is invalid",
1396
1420
  },
1397
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
+ }
1398
1546
  const count = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)));
1399
1547
  const decodeLineageCheck = Schema.decodeUnknownResult(Schema.Struct({
1400
1548
  length: count,
@@ -1412,9 +1560,16 @@ const decodeLineageDiagnostic = Schema.decodeUnknownResult(Schema.Struct({ work_
1412
1560
  * the tip while its rows are above it, so only those rows, and the first at
1413
1561
  * or below it, are visited.
1414
1562
  */
1415
- 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();
1416
1571
  return owner.sql
1417
- .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)
1418
1573
  .toArray();
1419
1574
  }
1420
1575
  /**
@@ -1513,7 +1668,7 @@ export function readSessionLineage(owner, input) {
1513
1668
  });
1514
1669
  }
1515
1670
  function readLineageRange(owner, lineage, oldest, newest) {
1516
- 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}
1517
1672
  FROM session_entries
1518
1673
  WHERE session_id = ? AND state != 'streaming'
1519
1674
  AND sequence BETWEEN ? AND ?
@@ -1546,18 +1701,38 @@ const decodeWindowSizes = Schema.decodeUnknownResult(Schema.Struct({
1546
1701
  * walk visits the rows from the tip down to the window, never the history
1547
1702
  * below it. The deepest walked row comes back with the sizes, so a walk that
1548
1703
  * ended at a missing parent above where it meant to stop is refused, never
1549
- * 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.
1550
1708
  */
1551
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";
1552
1727
  const ends = `SELECT
1553
1728
  (SELECT json_group_array(json_array(sequence, bytes)) FROM (
1554
- SELECT sequence, LENGTH(CAST(data_json AS BLOB)) AS bytes
1729
+ SELECT sequence, octet_length(data_json) AS bytes
1555
1730
  FROM session_entries
1556
1731
  WHERE ${from === "newest" ? "" : "session_id = ? AND"}
1557
1732
  state != 'streaming'
1558
1733
  AND sequence > ? AND sequence < ?
1559
- AND id IN (SELECT id FROM branch)
1560
- ORDER BY sequence ${from === "newest" ? "DESC" : "ASC"}
1734
+ AND id IN (SELECT id FROM ${ancestry === undefined ? "branch" : "candidates"})
1735
+ ORDER BY sequence ${direction}
1561
1736
  LIMIT ?)) AS sizes,
1562
1737
  (SELECT parent_id FROM branch ORDER BY sequence ASC LIMIT 1)
1563
1738
  AS deepest_parent,
@@ -1566,9 +1741,18 @@ function readWindowSizes(owner, lineage, from, after, before, rows) {
1566
1741
  const counts = (row) => `CASE WHEN ${row}.state != 'streaming'
1567
1742
  AND ${row}.sequence < ? AND ${row}.sequence > ?
1568
1743
  THEN 1 ELSE 0 END`;
1569
- const raw = from === "newest"
1744
+ const raw = ancestry !== undefined
1570
1745
  ? owner.sql
1571
- .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 (
1572
1756
  SELECT id, parent_id, sequence, state,
1573
1757
  ${counts("session_entries")}
1574
1758
  FROM session_entries
@@ -1576,14 +1760,15 @@ function readWindowSizes(owner, lineage, from, after, before, rows) {
1576
1760
  UNION ALL
1577
1761
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
1578
1762
  child.taken + ${counts("parent")}
1579
- FROM session_entries AS parent
1580
- JOIN branch AS child ON parent.id = child.parent_id
1581
- 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
1582
1767
  AND child.taken < ? AND child.sequence > ?
1583
1768
  )
1584
1769
  ${ends}`, before, after, lineage.sessionId, lineage.tipId, before, after, lineage.sessionId, rows, after, after, before, rows)
1585
- .toArray()
1586
- : lineageQuery(owner, lineage, after, ends, lineage.sessionId, after, before, rows);
1770
+ .toArray()
1771
+ : lineageQuery(owner, lineage, { floor: after }, ends, lineage.sessionId, after, before, rows);
1587
1772
  const decoded = decodeWindowSizes(raw[0]);
1588
1773
  if (decoded._tag === "Failure")
1589
1774
  throw unavailable("Stored Session entry size is invalid");
@@ -1627,7 +1812,7 @@ export function readLineageWindow(owner, lineage, input) {
1627
1812
  }
1628
1813
  /** Whether a complete row with this sequence lies on the lineage. */
1629
1814
  export function lineageHasSequence(owner, lineage, rowSequence) {
1630
- return (lineageQuery(owner, lineage, rowSequence - 1, `SELECT 1 AS present
1815
+ return (lineageQuery(owner, lineage, { floor: rowSequence - 1, ceiling: rowSequence }, `SELECT 1 AS present
1631
1816
  FROM session_entries
1632
1817
  WHERE session_id = ? AND state != 'streaming' AND sequence = ?
1633
1818
  AND id IN (SELECT id FROM branch)`, lineage.sessionId, rowSequence).length > 0);
@@ -1657,15 +1842,9 @@ export function lineageEntrySequence(owner, lineage, id) {
1657
1842
  function newestLineagePreference(owner, sessionId, tipId) {
1658
1843
  const continues = (row) => `${row}.id NOT IN (SELECT id FROM targets)
1659
1844
  AND ${row}.sequence > (SELECT MIN(sequence) FROM targets)`;
1660
- const [newest] = walkedTargets(owner.sql
1661
- .exec(`WITH RECURSIVE targets AS MATERIALIZED (
1662
- SELECT id, sequence FROM session_entries
1845
+ const [newest] = readTargetIds(owner, sessionId, tipId, `SELECT id, sequence FROM session_entries
1663
1846
  WHERE session_id = ? AND type = 'model_preference'
1664
- AND state != 'streaming'
1665
- ),
1666
- ${targetWalk(continues("child"))}
1667
- ${walkSteps(continues("walk"))}`, sessionId, sessionId, tipId, sessionId)
1668
- .toArray());
1847
+ AND state != 'streaming'`, [sessionId], continues);
1669
1848
  return newest === undefined
1670
1849
  ? undefined
1671
1850
  : readEntryRow(owner, sessionId, newest);
@@ -1679,7 +1858,7 @@ function readLineageDocument(owner, lineage, extensionId, name) {
1679
1858
  if (lineage === undefined)
1680
1859
  return undefined;
1681
1860
  // No index finds documents yet, so this still walks the whole lineage.
1682
- const raw = lineageQuery(owner, lineage, 0, `SELECT ${entryColumns}
1861
+ const raw = lineageQuery(owner, lineage, { floor: 0 }, `SELECT ${entryColumns}
1683
1862
  FROM session_entries
1684
1863
  WHERE session_id = ? AND state != 'streaming' AND type = 'document'
1685
1864
  AND json_extract(data_json, '$.extensionId') = ?
@@ -1696,7 +1875,10 @@ function readLineageDocument(owner, lineage, extensionId, name) {
1696
1875
  throw unavailable("Stored Session document disagrees with its row");
1697
1876
  return entry.valueJson;
1698
1877
  }
1699
- 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);
1700
1882
  /**
1701
1883
  * Look up, on the lineage, only the facts of the given works. Diagnostics come
1702
1884
  * only from owned terminal fields, never provider message or retry text. The works'
@@ -1711,30 +1893,31 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
1711
1893
  cancellations: new Map(),
1712
1894
  });
1713
1895
  const works = JSON.stringify([...workIds]);
1714
- const oldest = decodeOldestWorkRow(owner.sql
1715
- .exec(`SELECT MIN(sequence) AS sequence FROM (
1716
- 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
1717
1899
  FROM session_entries INDEXED BY idx_session_entries_work
1718
1900
  WHERE work_id IN (SELECT value FROM json_each(?))
1719
1901
  AND session_id = ?
1720
1902
  UNION ALL
1721
- SELECT MIN(sequence) FROM session_entries
1903
+ SELECT MIN(sequence), MAX(sequence) FROM session_entries
1722
1904
  INDEXED BY idx_session_entries_cancellation_target
1723
1905
  WHERE session_id = ? AND type = 'cancellation'
1724
1906
  AND json_extract(data_json, '$.targetWorkId')
1725
1907
  IN (SELECT value FROM json_each(?))
1726
1908
  )`, works, lineage.sessionId, lineage.sessionId, works)
1727
1909
  .one());
1728
- if (oldest._tag === "Failure")
1910
+ if (bounds._tag === "Failure")
1729
1911
  throw unavailable("Stored Session work order is invalid");
1730
- if (oldest.success.sequence === null)
1912
+ if (bounds.success.oldest === null || bounds.success.newest === null)
1731
1913
  return Object.freeze({
1732
1914
  commands: new Set(),
1733
1915
  diagnostics: new Map(),
1734
1916
  cancellations: new Map(),
1735
1917
  });
1736
- const floor = oldest.success.sequence - 1;
1737
- 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
1738
1921
  FROM session_entries
1739
1922
  WHERE session_id = ? AND sequence > ? AND state != 'streaming' AND type = 'command'
1740
1923
  AND work_id IN (SELECT value FROM json_each(?))
@@ -1745,7 +1928,7 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
1745
1928
  return decoded.success.work_id;
1746
1929
  }));
1747
1930
  const diagnostics = new Map();
1748
- 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 (
1749
1932
  SELECT work_id, sequence,
1750
1933
  json_extract(data_json, '$.diagnostic') AS diagnostic
1751
1934
  FROM session_entries
@@ -1762,7 +1945,7 @@ export function readLineageWorkFacts(owner, lineage, workIds) {
1762
1945
  diagnostics.set(decoded.success.work_id, decoded.success.diagnostic);
1763
1946
  }
1764
1947
  const cancellations = new Map();
1765
- for (const { entry } of lineageQuery(owner, lineage, floor, `SELECT ${entryColumns}
1948
+ for (const { entry } of lineageQuery(owner, lineage, range, `SELECT ${entryColumns}
1766
1949
  FROM session_entries
1767
1950
  WHERE session_id = ? AND sequence > ? AND state != 'streaming' AND type = 'cancellation'
1768
1951
  AND json_extract(data_json, '$.targetWorkId')
@@ -1791,25 +1974,80 @@ export function readSelectedPath(owner, input) {
1791
1974
  * steps past it; a statement without targets reads only its starting row,
1792
1975
  * so a missing one is still refused.
1793
1976
  */
1794
- function targetWalk(continues) {
1795
- 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 (
1796
1996
  SELECT id, parent_id, sequence, state
1797
1997
  FROM session_entries
1798
1998
  WHERE session_id = ? AND id = ?
1799
1999
  UNION ALL
1800
2000
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state
1801
- FROM session_entries AS parent
1802
- JOIN walk AS child ON parent.id = child.parent_id
1803
- 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
1804
2005
  AND ${continues}
1805
- )`;
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);
1806
2044
  }
1807
2045
  /**
1808
2046
  * The walked targets, newest first, then the deepest walked row, each with
1809
2047
  * whether the walk meant to pass it. Only these rows reach JavaScript.
1810
2048
  */
1811
2049
  function walkSteps(continues) {
1812
- return `SELECT id, parent_id, target, continues FROM (
2050
+ return `SELECT id, parent_id, sequence, target, continues FROM (
1813
2051
  SELECT id, parent_id, sequence,
1814
2052
  id IN (SELECT id FROM targets) AS target,
1815
2053
  COALESCE((${continues}), 0) AS continues
@@ -1822,6 +2060,7 @@ const flag = Schema.Literals([0, 1]);
1822
2060
  const decodeWalkStep = Schema.decodeUnknownResult(Schema.Struct({
1823
2061
  id: boundedId,
1824
2062
  parent_id: Schema.NullOr(Schema.String),
2063
+ sequence,
1825
2064
  target: flag,
1826
2065
  continues: flag,
1827
2066
  }), closed);
@@ -1866,9 +2105,10 @@ const contextWalk = `WITH RECURSIVE walk (id, parent_id, sequence, state, head)
1866
2105
  UNION ALL
1867
2106
  SELECT parent.id, parent.parent_id, parent.sequence, parent.state,
1868
2107
  COALESCE(child.head, ${compactionHead("parent")})
1869
- FROM session_entries AS parent
1870
- JOIN walk AS child ON parent.id = child.parent_id
1871
- 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
1872
2112
  AND (child.head IS NOT child.id OR child.state = 'streaming')
1873
2113
  )`;
1874
2114
  /**
@@ -1880,9 +2120,7 @@ const contextWalk = `WITH RECURSIVE walk (id, parent_id, sequence, state, head)
1880
2120
  function readDecidingFacts(owner, sessionId, kept, window, workId) {
1881
2121
  const commands = JSON.stringify(window.flatMap(({ entry }) => entry.kind === "command" ? [entry.workId] : []));
1882
2122
  const continues = (row) => `${row}.sequence > (SELECT MIN(sequence) FROM targets)`;
1883
- const facts = walkedTargets(owner.sql
1884
- .exec(`WITH RECURSIVE targets AS MATERIALIZED (
1885
- SELECT id, sequence FROM session_entries
2123
+ const facts = readTargetIds(owner, sessionId, kept.id, `SELECT id, sequence FROM session_entries
1886
2124
  WHERE session_id = ? AND type = 'compaction' AND work_id = ?
1887
2125
  AND sequence < ? AND state != 'streaming'
1888
2126
  UNION ALL
@@ -1892,11 +2130,7 @@ function readDecidingFacts(owner, sessionId, kept, window, workId) {
1892
2130
  WHERE session_id = ? AND type = 'cancellation'
1893
2131
  AND json_extract(data_json, '$.targetWorkId')
1894
2132
  IN (SELECT value FROM json_each(?))
1895
- AND sequence < ? AND state != 'streaming'
1896
- ),
1897
- ${targetWalk(continues("child"))}
1898
- ${walkSteps(continues("walk"))}`, sessionId, workId, kept.sequence, sessionId, commands, kept.sequence, sessionId, kept.id, sessionId)
1899
- .toArray());
2133
+ AND sequence < ? AND state != 'streaming'`, [sessionId, workId, kept.sequence, sessionId, commands, kept.sequence], continues);
1900
2134
  if (facts.length === 0)
1901
2135
  return Object.freeze([]);
1902
2136
  return Object.freeze(owner.sql
@@ -1915,12 +2149,40 @@ function readDecidingFacts(owner, sessionId, kept, window, workId) {
1915
2149
  */
1916
2150
  function readContextWindow(owner, sessionId, tipId) {
1917
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
+ };
1918
2180
  const rows = owner.sql
1919
- .exec(`${contextWalk}
2181
+ .exec(`${source.sql}
1920
2182
  SELECT ${entryColumns}
1921
2183
  FROM session_entries
1922
2184
  WHERE id IN (SELECT id FROM walk) AND state != 'streaming'
1923
- ORDER BY sequence ASC`, sessionId, tipId, sessionId)
2185
+ ORDER BY sequence ASC`, ...source.bindings)
1924
2186
  .toArray()
1925
2187
  .map((raw) => parseEntryRow(owner, raw));
1926
2188
  const kept = rows[0];
@@ -1981,7 +2243,7 @@ function readRunContextRows(owner, sessionId, command, current, workId) {
1981
2243
  throw unavailable("Accepted Session command lineage is unavailable");
1982
2244
  const tail = current === undefined
1983
2245
  ? []
1984
- : lineageQuery(owner, current, command.sequence, `SELECT ${entryColumns}
2246
+ : lineageQuery(owner, current, { floor: command.sequence }, `SELECT ${entryColumns}
1985
2247
  FROM session_entries
1986
2248
  WHERE session_id = ? AND sequence > ? AND state != 'streaming'
1987
2249
  AND id IN (SELECT id FROM branch)
@@ -2345,6 +2607,7 @@ export function acceptSessionCommandOwned(owner, sessionId, input, meta, origin,
2345
2607
  jobId: commandFactId,
2346
2608
  sessionId,
2347
2609
  commandFactId,
2610
+ delivery: input.type === "prompt" ? input.delivery : "followUp",
2348
2611
  });
2349
2612
  return acceptanceFromAdmission(command, "new");
2350
2613
  }
package/dist/node.js CHANGED
@@ -50,8 +50,13 @@ export function nodeRuntimeHost(observeFailure) {
50
50
  wakeTimer = setTimeout(() => {
51
51
  wakeTimer = undefined;
52
52
  wakeAt = undefined;
53
+ // A capped wait may finish before the actual deadline.
54
+ if (at > Date.now()) {
55
+ scheduleWake(at);
56
+ return;
57
+ }
53
58
  void bound?.requestWake().catch(reportFailure);
54
- }, Math.max(0, at - Date.now()));
59
+ }, Math.min(2_147_483_647, Math.max(0, at - Date.now())));
55
60
  };
56
61
  const runtime = Object.freeze({
57
62
  bind: ({ reconcile, pendingWork }) => {
@@ -133,7 +138,11 @@ export function nodeRuntimeHost(observeFailure) {
133
138
  return Object.freeze({
134
139
  runtime,
135
140
  scheduleWake,
136
- close: () => bound?.close() ?? Promise.resolve(),
141
+ close: () => {
142
+ scheduleWake(null);
143
+ closed = true;
144
+ return bound?.close() ?? Promise.resolve();
145
+ },
137
146
  });
138
147
  }
139
148
  /**