@sema-agent/core 7.11.1 → 7.12.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 (78) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/core/auto-mode-arming.d.ts +10 -14
  3. package/dist/core/auto-mode-arming.js +3 -9
  4. package/dist/core/auto-mode-defaults.d.ts +0 -2
  5. package/dist/core/auto-mode-defaults.js +0 -1
  6. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  7. package/dist/core/auto-mode-rebuild.js +0 -2
  8. package/dist/core/auto-mode.d.ts +30 -89
  9. package/dist/core/auto-mode.js +12 -59
  10. package/dist/core/checkpoint-store.d.ts +1 -3
  11. package/dist/core/gate-fold.js +1 -9
  12. package/dist/core/gate-lanes.js +15 -9
  13. package/dist/core/hooks.d.ts +6 -0
  14. package/dist/core/runner/compaction-knobs.d.ts +45 -0
  15. package/dist/core/runner/compaction-knobs.js +3 -0
  16. package/dist/core/runner/contracts.d.ts +49 -10
  17. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  18. package/dist/core/runner/denial-limit-arms.js +9 -7
  19. package/dist/core/runner/gate-exit.js +9 -1
  20. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  21. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  22. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  23. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  24. package/dist/core/runner/prepare-task.js +1 -1
  25. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  26. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  27. package/dist/core/runner/run-attachment-seats.d.ts +22 -0
  28. package/dist/core/runner/run-attachment-seats.js +187 -0
  29. package/dist/core/runner/run-brain-sinks.d.ts +29 -0
  30. package/dist/core/runner/run-brain-sinks.js +61 -0
  31. package/dist/core/runner/run-clock-and-content.d.ts +52 -0
  32. package/dist/core/runner/run-clock-and-content.js +26 -0
  33. package/dist/core/runner/run-compaction-machinery.d.ts +35 -0
  34. package/dist/core/runner/run-compaction-machinery.js +98 -0
  35. package/dist/core/runner/run-git-lane.d.ts +64 -0
  36. package/dist/core/runner/run-git-lane.js +102 -0
  37. package/dist/core/runner/run-identity-wiring.d.ts +77 -0
  38. package/dist/core/runner/run-identity-wiring.js +85 -0
  39. package/dist/core/runner/run-leg.d.ts +106 -0
  40. package/dist/core/runner/run-leg.js +462 -0
  41. package/dist/core/runner/run-notification-lane.d.ts +55 -0
  42. package/dist/core/runner/run-notification-lane.js +128 -0
  43. package/dist/core/runner/run-reasoning-seat.d.ts +27 -0
  44. package/dist/core/runner/run-reasoning-seat.js +48 -0
  45. package/dist/core/runner/run-recovery-lanes.d.ts +54 -0
  46. package/dist/core/runner/run-recovery-lanes.js +180 -0
  47. package/dist/core/runner/run-settle-and-teardown.d.ts +109 -0
  48. package/dist/core/runner/run-settle-and-teardown.js +324 -0
  49. package/dist/core/runner/run-stop-and-final-verify.d.ts +32 -0
  50. package/dist/core/runner/run-stop-and-final-verify.js +159 -0
  51. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +38 -0
  52. package/dist/core/runner/run-telemetry-and-budget-seats.js +159 -0
  53. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  54. package/dist/core/runner/run-terminal-adoption.js +120 -0
  55. package/dist/core/runner/run-tool-mount-facts.d.ts +26 -0
  56. package/dist/core/runner/run-tool-mount-facts.js +58 -0
  57. package/dist/core/runner/run-turn-boundary.d.ts +0 -35
  58. package/dist/core/runner/run-turn-boundary.js +1 -3
  59. package/dist/core/runner/runtask.d.ts +14 -3
  60. package/dist/core/runner/runtask.js +125 -2102
  61. package/dist/core/runner-deps.d.ts +4 -14
  62. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  63. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  64. package/dist/core/tool-policy.d.ts +37 -93
  65. package/dist/core/tool-policy.js +1 -11
  66. package/dist/core/trace.d.ts +6 -7
  67. package/dist/core/wiring-manifest.d.ts +5 -22
  68. package/dist/core/wiring-manifest.js +3 -11
  69. package/dist/core/workflow-journal-store.d.ts +35 -4
  70. package/dist/core/workflow-journal-store.js +19 -2
  71. package/dist/index.d.ts +3 -2
  72. package/dist/index.js +3 -2
  73. package/dist/orchestration/workflow.js +2 -0
  74. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  75. package/dist/stores/file/workflow-journal-store.js +2 -4
  76. package/dist/tools/fs/fs-bash.js +3 -2
  77. package/package.json +1 -1
  78. package/test/export-surface.snapshot.json +10 -10
@@ -1,6 +1,6 @@
1
1
  import { decisionText, describeThrown } from "./tool-policy.js";
2
2
  import { askOriginOf, classifierMayAnswer } from "./ask-origin.js";
3
- import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
3
+ import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { isRuleBehavior } from "./permission-rule-model.js";
6
6
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
@@ -388,9 +388,17 @@ export async function runGateLanes(pass) {
388
388
  ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
389
389
  };
390
390
  }
391
- else if (verdict.kind === "block") {
392
- const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
393
- const category = verdict.category ? inlineUntrusted(verdict.category) : "";
391
+ else if (verdict.kind === "unavailable") {
392
+ pass.decision = {
393
+ action: "deny",
394
+ message: classifierUnavailableDenyMessage(toolName, verdict.cause),
395
+ decisionReason: "classifier",
396
+ classifierUnavailable: { cause: verdict.cause },
397
+ };
398
+ }
399
+ else {
400
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
401
+ const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
394
402
  const tracked = input.autoMode.denialTracking?.recordBlock();
395
403
  if (tracked?.limitReached === true) {
396
404
  const fallbackAsk = {
@@ -405,15 +413,13 @@ export async function runGateLanes(pass) {
405
413
  else {
406
414
  pass.decision = {
407
415
  action: "deny",
408
- message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
416
+ message: verdict.kind === "block" ? `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : reason,
409
417
  decisionReason: "classifier",
410
418
  };
411
- pass.deniedBy = "classifier";
412
419
  }
413
420
  }
414
- else if (verdict.kind === "unavailable") {
415
- pass.decision = { ...pass.decision, classifierUnavailable: { cause: verdict.cause } };
416
- }
421
+ if (pass.decision.action === "deny")
422
+ pass.deniedBy = "classifier";
417
423
  }
418
424
  if (input.sandboxAdmission !== undefined &&
419
425
  pass.decision.action === "ask" &&
@@ -411,6 +411,12 @@ export interface PermissionDeniedPayload {
411
411
  * settled none.
412
412
  */
413
413
  gate: GateOutcome;
414
+ /** #661 (additive): present ⇔ this deny is the auto-mode classifier's UNAVAILABILITY (`deniedBy: "classifier"`
415
+ * with the verdict's cause word) — the structured half of the deny text's "the classifier is temporarily
416
+ * unavailable" sentence. A classifier BLOCK or parse-failure deny carries no member here. */
417
+ classifierUnavailable?: {
418
+ readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
419
+ };
414
420
  /** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
415
421
  * already happened and nothing this callback does can change it, so the signal says exactly one
416
422
  * thing: stop reading, nobody is waiting for your answer any more. */
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The run loop's three compaction knobs (design/393 S5): the consecutive-failure breaker trip, the anti-thrash
3
+ * regrowth factor and the effective-compaction epsilon. They were declared at the turn-boundary lane and the driver
4
+ * reached DOWN for them; the recovery lane (the forced-compaction pass the prompt-too-long and guard-chain arms
5
+ * share) reads the same three, and a lane may not name a sibling lane — so the three values sank to the machinery
6
+ * stratum, where the two lanes and the driver all reach down for them. Values verbatim, comments included.
7
+ *
8
+ * No `run-` family prefix, for the reason initial-run-state.ts gives: a module the lanes stand on is not a driven
9
+ * lane, and the prefix would put it under the phase-api gate and the run-lanes glob.
10
+ */
11
+ /**
12
+ * §17.4 (design/64 GAP-25): stop attempting within-task compaction after this many CONSECUTIVE
13
+ * failures in one task — a never-recovering summary gateway must not burn an API call at every turn
14
+ * boundary (CC: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3). Reset on any success; an open breaker also
15
+ * skips the end-of-task attempt in `finish()` (teardown unaffected). Per-task scope.
16
+ *
17
+ * RB-190 — this used to say "a flaky/never-recovering gateway", and the word `flaky` was not true. Because
18
+ * any success zeroes the counter, a gateway that alternates never reaches three CONSECUTIVE failures and
19
+ * the breaker never opens; it keeps paying for a call at every failed boundary, indefinitely.
20
+ *
21
+ * The behaviour is right and stays: a gateway succeeding half the time is compacting half the time, and
22
+ * opening the breaker on it would disable compaction outright and let the context overflow — trading a
23
+ * wasted call for a failed task. What was wrong is the comment claiming a guarantee the code does not
24
+ * make. An alternating gateway is deliberately NOT caught here; if that ever needs catching it wants a
25
+ * different signal (a failure RATE over a window), not a consecutive counter.
26
+ */
27
+ export declare const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
28
+ /**
29
+ * §25.2 anti-thrash (LONGRUN-1b, search [84]): after a compaction, the context must regrow past
30
+ * (tokensBefore − freed) × this factor — the post-compaction size in trigger units — before another
31
+ * summary call fires. An EFFECTIVE compaction (freed large) leaves the floor far below the threshold,
32
+ * so behavior is unchanged; an INEFFECTIVE one (small window, chunky turns, large summary ⇒ freed
33
+ * small, still at/over the threshold) raises the floor so attempts space out instead of firing at
34
+ * every boundary (measured: 26/39 boundaries, +135% wall). The floor binds only when post-compaction
35
+ * size > threshold/1.5 ≈ compaction freed <33% headroom. Degrades gracefully: the retained summary
36
+ * chain keeps early facts; only the un-summarized tail falls back to trim.
37
+ */
38
+ export declare const COMPACTION_REGROWTH_FACTOR = 1.5;
39
+ /**
40
+ * design/84 Seam C reuse guard (b): a compaction that frees fewer than this many (structural) tokens is
41
+ * treated as NO-EFFECTIVE-COMPACTION and does NOT raise the anti-thrash floor. Guards against a reused
42
+ * provider summary (or a pathological LLM summary) that reclaims no headroom yet suppresses the next real
43
+ * summary by inflating the floor to trigger×factor.
44
+ */
45
+ export declare const COMPACTION_FREED_EPSILON = 256;
@@ -0,0 +1,3 @@
1
+ export const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
2
+ export const COMPACTION_REGROWTH_FACTOR = 1.5;
3
+ export const COMPACTION_FREED_EPSILON = 256;
@@ -22,7 +22,7 @@ import type { WorkflowSizeGuideline } from "../../orchestration/workflow-size-gu
22
22
  import type { ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
23
23
  import type { CwdRef, ReadFace } from "../../tools/fs/index.js";
24
24
  import type { MaterializedA2a } from "../a2a.js";
25
- import type { CompactionForkContext, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
25
+ import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
26
26
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
27
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
28
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
@@ -1778,14 +1778,6 @@ export interface RunInternals {
1778
1778
  * channel, same posture as every other field here.
1779
1779
  */
1780
1780
  sessionReadStates?: SessionReadFileStates;
1781
- /**
1782
- * #616 — the Runner's per-session breaker ledger ({@link import("../auto-mode.js").AutoModeBreakerLedger}):
1783
- * the arming site's `onBreakerOpen` wrap records a trip here, the wiring-manifest phase of a LATER leg of
1784
- * the same session reads the most recent one onto `autoMode.breaker`. Always set by the Runner's own
1785
- * prepare call (overriding any caller value, like `sessionReadStates` beside it); absent on a standalone
1786
- * prepareTask, where no trip is recorded and none is reported. Trusted internals channel.
1787
- */
1788
- autoModeBreakerLedger?: import("../auto-mode.js").AutoModeBreakerLedger;
1789
1781
  onTaskNotification?: (notification: TaskNotificationPayload,
1790
1782
  /** Injection tier (design/373 — the ladder is LIVE): "next" = the running turn's next boundary
1791
1783
  * (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
@@ -2041,7 +2033,14 @@ export interface ParkAsk {
2041
2033
  * Grouped by lifecycle semantics per the B15 pre-recon partition. Function members start as inert
2042
2034
  * placeholders and are assigned at their ORIGINAL declaration sites (the original code could not
2043
2035
  * call them earlier either — TDZ proved it). Not on the npm face; this extraction is what enabled
2044
- * createTurnBoundary / createHarnessHandlers (三期, both in runtask.ts).
2036
+ * createTurnBoundary / createHarnessHandlers (三期; both are run lanes of their own since design/393 S2).
2037
+ *
2038
+ * WRITERS, by group (design/393 S5 — the driver mints the zero value, initial-run-state.ts, and hands it to every
2039
+ * lane as a borrowed-mutable seat): `telemetry` / `degrade` / `limits` / `budget` initial values and function
2040
+ * members — the telemetry-and-budget-seats lane; `counters` / `attach` initial values — the attachment-seats lane
2041
+ * (the clock lane zeroes `counters.walltimeSyncBackstopFired`; the tool-mount facts only read); the per-turn writes — the harness-handlers lane
2042
+ * (`turn`, `counters`, `limits`, `budget`), the turn-boundary lane (`counters`, `attach`, `budget`) and the
2043
+ * recovery lane (`counters.compactionFloor`, `attach.attachState`); the settle-time reads stay the driver's.
2045
2044
  */
2046
2045
  export interface RunState {
2047
2046
  telemetry: {
@@ -2468,6 +2467,46 @@ export interface ManualCompactRef {
2468
2467
  emitMooted?: (reason: string) => void;
2469
2468
  closed?: boolean;
2470
2469
  }
2470
+ /**
2471
+ * design/393 S6 — the end-of-task compaction pass's outcome (`Runner.finish`'s return): what the terminal-adoption lane
2472
+ * hands the settle lane, which reads it for the `compacted` frame, the phase-timings frame and the detector reset. Named
2473
+ * once here so the two lanes and the method spell one type.
2474
+ */
2475
+ export type EndOfTaskCompaction = {
2476
+ compacted: boolean;
2477
+ tokensBefore?: number;
2478
+ triggerTokens?: number;
2479
+ postTriggerTokens?: number;
2480
+ durationMs?: number;
2481
+ phaseDurations?: CompactionPhaseDurations;
2482
+ firstKeptEntryId?: string;
2483
+ attachedFiles?: Array<{
2484
+ path: string;
2485
+ chars: number;
2486
+ truncated: boolean;
2487
+ }>;
2488
+ modelFallback?: true;
2489
+ fallbackReason?: "window";
2490
+ clampedRatio?: number;
2491
+ clampReason?: "budget" | "tolerance";
2492
+ } | undefined;
2493
+ /**
2494
+ * design/393 S6 — the notification lane's BINDINGS: the run-notification-lane's own four `let`s as ONE seat (getters
2495
+ * and setters over the lane's variables, never a copy). The identity-wiring lane binds `harness` / `sessionId` /
2496
+ * `ident` the moment `prepared` exists (the lane's closures read them by variable — the routing listener, the park
2497
+ * destination, the injection entry); the leg lane flips `live` in its finally, from which point every notification
2498
+ * parks per session for the next run. One seat, four slots; nothing copies a binding out of it.
2499
+ */
2500
+ export interface NotificationLaneBindings {
2501
+ harness: AgentHarness | undefined;
2502
+ sessionId: string | undefined;
2503
+ ident: () => {
2504
+ eventId?: string;
2505
+ parentToolCallId?: string;
2506
+ sourceTaskId?: string;
2507
+ };
2508
+ live: boolean;
2509
+ }
2471
2510
  /** design/144 §2 — the `notify()` bridge: `runLocked` binds `inject` the moment the task-notification lane exists. */
2472
2511
  export interface NotifyRef {
2473
2512
  inject?: (n: TaskNotificationPayload, opts?: {
@@ -32,8 +32,7 @@ export interface InheritedAutoMode {
32
32
  * recorded recipe — `rebuildAutoModeDecider` hands back a decider and its arming, nothing live)
33
33
  * carries no denial tracker, and an entry with a decider but no tracker would count nothing: the
34
34
  * rebuilt classifier could block without bound on the redeemed leg, the exact gap the limit closes.
35
- * Attach a FRESH tracker (a fresh COUNT is the documented cross-process semantics the breaker
36
- * restarts the same way) under the ancestor's recorded BOUNDS tightened by this deployment's own
35
+ * Attach a FRESH tracker (a fresh COUNT is the documented cross-process semantics) under the ancestor's recorded BOUNDS tightened by this deployment's own
37
36
  * (#556, `tightenDenialLimit` — the same rule and the same call the arming fold makes, so the
38
37
  * recipe's account of the criteria and the tracker's actual bounds cannot drift). Before #556 the
39
38
  * bounds came from this deployment alone, so an ancestor that allowed three consecutive blocks
@@ -84,8 +83,8 @@ export type InheritedAsk = Extract<PermissionResult, {
84
83
  }>;
85
84
  /** The members of a surviving ask that ride onto the approval request with a COMPUTED value — what the
86
85
  * four request mint stations (the gate's own and the three inherited-lane ones) spread after the seats
87
- * they spell themselves: the ask's origin word, its denial-limit fallback and the classifier-unavailable
88
- * fact (#616). */
86
+ * they spell themselves: the ask's origin word, its denial-limit fallback and a policy-declared
87
+ * classifier-unavailable fact (#616; the engine's own fact rides the DENY since #661). */
89
88
  export interface AskRequestCarry {
90
89
  origin?: AskOrigin;
91
90
  denialLimitFallback?: DenialLimitFallback;
@@ -117,18 +116,15 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
117
116
  kind: "deny";
118
117
  result: PermissionResult;
119
118
  }
120
- /** Proceed to the frozen approver with `ask`: the incoming ask unchanged (classifier unavailable,
121
- * excluded, or not armed), or — at the bound — the fallback ask minted from it (`mintedHere`).
119
+ /** Proceed to the frozen approver with `ask`: the incoming ask unchanged (classifier excluded or not
120
+ * armed), or — at the bound — the fallback ask minted from it (`mintedHere`).
122
121
  * `fallback` is the member riding the ask either way (an incoming one is a deeper ancestor's).
123
122
  * `origin` is the ask's word under the station facts, threaded to the request mint: on the two
124
123
  * unchanged-ask branches it is the SAME read the exclusion judged by (a caller-owned decision with a
125
124
  * stateful accessor cannot answer the exclusion with one word and the card with another); on the
126
125
  * bound branch it is derived from the re-spoken ask this station minted (its own object). */
127
- /** `ask` is the incoming ask unchanged EXCEPT on an UNAVAILABLE round, where it is an engine-owned copy
128
- * carrying `classifierUnavailable: { cause }` (#616): the fact rides the ASK because the ask is what the
129
- * wrapper arms hand back to the gate when the frozen approver answers unavailable (the inherited-unavailable
130
- * float), and the gate's own park carry reads the fact off the decision it parks — a side member on this
131
- * judgment would have been lost on that route (codex r1). The request mint reads the same object. */
126
+ /** `ask` is the incoming ask unchanged (the caller's own object); an UNAVAILABLE round never reaches this arm
127
+ * it is a `deny` carrying `classifierUnavailable: { cause }` (#661). */
132
128
  | {
133
129
  kind: "resolve";
134
130
  ask: A;
@@ -142,8 +138,9 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
142
138
  * auto-allow (and the streak resets); `block` below the bound ⇒ deny, the exact verdict the ancestor's
143
139
  * gate would have produced; `block` AT the bound ⇒ the ask is re-spoken in the classifier's voice as
144
140
  * the fallback (`decisionReason:"classifier"`, `requiresRealApproval`, the member carrying counts +
145
- * window) for the arm to resolve at the frozen approver; anything else (unavailable / parse error /
146
- * excluded / no classifier) ⇒ the incoming ask, unchanged. The member may ALREADY be on the incoming
141
+ * window) for the arm to resolve at the frozen approver; `parse_error` the same block path with the CC
142
+ * parse-failure sentence (counted); `unavailable` a DENY that says so, carrying the fact (#661, CC
143
+ * 2.1.250 form); excluded / no classifier ⇒ the incoming ask, unchanged. The member may ALREADY be on the incoming
147
144
  * ask (a deeper ancestor's fallback, minted inside the nested fold — the case the exclusion names):
148
145
  * it rides to the approver and the headless arm unchanged, whichever tracker minted it; only a
149
146
  * tracker that counted THIS streak is reset by the person's later allow (`mintedHere`).
@@ -1,4 +1,4 @@
1
- import { createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
1
+ import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
2
2
  import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
3
3
  import { deliverEngineNotice } from "../types.js";
4
4
  import { inlineUntrusted } from "../untrusted-text.js";
@@ -85,19 +85,21 @@ export async function judgeInheritedClassifier(opts) {
85
85
  autoMode.denialTracking?.recordAllow();
86
86
  return { kind: "allow" };
87
87
  }
88
- if (verdict.kind !== "block") {
89
- const resolved = verdict.kind === "unavailable" ? { ...ask, classifierUnavailable: { cause: verdict.cause } } : ask;
90
- return { kind: "resolve", ask: resolved, fallback: ask.denialLimitFallback, mintedHere: false, origin };
88
+ if (verdict.kind === "unavailable") {
89
+ return {
90
+ kind: "deny",
91
+ result: { action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier", classifierUnavailable: { cause: verdict.cause } },
92
+ };
91
93
  }
92
- const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
93
- const category = verdict.category ? inlineUntrusted(verdict.category) : "";
94
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
95
+ const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
94
96
  const tracked = autoMode.denialTracking?.recordBlock();
95
97
  if (tracked?.limitReached !== true) {
96
98
  return {
97
99
  kind: "deny",
98
100
  result: {
99
101
  action: "deny",
100
- message: `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
102
+ message: verdict.kind === "block" ? `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : `${reason} (${opts.subject}, at an inherited ancestor layer)`,
101
103
  decisionReason: "classifier",
102
104
  },
103
105
  };
@@ -103,7 +103,15 @@ export async function exitGate(pass) {
103
103
  if (pass.deniedBy === undefined)
104
104
  throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
105
105
  const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
106
- await notifyPermissionDenied({ toolName, input: cloneObserverInput(pass.currentInput), toolCallId, reason: denyReason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
106
+ await notifyPermissionDenied({
107
+ toolName,
108
+ input: cloneObserverInput(pass.currentInput),
109
+ toolCallId,
110
+ reason: denyReason,
111
+ gate,
112
+ ...(input.identity !== undefined ? { identity: input.identity } : {}),
113
+ ...(pass.decision.action === "deny" && pass.decision.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: pass.decision.classifierUnavailable.cause } } : {}),
114
+ });
107
115
  return {
108
116
  block: true,
109
117
  reason: formatHookFeedback(denyReason, input.reminderMark),
@@ -49,7 +49,7 @@ export interface PrepareCapsAndWorkflowInput {
49
49
  deps: RunnerDeps;
50
50
  /** borrowed-readonly — the trusted spawn-side channel; the Workflow mount forwards `rootSessionId`, `placementRoot`,
51
51
  * `onTaskNotification`, `workflowDepth`, and the capture-floor getter reads `memoryCaptureAncestors`. */
52
- internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors" | "workflowParkedResume" | "autoModeBreakerLedger"> | undefined;
52
+ internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors" | "workflowParkedResume"> | undefined;
53
53
  /** borrowed-readonly — the acquired session; the classifier leg rebuilds its transcript window from `buildContext`. */
54
54
  session: Pick<StoredSession, "buildContext">;
55
55
  /** borrowed-readonly — the acquired session id (operator-line context, the refusal codes' phase record). */
@@ -195,11 +195,6 @@ export async function prepareCapsAndWorkflow(input) {
195
195
  autoModeDenialTracking = createAutoModeDenialTracker(am.denialLimit);
196
196
  autoModeDecider = createAutoModeDecider({
197
197
  ...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
198
- ...(am.failureThreshold !== undefined ? { failureThreshold: am.failureThreshold } : {}),
199
- onBreakerOpen: (info) => {
200
- internals?.autoModeBreakerLedger?.record(sessionId, { openedAtMs: Date.now(), lastCause: info.lastCause, failures: info.consecutiveFailures, runId });
201
- am.onBreakerOpen?.(info);
202
- },
203
198
  onClassified: (info) => emitTrace(deps.tracer, () => ({
204
199
  kind: "auto_mode.classified",
205
200
  version: 1,
@@ -17,7 +17,6 @@
17
17
  * the boundary parks and the park closure key off `saga`'s presence, so the orchestrator grows no branch.
18
18
  */
19
19
  import type { AgentHarness, ExecutionEnv } from "../../internal/harness.js";
20
- import type { AutoModeDecider } from "../auto-mode.js";
21
20
  import type { OwnOrgAdmissionVerdict } from "../memory-admission.js";
22
21
  import type { RemoteExecutionEnv, SnapshotId } from "../remote-env.js";
23
22
  import type { CheckpointStore, SerializedCheckpointState } from "../checkpoint-store.js";
@@ -128,7 +127,6 @@ export interface PrepareSuspendSagaInput {
128
127
  /** borrowed-readonly — the auto-mode intent (seat ∨ live ∨ seed), carried while the latch is healthy. */
129
128
  autoModeIntent: boolean;
130
129
  /** borrowed-readonly — the auto-mode decider, or undefined (the latch-health reading only). */
131
- autoModeDecider: AutoModeDecider | undefined;
132
130
  /** borrowed-readonly — the inherited admitted org scopes, or undefined (copied onto the row). */
133
131
  inheritedAdmittedOrgScopes: readonly string[] | undefined;
134
132
  /** borrowed-readonly — this leg's OWN org verdict seat; `current` is read at mint time. Not written here. */
@@ -77,16 +77,8 @@ export async function compensateUnparkedPause(remoteEnv, snapshotId, io) {
77
77
  function stampPlacementRootSessionId(placementRootResolved) {
78
78
  return placementValueOrAbsent(placementRootResolved);
79
79
  }
80
- function autoModeLatchHealthy(d) {
81
- try {
82
- return d.breakerOpen() !== true && typeof d.consecutiveFailures === "function" && d.consecutiveFailures() === 0;
83
- }
84
- catch {
85
- return false;
86
- }
87
- }
88
80
  export function prepareSuspendSaga(input) {
89
- const { gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, autoModeDecider, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter } = input;
81
+ const { gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter } = input;
90
82
  if (!gateMachineryActive)
91
83
  return { saga: undefined };
92
84
  const inFlightSpendMicroUsd = () => {
@@ -140,7 +132,7 @@ export function prepareSuspendSaga(input) {
140
132
  const constraintDigest = (inheritedParentConstraints?.length ?? 0) > 0
141
133
  ? constraintChainDigest(constraintChain)
142
134
  : seedInheritedGate?.constraintDigest;
143
- const autoModeIntentCarried = autoModeIntent && (autoModeDecider === undefined || autoModeLatchHealthy(autoModeDecider));
135
+ const autoModeIntentCarried = autoModeIntent;
144
136
  return inheritedAncestorRules !== undefined ||
145
137
  inheritedShellGate !== undefined ||
146
138
  autoModeIntentCarried ||
@@ -678,7 +678,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
678
678
  });
679
679
  }
680
680
  const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash });
681
- const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, autoModeDecider, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
681
+ const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
682
682
  const { suspendForResource, suspendForPlatformLimit, suspendForReview } = prepareBoundaryParks({ saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, pausedRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter });
683
683
  const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedBash, shellGatedMonitor, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
684
684
  prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedBash, shellGatedMonitor, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
@@ -46,7 +46,7 @@ export interface PrepareWiringManifestInput {
46
46
  deps: RunnerDeps;
47
47
  /** borrowed-readonly — the trusted spawn-side channel: `questionFaceStripped`, `isDelegatedChild`, `onTaskNotification`,
48
48
  * `peerSelfRef`, `explicitAgentName` (the drain), `insideFork` / `agentName` / `parentToolCallId` (the identity). */
49
- internals: Pick<RunInternals, "questionFaceStripped" | "isDelegatedChild" | "onTaskNotification" | "peerSelfRef" | "explicitAgentName" | "insideFork" | "agentName" | "parentToolCallId" | "autoModeBreakerLedger"> | undefined;
49
+ internals: Pick<RunInternals, "questionFaceStripped" | "isDelegatedChild" | "onTaskNotification" | "peerSelfRef" | "explicitAgentName" | "insideFork" | "agentName" | "parentToolCallId"> | undefined;
50
50
  /** borrowed-readonly — the resume leg, or undefined (the manifest's leg derivation reads presence only). */
51
51
  resume: Pick<PrepareResume, "seed"> | undefined;
52
52
  /** borrowed-readonly — the acquired session id. */
@@ -199,14 +199,7 @@ export function prepareWiringManifest(input) {
199
199
  memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
200
200
  retentionPolicyWired: deps.retentionPolicy !== undefined,
201
201
  ...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
202
- autoMode: {
203
- armed: autoModeArmReason === "armed",
204
- reason: autoModeArmReason,
205
- ...(() => {
206
- const trip = internals?.autoModeBreakerLedger?.lastTrip(sessionId);
207
- return trip !== undefined ? { breaker: trip } : {};
208
- })(),
209
- },
202
+ autoMode: { armed: autoModeArmReason === "armed", reason: autoModeArmReason },
210
203
  mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
211
204
  tools: toolRoster,
212
205
  });
@@ -0,0 +1,22 @@
1
+ import type { TaskSpec } from "../types.js";
2
+ import type { Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
3
+ export interface RunAttachmentSeatsInput<R> {
4
+ /** borrowed-readonly — the task spec: the attachments config and the `finalVerification` opt-in. */
5
+ spec: TaskSpec;
6
+ /** borrowed-mutable — the leg's prepared seat: `announcedListingsRef` is seeded; the session, the listing faces,
7
+ * the date-change / instruction-source / size-guideline faces, the tool effects and the reminder mark are read. */
8
+ prepared: Prepared;
9
+ /** borrowed-readonly — the resume plan when this leg resumes a checkpoint (its `announcedListings` mirror is the first seed rung). */
10
+ resume: ResumeRun | undefined;
11
+ /** borrowed-mutable — the run's mutable state: this lane is the WRITER of the `counters` and `attach` groups' initial
12
+ * values (later writers: the harness handlers, the boundary, the recovery pass). */
13
+ rs: RunState;
14
+ /** borrowed-readonly — the Runner's deployment deps, read once for `probeInstructionSources`. */
15
+ runner: RunnerDepsSeat;
16
+ /** borrowed-readonly — the lanes after this one, entered in this lane's last continuation (the seats it wrote are on `rs`). */
17
+ next: (seats: RunAttachmentSeatsResult) => Promise<R>;
18
+ }
19
+ /** Nothing comes back: the lane's products are the `counters` / `attach` groups it wrote on the borrowed run state. */
20
+ export interface RunAttachmentSeatsResult {
21
+ }
22
+ export declare function runAttachmentSeats<R>(input: RunAttachmentSeatsInput<R>): Promise<R>;