@sema-agent/server 7.83.0 → 7.84.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.
@@ -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,
@@ -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;
@@ -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");
@@ -320,6 +320,28 @@ 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
+ * ⏳ 子代地板(`ParentSpecSeat.memoryOff`:父关 ⇒ 子代/孙代同关)候 core 7.22.0,**本版不做**。 */
344
+ memory?: "off";
323
345
  /** 142-S4 projectId 线程化([ref] §2):这个 run 归属的项目(center 登记簿键,generic lowercase
324
346
  * UUID — core S1 `resolveProjectId` marker 或 S3 无仓 mint 的产物;客户端只透传,零铸造权威)。是「哪个
325
347
  * 项目」的选择器,不是身份/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",
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.83.0",
3
+ "version": "7.84.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",