@sema-agent/server 7.93.4 → 7.93.6
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/dist/bake-runner/main.d.ts +28 -1
- package/dist/bake-runner/main.js +122 -38
- package/dist/bake-runner/runner.d.ts +85 -7
- package/dist/bake-runner/runner.js +116 -27
- package/dist/boot/exit-boundary.d.ts +22 -0
- package/dist/boot/exit-boundary.js +73 -0
- package/dist/boot/shutdown.d.ts +2 -2
- package/dist/boot/shutdown.js +5 -62
- package/dist/hooks/hook-runner.js +5 -1
- package/dist/observability/fail-open.d.ts +16 -0
- package/dist/observability/fail-open.js +16 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/approval-ask-store-sql.js +36 -19
- package/dist/plugins/background-agent-store-sql.js +36 -16
- package/dist/plugins/checkpoint-store-sql.d.ts +5 -1
- package/dist/plugins/checkpoint-store-sql.js +111 -66
- package/dist/plugins/device-store-sql.d.ts +10 -0
- package/dist/plugins/device-store-sql.js +12 -6
- package/dist/plugins/host-platform.d.ts +70 -6
- package/dist/plugins/host-platform.js +14 -3
- package/dist/plugins/leader-run-store-sql.js +16 -6
- package/dist/plugins/one-time-migrations.d.ts +3 -0
- package/dist/plugins/one-time-migrations.js +16 -4
- package/dist/plugins/permission-rule-store-sql.d.ts +1 -0
- package/dist/plugins/permission-rule-store-sql.js +37 -28
- package/dist/plugins/remote-env-host.js +9 -21
- package/dist/plugins/run-store-sql.d.ts +1 -1
- package/dist/plugins/run-store-sql.js +41 -10
- package/dist/plugins/session-policy-store-sql.d.ts +24 -0
- package/dist/plugins/session-policy-store-sql.js +17 -18
- package/dist/plugins/sql-json-column.d.ts +177 -0
- package/dist/plugins/sql-json-column.js +74 -0
- package/dist/plugins/task-list-store-sql.d.ts +13 -1
- package/dist/plugins/task-list-store-sql.js +23 -10
- package/dist/plugins/workflow-run-store-sql.js +14 -4
- package/dist/run-local.d.ts +15 -0
- package/dist/run-local.js +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
2
|
+
import { liveHostForegroundChildCount, reapHostForegroundChildren } from "../plugins/host-platform.js";
|
|
3
|
+
import { liveEphemeralHostWorkspaceCount, reapEphemeralHostWorkspaces } from "../plugins/host-workspace-registry.js";
|
|
4
|
+
export function installProcessExitBoundary({ logger }) {
|
|
5
|
+
const reapOwnChildProcesses = () => {
|
|
6
|
+
try {
|
|
7
|
+
const reaped = defaultTaskRegistry.reapAllSessionBackground();
|
|
8
|
+
if (reaped > 0)
|
|
9
|
+
logger.info("shutdown_background_reaped", { reaped });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
logger.warn("shutdown_background_reap_failed", { err: String(err) });
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const fg = reapHostForegroundChildren();
|
|
16
|
+
if (fg.signalled > 0 || fg.alreadyDead > 0)
|
|
17
|
+
logger.info("shutdown_foreground_reaped", { signalled: fg.signalled, alreadyDead: fg.alreadyDead });
|
|
18
|
+
if (fg.failed.length > 0)
|
|
19
|
+
logger.warn("shutdown_foreground_reap_incomplete", { groups: fg.failed });
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const reapOwnHostWorkspaces = () => {
|
|
26
|
+
try {
|
|
27
|
+
const { removed, failed } = reapEphemeralHostWorkspaces();
|
|
28
|
+
if (removed.length > 0)
|
|
29
|
+
logger.info("shutdown_host_workspaces_reaped", { removed: removed.length });
|
|
30
|
+
if (failed.length > 0)
|
|
31
|
+
logger.warn("shutdown_host_workspace_reap_incomplete", { dirs: failed.map((f) => f.dir) });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.warn("shutdown_host_workspace_reap_failed", { err: String(err) });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const onProcessExit = () => {
|
|
38
|
+
const onBooks = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
39
|
+
try {
|
|
40
|
+
reapOwnChildProcesses();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error("shutdown_exit_boundary_child_reap_threw", err);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
reapOwnHostWorkspaces();
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error("shutdown_exit_boundary_workspace_reap_threw", err);
|
|
50
|
+
}
|
|
51
|
+
const left = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
52
|
+
try {
|
|
53
|
+
logger.info("shutdown_exit_boundary", { ...onBooks, leftForegroundChildren: left.foregroundChildren, leftHostWorkspaces: left.hostWorkspaces });
|
|
54
|
+
if (left.foregroundChildren > 0 || left.hostWorkspaces > 0)
|
|
55
|
+
logger.warn("shutdown_exit_boundary_incomplete", left);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error("shutdown_exit_boundary_log_threw", err);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
process.once("exit", onProcessExit);
|
|
62
|
+
let disposed = false;
|
|
63
|
+
return {
|
|
64
|
+
reapChildProcessesEarly: reapOwnChildProcesses,
|
|
65
|
+
dispose: () => {
|
|
66
|
+
if (disposed)
|
|
67
|
+
return;
|
|
68
|
+
disposed = true;
|
|
69
|
+
process.removeListener("exit", onProcessExit);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=exit-boundary.js.map
|
package/dist/boot/shutdown.d.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* "第二个信号"的语义读数。
|
|
13
13
|
* 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则
|
|
14
14
|
* 收尾期还会有 reaper tick 打向正在关闭的池。
|
|
15
|
-
* 4. **进程级监听一律成对**([ref] 二轮件4):本函数装的六只(三信号 + [ref]① 双钩 + S-557 退出边界钩
|
|
16
|
-
* 只能经返回的
|
|
15
|
+
* 4. **进程级监听一律成对**([ref] 二轮件4):本函数装的六只(三信号 + [ref]① 双钩 + S-557 退出边界钩
|
|
16
|
+
* —— S-560 起后者由 {@link installProcessExitBoundary} 代装,摘除仍并进同一把手)只能经返回的
|
|
17
17
|
* {@link ShutdownHandle} 摘 —— 调用方手写「摘三信号」的收尾会把双钩漏在进程上,而陈旧的那只
|
|
18
18
|
* rejection 钩仍握着 `process.exit(1)`,会对后续无关的 rejection 把进程打掉。
|
|
19
19
|
*/
|
package/dist/boot/shutdown.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Runner
|
|
1
|
+
import { Runner } from "@sema-agent/core";
|
|
2
2
|
import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
|
|
3
3
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
4
4
|
import { createParentWatch } from "../parent-watch.js";
|
|
5
|
-
import {
|
|
6
|
-
import { liveEphemeralHostWorkspaceCount, reapEphemeralHostWorkspaces } from "../plugins/host-workspace-registry.js";
|
|
5
|
+
import { installProcessExitBoundary } from "./exit-boundary.js";
|
|
7
6
|
import { createEngineLeaseTracker } from "./engine-lease.js";
|
|
8
7
|
import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
|
|
9
8
|
import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
|
|
@@ -32,63 +31,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
32
31
|
if (dataRoot)
|
|
33
32
|
writeCrashLast(dataRoot, { version, inflight: drainState.inflight?.(), err });
|
|
34
33
|
};
|
|
35
|
-
const
|
|
36
|
-
try {
|
|
37
|
-
const reaped = defaultTaskRegistry.reapAllSessionBackground();
|
|
38
|
-
if (reaped > 0)
|
|
39
|
-
logger.info("shutdown_background_reaped", { reaped });
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
logger.warn("shutdown_background_reap_failed", { err: String(err) });
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
const fg = reapHostForegroundChildren();
|
|
46
|
-
if (fg.signalled > 0 || fg.alreadyDead > 0)
|
|
47
|
-
logger.info("shutdown_foreground_reaped", { signalled: fg.signalled, alreadyDead: fg.alreadyDead });
|
|
48
|
-
if (fg.failed.length > 0)
|
|
49
|
-
logger.warn("shutdown_foreground_reap_incomplete", { groups: fg.failed });
|
|
50
|
-
}
|
|
51
|
-
catch (err) {
|
|
52
|
-
logger.warn("shutdown_foreground_reap_failed", { err: String(err) });
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
const reapOwnHostWorkspaces = () => {
|
|
56
|
-
try {
|
|
57
|
-
const { removed, failed } = reapEphemeralHostWorkspaces();
|
|
58
|
-
if (removed.length > 0)
|
|
59
|
-
logger.info("shutdown_host_workspaces_reaped", { removed: removed.length });
|
|
60
|
-
if (failed.length > 0)
|
|
61
|
-
logger.warn("shutdown_host_workspace_reap_incomplete", { dirs: failed.map((f) => f.dir) });
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
logger.warn("shutdown_host_workspace_reap_failed", { err: String(err) });
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const onProcessExit = () => {
|
|
68
|
-
const onBooks = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
69
|
-
try {
|
|
70
|
-
reapOwnChildProcesses();
|
|
71
|
-
}
|
|
72
|
-
catch (err) {
|
|
73
|
-
console.error("shutdown_exit_boundary_child_reap_threw", err);
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
reapOwnHostWorkspaces();
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
console.error("shutdown_exit_boundary_workspace_reap_threw", err);
|
|
80
|
-
}
|
|
81
|
-
const left = { foregroundChildren: liveHostForegroundChildCount(), hostWorkspaces: liveEphemeralHostWorkspaceCount() };
|
|
82
|
-
try {
|
|
83
|
-
logger.info("shutdown_exit_boundary", { ...onBooks, leftForegroundChildren: left.foregroundChildren, leftHostWorkspaces: left.hostWorkspaces });
|
|
84
|
-
if (left.foregroundChildren > 0 || left.hostWorkspaces > 0)
|
|
85
|
-
logger.warn("shutdown_exit_boundary_incomplete", left);
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
console.error("shutdown_exit_boundary_log_threw", err);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
process.once("exit", onProcessExit);
|
|
34
|
+
const exitBoundary = installProcessExitBoundary({ logger });
|
|
92
35
|
const onUncaughtException = (err) => {
|
|
93
36
|
try {
|
|
94
37
|
writeLastWords(err);
|
|
@@ -151,7 +94,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
151
94
|
subRunner.sessions.dispose();
|
|
152
95
|
void lspManager?.dispose?.();
|
|
153
96
|
void workflowNotifyJournal?.close?.();
|
|
154
|
-
|
|
97
|
+
exitBoundary.reapChildProcessesEarly();
|
|
155
98
|
void fleetClient?.stop();
|
|
156
99
|
server.close(() => {
|
|
157
100
|
void retentionTeardown.then(() => (backend ? backend.close() : Promise.resolve())).finally(() => process.exit(0));
|
|
@@ -248,7 +191,7 @@ export function installShutdownHandlers(ctx) {
|
|
|
248
191
|
if (disposed)
|
|
249
192
|
return;
|
|
250
193
|
disposed = true;
|
|
251
|
-
|
|
194
|
+
exitBoundary.dispose();
|
|
252
195
|
process.removeListener("uncaughtException", onUncaughtException);
|
|
253
196
|
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
254
197
|
process.removeListener("SIGTERM", drainThenShutdown);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { hostShell, resolveHostShell } from "../plugins/host-platform.js";
|
|
2
|
+
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
3
3
|
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, } from "@sema-agent/settings-schema/hooks";
|
|
4
4
|
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
5
5
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
@@ -271,6 +271,7 @@ function runCommandHook(entry, payload, ctx) {
|
|
|
271
271
|
resolve({ code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) });
|
|
272
272
|
return;
|
|
273
273
|
}
|
|
274
|
+
const ticket = adoptHostForegroundChild(child);
|
|
274
275
|
let stdout = "";
|
|
275
276
|
let stderr = "";
|
|
276
277
|
let timedOut = false;
|
|
@@ -280,6 +281,7 @@ function runCommandHook(entry, payload, ctx) {
|
|
|
280
281
|
return;
|
|
281
282
|
settled = true;
|
|
282
283
|
clearTimeout(timer);
|
|
284
|
+
ticket?.release();
|
|
283
285
|
resolve({ code, stdout, stderr, timedOut });
|
|
284
286
|
};
|
|
285
287
|
const timer = setTimeout(() => {
|
|
@@ -326,6 +328,7 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
326
328
|
stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
|
|
327
329
|
detached: true,
|
|
328
330
|
});
|
|
331
|
+
const ticket = adoptHostForegroundChild(child);
|
|
329
332
|
let killedByTimeout = false;
|
|
330
333
|
const timer = setTimeout(() => {
|
|
331
334
|
killedByTimeout = true;
|
|
@@ -355,6 +358,7 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
355
358
|
});
|
|
356
359
|
child.on("close", (code) => {
|
|
357
360
|
clearTimeout(timer);
|
|
361
|
+
ticket?.release();
|
|
358
362
|
noteAsyncOutcome(entry, ctx, event, payload, { code, rewake, killedByTimeout });
|
|
359
363
|
if (!rewake || code !== 2)
|
|
360
364
|
return;
|
|
@@ -121,6 +121,22 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
121
121
|
readonly cls: "F";
|
|
122
122
|
readonly note: "运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint.risk_descriptor` 这一格**不是合法 JSON 文本**(手改过的行 / 回滚残留 / 列语义漂;与同一 `rows.map` 里的 `rule_suggestions` 是同族坏法,两方言都是 TEXT 列)。放行的最坏后果 = **这一行**的分诊 descriptor 落 null:severity 折 0 排到有 descriptor 的行之后、`shadowedRule` 无可脱敏、读面派生的 `governanceForced` 徽章不亮 —— 三者都是**展示/分诊**,不是执法判据(门早已 park,真按 `shellGateDoctrine` 判的 `active-run-conflict.ts` 读的是 checkpoint blob 的 `get()`,不经本读面),且 `governanceForced` 的成文语义本就是「缺席 = 没有治理来源的**证据**」。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 cell 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),连同其余所有行一起消失。故 F 类;tag 与候选那格**分开计**,并计会让「哪一格在坏」在遥测里读不出来。";
|
|
123
123
|
};
|
|
124
|
+
readonly "server.sql-json.checkpoint.pending-steer-queue-lossy": {
|
|
125
|
+
readonly cls: "F";
|
|
126
|
+
readonly note: "S-550(`plugins/checkpoint-store-sql.ts` 的 `get()`,core 5.14.0 #147 的耐久转向队列):一条挂起 run 的 `checkpoint.pending_steer_queue` 这一格**没能完整读出来**(不是合法 JSON 文本 / 不是数组 / 某一条目的四个必备键不合形)⇒ 读不出的那部分**丢掉**,run 照常 resume。放行的最坏后果 = 一条已经 park 的操作员转向指令**没有**在这次 resume 上注入(人以为说过的话没送到);不放行的代价是这条挂起的 run **永远不可 resume** —— resume 是它唯一的出路,为一格读不出的转向把整条 run 变成砖是错误的失败方向(写侧刻意相反:`setPendingSteer` 往一份读不全的队列上追加会把读不出的条目永久抹掉,那里 fail-loud)。故 F 类。留痕从一条 `logger.info` 升成登记 tag 的理由:日志行在**遥测轴**上是零信号,「这台机器的转向队列一直读不出来」与「这台机器本来就没人 steer」在指标上完全同形。计数非零 = 去按日志里的 token 指纹查那一行。";
|
|
127
|
+
};
|
|
128
|
+
readonly "server.sql-json.checkpoint.outcome-unreadable": {
|
|
129
|
+
readonly cls: "F";
|
|
130
|
+
readonly note: "S-550(`plugins/checkpoint-store-sql.ts` 的 `findDecidedApprovalsForBinding`,`/decide` 的**幂等回放**腿):一条已决 checkpoint 行的 `outcome` 这一格读不出形(手改 / 撕裂写 / 迁移脚本改过)⇒ 这一行交出 `decision: null`(= 判别不出),调用方按「整次拒绝」处置。放行的最坏后果 = 一次 decide **重试**拿不到上次的判词,退回按新请求处理(而不是回放)—— 回放是锦上添花的腿,让一条读不出的 JSON 把一次重试变成 500 比这更坏;方向上没有任何批准被放宽(判不出就是判不出,调用方拒绝)。故 F 类。**只容忍体的问题**:`not-text`(读腿漏了投影)与连接/超时一律原样上抛 —— 那是码坏或故障,不是数据坏。";
|
|
131
|
+
};
|
|
132
|
+
readonly "server.sql-json.checkpoint.execution-outcome-unreadable": {
|
|
133
|
+
readonly cls: "F";
|
|
134
|
+
readonly note: "S-550(同上那条回放腿的第二格,`checkpoint.execution_outcome`,S-160 的执行判词):这一格读不出形 ⇒ 本行的 `executionOutcome` 交出 `null` = 「不知道这次批准有没有真的执行过」。放行的最坏后果 = 回放腿少一条「已执行」的证据,消费端据此**保守**处理(缺席在 core 的 `executionVerdict` 里读作 `unknown`,从来不读作「放行过」);不放行的代价与上一格同(一次 decide 重试 500)。⚠️ 同一格在 `get()` / `recordExecutionOutcome` 两条腿上**刻意相反**(抛,不容忍):那两条是结算记录的读写口,静默当缺席会让一份不同的记录覆盖已经在案的那份。tag 与上一格**分开计**:并计会让「哪一格在坏」在遥测里读不出来。";
|
|
135
|
+
};
|
|
136
|
+
readonly "server.sql-json.task-run.usage-result-unreadable": {
|
|
137
|
+
readonly cls: "F";
|
|
138
|
+
readonly note: "S-550(`plugins/run-store-sql.ts` 的 `usageScan`,`/v1/usage/*` 的扫窗腿):窗内某一行的 `task_run.result` 这一格读不出形 ⇒ 这一行**计 tasks、不计 token/cost**(`stats` 键缺席),其余行照常。放行的最坏后果 = 用量总数偏低一行的量(`projectUsageStats` 的成文降级口径逐字如此,三个后端共用);不放行的代价是**一行坏 cell 打掉整个租户的用量页**(扫窗上限 50k 行)。⚠️ 同一列在 `getRun` / `listRuns` 两条腿上**刻意相反**(抛,不容忍):那两条腿上这一格是提交方要的**答案**,把它折成「跑完了没有结果」是在替一次不知道的读编一个答案;而这里它只是一个加数。故 F 类;**只容忍体的问题**,`not-text`(读腿漏了投影)与连接 / 超时原样上抛。";
|
|
139
|
+
};
|
|
124
140
|
readonly "server.retention.sweep-report-dropped": {
|
|
125
141
|
readonly cls: "F";
|
|
126
142
|
readonly note: "留存 sweep 的**失败上报**自己抛了(poison error 的 toString / 一条抛异常的 logger transport)⇒ 那一条 warn 与 stuck 指标发不出去。放行的最坏后果是**可观测性**损失而不是数据面损失:删除与审计的正确性完全在店事务里,本臂只影响运维看不看得见「这个域连败了」。不放行的后果反而更坏 —— 一个从上报里逃出去的异常会打断 per-domain 循环、饿死后面每一个域(reapers 的同款保护逐字同理由)。";
|
|
@@ -112,6 +112,22 @@ export const FAIL_OPEN_TAGS = {
|
|
|
112
112
|
cls: "F",
|
|
113
113
|
note: "运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint.risk_descriptor` 这一格**不是合法 JSON 文本**(手改过的行 / 回滚残留 / 列语义漂;与同一 `rows.map` 里的 `rule_suggestions` 是同族坏法,两方言都是 TEXT 列)。放行的最坏后果 = **这一行**的分诊 descriptor 落 null:severity 折 0 排到有 descriptor 的行之后、`shadowedRule` 无可脱敏、读面派生的 `governanceForced` 徽章不亮 —— 三者都是**展示/分诊**,不是执法判据(门早已 park,真按 `shellGateDoctrine` 判的 `active-run-conflict.ts` 读的是 checkpoint blob 的 `get()`,不经本读面),且 `governanceForced` 的成文语义本就是「缺席 = 没有治理来源的**证据**」。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 cell 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),连同其余所有行一起消失。故 F 类;tag 与候选那格**分开计**,并计会让「哪一格在坏」在遥测里读不出来。",
|
|
114
114
|
},
|
|
115
|
+
"server.sql-json.checkpoint.pending-steer-queue-lossy": {
|
|
116
|
+
cls: "F",
|
|
117
|
+
note: "S-550(`plugins/checkpoint-store-sql.ts` 的 `get()`,core 5.14.0 #147 的耐久转向队列):一条挂起 run 的 `checkpoint.pending_steer_queue` 这一格**没能完整读出来**(不是合法 JSON 文本 / 不是数组 / 某一条目的四个必备键不合形)⇒ 读不出的那部分**丢掉**,run 照常 resume。放行的最坏后果 = 一条已经 park 的操作员转向指令**没有**在这次 resume 上注入(人以为说过的话没送到);不放行的代价是这条挂起的 run **永远不可 resume** —— resume 是它唯一的出路,为一格读不出的转向把整条 run 变成砖是错误的失败方向(写侧刻意相反:`setPendingSteer` 往一份读不全的队列上追加会把读不出的条目永久抹掉,那里 fail-loud)。故 F 类。留痕从一条 `logger.info` 升成登记 tag 的理由:日志行在**遥测轴**上是零信号,「这台机器的转向队列一直读不出来」与「这台机器本来就没人 steer」在指标上完全同形。计数非零 = 去按日志里的 token 指纹查那一行。",
|
|
118
|
+
},
|
|
119
|
+
"server.sql-json.checkpoint.outcome-unreadable": {
|
|
120
|
+
cls: "F",
|
|
121
|
+
note: "S-550(`plugins/checkpoint-store-sql.ts` 的 `findDecidedApprovalsForBinding`,`/decide` 的**幂等回放**腿):一条已决 checkpoint 行的 `outcome` 这一格读不出形(手改 / 撕裂写 / 迁移脚本改过)⇒ 这一行交出 `decision: null`(= 判别不出),调用方按「整次拒绝」处置。放行的最坏后果 = 一次 decide **重试**拿不到上次的判词,退回按新请求处理(而不是回放)—— 回放是锦上添花的腿,让一条读不出的 JSON 把一次重试变成 500 比这更坏;方向上没有任何批准被放宽(判不出就是判不出,调用方拒绝)。故 F 类。**只容忍体的问题**:`not-text`(读腿漏了投影)与连接/超时一律原样上抛 —— 那是码坏或故障,不是数据坏。",
|
|
122
|
+
},
|
|
123
|
+
"server.sql-json.checkpoint.execution-outcome-unreadable": {
|
|
124
|
+
cls: "F",
|
|
125
|
+
note: "S-550(同上那条回放腿的第二格,`checkpoint.execution_outcome`,S-160 的执行判词):这一格读不出形 ⇒ 本行的 `executionOutcome` 交出 `null` = 「不知道这次批准有没有真的执行过」。放行的最坏后果 = 回放腿少一条「已执行」的证据,消费端据此**保守**处理(缺席在 core 的 `executionVerdict` 里读作 `unknown`,从来不读作「放行过」);不放行的代价与上一格同(一次 decide 重试 500)。⚠️ 同一格在 `get()` / `recordExecutionOutcome` 两条腿上**刻意相反**(抛,不容忍):那两条是结算记录的读写口,静默当缺席会让一份不同的记录覆盖已经在案的那份。tag 与上一格**分开计**:并计会让「哪一格在坏」在遥测里读不出来。",
|
|
126
|
+
},
|
|
127
|
+
"server.sql-json.task-run.usage-result-unreadable": {
|
|
128
|
+
cls: "F",
|
|
129
|
+
note: "S-550(`plugins/run-store-sql.ts` 的 `usageScan`,`/v1/usage/*` 的扫窗腿):窗内某一行的 `task_run.result` 这一格读不出形 ⇒ 这一行**计 tasks、不计 token/cost**(`stats` 键缺席),其余行照常。放行的最坏后果 = 用量总数偏低一行的量(`projectUsageStats` 的成文降级口径逐字如此,三个后端共用);不放行的代价是**一行坏 cell 打掉整个租户的用量页**(扫窗上限 50k 行)。⚠️ 同一列在 `getRun` / `listRuns` 两条腿上**刻意相反**(抛,不容忍):那两条腿上这一格是提交方要的**答案**,把它折成「跑完了没有结果」是在替一次不知道的读编一个答案;而这里它只是一个加数。故 F 类;**只容忍体的问题**,`not-text`(读腿漏了投影)与连接 / 超时原样上抛。",
|
|
130
|
+
},
|
|
115
131
|
"server.retention.sweep-report-dropped": {
|
|
116
132
|
cls: "F",
|
|
117
133
|
note: "留存 sweep 的**失败上报**自己抛了(poison error 的 toString / 一条抛异常的 logger transport)⇒ 那一条 warn 与 stuck 指标发不出去。放行的最坏后果是**可观测性**损失而不是数据面损失:删除与审计的正确性完全在店事务里,本臂只影响运维看不看得见「这个域连败了」。不放行的后果反而更坏 —— 一个从上报里逃出去的异常会打断 per-domain 循环、饿死后面每一个域(reapers 的同款保护逐字同理由)。",
|
|
@@ -515,7 +515,7 @@ export declare function ensurePgApprovalAskSchema(q: PgQueryFn): Promise<void>;
|
|
|
515
515
|
*
|
|
516
516
|
* 病:本仓不发 `ALTER TABLE`(SCHEMA POLICY:改列就改 CREATE + 删库重建)。于是一台**没删表**就升上来
|
|
517
517
|
* 的部署,`CREATE TABLE IF NOT EXISTS` 对存量 `approval_ask` 是空操作 —— 本车新加的
|
|
518
|
-
* `rule_command` / `rule_scope_root` 两列不在,而 `ensureAsk` 的 INSERT 与 `
|
|
518
|
+
* `rule_command` / `rule_scope_root` 两列不在,而 `ensureAsk` 的 INSERT 与 `askCols` 的每一条读
|
|
519
519
|
* **无条件**引用它们。后果不是「少一个新功能」,是**整条流内审批协议**在那台机器上是坏的:
|
|
520
520
|
* · 每一只 ask 的 `ensureAsk` 撞 unknown column ⇒ 被 `noteStoreError` 吞掉 ⇒ 整只 ask 退回 park 路由
|
|
521
521
|
* (卡不落库、`GET /v1/approvals` 的重放面恒空、迟到决议腿恒 404);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
2
2
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
3
3
|
import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
|
|
4
|
+
import { decodeJsonColumn, foldJsonColumn, jsonColumnText, readAnyJsonValueColumn, readJsonObjectColumn } from "./sql-json-column.js";
|
|
4
5
|
import { ensureIndexes } from "./ensure-index.js";
|
|
5
6
|
export function assertIntegralEpochMs(field, value) {
|
|
6
7
|
if (!Number.isSafeInteger(value)) {
|
|
@@ -211,7 +212,7 @@ export async function ensurePgApprovalAskSchema(q) {
|
|
|
211
212
|
}
|
|
212
213
|
export async function assertApprovalAskRuleMaterialSchema(query, dialect) {
|
|
213
214
|
try {
|
|
214
|
-
await query(`SELECT rule_command, rule_scope_root, updated_input FROM ${APPROVAL_ASK_TABLE} WHERE 1=0`);
|
|
215
|
+
await query(`SELECT rule_command, rule_scope_root, ${jsonColumnText(dialect, "updated_input")} FROM ${APPROVAL_ASK_TABLE} WHERE 1=0`);
|
|
215
216
|
}
|
|
216
217
|
catch (err) {
|
|
217
218
|
if (!isMissingColumnError(err, dialect))
|
|
@@ -231,14 +232,21 @@ export async function assertApprovalAskRuleMaterialSchema(query, dialect) {
|
|
|
231
232
|
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
function askCols(dialect) {
|
|
236
|
+
return [
|
|
237
|
+
"ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state",
|
|
238
|
+
"provisional, rev, decision",
|
|
239
|
+
jsonColumnText(dialect, "decision_actor"),
|
|
240
|
+
"decision_note, settled_by",
|
|
241
|
+
jsonColumnText(dialect, "updated_input"),
|
|
242
|
+
"decided_at_ms, denied_reason, gate_token",
|
|
243
|
+
"gate_bound_call_id, gate_bound_input_hash, idempotency_key",
|
|
244
|
+
jsonColumnText(dialect, "card_json"),
|
|
245
|
+
"rule_command, rule_scope_root, schema_version, expires_at_ms",
|
|
246
|
+
"created_at_ms, updated_at_ms",
|
|
247
|
+
].join(", ");
|
|
241
248
|
}
|
|
249
|
+
const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, rev, created_at_ms, updated_at_ms";
|
|
242
250
|
function toBool(v) {
|
|
243
251
|
return Boolean(Number(v));
|
|
244
252
|
}
|
|
@@ -249,6 +257,15 @@ function strOrNull(v) {
|
|
|
249
257
|
return v === null || v === undefined ? null : String(v);
|
|
250
258
|
}
|
|
251
259
|
function mapAskRow(r) {
|
|
260
|
+
const askId = String(r.ask_id);
|
|
261
|
+
try {
|
|
262
|
+
return mapAskRowBody(r, askId);
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
throw foldJsonColumn(e, (m) => new Error(`approval-ask-store: ${m} — refusing rather than serve an ask row nobody can read`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function mapAskRowBody(r, askId) {
|
|
252
269
|
return {
|
|
253
270
|
askId: String(r.ask_id),
|
|
254
271
|
taskId: String(r.task_id),
|
|
@@ -264,17 +281,17 @@ function mapAskRow(r) {
|
|
|
264
281
|
provisional: toBool(r.provisional),
|
|
265
282
|
rev: Number(r.rev),
|
|
266
283
|
decision: strOrNull(r.decision) ?? null,
|
|
267
|
-
decisionActor: r.decision_actor == null ? null :
|
|
284
|
+
decisionActor: r.decision_actor == null ? null : decodeJsonColumn(r.decision_actor, readJsonObjectColumn, `${APPROVAL_ASK_TABLE}.decision_actor for ask ${askId}`),
|
|
268
285
|
decisionNote: strOrNull(r.decision_note),
|
|
269
286
|
settledBy: strOrNull(r.settled_by),
|
|
270
|
-
...(r.updated_input == null ? {} : { updatedInput:
|
|
287
|
+
...(r.updated_input == null ? {} : { updatedInput: decodeJsonColumn(r.updated_input, readAnyJsonValueColumn, `${APPROVAL_ASK_TABLE}.updated_input for ask ${askId}`) }),
|
|
271
288
|
decidedAtMs: numOrNull(r.decided_at_ms),
|
|
272
289
|
deniedReason: strOrNull(r.denied_reason),
|
|
273
290
|
gateToken: strOrNull(r.gate_token),
|
|
274
291
|
gateBoundCallId: strOrNull(r.gate_bound_call_id),
|
|
275
292
|
gateBoundInputHash: strOrNull(r.gate_bound_input_hash),
|
|
276
293
|
idempotencyKey: strOrNull(r.idempotency_key),
|
|
277
|
-
cardJson:
|
|
294
|
+
cardJson: decodeJsonColumn(r.card_json, readJsonObjectColumn, `${APPROVAL_ASK_TABLE}.card_json for ask ${askId}`),
|
|
278
295
|
ruleCommand: strOrNull(r.rule_command),
|
|
279
296
|
ruleScopeRoot: strOrNull(r.rule_scope_root),
|
|
280
297
|
schemaVersion: Number(r.schema_version),
|
|
@@ -337,7 +354,7 @@ export class SqlApprovalAskStore {
|
|
|
337
354
|
return isDupKeyError(this.db.dialect, e);
|
|
338
355
|
}
|
|
339
356
|
async getAskOn(conn, askId) {
|
|
340
|
-
const { rows } = await conn.query(this.q(`SELECT ${
|
|
357
|
+
const { rows } = await conn.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [askId]);
|
|
341
358
|
return rows[0] ? mapAskRow(rows[0]) : null;
|
|
342
359
|
}
|
|
343
360
|
async getBatchOn(conn, batchId) {
|
|
@@ -485,7 +502,7 @@ export class SqlApprovalAskStore {
|
|
|
485
502
|
}
|
|
486
503
|
const askRes = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state = ${ph(dialect, 1)}, rev = rev + 1, updated_at_ms = ${ph(dialect, 2)} WHERE ${pendingClaimWhere(dialect, 3)}`, [to, now, askId, batchId]);
|
|
487
504
|
if (askRes.affected !== 1) {
|
|
488
|
-
const sel = await conn.query(`SELECT ${
|
|
505
|
+
const sel = await conn.query(`SELECT ${askCols(dialect)} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ${ph(dialect, 1)} AND batch_id = ${ph(dialect, 2)} FOR UPDATE`, [askId, batchId]);
|
|
489
506
|
await conn.rollback();
|
|
490
507
|
return { claimed: false, current: projectAskTerminalClaimTruth(sel.rows[0] ? mapAskRow(sel.rows[0]) : null) };
|
|
491
508
|
}
|
|
@@ -614,32 +631,32 @@ export class SqlApprovalAskStore {
|
|
|
614
631
|
}
|
|
615
632
|
}
|
|
616
633
|
async listByState(state, limit) {
|
|
617
|
-
const { rows } = await this.db.query(this.q(`SELECT ${
|
|
634
|
+
const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE state = ? ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE state = $1 ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT $2`), [state, limit]);
|
|
618
635
|
return rows.map(mapAskRow);
|
|
619
636
|
}
|
|
620
637
|
async listPendingByTask(taskId, signal) {
|
|
621
638
|
if (signal?.aborted === true)
|
|
622
639
|
throw abortedReadError("listPendingByTask");
|
|
623
|
-
const { rows } = await withReadSignal(this.db.query(this.q(`SELECT ${
|
|
640
|
+
const { rows } = await withReadSignal(this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND state = 'STREAM_PENDING' ORDER BY created_at_ms ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND state = 'STREAM_PENDING' ORDER BY created_at_ms ASC`), [taskId]), signal, "listPendingByTask");
|
|
624
641
|
return rows.map(mapAskRow);
|
|
625
642
|
}
|
|
626
643
|
async listPendingBySession(sessionId, owner, signal) {
|
|
627
644
|
if (signal?.aborted === true)
|
|
628
645
|
throw abortedReadError("listPendingBySession");
|
|
629
646
|
const { rows } = await withReadSignal(this.db.query(owner === null
|
|
630
|
-
? this.q(`SELECT ${
|
|
631
|
-
: this.q(`SELECT ${
|
|
647
|
+
? this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`)
|
|
648
|
+
: this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
|
|
632
649
|
return rows.map(mapAskRow);
|
|
633
650
|
}
|
|
634
651
|
async getAsk(askId) {
|
|
635
|
-
const { rows } = await this.db.query(this.q(`SELECT ${
|
|
652
|
+
const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [
|
|
636
653
|
askId,
|
|
637
654
|
]);
|
|
638
655
|
return rows[0] ? mapAskRow(rows[0]) : null;
|
|
639
656
|
}
|
|
640
657
|
async getByIdempotencyKey(taskId, key) {
|
|
641
658
|
assertIdempotencyKeyShape(key);
|
|
642
|
-
const { rows } = await this.db.query(this.q(`SELECT ${
|
|
659
|
+
const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND idempotency_key = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND idempotency_key = $2`), [taskId, key]);
|
|
643
660
|
return rows[0] ? mapAskRow(rows[0]) : null;
|
|
644
661
|
}
|
|
645
662
|
async deferReconcile(askId, expectedState, expectedRev, nowMs) {
|
|
@@ -2,8 +2,30 @@ import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "@sem
|
|
|
2
2
|
import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
|
|
3
3
|
import { isMysqlDupKeyError } from "./sql-errors.js";
|
|
4
4
|
import { ensureIndexes } from "./ensure-index.js";
|
|
5
|
+
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
|
|
5
6
|
export const BACKGROUND_AGENT_TABLE = "background_agent";
|
|
6
|
-
|
|
7
|
+
function summaryCols(dialect) {
|
|
8
|
+
return ("handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, " +
|
|
9
|
+
`spawned_at_ms, updated_at_ms, settled_at_ms, ${jsonColumnText(dialect, "usage_json")}`);
|
|
10
|
+
}
|
|
11
|
+
const readAgentRecordBody = createObjectColumnReader();
|
|
12
|
+
const readUsageBody = createObjectColumnReader();
|
|
13
|
+
function readRecordColumn(v, handle) {
|
|
14
|
+
try {
|
|
15
|
+
return decodeJsonColumn(v, readAgentRecordBody, `${BACKGROUND_AGENT_TABLE}.record_json for handle ${handle}`);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
throw foldJsonColumn(e, (m) => new Error(`BackgroundAgentStore: ${m} — refusing rather than serve a hollow agent record`));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function readUsageColumn(v, handle) {
|
|
22
|
+
try {
|
|
23
|
+
return decodeNullableJsonColumn(v, readUsageBody, `${BACKGROUND_AGENT_TABLE}.usage_json for handle ${handle}`);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
throw foldJsonColumn(e, (m) => new Error(`BackgroundAgentStore: ${m} — refusing rather than report a usage figure nobody can read`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, admitted_org_scopes, admitted_org_write_scope, record_json";
|
|
8
30
|
const PROJ_N = PROJ_COLS.split(", ").length;
|
|
9
31
|
function assertScope(record) {
|
|
@@ -52,9 +74,7 @@ function rowToSummary(r) {
|
|
|
52
74
|
spawnedAt: Number(r.spawned_at_ms),
|
|
53
75
|
updatedAt: Number(r.updated_at_ms),
|
|
54
76
|
...(r.settled_at_ms !== null && r.settled_at_ms !== undefined ? { settledAt: Number(r.settled_at_ms) } : {}),
|
|
55
|
-
...(
|
|
56
|
-
? { usage: JSON.parse(String(r.usage_json)) }
|
|
57
|
-
: {}),
|
|
77
|
+
...((u) => (u === undefined ? {} : { usage: u }))(readUsageColumn(r.usage_json, String(r.handle))),
|
|
58
78
|
};
|
|
59
79
|
}
|
|
60
80
|
function flippedForm(live, now) {
|
|
@@ -192,8 +212,8 @@ export class TiDBBackgroundAgentStore {
|
|
|
192
212
|
}
|
|
193
213
|
}
|
|
194
214
|
async get(handle, scope) {
|
|
195
|
-
const [rows] = (await this.pool.query(`SELECT record_json FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]));
|
|
196
|
-
return rows.length > 0 ?
|
|
215
|
+
const [rows] = (await this.pool.query(`SELECT ${jsonColumnText("tidb", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND handle = ?`, [scope, handle]));
|
|
216
|
+
return rows.length > 0 ? readRecordColumn(rows[0].record_json, handle) : null;
|
|
197
217
|
}
|
|
198
218
|
async update(handle, scope, record, expect) {
|
|
199
219
|
const stored = storedForm(record, handle, scope, expect.rev + 1);
|
|
@@ -211,12 +231,12 @@ export class TiDBBackgroundAgentStore {
|
|
|
211
231
|
return res.affectedRows === 1;
|
|
212
232
|
}
|
|
213
233
|
async listBySession(scope, sessionId, opts) {
|
|
214
|
-
const [rows] = (await this.pool.query(`SELECT ${
|
|
234
|
+
const [rows] = (await this.pool.query(`SELECT ${summaryCols("tidb")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND (session_anchor = ? OR root_session_id = ?)${opts?.status !== undefined ? " AND status = ?" : ""}
|
|
215
235
|
ORDER BY spawned_at_ms DESC, handle DESC${limitClause(opts?.limit)}`, opts?.status !== undefined ? [scope, sessionId, sessionId, opts.status] : [scope, sessionId, sessionId]));
|
|
216
236
|
return rows.map(rowToSummary);
|
|
217
237
|
}
|
|
218
238
|
async listByScope(scope, opts) {
|
|
219
|
-
const [rows] = (await this.pool.query(`SELECT ${
|
|
239
|
+
const [rows] = (await this.pool.query(`SELECT ${summaryCols("tidb")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ?${opts?.status !== undefined ? " AND status = ?" : ""}
|
|
220
240
|
ORDER BY spawned_at_ms DESC, handle DESC${limitClause(opts?.limit)}`, opts?.status !== undefined ? [scope, opts.status] : [scope]));
|
|
221
241
|
return rows.map(rowToSummary);
|
|
222
242
|
}
|
|
@@ -229,9 +249,9 @@ export class TiDBBackgroundAgentStore {
|
|
|
229
249
|
return 0;
|
|
230
250
|
const flipped = new Set();
|
|
231
251
|
if (opts.staleRunningMaxAgeMs !== undefined) {
|
|
232
|
-
const [stale] = (await this.pool.query(`SELECT handle, record_json FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status = 'running' AND updated_at_ms < ?`, [scope, now - opts.staleRunningMaxAgeMs]));
|
|
252
|
+
const [stale] = (await this.pool.query(`SELECT handle, ${jsonColumnText("tidb", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status = 'running' AND updated_at_ms < ?`, [scope, now - opts.staleRunningMaxAgeMs]));
|
|
233
253
|
for (const row of stale) {
|
|
234
|
-
const live =
|
|
254
|
+
const live = readRecordColumn(row.record_json, String(row.handle));
|
|
235
255
|
if (await this.update(String(row.handle), scope, flippedForm(live, now), { rev: live.rev }))
|
|
236
256
|
flipped.add(String(row.handle));
|
|
237
257
|
}
|
|
@@ -272,8 +292,8 @@ export class PgBackgroundAgentStore {
|
|
|
272
292
|
}
|
|
273
293
|
}
|
|
274
294
|
async get(handle, scope) {
|
|
275
|
-
const { rows } = await this.q(`SELECT record_json FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
|
|
276
|
-
return rows.length > 0 ?
|
|
295
|
+
const { rows } = await this.q(`SELECT ${jsonColumnText("pg", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND handle = $2`, [scope, handle]);
|
|
296
|
+
return rows.length > 0 ? readRecordColumn(rows[0].record_json, handle) : null;
|
|
277
297
|
}
|
|
278
298
|
async update(handle, scope, record, expect) {
|
|
279
299
|
const stored = storedForm(record, handle, scope, expect.rev + 1);
|
|
@@ -293,12 +313,12 @@ export class PgBackgroundAgentStore {
|
|
|
293
313
|
return rows.length === 1;
|
|
294
314
|
}
|
|
295
315
|
async listBySession(scope, sessionId, opts) {
|
|
296
|
-
const { rows } = await this.q(`SELECT ${
|
|
316
|
+
const { rows } = await this.q(`SELECT ${summaryCols("pg")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND (session_anchor = $2 OR root_session_id = $3)${opts?.status !== undefined ? " AND status = $4" : ""}
|
|
297
317
|
ORDER BY spawned_at_ms DESC, handle DESC${limitClause(opts?.limit)}`, opts?.status !== undefined ? [scope, sessionId, sessionId, opts.status] : [scope, sessionId, sessionId]);
|
|
298
318
|
return rows.map(rowToSummary);
|
|
299
319
|
}
|
|
300
320
|
async listByScope(scope, opts) {
|
|
301
|
-
const { rows } = await this.q(`SELECT ${
|
|
321
|
+
const { rows } = await this.q(`SELECT ${summaryCols("pg")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1${opts?.status !== undefined ? " AND status = $2" : ""}
|
|
302
322
|
ORDER BY spawned_at_ms DESC, handle DESC${limitClause(opts?.limit)}`, opts?.status !== undefined ? [scope, opts.status] : [scope]);
|
|
303
323
|
return rows.map(rowToSummary);
|
|
304
324
|
}
|
|
@@ -311,9 +331,9 @@ export class PgBackgroundAgentStore {
|
|
|
311
331
|
return 0;
|
|
312
332
|
const affected = new Set();
|
|
313
333
|
if (opts.staleRunningMaxAgeMs !== undefined) {
|
|
314
|
-
const { rows: stale } = await this.q(`SELECT handle, record_json FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND status = 'running' AND updated_at_ms < $2`, [scope, now - opts.staleRunningMaxAgeMs]);
|
|
334
|
+
const { rows: stale } = await this.q(`SELECT handle, ${jsonColumnText("pg", "record_json")} FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND status = 'running' AND updated_at_ms < $2`, [scope, now - opts.staleRunningMaxAgeMs]);
|
|
315
335
|
for (const row of stale) {
|
|
316
|
-
const live =
|
|
336
|
+
const live = readRecordColumn(row.record_json, String(row.handle));
|
|
317
337
|
if (await this.update(String(row.handle), scope, flippedForm(live, now), { rev: live.rev }))
|
|
318
338
|
affected.add(String(row.handle));
|
|
319
339
|
}
|
|
@@ -381,8 +381,12 @@ export declare function approvalDecisionOfWinner(winner: ResolvedOutcome | undef
|
|
|
381
381
|
* - `{ readable: true, toolCallId: "…" }` —— 读出来了,拿去比。
|
|
382
382
|
*
|
|
383
383
|
* 逐层 `typeof` 收窄,不做裸 `as` 断言:blob 是持久层读回来的 `unknown`,是信任边界(宪法 [ref])。
|
|
384
|
+
*
|
|
385
|
+
* 🔴 S-550:入参是**已经解过码的体**,不是一格列文本 —— 本函数不再自己 `JSON.parse`(SQL 车道的解码
|
|
386
|
+
* 唯一属主是 `sql-json-column.ts`,见 {@link pendingActionOfColumn};local 孪生本来就递一只对象进来)。
|
|
387
|
+
* 于是「串就再解一次」这条读法在这里按构造消失,两条车道递进来的东西同形。
|
|
384
388
|
*/
|
|
385
|
-
export declare function pendingActionToolCallId(
|
|
389
|
+
export declare function pendingActionToolCallId(body: unknown): {
|
|
386
390
|
readable: true;
|
|
387
391
|
toolCallId: string | null;
|
|
388
392
|
boundInputHash: string | null;
|