@sema-agent/server 7.80.2 → 7.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +40 -4
- package/dist/approval-ask-audit-store.d.ts +13 -7
- package/dist/boot/memory-consolidation.d.ts +38 -0
- package/dist/boot/memory-consolidation.js +14 -0
- package/dist/boot/reapers.js +1 -2
- package/dist/boot/runner-deps.d.ts +11 -0
- package/dist/boot/runner-deps.js +15 -0
- package/dist/boot/stage-06-runners.js +13 -8
- package/dist/config-catalog.js +1 -0
- package/dist/config-types.d.ts +26 -0
- package/dist/config.js +9 -1
- package/dist/http/routes/diagnostics.d.ts +3 -2
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +1 -0
- package/dist/plugins/approval-ask-store-file.d.ts +171 -0
- package/dist/plugins/approval-ask-store-file.js +258 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +60 -2
- package/dist/plugins/approval-ask-store-memory.js +30 -12
- package/dist/plugins/checkpoint-store-sql.d.ts +22 -0
- package/dist/plugins/checkpoint-store-sql.js +1 -1
- package/dist/plugins/file-run-store.d.ts +6 -2
- package/dist/plugins/file-run-store.js +2 -1
- package/dist/plugins/local-checkpoint-store.d.ts +29 -1
- package/dist/plugins/local-checkpoint-store.js +24 -1
- package/dist/plugins/posix-shell-fs.d.ts +2 -1
- package/dist/plugins/posix-shell-fs.js +4 -1
- package/dist/plugins/remote-env-adb.d.ts +5 -1
- package/dist/plugins/remote-env-adb.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +8 -1
- package/dist/plugins/remote-env-e2b.js +4 -1
- package/dist/plugins/remote-env-host.d.ts +15 -1
- package/dist/plugins/remote-env-host.js +37 -1
- package/dist/plugins/remote-env-k8s.d.ts +5 -1
- package/dist/plugins/remote-env-k8s.js +3 -0
- package/dist/plugins/remote-env-local-docker.d.ts +5 -1
- package/dist/plugins/remote-env-local-docker.js +3 -0
- package/dist/plugins/remote-env-ssh.d.ts +5 -1
- package/dist/plugins/remote-env-ssh.js +3 -0
- package/dist/plugins/remote-shell.d.ts +78 -1
- package/dist/plugins/remote-shell.js +35 -0
- package/dist/plugins/store-backend.d.ts +9 -8
- package/dist/plugins/store-backend.js +7 -4
- package/dist/plugins/store-contracts.d.ts +20 -0
- package/dist/plugins/store-contracts.js +2 -0
- package/dist/tool-approval.d.ts +11 -6
- package/dist/tool-approval.js +0 -2
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.d.ts +18 -0
- package/dist/trace/project.js +11 -0
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -509,9 +509,13 @@ QUESTION_TTL_MS=300000
|
|
|
509
509
|
# 回滚键是下面的 UNATTENDED_APPROVAL_POLICY=deny(两根旋钮正交,别当一根用);它也**不**再回滚窗长
|
|
510
510
|
# (窗是活卡件不是协议件,见下一键——缺省值与旧硬编码窗同为 300000,没配过就仍然字节不变)
|
|
511
511
|
STREAM_APPROVAL_ENABLED=true
|
|
512
|
-
# 活卡窗(
|
|
513
|
-
#
|
|
514
|
-
#
|
|
512
|
+
# 活卡窗(毫秒)。**辖域=所有腿**:协议上不上场都按它计窗——无 checkpoint 店 / 关了协议的部署同样
|
|
513
|
+
# 生效(此前那几形上本键一个字节都不生效、窗恒 5min,且没有任何一行说出来)。
|
|
514
|
+
# 🔴 **缺省值是 posture 派生的,不是一个数**(S-178):单机 turnkey(`REQUIRE_PRINCIPAL` 未设)=
|
|
515
|
+
# **86400000(24 小时)**,多租户形 = 300000(5min)。**S-384 起这根旋钮在单机上是要读的那一根**:
|
|
516
|
+
# local 后端自此有持久 ask 账、流内协议上场,受门 ask 先发一张活卡、**窗满才转 park**(升级前是铸造
|
|
517
|
+
# 时刻立刻 park)⇒ 不配它的**无人值守**单机最多挂一天才落 park。无人值守机器请显式给几秒;
|
|
518
|
+
# 有人值守的单机正是那一天窗口的受益者。0 = 运维显式关窗 ⇒ **一张卡都不发**、恒走无人值守终局(不是"还原",还原用上面那个键;
|
|
515
519
|
# 缺省 park 政策 + park 设施在场 ⇒ 全落 durable park 经 /v1/approvals/:sessionId/decide 兑现;
|
|
516
520
|
# 配了 UNATTENDED_APPROVAL_POLICY=deny 或没有 park 设施的部署这里是"恒当场拒"= 受门工具全关,
|
|
517
521
|
# boot 期打一行 stream_ask_window_zero 点名)。负值**拒启**(此前静默等价于 0 还白发一张没人赢得了的卡)
|
|
@@ -570,7 +574,9 @@ UNREACHED_ASK_TTL_MS=3600000
|
|
|
570
574
|
耗尽(十几分钟量级)才感知——这段时间内真正的兜底是**活卡窗到期转 park**(`STREAM_ASK_WINDOW_MS`,
|
|
571
575
|
默认 5min),所以把窗改大等于把断连场景的最坏无人区拉长,改小前先看上面的跨旋钮不变量。half-open 的
|
|
572
576
|
主动探测(写水位判死)在攻关排期。
|
|
573
|
-
- **无 durable 前置的部署(
|
|
577
|
+
- **无 durable 前置的部署(四合取不满足:无 backend / 无 checkpoint 能力 / 显式关协议 / 无活卡腿;
|
|
578
|
+
⚠️ **S-384 起 `local` backend 不再是其中一项** —— 它有了文件形 ask 账,配齐 `DURABLE_APPROVAL=true`
|
|
579
|
+
+ checkpoint 店的单机部署上协议**照常上场**,`GET /v1/capabilities` 的 `streamApproval` 报 true)**:流内协议整个不上场
|
|
574
580
|
(启动一行 `stream_approval_disabled` 点名缺哪项),ask 走旧活卡腿——断连场景的最坏结局是活卡 TTL
|
|
575
581
|
窗满**自动 deny(fail-closed)后 run 继续**,不会死锁在等一张没人能回的卡上;代价是断连期间用户的
|
|
576
582
|
批准机会直接过期。(7.34.0 起这条 deny 由**引擎**在「没有可停靠的 durable 门」时给出——服务侧只报
|
|
@@ -580,6 +586,9 @@ UNREACHED_ASK_TTL_MS=3600000
|
|
|
580
586
|
(卡挂过窗、人才按 Yes)不能走 `POST /v1/tool-approvals/:id/respond`(没有持久 ask 行 ⇒ 恒 404
|
|
581
587
|
`tool_approval.not_pending`、不带 `cause`),要走 `POST /v1/approvals/:sessionId/decide`
|
|
582
588
|
——前提是配了 `DURABLE_APPROVAL=true`(local 车道有文件形 checkpoint 店),否则窗到期即 fail-closed 拒。
|
|
589
|
+
⚠️ **上面这段「没有持久 ask 行 ⇒ 恒 404」自 S-384 起不再适用于 `DB_BACKEND=local` 本身**:local 的
|
|
590
|
+
ask 账已是文件形,配齐 park 设施的单机部署上迟到决议走 `respond` 的赎回席是通的;本段现在只说
|
|
591
|
+
那几条**真的**不上场的部署形(无 backend / 无 checkpoint 能力 / 显式关协议)。
|
|
583
592
|
默认值(300s + 60s vs 7d)自然满足,只有显式改坏才会撞上。
|
|
584
593
|
- **调参方向**:想让人有更长时间点审批卡 → 调大 `STREAM_ASK_WINDOW_MS`;卡太多刷屏 → 调小两个 `ADMIT_MAX_*`
|
|
585
594
|
(代价是超限的 ask 走 park,要有人去审批队列捞);库压大 → 调小 `STREAM_APPROVAL_RECONCILE_BATCH`
|
|
@@ -1128,6 +1137,33 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
1128
1137
|
> ④**模型座位解析不出来**——既没有显式 chat 席,也没有 `roles.consolidate` / `roles.summarize`(含档位表的
|
|
1129
1138
|
> `flash` 绑定)。core **刻意不回落主模型**(整库蒸馏不该悄悄骑最贵的席位),本服务照抄那条判断,不在下游
|
|
1130
1139
|
> 补一个兜底席。
|
|
1140
|
+
> - `MEMORY_AUTO_CONSOLIDATION=on|off`(**未设 = 读引擎缺省**,见下):记忆**自动整理**的部署席
|
|
1141
|
+
> (core 7.21.0 的 `RunnerDeps.autoRunOnRecommendation` 席)。武装 ⇒ **每个产生了整理建议的任务
|
|
1142
|
+
> 之后**,为该 scope 起**一次**整库整理跑 —— 🔴 **记忆内容自动外流到配置的整理模型**。
|
|
1143
|
+
> **三态,别读成布尔**:
|
|
1144
|
+
> - **未设** = 不写引擎那个键 ⇒ 读引擎的具名缺省,而**本版装的引擎那个缺省是「开」**。
|
|
1145
|
+
> ⇒ 🔴 **不配这根旋钮 = 武装**(在下面那条前置满足时)。缺省的属主在引擎侧、会随提货漂,所以
|
|
1146
|
+
> 「什么都不配」这一态的含义**不是稳定的**;要一个稳定的答案就显式写一个词。
|
|
1147
|
+
> - `off` = **显式关,胜过缺省** —— 这是**关闭口**。不想让记忆自动外流的部署,靠它在 core 把缺省翻开
|
|
1148
|
+
> 之后仍然保持关闭。**这是本旋钮存在的理由。**
|
|
1149
|
+
> - `on` = 武装。**设之前请先读**「这是出口决定不是性能旋钮」那一句:一轮 consolidation 读**全库**
|
|
1150
|
+
> (~1e5 prompt tokens)并把内容交给整理模型,既是钱更是**数据出境**。
|
|
1151
|
+
> ⚠️ **闭集两词,别的一律拒启**(与上一根同一条纪律,方向在这根上更要紧:一个被读成「未设」的拼错关词
|
|
1152
|
+
> 会让那台部署跟着 core 的新缺省**静默武装**,而运维手里握着一份自以为关掉了的配置)。
|
|
1153
|
+
> 🔴 **前置 = 上一根旋钮**:引擎面的整理两席(协议席 + 模型席)在场 **iff** `MEMORY_CONSOLIDATION_DRIVER=on`
|
|
1154
|
+
> 且记忆引擎接了线 —— 两席与那两个 operator 口**共用同一只已解析座**(不做第二份装配)。
|
|
1155
|
+
> · 阀门关着 ⇒ 两席都不写键,引擎那半场**逐字节不变**;此时 `MEMORY_AUTO_CONSOLIDATION=on`
|
|
1156
|
+
> **启动期响亮拒**(core 对「武装但没接整理席」是**每一次 prepare 都抛** `config.auto_consolidation`,
|
|
1157
|
+
> 留到运行期就是一台**启动成功、每个任务都死**的机器)。
|
|
1158
|
+
> · 阀门开着 ⇒ 两席齐 ⇒ 缺省(开)当场生效。
|
|
1159
|
+
> 🔴🔴 **升级影响**:**升级前就配了 `MEMORY_CONSOLIDATION_DRIVER=on` 的部署,升到本版之后每个产生了
|
|
1160
|
+
> 整理建议的任务之后会自动跑一轮整库整理**(升级前那是只能由人按的手动阀门)。不想要就**升级前**显式写
|
|
1161
|
+
> `MEMORY_AUTO_CONSOLIDATION=off` —— **不配不等于关**。启动日志 `memory_consolidation_driver_armed`
|
|
1162
|
+
> 带一位 `autoRunOnRecommendation`(三态原样),用它核这台机器是哪一态。
|
|
1163
|
+
> **两根的分工**:上一根决定**有没有**整理能力,这一根决定那个能力**要不要自动触发**;关掉自动整理,
|
|
1164
|
+
> 手动阀门照常可用。
|
|
1165
|
+
> 未武装的部署,`wiring_manifest` 与它的 `configFingerprint` 对 7.20.1 **逐字节相同**;武装的部署,新段
|
|
1166
|
+
> `autoConsolidation: { onRecommendation: true }` **只出现在 operator 帧上**(契约附录 G.13)。
|
|
1131
1167
|
> - `MEMORY_CONSOLIDATION_SCOPES=user:local[,org:acme]`:阀门的 scope 表 = 这台 worker **声明**它会折叠哪些库。
|
|
1132
1168
|
> **恰好一条**时它是两个口省略 `scope` 的唯一缺省;零条或多条时省略 `scope` 一律 400(在两个库之间替人挑
|
|
1133
1169
|
> 一个去花全库模型钱是本设计要消灭的形)。**表非空时显式 scope 必须在表里**,否则 400 —— 一个陈旧或敲错
|
|
@@ -6,18 +6,24 @@
|
|
|
6
6
|
* 崩溃安全机制照 `orchestration/workflow-notify-journal.ts` 的 File 形:append-only JSONL、fsync 逐行、
|
|
7
7
|
* 重放容忍撕尾行)。
|
|
8
8
|
*
|
|
9
|
-
* ## 它治的病([ref] / [ref]①
|
|
10
|
-
* local 车道 ask 账进程内易失(`InMemoryApprovalAskStore`),活卡窗内崩溃 = **零痕迹**:重启后
|
|
11
|
-
* `/v1/approvals`
|
|
9
|
+
* ## 它治的病([ref] / [ref]① 车定界钉;⚠️ **病因描述是 [ref] 当时的事实,S-384 起已变**)
|
|
10
|
+
* 当时:local 车道 ask 账进程内易失(`InMemoryApprovalAskStore`),活卡窗内崩溃 = **零痕迹**:重启后
|
|
11
|
+
* `/v1/approvals` 两数组空,那次审批连一条审计行都没有。**S-384 起 local 的 ask 账已是持久的**
|
|
12
|
+
* (`FileApprovalAskStore`),所以「零痕迹」那句不再成立;本店今天还在的理由收窄成它**独有**的那一格:
|
|
13
|
+
* `crashConverged` 这个 local-only 的 additive 读面(孤儿行的 `orphanState` / `resumeSafe` 两位),
|
|
14
|
+
* ask 账本身没有「boot 收敛孤儿」这条动词。两本同根账并存是已登记的抽象欠账。本店把 ask **铸造 / 决议 / 腿闭**三类事件
|
|
12
15
|
* append 进磁盘账本(挂线点 = `ToolApprovalCoordinator`,见 `askAudit` ctor opt),boot 时收敛器把
|
|
13
16
|
* 孤儿行标 `crashed_before_park` 终态成因注,`GET /v1/approvals` 的 additive 键 `crashConverged` 供
|
|
14
17
|
* 操作员追溯。
|
|
15
18
|
*
|
|
16
19
|
* ## 它**不**做的事(裁 (c) 的边界,与稿 §1 (a) 臂的语义论证同源)
|
|
17
|
-
* - **不翻 `streamApproval`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* - **不翻 `streamApproval` 能力位**([ref] 当时的裁 (c);⚠️ **这一位已由 S-384 翻真,不是被本店翻的**):
|
|
21
|
+
* 当年不翻的理由是「该位在 SQL 车道隐含 durable park 兜底,local 给不了」+「local 的 ask 账是进程内
|
|
22
|
+
* 易失形」。后半条已由 `FileApprovalAskStore` 消掉(持久账落地);前半条的答案一直在谓词里 ——
|
|
23
|
+
* `resolveStreamApprovalGate` 的 `parkFacility` 合取项要求 `backend.checkpoint` 在场 ∧ `DURABLE_APPROVAL`,
|
|
24
|
+
* 所以位报 true 的 local 部署**必然**配齐了 File 形 checkpoint 店(`durable-approval-local-e2e` 第一格
|
|
25
|
+
* 实证:park 行跨双 kill 存活、重启后 decide 驱动 resume 真跑完)。本店与那一位自此无关:它治的是
|
|
26
|
+
* **活卡窗内崩溃**(park 还没落盘)那一格的零痕迹病,而那一格无论位真位假都不复活 run。
|
|
21
27
|
* - **不复活 run**:审计行不是 park 行,崩掉的 ask 不进 pending/livePending。恢复闭环真形 =
|
|
22
28
|
* core resume 既有补偿腿(悬空 tool_use 闭合「中断,工具未执行」⇒ 模型自然重发重弹卡),读面用
|
|
23
29
|
* `resumeSafe` 位指路(v1.1 §6 第二件)。
|
|
@@ -138,6 +138,44 @@ export declare function buildStopDetailAudit(input: {
|
|
|
138
138
|
outcome: string;
|
|
139
139
|
untrustedDetail: string;
|
|
140
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* S-362 / S-399 —— **`RunnerDeps.autoRunOnRecommendation` 武装席的装配不变量**(D1 的姊妹门)。
|
|
143
|
+
*
|
|
144
|
+
* 与 {@link assertConsolidationDriverWirable} 刻意**分家**而不是加第四臂:那道门问「阀门这根旋钮
|
|
145
|
+
* (`MEMORY_CONSOLIDATION_DRIVER`)开着而这台机器结构上跑不了」,本门问「武装这根旋钮
|
|
146
|
+
* (`MEMORY_AUTO_CONSOLIDATION`)开着而引擎那半场没有整理席」。两句诊断指向的动作不同,合成一道门就会
|
|
147
|
+
* 指错方向。
|
|
148
|
+
*
|
|
149
|
+
* ## 为什么是**拒启**而不是让 core 在 prepare 抛
|
|
150
|
+
*
|
|
151
|
+
* core 对「`autoRunOnRecommendation: true` 但没接整理席」的处置是**每一次 prepare 都抛**
|
|
152
|
+
* `config.auto_consolidation`(带码错,不是通知;逐字见 core `RunnerDeps.autoRunOnRecommendation` 的 d.ts)。
|
|
153
|
+
* 留到运行期买到的是一台**启动成功、每一个任务都死**的 worker —— 比阀门那三条(「启动成功、什么都没
|
|
154
|
+
* 折叠」)更坏。所以按 D1 的同一条(**安全/成本控件不得半开**)提前到启动期,并把**该去拧哪根**写进句子。
|
|
155
|
+
*
|
|
156
|
+
* ## 判据取的是**结构事实**,不是一个手抄的布尔
|
|
157
|
+
*
|
|
158
|
+
* `memoryConsolidationWired` 由调用点从**它刚刚铸出来的那只 `RunnerDeps`** 上读(`deps.memoryConsolidation
|
|
159
|
+
* !== undefined`),不是在这里写一个常量 —— S-399 把两席接上之后,本门**自动**从「恒拒」变成「阀门开着
|
|
160
|
+
* 就放行」,没有人需要记得回来改一个数。
|
|
161
|
+
*
|
|
162
|
+
* 🔴 **core 的拒绝谓词是两个析取项,本门只覆盖第一个**(对抗复审 F5 [low],验真后如实登记):
|
|
163
|
+
* core d.ts 逐字「`true` with nothing to run (**no `RunnerDeps.memoryConsolidation`, or no resolvable
|
|
164
|
+
* driver model seat**), or a non-boolean, is refused at prepare」。第二项(模型席解不出)本门不判,
|
|
165
|
+
* 而这在本仓**结构上不可达**:两席是**同一只已解析座**一起写上去的(`boot/runner-deps.ts` 的
|
|
166
|
+
* `ctx.consolidationSeat` 那一段)——座解析不出来时 {@link resolveConsolidationDriverSeat} 早在
|
|
167
|
+
* D1 那道门上就**拒启**了,轮不到这里。哪天有人把两席拆开各走各的路,这条「不可达」就失效,那时
|
|
168
|
+
* 本门要补第二项(core 侧的判据是 `consolidationSeatResolvable`,**不从包根导出**,届时要么向 core
|
|
169
|
+
* 要导出,要么在本层复现 —— 这一句留在这里就是为了那一天不必重新推一遍)。
|
|
170
|
+
*
|
|
171
|
+
* 未武装(`undefined` / `false`)⇒ 整条门不判。
|
|
172
|
+
*/
|
|
173
|
+
export declare function assertAutoConsolidationWirable(input: {
|
|
174
|
+
/** 部署席的三态读数(`config.memoryConsolidationDriver.autoRunOnRecommendation`)。 */
|
|
175
|
+
armed: boolean | undefined;
|
|
176
|
+
/** 铸好的 `RunnerDeps` 上**真的**有整理席吗(`deps.memoryConsolidation !== undefined`)。 */
|
|
177
|
+
memoryConsolidationWired: boolean;
|
|
178
|
+
}): void;
|
|
141
179
|
/** 两个 admin 口消费的窄能力面(`ServiceDeps.memoryConsolidation` 的实参来源)。 */
|
|
142
180
|
export interface MemoryConsolidationFaces {
|
|
143
181
|
/** D3 的座位自证(投影用;boot 期解析一次,restart-to-apply)。 */
|
|
@@ -107,6 +107,20 @@ export function buildStopDetailAudit(input) {
|
|
|
107
107
|
untrustedDetail: redactSecrets(input.stopDetail).slice(0, STOP_DETAIL_LOG_MAX),
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
export function assertAutoConsolidationWirable(input) {
|
|
111
|
+
if (input.armed !== true)
|
|
112
|
+
return;
|
|
113
|
+
if (input.memoryConsolidationWired)
|
|
114
|
+
return;
|
|
115
|
+
throw new Error(`MEMORY_AUTO_CONSOLIDATION=on arms automatic memory consolidation (a whole-library fold after every task that ` +
|
|
116
|
+
`recommended one — memory content EGRESSES to the configured consolidation model), but this deployment wires NO ` +
|
|
117
|
+
`engine-side consolidation seat (RunnerDeps.memoryConsolidation). The engine refuses that combination at EVERY ` +
|
|
118
|
+
`prepare (config.auto_consolidation), so this worker would boot fine and then fail every single task. ` +
|
|
119
|
+
`The engine-side seats are wired IFF MEMORY_CONSOLIDATION_DRIVER=on and a memory engine is wired — so either ` +
|
|
120
|
+
`set MEMORY_CONSOLIDATION_DRIVER=on (which also mounts the manual valve, and note that BOTH knobs then bill ` +
|
|
121
|
+
`whole-library model calls), or set MEMORY_AUTO_CONSOLIDATION=off (unsetting it is NOT the same: the engine's ` +
|
|
122
|
+
`named default is ON as of core 7.21.1).`);
|
|
123
|
+
}
|
|
110
124
|
export function createMemoryConsolidationFaces(store, input) {
|
|
111
125
|
const engine = new MemoryEngine({
|
|
112
126
|
backend: store.backend,
|
package/dist/boot/reapers.js
CHANGED
|
@@ -110,10 +110,9 @@ export function startReapers(ctx) {
|
|
|
110
110
|
const approvalReconciler = (() => {
|
|
111
111
|
if (!config.streamApproval.enabled || !backend)
|
|
112
112
|
return undefined;
|
|
113
|
-
const cpPort = checkpointStore;
|
|
114
113
|
return createApprovalReconciler({
|
|
115
114
|
askStore: backend.approvalAsk(),
|
|
116
|
-
|
|
115
|
+
...(checkpointStore !== undefined ? { checkpoints: checkpointStore } : {}),
|
|
117
116
|
runs: runStore,
|
|
118
117
|
logger,
|
|
119
118
|
metrics,
|
|
@@ -17,6 +17,7 @@ import type { ElicitationCoordinator } from "../elicitation.js";
|
|
|
17
17
|
import { FleetEventBus } from "../fleet/fleet-bus.js";
|
|
18
18
|
import type { Logger } from "../observability/logger.js";
|
|
19
19
|
import type { Metrics } from "../observability/metrics.js";
|
|
20
|
+
import { type ConsolidationDriverSeat } from "./memory-consolidation.js";
|
|
20
21
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
21
22
|
import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
|
|
22
23
|
import { type EngineNoticeRouter } from "../trace/engine-notice-wire.js";
|
|
@@ -148,6 +149,16 @@ export interface RunnerDepsCtx {
|
|
|
148
149
|
backend: MemoryBackend;
|
|
149
150
|
root: string;
|
|
150
151
|
} | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* S-399(core 7.21.1)—— 记忆整理的**已解析座**(`boot/memory-consolidation.ts` 的
|
|
154
|
+
* `resolveConsolidationDriverSeat` 产物),在场 **iff** `MEMORY_CONSOLIDATION_DRIVER=on` 且记忆引擎接了线。
|
|
155
|
+
*
|
|
156
|
+
* 🔴 **一座两用,不是两次装配**:同一只座同时喂 admin 阀门的操作面引擎与这里的引擎面两席
|
|
157
|
+
* (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)。第二次解析会让「谁在花钱」
|
|
158
|
+
* 在两条腿上各答一遍,而座是 boot 期定的(restart-to-apply,理由在该模块顶注)。
|
|
159
|
+
* 缺席 ⇒ 两席都**不写键** ⇒ 引擎那半场对 7.20.x 逐字节不变。
|
|
160
|
+
*/
|
|
161
|
+
consolidationSeat: ConsolidationDriverSeat | undefined;
|
|
151
162
|
/** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2)/ S-215:会话 capture opt-out 记录的**按平面取用源**
|
|
152
163
|
* (`backend.sessionCaptureRecords()`,main.ts 取**一次**、与四只 operator 面引擎同一只源 —— 全站同批
|
|
153
164
|
* 注入律,机器钉 = test/memory-capture-optout-lane.test.ts ②)。在场 ⇒ `capture:"off"` 声明与中途翻转
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
|
|
4
4
|
import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
|
|
5
5
|
import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
|
|
6
|
+
import { assertAutoConsolidationWirable } from "./memory-consolidation.js";
|
|
6
7
|
import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
|
|
7
8
|
import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
|
|
8
9
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
@@ -274,9 +275,23 @@ export function createRunnerDeps(ctx) {
|
|
|
274
275
|
});
|
|
275
276
|
return;
|
|
276
277
|
}
|
|
278
|
+
metrics.inc("runner_error_total", { phase: ctx.phase });
|
|
277
279
|
logger.error("runner_error", { phase: ctx.phase, sessionId: ctx.sessionId, err: String(err) });
|
|
278
280
|
},
|
|
281
|
+
...(ctx.consolidationSeat !== undefined
|
|
282
|
+
? {
|
|
283
|
+
memoryConsolidation: {},
|
|
284
|
+
memoryConsolidationDriver: { chat: ctx.consolidationSeat.options.chat, model: ctx.consolidationSeat.options.model },
|
|
285
|
+
}
|
|
286
|
+
: {}),
|
|
287
|
+
...(config.memoryConsolidationDriver.autoRunOnRecommendation !== undefined
|
|
288
|
+
? { autoRunOnRecommendation: config.memoryConsolidationDriver.autoRunOnRecommendation }
|
|
289
|
+
: {}),
|
|
279
290
|
};
|
|
291
|
+
assertAutoConsolidationWirable({
|
|
292
|
+
armed: config.memoryConsolidationDriver.autoRunOnRecommendation,
|
|
293
|
+
memoryConsolidationWired: runnerDeps.memoryConsolidation !== undefined,
|
|
294
|
+
});
|
|
280
295
|
return runnerDeps;
|
|
281
296
|
}
|
|
282
297
|
//# sourceMappingURL=runner-deps.js.map
|
|
@@ -62,15 +62,18 @@ export function runStage06(ctx) {
|
|
|
62
62
|
memoryEngineBackend: config.memoryEngineBackend,
|
|
63
63
|
...(config.memoryProvenance !== undefined ? { provenance: config.memoryProvenance } : {}),
|
|
64
64
|
});
|
|
65
|
-
const
|
|
65
|
+
const consolidationSeat = config.memoryConsolidationDriver.enabled && memoryEngine
|
|
66
|
+
? resolveConsolidationDriverSeat({
|
|
67
|
+
brain,
|
|
68
|
+
models: config.models,
|
|
69
|
+
tiers: config.tiers,
|
|
70
|
+
roles: config.roles,
|
|
71
|
+
getApiKeyAndHeaders: createPerModelAuthSeat(() => configCenter.getKeyResolver()),
|
|
72
|
+
})
|
|
73
|
+
: undefined;
|
|
74
|
+
const memoryConsolidationFaces = consolidationSeat && memoryEngine
|
|
66
75
|
? createMemoryConsolidationFaces(memoryEngine, {
|
|
67
|
-
seat:
|
|
68
|
-
brain,
|
|
69
|
-
models: config.models,
|
|
70
|
-
tiers: config.tiers,
|
|
71
|
-
roles: config.roles,
|
|
72
|
-
getApiKeyAndHeaders: createPerModelAuthSeat(() => configCenter.getKeyResolver()),
|
|
73
|
-
}),
|
|
76
|
+
seat: consolidationSeat,
|
|
74
77
|
scopes: config.memoryConsolidationDriver.scopes,
|
|
75
78
|
...(config.memoryProvenance !== undefined ? { provenance: config.memoryProvenance } : {}),
|
|
76
79
|
onIncident: (err) => logger.warn("memory_consolidation_incident", { code: err.code ?? null, detail: err.message }),
|
|
@@ -92,6 +95,7 @@ export function runStage06(ctx) {
|
|
|
92
95
|
model: memoryConsolidationFaces.model,
|
|
93
96
|
scopes: memoryConsolidationFaces.scopes,
|
|
94
97
|
periodic: false,
|
|
98
|
+
autoRunOnRecommendation: config.memoryConsolidationDriver.autoRunOnRecommendation ?? "engine-default(on)",
|
|
95
99
|
});
|
|
96
100
|
}
|
|
97
101
|
const governanceSeams = createGovernanceSeams({
|
|
@@ -108,6 +112,7 @@ export function runStage06(ctx) {
|
|
|
108
112
|
const workflowModelAllowlistSnapshot = workflowModelAllowlistFor(config) ?? [];
|
|
109
113
|
const sessionShellGateRegistry = createSessionShellGateRegistry();
|
|
110
114
|
const runnerDeps = createRunnerDeps({
|
|
115
|
+
consolidationSeat,
|
|
111
116
|
hands: commitHands,
|
|
112
117
|
mcpRevocations,
|
|
113
118
|
sessionShellGateExplicitlyOff: sessionShellGateRegistry.explicitlyOff,
|
package/dist/config-catalog.js
CHANGED
|
@@ -213,6 +213,7 @@ export const CONFIG_CATALOG = [
|
|
|
213
213
|
r("MCP_ELICITATION_MAX_TOTAL", "approval", "number", "elicitation 每 run 总量帽", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
|
|
214
214
|
r("MCP_ELICITATION_MIN_INTERVAL_MS", "approval", "number", "elicitation per-server 最小间隔(ms)", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
|
|
215
215
|
r("MCP_ELICITATION_TTL_MS", "approval", "number", "elicitation 无人应答释放窗(ms)", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
|
|
216
|
+
r("MEMORY_AUTO_CONSOLIDATION", "memory", "enum", "记忆自动整理部署席(on=每个建议任务后整库整理跑,记忆内容自动外流到整理模型;off=显式关,胜过引擎缺省;未设=读引擎具名缺省)", { enumValues: ["on", "off"], derivedDefaultNote: "缺席 ⇒ 引擎具名缺省(**本版装的引擎是开**)——不配不等于关;前置=MEMORY_CONSOLIDATION_DRIVER=on", danger: SEC, resolve: (c) => c.memoryConsolidationDriver.autoRunOnRecommendation, booleanEnum: { true: "on", false: "off" } }),
|
|
216
217
|
r("MEMORY_CAPTURE_POLICY", "memory", "enum", "记忆 capture 姿态(open=声明按面值;governed=按 per-principal verdict;capture-required=恒采)", { enumValues: ["open", "governed", "capture-required"], staticDefault: "open", danger: SEC, configKey: "memoryCapturePolicy" }),
|
|
217
218
|
r("MEMORY_CONSOLIDATION_DRIVER", "memory", "enum", "记忆折叠阀门(off=零装配零探针,两 admin 口 404 族)", { enumValues: ["on", "off"], staticDefault: "off", danger: BILL, resolve: (c) => c.memoryConsolidationDriver.enabled, booleanEnum: { true: "on", false: "off" } }),
|
|
218
219
|
r("MEMORY_CONSOLIDATION_INTERVAL_SEC", "memory", "number", "折叠周期腿节律(秒;本 build 任何正值拒启 —— 周期腿未落地,恒 0)", { staticDefault: "0" }),
|
package/dist/config-types.d.ts
CHANGED
|
@@ -140,6 +140,32 @@ export interface MemoryConsolidationDriverConfig {
|
|
|
140
140
|
* `src/memory-operator-faces.ts` 的 `erasureEnvelopeRefusal` 头注),在本层拿它当白名单就是**发明
|
|
141
141
|
* 第二套授权语义**。显式传的 scope 照跑,表只管缺省。 */
|
|
142
142
|
scopes: readonly string[];
|
|
143
|
+
/**
|
|
144
|
+
* S-362 / core 7.21.0 [ref] —— **`RunnerDeps.autoRunOnRecommendation` 的部署席**
|
|
145
|
+
* (env `MEMORY_AUTO_CONSOLIDATION=on|off`;**缺席 = 不写那个键**,由 core 的具名缺省
|
|
146
|
+
* `AUTO_RUN_ON_RECOMMENDATION_DEFAULT` 作答)。
|
|
147
|
+
*
|
|
148
|
+
* 🔴 **三态,不是布尔**,而且三态各有各的意思 —— 折成布尔会当场丢掉本旋钮存在的**唯一**理由:
|
|
149
|
+
* · `undefined`(env 未设)= 不写 `RunnerDeps` 那个键 ⇒ 读 core 的具名缺省。core 7.21.0 发的是
|
|
150
|
+
* `false`,而 clay [ref] 已裁**缺省开**、core 7.21.1 翻常量 —— 所以这一格的取值**会随提货漂**,
|
|
151
|
+
* 这正是它该有的样子(缺省的属主在 core)。
|
|
152
|
+
* · `false`(`MEMORY_AUTO_CONSOLIDATION=off`)= **显式关,胜过缺省**。这是本旋钮的**关闭口**:
|
|
153
|
+
* 一台不愿意让记忆内容自动外流的部署,靠它在 core 把缺省翻开之后仍然保持关闭。
|
|
154
|
+
* · `true`(`=on`)= 武装。**武装是一条出口决定,不是性能旋钮**:每个建议任务之后会有一次整库整理跑,
|
|
155
|
+
* 记忆内容自动外流到配置的整理模型 —— 设它之前请读 `boot/memory-consolidation.ts` 的 EGRESS 段。
|
|
156
|
+
*
|
|
157
|
+
* ⚠️ **前置 = `MEMORY_CONSOLIDATION_DRIVER=on`**(且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席
|
|
158
|
+
* (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)由**同一只已解析座**接上
|
|
159
|
+
* (S-399,`boot/runner-deps.ts` 的 `ctx.consolidationSeat` 那一段)⇒ 本旋钮真的生效;阀门关着 ⇒ 两席
|
|
160
|
+
* 一个键都不写,而 core 对「武装但没接整理席」的处置是**每一次 prepare 都抛** `config.auto_consolidation`
|
|
161
|
+
* —— 那会买到一台**启动成功、每个任务都死**的机器,所以本仓把那一拒提前到启动期响亮拒
|
|
162
|
+
* (`assertAutoConsolidationWirable`,读的是刚铸出来的那只 deps 而不是一个手抄的布尔),与
|
|
163
|
+
* `MEMORY_CONSOLIDATION_DRIVER` 的 D1「安全/成本控件不得半开」同一条。
|
|
164
|
+
* 🔴 **本段自 S-399 改判**:S-362 那一批这里写的是「本 build 上 `on` 一定拒启(本仓没接整理席)」——
|
|
165
|
+
* 同一批的后半场把两席接上之后,那句话成了谎。旧世界断言留在树上比欠账更危险(7.81.0 整批的教训),
|
|
166
|
+
* 所以真相变了就地改,不留「将来某一版」的口气。
|
|
167
|
+
*/
|
|
168
|
+
autoRunOnRecommendation?: boolean;
|
|
143
169
|
}
|
|
144
170
|
/** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
|
|
145
171
|
* scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
|
package/dist/config.js
CHANGED
|
@@ -1349,6 +1349,14 @@ function parseMemoryConsolidationDriver() {
|
|
|
1349
1349
|
`spellings to preserve, unlike MEMORY_ENGINE's two-family word table.`);
|
|
1350
1350
|
}
|
|
1351
1351
|
const enabled = word === "on";
|
|
1352
|
+
const autoWord = requireNonBlank("MEMORY_AUTO_CONSOLIDATION", process.env.MEMORY_AUTO_CONSOLIDATION);
|
|
1353
|
+
if (autoWord !== undefined && autoWord !== "on" && autoWord !== "off") {
|
|
1354
|
+
throw new Error(`MEMORY_AUTO_CONSOLIDATION must be exactly "on" or "off", got ${JSON.stringify(autoWord)} — this knob decides whether memory content ` +
|
|
1355
|
+
`EGRESSES to the consolidation model automatically after every recommending task. A misspelled word read as "unset" would hand the ` +
|
|
1356
|
+
`decision back to the engine's named default (which core is flipping to ON), so an operator who believes they turned it off would be ` +
|
|
1357
|
+
`exporting their users' memory. UNSET it to take the engine default, or spell it exactly.`);
|
|
1358
|
+
}
|
|
1359
|
+
const autoRunOnRecommendation = autoWord === undefined ? undefined : autoWord === "on";
|
|
1352
1360
|
let intervalSec = 0;
|
|
1353
1361
|
const intervalRaw = requireNonBlank("MEMORY_CONSOLIDATION_INTERVAL_SEC", process.env.MEMORY_CONSOLIDATION_INTERVAL_SEC);
|
|
1354
1362
|
if (intervalRaw !== undefined) {
|
|
@@ -1377,7 +1385,7 @@ function parseMemoryConsolidationDriver() {
|
|
|
1377
1385
|
`the consolidation valve is OFF, so neither knob reaches anything (the two admin endpoints are not even mounted). ` +
|
|
1378
1386
|
`Set MEMORY_CONSOLIDATION_DRIVER=on, or unset the other two.`);
|
|
1379
1387
|
}
|
|
1380
|
-
return { enabled, intervalSec, scopes };
|
|
1388
|
+
return { enabled, intervalSec, scopes, ...(autoRunOnRecommendation !== undefined ? { autoRunOnRecommendation } : {}) };
|
|
1381
1389
|
}
|
|
1382
1390
|
function parseMemoryDomain(ctx) {
|
|
1383
1391
|
const { requirePrincipal } = ctx;
|
|
@@ -14,8 +14,9 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
14
14
|
import type { WiringManifest } from "@sema-agent/core";
|
|
15
15
|
import { type StreamApprovalGate, type StreamApprovalGateInput } from "../../tool-approval.js";
|
|
16
16
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
17
|
-
/** 流内审批门的读数形:上场即 `"active"
|
|
18
|
-
* (与 `resolveStreamApprovalGate`
|
|
17
|
+
/** 流内审批门的读数形:上场即 `"active"`,否则是那**四**个合取项里**第一个**不满足的原因词
|
|
18
|
+
* (与 `resolveStreamApprovalGate` 同一闭集,词表**取型**自它 ⇒ 增删 reason 在此处自动跟随/编译红;
|
|
19
|
+
* S-384 删掉第 5 项「账必须持久」时本行数字漏改过一次,**数字是手抄的那一半**)。 */
|
|
19
20
|
export type StreamApprovalGateReading = "active" | Extract<StreamApprovalGate, {
|
|
20
21
|
active: false;
|
|
21
22
|
}>["reason"];
|
|
@@ -9,6 +9,14 @@ export interface FailOpenTagEntry {
|
|
|
9
9
|
}
|
|
10
10
|
/** 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。 */
|
|
11
11
|
export declare const FAIL_OPEN_TAGS: {
|
|
12
|
+
readonly "server.approval-ask-journal.compaction-failed": {
|
|
13
|
+
readonly cls: "F";
|
|
14
|
+
readonly note: "S-384(`plugins/approval-ask-store-file.ts` 的 `write()` 尾巴,local 车道的 File 形 ask 账):账本**压实**(整本重写成行快照)失败 —— 盘满 / 只读挂载 / `.tmp` 不可写。压实是**纯管家动作**,走到它的时候这一次写**已经 append+fsync 落盘、也已经翻进内存** ⇒ 把它的 I/O 错误抛给调用方 = 把一次已生效的人类批准报成失败(客户端重试会拿到 `ask_not_pending`),正是本店存在的理由被反过来用。所以吞 + 留痕:账本继续长(core 的 `AppendLog` 在 `closeForSwap` 之后惰性重开,一次瞬时错误不会把它变成砖),下一次写再试压实。放行的最坏后果 = 账本不再收缩(盘占用按写入量线性增长),**语义零影响**(行与转移一个不差)。计数非零 = 这台机器的数据根写不动了,运维要去看盘。";
|
|
15
|
+
};
|
|
16
|
+
readonly "server.approval-ask-journal.corrupt-record-skipped": {
|
|
17
|
+
readonly cls: "F";
|
|
18
|
+
readonly note: "S-384(`plugins/approval-ask-store-file.ts` 的启动重放,local 车道的 File 形 ask 账):账本**中间**有一行解析不出(位腐 / 被人手改 / 文件系统故障)⇒ core `readJsonlRecords` 跳过它,本店继续重放其余记录。**撕尾不走本 tag**(崩在半行是这个格式存在的理由,由 quarantine + warn 单独处置),**词表外动词也不走**(那是降级,直接拒启)。放行的最坏后果之所以有界,靠的是店的 CAS 形:每一条写都带 `WHERE state = <from>` 谓词 ⇒ 丢一条记录只能让**后续转移失败**(no-op),绝不可能让一条非法转移成功 —— 一条已决 ask 退回 STREAM_PENDING 的方向是 fail-closed(工具不会因此被放行),一条待决 ask 消失的方向是「等卡的那条腿窗到期走 park」= S-384 之前的行为。所以这条兜底丢的是**进度**,不是门。同批还有 quarantine 原件 + 一条点名路径的 warn;计数让「某台机器的 ask 账一直在腐」与「这台机器本来就没有审批」在遥测上分得开。";
|
|
19
|
+
};
|
|
12
20
|
readonly "server.terminal.persisted-plane-malformed": {
|
|
13
21
|
readonly cls: "F";
|
|
14
22
|
readonly note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。";
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { appendFileSync } from "node:fs";
|
|
2
2
|
export const FAIL_OPEN_TAGS = {
|
|
3
|
+
"server.approval-ask-journal.compaction-failed": {
|
|
4
|
+
cls: "F",
|
|
5
|
+
note: "S-384(`plugins/approval-ask-store-file.ts` 的 `write()` 尾巴,local 车道的 File 形 ask 账):账本**压实**(整本重写成行快照)失败 —— 盘满 / 只读挂载 / `.tmp` 不可写。压实是**纯管家动作**,走到它的时候这一次写**已经 append+fsync 落盘、也已经翻进内存** ⇒ 把它的 I/O 错误抛给调用方 = 把一次已生效的人类批准报成失败(客户端重试会拿到 `ask_not_pending`),正是本店存在的理由被反过来用。所以吞 + 留痕:账本继续长(core 的 `AppendLog` 在 `closeForSwap` 之后惰性重开,一次瞬时错误不会把它变成砖),下一次写再试压实。放行的最坏后果 = 账本不再收缩(盘占用按写入量线性增长),**语义零影响**(行与转移一个不差)。计数非零 = 这台机器的数据根写不动了,运维要去看盘。",
|
|
6
|
+
},
|
|
7
|
+
"server.approval-ask-journal.corrupt-record-skipped": {
|
|
8
|
+
cls: "F",
|
|
9
|
+
note: "S-384(`plugins/approval-ask-store-file.ts` 的启动重放,local 车道的 File 形 ask 账):账本**中间**有一行解析不出(位腐 / 被人手改 / 文件系统故障)⇒ core `readJsonlRecords` 跳过它,本店继续重放其余记录。**撕尾不走本 tag**(崩在半行是这个格式存在的理由,由 quarantine + warn 单独处置),**词表外动词也不走**(那是降级,直接拒启)。放行的最坏后果之所以有界,靠的是店的 CAS 形:每一条写都带 `WHERE state = <from>` 谓词 ⇒ 丢一条记录只能让**后续转移失败**(no-op),绝不可能让一条非法转移成功 —— 一条已决 ask 退回 STREAM_PENDING 的方向是 fail-closed(工具不会因此被放行),一条待决 ask 消失的方向是「等卡的那条腿窗到期走 park」= S-384 之前的行为。所以这条兜底丢的是**进度**,不是门。同批还有 quarantine 原件 + 一条点名路径的 warn;计数让「某台机器的 ask 账一直在腐」与「这台机器本来就没有审批」在遥测上分得开。",
|
|
10
|
+
},
|
|
3
11
|
"server.terminal.persisted-plane-malformed": {
|
|
4
12
|
cls: "F",
|
|
5
13
|
note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。",
|
|
@@ -229,6 +229,7 @@ export function createMetrics() {
|
|
|
229
229
|
m.histogram("plan_cache_qsig", "Significant-token count of task objectives (qSig length)", [1, 2, 3, 5, 10, 20]);
|
|
230
230
|
m.counter("memory_consolidation_ops_total", "Memory consolidation store ops (1.62 B-full), by op (update/delete)");
|
|
231
231
|
m.counter("mcp_server_unavailable_total", "MCP servers skipped fail-open at task start (1.68), unreachable/misconfigured");
|
|
232
|
+
m.counter("runner_error_total", "RunnerDeps.onError frames that no phase arm claimed, by core phase (hook / compaction / interrupt-reconcile / suggestions)");
|
|
232
233
|
m.counter("a2a_peer_unavailable_total", "A2A peers skipped at task start (DESIGN-269 client leg): card unreachable / spec unusable");
|
|
233
234
|
m.counter("a2a_serve_rpc_total", "A2A server-as-peer JSON-RPC calls by method and outcome (DESIGN-269 车2)");
|
|
234
235
|
m.counter("a2a_serve_rejected_total", "A2A server-as-peer requests refused before reaching a method handler, by reason (DESIGN-269 车2)");
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { AskState } from "../approval-ask-machine.js";
|
|
2
|
+
import type { AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
|
|
3
|
+
/** 数据根下的目录名。**按店命名**,与兄弟 File 店同规(`approval-nonces` / `approval-ask-audit` /
|
|
4
|
+
* `approval-exemptions` / `resume-anchors` / `checkpoint-ctx` / …)—— 首版写的是族名 `approvals`,而那
|
|
5
|
+
* 正是读者会去找**持久 park 行**的地方(它们其实在 `checkpoint-ctx`),且持久数据面的名字按硬 breaking
|
|
6
|
+
* 三句是「不能靠编译红通知」的那一面,改名只在落地前廉价(替补对抗复审 F8,采纳)。 */
|
|
7
|
+
export declare const APPROVAL_ASK_DIR = "approval-asks";
|
|
8
|
+
/** 账本文件名。 */
|
|
9
|
+
export declare const APPROVAL_ASK_JOURNAL_FILE = "asks.jsonl";
|
|
10
|
+
/**
|
|
11
|
+
* 压实的**下界**:记录数不到这个数,压实的 I/O 不值得付(整本重写 + fsync)。
|
|
12
|
+
* 与 {@link ASK_JOURNAL_COMPACT_GROWTH} 一起构成闭形判据,零 env、零启发式。
|
|
13
|
+
*/
|
|
14
|
+
export declare const ASK_JOURNAL_COMPACT_MIN_RECORDS = 2000;
|
|
15
|
+
/**
|
|
16
|
+
* 压实的**增长因子**:记录数必须同时超过「活行数 × 本因子」才压实。
|
|
17
|
+
* 少了它就是抖动 —— 一个稳定在 1_800 行的部署会在每条记录之后都把整本重写一遍。
|
|
18
|
+
*/
|
|
19
|
+
export declare const ASK_JOURNAL_COMPACT_GROWTH = 2;
|
|
20
|
+
/**
|
|
21
|
+
* 账本的**动词闭集**([ref] 形)。运行期的词表判定与编译期的穷尽 `switch` 读同一份:
|
|
22
|
+
* 往这里加一个词,{@link FileApprovalAskStore.replayOp} 的 switch 立刻编译红(漏一口不可能发布)。
|
|
23
|
+
*/
|
|
24
|
+
declare const JOURNAL_OPS: readonly ["ensureAsk", "transitionAsk", "decideAsk", "claimTerminal", "expireAsk", "bindBatch", "abortBatch", "deferReconcile", "resolveProvisional", "deleteByTask"];
|
|
25
|
+
type JournalOpWord = (typeof JOURNAL_OPS)[number];
|
|
26
|
+
/** 运行期词表(上面的数组)与类型面动词维({@link JournalCall})的**双向**对表 —— 任一边加词而另一边
|
|
27
|
+
* 没跟上,这个别名解析成 `never`,{@link JOURNAL_OP_TABLE_GATE} 的赋值当场编译红。 */
|
|
28
|
+
type JournalOpTableIsExhaustive = (JournalCall["op"] extends JournalOpWord ? (JournalOpWord extends JournalCall["op"] ? true : false) : false) extends true ? true : never;
|
|
29
|
+
/** 上面那条对表的**执法位**(占位常量形,`core-keyset-guard.ts` 的占位元组同精神):运行期无用,
|
|
30
|
+
* 存在的唯一理由是让「词表与类型面分家」在编译期就红,而不是等到某条记录重放不出来。 */
|
|
31
|
+
export declare const JOURNAL_OP_TABLE_GATE: JournalOpTableIsExhaustive;
|
|
32
|
+
/** 一次写调用的实参(动词维判别式)——盘上记的就是它,重放时原样喂回内核。 */
|
|
33
|
+
type JournalCall = {
|
|
34
|
+
op: "ensureAsk";
|
|
35
|
+
row: NewAskRow;
|
|
36
|
+
} | {
|
|
37
|
+
op: "transitionAsk";
|
|
38
|
+
askId: string;
|
|
39
|
+
from: AskState;
|
|
40
|
+
to: AskState;
|
|
41
|
+
patch: AskTransitionPatch;
|
|
42
|
+
} | {
|
|
43
|
+
op: "decideAsk";
|
|
44
|
+
askId: string;
|
|
45
|
+
batchId: string;
|
|
46
|
+
decision: DecideAskInput;
|
|
47
|
+
} | {
|
|
48
|
+
op: "claimTerminal";
|
|
49
|
+
askId: string;
|
|
50
|
+
batchId: string;
|
|
51
|
+
intent: AskTerminalClaimIntent;
|
|
52
|
+
} | {
|
|
53
|
+
op: "expireAsk";
|
|
54
|
+
askId: string;
|
|
55
|
+
batchId: string;
|
|
56
|
+
} | {
|
|
57
|
+
op: "bindBatch";
|
|
58
|
+
batchId: string;
|
|
59
|
+
askId: string;
|
|
60
|
+
gate: BindGateInput;
|
|
61
|
+
} | {
|
|
62
|
+
op: "abortBatch";
|
|
63
|
+
batchId: string;
|
|
64
|
+
} | {
|
|
65
|
+
op: "deferReconcile";
|
|
66
|
+
askId: string;
|
|
67
|
+
expectedState: AskState;
|
|
68
|
+
expectedRev: number;
|
|
69
|
+
nowMs: number;
|
|
70
|
+
} | {
|
|
71
|
+
op: "resolveProvisional";
|
|
72
|
+
askId: string;
|
|
73
|
+
from: AskState;
|
|
74
|
+
to: AskState;
|
|
75
|
+
patch: AskTransitionPatch;
|
|
76
|
+
} | {
|
|
77
|
+
op: "deleteByTask";
|
|
78
|
+
taskId: string;
|
|
79
|
+
};
|
|
80
|
+
export declare class FileApprovalAskStore implements ApprovalAskStore {
|
|
81
|
+
/** 语义内核(转移判定的唯一真源)。本类对它只做两件事:重放、代理。 */
|
|
82
|
+
private readonly core;
|
|
83
|
+
private readonly path;
|
|
84
|
+
private readonly tmpDir;
|
|
85
|
+
private log;
|
|
86
|
+
/** 活文件里的记录数(重放读数 + 每次 append,压实后归为快照行数)。压实判据读它。 */
|
|
87
|
+
private records;
|
|
88
|
+
/**
|
|
89
|
+
* 钉住的钟(见 {@link InMemoryApprovalAskStore} 顶注的读钟纪律)。非 null 的窗口**恒是同步的** ——
|
|
90
|
+
* 从 append 之后到内核方法返回 promise 为止,其间没有任何 `await` ⇒ 两次并发调用不可能互相看见对方的钉。
|
|
91
|
+
*/
|
|
92
|
+
private pinnedNow;
|
|
93
|
+
/** 重放期被内核拒掉的记录数(每条都已 warn;读面留给测试与将来的诊断)。 */
|
|
94
|
+
private replayRejectedCount;
|
|
95
|
+
/** 重放期被丢弃的坏行数(撕尾不计:那是本格式存在的理由,由 {@link corruptQuarantinePath} 单独说)。 */
|
|
96
|
+
private corruptLineCount;
|
|
97
|
+
/** 本次启动隔离出去的原件路径(无坏字节 ⇒ `undefined`)。 */
|
|
98
|
+
readonly corruptQuarantinePath: string | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* boot 期 fail-stop(File 店族同律,`approval-ask-audit-store.ts` 的同段先例):mkdir / 重放失败
|
|
101
|
+
* **拒启** —— 一台「审批账写不进却照常发卡」的机器,比起不起来危险得多。
|
|
102
|
+
*
|
|
103
|
+
* @param root local 数据根(= `LocalBackend` 的 `FileStorageBackend.root`,零新 env)。
|
|
104
|
+
*/
|
|
105
|
+
constructor(root: string);
|
|
106
|
+
/**
|
|
107
|
+
* 内核的钟。窗口外读 ⇒ **抛**(不静默回落 `Date.now()`):窗口外的读数按定义不在账本上,重放会得出
|
|
108
|
+
* 另一个时间戳 —— 那是一条**静默**的重放分岔,而本店的全部价值就是重放等价。今天内核的五个读钟点
|
|
109
|
+
* 全在同步前缀里;哪天有人挪到 `await` 之后,这里当场响。
|
|
110
|
+
*/
|
|
111
|
+
private clockRead;
|
|
112
|
+
private logFor;
|
|
113
|
+
/**
|
|
114
|
+
* 一次写:**先 append + fsync,再翻内存**(顶注的崩溃序)。append 与内核调用之间零 `await` ⇒
|
|
115
|
+
* 盘上的记录序 == 内存的转移序(内核的写体全在同步前缀里),两副读数永远对得上。
|
|
116
|
+
*
|
|
117
|
+
* 🔴 **内核吃的是「盘上那一份」,不是调用方那一份**(替补对抗复审 F7,采纳):整条调用先过一次 JSON
|
|
118
|
+
* round-trip,再**把 round-trip 的产物**同时交给账本与内核。不这么做的话,凡是 JSON round-trip 会改形
|
|
119
|
+
* 的值(`Date` → ISO 串、函数值键被**静默丢掉**、`undefined` 键消失)都会造出一条真分岔:活路径上内核
|
|
120
|
+
* 手里是原对象、重启重放之后是另一份 —— 而这正是本店整个形所依赖的那条等式。首版只为 `updatedInput`
|
|
121
|
+
* 一格做了这件事(它另有「编出来什么都不是 ⇒ 响亮拒」的三态判据),其余键(`cardJson` / `patch` /
|
|
122
|
+
* `gate` / `decisionActor`)留了一条没写出来的例外。一条规则覆盖所有键,例外消失。
|
|
123
|
+
* 代价如实:不可编码的值(BigInt / 循环引用)在本形上**抛**,而 InMemory 形收得下 —— 那与 SQL 孪生
|
|
124
|
+
* 同向(它也存不下),是**介质事实**,写进顶注的边界段。
|
|
125
|
+
*
|
|
126
|
+
* 🔴 **压实失败不许落到调用方头上**(替补对抗复审 F3 [medium],实测后修):走到 `maybeCompact()` 时这
|
|
127
|
+
* 一次写**已经 fsync 落盘、也已经翻进内存**,把一次纯管家动作的 I/O 错误抛给调用方 = 把一次已生效的
|
|
128
|
+
* 人类批准报成失败(客户端重试会拿到 `ask_not_pending`)。压实是 F 类兜底:失败留痕、下一次写再试。
|
|
129
|
+
*/
|
|
130
|
+
private write;
|
|
131
|
+
private maybeCompact;
|
|
132
|
+
/** 整本重写成行快照 + 原子替换。失败 ⇒ 原账本原样留着、`records` 不动(下一次写再试),日志由
|
|
133
|
+
* core 的 [ref] 惰性重开接住 —— 一次瞬时 I/O 错误绝不把这条日志变成砖。 */
|
|
134
|
+
private compact;
|
|
135
|
+
private replayRecord;
|
|
136
|
+
/** 闭集穷尽派发(漏一口 = 编译红,[ref] 形)。返回内核的 promise —— 结果本身在重放期无人消费。 */
|
|
137
|
+
private replayOp;
|
|
138
|
+
ensureAsk(row: NewAskRow): Promise<EnsureAskResult>;
|
|
139
|
+
transitionAsk(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
140
|
+
/**
|
|
141
|
+
* 🔴 `updatedInput` 的编码必须在 **append 之前**、且走内核**同一只** {@link encodeDecideUpdatedInput}:
|
|
142
|
+
* 账本本身是 JSON,而 `JSON.stringify` 对顶层函数是**静默丢键**不是抛 —— 不先编码就会写出一条
|
|
143
|
+
* 「没带编辑」的记录,而活路径那一侧是抛。编码抛 ⇒ 账本一个字节没写、内核一个字节没动(与内核自己的
|
|
144
|
+
* 「先编码、编码成功才一次性提交」同一条原子性)。
|
|
145
|
+
*/
|
|
146
|
+
decideAsk(askId: string, batchId: string, decision: DecideAskInput): Promise<DecideResult>;
|
|
147
|
+
claimTerminal(askId: string, batchId: string, intent: AskTerminalClaimIntent): Promise<AskTerminalClaimOutcome>;
|
|
148
|
+
expireAsk(askId: string, batchId: string): Promise<ExpireResult>;
|
|
149
|
+
bindBatch(batchId: string, askId: string, gate: BindGateInput): Promise<BindResult>;
|
|
150
|
+
abortBatch(batchId: string): Promise<string[]>;
|
|
151
|
+
deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
|
|
152
|
+
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
153
|
+
deleteByTask(taskId: string): Promise<void>;
|
|
154
|
+
listByState(state: AskState, limit: number): Promise<AskRow[]>;
|
|
155
|
+
listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
|
|
156
|
+
listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
|
|
157
|
+
getAsk(askId: string): Promise<AskRow | null>;
|
|
158
|
+
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
159
|
+
getBatch(batchId: string): Promise<BatchRow | null>;
|
|
160
|
+
/** 重放读数(测试与诊断):被内核拒掉的记录数 / 解析不出的坏行数 / 活文件当前的记录数。 */
|
|
161
|
+
replayStats(): {
|
|
162
|
+
rejected: number;
|
|
163
|
+
corruptLines: number;
|
|
164
|
+
records: number;
|
|
165
|
+
};
|
|
166
|
+
/** 释放追加 fd(`LocalBackend.close` → 优雅重启重开)。不包 try/catch 的理由同
|
|
167
|
+
* {@link FileApprovalNonceStore.dispose}:core 的 `AppendLog.close()` 自己就是全函数。 */
|
|
168
|
+
dispose(): void;
|
|
169
|
+
}
|
|
170
|
+
export {};
|
|
171
|
+
//# sourceMappingURL=approval-ask-store-file.d.ts.map
|