@sema-agent/core 7.37.1 → 7.38.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 (133) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/agents/observer.js +1 -1
  3. package/dist/agents/peer-notices.js +4 -4
  4. package/dist/agents/subagent.js +1 -1
  5. package/dist/agents/team-role.js +2 -2
  6. package/dist/brain/anthropic.js +10 -9
  7. package/dist/brain/errors.d.ts +26 -0
  8. package/dist/brain/errors.js +34 -0
  9. package/dist/brain/input-too-long.d.ts +0 -31
  10. package/dist/brain/input-too-long.js +1 -18
  11. package/dist/brain/open-responses.js +10 -9
  12. package/dist/brain/openai.js +10 -9
  13. package/dist/brain/status-sink.d.ts +1 -1
  14. package/dist/brain/stream-engine.js +10 -2
  15. package/dist/brain/stream-shared.d.ts +11 -3
  16. package/dist/brain/stream-shared.js +4 -2
  17. package/dist/brain/tool-call-id.d.ts +29 -8
  18. package/dist/brain/tool-call-id.js +36 -6
  19. package/dist/core/a2a-task-state.js +2 -2
  20. package/dist/core/artifact-host.js +6 -6
  21. package/dist/core/ask-origin.js +6 -6
  22. package/dist/core/ask-unresolvable-notice.js +1 -1
  23. package/dist/core/auto-mode.js +4 -4
  24. package/dist/core/background-agent-store.js +2 -2
  25. package/dist/core/checkpoint-execution-record.js +1 -1
  26. package/dist/core/checkpoint-store.d.ts +2 -2
  27. package/dist/core/checkpoint-store.js +2 -2
  28. package/dist/core/child-clamp-seats.js +3 -3
  29. package/dist/core/compliance.js +1 -1
  30. package/dist/core/context-edit.js +1 -1
  31. package/dist/core/context-guard.d.ts +0 -19
  32. package/dist/core/context-guard.js +0 -38
  33. package/dist/core/decision-receipt.d.ts +27 -1
  34. package/dist/core/decision-receipt.js +176 -1
  35. package/dist/core/engine-notice.d.ts +59 -8
  36. package/dist/core/engine-notice.js +2 -1
  37. package/dist/core/gate-fold.js +3 -2
  38. package/dist/core/gate-outcome.js +4 -4
  39. package/dist/core/governance-codes.d.ts +221 -13
  40. package/dist/core/governance-codes.js +104 -4
  41. package/dist/core/hook-seats.js +2 -2
  42. package/dist/core/hooks.d.ts +21 -2
  43. package/dist/core/hooks.js +4 -1
  44. package/dist/core/mailbox-store.js +3 -3
  45. package/dist/core/mcp-failure.js +4 -4
  46. package/dist/core/mcp-injection-drop.js +1 -1
  47. package/dist/core/memory-engine/consolidation.js +1 -1
  48. package/dist/core/memory-engine/delegation-settlement.d.ts +8 -0
  49. package/dist/core/memory-engine/delegation-settlement.js +7 -0
  50. package/dist/core/memory-engine/engine.d.ts +12 -2
  51. package/dist/core/memory-engine/engine.js +10 -6
  52. package/dist/core/memory-engine/export-bundle.js +1 -1
  53. package/dist/core/memory-engine/layout.d.ts +6 -6
  54. package/dist/core/memory-engine/layout.js +6 -8
  55. package/dist/core/memory-engine/session-write-home.d.ts +30 -0
  56. package/dist/core/memory-engine/session-write-home.js +34 -0
  57. package/dist/core/memory-engine/tools.js +1 -1
  58. package/dist/core/memory-engine/types.js +1 -1
  59. package/dist/core/memory.js +1 -1
  60. package/dist/core/mode-policy.d.ts +10 -1
  61. package/dist/core/mode-policy.js +15 -7
  62. package/dist/core/model-seat.js +1 -1
  63. package/dist/core/permission-rule-model.js +3 -3
  64. package/dist/core/permission-rule-store.js +2 -2
  65. package/dist/core/read-only-shell-table.js +8 -8
  66. package/dist/core/retention.js +2 -2
  67. package/dist/core/runner/assemble-result.js +15 -5
  68. package/dist/core/runner/classifier-ladder.js +1 -1
  69. package/dist/core/runner/denial-limit-arms.d.ts +2 -0
  70. package/dist/core/runner/denial-limit-arms.js +6 -2
  71. package/dist/core/runner/inherited-ask-grants.d.ts +6 -5
  72. package/dist/core/runner/inherited-ask-grants.js +3 -2
  73. package/dist/core/runner/prepare-context-lane.d.ts +19 -15
  74. package/dist/core/runner/prepare-context-lane.js +11 -15
  75. package/dist/core/runner/prepare-gate-stations.js +5 -5
  76. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +10 -10
  78. package/dist/core/runner/run-harness-handlers.js +3 -0
  79. package/dist/core/runner/run-identity-wiring.d.ts +1 -1
  80. package/dist/core/runner/run-identity-wiring.js +0 -2
  81. package/dist/core/runner/run-leg.js +2 -2
  82. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  83. package/dist/core/runner/run-turn-boundary.js +1 -1
  84. package/dist/core/runner/session-mode-seats.d.ts +11 -3
  85. package/dist/core/runner/session-mode-seats.js +6 -0
  86. package/dist/core/runner/session-rule-policy.js +2 -3
  87. package/dist/core/runner/stream-steer-verb.js +1 -1
  88. package/dist/core/runner/tool-call-id-fold-seat.js +16 -9
  89. package/dist/core/runner/tool-turn-repair-seat.d.ts +11 -1
  90. package/dist/core/runner/tool-turn-repair-seat.js +35 -0
  91. package/dist/core/runner/turn-attachments.js +1 -1
  92. package/dist/core/runtime-caps.js +1 -1
  93. package/dist/core/safety-axis-vocab.d.ts +5 -0
  94. package/dist/core/safety-axis-vocab.js +5 -0
  95. package/dist/core/session-reconcile.d.ts +9 -7
  96. package/dist/core/session-reconcile.js +21 -1
  97. package/dist/core/shared-memory/types.js +1 -1
  98. package/dist/core/side-query.d.ts +6 -1
  99. package/dist/core/store-contracts/read-back-shape-contract.js +1 -1
  100. package/dist/core/swappable-deps.js +1 -1
  101. package/dist/core/task-notification.js +4 -4
  102. package/dist/core/task-registry-shared.js +1 -1
  103. package/dist/core/terminal-cause.d.ts +19 -0
  104. package/dist/core/tool-face.js +6 -6
  105. package/dist/core/tool-policy.d.ts +17 -11
  106. package/dist/core/tool-policy.js +38 -72
  107. package/dist/core/tool-roster.js +1 -1
  108. package/dist/core/tool-search-setting.js +1 -1
  109. package/dist/core/trace.d.ts +5 -4
  110. package/dist/core/types.d.ts +2 -2
  111. package/dist/core/types.js +1 -1
  112. package/dist/core/wiring-manifest.js +2 -2
  113. package/dist/core/write-protect.js +1 -1
  114. package/dist/engine/harness/agent-harness.js +10 -6
  115. package/dist/engine/llm/tool-turns.d.ts +93 -4
  116. package/dist/engine/llm/tool-turns.js +63 -9
  117. package/dist/engine/llm/types.d.ts +11 -0
  118. package/dist/index.d.ts +5 -3
  119. package/dist/index.js +4 -2
  120. package/dist/internal/llm.d.ts +2 -2
  121. package/dist/internal/llm.js +1 -1
  122. package/dist/orchestration/workflow-governance.js +2 -2
  123. package/dist/orchestration/workflow-types.js +2 -2
  124. package/dist/shell/lex.js +7 -7
  125. package/dist/tools/fs/bash-program-position.js +2 -2
  126. package/dist/tools/fs/read-deny.js +2 -2
  127. package/dist/tools/fs/safety.js +1 -1
  128. package/dist/tools/fs/shell-read-judge.js +2 -2
  129. package/dist/tools/fs/shell-read-plan.js +2 -2
  130. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  131. package/package.json +1 -1
  132. package/test/export-surface.snapshot.json +17 -1
  133. package/wire-changes/core@7.38.0.json +664 -0
@@ -829,6 +829,11 @@ export interface SessionMemoryStatus {
829
829
  /** Newest lineage `lastAt` for this session (ms epoch) — rides the same single ledger read as
830
830
  * `committedCount`. Absent = ledger unreadable, or no committed contribution exists at all. */
831
831
  lastCaptureAt?: number;
832
+ /** The canonical absolute directory of the session's write home: the home of the write scope its
833
+ * newest leg wrote under (the path that leg's handle carries as `writableRoot`). Absent = no source
834
+ * names a scope, or a source faulted — see the method doc for the sources and the absence law. An
835
+ * engine predating the key never answers it, but absence alone does not identify such an engine. */
836
+ writeHome?: string;
832
837
  }
833
838
  export declare class MemoryEngine {
834
839
  private readonly backend;
@@ -1608,9 +1613,14 @@ export declare class MemoryEngine {
1608
1613
  * carried rather than dropped; absent ⇔ the ledger is unreadable OR the session has no
1609
1614
  * committed contribution at all (there is no such moment to name — the one key whose absence
1610
1615
  * also covers "no fact exists", stated here so consumers need not guess).
1616
+ * - `writeHome` — the canonical home of the write scope the session wrote under most recently (the newer of the dated account row and the newest committed contribution); sources, order and the
1617
+ * absence law in {@link sessionWriteHomeOf} (a faulted source never falls through).
1611
1618
  *
1612
- * Side-effect-free committed reads throughout (capture face + lineage sidecar + audit-face
1613
- * headers/getByIds); never throws — an unreadable source is an absent key, which IS the answer.
1619
+ * Committed reads throughout (capture face + lineage sidecar + session-account sidecar + scope registry +
1620
+ * audit-face headers/getByIds), none of which commits anything for this session; the File backend's
1621
+ * audit face runs that backend's layout seat as each of its lock-free read faces does (loose root content
1622
+ * in a v2 store is adopted into its owner's home — `foldedCount`'s header read has always reached it).
1623
+ * Never throws — an unreadable source is an absent key, which IS the answer.
1614
1624
  */
1615
1625
  sessionMemoryStatus(sessionId: string): Promise<SessionMemoryStatus>;
1616
1626
  /** The committed, side-effect-free audit read: the adoption-restricted committed view when the
@@ -13,6 +13,7 @@ import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSO
13
13
  import { deriveShadowedSet } from "./shadowing.js";
14
14
  import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, openLayerWriters, terminateSessionAccount, sessionAccountLeg, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, emptyUnattributedSet, convertV1SidecarKeys, } from "./delegation-settlement.js";
15
15
  import { noReplaceRestore, retireStagingToCustody, stagingPathBeside } from "./delegation-settlement.js";
16
+ import { sessionWriteHomeOf } from "./session-write-home.js";
16
17
  import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCandidates, readOnlyLayerCensus, recordLayerObservations, settleUndecidedLayerObservations, observationBacked, sameObservation, undecidedLayerObservations } from "./layer-observations.js";
17
18
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
18
19
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
@@ -2006,7 +2007,7 @@ export class MemoryEngine {
2006
2007
  unattributedSet = sessionUnattributedSet(this.controlDir, lineageSessionId);
2007
2008
  }
2008
2009
  }
2009
- const rec = reconcileLineage(this.controlDir, this.now);
2010
+ const rec = reconcileLineage(this.controlDir);
2010
2011
  this.settlePromotions(rec.promoted);
2011
2012
  for (const u of rec.undecidable) {
2012
2013
  report.warnings.push(u.kind === "latch-only"
@@ -2779,7 +2780,7 @@ export class MemoryEngine {
2779
2780
  if (lineageArmed) {
2780
2781
  try {
2781
2782
  recordLineageCredential(this.controlDir, txnId, patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
2782
- const promoted = promoteLineagePending(this.controlDir, txnId, this.now);
2783
+ const promoted = promoteLineagePending(this.controlDir, txnId);
2783
2784
  this.settlePromotions(promoted);
2784
2785
  }
2785
2786
  catch (err) {
@@ -3108,7 +3109,7 @@ export class MemoryEngine {
3108
3109
  }
3109
3110
  try {
3110
3111
  recordLineageCredential(this.controlDir, txnId, patchReport.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
3111
- const promoted = promoteLineagePending(this.controlDir, txnId, this.now);
3112
+ const promoted = promoteLineagePending(this.controlDir, txnId);
3112
3113
  this.settlePromotions(promoted);
3113
3114
  }
3114
3115
  catch (err) {
@@ -3720,7 +3721,7 @@ export class MemoryEngine {
3720
3721
  if (lineageArmed) {
3721
3722
  try {
3722
3723
  recordLineageCredential(this.controlDir, txnId, report.applied.filter((a) => a.op !== "delete").map((a) => a.id), this.now);
3723
- this.settlePromotions(promoteLineagePending(this.controlDir, txnId, this.now));
3724
+ this.settlePromotions(promoteLineagePending(this.controlDir, txnId));
3724
3725
  }
3725
3726
  catch (err) {
3726
3727
  plan.audit.push({ at: this.now(), event: "lineage_settle_failed", detail: thrownText(err) });
@@ -4325,13 +4326,13 @@ export class MemoryEngine {
4325
4326
  let mine;
4326
4327
  try {
4327
4328
  const committed = readLineageRecord(this.controlDir).committed;
4328
- const ids = new Set();
4329
+ const ids = new Map();
4329
4330
  let lastAt;
4330
4331
  for (const [entryId, sessions] of Object.entries(committed)) {
4331
4332
  const c = sessions[sessionId];
4332
4333
  if (c === undefined)
4333
4334
  continue;
4334
- ids.add(entryId);
4335
+ ids.set(entryId, c.lastAt);
4335
4336
  if (lastAt === undefined || c.lastAt > lastAt)
4336
4337
  lastAt = c.lastAt;
4337
4338
  }
@@ -4342,6 +4343,9 @@ export class MemoryEngine {
4342
4343
  }
4343
4344
  catch {
4344
4345
  }
4346
+ const writeHome = await sessionWriteHomeOf({ memoryDir: this.memoryDir, controlDir: this.controlDir, sessionId, contributions: mine, readEntries: (ids) => this.committedAuditFace().getByIds(ids) });
4347
+ if (writeHome !== undefined)
4348
+ out.writeHome = writeHome;
4345
4349
  if (mine !== undefined) {
4346
4350
  if (mine.size === 0) {
4347
4351
  out.foldedCount = 0;
@@ -3,7 +3,7 @@ import { canonicalJsonStringify } from "./file-backend.js";
3
3
  import { computeEntryRev } from "./frontmatter.js";
4
4
  import { MEMORY_ORIGIN_CAUSES } from "./types.js";
5
5
  import { ambiguousOriginRepresentation, committedOriginOf } from "./frontmatter.js";
6
- export const BUNDLE_SECTION_KEYS = ["meta", "entries", "governance", "residuals"];
6
+ export const BUNDLE_SECTION_KEYS = Object.freeze(["meta", "entries", "governance", "residuals"]);
7
7
  function sha256Line(text) {
8
8
  return createHash("sha256").update(`${text}\n`, "utf8").digest("hex");
9
9
  }
@@ -733,10 +733,10 @@ export interface LineagePromotion {
733
733
  marked?: true;
734
734
  }
735
735
  /** Promote a CREDENTIALED pending transaction: rows named by the credential's applied set join the
736
- * committed contribution set (deduped by (entryId, sessionId) — a repeat pair refreshes
737
- * lastRev/lastAt, never grows the set); rows NOT in the applied set (conflicted/refused ops) are
736
+ * committed contribution set (deduped by (entryId, sessionId) — a repeat pair takes the incoming
737
+ * lastRev and the credential's date, never grows the set); rows NOT in the applied set (conflicted/refused ops) are
738
738
  * dropped without ever becoming ancestry. Throws when the txn is unknown or uncredentialed. */
739
- export declare function promoteLineagePending(controlDir: string, txnId: string, now: () => number): LineagePromotion[];
739
+ export declare function promoteLineagePending(controlDir: string, txnId: string): LineagePromotion[];
740
740
  /** The host adjudication for the UNDECIDABLE crash window (a pending txn with no credential —
741
741
  * design/180 r5: never auto-discarded). `"promote"` = the host verified the commit landed (all
742
742
  * staged rows join the committed set); `"discard"` = the host verified it did not. Idempotent on
@@ -744,7 +744,7 @@ export declare function promoteLineagePending(controlDir: string, txnId: string,
744
744
  export declare function adjudicateLineagePending(controlDir: string, txnId: string, action: "promote" | "discard", now: () => number): LineagePromotion[];
745
745
  /** Reconciliation (harvest-opening / host-triggered): promote every CREDENTIALED orphan pending
746
746
  * txn; report the uncredentialed ones (their latch stays until the host adjudicates). */
747
- export declare function reconcileLineage(controlDir: string, now: () => number): {
747
+ export declare function reconcileLineage(controlDir: string): {
748
748
  promoted: LineagePromotion[];
749
749
  undecidable: Array<{
750
750
  txnId: string;
@@ -853,8 +853,8 @@ export interface LineageImportDivergence {
853
853
  }
854
854
  /**
855
855
  * design/178 v2-c §5.1 — the lineage FIDELITY importer: write bundle lineage rows into the
856
- * committed set VERBATIM (`lastRev`/`lastAt` carried byte-for-byte — the promote leg mints its own
857
- * `now` and is therefore unusable for carriage). Deterministic same-key merge, idempotent under
856
+ * committed set VERBATIM (`lastRev`/`lastAt` carried byte-for-byte: the promote leg dates a contribution by its credential,
857
+ * the import carries the exporter's date — neither reads a clock here). Deterministic same-key merge, idempotent under
858
858
  * re-import of the same bundle:
859
859
  * - destination has no (entryId, sessionId) row ⇒ the source row lands verbatim;
860
860
  * - both present, source `lastAt` is GREATER ⇒ source wins ("last contribution" is a time-axis
@@ -313,7 +313,7 @@ export function dirCaseFolds(dir) {
313
313
  }
314
314
  export const SCOPE_DIR_LABEL_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
315
315
  export const CASEFOLD_PROBE_PREFIX = ".casefold-probe-";
316
- export const SCOPE_REGISTRY_SHAPES = ["absent", "v1", "v2", "migrating"];
316
+ export const SCOPE_REGISTRY_SHAPES = Object.freeze(["absent", "v1", "v2", "migrating"]);
317
317
  export const REGISTRY_SHAPE_DERIVES_PATHS = {
318
318
  absent: true,
319
319
  v1: false,
@@ -1657,7 +1657,7 @@ export function recordLineageCredential(controlDir, txnId, appliedIds, now) {
1657
1657
  return { next: rec, result: undefined };
1658
1658
  });
1659
1659
  }
1660
- export function promoteLineagePending(controlDir, txnId, now) {
1660
+ export function promoteLineagePending(controlDir, txnId) {
1661
1661
  return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
1662
1662
  const txn = rec.pending[txnId];
1663
1663
  if (txn === undefined)
@@ -1666,7 +1666,7 @@ export function promoteLineagePending(controlDir, txnId, now) {
1666
1666
  throw new ControlPlaneCorruptError(`memory lineage ledger: refusing to promote uncredentialed pending transaction ${txnId} (the commit outcome is not durably known)`);
1667
1667
  }
1668
1668
  const applied = new Set(txn.credential.appliedIds);
1669
- const at = now();
1669
+ const at = txn.credential.at;
1670
1670
  const promoted = [];
1671
1671
  for (const row of txn.rows) {
1672
1672
  if (!applied.has(row.entryId))
@@ -1692,7 +1692,7 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
1692
1692
  return { next: rec, result: [] };
1693
1693
  }
1694
1694
  const applied = txn.credential !== undefined ? new Set(txn.credential.appliedIds) : undefined;
1695
- const at = now();
1695
+ const at = txn.credential?.at ?? now();
1696
1696
  const promoted = [];
1697
1697
  for (const row of txn.rows) {
1698
1698
  if (applied !== undefined && !applied.has(row.entryId))
@@ -1705,11 +1705,10 @@ export function adjudicateLineagePending(controlDir, txnId, action, now) {
1705
1705
  return { next: rec, result: promoted };
1706
1706
  });
1707
1707
  }
1708
- export function reconcileLineage(controlDir, now) {
1708
+ export function reconcileLineage(controlDir) {
1709
1709
  return lockedStrictUpdate(controlDir, LINEAGE_FILE, "memory lineage ledger", coerceLineage, (rec) => {
1710
1710
  const promoted = [];
1711
1711
  const undecidable = [];
1712
- let at;
1713
1712
  let changed = false;
1714
1713
  for (const [txnId, txn] of Object.entries(rec.pending)) {
1715
1714
  if (txn.credential === undefined) {
@@ -1720,9 +1719,8 @@ export function reconcileLineage(controlDir, now) {
1720
1719
  for (const row of txn.rows) {
1721
1720
  if (!applied.has(row.entryId))
1722
1721
  continue;
1723
- at ??= now();
1724
1722
  const sessions = (rec.committed[row.entryId] ??= Object.create(null));
1725
- sessions[txn.sessionId] = { lastRev: row.rev, lastAt: at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
1723
+ sessions[txn.sessionId] = { lastRev: row.rev, lastAt: txn.credential.at, ...(row.marked === true ? { marked: true } : {}), ...(txn.incarnation !== undefined ? { incarnation: txn.incarnation } : {}) };
1726
1724
  promoted.push({ entryId: row.entryId, sessionId: txn.sessionId, rev: row.rev, ...(row.marked === true ? { marked: true } : {}) });
1727
1725
  }
1728
1726
  delete rec.pending[txnId];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A session's write home, as {@link import("./engine.js").MemoryEngine.sessionMemoryStatus} reports it
3
+ * (`SessionMemoryStatus.writeHome`): the canonical absolute directory of the write scope the session wrote under MOST
4
+ * RECENTLY — `canonicalize(scopeDirFor(write scope))`, the derivation behind a leg's handle's `writableRoot`. Two sources
5
+ * name a scope, each dated, and the NEWER one wins:
6
+ * 1. the session's account row: every account-opening mount (a sessioned, unrestricted mount with a write face) records
7
+ * its write scope there, dated by the account's (re-)open time — readable from any engine on the control plane;
8
+ * 2. the entry carrying the session's newest committed lineage contribution, dated by that contribution — a session that
9
+ * wrote without opening an account (provenance "off", a row written before the field) names its home this way, and so
10
+ * does a leg that wrote AFTER an older account leg (an off-mode commit under another scope after a carry leg ended:
11
+ * the older row must not hide the newer home).
12
+ * 3. neither — ABSENT: a session that never opened an account and never committed.
13
+ * A FAULTED source (a read that throws: an unreadable account ledger or lineage) answers absent and never falls through —
14
+ * it may hide the newer scope, so the other source's word would be a stand-in. A SILENT source (the newest contribution's
15
+ * entry is gone — deleted or a tombstone — or there is no account row) yields to the other source: a deleted note is an
16
+ * ordinary state, not a fault, and the account row is the durable record of where the session writes. Older contributions
17
+ * are never consulted. A registry that cannot derive a home (v1, an unfinished layout move)
18
+ * answers absent before either source is read, so no read here goes through a face whose layout seat would finish the move
19
+ * first and then name a post-move home.
20
+ */
21
+ import type { MemoryEntry } from "./types.js";
22
+ export declare function sessionWriteHomeOf(input: {
23
+ memoryDir: string;
24
+ controlDir: string;
25
+ sessionId: string;
26
+ /** The caller's lineage read: entry id → this session's `lastAt` on it (undefined ⇔ that read faulted). */
27
+ contributions: ReadonlyMap<string, number> | undefined;
28
+ /** A committed, side-effect-free entry read (the engine's audit face). */
29
+ readEntries: (ids: readonly string[]) => Promise<MemoryEntry[]>;
30
+ }): Promise<string | undefined>;
@@ -0,0 +1,34 @@
1
+ import { canonicalize, REGISTRY_SHAPE_DERIVES_PATHS, scopeDirFor, scopeRegistryShape } from "./layout.js";
2
+ import { sessionAccountWriteHome } from "./delegation-settlement.js";
3
+ export async function sessionWriteHomeOf(input) {
4
+ const { memoryDir, controlDir, sessionId, contributions } = input;
5
+ try {
6
+ if (!REGISTRY_SHAPE_DERIVES_PATHS[scopeRegistryShape(controlDir)])
7
+ return undefined;
8
+ if (contributions === undefined)
9
+ return undefined;
10
+ let newest;
11
+ for (const [id, at] of contributions) {
12
+ if (newest === undefined || at > newest.at || (at === newest.at && id > newest.id))
13
+ newest = { id, at };
14
+ }
15
+ const account = sessionAccountWriteHome(controlDir, sessionId);
16
+ const sources = [];
17
+ const fromAccount = async () => account?.writeScope;
18
+ const fromNewest = async () => (newest === undefined ? undefined : (await input.readEntries([newest.id])).find((e) => e.id === newest.id)?.scope);
19
+ if (account !== undefined && (newest === undefined || account.openedAt >= newest.at))
20
+ sources.push(fromAccount, fromNewest);
21
+ else
22
+ sources.push(fromNewest, fromAccount);
23
+ let scope;
24
+ for (const source of sources) {
25
+ scope = await source();
26
+ if (scope !== undefined)
27
+ break;
28
+ }
29
+ return scope === undefined ? undefined : canonicalize(scopeDirFor(memoryDir, controlDir, scope));
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
@@ -11,7 +11,7 @@ import { toolFace } from "../tool-catalog-entries.js";
11
11
  export const MEMORY_SEARCH_TOOL_NAME = "memory_search";
12
12
  export const MEMORY_GET_TOOL_NAME = "memory_get";
13
13
  export const MEMORY_INDEX_TOOL_NAME = "memory_index";
14
- export const MEMORY_ENGINE_TOOL_NAMES = [MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME];
14
+ export const MEMORY_ENGINE_TOOL_NAMES = Object.freeze([MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME]);
15
15
  export const MEMORY_SEARCH_DEFAULT_LIMIT = 8;
16
16
  export const MEMORY_SEARCH_MAX_LIMIT = 20;
17
17
  export const MEMORY_INDEX_PAGE_ROWS = 100;
@@ -1,4 +1,4 @@
1
- export const MEMORY_ORIGIN_CAUSES = ["observed", "derived", "static", "unattributed"];
1
+ export const MEMORY_ORIGIN_CAUSES = Object.freeze(["observed", "derived", "static", "unattributed"]);
2
2
  export function writableRootOf(handle) {
3
3
  if (handle.writableRoot === null)
4
4
  throw new Error("memory session handle has no write face (writeScope is null) — writableRoot is not available");
@@ -363,7 +363,7 @@ export function parseNoteTimestamp(ts) {
363
363
  const ms = Date.parse(`${ts.replace(" ", "T")}:00Z`);
364
364
  return Number.isFinite(ms) ? { mtimeMs: ms } : { mtimeMs: 0, timestampMissing: true };
365
365
  }
366
- export const MEMORY_PROJECT_KEY_ABSENT_REASONS = ["not_git", "bare", "unreadable"];
366
+ export const MEMORY_PROJECT_KEY_ABSENT_REASONS = Object.freeze(["not_git", "bare", "unreadable"]);
367
367
  export function normalizeMemorySpec(input) {
368
368
  if (!input)
369
369
  return undefined;
@@ -128,9 +128,12 @@ export interface PermissionModeArming {
128
128
  * or a rejection from it is contained. */
129
129
  readonly onArmFault?: (error: unknown) => void;
130
130
  }
131
- /** A live ceiling: the parent leg's own controller (`current` tracks the parent's steers), or a recorded word. */
131
+ /** A live ceiling: the parent leg's own controller (`current` tracks the parent's steers), or a recorded word. A ceiling
132
+ * that can start something on a read (a controller, the engine's leg ceiling) also answers `observed` — the same word,
133
+ * read without starting anything; one without it is read through `current` on an observation too. */
132
134
  export type PermissionModeCeiling = {
133
135
  readonly current: PermissionMode;
136
+ readonly observed?: PermissionMode;
134
137
  } | PermissionMode;
135
138
  /**
136
139
  * The one resolution of a leg's effective mode. `current` is derived on every read from three inputs —
@@ -170,8 +173,14 @@ export declare class PermissionModeController {
170
173
  * failure goes to {@link PermissionModeArming.onArmFault}; the leg then has no classifier, so its asks go on to the
171
174
  * approval seat as under `default`. */
172
175
  get current(): PermissionMode;
176
+ /** The effective mode read to REPORT it (a hook context), not to decide with it: the word {@link current} answers,
177
+ * reached through the ceilings' own `observed`, so neither this leg nor an ancestor starts its classifier's arming.
178
+ * A decision read is what starts it; an observation that did would arm a leg earlier than its own gate, and so move
179
+ * who answers the leg's next ask. */
180
+ get observed(): PermissionMode;
173
181
  /** The three-member answer as it stands right now. */
174
182
  receipt(): PermissionModeReceipt;
183
+ private receiptOf;
175
184
  /** The ceiling's source moved to another leg (the parent session's next leg); `before` is this leg's receipt under the
176
185
  * previous one. A ceiling fold the move produced — or changed — is announced, as the construction-time fold is. */
177
186
  ceilingRebound(before: PermissionModeReceipt): void;
@@ -6,8 +6,8 @@ import { clampPermissionMode } from "./ask-origin.js";
6
6
  import { argvOf, lexShellText } from "../shell/lex.js";
7
7
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
8
8
  import { createSafeNotifier } from "./safe-notify.js";
9
- export const PERMISSION_MODE_FOLD_REASONS = ["operator_deny", "face_absent", "inherited_ceiling"];
10
- export const ACCEPT_EDITS_FILE_COMMANDS = ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"];
9
+ export const PERMISSION_MODE_FOLD_REASONS = Object.freeze(["operator_deny", "face_absent", "inherited_ceiling"]);
10
+ export const ACCEPT_EDITS_FILE_COMMANDS = Object.freeze(["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"]);
11
11
  const MODE_POLICY_TABLE = {
12
12
  plan: { enablePlanMode: true, writeGate: "plan", fileCommands: [], writeFace: undefined },
13
13
  acceptEdits: { enablePlanMode: false, writeGate: "accept_root", fileCommands: ACCEPT_EDITS_FILE_COMMANDS, writeFace: undefined },
@@ -94,14 +94,16 @@ export class PermissionModeController {
94
94
  get requested() {
95
95
  return this.steers.at(-1)?.word ?? this.requestedBySpec;
96
96
  }
97
- ceilingNow() {
98
- return this.ceiling === undefined ? undefined : typeof this.ceiling === "string" ? this.ceiling : this.ceiling.current;
97
+ ceilingNow(observe) {
98
+ if (this.ceiling === undefined || typeof this.ceiling === "string")
99
+ return this.ceiling;
100
+ return observe ? (this.ceiling.observed ?? this.ceiling.current) : this.ceiling.current;
99
101
  }
100
102
  get declared() {
101
103
  return this.declaration().effective;
102
104
  }
103
- declaration() {
104
- const ceiling = this.ceilingNow();
105
+ declaration(observe = false) {
106
+ const ceiling = this.ceilingNow(observe);
105
107
  if (ceiling === undefined)
106
108
  return { effective: this.requested ?? "default", clamped: false };
107
109
  return clampPermissionMode(this.requested, ceiling);
@@ -117,8 +119,14 @@ export class PermissionModeController {
117
119
  }
118
120
  return effective;
119
121
  }
122
+ get observed() {
123
+ return this.receiptOf(true).effective;
124
+ }
120
125
  receipt() {
121
- const { effective: declared, clamped } = this.declaration();
126
+ return this.receiptOf(false);
127
+ }
128
+ receiptOf(observe) {
129
+ const { effective: declared, clamped } = this.declaration(observe);
122
130
  const requested = this.requested;
123
131
  const base = requested !== undefined ? { requested } : {};
124
132
  const a = this.arming;
@@ -1 +1 @@
1
- export const MODEL_ROLES = ["default", "summarize", "subagent", "team", "synthesize", "advisor", "verifier", "classifier", "consolidate"];
1
+ export const MODEL_ROLES = Object.freeze(["default", "summarize", "subagent", "team", "synthesize", "advisor", "verifier", "classifier", "consolidate"]);
@@ -8,7 +8,7 @@ import { heldValues } from "./tool-name-readings.js";
8
8
  import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
9
9
  import { readShellCommand } from "./shell-lexer.js";
10
10
  import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
11
- export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
11
+ export const RULE_BEHAVIORS = Object.freeze(["deny", "ask", "allow"]);
12
12
  const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
13
13
  export function isRuleBehavior(v) {
14
14
  return RULE_BEHAVIOR_SET.has(v);
@@ -342,7 +342,7 @@ function parsePathRuleContent(text, tool, content, behavior) {
342
342
  }
343
343
  return { rule: { behavior, rule: formatRuleText(content, "path", tool), tool, match: "path", command: content } };
344
344
  }
345
- export const PATH_RULE_BASES = ["cwd", "root", "home"];
345
+ export const PATH_RULE_BASES = Object.freeze(["cwd", "root", "home"]);
346
346
  export const PATH_RULE_BASE_LABEL = {
347
347
  cwd: "the working directory",
348
348
  root: "the project root",
@@ -493,7 +493,7 @@ function hasUnescapedStar(s) {
493
493
  export function ruleAdmitsCommand(rule, command) {
494
494
  return admitsUnder(rule, command, MATCH_READING);
495
495
  }
496
- export const PROGRAM_RUN_REACHES = ["reached", "not_reached", "unreadable"];
496
+ export const PROGRAM_RUN_REACHES = Object.freeze(["reached", "not_reached", "unreadable"]);
497
497
  const REACHED = { reach: "reached" };
498
498
  const NOT_REACHED = { reach: "not_reached" };
499
499
  export function programRunReachOf(rule, command) {
@@ -462,8 +462,8 @@ export async function ruleStoreChecksum(payload) {
462
462
  const { createHash } = await import("node:crypto");
463
463
  return `sha256:${createHash("sha256").update(canonicalize(payload), "utf8").digest("hex")}`;
464
464
  }
465
- export const REMOVAL_LIVENESS_WORDS = ["yes", "no", "unknown"];
466
- export const RULE_WRITE_COMMITTED_WORDS = ["no", "unknown"];
465
+ export const REMOVAL_LIVENESS_WORDS = Object.freeze(["yes", "no", "unknown"]);
466
+ export const RULE_WRITE_COMMITTED_WORDS = Object.freeze(["no", "unknown"]);
467
467
  function resolveRuleWriter(store) {
468
468
  let w;
469
469
  try {
@@ -1,4 +1,4 @@
1
- export const READ_ONLY_FLAG_ARITIES = ["none", "number", "string", "char", "{}", "EOF"];
1
+ export const READ_ONLY_FLAG_ARITIES = Object.freeze(["none", "number", "string", "char", "{}", "EOF"]);
2
2
  export const FLAG_VALUE_ACCEPTS = {
3
3
  none: () => false,
4
4
  number: (value) => /^\d+$/.test(value),
@@ -474,12 +474,12 @@ export const READ_ONLY_COMMAND_TABLE = {
474
474
  },
475
475
  },
476
476
  };
477
- export const READ_ONLY_BARE_PROGRAMS = ["docker ps", "docker images", "cal", "uptime", "cat", "head", "tail", "wc", "stat", "strings", "hexdump", "od", "nl", "id", "uname", "free", "df", "du", "locale", "groups", "nproc", "basename", "dirname", "realpath", "cut", "paste", "tr", "column", "tac", "rev", "fold", "expand", "unexpand", "fmt", "comm", "cmp", "numfmt", "readlink", "diff", "true", "false", "sleep", "which", "type", "expr", "seq", "tsort", "pr"];
478
- export const READ_ONLY_GLOB_PROGRAMS = ["ls", "cat", "head", "tail", "wc", "stat", "grep", "egrep", "fgrep", "diff", "du", "df", "echo", "strings", "hexdump", "od", "nl", "cut", "column", "tr", "tac", "rev", "cmp", "basename", "dirname", "realpath", "readlink", "sha256sum", "sha1sum", "md5sum", "cd"];
477
+ export const READ_ONLY_BARE_PROGRAMS = Object.freeze(["docker ps", "docker images", "cal", "uptime", "cat", "head", "tail", "wc", "stat", "strings", "hexdump", "od", "nl", "id", "uname", "free", "df", "du", "locale", "groups", "nproc", "basename", "dirname", "realpath", "cut", "paste", "tr", "column", "tac", "rev", "fold", "expand", "unexpand", "fmt", "comm", "cmp", "numfmt", "readlink", "diff", "true", "false", "sleep", "which", "type", "expr", "seq", "tsort", "pr"]);
478
+ export const READ_ONLY_GLOB_PROGRAMS = Object.freeze(["ls", "cat", "head", "tail", "wc", "stat", "grep", "egrep", "fgrep", "diff", "du", "df", "echo", "strings", "hexdump", "od", "nl", "cut", "column", "tr", "tac", "rev", "cmp", "basename", "dirname", "realpath", "readlink", "sha256sum", "sha1sum", "md5sum", "cd"]);
479
479
  export const READ_ONLY_EXACT_FORMS = [["claude", "-h"], ["claude", "--help"], ["node", "-v"], ["node", "--version"], ["python", "--version"], ["python3", "--version"], ["ip", "addr"]];
480
- export const READ_ONLY_BARE_ONLY = ["pwd", "whoami", "alias"];
481
- export const FIND_ACTION_PRIMARIES = ["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from"];
482
- export const FIND_VALUE_PRIMARIES = ["-name", "-iname", "-path", "-ipath", "-lname", "-ilname", "-regex", "-iregex", "-wholename", "-iwholename", "-samefile", "-newer", "-anewer", "-cnewer", "-mnewer", "-perm", "-user", "-group", "-uid", "-gid", "-size", "-type", "-xtype", "-fstype", "-inum", "-links", "-used", "-context", "-amin", "-cmin", "-mmin", "-atime", "-ctime", "-mtime", "-mindepth", "-maxdepth", "-printf", "-regextype", "-D", "-f", "-flags", "-Bnewer", "-Btime", "-Bmin", "-files0-from", "-xattrname"];
480
+ export const READ_ONLY_BARE_ONLY = Object.freeze(["pwd", "whoami", "alias"]);
481
+ export const FIND_ACTION_PRIMARIES = Object.freeze(["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from"]);
482
+ export const FIND_VALUE_PRIMARIES = Object.freeze(["-name", "-iname", "-path", "-ipath", "-lname", "-ilname", "-regex", "-iregex", "-wholename", "-iwholename", "-samefile", "-newer", "-anewer", "-cnewer", "-mnewer", "-perm", "-user", "-group", "-uid", "-gid", "-size", "-type", "-xtype", "-fstype", "-inum", "-links", "-used", "-context", "-amin", "-cmin", "-mmin", "-atime", "-ctime", "-mtime", "-mindepth", "-maxdepth", "-printf", "-regextype", "-D", "-f", "-flags", "-Bnewer", "-Btime", "-Bmin", "-files0-from", "-xattrname"]);
483
483
  export const FIND_NEWER_PRIMARY = /^-newer[aBcm][aBcmt]$/;
484
- export const READ_ONLY_ENV_NAMES = ["GOEXPERIMENT", "GOOS", "GOARCH", "CGO_ENABLED", "GO111MODULE", "RUST_BACKTRACE", "RUST_LOG", "NODE_ENV", "PYTHONUNBUFFERED", "PYTHONDONTWRITEBYTECODE", "PYTEST_DISABLE_PLUGIN_AUTOLOAD", "PYTEST_DEBUG", "ANTHROPIC_API_KEY", "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "LC_TIME", "CHARSET", "TERM", "COLORTERM", "NO_COLOR", "FORCE_COLOR", "TZ", "LS_COLORS", "LSCOLORS", "GREP_COLOR", "GREP_COLORS", "GCC_COLORS", "TIME_STYLE", "BLOCK_SIZE", "BLOCKSIZE", "COLUMNS", "LINES", "CLICOLOR", "CLICOLOR_FORCE", "CI", "DEBIAN_FRONTEND", "GIT_TERMINAL_PROMPT"];
485
- export const XARGS_READ_ONLY_TARGETS = ["echo", "printf", "wc", "grep", "egrep", "fgrep", "head", "tail"];
484
+ export const READ_ONLY_ENV_NAMES = Object.freeze(["GOEXPERIMENT", "GOOS", "GOARCH", "CGO_ENABLED", "GO111MODULE", "RUST_BACKTRACE", "RUST_LOG", "NODE_ENV", "PYTHONUNBUFFERED", "PYTHONDONTWRITEBYTECODE", "PYTEST_DISABLE_PLUGIN_AUTOLOAD", "PYTEST_DEBUG", "ANTHROPIC_API_KEY", "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "LC_TIME", "CHARSET", "TERM", "COLORTERM", "NO_COLOR", "FORCE_COLOR", "TZ", "LS_COLORS", "LSCOLORS", "GREP_COLOR", "GREP_COLORS", "GCC_COLORS", "TIME_STYLE", "BLOCK_SIZE", "BLOCKSIZE", "COLUMNS", "LINES", "CLICOLOR", "CLICOLOR_FORCE", "CI", "DEBIAN_FRONTEND", "GIT_TERMINAL_PROMPT"]);
485
+ export const XARGS_READ_ONLY_TARGETS = Object.freeze(["echo", "printf", "wc", "grep", "egrep", "fgrep", "head", "tail"]);
@@ -1,5 +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
+ export const RETENTION_CASCADE_DOMAINS = Object.freeze(["checkpoints", "session-trees", "orphan-tool-results"]);
2
+ export const RETENTION_CASCADE_EXEMPT = Object.freeze(["session-pollution", "session-capture-optout", "lineage.json"]);
3
3
  function codedError(code, message) {
4
4
  const e = new Error(message);
5
5
  e.code = code;
@@ -1,5 +1,5 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
- import { extractErrorCode, isProviderEchoShape, stripErrorCodePrefix } from "../../brain/errors.js";
2
+ import { extractErrorCode, isProviderEchoShape, refusedTheRequest, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
4
  import { haltConsumed } from "./halt-attribution.js";
5
5
  import { providerReportedModel } from "../message-utils.js";
@@ -124,12 +124,17 @@ export function terminalCauseOf(final, stats, flags) {
124
124
  else if (final.stopReason === "error") {
125
125
  let message = final.errorMessage ?? "model error";
126
126
  let code;
127
- if (final.isApiErrorMessage === true) {
127
+ const apiError = readMember(final, "isApiErrorMessage") === true;
128
+ const apiStatus = readMember(final, "apiErrorStatus");
129
+ const apiRequestId = readMember(final, "requestId");
130
+ const apiCode = readMember(final, "apiErrorCode");
131
+ if (apiError) {
128
132
  apiFailure = {
129
- ...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
130
- ...(typeof final.requestId === "string" && isProviderEchoShape(final.requestId) ? { requestId: final.requestId } : {}),
133
+ ...(apiStatus !== undefined ? { status: apiStatus } : {}),
134
+ ...(typeof apiRequestId === "string" && isProviderEchoShape(apiRequestId) ? { requestId: apiRequestId } : {}),
131
135
  };
132
136
  }
137
+ let providerNamed = true;
133
138
  const lifted = extractErrorCode(message);
134
139
  if (lifted) {
135
140
  code = lifted;
@@ -137,14 +142,19 @@ export function terminalCauseOf(final, stats, flags) {
137
142
  }
138
143
  else if (flags.gitCoreOverBudget) {
139
144
  code = "irreducible_core_over_budget";
145
+ providerNamed = false;
140
146
  }
141
147
  else if (flags.conflict) {
142
148
  code = "conflict";
149
+ providerNamed = false;
143
150
  }
144
151
  else {
145
152
  code = "provider.error";
146
153
  }
147
- terminal = { kind: "failed", code, message };
154
+ const detail = apiError && providerNamed && typeof apiStatus === "number" && refusedTheRequest(apiStatus)
155
+ ? { kind: "provider_rejected", status: apiStatus, ...(typeof apiCode === "string" && isProviderEchoShape(apiCode) ? { code: apiCode } : {}) }
156
+ : undefined;
157
+ terminal = { kind: "failed", code, message, ...(detail !== undefined ? { detail } : {}) };
148
158
  }
149
159
  else {
150
160
  terminal = { kind: "completed" };
@@ -18,7 +18,7 @@ function sameRouteQuietly(a, b) {
18
18
  return false;
19
19
  }
20
20
  }
21
- export const CLASSIFIER_ROLE_DEFAULT_REASONS = ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
21
+ export const CLASSIFIER_ROLE_DEFAULT_REASONS = Object.freeze(["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"]);
22
22
  export function classifierSeatModel(args) {
23
23
  const { main } = args;
24
24
  const deps = normalizeSeatDeps(args.deps);
@@ -188,6 +188,8 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
188
188
  * vouches for no name and the seat stays empty (absence = unknown); a block AT the bound reaches no seat at
189
189
  * all, since it becomes an ask and the disposition then names that resolution.
190
190
  */
191
+ /** The provenance sentence an inherited card carries when the ancestor's ask brought no words of its own — the one spelling the request mint and the frame join share. */
192
+ export declare const inheritedAskFallbackMessage: (toolName: string) => string;
191
193
  export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
192
194
  autoMode: InheritedAutoMode | undefined;
193
195
  /** The leg's live permission mode (the gate's seat): under `dontAsk` / `bypassPermissions` / `plan` the decider is not
@@ -1,3 +1,4 @@
1
+ import { markTightenMessage } from "../safety-axis-vocab.js";
1
2
  import { classifierParseFailureDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
2
3
  import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
3
4
  import { classifierUnavailableArm } from "../classifier-unavailable-arm.js";
@@ -79,9 +80,12 @@ export function inheritedAskCarry(judged, liveApprover, tracker) {
79
80
  ...fallbackCarry(judged.fallback, liveApprover, tracker),
80
81
  };
81
82
  }
83
+ export const inheritedAskFallbackMessage = (toolName) => `approval required for "${toolName}" (inherited parent policy)`;
82
84
  export async function judgeInheritedClassifier(opts) {
83
- const { autoMode, ask, req } = opts;
84
- const facts = opts.facts ?? INHERITED_STATION_FACTS;
85
+ const { autoMode, req } = opts;
86
+ const facts = opts.facts ?? INHERITED_STATION_FACTS, m = facts.mandateMarks;
87
+ const frames = m.shellGated === true ? [] : [...(m.egress === true ? [markTightenMessage(req.toolName, "egress")] : []), ...(m.irreversibility === "always" ? [markTightenMessage(req.toolName, "irreversible")] : [])];
88
+ const ask = (frames.length > 0 ? { ...opts.ask, message: [opts.ask.message ?? inheritedAskFallbackMessage(req.toolName), ...frames].join(" — ") } : opts.ask);
85
89
  const origin = askOriginOf(ask, facts);
86
90
  const classification = classifyAsk(ask, facts);
87
91
  const classifierStandsDown = () => {
@@ -20,10 +20,8 @@ export declare function askGrantCovers(granted: readonly AskClass[], asked: read
20
20
  export declare function askGrantShapeOf(args: unknown): string | undefined;
21
21
  /**
22
22
  * Duplicate-frame collapse, the CONSUME half: an inherited layer already resolved THIS call as a clean allow at the
23
- * SAME approver function, on EXACTLY the byte-shape this caller-slot ask now presents, for a question that covers this
24
- * one — asking again is the 2-frame UX debt, not a second authority. Guards, all mismatches re-ask (⇒ `undefined`):
25
- * · SOURCE: only a caller-slot POLICY ask may reuse — a safety tighten (`decisionReason:"safety"`), a hook-promoted
26
- * ask ("hook") or a classifier ask carries a DIFFERENT authority and keeps its frame.
23
+ * SAME approver function, on EXACTLY the byte-shape this ask now presents, for a question that covers this one — asking
24
+ * again is the 2-frame UX debt, not a second authority. Guards, all mismatches re-ask (⇒ `undefined`):
27
25
  * · IDENTITY: the approver FUNCTION reference (never a name), compared through `askApproverIdentity`: a delegated
28
26
  * child's approver is the ancestor's own wrapped with provenance — a different function object, the same operator
29
27
  * (the wrapper grants no authority of its own); identity answers only "same consultation".
@@ -31,7 +29,10 @@ export declare function askGrantShapeOf(args: unknown): string | undefined;
31
29
  * `presentedInput` — the same binding as a main-gate approval (shown == executed, by construction).
32
30
  * · CLASS: the gate's stamped `classes` is covered by the set the person answered ({@link askGrantCovers}); a class
33
31
  * the gate's own judges add (a protected target, an out-of-root read) was not on the card. Not covered ⇒ the gate
34
- * asks its own question and the grant stays (it answers what it answered; the call-site sweep ends it).
32
+ * asks its own question and the grant stays (it answers what it answered; the call-site sweep ends it). The class
33
+ * set is also the AUTHORITY test (a hook's ask carries `hook`, a fallback `denial_limit_fallback`, a marked tool
34
+ * `operator_forced`, a protected write `write_protection`, an out-of-root read `probe`; the shell seat's classify-tier
35
+ * hesitation is `ordinary`, the one question a yes on the same bytes answers) — the ask's `decisionReason` is not read.
35
36
  * · LEDGER: the ONE real consultation (timed on the wrapper arm) is booked here — one frame happened, one is booked.
36
37
  * The grant lives within one gate evaluation (consumed on reuse, swept at the call site): a REOPENED checkpoint's
37
38
  * re-adjudication never sees a stale grant.
@@ -1,3 +1,4 @@
1
+ import { askMandateMember } from "../ask-origin.js";
1
2
  import { primaryActivityArg } from "../arg-summary.js";
2
3
  import { askApproverIdentity, carryEngineAttestations } from "../tool-policy.js";
3
4
  export function askGrantCovers(granted, asked) {
@@ -12,13 +13,13 @@ export function askGrantShapeOf(args) {
12
13
  }
13
14
  }
14
15
  export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision, req) {
15
- if (decision.decisionReason !== undefined && decision.decisionReason !== "rule")
16
- return undefined;
17
16
  const grant = grants.get(req.toolCallId);
18
17
  if (grant === undefined || askApproverIdentity(grant.approver) !== askApproverIdentity(onAsk) || grant.argsJson !== askGrantShapeOf(req.args))
19
18
  return undefined;
20
19
  if (!askGrantCovers(grant.classes, decision.action === "ask" ? decision.classes : undefined))
21
20
  return undefined;
21
+ if (decision.action === "ask" && askMandateMember(decision).mandate === "operator_always")
22
+ return undefined;
22
23
  grants.delete(req.toolCallId);
23
24
  humanReviewRef.count += 1;
24
25
  humanReviewRef.totalWaitMs += grant.waitMs;