@sema-agent/server 7.15.0 → 7.16.0-rc.1
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 +36 -2
- package/dist/approval-card.d.ts +65 -0
- package/dist/approval-card.js +54 -6
- package/dist/approval-reconciler.d.ts +17 -1
- package/dist/boot/resolve-spec.js +31 -4
- package/dist/boot/runner-deps.d.ts +41 -2
- package/dist/boot/runner-deps.js +43 -0
- package/dist/boot/session-faces.js +26 -4
- package/dist/boot/shutdown.js +17 -0
- package/dist/boot/stores.js +20 -0
- package/dist/config-center/apply-effective.d.ts +14 -0
- package/dist/config-center/apply-effective.js +81 -1
- package/dist/config-types.d.ts +28 -2
- package/dist/config.js +38 -2
- package/dist/fleet/fleet-terminal-window.d.ts +12 -0
- package/dist/fleet/fleet-terminal-window.js +27 -4
- package/dist/http/active-run-conflict.d.ts +41 -1
- package/dist/http/active-run-conflict.js +24 -10
- package/dist/http/routes/approvals-assistant.d.ts +11 -3
- package/dist/http/routes/approvals-assistant.js +97 -20
- package/dist/http/routes/capabilities.js +8 -1
- package/dist/http/routes/diagnostics.d.ts +18 -0
- package/dist/http/routes/diagnostics.js +26 -0
- package/dist/http/routes/runs.js +63 -17
- package/dist/http/routes/side-query.js +15 -1
- package/dist/http/routes/tasks.js +32 -3
- package/dist/http/server.js +67 -13
- package/dist/leader/fanout.d.ts +18 -0
- package/dist/leader/fanout.js +34 -1
- package/dist/leader/leader.js +9 -5
- package/dist/leader/wire.js +16 -5
- package/dist/main.js +1 -0
- package/dist/model-select.d.ts +43 -1
- package/dist/model-select.js +70 -2
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +3 -0
- package/dist/parent-watch.d.ts +57 -0
- package/dist/parent-watch.js +108 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +133 -7
- package/dist/plugins/local-checkpoint-store.js +11 -1
- package/dist/plugins/memory-embedder-fingerprint.d.ts +119 -0
- package/dist/plugins/memory-embedder-fingerprint.js +280 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +0 -3
- package/dist/plugins/permission-rule-store-sql.js +1 -7
- package/dist/plugins/pg-pool.js +3 -0
- package/dist/plugins/store-backend.d.ts +5 -6
- package/dist/plugins/store-backend.js +4 -1
- package/dist/plugins/store-contracts.d.ts +17 -0
- package/dist/plugins/store-contracts.js +33 -0
- package/dist/plugins/tidb-pool.js +7 -0
- package/dist/plugins/tool-result-store-sql.d.ts +18 -13
- package/dist/plugins/tool-result-store-sql.js +50 -29
- package/dist/run-local.js +1 -0
- package/dist/tool-approval.d.ts +9 -0
- package/dist/tool-approval.js +74 -3
- package/dist/trace/project.js +8 -0
- package/dist/trace/redact.d.ts +14 -1
- package/dist/trace/redact.js +14 -2
- package/package.json +3 -3
package/USAGE.md
CHANGED
|
@@ -167,9 +167,17 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
|
|
|
167
167
|
| `MEMORY_EMBEDDER_API_KEY` | 缺省 | 可选 Bearer(自托管 TEI/vllm/ollama 通常不需要;公有云端点需要) |
|
|
168
168
|
|
|
169
169
|
⚠️ **半配 = 拒启**:上表前三键是一个整体,少任何一件都启动报错并**点名缺哪几键**。静默忽略的后果不是「功能缺席」而是「operator 以为向量面开着」——检索照常出结果(词面档),差异只在答案质量里,零日志。同理:只配 `TIMEOUT`/`API_KEY` 而三键不全、或 `MEMORY_ENGINE=off` 却配了 embedder,都是死旋钮,一律拒启。
|
|
170
|
-
⚠️ **维度守卫是拒启/抛错,不是 warn**:向量列写错维度是**静默毒库**——本仓 pg 记忆表的 embedding 列是 `jsonb`(无维度约束),错长度向量既不会让 DB 报错,也不会让检索报错(那些行只是悄悄退回词面档)。所以坏 `DIM`(非正整数)启动即拒;运行期端点返回的向量长度与 `DIM` 不符,embedder
|
|
170
|
+
⚠️ **维度守卫是拒启/抛错,不是 warn**:向量列写错维度是**静默毒库**——本仓 pg 记忆表的 embedding 列是 `jsonb`(无维度约束),错长度向量既不会让 DB 报错,也不会让检索报错(那些行只是悄悄退回词面档)。所以坏 `DIM`(非正整数)启动即拒;运行期端点返回的向量长度与 `DIM` 不符,embedder **直接抛错**,绝不截断/补零。换 `MODEL`/`DIM` 的**清空由指纹门自动做**(见下条),不必手动清。
|
|
171
171
|
⚠️ **embedder 出故障的方向**:端点 5xx/超时会让当次记忆**写入**报一条 `io error: …` 冲突(不写坏向量),检索腿则退回词面档继续服务——即「坏了退回 lexical」,不是「坏了写坏数据」。故障有专门信号:metric `memory_embed_failed_total{backend}` + 日志 `memory_embedder_failed`(只看 PatchReport 里的冲突会把供应商故障误读成并发改动)。
|
|
172
|
-
⚠️ **只对新写入的条目生效(没有回填腿)**:开启前就存在的记忆条目 embedding 列是空的,只有被再次写入时才会补上向量;这些行在检索里按词面档参与排序(不会被丢掉,但也享受不到向量档)
|
|
172
|
+
⚠️ **只对新写入的条目生效(没有回填腿)**:开启前就存在的记忆条目 embedding 列是空的,只有被再次写入时才会补上向量;这些行在检索里按词面档参与排序(不会被丢掉,但也享受不到向量档)。要立刻全量生效,得自己重写一遍语料。
|
|
173
|
+
🔴 **换 `MODEL`/`DIM` 的行为:下次 boot 自动清空向量列(embedder 指纹门,7.15.0 之后的下一个发布起)**。本服务在 `agent_memory_engine_meta` 单行元表里记住当前 embedder 的**指纹** `{model, dimensions}`(明文,便于排障;**endpoint 不进指纹**——换域名/代理不换语义空间)。每次启动比对一次:
|
|
174
|
+
- **相等** ⇒ 零动作、零写;
|
|
175
|
+
- **不等 / 元表还没有这一行而库里已有向量 / 那一行读不出来**(手改、回滚残留)⇒ **先把 `agent_memory_engine_entry.embedding` 整列清成 NULL,再写新指纹**(次序不可换:反过来若中途崩溃,新指纹已记而旧向量还在,此后每次 boot 都判「相等」,残留永不复检);
|
|
176
|
+
- 启动日志 `memory_embedder_identity_changed` 带**旧值/新值/清了几行/耗时 ms/原因**(`changed` | `unattributed` | `unreadable`),首次记录指纹是 `memory_embedder_identity_armed`;比对腿**自身失败一律拒启**(吞掉 = 门在场却没跑成)。⚠️ 这条行按「identity 变了」发,**不是**按「清了几行」发:当时库里恰好没有向量可清(上一轮已清过 / 缺席窗内被写成 NULL)照样发,`cleared: 0` —— 换 embedder 这件事在启动日志里不该无痕。真正零响亮的只有「相等」那一档。
|
|
177
|
+
- 清空**只丢可再生的向量缓存**(正文/frontmatter 分毫不动);清完**不回填**,行被下次写入时自然按新模型重嵌,期间检索退回词面档。⚠️ **误配代价诚实说**:把 `MODEL` 改错一次再改回来,向量**不会自动回来**——只随行被再次写入才重嵌;对写完就不动的记忆库,误配一次 = 向量面实质长期缺席(正文零损失,检索退 lexical)。要立刻恢复只能自己重写一遍语料。
|
|
178
|
+
- ⚠️ **换 embedder 请停机换:先 `scale 0`,再起**。指纹门管的是 **boot 面**;**滚动升级窗**里新副本清完列之后,仍在服役的旧副本还在按**旧**模型写向量,滚动结束后表里混着两个空间而元表已是新指纹 ⇒ 此后恒判「相等」,门再也不会复检。这是纪律面的残余,不假装根治(行级 embedder 记账列可结构性根治,超出本件射程)。滚动事故后的补救 = 手动清一次:`UPDATE agent_memory_engine_entry SET embedding = NULL WHERE embedding IS NOT NULL;`(同一条也可用于「误配后立刻重置」)。
|
|
179
|
+
- **没配 `MEMORY_EMBEDDER_*` 时门整个不装**(元表不动,旧指纹保留),启动日志 `memory_embedder_identity_absent` 说明:缺席期间**被改写过**的行,其 embedding 本来就被写成 NULL,所以「配置回归同一 identity 后向量续用」只对缺席期间**未被触碰**的行成立。
|
|
180
|
+
- 残余:同名模型在不同供应商若其实是不同实现(自托管 finetune 撞名),指纹辨不出——明知异实现时改 `MEMORY_EMBEDDER_MODEL` 名,或手动跑上面那条清列 SQL。`MEMORY_ENGINE_BACKEND=tidb` 无向量面(v1 词面档),没有这张元表也没有这道门。
|
|
173
181
|
📋 **档位对运维可见**:启动日志 `memory_engine_enabled` 行带 `vectorMode` 字段(取自后端实例的真值,不是配置推断)——`lexical` 在跑就必须看得见;配了 embedder 时同行还带 `embedderModel`/`embedderDim`,换模型这件事在日志里留痕。
|
|
174
182
|
- **org 记忆准入(design/170 件A,7.0.0 起 BREAKING)**:`org:*` 记忆 scope 分**两个来源**——部署自证
|
|
175
183
|
(env `MEMORY_SCOPE` 的 org 形 + **单用户部署**的 `projects[].defaultScopes` org 键)直通;**多租户**
|
|
@@ -343,6 +351,32 @@ MCP_ELICITATION_TTL_MS=300000 # 一张没人填的表单挂多久后释放(
|
|
|
343
351
|
- 总开关关着时四个节流钮解析照跑但无消费者——不设=行为不变。帧格式见
|
|
344
352
|
[`docs/ASSISTANT-WIRE-CONTRACT.md` §4a-quater](docs/ASSISTANT-WIRE-CONTRACT.md)(`elicitation` / `elicitation_complete`)。
|
|
345
353
|
|
|
354
|
+
**可选 — 父进程存活监视(`SEMA_PARENT_PID`,默认不装配)**
|
|
355
|
+
|
|
356
|
+
壳(cli/TUI/桌面)自己 spawn 引擎的**同机形**里,壳被 `SIGKILL`(崩溃、`kill -9`、OOM killer)之后没有
|
|
357
|
+
任何清理钩子跑得起来:引擎被 reparent 到 init **继续活着**,占着端口、库连接池和模型配额,而且再没有人
|
|
358
|
+
会给它发 `SIGTERM`。设了这个键,引擎就自己盯着那个 pid:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
SEMA_PARENT_PID=$$ # 壳把自己的 pid 传给它 spawn 出来的引擎
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- **缺席 = 整件不装配**(opt-in)。不设 = 和以前**逐字节一致**,存量部署零变化。
|
|
365
|
+
- **探活** = `process.kill(pid, 0)`(不投递任何信号,只问"这个 pid 在不在")。`ESRCH` = 父不在;
|
|
366
|
+
**`EPERM` = 父还活着**(进程在,只是本进程无权给它发信号 —— 父跑在另一个 uid 下时很常见)。
|
|
367
|
+
- **连续两拍**(拍频固定 **15s**)都是 `ESRCH` 才判死,单拍抖动不误杀。判死后走的是**和 `SIGTERM`
|
|
368
|
+
逐字同一条**优雅排空腿(`DRAIN_GRACE_MS` 那条):in-flight 的 run 照常结算 / park,不是裸退出。
|
|
369
|
+
- **拍频不开旋钮**(15s 写死)。有真需求再议 —— 这里刻意不增殖一个只有一个人会调的键。
|
|
370
|
+
- 日志:装配时一行 `parent_watch_armed`(带 `pid`),自退时一行 `parent_watch_exit`(带两拍的时间戳)。
|
|
371
|
+
- 坏值(`0` / 负数 / 小数 / 非数字 / 超过 `2147483647`)**拒启并指路**:`0` 和负数在 `process.kill` 里的
|
|
372
|
+
语义是**进程组**,超出 int32 的值 `process.kill` 根本收不下(会变成"armed 了但永远探不动")。要关掉就
|
|
373
|
+
**不设**这个键。探活若给出既不是 `ESRCH` 也不是 `EPERM` 的 errno,会打一条(仅一条)
|
|
374
|
+
`parent_watch_probe_error` 并按"父还活着"处理 —— 监视此时是失效的,那行日志就是它唯一的告警。
|
|
375
|
+
- 🔴 **已知残余(不收窄,两条)**:① 两拍窗口(≤30s)内父 pid 被系统**复用**时,探活看到的是新进程,
|
|
376
|
+
监视判"父还活着"而不自退;② 父进程退出后成了**僵尸**(它自己的上级不回收子进程 —— 容器里的朴素
|
|
377
|
+
PID 1 是典型;终端 / launchd / systemd 不在此列),pid 表项仍在,探活同样判"活着"。两条同源:
|
|
378
|
+
`kill(pid, 0)` 是**弱身份**判据。这条腿是**尽力自愈**,不是强一致的父子生命周期绑定。
|
|
379
|
+
|
|
346
380
|
**布尔旋钮的取值与极性(运维必读)**
|
|
347
381
|
|
|
348
382
|
布尔 env **只认 `true` / `false` 两个字面量**。写成 `1` / `yes` / `TRUE` ⇒ 该旋钮退回自己的缺省值,并在启动时
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -26,6 +26,71 @@ import type { AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
|
26
26
|
* 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
|
|
27
27
|
* 不能指望壳去截——一条 100KB 的 agentName 在 server 侧就该被拒,而不是变成一张撑爆呈卡面的卡。 */
|
|
28
28
|
export declare const MAX_AGENT_NAME = 200;
|
|
29
|
+
/**
|
|
30
|
+
* **一条**规则候选的形 —— 同步腿(`ApprovalCardSchema.ruleSuggestions` / `card_json`)与耐久腿
|
|
31
|
+
* (durable park 行的 `PendingCheckpoint.ruleSuggestions`,由 `plugins/checkpoint-store-sql.ts` 窄读)
|
|
32
|
+
* **共用这一份**。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 单一属主(宪法 [2704]「schema 单一属主禁复制」):两条腿投的是 core 的**同一个** `RuleSuggestion`
|
|
35
|
+
* (同步腿 = `AskRequest.ruleSuggestions`,耐久腿 = `PendingAction.ruleSuggestions`,core
|
|
36
|
+
* `checkpoint-store.d.ts` 明写「CONTRACT (same as the synchronous `AskRequest.ruleSuggestions`)」)。
|
|
37
|
+
* 各写一份 zod 必然漂——闭词表 `match` 加词、上限改口径,只改一处就出两种形。
|
|
38
|
+
*/
|
|
39
|
+
export declare const RuleSuggestionSchema: z.ZodObject<{
|
|
40
|
+
rule: z.ZodString;
|
|
41
|
+
match: z.ZodEnum<{
|
|
42
|
+
exact: "exact";
|
|
43
|
+
prefix: "prefix";
|
|
44
|
+
}>;
|
|
45
|
+
command: z.ZodString;
|
|
46
|
+
}, z.core.$strict>;
|
|
47
|
+
/**
|
|
48
|
+
* {@link RuleSuggestionSchema} 的**不限长**孪生 —— 只判**结构**(两个字符串 + 闭词表 `match`),长度由
|
|
49
|
+
* 调用方在**脱敏之后**自己截。
|
|
50
|
+
*
|
|
51
|
+
* 🔴 为什么必须有它(codex 对抗复审 [medium],验真后修):`redactSecrets` **会变长**(一条
|
|
52
|
+
* `password=…` 换成更长的遮蔽标记)。拿带 `.max()` 的形去校验**脱敏前**的原文,再脱敏,产出的就可能是
|
|
53
|
+
* 一条超限的文本;而任何**回读**路径若再跑一遍同一个函数,那一条会在第二次校验时被整条丢掉 —— 于是
|
|
54
|
+
* 「落库时在、读回时不在」,并且**只发生在有回读的那条腿上**(SQL 有列回读、LOCAL 直接从 blob 投),
|
|
55
|
+
* 两个后端对同一份素材给出不同的卡面。同族先例逐字在 `tool-approval.ts` 的 `persistedRuleShadowed`
|
|
56
|
+
* 发帧点(「先脱敏再截,一次算定」)。
|
|
57
|
+
*
|
|
58
|
+
* ⇒ 正确顺序:**结构判(本 schema)→ 脱敏 → 截长 → 结果按 {@link RuleSuggestionSchema} 必然合形**。
|
|
59
|
+
* 这样函数对自己的输出**幂等**,两条腿同形。
|
|
60
|
+
*
|
|
61
|
+
* 🔴 **未知键 `strip`(合并码重扫放宽,重扫二轮改准形):窄读只判三键结构,多余键剥掉、且不拒收**。
|
|
62
|
+
* base 带 `.strict()`,而 zod 4 的 `.extend()` **保留** strict —— core 给 `RuleSuggestion` 加一个 additive
|
|
63
|
+
* 字段(它对这条面一贯的做法),逐条 `safeParse` 会**条条失败**,于是耐久腿的整只候选静默清零、列落
|
|
64
|
+
* NULL、读口省键,把「有供给」谎报成「无供给」,正是本键存在理由的反面(`checkpoint-store-sql.ts` 顶注与
|
|
65
|
+
* wire 契约都写死「缺席=真的没有候选」)。同步腿的 `buildApprovalCard` 是显式三键投影、对加字段天然免疫
|
|
66
|
+
* ⇒ 不放宽这一份,两条腿会在「上游长一格」这条轴上分家。
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ **写的是 `.strip()` 不是 `.loose()`**(重扫二轮,实测更正):zod 4 的 `loose` = **passthrough** ——
|
|
69
|
+
* 未知键**原样留在** `parsed.data` 里,与本注和契约文写的「多余键剥掉」相反。放宽要的是「不拒收」,
|
|
70
|
+
* 剥键是另一件事,`.loose()` 只做到了前者。旧形下真正在剥键的是 {@link boundedRuleSuggestions} 里那三行
|
|
71
|
+
* 显式投影,于是本 schema 对「剥键」这条判据是**空跑**;而它是**导出符号**,output 类型带 index signature,
|
|
72
|
+
* 任何新消费者拿 `parsed.data` 直投就把上游未知键(其上**没有** `redactSecrets` 覆盖)带上跨租户可见的
|
|
73
|
+
* `GET /v1/approvals`。⇒ 改默认 strip 形:additive 加键照收、未知键当场剥掉,两个意图各自成立。
|
|
74
|
+
* 落库/读回的 {@link ApprovalCardSchema} 仍 `.strict()`(投影只写三键,多余键根本到不了那一层)。
|
|
75
|
+
*/
|
|
76
|
+
export declare const RuleSuggestionRawSchema: z.ZodObject<{
|
|
77
|
+
match: z.ZodEnum<{
|
|
78
|
+
exact: "exact";
|
|
79
|
+
prefix: "prefix";
|
|
80
|
+
}>;
|
|
81
|
+
rule: z.ZodString;
|
|
82
|
+
command: z.ZodString;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
/**
|
|
85
|
+
* 候选**基数**的 server 执法上限。两条腿同用。
|
|
86
|
+
*
|
|
87
|
+
* 🔴 **两个数,别混**(合并码重扫,档实对齐):core 的契约是 **≤2**(`exact` 恒 index 0,至多再跟一条
|
|
88
|
+
* reviewed `prefix` 形 —— core `checkpoint-store.d.ts` 与 `permission-rule-model.d.ts` 逐字),那是**今天
|
|
89
|
+
* 真实的**上限;本常量是 server 侧的**容忍帽**,刻意留在 4:一个被改坏/未来放宽的上游不该让运维队列
|
|
90
|
+
* 的一行被整只拒收(同步腿这一格进的是 `.strict()` 的卡 schema,收紧到 2 会把「多给一条展示用候选」
|
|
91
|
+
* 变成铸卡失败——在一条纯展示轴上换来一次 park,方向反了)。⇒ 消费端按 **≤4** 布局,契约文同口径成文。
|
|
92
|
+
*/
|
|
93
|
+
export declare const MAX_RULE_SUGGESTIONS = 4;
|
|
29
94
|
/**
|
|
30
95
|
* design/172 §3.1 的**中性投影**——呈卡面看到的全部内容,与 wire 面的既有 `ToolApprovalFrame` 解耦。
|
|
31
96
|
*
|
package/dist/approval-card.js
CHANGED
|
@@ -28,6 +28,59 @@ import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
|
28
28
|
export const MAX_AGENT_NAME = 200;
|
|
29
29
|
/** 中性投影里的自由文本/标识符统一限长(工具名、toolCallId、sourceTaskId)。 */
|
|
30
30
|
const MAX_IDENT = 200;
|
|
31
|
+
/**
|
|
32
|
+
* **一条**规则候选的形 —— 同步腿(`ApprovalCardSchema.ruleSuggestions` / `card_json`)与耐久腿
|
|
33
|
+
* (durable park 行的 `PendingCheckpoint.ruleSuggestions`,由 `plugins/checkpoint-store-sql.ts` 窄读)
|
|
34
|
+
* **共用这一份**。
|
|
35
|
+
*
|
|
36
|
+
* 🔴 单一属主(宪法 [2704]「schema 单一属主禁复制」):两条腿投的是 core 的**同一个** `RuleSuggestion`
|
|
37
|
+
* (同步腿 = `AskRequest.ruleSuggestions`,耐久腿 = `PendingAction.ruleSuggestions`,core
|
|
38
|
+
* `checkpoint-store.d.ts` 明写「CONTRACT (same as the synchronous `AskRequest.ruleSuggestions`)」)。
|
|
39
|
+
* 各写一份 zod 必然漂——闭词表 `match` 加词、上限改口径,只改一处就出两种形。
|
|
40
|
+
*/
|
|
41
|
+
export const RuleSuggestionSchema = z
|
|
42
|
+
.object({ rule: z.string().max(MAX_RULE_TEXT_CHARS), match: z.enum(["exact", "prefix"]), command: z.string().max(MAX_RULE_TEXT_CHARS) })
|
|
43
|
+
.strict();
|
|
44
|
+
/**
|
|
45
|
+
* {@link RuleSuggestionSchema} 的**不限长**孪生 —— 只判**结构**(两个字符串 + 闭词表 `match`),长度由
|
|
46
|
+
* 调用方在**脱敏之后**自己截。
|
|
47
|
+
*
|
|
48
|
+
* 🔴 为什么必须有它(codex 对抗复审 [medium],验真后修):`redactSecrets` **会变长**(一条
|
|
49
|
+
* `password=…` 换成更长的遮蔽标记)。拿带 `.max()` 的形去校验**脱敏前**的原文,再脱敏,产出的就可能是
|
|
50
|
+
* 一条超限的文本;而任何**回读**路径若再跑一遍同一个函数,那一条会在第二次校验时被整条丢掉 —— 于是
|
|
51
|
+
* 「落库时在、读回时不在」,并且**只发生在有回读的那条腿上**(SQL 有列回读、LOCAL 直接从 blob 投),
|
|
52
|
+
* 两个后端对同一份素材给出不同的卡面。同族先例逐字在 `tool-approval.ts` 的 `persistedRuleShadowed`
|
|
53
|
+
* 发帧点(「先脱敏再截,一次算定」)。
|
|
54
|
+
*
|
|
55
|
+
* ⇒ 正确顺序:**结构判(本 schema)→ 脱敏 → 截长 → 结果按 {@link RuleSuggestionSchema} 必然合形**。
|
|
56
|
+
* 这样函数对自己的输出**幂等**,两条腿同形。
|
|
57
|
+
*
|
|
58
|
+
* 🔴 **未知键 `strip`(合并码重扫放宽,重扫二轮改准形):窄读只判三键结构,多余键剥掉、且不拒收**。
|
|
59
|
+
* base 带 `.strict()`,而 zod 4 的 `.extend()` **保留** strict —— core 给 `RuleSuggestion` 加一个 additive
|
|
60
|
+
* 字段(它对这条面一贯的做法),逐条 `safeParse` 会**条条失败**,于是耐久腿的整只候选静默清零、列落
|
|
61
|
+
* NULL、读口省键,把「有供给」谎报成「无供给」,正是本键存在理由的反面(`checkpoint-store-sql.ts` 顶注与
|
|
62
|
+
* wire 契约都写死「缺席=真的没有候选」)。同步腿的 `buildApprovalCard` 是显式三键投影、对加字段天然免疫
|
|
63
|
+
* ⇒ 不放宽这一份,两条腿会在「上游长一格」这条轴上分家。
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ **写的是 `.strip()` 不是 `.loose()`**(重扫二轮,实测更正):zod 4 的 `loose` = **passthrough** ——
|
|
66
|
+
* 未知键**原样留在** `parsed.data` 里,与本注和契约文写的「多余键剥掉」相反。放宽要的是「不拒收」,
|
|
67
|
+
* 剥键是另一件事,`.loose()` 只做到了前者。旧形下真正在剥键的是 {@link boundedRuleSuggestions} 里那三行
|
|
68
|
+
* 显式投影,于是本 schema 对「剥键」这条判据是**空跑**;而它是**导出符号**,output 类型带 index signature,
|
|
69
|
+
* 任何新消费者拿 `parsed.data` 直投就把上游未知键(其上**没有** `redactSecrets` 覆盖)带上跨租户可见的
|
|
70
|
+
* `GET /v1/approvals`。⇒ 改默认 strip 形:additive 加键照收、未知键当场剥掉,两个意图各自成立。
|
|
71
|
+
* 落库/读回的 {@link ApprovalCardSchema} 仍 `.strict()`(投影只写三键,多余键根本到不了那一层)。
|
|
72
|
+
*/
|
|
73
|
+
export const RuleSuggestionRawSchema = RuleSuggestionSchema.extend({ rule: z.string(), command: z.string() }).strip();
|
|
74
|
+
/**
|
|
75
|
+
* 候选**基数**的 server 执法上限。两条腿同用。
|
|
76
|
+
*
|
|
77
|
+
* 🔴 **两个数,别混**(合并码重扫,档实对齐):core 的契约是 **≤2**(`exact` 恒 index 0,至多再跟一条
|
|
78
|
+
* reviewed `prefix` 形 —— core `checkpoint-store.d.ts` 与 `permission-rule-model.d.ts` 逐字),那是**今天
|
|
79
|
+
* 真实的**上限;本常量是 server 侧的**容忍帽**,刻意留在 4:一个被改坏/未来放宽的上游不该让运维队列
|
|
80
|
+
* 的一行被整只拒收(同步腿这一格进的是 `.strict()` 的卡 schema,收紧到 2 会把「多给一条展示用候选」
|
|
81
|
+
* 变成铸卡失败——在一条纯展示轴上换来一次 park,方向反了)。⇒ 消费端按 **≤4** 布局,契约文同口径成文。
|
|
82
|
+
*/
|
|
83
|
+
export const MAX_RULE_SUGGESTIONS = 4;
|
|
31
84
|
/** 已 redact 的 ask 文案上限(`AskRequest.message` 的投影)。 */
|
|
32
85
|
const MAX_MESSAGE = 8192;
|
|
33
86
|
/**
|
|
@@ -117,12 +170,7 @@ export const ApprovalCardSchema = z
|
|
|
117
170
|
* ⇒ 处置 = 不缩键(缩了,同副本重连这条真实可用的路也一起没了),把「durable 回决腿的规则位」
|
|
118
171
|
* 登记为后续件(车4 域)。
|
|
119
172
|
*/
|
|
120
|
-
ruleSuggestions: z
|
|
121
|
-
.array(z
|
|
122
|
-
.object({ rule: z.string().max(MAX_RULE_TEXT_CHARS), match: z.enum(["exact", "prefix"]), command: z.string().max(MAX_RULE_TEXT_CHARS) })
|
|
123
|
-
.strict())
|
|
124
|
-
.max(4)
|
|
125
|
-
.optional(),
|
|
173
|
+
ruleSuggestions: z.array(RuleSuggestionSchema).max(MAX_RULE_SUGGESTIONS).optional(),
|
|
126
174
|
/** 委派出处(子代 ask 才在场;判别键 = `fromSubagent`,core RB-39②)。 */
|
|
127
175
|
fromSubagent: z.literal(true).optional(),
|
|
128
176
|
sourceTaskId: z.string().max(MAX_IDENT).optional(),
|
|
@@ -214,7 +214,23 @@ export interface ReconcileStats {
|
|
|
214
214
|
singleMintAskOnly: number;
|
|
215
215
|
singleMintCheckpointOnly: number;
|
|
216
216
|
singleMintNeither: number;
|
|
217
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* 祖先冻结 approver 层 fold 中途铸点(#168 件1④):退出硬相等,不是 mismatch 也不是单铸。
|
|
219
|
+
*
|
|
220
|
+
* 🔴 **今天恒零,而且是结构性的**(扫描P2,2026-08-12 亲读验真;登记而不假称已解):读口
|
|
221
|
+
* `findCheckpointCandidatesForAsk` 按 `session_id = ask.sessionId` 查,`session_id` 列写的是
|
|
222
|
+
* `cp.sessionId`,而 core 每个 checkpoint 铸点都填 `sourceTaskId: sessionId`
|
|
223
|
+
* (`runner/prepare-task.js` 四处;`checkpoint-store.d.ts` 逐字「= sessionId at the mint」)⇒ 读口
|
|
224
|
+
* 返回的候选恒满足 `c.sourceTaskId === ask.sessionId`;而判据 1 命中要 `c.sourceTaskId ===
|
|
225
|
+
* ask.sourceTaskId`,祖先层否决要 `ask.sourceTaskId !== ask.sessionId` —— 两者互斥,这一臂在生产
|
|
226
|
+
* 接线下一次也不会被返回。
|
|
227
|
+
*
|
|
228
|
+
* 所以**别把零读数当健康信号**(与 {@link unmatchableNoHash} 的「恒零才是健康态」正相反:那条能非零,
|
|
229
|
+
* 这条不能)。根因是本文件 `reconcileOne` 里已登记的读口 session 维缺口(委派腿的候选压根进不来),
|
|
230
|
+
* 修它需要自带判据 + 真 checkpoint 店的宿主/子代双 session 端到端钉,不在扫描批的口径内。
|
|
231
|
+
* 否决臂**不删**:它是纯减法的安全臂,读口一修好就重新承重。结构事实的钉:
|
|
232
|
+
* `test/approval-reconciler.test.ts` 的「扫描P2 ④ 结构钉」(翻转钉——读口修好那天它会变红)。
|
|
233
|
+
*/
|
|
218
234
|
ancestorFoldMint: number;
|
|
219
235
|
parked: number;
|
|
220
236
|
denied: number;
|
|
@@ -33,7 +33,7 @@ import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScr
|
|
|
33
33
|
import { FleetEventBus } from "../fleet/fleet-bus.js";
|
|
34
34
|
import { composeHooks, createTaskHooks } from "../hooks/hook-runner.js";
|
|
35
35
|
import { explicitOperatorOk } from "../http/server.js";
|
|
36
|
-
import { matchCatalogModel, modelSupportsImages, resolveTaskModel } from "../model-select.js";
|
|
36
|
+
import { isModelAllowlisted, matchCatalogModel, modelSupportsImages, resolveTaskModel, terminalCatalogName } from "../model-select.js";
|
|
37
37
|
import { PerTaskImageRegistry, resolveSandboxImageRef } from "../per-task-image.js";
|
|
38
38
|
import { isRemoteScratchpadLane, remoteScratchpadDirFor } from "../plugins/remote-scratchpad.js";
|
|
39
39
|
import { bindAttachmentsForTask } from "../plugins/task-attachment-store.js";
|
|
@@ -83,7 +83,7 @@ export function createResolveSpec(ctx) {
|
|
|
83
83
|
// 翻译表要是再开第三个算点,「顶层键 vs bundle defaultMode 谁赢」这条优先序就有三份可以各自漂移的
|
|
84
84
|
// 副本,而漂移在这条轴上的形态是「壳发的 bundle bypass 在这一面生效、在那一面没生效」——三审同点。
|
|
85
85
|
const effMode = effectivePermissionMode(body, gated.parsedSettings.settings);
|
|
86
|
-
const lane = bindSettingsCwdEnvAndModel(body, auth, gated, effMode);
|
|
86
|
+
const lane = bindSettingsCwdEnvAndModel(body, auth, gated, effMode, opts);
|
|
87
87
|
const anchors = await resolveHistoryAnchors(body, auth);
|
|
88
88
|
const spec = assembleSpecLiteral(body, auth, opts, { ...gated, ...lane, ...anchors }, effMode);
|
|
89
89
|
const folded = await foldGovernanceAndSettings(body, auth, spec, gated, effMode);
|
|
@@ -210,7 +210,7 @@ export function createResolveSpec(ctx) {
|
|
|
210
210
|
/** 阶段②(settings·cwd·env):吃 请求体 + auth + 阶段①的 objective/parsedSettings,吐 taskHooks、
|
|
211
211
|
* additionalDirectories、档位展开后的 wireCatalog 与选中的 picked;副作用=per-session cwd/shellEnv 注册与
|
|
212
212
|
* honored/ignored 日志、未知模型回落与无视觉模型降级的计数/告警。 */
|
|
213
|
-
const bindSettingsCwdEnvAndModel = (body, auth, gated, effMode) => {
|
|
213
|
+
const bindSettingsCwdEnvAndModel = (body, auth, gated, effMode, opts) => {
|
|
214
214
|
const { objective, parsedSettings } = gated;
|
|
215
215
|
// [#40 / TOC cwd seam] register the caller's launch dir so the HOST factory runs the agent there (read by
|
|
216
216
|
// ctx.sessionId). 🔒 GATED: only the single-user host lane (cwdHonored) honors it; on any other lane / multi-tenant
|
|
@@ -335,7 +335,22 @@ export function createResolveSpec(ctx) {
|
|
|
335
335
|
// the Runner; a refresh-time tier change makes this gate briefly AHEAD of the old Runner snapshot (a new
|
|
336
336
|
// tier word then fails loud in core instead of silently degrading — honest during the restart window).
|
|
337
337
|
const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
|
|
338
|
-
|
|
338
|
+
// #233 / A-002.8:`atModelAllowlist` 与门目录同源(增广视图)——名单按 catalog name 写,而用户可以
|
|
339
|
+
// 用档位词/别名/id 指同一只模型,归一在 isModelAllowlisted 里做(model-select.ts 顶注)。
|
|
340
|
+
const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog, config.atModelAllowlist);
|
|
341
|
+
// #233 拒形分流(clay 终裁 (a)):**fresh 提交** 400 `request.model_not_allowed`(治理拒——模型在目录
|
|
342
|
+
// 里但名单未含,与"目录里没有"是两回事,故与 request.unknown_reference 分码);**resume 续跑照旧**
|
|
343
|
+
// ——名单管"用户可点什么",resume 不是一次新选择;而本文件的 appendSystemPrompt 与 compactionModel
|
|
344
|
+
// 两处 fresh-vs-resume 分流注、加 model-select.ts 的 [865]② 头注,三处都已成文"砖死 resume 比降级
|
|
345
|
+
// 糟"。留声(warn+计数)让治理面看得见还有多少存量会话跑在名单外。
|
|
346
|
+
if (picked.notAllowed !== undefined) {
|
|
347
|
+
const { requested, resolved, source } = picked.notAllowed;
|
|
348
|
+
if (opts?.leg === "fresh") {
|
|
349
|
+
throw new HttpError(400, `model "${requested.slice(0, 120)}" is not in this deployment's @-mention allowlist (it resolves to catalog model "${resolved}") — the allowlist is configured by the operator (config-center models.atModelAllowlist)`, { code: "request.model_not_allowed" });
|
|
350
|
+
}
|
|
351
|
+
metrics.inc("task_model_not_allowlisted_resume_total");
|
|
352
|
+
logger.warn("task_model_not_allowlisted_resume", { requested: requested.slice(0, 120), resolved, source, sessionId: auth?.sessionId ?? null, note: "resumed leg keeps running the model it was started on — the allowlist gates fresh picks only" });
|
|
353
|
+
}
|
|
339
354
|
// [865]② 降级永远可见:fresh submit 的未知 body.model 已在 HTTP 门 400(不会到这);走到这的未知 ref
|
|
340
355
|
// 只剩 RESUME 重放(模型事后被移出目录)与 settings.model(lenient 文档面)——落 default 可跑,但必须
|
|
341
356
|
// 有声(session 中途换模型是最恶性的上下文污染路径,静默=病灶本体)。
|
|
@@ -650,6 +665,18 @@ export function createResolveSpec(ctx) {
|
|
|
650
665
|
logger.warn("compaction_model_unknown_fallback", { requested: body.compactionModel.slice(0, 120), sessionId: auth?.sessionId ?? null });
|
|
651
666
|
return {};
|
|
652
667
|
}
|
|
668
|
+
// #233 / A-002.8 第二执法点:`compactionModel` 也是用户逐 turn 选择通道(一次显式的省钱换挡),
|
|
669
|
+
// 与 body.model 同门同拒形。归一同源(isModelAllowlisted 收增广目录 + 名单)。resume 照旧续跑
|
|
670
|
+
// (终裁 (a)):这条腿上"照旧"= 保留该 compactionModel,只留声——把它丢回 summarize 角色是
|
|
671
|
+
// **静默换价**,正是本字段当初为之存在的病灶。
|
|
672
|
+
if (!isModelAllowlisted(cm, wireCatalog, config.atModelAllowlist)) {
|
|
673
|
+
const terminal = terminalCatalogName(cm, wireCatalog) ?? cm;
|
|
674
|
+
if (opts?.leg === "fresh") {
|
|
675
|
+
throw new HttpError(400, `compactionModel "${body.compactionModel.slice(0, 120)}" is not in this deployment's @-mention allowlist (it resolves to catalog model "${terminal}") — the allowlist is configured by the operator (config-center models.atModelAllowlist)`, { code: "request.model_not_allowed" });
|
|
676
|
+
}
|
|
677
|
+
metrics.inc("task_model_not_allowlisted_resume_total");
|
|
678
|
+
logger.warn("task_model_not_allowlisted_resume", { requested: body.compactionModel.slice(0, 120), resolved: terminal, source: "compaction", sessionId: auth?.sessionId ?? null, note: "resumed leg keeps the compaction model it was started with — the allowlist gates fresh picks only" });
|
|
679
|
+
}
|
|
653
680
|
return { compactionModel: cm };
|
|
654
681
|
})()),
|
|
655
682
|
// E7 (shell-host contract): reasoning-effort selection threaded to core's ThinkingLevel. Defensive
|
|
@@ -116,7 +116,7 @@ export interface RunnerDepsCtx {
|
|
|
116
116
|
}
|
|
117
117
|
/** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
|
|
118
118
|
* 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
|
|
119
|
-
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores">;
|
|
119
|
+
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "onNotice">;
|
|
120
120
|
/**
|
|
121
121
|
* design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
|
|
122
122
|
* `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
|
|
@@ -145,7 +145,46 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "
|
|
|
145
145
|
* 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
|
|
146
146
|
*/
|
|
147
147
|
/** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
|
|
148
|
-
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "hands" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores">;
|
|
148
|
+
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "hands" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores" | "logger">;
|
|
149
|
+
/**
|
|
150
|
+
* core `EngineNotice` → 结构化 `engine_notice` 行的**唯一**转发体(#240②)。
|
|
151
|
+
*
|
|
152
|
+
* 🔴 为什么是具名导出而不是就地闭包(合并码重扫):这一席此前只落在共享基座里,于是 leader 车道
|
|
153
|
+
* (`src/leader/wire.ts` 自拼 deps 的五处 `new Runner({...})`)一处都没有 —— core 的契约是「接线 ⇒ 走
|
|
154
|
+
* sink,缺席 ⇒ 逐字打 `console.warn`」,所以缺席不是静默而是**同一部署两条通告通道**:主车道进结构化
|
|
155
|
+
* JSON 行,leader 车道落裸 stderr,按 code 采集(`tool_result.offload_put_failed` 等)的运维面漏掉后者。
|
|
156
|
+
* 两处各写一份转发体必漂(键名/字段集),故收成一个函数;入参取**结构形** logger(leader 的 cfg 只带
|
|
157
|
+
* 可选 `warn`/`info` 两格,不是全形 `Logger`)。
|
|
158
|
+
*
|
|
159
|
+
* 🔴 `warn` 是**必填**(codex 复审第一轮,验真后修):可选链 `logger.warn?.(…)` 形在「只装了 `info` 的
|
|
160
|
+
* logger」上会把通告**吞掉** —— 比不接线还坏(不接线时 core 至少还打它自己的 `console.warn`)。所以
|
|
161
|
+
* 「有没有 warn」这个判断留在**装配点**:没有就别铸这一席,让 core 的响亮权留在原处。
|
|
162
|
+
*/
|
|
163
|
+
export declare function createEngineNoticeForwarder(logger: {
|
|
164
|
+
warn: (msg: string, fields?: Record<string, unknown>) => void;
|
|
165
|
+
}): NonNullable<RunnerDeps["onNotice"]>;
|
|
166
|
+
/**
|
|
167
|
+
* `onNotice` **席**的装配判据本身 —— 「`warn` 真在场才铸席」这一条从此是**一个可直接调用的函数**,
|
|
168
|
+
* 不是散在装配点、只能靠扫源码文本去猜的一行三元(codex 对抗复审 R1-[medium],验真后修)。
|
|
169
|
+
*
|
|
170
|
+
* 病(实测):把 leader 的门写成 `cfg.logger ? ((m, x) => cfg.logger?.warn?.(m, x)) : undefined` 时,
|
|
171
|
+
* 「装配点切片里出现过 `warn` 这个词」这类**源码文本**判据一律照绿 —— 而只装了 `info` 的部署会拿到一个
|
|
172
|
+
* truthy 闭包、照样铸席,通告随后被可选链**静默吞掉**,比不铸席更坏(不铸时 core 至少还打它自己的
|
|
173
|
+
* `console.warn`,响亮权留在原处)。任何「在这段文本里找关键字」的门都挡不住这一族变异,因为病灶在
|
|
174
|
+
* **值**(闭包 truthy)而不在字面。⇒ 把判据搬进函数,让它可以被**行为**测:info-only ⇒ 空席;带 warn ⇒
|
|
175
|
+
* 具名转发体。
|
|
176
|
+
*
|
|
177
|
+
* 命名(CLAUDE.md 工厂律):返回的袋子里装的是**活闭包**(有行为)⇒ `create*`,不是 `build*`。
|
|
178
|
+
* 返回**展开形**(`{}` 或 `{ onNotice }`)而不是 `onNotice: undefined`。理由是**键卫生**,不是 core 行为
|
|
179
|
+
* (R3 复扫更正:core 5.28 的 `deliverEngineNotice` 只判 `typeof onNotice === "function"`,显式 `undefined`
|
|
180
|
+
* 与键缺席在 core 那侧行为逐字相同、响亮权都留在 core——旧句「显式 undefined 也算 host 接管」与真码相反):
|
|
181
|
+
* 展开形让 `new Runner({...seat})` 的键集只随真装配变化,EXPECTED_SHARED_KEYS 类的键集绊线才数得准。
|
|
182
|
+
*/
|
|
183
|
+
export declare function createEngineNoticeSeat(logger: {
|
|
184
|
+
warn?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
185
|
+
} | undefined): {
|
|
186
|
+
onNotice?: NonNullable<RunnerDeps["onNotice"]>;
|
|
187
|
+
};
|
|
149
188
|
export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
|
|
150
189
|
export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
|
|
151
190
|
//# sourceMappingURL=runner-deps.d.ts.map
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -25,11 +25,54 @@ export function createRunnerDepsOnAsk(toolApproval) {
|
|
|
25
25
|
return undefined;
|
|
26
26
|
return (req, signal) => toolApproval.ask(req, signal);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* core `EngineNotice` → 结构化 `engine_notice` 行的**唯一**转发体(#240②)。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 为什么是具名导出而不是就地闭包(合并码重扫):这一席此前只落在共享基座里,于是 leader 车道
|
|
32
|
+
* (`src/leader/wire.ts` 自拼 deps 的五处 `new Runner({...})`)一处都没有 —— core 的契约是「接线 ⇒ 走
|
|
33
|
+
* sink,缺席 ⇒ 逐字打 `console.warn`」,所以缺席不是静默而是**同一部署两条通告通道**:主车道进结构化
|
|
34
|
+
* JSON 行,leader 车道落裸 stderr,按 code 采集(`tool_result.offload_put_failed` 等)的运维面漏掉后者。
|
|
35
|
+
* 两处各写一份转发体必漂(键名/字段集),故收成一个函数;入参取**结构形** logger(leader 的 cfg 只带
|
|
36
|
+
* 可选 `warn`/`info` 两格,不是全形 `Logger`)。
|
|
37
|
+
*
|
|
38
|
+
* 🔴 `warn` 是**必填**(codex 复审第一轮,验真后修):可选链 `logger.warn?.(…)` 形在「只装了 `info` 的
|
|
39
|
+
* logger」上会把通告**吞掉** —— 比不接线还坏(不接线时 core 至少还打它自己的 `console.warn`)。所以
|
|
40
|
+
* 「有没有 warn」这个判断留在**装配点**:没有就别铸这一席,让 core 的响亮权留在原处。
|
|
41
|
+
*/
|
|
42
|
+
export function createEngineNoticeForwarder(logger) {
|
|
43
|
+
return (notice) => logger.warn("engine_notice", { code: notice.code, message: notice.message, ...(notice.detail !== undefined ? { detail: notice.detail } : {}) });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `onNotice` **席**的装配判据本身 —— 「`warn` 真在场才铸席」这一条从此是**一个可直接调用的函数**,
|
|
47
|
+
* 不是散在装配点、只能靠扫源码文本去猜的一行三元(codex 对抗复审 R1-[medium],验真后修)。
|
|
48
|
+
*
|
|
49
|
+
* 病(实测):把 leader 的门写成 `cfg.logger ? ((m, x) => cfg.logger?.warn?.(m, x)) : undefined` 时,
|
|
50
|
+
* 「装配点切片里出现过 `warn` 这个词」这类**源码文本**判据一律照绿 —— 而只装了 `info` 的部署会拿到一个
|
|
51
|
+
* truthy 闭包、照样铸席,通告随后被可选链**静默吞掉**,比不铸席更坏(不铸时 core 至少还打它自己的
|
|
52
|
+
* `console.warn`,响亮权留在原处)。任何「在这段文本里找关键字」的门都挡不住这一族变异,因为病灶在
|
|
53
|
+
* **值**(闭包 truthy)而不在字面。⇒ 把判据搬进函数,让它可以被**行为**测:info-only ⇒ 空席;带 warn ⇒
|
|
54
|
+
* 具名转发体。
|
|
55
|
+
*
|
|
56
|
+
* 命名(CLAUDE.md 工厂律):返回的袋子里装的是**活闭包**(有行为)⇒ `create*`,不是 `build*`。
|
|
57
|
+
* 返回**展开形**(`{}` 或 `{ onNotice }`)而不是 `onNotice: undefined`。理由是**键卫生**,不是 core 行为
|
|
58
|
+
* (R3 复扫更正:core 5.28 的 `deliverEngineNotice` 只判 `typeof onNotice === "function"`,显式 `undefined`
|
|
59
|
+
* 与键缺席在 core 那侧行为逐字相同、响亮权都留在 core——旧句「显式 undefined 也算 host 接管」与真码相反):
|
|
60
|
+
* 展开形让 `new Runner({...seat})` 的键集只随真装配变化,EXPECTED_SHARED_KEYS 类的键集绊线才数得准。
|
|
61
|
+
*/
|
|
62
|
+
export function createEngineNoticeSeat(logger) {
|
|
63
|
+
const warn = logger?.warn?.bind(logger);
|
|
64
|
+
return warn ? { onNotice: createEngineNoticeForwarder({ warn }) } : {};
|
|
65
|
+
}
|
|
28
66
|
export function createSharedRunnerDeps(ctx) {
|
|
29
67
|
// 具名带类型的字面量(非裸 return)——deps-literal-shape-gate 的 POINTS 按 `const X: T = {` 咬装配
|
|
30
68
|
// 字面量,裸 return 形在它的覆盖外(复审 2026-07-30 F5):conditional-spread 病在这里就会失检。
|
|
31
69
|
const shared = {
|
|
32
70
|
brain: ctx.brain,
|
|
71
|
+
// core 5.28.0 EngineNotice 席(#240②,^5.28.0 floor 的编译锚之一):结构化运维通告——function 席
|
|
72
|
+
// **替换** console.warn 行(core 契约:host 接管即拥有响亮权,不双发),故必须真转发不吞。三族
|
|
73
|
+
// (config.env_timeout_discarded / config.materialize_env_discarded / tool_result.offload_put_failed)
|
|
74
|
+
// 全部按 code 落 logger.warn,detail 原样透传;共享基座=两 Runner(main/sub)同源同席([1543] 纪律)。
|
|
75
|
+
onNotice: createEngineNoticeForwarder(ctx.logger),
|
|
33
76
|
// core 1.265: the active tier table (tier words + CC aliases → catalog keys, expandTiers at Runner
|
|
34
77
|
// construction; empty = INERT by core contract). Same mutateInPlace reference applyEffective fills — a
|
|
35
78
|
// refresh-time tier change is restart-to-apply, same tier as models.
|
|
@@ -78,12 +78,34 @@ export function createSessionFaces(ctx) {
|
|
|
78
78
|
if ("active" in runs)
|
|
79
79
|
return { active: runs.active };
|
|
80
80
|
// checkpoint rows carry no owner column → owner-guarded via a session_meta.owner sub-select, so they MUST
|
|
81
|
-
// run while session_meta still exists (before the history delete below); tool_result
|
|
82
|
-
// `
|
|
81
|
+
// run while session_meta still exists (before the history delete below); tool_result selects by its
|
|
82
|
+
// RECORDED PROVENANCE column (`owner_session_id`) and stays route-guarded too (see its deleteBySession doc).
|
|
83
83
|
if (checkpointStore)
|
|
84
84
|
await checkpointStore.deleteBySession(sessionId, owner);
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// optional extra(R3 复扫更正:不再是「SQL twins only」——core 5.28 起 FileToolResultStore **实现了**
|
|
86
|
+
// deleteBySession,local 车道这条腿**真的会跑**。File 孪生的形:同步遍历目录、无 .owner.json 边车的
|
|
87
|
+
// 存量文件计入 unattributable 不删——所以 local 部署上这条 warn 在有 5.28 前存量时会以大数出现,
|
|
88
|
+
// 这是诚实读数不是事故。⚠️ R4 复扫再纠:**local 无 TTL 腿**(File 孪生没有 reapOlderThan,
|
|
89
|
+
// reapers 的可选链在 local 恒 no-op)⇒ 这些无主存量今天在 local 上**没有任何清理路径**
|
|
90
|
+
// (会话删除按契约不动它、TTL 也够不着)——只占磁盘不泄权,单用户 CC 姿态下按已知缺口登记,
|
|
91
|
+
// 清理腿(File 孪生补 reapOlderThan 或一次性迁移铸边车)是独立后续件,别把本注读成已有补偿。)
|
|
92
|
+
if (toolResultStore) {
|
|
93
|
+
const report = await toolResultStore.deleteBySession?.(sessionId);
|
|
94
|
+
// 🔴 **回执不许丢**(codex 复审 R1-[high],验真后修):core 把 `unattributable` 写成「the caller
|
|
95
|
+
// learns the deletion was **incomplete** rather than being told a clean "done"」——无出处的行按
|
|
96
|
+
// 契约永不删除,协调器把报告丢掉就等于把这句话吞了,而删除面吞掉的是**删除权**的真相。
|
|
97
|
+
// 这一格是**全库**无出处行数(core InMemory 孪生口径),读法 = 「本店存在没人认领的字节,任何
|
|
98
|
+
// 会话删除都不可能完整」,不是「本会话残留了 N 行」;实体清理归 TTL(`reapOlderThan`)。
|
|
99
|
+
// ⚠️ 登记后续件:把它也带上 DELETE 的 HTTP 回执是**加 wire 键**,归属主裁决,不在本批自作主张;
|
|
100
|
+
// 本批先保证它不再无声消失(结构化 warn = 运维查得到、审计追得上)。
|
|
101
|
+
if (report !== undefined && report.unattributable > 0) {
|
|
102
|
+
logger.warn("tool_result_unattributable_rows_survive_session_delete", {
|
|
103
|
+
sessionId,
|
|
104
|
+
deleted: report.deleted,
|
|
105
|
+
unattributable: report.unattributable,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
87
109
|
// E18 resume-at anchors are per-session privacy-relevant metadata → purge ALL of them with the session, scoped
|
|
88
110
|
// by session_id ALONE (the route already proved session ownership at the DELETE gate). A per-ROW owner guard
|
|
89
111
|
// here would LEAK: an anchor's owner is the per-run submitting principal, which diverges from the canonical
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -15,15 +15,20 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
17
17
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
18
|
+
import { createParentWatch } from "../parent-watch.js";
|
|
18
19
|
/** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 */
|
|
19
20
|
export function installShutdownHandlers(ctx) {
|
|
20
21
|
const { config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, } = ctx;
|
|
21
22
|
let closing = false;
|
|
23
|
+
/** #219:parent 监视腿(装配在本函数尾,`config.parentPid` 在场才建)。声明提前只为让 hardShutdown
|
|
24
|
+
* 能把它一并停掉 —— 文件头契约 3 同族(收尾期不再有后台 tick)。 */
|
|
25
|
+
let parentWatch;
|
|
22
26
|
const hardShutdown = () => {
|
|
23
27
|
if (closing)
|
|
24
28
|
return;
|
|
25
29
|
closing = true;
|
|
26
30
|
clearInterval(reaper);
|
|
31
|
+
parentWatch?.stop(); // #219:契约 3 同族——收尾期不再探父进程
|
|
27
32
|
storeLiveProbe?.stop(); // #131-2:契约 3 同族——收尾期不再有探针 tick 打向正在关闭的池
|
|
28
33
|
configCenter?.stopRefreshLoop(); // #131-2:停机中途不再热应用配置
|
|
29
34
|
otelExporter?.stop();
|
|
@@ -127,5 +132,17 @@ export function installShutdownHandlers(ctx) {
|
|
|
127
132
|
idleGraceMs: config.sighupIdleGraceMs,
|
|
128
133
|
log: (event, fields) => logger.info(event, fields),
|
|
129
134
|
}));
|
|
135
|
+
// #219(黑板 [3617]):壳被 SIGKILL 后引擎被 reparent 到 init 而继续活着(孤儿引擎持端口/持库连接,
|
|
136
|
+
// 且再没有人会给它发 SIGTERM)。`SEMA_PARENT_PID` 设了才装配 —— 连续两拍 ESRCH 判死,出口**就是**
|
|
137
|
+
// 上面 SIGTERM 用的这条 `drainThenShutdown`(结算/park 语义与人工停机逐字一致,不是裸 exit)。
|
|
138
|
+
// 位置:与三个 process.on 同段、在它们之后 —— 它和信号腿共享 closing/draining 两个 latch。
|
|
139
|
+
if (config.parentPid !== undefined) {
|
|
140
|
+
parentWatch = createParentWatch({
|
|
141
|
+
pid: config.parentPid,
|
|
142
|
+
isStopped: () => closing || draining,
|
|
143
|
+
drain: drainThenShutdown,
|
|
144
|
+
log: (event, fields) => logger.info(event, fields),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
130
147
|
}
|
|
131
148
|
//# sourceMappingURL=shutdown.js.map
|
package/dist/boot/stores.js
CHANGED
|
@@ -16,6 +16,7 @@ import { join } from "node:path";
|
|
|
16
16
|
import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore, FileUsageWindowStore, InMemoryUsageWindowStore } from "@sema-agent/core";
|
|
17
17
|
import { createMemorySyncRunner, createMemorySyncTransport } from "../memory-sync-client.js";
|
|
18
18
|
import { memoryEmbedderFor } from "../plugins/memory-embedder.js";
|
|
19
|
+
import { ensurePgMemoryEmbedderMetaSchema, reconcileEmbedderFingerprint } from "../plugins/memory-embedder-fingerprint.js";
|
|
19
20
|
import { PgMemoryEngineBackend, ensurePgMemoryEngineSchema } from "../plugins/memory-engine-pg.js";
|
|
20
21
|
import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "../plugins/memory-engine-tidb.js";
|
|
21
22
|
import { PgMemoryHistoryStore, PgMemorySyncStore, ensurePgMemoryHistorySchema, ensurePgMemorySyncSchema } from "../plugins/memory-sync-store-pg.js";
|
|
@@ -150,6 +151,25 @@ export async function openStores(ctx) {
|
|
|
150
151
|
});
|
|
151
152
|
},
|
|
152
153
|
});
|
|
154
|
+
// design/234 embedder 指纹门(clay 终裁 [3647]⑤ 方向=清空重建):同维度换模型是**静默**混空间形
|
|
155
|
+
// ——旧空间与新空间的向量在同一列里做 cosine,排序变噪音,没有任何一层会说出来。指纹
|
|
156
|
+
// `{model,dimensions}` 明文存单行元表,boot 一次比对:不等/无主/不可解析 ⇒ 先清 `embedding` 列、
|
|
157
|
+
// 后写元表(次序即正确性,反序中途崩溃=新 identity 已记而旧向量还在=正是被修的病),写后 CAS
|
|
158
|
+
// 重读收敛并发副本。⚠️ 位置:ensure*Schema 之后、`new PgMemoryEngineBackend` **之前** —— 比对跑在
|
|
159
|
+
// backend 构造之后 = 服务已经能用旧空间向量检索了。门自身失败一律抛(拒启)。
|
|
160
|
+
// A 态(embedder 缺席)门整个不装:连 ensure 都不调,元表不动(旧 identity 保留,配置回归同
|
|
161
|
+
// identity 时存量向量直接续用)。⚠️ ③b 扫的是**源码文本**,调用写在 if 内照样入册(K1)。
|
|
162
|
+
if (embedder !== undefined && config.memoryEmbedder !== undefined) {
|
|
163
|
+
await ensurePgMemoryEmbedderMetaSchema(q);
|
|
164
|
+
await reconcileEmbedderFingerprint(q, { model: config.memoryEmbedder.model, dimensions: config.memoryEmbedder.dimensions }, { log: logger });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
logger.info("memory_embedder_identity_absent", {
|
|
168
|
+
note: "no MEMORY_EMBEDDER_* configured — the vector fingerprint gate is not wired and any recorded identity is left as-is; " +
|
|
169
|
+
"note that rows rewritten while the embedder is absent get their embedding written NULL anyway, so re-configuring the " +
|
|
170
|
+
"SAME identity later reuses vectors only for rows that have not been touched in the meantime",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
153
173
|
const pgMem = new PgMemoryEngineBackend(q, {
|
|
154
174
|
history: countedHistorySink(new PgMemoryHistoryStore(q)),
|
|
155
175
|
...(embedder !== undefined ? { embedder } : {}),
|
|
@@ -8,6 +8,20 @@ import type { EffectiveConfig } from "./types.js";
|
|
|
8
8
|
* `this.deps.models/roles/pricing` LIVE per-task and `/v1/models` reads `config.models` — both share the
|
|
9
9
|
* reference captured at boot, so mutating it (vs reassigning) updates both with no split, no Runner rebuild. */
|
|
10
10
|
export declare function mutateInPlace<V>(target: Record<string, V>, source: Record<string, V>): void;
|
|
11
|
+
/**
|
|
12
|
+
* #233 / A-002.8 —— 无法解读的名单条目的**代表元**。
|
|
13
|
+
*
|
|
14
|
+
* 为什么需要它(codex 复审 R1 真 finding):把坏条目**丢掉**会让 `[7, null]` 这种「在场但一条都读不
|
|
15
|
+
* 懂」的名单塌成**空数组**,而空数组在消费端的语义恰恰是「治理关闭 = 全放行」—— 一个表达"我要限制"
|
|
16
|
+
* 的配置,因为写错了类型,反而把门整个打开。方向必须反过来:名单在场 = operator 意图限制;读不懂的
|
|
17
|
+
* 条目 ⇒ 换成一个**永不可能是 catalog name** 的代表元(前导 NUL,registry-core 的 name 词法不可能产
|
|
18
|
+
* 出),于是它在归一时解不到任何目录条目 ⇒ 谁都准不了。
|
|
19
|
+
*
|
|
20
|
+
* 后果是**有界的**:被拒的只有「用户逐 turn 显式点模型」这一件事;`default` / roles / 档位表照常,
|
|
21
|
+
* 任务照跑。写错配置的代价是"点不了模型"而不是"服务不可用",且 warn 当场点名成因。
|
|
22
|
+
* 🔴 只活在内存判定面:不进 wire(`/v1/models` 只发布尔)、不进日志(warn 打的是原始坏值的类型标记)。
|
|
23
|
+
*/
|
|
24
|
+
export declare const UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY = "\0<unparsable-at-model-allowlist-entry>";
|
|
11
25
|
/** 返回值=是否 COMMIT(false 仅在 [2283]③ 世代序 CAS 拒绝时)。caller 收到 false 必须把**同一世代的
|
|
12
26
|
* 旁路消费**(prompts 采用/pricing/keyResolver/etag 推进/LKG 落盘)一并跳过——否则 applyEffective 拒了
|
|
13
27
|
* 主面、旁路却半应用同一个被拒世代,混合世代从侧门回来。首次 apply(live 未登记)恒 true。 */
|