@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
@@ -22,7 +22,7 @@
22
22
  * 3. WORKER-ENV: the repair leg ALWAYS passes `workerEnv` so runRepairLoop's §5.1 isolation identity check is LIVE.
23
23
  * 4. PAIRED-SEED: `seed` is best-effort N-repeat replicate, NEVER a GO criterion; rows are distributional.
24
24
  * 5. COMPLETE TEAM COST: TEAM stats = Σ(worker stats.costBreakdown) + (merge.repair?.costUsd) +
25
- * (merge.conflictResolverCostUsd) — all three legs (the resolver cost was just un-dropped, merge.ts).
25
+ * 两条腿(7.83.0 起:冲突解决模型已随 [ref] ④ 删除,不再有第三条 resolver 腿)。
26
26
  *
27
27
  * 🔴 BLOCKED-but-honest structure: the REAL firm run is clay-gated (expensive E2B+DeepSeek). This file builds the
28
28
  * REAL arm logic against INJECTABLE profile seams (`ProfileDeps`) so the deterministic smoke drives it with a MOCK
@@ -143,9 +143,9 @@ export declare function classifyRunStatus(input: {
143
143
  */
144
144
  export declare function captureStats(stats: TaskResult["stats"] | undefined): EmittedStats;
145
145
  /**
146
- * 🔴 Corr #5 — the COMPLETE TEAM `stats`. Σ(worker stats.costBreakdown + humanReview) PLUS the merge legs:
147
- * `repair?.costUsd` and `conflictResolverCostUsd` (the resolver cost was just un-dropped in merge.ts). All three
148
- * legs, or TEAM C1 is undercounted ([ref]'s #1 confound). costUsd (float USD) → µUSD into llmRoot.
146
+ * 🔴 Corr #5 — the COMPLETE TEAM `stats`. Σ(worker stats.costBreakdown + humanReview) PLUS the merge leg
147
+ * `repair?.costUsd`. Both legs, or TEAM C1 is undercounted ([ref]'s #1 confound). costUsd (float USD) →
148
+ * µUSD into llmRoot. (7.83.0:第三条腿 `conflictResolverCostUsd` 随冲突解决模型删除,见下。)
149
149
  */
150
150
  export declare function captureTeamStats(reports: WorkerReport[], merge: MergeResult | undefined): EmittedStats;
151
151
  /**
@@ -60,8 +60,7 @@ export function captureTeamStats(reports, merge) {
60
60
  acc.humanReview.gates.push(...s.humanReview.gates);
61
61
  }
62
62
  const repairUsd = merge?.repair?.costUsd ?? 0;
63
- const resolverUsd = merge?.conflictResolverCostUsd ?? 0;
64
- acc.costBreakdown.llmRootMicroUsd += Math.round((repairUsd + resolverUsd) * 1e6);
63
+ acc.costBreakdown.llmRootMicroUsd += Math.round(repairUsd * 1e6);
65
64
  return acc;
66
65
  }
67
66
  export async function driveSupSuspendResume(initial, deps, implSpec, trap, advanceClock) {
@@ -362,7 +362,6 @@ async function runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId,
362
362
  const targetRef = "refs/heads/main";
363
363
  process.env.LEADER_ENABLED = "true";
364
364
  process.env.LEADER_WORKER_BUDGET_USD = String((rt.maxCostUsd ?? 1.0) / 2);
365
- process.env.LEADER_CONFLICT_ROUNDS = process.env.LEADER_CONFLICT_ROUNDS ?? "2";
366
365
  const run = createLeaderRunner({
367
366
  brain, models, roles, pricing, e2bApiKey: rt.e2bApiKey, workspace: W,
368
367
  ...(s1DebugGit() ? { logger: { warn: (m, x) => console.error(`[S1_DEBUG_LEADER warn] ${m}`, x ? JSON.stringify(x).slice(0, 300) : ""), info: (m, x) => console.error(`[S1_DEBUG_LEADER info] ${m}`, x ? JSON.stringify(x).slice(0, 200) : "") } } : {}),
@@ -8,9 +8,9 @@
8
8
  * `maxTokens`/`maxCostUsd` is a silently-unread dead key), and TEAM's per-worker budgets sum to ≤ the solo budget — so no
9
9
  * arm can win on a bigger pie. `assertBudgetMatch` (below) is the build-time guard the shape test runs.
10
10
  *
11
- * 🔴 BUDGET-MATCH for TEAM is NEW WIRING the live conflict test does NOT do: `leader-conflict-resolver-live.test.ts`
12
- * sets NO budget on `SubtaskSpec.spec` and NO `now` on any Runner. The harness STAMPS both (arms.ts) — copying the
13
- * live test verbatim would yield an unbounded TEAM (the confound) and an empty `humanReview` (C2 = undefined).
11
+ * 🔴 BUDGET-MATCH for TEAM is NEW WIRING no leader live test does: the (now-deleted, 7.83.0 / [ref] ④) conflict
12
+ * live test set NO budget on `SubtaskSpec.spec` and NO `now` on any Runner. The harness STAMPS both (arms.ts) —
13
+ * copying a live test verbatim would yield an unbounded TEAM (the confound) and an empty `humanReview` (C2 = undefined).
14
14
  *
15
15
  * The real-infra construction (E2B worker/grader envs + a live brain) is deferred to the live make-real proof
16
16
  * (`test/s1-value-live.test.ts`, gated on E2B+DEEPSEEK). This module only fixes the SHAPE + the budget invariant
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * 🔴 FIRM vs DIRECTIONAL (a HIGH review correction — do NOT reproduce the false "5 LIVE-VERIFIED" claim):
5
5
  * Only C1 / C2 / C4 have a real E2B+DeepSeek live make-real test in `test/` today
6
- * (supervisor-verify-fix-live / supervisor-honest-bound-live / leader-conflict-resolver-live).
6
+ * (supervisor-verify-fix-live / supervisor-honest-bound-live;C4 的那条 `leader-conflict-resolver-live`
7
+ * 已随 7.83.0 / clay [ref] ④ **删除** —— 它证的是「冲突解决模型合两边」,而那条通道不存在了;C4 今天
8
+ * 没有 live 证明位,按 DIRECTIONAL 计,不得再算作 FIRM).
7
9
  * C3 (honest-PARTIAL) has NO dedicated live test; C5 (safety-escalate) is ONLY a unit test
8
10
  * (`supervisor-safety-escalate.test.ts` header: "no E2B / no DeepSeek / no DB" — it only exercises
9
11
  * `createDurableAskPolicy` in-memory, NEVER a real boundary). Counting either as a measured datapoint is the
@@ -84,9 +86,11 @@ export declare const C1_VERIFY_FIX: TrapSpec;
84
86
  */
85
87
  export declare const C2_HONEST_BOUND: TrapSpec;
86
88
  /**
87
- * C4 — fan-out CONFLICT (two workers overlap one file → merge conflict → the bounded resolver keeps both).
88
- * FIRM (leader-conflict-resolver-live.test.ts). TEAM-primary. Objective COMMAND oracle = both functions exist
89
- * AND compute correctly.
89
+ * C4 — fan-out CONFLICT (two workers overlap one file → merge conflict).
90
+ * 🔴 7.83.0 / clay [ref] ④:合不上不再起解决模型,leader 把 `conflict` 段(分支 + 冲突文件)交给用户、
91
+ * run 状态 `needs_human` ⇒ 这一格的 TEAM 臂在冲突形上**不会自己合好**。原 FIRM 证明位
92
+ * (`leader-conflict-resolver-live` live 测试)随通道删除,C4 退回 DIRECTIONAL。TEAM-primary。
93
+ * Objective COMMAND oracle = both functions exist AND compute correctly.
90
94
  */
91
95
  export declare const C4_PARALLEL: TrapSpec;
92
96
  /**
@@ -184,7 +184,6 @@ export const CONFIG_CATALOG = [
184
184
  r("KUBERNETES_SERVICE_HOST", "orchestration", "string", "平台注入的 in-cluster k8s API host(K8S_API_URL 缺席时用)"),
185
185
  r("KUBERNETES_SERVICE_PORT", "orchestration", "string", "平台注入的 in-cluster k8s API 端口(缺省 443)"),
186
186
  r("LEADER_BUDGET_USD", "orchestration", "number", "leader replan-lite 扇出花费帽(USD)", { danger: BILL, derivedDefaultNote: "缺席 ⇒ replan-lite 自默认" }),
187
- r("LEADER_CONFLICT_ROUNDS", "orchestration", "number", "leader 合并冲突修复轮数", { danger: BILL, derivedDefaultNote: "缺席 ⇒ 场景配置 conflictRounds(默认 0)" }),
188
187
  r("LEADER_ENABLED", "orchestration", "boolean", "leader 端点(POST/GET /v1/leader;需隔离执行后端 + durable leader_run 店,缺则拒启)", { staticDefault: "false", configKey: "leaderEnabled" }),
189
188
  r("LEADER_FANOUT_ENABLED", "orchestration", "boolean", "leader 扇出许可(false=钉死单路由零 router 成本)", { staticDefault: "true", danger: BILL, configKey: "leaderFanoutEnabled" }),
190
189
  r("LEADER_MEASURE_GATES", "orchestration", "boolean", "leader 合并 push-hold-on-signal + measure-drives-repair 门(严格 'true' 才开)", { staticDefault: "false" }),
package/dist/config.js CHANGED
@@ -119,6 +119,7 @@ const RETIRED_ENVS = [
119
119
  ["OA_ISSUE_REPO", "the OA scenario was removed in 5.0.0", "removed"],
120
120
  ["OA_ISSUE_LABELS", "the OA scenario was removed in 5.0.0", "removed"],
121
121
  ["APPROVAL_POLL_MS", "the OA poll-approval lane was removed in 5.0.0; the durable checkpoint lane is the only gate", "removed"],
122
+ ["LEADER_CONFLICT_ROUNDS", "the bounded merge-conflict-resolution agent was deleted in 7.83.0 按 C-R63 删除,合不上交用户:leader 交回 result.conflict(分支 + 冲突文件),run 状态 needs_human", "removed"],
122
123
  ];
123
124
  function rejectRetiredEnvs() {
124
125
  const hit = RETIRED_ENVS.filter(([o]) => process.env[o] !== undefined && process.env[o] !== "");
@@ -3,6 +3,8 @@ import { redactErrorMessage, redactHead, redactLeaderResult, redactLeaderResultW
3
3
  import { withPrincipal } from "../observability/principal-context.js";
4
4
  import { leaderIdemKey } from "../plugins/leader-run-store-sql.js";
5
5
  function statusForLeaderResult(result) {
6
+ if (result.conflict)
7
+ return "needs_human";
6
8
  switch (result.repairTerminal) {
7
9
  case "candidate_only":
8
10
  case "needs_human_oracle":
@@ -104,10 +104,13 @@ export interface LeaderDeps {
104
104
  /** Replan-lite ([ref] §6): bounded, DETERMINISTIC reactions — no runtime DAG surgery. When set:
105
105
  * worker failed → redispatch ×1 on a FRESH env (clean base; a crashed worker's half-written workspace is
106
106
  * never reused) → still failed = collapse-remainder-to-single; verify FAIL (repair already bounded inside
107
- * the verify loop) → collapse; structural merge conflict → collapse + record; budgetUsd exceeded → stop
108
- * fan-out, single continues. Multi-trigger arbitration priority (C6): suspended > budget > merge-conflict >
109
- * verify-fail > worker-failed — only the highest-priority reaction runs. (The suspended reaction itself —
110
- * cancel the others, exempt the suspended worker, surface its token — is unconditional, not gated here.) */
107
+ * the verify loop) → collapse; budgetUsd exceeded → stop fan-out, single continues. Multi-trigger
108
+ * arbitration priority (C6): suspended > budget > verify-fail > worker-failed — only the highest-priority
109
+ * reaction runs. (The suspended reaction itself — cancel the others, exempt the suspended worker, surface
110
+ * its token — is unconditional, not gated here.)
111
+ *
112
+ * 🔴 S-113 / clay [ref] ④(7.83.0):**merge-conflict 不再是一条 replan 臂**。worker 分支合不上 ⇒ 不起
113
+ * collapse 模型,`conflict` 段(分支 + 冲突文件)交给**用户**,见 {@link LeaderResult.conflict}。 */
111
114
  replan?: {
112
115
  /** Fan-out budget (USD, sum of worker costs). Exceeded → stop fan-out, collapse to single (C5). */
113
116
  budgetUsd?: number;
@@ -154,12 +157,36 @@ export interface LeaderResult {
154
157
  route?: "single" | "fanout";
155
158
  /** Set when planning/provisioning failed before fan-out. */
156
159
  error?: string;
157
- /** Replan-lite ([ref] §6): which trigger won arbitration + what was done. */
160
+ /** Replan-lite ([ref] §6): which trigger won arbitration + what was done.
161
+ * 🔴 S-113 / [ref] ④(7.83.0):闭集删掉 `"merge-conflict"`(硬 breaking,零别名——消费方的 tsc 红即通知)。 */
158
162
  replan?: {
159
- trigger: "suspended" | "budget" | "merge-conflict" | "verify-fail" | "worker-failed";
163
+ trigger: "suspended" | "budget" | "verify-fail" | "worker-failed";
160
164
  redispatched?: string[];
161
165
  collapsed?: string;
162
166
  };
167
+ /**
168
+ * 🔴 S-113 / clay [ref] ④(7.83.0)——**合不上 = 交给用户**(对齐 CC 形)。merge 的 `phase:"apply"` 冲突
169
+ * 不再起 collapse 模型:`ok=false` + 本段,用户拿 `workers[].branch` + `files` 自己 `git merge` / cherry-pick
170
+ * (worker 分支本就留在集成沙箱 / 远端)。run 状态走既有第三终局 `needs_human`(endpoint 的状态映射),不加
171
+ * 新状态词。**缺席何义** = 这一趟没有冲突,或对面是老 server。
172
+ */
173
+ conflict?: {
174
+ /** 用户侧的合并基点 = leader 这一趟的 durable base。 */
175
+ baseSha: string;
176
+ /** 有冲突标记 / `.rej` 的路径(**不含内容**)。 */
177
+ files: string[];
178
+ /** 清单被截断(探测到的冲突路径多于上限)时在场;**缺席 = 这就是全部**(对抗复审 F5)。 */
179
+ filesTruncated?: true;
180
+ /** 全部 completed worker 的分支;`applied:false` = 这只 worker 的补丁没进树(冲突者本人,或排在它后面
181
+ * 还没轮到 apply 的——顺序 apply,首次失败即停)。 */
182
+ workers: Array<{
183
+ workerId: string;
184
+ branch: string;
185
+ applied: boolean;
186
+ }>;
187
+ /** `.rej` 头(经既有脱敏面,≤ 2 KiB)——给人看冲突长什么样。 */
188
+ rejHead?: string;
189
+ };
163
190
  /** C4: suspended workers' resume tokens, surfaced to the caller (their checkpoints + paused envs are intact —
164
191
  * the single-agent resume path owns them from here; orphan protection = the existing deadline reap). */
165
192
  suspended?: Array<{
@@ -1,8 +1,8 @@
1
1
  import { fanOut } from "./fanout.js";
2
- import { redactErrorMessage, redactHead, redactPatchHead } from "../observability/run-terminal-log.js";
2
+ import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
3
3
  import { pullDiff } from "./diffout.js";
4
4
  import { mergeBranches } from "./merge.js";
5
- import { Runner, runWithVerification, resumeWithVerification, delimitUntrusted, terminalProjection } from "@sema-agent/core";
5
+ import { Runner, runWithVerification, resumeWithVerification, terminalProjection } from "@sema-agent/core";
6
6
  import { pausedCauseOf } from "../terminal.js";
7
7
  const MAX_RESUME_SLICES = 200;
8
8
  export async function autoResumeOnResource(initial, signal, resume, maxSlices = MAX_RESUME_SLICES) {
@@ -152,20 +152,25 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
152
152
  : w.diffEnv
153
153
  ? pullDiff(w.diffEnv, w.baseSha, { ref: w.branch, ...(workerRepoDir ? { cwd: workerRepoDir } : {}) })
154
154
  : Promise.resolve({ ok: false, error: `worker ${w.workerId} has neither fetchDiff nor diffEnv` });
155
- const salvageNonCompleted = async (rs) => {
155
+ const notCompleted = (r) => r.status !== "completed";
156
+ const salvagePatches = async (rs) => {
156
157
  const out = [];
158
+ const skipped = (workerId, why, detail) => deps.logger?.warn?.("salvage_skipped", { workerId, why, ...(detail ? { detail: redactHead(detail, 200) } : {}) });
157
159
  await Promise.all(rs
158
- .filter((r) => r.status !== "completed")
159
160
  .map(async (r) => {
160
161
  const w = byId.get(r.workerId);
161
162
  if (!w)
162
- return;
163
+ return skipped(r.workerId, "unknown_worker");
163
164
  try {
164
165
  const d = await workerDiff(w);
165
- if (d.ok && d.patch.trim().length > 0)
166
- out.push({ workerId: r.workerId, sessionId: r.sessionId, patch: d.patch });
166
+ if (!d.ok)
167
+ return skipped(r.workerId, "diff_failed", d.error);
168
+ if (d.patch.trim().length === 0)
169
+ return skipped(r.workerId, "empty_diff");
170
+ out.push({ workerId: r.workerId, sessionId: r.sessionId, patch: d.patch });
167
171
  }
168
- catch {
172
+ catch (e) {
173
+ skipped(r.workerId, "diff_threw", e instanceof Error ? e.message : String(e));
169
174
  }
170
175
  }));
171
176
  return out;
@@ -226,7 +231,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
226
231
  if (sus.length === 0)
227
232
  return undefined;
228
233
  deps.logger?.info?.("replan_suspended", { workers: sus.map((r) => r.workerId) });
229
- const salvaged = await salvageNonCompleted(rs);
234
+ const salvaged = await salvagePatches(rs.filter(notCompleted));
230
235
  return {
231
236
  ok: false, reports: rs, cancelled: true, route,
232
237
  error: "worker suspended — fan-out cancelled (suspended worker exempt), resume token surfaced",
@@ -235,41 +240,26 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
235
240
  ...(salvaged.length > 0 ? { salvaged } : {}),
236
241
  };
237
242
  };
238
- const collapseToSingle = async (trigger, reason, priorMerge) => {
243
+ const conflictSegment = (c, rs) => ({
244
+ baseSha: durableBaseSha,
245
+ files: c.files,
246
+ ...(c.filesTruncated ? { filesTruncated: true } : {}),
247
+ workers: rs
248
+ .filter((r) => r.status === "completed")
249
+ .map((r) => ({ workerId: r.workerId, branch: byId.get(r.workerId).branch, applied: c.applied.includes(r.workerId) })),
250
+ ...(c.rejHead ? { rejHead: c.rejHead } : {}),
251
+ });
252
+ const collapseToSingle = async (trigger, reason) => {
239
253
  const safeReason = redactHead(reason, 300);
240
254
  deps.logger?.warn?.("replan_collapse_to_single", { trigger, reason: safeReason });
241
255
  const replan = { trigger, collapsed: safeReason, ...(redispatched ? { redispatched } : {}) };
242
- const priorDiffs = [];
243
- await Promise.all(reports.map(async (r) => {
244
- const w = byId.get(r.workerId);
245
- if (!w)
246
- return;
247
- try {
248
- const d = await workerDiff(w);
249
- if (d.ok && d.patch.trim().length > 0)
250
- priorDiffs.push({ workerId: r.workerId, status: r.status, patch: d.patch });
251
- }
252
- catch {
253
- }
254
- }));
255
- if (priorDiffs.length > 0)
256
- deps.logger?.info?.("collapse_reuse_priors", { workers: priorDiffs.map((d) => d.workerId) });
257
- const soloObjective = priorDiffs.length === 0
258
- ? task
259
- : [
260
- task,
261
- ``,
262
- `--- PRIOR PARALLEL WORK — REUSE IT, do not re-derive from scratch ---`,
263
- `${priorDiffs.length} worker(s) attempted this task in parallel; their changes could NOT be integrated cleanly (${safeReason.slice(0, 150)}). Each worker's diff over the base is below. COPY the correct, non-overlapping parts verbatim and produce ONE coherent change for the WHOLE task, resolving only the overlaps. Treat these as a strong starting point — fix anything wrong, but do not throw the work away.`,
264
- ...priorDiffs.map((d) => delimitUntrusted(`PRIOR WORKER '${d.workerId}' (${d.status}) DIFF (untrusted)`, redactPatchHead(d.patch, 8000))),
265
- ].join("\n");
266
- const soloSub = { workerId: "collapse-solo", branch: "worker/collapse-solo", files: ["."], spec: { objective: soloObjective } };
256
+ const soloSub = { workerId: "collapse-solo", branch: "worker/collapse-solo", files: ["."], spec: { objective: task } };
267
257
  let solo;
268
258
  try {
269
259
  solo = await deps.provisionWorker(soloSub);
270
260
  }
271
261
  catch (e) {
272
- const salvaged = await salvageNonCompleted(finalReports);
262
+ const salvaged = await salvagePatches(finalReports.filter(notCompleted));
273
263
  return { ok: false, reports: finalReports, cancelled, route, error: `collapse provision failed: ${e instanceof Error ? e.message : String(e)}`, replan, ...(salvaged.length > 0 ? { salvaged } : {}) };
274
264
  }
275
265
  workers.push(solo);
@@ -280,10 +270,20 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
280
270
  const sus = await surfaceSuspended(finalReports, fo.reports);
281
271
  if (sus)
282
272
  return { ...sus, replan: { ...replan, trigger: "suspended" }, ...(repairTerminal ? { repairTerminal } : {}), ...(candidatePatch ? { candidatePatch } : {}) };
283
- const mRaw = await mergeBranches(fo.reports, durableBaseSha, mergeDeps);
284
- const priorCost = priorMerge?.conflictResolverCostUsd ?? 0;
285
- const m = priorCost > 0 ? { ...mRaw, conflictResolverCostUsd: (mRaw.conflictResolverCostUsd ?? 0) + priorCost } : mRaw;
286
- const salvaged = await salvageNonCompleted(finalReports);
273
+ const m = await mergeBranches(fo.reports, durableBaseSha, mergeDeps);
274
+ if (!m.ok && m.conflict) {
275
+ const cc = m.conflict;
276
+ deps.logger?.warn?.("merge_conflict_to_user", { conflicted: cc.conflicted, applied: cc.applied, files: cc.files.slice(0, 20) });
277
+ const salvagedAll = await salvagePatches(finalReports);
278
+ return {
279
+ ok: false, reports: finalReports, merge: m, cancelled: fo.cancelled, route, replan,
280
+ conflict: conflictSegment(cc, fo.reports),
281
+ ...(salvagedAll.length > 0 ? { salvaged: salvagedAll } : {}),
282
+ ...(repairTerminal ? { repairTerminal } : {}),
283
+ ...(candidatePatch ? { candidatePatch } : {}),
284
+ };
285
+ }
286
+ const salvaged = await salvagePatches(finalReports.filter(notCompleted));
287
287
  return {
288
288
  ok: m.ok, reports: finalReports, merge: m, cancelled: fo.cancelled, route, replan,
289
289
  ...(salvaged.length > 0 ? { salvaged } : {}),
@@ -336,18 +336,27 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
336
336
  const sus2 = await surfaceSuspended(reports, fo2.reports);
337
337
  if (sus2)
338
338
  return sus2;
339
- const stillFailed = fo2.reports.filter((r) => r.status !== "completed").map((r) => r.workerId);
339
+ const stillFailed = fo2.reports.filter(notCompleted).map((r) => r.workerId);
340
340
  if (stillFailed.length > 0) {
341
341
  return collapseToSingle("worker-failed", `redispatch failed again: ${stillFailed.join(", ")}`);
342
342
  }
343
343
  }
344
344
  }
345
345
  const merge = await mergeBranches(reports, durableBaseSha, mergeDeps);
346
- if (!merge.ok && merge.phase === "apply" && deps.replan) {
347
- deps.logger?.warn?.("replan_merge_conflict", { reason: merge.reason, quarantine: merge.quarantine ?? [] });
348
- return collapseToSingle("merge-conflict", merge.reason, merge);
346
+ if (!merge.ok && merge.conflict) {
347
+ const c = merge.conflict;
348
+ deps.logger?.warn?.("merge_conflict_to_user", { conflicted: c.conflicted, applied: c.applied, files: c.files.slice(0, 20) });
349
+ const salvagedC = await salvagePatches(reports);
350
+ return {
351
+ ok: false, reports, merge, cancelled, route,
352
+ conflict: conflictSegment(c, reports),
353
+ ...(redispatched ? { replan: { trigger: "worker-failed", redispatched } } : {}),
354
+ ...(salvagedC.length > 0 ? { salvaged: salvagedC } : {}),
355
+ ...(repairTerminal ? { repairTerminal } : {}),
356
+ ...(candidatePatch ? { candidatePatch } : {}),
357
+ };
349
358
  }
350
- const salvaged = await salvageNonCompleted(reports);
359
+ const salvaged = await salvagePatches(reports.filter(notCompleted));
351
360
  return {
352
361
  ok: merge.ok, reports, merge, cancelled, route,
353
362
  ...(redispatched ? { replan: { trigger: "worker-failed", redispatched } } : {}),
@@ -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