@sema-agent/server 7.12.0 → 7.13.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 +30 -2
- package/dist/adoption/plan.d.ts +38 -4
- package/dist/adoption/plan.js +72 -0
- package/dist/adoption/quiesce.d.ts +70 -0
- package/dist/adoption/quiesce.js +148 -0
- package/dist/adoption/runner.js +63 -5
- package/dist/adoption/sql.d.ts +15 -0
- package/dist/adoption/sql.js +18 -0
- package/dist/adoption/wire.d.ts +7 -1
- package/dist/adoption/wire.js +6 -0
- package/dist/approval-card.d.ts +5 -0
- package/dist/approval-card.js +22 -0
- package/dist/boot/permission-rules-audit.js +29 -1
- package/dist/boot/reapers.d.ts +15 -0
- package/dist/boot/reapers.js +101 -44
- package/dist/boot/runner-deps.d.ts +16 -2
- package/dist/boot/runner-deps.js +5 -4
- package/dist/config-types.d.ts +14 -2
- package/dist/http/active-run-conflict.d.ts +33 -8
- package/dist/http/active-run-conflict.js +37 -2
- package/dist/http/routes/adoption.js +25 -2
- package/dist/http/routes/approvals-assistant.js +33 -3
- package/dist/http/routes/capabilities.js +25 -1
- package/dist/http/routes/images.js +18 -0
- package/dist/http/routes/runs.js +21 -5
- package/dist/http/routes/tasks.js +18 -6
- package/dist/http/server.d.ts +26 -8
- package/dist/http/server.js +102 -16
- package/dist/main.js +46 -6
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/plugins/adoption-log-sql.d.ts +40 -0
- package/dist/plugins/adoption-log-sql.js +69 -2
- package/dist/plugins/file-run-store.d.ts +85 -1
- package/dist/plugins/file-run-store.js +450 -17
- package/dist/plugins/permission-rule-store-sql.d.ts +45 -0
- package/dist/plugins/permission-rule-store-sql.js +60 -2
- package/dist/plugins/shared-memory-store-sql.d.ts +23 -9
- package/dist/plugins/shared-memory-store-sql.js +55 -18
- package/dist/plugins/sql-driver.d.ts +19 -0
- package/dist/plugins/sql-driver.js +12 -0
- package/dist/plugins/store-backend.js +24 -1
- package/dist/rules-consent.d.ts +33 -4
- package/dist/rules-consent.js +43 -2
- package/dist/run-local.js +6 -2
- package/dist/runtime-governance.d.ts +33 -0
- package/dist/runtime-governance.js +32 -0
- package/dist/tool-approval.d.ts +32 -0
- package/dist/tool-approval.js +20 -0
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/package.json +3 -3
package/dist/adoption/wire.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export type AdoptionConfigEntry = z.infer<typeof AdoptionConfigEntrySchema>;
|
|
|
68
68
|
* 🔴 这是**冻结的闭集**(下游按成员名写正向断言)。改名/删员 = wire 破坏;新增 = 新行为面,要先过三问。
|
|
69
69
|
* 判据形见 183 §10.5:「按设计不迁」与「漏了没迁」在黑盒上必须可分,所以它是**正向声明**而不是沉默。
|
|
70
70
|
*/
|
|
71
|
-
export declare const NOT_MIGRATED_FACES: readonly ["usage-window", "cost-quota", "rate-limit", "approval-exemption-grantor", "approval-ask-decision-actor", "image-bake-requested-by"];
|
|
71
|
+
export declare const NOT_MIGRATED_FACES: readonly ["usage-window", "cost-quota", "rate-limit", "approval-exemption-grantor", "approval-ask-decision-actor", "image-bake-requested-by", "permission-rule-approval-history", "permission-rule-import-ticket"];
|
|
72
72
|
export type NotMigratedFace = (typeof NOT_MIGRATED_FACES)[number];
|
|
73
73
|
export declare const AdoptionNotMigratedSchema: z.ZodObject<{
|
|
74
74
|
face: z.ZodEnum<{
|
|
@@ -78,6 +78,8 @@ export declare const AdoptionNotMigratedSchema: z.ZodObject<{
|
|
|
78
78
|
"approval-exemption-grantor": "approval-exemption-grantor";
|
|
79
79
|
"approval-ask-decision-actor": "approval-ask-decision-actor";
|
|
80
80
|
"image-bake-requested-by": "image-bake-requested-by";
|
|
81
|
+
"permission-rule-approval-history": "permission-rule-approval-history";
|
|
82
|
+
"permission-rule-import-ticket": "permission-rule-import-ticket";
|
|
81
83
|
}>;
|
|
82
84
|
ruling: z.ZodEnum<{
|
|
83
85
|
D1: "D1";
|
|
@@ -124,6 +126,8 @@ export declare const AdoptionReportSchema: z.ZodObject<{
|
|
|
124
126
|
"approval-exemption-grantor": "approval-exemption-grantor";
|
|
125
127
|
"approval-ask-decision-actor": "approval-ask-decision-actor";
|
|
126
128
|
"image-bake-requested-by": "image-bake-requested-by";
|
|
129
|
+
"permission-rule-approval-history": "permission-rule-approval-history";
|
|
130
|
+
"permission-rule-import-ticket": "permission-rule-import-ticket";
|
|
127
131
|
}>;
|
|
128
132
|
ruling: z.ZodEnum<{
|
|
129
133
|
D1: "D1";
|
|
@@ -183,6 +187,8 @@ export declare const AdoptionReceiptSchema: z.ZodObject<{
|
|
|
183
187
|
"approval-exemption-grantor": "approval-exemption-grantor";
|
|
184
188
|
"approval-ask-decision-actor": "approval-ask-decision-actor";
|
|
185
189
|
"image-bake-requested-by": "image-bake-requested-by";
|
|
190
|
+
"permission-rule-approval-history": "permission-rule-approval-history";
|
|
191
|
+
"permission-rule-import-ticket": "permission-rule-import-ticket";
|
|
186
192
|
}>;
|
|
187
193
|
ruling: z.ZodEnum<{
|
|
188
194
|
D1: "D1";
|
package/dist/adoption/wire.js
CHANGED
|
@@ -64,6 +64,12 @@ export const NOT_MIGRATED_FACES = [
|
|
|
64
64
|
"approval-exemption-grantor",
|
|
65
65
|
"approval-ask-decision-actor",
|
|
66
66
|
"image-bake-requested-by",
|
|
67
|
+
// A-010.22(台账 P1)—— 规则店族此前对回执**零表态**:`permission_rule` 那只**活桶**现在真迁了
|
|
68
|
+
// (`permission_rule#owner_key` 腿),但同族另外两张表按 D2/D1 刻意不迁,而「刻意不迁」与「漏了没迁」
|
|
69
|
+
// 在黑盒上必须可分(§10.5)。少了这两员,运维读到的回执是:`notMigratedByDesign` 里没有规则店的影子、
|
|
70
|
+
// `residualSourceRows` 又把它们数成 0 —— 一份「什么都没落下」的**假声明**。
|
|
71
|
+
"permission-rule-approval-history",
|
|
72
|
+
"permission-rule-import-ticket",
|
|
67
73
|
];
|
|
68
74
|
export const AdoptionNotMigratedSchema = z
|
|
69
75
|
.object({
|
package/dist/approval-card.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare const ApprovalCardSchema: z.ZodObject<{
|
|
|
50
50
|
requiresRealApproval: z.ZodBoolean;
|
|
51
51
|
}, z.core.$strict>;
|
|
52
52
|
governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
|
|
53
|
+
persistedRuleShadowed: z.ZodOptional<z.ZodString>;
|
|
53
54
|
ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
54
55
|
rule: z.ZodString;
|
|
55
56
|
match: z.ZodEnum<{
|
|
@@ -89,6 +90,7 @@ export declare const ApprovalCardEnvelopeSchema: z.ZodObject<{
|
|
|
89
90
|
requiresRealApproval: z.ZodBoolean;
|
|
90
91
|
}, z.core.$strict>;
|
|
91
92
|
governanceForced: z.ZodOptional<z.ZodLiteral<true>>;
|
|
93
|
+
persistedRuleShadowed: z.ZodOptional<z.ZodString>;
|
|
92
94
|
ruleSuggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
93
95
|
rule: z.ZodString;
|
|
94
96
|
match: z.ZodEnum<{
|
|
@@ -132,6 +134,9 @@ export interface ApprovalCardSource {
|
|
|
132
134
|
/** [2942]/[2943]:治理来源标 —— 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口),
|
|
133
135
|
* 于是 live 帧 / `card_json` / 重放帧三面同源,不是三处各判一遍。 */
|
|
134
136
|
governanceForced?: true;
|
|
137
|
+
/** #144(core 5.25.0):被越级的持久规则原文 —— **已 redactSecrets**(发帧点做,本模块只 clip)。
|
|
138
|
+
* 与 wire 帧**同一份素材**(`ToolApprovalFrame` 结构上满足本接口)⇒ live 帧 / `card_json` / 重放帧三面同源。 */
|
|
139
|
+
persistedRuleShadowed?: string;
|
|
135
140
|
/** #154 车二:引擎铸的规则候选(**只在规则店装配时**由发帧点填;语义与在场性契约见
|
|
136
141
|
* `ApprovalCardSchema.ruleSuggestions`)。与 wire 帧**同一份素材** ⇒ live 帧 / `card_json` / 重放帧
|
|
137
142
|
* 三面同源。 */
|
package/dist/approval-card.js
CHANGED
|
@@ -79,6 +79,24 @@ export const ApprovalCardSchema = z
|
|
|
79
79
|
* park(fail-safe),回滚窗结束即自愈。
|
|
80
80
|
*/
|
|
81
81
|
governanceForced: z.literal(true).optional(),
|
|
82
|
+
/**
|
|
83
|
+
* #144(core 5.25.0 [3438]/[3443],**ADDITIVE**):这次 ask **命中了**一条用户的持久 allow 规则,
|
|
84
|
+
* 但规则**清不掉**它 —— 值 = 那条被越级的规则**原文**(语义、消音边界与「缺席 ≠ 没有规则」的
|
|
85
|
+
* 硬条款逐字见 `tool-approval.ts` 的 `ToolApprovalFrame.persistedRuleShadowed`)。
|
|
86
|
+
*
|
|
87
|
+
* 放在**卡的顶层**、与 `governanceForced` 并列而不是塞进 `risk`:`risk` 是引擎对这次**操作**的风险
|
|
88
|
+
* 判定,这一格讲的是「你那条规则怎么了」——两件事。与 `governanceForced` 也**刻意分列**:那个说
|
|
89
|
+
* 「门是运维下的」,这个说「有一条你的规则在场但被越级」,两者可以同时在场也可以各自单独在场
|
|
90
|
+
* (governance 是不可消音的三个来源之一,doctrine `always` 与工具自带 egress/irreversible mark 是另两个)。
|
|
91
|
+
*
|
|
92
|
+
* 值域 = **用户内容族**(规则原文是人写的文本,core 侧已过 `inlineUntrusted` 200 cap):
|
|
93
|
+
* 写侧在**上游**(`tool-approval.ts` 发帧点)过 `redactSecrets` 再交进来,本层只截长(`clip`)。
|
|
94
|
+
* UNTRUSTED-for-display,同 `message`/`args` 待遇。
|
|
95
|
+
*
|
|
96
|
+
* 回滚窗代价与 `governanceForced` **逐字同族**(见上一段那条 ⚠️):`.strict()` 下旧二进制读不动带本键的
|
|
97
|
+
* 新行 ⇒ 重放跳过 + 幂等重入走 park(fail-safe),`schemaVersion` 不动。
|
|
98
|
+
*/
|
|
99
|
+
persistedRuleShadowed: z.string().max(MAX_RULE_TEXT_CHARS).optional(),
|
|
82
100
|
/**
|
|
83
101
|
* #154 车二(core 5.18.0 design/179):引擎为这次 ask 铸的**规则候选**(`Bash(git status)` 精确 /
|
|
84
102
|
* `Bash(git status:*)` 词界前缀)—— 呈卡面据它渲「不再询问」。
|
|
@@ -183,6 +201,7 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
|
|
|
183
201
|
const toolCallId = clip(source.toolCallId, MAX_IDENT);
|
|
184
202
|
const sourceTaskId = clip(source.sourceTaskId, MAX_IDENT);
|
|
185
203
|
const sourceAgentName = clip(source.sourceAgentName, MAX_AGENT_NAME);
|
|
204
|
+
const shadowedRule = clip(source.persistedRuleShadowed, MAX_RULE_TEXT_CHARS);
|
|
186
205
|
return {
|
|
187
206
|
toolName: clip(source.toolName, MAX_IDENT) ?? "",
|
|
188
207
|
message: clip(source.message, MAX_MESSAGE) ?? "",
|
|
@@ -195,6 +214,9 @@ export function buildApprovalCard(source, req, requiresRealApproval) {
|
|
|
195
214
|
},
|
|
196
215
|
// [2942]/[2943]:只在为真时投影(`=== true` 严判:非布尔真值不得把一张普通卡染成治理卡)。
|
|
197
216
|
...(source.governanceForced === true ? { governanceForced: true } : {}),
|
|
217
|
+
// #144:被越级的规则原文。**空串不投**(core 的四个 mint 点只在真有命中规则时带这个键 ⇒ 空串不是
|
|
218
|
+
// 「有一条空规则」而是坏值;投一个空串会让壳渲一行「你的规则 ⟨⟩ 仍在」)。截长同 `message` 待遇。
|
|
219
|
+
...(shadowedRule !== undefined && shadowedRule !== "" ? { persistedRuleShadowed: shadowedRule } : {}),
|
|
198
220
|
// #154 车二:候选**逐字**投影(空数组 ⇒ 不投键 —— 「没有候选」与「本部署不供候选」在卡面上都读作
|
|
199
221
|
// 「本卡无此选项」,铸一个空数组只会让消费端多一条无意义的分支)。拷贝一份:卡不与调用方共享可变引用。
|
|
200
222
|
...(source.ruleSuggestions !== undefined && source.ruleSuggestions.length > 0
|
|
@@ -44,12 +44,40 @@ export async function auditDormantPermissionRules(ctx) {
|
|
|
44
44
|
if (buckets > 0 && !ctx.explicit) {
|
|
45
45
|
// ②③ 都不成立才说话。文案给的是**一条可行动的因果**:看到少问了 ⇒ 这里有 N 只既有桶 ⇒ 用
|
|
46
46
|
// `GET /v1/rules` 看、用 `DELETE /v1/rules` 收,或者把旋钮显式关掉。
|
|
47
|
+
//
|
|
48
|
+
// 🔴 **local-owner 桶的现状必须同拍说清**(#203 收官件 / #205 件5,core 5.24.0 提货批):
|
|
49
|
+
// `countBuckets` 数的是**所有**桶,其中可能有一只**身份缺席**的 local-owner 桶(File 后端 =
|
|
50
|
+
// `permission-rules/local-owner.json`,SQL 后端 = owner_key `local-owner` 的行)。而 `GET`/`DELETE
|
|
51
|
+
// /v1/rules` 两口是 **principal 寻址**的(无 principal 一律 401,契约上刻意的边界)—— 对那只桶,
|
|
52
|
+
// 上面那句「inspect with GET / revoke with DELETE」是**兑不出来的承诺**。
|
|
53
|
+
//
|
|
54
|
+
// ⚠️ 文案只说**兑得出来**的话(codex 对抗复审 R1-[medium],验真后改):第一稿在这里写的是
|
|
55
|
+
// 「adopt it into a principal first」——**本仓没有那条路**。收编入口的来源词表逐字只有
|
|
56
|
+
// `{kind:"principal"}`(`adoption/wire.ts` 的 `AdoptionSourceSchema`,`.strict()` 拒 local-owner),
|
|
57
|
+
// core 的 `adoptFilePermissionRuleStore` 在 src/ 下零调用点,SQL 侧连对应原语都没有。指一条走不通
|
|
58
|
+
// 的路比不指更坏:运维会去试,试不通才发现自己被误导。
|
|
59
|
+
// 今天**真能做的**只有两件:①`PERMISSION_RULES_ENABLED=false` 把整条车道关掉(上面已给);
|
|
60
|
+
// ②运维直接动存储。
|
|
61
|
+
// 🔴 第二条的**谓词必须是真的**(codex 对抗复审 R2-[medium],验真后改):第一稿写的是
|
|
62
|
+
// 「owner_key='local-owner'」——那条 SQL **一行都删不到**。`buildRuleOwnerKey`
|
|
63
|
+
// (`plugins/permission-rule-store-sql.ts`)把 owner_key 算成 `sha256("local-owner")` 的十六进制,
|
|
64
|
+
// 字面判别式存在**另一列** `owner_kind` 上。给一条匹配零行的删除语句比不给更坏:它会静默成功,
|
|
65
|
+
// 运维以为收回了权限而规则还在放行。真谓词 = `permission_rule` 表的 `owner_kind='local-owner'`。
|
|
66
|
+
// File 后端那侧是固定文件名,照给。
|
|
67
|
+
// 店层与车道层其实已接通(core 5.24.0 的 `removePersistedRule` 收 `RuleOwner`,本仓
|
|
68
|
+
// `rules-consent.ts` 的 `listRules`/`removeRule` 已能寻址该桶,格见
|
|
69
|
+
// `test/rules-consent-local-owner.test.ts`),缺的只是一个 HTTP 开口 —— 开不开、开在哪条门
|
|
70
|
+
// (operator 面 / run-local 面)属产品面裁定,不在本车擅开。
|
|
47
71
|
ctx.logger.info("permission_rules_activated_by_default", {
|
|
48
72
|
buckets,
|
|
49
73
|
knob: "PERMISSION_RULES_ENABLED",
|
|
50
74
|
explicit: false,
|
|
51
75
|
note: `${buckets} pre-existing permission-rule bucket(s) went live with this release's ON-by-default flip — ` +
|
|
52
|
-
"inspect with GET /v1/rules, revoke with DELETE /v1/rules, or set PERMISSION_RULES_ENABLED=false to keep the lane off"
|
|
76
|
+
"inspect with GET /v1/rules, revoke with DELETE /v1/rules, or set PERMISSION_RULES_ENABLED=false to keep the lane off. " +
|
|
77
|
+
"NOTE: an identity-less local-owner bucket (if this count includes one) is NOT reachable through those two " +
|
|
78
|
+
"principal-addressed endpoints, and this build exposes NO self-service path for it — either turn the knob off, " +
|
|
79
|
+
"or remove the bucket at the storage layer (file backend: the permission-rules/local-owner.json file; " +
|
|
80
|
+
"SQL backend: the permission_rule row WHERE owner_kind='local-owner')",
|
|
53
81
|
});
|
|
54
82
|
}
|
|
55
83
|
return { buckets };
|
package/dist/boot/reapers.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface ThrottledReaperCatch {
|
|
|
51
51
|
* survives across ticks — a fresh instance per tick would never accumulate past 1.
|
|
52
52
|
*/
|
|
53
53
|
export declare function createThrottledReaperCatch(name: string, logger: Logger, threshold?: number): ThrottledReaperCatch;
|
|
54
|
+
/** A-010.17 —— 维护 tick 真正消费的那**一手**(窄口;理由见 `ReapersCtx.permissionRuleStores`)。
|
|
55
|
+
* 可选成员:File 车道没有这一面(追加日志的压实是另一件事,如实登记在 `rules-consent.ts` 的
|
|
56
|
+
* `PermissionRuleStoreBundle.reapExpired` 注里),缺席 ⇒ 本腿零调用。 */
|
|
57
|
+
export interface PermissionRuleRetentionSweeper {
|
|
58
|
+
reapExpired?(nowMs: number): Promise<number>;
|
|
59
|
+
}
|
|
54
60
|
export interface ReapersCtx {
|
|
55
61
|
config: ServiceConfig;
|
|
56
62
|
logger: Logger;
|
|
@@ -80,6 +86,15 @@ export interface ReapersCtx {
|
|
|
80
86
|
* `TOOL_APPROVAL_ENABLED=false` 的部署恒 undefined ⇒ 收敛器照常收敛,只是不发通知帧(壳侧靠
|
|
81
87
|
* 重连 preamble 对账,见 approval-card.ts 的 `ApprovalRevokeFrame` 顶注)。 */
|
|
82
88
|
toolApproval: ToolApprovalCoordinator | undefined;
|
|
89
|
+
/** A-010.17:规则店的**保留期口**。`PERMISSION_RULES_ENABLED=false` 或后端没实装 ⇒ undefined
|
|
90
|
+
* ⇒ 本腿根本不注册(零扫描),与它的同族旋钮腿同姿势。
|
|
91
|
+
*
|
|
92
|
+
* 🔴 类型刻意窄到 {@link PermissionRuleRetentionSweeper} 而不是整个 `PermissionRuleStoreBundle`:
|
|
93
|
+
* 维护 tick 对那只束的其余能力(provider / approvals / tickets)一无所知也不该知道 —— 窄口让测试
|
|
94
|
+
* 能用**两行**字面量驱动真实现的同一段代码,而不必为了满足一个大接口去铸 `as unknown as` 宽松断言
|
|
95
|
+
* (`boot/permission-rules-audit.ts` 的 `DormantRuleCounter` 是同一条判据的先例)。
|
|
96
|
+
* 生产传的仍是整只束(结构上满足这个窄口)。 */
|
|
97
|
+
permissionRuleStores: PermissionRuleRetentionSweeper | undefined;
|
|
83
98
|
/** 晚绑(server 造出来才有)——见文件头「位置即契约」①。 */
|
|
84
99
|
getRunDenySweep: () => ((now: number) => Promise<void>) | undefined;
|
|
85
100
|
}
|
package/dist/boot/reapers.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
16
16
|
import { sweepStaleScratchpads } from "../env-facts.js";
|
|
17
17
|
import { CostQuota } from "../observability/cost-quota.js";
|
|
18
|
+
import { isAdoptionQuiescing, trackMaintenanceLeg } from "../adoption/quiesce.js";
|
|
18
19
|
import { createApprovalReconciler } from "../approval-reconciler.js";
|
|
19
20
|
/** #157 / [2762]§五(sema-comms `audits/failopen-governance-176.md` §7 "Z 类 1 条"):default consecutive-failure
|
|
20
21
|
* count before a reaper leg's swallowed error gets a `warn` line. The ~15 `.catch(() => undefined)` sites
|
|
@@ -74,10 +75,22 @@ export function createThrottledReaperCatch(name, logger, threshold = REAPER_FAIL
|
|
|
74
75
|
}
|
|
75
76
|
/** 起后台维护 tick,返回定时器 handle(收尾段 clearInterval 用)。 */
|
|
76
77
|
export function startReapers(ctx) {
|
|
77
|
-
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, getRunDenySweep, } = ctx;
|
|
78
|
+
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, permissionRuleStores, getRunDenySweep, } = ctx;
|
|
78
79
|
// S7 (SILENT-FALLBACK P0-d): the sweeps' return counts were discarded — an instance death that batch-fails
|
|
79
80
|
// N orphans was indistinguishable from organic failures. Count + log ONLY when a sweep flipped rows (>0),
|
|
80
81
|
// so healthy ticks stay silent. Tolerant of void-returning stores (typeof guard).
|
|
82
|
+
/**
|
|
83
|
+
* 一条维护腿的**起飞登记**(codex 对抗复审 R1 [high] 的修复面)。
|
|
84
|
+
*
|
|
85
|
+
* 代替裸 `void <promise>`:语义与 `void` 完全一样(不 await、错误由各腿自己的 guard 接住),
|
|
86
|
+
* 只多一件事 —— 把这一份 promise 登记进收编闸的在飞集,好让一条正要动数据的收编弧**等它落地**。
|
|
87
|
+
* 少一处没换,那条腿就是闸的漏网者(而漏网是静默的),所以本文件里 tick 内**不该再出现裸 `void`**。
|
|
88
|
+
* 入参允许 `undefined`(可选链的腿:`void store?.reap()` 在店缺席时求值成 undefined)。
|
|
89
|
+
*/
|
|
90
|
+
const leg = (p) => {
|
|
91
|
+
if (p !== undefined)
|
|
92
|
+
trackMaintenanceLeg(p);
|
|
93
|
+
};
|
|
81
94
|
const reapCount = (metric, labels) => (n) => {
|
|
82
95
|
if (typeof n === "number" && n > 0) {
|
|
83
96
|
metrics.inc(metric, labels, n);
|
|
@@ -111,6 +124,8 @@ export function startReapers(ctx) {
|
|
|
111
124
|
const rosterReapOlderThanGuard = createThrottledReaperCatch("roster_reap_older_than", logger);
|
|
112
125
|
const scratchpadSweepStaleGuard = createThrottledReaperCatch("scratchpad_sweep_stale", logger);
|
|
113
126
|
const approvalReconcileGuard = createThrottledReaperCatch("approval_reconcile", logger);
|
|
127
|
+
const permissionRuleReapGuard = createThrottledReaperCatch("permission_rule_reap_expired", logger);
|
|
128
|
+
let permissionRuleReapInFlight = false; // A-010.17 / codex R2:重入守卫(邻居四腿同形,理由见调用点)
|
|
114
129
|
let approvalReconcileInFlight = false; // 车5 §8 C-8:重入守卫(邻居四先例同形——本腿时长随 PARKING 行数增长)
|
|
115
130
|
// #151 车5(design/172 §3.0 对账收敛器 + 崩溃恢复扫描):建腿条件 = 开关开 ∧ **有 `StoreBackend`**
|
|
116
131
|
// (见下方判据行 —— 是**任意** backend,不限 SQL twins:`local` 车道也算,它的 `approvalAsk()` 是进程内
|
|
@@ -237,12 +252,32 @@ export function startReapers(ctx) {
|
|
|
237
252
|
metrics.setGauge("fleet_clock_probe_ok", 0);
|
|
238
253
|
}
|
|
239
254
|
};
|
|
255
|
+
let quiesceLogged = false;
|
|
240
256
|
const reaper = setInterval(() => {
|
|
241
|
-
|
|
242
|
-
|
|
257
|
+
// 🔴 A-010.18(验真后修):**收编弧跑动期间整轮让路**。design/183 I1「引擎先停」此前在进程内
|
|
258
|
+
// 零执法 —— 弧在逐表 UPDATE 的同时,本 tick 的好几条腿写的正是被迁的那些表。最锋利的是审批收敛器:
|
|
259
|
+
// 它按 `owner` 读出一批行再 CAS 结算,而收编正在把 `approval_ask.owner` 从 A 改成 B ⇒ 一只本该
|
|
260
|
+
// PARKED 的 ask 可能被写成 **DENIED**,而 DENIED 是终局(收编回滚不了它,重跑也追认不回来)。
|
|
261
|
+
// 为什么让开**整轮**而不是挑腿避让、以及「只挡本副本」这条射程边界,逐字见 `adoption/quiesce.ts`。
|
|
262
|
+
// 让路是零代价的:每条腿都幂等、都能等下一轮,而弧是毫秒级的几条 UPDATE。
|
|
263
|
+
if (isAdoptionQuiescing()) {
|
|
264
|
+
if (!quiesceLogged) {
|
|
265
|
+
quiesceLogged = true; // 一次性 —— 弧毫秒级,逐 tick 刷屏没有信息量
|
|
266
|
+
logger.info("reaper_tick_yielded_to_adoption", {
|
|
267
|
+
// 文案只说**兑得出来**的话(codex R1 [high] 的教训):本行保证的是「不再**起飞**新腿」。
|
|
268
|
+
// 已经在飞的那些由弧侧的有界排空负责等,等不到会另打一条 `adoption_maintenance_drain_timeout`
|
|
269
|
+
// —— 两条读数合起来才是全貌,所以这一行不许自称「没有任何腿在写被迁的表」。
|
|
270
|
+
note: "an adoption arc is running in this process; the maintenance tick launches NO new legs for its duration. Legs already airborne when the arc began are waited on separately (bounded) — see adoption_maintenance_drain_timeout if that wait ran out (design/183 I1, in-process half)",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
quiesceLogged = false;
|
|
276
|
+
leg(probeClockSkew().then(() => probeClockSkewGuard.onSuccess(), probeClockSkewGuard.onError)); // S10
|
|
277
|
+
leg(runStore
|
|
243
278
|
?.reapStale(config.runStaleSec * 1000)
|
|
244
279
|
.then(reapCount("runs_reaped_total", { kind: "stale" }))
|
|
245
|
-
.then(() => runsReapStaleGuard.onSuccess(), runsReapStaleGuard.onError);
|
|
280
|
+
.then(() => runsReapStaleGuard.onSuccess(), runsReapStaleGuard.onError));
|
|
246
281
|
// Durable F4 (design/45) + design/80 §3 inv#3 crash-safe backstop: CAS-expire checkpoints past their
|
|
247
282
|
// deadline OR their absolute terminal_at_ms backstop (≈ deny), then fail the suspended run rows whose
|
|
248
283
|
// checkpoint was thereby expired (release task_active = unlock the session). These run EVERY tick,
|
|
@@ -256,51 +291,51 @@ export function startReapers(ctx) {
|
|
|
256
291
|
// 真 pin 只存在于 in-memory TtlSessionStore;本部署 durable checkpoint 恒配 durable session 后端
|
|
257
292
|
// (pin=no-op),deny-sweep 走 resumeCheckpoint=core 内部 unpin ✓。SESSION_BACKEND=memory+durable
|
|
258
293
|
// checkpoint 的 niche dev 组合下 abort-expire 臂会把被钉会话泄到进程终——显式接受,不为 dev 形加腿。
|
|
259
|
-
|
|
294
|
+
leg(checkpointStore
|
|
260
295
|
.reapExpired(Date.now())
|
|
261
296
|
.then(reapCount("checkpoints_reaped_total", {}))
|
|
262
|
-
.then(() => checkpointsReapExpiredGuard.onSuccess(), checkpointsReapExpiredGuard.onError);
|
|
263
|
-
|
|
297
|
+
.then(() => checkpointsReapExpiredGuard.onSuccess(), checkpointsReapExpiredGuard.onError));
|
|
298
|
+
leg(runStore
|
|
264
299
|
?.failSuspendedWithExpiredCheckpoint()
|
|
265
300
|
.then(reapCount("runs_reaped_total", { kind: "expired_checkpoint" }))
|
|
266
|
-
.then(() => runsFailSuspendedExpiredCheckpointGuard.onSuccess(), runsFailSuspendedExpiredCheckpointGuard.onError);
|
|
301
|
+
.then(() => runsFailSuspendedExpiredCheckpointGuard.onSuccess(), runsFailSuspendedExpiredCheckpointGuard.onError));
|
|
267
302
|
// D-D SLA-timer: resolve-DENY human/irreversible_ask gates past their deadline (graceful — the model
|
|
268
303
|
// continues with the denial), vs the abort reapExpired gives the other kinds. Bounded per tick.
|
|
269
|
-
|
|
270
|
-
.then(() => runDenySweepGuard.onSuccess(), runDenySweepGuard.onError); // A10 搬运改写:晚绑取值(原 `runDenySweep?.`)
|
|
304
|
+
leg(getRunDenySweep()?.(Date.now())
|
|
305
|
+
.then(() => runDenySweepGuard.onSuccess(), runDenySweepGuard.onError)); // A10 搬运改写:晚绑取值(原 `runDenySweep?.`)
|
|
271
306
|
}
|
|
272
307
|
// The finer, OPT-IN time-based suspended-run reaper (APPROVAL_TIMEOUT_SEC). Stays gated — reapSuspended
|
|
273
308
|
// with a 0 TTL would nuke ALL suspended rows (cutoff = now). The absolute backstop above is the safety
|
|
274
309
|
// floor; this is the operator-chosen deadline. ([2354] A-5:F4 轮询店的 expireStale 腿随车道退役。)
|
|
275
310
|
if (config.approvalTimeoutSec > 0) {
|
|
276
|
-
|
|
311
|
+
leg(runStore
|
|
277
312
|
?.reapSuspended(config.approvalTimeoutSec * 1000)
|
|
278
313
|
.then(reapCount("runs_reaped_total", { kind: "suspended" }))
|
|
279
|
-
.then(() => runsReapSuspendedGuard.onSuccess(), runsReapSuspendedGuard.onError);
|
|
314
|
+
.then(() => runsReapSuspendedGuard.onSuccess(), runsReapSuspendedGuard.onError));
|
|
280
315
|
}
|
|
281
316
|
// GC checkpoint_ctx rows whose checkpoint is gone (bound to the checkpoint lifecycle).
|
|
282
|
-
|
|
317
|
+
leg(checkpointStore
|
|
283
318
|
?.reapCtx(Date.now() - config.runStaleSec * 1000)
|
|
284
|
-
.then(() => checkpointReapCtxGuard.onSuccess(), checkpointReapCtxGuard.onError);
|
|
319
|
+
.then(() => checkpointReapCtxGuard.onSuccess(), checkpointReapCtxGuard.onError));
|
|
285
320
|
rateLimiter?.sweep();
|
|
286
321
|
if (costQuota instanceof CostQuota)
|
|
287
322
|
costQuota.reap(); // TiDB variant self-reaps in its flush loop
|
|
288
|
-
|
|
323
|
+
leg(toolResultStore
|
|
289
324
|
?.reapOlderThan?.(Date.now() - config.toolResultTtlSec * 1000)
|
|
290
|
-
?.then(() => toolResultReapOlderThanGuard.onSuccess(), toolResultReapOlderThanGuard.onError); // optional extra: SQL twins only (the local FileToolResultStore persists like transcripts)
|
|
325
|
+
?.then(() => toolResultReapOlderThanGuard.onSuccess(), toolResultReapOlderThanGuard.onError)); // optional extra: SQL twins only (the local FileToolResultStore persists like transcripts)
|
|
291
326
|
// 2c session-sync: GLOBALLY GC grace-window orphan blobs (standalone /sync/blobs PUTs that were never
|
|
292
327
|
// imported are otherwise collected only on reap()/deleteBySession(), which never fire for a never-imported scope).
|
|
293
328
|
// Bounds the standalone-PUT orphan-blob exhaustion (review finding) to the grace window. Durable twins only (local
|
|
294
329
|
// omits the seam → optional-chained no-op).
|
|
295
|
-
|
|
330
|
+
leg(fileSnapshotStore
|
|
296
331
|
?.sweepOrphanBlobs?.()
|
|
297
|
-
.then(() => fileSnapshotSweepOrphanBlobsGuard.onSuccess(), fileSnapshotSweepOrphanBlobsGuard.onError);
|
|
332
|
+
.then(() => fileSnapshotSweepOrphanBlobsGuard.onSuccess(), fileSnapshotSweepOrphanBlobsGuard.onError));
|
|
298
333
|
// D-1 附件 TTL:上传后从未被任何 task 引用(session_id NULL)且超过 attachmentUnboundTtlMs 的行收割
|
|
299
334
|
// (绑定行不在此收——随会话 E21 级联删)。best-effort,与其余 reaper 腿同姿。
|
|
300
|
-
|
|
335
|
+
leg(taskAttachmentStore
|
|
301
336
|
?.reapUnbound(Date.now() - config.attachmentUnboundTtlMs)
|
|
302
337
|
.then(reapCount("attachments_reaped_total", {}))
|
|
303
|
-
.then(() => attachmentReapUnboundGuard.onSuccess(), attachmentReapUnboundGuard.onError);
|
|
338
|
+
.then(() => attachmentReapUnboundGuard.onSuccess(), attachmentReapUnboundGuard.onError));
|
|
304
339
|
// D-1 孤儿**对象**彻底 GC(clay 拍 2026-07-28):对象存储 × meta 行对账,grace 默认 1h(上传先行窗
|
|
305
340
|
// 保护;`ATTACHMENT_ORPHAN_GRACE_MS=0` 关掉本腿——同 tick 邻居都有旋钮,复审 F10)。
|
|
306
341
|
// 列举失败=本轮 warn 跳过(「列不出来」绝不当「没有孤儿」),下轮再试。
|
|
@@ -309,7 +344,7 @@ export function startReapers(ctx) {
|
|
|
309
344
|
// wfRunReap/bgAgentReap 都有同款守卫。
|
|
310
345
|
if (config.attachmentOrphanGraceMs > 0 && !attachmentSweepInFlight && taskAttachmentStore?.sweepOrphanObjects) {
|
|
311
346
|
attachmentSweepInFlight = true;
|
|
312
|
-
|
|
347
|
+
leg(taskAttachmentStore
|
|
313
348
|
.sweepOrphanObjects(config.attachmentOrphanGraceMs)
|
|
314
349
|
.then((n) => { if (n > 0) {
|
|
315
350
|
metrics.inc("attachment_orphan_objects_swept_total", {}, n);
|
|
@@ -329,21 +364,21 @@ export function startReapers(ctx) {
|
|
|
329
364
|
// observability must never crash the tick — see comment above.
|
|
330
365
|
}
|
|
331
366
|
})
|
|
332
|
-
.finally(() => { attachmentSweepInFlight = false; });
|
|
367
|
+
.finally(() => { attachmentSweepInFlight = false; }));
|
|
333
368
|
}
|
|
334
369
|
// P1d-β 2c session-sync: GC ABANDONED staged imports — staging-id session_event rows (`%#stg-%`) with NO
|
|
335
370
|
// session_meta whose oldest row is older than the grace window (an in-flight stream stays fresh → never reaped).
|
|
336
371
|
// Bounds the orphan-staging-row growth (a Phase B that opened a staging then never committed). Durable session
|
|
337
372
|
// stores only (the local backend stages in memory → no durable rows; the seam is absent → optional-chained no-op).
|
|
338
|
-
|
|
339
|
-
.then(() => sessionSweepStagingGuard.onSuccess(), sessionSweepStagingGuard.onError);
|
|
373
|
+
leg(backend?.session()?.sweepStagingSessions?.()
|
|
374
|
+
.then(() => sessionSweepStagingGuard.onSuccess(), sessionSweepStagingGuard.onError));
|
|
340
375
|
// Bake-runner backstop (§P2.7): fail any `running` bake whose lease went stale (the runner crashed mid-build),
|
|
341
376
|
// append a synthetic terminal `done{failed}` so SSE readers settle, and force-release the single-flight lease.
|
|
342
377
|
// staleMs (≈3× the 30s heartbeat) bounds a healthy slow build so it is never wrongly reaped.
|
|
343
|
-
|
|
378
|
+
leg(imageBakes
|
|
344
379
|
?.reapStaleBakes(config.imageBakes.staleMs)
|
|
345
380
|
.then(reapCount("bakes_reaped_total", {}))
|
|
346
|
-
.then(() => imageBakesReapStaleGuard.onSuccess(), imageBakesReapStaleGuard.onError);
|
|
381
|
+
.then(() => imageBakesReapStaleGuard.onSuccess(), imageBakesReapStaleGuard.onError));
|
|
347
382
|
// SVC-3 worktree isolation: deregister worktrees orphaned by a process crash (the Runner never reached
|
|
348
383
|
// destroy → `git worktree remove` never ran). `git worktree prune` cleans registrations whose dirs are
|
|
349
384
|
// already gone. Best-effort, userland (core ships no post-kill Runner hook); never throws. Unset = no-op.
|
|
@@ -353,22 +388,22 @@ export function startReapers(ctx) {
|
|
|
353
388
|
// 照抄 bgAgentReap 的 in-flight + finally 释放形。
|
|
354
389
|
if (worktreeReap && !worktreeReapInFlight) {
|
|
355
390
|
worktreeReapInFlight = true;
|
|
356
|
-
|
|
391
|
+
leg(worktreeReap()
|
|
357
392
|
.then(() => worktreeReapGuard.onSuccess(), worktreeReapGuard.onError)
|
|
358
|
-
.finally(() => (worktreeReapInFlight = false));
|
|
393
|
+
.finally(() => (worktreeReapInFlight = false)));
|
|
359
394
|
}
|
|
360
395
|
// SVC-1 (adversarial-review HIGH): PERIODIC notify-recovery sweep (not just at boot) — re-delivers a terminal
|
|
361
396
|
// run whose in-process notify was lost, AND finalizes-as-abandoned a `running` run orphaned past the grace
|
|
362
397
|
// window (core never resumes/reaps a prior `running` row, so nothing else would). Idempotent; best-effort.
|
|
363
|
-
|
|
398
|
+
leg(workflowNotifyGate
|
|
364
399
|
?.recover(workflowRecoverOpts)
|
|
365
|
-
.then(() => workflowNotifyRecoverGuard.onSuccess(), workflowNotifyRecoverGuard.onError);
|
|
400
|
+
.then(() => workflowNotifyRecoverGuard.onSuccess(), workflowNotifyRecoverGuard.onError));
|
|
366
401
|
// SVC-2 (adversarial-review HIGH): time-based GC of the workflow_journal table (the heaviest, TaskResult-bearing
|
|
367
402
|
// one) — the per-run deleteByRun has no run-store reap hook, so this bounded sweep is what stops unbounded
|
|
368
403
|
// growth. A resume of a journal older than the retention window re-runs live (resume is an optimization).
|
|
369
|
-
|
|
404
|
+
leg(workflowJournalStore
|
|
370
405
|
?.reapExpired?.(Date.now(), config.workflowJournalRetentionMs)
|
|
371
|
-
.then(() => workflowJournalReapExpiredGuard.onSuccess(), workflowJournalReapExpiredGuard.onError);
|
|
406
|
+
.then(() => workflowJournalReapExpiredGuard.onSuccess(), workflowJournalReapExpiredGuard.onError));
|
|
372
407
|
// Retention (WorkflowRunStore never auto-purges, reap is explicit):
|
|
373
408
|
// age out TERMINAL workflow_run rows across ALL scopes. SQL twins only (reapAllScopes is their DISTINCT-scope
|
|
374
409
|
// extension; the contract itself has no cross-scope enumeration, and the File store keeps the transcripts-like
|
|
@@ -379,30 +414,30 @@ export function startReapers(ctx) {
|
|
|
379
414
|
const sweep = sqlWorkflowRunStore?.reapAllScopes?.(Date.now(), { maxAgeMs: config.workflowRunRetentionMs });
|
|
380
415
|
if (sweep) {
|
|
381
416
|
wfRunReapInFlight = true;
|
|
382
|
-
|
|
417
|
+
leg(sweep
|
|
383
418
|
.then(() => workflowRunReapAllScopesGuard.onSuccess(), workflowRunReapAllScopesGuard.onError)
|
|
384
|
-
.finally(() => (wfRunReapInFlight = false));
|
|
419
|
+
.finally(() => (wfRunReapInFlight = false)));
|
|
385
420
|
}
|
|
386
421
|
// 1.108: SQL notify-journal retention rides the same knob — ACKED rows are pure history (pending rows are
|
|
387
422
|
// the recovery backlog and are NEVER reaped; the orphan-grace sweep retires a stuck pending run).
|
|
388
|
-
|
|
423
|
+
leg(workflowNotifyJournal
|
|
389
424
|
?.reapAcked?.(Date.now() - config.workflowRunRetentionMs)
|
|
390
|
-
.then(() => workflowNotifyJournalReapAckedGuard.onSuccess(), workflowNotifyJournalReapAckedGuard.onError);
|
|
425
|
+
.then(() => workflowNotifyJournalReapAckedGuard.onSuccess(), workflowNotifyJournalReapAckedGuard.onError));
|
|
391
426
|
}
|
|
392
427
|
// core 1.364 durable bg-agent joint reap(定义在 interval 上方,契约注释在彼)。in-flight 守卫同
|
|
393
428
|
// wfRun sweep(per-scope 串行循环,短 tick + 多 scope 下不叠罗汉)。
|
|
394
429
|
if (reapBgAgents && !bgAgentReapInFlight) {
|
|
395
430
|
bgAgentReapInFlight = true;
|
|
396
|
-
|
|
431
|
+
leg(reapBgAgents()
|
|
397
432
|
.then(() => bgAgentsReapGuard.onSuccess(), bgAgentsReapGuard.onError)
|
|
398
|
-
.finally(() => (bgAgentReapInFlight = false));
|
|
433
|
+
.finally(() => (bgAgentReapInFlight = false)));
|
|
399
434
|
}
|
|
400
435
|
// [1522] MED2:agent_roster TTL 清理(core RB-23②③ 派给部署的半场)——SQL twins 扩展面
|
|
401
436
|
// (reapOlderThan,duck probe;File/Memory 店 core 自带 maxAgeMs,无此面=no-op)。
|
|
402
|
-
|
|
437
|
+
leg(rosterStore
|
|
403
438
|
?.reapOlderThan?.(Date.now() - config.rosterRetentionMs)
|
|
404
439
|
.then(reapCount("roster_rows_reaped_total", {}))
|
|
405
|
-
.then(() => rosterReapOlderThanGuard.onSuccess(), rosterReapOlderThanGuard.onError);
|
|
440
|
+
.then(() => rosterReapOlderThanGuard.onSuccess(), rosterReapOlderThanGuard.onError));
|
|
406
441
|
// 修8(三路复审 absorb-2,接线 (b)):periodic scratchpad sweep — 无 E21 purge 兜到的孤儿目录(session 从未
|
|
407
442
|
// DELETE、purge 当次失败、local 后端无 purge coordinator)按 mtime 过期回收。SCRATCHPAD_SWEEP_TTL_MS
|
|
408
443
|
// (default 7d,0=禁用)。activeSessionIds 不传:活跃判据的诚实边界在 env-facts.ts 的 sweep 文档——TTL 7d
|
|
@@ -413,18 +448,40 @@ export function startReapers(ctx) {
|
|
|
413
448
|
// 库延迟增长,短 REAP_INTERVAL_SEC 下无守卫会逐 tick 叠罗汉压同一批行。
|
|
414
449
|
if (approvalReconciler && !approvalReconcileInFlight) {
|
|
415
450
|
approvalReconcileInFlight = true;
|
|
416
|
-
|
|
451
|
+
leg(approvalReconciler
|
|
417
452
|
.runOnce(Date.now())
|
|
418
453
|
.then(() => approvalReconcileGuard.onSuccess(), approvalReconcileGuard.onError)
|
|
419
|
-
.finally(() => (approvalReconcileInFlight = false));
|
|
454
|
+
.finally(() => (approvalReconcileInFlight = false)));
|
|
455
|
+
}
|
|
456
|
+
// A-010.17:规则店的保留期腿。**只删可证已死的行**(过期导入票 + 超期孤儿 pending 记录)——
|
|
457
|
+
// 谓词、每轮上界、以及「为什么这条腿不需要旋钮」逐字见 `plugins/permission-rule-store-sql.ts`
|
|
458
|
+
// 的 `reapExpired` / `RULE_REAP_BATCH`。
|
|
459
|
+
// `?.` 的缺席臂 = File 车道没有这一面(如实登记在 `rules-consent.ts` 的接口注里,不是漏接)。
|
|
460
|
+
//
|
|
461
|
+
// 🔴 重入守卫(codex 对抗复审 R2 [high],验真后修):第一版按「两条等值 DELETE,时长不随在飞量增长」
|
|
462
|
+
// 判定无需守卫 —— 那句话**不成立**。`permission_rule_approval` 按设计永久留审计事实(只增不减),
|
|
463
|
+
// 而 MySQL 存量库在删库重建之前拿不到新加的清扫索引 ⇒ 这条腿的时长会随**审计史**增长。一旦越过
|
|
464
|
+
// tick 间隔,下一轮就叠在同一批行上。判据与邻居四腿(attachmentSweep / wfRunReap / bgAgentReap /
|
|
465
|
+
// worktreeReap)同形。
|
|
466
|
+
if (!permissionRuleReapInFlight) {
|
|
467
|
+
permissionRuleReapInFlight = true;
|
|
468
|
+
leg(permissionRuleStores
|
|
469
|
+
?.reapExpired?.(Date.now())
|
|
470
|
+
.then(reapCount("permission_rules_reaped_total", {}))
|
|
471
|
+
.then(() => permissionRuleReapGuard.onSuccess(), permissionRuleReapGuard.onError)
|
|
472
|
+
.finally(() => (permissionRuleReapInFlight = false)));
|
|
473
|
+
// 店缺席(File 车道 / 关旋钮)⇒ 上面整条求值成 undefined、`finally` 永不跑 ⇒ 守卫要当场归位,
|
|
474
|
+
// 否则第一轮之后这条腿就被自己的守卫永久锁死(而它本来就该零调用,锁死是静默的)。
|
|
475
|
+
if (permissionRuleStores?.reapExpired === undefined)
|
|
476
|
+
permissionRuleReapInFlight = false;
|
|
420
477
|
}
|
|
421
478
|
if (config.scratchpadSweepTtlMs > 0) {
|
|
422
|
-
|
|
479
|
+
leg(sweepStaleScratchpads(config.localDataRoot ?? localRoot, { olderThanMs: config.scratchpadSweepTtlMs })
|
|
423
480
|
.then((removed) => {
|
|
424
481
|
if (removed > 0)
|
|
425
482
|
logger.info("scratchpads_swept", { removed });
|
|
426
483
|
})
|
|
427
|
-
.then(() => scratchpadSweepStaleGuard.onSuccess(), scratchpadSweepStaleGuard.onError);
|
|
484
|
+
.then(() => scratchpadSweepStaleGuard.onSuccess(), scratchpadSweepStaleGuard.onError));
|
|
428
485
|
}
|
|
429
486
|
}, config.reapIntervalSec * 1000);
|
|
430
487
|
reaper.unref?.();
|
|
@@ -87,6 +87,20 @@ export interface RunnerDepsCtx {
|
|
|
87
87
|
/** #154 车二:持久化权限规则店 provider(core `RunnerDeps.permissionRuleStore`)。缺席 ⇒ 引擎的
|
|
88
88
|
* `permissionRules.storeWired` 如实报 false、`AskRequest.ruleSuggestions` 不铸(诚实缺席)。 */
|
|
89
89
|
permissionRuleStore: RunnerDeps["permissionRuleStore"];
|
|
90
|
+
/**
|
|
91
|
+
* 交接件⑤ —— commit 尾注的署名座(`RunnerDeps.hands.commitCoAuthor`)。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 为什么它属于**共享基座**而不是主 runner 的差异键:署名是**部署身份**([931]① clay 拍:
|
|
94
|
+
* 「署名 = 产品身份资产,归部署」),不是「哪一只 Runner 在跑」的属性。一个被委派出去的子代
|
|
95
|
+
* 提交进的是**同一个仓**、代表的是**同一个部署** —— 它的 commit 少一行 trailer 没有任何理由。
|
|
96
|
+
* 修前两条腿都漏:`main.ts` 与 `run-local.ts` 的 subRunner 都只在主 runner 上写了这一键,而两处
|
|
97
|
+
* 的「差异键」注释块逐条列了 sessionStore / checkpointStore / 四个座位,**都没提 hands**
|
|
98
|
+
* —— 也就是说它不是一次有理由的分歧,是漏配(与本函数头注记的 [1543]§三族A 同一个病族)。
|
|
99
|
+
* 由**调用方**给值(而不是在基座里按 `config` 现算):两条腿的判据本就不同(HTTP 腿按
|
|
100
|
+
* `configProvider === "local"` 分 branded/非 branded,run-local 恒是 Sema 本地形),
|
|
101
|
+
* 各自算一次、传进来一次,两只 Runner 自动同源。
|
|
102
|
+
*/
|
|
103
|
+
hands: RunnerDeps["hands"];
|
|
90
104
|
executionEnvFactory: RunnerDeps["executionEnvFactory"];
|
|
91
105
|
lspManager: RunnerDeps["lspManager"];
|
|
92
106
|
fleetBus: FleetEventBus;
|
|
@@ -102,7 +116,7 @@ export interface RunnerDepsCtx {
|
|
|
102
116
|
}
|
|
103
117
|
/** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
|
|
104
118
|
* 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
|
|
105
|
-
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "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">;
|
|
106
120
|
/**
|
|
107
121
|
* design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
|
|
108
122
|
* `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
|
|
@@ -131,7 +145,7 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "
|
|
|
131
145
|
* 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
|
|
132
146
|
*/
|
|
133
147
|
/** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
|
|
134
|
-
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "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">;
|
|
135
149
|
export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
|
|
136
150
|
export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
|
|
137
151
|
//# sourceMappingURL=runner-deps.d.ts.map
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -50,6 +50,8 @@ export function createSharedRunnerDeps(ctx) {
|
|
|
50
50
|
// [1070]① agent-team S1:持久名册 seam(具名 spawn advisory 写入;SendMessage 活注册表 miss 后咨询)。
|
|
51
51
|
rosterStore: ctx.rosterStore ? ctx.rosterStore : undefined,
|
|
52
52
|
hooks: ctx.deploymentHooks,
|
|
53
|
+
// 交接件⑤:署名座进基座 ⇒ 主/sub 两只 Runner 自动同源(理由见 RunnerDepsCtx.hands 的注)。
|
|
54
|
+
hands: ctx.hands,
|
|
53
55
|
toolResultStore: ctx.toolResultStore,
|
|
54
56
|
// E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
|
|
55
57
|
// carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
|
|
@@ -200,10 +202,9 @@ export function createRunnerDeps(ctx) {
|
|
|
200
202
|
// toolResultStore/sessionPolicyStore —— 共享基座展开,见 createSharedRunnerDeps 头注
|
|
201
203
|
// (含每键各自的历史 rationale 注释,搬到了那个函数里,不在此重复)。
|
|
202
204
|
...sharedRunnerDeps,
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
hands: config.configProvider === "local" ? { commitCoAuthor: "Sema <noreply@vivi-ai.com>" } : undefined,
|
|
205
|
+
// 交接件⑤:`hands` 已随共享基座展开(`...sharedRunnerDeps`)—— 这里**不再**手写同名键,
|
|
206
|
+
// 否则它会 override 展开、把 subRunner 重新甩开(本文件头注写死的那条:「不要再在任一调用点
|
|
207
|
+
// 手写同名键」)。判据([931]① branded 才署名)搬到了唯一的取值点 main.ts 的 `commitHands`。
|
|
207
208
|
// design/73 §1 (core 1.226 seam, clay 拍 2026-07-04 接): consume mechanical TaskOutcome facts into the
|
|
208
209
|
// outcome ledger — tidb/pg = SQL rows (coreOutcomeToLedgerRow mapping + verbatim `core_outcome` JSON so
|
|
209
210
|
// red-line ② oracleHadRedRun survives lossless), local = owner-only JSONL. Read-only v1: records facts,
|
package/dist/config-types.d.ts
CHANGED
|
@@ -552,8 +552,20 @@ export interface ServiceConfigFlat {
|
|
|
552
552
|
approvalAutoBudget: number;
|
|
553
553
|
/** design/80 D-E (inv#2 — safety asks are never budgetable): gated tools that ALWAYS require a human even when
|
|
554
554
|
* the auto-budget is on — the "irreversible / never auto-approve" set. empty = the budget may auto-approve any
|
|
555
|
-
* required tool
|
|
556
|
-
*
|
|
555
|
+
* required tool THIS layer sees. `APPROVAL_NEVER_AUTO`.
|
|
556
|
+
*
|
|
557
|
+
* 🔴 **NOT the only human-gate under an auto-budget**(2026-08-10 修口,#205 件2)。这里原来写的是
|
|
558
|
+
* "(In-service every gated ask is gate.kind=\"human\", so this set is the ONLY way to keep an irreversible
|
|
559
|
+
* tool human-gated under an auto-budget.)" —— 两句都已过期,且过期方向是**高估本旋钮的责任**:
|
|
560
|
+
* · design/80 D-2 起,带 egress / irreversibility 静态标记的工具铸的是 `irreversible_ask`,不是 `human`;
|
|
561
|
+
* · core 5.24.0(#130/#131/#120)起,带 `requiresRealApproval` 的 ask 一律铸
|
|
562
|
+
* `irreversible_ask` + `gate.realApproval:{origin}` + checkpoint v8 —— **不再铸可预算的 `human`**。
|
|
563
|
+
* 而 `requiresRealApproval` 的生产者(`createTranscriptIntegrityPolicy` / `createUnverifiableDeletePolicy`)
|
|
564
|
+
* 被 core 在 `prepare-task.js` 里**无条件**焊进每一条策略链,所以这条路径在**每一个**部署上都活着,
|
|
565
|
+
* 不是 org 治理部署的专属面。
|
|
566
|
+
* ⇒ 本旋钮承担的是**本层**(`createDurableAskPolicy` 的 per-leg 计数预算)的 inv#2;安全类 ask 另有
|
|
567
|
+
* core 侧那两层守卫兜底,两层各自独立、不互为前提。消费侧钉:`test/durable-approval-policy.test.ts`
|
|
568
|
+
* 的「#205 件2」describe(含反向锚:普通调用仍走预算自动批)。
|
|
557
569
|
*
|
|
558
570
|
* 🔴 **NOT required to be a subset of `approvalRequire`**(2026-07-31 修口)。这里原来写的是
|
|
559
571
|
* "Subset of approvalRequire",而 `approval.ts` 的 inv#2 执行面逐字相反:
|