@sema-agent/core 7.21.0 → 7.22.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 (81) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +2 -1
  5. package/dist/core/ask-origin.d.ts +1 -0
  6. package/dist/core/ask-origin.js +3 -2
  7. package/dist/core/auto-mode-rebuild.js +6 -2
  8. package/dist/core/auto-mode.d.ts +40 -18
  9. package/dist/core/auto-mode.js +63 -45
  10. package/dist/core/checkpoint-store.d.ts +8 -8
  11. package/dist/core/engine-notice.d.ts +49 -1
  12. package/dist/core/engine-notice.js +14 -1
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +4 -0
  15. package/dist/core/mcp.d.ts +65 -0
  16. package/dist/core/mcp.js +190 -41
  17. package/dist/core/memory-engine/consolidation.d.ts +5 -0
  18. package/dist/core/memory-engine/consolidation.js +7 -3
  19. package/dist/core/memory-engine/engine.d.ts +8 -0
  20. package/dist/core/memory-engine/engine.js +25 -19
  21. package/dist/core/memory-engine/layout.d.ts +47 -9
  22. package/dist/core/memory-engine/layout.js +157 -57
  23. package/dist/core/memory-engine/session-incarnation.d.ts +27 -0
  24. package/dist/core/memory-engine/session-incarnation.js +63 -0
  25. package/dist/core/parent-spec-seat.d.ts +15 -3
  26. package/dist/core/parent-spec-seat.js +5 -0
  27. package/dist/core/permission-rule-model.d.ts +1 -2
  28. package/dist/core/retention.d.ts +8 -1
  29. package/dist/core/retention.js +2 -0
  30. package/dist/core/runner/classifier-ladder.js +1 -0
  31. package/dist/core/runner/contracts.d.ts +11 -0
  32. package/dist/core/runner/mcp-redial.d.ts +109 -0
  33. package/dist/core/runner/mcp-redial.js +60 -0
  34. package/dist/core/runner/memory-consolidation-driver.d.ts +7 -4
  35. package/dist/core/runner/memory-consolidation-driver.js +1 -1
  36. package/dist/core/runner/prepare-caps-and-workflow.js +9 -12
  37. package/dist/core/runner/prepare-hands-readface.d.ts +3 -0
  38. package/dist/core/runner/prepare-hands-readface.js +13 -3
  39. package/dist/core/runner/prepare-memory.js +3 -3
  40. package/dist/core/runner/prepare-protocol-tools.d.ts +6 -1
  41. package/dist/core/runner/prepare-protocol-tools.js +20 -38
  42. package/dist/core/runner/prepare-task.js +3 -3
  43. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  44. package/dist/core/runner/prepare-wiring-manifest.js +2 -1
  45. package/dist/core/runner/run-identity-wiring.d.ts +3 -1
  46. package/dist/core/runner/run-identity-wiring.js +4 -1
  47. package/dist/core/runner/run-settle-and-teardown.d.ts +3 -1
  48. package/dist/core/runner/run-settle-and-teardown.js +6 -2
  49. package/dist/core/runner/run-terminal-adoption.d.ts +4 -1
  50. package/dist/core/runner/run-terminal-adoption.js +2 -6
  51. package/dist/core/runner/runtask.js +8 -5
  52. package/dist/core/runner/stream-lifecycle-verbs.d.ts +5 -1
  53. package/dist/core/runner/stream-lifecycle-verbs.js +23 -2
  54. package/dist/core/runner-deps.d.ts +17 -8
  55. package/dist/core/skill-listing-budget.d.ts +5 -4
  56. package/dist/core/task-result.d.ts +3 -2
  57. package/dist/core/task-spec.d.ts +2 -1
  58. package/dist/core/task-stream.d.ts +26 -0
  59. package/dist/core/tool-spec.d.ts +5 -1
  60. package/dist/core/trace.d.ts +5 -0
  61. package/dist/core/types.d.ts +2 -2
  62. package/dist/core/types.js +1 -1
  63. package/dist/core/wiring-manifest.d.ts +23 -0
  64. package/dist/core/wiring-manifest.js +3 -0
  65. package/dist/index.d.ts +4 -3
  66. package/dist/index.js +2 -2
  67. package/dist/orchestration/workflow-governance.js +1 -1
  68. package/dist/orchestration/workflow-primitives.d.ts +2 -2
  69. package/dist/tools/fs/bash-program-position.d.ts +26 -5
  70. package/dist/tools/fs/bash-program-position.js +63 -16
  71. package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -17
  72. package/dist/tools/fs/bash-readonly-classifier.js +134 -55
  73. package/dist/tools/fs/fs-bash.js +43 -18
  74. package/dist/tools/fs/index.d.ts +4 -2
  75. package/dist/tools/fs/read-deny.d.ts +25 -11
  76. package/dist/tools/fs/read-deny.js +5 -1
  77. package/dist/tools/fs/safety.d.ts +1 -4
  78. package/package.json +1 -1
  79. package/test/export-surface.snapshot.json +9 -1
  80. package/wire-changes/core@7.21.1.json +41 -0
  81. package/wire-changes/core@7.22.0.json +637 -0
@@ -1114,16 +1114,76 @@ export function writeIndexRevs(controlDir, revs) {
1114
1114
  export function scanFuseCount(controlDir, key) {
1115
1115
  return coerceFuse(readSidecarJson(controlDir, SCAN_FUSE_FILE))[key] ?? 0;
1116
1116
  }
1117
- export const SESSION_POLLUTION_DIR = "session-pollution";
1118
- function pollutionPath(controlDir, sessionId) {
1119
- return join(controlDir, SESSION_POLLUTION_DIR, `${encodeURIComponent(sessionId)}.json`);
1117
+ export function sessionRecordBindsIncarnation(recorded, asking) {
1118
+ return recorded === undefined || asking === undefined || recorded === asking;
1120
1119
  }
1121
- export function markSessionPolluted(controlDir, sessionId, reason, now, cause) {
1122
- const path = pollutionPath(controlDir, sessionId);
1120
+ function readIncarnationSeat(parsed) {
1121
+ return isIncarnationSeat(parsed.incarnation) && parsed.incarnation !== undefined ? parsed.incarnation : undefined;
1122
+ }
1123
+ function isIncarnationSeat(raw) {
1124
+ if (raw === undefined)
1125
+ return true;
1126
+ if (typeof raw !== "string" || raw === "")
1127
+ return false;
1128
+ try {
1129
+ encodeURIComponent(raw);
1130
+ return true;
1131
+ }
1132
+ catch {
1133
+ return false;
1134
+ }
1135
+ }
1136
+ export function usableSessionIncarnation(raw) {
1137
+ return isIncarnationSeat(raw) ? raw : undefined;
1138
+ }
1139
+ function sessionRecordPath(controlDir, dir, sessionId, incarnation) {
1140
+ const name = incarnation === undefined ? `${encodeURIComponent(sessionId)}.json` : `${encodeURIComponent(sessionId)}@${encodeURIComponent(incarnation)}.json`;
1141
+ return join(controlDir, dir, name);
1142
+ }
1143
+ function standingOwnerAt(path) {
1144
+ let raw;
1145
+ try {
1146
+ raw = readFileSync(path, "utf8");
1147
+ }
1148
+ catch (err) {
1149
+ if (err.code !== "ENOENT")
1150
+ return "unattributable";
1151
+ try {
1152
+ lstatSync(path);
1153
+ }
1154
+ catch {
1155
+ return "unattributable";
1156
+ }
1157
+ return { owner: undefined };
1158
+ }
1123
1159
  try {
1124
- ensureDirExists(dirname(path));
1125
- const record = { at: now(), reason, ...(cause !== undefined ? { cause } : {}) };
1126
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
1160
+ const parsed = JSON.parse(raw);
1161
+ if (parsed !== null && typeof parsed === "object")
1162
+ return { owner: readIncarnationSeat(parsed) };
1163
+ }
1164
+ catch {
1165
+ }
1166
+ return "unattributable";
1167
+ }
1168
+ function mintSessionRecordSlots(controlDir, dir, sessionId, incarnation, text) {
1169
+ const idSlot = sessionRecordPath(controlDir, dir, sessionId);
1170
+ try {
1171
+ ensureDirExists(dirname(idSlot));
1172
+ writeFileSync(idSlot, text, { encoding: "utf8", flag: "wx" });
1173
+ return "created";
1174
+ }
1175
+ catch (err) {
1176
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
1177
+ return "unpersisted";
1178
+ }
1179
+ const own = usableSessionIncarnation(incarnation);
1180
+ const standing = standingOwnerAt(idSlot);
1181
+ const vouches = standing !== "unattributable" && (standing.owner === undefined || standing.owner === own);
1182
+ const unnamed = vouches || own === undefined ? undefined : standingOwnerAt(sessionRecordPath(controlDir, dir, sessionId, ""));
1183
+ if (vouches || (unnamed !== undefined && unnamed !== "unattributable" && unnamed.owner === undefined))
1184
+ return "existed";
1185
+ try {
1186
+ writeFileSync(sessionRecordPath(controlDir, dir, sessionId, own ?? ""), text, { encoding: "utf8", flag: "wx" });
1127
1187
  return "created";
1128
1188
  }
1129
1189
  catch (err) {
@@ -1132,15 +1192,59 @@ export function markSessionPolluted(controlDir, sessionId, reason, now, cause) {
1132
1192
  return "unpersisted";
1133
1193
  }
1134
1194
  }
1135
- export function readSessionPollution(controlDir, sessionId) {
1136
- const path = pollutionPath(controlDir, sessionId);
1195
+ function bindOrSeparate(record, identity, asking, unknownSlot) {
1196
+ if (sessionRecordBindsIncarnation(record.incarnation, asking))
1197
+ return record;
1198
+ const unnamed = unknownSlot();
1199
+ if (unnamed !== undefined && sessionRecordBindsIncarnation(unnamed.incarnation, asking))
1200
+ return unnamed;
1201
+ if (record.incarnation !== undefined)
1202
+ identity?.onSeparated?.(record.incarnation);
1203
+ return undefined;
1204
+ }
1205
+ function sessionIdOfRecordName(name) {
1206
+ if (!name.endsWith(".json"))
1207
+ return undefined;
1208
+ const stem = name.slice(0, -".json".length);
1209
+ const at = stem.indexOf("@");
1210
+ try {
1211
+ return decodeURIComponent(at < 0 ? stem : stem.slice(0, at));
1212
+ }
1213
+ catch {
1214
+ return undefined;
1215
+ }
1216
+ }
1217
+ export const SESSION_POLLUTION_DIR = "session-pollution";
1218
+ function pollutionPath(controlDir, sessionId, incarnation) {
1219
+ return sessionRecordPath(controlDir, SESSION_POLLUTION_DIR, sessionId, incarnation);
1220
+ }
1221
+ export function markSessionPolluted(controlDir, sessionId, reason, now, cause, incarnation) {
1222
+ const record = { at: now(), reason, ...(cause !== undefined ? { cause } : {}), ...(incarnation !== undefined ? { incarnation } : {}) };
1223
+ return mintSessionRecordSlots(controlDir, SESSION_POLLUTION_DIR, sessionId, incarnation, `${JSON.stringify(record, null, 2)}\n`);
1224
+ }
1225
+ export function readSessionPollution(controlDir, sessionId, identity) {
1226
+ const asking = usableSessionIncarnation(identity?.incarnation);
1227
+ if (asking !== undefined) {
1228
+ const own = readPollutionAt(pollutionPath(controlDir, sessionId, asking));
1229
+ if (own !== undefined)
1230
+ return own;
1231
+ }
1232
+ const unnamedSlot = () => readPollutionAt(pollutionPath(controlDir, sessionId, ""));
1233
+ const standing = readPollutionAt(pollutionPath(controlDir, sessionId));
1234
+ return standing === undefined ? unnamedSlot() : bindOrSeparate(standing, identity, asking, unnamedSlot);
1235
+ }
1236
+ function readPollutionAt(path) {
1137
1237
  let raw;
1138
1238
  try {
1139
1239
  raw = readFileSync(path, "utf8");
1140
1240
  }
1141
1241
  catch {
1142
- if (!existsSync(path))
1242
+ try {
1243
+ lstatSync(path);
1244
+ }
1245
+ catch {
1143
1246
  return undefined;
1247
+ }
1144
1248
  return { at: 0, reason: "pollution marker present but unreadable (kept fail-closed)" };
1145
1249
  }
1146
1250
  try {
@@ -1150,8 +1254,9 @@ export function readSessionPollution(controlDir, sessionId) {
1150
1254
  const reason = parsed.reason;
1151
1255
  const cause = parsed.cause;
1152
1256
  const knownCause = typeof cause === "string" && MEMORY_ORIGIN_CAUSES.includes(cause) ? cause : undefined;
1257
+ const incarnation = readIncarnationSeat(parsed);
1153
1258
  if (typeof at === "number" && typeof reason === "string")
1154
- return { at, reason, ...(knownCause !== undefined ? { cause: knownCause } : {}) };
1259
+ return { at, reason, ...(knownCause !== undefined ? { cause: knownCause } : {}), ...(incarnation !== undefined ? { incarnation } : {}) };
1155
1260
  }
1156
1261
  }
1157
1262
  catch {
@@ -1171,16 +1276,10 @@ export function listSessionPollution(controlDir) {
1171
1276
  }
1172
1277
  const out = Object.create(null);
1173
1278
  for (const name of names.sort()) {
1174
- if (!name.endsWith(".json"))
1279
+ const sessionId = sessionIdOfRecordName(name);
1280
+ if (sessionId === undefined || Object.prototype.hasOwnProperty.call(out, sessionId))
1175
1281
  continue;
1176
- let sessionId;
1177
- try {
1178
- sessionId = decodeURIComponent(name.slice(0, -".json".length));
1179
- }
1180
- catch {
1181
- continue;
1182
- }
1183
- const rec = readSessionPollution(controlDir, sessionId);
1282
+ const rec = readPollutionAt(join(dir, name));
1184
1283
  if (rec !== undefined)
1185
1284
  out[sessionId] = rec;
1186
1285
  }
@@ -1201,25 +1300,25 @@ export function importSessionPollution(controlDir, sessionId, record) {
1201
1300
  return standing !== undefined && standing.at === record.at && standing.reason === record.reason ? "already" : "divergent";
1202
1301
  }
1203
1302
  export const SESSION_CAPTURE_OPTOUT_DIR = "session-capture-optout";
1204
- function captureOptOutPath(controlDir, sessionId) {
1205
- return join(controlDir, SESSION_CAPTURE_OPTOUT_DIR, `${encodeURIComponent(sessionId)}.json`);
1303
+ function captureOptOutPath(controlDir, sessionId, incarnation) {
1304
+ return sessionRecordPath(controlDir, SESSION_CAPTURE_OPTOUT_DIR, sessionId, incarnation);
1206
1305
  }
1207
- export function markSessionCaptureOptOut(controlDir, sessionId, reason, now) {
1208
- const path = captureOptOutPath(controlDir, sessionId);
1209
- try {
1210
- ensureDirExists(dirname(path));
1211
- const record = { at: now(), reason };
1212
- writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
1213
- return "created";
1214
- }
1215
- catch (err) {
1216
- if (err instanceof Error && "code" in err && err.code === "EEXIST")
1217
- return "existed";
1218
- return "unpersisted";
1306
+ export function markSessionCaptureOptOut(controlDir, sessionId, reason, now, incarnation) {
1307
+ const record = { at: now(), reason, ...(incarnation !== undefined ? { incarnation } : {}) };
1308
+ return mintSessionRecordSlots(controlDir, SESSION_CAPTURE_OPTOUT_DIR, sessionId, incarnation, `${JSON.stringify(record, null, 2)}\n`);
1309
+ }
1310
+ export function readSessionCaptureOptOut(controlDir, sessionId, identity) {
1311
+ const asking = usableSessionIncarnation(identity?.incarnation);
1312
+ if (asking !== undefined) {
1313
+ const own = readCaptureOptOutAt(captureOptOutPath(controlDir, sessionId, asking));
1314
+ if (own !== undefined)
1315
+ return own;
1219
1316
  }
1317
+ const unnamedSlot = () => readCaptureOptOutAt(captureOptOutPath(controlDir, sessionId, ""));
1318
+ const standing = readCaptureOptOutAt(captureOptOutPath(controlDir, sessionId));
1319
+ return standing === undefined ? unnamedSlot() : bindOrSeparate(standing, identity, asking, unnamedSlot);
1220
1320
  }
1221
- export function readSessionCaptureOptOut(controlDir, sessionId) {
1222
- const path = captureOptOutPath(controlDir, sessionId);
1321
+ function readCaptureOptOutAt(path) {
1223
1322
  let raw;
1224
1323
  try {
1225
1324
  raw = readFileSync(path, "utf8");
@@ -1249,8 +1348,9 @@ export function readSessionCaptureOptOut(controlDir, sessionId) {
1249
1348
  if (parsed !== null && typeof parsed === "object") {
1250
1349
  const at = parsed.at;
1251
1350
  const reason = parsed.reason;
1351
+ const incarnation = readIncarnationSeat(parsed);
1252
1352
  if (typeof at === "number" && typeof reason === "string")
1253
- return { at, reason };
1353
+ return { at, reason, ...(incarnation !== undefined ? { incarnation } : {}) };
1254
1354
  }
1255
1355
  }
1256
1356
  catch {
@@ -1270,16 +1370,10 @@ export function listSessionCaptureOptOut(controlDir) {
1270
1370
  }
1271
1371
  const out = Object.create(null);
1272
1372
  for (const name of names.sort()) {
1273
- if (!name.endsWith(".json"))
1373
+ const sessionId = sessionIdOfRecordName(name);
1374
+ if (sessionId === undefined || Object.prototype.hasOwnProperty.call(out, sessionId))
1274
1375
  continue;
1275
- let sessionId;
1276
- try {
1277
- sessionId = decodeURIComponent(name.slice(0, -".json".length));
1278
- }
1279
- catch {
1280
- continue;
1281
- }
1282
- const rec = readSessionCaptureOptOut(controlDir, sessionId);
1376
+ const rec = readCaptureOptOutAt(join(dir, name));
1283
1377
  if (rec !== undefined)
1284
1378
  out[sessionId] = rec;
1285
1379
  }
@@ -1287,8 +1381,8 @@ export function listSessionCaptureOptOut(controlDir) {
1287
1381
  }
1288
1382
  export function fileSessionCaptureRecordStore(controlDir) {
1289
1383
  return {
1290
- mark: (sessionId, record) => markSessionCaptureOptOut(controlDir, sessionId, record.reason, () => record.at),
1291
- read: (sessionId) => readSessionCaptureOptOut(controlDir, sessionId),
1384
+ mark: (sessionId, record) => markSessionCaptureOptOut(controlDir, sessionId, record.reason, () => record.at, record.incarnation),
1385
+ read: (sessionId, identity) => readSessionCaptureOptOut(controlDir, sessionId, identity),
1292
1386
  list: () => listSessionCaptureOptOut(controlDir),
1293
1387
  };
1294
1388
  }
@@ -1461,6 +1555,9 @@ function coerceLineage(raw) {
1461
1555
  }
1462
1556
  if (row.marked !== undefined && row.marked !== true)
1463
1557
  throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}] marked`);
1558
+ if (!isIncarnationSeat(row.incarnation)) {
1559
+ throw badShape(`committed[${JSON.stringify(entryId)}][${JSON.stringify(sid)}] incarnation`);
1560
+ }
1464
1561
  }
1465
1562
  }
1466
1563
  for (const [txnId, t] of Object.entries(r.pending)) {
@@ -1470,6 +1567,8 @@ function coerceLineage(raw) {
1470
1567
  }
1471
1568
  if (txn.kind !== undefined && txn.kind !== "latch-only")
1472
1569
  throw badShape(`pending[${JSON.stringify(txnId)}] kind`);
1570
+ if (!isIncarnationSeat(txn.incarnation))
1571
+ throw badShape(`pending[${JSON.stringify(txnId)}] incarnation`);
1473
1572
  for (const row of txn.rows) {
1474
1573
  const p = row;
1475
1574
  if (!p || typeof p !== "object" || typeof p.entryId !== "string" || typeof p.rev !== "string")
@@ -1498,9 +1597,9 @@ function coerceLineage(raw) {
1498
1597
  pending[txnId] = t;
1499
1598
  return { version: 1, committed, pending };
1500
1599
  }
1501
- export function stageLineagePending(controlDir, txnId, sessionId, rows, now) {
1600
+ export function stageLineagePending(controlDir, txnId, sessionId, rows, now, incarnation) {
1502
1601
  lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
1503
- rec.pending[txnId] = { sessionId, at: now(), rows: [...rows] };
1602
+ rec.pending[txnId] = { sessionId, at: now(), rows: [...rows], ...(incarnation !== undefined ? { incarnation } : {}) };
1504
1603
  return { next: rec, result: undefined };
1505
1604
  });
1506
1605
  }
@@ -1536,7 +1635,7 @@ export function promoteLineagePending(controlDir, txnId, now) {
1536
1635
  if (!applied.has(row.entryId))
1537
1636
  continue;
1538
1637
  const sessions = (rec.committed[row.entryId] ??= Object.create(null));
1539
- sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
1638
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
1540
1639
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
1541
1640
  }
1542
1641
  delete rec.pending[txnId];
@@ -1562,7 +1661,7 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
1562
1661
  if (applied !== undefined && !applied.has(row.entryId))
1563
1662
  continue;
1564
1663
  const sessions = (rec.committed[row.entryId] ??= Object.create(null));
1565
- sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
1664
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
1566
1665
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
1567
1666
  }
1568
1667
  delete rec.pending[txnId];
@@ -1586,7 +1685,7 @@ export function reconcileLineage(controlDir, now) {
1586
1685
  continue;
1587
1686
  at ??= now();
1588
1687
  const sessions = (rec.committed[row.entryId] ??= Object.create(null));
1589
- sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}) };
1688
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
1590
1689
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
1591
1690
  }
1592
1691
  delete rec.pending[txnId];
@@ -1663,13 +1762,14 @@ export function lineageLatchedIds(controlDir) {
1663
1762
  out.add(row.entryId);
1664
1763
  return out;
1665
1764
  }
1666
- export function lineageContributionsOfSession(controlDir, sessionId) {
1765
+ export function lineageContributionsOfSession(controlDir, sessionId, asking) {
1667
1766
  const rec = coerceLineage(readStrictSidecar(controlDir, LINEAGE_FILE, "memory lineage ledger"));
1668
1767
  const out = [];
1669
1768
  for (const [entryId, sessions] of Object.entries(rec.committed)) {
1670
1769
  const c = Object.prototype.hasOwnProperty.call(sessions, sessionId) ? sessions[sessionId] : undefined;
1671
- if (c !== undefined)
1672
- out.push({ entryId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
1770
+ if (c === undefined || !sessionRecordBindsIncarnation(c.incarnation, asking))
1771
+ continue;
1772
+ out.push({ entryId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
1673
1773
  }
1674
1774
  return out;
1675
1775
  }
@@ -0,0 +1,27 @@
1
+ import { type MemoryPartitionIncidentSink, type SessionIdentityRead } from "./layout.js";
2
+ /** The current occupant's token, and the identity seat a per-session record read is made under. */
3
+ export interface SessionIncarnationSeat {
4
+ incarnationOf(sessionId: string): string | undefined;
5
+ identityRead(sessionId: string): SessionIdentityRead;
6
+ }
7
+ /** Build the seat over a host resolver (absent ⇒ nothing stamped, nothing asked, every record binds
8
+ * by id — the pre-incarnation answer). Both disclosures are DEDUPED on the axis their fact belongs to: the resolver
9
+ * fault once per engine (a wiring fact), a separation once per session id (a fact about that id). */
10
+ export declare function createSessionIncarnationSeat(input: {
11
+ resolve: ((session: {
12
+ sessionId: string;
13
+ }) => string | undefined) | undefined;
14
+ onIncident: MemoryPartitionIncidentSink | undefined;
15
+ }): SessionIncarnationSeat;
16
+ /** Remember an in-process record under its session id, FIRST-WINS PER IDENTITY: this map is the
17
+ * durable half's fallback exactly when the durable write failed, so keeping only the first
18
+ * occupant's row would leave the next one unprotected on the one path it exists to cover. */
19
+ export declare function rememberForIdentity<T extends {
20
+ incarnation?: string;
21
+ }>(map: Map<string, T[]>, sessionId: string, record: T): void;
22
+ /** The remembered record that BINDS `asking` ({@link sessionRecordBindsIncarnation}), if any. */
23
+ export declare function recalledForIdentity<T extends {
24
+ incarnation?: string;
25
+ }>(map: Map<string, T[]>, sessionId: string, asking: string | undefined): T | undefined;
26
+ /** The first record remembered for `sessionId` by ANY occupant — for the id-blind faces. */
27
+ export declare function recalledForAnyIdentity<T>(map: Map<string, T[]>, sessionId: string): T | undefined;
@@ -0,0 +1,63 @@
1
+ import { sessionRecordBindsIncarnation, usableSessionIncarnation } from "./layout.js";
2
+ export function createSessionIncarnationSeat(input) {
3
+ const { resolve, onIncident } = input;
4
+ let faultAnnounced = false;
5
+ const separationAnnounced = new Set();
6
+ const disclose = (code, message) => {
7
+ if (onIncident === undefined)
8
+ return;
9
+ try {
10
+ const e = new Error(message);
11
+ e.code = code;
12
+ onIncident(e);
13
+ }
14
+ catch {
15
+ }
16
+ };
17
+ const incarnationOf = (sessionId) => {
18
+ if (resolve === undefined)
19
+ return undefined;
20
+ try {
21
+ const answer = resolve({ sessionId });
22
+ return usableSessionIncarnation(answer);
23
+ }
24
+ catch (err) {
25
+ if (!faultAnnounced) {
26
+ faultAnnounced = true;
27
+ disclose("memory.session_incarnation_unreadable", `the session incarnation resolver threw (${err instanceof Error ? err.message : String(err)}) — this run cannot tell one occupant of a session id from the next, so every durable session record binds by id alone (the conservative reading, and the behavior of a deployment that wires no resolver)`);
28
+ }
29
+ return undefined;
30
+ }
31
+ };
32
+ return {
33
+ incarnationOf,
34
+ identityRead: (sessionId) => {
35
+ const incarnation = incarnationOf(sessionId);
36
+ return {
37
+ ...(incarnation !== undefined ? { incarnation } : {}),
38
+ onSeparated: (recorded) => {
39
+ if (separationAnnounced.has(sessionId))
40
+ return;
41
+ separationAnnounced.add(sessionId);
42
+ disclose("memory.session_incarnation_mismatch", `a durable record stands for session ${sessionId} but it was minted by a DIFFERENT occupant of that id (recorded incarnation ${JSON.stringify(recorded)}, this run ${JSON.stringify(incarnation)}) — it does not bind this run and the record is kept where it is. Either the id was recycled after its previous session was deleted, or the deployment's incarnation resolver is not stable for the life of one session`);
43
+ },
44
+ };
45
+ },
46
+ };
47
+ }
48
+ export function rememberForIdentity(map, sessionId, record) {
49
+ const rows = map.get(sessionId);
50
+ if (rows === undefined) {
51
+ map.set(sessionId, [record]);
52
+ return;
53
+ }
54
+ if (rows.some((r) => r.incarnation === record.incarnation))
55
+ return;
56
+ rows.push(record);
57
+ }
58
+ export function recalledForIdentity(map, sessionId, asking) {
59
+ return map.get(sessionId)?.find((r) => sessionRecordBindsIncarnation(r.incarnation, asking));
60
+ }
61
+ export function recalledForAnyIdentity(map, sessionId) {
62
+ return map.get(sessionId)?.[0];
63
+ }
@@ -12,7 +12,7 @@ import type { OnQuestion } from "./ask-question.js";
12
12
  import type { ToolExecuteContext } from "./tool-spec.js";
13
13
  import type { TaskSpec } from "./types.js";
14
14
  /** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
15
- export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable">;
15
+ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory">;
16
16
  /**
17
17
  * The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
18
18
  * frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
@@ -26,6 +26,8 @@ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "cli
26
26
  * · `envFacts` — the sandbox facts (profile / egress / scratchpad).
27
27
  * · `memoryPersistenceCapable` — the deployment's DECLARED persistence statement: `false` is a REMOVAL (floor),
28
28
  * `true` a default a child's own declaration may narrow.
29
+ * · `memoryOff` — the request-level "memory is off" FACT (`spec.memory.enabled === false`): a REMOVAL
30
+ * (floor) that REWRITES the child's own memory declaration instead of travelling as a value.
29
31
  */
30
32
  export interface ParentSpecSeat {
31
33
  readonly principal?: string;
@@ -36,14 +38,15 @@ export interface ParentSpecSeat {
36
38
  readonly additionalReadDirectories?: readonly string[];
37
39
  readonly envFacts?: TaskSpec["envFacts"];
38
40
  readonly memoryPersistenceCapable?: boolean;
41
+ readonly memoryOff?: true;
39
42
  }
40
43
  /** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
41
44
  * the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
42
45
  export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQuestion">, frozenOnQuestion: OnQuestion | undefined): ParentSpecSeat;
43
46
  /**
44
47
  * Apply the seat to a child spec — ONE law for every member, both lanes:
45
- * a value the child spec already carries WINS; the seat FILLS an unset one; the two REMOVAL values
46
- * (`oneShot: true`, `memoryPersistenceCapable: false`) land UNCONDITIONALLY.
48
+ * a value the child spec already carries WINS; the seat FILLS an unset one; the three REMOVAL values
49
+ * (`oneShot: true`, `memoryPersistenceCapable: false`, `memoryOff: true`) land UNCONDITIONALLY.
47
50
  * "Already carries" is the child's own authorship — a deployment baseline, a trusted script, an agent definition's
48
51
  * fold (the Task lane folds the definition's `memoryPersistenceCapable` before applying; the workflow lane applies at
49
52
  * the launch site AFTER the agentType fold) — so a definition may narrow a parent `true` to `false`, a baseline-pinned
@@ -52,6 +55,15 @@ export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQue
52
55
  * No seat (a tool running outside a Runner task) ⇒ the child spec is returned as is.
53
56
  */
54
57
  export declare function applyParentSpecSeat<S extends ParentSpecSeatFields>(child: S, seat: ParentSpecSeat | undefined): S;
58
+ /** The ONE way the memory-off floor is written onto a spec — the seat's apply above and the resume lane's
59
+ * clamp fold both call it: keep every field the spec authored (scopes, write scope, content-safety knobs)
60
+ * and state `enabled: false` over them. Returning a value rather than deleting the key IS the mechanism:
61
+ * the next seat mint reads `spec.memory.enabled === false`, so a spec whose `memory` was removed would end
62
+ * the floor one level down. A spec that declared nothing gets the bare `{ enabled: false }`, which the
63
+ * normalizer reads as "no usable scopes" — identical to the absent declaration it replaces. */
64
+ export declare function memoryTurnedOff(memory: TaskSpec["memory"]): NonNullable<TaskSpec["memory"]> & {
65
+ enabled: false;
66
+ };
55
67
  /** The ctx keys that restate a seat member as THIS run's own fact (tools read them for scope / receipts / clamps). */
56
68
  declare const SEAT_FACTS_ON_CTX: readonly ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
57
69
  /**
@@ -8,6 +8,7 @@ export function parentSpecSeatOf(spec, frozenOnQuestion) {
8
8
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
9
9
  ...(spec.envFacts !== undefined ? { envFacts: Object.freeze({ ...spec.envFacts }) } : {}),
10
10
  ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
11
+ ...(spec.memory?.enabled === false ? { memoryOff: true } : {}),
11
12
  });
12
13
  }
13
14
  export function applyParentSpecSeat(child, seat) {
@@ -24,8 +25,12 @@ export function applyParentSpecSeat(child, seat) {
24
25
  ...fill("envFacts", (seat.envFacts !== undefined ? { ...seat.envFacts } : undefined)),
25
26
  ...(seat.oneShot === true ? { oneShot: true } : {}),
26
27
  ...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
28
+ ...(seat.memoryOff === true ? { memory: memoryTurnedOff(child.memory) } : {}),
27
29
  };
28
30
  }
31
+ export function memoryTurnedOff(memory) {
32
+ return { ...(memory ?? {}), enabled: false };
33
+ }
29
34
  const SEAT_FACTS_ON_CTX = ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
30
35
  export function parentSpecSeatOfCtx(ctx) {
31
36
  const seat = ctx.parentSpecSeat;
@@ -1195,8 +1195,7 @@ export declare function suggestRulesForCommand(command: string, ctx?: {
1195
1195
  * design/382 §2.4-6 — the deployment's sensitive-read negative control, SAME-SOURCE with its
1196
1196
  * compiled read-deny judge (`ReadDenyMatcher.matchPath !== null` is the canonical wiring): the
1197
1197
  * engine never PROPOSES a directory authorization for a directory this judge names. Absent ⇒
1198
- * the built-in default deny table stands in (refusal-only; a deployment with a wired judge
1199
- * passes its own so the two faces cannot disagree). Explicit-consent doors (hand-written,
1198
+ * {@link defaultDeniesDirectoryRead} stands in (refusal-only; every hands leg passes its OWN judge). Explicit-consent doors (hand-written,
1200
1199
  * import) are deliberately NOT behind this — they have their own preview and validation.
1201
1200
  */
1202
1201
  readonly deniesDirectoryRead?: (directory: string) => boolean;
@@ -47,7 +47,7 @@ export interface ManagedRetentionCapability {
47
47
  /** Lineage-aware transcript/session deletion: a session tree is deleted only when no live
48
48
  * reference (fork child, background child, workflow run, pending checkpoint) still points into
49
49
  * it — references are re-checked inside the deletion transaction, never assumed from a prior
50
- * scan. */
50
+ * scan; {@link RETENTION_CASCADE_EXEMPT} is never deleted with one. */
51
51
  deleteExpiredSessions(input: {
52
52
  domain: string;
53
53
  cutoffMs: number;
@@ -66,6 +66,13 @@ export interface ManagedRetentionCapability {
66
66
  * own occurrence id, operation, and timestamp inside the deleting transaction. Adding those here
67
67
  * would ask a return value to know its own call context.
68
68
  */
69
+ /** @contract retention.cascade_domains — a managed-retention cascade deletes checkpoint rows,
70
+ * session trees (transcripts and lineage) and orphan tool results; nothing else is session data. */
71
+ export declare const RETENTION_CASCADE_DOMAINS: readonly ["checkpoints", "session-trees", "orphan-tool-results"];
72
+ /** @contract retention.governance_records_outlive_the_purge — the pollution marker dir, the capture
73
+ * opt-out record dir and the lineage ledger are governance FACTS about content that exists, not
74
+ * session data: deleting them makes a one-way privacy record revocable. */
75
+ export declare const RETENTION_CASCADE_EXEMPT: readonly ["session-pollution", "session-capture-optout", "lineage.json"];
69
76
  export interface RetentionReceipt {
70
77
  domain: string;
71
78
  /** Rows destroyed by THIS call (idempotent retries count already-gone rows as 0, not errors). */
@@ -1,3 +1,5 @@
1
+ export const RETENTION_CASCADE_DOMAINS = ["checkpoints", "session-trees", "orphan-tool-results"];
2
+ export const RETENTION_CASCADE_EXEMPT = ["session-pollution", "session-capture-optout", "lineage.json"];
1
3
  function codedError(code, message) {
2
4
  const e = new Error(message);
3
5
  e.code = code;
@@ -217,6 +217,7 @@ export function classifierRoundObserver(args) {
217
217
  ms: info.ms,
218
218
  verdict: info.verdict,
219
219
  ...(info.cause !== undefined ? { cause: info.cause } : {}),
220
+ ...(info.notAsked !== undefined ? { notAsked: info.notAsked } : {}),
220
221
  ts: Date.now(),
221
222
  }));
222
223
  announceFallback(info.classifier);
@@ -248,6 +248,11 @@ export interface Prepared {
248
248
  thinking?: ThinkingLevel;
249
249
  compModel?: Model;
250
250
  mcp: MaterializedMcp;
251
+ /** The run handle's `reconnectMcpServer` implementation — {@link MaterializedMcp.reconnect} plus the
252
+ * live-roster application and the operator disclosure, which are runner concerns and not the MCP
253
+ * handle's. Present only when this leg DECLARED MCP servers; the stream verb's seat stays unbound
254
+ * otherwise and it refuses typed. */
255
+ mcpRedial?: (server: string) => Promise<import("../mcp.js").McpReconnectResult>;
251
256
  /** Present only when `spec.a2a` declared peers — the run loop owes it the same end-of-task dispose
252
257
  * it owes {@link mcp} (no sockets are held, but a disposed mount is what makes a post-task tool
253
258
  * call refuse loudly instead of dialing a peer on a dead task's behalf). */
@@ -2659,6 +2664,12 @@ export interface CaptureOptOutRef {
2659
2664
  outcome: "created" | "existed";
2660
2665
  }>;
2661
2666
  }
2667
+ /** The MCP re-dial verb's binding; bound only when the run declared MCP servers — a leg with
2668
+ * none has nothing to re-dial, and the stream verb then refuses TYPED rather than answering about a
2669
+ * materialization that was never made. Same shape and same law as {@link CaptureOptOutRef}. */
2670
+ export interface McpRedialRef {
2671
+ redial?: (server: string) => Promise<import("../mcp.js").McpReconnectResult>;
2672
+ }
2662
2673
  /**
2663
2674
  * The stream's LIVE state as the verb lanes read it — a view over the driver's own locals (getters, and one
2664
2675
  * setter), never a copy: `resultValue` is written by the run body's `setResult` callback and by the failure