@sema-agent/server 7.82.0 → 7.83.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 +17 -14
- package/dist/bench/s1/arms.d.ts +4 -4
- package/dist/bench/s1/arms.js +1 -2
- package/dist/bench/s1/live-deps.js +0 -1
- package/dist/bench/s1/runner-ctx.d.ts +3 -3
- package/dist/bench/s1/tasks.d.ts +8 -4
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/config-catalog.js +2 -3
- package/dist/config.js +1 -0
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/server.d.ts +8 -0
- package/dist/leader/endpoint.js +2 -0
- package/dist/leader/leader.d.ts +33 -6
- package/dist/leader/leader.js +54 -45
- package/dist/leader/merge.d.ts +38 -29
- package/dist/leader/merge.js +45 -38
- package/dist/leader/wire.d.ts +2 -17
- package/dist/leader/wire.js +5 -58
- package/dist/observability/fail-open.d.ts +5 -5
- package/dist/observability/fail-open.js +5 -2
- package/dist/observability/run-terminal-log.d.ts +0 -7
- package/dist/observability/run-terminal-log.js +9 -20
- package/dist/plugins/adoption-log-sql.d.ts +3 -0
- package/dist/plugins/adoption-log-sql.js +5 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +3 -0
- package/dist/plugins/approval-ask-store-sql.js +9 -5
- package/dist/plugins/background-agent-store-sql.d.ts +3 -0
- package/dist/plugins/background-agent-store-sql.js +7 -2
- package/dist/plugins/device-store-sql.d.ts +3 -0
- package/dist/plugins/device-store-sql.js +9 -4
- package/dist/plugins/ensure-index.d.ts +42 -0
- package/dist/plugins/ensure-index.js +81 -0
- package/dist/plugins/ensure-schema-lock-policy.d.ts +29 -0
- package/dist/plugins/ensure-schema-lock-policy.js +31 -0
- package/dist/plugins/image-bake-store-sql.d.ts +3 -0
- package/dist/plugins/image-bake-store-sql.js +6 -2
- package/dist/plugins/image-index-sql.d.ts +3 -0
- package/dist/plugins/image-index-sql.js +8 -4
- package/dist/plugins/leader-run-store-sql.d.ts +3 -0
- package/dist/plugins/leader-run-store-sql.js +6 -2
- package/dist/plugins/mailbox-store-sql.d.ts +3 -0
- package/dist/plugins/mailbox-store-sql.js +6 -1
- package/dist/plugins/memory-engine-pg.d.ts +4 -0
- package/dist/plugins/memory-engine-pg.js +6 -6
- package/dist/plugins/memory-engine-tidb.js +5 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +5 -0
- package/dist/plugins/memory-sync-store-pg.js +9 -2
- package/dist/plugins/memory-sync-store-tidb.js +6 -0
- package/dist/plugins/one-time-migrations.d.ts +1 -1
- package/dist/plugins/one-time-migrations.js +7 -14
- package/dist/plugins/outcome-ledger-sql.d.ts +3 -0
- package/dist/plugins/outcome-ledger-sql.js +7 -3
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +9 -5
- package/dist/plugins/pg-cost-quota.d.ts +3 -0
- package/dist/plugins/pg-cost-quota.js +5 -1
- package/dist/plugins/pg-pool.d.ts +23 -2
- package/dist/plugins/pg-pool.js +22 -37
- package/dist/plugins/pg-rate-limiter.d.ts +3 -0
- package/dist/plugins/pg-rate-limiter.js +5 -1
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +8 -4
- package/dist/plugins/remote-env-adb.d.ts +62 -3
- package/dist/plugins/remote-env-adb.js +27 -15
- package/dist/plugins/remote-env-host.js +1 -1
- package/dist/plugins/remote-shell.d.ts +7 -11
- package/dist/plugins/remote-shell.js +3 -3
- package/dist/plugins/retention-store-sql.d.ts +3 -0
- package/dist/plugins/retention-store-sql.js +7 -3
- package/dist/plugins/roster-store-sql.d.ts +3 -0
- package/dist/plugins/roster-store-sql.js +6 -1
- package/dist/plugins/schema-indexes.d.ts +21 -0
- package/dist/plugins/schema-indexes.js +25 -0
- package/dist/plugins/task-attachment-store.d.ts +3 -0
- package/dist/plugins/task-attachment-store.js +7 -2
- package/dist/plugins/tidb-pool.d.ts +15 -3
- package/dist/plugins/tidb-pool.js +38 -15
- package/dist/plugins/tool-result-store-sql.d.ts +3 -0
- package/dist/plugins/tool-result-store-sql.js +5 -1
- package/dist/plugins/web-search.d.ts +50 -2
- package/dist/plugins/web-search.js +9 -2
- package/dist/trace/project.d.ts +2 -1
- package/dist/trace/redact.d.ts +6 -0
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { contentForkRelation, fastForwardSharedContentDiverged } from "../sessio
|
|
|
8
8
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
9
9
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
10
10
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
11
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
11
12
|
const isDupKey = isPgUniqueViolation;
|
|
12
13
|
function parseEntry(p) {
|
|
13
14
|
return (typeof p === "string" ? JSON.parse(p) : p);
|
|
@@ -36,8 +37,6 @@ export const PG_SESSION_SCHEMA = [
|
|
|
36
37
|
placed_at TIMESTAMPTZ(3),
|
|
37
38
|
PRIMARY KEY (session_id)
|
|
38
39
|
)`,
|
|
39
|
-
`CREATE INDEX IF NOT EXISTS idx_session_meta_owner ON session_meta (owner)`,
|
|
40
|
-
`CREATE INDEX IF NOT EXISTS idx_session_meta_placement ON session_meta (placement_kind, placement_scope, placed_at)`,
|
|
41
40
|
`CREATE TABLE IF NOT EXISTS session_event (
|
|
42
41
|
session_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
43
42
|
seq BIGINT NOT NULL,
|
|
@@ -48,12 +47,17 @@ export const PG_SESSION_SCHEMA = [
|
|
|
48
47
|
ts TIMESTAMPTZ(3) NOT NULL,
|
|
49
48
|
PRIMARY KEY (session_id, seq)
|
|
50
49
|
)`,
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
];
|
|
51
|
+
export const SESSION_INDEXES = [
|
|
52
|
+
{ table: "session_meta", name: "idx_session_meta_owner", columns: ["owner"] },
|
|
53
|
+
{ table: "session_meta", name: "idx_session_meta_placement", columns: ["placement_kind", "placement_scope", "placed_at"] },
|
|
54
|
+
{ table: "session_event", name: "uq_session_event_entry", columns: ["session_id", "entry_id"], unique: true },
|
|
55
|
+
{ table: "session_event", name: "idx_session_event_type", columns: ["session_id", "type", "seq"] },
|
|
53
56
|
];
|
|
54
57
|
export async function ensureSchema(pool) {
|
|
55
58
|
for (const stmt of PG_SESSION_SCHEMA)
|
|
56
59
|
await pool.query(stmt);
|
|
60
|
+
await ensureIndexes((t, p) => pool.query(t, p), "pg", SESSION_INDEXES);
|
|
57
61
|
}
|
|
58
62
|
export class PgSessionStorage extends BaseSessionStorage {
|
|
59
63
|
pool;
|
|
@@ -41,7 +41,23 @@ export interface AdbEnvDeps {
|
|
|
41
41
|
connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
|
|
42
42
|
}
|
|
43
43
|
/** [R78]#1: adb stderr markers that mean the DEVICE TRANSPORT died mid-op (not a normal command failure) —
|
|
44
|
-
* typed retryable (reconnect + retry per idempotency), never a fake command result. Exported for tests.
|
|
44
|
+
* typed retryable (reconnect + retry per idempotency), never a fake command result. Exported for tests.
|
|
45
|
+
*
|
|
46
|
+
* 🔴 **S-401 把这个判据的输入面改了一半(复审 F12 纠正:原来写的「此前 stderr 恒空 ⇒ 整条是死码」是
|
|
47
|
+
* **overclaim**,被本车自己的真机读数推翻)**。`exec-out` 合流的是**设备**的 stderr;**adb 客户端自己的**
|
|
48
|
+
* 诊断在两种传输下都照样走宿主 stderr。真机实测(同一台设备,拔掉 serial):
|
|
49
|
+
* `adb -s BOGUS exec-out 'true'` ⇒ host exit **255** + stderr `error: device 'BOGUS' not found`;
|
|
50
|
+
* `adb -s BOGUS shell -T -- 'true'` ⇒ host exit **1** + stderr `adb: device 'BOGUS' not found`。
|
|
51
|
+
* ⇒ 本判据在 S-401 之前**对 adb 客户端级的传输故障是工作的**(这也正是原作者把词表写成这些 adb 自家
|
|
52
|
+
* 措辞的原因);S-401 改变的是**设备侧 stderr 从此也进这条通道**。
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ **随之而来的、如实登记的一格**:分流之后落到这里的 stderr **既有 adb 自己的诊断,也有设备命令自己的
|
|
55
|
+
* 输出**(adb 客户端把两者写在同一个 fd 上,真机实测无法在传输层分开)。于是设备上一条自己打印
|
|
56
|
+
* `Connection reset by peer` 的命令(curl/nc)会被判成 `transport_lost`:本次命令结果不返回、`connected`
|
|
57
|
+
* 置假、下一次 exec fail-closed 直到 reconnect。**刻意不收窄词表**——词表收窄的方向是「漏判一次真掉线」,
|
|
58
|
+
* 那是 fail-open;误判的方向是多断一次连接,是 fail-closed。[ref] 轴上后者必须赢(能力探测的失败方向要与
|
|
59
|
+
* 能力语义方向一致)。ssh / k8s 两腿不吃这一格,因为它们的传输层自己给「通道关闭却没有 exit status」这个
|
|
60
|
+
* **结构**信号;adb 的 shell v2 没有等价物,所以这里只能靠标记词,**这条臂就把话说在这里**。 */
|
|
45
61
|
export declare function isAdbTransportLost(stderr: string): boolean;
|
|
46
62
|
export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
47
63
|
/**
|
|
@@ -123,8 +139,15 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
123
139
|
execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
|
|
124
140
|
absolutePath(p: string): Promise<Result<string, FileError>>;
|
|
125
141
|
joinPath(parts: string[]): Promise<Result<string, FileError>>;
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
142
|
+
/** 文件面的门 —— 与 host/local-docker/k8s/e2b 的姿态对齐(它们的文件面在未连/已拆时都 fail-closed)。
|
|
143
|
+
*
|
|
144
|
+
* 🔴 **S-401 复审 F2:这道门此前只查 `destroyed`,不查 `connected`**,于是 `readBinaryFile` /
|
|
145
|
+
* `readTextFile` / `readTextLines` / `appendFile` 能在**从没连过**的 env 上直接打一次真设备往返。
|
|
146
|
+
* 在本批之前那只是个不一致;本批**新开的 `shell_v2` fail-closed 闸就长在 `connect()` 里**,
|
|
147
|
+
* 所以「不查 connected」= 那道闸对整个读面**不生效** —— 而 `readTextFile` 把错误文本当文件内容返回
|
|
148
|
+
* 正是本 P1 点名的症状之一。闸的射程必须覆盖它要保护的每一个面,否则它只是一句话。
|
|
149
|
+
* ⇒ 规则收成一条:**这只 env 在 connect 成功之前、destroy 之后,文件面一个字都不答。**
|
|
150
|
+
* (`exec`/`execStream` 本就是这条规则,见 `:257`/`:276`;文件面从此同判,不再是两套。) */
|
|
128
151
|
private fsGate;
|
|
129
152
|
readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
|
|
130
153
|
readTextFile(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
|
|
@@ -162,6 +185,42 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
|
|
|
162
185
|
}): Promise<Result<string, FileError>>;
|
|
163
186
|
/** Spawn the adb binary with the given args; collect stdout/stderr (+ raw bytes when binary), bound by timeout/abort. */
|
|
164
187
|
private adb;
|
|
188
|
+
/**
|
|
189
|
+
* 🔴 **S-401 —— 本适配器跑设备命令的唯一往返形**(`exec` / `execStream` / `readBinaryFile` 三个调用点
|
|
190
|
+
* 共用一只;此前 `exec-out` 写了三遍,于是「传输到底给不给退出码」这件事没有属主)。
|
|
191
|
+
*
|
|
192
|
+
* **`adb shell`(shell protocol v2)而不是 `adb exec-out`。** 协议出处 = adb 自己的 `adb help`:
|
|
193
|
+
* `-x: disable remote exit codes and stdout/stderr separation` —— 即**不带** `-x` 的 `adb shell` 就是
|
|
194
|
+
* 「远端退出码 + stdout/stderr 分流」那一形。`exec-out` 走的是另一条 `exec:` 服务:单条裸流,设备
|
|
195
|
+
* stderr **合流进 stdout**,宿主进程退出码**恒 0**。
|
|
196
|
+
*
|
|
197
|
+
* 真机实测(车GV 2026-09-17,SM-S721B / Android 15 / toybox 0.8.11 / adb 1.0.41 `35.0.2`,设备 features
|
|
198
|
+
* 含 `shell_v2`),逐条复现而不是从文档推断:
|
|
199
|
+
* - `exec-out 'false'` ⇒ 宿主 exit **0**;`shell -T -- 'false'` ⇒ **1**;`shell -T -- 'exit 7'` ⇒ **7**
|
|
200
|
+
* - `exec-out 'realpath -- <坏链>'` ⇒ exit 0 + 错误文本在 **stdout**;`shell -T --` ⇒ exit 1 + 在 **stderr**
|
|
201
|
+
* - 1 MiB 随机字节 `cat` 回读:两形 md5 **相同**(`shell -T` 无 CRLF 转换、无包裹字节),故 binary 面照用
|
|
202
|
+
* - 同一条 `realpath` 的墙钟:`shell -T` 68 ms/次 vs `exec-out` 79 ms/次(n=10)—— 不是性能代价
|
|
203
|
+
*
|
|
204
|
+
* 修前的后果链([ref] 安全轴静默 fail-open):共享基座 `posix-shell-fs.ts` 的**每一处**失败判据(7 处)
|
|
205
|
+
* 与链原语 `remote-shell.ts` 的 `runSymlinkChain`(第 8 处)认的都是 `exitCode !== 0`,而这条传输恒答 0
|
|
206
|
+
* ⇒ adb 腿上 `realpath` / `readlink` / `stat` / `ls` / `cat` **永不失败**,`canonicalPath` 把那行**错误
|
|
207
|
+
* 文本**当成 canonical key 交给敏感路径门。⚠️ **复审 F15 纠正:原文写「五处」是数错了**,而且漏掉的
|
|
208
|
+
* 第 8 处恰恰是最贵的一处 —— `canonicalChain`(S-362 / core 7.21.0 [ref],上一版刚接)在 exit 恒 0 下会把
|
|
209
|
+
* **合流进 stdout 的错误文本当成链上的跳**解析出来,等于给 core 的路径判官递一条**伪造的** symlink 链。
|
|
210
|
+
*
|
|
211
|
+
* 🔴 **`-T` 与 `--` 都是承重件,不是装饰**:
|
|
212
|
+
* - `-T` = 不分配 pty。带 pty 的交互 shell 会把 stderr 并进 stdout 并做 CRLF 转换(council §9.2 的那句话
|
|
213
|
+
* 本身没错,错的是它被用来给 `exec-out` 背书)。给了命令的 `adb shell` 默认本就不开 pty,`-T` 是**显式**
|
|
214
|
+
* 关闭,不依赖那个默认。
|
|
215
|
+
* - `--` = 终止 adb 自己的旗标解析。没有它,**首字符为 `-` 的设备命令会被 adb 当成自己的旗标吃掉**
|
|
216
|
+
* (真机复现:`adb shell -T '-x'` 不报错,直接变成一个挂住的交互 shell)—— 而 `-x` 恰好就是关掉
|
|
217
|
+
* 远端退出码与分流的那根旗标,即「一条以 `-x` 开头的命令能静默把本修关回去」。真机验证 `--` 之后的
|
|
218
|
+
* 参数原样落到设备 shell(`adb shell -T -- '-x'` ⇒ 设备答 `/system/bin/sh: -c: requires an argument`)。
|
|
219
|
+
*
|
|
220
|
+
* 命令永远是**最后一个**参数,整条包在一个 argv 元素里(设备侧 `sh -c` 收全串,与 `withCwdEnv` 的
|
|
221
|
+
* `cd … && …` 前缀语义一致)。
|
|
222
|
+
*/
|
|
223
|
+
private deviceShellArgs;
|
|
165
224
|
private withCwdEnv;
|
|
166
225
|
}
|
|
167
226
|
/**
|
|
@@ -5,7 +5,7 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
|
|
6
6
|
import { signalNumber } from "./host-platform.js";
|
|
7
7
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
8
|
-
import { fileErrorFromExec } from "./remote-env-file-error.js";
|
|
8
|
+
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
9
9
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
10
10
|
const PROVIDER = "adb";
|
|
11
11
|
const ok = (value) => ({ ok: true, value });
|
|
@@ -76,6 +76,18 @@ export class RemoteAdbExecutionEnv {
|
|
|
76
76
|
const state = st.value.stdout.trim();
|
|
77
77
|
const err = `${st.value.stdout} ${st.value.stderr}`.toLowerCase();
|
|
78
78
|
if (state === "device") {
|
|
79
|
+
const feats = await this.adb(["-s", this.cfg.serial, "features"], { signal, timeoutMs: this.cfg.commandTimeoutMs });
|
|
80
|
+
if (!feats.ok)
|
|
81
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `adb features failed: ${feats.error.message}`, feats.error) };
|
|
82
|
+
if (feats.value.exitCode !== 0) {
|
|
83
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", `adb features exited ${feats.value.exitCode} for ${this.cfg.serial}: ${feats.value.stderr.trim().slice(0, 200) || "(no stderr)"} — could not read the device's feature list this attempt`) };
|
|
84
|
+
}
|
|
85
|
+
if (!/(^|\s)shell_v2(\s|$)/.test(feats.value.stdout.trim())) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: new RemoteExecutionError("unsupported", `device ${this.cfg.serial} does not advertise the 'shell_v2' feature, so this adapter cannot establish that its command round-trip carries a real exit code with stderr kept separate — the two properties every filesystem judgment on this lane depends on to tell failure from success. Refusing to connect rather than run on an unverifiable transport: attach a device that supports adb shell protocol v2.`),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
79
91
|
this.connected = true;
|
|
80
92
|
return ok(this.workspaceHandle());
|
|
81
93
|
}
|
|
@@ -112,7 +124,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
112
124
|
if (!this.connected)
|
|
113
125
|
return { ok: false, error: new ExecutionError("shell_unavailable", "ADB device not connected — call connect() first") };
|
|
114
126
|
const wrapped = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
115
|
-
const r = await this.adb(
|
|
127
|
+
const r = await this.adb(this.deviceShellArgs(wrapped), {
|
|
116
128
|
signal: options?.abortSignal,
|
|
117
129
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : this.cfg.commandTimeoutMs,
|
|
118
130
|
onStdout: options?.onStdout,
|
|
@@ -130,7 +142,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
130
142
|
if (!this.connected)
|
|
131
143
|
throw new RemoteExecutionError("connect_failed", "ADB device not connected — call connect() first");
|
|
132
144
|
const wrapped = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
133
|
-
const child = this.spawnFn(this.cfg.adbPath,
|
|
145
|
+
const child = this.spawnFn(this.cfg.adbPath, this.deviceShellArgs(wrapped), { stdio: ["ignore", "pipe", "pipe"] });
|
|
134
146
|
const queue = [];
|
|
135
147
|
let finished = false;
|
|
136
148
|
let failure;
|
|
@@ -176,6 +188,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
176
188
|
if (queue.length >= 512 && !paused) {
|
|
177
189
|
paused = true;
|
|
178
190
|
child.stdout.pause();
|
|
191
|
+
child.stderr.pause();
|
|
179
192
|
}
|
|
180
193
|
signalReady();
|
|
181
194
|
};
|
|
@@ -206,6 +219,7 @@ export class RemoteAdbExecutionEnv {
|
|
|
206
219
|
if (paused) {
|
|
207
220
|
paused = false;
|
|
208
221
|
child.stdout.resume();
|
|
222
|
+
child.stderr.resume();
|
|
209
223
|
}
|
|
210
224
|
if (failure)
|
|
211
225
|
throw failure;
|
|
@@ -244,6 +258,8 @@ export class RemoteAdbExecutionEnv {
|
|
|
244
258
|
fsGate(p) {
|
|
245
259
|
if (this.destroyed)
|
|
246
260
|
return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
|
|
261
|
+
if (!this.connected)
|
|
262
|
+
return { ok: false, error: new FileError("unknown", "ADB device not connected — call connect() first", this.resolve(p)) };
|
|
247
263
|
return undefined;
|
|
248
264
|
}
|
|
249
265
|
async readBinaryFile(p, abortSignal) {
|
|
@@ -251,21 +267,11 @@ export class RemoteAdbExecutionEnv {
|
|
|
251
267
|
if (gated)
|
|
252
268
|
return gated;
|
|
253
269
|
const abs = this.resolve(p);
|
|
254
|
-
const r = await this.adb(
|
|
270
|
+
const r = await this.adb(this.deviceShellArgs(`cat ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs, binary: true });
|
|
255
271
|
if (!r.ok)
|
|
256
272
|
return { ok: false, error: fileErrorFromExec(r.error, abs) };
|
|
257
|
-
const errOut = r.value.stderr.trim();
|
|
258
|
-
if (errOut) {
|
|
259
|
-
if (/no such file/i.test(errOut))
|
|
260
|
-
return { ok: false, error: new FileError("not_found", errOut, abs) };
|
|
261
|
-
if (/permission denied/i.test(errOut))
|
|
262
|
-
return { ok: false, error: new FileError("permission_denied", errOut, abs) };
|
|
263
|
-
if (/is a directory/i.test(errOut))
|
|
264
|
-
return { ok: false, error: new FileError("is_directory", errOut, abs) };
|
|
265
|
-
return { ok: false, error: new FileError("unknown", errOut, abs) };
|
|
266
|
-
}
|
|
267
273
|
if (r.value.exitCode !== 0)
|
|
268
|
-
return { ok: false, error:
|
|
274
|
+
return { ok: false, error: classifyFsStderr(r.value.stderr.trim() || `adb cat exited ${r.value.exitCode}`, abs) };
|
|
269
275
|
return ok(r.value.stdoutBytes ?? new Uint8Array(0));
|
|
270
276
|
}
|
|
271
277
|
async readTextFile(p, abortSignal) {
|
|
@@ -284,6 +290,9 @@ export class RemoteAdbExecutionEnv {
|
|
|
284
290
|
return ok(lines);
|
|
285
291
|
}
|
|
286
292
|
async writeFile(p, content, abortSignal) {
|
|
293
|
+
const gated = this.fsGate(p);
|
|
294
|
+
if (gated)
|
|
295
|
+
return gated;
|
|
287
296
|
const abs = this.resolve(p);
|
|
288
297
|
const dir = path.posix.dirname(abs);
|
|
289
298
|
if (dir && dir !== "." && dir !== "/") {
|
|
@@ -436,6 +445,9 @@ export class RemoteAdbExecutionEnv {
|
|
|
436
445
|
});
|
|
437
446
|
});
|
|
438
447
|
}
|
|
448
|
+
deviceShellArgs(command) {
|
|
449
|
+
return ["-s", this.cfg.serial, "shell", "-T", "--", command];
|
|
450
|
+
}
|
|
439
451
|
withCwdEnv(command, cwd, env) {
|
|
440
452
|
const parts = [];
|
|
441
453
|
const dir = cwd ?? this.cwd;
|
|
@@ -1012,8 +1012,8 @@ export class RemoteHostExecutionEnv {
|
|
|
1012
1012
|
if (!ready.ok)
|
|
1013
1013
|
return ready;
|
|
1014
1014
|
const hops = [];
|
|
1015
|
-
const seen = new Set();
|
|
1016
1015
|
let cur = this.resolve(p);
|
|
1016
|
+
const seen = new Set([cur]);
|
|
1017
1017
|
for (let i = 0; i < SYMLINK_CHAIN_MAX_HOPS; i += 1) {
|
|
1018
1018
|
let target;
|
|
1019
1019
|
try {
|
|
@@ -64,16 +64,12 @@ export declare function buildSymlinkChainCommand(absPath: string): string;
|
|
|
64
64
|
* {@link buildSymlinkChainCommand} 的 stdout → `SymlinkChain.hops`。
|
|
65
65
|
*
|
|
66
66
|
* 三件事**只在这里**做一次(四条 shell 腿 + e2b 共读):
|
|
67
|
-
* · **环即止** —— 出现重复跳时在**重复的那一跳之前**截断(CC `vr` 的 `d.has(u)` 同律)
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* (
|
|
71
|
-
* `[b
|
|
72
|
-
*
|
|
73
|
-
* 造成误放行),而改成与 core 同形是**摘掉**一个视图 —— 在 [ref] 轴上那是错的方向,要动得先有裁定;
|
|
74
|
-
* ②那一跳是 `b` **真的写下的**目标,契约句「不重复被寻址的拼法」两种读法都讲得通(core 读作「起点不入表」,
|
|
75
|
-
* 本族读作「每条链接写下的目标都入表」),属**待 core 裁**的一格,不是本仓单方面能定的。
|
|
76
|
-
* ⚠️ 故各腿头注里「与 core 同形 / 逐字节对得上」那句话的射程是**无环链**;有环的那一格以本段为准。
|
|
67
|
+
* · **环即止** —— 出现重复跳时在**重复的那一跳之前**截断(CC `vr` 的 `d.has(u)` 同律),且 seen 表用
|
|
68
|
+
* **被寻址的拼法播种**(S-408,core 裁 2026-09-17:契约句「不重复被寻址的拼法」的正解是**起点本身不是一跳**——
|
|
69
|
+
* 链视图列的是「从被寻址拼法出发经过的中间拼法」,链绕回起点即结束、起点不再列;成环的事实由终点判决
|
|
70
|
+
* `canonicalPath`(realpath ELOOP ⇒ unresolvable ⇒ 拒)承担,不由链视图承担)。于是 `a -> b`、`b -> a`,问 `a`,
|
|
71
|
+
* 答 `[b]`,与 core `NodeExecutionEnv.canonicalChain`(`new Set([resolved])` 播种)逐字节同形。此前本族从空表
|
|
72
|
+
* 开始、答 `[b, a]`(7.82.0 及之前;多一个冗余视图 —— 判官对 `a` 已按拼法判过一次),裁定后对齐。
|
|
77
73
|
* 脚本侧不做:
|
|
78
74
|
* POSIX sh 里维护一张 seen 表要么靠内嵌换行的 `case` 模式(易碎),要么另起一个进程;放在这里则
|
|
79
75
|
* **一份实现**被四条腿与 e2b 共用,且与 host 腿的 Node 循环逐字节对得上。代价如实登记:链上真有环时,
|
|
@@ -83,7 +79,7 @@ export declare function buildSymlinkChainCommand(absPath: string): string;
|
|
|
83
79
|
* 处理,不假装支持)。残留如实写出来:目标拼法真带首尾空白时,trim 后的跳可能与真目标差几个字节 ——
|
|
84
80
|
* 方向上只会让一条跳**匹配不到**受保护行(少问,不是多放行),而终点判决(`canonicalPath`)完全不经本路。
|
|
85
81
|
*/
|
|
86
|
-
export declare function symlinkChainHopsFromStdout(stdout: string): string[];
|
|
82
|
+
export declare function symlinkChainHopsFromStdout(stdout: string, addressed: string): string[];
|
|
87
83
|
/** {@link runSymlinkChain} 的传输面 —— 各腿的 `exec` 收敛成的那一个形(与 `PosixShellFs` 基座的 `run` 同形)。 */
|
|
88
84
|
export type ShellChainRun = (cmd: string) => Promise<Result<{
|
|
89
85
|
stdout: string;
|
|
@@ -47,9 +47,9 @@ export function buildSymlinkChainCommand(absPath) {
|
|
|
47
47
|
`*) d="${"${c%/*}"}"; case "$d" in "") c="/$t" ;; */) c="$d$t" ;; *) c="$d/$t" ;; esac ;; esac; ` +
|
|
48
48
|
`printf '%s\\n' "$c"; n=$((n+1)); done; }`);
|
|
49
49
|
}
|
|
50
|
-
export function symlinkChainHopsFromStdout(stdout) {
|
|
50
|
+
export function symlinkChainHopsFromStdout(stdout, addressed) {
|
|
51
51
|
const hops = [];
|
|
52
|
-
const seen = new Set();
|
|
52
|
+
const seen = new Set([addressed]);
|
|
53
53
|
for (const raw of stdout.split("\n")) {
|
|
54
54
|
const hop = raw.trim();
|
|
55
55
|
if (hop === "")
|
|
@@ -69,6 +69,6 @@ export async function runSymlinkChain(absPath, run) {
|
|
|
69
69
|
return { ok: false, error: fileErrorFromExec(r.error, absPath) };
|
|
70
70
|
if (r.value.exitCode !== 0)
|
|
71
71
|
return { ok: false, error: classifyFsStderr(r.value.stderr, absPath, "unknown") };
|
|
72
|
-
return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout) } };
|
|
72
|
+
return { ok: true, value: { hops: symlinkChainHopsFromStdout(r.value.stdout, absPath) } };
|
|
73
73
|
}
|
|
74
74
|
//# sourceMappingURL=remote-shell.js.map
|
|
@@ -49,6 +49,7 @@ import type { ManagedRetentionCapability, RetentionDeclaration, RetentionReceipt
|
|
|
49
49
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
50
50
|
import type { Pool as PgPool } from "pg";
|
|
51
51
|
import { type SqlDriver, type SqlTxConn } from "./sql-driver.js";
|
|
52
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
52
53
|
/**
|
|
53
54
|
* 三只**被托管**的 SQL 店(session / checkpoint / tool-result)共用的 `retention` 声明常量。
|
|
54
55
|
*
|
|
@@ -90,6 +91,8 @@ export declare const TIDB_RETENTION_STATEMENTS: readonly string[];
|
|
|
90
91
|
/** PG 方言的建表语句(MySQL 孪生的逐条翻译:AUTO_INCREMENT→BIGSERIAL,逐列 `COLLATE "C"`,内联 KEY→独立
|
|
91
92
|
* CREATE INDEX)。语义逐字见 MySQL 孪生的行内注,此处不复述(两份注释迟早分叉)。 */
|
|
92
93
|
export declare const PG_RETENTION_SCHEMA: readonly string[];
|
|
94
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgRetentionSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
95
|
+
export declare const RETENTION_INDEXES: readonly IndexSpec[];
|
|
93
96
|
/** PG 侧的幂等 schema apply(由 `pg-pool.ts` 的中央 `ensurePgSchema` 组合;裸 `PgQueryFn` 形,与
|
|
94
97
|
* approval-ask / leader-run 同姿势 —— DDL 仍跑在中央那条 client 上,advisory lock 的 session 语义不受影响)。 */
|
|
95
98
|
export declare function ensurePgRetentionSchema(query: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
2
2
|
import { TASK_LIST_ITEM_TABLE, TASK_LIST_META_TABLE, taskListKeyFor } from "./task-list-store-sql.js";
|
|
3
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
3
4
|
export const MANAGED_RETENTION = "managed";
|
|
4
5
|
export const UNMANAGED_RETENTION = "none";
|
|
5
6
|
export const RETENTION_AUDIT_TABLE = "retention_audit";
|
|
@@ -93,8 +94,6 @@ export const PG_RETENTION_SCHEMA = [
|
|
|
93
94
|
error TEXT COLLATE "C" NULL,
|
|
94
95
|
PRIMARY KEY (id)
|
|
95
96
|
)`,
|
|
96
|
-
`CREATE INDEX IF NOT EXISTS idx_retention_audit_domain ON ${RETENTION_AUDIT_TABLE} (domain, id)`,
|
|
97
|
-
`CREATE INDEX IF NOT EXISTS idx_retention_audit_executed ON ${RETENTION_AUDIT_TABLE} (executed_at_ms)`,
|
|
98
97
|
`CREATE TABLE IF NOT EXISTS ${RETENTION_HOLD_TABLE} (
|
|
99
98
|
domain VARCHAR(190) COLLATE "C" NOT NULL,
|
|
100
99
|
-- held:1 = 冻结中,0 = 只是那把域级互斥行锁的哨兵。完整判据(为什么 presence 形不成立)见 MySQL 孪生。
|
|
@@ -119,11 +118,16 @@ export const PG_RETENTION_SCHEMA = [
|
|
|
119
118
|
-- 主键带 domain(理由逐字见 MySQL 孪生:会话 id 是调用方可自选的,跨租户同名合法)。
|
|
120
119
|
PRIMARY KEY (domain, kind, row_key)
|
|
121
120
|
)`,
|
|
122
|
-
|
|
121
|
+
];
|
|
122
|
+
export const RETENTION_INDEXES = [
|
|
123
|
+
{ table: `${RETENTION_AUDIT_TABLE}`, name: "idx_retention_audit_domain", columns: ["domain", "id"] },
|
|
124
|
+
{ table: `${RETENTION_AUDIT_TABLE}`, name: "idx_retention_audit_executed", columns: ["executed_at_ms"] },
|
|
125
|
+
{ table: `${RETENTION_TOMBSTONE_TABLE}`, name: "idx_retention_tombstone_key", columns: ["kind", "row_key"] },
|
|
123
126
|
];
|
|
124
127
|
export async function ensurePgRetentionSchema(query) {
|
|
125
128
|
for (const stmt of PG_RETENTION_SCHEMA)
|
|
126
129
|
await query(stmt);
|
|
130
|
+
await ensureIndexes(query, "pg", RETENTION_INDEXES);
|
|
127
131
|
}
|
|
128
132
|
class RetentionAborted extends Error {
|
|
129
133
|
why;
|
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
import type { Pool } from "mysql2/promise";
|
|
24
24
|
import { type RosterStore, type RosterEntry, type RosterAccess } from "@sema-agent/core";
|
|
25
25
|
import type { PgQueryFn } from "./pg-query.js";
|
|
26
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
26
27
|
export declare const ROSTER_TABLE = "agent_roster";
|
|
27
28
|
export declare function ensureTiDBRosterSchema(pool: Pool): Promise<void>;
|
|
29
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgRosterSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
30
|
+
export declare const ROSTER_INDEXES: readonly IndexSpec[];
|
|
28
31
|
export declare function ensurePgRosterSchema(q: PgQueryFn): Promise<void>;
|
|
29
32
|
export declare class TiDBRosterStore implements RosterStore {
|
|
30
33
|
private readonly pool;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeAgentName } from "@sema-agent/core";
|
|
2
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
2
3
|
export const ROSTER_TABLE = "agent_roster";
|
|
3
4
|
function rowToEntry(r) {
|
|
4
5
|
return {
|
|
@@ -42,7 +43,11 @@ export async function ensureTiDBRosterSchema(pool) {
|
|
|
42
43
|
PRIMARY KEY (name_key, owner_key, scope_key),
|
|
43
44
|
KEY idx_roster_agent (agent_id)
|
|
44
45
|
) COLLATE utf8mb4_bin`);
|
|
46
|
+
await ensureIndexes((text, params) => pool.query(text, params), "mysql", ROSTER_INDEXES);
|
|
45
47
|
}
|
|
48
|
+
export const ROSTER_INDEXES = [
|
|
49
|
+
{ table: `${ROSTER_TABLE}`, name: "idx_roster_agent", columns: ["agent_id"] },
|
|
50
|
+
];
|
|
46
51
|
export async function ensurePgRosterSchema(q) {
|
|
47
52
|
await q(`CREATE TABLE IF NOT EXISTS ${ROSTER_TABLE} (
|
|
48
53
|
name_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -66,7 +71,7 @@ export async function ensurePgRosterSchema(q) {
|
|
|
66
71
|
recorded_at_ms BIGINT NOT NULL,
|
|
67
72
|
PRIMARY KEY (name_key, owner_key, scope_key)
|
|
68
73
|
)`);
|
|
69
|
-
await q
|
|
74
|
+
await ensureIndexes(q, "pg", ROSTER_INDEXES);
|
|
70
75
|
}
|
|
71
76
|
function upsertParams(entry, now) {
|
|
72
77
|
return [
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中央 schema(`tidb-pool.ts` 的 `SCHEMA_STATEMENTS` ⇄ `pg-pool.ts` 的 `PG_SCHEMA_STATEMENTS` 这一对孪生)
|
|
3
|
+
* 所拥有的那批表的索引**声明** —— S-287,两方言**共用同一份**。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么自成一个模块
|
|
6
|
+
* 两只池各自 `import` 了自己的驱动(`mysql2/promise` / `pg`,都是**值**导入)。若把这份声明放进其中一只,
|
|
7
|
+
* 另一只就得把对方的驱动一起拖进进程 —— 单后端部署因此多驮一个它永远用不到的驱动。声明是纯数据、零驱动
|
|
8
|
+
* 依赖,所以它住在两只池**之外**,谁都可以白拿。
|
|
9
|
+
*
|
|
10
|
+
* ## 谁应用它
|
|
11
|
+
* · **PG**:`pg-pool.ts` 的 `ensurePgSchema`,在所有 `CREATE TABLE` 之后(PG 方言没有内联 `KEY`);
|
|
12
|
+
* · **MySQL/TiDB**:`tidb-pool.ts` 的 `ensureSchema`,同样在所有 `CREATE TABLE` 之后 —— 新建库上这批索引
|
|
13
|
+
* 早已随 `CREATE TABLE` 的内联 `KEY` 到货,`ensureIndex` 的探测因此**一条 DDL 都不发**;只有存量库
|
|
14
|
+
* (建表那天还没有这条索引)才真的发一次 `ALTER TABLE … ADD INDEX`。
|
|
15
|
+
*
|
|
16
|
+
* 🔴 每条声明的**列序**必须与 `tidb-pool.ts` 那张表的内联 `KEY` 逐字一致(等值前缀在前、排序列在后);
|
|
17
|
+
* 两处列序分叉 = 两方言的访问路径分叉,而探测只看索引名、看不出列序不同。
|
|
18
|
+
*/
|
|
19
|
+
import type { IndexSpec } from "./ensure-index.js";
|
|
20
|
+
export declare const CENTRAL_SCHEMA_INDEXES: readonly IndexSpec[];
|
|
21
|
+
//# sourceMappingURL=schema-indexes.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const CENTRAL_SCHEMA_INDEXES = [
|
|
2
|
+
{ table: "task_run", name: "idx_task_run_session_status", columns: ["session_id", "status"] },
|
|
3
|
+
{ table: "task_run", name: "idx_task_run_owner", columns: ["owner"] },
|
|
4
|
+
{ table: "task_run", name: "idx_task_run_created", columns: ["created_at"] },
|
|
5
|
+
{ table: "task_run", name: "idx_task_run_job", columns: ["job_id"] },
|
|
6
|
+
{ table: "task_active", name: "idx_task_active_task", columns: ["task_id"] },
|
|
7
|
+
{ table: "checkpoint", name: "idx_checkpoint_status_deadline", columns: ["status", "deadline"] },
|
|
8
|
+
{ table: "checkpoint", name: "idx_checkpoint_session_status", columns: ["session_id", "status"] },
|
|
9
|
+
{ table: "resume_anchor", name: "idx_resume_anchor_session", columns: ["session_id"] },
|
|
10
|
+
{ table: "approval_nonce", name: "idx_approval_nonce_expires", columns: ["expires_at_ms"] },
|
|
11
|
+
{ table: "sendfile_link", name: "idx_sendfile_link_scope", columns: ["scope", "id"] },
|
|
12
|
+
{ table: "session_policy", name: "idx_session_policy_session", columns: ["session_id"] },
|
|
13
|
+
{ table: "snapshot_blob", name: "idx_snapshot_blob_created", columns: ["created_at"] },
|
|
14
|
+
{ table: "file_history_version", name: "idx_file_history_version_blob", columns: ["blob_hash"] },
|
|
15
|
+
{ table: "file_history_boundary_file", name: "idx_file_history_bf_key", columns: ["scope", "key_hash"] },
|
|
16
|
+
{ table: "file_history_blob", name: "idx_file_history_blob_created", columns: ["created_at"] },
|
|
17
|
+
{ table: "workflow_run", name: "idx_wfrun_scope_created", columns: ["scope", "created_at_ms"] },
|
|
18
|
+
{ table: "workflow_run", name: "idx_wfrun_scope_status", columns: ["scope", "status"] },
|
|
19
|
+
{ table: "workflow_agent_session", name: "idx_wfagentsess_run", columns: ["run_id"] },
|
|
20
|
+
{ table: "workflow_completion_inbox", name: "idx_wfinbox_session_seq", columns: ["session_id", "seq"] },
|
|
21
|
+
{ table: "workflow_inbox_fence", name: "idx_wffence_until", columns: ["until_ms"] },
|
|
22
|
+
{ table: "workflow_notify_journal", name: "idx_wfnotify_acked", columns: ["acked"] },
|
|
23
|
+
{ table: "shared_memory_store", name: "idx_shared_memory_store_scope", columns: ["scope_key", "store_id"] },
|
|
24
|
+
];
|
|
25
|
+
//# sourceMappingURL=schema-indexes.js.map
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { Pool } from "mysql2/promise";
|
|
28
28
|
import type { PgQueryFn } from "./pg-query.js";
|
|
29
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
29
30
|
export interface TaskAttachmentRecord {
|
|
30
31
|
id: string;
|
|
31
32
|
owner: string;
|
|
@@ -116,6 +117,8 @@ export declare function materializeAttachmentsInto(env: {
|
|
|
116
117
|
}>;
|
|
117
118
|
}, store: TaskAttachmentStore, sessionId: string): Promise<number>;
|
|
118
119
|
export declare function ensureTiDBTaskAttachmentSchema(pool: Pool): Promise<void>;
|
|
120
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgTaskAttachmentSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
121
|
+
export declare const TASK_ATTACHMENT_INDEXES: readonly IndexSpec[];
|
|
119
122
|
export declare function ensurePgTaskAttachmentSchema(q: PgQueryFn): Promise<void>;
|
|
120
123
|
/** bytes → 4 MiB 块序列(共享切块器,两方言 + 完整性校验共用)。 */
|
|
121
124
|
export declare class TiDBTaskAttachmentStore implements TaskAttachmentStore {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
1
2
|
export const TASK_ATTACHMENT_TABLE = "task_attachment";
|
|
2
3
|
export function materializedRelPaths(atts) {
|
|
3
4
|
const used = new Set();
|
|
@@ -76,7 +77,12 @@ export async function ensureTiDBTaskAttachmentSchema(pool) {
|
|
|
76
77
|
KEY idx_att_session (session_id),
|
|
77
78
|
KEY idx_att_unbound (created_at_ms)
|
|
78
79
|
) COLLATE utf8mb4_bin`);
|
|
80
|
+
await ensureIndexes((text, params) => pool.query(text, params), "mysql", TASK_ATTACHMENT_INDEXES);
|
|
79
81
|
}
|
|
82
|
+
export const TASK_ATTACHMENT_INDEXES = [
|
|
83
|
+
{ table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_session", columns: ["session_id"] },
|
|
84
|
+
{ table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_unbound", columns: ["created_at_ms"] },
|
|
85
|
+
];
|
|
80
86
|
export async function ensurePgTaskAttachmentSchema(q) {
|
|
81
87
|
await q(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
|
|
82
88
|
id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
@@ -91,8 +97,7 @@ export async function ensurePgTaskAttachmentSchema(q) {
|
|
|
91
97
|
bound_at_ms BIGINT,
|
|
92
98
|
PRIMARY KEY (id)
|
|
93
99
|
)`);
|
|
94
|
-
await q
|
|
95
|
-
await q(`CREATE INDEX IF NOT EXISTS idx_att_unbound ON ${TASK_ATTACHMENT_TABLE} (created_at_ms)`);
|
|
100
|
+
await ensureIndexes(q, "pg", TASK_ATTACHMENT_INDEXES);
|
|
96
101
|
}
|
|
97
102
|
export class TiDBTaskAttachmentStore {
|
|
98
103
|
pool;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import mysql from "mysql2/promise";
|
|
2
2
|
import type { Pool, PoolConnection } from "mysql2/promise";
|
|
3
3
|
import type { ServiceConfig } from "../config-types.js";
|
|
4
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
4
5
|
/**
|
|
5
6
|
* Shared TiDB (MySQL wire-compatible) connection pool for the durable Session center (L1).
|
|
6
7
|
* Owned by `main.ts`; closed on shutdown.
|
|
@@ -57,11 +58,22 @@ export declare const ENSURE_SCHEMA_LOCK = "sema_ensure_schema";
|
|
|
57
58
|
* lock row — which both exhausts TiDB's "pessimistic lock retry limit" (surfacing as a thrown error) AND starves the
|
|
58
59
|
* app's OWN `SELECT … FOR UPDATE` paths (image-bake admission/lease) of pessimistic-lock budget. So we acquire
|
|
59
60
|
* NON-BLOCKING (GET_LOCK(name, 0) returns 0 immediately if held) and POLL with jittered backoff: one holder, the
|
|
60
|
-
* rest sleeping (not blocked in-DB).
|
|
61
|
-
*
|
|
62
|
-
export declare function acquireEnsureSchemaLock(conn: PoolConnection, tag?: string): Promise<
|
|
61
|
+
* rest sleeping (not blocked in-DB). Polls until acquired (7.83.0: never degrades to unserialized — the S-287
|
|
62
|
+
* `ADD INDEX` leg is not race-safe); a connection whose probe keeps throwing surfaces as a boot failure. */
|
|
63
|
+
export declare function acquireEnsureSchemaLock(conn: PoolConnection, tag?: string): Promise<void>;
|
|
63
64
|
/** Release {@link ENSURE_SCHEMA_LOCK} on `conn` (best-effort — the lock also auto-frees when the connection closes). */
|
|
64
65
|
export declare function releaseEnsureSchemaLock(conn: PoolConnection, tag?: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* S-287:`SCHEMA_STATEMENTS` 建的那批表**在存量库上可能缺的索引**(声明与 PG 侧逐条共用同一份 —— 见
|
|
68
|
+
* `plugins/ensure-index.ts` 头注与 `plugins/schema-indexes.ts`)。
|
|
69
|
+
*
|
|
70
|
+
* 组合口径与 `SCHEMA_STATEMENTS` **一一对应**:凡是它建的表,索引声明就进这个数组;它不建的表
|
|
71
|
+
* (device / roster / background-agent / mailbox / task-attachment / memory 两族……由 `boot/stores.ts`
|
|
72
|
+
* 各自的 `ensureTiDB*Schema` 腿建)由那条腿自己应用自己的声明 —— 否则这里会对一张还不存在的表发 ALTER。
|
|
73
|
+
*
|
|
74
|
+
* 新建库上这批索引早已随 `CREATE TABLE` 的内联 `KEY` 到货 ⇒ `ensureIndex` 的探测全部命中、**零 DDL**。
|
|
75
|
+
*/
|
|
76
|
+
export declare const SCHEMA_INDEXES: readonly IndexSpec[];
|
|
65
77
|
/** Create tables if absent (+ seed the image-bake admission sentinel). Safe to call on every startup.
|
|
66
78
|
*
|
|
67
79
|
* Per the SCHEMA POLICY on {@link SCHEMA_STATEMENTS} this is a pure CREATE run — there are NO additive
|
|
@@ -6,6 +6,21 @@ import { TIDB_LEADER_RUN_STATEMENTS } from "./leader-run-store-sql.js";
|
|
|
6
6
|
import { TIDB_ADOPTION_LOG_STATEMENTS } from "./adoption-log-sql.js";
|
|
7
7
|
import { TIDB_PERMISSION_RULE_STATEMENTS } from "./permission-rule-store-sql.js";
|
|
8
8
|
import { TIDB_RETENTION_STATEMENTS } from "./retention-store-sql.js";
|
|
9
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
10
|
+
import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
|
|
11
|
+
import { SESSION_INDEXES } from "./pg-session-storage.js";
|
|
12
|
+
import { TOOL_RESULT_INDEXES } from "./tool-result-store-sql.js";
|
|
13
|
+
import { COST_QUOTA_INDEXES } from "./pg-cost-quota.js";
|
|
14
|
+
import { RATE_LIMIT_INDEXES } from "./pg-rate-limiter.js";
|
|
15
|
+
import { OUTCOME_LEDGER_INDEXES } from "./outcome-ledger-sql.js";
|
|
16
|
+
import { IMAGE_INDEX_INDEXES } from "./image-index-sql.js";
|
|
17
|
+
import { IMAGE_BAKE_INDEXES } from "./image-bake-store-sql.js";
|
|
18
|
+
import { APPROVAL_ASK_INDEXES } from "./approval-ask-store-sql.js";
|
|
19
|
+
import { ADOPTION_LOG_INDEXES } from "./adoption-log-sql.js";
|
|
20
|
+
import { PERMISSION_RULE_INDEXES } from "./permission-rule-store-sql.js";
|
|
21
|
+
import { RETENTION_INDEXES } from "./retention-store-sql.js";
|
|
22
|
+
import { LEADER_RUN_INDEXES } from "./leader-run-store-sql.js";
|
|
23
|
+
import { pollUntilLocked } from "./ensure-schema-lock-policy.js";
|
|
9
24
|
export function tidbPoolOptions(tidb) {
|
|
10
25
|
const { host, port, user, password, database, connectionLimit } = tidb;
|
|
11
26
|
const limit = connectionLimit ?? 10;
|
|
@@ -670,20 +685,10 @@ export const SCHEMA_STATEMENTS = [
|
|
|
670
685
|
];
|
|
671
686
|
export const ENSURE_SCHEMA_LOCK = "sema_ensure_schema";
|
|
672
687
|
export async function acquireEnsureSchemaLock(conn, tag = "ensureSchema") {
|
|
673
|
-
await
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (Number(rows[0]?.got) === 1)
|
|
678
|
-
return true;
|
|
679
|
-
}
|
|
680
|
-
catch (err) {
|
|
681
|
-
console.warn(`[${tag}] GET_LOCK('${ENSURE_SCHEMA_LOCK}') attempt ${attempt + 1}/40:`, err?.message ?? err);
|
|
682
|
-
}
|
|
683
|
-
await new Promise((r) => setTimeout(r, 100 + Math.min(attempt, 3) * 100 + Math.floor(Math.random() * 150)));
|
|
684
|
-
}
|
|
685
|
-
console.warn(`[${tag}] could not acquire '${ENSURE_SCHEMA_LOCK}' after retries`);
|
|
686
|
-
return false;
|
|
688
|
+
await pollUntilLocked(async () => {
|
|
689
|
+
const [rows] = await conn.query("SELECT GET_LOCK(?, 0) AS got", [ENSURE_SCHEMA_LOCK]);
|
|
690
|
+
return Number(rows[0]?.got) === 1;
|
|
691
|
+
}, { tag, label: `GET_LOCK('${ENSURE_SCHEMA_LOCK}')` });
|
|
687
692
|
}
|
|
688
693
|
export async function releaseEnsureSchemaLock(conn, tag = "ensureSchema") {
|
|
689
694
|
try {
|
|
@@ -693,14 +698,32 @@ export async function releaseEnsureSchemaLock(conn, tag = "ensureSchema") {
|
|
|
693
698
|
console.warn(`[${tag}] RELEASE_LOCK('${ENSURE_SCHEMA_LOCK}') failed (lock auto-frees on connection close):`, err);
|
|
694
699
|
}
|
|
695
700
|
}
|
|
701
|
+
export const SCHEMA_INDEXES = [
|
|
702
|
+
...CENTRAL_SCHEMA_INDEXES,
|
|
703
|
+
...SESSION_INDEXES,
|
|
704
|
+
...TOOL_RESULT_INDEXES,
|
|
705
|
+
...COST_QUOTA_INDEXES,
|
|
706
|
+
...RATE_LIMIT_INDEXES,
|
|
707
|
+
...OUTCOME_LEDGER_INDEXES,
|
|
708
|
+
...IMAGE_INDEX_INDEXES,
|
|
709
|
+
...IMAGE_BAKE_INDEXES,
|
|
710
|
+
...APPROVAL_ASK_INDEXES,
|
|
711
|
+
...ADOPTION_LOG_INDEXES,
|
|
712
|
+
...PERMISSION_RULE_INDEXES,
|
|
713
|
+
...RETENTION_INDEXES,
|
|
714
|
+
...LEADER_RUN_INDEXES,
|
|
715
|
+
];
|
|
696
716
|
export async function ensureSchema(pool) {
|
|
697
717
|
const conn = await pool.getConnection();
|
|
698
|
-
|
|
718
|
+
let locked = false;
|
|
699
719
|
try {
|
|
720
|
+
await acquireEnsureSchemaLock(conn);
|
|
721
|
+
locked = true;
|
|
700
722
|
for (const stmt of SCHEMA_STATEMENTS) {
|
|
701
723
|
await conn.query(stmt);
|
|
702
724
|
}
|
|
703
725
|
await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES ('standard')");
|
|
726
|
+
await ensureIndexes((text, params) => conn.query(text, params), "mysql", SCHEMA_INDEXES);
|
|
704
727
|
}
|
|
705
728
|
finally {
|
|
706
729
|
if (locked)
|
|
@@ -2,10 +2,13 @@ import type { ToolResultProvenance, ToolResultStore, ToolResultSlice, ToolResult
|
|
|
2
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
3
|
import type { Pool as PgPool, PoolClient as PgPoolClient } from "pg";
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
5
6
|
/** PG schema for the tool_result table — the PG translation of tidb-pool.ts's tool_result DDL
|
|
6
7
|
* (LONGTEXT→TEXT, DATETIME(3)→TIMESTAMPTZ(3), inline KEY→separate CREATE INDEX). Disjoint from other stores'
|
|
7
8
|
* tables, so a self-contained ensureSchema is safe (central pg-pool aggregation is done separately). */
|
|
8
9
|
export declare const PG_TOOL_RESULT_SCHEMA: string[];
|
|
10
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensureSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
11
|
+
export declare const TOOL_RESULT_INDEXES: readonly IndexSpec[];
|
|
9
12
|
/** Idempotent schema apply for the tool_result table (for the integration test to call). */
|
|
10
13
|
export declare function ensureSchema(pool: PgPool | PgPoolClient): Promise<void>;
|
|
11
14
|
/**
|