@sema-agent/server 7.3.0 → 7.5.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 +63 -0
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +120 -16
- package/dist/approval-reconciler.js +146 -19
- package/dist/boot/coordinators.js +13 -3
- package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
- package/dist/boot/deferred-sandbox-path-env.js +279 -0
- package/dist/boot/execution-env.js +11 -1
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -2
- package/dist/boot/resolve-spec.js +175 -63
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/capabilities/center-prompts.js +4 -1
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-types.d.ts +32 -9
- package/dist/config.d.ts +6 -1
- package/dist/config.js +65 -12
- package/dist/elicitation.js +5 -1
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +6 -1
- package/dist/fleet/fleet-bus.js +25 -3
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +180 -24
- package/dist/http/routes/diagnostics.d.ts +84 -0
- package/dist/http/routes/diagnostics.js +145 -0
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +87 -29
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +29 -12
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/main.js +51 -8
- package/dist/observability/fail-open.d.ts +109 -0
- package/dist/observability/fail-open.js +227 -0
- package/dist/observability/prompt-manifest.d.ts +17 -0
- package/dist/observability/prompt-manifest.js +8 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
- package/dist/orchestration/workflow-notify-journal.js +137 -32
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +27 -5
- package/dist/plugins/approval-ask-store-sql.js +9 -2
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
- package/dist/plugins/checkpoint-store-sql.js +71 -11
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +57 -4
- package/dist/question.d.ts +18 -14
- package/dist/question.js +83 -34
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/task-settings.d.ts +16 -21
- package/dist/task-settings.js +22 -19
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +95 -30
- package/dist/trace/core-keyset-guard.d.ts +17 -3
- package/dist/trace/project.d.ts +36 -1
- package/dist/trace/project.js +55 -2
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -14
- package/dist/boot/lexical-path-env.js +0 -116
|
@@ -42,24 +42,82 @@ function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
|
|
|
42
42
|
]);
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* 这只 ask 是不是**祖先冻结 approver 层**在委派 fold 中途铸的那一份(#168 件1④,黑板 [2912]③)。
|
|
46
46
|
*
|
|
47
|
-
*
|
|
47
|
+
* 判别位是**产品自己发的**,不是外部约定:core 的 `withDelegationProvenance` 只包**子代自己那条缝**,
|
|
48
|
+
* 祖先层那次走未包装的原函数 ⇒ `AskRequest.delegation` 在不在,就是「这一份是哪一层发的」。落到行上,
|
|
49
|
+
* `delegation.parentToolCallId` 是必填字段,铸行时逐字透传进 `parent_tool_call_id` 列 ⇒ **列非 NULL
|
|
50
|
+
* ⇔ delegation 在场**(不需要新列,也不需要回 `req` 里再读一次)。
|
|
51
|
+
*
|
|
52
|
+
* 第二维 `sourceTaskId !== sessionId` 把「根腿」摘出去:根腿的 `sourceTaskId` 恒等于会话锚(core 给
|
|
53
|
+
* `AskRequest.sourceTaskId` 填的就是该腿 sessionId),它根本不在任何 fold 里,`parent_tool_call_id`
|
|
54
|
+
* 为 NULL 是它的常态而不是信号。
|
|
55
|
+
*
|
|
56
|
+
* 命中 ⇒ 退出硬相等(判据 1 结构上不命中,行落 ②③④⑤)。实测(test AI 围栏)checkpoint 存的摘要**逐字
|
|
57
|
+
* 等于子代自己那条缝**、只不等于祖先层那一次 —— 所以收窄到这一层,子代自己的缝照常参与。
|
|
58
|
+
*/
|
|
59
|
+
export function isAncestorFoldMint(ask) {
|
|
60
|
+
return ask.parentToolCallId === null && ask.sourceTaskId !== ask.sessionId;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 判据 1 的**硬谓词**(纯函数,§9 C2 + §8 D-2;#168 件1 起返回分类而不是布尔)。
|
|
64
|
+
*
|
|
65
|
+
* 逐条:
|
|
66
|
+
* - 祖先层 fold 中途铸点 ⇒ 直接退出(见 {@link isAncestorFoldMint});
|
|
48
67
|
* - `unparseable` 候选直接出局(读不出 ⇒ 不确定 ⇒ 不命中);
|
|
49
|
-
* - `
|
|
50
|
-
*
|
|
51
|
-
*
|
|
68
|
+
* - **身份三元组**:`sourceTaskId` **相等** ∧ `toolCallId` **相等** ∧ 时间维的**因果下界**
|
|
69
|
+
* `candidate.createdAtMs >= ask.createdAtMs`(⚠️ 第三维**不是等式** —— 把它读成等式会让合法的 park
|
|
70
|
+
* 几乎命不中,行随后落 ②③⑤ 被误判)—— 摘要**不是**身份([2897]③①:
|
|
71
|
+
* 同 args 的两次调用摘要天然相同,只靠摘要硬相等会把第二次投递并进第一次的票);任一维在候选侧
|
|
72
|
+
* 缺席(读不出的 blob 没有 `sourceTaskId`)即身份不成立;
|
|
73
|
+
* - **hash 双等**:两侧都必须在场且相等,任一侧缺席一律不 bind;禁「能取到时才比」的可选谓词。
|
|
74
|
+
* 归因分两级(顺序即代码顺序):身份这一层先判 —— 同身份候选为空**且**候选集非空 ⇒ `identity_miss`;
|
|
75
|
+
* 走到 hash 这一层才把缺席记成**单铸** `single_mint`(见 {@link GateMatchOutcome})。
|
|
52
76
|
*
|
|
53
77
|
* 多候选时的取舍:优先 `status === "pending"`(活着的那张 gate),否则取最早的一条(读口按
|
|
54
78
|
* `created_at ASC` 返回)。两者都满足全部硬谓词,选谁都不会错配;取 pending 只是让 `PARKED` 行落到
|
|
55
79
|
* 一个还能被 resume 的坐标上,对壳更有用。
|
|
56
80
|
*/
|
|
57
|
-
export function
|
|
81
|
+
export function classifyGateMatch(ask, candidates) {
|
|
82
|
+
const outcome = classifyGateMatchBeforeFoldVeto(ask, candidates);
|
|
83
|
+
// 🔴 祖先层否决**排在最后**、而且只否决已经成立的命中(纯减法)。放在最前面判会让这条谓词的每一次
|
|
84
|
+
// 假阳性(`sourceTaskId ≠ sessionId` 还有别的成因,例如 session-less 的 adhoc 腿)都白白关掉判据 1,
|
|
85
|
+
// 并把一条本该记成 `single_mint`/`identity_miss` 的行错记成「祖先层铸点」。放在这里,它最坏只是**不
|
|
86
|
+
// 命中**(与判据 1 的既有 fail-safe 同向),而计数从此只统计「本来会 bind、被这条裁定拦下」的行 ——
|
|
87
|
+
// 非零读数因此是真信号,不是噪声底。
|
|
88
|
+
if (outcome.kind === "match" && isAncestorFoldMint(ask))
|
|
89
|
+
return { kind: "ancestor_fold_mint" };
|
|
90
|
+
return outcome;
|
|
91
|
+
}
|
|
92
|
+
function classifyGateMatchBeforeFoldVeto(ask, candidates) {
|
|
93
|
+
// 身份三元组的前两维 + 因果下界(摘要不参与这一层——它是身份之外的**第二道**等式)。
|
|
94
|
+
const sameIdentity = candidates.filter((c) => c.unparseable !== true && c.sourceTaskId !== null && c.sourceTaskId === ask.sourceTaskId && c.boundCallId === ask.toolCallId && c.createdAtMs >= ask.createdAtMs);
|
|
58
95
|
const askHash = ask.boundInputHash;
|
|
96
|
+
if (sameIdentity.length === 0) {
|
|
97
|
+
// 「同身份的 checkpoint 一条都没有」有两种成因,不能混:
|
|
98
|
+
// · 候选集**空** ⇒ 这条弧上根本没有对家,只有 ask 侧铸过(或两侧都没铸)= 单铸;
|
|
99
|
+
// · 候选集非空 ⇒ 有对家、只是不是这一只(别的 sourceTaskId/callId),或者读不出(`unparseable`,
|
|
100
|
+
// 「不确定」)。两者都**不是**「只有一侧铸过」这个事实陈述,一律记 `identity_miss`——
|
|
101
|
+
// 把一条读不出的坏行说成「对家从来不存在」是拿不确定冒充确定。
|
|
102
|
+
if (candidates.length > 0)
|
|
103
|
+
return { kind: "identity_miss" };
|
|
104
|
+
return { kind: "single_mint", side: askHash === null ? "neither" : "ask_only" };
|
|
105
|
+
}
|
|
106
|
+
const withHash = sameIdentity.filter((c) => c.boundInputHash !== null);
|
|
59
107
|
if (askHash === null)
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
108
|
+
return { kind: "single_mint", side: withHash.length > 0 ? "checkpoint_only" : "neither" };
|
|
109
|
+
if (withHash.length === 0)
|
|
110
|
+
return { kind: "single_mint", side: "ask_only" }; // 同身份的 park 一条摘要都没有
|
|
111
|
+
const matches = withHash.filter((c) => c.boundInputHash === askHash);
|
|
112
|
+
const picked = matches.find((c) => c.status === "pending") ?? matches[0];
|
|
113
|
+
if (picked !== undefined)
|
|
114
|
+
return { kind: "match", candidate: picked };
|
|
115
|
+
return { kind: "hash_mismatch", candidates: withHash.length };
|
|
116
|
+
}
|
|
117
|
+
/** {@link classifyGateMatch} 的布尔面(判据 1 命中即返回那条候选)。分类信息由调用方按需另取。 */
|
|
118
|
+
export function selectGateCandidate(ask, candidates) {
|
|
119
|
+
const outcome = classifyGateMatch(ask, candidates);
|
|
120
|
+
return outcome.kind === "match" ? outcome.candidate : undefined;
|
|
63
121
|
}
|
|
64
122
|
/** 判据表 v2 的判定(纯函数;顺序 = 设计稿 §9 尾的五臂汇总,注见文件头)。 */
|
|
65
123
|
export function decideReconcileAction(input) {
|
|
@@ -119,7 +177,8 @@ export function createApprovalReconciler(deps) {
|
|
|
119
177
|
}
|
|
120
178
|
}
|
|
121
179
|
};
|
|
122
|
-
/** 一条 `PARKING` 行的收敛(读事实 → 纯判定 → 一次 CAS)。返回本行落到哪一臂(
|
|
180
|
+
/** 一条 `PARKING` 行的收敛(读事实 → 纯判定 → 一次 CAS)。返回本行落到哪一臂 + 判据 1 的分类(供统计;
|
|
181
|
+
* `match` 缺席 = 本轮压根没走到判据 1,与「走了但没命中」是两回事,不能记成同一格)。 */
|
|
123
182
|
const reconcileOne = async (ask, nowMs) => {
|
|
124
183
|
// ⓪ 批态**先读**(codex round3 R3-2):它是唯一一个能让本行「结构上再也不可能 bind」的事实,而且是
|
|
125
184
|
// 一次主键读。批已 `ABORTED` 或已绑给**别人** ⇒ 判据 1 的 `bindBatch` 谓词(`ROUTING_UNBOUND`)
|
|
@@ -133,9 +192,42 @@ export function createApprovalReconciler(deps) {
|
|
|
133
192
|
// 判据 1:窄谓词精确查(scope 必填 —— 租户门是读口自己的责任,§8 D-3)。因果下界直接下推成
|
|
134
193
|
// `sinceMs`,SQL 侧就把早于本 ask 的 checkpoint 排掉了(`selectGateCandidate` 里还会再判一次,
|
|
135
194
|
// 纵深防御)。
|
|
195
|
+
let gateMatch;
|
|
136
196
|
if (checkpoints && !irreversibleBatch) {
|
|
197
|
+
// 🔴 **已知缺口,登记而不假称已解**(codex 交叉复审 2026-08-07 [high] 一,验真;**先存**,不是本批
|
|
198
|
+
// 引入):这里的 session 维传的是 `ask.sessionId` = **投递上下文**的会话(`tool-approval.ts` 铸行时
|
|
199
|
+
// 取 `primary.sessionId`),而委派子代那条腿的 checkpoint 落在**子代自己**的 sessionId 上
|
|
200
|
+
// (core 铸 `cp.sessionId = 该腿 sessionId`,同值也写进 `cp.sourceTaskId`)。⇒ 子代自己那条 approver
|
|
201
|
+
// 缝的 park 在 SQL 谓词这一层就被滤掉了,判据 1 对委派腿结构上够不着 —— 黑板 [2912]③「子代自己那条
|
|
202
|
+
// 缝可以参与硬相等」的那一半,在本仓当前接线下还落不了地(行改走 ②③④⑤,fail-safe)。
|
|
203
|
+
//
|
|
204
|
+
// 顺带说清本批**改善**了什么:三元组把 `cp.sourceTaskId === ask.sourceTaskId` 变成硬谓词之后,
|
|
205
|
+
// 一只子代 ask 再也不可能认领**宿主**腿的 park(同 session 查出来的候选,callId 撞车 + 同 args 摘要
|
|
206
|
+
// 相同就够了,而 `PARKED` 不可回滚)——那是本批之前真实存在的错配面。
|
|
207
|
+
//
|
|
208
|
+
// 修法(未做,需自带判据):把 session 维换成「铸 park 的那条腿的会话」。风险在于 `ask.sourceTaskId`
|
|
209
|
+
// 有 `?? origin.taskId` 兜底,core 万一不填就会把一个 taskId 当 session 去查(比今天更差),所以
|
|
210
|
+
// **必须**配一条用真 checkpoint 店、宿主/子代两个不同 sessionId 的端到端钉才动。开题上报。
|
|
137
211
|
const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
|
|
138
|
-
|
|
212
|
+
gateMatch = classifyGateMatch(ask, candidates);
|
|
213
|
+
if (gateMatch.kind === "hash_mismatch") {
|
|
214
|
+
// 留痕不拒(见 ReconcileStats.hashMismatch 顶注):warn 说清成因方向,行继续按 ②③④⑤ 判。
|
|
215
|
+
try {
|
|
216
|
+
logger.warn("approval_reconcile_hash_mismatch", {
|
|
217
|
+
askId: ask.askId,
|
|
218
|
+
taskId: ask.taskId,
|
|
219
|
+
toolCallId: ask.toolCallId,
|
|
220
|
+
candidates: gateMatch.candidates,
|
|
221
|
+
reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
|
|
222
|
+
"the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
|
|
223
|
+
"or a stateful getter returning a fresh value per read), not tampering; this ask keeps its fail-safe route (no park, no denial)",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* 可观测面绝不成为故障源 */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
|
|
139
231
|
if (match) {
|
|
140
232
|
// 🔴 `bindBatch` 内部**一次**完成 `PARKING→PARKED` + gate 落行 + 兄弟连坐 VOID(§8 A-2:正文
|
|
141
233
|
// 「先 transitionAsk 再 bindBatch」是死锁形,已作废)。只调它一次。
|
|
@@ -146,7 +238,7 @@ export function createApprovalReconciler(deps) {
|
|
|
146
238
|
}), "bindBatch");
|
|
147
239
|
if (res.ok) {
|
|
148
240
|
revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
|
|
149
|
-
return "parked";
|
|
241
|
+
return { outcome: "parked", match: gateMatch };
|
|
150
242
|
}
|
|
151
243
|
// 判别式失败臂(§9 C3):拿到批的**真实**态 + 中选者,降级续判 —— `ABORTED` ⇒ VOID(abort 语义),
|
|
152
244
|
// `ROUTING_BOUND` 且中选者是兄弟 ⇒ VOID(bind-once 落选者,R2-3),其余按 ②③④⑤。
|
|
@@ -162,13 +254,13 @@ export function createApprovalReconciler(deps) {
|
|
|
162
254
|
case "bind": {
|
|
163
255
|
// `allowBind: false` 下结构上不可达;留一条防御性 hold,绝不在这里第二次调 bindBatch。
|
|
164
256
|
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.version, nowMs), "deferReconcile");
|
|
165
|
-
return "held";
|
|
257
|
+
return { outcome: "held", match: gateMatch };
|
|
166
258
|
}
|
|
167
259
|
case "deny": {
|
|
168
260
|
// 🔴 全仓唯一的 `DENIED` 写点(grep 钉)。归因是判据 2 的**终局证据**,不是超时推断。
|
|
169
261
|
// CAS 输(行在本轮判定期间被别人收走)= 正常,如实记成 held(没动过行),下轮重扫。
|
|
170
262
|
const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "DENIED", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(DENIED)");
|
|
171
|
-
return won ? "denied" : "held";
|
|
263
|
+
return { outcome: won ? "denied" : "held", match: gateMatch };
|
|
172
264
|
}
|
|
173
265
|
case "void": {
|
|
174
266
|
// 🔴 codex 交叉复审 C3(2026-08-06 真缺陷):**必须看 CAS 的返回值**。原先无条件记账 + 发撤卡帧
|
|
@@ -177,7 +269,7 @@ export function createApprovalReconciler(deps) {
|
|
|
177
269
|
// 壳会清掉一张仍然有效、还在等 gate 的卡。输 ⇒ 什么都不做,下轮按新态重判。
|
|
178
270
|
const won = await withDeadline(askStore.transitionAsk(ask.askId, "PARKING", "VOID", { deniedReason: action.reason, updatedAtMs: nowMs }), "transitionAsk(VOID)");
|
|
179
271
|
if (!won)
|
|
180
|
-
return "held";
|
|
272
|
+
return { outcome: "held", match: gateMatch };
|
|
181
273
|
if (action.reason === VOID_REASONS.ORPHAN_TTL_EXCEEDED) {
|
|
182
274
|
logger.warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
|
|
183
275
|
}
|
|
@@ -190,19 +282,34 @@ export function createApprovalReconciler(deps) {
|
|
|
190
282
|
// 落选者(R2-3)是**真的**被别人的 park 顶掉 ⇒ 用 `superseded_by_park`;其余单行 VOID 用 `"aborted"`
|
|
191
283
|
// (两员闭集里唯一诚实的那个,见上注)。
|
|
192
284
|
revoke(ask, [ask.askId], action.reason === VOID_REASONS.BATCH_BOUND_ELSEWHERE ? "superseded_by_park" : "aborted", nowMs);
|
|
193
|
-
return "voided";
|
|
285
|
+
return { outcome: "voided", match: gateMatch };
|
|
194
286
|
}
|
|
195
287
|
case "hold": {
|
|
196
288
|
// 队列轮转(§8 D-4 / §9 C5):推 `updated_at_ms` 排到队尾。CAS 输(版本已被别人推进)= 正常,
|
|
197
289
|
// 本轮判定已过期,下轮重来。
|
|
198
290
|
await withDeadline(askStore.deferReconcile(ask.askId, "PARKING", ask.version, nowMs), "deferReconcile");
|
|
199
|
-
return "held";
|
|
291
|
+
return { outcome: "held", match: gateMatch };
|
|
200
292
|
}
|
|
201
293
|
}
|
|
202
294
|
};
|
|
203
295
|
return {
|
|
204
296
|
async runOnce(nowMs) {
|
|
205
|
-
const stats = {
|
|
297
|
+
const stats = {
|
|
298
|
+
scanned: 0,
|
|
299
|
+
unmatchableNoHash: 0,
|
|
300
|
+
hashMismatch: 0,
|
|
301
|
+
singleMintAskOnly: 0,
|
|
302
|
+
singleMintCheckpointOnly: 0,
|
|
303
|
+
singleMintNeither: 0,
|
|
304
|
+
ancestorFoldMint: 0,
|
|
305
|
+
parked: 0,
|
|
306
|
+
denied: 0,
|
|
307
|
+
voided: 0,
|
|
308
|
+
held: 0,
|
|
309
|
+
failed: 0,
|
|
310
|
+
budgetExhausted: 0,
|
|
311
|
+
orphansExpired: 0,
|
|
312
|
+
};
|
|
206
313
|
// ── 段一:PARKING 判据表 ────────────────────────────────────────────────────────────────
|
|
207
314
|
const parking = await withDeadline(askStore.listByState("PARKING", batchLimit), "listByState(PARKING)");
|
|
208
315
|
stats.scanned = parking.length;
|
|
@@ -221,7 +328,7 @@ export function createApprovalReconciler(deps) {
|
|
|
221
328
|
if (ask.boundInputHash === null)
|
|
222
329
|
stats.unmatchableNoHash += 1; // 判据 1 盲区(见字段顶注)
|
|
223
330
|
try {
|
|
224
|
-
const outcome = await reconcileOne(ask, nowMs);
|
|
331
|
+
const { outcome, match } = await reconcileOne(ask, nowMs);
|
|
225
332
|
if (outcome === "parked")
|
|
226
333
|
stats.parked += 1;
|
|
227
334
|
else if (outcome === "denied")
|
|
@@ -230,6 +337,26 @@ export function createApprovalReconciler(deps) {
|
|
|
230
337
|
stats.voided += 1;
|
|
231
338
|
else
|
|
232
339
|
stats.held += 1;
|
|
340
|
+
// 判据 1 的分类计数(#168 件1②③④)——`match` 缺席 = 本轮没走到判据 1(无 checkpoint 面 /
|
|
341
|
+
// 批已不可逆),那既不是 mismatch 也不是单铸,一格都不记。
|
|
342
|
+
switch (match?.kind) {
|
|
343
|
+
case "hash_mismatch":
|
|
344
|
+
stats.hashMismatch += 1;
|
|
345
|
+
break;
|
|
346
|
+
case "ancestor_fold_mint":
|
|
347
|
+
stats.ancestorFoldMint += 1;
|
|
348
|
+
break;
|
|
349
|
+
case "single_mint":
|
|
350
|
+
if (match.side === "ask_only")
|
|
351
|
+
stats.singleMintAskOnly += 1;
|
|
352
|
+
else if (match.side === "checkpoint_only")
|
|
353
|
+
stats.singleMintCheckpointOnly += 1;
|
|
354
|
+
else
|
|
355
|
+
stats.singleMintNeither += 1;
|
|
356
|
+
break;
|
|
357
|
+
default:
|
|
358
|
+
break; // match / identity_miss / 未走到判据 1:无计数格
|
|
359
|
+
}
|
|
233
360
|
}
|
|
234
361
|
catch (err) {
|
|
235
362
|
// 🔴 per-row 隔离(邻居 bg-agent per-scope 先例):一条行的坏 blob / 一次瞬时错误不许饿死同批
|
|
@@ -17,7 +17,9 @@ export function createLiveCoordinators(ctx) {
|
|
|
17
17
|
// `RunnerDeps.onQuestion` (core mounts the AskUserQuestion tool + routes live asks here) AND consumed by the HTTP layer
|
|
18
18
|
// (the `POST /v1/questions/:id/respond` route + the per-run ALS context wraps on the streaming legs). Absent ⇒ core
|
|
19
19
|
// mounts the tool with the headless default (a run never hangs; the model just can't get a live answer).
|
|
20
|
-
|
|
20
|
+
// #166 裁2 接线:三旋钮(QUESTION_TTL_MS / QUESTION_MAX_*_PER_RUN)从 config 通到协调器;缺席=构造器
|
|
21
|
+
// 内建 DEFAULT_QUESTION_THROTTLE,默认值恒等钉在 test/question-unavailable.test.ts。
|
|
22
|
+
const question = config.askQuestionEnabled ? new QuestionCoordinator(config.questionThrottle) : undefined;
|
|
21
23
|
// [816]/[820]②: the live tool-approval HITL coordinator — the sibling of `question` on core's `RunnerDeps.onAsk`
|
|
22
24
|
// seam (1.290 sync-ask leg). Present ⇒ a policy `ask` on a LIVE streaming leg becomes a `tool_approval` frame the
|
|
23
25
|
// shell renders as the CC three-choice card, answered via POST /v1/tool-approvals/:id/respond.
|
|
@@ -41,7 +43,11 @@ export function createLiveCoordinators(ctx) {
|
|
|
41
43
|
parkFacility: durableEnabled,
|
|
42
44
|
});
|
|
43
45
|
// §8.4:协议**开着**却上不了场 = 合法部署形(不是配置错误)⇒ 一行 **info**(不是 warn),把原因说明白。
|
|
44
|
-
//
|
|
46
|
+
// 反过来「开关本来就没开」不打日志:显式关掉的部署自己知道为什么关,一行噪音没有信息量。
|
|
47
|
+
// ⚠️ 读数已反转:总开关默认在树上已是 ON(版本坐标见 config.ts 的 `streamApprovalConfig()` 头注),
|
|
48
|
+
// 所以这一行在**默认部署**上是常态 —— 任何没有 durable
|
|
49
|
+
// backend / park 设施的默认部署每次启动都会打一行 `stream_approval_disabled`(每次启动一行,不是每
|
|
50
|
+
// tick)。写这条判据时的前提「默认形绕开本路径」已不成立;要收紧,收紧点在本 if 而不是在开关处。
|
|
45
51
|
if (config.streamApproval.enabled && !streamApprovalGate.active) {
|
|
46
52
|
logger.info("stream_approval_disabled", {
|
|
47
53
|
reason: streamApprovalGate.reason,
|
|
@@ -53,8 +59,12 @@ export function createLiveCoordinators(ctx) {
|
|
|
53
59
|
}
|
|
54
60
|
const toolApproval = config.toolApprovalEnabled
|
|
55
61
|
? new ToolApprovalCoordinator({
|
|
56
|
-
// 协议上场 ⇒ 持久层 +
|
|
62
|
+
// 协议上场 ⇒ 持久层 + 配置窗(§7:`STREAM_ASK_WINDOW_MS` **只在开关内**生效,
|
|
57
63
|
// `DEFAULT_APPROVAL_TTL_MS` 一个字不动);不上场 ⇒ 两者都不传,窗保持既有 5min(D1 逐字不变)。
|
|
64
|
+
// ⚠️ 两臂的窗**默认已同长**:窗默认自 7.5.0 起 = 300000 = `DEFAULT_APPROVAL_TTL_MS`(clay 同裁把
|
|
65
|
+
// 「窗变短」从翻真里摘出去)。原注画的「上场 = 60s / 不上场 = 5min」这个对比在默认配置下不存在
|
|
66
|
+
// 了,别据它得出「开了协议窗会缩短」的运维结论:上场与否改变的是**持久层与协议帧**,窗只有在
|
|
67
|
+
// 部署显式设了 `STREAM_ASK_WINDOW_MS` 时才分叉。
|
|
58
68
|
...(streamApprovalGate.active ? { askStore: streamApprovalGate.askStore, ttlMs: config.streamApproval.windowMs } : {}),
|
|
59
69
|
// 余量旋钮恒传(既有键,窗长三元公式的第二项;legDeadlineMonotonic 缺席时它不参与计算)。
|
|
60
70
|
windowMarginMs: config.streamAskWindowMarginMs,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FileError, StubExecutionEnv, type ExecutionEnv, type ExecutionEnvFactory, type FileInfo, type Result } from "@sema-agent/core";
|
|
2
|
+
/** 哪些 lane 用本模块的代理裁决写门 —— `REMOTE_EXEC` 未设(进程内 host)与显式 `host` 之外的全部。
|
|
3
|
+
* 与 resolve-spec 的 `hostSemanticsLane` 是同一判别式的两面,取值处**只此一个**(两处各写一份正是漂移的成因)。 */
|
|
4
|
+
export declare function isSandboxPathAdjudicationLane(provider: string | undefined): boolean;
|
|
5
|
+
/** slot 取值的三态。`ok:false` 的两支都是 fail-closed,分开只为让留痕说得出**哪一种**缺席。 */
|
|
6
|
+
export type SandboxPathEnvLookup = {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly env: ExecutionEnv;
|
|
9
|
+
} | {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly reason: "no_session_id" | "unbound";
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* per-session 的真 env 登记簿 —— 工厂装饰器写,写门代理读。
|
|
15
|
+
*
|
|
16
|
+
* **同键改铸 = 顶替**(不排队、不判歧义)。依据是一条被两道门执法的不变量:一个 session 同时只有一条
|
|
17
|
+
* 活腿(HTTP 层 `conflict.session_active_run` + core 的 session acquire),而委派子代用的是**另一个**
|
|
18
|
+
* sessionId。所以工厂为某 session 铸出新 env 的那一刻,同键上还留着的登记按构造已经是死腿——最典型的
|
|
19
|
+
* 产地是 **park-only 车道**(非 suspendable 的远程 env:durable park 既不 `suspendVM` 也不 `destroy`,
|
|
20
|
+
* core 的 `teardownOwnedEnv` 在 checkpoint token 在场时整条跳过)。留着它只会让 resume 腿整轮拿不到
|
|
21
|
+
* 真 env(写门恒 fail-closed);顶替按**实例身份**收口,被顶掉那一方迟到的注销全部落空,不误伤后继。
|
|
22
|
+
*/
|
|
23
|
+
export declare class SandboxPathEnvSlots {
|
|
24
|
+
/** **弱持有**:登记簿只在「别人还用着这个 env」期间指向它。跑着的任务由 core 强持有(`prepared.ownedEnv`),
|
|
25
|
+
* 所以在场腿的裁决永远解得出;而一条 park 后再没人来 resume 的死腿,其 env 一旦无人引用即可被回收,
|
|
26
|
+
* 条目在下一次清扫时消失。强持有会把它们连同各自的 adapter 连接一起钉死到进程结束,并在 4096 条之后
|
|
27
|
+
* 让**每一个**新 session 拿不到真 env(守卫集下 = 每一次结构化写都 deny)—— 那是一道会自己关上的门。 */
|
|
28
|
+
private readonly bySession;
|
|
29
|
+
/** 登记一个刚铸出的 env(同键顶替),返回**幂等**的注销闭包;到达上限而拒收时返回 `undefined`。
|
|
30
|
+
* 注销按**实例身份**摘除,所以一个迟到的 destroy 不会误伤同键的后继 env。 */
|
|
31
|
+
bind(sessionId: string, env: ExecutionEnv): (() => void) | undefined;
|
|
32
|
+
resolve(sessionId: string | undefined): SandboxPathEnvLookup;
|
|
33
|
+
/** 登记簿规模(上限行为的可观测面;测试与运维探针用)。含尚未清扫的死条目。 */
|
|
34
|
+
get boundSessions(): number;
|
|
35
|
+
/** 清掉 env 已被回收的条目。只在触到上限时跑一遍(O(n) 的代价换掉一次拒收)。 */
|
|
36
|
+
private sweepCollected;
|
|
37
|
+
}
|
|
38
|
+
/** 进程内唯一的登记簿:工厂装饰器(boot/execution-env.ts)与写门 wiring(boot/resolve-spec.ts)分处
|
|
39
|
+
* 装配链两端,而中间的 core 只肯传 `ExecutionEnvFactoryContext`——两端共享同一个实例是它们唯一的会合点。
|
|
40
|
+
* 测试要隔离时自建 {@link SandboxPathEnvSlots} 实例注入即可(两个消费点都收可选参)。 */
|
|
41
|
+
export declare const sandboxPathEnvSlots: SandboxPathEnvSlots;
|
|
42
|
+
/** 结构化留痕面(与服务 `logger` 同形,调用处 optional-chain —— 与 remote-scratchpad 装饰器一致)。 */
|
|
43
|
+
export interface SandboxPathEnvLogger {
|
|
44
|
+
warn?(event: string, fields?: Record<string, unknown>): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 装饰工厂:每铸出一个 env 就按 `ctx.sessionId` 登记(同键顶替,见 {@link SandboxPathEnvSlots}),
|
|
48
|
+
* 终态时注销。
|
|
49
|
+
*
|
|
50
|
+
* 挂**最外层**(装配链尾):内层装饰器可能**换掉**env 实例(worktree 隔离的 `rootEnvAt`)或改写它的
|
|
51
|
+
* 方法(scratchpad 的 exec/canonicalPath 前置),写门要裁决的是 core 最终拿到手的那一个。
|
|
52
|
+
*
|
|
53
|
+
* 生命周期三面(顺序即语义):
|
|
54
|
+
* · `destroy` ⇒ 注销。
|
|
55
|
+
* · `suspendVM` 成功 ⇒ 注销。core 在挂起时**跳过** destroy(runtask 留着 env 做快照),所以这是可挂起
|
|
56
|
+
* 车道唯一的及时腾位点:挂起期该 session 没有活腿,登记留着只是白占登记簿容量。
|
|
57
|
+
* · `resumeVM` 成功 ⇒ **重新登记**。挂起并不必然终结这一轮:`commitSuspendSaga` 的 checkpoint 写失败
|
|
58
|
+
* 臂会 `resumeVM` + `postResumeInit` 把**同一个实例**复活,然后作废本次挂起让 run 继续跑
|
|
59
|
+
* (core dist 亲读)。只注销不复登记,一次瞬时 checkpoint 故障就会让这条 run 之后每一次写都失去真身
|
|
60
|
+
* 裁决(方向 fail-closed,但整轮写面被毒死)。复登记幂等:仍在场时是 no-op,所以正常 resume 腿
|
|
61
|
+
* (新 env 在工厂处已登记、随后被 core `resumeVM` 复原快照)照旧不动;后继腿已接管该 session 时也不抢回。
|
|
62
|
+
*
|
|
63
|
+
* 两个终态面都缺席的 env(五条沙箱 adapter 都不是这一形)照样登记——正确性优先于回收:不登记等于
|
|
64
|
+
* 让那条 lane 的写门恒 fail-closed。回收兜底=登记簿自己的上限。
|
|
65
|
+
*/
|
|
66
|
+
export declare function withSandboxPathEnvSlot(factory: ExecutionEnvFactory, slots: SandboxPathEnvSlots, logger?: SandboxPathEnvLogger): ExecutionEnvFactory;
|
|
67
|
+
export interface DeferredSandboxPathEnvOptions {
|
|
68
|
+
/** slot 键。缺席(= 部署没接 authorizer,`main.ts` 不产生这种形)⇒ 转发面恒 fail-closed。 */
|
|
69
|
+
sessionId: string | undefined;
|
|
70
|
+
slots: SandboxPathEnvSlots;
|
|
71
|
+
logger?: SandboxPathEnvLogger;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 写门用的 ExecutionEnv 代理(见文件头)。fs **读**原语转发 slot 里的真 env,`absolutePath` 保留词法形,
|
|
75
|
+
* 其余一切(写面 / shell 面 / listDir …)继承 `StubExecutionEnv` 的 `not_supported`。
|
|
76
|
+
*
|
|
77
|
+
* **继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会随 `StubExecutionEnv`
|
|
78
|
+
* 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
79
|
+
*/
|
|
80
|
+
export declare class DeferredSandboxPathEnv extends StubExecutionEnv {
|
|
81
|
+
private readonly sessionId;
|
|
82
|
+
private readonly slots;
|
|
83
|
+
private readonly logger;
|
|
84
|
+
/** 每实例一次的留痕闸:裁决面每个写目标要走 3-4 次转发,逐次 warn 会把日志刷成噪声。 */
|
|
85
|
+
private warned;
|
|
86
|
+
constructor(opts: DeferredSandboxPathEnvOptions);
|
|
87
|
+
/** 取真 env;取不到就地铸 fail-closed 错误(并留痕一次)。 */
|
|
88
|
+
private bound;
|
|
89
|
+
/** 词法一面,**不**转发 —— 理由见文件头裁定 1。 */
|
|
90
|
+
absolutePath(path: string): Promise<Result<string, FileError>>;
|
|
91
|
+
exists(path: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
|
|
92
|
+
canonicalPath(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
93
|
+
fileInfo(path: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
|
|
94
|
+
/** `readLink` 在 `ExecutionEnv` 上是**可选**面。本代理恒定义它,于是 core 永远走不到自己的
|
|
95
|
+
* 「env 没有 readLink」腿——真 env 缺这一面时由此处答错误,落到 core 同一条 `unresolvedSymlink` 出口,
|
|
96
|
+
* 裁决结果与那条腿完全一致。 */
|
|
97
|
+
readLink(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=deferred-sandbox-path-env.d.ts.map
|