@sema-agent/server 7.83.0 → 7.85.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.
package/USAGE.md CHANGED
@@ -669,8 +669,12 @@ READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitiv
669
669
  「读工作区外的文件」从被拒变成放行。**要钉回收敛姿态:显式 `READ_FACE=roots`。**
670
670
  · **多租户**(`REQUIRE_PRINCIPAL=true`):缺席仍**不铸**、不传给引擎(引擎默认 `roots`)——**一个字节不变**。
671
671
  · `READ_DENY_PATTERNS` / `READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE` 三键的「缺席不铸」
672
- **不变**;**内建读拒表照旧生效** —— `open` 放开的是「工作区外」这道围栏,凭证 / shell 历史 / 浏览器 /
673
- 钱包那几档 deny 在 `open` 面上照样拦(两者在引擎里是并列机制)。
672
+ **不变**。⚠️ **但「不铸」在引擎里的读法自 core 7.22.0(内置读 deny 表出厂 OFF 的裁定)起翻了面**:`READ_DENY_BUILTIN_TIERS`
673
+ 未设 = **OFF**(内建表一行不生效),此前是「除 shell-history 外四档全开」。⇒ 一台没配过这根 env 的机器
674
+ 升级后**读得到**根内的 `.npmrc` / `.ssh` / `.aws` / `.claude/settings.json` 这类名字,且整个递归读族
675
+ 零卡执行。**要老姿态就显式写** `READ_DENY_BUILTIN_TIERS=credentials,browser,wallet,agent-config`
676
+ (运维面全文见 `docs/DEPLOY-PREREQS.md` 的 core 7.22.0 段)。`open` 与这张表仍是**两个并列机制**:
677
+ `open` 放开的是「工作区外」那道围栏,内建表判的是名字 —— 只是后者现在默认空着。
674
678
  · 「这一档到底是谁定的」由 operator 面 `GET /v1/diagnostics/wiring` 的 `readFace` 段答:
675
679
  `{face, source, note}`,`source` ∈ `env`(本机 env 显式)/ `center`(组织下发)/ `posture`(单机派生)/
676
680
  `engine-default`(没钉,引擎当家),`note` 是指路句。
@@ -290,6 +290,13 @@ export type ReadRootCandidateProjection = ReadRootGrantCandidate;
290
290
  * 🔴 形不合 ⇒ **按缺席处置**;超限 ⇒ **整只丢**(见 {@link MAX_READ_ROOT_CANDIDATE_DIR})。
291
291
  * 🔴 **缺席不是断言**:它同时覆盖「这一族 ask 没有任何目录能清掉」(敏感路径 deny 行、读边界压根读不懂
292
292
  * 的命令、递归遍历、非读边界 ask……)与「老引擎」两形 —— 消费端只读在场,永远不读缺席。
293
+ *
294
+ * 🔴 **逐成员挑键的代价,与它的解药**(core 7.22.0 #862b 真发生):下面这条 `return` 是**逐键重建**的,
295
+ * 而返回型是 core 的 `ReadRootGrantCandidate` ⇒ core 给这只座加可选成员时,新成员被**静默剥掉**且
296
+ * tsc 一声不响(7.22.0 的 `covers` 就是这样被吞过一次:消费端拿到一条看起来正常的「目录出路」,按目录
297
+ * 回授,卡清不掉 —— 而那正是这个成员存在的唯一理由)。解药**不是**改成整只透传(那会让未经校验的远端
298
+ * 形上 wire),而是 `src/trace/core-keyset-guard.ts` ⑭ 的**成员级编译门** `_GuardReadRootCandidate`:
299
+ * core 加成员 ⇒ 那里当场红,由人判该不该投。**改本函数时必须同步那张表。**
293
300
  */
294
301
  export declare function readReadRootCandidate(req: unknown): ReadRootCandidateProjection | undefined;
295
302
  /**
@@ -162,7 +162,7 @@ export function readProbeCause(req) {
162
162
  }
163
163
  export const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
164
164
  const ReadRootCandidateEnvelopeSchema = z.object({
165
- readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true) }).optional(),
165
+ readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true), covers: z.literal("exact").optional() }).optional(),
166
166
  });
167
167
  export function readReadRootCandidate(req) {
168
168
  const parsed = ReadRootCandidateEnvelopeSchema.safeParse(req);
@@ -171,7 +171,7 @@ export function readReadRootCandidate(req) {
171
171
  return undefined;
172
172
  if (raw.dir.length > MAX_READ_ROOT_CANDIDATE_DIR)
173
173
  return undefined;
174
- return { dir: raw.dir, clearsThisAsk: true };
174
+ return { dir: raw.dir, clearsThisAsk: true, ...(raw.covers !== undefined ? { covers: raw.covers } : {}) };
175
175
  }
176
176
  const MAX_EVIDENCE_DOT_ACTOR = 256;
177
177
  const MAX_EVIDENCE_DOTS = 16;
@@ -25,6 +25,7 @@ import { gateExecutionLane } from "../runtime-caps-resolver.js";
25
25
  import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
26
26
  import { HttpError, encodeCheckpointScope } from "../security.js";
27
27
  import { memorySpecForRequest } from "../memory-scope.js";
28
+ import { singleMemberOffFieldIssue } from "../request-value-closure.js";
28
29
  import { a2aForScenario, mcpForScenario } from "../config-center/facade.js";
29
30
  import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolMaterializeStrategyFromBody, toolNameListFromBody } from "../spec-fields.js";
30
31
  import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
@@ -36,6 +37,7 @@ import { redactSecrets } from "../trace/redact.js";
36
37
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
37
38
  import { executionLaneHomeDir } from "../execution-lane-caps.js";
38
39
  import { effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
40
+ import { memoryFaceMounted } from "../memory-scope.js";
39
41
  export function createResolveSpec(ctx) {
40
42
  const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
41
43
  assertGuardPatternsUsable(config);
@@ -59,9 +61,9 @@ export function createResolveSpec(ctx) {
59
61
  throw new HttpError(400, `cwd "${conflict.cwd.asSpelled}" is inside this session's scratchpad "${conflict.scratchpadDir.asSpelled}" — the scratchpad is the fs-write gate's EXEMPT directory, so a task rooted there would auto-approve every write under its own root and the engine refuses to build the gate at all (config.write_exempt_dir_covers_root). Point cwd at the project directory instead (or send no cwd and let the session keep the one it has): this session registers the cwd you send, so a task that happens not to mount the write gate would still leave every LATER task on this session rooted in the exemption. A workflow that really is meant to write without prompts says so with a knob that means it (a permission mode, an open write face) and still keeps its root outside the scratchpad — those knobs do not lift this refusal, because the conflict is between two fields of this request, not a permission level.`, { code: "request.field_conflict", extra: { cwd: conflict.cwd, scratchpadDir: conflict.scratchpadDir } });
60
62
  }
61
63
  }
62
- const rawMemoryCapture = body.memoryCapture;
63
- if (rawMemoryCapture !== undefined && rawMemoryCapture !== "off") {
64
- throw new HttpError(400, "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)", { code: "request.field_invalid" });
64
+ const offFieldIssue = singleMemberOffFieldIssue(body);
65
+ if (offFieldIssue) {
66
+ throw new HttpError(400, offFieldIssue.message, { code: "request.field_invalid" });
65
67
  }
66
68
  assertRequestA2aUnlocked(body.a2aPeers, lockedKeys);
67
69
  const gated = await gateScenarioAndAppend(body, auth, opts);
@@ -373,7 +375,9 @@ export function createResolveSpec(ctx) {
373
375
  return { ...(Object.keys(merged).length > 0 ? { limits: merged } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
374
376
  })(),
375
377
  ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
376
- memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, body.memoryCapture) : undefined,
378
+ memory: memoryEngine
379
+ ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
380
+ : undefined,
377
381
  ...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(effectiveMemoryPersistenceCapable(config)),
378
382
  tools: ((base) => {
379
383
  const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
@@ -533,7 +537,7 @@ export function createResolveSpec(ctx) {
533
537
  governed = { ...governed, tools: stripDelegationTools(governed.tools) };
534
538
  }
535
539
  if (shouldDiscloseNoPersistentMemory({
536
- memory: governed.memory,
540
+ memoryFaceMounted: memoryFaceMounted(governed.memory),
537
541
  declaredCapable: effectiveMemoryPersistenceCapable(config),
538
542
  hands: gated.hands,
539
543
  handsReadOnly: governed.handsReadOnly === true,
@@ -1,4 +1,4 @@
1
- import { defaultTaskRegistry } from "@sema-agent/core";
1
+ import { autoRunOnRecommendationEffective, defaultTaskRegistry } from "@sema-agent/core";
2
2
  import { posIntEnv } from "../session-watch.js";
3
3
  import { brainSummary } from "../brain.js";
4
4
  import { resolveBindHost } from "../config.js";
@@ -82,6 +82,7 @@ export async function runStage10(ctx) {
82
82
  memoryCompliance: memoryComplianceFaces,
83
83
  memoryOriginFace: memoryOriginFaces,
84
84
  sessionMemoryStatus: sessionMemoryStatusFace,
85
+ memoryAutoConsolidationArmed: autoRunOnRecommendationEffective(runnerDeps),
85
86
  memoryConsolidation: memoryConsolidationFaces,
86
87
  memoryOptOutGrant: memoryOptOutGrantStore,
87
88
  orgMemoryDirectory: orgMemoryAdmission.directory,
@@ -18,7 +18,7 @@
18
18
  * roster 把落盘路**整批**卸掉时,手带在场也等于没有写通道),但负控仍然成立:必须**全部**已知落盘路
19
19
  * 都不在才算断链 —— `excludeTools:["Write"]` 不算(Bash/Edit 还在),请求面摘一个工具名改不动结论。
20
20
  */
21
- import { type PromptProvider, type TaskSpec } from "@sema-agent/core";
21
+ import { type PromptProvider } from "@sema-agent/core";
22
22
  import type { ScenarioHands } from "./hands-lane.js";
23
23
  /**
24
24
  * 「已知落盘路」的工具名闭集 —— 与 core 的 `rosterCanPersist` 同口径:**文件写工具**(Write/Edit/
@@ -35,9 +35,11 @@ export declare const KNOWN_PERSISTENCE_TOOL_NAMES: readonly ["Write", "Edit", "N
35
35
  export declare const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
36
36
  /** 判别式的全部输入 —— 每一位都是**部署/装配期事实**,没有一位来自请求体的自我表态。 */
37
37
  export interface MemoryFaceFacts {
38
- /** 本次任务最终的 `TaskSpec.memory`。`undefined` = 这一腿根本没有引擎记忆面(引擎未接、多租户 dark、
39
- * remote lane 平面分裂 fail-closed……三条路殊途同归:core 不会 prepare 记忆,也就不会挂任何披露)。 */
40
- memory: TaskSpec["memory"];
38
+ /** 这一腿挂不挂记忆面 —— 由唯一属主 `memory-scope.ts` `memoryFaceMounted(spec.memory)` 算出(与 core `prepareMemory`
39
+ * 同判:`normalizeMemorySpec(...)?.enabled === true`)。`false` = 引擎未接 / 多租户 dark / remote lane fail-closed /
40
+ * S-405 `memory:"off"`(键在场但 `enabled:false`)—— 殊途同归:core 不会 prepare 记忆,也就不会挂任何披露。
41
+ * 🔴 不再收 `TaskSpec.memory` 本体:按「键在不在场」判是 7.84.0 前的病(合并复审 车HF F1)。 */
42
+ memoryFaceMounted: boolean;
41
43
  /** 部署声明 `MEMORY_PERSISTENCE_CAPABLE`(三态)。`true` = operator 自证有推断看不见的持久通道
42
44
  * (自定义 writer / MCP 记忆服务)⇒ 披露会变成假话,撤回;`false` = 强制披露;缺席 ⇒ 走推断。 */
43
45
  declaredCapable: boolean | undefined;
@@ -9,7 +9,7 @@ function anyPersistenceToolSurvives(excludeTools) {
9
9
  return KNOWN_PERSISTENCE_TOOL_NAMES.some((name) => !excluded.has(name));
10
10
  }
11
11
  export function shouldDiscloseNoPersistentMemory(facts) {
12
- if (facts.memory !== undefined)
12
+ if (facts.memoryFaceMounted)
13
13
  return false;
14
14
  if (facts.declaredCapable !== undefined)
15
15
  return !facts.declaredCapable;
@@ -342,7 +342,7 @@ export const CONFIG_CATALOG = [
342
342
  r("QUESTION_TTL_MS", "approval", "number", "AskUserQuestion 无人应答释放窗(ms)", { derivedDefaultNote: "缺席 ⇒ 出厂窗 300000" }),
343
343
  r("RATE_LIMIT_RPM", "limitsHttp", "number", "计费提交每分钟限流(0=关)", { staticDefault: "0", danger: BILL, configKey: "rateLimitPerMin", center: cLimits("rateLimitPerMin") }),
344
344
  r("READ_DENY_BUILTIN_EXCLUDE", "orchestration", "csv", "按行名删内建 deny 条目(档位之后生效;绝不削任务显式 deny)", { danger: SEC, configKey: "readDenyBuiltinExclude", center: cRead("denyBuiltinExclude", "readDenyBuiltinExclude") }),
345
- r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集五档 credentials/shell-history/browser/wallet/agent-config;none=显式空姿态)", { danger: SEC, configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
345
+ r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集五档 credentials/shell-history/browser/wallet/agent-config;none=显式空姿态)", { danger: SEC, derivedDefaultNote: "🔴 缺席 ⇒ **OFF**(core 7.22.0 / C-R60 起内建表出厂全关:根内凭据名可读、整个递归读族零卡)。要 7.21.x 姿态就显式写 credentials,browser,wallet,agent-config;空值拒启(手滑与显式意图不许同形)", configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
346
346
  r("READ_DENY_PATTERNS", "orchestration", "csv", "READ 面额外 deny pattern 表", { danger: SEC, configKey: "readDenyPatterns", center: cRead("denyPatterns", "readDenyPatterns") }),
347
347
  r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 按 posture 派生(S-167):单用户 turnkey(REQUIRE_PRINCIPAL 未设)铸 open;多租户不铸键、core 默认 roots。生效来源见 GET /v1/diagnostics/wiring 的 readFace.source 四词", configKey: "readFace", center: cRead("face", "readFace") }),
348
348
  r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
@@ -1740,7 +1740,11 @@ export interface ServiceConfigFlat {
1740
1740
  * 行名的是**文件**不是配置**目录**(`.sema`/`.claude` 底下还住着 agents/skills/hooks 与记忆库 —— 模型
1741
1741
  * 当作日常材料读写的东西);同族工具的 token **存储**(`.credentials.json`/`.codex/auth.json` …)归
1742
1742
  * `credentials` 档,不归本档 ⇒ 为排查 agent 配置而关掉本档的部署,凭证行照样拦着。
1743
- * 缺席=不铸 ⇒ core 自默认(除 shell-history 外全开)。**仅 deployment 席**:TaskSpec 上没有同名键,
1743
+ * 🔴 **缺席 = 不铸 ⇒ OFF**(core 7.22.0 / [ref] 翻面;7.21.x 及以前是「除 shell-history 外全开」)。
1744
+ * 未配置的部署从内建表里**什么都不拒**,而且 shell 读边界的 deny 判官席 present-iff 编译集有条目 ⇒
1745
+ * 递归读降级臂也随之不接线(`grep -r` 一族零卡)。要老姿态**必须显式写下**
1746
+ * `credentials,browser,wallet,agent-config`。本仓**不复制上游缺省**:这里只记「不铸」,解析归引擎。
1747
+ * **仅 deployment 席**:TaskSpec 上没有同名键,
1744
1748
  * 任务/受治脚本无法据此放宽自己的部署(core 侧的设计,与本仓「部署级旋钮无条件施加」同向)。 */
1745
1749
  readDenyBuiltinTiers?: readonly string[];
1746
1750
  /** [ref] 件 G5:按**行名**(内建表的 canonical pattern)删内建条目,在档位选择**之后**生效。
@@ -3,6 +3,7 @@ import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
3
3
  import { HttpError } from "../security.js";
4
4
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
5
5
  import { taskBodyKeyIssue } from "../request-key-closure.js";
6
+ import { singleMemberOffFieldIssue } from "../request-value-closure.js";
6
7
  import { selfOrchestrationDenial } from "../task-workflow.js";
7
8
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
8
9
  import { parseHooksConfig } from "../hooks/hook-runner.js";
@@ -307,9 +308,12 @@ export function createAdmission(ctx) {
307
308
  sendError(res, 400, "request.field_invalid", "memoryWrite must be a boolean (false = pause memory writes for this run)");
308
309
  return null;
309
310
  }
310
- if (body.memoryCapture !== undefined && body.memoryCapture !== "off") {
311
- sendError(res, 400, "request.field_invalid", "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)");
312
- return null;
311
+ {
312
+ const offFieldIssue = singleMemberOffFieldIssue(body);
313
+ if (offFieldIssue) {
314
+ sendError(res, 400, "request.field_invalid", offFieldIssue.message);
315
+ return null;
316
+ }
313
317
  }
314
318
  if (body.requireExistingSession !== undefined && typeof body.requireExistingSession !== "boolean") {
315
319
  sendError(res, 400, "request.field_invalid", "requireExistingSession must be a boolean");
@@ -549,6 +549,22 @@ export declare function acceptedResumingReceipt(taskId: string, sessionId: strin
549
549
  status: number;
550
550
  body: object;
551
551
  };
552
+ /**
553
+ * 「**证得出**这条 run 活在别的副本上」——「他副本」那一句话的**唯一判据**(S-381 收编)。
554
+ *
555
+ * 成案:这个合取式此前在四处**逐字手抄**(`routes/runs.ts` 的 steer 臂与 interrupt 臂、同文件
556
+ * `sendHandleMiss`、`routes/run-memory-capture.ts`),第五处就要随 `POST /v1/sessions/:id/mcp/reconnect`
557
+ * 出现 —— 而它的每一个合取项都是一条**被复审逐条挣出来的**纪律,不是随手的判空:
558
+ * · `run.instanceId` 缺席(`null` / 建列之前的行 / 替身行的 `undefined`)= **不知道它在哪**,不是「在别处」;
559
+ * · `thisInstanceId` 缺席 = 本副本连自己是谁都说不上,更不能断言别人;
560
+ * · 两者都在场且**不等**才够格说「他副本」(`markResuming` 认领后该列语义 = **当前持有副本**)。
561
+ * 抄第五份 = 第五个写者;收成一只谓词之后,纪律改一次就四处同改。**`status` 不进本谓词**:
562
+ * `sendHandleMiss` 只对 `running` 行说这句话,而 steer 族在 park/终态两臂之后才走到这里 —— 各调用点
563
+ * 的前置臂不同族,合进来会让某一处悄悄改判。
564
+ */
565
+ export declare function runProvenElsewhere(run: {
566
+ instanceId?: string | null;
567
+ }, thisInstanceId: string | undefined): boolean;
552
568
  export declare const RUN_NOT_FOUND_MESSAGE = "run not found \u2014 the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
553
569
  /** [ref] 前缀=域判别子既成契约:a\* 与 wa\* 形 id 给 TaskOutput/journal 指路;其余给 {@link RUN_NOT_FOUND_MESSAGE}。 */
554
570
  export declare function runNotFoundMessage(id: string): string;
@@ -24,6 +24,9 @@ export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "DELETE"];
24
24
  export function acceptedResumingReceipt(taskId, sessionId) {
25
25
  return { status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } };
26
26
  }
27
+ export function runProvenElsewhere(run, thisInstanceId) {
28
+ return typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId;
29
+ }
27
30
  export const RUN_NOT_FOUND_MESSAGE = "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
28
31
  export function runNotFoundMessage(id) {
29
32
  const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
@@ -28,6 +28,7 @@ import { handleNotifyWake, NOTIFY_WAKE_ROUTES } from "./routes/notify-wake.js";
28
28
  import { handleApprovalsAssistant, APPROVALS_ASSISTANT_ROUTES } from "./routes/approvals-assistant.js";
29
29
  import { handleRuns, handleRunVerbs, RUNS_ROUTES, RUN_VERBS_ROUTES } from "./routes/runs.js";
30
30
  import { handleRunMemoryCapture, RUN_MEMORY_CAPTURE_ROUTES } from "./routes/run-memory-capture.js";
31
+ import { handleMcpReconnect, MCP_RECONNECT_ROUTES } from "./routes/mcp-reconnect.js";
31
32
  import { handleSideQuery, SIDE_QUERY_ROUTES } from "./routes/side-query.js";
32
33
  import { handleTasks, TASKS_ROUTES } from "./routes/tasks.js";
33
34
  import { handleLeader, LEADER_ROUTES } from "./routes/leader.js";
@@ -48,6 +49,7 @@ export const ROUTES = [
48
49
  row({ handler: handleWorkflowAgentSteer, decls: WORKFLOW_AGENT_STEER_ROUTES, position: "post-credential", label: "workflow-agent-steer" }),
49
50
  row({ handler: handleRunVerbs, decls: RUN_VERBS_ROUTES, position: "post-credential", label: "run-verbs" }),
50
51
  row({ handler: handleRunMemoryCapture, decls: RUN_MEMORY_CAPTURE_ROUTES, position: "post-credential", label: "run-memory-capture" }),
52
+ row({ handler: handleMcpReconnect, decls: MCP_RECONNECT_ROUTES, position: "post-credential", label: "mcp-reconnect" }),
51
53
  row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader", enabled: (deps) => Boolean(deps.leaderEndpoint) }),
52
54
  row({ handler: handleImages, decls: IMAGES_ROUTES, position: "post-credential", label: "images" }),
53
55
  row({ handler: handleApprovalsAssistant, decls: APPROVALS_ASSISTANT_ROUTES, position: "post-credential", label: "approvals-assistant", enabled: (deps) => Boolean(deps.checkpointStore) }),
@@ -80,6 +80,7 @@ export async function handleCapabilities(req, res, match, ctx) {
80
80
  webSearch: projectWebSearchCapability(deps.webSearchProvider),
81
81
  sessionBackgroundable: Boolean(deps.runStore),
82
82
  runMemoryCaptureOptOut: Boolean(deps.runStore),
83
+ mcpReconnect: Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf),
83
84
  sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
84
85
  adoption: Boolean(deps.backend?.adoptionLog) && deps.config.operatorPrincipals.length > 0,
85
86
  memoryBundle: Boolean(deps.memoryBundleExport && deps.memoryBundleImport) && deps.config.operatorPrincipals.length > 0,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * S-381(core 7.22.0 [ref] 的 server 半场;提货单 [ref] ⓪ @server 第 3 条 / 判据帖 [ref] G1)——
3
+ * **会话内 MCP re-dial** 的 wire 面:
4
+ *
5
+ * `POST /v1/sessions/:id/mcp/reconnect` body `{ server: string }`
6
+ * → 200 `{ taskId, sessionId, server, outcome: "accepted" | "refused" | "unsupported", … }`
7
+ *
8
+ * 语义 = core `TaskStream.reconnectMcpServer(server)`:传输断了的那台 MCP server,在**不结束任务**的前提下
9
+ * 重拨一次号。**一台具名 server,必需,没有全部形**(core 原话:全体形会 CLOSE 掉健康连接,而拥有设备/端口的
10
+ * stdio server 恰恰是这个动词存在的理由)。
11
+ *
12
+ * ── 🔴 一次交易,不是一个「刷新」按钮(core 原话,必须进 UI 文案与契约)───────────────────────────────
13
+ * **失败的重拨让 server 留在断线状态** —— 旧连接在拨新号**之前**就被关(对拥有设备/端口的 stdio server 没有
14
+ * 别的顺序可用),所以一次失败的拨号花掉了一条能用的连接:server 保持断开、**它的工具从模型名册撤走**,在飞
15
+ * 调用随之失败。**在一台健康的 server 上提供「重连」按钮 = 提供一次交易**。契约 §G.7 与本面的 `outcome`
16
+ * 三词都按这句话写;壳不许把它渲成无代价的刷新。
17
+ *
18
+ * ── 三态映射(`unsupported` 是**本仓自己的词**,core 无此词)──────────────────────────────────────
19
+ * · core `reconnected` → `accepted`
20
+ * · core `refused` → `refused`
21
+ * · core `not_declared` → `refused`(未知 server 名是**名册事实**,不是引擎能力事实)
22
+ * · core **抛** `mcp.reconnect_unavailable`(本 run 未声明任何 MCP)→ `unsupported`
23
+ * 🔴 **`outcome` 是唯一判别键**:三态**全部走 200**。HTTP 状态只回答「动词有没有到达引擎」——与 steer 回执的
24
+ * 「branch on `delivery`, not the status」同一条律。把 `refused` 挂到 4xx 会让同一件事有两个判别键,而
25
+ * `unsupported` 挂 409 更会把「这条 run 没有 MCP 面」谎报成一个活性问题。
26
+ *
27
+ * ── 下一步动作按 `status`,不解析 `reason`(core [ref] 键表逐字)────────────────────────────────
28
+ * `status.errorCode` / `status.httpStatus` 是机器面(`http_status` 401 是一扇重新授权的门,`spawn_failed` 是
29
+ * 缺二进制);`reason` 是**人读**的一句话,经本仓 `redactSecrets` + 定长截断后投出(它可能嵌远端文本),
30
+ * 消费端**不许**解析它。名册**按 `tools` 的在场性**投成 `toolNames`(present-iff),**不按 `outcome`**。
31
+ *
32
+ * ── 查找与拒绝形(与 steer / capture-optout **同一只**,不新造)───────────────────────────────────
33
+ * 路径是 session,动词挂在 **run 句柄**上 ⇒ 会话属主门(`sessionStorage.ownerOf`,未知与他人**同码同串**
34
+ * 404 无 oracle,与 `POST /v1/sessions/:id/notify` 逐字同形)→ `getActiveTaskId` 取本会话的活 run →
35
+ * `steerableRuns` 取本副本活流。非 live 的四臂一码 409 `steering.not_running`,判据取词表属主
36
+ * (`isParkedRunStatus` / `isTerminalRunStatus`)与 `runProvenElsewhere`(四处手抄已收编成一只谓词)。
37
+ * 显式 operator 可重拨任何租户的会话(监督动作,与 notify / steer 同臂;**不是** bare `isOperator`)。
38
+ *
39
+ * ── 计费/门 ─────────────────────────────────────────────────────────────────────────────────────
40
+ * 零模型工作(不进 billable;不吃 drain / roster-pending);mutating ⇒ `rateLimited`。能力位 `mcpReconnect`
41
+ * = 持久 run 账面 ∧ 会话属主面(两道 501 的同一批 deps 推导出来,「说 yes 但 501」结构上不可能)。
42
+ */
43
+ import type { IncomingMessage, ServerResponse } from "node:http";
44
+ import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
45
+ /** 本面的三词闭集 —— **`unsupported` 是本仓的词**(core 的三词是 `reconnected`/`refused`/`not_declared`)。 */
46
+ export type McpReconnectOutcome = "accepted" | "refused" | "unsupported";
47
+ export declare function handleMcpReconnect(req: IncomingMessage, res: ServerResponse, match: RouteMatch<McpReconnectRouteId>, ctx: RouteCtx): Promise<void>;
48
+ export declare const MCP_RECONNECT_ROUTES: readonly [{
49
+ readonly id: "mcp-reconnect";
50
+ readonly pattern: RegExp;
51
+ readonly label: "/v1/sessions/:id/mcp/reconnect";
52
+ readonly methods: readonly ["POST"];
53
+ }];
54
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
55
+ export type McpReconnectRouteId = RouteIdsOf<typeof MCP_RECONNECT_ROUTES>;
56
+ //# sourceMappingURL=mcp-reconnect.d.ts.map
@@ -0,0 +1,139 @@
1
+ import { z } from "zod";
2
+ import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
3
+ import { redactSecrets } from "../../trace/redact.js";
4
+ import { sendJson, sendError } from "../send.js";
5
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
+ import { runProvenElsewhere } from "../route-ctx.js";
7
+ import { assertNever } from "../route-ctx.js";
8
+ const BodySchema = z.object({ server: z.string().min(1).max(190) }).strict();
9
+ const NEUTRALIZED_TEXT_MAX = 200;
10
+ const humanText = (s) => redactSecrets(s).slice(0, NEUTRALIZED_TEXT_MAX);
11
+ function outcomeOf(coreOutcome) {
12
+ switch (coreOutcome) {
13
+ case "reconnected": return "accepted";
14
+ case "refused": return "refused";
15
+ case "not_declared": return "refused";
16
+ default: return assertNever(coreOutcome);
17
+ }
18
+ }
19
+ function projectStatus(s) {
20
+ return {
21
+ name: s.name,
22
+ status: s.status,
23
+ ...(s.errorCode !== undefined ? { errorCode: s.errorCode } : {}),
24
+ ...(s.httpStatus !== undefined ? { httpStatus: s.httpStatus } : {}),
25
+ ...(s.delivered !== undefined ? { delivered: s.delivered } : {}),
26
+ ...(s.serverInfo !== undefined ? { serverInfo: s.serverInfo } : {}),
27
+ ...(s.error !== undefined ? { error: humanText(String(s.error)) } : {}),
28
+ ...(s.transportClosed !== undefined ? { transportClosed: s.transportClosed } : {}),
29
+ };
30
+ }
31
+ function projectListingIncomplete(l) {
32
+ return {
33
+ reason: l.reason,
34
+ pages: l.pages,
35
+ ...(l.budgetMs !== undefined ? { budgetMs: l.budgetMs } : {}),
36
+ ...(l.error !== undefined ? { error: humanText(l.error) } : {}),
37
+ };
38
+ }
39
+ function projectResult(r) {
40
+ return {
41
+ server: r.server,
42
+ outcome: outcomeOf(r.outcome),
43
+ prefix: r.prefix,
44
+ toolCount: r.toolCount,
45
+ added: r.added,
46
+ removed: r.removed,
47
+ ...(r.tools !== undefined ? { toolNames: r.tools.map((t) => t.name) } : {}),
48
+ ...(r.reason !== undefined ? { reason: humanText(r.reason) } : {}),
49
+ ...(r.status !== undefined ? { status: projectStatus(r.status) } : {}),
50
+ ...(r.listingIncomplete !== undefined ? { listingIncomplete: projectListingIncomplete(r.listingIncomplete) } : {}),
51
+ };
52
+ }
53
+ export async function handleMcpReconnect(req, res, match, ctx) {
54
+ const { deps } = ctx;
55
+ switch (match.decl.id) {
56
+ case "mcp-reconnect": {
57
+ if (ctx.helpers.rateLimited(req, res))
58
+ return;
59
+ if (!deps.runStore) {
60
+ sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
61
+ return;
62
+ }
63
+ const principal = gatedPrincipal(req, deps.config);
64
+ if (deps.config.requirePrincipal && principal === undefined) {
65
+ sendPrincipalRefusal(res, req, deps.config);
66
+ return;
67
+ }
68
+ const parsed = BodySchema.safeParse((await ctx.helpers.readJson(req)) ?? {});
69
+ if (!parsed.success) {
70
+ sendError(res, 400, "request.body_shape", "body must be { server: string (non-empty, at most 190 characters) } — one NAMED MCP server, required; there is no all-servers form (re-dialing every server would CLOSE the healthy ones, and a stdio server that owns a device cannot be re-dialed while the old connection still holds it)");
71
+ return;
72
+ }
73
+ const server = parsed.data.server;
74
+ const sessionId = ctx.helpers.safeDecode(match.params.sessionId);
75
+ if (sessionId === null) {
76
+ sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
77
+ return;
78
+ }
79
+ if (!deps.sessionStorage?.ownerOf) {
80
+ sendError(res, 501, "capability.session_ownership_required", "the MCP re-dial verb requires the session-ownership face (sessionStorage.ownerOf)");
81
+ return;
82
+ }
83
+ const owner = await deps.sessionStorage.ownerOf(sessionId);
84
+ if (owner === undefined) {
85
+ sendError(res, 404, "not_found.session", "session not found");
86
+ return;
87
+ }
88
+ const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
89
+ if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
90
+ return;
91
+ const activeTaskId = await deps.runStore.getActiveTaskId(sessionId);
92
+ const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
93
+ if (live) {
94
+ try {
95
+ const result = await live.reconnectMcpServer(server);
96
+ sendJson(res, 200, { taskId: activeTaskId, sessionId, ...projectResult(result) });
97
+ return;
98
+ }
99
+ catch (e) {
100
+ const code = e.code;
101
+ const msg = e instanceof Error ? e.message : String(e);
102
+ switch (code) {
103
+ case "mcp.reconnect_unavailable":
104
+ sendJson(res, 200, { taskId: activeTaskId, sessionId, server, outcome: "unsupported", reason: humanText(msg) });
105
+ return;
106
+ case "steering.invalid_content":
107
+ sendError(res, 422, "steering.invalid_content", msg);
108
+ return;
109
+ case "steering.not_running":
110
+ sendError(res, 409, "steering.not_running", "run just finished — the MCP re-dial verb needs a live run; a resumed continuation takes a fresh first listing of every declared server anyway");
111
+ return;
112
+ default: throw e;
113
+ }
114
+ }
115
+ }
116
+ const run = activeTaskId === undefined ? undefined : await deps.runStore.getRun(activeTaskId);
117
+ if (run && isParkedRunStatus(run.status)) {
118
+ sendError(res, 409, "steering.not_running", "run is waiting on a pending decision — the MCP re-dial verb needs a live turn; a resumed continuation dials every declared server afresh (the re-dial applies to the current turn only and is not persisted), so resume this run first — a re-dial cannot be queued behind the decision");
119
+ return;
120
+ }
121
+ if (run && isTerminalRunStatus(run.status)) {
122
+ sendError(res, 409, "steering.not_running", "run already ended — the MCP re-dial verb needs a live run; the next submit of this session materializes every declared MCP server from scratch");
123
+ return;
124
+ }
125
+ if (run && runProvenElsewhere(run, deps.instanceId)) {
126
+ sendError(res, 409, "steering.not_running", "run is live on another replica — the MCP re-dial verb must reach the replica holding the stream (the MCP connections belong to that process); resend it there");
127
+ return;
128
+ }
129
+ sendError(res, 409, "steering.not_running", "no live steerable face for this session on this replica — there is no active run here to re-dial on (a verify or cascade submission exposes none, and an idle session holds no MCP connections at all)");
130
+ return;
131
+ }
132
+ default:
133
+ return assertNever(match.decl.id);
134
+ }
135
+ }
136
+ export const MCP_RECONNECT_ROUTES = [
137
+ { id: "mcp-reconnect", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/mcp\/reconnect$/, label: "/v1/sessions/:id/mcp/reconnect", methods: ["POST"] },
138
+ ];
139
+ //# sourceMappingURL=mcp-reconnect.js.map
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
5
- import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
5
+ import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage, runProvenElsewhere } from "../route-ctx.js";
6
6
  import { assertNever } from "../route-ctx.js";
7
7
  const BodySchema = z.object({ reason: z.string().min(1).max(500).optional() }).strict();
8
8
  export async function handleRunMemoryCapture(req, res, match, ctx) {
@@ -79,7 +79,7 @@ export async function handleRunMemoryCapture(req, res, match, ctx) {
79
79
  sendError(res, 409, "steering.not_running", "run already ended — the memory-capture opt-out verb needs a live run; submit the next turn of this session with memoryCapture: 'off' (the declaration writes the same one-way session record)");
80
80
  return;
81
81
  }
82
- const provenElsewhere = typeof run.instanceId === "string" && run.instanceId.length > 0 && deps.instanceId !== undefined && run.instanceId !== deps.instanceId;
82
+ const provenElsewhere = runProvenElsewhere(run, deps.instanceId);
83
83
  if (provenElsewhere) {
84
84
  sendError(res, 409, "steering.not_running", "run is live on another replica — the memory-capture opt-out verb must reach the replica holding the stream; resend it there, or declare memoryCapture: 'off' on the next submit");
85
85
  return;
@@ -27,7 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } f
27
27
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
28
28
  import { assertNever } from "../route-ctx.js";
29
29
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
30
- import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
30
+ import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage, runProvenElsewhere } from "../route-ctx.js";
31
31
  import { withPrincipal } from "../../observability/principal-context.js";
32
32
  const ASK_PARKING_RETRY_AFTER_SEC = 2;
33
33
  const ASK_ACTOR_LABEL_MAX = 256;
@@ -88,7 +88,7 @@ function sendHandleMiss(res, run, thisInstanceId) {
88
88
  sendError(res, 404, "not_found.task_owner_settled", "no such background task handle here. Routing hint about its OWNER (not about this handle): the run has already settled, and background handles live in the replica-local registry for the parent's lifetime, so none of its handles are readable any more. Read the run's own outcome via GET /v1/runs/:id");
89
89
  return true;
90
90
  }
91
- if (run.status === "running" && typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId) {
91
+ if (run.status === "running" && runProvenElsewhere(run, thisInstanceId)) {
92
92
  sendError(res, 404, "not_found.task_owner_elsewhere", "no such background task handle here. Routing hint about its OWNER (not about this handle): the run is executing on another replica, and this one's registry holds none of its handles. Ask the replica that owns the run, or read the run's own outcome via GET /v1/runs/:id");
93
93
  return true;
94
94
  }
@@ -532,7 +532,7 @@ export async function handleRuns(req, res, match, ctx) {
532
532
  return;
533
533
  }
534
534
  if (run.status === "running") {
535
- const provenElsewhere = typeof run.instanceId === "string" && run.instanceId.length > 0 && deps.instanceId !== undefined && run.instanceId !== deps.instanceId;
535
+ const provenElsewhere = runProvenElsewhere(run, deps.instanceId);
536
536
  if (provenElsewhere) {
537
537
  sendNotRunning("run is live on another replica — a live steer must reach the replica holding the stream; resend it there, or use POST /v1/runs/:id/cancel for a run-level stop");
538
538
  }
@@ -731,7 +731,7 @@ export async function handleRuns(req, res, match, ctx) {
731
731
  return;
732
732
  }
733
733
  if (status === "running") {
734
- const provenElsewhere = typeof run.instanceId === "string" && run.instanceId.length > 0 && deps.instanceId !== undefined && run.instanceId !== deps.instanceId;
734
+ const provenElsewhere = runProvenElsewhere(run, deps.instanceId);
735
735
  if (provenElsewhere) {
736
736
  sendError(res, 409, "interrupt.not_held", "run is live on another replica — interrupt must reach the replica holding the stream; resend it there, or use POST /v1/runs/:id/cancel for a run-level stop");
737
737
  }
@@ -544,6 +544,7 @@ export async function handleSessions(req, res, match, ctx) {
544
544
  const s = await entry.status;
545
545
  sendJson(res, 200, {
546
546
  sessionId,
547
+ autoConsolidationArmed: deps.memoryAutoConsolidationArmed,
547
548
  ...(s.captureOptedOut !== undefined ? { captureOptedOut: s.captureOptedOut } : {}),
548
549
  ...(s.committedCount !== undefined ? { committedCount: s.committedCount } : {}),
549
550
  ...(s.foldedCount !== undefined ? { foldedCount: s.foldedCount } : {}),
@@ -417,6 +417,20 @@ export interface ServiceSeamDeps {
417
417
  sessionMemoryStatus?: {
418
418
  status: (sessionId: string) => Promise<SessionMemoryStatus>;
419
419
  };
420
+ /**
421
+ * S-403(core 7.22.0 [ref] W1)—— 本**部署**的自动整理武装事实,`GET /v1/sessions/:id/memory-status`
422
+ * 的 `autoConsolidationArmed` 格。
423
+ *
424
+ * 🔴 **唯一读法**:值必须由 core 的 `autoRunOnRecommendationEffective(RunnerDeps)` 组
425
+ * (契约 `memory.auto_consolidation.arming_has_one_resolver`),在 **boot 期算一次**
426
+ * (`boot/stage-10-http-server.ts` 单点)。`RunnerDeps.autoRunOnRecommendation` 的**缺席不是「关」**
427
+ * (core 7.21.1 起具名缺省是开,且只在 `memoryConsolidation` ∧ 可解整理模型席都接线时才真武装)——
428
+ * 所以「按席在不在场判」与「按席值判」**两个方向都错**,本仓不自铸第二套派生。
429
+ * 🔴 **必填,不是可选**:它是一个纯函数在 boot 期的答案,任何装配点都算得出;做成可选就等于在
430
+ * wire 上多出一个「答不出」的第三态,而这一格读错的方向是**把一台正在外流记忆的部署报成没在外流**。
431
+ * 装配漏接 ⇒ tsc 红(本行就是通知);测试桩在 `test/helpers/boot.ts` 显式给 false。
432
+ */
433
+ memoryAutoConsolidationArmed: boolean;
420
434
  /** [ref] 件②(core 5.59.0 `MemoryEngine.listExternalOriginEntries` / `listOriginClearances` /
421
435
  * `clearEntryOrigin`):**外源标记人面**。启用 `GET /v1/memory/origin/external`、
422
436
  * `GET /v1/memory/origin/clearances`、`POST /v1/memory/origin/entries/:entryId/clear`(三口 operator-only)。
@@ -320,6 +320,32 @@ export interface TaskRequestBody {
320
320
  * 记忆面暗的部署(引擎未接线 / 多租户)上本键随 `memory` 整体缺席而无处落 —— 那里本就零采集。
321
321
  * Rides the persisted body onto resume legs(与 memoryWrite 同姿势)。 */
322
322
  memoryCapture?: "off";
323
+ /** core [ref] r5 §5 W3(server S-405):**请求级「记忆整面关」声明** —— `"off"` = 这一次运行**不挂记忆面**
324
+ * (没有 `# Memory` 段/索引/三只记忆工具/写准入)。单成员闭集,**没有** `"on"`(缺席就是记忆照常挂的唯一形;
325
+ * 声明只能单向收紧)。透传为 `TaskSpec.memory.enabled:false` —— ⚠️ **保留事实,不折成整键缺席**:core 的
326
+ * 终局观测面把「有 spec 但被关」读作 `reason:"disabled"`、把「没有 spec」读作 `reason:"no-spec"`,审计上
327
+ * 不是一件事,所以本仓铸的是 `{ …正常 scopes, enabled:false }`。
328
+ * ⚠️ **语义如实**:这是「不挂记忆面」,**不是**文件系统级的禁读禁写 —— 记忆根若落在本次授权的工作区内,
329
+ * 普通 Read/Write 照走普通围栏(没有记忆面 = 没有记忆写门,不是多了一道禁令)。
330
+ * 与 `memoryCapture:"off"` **不是一个轴**:那是「记忆照常挂,但本会话的内容永远不进长期记忆」
331
+ * (采集面,落一条单向会话记录);这是「本次运行整个记忆面都不挂」(读写整面,逐请求)。
332
+ * 🔴 **两键同发时不要以为买到了两件事**(亲核 core 7.21.1 `prepare-memory.js:48/:55`):core 把 capture 腿与
333
+ * 引擎挂载合取,`enabled:false` 下 capture 的**单向会话记录不落**(core 发 `phase:"config"` 一行披露)⇒
334
+ * 下一 turn 不带本键时采集照常。要「这个会话永久别记」,那一 turn 就**别**同时发本键。详见契约 §12.7。
335
+ * 与 `memoryWrite:false`(本 run 只读平面:照常挂、照常读、harvest 不提交)同样正交、同样可并存。
336
+ * 🔴 **与 `GET /v1/capabilities` 的 legacy 能力位 `memory`(恒假)同名不同物**:那一位说的是退役的
337
+ * MemoryStore HTTP 动词族,与本键零关系 —— 别按那一位判断本键可不可用。
338
+ * ⚠️ 拼写门**两腿同判、响亮拒**:fresh 400 `request.field_invalid`;resume 重放同判(不做 resume 降级 ——
339
+ * 把一条收紧声明按打字错误静默丢掉是不可逆方向的静默降级)。两腿共用同一只判据
340
+ * (`request-value-closure.ts` 的 `singleMemberOffFieldIssue`,与 `memoryCapture` 同表)。
341
+ * 记忆面暗的部署(引擎未接线 / 多租户)上 `memory` 整键本就缺席,本声明无处落也无事可关。
342
+ * Rides the persisted body onto resume legs(与 `memoryWrite` 同姿势)。
343
+ * ✅ **子代地板自 core 7.22.0 到货,本仓零接线**:core 的 `parentSpecSeatOf` 从**这条 run 自己的**
344
+ * `spec.memory.enabled === false` 铸 `ParentSpecSeat.memoryOff`,`applyParentSpecSeat` 作**无条件
345
+ * REMOVAL** 施加(改写成 `{ …子代自己的 scopes, enabled:false }`,永不删键)⇒ 父关 ⇒ 子代 / 孙代 /
346
+ * workflow 子 spec / 跨 run 唤醒与保留 resume 一律无记忆面。🚧 **tier-3 耐久复活不带**(core 已记残余)。
347
+ * 链路的机器锚:`test/memory-off-request.test.ts` ⑥ 段(亲读 core dist 的铸点与施加点)。 */
348
+ memory?: "off";
323
349
  /** 142-S4 projectId 线程化([ref] §2):这个 run 归属的项目(center 登记簿键,generic lowercase
324
350
  * UUID — core S1 `resolveProjectId` marker 或 S3 无仓 mint 的产物;客户端只透传,零铸造权威)。是「哪个
325
351
  * 项目」的选择器,不是身份/capability — scope 的 tenant 段永远来自 verified principal。SHAPE 门在
@@ -1,4 +1,4 @@
1
- import { FileMemoryEngineBackend } from "@sema-agent/core";
1
+ import { FileMemoryEngineBackend, type TaskSpec } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "./config-types.js";
3
3
  /** 前缀词表本体(供门/诊断读;判定一律走 {@link isV2ScopeKey},别在别处重写 startsWith 链)。 */
4
4
  export declare const V2_SCOPE_PREFIXES: readonly string[];
@@ -175,6 +175,10 @@ onIncident?: (err: Error & {
175
175
  * commits nothing). Absent/true ⇒ normal read+write. No scope (engine off / multi-tenant dark) ⇒ undefined
176
176
  * (memory feature off for this run). Pure (testable in isolation) — main.ts's resolveSpec composes it with the
177
177
  * engine-backend presence guard.
178
+ *
179
+ * 🔴 **三轴正交**(S-405 起;三者可任意并存,互不吞):`memoryWrite:false` = 本 run 只读平面(挂、读、
180
+ * harvest 不提交)· `memoryCapture:"off"` = 采集面关(挂、读、本会话内容永不进长期记忆)· `memory:"off"` =
181
+ * **整面关**(`enabled:false`,这一次连记忆面都不挂)。后两者从 `declarations` 进(键名与 wire 逐字同名)。
178
182
  */
179
183
  export declare function memorySpecForRequest(scope: string | undefined, memoryWrite: boolean | undefined, defaultScopes?: string[],
180
184
  /** [ref] 件A([ref] 件3④):origin 盖章的部署形态维(N2)。`multiTenant=true`(requirePrincipal)
@@ -185,14 +189,38 @@ export declare function memorySpecForRequest(scope: string | undefined, memoryWr
185
189
  originPolicy?: {
186
190
  multiTenant: boolean;
187
191
  },
188
- /** [ref] §2.1([ref]):请求的 `memoryCapture:"off"` 声明 ⇒ `TaskSpec.memory.capture:"off"`(单成员闭集,
189
- * 拼写门在 prepareSpec 的 400 门 + core 的 `config.memory_capture_spelling`)。缺席 ⇒ 键不铸(缺席就是照常采集
190
- * 的唯一形)。与 `memoryWrite` 正交:那是「本 run 只读平面」,这是「本会话永远不进长期记忆」。 */
191
- memoryCapture?: "off"): {
192
+ /** 请求体上的**单成员闭集声明**(键名与 wire 逐字同名,零翻译;拼写门的唯一属主是
193
+ * `request-value-closure.ts`,两腿共用 —— 到这里的值已经过门,只可能是 `"off"` 或缺席)。
194
+ *
195
+ * · `memoryCapture:"off"`([ref] §2.1,[ref])⇒ `TaskSpec.memory.capture:"off"`:**采集面**关 ——
196
+ * 记忆照常挂、照常读,但本会话的内容永远不进长期记忆(core 落一条单向会话记录,按部署姿态 + verdict 裁)。
197
+ * · `memory:"off"`(core [ref] r5 §5 W3,S-405)⇒ `TaskSpec.memory.enabled:false`:**整面**关 ——
198
+ * 这一次运行不挂记忆面。**保留事实不折缺席**(codex r2 #4):core 的终局观测面把「有 spec 但被关」读作
199
+ * `reason:"disabled"`、把「整键缺席」读作 `reason:"no-spec"`,审计上不是一件事;而 `normalizeMemorySpec`
200
+ * 在 scopes 为空时整体返回 `undefined`,所以**关的那一形也必须带正常 scopes**。
201
+ *
202
+ * 两键与 `memoryWrite`(本 run 只读平面)在**铸型上**三轴正交、可并存,本函数不替谁做取舍;
203
+ * 整键缺席 ⇒ 都不铸(形状逐字节不变=additive 锁)。
204
+ * ⚠️ **铸型正交 ≠ 裁决正交**(亲核 core 7.21.1 `dist/core/runner/prepare-memory.js:48/:55`):core 把
205
+ * capture 腿与引擎挂载**合取**(`captureDeclared = captureDeclaredRaw && useMemoryEngine`),所以
206
+ * `memory:"off"` 那一 turn 上 capture 的**单向会话记录不落**(core 发 `phase:"config"` 一行响亮披露)。
207
+ * 两键同发要不要一起送、送了买到什么,口径全文在契约 §12.7;钉在 memory-off-request.test.ts ⑤。 */
208
+ declarations?: {
209
+ memory?: "off";
210
+ memoryCapture?: "off";
211
+ }): {
192
212
  scopes: string[];
193
213
  writeScope?: string | null;
194
214
  scopeContract?: "v2";
195
215
  scopeOrigins?: Record<string, "deployment" | "request">;
196
216
  capture?: "off";
217
+ enabled?: false;
197
218
  } | undefined;
219
+ /**
220
+ * 「这一腿挂不挂记忆面」的**唯一属主**(合并复审 车HF F1,2026-09-18):与 core `prepareMemory` 的判据同源 ——
221
+ * `normalizeMemorySpec(spec)?.enabled === true`。S-405 之后 `memory:"off"` 铸的是「键在场 + `enabled:false`」(保留事实),
222
+ * 任何按「键在不在场」判挂没挂的读者从那一天起都读错(改前 `memory-notice` 正是那样,把无记忆面的披露一起吞掉)。
223
+ * 消费方拿本函数的布尔,不拿 `spec.memory` 自己判。
224
+ */
225
+ export declare function memoryFaceMounted(memory: TaskSpec["memory"]): boolean;
198
226
  //# sourceMappingURL=memory-scope.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { FileMemoryEngineBackend, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope } from "@sema-agent/core";
2
+ import { FileMemoryEngineBackend, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope, normalizeMemorySpec } from "@sema-agent/core";
3
3
  import { assertMemoryScopeWidth } from "./plugins/memory-key-guards.js";
4
4
  const V2_SCOPE_PREFIX_BY_KIND = {
5
5
  user: "user:",
@@ -96,7 +96,7 @@ export function memoryEngineBackendFor(config, fallbackRoot, onIncident) {
96
96
  root: plane.root,
97
97
  };
98
98
  }
99
- export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPolicy, memoryCapture) {
99
+ export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPolicy, declarations) {
100
100
  if (!scope)
101
101
  return undefined;
102
102
  const isV2 = isV2ScopeKey;
@@ -113,7 +113,13 @@ export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPo
113
113
  }
114
114
  }
115
115
  const origins = Object.keys(stamped).length > 0 ? { scopeOrigins: stamped } : {};
116
- const capture = memoryCapture === "off" ? { capture: "off" } : {};
117
- return memoryWrite === false ? { ...layered, writeScope: null, ...v2, ...origins, ...capture } : { ...layered, ...v2, ...origins, ...capture };
116
+ const capture = declarations?.memoryCapture === "off" ? { capture: "off" } : {};
117
+ const enabled = declarations?.memory === "off" ? { enabled: false } : {};
118
+ return memoryWrite === false
119
+ ? { ...layered, writeScope: null, ...v2, ...origins, ...capture, ...enabled }
120
+ : { ...layered, ...v2, ...origins, ...capture, ...enabled };
121
+ }
122
+ export function memoryFaceMounted(memory) {
123
+ return normalizeMemorySpec(memory)?.enabled === true;
118
124
  }
119
125
  //# sourceMappingURL=memory-scope.js.map
@@ -102,6 +102,8 @@ export declare function redactTerminalResult<T extends {
102
102
  * 声明成 `{path,edits}[]` 会逼每一个喂坏形的调用点铸一次宽松断言 —— 而「坏形从哪来」正是本座要
103
103
  * 处理的事(同一份不信任在 `remoteEnvFailures` 的实现里也已成文)。运行期逐项判形,不抛。 */
104
104
  editedFiles?: unknown;
105
+ /** S-394:形上同样 `unknown` —— 本函数是读盘面,`stats` 的树形逐层现判(见 {@link redactStats})。 */
106
+ stats?: unknown;
105
107
  } | null | undefined>(result: T): T;
106
108
  /** S-107(第五轮合并重扫 wf_f00c00bc 两根 [high]):同一个 `TaskResult` 除 run 行外还被逐字追加进 run_events 的 `done` 行
107
109
  * (bg/verify/cascade/stream/resume 五腿),`failed` 行带原文 `errorMessage`,`GET /v1/runs/:id/events` 原样回放。
@@ -104,7 +104,7 @@ const RESULT_KEY_TREATMENT = {
104
104
  effectiveWriteFace: "verbatim",
105
105
  effectiveReadDenyPatterns: "verbatim",
106
106
  effectiveMemoryScopes: "verbatim",
107
- stats: "verbatim",
107
+ stats: "redacted",
108
108
  };
109
109
  const LEGACY_RESULT_LEAVES = ["errorMessage", "blockedReason", "checkpointGate"];
110
110
  export const REDACTABLE_RESULT_LEAVES = [
@@ -207,6 +207,13 @@ export function redactTerminalResult(result) {
207
207
  : r.rewindNotes;
208
208
  changed = true;
209
209
  }
210
+ if (r.stats !== undefined) {
211
+ const next = redactStats(r.stats);
212
+ if (next !== r.stats) {
213
+ out.stats = next;
214
+ changed = true;
215
+ }
216
+ }
210
217
  return changed ? out : result;
211
218
  }
212
219
  export function redactLedgerEventData(type, data) {
@@ -317,6 +324,108 @@ export function redactRiskDescriptor(rd) {
317
324
  }
318
325
  return changed ? out : rd;
319
326
  }
327
+ const STATS_KEY_TREATMENT = {
328
+ turns: "verbatim",
329
+ tokens: "verbatim",
330
+ toolCalls: "verbatim",
331
+ usageMissing: "verbatim",
332
+ promptTokens: "verbatim",
333
+ cachedTokens: "verbatim",
334
+ cacheWriteTokens: "verbatim",
335
+ cacheHitRate: "verbatim",
336
+ totalInputTokens: "verbatim",
337
+ outputTokens: "verbatim",
338
+ cacheWriteTokensLong: "verbatim",
339
+ costMicroUsd: "verbatim",
340
+ nested: "verbatim",
341
+ memory: "verbatim",
342
+ suggestions: "verbatim",
343
+ costBreakdown: "verbatim",
344
+ mechanisms: "mechanisms",
345
+ humanReview: "human-review",
346
+ };
347
+ const STATS_MECHANISMS_TREATMENT = {
348
+ finalVerifyInjected: "verbatim",
349
+ finalVerifyInjections: "verbatim",
350
+ attachmentsInjected: "verbatim",
351
+ approachNoticesSent: "verbatim",
352
+ repetitionCuts: "verbatim",
353
+ repetitionSpared: "verbatim",
354
+ repetitionEvents: "repetition-rows",
355
+ reminderDisclosures: "verbatim",
356
+ };
357
+ const STATS_HUMAN_REVIEW_TREATMENT = {
358
+ count: "verbatim",
359
+ totalWaitMs: "verbatim",
360
+ gates: "gate-rows",
361
+ };
362
+ const STATS_GATE_ROW_TREATMENT = {
363
+ kind: "verbatim",
364
+ waitMs: "verbatim",
365
+ decision: "verbatim",
366
+ toolName: "verbatim",
367
+ toolArg: "model-text",
368
+ };
369
+ const STATS_REPETITION_EVENT_TREATMENT = {
370
+ turn: "verbatim",
371
+ action: "verbatim",
372
+ rule: "verbatim",
373
+ period: "verbatim",
374
+ reps: "verbatim",
375
+ segment: "model-text",
376
+ };
377
+ function redactStatsNode(node, table) {
378
+ if (node === null || typeof node !== "object" || Array.isArray(node))
379
+ return node;
380
+ const n = node;
381
+ let changed = false;
382
+ const out = { ...n };
383
+ const into = (v, t) => redactStatsNode(v, t);
384
+ const rows = (v, t) => {
385
+ if (!Array.isArray(v))
386
+ return v;
387
+ const next = v.map((row) => redactStatsNode(row, t));
388
+ return next.every((x, i) => x === v[i]) ? v : next;
389
+ };
390
+ for (const [key, treatment] of Object.entries(table)) {
391
+ const v = n[key];
392
+ if (v === undefined)
393
+ continue;
394
+ let next = v;
395
+ switch (treatment) {
396
+ case "verbatim":
397
+ break;
398
+ case "model-text":
399
+ if (typeof v === "string")
400
+ next = redactModelText(v);
401
+ break;
402
+ case "mechanisms":
403
+ next = into(v, STATS_MECHANISMS_TREATMENT);
404
+ break;
405
+ case "human-review":
406
+ next = into(v, STATS_HUMAN_REVIEW_TREATMENT);
407
+ break;
408
+ case "gate-rows":
409
+ next = rows(v, STATS_GATE_ROW_TREATMENT);
410
+ break;
411
+ case "repetition-rows":
412
+ next = rows(v, STATS_REPETITION_EVENT_TREATMENT);
413
+ break;
414
+ default: {
415
+ const unhandled = treatment;
416
+ throw new Error(`unhandled stats treatment: ${String(unhandled)}`);
417
+ }
418
+ }
419
+ if (next !== v) {
420
+ out[key] = next;
421
+ changed = true;
422
+ }
423
+ }
424
+ return changed ? out : node;
425
+ }
426
+ function redactStats(stats) {
427
+ return redactStatsNode(stats, STATS_KEY_TREATMENT);
428
+ }
320
429
  const CHECKPOINT_GATE_TREATMENT = {
321
430
  kind: "verbatim",
322
431
  reason: "text",
@@ -23,8 +23,12 @@
23
23
  *
24
24
  * ## 定界
25
25
  * 本模块**只**管 `face` 一档。内建读拒表(`readDenyPatterns` / `readDenyBuiltinTiers` /
26
- * `readDenyBuiltinExclude`)一个字节不动 —— 放开容纳面不等于放开凭证/shell 历史/钱包那几档 deny,
27
- * 两者在 core 里是并列机制(deny 表在 open 面上照样拦)。core 的 `config.read_face_deployment_clamped`
26
+ * `readDenyBuiltinExclude`)一个字节不动 —— 放开容纳面不等于放开那几档 deny,两者在 core 里是**并列
27
+ * 机制**(有活行的 deny 表在 open 面上照样拦)。
28
+ * ⚠️ **但别把这句话读成「凭证/shell 历史/钱包那几档一直在岗」**:core 7.22.0([ref])起内建表的**缺省
29
+ * 选择是空** —— 一台没写 `READ_DENY_BUILTIN_TIERS` 的部署上,那张表**一行都不生效**(要老姿态就显式写
30
+ * `credentials,browser,wallet,agent-config`;运维面全文见 `docs/DEPLOY-PREREQS.md` 的 core 7.22.0 段)。
31
+ * 本模块的定界不变,变的是「并列的那一侧默认开着」这个**前提**。core 的 `config.read_face_deployment_clamped`
28
32
  * (只读挂载钳制)同样不受本件影响。
29
33
  */
30
34
  import { type PostureSource } from "./posture-source.js";
@@ -19,6 +19,13 @@ export declare function scanKeyClosure(bag: object, prefix: string, accepted: Re
19
19
  }): void;
20
20
  /** 一张分类表的文案片段(封顶后只报数)。 */
21
21
  export declare function keyClosureClause(names: string[], label: string): string;
22
+ /** 剥掉索引签名,只留**具名**键 —— `TaskRequestBody` 上那条 `[k: string]: unknown` 会让
23
+ * `keyof` 退化成 `string`,双向门就永远恒真(零判别力)。
24
+ * 🔴 **导出**(S-405):姊妹门 `request-value-closure.ts` 的双向钉要同一个剥法 —— 抄第二份 = 两处哪天
25
+ * 分岔(一边看得见索引签名、一边看不见)谁都发现不了,而两边的判别力都建立在「剥干净了」这句话上。 */
26
+ export type NamedKeysOf<T> = {
27
+ [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
28
+ };
22
29
  /**
23
30
  * FRESH 提交面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
24
31
  * 全部受理 ⇒ `null`。
@@ -22,7 +22,7 @@ const TASK_BODY_DECLARED_KEYS = [
22
22
  "appendSystemPrompt", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
23
23
  "compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "enableFork", "excludeTools",
24
24
  "finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
25
- "maxTokens", "mcpServers", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
25
+ "maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
26
26
  "outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
27
27
  "resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
28
28
  "rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
@@ -0,0 +1,31 @@
1
+ /** [ref] §2.1([ref]):`memoryCapture` 的拼写门文案。**wire 可见文本** —— 改一个字就是对端契约漂移
2
+ * (SDK/壳/文档锚它),字节冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格。 */
3
+ export declare const MEMORY_CAPTURE_SPELLING_MESSAGE = "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual \u2014 there is no 'on' spelling)";
4
+ /** core [ref] r5 §5 W3(S-405):请求级「记忆整面关」`memory` 的拼写门文案。同上,wire 可见、字节冻结。
5
+ * 文案刻意点明 capture 的分工 —— 两个键都在记忆轴上、形又一模一样,拿错的人要在同一条回执里读到该用哪个。 */
6
+ export declare const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual \u2014 there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
7
+ /**
8
+ * **单成员闭集键表** —— 合法值只有 `"off"`,缺席=照常,没有 `"on"`。
9
+ *
10
+ * 🔴 顺序 = **裁决顺序**,且是**既存键在前**:一个两键都畸形的存量体,判决与加 `memory` 之前逐字节相同
11
+ * (新键不许改写既有体的回执)。新增同形键一律**追加在尾部**。
12
+ */
13
+ declare const SINGLE_MEMBER_OFF_FIELDS: readonly [{
14
+ readonly field: "memoryCapture";
15
+ readonly message: "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)";
16
+ }, {
17
+ readonly field: "memory";
18
+ readonly message: "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual — there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
19
+ }];
20
+ /** 一次值闭集裁决的产物:出问题的键名(机读)+ 一句 wire 文案。 */
21
+ export interface SingleMemberOffFieldIssue {
22
+ field: (typeof SINGLE_MEMBER_OFF_FIELDS)[number]["field"];
23
+ message: string;
24
+ }
25
+ /**
26
+ * 单成员闭集键的值形裁决:表内任一键在场且不是字面 `"off"` ⇒ 返回拒体(调用方 400 `request.field_invalid`);
27
+ * 全部合法或全部缺席 ⇒ `null`。**两腿(fresh admission / resume 重放)共用这一只**。
28
+ */
29
+ export declare function singleMemberOffFieldIssue(raw: unknown): SingleMemberOffFieldIssue | null;
30
+ export {};
31
+ //# sourceMappingURL=request-value-closure.d.ts.map
@@ -0,0 +1,18 @@
1
+ export const MEMORY_CAPTURE_SPELLING_MESSAGE = "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)";
2
+ export const MEMORY_OFF_SPELLING_MESSAGE = "memory must be the string 'off' when present (the request-level declaration that this run mounts no memory face at all; absent = memory as usual — there is no 'on' spelling; to keep memory mounted but stop this session's content from entering long-term memory, send memoryCapture: 'off' instead)";
3
+ const SINGLE_MEMBER_OFF_FIELDS = [
4
+ { field: "memoryCapture", message: MEMORY_CAPTURE_SPELLING_MESSAGE },
5
+ { field: "memory", message: MEMORY_OFF_SPELLING_MESSAGE },
6
+ ];
7
+ export function singleMemberOffFieldIssue(raw) {
8
+ if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
9
+ return null;
10
+ const body = raw;
11
+ for (const { field, message } of SINGLE_MEMBER_OFF_FIELDS) {
12
+ const value = body[field];
13
+ if (value !== undefined && value !== "off")
14
+ return { field, message };
15
+ }
16
+ return null;
17
+ }
18
+ //# sourceMappingURL=request-value-closure.js.map
@@ -17,7 +17,7 @@
17
17
  * ⑨⑩([ref],2026-08-31)把两个最大的 wire 面也纳入:TaskSpec 按「server 有无写点」记 Wired/Unwired,
18
18
  * TaskResult 按「转发/摘除」记 Forwarded/Stripped —— 谁改 resolve-spec.ts / runs.ts 的发射点谁同步本文件。
19
19
  */
20
- import type { TaskNotificationPayload, SemaProvenance, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult, WiringManifest, GateOutcome, GateDisposition, Settlement, PausedCause, WorkflowRun } from "@sema-agent/core";
20
+ import type { TaskNotificationPayload, SemaProvenance, BackgroundChildEvent, RosterEntry, AskRequest, CheckpointSummary, TaskEvent, TraceEvent, MailboxMessage, MailboxAppendMessage, TaskSpec, TaskResult, WiringManifest, GateOutcome, GateDisposition, Settlement, PausedCause, WorkflowRun, ReadRootGrantCandidate, McpReconnectResult, McpServerStatus } from "@sema-agent/core";
21
21
  import type { ToolApprovalFrame } from "../tool-approval.js";
22
22
  /** 编译期断言:T 必须收敛到 never(有残余键 = tsc 红)。
23
23
  * 🔴 **导出**([ref] U1/U2):本仓「词表闭集,未处置成员=编译红」的钉法只许有**一个**形 ——
@@ -60,7 +60,7 @@ type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peer
60
60
  type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
61
61
  type _GuardMailboxAppendReverse = AssertAllKeysHandled<Exclude<MailboxAppendProjected, keyof MailboxAppendMessage>>;
62
62
  type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "writeProtection";
63
- type WiringSectionExcluded = "governance" | "configFingerprint" | "autoConsolidation";
63
+ type WiringSectionExcluded = "governance" | "configFingerprint" | "autoConsolidation" | "readDeny";
64
64
  type _GuardWiringSections = AssertAllKeysHandled<Exclude<keyof WiringManifest, WiringSectionProjected | WiringSectionExcluded>>;
65
65
  type _GuardWiringSectionsReverse = AssertAllKeysHandled<Exclude<WiringSectionProjected | WiringSectionExcluded, keyof WiringManifest>>;
66
66
  type WiringSection<K extends keyof WiringManifest> = NonNullable<WiringManifest[K]>;
@@ -103,6 +103,9 @@ type _GuardWiringRulesReverse = AssertAllKeysHandled<Exclude<WiringRulesHandled,
103
103
  type WiringWriteProtectionHandled = "targetView";
104
104
  type _GuardWiringWriteProtection = AssertAllKeysHandled<Exclude<keyof WiringSection<"writeProtection">, WiringWriteProtectionHandled>>;
105
105
  type _GuardWiringWriteProtectionReverse = AssertAllKeysHandled<Exclude<WiringWriteProtectionHandled, keyof WiringSection<"writeProtection">>>;
106
+ type WiringReadDenyHandled = "builtinTiers";
107
+ type _GuardWiringReadDeny = AssertAllKeysHandled<Exclude<keyof WiringSection<"readDeny">, WiringReadDenyHandled>>;
108
+ type _GuardWiringReadDenyReverse = AssertAllKeysHandled<Exclude<WiringReadDenyHandled, keyof WiringSection<"readDeny">>>;
106
109
  type WiringAutoConsolidationHandled = "onRecommendation";
107
110
  type _GuardWiringAutoConsolidation = AssertAllKeysHandled<Exclude<keyof WiringSection<"autoConsolidation">, WiringAutoConsolidationHandled>>;
108
111
  type _GuardWiringAutoConsolidationReverse = AssertAllKeysHandled<Exclude<WiringAutoConsolidationHandled, keyof WiringSection<"autoConsolidation">>>;
@@ -164,6 +167,32 @@ type ParkRowWithheld = "token";
164
167
  type _GuardParkRow = AssertAllKeysHandled<Exclude<ParkRowKeys, ParkRowProjected | ParkRowWithheld>>;
165
168
  type _NoDoubleBookParkRow = AssertAllKeysHandled<Extract<ParkRowProjected, ParkRowWithheld>>;
166
169
  type _GuardParkRowReverse = AssertAllKeysHandled<Exclude<ParkRowProjected | ParkRowWithheld, ParkRowKeys>>;
170
+ type ReadRootCandidateProjected = "dir" | "clearsThisAsk" | "covers";
171
+ type _GuardReadRootCandidate = AssertAllKeysHandled<Exclude<keyof ReadRootGrantCandidate, ReadRootCandidateProjected>>;
172
+ type _GuardReadRootCandidateReverse = AssertAllKeysHandled<Exclude<ReadRootCandidateProjected, keyof ReadRootGrantCandidate>>;
173
+ type AskRiskAxes = NonNullable<AskRequest["riskAxes"]>;
174
+ type RiskAxesProjected = "irreversible" | "egress";
175
+ type _GuardRiskAxes = AssertAllKeysHandled<Exclude<keyof AskRiskAxes, RiskAxesProjected>>;
176
+ type _GuardRiskAxesReverse = AssertAllKeysHandled<Exclude<RiskAxesProjected, keyof AskRiskAxes>>;
177
+ type McpReconnectProjected = "server" | "prefix" | "toolCount" | "added" | "removed" | "outcome" | "tools" | "reason" | "status" | "listingIncomplete";
178
+ type McpReconnectWithheld = "axes" | "dropped" | "warnings";
179
+ type _GuardMcpReconnect = AssertAllKeysHandled<Exclude<keyof McpReconnectResult, McpReconnectProjected | McpReconnectWithheld>>;
180
+ type _NoDoubleBookMcpReconnect = AssertAllKeysHandled<Extract<McpReconnectProjected, McpReconnectWithheld>>;
181
+ type _GuardMcpReconnectReverse = AssertAllKeysHandled<Exclude<McpReconnectProjected | McpReconnectWithheld, keyof McpReconnectResult>>;
182
+ type McpRedialStatusProjected = "name" | "status" | "errorCode" | "httpStatus" | "delivered" | "serverInfo" | "error" | "transportClosed";
183
+ type McpRedialStatusWithheld = "toolNames";
184
+ type _GuardMcpRedialStatus = AssertAllKeysHandled<Exclude<keyof McpServerStatus, McpRedialStatusProjected | McpRedialStatusWithheld>>;
185
+ type _NoDoubleBookMcpRedialStatus = AssertAllKeysHandled<Extract<McpRedialStatusProjected, McpRedialStatusWithheld>>;
186
+ type _GuardMcpRedialStatusReverse = AssertAllKeysHandled<Exclude<McpRedialStatusProjected | McpRedialStatusWithheld, keyof McpServerStatus>>;
187
+ type McpPanelStatusProjected = "name" | "status" | "serverInfo" | "toolNames" | "error";
188
+ type McpPanelStatusWithheld = "errorCode" | "httpStatus" | "delivered" | "transportClosed";
189
+ type _GuardMcpPanelStatus = AssertAllKeysHandled<Exclude<keyof McpServerStatus, McpPanelStatusProjected | McpPanelStatusWithheld>>;
190
+ type _NoDoubleBookMcpPanelStatus = AssertAllKeysHandled<Extract<McpPanelStatusProjected, McpPanelStatusWithheld>>;
191
+ type _GuardMcpPanelStatusReverse = AssertAllKeysHandled<Exclude<McpPanelStatusProjected | McpPanelStatusWithheld, keyof McpServerStatus>>;
192
+ type McpListingIncompleteKeys = keyof NonNullable<McpReconnectResult["listingIncomplete"]>;
193
+ type McpListingIncompleteProjected = "reason" | "pages" | "budgetMs" | "error";
194
+ type _GuardMcpListingIncomplete = AssertAllKeysHandled<Exclude<McpListingIncompleteKeys, McpListingIncompleteProjected>>;
195
+ type _GuardMcpListingIncompleteReverse = AssertAllKeysHandled<Exclude<McpListingIncompleteProjected, McpListingIncompleteKeys>>;
167
196
  export type CoreKeysetGuards = [
168
197
  _GuardNotification,
169
198
  _GuardNotificationReverse,
@@ -188,6 +217,8 @@ export type CoreKeysetGuards = [
188
217
  _GuardWiringSections,
189
218
  _GuardWiringWriteProtection,
190
219
  _GuardWiringWriteProtectionReverse,
220
+ _GuardWiringReadDeny,
221
+ _GuardWiringReadDenyReverse,
191
222
  _GuardWiringAutoConsolidation,
192
223
  _GuardWiringAutoConsolidationReverse,
193
224
  _GuardWiringSectionsReverse,
@@ -242,7 +273,22 @@ export type CoreKeysetGuards = [
242
273
  _GuardWorkflowRunReverse,
243
274
  _GuardParkRow,
244
275
  _NoDoubleBookParkRow,
245
- _GuardParkRowReverse
276
+ _GuardParkRowReverse,
277
+ _GuardReadRootCandidate,
278
+ _GuardReadRootCandidateReverse,
279
+ _GuardRiskAxes,
280
+ _GuardRiskAxesReverse,
281
+ _GuardMcpReconnect,
282
+ _NoDoubleBookMcpReconnect,
283
+ _GuardMcpReconnectReverse,
284
+ _GuardMcpRedialStatus,
285
+ _NoDoubleBookMcpRedialStatus,
286
+ _GuardMcpRedialStatusReverse,
287
+ _GuardMcpPanelStatus,
288
+ _NoDoubleBookMcpPanelStatus,
289
+ _GuardMcpPanelStatusReverse,
290
+ _GuardMcpListingIncomplete,
291
+ _GuardMcpListingIncompleteReverse
246
292
  ];
247
293
  export {};
248
294
  //# sourceMappingURL=core-keyset-guard.d.ts.map
@@ -613,6 +613,18 @@ export declare function workspaceChangedEventData(ev: {
613
613
  * 并且它对调用方**本就不可观测**(整理跑在终端 harvest 之后、fire-and-forget、宿主侧),剥掉它不收走
614
614
  * 任何能力 —— 这正是 `writeProtection` 判据①**不成立**的那一格。
615
615
  * ⚠️ **缺席 = 未武装**(core present-iff),不折 `false`、不读「未知」。
616
+ * 🔴 **S-403(7.85.0)起本条要连着 `core-keyset-guard.ts` ⑥b 判据②的补句读**:同一个事实在会话面
617
+ * (`GET /v1/sessions/:id/memory-status` 的 `autoConsolidationArmed`)是可读的,而这一段仍然剥 ——
618
+ * 「一个事实一张脸」,两处同源、唯一读点。裁定正文在那一处,这里不抄第二份。
619
+ * · `readDeny`(`{ builtinTiers: readonly string[] }`;core 7.22.0 [ref],S-417 提货件)—— **裁:operator
620
+ * 受众,租户面剥**,所以它**不在本构造器里**,在 {@link wiringManifestOperatorEventData}。四条判据逐字在
621
+ * `core-keyset-guard.ts` ⑥b 的 `WiringSectionExcluded` 那一行(与 `governance` / `configFingerprint` /
622
+ * `autoConsolidation` 同表同处,免得同一条裁定有两个写者)。一句话:它答的是**这台部署选了哪几档内置
623
+ * 敏感路径 deny 表**——一条部署治理面的**选择**,与 `writeProtection` 那种「这条腿手里有什么设施」的
624
+ * 自述不同族;而且它是本仓 env `READ_DENY_BUILTIN_TIERS` 的生效读出口,名表面归 operator(S-138)。
625
+ * ⚠️ **缺席 = 没有内置行判这条腿**(core present-iff:实质激活至少一行才铸),**不是**「没有读被 deny
626
+ * 判」—— 部署自己的 `readDenyPatterns` 刻意不上报(模式文本是部署材料,不是枚举)。**空数组永不发出**
627
+ * (「关」与「开到什么都不判」是同一个事实)⇒ 收到空数组按**坏 producer** 处置并留痕,见那里。
616
628
  * 未来 core 加一段而它没有 audience 标签时,**先在这里补一条裁定再决定挑不挑键** —— 段级完备性钉
617
629
  * (test/wiring-manifest-projection.test.ts)会在那一刻先把人拦下来 —— ⚠️ **但那条钉是夹具驱动的**:它只看
618
630
  * `deriveWiringManifest(EFFECTIVE_FACTS)` 真铸出来的段,facts 不带的可选段(147③ 的 `autoMode` 就是)它一格
@@ -554,6 +554,19 @@ export function wiringManifestOperatorEventData(ev) {
554
554
  ...(bool(g.memoryAdmission) !== undefined ? { memoryAdmission: g.memoryAdmission } : {}),
555
555
  ...(bool(g.retention) !== undefined ? { retention: g.retention } : {}),
556
556
  };
557
+ const readDeny = (() => {
558
+ if (m.readDeny === undefined)
559
+ return undefined;
560
+ const seg = sec(m.readDeny);
561
+ if (seg === undefined)
562
+ return dropProjectedFace("wiring_manifest.readDeny is not an object");
563
+ const tiers = seg.builtinTiers;
564
+ if (!Array.isArray(tiers) || tiers.length === 0)
565
+ return dropProjectedFace("wiring_manifest.readDeny.builtinTiers is not a non-empty array (core never emits an empty one)");
566
+ if (!tiers.every((t) => str(t) !== undefined))
567
+ return dropProjectedFace("wiring_manifest.readDeny.builtinTiers has a non-string / empty member");
568
+ return { builtinTiers: [...tiers] };
569
+ })();
557
570
  const autoConsolidation = (() => {
558
571
  if (m.autoConsolidation === undefined)
559
572
  return undefined;
@@ -567,6 +580,7 @@ export function wiringManifestOperatorEventData(ev) {
567
580
  return {
568
581
  ...tenant,
569
582
  ...(Object.keys(governance).length > 0 ? { governance } : {}),
583
+ ...(readDeny !== undefined ? { readDeny } : {}),
570
584
  ...(autoConsolidation !== undefined ? { autoConsolidation } : {}),
571
585
  ...(str(m.configFingerprint) !== undefined ? { configFingerprint: m.configFingerprint } : {}),
572
586
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.83.0",
3
+ "version": "7.85.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.21.1",
57
+ "@sema-agent/core": "7.22.0",
58
58
  "@sema-agent/settings-schema": "2.1.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",