@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
package/USAGE.md CHANGED
@@ -93,12 +93,12 @@ WEB_SEARCH_PROBE_ON_BOOT=true # boot 期一次性真出网探活,
93
93
 
94
94
  | env 键 | 类型 | 默认 | 缺席行为 | 坏值行为 |
95
95
  |---|---|---|---|---|
96
- | `WEB_SEARCH_PROVIDER` | 闭集(`brave`\|`tavily`\|`searxng`) | 无 | WebSearch 工具整体不装配(功能缺席,无 warn) | 三词之外的任何值 = 同缺席处理,不装配、不 warn(`:296-297`) |
97
- | `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`:152`/`:164`);searxng:本键无消费点 | 空字符串同缺席(`env.WEB_SEARCH_API_KEY ?` 只认真值,`:303`) |
98
- | `WEB_SEARCH_ENDPOINT` | url string | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(`:193`) | 不做 URL 形校验——写不成 URL 由 `new URL()` 抛出,同样落到"首次调用才现形"那条路径,不拒启 |
99
- | `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 非数字/≤0 → 回落默认 10;是数字则向下取整;最终值再夹在 `[1,20]`(设 999 也被 clamp 到 20,`:227`) |
100
- | `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 非数字/≤0 → 回落默认;最终值下限夹到 1000ms(`Math.max(1000,…)`,`:228`) |
101
- | `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 一个 `k=v` 对都解析不出(没有 `=`,或 `k`/`v` 任一为空)→ 同缺席,整串忽略,不拒启不 warn(`:260-277`);env 侧值恒为字符串,不会触发下面 per-request 那个"数组被误当对象吸收"的边角(见 §9.5) |
96
+ | `WEB_SEARCH_PROVIDER` | 闭集(`brave`\|`tavily`\|`searxng`) | 无 | WebSearch 工具整体不装配(功能缺席,无 warn) | 三词之外的任何值 = 同缺席处理,不装配、不 warn(`webSearchConfigFromEnv` 的词表守卫 `isWebSearchProvider`)。**读面(S-382 起)**:`GET /v1/capabilities` 的 `webSearch.backend` 把这一格的生效值广告成闭集词(缺席/坏值都报 `"none"` —— 两者行为本就相同);端点/密钥/配额**不上 wire**,那些仍只在 operator 面 `GET /v1/config/catalog` |
97
+ | `WEB_SEARCH_API_KEY` | secret string | 无 | brave/tavily:后端仍会装配(装配只看 `PROVIDER`),**首次真实工具调用**时抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`(`braveSearch` / `tavilySearch` 的首行守卫);searxng:本键无消费点 | 空字符串同缺席(`env.WEB_SEARCH_API_KEY ?` 只认真值,`webSearchConfigFromEnv` 的条件展开) |
98
+ | `WEB_SEARCH_ENDPOINT` | url string | brave/tavily → 各自官方 API;searxng → 无默认 | brave/tavily:落官方 endpoint;searxng:**首次真实工具调用**时抛 `WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider`(`searxngSearch` 的首行守卫) | 不做 URL 形校验——写不成 URL 由 `new URL()` 抛出,同样落到"首次调用才现形"那条路径,不拒启 |
99
+ | `WEB_SEARCH_MAX_RESULTS` | number | `10` | 用默认 10 | 非数字/≤0 → 回落默认 10;是数字则向下取整;最终值再夹在 `[1,20]`(设 999 也被 clamp 到 20 —— `createWebSearchBackend` 里的 `maxResults` clamp) |
100
+ | `WEB_SEARCH_TIMEOUT_MS` | number(ms) | `10000` | 用默认 10000 | 非数字/≤0 → 回落默认;最终值下限夹到 1000ms(`createWebSearchBackend` 里的 `Math.max(1000, …)`) |
101
+ | `WEB_SEARCH_SEARXNG_PARAMS` | string(`k=v;k=v`) | 无(不铸键 = 不传 `extraParams`,交给 core adapter 自己的缺省) | 键整个不铸 | 一个 `k=v` 对都解析不出(没有 `=`,或 `k`/`v` 任一为空)→ 同缺席,整串忽略,不拒启不 warn(`parseSearxngParams`);env 侧值恒为字符串,不会触发下面 per-request 那个"数组被误当对象吸收"的边角(见 §9.5) |
102
102
  | `WEB_SEARCH_PROBE_ON_BOOT` | boolean(仅认 `"true"`/`"1"`) | `false`(OFF) | 不探活 | trim+小写后不等于 `"true"`/`"1"` 的任何拼法(含 `"yes"`/`"on"`)一律当 `false`;探活失败(网络不通/后端拒绝)只 `warn`(`web_search_probe_failed`),**不拒启**——功能型能力缺席走降级,不是保护型旋钮(`main.ts:1028-1034`) |
103
103
 
104
104
  **结论:七键无一在 boot 期拒启。** 与本仓其它安全轴旋钮(`MODEL_DEGRADE_ON`、思考档三键等,拼错即拒启)
@@ -1542,6 +1542,9 @@ const webSearch = reqWebSearch ? createWebSearchBackend(reqWebSearch) : deps.web
1542
1542
  TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结构性够不着**——不是被拒、也不留任何
1543
1543
  `warn`/`capabilities` 位说"你发的 webSearch 被忽略了"(对比 `mcpServers` 有 `capabilities.mcpInjection` +
1544
1544
  `mcp_injection_dropped` 日志可读;`settings.webSearch` 没有对应的能力探测位,`src/http/wire-types.ts:320-323`)。
1545
+ 🔴 **别把 S-382 的新位读成这个探测位**:`GET /v1/capabilities` 的 `webSearch.backend`(S-382)说的是
1546
+ **部署默认后端是哪一只**,**不是**「我发的 per-request `settings.webSearch` 会不会被采纳」——后者在多租户上
1547
+ 依旧是结构性够不着、无 warn、无位,这一段的结论一字未变。
1545
1548
  原因是安全边界(与多租户能力配置隔离规则同源):per-request `endpoint`/`apiKey` 是能力配置,多租户下一个租户把 `searxng`
1546
1549
  指向内网地址就是 SSRF,所以多租户上只认部署 env 配的 backend。
1547
1550
 
@@ -1550,11 +1553,11 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
1550
1553
 
1551
1554
  | 键 | 类型 | 必填 | 默认 | 消费点 |
1552
1555
  |---|---|---|---|---|
1553
- | `provider` | `"brave"|"tavily"|"searxng"` | 是(缺席/非三词之一 ⇒ 整个 `settings.webSearch` 被丢,回落到 env 的 backend,**不报错**) | 无 | `:321-322` |
1554
- | `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) | `:326` |
1555
- | `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) | `:327` |
1556
- | `searxngParams` | `Record<string,string>`(对象)或 `"k=v;k=v"`(字符串) | 否 | 无 | `:330-333`,解析逻辑与 env 腿共用 `parseSearxngParams`(`:260-277`)。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
1557
- | `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整) | `:323` |
1556
+ | `provider` | `"brave"|"tavily"|"searxng"` | 是(缺席/非三词之一 ⇒ 整个 `settings.webSearch` 被丢,回落到 env 的 backend,**不报错**) | 无 | `webSearchConfigFromSettings` 的词表守卫(与 env 腿**同一只** `isWebSearchProvider`,词表属主 = `WEB_SEARCH_PROVIDERS`) |
1557
+ | `apiKey` | string(明文) | brave/tavily 建议带(缺了首次调用才报错,见下表);searxng 不读 | 无(不继承 env 的 `WEB_SEARCH_API_KEY`) | `webSearchConfigFromSettings` 的 `apiKey` 条件展开 |
1558
+ | `endpoint` | string | searxng 必需(缺了首次调用才报错);brave/tavily 可选覆盖 | 无(不继承 env 的 `WEB_SEARCH_ENDPOINT`) | `webSearchConfigFromSettings` 的 `endpoint` 条件展开 |
1559
+ | `searxngParams` | `Record<string,string>`(对象)或 `"k=v;k=v"`(字符串) | 否 | 无 | `webSearchConfigFromSettings` 的 `searxngParams` 臂,解析逻辑与 env 腿共用 `parseSearxngParams`。⚠️ **未列入已发布的 `@sema-agent/sdk` 8.8.0 `SettingsWebSearch` 类型**(该接口只有 `provider`/`apiKey`/`endpoint`/`maxResults` 四键、无开放下标)——server 侧代码认这个键,但当前发布的 TS 类型接不到它;手写 JSON 请求体仍可以发,server 会照常解析(源码头注自述:字段和消费点先落地,配置录入面尚未跟上,是一笔尚未还清的既有债务) |
1560
+ | `maxResults` | number | 否 | 10(与 env 同一 clamp,`[1,20]`,小数向下取整) | `webSearchConfigFromSettings` 的 `maxResults` 归一 |
1558
1561
  | *(无)* `timeoutMs` | — | — | — | **per-request 车道没有这个字段**——即便部署用 `WEB_SEARCH_TIMEOUT_MS` 配了非默认超时,per-request 生效那次调用永远退回 backend 自己的默认(10000ms,下限 1000ms),因为"整段整取"意味着 env 的 `timeoutMs` 根本不在 `reqWebSearch` 那个新对象里 |
1559
1562
  | *(无)* `fetchImpl` | — | — | — | 同上,仅测试注入用,per-request 车道不可达 |
1560
1563
 
@@ -1575,7 +1578,7 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
1575
1578
  | 缺 `apiKey`(brave/tavily,env 或 per-request 均未给) | 首次调用抛 `WEB_SEARCH_API_KEY is required for the <provider> provider`,core 接住转成 `Error (WebSearch): the search backend failed. …` 文本回模型 | `"unknown"`(消息里没有 HTTP 状态码模式,`classifySearchFailure` 落最后一条默认分支) |
1576
1579
  | `searxngParams` 非对象/非字符串(如数字、布尔、`null`) | 静默丢弃——`parseSearxngParams` 落到"非 object 且非 string"分支,返回 `undefined`,键整个不铸,**不报错、不 warn** | 不适用 |
1577
1580
  | ⚠️ `searxngParams` 是**数组**(如 `["engines=bing"]`) | `typeof [] === "object"` 让它落进对象分支:`Object.entries` 按数组下标产出 `{"0":"engines=bing"}`——**不被拒绝**,但产出的键是数字字符串、值是未拆分的原始 `"k=v"` 串,传给 core adapter 的 `extraParams` 后是一组没有意义的查询参数(不是解析出 `engines=bing`)。这条边角只在 per-request 车道可达(env 值恒为字符串,不会触发);已用与源码逐字一致的独立复现脚本核验(见收车档),未改代码 | 不适用(不是异常路径) |
1578
- | 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | 三个 provider 各自的 `!res.ok` 分支抛错,消息形固定为 `"<provider> search failed (<status>): <body首 200 字符>"`(brave/tavily,`:158`/`:174`)或 `"SearXNG <status> <statusText> from <url>"`(searxng,core adapter) | ⚠️ **实测几乎恒为 `"unknown"`,不是按状态码分档**:`classifySearchFailure` 的状态码分支要求 `http`/`status`/`code`/`error` 四词之一紧邻数字前(`\D{0,12}`内),但本仓三个 adapter 的消息把状态码写在 `failed (…)`/`SearXNG …` 之后,不触发该分支;独立复现脚本核验(见收车档)brave/tavily/searxng 的 429/500/403/403 全部落到最后一条默认分支 `retryable:"unknown"`。**真正被分类对的只有两条兜底正则**:上游错误体文本里若真含 `"rate limit"` 才判 429 类 `true`,含 `"timed out"`/`"timeout"` 才判 408 类 `true`——都取决于上游返回的具体措辞,不是本仓能保证的 |
1581
+ | 搜索后端返回**非 2xx HTTP 响应**(鉴权失败/限流/服务端 5xx 等) | 三个 provider 各自的 `!res.ok` 分支抛错,消息形固定为 `"<provider> search failed (<status>): <body首 200 字符>"`(brave/tavily,`braveSearch` / `tavilySearch` 的 `!res.ok` 分支)或 `"SearXNG <status> <statusText> from <url>"`(searxng,core adapter) | ⚠️ **实测几乎恒为 `"unknown"`,不是按状态码分档**:`classifySearchFailure` 的状态码分支要求 `http`/`status`/`code`/`error` 四词之一紧邻数字前(`\D{0,12}`内),但本仓三个 adapter 的消息把状态码写在 `failed (…)`/`SearXNG …` 之后,不触发该分支;独立复现脚本核验(见收车档)brave/tavily/searxng 的 429/500/403/403 全部落到最后一条默认分支 `retryable:"unknown"`。**真正被分类对的只有两条兜底正则**:上游错误体文本里若真含 `"rate limit"` 才判 429 类 `true`,含 `"timed out"`/`"timeout"` 才判 408 类 `true`——都取决于上游返回的具体措辞,不是本仓能保证的 |
1579
1582
  | `endpoint` **网络层**不可达(连接被拒/DNS 解析失败/fetch 自身抛错,尚未拿到任何 HTTP 响应) | `fetch` 抛出的传输层错误(`ECONNREFUSED`/`ENOTFOUND`/`fetch failed` 等 Node/undici 标准措辞)被同一 `catch` 接住 | `true`——这条路径的错误文本天然含 `econnrefused`/`enotfound`/`fetch failed`/`dns` 等词,`classifySearchFailure` 的网络故障正则能命中(与上一行"已拿到 HTTP 响应但非 2xx"是两条不同的失败路径,别混淆) |
1580
1583
 
1581
1584
  ⚠️ **"坏 provider 静默回落"这条对调用方是否可观察,取决于部署 env 有没有配 backend**:上表第一行说
@@ -1588,8 +1591,8 @@ TOC 本地形)时被读取;`REQUIRE_PRINCIPAL=true`(多租户)上这个键**结
1588
1591
  见收车档)。
1589
1592
 
1590
1593
  **`apiKey` 明文与 env 槽边界**:server 收到的 `settings.webSearch.apiKey` 是**明文字符串**,没有任何服务端
1591
- 密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(`:326`,与 `WEB_SEARCH_API_KEY` 同一条消费路径,
1592
- `src/plugins/web-search.ts:63-64`)。**server 本批不改受理面**:明文字段的形状维持原样。调用方(壳)如何在
1594
+ 密钥槽位/引用间接——收到什么字符串就直接进 backend 闭包(`webSearchConfigFromSettings` 的 `apiKey` 条件展开,与 `WEB_SEARCH_API_KEY` 同一条消费路径,
1595
+ 见 `src/plugins/web-search.ts` 的 `WebSearchBackendConfig.apiKey` 字段注)。**server 本批不改受理面**:明文字段的形状维持原样。调用方(壳)如何在
1593
1596
  自己机器上管理这份明文是调用方的事——例如 cli 壳侧的约定是在**调用方自己的环境**里按
1594
1597
  `SEMA_WEBSEARCH_KEY_<PROVIDER>` 这样的命名空间存放每个 provider 的 key,由壳在本地读出后把明文塞进请求体;
1595
1598
  这纯粹是**客户端约定**,本仓不读取、不校验、也不感知任何这类客户端侧 env 命名(这条命名事实来自任务书,
@@ -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
  /**
@@ -24,6 +24,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
24
24
  maxTurns: number | undefined;
25
25
  };
26
26
  taskTimeoutSec: number;
27
+ webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
27
28
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
28
29
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
29
30
  checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
@@ -158,6 +158,6 @@ export async function runStage07(ctx) {
158
158
  const builtinScenarioNames = Object.keys(scenarioDetails);
159
159
  configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
160
160
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
161
- return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveTool, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, singleUserAutoAcceptBaseline, taskLimitCaps, taskTimeoutSec };
161
+ return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveTool, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, singleUserAutoAcceptBaseline, taskLimitCaps, taskTimeoutSec, webSearchProvider: webSearchCfg?.provider };
162
162
  }
163
163
  //# sourceMappingURL=stage-07-capability-layer.js.map
@@ -37,6 +37,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
37
37
  maxTurns: number | undefined;
38
38
  };
39
39
  taskTimeoutSec: number;
40
+ webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
40
41
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
41
42
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
42
43
  checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
@@ -71,6 +71,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
71
71
  maxTurns: number | undefined;
72
72
  };
73
73
  taskTimeoutSec: number;
74
+ webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
74
75
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
75
76
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
76
77
  checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
@@ -71,6 +71,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
71
71
  maxTurns: number | undefined;
72
72
  };
73
73
  taskTimeoutSec: number;
74
+ webSearchProvider: "brave" | "tavily" | "searxng" | undefined;
74
75
  approvalAskAudit: import("../approval-ask-audit-store.js").FileApprovalAskAuditStore | undefined;
75
76
  centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
76
77
  checkpointStore: import("../plugins/store-backend.js").CheckpointStoreFull | undefined;
@@ -22,7 +22,7 @@ import { installShutdownHandlers } from "./shutdown.js";
22
22
  import { bindServerLateSeats } from "./late-binds.js";
23
23
  import { serviceVersion } from "../service-version.js";
24
24
  export async function runStage10(ctx) {
25
- const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveTool, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, singleUserAutoAcceptBaseline, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
25
+ const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveTool, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, singleUserAutoAcceptBaseline, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
26
26
  const stores = {
27
27
  runStore,
28
28
  resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
@@ -133,6 +133,7 @@ export async function runStage10(ctx) {
133
133
  memoryPosture,
134
134
  sqlEngineFacts,
135
135
  writeProtection: buildWriteProtectionPosture(config),
136
+ webSearchProvider,
136
137
  instanceId,
137
138
  capabilities: {
138
139
  version: serviceVersion(),
@@ -1,7 +1,7 @@
1
1
  import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
2
  import { THINKING_FORMATS } from "./model-compat.js";
3
3
  import { RESILIENCE_DEFAULTS } from "./config-types.js";
4
- import { webSearchConfigFromEnv } from "./plugins/web-search.js";
4
+ import { webSearchConfigFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
5
5
  import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
6
6
  export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
7
7
  export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
@@ -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" }),
@@ -447,7 +446,7 @@ export const CONFIG_CATALOG = [
447
446
  r("WEB_SEARCH_ENDPOINT", "orchestration", "url", "web search 后端 endpoint 覆写(searxng 自建面)"),
448
447
  r("WEB_SEARCH_MAX_RESULTS", "orchestration", "number", "web search 单次结果数上限", { derivedDefaultNote: "缺席/非正 ⇒ backend 默认" }),
449
448
  r("WEB_SEARCH_PROBE_ON_BOOT", "orchestration", "boolean", "boot 期 web search 连通性探测(只认 true/1;含糊值当没开——猜错方向是凭空出网)", { staticDefault: "false" }),
450
- r("WEB_SEARCH_PROVIDER", "orchestration", "enum", "web search 工具后端(设合法词=装配 WebSearch;其它/缺席=不装配)", { enumValues: ["brave", "tavily", "searxng"], danger: SEC, resolve: (_c, env) => webSearchConfigFromEnv(env)?.provider ?? null }),
449
+ r("WEB_SEARCH_PROVIDER", "orchestration", "enum", "web search 工具后端(设合法词=装配 WebSearch;其它/缺席=不装配)", { enumValues: WEB_SEARCH_PROVIDERS, danger: SEC, resolve: (_c, env) => webSearchConfigFromEnv(env)?.provider ?? null }),
451
450
  r("WEB_SEARCH_SEARXNG_PARAMS", "orchestration", "string", "searxng 额外查询参数(k=v 对;解析不出任何一对 ⇒ 键不铸)"),
452
451
  r("WEB_SEARCH_TIMEOUT_MS", "orchestration", "number", "web search 单次超时(ms)", { derivedDefaultNote: "缺席/非正 ⇒ backend 默认" }),
453
452
  r("WORKFLOW_AGENTS_READONLY", "orchestration", "boolean", "workflow 子 agent 只读钳(TOB 保守旋钮;默认同权 CC parity)", { staticDefault: "false", danger: SEC, configKey: "workflowAgentsReadOnly" }),
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] !== "");
@@ -12,6 +12,7 @@ import { resolveStreamApprovalGate } from "../../tool-approval.js";
12
12
  import { projectMemoryEngineCapability } from "../../memory-posture.js";
13
13
  import { projectWriteProtectionCapability } from "../../write-protection.js";
14
14
  import { projectSqlEngineCapability } from "../../sql-engine-posture.js";
15
+ import { projectWebSearchCapability } from "../../plugins/web-search.js";
15
16
  import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
16
17
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
17
18
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
@@ -76,6 +77,7 @@ export async function handleCapabilities(req, res, match, ctx) {
76
77
  memoryWrite: false,
77
78
  memoryEngine: projectMemoryEngineCapability(deps.memoryPosture),
78
79
  sql: projectSqlEngineCapability(deps.sqlEngineFacts?.()),
80
+ webSearch: projectWebSearchCapability(deps.webSearchProvider),
79
81
  sessionBackgroundable: Boolean(deps.runStore),
80
82
  runMemoryCaptureOptOut: Boolean(deps.runStore),
81
83
  sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
@@ -689,6 +689,14 @@ export interface ServiceDeploymentDeps {
689
689
  * boot 期算一次的**存活对象**(纯 config 派生,运行期不变;core 座没有热改腿)。
690
690
  * 缺席 = 这个进程不是由 composition root 装起来的(测试夹具形)⇒ 诊断面报 null、能力位报 null。 */
691
691
  writeProtection?: import("../write-protection.js").WriteProtectionPosture;
692
+ /** S-382:**部署侧 WebSearch 后端**(`WEB_SEARCH_PROVIDER`)—— `GET /v1/capabilities` 的 `webSearch` 位的
693
+ * 唯一席位。值**逐字**取自 boot 期真正拿去造后端的那份 `webSearchConfigFromEnv()` 产物
694
+ * (`boot/stage-07-capability-layer.ts`),所以「能力位说 brave、装配里装的是 tavily」结构上不可能。
695
+ * 缺席 = 本部署没有搜索后端(未设 / 拼错 / 装配没喂席位)⇒ 窄投影报 `{backend:"none"}`;
696
+ * 折词的属主与全部判据在 `plugins/web-search.ts` 的 `projectWebSearchCapability` 头注。
697
+ * ⚠️ 席位只承载**部署默认**:单用户车道上每请求的 `settings.webSearch` 压过它,但那条腿**不经过本席**
698
+ * (它在场景装配里每请求现算),所以本位不会随调用方漂。 */
699
+ webSearchProvider?: import("../plugins/web-search.js").WebSearchProvider;
692
700
  /** ① core ruling — SPLIT: `capabilities.workflows` = the ENGINE-CAN axis, boot-computed from core's own
693
701
  * `workflowsCapability(deps)` (hardened script runner ∧ governance), NOT the `Boolean(workflowRunStore)` store
694
702
  * proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store
@@ -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 } } : {}),