@sema-agent/core 7.11.2 → 7.13.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 (91) hide show
  1. package/CHANGELOG.md +86 -13
  2. package/dist/core/ask-origin.d.ts +55 -0
  3. package/dist/core/ask-origin.js +21 -0
  4. package/dist/core/auto-mode-arming.d.ts +10 -14
  5. package/dist/core/auto-mode-arming.js +3 -9
  6. package/dist/core/auto-mode-defaults.d.ts +0 -2
  7. package/dist/core/auto-mode-defaults.js +0 -1
  8. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  9. package/dist/core/auto-mode-rebuild.js +0 -2
  10. package/dist/core/auto-mode.d.ts +30 -89
  11. package/dist/core/auto-mode.js +12 -59
  12. package/dist/core/checkpoint-store.d.ts +1 -3
  13. package/dist/core/engine-notice.d.ts +18 -0
  14. package/dist/core/gate-fold.js +1 -9
  15. package/dist/core/gate-lanes.d.ts +0 -40
  16. package/dist/core/gate-lanes.js +18 -26
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +2 -0
  19. package/dist/core/hooks.d.ts +7 -1
  20. package/dist/core/hooks.js +1 -1
  21. package/dist/core/runner/assemble-result.d.ts +45 -1
  22. package/dist/core/runner/assemble-result.js +4 -1
  23. package/dist/core/runner/compaction-seams.d.ts +42 -0
  24. package/dist/core/runner/compaction-seams.js +80 -0
  25. package/dist/core/runner/contracts.d.ts +85 -15
  26. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  27. package/dist/core/runner/denial-limit-arms.js +12 -9
  28. package/dist/core/runner/gate-exit.js +9 -1
  29. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  30. package/dist/core/runner/permission-rule-lanes.js +2 -1
  31. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  32. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  33. package/dist/core/runner/prepare-policy-chain.js +3 -2
  34. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  35. package/dist/core/runner/prepare-safety-scan.js +11 -16
  36. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  37. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  38. package/dist/core/runner/prepare-task.d.ts +9 -1
  39. package/dist/core/runner/prepare-task.js +7 -2
  40. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  41. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  42. package/dist/core/runner/resume-claim.d.ts +2 -2
  43. package/dist/core/runner/resume-preflight.d.ts +2 -2
  44. package/dist/core/runner/run-attachment-seats.d.ts +6 -4
  45. package/dist/core/runner/run-attachment-seats.js +2 -2
  46. package/dist/core/runner/run-git-lane.d.ts +1 -1
  47. package/dist/core/runner/run-harness-handlers.js +2 -0
  48. package/dist/core/runner/run-identity-wiring.d.ts +21 -33
  49. package/dist/core/runner/run-identity-wiring.js +6 -3
  50. package/dist/core/runner/run-leg.d.ts +108 -0
  51. package/dist/core/runner/run-leg.js +713 -0
  52. package/dist/core/runner/run-notification-lane.d.ts +56 -0
  53. package/dist/core/runner/run-notification-lane.js +128 -0
  54. package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
  55. package/dist/core/runner/run-reasoning-seat.js +7 -7
  56. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  57. package/dist/core/runner/run-recovery-lanes.js +5 -4
  58. package/dist/core/runner/run-settle-and-teardown.d.ts +113 -0
  59. package/dist/core/runner/run-settle-and-teardown.js +432 -0
  60. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  61. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  62. package/dist/core/runner/run-terminal-adoption.js +200 -0
  63. package/dist/core/runner/run-turn-boundary.js +5 -4
  64. package/dist/core/runner/runtask.d.ts +10 -79
  65. package/dist/core/runner/runtask.js +83 -1557
  66. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  67. package/dist/core/runner-deps.d.ts +4 -14
  68. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  69. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  70. package/dist/core/task-stream.d.ts +10 -1
  71. package/dist/core/tool-face.d.ts +8 -0
  72. package/dist/core/tool-face.js +1 -0
  73. package/dist/core/tool-policy.d.ts +42 -96
  74. package/dist/core/tool-policy.js +1 -11
  75. package/dist/core/tool-registry.d.ts +11 -3
  76. package/dist/core/tool-registry.js +7 -1
  77. package/dist/core/tool-roster.d.ts +26 -0
  78. package/dist/core/tool-roster.js +38 -7
  79. package/dist/core/trace.d.ts +6 -7
  80. package/dist/core/wiring-manifest.d.ts +5 -22
  81. package/dist/core/wiring-manifest.js +3 -11
  82. package/dist/core/workflow-journal-store.d.ts +35 -4
  83. package/dist/core/workflow-journal-store.js +19 -2
  84. package/dist/index.d.ts +6 -5
  85. package/dist/index.js +6 -5
  86. package/dist/orchestration/workflow.js +2 -0
  87. package/dist/server/http.js +2 -1
  88. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  89. package/dist/stores/file/workflow-journal-store.js +2 -4
  90. package/package.json +1 -1
  91. package/test/export-surface.snapshot.json +21 -11
@@ -41,6 +41,9 @@ function assistantText(msg) {
41
41
  .join("");
42
42
  return stripThinkTags(text);
43
43
  }
44
+ export function answerSettled(final, continuationCut = false) {
45
+ return final?.stopReason === "stop" && final.partialFinalized !== true && !continuationCut;
46
+ }
44
47
  export function terminalCauseOf(final, stats, flags) {
45
48
  let terminal;
46
49
  let apiFailure;
@@ -96,7 +99,7 @@ export function terminalCauseOf(final, stats, flags) {
96
99
  else if (flags.paused) {
97
100
  terminal = flags.paused;
98
101
  }
99
- else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal) {
102
+ else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal && flags.answerSettled !== true) {
100
103
  const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
101
104
  terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
102
105
  }
@@ -0,0 +1,42 @@
1
+ import type { MaybeCompactOptions } from "../auto-compaction.js";
2
+ import type { TaskSpec } from "../types.js";
3
+ import type { Prepared, RunnerDepsSeat } from "./contracts.js";
4
+ /**
5
+ * design/84 Seam C — the cost-optimization compaction options threaded into BOTH `maybeCompact` call
6
+ * sites (within-task turn boundary + `finish()`). All three fields come from `RunnerDeps` (a trusted
7
+ * FUNCTION seam — never `TaskSpec`, which is serializable/durable-resumable/untrusted-caller). The
8
+ * Runner OWNS the consecutive-reuse counter (`prepared.compactionReuseRef`) so the
9
+ * `maxConsecutiveProviderReuse` drift guard spans the whole task across both sites: it FEEDS the current
10
+ * count in as `consecutiveProviderReuse`, and {@link recordCompactionReuse} updates it from the result.
11
+ * Returns `undefined` when no provider is wired (so the call site spreads nothing → byte-identical to
12
+ * the pre-design/84 behavior).
13
+ */
14
+ export declare function seamCCompactionOptions(runner: RunnerDepsSeat, prepared: Prepared): Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse"> | undefined;
15
+ /** design/134 §3.2 — resolve the pre/postCompact lifecycle callbacks (whole-slot `spec.hooks ??
16
+ * deps.hooks`, same resolution as the stop hook) and wrap each in a SWALLOW+TRACE shell before
17
+ * threading them into maybeCompact. The wrapper owns the observability half of the R3 MED contract
18
+ * (maybeCompact swallows defensively too, but has no sink): a throwing callback is reported via
19
+ * `onError(phase:"hook")` and treated as absent; a `block` returned under a "forced" trigger is
20
+ * reported as ignored (maybeCompact enforces the ignore — blocking a compaction the provider/trim
21
+ * layer already demanded would kill the run). */
22
+ export declare function compactionHookOptions(runner: RunnerDepsSeat, spec: TaskSpec, sessionId: string, trigger: "auto" | "manual" | "forced",
23
+ /** #281 件A — the leg's identity envelope, spread onto both compaction contexts by the wrappers
24
+ * below (the compaction module builds its contexts below the runner and cannot know the leg;
25
+ * the wrapper is the runner's seam). Every runner call site supplies `prepared.hookIdentity`;
26
+ * optional only so a context built without a leg stays honestly identity-less. */
27
+ identity?: import("../hooks.js").HookInvocationIdentity,
28
+ /** The leg's already-validated seat bound (`Prepared.hookTimeoutMs`) and its abort signal. Optional
29
+ * for the same reason `identity` is — a wrapper built without a prepared leg still works, on the
30
+ * default bound and with no signal. The signal is what keeps a hung compaction hook from holding a
31
+ * CANCELLED run's turn boundary for the whole deadline (the seat's post-abort grace). */
32
+ seatBound?: {
33
+ timeoutMs: number;
34
+ signal?: AbortSignal;
35
+ }): Pick<MaybeCompactOptions, "trigger" | "preCompact" | "postCompact">;
36
+ /** design/84 Seam C — fold a finished compaction's `reused` flag into the run-scoped consecutive-reuse
37
+ * counter: a reused (provider) summary increments it, a real (LLM) summary resets it to 0. A no-op
38
+ * compaction (`compacted:false`) leaves the counter untouched. No-op when no provider is wired. */
39
+ export declare function recordCompactionReuse(runner: RunnerDepsSeat, prepared: Prepared, comp: {
40
+ compacted: boolean;
41
+ reused?: boolean;
42
+ }): void;
@@ -0,0 +1,80 @@
1
+ import { hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
2
+ export function seamCCompactionOptions(runner, prepared) {
3
+ if (!runner.deps.summaryProvider)
4
+ return undefined;
5
+ return {
6
+ summaryProvider: runner.deps.summaryProvider,
7
+ onCompaction: runner.deps.onCompaction,
8
+ maxConsecutiveProviderReuse: runner.deps.maxConsecutiveProviderReuse,
9
+ consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
10
+ };
11
+ }
12
+ export function compactionHookOptions(runner, spec, sessionId, trigger, identity, seatBound) {
13
+ const hooks = spec.hooks ?? runner.deps.hooks;
14
+ const pre = hooks?.preCompact;
15
+ const post = hooks?.postCompact;
16
+ const seatMs = seatBound?.timeoutMs ?? resolveHookTimeoutMs(hooks?.timeoutMs);
17
+ const seatSignal = seatBound?.signal;
18
+ const withIdentity = (ctx) => identity !== undefined ? { ...ctx, identity } : ctx;
19
+ return {
20
+ trigger,
21
+ ...(pre
22
+ ? {
23
+ preCompact: async (rawCtx) => {
24
+ const ctx = withIdentity(rawCtx);
25
+ const report = (err) => {
26
+ try {
27
+ runner.deps.onError?.(err, { phase: "hook", sessionId });
28
+ }
29
+ catch {
30
+ }
31
+ };
32
+ try {
33
+ const seat = await runHookSeat("preCompact", { timeoutMs: seatMs, ...(seatSignal !== undefined ? { signal: seatSignal } : {}) }, (sig) => pre.call(hooks, { ...ctx, signal: sig }));
34
+ if (seat.expired) {
35
+ report(hookSeatExpiredError("preCompact", seatMs, seat.cause, `the "${ctx.trigger}" compaction PROCEEDED unblocked and with no additional instructions`));
36
+ return undefined;
37
+ }
38
+ const r = seat.value;
39
+ if (r?.block && ctx.trigger === "forced") {
40
+ report(new Error(`a preCompact callback blocked a "forced" compaction — ignored (PTL/trim-pressure compaction is not optional): ${r.block}`));
41
+ }
42
+ return r;
43
+ }
44
+ catch (err) {
45
+ report(err instanceof Error ? err : new Error(String(err)));
46
+ return undefined;
47
+ }
48
+ },
49
+ }
50
+ : {}),
51
+ ...(post
52
+ ? {
53
+ postCompact: async (rawCtx) => {
54
+ try {
55
+ const seat = await runHookSeat("postCompact", { timeoutMs: seatMs, ...(seatSignal !== undefined ? { signal: seatSignal } : {}) }, (sig) => post.call(hooks, { ...withIdentity(rawCtx), signal: sig }));
56
+ if (seat.expired) {
57
+ try {
58
+ runner.deps.onError?.(hookSeatExpiredError("postCompact", seatMs, seat.cause, "the observation was abandoned; the compaction that already landed is unchanged"), { phase: "hook", sessionId });
59
+ }
60
+ catch {
61
+ }
62
+ }
63
+ }
64
+ catch (err) {
65
+ try {
66
+ runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
67
+ }
68
+ catch {
69
+ }
70
+ }
71
+ },
72
+ }
73
+ : {}),
74
+ };
75
+ }
76
+ export function recordCompactionReuse(runner, prepared, comp) {
77
+ if (!runner.deps.summaryProvider || !comp.compacted)
78
+ return;
79
+ prepared.compactionReuseRef.consecutive = comp.reused ? prepared.compactionReuseRef.consecutive + 1 : 0;
80
+ }
@@ -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";
@@ -652,6 +652,24 @@ export interface Prepared {
652
652
  callIssuedAtRef: {
653
653
  current?: number;
654
654
  };
655
+ /** #677 — was the run's WORK cut by a stop? Two observation points, one record, read by the answer-settled
656
+ * verdict (`answerSettled`) beside the harness's own undrained-input account:
657
+ * - `continuation` — the loop decided to CONTINUE past its last assistant turn and the abort caught it
658
+ * before that continuation dispatched. Written by the loop-trace sink from the loop's own steps: a
659
+ * `continue` step (a drained steer / follow-up, a truncated-output or thinking-only or degenerate-output
660
+ * nudge, a next tool turn) sets it; the terminal step clears it unless the loop exited at its
661
+ * pre-dispatch abort guard (`aborted_before_stream`) — the one exit where `final` is still the PREVIOUS
662
+ * turn's message. A clean "stop" the loop itself had decided was not the end is not a settled answer.
663
+ * - `abortedBeforeTurnEnd` — the run's abort was already in force when a turn ENDED (written by the
664
+ * `turn_end` handler before the turns cap fires its own abort). The ruling's line is the last
665
+ * `turn_end`: a stop that landed before it landed on work — even when the brain ignored the signal
666
+ * and delivered a full "stop" answer through it (the loop then ends at its own decision, so the
667
+ * continuation bit never sets); a stop that landed after it landed on the tail.
668
+ * Always present; both false on a run nothing stopped. */
669
+ workCutRef: {
670
+ continuation: boolean;
671
+ abortedBeforeTurnEnd: boolean;
672
+ };
655
673
  /** RB-458 — records the FIRST brain call this run's outer guardrail gave up on (see
656
674
  * {@link import("../../brain/timeout.js").withBrainCallGuardrail}). Always present; `timedOut`
657
675
  * stays absent unless the guardrail fired. The run loop reads it AFTER the loop settles and gives
@@ -1778,14 +1796,6 @@ export interface RunInternals {
1778
1796
  * channel, same posture as every other field here.
1779
1797
  */
1780
1798
  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
1799
  onTaskNotification?: (notification: TaskNotificationPayload,
1790
1800
  /** Injection tier (design/373 — the ladder is LIVE): "next" = the running turn's next boundary
1791
1801
  * (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
@@ -2340,13 +2350,11 @@ export interface TurnBoundaryDeps {
2340
2350
  drainManualCompact: (outcome: CompactOutcome) => void;
2341
2351
  runnerHooks: {
2342
2352
  onError: RunnerDeps["onError"];
2343
- seamCCompactionOptions: (prepared: Prepared) => Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse"> | undefined;
2344
- compactionHookOptions: (spec: TaskSpec, sessionId: string, trigger: "auto" | "manual" | "forced") => Pick<MaybeCompactOptions, "trigger" | "preCompact" | "postCompact">;
2345
- recordCompactionReuse: (prepared: Prepared, comp: {
2346
- compacted: boolean;
2347
- reused?: boolean;
2348
- }) => void;
2349
2353
  };
2354
+ /** The Runner's deployment deps, read LIVE ({@link RunnerDepsSeat}): the compaction seams (compaction-seams.ts) the
2355
+ * boundary's compaction pass reaches down for read the summary provider, the hooks slot and the error sink through it
2356
+ * on every pass — the three Runner methods that used to sit on `runnerHooks` as delegates (design/393 S7). */
2357
+ runner: RunnerDepsSeat;
2350
2358
  }
2351
2359
  /** design/157 B15 尾件 — R5(harness 事件处理器族)的依赖包。全部为 runLocked 内声明顺序早于
2352
2360
  * 工厂调用点的 const 稳定引用 + 三个 runLocked 形参;this 面(deps.onError)经 runnerHooks 打包
@@ -2431,6 +2439,28 @@ export interface RunnerSelfSeat {
2431
2439
  /** Streaming form of {@link RunnerSelfSeat.resume}: the pre-CAS guards and the CAS run first, then the live stream is returned. */
2432
2440
  resumeStream(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, internals?: RunInternals): Promise<TaskStream>;
2433
2441
  }
2442
+ /**
2443
+ * design/393 S7 (#675) — the orchestrator's entry (`prepareTask`, prepare-task.ts) as ONE contract: the notification lane's
2444
+ * `prepareTask` seat names it, and the orchestrator pins its own declaration against it (`PrepareTaskIsTheContract`), so a
2445
+ * signature change reds at the declaration — not at the driver's hand-in, and not as a seat spelled a second time. It lives
2446
+ * on the floor because a lane may not name the orchestrator (docs/LAYERING.json: layer 4 sits above the lanes). Positional,
2447
+ * exactly as the entry is declared — the seat carries the orchestrator's function itself, not a lane Input.
2448
+ */
2449
+ export type PrepareTaskFn = (spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals, runnerSelf?: RunnerSelfSeat, runIdSink?: {
2450
+ runId?: string;
2451
+ }) => Promise<Prepared>;
2452
+ /**
2453
+ * design/393 S7 (#670) — the Result of an INSTALLING phase: a lane whose every product is installed on a seat it borrowed
2454
+ * (the harness's stop gate and recovery chain, the run state's counter groups, the stream's result setter, the claimed
2455
+ * row's registries) hands nothing back, and says so by extending this marker. The marker is machine-read by gate:phase-api
2456
+ * (design/238 R-1's third clause): a Result with no members MUST extend it, a Result with members MAY NOT, and no Result
2457
+ * inherits anything else — so an empty Result is a stated fact about the phase, never an interface someone forgot to
2458
+ * fill, and a phase that starts handing a product back must drop the declaration in the same edit. Six lanes carry it:
2459
+ * the attachment seats, the stop gate, the recovery lanes, the settle and teardown (run-), the preflight and the claim
2460
+ * (resume-).
2461
+ */
2462
+ export interface InstallingPhaseResult {
2463
+ }
2434
2464
  /** The live-task handle `runLocked` publishes once the harness exists (design/47): the harness + abort
2435
2465
  * controller, the loop-liveness latch (`ended` flips when the single `harness.prompt` settles; `userInterrupted`
2436
2466
  * / `userHalted` are the interrupt and halt verbs' attribution seats), the run's reminder mark, its session and
@@ -2475,6 +2505,46 @@ export interface ManualCompactRef {
2475
2505
  emitMooted?: (reason: string) => void;
2476
2506
  closed?: boolean;
2477
2507
  }
2508
+ /**
2509
+ * design/393 S6 — the end-of-task compaction pass's outcome (`Runner.finish`'s return): what the terminal-adoption lane
2510
+ * hands the settle lane, which reads it for the `compacted` frame, the phase-timings frame and the detector reset. Named
2511
+ * once here so the two lanes and the method spell one type.
2512
+ */
2513
+ export type EndOfTaskCompaction = {
2514
+ compacted: boolean;
2515
+ tokensBefore?: number;
2516
+ triggerTokens?: number;
2517
+ postTriggerTokens?: number;
2518
+ durationMs?: number;
2519
+ phaseDurations?: CompactionPhaseDurations;
2520
+ firstKeptEntryId?: string;
2521
+ attachedFiles?: Array<{
2522
+ path: string;
2523
+ chars: number;
2524
+ truncated: boolean;
2525
+ }>;
2526
+ modelFallback?: true;
2527
+ fallbackReason?: "window";
2528
+ clampedRatio?: number;
2529
+ clampReason?: "budget" | "tolerance";
2530
+ } | undefined;
2531
+ /**
2532
+ * design/393 S6 — the notification lane's BINDINGS: the run-notification-lane's own four `let`s as ONE seat (getters
2533
+ * and setters over the lane's variables, never a copy). The identity-wiring lane binds `harness` / `sessionId` /
2534
+ * `ident` the moment `prepared` exists (the lane's closures read them by variable — the routing listener, the park
2535
+ * destination, the injection entry); the leg lane flips `live` in its finally, from which point every notification
2536
+ * parks per session for the next run. One seat, four slots; nothing copies a binding out of it.
2537
+ */
2538
+ export interface NotificationLaneBindings {
2539
+ harness: AgentHarness | undefined;
2540
+ sessionId: string | undefined;
2541
+ ident: () => {
2542
+ eventId?: string;
2543
+ parentToolCallId?: string;
2544
+ sourceTaskId?: string;
2545
+ };
2546
+ live: boolean;
2547
+ }
2478
2548
  /** design/144 §2 — the `notify()` bridge: `runLocked` binds `inject` the moment the task-notification lane exists. */
2479
2549
  export interface NotifyRef {
2480
2550
  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,8 +1,8 @@
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";
5
- import { askOriginOf, classifierMayAnswer } from "../ask-origin.js";
5
+ import { askOriginOf, classifierMayAnswer, probeMandatedAsk } from "../ask-origin.js";
6
6
  export function attachRebuiltDenialTrackers(entries, denialLimit) {
7
7
  if (entries === undefined)
8
8
  return undefined;
@@ -76,8 +76,9 @@ export function inheritedAskCarry(judged, liveApprover, tracker) {
76
76
  export async function judgeInheritedClassifier(opts) {
77
77
  const { autoMode, ask, req } = opts;
78
78
  const origin = askOriginOf(ask, INHERITED_STATION_FACTS);
79
- if (autoMode === undefined || !classifierMayAnswer(origin))
79
+ if (autoMode === undefined || !classifierMayAnswer(origin) || probeMandatedAsk(ask)) {
80
80
  return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
81
+ }
81
82
  const verdict = await autoMode.decider
82
83
  .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
83
84
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -85,19 +86,21 @@ export async function judgeInheritedClassifier(opts) {
85
86
  autoMode.denialTracking?.recordAllow();
86
87
  return { kind: "allow" };
87
88
  }
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 };
89
+ if (verdict.kind === "unavailable") {
90
+ return {
91
+ kind: "deny",
92
+ result: { action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier", classifierUnavailable: { cause: verdict.cause } },
93
+ };
91
94
  }
92
- const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
93
- const category = verdict.category ? inlineUntrusted(verdict.category) : "";
95
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
96
+ const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
94
97
  const tracked = autoMode.denialTracking?.recordBlock();
95
98
  if (tracked?.limitReached !== true) {
96
99
  return {
97
100
  kind: "deny",
98
101
  result: {
99
102
  action: "deny",
100
- message: `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
103
+ 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
104
  decisionReason: "classifier",
102
105
  },
103
106
  };
@@ -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),
@@ -26,7 +26,8 @@
26
26
  * silence the ungated-write warning for deployments that wired no policy at all.
27
27
  */
28
28
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
29
- import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
29
+ import { persistedRuleMandateOf } from "../ask-origin.js";
30
+ import { type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
30
31
  import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
31
32
  import { type OrgRuleResolution } from "../permission-rule-org.js";
32
33
  import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
@@ -1,4 +1,5 @@
1
- import { persistedRuleMandateOf } from "../hooks.js";
1
+ import { persistedRuleMandateOf } from "../ask-origin.js";
2
+ import {} from "../hooks.js";
2
3
  import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
4
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
@@ -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,
@@ -10,6 +10,7 @@ import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
10
10
  import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
11
11
  import { emitTrace } from "../trace.js";
12
12
  import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
13
+ import { probeMandatedAsk } from "../ask-origin.js";
13
14
  import { headlessDenyAtFold, headlessDenyAtRecheck, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
14
15
  import { askGrantShapeOf } from "./inherited-ask-grants.js";
15
16
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
@@ -374,7 +375,7 @@ export async function preparePolicyChain(input) {
374
375
  if (judged.kind === "deny")
375
376
  return judged.result;
376
377
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
377
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
378
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName) && !probeMandatedAsk(inheritedAsk)) {
378
379
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
379
380
  return { action: "allow" };
380
381
  }
@@ -454,7 +455,7 @@ export async function preparePolicyChain(input) {
454
455
  if (judged.kind === "deny")
455
456
  return judged.result;
456
457
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
457
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
458
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName) && !probeMandatedAsk(inheritedAsk)) {
458
459
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
459
460
  return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
460
461
  }
@@ -3,12 +3,17 @@ import { materializeMcpTools } from "../mcp.js";
3
3
  import { materializeA2aTools } from "../a2a.js";
4
4
  import { deliverEngineNotice } from "../types.js";
5
5
  import { RosterBuilder } from "../tool-roster.js";
6
+ import { TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
6
7
  import { REFRESH_MCP_TOOLS_TOOL_NAME, toolFace } from "../tool-catalog-entries.js";
7
8
  import { MCP_NAMESPACE, A2A_NAMESPACE } from "../protocol-table.js";
8
9
  import { mintNamespacePrefix } from "../protocol-naming.js";
9
10
  import { applyMcpToolFaces } from "./tool-face-overlay.js";
10
11
  export async function prepareProtocolTools(input) {
11
12
  const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback } = input;
13
+ const overPeer = (lockedPreflight.mcp ?? []).find((s) => s.name.length > TOOL_WIRE_NAME_MAX_CHARS);
14
+ if (overPeer !== undefined) {
15
+ throw Object.assign(new Error(`MCP server name ${JSON.stringify(overPeer.name.slice(0, 40) + "…")} is ${overPeer.name.length} characters; a peer rides every roster row of its tools and the roster's bound on a name is ${TOOL_WIRE_NAME_MAX_CHARS}.`), { code: "config.tool_name_too_long" });
16
+ }
12
17
  const mcp = lockedPreflight.mcp?.length
13
18
  ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps, runId))
14
19
  : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
@@ -1,7 +1,6 @@
1
1
  import { NAMESPACED_NAME_SHAPES } from "../protocol-table.js";
2
2
  import { engineCardTypes, handBandEffects } from "../tool-registry.js";
3
- import { TOOL_CONTRACT_MAX_CHARS, TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
4
- import { callerCardIdOf, offendingResultCard, schemaKeyBoundProblem } from "../tool-roster.js";
3
+ import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKeyBoundProblem } from "../tool-roster.js";
5
4
  import { toolFaceProblem } from "./tool-face-overlay.js";
6
5
  import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
7
6
  import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
@@ -16,23 +15,19 @@ export function prepareSafetyScan(input) {
16
15
  const axisExplicitNegatives = new Map();
17
16
  const reversibilityProbes = new Map();
18
17
  for (const t of spec.tools ?? []) {
19
- if (t.name.length > TOOL_WIRE_NAME_MAX_CHARS) {
20
- const e = new Error(`Tool name "${t.name.slice(0, 40)}…" is invalid: ${t.name.length} characters, the roster's bound is ${TOOL_WIRE_NAME_MAX_CHARS}.`);
21
- e.code = "config.tool_name_too_long";
22
- throw e;
23
- }
24
- for (const alias of t.aliases ?? []) {
25
- if (alias.length > TOOL_WIRE_NAME_MAX_CHARS) {
26
- const e = new Error(`Tool alias "${alias.slice(0, 40)}…" (of "${t.name}") is invalid: ${alias.length} characters, the roster's bound is ${TOOL_WIRE_NAME_MAX_CHARS}.`);
27
- e.code = "config.tool_name_too_long";
18
+ {
19
+ const bound = rosterMemberBoundProblem({
20
+ name: t.name,
21
+ aliases: t.aliases,
22
+ ...(t.contract !== undefined ? { contractId: t.contract.contractId, implementationRevision: t.contract.implementationRevision } : {}),
23
+ ...(typeof t.modelGate === "string" ? { modelGate: t.modelGate } : {}),
24
+ });
25
+ if (bound !== undefined) {
26
+ const e = new Error(`Tool "${t.name.slice(0, 40)}" cannot ride a roster row: ${bound.message}.`);
27
+ e.code = bound.code;
28
28
  throw e;
29
29
  }
30
30
  }
31
- if (t.contract !== undefined && (t.contract.contractId.length > TOOL_CONTRACT_MAX_CHARS || t.contract.implementationRevision.length > TOOL_CONTRACT_MAX_CHARS)) {
32
- const e = new Error(`Tool "${t.name}" declares a contract member over ${TOOL_CONTRACT_MAX_CHARS} characters (contractId ${t.contract.contractId.length}, implementationRevision ${t.contract.implementationRevision.length}) — the roster's bound.`);
33
- e.code = "config.tool_contract_too_long";
34
- throw e;
35
- }
36
31
  {
37
32
  const keyProblem = schemaKeyBoundProblem(t.parameters);
38
33
  if (keyProblem !== undefined) {
@@ -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 ||
@@ -8,7 +8,7 @@ export { mcpManifestEntries } from "./prepare-wiring-manifest.js";
8
8
  export { __resetMaterializeEnvAnnouncements } from "./prepare-tool-disclosure-mount.js";
9
9
  export { fileHistoryFilesystemIdentity, resolveFileHistoryScope } from "./prepare-file-history.js";
10
10
  import type { RunnerSelfSeat } from "./contracts.js";
11
- import type { Prepared, PrepareResume, RunInternals } from "./contracts.js";
11
+ import type { Prepared, PrepareResume, PrepareTaskFn, RunInternals } from "./contracts.js";
12
12
  export type { FileHistoryBoundarySeat, InheritedGate, Prepared, PreparedMicroCompact, PrepareResume, ResolvedWorkspace, RunInternals, UsageGovernance } from "./contracts.js";
13
13
  import type { ExecutionEnv } from "../../internal/harness.js";
14
14
  import type { RunnerDeps, TaskSpec } from "../types.js";
@@ -114,3 +114,11 @@ runnerSelf?: RunnerSelfSeat,
114
114
  runIdSink?: {
115
115
  runId?: string;
116
116
  }): Promise<Prepared>;
117
+ /** design/393 S7 (#675) — the entry IS the contract the notification lane's seat names ({@link PrepareTaskFn}, contracts.ts):
118
+ * a signature change reds HERE, at the declaration, as `false` failing the `true` bound. IDENTITY, not assignability (review
119
+ * round 1): the parameter tuple (arity, optionality, each type) and the return type must be the SAME type in both directions —
120
+ * one-way `extends` let a dropped trailing parameter, an added optional one or a narrowed return slip through. Type-level only —
121
+ * nothing is emitted. */
122
+ type Holds<T extends true> = T;
123
+ type Same<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
124
+ export type PrepareTaskIsTheContract = Holds<Same<Parameters<typeof prepareTask>, Parameters<PrepareTaskFn>>> & Holds<Same<ReturnType<typeof prepareTask>, ReturnType<PrepareTaskFn>>> & Holds<Same<ThisParameterType<typeof prepareTask>, ThisParameterType<PrepareTaskFn>>> & Holds<Same<typeof prepareTask, PrepareTaskFn>>;