cc-viewer 1.6.345 → 1.7.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 (142) hide show
  1. package/README.md +14 -2
  2. package/cli.js +116 -37
  3. package/dist/assets/App-DTkacfRW.js +2 -0
  4. package/dist/assets/App-dYPa5-df.css +1 -0
  5. package/dist/assets/{MdxEditorPanel-C8ITEOzT.js → MdxEditorPanel-C5Q6uUyH.js} +1 -1
  6. package/dist/assets/Mobile-MoBp9c8k.js +1 -0
  7. package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
  8. package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
  9. package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
  10. package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
  11. package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
  12. package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
  13. package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
  14. package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
  15. package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
  16. package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
  17. package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
  18. package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
  20. package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
  21. package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
  22. package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
  23. package/dist/assets/clone-BQtvy9Ae.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
  25. package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
  26. package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
  27. package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
  28. package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
  29. package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
  30. package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
  31. package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
  32. package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
  34. package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
  35. package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
  36. package/dist/assets/index-Ac8vnV3v.js +2 -0
  37. package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
  38. package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
  39. package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
  40. package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
  41. package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
  42. package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
  43. package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
  45. package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
  46. package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
  47. package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
  48. package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
  49. package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
  50. package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
  51. package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
  52. package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
  53. package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
  54. package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
  55. package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
  57. package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +2 -0
  58. package/dist/assets/{seqResourceLoaders-B3TSSS6T.css → seqResourceLoaders-CxuD1CYl.css} +1 -1
  59. package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
  60. package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
  61. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
  62. package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
  63. package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
  64. package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
  65. package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
  66. package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
  67. package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
  68. package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
  69. package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
  71. package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
  72. package/dist/index.html +4 -4
  73. package/findcc.js +79 -7
  74. package/package.json +2 -1
  75. package/server/i18n.js +21 -141
  76. package/server/interceptor.js +211 -403
  77. package/server/lib/adapters/dingtalk-adapter.js +16 -2
  78. package/server/lib/adapters/discord-adapter.js +28 -6
  79. package/server/lib/adapters/feishu-adapter.js +50 -4
  80. package/server/lib/adapters/wecom-adapter.js +24 -0
  81. package/server/lib/async-write-queue.js +12 -1
  82. package/server/lib/create_system_prompt.js +13 -7
  83. package/server/lib/delta-reconstructor.js +3 -3
  84. package/server/lib/error-report.js +45 -0
  85. package/server/lib/im-bridge-core.js +71 -4
  86. package/server/lib/im-lock.js +16 -2
  87. package/server/lib/im-log-watcher.js +68 -8
  88. package/server/lib/im-process-manager.js +7 -1
  89. package/server/lib/interceptor-core.js +19 -174
  90. package/server/lib/log-management.js +214 -247
  91. package/server/lib/log-stream.js +59 -108
  92. package/server/lib/log-watcher.js +34 -272
  93. package/server/lib/log-zip.js +148 -0
  94. package/server/lib/model-system-prompts.js +10 -6
  95. package/server/lib/session-boundary.js +266 -0
  96. package/server/lib/session-pin-store.js +10 -19
  97. package/server/lib/spawn-model-resolver.js +109 -0
  98. package/server/lib/stats-worker.js +138 -267
  99. package/server/lib/system-prompt-files.js +7 -3
  100. package/server/lib/system-prompt-render.js +79 -0
  101. package/server/lib/user-prompt-extract.js +250 -0
  102. package/server/lib/v2/adapter.js +1059 -0
  103. package/server/lib/v2/blob-store.js +44 -0
  104. package/server/lib/v2/conversation-store.js +217 -0
  105. package/server/lib/v2/convert-manager.js +79 -0
  106. package/server/lib/v2/convert-worker.js +42 -0
  107. package/server/lib/v2/convert.js +409 -0
  108. package/server/lib/v2/identity.js +204 -0
  109. package/server/lib/v2/journal.js +62 -0
  110. package/server/lib/v2/layout.js +167 -0
  111. package/server/lib/v2/live-feed.js +451 -0
  112. package/server/lib/v2/migrate-prompt.js +54 -0
  113. package/server/lib/v2/replay.js +191 -0
  114. package/server/lib/v2/session-select.js +222 -0
  115. package/server/lib/v2/singleflight.js +104 -0
  116. package/server/lib/v2/v2-writer.js +513 -0
  117. package/server/lib/v2/verify.js +233 -0
  118. package/server/pty-manager.js +115 -36
  119. package/server/routes/dingtalk.js +9 -3
  120. package/server/routes/events.js +30 -130
  121. package/server/routes/files-fs.js +4 -2
  122. package/server/routes/im.js +67 -20
  123. package/server/routes/logs.js +194 -152
  124. package/server/routes/preferences.js +0 -3
  125. package/server/routes/project-meta.js +2 -3
  126. package/server/routes/session-pin.js +7 -8
  127. package/server/routes/workspaces.js +44 -9
  128. package/server/server.js +59 -66
  129. package/server/workspace-registry.js +24 -4
  130. package/dist/assets/App-BjHJ9KJ6.css +0 -1
  131. package/dist/assets/App-CCmR0aki.js +0 -2
  132. package/dist/assets/Mobile-DCHg97f1.js +0 -1
  133. package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
  134. package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
  135. package/dist/assets/clone-md-Mc1-T.js +0 -1
  136. package/dist/assets/index-CfPXhStV.js +0 -2
  137. package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
  138. package/dist/assets/seqResourceLoaders-CAYxnMSH.js +0 -2
  139. package/server/lib/instance-registry.js +0 -57
  140. package/server/lib/jsonl-archive.js +0 -180
  141. package/server/lib/teammate-detect.js +0 -32
  142. package/server/lib/zip-safety.js +0 -68
@@ -12,7 +12,7 @@
12
12
  // 变化时触发——与 findRecentLog 的「最新真实日志」语义对齐,过滤流式临时文件噪声。
13
13
  // - watchImpl / mkdirImpl 可注入,便于确定性单测(无需真实 FS 时序)。
14
14
 
15
- import { watch as fsWatch, mkdirSync, existsSync } from 'node:fs';
15
+ import { watch as fsWatch, mkdirSync, existsSync, readdirSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
 
18
18
  // fs.watch 一次文件写会抖多次(macOS/Linux 尤甚),通常 50~300ms 内合并;选 250ms 平衡实时性与稳定性。
@@ -21,14 +21,17 @@ const DEFAULT_DEBOUNCE_MS = 250;
21
21
  // 杜绝被污染的 id 经 `IM_${id}` 拼进路径造成穿越(与路由正则 /^[a-z0-9_-]+$/ 同集)。
22
22
  const PLATFORM_ID_RE = /^[a-z0-9_-]+$/;
23
23
 
24
- export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl } = {}) {
24
+ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl, readdirImpl } = {}) {
25
25
  const _watch = watchImpl || fsWatch;
26
26
  const _mkdir = mkdirImpl || ((d) => { try { mkdirSync(d, { recursive: true }); } catch { /* ignore */ } });
27
27
  const _exists = existsImpl || existsSync;
28
+ const _readdir = readdirImpl || ((d) => { try { return readdirSync(d); } catch { return []; } });
28
29
  const _onChange = typeof onChange === 'function' ? onChange : () => {};
29
30
  const _getLogDir = typeof getLogDir === 'function' ? getLogDir : () => '';
30
31
 
31
- const watchers = new Map(); // platformId -> { w: FSWatcher, dir: string } —— 连同所属目录登记,便于 LOG_DIR 切换检测
32
+ // platformId -> { w, dir, sessionsW, sidWatchers: Map<sid, FSWatcher> }
33
+ // 连同所属目录登记,便于 LOG_DIR 切换检测。
34
+ const watchers = new Map();
32
35
  const timers = new Map(); // platformId -> timeout
33
36
  let _disposed = false;
34
37
 
@@ -51,6 +54,52 @@ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_D
51
54
  return true;
52
55
  }
53
56
 
57
+ function _closeReg(reg) {
58
+ try { reg.w.close(); } catch { /* ignore */ }
59
+ if (reg.sessionsW) { try { reg.sessionsW.close(); } catch { /* ignore */ } }
60
+ for (const sw of reg.sidWatchers.values()) { try { sw.close(); } catch { /* ignore */ } }
61
+ reg.sidWatchers.clear();
62
+ }
63
+
64
+ // wire-v2: session appends land at IM_<id>/sessions/<sid>/journal.jsonl —
65
+ // two levels below the platform dir, invisible to its non-recursive watch.
66
+ // Per-sid watchers see journal appends (direct children of the sid dir);
67
+ // the sessions/-level watcher sees new sid dirs appear.
68
+ function _watchSid(platformId, reg, sid) {
69
+ if (reg.sidWatchers.has(sid)) return;
70
+ const sidDir = join(reg.dir, 'sessions', sid);
71
+ let sw;
72
+ try {
73
+ sw = _watch(sidDir, (_eventType, filename) => {
74
+ if (_relevant(filename)) _schedule(platformId);
75
+ });
76
+ } catch { return; }
77
+ if (sw && typeof sw.on === 'function') {
78
+ sw.on('error', () => { try { sw.close(); } catch { /* ignore */ } reg.sidWatchers.delete(sid); });
79
+ }
80
+ reg.sidWatchers.set(sid, sw);
81
+ }
82
+
83
+ function _ensureSessionsWatch(platformId, reg) {
84
+ const sessionsDir = join(reg.dir, 'sessions');
85
+ if (!_exists(sessionsDir)) return; // re-tried on the next platform-dir event
86
+ if (!reg.sessionsW) {
87
+ let sw;
88
+ try {
89
+ sw = _watch(sessionsDir, (_eventType, filename) => {
90
+ // New session dir → a fresh conversation exists; watch it and refresh.
91
+ if (filename) _watchSid(platformId, reg, String(filename));
92
+ _schedule(platformId);
93
+ });
94
+ } catch { return; }
95
+ if (sw && typeof sw.on === 'function') {
96
+ sw.on('error', () => { try { sw.close(); } catch { /* ignore */ } reg.sessionsW = null; });
97
+ }
98
+ reg.sessionsW = sw;
99
+ }
100
+ for (const sid of _readdir(sessionsDir)) _watchSid(platformId, reg, sid);
101
+ }
102
+
54
103
  function ensure(platformId) {
55
104
  if (_disposed || !platformId) return;
56
105
  if (!PLATFORM_ID_RE.test(platformId)) return; // 纵深防御:非白名单字符不放行(防 `IM_${id}` 路径穿越)
@@ -62,29 +111,40 @@ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_D
62
111
  // 幂等仅当「目录路径未变且仍存在」时成立;否则关旧重建:
63
112
  // - reg.dir !== dir:LOG_DIR 运行时切换(切项目),旧 watcher 还盯着旧目录,新目录无人监听;
64
113
  // - !exists(dir):目录被删(某些平台 fs.watch 删目录不报 error,留下永不恢复的幽灵监听)。
65
- if (reg.dir === dir && _exists(dir)) return;
66
- try { reg.w.close(); } catch { /* ignore */ }
114
+ if (reg.dir === dir && _exists(dir)) {
115
+ _ensureSessionsWatch(platformId, reg); // sessions/ may have appeared since
116
+ return;
117
+ }
118
+ _closeReg(reg);
67
119
  watchers.delete(platformId);
68
120
  }
69
121
  _mkdir(dir);
70
122
  let w;
123
+ const newReg = { w: null, dir, sessionsW: null, sidWatchers: new Map() };
71
124
  try {
72
125
  w = _watch(dir, (_eventType, filename) => {
126
+ // v2: 'sessions' appearing (or any event) is the moment to arm the
127
+ // deeper watchers; legacy v1 .jsonl writes keep firing directly.
128
+ if (String(filename || '') === 'sessions' || !filename) {
129
+ _ensureSessionsWatch(platformId, newReg);
130
+ }
73
131
  if (_relevant(filename)) _schedule(platformId);
74
132
  });
75
133
  } catch { return; }
76
134
  // watcher 出错(目录被删等)→ 关闭并撤销登记,下次 ensure 可重建。
77
135
  if (w && typeof w.on === 'function') {
78
- w.on('error', () => { try { w.close(); } catch { /* ignore */ } watchers.delete(platformId); });
136
+ w.on('error', () => { _closeReg(newReg); watchers.delete(platformId); });
79
137
  }
80
- watchers.set(platformId, { w, dir });
138
+ newReg.w = w;
139
+ watchers.set(platformId, newReg);
140
+ _ensureSessionsWatch(platformId, newReg);
81
141
  }
82
142
 
83
143
  function dispose() {
84
144
  _disposed = true;
85
145
  for (const t of timers.values()) clearTimeout(t);
86
146
  timers.clear();
87
- for (const reg of watchers.values()) { try { reg.w.close(); } catch { /* ignore */ } }
147
+ for (const reg of watchers.values()) _closeReg(reg);
88
148
  watchers.clear();
89
149
  }
90
150
 
@@ -139,7 +139,7 @@ export async function stopImProcess(id, opts = {}) {
139
139
 
140
140
  /**
141
141
  * 查询某 IM 的进程状态(供 header chip / 路由)。
142
- * @returns {Promise<{state:string, running:boolean, connected:boolean, pid:number|null, port:number|null, startedAt:string|null}>}
142
+ * @returns {Promise<{state:string, running:boolean, connected:boolean, connectionState:string, lastError:string|null, pid:number|null, port:number|null, startedAt:string|null}>}
143
143
  */
144
144
  export async function getImProcessStatus(id, opts = {}) {
145
145
  const live = await getImLiveness(id, opts);
@@ -147,6 +147,12 @@ export async function getImProcessStatus(id, opts = {}) {
147
147
  state: live.state,
148
148
  running: live.state === 'ready' || live.state === 'booting' || live.state === 'hung',
149
149
  connected: live.state === 'ready' && !!live.connected,
150
+ // Tri-state IM link status from the worker's own report; a non-ready process cannot be
151
+ // connected or reconnecting, so it reads as disconnected.
152
+ connectionState: live.state === 'ready'
153
+ ? (live.connectionState || (live.connected ? 'connected' : 'disconnected'))
154
+ : 'disconnected',
155
+ lastError: live.lastError ?? null,
150
156
  pid: live.lock?.pid ?? null,
151
157
  port: live.lock?.port ?? null,
152
158
  startedAt: live.lock?.startedAt ?? null,
@@ -1,6 +1,5 @@
1
- import { appendFileSync, existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { renameSyncWithRetry } from './file-api.js';
3
- import { join, basename } from 'node:path';
1
+ import { readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
4
3
 
5
4
  const SUBAGENT_SYSTEM_RE = /(?:command execution|file search|planning) specialist|general-purpose agent|security monitor|performing a web search/i;
6
5
 
@@ -15,7 +14,9 @@ const SUBAGENT_BILLING_RE = /cc_is_subagent=true\b/;
15
14
  // 两处服务端实现(本文件 + kv-cache-analyzer)由 test/interceptor-core-mainagent.test.js 互校防漂移;
16
15
  // 前端 contentFilter 那份由 test/content-filter-unit.test.js 单测覆盖。
17
16
  const TEAMMATE_SYSTEM_RE = /running as an agent in a team|Agent Teammate Communication/i;
18
- // Exported for server/lib/teammate-detect.js (previous-segment backfill filter).
17
+ // 1.7.0: the last external consumer (teammate-detect.js) retired with the
18
+ // prev-segment backfill; kept unexported-in-spirit for the KEEP-IN-SYNC trio
19
+ // above (tests still cross-check the three copies).
19
20
  export { TEAMMATE_SYSTEM_RE, SUBAGENT_BILLING_RE };
20
21
 
21
22
  // Rotation carry-forward: prompt-prefix → teammate-name pairs extracted from a
@@ -40,20 +41,6 @@ export function extractAgentSpawnPairs(responseBody) {
40
41
  return pairs;
41
42
  }
42
43
 
43
- // Parses the first frame of a log-file head string; returns the entry when it
44
- // is a rotation-context sentinel, else null. Callers pass a BOUNDED head read
45
- // (never a whole segment).
46
- export function parseRotationContextHead(headString) {
47
- try {
48
- const frameEnd = headString.indexOf('\n---\n');
49
- if (frameEnd <= 0) return null;
50
- const entry = JSON.parse(headString.slice(0, frameEnd));
51
- return entry && entry.ccvRotationContext ? entry : null;
52
- } catch {
53
- return null;
54
- }
55
- }
56
-
57
44
  export function getSystemText(body) {
58
45
  const system = body?.system;
59
46
  if (typeof system === 'string') return system;
@@ -298,29 +285,27 @@ export function createStreamAssembler() {
298
285
  };
299
286
  }
300
287
 
301
- // 日志文件名前缀【单一来源】:writer(generateNewLogFilePath / initForWorkspace)与 matcher 共用,
302
- // 防止两侧命名漂移。默认 `<project>_`;`--pid` 实例 `<pid>__<project>_`。完整文件名 = `${prefix}${ts}.jsonl`。
303
- export function logFilePrefix(projectName, instanceId) {
304
- return instanceId ? `${instanceId}__${projectName}_` : `${projectName}_`;
288
+ // Log filename prefix (single source): v1 files were always `<project>_<ts>.jsonl`
289
+ // (`<pid>__`-prefixed variants came from the removed multi-instance feature and
290
+ // are excluded by the matcher below). Kept for the v1 lookups that still exist
291
+ // (migration-era readers, e.g. findRecentLog for IM).
292
+ export function logFilePrefix(projectName) {
293
+ return `${projectName}_`;
305
294
  }
306
295
 
307
- // 日志文件名归属判定(实例隔离)。`--pid` 实例文件名为 `<pid>__<project>_<ts>…`,无 pid `<project>_<ts>…`。
308
- // - instanceId 非空:必须精确以 `<pid>__<project>_` 开头(只认本实例)。
309
- // - instanceId 空(默认):以 `<project>_` 开头【且】不含 pid 分隔特征 `__<project>_`。后一条堵住前缀碰撞——
310
- // 若用户把 --pid 取成项目名或 `<项目>_…`(如项目 proj --pid=proj → `proj__proj_…` 也会 startsWith `proj_`),
311
- // 仅靠前缀无法排除;而任何 pid 文件按构造必含 `__<project>_`,无标签文件绝不含(即便项目名自身带 `__`),据此精确排除。
312
- export function logFileMatcher(projectName, instanceId) {
313
- const p = logFilePrefix(projectName, instanceId);
314
- if (instanceId) return (f) => f.startsWith(p);
296
+ // v1 log filename ownership test: starts with `<project>_` and does NOT carry
297
+ // the legacy `<pid>__<project>_` instance mark (any instance-tagged file
298
+ // contains `__<project>_` by construction; untagged files never do, even when
299
+ // the project name itself contains `__`).
300
+ export function logFileMatcher(projectName) {
301
+ const p = logFilePrefix(projectName);
315
302
  const pidMark = `__${projectName}_`;
316
303
  return (f) => f.startsWith(p) && !f.includes(pidMark);
317
304
  }
318
305
 
319
- // instanceId 为可选第 3 参(默认 null = 现状):非空时只在本实例自己的日志里找(多进程隔离),
320
- // 现有 2 参调用方与单测零改动。
321
- export function findRecentLog(dir, projectName, instanceId = null) {
306
+ export function findRecentLog(dir, projectName) {
322
307
  try {
323
- const owns = logFileMatcher(projectName, instanceId);
308
+ const owns = logFileMatcher(projectName);
324
309
  const files = readdirSync(dir)
325
310
  // 排除 *_temp.jsonl:临时文件是未完成的写入态(resume 流程中途产物),
326
311
  // 不应被当作"最近完整日志"(否则 _temp 因 sort 排在正式文件之后会被误选)。
@@ -333,114 +318,6 @@ export function findRecentLog(dir, projectName, instanceId = null) {
333
318
  return null;
334
319
  }
335
320
 
336
- // 首启接管(仅 --pid 实例):本 pid 还没有自己的日志时,把项目里最近的【无标签】日志原子 rename 成
337
- // `<pid>__<project>_<ts>.jsonl`,并入该 pid 血脉(move 而非 copy:不双计入统计)。返回接管后的新路径,或 null。
338
- //
339
- // 这是 best-effort 便利特性,不是核心隔离(核心隔离=各 pid 读自己的日志,不依赖接管)。为不误抢一个仍存活的
340
- // 无 pid 实例(评审标记的 live-writer / 卡在 resume 的窄边角),双重守卫:① 项目里若存在任何无标签 `*_temp.jsonl`
341
- // (某个无 pid 实例正卡在 resume / 在写)→ 放弃;② 无标签日志 mtime 必须早于 freshnessMs(默认 5min)。仍有理论窄缝
342
- // (进程挂起 >5min 且无 temp),属可接受的便利取舍。并发下原子 rename 只一个赢、输者 throw→吞→null(回退全新)。
343
- export function claimUntaggedLog(dir, projectName, instanceId, { freshnessMs = 300000 } = {}) {
344
- if (!instanceId) return null;
345
- try {
346
- const untagged = findRecentLog(dir, projectName, null); // 最近无标签日志(已排除 pid 文件 + temp)
347
- if (!untagged) return null;
348
- // ① 存在无标签 temp ⇒ 有无 pid 实例正活动/卡在 resume → 不接管。
349
- const owns = logFileMatcher(projectName, null);
350
- const hasUntaggedTemp = readdirSync(dir).some(f => owns(f) && f.endsWith('_temp.jsonl'));
351
- if (hasUntaggedTemp) return null;
352
- let st;
353
- try { st = statSync(untagged); } catch { return null; }
354
- if (Date.now() - st.mtimeMs < freshnessMs) return null; // ② 可能是活动写者 → 不碰
355
- const claimed = join(dir, `${instanceId}__${basename(untagged)}`);
356
- if (existsSync(claimed)) return null; // 极端撞名 → 放弃
357
- renameSyncWithRetry(untagged, claimed);
358
- return claimed;
359
- } catch { return null; }
360
- }
361
-
362
- export function cleanupTempFiles(dir, projectName, instanceId = null) {
363
- try {
364
- const owns = logFileMatcher(projectName, instanceId);
365
- const tempFiles = readdirSync(dir)
366
- .filter(f => owns(f) && f.endsWith('_temp.jsonl'));
367
- for (const f of tempFiles) {
368
- try {
369
- const tempPath = join(dir, f);
370
- const newPath = tempPath.replace('_temp.jsonl', '.jsonl');
371
- if (existsSync(newPath)) {
372
- const tempContent = readFileSync(tempPath, 'utf-8');
373
- if (tempContent.trim()) {
374
- appendFileSync(newPath, tempContent);
375
- }
376
- unlinkSync(tempPath);
377
- } else {
378
- // 只有非空 temp 文件才 rename,空文件直接删除
379
- const sz = statSync(tempPath).size;
380
- if (sz > 0) {
381
- renameSyncWithRetry(tempPath, newPath);
382
- } else {
383
- unlinkSync(tempPath);
384
- }
385
- }
386
- } catch { }
387
- }
388
- } catch { }
389
- }
390
-
391
- export function migrateConversationContext(oldFile, newFile) {
392
- try {
393
- const content = readFileSync(oldFile, 'utf-8');
394
- if (!content.trim()) return;
395
-
396
- const parts = content.split('\n---\n').filter(p => p.trim());
397
- if (parts.length === 0) return;
398
-
399
- let originIndex = -1;
400
- for (let i = parts.length - 1; i >= 0; i--) {
401
- if (!/"mainAgent"\s*:\s*true/.test(parts[i])) continue;
402
- try {
403
- const entry = JSON.parse(parts[i]);
404
- if (entry.mainAgent) {
405
- const msgs = entry.body?.messages;
406
- // Delta storage: 使用 _totalMessageCount(delta 条目)或 msgs.length(旧格式)
407
- const msgCount = entry._totalMessageCount || (Array.isArray(msgs) ? msgs.length : 0);
408
- if (msgCount === 1) {
409
- originIndex = i;
410
- break;
411
- }
412
- }
413
- } catch { }
414
- }
415
-
416
- if (originIndex < 0) return;
417
-
418
- let migrationStart = originIndex;
419
- if (originIndex > 0) {
420
- try {
421
- const prevContent = parts[originIndex - 1];
422
- if (prevContent.trim().startsWith('{')) {
423
- const prev = JSON.parse(prevContent);
424
- if (isPreflightEntry(prev)) {
425
- migrationStart = originIndex - 1;
426
- }
427
- }
428
- } catch { }
429
- }
430
-
431
- const migratedParts = parts.slice(migrationStart);
432
- writeFileSync(newFile, migratedParts.join('\n---\n') + '\n---\n');
433
-
434
- const remainingParts = parts.slice(0, migrationStart);
435
- if (remainingParts.length > 0) {
436
- writeFileSync(oldFile, remainingParts.join('\n---\n') + '\n---\n');
437
- } else {
438
- // 所有内容已迁移到新文件,清空旧文件(不能删除,watcher 需要检测 truncation 来触发轮转)
439
- writeFileSync(oldFile, '');
440
- }
441
- } catch { }
442
- }
443
-
444
321
  /**
445
322
  * 计算单条 message 的轻量身份指纹,用于 delta storage 的 in-place last-msg replace 检测。
446
323
  * 仅服务端 interceptor 使用 —— 触发 Plan C checkpoint 让客户端拿到 wire 真实内容。
@@ -479,38 +356,6 @@ export function fingerprintMsg(m) {
479
356
  return (m.role || '?') + ':' + snip.replace(/\s+/g, ' ').slice(0, 80);
480
357
  }
481
358
 
482
- /**
483
- * Rotate log file when it exceeds maxSize.
484
- * Creates a new file (no content migration) and appends '\n' to old file
485
- * to trigger fs.watchFile callback for watcher migration.
486
- *
487
- * initialContent (optional) is written INTO the new file at creation time —
488
- * used for the rotation-context sentinel. It must be baked into creation
489
- * rather than queued afterwards: the old-file trigger byte below fires the
490
- * watcher's rotation-follow after a short debounce, and a late-flushing
491
- * queued write can land between the new file's initial stream read and its
492
- * lastByteOffset snapshot, in which case it is never delivered to clients.
493
- *
494
- * @param {string} currentFile - current log file path
495
- * @param {string} newFile - new log file path to rotate to
496
- * @param {number} maxSize - max file size in bytes
497
- * @param {string} [initialContent] - content the new file is created with
498
- * @returns {{ rotated: boolean, oldFile?: string, newFile?: string }}
499
- */
500
- export function rotateLogFile(currentFile, newFile, maxSize, initialContent = '') {
501
- try {
502
- if (!existsSync(currentFile)) return { rotated: false };
503
- const size = statSync(currentFile).size;
504
- if (size < maxSize) return { rotated: false };
505
- // 不迁移旧内容,创建新文件(立即创建,避免 watcher 时序窗口)
506
- try { writeFileSync(newFile, initialContent); } catch { }
507
- // 触发旧文件 watcher 回调,使其检测到文件变更并切换到新文件
508
- try { appendFileSync(currentFile, '\n'); } catch { }
509
- return { rotated: true, oldFile: currentFile, newFile };
510
- } catch { }
511
- return { rotated: false };
512
- }
513
-
514
359
  /**
515
360
  * 在原始 JSON 字符串上定向替换顶层 "model" 字段的值,避免对巨型 wire body
516
361
  * (`-c` 重启后的全量 checkpoint 请求可达数十 MB)做二次 JSON.parse + 全量 re-stringify。