@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.
Files changed (89) hide show
  1. package/USAGE.md +17 -14
  2. package/dist/bench/s1/arms.d.ts +4 -4
  3. package/dist/bench/s1/arms.js +1 -2
  4. package/dist/bench/s1/live-deps.js +0 -1
  5. package/dist/bench/s1/runner-ctx.d.ts +3 -3
  6. package/dist/bench/s1/tasks.d.ts +8 -4
  7. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  8. package/dist/boot/stage-07-capability-layer.js +1 -1
  9. package/dist/boot/stage-08-reapers.d.ts +1 -0
  10. package/dist/boot/stage-09-leader.d.ts +1 -0
  11. package/dist/boot/stage-10-http-server.d.ts +1 -0
  12. package/dist/boot/stage-10-http-server.js +2 -1
  13. package/dist/config-catalog.js +2 -3
  14. package/dist/config.js +1 -0
  15. package/dist/http/routes/capabilities.js +2 -0
  16. package/dist/http/server.d.ts +8 -0
  17. package/dist/leader/endpoint.js +2 -0
  18. package/dist/leader/leader.d.ts +33 -6
  19. package/dist/leader/leader.js +54 -45
  20. package/dist/leader/merge.d.ts +38 -29
  21. package/dist/leader/merge.js +45 -38
  22. package/dist/leader/wire.d.ts +2 -17
  23. package/dist/leader/wire.js +5 -58
  24. package/dist/observability/fail-open.d.ts +5 -5
  25. package/dist/observability/fail-open.js +5 -2
  26. package/dist/observability/run-terminal-log.d.ts +0 -7
  27. package/dist/observability/run-terminal-log.js +9 -20
  28. package/dist/plugins/adoption-log-sql.d.ts +3 -0
  29. package/dist/plugins/adoption-log-sql.js +5 -1
  30. package/dist/plugins/approval-ask-store-sql.d.ts +3 -0
  31. package/dist/plugins/approval-ask-store-sql.js +9 -5
  32. package/dist/plugins/background-agent-store-sql.d.ts +3 -0
  33. package/dist/plugins/background-agent-store-sql.js +7 -2
  34. package/dist/plugins/device-store-sql.d.ts +3 -0
  35. package/dist/plugins/device-store-sql.js +9 -4
  36. package/dist/plugins/ensure-index.d.ts +42 -0
  37. package/dist/plugins/ensure-index.js +81 -0
  38. package/dist/plugins/ensure-schema-lock-policy.d.ts +29 -0
  39. package/dist/plugins/ensure-schema-lock-policy.js +31 -0
  40. package/dist/plugins/image-bake-store-sql.d.ts +3 -0
  41. package/dist/plugins/image-bake-store-sql.js +6 -2
  42. package/dist/plugins/image-index-sql.d.ts +3 -0
  43. package/dist/plugins/image-index-sql.js +8 -4
  44. package/dist/plugins/leader-run-store-sql.d.ts +3 -0
  45. package/dist/plugins/leader-run-store-sql.js +6 -2
  46. package/dist/plugins/mailbox-store-sql.d.ts +3 -0
  47. package/dist/plugins/mailbox-store-sql.js +6 -1
  48. package/dist/plugins/memory-engine-pg.d.ts +4 -0
  49. package/dist/plugins/memory-engine-pg.js +6 -6
  50. package/dist/plugins/memory-engine-tidb.js +5 -0
  51. package/dist/plugins/memory-sync-store-pg.d.ts +5 -0
  52. package/dist/plugins/memory-sync-store-pg.js +9 -2
  53. package/dist/plugins/memory-sync-store-tidb.js +6 -0
  54. package/dist/plugins/one-time-migrations.d.ts +1 -1
  55. package/dist/plugins/one-time-migrations.js +7 -14
  56. package/dist/plugins/outcome-ledger-sql.d.ts +3 -0
  57. package/dist/plugins/outcome-ledger-sql.js +7 -3
  58. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  59. package/dist/plugins/permission-rule-store-sql.js +9 -5
  60. package/dist/plugins/pg-cost-quota.d.ts +3 -0
  61. package/dist/plugins/pg-cost-quota.js +5 -1
  62. package/dist/plugins/pg-pool.d.ts +23 -2
  63. package/dist/plugins/pg-pool.js +22 -37
  64. package/dist/plugins/pg-rate-limiter.d.ts +3 -0
  65. package/dist/plugins/pg-rate-limiter.js +5 -1
  66. package/dist/plugins/pg-session-storage.d.ts +3 -0
  67. package/dist/plugins/pg-session-storage.js +8 -4
  68. package/dist/plugins/remote-env-adb.d.ts +62 -3
  69. package/dist/plugins/remote-env-adb.js +27 -15
  70. package/dist/plugins/remote-env-host.js +1 -1
  71. package/dist/plugins/remote-shell.d.ts +7 -11
  72. package/dist/plugins/remote-shell.js +3 -3
  73. package/dist/plugins/retention-store-sql.d.ts +3 -0
  74. package/dist/plugins/retention-store-sql.js +7 -3
  75. package/dist/plugins/roster-store-sql.d.ts +3 -0
  76. package/dist/plugins/roster-store-sql.js +6 -1
  77. package/dist/plugins/schema-indexes.d.ts +21 -0
  78. package/dist/plugins/schema-indexes.js +25 -0
  79. package/dist/plugins/task-attachment-store.d.ts +3 -0
  80. package/dist/plugins/task-attachment-store.js +7 -2
  81. package/dist/plugins/tidb-pool.d.ts +15 -3
  82. package/dist/plugins/tidb-pool.js +38 -15
  83. package/dist/plugins/tool-result-store-sql.d.ts +3 -0
  84. package/dist/plugins/tool-result-store-sql.js +5 -1
  85. package/dist/plugins/web-search.d.ts +50 -2
  86. package/dist/plugins/web-search.js +9 -2
  87. package/dist/trace/project.d.ts +2 -1
  88. package/dist/trace/redact.d.ts +6 -0
  89. package/package.json +2 -2
@@ -4,7 +4,8 @@
4
4
  * The deterministic Coordinator integrates N completed workers' branches under FULL isolation:
5
5
  * 1. pull each worker's patch OUT of its container (M2 `pullDiff` — no creds go in).
6
6
  * 2. provision an EPHEMERAL no-net/no-creds integration sandbox (base pre-provisioned by the dep).
7
- * 3. `git apply --3way` each patch in sequence (partial-apply → capture `.rej`, quarantine — council#3).
7
+ * 3. `git apply --3way` each patch in sequence (conflict ⇒ TERMINAL: capture `.rej` + the conflict PATH list and
8
+ * hand them to the USER as {@link MergeConflict} — S-113 / clay [ref] ④; council#3's quarantine unchanged).
8
9
  * 4. run the full test suite IN the sandbox (the gate; untrusted diff never touches the control plane — [26]§16).
9
10
  * 5. green → the **Coordinator** (trusted control plane, holds creds — NOT the sandbox, council#1) takes the
10
11
  * integrated patch and pushes with CAS `--force-with-lease=<ref>:<baseSha>` (push race → retry — council#2).
@@ -57,31 +58,25 @@ export interface IntegrationRepair {
57
58
  }>;
58
59
  }
59
60
  /**
60
- * Bounded merge-conflict resolution (search 2026-06-16). Symmetric with {@link IntegrationRepair}: when a worker's
61
- * patch CONFLICTS on `git apply --3way` (overlapping edits a sibling already touched), run a bounded agent IN the
62
- * integration sandbox to integrate THIS worker's patch — instead of the leader discarding ALL fan-out work and
63
- * redoing the whole task as one fresh worker (collapseToSingle, leader.ts: "correctness over thrift; half-done
64
- * adoption is future"). The agent merges the in-tree conflict markers + applies the `.rej` hunks; mergeBranches
65
- * then VERIFIES no marker/.rej remains before continuing, and the downstream test gate still guards correctness.
66
- * Wired by the caller (provisionIntegrationSandbox); when unset, an apply conflict stays terminal exactly as before
67
- * (zero behaviour change). Reuses the IntegrationRepair sandbox + hands pattern → no new core/env seam (the seam
68
- * [ref] §8 feared for "adopting N half-done workspaces" is unnecessary for the conflict case).
61
+ * S-113 / clay [ref] ④ — **合不上 = 交给用户**(对齐 CC 形)。`git apply --3way` 冲突时的产出:冲突**路径**
62
+ * (不含内容)+ 哪几只 worker 的补丁已经进树,由 leader 铸成 `LeaderResult.conflict` 段交到用户手上。
63
+ * 本段刻意**只有路径与身份**:CC 259 语料对 collapse / conflict resolution 零命中,CC 的并行是同机 teammates
64
+ * 各 worktree、结束由 git 重放到用户 HEAD、冲突列给用户(cli259.js @2303113)——没有任何模型去读别的 worker
65
+ * 的 diff。7.83.0 删掉的那条「冲突解决模型」通道(它的类型面与 `LEADER_CONFLICT_ROUNDS` 旋钮)正是此意。
69
66
  */
70
- export interface ConflictResolver {
71
- /** Max resolution attempts before giving up (each = one agent run + one marker/.rej re-check). */
72
- maxRounds: number;
73
- /** Run the resolver agent against the live conflicted tree. Edits the working tree to integrate the worker's
74
- * patch + removes the `.rej`/`.orig` files; MUST NOT commit / push / add (the merge squashes at the end). */
75
- run(ctx: {
76
- round: number;
77
- workerId: string;
78
- conflictText: string;
79
- patch: string;
80
- repoDir: string;
81
- }): Promise<{
82
- costUsd?: number;
83
- note?: string;
84
- }>;
67
+ export interface MergeConflict {
68
+ /** 有冲突标记或 `.rej` 的路径(**不含内容**;`.rej` 后缀已剥,指向源文件)。 */
69
+ files: string[];
70
+ /** 对抗复审 F5 [medium]:清单**被截断**(命中 {@link CONFLICT_FILES_MAX})时在场 —— 缺席 = 这就是全部。
71
+ * 「完整清单」是本终局对用户的补偿,一次静默的 200 封顶会让用户以为冲突只有 200 处。 */
72
+ filesTruncated?: true;
73
+ /** 冲突发生**之前**已经干净 apply 进树的 workerId(顺序 apply,失败即停)。 */
74
+ applied: string[];
75
+ /** 补丁没能 apply 的那只 worker。 */
76
+ conflicted: string;
77
+ /** `.rej` 头(经既有**工件**脱敏面 `redactArtifactText` —— 与 `rej` 同一批 diff 字节同一口 —— 再切 ≤ 2 KiB)
78
+ * ——给用户看「冲突长什么样」,不是给模型的输入。 */
79
+ rejHead?: string;
85
80
  }
86
81
  export interface MergeDeps {
87
82
  /** Pull one worker's patch from ITS container (M2 pullDiff bound to the worker's env). */
@@ -93,7 +88,6 @@ export interface MergeDeps {
93
88
  env: IntegrationEnv;
94
89
  destroy: () => Promise<void>;
95
90
  repair?: IntegrationRepair;
96
- conflictResolver?: ConflictResolver;
97
91
  }>;
98
92
  /**
99
93
  * The CREDENTIALED push, done by the Coordinator (control plane), NOT the sandbox (council#1). Takes the
@@ -177,8 +171,6 @@ export type MergeResult = {
177
171
  reason?: string;
178
172
  };
179
173
  repair?: RepairMeta;
180
- conflictsResolved?: string[];
181
- conflictResolverCostUsd?: number;
182
174
  terminal?: RepairTerminal;
183
175
  candidatePatch?: string;
184
176
  } | {
@@ -187,10 +179,10 @@ export type MergeResult = {
187
179
  phase: "no-workers" | "diff-out" | "provision" | "apply" | "test" | "push";
188
180
  quarantine?: string[];
189
181
  rej?: string;
182
+ conflict?: MergeConflict;
190
183
  retry?: boolean;
191
184
  details?: string;
192
185
  repair?: RepairMeta;
193
- conflictResolverCostUsd?: number;
194
186
  terminal?: RepairTerminal;
195
187
  candidatePatch?: string;
196
188
  measure?: {
@@ -200,4 +192,21 @@ export type MergeResult = {
200
192
  };
201
193
  /** Integrate completed workers' branches. Never throws — failures are typed MergeResult. */
202
194
  export declare function mergeBranches(reports: WorkerReport[], baseSha: string, deps: MergeDeps): Promise<MergeResult>;
195
+ /** 探测脚本里某一半失败时打到 stdout 的标记前缀(`grep` / `find` 各一);只由 {@link conflictFilesOf} 认。
196
+ * ⚠️ 判序承重:`conflictFilesOf` **先**测本前缀、**后**剥 `./` —— `grep -rl` / `find .` 输出的真路径恒带 `./`,
197
+ * 所以一个真叫 `__PROBE_FAIL_grep` 的文件到达时是 `./__PROBE_FAIL_grep`,不与标记二义;把剥 `./` 挪到前面就静默破功。 */
198
+ export declare const PROBE_FAIL_PREFIX = "__PROBE_FAIL_";
199
+ /**
200
+ * S-113 / [ref] ④ — 把 `grep -rIl` + `find` 的输出变成**冲突路径清单**(给用户,不含内容)。
201
+ * 逐行:去掉 `./` 前缀、剥掉 `.rej` 后缀(`pkg/a.ts.rej` 与 `pkg/a.ts` 是同一处冲突的两种痕迹)、去重、
202
+ * 按 {@link CONFLICT_FILES_MAX} 封顶并**交回是否截断**(F5:静默封顶会让用户把 200 当成全部)。两条命令合成
203
+ * 一次 exec,所以 stdout 里两族路径混在一起 —— 本函数是它唯一的解释者。任一半探测失败 ⇒ 那一半打出
204
+ * {@link PROBE_FAIL_PREFIX} 标记、另一半的清单照常回来 ⇒ `probeFailed` 点名失败的那半、`files` 只是**不完整**而不是
205
+ * 「没有冲突」:`conflict` 段仍在场(用户仍拿得到分支与冲突者身份),调用点另记一行响亮日志(合并复审 车HB H-7)。
206
+ */
207
+ export declare function conflictFilesOf(stdout: string): {
208
+ files: string[];
209
+ truncated: boolean;
210
+ probeFailed: string[];
211
+ };
203
212
  //# sourceMappingURL=merge.d.ts.map
@@ -1,7 +1,9 @@
1
1
  import { delimitUntrusted } from "@sema-agent/core";
2
- import { OVERSIZED_DIAG_PLACEHOLDER, redactErrorMessage, redactHead, redactTail } from "../observability/run-terminal-log.js";
2
+ import { OVERSIZED_DIAG_PLACEHOLDER, redactArtifactText, redactErrorMessage, redactHead, redactTail } from "../observability/run-terminal-log.js";
3
3
  import { applyAndGrade, changedFilesOutOfAllowlist } from "./grader-env-factory.js";
4
4
  import { shellQuote as Q } from "../plugins/remote-shell.js";
5
+ const CONFLICT_FILES_MAX = 200;
6
+ const CONFLICT_REJ_HEAD_MAX = 2048;
5
7
  export async function mergeBranches(reports, baseSha, deps) {
6
8
  const repo = deps.repoDir ?? "/repo";
7
9
  const mergeable = reports.filter((r) => r.status === "completed");
@@ -10,7 +12,6 @@ export async function mergeBranches(reports, baseSha, deps) {
10
12
  }
11
13
  let destroy;
12
14
  let phase = "diff-out";
13
- let conflictResolverCostUsd = 0;
14
15
  let repairMeta;
15
16
  let measure;
16
17
  try {
@@ -28,23 +29,29 @@ export async function mergeBranches(reports, baseSha, deps) {
28
29
  const { env } = integ;
29
30
  const integBase = (await env.exec("git rev-parse HEAD", { cwd: repo })).stdout.trim();
30
31
  phase = "apply";
31
- const conflictsResolved = [];
32
+ const applied = [];
32
33
  for (const p of patches) {
33
34
  const patchPath = `/tmp/${p.workerId}.patch`;
34
35
  await env.writeFile(patchPath, p.patch);
35
36
  const ap = await env.exec(`git apply --3way --whitespace=nowarn ${Q(patchPath)}`, { cwd: repo });
36
37
  if (ap.exitCode !== 0) {
37
38
  const rej = await env.exec(`find . -name '*.rej' -print -exec cat {} +`, { cwd: repo }).catch(() => ({ stdout: "", stderr: "", exitCode: -1 }));
38
- const cr = integ.conflictResolver && integ.conflictResolver.maxRounds > 0
39
- ? await resolveApplyConflict(env, repo, p, rej.stdout, ap.stderr, integ.conflictResolver, deps)
40
- : { resolved: false, costUsd: 0 };
41
- conflictResolverCostUsd += cr.costUsd;
42
- if (!cr.resolved) {
43
- deps.logger?.warn?.("merge_apply_failed", { workerId: p.workerId, exitCode: ap.exitCode });
44
- return { ok: false, phase: "apply", reason: `git apply --3way failed for ${p.workerId} (conflict)`, quarantine: [p.workerId], rej: rej.stdout, details: redactHead(ap.stderr, 800), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
45
- }
46
- conflictsResolved.push(p.workerId);
39
+ const listing = await env
40
+ .exec(`grep -rIlE --exclude-dir=.git '^(<<<<<<<|>>>>>>>)' . 2>/dev/null; [ $? -le 1 ] || echo ${PROBE_FAIL_PREFIX}grep; ` +
41
+ `find . -path ./.git -prune -o -name '*.rej' -print 2>/dev/null || echo ${PROBE_FAIL_PREFIX}find`, { cwd: repo })
42
+ .catch(() => ({ exitCode: -1, stdout: "", stderr: "" }));
43
+ const { files, truncated, probeFailed } = conflictFilesOf(listing.stdout);
44
+ if (listing.exitCode !== 0 || probeFailed.length > 0)
45
+ deps.logger?.warn?.("merge_conflict_files_probe_failed", { workerId: p.workerId, exitCode: listing.exitCode, probeFailed });
46
+ deps.logger?.warn?.("merge_apply_failed", { workerId: p.workerId, exitCode: ap.exitCode, files: files.slice(0, 20) });
47
+ const rejHead = redactArtifactText(rej.stdout).slice(0, CONFLICT_REJ_HEAD_MAX);
48
+ return {
49
+ ok: false, phase: "apply", reason: `git apply --3way failed for ${p.workerId} (conflict)`,
50
+ quarantine: [p.workerId], rej: rej.stdout, details: redactHead(ap.stderr, 800),
51
+ conflict: { files, ...(truncated ? { filesTruncated: true } : {}), applied: [...applied], conflicted: p.workerId, ...(rejHead ? { rejHead } : {}) },
52
+ };
47
53
  }
54
+ applied.push(p.workerId);
48
55
  }
49
56
  phase = "test";
50
57
  let test = await env.exec(deps.testCmd, { cwd: repo });
@@ -74,7 +81,7 @@ export async function mergeBranches(reports, baseSha, deps) {
74
81
  deps.logger?.info?.("integration_repair_done", { ...repairMeta });
75
82
  }
76
83
  if (test.exitCode !== 0) {
77
- return { ok: false, phase: "test", reason: `integration test failed (exit ${test.exitCode})`, details: redactTail(test.stdout + "\n" + test.stderr, 2000), ...(repairMeta ? { repair: repairMeta } : {}), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
84
+ return { ok: false, phase: "test", reason: `integration test failed (exit ${test.exitCode})`, details: redactTail(test.stdout + "\n" + test.stderr, 2000), ...(repairMeta ? { repair: repairMeta } : {}) };
78
85
  }
79
86
  if (deps.measureCmd) {
80
87
  const mr = await env.exec(deps.measureCmd, { cwd: repo });
@@ -83,7 +90,7 @@ export async function mergeBranches(reports, baseSha, deps) {
83
90
  phase = "push";
84
91
  if (repairMeta)
85
92
  await env.exec(`git reset --soft ${Q(integBase)}`, { cwd: repo });
86
- const costTail = { ...(repairMeta ? { repair: repairMeta } : {}), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
93
+ const costTail = { ...(repairMeta ? { repair: repairMeta } : {}) };
87
94
  const commit = await env.exec(`git add -A && git -c user.email=leader@local -c user.name=leader commit -m ${Q(`merge: integrate ${mergeable.map((r) => r.workerId).join("+")}`)} --allow-empty`, { cwd: repo });
88
95
  if (commit.exitCode !== 0) {
89
96
  return { ok: false, phase: "push", reason: `failed to create integration commit (exit ${commit.exitCode})`, details: redactHead(commit.stdout + "\n" + commit.stderr, 800), ...costTail };
@@ -107,7 +114,6 @@ export async function mergeBranches(reports, baseSha, deps) {
107
114
  candidatePatch: h.candidatePatch,
108
115
  ...(measure ? { measure } : {}),
109
116
  ...(repairMeta ? { repair: repairMeta } : {}),
110
- ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}),
111
117
  };
112
118
  }
113
119
  const push = await deps.push(integrated.stdout, baseSha);
@@ -117,7 +123,7 @@ export async function mergeBranches(reports, baseSha, deps) {
117
123
  return { ok: false, phase: "push", reason: `push failed: ${push.error}`, ...costTail };
118
124
  }
119
125
  deps.logger?.info?.("merge_ok", { ref: push.ref, merged: mergeable.map((r) => r.workerId), ...(repairMeta ? { repairRounds: repairMeta.rounds } : {}) });
120
- return { ok: true, ref: push.ref, merged: mergeable.map((r) => r.workerId), ...(measure ? { measure } : {}), ...(repairMeta ? { repair: repairMeta } : {}), ...(conflictsResolved.length > 0 ? { conflictsResolved } : {}), ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}) };
126
+ return { ok: true, ref: push.ref, merged: mergeable.map((r) => r.workerId), ...(measure ? { measure } : {}), ...(repairMeta ? { repair: repairMeta } : {}) };
121
127
  }
122
128
  catch (e) {
123
129
  return {
@@ -125,7 +131,6 @@ export async function mergeBranches(reports, baseSha, deps) {
125
131
  quarantine: mergeable.map((r) => r.workerId),
126
132
  ...(measure ? { measure } : {}),
127
133
  ...(repairMeta ? { repair: repairMeta } : {}),
128
- ...(conflictResolverCostUsd > 0 ? { conflictResolverCostUsd } : {}),
129
134
  };
130
135
  }
131
136
  finally {
@@ -170,28 +175,30 @@ async function evaluatePushHold(mergeable, observeMeasure, integratedPatch, deps
170
175
  }
171
176
  return { measure };
172
177
  }
173
- async function resolveApplyConflict(env, repo, p, rejText, applyStderr, resolver, deps) {
174
- let costUsd = 0;
175
- const conflictText = `git apply --3way could not cleanly apply worker '${p.workerId}'.\n--- apply stderr ---\n${redactHead(applyStderr, 1500)}\n--- reject hunks (*.rej) ---\n${redactHead(rejText, 3000)}`;
176
- for (let round = 0; round < resolver.maxRounds; round++) {
177
- deps.logger?.warn?.("merge_conflict_repair_round", { workerId: p.workerId, round: round + 1, maxRounds: resolver.maxRounds });
178
- try {
179
- const r = await resolver.run({ round: round + 1, workerId: p.workerId, conflictText, patch: p.patch, repoDir: repo });
180
- costUsd += r.costUsd ?? 0;
181
- }
182
- catch (e) {
183
- deps.logger?.warn?.("merge_conflict_repair_error", { workerId: p.workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
184
- return { resolved: false, costUsd };
185
- }
186
- const check = await env
187
- .exec(`grep -rIlE '^(<<<<<<<|>>>>>>>)' . 2>/dev/null; find . -name '*.rej' 2>/dev/null | head -20`, { cwd: repo })
188
- .catch(() => ({ exitCode: -1, stdout: "DIRTY", stderr: "" }));
189
- if (!check.stdout.trim()) {
190
- await env.exec(`find . -name '*.orig' -delete 2>/dev/null || true`, { cwd: repo }).catch(() => { });
191
- deps.logger?.info?.("merge_conflict_resolved", { workerId: p.workerId, rounds: round + 1 });
192
- return { resolved: true, costUsd };
178
+ export const PROBE_FAIL_PREFIX = "__PROBE_FAIL_";
179
+ export function conflictFilesOf(stdout) {
180
+ const out = [];
181
+ const probeFailed = [];
182
+ const seen = new Set();
183
+ for (const raw of stdout.split("\n")) {
184
+ let f = raw.trim();
185
+ if (f === "")
186
+ continue;
187
+ if (f.startsWith(PROBE_FAIL_PREFIX)) {
188
+ probeFailed.push(f.slice(PROBE_FAIL_PREFIX.length));
189
+ continue;
193
190
  }
191
+ if (f.startsWith("./"))
192
+ f = f.slice(2);
193
+ if (f.endsWith(".rej"))
194
+ f = f.slice(0, -4);
195
+ if (f === "" || seen.has(f))
196
+ continue;
197
+ seen.add(f);
198
+ if (out.length >= CONFLICT_FILES_MAX)
199
+ return { files: out, truncated: true, probeFailed };
200
+ out.push(f);
194
201
  }
195
- return { resolved: false, costUsd };
202
+ return { files: out, truncated: false, probeFailed };
196
203
  }
197
204
  //# sourceMappingURL=merge.js.map
@@ -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) {