@sema-agent/core 7.24.1 → 7.24.3

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 (41) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agents/subagent.js +4 -1
  3. package/dist/brain/anthropic.js +2 -2
  4. package/dist/brain/errors.d.ts +6 -4
  5. package/dist/brain/open-responses.js +2 -2
  6. package/dist/brain/openai.js +2 -2
  7. package/dist/brain/route-adjudicator.d.ts +41 -13
  8. package/dist/brain/route-adjudicator.js +97 -18
  9. package/dist/core/checkpoint-store.d.ts +127 -102
  10. package/dist/core/checkpoint-store.js +33 -9
  11. package/dist/core/mcp-failure.d.ts +73 -0
  12. package/dist/core/mcp-failure.js +21 -0
  13. package/dist/core/mcp.d.ts +18 -1
  14. package/dist/core/mcp.js +33 -12
  15. package/dist/core/memory-engine/distiller.js +3 -3
  16. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  17. package/dist/core/runner/prepare-park-ask.js +2 -2
  18. package/dist/core/runner/prepare-wiring-manifest.d.ts +3 -2
  19. package/dist/core/runner/prepare-wiring-manifest.js +3 -1
  20. package/dist/core/runner/resume-checkpoint-screen.js +11 -10
  21. package/dist/core/runner/resume-policy-outcome.js +2 -2
  22. package/dist/core/wiring-manifest.d.ts +20 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +2 -2
  25. package/dist/prompt-assembly/packs/sema-default.js +15 -0
  26. package/dist/prompts/default.d.ts +1 -1
  27. package/dist/prompts/default.js +4 -4
  28. package/dist/tools/fs/bash-program-position.d.ts +5 -0
  29. package/dist/tools/fs/bash-program-position.js +1 -1
  30. package/dist/tools/fs/bash-readonly-classifier.d.ts +476 -1
  31. package/dist/tools/fs/bash-readonly-classifier.js +98 -34
  32. package/dist/tools/fs/fs-bash.js +2 -325
  33. package/dist/tools/fs/shell-read-establish.d.ts +127 -0
  34. package/dist/tools/fs/shell-read-establish.js +326 -0
  35. package/dist/tools/fs/shell-read-plan.d.ts +113 -0
  36. package/dist/tools/fs/shell-read-plan.js +700 -0
  37. package/package.json +1 -1
  38. package/test/export-surface.snapshot.json +9 -1
  39. package/wire-changes/core@7.24.1.json +3 -17
  40. package/wire-changes/core@7.24.2.json +81 -0
  41. package/wire-changes/core@7.24.3.json +244 -0
@@ -1288,110 +1288,135 @@ export type SerializedCheckpointState = {
1288
1288
  [K in keyof Required<CheckpointState>]: CheckpointState[K];
1289
1289
  };
1290
1290
  /**
1291
- * The BASELINE {@link Checkpoint} schema version (design/49 §3, code-ready council round-2): the
1292
- * shape a plain human-approval suspend carried before any feature raised the format. It is NOT the highest
1293
- * a resume accepts — that is {@link MAX_SUPPORTED_CHECKPOINT_VERSION}, which the per-feature stamps
1294
- * (RESOURCE / BINDING / TOKEN / ORG_ADMISSION) have each raised on their own, deliberately without moving
1295
- * this constant. Resume stays forward-compatible by `<=` against MAX, never `===`. Every live mint site
1296
- * stamps one of the feature versions, so nothing in-tree produces this value today; it is kept as the
1297
- * floor of the version ladder and as the reading for a row written before any feature raised the format.
1298
- */
1299
- export declare const CURRENT_CHECKPOINT_VERSION = 1;
1300
- /** design/74 R3-B: the schema version a `resource_limit` suspend stamps (v2 — it adds `resourceLedger` and a
1301
- * `resource_limit` `pendingAction`/`gate` an old worker can't handle). An old worker (`MAX_SUPPORTED`=1)
1302
- * rejects it pre-CAS (stays `pending`, retryable on a new worker). */
1303
- export declare const RESOURCE_CHECKPOINT_VERSION = 2;
1304
- /**
1305
- * design/80 D-1 (version-skew downgrade fix): the schema version a **binding-bearing** human/irreversible_ask
1306
- * `tool_approval` suspend stamps. These checkpoints carry the decision-action binding (`boundCallId` +
1307
- * `boundInputHash`) whose enforcement lives ENTIRELY in the resuming worker's resume path. A pre-D-1 worker
1308
- * (released 1.100.0: `MAX_SUPPORTED`=2, and its resume code has ZERO binding logic) would otherwise resume a
1309
- * D-1-minted v1 checkpoint and execute the pending tool with NO decision-action verification — the exact
1310
- * "approve vendor-A $5 → execute vendor-B $5000" circumvent the binding exists to prevent (council BLOCKER #1).
1311
- * Stamping these at **v3 (> the old worker's MAX of 2)** forces a pre-D-1 worker to reject them PRE-CAS
1312
- * (`unsupported_version`, stays `pending`, retried on a binding-enforcing worker) instead of silently voiding
1313
- * the binding. A resource_limit suspend keeps stamping v2; a pre-binding (legacy 1.100.0) checkpoint is v1.
1314
- */
1315
- export declare const BINDING_CHECKPOINT_VERSION = 3;
1316
- /**
1317
- * design/164 (the BINDING_CHECKPOINT_VERSION precedent replayed a third time): the schema version a suspend
1318
- * stamps when its ledger CARRIES a cross-slice TOKEN allocation (`resourceLedger.totalTokens` set). The
1319
- * enforcement — window clamp against the remaining allocation, exhausted-resume fail-fast — lives ENTIRELY
1320
- * in the resuming worker, so a pre-164 worker (MAX_SUPPORTED=4) would accept the row, ignore
1321
- * `spentTokens` as a bound, and grant a fresh full window. Stamping v5 forces it to reject PRE-CAS
1322
- * (`unsupported_version`, stays `pending`, retried on an enforcing worker). A ledger with NO token total
1323
- * keeps the historic stamps (v2 resource / v3 binding) — deployments not using it see zero version movement.
1291
+ * The checkpoint FEATURE TABLE — one row per feature that ever raised the row format. A row carries the
1292
+ * feature's schema version, the persisted mark by which a reader tells the feature was written on a row
1293
+ * (`recordedOn`, the READ face), and what a mint owes when the feature is present (`writes`, the WRITE
1294
+ * face — the two faces are deliberately separate columns). Minting is derived from the rows
1295
+ * ({@link checkpointVersionFor}: the highest version among the features present), the per-feature
1296
+ * constants below ARE the rows' versions, and the resume screen reads the rows — so adding a feature is
1297
+ * one row here plus an explicit answer at each mint site (the presence record's keys are mandatory).
1324
1298
  *
1325
- * v4 (the retired wall-clock total) is deliberately NOT reused: this worker still READS v4 rows, and one
1326
- * whose ledger carries the retired axis is refused with its own typed code (see
1327
- * {@link CheckpointError} `checkpoint.walltime_axis_retired`) instead of silently resuming under a bound
1328
- * nobody enforces any more.
1329
- */
1330
- export declare const TOKEN_CHECKPOINT_VERSION = 5;
1331
- /**
1332
- * Org-memory admission state (falsification-style round 2, 2026-08-05 — the BINDING_CHECKPOINT_VERSION
1333
- * precedent replayed a fourth time): the schema version a suspend stamps when its state carries
1334
- * org-admission freeze fields (`inheritedGate.admittedOrgScopes` / `ownAdmittedOrgScopes` /
1335
- * `ownAdmittedOrgWriteScope` — the adjudicated-empty record included). The enforcement — the
1336
- * session/delegation freeze on resume, the tenant-admission door itself — lives ENTIRELY in the
1337
- * resuming worker, so a pre-admission worker (MAX_SUPPORTED=5) would accept the row, ignore the
1338
- * freeze fields, and remount the raw org scopes with no admission at all. Stamping v6 forces it to
1339
- * reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A
1340
- * checkpoint with NO org-admission state keeps the historic stamps — deployments not using org
1341
- * memory see zero version movement.
1342
- */
1343
- export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
1344
- /**
1345
- * F-012 (2026-08-09 — the ORG_ADMISSION_CHECKPOINT_VERSION precedent replayed a fifth time
1346
- * falsification-style finding): the schema version a suspend stamps when its state carries the F-012 durable
1347
- * enforcement fields — `inheritedGate.constraintChain`/`constraintDigest` (the frozen ancestor
1348
- * projection chain the resume-edit re-adjudication executes) or `delegationProvenance` (the monotonic
1349
- * provenance aggregate a resume re-seeds). Both enforcements live ENTIRELY in the resuming worker: a
1350
- * pre-F-012 worker (MAX_SUPPORTED=6) would accept the row, ignore constraintChain (so an approver-edited
1351
- * resume executes WITHOUT the frozen ancestor deny), and restart the aggregate clean (false-clean after
1352
- * pre-suspend external activity). Stamping v7 forces such a worker to reject PRE-CAS
1353
- * (`unsupported_version`, stays `pending`, retried on an enforcing worker). A checkpoint with NO F-012
1354
- * state keeps the historic stamps — deployments not delegating see zero version movement.
1355
- */
1356
- export declare const F012_CHECKPOINT_VERSION = 7;
1357
- /**
1358
- * #130/#120 (2026-08-10 — the F012_CHECKPOINT_VERSION precedent replayed a sixth time): the schema
1359
- * version a suspend stamps when its gate carries {@link RealApprovalGateBit}. The enforcement lives
1360
- * ENTIRELY in the resuming worker (the no-org-wiring refusal, the org_unavailable resume-belt
1361
- * distinction): a pre-131 worker (MAX_SUPPORTED=7) would accept the row, ignore `realApproval`, and
1362
- * either redeem a governed row with no org wiring at all (#120's exact hole, replayed through version
1363
- * skew) or burn the approval on the unavailable belt the bit exists to soften. Stamping v8 forces it
1364
- * to reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A gate
1365
- * with NO `realApproval` keeps the historic stamps. NOTE (erratum 2026-08-10, downstream-measured):
1366
- * "zero version movement for ungoverned deployments" is TOO WIDE — the two always-mounted integrity
1367
- * policies (transcript-integrity, unverifiable-delete) mint `requiresRealApproval` asks, so a park on
1368
- * one of THOSE stamps v8 with `origin:"policy"` on ANY durable deployment, org-governed or not. What
1369
- * stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
1370
- */
1371
- export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
1372
- /**
1373
- * design/199 (2026-08-13 — the precedent replayed a seventh time): the schema version a suspend
1374
- * stamps when its state carries the READ-FACE section ({@link CheckpointState.readFace} — the
1375
- * resolved open face and/or deny-set additions). The enforcement lives ENTIRELY in the resuming
1376
- * worker (the stricter-wins face fold + the deny-entry union at prepare): a pre-199 worker
1377
- * (MAX_SUPPORTED=8) would accept the row, ignore the section, and resume the leg under its own
1378
- * defaults — silently DROPPING the frozen deny additions (a widening), and re-deriving the face
1379
- * from live config alone. Stamping v9 forces it to reject PRE-CAS (`unsupported_version`, stays
1380
- * `pending`, retried on an enforcing worker). A row whose task ran the default posture (face roots,
1381
- * no additions) carries NO section and keeps the historic stamps — such a resume on ANY worker
1382
- * folds to roots + built-ins, which is exactly the semantics it was cut under.
1299
+ * Because the stamp is a maximum, the version records only the HIGHEST feature on a row; whether a lower
1300
+ * feature is present is said by that feature's own mark, never by the number. A resume rule keyed on the
1301
+ * version alone is therefore exact to the version at which its feature was the top: `realApproval` is
1302
+ * version-recorded at v8 and marker-recorded (`readFace.realApproval`) from v9 on. The same law binds the
1303
+ * NEXT feature: once a row can be stamped above v9, "v9+ ⇒ read-face section present" no longer follows
1304
+ * from the stamp, and the face needs a mark of its own (or the row a per-feature presence record).
1305
+ *
1306
+ * v4 is a GAP, deliberately: the retired cross-slice wall-clock allocation. This worker still reads v4
1307
+ * rows; a ledger carrying the retired axis is refused by FIELD (`walltime_axis_retired`) at any version.
1383
1308
  */
1384
- export declare const FACE_CHECKPOINT_VERSION = 9;
1385
- /** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
1386
- * {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
1387
- * Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
1388
- * (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
1389
- * wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
1390
- * state), v7 (F-012 constraint-chain / delegation-provenance enforcement state), v8 (a
1391
- * non-budgetable `realApproval` gate bit with its org-origin resume semantics), v9 (the
1392
- * design/199 read-face section: resolved face + deny-set additions, folded stricter-wins on
1393
- * resume). */
1394
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
1309
+ export declare const CHECKPOINT_FEATURES: {
1310
+ /** The baseline shape: a plain human-approval suspend before any feature raised the format. No live
1311
+ * mint stamps it today; it is the floor of the ladder and the reading of a row written before any feature. */
1312
+ readonly baseline: {
1313
+ readonly version: 1;
1314
+ readonly introduced: "design/49";
1315
+ readonly recordedOn: (_cp: Checkpoint) => boolean;
1316
+ readonly writes: "the row itself";
1317
+ };
1318
+ /** A `resource_limit` suspend: `resourceLedger` plus a `resource_limit` gate/pendingAction an older worker
1319
+ * cannot handle (it rejects pre-CAS; the row stays `pending`, retryable on a newer worker). */
1320
+ readonly resource: {
1321
+ readonly version: 2;
1322
+ readonly introduced: "design/74";
1323
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1324
+ readonly writes: "resourceLedger; gate/pendingAction kind resource_limit";
1325
+ };
1326
+ /** A HUMAN-DECISION park (tool approval or plan_review) of the binding era: the decision-action binding
1327
+ * (`boundCallId` + `boundInputHash` on a tool_approval) is enforced ENTIRELY by the resuming worker, so a
1328
+ * pre-binding worker must reject the row rather than execute the pending tool unverified. Like `resource`,
1329
+ * this row is a park-kind FLOOR: a plan_review park carries it with no binding (it was introduced above it);
1330
+ * whether a tool_approval row carries its hash is the resume binding rung's own conjunction. */
1331
+ readonly binding: {
1332
+ readonly version: 3;
1333
+ readonly introduced: "design/80";
1334
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1335
+ readonly writes: "pendingAction.boundCallId + boundInputHash on a tool_approval; a plan_review carries the stamp only";
1336
+ };
1337
+ /** A ledger carrying a cross-slice TOKEN allocation: the window clamp and the exhausted-resume fail-fast
1338
+ * live in the resuming worker. A ledger with no token total keeps the historic stamps. */
1339
+ readonly token: {
1340
+ readonly version: 5;
1341
+ readonly introduced: "design/164";
1342
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1343
+ readonly writes: "resourceLedger.totalTokens";
1344
+ };
1345
+ /** Org-memory admission freeze state (the adjudicated-empty record included): the session/delegation
1346
+ * freeze on resume and the tenant-admission door live in the resuming worker. */
1347
+ readonly orgAdmission: {
1348
+ readonly version: 6;
1349
+ readonly introduced: "org-admission";
1350
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1351
+ readonly writes: "inheritedGate.admittedOrgScopes / ownAdmittedOrgScopes / ownAdmittedOrgWriteScope / orgAdmissionGoverned";
1352
+ };
1353
+ /** The F-012 durable enforcement fields: the frozen ancestor projection chain + its digest (one write, the
1354
+ * resume-edit re-adjudication executes it) and/or the delegation-provenance aggregate a resume re-seeds. */
1355
+ readonly f012: {
1356
+ readonly version: 7;
1357
+ readonly introduced: "F-012";
1358
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1359
+ readonly writes: "inheritedGate.constraintChain + constraintDigest together; delegationProvenance";
1360
+ };
1361
+ /** A gate carrying {@link RealApprovalGateBit}: the no-org-wiring refusal and the org_unavailable resume
1362
+ * belt live in the resuming worker. Minted at ONE site (the approval park); the two always-mounted
1363
+ * integrity policies mint it on any durable deployment, org-governed or not. */
1364
+ readonly realApproval: {
1365
+ readonly version: 8;
1366
+ readonly introduced: "#130/#120";
1367
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1368
+ readonly writes: "gate.realApproval (a well-formed bit) + pendingAction.requiresRealApproval; from v9: readFace.realApproval = true beside it";
1369
+ };
1370
+ /** The READ-face section ({@link CheckpointState.readFace}): resolved face + normalized deny-set additions,
1371
+ * written only for a non-default posture; the stricter-wins fold lives in the resuming worker. Carries the
1372
+ * redundant governance marker for `realApproval` (see the table note above). Absent ⇒ default posture. */
1373
+ readonly face: {
1374
+ readonly version: 9;
1375
+ readonly introduced: "design/199";
1376
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1377
+ readonly writes: "state.readFace { face, denyEntries? } for a non-default posture; realApproval: true only at the mint whose gate carries the bit (a hands-less re-park never carries the seed's marker forward)";
1378
+ };
1379
+ };
1380
+ /** One row of {@link CHECKPOINT_FEATURES}. */
1381
+ export interface CheckpointFeatureRow {
1382
+ readonly version: number;
1383
+ /** Where the feature came from — a design or ticket tag, documentation only. */
1384
+ readonly introduced: string;
1385
+ /** READ face: true iff the row carries the feature's persisted mark. */
1386
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1387
+ /** WRITE face: what a mint must write when the feature is present (prose; the mint sites are the code). */
1388
+ readonly writes: string;
1389
+ }
1390
+ /** The feature words — the row keys of {@link CHECKPOINT_FEATURES}. */
1391
+ export type CheckpointFeature = keyof typeof CHECKPOINT_FEATURES;
1392
+ /** The BASELINE {@link Checkpoint} schema version — the `baseline` row. NOT the highest a resume accepts (that
1393
+ * is {@link MAX_SUPPORTED_CHECKPOINT_VERSION}); resume stays forward-compatible by `<=` against MAX, never `===`. */
1394
+ export declare const CURRENT_CHECKPOINT_VERSION: 1;
1395
+ /** The `resource` row's version. */
1396
+ export declare const RESOURCE_CHECKPOINT_VERSION: 2;
1397
+ /** The `binding` row's version. */
1398
+ export declare const BINDING_CHECKPOINT_VERSION: 3;
1399
+ /** The `token` row's version. v4 (the retired wall-clock total) is deliberately not reused. */
1400
+ export declare const TOKEN_CHECKPOINT_VERSION: 5;
1401
+ /** The `orgAdmission` row's version. */
1402
+ export declare const ORG_ADMISSION_CHECKPOINT_VERSION: 6;
1403
+ /** The `f012` row's version. */
1404
+ export declare const F012_CHECKPOINT_VERSION: 7;
1405
+ /** The `realApproval` row's version. */
1406
+ export declare const REAL_APPROVAL_CHECKPOINT_VERSION: 8;
1407
+ /** The `face` row's version. */
1408
+ export declare const FACE_CHECKPOINT_VERSION: 9;
1409
+ /** The highest {@link Checkpoint.version} `runner.resume` will act on — the top row of the table; a higher one
1410
+ * is rejected pre-CAS with {@link CheckpointError} `unsupported_version` (the row stays `pending`, retryable on
1411
+ * a newer worker). This is the CEILING entrance: the storage-faithfulness probe stamps it directly on a
1412
+ * synthetic row that is never resumed, and does not go through feature detection. */
1413
+ export declare const MAX_SUPPORTED_CHECKPOINT_VERSION: 9;
1414
+ /** What a mint site answers for: every row but the baseline, every key MANDATORY — a new row is a compile
1415
+ * error at each mint site until that site says whether its rows carry the feature. */
1416
+ export type CheckpointFeaturePresence = Record<Exclude<CheckpointFeature, "baseline">, boolean>;
1417
+ /** The schema version a mint stamps: the highest version among the features PRESENT on the row, the baseline
1418
+ * when none is. One rule for every mint site — the precedence between features is the version numbers. */
1419
+ export declare function checkpointVersionFor(presence: CheckpointFeaturePresence): number;
1395
1420
  /**
1396
1421
  * Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
1397
1422
  * written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
@@ -186,15 +186,39 @@ export function realApprovalOrgFact(bit) {
186
186
  return bit.org;
187
187
  return bit.origin === "org_rule" ? "rule" : bit.origin === "org_unavailable" ? "unavailable" : undefined;
188
188
  }
189
- export const CURRENT_CHECKPOINT_VERSION = 1;
190
- export const RESOURCE_CHECKPOINT_VERSION = 2;
191
- export const BINDING_CHECKPOINT_VERSION = 3;
192
- export const TOKEN_CHECKPOINT_VERSION = 5;
193
- export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
194
- export const F012_CHECKPOINT_VERSION = 7;
195
- export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
196
- export const FACE_CHECKPOINT_VERSION = 9;
197
- export const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
189
+ export const CHECKPOINT_FEATURES = {
190
+ baseline: { version: 1, introduced: "design/49", recordedOn: (_cp) => true, writes: "the row itself" },
191
+ resource: { version: 2, introduced: "design/74", recordedOn: (cp) => cp.gate.kind === "resource_limit", writes: "resourceLedger; gate/pendingAction kind resource_limit" },
192
+ binding: { version: 3, introduced: "design/80", recordedOn: (cp) => cp.pendingAction.kind === "tool_approval" || cp.pendingAction.kind === "plan_review", writes: "pendingAction.boundCallId + boundInputHash on a tool_approval; a plan_review carries the stamp only" },
193
+ token: { version: 5, introduced: "design/164", recordedOn: (cp) => cp.resourceLedger?.totalTokens !== undefined, writes: "resourceLedger.totalTokens" },
194
+ orgAdmission: {
195
+ version: 6,
196
+ introduced: "org-admission",
197
+ recordedOn: (cp) => cp.state.inheritedGate?.admittedOrgScopes !== undefined || cp.state.inheritedGate?.ownAdmittedOrgScopes !== undefined || cp.state.inheritedGate?.ownAdmittedOrgWriteScope !== undefined || cp.state.inheritedGate?.orgAdmissionGoverned === true,
198
+ writes: "inheritedGate.admittedOrgScopes / ownAdmittedOrgScopes / ownAdmittedOrgWriteScope / orgAdmissionGoverned",
199
+ },
200
+ f012: { version: 7, introduced: "F-012", recordedOn: (cp) => cp.state.inheritedGate?.constraintChain !== undefined || cp.state.delegationProvenance !== undefined, writes: "inheritedGate.constraintChain + constraintDigest together; delegationProvenance" },
201
+ realApproval: { version: 8, introduced: "#130/#120", recordedOn: (cp) => cp.gate.kind === "irreversible_ask" && cp.gate.realApproval !== undefined, writes: "gate.realApproval (a well-formed bit) + pendingAction.requiresRealApproval; from v9: readFace.realApproval = true beside it" },
202
+ face: { version: 9, introduced: "design/199", recordedOn: (cp) => cp.state.readFace !== undefined, writes: "state.readFace { face, denyEntries? } for a non-default posture; realApproval: true only at the mint whose gate carries the bit (a hands-less re-park never carries the seed's marker forward)" },
203
+ };
204
+ export const CURRENT_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.baseline.version;
205
+ export const RESOURCE_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.resource.version;
206
+ export const BINDING_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.binding.version;
207
+ export const TOKEN_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.token.version;
208
+ export const ORG_ADMISSION_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.orgAdmission.version;
209
+ export const F012_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.f012.version;
210
+ export const REAL_APPROVAL_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.realApproval.version;
211
+ export const FACE_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.face.version;
212
+ export const MAX_SUPPORTED_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.face.version;
213
+ export function checkpointVersionFor(presence) {
214
+ let version = CHECKPOINT_FEATURES.baseline.version;
215
+ for (const feature of Object.keys(CHECKPOINT_FEATURES)) {
216
+ const row = CHECKPOINT_FEATURES[feature];
217
+ if (presence[feature] === true && row.version > version)
218
+ version = row.version;
219
+ }
220
+ return version;
221
+ }
198
222
  export function checkpointVersionOf(cp) {
199
223
  return cp.version ?? 0;
200
224
  }
@@ -46,6 +46,79 @@ export declare const DELIVERED_BY_KIND: {
46
46
  };
47
47
  /** The fence over the delivery table: `never` while every kind has a verdict. */
48
48
  export type DeliveryTableCoversEveryMcpFailureKind = AssertAllKeysHandled<Exclude<McpFailureKind, keyof typeof DELIVERED_BY_KIND>>;
49
+ /**
50
+ * The closed set of {@link McpServerLiveness.state} — the engine's answer to ONE question, "can this
51
+ * client complete an exchange with this server":
52
+ * · `reachable` — an exchange COMPLETED (the dial handshaked and listed, a re-dial succeeded, or the
53
+ * server itself answered — an MCP-level rejection is an answer, so it proves reach
54
+ * even though it failed);
55
+ * · `unreachable` — an exchange was attempted and lost at the transport or the clock (refused, reset,
56
+ * closed, unspawnable, timed out);
57
+ * · `unknown` — an exchange was attempted and what came back does not answer the question (a status
58
+ * a gateway in front of the server may have written, a non-MCP payload, a thrown value
59
+ * with no structure this table reads).
60
+ * ABSENCE is the fourth reading and is NOT one of these words: no observation exists at all (the leg
61
+ * never reached this server, or the declaration could not be dialed). A consumer must not fold an absent
62
+ * record into either `unknown` or a health verdict — absent means indeterminate, and `unknown` means
63
+ * "we looked and could not tell".
64
+ */
65
+ export declare const MCP_LIVENESS_STATES: readonly ["reachable", "unreachable", "unknown"];
66
+ export type McpLivenessState = (typeof MCP_LIVENESS_STATES)[number];
67
+ /**
68
+ * One liveness OBSERVATION about one server (see {@link MCP_LIVENESS_STATES} for the words). It answers
69
+ * what the engine last LEARNED, at `observedAt` (epoch ms) — never what is true now, and never a poll:
70
+ * the engine makes no probe of its own, so every record here is a by-product of work the task already
71
+ * did (the dial, a re-dial, a transport close).
72
+ *
73
+ * `errorCode` rides `unreachable` ONLY, and names the failure class that decided it (a `spawn_failed` is
74
+ * a missing binary, a `connect_refused` a closed port). It is deliberately absent on `unknown`: the whole
75
+ * point of that word is that the failure did NOT decide reachability, so carrying its class there would
76
+ * invite a consumer to key a reach action off evidence that says nothing about reach. The connect-time
77
+ * class is still reported in its own right beside this record (the status / manifest row's `errorCode`).
78
+ */
79
+ export interface McpServerLiveness {
80
+ state: McpLivenessState;
81
+ observedAt: number;
82
+ errorCode?: McpFailureKind;
83
+ }
84
+ /**
85
+ * What each failure CLASS says about reachability — the second disposition table over
86
+ * {@link MCP_FAILURE_KINDS}, beside {@link DELIVERED_BY_KIND}, and deliberately NOT derived from it:
87
+ * delivery asks "did the caller's request run", reachability asks "did an exchange complete", and the two
88
+ * part company on both edges — a `timeout` leaves delivery `unknown` while the exchange provably did not
89
+ * complete, and a `protocol` rejection is a failure the caller must handle while being the strongest
90
+ * proof of reach there is.
91
+ *
92
+ * `invalid_config` maps to `undefined` — the one class that is NOT an observation: a declaration that
93
+ * could not be dialed means no transport was ever attempted, so the honest record is no record (see the
94
+ * absence reading in {@link MCP_LIVENESS_STATES}), not a third-hand `unknown`.
95
+ */
96
+ export declare const LIVENESS_BY_FAILURE_KIND: {
97
+ readonly connect_refused: "unreachable";
98
+ readonly connection_failed: "unreachable";
99
+ readonly connection_closed: "unreachable";
100
+ readonly http_status: "unknown";
101
+ readonly not_mcp_response: "unknown";
102
+ readonly spawn_failed: "unreachable";
103
+ readonly timeout: "unreachable";
104
+ readonly protocol: "reachable";
105
+ readonly invalid_config: undefined;
106
+ readonly unknown: "unknown";
107
+ };
108
+ /** The fence over the liveness table: `never` while every kind has a verdict. */
109
+ export type LivenessTableCoversEveryMcpFailureKind = AssertAllKeysHandled<Exclude<McpFailureKind, keyof typeof LIVENESS_BY_FAILURE_KIND>>;
110
+ /**
111
+ * What ONE exchange with a server tells the liveness record: the CLASS of the failure it ended in, or
112
+ * `"completed"` when it finished. Every round trip the engine makes — a dial, a tool call, a resource
113
+ * read, a listing, a refresh — reports exactly this, so the engine needs no second vocabulary and no
114
+ * per-site judgment about which failures "count".
115
+ */
116
+ export type McpExchangeOutcome = McpFailureKind | "completed";
117
+ /** The liveness observation an exchange supports, or `undefined` when the outcome is not an observation
118
+ * at all (see {@link LIVENESS_BY_FAILURE_KIND}: a declaration that could never be dialed). THE single
119
+ * mint of the record — no two sites can disagree about the word, about which words may carry
120
+ * `errorCode`, or about what a completed exchange proves. */
121
+ export declare function livenessOf(outcome: McpExchangeOutcome, observedAt: number): McpServerLiveness | undefined;
49
122
  /** Where the failure was judged: at the dial (connect + initial listing), where an errno on a stdio
50
123
  * transport is the child process failing to spawn, or on a request over an established connection. */
51
124
  export type McpFailureSite = {
@@ -25,6 +25,27 @@ export const DELIVERED_BY_KIND = {
25
25
  invalid_config: "no",
26
26
  unknown: "unknown",
27
27
  };
28
+ export const MCP_LIVENESS_STATES = ["reachable", "unreachable", "unknown"];
29
+ export const LIVENESS_BY_FAILURE_KIND = {
30
+ connect_refused: "unreachable",
31
+ connection_failed: "unreachable",
32
+ connection_closed: "unreachable",
33
+ http_status: "unknown",
34
+ not_mcp_response: "unknown",
35
+ spawn_failed: "unreachable",
36
+ timeout: "unreachable",
37
+ protocol: "reachable",
38
+ invalid_config: undefined,
39
+ unknown: "unknown",
40
+ };
41
+ export function livenessOf(outcome, observedAt) {
42
+ if (outcome === "completed")
43
+ return { state: "reachable", observedAt };
44
+ const state = LIVENESS_BY_FAILURE_KIND[outcome];
45
+ if (state === undefined)
46
+ return undefined;
47
+ return state === "unreachable" ? { state, observedAt, errorCode: outcome } : { state, observedAt };
48
+ }
28
49
  const CONNECT_KEEPS_THE_TABLE_VERDICT = new Set(["protocol", "http_status"]);
29
50
  const NETWORK_CODES_NEVER_DELIVERED = new Set([
30
51
  "ECONNREFUSED",
@@ -1,5 +1,5 @@
1
1
  import { type ProtocolId } from "./protocol-table.js";
2
- import { type McpDelivered, type McpFailureKind } from "./mcp-failure.js";
2
+ import { type McpDelivered, type McpFailureKind, type McpServerLiveness } from "./mcp-failure.js";
3
3
  import type { AgentTool } from "../internal/harness-types.js";
4
4
  import type { ImageContent, TextContent } from "../internal/llm.js";
5
5
  import { type McpImageResizer } from "./image-downsample.js";
@@ -344,6 +344,23 @@ export interface McpServerStatus {
344
344
  * server name and the outcome) and this field stays absent.
345
345
  */
346
346
  transportClosed?: boolean;
347
+ /**
348
+ * The engine's last liveness OBSERVATION about this server ({@link McpServerLiveness}) — a different
349
+ * question from all three of its neighbours, and the note each of them carries says so:
350
+ * · {@link status} is the CONNECT-TIME verdict of this leg, frozen at the dial — it is not this;
351
+ * · {@link transportClosed} is one EVENT (a close notification arrived), on the transports that have
352
+ * one — it is not this (this record also carries an `unreachable` that no close ever announced);
353
+ * · `McpReconnectResult.outcome` is one ACTION's judgment — it is not this (it says what a re-dial
354
+ * did, this says what the connection layer last learned).
355
+ * Written where the connection layer establishes or loses a connection (the dial, a re-dial, a close,
356
+ * a request that comes back with the transport gone), never on a timer and never by a probe: the
357
+ * engine adds no traffic of its own to answer it.
358
+ *
359
+ * ABSENT = no observation ({@link import("./mcp-failure.js").MCP_LIVENESS_STATES}): a declaration that could not be dialed
360
+ * (`invalid_config`) never touched a transport, and an older / foreign record predates the field. A
361
+ * consumer must read absence as indeterminate — never as healthy, never as off.
362
+ */
363
+ liveness?: McpServerLiveness;
347
364
  }
348
365
  /**
349
366
  * design/108 ① (CC 2.1.187 parity) — the GLOBAL `mcp__` marker prefix. CC names every MCP tool
package/dist/core/mcp.js CHANGED
@@ -6,7 +6,7 @@ import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolNa
6
6
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
7
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
8
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
- import { classifyMcpFailure, describeHttpTransportFailure } from "./mcp-failure.js";
9
+ import { classifyMcpFailure, describeHttpTransportFailure, livenessOf } from "./mcp-failure.js";
10
10
  import { lstat, mkdir, writeFile } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
@@ -213,6 +213,7 @@ function rethrowHonestMcpError(err, ctx) {
213
213
  };
214
214
  sanitizeMcpErrorTextInPlace(err);
215
215
  if (ctx.idle?.signal.reason === IDLE_WATCHDOG_ABORT_REASON) {
216
+ ctx.liveness?.observe?.("timeout");
216
217
  const serverLabel = inlineUntrusted(ctx.server);
217
218
  finish(markMcpFailure(new Error(`${ctx.what} on MCP server "${serverLabel}" received no response for ${ctx.idle.idleMs}ms (idle watchdog — ` +
218
219
  `distinct from the ${ctx.timeoutMs}ms total ceiling above). The server may still be alive but is not ` +
@@ -224,6 +225,7 @@ function rethrowHonestMcpError(err, ctx) {
224
225
  const serverLabel = inlineUntrusted(ctx.server);
225
226
  const fenceServerText = (label, raw) => delimitUntrusted(label, truncateMcpErrorText(raw));
226
227
  const failure = classifyMcpFailure(err, { phase: "request" });
228
+ ctx.liveness?.observe?.(failure.kind);
227
229
  const details = (extra) => ({ code: failure.kind, delivered: failure.delivered, server: ctx.server, ...extra });
228
230
  switch (failure.kind) {
229
231
  case "timeout": {
@@ -721,10 +723,19 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
721
723
  else
722
724
  serverHandles[hIdx] = handle;
723
725
  const hasInstructions = s.instructions !== undefined;
726
+ const observe = (outcome) => {
727
+ if (disposing)
728
+ return;
729
+ const observed = livenessOf(outcome, Date.now());
730
+ if (observed !== undefined)
731
+ s.status.liveness = observed;
732
+ };
733
+ s.health.observe = observe;
724
734
  const announceClose = () => {
725
735
  if (disposing)
726
736
  return;
727
737
  s.status.transportClosed = true;
738
+ observe("connection_closed");
728
739
  if (hasInstructions)
729
740
  instructionsDelta.pendingRemovals.push(spec.name);
730
741
  };
@@ -807,7 +818,8 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
807
818
  else {
808
819
  warnings.push(asServerWarning(spec, r.reason));
809
820
  const { kind, delivered, httpStatus } = classifyMcpFailure(r.reason, { phase: "connect", transport: dialKinds[i] });
810
- statuses.push({ name: spec.name, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(r.reason) });
821
+ const liveness = livenessOf(kind, Date.now());
822
+ statuses.push({ name: spec.name, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(r.reason), ...(liveness !== undefined ? { liveness } : {}) });
811
823
  }
812
824
  }
813
825
  const resourceTools = buildResourceTools(resourceServers, isServerRevoked);
@@ -838,6 +850,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
838
850
  const priorInstructions = prior?.instructions;
839
851
  if (prior) {
840
852
  prior.announce.fn = undefined;
853
+ prior.health.observe = undefined;
841
854
  serverHandles.splice(hIdx, 1);
842
855
  const cIdx = clients.indexOf(prior.client);
843
856
  if (cIdx !== -1)
@@ -851,7 +864,8 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
851
864
  }
852
865
  catch (err) {
853
866
  const { kind, delivered, httpStatus } = classifyMcpFailure(err, { phase: "connect", transport: dialKind });
854
- const status = { name: server, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(err) };
867
+ const redialLiveness = livenessOf(kind, Date.now());
868
+ const status = { name: server, status: "failed", errorCode: kind, delivered, ...(httpStatus !== undefined ? { httpStatus } : {}), error: namedMcpFailureText(err), ...(redialLiveness !== undefined ? { liveness: redialLiveness } : {}) };
855
869
  const sIdx = statuses.findIndex((st) => st.name === server);
856
870
  if (sIdx === -1)
857
871
  statuses.push(status);
@@ -953,6 +967,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
953
967
  });
954
968
  }
955
969
  catch (err) {
970
+ h.health.observe?.(classifyMcpFailure(err, { phase: "request" }).kind);
956
971
  results.push({
957
972
  server: h.name,
958
973
  prefix: prefixOf(h.name),
@@ -1334,7 +1349,8 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
1334
1349
  try {
1335
1350
  res = await rs.client
1336
1351
  .readResource({ uri }, { signal: watchdog.combinedSignal, timeout: timeoutMs })
1337
- .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
1352
+ .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs }, liveness: rs.health }));
1353
+ rs.health.observe?.("completed");
1338
1354
  }
1339
1355
  finally {
1340
1356
  watchdog.dispose();
@@ -1393,7 +1409,7 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
1393
1409
  try {
1394
1410
  const ext = rs.client.getServerCapabilities()?.extensions?.[MCP_SKILLS_EXTENSION];
1395
1411
  if (ext?.directoryRead === true) {
1396
- const r = await readDirViaExtension(rs, uri, signal, timeoutMs, watchdog).catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
1412
+ const r = await readDirViaExtension(rs, uri, signal, timeoutMs, watchdog).catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs }, liveness: rs.health }));
1397
1413
  const serverSaid = (detail) => `\nThe server's error text follows as external/untrusted data:\n${delimitUntrusted(`${server} error`, truncateMcpErrorText(detail))}`;
1398
1414
  if (r.kind === "not_found") {
1399
1415
  return {
@@ -1421,7 +1437,8 @@ function buildResourceTools(resourceServers, isServerRevoked = () => false) {
1421
1437
  const walk = await walkMcpListPages(async (cursor, remainingMs) => {
1422
1438
  const page = await rs.client
1423
1439
  .listResources(cursor === undefined ? undefined : { cursor }, { signal: watchdog.combinedSignal, timeout: remainingMs })
1424
- .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs: remainingMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
1440
+ .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs: remainingMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs }, liveness: rs.health }));
1441
+ rs.health.observe?.("completed");
1425
1442
  return { items: page.resources, ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}) };
1426
1443
  }, { budgetMs: timeoutMs, onPage: () => watchdog.rearm(), fingerprintOf: listEntryFingerprint, ...(signal !== undefined ? { signal } : {}) });
1427
1444
  const dirPrefix = uri.endsWith("/") ? uri : `${uri}/`;
@@ -1552,13 +1569,15 @@ async function connectServer(spec, principal, onElicit, imageResizer, reminderDi
1552
1569
  const rawInstructions = client.getInstructions();
1553
1570
  const instructions = rawInstructions && rawInstructions.trim() ? sanitizeUntrustedText(rawInstructions.trim()) : undefined;
1554
1571
  const info = client.getServerVersion();
1572
+ const status = {
1573
+ name: spec.name,
1574
+ status: "connected",
1575
+ ...(info ? { serverInfo: { name: info.name, version: info.version } } : {}),
1576
+ toolNames: serverTools.map((t) => t.name),
1577
+ liveness: livenessOf("completed", Date.now()),
1578
+ };
1555
1579
  return {
1556
- status: {
1557
- name: spec.name,
1558
- status: "connected",
1559
- ...(info ? { serverInfo: { name: info.name, version: info.version } } : {}),
1560
- toolNames: serverTools.map((t) => t.name),
1561
- },
1580
+ status,
1562
1581
  client,
1563
1582
  health,
1564
1583
  announce,
@@ -1693,10 +1712,12 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1693
1712
  writeEffect,
1694
1713
  signal,
1695
1714
  idle: { signal: watchdog.idleSignal, idleMs },
1715
+ liveness: health,
1696
1716
  ...(reminderDisclosure !== undefined
1697
1717
  ? { reminder: { mark: reminderDisclosure.mark, outlet: "mcp", ...(reminderDisclosure.counts !== undefined ? { counts: reminderDisclosure.counts } : {}) } }
1698
1718
  : {}),
1699
1719
  }));
1720
+ health.observe?.("completed");
1700
1721
  }
1701
1722
  finally {
1702
1723
  watchdog.dispose();
@@ -1,5 +1,5 @@
1
1
  import { readMember, thrownText } from "../thrown-value.js";
2
- import { assertServableRequestRoot } from "../../brain/route-adjudicator.js";
2
+ import { assertServableRequestRoot, joinRequestUrl } from "../../brain/route-adjudicator.js";
3
3
  export const DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT = 32;
4
4
  const SIZE_CLAUSE = {
5
5
  "dl-2": "- the index should be as small as it can be made without losing either of the above.",
@@ -146,7 +146,7 @@ export function openAiCompatChatSeat(seat) {
146
146
  throw distillerConfigRefusal("memory consolidation driver chat seat needs a non-empty baseUrl.");
147
147
  let requestRoot;
148
148
  try {
149
- requestRoot = (assertServableRequestRoot(seat.baseUrl, "memory consolidation driver chat seat baseUrl") ?? "").replace(/\/+$/, "");
149
+ requestRoot = assertServableRequestRoot(seat.baseUrl, "memory consolidation driver chat seat baseUrl") ?? "";
150
150
  }
151
151
  catch (e) {
152
152
  throw distillerConfigRefusal(thrownText(e), e);
@@ -157,7 +157,7 @@ export function openAiCompatChatSeat(seat) {
157
157
  throw distillerConfigRefusal(`memory consolidation driver chat seat needs an EXPLICIT model id, got ${JSON.stringify(seat.model)} — an alias can be re-pointed upstream and would move the driver with no trace in the plan.`);
158
158
  }
159
159
  return async (req) => {
160
- const res = await fetch(`${requestRoot}/chat/completions`, {
160
+ const res = await fetch(joinRequestUrl(requestRoot, "/chat/completions"), {
161
161
  method: "POST",
162
162
  headers: { "content-type": "application/json", authorization: `Bearer ${seat.apiKey}` },
163
163
  body: JSON.stringify({