@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
@@ -3,11 +3,18 @@ import { type PathFamily } from "../tools/fs/safety.js";
3
3
  /**
4
4
  * Every spelling the leaf symlink chain of `path` passes through, or `[]`.
5
5
  *
6
- * `[]` has ONE reading — nothing to judge beyond the spelling and the terminal — reached three ways that are
7
- * deliberately the same word: no `canonicalChain` declared, `ok: false`, or an empty chain. An unanswerable
8
- * chain is never a refusal: the terminal's own canonicalization already refuses what it cannot resolve, and a
6
+ * `[]` has ONE reading for the VERDICT — nothing to judge beyond the spelling and the terminal — reached four
7
+ * ways that are deliberately the same word: no `canonicalChain` declared, a refused namespace spelling, an
8
+ * empty chain, and the environment answering `ok: false`. An unanswerable chain is never a refusal: this
9
+ * reader's callers are path guards whose own absence law says a view that cannot answer does not vote, and a
9
10
  * second refusal law over one target would be a second rule. The spelling is anchored as the canonicalizer
10
11
  * anchors it (`baseCwd`). A refused namespace spelling and a win-form UNC spelling are not walked (the reference
11
12
  * CLI's variant set stops at a network spelling); the share is recognized by the throat's own `isUncSpelling`.
13
+ *
14
+ * `onUnanswerable` separates ONE of those four from the rest for a caller that DISCLOSES its absent views: the
15
+ * environment was ASKED and could not answer (`ok: false`), which is a fact about that environment. The other
16
+ * three are not failures — nothing was asked, or the answer was genuinely "no hops" — so they stay silent. The
17
+ * verdict is `[]` either way; this sink changes nothing about what is judged, only what is said out loud. A
18
+ * throwing sink is the caller's bug and is not caught here.
12
19
  */
13
- export declare function symlinkChainHops(env: ExecutionEnv, path: string, signal: AbortSignal | undefined, family: PathFamily, baseCwd?: string): Promise<readonly string[]>;
20
+ export declare function symlinkChainHops(env: ExecutionEnv, path: string, signal: AbortSignal | undefined, family: PathFamily, baseCwd?: string, onUnanswerable?: (cause: string) => void): Promise<readonly string[]>;
@@ -1,5 +1,5 @@
1
1
  import { isAbsoluteForFamily, isUncSpelling, joinForFamily, win32NamespaceScreen } from "../tools/fs/safety.js";
2
- export async function symlinkChainHops(env, path, signal, family, baseCwd) {
2
+ export async function symlinkChainHops(env, path, signal, family, baseCwd, onUnanswerable) {
3
3
  if (typeof env.canonicalChain !== "function")
4
4
  return [];
5
5
  const screened = win32NamespaceScreen(path, family);
@@ -10,5 +10,8 @@ export async function symlinkChainHops(env, path, signal, family, baseCwd) {
10
10
  return [];
11
11
  const target = baseCwd !== undefined && baseCwd.length > 0 && !isAbsoluteForFamily(family, spelled) ? joinForFamily(family, baseCwd, spelled) : spelled;
12
12
  const chain = await env.canonicalChain(target, signal);
13
- return chain.ok ? chain.value.hops : [];
13
+ if (chain.ok)
14
+ return chain.value.hops;
15
+ onUnanswerable?.(chain.error.message);
16
+ return [];
14
17
  }
@@ -1195,8 +1195,7 @@ export declare function suggestRulesForCommand(command: string, ctx?: {
1195
1195
  * design/382 §2.4-6 — the deployment's sensitive-read negative control, SAME-SOURCE with its
1196
1196
  * compiled read-deny judge (`ReadDenyMatcher.matchPath !== null` is the canonical wiring): the
1197
1197
  * engine never PROPOSES a directory authorization for a directory this judge names. Absent ⇒
1198
- * the built-in default deny table stands in (refusal-only; a deployment with a wired judge
1199
- * passes its own so the two faces cannot disagree). Explicit-consent doors (hand-written,
1198
+ * {@link defaultDeniesDirectoryRead} stands in (refusal-only; every hands leg passes its OWN judge). Explicit-consent doors (hand-written,
1200
1199
  * import) are deliberately NOT behind this — they have their own preview and validation.
1201
1200
  */
1202
1201
  readonly deniesDirectoryRead?: (directory: string) => boolean;
@@ -20,5 +20,15 @@ export function normalizeReadRootCandidate(value) {
20
20
  return undefined;
21
21
  if (!win32NamespaceScreen(dir, pathFamilyOf({ root: dir })).ok)
22
22
  return undefined;
23
- return { dir, clearsThisAsk: true };
23
+ let covers;
24
+ try {
25
+ const d = Object.getOwnPropertyDescriptor(value, "covers");
26
+ covers = d === undefined ? "absent" : "value" in d && d.value === "exact" ? "exact" : "refuse";
27
+ }
28
+ catch {
29
+ covers = "refuse";
30
+ }
31
+ if (covers === "refuse")
32
+ return undefined;
33
+ return covers === "exact" ? { dir, clearsThisAsk: true, covers: "exact" } : { dir, clearsThisAsk: true };
24
34
  }
@@ -47,7 +47,7 @@ export interface ManagedRetentionCapability {
47
47
  /** Lineage-aware transcript/session deletion: a session tree is deleted only when no live
48
48
  * reference (fork child, background child, workflow run, pending checkpoint) still points into
49
49
  * it — references are re-checked inside the deletion transaction, never assumed from a prior
50
- * scan. */
50
+ * scan; {@link RETENTION_CASCADE_EXEMPT} is never deleted with one. */
51
51
  deleteExpiredSessions(input: {
52
52
  domain: string;
53
53
  cutoffMs: number;
@@ -66,6 +66,13 @@ export interface ManagedRetentionCapability {
66
66
  * own occurrence id, operation, and timestamp inside the deleting transaction. Adding those here
67
67
  * would ask a return value to know its own call context.
68
68
  */
69
+ /** @contract retention.cascade_domains — a managed-retention cascade deletes checkpoint rows,
70
+ * session trees (transcripts and lineage) and orphan tool results; nothing else is session data. */
71
+ export declare const RETENTION_CASCADE_DOMAINS: readonly ["checkpoints", "session-trees", "orphan-tool-results"];
72
+ /** @contract retention.governance_records_outlive_the_purge — the pollution marker dir, the capture
73
+ * opt-out record dir and the lineage ledger are governance FACTS about content that exists, not
74
+ * session data: deleting them makes a one-way privacy record revocable. */
75
+ export declare const RETENTION_CASCADE_EXEMPT: readonly ["session-pollution", "session-capture-optout", "lineage.json"];
69
76
  export interface RetentionReceipt {
70
77
  domain: string;
71
78
  /** Rows destroyed by THIS call (idempotent retries count already-gone rows as 0, not errors). */
@@ -1,3 +1,5 @@
1
+ export const RETENTION_CASCADE_DOMAINS = ["checkpoints", "session-trees", "orphan-tool-results"];
2
+ export const RETENTION_CASCADE_EXEMPT = ["session-pollution", "session-capture-optout", "lineage.json"];
1
3
  function codedError(code, message) {
2
4
  const e = new Error(message);
3
5
  e.code = code;
@@ -1,4 +1,5 @@
1
1
  import type { AssistantMessage } from "../../internal/llm.js";
2
+ import type { HumanReviewGateRow } from "../task-result.js";
2
3
  import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
3
4
  import type { HaltConsumption } from "../../internal/harness.js";
4
5
  /** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
@@ -40,13 +41,7 @@ export interface Stats {
40
41
  humanReview?: {
41
42
  count: number;
42
43
  totalWaitMs: number;
43
- gates: Array<{
44
- kind: string;
45
- waitMs: number;
46
- decision?: string;
47
- toolName?: string;
48
- toolArg?: string;
49
- }>;
44
+ gates: HumanReviewGateRow[];
50
45
  };
51
46
  /** TB telemetry B2: engine-mechanism engagement counters. Set by the run loop
52
47
  * only when ≥1 engaged; flows straight through to `TaskResult.stats.mechanisms` via the
@@ -2,6 +2,7 @@ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
3
  import { terminalProjection } from "./terminal-projection.js";
4
4
  import { haltConsumed } from "./halt-attribution.js";
5
+ import { describeRejectedValue } from "../tool-search-setting.js";
5
6
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
6
7
  "output.degenerate",
7
8
  "limits.max_tokens_exceeded",
@@ -11,18 +12,34 @@ const SALVAGE_ELIGIBLE_TERMINALS = new Set([
11
12
  "env.lifetime_expired",
12
13
  "usage.window_exhausted",
13
14
  ]);
15
+ function thrownMessage(threw) {
16
+ try {
17
+ if (threw instanceof Error) {
18
+ const m = threw.message;
19
+ if (typeof m === "string")
20
+ return m;
21
+ }
22
+ }
23
+ catch {
24
+ }
25
+ return describeRejectedValue(threw);
26
+ }
14
27
  export function errorCodeOf(err) {
15
28
  let cur = err;
16
29
  let fallback;
17
- for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
18
- const code = cur.code;
19
- if (typeof code === "string" && code) {
20
- if (code !== "unknown") {
21
- return code;
30
+ try {
31
+ for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
32
+ const code = cur.code;
33
+ if (typeof code === "string" && code) {
34
+ if (code !== "unknown") {
35
+ return code;
36
+ }
37
+ fallback ??= code;
22
38
  }
23
- fallback ??= code;
39
+ cur = cur.cause;
24
40
  }
25
- cur = cur.cause;
41
+ }
42
+ catch {
26
43
  }
27
44
  return fallback;
28
45
  }
@@ -84,7 +101,7 @@ export function terminalCauseOf(final, stats, flags) {
84
101
  terminal = { kind: "failed", code: "suspend.loop", message };
85
102
  }
86
103
  else if (flags.threw) {
87
- const message = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
104
+ const message = thrownMessage(flags.threw);
88
105
  const code = flags.abortedForTimeout
89
106
  ? "limits.max_walltime_exceeded"
90
107
  : flags.abortedForTurns
@@ -217,6 +217,7 @@ export function classifierRoundObserver(args) {
217
217
  ms: info.ms,
218
218
  verdict: info.verdict,
219
219
  ...(info.cause !== undefined ? { cause: info.cause } : {}),
220
+ ...(info.notAsked !== undefined ? { notAsked: info.notAsked } : {}),
220
221
  ts: Date.now(),
221
222
  }));
222
223
  announceFallback(info.classifier);
@@ -13,6 +13,7 @@
13
13
  * without adding a runtime edge anywhere. The blocks below are verbatim moves, in their original
14
14
  * order, JSDoc included, so the move stays auditable as a move.
15
15
  */
16
+ import type { HumanReviewGateRow } from "../task-result.js";
16
17
  import type { RepairBundle } from "../../agents/repair-loop.js";
17
18
  import type { SubagentRetainLedger } from "../../agents/retain-ledger.js";
18
19
  import type { BrainCallGuardrailRef } from "../../brain/timeout.js";
@@ -248,6 +249,11 @@ export interface Prepared {
248
249
  thinking?: ThinkingLevel;
249
250
  compModel?: Model;
250
251
  mcp: MaterializedMcp;
252
+ /** The run handle's `reconnectMcpServer` implementation — {@link MaterializedMcp.reconnect} plus the
253
+ * live-roster application and the operator disclosure, which are runner concerns and not the MCP
254
+ * handle's. Present only when this leg DECLARED MCP servers; the stream verb's seat stays unbound
255
+ * otherwise and it refuses typed. */
256
+ mcpRedial?: (server: string) => Promise<import("../mcp.js").McpReconnectResult>;
251
257
  /** Present only when `spec.a2a` declared peers — the run loop owes it the same end-of-task dispose
252
258
  * it owes {@link mcp} (no sockets are held, but a disposed mount is what makes a post-task tool
253
259
  * call refuse loudly instead of dialing a peer on a dead task's behalf). */
@@ -783,16 +789,14 @@ export interface Prepared {
783
789
  * prior-leg burden seeded from the resumed checkpoint). The run loop ADDS the durable-resume latency
784
790
  * (`now() − cp.suspendedAt`) on a resume, then surfaces it as `stats.humanReview` at assembly (omitted when
785
791
  * empty). **Budget-EXCLUDED** — never folded into cost/the budget gate. */
792
+ /** borrowed-mutable — the hands' read-state map (the SAME `Map` object the toolkit and the context
793
+ * lane hold; absent without hands). Read by the provider-rejection recovery arm so its stale-result
794
+ * clears retire the entries those transmissions fed, through the same landing the frontier pass uses. */
795
+ readFileState?: import("../../tools/fs/safety.js").ReadFileState;
786
796
  humanReviewRef: {
787
797
  count: number;
788
798
  totalWaitMs: number;
789
- gates: Array<{
790
- kind: string;
791
- waitMs: number;
792
- decision?: string;
793
- toolName?: string;
794
- toolArg?: string;
795
- }>;
799
+ gates: HumanReviewGateRow[];
796
800
  };
797
801
  /** The injectable wall-clock the run loop uses for the durable-resume human-review latency
798
802
  * (`humanLatencyMs = now() − cp.suspendedAt`), so it reads the SAME clock as the suspend-side `suspendedAt`. */
@@ -908,7 +912,14 @@ export interface Prepared {
908
912
  * that were re-attached WHOLE. Present only with hands (same gate as readTaskFile).
909
913
  * `preserveReadState` carries the kept-tail-visible files that were
910
914
  * deliberately skipped for re-attachment — their existing entry must survive the clear too (same
911
- * reasoning as a seeded entry: the model's view of the file did not change). */
915
+ * reasoning as a seeded entry: the model's view of the file did not change).
916
+ *
917
+ * SCOPE — read "every compaction lane" as exactly that and no wider. The request-layer
918
+ * stale-result pass takes a Read body off the wire with no compaction at all, and retires its own
919
+ * entries at its own landing site (`prepare-context-lane.ts`'s `landStaleResultClears`) through the
920
+ * SAME function this seat calls, on its `tool-results-cleared` arm: ONE retirement mechanism, two
921
+ * ways for content to leave. Taken as a claim about every SHRINK mechanism this sentence was true
922
+ * of the lanes it named and silently false of the one it did not. */
912
923
  onCompactionApplied?: (attachedComplete: ReadonlyArray<{
913
924
  path: string;
914
925
  content: string;
@@ -1184,13 +1195,7 @@ export interface PrepareResume {
1184
1195
  priorHumanReview?: {
1185
1196
  count: number;
1186
1197
  totalWaitMs: number;
1187
- gates: Array<{
1188
- kind: string;
1189
- waitMs: number;
1190
- decision?: string;
1191
- toolName?: string;
1192
- toolArg?: string;
1193
- }>;
1198
+ gates: HumanReviewGateRow[];
1194
1199
  };
1195
1200
  /** When the suspend ran with a remote workspace, the {@link CheckpointState.workspaceHandle}
1196
1201
  * to restore — prepare rebuilds the per-task env via `deps.executionEnvFactory` then `resumeVM(snapshotId)`
@@ -2659,6 +2664,12 @@ export interface CaptureOptOutRef {
2659
2664
  outcome: "created" | "existed";
2660
2665
  }>;
2661
2666
  }
2667
+ /** The MCP re-dial verb's binding; bound only when the run declared MCP servers — a leg with
2668
+ * none has nothing to re-dial, and the stream verb then refuses TYPED rather than answering about a
2669
+ * materialization that was never made. Same shape and same law as {@link CaptureOptOutRef}. */
2670
+ export interface McpRedialRef {
2671
+ redial?: (server: string) => Promise<import("../mcp.js").McpReconnectResult>;
2672
+ }
2662
2673
  /**
2663
2674
  * The stream's LIVE state as the verb lanes read it — a view over the driver's own locals (getters, and one
2664
2675
  * setter), never a copy: `resultValue` is written by the run body's `setResult` callback and by the failure
@@ -1,3 +1,4 @@
1
+ import type { HumanReviewGateRow } from "../task-result.js";
1
2
  import { type OnAsk, type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
2
3
  /** JSON.stringify throws on a cyclic value (a deployment
3
4
  * policy's rewrite object carries no acyclicity guarantee) — `undefined` here means "no comparable
@@ -37,11 +38,5 @@ export declare function consumeInheritedAskGrant(grants: Map<string, {
37
38
  }>, onAsk: OnAsk | undefined, humanReviewRef: {
38
39
  count: number;
39
40
  totalWaitMs: number;
40
- gates: Array<{
41
- kind: string;
42
- waitMs: number;
43
- decision?: string;
44
- toolName?: string;
45
- toolArg?: string;
46
- }>;
41
+ gates: HumanReviewGateRow[];
47
42
  }, decision: PermissionResult, req: ToolCallRequest): ResolvedAsk | undefined;
@@ -23,6 +23,7 @@ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision
23
23
  waitMs: grant.waitMs,
24
24
  decision: "allow",
25
25
  toolName: req.toolName,
26
+ toolCallId: req.toolCallId,
26
27
  ...(toolArg !== undefined ? { toolArg } : {}),
27
28
  });
28
29
  const reused = { action: "allow", presentedInput: grant.presented };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The LIVE MCP roster lane: applying one server's intake result to a RUNNING roster, and the
3
+ * host-driven re-dial that is the second caller of it.
4
+ *
5
+ * Two faces change a task's MCP roster after prepare has finished — the model's `RefreshMcpTools`
6
+ * tool and the run handle's `reconnectMcpServer` verb — and what they must do to the roster is the
7
+ * same list in the same order: drop the deployment-excluded names, re-fold the safety axes (with the
8
+ * non-monotone restore arm on refusal), replace the server's whole name-prefix domain at its own
9
+ * anchor, rebuild the harness toolset, and re-mint the roster delta. That list lived inside the
10
+ * refresh tool's execute closure, where the second face could only have copied it; a copy would have
11
+ * been a second answer to "what does a mid-task roster change consist of".
12
+ *
13
+ * It is machinery, not a phase: the protocol-tools phase reaches DOWN for it to build the refresh
14
+ * tool, and the run's verb wiring reaches down for the same functions.
15
+ */
16
+ import type { AgentTool } from "../../internal/harness.js";
17
+ import type { McpDroppedTool, McpReconnectResult, MaterializedMcp, McpToolAxis } from "../mcp.js";
18
+ import type { RunnerDeps, ToolEffect } from "../types.js";
19
+ import type { IrreversibilityTier, ToolFaceSnapshot } from "./contracts.js";
20
+ import type { RosterBuilder, ToolRosterDeltaSeat } from "../tool-roster.js";
21
+ /** One server's assertion about its own prefix domain — the shape both faces reduce to. `tools` is
22
+ * the roster the domain must END UP with (empty withdraws it); `added`/`removed` are the reporting
23
+ * half and never drive the swap. */
24
+ export interface McpRosterDelta {
25
+ server: string;
26
+ prefix: string;
27
+ tools: AgentTool[];
28
+ axes: McpToolAxis[];
29
+ dropped: McpDroppedTool[];
30
+ }
31
+ /** The seats a roster application writes through. Every one of them is borrowed from the phase that
32
+ * minted it — this module owns nothing. */
33
+ export interface McpRosterApplyInput {
34
+ /** borrowed-readonly — the frozen tool face; `exclude` keeps a deployment-excluded tool from ever
35
+ * being re-mounted by a face the model or a host can trigger. */
36
+ toolFaceSnapshot: Pick<ToolFaceSnapshot, "exclude">;
37
+ /** borrowed-readonly — the per-server face overlay (the phase's `withFaces`). */
38
+ withFaces: (tools: AgentTool[], server: string) => AgentTool[];
39
+ /** borrowed-readonly — the large-result wrapper every protocol tool is mounted through. */
40
+ remoteToolOffload: (tool: AgentTool) => AgentTool;
41
+ /** borrowed-readonly — the phase's axis fold (throws on a config contradiction). */
42
+ foldProtocolAxes: (axes: McpToolAxis[], label: string) => void;
43
+ /** borrowed-mutable — the two NON-MONOTONE tables the fold writes and this applier snapshots. */
44
+ toolEffects: Map<string, ToolEffect>;
45
+ axisExplicitNegatives: Map<string, {
46
+ irreversible?: false;
47
+ egress?: false;
48
+ }>;
49
+ /** borrowed-mutable — the run's shared roster. */
50
+ roster: RosterBuilder;
51
+ }
52
+ /** What one application did, for the caller's receipt. `applied:false` means the axis fold refused
53
+ * and the domain was left exactly as it was (the tables restored) — no roster write happened. */
54
+ export type McpRosterApplyOutcome = {
55
+ applied: true;
56
+ pushed: number;
57
+ excluded: string[];
58
+ } | {
59
+ applied: false;
60
+ foldError: string;
61
+ };
62
+ /**
63
+ * Apply one server's delta to the live roster. Behavior-identical to the body this was extracted
64
+ * from; the comments are the extracted reasoning, not new claims.
65
+ */
66
+ export declare function applyMcpRosterDelta(input: McpRosterApplyInput, delta: McpRosterDelta): McpRosterApplyOutcome;
67
+ /** The seats the post-application commit writes through. */
68
+ export interface McpRosterCommitInput {
69
+ rebuildHarnessToolsRef: {
70
+ current?: () => Promise<void>;
71
+ };
72
+ rosterSeat: ToolRosterDeltaSeat;
73
+ toolCallGateArmedRef: {
74
+ armed: boolean;
75
+ };
76
+ irreversibleTools: ReadonlySet<string>;
77
+ egressTools: ReadonlySet<string>;
78
+ }
79
+ /**
80
+ * Publish whatever the applications above changed: rebuild the harness toolset (the content-origin
81
+ * re-wrap runs inside it), then re-mint the roster delta so the stream's rows describe the LIVE
82
+ * objects. Batched deliberately — a refresh spanning several servers rebuilds once.
83
+ *
84
+ * Returns the gate-honesty line when one is due. The built-in integrity policies are composed
85
+ * UNCONDITIONALLY, so `armed` is always true in this engine and the line never fires today; the arm
86
+ * stays as a zero-cost guard for a refactor that would make the composition conditional.
87
+ */
88
+ export declare function commitMcpRosterChanges(input: McpRosterCommitInput): Promise<string | undefined>;
89
+ /** The re-dial's own seats: the handle it drives, the two operator sinks, and the run identity the
90
+ * disclosure names. */
91
+ export interface McpRedialInput extends McpRosterApplyInput, McpRosterCommitInput {
92
+ mcp: MaterializedMcp;
93
+ deps: Pick<RunnerDeps, "onError" | "onNotice">;
94
+ sessionId: string;
95
+ runId: string;
96
+ irreversibilityTier: IrreversibilityTier;
97
+ }
98
+ /**
99
+ * The run handle's `reconnectMcpServer` verb, phase-side: re-dial one server, put whatever it
100
+ * asserted about its prefix domain onto the live roster, and disclose.
101
+ *
102
+ * The roster is written IFF the result carries `tools` — the one rule this lane reads. That covers
103
+ * both directions with no second condition: a successful re-dial asserts its fresh roster, a failed
104
+ * DIAL asserts the empty one (the connection it tore down is gone, so leaving its names mounted would
105
+ * leave the model holding tools that can only fail), and an arm that never touched the connection —
106
+ * unknown name, revoked server, re-dial already in flight, disposed materialization — asserts nothing
107
+ * and the mounted roster is left alone.
108
+ */
109
+ export declare function redialMcpServer(input: McpRedialInput, server: string): Promise<McpReconnectResult>;
@@ -0,0 +1,60 @@
1
+ import { deliverEngineNotice } from "../types.js";
2
+ export function applyMcpRosterDelta(input, delta) {
3
+ const { toolFaceSnapshot, withFaces, remoteToolOffload, foldProtocolAxes, toolEffects, axisExplicitNegatives, roster } = input;
4
+ const excludedSet = new Set(toolFaceSnapshot.exclude ?? []);
5
+ const pushable = delta.tools.filter((t) => !excludedSet.has(t.name));
6
+ const excluded = delta.tools.filter((t) => excludedSet.has(t.name)).map((t) => t.name);
7
+ const domainSnapshot = (m) => new Map([...m].filter(([name]) => name.startsWith(delta.prefix)));
8
+ const restoreDomain = (m, snap) => {
9
+ for (const name of [...m.keys()])
10
+ if (name.startsWith(delta.prefix))
11
+ m.delete(name);
12
+ for (const [name, v] of snap)
13
+ m.set(name, v);
14
+ };
15
+ const priorDomainEffects = domainSnapshot(toolEffects);
16
+ const priorDomainNegatives = domainSnapshot(axisExplicitNegatives);
17
+ for (const name of priorDomainEffects.keys())
18
+ toolEffects.delete(name);
19
+ for (const name of priorDomainNegatives.keys())
20
+ axisExplicitNegatives.delete(name);
21
+ try {
22
+ foldProtocolAxes(delta.axes.filter((a) => !excludedSet.has(a.name)), "MCP");
23
+ }
24
+ catch (foldErr) {
25
+ restoreDomain(toolEffects, priorDomainEffects);
26
+ restoreDomain(axisExplicitNegatives, priorDomainNegatives);
27
+ return { applied: false, foldError: foldErr instanceof Error ? foldErr.message : String(foldErr) };
28
+ }
29
+ roster.replaceDomain(delta.prefix, withFaces(pushable, delta.server).map((t) => remoteToolOffload(t)), { source: "mcp", mountedBy: "optional", origin: { peer: delta.server } });
30
+ return { applied: true, pushed: pushable.length, excluded };
31
+ }
32
+ export async function commitMcpRosterChanges(input) {
33
+ await input.rebuildHarnessToolsRef.current?.();
34
+ input.rosterSeat.republish();
35
+ if (!input.toolCallGateArmedRef.armed && (input.irreversibleTools.size > 0 || input.egressTools.size > 0)) {
36
+ return "WARNING: refreshed tools declare irreversible/egress safety hints, but this task started with no approval gate registered (no policy/hooks and no gated tools at start) — these hints cannot arm a gate mid-task (RB-46); the refreshed tools run ungated on this deployment shape.";
37
+ }
38
+ return undefined;
39
+ }
40
+ export async function redialMcpServer(input, server) {
41
+ const result = await input.mcp.reconnect(server);
42
+ if (result.tools !== undefined) {
43
+ const outcome = applyMcpRosterDelta(input, { server: result.server, prefix: result.prefix, tools: result.tools, axes: result.axes ?? [], dropped: result.dropped ?? [] });
44
+ if (!outcome.applied)
45
+ input.deps.onError?.(Object.assign(new Error(`MCP re-dial of "${server}" connected but its safety-axis fold refused, so its tools were not mounted: ${outcome.foldError}`), { code: "mcp.server_unavailable" }), { phase: "mcp", sessionId: input.sessionId });
46
+ const ungated = await commitMcpRosterChanges(input);
47
+ if (ungated !== undefined)
48
+ input.deps.onError?.(Object.assign(new Error(ungated), { code: "mcp.server_unavailable" }), { phase: "mcp", sessionId: input.sessionId });
49
+ }
50
+ for (const w of result.warnings ?? [])
51
+ input.deps.onError?.(Object.assign(new Error(w), { code: "mcp.server_unavailable" }), { phase: "mcp", sessionId: input.sessionId });
52
+ deliverEngineNotice(input.deps.onNotice, {
53
+ code: "mcp.server_redialed",
54
+ message: result.outcome === "reconnected"
55
+ ? `MCP server "${server}" was re-dialed at the host's request: ${result.toolCount} tool${result.toolCount === 1 ? "" : "s"} mounted (${result.added.length} added, ${result.removed.length} removed)`
56
+ : `MCP server "${server}" was NOT re-dialed (${result.outcome})${result.reason !== undefined ? `: ${result.reason}` : ""}`,
57
+ detail: { server, outcome: result.outcome, added: result.added.length, removed: result.removed.length, ...(result.reason !== undefined ? { reason: result.reason } : {}), sessionId: input.sessionId, runId: input.runId },
58
+ });
59
+ return result;
60
+ }
@@ -94,6 +94,8 @@ export declare const AUTO_RUN_ON_RECOMMENDATION_DEFAULT = true;
94
94
  * switch wired AND a model seat resolvable) — the reference CLI's "auto-dream: off while
95
95
  * auto-memory is off": a default-on world must not arm a deployment that wired no consolidation;
96
96
  * - any other value ⇒ off (never arm on garbage; the door refuses it at prepare anyway).
97
+ *
98
+ * @contract memory.auto_consolidation.arming_has_one_resolver — whether a deployment's automatic consolidation is armed is answered here and nowhere else, a host that merely DISPLAYS the arming included: an ABSENT seat is not "off" (it reads the named default, and only where both seats are wired), so a reader branching on `RunnerDeps.autoRunOnRecommendation` alone is wrong in BOTH directions — it calls a default-armed deployment unarmed and a deployment with no consolidation armed. A deployment fact, never a per-session one; exported from the package root for that reason.
97
99
  */
98
100
  export declare function autoRunOnRecommendationEffective(deps: MemoryConsolidationDriverDeps & {
99
101
  autoRunOnRecommendation?: unknown;
@@ -115,6 +115,7 @@ export function prepareAskLane(input) {
115
115
  waitMs,
116
116
  decision: resolved.action === "allow" ? "allow" : "deny",
117
117
  toolName: req.toolName,
118
+ toolCallId: req.toolCallId,
118
119
  ...(toolArg !== undefined ? { toolArg } : {}),
119
120
  });
120
121
  }
@@ -7,7 +7,7 @@ import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-r
7
7
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
8
8
  import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
9
9
  import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
10
- import { autoModeClassifierFallbackOf, autoModeRoundNotAsked, createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
10
+ import { autoModeClassifierFallbackOf, createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
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";
@@ -207,27 +207,24 @@ export async function prepareCapsAndWorkflow(input) {
207
207
  const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
208
208
  const classifierRuntime = brainToRuntime(deps.brain);
209
209
  const classifierRungs = classifierLadderRungs(am.classifierFallback, classifierModel, model);
210
- const roundPrompt = new WeakMap();
211
210
  autoModeDenialTracking = createAutoModeDenialTracker(am.denialLimit);
212
211
  autoModeDecider = createAutoModeDecider({
213
212
  ...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
214
213
  candidates: classifierRungs.map((m) => m.id),
215
214
  onClassified: classifierRoundObserver({ tracer: deps.tracer, onNotice: deps.onNotice, taskId: hostTaskId, seatModelId: classifierModel.id, ...(sessionId !== undefined ? { sessionId } : {}), ...(runId !== undefined ? { runId } : {}) }),
216
- classify: async (input, signal, rung) => {
215
+ assemble: async (input) => {
216
+ const ctx = await session.buildContext();
217
+ const known = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m) && (m.role === "user" || m.role === "assistant" || m.role === "toolResult"));
218
+ return renderAutoModeWindow(known, am.window) + renderAutoModeAction(input);
219
+ },
220
+ classify: async (_input, signal, rung, userPrompt) => {
217
221
  const rungModel = rung !== undefined ? classifierRungs[rung.index] : undefined;
218
222
  if (rung === undefined || rungModel === undefined) {
219
223
  throw new Error(`the auto-mode classifier was asked at ladder position ${JSON.stringify(rung?.index)}, which this arming did not build`);
220
224
  }
221
- let build = rung.index > 0 ? roundPrompt.get(input) : undefined;
222
- if (build === undefined) {
223
- build = (async () => {
224
- const ctx = await session.buildContext();
225
- const known = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m) && (m.role === "user" || m.role === "assistant" || m.role === "toolResult"));
226
- return renderAutoModeWindow(known, am.window) + renderAutoModeAction(input);
227
- })();
228
- roundPrompt.set(input, build);
225
+ if (userPrompt === undefined) {
226
+ throw new Error("the auto-mode classifier was asked with no assembled question, which this arming builds once per classification");
229
227
  }
230
- const userPrompt = await build.catch((e) => { throw autoModeRoundNotAsked(e); });
231
228
  const classifierAuth = await spec.getApiKeyAndHeaders?.(rungModel);
232
229
  const request = async (seat, suffix) => {
233
230
  const response = await classifierRuntime.completeSimple(rungModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt + suffix, timestamp: Date.now() }] }, {
@@ -17,11 +17,12 @@
17
17
  */
18
18
  import { type AgentHarness, type AgentMessage } from "../../internal/harness.js";
19
19
  import type { Model } from "../../internal/llm.js";
20
- import { type MicroCompactMachine, type OccurrenceIndex } from "../context-edit.js";
20
+ import { type ClearedProjectionLedger, type MicroCompactMachine, type OccurrenceFingerprint, type OccurrenceIndex } from "../context-edit.js";
21
21
  import { type ToolResultStore } from "../tool-result-store.js";
22
22
  import { type RunnerDeps } from "../types.js";
23
23
  import type { Prepared, PreparedMicroCompact } from "./contracts.js";
24
24
  import { type GitStatusLaneRef } from "./git-status-frame.js";
25
+ import type { ReadFileState } from "../../tools/fs/safety.js";
25
26
  export interface PrepareContextLaneInput {
26
27
  /** borrowed-readonly — the task model: the budgets, the trigger-window geometry and `charsPerToken` are read off it. */
27
28
  model: Model;
@@ -56,6 +57,12 @@ export interface PrepareContextLaneInput {
56
57
  requestLossyRef: {
57
58
  current: boolean;
58
59
  };
60
+ /** borrowed-mutable — the hands' read-state map, or undefined without hands. Writer here: a landed stale-result
61
+ * clear RETIRES the entries whose transmission it just replaced with a marker — the same retirement the compaction
62
+ * lanes run through the SAME function (`applyContextShrinkToReadFileState`), because a Read body taken off the wire
63
+ * is off the wire whichever mechanism took it. Without it a re-read of an unchanged file answers "content omitted"
64
+ * pointing at a cleared marker. */
65
+ readFileState?: ReadFileState;
59
66
  /** borrowed-mutable — the trim-pressure flag (`Prepared.trimPressureRef`). Writer here: the guard chain's trim arm sets
60
67
  * `droppedMessages`; the run loop's boundary reads and clears it. */
61
68
  trimPressureRef: {
@@ -72,6 +79,38 @@ export interface PrepareContextLaneResult {
72
79
  }
73
80
  /** The M21 phase body — prepareTask's context-lane stretch, verbatim (see the module header). */
74
81
  export declare function prepareContextLane(input: PrepareContextLaneInput): PrepareContextLaneResult;
82
+ /** Record a cc-machine pass's clears into the ledger + emit the G11
83
+ * frame (one frame per firing pass; the replay of these markers on later requests emits nothing —
84
+ * cardinality is per CLEAR, not per request). `trigger` names the arm: `"frontier"` = the
85
+ * proactive request-build pass, `"blocking"` = the slice-3 arm-A pre-guard pass (the machine's
86
+ * one shot when the frontier hook is off). Marker bytes are read back off the edited array
87
+ * (the machine composed them, ref/media notes included) so the ledger replays EXACTLY what went
88
+ * on the wire; an occurrence the index cannot key (pathological same-id-same-timestamp ambiguity)
89
+ * is simply not recorded — its clear stays request-local, the safe (full-text-returns) direction.
90
+ * `at.fp` is the occurrence's fingerprint in the PRE-CAP replayed view (the same bytes the next
91
+ * session rebuild presents) — the replay-side mis-paint guard's comparand. */
92
+ /**
93
+ * The ONE landing of a set of stale-result clears: the ledger entry that makes the marker survive every
94
+ * later rebuild, and the retirement of the read-state entries those transmissions carried. The pair is
95
+ * inseparable — a ledger entry WITHOUT the retirement leaves a dedup stub pointing at a marker — which
96
+ * is why it is a function rather than two statements repeated at each of the two clear sites (the
97
+ * frontier/blocking pass and the provider-rejection recovery arm, which used to write the ledger
98
+ * independently, so only one of them retired anything). Posture: a ledger entry means the marker is
99
+ * repainted on every later request build, so the occurrence is cleared for good even when the
100
+ * projection that produced it is discarded — which is why the retirement belongs here and not in the
101
+ * per-request observation buffer.
102
+ */
103
+ export declare function landStaleResultClears(args: {
104
+ ledger: ClearedProjectionLedger;
105
+ cleared: ReadonlyArray<{
106
+ key: string;
107
+ marker: string;
108
+ groupCount: number;
109
+ fp: OccurrenceFingerprint;
110
+ toolCallId?: string;
111
+ }>;
112
+ readFileState?: ReadFileState;
113
+ }): void;
75
114
  /** See {@link runGuardChain}. */
76
115
  export interface GuardChainArgs {
77
116
  /** The post-frontier view the chain starts from. */
@@ -94,6 +133,9 @@ export interface GuardChainArgs {
94
133
  /** The r7 per-request observation buffer — the chain DROPS it on its early returns (a discarded
95
134
  * provisional projection must not narrate itself). */
96
135
  pendingProjectionObservations: Array<() => void>;
136
+ /** The hands' read-state map, or undefined without hands — arm A's clear retires the entries its
137
+ * cleared transmissions carried (see `PrepareContextLaneInput.readFileState`). */
138
+ readFileState?: ReadFileState;
97
139
  }
98
140
  export type GuardChainOutcome = {
99
141
  earlyReturn: {