@sema-agent/core 5.64.0 → 6.0.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 (165) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +113 -4
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +48 -8
  35. package/dist/core/hooks.js +39 -22
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +6 -0
  45. package/dist/core/memory-engine/engine.d.ts +329 -15
  46. package/dist/core/memory-engine/engine.js +364 -34
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +80 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/remote-env.d.ts +3 -3
  71. package/dist/core/retention-policy.d.ts +9 -0
  72. package/dist/core/retention-policy.js +5 -2
  73. package/dist/core/retention.d.ts +13 -2
  74. package/dist/core/runner/assemble-result.d.ts +19 -1
  75. package/dist/core/runner/assemble-result.js +17 -2
  76. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  77. package/dist/core/runner/compaction-call-options.js +3 -0
  78. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  79. package/dist/core/runner/memory-capture-optout.js +53 -0
  80. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  81. package/dist/core/runner/prepare-config-doors.js +16 -0
  82. package/dist/core/runner/prepare-hands-readface.d.ts +119 -5
  83. package/dist/core/runner/prepare-hands-readface.js +103 -8
  84. package/dist/core/runner/prepare-memory.d.ts +88 -0
  85. package/dist/core/runner/prepare-memory.js +306 -25
  86. package/dist/core/runner/prepare-task.d.ts +156 -5
  87. package/dist/core/runner/prepare-task.js +488 -98
  88. package/dist/core/runner/runtask.d.ts +27 -20
  89. package/dist/core/runner/runtask.js +283 -99
  90. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  91. package/dist/core/runner/session-file-state-replay.js +52 -1
  92. package/dist/core/runner/tool-disclosure.js +2 -1
  93. package/dist/core/runner/turn-attachments.d.ts +22 -12
  94. package/dist/core/session-store.d.ts +1 -1
  95. package/dist/core/session-store.js +6 -1
  96. package/dist/core/session.d.ts +34 -1
  97. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  98. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  99. package/dist/core/stub-env.d.ts +4 -0
  100. package/dist/core/stub-env.js +1 -0
  101. package/dist/core/task-registry-shared.js +30 -2
  102. package/dist/core/tool-errors.js +1 -0
  103. package/dist/core/tool-policy.d.ts +172 -1
  104. package/dist/core/tool-policy.js +32 -1
  105. package/dist/core/tool-result-store.js +2 -1
  106. package/dist/core/trace.d.ts +24 -0
  107. package/dist/core/types.d.ts +875 -97
  108. package/dist/core/types.js +4 -3
  109. package/dist/core/untrusted-text.d.ts +1 -1
  110. package/dist/core/untrusted-text.js +8 -0
  111. package/dist/core/workflow-run-store-contract.js +17 -0
  112. package/dist/core/workflow-run-store.d.ts +20 -0
  113. package/dist/core/workflow-run-store.js +1 -0
  114. package/dist/engine/compaction/compaction.d.ts +88 -10
  115. package/dist/engine/compaction/compaction.js +109 -30
  116. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  117. package/dist/engine/execution-env/node-execution-env.js +28 -0
  118. package/dist/engine/harness/agent-harness.d.ts +52 -1
  119. package/dist/engine/harness/agent-harness.js +36 -1
  120. package/dist/engine/harness/types.d.ts +44 -1
  121. package/dist/engine/llm/types.d.ts +50 -4
  122. package/dist/engine/loop/agent-loop.d.ts +5 -1
  123. package/dist/engine/loop/agent-loop.js +25 -0
  124. package/dist/engine/loop/types.d.ts +19 -0
  125. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  126. package/dist/engine/session/session.js +1 -1
  127. package/dist/index.d.ts +18 -8
  128. package/dist/index.js +14 -6
  129. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  130. package/dist/orchestration/run-workflow-tool.js +22 -3
  131. package/dist/orchestration/workflow-governance.d.ts +59 -1
  132. package/dist/orchestration/workflow-governance.js +61 -8
  133. package/dist/orchestration/workflow-meta.d.ts +4 -2
  134. package/dist/orchestration/workflow-primitives.js +56 -13
  135. package/dist/orchestration/workflow-types.d.ts +112 -1
  136. package/dist/orchestration/workflow-types.js +2 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +182 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +15 -7
  141. package/dist/prompts/default.js +3 -0
  142. package/dist/stores/file/file-history-store.d.ts +368 -0
  143. package/dist/stores/file/file-history-store.js +1248 -0
  144. package/dist/stores/file/index.d.ts +22 -13
  145. package/dist/stores/file/index.js +4 -4
  146. package/dist/stores/file/permission-rule-store.js +1 -0
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +159 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -8,6 +8,10 @@ import { type ExecResult, type ExecutionEnv, ExecutionError, FileError, type Fil
8
8
  */
9
9
  export declare class StubExecutionEnv implements ExecutionEnv {
10
10
  cwd: string;
11
+ /** design/380 O9a — declared on the class so the prepare fold can read the optional interface
12
+ * member off a `ExecutionEnv | StubExecutionEnv` union; the no-I/O stub produces no target
13
+ * content, so it never declares (always `undefined` — the trusted-side default). */
14
+ readonly externalContentTarget?: boolean;
11
15
  constructor(cwd?: string);
12
16
  private fsErr;
13
17
  absolutePath(path: string): Promise<Result<string, FileError>>;
@@ -1,6 +1,7 @@
1
1
  import { ExecutionError, FileError, err, } from "../internal/harness.js";
2
2
  export class StubExecutionEnv {
3
3
  cwd;
4
+ externalContentTarget;
4
5
  constructor(cwd = "/") {
5
6
  this.cwd = cwd;
6
7
  }
@@ -166,6 +166,32 @@ function workflowAgentRow(a, ordinal) {
166
166
  ...(a.replayed === true ? { replayed: true } : {}),
167
167
  };
168
168
  }
169
+ function budgetOvershootNote(overshoot) {
170
+ if (overshoot === undefined)
171
+ return "";
172
+ const unsettled = overshoot.unsettledTokens ?? 0;
173
+ const total = overshoot.spentTokens + unsettled;
174
+ return (` token budget OVERSHOT: this run spent ${total.toLocaleString()} total tokens against a ${overshoot.budgetTokens.toLocaleString()} ceiling ` +
175
+ `(over by ${(total - overshoot.budgetTokens).toLocaleString()}` +
176
+ `${unsettled > 0 ? `, of which ${unsettled.toLocaleString()} was observed on agents still in flight at the terminal and never settled` : ""}).`);
177
+ }
178
+ function timeoutInterruptionNote(interruption) {
179
+ if (interruption === undefined)
180
+ return "";
181
+ return (` INTERRUPTED by the workflow's total timeout (${interruption.timeoutMs.toLocaleString()}ms): when the deadline fired ` +
182
+ `${interruption.agentsCompleted} agent(s) had completed, ${interruption.agentsFailed} had failed, and ` +
183
+ `${interruption.agentsInFlight} were still in flight — the failed run status is the deadline's, not a verdict on those agents.` +
184
+ (interruption.approvalWaitedMs !== undefined
185
+ ? ` The longest single agent wait on a human approval reported for this run was ${interruption.approvalWaitedMs.toLocaleString()}ms` +
186
+ (interruption.approvalWaitedMs > interruption.timeoutMs
187
+ ? ` — MORE than the ${interruption.timeoutMs.toLocaleString()}ms window itself, so the deployment's approval clock is` +
188
+ ` counting time this run did not (an earlier leg's wait carried forward, or a mis-scaled figure). Read it as` +
189
+ ` "waiting dominated", not as a slice of this window.`
190
+ : ` — that much of the ${interruption.timeoutMs.toLocaleString()}ms window was one agent BLOCKED on a person` +
191
+ ` rather than working (the longest single wait, not a total).`) +
192
+ ` Raising the timeout only helps if that approval gets answered.`
193
+ : ""));
194
+ }
169
195
  export function formatWorkflowRun(run) {
170
196
  const summary = summarizeWorkflowRun(run);
171
197
  const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length;
@@ -223,7 +249,7 @@ export function formatWorkflowRun(run) {
223
249
  : {}),
224
250
  }
225
251
  : {}),
226
- note: run.status === "running"
252
+ note: (run.status === "running"
227
253
  ? "still running — poll again shortly."
228
254
  : run.status === "completed"
229
255
  ? run.result !== undefined
@@ -231,7 +257,9 @@ export function formatWorkflowRun(run) {
231
257
  : "completed — this run predates result persistence; its result was delivered on the completion notification."
232
258
  : finishedAgents.length > 0
233
259
  ? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.`
234
- : "failed — see error.",
260
+ : "failed — see error.") +
261
+ timeoutInterruptionNote(summary.timeoutInterruption) +
262
+ budgetOvershootNote(summary.budgetOvershoot),
235
263
  }),
236
264
  details: {
237
265
  task_id: run.id,
@@ -72,6 +72,7 @@ const EXACT_CODE_CLASS = new Map([
72
72
  ["limits.max_walltime_exceeded", "limit"],
73
73
  ["memory.admission_required", "config"],
74
74
  ["memory.admission_denied", "config"],
75
+ ["memory.capture_optout_denied", "config"],
75
76
  ["env.lifetime_expired", "config"],
76
77
  ]);
77
78
  const DOTTED_PREFIX_CLASS = [
@@ -216,6 +216,15 @@ export type PermissionResult = {
216
216
  * understands (see {@link import("./checkpoint-store.js").ProbeCause}). Engine-stamped and
217
217
  * validated at the irreversibility tighten; same display-only posture as the prose sibling. */
218
218
  probeCause?: import("./checkpoint-store.js").ProbeCause;
219
+ /** #502 (additive): the reversibility probe declared this demotion STRUCTURAL — see
220
+ * {@link import("./types.js").ReversibilityVerdict.mandated}. Unlike its two `probe*` neighbours
221
+ * this is NOT display metadata: it joins the #144 mandate family, so the persisted-rule lane
222
+ * discloses a matching allow rule as shadowed instead of clearing the ask, and the card's
223
+ * "stop asking me this" offer is withheld. ENGINE-STAMPED at the irreversibility tighten, which
224
+ * constructs this decision wholesale — a policy that self-declares it is overwritten there, and
225
+ * in the one direction the stamp cannot reach (a caller composing its own ask) the claim is
226
+ * tightening-only: it can make an ask outlive a rule, never turn one into an allow. */
227
+ probeMandated?: true;
219
228
  /** An EXPLICIT `ask` permission rule matched this call (design/127 DSL — the CC `alwaysAskRules`
220
229
  * shape); carries the matched rule's text. Present ⇔ a rule someone WROTE says "ask about this",
221
230
  * never for a `defaultAction:"ask"` fallback (an unmatched call is default-closed posture, not a
@@ -457,6 +466,28 @@ export declare function checkToolPolicyProjection(projection: Pick<ToolPolicyPro
457
466
  export type ConstraintChainEntryMeta = {
458
467
  /** True iff the layer carried a frozen auto-mode classifier (`autoMode`). */
459
468
  autoModeArmed?: true;
469
+ /**
470
+ * #503 — the layer's serializable auto-mode ARMING RECIPE: the criteria half of the frozen classifier
471
+ * (assembly inputs + knobs + the deployment's settings epoch), recorded so a CROSS-PROCESS redemption
472
+ * can rebuild an equivalent decider over its own fresh model leg
473
+ * ({@link import("./auto-mode-arming.js").rebuildAutoModeDecider}) instead of answering `unavailable`
474
+ * forever and parking the same approval for a human on every redemption.
475
+ *
476
+ * OPT-IN and ADDITIVE: present only when the arming deployment asked for it (`RunnerDeps.autoMode.
477
+ * persistArming`), so an unchanged deployment mints byte-identical entries and every pre-#503 row
478
+ * (and every row minted without the opt-in) reads as absence = the pre-#503 behavior. Absence is also
479
+ * what an OPEN BREAKER produces — see {@link constraintChainEntryOfLayer}.
480
+ *
481
+ * The digest binds it: a re-supply that DROPS the recorded recipe, or hands back a DIFFERENT one, is
482
+ * rejected pre-CAS — so the re-supply contract is exactly "hand back what the row carries" (a redeeming
483
+ * host reads the recipe off this field and passes it straight through). The verify side takes the
484
+ * PRESENCE of this field from the row, never from the re-supplied layer's live classifier health, so a
485
+ * row that recorded no recipe does not refuse a re-supply that offers one — the reasoning, and why that
486
+ * direction cannot be recovered, is recorded on {@link constraintChainEntryOfSuppliedLayer}.
487
+ * `checkToolPolicyProjection` never reads it: like the other metadata here it exists to be digested,
488
+ * not to execute.
489
+ */
490
+ autoModeArming?: import("./auto-mode-arming.js").AutoModeArmingRecipe;
460
491
  durableMandate?: true;
461
492
  contentMandate?: true;
462
493
  };
@@ -469,6 +500,105 @@ export type ConstraintChainEntry = ConstraintChainEntryMeta & ({
469
500
  /** Project one policy into its chain entry (opaque placeholder when it exports no projection),
470
501
  * carrying the layer's decision-chain metadata posture for the digest (round 12). */
471
502
  export declare function constraintChainEntryOf(policy: ToolPolicy, meta?: ConstraintChainEntryMeta): ConstraintChainEntry;
503
+ /**
504
+ * One inherited-chain LAYER as both chain-entry mint sites see it — structurally an
505
+ * `InheritedGate.parentConstraints[]` element, typed here structurally so this module (which owns the
506
+ * digest) does not depend on the runner's shapes.
507
+ */
508
+ export interface ConstraintChainLayerView {
509
+ policy: ToolPolicy;
510
+ autoMode?: {
511
+ decider: {
512
+ breakerOpen: () => boolean;
513
+ consecutiveFailures?: () => number;
514
+ };
515
+ arming?: unknown;
516
+ };
517
+ durableMandate?: boolean;
518
+ contentMandate?: boolean;
519
+ }
520
+ /**
521
+ * The WRITE-side mint: the chain entry a durable PARK records for a live inherited layer.
522
+ *
523
+ * #503 safety bound ① lives HERE, at the persistence point rather than at arming time: the recipe is
524
+ * carried only while the layer's breaker is UNTRIPPED AND UNTOUCHED. An open breaker is the ancestor
525
+ * session's one-way "fell back to non-auto" latch — its arming has been withdrawn, and handing that
526
+ * arming to a redemption would let the redeemed leg run auto-classified where the ancestor no longer does
527
+ * (a redemption WIDER than the ancestor it inherits). The latch is read at the moment the row is written,
528
+ * so an ancestor whose classifier died mid-run parks WITHOUT a recipe even though it was armed when the
529
+ * chain was assembled.
530
+ *
531
+ * `breakerOpen()` alone is NOT the whole latch, and a closed-only test left a real widening: a rebuilt
532
+ * decider starts a FRESH failure streak, so an ancestor sitting at 2 failures out of 3 — one bad
533
+ * classification from dropping the whole session to non-auto — would hand a redemption the full budget
534
+ * again, and the redeemed leg stays auto-classified through failures the ancestor would not have
535
+ * survived. So the streak must read ZERO as well. A decider that does not expose it at all is UNKNOWN
536
+ * state, and unknown takes the same arm as tripped: nothing is recorded (which is why a host that hand
537
+ * rolls a decider cannot use the persisted-arming path — it must mint through `createAutoModeDecider`,
538
+ * on both the arming and the redeeming side).
539
+ *
540
+ * Every read here is guarded, and every failure to read is treated as "withdrawn" — the opposite default
541
+ * from the gate's demote-to-ask breaker reader, and for the same reason both are right: there, an
542
+ * unreadable latch must not suppress a tighten; here, it must not license a carry-forward.
543
+ *
544
+ * ⚠️ WRITE SIDE ONLY. This gate reads MUTABLE state (a one-way latch, plus a streak that both grows and
545
+ * resets to zero on any healthy classification). The resume re-supply must NOT re-run it — it samples a
546
+ * different instant, and the two instants disagree for perfectly ordinary reasons: an ancestor keeps
547
+ * running while its delegated child sits parked awaiting a human. See
548
+ * {@link constraintChainEntryOfSuppliedLayer} for the verify side and the full account.
549
+ *
550
+ * ⚖️ WHAT THIS BOUND DOES NOT DO (recorded, precise shape). A withdrawn arming leaves the entry at
551
+ * `autoModeArmed: true` with no recipe — digest-identical to an armed layer whose deployment never opted
552
+ * in. So the digest does not, and cannot, stop a re-supplier from handing back a FRESH healthy decider of
553
+ * its own: that is the pre-existing re-supply trust posture (see `resumeStream`'s contract — a caller
554
+ * able to invoke it already holds full Runner authority, and the chain checks catch shape MISTAKES, not
555
+ * a hostile trusted caller), and it is unchanged here. What this bound governs is what CORE hands
556
+ * forward: a withdrawn ancestor's CRITERIA never travel, so a redemption cannot reconstruct the
557
+ * ancestor's classifier from the row. For a fleet running `persistArming` everywhere, the pair
558
+ * (`autoModeArmed` present, `autoModeArming` absent) is itself the readable signal that the ancestor's
559
+ * arming was withdrawn or unrecordable, and such a host should re-supply a refusing decider rather than a
560
+ * fresh one. Making the WITHDRAWAL itself digest-bound is a coherent stronger design — it needs a new
561
+ * marker plus a new re-supply obligation to reproduce it (a withdrawn row carries no recipe, so the
562
+ * marker cannot be derived from one), which is a scope decision for this seam's owner, not a silent
563
+ * mechanism change.
564
+ */
565
+ export declare function constraintChainEntryOfLayer(layer: ConstraintChainLayerView): ConstraintChainEntry;
566
+ /**
567
+ * The VERIFY-side mint: the entry a resume's RE-SUPPLIED layer must reproduce for the row it is
568
+ * redeeming. Same body as the park mint, with the arming carry taken from the ROW rather than from the
569
+ * layer's live classifier health — so the digest equality is over bytes the row itself recorded, and
570
+ * over nothing that can move while the row sits parked.
571
+ *
572
+ * WHY THE CARRY CANNOT BE RE-DERIVED HERE. Safety bound ① (see {@link constraintChainEntryOfLayer}) is a
573
+ * statement about the ancestor at the instant the row was WRITTEN. Re-running it against the same live
574
+ * layer at redemption time samples a different instant, and both directions of the disagreement strand an
575
+ * approval a human already granted:
576
+ * - the ancestor was healthy at the park (the row carries the recipe) and its breaker opened while the
577
+ * child sat parked ⇒ the re-supply mints WITHOUT the recipe, the digests differ, and because the
578
+ * latch is ONE-WAY the row is refused `resume.parent_constraint_mismatch` forever;
579
+ * - the ancestor was one failure into its streak at the park (the row carries no recipe) and the streak
580
+ * cleared on its next healthy classification ⇒ the re-supply mints WITH a recipe the row never
581
+ * carried, same refusal.
582
+ * Neither needs a host mistake: the ancestor and its siblings keep running while the child is parked, and
583
+ * the same-process `resume()` verb re-supplies the very same live layer objects out of the Runner's own
584
+ * parked-constraint registry. Nothing about the row changed; only a classifier's health did.
585
+ *
586
+ * WHAT THIS COSTS, NAMED. A row that recorded NO recipe no longer refuses a re-supply that hands one
587
+ * back. That is not a tunable — the re-supply side literally cannot tell "the row withdrew a recipe this
588
+ * same live layer still declares" (the second bullet above, a legitimate redemption) from "a re-supplier
589
+ * is putting a recipe onto a row that withdrew one": both are the identical pair of inputs. It is also
590
+ * thinner than it looks. The recipe is not an enforcement object — the DECIDER classifies, and the
591
+ * re-supply trust posture already permits handing back a fresh healthy one, which is the capability that
592
+ * would matter. What the row's absence still means is unchanged and is the property bound ① was written
593
+ * for: a withdrawn ancestor's criteria never travel ON THE ROW, so no redemption can reconstruct that
594
+ * ancestor's classifier from the checkpoint.
595
+ *
596
+ * WHAT THE DIGEST STILL BINDS. A row that DID record a recipe must have that recipe handed back: the
597
+ * carry is on, so a re-supply that drops the arming, or hands back a different one, mints a different
598
+ * entry and is refused pre-CAS exactly as before. That is the whole of the documented re-supply contract
599
+ * — "hand back what the row carries".
600
+ */
601
+ export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintChainLayerView, recorded: ConstraintChainEntry | undefined): ConstraintChainEntry;
472
602
  /**
473
603
  * Content digest over an ordered constraint chain (F-012 L1) — the durable half's anti-tamper anchor.
474
604
  * Binds the chain TOPOLOGY (order, layer boundaries, opaque placeholders) and each projection's full
@@ -900,7 +1030,10 @@ export interface AskRequest {
900
1030
  * substitution yields NO offer, which is the honest answer rather than an option that would be
901
1031
  * refused on redemption) AND the ask is one a persisted rule could actually clear — a mandated ask
902
1032
  * (operator shellGate:"always", the tool's own egress/irreversibility marks, a
903
- * `requiresRealApproval` demand) and an ask carrying {@link persistedRuleShadowed} offer none.
1033
+ * `requiresRealApproval` demand, or #502, the one PER-CALL member of that family — a demotion the
1034
+ * call's own reversibility probe declared structural, which for the built-in shell probe means a read
1035
+ * outside the session's allowed directories) and an ask carrying {@link persistedRuleShadowed} offer
1036
+ * none.
904
1037
  *
905
1038
  * CONTRACT — see {@link import("./permission-rule-model.js").RuleOffer} for the closed union, the
906
1039
  * per-element parse/degrade rules and the original-index-keying obligation. Array order is display
@@ -919,6 +1052,44 @@ export interface AskRequest {
919
1052
  * from it across a coverage change — harmlessly, since the record is what gets confirmed.
920
1053
  */
921
1054
  readonly ruleOffers?: readonly import("./permission-rule-model.js").RuleOffer[];
1055
+ /**
1056
+ * #490 修② — WHY {@link ruleOffers} is absent, when the rule-offer lane is in play and has nothing
1057
+ * to give. A CLOSED set, mutually exclusive with {@link ruleOffers} (never both, never neither once
1058
+ * the lane is in play):
1059
+ * · `"mandated"` — no persisted rule could ever clear this ask: an org/governance
1060
+ * `requiresRealApproval` demand, an operator `shellGate:"always"` or the tool's own
1061
+ * egress/irreversibility marks, a hook-raised ask, an ask whose authority is an ANCESTOR's
1062
+ * approver rather than this task's rule lane, or (#502) a demotion this CALL's reversibility
1063
+ * probe declared structural — for the built-in shell probe, a read outside the directories this
1064
+ * session declared. A surface must NOT point at rule-writing here — "allow rules silence the
1065
+ * classifier's questions, never a mandated one", so a rule minted in answer to this line would
1066
+ * never take effect where it was minted. It must not point at a directory GRANT either: the
1067
+ * out-of-root arm has no clearing configuration in this version, and confirming the call is the
1068
+ * whole of what a person can do about it;
1069
+ * · `"shadowed"` — a rule the person already wrote is speaking and does not clear this ask (a
1070
+ * matching allow rule that the lane refuses to honor here, or their own explicit `ask` rule);
1071
+ * · `"lane_cannot_speak"` — the lane is armed and this ask IS clearable, but the offer lane
1072
+ * produced no rule for this command. Usually because the grammar has no text for it (a
1073
+ * substitution, a subshell, a backgrounded process, or a simple command carrying a redirection —
1074
+ * a compound is judged per segment, so this arm means EVERY segment was unspeakable, since one
1075
+ * dirty segment among clean ones yields a batch instead). It also covers the narrower case where a
1076
+ * text EXISTS but the card's own array-order contract declines to present it alone (a simple
1077
+ * command whose exact form the validator refuses offers nothing rather than promoting a lone
1078
+ * prefix to index 0 — see `suggestRulesForCommand`). The seat says "this card has no rule to
1079
+ * offer you", which is true of both; it does not claim the grammar is empty, and a surface must
1080
+ * not word it as though no rule could ever be written for the command.
1081
+ *
1082
+ * ABSENT is not a claim, exactly as {@link previewWithheld}'s absence is not: it also covers the
1083
+ * ordinary "there are offers" case and the three STRUCTURAL doors that answer with silence on
1084
+ * purpose (no rule lane wired, a tool the lane does not speak for, a task that cannot hold a rule at
1085
+ * all) — a reason line on every ask of a deployment that never had the feature would be noise about
1086
+ * a lane that does not exist. Read presence, never absence.
1087
+ *
1088
+ * ADVISORY disclosure metadata, never adjudication input: it changes nothing about what is allowed,
1089
+ * and a surface that ignores it renders exactly the card it rendered before. The durable park row
1090
+ * carries the same seat under the same contract, filled by the same one factory.
1091
+ */
1092
+ readonly ruleOffersAbsence?: "mandated" | "lane_cannot_speak" | "shadowed";
922
1093
  /** The human/model-readable prompt explaining what needs approval. */
923
1094
  message: string;
924
1095
  /** Runner-filled source identity (design S1d §1): the end-user {@link AskRequest} issuing task's
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { brandPolicyAskClass } from "./ask-class.js";
4
+ import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
4
5
  import { join, normalize as normalizePath, posix as posixPath, sep, win32 as winPath } from "node:path";
5
6
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
6
7
  import { boundInputHashOf } from "./canonical-json.js";
@@ -85,6 +86,7 @@ export function checkToolPolicyProjection(projection, req) {
85
86
  export function constraintChainEntryOf(policy, meta) {
86
87
  const m = {
87
88
  ...(meta?.autoModeArmed ? { autoModeArmed: true } : {}),
89
+ ...(meta?.autoModeArming !== undefined ? { autoModeArming: meta.autoModeArming } : {}),
88
90
  ...(meta?.durableMandate ? { durableMandate: true } : {}),
89
91
  ...(meta?.contentMandate ? { contentMandate: true } : {}),
90
92
  };
@@ -93,6 +95,31 @@ export function constraintChainEntryOf(policy, meta) {
93
95
  return { opaque: true, ...m };
94
96
  return { components: p.components, requiresLiveRemainder: p.requiresLiveRemainder, ...m };
95
97
  }
98
+ function chainEntryOfLayerCarrying(layer, carryArming) {
99
+ const arming = carryArming ? sanitizeAutoModeArmingRecipe(layer.autoMode?.arming) : undefined;
100
+ return constraintChainEntryOf(layer.policy, {
101
+ ...(layer.autoMode !== undefined ? { autoModeArmed: true } : {}),
102
+ ...(arming !== undefined ? { autoModeArming: arming } : {}),
103
+ ...(layer.durableMandate === true ? { durableMandate: true } : {}),
104
+ ...(layer.contentMandate === true ? { contentMandate: true } : {}),
105
+ });
106
+ }
107
+ export function constraintChainEntryOfLayer(layer) {
108
+ let armingLive = false;
109
+ if (layer.autoMode?.arming !== undefined) {
110
+ try {
111
+ const d = layer.autoMode.decider;
112
+ armingLive = d.breakerOpen() !== true && typeof d.consecutiveFailures === "function" && d.consecutiveFailures() === 0;
113
+ }
114
+ catch {
115
+ armingLive = false;
116
+ }
117
+ }
118
+ return chainEntryOfLayerCarrying(layer, armingLive);
119
+ }
120
+ export function constraintChainEntryOfSuppliedLayer(layer, recorded) {
121
+ return chainEntryOfLayerCarrying(layer, recorded?.autoModeArming !== undefined);
122
+ }
96
123
  function stableJson(v) {
97
124
  if (v === null || typeof v !== "object")
98
125
  return JSON.stringify(v) ?? "null";
@@ -354,6 +381,7 @@ export function combinePolicies(...policies) {
354
381
  let rewrite;
355
382
  let settled;
356
383
  let ruleAskText;
384
+ let probeMandateSeen = false;
357
385
  for (const p of policies) {
358
386
  const d = refuseOutOfContractDecision(await p.check(current, signal));
359
387
  if (d.action === "deny") {
@@ -381,10 +409,13 @@ export function combinePolicies(...policies) {
381
409
  if (d.action === "ask" && d.matchedAskRule !== undefined && ruleAskText === undefined) {
382
410
  ruleAskText = d.matchedAskRule;
383
411
  }
412
+ if (d.action === "ask" && d.probeMandated === true)
413
+ probeMandateSeen = true;
384
414
  }
385
415
  if (asked) {
386
416
  const merged = rewrite?.updatedInput;
387
- const withMark = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
417
+ const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
418
+ const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
388
419
  return merged !== undefined ? { ...withMark, updatedInput: merged } : withMark;
389
420
  }
390
421
  const allowed = rewrite ?? ALLOW;
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Type } from "typebox";
3
+ import { uuidv7 } from "../engine/session/uuid.js";
3
4
  import { defineTool, errorResult } from "./tools.js";
4
5
  import { deliverEngineNotice } from "./types.js";
5
6
  import { TOOL_SEARCH_NAME } from "./runner/tool-disclosure.js";
@@ -178,7 +179,7 @@ export function createOffloadPersist(store, sessionId, onNotice) {
178
179
  const message = `tool-result offload: persisting ref "${ref}" failed (${cause}) — this write attempt stored nothing; ` +
179
180
  `unless an earlier attempt already stored this ref, the inline preview is all that survived. ` +
180
181
  `A later re-clear of the same result retries the write.`;
181
- deliverEngineNotice(onNotice, { code: "tool_result.offload_put_failed", message, detail: { ref, sessionId, cause } });
182
+ deliverEngineNotice(onNotice, { code: "tool_result.offload_put_failed", message, detail: { ref, sessionId, cause, occurrenceId: uuidv7() } });
182
183
  };
183
184
  try {
184
185
  void Promise.resolve(store.put(ref, fullText, provenance)).catch(reportFailedWrite);
@@ -58,6 +58,23 @@ export type TraceEvent = {
58
58
  kind: "task.start";
59
59
  version: 1;
60
60
  taskId: string;
61
+ /** #499 (additive) — the ENGINE-minted identity of THIS run (`TaskResult.runId`). `taskId` is
62
+ * `spec.taskId ?? sessionId`, so two runs of one session that declare no host task id emit two
63
+ * task.start frames under ONE id and a consumer pairing start↔end has no way to say which end
64
+ * closes which start. Present on every `task.start` (the frame is emitted after prepare, which
65
+ * is where the id is minted).
66
+ *
67
+ * PAIRING CONTRACT, stated in full because it is not symmetric: a `task.end` may carry a
68
+ * `runId` this frame never announced — a run whose PREPARE threw emits the end and no start
69
+ * (there was nothing to start), and that end still names its run because prepare-time
70
+ * disclosures already did. So: every start has an end; not every end has a start. A consumer
71
+ * building spans must treat an unmatched end as a run that never began, not as a lost start.
72
+ *
73
+ * SCOPE, stated so it is not mistaken for more: only this frame and `task.end` carry the run
74
+ * axis today. Every other frame in this union still keys on `taskId` alone, so frames from two
75
+ * CONCURRENT runs that share a host `taskId` remain mutually indistinguishable inside their
76
+ * windows. Extending the axis across the union is a separate pass. */
77
+ runId?: string;
61
78
  model: string;
62
79
  /** R4 (1.285): the engine build (package.json version, runtime-resolved) that produced this
63
80
  * run — binds trace forensics to an exact build. "unknown" if resolution failed. */
@@ -247,6 +264,13 @@ export type TraceEvent = {
247
264
  kind: "task.end";
248
265
  version: 1;
249
266
  taskId: string;
267
+ /** #499 (additive) — the run this terminal closes; pairs with the `task.start` frame's `runId`
268
+ * (see that field for why `taskId` cannot carry the pairing, and for the ASYMMETRY: this frame
269
+ * is also emitted on the prepare-throw backstop, where no `task.start` exists — it still names
270
+ * its run, because the id is minted at prepare's first statement and prepare-time disclosures
271
+ * already carry it). Absent only where prepare was never entered at all, which is the same
272
+ * condition its `taskId: "unknown"` states. */
273
+ runId?: string;
250
274
  status: TaskStatus;
251
275
  errorCode?: string;
252
276
  turns: number;