@sema-agent/core 5.0.1 → 5.1.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 (54) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agents/roster-store.d.ts +9 -2
  3. package/dist/agents/roster-store.js +26 -5
  4. package/dist/agents/subagent.js +8 -3
  5. package/dist/bin/sema-tb.d.ts +1 -2
  6. package/dist/bin/sema-tb.js +13 -25
  7. package/dist/brain/anthropic.js +2 -3
  8. package/dist/brain/degrading.d.ts +2 -8
  9. package/dist/brain/degrading.js +3 -3
  10. package/dist/brain/terminal-cause.js +2 -8
  11. package/dist/core/hooks.js +4 -4
  12. package/dist/core/lsp.js +2 -3
  13. package/dist/core/mcp.d.ts +1 -1
  14. package/dist/core/mcp.js +2 -1
  15. package/dist/core/memory-engine/engine.js +2 -2
  16. package/dist/core/memory-engine/file-backend.js +3 -1
  17. package/dist/core/memory-engine/layout.d.ts +3 -0
  18. package/dist/core/memory-engine/layout.js +51 -0
  19. package/dist/core/memory.d.ts +0 -1
  20. package/dist/core/memory.js +1 -1
  21. package/dist/core/permission-rules.js +8 -7
  22. package/dist/core/protocol-table.d.ts +14 -0
  23. package/dist/core/protocol-table.js +23 -0
  24. package/dist/core/runner/active-skill-scope.js +7 -7
  25. package/dist/core/runner/prepare-memory.js +4 -1
  26. package/dist/core/runner/prepare-task.js +17 -16
  27. package/dist/core/runner/runtask.js +9 -3
  28. package/dist/core/runner/session-rule-policy.js +1 -1
  29. package/dist/core/runner/synthetic-tools.js +2 -2
  30. package/dist/core/runner/tool-disclosure.d.ts +0 -1
  31. package/dist/core/runner/tool-disclosure.js +0 -2
  32. package/dist/core/runner/turn-attachments.js +2 -1
  33. package/dist/core/sensitive-path-policy.js +2 -2
  34. package/dist/core/tool-policy.d.ts +0 -3
  35. package/dist/core/tool-policy.js +29 -15
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/orchestration/run-spec.js +1 -1
  39. package/dist/orchestration/run-workflow-tool.js +2 -2
  40. package/dist/orchestration/workflow-script-store.d.ts +1 -1
  41. package/dist/orchestration/workflow-script-store.js +1 -1
  42. package/dist/prompt-assembly/assemble.js +0 -1
  43. package/dist/prompt-assembly/tool-catalog.d.ts +2 -1
  44. package/dist/prompts/default.d.ts +0 -2
  45. package/dist/prompts/default.js +1 -5
  46. package/dist/stores/cc/mailbox-store.d.ts +4 -0
  47. package/dist/stores/cc/mailbox-store.js +16 -2
  48. package/dist/stores/file/session-policy-store.d.ts +10 -1
  49. package/dist/stores/file/session-policy-store.js +20 -4
  50. package/dist/tools/fs/fs-search-tools.js +1 -2
  51. package/dist/tools/fs/fs-shared.d.ts +0 -1
  52. package/dist/tools/fs/fs-shared.js +0 -1
  53. package/dist/tools/todo.js +1 -1
  54. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.0 (2026-08-03)
4
+
5
+ _The second (and final planned) baggage sweep: every remaining compatibility arm, plus a durable memory-key fix with an automatic adoption leg and the protocol table that clears the road for a second tool protocol. Version-line policy (project ruling): the major stays at 5 — breaking changes below are declared loudly here and coordinated with all in-org consumers, but do not mint a new major._
6
+
7
+ **BREAKING (declared, rides the 5.x line by project ruling)**
8
+
9
+ 0. **The registry closes — every remaining compatibility arm is retired in this release:**
10
+ - `createDegradingBrain`: ONE form — the ordered `fallbacks` chain (`fallback`+`fallbackModel` retired; a chain-of-one is `fallbacks: [{brain, model}]`). The pair's two-trigger default retires with it: every construction gets the four-class gate; narrow explicitly via `downgradeOn`.
11
+ - `ToolSearch`: the pre-design/116 `select` ARRAY argument resolves nothing (the CC-canonical `select:A,B,C` string prefix in `query` is the live selection form).
12
+ - `Skill`: `skill` is REQUIRED (the `{name}` arm retired) — a stale replay bounces as loud, model-correctable invalid-arguments.
13
+ - `LSP`: the deprecated `symbol` alias for `query` leaves the tool schema (the `LspRequest.symbol` SEAM field is the wire contract, untouched).
14
+ - `TodoWrite`: `activeForm` is REQUIRED (exactly CC's TodoItem).
15
+ - `WorkflowScriptStore.resolveName`: ONE return shape — `{script, defaultArgs?}` (the bare-string arm retired).
16
+ - `sema-tb`: the `--message` argv leg is a usage error (process-list exposure once made a model's pgrep cleanup kill its own agent); use `--objective-file` or stdin.
17
+ - Anthropic brain: thinking enables ONLY on a valid non-`"off"` `ThinkingLevel` — the legacy-boolean silent 0.5-share enable is gone (out-of-contract value = absence).
18
+ - Terminal-cause predicates read `errorKind` ONLY — the `errorMessage`-sentinel fallback is deleted; stamping `errorKind` on every cut turn is now a documented Brain obligation.
19
+ - Read/Edit/Write schemas declare `file_path` only (the durable `path` alias retired; the security-gate readers keep their `path` over-read — that side is fail-closed coverage, not compat).
20
+ - The legacy prompt composer loses `memoryEnabled`/`consolidationEnabled` (dead in production since design/138 S4; `MEMORY_SAFETY`/`MEMORY_HYGIENE`/`MEMORY_GUIDANCE` stay exported as standalone assets).
21
+ 1. **`PermissionResult` loses the legacy `reason` field.** All three arms carry `message` only, and `decisionText` reads `message` only. A TS policy returning `{reason}` compiles red; a JS policy keeps its verdict but its custom text degrades to the per-site generic default. Migrate: `reason:` → `message:`.
22
+ 2. **`MemorySpecInput` is ONE form.** The singular `scope` field is removed; `scopes: string[]` is the only spelling. Migrate: `{scope: "x"}` → `{scopes: ["x"]}`. An untyped caller still passing `{scope}` gets "no usable scopes" = memory off for that task.
23
+ 3. **The approval folds are STRICT.** `createApprovalPolicy.approve` and the `onAsk` boolean arm approve ONLY on the literal `true`; an out-of-contract truthy (`"yes"`, `1`, …) from an untyped bridge is a fail-closed deny naming the defect. The historical truthy leniency was a fail-open on the security face with no live producer (server bridge verified).
24
+ 4. **Grep drops the retired keys (CC alignment).** `max_results` leaves the schema and the wiring — a stale caller's value applies NOTHING (`head_limit` is the one spelling). The `regex` key has no declared arm anywhere.
25
+
26
+ **Fixed**
27
+
28
+ - **One repo, one memory (RB-472).** `deriveRepoKey` canonicalizes (realpath) before minting, so a repo reached through a symlink alias and through its physical path no longer splits into two memory partitions. A pre-6.x deployment's legacy-keyed dirs are ADOPTED automatically at mount (one-shot whole-dir rename, concurrent-open race-tolerant). A store that already split is a loud `memory.partition_split` incident with both coordinates — never silently merged. Rename failure = `memory.partition_adopt_failed` disclosure; the mount never fails over adoption.
29
+
30
+ **Added**
31
+
32
+ - **Protocol namespace table**: `PROTOCOL_TABLE` / `MCP_NAMESPACE` / `protocolOf` / `ProtocolNamespace` / `ProtocolId` — "which protocol owns this tool name" as a lookup (`MCP_PREFIX` is now the table entry's prefix; a second protocol lands by appending one entry). `ToolOrigin`'s protocol slice derives from `ProtocolId`.
33
+ - **Corrupt-read disclosure seats** (the tolerances stay; the silence goes): `FileSessionPolicyStore`/`FileRosterStore` constructor `onCorruptRead`, `CcFileMailboxStoreOptions.onCorruptRead`, `recordRosterSpawn` optional `onError` (threaded to `onObserverError`, site `roster.recordSpawn`), and the durable-approval continuity read disclosing via `onObserverError` (site `resume.durableApprovalContinuity`). Plain ENOENT never fires any of them.
34
+
35
+ **Migration one-line table**: `reason:` → `message:` / `{scope}` → `{scopes: [..]}` / approver returns literal `true`·`false`·`"unavailable"`·`{allow}` only / `max_results` → `head_limit`.
36
+
3
37
  ## 5.0.1 (2026-08-02)
4
38
 
5
39
  _Post-release review patch: the 5.0.0 review wave (five domain reviewers + two independent codex legs) surfaced silent-widening gaps in the new audit surfaces plus wiring/anchoring stragglers. All fixes are loud-direction; no new API._
@@ -42,7 +42,14 @@ export declare class FileRosterStore implements RosterStore {
42
42
  private readonly path;
43
43
  private readonly maxAgeMs;
44
44
  private readonly gc;
45
- constructor(path: string, opts?: RosterGcOptions);
45
+ private readonly onCorruptRead;
46
+ constructor(path: string, opts?: RosterGcOptions & {
47
+ onCorruptRead?: (info: {
48
+ path: string;
49
+ reason: string;
50
+ }) => void;
51
+ });
52
+ private discloseCorrupt;
46
53
  private read;
47
54
  private write;
48
55
  record(entry: RosterEntry): void;
@@ -50,4 +57,4 @@ export declare class FileRosterStore implements RosterStore {
50
57
  list(): RosterEntry[];
51
58
  releaseAgent(agentId: string): void;
52
59
  }
53
- export declare function recordRosterSpawn(roster: RosterStore | undefined, entry: RosterEntry): void;
60
+ export declare function recordRosterSpawn(roster: RosterStore | undefined, entry: RosterEntry, onError?: (err: unknown) => void): void;
@@ -76,10 +76,19 @@ export class FileRosterStore {
76
76
  path;
77
77
  maxAgeMs;
78
78
  gc;
79
+ onCorruptRead;
79
80
  constructor(path, opts) {
80
81
  this.path = path;
81
82
  this.maxAgeMs = opts?.maxAgeMs;
82
83
  this.gc = opts;
84
+ this.onCorruptRead = opts?.onCorruptRead;
85
+ }
86
+ discloseCorrupt(reason) {
87
+ try {
88
+ this.onCorruptRead?.({ path: this.path, reason });
89
+ }
90
+ catch {
91
+ }
83
92
  }
84
93
  read(mode) {
85
94
  let text;
@@ -89,8 +98,10 @@ export class FileRosterStore {
89
98
  catch (e) {
90
99
  if (e.code === "ENOENT")
91
100
  return [];
92
- if (mode === "lenient")
101
+ if (mode === "lenient") {
102
+ this.discloseCorrupt(`read failed (non-ENOENT): ${e.message}`);
93
103
  return [];
104
+ }
94
105
  throw e;
95
106
  }
96
107
  if (text.trim() === "")
@@ -102,8 +113,10 @@ export class FileRosterStore {
102
113
  return raw.entries.filter((e) => typeof e === "object" && e !== null && typeof e.name === "string" && typeof e.agentId === "string" && typeof e.createdAt === "number");
103
114
  }
104
115
  catch (e) {
105
- if (mode === "lenient")
116
+ if (mode === "lenient") {
117
+ this.discloseCorrupt(`corrupt roster document: ${e.message}`);
106
118
  return [];
119
+ }
107
120
  throw e;
108
121
  }
109
122
  }
@@ -127,12 +140,20 @@ export class FileRosterStore {
127
140
  this.write(this.read("strict").filter((e) => e.agentId !== agentId));
128
141
  }
129
142
  }
130
- export function recordRosterSpawn(roster, entry) {
143
+ export function recordRosterSpawn(roster, entry, onError) {
131
144
  if (!roster)
132
145
  return;
146
+ const disclose = (err) => {
147
+ try {
148
+ onError?.(err);
149
+ }
150
+ catch {
151
+ }
152
+ };
133
153
  try {
134
- void Promise.resolve(roster.record(entry)).catch(() => { });
154
+ void Promise.resolve(roster.record(entry)).catch(disclose);
135
155
  }
136
- catch {
156
+ catch (err) {
157
+ disclose(err);
137
158
  }
138
159
  }
@@ -1693,7 +1693,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1693
1693
  }, { priority: "later" }), "subagent.reapTerminalNotify");
1694
1694
  });
1695
1695
  if (agentName !== undefined) {
1696
- recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() });
1696
+ recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
1697
1697
  }
1698
1698
  const bgSink = ctx.onBackgroundChildEvent;
1699
1699
  const sinkEmit = (event) => {
@@ -2096,7 +2096,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2096
2096
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2097
2097
  }
2098
2098
  if (agentName !== undefined) {
2099
- recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() });
2099
+ recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2100
2100
  }
2101
2101
  const bgSink = ctx.onBackgroundChildEvent;
2102
2102
  const sinkEmit = (event) => {
@@ -2333,7 +2333,12 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2333
2333
  resumeConfig.durableApproval = { scope: cp0.scope };
2334
2334
  }
2335
2335
  }
2336
- catch {
2336
+ catch (err) {
2337
+ try {
2338
+ opts.onObserverError?.(err, { site: "resume.durableApprovalContinuity" });
2339
+ }
2340
+ catch {
2341
+ }
2337
2342
  }
2338
2343
  }
2339
2344
  const bgStream = await opts.runner.resumeStream(pr.ticket.token, pr.outcome, resumeConfig, {
@@ -2,7 +2,6 @@
2
2
  import { type Model } from "../index.js";
3
3
  interface Args {
4
4
  model: string;
5
- message: string | undefined;
6
5
  objectiveFile: string | undefined;
7
6
  jsonl: string | undefined;
8
7
  }
@@ -19,7 +18,7 @@ export type InstructionResolution = {
19
18
  } | {
20
19
  error: string;
21
20
  };
22
- export declare function resolveInstruction(args: Pick<Args, "message" | "objectiveFile">, readInput: (source: string | 0) => string): InstructionResolution;
21
+ export declare function resolveInstruction(args: Pick<Args, "objectiveFile">, readInput: (source: string | 0) => string): InstructionResolution;
23
22
  type TbCost = Model["cost"];
24
23
  export type TbCostResolution = {
25
24
  cost: TbCost;
@@ -6,11 +6,11 @@ import { positiveIntFromEnv, tbModelInput } from "./tb-env.js";
6
6
  import { inferMaxTokensField } from "../brain/openai.js";
7
7
  function usage(code = 2) {
8
8
  const out = code === 0 ? process.stdout : process.stderr;
9
- out.write("Usage: sema-tb --model <provider/model> [--message <instruction> | --objective-file <path>] [--jsonl <path>]\n" +
10
- " Instruction legs: --objective-file <path> (preferred for real task text) | --message <text> (argv)\n" +
9
+ out.write("Usage: sema-tb --model <provider/model> [--objective-file <path>] [--jsonl <path>]\n" +
10
+ " Instruction legs: --objective-file <path> (preferred for real task text) | stdin\n" +
11
11
  " | stdin (default when neither flag is given). Both flags together = usage error.\n" +
12
12
  " WARNING: argv is visible to every process in the sandbox (ps / pgrep -f / /proc/*/cmdline) — long\n" +
13
- " or sensitive instructions should ride --objective-file or stdin, never --message.\n" +
13
+ " Instructions ride --objective-file or stdin; the retired --message argv leg is refused (process-list exposure).\n" +
14
14
  "Env: <PROVIDER>_API_KEY / <PROVIDER>_BASE_URL, TB_MAX_TURNS, TB_TIMEOUT_SEC, TB_FULLBODY, TB_TODOREMIND, TB_BGTASKS, TB_SUMMARIZE,\n" +
15
15
  " TB_FINALVERIFY, TB_CALLCAP, TB_FINALIZE, TB_REASONING, TB_JSONL, TB_CONTEXT_WINDOW, TB_MAX_OUTPUT_TOKENS, TB_TASKLIST, TB_VISION\n" +
16
16
  ' TB_COST="input,output[,cacheRead[,cacheWrite]]" — USD per MTok; explicit pricing override (omitted\n' +
@@ -20,19 +20,19 @@ function usage(code = 2) {
20
20
  }
21
21
  export function parseArgs(argv, env = process.env) {
22
22
  let model;
23
- let message;
24
23
  let objectiveFile;
25
24
  let jsonl;
26
25
  for (let i = 0; i < argv.length; i++) {
27
26
  const a = argv[i];
28
- if (a === "--model" || a === "--message" || a === "--objective-file" || a === "--jsonl") {
27
+ if (a === "--message") {
28
+ return { usageError: "--message is retired (argv is visible to every sandbox process — the exposure once made a model's pgrep cleanup kill its own agent). Use --objective-file <path> or pipe the instruction on stdin." };
29
+ }
30
+ if (a === "--model" || a === "--objective-file" || a === "--jsonl") {
29
31
  const v = argv[++i];
30
32
  if (v === undefined)
31
33
  return { usageError: `missing value for ${a}` };
32
34
  if (a === "--model")
33
35
  model = v;
34
- else if (a === "--message")
35
- message = v;
36
36
  else if (a === "--objective-file")
37
37
  objectiveFile = v;
38
38
  else
@@ -46,28 +46,16 @@ export function parseArgs(argv, env = process.env) {
46
46
  if (!model || !model.includes("/")) {
47
47
  return { usageError: "--model must be <provider/model> (e.g. deepseek/deepseek-v4-flash)" };
48
48
  }
49
- return { args: { model, message, objectiveFile, jsonl: jsonl ?? env.TB_JSONL } };
49
+ return { args: { model, objectiveFile, jsonl: jsonl ?? env.TB_JSONL } };
50
50
  }
51
51
  export function resolveInstruction(args, readInput) {
52
- if (args.message !== undefined && args.objectiveFile !== undefined) {
53
- return { error: "--message and --objective-file are mutually exclusive (pick one instruction leg)" };
54
- }
55
- const leg = args.objectiveFile !== undefined
56
- ? `--objective-file ${args.objectiveFile}`
57
- : args.message !== undefined
58
- ? "--message"
59
- : "stdin (no --message/--objective-file)";
52
+ const leg = args.objectiveFile !== undefined ? `--objective-file ${args.objectiveFile}` : "stdin (no --objective-file)";
60
53
  let raw;
61
- if (args.message !== undefined) {
62
- raw = args.message;
54
+ try {
55
+ raw = readInput(args.objectiveFile ?? 0);
63
56
  }
64
- else {
65
- try {
66
- raw = readInput(args.objectiveFile ?? 0);
67
- }
68
- catch (e) {
69
- return { error: `cannot read instruction from ${leg}: ${e instanceof Error ? e.message : String(e)}` };
70
- }
57
+ catch (e) {
58
+ return { error: `cannot read instruction from ${leg}: ${e instanceof Error ? e.message : String(e)}` };
71
59
  }
72
60
  const instruction = raw.trim();
73
61
  if (!instruction)
@@ -253,7 +253,7 @@ export function createAnthropicBrain(config = {}) {
253
253
  if (options?.temperature !== undefined && anthCompat.supportsTemperature !== false) {
254
254
  body.temperature = options.temperature;
255
255
  }
256
- if (model.reasoning && options?.reasoning && options.reasoning !== "off") {
256
+ if (model.reasoning && isThinkingLevel(options?.reasoning) && options.reasoning !== "off") {
257
257
  if (anthCompat.thinkingMode === "adaptive") {
258
258
  body.thinking = { type: "adaptive" };
259
259
  }
@@ -264,8 +264,7 @@ export function createAnthropicBrain(config = {}) {
264
264
  delete body.thinking;
265
265
  }
266
266
  else {
267
- const tierShare = isThinkingLevel(options.reasoning) ? reasoningBudgetShare(options.reasoning) : undefined;
268
- const share = config.thinkingBudgetShare ?? tierShare ?? 0.5;
267
+ const share = config.thinkingBudgetShare ?? reasoningBudgetShare(options.reasoning);
269
268
  const { max, budget } = thinkingBudget(body.max_tokens, share, config.thinkingBudgetTokens, hardCap);
270
269
  body.max_tokens = max;
271
270
  body.thinking = { type: "enabled", budget_tokens: budget };
@@ -12,18 +12,12 @@ interface DegradingBrainBaseOptions {
12
12
  primary: Brain;
13
13
  downgradeOn?: DegradeReason[];
14
14
  }
15
- export type DegradingBrainOptions = (DegradingBrainBaseOptions & {
15
+ export type DegradingBrainOptions = DegradingBrainBaseOptions & {
16
16
  fallbacks: Array<{
17
17
  brain: Brain;
18
18
  model: Model;
19
19
  }>;
20
- fallback?: never;
21
- fallbackModel?: never;
22
- }) | (DegradingBrainBaseOptions & {
23
- fallbacks?: never;
24
- fallback: Brain;
25
- fallbackModel: Model;
26
- });
20
+ };
27
21
  export declare function readDegradation(msg: {
28
22
  diagnostics?: AssistantMessageDiagnostic[];
29
23
  } | undefined): DegradationInfo | undefined;
@@ -115,12 +115,12 @@ export function passthroughStream(brain, model, context, options) {
115
115
  return out;
116
116
  }
117
117
  export function createDegradingBrain(opts) {
118
- const triggers = new Set(opts.downgradeOn ?? (opts.fallbacks ? ["rate_limit", "breaker_open", "server_error", "last_resort"] : ["rate_limit", "breaker_open"]));
119
- const rawHops = opts.fallbacks ?? (opts.fallback && opts.fallbackModel ? [{ brain: opts.fallback, model: opts.fallbackModel }] : []);
118
+ const triggers = new Set(opts.downgradeOn ?? ["rate_limit", "breaker_open", "server_error", "last_resort"]);
119
+ const rawHops = opts.fallbacks ?? [];
120
120
  const seenIds = new Set();
121
121
  const hops = rawHops.filter((h) => (seenIds.has(h.model.id) ? false : (seenIds.add(h.model.id), true))).slice(0, MAX_CHAIN);
122
122
  if (hops.length === 0) {
123
- throw new Error("createDegradingBrain: provide `fallbacks` (design/126 chain) or the legacy `fallback`+`fallbackModel` pair");
123
+ throw new Error("createDegradingBrain: `fallbacks` needs at least one {brain, model} hop (RB-482 #3: the legacy fallback+fallbackModel pair is retired — a chain-of-one is `fallbacks: [{brain, model}]`)");
124
124
  }
125
125
  const stream = (model, context, options) => {
126
126
  if (options?.resilience?.allowDegrade === false) {
@@ -1,12 +1,6 @@
1
- import { DEGENERATE_MESSAGE } from "./repetition.js";
2
- import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
3
1
  export function isDegenerateCutMessage(message) {
4
- if (message.errorKind === "degenerate")
5
- return true;
6
- return message.errorMessage === DEGENERATE_MESSAGE;
2
+ return message.errorKind === "degenerate";
7
3
  }
8
4
  export function isWalltimeCutMessage(message) {
9
- if (message.errorKind === "walltime_cutoff")
10
- return true;
11
- return message.errorMessage === WALLTIME_CUTOFF_MESSAGE;
5
+ return message.errorKind === "walltime_cutoff";
12
6
  }
@@ -177,7 +177,7 @@ export async function runToolGate(input) {
177
177
  if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
178
178
  decision = {
179
179
  action: "deny",
180
- reason: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
180
+ message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
181
181
  "facility, or the suspend failed) and a synchronous permission approver cannot carry an answer — " +
182
182
  "the question was NOT shown to anyone. Proceed with your best judgment and state the assumption " +
183
183
  "you made.",
@@ -211,7 +211,7 @@ export async function runToolGate(input) {
211
211
  }
212
212
  catch (err) {
213
213
  traceHookCrash(input, err, notifier);
214
- editDenied = { action: "deny", reason: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
214
+ editDenied = { action: "deny", message: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
215
215
  denySource = "hook";
216
216
  break;
217
217
  }
@@ -219,7 +219,7 @@ export async function runToolGate(input) {
219
219
  if (hr.additionalContext)
220
220
  preToolContext.push(hr.additionalContext);
221
221
  if (hr.action === "deny") {
222
- editDenied = { action: "deny", reason: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
222
+ editDenied = { action: "deny", message: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
223
223
  denySource = "hook";
224
224
  break;
225
225
  }
@@ -244,7 +244,7 @@ export async function runToolGate(input) {
244
244
  catch (err) {
245
245
  recheck = {
246
246
  action: "deny",
247
- reason: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
247
+ message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
248
248
  };
249
249
  }
250
250
  if (recheck.action === "deny") {
package/dist/core/lsp.js CHANGED
@@ -171,18 +171,17 @@ export function createLspTool(manager, opts = {}) {
171
171
  line: Type.Optional(Type.Number({ description: "The line number (1-based, as shown in editors)." })),
172
172
  character: Type.Optional(Type.Number({ description: "The character offset (1-based, as shown in editors)." })),
173
173
  query: Type.Optional(Type.String({ description: "Symbol query for workspaceSymbol (may be empty to browse broadly)." })),
174
- symbol: Type.Optional(Type.String({ description: "Deprecated alias for query." })),
175
174
  }),
176
175
  effect: "read",
177
176
  execute: async (args, ctx) => {
178
- const { operation, filePath, line, character, query, symbol } = args;
177
+ const { operation, filePath, line, character, query } = args;
179
178
  if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
180
179
  return errorResult(`Error (LSP): the "${operation}" operation needs both line and character.`);
181
180
  }
182
181
  if (POSITION_OPS.has(operation) && (!Number.isInteger(line) || line < 1 || !Number.isInteger(character) || character < 1)) {
183
182
  return errorResult(`Error (LSP): line and character are 1-based integers (as shown in editors); got line=${line}, character=${character}.`);
184
183
  }
185
- const wsQuery = query ?? symbol;
184
+ const wsQuery = query;
186
185
  if (operation === "workspaceSymbol" && wsQuery === undefined) {
187
186
  return errorResult(`Error (LSP): the "workspaceSymbol" operation needs a query.`);
188
187
  }
@@ -56,7 +56,7 @@ export interface McpServerStatus {
56
56
  error?: string;
57
57
  transportClosed?: boolean;
58
58
  }
59
- export declare const MCP_PREFIX = "mcp__";
59
+ export declare const MCP_PREFIX: string;
60
60
  export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
61
61
  export declare function gateMcpOutput(content: Array<TextContent | ImageContent>, limitTokens?: number): Array<TextContent | ImageContent>;
62
62
  export declare function truncateMcpErrorText(s: string): string;
package/dist/core/mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { MCP_NAMESPACE } from "./protocol-table.js";
2
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
5
  import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -12,7 +13,7 @@ import { truncateError } from "./tool-errors.js";
12
13
  import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
13
14
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
14
15
  const NAME_SEP = "__";
15
- export const MCP_PREFIX = "mcp__";
16
+ export const MCP_PREFIX = MCP_NAMESPACE.prefix;
16
17
  const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
17
18
  const MCP_CHARS_PER_TOKEN = 4;
18
19
  const MCP_META_RESULT_SIZE_CAP = 500_000;
@@ -6,7 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
6
6
  import { formatMemoryAge } from "../memory-recall.js";
7
7
  import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
8
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
9
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
9
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptLegacyRepoDirs, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
10
10
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
11
11
  export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
12
12
 
@@ -61,7 +61,7 @@ export class MemoryEngine {
61
61
  this.controlDir =
62
62
  typeof backendControl === "string" && backendControl
63
63
  ? backendControl
64
- : (opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), opts.memoryDir));
64
+ : (opts.controlDir ?? (adoptLegacyRepoDirs(resolveMemoryEngineRoot(), opts.memoryDir), deriveControlPlaneDir(resolveMemoryEngineRoot(), opts.memoryDir)));
65
65
  this.now = opts.now ?? Date.now;
66
66
  this.perScopeBudgetBytes = opts.perScopeBudgetBytes ?? Number.MAX_SAFE_INTEGER;
67
67
  this.perFileBytes = opts.perFileBytes ?? MAX_MEMORY_BYTES;
@@ -5,7 +5,7 @@ import { jaccardDistance, termSet } from "../memory-vector.js";
5
5
  import { MAX_MEMORY_BYTES } from "../memory.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { computeEntryRev, entryFromFile, isValidEntryId, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
- import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, claimRootScope, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirName, writeAllSync, } from "./layout.js";
8
+ import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, claimRootScope, adoptLegacyRepoDirs, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirName, writeAllSync, } from "./layout.js";
9
9
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
10
10
  export const MEMORY_INDEX_FILENAME = "MEMORY.md";
11
11
  export const DEFAULT_MAX_ENTRY_DEPTH = 3;
@@ -78,6 +78,8 @@ export class FileMemoryEngineBackend {
78
78
  constructor(dir, opts = {}) {
79
79
  this.now = opts.now ?? Date.now;
80
80
  this.directoryRoot = dir;
81
+ if (opts.controlDir === undefined)
82
+ adoptLegacyRepoDirs(resolveMemoryEngineRoot(), dir);
81
83
  this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
82
84
  ensureDirExists(dir);
83
85
  ensureDirExists(this.controlPlaneRoot);
@@ -6,6 +6,9 @@ export declare class ControlPlaneCorruptError extends Error {
6
6
  });
7
7
  }
8
8
  export declare function deriveRepoKey(repoRoot: string): string;
9
+ export declare function adoptLegacyRepoDirs(configRoot: string, repoRoot: string, onIncident?: (err: Error & {
10
+ code?: string;
11
+ }) => void): void;
9
12
  export declare function deriveRepoMemoryDir(configRoot: string, repoRoot: string): string;
10
13
  export declare function deriveProjectMemoryDir(configRoot: string, projectId: string): string;
11
14
  export declare function deriveProjectControlDir(configRoot: string, projectId: string): string;
@@ -11,8 +11,59 @@ export class ControlPlaneCorruptError extends Error {
11
11
  }
12
12
  }
13
13
  export function deriveRepoKey(repoRoot) {
14
+ return canonicalize(resolve(repoRoot)).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-");
15
+ }
16
+ function deriveRepoKeyLegacy(repoRoot) {
14
17
  return resolve(repoRoot).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-");
15
18
  }
19
+ export function adoptLegacyRepoDirs(configRoot, repoRoot, onIncident) {
20
+ const disclose = (code, message) => {
21
+ try {
22
+ const e = new Error(message);
23
+ e.code = code;
24
+ onIncident?.(e);
25
+ }
26
+ catch {
27
+ }
28
+ };
29
+ try {
30
+ const newKey = deriveRepoKey(repoRoot);
31
+ const oldKey = deriveRepoKeyLegacy(repoRoot);
32
+ if (newKey === oldKey)
33
+ return;
34
+ const oldDir = join(configRoot, oldKey);
35
+ const newDir = join(configRoot, newKey);
36
+ const exists = (p) => {
37
+ try {
38
+ statSync(p);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ };
45
+ if (!exists(oldDir))
46
+ return;
47
+ if (exists(newDir)) {
48
+ disclose("memory.partition_split", `memory partition split detected for ${repoRoot}: BOTH the legacy alias-keyed dir (${oldDir}) and the canonical dir (${newDir}) exist. ` +
49
+ `This session uses the canonical dir; the legacy dir is left untouched (never merged — CAS baselines would collide). ` +
50
+ `If the legacy side holds newer memory, move its rows by hand and remove it.`);
51
+ return;
52
+ }
53
+ try {
54
+ renameSync(oldDir, newDir);
55
+ }
56
+ catch (err) {
57
+ const code = err.code;
58
+ if ((code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") && exists(newDir))
59
+ return;
60
+ disclose("memory.partition_adopt_failed", `could not adopt the legacy memory partition ${oldDir} → ${newDir} (${code ?? String(err)}): this session proceeds on the canonical dir; ` +
61
+ `the legacy dir still holds the prior memory — move it by hand.`);
62
+ }
63
+ }
64
+ catch {
65
+ }
66
+ }
16
67
  export function deriveRepoMemoryDir(configRoot, repoRoot) {
17
68
  return join(configRoot, deriveRepoKey(repoRoot), "memory");
18
69
  }
@@ -91,7 +91,6 @@ export interface NormalizedMemorySpec {
91
91
  scopeContract?: "v2";
92
92
  }
93
93
  export type MemorySpecInput = {
94
- scope?: string;
95
94
  scopes?: ReadonlyArray<string>;
96
95
  writeScope?: string | null;
97
96
  enabled?: boolean;
@@ -354,7 +354,7 @@ function mtimeMsOf(ts) {
354
354
  export function normalizeMemorySpec(input) {
355
355
  if (!input)
356
356
  return undefined;
357
- const raw = input.scopes && input.scopes.length > 0 ? input.scopes : input.scope !== undefined ? [input.scope] : [];
357
+ const raw = input.scopes ?? [];
358
358
  const scopes = [];
359
359
  const seen = new Set();
360
360
  for (const s of raw) {
@@ -1,3 +1,4 @@
1
+ import { MCP_NAMESPACE } from "./protocol-table.js";
1
2
  export const BASH_GENERIC_PARAMS = new Set(["command", "timeout", "description", "run_in_background"]);
2
3
  const DEFAULT_CAPS = {
3
4
  maxRules: 256,
@@ -95,7 +96,7 @@ const FILE_TOOL_PARAMS = {
95
96
  Write: new Set(["file_path", "path", "content"]),
96
97
  NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
97
98
  Grep: new Set([
98
- "pattern", "path", "glob", "type", "output_mode", "head_limit", "max_results", "offset",
99
+ "pattern", "path", "glob", "type", "output_mode", "head_limit", "offset",
99
100
  "multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
100
101
  ]),
101
102
  Glob: new Set(["pattern", "path", "max_results"]),
@@ -142,7 +143,7 @@ function compile(rules, caps, primaryFieldGeneric) {
142
143
  }
143
144
  }
144
145
  const toolName = parsed.toolName;
145
- if (toolName.startsWith("mcp__") && (parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1)) {
146
+ if (toolName.startsWith(MCP_NAMESPACE.prefix) && (parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1)) {
146
147
  bad(text, "unsupported.mcp_paren", "MCP rules do not support patterns in parentheses (CC parity); use the toolAxes system");
147
148
  continue;
148
149
  }
@@ -274,18 +275,18 @@ export function createPermissionRulePolicy(rules, opts) {
274
275
  const entry = byTool.get(req.toolName);
275
276
  if (entry) {
276
277
  if (entry.bare.deny) {
277
- return { action: "deny", reason: ruleMessage("denied", entry.bare.deny.ruleText, entry.bare.deny.source) };
278
+ return { action: "deny", message: ruleMessage("denied", entry.bare.deny.ruleText, entry.bare.deny.source) };
278
279
  }
279
280
  const paramDeny = matchParamRules(entry.param.deny, req.args, caps.maxScalarValueChars);
280
281
  if (paramDeny) {
281
- return { action: "deny", reason: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
282
+ return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
282
283
  }
283
284
  if (entry.bare.ask) {
284
- return { action: "ask", reason: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
285
+ return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
285
286
  }
286
287
  const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
287
288
  if (paramAsk) {
288
- return { action: "ask", reason: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
289
+ return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
289
290
  }
290
291
  if (entry.bare.allow) {
291
292
  return { action: "allow" };
@@ -293,7 +294,7 @@ export function createPermissionRulePolicy(rules, opts) {
293
294
  }
294
295
  if (defaultAction === "allow")
295
296
  return { action: "allow" };
296
- return { action: defaultAction, reason: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
297
+ return { action: defaultAction, message: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
297
298
  },
298
299
  };
299
300
  }
@@ -0,0 +1,14 @@
1
+ export type ProtocolId = "mcp";
2
+ export interface ProtocolNamespace {
3
+ readonly id: ProtocolId;
4
+ readonly prefix: string;
5
+ makeName(peer: string, tool: string): string;
6
+ parse(name: string): {
7
+ peer: string;
8
+ tool: string;
9
+ } | undefined;
10
+ displayGroupKey(name: string): string;
11
+ }
12
+ export declare const MCP_NAMESPACE: ProtocolNamespace;
13
+ export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
14
+ export declare function protocolOf(name: string): ProtocolNamespace | undefined;