@sema-agent/core 7.17.6 → 7.18.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 (164) hide show
  1. package/CHANGELOG.md +165 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/route-adjudicator.d.ts +18 -0
  8. package/dist/brain/route-adjudicator.js +16 -0
  9. package/dist/core/agent-definition.d.ts +10 -0
  10. package/dist/core/artifact-host.d.ts +41 -4
  11. package/dist/core/artifact-host.js +18 -2
  12. package/dist/core/auto-mode.d.ts +141 -45
  13. package/dist/core/auto-mode.js +109 -3
  14. package/dist/core/checkpoint-store.d.ts +16 -2
  15. package/dist/core/engine-notice.d.ts +47 -1
  16. package/dist/core/engine-notice.js +7 -0
  17. package/dist/core/gate-lanes.js +2 -1
  18. package/dist/core/gate-outcome.d.ts +17 -2
  19. package/dist/core/gate-outcome.js +16 -0
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +8 -0
  22. package/dist/core/hook-seats.d.ts +66 -0
  23. package/dist/core/hook-seats.js +37 -0
  24. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  25. package/dist/core/runner/assemble-result.d.ts +2 -1
  26. package/dist/core/runner/assemble-result.js +1 -1
  27. package/dist/core/runner/classifier-ladder.d.ts +89 -0
  28. package/dist/core/runner/classifier-ladder.js +65 -0
  29. package/dist/core/runner/contracts.d.ts +69 -13
  30. package/dist/core/runner/decide-continuation.d.ts +1 -3
  31. package/dist/core/runner/decide-continuation.js +1 -7
  32. package/dist/core/runner/deferral-census.d.ts +52 -0
  33. package/dist/core/runner/deferral-census.js +44 -0
  34. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  35. package/dist/core/runner/derived-route-fallback.js +8 -3
  36. package/dist/core/runner/gate-exit.d.ts +12 -0
  37. package/dist/core/runner/gate-exit.js +6 -3
  38. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  39. package/dist/core/runner/prepare-artifact.js +5 -4
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  41. package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
  42. package/dist/core/runner/prepare-config-doors.d.ts +1 -1
  43. package/dist/core/runner/prepare-config-doors.js +5 -3
  44. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  45. package/dist/core/runner/prepare-defer-classify.js +8 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  47. package/dist/core/runner/prepare-gate-stations.js +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  49. package/dist/core/runner/prepare-hands-readface.js +60 -24
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  51. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  52. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  53. package/dist/core/runner/prepare-lsp.js +1 -1
  54. package/dist/core/runner/prepare-policy-chain.js +2 -0
  55. package/dist/core/runner/prepare-project-context.js +4 -2
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  57. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  58. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  59. package/dist/core/runner/prepare-run-refs.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  61. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  62. package/dist/core/runner/prepare-task.js +18 -13
  63. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  64. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  65. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  66. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  70. package/dist/core/runner/run-harness-handlers.js +4 -1
  71. package/dist/core/runner/run-identity-wiring.js +6 -2
  72. package/dist/core/runner/run-leg.d.ts +2 -0
  73. package/dist/core/runner/run-leg.js +35 -41
  74. package/dist/core/runner/run-notification-lane.js +55 -27
  75. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  76. package/dist/core/runner/run-terminal-adoption.js +16 -13
  77. package/dist/core/runner/run-turn-boundary.js +3 -1
  78. package/dist/core/runner/runtask.d.ts +6 -5
  79. package/dist/core/runner/runtask.js +26 -14
  80. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  81. package/dist/core/runner/skill-base-dir.js +42 -0
  82. package/dist/core/runner/synthetic-tools.d.ts +16 -19
  83. package/dist/core/runner/synthetic-tools.js +18 -10
  84. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  85. package/dist/core/runner/tool-defer-gate.js +15 -5
  86. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  87. package/dist/core/runner/tool-disclosure.js +2 -0
  88. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  89. package/dist/core/runner/tool-progress-lane.js +61 -0
  90. package/dist/core/runner-deps.d.ts +48 -7
  91. package/dist/core/swappable-deps.d.ts +9 -7
  92. package/dist/core/swappable-deps.js +7 -3
  93. package/dist/core/task-event.d.ts +96 -29
  94. package/dist/core/task-notification.d.ts +142 -16
  95. package/dist/core/task-notification.js +106 -0
  96. package/dist/core/task-result.d.ts +9 -1
  97. package/dist/core/task-spec.d.ts +20 -3
  98. package/dist/core/tighten-task-spec.js +7 -5
  99. package/dist/core/tool-model-gate.d.ts +1 -1
  100. package/dist/core/tool-search-setting.d.ts +78 -0
  101. package/dist/core/tool-search-setting.js +90 -0
  102. package/dist/core/tool-spec.d.ts +8 -0
  103. package/dist/core/types.d.ts +1 -1
  104. package/dist/core/types.js +1 -1
  105. package/dist/core/wiring-manifest.d.ts +46 -0
  106. package/dist/core/wiring-manifest.js +9 -1
  107. package/dist/engine/execution-env/node-execution-env.js +10 -4
  108. package/dist/engine/harness/agent-harness.d.ts +42 -0
  109. package/dist/engine/harness/agent-harness.js +61 -29
  110. package/dist/engine/loop/agent-loop.d.ts +68 -3
  111. package/dist/engine/loop/agent-loop.js +109 -16
  112. package/dist/index.d.ts +10 -5
  113. package/dist/index.js +9 -5
  114. package/dist/internal/harness.d.ts +1 -1
  115. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  116. package/dist/orchestration/run-workflow-tool.js +6 -1
  117. package/dist/orchestration/workflow-governance.d.ts +7 -1
  118. package/dist/orchestration/workflow-governance.js +9 -5
  119. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  120. package/dist/orchestration/workflow-primitives.js +6 -1
  121. package/dist/orchestration/workflow-types.d.ts +48 -13
  122. package/dist/orchestration/workflow-types.js +1 -1
  123. package/dist/orchestration/workflow.js +45 -17
  124. package/dist/prompt-assembly/epoch.js +2 -0
  125. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  126. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  127. package/dist/prompt-assembly/section-usage.js +10 -0
  128. package/dist/prompt-assembly/types.d.ts +8 -6
  129. package/dist/prompts/default.d.ts +13 -0
  130. package/dist/prompts/default.js +16 -8
  131. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  132. package/dist/tools/artifact/artifact-text.js +6 -3
  133. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  134. package/dist/tools/artifact/artifact-tool.js +203 -70
  135. package/dist/tools/artifact/conformance.d.ts +56 -0
  136. package/dist/tools/artifact/conformance.js +349 -0
  137. package/dist/tools/artifact/local-stub.js +1 -1
  138. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  139. package/dist/tools/artifact/reject-breaker.js +42 -0
  140. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  141. package/dist/tools/fs/bash-lexer.js +563 -0
  142. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  143. package/dist/tools/fs/bash-program-position.js +125 -0
  144. package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
  145. package/dist/tools/fs/bash-readonly-classifier.js +375 -530
  146. package/dist/tools/fs/containment-face.d.ts +89 -0
  147. package/dist/tools/fs/containment-face.js +81 -0
  148. package/dist/tools/fs/fs-bash.d.ts +46 -1
  149. package/dist/tools/fs/fs-bash.js +158 -22
  150. package/dist/tools/fs/fs-read.d.ts +1 -1
  151. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  152. package/dist/tools/fs/fs-write.d.ts +4 -3
  153. package/dist/tools/fs/fs-write.js +9 -9
  154. package/dist/tools/fs/index.d.ts +24 -5
  155. package/dist/tools/fs/index.js +18 -6
  156. package/dist/tools/fs/repo-map.d.ts +2 -2
  157. package/dist/tools/fs/safety.d.ts +3 -3
  158. package/dist/tools/fs/safety.js +4 -4
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +85 -9
  161. package/wire-changes/core@7.17.6.json +81 -0
  162. package/wire-changes/core@7.17.7.json +49 -0
  163. package/dist/tools/fs/read-face.d.ts +0 -68
  164. package/dist/tools/fs/read-face.js +0 -47
@@ -184,6 +184,22 @@ export function fallbackToPrimaryNotice(args) {
184
184
  },
185
185
  };
186
186
  }
187
+ export function unroutableSeatNotice(args) {
188
+ return {
189
+ code: "route.classifier_unroutable",
190
+ message: `derived model "${args.from}" (${args.seat}) is not routable (${args.verdict.code}) and this deployment declared no fallback — ` +
191
+ `the seat is kept and its calls are refused rather than served by "${args.primary}". ${args.verdict.detail.fixHint}`,
192
+ detail: {
193
+ seat: args.seat,
194
+ from: args.from,
195
+ cause: args.verdict.code,
196
+ fixHint: args.verdict.detail.fixHint,
197
+ action: "refused",
198
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
199
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
200
+ },
201
+ };
202
+ }
187
203
  export function authCarrierFingerprint(headers) {
188
204
  if (!headers)
189
205
  return "";
@@ -169,4 +169,14 @@ export interface SkillSpec {
169
169
  path: string;
170
170
  content: string;
171
171
  }>;
172
+ /**
173
+ * The skill's own DIRECTORY on the machine the tools run on, so the model can reach its bundled material
174
+ * by path instead of searching the workspace. Core does not resolve it, read it, or check that it exists
175
+ * (the brain-outside posture {@link files} states). Core does exactly two textual things with it: the
176
+ * result opens with `Base directory for this skill: <baseDir>`, and both root placeholder spellings
177
+ * (`${SKILL_ROOT}`, `${CLAUDE_PLUGIN_ROOT}`) are substituted for it in the body and in every attachment's
178
+ * content, with `files[].path` then read RELATIVE to it. Absent ⇒ no line, no substitution, a
179
+ * byte-identical result; a placeholder with no directory stays verbatim.
180
+ */
181
+ baseDir?: string;
172
182
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * The Artifacts hosting seam: the deployment-injected `ArtifactHost` (the `RunnerDeps.artifactHost` seat),
3
+ * its capability declaration, the call context the engine mints for every host call, the request /
4
+ * outcome shapes, and the closed word sets the tool face is assembled from. A host declares WHAT it
5
+ * supports (`capabilities()`); the engine assembles the `Artifact` tool from that declaration alone and
6
+ * never inspects the implementation. `localArtifactStub` (src/tools/artifact/local-stub.ts) is the first
7
+ * implementation of this interface; a server's hosting plane is the second.
8
+ */
1
9
  import type { AssertAllKeysHandled } from "./ask-origin.js";
2
10
  /** The model-facing action words. `publish` is the default action and the one every host must declare. */
3
11
  export declare const ARTIFACT_ACTIONS: readonly ["publish", "list", "read", "list_files", "read_file"];
@@ -6,6 +14,17 @@ export type ArtifactAction = (typeof ARTIFACT_ACTIONS)[number];
6
14
  * `"hosted"` = a hosting plane that mints its own URLs. Declared by the host, never detected. */
7
15
  export declare const ARTIFACT_PLANES: readonly ["hosted", "local-stub"];
8
16
  export type ArtifactPlane = (typeof ARTIFACT_PLANES)[number];
17
+ /** How a throw out of `publish` relates to the host's commit point. `"throw_means_uncommitted"`: every fallible
18
+ * step precedes the commit, so a throw proves nothing was committed; `"uncertain"`: a throw may follow it. */
19
+ export declare const ARTIFACT_COMMIT_CERTAINTIES: readonly ["throw_means_uncommitted", "uncertain"];
20
+ export type ArtifactCommitCertainty = (typeof ARTIFACT_COMMIT_CERTAINTIES)[number];
21
+ /** The `artifact.host_error` outcome word the tool reports when the host THREW, by declaration. An aborted or
22
+ * malformed answer is `"unknown"` under every declaration: the throw guarantee says nothing about those arms. */
23
+ export declare const ARTIFACT_COMMIT_THROW_OUTCOME: {
24
+ readonly throw_means_uncommitted: "not_executed";
25
+ readonly uncertain: "unknown";
26
+ };
27
+ export type ThrowOutcomeTableCoversEveryCommitCertainty = AssertAllKeysHandled<Exclude<ArtifactCommitCertainty, keyof typeof ARTIFACT_COMMIT_THROW_OUTCOME>>;
9
28
  /** The `publishContext` word a publish carries (default `"plain"`). */
10
29
  export declare const ARTIFACT_PUBLISH_CONTEXTS: readonly ["pr_review", "workshop", "whiteboard", "prototype", "plain"];
11
30
  export type ArtifactPublishContext = (typeof ARTIFACT_PUBLISH_CONTEXTS)[number];
@@ -31,6 +50,16 @@ export declare const ARTIFACT_ACTION_HOST_METHOD: {
31
50
  readonly read_file: "readFile";
32
51
  };
33
52
  export type HostMethodTableCoversEveryAction = AssertAllKeysHandled<Exclude<ArtifactAction, keyof typeof ARTIFACT_ACTION_HOST_METHOD>>;
53
+ /** Whether a call of the action may run inside a parallel batch: the read family is (host reads, no host write); a
54
+ * publish is not (it writes the host and the tool's path-binding state). Read by the tool's `isConcurrencySafe`. */
55
+ export declare const ARTIFACT_ACTION_CONCURRENCY_SAFE: {
56
+ readonly publish: false;
57
+ readonly list: true;
58
+ readonly read: true;
59
+ readonly list_files: true;
60
+ readonly read_file: true;
61
+ };
62
+ export type ConcurrencyTableCoversEveryAction = AssertAllKeysHandled<Exclude<ArtifactAction, keyof typeof ARTIFACT_ACTION_CONCURRENCY_SAFE>>;
34
63
  /** The typed failure words a host outcome may carry (`ok: false`). */
35
64
  export declare const ARTIFACT_HOST_FAILURE_CODES: readonly ["conflict", "url_foreign", "forbidden", "payload_too_large", "not_found", "force_refused", "refused"];
36
65
  export type ArtifactHostFailureCode = (typeof ARTIFACT_HOST_FAILURE_CODES)[number];
@@ -56,11 +85,16 @@ export declare const ARTIFACT_CARD = "artifact";
56
85
  * containing `"publish"`; `plane: "local-stub"` ⇒ `actions` is exactly `["publish"]`;
57
86
  * `defaultVisibility` is `"private"`; both byte limits are positive finite integers; `multiFile` is a
58
87
  * boolean; every declared action's host method exists; `plane: "local-stub"` ⇒ `storageDir` is a non-empty
59
- * absolute path.
88
+ * fully qualified path under its own spelling family (`C:\\…` / UNC / `/…` — read from the spelling, never from
89
+ * `process.platform`); `commit` is a word of {@link ARTIFACT_COMMIT_CERTAINTIES} (no default — an undeclared
90
+ * commit protocol is an invalid declaration, never read as either word).
60
91
  */
61
92
  export interface ArtifactHostCapabilities {
62
93
  plane: ArtifactPlane;
63
94
  actions: ReadonlyArray<ArtifactAction>;
95
+ /** What a throw out of `publish` proves about the commit (see {@link ARTIFACT_COMMIT_CERTAINTIES}); the tool words
96
+ * its host-fault result from this and from nothing else about the host. */
97
+ commit: ArtifactCommitCertainty;
64
98
  /** The on-disk store of a `"local-stub"` plane (required there; ignored on `"hosted"`). Contract: the store
65
99
  * must lie OUTSIDE every root the run may write or read (the task root, `additionalDirectories`,
66
100
  * `additionalReadDirectories`) — a store the model can reach through the fs tools is not a trust boundary;
@@ -77,9 +111,12 @@ export interface ArtifactHostCapabilities {
77
111
  * Engine-minted call attribution and idempotency keys — closure-supplied, never a tool parameter.
78
112
  * `taskId` / `sessionId` / `agentName` are audit attribution, never an authorization subject: a host
79
113
  * instance is bound to one principal at construction. `runId` and `attemptId` are engine-minted trusted
80
- * keys: `runId` is the same value `onMount` received; `attemptId` is one opaque value per tool call,
81
- * stable across any host retry of that call, so a host MUST replay the first outcome for a repeated
82
- * `attemptId` (a fresh publish never mints a second page, an update never advances twice).
114
+ * keys: `runId` is the same value `onMount` received; `attemptId` is one opaque value per HOST CALL — the
115
+ * engine mints a fresh one for every call it makes, so two calls of a single tool invocation (the probing
116
+ * `read` that establishes ownership and the `list_files` it gates, say) never arrive under one key — and it
117
+ * is stable across any retry the HOST makes of that call, so a host MUST replay the first outcome for a
118
+ * repeated `attemptId` (a fresh publish never mints a second page, an update never advances twice). The rule
119
+ * is keyed on the value alone: a host may hold one attempt ledger for all its methods.
83
120
  */
84
121
  export interface ArtifactCallContext {
85
122
  taskId: string;
@@ -1,6 +1,11 @@
1
- import { isAbsolute } from "node:path";
1
+ import { isAbsoluteForFamily, pathFamilyOf } from "../tools/fs/safety.js";
2
2
  export const ARTIFACT_ACTIONS = ["publish", "list", "read", "list_files", "read_file"];
3
3
  export const ARTIFACT_PLANES = ["hosted", "local-stub"];
4
+ export const ARTIFACT_COMMIT_CERTAINTIES = ["throw_means_uncommitted", "uncertain"];
5
+ export const ARTIFACT_COMMIT_THROW_OUTCOME = {
6
+ throw_means_uncommitted: "not_executed",
7
+ uncertain: "unknown",
8
+ };
4
9
  export const ARTIFACT_PUBLISH_CONTEXTS = ["pr_review", "workshop", "whiteboard", "prototype", "plain"];
5
10
  export const ARTIFACT_LIST_SCOPES = ["mine", "shared", "all"];
6
11
  export const ARTIFACT_PLANE_CONTENT_ORIGIN = {
@@ -14,6 +19,13 @@ export const ARTIFACT_ACTION_HOST_METHOD = {
14
19
  list_files: "listFiles",
15
20
  read_file: "readFile",
16
21
  };
22
+ export const ARTIFACT_ACTION_CONCURRENCY_SAFE = {
23
+ publish: false,
24
+ list: true,
25
+ read: true,
26
+ list_files: true,
27
+ read_file: true,
28
+ };
17
29
  export const ARTIFACT_HOST_FAILURE_CODES = ["conflict", "url_foreign", "forbidden", "payload_too_large", "not_found", "force_refused", "refused"];
18
30
  export const ARTIFACT_FAILURE_RESULT_CODE = {
19
31
  conflict: "artifact.publish_conflict",
@@ -59,6 +71,9 @@ export function readArtifactHostCapabilities(host) {
59
71
  return { ok: false, problem: 'capabilities.actions must include "publish"' };
60
72
  if (plane === "local-stub" && (actions.length !== 1 || actions[0] !== "publish"))
61
73
  return { ok: false, problem: 'a "local-stub" plane must declare exactly ["publish"]' };
74
+ const commit = c["commit"];
75
+ if (!wordOf(ARTIFACT_COMMIT_CERTAINTIES, commit))
76
+ return { ok: false, problem: `capabilities.commit must be one of ${ARTIFACT_COMMIT_CERTAINTIES.join(" | ")}` };
62
77
  if (c["defaultVisibility"] !== "private")
63
78
  return { ok: false, problem: 'capabilities.defaultVisibility must be "private"' };
64
79
  if (!isPositiveInt(c["maxFileBytes"]))
@@ -68,7 +83,7 @@ export function readArtifactHostCapabilities(host) {
68
83
  if (typeof c["multiFile"] !== "boolean")
69
84
  return { ok: false, problem: "capabilities.multiFile must be a boolean" };
70
85
  const storageDir = c["storageDir"];
71
- if (plane === "local-stub" && (typeof storageDir !== "string" || storageDir.length === 0 || !isAbsolute(storageDir)))
86
+ if (plane === "local-stub" && (typeof storageDir !== "string" || storageDir.length === 0 || !isAbsoluteForFamily(pathFamilyOf({ root: storageDir }), storageDir)))
72
87
  return { ok: false, problem: 'a "local-stub" plane must declare its absolute storageDir' };
73
88
  if (storageDir !== undefined && typeof storageDir !== "string")
74
89
  return { ok: false, problem: "capabilities.storageDir must be a string when present" };
@@ -82,6 +97,7 @@ export function readArtifactHostCapabilities(host) {
82
97
  capabilities: Object.freeze({
83
98
  plane,
84
99
  actions: Object.freeze([...actions]),
100
+ commit,
85
101
  defaultVisibility: "private",
86
102
  maxFileBytes: c["maxFileBytes"],
87
103
  maxTotalBytes: c["maxTotalBytes"],
@@ -10,37 +10,117 @@ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"]
10
10
  export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number];
11
11
  /** Whether a value is a member of the unavailable-cause set — the screen a row reader applies to a stored word. */
12
12
  export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUnavailableCause;
13
- /** The classifier's verdict over one pending `ask`. */
14
- export type AutoModeVerdict = {
13
+ /**
14
+ * WHY a classification moved DOWN the candidate ladder — the closed set behind
15
+ * {@link AutoModeClassifierRound}'s `fallback.cause`, i.e. the ways a candidate produces NO RULING
16
+ * ({@link rungDidNotRule}). Two words are the unavailable causes verbatim (the leg threw / rejected; the
17
+ * round-trip cap elapsed); `parse_error` is the candidate that ANSWERED without a verdict. A ruling is not a
18
+ * member: the ladder stops at a verdict either way.
19
+ */
20
+ export declare const AUTO_MODE_FALLBACK_CAUSES: readonly ["error", "timeout", "parse_error"];
21
+ export type AutoModeFallbackCause = (typeof AUTO_MODE_FALLBACK_CAUSES)[number];
22
+ /** Whether a value is a member of the fallback-cause set — the screen a row reader applies to a stored word. */
23
+ export declare function isAutoModeFallbackCause(v: unknown): v is AutoModeFallbackCause;
24
+ /**
25
+ * The deployment's answer to "what happens when the classifier seat does not rule?"
26
+ * (`RunnerDeps.autoMode.classifierFallback`) — the closed set behind the candidate ladder's LENGTH:
27
+ * `main` = [classifier seat, the run's main model]; `none` = [classifier seat].
28
+ */
29
+ export declare const AUTO_MODE_CLASSIFIER_FALLBACKS: readonly ["main", "none"];
30
+ export type AutoModeClassifierFallback = (typeof AUTO_MODE_CLASSIFIER_FALLBACKS)[number];
31
+ /** Whether a value is a member of the classifier-fallback set. */
32
+ export declare function isAutoModeClassifierFallback(v: unknown): v is AutoModeClassifierFallback;
33
+ /** Read the knob: absent ⇒ the default `main`; a member ⇒ itself; ANYTHING ELSE ⇒ a loud refusal carrying
34
+ * `config.auto_mode_classifier_fallback_invalid`. A typo'd knob must not read as the default: `"non"` would
35
+ * keep paying for the rung it meant to turn off. */
36
+ export declare function autoModeClassifierFallbackOf(v: unknown): AutoModeClassifierFallback;
37
+ /** The classifier's verdict over one pending `ask`. Every arm may carry the classification's model
38
+ * attribution ({@link AutoModeClassifierRound}), present iff the decider was wired with a candidate ladder. */
39
+ export type AutoModeVerdict = ({
15
40
  kind: "allow";
16
- }
41
+ } & AutoModeRoundAttributed)
17
42
  /** `category` is the matched BLOCK rule name (207 output contract); `reason` starts with `[Rule Name]`. */
18
- | {
43
+ | ({
19
44
  kind: "block";
20
45
  category: string;
21
46
  reason: string;
22
- }
47
+ } & AutoModeRoundAttributed)
23
48
  /** The classifier could not run: the model leg threw / rejected, or ran past the cap. NOT a decision — the
24
49
  * gate denies and says so ({@link classifierUnavailableDenyMessage}). */
25
- | {
50
+ | ({
26
51
  kind: "unavailable";
27
52
  cause: AutoModeUnavailableCause;
28
- }
53
+ } & AutoModeRoundAttributed)
29
54
  /** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC
30
55
  * parse-failure sentence ({@link CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE}), counted by the denial limit. */
31
- | {
56
+ | ({
32
57
  kind: "parse_error";
33
58
  raw: string;
59
+ } & AutoModeRoundAttributed);
60
+ /** The member every verdict arm shares: WHICH MODEL this classification asked and which one answered. */
61
+ export interface AutoModeRoundAttributed {
62
+ /** Absent when the decider was given no candidate ladder (a hand-built decider, the cross-process rebuild's
63
+ * single fresh leg): it ran on whatever model the leg is bound to and can vouch for no name. Never faked. */
64
+ readonly classifier?: AutoModeClassifierRound;
65
+ }
66
+ /**
67
+ * WHICH MODEL answered one classification — the record a walk of the CANDIDATE LADDER
68
+ * ({@link AutoModeDeciderOptions.candidates}) hands its consumers: `modelRequested` = the ladder's FIRST rung
69
+ * (the seat the classification was for); `modelUsed` = the rung whose answer the gate acted on, and where NO
70
+ * candidate ruled, the last rung ATTEMPTED (stated, not implied by absence); `fallback` = present exactly
71
+ * when the ladder MOVED to reach `modelUsed` — which rung it left, which it arrived at, and why
72
+ * ({@link AutoModeFallbackCause}); absent = the first rung answered.
73
+ */
74
+ export interface AutoModeClassifierRound {
75
+ readonly modelRequested: string;
76
+ readonly modelUsed: string;
77
+ readonly fallback?: {
78
+ readonly from: string;
79
+ readonly to: string;
80
+ readonly cause: AutoModeFallbackCause;
81
+ };
82
+ }
83
+ /**
84
+ * The classify hook's word for "this classification was NEVER PUT TO A MODEL" — the failure is in assembling
85
+ * the QUESTION (the transcript window every rung shares), not in a rung's answer.
86
+ *
87
+ * WHY IT EXISTS. The ladder advances on "the rung produced no ruling", and a hook that rejects looks exactly
88
+ * like that. But a classification whose prompt could not be built fails IDENTICALLY on every rung (the build is one
89
+ * shared promise, by contract — "the same prompt, re-asked" is literal), so the walk recorded a fallback
90
+ * `{from: seat, to: main, cause: "error"}` and a deny sentence naming two models, for a classification in which NO
91
+ * MODEL WAS EVER DISPATCHED: a transcript-store outage read, on every face a person can see, as a broken
92
+ * classifier seat. The verdict is unchanged (fail-closed `unavailable`); what changes is that the record
93
+ * stops attributing it to models that were never asked.
94
+ *
95
+ * Carried as a `code` on the thrown error rather than a class so any classify hook — including one this
96
+ * package never constructed — can raise it without importing a constructor.
97
+ *
98
+ * WHAT IT DOES NOT COVER, stated: a build that OUTRUNS a rung's cap is still recorded as that rung's
99
+ * `timeout`. The cap bounds the whole classify call, assembly included, so at that point nothing distinguishes
100
+ * a slow transcript store from a slow model. Moving the per-classification build outside the rung cap is a
101
+ * seam change, filed for a later version.
102
+ */
103
+ export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
104
+ /** Mint the marker above around whatever failed, keeping the original as `cause` for the host's own logs.
105
+ * TOTAL by construction: the failure it wraps is an arbitrary value, and an `instanceof` or a `.message` read
106
+ * on a revoked proxy (or a throwing getter) THROWS — which would replace the marker with an ordinary error and
107
+ * hand the walk back the attribution this whole seat exists to remove. So the detail is a guarded read that
108
+ * must already be a string, and the total describer otherwise. */
109
+ export declare function autoModeRoundNotAsked(reason: unknown): Error & {
110
+ code: string;
34
111
  };
112
+ /** Why a rung did not settle the classification, or `undefined` when it RULED and the ladder stops — the ONE
113
+ * predicate behind the advance (there is no `if (fallback)` arm anywhere). */
114
+ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCause | undefined;
35
115
  /**
36
116
  * The model-facing deny text for an `unavailable` verdict — CC 2.1.250 `x1t`, with the parenthetical `EIt`
37
117
  * spells for the causes this engine distinguishes (a wall-clock / connection timeout reads " (timed out)";
38
- * an error of unknown kind reads nothing — CC spells HTTP statuses it has, and this decider does not). The
39
- * subject is the classifier itself rather than a model id: the decider does not know which model answered,
40
- * and the sentence must not name one it cannot vouch for. One writer for every deny site (the gate's own
41
- * station and the inherited-lane arms).
118
+ * an unknown kind reads nothing). The subject is the classifier itself rather than a model id: without a
119
+ * candidate ladder the decider cannot vouch for a name. One writer for every deny site. ROOT CAUSE, when
120
+ * `attribution.fallback` is present: the sentence names BOTH rungs that failed to answer; a ladder that did
121
+ * NOT move keeps the CC text byte for byte.
42
122
  */
43
- export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause): string;
123
+ export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
44
124
  /** The model-facing deny text for a `parse_error` verdict — CC 2.1.250 `Ure` over the R3t stem: the classifier
45
125
  * answered, but not with a verdict, and auto mode blocks what it could not evaluate. */
46
126
  export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety \u2014 the classifier's reply carried no verdict.";
@@ -63,13 +143,11 @@ export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could no
63
143
  * no whitespace inside (the word-boundary form — `<block>yesterday` is not a hit) and case does not matter.
64
144
  *
65
145
  * A stated BLOCK stands with or without its tail: `<category>` (the BLOCK rule NAME the prompt asked
66
- * for, read case-insensitively as free text — the deployment authors those names, so there is no
67
- * fixed vocabulary to screen them against) and `<reason>` (first `<reason>…</reason>` pair on the
68
- * stripped text, the tag spelled as the prompt spells it, trimmed) are carried when present and `""`
69
- * when absent; the consumers render their
70
- * own fallback line from the tool name in that case. The block intent is unambiguous either way, and
71
- * downgrading a stated block to `parse_error` (a block with the generic parse-failure sentence instead
72
- * of the classifier's own reason) would lose the classifier's explicit verdict on the exact calls it flagged.
146
+ * for, read case-insensitively as free text — the deployment authors those names, so there is no fixed
147
+ * vocabulary to screen them against) and `<reason>` (first `<reason>…</reason>` pair on the stripped
148
+ * text, trimmed) are carried when present and `""` when absent; the consumers render their own fallback
149
+ * line from the tool name in that case. Downgrading a stated block to `parse_error` would lose the
150
+ * classifier's explicit verdict on the exact calls it flagged.
73
151
  *
74
152
  * `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.250's own outcome for a reply with no
75
153
  * verdict) that the denial limit counts like any other — never a silent allow, and never a question handed
@@ -84,19 +162,42 @@ export interface AutoModeClassifyInput {
84
162
  /** The human-readable text of the ask being decided (policy/safety message), when present. */
85
163
  askMessage?: string;
86
164
  }
87
- /** The pluggable model leg: given the classify input, return the model's raw text response.
88
- * Deployment-injected (roster cheap tier per); MAY throw / reject — the decider fail-closes. */
89
- export type AutoModeClassifyFn = (input: AutoModeClassifyInput, signal?: AbortSignal) => Promise<string>;
165
+ /** One rung of the ladder, as the leg is told about it. `index` is the ADDRESS (a ladder may carry two rungs
166
+ * whose model id is the same string), while `model` is the NAME the record will carry. */
167
+ export interface AutoModeLadderRung {
168
+ readonly index: number;
169
+ readonly model: string;
170
+ }
171
+ /** The pluggable model leg: given the classify input and the RUNG the ladder is on, return that model's raw
172
+ * text response. Deployment-injected; MAY throw / reject — the decider fail-closes. A rejection carrying
173
+ * {@link AUTO_MODE_ROUND_NOT_ASKED_CODE} says the failure was in assembling the QUESTION rather than in this
174
+ * rung's answer, and stops the walk instead of blaming the next rung. `rung` is `undefined`
175
+ * for a decider wired with no ladder. Both trailing parameters are OPTIONAL, not `| undefined`: a caller
176
+ * INVOKING it with one or two arguments must keep compiling. */
177
+ export type AutoModeClassifyFn = (input: AutoModeClassifyInput, signal?: AbortSignal, rung?: AutoModeLadderRung) => Promise<string>;
90
178
  export interface AutoModeDeciderOptions {
91
179
  classify: AutoModeClassifyFn;
92
- /** Hard cap on one classification round-trip. Default 15_000 ms (sema 裁量 — CC's constant is not
93
- * established; a permission gate must not stall the whole run on a slow classifier). */
180
+ /**
181
+ * The CANDIDATE LADDER this decider walks, in order — the models a classification may be asked on, NOT a
182
+ * second way to say what `classify` says (that hook is the DISPATCH; this is the sequence it dispatches
183
+ * to). Rung 0 is the seat the classification is for; every later rung is consulted only when the one
184
+ * before it produced NO RULING ({@link rungDidNotRule}). The FIRST ruling ends the walk and is the
185
+ * verdict; when no rung rules, the LAST rung's own non-ruling verdict stands.
186
+ *
187
+ * Omitted = a one-rung anonymous ladder: the leg as wired, no attribution on the verdict. An EMPTY array,
188
+ * or a rung that is not a non-empty string, is REFUSED loudly at construction (a deployment that computed
189
+ * its ladder and got nothing has a bug, not a default). COST: {@link timeoutMs} bounds ONE rung, so the
190
+ * worst case is the cap times the rungs actually walked.
191
+ */
192
+ candidates?: readonly string[];
193
+ /** Hard cap on one classification round-trip — one LADDER RUNG, not the whole classification (see
194
+ * {@link candidates}). Default 15_000 ms: a permission gate must not stall the whole run on a slow classifier. */
94
195
  timeoutMs?: number;
95
196
  /**
96
- * Fired once per `decide` call with the verdict the gate is about to act on and the wall time it
97
- * waited (`ms`, integer ≥ 0; a timed-out round reads the deadline). `cause` is present iff the verdict
98
- * is `unavailable`. The engine's own wiring turns this into the `auto_mode.classified` trace frame; a
99
- * hand-built decider need not implement it. A throwing hook never breaks the gate.
197
+ * Fired once per `decide` call with the verdict the gate is about to act on and the wall time it waited
198
+ * (`ms`, integer ≥ 0; a timed-out classification reads the deadline, one that walked two rungs the whole
199
+ * walk). `cause` is present iff the verdict is `unavailable`; `classifier` iff a ladder was wired. A
200
+ * throwing hook never breaks the gate.
100
201
  */
101
202
  onClassified?: (info: AutoModeClassified) => void;
102
203
  }
@@ -110,16 +211,17 @@ export interface AutoModeClassified {
110
211
  cause?: Extract<AutoModeVerdict, {
111
212
  kind: "unavailable";
112
213
  }>["cause"];
214
+ /** The classification's model attribution, present exactly when the decider was wired with a candidate ladder
215
+ * ({@link AutoModeClassifierRound}) — the same object the verdict carries. */
216
+ classifier?: AutoModeClassifierRound;
113
217
  }
114
218
  export interface AutoModeDecider {
115
219
  /** Never rejects. Any internal failure surfaces as `unavailable`/`parse_error` (fail-closed). */
116
220
  decide(input: AutoModeClassifyInput, signal?: AbortSignal): Promise<AutoModeVerdict>;
117
221
  }
118
- /**
119
- * The per-run decider: timeout + fail-closed error mapping, and nothing carried between rounds. One instance
120
- * per run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the
121
- * next one, which asks the model again.
122
- */
222
+ /** The per-run decider: timeout + fail-closed error mapping, nothing carried between rounds. One instance per
223
+ * run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the next,
224
+ * which asks the model again. */
123
225
  export declare function createAutoModeDecider(opts: AutoModeDeciderOptions): AutoModeDecider;
124
226
  /** The deployment's bounds for the denial limit (`RunnerDeps.autoMode.denialLimit`). Every member
125
227
  * optional; an omitted member takes its CC default. A present member with a bad value is REFUSED
@@ -152,24 +254,18 @@ export interface DenialLimitCounts {
152
254
  * The DECISION-layer member of a denial-limit fallback ask (`PermissionResult` ask arm): the counts
153
255
  * that tripped the bound, and NOTHING about a window. A window is a fact of the ask's ROUTE — a live
154
256
  * function approver can count down, a durable park row and a headless deny cannot — and the route is
155
- * chosen downstream of the count, so the decision that carries these counts is by construction not in
156
- * a position to state one. The type says so: there is no window member to write here, and the only
257
+ * chosen downstream of the count. The type says so: there is no window member to write here, and the only
157
258
  * ways to reach a {@link DenialLimitFallback} (the member an {@link
158
259
  * import("./tool-policy.js").AskRequest} carries) are the two route stations,
159
- * {@link AutoModeDenialTracker.armTimedWindow} and {@link unarmedWindow}. A host that builds its own
160
- * gate adapter therefore cannot forward a decision's member straight into an approval request: the
161
- * compiler stops it and names the station it skipped.
260
+ * {@link AutoModeDenialTracker.armTimedWindow} and {@link unarmedWindow}.
162
261
  */
163
262
  export interface UnarmedDenialLimitFallback extends DenialLimitCounts {
164
263
  /** FORBIDDEN on a decision, and stated rather than merely omitted. Leaving the member out would only
165
264
  * stop a FRESH object literal (TypeScript's excess-property check); a policy that writes its window
166
265
  * through a `const`, an `as const`, or an annotated return type would still compile — and would then
167
- * have its number silently replaced at whichever route station the ask reached, turning a bounded
168
- * wait into an unbounded one with no diagnostic. `never` makes the old shape a compile error at the
169
- * policy boundary instead, so the retirement of the self-declared window is a migration a host is
170
- * TOLD about rather than one it discovers when nobody is auto-denied. (A host that casts past the
171
- * type still meets the route stations' runtime belt: they write the route's number, never the
172
- * decision's.) */
266
+ * have its number silently replaced at whichever route station the ask reached. `never` makes the old
267
+ * shape a compile error at the policy boundary instead. (A host that casts past the type still meets
268
+ * the route stations' runtime belt: they write the route's number, never the decision's.) */
173
269
  readonly autoDenyAfterMs?: never;
174
270
  }
175
271
  /** The REQUEST-layer member (`AskRequest`, a durable park row, a checkpoint summary's echo): the same
@@ -1,12 +1,65 @@
1
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
+ import { describeRejectedValue } from "./tool-search-setting.js";
2
3
  export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"];
3
4
  const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
4
5
  export function isAutoModeUnavailableCause(v) {
5
6
  return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
6
7
  }
7
- export function classifierUnavailableDenyMessage(toolName, cause) {
8
+ export const AUTO_MODE_FALLBACK_CAUSES = ["error", "timeout", "parse_error"];
9
+ const AUTO_MODE_FALLBACK_CAUSE_SET = new Set(AUTO_MODE_FALLBACK_CAUSES);
10
+ export function isAutoModeFallbackCause(v) {
11
+ return AUTO_MODE_FALLBACK_CAUSE_SET.has(v);
12
+ }
13
+ export const AUTO_MODE_CLASSIFIER_FALLBACKS = ["main", "none"];
14
+ const AUTO_MODE_CLASSIFIER_FALLBACK_SET = new Set(AUTO_MODE_CLASSIFIER_FALLBACKS);
15
+ export function isAutoModeClassifierFallback(v) {
16
+ return AUTO_MODE_CLASSIFIER_FALLBACK_SET.has(v);
17
+ }
18
+ export function autoModeClassifierFallbackOf(v) {
19
+ if (v === undefined)
20
+ return "main";
21
+ if (isAutoModeClassifierFallback(v))
22
+ return v;
23
+ const e = new Error(`RunnerDeps.autoMode.classifierFallback must be one of ${AUTO_MODE_CLASSIFIER_FALLBACKS.map((x) => `"${x}"`).join(" | ")} ` +
24
+ `(got ${describeRejectedValue(v)}) — omit it for the default "main"`);
25
+ e.code = "config.auto_mode_classifier_fallback_invalid";
26
+ throw e;
27
+ }
28
+ export const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
29
+ export function autoModeRoundNotAsked(reason) {
30
+ let detail;
31
+ try {
32
+ const m = reason.message;
33
+ if (typeof m === "string")
34
+ detail = m.slice(0, 200);
35
+ }
36
+ catch {
37
+ }
38
+ const e = new Error(`the auto-mode classification could not be assembled: ${detail ?? describeRejectedValue(reason)}`, { cause: reason });
39
+ e.code = AUTO_MODE_ROUND_NOT_ASKED_CODE;
40
+ return e;
41
+ }
42
+ function isAutoModeRoundNotAsked(e) {
43
+ try {
44
+ return typeof e === "object" && e !== null && e.code === AUTO_MODE_ROUND_NOT_ASKED_CODE;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ export function rungDidNotRule(v) {
51
+ if (v.kind === "unavailable")
52
+ return v.cause;
53
+ if (v.kind === "parse_error")
54
+ return "parse_error";
55
+ return undefined;
56
+ }
57
+ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
8
58
  const parenthetical = cause === "timeout" ? " (timed out)" : "";
59
+ const fb = attribution?.fallback;
60
+ const rootCause = fb === undefined ? "" : `The classifier model "${fb.from}" did not answer (${fb.cause}) and neither did the model it fell back to, "${fb.to}" (${cause}). `;
9
61
  return (`The auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode cannot determine the safety of ${toolName} right now. ` +
62
+ rootCause +
10
63
  "Wait a moment and then try this action again. " +
11
64
  "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
12
65
  "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
@@ -31,6 +84,15 @@ export function parseAutoModeResponse(text) {
31
84
  }
32
85
  export function createAutoModeDecider(opts) {
33
86
  const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
87
+ if (opts.candidates !== undefined) {
88
+ if (opts.candidates.length === 0) {
89
+ throw new Error("AutoModeDeciderOptions.candidates must name at least one model — omit the member for the leg's own model, never an empty ladder");
90
+ }
91
+ const bad = opts.candidates.findIndex((m) => typeof m !== "string" || m.trim() === "");
92
+ if (bad >= 0)
93
+ throw new Error(`AutoModeDeciderOptions.candidates[${bad}] must be a non-empty model id (got ${JSON.stringify(opts.candidates[bad])})`);
94
+ }
95
+ const ladder = opts.candidates ?? [undefined];
34
96
  const decide = async (input, signal) => {
35
97
  const startedAt = performance.now();
36
98
  const verdict = await decideUnreported(input, signal);
@@ -41,6 +103,7 @@ export function createAutoModeDecider(opts) {
41
103
  ms: Math.max(0, Math.round(performance.now() - startedAt)),
42
104
  verdict: verdict.kind,
43
105
  ...(verdict.kind === "unavailable" ? { cause: verdict.cause } : {}),
106
+ ...(verdict.classifier !== undefined ? { classifier: verdict.classifier } : {}),
44
107
  });
45
108
  }
46
109
  catch {
@@ -50,6 +113,37 @@ export function createAutoModeDecider(opts) {
50
113
  };
51
114
  return { decide };
52
115
  async function decideUnreported(input, signal) {
116
+ const causes = [];
117
+ const round = { notAsked: false };
118
+ let verdict = { kind: "unavailable", cause: "error" };
119
+ let i = 0;
120
+ for (; i < ladder.length; i++) {
121
+ const name = ladder[i];
122
+ verdict = await askRung(input, signal, name !== undefined ? { index: i, model: name } : undefined, round);
123
+ const cause = rungDidNotRule(verdict);
124
+ if (cause === undefined)
125
+ break;
126
+ causes.push(cause);
127
+ if (round.notAsked || signal?.aborted)
128
+ break;
129
+ }
130
+ const landed = round.notAsked ? 0 : Math.min(i, ladder.length - 1);
131
+ const requested = ladder[0];
132
+ const used = ladder[landed];
133
+ if (requested === undefined || used === undefined)
134
+ return verdict;
135
+ const from = landed > 0 ? ladder[landed - 1] : undefined;
136
+ const movedBy = landed > 0 ? causes[landed - 1] : undefined;
137
+ return {
138
+ ...verdict,
139
+ classifier: Object.freeze({
140
+ modelRequested: requested,
141
+ modelUsed: used,
142
+ ...(from !== undefined && movedBy !== undefined ? { fallback: Object.freeze({ from, to: used, cause: movedBy }) } : {}),
143
+ }),
144
+ };
145
+ }
146
+ async function askRung(input, signal, rung, round) {
53
147
  let timer;
54
148
  const inner = new AbortController();
55
149
  const onOuterAbort = () => inner.abort();
@@ -69,7 +163,7 @@ export function createAutoModeDecider(opts) {
69
163
  reject(new AutoModeTimeout());
70
164
  }
71
165
  }, timeoutMs);
72
- opts.classify(input, inner.signal).then((v) => {
166
+ opts.classify(input, inner.signal, rung).then((v) => {
73
167
  if (!settled) {
74
168
  settled = true;
75
169
  resolve(v);
@@ -84,9 +178,13 @@ export function createAutoModeDecider(opts) {
84
178
  return parseAutoModeResponse(raw);
85
179
  }
86
180
  catch (e) {
181
+ if (isAutoModeRoundNotAsked(e)) {
182
+ round.notAsked = true;
183
+ return { kind: "unavailable", cause: "error" };
184
+ }
87
185
  if (signal?.aborted)
88
186
  return { kind: "unavailable", cause: "error" };
89
- return { kind: "unavailable", cause: e instanceof AutoModeTimeout ? "timeout" : "error" };
187
+ return { kind: "unavailable", cause: isAutoModeTimeout(e) ? "timeout" : "error" };
90
188
  }
91
189
  finally {
92
190
  if (timer !== undefined)
@@ -101,6 +199,14 @@ class AutoModeTimeout extends Error {
101
199
  super("auto-mode classify timeout");
102
200
  }
103
201
  }
202
+ function isAutoModeTimeout(e) {
203
+ try {
204
+ return e instanceof AutoModeTimeout;
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
104
210
  export function readDenialLimitFallback(v) {
105
211
  if (typeof v !== "object" || v === null)
106
212
  return undefined;
@@ -969,6 +969,16 @@ export interface CheckpointState {
969
969
  * scope for any in-row invariant). Absent = the row was minted ungoverned. */
970
970
  realApproval?: true;
971
971
  };
972
+ /**
973
+ * The WRITE-face containment state at suspend: the RESOLVED write face, written ONLY when it is
974
+ * non-default ("open"). An absent section IS "roots" for every reader — which is why it carries NO
975
+ * version stamp: losing it can only make a resume STRICTER. Resume folds STRICTER-WINS, the read seat's
976
+ * law: roots unless the section says open AND the live resolution says open. No deny half and no
977
+ * governance marker.
978
+ */
979
+ writeFace?: {
980
+ face: "open" | "roots";
981
+ };
972
982
  /**
973
983
  * design/319 (A ticket) — the session's reminder provenance mark
974
984
  * ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
@@ -1961,10 +1971,14 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
1961
1971
  * parks a steer in the retired pre-queue single seat — terminal for the row's bytes; no worker version
1962
1972
  * reads that seat, the host decides the parked instruction itself).
1963
1973
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
1964
- * `"read_face_damaged"`, `"read_face_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1974
+ * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
1965
1975
  * current bytes — no worker version redeems a damaged row).
1966
1976
  */
1967
- export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
1977
+ export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
1978
+ /** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
1979
+ * legal-and-absent at every version, so there is no version to judge a fabrication against — and a
1980
+ * fabricated "open" still needs the LIVE resolution to say open too. */
1981
+ | "write_face_damaged" | "constraint_chain_missing";
1968
1982
  /** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
1969
1983
  export declare class CheckpointError extends Error {
1970
1984
  readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"