@sema-agent/server 7.45.0 → 7.46.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.
Files changed (41) hide show
  1. package/USAGE.md +38 -0
  2. package/dist/approval-card.d.ts +106 -40
  3. package/dist/approval-card.js +45 -8
  4. package/dist/boot/coordinators.d.ts +1 -1
  5. package/dist/boot/memory-consolidation.d.ts +191 -0
  6. package/dist/boot/memory-consolidation.js +132 -0
  7. package/dist/boot/runner-deps.d.ts +1 -1
  8. package/dist/config-types.d.ts +48 -2
  9. package/dist/config-types.js +1 -0
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.js +53 -3
  12. package/dist/http/routes/capabilities.js +4 -0
  13. package/dist/http/routes/memory-compliance.d.ts +102 -0
  14. package/dist/http/routes/memory-compliance.js +113 -0
  15. package/dist/http/routes/memory-consolidation.d.ts +60 -0
  16. package/dist/http/routes/memory-consolidation.js +155 -0
  17. package/dist/http/routes/memory-origin.d.ts +124 -0
  18. package/dist/http/routes/memory-origin.js +193 -0
  19. package/dist/http/routes/rules.js +1 -1
  20. package/dist/http/routes/side-query.js +1 -0
  21. package/dist/http/server.d.ts +71 -2
  22. package/dist/http/server.js +27 -2
  23. package/dist/main.js +54 -2
  24. package/dist/memory-operator-faces.d.ts +211 -0
  25. package/dist/memory-operator-faces.js +76 -0
  26. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  27. package/dist/plugins/checkpoint-store-sql.js +31 -17
  28. package/dist/plugins/local-checkpoint-store.js +3 -3
  29. package/dist/plugins/permission-rule-store-file.d.ts +2 -2
  30. package/dist/plugins/permission-rule-store-file.js +41 -4
  31. package/dist/plugins/permission-rule-store-sql.d.ts +2 -2
  32. package/dist/plugins/permission-rule-store-sql.js +59 -18
  33. package/dist/plugins/store-backend.d.ts +1 -1
  34. package/dist/plugins/tidb-pool.js +3 -3
  35. package/dist/rules-consent.d.ts +30 -3
  36. package/dist/rules-consent.js +50 -7
  37. package/dist/task-cwd.d.ts +1 -1
  38. package/dist/tool-approval.d.ts +43 -10
  39. package/dist/tool-approval.js +105 -27
  40. package/dist/trace/core-keyset-guard.d.ts +2 -2
  41. package/package.json +3 -3
package/USAGE.md CHANGED
@@ -765,6 +765,44 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
765
765
  > 且**状态变更与审计行同一个事务**(不存在"解冻了但账上没有")。
766
766
  > - `GET /v1/ops/retention/audit?domain=&limit=&before=` —— 审计读面(keyset 分页,游标形同名册面)。
767
767
  > - 能力位 `capabilities.retention` = `{mode, maxAgeDays}`(lane 开着)或 `null`(关着)。
768
+ > - `MEMORY_CONSOLIDATION_DRIVER=on|off`(默认 `off`):记忆**折叠(consolidation)阀门**。一个从未折叠过的
769
+ > 记忆库只涨不折,检索质量随库龄衰减;开这根旋钮就给 worker 装上两个 operator 口,让人在自己选的节奏上
770
+ > 跑折叠。⚠️ **闭集两词,别的一律拒启**——本旋钮是新的,不继承 `MEMORY_ENGINE` 的 `true/1/yes` 放宽:
771
+ > 一个被静默读成 `off` 的开词买到的是「运维以为在折叠、其实一次都没跑过」,而这条面的失败恰恰看不出来。
772
+ > 🔴 **`on` 而这台机器结构上跑不了 ⇒ 拒启**(四形各有点名文案):①没接记忆引擎(`MEMORY_ENGINE=off`,
773
+ > 或多租户形——记忆引擎只在单用户部署上装配);②记忆后端不自带引擎控制面归属(`MEMORY_ENGINE_BACKEND=file`
774
+ > 有,`pg`/`tidb` 没有 —— core 会把控制面落到副本本地盘,而一条 run 的续跑账死在 pod 重建上等于下次整库
775
+ > 重新蒸馏、真金白银);③`MEMORY_PROVENANCE=off`(折叠法必须铸得出 origin 标记,否则产物会不带标记提交);
776
+ > ④**模型座位解析不出来**——既没有显式 chat 席,也没有 `roles.consolidate` / `roles.summarize`(含档位表的
777
+ > `flash` 绑定)。core **刻意不回落主模型**(整库蒸馏不该悄悄骑最贵的席位),本服务照抄那条判断,不在下游
778
+ > 补一个兜底席。
779
+ > - `MEMORY_CONSOLIDATION_SCOPES=user:local[,org:acme]`:阀门的 scope 表 = 这台 worker **声明**它会折叠哪些库。
780
+ > **恰好一条**时它是两个口省略 `scope` 的唯一缺省;零条或多条时省略 `scope` 一律 400(在两个库之间替人挑
781
+ > 一个去花全库模型钱是本设计要消灭的形)。**表非空时显式 scope 必须在表里**,否则 400 —— 一个陈旧或敲错
782
+ > 的 scope 会把整库蒸馏的钱花在另一个库上并把它的条目标成 superseded。⚠️ 这是**运维安全网**,不是授权边界
783
+ > (本服务没有任何授权源能对一个 operator 收窄 scope);表**空**时无从执法,显式 scope 照跑。
784
+ > - `MEMORY_CONSOLIDATION_INTERVAL_SEC=<非负整数>`(默认 `0`):周期腿节律。
785
+ > 🔴 **本版本没有周期腿,任何正值一律拒启**(不是"设了但不生效"——一台看着健康、库却永远不折叠的机器
786
+ > 正是这条阀门要消灭的形)。理由如实登记:真互斥要一把 durable 租约,而本仓唯一那把住在留存专用的 SQL
787
+ > 店里(`LEADER_ENABLED` 是纯布尔配置门,零选举零租约,拿它当互斥就是给运维一个假的独占承诺,而这条腿
788
+ > 并发跑的代价是重复的整库模型开销)。**要周期跑就用外部调度器**(cron / k8s CronJob)打下面那一口;
789
+ > 腿落地那天下界 `300` 秒会生效(拒启文案里已写明)。
790
+ >
791
+ > **配套的 operator 面**(全部 `operator-only`,见 `OPERATOR_PRINCIPALS`;阀门关着时两口是 **404,不是
792
+ > 501** —— 消费端的动作是「让运维开旋钮」,不是「换部署」):
793
+ > - `POST /v1/admin/memory/consolidation/run` `{"scope"?}` —— 跑(或**续跑**)一轮,同步回收执摘要。
794
+ > ⚠️ **分钟级持久作业**:客户端超时后**原样重发**是安全的 —— 同副本上在飞的那一轮会被并入(单飞),
795
+ > 落定之后的重发走 core 的 durable run 行续跑(零新 mint)。**跨副本**仍是 at-least-once,别从两个地方
796
+ > 同时打同一个 scope。
797
+ > ⚠️ 它**真的烧模型** ⇒ 与 `/v1/tasks` 同吃三道 503 门(drain / roster 未落 / 无 service 凭证)。
798
+ > 🔴 因此:**没配 `SERVICE_AUTH_TOKEN` 且未开 `ALLOW_UNAUTHED_WRITES` 的 worker 上这一口恒 503**,
799
+ > 而能力位仍报 true(本族共有缺口,附录 B.1 已如实登记)。启动日志为此打一条
800
+ > `memory_consolidation_run_credential_gated`;状态读那一口不受影响(它不烧模型)。
801
+ > 🔴 另一条启动告警:`OPERATOR_PRINCIPALS` 为空时两口恒 403(空名单 = 没有任何人是 operator),
802
+ > 日志里是 `memory_consolidation_valve_unreachable`。
803
+ > - `GET /v1/admin/memory/consolidation[?scope=]` —— 状态投影(阀门/座位/scope 表/上一轮的账)。
804
+ > - 能力位 `capabilities.memoryConsolidationDriver`(阀门上了场 ∧ operator 名单非空)。
805
+ > - 逐键语义与停因闭集见 `docs/ASSISTANT-WIRE-CONTRACT.md` §10。
768
806
 
769
807
  > **⚠️ hook-wired 部署里,parked 后台子代可能赎回不了(常态,不是升级窗口)。**
770
808
  > 引擎 5.19.0 起,一个任务的 **PreToolUse screening 面下延管辖它委派出去的子代**,于是 hook-wired 父
@@ -21,32 +21,57 @@
21
21
  * 没有方法、没有捕获的行为。
22
22
  */
23
23
  import { z } from "zod";
24
- import { type AskEvidenceAbsence } from "@sema-agent/core";
24
+ import { type AskEvidenceAbsence, type RuleOffer } from "@sema-agent/core";
25
25
  import type { AskRow } from "./plugins/approval-ask-store-sql.js";
26
26
  /** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
27
27
  * 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
28
28
  * 不能指望壳去截——一条 100KB 的 agentName 在 server 侧就该被拒,而不是变成一张撑爆呈卡面的卡。 */
29
29
  export declare const MAX_AGENT_NAME = 200;
30
30
  /**
31
- * **一条**规则候选的形 —— 同步腿(`ApprovalCardSchema.ruleSuggestions` / `card_json`)与耐久腿
32
- * (durable park 行的 `PendingCheckpoint.ruleSuggestions`,由 `plugins/checkpoint-store-sql.ts` 窄读)
33
- * **共用这一份**。
31
+ * OFFER **基数**的 server 执法上限。两条腿同用。
34
32
  *
35
- * 🔴 单一属主(宪法 [2704]「schema 单一属主禁复制」):两条腿投的是 core 的**同一个** `RuleSuggestion`
36
- * (同步腿 = `AskRequest.ruleSuggestions`,耐久腿 = `PendingAction.ruleSuggestions`,core
37
- * `checkpoint-store.d.ts` 明写「CONTRACT (same as the synchronous `AskRequest.ruleSuggestions`)」)。
38
- * 各写一份 zod 必然漂——闭词表 `match` 加词、上限改口径,只改一处就出两种形。
33
+ * 🔴 **两个数,别混**(合并码重扫,档实对齐;core 5.58.0 换形后口径逐字不变):core 的契约是 **≤2**
34
+ * (whole-string exact `single` index 0,`batch` 至多一条恒末位 —— core `checkpoint-store.d.ts` 与
35
+ * `permission-rule-model.d.ts` 逐字),那是**今天真实的**上限;本常量是 server 侧的**容忍帽**,刻意留在
36
+ * 4:一个被改坏/未来放宽的上游不该让运维队列的一行被整只拒收(同步腿这一格进的是 `.strict()` 的卡
37
+ * schema,收紧到 2 会把「多给一条展示用 offer」变成铸卡失败——在一条纯展示轴上换来一次 park,方向反了)。
38
+ * ⇒ 消费端按 **≤4** 布局,契约文同口径成文。
39
+ *
40
+ * 🔴 **截断只许取前缀**:序是 core 的契约(exact 恒 0、batch 恒末),而**选择键在原始下标上**
41
+ * (core d.ts:"keeping the ORIGINAL wire index for every element they keep")。取前缀保住了这两条;
42
+ * 任何重排/中间剔除都会让「人点的第 k 个」与「服务端兑的第 k 个」指向两条不同的规则。
43
+ */
44
+ export declare const MAX_RULE_OFFERS = 4;
45
+ /**
46
+ * 一条 `batch` offer 的**成员**基数容忍帽。core 契约是 1..5(卡道铸造帽),本数同样是**容忍余量**
47
+ * (理由与 {@link MAX_RULE_OFFERS} 逐字同源:纯展示轴上宁可多渲一条,也不为一条多出来的成员把整只卡
48
+ * 铸失败换成一次 park)。⚠️ 与 offer 基数分家的理由:两者数的是不同的东西(几个选项 vs 一个选项里
49
+ * 几条规则),混成一个数会让「上游把 batch 放宽到 6 条成员」误伤到 offer 条数这条无关轴。
39
50
  */
40
- export declare const RuleSuggestionSchema: z.ZodObject<{
51
+ export declare const MAX_RULE_OFFER_BATCH_MEMBERS = 8;
52
+ export declare const RuleOfferSchema: z.ZodUnion<readonly [z.ZodObject<{
53
+ kind: z.ZodLiteral<"single">;
41
54
  rule: z.ZodString;
42
55
  match: z.ZodEnum<{
43
56
  exact: "exact";
44
57
  prefix: "prefix";
45
58
  }>;
46
59
  command: z.ZodString;
47
- }, z.core.$strict>;
60
+ }, z.core.$strict>, z.ZodObject<{
61
+ kind: z.ZodLiteral<"batch">;
62
+ rules: z.ZodArray<z.ZodObject<{
63
+ rule: z.ZodString;
64
+ match: z.ZodEnum<{
65
+ exact: "exact";
66
+ prefix: "prefix";
67
+ }>;
68
+ command: z.ZodString;
69
+ segment: z.ZodString;
70
+ }, z.core.$strict>>;
71
+ uncoveredSegments: z.ZodNumber;
72
+ }, z.core.$strict>]>;
48
73
  /**
49
- * {@link RuleSuggestionSchema} 的**不限长**孪生 —— 只判**结构**(两个字符串 + 闭词表 `match`),长度由
74
+ * {@link RuleOfferSchema} 的**不限长**孪生 —— 只判**结构**(闭词表 `kind`/`match` + 若干字符串),长度由
50
75
  * 调用方在**脱敏之后**自己截。
51
76
  *
52
77
  * 🔴 为什么必须有它(codex 对抗复审 [medium],验真后修):`redactSecrets` **会变长**(一条
@@ -56,42 +81,61 @@ export declare const RuleSuggestionSchema: z.ZodObject<{
56
81
  * 两个后端对同一份素材给出不同的卡面。同族先例逐字在 `tool-approval.ts` 的 `persistedRuleShadowed`
57
82
  * 发帧点(「先脱敏再截,一次算定」)。
58
83
  *
59
- * ⇒ 正确顺序:**结构判(本 schema)→ 脱敏 → 截长 → 结果按 {@link RuleSuggestionSchema} 必然合形**。
84
+ * ⇒ 正确顺序:**结构判(本 schema)→ 脱敏 → 截长 → 结果按 {@link RuleOfferSchema} 必然合形**。
60
85
  * 这样函数对自己的输出**幂等**,两条腿同形。
61
86
  *
62
- * 🔴 **未知键 `strip`(合并码重扫放宽,重扫二轮改准形):窄读只判三键结构,多余键剥掉、且不拒收**。
63
- * base 带 `.strict()`,而 zod 4 的 `.extend()` **保留** strict —— core 给 `RuleSuggestion` 加一个 additive
64
- * 字段(它对这条面一贯的做法),逐条 `safeParse` 会**条条失败**,于是耐久腿的整只候选静默清零、列落
87
+ * 🔴 **未知键 `strip`(合并码重扫放宽,重扫二轮改准形):窄读只判结构,多余键剥掉、且不拒收**。
88
+ * base 带 `.strict()`,而 zod 4 的 `.extend()` **保留** strict —— core 给 `RuleOffer` 加一个 additive
89
+ * 字段(它对这条面一贯的做法),逐条 `safeParse` 会**条条失败**,于是耐久腿的整只 offer 静默清零、列落
65
90
  * NULL、读口省键,把「有供给」谎报成「无供给」,正是本键存在理由的反面(`checkpoint-store-sql.ts` 顶注与
66
- * wire 契约都写死「缺席=真的没有候选」)。同步腿的 `buildApprovalCard` 是显式三键投影、对加字段天然免疫
91
+ * wire 契约都写死「缺席=真的没有 offer」)。同步腿的 `buildApprovalCard` 是显式逐键投影、对加字段天然免疫
67
92
  * ⇒ 不放宽这一份,两条腿会在「上游长一格」这条轴上分家。
68
93
  *
69
94
  * ⚠️ **写的是 `.strip()` 不是 `.loose()`**(重扫二轮,实测更正):zod 4 的 `loose` = **passthrough** ——
70
95
  * 未知键**原样留在** `parsed.data` 里,与本注和契约文写的「多余键剥掉」相反。放宽要的是「不拒收」,
71
- * 剥键是另一件事,`.loose()` 只做到了前者。旧形下真正在剥键的是 {@link boundedRuleSuggestions} 里那三行
96
+ * 剥键是另一件事,`.loose()` 只做到了前者。旧形下真正在剥键的是 {@link boundedRuleOffers} 里那几行
72
97
  * 显式投影,于是本 schema 对「剥键」这条判据是**空跑**;而它是**导出符号**,output 类型带 index signature,
73
98
  * 任何新消费者拿 `parsed.data` 直投就把上游未知键(其上**没有** `redactSecrets` 覆盖)带上跨租户可见的
74
99
  * `GET /v1/approvals`。⇒ 改默认 strip 形:additive 加键照收、未知键当场剥掉,两个意图各自成立。
75
- * 落库/读回的 {@link ApprovalCardSchema} 仍 `.strict()`(投影只写三键,多余键根本到不了那一层)。
100
+ * 落库/读回的 {@link ApprovalCardSchema} 仍 `.strict()`(投影只写已知键,多余键根本到不了那一层)。
101
+ *
102
+ * 🔴 **判别联合上的 strip 要逐臂加**(zod 4:`z.union` 自己没有 unknownKeys 政策,政策在成员上)——
103
+ * 两臂各自 `.strip()`,否则 batch 臂对 additive 加键仍是 strict 的,而那正是本注要拆的雷。
76
104
  */
77
- export declare const RuleSuggestionRawSchema: z.ZodObject<{
105
+ export declare const RuleOfferRawSchema: z.ZodUnion<readonly [z.ZodObject<{
106
+ kind: z.ZodLiteral<"single">;
107
+ rule: z.ZodString;
78
108
  match: z.ZodEnum<{
79
109
  exact: "exact";
80
110
  prefix: "prefix";
81
111
  }>;
82
- rule: z.ZodString;
83
112
  command: z.ZodString;
84
- }, z.core.$strip>;
113
+ }, z.core.$strip>, z.ZodObject<{
114
+ kind: z.ZodLiteral<"batch">;
115
+ rules: z.ZodArray<z.ZodObject<{
116
+ rule: z.ZodString;
117
+ match: z.ZodEnum<{
118
+ exact: "exact";
119
+ prefix: "prefix";
120
+ }>;
121
+ command: z.ZodString;
122
+ segment: z.ZodString;
123
+ }, z.core.$strip>>;
124
+ uncoveredSegments: z.ZodNumber;
125
+ }, z.core.$strip>]>;
85
126
  /**
86
- * 候选**基数**的 server 执法上限。两条腿同用。
87
- *
88
- * 🔴 **两个数,别混**(合并码重扫,档实对齐):core 的契约是 **≤2**(`exact` 恒 index 0,至多再跟一条
89
- * reviewed `prefix` 形 —— core `checkpoint-store.d.ts` 与 `permission-rule-model.d.ts` 逐字),那是**今天
90
- * 真实的**上限;本常量是 server 侧的**容忍帽**,刻意留在 4:一个被改坏/未来放宽的上游不该让运维队列
91
- * 的一行被整只拒收(同步腿这一格进的是 `.strict()` 的卡 schema,收紧到 2 会把「多给一条展示用候选」
92
- * 变成铸卡失败——在一条纯展示轴上换来一次 park,方向反了)。⇒ 消费端按 **≤4** 布局,契约文同口径成文。
127
+ * 卡面/帧面共用的 offer **投影型**(= {@link RuleOfferSchema} 的输出形)。判别位 `kind` 上闭集,
128
+ * 消费端(SDK/cli 渲染)按 core 契约分臂;`batch.rules` 成员带 `segment` 渲染座(core 明写
129
+ * 「渲染座、永不参与裁决」,与 rule/command 同为 UNTRUSTED-for-display)。
93
130
  */
94
- export declare const MAX_RULE_SUGGESTIONS = 4;
131
+ export type RuleOfferProjection = z.infer<typeof RuleOfferSchema>;
132
+ /**
133
+ * 帧→卡素材的**显式逐键**复制(与旧形 `ruleSuggestions` 的内联三键 map 同职,换形后判别联合需要
134
+ * 分臂,抽成具名函数)。为什么不 `structuredClone`/展开:显式逐键投影是本模块对「上游 additive
135
+ * 加键」的免疫层(见 {@link RuleOfferRawSchema} 顶注)——加键到不了 `card_json`,strip 语义在
136
+ * 投影处兑现,不靠 schema 层兜。
137
+ */
138
+ export declare function copyRuleOffer(o: RuleOffer): RuleOfferProjection;
95
139
  /**
96
140
  * #253 件 G1 —— `probeCause` 的形(落库/读面共用这一份;窄读入口是 {@link readProbeCause})。
97
141
  *
@@ -178,14 +222,27 @@ export declare const ApprovalCardSchema: z.ZodObject<{
178
222
  governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
179
223
  inputHasBidi: z.ZodOptional<z.ZodLiteral<true>>;
180
224
  persistedRuleShadowed: z.ZodOptional<z.ZodString>;
181
- ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
225
+ ruleOffers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
226
+ kind: z.ZodLiteral<"single">;
182
227
  rule: z.ZodString;
183
228
  match: z.ZodEnum<{
184
229
  exact: "exact";
185
230
  prefix: "prefix";
186
231
  }>;
187
232
  command: z.ZodString;
188
- }, z.core.$strict>>>;
233
+ }, z.core.$strict>, z.ZodObject<{
234
+ kind: z.ZodLiteral<"batch">;
235
+ rules: z.ZodArray<z.ZodObject<{
236
+ rule: z.ZodString;
237
+ match: z.ZodEnum<{
238
+ exact: "exact";
239
+ prefix: "prefix";
240
+ }>;
241
+ command: z.ZodString;
242
+ segment: z.ZodString;
243
+ }, z.core.$strict>>;
244
+ uncoveredSegments: z.ZodNumber;
245
+ }, z.core.$strict>]>>>;
189
246
  probeCause: z.ZodOptional<z.ZodObject<{
190
247
  code: z.ZodString;
191
248
  roots: z.ZodObject<{
@@ -241,14 +298,27 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
241
298
  governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
242
299
  inputHasBidi: z.ZodOptional<z.ZodLiteral<true>>;
243
300
  persistedRuleShadowed: z.ZodOptional<z.ZodString>;
244
- ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
301
+ ruleOffers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
302
+ kind: z.ZodLiteral<"single">;
245
303
  rule: z.ZodString;
246
304
  match: z.ZodEnum<{
247
305
  exact: "exact";
248
306
  prefix: "prefix";
249
307
  }>;
250
308
  command: z.ZodString;
251
- }, z.core.$strict>>>;
309
+ }, z.core.$strict>, z.ZodObject<{
310
+ kind: z.ZodLiteral<"batch">;
311
+ rules: z.ZodArray<z.ZodObject<{
312
+ rule: z.ZodString;
313
+ match: z.ZodEnum<{
314
+ exact: "exact";
315
+ prefix: "prefix";
316
+ }>;
317
+ command: z.ZodString;
318
+ segment: z.ZodString;
319
+ }, z.core.$strict>>;
320
+ uncoveredSegments: z.ZodNumber;
321
+ }, z.core.$strict>]>>>;
252
322
  probeCause: z.ZodOptional<z.ZodObject<{
253
323
  code: z.ZodString;
254
324
  roots: z.ZodObject<{
@@ -312,14 +382,10 @@ export interface ApprovalCardSource {
312
382
  /** #144(core 5.25.0):被越级的持久规则原文 —— **已 redactSecrets**(发帧点做,本模块只 clip)。
313
383
  * 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口)⇒ live 帧 / `card_json` / 重放帧三面同源。 */
314
384
  persistedRuleShadowed?: string;
315
- /** #154 车二:引擎铸的规则候选(**只在规则店装配时**由发帧点填;语义与在场性契约见
316
- * `ApprovalCardSchema.ruleSuggestions`)。与 wire 帧**同一份素材** ⇒ live 帧 / `card_json` / 重放帧
385
+ /** #346(core 5.58.0):引擎铸的「不再询问」OFFER(**只在规则店装配时**由发帧点填;语义与在场性契约见
386
+ * `ApprovalCardSchema.ruleOffers`)。与 wire 帧**同一份素材** ⇒ live 帧 / `card_json` / 重放帧
317
387
  * 三面同源。 */
318
- ruleSuggestions?: ReadonlyArray<{
319
- rule: string;
320
- match: "exact" | "prefix";
321
- command: string;
322
- }>;
388
+ ruleOffers?: readonly RuleOffer[];
323
389
  fromSubagent?: true;
324
390
  sourceTaskId?: string;
325
391
  /** 已 redactSecrets。 */
@@ -3,11 +3,50 @@ import { ASK_EVIDENCE_ABSENCE_VALUES, MAX_RULE_TEXT_CHARS } from "@sema-agent/co
3
3
  import { redactSecrets } from "./trace/redact.js";
4
4
  export const MAX_AGENT_NAME = 200;
5
5
  const MAX_IDENT = 200;
6
- export const RuleSuggestionSchema = z
7
- .object({ rule: z.string().max(MAX_RULE_TEXT_CHARS), match: z.enum(["exact", "prefix"]), command: z.string().max(MAX_RULE_TEXT_CHARS) })
6
+ export const MAX_RULE_OFFERS = 4;
7
+ export const MAX_RULE_OFFER_BATCH_MEMBERS = 8;
8
+ const SegmentRuleSuggestionSchema = z
9
+ .object({
10
+ rule: z.string().max(MAX_RULE_TEXT_CHARS),
11
+ match: z.enum(["exact", "prefix"]),
12
+ command: z.string().max(MAX_RULE_TEXT_CHARS),
13
+ segment: z.string().max(MAX_RULE_TEXT_CHARS),
14
+ })
8
15
  .strict();
9
- export const RuleSuggestionRawSchema = RuleSuggestionSchema.extend({ rule: z.string(), command: z.string() }).strip();
10
- export const MAX_RULE_SUGGESTIONS = 4;
16
+ export const RuleOfferSchema = z.union([
17
+ z.object({ kind: z.literal("single"), rule: z.string().max(MAX_RULE_TEXT_CHARS), match: z.enum(["exact", "prefix"]), command: z.string().max(MAX_RULE_TEXT_CHARS) }).strict(),
18
+ z
19
+ .object({
20
+ kind: z.literal("batch"),
21
+ rules: z.array(SegmentRuleSuggestionSchema).min(1).max(MAX_RULE_OFFER_BATCH_MEMBERS),
22
+ uncoveredSegments: z.number().int().nonnegative().safe(),
23
+ })
24
+ .strict(),
25
+ ]);
26
+ export const RuleOfferRawSchema = z.union([
27
+ z.object({ kind: z.literal("single"), rule: z.string(), match: z.enum(["exact", "prefix"]), command: z.string() }).strip(),
28
+ z
29
+ .object({
30
+ kind: z.literal("batch"),
31
+ rules: z.array(z.object({ rule: z.string(), match: z.enum(["exact", "prefix"]), command: z.string(), segment: z.string() }).strip()).min(1),
32
+ uncoveredSegments: z.number().int().nonnegative().safe(),
33
+ })
34
+ .strip(),
35
+ ]);
36
+ export function copyRuleOffer(o) {
37
+ switch (o.kind) {
38
+ case "single":
39
+ return { kind: "single", rule: o.rule, match: o.match, command: o.command };
40
+ case "batch":
41
+ return {
42
+ kind: "batch",
43
+ rules: o.rules.map((r) => ({ rule: r.rule, match: r.match, command: r.command, segment: r.segment })),
44
+ uncoveredSegments: o.uncoveredSegments,
45
+ };
46
+ default:
47
+ throw new Error(`unknown rule-offer kind: ${JSON.stringify(o)}`);
48
+ }
49
+ }
11
50
  const MAX_MESSAGE = 8192;
12
51
  const MAX_PROBE_CAUSE_CODE = 200;
13
52
  const MAX_PROBE_CAUSE_PATH = 200;
@@ -129,7 +168,7 @@ export const ApprovalCardSchema = z
129
168
  governanceForced: z.literal(true).optional(),
130
169
  inputHasBidi: z.literal(true).optional(),
131
170
  persistedRuleShadowed: z.string().max(MAX_RULE_TEXT_CHARS).optional(),
132
- ruleSuggestions: z.array(RuleSuggestionSchema).max(MAX_RULE_SUGGESTIONS).optional(),
171
+ ruleOffers: z.array(RuleOfferSchema).max(MAX_RULE_OFFERS).optional(),
133
172
  probeCause: ProbeCauseSchema.optional(),
134
173
  ruleEvidence: RuleEvidenceSchema.optional(),
135
174
  fromSubagent: z.literal(true).optional(),
@@ -189,9 +228,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
189
228
  ...(shadowedRule !== undefined && shadowedRule !== "" ? { persistedRuleShadowed: shadowedRule } : {}),
190
229
  ...(probeCause !== undefined ? { probeCause } : {}),
191
230
  ...(ruleEvidence !== undefined ? { ruleEvidence } : {}),
192
- ...(source.ruleSuggestions !== undefined && source.ruleSuggestions.length > 0
193
- ? { ruleSuggestions: source.ruleSuggestions.map((s) => ({ rule: s.rule, match: s.match, command: s.command })) }
194
- : {}),
231
+ ...(source.ruleOffers !== undefined && source.ruleOffers.length > 0 ? { ruleOffers: source.ruleOffers.map(copyRuleOffer) } : {}),
195
232
  ...(source.fromSubagent === true ? { fromSubagent: true } : {}),
196
233
  ...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
197
234
  ...(sourceAgentName !== undefined ? { sourceAgentName } : {}),
@@ -22,7 +22,7 @@ export interface LiveCoordinatorsCtx {
22
22
  backend: StoreBackend | undefined;
23
23
  sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
24
24
  /** #154 车二:持久化权限规则的同意车道(main.ts 与 `RunnerDeps.permissionRuleStore` **同源**于
25
- * `backend.permissionRule()` 的同一个返回值)。在场 ⇒ ask 帧投 `ruleSuggestions` + 回决可兑付。 */
25
+ * `backend.permissionRule()` 的同一个返回值)。在场 ⇒ ask 帧投 `ruleOffers` + 回决可兑付。 */
26
26
  ruleConsent: RuleConsentLane | undefined;
27
27
  /** #295(F-1):卡批规则的 project root 解析器(语义单点=task-cwd.ts `cardRuleScopeRoot`;main.ts
28
28
  * 用 per-session cwd 登记簿 + config 铸)。缺席 ⇒ 不铸 scope,卡批规则落 core 的 global 缺省。 */
@@ -0,0 +1,191 @@
1
+ import { type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type MemoryBackend, type MemoryConsolidationDriverDeps, type RunMemoryConsolidationOptions } from "@sema-agent/core";
2
+ /** D3 投影的座标签闭集。`role:<name>` 里的 name 只可能是 core 的 role 链两跳之一。 */
3
+ export type ConsolidationSeatLabel = "explicit" | "role:consolidate" | "role:summarize";
4
+ /** {@link buildConsolidationValveAudit} 的告警名闭集(事件名进日志 ⇒ 运维可按名过滤/告警)。 */
5
+ export type ConsolidationValveWarningTag = "memory_consolidation_valve_unreachable" | "memory_consolidation_run_credential_gated";
6
+ /** 座解析的产物:core 的 options 原样 + 给投影用的两位自证。 */
7
+ export interface ConsolidationDriverSeat {
8
+ /** `runMemoryConsolidationDriver` 的第三参,core 的 `resolveMemoryConsolidationDriver` 原样产出。 */
9
+ readonly options: RunMemoryConsolidationOptions;
10
+ /** D3 的 `seat` 位。 */
11
+ readonly seat: ConsolidationSeatLabel;
12
+ /** 真正会被记进 run 行与 mint 归档的模型 id(= `options.model`,单独拎出来是投影面的便利)。 */
13
+ readonly model: string;
14
+ }
15
+ /**
16
+ * 座解析 + **拒启**包装(D1)。
17
+ *
18
+ * ## 标签怎么来的:拿 core 自己的解析器**再问一次**,不抄它的判据
19
+ *
20
+ * core 的链是 `显式 chat → roles.consolidate → roles.summarize → coded refusal`,而"哪一跳答的"这件事
21
+ * 它不回报。本层**不**照着 `roleModelIfSet` 再写一份判断(那就是第二真源,而且真会答错:
22
+ * `ROLE_TIER_DEFAULTS` 给 `summarize` 一个 `flash` 档位缺省、给 `consolidate` 一个都没有,所以
23
+ * 「roles 表里有没有写 consolidate」这个直觉判据在档位部署上恒错)。
24
+ *
25
+ * 做法是**行为探针**:把 summarize 那一跳**结构性关掉**,再调**同一只**
26
+ * `resolveMemoryConsolidationDriver` 问一次 —— 它答得出来 ⇒ consolidate 那一跳赢;它拒 ⇒ 完整解析既然
27
+ * 成功,赢的就只能是 summarize。于是标签与 core 的判决**结构上**不可能分歧,上游哪天调整链条,这只探针
28
+ * 跟着变,不需要有人记得回来改。「怎么关掉那一跳」的两个细节(以及两版被证否的做法)见 {@link seatLabel}
29
+ * 体内的注 —— 那里才是它真正生效的地方,写在这里会随实现漂。
30
+ * (探针只构造闭包、不发任何请求、无副作用。)
31
+ */
32
+ export declare function resolveConsolidationDriverSeat(deps: MemoryConsolidationDriverDeps): ConsolidationDriverSeat;
33
+ /**
34
+ * **每 scope 单飞**(codex 对抗复审 [high],验真后修)—— 同一 scope 上在飞的那一轮被后来者**并入**,
35
+ * 而不是各起一轮。`create*`:返回带状态的活对象。
36
+ *
37
+ * ## 病(不是理论)
38
+ *
39
+ * 这一口是**分钟级**的:出厂缺省下一次冷启动折叠是十几个 cycle,cycle 之间还有 60s 硬节流。于是 HTTP
40
+ * 客户端**必然**会先超时 —— 而超时**不取消**服务端那一轮。文档教人「原样重发」,重发恰好落在**重叠**
41
+ * 这一格上,而 core 明写它的 attempt CAS 序列化的是**账**、不是模型开销("model spend under a genuine
42
+ * concurrent double-call is still at-least-once")。⇒ 一次超时重试可以买到**两份**整库蒸馏的账单。
43
+ *
44
+ * ## 边界(必须说准,不许当成分布式互斥卖)
45
+ *
46
+ * 本单飞是**进程内**的:同副本上的重叠被消灭,跨副本仍是 at-least-once(那需要一把 durable 租约,
47
+ * 本仓没有 —— 同 §遗留 遗-1 的那条基建缺口)。今天够得着这条阀门的部署形态是 file 记忆后端,那种部署
48
+ * 实际上是单副本,所以进程内单飞覆盖了真实的失败形;但话按**它真正保证的**说,不按它常常够用说。
49
+ *
50
+ * 单飞**不是缓存**:那一轮落定(无论成败)即刻放行下一轮 —— 否则一次失败会把这个 scope 永久钉死。
51
+ */
52
+ export declare function createPerScopeSingleFlight<T>(): (scope: string, run: () => Promise<T>) => Promise<T>;
53
+ /**
54
+ * 阀门自持的 boot 不变式(`boot/retention-lane.ts` 的 `assertRetentionLaneWirable` 同族同姿态)——
55
+ * 「阀门开着的时候,它真的跑得起来吗」。三条判据都是**永久性**事实(换部署形态才会变),所以判在启动期:
56
+ *
57
+ * ① **记忆引擎没接线**(`MEMORY_ENGINE=off`,或多租户形——本仓的记忆引擎只在单用户部署上装配):
58
+ * 没有引擎就没有库可折叠,阀门是一句空话;
59
+ * ② **后端不自带控制面归属**(`controlPlaneRoot`):driver 的 run 账(`distiller-runs.json`)、mint 归档
60
+ * 与引擎的 consolidation gate 全住在控制面上。缺它时 core 会把控制面**回落到本进程的本地盘**
61
+ * (`deriveControlPlaneDir`),而本仓是 stateless replicas ⇒ 一轮分钟级 run 的续跑账会落在某个随机
62
+ * pod 的盘上,pod 重建即失,而 run 是**可续跑**设计的(重跑=整库重新 mint,真金白银)。判据与
63
+ * `createMemoryComplianceFaces` 逐字同源(读的是后端的**面**,不看后端方言名);
64
+ * ③ **`MEMORY_PROVENANCE=off`**:core 的 `screenConsolidationOptions` 明写 consolidation 不能在
65
+ * `provenance:"off"` 下开(fold law 必须铸得出 origin 标记,否则一个被标记过的输入折出来的产物会
66
+ * **不带标记**地提交)。这一条本来就会在引擎构造期抛,提前判只为一件事:把**两根**旋钮的名字同时
67
+ * 写进句子 —— core 的那句话只提到 provenance,而运维要知道是哪一根把这台机器拦下的。
68
+ *
69
+ * 阀门关着 ⇒ 整条门不判(缺省姿态,#210「缺席不在律内」)。
70
+ */
71
+ export declare function assertConsolidationDriverWirable(input: {
72
+ enabled: boolean;
73
+ /** 本部署接了记忆引擎吗(`boot/stores.ts` 的 `memoryEngine !== undefined`)。 */
74
+ memoryEngineWired: boolean;
75
+ /** 记忆后端自带控制面归属吗(`typeof backend.controlPlaneRoot === "string"` 且非空)。 */
76
+ controlPlaneOwned: boolean;
77
+ /** 只进文案 —— 让「该去拧哪根旋钮」这句话指向该腿上**真的**有效的那一根。 */
78
+ memoryEngineBackend: "file" | "pg" | "tidb";
79
+ /** 部署声明的 provenance 姿态(`config.memoryProvenance`;缺席 ≡ core 缺省 `carry`)。 */
80
+ provenance?: "off" | "carry";
81
+ }): void;
82
+ /**
83
+ * 阀门上了场之后的**可达性**告警两条(自查 + codex 对抗复审;姿态=warn 不拒启)。
84
+ *
85
+ * ## 病(结构性,而且**只在**这条阀门够得着的那种部署上成立)
86
+ *
87
+ * consolidation 要后端自带引擎控制面归属 ⇒ 今天只有 file 记忆后端满足 ⇒ 本仓只在**单用户**形
88
+ * (`REQUIRE_PRINCIPAL !== true`)上装配记忆引擎 —— 而那种部署的 `OPERATOR_PRINCIPALS` 通常是**空的**。
89
+ * 空名单下 `explicitOperatorOk` 对任何身份恒假(那是它与 `isOperator` 刻意分家的地方:空名单绝不等于
90
+ * 「人人都是 operator」),于是:阀门开着、启动成功、两口挂着、**每一次调用 403**,能力位诚实地报
91
+ * `false` —— 唯独运维不知道为什么,而他刚刚显式拧了一根写着 "on" 的旋钮。
92
+ *
93
+ * ## 为什么是 warn 而不是拒启(与 D1 的三条拒启臂**刻意不同档**)
94
+ *
95
+ * `OPERATOR_PRINCIPALS` 是**跨面共用**的部署级旋钮(adoption / retention-ops / memory-bundle /
96
+ * memory-compliance / admin-drain 全在同一张名单上)。为一条阀门拒掉整台机器,比这条阀门本身的价值重;
97
+ * 而那三条拒启臂拒的是「这台机器**结构上**跑不了这件事」(没有库 / 账会丢 / 折叠法铸不出标记),
98
+ * 本条拒的是「配齐了但没人够得着」——后者是一次**配置遗漏**,补一行 env 即可,不该要一次重启失败来教。
99
+ * 同族先例逐字:`permission_rules_lane_unavailable`(旋钮开着却上不了场 ⇒ 打一行,不拒启)。
100
+ * 反过来说,**沉默**同样不行:那正是「设了但没生效」病族,而这一形连能力位都是诚实的假,唯一缺的
101
+ * 就是一句把因果说清楚的话。
102
+ */
103
+ export declare function buildConsolidationValveAudit(input: {
104
+ enabled: boolean;
105
+ operatorPrincipals: readonly string[];
106
+ /** 这台部署配了任何一把 service 凭证吗(`config.authToken` 或 `config.authTokens` 非空)。 */
107
+ hasServiceAuth: boolean;
108
+ /** 本地开发的显式逃生口(`ALLOW_UNAUTHED_WRITES=true`)。 */
109
+ allowUnauthedWrites: boolean;
110
+ }): {
111
+ warnings: Array<{
112
+ tag: ConsolidationValveWarningTag;
113
+ detail: string;
114
+ }>;
115
+ };
116
+ /**
117
+ * 停因诊断行的**运维日志**投影(codex 对抗复审 R2-[medium],验真后修)。`build*`:纯数据。
118
+ *
119
+ * `stopDetail` 已经不上 wire(`routes/memory-consolidation.ts` 的同名段:core 的 `driver_failed` 支把
120
+ * 模型座抛出的 `Error.message` 逐字拼进去)。但**落日志同样要有信任边界** —— 集中式日志是人读的,
121
+ * 而一段来路不明的文本混在自家诊断里,既可能夹带凭据,也可能把攻击者写的"操作建议"摆成本服务的口吻。
122
+ * 三件:
123
+ * ① 字段名 `untrustedDetail` **自陈**(一个叫 `detail` 的键会被下一个读日志的人当成本服务自己的诊断);
124
+ * ② `redactSecrets` 脱敏(与审批卡片面同一只属主函数,不在本层另写一份模式表);
125
+ * ③ **先脱敏再截长** —— `redactSecrets` 会**变长**,先截后脱敏会把一半的凭据留在窗口外
126
+ * (approval-card.ts 的同款先例逐字,那里踩过)。
127
+ * 调用点在 {@link createMemoryConsolidationFaces} 的单飞回调**之内** ⇒ 一轮真 run 只落一行,而不是
128
+ * 每个并入的 HTTP 等待者各落一行。
129
+ */
130
+ export declare function buildStopDetailAudit(input: {
131
+ scope: string;
132
+ runId: string;
133
+ outcome: string;
134
+ stopDetail: string;
135
+ }): {
136
+ scope: string;
137
+ runId: string;
138
+ outcome: string;
139
+ untrustedDetail: string;
140
+ };
141
+ /** 两个 admin 口消费的窄能力面(`ServiceDeps.memoryConsolidation` 的实参来源)。 */
142
+ export interface MemoryConsolidationFaces {
143
+ /** D3 的座位自证(投影用;boot 期解析一次,restart-to-apply)。 */
144
+ readonly seat: ConsolidationSeatLabel;
145
+ /** 座位解析出来的模型 id —— 它就是会被记进 run 行与 mint 归档的那一个。 */
146
+ readonly model: string;
147
+ /** 配置表的 scope(`MEMORY_CONSOLIDATION_SCOPES`)。恰好一条时是两个口的**唯一**缺省。 */
148
+ readonly scopes: readonly string[];
149
+ /** 跑(或**续跑**)一轮。core 的 run 行是持久的:同一 scope 上有 pending run 时本调用 CONTINUE 它
150
+ * (同一 plan 缓存、同一 force requestId、零新 mint)—— 所以这一口对**重试安全**。 */
151
+ run(scope: string): Promise<ConsolidationRunReceipt>;
152
+ /** 该 scope 最近一次 driver run 的账(`undefined` = 从没跑过 —— 诚实缺席,不编一个空壳)。 */
153
+ lastRun(scope: string): ConsolidationDriverRunRow | undefined;
154
+ }
155
+ /**
156
+ * 在**已装配的记忆后端**上建 consolidation 操作面。命名照 CLAUDE.md 工厂律:返回带行为的活对象 ⇒ `create*`。
157
+ *
158
+ * ## 为什么这里现构一只引擎(与 `createMemoryComplianceFaces` / `createMemoryBundleFaces` 同一条理由)
159
+ *
160
+ * 本仓**没有**一个长命的 `MemoryEngine` 实例:`boot/stores.ts` 装配出来的 `memoryEngine` 是
161
+ * `{ backend, root }` 二元组,真引擎由 core 的 Runner **每任务现构**。HTTP 面要一个引擎就得自己构。
162
+ * 两只引擎不会各说各话:控制面由后端钉死(`backend.controlPlaneRoot`),gate / run 账 / plan 归档
163
+ * 全住在那里,所以操作面引擎与每任务引擎看的是**同一本**账。
164
+ *
165
+ * ## `consolidation: {}` = 出厂缺省,**显式在场**
166
+ *
167
+ * core 的 `MemoryEngineOptions.consolidation` 是 **absent = OFF**(v3 缺省),而 driver 的四个 verb 在
168
+ * OFF 的引擎上一律 coded-refuse。所以这只引擎必须**显式**带上它 —— 否则能力位说 yes 而每一次调用
169
+ * 确定性失败,正是本仓「says yes ⟺ route works」明令禁止的那种恒假的 yes。传空对象 = 逐项取 core 的
170
+ * 出厂缺省(fuse 0.25/floor 4、每轮 64 产物、24h 时间闸、5 会话闸),**刻意不为它们各开一根旋钮**:
171
+ * 那七个数是 core 自己校准的写协议参数,本仓没有任何测量说明该动它们(要动时先有部署案例,再开旋钮)。
172
+ * `multiNode` 也**刻意不声明**:声明它就必须注入一只全局租约,而本仓今天没有(见顶注「v1 没有周期腿」
173
+ * 那一段的同一条理由)——不声明 = 单机 plan-seat CAS 是唯一互斥,core 的契约原话如此,不是偷偷降级。
174
+ */
175
+ export declare function createMemoryConsolidationFaces(store: {
176
+ backend: MemoryBackend;
177
+ root: string;
178
+ }, input: {
179
+ seat: ConsolidationDriverSeat;
180
+ scopes: readonly string[];
181
+ /** 部署的 provenance 姿态,原样交给引擎(缺席 ⇒ core 缺省 `carry`)。 */
182
+ provenance?: "off" | "carry";
183
+ /** 引擎的 advisory incident 座(与 `createMemoryComplianceFaces` 同一条理由:只走这个座的码不接就没了)。 */
184
+ onIncident?: (err: Error & {
185
+ code?: string;
186
+ }) => void;
187
+ /** 停因自由文本的**运维日志**座(缺席 = 没人听,不影响任何行为)。载荷由 {@link buildStopDetailAudit}
188
+ * 铸,调用点在单飞回调**之内** ⇒ 一轮真 run 一行,并入的等待者不各落一行。 */
189
+ onStopDetail?: (audit: ReturnType<typeof buildStopDetailAudit>) => void;
190
+ }): MemoryConsolidationFaces;
191
+ //# sourceMappingURL=memory-consolidation.d.ts.map