@zhushanwen/subagent-core 0.5.1 → 0.6.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 (68) hide show
  1. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  2. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  3. package/dist/index.cjs +1122 -452
  4. package/dist/index.d.cts +188 -5
  5. package/dist/index.d.ts +188 -5
  6. package/dist/index.js +1133 -463
  7. package/dist/{types-Dv4QhSJ_.d.cts → types-C3fE3R9x.d.cts} +65 -0
  8. package/dist/{types-Dv4QhSJ_.d.ts → types-C3fE3R9x.d.ts} +65 -0
  9. package/dist.bundle/index.cjs +34933 -0
  10. package/package.json +2 -2
  11. package/src/execution/__tests__/agent-registry.test.ts +1 -1
  12. package/src/execution/__tests__/alive-store.test.ts +1 -1
  13. package/src/execution/__tests__/collect-budget.test.ts +291 -0
  14. package/src/execution/__tests__/collect-coordinator-service.test.ts +327 -0
  15. package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
  16. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +262 -0
  17. package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
  18. package/src/execution/__tests__/config.test.ts +1 -1
  19. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +1 -1
  20. package/src/execution/__tests__/finalized-marker.test.ts +1 -1
  21. package/src/execution/__tests__/gc-timer.test.ts +1 -1
  22. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
  23. package/src/execution/__tests__/notify-batch.test.ts +500 -0
  24. package/src/execution/__tests__/notify-ledger.test.ts +27 -0
  25. package/src/execution/__tests__/pi-invocation.test.ts +34 -2
  26. package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
  27. package/src/execution/__tests__/relay-env.test.ts +12 -1
  28. package/src/execution/__tests__/session-file-gc.test.ts +1 -1
  29. package/src/execution/__tests__/spawn-event-adapter.test.ts +2 -2
  30. package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
  31. package/src/execution/__tests__/start-sync-model-guard.test.ts +1 -1
  32. package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
  33. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +13 -0
  34. package/src/execution/__tests__/sync-collect-recovery.test.ts +1117 -0
  35. package/src/execution/__tests__/tombstone-store.test.ts +1 -1
  36. package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
  37. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
  38. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
  39. package/src/execution/__tests__/worktree-registry.test.ts +1 -1
  40. package/src/execution/collect-coordinator.ts +200 -0
  41. package/src/execution/config.ts +77 -5
  42. package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
  43. package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
  44. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +1 -1
  45. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
  46. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +1 -1
  47. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +1 -1
  48. package/src/execution/execution-record.ts +7 -0
  49. package/src/execution/finalize-record.ts +15 -11
  50. package/src/execution/notifier.ts +234 -25
  51. package/src/execution/notify-host.ts +21 -1
  52. package/src/execution/record-entry.ts +16 -0
  53. package/src/execution/record-store.ts +161 -30
  54. package/src/execution/subagent-actions-core.ts +62 -6
  55. package/src/execution/subagent-service.ts +416 -7
  56. package/src/execution/sync-rebuild.ts +87 -0
  57. package/src/execution/types.ts +63 -0
  58. package/src/index.ts +1 -1
  59. package/src/orchestration/__tests__/config-loader.test.ts +1 -1
  60. package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
  61. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
  62. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
  63. package/src/orchestration/__tests__/script-generate.test.ts +5 -5
  64. package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
  65. package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
  66. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
  67. package/src/shared/__tests__/atomic-write.test.ts +1 -1
  68. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
package/dist/index.js CHANGED
@@ -674,8 +674,202 @@ function bestEffort(err, context, level = "debug") {
674
674
  }
675
675
  }
676
676
 
677
- // src/execution/lifecycle-manager.ts
677
+ // src/execution/collect-coordinator.ts
678
+ var COLLECT_SCAN_LIMIT = 1e3;
679
+ var CollectCoordinator = class {
680
+ constructor(deps) {
681
+ this.deps = deps;
682
+ }
683
+ deps;
684
+ /** 批缓冲(内存):已终态未 flush 的 sync 成员终态快照,跨 route 累积。
685
+ * [U8] 排程合批窗口期内保持可枚举(E9 dispose 转换依赖 pendingMembers 全量)。 */
686
+ buffer = [];
687
+ /** [U8] 挂起的合批排程定时器(undefined = 无排程)。 */
688
+ flushTimer;
689
+ /** notifyComplete 唯一路由入口。返回去向供测试/诊断断言。 */
690
+ route(record) {
691
+ if (record.collectMode !== "sync") {
692
+ this.deps.notifyAsync(record);
693
+ return "async";
694
+ }
695
+ const snapshotRecord = this.deps.toNotifyRecord(record);
696
+ if (!snapshotRecord) return "sync-skipped";
697
+ this.buffer.push(snapshotRecord);
698
+ return this.armFlushIfClosed() ? "sync-flushed" : "sync-buffered";
699
+ }
700
+ /** 诊断/测试:当前缓冲成员数(= 已终态未 flush 的 sync 成员数,含合批窗口内成员)。 */
701
+ get pendingCount() {
702
+ return this.buffer.length;
703
+ }
704
+ /** 诊断/测试 + E9 dispose 转换数据源:缓冲成员快照(副本,非活引用)。 */
705
+ pendingMembers() {
706
+ return [...this.buffer];
707
+ }
708
+ /** [U8·E9 交互] 取消挂起的合批排程(缓冲原样保留——dispose 转 async 通路仍读得到
709
+ * 全部成员)。无排程时 no-op;幂等。 */
710
+ cancelScheduledFlush() {
711
+ if (this.flushTimer !== void 0) {
712
+ clearTimeout(this.flushTimer);
713
+ this.flushTimer = void 0;
714
+ }
715
+ }
716
+ /** 闭合判定 + 合批排程武装:缓冲非空 && 无非终态 sync 成员 → 排程 flush。
717
+ * 已有排程挂起时幂等(同窗口合批,不重复定时器)。(S6 改名:原名 closeDetected
718
+ * 读作纯检测,实带副作用——武装排程改状态,名实相符优先。) */
719
+ armFlushIfClosed() {
720
+ if (this.buffer.length === 0) return false;
721
+ if (this.hasRunningSync()) return false;
722
+ this.scheduleFlush();
723
+ return true;
724
+ }
725
+ /** [U8] 同宏任务去抖合批排程:setTimeout(0)(Node 1ms clamp)——窗口跨度覆盖一个
726
+ * 完整事件循环轮次(当前 poll 段 I/O 回调 + 微任务链排空),足以收纳 finalize 链
727
+ * 间隙内的背靠背 route,同时无可观察延迟(毫秒级 < 任意 LLM/投递时标)。 */
728
+ scheduleFlush() {
729
+ if (this.flushTimer !== void 0) return;
730
+ this.flushTimer = setTimeout(() => {
731
+ this.flushTimer = void 0;
732
+ this.flushIfClosed();
733
+ }, 0);
734
+ }
735
+ /** 合批窗口到期:重验闭合条件后整体移交 flushBatch 并清空。
736
+ * 窗口内新 sync 成员注册 running(跨轮续累反转)→ 保留缓冲不 flush,等其终态
737
+ * route 重新排程(终态必经 notifyComplete,不丢、不悬挂)。
738
+ * void 不等待 flush 的 async 屏障序列(manifest → 写账 → 落标):缓冲已 splice
739
+ * 先行整体移交,屏障 await 期间窗口内新成员重新入空缓冲开新批,互不干扰。 */
740
+ flushIfClosed() {
741
+ if (this.buffer.length === 0) return;
742
+ if (this.hasRunningSync()) return;
743
+ const members = this.buffer.splice(0, this.buffer.length);
744
+ void this.deps.flushBatch(members);
745
+ }
746
+ /** 是否存在非终态 sync 成员(collectMode=sync && 无 batchFinalized && 非终态)。
747
+ * 非终态口径(⛔3 U1 已核实 + U3 resumable 补丁):status 非 closed 且非 resumable
748
+ * ——池排队/在跑成员在 store.register 时即 status="running",自动计入(闭合等待它);
749
+ * batchFinalized=true 的已离场成员不阻止闭合;running+resumable(SP-5 one-shot
750
+ * 成功回退态,进程已死结果已定格)视为已完成,不阻止闭合。 */
751
+ hasRunningSync() {
752
+ for (const record of this.deps.listRecords(COLLECT_SCAN_LIMIT)) {
753
+ if (record.collectMode === "sync" && record.batchFinalized !== true && record.resumable !== true && record.status !== "closed") {
754
+ return true;
755
+ }
756
+ }
757
+ return false;
758
+ }
759
+ };
760
+
761
+ // src/execution/config.ts
762
+ import * as fs3 from "fs";
763
+ import * as path3 from "path";
678
764
  var logger4 = getLogger("subagents");
765
+ var DEFAULT_CONFIG = {
766
+ version: 1,
767
+ maxConcurrent: 6
768
+ };
769
+ var DEFAULT_MAX_CONCURRENT = 6;
770
+ var DEFAULT_COLLECT_SYNC = {
771
+ default: "async",
772
+ perItemChars: 4e3,
773
+ totalChars: 24e3
774
+ };
775
+ function getGlobalConfigPath(agentDir) {
776
+ return path3.join(agentDir, "subagents", "config.json");
777
+ }
778
+ function loadGlobalConfig(agentDir) {
779
+ try {
780
+ const raw = fs3.readFileSync(getGlobalConfigPath(agentDir), "utf-8");
781
+ const parsed = JSON.parse(raw);
782
+ return sanitizeParsedConfig(parsed);
783
+ } catch {
784
+ return { ...DEFAULT_CONFIG };
785
+ }
786
+ }
787
+ function readGlobalConfig(agentDir) {
788
+ const configPath = getGlobalConfigPath(agentDir);
789
+ let raw;
790
+ try {
791
+ raw = fs3.readFileSync(configPath, "utf-8");
792
+ } catch (err) {
793
+ if (errnoCodeOf(err) === "ENOENT") {
794
+ return { status: "absent", config: { ...DEFAULT_CONFIG } };
795
+ }
796
+ return readFailure(configPath, err);
797
+ }
798
+ try {
799
+ const parsed = JSON.parse(raw);
800
+ return { status: "ok", config: sanitizeParsedConfig(parsed) };
801
+ } catch (err) {
802
+ return readFailure(configPath, err);
803
+ }
804
+ }
805
+ function readFailure(configPath, err) {
806
+ const reason = err instanceof Error ? err.message : String(err);
807
+ logger4.warn(`[subagents] global config read failed (read-failure) at ${configPath}: ${reason}`);
808
+ return { status: "failed", reason };
809
+ }
810
+ function errnoCodeOf(err) {
811
+ if (typeof err !== "object" || err === null || !("code" in err)) return void 0;
812
+ const code = Reflect.get(err, "code");
813
+ return typeof code === "string" ? code : void 0;
814
+ }
815
+ function sanitizeParsedConfig(parsed) {
816
+ const defaultEngine = sanitizeDefaultEngine(parsed.defaultEngine);
817
+ const engineRouting = sanitizeEngineRouting(parsed.engineRouting);
818
+ const collectSync = sanitizeCollectSync(parsed.collectSync);
819
+ return {
820
+ version: parsed.version ?? DEFAULT_CONFIG.version,
821
+ maxConcurrent: sanitizeMaxConcurrent(parsed.maxConcurrent),
822
+ ...defaultEngine !== void 0 ? { defaultEngine } : {},
823
+ ...engineRouting !== void 0 ? { engineRouting } : {},
824
+ ...collectSync !== void 0 ? { collectSync } : {}
825
+ };
826
+ }
827
+ function sanitizeMaxConcurrent(value) {
828
+ return sanitizePositiveInt(value, DEFAULT_MAX_CONCURRENT);
829
+ }
830
+ function sanitizeCollectSync(value) {
831
+ if (typeof value !== "object" || value === null) return void 0;
832
+ const v = value;
833
+ const fallback = DEFAULT_COLLECT_SYNC;
834
+ const fmt = (val) => typeof val === "string" ? JSON.stringify(val) : String(val);
835
+ const bad = [];
836
+ if (v.default !== void 0 && v.default !== "sync" && v.default !== "async") {
837
+ bad.push(`default=${fmt(v.default)}`);
838
+ }
839
+ if (v.perItemChars !== void 0 && !isPositiveInt(v.perItemChars)) {
840
+ bad.push(`perItemChars=${fmt(v.perItemChars)}`);
841
+ }
842
+ if (v.totalChars !== void 0 && !isPositiveInt(v.totalChars)) {
843
+ bad.push(`totalChars=${fmt(v.totalChars)}`);
844
+ }
845
+ if (bad.length > 0) {
846
+ logger4.warn(
847
+ `[subagents] config collectSync invalid field(s) reverted to defaults: ${bad.join(", ")}`
848
+ );
849
+ }
850
+ return {
851
+ default: v.default === "sync" ? "sync" : v.default === "async" ? "async" : fallback.default,
852
+ perItemChars: sanitizePositiveInt(v.perItemChars, fallback.perItemChars),
853
+ totalChars: sanitizePositiveInt(v.totalChars, fallback.totalChars)
854
+ };
855
+ }
856
+ function sanitizePositiveInt(value, fallback) {
857
+ return isPositiveInt(value) ? value : fallback;
858
+ }
859
+ function isPositiveInt(value) {
860
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
861
+ }
862
+ function sanitizeDefaultEngine(value) {
863
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
864
+ }
865
+ function sanitizeEngineRouting(value) {
866
+ if (typeof value !== "object" || value === null) return void 0;
867
+ const strict = value.strict;
868
+ return typeof strict === "boolean" ? { strict } : void 0;
869
+ }
870
+
871
+ // src/execution/lifecycle-manager.ts
872
+ var logger5 = getLogger("subagents");
679
873
  var MS_PER_SECOND = 1e3;
680
874
  var SECONDS_PER_MINUTE = 60;
681
875
  var IDLE_TIMEOUT_MINUTES = 5;
@@ -685,7 +879,7 @@ function getEnvIdleTimeoutMs() {
685
879
  if (!raw) return void 0;
686
880
  const parsed = Number(raw);
687
881
  if (!Number.isFinite(parsed) || parsed <= 0) {
688
- logger4.warn(
882
+ logger5.warn(
689
883
  `[lifecycle-manager] XYZ_SUBAGENT_IDLE_TIMEOUT_MS="${raw}" is invalid (expected a positive millisecond number) \u2014 falling back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms); set a plain ms value (e.g. 1800000) to override`
690
884
  );
691
885
  return void 0;
@@ -845,7 +1039,7 @@ function createConcurrencyPool(options) {
845
1039
  }
846
1040
 
847
1041
  // src/execution/cold-resurrect.ts
848
- import * as fs3 from "fs";
1042
+ import * as fs4 from "fs";
849
1043
 
850
1044
  // src/execution/execution-record.ts
851
1045
  var ACTIVITY_LABEL_MAX = 60;
@@ -920,6 +1114,7 @@ function createRecord(id, identity) {
920
1114
  idleTimeoutMs: identity.idleTimeoutMs,
921
1115
  engine: identity.engine,
922
1116
  engineFallback: identity.engineFallback,
1117
+ collectMode: identity.collectMode,
923
1118
  // 状态(实时更新)
924
1119
  status: "running",
925
1120
  // turns[] 初始化为 [空 turn]——第一个 turn 从创建即存在,
@@ -1350,7 +1545,7 @@ function resurrectColdRecord(deps, found, id) {
1350
1545
  if (wasClosed) {
1351
1546
  if (record.sessionFile) {
1352
1547
  try {
1353
- fs3.rmSync(`${record.sessionFile}.finalized`, { force: true });
1548
+ fs4.rmSync(`${record.sessionFile}.finalized`, { force: true });
1354
1549
  writeAliveMarker(record.sessionFile, { pid: process.pid, id, startedAt: Date.now() });
1355
1550
  } catch (_e) {
1356
1551
  void _e;
@@ -1381,41 +1576,41 @@ function coldLookupForAction(deps, id, allowReconnect) {
1381
1576
  }
1382
1577
 
1383
1578
  // src/execution/finalize-record.ts
1384
- import * as fs7 from "fs";
1385
- import * as path4 from "path";
1579
+ import * as fs8 from "fs";
1580
+ import * as path5 from "path";
1386
1581
 
1387
1582
  // src/execution/finalized-marker.ts
1388
- import * as fs4 from "fs";
1583
+ import * as fs5 from "fs";
1389
1584
  function writeFinalized(sessionFile, reason) {
1390
1585
  try {
1391
- fs4.rmSync(`${sessionFile}.cancelled`, { force: true });
1392
- fs4.writeFileSync(`${sessionFile}.finalized`, reason ?? "", "utf-8");
1586
+ fs5.rmSync(`${sessionFile}.cancelled`, { force: true });
1587
+ fs5.writeFileSync(`${sessionFile}.finalized`, reason ?? "", "utf-8");
1393
1588
  } catch (_e) {
1394
1589
  void _e;
1395
1590
  }
1396
1591
  }
1397
1592
  function readFinalizedReason(sessionFile) {
1398
1593
  try {
1399
- return fs4.readFileSync(`${sessionFile}.finalized`, "utf-8").trim();
1594
+ return fs5.readFileSync(`${sessionFile}.finalized`, "utf-8").trim();
1400
1595
  } catch {
1401
1596
  return void 0;
1402
1597
  }
1403
1598
  }
1404
1599
 
1405
1600
  // src/execution/path-encoding.ts
1406
- import * as path3 from "path";
1601
+ import * as path4 from "path";
1407
1602
  function encodeCwd(cwd) {
1408
1603
  return "--" + cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-") + "--";
1409
1604
  }
1410
1605
  function getSubagentSessionDir(agentDir, mainCwd) {
1411
- return path3.join(agentDir, "subagents", encodeCwd(mainCwd), "sessions");
1606
+ return path4.join(agentDir, "subagents", encodeCwd(mainCwd), "sessions");
1412
1607
  }
1413
1608
  function getSubagentRecordsDir(agentDir, mainCwd) {
1414
- return path3.join(agentDir, "subagents", encodeCwd(mainCwd), "records");
1609
+ return path4.join(agentDir, "subagents", encodeCwd(mainCwd), "records");
1415
1610
  }
1416
1611
 
1417
1612
  // src/execution/session-reconstructor.ts
1418
- import * as fs5 from "fs";
1613
+ import * as fs6 from "fs";
1419
1614
  var IDENTITY_CUSTOM_TYPE = "subagent-identity";
1420
1615
  var TURN_SUMMARY_MAX2 = 80;
1421
1616
  function deriveEventLog(turns, lastError, startedAt) {
@@ -1469,7 +1664,7 @@ function isIdentityData(data) {
1469
1664
  function readJsonlEntries(sessionFile) {
1470
1665
  let raw;
1471
1666
  try {
1472
- raw = fs5.readFileSync(sessionFile, "utf-8");
1667
+ raw = fs6.readFileSync(sessionFile, "utf-8");
1473
1668
  } catch {
1474
1669
  return void 0;
1475
1670
  }
@@ -1639,18 +1834,18 @@ var IDENTITY_HEAD_BYTES = 65536;
1639
1834
  function readIdentityHeader(sessionFile) {
1640
1835
  let text;
1641
1836
  try {
1642
- const fd = fs5.openSync(sessionFile, "r");
1837
+ const fd = fs6.openSync(sessionFile, "r");
1643
1838
  try {
1644
1839
  const buf = Buffer.alloc(IDENTITY_HEAD_BYTES);
1645
1840
  let total = 0;
1646
1841
  while (total < buf.length) {
1647
- const n = fs5.readSync(fd, buf, total, buf.length - total, total);
1842
+ const n = fs6.readSync(fd, buf, total, buf.length - total, total);
1648
1843
  if (n <= 0) break;
1649
1844
  total += n;
1650
1845
  }
1651
1846
  text = buf.toString("utf-8", 0, total);
1652
1847
  } finally {
1653
- fs5.closeSync(fd);
1848
+ fs6.closeSync(fd);
1654
1849
  }
1655
1850
  } catch {
1656
1851
  return void 0;
@@ -1660,7 +1855,7 @@ function readIdentityHeader(sessionFile) {
1660
1855
  function readIdentityAnywhere(sessionFile) {
1661
1856
  let text;
1662
1857
  try {
1663
- text = fs5.readFileSync(sessionFile, "utf-8");
1858
+ text = fs6.readFileSync(sessionFile, "utf-8");
1664
1859
  } catch {
1665
1860
  return void 0;
1666
1861
  }
@@ -1676,20 +1871,20 @@ function readIdentityAnywhere(sessionFile) {
1676
1871
  function readIdentityTail(sessionFile) {
1677
1872
  let text;
1678
1873
  try {
1679
- const { size } = fs5.statSync(sessionFile);
1874
+ const { size } = fs6.statSync(sessionFile);
1680
1875
  const start = Math.max(0, size - IDENTITY_HEAD_BYTES);
1681
- const fd = fs5.openSync(sessionFile, "r");
1876
+ const fd = fs6.openSync(sessionFile, "r");
1682
1877
  try {
1683
1878
  const buf = Buffer.alloc(size - start);
1684
1879
  let total = 0;
1685
1880
  while (total < buf.length) {
1686
- const n = fs5.readSync(fd, buf, total, buf.length - total, start + total);
1881
+ const n = fs6.readSync(fd, buf, total, buf.length - total, start + total);
1687
1882
  if (n <= 0) break;
1688
1883
  total += n;
1689
1884
  }
1690
1885
  text = buf.toString("utf-8", 0, total);
1691
1886
  } finally {
1692
- fs5.closeSync(fd);
1887
+ fs6.closeSync(fd);
1693
1888
  }
1694
1889
  } catch {
1695
1890
  return void 0;
@@ -1764,11 +1959,11 @@ function parseIdentityFromText(text, sessionFile) {
1764
1959
  }
1765
1960
 
1766
1961
  // src/execution/tombstone-store.ts
1767
- import * as fs6 from "fs";
1962
+ import * as fs7 from "fs";
1768
1963
  function writeCancelledTombstone(sessionFile, meta) {
1769
1964
  try {
1770
1965
  const tombstonePath = `${sessionFile}.cancelled`;
1771
- fs6.writeFileSync(tombstonePath, `${JSON.stringify(meta)}
1966
+ fs7.writeFileSync(tombstonePath, `${JSON.stringify(meta)}
1772
1967
  `, "utf-8");
1773
1968
  } catch (_e) {
1774
1969
  void _e;
@@ -1777,7 +1972,7 @@ function writeCancelledTombstone(sessionFile, meta) {
1777
1972
  function readCancelledTombstone(sessionFile) {
1778
1973
  let raw;
1779
1974
  try {
1780
- raw = fs6.readFileSync(`${sessionFile}.cancelled`, "utf-8");
1975
+ raw = fs7.readFileSync(`${sessionFile}.cancelled`, "utf-8");
1781
1976
  } catch {
1782
1977
  return void 0;
1783
1978
  }
@@ -1793,17 +1988,17 @@ function readCancelledTombstone(sessionFile) {
1793
1988
  }
1794
1989
 
1795
1990
  // src/execution/finalize-record.ts
1796
- var logger5 = getLogger("subagents");
1991
+ var logger6 = getLogger("subagents");
1797
1992
  function findSessionFileByRecordIdentity(sessionDir, recordId) {
1798
1993
  let names;
1799
1994
  try {
1800
- names = fs7.readdirSync(sessionDir);
1995
+ names = fs8.readdirSync(sessionDir);
1801
1996
  } catch {
1802
1997
  return void 0;
1803
1998
  }
1804
1999
  for (const name of names) {
1805
2000
  if (!name.endsWith(".jsonl")) continue;
1806
- const full = path4.join(sessionDir, name);
2001
+ const full = path5.join(sessionDir, name);
1807
2002
  const recon = readIdentityHeader(full) ?? readIdentityTail(full);
1808
2003
  if (recon?.id === recordId) return full;
1809
2004
  }
@@ -1814,7 +2009,7 @@ function resolveMissingSessionFile(deps, record) {
1814
2009
  const resolved = findSessionFileByRecordIdentity(deps.sessionDir, record.id);
1815
2010
  if (resolved) {
1816
2011
  record.sessionFile = resolved;
1817
- logger5.warn(
2012
+ logger6.warn(
1818
2013
  `[subagent] finalizeRecord: sessionFile was missing, resolved via sessionDir identity lookup: ${resolved}`
1819
2014
  );
1820
2015
  }
@@ -1826,8 +2021,8 @@ async function collectPatchIfWorktree(deps, record) {
1826
2021
  deps.modelService.getAgentDir(),
1827
2022
  record.worktreeHandle.mainCwd
1828
2023
  );
1829
- fs7.mkdirSync(sessionsDir, { recursive: true });
1830
- const patchFile = path4.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
2024
+ fs8.mkdirSync(sessionsDir, { recursive: true });
2025
+ const patchFile = path5.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
1831
2026
  const patch = await deps.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
1832
2027
  if (patch.written) record.patchFile = patchFile;
1833
2028
  } catch (pe) {
@@ -1886,7 +2081,7 @@ async function writeManifestBestEffort(deps, record) {
1886
2081
  });
1887
2082
  } catch (err) {
1888
2083
  const msg = err instanceof Error ? err.message : String(err);
1889
- logger5.error(`[subagent] manifest \u5199\u5165\u5931\u8D25 (record=${record.id}): ${msg}`);
2084
+ logger6.error(`[subagent] manifest \u5199\u5165\u5931\u8D25 (record=${record.id}): ${msg}`);
1890
2085
  deps.pi?.appendEntry?.("subagent:manifest-write-failed", {
1891
2086
  id: record.id,
1892
2087
  error: msg
@@ -2018,7 +2213,7 @@ var ExecutionNestingContext = class {
2018
2213
  };
2019
2214
 
2020
2215
  // src/execution/engine/common/data-dir.ts
2021
- var logger6 = getLogger("subagents");
2216
+ var logger7 = getLogger("subagents");
2022
2217
  var XYZ_DATA_DIR_ENV = "XYZ_AGENT_DATA_DIR";
2023
2218
  var warned = false;
2024
2219
  function getEngineDataDir(env = process.env, warn = defaultWarn) {
@@ -2034,14 +2229,14 @@ function getEngineDataDir(env = process.env, warn = defaultWarn) {
2034
2229
  return fallback;
2035
2230
  }
2036
2231
  function defaultWarn(msg) {
2037
- logger6.warn(msg);
2232
+ logger7.warn(msg);
2038
2233
  }
2039
2234
 
2040
2235
  // src/execution/engine/common/event-journal.ts
2041
2236
  import { appendFile, mkdir, open as open2 } from "fs/promises";
2042
- import { readFileSync as readFileSync6 } from "fs";
2237
+ import { readFileSync as readFileSync7 } from "fs";
2043
2238
  import { dirname as dirname2 } from "path";
2044
- var logger7 = getLogger("subagents");
2239
+ var logger8 = getLogger("subagents");
2045
2240
  var FLUSH_THRESHOLD_LINES = 64;
2046
2241
  var FLUSH_THRESHOLD_BYTES = 32 * 1024;
2047
2242
  var defaultFs = {
@@ -2150,12 +2345,12 @@ var JournalWriter = class {
2150
2345
  }
2151
2346
  };
2152
2347
  function defaultWarn2(msg) {
2153
- logger7.warn(msg);
2348
+ logger8.warn(msg);
2154
2349
  }
2155
2350
  function replayJournal(path21) {
2156
2351
  let raw;
2157
2352
  try {
2158
- raw = readFileSync6(path21, "utf8");
2353
+ raw = readFileSync7(path21, "utf8");
2159
2354
  } catch {
2160
2355
  return [];
2161
2356
  }
@@ -2246,8 +2441,8 @@ function executeOptionsToEngineTaskSpec(opts) {
2246
2441
 
2247
2442
  // src/execution/engine/engines/pi/pi-engine.ts
2248
2443
  import { execFile as execFile2 } from "child_process";
2249
- import * as fs13 from "fs";
2250
- import * as path8 from "path";
2444
+ import * as fs14 from "fs";
2445
+ import * as path9 from "path";
2251
2446
 
2252
2447
  // src/orchestration/models/types.ts
2253
2448
  var VALID_RUN_TRANSITIONS = {
@@ -2281,11 +2476,11 @@ function stringifySchemaCached(schema, mode) {
2281
2476
  }
2282
2477
 
2283
2478
  // src/execution/engine/engines/pi/pi-invocation.ts
2284
- import * as fs8 from "fs";
2285
- import * as path5 from "path";
2479
+ import * as fs9 from "fs";
2480
+ import * as path6 from "path";
2286
2481
  var BUN_VIRTUAL_PREFIX = "/$bunfs/root/";
2287
2482
  function isGenericRuntime(execPath) {
2288
- const execName = path5.basename(execPath).toLowerCase();
2483
+ const execName = path6.basename(execPath).toLowerCase();
2289
2484
  return /^(node|bun)(\.exe)?$/.test(execName);
2290
2485
  }
2291
2486
  var scriptExistsCache;
@@ -2294,7 +2489,7 @@ function currentScriptExists() {
2294
2489
  if (scriptExistsCache === void 0 || scriptExistsCache.script !== currentScript) {
2295
2490
  scriptExistsCache = {
2296
2491
  script: currentScript,
2297
- exists: currentScript !== void 0 && !currentScript.startsWith(BUN_VIRTUAL_PREFIX) && fs8.existsSync(currentScript)
2492
+ exists: currentScript !== void 0 && !currentScript.startsWith(BUN_VIRTUAL_PREFIX) && fs9.existsSync(currentScript)
2298
2493
  };
2299
2494
  }
2300
2495
  return scriptExistsCache.exists;
@@ -2405,11 +2600,11 @@ import {
2405
2600
  spawn,
2406
2601
  spawnSync
2407
2602
  } from "child_process";
2408
- import * as fs12 from "fs";
2603
+ import * as fs13 from "fs";
2409
2604
 
2410
2605
  // src/execution/session-pending.ts
2411
- import * as fs9 from "fs";
2412
- var logger8 = getLogger("subagents");
2606
+ import * as fs10 from "fs";
2607
+ var logger9 = getLogger("subagents");
2413
2608
  var RECENT_UNREGISTER_WINDOW_MS = 6e4;
2414
2609
  var cursors = /* @__PURE__ */ new Map();
2415
2610
  function prunePendingCursor(sessionFile) {
@@ -2447,7 +2642,7 @@ function pendingFileUnreadableMessage(err) {
2447
2642
  }
2448
2643
  function statPendingFileSize(sessionFile) {
2449
2644
  try {
2450
- return fs9.statSync(sessionFile).size;
2645
+ return fs10.statSync(sessionFile).size;
2451
2646
  } catch (err) {
2452
2647
  cursors.delete(sessionFile);
2453
2648
  return { error: pendingFileUnreadableMessage(err) };
@@ -2456,7 +2651,7 @@ function statPendingFileSize(sessionFile) {
2456
2651
  function readPendingChunk(sessionFile, cursor, size) {
2457
2652
  try {
2458
2653
  if (cursor.offset === 0) {
2459
- return fs9.readFileSync(sessionFile, "utf-8");
2654
+ return fs10.readFileSync(sessionFile, "utf-8");
2460
2655
  }
2461
2656
  return readPendingChunkFrom(sessionFile, cursor.offset, size - cursor.offset);
2462
2657
  } catch (err) {
@@ -2466,17 +2661,17 @@ function readPendingChunk(sessionFile, cursor, size) {
2466
2661
  }
2467
2662
  function readPendingChunkFrom(sessionFile, offset, len) {
2468
2663
  const buf = Buffer.alloc(len);
2469
- const fd = fs9.openSync(sessionFile, "r");
2664
+ const fd = fs10.openSync(sessionFile, "r");
2470
2665
  try {
2471
2666
  let total = 0;
2472
2667
  while (total < len) {
2473
- const n = fs9.readSync(fd, buf, total, len - total, offset + total);
2668
+ const n = fs10.readSync(fd, buf, total, len - total, offset + total);
2474
2669
  if (n <= 0) break;
2475
2670
  total += n;
2476
2671
  }
2477
2672
  return buf.toString("utf-8", 0, total);
2478
2673
  } finally {
2479
- fs9.closeSync(fd);
2674
+ fs10.closeSync(fd);
2480
2675
  }
2481
2676
  }
2482
2677
  function consumePendingLines(complete, cursor, sessionFile) {
@@ -2501,7 +2696,7 @@ function applyPendingLine(line, cursor, sessionFile) {
2501
2696
  if (Number.isFinite(ts) && ts > cursor.latestUnregisterMs) cursor.latestUnregisterMs = ts;
2502
2697
  }
2503
2698
  } catch {
2504
- logger8.debug("skipped malformed pending line", { sessionFile });
2699
+ logger9.debug("skipped malformed pending line", { sessionFile });
2505
2700
  }
2506
2701
  }
2507
2702
  function extractPendingEntryId(data) {
@@ -2542,7 +2737,7 @@ function listActivePendingFromSessionFile(sessionFile) {
2542
2737
  }
2543
2738
 
2544
2739
  // src/execution/engine/common/kill-chain.ts
2545
- var logger9 = getLogger("subagents");
2740
+ var logger10 = getLogger("subagents");
2546
2741
  var MS_PER_SECOND3 = 1e3;
2547
2742
  var SIGKILL_REAP_TIMEOUT_MS = 1e4;
2548
2743
  var HOST_TIMEOUT_ABORT_REASON = "agent-call-timeout";
@@ -2554,7 +2749,7 @@ async function killChain(child, opts) {
2554
2749
  if (graceful === "settled") return "terminated";
2555
2750
  if (child.exitCode !== null || child.signalCode !== null) return "terminated";
2556
2751
  if (opts.escalationNote !== void 0) {
2557
- logger9.warn(
2752
+ logger10.warn(
2558
2753
  `[kill-chain] ${opts.escalationNote} still alive ${opts.graceMs / MS_PER_SECOND3}s after SIGTERM, escalating to SIGKILL`
2559
2754
  );
2560
2755
  }
@@ -2566,7 +2761,7 @@ function safeKill(child, signal) {
2566
2761
  try {
2567
2762
  child.kill(signal);
2568
2763
  } catch (err) {
2569
- logger9.debug(
2764
+ logger10.debug(
2570
2765
  `[kill-chain] ${signal} on exited process: ${toErrorMessage(err)}`
2571
2766
  );
2572
2767
  }
@@ -2688,7 +2883,7 @@ function parseMirrorFlagArgs(flagArgs) {
2688
2883
 
2689
2884
  // src/execution/engine/engines/pi/stdin-writer.ts
2690
2885
  import * as crypto2 from "crypto";
2691
- var logger10 = getLogger("subagents");
2886
+ var logger11 = getLogger("subagents");
2692
2887
  var epipeConsecutiveFailures = /* @__PURE__ */ new Map();
2693
2888
  var EPIPE_FAILURE_THRESHOLD = 2;
2694
2889
  function recordEpipeFailure(recordId) {
@@ -2710,7 +2905,7 @@ function respond(child, id, out, signal) {
2710
2905
  else if ("confirmed" in out) line = JSON.stringify({ type: "extension_ui_response", id, confirmed: out.confirmed });
2711
2906
  else if ("cancelled" in out) line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
2712
2907
  } catch (err) {
2713
- logger10.warn(`[subagents] JSON.stringify failed for ui response ${id}, degrading to cancelled`, {
2908
+ logger11.warn(`[subagents] JSON.stringify failed for ui response ${id}, degrading to cancelled`, {
2714
2909
  detail: toErrorMessage(err)
2715
2910
  });
2716
2911
  line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
@@ -2743,14 +2938,14 @@ function writeStdinLine(child, line, warnTag) {
2743
2938
  if (!child.stdin || child.stdin.destroyed) return;
2744
2939
  try {
2745
2940
  const ok = child.stdin.write(line + "\n");
2746
- if (!ok) logger10.warn(`[subagents] stdin backpressure on ${warnTag}`);
2941
+ if (!ok) logger11.warn(`[subagents] stdin backpressure on ${warnTag}`);
2747
2942
  } catch (err) {
2748
2943
  if (err !== null && typeof err === "object" && "code" in err && (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED")) {
2749
2944
  throw new Error(
2750
2945
  `[subagents] EPIPE on stdin write (${warnTag}): pipe broken, child process likely exited. Recovery: treat as dead process and resume via cold path.`
2751
2946
  );
2752
2947
  }
2753
- logger10.warn(`[subagents] unexpected stdin write error on ${warnTag}`, {
2948
+ logger11.warn(`[subagents] unexpected stdin write error on ${warnTag}`, {
2754
2949
  detail: toErrorMessage(err)
2755
2950
  });
2756
2951
  }
@@ -3059,7 +3254,7 @@ function schemaEnvByteLength(schemaEnv) {
3059
3254
  }
3060
3255
 
3061
3256
  // src/execution/settled-watchdog.ts
3062
- var logger11 = getLogger("subagents");
3257
+ var logger12 = getLogger("subagents");
3063
3258
  var MS_PER_SECOND4 = 1e3;
3064
3259
  var SECONDS_PER_MINUTE2 = 60;
3065
3260
  var MID_ROUND_MINUTES = 30;
@@ -3075,13 +3270,13 @@ function resolveSettledWatchdogEnv() {
3075
3270
  if (raw === void 0 || raw.trim() === "") return envCache;
3076
3271
  const parsed = Number(raw);
3077
3272
  if (!Number.isFinite(parsed)) {
3078
- logger11.warn(
3273
+ logger12.warn(
3079
3274
  `[settled-watchdog] ${SETTLED_WATCHDOG_ENV}="${raw}" is invalid (expected a millisecond number) \u2014 falling back to default settled phase limit (${SETTLED_WATCHDOG_TIMEOUT_MS}ms); set a plain ms value (e.g. 60000) to override, or 0 to disable both phases`
3080
3275
  );
3081
3276
  return envCache;
3082
3277
  }
3083
3278
  if (parsed <= 0) {
3084
- logger11.warn(
3279
+ logger12.warn(
3085
3280
  `[settled-watchdog] ${SETTLED_WATCHDOG_ENV}=${parsed} disables BOTH watchdog phases (mid-round no-progress + settled phase limit). Consequence: a wedged chatMode round (no agent_end, or agent_settled never arriving) has NO independent recovery timer \u2014 the process leaks until the host exits (the "three-no-window" shape). Recovery: unset the env or set a positive ms value.`
3086
3281
  );
3087
3282
  envCache.disabled = true;
@@ -3108,7 +3303,7 @@ function armMidRoundNoProgress(recordId, handlers) {
3108
3303
  assertSafeTimerDelay(SETTLED_MID_ROUND_NO_PROGRESS_MS, "settled watchdog (mid-round)");
3109
3304
  const timer = setTimeout(() => {
3110
3305
  armedEntries.delete(recordId);
3111
- logger11.debug(
3306
+ logger12.debug(
3112
3307
  `[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`
3113
3308
  );
3114
3309
  handlers.onMidTimeout({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
@@ -3128,7 +3323,7 @@ function armSettledWatchdog(recordId, onTimeout) {
3128
3323
  assertSafeTimerDelay(windowMs, "settled watchdog");
3129
3324
  const timer = setTimeout(() => {
3130
3325
  armedEntries.delete(recordId);
3131
- logger11.debug(
3326
+ logger12.debug(
3132
3327
  `[settled-watchdog] settled phase fired for ${recordId} after ${windowMs}ms without agent_settled`
3133
3328
  );
3134
3329
  onTimeout({ phase: "settled", waitedMs: windowMs });
@@ -3151,7 +3346,7 @@ function refreshMidRoundNoProgress(recordId) {
3151
3346
  const onMidTimeout = entry.onMidTimeout;
3152
3347
  const timer = setTimeout(() => {
3153
3348
  armedEntries.delete(recordId);
3154
- logger11.debug(
3349
+ logger12.debug(
3155
3350
  `[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`
3156
3351
  );
3157
3352
  onMidTimeout?.({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
@@ -3167,8 +3362,8 @@ function disarmSettledWatchdog(recordId) {
3167
3362
  var MAX_FORK_DEPTH = 10;
3168
3363
 
3169
3364
  // src/execution/engine/engines/pi/spawn-event-adapter.ts
3170
- import * as fs10 from "fs";
3171
- import * as path6 from "path";
3365
+ import * as fs11 from "fs";
3366
+ import * as path7 from "path";
3172
3367
  function isSessionHeader(obj) {
3173
3368
  if (typeof obj !== "object" || obj === null) return false;
3174
3369
  const r = obj;
@@ -3316,28 +3511,28 @@ function deriveSessionFilePath(header, sessionDir) {
3316
3511
  }
3317
3512
  function findSessionFileByHeaderId(sessionDir, sessionId) {
3318
3513
  try {
3319
- const files = fs10.readdirSync(sessionDir);
3514
+ const files = fs11.readdirSync(sessionDir);
3320
3515
  const match = files.find((f) => f.endsWith(`_${sessionId}.jsonl`));
3321
- return match ? path6.join(sessionDir, match) : void 0;
3516
+ return match ? path7.join(sessionDir, match) : void 0;
3322
3517
  } catch {
3323
3518
  return void 0;
3324
3519
  }
3325
3520
  }
3326
3521
 
3327
3522
  // src/execution/engine/engines/pi/temp-prompt.ts
3328
- import * as fs11 from "fs";
3523
+ import * as fs12 from "fs";
3329
3524
  import * as os from "os";
3330
- import * as path7 from "path";
3525
+ import * as path8 from "path";
3331
3526
  async function writePromptToTempFile(agentName, prompt) {
3332
- const dir = await fs11.promises.mkdtemp(path7.join(os.tmpdir(), "pi-subagent-"));
3527
+ const dir = await fs12.promises.mkdtemp(path8.join(os.tmpdir(), "pi-subagent-"));
3333
3528
  const safeName = agentName.replace(/[^\w.-]+/g, "_");
3334
- const filePath = path7.join(dir, `prompt-${safeName}.md`);
3335
- await fs11.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
3529
+ const filePath = path8.join(dir, `prompt-${safeName}.md`);
3530
+ await fs12.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
3336
3531
  return { dir, filePath };
3337
3532
  }
3338
3533
  async function cleanupTempPrompt(file) {
3339
3534
  try {
3340
- await fs11.promises.rm(file.dir, { recursive: true, force: true });
3535
+ await fs12.promises.rm(file.dir, { recursive: true, force: true });
3341
3536
  } catch (err) {
3342
3537
  bestEffort(err, `cleanup temp prompt dir ${file.dir}`);
3343
3538
  }
@@ -3482,7 +3677,7 @@ function createUiChannelRegistry() {
3482
3677
  }
3483
3678
 
3484
3679
  // src/execution/ui-request-observability.ts
3485
- var logger12 = getLogger("subagents");
3680
+ var logger13 = getLogger("subagents");
3486
3681
  var GLOBAL_OBSERVABILITY_KEY = /* @__PURE__ */ Symbol.for("pi-subagent-workflow.ui-observability");
3487
3682
  function registerGlobalObservability(obs) {
3488
3683
  globalThis[GLOBAL_OBSERVABILITY_KEY] = obs;
@@ -3492,7 +3687,7 @@ function notifyMissingHandlerGlobal(sessionId) {
3492
3687
  if (obs) {
3493
3688
  obs.notifyMissingHandler(sessionId);
3494
3689
  } else {
3495
- logger12.warn(
3690
+ logger13.warn(
3496
3691
  `[subagents] uiRequestHandler missing (session=${sessionId}, global observability not registered)`
3497
3692
  );
3498
3693
  }
@@ -3520,12 +3715,12 @@ var UiRequestObservability = class {
3520
3715
  this.warnedMissingHandlerSessions.clear();
3521
3716
  }
3522
3717
  this.warnedMissingHandlerSessions.add(sessionId);
3523
- logger12.warn(`[subagents] uiRequestHandler missing (session=${sessionId}, mode=${this.sessionMode})`);
3718
+ logger13.warn(`[subagents] uiRequestHandler missing (session=${sessionId}, mode=${this.sessionMode})`);
3524
3719
  }
3525
3720
  };
3526
3721
 
3527
3722
  // src/execution/ui-request-queue.ts
3528
- var logger13 = getLogger("subagents");
3723
+ var logger14 = getLogger("subagents");
3529
3724
  function createUiRequestQueue(child, ctx) {
3530
3725
  const abortController = new AbortController();
3531
3726
  const queue = [];
@@ -3537,7 +3732,7 @@ function createUiRequestQueue(child, ctx) {
3537
3732
  const { id, request, signal } = queue.shift();
3538
3733
  handleUiRequest(child, id, request, ctx, signal).catch((err) => {
3539
3734
  const m = toErrorMessage(err);
3540
- logger13.error(`[subagents] ui request ${id} (${request.method}) failed unexpectedly: ${m}`);
3735
+ logger14.error(`[subagents] ui request ${id} (${request.method}) failed unexpectedly: ${m}`);
3541
3736
  }).finally(() => {
3542
3737
  processing = false;
3543
3738
  processNext();
@@ -3582,7 +3777,7 @@ async function handleUiRequest(child, id, request, ctx, signal) {
3582
3777
  respond(child, id, result, signal);
3583
3778
  } catch (err) {
3584
3779
  if (signal?.aborted) return;
3585
- logger13.error("[subagents] uiRequestHandler threw", {
3780
+ logger14.error("[subagents] uiRequestHandler threw", {
3586
3781
  detail: toErrorMessage(err)
3587
3782
  });
3588
3783
  respond(child, id, { cancelled: true }, signal);
@@ -3625,7 +3820,7 @@ function extractMethodFields(req) {
3625
3820
  }
3626
3821
 
3627
3822
  // src/execution/engine/engines/pi/session-runner.ts
3628
- var logger14 = getLogger("subagents");
3823
+ var logger15 = getLogger("subagents");
3629
3824
  function isSdkEvent(x) {
3630
3825
  if (typeof x !== "object" || x === null) return false;
3631
3826
  if (!("type" in x)) return false;
@@ -3678,7 +3873,7 @@ function getEnvSpawnWatchdogMs() {
3678
3873
  if (!raw) return void 0;
3679
3874
  const parsed = Number(raw);
3680
3875
  if (!Number.isFinite(parsed) || parsed <= 0) {
3681
- logger14.warn(
3876
+ logger15.warn(
3682
3877
  `[session-runner] ${SPAWN_WATCHDOG_ENV}="${raw}" is invalid (expected a positive millisecond number) \u2014 spawn watchdog NOT armed, equivalent to disabled; set a plain ms value (e.g. 1800000) to enable`
3683
3878
  );
3684
3879
  return void 0;
@@ -3745,7 +3940,7 @@ function killAllSpawnedChildren(signal = "SIGTERM") {
3745
3940
  child.kill(child.killed ? "SIGKILL" : signal);
3746
3941
  n++;
3747
3942
  } catch (err) {
3748
- logger14.debug(
3943
+ logger15.debug(
3749
3944
  `[session-runner] killAllSpawnedChildren: kill failed (best-effort continue): ${toErrorMessage(err)}`
3750
3945
  );
3751
3946
  }
@@ -3789,7 +3984,7 @@ function killPidWithEscalation(pid, label) {
3789
3984
  try {
3790
3985
  process.kill(pid, "SIGTERM");
3791
3986
  } catch (err) {
3792
- logger14.debug(
3987
+ logger15.debug(
3793
3988
  `[session-runner] ${label}: SIGTERM to pid ${pid} failed (best-effort continue): ${toErrorMessage(err)}`
3794
3989
  );
3795
3990
  return;
@@ -3797,7 +3992,7 @@ function killPidWithEscalation(pid, label) {
3797
3992
  assertSafeTimerDelay(SIGKILL_ESCALATION_MS, `descendant SIGKILL escalation (${label})`);
3798
3993
  const escalation = setTimeout(() => {
3799
3994
  if (isProcessAlive(pid)) {
3800
- logger14.warn(
3995
+ logger15.warn(
3801
3996
  `[session-runner] ${label}: descendant pid ${pid} still alive ${SIGKILL_ESCALATION_MS / MS_PER_SECOND5}s after SIGTERM, escalating to SIGKILL`
3802
3997
  );
3803
3998
  try {
@@ -3820,7 +4015,7 @@ function sweepDescendantsOfSession(rootSessionFile, sessionDir, source) {
3820
4015
  visited.add(sessionFile);
3821
4016
  const list = listActivePendingFromSessionFile(sessionFile);
3822
4017
  if (list.error) {
3823
- logger14.debug(
4018
+ logger15.debug(
3824
4019
  `[session-runner] descendant sweep (${source}): pending list unreadable for ${sessionFile}: ${list.error}`
3825
4020
  );
3826
4021
  continue;
@@ -3864,7 +4059,7 @@ function sweepDescendantsOfSession(rootSessionFile, sessionDir, source) {
3864
4059
  });
3865
4060
  continue;
3866
4061
  }
3867
- logger14.warn(
4062
+ logger15.warn(
3868
4063
  `[session-runner] descendant sweep (${source}): killing orphan descendant pid=${marker.pid} session=${item.sessionId} (${item.id})`
3869
4064
  );
3870
4065
  killPidWithEscalation(marker.pid, `descendant sweep (${source})`);
@@ -3927,7 +4122,7 @@ async function buildEnvBlock(cwd, forkDepth, nestingDepth) {
3927
4122
  );
3928
4123
  });
3929
4124
  } catch (err) {
3930
- logger14.debug(
4125
+ logger15.debug(
3931
4126
  `[session-runner] buildEnvBlock: git branch lookup failed for ${cwd}, fallback to empty: ${toErrorMessage(err)}`
3932
4127
  );
3933
4128
  branch = "";
@@ -3978,7 +4173,7 @@ function writeAliveMarkerBestEffort(sessionFile, pid, id) {
3978
4173
  try {
3979
4174
  writeAliveMarker(sessionFile, { pid, id, startedAt: Date.now() });
3980
4175
  } catch (err) {
3981
- logger14.debug(
4176
+ logger15.debug(
3982
4177
  `[session-runner] alive marker write failed (best-effort continue): ${toErrorMessage(err)}`
3983
4178
  );
3984
4179
  }
@@ -4014,7 +4209,7 @@ function killRecordChildWithEscalation(recordId, source) {
4014
4209
  const escalation = setTimeout(
4015
4210
  () => {
4016
4211
  if (child.exitCode === null && child.signalCode === null) {
4017
- logger14.warn(
4212
+ logger15.warn(
4018
4213
  `[session-runner] child ${recordId} still alive ${SIGKILL_ESCALATION_MS / MS_PER_SECOND5}s after SIGTERM, escalating to SIGKILL (source: ${source})`
4019
4214
  );
4020
4215
  child.kill("SIGKILL");
@@ -4034,7 +4229,7 @@ function killRecordChildWithEscalation(recordId, source) {
4034
4229
  function hasLiveActiveDescendant(sessionFile, sessionDir) {
4035
4230
  const list = listActivePendingFromSessionFile(sessionFile);
4036
4231
  if (list.error) {
4037
- logger14.warn(
4232
+ logger15.warn(
4038
4233
  `[session-runner] keep-alive no-progress re-check failed (treating as no live descendants): ${list.error}`
4039
4234
  );
4040
4235
  return false;
@@ -4054,13 +4249,13 @@ function armKeepAliveNoProgressTimer(state, child, sessionDir) {
4054
4249
  state.keepAliveNoProgressTimer = setTimeout(() => {
4055
4250
  if (hasLiveActiveDescendant(state.record.sessionFile, sessionDir)) {
4056
4251
  touchAliveMarkerForHeartbeat(state.record.sessionFile, child.pid, state.record.id);
4057
- logger14.debug(
4252
+ logger15.debug(
4058
4253
  `[session-runner] keep-alive no-progress re-check: live descendant(s) present for ${state.record.id}, re-arm (cadence ${KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS / MS_PER_SECOND5 / SECONDS_PER_MINUTE3} min)`
4059
4254
  );
4060
4255
  armKeepAliveNoProgressTimer(state, child, sessionDir);
4061
4256
  return;
4062
4257
  }
4063
- logger14.warn(
4258
+ logger15.warn(
4064
4259
  `[session-runner] keep-alive no-progress watchdog fired for ${state.record.id}: no child output for ${KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS / MS_PER_SECOND5 / SECONDS_PER_MINUTE3} min and no live descendant (bare-default keep-alive without maxTurns/env), terminating`
4065
4260
  );
4066
4261
  state.sweepDescendantsOnClose = true;
@@ -4121,7 +4316,7 @@ function createSpawnEventHandlers(state) {
4121
4316
  bestEffort(err, "armIdleTimer (agent_settled chatMode)", "error");
4122
4317
  try {
4123
4318
  armIdleTimer(record.id, armIdleTimerOnTimeout, DEFAULT_IDLE_TIMEOUT_MS);
4124
- logger14.warn(
4319
+ logger15.warn(
4125
4320
  `[session-runner] idleTimeoutMs invalid for ${record.id}, fell back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms) \u2014 idle GC and round notification gate stay active`
4126
4321
  );
4127
4322
  } catch (fallbackErr) {
@@ -4283,7 +4478,7 @@ async function backfillSessionFileViaGetState(state, child, registerGetStateList
4283
4478
  if (child.pid) {
4284
4479
  writeAliveMarkerBestEffort(r.sessionFile, child.pid, r.sessionId ?? record.id);
4285
4480
  }
4286
- logger14.warn(
4481
+ logger15.warn(
4287
4482
  `[session-runner] agent_end: sessionFile backfilled via lazy get_state (spawn handshake had failed): ${r.sessionFile}`
4288
4483
  );
4289
4484
  }
@@ -4295,11 +4490,11 @@ function keepAliveOnAgentEnd(state, child, sessionDir, pending) {
4295
4490
  const { record, opts } = state;
4296
4491
  touchAliveMarkerForHeartbeat(record.sessionFile, child.pid, record.id);
4297
4492
  if (pending.error) {
4298
- logger14.warn(
4493
+ logger15.warn(
4299
4494
  `[session-runner] agent_end: keep alive (sessionFile unreadable, conservative): ${pending.error}`
4300
4495
  );
4301
4496
  } else {
4302
- logger14.debug(
4497
+ logger15.debug(
4303
4498
  `[session-runner] agent_end: keep alive, ${pending.count} active descendant(s) pending`
4304
4499
  );
4305
4500
  }
@@ -4326,7 +4521,7 @@ function keepAliveOnAgentEnd(state, child, sessionDir, pending) {
4326
4521
  function keepAliveForWakeupGrace(state, child) {
4327
4522
  const { record } = state;
4328
4523
  touchAliveMarkerForHeartbeat(record.sessionFile, child.pid, record.id);
4329
- logger14.debug(
4524
+ logger15.debug(
4330
4525
  "[session-runner] agent_end: keep alive, recent descendant completion (wake-up in flight)"
4331
4526
  );
4332
4527
  clearTimeout(state.watchdog);
@@ -4351,7 +4546,7 @@ function attachStdoutPump(child, state, sessionDir, handleSdkEvent) {
4351
4546
  invalidLineSamples.push(truncated);
4352
4547
  }
4353
4548
  if (invalidLineCount <= MAX_INVALID_LINE_SAMPLES) {
4354
- logger14.debug(
4549
+ logger15.debug(
4355
4550
  `[session-runner] stdout invalid line #${invalidLineCount} dropped (${reason}): ${truncated}`
4356
4551
  );
4357
4552
  }
@@ -4461,7 +4656,7 @@ function attachStdoutPump(child, state, sessionDir, handleSdkEvent) {
4461
4656
  }
4462
4657
  }
4463
4658
  if (invalidLineCount > 0) {
4464
- logger14.debug(
4659
+ logger15.debug(
4465
4660
  `[session-runner] stdout had ${invalidLineCount} invalid line(s) dropped in total; sample(s): ${invalidLineSamples.join(" | ")}`
4466
4661
  );
4467
4662
  }
@@ -4508,7 +4703,7 @@ function setupFreshChild(state, child, task) {
4508
4703
  try {
4509
4704
  void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
4510
4705
  } catch (err) {
4511
- logger14.warn("[worktree] worktree pid registration failed (defensive)", {
4706
+ logger15.warn("[worktree] worktree pid registration failed (defensive)", {
4512
4707
  branch: opts.worktree.branch,
4513
4708
  pid: child.pid,
4514
4709
  err: toErrorMessage(err)
@@ -4520,7 +4715,7 @@ function setupFreshChild(state, child, task) {
4520
4715
  child.stdin.on("error", (err) => {
4521
4716
  removeChildRegistration(record.id, child);
4522
4717
  const count = recordEpipeFailure(record.id);
4523
- logger14.warn(`[subagents] async stdin error for ${record.id}`, {
4718
+ logger15.warn(`[subagents] async stdin error for ${record.id}`, {
4524
4719
  detail: err.message,
4525
4720
  epipeCount: count,
4526
4721
  threshold: EPIPE_FAILURE_THRESHOLD,
@@ -4534,14 +4729,14 @@ function armChatModeSettledWatchdog(state, child) {
4534
4729
  if (record.chatMode) {
4535
4730
  armMidRoundNoProgress(record.id, {
4536
4731
  onMidTimeout: (fire) => {
4537
- logger14.warn(
4732
+ logger15.warn(
4538
4733
  `[session-runner] settled watchdog (mid-round) fired for ${record.id}: no valid protocol event for ${fire.waitedMs / MS_PER_SECOND5 / SECONDS_PER_MINUTE3} min, terminating (LC-1 wedge recovery)`
4539
4734
  );
4540
4735
  state.settledWatchdogFired = fire;
4541
4736
  killChildWithEscalation(state, child, "settled watchdog");
4542
4737
  },
4543
4738
  onSettleTimeout: (fire) => {
4544
- logger14.warn(
4739
+ logger15.warn(
4545
4740
  `[session-runner] settled watchdog (settled phase) fired for ${record.id}: no agent_settled within ${fire.waitedMs / MS_PER_SECOND5}s after agent_end, terminating (LC-1 wedge recovery)`
4546
4741
  );
4547
4742
  state.settledWatchdogFired = fire;
@@ -4572,14 +4767,14 @@ function startGetStateHandshake(child, pump) {
4572
4767
  if (pump.isHandshakePending()) pump.finishHandshake(r);
4573
4768
  }).catch((err) => {
4574
4769
  const m = toErrorMessage(err);
4575
- logger14.error(`[session-runner] get_state handshake failed: ${m}`);
4770
+ logger15.error(`[session-runner] get_state handshake failed: ${m}`);
4576
4771
  pump.abandonHandshake();
4577
4772
  });
4578
4773
  }
4579
4774
  function backfillSessionFileByLookup(state, sessionDir) {
4580
4775
  const { record } = state;
4581
4776
  const lookupId = state.sessionHeader?.id ?? state.handshakeResult?.sessionId;
4582
- const needsLookup = record.sessionFile ? !fs12.existsSync(record.sessionFile) : lookupId !== void 0;
4777
+ const needsLookup = record.sessionFile ? !fs13.existsSync(record.sessionFile) : lookupId !== void 0;
4583
4778
  if (lookupId && needsLookup) {
4584
4779
  const actual = findSessionFileByHeaderId(sessionDir, lookupId);
4585
4780
  if (actual && actual !== record.sessionFile) record.sessionFile = actual;
@@ -4591,7 +4786,7 @@ function sweepDescendantsOnChildClose(state, sessionDir) {
4591
4786
  try {
4592
4787
  const sweep = sweepDescendantsOfSession(record.sessionFile, sessionDir, "keep-alive watchdog");
4593
4788
  if (sweep.killed.length > 0 || sweep.skipped.length > 0) {
4594
- logger14.warn(
4789
+ logger15.warn(
4595
4790
  `[session-runner] descendant sweep (keep-alive watchdog): killed=[${sweep.killed.join(", ")}] skipped=${JSON.stringify(sweep.skipped)}`
4596
4791
  );
4597
4792
  }
@@ -4644,7 +4839,7 @@ async function runSpawn(record, task, opts, ctx, resume) {
4644
4839
  };
4645
4840
  const handleSdkEvent = createSpawnEventHandlers(state);
4646
4841
  const sessionDir = getSubagentSessionDir(ctx.agentDir, ctx.rootCwd);
4647
- fs12.mkdirSync(sessionDir, { recursive: true });
4842
+ fs13.mkdirSync(sessionDir, { recursive: true });
4648
4843
  const spawnCwd = opts.worktree?.path ?? ctx.cwd;
4649
4844
  const forkSource = opts.forkSource ?? (opts.fork ? ctx.mainSessionFile : void 0);
4650
4845
  const tempPromptFile = await writeAppendSystemPromptFile(record, opts, ctx);
@@ -4691,7 +4886,7 @@ async function runSpawn(record, task, opts, ctx, resume) {
4691
4886
  }
4692
4887
 
4693
4888
  // src/execution/engine/engines/pi/pi-engine.ts
4694
- var logger15 = getLogger("subagents");
4889
+ var logger16 = getLogger("subagents");
4695
4890
  var PI_ENGINE_ID2 = "pi";
4696
4891
  var PI_ADAPTER_VERSION = "1.0.0";
4697
4892
  var PI_POOL_KEY = "shared";
@@ -4871,7 +5066,7 @@ var PiEngine = class {
4871
5066
  sendPromptCommand(child, text, { streamingBehavior: interrupt ? "steer" : "followUp" });
4872
5067
  clearEpipeFailure(record.id);
4873
5068
  if (child.exitCode !== null || child.signalCode !== null) {
4874
- logger15.warn(
5069
+ logger16.warn(
4875
5070
  `[subagents] deliverMessage: child ${record.id} died around stdin write, message may be lost`,
4876
5071
  {
4877
5072
  msgType: interrupt ? "steer" : "followUp",
@@ -4885,7 +5080,7 @@ var PiEngine = class {
4885
5080
  service.reportRecordTransition?.(record);
4886
5081
  } catch (err) {
4887
5082
  if (err instanceof Error && err.message.includes("EPIPE")) {
4888
- logger15.warn(`[subagents] EPIPE on hot path for ${record.id}, falling back to cold path resume`, {
5083
+ logger16.warn(`[subagents] EPIPE on hot path for ${record.id}, falling back to cold path resume`, {
4889
5084
  detail: err.message
4890
5085
  });
4891
5086
  if (spawnedChildren.get(record.id) === child) {
@@ -4936,7 +5131,7 @@ var PiEngine = class {
4936
5131
  return;
4937
5132
  }
4938
5133
  }
4939
- logger15.warn(
5134
+ logger16.warn(
4940
5135
  `[subagents] deliverPrompt hot path failed for ${record.id}; idle timer re-armed to keep process recovery bounded`,
4941
5136
  { detail }
4942
5137
  );
@@ -5085,7 +5280,7 @@ async function defaultProbeVersion(invocation) {
5085
5280
  );
5086
5281
  });
5087
5282
  } catch (err) {
5088
- logger15.debug(
5283
+ logger16.debug(
5089
5284
  `[pi-engine] probe version check failed (best-effort continue): ${toErrorMessage(err)}`
5090
5285
  );
5091
5286
  return void 0;
@@ -5094,12 +5289,12 @@ async function defaultProbeVersion(invocation) {
5094
5289
  function isInvocationResolvable(invocation) {
5095
5290
  if (invocation.command !== "pi") return true;
5096
5291
  const pathEnv = process.env.PATH ?? "";
5097
- for (const dir of pathEnv.split(path8.delimiter)) {
5292
+ for (const dir of pathEnv.split(path9.delimiter)) {
5098
5293
  if (dir === "") continue;
5099
5294
  try {
5100
- if (fs13.existsSync(path8.join(dir, "pi"))) return true;
5295
+ if (fs14.existsSync(path9.join(dir, "pi"))) return true;
5101
5296
  } catch (err) {
5102
- logger15.debug(
5297
+ logger16.debug(
5103
5298
  `[pi-engine] PATH dir probe failed (continue scanning): ${dir}: ${toErrorMessage(err)}`
5104
5299
  );
5105
5300
  }
@@ -5228,14 +5423,14 @@ function safeEngineDefault(engine) {
5228
5423
  }
5229
5424
 
5230
5425
  // src/execution/manifest-store.ts
5231
- import * as fs14 from "fs";
5426
+ import * as fs15 from "fs";
5232
5427
  import * as fsPromises2 from "fs/promises";
5233
- import * as path9 from "path";
5234
- var logger16 = getLogger("subagents");
5428
+ import * as path10 from "path";
5429
+ var logger17 = getLogger("subagents");
5235
5430
  var MANIFEST_INDENT_SPACES = 2;
5236
5431
  function statStamp(p) {
5237
5432
  try {
5238
- const s = fs14.statSync(p);
5433
+ const s = fs15.statSync(p);
5239
5434
  return { mtimeMs: s.mtimeMs, size: s.size };
5240
5435
  } catch {
5241
5436
  return null;
@@ -5263,8 +5458,8 @@ var ManifestStore = class {
5263
5458
  cache = /* @__PURE__ */ new Map();
5264
5459
  constructor(dir) {
5265
5460
  this.dir = dir;
5266
- if (!fs14.existsSync(dir)) {
5267
- fs14.mkdirSync(dir, { recursive: true });
5461
+ if (!fs15.existsSync(dir)) {
5462
+ fs15.mkdirSync(dir, { recursive: true });
5268
5463
  }
5269
5464
  }
5270
5465
  /**
@@ -5276,7 +5471,7 @@ var ManifestStore = class {
5276
5471
  * 调用方(finalizeRecord)决定降级策略。
5277
5472
  */
5278
5473
  async writeManifest(record) {
5279
- const filePath = path9.join(this.dir, `${record.id}.json`);
5474
+ const filePath = path10.join(this.dir, `${record.id}.json`);
5280
5475
  const content = JSON.stringify(record, null, MANIFEST_INDENT_SPACES);
5281
5476
  await writeAtomicFile(filePath, content, { ensureDir: false });
5282
5477
  }
@@ -5285,7 +5480,7 @@ var ManifestStore = class {
5285
5480
  * 调用方需处理 null。
5286
5481
  */
5287
5482
  async readManifest(id) {
5288
- const filePath = path9.join(this.dir, `${id}.json`);
5483
+ const filePath = path10.join(this.dir, `${id}.json`);
5289
5484
  try {
5290
5485
  const content = await fsPromises2.readFile(filePath, "utf-8");
5291
5486
  const parsed = JSON.parse(content);
@@ -5305,7 +5500,7 @@ var ManifestStore = class {
5305
5500
  listAllSync() {
5306
5501
  let files;
5307
5502
  try {
5308
- files = fs14.readdirSync(this.dir);
5503
+ files = fs15.readdirSync(this.dir);
5309
5504
  } catch {
5310
5505
  return [];
5311
5506
  }
@@ -5316,7 +5511,7 @@ var ManifestStore = class {
5316
5511
  }
5317
5512
  const results = [];
5318
5513
  for (const file of names) {
5319
- const filePath = path9.join(this.dir, file);
5514
+ const filePath = path10.join(this.dir, file);
5320
5515
  const stamp = statStamp(filePath);
5321
5516
  if (!stamp) {
5322
5517
  this.cache.delete(file);
@@ -5328,7 +5523,7 @@ var ManifestStore = class {
5328
5523
  continue;
5329
5524
  }
5330
5525
  try {
5331
- const content = fs14.readFileSync(filePath, "utf-8");
5526
+ const content = fs15.readFileSync(filePath, "utf-8");
5332
5527
  const parsed = JSON.parse(content);
5333
5528
  const record = isValidManifest(parsed) ? parsed : null;
5334
5529
  this.cache.set(file, { stamp, record });
@@ -5356,41 +5551,41 @@ var ManifestStore = class {
5356
5551
  let deleted = 0;
5357
5552
  let recovered = 0;
5358
5553
  let failed = 0;
5359
- const files = fs14.readdirSync(this.dir);
5554
+ const files = fs15.readdirSync(this.dir);
5360
5555
  const tmpFiles = files.filter((f) => f.includes(".json.tmp."));
5361
5556
  for (const tmpFile of tmpFiles) {
5362
- const tmpPath = path9.join(this.dir, tmpFile);
5557
+ const tmpPath = path10.join(this.dir, tmpFile);
5363
5558
  const manifestId = tmpFile.split(".json.tmp.")[0];
5364
- const manifestPath = path9.join(this.dir, `${manifestId}.json`);
5559
+ const manifestPath = path10.join(this.dir, `${manifestId}.json`);
5365
5560
  try {
5366
- if (fs14.existsSync(manifestPath)) {
5367
- fs14.unlinkSync(tmpPath);
5561
+ if (fs15.existsSync(manifestPath)) {
5562
+ fs15.unlinkSync(tmpPath);
5368
5563
  deleted++;
5369
5564
  } else {
5370
5565
  try {
5371
- const content = fs14.readFileSync(tmpPath, "utf-8");
5566
+ const content = fs15.readFileSync(tmpPath, "utf-8");
5372
5567
  const parsed = JSON.parse(content);
5373
5568
  if (isValidManifest(parsed)) {
5374
- fs14.renameSync(tmpPath, manifestPath);
5569
+ fs15.renameSync(tmpPath, manifestPath);
5375
5570
  recovered++;
5376
5571
  } else {
5377
- fs14.unlinkSync(tmpPath);
5572
+ fs15.unlinkSync(tmpPath);
5378
5573
  deleted++;
5379
5574
  }
5380
5575
  } catch {
5381
- fs14.unlinkSync(tmpPath);
5576
+ fs15.unlinkSync(tmpPath);
5382
5577
  deleted++;
5383
5578
  }
5384
5579
  }
5385
5580
  } catch (fileErr) {
5386
5581
  failed++;
5387
- logger16.warn(`[subagents] recoverTmpFiles: failed to recover ${tmpFile}, skipping (leftovers retry on next startup)`, {
5582
+ logger17.warn(`[subagents] recoverTmpFiles: failed to recover ${tmpFile}, skipping (leftovers retry on next startup)`, {
5388
5583
  detail: fileErr instanceof Error ? fileErr.message : String(fileErr)
5389
5584
  });
5390
5585
  }
5391
5586
  }
5392
5587
  if (failed > 0) {
5393
- logger16.warn(
5588
+ logger17.warn(
5394
5589
  `[subagents] recoverTmpFiles: ${failed} of ${tmpFiles.length} tmp file(s) could not be recovered`
5395
5590
  );
5396
5591
  }
@@ -5400,7 +5595,7 @@ var ManifestStore = class {
5400
5595
 
5401
5596
  // src/shared/agent-ref.ts
5402
5597
  import { homedir as homedir2 } from "os";
5403
- import { isAbsolute, join as join10 } from "path";
5598
+ import { isAbsolute, join as join11 } from "path";
5404
5599
  var HOME_DIR_PREFIX = "~/";
5405
5600
  function hasParentSegment(ref) {
5406
5601
  return ref.split(/[\\/]/).includes("..");
@@ -5409,7 +5604,7 @@ function normalizeRef(ref, ext) {
5409
5604
  const trimmed = ref.trim();
5410
5605
  if (!trimmed) return null;
5411
5606
  if (hasParentSegment(trimmed)) return null;
5412
- const expanded = trimmed.startsWith(HOME_DIR_PREFIX) ? join10(homedir2(), trimmed.slice(HOME_DIR_PREFIX.length)) : trimmed;
5607
+ const expanded = trimmed.startsWith(HOME_DIR_PREFIX) ? join11(homedir2(), trimmed.slice(HOME_DIR_PREFIX.length)) : trimmed;
5413
5608
  if (!isAbsolute(expanded)) return null;
5414
5609
  if (ext !== void 0 && !expanded.endsWith(ext)) return null;
5415
5610
  return expanded;
@@ -5429,7 +5624,7 @@ function normalizeWorkflowRef(ref, opts) {
5429
5624
  if (looksLikePath) {
5430
5625
  const path21 = normalizeRef(trimmed, WORKFLOW_REF_EXT);
5431
5626
  if (path21 !== null) return { kind: "path", path: path21 };
5432
- const expanded = trimmed.startsWith(HOME_DIR_PREFIX) ? join10(homedir2(), trimmed.slice(HOME_DIR_PREFIX.length)) : trimmed;
5627
+ const expanded = trimmed.startsWith(HOME_DIR_PREFIX) ? join11(homedir2(), trimmed.slice(HOME_DIR_PREFIX.length)) : trimmed;
5433
5628
  const reason = hasParentSegment(trimmed) ? "parent_segment" : !isAbsolute(expanded) ? "not_absolute" : "bad_ext";
5434
5629
  return { kind: "invalid", ref, reason };
5435
5630
  }
@@ -5463,8 +5658,11 @@ function isResumable(record) {
5463
5658
  return record.status === "running" && !hasLiveProcessHandle(record.id);
5464
5659
  }
5465
5660
 
5661
+ // src/execution/notifier.ts
5662
+ import { createHash } from "crypto";
5663
+
5466
5664
  // src/execution/notify-ledger.ts
5467
- var logger17 = getLogger("subagents");
5665
+ var logger18 = getLogger("subagents");
5468
5666
  var NOTIFY_LEDGER_CUSTOM_TYPE = "subagent-bg-notify-ledger";
5469
5667
  var NOTIFY_ACK_CUSTOM_TYPE = "subagent-bg-notify-ack";
5470
5668
  var NOTIFY_ABANDONED_CUSTOM_TYPE = "subagent-bg-notify-abandoned";
@@ -5690,7 +5888,7 @@ function createNotifyLedger(host, options) {
5690
5888
  host.appendLedgerEntry(NOTIFY_ABANDONED_CUSTOM_TYPE, { v: 1, notifyId: item.notifyId });
5691
5889
  items.delete(item.notifyId);
5692
5890
  abandonedIds.add(item.notifyId);
5693
- logger17.warn(
5891
+ logger18.warn(
5694
5892
  `Subagent "${itemLabel(item)}" notification abandoned - no receipt after ${NOTIFY_REDELIVERY_MAX_ATTEMPTS} delivery attempts; verify manually via subagents action:"list"`,
5695
5893
  { notifyId: item.notifyId, attempts: item.attempts }
5696
5894
  );
@@ -5741,7 +5939,7 @@ function createNotifyLedger(host, options) {
5741
5939
  api.checkReceipts();
5742
5940
  }
5743
5941
  function emitBucketLog(bucket, total, extra) {
5744
- logger17.warn(`notify delivery bucket [${bucket}]`, { total, ...extra });
5942
+ logger18.warn(`notify delivery bucket [${bucket}]`, { total, ...extra });
5745
5943
  }
5746
5944
  if (options?.registerSettledListener !== false) {
5747
5945
  host.onAgentSettled(() => {
@@ -5789,6 +5987,70 @@ function getBoundNotifyLedger() {
5789
5987
 
5790
5988
  // src/execution/notifier.ts
5791
5989
  var notifyLogger = getLogger("subagents");
5990
+ var BATCH_NOTIFY_ID_PREFIX = "sync-batch:";
5991
+ function buildBatchNotifyId(memberIds) {
5992
+ const digest = createHash("sha1").update([...memberIds].sort().join(",")).digest("hex");
5993
+ return `${BATCH_NOTIFY_ID_PREFIX}${digest}`;
5994
+ }
5995
+ var DEFAULT_BATCH_BUDGET = { perItemChars: 4e3, totalChars: 24e3 };
5996
+ var LIST_ONLY_FLOOR = 200;
5997
+ function computeBatchBudget(bodyLengths, perItemChars, totalChars) {
5998
+ const n = bodyLengths.length;
5999
+ const sumAfterPerItem = bodyLengths.reduce((sum, len) => sum + Math.min(len, perItemChars), 0);
6000
+ if (n === 0 || sumAfterPerItem <= totalChars) {
6001
+ return { tightened: false, effectivePerItem: perItemChars, listOnly: false };
6002
+ }
6003
+ const tight = Math.floor(totalChars / n);
6004
+ if (tight < LIST_ONLY_FLOOR) {
6005
+ return { tightened: true, effectivePerItem: 0, listOnly: true };
6006
+ }
6007
+ return { tightened: true, effectivePerItem: Math.min(tight, perItemChars), listOnly: false };
6008
+ }
6009
+ function formatChars(n) {
6010
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
6011
+ }
6012
+ var SESSION_READ_DEFAULT_LIMIT = 8e3;
6013
+ function buildTruncationPointer(id, kept, total, perItemLimit) {
6014
+ const limitPart = perItemLimit > SESSION_READ_DEFAULT_LIMIT ? `,"limit":${perItemLimit}` : "";
6015
+ return `[truncated ${formatChars(total - kept)} of ${formatChars(total)} chars \u2014 full result: session_read {"action":"result","session":"${id}"${limitPart}}]`;
6016
+ }
6017
+ function withMaterializedOutcome(record) {
6018
+ return record.status === "closed" ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error) } : record;
6019
+ }
6020
+ function buildBatchLlmContent(records, budget = DEFAULT_BATCH_BUDGET) {
6021
+ const mat = records.map(withMaterializedOutcome);
6022
+ let finished = 0;
6023
+ let failed = 0;
6024
+ let cancelled = 0;
6025
+ const bodies = mat.map((record) => {
6026
+ if (record.status === "closed" && record.outcome !== "completed") return null;
6027
+ return record.result ?? "(empty)";
6028
+ });
6029
+ const plan = computeBatchBudget(
6030
+ bodies.map((body) => body?.length ?? 0),
6031
+ budget.perItemChars,
6032
+ budget.totalChars
6033
+ );
6034
+ const items = mat.map((record, i) => {
6035
+ if (record.status === "closed") {
6036
+ const outcome = record.outcome;
6037
+ if (outcome === "completed") finished += 1;
6038
+ else if (outcome === "failed") failed += 1;
6039
+ else if (outcome === "cancelled") cancelled += 1;
6040
+ }
6041
+ const body = bodies[i];
6042
+ if (body === null || body.length <= plan.effectivePerItem) {
6043
+ return buildLlmContent(record);
6044
+ }
6045
+ const kept = body.slice(0, plan.effectivePerItem);
6046
+ const truncated = buildLlmContent({ ...record, result: plan.listOnly ? "" : `${kept}\u2026` });
6047
+ const stripped = plan.listOnly ? truncated.replace(/\n$/, "") : truncated;
6048
+ return `${stripped}
6049
+ ${buildTruncationPointer(record.id, kept.length, body.length, plan.effectivePerItem)}`;
6050
+ });
6051
+ const header = `Subagent batch completed: ${finished} finished, ${failed} failed, ${cancelled} cancelled.`;
6052
+ return [header, ...items].join("\n\n---\n\n");
6053
+ }
5792
6054
  function buildLlmContent(record) {
5793
6055
  const agent = record.agent;
5794
6056
  const id = record.id;
@@ -5906,31 +6168,45 @@ function createNotifier(host) {
5906
6168
  });
5907
6169
  };
5908
6170
  let handle = createHandle();
6171
+ function deliverViaLedgerOrKernel(notifyId, content, details) {
6172
+ const ledger = getBoundNotifyLedger();
6173
+ if (ledger) {
6174
+ if (!ledger.record(notifyId, content, details)) return false;
6175
+ ledger.attemptDeliver();
6176
+ return true;
6177
+ }
6178
+ notifyLogger.warn("notify ledger not bound, falling back to delivery kernel path (at-most-once)", {
6179
+ notifyId
6180
+ });
6181
+ handle.send({
6182
+ payload: {
6183
+ kind: "custom",
6184
+ customType: NOTIFY_CUSTOM_TYPE,
6185
+ content,
6186
+ display: true,
6187
+ details
6188
+ },
6189
+ dedupeKey: notifyId
6190
+ });
6191
+ return true;
6192
+ }
5909
6193
  return {
5910
6194
  notify(record) {
5911
6195
  if (disposed) return;
5912
6196
  const notifyId = record.round != null ? `${record.id}:${record.round}` : record.id;
5913
6197
  const payload = record.status === "closed" ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error), notifyId } : { ...record, notifyId };
5914
6198
  const content = buildLlmContent(payload);
5915
- const ledger = getBoundNotifyLedger();
5916
- if (ledger) {
5917
- if (!ledger.record(notifyId, content, payload)) return;
5918
- ledger.attemptDeliver();
5919
- return;
5920
- }
5921
- notifyLogger.warn("notify ledger not bound, falling back to delivery kernel path (at-most-once)", {
5922
- notifyId
5923
- });
5924
- handle.send({
5925
- payload: {
5926
- kind: "custom",
5927
- customType: NOTIFY_CUSTOM_TYPE,
5928
- content,
5929
- display: true,
5930
- details: payload
5931
- },
5932
- dedupeKey: notifyId
5933
- });
6199
+ deliverViaLedgerOrKernel(notifyId, content, payload);
6200
+ },
6201
+ notifyBatch(records, budget) {
6202
+ if (disposed || records.length === 0) return false;
6203
+ const payloads = records.map(
6204
+ (record) => record.status === "closed" ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error) } : { ...record }
6205
+ );
6206
+ const batchNotifyId = buildBatchNotifyId(payloads.map((p) => p.id));
6207
+ const content = buildBatchLlmContent(payloads, budget);
6208
+ const details = { batch: true, notifyId: batchNotifyId, items: payloads };
6209
+ return deliverViaLedgerOrKernel(batchNotifyId, content, details);
5934
6210
  },
5935
6211
  flushPendingNotifications() {
5936
6212
  const ledger = getBoundNotifyLedger();
@@ -6054,6 +6330,15 @@ function createNotifyHost(deps) {
6054
6330
  emitPendingUnregister(id, reason) {
6055
6331
  emitPendingUnregister(deps.getPi(), id, reason);
6056
6332
  },
6333
+ toNotifyRecord(record) {
6334
+ return toNotifyRecord(record);
6335
+ },
6336
+ notify(record) {
6337
+ notifier.notify(record);
6338
+ },
6339
+ notifyBatch(records, budget) {
6340
+ return notifier.notifyBatch(records, budget);
6341
+ },
6057
6342
  revive() {
6058
6343
  notifier.revive();
6059
6344
  },
@@ -6067,7 +6352,7 @@ function createNotifyHost(deps) {
6067
6352
  }
6068
6353
 
6069
6354
  // src/execution/round-settlement.ts
6070
- var logger18 = getLogger("subagents");
6355
+ var logger19 = getLogger("subagents");
6071
6356
  function createRoundSettler(deps) {
6072
6357
  return (record) => {
6073
6358
  record.round = (record.round ?? 0) + 1;
@@ -6076,7 +6361,7 @@ function createRoundSettler(deps) {
6076
6361
  deps.notifyComplete(record);
6077
6362
  const lastTurn = record.turns[record.turns.length - 1];
6078
6363
  if (lastTurn !== void 0 && !lastTurn.closed && lastTurn.text.length > 0) {
6079
- logger18.warn(
6364
+ logger19.warn(
6080
6365
  `[subagents] round settle with unclosed non-empty turn (record=${record.id}, turnIndex=${record.turns.length - 1}) \u2014 pi turn_end/agent_end ordering may have changed`
6081
6366
  );
6082
6367
  }
@@ -6090,8 +6375,8 @@ function createRoundSettler(deps) {
6090
6375
  }
6091
6376
 
6092
6377
  // src/execution/record-store.ts
6093
- import * as fs16 from "fs";
6094
- import * as path11 from "path";
6378
+ import * as fs17 from "fs";
6379
+ import * as path12 from "path";
6095
6380
 
6096
6381
  // src/execution/record-entry.ts
6097
6382
  var SUBAGENT_RECORD_CUSTOM_TYPE = "subagent-record";
@@ -6126,14 +6411,18 @@ function toSubagentRecordEntry(record) {
6126
6411
  resumable: record.resumable,
6127
6412
  engine: record.engine,
6128
6413
  engineFallback: record.engineFallback,
6129
- engineHandle: record.engineHandle
6414
+ engineHandle: record.engineHandle,
6415
+ // 同步收集两字段(U1 foundation):undefined 经 JSON.stringify 自然缺省,
6416
+ // 旧记录/旧 entry 序列化产物字节不变(零迁移)。
6417
+ collectMode: record.collectMode,
6418
+ batchFinalized: record.batchFinalized
6130
6419
  };
6131
6420
  }
6132
6421
 
6133
6422
  // src/execution/sessions-index.ts
6134
- import * as fs15 from "fs";
6135
- import * as path10 from "path";
6136
- var logger19 = getLogger("subagents");
6423
+ import * as fs16 from "fs";
6424
+ import * as path11 from "path";
6425
+ var logger20 = getLogger("subagents");
6137
6426
  var INDEX_FILENAME = "sessions-index.json";
6138
6427
  var INDEX_VERSION = 1;
6139
6428
  var INDEX_WRITE_MIN_INTERVAL_MS = 6e4;
@@ -6172,11 +6461,11 @@ function errorCodeOf(err) {
6172
6461
  }
6173
6462
  function readIndexFile(indexPath, encDir) {
6174
6463
  try {
6175
- return fs15.readFileSync(indexPath, "utf-8");
6464
+ return fs16.readFileSync(indexPath, "utf-8");
6176
6465
  } catch (err) {
6177
6466
  const code = errorCodeOf(err);
6178
6467
  if (code !== "ENOENT") {
6179
- logger19.debug("[subagents] sessions-index read failed, fallback to empty", {
6468
+ logger20.debug("[subagents] sessions-index read failed, fallback to empty", {
6180
6469
  detail: { dir: encDir, code }
6181
6470
  });
6182
6471
  }
@@ -6187,7 +6476,7 @@ function parseIndexJson(raw, indexPath) {
6187
6476
  try {
6188
6477
  return JSON.parse(raw);
6189
6478
  } catch (err) {
6190
- logger19.debug("[subagents] sessions-index corrupted JSON, fallback to empty", {
6479
+ logger20.debug("[subagents] sessions-index corrupted JSON, fallback to empty", {
6191
6480
  detail: { path: indexPath, error: err instanceof Error ? err.message : String(err) }
6192
6481
  });
6193
6482
  return null;
@@ -6198,14 +6487,14 @@ function isIndexTopHeader(v) {
6198
6487
  }
6199
6488
  function readTopLevel(parsed, indexPath) {
6200
6489
  if (typeof parsed !== "object" || parsed === null) {
6201
- logger19.debug("[subagents] sessions-index invalid top-level shape, fallback to empty", {
6490
+ logger20.debug("[subagents] sessions-index invalid top-level shape, fallback to empty", {
6202
6491
  detail: { path: indexPath }
6203
6492
  });
6204
6493
  return null;
6205
6494
  }
6206
6495
  const top = parsed;
6207
6496
  if (!isIndexTopHeader(top)) {
6208
- logger19.debug("[subagents] sessions-index invalid header fields, fallback to empty", {
6497
+ logger20.debug("[subagents] sessions-index invalid header fields, fallback to empty", {
6209
6498
  detail: { path: indexPath }
6210
6499
  });
6211
6500
  return null;
@@ -6222,7 +6511,7 @@ function collectValidEntries(entriesObj) {
6222
6511
  }
6223
6512
  function loadIndex(encDir) {
6224
6513
  const empty = { entries: /* @__PURE__ */ new Map(), higherVersion: false };
6225
- const indexPath = path10.join(encDir, INDEX_FILENAME);
6514
+ const indexPath = path11.join(encDir, INDEX_FILENAME);
6226
6515
  const raw = readIndexFile(indexPath, encDir);
6227
6516
  if (raw === null) return empty;
6228
6517
  const parsed = parseIndexJson(raw, indexPath);
@@ -6233,7 +6522,7 @@ function loadIndex(encDir) {
6233
6522
  return { entries: /* @__PURE__ */ new Map(), higherVersion: true };
6234
6523
  }
6235
6524
  if (top.version < INDEX_VERSION) {
6236
- logger19.debug("[subagents] sessions-index stale version, discarded", {
6525
+ logger20.debug("[subagents] sessions-index stale version, discarded", {
6237
6526
  detail: { path: indexPath, version: top.version, expected: INDEX_VERSION }
6238
6527
  });
6239
6528
  return empty;
@@ -6241,7 +6530,7 @@ function loadIndex(encDir) {
6241
6530
  return { entries: collectValidEntries(top.entries), higherVersion: false };
6242
6531
  }
6243
6532
  async function saveIndex(encDir, data) {
6244
- const filePath = path10.join(encDir, INDEX_FILENAME);
6533
+ const filePath = path11.join(encDir, INDEX_FILENAME);
6245
6534
  const file = {
6246
6535
  version: INDEX_VERSION,
6247
6536
  pid: process.pid,
@@ -6251,7 +6540,7 @@ async function saveIndex(encDir, data) {
6251
6540
  }
6252
6541
 
6253
6542
  // src/execution/record-store.ts
6254
- var logger20 = getLogger("subagents");
6543
+ var logger21 = getLogger("subagents");
6255
6544
  var STATUS_PRIORITY = {
6256
6545
  running: 0,
6257
6546
  closed: 3
@@ -6270,14 +6559,14 @@ var MIN_SEGMENTS_WITH_BOUNDARY = 2;
6270
6559
  function readLastJsonlLine(sessionFile) {
6271
6560
  let fd;
6272
6561
  try {
6273
- fd = fs16.openSync(sessionFile, "r");
6274
- const size = fs16.fstatSync(fd).size;
6562
+ fd = fs17.openSync(sessionFile, "r");
6563
+ const size = fs17.fstatSync(fd).size;
6275
6564
  let windowBytes = LAST_LINE_WINDOW_BYTES;
6276
6565
  let windowStart = Math.max(0, size - windowBytes);
6277
6566
  let lines = [];
6278
6567
  while (true) {
6279
6568
  const buf = Buffer.alloc(size - windowStart);
6280
- fs16.readSync(fd, buf, 0, buf.length, windowStart);
6569
+ fs17.readSync(fd, buf, 0, buf.length, windowStart);
6281
6570
  lines = buf.toString("utf-8").split("\n").filter((l) => l.length > 0);
6282
6571
  if (windowStart === 0 || lines.length >= MIN_SEGMENTS_WITH_BOUNDARY) break;
6283
6572
  windowBytes *= WINDOW_GROWTH_FACTOR;
@@ -6294,7 +6583,7 @@ function readLastJsonlLine(sessionFile) {
6294
6583
  } finally {
6295
6584
  if (fd !== void 0) {
6296
6585
  try {
6297
- fs16.closeSync(fd);
6586
+ fs17.closeSync(fd);
6298
6587
  } catch (_e) {
6299
6588
  void _e;
6300
6589
  }
@@ -6317,7 +6606,7 @@ function collectLastRecordEntries(content) {
6317
6606
  try {
6318
6607
  entry = asSubagentRecordEntry(JSON.parse(line));
6319
6608
  } catch (err) {
6320
- logger20.debug("[subagents] entry-only orphan scan: skip unparsable line", {
6609
+ logger21.debug("[subagents] entry-only orphan scan: skip unparsable line", {
6321
6610
  reason: err instanceof Error ? err.message : String(err)
6322
6611
  });
6323
6612
  }
@@ -6325,37 +6614,64 @@ function collectLastRecordEntries(content) {
6325
6614
  }
6326
6615
  return lastById;
6327
6616
  }
6617
+ function entryStr(d, k) {
6618
+ return typeof d[k] === "string" ? d[k] : void 0;
6619
+ }
6620
+ function entryNum(d, k) {
6621
+ return typeof d[k] === "number" ? d[k] : void 0;
6622
+ }
6623
+ function readEntryTerminalFields(d) {
6624
+ const closedReason = entryStr(d, "closedReason");
6625
+ return {
6626
+ status: d.status === "closed" ? "closed" : "running",
6627
+ closedReason: isValidClosedReason(closedReason) ? closedReason : void 0
6628
+ };
6629
+ }
6630
+ function readEntryBatchFields(d) {
6631
+ return {
6632
+ collectMode: d.collectMode === "sync" ? "sync" : void 0,
6633
+ batchFinalized: d.batchFinalized === true ? true : void 0,
6634
+ resumable: d.resumable === true ? true : void 0
6635
+ };
6636
+ }
6637
+ function readEntryEngineFields(d) {
6638
+ return {
6639
+ engine: entryStr(d, "engine"),
6640
+ engineFallback: isEngineFallbackShape(d.engineFallback) ? d.engineFallback : void 0,
6641
+ engineHandle: isEngineHandleShape(d.engineHandle) ? d.engineHandle : void 0
6642
+ };
6643
+ }
6328
6644
  function rebuildEntryRecord(id, d) {
6329
- const str = (k) => typeof d[k] === "string" ? d[k] : void 0;
6330
- const num = (k) => typeof d[k] === "number" ? d[k] : void 0;
6331
- const agent = str("agent");
6332
- const task = str("task");
6333
- const startedAt = num("startedAt");
6645
+ const agent = entryStr(d, "agent");
6646
+ const task = entryStr(d, "task");
6647
+ const startedAt = entryNum(d, "startedAt");
6334
6648
  if (agent === void 0 || task === void 0 || startedAt === void 0) return null;
6335
6649
  return {
6336
6650
  id,
6337
6651
  agent,
6338
6652
  task,
6339
- slug: str("slug") ?? "",
6340
- status: "running",
6653
+ slug: entryStr(d, "slug") ?? "",
6654
+ ...readEntryTerminalFields(d),
6341
6655
  mode: "background",
6342
6656
  startedAt,
6343
- rootSessionId: str("rootSessionId"),
6344
- parentRecordId: str("parentRecordId"),
6345
- depth: num("depth") ?? 0,
6346
- endedAt: void 0,
6347
- turns: num("turns") ?? 0,
6348
- totalTokens: num("totalTokens") ?? 0,
6349
- model: str("model") ?? "",
6350
- thinkingLevel: str("thinkingLevel"),
6657
+ rootSessionId: entryStr(d, "rootSessionId"),
6658
+ parentRecordId: entryStr(d, "parentRecordId"),
6659
+ depth: entryNum(d, "depth") ?? 0,
6660
+ endedAt: entryNum(d, "endedAt"),
6661
+ turns: entryNum(d, "turns") ?? 0,
6662
+ totalTokens: entryNum(d, "totalTokens") ?? 0,
6663
+ model: entryStr(d, "model") ?? "",
6664
+ thinkingLevel: entryStr(d, "thinkingLevel"),
6351
6665
  eventLog: [],
6352
6666
  displayItems: [],
6353
- sessionFile: void 0,
6667
+ result: entryStr(d, "result"),
6668
+ error: entryStr(d, "error"),
6669
+ sessionFile: entryStr(d, "sessionFile"),
6670
+ patchFile: entryStr(d, "patchFile"),
6354
6671
  chatMode: d.chatMode === true,
6355
- round: num("round"),
6356
- engine: str("engine"),
6357
- engineFallback: isEngineFallbackShape(d.engineFallback) ? d.engineFallback : void 0,
6358
- engineHandle: isEngineHandleShape(d.engineHandle) ? d.engineHandle : void 0
6672
+ round: entryNum(d, "round"),
6673
+ ...readEntryEngineFields(d),
6674
+ ...readEntryBatchFields(d)
6359
6675
  };
6360
6676
  }
6361
6677
  function isEngineFallbackShape(v) {
@@ -6378,7 +6694,7 @@ function isEngineHandleShape(v) {
6378
6694
  }
6379
6695
  function statStamp2(p) {
6380
6696
  try {
6381
- const s = fs16.statSync(p);
6697
+ const s = fs17.statSync(p);
6382
6698
  return { mtimeMs: s.mtimeMs, size: s.size };
6383
6699
  } catch {
6384
6700
  return null;
@@ -6554,7 +6870,7 @@ var RecordStore = class _RecordStore {
6554
6870
  if (rootSessionFilter !== void 0 && manifest.rootSessionId !== rootSessionFilter) continue;
6555
6871
  const rec = _RecordStore.manifestToSubagent(manifest);
6556
6872
  if (!rec) {
6557
- logger20.warn("[subagents] skip manifest with invalid status", {
6873
+ logger21.warn("[subagents] skip manifest with invalid status", {
6558
6874
  detail: { id: manifest.id, status: manifest.status }
6559
6875
  });
6560
6876
  this.pi?.appendEntry?.("subagent:manifest-invalid-status", {
@@ -6601,31 +6917,44 @@ var RecordStore = class _RecordStore {
6601
6917
  * - 文件不可读(IO 错误,可能暂时)→ 不判终态,落 resumable entry(防御性路径,
6602
6918
  * IO 恢复后重开可重判)。
6603
6919
  *
6920
+ * [v2 D3] mainSessionFile = 覆写 merge 数据源(主 session 每 id 末条 entry,与 E1
6921
+ * 的 scanLastRecordEntries 同款通路):崩溃前的批域标记(collectMode/batchFinalized)
6922
+ * 与轮终 result/model 只活在主文件 entry——重建矩阵(buildRecord)的数据源
6923
+ * (sidecar/子文件 identity)不含它们,覆写前不 merge 就会被抹掉(v2 §2.2 断链 2:
6924
+ * E1 候选集恒空的真根因)。缺省(undefined)时 merge 无源,行为与旧版一致。
6925
+ * 参数为追加式第二参(rootSessionFilter 保持首参):既有调用面只传过滤参。
6926
+ *
6604
6927
  * 防重:orphanJudged 实例级缓存(resumable 形态无 sidecar 锚,同进程重复调用跳过;
6605
6928
  * 终态形态双重防护 = sidecar + 缓存)。调用方:index.ts session_start 恢复段(一次)。
6606
6929
  */
6607
- recoverOrphanRecords(rootSessionFilter) {
6930
+ recoverOrphanRecords(rootSessionFilter, mainSessionFile) {
6931
+ const lastById = new Map(this.scanLastRecordEntries(mainSessionFile).map((r) => [r.id, r]));
6608
6932
  for (const rec of this.reconstructAll(rootSessionFilter)) {
6609
6933
  if (rec.status !== "running" || rec.externalInstance !== void 0) continue;
6610
6934
  if (this.orphanJudged.has(rec.id)) continue;
6611
6935
  this.orphanJudged.add(rec.id);
6612
- this.finalizeOrphanRecord(rec);
6936
+ this.finalizeOrphanRecord(rec, lastById.get(rec.id));
6613
6937
  }
6614
6938
  }
6615
6939
  /**
6616
6940
  * 单孤儿 record 的终态判定与落 entry(residual-fixes §5.2 三判据 + chat 分流)。
6617
6941
  * 防重锚(orphanJudged 标记)已由调用方完成。
6942
+ *
6943
+ * [v2 D3] 覆写前 merge(lastEntry = 主 session 同 id 末条 entry 重建,调用方构建):
6944
+ * 覆写是状态迁移不是信息重建,迁移不应丢末条既有信息。三个落 entry 分支(chatMode
6945
+ * 分流 / IO 保守 / 终态覆写)统一基于 merge 后的 rec,同款口径。
6618
6946
  */
6619
- finalizeOrphanRecord(rec) {
6620
- if (rec.chatMode === true) {
6621
- this.reportSubagentRecord({ ...rec, resumable: true });
6947
+ finalizeOrphanRecord(rec, lastEntry) {
6948
+ const rec0 = lastEntry === void 0 ? rec : _RecordStore.mergeOrphanLastEntry(rec, lastEntry);
6949
+ if (rec0.chatMode === true) {
6950
+ this.reportSubagentRecord({ ...rec0, resumable: true });
6622
6951
  return;
6623
6952
  }
6624
- const sessionFile = rec.sessionFile;
6953
+ const sessionFile = rec0.sessionFile;
6625
6954
  if (sessionFile === void 0) return;
6626
6955
  const lastLine = readLastJsonlLine(sessionFile);
6627
6956
  if (!lastLine.ok) {
6628
- this.reportSubagentRecord({ ...rec, resumable: true });
6957
+ this.reportSubagentRecord({ ...rec0, resumable: true });
6629
6958
  return;
6630
6959
  }
6631
6960
  let parseOk = false;
@@ -6637,13 +6966,30 @@ var RecordStore = class _RecordStore {
6637
6966
  }
6638
6967
  writeFinalized(sessionFile, "gc");
6639
6968
  this.reportSubagentRecord({
6640
- ...rec,
6969
+ ...rec0,
6641
6970
  status: "closed",
6642
6971
  closedReason: "gc",
6643
6972
  endedAt: Date.now(),
6644
6973
  ...parseOk ? {} : { error: "orphan recovery: subagent session ended abnormally (truncated last line)" }
6645
6974
  });
6646
6975
  }
6976
+ /** [v2 D3] 孤儿覆写 merge 字段集:末条 entry 的批域标记 + 轮终正文/模型,仅补 rec 侧
6977
+ * undefined/空值(model 的空值形态是 ""——light 重建无 model_change entry 时起步
6978
+ * 空串),不覆盖已有值。merge 后写 entry 经 reportSubagentRecord →
6979
+ * toSubagentRecordEntry 序列化,undefined 字段自然缺省(不引入显式 null)。 */
6980
+ static mergeOrphanLastEntry(rec, last) {
6981
+ const pickStr = (cur, src) => cur !== void 0 && cur !== "" ? cur : src;
6982
+ return {
6983
+ ...rec,
6984
+ collectMode: rec.collectMode ?? last.collectMode,
6985
+ batchFinalized: rec.batchFinalized ?? last.batchFinalized,
6986
+ result: pickStr(rec.result, last.result),
6987
+ // 类型收尾:两侧实参恒 string(rec.model 类型非可选;last.model 重建投影自带
6988
+ // `?? ""`),pickStr 签名宽返回 string|undefined —— `?? ""` 运行时不可达,
6989
+ // 仅满足 model 非可选类型,空串回退语义不变(cur 空 → src,src 也空 → "")。
6990
+ model: pickStr(rec.model, last.model) ?? ""
6991
+ };
6992
+ }
6647
6993
  /**
6648
6994
  * [E2E 实测缺口] entry-born 孤儿恢复:register entry 已落主 session、但子 session 文件
6649
6995
  * 从未创建(父进程死在 spawn 窗口期——register 写点与子进程首笔写入之间的窗口;外部
@@ -6660,7 +7006,7 @@ var RecordStore = class _RecordStore {
6660
7006
  if (mainSessionFile === void 0) return;
6661
7007
  let content;
6662
7008
  try {
6663
- content = fs16.readFileSync(mainSessionFile, "utf-8");
7009
+ content = fs17.readFileSync(mainSessionFile, "utf-8");
6664
7010
  } catch {
6665
7011
  return;
6666
7012
  }
@@ -6699,6 +7045,31 @@ var RecordStore = class _RecordStore {
6699
7045
  }
6700
7046
  });
6701
7047
  }
7048
+ /**
7049
+ * [E1/U5] sync 批崩溃恢复扫描:主 session 文件「每 id 末条 subagent-record entry」
7050
+ * (collectLastRecordEntries + rebuildEntryRecord 组合通路,设计 §3.1.3「标记读取
7051
+ * 通路」——batchFinalized 落标 entry 写主 session 文件,本扫描同文件域才可见;禁走
7052
+ * collectRecords light 路径,它只读子文件 identity 头+sidecar,主 session 落标
7053
+ * entry 不可见)。返回每 id 末条重建的完整 record(含 collectMode/batchFinalized /
7054
+ * 终态五字段,损坏 entry 跳过);调用方(service.recoverSyncCollectBatch 的 E1 过滤、
7055
+ * recoverOrphanRecords 的覆写 merge)自行取舍。主文件不可读(含新 session 未 flush
7056
+ * 的 ENOENT)→ 空数组静默。
7057
+ */
7058
+ scanLastRecordEntries(mainSessionFile) {
7059
+ if (mainSessionFile === void 0) return [];
7060
+ let content;
7061
+ try {
7062
+ content = fs17.readFileSync(mainSessionFile, "utf-8");
7063
+ } catch {
7064
+ return [];
7065
+ }
7066
+ const out = [];
7067
+ for (const [id, d] of collectLastRecordEntries(content)) {
7068
+ const rec = rebuildEntryRecord(id, d);
7069
+ if (rec !== null) out.push(rec);
7070
+ }
7071
+ return out;
7072
+ }
6702
7073
  /** 订阅变更。返回取消订阅函数。 */
6703
7074
  onChange(listener) {
6704
7075
  this.listeners.add(listener);
@@ -6761,12 +7132,12 @@ var RecordStore = class _RecordStore {
6761
7132
  reconstructAll(rootSessionFilter) {
6762
7133
  let dirMtimeMs;
6763
7134
  try {
6764
- dirMtimeMs = fs16.statSync(this.sessionsDir).mtimeMs;
7135
+ dirMtimeMs = fs17.statSync(this.sessionsDir).mtimeMs;
6765
7136
  } catch {
6766
7137
  return [];
6767
7138
  }
6768
7139
  if (this.dirStamp === null) {
6769
- const loaded = loadIndex(path11.dirname(this.sessionsDir));
7140
+ const loaded = loadIndex(path12.dirname(this.sessionsDir));
6770
7141
  this.indexEntries = loaded.entries;
6771
7142
  this.indexHigherVersion = loaded.higherVersion;
6772
7143
  }
@@ -6782,7 +7153,7 @@ var RecordStore = class _RecordStore {
6782
7153
  }
6783
7154
  let files;
6784
7155
  try {
6785
- files = fs16.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl")).map((f) => path11.join(this.sessionsDir, f));
7156
+ files = fs17.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl")).map((f) => path12.join(this.sessionsDir, f));
6786
7157
  } catch {
6787
7158
  this.indexEntries = null;
6788
7159
  return [];
@@ -6856,7 +7227,7 @@ var RecordStore = class _RecordStore {
6856
7227
  */
6857
7228
  buildEntryFromIndex(file, stamps, now) {
6858
7229
  if (this.indexEntries === null) return void 0;
6859
- const hit = this.indexEntries.get(path11.basename(file));
7230
+ const hit = this.indexEntries.get(path12.basename(file));
6860
7231
  if (hit === void 0 || hit.mtimeMs !== stamps.jsonl.mtimeMs || hit.size !== stamps.jsonl.size) {
6861
7232
  return void 0;
6862
7233
  }
@@ -6888,12 +7259,12 @@ var RecordStore = class _RecordStore {
6888
7259
  if (Date.now() - this.lastIndexWriteAt < INDEX_WRITE_MIN_INTERVAL_MS) return;
6889
7260
  const entries = this.projectIndexEntries();
6890
7261
  this.indexDirty = false;
6891
- const encDir = path11.dirname(this.sessionsDir);
7262
+ const encDir = path12.dirname(this.sessionsDir);
6892
7263
  saveIndex(encDir, { entries }).then(() => {
6893
7264
  this.lastIndexWriteAt = Date.now();
6894
7265
  }).catch((err) => {
6895
7266
  this.indexDirty = true;
6896
- logger20.warn("[subagents] sessions-index write failed", {
7267
+ logger21.warn("[subagents] sessions-index write failed", {
6897
7268
  detail: { dir: encDir, error: err instanceof Error ? err.message : String(err) }
6898
7269
  });
6899
7270
  });
@@ -6907,7 +7278,7 @@ var RecordStore = class _RecordStore {
6907
7278
  projectIndexEntries() {
6908
7279
  const entries = /* @__PURE__ */ new Map();
6909
7280
  for (const [file, cached] of this.fileCache) {
6910
- const base = path11.basename(file);
7281
+ const base = path12.basename(file);
6911
7282
  if (cached.negative) {
6912
7283
  entries.set(base, { negative: true, mtimeMs: cached.jsonl.mtimeMs, size: cached.jsonl.size });
6913
7284
  } else {
@@ -7162,15 +7533,65 @@ var RecordStore = class _RecordStore {
7162
7533
  engine: r.engine,
7163
7534
  engineFallback: r.engineFallback,
7164
7535
  // U2:engineHandle 经 entry 持久化(register/archive 双写点均经本投影),无则 undefined 自然省略
7165
- engineHandle: r.engineHandle
7536
+ engineHandle: r.engineHandle,
7537
+ // [U5 修复 U2 披露的投影缺口] 同步收集两字段随本投影持久化(register entry /
7538
+ // archive entry 双写点)——原缺失时闭合判定 flushBatch 重建、E1 重建扫描等消费方
7539
+ // 读不到原始值。undefined 经 JSON.stringify 自然缺省,旧 entry 零迁移。
7540
+ collectMode: r.collectMode,
7541
+ batchFinalized: r.batchFinalized
7166
7542
  };
7167
7543
  }
7168
7544
  };
7169
7545
 
7546
+ // src/execution/sync-rebuild.ts
7547
+ function syncRebuildToNotifyMember(rec) {
7548
+ const status = rec.status === "closed" || !rec.chatMode ? "closed" : "running";
7549
+ return {
7550
+ id: rec.id,
7551
+ status,
7552
+ closedReason: rec.closedReason,
7553
+ outcome: status === "closed" ? deriveOutcome(rec.closedReason, rec.error) : void 0,
7554
+ agent: rec.agent,
7555
+ model: rec.model,
7556
+ result: rec.result,
7557
+ error: rec.error,
7558
+ startedAt: rec.startedAt,
7559
+ endedAt: rec.endedAt,
7560
+ patchFile: rec.patchFile
7561
+ };
7562
+ }
7563
+ function bufferedMemberFallbackRecord(m, rootSessionId) {
7564
+ return {
7565
+ id: m.id,
7566
+ agent: m.agent,
7567
+ task: "",
7568
+ slug: "",
7569
+ status: "closed",
7570
+ closedReason: m.closedReason,
7571
+ mode: "background",
7572
+ startedAt: m.startedAt,
7573
+ rootSessionId,
7574
+ parentRecordId: void 0,
7575
+ depth: 0,
7576
+ endedAt: m.endedAt,
7577
+ turns: 0,
7578
+ totalTokens: 0,
7579
+ model: m.model ?? "",
7580
+ thinkingLevel: void 0,
7581
+ eventLog: [],
7582
+ displayItems: [],
7583
+ result: m.result,
7584
+ error: m.error,
7585
+ sessionFile: void 0,
7586
+ chatMode: false,
7587
+ patchFile: m.patchFile
7588
+ };
7589
+ }
7590
+
7170
7591
  // src/execution/engine/common/pool-manager.ts
7171
7592
  import * as fsSync from "fs";
7172
- import { join as join13 } from "path";
7173
- var logger21 = getLogger("subagents");
7593
+ import { join as join14 } from "path";
7594
+ var logger22 = getLogger("subagents");
7174
7595
  var POOL_CLEANUP_FAILED_MARKER = ".pool-cleanup-failed";
7175
7596
  var REFS_JSON_FILENAME = "refs.json";
7176
7597
  var REFS_VERSION = 1;
@@ -7180,13 +7601,13 @@ function emptyRefs() {
7180
7601
  return { v: REFS_VERSION, refs: {} };
7181
7602
  }
7182
7603
  function readPoolRefs(poolDir, fs27) {
7183
- const refsPath = join13(poolDir, REFS_JSON_FILENAME);
7604
+ const refsPath = join14(poolDir, REFS_JSON_FILENAME);
7184
7605
  if (!fs27.existsSync(refsPath)) return emptyRefs();
7185
7606
  let parsed;
7186
7607
  try {
7187
7608
  parsed = JSON.parse(fs27.readFileSync(refsPath));
7188
7609
  } catch (err) {
7189
- logger21.warn(
7610
+ logger22.warn(
7190
7611
  `[pool-manager] refs.json unparsable for ${poolDir}, starting from empty refs: ${toErrorMessage(err)}`
7191
7612
  );
7192
7613
  return emptyRefs();
@@ -7194,7 +7615,7 @@ function readPoolRefs(poolDir, fs27) {
7194
7615
  if (typeof parsed !== "object" || parsed === null) return emptyRefs();
7195
7616
  const obj = parsed;
7196
7617
  if (obj.v !== REFS_VERSION || typeof obj.refs !== "object" || obj.refs === null) {
7197
- logger21.warn(`[pool-manager] refs.json unexpected shape for ${poolDir}, starting from empty refs`);
7618
+ logger22.warn(`[pool-manager] refs.json unexpected shape for ${poolDir}, starting from empty refs`);
7198
7619
  return emptyRefs();
7199
7620
  }
7200
7621
  const refs = {};
@@ -7207,7 +7628,7 @@ function readPoolRefs(poolDir, fs27) {
7207
7628
  return { v: REFS_VERSION, refs };
7208
7629
  }
7209
7630
  function writePoolRefs(poolDir, file, fs27) {
7210
- const refsPath = join13(poolDir, REFS_JSON_FILENAME);
7631
+ const refsPath = join14(poolDir, REFS_JSON_FILENAME);
7211
7632
  const tmpPath = `${refsPath}.tmp.${process.pid}`;
7212
7633
  try {
7213
7634
  fs27.writeFileSync(tmpPath, `${JSON.stringify(file)}
@@ -7228,7 +7649,7 @@ function releasePoolRef(dataDir, engineId, poolKey, taskId, fs27 = nodeFs) {
7228
7649
  removeJournalFile(dataDir, engineId, poolKey, taskId, fs27);
7229
7650
  const file = readPoolRefs(poolDir, fs27);
7230
7651
  if (file.refs[taskId] === void 0) {
7231
- logger21.debug(`[pool-manager] release without live ref, skip pool deletion: ${poolDir} (${taskId})`);
7652
+ logger22.debug(`[pool-manager] release without live ref, skip pool deletion: ${poolDir} (${taskId})`);
7232
7653
  return;
7233
7654
  }
7234
7655
  delete file.refs[taskId];
@@ -7259,7 +7680,7 @@ function cleanupExpiredPoolRefs(dataDir, ttlMs, fs27 = nodeFs, now = Date.now())
7259
7680
  }
7260
7681
  for (const engineEntry of engines) {
7261
7682
  if (!engineEntry.isDirectory()) continue;
7262
- const engineDir = join13(enginesRoot, engineEntry.name);
7683
+ const engineDir = join14(enginesRoot, engineEntry.name);
7263
7684
  let pools;
7264
7685
  try {
7265
7686
  pools = fs27.readdirSync(engineDir);
@@ -7268,7 +7689,7 @@ function cleanupExpiredPoolRefs(dataDir, ttlMs, fs27 = nodeFs, now = Date.now())
7268
7689
  }
7269
7690
  for (const poolEntry of pools) {
7270
7691
  if (!poolEntry.isDirectory()) continue;
7271
- cleanupPoolByTtl(join13(engineDir, poolEntry.name), ttlMs, fs27, now);
7692
+ cleanupPoolByTtl(join14(engineDir, poolEntry.name), ttlMs, fs27, now);
7272
7693
  }
7273
7694
  }
7274
7695
  }
@@ -7292,7 +7713,7 @@ function cleanupPoolByTtl(poolDir, ttlMs, fs27, now) {
7292
7713
  function removeExpiredRefEntries(poolDir, file, ttlMs, fs27, now) {
7293
7714
  let changed = false;
7294
7715
  for (const [taskId, entry] of Object.entries(file.refs)) {
7295
- const journalPath = join13(poolDir, `${JOURNAL_PREFIX}${sanitizeSeg(taskId)}${JOURNAL_SUFFIX}`);
7716
+ const journalPath = join14(poolDir, `${JOURNAL_PREFIX}${sanitizeSeg(taskId)}${JOURNAL_SUFFIX}`);
7296
7717
  if (fs27.existsSync(journalPath)) {
7297
7718
  const age = now - fs27.statSync(journalPath).mtimeMs;
7298
7719
  if (age > ttlMs) {
@@ -7314,14 +7735,14 @@ function removeOrphanJournals(poolDir, file, entries, ttlMs, fs27, now) {
7314
7735
  if (!isJournalFile(entry.name)) continue;
7315
7736
  const segment = journalTaskSegment(entry.name);
7316
7737
  if (segment !== void 0 && ownedSegments.has(segment)) continue;
7317
- const journalPath = join13(poolDir, entry.name);
7738
+ const journalPath = join14(poolDir, entry.name);
7318
7739
  try {
7319
7740
  if (now - fs27.statSync(journalPath).mtimeMs > ttlMs) {
7320
7741
  unlinkBestEffort(journalPath, fs27);
7321
7742
  changed = true;
7322
7743
  }
7323
7744
  } catch (err) {
7324
- logger21.debug(
7745
+ logger22.debug(
7325
7746
  `[pool-manager] ttl cleanup stat failed for ${journalPath}: ${toErrorMessage(err)}`
7326
7747
  );
7327
7748
  }
@@ -7336,7 +7757,7 @@ function unlinkBestEffort(path21, fs27) {
7336
7757
  try {
7337
7758
  fs27.rmSync(path21, { force: true, recursive: true });
7338
7759
  } catch (err) {
7339
- logger21.debug(`[pool-manager] ttl cleanup failed for ${path21}: ${toErrorMessage(err)}`);
7760
+ logger22.debug(`[pool-manager] ttl cleanup failed for ${path21}: ${toErrorMessage(err)}`);
7340
7761
  }
7341
7762
  }
7342
7763
  function isJournalFile(name) {
@@ -7353,7 +7774,7 @@ function deletePoolNativeState(poolDir, fs27) {
7353
7774
  for (const entry of entries) {
7354
7775
  if (isJournalFile(entry.name)) continue;
7355
7776
  try {
7356
- fs27.rmSync(join13(poolDir, entry.name), { recursive: true, force: true });
7777
+ fs27.rmSync(join14(poolDir, entry.name), { recursive: true, force: true });
7357
7778
  } catch (err) {
7358
7779
  failures.push(`${entry.name}: ${toErrorMessage(err)}`);
7359
7780
  }
@@ -7375,16 +7796,16 @@ function deletePoolNativeState(poolDir, fs27) {
7375
7796
  }
7376
7797
  }
7377
7798
  function markCleanupFailed(poolDir, failures, fs27) {
7378
- const marker = join13(poolDir, POOL_CLEANUP_FAILED_MARKER);
7799
+ const marker = join14(poolDir, POOL_CLEANUP_FAILED_MARKER);
7379
7800
  const payload = JSON.stringify({ ts: Date.now(), failures });
7380
- logger21.warn(
7801
+ logger22.warn(
7381
7802
  `[pool-manager] pool cleanup failed for ${poolDir} (${failures.length} item(s)); marker written to ${marker} \u2014 re-run cleanup after fixing the underlying error`
7382
7803
  );
7383
7804
  try {
7384
7805
  fs27.writeFileSync(marker, `${payload}
7385
7806
  `);
7386
7807
  } catch (err) {
7387
- logger21.warn(
7808
+ logger22.warn(
7388
7809
  `[pool-manager] failed to write cleanup-failed marker ${marker}: ${toErrorMessage(err)}`
7389
7810
  );
7390
7811
  }
@@ -7402,7 +7823,7 @@ var nodeFs = {
7402
7823
  };
7403
7824
 
7404
7825
  // src/execution/idle-gc.ts
7405
- var logger22 = getLogger("subagents");
7826
+ var logger23 = getLogger("subagents");
7406
7827
  var GC_INTERVAL_MS = 60 * 60 * 1e3;
7407
7828
  var IDLE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
7408
7829
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
@@ -7413,7 +7834,7 @@ function startIdleGc(store) {
7413
7834
  if (isResumable(record) && record.idleSince) {
7414
7835
  const age = now - record.idleSince;
7415
7836
  if (age > IDLE_TTL_MS) {
7416
- logger22.warn(`[subagents] GC: archiving idle record ${record.id} (idle for ${Math.round(age / MS_PER_DAY)}d)`);
7837
+ logger23.warn(`[subagents] GC: archiving idle record ${record.id} (idle for ${Math.round(age / MS_PER_DAY)}d)`);
7417
7838
  try {
7418
7839
  store.archive(record);
7419
7840
  } catch (err) {
@@ -7496,15 +7917,15 @@ function createBackgroundStream(recordId, sink, mode, env) {
7496
7917
 
7497
7918
  // src/execution/worktree-manager.ts
7498
7919
  import { execFile as execFile3 } from "child_process";
7499
- import * as fs18 from "fs";
7920
+ import * as fs19 from "fs";
7500
7921
  import * as os2 from "os";
7501
- import * as path13 from "path";
7922
+ import * as path14 from "path";
7502
7923
 
7503
7924
  // src/execution/worktree-registry.ts
7504
- import * as fs17 from "fs";
7505
- import * as path12 from "path";
7925
+ import * as fs18 from "fs";
7926
+ import * as path13 from "path";
7506
7927
  import lockfile from "proper-lockfile";
7507
- var logger23 = getLogger("subagents");
7928
+ var logger24 = getLogger("subagents");
7508
7929
  var SPAWN_GRACE_MS = 6e4;
7509
7930
  var JSON_INDENT2 = 2;
7510
7931
  var LOCK_STALE_MS = 3e4;
@@ -7515,7 +7936,7 @@ function isRegistryData(value) {
7515
7936
  var WorktreeRegistry = class {
7516
7937
  filePath;
7517
7938
  constructor(agentDir) {
7518
- this.filePath = path12.join(agentDir, "subagents", "worktrees.json");
7939
+ this.filePath = path13.join(agentDir, "subagents", "worktrees.json");
7519
7940
  }
7520
7941
  /**
7521
7942
  * 新增条目(create 成功后调,pid=0 占位)。
@@ -7587,7 +8008,7 @@ var WorktreeRegistry = class {
7587
8008
  run();
7588
8009
  });
7589
8010
  } catch (lockErr) {
7590
- logger23.warn("[worktree] registry lock unavailable, degraded to lock-free RMW", {
8011
+ logger24.warn("[worktree] registry lock unavailable, degraded to lock-free RMW", {
7591
8012
  ...context ?? {},
7592
8013
  err: lockErr instanceof Error ? lockErr.message : String(lockErr)
7593
8014
  });
@@ -7612,8 +8033,8 @@ var WorktreeRegistry = class {
7612
8033
  * 锁参数值由 LOCK_STALE_MS / LOCK_RETRIES 常量承载(防漂移说明见常量注释)。
7613
8034
  */
7614
8035
  async withLock(fn) {
7615
- const dir = path12.dirname(this.filePath);
7616
- if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
8036
+ const dir = path13.dirname(this.filePath);
8037
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
7617
8038
  let compromised;
7618
8039
  const release = await lockfile.lock(this.filePath, {
7619
8040
  realpath: false,
@@ -7636,7 +8057,7 @@ var WorktreeRegistry = class {
7636
8057
  try {
7637
8058
  await release();
7638
8059
  } catch (unlockErr) {
7639
- logger23.debug("unlock failed after compromise (ignorable)", {
8060
+ logger24.debug("unlock failed after compromise (ignorable)", {
7640
8061
  detail: { err: unlockErr instanceof Error ? unlockErr.message : String(unlockErr) }
7641
8062
  });
7642
8063
  }
@@ -7648,7 +8069,7 @@ var WorktreeRegistry = class {
7648
8069
  */
7649
8070
  load() {
7650
8071
  try {
7651
- const raw = fs17.readFileSync(this.filePath, "utf-8");
8072
+ const raw = fs18.readFileSync(this.filePath, "utf-8");
7652
8073
  const parsed = JSON.parse(raw);
7653
8074
  if (isRegistryData(parsed)) {
7654
8075
  return parsed.entries;
@@ -7671,7 +8092,7 @@ var WorktreeRegistry = class {
7671
8092
  writeAtomicFileSync(this.filePath, JSON.stringify({ entries }, null, JSON_INDENT2));
7672
8093
  } catch (err) {
7673
8094
  bestEffort(err, "worktree registry save");
7674
- logger23.warn(
8095
+ logger24.warn(
7675
8096
  "[worktree] registry save failed; pid may stay 0 and be reaped by orphan reaper",
7676
8097
  { ...context ?? {}, err: err instanceof Error ? err.message : String(err) }
7677
8098
  );
@@ -7680,7 +8101,7 @@ var WorktreeRegistry = class {
7680
8101
  };
7681
8102
 
7682
8103
  // src/execution/worktree-manager.ts
7683
- var logger24 = getLogger("subagents");
8104
+ var logger25 = getLogger("subagents");
7684
8105
  var SAFE_ID_RE = /^[\w-]+$/;
7685
8106
  var GIT_TIMEOUT_MS = 3e4;
7686
8107
  var WORKTREE_TMP_ROOT = "pi-subagents";
@@ -7705,13 +8126,13 @@ function isWriteCommand(args) {
7705
8126
  }
7706
8127
  function resolveRepoFromCheckout(checkout) {
7707
8128
  try {
7708
- const raw = fs18.readFileSync(path13.join(checkout, ".git"), "utf-8").trim();
8129
+ const raw = fs19.readFileSync(path14.join(checkout, ".git"), "utf-8").trim();
7709
8130
  if (!raw.startsWith("gitdir:")) return void 0;
7710
8131
  const gitdir = raw.slice("gitdir:".length).trim();
7711
- const worktreesDir = path13.dirname(gitdir);
7712
- if (path13.basename(worktreesDir) !== "worktrees") return void 0;
7713
- const gitRootDir = path13.dirname(worktreesDir);
7714
- return path13.dirname(gitRootDir);
8132
+ const worktreesDir = path14.dirname(gitdir);
8133
+ if (path14.basename(worktreesDir) !== "worktrees") return void 0;
8134
+ const gitRootDir = path14.dirname(worktreesDir);
8135
+ return path14.dirname(gitRootDir);
7715
8136
  } catch {
7716
8137
  return void 0;
7717
8138
  }
@@ -7764,10 +8185,10 @@ ${statusText}`
7764
8185
  if (revR.status === "rejected") throw revR.reason;
7765
8186
  const baseCommit = revR.value.trim();
7766
8187
  const branch = `pi-sub-${recordId}`;
7767
- const worktreePath = path13.join(os2.tmpdir(), "pi-subagents", encodeCwd(mainCwd), branch);
7768
- if (fs18.existsSync(worktreePath)) {
8188
+ const worktreePath = path14.join(os2.tmpdir(), "pi-subagents", encodeCwd(mainCwd), branch);
8189
+ if (fs19.existsSync(worktreePath)) {
7769
8190
  try {
7770
- fs18.rmSync(worktreePath, { recursive: true, force: true });
8191
+ fs19.rmSync(worktreePath, { recursive: true, force: true });
7771
8192
  } catch (cleanErr) {
7772
8193
  bestEffort(cleanErr, "pre-create checkout cleanup");
7773
8194
  }
@@ -7783,10 +8204,10 @@ ${statusText}`
7783
8204
  createdAt: Date.now()
7784
8205
  });
7785
8206
  try {
7786
- const mainNodeModules = path13.join(mainCwd, "node_modules");
7787
- const worktreeNodeModules = path13.join(worktreePath, "node_modules");
7788
- if (fs18.existsSync(mainNodeModules) && !fs18.existsSync(worktreeNodeModules)) {
7789
- fs18.symlinkSync(mainNodeModules, worktreeNodeModules);
8207
+ const mainNodeModules = path14.join(mainCwd, "node_modules");
8208
+ const worktreeNodeModules = path14.join(worktreePath, "node_modules");
8209
+ if (fs19.existsSync(mainNodeModules) && !fs19.existsSync(worktreeNodeModules)) {
8210
+ fs19.symlinkSync(mainNodeModules, worktreeNodeModules);
7790
8211
  }
7791
8212
  return Object.freeze({
7792
8213
  path: worktreePath,
@@ -7874,7 +8295,7 @@ ${statusText}`
7874
8295
  return Object.freeze({ patchFile, failed: false, written: false });
7875
8296
  }
7876
8297
  try {
7877
- fs18.writeFileSync(patchFile, diff, "utf-8");
8298
+ fs19.writeFileSync(patchFile, diff, "utf-8");
7878
8299
  return Object.freeze({ patchFile, failed: false, written: true });
7879
8300
  } catch {
7880
8301
  return Object.freeze({ patchFile, failed: true, written: false });
@@ -7937,9 +8358,9 @@ ${statusText}`
7937
8358
  const branches = branchesByRepo.get(entry.repo);
7938
8359
  if (branches === void 0) continue;
7939
8360
  const branchGone = !branches.has(entry.branch);
7940
- const checkoutGone = !fs18.existsSync(entry.checkout);
8361
+ const checkoutGone = !fs19.existsSync(entry.checkout);
7941
8362
  if (branchGone && checkoutGone) {
7942
- logger24.warn("[worktree] reconcile: registry entry has no physical worktree/branch, removing entry", {
8363
+ logger25.warn("[worktree] reconcile: registry entry has no physical worktree/branch, removing entry", {
7943
8364
  branch: entry.branch,
7944
8365
  repo: entry.repo,
7945
8366
  pid: entry.pid
@@ -7983,14 +8404,14 @@ ${statusText}`
7983
8404
  }
7984
8405
  if (alivePids.length === 1 && list.length === 1) {
7985
8406
  const pt = list[0];
7986
- logger24.warn("[worktree] reconcile: unregistered physical worktree with one alive pid, re-registering (self-heal)", {
8407
+ logger25.warn("[worktree] reconcile: unregistered physical worktree with one alive pid, re-registering (self-heal)", {
7987
8408
  branch: pt.branch,
7988
8409
  checkout: pt.checkout,
7989
8410
  repo: pt.repo,
7990
8411
  pid: alivePids[0]
7991
8412
  });
7992
8413
  await this.registry.add({
7993
- repo: pt.repo ?? path13.dirname(pt.checkout),
8414
+ repo: pt.repo ?? path14.dirname(pt.checkout),
7994
8415
  branch: pt.branch,
7995
8416
  checkout: pt.checkout,
7996
8417
  pid: alivePids[0],
@@ -8006,7 +8427,7 @@ ${statusText}`
8006
8427
  if (cycles < RECONCILE_SKIP_ESCALATION_CYCLES) continue;
8007
8428
  escalated++;
8008
8429
  const repoHint = pt.repo ?? "<main-repo>";
8009
- logger24.warn(
8430
+ logger25.warn(
8010
8431
  `[worktree] reconcile: unregistered physical worktree skipped for ${cycles} consecutive cycles (alive-pid mapping still ambiguous) \u2014 manual cleanup may be needed. Inspect: git -C ${repoHint} worktree list. If no live process owns it: git -C ${repoHint} worktree remove --force ${pt.checkout} && git -C ${repoHint} branch -D ${pt.branch}. Ownerless checkout (repo unknown, delete the directory directly): rm -rf ${pt.checkout}`,
8011
8432
  {
8012
8433
  branch: pt.branch,
@@ -8017,7 +8438,7 @@ ${statusText}`
8017
8438
  );
8018
8439
  }
8019
8440
  if (escalated < list.length) {
8020
- logger24.warn("[worktree] reconcile: unregistered physical worktrees present but alive-pid mapping ambiguous, skipping this cycle", {
8441
+ logger25.warn("[worktree] reconcile: unregistered physical worktrees present but alive-pid mapping ambiguous, skipping this cycle", {
8021
8442
  enc,
8022
8443
  orphans: list.length - escalated,
8023
8444
  alivePids: alivePids.length
@@ -8030,7 +8451,7 @@ ${statusText}`
8030
8451
  for (const pt of list) {
8031
8452
  const age = Date.now() - pt.mtimeMs;
8032
8453
  if (age <= SPAWN_GRACE_MS) continue;
8033
- logger24.warn("[worktree] reconcile: unregistered physical worktree with no alive pid, cleaning up", {
8454
+ logger25.warn("[worktree] reconcile: unregistered physical worktree with no alive pid, cleaning up", {
8034
8455
  branch: pt.branch,
8035
8456
  checkout: pt.checkout,
8036
8457
  repo: pt.repo,
@@ -8046,10 +8467,10 @@ ${statusText}`
8046
8467
  * 推导失败(残缺 checkout)repo=undefined,由调用方按无主残留处置。
8047
8468
  */
8048
8469
  async discoverPhysicalWorktrees() {
8049
- const root = path13.join(os2.tmpdir(), WORKTREE_TMP_ROOT);
8470
+ const root = path14.join(os2.tmpdir(), WORKTREE_TMP_ROOT);
8050
8471
  let encDirs;
8051
8472
  try {
8052
- encDirs = fs18.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
8473
+ encDirs = fs19.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
8053
8474
  } catch {
8054
8475
  return [];
8055
8476
  }
@@ -8057,14 +8478,14 @@ ${statusText}`
8057
8478
  for (const enc of encDirs) {
8058
8479
  let branchDirs;
8059
8480
  try {
8060
- branchDirs = fs18.readdirSync(path13.join(root, enc), { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith(BRANCH_PREFIX)).map((d) => d.name);
8481
+ branchDirs = fs19.readdirSync(path14.join(root, enc), { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith(BRANCH_PREFIX)).map((d) => d.name);
8061
8482
  } catch {
8062
8483
  continue;
8063
8484
  }
8064
8485
  for (const branch of branchDirs) {
8065
- const checkout = path13.join(root, enc, branch);
8486
+ const checkout = path14.join(root, enc, branch);
8066
8487
  try {
8067
- const mtimeMs = fs18.statSync(checkout).mtimeMs;
8488
+ const mtimeMs = fs19.statSync(checkout).mtimeMs;
8068
8489
  result.push({ enc, branch, checkout, repo: resolveRepoFromCheckout(checkout), mtimeMs });
8069
8490
  } catch (err) {
8070
8491
  bestEffort(err, "physical worktree stat (reconcile)");
@@ -8102,17 +8523,17 @@ ${statusText}`
8102
8523
  * 崩溃残留的 .alive(pid 已死)天然过滤掉——这正是「死活判据」的物理面来源。
8103
8524
  */
8104
8525
  collectAlivePids(enc) {
8105
- const sessionsDir = path13.join(this.agentDir, "subagents", enc, "sessions");
8526
+ const sessionsDir = path14.join(this.agentDir, "subagents", enc, "sessions");
8106
8527
  let files;
8107
8528
  try {
8108
- files = fs18.readdirSync(sessionsDir);
8529
+ files = fs19.readdirSync(sessionsDir);
8109
8530
  } catch {
8110
8531
  return [];
8111
8532
  }
8112
8533
  const pids = /* @__PURE__ */ new Set();
8113
8534
  for (const file of files) {
8114
8535
  if (!file.endsWith(".alive")) continue;
8115
- const marker = readAliveMarker(path13.join(sessionsDir, file.slice(0, -".alive".length)));
8536
+ const marker = readAliveMarker(path14.join(sessionsDir, file.slice(0, -".alive".length)));
8116
8537
  if (marker && isProcessAlive(marker.pid)) {
8117
8538
  pids.add(marker.pid);
8118
8539
  }
@@ -8144,8 +8565,8 @@ ${statusText}`
8144
8565
  }
8145
8566
  }
8146
8567
  try {
8147
- if (fs18.existsSync(pt.checkout)) {
8148
- fs18.rmSync(pt.checkout, { recursive: true, force: true });
8568
+ if (fs19.existsSync(pt.checkout)) {
8569
+ fs19.rmSync(pt.checkout, { recursive: true, force: true });
8149
8570
  }
8150
8571
  } catch (err) {
8151
8572
  bestEffort(err, "checkout dir rm (reconcile)");
@@ -8158,7 +8579,7 @@ ${statusText}`
8158
8579
  if (entry.pid === 0) {
8159
8580
  const expired = now - entry.createdAt > SPAWN_GRACE_MS;
8160
8581
  if (expired) {
8161
- logger24.warn(
8582
+ logger25.warn(
8162
8583
  "[worktree] orphan reaper: pid=0 entry exceeded SPAWN_GRACE_MS, treating as orphan",
8163
8584
  { branch: entry.branch, checkout: entry.checkout, createdAt: entry.createdAt, now }
8164
8585
  );
@@ -8234,7 +8655,7 @@ ${statusText}`
8234
8655
  };
8235
8656
 
8236
8657
  // src/execution/subagent-service.ts
8237
- var logger25 = getLogger("subagents");
8658
+ var logger26 = getLogger("subagents");
8238
8659
  var disposedUiRequestStub = () => Promise.resolve({ cancelled: true });
8239
8660
  var PRIORITY_BACKGROUND = 1e3;
8240
8661
  var MS_PER_SECOND6 = 1e3;
@@ -8249,6 +8670,7 @@ var ENV_ROOT_SESSION_ID = "PI_SUBAGENT_ROOT_SESSION_ID";
8249
8670
  var ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID";
8250
8671
  var ENV_DEPTH = "PI_SUBAGENT_DEPTH";
8251
8672
  var ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD";
8673
+ var SETTLED_RESCAN_LIMIT = 8;
8252
8674
  var SubagentService = class {
8253
8675
  pool;
8254
8676
  store;
@@ -8299,12 +8721,29 @@ var SubagentService = class {
8299
8721
  /** [D4-①] 通知簇 host 面(notifyComplete/notifyClosed/pending 注册注销 + notifier
8300
8722
  * 实例封装,原私有通知簇四方法与模块函数的搬移落点——notify-host.ts)。
8301
8723
  * deps 惰性求值(pi/session 级状态运行时可变),行为与原 constructor 内
8302
- * createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。 */
8724
+ * createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。
8725
+ * [sync-collect 合并] toNotifyRecord/notify/notifyBatch 随批路由需要由 host 导出
8726
+ * (collectCoordinator 闭包与 E9/E1 直发路径消费,见 notify-host.ts 接口注释)。 */
8303
8727
  notifyHost = createNotifyHost({
8304
8728
  getPi: () => this.pi,
8305
8729
  listRunning: () => this.store.listRunning(),
8306
8730
  getIsIdle: () => this.isIdleFn
8307
8731
  });
8732
+ /** collectCoordinator(subagent-sync-collect U2):sync 批缓冲 + 闭合检测 + flush 分流。 */
8733
+ collectCoordinator;
8734
+ /** [E9] dispose 时已转 async 写账的成员 id(revive 后 flushBatch 防御过滤用)。
8735
+ * 背景:dispose 后同进程 revive(/resume /fork /new)时协调器内部缓冲仍持有已转换
8736
+ * 成员快照(协调器无 drain API,U5 领地不含 collect-coordinator.ts)——若后续新
8737
+ * sync 成员触发闭合,陈旧快照会随批重投(新成员集新 hash,账本跨键不拦)→ 双重
8738
+ * 通知。flushBatch 闭包按本集过滤,陈旧成员零重投。id 唯一 per spawn,无误伤面。 */
8739
+ e9ConvertedIds = /* @__PURE__ */ new Set();
8740
+ /** [v2 D4] E1 等待分支的 settled 有界重扫状态(null = 未注册)。disposed 后保持
8741
+ * 非 null——同 session 内不再重复注册(补发完成/达限后 settled 边沿已无事可做,
8742
+ * 单注册即单重扫);initSession(revive)置 null 允许新 session 重新注册。
8743
+ * pi.on 无 off(见 armSettledRescan):旧 handler 闭包捕获旧 state,未 disposed 时
8744
+ * 遇 settled 边沿仍会执行,扫描 this.mainSessionFile 当前值(handler 不绑定注册时
8745
+ * 的文件域);dispose 的惰化处置见 dispose()。 */
8746
+ settledRescanState = null;
8308
8747
  /** [MF#4][MF#2] fork 深度按 async 调用链传递(AsyncLocalStorage),替代共享可变计数器。
8309
8748
  * 主 session=0;fork 进入子 session 期间推进为子深度,供嵌套 fork 经 ALS 读到自身深度作为
8310
8749
  * parentForkDepth。并发 background fork 各自独立调用链,不再互相压低深度值。
@@ -8333,7 +8772,7 @@ var SubagentService = class {
8333
8772
  /** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
8334
8773
  * 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
8335
8774
  settleRound = createRoundSettler({
8336
- notifyComplete: (record) => this.notifyHost.notifyComplete(record),
8775
+ notifyComplete: (record) => this.collectCoordinator.route(record),
8337
8776
  reportRecordTransition: (record) => this.store.reportRecordTransition(record),
8338
8777
  closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record)
8339
8778
  });
@@ -8364,6 +8803,11 @@ var SubagentService = class {
8364
8803
  deliverChatMessage: (record, text, interrupt) => this.deliverChatMessage(record, text, interrupt)
8365
8804
  };
8366
8805
  manifestStore;
8806
+ /**
8807
+ * [D6 #7a] records 目录(与 manifestStore 同源同一推导)——屏障失败 warn 带 manifest
8808
+ * 文件路径用(ManifestStore.dir 私有,此处不破封装另存同源值;漂移由构造点同语句保证不发生)。
8809
+ */
8810
+ recordsDir;
8367
8811
  /**
8368
8812
  * [T1/PS-9] subagent sessionDir(getSubagentSessionDir 推导,与 store 同源同一 rootCwd)。
8369
8813
  * 传给 doFinalizeRecord 的 FinalizeDeps.sessionDir——record.sessionFile 缺失时 finalize
@@ -8382,8 +8826,42 @@ var SubagentService = class {
8382
8826
  const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
8383
8827
  const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
8384
8828
  this.sessionsDir = sessionsDir;
8829
+ this.recordsDir = recordsDir;
8385
8830
  this.manifestStore = new ManifestStore(recordsDir);
8386
8831
  this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? void 0);
8832
+ this.collectCoordinator = new CollectCoordinator({
8833
+ notifyAsync: (record) => {
8834
+ const notify = this.notifyHost.toNotifyRecord(record);
8835
+ if (notify) this.notifyHost.notify(notify);
8836
+ },
8837
+ toNotifyRecord: (record) => this.notifyHost.toNotifyRecord(record),
8838
+ // 闭合判定数据源:listAllActive(原始 ExecutionRecord 内存态,携带 collectMode/
8839
+ // batchFinalized 原始值)。[U3 修正] 原接 collectRecords——其经 recordToSubagent
8840
+ // 投影丢 collectMode(U2 披露的投影缺口)→ 真链上闭合判定恒立即闭合、跨轮续累
8841
+ // 失效(真链 trace 实证)。非终态 sync 成员必在内存(archive 只删终态;磁盘重建
8842
+ // 残留属孤儿恢复域),内存视图语义完整。
8843
+ listRecords: (limit) => this.store.listAllActive().slice(0, limit),
8844
+ flushBatch: async (members) => {
8845
+ const live = members.filter((m) => !this.e9ConvertedIds.has(m.id));
8846
+ if (live.length === 0) return;
8847
+ const fulls = [];
8848
+ const fullMissed = [];
8849
+ for (const m of live) {
8850
+ const full = this.store.getFullRecord(m.id);
8851
+ if (full) fulls.push(full);
8852
+ else fullMissed.push(m);
8853
+ }
8854
+ await this.writeSyncBatchManifestBarrier(fulls);
8855
+ const accepted = this.notifyHost.notifyBatch(live, this.getCollectSyncBudget());
8856
+ if (!accepted) return;
8857
+ for (const full of fulls) {
8858
+ this.appendBatchFinalizedEntry(full);
8859
+ }
8860
+ for (const m of fullMissed) {
8861
+ this.appendBatchFinalizedEntry(bufferedMemberFallbackRecord(m, this.sessionRootId ?? void 0));
8862
+ }
8863
+ }
8864
+ });
8387
8865
  registerGlobalObservability(this.uiObservability);
8388
8866
  }
8389
8867
  // ── 生命周期(index.ts 调)──────────────────────────────
@@ -8408,6 +8886,7 @@ var SubagentService = class {
8408
8886
  this.sessionRootId = envRoot ?? init.sessionId;
8409
8887
  this.initExecContextBaseline(envRoot, init.sessionId);
8410
8888
  this._disposed = false;
8889
+ this.settledRescanState = null;
8411
8890
  this.store.revive();
8412
8891
  this.notifyHost.revive();
8413
8892
  this.recoverOrphansIfRootProcess();
@@ -8441,7 +8920,7 @@ var SubagentService = class {
8441
8920
  this.execNesting.setBaseline({ recordId: envSelfRecord, depth: nestingDepth });
8442
8921
  this.execNesting.enterWith({ recordId: envSelfRecord, depth: nestingDepth });
8443
8922
  if (process.env.XYZ_AGENT_DEBUG) {
8444
- logger25.debug(
8923
+ logger26.debug(
8445
8924
  `[subagents] execNesting initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ?? sessionId}`
8446
8925
  );
8447
8926
  }
@@ -8465,26 +8944,28 @@ var SubagentService = class {
8465
8944
  if (!isChildProcess) {
8466
8945
  this.recoverOrphanRecords();
8467
8946
  } else if (process.env.XYZ_AGENT_DEBUG) {
8468
- logger25.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
8947
+ logger26.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
8469
8948
  }
8470
8949
  }
8471
8950
  /** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一调用入口,维持 store
8472
8951
  * private 封装——与 recoverManifestTmpFiles 同模式;[D4] public 面收窄:唯一调用方
8473
8952
  * 是 initSession,转 private)。判定语义见 store 侧注释。
8953
+ * mainSessionFile 随调用透传(v2 D3 覆写 merge 数据源:主文件末条 entry 的批域
8954
+ * 标记与轮终 result/model;initSession 先赋值后恢复,时序就绪)。
8474
8955
  * 随后跑 entry-born 孤儿恢复(无子文件锚的 register-only record,spawn 窗口期死亡,
8475
8956
  * E2E 实测缺口)——主 session 文件经 getMainSessionFile 注入(构造期可空)。 */
8476
8957
  recoverOrphanRecords() {
8477
8958
  try {
8478
- this.store.recoverOrphanRecords(this.sessionRootId ?? void 0);
8959
+ this.store.recoverOrphanRecords(this.sessionRootId ?? void 0, this.mainSessionFile);
8479
8960
  } catch (err) {
8480
- logger25.warn("[subagents] orphan recovery failed", {
8961
+ logger26.warn("[subagents] orphan recovery failed", {
8481
8962
  reason: toErrorMessage(err)
8482
8963
  });
8483
8964
  }
8484
8965
  try {
8485
8966
  this.store.recoverEntryOnlyOrphans(this.mainSessionFile, this.sessionRootId ?? void 0);
8486
8967
  } catch (err) {
8487
- logger25.warn("[subagents] entry-only orphan recovery failed", {
8968
+ logger26.warn("[subagents] entry-only orphan recovery failed", {
8488
8969
  reason: toErrorMessage(err)
8489
8970
  });
8490
8971
  }
@@ -8582,14 +9063,232 @@ var SubagentService = class {
8582
9063
  * listener 仍然存活。若 pending-notifications 先于本扩展执行 session_shutdown(后注册
8583
9064
  * 先执行的语义下会如此),listener 已注销,unregister 事件被静默丢弃。这是可接受的
8584
9065
  * 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
9066
+ /** [U4 deviation #8 接线] collectSync 预算热读(flush 时读值,与 getCollectSyncDefault
9067
+ * 同款访问链 modelService.getGlobalConfig().collectSync)。节缺失/读失败 → undefined
9068
+ * → notifyBatch 落 buildBatchLlmContent 设计默认值(4000/24000,E5 不炸启动)。
9069
+ * sanitizeCollectSync 保证节存在时两字段必有合法正整数。 */
9070
+ getCollectSyncBudget() {
9071
+ const cs = this.collectSyncSection();
9072
+ return cs !== void 0 ? { perItemChars: cs.perItemChars, totalChars: cs.totalChars } : void 0;
9073
+ }
9074
+ /** [E9 专用] batchFinalized 落标 + manifest fire-and-forget 写(设计 §3.1.5 E9)。
9075
+ * 批通知路径(flush/E1)已改走「manifest 屏障 → 写账 → 纯落标」序列(「通知可达
9076
+ * ⇒ 索引就位」的构造性保证,见 flushBatch 闭包 / runSyncCollectRecoveryScan),
9077
+ * 不再经本 helper;仅 E9 转换的成员保持原形态——其走 async 单条通知(全文注入、
9078
+ * 无指针行消费),manifest 无时序要求,落标后 fire-and-forget 补写(list 后手动
9079
+ * 反查的顺带索引)。
9080
+ * 路径 = getFullRecord 冷路径重建 → appendBatchFinalizedEntry 纯落标 → fire
9081
+ * manifest。getFullRecord 不可达(子 session 文件缺失/已 GC)→ 跳过该成员
9082
+ * (详见 flushBatch 闭包注释)。 */
9083
+ markMembersBatchFinalized(memberIds) {
9084
+ for (const id of memberIds) {
9085
+ const full = this.store.getFullRecord(id);
9086
+ if (!full) continue;
9087
+ this.appendBatchFinalizedEntry(full);
9088
+ void this.writeBatchMemberManifest(full).catch((err) => {
9089
+ logger26.debug(
9090
+ `[subagents] batch-finalized manifest write failed (record=${full.id})`,
9091
+ { reason: err instanceof Error ? err.message : String(err) }
9092
+ );
9093
+ });
9094
+ }
9095
+ }
9096
+ /** batchFinalized 落标唯一出口(appendEntry 公共末步,纯落标):显式覆写
9097
+ * collectMode/batchFinalized → reportSubagentRecord。覆写动机:recordToSubagent
9098
+ * 投影已含两字段(U5 修复),但 getFullRecord 冷路径含 sidecar/manifest 重建分支
9099
+ * (非 entry 源),显式赋值防非 entry 源重建时丢标记。
9100
+ *
9101
+ * [v2 D1 断链 1] 落标即「离开批 = 通知已/即将送达 = 指针行即将被消费」——成功
9102
+ * 成员走 SP-5 改道 doFinalizeRoundToIdle(不写 manifest),批路径不补写则
9103
+ * records/<sa-id>.json 永不产生、session-reader 反查 0 命中。manifest 写点已从
9104
+ * 本出口的 fire-and-forget 前移至各调用方:批通知路径(flush/E1)在写账前屏障
9105
+ * await 全部落盘(「通知可达 ⇒ 索引就位」的构造性保证);E9 保持落标后
9106
+ * fire-and-forget(async 单条通知无指针行消费,无时序要求)。
9107
+ * rec 两来源(flush 的 getFullRecord 内存全量 / E1 的 rebuildEntryRecord 重建
9108
+ * 快照)必需字段恒齐备(id/agentName←agent/rootSessionId/createdAt←startedAt),
9109
+ * task/slug/parentRecordId 等可选 undefined 自然缺省。 */
9110
+ appendBatchFinalizedEntry(rec) {
9111
+ this.store.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
9112
+ }
9113
+ /** 批成员 manifest(sa- id → sessionFile 反查索引)写的唯一投影点(D2 字段投影 +
9114
+ * status 如实投影:成功成员此刻 record 实态 running+resumable → "running",后续
9115
+ * message upgrade 走完整 finalize 时 Step 4 原子覆盖为 "closed")。
9116
+ * 返回原始 promise 不吞错——失败语义由调用方定:批通知路径经
9117
+ * writeSyncBatchManifestBarrier 的 allSettled(debug 不阻断写账);E9 经
9118
+ * fire-and-forget catch(debug 不阻断落标)。 */
9119
+ writeBatchMemberManifest(rec) {
9120
+ return this.manifestStore.writeManifest({
9121
+ id: rec.id,
9122
+ rootSessionId: rec.rootSessionId ?? "",
9123
+ parentRecordId: rec.parentRecordId,
9124
+ agentName: rec.agent,
9125
+ status: rec.status,
9126
+ createdAt: rec.startedAt,
9127
+ completedAt: rec.endedAt,
9128
+ sessionFile: rec.sessionFile,
9129
+ task: rec.task,
9130
+ slug: rec.slug,
9131
+ model: rec.model
9132
+ });
9133
+ }
9134
+ /** [时序屏障] 批通知路径(flush/E1)专用:成员 manifest 并行写 + await 全部完成
9135
+ * (allSettled)后才允许写账投递——「通知可达 ⇒ 索引就位」的构造性保证。写失败
9136
+ * 不阻断(best-effort 语义与 doFinalizeRecord Step 4 一致:反查索引缺失只影响指针行
9137
+ * 反查,session-reader 错误文案已指引绝对路径兜底,不构成写账失败);warn 留痕
9138
+ * (D6 #7a / SC-1:屏障失败意味着该成员指针行反查索引缺失,debug 级在排障时不可见)。 */
9139
+ async writeSyncBatchManifestBarrier(recs) {
9140
+ const results = await Promise.allSettled(recs.map((rec) => this.writeBatchMemberManifest(rec)));
9141
+ for (let i = 0; i < results.length; i++) {
9142
+ const result = results[i];
9143
+ if (result.status === "rejected") {
9144
+ logger26.warn(
9145
+ `[subagents] batch-finalized manifest write failed (record=${recs[i].id}, manifest=${this.recordsDir}/${recs[i].id}.json)`,
9146
+ { reason: result.reason instanceof Error ? result.reason.message : String(result.reason) }
9147
+ );
9148
+ }
9149
+ }
9150
+ }
9151
+ /** [E9] dispose 时批未闭合:缓冲中已终态未通知成员逐条转 async 语义写账(放弃攒批)
9152
+ * + 落 batchFinalized 标记(E1 重建扫描据此排除,防双重通知),交由既有 shutdown
9153
+ * flush / resume 重放兑底;仍在跑的成员走现有退出路径(disposeAllRecords 关闭,
9154
+ * 与 async 一致)。写账用 notifier.notify 现有通路(ledger.record + attemptDeliver)。
9155
+ * 源序:写账先于落标——写账后崩溃 → E1 重建收该成员,但 async notifyId 与批 hash
9156
+ * 跨键不拦的重发属设计披露的 E9 残余窗(at-least-once 良性,PS-17 同族,v1 接受)。 */
9157
+ convertPendingSyncBufferToAsync() {
9158
+ this.collectCoordinator.cancelScheduledFlush();
9159
+ const members = this.collectCoordinator.pendingMembers();
9160
+ if (members.length === 0) return;
9161
+ for (const member of members) {
9162
+ this.e9ConvertedIds.add(member.id);
9163
+ this.notifyHost.notify(member);
9164
+ }
9165
+ this.markMembersBatchFinalized(members.map((m) => m.id));
9166
+ logger26.warn(
9167
+ `[subagents] E9 dispose: converted ${members.length} buffered sync member(s) to async notify`,
9168
+ { ids: members.map((m) => m.id) }
9169
+ );
9170
+ }
9171
+ /**
9172
+ * [E1] sync 批崩溃恢复钩子(设计 §3.1.5 E1,index.ts session_start 恢复编排处调用,
9173
+ * 须晚于 initSession——孤儿终态恢复先行收敛 running 成员,「全员终态」判定才可达):
9174
+ *
9175
+ * - 扫描主 session 文件每 id 末条 subagent-record entry(store.scanLastRecordEntries,
9176
+ * collectLastRecordEntries 同构 + 投影扩展含 collectMode/batchFinalized + 终态五
9177
+ * 字段;禁走 collectRecords light 路径——主 session 落标 entry 对它不可见);
9178
+ * - 只收 collectMode=sync 且无 batchFinalized 的成员(排除已通过批 flush 或 E9
9179
+ * 转换离场的,防双重通知),按 rootSessionId 过滤当前根;
9180
+ * - 全员终态且账本无同成员集批记录 → manifest 屏障(await 落盘,「通知可达 ⇒
9181
+ * 索引就位」构造性保证,与 flushBatch 同款)→ notifyBatch 补发(内容 = 末条
9182
+ * entry 终态快照;账本 record 同 hash 幂等拒绝 = 已投递/已在账,两种结局都算
9183
+ * 「已处理」);
9184
+ * - 仍有 running → 本次不动,注册 settled 有界重扫(D4,见 armSettledRescan)——
9185
+ * 成员延迟终态(主 agent 冷路径 resume → 正常流落 entry)由 settled 边沿驱动
9186
+ * 重扫收敛,不再依赖「下次 session_start」作唯一再驱动(v2 §2.4 断链 4);
9187
+ * running 口径与协调器同构
9188
+ * (resumable 豁免,v2 D3——覆写不可达的防御分支残余不被误判「仍在跑」);
9189
+ * - 补发尝试后统一补 batchFinalized 标记(账本拒绝也算已投递;直接用末条重建快照
9190
+ * 落标不经 getFullRecord——子文件缺失/已 GC 时标记仍可落盘,窗口自愈不依赖二次
9191
+ * 重启;补标自身崩溃重入幂等收敛,末条 entry last-writer-wins)。
9192
+ *
9193
+ * async 化(时序屏障修复):返回 Promise 但**内部自捕获不外抛**——宿主 index.ts
9194
+ * session_start 以同步 try/catch 调用(其 catch 兑现不到 promise 内的异常),
9195
+ * 自捕获维持同款 warn 容错语义,浮动调用零适配、不产生 unhandled rejection。
9196
+ *
9197
+ * [U8 拆批修复] 与协调器合批排程无交集:E1 只在 session_start 编排处运行(此前
9198
+ * dispose 已取消挂起排程),补发直走 notifier.notifyBatch 不经协调器;异常时序
9199
+ * 相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
9200
+ */
9201
+ async recoverSyncCollectBatch() {
9202
+ try {
9203
+ const { outcome } = await this.runSyncCollectRecoveryScan();
9204
+ if (outcome === "waiting") {
9205
+ this.armSettledRescan();
9206
+ }
9207
+ } catch (err) {
9208
+ logger26.warn("[subagents] sync collect batch recovery failed", {
9209
+ reason: err instanceof Error ? err.message : String(err)
9210
+ });
9211
+ }
9212
+ }
9213
+ /** [E1/D4] 单次「扫描→判定→可达则补发+落标」,E1 首扫与 settled 重扫共用同一实现
9214
+ * (防两处复制粘贴分岔)。三态返回:idle(无 sync 候选——已全部落标/E9 转换/异根,
9215
+ * 无事可等)/ waiting(仍有 running 成员,本次不动)/ dispatched(全员终态,已补发
9216
+ * +统一落标)。async:补发前有 manifest 屏障 await(见函数头 E1 注释)。 */
9217
+ /** 返回 outcome + waitingIds(达限 warn 需滞留成员 id,D6 #7b——仅 waiting 态非空)。 */
9218
+ async runSyncCollectRecoveryScan() {
9219
+ const lastRecords = this.store.scanLastRecordEntries(this.mainSessionFile);
9220
+ if (lastRecords.length === 0) return { outcome: "idle", waitingIds: [] };
9221
+ const rootFilter = this.sessionRootId;
9222
+ const candidates = lastRecords.filter(
9223
+ (r) => r.collectMode === "sync" && r.batchFinalized !== true && (rootFilter === void 0 || r.rootSessionId === rootFilter)
9224
+ );
9225
+ if (candidates.length === 0) return { outcome: "idle", waitingIds: [] };
9226
+ const running = candidates.filter((r) => r.resumable !== true && r.status !== "closed");
9227
+ if (running.length > 0) {
9228
+ logger26.debug(
9229
+ `[subagents] E1 sync batch recovery: ${running.length} member(s) still running, wait for natural completion`,
9230
+ { ids: running.map((r) => r.id) }
9231
+ );
9232
+ return { outcome: "waiting", waitingIds: running.map((r) => r.id) };
9233
+ }
9234
+ await this.writeSyncBatchManifestBarrier(candidates);
9235
+ const members = candidates.map((r) => syncRebuildToNotifyMember(r));
9236
+ const accepted = this.notifyHost.notifyBatch(members, this.getCollectSyncBudget());
9237
+ for (const rec of candidates) {
9238
+ this.appendBatchFinalizedEntry(rec);
9239
+ }
9240
+ logger26.warn(
9241
+ `[subagents] E1 sync batch recovery: re-notified ${members.length} member(s) (ledger accepted=${accepted})`,
9242
+ { ids: members.map((m) => m.id) }
9243
+ );
9244
+ return { outcome: "dispatched", waitingIds: [] };
9245
+ }
9246
+ /** [v2 D4] 注册 agent_settled 有界重扫(幂等:settledRescanState 非 null 不叠加注册
9247
+ * ——E1 现仅 session_start 单调用点,守卫是防第二入口引入时的注册叠加断言面)。
9248
+ * 每次 settled 边沿重跑同一 E1 扫描(runSyncCollectRecoveryScan):dispatched
9249
+ * (补发+落标完成,scan 的 warn 已留痕)或 idle(候选已被其他通路落标)→ disposed;
9250
+ * 累计 SETTLED_RESCAN_LIMIT 次仍在等 → disposed + warn 留痕(D6 #7b / SC-2:达限
9251
+ * 放弃重扫意味着滞留成员的批通知要等下次 session_start 才收敛,含滞留 id 的 warn
9252
+ * 是唯一线索,debug 级排障不可见;后续事件零处理,下次 session_start 再收敛)。
9253
+ * pi.on 无 off(0.84.4 实装)——disposed 标志包装兑现退订(scheduler extension
9254
+ * index.ts subscribeSettled 同款先例)。P-settled 定谳(0.84.4 dist 实装证据):
9255
+ * pi.on 为 per-extension 列表分发——loader.js `on()` 把 handler push 进
9256
+ * extension.handlers.get(event) 数组(非覆盖),runner.js `emit()` 对全部
9257
+ * extension 的全部 handler 逐一 await;故本注册与 ledger host 经
9258
+ * piAdapter.onAgentSettled 注册的 settled 分发互不干扰,无需降级并入 host 链。 */
9259
+ armSettledRescan() {
9260
+ if (this.settledRescanState !== null) return;
9261
+ const state = { disposed: false, scans: 0 };
9262
+ this.settledRescanState = state;
9263
+ this.pi?.on?.("agent_settled", async () => {
9264
+ if (state.disposed) return;
9265
+ state.scans += 1;
9266
+ const { outcome, waitingIds } = await this.runSyncCollectRecoveryScan();
9267
+ if (outcome === "waiting" && state.scans < SETTLED_RESCAN_LIMIT) return;
9268
+ state.disposed = true;
9269
+ if (outcome === "waiting") {
9270
+ logger26.warn(
9271
+ `[subagents] E1 settled rescan: reached limit (${SETTLED_RESCAN_LIMIT}) with member(s) still running, disposed until next session_start`,
9272
+ { ids: waitingIds }
9273
+ );
9274
+ }
9275
+ });
9276
+ }
9277
+ // [E1 语义对齐 toNotifyRecord] 补发成员映射 syncRebuildToNotifyMember 拆至
9278
+ // sync-rebuild.ts(变化轴:恢复批通知语义):one-shot 成功成员末条恒
9279
+ // running+resumable(SP-5),直通 status 会让恢复批批头「0 finished」且丢
9280
+ // patchFile 的 git-apply 指针——对齐后补发记录为 closed + outcome 物化 +
9281
+ // patchFile 透传。调用点:runSyncCollectRecoveryScan。
8585
9282
  dispose() {
8586
9283
  if (this._disposed) return;
8587
9284
  this._disposed = true;
8588
9285
  this.stopGcTimer();
9286
+ if (this.settledRescanState !== null) this.settledRescanState.disposed = true;
8589
9287
  this.uiRequestHandler = disposedUiRequestStub;
8590
9288
  this.uiObservability.resetMissingHandlerWarnings();
8591
9289
  this.store.abortRunningControllers();
8592
9290
  killAllSpawnedChildren();
9291
+ this.convertPendingSyncBufferToAsync();
8593
9292
  this.disposeAllRecords("parent-shutdown");
8594
9293
  resetAllEpipeFailures();
8595
9294
  this.resumesInFlight.clear();
@@ -8622,7 +9321,7 @@ var SubagentService = class {
8622
9321
  record: item.record
8623
9322
  });
8624
9323
  }
8625
- logger25.warn(
9324
+ logger26.warn(
8626
9325
  `[subagents] shutdown flush blocked by busy main agent: ${pending.length} pending notification(s) persisted to ledger for replay on next session_start`,
8627
9326
  { count: pending.length }
8628
9327
  );
@@ -8852,7 +9551,7 @@ var SubagentService = class {
8852
9551
  */
8853
9552
  onHotPathSettledWatchdogTimeout(record, fire) {
8854
9553
  const windowDesc = fire.phase === "mid-round" ? `no valid protocol event for ${fire.waitedMs / MS_PER_SECOND6 / SECONDS_PER_MINUTE4} min after prompt (mid-round no-progress)` : `no agent_settled within ${fire.waitedMs / MS_PER_SECOND6}s after agent_end (settled phase)`;
8855
- logger25.warn(
9554
+ logger26.warn(
8856
9555
  `[subagents] settled watchdog (${fire.phase}) fired for ${record.id}: ${windowDesc}, terminating (LC-1 wedge recovery)`
8857
9556
  );
8858
9557
  killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
@@ -8869,7 +9568,7 @@ var SubagentService = class {
8869
9568
  return;
8870
9569
  }
8871
9570
  const finalize = record.chatMode ? this.finalizeRoundToIdle(record, failedResult) : this.finalizeRecord(record, failedResult, "closed", "gc");
8872
- void finalize.then(() => this.notifyHost.notifyComplete(record)).catch((err) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
9571
+ void finalize.then(() => this.collectCoordinator.route(record)).catch((err) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
8873
9572
  }
8874
9573
  // [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
8875
9574
  // 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/engines/pi/pi-engine.ts 的
@@ -9100,6 +9799,21 @@ var SubagentService = class {
9100
9799
  collectRecords(limit, statusFilter = "all") {
9101
9800
  return this.store.collectRecords(limit, statusFilter, this.sessionRootId ?? this.sessionId ?? void 0);
9102
9801
  }
9802
+ /**
9803
+ * collectSync.default 当前生效值(subagent-sync-collect U2,偏差#3 接线:
9804
+ * startHandler 缺省 collect 解析用)。
9805
+ * config 未配/读失败 → DEFAULT_COLLECT_SYNC.default 兜底(E5 不炸启动)。
9806
+ * 新 session 生效语义与 engine 配置一致(globalConfig 由 ModelConfigService
9807
+ * reloadGlobalConfig 刷新)。
9808
+ */
9809
+ /** collectSync 节单读取点(S9,code-simplify):「读节」一处,「投影成 default 或
9810
+ * budget」各自 accessor 负责(getCollectSyncDefault / getCollectSyncBudget)。 */
9811
+ collectSyncSection() {
9812
+ return this.modelService.getGlobalConfig().collectSync;
9813
+ }
9814
+ getCollectSyncDefault() {
9815
+ return this.collectSyncSection()?.default ?? DEFAULT_COLLECT_SYNC.default;
9816
+ }
9103
9817
  /** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
9104
9818
  * 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
9105
9819
  * 返回 undefined:id 不存在于内存与磁盘。 */
@@ -9192,6 +9906,10 @@ var SubagentService = class {
9192
9906
  // 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
9193
9907
  engine: opts.engine,
9194
9908
  engineFallback: opts.engineFallback,
9909
+ // subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
9910
+ // 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
9911
+ // undefined = async(缺省语义,旧记录零迁移)。
9912
+ collectMode: opts.collect === "sync" ? "sync" : void 0,
9195
9913
  controller
9196
9914
  });
9197
9915
  this.store.register(record);
@@ -9279,7 +9997,7 @@ var SubagentService = class {
9279
9997
  try {
9280
9998
  await this.runEngineTask(record, opts, engine, signal);
9281
9999
  if (record.closedReason !== "cancelled") {
9282
- this.notifyHost.notifyComplete(record);
10000
+ this.collectCoordinator.route(record);
9283
10001
  }
9284
10002
  } finally {
9285
10003
  this.pool.release();
@@ -9556,12 +10274,12 @@ var SubagentService = class {
9556
10274
  { taskId: record.id, poolKey: PI_POOL_KEY, signal, stream }
9557
10275
  ).then(() => {
9558
10276
  if (notifyGateAllowsDelivery(record.closedReason)) {
9559
- this.notifyHost.notifyComplete(record);
10277
+ this.collectCoordinator.route(record);
9560
10278
  }
9561
10279
  }).catch((err) => {
9562
10280
  this.chatRoundTickets.delete(record.id);
9563
10281
  if (err instanceof Error) {
9564
- logger25.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
10282
+ logger26.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
9565
10283
  }
9566
10284
  });
9567
10285
  }
@@ -9654,7 +10372,7 @@ var SubagentService = class {
9654
10372
  }
9655
10373
  }
9656
10374
  this.notifyHost.emitPendingUnregister(record.id, "closed");
9657
- this.notifyHost.notifyComplete(record);
10375
+ this.collectCoordinator.route(record);
9658
10376
  return true;
9659
10377
  }
9660
10378
  /**
@@ -9825,9 +10543,9 @@ function registerPiEngine() {
9825
10543
 
9826
10544
  // src/execution/engine/engines/zcode/zcode-engine.ts
9827
10545
  import { execFile as execFile4 } from "child_process";
9828
- import * as fs22 from "fs";
10546
+ import * as fs23 from "fs";
9829
10547
  import * as os4 from "os";
9830
- import * as path16 from "path";
10548
+ import * as path17 from "path";
9831
10549
 
9832
10550
  // src/execution/engine/common/schema-emulation.ts
9833
10551
  import Ajv from "ajv";
@@ -9967,9 +10685,9 @@ function synthesizeCoarseEvents(response, usage) {
9967
10685
  }
9968
10686
 
9969
10687
  // src/execution/engine/engines/zcode/preparer.ts
9970
- import * as fs19 from "fs";
10688
+ import * as fs20 from "fs";
9971
10689
  import * as os3 from "os";
9972
- import * as path14 from "path";
10690
+ import * as path15 from "path";
9973
10691
  var ZcodePrepareError = class extends Error {
9974
10692
  code;
9975
10693
  constructor(code, message) {
@@ -9988,7 +10706,7 @@ function readSourceConfig(absPath) {
9988
10706
  const empty = { providers: /* @__PURE__ */ new Map() };
9989
10707
  let raw;
9990
10708
  try {
9991
- raw = fs19.readFileSync(absPath, "utf8");
10709
+ raw = fs20.readFileSync(absPath, "utf8");
9992
10710
  } catch {
9993
10711
  return empty;
9994
10712
  }
@@ -10022,7 +10740,7 @@ function hasApiKey(entry) {
10022
10740
  return typeof key === "string" && key !== "";
10023
10741
  }
10024
10742
  function defaultV2ConfigPath() {
10025
- return path14.join(os3.homedir(), ...ZCODE_V2_CONFIG_PATH_SUFFIX);
10743
+ return path15.join(os3.homedir(), ...ZCODE_V2_CONFIG_PATH_SUFFIX);
10026
10744
  }
10027
10745
  var DEFAULT_PROVIDER_ID = "builtin:bigmodel-coding-plan";
10028
10746
  function defaultProviderForShortName(merged, withKey) {
@@ -10085,8 +10803,8 @@ function listZcodeModels(sources) {
10085
10803
  }
10086
10804
 
10087
10805
  // src/execution/engine/engines/zcode/appserver-launcher.ts
10088
- import * as fs20 from "fs";
10089
- import * as path15 from "path";
10806
+ import * as fs21 from "fs";
10807
+ import * as path16 from "path";
10090
10808
  var ZCODE_APPSERVER_LAUNCHER_NAME = "appserver-launcher.cjs";
10091
10809
  var APPSERVER_LAUNCHER_SOURCE = `'use strict';
10092
10810
  // zcode app-server fs \u62E6\u622A wrapper\uFF08\u7531 subagent-core appserver-launcher \u843D\u76D8\u751F\u6210\uFF09
@@ -10192,15 +10910,15 @@ import(CLI_PATH).catch((err) => {
10192
10910
  });
10193
10911
  `;
10194
10912
  function ensureAppServerLauncher(engineDataDir) {
10195
- const dir = path15.join(engineDataDir, "engines", "zcode");
10196
- const file = path15.join(dir, ZCODE_APPSERVER_LAUNCHER_NAME);
10197
- const existing = fs20.existsSync(file) && fs20.statSync(file).isFile() ? fs20.readFileSync(file, "utf8") : void 0;
10913
+ const dir = path16.join(engineDataDir, "engines", "zcode");
10914
+ const file = path16.join(dir, ZCODE_APPSERVER_LAUNCHER_NAME);
10915
+ const existing = fs21.existsSync(file) && fs21.statSync(file).isFile() ? fs21.readFileSync(file, "utf8") : void 0;
10198
10916
  if (existing === APPSERVER_LAUNCHER_SOURCE) return file;
10199
10917
  try {
10200
- fs20.mkdirSync(dir, { recursive: true });
10918
+ fs21.mkdirSync(dir, { recursive: true });
10201
10919
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
10202
- fs20.writeFileSync(tmp, APPSERVER_LAUNCHER_SOURCE);
10203
- fs20.renameSync(tmp, file);
10920
+ fs21.writeFileSync(tmp, APPSERVER_LAUNCHER_SOURCE);
10921
+ fs21.renameSync(tmp, file);
10204
10922
  } catch (err) {
10205
10923
  const reason = toErrorMessage(err);
10206
10924
  throw new Error(
@@ -10212,9 +10930,9 @@ function ensureAppServerLauncher(engineDataDir) {
10212
10930
 
10213
10931
  // src/execution/engine/engines/zcode/connection.ts
10214
10932
  import { spawn as spawn2 } from "child_process";
10215
- import * as fs21 from "fs";
10933
+ import * as fs22 from "fs";
10216
10934
  import { dirname as dirname6 } from "path";
10217
- var logger26 = getLogger("subagents");
10935
+ var logger27 = getLogger("subagents");
10218
10936
  var RAW_FRAME_LOG_CHARS = 200;
10219
10937
  var RUNTIME_PREFERENCES = Object.freeze({
10220
10938
  nativeSearchEnhancementsEnabled: true,
@@ -10419,7 +11137,7 @@ var AppServerConnection = class {
10419
11137
  try {
10420
11138
  this.onSpawned(child);
10421
11139
  } catch (err) {
10422
- logger26.warn(`onSpawned \u56DE\u8C03\u5F02\u5E38\uFF08\u5FFD\u7565\uFF09: ${errMessage(err)}`);
11140
+ logger27.warn(`onSpawned \u56DE\u8C03\u5F02\u5E38\uFF08\u5FFD\u7565\uFF09: ${errMessage(err)}`);
10423
11141
  }
10424
11142
  }
10425
11143
  let finalized = false;
@@ -10438,7 +11156,7 @@ var AppServerConnection = class {
10438
11156
  try {
10439
11157
  fn(reason);
10440
11158
  } catch (err2) {
10441
- logger26.warn(`close handler \u5F02\u5E38: ${errMessage(err2)}`);
11159
+ logger27.warn(`close handler \u5F02\u5E38: ${errMessage(err2)}`);
10442
11160
  }
10443
11161
  }
10444
11162
  };
@@ -10478,14 +11196,14 @@ var AppServerConnection = class {
10478
11196
  try {
10479
11197
  frame = JSON.parse(text);
10480
11198
  } catch {
10481
- logger26.warn(`\u65E0\u6CD5\u89E3\u6790\u7684\u534F\u8BAE\u884C\uFF08\u5FFD\u7565\uFF09: ${text.slice(0, RAW_FRAME_LOG_CHARS)}`);
11199
+ logger27.warn(`\u65E0\u6CD5\u89E3\u6790\u7684\u534F\u8BAE\u884C\uFF08\u5FFD\u7565\uFF09: ${text.slice(0, RAW_FRAME_LOG_CHARS)}`);
10482
11200
  return;
10483
11201
  }
10484
11202
  this.handleFrame(frame, text);
10485
11203
  }
10486
11204
  handleFrame(frame, rawText) {
10487
11205
  if (!isRecord2(frame)) {
10488
- logger26.warn(`\u975E\u5BF9\u8C61\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
11206
+ logger27.warn(`\u975E\u5BF9\u8C61\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
10489
11207
  return;
10490
11208
  }
10491
11209
  if (isRecord2(frame.protocol)) this.capturedProtocolInfo = frame.protocol;
@@ -10500,19 +11218,19 @@ var AppServerConnection = class {
10500
11218
  }
10501
11219
  if (frame.id !== void 0 && frame.id !== null) {
10502
11220
  if (typeof frame.id !== "number") {
10503
- logger26.warn(`\u5E94\u7B54 id \u975E\u6570\u5B57\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
11221
+ logger27.warn(`\u5E94\u7B54 id \u975E\u6570\u5B57\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
10504
11222
  return;
10505
11223
  }
10506
11224
  this.settlePending(frame.id, frame);
10507
11225
  return;
10508
11226
  }
10509
11227
  if (isRecord2(frame.protocol)) return;
10510
- logger26.warn(`\u65E0\u6CD5\u5F52\u7C7B\u7684\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
11228
+ logger27.warn(`\u65E0\u6CD5\u5F52\u7C7B\u7684\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
10511
11229
  }
10512
11230
  settlePending(id, frame) {
10513
11231
  const entry = this.pending.get(id);
10514
11232
  if (!entry) {
10515
- logger26.warn(`\u54CD\u5E94\u65E0\u5339\u914D\u8BF7\u6C42 id=${id}\uFF08\u5FFD\u7565\uFF09`);
11233
+ logger27.warn(`\u54CD\u5E94\u65E0\u5339\u914D\u8BF7\u6C42 id=${id}\uFF08\u5FFD\u7565\uFF09`);
10516
11234
  return;
10517
11235
  }
10518
11236
  this.pending.delete(id);
@@ -10538,7 +11256,7 @@ var AppServerConnection = class {
10538
11256
  try {
10539
11257
  fn(params);
10540
11258
  } catch (err) {
10541
- logger26.warn(`push handler \u5F02\u5E38\uFF08${method}\uFF09: ${errMessage(err)}`);
11259
+ logger27.warn(`push handler \u5F02\u5E38\uFF08${method}\uFF09: ${errMessage(err)}`);
10542
11260
  }
10543
11261
  }
10544
11262
  }
@@ -10548,12 +11266,12 @@ var AppServerConnection = class {
10548
11266
  answerReverse(id, method, params) {
10549
11267
  const handler = this.reverseHandlers[method];
10550
11268
  if (!handler) {
10551
- logger26.warn(`\u672A\u77E5\u53CD\u5411\u8BF7\u6C42 ${method}\uFF08id=${id}\uFF09\uFF1A\u56DE\u7A7A result\uFF08\u4E0D\u7B54\u4F1A 15s \u8D85\u65F6\u65AD\u8FDE\uFF0C\u65E7\u5B9E\u6D4B -32022\uFF09`);
11269
+ logger27.warn(`\u672A\u77E5\u53CD\u5411\u8BF7\u6C42 ${method}\uFF08id=${id}\uFF09\uFF1A\u56DE\u7A7A result\uFF08\u4E0D\u7B54\u4F1A 15s \u8D85\u65F6\u65AD\u8FDE\uFF0C\u65E7\u5B9E\u6D4B -32022\uFF09`);
10552
11270
  this.writeFrame({ id, result: {} });
10553
11271
  return;
10554
11272
  }
10555
11273
  Promise.resolve().then(() => handler(params)).then((result) => this.writeFrame({ id, result: result ?? {} })).catch((err) => {
10556
- logger26.warn(`\u53CD\u5411\u8BF7\u6C42 ${method} handler \u5F02\u5E38: ${errMessage(err)}`);
11274
+ logger27.warn(`\u53CD\u5411\u8BF7\u6C42 ${method} handler \u5F02\u5E38: ${errMessage(err)}`);
10557
11275
  this.writeFrame({ id, error: { code: -32e3, message: errMessage(err) } });
10558
11276
  });
10559
11277
  }
@@ -10569,7 +11287,7 @@ var AppServerConnection = class {
10569
11287
  `);
10570
11288
  return true;
10571
11289
  } catch (err) {
10572
- logger26.warn(`\u5199\u5165 app-server \u5931\u8D25: ${errMessage(err)}`);
11290
+ logger27.warn(`\u5199\u5165 app-server \u5931\u8D25: ${errMessage(err)}`);
10573
11291
  return false;
10574
11292
  }
10575
11293
  }
@@ -10578,8 +11296,8 @@ var AppServerConnection = class {
10578
11296
  if (this.stderrStreamFailed) return;
10579
11297
  if (this.stderrStream === null) {
10580
11298
  try {
10581
- fs21.mkdirSync(dirname6(this.stderrLogPath), { recursive: true });
10582
- this.stderrStream = fs21.createWriteStream(this.stderrLogPath, { flags: "a" });
11299
+ fs22.mkdirSync(dirname6(this.stderrLogPath), { recursive: true });
11300
+ this.stderrStream = fs22.createWriteStream(this.stderrLogPath, { flags: "a" });
10583
11301
  this.stderrStream.on("error", () => {
10584
11302
  this.stderrStreamFailed = true;
10585
11303
  });
@@ -10601,8 +11319,8 @@ var AppServerConnection = class {
10601
11319
  };
10602
11320
 
10603
11321
  // src/execution/engine/engines/zcode/session-channel.ts
10604
- import { createHash } from "crypto";
10605
- var logger27 = getLogger("subagents");
11322
+ import { createHash as createHash2 } from "crypto";
11323
+ var logger28 = getLogger("subagents");
10606
11324
  var SUBSCRIBE_DELIVERY_KIND = "desktop-continuous";
10607
11325
  var WORKSPACE_KEY_HASH_CHARS = 16;
10608
11326
  var CREATE_REPLY_LOG_CHARS = 300;
@@ -10614,7 +11332,7 @@ function errMessage2(err) {
10614
11332
  return toErrorMessage(err);
10615
11333
  }
10616
11334
  function stableWorkspaceKey(workspacePath) {
10617
- return "ws-" + createHash("sha256").update(workspacePath).digest("hex").slice(0, WORKSPACE_KEY_HASH_CHARS);
11335
+ return "ws-" + createHash2("sha256").update(workspacePath).digest("hex").slice(0, WORKSPACE_KEY_HASH_CHARS);
10618
11336
  }
10619
11337
  function extractCreatedSessionId(created) {
10620
11338
  if (!isRecord3(created)) return void 0;
@@ -10744,7 +11462,7 @@ function resolveTurnTimerMs(parts) {
10744
11462
  source = `env ${parts.envName}=${raw}`;
10745
11463
  } else {
10746
11464
  if (parsed.state === "invalid") {
10747
- logger27.warn(
11465
+ logger28.warn(
10748
11466
  `[session-channel] ${parts.envName}="${raw}" \u975E\u6CD5\uFF08\u5E94\u4E3A\u6BEB\u79D2\u6570\u5B57\uFF09\u2014\u2014\u56DE\u843D\u9ED8\u8BA4 ${parts.fallbackMs}ms\uFF08${parts.label}\uFF09\u3002\u8BBE\u7F6E\u6B63\u6BEB\u79D2\u503C\u8986\u76D6\uFF0C\u6216 0 \u663E\u5F0F\u5173\u95ED`
10749
11467
  );
10750
11468
  }
@@ -10753,7 +11471,7 @@ function resolveTurnTimerMs(parts) {
10753
11471
  }
10754
11472
  }
10755
11473
  if (value <= 0) {
10756
- logger27.warn(
11474
+ logger28.warn(
10757
11475
  `[session-channel] zcode turn ${parts.label}\u5DF2\u5173\u95ED\uFF08${source}\uFF09\u2014\u2014${parts.offConsequence}\u3002\u8BBE\u6B63\u6BEB\u79D2\u503C\uFF08env ${parts.envName} \u6216\u663E\u5F0F\u4F20\u53C2\uFF09\u6062\u590D\u56DE\u6536\u5C42`
10758
11476
  );
10759
11477
  }
@@ -10875,7 +11593,7 @@ var SessionChannel = class {
10875
11593
  { timeoutMs: ZCODE_APPSERVER_TURN_CLOSE_TIMEOUT_MS }
10876
11594
  );
10877
11595
  } catch (err) {
10878
- logger27.warn(
11596
+ logger28.warn(
10879
11597
  `session/close \u5931\u8D25\uFF08\u4F1A\u8BDD ${sessionId}\uFF0Cbest-effort \u5FFD\u7565\uFF09: ${errMessage2(
10880
11598
  err
10881
11599
  )}`
@@ -11076,7 +11794,7 @@ var SessionChannel = class {
11076
11794
  applyStreamDelta(turn, payload) {
11077
11795
  if (typeof payload.delta !== "string" || payload.delta === "") return;
11078
11796
  if (turn.settled) {
11079
- logger27.warn(
11797
+ logger28.warn(
11080
11798
  `\u7EC8\u6001\u540E\u8FDF\u5230\u7684 delta \u4E22\u5F03\uFF08\u4F1A\u8BDD ${turn.sessionId}\uFF0C\u4E0D\u53D8\u91CF 2\uFF1Aresolve \u540E\u4E0D\u518D\u53D1\u4E8B\u4EF6\uFF09: ${payload.delta.slice(
11081
11799
  0,
11082
11800
  DELTA_LOG_CHARS
@@ -11105,7 +11823,7 @@ var SessionChannel = class {
11105
11823
  turn.lastTerminalStatus = status;
11106
11824
  this.recordTerminalError(turn, params);
11107
11825
  if (turn.settled) {
11108
- logger27.warn(
11826
+ logger28.warn(
11109
11827
  `\u6743\u5A01\u7EC8\u6001\u665A\u4E8E\u843D\u5B9A\u7ED3\u679C\u5230\u8FBE\uFF08\u4F1A\u8BDD ${turn.sessionId}\uFF0C\u5DF2\u843D\u5B9A source=${turn.terminal?.source}\uFF09\uFF1Aturn.terminal status="${status}" \u4EC5\u8BB0\u5F55\u4E0D\u6539\u5199\uFF08P0-1 D5\u2460\uFF09`
11110
11828
  );
11111
11829
  return;
@@ -11151,7 +11869,7 @@ var SessionChannel = class {
11151
11869
  }
11152
11870
  );
11153
11871
  } catch (err) {
11154
- logger27.warn(
11872
+ logger28.warn(
11155
11873
  `session/read \u515C\u5E95\u5931\u8D25\uFF08\u4F1A\u8BDD ${sessionId}\uFF0C\u964D\u7EA7\u6536\u5C3E\u5E27/delta \u805A\u5408\uFF09: ${errMessage2(
11156
11874
  err
11157
11875
  )}`
@@ -11162,11 +11880,11 @@ var SessionChannel = class {
11162
11880
  };
11163
11881
 
11164
11882
  // src/execution/engine/engines/zcode/zcode-engine.ts
11165
- var logger28 = getLogger("subagents");
11883
+ var logger29 = getLogger("subagents");
11166
11884
  var PROBE_VERSION_TIMEOUT_MS2 = 15e3;
11167
11885
  var COMMON_THOUGHT_LEVELS = ["low", "high", "max"];
11168
11886
  function hostZcodeDbPath() {
11169
- return path16.join(os4.homedir(), ...ZCODE_HOST_DB_SUFFIX);
11887
+ return path17.join(os4.homedir(), ...ZCODE_HOST_DB_SUFFIX);
11170
11888
  }
11171
11889
  var ZcodeEngine = class {
11172
11890
  id = ZCODE_ENGINE_ID;
@@ -11237,7 +11955,7 @@ var ZcodeEngine = class {
11237
11955
  }
11238
11956
  /** check 1:二进制存在性(isFile 才算——同名目录不是可执行入口)。 */
11239
11957
  probeBinaryCheck(cliPath) {
11240
- const binaryOk = fs22.existsSync(cliPath) && fs22.statSync(cliPath).isFile();
11958
+ const binaryOk = fs23.existsSync(cliPath) && fs23.statSync(cliPath).isFile();
11241
11959
  return {
11242
11960
  name: "binary",
11243
11961
  ok: binaryOk,
@@ -11331,11 +12049,11 @@ var ZcodeEngine = class {
11331
12049
  if (final.kind === "run-failed" && final.transient !== void 0 && ctx.signal?.aborted !== true && !this.disposed) {
11332
12050
  const budget = resolveTransientRetryBudget(explicitTurnBudgetMs(), Date.now() - attemptStartedAt);
11333
12051
  if (budget.state === "depleted") {
11334
- logger28.warn(
12052
+ logger29.warn(
11335
12053
  `[zcode-engine] \u672B\u6B21 attempt \u77AC\u65F6\u5931\u8D25\uFF08${final.transient}\uFF09\u2014\u2014\u663E\u5F0F\u603B\u4E0A\u754C\u9884\u7B97\u5269\u4F59\u4E0D\u8DB3 ${ZCODE_TURN_RETRY_MIN_BUDGET_MS}ms\uFF0C\u4E0D\u91CD\u8BD5\u76F4\u63A5\u7EC8\u6001\u5316\uFF08\u9884\u7B97\u7EE7\u627F\uFF1A\u91CD\u8BD5\u4E0D\u91CD\u7F6E\u603B\u9884\u7B97\uFF09`
11336
12054
  );
11337
12055
  } else {
11338
- logger28.warn(
12056
+ logger29.warn(
11339
12057
  `[zcode-engine] \u672B\u6B21 attempt \u77AC\u65F6\u5931\u8D25\uFF08${final.transient}\uFF09\u2014\u2014\u6B62\u635F\u94FE\u5DF2\u7EC8\u5C40\uFF0C\u65B0\u4F1A\u8BDD\u81EA\u52A8\u91CD\u8BD5\u4E00\u6B21` + (budget.state === "inherit" ? `\uFF08\u9884\u7B97\u7EE7\u627F\uFF1A\u91CD\u8BD5\u8F6E\u603B\u4E0A\u754C=\u5269\u4F59 ${budget.remainingMs}ms\uFF0C\u4E0D\u91CD\u7F6E\u603B\u9884\u7B97\uFF09` : "\uFF08\u65E0\u663E\u5F0F\u603B\u4E0A\u754C\u9884\u7B97\uFF0C\u91CD\u8BD5\u8F6E\u8D70 env/\u9ED8\u8BA4\u4E0A\u754C\uFF09")
11340
12058
  );
11341
12059
  const retry = await this.attemptAppServerTurn(task, ctx, modelRef, cwd, basePrompt, {
@@ -11489,7 +12207,7 @@ var ZcodeEngine = class {
11489
12207
  delayResolved(ZCODE_APPSERVER_ABORT_GRACE_MS, false)
11490
12208
  ]);
11491
12209
  if (settled) return "settled-in-grace";
11492
- logger28.warn(
12210
+ logger29.warn(
11493
12211
  `[zcode-engine] abort grace \u7A97\u53E3\u5185\u672A\u89C1\u7EC8\u6001\u2014\u2014killChain \u6536\u5272\u5171\u4EAB\u8FDB\u7A0B\uFF08\u63A5\u53D7\u8FDE\u5750\uFF0C\u5728\u9014\u4EFB\u52A1\u8D70\u5D29\u6E83\u8DEF\u5F84\uFF09`
11494
12212
  );
11495
12213
  await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
@@ -11514,19 +12232,19 @@ var ZcodeEngine = class {
11514
12232
  } catch (err) {
11515
12233
  if (entry.escalateOn === "stop-outcome") {
11516
12234
  if (isAppServerRpcError(err)) {
11517
- logger28.debug(
12235
+ logger29.debug(
11518
12236
  `[zcode-engine] session/stop \u62A5\u534F\u8BAE\u6027 error\uFF08${errMessage3(err)}\uFF09\u2014\u2014\u4F1A\u8BDD\u5DF2\u56DE\u6536\uFF0C\u63A7\u5236\u9762\u5B58\u6D3B\uFF0C\u4E0D\u5347\u7EA7\u6740\u94FE`
11519
12237
  );
11520
12238
  return "stop-rejected";
11521
12239
  }
11522
- logger28.warn(
12240
+ logger29.warn(
11523
12241
  `[zcode-engine] session/stop \u65E0\u5E94\u7B54\uFF08${errMessage3(err)}\uFF09\u2014\u2014\u5347\u7EA7 killChain \u6536\u5272\u5171\u4EAB\u8FDB\u7A0B\uFF08\u8D85\u65F6\u5165\u53E3\uFF0C\u63A5\u53D7\u8FDE\u5750\uFF09`
11524
12242
  );
11525
12243
  await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
11526
12244
  await this.awaitConnFinalized(rt);
11527
12245
  return "stop-unreachable-killed";
11528
12246
  }
11529
- logger28.debug(
12247
+ logger29.debug(
11530
12248
  `[zcode-engine] session/stop \u5931\u8D25\uFF08${errMessage3(err)}\uFF09\u2014\u2014grace \u540E\u8D70 killChain \u515C\u5E95`
11531
12249
  );
11532
12250
  }
@@ -11557,7 +12275,7 @@ var ZcodeEngine = class {
11557
12275
  cwd: engineDataDir,
11558
12276
  env,
11559
12277
  launcherScript,
11560
- stderrLogPath: path16.join(engineDataDir, "logs", "zcode-appserver-stderr.log")
12278
+ stderrLogPath: path17.join(engineDataDir, "logs", "zcode-appserver-stderr.log")
11561
12279
  });
11562
12280
  const rt = {
11563
12281
  conn,
@@ -11722,15 +12440,15 @@ var ZcodeEngine = class {
11722
12440
  const dbPathRaw = handle.data.sessionRef["dbPath"];
11723
12441
  if (typeof sessionId === "string" && typeof dbPathRaw === "string") {
11724
12442
  let dbPath;
11725
- if (path16.isAbsolute(dbPathRaw)) {
12443
+ if (path17.isAbsolute(dbPathRaw)) {
11726
12444
  if (dbPathRaw === hostZcodeDbPath()) dbPath = dbPathRaw;
11727
12445
  else {
11728
- logger28.warn("[zcode-engine] record dbPath \u975E\u5BBF\u4E3B db \u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u62D2\u7EDD \u2460\u7EA7\u8BFB\u53D6\u964D journal", {
12446
+ logger29.warn("[zcode-engine] record dbPath \u975E\u5BBF\u4E3B db \u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u62D2\u7EDD \u2460\u7EA7\u8BFB\u53D6\u964D journal", {
11729
12447
  dbPath: dbPathRaw
11730
12448
  });
11731
12449
  }
11732
12450
  } else {
11733
- dbPath = path16.join(
12451
+ dbPath = path17.join(
11734
12452
  resolvePoolDir(this.deps.engineDataDir(), ZCODE_ENGINE_ID, handle.data.poolKey),
11735
12453
  dbPathRaw
11736
12454
  );
@@ -11739,7 +12457,7 @@ var ZcodeEngine = class {
11739
12457
  try {
11740
12458
  return await readZcodeSessionView(dbPath, sessionId);
11741
12459
  } catch (err) {
11742
- logger28.warn("[zcode-engine] native session read failed, degrade to journal replay", {
12460
+ logger29.warn("[zcode-engine] native session read failed, degrade to journal replay", {
11743
12461
  dbPath,
11744
12462
  sessionId,
11745
12463
  reason: toErrorMessage(err)
@@ -11765,7 +12483,7 @@ var ZcodeEngine = class {
11765
12483
  const thoughtLevel = task.thinkingLevel?.trim();
11766
12484
  if (thoughtLevel === void 0 || thoughtLevel === "") return;
11767
12485
  if (COMMON_THOUGHT_LEVELS.includes(thoughtLevel)) return;
11768
- logger28.warn(
12486
+ logger29.warn(
11769
12487
  `[zcode-engine] thinkingLevel=${thoughtLevel} \u5DF2\u900F\u4F20\u4E3A thoughtLevel\uFF08\u975E\u5E38\u89C1\u6863\u4F4D\uFF09\uFF1A\u82E5\u76EE\u6807\u6A21\u578B\u4E0D\u652F\u6301\u8BE5\u6863\u4F4D\u5C06\u88AB\u5FFD\u7565/\u56DE\u843D\u5230\u6A21\u578B\u7F3A\u7701\u63A8\u7406\u6863\u4F4D\uFF08\u5E38\u89C1\u6863\u4F4D\uFF1A${COMMON_THOUGHT_LEVELS.join("/")}\uFF09\uFF1B\u6863\u4F4D\u662F\u5426\u751F\u6548\u4EE5\u6A21\u578B\u5B9E\u9645\u884C\u4E3A\u4E3A\u51C6`,
11770
12488
  { taskId: ctx.taskId }
11771
12489
  );
@@ -11783,7 +12501,7 @@ var ZcodeEngine = class {
11783
12501
  if (ctx.ctxModel === void 0) return;
11784
12502
  const requested = task.model?.trim();
11785
12503
  if (requested !== void 0 && requested !== "") return;
11786
- logger28.warn(
12504
+ logger29.warn(
11787
12505
  `[zcode-engine] ctx.ctxModel\uFF08${ctx.ctxModel.id}\uFF09\u88AB\u5FFD\u7565\u2014\u2014ctxModel \u662F pi \u94FE\u8DEF\u515C\u5E95\uFF0Czcode \u4E0D\u6D88\u8D39\uFF1Btask.model \u672A\u663E\u5F0F\u6307\u5B9A\uFF0C\u5B9E\u9645\u4F7F\u7528\u5F15\u64CE\u7F3A\u7701\u6A21\u578B ${modelRef}`,
11788
12506
  { taskId: ctx.taskId }
11789
12507
  );
@@ -11996,7 +12714,7 @@ async function defaultProbeVersion2(cliPath) {
11996
12714
  );
11997
12715
  });
11998
12716
  } catch (err) {
11999
- logger28.debug(
12717
+ logger29.debug(
12000
12718
  `[zcode-engine] probe version check failed (best-effort continue): ${toErrorMessage(err)}`
12001
12719
  );
12002
12720
  return void 0;
@@ -12116,7 +12834,7 @@ function parseEngineHandle(raw) {
12116
12834
  import { randomUUID as randomUUID2 } from "crypto";
12117
12835
  import { homedir as homedir5 } from "os";
12118
12836
  import { isAbsolute as isAbsolute3, relative, resolve } from "path";
12119
- var logger29 = getLogger("subagents");
12837
+ var logger30 = getLogger("subagents");
12120
12838
  var DEFAULT_ENGINE_ID2 = "pi";
12121
12839
  function extractEngineId(record) {
12122
12840
  const engine = record.engine;
@@ -12141,13 +12859,13 @@ async function readZcodeNativeTier(handle, dataDir) {
12141
12859
  const dbPathRaw = handle.sessionRef["dbPath"];
12142
12860
  const sessionId = handle.sessionRef["sessionId"];
12143
12861
  if (typeof dbPathRaw !== "string" || typeof sessionId !== "string") {
12144
- logger29.debug("[session-view-service] zcode tier1 skipped: handle missing dbPath/sessionId");
12862
+ logger30.debug("[session-view-service] zcode tier1 skipped: handle missing dbPath/sessionId");
12145
12863
  return void 0;
12146
12864
  }
12147
12865
  let dbPath;
12148
12866
  if (dbPathRaw.startsWith("/")) {
12149
12867
  if (dbPathRaw !== resolve(homedir5(), ...ZCODE_HOST_DB_SUFFIX)) {
12150
- logger29.warn(
12868
+ logger30.warn(
12151
12869
  `[session-view-service] zcode absolute dbPath not host db, reject tier1: ${dbPathRaw}`
12152
12870
  );
12153
12871
  return void 0;
@@ -12157,14 +12875,14 @@ async function readZcodeNativeTier(handle, dataDir) {
12157
12875
  const poolDir = resolvePoolDir(dataDir, ZCODE_ENGINE_ID, handle.poolKey);
12158
12876
  dbPath = resolve(poolDir, dbPathRaw);
12159
12877
  if (!isStrictlyUnder(poolDir, dbPath)) {
12160
- logger29.warn(`[session-view-service] zcode dbPath escapes pool dir, reject tier1: ${dbPath}`);
12878
+ logger30.warn(`[session-view-service] zcode dbPath escapes pool dir, reject tier1: ${dbPath}`);
12161
12879
  return void 0;
12162
12880
  }
12163
12881
  }
12164
12882
  try {
12165
12883
  return await readZcodeSessionView(dbPath, sessionId);
12166
12884
  } catch (err) {
12167
- logger29.debug(
12885
+ logger30.debug(
12168
12886
  `[session-view-service] zcode tier1 read failed, degrade to journal tier: ${toErrorMessage(err)}`
12169
12887
  );
12170
12888
  return void 0;
@@ -12173,18 +12891,18 @@ async function readZcodeNativeTier(handle, dataDir) {
12173
12891
  function readJournalTier(record, handle, dataDir) {
12174
12892
  const journalPath = handle.journalPath;
12175
12893
  if (journalPath === void 0) {
12176
- logger29.debug("[session-view-service] tier2 skipped: no journalPath in handle");
12894
+ logger30.debug("[session-view-service] tier2 skipped: no journalPath in handle");
12177
12895
  return void 0;
12178
12896
  }
12179
12897
  if (!isStrictlyUnder(resolveEnginesRoot(dataDir), journalPath)) {
12180
- logger29.warn(
12898
+ logger30.warn(
12181
12899
  `[session-view-service] journalPath escapes engines root, reject tier2: ${journalPath}`
12182
12900
  );
12183
12901
  return void 0;
12184
12902
  }
12185
12903
  const messages = replayEventsToHistory(replayJournal(journalPath), record);
12186
12904
  if (messages === void 0) {
12187
- logger29.debug(
12905
+ logger30.debug(
12188
12906
  `[session-view-service] tier2 journal replay produced no content, degrade to outcome-only (path=${journalPath})`
12189
12907
  );
12190
12908
  }
@@ -12354,14 +13072,14 @@ async function readSubagentHistoryMessages(record, dataDir) {
12354
13072
  if (engineId === DEFAULT_ENGINE_ID2) return [];
12355
13073
  const handle = parseEngineHandle(record.engineHandle);
12356
13074
  if (handle === void 0) {
12357
- logger29.debug(
13075
+ logger30.debug(
12358
13076
  `[session-view-service] engine '${engineId}' record has no engineHandle, degrade to outcome-only (subagentId=${record.subagentId})`
12359
13077
  );
12360
13078
  return outcomeOnlyMessages(record);
12361
13079
  }
12362
13080
  const reader = lookupNativeSessionReader(engineId);
12363
13081
  if (reader === void 0) {
12364
- logger29.debug(
13082
+ logger30.debug(
12365
13083
  `[session-view-service] engine '${engineId}' has no native reader tier, degrade to outcome-only (subagentId=${record.subagentId})`
12366
13084
  );
12367
13085
  return outcomeOnlyMessages(record);
@@ -12378,14 +13096,14 @@ function isStrictlyUnder(parent, child) {
12378
13096
  }
12379
13097
 
12380
13098
  // src/execution/agent-registry.ts
12381
- import * as path17 from "path";
13099
+ import * as path18 from "path";
12382
13100
 
12383
13101
  // src/shared/resource-discovery.ts
12384
13102
  import * as fsSync2 from "fs";
12385
13103
  import { access, readdir, readFile as readFile2, realpath, stat } from "fs/promises";
12386
13104
  import { homedir as homedir6 } from "os";
12387
- import { delimiter as delimiter2, join as join19, resolve as resolve2 } from "path";
12388
- var logger30 = getLogger("subagents");
13105
+ import { delimiter as delimiter2, join as join20, resolve as resolve2 } from "path";
13106
+ var logger31 = getLogger("subagents");
12389
13107
  var shadowWarnDedup = /* @__PURE__ */ new Set();
12390
13108
  var MAX_SHADOW_WARN_DEDUP = 1024;
12391
13109
  var MACHINE_SOURCES = /* @__PURE__ */ new Set([
@@ -12625,7 +13343,7 @@ async function scanNpmDir(nodeModulesDir, kind) {
12625
13343
  function readExtensionPaths() {
12626
13344
  const raw = process.env.XYZ_EXTENSION_PATHS;
12627
13345
  if (!raw) return [];
12628
- const paths = raw.split(delimiter2).map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.startsWith("~") ? join19(homedir6(), p.slice(1)) : p);
13346
+ const paths = raw.split(delimiter2).map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.startsWith("~") ? join20(homedir6(), p.slice(1)) : p);
12629
13347
  return [...new Set(paths)];
12630
13348
  }
12631
13349
  function buildScanTargets(config) {
@@ -12641,16 +13359,16 @@ function buildScanTargets(config) {
12641
13359
  const targets = [];
12642
13360
  targets.push(...hostTargets("user-pi"));
12643
13361
  targets.push(...hostTargets("user-agents"));
12644
- targets.push({ dir: join19(home, ".agents", kind), source: "user-agents", enabled: true });
13362
+ targets.push({ dir: join20(home, ".agents", kind), source: "user-agents", enabled: true });
12645
13363
  targets.push(...hostTargets("npm"));
12646
13364
  targets.push(...hostTargets("npm-dev"));
12647
13365
  targets.push(
12648
13366
  ...readExtensionPaths().map((dir) => ({ dir, source: "user-extension-paths", enabled: true }))
12649
13367
  );
12650
- targets.push({ dir: join19(workspaceRoot, ".pi", kind), source: "project-pi", enabled: true });
13368
+ targets.push({ dir: join20(workspaceRoot, ".pi", kind), source: "project-pi", enabled: true });
12651
13369
  if (includeTmp) {
12652
13370
  targets.push({
12653
- dir: join19(workspaceRoot, ".pi", kind, ".tmp"),
13371
+ dir: join20(workspaceRoot, ".pi", kind, ".tmp"),
12654
13372
  source: "project-pi-tmp",
12655
13373
  enabled: true
12656
13374
  });
@@ -12658,7 +13376,7 @@ function buildScanTargets(config) {
12658
13376
  targets.push(...hostTargets("project-host"));
12659
13377
  targets.push(...hostTargets("project-agents"));
12660
13378
  targets.push({
12661
- dir: join19(workspaceRoot, ".agents", kind),
13379
+ dir: join20(workspaceRoot, ".agents", kind),
12662
13380
  source: "project-agents",
12663
13381
  enabled: true
12664
13382
  });
@@ -12708,7 +13426,7 @@ async function discoverResources(config) {
12708
13426
  const msg = `[resource-discovery] duplicate ${config.kind} "${key}" from ${r.source} shadows ${existing.source}`;
12709
13427
  const data = { shadowed: existing.path, kept: r.path };
12710
13428
  if (isMachineSource(existing.source) || isMachineSource(r.source)) {
12711
- logger30.debug(msg, data);
13429
+ logger31.debug(msg, data);
12712
13430
  } else {
12713
13431
  const dedupKey = `${config.kind}|${key}|${existing.path}|${r.path}`;
12714
13432
  if (!shadowWarnDedup.has(dedupKey)) {
@@ -12716,7 +13434,7 @@ async function discoverResources(config) {
12716
13434
  shadowWarnDedup.clear();
12717
13435
  }
12718
13436
  shadowWarnDedup.add(dedupKey);
12719
- logger30.warn(msg, data);
13437
+ logger31.warn(msg, data);
12720
13438
  }
12721
13439
  }
12722
13440
  }
@@ -13357,10 +14075,10 @@ function lintScript(source) {
13357
14075
  }
13358
14076
 
13359
14077
  // src/execution/agent-registry.ts
13360
- var logger31 = getLogger("subagents");
14078
+ var logger32 = getLogger("subagents");
13361
14079
  var FM_DELIM = "---";
13362
14080
  function parseAgentWithMeta(filePath, content) {
13363
- const name = path17.basename(filePath, ".md");
14081
+ const name = path18.basename(filePath, ".md");
13364
14082
  const fm = splitAgentFrontmatter(content);
13365
14083
  if (fm.kind === "none") {
13366
14084
  return { config: { name, systemPrompt: content.trim() }, meta: null };
@@ -13390,7 +14108,7 @@ function resolveLegacyRoutingFallbacks(agentMeta, yamlBlock, filePath) {
13390
14108
  const modelFallback = extractYamlField(yamlBlock, "model");
13391
14109
  const toolsFallback = parseCommaListFallback(extractYamlField(yamlBlock, "tools"));
13392
14110
  if (!agentMeta && /^model:|^tools:/m.test(yamlBlock)) {
13393
- logger31.warn(
14111
+ logger32.warn(
13394
14112
  `[agent-registry] ${filePath}: agent frontmatter \u7F3A name/description\uFF08IF1 \u5FC5\u586B\uFF09\uFF0Cmodel/tools \u7ECF legacy fallback \u751F\u6548\uFF08\u76F4\u63A5\u8DEF\u5F84\u4E0D\u4E22\u914D\u7F6E\uFF09\uFF0C\u4F46\u7ED3\u6784\u5316\u8DEF\u7531\u4E0D\u53EF\u89C1\u2014\u2014\u8BF7\u8865\u5145 description`
13395
14113
  );
13396
14114
  }
@@ -13428,7 +14146,7 @@ function extractYamlField(yaml, key) {
13428
14146
  return value || void 0;
13429
14147
  }
13430
14148
  function parseAgentProfile(text, filePath) {
13431
- const stem3 = path17.basename(filePath, ".md");
14149
+ const stem3 = path18.basename(filePath, ".md");
13432
14150
  const warnings = [];
13433
14151
  const fm = splitAgentFrontmatter(text);
13434
14152
  if (fm.kind === "none") {
@@ -13566,84 +14284,13 @@ var AgentRegistry = class {
13566
14284
  const { config, meta } = parseAgentWithMeta(filePath, file.content);
13567
14285
  const lintFindings = meta ? lintAgentMeta(meta) : [];
13568
14286
  for (const finding of lintFindings) {
13569
- logger31.warn(`[agent-registry] ${filePath}: ${finding.message}`);
14287
+ logger32.warn(`[agent-registry] ${filePath}: ${finding.message}`);
13570
14288
  }
13571
14289
  this.fileCache.set(filePath, { mtimeMs: file.mtimeMs, config, meta });
13572
14290
  return config;
13573
14291
  }
13574
14292
  };
13575
14293
 
13576
- // src/execution/config.ts
13577
- import * as fs23 from "fs";
13578
- import * as path18 from "path";
13579
- var logger32 = getLogger("subagents");
13580
- var DEFAULT_CONFIG = {
13581
- version: 1,
13582
- maxConcurrent: 6
13583
- };
13584
- var DEFAULT_MAX_CONCURRENT = 6;
13585
- function getGlobalConfigPath(agentDir) {
13586
- return path18.join(agentDir, "subagents", "config.json");
13587
- }
13588
- function loadGlobalConfig(agentDir) {
13589
- try {
13590
- const raw = fs23.readFileSync(getGlobalConfigPath(agentDir), "utf-8");
13591
- const parsed = JSON.parse(raw);
13592
- return sanitizeParsedConfig(parsed);
13593
- } catch {
13594
- return { ...DEFAULT_CONFIG };
13595
- }
13596
- }
13597
- function readGlobalConfig(agentDir) {
13598
- const configPath = getGlobalConfigPath(agentDir);
13599
- let raw;
13600
- try {
13601
- raw = fs23.readFileSync(configPath, "utf-8");
13602
- } catch (err) {
13603
- if (errnoCodeOf(err) === "ENOENT") {
13604
- return { status: "absent", config: { ...DEFAULT_CONFIG } };
13605
- }
13606
- return readFailure(configPath, err);
13607
- }
13608
- try {
13609
- const parsed = JSON.parse(raw);
13610
- return { status: "ok", config: sanitizeParsedConfig(parsed) };
13611
- } catch (err) {
13612
- return readFailure(configPath, err);
13613
- }
13614
- }
13615
- function readFailure(configPath, err) {
13616
- const reason = err instanceof Error ? err.message : String(err);
13617
- logger32.warn(`[subagents] global config read failed (read-failure) at ${configPath}: ${reason}`);
13618
- return { status: "failed", reason };
13619
- }
13620
- function errnoCodeOf(err) {
13621
- if (typeof err !== "object" || err === null || !("code" in err)) return void 0;
13622
- const code = Reflect.get(err, "code");
13623
- return typeof code === "string" ? code : void 0;
13624
- }
13625
- function sanitizeParsedConfig(parsed) {
13626
- const defaultEngine = sanitizeDefaultEngine(parsed.defaultEngine);
13627
- const engineRouting = sanitizeEngineRouting(parsed.engineRouting);
13628
- return {
13629
- version: parsed.version ?? DEFAULT_CONFIG.version,
13630
- maxConcurrent: sanitizeMaxConcurrent(parsed.maxConcurrent),
13631
- ...defaultEngine !== void 0 ? { defaultEngine } : {},
13632
- ...engineRouting !== void 0 ? { engineRouting } : {}
13633
- };
13634
- }
13635
- function sanitizeMaxConcurrent(value) {
13636
- return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : DEFAULT_MAX_CONCURRENT;
13637
- }
13638
- function sanitizeDefaultEngine(value) {
13639
- return typeof value === "string" && value.trim() !== "" ? value : void 0;
13640
- }
13641
- function sanitizeEngineRouting(value) {
13642
- if (typeof value !== "object" || value === null) return void 0;
13643
- const strict = value.strict;
13644
- return typeof strict === "boolean" ? { strict } : void 0;
13645
- }
13646
-
13647
14294
  // src/execution/model-resolver.ts
13648
14295
  var MODEL_LIST_LIMIT2 = 20;
13649
14296
  function resolveModel(agentConfig, modelRegistry, paramOverride, ctxModel) {
@@ -14593,6 +15240,12 @@ async function startHandler(service, input, signal, ctxModel) {
14593
15240
  `slug is required for action:'start' (top-level field, must not be whitespace-only). Correct: {"action":"start","task":"...","slug":"<kebab-case>"}`
14594
15241
  );
14595
15242
  if (slug.length > SLUG_MAX_LENGTH) throw new Error(`slug must be \u2264${SLUG_MAX_LENGTH} chars (got ${slug.length}). Shorten to a kebab-case label, e.g. "fix-login", "extract-urls".`);
15243
+ const resolvedCollect = input.collect ?? service.getCollectSyncDefault();
15244
+ if (input.conversation === true && resolvedCollect === "sync") {
15245
+ throw new Error(
15246
+ 'collect:"sync" only supports one-shot subagents \u2014 it cannot be combined with conversation:true. Remove either conversation or collect (use collect:"async", or omit it, for conversational subagents).'
15247
+ );
15248
+ }
14596
15249
  const handle = await service.execute({
14597
15250
  task,
14598
15251
  slug,
@@ -14610,10 +15263,25 @@ async function startHandler(service, input, signal, ctxModel) {
14610
15263
  conversation: input.conversation,
14611
15264
  idleTimeoutMs: input.idleTimeoutMs,
14612
15265
  engine: input.engine,
15266
+ // B1(code-simplify 审查发现的行为缺口):config collectSync.default=sync 且调用方
15267
+ // 省略 collect 时,record 本体也要落 sync(设计 §3.1.3「缺省 = config 默认」作用于
15268
+ // record,而非仅回显)——createRecordForMode 只认 opts.collect==="sync",原样透传
15269
+ // input.collect 会让 record 走 async 逐条通知而响应声称已入批。仅 sync 落值:
15270
+ // async/缺省路径传 undefined 语义(旧 record 零迁移)字节不变。
15271
+ collect: resolvedCollect === "sync" ? "sync" : input.collect,
14613
15272
  ctxModel,
14614
15273
  signal
14615
15274
  // background detached 运行,完成由 notify 驱动新 turn。
14616
15275
  });
15276
+ const response = {
15277
+ status: "running",
15278
+ mode: "background",
15279
+ message: BG_MESSAGE,
15280
+ notifyContract: NOTIFY_CONTRACT
15281
+ };
15282
+ if (resolvedCollect === "sync") {
15283
+ response.collect = { mode: "sync", pendingSyncCount: countPendingSyncRecords(service) };
15284
+ }
14617
15285
  return {
14618
15286
  kind: "bg",
14619
15287
  subagentId: handle.subagentId,
@@ -14621,14 +15289,16 @@ async function startHandler(service, input, signal, ctxModel) {
14621
15289
  slug: handle.details.slug,
14622
15290
  // registry 全等回显:record.model 由 resolved(裁决放行条目)拼接,原样透出。
14623
15291
  model: handle.details.model,
14624
- response: {
14625
- status: "running",
14626
- mode: "background",
14627
- message: BG_MESSAGE,
14628
- notifyContract: NOTIFY_CONTRACT
14629
- }
15292
+ response
14630
15293
  };
14631
15294
  }
15295
+ function countPendingSyncRecords(service) {
15296
+ let count = 0;
15297
+ for (const r of service.queries.collectRecords(COLLECT_SCAN_LIMIT, "all")) {
15298
+ if (r.collectMode === "sync" && r.batchFinalized !== true) count += 1;
15299
+ }
15300
+ return count;
15301
+ }
14632
15302
  function listHandler(service, input) {
14633
15303
  const includeFinished = input?.includeFinished === true;
14634
15304
  const rawLimit = input?.limit ?? DEFAULT_LIST_LIMIT;
@@ -18192,7 +18862,7 @@ function boundedPrettySerialize(value, budget) {
18192
18862
  }
18193
18863
 
18194
18864
  // src/index.ts
18195
- var CORE_PACKAGE_VERSION = "0.5.1";
18865
+ var CORE_PACKAGE_VERSION = "0.6.0";
18196
18866
  export {
18197
18867
  AGENT_REF_EXT,
18198
18868
  AgentCall,