@sema-agent/server 7.109.0 → 7.110.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/MIGRATION.md +17 -0
  2. package/README.md +8 -0
  3. package/README.zh-CN.md +5 -0
  4. package/USAGE.md +2 -2
  5. package/dist/approval-card.d.ts +6 -5
  6. package/dist/approval-card.js +8 -11
  7. package/dist/boot/reapers.d.ts +3 -0
  8. package/dist/boot/reapers.js +3 -2
  9. package/dist/boot/resolve-spec.js +3 -4
  10. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  11. package/dist/boot/stage-07-capability-layer.js +17 -3
  12. package/dist/boot/stage-08-reapers.d.ts +1 -0
  13. package/dist/boot/stage-08-reapers.js +2 -2
  14. package/dist/boot/stage-09-leader.d.ts +1 -0
  15. package/dist/boot/stage-10-http-server.d.ts +1 -0
  16. package/dist/boot/stage-10-http-server.js +2 -1
  17. package/dist/config-catalog.js +1 -1
  18. package/dist/config-types.d.ts +3 -2
  19. package/dist/config.js +19 -7
  20. package/dist/deployment-governance.d.ts +8 -2
  21. package/dist/deployment-governance.js +9 -8
  22. package/dist/hooks/hook-runner.d.ts +2 -2
  23. package/dist/hooks/hook-runner.js +40 -26
  24. package/dist/http/durable-run-leg.js +1 -1
  25. package/dist/http/resume-legs.js +6 -1
  26. package/dist/http/routes/runs.d.ts +0 -1
  27. package/dist/http/routes/runs.js +22 -0
  28. package/dist/http/routes/sessions.js +12 -2
  29. package/dist/http/routes/tasks.js +5 -1
  30. package/dist/http/server.d.ts +5 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/instruction-source-notice.d.ts +3 -3
  34. package/dist/memory-layer-legacy-lock.d.ts +3 -3
  35. package/dist/model-entry-headers.d.ts +5 -2
  36. package/dist/model-entry-headers.js +4 -4
  37. package/dist/model-unreachable-notice.d.ts +2 -2
  38. package/dist/observability/corrupt-read-seat.d.ts +6 -0
  39. package/dist/observability/corrupt-read-seat.js +6 -0
  40. package/dist/observability/fail-open.d.ts +13 -1
  41. package/dist/observability/fail-open.js +13 -1
  42. package/dist/observability/metrics.js +2 -1
  43. package/dist/observability/run-terminal.js +10 -0
  44. package/dist/operator-ask.d.ts +9 -8
  45. package/dist/operator-ask.js +9 -1
  46. package/dist/plugins/file-run-store.js +2 -2
  47. package/dist/plugins/memory-run-store.d.ts +1 -1
  48. package/dist/plugins/memory-run-store.js +2 -1
  49. package/dist/plugins/pg-pool.js +15 -0
  50. package/dist/plugins/run-store-sql.d.ts +1 -1
  51. package/dist/plugins/run-store-sql.js +3 -2
  52. package/dist/plugins/schema-indexes.js +2 -0
  53. package/dist/plugins/store-backend.d.ts +4 -0
  54. package/dist/plugins/store-backend.js +8 -0
  55. package/dist/plugins/store-contracts.d.ts +7 -0
  56. package/dist/plugins/store-contracts.js +1 -0
  57. package/dist/plugins/tidb-pool.js +17 -0
  58. package/dist/plugins/user-input-ledger-file.d.ts +33 -0
  59. package/dist/plugins/user-input-ledger-file.js +98 -0
  60. package/dist/plugins/user-input-ledger-sql.d.ts +44 -0
  61. package/dist/plugins/user-input-ledger-sql.js +101 -0
  62. package/dist/run-local.d.ts +18 -5
  63. package/dist/run-local.js +24 -9
  64. package/dist/runs.d.ts +8 -1
  65. package/dist/runs.js +6 -2
  66. package/dist/runtime-governance.d.ts +20 -8
  67. package/dist/runtime-governance.js +7 -12
  68. package/dist/session-records-skipped-notice.d.ts +4 -3
  69. package/dist/task-settings.d.ts +0 -6
  70. package/dist/task-settings.js +0 -1
  71. package/dist/tool-approval.d.ts +8 -1
  72. package/dist/tool-approval.js +4 -1
  73. package/dist/trace/core-keyset-guard.d.ts +16 -1
  74. package/dist/trace/engine-notice-wire.d.ts +30 -35
  75. package/dist/trace/engine-notice-wire.js +23 -23
  76. package/dist/trace/self-minted-wire.d.ts +14 -0
  77. package/dist/trace/self-minted-wire.js +6 -0
  78. package/dist/user-input-ledger.d.ts +296 -0
  79. package/dist/user-input-ledger.js +337 -0
  80. package/package.json +3 -3
package/MIGRATION.md CHANGED
@@ -7,6 +7,23 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.110.0(车ST,S-955;小车 SV,S-956;车SX,S-958 / S-959;小车 SA,S-962 / S-963;主会话 S-964;车SW,S-928)—— `@sema-agent/core` 7.38.0(minor,core 硬破坏 0):通告码受众 / 铸者改读 core 表 · `terminal.failed.detail` 过境 · hook 载荷 `permission_mode` = 腿实时模式 · 类对判据走 core 屏 · run-local 无 TTY 拒记部署政策名下 · 复合体读不出词的 PreToolUse 结果交引擎屏 · 🔴 `MEMORY_SYNC_SCOPE` 缺省不再从非 v2 `MEMORY_SCOPE` 派生(拒启) · 崩溃丢掉的 200-applied steer 下一条 run 告知(用户输入受理账本)
11
+
12
+ - **🔴 记忆同步 env(S-964,配置面硬破坏)**:单用户 file 引擎上设了 `MEMORY_SYNC_URL` / `MEMORY_SYNC_TOKEN`、**没设** `MEMORY_SYNC_SCOPE`、且 `MEMORY_SCOPE` 不是 typed 键(`user:` / `org:` / `proj:` / `userproj:`;含缺省 `local`)的部署**升级后拒启**。此前这一形派生同步层 `user:<MEMORY_SCOPE>` —— 一个本部署从不写的层(写家是裸 `MEMORY_SCOPE`,core 同步按同一个名字取条目)⇒ 同步腿自 142-S2.5-W1 起每轮推 0 条。出路二选一(错误句原文点名):`MEMORY_SYNC_SCOPE=<要复制的层>`,或把 `MEMORY_SCOPE` 设成中心期待的 typed 键(如 `user:<tenant>`,缺省同步层随之同名)。显式 `MEMORY_SYNC_SCOPE` 的部署零变化。
13
+
14
+ - **hook 作者(stdin 载荷契约)**:`permission_mode` 的值源从「请求上的模式词」改为**腿的实时模式**(core 回调上下文 `permissionMode`,plan 腿续跑 / 模式释放后的真实值);PreCompact / PostCompact / StopFailure 三事件与上下文缺席时**不带**该键(此前恒带请求词)。按旧值做分支的 hook 改读实时词、对缺席作「无模式语义」处理。**接手屏**(core 7.38.0):PreToolUse hook 结果越界 `action` / 字串布尔 / `null` 成员 / 不可克隆改写 ⇒ 引擎拒(工具不跑、模型读接手句、`permissionDenied` 观察者响);无 `action` 仍是无异议 —— 本服务映射核在约内,部署自写 hook 自查。经本服务 hooks 复合体(部署基线恒挂,挂了 hooks 的 run 都经过它)的结果同判:读不出词的结果(集外 `action`、非对象 —— 含 `0` / `""` / `NaN`;无异议只认 `undefined` / `null` / `false`)原样交引擎屏 ⇒ 引擎拒(S-959;修前复合体把它读作无异议、照跑,带 `additionalContext` 的还被译成 allow)。
15
+ - **嵌入方(类型面,不在包导出面)**:`SERVER_NOTICE_AUDIENCE` / `ServerNoticeCode` / `isServerNoticeCode` / `HookRunnerContext.permissionMode` / `ENGINE_ABSENT_PERMISSION_MODE` 仓内硬删 —— 四枚本服务铸码的受众 / 铸者改读 core `NOTICE_AUDIENCE` / `NOTICE_MINTER`;client-core 镜像行可退役(core 包已含四码)。
16
+ - **结果面(additive)**:`terminal.failed.detail? { kind:"provider_rejected", status, code? }` 过境(同步 200 体 / `done.result` / `GET /v1/runs/:id` / `done` 账本行);判「可改重发」看 `status`(401 / 403 / 429 也带)。`engine_notice` D.3 白名单 +`task.tool_turn_incomplete`(user);`memory.layer_locked_legacy` 明拒(operator)。`/metrics` `brain_echo_dropped_total{field}` 可出现 `field="apiErrorCode"`。
17
+ - **审批面(值层,键不变)**:`askClass` / `classes` 读写两面统一走 core `screenAskClassMembers`:投规范序副本;重复员 / 非规范序首员不符形改判(只在 core 自己不写出的形上);`card_json` 存量卡零影响。
18
+ - **🔴 迁移窗口(core #1213)**:本版提货 core 7.38.0 **仍迁移旧模式键检查点行、不拒**;排空窗到提货 core 7.39.0 的 server 版前(7.109.0 段原句)。
19
+ - **run-local(S-956)**:无 TTY 被拒的 run 不再带 `haltedOnUserRejection`。退出码仍为 1;非 `--json` 停因行从 `task stopped (haltedOnUserRejection) …` 改为 `task incomplete (approval unanswered) …`;门记录 `settlement.kind` `human_refused` → `policy_refused`、`who.party` `person` → `none`;模型读 core 策略句 + 拒因。按 7.101.0 迁移建议读 `--json` 的 `haltedOnUserRejection` 判拒的脚本,改看退出码 / stderr 停因行。同一拍的兄弟调用不再被 `gate.batch_halted` 连坐,各自过门。委派子代里无 TTY 的拒也计数 ⇒ CLI 退 1(更严;TTY 下子代人答 N 仍退 0)。
20
+ - **嵌入方(类型面硬破坏,S-928)**:`StoreBackend`(包根导出型)新增**必选** `userInputLedger(): UserInputLedgerStore`;自建 StoreBackend 的嵌入方 tsc 红即通知(7.109.0 `listByCall` 同形);返回型 `UserInputLedgerStore` 及行型 `UserInputAcceptance` / `NewUserInputAcceptance` / `SettleHow` / `OpenScanCursor` / `WriterGoneEvidence` 自本版从包根导出。
21
+ - **用户输入受理账本(S-928;零动作)**:live 腿 steer 在回 200 之前先落一行受理(local:`<数据根>/user-input-ledger/ledger.jsonl`;SQL:新表 `user_input_acceptance`,
22
+ 中央 `ensureSchema` 以 `CREATE TABLE IF NOT EXISTS` 自建,两方言 + 两条索引,无 ALTER、无回填)。账本写不进 ⇒ steer 回 **503 `steering.ledger_unavailable`**(此前 200;引擎零字节)——
23
+ local 车道数据根只读 / 盘满、SQL 车道库不可达时出现,其余路由不受影响。进程崩溃时丢掉的受理在该会话下一条 run 首拍告知 `task.user_steer_undrained{cause:"crashed"}`,
24
+ `GET /v1/sessions/:id` 多一个可选键 `userInputUndrained`(缺席 = 零条)。**回滚**:回到 ≤7.109.x 时账本文件 / 表留着无害(旧二进制不读);滚回之后的崩溃丢失恢复修前的静默。
25
+ - **零数据迁移**;DDL 只有上一条的新表(自建)。
26
+
10
27
  ## 7.109.0(车SK,S-934)—— `@sema-agent/core` 7.37.1(7.37.0 的全部变更 + #1224 工具回合律请求修复,运维通告 `context.tool_turn_repaired` 不上会话流):无人值守腿按类作答(`UNATTENDED_PERMISSION_MODE` 退役,设了即拒启)· ask 类对 `askClass` / `classes` 上 wire · 策略决定对象的普通对象律 · 迁移窗口
11
28
 
12
29
  - **🔴 env:`UNATTENDED_PERMISSION_MODE` 设了即拒启**(任何值,含 7.106.0 的缺省 `bypassPermissions`;拒因带 `config.env_key_retired` 并点名键)。
package/README.md CHANGED
@@ -128,6 +128,9 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
128
128
  7.104.0 the execution-lane start refusals (`LEADER_ENABLED` on the host lane, a multi-tenant posture on a
129
129
  shared lane, the device lane without its durable stores) are judged there, not in `loadConfig()`;
130
130
  `assertExecutionLaneInvariants(config)` is the same judgment without the `config.d/remote-exec.json` override.
131
+ Build-time tools (e.g. a catalog editor) read two allowlists from the package root instead of hand-copying them: `RETIRED_ENVS` (the retired-env
132
+ tombstones) and, since 7.110.0, `CATALOG_ENTRY_HEADER_LANES` / `CATALOG_ENTRY_HEADER_NAMES` (+ type `CatalogEntryHeaderName`) — the closed allowlist of
133
+ model-catalog entry `headers` and the API line that reads each name. Both are frozen, the very objects the server judges with.
131
134
  - **Bundled binaries**: the package ships two `bin` entries — `run-local` (single-machine local
132
135
  runner) and `sema-up` (deployment bootstrap script).
133
136
  - `run-local` boots the same engine in-process, runs one task from the CLI objective, prints the
@@ -156,6 +159,11 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
156
159
  - **A run stopped by a refusal exits 1** (since 7.101.0; it used to exit 0), `--json` included: whatever the
157
160
  model wrote before the refusal still goes to stdout, and without `--json` stderr names the stop
158
161
  (`task stopped (haltedOnUserRejection) …`).
162
+ - **A refusal nobody was asked for is the deployment's policy, not yours** (since 7.110.0): with no TTY (or
163
+ arguments that cannot be displayed) the gate records `policy_refused` / `who.party: "none"`, the model reads
164
+ the engine's policy sentence instead of "the user doesn't want to proceed", and it carries on with the parts
165
+ that need no approval. The run still exits 1 — that call never ran — and stderr names the stop
166
+ (`task incomplete (approval unanswered) …`).
159
167
  - **Full-stack, one command** (DB + object store + registry web + sandbox pool; Docker and k8s
160
168
  paths): [`sema-agent/sema-deploy`](https://github.com/sema-agent/sema-deploy).
161
169
  - **Sandbox package sources**: default = official upstreams (pypi/npmjs/crates.io/…). For
package/README.zh-CN.md CHANGED
@@ -112,6 +112,8 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
112
112
  `composeExecutionLane(config, ctx)` —— HTTP 服务与 `run-local` 都这么做。7.104.0 起执行车道的几条拒启(host 车道上开
113
113
  `LEADER_ENABLED`、多租户姿态落在共享车道、device 车道缺耐久店)判在这一步,不在 `loadConfig()` 里;
114
114
  `assertExecutionLaneInvariants(config)` 是同一份判据,不做 `config.d/remote-exec.json` 覆写。
115
+ 构建期工具(如目录编辑页)从包根取两张表、不手抄:`RETIRED_ENVS`(退役 env 墓碑)与 7.110.0 起的 `CATALOG_ENTRY_HEADER_LANES` /
116
+ `CATALOG_ENTRY_HEADER_NAMES`(+ 型 `CatalogEntryHeaderName`)—— 模型目录条目 `headers` 的闭集白名单及读每个名字的线路。两者都冻结,就是服务端自己判据用的那一只。
115
117
  - **随包二进制**:包内带两个 `bin` —— `run-local`(单机本地 runner)与
116
118
  `sema-up`(部署引导脚本)。
117
119
  - `run-local` 在进程内启同一套引擎,把命令行给的目标跑完一次、打印结果、退出——没有 HTTP 服务,
@@ -133,6 +135,9 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
133
135
  一问 —— 主任务与子代的并发询问依次排队。
134
136
  - **因拒答而停下的 run 退 1**(7.101.0 起,`--json` 也一样;此前退 0):模型在拒答前写的文字照打 stdout,
135
137
  非 `--json` 形 stderr 一行点名停因(`task stopped (haltedOnUserRejection) …`)。
138
+ - **没有人被问的拒记在部署政策名下,不记在你名下**(7.110.0 起):无 TTY(或参数展示不出)时门记录是
139
+ `policy_refused` / `who.party: "none"`,模型读引擎的策略句而不是「用户不想继续」,接着做不需要审批的部分;
140
+ 那次调用没执行 ⇒ run 照样退 1,stderr 停因行 `task incomplete (approval unanswered) …`。
136
141
  - **一键全栈部署**(DB + 对象存储 + registry 网站 + 沙箱池,docker/k8s 双路径):
137
142
  [`sema-agent/sema-deploy`](https://github.com/sema-agent/sema-deploy)。
138
143
  - **沙箱装包源**:缺省 = 官方源(pypi/npmjs/crates.io/…)。中国大陆部署配
package/USAGE.md CHANGED
@@ -690,7 +690,7 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启公
690
690
  | 记忆整理阀门判决 + 驱动席(`MEMORY_CONSOLIDATION_DRIVER=on`,或未设而出厂缺省按目录派生 —— 7.104.0 起) | 重启生效 | 阀门开关与整理跑用的模型都在启动期定一次;中心改了 roles / 档位 / 目录条目后,**判决会翻**(派生开 ↔ 关,含只把主模型挪到整理座那一条上)或开着而座漂了 ⇒ `/health` 报 `restartRequired`,`restart.reasons` 含 `memory-consolidation`(7.100.0 新理由;7.104.0 起派生关的那台同样报) |
691
691
 
692
692
  生效与否的观测口=`/health` 世代账键(`configTargetVersion`/`configAppliedVersion`+两 ordinal+`configApplyStaleMs`,见 API 表 `/health` 行)——target≠applied 持续=「改了没生效」的机读信号。
693
- - **`run-local`(一次性本机 CLI)上三张审批名单同样执法**(7.101.0 起,TTY 问 / 无 TTY 拒):`APPROVAL_DENY` 当场拒;`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ stdin 是 TTY 就 `y/N` 问人,否则当场拒并在 stderr 点名三张名单键;`APPROVAL_AUTO_BUDGET` 照常生效(never-auto 不吃预算);TTY 问句带上这次调用的参数,拒答而停下的 run 退 1。这条腿没有 checkpoint 店也**不**拒启 —— 它有一只当场作答的席,名单的 ask 不需要 park(上表「无 checkpoint 店 ⇒ 整组拒」只说服务腿)。此前三张名单在这条腿上无声不生效。
693
+ - **`run-local`(一次性本机 CLI)上三张审批名单同样执法**(7.101.0 起,TTY 问 / 无 TTY 拒):`APPROVAL_DENY` 当场拒;`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ stdin 是 TTY 就 `y/N` 问人,否则当场拒并在 stderr 点名三张名单键;`APPROVAL_AUTO_BUDGET` 照常生效(never-auto 不吃预算);TTY 问句带上这次调用的参数,人答 N 而停下的 run 退 1(停因 `haltedOnUserRejection`)。**无 TTY 的拒记在部署政策名下**(7.110.0 起,S-956;此前记成人拒):该次调用 `tool_end.gate.settlement` = `{kind:"policy_refused", who:{party:"none"}}`,模型读到的是引擎的策略句(「部署政策拒了、此处没有交互审批,别重试,先做不需要审批的部分」)加一句拒因,而不是「用户不想继续,停下来等他」—— 不停批,模型接着做别的部分;那只要人批的动作没执行 ⇒ run 照样退 1,非 `--json` 形 stderr 停因行是 `task incomplete (approval unanswered): …`。参数展示不出而不问就拒的那一形(TTY 在场也一样)同此。这条腿没有 checkpoint 店也**不**拒启 —— 它有一只当场作答的席,名单的 ask 不需要 park(上表「无 checkpoint 店 ⇒ 整组拒」只说服务腿)。此前三张名单在这条腿上无声不生效。
694
694
  - **仅 `/v1/tasks`(同步)+ `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多次尝试非单流,400)。与 `verify` **互斥**(同时给 → 400)。
695
695
  - 成本上界 = 任务的 `maxCostUsd`(防冷重跑税)。**三条 core 警示**:① 每档**冷重跑**重付输入成本(便宜档常过才划算);② 门收到**未脱敏**输出(自定义门转发外部 verifier 要脱敏);③ **写工具会跑 N 次**——**只用于只读/幂等任务**(每次升级整任务重跑)。开放式任务(找全 bug/文笔)没有可判定 oracle、级联会空转,那种用 `discuss`(广度交叉)而非级联(深度阶梯)。
696
696
 
@@ -873,7 +873,7 @@ UNATTENDED_APPROVAL_POLICY=park
873
873
  `ordinary` 的问 —— 模式写门对普通写的问、形状层对非只读 shell 的问(写与命令照常落地,与 7.105.0 同)。其余每一类都不答:
874
874
  写保护表目标(`write_protection`)、shell 读会话目录外(`probe`)、MCP / A2A 工具标记、transcript 写、规则店读不出、共享制品读、
875
875
  plan 的写问(`mode`),以及运维 / hook / 规则 / 组织为人提的问(审批名单、治理层、`APPROVAL_NEVER_AUTO` …)。不答的问:
876
- - **有席的腿交给席**:run-local 在终端里跑 ⇒ 问你 y/N;没有 TTY ⇒ 当场拒(stderr 一句)。
876
+ - **有席的腿交给席**:run-local 在终端里跑 ⇒ 问你 y/N;没有 TTY ⇒ 当场拒(stderr 一句;门记录记在部署政策名下 `policy_refused` / `who.party:"none"`,7.110.0 起,S-956)。
877
877
  - **无席的腿**(leader 各腿):配了 durable 审批块的部署 ⇒ park,等人经 `/decide` 答(与 7.105.0 同形);没配 ⇒ 当场拒(fail-closed)。
878
878
  - 答集是常量(只 `ordinary`,core 推荐形),**不是旋钮**。7.106.0–7.108.0 的 `UNATTENDED_PERMISSION_MODE` 已退役:**设了即拒启**
879
879
  (`config.env_key_retired`,任何值都拒);从 `.env` 里删掉它。要让机器多答某一类问,先立案,不开旋钮。
@@ -436,11 +436,12 @@ type ClosedWordOf<S extends ClosedWordSeat> = NonNullable<ToolApprovalPark[S]>;
436
436
  *
437
437
  * 判据(display,不是裁决输入 —— 裁决在 core 的仲裁点,本仓无人值守腿的按类作答读的是 core 交来的 `AskRequest.classes` 本身,不经本函数):
438
438
  * · **两键都缺席 ⇒ 缺席**(不计数):老引擎(core 7.37.0 之前)、老行(续跑不补,KL-1214-b)、不盖 `origin` 的站(KL-1214-a)。缺席从不编默认类;
439
- * · **在场 ⇒ 必须是一整只好对**:`askClass` 过 core `isAskClass`;`classes` 是非空数组、每员过 `isAskClass`、首员 === `askClass`(core d.ts
440
- * 「`classes[0] === askClass`」「Present exactly when askClass is」)。半对 / 集外词 / 首员不符 ⇒ **两键都不投**(卡上少一行类 ≪ 卡上出现生词
441
- * 或半只对)+ 计一次 `server.approval-card.closed-word-out-of-set`(S-249 族,detail 前缀 `askClass=` / `classes=` 判别是哪一格读不出)。
442
- * · 不自造:本仓不重算类、不排序、不去重(core 的屏 `screenAskClassMembers` 在写停车行前已经筛过;它不在包根导出面上 —— 转 core 请求导出,
443
- * 收车档 ⑤)。投出去的 `classes` 是新数组(帧不与载体共享可变引用)。
439
+ * · **在场 ⇒ 读法就是 core 的屏** `screenAskClassMembers`(S-955 ⑥,core 7.38.0 #1249 导出;引擎在写停车行前用的同一只):`askClass` 在集、`classes`
440
+ * 非空数组且每员在集、无重复、按 `ASK_CLASSES` 规范序取首员 === `askClass`;屏收 ⇒ 投屏给的规范序副本。屏拒(半对 / 集外词 / 首员不符 /
441
+ * 重复员)⇒ **两键都不投**(卡上少一行类 ≪ 卡上出现生词或半只对)+ 计一次 `server.approval-card.closed-word-out-of-set`(S-249 族,detail 前缀
442
+ * `askClass=` / `classes=` 判别是哪一格读不出 —— 前缀只是诊断,判据只有屏一条)。
443
+ * · 本仓**零自写对判**(修前这里与 {@link ApprovalCardSchema} 的 superRefine 各写一份「`classes[0] === askClass`」,按输入首员比、收重复员,与引擎的
444
+ * 屏是两条规则)。投出去的 `classes` 是新数组(帧不与载体共享可变引用;屏给的是冻结数组)。
444
445
  */
445
446
  export declare function readAskClassPair(carrier: object): AskClassPair | undefined;
446
447
  /** 类对的投影形(帧 / 卡 / 队列行同一形):两键同在同缺。值型取 core 寄存行上的座(core 改型 ⇒ 下面的围栏编译红)。 */
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, isAskClass, isAskOrigin, isPersistedRuleMandate } from "@sema-agent/core";
2
+ import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, isAskClass, isAskOrigin, isPersistedRuleMandate, screenAskClassMembers } from "@sema-agent/core";
3
3
  import { redactSecrets } from "./trace/redact.js";
4
4
  import { recordFailOpen } from "./observability/fail-open.js";
5
5
  import { PERSISTED_RULE_MATCHES, UNCOVERED_SEGMENT_REASONS } from "./permission-rule-vocab.js";
@@ -289,9 +289,9 @@ export function readAskClassPair(carrier) {
289
289
  const classes = "classes" in carrier ? carrier.classes : undefined;
290
290
  if (askClass === undefined && classes === undefined)
291
291
  return undefined;
292
- if (isAskClass(askClass) && Array.isArray(classes) && classes.length > 0 && classes.every(isAskClass) && classes[0] === askClass) {
293
- return { askClass, classes: [...classes] };
294
- }
292
+ const screened = screenAskClassMembers(askClass, classes);
293
+ if (screened !== undefined)
294
+ return { askClass: screened.askClass, classes: [...screened.classes] };
295
295
  recordFailOpen("server.approval-card.closed-word-out-of-set", isAskClass(askClass) ? `classes=${outOfSetDetail(classes)}` : `askClass=${outOfSetDetail(askClass)}`);
296
296
  return undefined;
297
297
  }
@@ -365,13 +365,10 @@ export const ApprovalCardSchema = z
365
365
  })
366
366
  .strict()
367
367
  .superRefine((card, ctx) => {
368
- const hasClass = card.askClass !== undefined;
369
- const hasClasses = card.classes !== undefined;
370
- if (hasClass !== hasClasses) {
371
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: [hasClass ? "classes" : "askClass"], message: "askClass and classes must be present together" });
372
- }
373
- else if (hasClass && card.classes[0] !== card.askClass) {
374
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["classes"], message: "classes[0] must equal askClass" });
368
+ if (card.askClass === undefined && card.classes === undefined)
369
+ return;
370
+ if (screenAskClassMembers(card.askClass, card.classes) === undefined) {
371
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["classes"], message: "askClass / classes must be a pair the engine's own screen (screenAskClassMembers) admits" });
375
372
  }
376
373
  });
377
374
  export const ApprovalCardEnvelopeSchema = z
@@ -16,6 +16,7 @@ import { Runner, type RunnerDeps, type WorkflowRunStore } from "@sema-agent/core
16
16
  import type { ServiceConfig } from "../config.js";
17
17
  import { CostQuota } from "../observability/cost-quota.js";
18
18
  import type { RateLimiter } from "../observability/rate-limit.js";
19
+ import type { UserInputLedger } from "../user-input-ledger.js";
19
20
  import type { Logger } from "../observability/logger.js";
20
21
  import type { Metrics } from "../observability/metrics.js";
21
22
  import type { WorkflowNotifyGate, WorkflowNotifyJournalStore } from "../orchestration/workflow-notify-journal.js";
@@ -98,6 +99,8 @@ export interface ReapersCtx {
98
99
  * (`boot/permission-rules-audit.ts` 的 `DormantRuleCounter` 是同一条判据的先例)。
99
100
  * 生产传的仍是整只束(结构上满足这个窄口)。 */
100
101
  permissionRuleStores: PermissionRuleRetentionSweeper | undefined;
102
+ /** S-928 用户输入受理账本(stage-07 装配;缺席 = 无 backend)。每拍在 reapStale **之后**收敛一次(同一条腿串行,理由见 tick 里那一行)。 */
103
+ userInputLedger: Pick<UserInputLedger, "converge"> | undefined;
101
104
  /** S-280 直连门 nonce 的消费记录店 —— 本 tick 只用它的**到期清扫**一手。行至多活 ~150 秒(= 证明还能被
102
105
  * 受理的最后一刻),所以存量是「到达率 × TTL」而不是历史总量,清扫无需分批。无 backend 的 env-only
103
106
  * 部署 ⇒ undefined ⇒ 本腿零调用(那种部署也没有 durable 审批门)。 */
@@ -56,7 +56,7 @@ export function createPlacementAwareReapSessions(deps) {
56
56
  };
57
57
  }
58
58
  export function startReapers(ctx) {
59
- const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, } = ctx;
59
+ const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, getRunDenySweep, userInputLedger, } = ctx;
60
60
  const leg = (p) => {
61
61
  if (p !== undefined)
62
62
  trackMaintenanceLeg(p);
@@ -196,7 +196,8 @@ export function startReapers(ctx) {
196
196
  leg(runStore
197
197
  ?.reapStale(config.runStaleSec * 1000)
198
198
  .then(reapCount("runs_reaped_total", { kind: "stale" }))
199
- .then(() => runsReapStaleGuard.onSuccess(), runsReapStaleGuard.onError));
199
+ .then(() => runsReapStaleGuard.onSuccess(), runsReapStaleGuard.onError)
200
+ .then(() => userInputLedger?.converge().then(reapCount("user_input_converged_total", {}))));
200
201
  if (checkpointStore) {
201
202
  leg(checkpointStore
202
203
  .reapExpired(Date.now())
@@ -34,7 +34,7 @@ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, norma
34
34
  import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, isValidCwd, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
35
35
  import { assertRequestA2aUnlocked, resolveRequestA2a } from "../task-a2a.js";
36
36
  import { assertRequestMcpContentOrigin, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
37
- import { ENGINE_ABSENT_PERMISSION_MODE, MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
37
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
38
38
  import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
39
39
  import { redactSecrets } from "../trace/redact.js";
40
40
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
@@ -109,7 +109,7 @@ export function createResolveSpec(ctx) {
109
109
  if (why !== undefined)
110
110
  throw new HttpError(422, why, { code: HOST_POSTURE_REFUSED_CODE });
111
111
  }
112
- const lane = bindSettingsCwdEnvAndModel(body, auth, gated, effMode, opts);
112
+ const lane = bindSettingsCwdEnvAndModel(body, auth, gated, opts);
113
113
  if (auth?.sessionId && assemblyIncarnationRead)
114
114
  noteSessionIncarnation(auth.sessionId, await assemblyIncarnationRead(auth.sessionId));
115
115
  const anchors = await resolveHistoryAnchors(body, auth);
@@ -178,7 +178,7 @@ export function createResolveSpec(ctx) {
178
178
  }
179
179
  return { scenarioName, cap, hands: cap.hands, centerDecls, appendLessPack, acceptedAppend, objective, parsedSettings, attachmentNotice };
180
180
  };
181
- const bindSettingsCwdEnvAndModel = (body, auth, gated, effMode, opts) => {
181
+ const bindSettingsCwdEnvAndModel = (body, auth, gated, opts) => {
182
182
  const { objective, parsedSettings } = gated;
183
183
  const cwdWriteGate = cwdHonored(config) || deviceCwdHonored(config);
184
184
  const execLane = executionLaneOf(config.remoteExec?.provider);
@@ -218,7 +218,6 @@ export function createResolveSpec(ctx) {
218
218
  logger,
219
219
  sessionId: auth?.sessionId ?? "",
220
220
  cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
221
- permissionMode: effMode ?? ENGINE_ABSENT_PERMISSION_MODE,
222
221
  ...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
223
222
  hookLlm,
224
223
  hookAgent,
@@ -23,6 +23,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
23
23
  maxTurns: number | undefined;
24
24
  };
25
25
  taskTimeoutSec: number;
26
+ userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
26
27
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
27
28
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
28
29
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MODEL_REF } from "../model-select.js";
2
- import { probeSearchBackend } from "@sema-agent/core";
2
+ import { probeSearchBackend, uuidv7 } from "@sema-agent/core";
3
3
  import { selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
4
4
  import { subagentSendUserFileExtraTools } from "../capabilities/send-user-file-tool.js";
5
5
  import { parseCapEnv, parseNumOrFailNonNegative } from "../config.js";
@@ -16,6 +16,7 @@ import { explicitOperatorOk } from "../http/server.js";
16
16
  import { selfOrchestrationDenial } from "../task-workflow.js";
17
17
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
18
18
  import { reclaimOrphansAtBoot } from "../boot-reclaim.js";
19
+ import { createUserInputLedgerAtBoot, writerGoneEvidenceFor } from "../user-input-ledger.js";
19
20
  import { createParkedReviveInheritedGate } from "./parked-revive-gate.js";
20
21
  import { createParkedReviveSurface } from "../parked-revive-surface.js";
21
22
  import { createWebFetchSummarizeLane } from "./webfetch-summarize-lane.js";
@@ -23,7 +24,7 @@ import { createRunnerDepsOnAsk } from "./runner-deps.js";
23
24
  import { bindRateLimiter, bindRunStore } from "./late-binds.js";
24
25
  import { buildDisplaySafeUrl } from "../trace/redact.js";
25
26
  export async function runStage07(ctx) {
26
- const { approvalExemptionStore, backend, backgroundAgentStore, brain, checkpointStore, config, configCenter, counterDegradeHook, durableEnabled, handslessSubRunner, localRoot, logger, memoryEngine, metrics, question, runner, runnerDeps, sendUserFileToolSpec, sessionEnvSelection, sessionStore, subRunner, subRunnerSessions, taskListLane, toolApproval } = ctx;
27
+ const { approvalExemptionStore, backend, backgroundAgentStore, brain, checkpointStore, config, configCenter, counterDegradeHook, durableEnabled, handslessSubRunner, instanceId, localRoot, logger, memoryEngine, metrics, question, runner, runnerDeps, sendUserFileToolSpec, sessionEnvSelection, sessionStore, subRunner, subRunnerSessions, taskListLane, toolApproval } = ctx;
27
28
  const authorize = createAuthorizer(config, sessionStore);
28
29
  const runStore = backend ? backend.run() : undefined;
29
30
  bindRunStore(runStore);
@@ -50,6 +51,19 @@ export async function runStage07(ctx) {
50
51
  if (gateRefusal !== undefined)
51
52
  throw new Error(gateRefusal);
52
53
  await reclaimOrphansAtBoot(runStore, checkpointStore, logger);
54
+ const userInputLedger = backend && runStore
55
+ ? await (async () => {
56
+ const { ledger, converged } = await createUserInputLedgerAtBoot({
57
+ store: backend.userInputLedger(),
58
+ writer: instanceId,
59
+ evidence: writerGoneEvidenceFor(backend.kind, runStore),
60
+ newId: uuidv7,
61
+ });
62
+ if (converged > 0)
63
+ logger.warn("user_input_ledger_crash_converged", { converged, note: "steers accepted with 200 \"applied\" by a previous process that ended before its run settled were converged as cause:crashed; each session's next run announces them (task.user_steer_undrained) and GET /v1/sessions/:id lists them under userInputUndrained" });
64
+ return ledger;
65
+ })()
66
+ : undefined;
53
67
  const imageIndex = backend?.imageIndex ? backend.imageIndex() : undefined;
54
68
  const selectEnvTool = config.selectEnvironmentTool && config.remoteExec?.provider === "k8s" && imageIndex
55
69
  ? selectEnvironmentTool({
@@ -156,6 +170,6 @@ export async function runStage07(ctx) {
156
170
  const builtinScenarioNames = Object.keys(scenarioDetails);
157
171
  configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
158
172
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
159
- return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveSurface, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, webSearchProvider: webSearchCfg?.provider };
173
+ return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveSurface, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, userInputLedger, webSearchProvider: webSearchCfg?.provider };
160
174
  }
161
175
  //# sourceMappingURL=stage-07-capability-layer.js.map
@@ -36,6 +36,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
36
36
  maxTurns: number | undefined;
37
37
  };
38
38
  taskTimeoutSec: number;
39
+ userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
39
40
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
40
41
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
41
42
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -5,12 +5,12 @@ import { startRetentionLane, RETENTION_LEASE_TTL_FACTOR } from "./retention-lane
5
5
  import { createLimitSync } from "./limit-sync.js";
6
6
  import { runDenySweep } from "./late-binds.js";
7
7
  export function runStage08(ctx) {
8
- const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
8
+ const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, userInputLedger, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
9
9
  const reaper = startReapers({
10
10
  config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
11
11
  rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap,
12
12
  workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
13
- backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore,
13
+ backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, approvalNonceStore, userInputLedger,
14
14
  getRunDenySweep: () => runDenySweep,
15
15
  });
16
16
  const ttlMs = config.retentionSweep.intervalSec * 1000 * RETENTION_LEASE_TTL_FACTOR;
@@ -72,6 +72,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
72
72
  maxTurns: number | undefined;
73
73
  };
74
74
  taskTimeoutSec: number;
75
+ userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
75
76
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
76
77
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
77
78
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -72,6 +72,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
72
72
  maxTurns: number | undefined;
73
73
  };
74
74
  taskTimeoutSec: number;
75
+ userInputLedger: import("../user-input-ledger.js").UserInputLedger | undefined;
75
76
  webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
76
77
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
77
78
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
@@ -26,7 +26,7 @@ import { bindServerLateSeats } from "./late-binds.js";
26
26
  import { serviceVersion } from "../service-version.js";
27
27
  import { buildDisplaySafeUrl } from "../trace/redact.js";
28
28
  export async function runStage10(ctx) {
29
- const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, parkCommits, config, configCenter, consolidationValve, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, noteSessionIncarnation, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveSurface, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionInheritEnv, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storePosture, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
29
+ const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, parkCommits, config, configCenter, consolidationValve, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, noteSessionIncarnation, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveSurface, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionInheritEnv, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storePosture, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, userInputLedger, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
30
30
  const stores = {
31
31
  runStore,
32
32
  resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
@@ -48,6 +48,7 @@ export async function runStage10(ctx) {
48
48
  imageBakes,
49
49
  outcomeSink: outcomeSink ? outcomeSink : undefined,
50
50
  sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
51
+ userInputLedger,
51
52
  backend: backend ? backend : undefined,
52
53
  };
53
54
  let resolveFleetRecovery;
@@ -244,7 +244,7 @@ export const CONFIG_CATALOG = [
244
244
  r("MEMORY_SCOPE", "memory", "string", "单用户部署的记忆 scope 名", { derivedDefaultNote: "缺席:引擎开 ∧ 单用户 ⇒ local;其余 ⇒ 缺席(记忆暗)", configKey: "memoryScope" }),
245
245
  r("MEMORY_SYNC_MAX_PULL_ENTRIES", "memory", "number", "TOC 同步单轮 pull 条数帽(设了必须 ≥1 整数)", { derivedDefaultNote: "缺席 ⇒ 键缺席,core 默认" }),
246
246
  r("MEMORY_SYNC_MAX_PUSH_ENTRIES", "memory", "number", "TOC 同步单轮 push 条数帽(设了必须 ≥1 整数)", { derivedDefaultNote: "缺席 ⇒ 键缺席,core 默认" }),
247
- r("MEMORY_SYNC_SCOPE", "memory", "string", "TOC 同步 scope 覆写", { derivedDefaultNote: "缺席 ⇒ 从 MEMORY_SCOPE 铸 user 盘键" }),
247
+ r("MEMORY_SYNC_SCOPE", "memory", "string", "TOC 同步 scope 覆写", { derivedDefaultNote: "缺席 ⇒ = MEMORY_SCOPE,仅当它已是 typed 键;非 typed 键拒启(S-964)" }),
248
248
  r("MEMORY_SYNC_TOKEN", "memory", "secret", "TOC 同步中心面的 Bearer(URL 设了它必须在,半配拒启)", { danger: SEC }),
249
249
  r("MEMORY_SYNC_URL", "memory", "url", "TOC 同步中心地址(设了=开;file 记忆形专用,DB 后端配了拒启;同步只覆盖一层 ⇒ 单用户任务不按项目键控、写家落全局层 MEMORY_SCOPE(S-512 让位,启动通知 `memory_project_scope_yields_to_sync`))"),
250
250
  r("METRICS_TOKEN", "observability", "secret", "/metrics 只读令牌", { danger: SEC }),
@@ -680,8 +680,9 @@ export interface ServiceConfigFlat {
680
680
  memoryPersistenceCapable?: boolean;
681
681
  /** 142-S2.5-W1: TOC 同步 client 腿 —— file memory 形态把本地记忆盘与中心(DB 权威,
682
682
  * POST /v1/memory/sync/:scope)做一轮一 RTT 双向同步。env 三键:`MEMORY_SYNC_URL`(设了=开;缺省
683
- * undefined=纯本地现状零变化)/`MEMORY_SYNC_TOKEN`(Bearer)/`MEMORY_SYNC_SCOPE`(可选,缺省=
684
- * formatUserScope(memoryScope);已是 v2 typed key 则原样直通,见 loadConfig)。半配置 fail-loud
683
+ * undefined=纯本地现状零变化)/`MEMORY_SYNC_TOKEN`(Bearer)/`MEMORY_SYNC_SCOPE`(可选,缺省 =
684
+ * memoryScope 本身**仅当它已是 v2 typed key**;非 typed 键 ⇒ 拒启,S-964 —— 旧 formatUserScope 派生臂铸出的是本部署
685
+ * 从不写的层,见 loadConfig)。半配置 fail-loud
685
686
  * (memoryEngineBackend 同款姿势);DB backend(pg/tidb)配了这些键=boot throw(中心侧自己就是
686
687
  * 同步权威,不该再跑 client 腿)。 */
687
688
  memorySync?: {
package/dist/config.js CHANGED
@@ -3,11 +3,11 @@ import { DEFAULT_MODEL_REF } from "./model-select.js";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isModelPriced, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
6
+ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, isModelPriced, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
7
7
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES, CENTER_CREDENTIAL_ENV } from "@sema-agent/settings-schema";
8
8
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
9
9
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
- import { isV2ScopeKey, memoryReplicationCoverage } from "./memory-scope.js";
10
+ import { isV2ScopeKey, memoryReplicationCoverage, replicationCovers } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
13
  import { CONFIG_PROVIDER_WORDS, DB_BACKEND_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
@@ -1666,9 +1666,14 @@ function parseMemoryDomain(ctx) {
1666
1666
  if (memorySyncToken === undefined) {
1667
1667
  throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
1668
1668
  }
1669
- const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
1669
+ if (memorySyncScopeRaw === undefined && memoryScope !== undefined && !isV2ScopeKey(memoryScope)) {
1670
+ throw new Error(`MEMORY_SYNC_URL is set but MEMORY_SYNC_SCOPE is not set and MEMORY_SCOPE (${JSON.stringify(memoryScope)}) is not a typed scope key — the sync leg replicates exactly ONE scope by name ` +
1671
+ "(core syncMemoryScope takes the entries whose scope === the sync scope and POSTs them under that same name), so deriving `user:<MEMORY_SCOPE>` would name a scope this deployment never writes and nothing would ever be synced (S-964). " +
1672
+ "Either set MEMORY_SYNC_SCOPE to the scope to replicate, or set MEMORY_SCOPE to the typed key the center expects (e.g. user:<tenant>) so the sync scope defaults to the same name.");
1673
+ }
1674
+ const scope = memorySyncScopeRaw ?? memoryScope;
1670
1675
  if (scope === undefined) {
1671
- throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
1676
+ throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine with a typed MEMORY_SCOPE (e.g. user:<tenant>) so it provides one");
1672
1677
  }
1673
1678
  memorySync = {
1674
1679
  url: memorySyncUrl,
@@ -1680,15 +1685,22 @@ function parseMemoryDomain(ctx) {
1680
1685
  }
1681
1686
  const replication = memoryReplicationCoverage(memorySync !== undefined ? { memorySync } : {});
1682
1687
  if (replication.kind === "scopes" && memoryEngineEnabled && !requirePrincipal && memoryScope !== undefined) {
1688
+ const YIELD_HINT = {
1689
+ in: "memory sync (MEMORY_SYNC_*) replicates only the scope(s) listed in replicatedScopes — the per-project memory layer (S-512, repo:<key>) is not replicated, " +
1690
+ "so sessions do NOT write project-keyed memory here: new memory lands in the global layer (writeScope = MEMORY_SCOPE), which IS replicated, and every " +
1691
+ "project reads it. Project-layer replication is a separate change; unset MEMORY_SYNC_URL to get per-project memory without sync.",
1692
+ out: "memory sync (MEMORY_SYNC_*) replicates only the scope(s) listed in replicatedScopes — the per-project memory layer (S-512, repo:<key>) is not replicated, " +
1693
+ "so sessions do NOT write project-keyed memory here: new memory lands in the global layer (writeScope = MEMORY_SCOPE), every project reads it, " +
1694
+ "and that write scope is not among the replicated scopes either — NONE of the memory this deployment writes is synced. " +
1695
+ "Point MEMORY_SYNC_SCOPE at the write scope (sync replicates one scope), or unset MEMORY_SYNC_URL to stop expecting sync.",
1696
+ };
1683
1697
  CONFIG_NOTICES.push({
1684
1698
  level: "warn",
1685
1699
  event: "memory_project_scope_yields_to_sync",
1686
1700
  fields: {
1687
1701
  replicatedScopes: replication.scopes.join(","),
1688
1702
  writeScope: memoryScope,
1689
- hint: "memory sync (MEMORY_SYNC_*) replicates only the scope(s) listed — the per-project memory layer (S-512, repo:<key>) is not replicated, " +
1690
- "so sessions do NOT write project-keyed memory here: new memory lands in the global layer (MEMORY_SCOPE), the same as 7.97.0, and every " +
1691
- "project reads it. Project-layer replication is a separate change; unset MEMORY_SYNC_URL to get per-project memory without sync.",
1703
+ hint: YIELD_HINT[replicationCovers(replication, memoryScope) ? "in" : "out"],
1692
1704
  },
1693
1705
  });
1694
1706
  }
@@ -243,8 +243,14 @@ export declare function buildOnlySensitiveBaselineWarning(config: Pick<Deploymen
243
243
  * `applyRuntimeGovernance` 的 governance 实参预铸([ref] 件一)。
244
244
  *
245
245
  * 键存在性 profile 逐字保持消费腿原样:`autonomy`/`commandPolicy` 恒在场(值可为 `undefined`),
246
- * `manualModeShellGate`/`sensitivePathPolicy` 按在场性条件展开 —— `applyRuntimeGovernance` 的
247
- * `!== undefined` 判据对两者等价,但 profile 是折叠面的可观测字节,搬家不许顺手改。
246
+ * `manualModeShellGate`/`sensitivePathLayers` 按在场性条件展开 —— `applyRuntimeGovernance` 对缺席与空的判读等价,
247
+ * 但 profile 是折叠面的可观测字节,搬家不许顺手改。
248
+ *
249
+ * 🔴 **S-958(core #1266 7.39.0)—— 守卫集交叶,不交折叠**:修前这里铸一只预折的 `sensitivePathPolicy`
250
+ * (`combinePolicies(真身层, 誊本门)`,沙箱形再套一层两形的折叠),治理层随后在**整个治理折叠的输出上**展开盖部署出身。
251
+ * 现在治理层逐叶盖位、只折一次(`runtime-governance.ts` `applyRuntimeGovernance` 顶注末段),所以本口交出的是叶的列表:
252
+ * 本口不知道出身这件事,也不再有自己的折叠点。叶的顺序 = 修前折叠的顺序(真身层 → 词法层 → 誊本门),deny 优先 / 首问 / 成员合并
253
+ * 的读法因此不变。
248
254
  */
249
255
  export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication, roots: DeploymentGovernanceRoots): DeploymentGovernanceInputs;
250
256
  /**
@@ -84,7 +84,7 @@ export function buildOnlySensitiveBaselineWarning(config, seat) {
84
84
  };
85
85
  }
86
86
  export function createDeploymentGovernanceInputs(config, pathAdjudication, roots) {
87
- const sensitivePathPolicy = config.sensitiveWritePatterns.length > 0
87
+ const sensitivePathLayers = config.sensitiveWritePatterns.length > 0
88
88
  ? (() => {
89
89
  const dataRootOpt = roots.engineDataRoot !== undefined ? { dataRoot: roots.engineDataRoot } : {};
90
90
  const realTarget = createSensitivePathPolicy({
@@ -93,20 +93,21 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
93
93
  ...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
94
94
  ...dataRootOpt,
95
95
  });
96
- const transcriptGuard = config.localDataRoot !== undefined
97
- ? createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })
98
- : undefined;
99
- const withTranscripts = (p) => (transcriptGuard ? combinePolicies(p, transcriptGuard) : p);
96
+ const transcriptGuard = config.localDataRoot !== undefined ? [createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })] : [];
100
97
  if (pathAdjudication.cwd !== undefined)
101
- return withTranscripts(realTarget);
102
- return withTranscripts(combinePolicies(scopedToWriteTargetForm(realTarget, "absolute-or-stamped"), scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative")));
98
+ return [realTarget, ...transcriptGuard];
99
+ return [
100
+ scopedToWriteTargetForm(realTarget, "absolute-or-stamped"),
101
+ scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative"),
102
+ ...transcriptGuard,
103
+ ];
103
104
  })()
104
105
  : undefined;
105
106
  return {
106
107
  autonomy: config.autonomy,
107
108
  commandPolicy: config.commandPolicy,
108
109
  ...(config.manualModeShellGate ? { manualModeShellGate: config.manualModeShellGate } : {}),
109
- ...(sensitivePathPolicy ? { sensitivePathPolicy } : {}),
110
+ ...(sensitivePathLayers ? { sensitivePathLayers } : {}),
110
111
  };
111
112
  }
112
113
  export function createApprovalBaselinePolicy(config, seat) {
@@ -63,7 +63,6 @@ export interface HookRunnerContext {
63
63
  sessionId: string;
64
64
  /** hook 命令的工作目录 + payload `cwd` + `CLAUDE_PROJECT_DIR`(host lane 的 session cwd;无则 server cwd)。 */
65
65
  cwd: string;
66
- permissionMode?: string;
67
66
  /** 请求自带的 `settings.env`(单用户闸后)——hook 进程 env 与 http $NAME 插值的用户变量通道。 */
68
67
  shellEnv?: Record<string, string>;
69
68
  /** 阶段三b:`prompt` 条目的模型调用载体(部署组装:一次非流式 completion,费用不折 task budget)。
@@ -175,7 +174,8 @@ export declare function createTaskHooks(config: HooksConfig, ctx: HookRunnerCont
175
174
  * 把部署级 hooks 基线(deps.hooks,如 TOOL_TRACE 观测)折进 task 级 hooks——core 是整槽覆盖
176
175
  * (`spec.hooks ?? deps.hooks`),不折叠就静默 shadow 部署观测(runtask gateBaseline 注释点名的坑)。
177
176
  * 语义:部署槽先跑(观测看到原始输入/输出),task 槽后跑;结果合并:
178
- * - preToolUse:deny > ask > allow 折叠;updatedInput 串行线程(部署重写喂给 task 槽);context 拼接。
177
+ * - preToolUse:deny > ask > allow 折叠;updatedInput 串行线程(部署重写喂给 task 槽);context 拼接。读不出词的结果
178
+ * (非对象、集外 `action`)原样交回、由引擎的接手屏判(S-959,短路同 deny)。
179
179
  * - postToolUse/postToolUseFailure/postToolBatch:双跑,additionalContext 拼接,updatedOutput 取
180
180
  * 后者(task 槽)优先。
181
181
  * - userPromptSubmit/stop:部署 block 优先短路(不再跑 task 槽),否则双跑、context 拼接、task block 生效。