@sema-agent/core 7.21.1 → 7.23.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 (110) hide show
  1. package/CHANGELOG.md +290 -1
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +2 -1
  5. package/dist/core/ask-origin.d.ts +1 -0
  6. package/dist/core/ask-origin.js +3 -2
  7. package/dist/core/auto-compaction.d.ts +2 -2
  8. package/dist/core/auto-mode-rebuild.js +6 -2
  9. package/dist/core/auto-mode.d.ts +40 -18
  10. package/dist/core/auto-mode.js +63 -45
  11. package/dist/core/checkpoint-store.d.ts +9 -13
  12. package/dist/core/context-edit.js +1 -1
  13. package/dist/core/engine-notice.d.ts +62 -1
  14. package/dist/core/engine-notice.js +14 -1
  15. package/dist/core/gate-bounded-await.d.ts +13 -3
  16. package/dist/core/gate-fold.js +3 -3
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +6 -0
  19. package/dist/core/mcp.d.ts +65 -0
  20. package/dist/core/mcp.js +190 -41
  21. package/dist/core/memory-engine/consolidation.d.ts +5 -0
  22. package/dist/core/memory-engine/consolidation.js +7 -3
  23. package/dist/core/memory-engine/engine.d.ts +8 -0
  24. package/dist/core/memory-engine/engine.js +25 -19
  25. package/dist/core/memory-engine/layout.d.ts +47 -9
  26. package/dist/core/memory-engine/layout.js +157 -57
  27. package/dist/core/memory-engine/session-incarnation.d.ts +27 -0
  28. package/dist/core/memory-engine/session-incarnation.js +63 -0
  29. package/dist/core/parent-spec-seat.d.ts +15 -3
  30. package/dist/core/parent-spec-seat.js +5 -0
  31. package/dist/core/park-selfcheck.js +1 -1
  32. package/dist/core/path-identity.d.ts +11 -4
  33. package/dist/core/path-identity.js +5 -2
  34. package/dist/core/permission-rule-model.d.ts +1 -2
  35. package/dist/core/read-root-candidate.js +11 -1
  36. package/dist/core/retention.d.ts +8 -1
  37. package/dist/core/retention.js +2 -0
  38. package/dist/core/runner/assemble-result.d.ts +2 -7
  39. package/dist/core/runner/assemble-result.js +25 -8
  40. package/dist/core/runner/classifier-ladder.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +26 -15
  42. package/dist/core/runner/inherited-ask-grants.d.ts +2 -7
  43. package/dist/core/runner/inherited-ask-grants.js +1 -0
  44. package/dist/core/runner/mcp-redial.d.ts +109 -0
  45. package/dist/core/runner/mcp-redial.js +60 -0
  46. package/dist/core/runner/memory-consolidation-driver.d.ts +2 -0
  47. package/dist/core/runner/prepare-ask-lane.js +1 -0
  48. package/dist/core/runner/prepare-caps-and-workflow.js +9 -12
  49. package/dist/core/runner/prepare-context-lane.d.ts +43 -1
  50. package/dist/core/runner/prepare-context-lane.js +25 -4
  51. package/dist/core/runner/prepare-hands-readface.d.ts +3 -0
  52. package/dist/core/runner/prepare-hands-readface.js +13 -3
  53. package/dist/core/runner/prepare-memory.js +3 -3
  54. package/dist/core/runner/prepare-protocol-tools.d.ts +6 -1
  55. package/dist/core/runner/prepare-protocol-tools.js +20 -38
  56. package/dist/core/runner/prepare-task.js +4 -4
  57. package/dist/core/runner/prepare-turn-wiring.js +2 -2
  58. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  59. package/dist/core/runner/prepare-wiring-manifest.js +2 -1
  60. package/dist/core/runner/run-harness-handlers.js +4 -2
  61. package/dist/core/runner/run-identity-wiring.d.ts +3 -1
  62. package/dist/core/runner/run-identity-wiring.js +4 -1
  63. package/dist/core/runner/run-recovery-lanes.js +6 -3
  64. package/dist/core/runner/run-settle-and-teardown.d.ts +3 -1
  65. package/dist/core/runner/run-settle-and-teardown.js +6 -2
  66. package/dist/core/runner/run-telemetry-and-budget-seats.js +11 -1
  67. package/dist/core/runner/run-terminal-adoption.d.ts +4 -1
  68. package/dist/core/runner/run-terminal-adoption.js +2 -6
  69. package/dist/core/runner/runtask.js +8 -5
  70. package/dist/core/runner/stream-lifecycle-verbs.d.ts +5 -1
  71. package/dist/core/runner/stream-lifecycle-verbs.js +23 -2
  72. package/dist/core/runner/write-protection-station.d.ts +7 -25
  73. package/dist/core/runner/write-protection-station.js +18 -0
  74. package/dist/core/runner-deps.d.ts +16 -7
  75. package/dist/core/skill-listing-budget.d.ts +5 -4
  76. package/dist/core/task-registry.d.ts +0 -6
  77. package/dist/core/task-result.d.ts +31 -12
  78. package/dist/core/task-spec.d.ts +2 -1
  79. package/dist/core/task-stream.d.ts +26 -0
  80. package/dist/core/tool-spec.d.ts +5 -1
  81. package/dist/core/trace.d.ts +5 -0
  82. package/dist/core/types.d.ts +3 -3
  83. package/dist/core/types.js +1 -1
  84. package/dist/core/wiring-manifest.d.ts +23 -0
  85. package/dist/core/wiring-manifest.js +3 -0
  86. package/dist/core/write-protect.d.ts +64 -12
  87. package/dist/core/write-protect.js +88 -34
  88. package/dist/engine/execution-env/node-execution-env.d.ts +19 -2
  89. package/dist/engine/execution-env/node-execution-env.js +5 -1
  90. package/dist/index.d.ts +5 -4
  91. package/dist/index.js +2 -2
  92. package/dist/orchestration/workflow-governance.js +1 -1
  93. package/dist/orchestration/workflow-primitives.d.ts +2 -2
  94. package/dist/tools/fs/bash-program-position.d.ts +26 -5
  95. package/dist/tools/fs/bash-program-position.js +63 -16
  96. package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -17
  97. package/dist/tools/fs/bash-readonly-classifier.js +134 -55
  98. package/dist/tools/fs/fs-bash.js +43 -18
  99. package/dist/tools/fs/fs-read.js +2 -1
  100. package/dist/tools/fs/fs-shared.d.ts +33 -7
  101. package/dist/tools/fs/fs-shared.js +12 -1
  102. package/dist/tools/fs/index.d.ts +4 -2
  103. package/dist/tools/fs/read-deny.d.ts +25 -11
  104. package/dist/tools/fs/read-deny.js +5 -1
  105. package/dist/tools/fs/safety.d.ts +38 -4
  106. package/dist/tools/fs/safety.js +31 -3
  107. package/package.json +2 -2
  108. package/test/export-surface.snapshot.json +11 -1
  109. package/wire-changes/core@7.22.0.json +637 -0
  110. package/wire-changes/core@7.23.0.json +316 -0
@@ -4,7 +4,9 @@ import type { ToolCallRequest } from "./tool-policy.js";
4
4
  * sentence's parenthetical and the `auto_mode.classified` trace frame's `cause`, so the three spell one
5
5
  * word (the deny's structural face carries only the round's FORM, `GateDisposition.cause`). `error` = the model leg
6
6
  * threw or rejected (a route failure at classify time reads here too — the derived-route pre-flight fell
7
- * back BEFORE any decide, so there is no separate word for it); `timeout` = the round-trip cap.
7
+ * back BEFORE any decide, so there is no separate word for it; so does a question that never ASSEMBLED,
8
+ * whether its build threw or outran its cap — nothing was dispatched either way); `timeout` = a rung's
9
+ * DISPATCH cap.
8
10
  */
9
11
  export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"];
10
12
  export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number];
@@ -100,8 +102,9 @@ export interface AutoModeClassifierRound {
100
102
  };
101
103
  }
102
104
  /**
103
- * The classify hook's word for "this classification was NEVER PUT TO A MODEL" — the failure is in assembling
104
- * the QUESTION (the transcript window every rung shares), not in a rung's answer.
105
+ * The word for "this classification was NEVER PUT TO A MODEL" — the failure is in assembling the QUESTION (the
106
+ * transcript window every rung shares), not in a rung's answer. The decider raises it for its own
107
+ * {@link AutoModeDeciderOptions.assemble} seat; a hook that owns assembly throws an error carrying this code.
105
108
  *
106
109
  * WHY IT EXISTS. The ladder advances on "the rung produced no ruling", and a hook that rejects looks exactly
107
110
  * like that. But a classification whose prompt could not be built fails IDENTICALLY on every rung (the build is
@@ -113,9 +116,8 @@ export interface AutoModeClassifierRound {
113
116
  * Carried as a `code` on the thrown error rather than a class so any classify hook — including one this
114
117
  * package never constructed — can raise it without importing a constructor.
115
118
  *
116
- * WHAT IT DOES NOT COVER, stated: a build that OUTRUNS a rung's cap is still recorded as that rung's
117
- * `timeout`. The cap bounds the whole classify call, assembly included, so at that point nothing distinguishes
118
- * a slow transcript store from a slow model.
119
+ * A build that OUTRUNS its cap is the same fact and takes the same exit — see
120
+ * {@link AutoModeDeciderOptions.assemble}, the seat that bounds it.
119
121
  */
120
122
  export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
121
123
  /** Mint the marker above around whatever failed, keeping the original as `cause` for the host's own logs.
@@ -216,15 +218,28 @@ export interface AutoModeLadderRung {
216
218
  readonly index: number;
217
219
  readonly model: string;
218
220
  }
219
- /** The pluggable model leg: given the classify input and the RUNG the ladder is on, return that model's raw
220
- * text response. Deployment-injected; MAY throw / reject — the decider fail-closes. A rejection carrying
221
+ /** The pluggable model leg: given the classify input, the RUNG the ladder is on and whatever
222
+ * {@link AutoModeAssembleFn} built for this classification, return that model's raw text response.
223
+ * Deployment-injected; MAY throw / reject — the decider fail-closes. A rejection carrying
221
224
  * {@link AUTO_MODE_ROUND_NOT_ASKED_CODE} says the failure was in assembling the QUESTION rather than in this
222
225
  * rung's answer, and stops the walk instead of blaming the next rung. `rung` is `undefined`
223
- * for a decider wired with no ladder. Both trailing parameters are OPTIONAL, not `| undefined`: a caller
224
- * INVOKING it with one or two arguments must keep compiling. */
225
- export type AutoModeClassifyFn = (input: AutoModeClassifyInput, signal?: AbortSignal, rung?: AutoModeLadderRung) => Promise<string>;
226
- export interface AutoModeDeciderOptions {
227
- classify: AutoModeClassifyFn;
226
+ * for a decider wired with no ladder; `assembled` is `undefined` for one wired with no `assemble` seat (the
227
+ * leg then owns assembly, inside this rung's cap). All trailing parameters are OPTIONAL, not `| undefined`:
228
+ * a caller INVOKING it with one, two or three arguments must keep compiling. */
229
+ export type AutoModeClassifyFn<T = void> = (input: AutoModeClassifyInput, signal?: AbortSignal, rung?: AutoModeLadderRung, assembled?: T) => Promise<string>;
230
+ /** The pluggable ASSEMBLY leg: build this classification's question ONCE, before any rung is asked, on its own
231
+ * turn of {@link AutoModeDeciderOptions.timeoutMs}. The question is built from state that is not the model's
232
+ * (a transcript window, a store read), so its cost is not a rung's: a build that fails or outruns the cap is
233
+ * the {@link AUTO_MODE_ROUND_NOT_ASKED_CODE} fact (nothing dispatched, no rung blamed), and the rung cap
234
+ * bounds a DISPATCH alone — which is what makes that cap's `timeout` word true. MAY throw / reject; the
235
+ * decider fail-closes and does not start the walk. Its signal aborts when the run does AND at the cap.
236
+ * Omitted ⇒ `assembled` reaches `classify` as `undefined`. */
237
+ export type AutoModeAssembleFn<T> = (input: AutoModeClassifyInput, signal?: AbortSignal) => Promise<T>;
238
+ export interface AutoModeDeciderOptions<T = void> {
239
+ classify: AutoModeClassifyFn<T>;
240
+ /** Build the question ONCE per classification, before the walk, on its own turn of {@link timeoutMs} — see
241
+ * {@link AutoModeAssembleFn}. Omitted = the leg assembles inside `classify`, inside the rung cap. */
242
+ assemble?: AutoModeAssembleFn<T>;
228
243
  /**
229
244
  * The CANDIDATE LADDER this decider walks, in order — the models a classification may be asked on, NOT a
230
245
  * second way to say what `classify` says (that hook is the DISPATCH; this is the sequence it dispatches
@@ -234,12 +249,14 @@ export interface AutoModeDeciderOptions {
234
249
  *
235
250
  * Omitted = a one-rung anonymous ladder: the leg as wired, no attribution on the verdict. An EMPTY array,
236
251
  * or a rung that is not a non-empty string, is REFUSED loudly at construction (a deployment that computed
237
- * its ladder and got nothing has a bug, not a default). COST: {@link timeoutMs} bounds ONE rung, so the
238
- * worst case is the cap times the rungs actually walked.
252
+ * its ladder and got nothing has a bug, not a default). COST: {@link timeoutMs} bounds ONE rung's dispatch
253
+ * and, once, the {@link assemble} call — worst case, the cap times the rungs walked, plus one.
239
254
  */
240
255
  candidates?: readonly string[];
241
- /** Hard cap on one classification round-trip — one LADDER RUNG, not the whole classification (see
242
- * {@link candidates}). Default 15_000 ms: a permission gate must not stall the whole run on a slow classifier. */
256
+ /** Hard cap on one classification round-trip — one LADDER RUNG's DISPATCH, not the whole classification
257
+ * (see {@link candidates}) — and, separately, the one {@link assemble} call that precedes the walk (ONE
258
+ * number: a second knob would be a second answer to "how long may a classification make the gate wait").
259
+ * Default 15_000 ms: a permission gate must not stall the whole run on a slow classifier. */
243
260
  timeoutMs?: number;
244
261
  /**
245
262
  * Fired once per `decide` call with the verdict the gate is about to act on and the wall time it waited
@@ -259,6 +276,11 @@ export interface AutoModeClassified {
259
276
  cause?: Extract<AutoModeVerdict, {
260
277
  kind: "unavailable";
261
278
  }>["cause"];
279
+ /** Present exactly when no model was asked because the QUESTION did not assemble: `"timeout"` = the build
280
+ * outran its cap, `"failed"` = it threw (also what a classify hook owning its own assembly reports). ABSENT
281
+ * = a model was asked. The verdict is `unavailable` / `error` either way — nothing was dispatched — so this
282
+ * is what tells an operator "the build is slow" from "the provider is down". */
283
+ notAsked?: "failed" | "timeout";
262
284
  /** The classification's model attribution, present exactly when the decider was wired with a candidate ladder
263
285
  * ({@link AutoModeClassifierRound}) — the same object the verdict carries. */
264
286
  classifier?: AutoModeClassifierRound;
@@ -270,7 +292,7 @@ export interface AutoModeDecider {
270
292
  /** The per-run decider: timeout + fail-closed error mapping, nothing carried between rounds. One instance per
271
293
  * run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the next,
272
294
  * which asks the model again. */
273
- export declare function createAutoModeDecider(opts: AutoModeDeciderOptions): AutoModeDecider;
295
+ export declare function createAutoModeDecider<T = void>(opts: AutoModeDeciderOptions<T>): AutoModeDecider;
274
296
  /** The deployment's bounds for the denial limit (`RunnerDeps.autoMode.denialLimit`). Every member
275
297
  * optional; an omitted member takes its CC default. A present member with a bad value is REFUSED
276
298
  * loudly at construction (never clamped, never read as the default). */
@@ -164,7 +164,8 @@ export function createAutoModeDecider(opts) {
164
164
  const ladder = opts.candidates ?? [undefined];
165
165
  const decide = async (input, signal) => {
166
166
  const startedAt = performance.now();
167
- const verdict = await decideUnreported(input, signal);
167
+ const round = {};
168
+ const verdict = await decideUnreported(input, round, signal);
168
169
  if (opts.onClassified !== undefined) {
169
170
  try {
170
171
  opts.onClassified({
@@ -172,6 +173,7 @@ export function createAutoModeDecider(opts) {
172
173
  ms: Math.max(0, Math.round(performance.now() - startedAt)),
173
174
  verdict: verdict.kind,
174
175
  ...(verdict.kind === "unavailable" ? { cause: verdict.cause } : {}),
176
+ ...(round.notAsked !== undefined ? { notAsked: round.notAsked } : {}),
175
177
  ...(verdict.classifier !== undefined ? { classifier: verdict.classifier } : {}),
176
178
  });
177
179
  }
@@ -181,22 +183,22 @@ export function createAutoModeDecider(opts) {
181
183
  return verdict;
182
184
  };
183
185
  return { decide };
184
- async function decideUnreported(input, signal) {
186
+ async function decideUnreported(input, round, signal) {
185
187
  const causes = [];
186
- const round = { notAsked: false };
187
188
  let verdict = { kind: "unavailable", cause: "error" };
188
189
  let i = 0;
189
- for (; i < ladder.length; i++) {
190
+ const assembled = await assembleRound(input, round, signal);
191
+ for (; round.notAsked === undefined && i < ladder.length; i++) {
190
192
  const name = ladder[i];
191
- verdict = await askRung(input, signal, name !== undefined ? { index: i, model: name } : undefined, round);
193
+ verdict = await askRung(input, signal, name !== undefined ? { index: i, model: name } : undefined, round, assembled);
192
194
  const cause = rungDidNotRule(verdict);
193
195
  if (cause === undefined)
194
196
  break;
195
197
  causes.push(cause);
196
- if (round.notAsked || signal?.aborted)
198
+ if (round.notAsked !== undefined || signal?.aborted)
197
199
  break;
198
200
  }
199
- const landed = round.notAsked ? 0 : Math.min(i, ladder.length - 1);
201
+ const landed = round.notAsked !== undefined ? 0 : Math.min(i, ladder.length - 1);
200
202
  const requested = ladder[0];
201
203
  const used = ladder[landed];
202
204
  if (requested === undefined || used === undefined)
@@ -212,55 +214,71 @@ export function createAutoModeDecider(opts) {
212
214
  }),
213
215
  };
214
216
  }
215
- async function askRung(input, signal, rung, round) {
216
- let timer;
217
- const inner = new AbortController();
218
- const onOuterAbort = () => inner.abort();
219
- if (signal !== undefined) {
220
- if (signal.aborted)
221
- inner.abort();
222
- else
223
- signal.addEventListener("abort", onOuterAbort, { once: true });
217
+ async function assembleRound(input, round, signal) {
218
+ const assemble = opts.assemble;
219
+ if (assemble === undefined)
220
+ return undefined;
221
+ try {
222
+ return await withDeadline(timeoutMs, signal, (inner) => assemble(input, inner));
223
+ }
224
+ catch (e) {
225
+ round.notAsked = isAutoModeTimeout(e) ? "timeout" : "failed";
226
+ return undefined;
224
227
  }
228
+ }
229
+ async function askRung(input, signal, rung, round, assembled) {
225
230
  try {
226
- const raw = await new Promise((resolve, reject) => {
227
- let settled = false;
228
- timer = setTimeout(() => {
229
- if (!settled) {
230
- settled = true;
231
- inner.abort();
232
- reject(new AutoModeTimeout());
233
- }
234
- }, timeoutMs);
235
- opts.classify(input, inner.signal, rung).then((v) => {
236
- if (!settled) {
237
- settled = true;
238
- resolve(v);
239
- }
240
- }, (e) => {
241
- if (!settled) {
242
- settled = true;
243
- reject(e);
244
- }
245
- });
246
- });
247
- return parseAutoModeResponse(raw);
231
+ return parseAutoModeResponse(await withDeadline(timeoutMs, signal, (inner) => opts.classify(input, inner, rung, assembled)));
248
232
  }
249
233
  catch (e) {
250
234
  if (isAutoModeRoundNotAsked(e)) {
251
- round.notAsked = true;
235
+ round.notAsked = "failed";
252
236
  return { kind: "unavailable", cause: "error" };
253
237
  }
254
238
  if (signal?.aborted)
255
239
  return { kind: "unavailable", cause: "error" };
256
240
  return { kind: "unavailable", cause: isAutoModeTimeout(e) ? "timeout" : "error" };
257
241
  }
258
- finally {
259
- if (timer !== undefined)
260
- clearTimeout(timer);
261
- if (signal !== undefined)
262
- signal.removeEventListener("abort", onOuterAbort);
263
- }
242
+ }
243
+ }
244
+ async function withDeadline(capMs, signal, call) {
245
+ let timer;
246
+ const inner = new AbortController();
247
+ const onOuterAbort = () => inner.abort();
248
+ if (signal !== undefined) {
249
+ if (signal.aborted)
250
+ inner.abort();
251
+ else
252
+ signal.addEventListener("abort", onOuterAbort, { once: true });
253
+ }
254
+ try {
255
+ return await new Promise((resolve, reject) => {
256
+ let settled = false;
257
+ timer = setTimeout(() => {
258
+ if (!settled) {
259
+ settled = true;
260
+ inner.abort();
261
+ reject(new AutoModeTimeout());
262
+ }
263
+ }, capMs);
264
+ call(inner.signal).then((v) => {
265
+ if (!settled) {
266
+ settled = true;
267
+ resolve(v);
268
+ }
269
+ }, (e) => {
270
+ if (!settled) {
271
+ settled = true;
272
+ reject(e);
273
+ }
274
+ });
275
+ });
276
+ }
277
+ finally {
278
+ if (timer !== undefined)
279
+ clearTimeout(timer);
280
+ if (signal !== undefined)
281
+ signal.removeEventListener("abort", onOuterAbort);
264
282
  }
265
283
  }
266
284
  class AutoModeTimeout extends Error {
@@ -102,11 +102,11 @@ export type PlatformLimitReason = Extract<ResourceLimitReason, "env_lifetime" |
102
102
  export interface SafetyAxis {
103
103
  /** design/70: the tool is egress-marked (`ToolSpec.egress` — an external write: push, open PR, send). */
104
104
  egress?: boolean;
105
- /** design/77 §4: this call carries an irreversibility MANDATE — the tool's own `always`/`maybe` tier, the
106
- * operator's `shellGate:"always"`, the probe's structural verdict (mandated / unanswered), or the shell
107
- * gate's own tighten under its `"maybe"` tier; a policy ask the doctrine's probe neither tightened nor
108
- * mandated carries no bit (its park is the budgetable kind). */
105
+ /** design/77 §4: an irreversibility MANDATE **of the TOOL's own** (its `always`/`maybe` tier); a SHELL SEAT raise is NOT this axis, see {@link shell}. */
109
106
  irreversible?: boolean;
107
+ /** The engine's SHELL SEAT raised this ask. Its question is about TEXT — not about undoing an act — so
108
+ * the two axes are mutually exclusive. */
109
+ shell?: boolean;
110
110
  }
111
111
  /**
112
112
  * design/80 §D-E: a DETERMINISTIC, REDACTED risk summary attached AT MINT to an escalation
@@ -948,10 +948,10 @@ export interface CheckpointState {
948
948
  /**
949
949
  * design/199 — the READ-face containment state at suspend (v9,
950
950
  * {@link FACE_CHECKPOINT_VERSION}): the RESOLVED face plus the NORMALIZED deny-set ADDITIONS in
951
- * force (deployment ∪ task layers; the built-in table is engine code, never frozen here — a newer
952
- * engine's built-ins apply on resume as everywhere else). Written ONLY when the posture is
953
- * non-default (face "open", or additions present) — an absent section IS the default posture and
954
- * resumes as roots + built-ins on any worker. Resume folds STRICTER-WINS: face = roots unless the
951
+ * force (deployment ∪ task layers; the built-in table is engine code under the RESUMING deployment's
952
+ * own tier selection — empty unless it made one — never frozen here). Written ONLY when the posture
953
+ * is non-default (face "open", or additions present) — an absent section IS the default posture and
954
+ * resumes as roots plus whatever built-in tiers the resuming worker selected. Resume folds STRICTER-WINS: face = roots unless the
955
955
  * section says open AND the live resolution says open; deny additions = frozen ∪ live (add-only,
956
956
  * monotone — a resume can never widen what the cut leg could read).
957
957
  *
@@ -1677,11 +1677,7 @@ export interface CheckpointRow {
1677
1677
  humanReview?: {
1678
1678
  count: number;
1679
1679
  totalWaitMs: number;
1680
- gates: Array<{
1681
- kind: string;
1682
- waitMs: number;
1683
- decision?: string;
1684
- }>;
1680
+ gates: import("./task-result.js").HumanReviewGateRow[];
1685
1681
  };
1686
1682
  /**
1687
1683
  * design/80 D-1 (persist-winner): the winning resume binding, recorded by {@link CheckpointStore.resolve}
@@ -238,7 +238,7 @@ export function planRejectionClears(messages, opts) {
238
238
  const cleared = clearable.map(({ m, at }) => {
239
239
  const source = at.occurrence !== undefined && isToolResult(at.occurrence) ? at.occurrence : m;
240
240
  const clearedMsg = clearOneResult(source, opts.offload);
241
- return { key: at.key, groupCount: at.groupCount, marker: clearedMsg.content[0]?.text ?? CC_CLEARED_MARKER, fp: at.fp };
241
+ return { key: at.key, groupCount: at.groupCount, marker: clearedMsg.content[0]?.text ?? CC_CLEARED_MARKER, fp: at.fp, toolCallId: m.toolCallId };
242
242
  });
243
243
  return { cleared, tokensSavedEstimate };
244
244
  }
@@ -34,6 +34,19 @@ export interface EngineNotice {
34
34
  * prepared leg, never per judgment and never re-fired for a mid-run roster delta; absent ⇒ the judge
35
35
  * has its target view, or no covered write tool was mounted AT PREPARE, or there is no judge —
36
36
  * `wiringManifest.writeProtection` reports the judge's reading either way.
37
+ * - `"config.write_protection_unresolved"` (#869) — this leg's write-protection judge HAS a target view
38
+ * and the environment behind it could not answer: it refused a spelling, could not follow a link, or
39
+ * threw. That view then does not vote and the write is judged by its SPELLING alone (the judge's
40
+ * absence law), so the disclosure is the only place a deployment learns that its write floor is
41
+ * running on half a judgment. Announce, never narrow — no verdict moves because of this notice; the
42
+ * verdict already moved when the environment failed to answer. Audience `operator`.
43
+ * `detail: { reason, cause, sessionId, runId }` — `reason` is `"unresolved-symlink"` (a link whose
44
+ * real target is unknowable) or `"unresolvable-target"` (everything else: a refused spelling, an
45
+ * errored or throwing probe), and `cause` is the environment's own first sentence for that reason.
46
+ * ONCE PER LEG PER `reason` — the fact is about the ENVIRONMENT, not about a write, so it carries no
47
+ * call identity and a second failing write of the same shape adds nothing. Absent ⇒ every judgment
48
+ * this leg made had the views it was compiled for, OR the judge never finished at all (a judgment the
49
+ * gate's bounded wait gave up on is reported on the hook-error lane instead, `onHookError`).
37
50
  * - `"config.tool_card_undeclared"` (design/388 L4) — a tool minted a `details.type` card its definition
38
51
  * (or its caller declaration's `renderHints.resultCards`) never declared; the card was dropped from
39
52
  * `tool_end.structured`; once per leg per type; `detail: { type, sessionId, runId }`.
@@ -109,6 +122,18 @@ export interface EngineNotice {
109
122
  * its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
110
123
  * no engine-minted code is audience-defaulted any more). The refusal itself
111
124
  * (`mcp.server_revoked`) is a tool RESULT code, not a notice.
125
+ * - `"mcp.server_redialed"` — a host asked this run to RE-DIAL one MCP server (the run
126
+ * handle's `reconnectMcpServer` verb) and this is what came of it. `detail: { server, outcome,
127
+ * added, removed, reason?, sessionId, runId }` — `outcome` is the result's own three-word answer
128
+ * (`reconnected` / `refused` / `not_declared`) and `added`/`removed` are COUNTS of namespaced
129
+ * tool names, never the names themselves (a server's tool names are server-authored text and
130
+ * this is an operator line, not a disclosure channel). ONE code rather than a success code and a
131
+ * failure code: the outcome is data about one event, not two kinds of event, and both would
132
+ * carry the same presentation tier — a consumer alerting on failures reads one field.
133
+ * `"operator"` audience: re-dialing is deployment plumbing and a failed one leaves the server
134
+ * DOWN, which only the operator can act on. Dedup unit: once per ATTEMPT — never folded per run,
135
+ * because two attempts on one server are two different answers and folding the second would hide
136
+ * the one that says the server is still down.
112
137
  * - `"mcp.injection_dropped"` (L-167) — an entry of the user's own request-lane MCP injection
113
138
  * was NOT mounted for the leg. The HOST owns the drop sites and supplies the facts; CORE composes
114
139
  * the notice ({@link import("./mcp-injection-drop.js").mcpInjectionDroppedNotice}, the one mint).
@@ -707,6 +732,28 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
707
732
  * one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
708
733
  * once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
709
734
  */
735
+ /** What a settled leg still owes a person BEFORE the compensation lane has had its say: the facts the
736
+ * terminal-adoption lane holds, carried to the settle lane that closes the account
737
+ * ({@link settleOwedUserInput}). */
738
+ export interface OwedUserInputAccount {
739
+ /** The harness's at-agent_end snapshot, held because the tail still had something to say about it. */
740
+ readonly held: {
741
+ steer: number;
742
+ followUp: number;
743
+ } | undefined;
744
+ /** What a durable park's migration carried onto the new checkpoint, by family. */
745
+ readonly migrated: {
746
+ steer: number;
747
+ followUp: number;
748
+ };
749
+ /** The resumed leg's undelivered continuation frames, and how many of them the migration carried. */
750
+ readonly continuation: {
751
+ frames: ReadonlyArray<{
752
+ seq?: number;
753
+ }>;
754
+ carried: number;
755
+ };
756
+ }
710
757
  /**
711
758
  * What this leg still OWES a person, one count per family — the input to {@link undrainedUserInputNotices}.
712
759
  * TWO lanes accept caller input and either can be holding some when a run ends: the harness's live queue
@@ -727,11 +774,25 @@ export declare function owedUserInputCounts(held: {
727
774
  seq?: number;
728
775
  }>;
729
776
  carried: number;
730
- rowMayReturn: boolean;
777
+ rowReturned: boolean;
731
778
  }): {
732
779
  steer: number;
733
780
  followUp: number;
734
781
  };
782
+ /** Close the account — the ONE settlement point, called AFTER the reopen compensation with what that
783
+ * compensation actually did. Ordering is the whole rule: the counts are arithmetic, and the only fact they
784
+ * cannot be computed without is one the lane before this one does not have yet. EXACTLY ONCE per account,
785
+ * which is what lets the teardown call it again as a backstop for a settle lane that threw on its way here
786
+ * (an account nobody closes is the silence this seat exists to end; a second row would be the same loss
787
+ * counted twice). Keyed on the account OBJECT — one per run, minted by the lane that assembles it. NEVER
788
+ * THROWS: a station calls it from a teardown path where the run is already failing, and a disclosure that
789
+ * takes the cleanup down with it is worse than the silence it was fixing (the absent-sink arm of
790
+ * {@link deliverEngineNotice} ends at a bare `console.warn`, which a host-replaced console can throw from). */
791
+ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowReturned: boolean, ids: {
792
+ taskId?: string;
793
+ sessionId?: string;
794
+ runId?: string;
795
+ }, onNotice: ((notice: EngineNotice) => void) | undefined): void;
735
796
  export declare function undrainedUserInputNotices(counts: {
736
797
  steer: number;
737
798
  followUp: number;
@@ -32,12 +32,25 @@ export function deliverEngineNotice(onNotice, notice) {
32
32
  console.warn(notice.message);
33
33
  }
34
34
  export function owedUserInputCounts(held, migrated, continuation) {
35
- const owedContinuation = (continuation.rowMayReturn ? continuation.frames.filter((f) => f.seq === undefined).length : continuation.frames.length) - continuation.carried;
35
+ const owedContinuation = (continuation.rowReturned ? continuation.frames.filter((f) => f.seq === undefined).length : continuation.frames.length) - continuation.carried;
36
36
  return {
37
37
  steer: Math.max(0, (held?.steer ?? 0) - migrated.steer) + Math.max(0, owedContinuation),
38
38
  followUp: Math.max(0, (held?.followUp ?? 0) - migrated.followUp),
39
39
  };
40
40
  }
41
+ export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
42
+ if (SETTLED_OWED_ACCOUNTS.has(account))
43
+ return;
44
+ SETTLED_OWED_ACCOUNTS.add(account);
45
+ try {
46
+ const owed = owedUserInputCounts(account.held, account.migrated, { ...account.continuation, rowReturned });
47
+ for (const notice of undrainedUserInputNotices(owed, ids.taskId, ids.sessionId, ids.runId))
48
+ deliverEngineNotice(onNotice, notice);
49
+ }
50
+ catch {
51
+ }
52
+ }
53
+ const SETTLED_OWED_ACCOUNTS = new WeakSet();
41
54
  export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
42
55
  const tid = taskId !== undefined ? { taskId } : {};
43
56
  const sid = sessionId !== undefined ? { sessionId } : {};
@@ -36,8 +36,18 @@ export declare function createGateDeadline(input: {
36
36
  approvalTimeoutMs?: number;
37
37
  }, notifier: Parameters<typeof traceHookCrash>[2]): () => number;
38
38
  /**
39
- * The write-protection judge's wait, at both of its call sites. An elapsed or aborted judgment is the
40
- * judge's own `unresolvable` verdict — which the fold denies and the approved-edit arm denies — because a
41
- * wait that did not finish IS a target nobody could resolve, and an unjudged target is never vouched.
39
+ * The write-protection judge's wait, at both of its call sites — and the ONE minter of the `unresolvable`
40
+ * verdict. An elapsed or aborted judgment is not "a target the environment could not resolve" (the judge
41
+ * answers that one itself, by dropping the view that could not contribute and letting the lexical half
42
+ * stand); it is NO judgment at all — not even the lexical half's answer came back — and there is nothing
43
+ * for the fold or the approved-edit arm to act on but their own fail-closed arm. A wait that produced
44
+ * nothing knows nothing about what it was waiting for, which is why `unresolvedSymlink` is `false` here
45
+ * and not a guess.
46
+ *
47
+ * KNOWN, AND NARROWER THAN IT LOOKS: the lexical half is computed synchronously at the top of `judge`, so
48
+ * a spelling HIT is already on a settled promise before this wait begins and cannot elapse; what this arm
49
+ * refuses is a spelling MISS whose environment half never came back. Closing even that would mean handing
50
+ * the deadline INTO the judge so the missing half could be dropped there too — a seat change on a public
51
+ * type, deliberately not taken here.
42
52
  */
43
53
  export declare function awaitWriteProtectionJudgment(judged: Promise<WriteProtectionVerdict>, ms: number, signal?: AbortSignal, onFailure?: (err: unknown) => void): Promise<WriteProtectionVerdict>;
@@ -110,8 +110,8 @@ export async function runGateFold(pass) {
110
110
  };
111
111
  pass.tightenedBy = "safety";
112
112
  }
113
- const writeProtectionStandingAsk = pass.decision.action === "ask";
114
- if ((pass.decision.action === "allow" || writeProtectionStandingAsk) && input.writeProtectionCheck !== undefined) {
113
+ const writeProtectionStanding = pass.decision.action;
114
+ if ((writeProtectionStanding === "allow" || writeProtectionStanding === "ask") && input.writeProtectionCheck !== undefined) {
115
115
  const judged = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal);
116
116
  const verdict = judged === null
117
117
  ? undefined
@@ -126,7 +126,7 @@ export async function runGateFold(pass) {
126
126
  if (verdict !== undefined && writeProtectionUnresolved(verdict))
127
127
  pass.writeProtectionUnresolved = true;
128
128
  const decision = verdict === undefined ? undefined : writeProtectionTightenDecision(toolName, verdict);
129
- if (decision !== undefined && !writeProtectionStandingAsk) {
129
+ if (decision !== undefined && writeProtectionStanding === "allow") {
130
130
  pass.decision = decision;
131
131
  if (decision.action === "ask")
132
132
  pass.tightenedBy = "safety";
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
119
119
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
120
120
  * so the catalog is not written a second time here and the two cannot drift.
121
121
  */
122
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
122
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
123
123
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
124
124
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
125
125
  * sink is a supported shape, and narrowing that field would break it. */
@@ -18,6 +18,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
18
18
  "memory.layout_unowned_root_content",
19
19
  "memory.tail",
20
20
  "memory.pollution_mark_failed",
21
+ "memory.session_incarnation_mismatch",
22
+ "memory.session_incarnation_unreadable",
21
23
  "memory.session_polluted",
22
24
  "memory.harvest_quarantined",
23
25
  "memory.delegation_static_mark_waived",
@@ -112,6 +114,7 @@ export const ENGINE_NOTICE_CODES = [
112
114
  "config.artifact_host_invalid",
113
115
  "config.task_root_not_canonical",
114
116
  "config.write_protection_target_view_absent",
117
+ "config.write_protection_unresolved",
115
118
  "config.execution_env_capability_invalid",
116
119
  "config.tool_card_undeclared",
117
120
  "config.tool_face_undeclared",
@@ -132,6 +135,7 @@ export const ENGINE_NOTICE_CODES = [
132
135
  "delegation.ask_unresolvable",
133
136
  "mcp.injection_dropped",
134
137
  "mcp.revocation_probe_failed",
138
+ "mcp.server_redialed",
135
139
  "workflow.governance_key_stripped",
136
140
  "workflow.agent_option_ignored",
137
141
  "memory.session_polluted",
@@ -201,6 +205,7 @@ const NOTICE_AUDIENCE_TABLE = {
201
205
  "config.artifact_host_invalid": "operator",
202
206
  "config.task_root_not_canonical": "operator",
203
207
  "config.write_protection_target_view_absent": "operator",
208
+ "config.write_protection_unresolved": "operator",
204
209
  "config.execution_env_capability_invalid": "operator",
205
210
  "config.tool_card_undeclared": "operator",
206
211
  "config.tool_face_undeclared": "operator",
@@ -214,6 +219,7 @@ const NOTICE_AUDIENCE_TABLE = {
214
219
  "checkpoint.execution_outcome_unrecorded": "operator",
215
220
  "mcp.injection_dropped": "user",
216
221
  "mcp.revocation_probe_failed": "operator",
222
+ "mcp.server_redialed": "operator",
217
223
  "workflow.governance_key_stripped": "operator",
218
224
  "workflow.agent_option_ignored": "operator",
219
225
  "memory.content_class_declared": "operator",
@@ -190,6 +190,25 @@ export interface MaterializedMcp {
190
190
  * misroute).
191
191
  */
192
192
  refresh: (server?: string) => Promise<McpRefreshResult[]>;
193
+ /**
194
+ * RE-DIAL one named server — the action {@link refresh} deliberately is not. It closes
195
+ * whatever handle the server still has and dials it again through the SAME `connectServer` the
196
+ * materialization used, so the schema gate, the namespacing, the axis derivation and the metadata
197
+ * cache seeding cannot drift between a first connect and a re-dial: one function, not one
198
+ * convention.
199
+ *
200
+ * ONE SERVER, NAMED — CC 2.1.259's control-channel request carries a REQUIRED `serverName` and has
201
+ * no all-servers form, and an all-servers arm would close HEALTHY connections to re-dial them (a
202
+ * stdio server owning a device or a port is exactly the one that cannot be re-dialed while its
203
+ * predecessor still holds the device). CLOSE-THEN-DIAL, so a re-dial that FAILS leaves the server
204
+ * DISCONNECTED until another re-dial or the next leg's materialize, and calls in flight on the
205
+ * replaced connection fail with it — inherent to re-dialing, and CC's behavior too.
206
+ *
207
+ * NOT A MODEL FACE (CC has no such tool either): a verb that can leave a connection down is a host
208
+ * act, and its consumer entrance is the run handle's `reconnectMcpServer`. LEG-LOCAL like
209
+ * {@link refresh}: the result rides no checkpoint, and a durable resume takes a fresh listing.
210
+ */
211
+ reconnect: (server: string) => Promise<McpReconnectResult>;
193
212
  dispose: () => Promise<void>;
194
213
  }
195
214
  /** One per-server entry of {@link MaterializedMcp.refresh}. `added`/`removed` are namespaced
@@ -244,6 +263,52 @@ export interface McpRefreshResult {
244
263
  */
245
264
  listingIncomplete?: McpListingIncomplete;
246
265
  }
266
+ /**
267
+ * The result of {@link MaterializedMcp.reconnect} — ONE server, because the face takes one.
268
+ *
269
+ * `outcome` is the whole top level, three words, so a consumer mapping it onto a three-state wire
270
+ * answer needs no table: `reconnected` is the only success, and everything that did not reconnect is
271
+ * either `refused` (the engine declined, or the dial/first listing failed — `reason` says which) or
272
+ * `not_declared` (no server of that name in this leg's spec). Sub-reasons ride `reason`; a fourth
273
+ * word would be a new state for every downstream to learn.
274
+ *
275
+ * @contract mcp.redial.roster_assertion — a re-dial result carries `tools` IF AND ONLY IF it asserts a
276
+ * roster for its `prefix` domain, and that one rule is what every consumer of it reads: the fresh intake
277
+ * on `reconnected`, the EMPTY roster on a dial that failed after the old connection was already closed
278
+ * (the server is down, so its tools are withdrawn rather than left mounted and unusable), and NOTHING at
279
+ * all on an arm that never touched the connection — an unknown name, a revoked server, a re-dial already
280
+ * in flight, a disposed materialization — whose mounted roster is left exactly as it was.
281
+ */
282
+ export interface McpReconnectResult {
283
+ server: string;
284
+ /** The consumer's splice domain, same meaning and same mint as {@link McpRefreshResult.prefix}. */
285
+ prefix: string;
286
+ outcome: "reconnected" | "refused" | "not_declared";
287
+ /** Why, on every non-`reconnected` outcome (neutralized + bounded when it embeds remote text).
288
+ * Never the only machine-readable fact: see {@link status}. */
289
+ reason?: string;
290
+ toolCount: number;
291
+ /** Namespaced names this re-dial added / took away relative to what the server had mounted a moment
292
+ * ago — a first successful dial adds its whole roster, a failed dial removes it. */
293
+ added: string[];
294
+ removed: string[];
295
+ /** See the type docstring: present iff this result asserts a roster for `prefix`. */
296
+ tools?: AgentTool[];
297
+ axes?: McpToolAxis[];
298
+ dropped?: McpDroppedTool[];
299
+ /** The OPERATOR lines this re-dial produced (an incomplete listing walk, a kept-but-defective tool
300
+ * schema, a spec naming tools a resources-only server cannot serve). Also appended to
301
+ * {@link MaterializedMcp.warnings}, which the runner drains ONCE at prepare — so the copy here is
302
+ * what the run handle's verb forwards to `onError(phase:"mcp")`. One production, two drains. */
303
+ warnings?: string[];
304
+ /** The NEW connect-time status record (also swapped into {@link MaterializedMcp.statuses}), present
305
+ * whenever the dial was attempted: a consumer keys its next action off `errorCode` / `httpStatus`
306
+ * (a 401 is a re-authorization door, a `spawn_failed` a missing binary), not off {@link reason}. */
307
+ status?: McpServerStatus;
308
+ /** The re-dial's own `tools/list` walk did not finish — {@link McpRefreshResult.listingIncomplete}
309
+ * minus the retention arm: a re-dial has no previous listing on this connection to retain from. */
310
+ listingIncomplete?: McpListingIncomplete;
311
+ }
247
312
  /** design/99 §E9 — projected per-server MCP status (see {@link MaterializedMcp.statuses}). NOTE: `serverInfo`
248
313
  * and `error` are SERVER-controlled strings (verbatim from the remote) — UNTRUSTED; a consumer rendering them
249
314
  * into a TUI/log must treat them as such (a misbehaving server could embed ANSI/break-out sequences). `toolNames`