@sema-agent/core 7.11.2 → 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 (57) hide show
  1. package/CHANGELOG.md +42 -13
  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/contracts.d.ts +41 -9
  15. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  16. package/dist/core/runner/denial-limit-arms.js +9 -7
  17. package/dist/core/runner/gate-exit.js +9 -1
  18. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  19. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  20. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  21. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  22. package/dist/core/runner/prepare-task.js +1 -1
  23. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  24. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  25. package/dist/core/runner/run-attachment-seats.d.ts +4 -2
  26. package/dist/core/runner/run-attachment-seats.js +2 -2
  27. package/dist/core/runner/run-identity-wiring.d.ts +14 -33
  28. package/dist/core/runner/run-identity-wiring.js +4 -3
  29. package/dist/core/runner/run-leg.d.ts +106 -0
  30. package/dist/core/runner/run-leg.js +462 -0
  31. package/dist/core/runner/run-notification-lane.d.ts +55 -0
  32. package/dist/core/runner/run-notification-lane.js +128 -0
  33. package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
  34. package/dist/core/runner/run-reasoning-seat.js +7 -7
  35. package/dist/core/runner/run-settle-and-teardown.d.ts +109 -0
  36. package/dist/core/runner/run-settle-and-teardown.js +324 -0
  37. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  38. package/dist/core/runner/run-terminal-adoption.js +120 -0
  39. package/dist/core/runner/runtask.d.ts +14 -3
  40. package/dist/core/runner/runtask.js +55 -1010
  41. package/dist/core/runner-deps.d.ts +4 -14
  42. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  43. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  44. package/dist/core/tool-policy.d.ts +37 -93
  45. package/dist/core/tool-policy.js +1 -11
  46. package/dist/core/trace.d.ts +6 -7
  47. package/dist/core/wiring-manifest.d.ts +5 -22
  48. package/dist/core/wiring-manifest.js +3 -11
  49. package/dist/core/workflow-journal-store.d.ts +35 -4
  50. package/dist/core/workflow-journal-store.js +19 -2
  51. package/dist/index.d.ts +3 -2
  52. package/dist/index.js +3 -2
  53. package/dist/orchestration/workflow.js +2 -0
  54. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  55. package/dist/stores/file/workflow-journal-store.js +2 -4
  56. package/package.json +1 -1
  57. package/test/export-surface.snapshot.json +10 -10
@@ -1,14 +1,17 @@
1
- import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
2
- export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout", "breaker_open"];
1
+ import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
2
+ export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"];
3
3
  const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
4
4
  export function isAutoModeUnavailableCause(v) {
5
5
  return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
6
6
  }
7
- export const AUTO_MODE_BREAKER_CAUSES = ["error", "timeout", "parse_error"];
8
- const AUTO_MODE_BREAKER_CAUSE_SET = new Set(AUTO_MODE_BREAKER_CAUSES);
9
- export function isAutoModeBreakerCause(v) {
10
- return AUTO_MODE_BREAKER_CAUSE_SET.has(v);
7
+ export function classifierUnavailableDenyMessage(toolName, cause) {
8
+ const parenthetical = cause === "timeout" ? " (timed out)" : "";
9
+ return (`The auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode cannot determine the safety of ${toolName} right now. ` +
10
+ "Wait a moment and then try this action again. " +
11
+ "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
12
+ "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
11
13
  }
14
+ export const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety — the classifier's reply carried no verdict.";
12
15
  export function parseAutoModeResponse(text) {
13
16
  const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
14
17
  if (rawAnswers.size > 1)
@@ -28,20 +31,6 @@ export function parseAutoModeResponse(text) {
28
31
  }
29
32
  export function createAutoModeDecider(opts) {
30
33
  const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
31
- const threshold = Math.max(1, Math.floor(opts.failureThreshold ?? AUTO_MODE_DEFAULT_FAILURE_THRESHOLD));
32
- let consecutiveFailures = 0;
33
- let open = false;
34
- const recordFailure = (cause) => {
35
- consecutiveFailures++;
36
- if (!open && consecutiveFailures >= threshold) {
37
- open = true;
38
- try {
39
- opts.onBreakerOpen?.({ consecutiveFailures, lastCause: cause });
40
- }
41
- catch {
42
- }
43
- }
44
- };
45
34
  const decide = async (input, signal) => {
46
35
  const startedAt = performance.now();
47
36
  const verdict = await decideUnreported(input, signal);
@@ -59,14 +48,8 @@ export function createAutoModeDecider(opts) {
59
48
  }
60
49
  return verdict;
61
50
  };
62
- return {
63
- breakerOpen: () => open,
64
- consecutiveFailures: () => consecutiveFailures,
65
- decide,
66
- };
51
+ return { decide };
67
52
  async function decideUnreported(input, signal) {
68
- if (open)
69
- return { kind: "unavailable", cause: "breaker_open" };
70
53
  let timer;
71
54
  const inner = new AbortController();
72
55
  const onOuterAbort = () => inner.abort();
@@ -98,21 +81,12 @@ export function createAutoModeDecider(opts) {
98
81
  }
99
82
  });
100
83
  });
101
- if (open)
102
- return { kind: "unavailable", cause: "breaker_open" };
103
- const verdict = parseAutoModeResponse(raw);
104
- if (verdict.kind === "parse_error")
105
- recordFailure("parse_error");
106
- else
107
- consecutiveFailures = 0;
108
- return verdict;
84
+ return parseAutoModeResponse(raw);
109
85
  }
110
86
  catch (e) {
111
87
  if (signal?.aborted)
112
88
  return { kind: "unavailable", cause: "error" };
113
- const timedOut = e instanceof AutoModeTimeout;
114
- recordFailure(timedOut ? "timeout" : "error");
115
- return { kind: "unavailable", cause: timedOut ? "timeout" : "error" };
89
+ return { kind: "unavailable", cause: e instanceof AutoModeTimeout ? "timeout" : "error" };
116
90
  }
117
91
  finally {
118
92
  if (timer !== undefined)
@@ -127,27 +101,6 @@ class AutoModeTimeout extends Error {
127
101
  super("auto-mode classify timeout");
128
102
  }
129
103
  }
130
- export class AutoModeBreakerLedger {
131
- cap;
132
- trips = new Map();
133
- constructor(cap = 1024) {
134
- this.cap = cap;
135
- }
136
- record(sessionId, trip) {
137
- if (this.trips.has(sessionId))
138
- this.trips.delete(sessionId);
139
- this.trips.set(sessionId, trip);
140
- while (this.trips.size > this.cap) {
141
- const oldest = this.trips.keys().next().value;
142
- if (oldest === undefined)
143
- break;
144
- this.trips.delete(oldest);
145
- }
146
- }
147
- lastTrip(sessionId) {
148
- return this.trips.get(sessionId);
149
- }
150
- }
151
104
  export function readDenialLimitFallback(v) {
152
105
  if (typeof v !== "object" || v === null)
153
106
  return undefined;
@@ -1123,9 +1123,7 @@ export interface CheckpointState {
1123
1123
  * suspend leg did. It is a MEMORY of intent, never an authorization: the resuming deployment's
1124
1124
  * face (`RunnerDeps.autoMode`) and the resuming principal's deny bit (`RuntimeCaps.autoMode`)
1125
1125
  * are judged afresh on every leg — a bit on the row cannot arm where the redeeming deployment
1126
- * would not. Follows the classifier's latch: a leg armed here writes it only while its own breaker
1127
- * is untripped and untouched (a session that fell back to non-auto hands nothing forward); a leg
1128
- * never armed here carries the memory as is. Absent on older checkpoints and on non-auto tasks
1126
+ * would not. A leg never armed here carries the memory as is. Absent on older checkpoints and on non-auto tasks
1129
1127
  * (byte-identical to the pre-bit row); an older worker that ignores it resumes un-armed, the
1130
1128
  * narrower direction. */
1131
1129
  autoModeRequested?: true;
@@ -128,15 +128,7 @@ export async function runGateFold(pass) {
128
128
  pass.decision = { ...pass.decision, probeMandated: true };
129
129
  }
130
130
  }
131
- const breakerKnownOpen = (d) => {
132
- try {
133
- return d.breakerOpen();
134
- }
135
- catch {
136
- return false;
137
- }
138
- };
139
- if (input.peerMessage === true && pass.decision.action === "allow" && input.autoMode !== undefined && !breakerKnownOpen(input.autoMode.decider)) {
131
+ if (input.peerMessage === true && pass.decision.action === "allow" && input.autoMode !== undefined) {
140
132
  pass.decision = {
141
133
  action: "ask",
142
134
  message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
@@ -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. */
@@ -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
@@ -2475,6 +2467,46 @@ export interface ManualCompactRef {
2475
2467
  emitMooted?: (reason: string) => void;
2476
2468
  closed?: boolean;
2477
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
+ }
2478
2510
  /** design/144 §2 — the `notify()` bridge: `runLocked` binds `inject` the moment the task-notification lane exists. */
2479
2511
  export interface NotifyRef {
2480
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
  });
@@ -1,6 +1,6 @@
1
1
  import type { TaskSpec } from "../types.js";
2
2
  import type { Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
3
- export interface RunAttachmentSeatsInput {
3
+ export interface RunAttachmentSeatsInput<R> {
4
4
  /** borrowed-readonly — the task spec: the attachments config and the `finalVerification` opt-in. */
5
5
  spec: TaskSpec;
6
6
  /** borrowed-mutable — the leg's prepared seat: `announcedListingsRef` is seeded; the session, the listing faces,
@@ -13,8 +13,10 @@ export interface RunAttachmentSeatsInput {
13
13
  rs: RunState;
14
14
  /** borrowed-readonly — the Runner's deployment deps, read once for `probeInstructionSources`. */
15
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>;
16
18
  }
17
19
  /** Nothing comes back: the lane's products are the `counters` / `attach` groups it wrote on the borrowed run state. */
18
20
  export interface RunAttachmentSeatsResult {
19
21
  }
20
- export declare function runAttachmentSeats(input: RunAttachmentSeatsInput): Promise<RunAttachmentSeatsResult>;
22
+ export declare function runAttachmentSeats<R>(input: RunAttachmentSeatsInput<R>): Promise<R>;
@@ -3,7 +3,7 @@ import { stripGitStatusUnits } from "./git-status-frame.js";
3
3
  import { SKILLS_LISTING_PROBE_HEADER } from "./synthetic-tools.js";
4
4
  import { AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, agentListingDeltaHeader, agentListingInitialHeader, createAttachmentState, replayAnnouncedListing, replayAnnouncedModels } from "./turn-attachments.js";
5
5
  export async function runAttachmentSeats(input) {
6
- const { spec, prepared, resume, rs, runner } = input;
6
+ const { spec, prepared, resume, rs, runner, next } = input;
7
7
  rs.counters.wroteThisRun = false;
8
8
  rs.counters.finalVerifyInjections = 0;
9
9
  rs.counters.groundingSignalPreR9 = false;
@@ -183,5 +183,5 @@ export async function runAttachmentSeats(input) {
183
183
  }
184
184
  }
185
185
  rs.attach.attachmentsInjected = 0;
186
- return {};
186
+ return await next({});
187
187
  }
@@ -1,20 +1,9 @@
1
- /**
2
- * design/393 S5 — the run body's IDENTITY WIRING (R1), verbatim from `Runner.runLocked`, the first thing after
3
- * `prepared` exists: the notification lane's bindings (harness, session anchor, identity mint), the backstop
4
- * carrier's immediate seats, the event-identity mint (`ident`) and the run's canonical task id, the wiring-manifest
5
- * frame and the roster-delta subscription, the delegation-lifecycle spawn station and its owed-terminal carrier,
6
- * the manual-compact mooted channel, the harness sinks for undrained engine notes / user inputs / consumed
7
- * engine notes, the notify / capture-opt-out bridges, the spawner's injector hand-back, the idle-parked
8
- * notification redelivery at turn open, the loop latch and the live-task handle publication, and the run's
9
- * usage counters. Every push here happens before the run's first model/tool interaction, as it did.
10
- */
11
- import { type AgentHarness } from "../../internal/harness.js";
12
1
  import type { PeerInboundChainRef } from "../../agents/peer-admission.js";
13
2
  import { type PendingSessionNotifications, type SystemInjectionPriority, type TaskNotificationPayload } from "../task-notification.js";
14
3
  import type { PushQueue } from "../push-queue.js";
15
4
  import type { TaskEvent, TaskSpec } from "../types.js";
16
5
  import type { Stats } from "./assemble-result.js";
17
- import type { CaptureOptOutRef, LiveHandle, ManualCompactRef, NotifyRef, Prepared, RunInternals, RunnerDepsSeat, TaskIdRef } from "./contracts.js";
6
+ import type { CaptureOptOutRef, LiveHandle, ManualCompactRef, NotificationLaneBindings, NotifyRef, Prepared, RunInternals, RunnerDepsSeat, TaskIdRef } from "./contracts.js";
18
7
  export interface RunIdentityWiringInput {
19
8
  /** borrowed-readonly — the task spec: the task id and the memory / notification seats it names. */
20
9
  spec: TaskSpec;
@@ -38,7 +27,7 @@ export interface RunIdentityWiringInput {
38
27
  notifyRef: NotifyRef | undefined;
39
28
  /** borrowed-mutable — the capture opt-out bridge: `flip` is bound here when a memory session mounted. */
40
29
  captureOptOutRef: CaptureOptOutRef | undefined;
41
- /** borrowed-readonly — the notification lane's ONE injection entry (the driver's, until the lane itself moves). */
30
+ /** borrowed-readonly — the notification lane's ONE injection entry (the R0 lane's). */
42
31
  injectTaskNotification: (notification: TaskNotificationPayload, opts?: {
43
32
  priority?: SystemInjectionPriority;
44
33
  }) => Promise<"queued" | "parked" | "dropped_duplicate">;
@@ -46,26 +35,10 @@ export interface RunIdentityWiringInput {
46
35
  deliveredAtTurnOpen: Set<string>;
47
36
  /** borrowed-mutable — design/176: the peer inbound chain, overwritten by each consumed peer-class payload. */
48
37
  peerInboundChainRef: PeerInboundChainRef;
49
- /** borrowed-mutable — the driver's notification-lane bindings, a VIEW over its `let`s (never a copy): this lane binds
50
- * the harness, the session anchor and the identity mint the lane's closures read, and reads the anchor back in
51
- * the undrained-notes sink. */
52
- notificationLane: {
53
- harness: AgentHarness | undefined;
54
- sessionId: string | undefined;
55
- ident: () => {
56
- eventId?: string;
57
- parentToolCallId?: string;
58
- sourceTaskId?: string;
59
- };
60
- };
61
- /** borrowed-mutable — the driver's held agent_end account (backlog #389), a view over its `let`: the undrained-inputs
62
- * sink writes it when the run committed a durable park; the driver's tail reads it. */
63
- undrainedUserAtEnd: {
64
- current: {
65
- steer: number;
66
- followUp: number;
67
- } | undefined;
68
- };
38
+ /** borrowed-mutable — the notification lane's bindings seat (its own `let`s behind getters / setters, never a copy):
39
+ * this lane binds the harness, the session anchor and the identity mint the lane's closures read, and reads the
40
+ * anchor back in the undrained-notes sink. */
41
+ notificationLane: NotificationLaneBindings;
69
42
  /** borrowed-mutable — the Runner's per-session parked-notification store: drained at turn open, re-pended on failure. */
70
43
  pendingSessionNotifications: PendingSessionNotifications;
71
44
  /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onDelegationLifecycle`, `onNotice`). */
@@ -92,5 +65,13 @@ export interface RunIdentityWiringResult {
92
65
  };
93
66
  /** This leg's usage counters, zeroed here; the harness-handlers lane is their writer from the first turn on. */
94
67
  stats: Stats;
68
+ /** backlog #389 — the held agent_end account, a READ FACE over this lane's own `let` (the undrained-inputs sink writes it
69
+ * when the run committed a durable park); the terminal-adoption lane settles it minus what the park carried. */
70
+ undrainedUserAtEnd: {
71
+ readonly current: {
72
+ steer: number;
73
+ followUp: number;
74
+ } | undefined;
75
+ };
95
76
  }
96
77
  export declare function runIdentityWiring(input: RunIdentityWiringInput): RunIdentityWiringResult;