@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.cjs CHANGED
@@ -830,8 +830,202 @@ function bestEffort(err, context, level = "debug") {
830
830
  }
831
831
  }
832
832
 
833
- // src/execution/lifecycle-manager.ts
833
+ // src/execution/collect-coordinator.ts
834
+ var COLLECT_SCAN_LIMIT = 1e3;
835
+ var CollectCoordinator = class {
836
+ constructor(deps) {
837
+ this.deps = deps;
838
+ }
839
+ deps;
840
+ /** 批缓冲(内存):已终态未 flush 的 sync 成员终态快照,跨 route 累积。
841
+ * [U8] 排程合批窗口期内保持可枚举(E9 dispose 转换依赖 pendingMembers 全量)。 */
842
+ buffer = [];
843
+ /** [U8] 挂起的合批排程定时器(undefined = 无排程)。 */
844
+ flushTimer;
845
+ /** notifyComplete 唯一路由入口。返回去向供测试/诊断断言。 */
846
+ route(record) {
847
+ if (record.collectMode !== "sync") {
848
+ this.deps.notifyAsync(record);
849
+ return "async";
850
+ }
851
+ const snapshotRecord = this.deps.toNotifyRecord(record);
852
+ if (!snapshotRecord) return "sync-skipped";
853
+ this.buffer.push(snapshotRecord);
854
+ return this.armFlushIfClosed() ? "sync-flushed" : "sync-buffered";
855
+ }
856
+ /** 诊断/测试:当前缓冲成员数(= 已终态未 flush 的 sync 成员数,含合批窗口内成员)。 */
857
+ get pendingCount() {
858
+ return this.buffer.length;
859
+ }
860
+ /** 诊断/测试 + E9 dispose 转换数据源:缓冲成员快照(副本,非活引用)。 */
861
+ pendingMembers() {
862
+ return [...this.buffer];
863
+ }
864
+ /** [U8·E9 交互] 取消挂起的合批排程(缓冲原样保留——dispose 转 async 通路仍读得到
865
+ * 全部成员)。无排程时 no-op;幂等。 */
866
+ cancelScheduledFlush() {
867
+ if (this.flushTimer !== void 0) {
868
+ clearTimeout(this.flushTimer);
869
+ this.flushTimer = void 0;
870
+ }
871
+ }
872
+ /** 闭合判定 + 合批排程武装:缓冲非空 && 无非终态 sync 成员 → 排程 flush。
873
+ * 已有排程挂起时幂等(同窗口合批,不重复定时器)。(S6 改名:原名 closeDetected
874
+ * 读作纯检测,实带副作用——武装排程改状态,名实相符优先。) */
875
+ armFlushIfClosed() {
876
+ if (this.buffer.length === 0) return false;
877
+ if (this.hasRunningSync()) return false;
878
+ this.scheduleFlush();
879
+ return true;
880
+ }
881
+ /** [U8] 同宏任务去抖合批排程:setTimeout(0)(Node 1ms clamp)——窗口跨度覆盖一个
882
+ * 完整事件循环轮次(当前 poll 段 I/O 回调 + 微任务链排空),足以收纳 finalize 链
883
+ * 间隙内的背靠背 route,同时无可观察延迟(毫秒级 < 任意 LLM/投递时标)。 */
884
+ scheduleFlush() {
885
+ if (this.flushTimer !== void 0) return;
886
+ this.flushTimer = setTimeout(() => {
887
+ this.flushTimer = void 0;
888
+ this.flushIfClosed();
889
+ }, 0);
890
+ }
891
+ /** 合批窗口到期:重验闭合条件后整体移交 flushBatch 并清空。
892
+ * 窗口内新 sync 成员注册 running(跨轮续累反转)→ 保留缓冲不 flush,等其终态
893
+ * route 重新排程(终态必经 notifyComplete,不丢、不悬挂)。
894
+ * void 不等待 flush 的 async 屏障序列(manifest → 写账 → 落标):缓冲已 splice
895
+ * 先行整体移交,屏障 await 期间窗口内新成员重新入空缓冲开新批,互不干扰。 */
896
+ flushIfClosed() {
897
+ if (this.buffer.length === 0) return;
898
+ if (this.hasRunningSync()) return;
899
+ const members = this.buffer.splice(0, this.buffer.length);
900
+ void this.deps.flushBatch(members);
901
+ }
902
+ /** 是否存在非终态 sync 成员(collectMode=sync && 无 batchFinalized && 非终态)。
903
+ * 非终态口径(⛔3 U1 已核实 + U3 resumable 补丁):status 非 closed 且非 resumable
904
+ * ——池排队/在跑成员在 store.register 时即 status="running",自动计入(闭合等待它);
905
+ * batchFinalized=true 的已离场成员不阻止闭合;running+resumable(SP-5 one-shot
906
+ * 成功回退态,进程已死结果已定格)视为已完成,不阻止闭合。 */
907
+ hasRunningSync() {
908
+ for (const record of this.deps.listRecords(COLLECT_SCAN_LIMIT)) {
909
+ if (record.collectMode === "sync" && record.batchFinalized !== true && record.resumable !== true && record.status !== "closed") {
910
+ return true;
911
+ }
912
+ }
913
+ return false;
914
+ }
915
+ };
916
+
917
+ // src/execution/config.ts
918
+ var fs3 = __toESM(require("fs"), 1);
919
+ var path3 = __toESM(require("path"), 1);
834
920
  var logger4 = getLogger("subagents");
921
+ var DEFAULT_CONFIG = {
922
+ version: 1,
923
+ maxConcurrent: 6
924
+ };
925
+ var DEFAULT_MAX_CONCURRENT = 6;
926
+ var DEFAULT_COLLECT_SYNC = {
927
+ default: "async",
928
+ perItemChars: 4e3,
929
+ totalChars: 24e3
930
+ };
931
+ function getGlobalConfigPath(agentDir) {
932
+ return path3.join(agentDir, "subagents", "config.json");
933
+ }
934
+ function loadGlobalConfig(agentDir) {
935
+ try {
936
+ const raw = fs3.readFileSync(getGlobalConfigPath(agentDir), "utf-8");
937
+ const parsed = JSON.parse(raw);
938
+ return sanitizeParsedConfig(parsed);
939
+ } catch {
940
+ return { ...DEFAULT_CONFIG };
941
+ }
942
+ }
943
+ function readGlobalConfig(agentDir) {
944
+ const configPath = getGlobalConfigPath(agentDir);
945
+ let raw;
946
+ try {
947
+ raw = fs3.readFileSync(configPath, "utf-8");
948
+ } catch (err) {
949
+ if (errnoCodeOf(err) === "ENOENT") {
950
+ return { status: "absent", config: { ...DEFAULT_CONFIG } };
951
+ }
952
+ return readFailure(configPath, err);
953
+ }
954
+ try {
955
+ const parsed = JSON.parse(raw);
956
+ return { status: "ok", config: sanitizeParsedConfig(parsed) };
957
+ } catch (err) {
958
+ return readFailure(configPath, err);
959
+ }
960
+ }
961
+ function readFailure(configPath, err) {
962
+ const reason = err instanceof Error ? err.message : String(err);
963
+ logger4.warn(`[subagents] global config read failed (read-failure) at ${configPath}: ${reason}`);
964
+ return { status: "failed", reason };
965
+ }
966
+ function errnoCodeOf(err) {
967
+ if (typeof err !== "object" || err === null || !("code" in err)) return void 0;
968
+ const code = Reflect.get(err, "code");
969
+ return typeof code === "string" ? code : void 0;
970
+ }
971
+ function sanitizeParsedConfig(parsed) {
972
+ const defaultEngine = sanitizeDefaultEngine(parsed.defaultEngine);
973
+ const engineRouting = sanitizeEngineRouting(parsed.engineRouting);
974
+ const collectSync = sanitizeCollectSync(parsed.collectSync);
975
+ return {
976
+ version: parsed.version ?? DEFAULT_CONFIG.version,
977
+ maxConcurrent: sanitizeMaxConcurrent(parsed.maxConcurrent),
978
+ ...defaultEngine !== void 0 ? { defaultEngine } : {},
979
+ ...engineRouting !== void 0 ? { engineRouting } : {},
980
+ ...collectSync !== void 0 ? { collectSync } : {}
981
+ };
982
+ }
983
+ function sanitizeMaxConcurrent(value) {
984
+ return sanitizePositiveInt(value, DEFAULT_MAX_CONCURRENT);
985
+ }
986
+ function sanitizeCollectSync(value) {
987
+ if (typeof value !== "object" || value === null) return void 0;
988
+ const v = value;
989
+ const fallback = DEFAULT_COLLECT_SYNC;
990
+ const fmt = (val) => typeof val === "string" ? JSON.stringify(val) : String(val);
991
+ const bad = [];
992
+ if (v.default !== void 0 && v.default !== "sync" && v.default !== "async") {
993
+ bad.push(`default=${fmt(v.default)}`);
994
+ }
995
+ if (v.perItemChars !== void 0 && !isPositiveInt(v.perItemChars)) {
996
+ bad.push(`perItemChars=${fmt(v.perItemChars)}`);
997
+ }
998
+ if (v.totalChars !== void 0 && !isPositiveInt(v.totalChars)) {
999
+ bad.push(`totalChars=${fmt(v.totalChars)}`);
1000
+ }
1001
+ if (bad.length > 0) {
1002
+ logger4.warn(
1003
+ `[subagents] config collectSync invalid field(s) reverted to defaults: ${bad.join(", ")}`
1004
+ );
1005
+ }
1006
+ return {
1007
+ default: v.default === "sync" ? "sync" : v.default === "async" ? "async" : fallback.default,
1008
+ perItemChars: sanitizePositiveInt(v.perItemChars, fallback.perItemChars),
1009
+ totalChars: sanitizePositiveInt(v.totalChars, fallback.totalChars)
1010
+ };
1011
+ }
1012
+ function sanitizePositiveInt(value, fallback) {
1013
+ return isPositiveInt(value) ? value : fallback;
1014
+ }
1015
+ function isPositiveInt(value) {
1016
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
1017
+ }
1018
+ function sanitizeDefaultEngine(value) {
1019
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
1020
+ }
1021
+ function sanitizeEngineRouting(value) {
1022
+ if (typeof value !== "object" || value === null) return void 0;
1023
+ const strict = value.strict;
1024
+ return typeof strict === "boolean" ? { strict } : void 0;
1025
+ }
1026
+
1027
+ // src/execution/lifecycle-manager.ts
1028
+ var logger5 = getLogger("subagents");
835
1029
  var MS_PER_SECOND = 1e3;
836
1030
  var SECONDS_PER_MINUTE = 60;
837
1031
  var IDLE_TIMEOUT_MINUTES = 5;
@@ -841,7 +1035,7 @@ function getEnvIdleTimeoutMs() {
841
1035
  if (!raw) return void 0;
842
1036
  const parsed = Number(raw);
843
1037
  if (!Number.isFinite(parsed) || parsed <= 0) {
844
- logger4.warn(
1038
+ logger5.warn(
845
1039
  `[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`
846
1040
  );
847
1041
  return void 0;
@@ -1001,7 +1195,7 @@ function createConcurrencyPool(options) {
1001
1195
  }
1002
1196
 
1003
1197
  // src/execution/cold-resurrect.ts
1004
- var fs3 = __toESM(require("fs"), 1);
1198
+ var fs4 = __toESM(require("fs"), 1);
1005
1199
 
1006
1200
  // src/execution/execution-record.ts
1007
1201
  var ACTIVITY_LABEL_MAX = 60;
@@ -1076,6 +1270,7 @@ function createRecord(id, identity) {
1076
1270
  idleTimeoutMs: identity.idleTimeoutMs,
1077
1271
  engine: identity.engine,
1078
1272
  engineFallback: identity.engineFallback,
1273
+ collectMode: identity.collectMode,
1079
1274
  // 状态(实时更新)
1080
1275
  status: "running",
1081
1276
  // turns[] 初始化为 [空 turn]——第一个 turn 从创建即存在,
@@ -1506,7 +1701,7 @@ function resurrectColdRecord(deps, found, id) {
1506
1701
  if (wasClosed) {
1507
1702
  if (record.sessionFile) {
1508
1703
  try {
1509
- fs3.rmSync(`${record.sessionFile}.finalized`, { force: true });
1704
+ fs4.rmSync(`${record.sessionFile}.finalized`, { force: true });
1510
1705
  writeAliveMarker(record.sessionFile, { pid: process.pid, id, startedAt: Date.now() });
1511
1706
  } catch (_e) {
1512
1707
  void _e;
@@ -1537,41 +1732,41 @@ function coldLookupForAction(deps, id, allowReconnect) {
1537
1732
  }
1538
1733
 
1539
1734
  // src/execution/finalize-record.ts
1540
- var fs7 = __toESM(require("fs"), 1);
1541
- var path4 = __toESM(require("path"), 1);
1735
+ var fs8 = __toESM(require("fs"), 1);
1736
+ var path5 = __toESM(require("path"), 1);
1542
1737
 
1543
1738
  // src/execution/finalized-marker.ts
1544
- var fs4 = __toESM(require("fs"), 1);
1739
+ var fs5 = __toESM(require("fs"), 1);
1545
1740
  function writeFinalized(sessionFile, reason) {
1546
1741
  try {
1547
- fs4.rmSync(`${sessionFile}.cancelled`, { force: true });
1548
- fs4.writeFileSync(`${sessionFile}.finalized`, reason ?? "", "utf-8");
1742
+ fs5.rmSync(`${sessionFile}.cancelled`, { force: true });
1743
+ fs5.writeFileSync(`${sessionFile}.finalized`, reason ?? "", "utf-8");
1549
1744
  } catch (_e) {
1550
1745
  void _e;
1551
1746
  }
1552
1747
  }
1553
1748
  function readFinalizedReason(sessionFile) {
1554
1749
  try {
1555
- return fs4.readFileSync(`${sessionFile}.finalized`, "utf-8").trim();
1750
+ return fs5.readFileSync(`${sessionFile}.finalized`, "utf-8").trim();
1556
1751
  } catch {
1557
1752
  return void 0;
1558
1753
  }
1559
1754
  }
1560
1755
 
1561
1756
  // src/execution/path-encoding.ts
1562
- var path3 = __toESM(require("path"), 1);
1757
+ var path4 = __toESM(require("path"), 1);
1563
1758
  function encodeCwd(cwd) {
1564
1759
  return "--" + cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-") + "--";
1565
1760
  }
1566
1761
  function getSubagentSessionDir(agentDir, mainCwd) {
1567
- return path3.join(agentDir, "subagents", encodeCwd(mainCwd), "sessions");
1762
+ return path4.join(agentDir, "subagents", encodeCwd(mainCwd), "sessions");
1568
1763
  }
1569
1764
  function getSubagentRecordsDir(agentDir, mainCwd) {
1570
- return path3.join(agentDir, "subagents", encodeCwd(mainCwd), "records");
1765
+ return path4.join(agentDir, "subagents", encodeCwd(mainCwd), "records");
1571
1766
  }
1572
1767
 
1573
1768
  // src/execution/session-reconstructor.ts
1574
- var fs5 = __toESM(require("fs"), 1);
1769
+ var fs6 = __toESM(require("fs"), 1);
1575
1770
  var IDENTITY_CUSTOM_TYPE = "subagent-identity";
1576
1771
  var TURN_SUMMARY_MAX2 = 80;
1577
1772
  function deriveEventLog(turns, lastError, startedAt) {
@@ -1625,7 +1820,7 @@ function isIdentityData(data) {
1625
1820
  function readJsonlEntries(sessionFile) {
1626
1821
  let raw;
1627
1822
  try {
1628
- raw = fs5.readFileSync(sessionFile, "utf-8");
1823
+ raw = fs6.readFileSync(sessionFile, "utf-8");
1629
1824
  } catch {
1630
1825
  return void 0;
1631
1826
  }
@@ -1795,18 +1990,18 @@ var IDENTITY_HEAD_BYTES = 65536;
1795
1990
  function readIdentityHeader(sessionFile) {
1796
1991
  let text;
1797
1992
  try {
1798
- const fd = fs5.openSync(sessionFile, "r");
1993
+ const fd = fs6.openSync(sessionFile, "r");
1799
1994
  try {
1800
1995
  const buf = Buffer.alloc(IDENTITY_HEAD_BYTES);
1801
1996
  let total = 0;
1802
1997
  while (total < buf.length) {
1803
- const n = fs5.readSync(fd, buf, total, buf.length - total, total);
1998
+ const n = fs6.readSync(fd, buf, total, buf.length - total, total);
1804
1999
  if (n <= 0) break;
1805
2000
  total += n;
1806
2001
  }
1807
2002
  text = buf.toString("utf-8", 0, total);
1808
2003
  } finally {
1809
- fs5.closeSync(fd);
2004
+ fs6.closeSync(fd);
1810
2005
  }
1811
2006
  } catch {
1812
2007
  return void 0;
@@ -1816,7 +2011,7 @@ function readIdentityHeader(sessionFile) {
1816
2011
  function readIdentityAnywhere(sessionFile) {
1817
2012
  let text;
1818
2013
  try {
1819
- text = fs5.readFileSync(sessionFile, "utf-8");
2014
+ text = fs6.readFileSync(sessionFile, "utf-8");
1820
2015
  } catch {
1821
2016
  return void 0;
1822
2017
  }
@@ -1832,20 +2027,20 @@ function readIdentityAnywhere(sessionFile) {
1832
2027
  function readIdentityTail(sessionFile) {
1833
2028
  let text;
1834
2029
  try {
1835
- const { size } = fs5.statSync(sessionFile);
2030
+ const { size } = fs6.statSync(sessionFile);
1836
2031
  const start = Math.max(0, size - IDENTITY_HEAD_BYTES);
1837
- const fd = fs5.openSync(sessionFile, "r");
2032
+ const fd = fs6.openSync(sessionFile, "r");
1838
2033
  try {
1839
2034
  const buf = Buffer.alloc(size - start);
1840
2035
  let total = 0;
1841
2036
  while (total < buf.length) {
1842
- const n = fs5.readSync(fd, buf, total, buf.length - total, start + total);
2037
+ const n = fs6.readSync(fd, buf, total, buf.length - total, start + total);
1843
2038
  if (n <= 0) break;
1844
2039
  total += n;
1845
2040
  }
1846
2041
  text = buf.toString("utf-8", 0, total);
1847
2042
  } finally {
1848
- fs5.closeSync(fd);
2043
+ fs6.closeSync(fd);
1849
2044
  }
1850
2045
  } catch {
1851
2046
  return void 0;
@@ -1920,11 +2115,11 @@ function parseIdentityFromText(text, sessionFile) {
1920
2115
  }
1921
2116
 
1922
2117
  // src/execution/tombstone-store.ts
1923
- var fs6 = __toESM(require("fs"), 1);
2118
+ var fs7 = __toESM(require("fs"), 1);
1924
2119
  function writeCancelledTombstone(sessionFile, meta) {
1925
2120
  try {
1926
2121
  const tombstonePath = `${sessionFile}.cancelled`;
1927
- fs6.writeFileSync(tombstonePath, `${JSON.stringify(meta)}
2122
+ fs7.writeFileSync(tombstonePath, `${JSON.stringify(meta)}
1928
2123
  `, "utf-8");
1929
2124
  } catch (_e) {
1930
2125
  void _e;
@@ -1933,7 +2128,7 @@ function writeCancelledTombstone(sessionFile, meta) {
1933
2128
  function readCancelledTombstone(sessionFile) {
1934
2129
  let raw;
1935
2130
  try {
1936
- raw = fs6.readFileSync(`${sessionFile}.cancelled`, "utf-8");
2131
+ raw = fs7.readFileSync(`${sessionFile}.cancelled`, "utf-8");
1937
2132
  } catch {
1938
2133
  return void 0;
1939
2134
  }
@@ -1949,17 +2144,17 @@ function readCancelledTombstone(sessionFile) {
1949
2144
  }
1950
2145
 
1951
2146
  // src/execution/finalize-record.ts
1952
- var logger5 = getLogger("subagents");
2147
+ var logger6 = getLogger("subagents");
1953
2148
  function findSessionFileByRecordIdentity(sessionDir, recordId) {
1954
2149
  let names;
1955
2150
  try {
1956
- names = fs7.readdirSync(sessionDir);
2151
+ names = fs8.readdirSync(sessionDir);
1957
2152
  } catch {
1958
2153
  return void 0;
1959
2154
  }
1960
2155
  for (const name of names) {
1961
2156
  if (!name.endsWith(".jsonl")) continue;
1962
- const full = path4.join(sessionDir, name);
2157
+ const full = path5.join(sessionDir, name);
1963
2158
  const recon = readIdentityHeader(full) ?? readIdentityTail(full);
1964
2159
  if (recon?.id === recordId) return full;
1965
2160
  }
@@ -1970,7 +2165,7 @@ function resolveMissingSessionFile(deps, record) {
1970
2165
  const resolved = findSessionFileByRecordIdentity(deps.sessionDir, record.id);
1971
2166
  if (resolved) {
1972
2167
  record.sessionFile = resolved;
1973
- logger5.warn(
2168
+ logger6.warn(
1974
2169
  `[subagent] finalizeRecord: sessionFile was missing, resolved via sessionDir identity lookup: ${resolved}`
1975
2170
  );
1976
2171
  }
@@ -1982,8 +2177,8 @@ async function collectPatchIfWorktree(deps, record) {
1982
2177
  deps.modelService.getAgentDir(),
1983
2178
  record.worktreeHandle.mainCwd
1984
2179
  );
1985
- fs7.mkdirSync(sessionsDir, { recursive: true });
1986
- const patchFile = path4.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
2180
+ fs8.mkdirSync(sessionsDir, { recursive: true });
2181
+ const patchFile = path5.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
1987
2182
  const patch = await deps.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
1988
2183
  if (patch.written) record.patchFile = patchFile;
1989
2184
  } catch (pe) {
@@ -2042,7 +2237,7 @@ async function writeManifestBestEffort(deps, record) {
2042
2237
  });
2043
2238
  } catch (err) {
2044
2239
  const msg = err instanceof Error ? err.message : String(err);
2045
- logger5.error(`[subagent] manifest \u5199\u5165\u5931\u8D25 (record=${record.id}): ${msg}`);
2240
+ logger6.error(`[subagent] manifest \u5199\u5165\u5931\u8D25 (record=${record.id}): ${msg}`);
2046
2241
  deps.pi?.appendEntry?.("subagent:manifest-write-failed", {
2047
2242
  id: record.id,
2048
2243
  error: msg
@@ -2174,7 +2369,7 @@ var ExecutionNestingContext = class {
2174
2369
  };
2175
2370
 
2176
2371
  // src/execution/engine/common/data-dir.ts
2177
- var logger6 = getLogger("subagents");
2372
+ var logger7 = getLogger("subagents");
2178
2373
  var XYZ_DATA_DIR_ENV = "XYZ_AGENT_DATA_DIR";
2179
2374
  var warned = false;
2180
2375
  function getEngineDataDir(env = process.env, warn = defaultWarn) {
@@ -2190,14 +2385,14 @@ function getEngineDataDir(env = process.env, warn = defaultWarn) {
2190
2385
  return fallback;
2191
2386
  }
2192
2387
  function defaultWarn(msg) {
2193
- logger6.warn(msg);
2388
+ logger7.warn(msg);
2194
2389
  }
2195
2390
 
2196
2391
  // src/execution/engine/common/event-journal.ts
2197
2392
  var import_promises = require("fs/promises");
2198
2393
  var import_node_fs2 = require("fs");
2199
2394
  var import_node_path2 = require("path");
2200
- var logger7 = getLogger("subagents");
2395
+ var logger8 = getLogger("subagents");
2201
2396
  var FLUSH_THRESHOLD_LINES = 64;
2202
2397
  var FLUSH_THRESHOLD_BYTES = 32 * 1024;
2203
2398
  var defaultFs = {
@@ -2306,7 +2501,7 @@ var JournalWriter = class {
2306
2501
  }
2307
2502
  };
2308
2503
  function defaultWarn2(msg) {
2309
- logger7.warn(msg);
2504
+ logger8.warn(msg);
2310
2505
  }
2311
2506
  function replayJournal(path21) {
2312
2507
  let raw;
@@ -2422,8 +2617,8 @@ function executeOptionsToEngineTaskSpec(opts) {
2422
2617
 
2423
2618
  // src/execution/engine/engines/pi/pi-engine.ts
2424
2619
  var import_node_child_process2 = require("child_process");
2425
- var fs13 = __toESM(require("fs"), 1);
2426
- var path8 = __toESM(require("path"), 1);
2620
+ var fs14 = __toESM(require("fs"), 1);
2621
+ var path9 = __toESM(require("path"), 1);
2427
2622
 
2428
2623
  // src/orchestration/models/types.ts
2429
2624
  var VALID_RUN_TRANSITIONS = {
@@ -2457,8 +2652,8 @@ function stringifySchemaCached(schema, mode) {
2457
2652
  }
2458
2653
 
2459
2654
  // src/execution/engine/engines/pi/pi-invocation.ts
2460
- var fs8 = __toESM(require("fs"), 1);
2461
- var path5 = __toESM(require("path"), 1);
2655
+ var fs9 = __toESM(require("fs"), 1);
2656
+ var path6 = __toESM(require("path"), 1);
2462
2657
 
2463
2658
  // src/execution/relay-env.ts
2464
2659
  var RELAY_ENV_SOCKET = "XYZ_SUBAGENT_RELAY_SOCKET";
@@ -2473,7 +2668,7 @@ function isRelayActive(env) {
2473
2668
  // src/execution/engine/engines/pi/pi-invocation.ts
2474
2669
  var BUN_VIRTUAL_PREFIX = "/$bunfs/root/";
2475
2670
  function isGenericRuntime(execPath) {
2476
- const execName = path5.basename(execPath).toLowerCase();
2671
+ const execName = path6.basename(execPath).toLowerCase();
2477
2672
  return /^(node|bun)(\.exe)?$/.test(execName);
2478
2673
  }
2479
2674
  var scriptExistsCache;
@@ -2482,7 +2677,7 @@ function currentScriptExists() {
2482
2677
  if (scriptExistsCache === void 0 || scriptExistsCache.script !== currentScript) {
2483
2678
  scriptExistsCache = {
2484
2679
  script: currentScript,
2485
- exists: currentScript !== void 0 && !currentScript.startsWith(BUN_VIRTUAL_PREFIX) && fs8.existsSync(currentScript)
2680
+ exists: currentScript !== void 0 && !currentScript.startsWith(BUN_VIRTUAL_PREFIX) && fs9.existsSync(currentScript)
2486
2681
  };
2487
2682
  }
2488
2683
  return scriptExistsCache.exists;
@@ -2589,11 +2784,11 @@ async function readPiSessionView(sessionFile) {
2589
2784
 
2590
2785
  // src/execution/engine/engines/pi/session-runner.ts
2591
2786
  var import_node_child_process = require("child_process");
2592
- var fs12 = __toESM(require("fs"), 1);
2787
+ var fs13 = __toESM(require("fs"), 1);
2593
2788
 
2594
2789
  // src/execution/session-pending.ts
2595
- var fs9 = __toESM(require("fs"), 1);
2596
- var logger8 = getLogger("subagents");
2790
+ var fs10 = __toESM(require("fs"), 1);
2791
+ var logger9 = getLogger("subagents");
2597
2792
  var RECENT_UNREGISTER_WINDOW_MS = 6e4;
2598
2793
  var cursors = /* @__PURE__ */ new Map();
2599
2794
  function prunePendingCursor(sessionFile) {
@@ -2631,7 +2826,7 @@ function pendingFileUnreadableMessage(err) {
2631
2826
  }
2632
2827
  function statPendingFileSize(sessionFile) {
2633
2828
  try {
2634
- return fs9.statSync(sessionFile).size;
2829
+ return fs10.statSync(sessionFile).size;
2635
2830
  } catch (err) {
2636
2831
  cursors.delete(sessionFile);
2637
2832
  return { error: pendingFileUnreadableMessage(err) };
@@ -2640,7 +2835,7 @@ function statPendingFileSize(sessionFile) {
2640
2835
  function readPendingChunk(sessionFile, cursor, size) {
2641
2836
  try {
2642
2837
  if (cursor.offset === 0) {
2643
- return fs9.readFileSync(sessionFile, "utf-8");
2838
+ return fs10.readFileSync(sessionFile, "utf-8");
2644
2839
  }
2645
2840
  return readPendingChunkFrom(sessionFile, cursor.offset, size - cursor.offset);
2646
2841
  } catch (err) {
@@ -2650,17 +2845,17 @@ function readPendingChunk(sessionFile, cursor, size) {
2650
2845
  }
2651
2846
  function readPendingChunkFrom(sessionFile, offset, len) {
2652
2847
  const buf = Buffer.alloc(len);
2653
- const fd = fs9.openSync(sessionFile, "r");
2848
+ const fd = fs10.openSync(sessionFile, "r");
2654
2849
  try {
2655
2850
  let total = 0;
2656
2851
  while (total < len) {
2657
- const n = fs9.readSync(fd, buf, total, len - total, offset + total);
2852
+ const n = fs10.readSync(fd, buf, total, len - total, offset + total);
2658
2853
  if (n <= 0) break;
2659
2854
  total += n;
2660
2855
  }
2661
2856
  return buf.toString("utf-8", 0, total);
2662
2857
  } finally {
2663
- fs9.closeSync(fd);
2858
+ fs10.closeSync(fd);
2664
2859
  }
2665
2860
  }
2666
2861
  function consumePendingLines(complete, cursor, sessionFile) {
@@ -2685,7 +2880,7 @@ function applyPendingLine(line, cursor, sessionFile) {
2685
2880
  if (Number.isFinite(ts) && ts > cursor.latestUnregisterMs) cursor.latestUnregisterMs = ts;
2686
2881
  }
2687
2882
  } catch {
2688
- logger8.debug("skipped malformed pending line", { sessionFile });
2883
+ logger9.debug("skipped malformed pending line", { sessionFile });
2689
2884
  }
2690
2885
  }
2691
2886
  function extractPendingEntryId(data) {
@@ -2726,7 +2921,7 @@ function listActivePendingFromSessionFile(sessionFile) {
2726
2921
  }
2727
2922
 
2728
2923
  // src/execution/engine/common/kill-chain.ts
2729
- var logger9 = getLogger("subagents");
2924
+ var logger10 = getLogger("subagents");
2730
2925
  var MS_PER_SECOND3 = 1e3;
2731
2926
  var SIGKILL_REAP_TIMEOUT_MS = 1e4;
2732
2927
  var HOST_TIMEOUT_ABORT_REASON = "agent-call-timeout";
@@ -2738,7 +2933,7 @@ async function killChain(child, opts) {
2738
2933
  if (graceful === "settled") return "terminated";
2739
2934
  if (child.exitCode !== null || child.signalCode !== null) return "terminated";
2740
2935
  if (opts.escalationNote !== void 0) {
2741
- logger9.warn(
2936
+ logger10.warn(
2742
2937
  `[kill-chain] ${opts.escalationNote} still alive ${opts.graceMs / MS_PER_SECOND3}s after SIGTERM, escalating to SIGKILL`
2743
2938
  );
2744
2939
  }
@@ -2750,7 +2945,7 @@ function safeKill(child, signal) {
2750
2945
  try {
2751
2946
  child.kill(signal);
2752
2947
  } catch (err) {
2753
- logger9.debug(
2948
+ logger10.debug(
2754
2949
  `[kill-chain] ${signal} on exited process: ${toErrorMessage(err)}`
2755
2950
  );
2756
2951
  }
@@ -2872,7 +3067,7 @@ function parseMirrorFlagArgs(flagArgs) {
2872
3067
 
2873
3068
  // src/execution/engine/engines/pi/stdin-writer.ts
2874
3069
  var crypto2 = __toESM(require("crypto"), 1);
2875
- var logger10 = getLogger("subagents");
3070
+ var logger11 = getLogger("subagents");
2876
3071
  var epipeConsecutiveFailures = /* @__PURE__ */ new Map();
2877
3072
  var EPIPE_FAILURE_THRESHOLD = 2;
2878
3073
  function recordEpipeFailure(recordId) {
@@ -2894,7 +3089,7 @@ function respond(child, id, out, signal) {
2894
3089
  else if ("confirmed" in out) line = JSON.stringify({ type: "extension_ui_response", id, confirmed: out.confirmed });
2895
3090
  else if ("cancelled" in out) line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
2896
3091
  } catch (err) {
2897
- logger10.warn(`[subagents] JSON.stringify failed for ui response ${id}, degrading to cancelled`, {
3092
+ logger11.warn(`[subagents] JSON.stringify failed for ui response ${id}, degrading to cancelled`, {
2898
3093
  detail: toErrorMessage(err)
2899
3094
  });
2900
3095
  line = JSON.stringify({ type: "extension_ui_response", id, cancelled: true });
@@ -2927,14 +3122,14 @@ function writeStdinLine(child, line, warnTag) {
2927
3122
  if (!child.stdin || child.stdin.destroyed) return;
2928
3123
  try {
2929
3124
  const ok = child.stdin.write(line + "\n");
2930
- if (!ok) logger10.warn(`[subagents] stdin backpressure on ${warnTag}`);
3125
+ if (!ok) logger11.warn(`[subagents] stdin backpressure on ${warnTag}`);
2931
3126
  } catch (err) {
2932
3127
  if (err !== null && typeof err === "object" && "code" in err && (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED")) {
2933
3128
  throw new Error(
2934
3129
  `[subagents] EPIPE on stdin write (${warnTag}): pipe broken, child process likely exited. Recovery: treat as dead process and resume via cold path.`
2935
3130
  );
2936
3131
  }
2937
- logger10.warn(`[subagents] unexpected stdin write error on ${warnTag}`, {
3132
+ logger11.warn(`[subagents] unexpected stdin write error on ${warnTag}`, {
2938
3133
  detail: toErrorMessage(err)
2939
3134
  });
2940
3135
  }
@@ -3243,7 +3438,7 @@ function schemaEnvByteLength(schemaEnv) {
3243
3438
  }
3244
3439
 
3245
3440
  // src/execution/settled-watchdog.ts
3246
- var logger11 = getLogger("subagents");
3441
+ var logger12 = getLogger("subagents");
3247
3442
  var MS_PER_SECOND4 = 1e3;
3248
3443
  var SECONDS_PER_MINUTE2 = 60;
3249
3444
  var MID_ROUND_MINUTES = 30;
@@ -3259,13 +3454,13 @@ function resolveSettledWatchdogEnv() {
3259
3454
  if (raw === void 0 || raw.trim() === "") return envCache;
3260
3455
  const parsed = Number(raw);
3261
3456
  if (!Number.isFinite(parsed)) {
3262
- logger11.warn(
3457
+ logger12.warn(
3263
3458
  `[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`
3264
3459
  );
3265
3460
  return envCache;
3266
3461
  }
3267
3462
  if (parsed <= 0) {
3268
- logger11.warn(
3463
+ logger12.warn(
3269
3464
  `[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.`
3270
3465
  );
3271
3466
  envCache.disabled = true;
@@ -3292,7 +3487,7 @@ function armMidRoundNoProgress(recordId, handlers) {
3292
3487
  assertSafeTimerDelay(SETTLED_MID_ROUND_NO_PROGRESS_MS, "settled watchdog (mid-round)");
3293
3488
  const timer = setTimeout(() => {
3294
3489
  armedEntries.delete(recordId);
3295
- logger11.debug(
3490
+ logger12.debug(
3296
3491
  `[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`
3297
3492
  );
3298
3493
  handlers.onMidTimeout({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
@@ -3312,7 +3507,7 @@ function armSettledWatchdog(recordId, onTimeout) {
3312
3507
  assertSafeTimerDelay(windowMs, "settled watchdog");
3313
3508
  const timer = setTimeout(() => {
3314
3509
  armedEntries.delete(recordId);
3315
- logger11.debug(
3510
+ logger12.debug(
3316
3511
  `[settled-watchdog] settled phase fired for ${recordId} after ${windowMs}ms without agent_settled`
3317
3512
  );
3318
3513
  onTimeout({ phase: "settled", waitedMs: windowMs });
@@ -3335,7 +3530,7 @@ function refreshMidRoundNoProgress(recordId) {
3335
3530
  const onMidTimeout = entry.onMidTimeout;
3336
3531
  const timer = setTimeout(() => {
3337
3532
  armedEntries.delete(recordId);
3338
- logger11.debug(
3533
+ logger12.debug(
3339
3534
  `[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`
3340
3535
  );
3341
3536
  onMidTimeout?.({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
@@ -3351,8 +3546,8 @@ function disarmSettledWatchdog(recordId) {
3351
3546
  var MAX_FORK_DEPTH = 10;
3352
3547
 
3353
3548
  // src/execution/engine/engines/pi/spawn-event-adapter.ts
3354
- var fs10 = __toESM(require("fs"), 1);
3355
- var path6 = __toESM(require("path"), 1);
3549
+ var fs11 = __toESM(require("fs"), 1);
3550
+ var path7 = __toESM(require("path"), 1);
3356
3551
  function isSessionHeader(obj) {
3357
3552
  if (typeof obj !== "object" || obj === null) return false;
3358
3553
  const r = obj;
@@ -3500,28 +3695,28 @@ function deriveSessionFilePath(header, sessionDir) {
3500
3695
  }
3501
3696
  function findSessionFileByHeaderId(sessionDir, sessionId) {
3502
3697
  try {
3503
- const files = fs10.readdirSync(sessionDir);
3698
+ const files = fs11.readdirSync(sessionDir);
3504
3699
  const match = files.find((f) => f.endsWith(`_${sessionId}.jsonl`));
3505
- return match ? path6.join(sessionDir, match) : void 0;
3700
+ return match ? path7.join(sessionDir, match) : void 0;
3506
3701
  } catch {
3507
3702
  return void 0;
3508
3703
  }
3509
3704
  }
3510
3705
 
3511
3706
  // src/execution/engine/engines/pi/temp-prompt.ts
3512
- var fs11 = __toESM(require("fs"), 1);
3707
+ var fs12 = __toESM(require("fs"), 1);
3513
3708
  var os = __toESM(require("os"), 1);
3514
- var path7 = __toESM(require("path"), 1);
3709
+ var path8 = __toESM(require("path"), 1);
3515
3710
  async function writePromptToTempFile(agentName, prompt) {
3516
- const dir = await fs11.promises.mkdtemp(path7.join(os.tmpdir(), "pi-subagent-"));
3711
+ const dir = await fs12.promises.mkdtemp(path8.join(os.tmpdir(), "pi-subagent-"));
3517
3712
  const safeName = agentName.replace(/[^\w.-]+/g, "_");
3518
- const filePath = path7.join(dir, `prompt-${safeName}.md`);
3519
- await fs11.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
3713
+ const filePath = path8.join(dir, `prompt-${safeName}.md`);
3714
+ await fs12.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 384 });
3520
3715
  return { dir, filePath };
3521
3716
  }
3522
3717
  async function cleanupTempPrompt(file) {
3523
3718
  try {
3524
- await fs11.promises.rm(file.dir, { recursive: true, force: true });
3719
+ await fs12.promises.rm(file.dir, { recursive: true, force: true });
3525
3720
  } catch (err) {
3526
3721
  bestEffort(err, `cleanup temp prompt dir ${file.dir}`);
3527
3722
  }
@@ -3666,7 +3861,7 @@ function createUiChannelRegistry() {
3666
3861
  }
3667
3862
 
3668
3863
  // src/execution/ui-request-observability.ts
3669
- var logger12 = getLogger("subagents");
3864
+ var logger13 = getLogger("subagents");
3670
3865
  var GLOBAL_OBSERVABILITY_KEY = /* @__PURE__ */ Symbol.for("pi-subagent-workflow.ui-observability");
3671
3866
  function registerGlobalObservability(obs) {
3672
3867
  globalThis[GLOBAL_OBSERVABILITY_KEY] = obs;
@@ -3676,7 +3871,7 @@ function notifyMissingHandlerGlobal(sessionId) {
3676
3871
  if (obs) {
3677
3872
  obs.notifyMissingHandler(sessionId);
3678
3873
  } else {
3679
- logger12.warn(
3874
+ logger13.warn(
3680
3875
  `[subagents] uiRequestHandler missing (session=${sessionId}, global observability not registered)`
3681
3876
  );
3682
3877
  }
@@ -3704,12 +3899,12 @@ var UiRequestObservability = class {
3704
3899
  this.warnedMissingHandlerSessions.clear();
3705
3900
  }
3706
3901
  this.warnedMissingHandlerSessions.add(sessionId);
3707
- logger12.warn(`[subagents] uiRequestHandler missing (session=${sessionId}, mode=${this.sessionMode})`);
3902
+ logger13.warn(`[subagents] uiRequestHandler missing (session=${sessionId}, mode=${this.sessionMode})`);
3708
3903
  }
3709
3904
  };
3710
3905
 
3711
3906
  // src/execution/ui-request-queue.ts
3712
- var logger13 = getLogger("subagents");
3907
+ var logger14 = getLogger("subagents");
3713
3908
  function createUiRequestQueue(child, ctx) {
3714
3909
  const abortController = new AbortController();
3715
3910
  const queue = [];
@@ -3721,7 +3916,7 @@ function createUiRequestQueue(child, ctx) {
3721
3916
  const { id, request, signal } = queue.shift();
3722
3917
  handleUiRequest(child, id, request, ctx, signal).catch((err) => {
3723
3918
  const m = toErrorMessage(err);
3724
- logger13.error(`[subagents] ui request ${id} (${request.method}) failed unexpectedly: ${m}`);
3919
+ logger14.error(`[subagents] ui request ${id} (${request.method}) failed unexpectedly: ${m}`);
3725
3920
  }).finally(() => {
3726
3921
  processing = false;
3727
3922
  processNext();
@@ -3766,7 +3961,7 @@ async function handleUiRequest(child, id, request, ctx, signal) {
3766
3961
  respond(child, id, result, signal);
3767
3962
  } catch (err) {
3768
3963
  if (signal?.aborted) return;
3769
- logger13.error("[subagents] uiRequestHandler threw", {
3964
+ logger14.error("[subagents] uiRequestHandler threw", {
3770
3965
  detail: toErrorMessage(err)
3771
3966
  });
3772
3967
  respond(child, id, { cancelled: true }, signal);
@@ -3809,7 +4004,7 @@ function extractMethodFields(req) {
3809
4004
  }
3810
4005
 
3811
4006
  // src/execution/engine/engines/pi/session-runner.ts
3812
- var logger14 = getLogger("subagents");
4007
+ var logger15 = getLogger("subagents");
3813
4008
  function isSdkEvent(x) {
3814
4009
  if (typeof x !== "object" || x === null) return false;
3815
4010
  if (!("type" in x)) return false;
@@ -3862,7 +4057,7 @@ function getEnvSpawnWatchdogMs() {
3862
4057
  if (!raw) return void 0;
3863
4058
  const parsed = Number(raw);
3864
4059
  if (!Number.isFinite(parsed) || parsed <= 0) {
3865
- logger14.warn(
4060
+ logger15.warn(
3866
4061
  `[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`
3867
4062
  );
3868
4063
  return void 0;
@@ -3929,7 +4124,7 @@ function killAllSpawnedChildren(signal = "SIGTERM") {
3929
4124
  child.kill(child.killed ? "SIGKILL" : signal);
3930
4125
  n++;
3931
4126
  } catch (err) {
3932
- logger14.debug(
4127
+ logger15.debug(
3933
4128
  `[session-runner] killAllSpawnedChildren: kill failed (best-effort continue): ${toErrorMessage(err)}`
3934
4129
  );
3935
4130
  }
@@ -3973,7 +4168,7 @@ function killPidWithEscalation(pid, label) {
3973
4168
  try {
3974
4169
  process.kill(pid, "SIGTERM");
3975
4170
  } catch (err) {
3976
- logger14.debug(
4171
+ logger15.debug(
3977
4172
  `[session-runner] ${label}: SIGTERM to pid ${pid} failed (best-effort continue): ${toErrorMessage(err)}`
3978
4173
  );
3979
4174
  return;
@@ -3981,7 +4176,7 @@ function killPidWithEscalation(pid, label) {
3981
4176
  assertSafeTimerDelay(SIGKILL_ESCALATION_MS, `descendant SIGKILL escalation (${label})`);
3982
4177
  const escalation = setTimeout(() => {
3983
4178
  if (isProcessAlive(pid)) {
3984
- logger14.warn(
4179
+ logger15.warn(
3985
4180
  `[session-runner] ${label}: descendant pid ${pid} still alive ${SIGKILL_ESCALATION_MS / MS_PER_SECOND5}s after SIGTERM, escalating to SIGKILL`
3986
4181
  );
3987
4182
  try {
@@ -4004,7 +4199,7 @@ function sweepDescendantsOfSession(rootSessionFile, sessionDir, source) {
4004
4199
  visited.add(sessionFile);
4005
4200
  const list = listActivePendingFromSessionFile(sessionFile);
4006
4201
  if (list.error) {
4007
- logger14.debug(
4202
+ logger15.debug(
4008
4203
  `[session-runner] descendant sweep (${source}): pending list unreadable for ${sessionFile}: ${list.error}`
4009
4204
  );
4010
4205
  continue;
@@ -4048,7 +4243,7 @@ function sweepDescendantsOfSession(rootSessionFile, sessionDir, source) {
4048
4243
  });
4049
4244
  continue;
4050
4245
  }
4051
- logger14.warn(
4246
+ logger15.warn(
4052
4247
  `[session-runner] descendant sweep (${source}): killing orphan descendant pid=${marker.pid} session=${item.sessionId} (${item.id})`
4053
4248
  );
4054
4249
  killPidWithEscalation(marker.pid, `descendant sweep (${source})`);
@@ -4111,7 +4306,7 @@ async function buildEnvBlock(cwd, forkDepth, nestingDepth) {
4111
4306
  );
4112
4307
  });
4113
4308
  } catch (err) {
4114
- logger14.debug(
4309
+ logger15.debug(
4115
4310
  `[session-runner] buildEnvBlock: git branch lookup failed for ${cwd}, fallback to empty: ${toErrorMessage(err)}`
4116
4311
  );
4117
4312
  branch = "";
@@ -4162,7 +4357,7 @@ function writeAliveMarkerBestEffort(sessionFile, pid, id) {
4162
4357
  try {
4163
4358
  writeAliveMarker(sessionFile, { pid, id, startedAt: Date.now() });
4164
4359
  } catch (err) {
4165
- logger14.debug(
4360
+ logger15.debug(
4166
4361
  `[session-runner] alive marker write failed (best-effort continue): ${toErrorMessage(err)}`
4167
4362
  );
4168
4363
  }
@@ -4198,7 +4393,7 @@ function killRecordChildWithEscalation(recordId, source) {
4198
4393
  const escalation = setTimeout(
4199
4394
  () => {
4200
4395
  if (child.exitCode === null && child.signalCode === null) {
4201
- logger14.warn(
4396
+ logger15.warn(
4202
4397
  `[session-runner] child ${recordId} still alive ${SIGKILL_ESCALATION_MS / MS_PER_SECOND5}s after SIGTERM, escalating to SIGKILL (source: ${source})`
4203
4398
  );
4204
4399
  child.kill("SIGKILL");
@@ -4218,7 +4413,7 @@ function killRecordChildWithEscalation(recordId, source) {
4218
4413
  function hasLiveActiveDescendant(sessionFile, sessionDir) {
4219
4414
  const list = listActivePendingFromSessionFile(sessionFile);
4220
4415
  if (list.error) {
4221
- logger14.warn(
4416
+ logger15.warn(
4222
4417
  `[session-runner] keep-alive no-progress re-check failed (treating as no live descendants): ${list.error}`
4223
4418
  );
4224
4419
  return false;
@@ -4238,13 +4433,13 @@ function armKeepAliveNoProgressTimer(state, child, sessionDir) {
4238
4433
  state.keepAliveNoProgressTimer = setTimeout(() => {
4239
4434
  if (hasLiveActiveDescendant(state.record.sessionFile, sessionDir)) {
4240
4435
  touchAliveMarkerForHeartbeat(state.record.sessionFile, child.pid, state.record.id);
4241
- logger14.debug(
4436
+ logger15.debug(
4242
4437
  `[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)`
4243
4438
  );
4244
4439
  armKeepAliveNoProgressTimer(state, child, sessionDir);
4245
4440
  return;
4246
4441
  }
4247
- logger14.warn(
4442
+ logger15.warn(
4248
4443
  `[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`
4249
4444
  );
4250
4445
  state.sweepDescendantsOnClose = true;
@@ -4305,7 +4500,7 @@ function createSpawnEventHandlers(state) {
4305
4500
  bestEffort(err, "armIdleTimer (agent_settled chatMode)", "error");
4306
4501
  try {
4307
4502
  armIdleTimer(record.id, armIdleTimerOnTimeout, DEFAULT_IDLE_TIMEOUT_MS);
4308
- logger14.warn(
4503
+ logger15.warn(
4309
4504
  `[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`
4310
4505
  );
4311
4506
  } catch (fallbackErr) {
@@ -4467,7 +4662,7 @@ async function backfillSessionFileViaGetState(state, child, registerGetStateList
4467
4662
  if (child.pid) {
4468
4663
  writeAliveMarkerBestEffort(r.sessionFile, child.pid, r.sessionId ?? record.id);
4469
4664
  }
4470
- logger14.warn(
4665
+ logger15.warn(
4471
4666
  `[session-runner] agent_end: sessionFile backfilled via lazy get_state (spawn handshake had failed): ${r.sessionFile}`
4472
4667
  );
4473
4668
  }
@@ -4479,11 +4674,11 @@ function keepAliveOnAgentEnd(state, child, sessionDir, pending) {
4479
4674
  const { record, opts } = state;
4480
4675
  touchAliveMarkerForHeartbeat(record.sessionFile, child.pid, record.id);
4481
4676
  if (pending.error) {
4482
- logger14.warn(
4677
+ logger15.warn(
4483
4678
  `[session-runner] agent_end: keep alive (sessionFile unreadable, conservative): ${pending.error}`
4484
4679
  );
4485
4680
  } else {
4486
- logger14.debug(
4681
+ logger15.debug(
4487
4682
  `[session-runner] agent_end: keep alive, ${pending.count} active descendant(s) pending`
4488
4683
  );
4489
4684
  }
@@ -4510,7 +4705,7 @@ function keepAliveOnAgentEnd(state, child, sessionDir, pending) {
4510
4705
  function keepAliveForWakeupGrace(state, child) {
4511
4706
  const { record } = state;
4512
4707
  touchAliveMarkerForHeartbeat(record.sessionFile, child.pid, record.id);
4513
- logger14.debug(
4708
+ logger15.debug(
4514
4709
  "[session-runner] agent_end: keep alive, recent descendant completion (wake-up in flight)"
4515
4710
  );
4516
4711
  clearTimeout(state.watchdog);
@@ -4535,7 +4730,7 @@ function attachStdoutPump(child, state, sessionDir, handleSdkEvent) {
4535
4730
  invalidLineSamples.push(truncated);
4536
4731
  }
4537
4732
  if (invalidLineCount <= MAX_INVALID_LINE_SAMPLES) {
4538
- logger14.debug(
4733
+ logger15.debug(
4539
4734
  `[session-runner] stdout invalid line #${invalidLineCount} dropped (${reason}): ${truncated}`
4540
4735
  );
4541
4736
  }
@@ -4645,7 +4840,7 @@ function attachStdoutPump(child, state, sessionDir, handleSdkEvent) {
4645
4840
  }
4646
4841
  }
4647
4842
  if (invalidLineCount > 0) {
4648
- logger14.debug(
4843
+ logger15.debug(
4649
4844
  `[session-runner] stdout had ${invalidLineCount} invalid line(s) dropped in total; sample(s): ${invalidLineSamples.join(" | ")}`
4650
4845
  );
4651
4846
  }
@@ -4692,7 +4887,7 @@ function setupFreshChild(state, child, task) {
4692
4887
  try {
4693
4888
  void ctx.onWorktreePid?.(opts.worktree.branch, child.pid);
4694
4889
  } catch (err) {
4695
- logger14.warn("[worktree] worktree pid registration failed (defensive)", {
4890
+ logger15.warn("[worktree] worktree pid registration failed (defensive)", {
4696
4891
  branch: opts.worktree.branch,
4697
4892
  pid: child.pid,
4698
4893
  err: toErrorMessage(err)
@@ -4704,7 +4899,7 @@ function setupFreshChild(state, child, task) {
4704
4899
  child.stdin.on("error", (err) => {
4705
4900
  removeChildRegistration(record.id, child);
4706
4901
  const count = recordEpipeFailure(record.id);
4707
- logger14.warn(`[subagents] async stdin error for ${record.id}`, {
4902
+ logger15.warn(`[subagents] async stdin error for ${record.id}`, {
4708
4903
  detail: err.message,
4709
4904
  epipeCount: count,
4710
4905
  threshold: EPIPE_FAILURE_THRESHOLD,
@@ -4718,14 +4913,14 @@ function armChatModeSettledWatchdog(state, child) {
4718
4913
  if (record.chatMode) {
4719
4914
  armMidRoundNoProgress(record.id, {
4720
4915
  onMidTimeout: (fire) => {
4721
- logger14.warn(
4916
+ logger15.warn(
4722
4917
  `[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)`
4723
4918
  );
4724
4919
  state.settledWatchdogFired = fire;
4725
4920
  killChildWithEscalation(state, child, "settled watchdog");
4726
4921
  },
4727
4922
  onSettleTimeout: (fire) => {
4728
- logger14.warn(
4923
+ logger15.warn(
4729
4924
  `[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)`
4730
4925
  );
4731
4926
  state.settledWatchdogFired = fire;
@@ -4756,14 +4951,14 @@ function startGetStateHandshake(child, pump) {
4756
4951
  if (pump.isHandshakePending()) pump.finishHandshake(r);
4757
4952
  }).catch((err) => {
4758
4953
  const m = toErrorMessage(err);
4759
- logger14.error(`[session-runner] get_state handshake failed: ${m}`);
4954
+ logger15.error(`[session-runner] get_state handshake failed: ${m}`);
4760
4955
  pump.abandonHandshake();
4761
4956
  });
4762
4957
  }
4763
4958
  function backfillSessionFileByLookup(state, sessionDir) {
4764
4959
  const { record } = state;
4765
4960
  const lookupId = state.sessionHeader?.id ?? state.handshakeResult?.sessionId;
4766
- const needsLookup = record.sessionFile ? !fs12.existsSync(record.sessionFile) : lookupId !== void 0;
4961
+ const needsLookup = record.sessionFile ? !fs13.existsSync(record.sessionFile) : lookupId !== void 0;
4767
4962
  if (lookupId && needsLookup) {
4768
4963
  const actual = findSessionFileByHeaderId(sessionDir, lookupId);
4769
4964
  if (actual && actual !== record.sessionFile) record.sessionFile = actual;
@@ -4775,7 +4970,7 @@ function sweepDescendantsOnChildClose(state, sessionDir) {
4775
4970
  try {
4776
4971
  const sweep = sweepDescendantsOfSession(record.sessionFile, sessionDir, "keep-alive watchdog");
4777
4972
  if (sweep.killed.length > 0 || sweep.skipped.length > 0) {
4778
- logger14.warn(
4973
+ logger15.warn(
4779
4974
  `[session-runner] descendant sweep (keep-alive watchdog): killed=[${sweep.killed.join(", ")}] skipped=${JSON.stringify(sweep.skipped)}`
4780
4975
  );
4781
4976
  }
@@ -4828,7 +5023,7 @@ async function runSpawn(record, task, opts, ctx, resume) {
4828
5023
  };
4829
5024
  const handleSdkEvent = createSpawnEventHandlers(state);
4830
5025
  const sessionDir = getSubagentSessionDir(ctx.agentDir, ctx.rootCwd);
4831
- fs12.mkdirSync(sessionDir, { recursive: true });
5026
+ fs13.mkdirSync(sessionDir, { recursive: true });
4832
5027
  const spawnCwd = opts.worktree?.path ?? ctx.cwd;
4833
5028
  const forkSource = opts.forkSource ?? (opts.fork ? ctx.mainSessionFile : void 0);
4834
5029
  const tempPromptFile = await writeAppendSystemPromptFile(record, opts, ctx);
@@ -4875,7 +5070,7 @@ async function runSpawn(record, task, opts, ctx, resume) {
4875
5070
  }
4876
5071
 
4877
5072
  // src/execution/engine/engines/pi/pi-engine.ts
4878
- var logger15 = getLogger("subagents");
5073
+ var logger16 = getLogger("subagents");
4879
5074
  var PI_ENGINE_ID2 = "pi";
4880
5075
  var PI_ADAPTER_VERSION = "1.0.0";
4881
5076
  var PI_POOL_KEY = "shared";
@@ -5055,7 +5250,7 @@ var PiEngine = class {
5055
5250
  sendPromptCommand(child, text, { streamingBehavior: interrupt ? "steer" : "followUp" });
5056
5251
  clearEpipeFailure(record.id);
5057
5252
  if (child.exitCode !== null || child.signalCode !== null) {
5058
- logger15.warn(
5253
+ logger16.warn(
5059
5254
  `[subagents] deliverMessage: child ${record.id} died around stdin write, message may be lost`,
5060
5255
  {
5061
5256
  msgType: interrupt ? "steer" : "followUp",
@@ -5069,7 +5264,7 @@ var PiEngine = class {
5069
5264
  service.reportRecordTransition?.(record);
5070
5265
  } catch (err) {
5071
5266
  if (err instanceof Error && err.message.includes("EPIPE")) {
5072
- logger15.warn(`[subagents] EPIPE on hot path for ${record.id}, falling back to cold path resume`, {
5267
+ logger16.warn(`[subagents] EPIPE on hot path for ${record.id}, falling back to cold path resume`, {
5073
5268
  detail: err.message
5074
5269
  });
5075
5270
  if (spawnedChildren.get(record.id) === child) {
@@ -5120,7 +5315,7 @@ var PiEngine = class {
5120
5315
  return;
5121
5316
  }
5122
5317
  }
5123
- logger15.warn(
5318
+ logger16.warn(
5124
5319
  `[subagents] deliverPrompt hot path failed for ${record.id}; idle timer re-armed to keep process recovery bounded`,
5125
5320
  { detail }
5126
5321
  );
@@ -5269,7 +5464,7 @@ async function defaultProbeVersion(invocation) {
5269
5464
  );
5270
5465
  });
5271
5466
  } catch (err) {
5272
- logger15.debug(
5467
+ logger16.debug(
5273
5468
  `[pi-engine] probe version check failed (best-effort continue): ${toErrorMessage(err)}`
5274
5469
  );
5275
5470
  return void 0;
@@ -5278,12 +5473,12 @@ async function defaultProbeVersion(invocation) {
5278
5473
  function isInvocationResolvable(invocation) {
5279
5474
  if (invocation.command !== "pi") return true;
5280
5475
  const pathEnv = process.env.PATH ?? "";
5281
- for (const dir of pathEnv.split(path8.delimiter)) {
5476
+ for (const dir of pathEnv.split(path9.delimiter)) {
5282
5477
  if (dir === "") continue;
5283
5478
  try {
5284
- if (fs13.existsSync(path8.join(dir, "pi"))) return true;
5479
+ if (fs14.existsSync(path9.join(dir, "pi"))) return true;
5285
5480
  } catch (err) {
5286
- logger15.debug(
5481
+ logger16.debug(
5287
5482
  `[pi-engine] PATH dir probe failed (continue scanning): ${dir}: ${toErrorMessage(err)}`
5288
5483
  );
5289
5484
  }
@@ -5412,14 +5607,14 @@ function safeEngineDefault(engine) {
5412
5607
  }
5413
5608
 
5414
5609
  // src/execution/manifest-store.ts
5415
- var fs14 = __toESM(require("fs"), 1);
5610
+ var fs15 = __toESM(require("fs"), 1);
5416
5611
  var fsPromises2 = __toESM(require("fs/promises"), 1);
5417
- var path9 = __toESM(require("path"), 1);
5418
- var logger16 = getLogger("subagents");
5612
+ var path10 = __toESM(require("path"), 1);
5613
+ var logger17 = getLogger("subagents");
5419
5614
  var MANIFEST_INDENT_SPACES = 2;
5420
5615
  function statStamp(p) {
5421
5616
  try {
5422
- const s = fs14.statSync(p);
5617
+ const s = fs15.statSync(p);
5423
5618
  return { mtimeMs: s.mtimeMs, size: s.size };
5424
5619
  } catch {
5425
5620
  return null;
@@ -5447,8 +5642,8 @@ var ManifestStore = class {
5447
5642
  cache = /* @__PURE__ */ new Map();
5448
5643
  constructor(dir) {
5449
5644
  this.dir = dir;
5450
- if (!fs14.existsSync(dir)) {
5451
- fs14.mkdirSync(dir, { recursive: true });
5645
+ if (!fs15.existsSync(dir)) {
5646
+ fs15.mkdirSync(dir, { recursive: true });
5452
5647
  }
5453
5648
  }
5454
5649
  /**
@@ -5460,7 +5655,7 @@ var ManifestStore = class {
5460
5655
  * 调用方(finalizeRecord)决定降级策略。
5461
5656
  */
5462
5657
  async writeManifest(record) {
5463
- const filePath = path9.join(this.dir, `${record.id}.json`);
5658
+ const filePath = path10.join(this.dir, `${record.id}.json`);
5464
5659
  const content = JSON.stringify(record, null, MANIFEST_INDENT_SPACES);
5465
5660
  await writeAtomicFile(filePath, content, { ensureDir: false });
5466
5661
  }
@@ -5469,7 +5664,7 @@ var ManifestStore = class {
5469
5664
  * 调用方需处理 null。
5470
5665
  */
5471
5666
  async readManifest(id) {
5472
- const filePath = path9.join(this.dir, `${id}.json`);
5667
+ const filePath = path10.join(this.dir, `${id}.json`);
5473
5668
  try {
5474
5669
  const content = await fsPromises2.readFile(filePath, "utf-8");
5475
5670
  const parsed = JSON.parse(content);
@@ -5489,7 +5684,7 @@ var ManifestStore = class {
5489
5684
  listAllSync() {
5490
5685
  let files;
5491
5686
  try {
5492
- files = fs14.readdirSync(this.dir);
5687
+ files = fs15.readdirSync(this.dir);
5493
5688
  } catch {
5494
5689
  return [];
5495
5690
  }
@@ -5500,7 +5695,7 @@ var ManifestStore = class {
5500
5695
  }
5501
5696
  const results = [];
5502
5697
  for (const file of names) {
5503
- const filePath = path9.join(this.dir, file);
5698
+ const filePath = path10.join(this.dir, file);
5504
5699
  const stamp = statStamp(filePath);
5505
5700
  if (!stamp) {
5506
5701
  this.cache.delete(file);
@@ -5512,7 +5707,7 @@ var ManifestStore = class {
5512
5707
  continue;
5513
5708
  }
5514
5709
  try {
5515
- const content = fs14.readFileSync(filePath, "utf-8");
5710
+ const content = fs15.readFileSync(filePath, "utf-8");
5516
5711
  const parsed = JSON.parse(content);
5517
5712
  const record = isValidManifest(parsed) ? parsed : null;
5518
5713
  this.cache.set(file, { stamp, record });
@@ -5540,41 +5735,41 @@ var ManifestStore = class {
5540
5735
  let deleted = 0;
5541
5736
  let recovered = 0;
5542
5737
  let failed = 0;
5543
- const files = fs14.readdirSync(this.dir);
5738
+ const files = fs15.readdirSync(this.dir);
5544
5739
  const tmpFiles = files.filter((f) => f.includes(".json.tmp."));
5545
5740
  for (const tmpFile of tmpFiles) {
5546
- const tmpPath = path9.join(this.dir, tmpFile);
5741
+ const tmpPath = path10.join(this.dir, tmpFile);
5547
5742
  const manifestId = tmpFile.split(".json.tmp.")[0];
5548
- const manifestPath = path9.join(this.dir, `${manifestId}.json`);
5743
+ const manifestPath = path10.join(this.dir, `${manifestId}.json`);
5549
5744
  try {
5550
- if (fs14.existsSync(manifestPath)) {
5551
- fs14.unlinkSync(tmpPath);
5745
+ if (fs15.existsSync(manifestPath)) {
5746
+ fs15.unlinkSync(tmpPath);
5552
5747
  deleted++;
5553
5748
  } else {
5554
5749
  try {
5555
- const content = fs14.readFileSync(tmpPath, "utf-8");
5750
+ const content = fs15.readFileSync(tmpPath, "utf-8");
5556
5751
  const parsed = JSON.parse(content);
5557
5752
  if (isValidManifest(parsed)) {
5558
- fs14.renameSync(tmpPath, manifestPath);
5753
+ fs15.renameSync(tmpPath, manifestPath);
5559
5754
  recovered++;
5560
5755
  } else {
5561
- fs14.unlinkSync(tmpPath);
5756
+ fs15.unlinkSync(tmpPath);
5562
5757
  deleted++;
5563
5758
  }
5564
5759
  } catch {
5565
- fs14.unlinkSync(tmpPath);
5760
+ fs15.unlinkSync(tmpPath);
5566
5761
  deleted++;
5567
5762
  }
5568
5763
  }
5569
5764
  } catch (fileErr) {
5570
5765
  failed++;
5571
- logger16.warn(`[subagents] recoverTmpFiles: failed to recover ${tmpFile}, skipping (leftovers retry on next startup)`, {
5766
+ logger17.warn(`[subagents] recoverTmpFiles: failed to recover ${tmpFile}, skipping (leftovers retry on next startup)`, {
5572
5767
  detail: fileErr instanceof Error ? fileErr.message : String(fileErr)
5573
5768
  });
5574
5769
  }
5575
5770
  }
5576
5771
  if (failed > 0) {
5577
- logger16.warn(
5772
+ logger17.warn(
5578
5773
  `[subagents] recoverTmpFiles: ${failed} of ${tmpFiles.length} tmp file(s) could not be recovered`
5579
5774
  );
5580
5775
  }
@@ -5647,8 +5842,11 @@ function isResumable(record) {
5647
5842
  return record.status === "running" && !hasLiveProcessHandle(record.id);
5648
5843
  }
5649
5844
 
5845
+ // src/execution/notifier.ts
5846
+ var import_node_crypto = require("crypto");
5847
+
5650
5848
  // src/execution/notify-ledger.ts
5651
- var logger17 = getLogger("subagents");
5849
+ var logger18 = getLogger("subagents");
5652
5850
  var NOTIFY_LEDGER_CUSTOM_TYPE = "subagent-bg-notify-ledger";
5653
5851
  var NOTIFY_ACK_CUSTOM_TYPE = "subagent-bg-notify-ack";
5654
5852
  var NOTIFY_ABANDONED_CUSTOM_TYPE = "subagent-bg-notify-abandoned";
@@ -5874,7 +6072,7 @@ function createNotifyLedger(host, options) {
5874
6072
  host.appendLedgerEntry(NOTIFY_ABANDONED_CUSTOM_TYPE, { v: 1, notifyId: item.notifyId });
5875
6073
  items.delete(item.notifyId);
5876
6074
  abandonedIds.add(item.notifyId);
5877
- logger17.warn(
6075
+ logger18.warn(
5878
6076
  `Subagent "${itemLabel(item)}" notification abandoned - no receipt after ${NOTIFY_REDELIVERY_MAX_ATTEMPTS} delivery attempts; verify manually via subagents action:"list"`,
5879
6077
  { notifyId: item.notifyId, attempts: item.attempts }
5880
6078
  );
@@ -5925,7 +6123,7 @@ function createNotifyLedger(host, options) {
5925
6123
  api.checkReceipts();
5926
6124
  }
5927
6125
  function emitBucketLog(bucket, total, extra) {
5928
- logger17.warn(`notify delivery bucket [${bucket}]`, { total, ...extra });
6126
+ logger18.warn(`notify delivery bucket [${bucket}]`, { total, ...extra });
5929
6127
  }
5930
6128
  if (options?.registerSettledListener !== false) {
5931
6129
  host.onAgentSettled(() => {
@@ -5973,6 +6171,70 @@ function getBoundNotifyLedger() {
5973
6171
 
5974
6172
  // src/execution/notifier.ts
5975
6173
  var notifyLogger = getLogger("subagents");
6174
+ var BATCH_NOTIFY_ID_PREFIX = "sync-batch:";
6175
+ function buildBatchNotifyId(memberIds) {
6176
+ const digest = (0, import_node_crypto.createHash)("sha1").update([...memberIds].sort().join(",")).digest("hex");
6177
+ return `${BATCH_NOTIFY_ID_PREFIX}${digest}`;
6178
+ }
6179
+ var DEFAULT_BATCH_BUDGET = { perItemChars: 4e3, totalChars: 24e3 };
6180
+ var LIST_ONLY_FLOOR = 200;
6181
+ function computeBatchBudget(bodyLengths, perItemChars, totalChars) {
6182
+ const n = bodyLengths.length;
6183
+ const sumAfterPerItem = bodyLengths.reduce((sum, len) => sum + Math.min(len, perItemChars), 0);
6184
+ if (n === 0 || sumAfterPerItem <= totalChars) {
6185
+ return { tightened: false, effectivePerItem: perItemChars, listOnly: false };
6186
+ }
6187
+ const tight = Math.floor(totalChars / n);
6188
+ if (tight < LIST_ONLY_FLOOR) {
6189
+ return { tightened: true, effectivePerItem: 0, listOnly: true };
6190
+ }
6191
+ return { tightened: true, effectivePerItem: Math.min(tight, perItemChars), listOnly: false };
6192
+ }
6193
+ function formatChars(n) {
6194
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
6195
+ }
6196
+ var SESSION_READ_DEFAULT_LIMIT = 8e3;
6197
+ function buildTruncationPointer(id, kept, total, perItemLimit) {
6198
+ const limitPart = perItemLimit > SESSION_READ_DEFAULT_LIMIT ? `,"limit":${perItemLimit}` : "";
6199
+ return `[truncated ${formatChars(total - kept)} of ${formatChars(total)} chars \u2014 full result: session_read {"action":"result","session":"${id}"${limitPart}}]`;
6200
+ }
6201
+ function withMaterializedOutcome(record) {
6202
+ return record.status === "closed" ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error) } : record;
6203
+ }
6204
+ function buildBatchLlmContent(records, budget = DEFAULT_BATCH_BUDGET) {
6205
+ const mat = records.map(withMaterializedOutcome);
6206
+ let finished = 0;
6207
+ let failed = 0;
6208
+ let cancelled = 0;
6209
+ const bodies = mat.map((record) => {
6210
+ if (record.status === "closed" && record.outcome !== "completed") return null;
6211
+ return record.result ?? "(empty)";
6212
+ });
6213
+ const plan = computeBatchBudget(
6214
+ bodies.map((body) => body?.length ?? 0),
6215
+ budget.perItemChars,
6216
+ budget.totalChars
6217
+ );
6218
+ const items = mat.map((record, i) => {
6219
+ if (record.status === "closed") {
6220
+ const outcome = record.outcome;
6221
+ if (outcome === "completed") finished += 1;
6222
+ else if (outcome === "failed") failed += 1;
6223
+ else if (outcome === "cancelled") cancelled += 1;
6224
+ }
6225
+ const body = bodies[i];
6226
+ if (body === null || body.length <= plan.effectivePerItem) {
6227
+ return buildLlmContent(record);
6228
+ }
6229
+ const kept = body.slice(0, plan.effectivePerItem);
6230
+ const truncated = buildLlmContent({ ...record, result: plan.listOnly ? "" : `${kept}\u2026` });
6231
+ const stripped = plan.listOnly ? truncated.replace(/\n$/, "") : truncated;
6232
+ return `${stripped}
6233
+ ${buildTruncationPointer(record.id, kept.length, body.length, plan.effectivePerItem)}`;
6234
+ });
6235
+ const header = `Subagent batch completed: ${finished} finished, ${failed} failed, ${cancelled} cancelled.`;
6236
+ return [header, ...items].join("\n\n---\n\n");
6237
+ }
5976
6238
  function buildLlmContent(record) {
5977
6239
  const agent = record.agent;
5978
6240
  const id = record.id;
@@ -6090,31 +6352,45 @@ function createNotifier(host) {
6090
6352
  });
6091
6353
  };
6092
6354
  let handle = createHandle();
6355
+ function deliverViaLedgerOrKernel(notifyId, content, details) {
6356
+ const ledger = getBoundNotifyLedger();
6357
+ if (ledger) {
6358
+ if (!ledger.record(notifyId, content, details)) return false;
6359
+ ledger.attemptDeliver();
6360
+ return true;
6361
+ }
6362
+ notifyLogger.warn("notify ledger not bound, falling back to delivery kernel path (at-most-once)", {
6363
+ notifyId
6364
+ });
6365
+ handle.send({
6366
+ payload: {
6367
+ kind: "custom",
6368
+ customType: NOTIFY_CUSTOM_TYPE,
6369
+ content,
6370
+ display: true,
6371
+ details
6372
+ },
6373
+ dedupeKey: notifyId
6374
+ });
6375
+ return true;
6376
+ }
6093
6377
  return {
6094
6378
  notify(record) {
6095
6379
  if (disposed) return;
6096
6380
  const notifyId = record.round != null ? `${record.id}:${record.round}` : record.id;
6097
6381
  const payload = record.status === "closed" ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error), notifyId } : { ...record, notifyId };
6098
6382
  const content = buildLlmContent(payload);
6099
- const ledger = getBoundNotifyLedger();
6100
- if (ledger) {
6101
- if (!ledger.record(notifyId, content, payload)) return;
6102
- ledger.attemptDeliver();
6103
- return;
6104
- }
6105
- notifyLogger.warn("notify ledger not bound, falling back to delivery kernel path (at-most-once)", {
6106
- notifyId
6107
- });
6108
- handle.send({
6109
- payload: {
6110
- kind: "custom",
6111
- customType: NOTIFY_CUSTOM_TYPE,
6112
- content,
6113
- display: true,
6114
- details: payload
6115
- },
6116
- dedupeKey: notifyId
6117
- });
6383
+ deliverViaLedgerOrKernel(notifyId, content, payload);
6384
+ },
6385
+ notifyBatch(records, budget) {
6386
+ if (disposed || records.length === 0) return false;
6387
+ const payloads = records.map(
6388
+ (record) => record.status === "closed" ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error) } : { ...record }
6389
+ );
6390
+ const batchNotifyId = buildBatchNotifyId(payloads.map((p) => p.id));
6391
+ const content = buildBatchLlmContent(payloads, budget);
6392
+ const details = { batch: true, notifyId: batchNotifyId, items: payloads };
6393
+ return deliverViaLedgerOrKernel(batchNotifyId, content, details);
6118
6394
  },
6119
6395
  flushPendingNotifications() {
6120
6396
  const ledger = getBoundNotifyLedger();
@@ -6238,6 +6514,15 @@ function createNotifyHost(deps) {
6238
6514
  emitPendingUnregister(id, reason) {
6239
6515
  emitPendingUnregister(deps.getPi(), id, reason);
6240
6516
  },
6517
+ toNotifyRecord(record) {
6518
+ return toNotifyRecord(record);
6519
+ },
6520
+ notify(record) {
6521
+ notifier.notify(record);
6522
+ },
6523
+ notifyBatch(records, budget) {
6524
+ return notifier.notifyBatch(records, budget);
6525
+ },
6241
6526
  revive() {
6242
6527
  notifier.revive();
6243
6528
  },
@@ -6251,7 +6536,7 @@ function createNotifyHost(deps) {
6251
6536
  }
6252
6537
 
6253
6538
  // src/execution/round-settlement.ts
6254
- var logger18 = getLogger("subagents");
6539
+ var logger19 = getLogger("subagents");
6255
6540
  function createRoundSettler(deps) {
6256
6541
  return (record) => {
6257
6542
  record.round = (record.round ?? 0) + 1;
@@ -6260,7 +6545,7 @@ function createRoundSettler(deps) {
6260
6545
  deps.notifyComplete(record);
6261
6546
  const lastTurn = record.turns[record.turns.length - 1];
6262
6547
  if (lastTurn !== void 0 && !lastTurn.closed && lastTurn.text.length > 0) {
6263
- logger18.warn(
6548
+ logger19.warn(
6264
6549
  `[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`
6265
6550
  );
6266
6551
  }
@@ -6274,8 +6559,8 @@ function createRoundSettler(deps) {
6274
6559
  }
6275
6560
 
6276
6561
  // src/execution/record-store.ts
6277
- var fs16 = __toESM(require("fs"), 1);
6278
- var path11 = __toESM(require("path"), 1);
6562
+ var fs17 = __toESM(require("fs"), 1);
6563
+ var path12 = __toESM(require("path"), 1);
6279
6564
 
6280
6565
  // src/execution/record-entry.ts
6281
6566
  var SUBAGENT_RECORD_CUSTOM_TYPE = "subagent-record";
@@ -6310,14 +6595,18 @@ function toSubagentRecordEntry(record) {
6310
6595
  resumable: record.resumable,
6311
6596
  engine: record.engine,
6312
6597
  engineFallback: record.engineFallback,
6313
- engineHandle: record.engineHandle
6598
+ engineHandle: record.engineHandle,
6599
+ // 同步收集两字段(U1 foundation):undefined 经 JSON.stringify 自然缺省,
6600
+ // 旧记录/旧 entry 序列化产物字节不变(零迁移)。
6601
+ collectMode: record.collectMode,
6602
+ batchFinalized: record.batchFinalized
6314
6603
  };
6315
6604
  }
6316
6605
 
6317
6606
  // src/execution/sessions-index.ts
6318
- var fs15 = __toESM(require("fs"), 1);
6319
- var path10 = __toESM(require("path"), 1);
6320
- var logger19 = getLogger("subagents");
6607
+ var fs16 = __toESM(require("fs"), 1);
6608
+ var path11 = __toESM(require("path"), 1);
6609
+ var logger20 = getLogger("subagents");
6321
6610
  var INDEX_FILENAME = "sessions-index.json";
6322
6611
  var INDEX_VERSION = 1;
6323
6612
  var INDEX_WRITE_MIN_INTERVAL_MS = 6e4;
@@ -6356,11 +6645,11 @@ function errorCodeOf(err) {
6356
6645
  }
6357
6646
  function readIndexFile(indexPath, encDir) {
6358
6647
  try {
6359
- return fs15.readFileSync(indexPath, "utf-8");
6648
+ return fs16.readFileSync(indexPath, "utf-8");
6360
6649
  } catch (err) {
6361
6650
  const code = errorCodeOf(err);
6362
6651
  if (code !== "ENOENT") {
6363
- logger19.debug("[subagents] sessions-index read failed, fallback to empty", {
6652
+ logger20.debug("[subagents] sessions-index read failed, fallback to empty", {
6364
6653
  detail: { dir: encDir, code }
6365
6654
  });
6366
6655
  }
@@ -6371,7 +6660,7 @@ function parseIndexJson(raw, indexPath) {
6371
6660
  try {
6372
6661
  return JSON.parse(raw);
6373
6662
  } catch (err) {
6374
- logger19.debug("[subagents] sessions-index corrupted JSON, fallback to empty", {
6663
+ logger20.debug("[subagents] sessions-index corrupted JSON, fallback to empty", {
6375
6664
  detail: { path: indexPath, error: err instanceof Error ? err.message : String(err) }
6376
6665
  });
6377
6666
  return null;
@@ -6382,14 +6671,14 @@ function isIndexTopHeader(v) {
6382
6671
  }
6383
6672
  function readTopLevel(parsed, indexPath) {
6384
6673
  if (typeof parsed !== "object" || parsed === null) {
6385
- logger19.debug("[subagents] sessions-index invalid top-level shape, fallback to empty", {
6674
+ logger20.debug("[subagents] sessions-index invalid top-level shape, fallback to empty", {
6386
6675
  detail: { path: indexPath }
6387
6676
  });
6388
6677
  return null;
6389
6678
  }
6390
6679
  const top = parsed;
6391
6680
  if (!isIndexTopHeader(top)) {
6392
- logger19.debug("[subagents] sessions-index invalid header fields, fallback to empty", {
6681
+ logger20.debug("[subagents] sessions-index invalid header fields, fallback to empty", {
6393
6682
  detail: { path: indexPath }
6394
6683
  });
6395
6684
  return null;
@@ -6406,7 +6695,7 @@ function collectValidEntries(entriesObj) {
6406
6695
  }
6407
6696
  function loadIndex(encDir) {
6408
6697
  const empty = { entries: /* @__PURE__ */ new Map(), higherVersion: false };
6409
- const indexPath = path10.join(encDir, INDEX_FILENAME);
6698
+ const indexPath = path11.join(encDir, INDEX_FILENAME);
6410
6699
  const raw = readIndexFile(indexPath, encDir);
6411
6700
  if (raw === null) return empty;
6412
6701
  const parsed = parseIndexJson(raw, indexPath);
@@ -6417,7 +6706,7 @@ function loadIndex(encDir) {
6417
6706
  return { entries: /* @__PURE__ */ new Map(), higherVersion: true };
6418
6707
  }
6419
6708
  if (top.version < INDEX_VERSION) {
6420
- logger19.debug("[subagents] sessions-index stale version, discarded", {
6709
+ logger20.debug("[subagents] sessions-index stale version, discarded", {
6421
6710
  detail: { path: indexPath, version: top.version, expected: INDEX_VERSION }
6422
6711
  });
6423
6712
  return empty;
@@ -6425,7 +6714,7 @@ function loadIndex(encDir) {
6425
6714
  return { entries: collectValidEntries(top.entries), higherVersion: false };
6426
6715
  }
6427
6716
  async function saveIndex(encDir, data) {
6428
- const filePath = path10.join(encDir, INDEX_FILENAME);
6717
+ const filePath = path11.join(encDir, INDEX_FILENAME);
6429
6718
  const file = {
6430
6719
  version: INDEX_VERSION,
6431
6720
  pid: process.pid,
@@ -6435,7 +6724,7 @@ async function saveIndex(encDir, data) {
6435
6724
  }
6436
6725
 
6437
6726
  // src/execution/record-store.ts
6438
- var logger20 = getLogger("subagents");
6727
+ var logger21 = getLogger("subagents");
6439
6728
  var STATUS_PRIORITY = {
6440
6729
  running: 0,
6441
6730
  closed: 3
@@ -6454,14 +6743,14 @@ var MIN_SEGMENTS_WITH_BOUNDARY = 2;
6454
6743
  function readLastJsonlLine(sessionFile) {
6455
6744
  let fd;
6456
6745
  try {
6457
- fd = fs16.openSync(sessionFile, "r");
6458
- const size = fs16.fstatSync(fd).size;
6746
+ fd = fs17.openSync(sessionFile, "r");
6747
+ const size = fs17.fstatSync(fd).size;
6459
6748
  let windowBytes = LAST_LINE_WINDOW_BYTES;
6460
6749
  let windowStart = Math.max(0, size - windowBytes);
6461
6750
  let lines = [];
6462
6751
  while (true) {
6463
6752
  const buf = Buffer.alloc(size - windowStart);
6464
- fs16.readSync(fd, buf, 0, buf.length, windowStart);
6753
+ fs17.readSync(fd, buf, 0, buf.length, windowStart);
6465
6754
  lines = buf.toString("utf-8").split("\n").filter((l) => l.length > 0);
6466
6755
  if (windowStart === 0 || lines.length >= MIN_SEGMENTS_WITH_BOUNDARY) break;
6467
6756
  windowBytes *= WINDOW_GROWTH_FACTOR;
@@ -6478,7 +6767,7 @@ function readLastJsonlLine(sessionFile) {
6478
6767
  } finally {
6479
6768
  if (fd !== void 0) {
6480
6769
  try {
6481
- fs16.closeSync(fd);
6770
+ fs17.closeSync(fd);
6482
6771
  } catch (_e) {
6483
6772
  void _e;
6484
6773
  }
@@ -6501,7 +6790,7 @@ function collectLastRecordEntries(content) {
6501
6790
  try {
6502
6791
  entry = asSubagentRecordEntry(JSON.parse(line));
6503
6792
  } catch (err) {
6504
- logger20.debug("[subagents] entry-only orphan scan: skip unparsable line", {
6793
+ logger21.debug("[subagents] entry-only orphan scan: skip unparsable line", {
6505
6794
  reason: err instanceof Error ? err.message : String(err)
6506
6795
  });
6507
6796
  }
@@ -6509,37 +6798,64 @@ function collectLastRecordEntries(content) {
6509
6798
  }
6510
6799
  return lastById;
6511
6800
  }
6801
+ function entryStr(d, k) {
6802
+ return typeof d[k] === "string" ? d[k] : void 0;
6803
+ }
6804
+ function entryNum(d, k) {
6805
+ return typeof d[k] === "number" ? d[k] : void 0;
6806
+ }
6807
+ function readEntryTerminalFields(d) {
6808
+ const closedReason = entryStr(d, "closedReason");
6809
+ return {
6810
+ status: d.status === "closed" ? "closed" : "running",
6811
+ closedReason: isValidClosedReason(closedReason) ? closedReason : void 0
6812
+ };
6813
+ }
6814
+ function readEntryBatchFields(d) {
6815
+ return {
6816
+ collectMode: d.collectMode === "sync" ? "sync" : void 0,
6817
+ batchFinalized: d.batchFinalized === true ? true : void 0,
6818
+ resumable: d.resumable === true ? true : void 0
6819
+ };
6820
+ }
6821
+ function readEntryEngineFields(d) {
6822
+ return {
6823
+ engine: entryStr(d, "engine"),
6824
+ engineFallback: isEngineFallbackShape(d.engineFallback) ? d.engineFallback : void 0,
6825
+ engineHandle: isEngineHandleShape(d.engineHandle) ? d.engineHandle : void 0
6826
+ };
6827
+ }
6512
6828
  function rebuildEntryRecord(id, d) {
6513
- const str = (k) => typeof d[k] === "string" ? d[k] : void 0;
6514
- const num = (k) => typeof d[k] === "number" ? d[k] : void 0;
6515
- const agent = str("agent");
6516
- const task = str("task");
6517
- const startedAt = num("startedAt");
6829
+ const agent = entryStr(d, "agent");
6830
+ const task = entryStr(d, "task");
6831
+ const startedAt = entryNum(d, "startedAt");
6518
6832
  if (agent === void 0 || task === void 0 || startedAt === void 0) return null;
6519
6833
  return {
6520
6834
  id,
6521
6835
  agent,
6522
6836
  task,
6523
- slug: str("slug") ?? "",
6524
- status: "running",
6837
+ slug: entryStr(d, "slug") ?? "",
6838
+ ...readEntryTerminalFields(d),
6525
6839
  mode: "background",
6526
6840
  startedAt,
6527
- rootSessionId: str("rootSessionId"),
6528
- parentRecordId: str("parentRecordId"),
6529
- depth: num("depth") ?? 0,
6530
- endedAt: void 0,
6531
- turns: num("turns") ?? 0,
6532
- totalTokens: num("totalTokens") ?? 0,
6533
- model: str("model") ?? "",
6534
- thinkingLevel: str("thinkingLevel"),
6841
+ rootSessionId: entryStr(d, "rootSessionId"),
6842
+ parentRecordId: entryStr(d, "parentRecordId"),
6843
+ depth: entryNum(d, "depth") ?? 0,
6844
+ endedAt: entryNum(d, "endedAt"),
6845
+ turns: entryNum(d, "turns") ?? 0,
6846
+ totalTokens: entryNum(d, "totalTokens") ?? 0,
6847
+ model: entryStr(d, "model") ?? "",
6848
+ thinkingLevel: entryStr(d, "thinkingLevel"),
6535
6849
  eventLog: [],
6536
6850
  displayItems: [],
6537
- sessionFile: void 0,
6851
+ result: entryStr(d, "result"),
6852
+ error: entryStr(d, "error"),
6853
+ sessionFile: entryStr(d, "sessionFile"),
6854
+ patchFile: entryStr(d, "patchFile"),
6538
6855
  chatMode: d.chatMode === true,
6539
- round: num("round"),
6540
- engine: str("engine"),
6541
- engineFallback: isEngineFallbackShape(d.engineFallback) ? d.engineFallback : void 0,
6542
- engineHandle: isEngineHandleShape(d.engineHandle) ? d.engineHandle : void 0
6856
+ round: entryNum(d, "round"),
6857
+ ...readEntryEngineFields(d),
6858
+ ...readEntryBatchFields(d)
6543
6859
  };
6544
6860
  }
6545
6861
  function isEngineFallbackShape(v) {
@@ -6562,7 +6878,7 @@ function isEngineHandleShape(v) {
6562
6878
  }
6563
6879
  function statStamp2(p) {
6564
6880
  try {
6565
- const s = fs16.statSync(p);
6881
+ const s = fs17.statSync(p);
6566
6882
  return { mtimeMs: s.mtimeMs, size: s.size };
6567
6883
  } catch {
6568
6884
  return null;
@@ -6738,7 +7054,7 @@ var RecordStore = class _RecordStore {
6738
7054
  if (rootSessionFilter !== void 0 && manifest.rootSessionId !== rootSessionFilter) continue;
6739
7055
  const rec = _RecordStore.manifestToSubagent(manifest);
6740
7056
  if (!rec) {
6741
- logger20.warn("[subagents] skip manifest with invalid status", {
7057
+ logger21.warn("[subagents] skip manifest with invalid status", {
6742
7058
  detail: { id: manifest.id, status: manifest.status }
6743
7059
  });
6744
7060
  this.pi?.appendEntry?.("subagent:manifest-invalid-status", {
@@ -6785,31 +7101,44 @@ var RecordStore = class _RecordStore {
6785
7101
  * - 文件不可读(IO 错误,可能暂时)→ 不判终态,落 resumable entry(防御性路径,
6786
7102
  * IO 恢复后重开可重判)。
6787
7103
  *
7104
+ * [v2 D3] mainSessionFile = 覆写 merge 数据源(主 session 每 id 末条 entry,与 E1
7105
+ * 的 scanLastRecordEntries 同款通路):崩溃前的批域标记(collectMode/batchFinalized)
7106
+ * 与轮终 result/model 只活在主文件 entry——重建矩阵(buildRecord)的数据源
7107
+ * (sidecar/子文件 identity)不含它们,覆写前不 merge 就会被抹掉(v2 §2.2 断链 2:
7108
+ * E1 候选集恒空的真根因)。缺省(undefined)时 merge 无源,行为与旧版一致。
7109
+ * 参数为追加式第二参(rootSessionFilter 保持首参):既有调用面只传过滤参。
7110
+ *
6788
7111
  * 防重:orphanJudged 实例级缓存(resumable 形态无 sidecar 锚,同进程重复调用跳过;
6789
7112
  * 终态形态双重防护 = sidecar + 缓存)。调用方:index.ts session_start 恢复段(一次)。
6790
7113
  */
6791
- recoverOrphanRecords(rootSessionFilter) {
7114
+ recoverOrphanRecords(rootSessionFilter, mainSessionFile) {
7115
+ const lastById = new Map(this.scanLastRecordEntries(mainSessionFile).map((r) => [r.id, r]));
6792
7116
  for (const rec of this.reconstructAll(rootSessionFilter)) {
6793
7117
  if (rec.status !== "running" || rec.externalInstance !== void 0) continue;
6794
7118
  if (this.orphanJudged.has(rec.id)) continue;
6795
7119
  this.orphanJudged.add(rec.id);
6796
- this.finalizeOrphanRecord(rec);
7120
+ this.finalizeOrphanRecord(rec, lastById.get(rec.id));
6797
7121
  }
6798
7122
  }
6799
7123
  /**
6800
7124
  * 单孤儿 record 的终态判定与落 entry(residual-fixes §5.2 三判据 + chat 分流)。
6801
7125
  * 防重锚(orphanJudged 标记)已由调用方完成。
7126
+ *
7127
+ * [v2 D3] 覆写前 merge(lastEntry = 主 session 同 id 末条 entry 重建,调用方构建):
7128
+ * 覆写是状态迁移不是信息重建,迁移不应丢末条既有信息。三个落 entry 分支(chatMode
7129
+ * 分流 / IO 保守 / 终态覆写)统一基于 merge 后的 rec,同款口径。
6802
7130
  */
6803
- finalizeOrphanRecord(rec) {
6804
- if (rec.chatMode === true) {
6805
- this.reportSubagentRecord({ ...rec, resumable: true });
7131
+ finalizeOrphanRecord(rec, lastEntry) {
7132
+ const rec0 = lastEntry === void 0 ? rec : _RecordStore.mergeOrphanLastEntry(rec, lastEntry);
7133
+ if (rec0.chatMode === true) {
7134
+ this.reportSubagentRecord({ ...rec0, resumable: true });
6806
7135
  return;
6807
7136
  }
6808
- const sessionFile = rec.sessionFile;
7137
+ const sessionFile = rec0.sessionFile;
6809
7138
  if (sessionFile === void 0) return;
6810
7139
  const lastLine = readLastJsonlLine(sessionFile);
6811
7140
  if (!lastLine.ok) {
6812
- this.reportSubagentRecord({ ...rec, resumable: true });
7141
+ this.reportSubagentRecord({ ...rec0, resumable: true });
6813
7142
  return;
6814
7143
  }
6815
7144
  let parseOk = false;
@@ -6821,13 +7150,30 @@ var RecordStore = class _RecordStore {
6821
7150
  }
6822
7151
  writeFinalized(sessionFile, "gc");
6823
7152
  this.reportSubagentRecord({
6824
- ...rec,
7153
+ ...rec0,
6825
7154
  status: "closed",
6826
7155
  closedReason: "gc",
6827
7156
  endedAt: Date.now(),
6828
7157
  ...parseOk ? {} : { error: "orphan recovery: subagent session ended abnormally (truncated last line)" }
6829
7158
  });
6830
7159
  }
7160
+ /** [v2 D3] 孤儿覆写 merge 字段集:末条 entry 的批域标记 + 轮终正文/模型,仅补 rec 侧
7161
+ * undefined/空值(model 的空值形态是 ""——light 重建无 model_change entry 时起步
7162
+ * 空串),不覆盖已有值。merge 后写 entry 经 reportSubagentRecord →
7163
+ * toSubagentRecordEntry 序列化,undefined 字段自然缺省(不引入显式 null)。 */
7164
+ static mergeOrphanLastEntry(rec, last) {
7165
+ const pickStr = (cur, src) => cur !== void 0 && cur !== "" ? cur : src;
7166
+ return {
7167
+ ...rec,
7168
+ collectMode: rec.collectMode ?? last.collectMode,
7169
+ batchFinalized: rec.batchFinalized ?? last.batchFinalized,
7170
+ result: pickStr(rec.result, last.result),
7171
+ // 类型收尾:两侧实参恒 string(rec.model 类型非可选;last.model 重建投影自带
7172
+ // `?? ""`),pickStr 签名宽返回 string|undefined —— `?? ""` 运行时不可达,
7173
+ // 仅满足 model 非可选类型,空串回退语义不变(cur 空 → src,src 也空 → "")。
7174
+ model: pickStr(rec.model, last.model) ?? ""
7175
+ };
7176
+ }
6831
7177
  /**
6832
7178
  * [E2E 实测缺口] entry-born 孤儿恢复:register entry 已落主 session、但子 session 文件
6833
7179
  * 从未创建(父进程死在 spawn 窗口期——register 写点与子进程首笔写入之间的窗口;外部
@@ -6844,7 +7190,7 @@ var RecordStore = class _RecordStore {
6844
7190
  if (mainSessionFile === void 0) return;
6845
7191
  let content;
6846
7192
  try {
6847
- content = fs16.readFileSync(mainSessionFile, "utf-8");
7193
+ content = fs17.readFileSync(mainSessionFile, "utf-8");
6848
7194
  } catch {
6849
7195
  return;
6850
7196
  }
@@ -6883,6 +7229,31 @@ var RecordStore = class _RecordStore {
6883
7229
  }
6884
7230
  });
6885
7231
  }
7232
+ /**
7233
+ * [E1/U5] sync 批崩溃恢复扫描:主 session 文件「每 id 末条 subagent-record entry」
7234
+ * (collectLastRecordEntries + rebuildEntryRecord 组合通路,设计 §3.1.3「标记读取
7235
+ * 通路」——batchFinalized 落标 entry 写主 session 文件,本扫描同文件域才可见;禁走
7236
+ * collectRecords light 路径,它只读子文件 identity 头+sidecar,主 session 落标
7237
+ * entry 不可见)。返回每 id 末条重建的完整 record(含 collectMode/batchFinalized /
7238
+ * 终态五字段,损坏 entry 跳过);调用方(service.recoverSyncCollectBatch 的 E1 过滤、
7239
+ * recoverOrphanRecords 的覆写 merge)自行取舍。主文件不可读(含新 session 未 flush
7240
+ * 的 ENOENT)→ 空数组静默。
7241
+ */
7242
+ scanLastRecordEntries(mainSessionFile) {
7243
+ if (mainSessionFile === void 0) return [];
7244
+ let content;
7245
+ try {
7246
+ content = fs17.readFileSync(mainSessionFile, "utf-8");
7247
+ } catch {
7248
+ return [];
7249
+ }
7250
+ const out = [];
7251
+ for (const [id, d] of collectLastRecordEntries(content)) {
7252
+ const rec = rebuildEntryRecord(id, d);
7253
+ if (rec !== null) out.push(rec);
7254
+ }
7255
+ return out;
7256
+ }
6886
7257
  /** 订阅变更。返回取消订阅函数。 */
6887
7258
  onChange(listener) {
6888
7259
  this.listeners.add(listener);
@@ -6945,12 +7316,12 @@ var RecordStore = class _RecordStore {
6945
7316
  reconstructAll(rootSessionFilter) {
6946
7317
  let dirMtimeMs;
6947
7318
  try {
6948
- dirMtimeMs = fs16.statSync(this.sessionsDir).mtimeMs;
7319
+ dirMtimeMs = fs17.statSync(this.sessionsDir).mtimeMs;
6949
7320
  } catch {
6950
7321
  return [];
6951
7322
  }
6952
7323
  if (this.dirStamp === null) {
6953
- const loaded = loadIndex(path11.dirname(this.sessionsDir));
7324
+ const loaded = loadIndex(path12.dirname(this.sessionsDir));
6954
7325
  this.indexEntries = loaded.entries;
6955
7326
  this.indexHigherVersion = loaded.higherVersion;
6956
7327
  }
@@ -6966,7 +7337,7 @@ var RecordStore = class _RecordStore {
6966
7337
  }
6967
7338
  let files;
6968
7339
  try {
6969
- files = fs16.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl")).map((f) => path11.join(this.sessionsDir, f));
7340
+ files = fs17.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".jsonl")).map((f) => path12.join(this.sessionsDir, f));
6970
7341
  } catch {
6971
7342
  this.indexEntries = null;
6972
7343
  return [];
@@ -7040,7 +7411,7 @@ var RecordStore = class _RecordStore {
7040
7411
  */
7041
7412
  buildEntryFromIndex(file, stamps, now) {
7042
7413
  if (this.indexEntries === null) return void 0;
7043
- const hit = this.indexEntries.get(path11.basename(file));
7414
+ const hit = this.indexEntries.get(path12.basename(file));
7044
7415
  if (hit === void 0 || hit.mtimeMs !== stamps.jsonl.mtimeMs || hit.size !== stamps.jsonl.size) {
7045
7416
  return void 0;
7046
7417
  }
@@ -7072,12 +7443,12 @@ var RecordStore = class _RecordStore {
7072
7443
  if (Date.now() - this.lastIndexWriteAt < INDEX_WRITE_MIN_INTERVAL_MS) return;
7073
7444
  const entries = this.projectIndexEntries();
7074
7445
  this.indexDirty = false;
7075
- const encDir = path11.dirname(this.sessionsDir);
7446
+ const encDir = path12.dirname(this.sessionsDir);
7076
7447
  saveIndex(encDir, { entries }).then(() => {
7077
7448
  this.lastIndexWriteAt = Date.now();
7078
7449
  }).catch((err) => {
7079
7450
  this.indexDirty = true;
7080
- logger20.warn("[subagents] sessions-index write failed", {
7451
+ logger21.warn("[subagents] sessions-index write failed", {
7081
7452
  detail: { dir: encDir, error: err instanceof Error ? err.message : String(err) }
7082
7453
  });
7083
7454
  });
@@ -7091,7 +7462,7 @@ var RecordStore = class _RecordStore {
7091
7462
  projectIndexEntries() {
7092
7463
  const entries = /* @__PURE__ */ new Map();
7093
7464
  for (const [file, cached] of this.fileCache) {
7094
- const base = path11.basename(file);
7465
+ const base = path12.basename(file);
7095
7466
  if (cached.negative) {
7096
7467
  entries.set(base, { negative: true, mtimeMs: cached.jsonl.mtimeMs, size: cached.jsonl.size });
7097
7468
  } else {
@@ -7346,15 +7717,65 @@ var RecordStore = class _RecordStore {
7346
7717
  engine: r.engine,
7347
7718
  engineFallback: r.engineFallback,
7348
7719
  // U2:engineHandle 经 entry 持久化(register/archive 双写点均经本投影),无则 undefined 自然省略
7349
- engineHandle: r.engineHandle
7720
+ engineHandle: r.engineHandle,
7721
+ // [U5 修复 U2 披露的投影缺口] 同步收集两字段随本投影持久化(register entry /
7722
+ // archive entry 双写点)——原缺失时闭合判定 flushBatch 重建、E1 重建扫描等消费方
7723
+ // 读不到原始值。undefined 经 JSON.stringify 自然缺省,旧 entry 零迁移。
7724
+ collectMode: r.collectMode,
7725
+ batchFinalized: r.batchFinalized
7350
7726
  };
7351
7727
  }
7352
7728
  };
7353
7729
 
7730
+ // src/execution/sync-rebuild.ts
7731
+ function syncRebuildToNotifyMember(rec) {
7732
+ const status = rec.status === "closed" || !rec.chatMode ? "closed" : "running";
7733
+ return {
7734
+ id: rec.id,
7735
+ status,
7736
+ closedReason: rec.closedReason,
7737
+ outcome: status === "closed" ? deriveOutcome(rec.closedReason, rec.error) : void 0,
7738
+ agent: rec.agent,
7739
+ model: rec.model,
7740
+ result: rec.result,
7741
+ error: rec.error,
7742
+ startedAt: rec.startedAt,
7743
+ endedAt: rec.endedAt,
7744
+ patchFile: rec.patchFile
7745
+ };
7746
+ }
7747
+ function bufferedMemberFallbackRecord(m, rootSessionId) {
7748
+ return {
7749
+ id: m.id,
7750
+ agent: m.agent,
7751
+ task: "",
7752
+ slug: "",
7753
+ status: "closed",
7754
+ closedReason: m.closedReason,
7755
+ mode: "background",
7756
+ startedAt: m.startedAt,
7757
+ rootSessionId,
7758
+ parentRecordId: void 0,
7759
+ depth: 0,
7760
+ endedAt: m.endedAt,
7761
+ turns: 0,
7762
+ totalTokens: 0,
7763
+ model: m.model ?? "",
7764
+ thinkingLevel: void 0,
7765
+ eventLog: [],
7766
+ displayItems: [],
7767
+ result: m.result,
7768
+ error: m.error,
7769
+ sessionFile: void 0,
7770
+ chatMode: false,
7771
+ patchFile: m.patchFile
7772
+ };
7773
+ }
7774
+
7354
7775
  // src/execution/engine/common/pool-manager.ts
7355
7776
  var fsSync = __toESM(require("fs"), 1);
7356
7777
  var import_node_path5 = require("path");
7357
- var logger21 = getLogger("subagents");
7778
+ var logger22 = getLogger("subagents");
7358
7779
  var POOL_CLEANUP_FAILED_MARKER = ".pool-cleanup-failed";
7359
7780
  var REFS_JSON_FILENAME = "refs.json";
7360
7781
  var REFS_VERSION = 1;
@@ -7370,7 +7791,7 @@ function readPoolRefs(poolDir, fs28) {
7370
7791
  try {
7371
7792
  parsed = JSON.parse(fs28.readFileSync(refsPath));
7372
7793
  } catch (err) {
7373
- logger21.warn(
7794
+ logger22.warn(
7374
7795
  `[pool-manager] refs.json unparsable for ${poolDir}, starting from empty refs: ${toErrorMessage(err)}`
7375
7796
  );
7376
7797
  return emptyRefs();
@@ -7378,7 +7799,7 @@ function readPoolRefs(poolDir, fs28) {
7378
7799
  if (typeof parsed !== "object" || parsed === null) return emptyRefs();
7379
7800
  const obj = parsed;
7380
7801
  if (obj.v !== REFS_VERSION || typeof obj.refs !== "object" || obj.refs === null) {
7381
- logger21.warn(`[pool-manager] refs.json unexpected shape for ${poolDir}, starting from empty refs`);
7802
+ logger22.warn(`[pool-manager] refs.json unexpected shape for ${poolDir}, starting from empty refs`);
7382
7803
  return emptyRefs();
7383
7804
  }
7384
7805
  const refs = {};
@@ -7412,7 +7833,7 @@ function releasePoolRef(dataDir, engineId, poolKey, taskId, fs28 = nodeFs) {
7412
7833
  removeJournalFile(dataDir, engineId, poolKey, taskId, fs28);
7413
7834
  const file = readPoolRefs(poolDir, fs28);
7414
7835
  if (file.refs[taskId] === void 0) {
7415
- logger21.debug(`[pool-manager] release without live ref, skip pool deletion: ${poolDir} (${taskId})`);
7836
+ logger22.debug(`[pool-manager] release without live ref, skip pool deletion: ${poolDir} (${taskId})`);
7416
7837
  return;
7417
7838
  }
7418
7839
  delete file.refs[taskId];
@@ -7505,7 +7926,7 @@ function removeOrphanJournals(poolDir, file, entries, ttlMs, fs28, now) {
7505
7926
  changed = true;
7506
7927
  }
7507
7928
  } catch (err) {
7508
- logger21.debug(
7929
+ logger22.debug(
7509
7930
  `[pool-manager] ttl cleanup stat failed for ${journalPath}: ${toErrorMessage(err)}`
7510
7931
  );
7511
7932
  }
@@ -7520,7 +7941,7 @@ function unlinkBestEffort(path21, fs28) {
7520
7941
  try {
7521
7942
  fs28.rmSync(path21, { force: true, recursive: true });
7522
7943
  } catch (err) {
7523
- logger21.debug(`[pool-manager] ttl cleanup failed for ${path21}: ${toErrorMessage(err)}`);
7944
+ logger22.debug(`[pool-manager] ttl cleanup failed for ${path21}: ${toErrorMessage(err)}`);
7524
7945
  }
7525
7946
  }
7526
7947
  function isJournalFile(name) {
@@ -7561,14 +7982,14 @@ function deletePoolNativeState(poolDir, fs28) {
7561
7982
  function markCleanupFailed(poolDir, failures, fs28) {
7562
7983
  const marker = (0, import_node_path5.join)(poolDir, POOL_CLEANUP_FAILED_MARKER);
7563
7984
  const payload = JSON.stringify({ ts: Date.now(), failures });
7564
- logger21.warn(
7985
+ logger22.warn(
7565
7986
  `[pool-manager] pool cleanup failed for ${poolDir} (${failures.length} item(s)); marker written to ${marker} \u2014 re-run cleanup after fixing the underlying error`
7566
7987
  );
7567
7988
  try {
7568
7989
  fs28.writeFileSync(marker, `${payload}
7569
7990
  `);
7570
7991
  } catch (err) {
7571
- logger21.warn(
7992
+ logger22.warn(
7572
7993
  `[pool-manager] failed to write cleanup-failed marker ${marker}: ${toErrorMessage(err)}`
7573
7994
  );
7574
7995
  }
@@ -7586,7 +8007,7 @@ var nodeFs = {
7586
8007
  };
7587
8008
 
7588
8009
  // src/execution/idle-gc.ts
7589
- var logger22 = getLogger("subagents");
8010
+ var logger23 = getLogger("subagents");
7590
8011
  var GC_INTERVAL_MS = 60 * 60 * 1e3;
7591
8012
  var IDLE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
7592
8013
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
@@ -7597,7 +8018,7 @@ function startIdleGc(store) {
7597
8018
  if (isResumable(record) && record.idleSince) {
7598
8019
  const age = now - record.idleSince;
7599
8020
  if (age > IDLE_TTL_MS) {
7600
- logger22.warn(`[subagents] GC: archiving idle record ${record.id} (idle for ${Math.round(age / MS_PER_DAY)}d)`);
8021
+ logger23.warn(`[subagents] GC: archiving idle record ${record.id} (idle for ${Math.round(age / MS_PER_DAY)}d)`);
7601
8022
  try {
7602
8023
  store.archive(record);
7603
8024
  } catch (err) {
@@ -7680,15 +8101,15 @@ function createBackgroundStream(recordId, sink, mode, env) {
7680
8101
 
7681
8102
  // src/execution/worktree-manager.ts
7682
8103
  var import_node_child_process3 = require("child_process");
7683
- var fs18 = __toESM(require("fs"), 1);
8104
+ var fs19 = __toESM(require("fs"), 1);
7684
8105
  var os2 = __toESM(require("os"), 1);
7685
- var path13 = __toESM(require("path"), 1);
8106
+ var path14 = __toESM(require("path"), 1);
7686
8107
 
7687
8108
  // src/execution/worktree-registry.ts
7688
- var fs17 = __toESM(require("fs"), 1);
7689
- var path12 = __toESM(require("path"), 1);
8109
+ var fs18 = __toESM(require("fs"), 1);
8110
+ var path13 = __toESM(require("path"), 1);
7690
8111
  var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
7691
- var logger23 = getLogger("subagents");
8112
+ var logger24 = getLogger("subagents");
7692
8113
  var SPAWN_GRACE_MS = 6e4;
7693
8114
  var JSON_INDENT2 = 2;
7694
8115
  var LOCK_STALE_MS = 3e4;
@@ -7699,7 +8120,7 @@ function isRegistryData(value) {
7699
8120
  var WorktreeRegistry = class {
7700
8121
  filePath;
7701
8122
  constructor(agentDir) {
7702
- this.filePath = path12.join(agentDir, "subagents", "worktrees.json");
8123
+ this.filePath = path13.join(agentDir, "subagents", "worktrees.json");
7703
8124
  }
7704
8125
  /**
7705
8126
  * 新增条目(create 成功后调,pid=0 占位)。
@@ -7771,7 +8192,7 @@ var WorktreeRegistry = class {
7771
8192
  run();
7772
8193
  });
7773
8194
  } catch (lockErr) {
7774
- logger23.warn("[worktree] registry lock unavailable, degraded to lock-free RMW", {
8195
+ logger24.warn("[worktree] registry lock unavailable, degraded to lock-free RMW", {
7775
8196
  ...context ?? {},
7776
8197
  err: lockErr instanceof Error ? lockErr.message : String(lockErr)
7777
8198
  });
@@ -7796,8 +8217,8 @@ var WorktreeRegistry = class {
7796
8217
  * 锁参数值由 LOCK_STALE_MS / LOCK_RETRIES 常量承载(防漂移说明见常量注释)。
7797
8218
  */
7798
8219
  async withLock(fn) {
7799
- const dir = path12.dirname(this.filePath);
7800
- if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
8220
+ const dir = path13.dirname(this.filePath);
8221
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
7801
8222
  let compromised;
7802
8223
  const release = await import_proper_lockfile.default.lock(this.filePath, {
7803
8224
  realpath: false,
@@ -7820,7 +8241,7 @@ var WorktreeRegistry = class {
7820
8241
  try {
7821
8242
  await release();
7822
8243
  } catch (unlockErr) {
7823
- logger23.debug("unlock failed after compromise (ignorable)", {
8244
+ logger24.debug("unlock failed after compromise (ignorable)", {
7824
8245
  detail: { err: unlockErr instanceof Error ? unlockErr.message : String(unlockErr) }
7825
8246
  });
7826
8247
  }
@@ -7832,7 +8253,7 @@ var WorktreeRegistry = class {
7832
8253
  */
7833
8254
  load() {
7834
8255
  try {
7835
- const raw = fs17.readFileSync(this.filePath, "utf-8");
8256
+ const raw = fs18.readFileSync(this.filePath, "utf-8");
7836
8257
  const parsed = JSON.parse(raw);
7837
8258
  if (isRegistryData(parsed)) {
7838
8259
  return parsed.entries;
@@ -7855,7 +8276,7 @@ var WorktreeRegistry = class {
7855
8276
  writeAtomicFileSync(this.filePath, JSON.stringify({ entries }, null, JSON_INDENT2));
7856
8277
  } catch (err) {
7857
8278
  bestEffort(err, "worktree registry save");
7858
- logger23.warn(
8279
+ logger24.warn(
7859
8280
  "[worktree] registry save failed; pid may stay 0 and be reaped by orphan reaper",
7860
8281
  { ...context ?? {}, err: err instanceof Error ? err.message : String(err) }
7861
8282
  );
@@ -7864,7 +8285,7 @@ var WorktreeRegistry = class {
7864
8285
  };
7865
8286
 
7866
8287
  // src/execution/worktree-manager.ts
7867
- var logger24 = getLogger("subagents");
8288
+ var logger25 = getLogger("subagents");
7868
8289
  var SAFE_ID_RE = /^[\w-]+$/;
7869
8290
  var GIT_TIMEOUT_MS = 3e4;
7870
8291
  var WORKTREE_TMP_ROOT = "pi-subagents";
@@ -7889,13 +8310,13 @@ function isWriteCommand(args) {
7889
8310
  }
7890
8311
  function resolveRepoFromCheckout(checkout) {
7891
8312
  try {
7892
- const raw = fs18.readFileSync(path13.join(checkout, ".git"), "utf-8").trim();
8313
+ const raw = fs19.readFileSync(path14.join(checkout, ".git"), "utf-8").trim();
7893
8314
  if (!raw.startsWith("gitdir:")) return void 0;
7894
8315
  const gitdir = raw.slice("gitdir:".length).trim();
7895
- const worktreesDir = path13.dirname(gitdir);
7896
- if (path13.basename(worktreesDir) !== "worktrees") return void 0;
7897
- const gitRootDir = path13.dirname(worktreesDir);
7898
- return path13.dirname(gitRootDir);
8316
+ const worktreesDir = path14.dirname(gitdir);
8317
+ if (path14.basename(worktreesDir) !== "worktrees") return void 0;
8318
+ const gitRootDir = path14.dirname(worktreesDir);
8319
+ return path14.dirname(gitRootDir);
7899
8320
  } catch {
7900
8321
  return void 0;
7901
8322
  }
@@ -7948,10 +8369,10 @@ ${statusText}`
7948
8369
  if (revR.status === "rejected") throw revR.reason;
7949
8370
  const baseCommit = revR.value.trim();
7950
8371
  const branch = `pi-sub-${recordId}`;
7951
- const worktreePath = path13.join(os2.tmpdir(), "pi-subagents", encodeCwd(mainCwd), branch);
7952
- if (fs18.existsSync(worktreePath)) {
8372
+ const worktreePath = path14.join(os2.tmpdir(), "pi-subagents", encodeCwd(mainCwd), branch);
8373
+ if (fs19.existsSync(worktreePath)) {
7953
8374
  try {
7954
- fs18.rmSync(worktreePath, { recursive: true, force: true });
8375
+ fs19.rmSync(worktreePath, { recursive: true, force: true });
7955
8376
  } catch (cleanErr) {
7956
8377
  bestEffort(cleanErr, "pre-create checkout cleanup");
7957
8378
  }
@@ -7967,10 +8388,10 @@ ${statusText}`
7967
8388
  createdAt: Date.now()
7968
8389
  });
7969
8390
  try {
7970
- const mainNodeModules = path13.join(mainCwd, "node_modules");
7971
- const worktreeNodeModules = path13.join(worktreePath, "node_modules");
7972
- if (fs18.existsSync(mainNodeModules) && !fs18.existsSync(worktreeNodeModules)) {
7973
- fs18.symlinkSync(mainNodeModules, worktreeNodeModules);
8391
+ const mainNodeModules = path14.join(mainCwd, "node_modules");
8392
+ const worktreeNodeModules = path14.join(worktreePath, "node_modules");
8393
+ if (fs19.existsSync(mainNodeModules) && !fs19.existsSync(worktreeNodeModules)) {
8394
+ fs19.symlinkSync(mainNodeModules, worktreeNodeModules);
7974
8395
  }
7975
8396
  return Object.freeze({
7976
8397
  path: worktreePath,
@@ -8058,7 +8479,7 @@ ${statusText}`
8058
8479
  return Object.freeze({ patchFile, failed: false, written: false });
8059
8480
  }
8060
8481
  try {
8061
- fs18.writeFileSync(patchFile, diff, "utf-8");
8482
+ fs19.writeFileSync(patchFile, diff, "utf-8");
8062
8483
  return Object.freeze({ patchFile, failed: false, written: true });
8063
8484
  } catch {
8064
8485
  return Object.freeze({ patchFile, failed: true, written: false });
@@ -8121,9 +8542,9 @@ ${statusText}`
8121
8542
  const branches = branchesByRepo.get(entry.repo);
8122
8543
  if (branches === void 0) continue;
8123
8544
  const branchGone = !branches.has(entry.branch);
8124
- const checkoutGone = !fs18.existsSync(entry.checkout);
8545
+ const checkoutGone = !fs19.existsSync(entry.checkout);
8125
8546
  if (branchGone && checkoutGone) {
8126
- logger24.warn("[worktree] reconcile: registry entry has no physical worktree/branch, removing entry", {
8547
+ logger25.warn("[worktree] reconcile: registry entry has no physical worktree/branch, removing entry", {
8127
8548
  branch: entry.branch,
8128
8549
  repo: entry.repo,
8129
8550
  pid: entry.pid
@@ -8167,14 +8588,14 @@ ${statusText}`
8167
8588
  }
8168
8589
  if (alivePids.length === 1 && list.length === 1) {
8169
8590
  const pt = list[0];
8170
- logger24.warn("[worktree] reconcile: unregistered physical worktree with one alive pid, re-registering (self-heal)", {
8591
+ logger25.warn("[worktree] reconcile: unregistered physical worktree with one alive pid, re-registering (self-heal)", {
8171
8592
  branch: pt.branch,
8172
8593
  checkout: pt.checkout,
8173
8594
  repo: pt.repo,
8174
8595
  pid: alivePids[0]
8175
8596
  });
8176
8597
  await this.registry.add({
8177
- repo: pt.repo ?? path13.dirname(pt.checkout),
8598
+ repo: pt.repo ?? path14.dirname(pt.checkout),
8178
8599
  branch: pt.branch,
8179
8600
  checkout: pt.checkout,
8180
8601
  pid: alivePids[0],
@@ -8190,7 +8611,7 @@ ${statusText}`
8190
8611
  if (cycles < RECONCILE_SKIP_ESCALATION_CYCLES) continue;
8191
8612
  escalated++;
8192
8613
  const repoHint = pt.repo ?? "<main-repo>";
8193
- logger24.warn(
8614
+ logger25.warn(
8194
8615
  `[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}`,
8195
8616
  {
8196
8617
  branch: pt.branch,
@@ -8201,7 +8622,7 @@ ${statusText}`
8201
8622
  );
8202
8623
  }
8203
8624
  if (escalated < list.length) {
8204
- logger24.warn("[worktree] reconcile: unregistered physical worktrees present but alive-pid mapping ambiguous, skipping this cycle", {
8625
+ logger25.warn("[worktree] reconcile: unregistered physical worktrees present but alive-pid mapping ambiguous, skipping this cycle", {
8205
8626
  enc,
8206
8627
  orphans: list.length - escalated,
8207
8628
  alivePids: alivePids.length
@@ -8214,7 +8635,7 @@ ${statusText}`
8214
8635
  for (const pt of list) {
8215
8636
  const age = Date.now() - pt.mtimeMs;
8216
8637
  if (age <= SPAWN_GRACE_MS) continue;
8217
- logger24.warn("[worktree] reconcile: unregistered physical worktree with no alive pid, cleaning up", {
8638
+ logger25.warn("[worktree] reconcile: unregistered physical worktree with no alive pid, cleaning up", {
8218
8639
  branch: pt.branch,
8219
8640
  checkout: pt.checkout,
8220
8641
  repo: pt.repo,
@@ -8230,10 +8651,10 @@ ${statusText}`
8230
8651
  * 推导失败(残缺 checkout)repo=undefined,由调用方按无主残留处置。
8231
8652
  */
8232
8653
  async discoverPhysicalWorktrees() {
8233
- const root = path13.join(os2.tmpdir(), WORKTREE_TMP_ROOT);
8654
+ const root = path14.join(os2.tmpdir(), WORKTREE_TMP_ROOT);
8234
8655
  let encDirs;
8235
8656
  try {
8236
- encDirs = fs18.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
8657
+ encDirs = fs19.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
8237
8658
  } catch {
8238
8659
  return [];
8239
8660
  }
@@ -8241,14 +8662,14 @@ ${statusText}`
8241
8662
  for (const enc of encDirs) {
8242
8663
  let branchDirs;
8243
8664
  try {
8244
- branchDirs = fs18.readdirSync(path13.join(root, enc), { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith(BRANCH_PREFIX)).map((d) => d.name);
8665
+ branchDirs = fs19.readdirSync(path14.join(root, enc), { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith(BRANCH_PREFIX)).map((d) => d.name);
8245
8666
  } catch {
8246
8667
  continue;
8247
8668
  }
8248
8669
  for (const branch of branchDirs) {
8249
- const checkout = path13.join(root, enc, branch);
8670
+ const checkout = path14.join(root, enc, branch);
8250
8671
  try {
8251
- const mtimeMs = fs18.statSync(checkout).mtimeMs;
8672
+ const mtimeMs = fs19.statSync(checkout).mtimeMs;
8252
8673
  result.push({ enc, branch, checkout, repo: resolveRepoFromCheckout(checkout), mtimeMs });
8253
8674
  } catch (err) {
8254
8675
  bestEffort(err, "physical worktree stat (reconcile)");
@@ -8286,17 +8707,17 @@ ${statusText}`
8286
8707
  * 崩溃残留的 .alive(pid 已死)天然过滤掉——这正是「死活判据」的物理面来源。
8287
8708
  */
8288
8709
  collectAlivePids(enc) {
8289
- const sessionsDir = path13.join(this.agentDir, "subagents", enc, "sessions");
8710
+ const sessionsDir = path14.join(this.agentDir, "subagents", enc, "sessions");
8290
8711
  let files;
8291
8712
  try {
8292
- files = fs18.readdirSync(sessionsDir);
8713
+ files = fs19.readdirSync(sessionsDir);
8293
8714
  } catch {
8294
8715
  return [];
8295
8716
  }
8296
8717
  const pids = /* @__PURE__ */ new Set();
8297
8718
  for (const file of files) {
8298
8719
  if (!file.endsWith(".alive")) continue;
8299
- const marker = readAliveMarker(path13.join(sessionsDir, file.slice(0, -".alive".length)));
8720
+ const marker = readAliveMarker(path14.join(sessionsDir, file.slice(0, -".alive".length)));
8300
8721
  if (marker && isProcessAlive(marker.pid)) {
8301
8722
  pids.add(marker.pid);
8302
8723
  }
@@ -8328,8 +8749,8 @@ ${statusText}`
8328
8749
  }
8329
8750
  }
8330
8751
  try {
8331
- if (fs18.existsSync(pt.checkout)) {
8332
- fs18.rmSync(pt.checkout, { recursive: true, force: true });
8752
+ if (fs19.existsSync(pt.checkout)) {
8753
+ fs19.rmSync(pt.checkout, { recursive: true, force: true });
8333
8754
  }
8334
8755
  } catch (err) {
8335
8756
  bestEffort(err, "checkout dir rm (reconcile)");
@@ -8342,7 +8763,7 @@ ${statusText}`
8342
8763
  if (entry.pid === 0) {
8343
8764
  const expired = now - entry.createdAt > SPAWN_GRACE_MS;
8344
8765
  if (expired) {
8345
- logger24.warn(
8766
+ logger25.warn(
8346
8767
  "[worktree] orphan reaper: pid=0 entry exceeded SPAWN_GRACE_MS, treating as orphan",
8347
8768
  { branch: entry.branch, checkout: entry.checkout, createdAt: entry.createdAt, now }
8348
8769
  );
@@ -8418,7 +8839,7 @@ ${statusText}`
8418
8839
  };
8419
8840
 
8420
8841
  // src/execution/subagent-service.ts
8421
- var logger25 = getLogger("subagents");
8842
+ var logger26 = getLogger("subagents");
8422
8843
  var disposedUiRequestStub = () => Promise.resolve({ cancelled: true });
8423
8844
  var PRIORITY_BACKGROUND = 1e3;
8424
8845
  var MS_PER_SECOND6 = 1e3;
@@ -8433,6 +8854,7 @@ var ENV_ROOT_SESSION_ID = "PI_SUBAGENT_ROOT_SESSION_ID";
8433
8854
  var ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID";
8434
8855
  var ENV_DEPTH = "PI_SUBAGENT_DEPTH";
8435
8856
  var ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD";
8857
+ var SETTLED_RESCAN_LIMIT = 8;
8436
8858
  var SubagentService = class {
8437
8859
  pool;
8438
8860
  store;
@@ -8483,12 +8905,29 @@ var SubagentService = class {
8483
8905
  /** [D4-①] 通知簇 host 面(notifyComplete/notifyClosed/pending 注册注销 + notifier
8484
8906
  * 实例封装,原私有通知簇四方法与模块函数的搬移落点——notify-host.ts)。
8485
8907
  * deps 惰性求值(pi/session 级状态运行时可变),行为与原 constructor 内
8486
- * createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。 */
8908
+ * createNotifier(this.piAdapter()) 逐字节等价。session_start revive,shutdown dispose。
8909
+ * [sync-collect 合并] toNotifyRecord/notify/notifyBatch 随批路由需要由 host 导出
8910
+ * (collectCoordinator 闭包与 E9/E1 直发路径消费,见 notify-host.ts 接口注释)。 */
8487
8911
  notifyHost = createNotifyHost({
8488
8912
  getPi: () => this.pi,
8489
8913
  listRunning: () => this.store.listRunning(),
8490
8914
  getIsIdle: () => this.isIdleFn
8491
8915
  });
8916
+ /** collectCoordinator(subagent-sync-collect U2):sync 批缓冲 + 闭合检测 + flush 分流。 */
8917
+ collectCoordinator;
8918
+ /** [E9] dispose 时已转 async 写账的成员 id(revive 后 flushBatch 防御过滤用)。
8919
+ * 背景:dispose 后同进程 revive(/resume /fork /new)时协调器内部缓冲仍持有已转换
8920
+ * 成员快照(协调器无 drain API,U5 领地不含 collect-coordinator.ts)——若后续新
8921
+ * sync 成员触发闭合,陈旧快照会随批重投(新成员集新 hash,账本跨键不拦)→ 双重
8922
+ * 通知。flushBatch 闭包按本集过滤,陈旧成员零重投。id 唯一 per spawn,无误伤面。 */
8923
+ e9ConvertedIds = /* @__PURE__ */ new Set();
8924
+ /** [v2 D4] E1 等待分支的 settled 有界重扫状态(null = 未注册)。disposed 后保持
8925
+ * 非 null——同 session 内不再重复注册(补发完成/达限后 settled 边沿已无事可做,
8926
+ * 单注册即单重扫);initSession(revive)置 null 允许新 session 重新注册。
8927
+ * pi.on 无 off(见 armSettledRescan):旧 handler 闭包捕获旧 state,未 disposed 时
8928
+ * 遇 settled 边沿仍会执行,扫描 this.mainSessionFile 当前值(handler 不绑定注册时
8929
+ * 的文件域);dispose 的惰化处置见 dispose()。 */
8930
+ settledRescanState = null;
8492
8931
  /** [MF#4][MF#2] fork 深度按 async 调用链传递(AsyncLocalStorage),替代共享可变计数器。
8493
8932
  * 主 session=0;fork 进入子 session 期间推进为子深度,供嵌套 fork 经 ALS 读到自身深度作为
8494
8933
  * parentForkDepth。并发 background fork 各自独立调用链,不再互相压低深度值。
@@ -8517,7 +8956,7 @@ var SubagentService = class {
8517
8956
  /** [D4-②] 轮次结算回调(原 buildSessionRunnerContext 内的 onRoundSettled 业务闭包
8518
8957
  * 搬移至 round-settlement.ts;deps 回调闭包惰性求值,session-runner agent_settled 时消费)。 */
8519
8958
  settleRound = createRoundSettler({
8520
- notifyComplete: (record) => this.notifyHost.notifyComplete(record),
8959
+ notifyComplete: (record) => this.collectCoordinator.route(record),
8521
8960
  reportRecordTransition: (record) => this.store.reportRecordTransition(record),
8522
8961
  closeAfterRoundSettled: (record) => this.closeAfterRoundSettled(record)
8523
8962
  });
@@ -8548,6 +8987,11 @@ var SubagentService = class {
8548
8987
  deliverChatMessage: (record, text, interrupt) => this.deliverChatMessage(record, text, interrupt)
8549
8988
  };
8550
8989
  manifestStore;
8990
+ /**
8991
+ * [D6 #7a] records 目录(与 manifestStore 同源同一推导)——屏障失败 warn 带 manifest
8992
+ * 文件路径用(ManifestStore.dir 私有,此处不破封装另存同源值;漂移由构造点同语句保证不发生)。
8993
+ */
8994
+ recordsDir;
8551
8995
  /**
8552
8996
  * [T1/PS-9] subagent sessionDir(getSubagentSessionDir 推导,与 store 同源同一 rootCwd)。
8553
8997
  * 传给 doFinalizeRecord 的 FinalizeDeps.sessionDir——record.sessionFile 缺失时 finalize
@@ -8566,8 +9010,42 @@ var SubagentService = class {
8566
9010
  const sessionsDir = getSubagentSessionDir(this.modelService.getAgentDir(), this.rootCwd);
8567
9011
  const recordsDir = getSubagentRecordsDir(this.modelService.getAgentDir(), this.rootCwd);
8568
9012
  this.sessionsDir = sessionsDir;
9013
+ this.recordsDir = recordsDir;
8569
9014
  this.manifestStore = new ManifestStore(recordsDir);
8570
9015
  this.store = new RecordStore(sessionsDir, this.manifestStore, this.pi ?? void 0);
9016
+ this.collectCoordinator = new CollectCoordinator({
9017
+ notifyAsync: (record) => {
9018
+ const notify = this.notifyHost.toNotifyRecord(record);
9019
+ if (notify) this.notifyHost.notify(notify);
9020
+ },
9021
+ toNotifyRecord: (record) => this.notifyHost.toNotifyRecord(record),
9022
+ // 闭合判定数据源:listAllActive(原始 ExecutionRecord 内存态,携带 collectMode/
9023
+ // batchFinalized 原始值)。[U3 修正] 原接 collectRecords——其经 recordToSubagent
9024
+ // 投影丢 collectMode(U2 披露的投影缺口)→ 真链上闭合判定恒立即闭合、跨轮续累
9025
+ // 失效(真链 trace 实证)。非终态 sync 成员必在内存(archive 只删终态;磁盘重建
9026
+ // 残留属孤儿恢复域),内存视图语义完整。
9027
+ listRecords: (limit) => this.store.listAllActive().slice(0, limit),
9028
+ flushBatch: async (members) => {
9029
+ const live = members.filter((m) => !this.e9ConvertedIds.has(m.id));
9030
+ if (live.length === 0) return;
9031
+ const fulls = [];
9032
+ const fullMissed = [];
9033
+ for (const m of live) {
9034
+ const full = this.store.getFullRecord(m.id);
9035
+ if (full) fulls.push(full);
9036
+ else fullMissed.push(m);
9037
+ }
9038
+ await this.writeSyncBatchManifestBarrier(fulls);
9039
+ const accepted = this.notifyHost.notifyBatch(live, this.getCollectSyncBudget());
9040
+ if (!accepted) return;
9041
+ for (const full of fulls) {
9042
+ this.appendBatchFinalizedEntry(full);
9043
+ }
9044
+ for (const m of fullMissed) {
9045
+ this.appendBatchFinalizedEntry(bufferedMemberFallbackRecord(m, this.sessionRootId ?? void 0));
9046
+ }
9047
+ }
9048
+ });
8571
9049
  registerGlobalObservability(this.uiObservability);
8572
9050
  }
8573
9051
  // ── 生命周期(index.ts 调)──────────────────────────────
@@ -8592,6 +9070,7 @@ var SubagentService = class {
8592
9070
  this.sessionRootId = envRoot ?? init.sessionId;
8593
9071
  this.initExecContextBaseline(envRoot, init.sessionId);
8594
9072
  this._disposed = false;
9073
+ this.settledRescanState = null;
8595
9074
  this.store.revive();
8596
9075
  this.notifyHost.revive();
8597
9076
  this.recoverOrphansIfRootProcess();
@@ -8625,7 +9104,7 @@ var SubagentService = class {
8625
9104
  this.execNesting.setBaseline({ recordId: envSelfRecord, depth: nestingDepth });
8626
9105
  this.execNesting.enterWith({ recordId: envSelfRecord, depth: nestingDepth });
8627
9106
  if (process.env.XYZ_AGENT_DEBUG) {
8628
- logger25.debug(
9107
+ logger26.debug(
8629
9108
  `[subagents] execNesting initialized: recordId=${envSelfRecord} depth=${nestingDepth} rootSessionId=${envRoot ?? sessionId}`
8630
9109
  );
8631
9110
  }
@@ -8649,26 +9128,28 @@ var SubagentService = class {
8649
9128
  if (!isChildProcess) {
8650
9129
  this.recoverOrphanRecords();
8651
9130
  } else if (process.env.XYZ_AGENT_DEBUG) {
8652
- logger25.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
9131
+ logger26.debug("[subagents] child process detected (PI_SUBAGENT_SELF_RECORD_ID set), skipping orphan recovery scan");
8653
9132
  }
8654
9133
  }
8655
9134
  /** 孤儿终态恢复委托(RecordStore.recoverOrphanRecords 的唯一调用入口,维持 store
8656
9135
  * private 封装——与 recoverManifestTmpFiles 同模式;[D4] public 面收窄:唯一调用方
8657
9136
  * 是 initSession,转 private)。判定语义见 store 侧注释。
9137
+ * mainSessionFile 随调用透传(v2 D3 覆写 merge 数据源:主文件末条 entry 的批域
9138
+ * 标记与轮终 result/model;initSession 先赋值后恢复,时序就绪)。
8658
9139
  * 随后跑 entry-born 孤儿恢复(无子文件锚的 register-only record,spawn 窗口期死亡,
8659
9140
  * E2E 实测缺口)——主 session 文件经 getMainSessionFile 注入(构造期可空)。 */
8660
9141
  recoverOrphanRecords() {
8661
9142
  try {
8662
- this.store.recoverOrphanRecords(this.sessionRootId ?? void 0);
9143
+ this.store.recoverOrphanRecords(this.sessionRootId ?? void 0, this.mainSessionFile);
8663
9144
  } catch (err) {
8664
- logger25.warn("[subagents] orphan recovery failed", {
9145
+ logger26.warn("[subagents] orphan recovery failed", {
8665
9146
  reason: toErrorMessage(err)
8666
9147
  });
8667
9148
  }
8668
9149
  try {
8669
9150
  this.store.recoverEntryOnlyOrphans(this.mainSessionFile, this.sessionRootId ?? void 0);
8670
9151
  } catch (err) {
8671
- logger25.warn("[subagents] entry-only orphan recovery failed", {
9152
+ logger26.warn("[subagents] entry-only orphan recovery failed", {
8672
9153
  reason: toErrorMessage(err)
8673
9154
  });
8674
9155
  }
@@ -8766,14 +9247,232 @@ var SubagentService = class {
8766
9247
  * listener 仍然存活。若 pending-notifications 先于本扩展执行 session_shutdown(后注册
8767
9248
  * 先执行的语义下会如此),listener 已注销,unregister 事件被静默丢弃。这是可接受的
8768
9249
  * 退化——进程退出后两侧状态本就不保证一致,下次 session_start 的 crash recovery 会修正。 */
9250
+ /** [U4 deviation #8 接线] collectSync 预算热读(flush 时读值,与 getCollectSyncDefault
9251
+ * 同款访问链 modelService.getGlobalConfig().collectSync)。节缺失/读失败 → undefined
9252
+ * → notifyBatch 落 buildBatchLlmContent 设计默认值(4000/24000,E5 不炸启动)。
9253
+ * sanitizeCollectSync 保证节存在时两字段必有合法正整数。 */
9254
+ getCollectSyncBudget() {
9255
+ const cs = this.collectSyncSection();
9256
+ return cs !== void 0 ? { perItemChars: cs.perItemChars, totalChars: cs.totalChars } : void 0;
9257
+ }
9258
+ /** [E9 专用] batchFinalized 落标 + manifest fire-and-forget 写(设计 §3.1.5 E9)。
9259
+ * 批通知路径(flush/E1)已改走「manifest 屏障 → 写账 → 纯落标」序列(「通知可达
9260
+ * ⇒ 索引就位」的构造性保证,见 flushBatch 闭包 / runSyncCollectRecoveryScan),
9261
+ * 不再经本 helper;仅 E9 转换的成员保持原形态——其走 async 单条通知(全文注入、
9262
+ * 无指针行消费),manifest 无时序要求,落标后 fire-and-forget 补写(list 后手动
9263
+ * 反查的顺带索引)。
9264
+ * 路径 = getFullRecord 冷路径重建 → appendBatchFinalizedEntry 纯落标 → fire
9265
+ * manifest。getFullRecord 不可达(子 session 文件缺失/已 GC)→ 跳过该成员
9266
+ * (详见 flushBatch 闭包注释)。 */
9267
+ markMembersBatchFinalized(memberIds) {
9268
+ for (const id of memberIds) {
9269
+ const full = this.store.getFullRecord(id);
9270
+ if (!full) continue;
9271
+ this.appendBatchFinalizedEntry(full);
9272
+ void this.writeBatchMemberManifest(full).catch((err) => {
9273
+ logger26.debug(
9274
+ `[subagents] batch-finalized manifest write failed (record=${full.id})`,
9275
+ { reason: err instanceof Error ? err.message : String(err) }
9276
+ );
9277
+ });
9278
+ }
9279
+ }
9280
+ /** batchFinalized 落标唯一出口(appendEntry 公共末步,纯落标):显式覆写
9281
+ * collectMode/batchFinalized → reportSubagentRecord。覆写动机:recordToSubagent
9282
+ * 投影已含两字段(U5 修复),但 getFullRecord 冷路径含 sidecar/manifest 重建分支
9283
+ * (非 entry 源),显式赋值防非 entry 源重建时丢标记。
9284
+ *
9285
+ * [v2 D1 断链 1] 落标即「离开批 = 通知已/即将送达 = 指针行即将被消费」——成功
9286
+ * 成员走 SP-5 改道 doFinalizeRoundToIdle(不写 manifest),批路径不补写则
9287
+ * records/<sa-id>.json 永不产生、session-reader 反查 0 命中。manifest 写点已从
9288
+ * 本出口的 fire-and-forget 前移至各调用方:批通知路径(flush/E1)在写账前屏障
9289
+ * await 全部落盘(「通知可达 ⇒ 索引就位」的构造性保证);E9 保持落标后
9290
+ * fire-and-forget(async 单条通知无指针行消费,无时序要求)。
9291
+ * rec 两来源(flush 的 getFullRecord 内存全量 / E1 的 rebuildEntryRecord 重建
9292
+ * 快照)必需字段恒齐备(id/agentName←agent/rootSessionId/createdAt←startedAt),
9293
+ * task/slug/parentRecordId 等可选 undefined 自然缺省。 */
9294
+ appendBatchFinalizedEntry(rec) {
9295
+ this.store.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
9296
+ }
9297
+ /** 批成员 manifest(sa- id → sessionFile 反查索引)写的唯一投影点(D2 字段投影 +
9298
+ * status 如实投影:成功成员此刻 record 实态 running+resumable → "running",后续
9299
+ * message upgrade 走完整 finalize 时 Step 4 原子覆盖为 "closed")。
9300
+ * 返回原始 promise 不吞错——失败语义由调用方定:批通知路径经
9301
+ * writeSyncBatchManifestBarrier 的 allSettled(debug 不阻断写账);E9 经
9302
+ * fire-and-forget catch(debug 不阻断落标)。 */
9303
+ writeBatchMemberManifest(rec) {
9304
+ return this.manifestStore.writeManifest({
9305
+ id: rec.id,
9306
+ rootSessionId: rec.rootSessionId ?? "",
9307
+ parentRecordId: rec.parentRecordId,
9308
+ agentName: rec.agent,
9309
+ status: rec.status,
9310
+ createdAt: rec.startedAt,
9311
+ completedAt: rec.endedAt,
9312
+ sessionFile: rec.sessionFile,
9313
+ task: rec.task,
9314
+ slug: rec.slug,
9315
+ model: rec.model
9316
+ });
9317
+ }
9318
+ /** [时序屏障] 批通知路径(flush/E1)专用:成员 manifest 并行写 + await 全部完成
9319
+ * (allSettled)后才允许写账投递——「通知可达 ⇒ 索引就位」的构造性保证。写失败
9320
+ * 不阻断(best-effort 语义与 doFinalizeRecord Step 4 一致:反查索引缺失只影响指针行
9321
+ * 反查,session-reader 错误文案已指引绝对路径兜底,不构成写账失败);warn 留痕
9322
+ * (D6 #7a / SC-1:屏障失败意味着该成员指针行反查索引缺失,debug 级在排障时不可见)。 */
9323
+ async writeSyncBatchManifestBarrier(recs) {
9324
+ const results = await Promise.allSettled(recs.map((rec) => this.writeBatchMemberManifest(rec)));
9325
+ for (let i = 0; i < results.length; i++) {
9326
+ const result = results[i];
9327
+ if (result.status === "rejected") {
9328
+ logger26.warn(
9329
+ `[subagents] batch-finalized manifest write failed (record=${recs[i].id}, manifest=${this.recordsDir}/${recs[i].id}.json)`,
9330
+ { reason: result.reason instanceof Error ? result.reason.message : String(result.reason) }
9331
+ );
9332
+ }
9333
+ }
9334
+ }
9335
+ /** [E9] dispose 时批未闭合:缓冲中已终态未通知成员逐条转 async 语义写账(放弃攒批)
9336
+ * + 落 batchFinalized 标记(E1 重建扫描据此排除,防双重通知),交由既有 shutdown
9337
+ * flush / resume 重放兑底;仍在跑的成员走现有退出路径(disposeAllRecords 关闭,
9338
+ * 与 async 一致)。写账用 notifier.notify 现有通路(ledger.record + attemptDeliver)。
9339
+ * 源序:写账先于落标——写账后崩溃 → E1 重建收该成员,但 async notifyId 与批 hash
9340
+ * 跨键不拦的重发属设计披露的 E9 残余窗(at-least-once 良性,PS-17 同族,v1 接受)。 */
9341
+ convertPendingSyncBufferToAsync() {
9342
+ this.collectCoordinator.cancelScheduledFlush();
9343
+ const members = this.collectCoordinator.pendingMembers();
9344
+ if (members.length === 0) return;
9345
+ for (const member of members) {
9346
+ this.e9ConvertedIds.add(member.id);
9347
+ this.notifyHost.notify(member);
9348
+ }
9349
+ this.markMembersBatchFinalized(members.map((m) => m.id));
9350
+ logger26.warn(
9351
+ `[subagents] E9 dispose: converted ${members.length} buffered sync member(s) to async notify`,
9352
+ { ids: members.map((m) => m.id) }
9353
+ );
9354
+ }
9355
+ /**
9356
+ * [E1] sync 批崩溃恢复钩子(设计 §3.1.5 E1,index.ts session_start 恢复编排处调用,
9357
+ * 须晚于 initSession——孤儿终态恢复先行收敛 running 成员,「全员终态」判定才可达):
9358
+ *
9359
+ * - 扫描主 session 文件每 id 末条 subagent-record entry(store.scanLastRecordEntries,
9360
+ * collectLastRecordEntries 同构 + 投影扩展含 collectMode/batchFinalized + 终态五
9361
+ * 字段;禁走 collectRecords light 路径——主 session 落标 entry 对它不可见);
9362
+ * - 只收 collectMode=sync 且无 batchFinalized 的成员(排除已通过批 flush 或 E9
9363
+ * 转换离场的,防双重通知),按 rootSessionId 过滤当前根;
9364
+ * - 全员终态且账本无同成员集批记录 → manifest 屏障(await 落盘,「通知可达 ⇒
9365
+ * 索引就位」构造性保证,与 flushBatch 同款)→ notifyBatch 补发(内容 = 末条
9366
+ * entry 终态快照;账本 record 同 hash 幂等拒绝 = 已投递/已在账,两种结局都算
9367
+ * 「已处理」);
9368
+ * - 仍有 running → 本次不动,注册 settled 有界重扫(D4,见 armSettledRescan)——
9369
+ * 成员延迟终态(主 agent 冷路径 resume → 正常流落 entry)由 settled 边沿驱动
9370
+ * 重扫收敛,不再依赖「下次 session_start」作唯一再驱动(v2 §2.4 断链 4);
9371
+ * running 口径与协调器同构
9372
+ * (resumable 豁免,v2 D3——覆写不可达的防御分支残余不被误判「仍在跑」);
9373
+ * - 补发尝试后统一补 batchFinalized 标记(账本拒绝也算已投递;直接用末条重建快照
9374
+ * 落标不经 getFullRecord——子文件缺失/已 GC 时标记仍可落盘,窗口自愈不依赖二次
9375
+ * 重启;补标自身崩溃重入幂等收敛,末条 entry last-writer-wins)。
9376
+ *
9377
+ * async 化(时序屏障修复):返回 Promise 但**内部自捕获不外抛**——宿主 index.ts
9378
+ * session_start 以同步 try/catch 调用(其 catch 兑现不到 promise 内的异常),
9379
+ * 自捕获维持同款 warn 容错语义,浮动调用零适配、不产生 unhandled rejection。
9380
+ *
9381
+ * [U8 拆批修复] 与协调器合批排程无交集:E1 只在 session_start 编排处运行(此前
9382
+ * dispose 已取消挂起排程),补发直走 notifier.notifyBatch 不经协调器;异常时序
9383
+ * 相撞由账本 sync-batch:<hash> 幂等拒绝兜底。
9384
+ */
9385
+ async recoverSyncCollectBatch() {
9386
+ try {
9387
+ const { outcome } = await this.runSyncCollectRecoveryScan();
9388
+ if (outcome === "waiting") {
9389
+ this.armSettledRescan();
9390
+ }
9391
+ } catch (err) {
9392
+ logger26.warn("[subagents] sync collect batch recovery failed", {
9393
+ reason: err instanceof Error ? err.message : String(err)
9394
+ });
9395
+ }
9396
+ }
9397
+ /** [E1/D4] 单次「扫描→判定→可达则补发+落标」,E1 首扫与 settled 重扫共用同一实现
9398
+ * (防两处复制粘贴分岔)。三态返回:idle(无 sync 候选——已全部落标/E9 转换/异根,
9399
+ * 无事可等)/ waiting(仍有 running 成员,本次不动)/ dispatched(全员终态,已补发
9400
+ * +统一落标)。async:补发前有 manifest 屏障 await(见函数头 E1 注释)。 */
9401
+ /** 返回 outcome + waitingIds(达限 warn 需滞留成员 id,D6 #7b——仅 waiting 态非空)。 */
9402
+ async runSyncCollectRecoveryScan() {
9403
+ const lastRecords = this.store.scanLastRecordEntries(this.mainSessionFile);
9404
+ if (lastRecords.length === 0) return { outcome: "idle", waitingIds: [] };
9405
+ const rootFilter = this.sessionRootId;
9406
+ const candidates = lastRecords.filter(
9407
+ (r) => r.collectMode === "sync" && r.batchFinalized !== true && (rootFilter === void 0 || r.rootSessionId === rootFilter)
9408
+ );
9409
+ if (candidates.length === 0) return { outcome: "idle", waitingIds: [] };
9410
+ const running = candidates.filter((r) => r.resumable !== true && r.status !== "closed");
9411
+ if (running.length > 0) {
9412
+ logger26.debug(
9413
+ `[subagents] E1 sync batch recovery: ${running.length} member(s) still running, wait for natural completion`,
9414
+ { ids: running.map((r) => r.id) }
9415
+ );
9416
+ return { outcome: "waiting", waitingIds: running.map((r) => r.id) };
9417
+ }
9418
+ await this.writeSyncBatchManifestBarrier(candidates);
9419
+ const members = candidates.map((r) => syncRebuildToNotifyMember(r));
9420
+ const accepted = this.notifyHost.notifyBatch(members, this.getCollectSyncBudget());
9421
+ for (const rec of candidates) {
9422
+ this.appendBatchFinalizedEntry(rec);
9423
+ }
9424
+ logger26.warn(
9425
+ `[subagents] E1 sync batch recovery: re-notified ${members.length} member(s) (ledger accepted=${accepted})`,
9426
+ { ids: members.map((m) => m.id) }
9427
+ );
9428
+ return { outcome: "dispatched", waitingIds: [] };
9429
+ }
9430
+ /** [v2 D4] 注册 agent_settled 有界重扫(幂等:settledRescanState 非 null 不叠加注册
9431
+ * ——E1 现仅 session_start 单调用点,守卫是防第二入口引入时的注册叠加断言面)。
9432
+ * 每次 settled 边沿重跑同一 E1 扫描(runSyncCollectRecoveryScan):dispatched
9433
+ * (补发+落标完成,scan 的 warn 已留痕)或 idle(候选已被其他通路落标)→ disposed;
9434
+ * 累计 SETTLED_RESCAN_LIMIT 次仍在等 → disposed + warn 留痕(D6 #7b / SC-2:达限
9435
+ * 放弃重扫意味着滞留成员的批通知要等下次 session_start 才收敛,含滞留 id 的 warn
9436
+ * 是唯一线索,debug 级排障不可见;后续事件零处理,下次 session_start 再收敛)。
9437
+ * pi.on 无 off(0.84.4 实装)——disposed 标志包装兑现退订(scheduler extension
9438
+ * index.ts subscribeSettled 同款先例)。P-settled 定谳(0.84.4 dist 实装证据):
9439
+ * pi.on 为 per-extension 列表分发——loader.js `on()` 把 handler push 进
9440
+ * extension.handlers.get(event) 数组(非覆盖),runner.js `emit()` 对全部
9441
+ * extension 的全部 handler 逐一 await;故本注册与 ledger host 经
9442
+ * piAdapter.onAgentSettled 注册的 settled 分发互不干扰,无需降级并入 host 链。 */
9443
+ armSettledRescan() {
9444
+ if (this.settledRescanState !== null) return;
9445
+ const state = { disposed: false, scans: 0 };
9446
+ this.settledRescanState = state;
9447
+ this.pi?.on?.("agent_settled", async () => {
9448
+ if (state.disposed) return;
9449
+ state.scans += 1;
9450
+ const { outcome, waitingIds } = await this.runSyncCollectRecoveryScan();
9451
+ if (outcome === "waiting" && state.scans < SETTLED_RESCAN_LIMIT) return;
9452
+ state.disposed = true;
9453
+ if (outcome === "waiting") {
9454
+ logger26.warn(
9455
+ `[subagents] E1 settled rescan: reached limit (${SETTLED_RESCAN_LIMIT}) with member(s) still running, disposed until next session_start`,
9456
+ { ids: waitingIds }
9457
+ );
9458
+ }
9459
+ });
9460
+ }
9461
+ // [E1 语义对齐 toNotifyRecord] 补发成员映射 syncRebuildToNotifyMember 拆至
9462
+ // sync-rebuild.ts(变化轴:恢复批通知语义):one-shot 成功成员末条恒
9463
+ // running+resumable(SP-5),直通 status 会让恢复批批头「0 finished」且丢
9464
+ // patchFile 的 git-apply 指针——对齐后补发记录为 closed + outcome 物化 +
9465
+ // patchFile 透传。调用点:runSyncCollectRecoveryScan。
8769
9466
  dispose() {
8770
9467
  if (this._disposed) return;
8771
9468
  this._disposed = true;
8772
9469
  this.stopGcTimer();
9470
+ if (this.settledRescanState !== null) this.settledRescanState.disposed = true;
8773
9471
  this.uiRequestHandler = disposedUiRequestStub;
8774
9472
  this.uiObservability.resetMissingHandlerWarnings();
8775
9473
  this.store.abortRunningControllers();
8776
9474
  killAllSpawnedChildren();
9475
+ this.convertPendingSyncBufferToAsync();
8777
9476
  this.disposeAllRecords("parent-shutdown");
8778
9477
  resetAllEpipeFailures();
8779
9478
  this.resumesInFlight.clear();
@@ -8806,7 +9505,7 @@ var SubagentService = class {
8806
9505
  record: item.record
8807
9506
  });
8808
9507
  }
8809
- logger25.warn(
9508
+ logger26.warn(
8810
9509
  `[subagents] shutdown flush blocked by busy main agent: ${pending.length} pending notification(s) persisted to ledger for replay on next session_start`,
8811
9510
  { count: pending.length }
8812
9511
  );
@@ -9036,7 +9735,7 @@ var SubagentService = class {
9036
9735
  */
9037
9736
  onHotPathSettledWatchdogTimeout(record, fire) {
9038
9737
  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)`;
9039
- logger25.warn(
9738
+ logger26.warn(
9040
9739
  `[subagents] settled watchdog (${fire.phase}) fired for ${record.id}: ${windowDesc}, terminating (LC-1 wedge recovery)`
9041
9740
  );
9042
9741
  killRecordChildWithEscalation(record.id, "settled watchdog (hot path)");
@@ -9053,7 +9752,7 @@ var SubagentService = class {
9053
9752
  return;
9054
9753
  }
9055
9754
  const finalize = record.chatMode ? this.finalizeRoundToIdle(record, failedResult) : this.finalizeRecord(record, failedResult, "closed", "gc");
9056
- void finalize.then(() => this.notifyHost.notifyComplete(record)).catch((err) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
9755
+ void finalize.then(() => this.collectCoordinator.route(record)).catch((err) => bestEffort(err, "settled watchdog hot-path finalize", "error"));
9057
9756
  }
9058
9757
  // [T2⑧ / PS-3] 非 EPIPE 热路径写失败后的 idle timer 再武装已随 D2 投递下沉迁移落位:
9059
9758
  // 挂载点在 PiEngine.deliverPrompt 的非 EPIPE catch(engine/engines/pi/pi-engine.ts 的
@@ -9284,6 +9983,21 @@ var SubagentService = class {
9284
9983
  collectRecords(limit, statusFilter = "all") {
9285
9984
  return this.store.collectRecords(limit, statusFilter, this.sessionRootId ?? this.sessionId ?? void 0);
9286
9985
  }
9986
+ /**
9987
+ * collectSync.default 当前生效值(subagent-sync-collect U2,偏差#3 接线:
9988
+ * startHandler 缺省 collect 解析用)。
9989
+ * config 未配/读失败 → DEFAULT_COLLECT_SYNC.default 兜底(E5 不炸启动)。
9990
+ * 新 session 生效语义与 engine 配置一致(globalConfig 由 ModelConfigService
9991
+ * reloadGlobalConfig 刷新)。
9992
+ */
9993
+ /** collectSync 节单读取点(S9,code-simplify):「读节」一处,「投影成 default 或
9994
+ * budget」各自 accessor 负责(getCollectSyncDefault / getCollectSyncBudget)。 */
9995
+ collectSyncSection() {
9996
+ return this.modelService.getGlobalConfig().collectSync;
9997
+ }
9998
+ getCollectSyncDefault() {
9999
+ return this.collectSyncSection()?.default ?? DEFAULT_COLLECT_SYNC.default;
10000
+ }
9287
10001
  /** [perf] 单 record 详情懒加载(全量:eventLog/displayItems/result/turns/tokens)。
9288
10002
  * 内存 running record 直接投影;磁盘 record 全量重建(per-file 缓存,stat 戳校验)。
9289
10003
  * 返回 undefined:id 不存在于内存与磁盘。 */
@@ -9376,6 +10090,10 @@ var SubagentService = class {
9376
10090
  // 从 RunContext 回填;缺省 = pi 投影,存量调用方零感知)
9377
10091
  engine: opts.engine,
9378
10092
  engineFallback: opts.engineFallback,
10093
+ // subagent-sync-collect U2(偏差#4 接线):sync record 落 collectMode——
10094
+ // 协调器路由判据 + startHandler pendingSyncCount 枚举含本条的数据源。
10095
+ // undefined = async(缺省语义,旧记录零迁移)。
10096
+ collectMode: opts.collect === "sync" ? "sync" : void 0,
9379
10097
  controller
9380
10098
  });
9381
10099
  this.store.register(record);
@@ -9463,7 +10181,7 @@ var SubagentService = class {
9463
10181
  try {
9464
10182
  await this.runEngineTask(record, opts, engine, signal);
9465
10183
  if (record.closedReason !== "cancelled") {
9466
- this.notifyHost.notifyComplete(record);
10184
+ this.collectCoordinator.route(record);
9467
10185
  }
9468
10186
  } finally {
9469
10187
  this.pool.release();
@@ -9740,12 +10458,12 @@ var SubagentService = class {
9740
10458
  { taskId: record.id, poolKey: PI_POOL_KEY, signal, stream }
9741
10459
  ).then(() => {
9742
10460
  if (notifyGateAllowsDelivery(record.closedReason)) {
9743
- this.notifyHost.notifyComplete(record);
10461
+ this.collectCoordinator.route(record);
9744
10462
  }
9745
10463
  }).catch((err) => {
9746
10464
  this.chatRoundTickets.delete(record.id);
9747
10465
  if (err instanceof Error) {
9748
- logger25.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
10466
+ logger26.debug(`[subagent] background finalize error (record=${record.id}): ${err.message}`);
9749
10467
  }
9750
10468
  });
9751
10469
  }
@@ -9838,7 +10556,7 @@ var SubagentService = class {
9838
10556
  }
9839
10557
  }
9840
10558
  this.notifyHost.emitPendingUnregister(record.id, "closed");
9841
- this.notifyHost.notifyComplete(record);
10559
+ this.collectCoordinator.route(record);
9842
10560
  return true;
9843
10561
  }
9844
10562
  /**
@@ -10040,9 +10758,9 @@ var ZCODE_APPSERVER_HARVEST_GRACE_MS = 1e3;
10040
10758
 
10041
10759
  // src/execution/engine/engines/zcode/zcode-engine.ts
10042
10760
  var import_node_child_process5 = require("child_process");
10043
- var fs23 = __toESM(require("fs"), 1);
10761
+ var fs24 = __toESM(require("fs"), 1);
10044
10762
  var os4 = __toESM(require("os"), 1);
10045
- var path16 = __toESM(require("path"), 1);
10763
+ var path17 = __toESM(require("path"), 1);
10046
10764
 
10047
10765
  // src/execution/engine/common/schema-emulation.ts
10048
10766
  var import_ajv = __toESM(require("ajv"), 1);
@@ -10182,9 +10900,9 @@ function synthesizeCoarseEvents(response, usage) {
10182
10900
  }
10183
10901
 
10184
10902
  // src/execution/engine/engines/zcode/preparer.ts
10185
- var fs19 = __toESM(require("fs"), 1);
10903
+ var fs20 = __toESM(require("fs"), 1);
10186
10904
  var os3 = __toESM(require("os"), 1);
10187
- var path14 = __toESM(require("path"), 1);
10905
+ var path15 = __toESM(require("path"), 1);
10188
10906
  var ZcodePrepareError = class extends Error {
10189
10907
  code;
10190
10908
  constructor(code, message) {
@@ -10203,7 +10921,7 @@ function readSourceConfig(absPath) {
10203
10921
  const empty = { providers: /* @__PURE__ */ new Map() };
10204
10922
  let raw;
10205
10923
  try {
10206
- raw = fs19.readFileSync(absPath, "utf8");
10924
+ raw = fs20.readFileSync(absPath, "utf8");
10207
10925
  } catch {
10208
10926
  return empty;
10209
10927
  }
@@ -10237,7 +10955,7 @@ function hasApiKey(entry) {
10237
10955
  return typeof key === "string" && key !== "";
10238
10956
  }
10239
10957
  function defaultV2ConfigPath() {
10240
- return path14.join(os3.homedir(), ...ZCODE_V2_CONFIG_PATH_SUFFIX);
10958
+ return path15.join(os3.homedir(), ...ZCODE_V2_CONFIG_PATH_SUFFIX);
10241
10959
  }
10242
10960
  var DEFAULT_PROVIDER_ID = "builtin:bigmodel-coding-plan";
10243
10961
  function defaultProviderForShortName(merged, withKey) {
@@ -10300,8 +11018,8 @@ function listZcodeModels(sources) {
10300
11018
  }
10301
11019
 
10302
11020
  // src/execution/engine/engines/zcode/appserver-launcher.ts
10303
- var fs20 = __toESM(require("fs"), 1);
10304
- var path15 = __toESM(require("path"), 1);
11021
+ var fs21 = __toESM(require("fs"), 1);
11022
+ var path16 = __toESM(require("path"), 1);
10305
11023
  var ZCODE_APPSERVER_LAUNCHER_NAME = "appserver-launcher.cjs";
10306
11024
  var APPSERVER_LAUNCHER_SOURCE = `'use strict';
10307
11025
  // zcode app-server fs \u62E6\u622A wrapper\uFF08\u7531 subagent-core appserver-launcher \u843D\u76D8\u751F\u6210\uFF09
@@ -10407,15 +11125,15 @@ import(CLI_PATH).catch((err) => {
10407
11125
  });
10408
11126
  `;
10409
11127
  function ensureAppServerLauncher(engineDataDir) {
10410
- const dir = path15.join(engineDataDir, "engines", "zcode");
10411
- const file = path15.join(dir, ZCODE_APPSERVER_LAUNCHER_NAME);
10412
- const existing = fs20.existsSync(file) && fs20.statSync(file).isFile() ? fs20.readFileSync(file, "utf8") : void 0;
11128
+ const dir = path16.join(engineDataDir, "engines", "zcode");
11129
+ const file = path16.join(dir, ZCODE_APPSERVER_LAUNCHER_NAME);
11130
+ const existing = fs21.existsSync(file) && fs21.statSync(file).isFile() ? fs21.readFileSync(file, "utf8") : void 0;
10413
11131
  if (existing === APPSERVER_LAUNCHER_SOURCE) return file;
10414
11132
  try {
10415
- fs20.mkdirSync(dir, { recursive: true });
11133
+ fs21.mkdirSync(dir, { recursive: true });
10416
11134
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
10417
- fs20.writeFileSync(tmp, APPSERVER_LAUNCHER_SOURCE);
10418
- fs20.renameSync(tmp, file);
11135
+ fs21.writeFileSync(tmp, APPSERVER_LAUNCHER_SOURCE);
11136
+ fs21.renameSync(tmp, file);
10419
11137
  } catch (err) {
10420
11138
  const reason = toErrorMessage(err);
10421
11139
  throw new Error(
@@ -10426,7 +11144,7 @@ function ensureAppServerLauncher(engineDataDir) {
10426
11144
  }
10427
11145
 
10428
11146
  // src/execution/engine/engines/zcode/reader.ts
10429
- var fs21 = __toESM(require("fs"), 1);
11147
+ var fs22 = __toESM(require("fs"), 1);
10430
11148
  var ZcodeReaderError = class extends Error {
10431
11149
  code;
10432
11150
  /** 原始失败细节(缺文件/表漂移/运行时不支持)。 */
@@ -10625,7 +11343,7 @@ function buildView(db, sessionId) {
10625
11343
  };
10626
11344
  }
10627
11345
  async function readZcodeSessionView(dbPath, sessionId) {
10628
- if (!fs21.existsSync(dbPath)) {
11346
+ if (!fs22.existsSync(dbPath)) {
10629
11347
  throw new ZcodeReaderError(`db \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${dbPath}`);
10630
11348
  }
10631
11349
  const sqliteModuleId = "node:sqlite";
@@ -10659,9 +11377,9 @@ async function readZcodeSessionView(dbPath, sessionId) {
10659
11377
 
10660
11378
  // src/execution/engine/engines/zcode/connection.ts
10661
11379
  var import_node_child_process4 = require("child_process");
10662
- var fs22 = __toESM(require("fs"), 1);
11380
+ var fs23 = __toESM(require("fs"), 1);
10663
11381
  var import_node_path6 = require("path");
10664
- var logger26 = getLogger("subagents");
11382
+ var logger27 = getLogger("subagents");
10665
11383
  var RAW_FRAME_LOG_CHARS = 200;
10666
11384
  var RUNTIME_PREFERENCES = Object.freeze({
10667
11385
  nativeSearchEnhancementsEnabled: true,
@@ -10866,7 +11584,7 @@ var AppServerConnection = class {
10866
11584
  try {
10867
11585
  this.onSpawned(child);
10868
11586
  } catch (err) {
10869
- logger26.warn(`onSpawned \u56DE\u8C03\u5F02\u5E38\uFF08\u5FFD\u7565\uFF09: ${errMessage(err)}`);
11587
+ logger27.warn(`onSpawned \u56DE\u8C03\u5F02\u5E38\uFF08\u5FFD\u7565\uFF09: ${errMessage(err)}`);
10870
11588
  }
10871
11589
  }
10872
11590
  let finalized = false;
@@ -10885,7 +11603,7 @@ var AppServerConnection = class {
10885
11603
  try {
10886
11604
  fn(reason);
10887
11605
  } catch (err2) {
10888
- logger26.warn(`close handler \u5F02\u5E38: ${errMessage(err2)}`);
11606
+ logger27.warn(`close handler \u5F02\u5E38: ${errMessage(err2)}`);
10889
11607
  }
10890
11608
  }
10891
11609
  };
@@ -10925,14 +11643,14 @@ var AppServerConnection = class {
10925
11643
  try {
10926
11644
  frame = JSON.parse(text);
10927
11645
  } catch {
10928
- logger26.warn(`\u65E0\u6CD5\u89E3\u6790\u7684\u534F\u8BAE\u884C\uFF08\u5FFD\u7565\uFF09: ${text.slice(0, RAW_FRAME_LOG_CHARS)}`);
11646
+ logger27.warn(`\u65E0\u6CD5\u89E3\u6790\u7684\u534F\u8BAE\u884C\uFF08\u5FFD\u7565\uFF09: ${text.slice(0, RAW_FRAME_LOG_CHARS)}`);
10929
11647
  return;
10930
11648
  }
10931
11649
  this.handleFrame(frame, text);
10932
11650
  }
10933
11651
  handleFrame(frame, rawText) {
10934
11652
  if (!isRecord2(frame)) {
10935
- logger26.warn(`\u975E\u5BF9\u8C61\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
11653
+ logger27.warn(`\u975E\u5BF9\u8C61\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
10936
11654
  return;
10937
11655
  }
10938
11656
  if (isRecord2(frame.protocol)) this.capturedProtocolInfo = frame.protocol;
@@ -10947,19 +11665,19 @@ var AppServerConnection = class {
10947
11665
  }
10948
11666
  if (frame.id !== void 0 && frame.id !== null) {
10949
11667
  if (typeof frame.id !== "number") {
10950
- logger26.warn(`\u5E94\u7B54 id \u975E\u6570\u5B57\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
11668
+ logger27.warn(`\u5E94\u7B54 id \u975E\u6570\u5B57\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
10951
11669
  return;
10952
11670
  }
10953
11671
  this.settlePending(frame.id, frame);
10954
11672
  return;
10955
11673
  }
10956
11674
  if (isRecord2(frame.protocol)) return;
10957
- logger26.warn(`\u65E0\u6CD5\u5F52\u7C7B\u7684\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
11675
+ logger27.warn(`\u65E0\u6CD5\u5F52\u7C7B\u7684\u534F\u8BAE\u5E27\uFF08\u5FFD\u7565\uFF09: ${rawText.slice(0, RAW_FRAME_LOG_CHARS)}`);
10958
11676
  }
10959
11677
  settlePending(id, frame) {
10960
11678
  const entry = this.pending.get(id);
10961
11679
  if (!entry) {
10962
- logger26.warn(`\u54CD\u5E94\u65E0\u5339\u914D\u8BF7\u6C42 id=${id}\uFF08\u5FFD\u7565\uFF09`);
11680
+ logger27.warn(`\u54CD\u5E94\u65E0\u5339\u914D\u8BF7\u6C42 id=${id}\uFF08\u5FFD\u7565\uFF09`);
10963
11681
  return;
10964
11682
  }
10965
11683
  this.pending.delete(id);
@@ -10985,7 +11703,7 @@ var AppServerConnection = class {
10985
11703
  try {
10986
11704
  fn(params);
10987
11705
  } catch (err) {
10988
- logger26.warn(`push handler \u5F02\u5E38\uFF08${method}\uFF09: ${errMessage(err)}`);
11706
+ logger27.warn(`push handler \u5F02\u5E38\uFF08${method}\uFF09: ${errMessage(err)}`);
10989
11707
  }
10990
11708
  }
10991
11709
  }
@@ -10995,12 +11713,12 @@ var AppServerConnection = class {
10995
11713
  answerReverse(id, method, params) {
10996
11714
  const handler = this.reverseHandlers[method];
10997
11715
  if (!handler) {
10998
- 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`);
11716
+ 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`);
10999
11717
  this.writeFrame({ id, result: {} });
11000
11718
  return;
11001
11719
  }
11002
11720
  Promise.resolve().then(() => handler(params)).then((result) => this.writeFrame({ id, result: result ?? {} })).catch((err) => {
11003
- logger26.warn(`\u53CD\u5411\u8BF7\u6C42 ${method} handler \u5F02\u5E38: ${errMessage(err)}`);
11721
+ logger27.warn(`\u53CD\u5411\u8BF7\u6C42 ${method} handler \u5F02\u5E38: ${errMessage(err)}`);
11004
11722
  this.writeFrame({ id, error: { code: -32e3, message: errMessage(err) } });
11005
11723
  });
11006
11724
  }
@@ -11016,7 +11734,7 @@ var AppServerConnection = class {
11016
11734
  `);
11017
11735
  return true;
11018
11736
  } catch (err) {
11019
- logger26.warn(`\u5199\u5165 app-server \u5931\u8D25: ${errMessage(err)}`);
11737
+ logger27.warn(`\u5199\u5165 app-server \u5931\u8D25: ${errMessage(err)}`);
11020
11738
  return false;
11021
11739
  }
11022
11740
  }
@@ -11025,8 +11743,8 @@ var AppServerConnection = class {
11025
11743
  if (this.stderrStreamFailed) return;
11026
11744
  if (this.stderrStream === null) {
11027
11745
  try {
11028
- fs22.mkdirSync((0, import_node_path6.dirname)(this.stderrLogPath), { recursive: true });
11029
- this.stderrStream = fs22.createWriteStream(this.stderrLogPath, { flags: "a" });
11746
+ fs23.mkdirSync((0, import_node_path6.dirname)(this.stderrLogPath), { recursive: true });
11747
+ this.stderrStream = fs23.createWriteStream(this.stderrLogPath, { flags: "a" });
11030
11748
  this.stderrStream.on("error", () => {
11031
11749
  this.stderrStreamFailed = true;
11032
11750
  });
@@ -11048,8 +11766,8 @@ var AppServerConnection = class {
11048
11766
  };
11049
11767
 
11050
11768
  // src/execution/engine/engines/zcode/session-channel.ts
11051
- var import_node_crypto = require("crypto");
11052
- var logger27 = getLogger("subagents");
11769
+ var import_node_crypto2 = require("crypto");
11770
+ var logger28 = getLogger("subagents");
11053
11771
  var SUBSCRIBE_DELIVERY_KIND = "desktop-continuous";
11054
11772
  var WORKSPACE_KEY_HASH_CHARS = 16;
11055
11773
  var CREATE_REPLY_LOG_CHARS = 300;
@@ -11061,7 +11779,7 @@ function errMessage2(err) {
11061
11779
  return toErrorMessage(err);
11062
11780
  }
11063
11781
  function stableWorkspaceKey(workspacePath) {
11064
- return "ws-" + (0, import_node_crypto.createHash)("sha256").update(workspacePath).digest("hex").slice(0, WORKSPACE_KEY_HASH_CHARS);
11782
+ return "ws-" + (0, import_node_crypto2.createHash)("sha256").update(workspacePath).digest("hex").slice(0, WORKSPACE_KEY_HASH_CHARS);
11065
11783
  }
11066
11784
  function extractCreatedSessionId(created) {
11067
11785
  if (!isRecord3(created)) return void 0;
@@ -11191,7 +11909,7 @@ function resolveTurnTimerMs(parts) {
11191
11909
  source = `env ${parts.envName}=${raw}`;
11192
11910
  } else {
11193
11911
  if (parsed.state === "invalid") {
11194
- logger27.warn(
11912
+ logger28.warn(
11195
11913
  `[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`
11196
11914
  );
11197
11915
  }
@@ -11200,7 +11918,7 @@ function resolveTurnTimerMs(parts) {
11200
11918
  }
11201
11919
  }
11202
11920
  if (value <= 0) {
11203
- logger27.warn(
11921
+ logger28.warn(
11204
11922
  `[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`
11205
11923
  );
11206
11924
  }
@@ -11322,7 +12040,7 @@ var SessionChannel = class {
11322
12040
  { timeoutMs: ZCODE_APPSERVER_TURN_CLOSE_TIMEOUT_MS }
11323
12041
  );
11324
12042
  } catch (err) {
11325
- logger27.warn(
12043
+ logger28.warn(
11326
12044
  `session/close \u5931\u8D25\uFF08\u4F1A\u8BDD ${sessionId}\uFF0Cbest-effort \u5FFD\u7565\uFF09: ${errMessage2(
11327
12045
  err
11328
12046
  )}`
@@ -11523,7 +12241,7 @@ var SessionChannel = class {
11523
12241
  applyStreamDelta(turn, payload) {
11524
12242
  if (typeof payload.delta !== "string" || payload.delta === "") return;
11525
12243
  if (turn.settled) {
11526
- logger27.warn(
12244
+ logger28.warn(
11527
12245
  `\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(
11528
12246
  0,
11529
12247
  DELTA_LOG_CHARS
@@ -11552,7 +12270,7 @@ var SessionChannel = class {
11552
12270
  turn.lastTerminalStatus = status;
11553
12271
  this.recordTerminalError(turn, params);
11554
12272
  if (turn.settled) {
11555
- logger27.warn(
12273
+ logger28.warn(
11556
12274
  `\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`
11557
12275
  );
11558
12276
  return;
@@ -11598,7 +12316,7 @@ var SessionChannel = class {
11598
12316
  }
11599
12317
  );
11600
12318
  } catch (err) {
11601
- logger27.warn(
12319
+ logger28.warn(
11602
12320
  `session/read \u515C\u5E95\u5931\u8D25\uFF08\u4F1A\u8BDD ${sessionId}\uFF0C\u964D\u7EA7\u6536\u5C3E\u5E27/delta \u805A\u5408\uFF09: ${errMessage2(
11603
12321
  err
11604
12322
  )}`
@@ -11609,11 +12327,11 @@ var SessionChannel = class {
11609
12327
  };
11610
12328
 
11611
12329
  // src/execution/engine/engines/zcode/zcode-engine.ts
11612
- var logger28 = getLogger("subagents");
12330
+ var logger29 = getLogger("subagents");
11613
12331
  var PROBE_VERSION_TIMEOUT_MS2 = 15e3;
11614
12332
  var COMMON_THOUGHT_LEVELS = ["low", "high", "max"];
11615
12333
  function hostZcodeDbPath() {
11616
- return path16.join(os4.homedir(), ...ZCODE_HOST_DB_SUFFIX);
12334
+ return path17.join(os4.homedir(), ...ZCODE_HOST_DB_SUFFIX);
11617
12335
  }
11618
12336
  var ZcodeEngine = class {
11619
12337
  id = ZCODE_ENGINE_ID;
@@ -11684,7 +12402,7 @@ var ZcodeEngine = class {
11684
12402
  }
11685
12403
  /** check 1:二进制存在性(isFile 才算——同名目录不是可执行入口)。 */
11686
12404
  probeBinaryCheck(cliPath) {
11687
- const binaryOk = fs23.existsSync(cliPath) && fs23.statSync(cliPath).isFile();
12405
+ const binaryOk = fs24.existsSync(cliPath) && fs24.statSync(cliPath).isFile();
11688
12406
  return {
11689
12407
  name: "binary",
11690
12408
  ok: binaryOk,
@@ -11778,11 +12496,11 @@ var ZcodeEngine = class {
11778
12496
  if (final.kind === "run-failed" && final.transient !== void 0 && ctx.signal?.aborted !== true && !this.disposed) {
11779
12497
  const budget = resolveTransientRetryBudget(explicitTurnBudgetMs(), Date.now() - attemptStartedAt);
11780
12498
  if (budget.state === "depleted") {
11781
- logger28.warn(
12499
+ logger29.warn(
11782
12500
  `[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`
11783
12501
  );
11784
12502
  } else {
11785
- logger28.warn(
12503
+ logger29.warn(
11786
12504
  `[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")
11787
12505
  );
11788
12506
  const retry = await this.attemptAppServerTurn(task, ctx, modelRef, cwd, basePrompt, {
@@ -11936,7 +12654,7 @@ var ZcodeEngine = class {
11936
12654
  delayResolved(ZCODE_APPSERVER_ABORT_GRACE_MS, false)
11937
12655
  ]);
11938
12656
  if (settled) return "settled-in-grace";
11939
- logger28.warn(
12657
+ logger29.warn(
11940
12658
  `[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`
11941
12659
  );
11942
12660
  await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
@@ -11961,19 +12679,19 @@ var ZcodeEngine = class {
11961
12679
  } catch (err) {
11962
12680
  if (entry.escalateOn === "stop-outcome") {
11963
12681
  if (isAppServerRpcError(err)) {
11964
- logger28.debug(
12682
+ logger29.debug(
11965
12683
  `[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`
11966
12684
  );
11967
12685
  return "stop-rejected";
11968
12686
  }
11969
- logger28.warn(
12687
+ logger29.warn(
11970
12688
  `[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`
11971
12689
  );
11972
12690
  await rt.conn.shutdown({ graceMs: ZCODE_KILL_GRACE_MS });
11973
12691
  await this.awaitConnFinalized(rt);
11974
12692
  return "stop-unreachable-killed";
11975
12693
  }
11976
- logger28.debug(
12694
+ logger29.debug(
11977
12695
  `[zcode-engine] session/stop \u5931\u8D25\uFF08${errMessage3(err)}\uFF09\u2014\u2014grace \u540E\u8D70 killChain \u515C\u5E95`
11978
12696
  );
11979
12697
  }
@@ -12004,7 +12722,7 @@ var ZcodeEngine = class {
12004
12722
  cwd: engineDataDir,
12005
12723
  env,
12006
12724
  launcherScript,
12007
- stderrLogPath: path16.join(engineDataDir, "logs", "zcode-appserver-stderr.log")
12725
+ stderrLogPath: path17.join(engineDataDir, "logs", "zcode-appserver-stderr.log")
12008
12726
  });
12009
12727
  const rt = {
12010
12728
  conn,
@@ -12169,15 +12887,15 @@ var ZcodeEngine = class {
12169
12887
  const dbPathRaw = handle.data.sessionRef["dbPath"];
12170
12888
  if (typeof sessionId === "string" && typeof dbPathRaw === "string") {
12171
12889
  let dbPath;
12172
- if (path16.isAbsolute(dbPathRaw)) {
12890
+ if (path17.isAbsolute(dbPathRaw)) {
12173
12891
  if (dbPathRaw === hostZcodeDbPath()) dbPath = dbPathRaw;
12174
12892
  else {
12175
- logger28.warn("[zcode-engine] record dbPath \u975E\u5BBF\u4E3B db \u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u62D2\u7EDD \u2460\u7EA7\u8BFB\u53D6\u964D journal", {
12893
+ logger29.warn("[zcode-engine] record dbPath \u975E\u5BBF\u4E3B db \u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u62D2\u7EDD \u2460\u7EA7\u8BFB\u53D6\u964D journal", {
12176
12894
  dbPath: dbPathRaw
12177
12895
  });
12178
12896
  }
12179
12897
  } else {
12180
- dbPath = path16.join(
12898
+ dbPath = path17.join(
12181
12899
  resolvePoolDir(this.deps.engineDataDir(), ZCODE_ENGINE_ID, handle.data.poolKey),
12182
12900
  dbPathRaw
12183
12901
  );
@@ -12186,7 +12904,7 @@ var ZcodeEngine = class {
12186
12904
  try {
12187
12905
  return await readZcodeSessionView(dbPath, sessionId);
12188
12906
  } catch (err) {
12189
- logger28.warn("[zcode-engine] native session read failed, degrade to journal replay", {
12907
+ logger29.warn("[zcode-engine] native session read failed, degrade to journal replay", {
12190
12908
  dbPath,
12191
12909
  sessionId,
12192
12910
  reason: toErrorMessage(err)
@@ -12212,7 +12930,7 @@ var ZcodeEngine = class {
12212
12930
  const thoughtLevel = task.thinkingLevel?.trim();
12213
12931
  if (thoughtLevel === void 0 || thoughtLevel === "") return;
12214
12932
  if (COMMON_THOUGHT_LEVELS.includes(thoughtLevel)) return;
12215
- logger28.warn(
12933
+ logger29.warn(
12216
12934
  `[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`,
12217
12935
  { taskId: ctx.taskId }
12218
12936
  );
@@ -12230,7 +12948,7 @@ var ZcodeEngine = class {
12230
12948
  if (ctx.ctxModel === void 0) return;
12231
12949
  const requested = task.model?.trim();
12232
12950
  if (requested !== void 0 && requested !== "") return;
12233
- logger28.warn(
12951
+ logger29.warn(
12234
12952
  `[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}`,
12235
12953
  { taskId: ctx.taskId }
12236
12954
  );
@@ -12443,7 +13161,7 @@ async function defaultProbeVersion2(cliPath) {
12443
13161
  );
12444
13162
  });
12445
13163
  } catch (err) {
12446
- logger28.debug(
13164
+ logger29.debug(
12447
13165
  `[zcode-engine] probe version check failed (best-effort continue): ${toErrorMessage(err)}`
12448
13166
  );
12449
13167
  return void 0;
@@ -12560,10 +13278,10 @@ function parseEngineHandle(raw) {
12560
13278
  }
12561
13279
 
12562
13280
  // src/execution/engine/common/session-view-service.ts
12563
- var import_node_crypto2 = require("crypto");
13281
+ var import_node_crypto3 = require("crypto");
12564
13282
  var import_node_os3 = require("os");
12565
13283
  var import_node_path7 = require("path");
12566
- var logger29 = getLogger("subagents");
13284
+ var logger30 = getLogger("subagents");
12567
13285
  var DEFAULT_ENGINE_ID2 = "pi";
12568
13286
  function extractEngineId(record) {
12569
13287
  const engine = record.engine;
@@ -12588,13 +13306,13 @@ async function readZcodeNativeTier(handle, dataDir) {
12588
13306
  const dbPathRaw = handle.sessionRef["dbPath"];
12589
13307
  const sessionId = handle.sessionRef["sessionId"];
12590
13308
  if (typeof dbPathRaw !== "string" || typeof sessionId !== "string") {
12591
- logger29.debug("[session-view-service] zcode tier1 skipped: handle missing dbPath/sessionId");
13309
+ logger30.debug("[session-view-service] zcode tier1 skipped: handle missing dbPath/sessionId");
12592
13310
  return void 0;
12593
13311
  }
12594
13312
  let dbPath;
12595
13313
  if (dbPathRaw.startsWith("/")) {
12596
13314
  if (dbPathRaw !== (0, import_node_path7.resolve)((0, import_node_os3.homedir)(), ...ZCODE_HOST_DB_SUFFIX)) {
12597
- logger29.warn(
13315
+ logger30.warn(
12598
13316
  `[session-view-service] zcode absolute dbPath not host db, reject tier1: ${dbPathRaw}`
12599
13317
  );
12600
13318
  return void 0;
@@ -12604,14 +13322,14 @@ async function readZcodeNativeTier(handle, dataDir) {
12604
13322
  const poolDir = resolvePoolDir(dataDir, ZCODE_ENGINE_ID, handle.poolKey);
12605
13323
  dbPath = (0, import_node_path7.resolve)(poolDir, dbPathRaw);
12606
13324
  if (!isStrictlyUnder(poolDir, dbPath)) {
12607
- logger29.warn(`[session-view-service] zcode dbPath escapes pool dir, reject tier1: ${dbPath}`);
13325
+ logger30.warn(`[session-view-service] zcode dbPath escapes pool dir, reject tier1: ${dbPath}`);
12608
13326
  return void 0;
12609
13327
  }
12610
13328
  }
12611
13329
  try {
12612
13330
  return await readZcodeSessionView(dbPath, sessionId);
12613
13331
  } catch (err) {
12614
- logger29.debug(
13332
+ logger30.debug(
12615
13333
  `[session-view-service] zcode tier1 read failed, degrade to journal tier: ${toErrorMessage(err)}`
12616
13334
  );
12617
13335
  return void 0;
@@ -12620,18 +13338,18 @@ async function readZcodeNativeTier(handle, dataDir) {
12620
13338
  function readJournalTier(record, handle, dataDir) {
12621
13339
  const journalPath = handle.journalPath;
12622
13340
  if (journalPath === void 0) {
12623
- logger29.debug("[session-view-service] tier2 skipped: no journalPath in handle");
13341
+ logger30.debug("[session-view-service] tier2 skipped: no journalPath in handle");
12624
13342
  return void 0;
12625
13343
  }
12626
13344
  if (!isStrictlyUnder(resolveEnginesRoot(dataDir), journalPath)) {
12627
- logger29.warn(
13345
+ logger30.warn(
12628
13346
  `[session-view-service] journalPath escapes engines root, reject tier2: ${journalPath}`
12629
13347
  );
12630
13348
  return void 0;
12631
13349
  }
12632
13350
  const messages = replayEventsToHistory(replayJournal(journalPath), record);
12633
13351
  if (messages === void 0) {
12634
- logger29.debug(
13352
+ logger30.debug(
12635
13353
  `[session-view-service] tier2 journal replay produced no content, degrade to outcome-only (path=${journalPath})`
12636
13354
  );
12637
13355
  }
@@ -12724,7 +13442,7 @@ function turnsToMessages(turns, record) {
12724
13442
  const messages = [];
12725
13443
  if (record.task.length > 0) {
12726
13444
  messages.push({
12727
- id: (0, import_node_crypto2.randomUUID)(),
13445
+ id: (0, import_node_crypto3.randomUUID)(),
12728
13446
  role: "user",
12729
13447
  content: record.task,
12730
13448
  status: "complete",
@@ -12733,11 +13451,11 @@ function turnsToMessages(turns, record) {
12733
13451
  }
12734
13452
  for (const [i, turn] of turns.entries()) {
12735
13453
  messages.push({
12736
- id: (0, import_node_crypto2.randomUUID)(),
13454
+ id: (0, import_node_crypto3.randomUUID)(),
12737
13455
  role: "assistant",
12738
13456
  content: turn.text,
12739
13457
  status: "complete",
12740
- ...turn.thinking.length > 0 ? { thinking: [{ id: (0, import_node_crypto2.randomUUID)(), content: turn.thinking, collapsed: true }] } : {},
13458
+ ...turn.thinking.length > 0 ? { thinking: [{ id: (0, import_node_crypto3.randomUUID)(), content: turn.thinking, collapsed: true }] } : {},
12741
13459
  ...turn.toolCalls.length > 0 ? { toolCalls: turn.toolCalls.map((tc) => toHistoryToolCall(tc, base + i + 1)) } : {},
12742
13460
  ...turn.usage !== void 0 ? { usage: { inputTokens: turn.usage.input, outputTokens: turn.usage.output } } : {},
12743
13461
  timestamp: base + i + 1
@@ -12749,7 +13467,7 @@ function toHistoryToolCall(tc, ts) {
12749
13467
  const output = extractTextFromContent(tc.result?.content);
12750
13468
  const details = tc.result?.details;
12751
13469
  return {
12752
- id: (0, import_node_crypto2.randomUUID)(),
13470
+ id: (0, import_node_crypto3.randomUUID)(),
12753
13471
  toolName: tc.toolName,
12754
13472
  input: tc.args ?? {},
12755
13473
  ...output !== void 0 ? { output } : {},
@@ -12777,7 +13495,7 @@ function outcomeOnlyMessages(record) {
12777
13495
  const messages = [];
12778
13496
  if (record.task.length > 0) {
12779
13497
  messages.push({
12780
- id: (0, import_node_crypto2.randomUUID)(),
13498
+ id: (0, import_node_crypto3.randomUUID)(),
12781
13499
  role: "user",
12782
13500
  content: record.task,
12783
13501
  status: "complete",
@@ -12786,7 +13504,7 @@ function outcomeOnlyMessages(record) {
12786
13504
  }
12787
13505
  const isErrorOutcome = record.result === void 0 && record.error !== void 0;
12788
13506
  messages.push({
12789
- id: (0, import_node_crypto2.randomUUID)(),
13507
+ id: (0, import_node_crypto3.randomUUID)(),
12790
13508
  role: "assistant",
12791
13509
  // result 优先(正常轮终文本);error 次之(失败终态);双缺占位(运行中被杀等)
12792
13510
  content: record.result ?? record.error ?? "(no outcome recorded)",
@@ -12801,14 +13519,14 @@ async function readSubagentHistoryMessages(record, dataDir) {
12801
13519
  if (engineId === DEFAULT_ENGINE_ID2) return [];
12802
13520
  const handle = parseEngineHandle(record.engineHandle);
12803
13521
  if (handle === void 0) {
12804
- logger29.debug(
13522
+ logger30.debug(
12805
13523
  `[session-view-service] engine '${engineId}' record has no engineHandle, degrade to outcome-only (subagentId=${record.subagentId})`
12806
13524
  );
12807
13525
  return outcomeOnlyMessages(record);
12808
13526
  }
12809
13527
  const reader = lookupNativeSessionReader(engineId);
12810
13528
  if (reader === void 0) {
12811
- logger29.debug(
13529
+ logger30.debug(
12812
13530
  `[session-view-service] engine '${engineId}' has no native reader tier, degrade to outcome-only (subagentId=${record.subagentId})`
12813
13531
  );
12814
13532
  return outcomeOnlyMessages(record);
@@ -12825,14 +13543,14 @@ function isStrictlyUnder(parent, child) {
12825
13543
  }
12826
13544
 
12827
13545
  // src/execution/agent-registry.ts
12828
- var path17 = __toESM(require("path"), 1);
13546
+ var path18 = __toESM(require("path"), 1);
12829
13547
 
12830
13548
  // src/shared/resource-discovery.ts
12831
13549
  var fsSync2 = __toESM(require("fs"), 1);
12832
13550
  var import_promises2 = require("fs/promises");
12833
13551
  var import_node_os4 = require("os");
12834
13552
  var import_node_path8 = require("path");
12835
- var logger30 = getLogger("subagents");
13553
+ var logger31 = getLogger("subagents");
12836
13554
  var shadowWarnDedup = /* @__PURE__ */ new Set();
12837
13555
  var MAX_SHADOW_WARN_DEDUP = 1024;
12838
13556
  var MACHINE_SOURCES = /* @__PURE__ */ new Set([
@@ -13155,7 +13873,7 @@ async function discoverResources(config) {
13155
13873
  const msg = `[resource-discovery] duplicate ${config.kind} "${key}" from ${r.source} shadows ${existing.source}`;
13156
13874
  const data = { shadowed: existing.path, kept: r.path };
13157
13875
  if (isMachineSource(existing.source) || isMachineSource(r.source)) {
13158
- logger30.debug(msg, data);
13876
+ logger31.debug(msg, data);
13159
13877
  } else {
13160
13878
  const dedupKey = `${config.kind}|${key}|${existing.path}|${r.path}`;
13161
13879
  if (!shadowWarnDedup.has(dedupKey)) {
@@ -13163,7 +13881,7 @@ async function discoverResources(config) {
13163
13881
  shadowWarnDedup.clear();
13164
13882
  }
13165
13883
  shadowWarnDedup.add(dedupKey);
13166
- logger30.warn(msg, data);
13884
+ logger31.warn(msg, data);
13167
13885
  }
13168
13886
  }
13169
13887
  }
@@ -13804,10 +14522,10 @@ function lintScript(source) {
13804
14522
  }
13805
14523
 
13806
14524
  // src/execution/agent-registry.ts
13807
- var logger31 = getLogger("subagents");
14525
+ var logger32 = getLogger("subagents");
13808
14526
  var FM_DELIM = "---";
13809
14527
  function parseAgentWithMeta(filePath, content) {
13810
- const name = path17.basename(filePath, ".md");
14528
+ const name = path18.basename(filePath, ".md");
13811
14529
  const fm = splitAgentFrontmatter(content);
13812
14530
  if (fm.kind === "none") {
13813
14531
  return { config: { name, systemPrompt: content.trim() }, meta: null };
@@ -13837,7 +14555,7 @@ function resolveLegacyRoutingFallbacks(agentMeta, yamlBlock, filePath) {
13837
14555
  const modelFallback = extractYamlField(yamlBlock, "model");
13838
14556
  const toolsFallback = parseCommaListFallback(extractYamlField(yamlBlock, "tools"));
13839
14557
  if (!agentMeta && /^model:|^tools:/m.test(yamlBlock)) {
13840
- logger31.warn(
14558
+ logger32.warn(
13841
14559
  `[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`
13842
14560
  );
13843
14561
  }
@@ -13875,7 +14593,7 @@ function extractYamlField(yaml, key) {
13875
14593
  return value || void 0;
13876
14594
  }
13877
14595
  function parseAgentProfile(text, filePath) {
13878
- const stem3 = path17.basename(filePath, ".md");
14596
+ const stem3 = path18.basename(filePath, ".md");
13879
14597
  const warnings = [];
13880
14598
  const fm = splitAgentFrontmatter(text);
13881
14599
  if (fm.kind === "none") {
@@ -14013,84 +14731,13 @@ var AgentRegistry = class {
14013
14731
  const { config, meta } = parseAgentWithMeta(filePath, file.content);
14014
14732
  const lintFindings = meta ? lintAgentMeta(meta) : [];
14015
14733
  for (const finding of lintFindings) {
14016
- logger31.warn(`[agent-registry] ${filePath}: ${finding.message}`);
14734
+ logger32.warn(`[agent-registry] ${filePath}: ${finding.message}`);
14017
14735
  }
14018
14736
  this.fileCache.set(filePath, { mtimeMs: file.mtimeMs, config, meta });
14019
14737
  return config;
14020
14738
  }
14021
14739
  };
14022
14740
 
14023
- // src/execution/config.ts
14024
- var fs24 = __toESM(require("fs"), 1);
14025
- var path18 = __toESM(require("path"), 1);
14026
- var logger32 = getLogger("subagents");
14027
- var DEFAULT_CONFIG = {
14028
- version: 1,
14029
- maxConcurrent: 6
14030
- };
14031
- var DEFAULT_MAX_CONCURRENT = 6;
14032
- function getGlobalConfigPath(agentDir) {
14033
- return path18.join(agentDir, "subagents", "config.json");
14034
- }
14035
- function loadGlobalConfig(agentDir) {
14036
- try {
14037
- const raw = fs24.readFileSync(getGlobalConfigPath(agentDir), "utf-8");
14038
- const parsed = JSON.parse(raw);
14039
- return sanitizeParsedConfig(parsed);
14040
- } catch {
14041
- return { ...DEFAULT_CONFIG };
14042
- }
14043
- }
14044
- function readGlobalConfig(agentDir) {
14045
- const configPath = getGlobalConfigPath(agentDir);
14046
- let raw;
14047
- try {
14048
- raw = fs24.readFileSync(configPath, "utf-8");
14049
- } catch (err) {
14050
- if (errnoCodeOf(err) === "ENOENT") {
14051
- return { status: "absent", config: { ...DEFAULT_CONFIG } };
14052
- }
14053
- return readFailure(configPath, err);
14054
- }
14055
- try {
14056
- const parsed = JSON.parse(raw);
14057
- return { status: "ok", config: sanitizeParsedConfig(parsed) };
14058
- } catch (err) {
14059
- return readFailure(configPath, err);
14060
- }
14061
- }
14062
- function readFailure(configPath, err) {
14063
- const reason = err instanceof Error ? err.message : String(err);
14064
- logger32.warn(`[subagents] global config read failed (read-failure) at ${configPath}: ${reason}`);
14065
- return { status: "failed", reason };
14066
- }
14067
- function errnoCodeOf(err) {
14068
- if (typeof err !== "object" || err === null || !("code" in err)) return void 0;
14069
- const code = Reflect.get(err, "code");
14070
- return typeof code === "string" ? code : void 0;
14071
- }
14072
- function sanitizeParsedConfig(parsed) {
14073
- const defaultEngine = sanitizeDefaultEngine(parsed.defaultEngine);
14074
- const engineRouting = sanitizeEngineRouting(parsed.engineRouting);
14075
- return {
14076
- version: parsed.version ?? DEFAULT_CONFIG.version,
14077
- maxConcurrent: sanitizeMaxConcurrent(parsed.maxConcurrent),
14078
- ...defaultEngine !== void 0 ? { defaultEngine } : {},
14079
- ...engineRouting !== void 0 ? { engineRouting } : {}
14080
- };
14081
- }
14082
- function sanitizeMaxConcurrent(value) {
14083
- return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : DEFAULT_MAX_CONCURRENT;
14084
- }
14085
- function sanitizeDefaultEngine(value) {
14086
- return typeof value === "string" && value.trim() !== "" ? value : void 0;
14087
- }
14088
- function sanitizeEngineRouting(value) {
14089
- if (typeof value !== "object" || value === null) return void 0;
14090
- const strict = value.strict;
14091
- return typeof strict === "boolean" ? { strict } : void 0;
14092
- }
14093
-
14094
14741
  // src/execution/model-resolver.ts
14095
14742
  var MODEL_LIST_LIMIT2 = 20;
14096
14743
  function resolveModel(agentConfig, modelRegistry, paramOverride, ctxModel) {
@@ -15040,6 +15687,12 @@ async function startHandler(service, input, signal, ctxModel) {
15040
15687
  `slug is required for action:'start' (top-level field, must not be whitespace-only). Correct: {"action":"start","task":"...","slug":"<kebab-case>"}`
15041
15688
  );
15042
15689
  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".`);
15690
+ const resolvedCollect = input.collect ?? service.getCollectSyncDefault();
15691
+ if (input.conversation === true && resolvedCollect === "sync") {
15692
+ throw new Error(
15693
+ '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).'
15694
+ );
15695
+ }
15043
15696
  const handle = await service.execute({
15044
15697
  task,
15045
15698
  slug,
@@ -15057,10 +15710,25 @@ async function startHandler(service, input, signal, ctxModel) {
15057
15710
  conversation: input.conversation,
15058
15711
  idleTimeoutMs: input.idleTimeoutMs,
15059
15712
  engine: input.engine,
15713
+ // B1(code-simplify 审查发现的行为缺口):config collectSync.default=sync 且调用方
15714
+ // 省略 collect 时,record 本体也要落 sync(设计 §3.1.3「缺省 = config 默认」作用于
15715
+ // record,而非仅回显)——createRecordForMode 只认 opts.collect==="sync",原样透传
15716
+ // input.collect 会让 record 走 async 逐条通知而响应声称已入批。仅 sync 落值:
15717
+ // async/缺省路径传 undefined 语义(旧 record 零迁移)字节不变。
15718
+ collect: resolvedCollect === "sync" ? "sync" : input.collect,
15060
15719
  ctxModel,
15061
15720
  signal
15062
15721
  // background detached 运行,完成由 notify 驱动新 turn。
15063
15722
  });
15723
+ const response = {
15724
+ status: "running",
15725
+ mode: "background",
15726
+ message: BG_MESSAGE,
15727
+ notifyContract: NOTIFY_CONTRACT
15728
+ };
15729
+ if (resolvedCollect === "sync") {
15730
+ response.collect = { mode: "sync", pendingSyncCount: countPendingSyncRecords(service) };
15731
+ }
15064
15732
  return {
15065
15733
  kind: "bg",
15066
15734
  subagentId: handle.subagentId,
@@ -15068,14 +15736,16 @@ async function startHandler(service, input, signal, ctxModel) {
15068
15736
  slug: handle.details.slug,
15069
15737
  // registry 全等回显:record.model 由 resolved(裁决放行条目)拼接,原样透出。
15070
15738
  model: handle.details.model,
15071
- response: {
15072
- status: "running",
15073
- mode: "background",
15074
- message: BG_MESSAGE,
15075
- notifyContract: NOTIFY_CONTRACT
15076
- }
15739
+ response
15077
15740
  };
15078
15741
  }
15742
+ function countPendingSyncRecords(service) {
15743
+ let count = 0;
15744
+ for (const r of service.queries.collectRecords(COLLECT_SCAN_LIMIT, "all")) {
15745
+ if (r.collectMode === "sync" && r.batchFinalized !== true) count += 1;
15746
+ }
15747
+ return count;
15748
+ }
15079
15749
  function listHandler(service, input) {
15080
15750
  const includeFinished = input?.includeFinished === true;
15081
15751
  const rawLimit = input?.limit ?? DEFAULT_LIST_LIMIT;
@@ -18639,7 +19309,7 @@ function boundedPrettySerialize(value, budget) {
18639
19309
  }
18640
19310
 
18641
19311
  // src/index.ts
18642
- var CORE_PACKAGE_VERSION = "0.5.1";
19312
+ var CORE_PACKAGE_VERSION = "0.6.0";
18643
19313
  // Annotate the CommonJS export names for ESM import in node:
18644
19314
  0 && (module.exports = {
18645
19315
  AGENT_REF_EXT,