@sema-agent/core 5.65.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 (159) hide show
  1. package/CHANGELOG.md +46 -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 +18 -0
  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 +39 -7
  35. package/dist/core/hooks.js +38 -21
  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 +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  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 +64 -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/retention-policy.d.ts +9 -0
  71. package/dist/core/retention-policy.js +5 -2
  72. package/dist/core/retention.d.ts +13 -2
  73. package/dist/core/runner/assemble-result.d.ts +19 -1
  74. package/dist/core/runner/assemble-result.js +17 -2
  75. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  76. package/dist/core/runner/compaction-call-options.js +3 -0
  77. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  78. package/dist/core/runner/memory-capture-optout.js +53 -0
  79. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  80. package/dist/core/runner/prepare-config-doors.js +16 -0
  81. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  82. package/dist/core/runner/prepare-hands-readface.js +99 -7
  83. package/dist/core/runner/prepare-memory.d.ts +88 -0
  84. package/dist/core/runner/prepare-memory.js +305 -24
  85. package/dist/core/runner/prepare-task.d.ts +141 -1
  86. package/dist/core/runner/prepare-task.js +443 -79
  87. package/dist/core/runner/runtask.d.ts +9 -20
  88. package/dist/core/runner/runtask.js +133 -96
  89. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  90. package/dist/core/runner/session-file-state-replay.js +52 -1
  91. package/dist/core/runner/tool-disclosure.js +2 -1
  92. package/dist/core/runner/turn-attachments.d.ts +22 -12
  93. package/dist/core/session-store.d.ts +1 -1
  94. package/dist/core/session-store.js +6 -1
  95. package/dist/core/session.d.ts +34 -1
  96. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  97. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  98. package/dist/core/task-registry-shared.js +11 -1
  99. package/dist/core/tool-errors.js +1 -0
  100. package/dist/core/tool-policy.d.ts +172 -1
  101. package/dist/core/tool-policy.js +32 -1
  102. package/dist/core/tool-result-store.js +2 -1
  103. package/dist/core/trace.d.ts +24 -0
  104. package/dist/core/types.d.ts +784 -89
  105. package/dist/core/types.js +4 -3
  106. package/dist/core/untrusted-text.d.ts +1 -1
  107. package/dist/core/untrusted-text.js +8 -0
  108. package/dist/core/workflow-run-store-contract.js +6 -2
  109. package/dist/core/workflow-run-store.d.ts +4 -1
  110. package/dist/engine/compaction/compaction.d.ts +88 -10
  111. package/dist/engine/compaction/compaction.js +109 -30
  112. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  113. package/dist/engine/execution-env/node-execution-env.js +28 -0
  114. package/dist/engine/harness/agent-harness.d.ts +52 -1
  115. package/dist/engine/harness/agent-harness.js +36 -1
  116. package/dist/engine/harness/types.d.ts +26 -1
  117. package/dist/engine/llm/types.d.ts +50 -4
  118. package/dist/engine/loop/agent-loop.d.ts +5 -1
  119. package/dist/engine/loop/agent-loop.js +25 -0
  120. package/dist/engine/loop/types.d.ts +19 -0
  121. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  122. package/dist/engine/session/session.js +1 -1
  123. package/dist/index.d.ts +18 -8
  124. package/dist/index.js +14 -6
  125. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  126. package/dist/orchestration/run-workflow-tool.js +22 -3
  127. package/dist/orchestration/workflow-governance.d.ts +59 -1
  128. package/dist/orchestration/workflow-governance.js +61 -8
  129. package/dist/orchestration/workflow-meta.d.ts +4 -2
  130. package/dist/orchestration/workflow-primitives.js +56 -13
  131. package/dist/orchestration/workflow-types.d.ts +78 -2
  132. package/dist/orchestration/workflow.d.ts +20 -0
  133. package/dist/orchestration/workflow.js +163 -14
  134. package/dist/prompt-assembly/event-registry.js +1 -1
  135. package/dist/prompts/default.d.ts +7 -7
  136. package/dist/stores/file/file-history-store.d.ts +368 -0
  137. package/dist/stores/file/file-history-store.js +1248 -0
  138. package/dist/stores/file/index.d.ts +22 -13
  139. package/dist/stores/file/index.js +4 -4
  140. package/dist/stores/file/permission-rule-store.js +1 -0
  141. package/dist/stores/file/strategy-store.d.ts +3 -3
  142. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  143. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  144. package/dist/tools/fs/fs-bash.js +9 -5
  145. package/dist/tools/fs/fs-shared.d.ts +52 -1
  146. package/dist/tools/fs/fs-shared.js +14 -0
  147. package/dist/tools/fs/fs-write.d.ts +5 -5
  148. package/dist/tools/fs/fs-write.js +71 -14
  149. package/dist/tools/fs/index.d.ts +6 -1
  150. package/dist/tools/fs/index.js +1 -1
  151. package/dist/tools/web.js +2 -1
  152. package/package.json +5 -1
  153. package/test/export-surface.snapshot.json +155 -23
  154. package/dist/core/file-snapshot-store.d.ts +0 -165
  155. package/dist/core/file-snapshot-store.js +0 -259
  156. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  157. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  158. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  159. 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
@@ -67,6 +67,32 @@ export declare function contextEditFrontier(window: number): number;
67
67
  * re-run when the frontier pass is off → in-turn forced compaction behind the adopt seam → trim
68
68
  * demoted to the disaster-only last resort), so the default configuration never hands an
69
69
  * under-20k refusal straight to a message-dropping trim.
70
+ *
71
+ * WHOLE-MACHINE CROSS-CHECK against the 2.1.250 corpus (design/374 slice 4 — the "the knob number
72
+ * was copied, the machine was not" disease is checked by naming the machine's parts, not the one
73
+ * number). CC's four load-bearing parts, each against its 250 coordinate, all verified present here:
74
+ * ① the ≥20000-token minimum-savings gate — {@link MIN_CLEAR_SAVINGS_TOKENS} vs `Mnn`
75
+ * (cli250.js:41714, `BBn`: `if (tokensSaved < Mnn) return null`). CC gates TWICE (execution +
76
+ * before asking the server to reject); sema has one point because the "ask for the rejection"
77
+ * half needs a cooperating server (see ④).
78
+ * ② the nine-member clearable whitelist — {@link COMPACTABLE_TOOLS} vs `eQt` (cli250.js:41714),
79
+ * re-resolved member-by-member off the 250 corpus, byte-identical (see that constant).
80
+ * ③ cleared-but-recoverable — CC replaces the content with a POINTER, not a void
81
+ * (`<persisted-output>Tool result saved to: {path}\n\nUse Read to view</persisted-output>`,
82
+ * cli250.js:40044), which is what makes a keep window of 5 survivable at all. Ours is the same
83
+ * construction in this engine's carrier: the full text is offloaded first and the marker
84
+ * carries the store ref ({@link ContextEditOptions.offload} → {@link clearOneResult}), read
85
+ * back through ReadToolResult instead of a filesystem path (registered divergence D-9). The
86
+ * runner wires it on BOTH arms by default — the frontier pass and the MC-R rejection arm share
87
+ * one persist closure — so the shipped default clears to a pointer, never to a dead end.
88
+ * ④ the TRIGGER — CC's client machine fires ONLY on the server's `context_hint` 422/424 refusal
89
+ * (cli250.js:58375; `tengu_time_based_microcompact` is a historical event name, its `trigger`
90
+ * is the constant `"context_hint"`). Under BYOM there is no such server, so the ask half is
91
+ * unportable and the refusal half is aligned on wire truth (the MC-R arm classifies the
92
+ * provider's own context-overflow refusal) — registered divergence D-3. sema additionally
93
+ * keeps a PROACTIVE frontier trigger CC's client does not have, anchored to Anthropic's
94
+ * platform context-management policy rather than to CC-CLI — registered divergence D-1, and
95
+ * re-stated at {@link clearStaleToolResults}'s ANCHOR paragraph.
70
96
  */
71
97
  export type ContextEditMachine = "legacy" | "cc";
72
98
  /**
@@ -82,8 +108,10 @@ export type ContextEditMachine = "legacy" | "cc";
82
108
  * exactly the terminal-batch shape that needs it. This is why the slice-3 "keep default 3→5"
83
109
  * flip rides the MACHINE default, not this constant: painting 5 onto the opt-out machine would
84
110
  * recreate the probed failure inside the escape hatch.
85
- * - CC machine (the default since slice 3): 5 (2.1.223 `uAp`; same value on CC 88's
86
- * `timeBasedMCConfig` stable across both corpus generations). The #384 adjudication adopted 5
111
+ * - CC machine (the default since slice 3): 5 (2.1.223 `uAp`; 2.1.250 `var k = 5` at the call site
112
+ * cli250.js:58375, and 2.1.245 carries the same value zero drift across 245→250; same value
113
+ * again on CC 88's `timeBasedMCConfig`, i.e. stable across all three corpus generations, and it
114
+ * is a hard-coded constant upstream, not a setting). The #384 adjudication adopted 5
87
115
  * CONDITIONAL on the whole machine coming with it (20k gate + deep clear + re-ordered
88
116
  * backstops); the cc machine carries that condition, and slice 3's backstop re-ordering
89
117
  * completed it — 5 is now the effective default keep, and the 200k probe above is answered by
@@ -92,7 +120,8 @@ export type ContextEditMachine = "legacy" | "cc";
92
120
  */
93
121
  export declare const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
94
122
  export declare const CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS = 5;
95
- /** design/374 §3.1 — CC's minimum-savings gate (`vUs` :397773): the cc machine refuses to clear at
123
+ /** design/374 §3.1 — CC's minimum-savings gate (`vUs` :397773; 2.1.250 `Mnn` cli250.js:41714, same
124
+ * value): the cc machine refuses to clear at
96
125
  * all unless the structural estimate of what WOULD be cleared reaches this many tokens. Rationale
97
126
  * (independent of "CC does it"): every clear breaks the provider prefix cache once, so a clear that
98
127
  * saves less than the re-prefill it causes is a net loss; the gate prices that trade. The LEGACY
@@ -105,7 +134,18 @@ export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
105
134
  * checklist #54 (CC microCompact.ts:41-50 `COMPACTABLE_TOOLS` parity): the INCLUSION whitelist of
106
135
  * tools whose results may be content-cleared by the stale-result pass. CC's set is exactly
107
136
  * { Read, Bash, PowerShell (SHELL_TOOL_NAMES), Grep, Glob, WebSearch, WebFetch, Edit, Write } —
108
- * cheap-to-regenerate I/O. Everything else is PRESERVED: expensive/decision-bearing results
137
+ * cheap-to-regenerate I/O.
138
+ *
139
+ * RE-VERIFIED against the 2.1.250 corpus (design/374 slice 4, member-by-member — a set is exactly
140
+ * the shape where "the count still matches" is not evidence): `eQt = new Set([wt, ...VD, Yo, Xo,
141
+ * yO, Yr, tn, er])` at cli250.js:41714, whose aliases resolve to `wt`="Read", `VD`=[`Xe`="Bash",
142
+ * `Nt`="PowerShell"], `Yo`="Grep", `Xo`="Glob", `yO`="WebSearch", `Yr`="WebFetch", `tn`="Edit",
143
+ * `er`="Write" — nine members, byte-identical to the set below, zero drift from the 223 reading.
144
+ * The exclusion side is the load-bearing half and is unchanged too: everything NOT here is
145
+ * PRESERVED (Agent/Task/Skill/ToolSearch/TodoWrite/Monitor/SendMessage/NotebookEdit and ALL MCP
146
+ * tools are never content-cleared by this pass, upstream or here).
147
+ *
148
+ * Everything else is PRESERVED: expensive/decision-bearing results
109
149
  * (Agent subagent reports, Task* registry state, TodoWrite acks, Monitor, MCP tools, custom
110
150
  * ToolSpecs) survive to the harder defenses (compaction summarizes them instead of blanking them).
111
151
  * PowerShell is kept for CC fidelity even though sema does not mount it. Overridable per call via
@@ -184,7 +224,9 @@ export interface ContextEditOptions {
184
224
  * Absent ⇒ legacy structural gate + accounting (byte-identical behavior).
185
225
  */
186
226
  anchoredTotalTokens?: number;
187
- /** design/123 D2 — structural coefficient (`Model.charsPerToken`); default 4. */
227
+ /** design/123 D2 — structural coefficient (`Model.charsPerToken`); default 3 (design/374 slice 4;
228
+ * it was 4 before). Note this does NOT reach the cc machine's ≥20k gate, which is CC-exactly
229
+ * hard-coded to 4 — see {@link structuralClearSavings}. */
188
230
  charsPerToken?: number;
189
231
  /**
190
232
  * checklist #54 — which tools' results are clearable. Defaults to {@link COMPACTABLE_TOOLS}
@@ -16,7 +16,7 @@ import type { Model } from "../internal/llm.js";
16
16
  * message AT/BEFORE the anchor reduces it by ZERO (the anchor usage was billed with that content —
17
17
  * a request-view drop only pays off on the NEXT request), so the trim keeps cutting deeper until
18
18
  * the anchored total meets the budget or only the minimal tail is left. Absent ⇒ legacy structural
19
- * behavior, byte-identical. `charsPerToken` (design/123 D2): structural coefficient, default 4.
19
+ * behavior, byte-identical. `charsPerToken` (design/123 D2): structural coefficient, default 3 (design/374 slice 4).
20
20
  */
21
21
  export declare function trimToBudget(messages: AgentMessage[], budgetTokens: number, anchoredTotalTokens?: number, charsPerToken?: number): AgentMessage[];
22
22
  /**