@sema-agent/server 7.82.1 → 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.
Files changed (73) hide show
  1. package/dist/bench/s1/arms.d.ts +4 -4
  2. package/dist/bench/s1/arms.js +1 -2
  3. package/dist/bench/s1/live-deps.js +0 -1
  4. package/dist/bench/s1/runner-ctx.d.ts +3 -3
  5. package/dist/bench/s1/tasks.d.ts +8 -4
  6. package/dist/config-catalog.js +0 -1
  7. package/dist/config.js +1 -0
  8. package/dist/leader/endpoint.js +2 -0
  9. package/dist/leader/leader.d.ts +33 -6
  10. package/dist/leader/leader.js +54 -45
  11. package/dist/leader/merge.d.ts +38 -29
  12. package/dist/leader/merge.js +45 -38
  13. package/dist/leader/wire.d.ts +2 -17
  14. package/dist/leader/wire.js +5 -58
  15. package/dist/observability/fail-open.d.ts +5 -5
  16. package/dist/observability/fail-open.js +5 -2
  17. package/dist/observability/run-terminal-log.d.ts +0 -7
  18. package/dist/observability/run-terminal-log.js +9 -20
  19. package/dist/plugins/adoption-log-sql.d.ts +3 -0
  20. package/dist/plugins/adoption-log-sql.js +5 -1
  21. package/dist/plugins/approval-ask-store-sql.d.ts +3 -0
  22. package/dist/plugins/approval-ask-store-sql.js +9 -5
  23. package/dist/plugins/background-agent-store-sql.d.ts +3 -0
  24. package/dist/plugins/background-agent-store-sql.js +7 -2
  25. package/dist/plugins/device-store-sql.d.ts +3 -0
  26. package/dist/plugins/device-store-sql.js +9 -4
  27. package/dist/plugins/ensure-index.d.ts +42 -0
  28. package/dist/plugins/ensure-index.js +81 -0
  29. package/dist/plugins/ensure-schema-lock-policy.d.ts +29 -0
  30. package/dist/plugins/ensure-schema-lock-policy.js +31 -0
  31. package/dist/plugins/image-bake-store-sql.d.ts +3 -0
  32. package/dist/plugins/image-bake-store-sql.js +6 -2
  33. package/dist/plugins/image-index-sql.d.ts +3 -0
  34. package/dist/plugins/image-index-sql.js +8 -4
  35. package/dist/plugins/leader-run-store-sql.d.ts +3 -0
  36. package/dist/plugins/leader-run-store-sql.js +6 -2
  37. package/dist/plugins/mailbox-store-sql.d.ts +3 -0
  38. package/dist/plugins/mailbox-store-sql.js +6 -1
  39. package/dist/plugins/memory-engine-pg.d.ts +4 -0
  40. package/dist/plugins/memory-engine-pg.js +6 -6
  41. package/dist/plugins/memory-engine-tidb.js +5 -0
  42. package/dist/plugins/memory-sync-store-pg.d.ts +5 -0
  43. package/dist/plugins/memory-sync-store-pg.js +9 -2
  44. package/dist/plugins/memory-sync-store-tidb.js +6 -0
  45. package/dist/plugins/one-time-migrations.d.ts +1 -1
  46. package/dist/plugins/one-time-migrations.js +7 -14
  47. package/dist/plugins/outcome-ledger-sql.d.ts +3 -0
  48. package/dist/plugins/outcome-ledger-sql.js +7 -3
  49. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  50. package/dist/plugins/permission-rule-store-sql.js +9 -5
  51. package/dist/plugins/pg-cost-quota.d.ts +3 -0
  52. package/dist/plugins/pg-cost-quota.js +5 -1
  53. package/dist/plugins/pg-pool.d.ts +23 -2
  54. package/dist/plugins/pg-pool.js +22 -37
  55. package/dist/plugins/pg-rate-limiter.d.ts +3 -0
  56. package/dist/plugins/pg-rate-limiter.js +5 -1
  57. package/dist/plugins/pg-session-storage.d.ts +3 -0
  58. package/dist/plugins/pg-session-storage.js +8 -4
  59. package/dist/plugins/retention-store-sql.d.ts +3 -0
  60. package/dist/plugins/retention-store-sql.js +7 -3
  61. package/dist/plugins/roster-store-sql.d.ts +3 -0
  62. package/dist/plugins/roster-store-sql.js +6 -1
  63. package/dist/plugins/schema-indexes.d.ts +21 -0
  64. package/dist/plugins/schema-indexes.js +25 -0
  65. package/dist/plugins/task-attachment-store.d.ts +3 -0
  66. package/dist/plugins/task-attachment-store.js +7 -2
  67. package/dist/plugins/tidb-pool.d.ts +15 -3
  68. package/dist/plugins/tidb-pool.js +38 -15
  69. package/dist/plugins/tool-result-store-sql.d.ts +3 -0
  70. package/dist/plugins/tool-result-store-sql.js +5 -1
  71. package/dist/trace/project.d.ts +2 -1
  72. package/dist/trace/redact.d.ts +6 -0
  73. package/package.json +1 -1
@@ -129,11 +129,6 @@ export interface LeaderWireConfig {
129
129
  * 0 / unset = OFF (a failing integration test is terminal, the pre-2026-06-14 behavior). Env override:
130
130
  * LEADER_REPAIR_ROUNDS. Repair runs the strong (default) model with hands in the live merged sandbox. */
131
131
  repairRounds?: number;
132
- /** Max bounded merge-conflict-resolution rounds on a `git apply --3way` conflict (see merge.ts ConflictResolver).
133
- * 0 / unset = OFF (an apply conflict collapses the whole run to a single worker, the pre-2026-06-16 behavior).
134
- * Env override: LEADER_CONFLICT_ROUNDS. Resolves the conflict in-place + keeps the other workers' work, instead
135
- * of discarding all fan-out output and redoing single. Runs the strong (default) model with hands in the sandbox. */
136
- conflictRounds?: number;
137
132
  /** [ref] §8 F2(a): when set, workers run on FACTORY envs (runner-owned lifecycle — the durable-suspend-
138
133
  * complete path; a suspended worker's paused env is exempt from the leader reap by construction) and diff-out
139
134
  * is the worker's mandatory self-upload to this MinIO/S3 store (presigned per worker; creds adapter-held).
@@ -173,14 +168,6 @@ type WireEnv = Pick<RemoteExecutionEnv, "exec" | "writeFile">;
173
168
  * k8s、host `RemoteHostExecutionEnv`——`hostExecutionEnvFactory` 恒 `new RemoteHostExecutionEnv`),
174
169
  * 所以这条臂是**防御性**的:它真响的那天,意思是有人接了一条新工厂车道而 leader 收不动它的沙盒。
175
170
  */
176
- /** 冲突解决模型的目标文本——纯函数(第十三轮重扫 [medium]:wire 内联时无法钉「补丁先脱」;抽出来直接钉)。
177
- * 补丁走补丁头窗(头 256KB 扫再切 6000;解决者仍有工作树里的冲突标记),conflictText 由 merge.ts 先脱后切。 */
178
- export declare function buildConflictResolverObjective(args: {
179
- workerId: string;
180
- repoDir: string;
181
- patch: string;
182
- conflictText: string;
183
- }): string;
184
171
  export declare function createEnvReaper(env: ExecutionEnv, what: string): () => Promise<void>;
185
172
  /**
186
173
  * Resume-safe worker staging (the [ref] F2(a) factory-env path). Runs adapter-side before the agent's
@@ -247,13 +234,12 @@ export declare function leaderResourceConfig(env?: NodeJS.ProcessEnv): {
247
234
  maxSuspends: number;
248
235
  };
249
236
  };
250
- /** Repair/conflict/replan-loop knobs derived from env + optional `LeaderWireConfig` fallbacks (`repairRounds`/
251
- * `conflictRounds` land in cfg when the caller wires them programmatically instead of via env). Split out of
237
+ /** Repair/replan-loop knobs derived from env + optional `LeaderWireConfig` fallbacks (`repairRounds` lands in
238
+ * cfg when the caller wires it programmatically instead of via env). Split out of
252
239
  * `createLeaderRunner`'s closure so it's a plain, unit-testable data constructor. */
253
240
  export interface LeaderLoopConfig {
254
241
  repairRounds: number;
255
242
  repairBudgetUsd: number;
256
- conflictRounds: number;
257
243
  repairLoopOn: boolean;
258
244
  measureGatesOn: boolean;
259
245
  repairLoopAttempts: number;
@@ -264,7 +250,6 @@ export interface LeaderLoopConfig {
264
250
  }
265
251
  export declare function leaderLoopConfig(cfg: {
266
252
  repairRounds?: number;
267
- conflictRounds?: number;
268
253
  }, env?: NodeJS.ProcessEnv): LeaderLoopConfig;
269
254
  export declare const LEADER_PUSH_NETWORK_TIMEOUT_MS = 120000;
270
255
  export declare const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30000;
@@ -16,7 +16,7 @@ import { attachRepairLoopDeps } from "./repair-wire.js";
16
16
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
17
17
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
18
18
  import { currentPrincipal } from "../observability/principal-context.js";
19
- import { redactHead, redactPatchHead } from "../observability/run-terminal-log.js";
19
+ import { redactHead } from "../observability/run-terminal-log.js";
20
20
  const REPAIR_LOOP_WIRED = true;
21
21
  const sh = (env) => async (cmd) => {
22
22
  const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
@@ -34,18 +34,6 @@ const asIntegrationEnv = (env) => ({
34
34
  writeFile: async (p, c) => { const r = await env.writeFile(p, c); if (!r.ok)
35
35
  throw r.error; },
36
36
  });
37
- export function buildConflictResolverObjective(args) {
38
- const { workerId, repoDir, patch, conflictText } = args;
39
- return [
40
- `A parallel worker '${workerId}' ported a module on its own branch, but applying its patch onto the already-integrated tree at ${repoDir} produced a MERGE CONFLICT (git apply --3way). The OTHER workers' patches already applied cleanly — integrate THIS worker's changes too, resolving the overlap.`,
41
- `Resolve EVERY conflict in the working tree: open each file containing conflict markers (<<<<<<< / ======= / >>>>>>>) and merge BOTH sides' real intent (keep both workers' behaviour — never drop one side just to make it apply). Then apply any rejected hunks recorded in *.rej files by hand, and DELETE every *.rej and *.orig file.`,
42
- `When done, NO conflict markers and NO *.rej files may remain under ${repoDir}. Verify yourself before finishing: grep -rIlE '^(<<<<<<<|>>>>>>>)' . ; find . -name '*.rej' — BOTH must print nothing.`,
43
- `HARD RULES: do NOT git commit / git add / git push (the coordinator squashes at the end); do NOT modify build configuration; do NOT call AskUserQuestion — decide autonomously. Preserve both sides' ported semantics.`,
44
- ``,
45
- delimitUntrusted("CONFLICTING WORKER PATCH (untrusted)", redactPatchHead(patch, 6000)),
46
- delimitUntrusted("CONFLICT DETAILS (untrusted)", conflictText),
47
- ].join("\n");
48
- }
49
37
  export function createEnvReaper(env, what) {
50
38
  if (!hasDestroy(env)) {
51
39
  throw new Error(`${what}: the execution-env factory returned an env with no destroy() — the leader OWNS this dedicated sandbox and this closure is its only teardown, so it would leak (and keep billing) until the backend's own timeout reaps it. Deploy a factory whose env implements RemoteExecutionEnv.destroy for the leader lane.`);
@@ -98,15 +86,12 @@ export function leaderLoopConfig(cfg, env = process.env) {
98
86
  ? parseNumOrFailNonNegative("LEADER_REPAIR_ROUNDS", env.LEADER_REPAIR_ROUNDS)
99
87
  : (cfg.repairRounds ?? 0)) || 0);
100
88
  const repairBudgetUsd = parseNumOrFailNonNegative("LEADER_REPAIR_BUDGET_USD", env.LEADER_REPAIR_BUDGET_USD ?? "4") || 4;
101
- const conflictRounds = Math.max(0, Math.floor(env.LEADER_CONFLICT_ROUNDS !== undefined
102
- ? parseNumOrFailNonNegative("LEADER_CONFLICT_ROUNDS", env.LEADER_CONFLICT_ROUNDS)
103
- : (cfg.conflictRounds ?? 0)) || 0);
104
89
  const repairLoopOn = String(env.LEADER_REPAIR_LOOP ?? "").toLowerCase() === "true";
105
90
  const measureGatesOn = String(env.LEADER_MEASURE_GATES ?? "").toLowerCase() === "true";
106
91
  const repairLoopAttempts = Math.min(3, Math.max(2, Math.floor(parseNumOrFailNonNegative("LEADER_REPAIR_LOOP_ATTEMPTS", env.LEADER_REPAIR_LOOP_ATTEMPTS ?? "2")) || 2));
107
92
  const oracleFlakyK = Math.max(1, Math.floor(parseNumOrFailNonNegative("LEADER_ORACLE_FLAKY_K", env.LEADER_ORACLE_FLAKY_K ?? "2")) || 2);
108
93
  return {
109
- repairRounds, repairBudgetUsd, conflictRounds, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK,
94
+ repairRounds, repairBudgetUsd, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK,
110
95
  ...(env.LEADER_BUDGET_USD ? { replanBudgetUsd: parseNumOrFailNonNegative("LEADER_BUDGET_USD", env.LEADER_BUDGET_USD) } : {}),
111
96
  };
112
97
  }
@@ -148,7 +133,7 @@ export function createLeaderRunner(cfg) {
148
133
  const repo = `${W}/repo`;
149
134
  const ident = cfg.git ?? { name: "leader", email: "leader@local" };
150
135
  const { leaderTimeoutMs, workerLimits, presignTtlSec, resourceSuspend: resourceCfg } = leaderResourceConfig();
151
- const { repairRounds, repairBudgetUsd, conflictRounds, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK, replanBudgetUsd, } = leaderLoopConfig({ repairRounds: cfg.repairRounds, conflictRounds: cfg.conflictRounds });
136
+ const { repairRounds, repairBudgetUsd, repairLoopOn, measureGatesOn, repairLoopAttempts, oracleFlakyK, replanBudgetUsd, } = leaderLoopConfig({ repairRounds: cfg.repairRounds });
152
137
  const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
153
138
  const governanceSeat = cfg.governance ?? {};
154
139
  const deploymentPostureSeat = () => cfg.deploymentPosture?.() ?? {};
@@ -204,43 +189,6 @@ export function createLeaderRunner(cfg) {
204
189
  },
205
190
  };
206
191
  };
207
- const mkConflictResolver = (rawEnv, repoDir, oracleFiles) => {
208
- if (conflictRounds <= 0)
209
- return undefined;
210
- return {
211
- maxRounds: conflictRounds,
212
- run: async ({ round, workerId, conflictText, patch }) => {
213
- for (const f of oracleFiles) {
214
- await rawEnv.remove(f.path).catch(() => { });
215
- const ex = await rawEnv.exists(f.path);
216
- if (ex.ok && ex.value)
217
- throw new Error(`oracle still present after remove (${f.path}) — refusing conflict-resolve (measurement integrity)`);
218
- }
219
- try {
220
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
221
- const objective = buildConflictResolverObjective({ workerId, repoDir, patch, conflictText });
222
- const res = await runner
223
- .runTaskStream({
224
- objective,
225
- ...principalSpec(),
226
- sessionId: `leader-conflict-${Date.now()}-${round}`,
227
- limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
228
- })
229
- .result();
230
- if (res.terminal.kind !== "completed")
231
- throw new Error(`conflict-resolve round ${round} did not complete (status=${terminalProjection(res.terminal).status})`);
232
- return { costUsd: res.stats?.costMicroUsd !== undefined ? res.stats.costMicroUsd / 1e6 : undefined, note: res.result?.slice(0, 200) };
233
- }
234
- finally {
235
- for (const f of oracleFiles) {
236
- const wr = await rawEnv.writeFile(f.path, f.content);
237
- if (!wr.ok)
238
- throw wr.error;
239
- }
240
- }
241
- },
242
- };
243
- };
244
192
  return async (raw) => {
245
193
  const body = raw;
246
194
  if (!body.objective || !body.durableRemote || !body.testCmd || !body.seedCmd || !body.baseSha) {
@@ -406,8 +354,7 @@ export function createLeaderRunner(cfg) {
406
354
  const rawEnv = env;
407
355
  const oracleFiles = body.oracleFiles ?? [];
408
356
  const repair = mkRepair(rawEnv, repo, oracleFiles);
409
- const conflictResolver = mkConflictResolver(rawEnv, repo, oracleFiles);
410
- return { env: asIntegrationEnv(env), destroy: reap, ...(repair ? { repair } : {}), ...(conflictResolver ? { conflictResolver } : {}) };
357
+ return { env: asIntegrationEnv(env), destroy: reap, ...(repair ? { repair } : {}) };
411
358
  }
412
359
  if (!cfg.e2bApiKey)
413
360
  throw new Error("leader wire: no envFactory and no e2bApiKey");
@@ -424,7 +371,7 @@ export function createLeaderRunner(cfg) {
424
371
  await env.destroy().catch(() => { });
425
372
  throw e;
426
373
  }
427
- return { env: asIntegrationEnv(env), destroy: () => env.destroy().then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? []) ? { repair: mkRepair(env, repo, body.oracleFiles ?? []) } : {}), ...(mkConflictResolver(env, repo, body.oracleFiles ?? []) ? { conflictResolver: mkConflictResolver(env, repo, body.oracleFiles ?? []) } : {}) };
374
+ return { env: asIntegrationEnv(env), destroy: () => env.destroy().then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? []) ? { repair: mkRepair(env, repo, body.oracleFiles ?? []) } : {}) };
428
375
  };
429
376
  const push = createCoordinatorPush(body.durableRemote, body.targetRef, ident);
430
377
  const deps = {
@@ -215,7 +215,7 @@ export declare const FAIL_OPEN_TAGS: {
215
215
  };
216
216
  readonly "server.run-terminal.redact-threw": {
217
217
  readonly cls: "F";
218
- readonly note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
218
+ readonly note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
219
219
  };
220
220
  readonly "server.run-terminal.redact-capped": {
221
221
  readonly cls: "F";
@@ -229,10 +229,6 @@ export declare const FAIL_OPEN_TAGS: {
229
229
  readonly cls: "F";
230
230
  readonly note: "S-107:诊断文本座(merge details/measure/grader trace、leader 日志座、planner 样本、journal 预览、workflow_run error cut)长度 > DIAG_INPUT_CAP(256KB)⇒ 整段换 «redacted:oversized-diagnostic» [len=N](fail-closed 取证丢弃;固定窗不 fail-closed 故不开窗);计数=多少段诊断文本被整段丢弃";
231
231
  };
232
- readonly "server.run-terminal.patch-head-cut": {
233
- readonly cls: "F";
234
- readonly note: "S-107:进模型的 worker 补丁(collapse 目标 / 冲突解决目标)> DIAG_INPUT_CAP(256KB)⇒ **不内联**,整份换 «redacted:oversized-patch-not-inlined» [len=N](第十三轮开的 256KB 头窗及其裁切守卫在五轮复审后撤回);计数=多少份补丁因超限未内联";
235
- };
236
232
  readonly "server.run-terminal.output-truncated": {
237
233
  readonly cls: "F";
238
234
  readonly note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断";
@@ -253,6 +249,10 @@ export declare const FAIL_OPEN_TAGS: {
253
249
  readonly cls: "F";
254
250
  readonly note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js:344),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。";
255
251
  };
252
+ readonly "server.ensure-index.concurrently-unsupported": {
253
+ readonly cls: "F";
254
+ readonly note: "S-287(`plugins/ensure-index.ts` 的 PG 臂):这口 PG **协议**面不认 `CREATE INDEX CONCURRENTLY`。今天唯一的真消费者是**仓内离线镜像的手搓 `PgQueryFn` 假件**(`pg-mem` 不是本仓依赖,`test/memory-embedder-fingerprint.test.ts` 头注已为此记过一次档,这里不重复那个错)。判据是**能力面**不是名字,且**只认阳性信号**:SQLSTATE ∈ {`0A000` feature_not_supported、`42601` syntax_error},或错误**完全不带 `code`**(= 不是驱动也不是服务端给的错误)⇒ 判「不认」,回退成普通 `CREATE INDEX IF NOT EXISTS`;**带任何其它 `code` 的一律原样抛** —— SQLSTATE 的 `55P03`/`57014`/`23505`/`42501`/`25001` 与 libuv 的 `ECONNRESET`/`EPIPE`/`ETIMEDOUT` 都在这一侧(⚠️ 不要按「5 位大写」的形状去分这两族:`EPIPE` 恰好也是 5 位大写)。放行的最坏后果 = 这一次建索引持表级 `SHARE` 锁阻塞该表的写(纯**性能/可用性**面,索引本身照样建成、语义零差别),而能走到这条臂的面**按定义**不是生产 PG。计数非零 = 有一口本以为是真 PG 的部署其实不认 CONCURRENTLY,运维要去看它到底是什么。detail = `<表>.<索引名>`。";
255
+ };
256
256
  };
257
257
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
258
258
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
@@ -206,12 +206,11 @@ export const FAIL_OPEN_TAGS = {
206
206
  },
207
207
  "server.run-terminal.redact-threw": {
208
208
  cls: "F",
209
- note: "四个发射座(errorMessage 终态文案 / 诊断文本 diag / 补丁头 patch-head / 工件 artifact,probe 前缀判别;普查 56 行):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
209
+ note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
210
210
  },
211
211
  "server.run-terminal.redact-capped": { cls: "F", note: "S-107:终态自由文本(errorMessage/blockedReason/remoteEnvFailures[].message/error 列/done·failed 行)长度 > REDACT_INPUT_CAP(64KB)⇒ 整段换 «redacted:oversized»(fail-closed 取证丢弃,不截断不扫);计数=多少条终态文案被整段丢弃" },
212
212
  "server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
213
213
  "server.run-terminal.diag-capped": { cls: "F", note: "S-107:诊断文本座(merge details/measure/grader trace、leader 日志座、planner 样本、journal 预览、workflow_run error cut)长度 > DIAG_INPUT_CAP(256KB)⇒ 整段换 «redacted:oversized-diagnostic» [len=N](fail-closed 取证丢弃;固定窗不 fail-closed 故不开窗);计数=多少段诊断文本被整段丢弃" },
214
- "server.run-terminal.patch-head-cut": { cls: "F", note: "S-107:进模型的 worker 补丁(collapse 目标 / 冲突解决目标)> DIAG_INPUT_CAP(256KB)⇒ **不内联**,整份换 «redacted:oversized-patch-not-inlined» [len=N](第十三轮开的 256KB 头窗及其裁切守卫在五轮复审后撤回);计数=多少份补丁因超限未内联" },
215
214
  "server.run-terminal.output-truncated": { cls: "F", note: "S-107:持久 error 脱后 UTF-8 字节 > 65,000(TiDB TEXT 65,535 字节)⇒ 脱后切并带标记(切在占位里不是泄漏);计数=多少条终态文案被字节封顶截断" },
216
215
  "server.runs.terminal-ledger-write-failed": { cls: "F", note: "S-107(全量门 silent-degradation 棘轮抓出的五处静默 `catch {}`):run 终局链里**账本腿**(flush 缓冲 text/reasoning、append `failed` 行)与终态翻转(setTerminal)已不共命运——账本 INSERT 抛不再让 run 卡 running 到 reaper;但那条抛掉的账本行之前是**静默**吞的。detail `arm=` 四臂(flush-clean-cancel / append-clean-cancel / flush-catch / append-thrown-cancel / append-failed):缺的是 run_events 尾巴(缓冲文本或 failed 事件),run 行本身仍是 durable truth,读面/reaper 容忍缺失,故 F 类;计数=多少次终局账本写失败。" },
217
216
  "server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
@@ -223,6 +222,10 @@ export const FAIL_OPEN_TAGS = {
223
222
  cls: "F",
224
223
  note: "S-55(test [5864] 独立复现):pg 连接池里一条连接被外部终止(pg_terminate_backend / 云端故障切换 / LB 空闲回收)或自身网络错误。**两态一 tag,detail 判别**(`state=idle` / `state=checked-out`;先例=park 墓碑行的两臂 detail 判别):①idle 态 —— node-postgres 语义(pg-pool@3.14.0 makeIdleListener):池在 emit 'error' **之前**已 `_remove` 掉该 client,坏连接自然淘汰,下次 acquire 发新连接,在飞查询零影响;修前 Pool 上无 'error' 监听 ⇒ EventEmitter 把事件转 throw ⇒ uncaughtException ⇒ 整进程 exit 1。②checked-out 态(codex R1-[high] 补,同批修)—— pg-pool 借出时摘 idle 监听(index.js:344),`pool.connect()` 显式持有的连接(事务)在 checkout 期死时 client 直接 emit 'error' 同样打死进程;守卫只观察:中毒 client 的后续查询由 pg 以 `_queryable=false` 响亮拒,归还时池淘汰;同一次 checkout 的死可能双发事件(FATAL 消息 + socket end),warn 逐枚、计数按 checkout 去重。放行的最坏后果 = 下一次 acquire 多付一次建连,故 F 类。必须留痕:「连接不断被外部杀」(故障切换风暴 / 回收策略过激 / 有人在库上清连接)与「一切正常」在服务面同形;每次事件另有逐次结构化 warn(`pg_pool_idle_client_error` / `pg_pool_checked_out_client_error`,含 code/severity),本计数让频率曲线可读。`pool.query()` 快路不在洞内(pg-pool 自带 checkout 期 once('error') 守卫)。mysql2 孪生(tidb-pool)**不同病**:PoolConnection 构造器恒挂 once('error') 库内吞并淘汰、借出期同在(坐标成文在 createTidbPool 头注),故无同族 tag。",
225
224
  },
225
+ "server.ensure-index.concurrently-unsupported": {
226
+ cls: "F",
227
+ note: "S-287(`plugins/ensure-index.ts` 的 PG 臂):这口 PG **协议**面不认 `CREATE INDEX CONCURRENTLY`。今天唯一的真消费者是**仓内离线镜像的手搓 `PgQueryFn` 假件**(`pg-mem` 不是本仓依赖,`test/memory-embedder-fingerprint.test.ts` 头注已为此记过一次档,这里不重复那个错)。判据是**能力面**不是名字,且**只认阳性信号**:SQLSTATE ∈ {`0A000` feature_not_supported、`42601` syntax_error},或错误**完全不带 `code`**(= 不是驱动也不是服务端给的错误)⇒ 判「不认」,回退成普通 `CREATE INDEX IF NOT EXISTS`;**带任何其它 `code` 的一律原样抛** —— SQLSTATE 的 `55P03`/`57014`/`23505`/`42501`/`25001` 与 libuv 的 `ECONNRESET`/`EPIPE`/`ETIMEDOUT` 都在这一侧(⚠️ 不要按「5 位大写」的形状去分这两族:`EPIPE` 恰好也是 5 位大写)。放行的最坏后果 = 这一次建索引持表级 `SHARE` 锁阻塞该表的写(纯**性能/可用性**面,索引本身照样建成、语义零差别),而能走到这条臂的面**按定义**不是生产 PG。计数非零 = 有一口本以为是真 PG 的部署其实不认 CONCURRENTLY,运维要去看它到底是什么。detail = `<表>.<索引名>`。"
228
+ },
226
229
  };
227
230
  export function failOpenTagForDroppedFrame(frameType) {
228
231
  switch (frameType) {
@@ -135,13 +135,6 @@ export declare const DIAG_INPUT_CAP = 262144;
135
135
  export declare const OVERSIZED_DIAG_PLACEHOLDER = "\u00ABredacted:oversized-diagnostic\u00BB";
136
136
  export declare function redactDiagnosticText(text: string): string;
137
137
  export declare function redactHead(text: string, keep: number): string;
138
- /** 进模型的 worker 补丁头(collapse 目标 / 冲突解决目标):**不开窗**。第十三轮为保留大 diff 的可复用头部开了 256KB 头窗,
139
- * 此后 codex r12–r15 与第十五至十七轮重扫五轮各证一次「窗上的裁切守卫可被打穿」(无上界的 JSON 值/URL 密码臂;脱前/脱后坐标
140
- * 错位;PEM 逐块;前面一处脱敏会**吃掉**守卫所依赖的锚点)——同 S-103 的教训:猜边界的规则总有反例。终形=补丁 ≤256KB 整段扫
141
- * 再切 keep(完全正确);>256KB **不内联**(占位带原长,tag `patch-head-cut`):冲突解决者仍有工作树里的冲突标记,collapse solo
142
- * 对 lockfile 级大 diff 失去内联上下文(worker 分支仍在)——这是有意成文的安全轴取舍。 */
143
- export declare const OVERSIZED_PATCH_PLACEHOLDER = "\u00ABredacted:oversized-patch-not-inlined\u00BB";
144
- export declare function redactPatchHead(patch: string, keep: number): string;
145
138
  export declare function redactTail(text: string, keep: number): string;
146
139
  export declare const ARTIFACT_INPUT_CAP = 262144;
147
140
  export declare const OVERSIZED_ARTIFACT_PLACEHOLDER = "\u00ABredacted:oversized-artifact\u00BB";
@@ -383,7 +383,6 @@ export function redactDiagnosticText(text) {
383
383
  export function redactHead(text, keep) {
384
384
  return redactDiagnosticText(text).slice(0, keep);
385
385
  }
386
- export const OVERSIZED_PATCH_PLACEHOLDER = "«redacted:oversized-patch-not-inlined»";
387
386
  function diffScanRefusal(patch, direct) {
388
387
  if (/^diff --(?:cc|combined) |^@{3,} /m.test(patch))
389
388
  return "combined-diff";
@@ -415,25 +414,6 @@ function diffScanRefusal(patch, direct) {
415
414
  }
416
415
  return undefined;
417
416
  }
418
- export function redactPatchHead(patch, keep) {
419
- if (patch.length > DIAG_INPUT_CAP) {
420
- recordFailOpen("server.run-terminal.patch-head-cut", `not-inlined len=${patch.length} keep=${keep}`);
421
- return `${OVERSIZED_PATCH_PLACEHOLDER} [len=${patch.length}]`;
422
- }
423
- try {
424
- const direct = redactSecrets(patch);
425
- const refusal = diffScanRefusal(patch, direct);
426
- if (refusal !== undefined) {
427
- recordFailOpen("server.run-terminal.patch-head-cut", `${refusal} len=${patch.length} keep=${keep}`);
428
- return `${OVERSIZED_PATCH_PLACEHOLDER} [len=${patch.length}]`;
429
- }
430
- return direct.slice(0, keep);
431
- }
432
- catch (err) {
433
- recordFailOpen("server.run-terminal.redact-threw", `patch-head len=${patch.length} err=${err instanceof Error ? err.name : String(err)}`);
434
- return OVERSIZED_REDACTION_PLACEHOLDER;
435
- }
436
- }
437
417
  export function redactTail(text, keep) {
438
418
  return redactDiagnosticText(text).slice(-keep);
439
419
  }
@@ -506,6 +486,15 @@ export function redactLeaderResultWithFlag(result) {
506
486
  m.measure = { ...measure, reason: str(measure.reason) };
507
487
  out.merge = m;
508
488
  }
489
+ const conflict = obj(r.conflict);
490
+ if (conflict) {
491
+ const c = { ...conflict };
492
+ if (typeof conflict.rejHead === "string")
493
+ c.rejHead = art(conflict.rejHead);
494
+ if (Array.isArray(conflict.files))
495
+ c.files = conflict.files.map((f) => (typeof f === "string" ? str(f) : f));
496
+ out.conflict = c;
497
+ }
509
498
  const cp = obj(r.candidatePatch);
510
499
  if (cp)
511
500
  out.candidatePatch = { ...cp, ...(typeof cp.reason === "string" ? { reason: str(cp.reason) } : {}), ...(typeof cp.patch === "string" ? { patch: art(cp.patch) } : {}) };
@@ -24,6 +24,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
24
24
  import type { Pool as PgPool } from "pg";
25
25
  import { type SqlDriver, type SqlExec, type SqlTxConn } from "./sql-driver.js";
26
26
  import type { AdoptionRejectCode } from "../adoption/wire.js";
27
+ import { type IndexSpec } from "./ensure-index.js";
27
28
  export declare const ADOPTION_LOG_TABLE = "adoption_log";
28
29
  /** 弧的阶段(183 §3.2 根级状态机的 form b 投影)。**单调**,只增不减。 */
29
30
  export declare const ADOPTION_PHASE: {
@@ -106,6 +107,8 @@ export interface AdoptionLogStore {
106
107
  /** MySQL 协议方言的建表语句(展开进 tidb-pool 的中央 `SCHEMA_STATEMENTS`,与 approval-ask 同姿势:
107
108
  * 跟着中央 ensureSchema 在 named-lock 的那条 conn 上建,不绕开 DDL 串行化)。 */
108
109
  export declare const TIDB_ADOPTION_LOG_STATEMENTS: readonly string[];
110
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgAdoptionLogSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
111
+ export declare const ADOPTION_LOG_INDEXES: readonly IndexSpec[];
109
112
  /** PostgreSQL 孪生(由 `ensurePgSchema` 在 advisory lock 的那条 client 上调用)。 */
110
113
  export declare function ensurePgAdoptionLogSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
111
114
  /**
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
3
+ import { ensureIndexes } from "./ensure-index.js";
3
4
  export const ADOPTION_LOG_TABLE = "adoption_log";
4
5
  export const ADOPTION_PHASE = {
5
6
  INTENT: 2,
@@ -30,6 +31,9 @@ export const TIDB_ADOPTION_LOG_STATEMENTS = [
30
31
  KEY idx_adoption_log_state (state)
31
32
  ) COLLATE utf8mb4_bin`,
32
33
  ];
34
+ export const ADOPTION_LOG_INDEXES = [
35
+ { table: `${ADOPTION_LOG_TABLE}`, name: "idx_adoption_log_state", columns: ["state"] },
36
+ ];
33
37
  export async function ensurePgAdoptionLogSchema(q) {
34
38
  await q(`CREATE TABLE IF NOT EXISTS ${ADOPTION_LOG_TABLE} (
35
39
  adoption_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -48,7 +52,7 @@ export async function ensurePgAdoptionLogSchema(q) {
48
52
  PRIMARY KEY (adoption_id),
49
53
  CONSTRAINT uq_adoption_log_from UNIQUE (from_principal)
50
54
  )`);
51
- await q(`CREATE INDEX IF NOT EXISTS idx_adoption_log_state ON ${ADOPTION_LOG_TABLE} (state)`);
55
+ await ensureIndexes(q, "pg", ADOPTION_LOG_INDEXES);
52
56
  }
53
57
  const COLS = "adoption_id, from_principal, to_principal, phase, state, legs, configs, report, reject_code, reject_detail, created_at_ms, updated_at_ms";
54
58
  function toRow(r) {
@@ -41,6 +41,7 @@ import type { Pool as PgPool } from "pg";
41
41
  import type { PgQueryFn } from "./pg-query.js";
42
42
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
43
43
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
44
+ import { type IndexSpec } from "./ensure-index.js";
44
45
  export type AskDecision = "approve" | "deny";
45
46
  export interface AskRow {
46
47
  askId: string;
@@ -449,6 +450,8 @@ export declare const TIDB_APPROVAL_ASK_STATEMENTS: readonly string[];
449
450
  /** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
450
451
  * (那里展开了同一份数组),本函数留给只需要这两张表的集成测试。 */
451
452
  export declare function ensureTiDBApprovalAskSchema(pool: MySqlPool): Promise<void>;
453
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgApprovalAskSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
454
+ export declare const APPROVAL_ASK_INDEXES: readonly IndexSpec[];
452
455
  export declare function ensurePgApprovalAskSchema(q: PgQueryFn): Promise<void>;
453
456
  /**
454
457
  * [ref] 升级前置断言 —— **拒启**,不是 warn(形照 [ref] 的 `assertPermissionRuleApprovalSchema` /
@@ -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 { ensureIndexes } from "./ensure-index.js";
4
5
  export function assertIntegralEpochMs(field, value) {
5
6
  if (!Number.isSafeInteger(value)) {
6
7
  throw new TypeError(`approval-ask store: ${field} must be an integer epoch-ms (BIGINT column), got ${String(value)}`);
@@ -126,6 +127,13 @@ export async function ensureTiDBApprovalAskSchema(pool) {
126
127
  for (const stmt of TIDB_APPROVAL_ASK_STATEMENTS)
127
128
  await pool.query(stmt);
128
129
  }
130
+ export const APPROVAL_ASK_INDEXES = [
131
+ { table: `${APPROVAL_BATCH_TABLE}`, name: "idx_batch_task", columns: ["task_id"] },
132
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_task_state", columns: ["task_id", "state"] },
133
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_session_state", columns: ["session_id", "state"] },
134
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_batch", columns: ["batch_id"] },
135
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_state", columns: ["state"] },
136
+ ];
129
137
  export async function ensurePgApprovalAskSchema(q) {
130
138
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCH_TABLE} (
131
139
  batch_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -137,7 +145,6 @@ export async function ensurePgApprovalAskSchema(q) {
137
145
  updated_at_ms BIGINT NOT NULL,
138
146
  PRIMARY KEY (batch_id)
139
147
  )`);
140
- await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCH_TABLE} (task_id)`);
141
148
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASK_TABLE} (
142
149
  ask_id VARCHAR(64) COLLATE "C" NOT NULL,
143
150
  task_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -179,10 +186,7 @@ export async function ensurePgApprovalAskSchema(q) {
179
186
  -- (task_id, idempotency_key) 复合唯一——MySQL twin 逐字同形(改动理由见那侧注)。
180
187
  CONSTRAINT uq_approval_ask_idem UNIQUE (task_id, idempotency_key)
181
188
  )`);
182
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_task_state ON ${APPROVAL_ASK_TABLE} (task_id, state)`);
183
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_session_state ON ${APPROVAL_ASK_TABLE} (session_id, state)`);
184
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_batch ON ${APPROVAL_ASK_TABLE} (batch_id)`);
185
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_state ON ${APPROVAL_ASK_TABLE} (state)`);
189
+ await ensureIndexes(q, "pg", APPROVAL_ASK_INDEXES);
186
190
  }
187
191
  export async function assertApprovalAskRuleMaterialSchema(query, dialect) {
188
192
  try {
@@ -32,6 +32,7 @@
32
32
  import type { Pool } from "mysql2/promise";
33
33
  import { type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore } from "@sema-agent/core";
34
34
  import type { PgQueryFn } from "./pg-query.js";
35
+ import { type IndexSpec } from "./ensure-index.js";
35
36
  export declare const BACKGROUND_AGENT_TABLE = "background_agent";
36
37
  type ReapOpts = {
37
38
  maxAgeMs?: number;
@@ -43,6 +44,8 @@ type ListOpts = {
43
44
  limit?: number;
44
45
  };
45
46
  export declare function ensureTiDBBackgroundAgentSchema(pool: Pool): Promise<void>;
47
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgBackgroundAgentSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
48
+ export declare const BACKGROUND_AGENT_INDEXES: readonly IndexSpec[];
46
49
  export declare function ensurePgBackgroundAgentSchema(q: PgQueryFn): Promise<void>;
47
50
  export declare class TiDBBackgroundAgentStore implements BackgroundAgentStore {
48
51
  private readonly pool;
@@ -1,6 +1,7 @@
1
1
  import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "@sema-agent/core";
2
2
  import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
3
3
  import { isMysqlDupKeyError } from "./sql-errors.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
4
5
  export const BACKGROUND_AGENT_TABLE = "background_agent";
5
6
  const SUMMARY_COLS = "handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json";
6
7
  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";
@@ -135,7 +136,12 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
135
136
  KEY idx_bga_anchor (scope_key, session_anchor, spawned_at_ms),
136
137
  KEY idx_bga_status (scope_key, status, updated_at_ms)
137
138
  ) COLLATE utf8mb4_bin`);
139
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", BACKGROUND_AGENT_INDEXES);
138
140
  }
141
+ export const BACKGROUND_AGENT_INDEXES = [
142
+ { table: `${BACKGROUND_AGENT_TABLE}`, name: "idx_bga_anchor", columns: ["scope_key", "session_anchor", "spawned_at_ms"] },
143
+ { table: `${BACKGROUND_AGENT_TABLE}`, name: "idx_bga_status", columns: ["scope_key", "status", "updated_at_ms"] },
144
+ ];
139
145
  export async function ensurePgBackgroundAgentSchema(q) {
140
146
  await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
141
147
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
@@ -165,8 +171,7 @@ export async function ensurePgBackgroundAgentSchema(q) {
165
171
  record_json TEXT COLLATE "C" NOT NULL,
166
172
  PRIMARY KEY (scope_key, handle)
167
173
  )`);
168
- await q(`CREATE INDEX IF NOT EXISTS idx_bga_anchor ON ${BACKGROUND_AGENT_TABLE} (scope_key, session_anchor, spawned_at_ms)`);
169
- await q(`CREATE INDEX IF NOT EXISTS idx_bga_status ON ${BACKGROUND_AGENT_TABLE} (scope_key, status, updated_at_ms)`);
174
+ await ensureIndexes(q, "pg", BACKGROUND_AGENT_INDEXES);
170
175
  }
171
176
  export class TiDBBackgroundAgentStore {
172
177
  pool;
@@ -47,6 +47,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
47
47
  import type { Pool as PgPool } from "pg";
48
48
  import { type SqlDriver } from "./sql-driver.js";
49
49
  import { type AcquireConnectionInput, type AcquireConnectionResult, type AppendAuditInput, type BindSessionInput, type BindSessionResult, type ConsumeEnrollTokenInput, type ConsumeEnrollTokenResult, type DeviceAdmissionView, type DeviceAuditRow, type DeviceEnrollTokenRow, type DeviceOwner, type DeviceRow, type DeviceSessionRow, type DeviceStore, type HeartbeatConnectionInput, type IssueEnrollTokenInput, type ListAuditFilter, type RebindSessionInput, type RebindSessionResult, type RevokeDeviceInput, type RevokeDeviceResult } from "../device-store.js";
50
+ import { type IndexSpec } from "./ensure-index.js";
50
51
  /**
51
52
  * ── 命名(**落库一律单数**,与设计稿的复数形不同)──────────────────────────────────────────────────
52
53
  * design v2 §6 的 DDL 写的是 `devices` / `device_sessions` / `device_enrollment_tokens`(复数),但本仓有一条
@@ -63,6 +64,8 @@ export declare const TIDB_DEVICE_STATEMENTS: readonly string[];
63
64
  /** {@link TIDB_DEVICE_STATEMENTS} 的遍历壳(集成测试用;生产装配由中央 ensureSchema 展开同一份数组)。 */
64
65
  export declare function ensureTiDBDeviceSchema(pool: MySqlPool): Promise<void>;
65
66
  type PgQuery = (sql: string, params?: unknown[]) => Promise<unknown>;
67
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgDeviceSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
68
+ export declare const DEVICE_INDEXES: readonly IndexSpec[];
66
69
  export declare function ensurePgDeviceSchema(q: PgQuery): Promise<void>;
67
70
  /**
68
71
  * O4 升级前置探针 —— **拒启,不是 warn**([ref]/[ref]/[ref] `assert*Schema` 族先例):`device_audit` 的
@@ -1,6 +1,7 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { isDupKeyError, isCheckViolationError } from "./sql-errors.js";
3
3
  import { DEVICE_AUDIT_EVENTS, assertAcquireShape, assertDeviceIdShape, assertDeviceOwnerShape, assertEnrollTokenHashShape, assertIssueTokenShape, assertNewDeviceShape, assertRebindShape, assertRevokeShape, assertRootSessionIdShape, assertWholeSeconds, parseDeviceAuditEvent, parseDevicePlatformOs, parseDeviceStatus, } from "../device-store.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
4
5
  export const DEVICES_TABLE = "device";
5
6
  export const DEVICE_ENROLL_TOKENS_TABLE = "device_enroll_token";
6
7
  export const DEVICE_SESSIONS_TABLE = "device_session";
@@ -84,7 +85,14 @@ export const TIDB_DEVICE_STATEMENTS = [
84
85
  export async function ensureTiDBDeviceSchema(pool) {
85
86
  for (const stmt of TIDB_DEVICE_STATEMENTS)
86
87
  await pool.query(stmt);
88
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", DEVICE_INDEXES);
87
89
  }
90
+ export const DEVICE_INDEXES = [
91
+ { table: `${DEVICES_TABLE}`, name: "idx_device_owner", columns: ["owner_tenant", "owner_subject"] },
92
+ { table: `${DEVICE_SESSIONS_TABLE}`, name: "idx_device_session_device", columns: ["device_id"] },
93
+ { table: `${DEVICE_AUDIT_TABLE}`, name: "idx_device_audit_device", columns: ["device_id", "at"] },
94
+ { table: `${DEVICE_AUDIT_TABLE}`, name: "idx_device_audit_event", columns: ["event", "at"] },
95
+ ];
88
96
  export async function ensurePgDeviceSchema(q) {
89
97
  await q(`CREATE TABLE IF NOT EXISTS ${DEVICES_TABLE} (
90
98
  device_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -112,7 +120,6 @@ export async function ensurePgDeviceSchema(q) {
112
120
  PRIMARY KEY (device_id),
113
121
  CONSTRAINT ${UQ_DEVICES_PUBKEY} UNIQUE (owner_tenant, pubkey)
114
122
  )`);
115
- await q(`CREATE INDEX IF NOT EXISTS idx_device_owner ON ${DEVICES_TABLE} (owner_tenant, owner_subject)`);
116
123
  await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_ENROLL_TOKENS_TABLE} (
117
124
  token_hash VARCHAR(128) COLLATE "C" NOT NULL,
118
125
  target_tenant VARCHAR(190) COLLATE "C" NOT NULL,
@@ -135,7 +142,6 @@ export async function ensurePgDeviceSchema(q) {
135
142
  rev BIGINT NOT NULL DEFAULT 0,
136
143
  PRIMARY KEY (root_session_id)
137
144
  )`);
138
- await q(`CREATE INDEX IF NOT EXISTS idx_device_session_device ON ${DEVICE_SESSIONS_TABLE} (device_id)`);
139
145
  await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_AUDIT_TABLE} (
140
146
  id BIGSERIAL PRIMARY KEY,
141
147
  at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
@@ -146,8 +152,7 @@ export async function ensurePgDeviceSchema(q) {
146
152
  detail JSONB,
147
153
  CONSTRAINT ck_device_audit_event CHECK (event IN (${AUDIT_EVENT_CHECK}))
148
154
  )`);
149
- await q(`CREATE INDEX IF NOT EXISTS idx_device_audit_device ON ${DEVICE_AUDIT_TABLE} (device_id, at)`);
150
- await q(`CREATE INDEX IF NOT EXISTS idx_device_audit_event ON ${DEVICE_AUDIT_TABLE} (event, at)`);
155
+ await ensureIndexes(q, "pg", DEVICE_INDEXES);
151
156
  }
152
157
  export async function assertDeviceAuditRebindEventSchema(db) {
153
158
  const conn = await db.connect();
@@ -0,0 +1,42 @@
1
+ /** 本原语认得的两种方言(闭集;新方言进来是编译期的事,不是运行期的 `default` 臂)。 */
2
+ export type SqlDialect = "mysql" | "pg";
3
+ /**
4
+ * 一条索引的**声明**(纯数据)。两方言共用同一份声明 —— 这就是"一处属主"的兑现物:
5
+ * per-store 文件各导出自己那份 `readonly IndexSpec[]`,PG 侧由该 store 的 `ensurePg*Schema` 应用,
6
+ * MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 统一应用(与 `SCHEMA_STATEMENTS` 的组合姿势同形)。
7
+ */
8
+ export interface IndexSpec {
9
+ /** 表名(与两方言的 `CREATE TABLE` 同名——本仓两方言表名逐字相同)。 */
10
+ readonly table: string;
11
+ /** 索引名(两方言同名;MySQL 侧它同时是 `CREATE TABLE` 里那条内联 `KEY` 的名字)。 */
12
+ readonly name: string;
13
+ /** 列序(等值前缀在前、排序列在后;与 MySQL 内联 `KEY` 的列序**必须**逐字一致)。 */
14
+ readonly columns: readonly string[];
15
+ /** UNIQUE 索引(MySQL 内联 `UNIQUE KEY` 的孪生)。缺省 = 普通索引。 */
16
+ readonly unique?: boolean;
17
+ }
18
+ /**
19
+ * 两方言的查询面。mysql2 的 `conn.query` 答 `[rows, fields]`,node-pg 答 `{ rows }` —— 两种形的读法
20
+ * 都关在本文件里(调用点只管把自己手上那只 `query` 传进来)。
21
+ *
22
+ * 🔴 **本原语一条绑定参数都不发**(探针与 DDL 全是字面量)。两条理由:
23
+ * ① 它的输入是**声明**不是数据 —— `IndexSpec` 的表名/索引名都是编译期常量,没有用户输入可注入,
24
+ * 字面量与占位符在安全性上等价;
25
+ * ② PG 那半的 `q` 是**调用方递进来的** `PgQueryFn`(十来个 `ensurePg*Schema` 都收它),而那些 ensure
26
+ * 腿在本批之前**从不带参**,于是仓里真有丢掉 `params` 只转发 sql 的包装(如
27
+ * `test/permission-rule-store-db-integration.test.ts` 的 `(sql) => db.query(sql, [])`)。一条带 `$1`
28
+ * 的探针会在那种包装上报 `there is no parameter $1` —— 而那是**调用方**的形,不是本原语能管的。
29
+ * 不带参 ⇒ 这一整类接缝脆弱性结构上不存在。两方言同律,不留一边带参一边不带的不对称。
30
+ */
31
+ export type IndexQueryFn = (sql: string, params?: unknown[]) => Promise<unknown>;
32
+ /**
33
+ * 幂等地保证一条索引在场。**这是本仓唯一允许发加索引 DDL 的地方**(SCHEMA POLICY 窄放的全部内容)。
34
+ *
35
+ * @param q 方言对应的查询面(mysql2 conn/pool 的 `query`,或裸 `PgQueryFn`)
36
+ * @param dialect 方言闭集
37
+ * @param spec 索引声明(两方言共用同一份)
38
+ */
39
+ export declare function ensureIndex(q: IndexQueryFn, dialect: SqlDialect, spec: IndexSpec): Promise<void>;
40
+ /** {@link ensureIndex} 的**逐条串行**遍历(DDL 并发打同一张表会踩 catalog 竞争,故不并行)。 */
41
+ export declare function ensureIndexes(q: IndexQueryFn, dialect: SqlDialect, specs: readonly IndexSpec[]): Promise<void>;
42
+ //# sourceMappingURL=ensure-index.d.ts.map