@sema-agent/core 7.18.0 → 7.19.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 (72) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/dist/brain/anthropic.js +6 -3
  3. package/dist/brain/open-responses.js +10 -4
  4. package/dist/brain/openai.js +5 -2
  5. package/dist/brain/reasoning.d.ts +13 -13
  6. package/dist/brain/reasoning.js +2 -2
  7. package/dist/brain/stream-engine.js +2 -1
  8. package/dist/brain/stream-shared.d.ts +3 -0
  9. package/dist/brain/stream-shared.js +1 -0
  10. package/dist/core/ask-origin.d.ts +16 -0
  11. package/dist/core/ask-origin.js +8 -0
  12. package/dist/core/auto-mode-arming.d.ts +6 -0
  13. package/dist/core/auto-mode-arming.js +5 -5
  14. package/dist/core/auto-mode-defaults.d.ts +43 -15
  15. package/dist/core/auto-mode-defaults.js +4 -1
  16. package/dist/core/auto-mode.d.ts +43 -14
  17. package/dist/core/auto-mode.js +79 -10
  18. package/dist/core/checkpoint-store.d.ts +7 -0
  19. package/dist/core/checkpoint-store.js +2 -2
  20. package/dist/core/engine-notice.d.ts +30 -0
  21. package/dist/core/gate-fold.js +5 -1
  22. package/dist/core/gate-lanes.js +7 -7
  23. package/dist/core/gate-outcome.d.ts +25 -0
  24. package/dist/core/gate-outcome.js +7 -0
  25. package/dist/core/governance-codes.d.ts +1 -1
  26. package/dist/core/governance-codes.js +6 -0
  27. package/dist/core/hooks.d.ts +1 -1
  28. package/dist/core/read-root-candidate.d.ts +12 -0
  29. package/dist/core/read-root-candidate.js +24 -0
  30. package/dist/core/roles.d.ts +9 -1
  31. package/dist/core/roles.js +4 -0
  32. package/dist/core/runner/classifier-ladder.d.ts +149 -11
  33. package/dist/core/runner/classifier-ladder.js +159 -10
  34. package/dist/core/runner/contracts.d.ts +2 -0
  35. package/dist/core/runner/denial-limit-arms.js +2 -2
  36. package/dist/core/runner/prepare-ask-lane.js +1 -0
  37. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  38. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  39. package/dist/core/runner/prepare-config-doors.js +8 -10
  40. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  41. package/dist/core/runner/prepare-project-context.js +3 -2
  42. package/dist/core/runner/prepare-task.js +1 -1
  43. package/dist/core/runner/resume-apply.js +3 -3
  44. package/dist/core/runner/run-leg.js +10 -4
  45. package/dist/core/runner/run-turn-boundary.js +12 -4
  46. package/dist/core/runner/synthetic-tools.d.ts +67 -29
  47. package/dist/core/runner/synthetic-tools.js +51 -22
  48. package/dist/core/runner/turn-attachments.d.ts +43 -12
  49. package/dist/core/runner/turn-attachments.js +58 -14
  50. package/dist/core/sensitive-path-policy.d.ts +58 -17
  51. package/dist/core/sensitive-path-policy.js +33 -5
  52. package/dist/core/skill-listing-budget.d.ts +112 -0
  53. package/dist/core/skill-listing-budget.js +58 -0
  54. package/dist/core/task-spec.d.ts +26 -0
  55. package/dist/core/tool-policy.d.ts +19 -4
  56. package/dist/core/tool-policy.js +6 -7
  57. package/dist/core/tool-spec.d.ts +37 -4
  58. package/dist/core/types.d.ts +1 -1
  59. package/dist/core/write-protect.d.ts +8 -3
  60. package/dist/core/write-protect.js +8 -5
  61. package/dist/index.d.ts +4 -4
  62. package/dist/index.js +3 -3
  63. package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -0
  64. package/dist/tools/fs/bash-readonly-classifier.js +59 -6
  65. package/dist/tools/fs/fs-bash.js +7 -3
  66. package/dist/tools/fs/read-deny.d.ts +4 -3
  67. package/dist/tools/fs/safety.d.ts +15 -0
  68. package/dist/tools/fs/safety.js +7 -0
  69. package/package.json +1 -1
  70. package/test/export-surface.snapshot.json +15 -5
  71. package/wire-changes/core@7.18.0.json +1273 -0
  72. package/wire-changes/core@7.19.0.json +457 -0
@@ -1,6 +1,8 @@
1
1
  /**
2
- * The auto-mode classifier's CANDIDATE LADDER, as one module: which models a classification may be asked on,
3
- * its per-rung reasoning seat, and the once-per-run disclosure its first move mints.
2
+ * The auto-mode classifier's CANDIDATE LADDER, as one module: which models a classification may be asked on
3
+ * (and which model stands in when no `classifier` role names one), its per-rung reasoning seat and the
4
+ * per-attempt user-message tail, how a rung's reply is read for a re-ask or a failure, and the once-per-run
5
+ * disclosures its first move and its broken contract mint.
4
6
  *
5
7
  * Beside `derived-route-fallback.ts` and in the same family — both are seats the caps phase's classifier
6
8
  * arming reaches DOWN for, and both answer "this leg's model did not work out" one layer apart. Which is why
@@ -30,7 +32,7 @@
30
32
  * `modelRequested` reading above; the unification (the pre-flight as rung 0 under its own cause word, one
31
33
  * record face for both) changes the notice set and is a ticket, not a repair.
32
34
  */
33
- import type { Model } from "../../internal/llm.js";
35
+ import type { AssistantMessage, Model } from "../../internal/llm.js";
34
36
  import { type AutoModeClassified } from "../auto-mode.js";
35
37
  import type { RunnerDeps } from "../types.js";
36
38
  /**
@@ -51,14 +53,69 @@ import type { RunnerDeps } from "../types.js";
51
53
  * loud refusal at the brain's request gate.
52
54
  */
53
55
  export declare function classifierLadderRungs(knob: unknown, classifier: Model, main: Model): readonly Model[];
56
+ /**
57
+ * WHY the classifier seat is the model it is when no `classifier` role names one — the closed set behind the
58
+ * `classifier.role_defaulted` notice's `reason`. `tier_below_main` = the nearest directly-bound tier below the
59
+ * run's main model; `tier_table` = the static `flash` row (a main bound to no tier); `role_fallback` = the
60
+ * `summarize → default` chain (no tiers at all); `main_only` = the main model is the only model there is (a
61
+ * single-model catalog, or nothing bound below the main's tier); `role_unresolvable` = a `classifier` role
62
+ * NAMED a model the catalog cannot resolve (a stale or misspelled reference), so the main model stands in —
63
+ * the pre-#805 leg fell back the same way, silently.
64
+ */
65
+ export declare const CLASSIFIER_ROLE_DEFAULT_REASONS: readonly ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
66
+ export type ClassifierRoleDefaultReason = (typeof CLASSIFIER_ROLE_DEFAULT_REASONS)[number];
67
+ /** What {@link classifierSeatModel} returns: the model, and — when no role NAMED it — why it is that one. */
68
+ export type ClassifierSeat = {
69
+ readonly model: Model;
70
+ readonly defaulted?: ClassifierRoleDefaultReason;
71
+ };
72
+ /**
73
+ * The model the classifier SEAT resolves to for this run — ONE resolver for "which model classifies":
74
+ * · a `classifier` role that NAMES a model (`roleSpecNamesModel`: a string ref, a Model object, `{model}`,
75
+ * `{select}`) ⇒ the role resolver's answer, exactly as before (a preset-only role — `{thinking}` alone —
76
+ * does not name one and takes the default below);
77
+ * · else, with tier bindings declared (`deps.tiers`) and a main model on a tier ⇒ the nearest tier strictly
78
+ * BELOW the main's whose route differs from the main's — `tier_below_main`; the main's tier is the LOWEST
79
+ * tier its route sits on (a route bound as both `max` and `flash` is a `flash` main: deterministic, and
80
+ * the cheaper reading); nothing below ⇒ the main itself, `main_only`. Read off the expanded catalog's
81
+ * tier keys, the Runner's own resolution of a tier word: a key synthesised for an unbound tier falls DOWN
82
+ * to the next bound tier, which is the model this walk reaches anyway, and a catalog key spelled like a
83
+ * tier shadows the binding here as it does everywhere. No thinking preference: whether a model's
84
+ * thinking can be turned off is the SEAT's business ({@link classifierRungSeat}), not routing's;
85
+ * · else the role resolver's own fallback (the static tier row `classifier → flash` when tiers are active,
86
+ * `tier_table`; the `summarize → default` chain otherwise, `role_fallback`); a catalog with no roles at
87
+ * all ⇒ the main model, `main_only`.
88
+ * The relative pick follows the run's MAIN model on purpose: a side channel that resolved without it is the
89
+ * misfit the ticket names (a `flash` classifier above a `lite` main; two tiers below a `max` main).
90
+ */
91
+ export declare function classifierSeatModel(args: {
92
+ deps: Pick<RunnerDeps, "models" | "roles" | "tiers">;
93
+ main: Model;
94
+ }): ClassifierSeat;
95
+ /**
96
+ * The `classifier.role_defaulted` line — minted ONCE per run by the arming, AFTER the route pre-flight, so it
97
+ * names the seat actually asked (a pre-flight swap to the main model is its own line,
98
+ * `route.fallback_to_primary`, same run). Silent when a role named the model: nothing was defaulted.
99
+ */
100
+ export declare function announceClassifierRoleDefaulted(args: {
101
+ onNotice: RunnerDeps["onNotice"];
102
+ seat: ClassifierSeat;
103
+ sessionId?: string;
104
+ runId?: string;
105
+ }): void;
54
106
  /**
55
107
  * The classifier's REASONING SEAT, read off the model a classification is actually asked on — ONE predicate:
56
- * can the wire say off? Yes ⇒ `off` + the verdict-sized cap (the request pays for the answer alone); no ⇒
57
- * `low` + the verdict cap + the low-reasoning budget (a model that thinks regardless is told to think the
58
- * LEAST its wire can spell, in exactly the room that tier needs). PER RUNG, not once per arming: the second
59
- * rung is a different model with its own wire, and asking it under the first rung's seat is how a correct
60
- * fallback comes back with zero text. The `low` seat is also the re-ask seat: an unknown model that answers
61
- * NOTHING at the `off` cap has just told us it thinks anyway, and is re-asked as one that does.
108
+ * does the wire SPELL off ({@link thinkingOffSpelled})? Yes ⇒ `off` + the verdict-sized cap (the request pays
109
+ * for the answer alone); no ⇒ `low` + the verdict cap + the low allowance (a model whose thinking the wire
110
+ * cannot turn off is asked for the least it can grade, in the room that costs — CC 2.1.259 `MU`: spell off,
111
+ * or leave thinking to the provider and pay 2048). A model that declares no reasoning at all is on the `low`
112
+ * seat too: its wire carries nothing either way, so the request is the gateway's default, and a default-on
113
+ * gateway thinks — the arm that used to read it as "never thinks" is the one the field report was about.
114
+ * PER RUNG, not once per arming: the second rung is a different model with its own wire.
115
+ *
116
+ * The THINK seat is the RE-ASK seat ({@link classifierReplyAsksAgain}): a first attempt that produced no
117
+ * verdict is asked once more with thinking allowed the room of CC's stage-2 classifier (8192) — an unknown
118
+ * model that answered NOTHING at its cap has just told us it thinks past it, and is re-asked as one that does.
62
119
  */
63
120
  export declare const CLASSIFIER_OFF_SEAT: {
64
121
  reasoning: "off";
@@ -68,11 +125,92 @@ export declare const CLASSIFIER_LOW_SEAT: {
68
125
  reasoning: "low";
69
126
  maxTokens: number;
70
127
  };
128
+ export declare const CLASSIFIER_THINK_SEAT: {
129
+ reasoning: "low";
130
+ maxTokens: number;
131
+ };
132
+ export type ClassifierSeatRequest = typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT | typeof CLASSIFIER_THINK_SEAT;
133
+ /**
134
+ * The seat for one rung's FIRST attempt — see the seat table above.
135
+ *
136
+ * @contract classifier.budget_is_room — every classifier output cap is ROOM the request asks for, never a
137
+ * bound the engine can vouch for: a gateway may exclude reasoning tokens from `max_tokens` (a vLLM-fronted
138
+ * Qwen gateway answered a cap of 128 with 938 output tokens and `stop`), so an over-long think is cut by the
139
+ * round-trip timeout (`unavailable` / `timeout`), not by the cap (`parse_error` on an empty reply), and the
140
+ * two are attested as the two different facts they are.
141
+ */
71
142
  export declare function classifierRungSeat(model: Model): typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT;
143
+ /**
144
+ * The user-message TAIL of each attempt — CC 2.1.259's classifier suffixes, verbatim: the FIRST attempt
145
+ * carries the fast-mode suffix (`$ns`: the answer must begin with `<block>`, no preamble), the RE-ASK carries
146
+ * the thinking-stage suffix (`Uns`: think in `<thinking>` first, then answer). The base prompt (2.1.250 `KRg`)
147
+ * already says "no preamble" once; upstream repeats it at the END of the user message, and it is the repeat
148
+ * that lands — live 2026-09-15, a Qwen 3.8 with thinking off went from 70% tagged replies (1.2–3.9k chars of
149
+ * "Looking at this action…" prose) to 90% at 8 output tokens with the first suffix, and the re-ask suffix
150
+ * had it write its reasoning inside `<thinking>` (stripped by the reader) before the tag.
151
+ */
152
+ export declare const CLASSIFIER_ATTEMPT_SUFFIX: {
153
+ readonly first: "\nErr on the side of blocking. Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No \"Looking at...\" or similar preamble.";
154
+ readonly reask: "\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>. Think longer on ambiguous or borderline actions; keep reasoning brief for clear-cut ones.";
155
+ };
156
+ /** The classifier reply's answer text — every text block joined; thinking blocks are not the verdict. */
157
+ export declare function classifierReplyText(reply: AssistantMessage): string;
158
+ /**
159
+ * CC 2.1.259 `$Tt`: an EMPTY reply that stopped at the output cap is "unparseable" — a reply to ask for again,
160
+ * not a refusal and not a verdict. The engine's brains report that cut two ways: `stopReason:"length"` (an
161
+ * engine-shrunk cap) or `stopReason:"error"` with `errorKind:"length_empty"` (a static cap — the classifier's
162
+ * own). An empty reply with any OTHER stop is CC's `policy_refusal` arm: not re-asked.
163
+ */
164
+ export declare function classifierReplyEmptyAtCap(reply: AssistantMessage): boolean;
165
+ /**
166
+ * A reply that may answer in the TAG form only: its text is not a complete answer of its own. Output cut by the cap
167
+ * (`stopReason: "length"`), a dropped connection or a stalled stream (`partialFinalized`, with a synthesised
168
+ * `stop`), and a reply that CALLED A TOOL (`stopReason: "toolUse"`, or a tool-call block in the content — the
169
+ * classifier is given no tools, and the openai brain reports a length cut with a surviving call as `toolUse`):
170
+ * whatever text arrived is a prefix or a fragment — a `No` on its way to `No, this must be blocked` — so it may
171
+ * carry a verdict only through a `<block>` tag, which reads a `<block>no` prefix as CC does; the bare-answer arm
172
+ * never sees it.
173
+ */
174
+ export declare function classifierReplyTagOnly(reply: AssistantMessage): boolean;
175
+ /**
176
+ * The text the DECIDER reads for its verdict: the reply's answer text, except that a tag-only reply
177
+ * ({@link classifierReplyTagOnly}) whose text carries NO tagged answer reads as EMPTY — a cut prefix must not
178
+ * reach the bare-answer arm as if it were the whole reply (adversarial passes 2–3: a connection cut after `No`,
179
+ * and a scratch `<block>` mention before a cut `No`, executed the gated tool). Judged by the reader itself with
180
+ * its bare arm closed, so "no tagged verdict" and "no verdict" cannot drift. Only the MISSING form is emptied:
181
+ * a tag-only reply that carries a CONFLICT keeps its text, so the decider records the conflict and the leg
182
+ * does not re-ask it (adversarial pass 4: emptying every parse failure had turned a cut conflict into a
183
+ * missing reply, and the re-ask into an allow). The decider then records `parse_error` / `missing` for the
184
+ * emptied text, the same word an empty reply gets.
185
+ */
186
+ export declare function classifierReplyVerdictText(reply: AssistantMessage): string;
187
+ /**
188
+ * Whether a first attempt is asked AGAIN on the THINK seat — ONE rule: the reply is MISSING a verdict, either
189
+ * empty at the cap ({@link classifierReplyEmptyAtCap}), cut or fragmentary without a tagged verdict
190
+ * ({@link classifierReplyTagOnly}), or text in which the reader finds no answer at all (`parse_error` of form
191
+ * `missing`: prose, a thinking-only reply). Not a CONFLICT (two `<block>` answers — it carries a `yes`, and a second attempt that answered `no`
192
+ * would erase it: the fail-closed `parse_error` it is stands) and not an empty reply with a non-cap stop
193
+ * (CC's refusal arm). CC 2.1.259 `Vrr` re-asks an unparseable reply up to `maxRetries` times; sema re-asks
194
+ * ONCE, on the stage-2 room, so a rung costs at most two calls. A failed reply ({@link classifierReplyFailure})
195
+ * is judged before this and never reaches it.
196
+ */
197
+ export declare function classifierReplyAsksAgain(reply: AssistantMessage): boolean;
198
+ /**
199
+ * A reply that is a LEG FAILURE rather than an answer — the brains RETURN a provider error as a message
200
+ * (`stopReason: "error"` with the provider's text on `errorMessage`: an HTTP 4xx/5xx, a quota refusal, an
201
+ * overloaded gateway) and a cancelled request as `"aborted"`; neither is a reply the reader may judge. Read as
202
+ * text they were `parse_error` — a card saying "the classifier's reply carried no verdict" about a model that
203
+ * never answered, a ladder record blaming `parse_error` instead of `error`. The leg THROWS what this returns
204
+ * and the decider maps the throw to `unavailable` (`error`), as it maps every leg failure; the sibling
205
+ * side-channel legs (compaction, consolidation) already read an error stop this way. The cap-cut form
206
+ * (`errorKind: "length_empty"`) is not a failure: it is the empty-at-cap reply the re-ask owns.
207
+ */
208
+ export declare function classifierReplyFailure(reply: AssistantMessage): Error | undefined;
72
209
  /**
73
210
  * The per-decision READ of a classification, as one seat: the `auto_mode.classified` trace frame (wall time,
74
- * verdict, cause) and the ladder's throttled `classifier.fallback` disclosure. Both are observers — a
75
- * throwing sink never reaches the gate (the decider swallows it), and neither changes a verdict.
211
+ * verdict, cause), the ladder's throttled `classifier.fallback` disclosure and the per-model
212
+ * `classifier.contract_broken` streak. All are observers — a throwing sink never reaches the gate (the decider
213
+ * swallows it), and none changes a verdict.
76
214
  *
77
215
  * The frame's `model` is the model the classification RAN ON, which is what the field has always meant (its
78
216
  * declaration reads "the resolved classifier role, or the main model it fell back to"): the route
@@ -1,20 +1,87 @@
1
1
  import { sameRouteIdentity } from "../../brain/route-adjudicator.js";
2
- import { thinkingOffExpressible } from "../../brain/reasoning.js";
3
- import { AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
4
- import { autoModeClassifierFallbackOf } from "../auto-mode.js";
2
+ import { thinkingOffSpelled } from "../../brain/reasoning.js";
3
+ import { AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER, AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS } from "../auto-mode-defaults.js";
4
+ import { autoModeClassifierFallbackOf, parseAutoModeResponse } from "../auto-mode.js";
5
+ import { DEFAULT_TIER_ORDER, resolveTaskModel, roleSpecNamesModel } from "../roles.js";
5
6
  import { emitTrace } from "../trace.js";
6
7
  import { deliverEngineNotice } from "../types.js";
7
8
  export function classifierLadderRungs(knob, classifier, main) {
8
9
  if (autoModeClassifierFallbackOf(knob) === "none")
9
10
  return [classifier];
10
- let sameRoute;
11
+ return sameRouteQuietly(classifier, main) ? [classifier] : [classifier, main];
12
+ }
13
+ function sameRouteQuietly(a, b) {
11
14
  try {
12
- sameRoute = sameRouteIdentity(classifier, main);
15
+ return sameRouteIdentity(a, b);
13
16
  }
14
17
  catch {
15
- sameRoute = false;
18
+ return false;
16
19
  }
17
- return sameRoute ? [classifier] : [classifier, main];
20
+ }
21
+ export const CLASSIFIER_ROLE_DEFAULT_REASONS = ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
22
+ export function classifierSeatModel(args) {
23
+ const { deps, main } = args;
24
+ if (roleSpecNamesModel(deps.roles?.classifier)) {
25
+ try {
26
+ return { model: resolveTaskModel({ modelRole: "classifier" }, deps).model };
27
+ }
28
+ catch {
29
+ return { model: main, defaulted: "role_unresolvable" };
30
+ }
31
+ }
32
+ const below = tierBelowMain(deps, main);
33
+ if (below !== undefined)
34
+ return below;
35
+ try {
36
+ const model = resolveTaskModel({ modelRole: "classifier" }, deps).model;
37
+ if (sameRouteQuietly(model, main))
38
+ return { model, defaulted: "main_only" };
39
+ return { model, defaulted: deps.tiers !== undefined && Object.keys(deps.tiers).length > 0 ? "tier_table" : "role_fallback" };
40
+ }
41
+ catch {
42
+ return { model: main, defaulted: "main_only" };
43
+ }
44
+ }
45
+ function tierBelowMain(deps, main) {
46
+ if (deps.tiers === undefined || Object.keys(deps.tiers).length === 0 || deps.models === undefined)
47
+ return undefined;
48
+ const bound = DEFAULT_TIER_ORDER.map((tier) => deps.models?.[tier]);
49
+ let mainTier = -1;
50
+ for (let i = bound.length - 1; i >= 0; i--) {
51
+ const m = bound[i];
52
+ if (m !== undefined && sameRouteQuietly(m, main)) {
53
+ mainTier = i;
54
+ break;
55
+ }
56
+ }
57
+ if (mainTier < 0)
58
+ return undefined;
59
+ for (let i = mainTier + 1; i < bound.length; i++) {
60
+ const m = bound[i];
61
+ if (m !== undefined && !sameRouteQuietly(m, main))
62
+ return { model: m, defaulted: "tier_below_main" };
63
+ }
64
+ return { model: main, defaulted: "main_only" };
65
+ }
66
+ export function announceClassifierRoleDefaulted(args) {
67
+ const { seat } = args;
68
+ if (seat.defaulted === undefined)
69
+ return;
70
+ const spelled = thinkingOffSpelled(seat.model);
71
+ deliverEngineNotice(args.onNotice, {
72
+ code: "classifier.role_defaulted",
73
+ message: `no classifier role names a model — "${seat.model.id}" classifies this run (${seat.defaulted}); ` +
74
+ (spelled ? "its wire can turn thinking off for the verdict. " : "its wire cannot turn thinking off, so each classification carries a thinking allowance. ") +
75
+ "Name a `classifier` role, or declare the model's thinking format (`compat.thinkingFormat`) so the seat can spell off.",
76
+ detail: {
77
+ model: seat.model.id,
78
+ reason: seat.defaulted,
79
+ thinkingOffSpelled: spelled,
80
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
81
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
82
+ },
83
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
84
+ });
18
85
  }
19
86
  function classifierFallbackAnnouncer(args) {
20
87
  const announced = new Set();
@@ -41,13 +108,94 @@ function classifierFallbackAnnouncer(args) {
41
108
  });
42
109
  };
43
110
  }
111
+ function classifierContractAnnouncer(args) {
112
+ const streak = new Map();
113
+ const announced = new Set();
114
+ const noVerdict = (rung, model) => {
115
+ const key = `${rung}:${model}`;
116
+ const n = (streak.get(key) ?? 0) + 1;
117
+ streak.set(key, n);
118
+ if (n < AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER || announced.has(key))
119
+ return;
120
+ announced.add(key);
121
+ deliverEngineNotice(args.onNotice, {
122
+ code: "classifier.contract_broken",
123
+ message: `the auto-mode classifier model "${model}" has answered ${n} consecutive classifications without a verdict (each asked twice) — ` +
124
+ "every gated call it judges is denied. Declare the model's thinking format (`compat.thinkingFormat`) so the seat can turn thinking off, " +
125
+ "or name a `classifier` role whose model follows the <block> contract.",
126
+ detail: {
127
+ model,
128
+ rung,
129
+ consecutive: n,
130
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
131
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
132
+ },
133
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
134
+ });
135
+ };
136
+ return (info) => {
137
+ const record = info.classifier;
138
+ if (record === undefined)
139
+ return;
140
+ if (record.fallback?.cause === "parse_error")
141
+ noVerdict(0, record.fallback.from);
142
+ const landed = record.fallback !== undefined ? 1 : 0;
143
+ if (info.verdict === "parse_error")
144
+ noVerdict(landed, record.modelUsed);
145
+ else if (info.verdict === "allow" || info.verdict === "block")
146
+ streak.delete(`${landed}:${record.modelUsed}`);
147
+ };
148
+ }
44
149
  export const CLASSIFIER_OFF_SEAT = { reasoning: "off", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS };
45
150
  export const CLASSIFIER_LOW_SEAT = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS };
151
+ export const CLASSIFIER_THINK_SEAT = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS };
46
152
  export function classifierRungSeat(model) {
47
- return thinkingOffExpressible(model) ? CLASSIFIER_OFF_SEAT : CLASSIFIER_LOW_SEAT;
153
+ return thinkingOffSpelled(model) ? CLASSIFIER_OFF_SEAT : CLASSIFIER_LOW_SEAT;
154
+ }
155
+ export const CLASSIFIER_ATTEMPT_SUFFIX = {
156
+ first: '\nErr on the side of blocking. Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No "Looking at..." or similar preamble.',
157
+ reask: "\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>. Think longer on ambiguous or borderline actions; keep reasoning brief for clear-cut ones.",
158
+ };
159
+ export function classifierReplyText(reply) {
160
+ return reply.content
161
+ .filter((c) => c.type === "text")
162
+ .map((c) => c.text)
163
+ .join("");
164
+ }
165
+ export function classifierReplyEmptyAtCap(reply) {
166
+ if (classifierReplyText(reply) !== "")
167
+ return false;
168
+ return reply.stopReason === "length" || (reply.stopReason === "error" && reply.errorKind === "length_empty");
169
+ }
170
+ export function classifierReplyTagOnly(reply) {
171
+ return reply.stopReason === "length" || reply.stopReason === "toolUse" || reply.partialFinalized === true || reply.content.some((c) => c.type === "toolCall");
172
+ }
173
+ export function classifierReplyVerdictText(reply) {
174
+ const text = classifierReplyText(reply);
175
+ if (!classifierReplyTagOnly(reply))
176
+ return text;
177
+ const tagged = parseAutoModeResponse(text, { bareAnswer: false });
178
+ return tagged.kind === "parse_error" && tagged.form === "missing" ? "" : text;
179
+ }
180
+ export function classifierReplyAsksAgain(reply) {
181
+ if (classifierReplyEmptyAtCap(reply))
182
+ return true;
183
+ const text = classifierReplyVerdictText(reply);
184
+ if (text === "")
185
+ return classifierReplyTagOnly(reply) && classifierReplyText(reply) !== "";
186
+ const verdict = parseAutoModeResponse(text);
187
+ return verdict.kind === "parse_error" && verdict.form === "missing";
188
+ }
189
+ export function classifierReplyFailure(reply) {
190
+ if (reply.stopReason === "aborted")
191
+ return new Error("the auto-mode classifier request was aborted");
192
+ if (reply.stopReason !== "error" || reply.errorKind === "length_empty")
193
+ return undefined;
194
+ return new Error(`the auto-mode classifier request failed: ${reply.errorMessage ?? "the model leg reported an error with no message"}`);
48
195
  }
49
196
  export function classifierRoundObserver(args) {
50
- const announce = classifierFallbackAnnouncer(args);
197
+ const announceFallback = classifierFallbackAnnouncer(args);
198
+ const countContract = classifierContractAnnouncer(args);
51
199
  return (info) => {
52
200
  emitTrace(args.tracer, () => ({
53
201
  kind: "auto_mode.classified",
@@ -60,6 +208,7 @@ export function classifierRoundObserver(args) {
60
208
  ...(info.cause !== undefined ? { cause: info.cause } : {}),
61
209
  ts: Date.now(),
62
210
  }));
63
- announce(info.classifier);
211
+ announceFallback(info.classifier);
212
+ countContract(info);
64
213
  };
65
214
  }
@@ -32,6 +32,7 @@ import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } fr
32
32
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
33
33
  import type { MaterializedMcp } from "../mcp.js";
34
34
  import type { MemoryEngine } from "../memory-engine/engine.js";
35
+ import type { ResolvedSkillListingBudget } from "../skill-listing-budget.js";
35
36
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
36
37
  import type { SessionPermissionRules } from "../session-policy-store.js";
37
38
  import type { RecoveredOrphan } from "../session-reconcile.js";
@@ -1024,6 +1025,7 @@ export interface Prepared {
1024
1025
  skillsListing?: {
1025
1026
  entries: ReadonlyArray<SkillListingEntry>;
1026
1027
  seedAnnounced: boolean;
1028
+ budget: ResolvedSkillListingBudget;
1027
1029
  };
1028
1030
  /** [c209-C] Q5 — the run loop's live mirror of the listing frames' announced NAME-SETS (updated at
1029
1031
  * every commit point: first-frame delivery, intact boundary survival, resume seed). Read by the
@@ -1,4 +1,4 @@
1
- import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
1
+ import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
2
2
  import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
3
3
  import { attestClassifierDenyCause } from "../tool-policy.js";
4
4
  import { deliverEngineNotice } from "../types.js";
@@ -92,7 +92,7 @@ export async function judgeInheritedClassifier(opts) {
92
92
  result: attestClassifierDenyCause({ action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier" }, "unavailable", req),
93
93
  };
94
94
  }
95
- const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
95
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
96
96
  const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
97
97
  const tracked = autoMode.denialTracking?.recordBlock();
98
98
  if (tracked?.limitReached !== true) {
@@ -102,6 +102,7 @@ export function prepareAskLane(input) {
102
102
  ...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking) : {}),
103
103
  ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
104
104
  ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
105
+ ...(decision.action === "ask" && decision.probeReadRootCandidate !== undefined ? { readRootCandidate: decision.probeReadRootCandidate } : {}),
105
106
  ...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
106
107
  }, onAsk, composedCallSignal(callSignal), lateAskSettlementObserver({ toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
107
108
  const waitMs = Math.max(0, now() - t0);
@@ -11,26 +11,14 @@ import { autoModeClassifierFallbackOf, autoModeRoundNotAsked, createAutoModeDeci
11
11
  import { checkpointStoreOfSeat } from "../checkpoint-seat.js";
12
12
  import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, resolveComplianceDenies } from "../compliance.js";
13
13
  import { createPresentPlanTool, createEnterPlanModeTool } from "../present-plan-tool.js";
14
- import { resolveTaskModel } from "../roles.js";
15
14
  import { brainToRuntime } from "../runtime.js";
16
15
  import { defaultTaskRegistry } from "../task-registry.js";
17
16
  import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
18
17
  const MOUNT_REMEDY = "author it as a plain-object ToolSpec (own `name` and `execute`), hand the defineTool product itself, or wrap a product through the supported wrapper form (a fresh object re-stamped with `stampDefineToolBrand` whose rebind re-wraps the rebound product)";
19
18
  import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "../runtime-caps.js";
20
19
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
21
- import { CLASSIFIER_LOW_SEAT, CLASSIFIER_OFF_SEAT, classifierLadderRungs, classifierRoundObserver, classifierRungSeat } from "./classifier-ladder.js";
20
+ import { CLASSIFIER_ATTEMPT_SUFFIX, CLASSIFIER_THINK_SEAT, announceClassifierRoleDefaulted, classifierLadderRungs, classifierReplyAsksAgain, classifierReplyFailure, classifierReplyVerdictText, classifierRoundObserver, classifierRungSeat, classifierSeatModel } from "./classifier-ladder.js";
22
21
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
23
- function classifierReplyText(reply) {
24
- return reply.content
25
- .filter((c) => c.type === "text")
26
- .map((c) => c.text)
27
- .join("");
28
- }
29
- function classifierReplyEmptyAtCap(reply) {
30
- if (classifierReplyText(reply) !== "")
31
- return false;
32
- return reply.stopReason === "length" || (reply.stopReason === "error" && reply.errorKind === "length_empty");
33
- }
34
22
  import { RosterBuilder, callerMountSource } from "../tool-roster.js";
35
23
  function assembleParentCaptureState(o, i, ctl, ancestors) {
36
24
  const build = (optedOut, indeterminate) => ({
@@ -209,16 +197,12 @@ export async function prepareCapsAndWorkflow(input) {
209
197
  const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps, runtimeCapsFaulted);
210
198
  if (autoModeIntent && deps.autoMode !== undefined && runtimeCaps?.autoMode !== false) {
211
199
  const am = deps.autoMode;
212
- let classifierModel;
213
- try {
214
- classifierModel = resolveTaskModel({ modelRole: "classifier" }, deps).model;
215
- }
216
- catch {
217
- classifierModel = model;
218
- }
200
+ const classifierSeat = classifierSeatModel({ deps, main: model });
201
+ let classifierModel = classifierSeat.model;
219
202
  if (await derivedRouteFallsBack({ seat: "auto-mode-classifier", derived: classifierModel, primary: model, brain: deps.brain, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, onNotice: deps.onNotice, sessionId, runId, onBrokenPairing: autoModeClassifierFallbackOf(am.classifierFallback) === "none" ? "refuse" : "fall_back" })) {
220
203
  classifierModel = model;
221
204
  }
205
+ announceClassifierRoleDefaulted({ onNotice: deps.onNotice, seat: { ...classifierSeat, model: classifierModel }, ...(sessionId !== undefined ? { sessionId } : {}), ...(runId !== undefined ? { runId } : {}) });
222
206
  const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
223
207
  const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
224
208
  const classifierRuntime = brainToRuntime(deps.brain);
@@ -245,17 +229,22 @@ export async function prepareCapsAndWorkflow(input) {
245
229
  }
246
230
  const userPrompt = await build.catch((e) => { throw autoModeRoundNotAsked(e); });
247
231
  const classifierAuth = await spec.getApiKeyAndHeaders?.(rungModel);
248
- const request = (seat) => classifierRuntime.completeSimple(rungModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
249
- signal,
250
- ...seat,
251
- ...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
252
- ...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
253
- });
254
- const rungSeat = classifierRungSeat(rungModel);
255
- let response = await request(rungSeat);
256
- if (rungSeat === CLASSIFIER_OFF_SEAT && classifierReplyEmptyAtCap(response))
257
- response = await request(CLASSIFIER_LOW_SEAT);
258
- return classifierReplyText(response);
232
+ const request = async (seat, suffix) => {
233
+ const response = await classifierRuntime.completeSimple(rungModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt + suffix, timestamp: Date.now() }] }, {
234
+ signal,
235
+ ...seat,
236
+ ...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
237
+ ...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
238
+ });
239
+ const failure = classifierReplyFailure(response);
240
+ if (failure !== undefined)
241
+ throw failure;
242
+ return response;
243
+ };
244
+ let response = await request(classifierRungSeat(rungModel), CLASSIFIER_ATTEMPT_SUFFIX.first);
245
+ if (classifierReplyAsksAgain(response) && signal?.aborted !== true)
246
+ response = await request(CLASSIFIER_THINK_SEAT, CLASSIFIER_ATTEMPT_SUFFIX.reask);
247
+ return classifierReplyVerdictText(response);
259
248
  },
260
249
  });
261
250
  if (am.persistArming === true)
@@ -25,6 +25,7 @@ import type { ThinkingLevel } from "../../internal/harness.js";
25
25
  import { type MicroCompactMachine } from "../context-edit.js";
26
26
  import { type LockedPreflight } from "../locked-config.js";
27
27
  import { type ResolvedRole } from "../roles.js";
28
+ import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
28
29
  import type { SessionStore } from "../session.js";
29
30
  import type { RunnerDeps, TaskLimits, TaskSpec } from "../types.js";
30
31
  import { type UsageWindow } from "../usage-window-store.js";
@@ -184,6 +185,11 @@ export interface PrepareConfigDoorsResult {
184
185
  * mutated (or getter-backed) after the door must not present an unscreened value to the mark
185
186
  * branch (the read-once posture). */
186
187
  memoryDelegationEvidence: "static-face" | "attested-only";
188
+ /** owned — the lowered skills-listing budget seats (`TaskSpec.skillListingBudgetFraction` /
189
+ * `skillListingMaxDescChars`): the renderer's two numbers, resolved once against the resolved main
190
+ * model's window. Read at the door so a bad value refuses BEFORE session acquisition, and carried so
191
+ * the listing phase and the boundary renderer measure against the same resolution. */
192
+ skillListingBudget: ResolvedSkillListingBudget;
187
193
  /** owned — the NORMALIZED memory-provenance mode: the deps seat read ONCE in
188
194
  * the synchronous pre-await stretch, screened, absent folded to the `"carry"` default.
189
195
  * The memory phase constructs every engine with THIS value — never a live deps re-read. */
@@ -7,6 +7,8 @@ import { isModelPriced, malformedPricingField, modelCostToPricing } from "../pri
7
7
  import { preflightLockedConfig } from "../locked-config.js";
8
8
  import { assertRetentionCapability } from "../retention.js";
9
9
  import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
10
+ import { resolveSkillListingBudget } from "../skill-listing-budget.js";
11
+ import { describeRejectedValue } from "../tool-search-setting.js";
10
12
  import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from "../tool-model-gate.js";
11
13
  import { resolveUsageWindows } from "../usage-window-store.js";
12
14
  import { deriveAskEffective, resolveAskSeamForm, resolveQuestionSeam } from "../wiring-manifest.js";
@@ -35,16 +37,6 @@ export function limitConfigError(code, message) {
35
37
  e.code = code;
36
38
  return e;
37
39
  }
38
- function describeRejectedValue(value) {
39
- try {
40
- const quoted = JSON.stringify(value);
41
- if (quoted !== undefined)
42
- return quoted;
43
- }
44
- catch {
45
- }
46
- return `a ${typeof value} value`;
47
- }
48
40
  export function resolveTaskLimits(limits) {
49
41
  if (limits === undefined)
50
42
  return undefined;
@@ -416,6 +408,11 @@ export function prepareConfigDoors(input) {
416
408
  if (gateDecision.survivors !== undefined) {
417
409
  spec = { ...spec, tools: gateDecision.survivors };
418
410
  }
411
+ const skillListingBudget = resolveSkillListingBudget({
412
+ fraction: spec.skillListingBudgetFraction,
413
+ maxDescChars: spec.skillListingMaxDescChars,
414
+ model,
415
+ });
419
416
  const modelGate = Object.freeze({
420
417
  removedByClass: gateDecision.removedByClass,
421
418
  unknownClasses: gateDecision.unknownClasses,
@@ -439,6 +436,7 @@ export function prepareConfigDoors(input) {
439
436
  fableMitigations,
440
437
  modelGate,
441
438
  memoryDelegationEvidence,
439
+ skillListingBudget,
442
440
  memoryProvenance,
443
441
  memoryCapturePolicy,
444
442
  usageWindows,
@@ -1,3 +1,4 @@
1
+ import type { ResolvedSkillListingBudget } from "../skill-listing-budget.js";
1
2
  import { type StoredSession } from "../session.js";
2
3
  import type { RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
3
4
  import type { ActiveSkillScope } from "./active-skill-scope.js";
@@ -8,7 +9,7 @@ export interface PrepareProjectContextInput {
8
9
  /** borrowed-readonly — the deployment seams this phase reads: `loadProjectMemory` (the snapshot),
9
10
  * `onError` (the operator lane for a throwing seam / seeding / oversize skill), `sharedMemoryStores`
10
11
  * (the shared-memory pair's switch: wired ⇒ mount). Receiver preserved (`deps.onError?.(…)`). */
11
- deps: Pick<RunnerDeps, "loadProjectMemory" | "onError" | "sharedMemoryStores">;
12
+ deps: Pick<RunnerDeps, "loadProjectMemory" | "onError" | "sharedMemoryStores" | "onNotice">;
12
13
  /** borrowed-readonly — the REBOUND spec (`spec′`). Read: `sessionId` (phase derivation + the
13
14
  * continuation projections), `skills` (the roster), `principal` (the shared-memory tools' trusted
14
15
  * identity). Never mutated. */
@@ -69,6 +70,9 @@ export interface PrepareProjectContextInput {
69
70
  /** borrowed-readonly — the durable-resume carrier; read for `!== undefined` (seedAnnounced) and
70
71
  * `seed.announcedListings.skills` (the removal-to-zero projection's gate). */
71
72
  resume: Pick<PrepareResume, "seed"> | undefined;
73
+ /** borrowed-readonly — the lowered listing-budget seats from the config door. The listing projection
74
+ * carries it to the boundary renderer, and the over-budget disclosure below is priced against it. */
75
+ skillListingBudget: ResolvedSkillListingBudget;
72
76
  }
73
77
  export interface PrepareProjectContextResult {
74
78
  /** owned — the memory block with the project layer appended at the tail (unchanged when the seam is
@@ -9,7 +9,7 @@ import { SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createSkillTool, normalizeSki
9
9
  import { substitutedSkillLength } from "./skill-base-dir.js";
10
10
  import { RosterBuilder } from "../tool-roster.js";
11
11
  export async function prepareProjectContext(input) {
12
- const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume } = input;
12
+ const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume, skillListingBudget } = input;
13
13
  const tools = roster.tools;
14
14
  let memoryBlock = memoryBlockFromEngine;
15
15
  let instructionSources;
@@ -142,11 +142,12 @@ export async function prepareProjectContext(input) {
142
142
  ...(declaredSkillRank.get(s.name) !== undefined ? { declaredRank: declaredSkillRank.get(s.name) } : {}),
143
143
  })),
144
144
  seedAnnounced: resume !== undefined || spec.sessionId !== undefined,
145
+ budget: skillListingBudget,
145
146
  }
146
147
  :
147
148
  (resume?.seed.announcedListings?.skills?.length ?? 0) > 0 || spec.sessionId !== undefined
148
149
  ?
149
- { entries: [], seedAnnounced: true }
150
+ { entries: [], seedAnnounced: true, budget: skillListingBudget }
150
151
  : undefined;
151
152
  return { memoryBlock, instructionSources, projectInstructionContent, skillSpecs, hasSkillManifest, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted };
152
153
  }