@sema-agent/server 7.8.1 → 7.10.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 +6 -0
- package/dist/approval-reconciler.d.ts +1 -1
- package/dist/approval-reconciler.js +13 -5
- package/dist/boot/execution-env.js +6 -0
- package/dist/boot/reapers.js +3 -3
- package/dist/capabilities/repo-tools.d.ts +36 -2
- package/dist/capabilities/repo-tools.js +125 -11
- package/dist/capabilities/scenarios.d.ts +2 -2
- package/dist/capabilities/scenarios.js +11 -2
- package/dist/config-types.d.ts +15 -6
- package/dist/config.js +43 -2
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.js +1 -1
- package/dist/git-api-kind.d.ts +7 -0
- package/dist/git-api-kind.js +8 -0
- package/dist/hooks/hook-runner.js +41 -26
- package/dist/http/routes/approvals-assistant.d.ts +7 -0
- package/dist/http/routes/approvals-assistant.js +9 -2
- package/dist/http/routes/side-query.d.ts +4 -0
- package/dist/http/routes/side-query.js +85 -5
- package/dist/http/routes/workflows.js +16 -1
- package/dist/http/server.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +2 -2
- package/dist/memory-scope.d.ts +16 -0
- package/dist/memory-scope.js +42 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +9 -9
- package/dist/plugins/checkpoint-store-sql.js +32 -31
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +5 -4
- package/dist/plugins/memory-engine-pg.js +13 -3
- package/dist/plugins/memory-engine-tidb.js +11 -0
- package/dist/plugins/memory-key-guards.d.ts +8 -0
- package/dist/plugins/memory-key-guards.js +13 -0
- package/dist/plugins/pg-pool.js +7 -7
- package/dist/plugins/remote-env-k8s.js +15 -4
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +3 -3
- package/dist/plugins/tidb-pool.js +8 -8
- package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
- package/dist/plugins/workflow-journal-store-sql.js +6 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-run-store-sql.js +12 -12
- package/dist/run-local.js +18 -5
- package/dist/tool-approval.d.ts +9 -0
- package/dist/tool-approval.js +10 -0
- package/package.json +2 -2
|
@@ -499,35 +499,50 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
499
499
|
// 顺序是承重的 —— 包装句逐字预设会话在上文,放反了那句话本身就是在骗模型。
|
|
500
500
|
const prompt = extra ? `${extra.transcript}\n\n---\n\n${wrapCondition("Stop", substituted)}` : substituted;
|
|
501
501
|
// 载体外再包一层硬顶:契约说载体自己兜超时,但一个部署组装 bug 不该能挂死工具门(纵深)。
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}),
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
502
|
+
// A-002.12:硬顶只 unref 不清 ⇒ 每次被门到的工具调用留一只带闭包的定时器,最长挂
|
|
503
|
+
// MAX_HOOK_TIMEOUT_SECONDS+5s=605s(工具门是热路径,一个任务几十上百次)。整个函数收在
|
|
504
|
+
// try/finally 里,任何出口都 clearTimeout。
|
|
505
|
+
// 🔴 硬顶**一只到底**、两次 invoke 共用(语义原样不动):它兜的是「本次 hook 判决整体不许挂死」,
|
|
506
|
+
// 不是「每次载体调用各给一个新窗」——改成 per-invoke 会把最坏墙钟翻倍。
|
|
507
|
+
let hardTopTimer;
|
|
508
|
+
const hardTop = new Promise((r) => {
|
|
509
|
+
hardTopTimer = setTimeout(() => r({ ok: false, error: "hook llm hard-timeout backstop fired (carrier did not settle within timeoutMs+5s — deployment assembly bug)", code: "hard_timeout" }), timeoutMs + 5_000);
|
|
510
|
+
hardTopTimer.unref?.();
|
|
511
|
+
});
|
|
512
|
+
try {
|
|
513
|
+
const invoke = () => Promise.race([
|
|
514
|
+
call({
|
|
515
|
+
prompt,
|
|
516
|
+
...(entry.model ? { model: entry.model } : {}),
|
|
517
|
+
timeoutMs,
|
|
518
|
+
...(extra ? { system: extra.system, maxOutputTokens: CC_EVALUATOR_MAX_OUTPUT_TOKENS } : {}),
|
|
519
|
+
}),
|
|
520
|
+
hardTop,
|
|
521
|
+
]).catch((e) => ({ ok: false, error: String(e) }));
|
|
522
|
+
let res = await invoke();
|
|
523
|
+
// 🔴 **"一个字都没吐出来"重试一次**(仅 CC 评估者路)。这个失败形的后果是**判词被丢弃 ⇒ fail-open**
|
|
524
|
+
// (该拦没拦),而它是**瞬态**的(推理档模型偶尔把额度用在 thinking 上)。
|
|
525
|
+
// ⚠️ 只重试**无内容**,不重试"有内容但读不懂" —— 后者重试一次多半还是读不懂,而且那一格按设计就该放行。
|
|
526
|
+
if (extra && !res.ok && res.code === "no_content") { // B8:判别走码不走文案(v3.1 批2)
|
|
527
|
+
ctx.logger.warn("hook_llm_no_content_retry", { event: "Stop" });
|
|
528
|
+
res = await invoke();
|
|
529
|
+
// 重试之后**仍然**没有内容 ⇒ 这一轮的守卫确实没能评估。发观测帧(纯 observe,不改变运行)——
|
|
530
|
+
// 方向仍是 fail-open,但用户/壳侧要能知道「这轮没看住」,否则那个放行与「已达成」无法区分。
|
|
531
|
+
if (!res.ok && res.code === "no_content") {
|
|
532
|
+
ctx.onHookNotice?.({ kind: "hook_decision_unavailable", event: "Stop", reason: "no_content", detail: "carrier returned no content (after one retry)" });
|
|
533
|
+
}
|
|
523
534
|
}
|
|
535
|
+
if (!res.ok) {
|
|
536
|
+
return res.code === "hard_timeout"
|
|
537
|
+
? { code: null, stdout: "", stderr: "", timedOut: true }
|
|
538
|
+
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError: clip(res.error, 500) };
|
|
539
|
+
}
|
|
540
|
+
return { code: 0, stdout: clip(res.text, 1024 * 1024), stderr: "", timedOut: false };
|
|
524
541
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError: clip(res.error, 500) };
|
|
542
|
+
finally {
|
|
543
|
+
if (hardTopTimer !== undefined)
|
|
544
|
+
clearTimeout(hardTopTimer);
|
|
529
545
|
}
|
|
530
|
-
return { code: 0, stdout: clip(res.text, 1024 * 1024), stderr: "", timedOut: false };
|
|
531
546
|
}
|
|
532
547
|
async function runHttpHook(entry, payload, ctx) {
|
|
533
548
|
const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
|
|
@@ -32,6 +32,13 @@ export declare function handleApprovalsAssistant(req: IncomingMessage, res: Serv
|
|
|
32
32
|
* heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
|
|
33
33
|
* Poll-granularity caveat: a pending that resolves AND re-suspends on the same (session,toolCallId) within one
|
|
34
34
|
* interval shows no delta — acceptable (the live snapshot is always eventually correct; no decision is missed).
|
|
35
|
+
*
|
|
36
|
+
* A-002.19(2026-08-09 亲验定性=设计接受,非欠账):每连接自跑 pollMs 轮询打共享 checkpoint 表,
|
|
37
|
+
* 之所以不建 fan-out/单轮询器基建——①消费方=portal 运营面,并发连接数量级为个位(不是用户面);
|
|
38
|
+
* ②listPending 走 (scope,status) 索引,单查成本与一次 /v1/approvals 轮询同阶,而本路正是替代客户端
|
|
39
|
+
* ~10s 轮询的(净负载持平或更低);③DB blip 已 fail-soft(心跳+下拍重试),15-min cap 兜顶。连接
|
|
40
|
+
* 数假设若被打破(portal 多开成常态),届时把 poll 收敛为进程内单轮询器多路复用——那是量级触发的
|
|
41
|
+
* 演化,不是现在的缺陷。
|
|
35
42
|
*/
|
|
36
43
|
export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">, scope: string | undefined, pollMs?: number): Promise<void>;
|
|
37
44
|
//# sourceMappingURL=approvals-assistant.d.ts.map
|
|
@@ -178,9 +178,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
178
178
|
};
|
|
179
179
|
}))
|
|
180
180
|
// severity DESC, then **explicitly** oldest-first within a tier ([2027] 第五节欠单②,红先绿后:
|
|
181
|
-
// test/assistant-triage-ordering.test.ts)。旧姿势只比 severity,靠「listByScope 是
|
|
181
|
+
// test/assistant-triage-ordering.test.ts)。旧姿势只比 severity,靠「listByScope 是 created_at_ms ASC
|
|
182
182
|
// + Array#sort 稳定」来兑现 §2 那句 oldest-first —— 而那个前提**只在本仓的 SQL 后端成立**
|
|
183
|
-
// (checkpoint-store-sql.ts 的 `ORDER BY
|
|
183
|
+
// (checkpoint-store-sql.ts 的 `ORDER BY created_at_ms ASC`):core 的 `CheckpointStore.listByScope` 接口
|
|
184
184
|
// 零顺序声明,InMemory/File 两个实现直接遍历 Map(LOCAL 车道包的正是 File store)⇒ 那句承诺过去是
|
|
185
185
|
// **后端相关**的。比较器自带兜底后它与 store 顺序无关。形与 listPending 的同族 sort 逐字一致
|
|
186
186
|
// (checkpoint-store-sql.ts `|| a.createdAt - b.createdAt`)。createdAt 缺席(pre-1.116 投影的老行)
|
|
@@ -667,6 +667,13 @@ const APPROVALS_STREAM_POLL_MS = 3000;
|
|
|
667
667
|
* heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
|
|
668
668
|
* Poll-granularity caveat: a pending that resolves AND re-suspends on the same (session,toolCallId) within one
|
|
669
669
|
* interval shows no delta — acceptable (the live snapshot is always eventually correct; no decision is missed).
|
|
670
|
+
*
|
|
671
|
+
* A-002.19(2026-08-09 亲验定性=设计接受,非欠账):每连接自跑 pollMs 轮询打共享 checkpoint 表,
|
|
672
|
+
* 之所以不建 fan-out/单轮询器基建——①消费方=portal 运营面,并发连接数量级为个位(不是用户面);
|
|
673
|
+
* ②listPending 走 (scope,status) 索引,单查成本与一次 /v1/approvals 轮询同阶,而本路正是替代客户端
|
|
674
|
+
* ~10s 轮询的(净负载持平或更低);③DB blip 已 fail-soft(心跳+下拍重试),15-min cap 兜顶。连接
|
|
675
|
+
* 数假设若被打破(portal 多开成常态),届时把 poll 收敛为进程内单轮询器多路复用——那是量级触发的
|
|
676
|
+
* 演化,不是现在的缺陷。
|
|
670
677
|
*/
|
|
671
678
|
export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_STREAM_POLL_MS) {
|
|
672
679
|
// JSON-encode the (sessionId, toolCallId) pair so distinct pendings can NEVER collide into one Map key,
|
|
@@ -8,5 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
10
10
|
import type { RouteCtx } from "../route-ctx.js";
|
|
11
|
+
/** 表内=core 的前置校验形(客户端可改请求)⇒ 400;表外一律 500。 */
|
|
12
|
+
export declare function isCorePreflightValidationError(message: string): boolean;
|
|
13
|
+
/** 供跨仓锚门读:门要拿这张表去装机的 core 里逐条对账(见上方 doc)。 */
|
|
14
|
+
export declare const CORE_PREFLIGHT_VALIDATION_ANCHORS: readonly string[];
|
|
11
15
|
export declare function handleSideQuery(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
12
16
|
//# sourceMappingURL=side-query.d.ts.map
|
|
@@ -4,6 +4,79 @@ import { cacheFamilyOfMirror } from "../../budget.js";
|
|
|
4
4
|
import { redactSecrets } from "../../trace/redact.js";
|
|
5
5
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
6
6
|
import { gatedPrincipal } from "../principal-gate.js";
|
|
7
|
+
/**
|
|
8
|
+
* A-002.2 —— core 前置校验异常的分类词表(400 客户端可改 / 500 我方缺陷)。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **亲验记账(core 5.x dist,本批复核)**:core 在这条路径上抛的是**裸 `Error`**,异常对象上
|
|
11
|
+
* 没有 `code`/`name`/`status` 任何结构化判别位 ——
|
|
12
|
+
* · `core/roles.js:8` `throw new Error(\`Unknown model ref "\${ref}". …\`)`
|
|
13
|
+
* · `core/roles.js:179` `throw new Error(\`No model for role "\${start}": …\`)`
|
|
14
|
+
* 所以「优先消费结构化位、正则降兜底」在当前 core 上**无位可消费**:这里如实停在文案匹配,并把跨仓
|
|
15
|
+
* 耦合交给一道机器门看着 —— test/side-query-http.test.ts 的「A-002.2 跨仓锚」**调装机 core 的两个导出
|
|
16
|
+
* 解析器把异常真抛出来**,拿真 message 过本谓词。
|
|
17
|
+
* 🔴 为什么不是「扫 core 源码里有没有这两句字面」:那种锚有一个致命假绿形(codex 复审 medium,已采纳)
|
|
18
|
+
* ——core 完全可以保留字面却在抛出时加前缀/包一层,源码锚照绿而 `startsWith` 当场失效,客户端错误静默
|
|
19
|
+
* 变成 500。真抛出预言机没有这个缝(它当场就揪出了下面 `modelRole` 那条连带缺陷)。
|
|
20
|
+
* core 哪天给这两处加了 code,本函数就该改成读那个 code、文案匹配退成兜底 —— 那道门会先红,提醒动这里。
|
|
21
|
+
*
|
|
22
|
+
* 🪦 **第三臂已删(死支确认)**:原表还有 `requires a non-empty messages array`(core
|
|
23
|
+
* `core/side-query.js:10`)。它**不可达** —— 本路由在调 core 之前就有
|
|
24
|
+
* `!Array.isArray(body.messages) || body.messages.length === 0 ⇒ 400 request.body_shape` 的前拦
|
|
25
|
+
* (见下方路由体),两个条件与 core 那条判据逐字同集,所以 core 那句永远轮不到抛。留着=养一条永假的
|
|
26
|
+
* 分支,删了并立此墓碑;前拦一旦被摘,side-query-http 的「空/非数组 messages → 400 request.body_shape」
|
|
27
|
+
* 两格先红。
|
|
28
|
+
*
|
|
29
|
+
* 失败方向:表外一律 500(响亮、可告警、可重试),绝不把不认识的异常猜成客户端错误。
|
|
30
|
+
*/
|
|
31
|
+
const CORE_PREFLIGHT_VALIDATION_PREFIXES = ["Unknown model ref ", "No model for role "];
|
|
32
|
+
/** 表内=core 的前置校验形(客户端可改请求)⇒ 400;表外一律 500。 */
|
|
33
|
+
export function isCorePreflightValidationError(message) {
|
|
34
|
+
return CORE_PREFLIGHT_VALIDATION_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
35
|
+
}
|
|
36
|
+
/** 供跨仓锚门读:门要拿这张表去装机的 core 里逐条对账(见上方 doc)。 */
|
|
37
|
+
export const CORE_PREFLIGHT_VALIDATION_ANCHORS = CORE_PREFLIGHT_VALIDATION_PREFIXES;
|
|
38
|
+
/**
|
|
39
|
+
* A-002.2 连带 —— `modelRole` 的**选路可达性**门。
|
|
40
|
+
*
|
|
41
|
+
* 🔴 由来(A-002.2 的真抛出预言机当场揪出来的,源码扫描锚与 mock 都照不到):core 的 `ModelRole` 是
|
|
42
|
+
* 闭集(8 值),而 core 的 `resolveTaskModel` 在**无显式 model** 时要走 `FALLBACK[start]` —— 表外角色
|
|
43
|
+
* 在那张表里是 `undefined`,于是真抛 `TypeError: FALLBACK[start] is not iterable`。一条**客户端可改**的
|
|
44
|
+
* 输入错误,以「服务器内部错误 + 可重试」的面目回给客户端;而且这一条连 400/500 分类都救不了(那句
|
|
45
|
+
* TypeError 文案里没有任何可分类的痕迹)。所以拒在本层。
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ **只在这个字段真参与选路时才拒**(codex 复审 R2 medium,已采纳并收窄):`spec.model` 在场时
|
|
48
|
+
* `resolveTaskModel` 走**短路分支**,压根不碰 `FALLBACK` —— `{model:"<有效 ref>", modelRole:"cheap"}`
|
|
49
|
+
* 是一条**今天就能跑通**的既有形,而 SDK 的 `SideQueryRequest.modelRole` 又逐字声明为开放 `string`
|
|
50
|
+
* (spec/openapi.yaml)。对它一并拒 = 无版本协商地收窄已发布契约。首版正是这么写的,复审逮到。
|
|
51
|
+
* 同理也不能「有 model 时把表外角色丢掉」:core 会拿 `merged[start]` 取该角色上的 thinking/systemPrompt
|
|
52
|
+
* (`runSideQuery` 里 `spec.systemPrompt ?? resolved.systemPrompt`),丢掉它会让配置好的 `default` 角色的
|
|
53
|
+
* systemPrompt 意外生效 —— 那也是行为改动。⇒ 有 model 时**原样透传**,行为逐字不变。
|
|
54
|
+
*
|
|
55
|
+
* 表是 `Record<ModelRole, true>` 穷举:core 增删一个角色词,本表编译期先红(闭集词表的成文纪律)。
|
|
56
|
+
*/
|
|
57
|
+
const MODEL_ROLE_TABLE = {
|
|
58
|
+
default: true,
|
|
59
|
+
summarize: true,
|
|
60
|
+
subagent: true,
|
|
61
|
+
team: true,
|
|
62
|
+
synthesize: true,
|
|
63
|
+
advisor: true,
|
|
64
|
+
verifier: true,
|
|
65
|
+
classifier: true,
|
|
66
|
+
};
|
|
67
|
+
/** 表内 ⇒ 收窄成 `ModelRole`(类型守卫)。门用它判「这个角色能不能自己选出模型」。 */
|
|
68
|
+
function isModelRole(value) {
|
|
69
|
+
return typeof value === "string" && Object.hasOwn(MODEL_ROLE_TABLE, value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 线上契约(SDK `SideQueryRequest.modelRole: string`)比 core 的 `ModelRole` 闭集**宽**,而 core 对该字段的
|
|
73
|
+
* 实际用法是**开放键查表**(`mergeRoles(...)[start]`,查不到即空)——所以表外值在有显式 model 时是合法且
|
|
74
|
+
* 惰性的。这处窄化断言是那道宽窄差的**唯一**落点(其余路径已由上面的门挡住),集中在此并说明缘由,
|
|
75
|
+
* 而不是散在装配字面量里当作无声的裸 cast。
|
|
76
|
+
*/
|
|
77
|
+
function passthroughModelRole(value) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
7
80
|
export async function handleSideQuery(req, res, url, ctx) {
|
|
8
81
|
const miss = { fell: false };
|
|
9
82
|
await handleSideQueryBody(req, res, url, ctx, miss);
|
|
@@ -55,12 +128,19 @@ async function handleSideQueryBody(req, res, url, ctx, miss) {
|
|
|
55
128
|
sendError(res, 400, "request.field_invalid", "invalid thinking level");
|
|
56
129
|
return;
|
|
57
130
|
}
|
|
131
|
+
// 选路可达性门(见 MODEL_ROLE_TABLE 顶注):**仅当该字段真参与选路**(无显式 model)时,表外角色
|
|
132
|
+
// 才 fail-closed 在这里 —— 那条路上 core 抛的是无从分类的 TypeError,400/500 分类救不回来。
|
|
133
|
+
// 有显式 model ⇒ core 走短路分支,该值惰性,原样透传(既有可用形,不许无版本协商地收窄)。
|
|
134
|
+
if (typeof body.model !== "string" && body.modelRole !== undefined && !isModelRole(body.modelRole)) {
|
|
135
|
+
sendError(res, 400, "request.field_invalid", `invalid modelRole (expected one of: ${Object.keys(MODEL_ROLE_TABLE).join(", ")}) — a role only resolves a model when 'model' is absent`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
58
138
|
if (ac.signal.aborted)
|
|
59
139
|
return; // client already gone — don't start the brain call at all
|
|
60
140
|
const spec = {
|
|
61
141
|
messages: body.messages,
|
|
62
142
|
...(typeof body.model === "string" ? { model: body.model } : {}),
|
|
63
|
-
...(typeof body.modelRole === "string" ? { modelRole: body.modelRole } : {}),
|
|
143
|
+
...(typeof body.modelRole === "string" ? { modelRole: passthroughModelRole(body.modelRole) } : {}),
|
|
64
144
|
...(isThinkingLevel(body.thinking) ? { thinking: body.thinking } : {}),
|
|
65
145
|
...(typeof body.systemPrompt === "string" ? { systemPrompt: body.systemPrompt } : {}),
|
|
66
146
|
...(Array.isArray(body.tools) ? { tools: body.tools } : {}),
|
|
@@ -91,13 +171,13 @@ async function handleSideQueryBody(req, res, url, ctx, miss) {
|
|
|
91
171
|
sendJson(res, 200, result);
|
|
92
172
|
}
|
|
93
173
|
catch (e) {
|
|
94
|
-
// 400 仅限 core
|
|
95
|
-
//
|
|
96
|
-
//
|
|
174
|
+
// 400 仅限 core 前置校验的**已知形**;其余异常=server/adapter/config 缺陷,走 500(可告警、可重试
|
|
175
|
+
// 语义)。stream 后错误按契约进 result.errorMessage 不 throw;断连 abort 的抛出无人收,
|
|
176
|
+
// writableEnded/aborted 双判后静默。
|
|
97
177
|
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
98
178
|
if (!res.writableEnded && !ac.signal.aborted) {
|
|
99
179
|
const m = msg(e);
|
|
100
|
-
const isValidation =
|
|
180
|
+
const isValidation = isCorePreflightValidationError(m);
|
|
101
181
|
// B7:机器码随状态码同分——已知前置校验形=`request.field_invalid`(客户端可改请求),其余=`internal.error`。
|
|
102
182
|
sendError(res, isValidation ? 400 : 500, isValidation ? "request.field_invalid" : "internal.error", redactSecrets(m));
|
|
103
183
|
}
|
|
@@ -108,12 +108,27 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
|
|
|
108
108
|
const jOffset = Math.max(0, Number(jq.get("offset") ?? 0) || 0);
|
|
109
109
|
const MAX_ROW_BYTES = 64 * 1024;
|
|
110
110
|
const firstLine = (t) => (t ? redactSecrets(t.split("\n")[0].slice(0, 300)) : undefined);
|
|
111
|
+
const MAX_RESULT_CHARS = 2000;
|
|
112
|
+
// A-002.6(披露双标,P1):>64KiB 的行诚实标 `truncated`/`resultBytes`(下方两臂),而 2000 字符~
|
|
113
|
+
// 64KiB 之间的行此前被 `slice(0, 2000)` **静默**截断 —— 同一个响应数组里一行说得出「我被截了」、
|
|
114
|
+
// 另一行说不出,消费者也无从区分「结果正好 2000 字」与「结果被截到 2000 字」。
|
|
115
|
+
// 🔴 键刻意**不复用** `truncated`:兄弟臂的 `truncated:true` 语义是「整行没内容,只有尺寸存根」,
|
|
116
|
+
// 盖到一行有内容的行上就是把兄弟臂的判别位改义(按它分支的消费者会把有内容的行当空存根)。
|
|
117
|
+
// `resultTruncated`/`resultChars` 描述的是 `result` **这个字段**,无歧义。原始长度量的是
|
|
118
|
+
// redact 之前的字符数(与兄弟臂 `resultBytes` 同精神,基准不同故不同名)。
|
|
119
|
+
// ⚠️ wire 记账:SDK spec 的 `WorkflowJournalEntry` 是 `additionalProperties: false`,两枚 additive
|
|
120
|
+
// 键需要 sema-sdk spec/openapi.yaml 同源补声明(SDK 件另走)。
|
|
111
121
|
const projectResult = (callKey, r) => ({
|
|
112
122
|
callKey,
|
|
113
123
|
ordinal: callKeyOrdinal(callKey),
|
|
114
124
|
status: r.status,
|
|
115
125
|
...(r.errorMessage ? { error: firstLine(r.errorMessage) } : {}),
|
|
116
|
-
...(r.result
|
|
126
|
+
...(r.result
|
|
127
|
+
? {
|
|
128
|
+
result: redactSecrets(r.result.slice(0, MAX_RESULT_CHARS)),
|
|
129
|
+
...(r.result.length > MAX_RESULT_CHARS ? { resultTruncated: true, resultChars: r.result.length } : {}),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
117
132
|
...(r.stats ? { tokens: r.stats.tokens, turns: r.stats.turns } : {}),
|
|
118
133
|
});
|
|
119
134
|
const js = deps.workflowJournalStore;
|
package/dist/http/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import {} from "../orchestration/workflow-agent-steer.js";
|
|
|
18
18
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
19
19
|
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
|
|
20
20
|
import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
|
|
21
|
-
import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
|
|
21
|
+
import { createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
|
|
22
22
|
import {} from "../observability/rate-limit.js";
|
|
23
23
|
import { withPrincipal } from "../observability/principal-context.js";
|
|
24
24
|
import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
|
|
@@ -1322,9 +1322,9 @@ export function createHttpServer(rawDeps) {
|
|
|
1322
1322
|
// 200 真跑完却拿不到 taskId、run 行永停 failed(账本与现实分叉)。这里 pre-CAS 纯拒绝:不动 checkpoint、
|
|
1323
1323
|
// 不动 run 行、不烧模型腿,与另外两条腿的守卫同姿势(core 的 gate-match 仍是 fail-closed 兜底)。
|
|
1324
1324
|
// ⚠️ 门放在 parked 赎回腿**之前**:赎回腿同样只铸 policy_ask outcome,错门 cp 在那条链上只会更深地炸。
|
|
1325
|
-
// D-D SLA deny-sweep 不受影响:它的取行查询本就只选 gate_kind
|
|
1325
|
+
// D-D SLA deny-sweep 不受影响:它的取行查询本就只选 gate_kind ∈ APPROVAL_GATE_KINDS(tool-approval.ts 单一属主)。
|
|
1326
1326
|
const decideGateKind = cp.gate?.kind;
|
|
1327
|
-
if (decideGateKind
|
|
1327
|
+
if (!isApprovalGateKind(decideGateKind)) { // A-002.1 单一属主
|
|
1328
1328
|
// [2400] HITL-6(clay 裁 2026-08-03,不做兼容):补机读指路——与 [2255]① 的 409 材料同形
|
|
1329
1329
|
// (pendingGate{kind, decidePath},resumeEntryForGate 单源表),客户端凭 decidePath 直达对的
|
|
1330
1330
|
// resume 入口,不再解析人话文案。taskId 源=cp.sourceTaskId;缺席 ⇒ 只带 kind,诚实不铸假路径。
|
|
@@ -1406,8 +1406,8 @@ export function createHttpServer(rawDeps) {
|
|
|
1406
1406
|
// escaping HttpError here was (a) attributed to the OPERATOR's /decide request as a bare 400/403 — undiagnosable
|
|
1407
1407
|
// from that contract — and (b) swallowed by the D-D SLA deny-sweep's catch, silently failing EVERY tick. Fold it
|
|
1408
1408
|
// into a typed 409 result instead: the operator sees "this parked task is blocked by a policy change" (retry
|
|
1409
|
-
// after the policy is restored, or let
|
|
1410
|
-
// tick,
|
|
1409
|
+
// after the policy is restored, or let terminal_at_ms abort it); the sweep skips the row this tick (retried next
|
|
1410
|
+
// tick, terminal_at_ms backstop — its documented per-call-failure semantics).
|
|
1411
1411
|
let spec;
|
|
1412
1412
|
try {
|
|
1413
1413
|
spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
|
|
@@ -2651,7 +2651,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2651
2651
|
* (graceful; vs the abort the reaper's expire() gives resource_limit/needs_review). Reuses resumeCheckpoint,
|
|
2652
2652
|
* so the markResuming CAS makes it idempotent across replicas (one replica wins each resume) and the parked
|
|
2653
2653
|
* run row is driven correctly. A per-call failure (e.g. a redeployed scenario) is swallowed → the row stays
|
|
2654
|
-
* pending and is retried next tick, with the
|
|
2654
|
+
* pending and is retried next tick, with the terminal_at_ms backstop as the eventual abort if the deny never
|
|
2655
2655
|
* succeeds. Bounded per tick by the store query's LIMIT. Wired into main.ts's reaper.
|
|
2656
2656
|
*/
|
|
2657
2657
|
async function denyExpiredApprovals(now) {
|
|
@@ -2659,7 +2659,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2659
2659
|
if (!cs)
|
|
2660
2660
|
return;
|
|
2661
2661
|
const expired = await cs.listExpiredApprovalGates(now).catch(() => []);
|
|
2662
|
-
// [1591] 候裁③ 静默臂:parked 后台子代的过期 cp 不走 sweep 的重活赎回(deadline→
|
|
2662
|
+
// [1591] 候裁③ 静默臂:parked 后台子代的过期 cp 不走 sweep 的重活赎回(deadline→terminal_at_ms 窗内
|
|
2663
2663
|
// 每行每 tick 一条 warn 的噪声源)——其收割属 reaper 的 expire+reconcileParkedAgents 车道。判别在
|
|
2664
2664
|
// resume 尝试之前做(省掉注定 409 的整条 ctx/resolveSpec 重建尝试;代价=每过期行两次店读,过期集
|
|
2665
2665
|
// 本就被店查询 LIMIT 界住);命中聚合为单条 info 留痕。判别自身故障 ⇒ 按非 parked 处理(保留 warn,
|
|
@@ -2684,7 +2684,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2684
2684
|
for (const { sessionId } of expired) {
|
|
2685
2685
|
// 对抗评审 2026-07-11: log the swallow — a per-tick failure retried forever (e.g. a policy change now folded
|
|
2686
2686
|
// into a 409 by resumeCheckpoint, or a redeployed scenario) was fully silent; the row sat pending to its
|
|
2687
|
-
//
|
|
2687
|
+
// terminal_at_ms with zero operator-visible signal. Behavior unchanged (skip + retry next tick), now diagnosable.
|
|
2688
2688
|
if (await isParkedOwned(sessionId)) {
|
|
2689
2689
|
parkedSkipped += 1;
|
|
2690
2690
|
continue;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { buildSessionAudit, type SessionAudit } from "./audit.js";
|
|
|
12
12
|
export { createLogger, type Logger, type LogLevel } from "./observability/logger.js";
|
|
13
13
|
export { Metrics, createMetrics, type Labels } from "./observability/metrics.js";
|
|
14
14
|
export { RateLimiter, type RateDecision } from "./observability/rate-limit.js";
|
|
15
|
-
export { GiteaClient, repoToolsFor, parseRepo, type RepoCoords } from "./capabilities/repo-tools.js";
|
|
15
|
+
export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo, type GitApiKind, type RepoCoords, type RepoReadClient } from "./capabilities/repo-tools.js";
|
|
16
16
|
export { nowTool } from "./capabilities/builtin-tools.js";
|
|
17
17
|
export { createCouncilTool } from "./capabilities/code-review-council.js";
|
|
18
18
|
export { loadSkills, skillsForScenario, type LoadedSkill } from "./capabilities/skills.js";
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { buildSessionAudit } from "./audit.js";
|
|
|
17
17
|
export { createLogger } from "./observability/logger.js";
|
|
18
18
|
export { Metrics, createMetrics } from "./observability/metrics.js";
|
|
19
19
|
export { RateLimiter } from "./observability/rate-limit.js";
|
|
20
|
-
export { GiteaClient, repoToolsFor, parseRepo } from "./capabilities/repo-tools.js";
|
|
20
|
+
export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKind, repoToolsFor, parseRepo } from "./capabilities/repo-tools.js";
|
|
21
21
|
export { nowTool } from "./capabilities/builtin-tools.js";
|
|
22
22
|
export { createCouncilTool } from "./capabilities/code-review-council.js";
|
|
23
23
|
export { loadSkills, skillsForScenario } from "./capabilities/skills.js";
|
package/dist/main.js
CHANGED
|
@@ -16,7 +16,7 @@ import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadO
|
|
|
16
16
|
import { createAuthorizer, encodeCheckpointScope } from "./security.js";
|
|
17
17
|
import { assertGateIntentServiceable, hasOperatorGateIntent } from "./approval.js";
|
|
18
18
|
import { loadSkills } from "./capabilities/skills.js";
|
|
19
|
-
import {
|
|
19
|
+
import { createRepoClient } from "./capabilities/repo-tools.js";
|
|
20
20
|
import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
|
|
21
21
|
import { createHandsLaneRegistry, pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
|
|
22
22
|
import { createLogger } from "./observability/logger.js";
|
|
@@ -570,7 +570,7 @@ async function main() {
|
|
|
570
570
|
// 解析搬到 boot/config-center.ts(逐字)。⚠️ 位置即契约:loadSkills 之后、buildScenarios 之前 ——
|
|
571
571
|
// LKG 落盘点必须晚于 skill 正文装载(F7/codex R26),plugins 让位判据要求 plugins 晚于 center 直发 skills。
|
|
572
572
|
skills = await configCenter.applyCenterCapabilities(skills);
|
|
573
|
-
const repoClient = config.gitApiBaseUrl ?
|
|
573
|
+
const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
|
|
574
574
|
// CC-parity: deployment-injected WebSearch backend (the leg core leaves open). Absent WEB_SEARCH_PROVIDER →
|
|
575
575
|
// undefined → the default scenario doesn't assemble the WebSearch tool. The API key stays in the backend closure.
|
|
576
576
|
const webSearchCfg = webSearchConfigFromEnv();
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { FileMemoryEngineBackend } from "@sema-agent/core";
|
|
2
2
|
import type { ServiceConfig } from "./config-types.js";
|
|
3
|
+
/** 前缀词表本体(供门/诊断读;判定一律走 {@link isV2ScopeKey},别在别处重写 startsWith 链)。 */
|
|
4
|
+
export declare const V2_SCOPE_PREFIXES: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* 一个 scope 键是否带 v2 typed 前缀。
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ 判据刻意停在**前缀**层,不升级成 core 的 `parseScopeKey`:后者对「带可识别前缀但结构坏」的键
|
|
9
|
+
* 是 fail-loud 抛错,而本谓词的两个调用点都只是在决定「要不要挂 v2 标 / 要不要再包一层」——把抛点
|
|
10
|
+
* 挪到这里会把 core 的校验时机整体前移(行为面改动,不在本条案的射程内)。坏键仍由 core 在它自己的
|
|
11
|
+
* 校验点 fail-loud。
|
|
12
|
+
*/
|
|
13
|
+
export declare function isV2ScopeKey(key: string): boolean;
|
|
3
14
|
/** Resolve the memory scope for a request (design/138 S1 — the file-based memory engine is the only plane).
|
|
4
15
|
*
|
|
5
16
|
* SINGLE-USER (REQUIRE_PRINCIPAL !== "true"): ONE shared scope — `config.memoryScope` (default "local"),
|
|
@@ -11,6 +22,11 @@ import type { ServiceConfig } from "./config-types.js";
|
|
|
11
22
|
* fail-closed. The engine's file basement carries NO tenant isolation (one shared directory tree), so
|
|
12
23
|
* handing tenants per-principal scopes over it would be a cross-tenant bleed surface; until a
|
|
13
24
|
* tenant-isolated backend exists, multi-tenant memory is off by construction. */
|
|
25
|
+
/** R5(批γ):memory scope 列宽(两方言 VARCHAR(190) 同宽)。principal 上限 190 **字符**在
|
|
26
|
+
* security.ts 把身份轴守住了,但 `formatUserScope`/`formatProjScope` 的段编码(百分号转义)会
|
|
27
|
+
* **膨胀**——非 ASCII principal 编码后可超列宽,此前落裸 SQL 错。产出处响亮拒,错误可分类。 */
|
|
28
|
+
export declare const MEMORY_SCOPE_COLUMN_CHARS = 190;
|
|
29
|
+
export declare function assertMemoryScopeWidth(scope: string): void;
|
|
14
30
|
export declare function memoryScopeFor(config: ServiceConfig, principal?: string, projectId?: string): string | undefined;
|
|
15
31
|
/**
|
|
16
32
|
* N0 (通宵测试 2026-07-09,三重坐实 e2b+kata+代码): the file memory engine (core design/138) materializes and
|
package/dist/memory-scope.js
CHANGED
|
@@ -5,6 +5,34 @@
|
|
|
5
5
|
// "exports" only ever exposed "." and "./main" — dist/security.js was never a reachable deep-import path).
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { FileMemoryEngineBackend, resolveMemoryEngineRoot, deriveControlPlaneDir, formatUserScope, formatProjScope } from "@sema-agent/core";
|
|
8
|
+
/**
|
|
9
|
+
* design/142 §1.2 的 v2 typed scope-key 前缀表 —— **单一属主**(A-002.4:`config.ts` 的
|
|
10
|
+
* MEMORY_SYNC_SCOPE 派生处曾有一份逐字副本,两处咬合用途不同[盖 v2 契约标 vs 判要不要再包一层
|
|
11
|
+
* `formatUserScope`],但判据必须是同一份,否则 core 加第五种 typed 前缀时两处各自漂)。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 表按 core 的 {@link ParsedScopeKey} 判别词**穷举**:`Record<Exclude<kind,"legacy">, …>` 少一格
|
|
14
|
+
* 编译红、多一格也编译红 —— core 新增/改名一种 typed kind,本表在编译期就先红,而不是等某个运行期
|
|
15
|
+
* 分支静默走错。(`legacy` 排除在外正是本谓词要判的反面:无可识别前缀 = 不透明旧键。)
|
|
16
|
+
*/
|
|
17
|
+
const V2_SCOPE_PREFIX_BY_KIND = {
|
|
18
|
+
user: "user:",
|
|
19
|
+
org: "org:",
|
|
20
|
+
proj: "proj:",
|
|
21
|
+
userproj: "userproj:",
|
|
22
|
+
};
|
|
23
|
+
/** 前缀词表本体(供门/诊断读;判定一律走 {@link isV2ScopeKey},别在别处重写 startsWith 链)。 */
|
|
24
|
+
export const V2_SCOPE_PREFIXES = Object.freeze(Object.values(V2_SCOPE_PREFIX_BY_KIND));
|
|
25
|
+
/**
|
|
26
|
+
* 一个 scope 键是否带 v2 typed 前缀。
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ 判据刻意停在**前缀**层,不升级成 core 的 `parseScopeKey`:后者对「带可识别前缀但结构坏」的键
|
|
29
|
+
* 是 fail-loud 抛错,而本谓词的两个调用点都只是在决定「要不要挂 v2 标 / 要不要再包一层」——把抛点
|
|
30
|
+
* 挪到这里会把 core 的校验时机整体前移(行为面改动,不在本条案的射程内)。坏键仍由 core 在它自己的
|
|
31
|
+
* 校验点 fail-loud。
|
|
32
|
+
*/
|
|
33
|
+
export function isV2ScopeKey(key) {
|
|
34
|
+
return V2_SCOPE_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
35
|
+
}
|
|
8
36
|
/** Resolve the memory scope for a request (design/138 S1 — the file-based memory engine is the only plane).
|
|
9
37
|
*
|
|
10
38
|
* SINGLE-USER (REQUIRE_PRINCIPAL !== "true"): ONE shared scope — `config.memoryScope` (default "local"),
|
|
@@ -16,6 +44,16 @@ import { FileMemoryEngineBackend, resolveMemoryEngineRoot, deriveControlPlaneDir
|
|
|
16
44
|
* fail-closed. The engine's file basement carries NO tenant isolation (one shared directory tree), so
|
|
17
45
|
* handing tenants per-principal scopes over it would be a cross-tenant bleed surface; until a
|
|
18
46
|
* tenant-isolated backend exists, multi-tenant memory is off by construction. */
|
|
47
|
+
/** R5(批γ):memory scope 列宽(两方言 VARCHAR(190) 同宽)。principal 上限 190 **字符**在
|
|
48
|
+
* security.ts 把身份轴守住了,但 `formatUserScope`/`formatProjScope` 的段编码(百分号转义)会
|
|
49
|
+
* **膨胀**——非 ASCII principal 编码后可超列宽,此前落裸 SQL 错。产出处响亮拒,错误可分类。 */
|
|
50
|
+
export const MEMORY_SCOPE_COLUMN_CHARS = 190;
|
|
51
|
+
export function assertMemoryScopeWidth(scope) {
|
|
52
|
+
if (scope.length > MEMORY_SCOPE_COLUMN_CHARS) {
|
|
53
|
+
throw new Error(`memory scope exceeds ${MEMORY_SCOPE_COLUMN_CHARS} characters after segment encoding (the scope column width ` +
|
|
54
|
+
`both SQL dialects pin); got ${scope.length} — use a shorter principal/projectId (non-ASCII characters expand ~9x when encoded)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
19
57
|
export function memoryScopeFor(config, principal, projectId) {
|
|
20
58
|
if (config.requirePrincipal === true) {
|
|
21
59
|
// S3-TOB(设计 §1.3):multi-tenant lights up ONLY on a DB backend (scope column = tenant
|
|
@@ -34,7 +72,9 @@ export function memoryScopeFor(config, principal, projectId) {
|
|
|
34
72
|
if (config.memoryEngineBackend === "pg" || config.memoryEngineBackend === "tidb") {
|
|
35
73
|
if (!principal)
|
|
36
74
|
return undefined;
|
|
37
|
-
|
|
75
|
+
const scope = projectId ? formatProjScope(principal, projectId) : formatUserScope(principal);
|
|
76
|
+
assertMemoryScopeWidth(scope); // R5:编码膨胀在产出处接住,不落 store 裸错
|
|
77
|
+
return scope;
|
|
38
78
|
}
|
|
39
79
|
return undefined; // file backend: dark, fail-closed (no tenant isolation in the file engine)
|
|
40
80
|
}
|
|
@@ -113,7 +153,7 @@ originPolicy) {
|
|
|
113
153
|
// OPERATOR's center-registry declaration (config.projects — 登记簿说明其形态=v2 scope 键串),so the
|
|
114
154
|
// prefix is a self/operator-produced marker, never a guess about caller data; a legacy opaque scope
|
|
115
155
|
// (single-user config.memoryScope) with no defaultScopes stays contract-less byte-identical.
|
|
116
|
-
const isV2 =
|
|
156
|
+
const isV2 = isV2ScopeKey; // A-002.4:词表单一属主(见文件顶部 V2_SCOPE_PREFIX_BY_KIND)
|
|
117
157
|
// 142-S4 defaultScopes 种子(config.projects[projectId].defaultScopes):额外的 READ 层(去重、去 scope
|
|
118
158
|
// 自身、去空串;保 center 声明顺序)。写路由不变:core normalizeMemorySpec 的 writeScope 缺省=scopes 的
|
|
119
159
|
// 最后一层,所以带种子时必须 EXPLICIT 钉 writeScope=派生 scope(否则 harvest 会写进登记簿的最后一个默认
|
|
@@ -4,7 +4,7 @@ import { type Checkpoint, type CheckpointGate, type CheckpointState, type Checkp
|
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
5
|
/**
|
|
6
6
|
* design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
|
|
7
|
-
* checkpoint's lifetime, stamped at put() into `
|
|
7
|
+
* checkpoint's lifetime, stamped at put() into `terminal_at_ms` INDEPENDENT of the per-approval `deadline`. The
|
|
8
8
|
* SLA-timer (D-D) does the fine, per-gate-kind resolve-deny; THIS coarse backstop ensures even a pending row
|
|
9
9
|
* with a NULL `deadline` (no approval TTL configured) is eventually GC'd if the SLA service dies — closing a
|
|
10
10
|
* forever-leak of a never-resolved suspension (the current `reap`/`reapExpired` only catch non-NULL deadlines).
|
|
@@ -13,9 +13,9 @@ import { type SqlDriver } from "./sql-driver.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const TERMINAL_BACKSTOP_MS: number;
|
|
15
15
|
/**
|
|
16
|
-
* design/80 D-D (adversarial fix): the crash-safe `
|
|
17
|
-
* `deadline`, never AT it. `
|
|
18
|
-
* operator tuned APPROVAL_TERMINAL_BACKSTOP_MS at/below the SLA — and reapExpired's
|
|
16
|
+
* design/80 D-D (adversarial fix): the crash-safe `terminal_at_ms` backstop must fall STRICTLY AFTER any SLA
|
|
17
|
+
* `deadline`, never AT it. `terminal_at_ms = max(createdAt+backstop, deadline)` made the two coincide whenever an
|
|
18
|
+
* operator tuned APPROVAL_TERMINAL_BACKSTOP_MS at/below the SLA — and reapExpired's terminal_at_ms-branch (which
|
|
19
19
|
* has NO gate_kind filter) then abort-EXPIRED a human/irreversible_ask gate in the SAME tick the deny-sweep
|
|
20
20
|
* wanted to gracefully DENY it, racing it away. Adding this grace to the deadline term guarantees the deny-sweep
|
|
21
21
|
* at least this window of clean ticks before the absolute backstop can fire. Far smaller than the backstop, so
|
|
@@ -252,8 +252,8 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
252
252
|
/**
|
|
253
253
|
* GLOBAL sweep for the service's per-replica TTL reaper (expiry isn't tenant-
|
|
254
254
|
* sensitive — only `resolve` is scoped). Idempotent across replicas (DB serializes; no election). Returns count.
|
|
255
|
-
* Called with `cutoff = Date.now()` (deadline/
|
|
256
|
-
* whose per-approval `deadline` OR its design/80 D-1 §3-inv#3 `
|
|
255
|
+
* Called with `cutoff = Date.now()` (deadline/terminal_at_ms are ABSOLUTE epoch-ms), so it expires any pending row
|
|
256
|
+
* whose per-approval `deadline` OR its design/80 D-1 §3-inv#3 `terminal_at_ms` crash-safe backstop has passed —
|
|
257
257
|
* the latter closes the forever-leak of a pending row with a NULL `deadline` (no approval TTL was configured).
|
|
258
258
|
*
|
|
259
259
|
* design/80 D-D: the deadline-branch EXPIRES (≈ abort) every kind EXCEPT a tool-approval human/irreversible_ask
|
|
@@ -262,7 +262,7 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
262
262
|
* AskUserQuestion ALSO mints gate.kind='human' (no question-specific kind in core) — but DENYING a question is
|
|
263
263
|
* incoherent (the model gets a "denied" tool-result, not an answer), so it is carved BACK INTO the expire path
|
|
264
264
|
* (COALESCE(tool_name,'')='AskUserQuestion') to abort-expire on timeout instead. Legacy rows (gate_kind NULL)
|
|
265
|
-
* stay on the expire path. The
|
|
265
|
+
* stay on the expire path. The terminal_at_ms-branch is the crash-safe backstop for ANY kind (incl. a human gate
|
|
266
266
|
* whose deny-resume keeps failing) — it always abort-expires past the absolute cap (which is now STRICTLY after
|
|
267
267
|
* the deadline, so it never races the deny-sweep at the deadline instant).
|
|
268
268
|
*/
|
|
@@ -291,7 +291,7 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
291
291
|
* (gate kind + risk severity + budget spent + deadline per suspended task), no N+1 `get`s. The projection is
|
|
292
292
|
* core's shared {@link summarizeCheckpoint} run over the persisted blob (the `checkpoint` column = the same full
|
|
293
293
|
* {@link Checkpoint} `get()` parses), so this stays byte-identical to core's InMemory/Pg/File impls. Order =
|
|
294
|
-
*
|
|
294
|
+
* created_at_ms ASC; callers (the inbox/scheduler) sort by severity. The COLUMN `status` is authoritative (the blob
|
|
295
295
|
* is the suspend-time snapshot), so it overrides the blob's status before the summary is derived.
|
|
296
296
|
*
|
|
297
297
|
* LIMIT bounds the fan-out (review w16yqkkxv): a triage view never needs more than a few — 500 is a generous
|
|
@@ -309,7 +309,7 @@ export declare class SqlCheckpointStore implements CheckpointStore {
|
|
|
309
309
|
*
|
|
310
310
|
* 「这条 PARKING 的 ask 究竟 park 成了哪张 checkpoint?」的唯一读法。为什么不是「按 session 翻历史页」:
|
|
311
311
|
* 分页宽读会漏匹配,而漏匹配在收敛器那侧的后果是**假阴性 ⇒ 落一条不可逆的 DENIED**。所以这里改成
|
|
312
|
-
* 谓词精确查——`(scope, session_id, tool_call_id,
|
|
312
|
+
* 谓词精确查——`(scope, session_id, tool_call_id, created_at_ms ≥ sinceMs)` 这组条件下的行数天然极小,
|
|
313
313
|
* 一次全量返回,结构上没有分页假阴性。
|
|
314
314
|
*
|
|
315
315
|
* 三条口径,逐条都是判据:
|