@sema-agent/core 5.64.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +113 -4
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +48 -8
  35. package/dist/core/hooks.js +39 -22
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +6 -0
  45. package/dist/core/memory-engine/engine.d.ts +329 -15
  46. package/dist/core/memory-engine/engine.js +364 -34
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +80 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/remote-env.d.ts +3 -3
  71. package/dist/core/retention-policy.d.ts +9 -0
  72. package/dist/core/retention-policy.js +5 -2
  73. package/dist/core/retention.d.ts +13 -2
  74. package/dist/core/runner/assemble-result.d.ts +19 -1
  75. package/dist/core/runner/assemble-result.js +17 -2
  76. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  77. package/dist/core/runner/compaction-call-options.js +3 -0
  78. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  79. package/dist/core/runner/memory-capture-optout.js +53 -0
  80. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  81. package/dist/core/runner/prepare-config-doors.js +16 -0
  82. package/dist/core/runner/prepare-hands-readface.d.ts +119 -5
  83. package/dist/core/runner/prepare-hands-readface.js +103 -8
  84. package/dist/core/runner/prepare-memory.d.ts +88 -0
  85. package/dist/core/runner/prepare-memory.js +306 -25
  86. package/dist/core/runner/prepare-task.d.ts +156 -5
  87. package/dist/core/runner/prepare-task.js +488 -98
  88. package/dist/core/runner/runtask.d.ts +27 -20
  89. package/dist/core/runner/runtask.js +283 -99
  90. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  91. package/dist/core/runner/session-file-state-replay.js +52 -1
  92. package/dist/core/runner/tool-disclosure.js +2 -1
  93. package/dist/core/runner/turn-attachments.d.ts +22 -12
  94. package/dist/core/session-store.d.ts +1 -1
  95. package/dist/core/session-store.js +6 -1
  96. package/dist/core/session.d.ts +34 -1
  97. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  98. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  99. package/dist/core/stub-env.d.ts +4 -0
  100. package/dist/core/stub-env.js +1 -0
  101. package/dist/core/task-registry-shared.js +30 -2
  102. package/dist/core/tool-errors.js +1 -0
  103. package/dist/core/tool-policy.d.ts +172 -1
  104. package/dist/core/tool-policy.js +32 -1
  105. package/dist/core/tool-result-store.js +2 -1
  106. package/dist/core/trace.d.ts +24 -0
  107. package/dist/core/types.d.ts +875 -97
  108. package/dist/core/types.js +4 -3
  109. package/dist/core/untrusted-text.d.ts +1 -1
  110. package/dist/core/untrusted-text.js +8 -0
  111. package/dist/core/workflow-run-store-contract.js +17 -0
  112. package/dist/core/workflow-run-store.d.ts +20 -0
  113. package/dist/core/workflow-run-store.js +1 -0
  114. package/dist/engine/compaction/compaction.d.ts +88 -10
  115. package/dist/engine/compaction/compaction.js +109 -30
  116. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  117. package/dist/engine/execution-env/node-execution-env.js +28 -0
  118. package/dist/engine/harness/agent-harness.d.ts +52 -1
  119. package/dist/engine/harness/agent-harness.js +36 -1
  120. package/dist/engine/harness/types.d.ts +44 -1
  121. package/dist/engine/llm/types.d.ts +50 -4
  122. package/dist/engine/loop/agent-loop.d.ts +5 -1
  123. package/dist/engine/loop/agent-loop.js +25 -0
  124. package/dist/engine/loop/types.d.ts +19 -0
  125. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  126. package/dist/engine/session/session.js +1 -1
  127. package/dist/index.d.ts +18 -8
  128. package/dist/index.js +14 -6
  129. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  130. package/dist/orchestration/run-workflow-tool.js +22 -3
  131. package/dist/orchestration/workflow-governance.d.ts +59 -1
  132. package/dist/orchestration/workflow-governance.js +61 -8
  133. package/dist/orchestration/workflow-meta.d.ts +4 -2
  134. package/dist/orchestration/workflow-primitives.js +56 -13
  135. package/dist/orchestration/workflow-types.d.ts +112 -1
  136. package/dist/orchestration/workflow-types.js +2 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +182 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +15 -7
  141. package/dist/prompts/default.js +3 -0
  142. package/dist/stores/file/file-history-store.d.ts +368 -0
  143. package/dist/stores/file/file-history-store.js +1248 -0
  144. package/dist/stores/file/index.d.ts +22 -13
  145. package/dist/stores/file/index.js +4 -4
  146. package/dist/stores/file/permission-rule-store.js +1 -0
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +159 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -0,0 +1,181 @@
1
+ import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, } from "./auto-mode-defaults.js";
2
+ export const AUTO_MODE_ARMING_RECIPE_VERSION = 1;
3
+ const isPlainRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
+ function canonicalStrings(v) {
5
+ if (v === undefined)
6
+ return undefined;
7
+ if (!Array.isArray(v))
8
+ return null;
9
+ for (let i = 0; i < v.length; i++) {
10
+ if (!(i in v) || typeof v[i] !== "string")
11
+ return null;
12
+ }
13
+ return v.length === 0 ? undefined : v.slice();
14
+ }
15
+ function canonicalRuleSection(v) {
16
+ const list = canonicalStrings(v);
17
+ if (list === null || list === undefined)
18
+ return list;
19
+ let seen = false;
20
+ const deduped = list.filter((r) => (r === AUTO_MODE_DEFAULTS_SENTINEL ? (seen ? false : ((seen = true), true)) : true));
21
+ if (deduped.length === 0)
22
+ return undefined;
23
+ return deduped.length === 1 && deduped[0] === AUTO_MODE_DEFAULTS_SENTINEL ? undefined : deduped;
24
+ }
25
+ function canonicalNumber(v, min, floor) {
26
+ if (v === undefined)
27
+ return undefined;
28
+ if (typeof v !== "number" || !Number.isFinite(v))
29
+ return null;
30
+ const n = floor ? Math.floor(v) : v;
31
+ return n < min ? null : n;
32
+ }
33
+ export function sanitizeAutoModeArmingRecipe(value) {
34
+ if (!isPlainRecord(value))
35
+ return undefined;
36
+ if (value.v !== AUTO_MODE_ARMING_RECIPE_VERSION)
37
+ return undefined;
38
+ const settingsDenyRules = canonicalStrings(value.settingsDenyRules);
39
+ const sessionContext = canonicalStrings(value.sessionContext);
40
+ const timeoutMs = canonicalNumber(value.timeoutMs, 1, false);
41
+ const failureThreshold = canonicalNumber(value.failureThreshold, 1, true);
42
+ if (settingsDenyRules === null || sessionContext === null || timeoutMs === null || failureThreshold === null)
43
+ return undefined;
44
+ let rules;
45
+ if (value.rules !== undefined) {
46
+ if (!isPlainRecord(value.rules))
47
+ return undefined;
48
+ const allow = canonicalRuleSection(value.rules.allow);
49
+ const softDeny = canonicalRuleSection(value.rules.softDeny);
50
+ const hardDeny = canonicalRuleSection(value.rules.hardDeny);
51
+ const environment = canonicalRuleSection(value.rules.environment);
52
+ if (allow === null || softDeny === null || hardDeny === null || environment === null)
53
+ return undefined;
54
+ const merged = {
55
+ ...(allow !== undefined ? { allow } : {}),
56
+ ...(softDeny !== undefined ? { softDeny } : {}),
57
+ ...(hardDeny !== undefined ? { hardDeny } : {}),
58
+ ...(environment !== undefined ? { environment } : {}),
59
+ };
60
+ if (Object.keys(merged).length > 0)
61
+ rules = merged;
62
+ }
63
+ let window;
64
+ if (value.window !== undefined) {
65
+ if (!isPlainRecord(value.window))
66
+ return undefined;
67
+ const maxEntries = canonicalNumber(value.window.maxEntries, 0, true);
68
+ const maxCharsPerEntry = canonicalNumber(value.window.maxCharsPerEntry, 0, true);
69
+ if (maxEntries === null || maxCharsPerEntry === null)
70
+ return undefined;
71
+ const merged = {
72
+ ...(maxEntries !== undefined && maxEntries !== AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES ? { maxEntries } : {}),
73
+ ...(maxCharsPerEntry !== undefined && maxCharsPerEntry !== AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS ? { maxCharsPerEntry } : {}),
74
+ };
75
+ if (Object.keys(merged).length > 0)
76
+ window = merged;
77
+ }
78
+ const settingsEpoch = value.settingsEpoch;
79
+ if (settingsEpoch !== undefined && typeof settingsEpoch !== "string")
80
+ return undefined;
81
+ const promptDigest = value.promptDigest;
82
+ if (promptDigest !== undefined && (typeof promptDigest !== "string" || promptDigest === ""))
83
+ return undefined;
84
+ return {
85
+ v: AUTO_MODE_ARMING_RECIPE_VERSION,
86
+ ...(promptDigest !== undefined ? { promptDigest } : {}),
87
+ ...(rules !== undefined ? { rules } : {}),
88
+ ...(settingsDenyRules !== undefined ? { settingsDenyRules } : {}),
89
+ ...(sessionContext !== undefined ? { sessionContext } : {}),
90
+ ...(window !== undefined ? { window } : {}),
91
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
92
+ ...(failureThreshold !== undefined ? { failureThreshold } : {}),
93
+ ...(settingsEpoch !== undefined && settingsEpoch !== "" ? { settingsEpoch } : {}),
94
+ };
95
+ }
96
+ export function autoModeArmingRecipeOf(face, bind) {
97
+ return sanitizeAutoModeArmingRecipe({
98
+ v: AUTO_MODE_ARMING_RECIPE_VERSION,
99
+ ...(bind?.promptDigest !== undefined ? { promptDigest: bind.promptDigest } : {}),
100
+ ...(face.rules !== undefined ? { rules: face.rules } : {}),
101
+ ...(face.settingsDenyRules !== undefined ? { settingsDenyRules: face.settingsDenyRules } : {}),
102
+ ...(face.sessionContext !== undefined ? { sessionContext: face.sessionContext } : {}),
103
+ ...(face.window !== undefined ? { window: face.window } : {}),
104
+ ...(face.timeoutMs !== undefined ? { timeoutMs: face.timeoutMs } : {}),
105
+ ...(face.failureThreshold !== undefined ? { failureThreshold: face.failureThreshold } : {}),
106
+ ...(face.settingsEpoch !== undefined ? { settingsEpoch: face.settingsEpoch } : {}),
107
+ });
108
+ }
109
+ function sameArmingBody(a, b) {
110
+ const body = (r) => JSON.stringify([
111
+ r.rules?.allow ?? null,
112
+ r.rules?.softDeny ?? null,
113
+ r.rules?.hardDeny ?? null,
114
+ r.rules?.environment ?? null,
115
+ r.settingsDenyRules ?? null,
116
+ r.sessionContext ?? null,
117
+ r.window?.maxEntries ?? null,
118
+ r.window?.maxCharsPerEntry ?? null,
119
+ ]);
120
+ return body(a) === body(b);
121
+ }
122
+ export function foldAutoModeArming(recorded, current) {
123
+ const rec = sanitizeAutoModeArmingRecipe(recorded);
124
+ if (rec === undefined) {
125
+ return {
126
+ ok: false,
127
+ reason: "malformed_recipe",
128
+ message: "the recorded auto-mode arming recipe is not readable by this build (unknown recipe version, or not plain recipe data) — " +
129
+ "no classifier is rebuilt; the redemption keeps its honest refusal and the inherited ask flows the original chain",
130
+ };
131
+ }
132
+ if (current === undefined) {
133
+ return {
134
+ ok: false,
135
+ reason: "current_settings_missing",
136
+ message: "no current auto-mode settings were declared for this rebuild, so nothing bounds it — the recorded arming is refused " +
137
+ "rather than rebuilt unbounded (pass the redeeming deployment's own face through autoModeArmingRecipeOf; a deployment " +
138
+ "with no overrides of its own still declares that, and the declaration is what the bound is made of)",
139
+ };
140
+ }
141
+ const cur = sanitizeAutoModeArmingRecipe(current);
142
+ if (cur === undefined) {
143
+ return {
144
+ ok: false,
145
+ reason: "malformed_current_settings",
146
+ message: "the redeeming deployment's own auto-mode settings did not canonicalize into a recipe, so the recorded arming cannot be " +
147
+ "bounded by the settings in force — no classifier is rebuilt (fail-closed: an unbounded rebuild is the one outcome this fold exists to prevent)",
148
+ };
149
+ }
150
+ if (!sameArmingBody(rec, cur)) {
151
+ const sameEpoch = rec.settingsEpoch !== undefined && rec.settingsEpoch === cur.settingsEpoch;
152
+ return sameEpoch
153
+ ? {
154
+ ok: false,
155
+ reason: "epoch_inconsistent",
156
+ message: `the recorded arming and the current settings both declare settings epoch ${JSON.stringify(rec.settingsEpoch)} while their ` +
157
+ "prompt bodies differ — one of the two is mislabelled; refusing to rebuild rather than choosing which set of rules is the policy in force",
158
+ }
159
+ : {
160
+ ok: false,
161
+ reason: "settings_moved",
162
+ message: "the auto-mode settings moved between the recorded arming and this deployment's current ones (rule sections / settings-deny rules / " +
163
+ "session context / window bounds differ), and free-text rule sets have no sound stricter-than ordering — refusing to rebuild " +
164
+ "(the strictest decidable answer: the inherited ask flows the original chain to a human)",
165
+ };
166
+ }
167
+ const minKnob = (a, b, fallback) => Math.min(a ?? fallback, b ?? fallback);
168
+ const timeoutMs = minKnob(rec.timeoutMs, cur.timeoutMs, AUTO_MODE_DEFAULT_TIMEOUT_MS);
169
+ const failureThreshold = minKnob(rec.failureThreshold, cur.failureThreshold, AUTO_MODE_DEFAULT_FAILURE_THRESHOLD);
170
+ return {
171
+ ok: true,
172
+ recorded: rec,
173
+ effective: {
174
+ ...rec,
175
+ timeoutMs,
176
+ failureThreshold,
177
+ ...(cur.settingsEpoch !== undefined ? { settingsEpoch: cur.settingsEpoch } : {}),
178
+ },
179
+ tightened: timeoutMs < (rec.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS) || failureThreshold < (rec.failureThreshold ?? AUTO_MODE_DEFAULT_FAILURE_THRESHOLD),
180
+ };
181
+ }
@@ -0,0 +1,13 @@
1
+ /** Hard cap on one classification round-trip when `AutoModeDeciderOptions.timeoutMs` is omitted. */
2
+ export declare const AUTO_MODE_DEFAULT_TIMEOUT_MS = 15000;
3
+ /** Consecutive-failure streak that opens the session breaker when `failureThreshold` is omitted. */
4
+ export declare const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
5
+ /** Newest transcript entries in the classify window when `AutoModeWindowOptions.maxEntries` is omitted. */
6
+ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
7
+ /** Per-entry excerpt cap when `AutoModeWindowOptions.maxCharsPerEntry` is omitted. */
8
+ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
9
+ /** The sentinel a deployment puts INSIDE a paired rule list to splice the CC default rules back in at
10
+ * that position (CC `XYt = "$defaults"`). It lives here, beside the other defaults, because the #503
11
+ * recipe canonicalizer needs its VALUE and must not load the assembly face (and its SHA-locked assets)
12
+ * to get it; `auto-mode-prompt.js` re-exports it, so the public name and its module face are unchanged. */
13
+ export declare const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
@@ -0,0 +1,5 @@
1
+ export const AUTO_MODE_DEFAULT_TIMEOUT_MS = 15_000;
2
+ export const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
3
+ export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
4
+ export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
5
+ export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
@@ -1,8 +1,10 @@
1
1
  import type { AutoModeClassifyInput } from "./auto-mode.js";
2
2
  import type { Message } from "../internal/llm.js";
3
3
  /** The sentinel a deployment puts INSIDE a rule list to splice the CC default rules back in at that
4
- * position (CC `XYt = "$defaults"`). Without it, a non-empty user list REPLACES the default section. */
5
- export declare const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
4
+ * position (CC `XYt = "$defaults"`). Without it, a non-empty user list REPLACES the default section.
5
+ * DECLARED in `./auto-mode-defaults.js` (the leaf the #503 recipe canonicalizer can reach without
6
+ * loading these assets) and re-exported here, where every caller already looks for it. */
7
+ export { AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
6
8
  /** Deployment-authored rule overrides for the four paired sections of the permissions document.
7
9
  * Each entry is ONE rule (rendered as a `- ` bullet); omit a field (or pass an empty array) to keep
8
10
  * the CC default rules for that section verbatim. */
@@ -29,7 +31,8 @@ export interface BuildAutoModePromptOptions {
29
31
  */
30
32
  export declare function buildAutoModePrompt(options?: BuildAutoModePromptOptions): string;
31
33
  export interface AutoModeWindowOptions {
32
- /** Newest transcript entries included (default 40). */
34
+ /** Newest transcript entries INCLUDED (default 40). Zero includes none — the classifier judges the
35
+ * action with no run history, and the omission line still names every entry left out. */
33
36
  maxEntries?: number;
34
37
  /** Per-entry excerpt cap in characters (default 2_000 — tool results and long turns are truncated
35
38
  * head-first with a `[… N chars truncated]` marker; the classifier weighs shape over bulk). */
@@ -39,6 +42,14 @@ export interface AutoModeWindowOptions {
39
42
  * Render the transcript window half of the classifier's user prompt: the newest `maxEntries` messages,
40
43
  * oldest→newest, each labeled by lane. Plain data — see the module header for the declared v1 gap vs
41
44
  * CC's entry-formatter stack.
45
+ *
46
+ * `maxEntries` counts the entries INCLUDED, so a bound of zero includes none and says so. That case is
47
+ * handled explicitly rather than left to `slice`: `slice(-0)` is `slice(0)`, i.e. the WHOLE transcript,
48
+ * which inverts a declared bound into "unbounded" — and silently, because the omitted count would then
49
+ * be zero and the omission header would not render either. The recipe canonicalizer admits zero for both
50
+ * window knobs deliberately (its sibling knobs use a minimum of one), so a deployment can genuinely
51
+ * declare "classify the action with no transcript context"; a negative bound is not a coherent window
52
+ * either and takes the same arm rather than `slice`'s "drop the oldest N".
42
53
  */
43
54
  export declare function renderAutoModeWindow(messages: readonly Message[], options?: AutoModeWindowOptions): string;
44
55
  /**
@@ -1,6 +1,7 @@
1
+ import { AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
1
2
  import { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./auto-mode-prompt-assets.js";
2
3
  import { cutEnginePrefix, cutEngineSegments, flattenableUserText } from "./untrusted-text.js";
3
- export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
4
+ export { AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
4
5
  function mergeRuleSection(user, defaultsInner) {
5
6
  if (!user?.length)
6
7
  return defaultsInner;
@@ -98,14 +99,16 @@ function renderEntry(m, cap) {
98
99
  return `[assistant]\n${parts.join("\n")}`;
99
100
  }
100
101
  export function renderAutoModeWindow(messages, options) {
101
- const maxEntries = options?.maxEntries ?? 40;
102
- const cap = options?.maxCharsPerEntry ?? 2_000;
103
- const window = messages.slice(-maxEntries);
104
- if (window.length === 0)
102
+ const maxEntries = options?.maxEntries ?? AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES;
103
+ const cap = options?.maxCharsPerEntry ?? AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS;
104
+ const window = maxEntries > 0 ? messages.slice(-maxEntries) : [];
105
+ if (messages.length === 0)
105
106
  return "(transcript empty — this is the run's first gated action)\n";
106
107
  const dropped = messages.length - window.length;
107
- const head = dropped > 0 ? `(… ${dropped} earlier transcript entries omitted)\n\n` : "";
108
- return head + window.map((m) => renderEntry(m, cap)).join("\n\n") + "\n";
108
+ const head = dropped > 0 ? `(… ${dropped} earlier transcript entries omitted)\n` : "";
109
+ if (window.length === 0)
110
+ return head;
111
+ return `${head}${head === "" ? "" : "\n"}${window.map((m) => renderEntry(m, cap)).join("\n\n")}\n`;
109
112
  }
110
113
  export function renderAutoModeAction(input) {
111
114
  const ask = input.askMessage ? `\npermission gate: ${input.askMessage}` : "";
@@ -0,0 +1,75 @@
1
+ import { type AutoModeDecider, type AutoModeDeciderOptions } from "./auto-mode.js";
2
+ import { type AutoModeArmingRecipe, type AutoModeRebuildRefusal } from "./auto-mode-arming.js";
3
+ import type { Message } from "../internal/llm.js";
4
+ /** The redeeming deployment's FRESH model leg: one tool-less completion over a prompt CORE assembles.
5
+ * The split is the trust gate's: rules reach the prompt only through the recipe (a deployment-authored
6
+ * object), never through the leg — the leg dispatches, it does not compose. */
7
+ export type AutoModeClassifierCompletion = (call: {
8
+ systemPrompt: string;
9
+ userPrompt: string;
10
+ }, signal?: AbortSignal) => Promise<string>;
11
+ export interface AutoModeRebuildOptions {
12
+ /** The recipe read off the parked chain entry (`ConstraintChainEntryMeta.autoModeArming`). */
13
+ recorded: unknown;
14
+ /** The redeeming deployment's CURRENT auto-mode settings, minted with `autoModeArmingRecipeOf` from
15
+ * its own face. REQUIRED: bound ② is "the rebuilt arming is bounded by the settings in force", and a
16
+ * caller that declares none has put nothing in force — see {@link foldAutoModeArming}. A deployment
17
+ * with no overrides of its own still declares that (an empty face mints a bare recipe). */
18
+ current: unknown;
19
+ /** The fresh model leg. */
20
+ complete: AutoModeClassifierCompletion;
21
+ /**
22
+ * The transcript the classifier judges the action IN CONTEXT of — the REDEEMING leg's own messages,
23
+ * read at classify time (the window is rebuilt per classification, exactly as the live arming does).
24
+ *
25
+ * REQUIRED, and the reason is a permissiveness one rather than a quality one: the 207 process judges an
26
+ * action against the run that produced it (session linking, earlier writes), so a classifier handed no
27
+ * history can allow what the same classifier would block having seen it — and the empty window renders
28
+ * an affirmative claim, "this is the run's first gated action", that would simply be false. A provider
29
+ * that returns `[]` is DECLARING the transcript empty, which is a different statement from an engine
30
+ * that silently had none to show. A provider that throws fails the classification closed
31
+ * (`unavailable` ⇒ the original chain), never into a blind verdict.
32
+ */
33
+ transcript: () => readonly Message[] | Promise<readonly Message[]>;
34
+ /** The redeeming process's breaker alarm (the recorded arming cannot carry the ancestor's closure). */
35
+ onBreakerOpen?: AutoModeDeciderOptions["onBreakerOpen"];
36
+ }
37
+ export type AutoModeRebuildResult = {
38
+ ok: true;
39
+ /** A decider carrying the ancestor's criteria, over the caller's model leg. */
40
+ decider: AutoModeDecider;
41
+ /**
42
+ * The RECORDED recipe, canonical — put THIS back on the re-supplied chain entry
43
+ * (`parentConstraints[].autoMode.arming`) so the constraint-chain digest matches the parked row.
44
+ * Deliberately not `effective`: the digest binds what the ANCESTOR recorded, and a local tighten
45
+ * must not read as a different ancestor chain.
46
+ */
47
+ arming: AutoModeArmingRecipe;
48
+ /** What the rebuilt decider actually runs under (the tighten-only fold of recorded ∧ current). */
49
+ effective: AutoModeArmingRecipe;
50
+ /** True iff the current settings pulled a knob in. */
51
+ tightened: boolean;
52
+ } | {
53
+ ok: false;
54
+ reason: AutoModeRebuildRefusal;
55
+ message: string;
56
+ };
57
+ /**
58
+ * Rebuild a decider with a PARKED ancestor's criteria from its recorded recipe plus a fresh model leg.
59
+ *
60
+ * What is reproduced: the assembled system prompt (`buildAutoModePrompt` over the recorded rule
61
+ * overrides / settings-deny rules / session context), the transcript-window bounds, the round-trip
62
+ * timeout and the breaker threshold — i.e. every input the ancestor's own `createAutoModeDecider` call
63
+ * had except the model leg and the alarm closure.
64
+ *
65
+ * What is NOT reproduced, deliberately: the ancestor's BREAKER STATE. The rebuilt decider starts closed,
66
+ * and that is sound only because a recipe is never recorded from a decider whose breaker is open OR whose
67
+ * failure streak is non-zero (bound ①, at the persistence point) — either state means the ancestor's own
68
+ * budget was spent or spending, and the row then carries no recipe at all.
69
+ *
70
+ * Also NOT reproduced: this build's PROMPT ASSETS are its own. The recipe records the deployment's
71
+ * overrides, so the assembled prompt is checked against the digest the arming recorded — a fleet running
72
+ * two asset versions refuses to rebuild rather than enforce a different set of default rules under the
73
+ * ancestor's name.
74
+ */
75
+ export declare function rebuildAutoModeDecider(opts: AutoModeRebuildOptions): AutoModeRebuildResult;
@@ -0,0 +1,41 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createAutoModeDecider } from "./auto-mode.js";
3
+ import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "./auto-mode-prompt.js";
4
+ import { foldAutoModeArming, } from "./auto-mode-arming.js";
5
+ export function rebuildAutoModeDecider(opts) {
6
+ const folded = foldAutoModeArming(opts.recorded, opts.current);
7
+ if (!folded.ok)
8
+ return folded;
9
+ const effective = folded.effective;
10
+ const promptOptions = {
11
+ ...(effective.rules !== undefined ? { rules: effective.rules } : {}),
12
+ ...(effective.settingsDenyRules !== undefined ? { settingsDenyRules: effective.settingsDenyRules } : {}),
13
+ ...(effective.sessionContext !== undefined ? { sessionContext: effective.sessionContext } : {}),
14
+ };
15
+ const systemPrompt = buildAutoModePrompt(promptOptions);
16
+ const assembledDigest = `apv1:${createHash("sha256").update(systemPrompt).digest("hex")}`;
17
+ if (folded.recorded.promptDigest !== assembledDigest) {
18
+ return {
19
+ ok: false,
20
+ reason: "prompt_assets_moved",
21
+ message: folded.recorded.promptDigest === undefined
22
+ ? "the recorded arming carries no classifier-prompt digest, so this build cannot establish that the prompt it assembles is " +
23
+ "the one the ancestor enforced (the recipe records overrides; the base shell and permissions document come from the build's " +
24
+ "own assets) — refusing to rebuild rather than enforce criteria that may not be the ancestor's"
25
+ : "this build assembles a DIFFERENT classifier prompt than the recorded arming did (prompt-asset version skew across the fleet: " +
26
+ `recorded ${folded.recorded.promptDigest}, assembled here ${assembledDigest}) — the overrides match but the default rules they ` +
27
+ "merge into do not, so the rebuilt classifier would enforce criteria the ancestor never ran; refusing to rebuild",
28
+ };
29
+ }
30
+ const decider = createAutoModeDecider({
31
+ ...(effective.timeoutMs !== undefined ? { timeoutMs: effective.timeoutMs } : {}),
32
+ ...(effective.failureThreshold !== undefined ? { failureThreshold: effective.failureThreshold } : {}),
33
+ ...(opts.onBreakerOpen !== undefined ? { onBreakerOpen: opts.onBreakerOpen } : {}),
34
+ classify: async (input, signal) => {
35
+ const messages = await opts.transcript();
36
+ const userPrompt = renderAutoModeWindow(messages, effective.window) + renderAutoModeAction(input);
37
+ return opts.complete({ systemPrompt, userPrompt }, signal);
38
+ },
39
+ });
40
+ return { ok: true, decider, arming: folded.recorded, effective, tightened: folded.tightened };
41
+ }
@@ -77,6 +77,21 @@ export interface AutoModeDecider {
77
77
  /** True once the session breaker has opened (it never half-opens: is a SESSION fallback
78
78
  * to non-auto, not a retry window — a flapping classifier must not oscillate the permission mode). */
79
79
  breakerOpen(): boolean;
80
+ /**
81
+ * The CURRENT consecutive-failure streak — the part of the breaker's state that `breakerOpen()` alone
82
+ * cannot express. A latch that has not tripped yet still carries how much budget is left before it
83
+ * does, and #503 needs exactly that: an arming recorded for cross-process rebuild starts a FRESH
84
+ * decider, so recording one from a decider that is already 2 failures into a threshold of 3 would hand
85
+ * the redemption more tolerance for a failing classifier than the ancestor had left — the redeemed leg
86
+ * stays auto-classified through failures that would have dropped the ancestor back to non-auto.
87
+ * {@link import("./tool-policy.js").constraintChainEntryOfLayer} therefore records an arming only at a
88
+ * ZERO streak, and reads a decider that does not implement this member as unknown (⇒ records nothing).
89
+ *
90
+ * OPTIONAL so that hand-built deciders keep type-checking; the engine's own
91
+ * {@link createAutoModeDecider} always implements it. A deployment that hand-rolls a decider and wants
92
+ * the persisted-arming path must implement it too — the conservative reading of absence is deliberate.
93
+ */
94
+ consecutiveFailures?(): number;
80
95
  }
81
96
  /**
82
97
  * Session-scoped decider: timeout + fail-closed error mapping + a one-way circuit breaker.
@@ -1,3 +1,4 @@
1
+ import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS } from "./auto-mode-defaults.js";
1
2
  export function parseAutoModeResponse(text) {
2
3
  const t = text
3
4
  .replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
@@ -16,8 +17,8 @@ export function parseAutoModeResponse(text) {
16
17
  return { kind: "block", category, reason };
17
18
  }
18
19
  export function createAutoModeDecider(opts) {
19
- const timeoutMs = opts.timeoutMs ?? 15_000;
20
- const threshold = Math.max(1, Math.floor(opts.failureThreshold ?? 3));
20
+ const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
21
+ const threshold = Math.max(1, Math.floor(opts.failureThreshold ?? AUTO_MODE_DEFAULT_FAILURE_THRESHOLD));
21
22
  let consecutiveFailures = 0;
22
23
  let open = false;
23
24
  const recordFailure = (cause) => {
@@ -33,6 +34,7 @@ export function createAutoModeDecider(opts) {
33
34
  };
34
35
  return {
35
36
  breakerOpen: () => open,
37
+ consecutiveFailures: () => consecutiveFailures,
36
38
  async decide(input, signal) {
37
39
  if (open)
38
40
  return { kind: "unavailable", cause: "breaker_open" };
@@ -768,6 +768,24 @@ export type PendingAction = {
768
768
  * original-index preservation. The park→resume redemption chain walks the same consent
769
769
  * protocol as the synchronous card — there is no second form. */
770
770
  ruleOffers?: readonly import("./permission-rule-model.js").RuleOffer[];
771
+ /** #490 修② (additive; no checkpoint-version bump — the `previewWithheld`/`hasBidiControls`
772
+ * precedents above: an optional field an older reader ignores and the resume path never reads)
773
+ * — the PARK twin of `AskRequest.ruleOffersAbsence`, minted by the SAME one factory as
774
+ * {@link ruleOffers} so the parked card and the live card cannot answer differently. Present ⇔
775
+ * the rule-offer lane is in play for this call and has nothing to give, naming which door
776
+ * closed: `"mandated"` (no persisted rule could ever clear this ask — a real-approval demand,
777
+ * the tool's own mandate marks, a hook-raised ask, an ancestor's authority, or — #502 — a
778
+ * demotion this CALL's reversibility probe declared structural, which for the built-in shell
779
+ * probe means a read outside the directories the session declared; an inbox must NOT point at
780
+ * rule-writing on this arm, nor at a directory grant, which does not exist in this version),
781
+ * `"shadowed"` (a rule the person already wrote is
782
+ * speaking and does not clear it), `"lane_cannot_speak"` (this card has no rule to offer — the
783
+ * grammar has no text for the command, or the card's array-order contract declines to present
784
+ * the one text that exists; it is NOT a claim that no rule could ever be written).
785
+ * Mutually exclusive with {@link ruleOffers}. ABSENCE is not a claim — it also covers
786
+ * "there are offers" and the structural doors (no lane wired, another tool, a task that cannot
787
+ * hold a rule) — so read presence. See the synchronous seat's doc for the full contract. */
788
+ ruleOffersAbsence?: "mandated" | "lane_cannot_speak" | "shadowed";
771
789
  /**
772
790
  * design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
773
791
  * SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
@@ -1160,6 +1178,43 @@ export interface CheckpointState {
1160
1178
  * delegated child (never `false` — presence-coded like the other monotonic bits here).
1161
1179
  */
1162
1180
  isDelegatedChild?: true;
1181
+ /**
1182
+ * design/380 O1③ — the run TREE's placement fixed point at suspend: the SAME three-segment fold the
1183
+ * env-factory ctx mint uses (explicit `RunInternals.placementRoot` > the delegation lanes'
1184
+ * `rootSessionId` > the leg's own resolved session), stamped from TRUSTED internals so a durable
1185
+ * resume can restore it instead of falling back to the parked leg's own session (a target-bound
1186
+ * factory keys its placement lookup on the fixed point — the self fallback read a resumed leg as a
1187
+ * NEW placement). Restore is the `isDelegatedChild` posture replayed on the placement axis: an
1188
+ * EXPLICIT live `internals.placementRoot` wins when re-supplied; otherwise THIS key restores into
1189
+ * `internals.placementRoot` at prepare so descendants and the NEXT re-suspend mint inherit it too
1190
+ * — outranking a supplied `rootSessionId`, which is DELEGATION IDENTITY (the peer/registry axis a
1191
+ * revive leg re-supplies from its row), never a placement claim (rescan C1: reading it as one
1192
+ * permanently stuck every parked agent spawned under an explicit placement root). The resume entry
1193
+ * ALSO reads this key as an identity record: a resume whose supplied `internals.placementRoot`
1194
+ * CONTRADICTS it is refused pre-CAS (`resume.placement_mismatch`); either side absent skips the
1195
+ * check (an older row / a claim-less resume — the honest degradation arm), and `""` reads as
1196
+ * absence (the identity fields' discipline). Schema ADDITION (additive, old readers ignore it — the Q5 posture;
1197
+ * deliberately NOT version-gated: same rolling-deployment account as {@link Checkpoint.principal},
1198
+ * whose doc records version-gating as a design candidate — any new stamp is a lattice-wide window
1199
+ * rewrite, owned by that ticket, not ad-hoc here). Absent ⇒ minted before the field, or the fold
1200
+ * resolved empty on a factory-less run (never persisted as garbage).
1201
+ */
1202
+ placementRootSessionId?: string;
1203
+ /**
1204
+ * design/380 O9a — MONOTONIC presence bit: this run's hand-tool content fold was EXTERNAL-TARGET
1205
+ * armed (the env declared `ExecutionEnv.externalContentTarget`, or an earlier leg of this suspend
1206
+ * chain did). Stamped at the suspend mint from the run's RESOLVED fold state — never from first
1207
+ * tool use, so a park landing BEFORE the first hand call is covered too. Presence-coded like
1208
+ * {@link isDelegatedChild} (absent = never armed; never `false`). The resume side folds
1209
+ * **persisted OR live declaration** (a monotonic join, never a downgrade): a run minted under a
1210
+ * declaring adapter keeps its memory fold and its prompt disclosure when resumed on an
1211
+ * undeclared/older adapter — the downgrade is closed in the derivation itself, deliberately
1212
+ * WITHOUT a new refusal arm (one fewer failure path, same safety). Feeds the memory face and the
1213
+ * prompt disclosure only — no gate/policy/roster behavior reads it (the design/378 red line).
1214
+ * Schema ADDITION (additive, Q5 posture; the version-gate residual is shared with
1215
+ * {@link placementRootSessionId} above and owned by the same version-lattice ticket).
1216
+ */
1217
+ externalContentTarget?: true;
1163
1218
  }
1164
1219
  /**
1165
1220
  * {@link CheckpointState} with **every** field made required-PRESENT, while each value keeps its original
@@ -1869,6 +1924,30 @@ export declare class CheckpointError extends Error {
1869
1924
  * refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
1870
1925
  * original principal, or omit the field to inherit the recorded one. */
1871
1926
  | "resume.principal_mismatch"
1927
+ /** design/380 O1③ (placement identity continuity — the `resume.principal_mismatch` rung's twin
1928
+ * on the placement axis): the resume supplied trusted internals whose EXPLICIT
1929
+ * `placementRoot` differs from the {@link CheckpointState.placementRootSessionId} recorded at
1930
+ * the suspend mint. A target-bound env factory keys its placement lookup on that fixed point,
1931
+ * so silently resuming under a different one would re-place the leg — and every descendant
1932
+ * the child-internals chain hands it to — on another target than the parked workspace lives
1933
+ * on. A supplied `rootSessionId` deliberately does NOT take this refusal (rescan C1): it is
1934
+ * delegation identity, re-supplied by every revive leg, and legitimately differs from the
1935
+ * recorded root whenever the spawner carried an explicit placement — the recorded root simply
1936
+ * outranks it in prepare's restore fold. Rejected PRE-CAS (the checkpoint stays `pending`);
1937
+ * re-resume with the recorded root, or omit `internals.placementRoot` to inherit it. Either
1938
+ * side absent (an older row / a claim-less resume) skips the check, and `""` reads as absence
1939
+ * — the honest degradation arm. */
1940
+ | "resume.placement_mismatch"
1941
+ /** design/380 O2: the deployment's `RunnerDeps.resumePreflight` refused this resume — an
1942
+ * explicit refusal verdict, a throw, a timeout past its bounded deadline, or an unreadable
1943
+ * answer (all fail-closed). The LAST rung before the CAS, so nothing was consumed. Two arms,
1944
+ * discriminated by the message: the default `retry_later` arm leaves the checkpoint `pending`
1945
+ * and the SAME token redeemable once the obstacle clears (`detail.retryAfterMs` carries the
1946
+ * deployment's wait hint when it gave one — the #449 G1 carrier); the explicit `terminal` arm
1947
+ * has ALREADY settled the row out of `pending` via the single-shot `expire` CAS (reaper
1948
+ * parity) before this throw — the token is NOT redeemable, by the deployment's own permanent
1949
+ * verdict (a dead binding must not be redialed forever). */
1950
+ | "resume.preflight_rejected"
1872
1951
  /** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
1873
1952
  * constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
1874
1953
  * edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
@@ -1896,8 +1975,11 @@ export declare class CheckpointError extends Error {
1896
1975
  * discriminant. */
1897
1976
  readonly detail?: {
1898
1977
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
1899
- /** #449 G1 — on `resume.usage_window_exhausted` ONLY: how long until the binding window frees
1900
- * (the same wait a suspended row carries as `gate.resumeAfterMs`). Absent on every other code. */
1978
+ /** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
1979
+ * (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 on a
1980
+ * retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
1981
+ * gave one (same carrier, same semantics: retry the SAME token after the wait). Absent on
1982
+ * every other code. */
1901
1983
  retryAfterMs?: number;
1902
1984
  /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1903
1985
  * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
@@ -2024,6 +2106,30 @@ export declare class CheckpointError extends Error {
2024
2106
  * refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
2025
2107
  * original principal, or omit the field to inherit the recorded one. */
2026
2108
  | "resume.principal_mismatch"
2109
+ /** design/380 O1③ (placement identity continuity — the `resume.principal_mismatch` rung's twin
2110
+ * on the placement axis): the resume supplied trusted internals whose EXPLICIT
2111
+ * `placementRoot` differs from the {@link CheckpointState.placementRootSessionId} recorded at
2112
+ * the suspend mint. A target-bound env factory keys its placement lookup on that fixed point,
2113
+ * so silently resuming under a different one would re-place the leg — and every descendant
2114
+ * the child-internals chain hands it to — on another target than the parked workspace lives
2115
+ * on. A supplied `rootSessionId` deliberately does NOT take this refusal (rescan C1): it is
2116
+ * delegation identity, re-supplied by every revive leg, and legitimately differs from the
2117
+ * recorded root whenever the spawner carried an explicit placement — the recorded root simply
2118
+ * outranks it in prepare's restore fold. Rejected PRE-CAS (the checkpoint stays `pending`);
2119
+ * re-resume with the recorded root, or omit `internals.placementRoot` to inherit it. Either
2120
+ * side absent (an older row / a claim-less resume) skips the check, and `""` reads as absence
2121
+ * — the honest degradation arm. */
2122
+ | "resume.placement_mismatch"
2123
+ /** design/380 O2: the deployment's `RunnerDeps.resumePreflight` refused this resume — an
2124
+ * explicit refusal verdict, a throw, a timeout past its bounded deadline, or an unreadable
2125
+ * answer (all fail-closed). The LAST rung before the CAS, so nothing was consumed. Two arms,
2126
+ * discriminated by the message: the default `retry_later` arm leaves the checkpoint `pending`
2127
+ * and the SAME token redeemable once the obstacle clears (`detail.retryAfterMs` carries the
2128
+ * deployment's wait hint when it gave one — the #449 G1 carrier); the explicit `terminal` arm
2129
+ * has ALREADY settled the row out of `pending` via the single-shot `expire` CAS (reaper
2130
+ * parity) before this throw — the token is NOT redeemable, by the deployment's own permanent
2131
+ * verdict (a dead binding must not be redialed forever). */
2132
+ | "resume.preflight_rejected"
2027
2133
  /** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
2028
2134
  * constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
2029
2135
  * edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
@@ -2051,8 +2157,11 @@ export declare class CheckpointError extends Error {
2051
2157
  * discriminant. */
2052
2158
  detail?: {
2053
2159
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
2054
- /** #449 G1 — on `resume.usage_window_exhausted` ONLY: how long until the binding window frees
2055
- * (the same wait a suspended row carries as `gate.resumeAfterMs`). Absent on every other code. */
2160
+ /** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
2161
+ * (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 on a
2162
+ * retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
2163
+ * gave one (same carrier, same semantics: retry the SAME token after the wait). Absent on
2164
+ * every other code. */
2056
2165
  retryAfterMs?: number;
2057
2166
  /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
2058
2167
  * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is