cc-viewer 1.6.348 → 1.7.1

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 (222) hide show
  1. package/README.md +13 -1
  2. package/cli.js +116 -37
  3. package/dist/assets/App-gUSmU6Ny.js +2 -0
  4. package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
  5. package/dist/assets/Mobile-qfRsRth0.js +1 -0
  6. package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
  7. package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
  11. package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
  22. package/dist/assets/clone-BQtvy9Ae.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
  33. package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
  34. package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
  35. package/dist/assets/index-B51Ci0GL.js +2 -0
  36. package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
  37. package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
  38. package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
  39. package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
  40. package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
  41. package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
  42. package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
  45. package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
  46. package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
  47. package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
  48. package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
  49. package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
  50. package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
  51. package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
  52. package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
  54. package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
  56. package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
  57. package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
  58. package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
  59. package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
  61. package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
  62. package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
  63. package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
  64. package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
  65. package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
  66. package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
  67. package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
  68. package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
  69. package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
  70. package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
  71. package/dist/index.html +4 -4
  72. package/findcc.js +4 -3
  73. package/package.json +3 -1
  74. package/server/i18n.js +21 -141
  75. package/server/interceptor.js +211 -403
  76. package/server/lib/async-write-queue.js +12 -1
  77. package/server/lib/delta-reconstructor.js +3 -3
  78. package/server/lib/error-report.js +45 -0
  79. package/server/lib/im-log-watcher.js +68 -8
  80. package/server/lib/interceptor-core.js +19 -174
  81. package/server/lib/log-management.js +214 -247
  82. package/server/lib/log-stream.js +59 -108
  83. package/server/lib/log-watcher.js +62 -274
  84. package/server/lib/log-zip.js +148 -0
  85. package/server/lib/session-boundary.js +266 -0
  86. package/server/lib/session-pin-store.js +10 -19
  87. package/server/lib/stats-worker.js +138 -267
  88. package/server/lib/user-prompt-extract.js +250 -0
  89. package/server/lib/v2/adapter.js +1124 -0
  90. package/server/lib/v2/blob-store.js +44 -0
  91. package/server/lib/v2/conversation-store.js +217 -0
  92. package/server/lib/v2/convert-manager.js +79 -0
  93. package/server/lib/v2/convert-worker.js +42 -0
  94. package/server/lib/v2/convert.js +409 -0
  95. package/server/lib/v2/identity.js +204 -0
  96. package/server/lib/v2/journal.js +62 -0
  97. package/server/lib/v2/layout.js +167 -0
  98. package/server/lib/v2/live-feed.js +558 -0
  99. package/server/lib/v2/meta-rows.js +360 -0
  100. package/server/lib/v2/migrate-prompt.js +54 -0
  101. package/server/lib/v2/replay.js +191 -0
  102. package/server/lib/v2/session-select.js +222 -0
  103. package/server/lib/v2/singleflight.js +104 -0
  104. package/server/lib/v2/v2-writer.js +528 -0
  105. package/server/lib/v2/verify.js +233 -0
  106. package/server/lib/wire-compress.js +167 -0
  107. package/server/routes/events.js +120 -150
  108. package/server/routes/files-fs.js +4 -2
  109. package/server/routes/im.js +11 -2
  110. package/server/routes/logs.js +209 -164
  111. package/server/routes/preferences.js +0 -3
  112. package/server/routes/project-meta.js +2 -3
  113. package/server/routes/session-pin.js +7 -8
  114. package/server/routes/v2.js +60 -0
  115. package/server/routes/workspaces.js +50 -14
  116. package/server/server.js +70 -66
  117. package/server/workspace-registry.js +24 -4
  118. package/src/utils/apiUrl.js +26 -0
  119. package/src/utils/askFallback.js +21 -0
  120. package/src/utils/askOptionDesc.js +55 -0
  121. package/src/utils/askPortalMatcher.js +54 -0
  122. package/src/utils/autoApproveOptions.js +27 -0
  123. package/src/utils/avatarAnimationPostPass.js +49 -0
  124. package/src/utils/builtinPresets.js +18 -0
  125. package/src/utils/clearCheckpoint.js +7 -0
  126. package/src/utils/commandValidator.js +45 -0
  127. package/src/utils/contentFilter.js +620 -0
  128. package/src/utils/contextRaw.js +18 -0
  129. package/src/utils/contextSidebarNavigation.js +16 -0
  130. package/src/utils/contextTurns.js +117 -0
  131. package/src/utils/displayScaleHelper.js +47 -0
  132. package/src/utils/effectiveModel.js +13 -0
  133. package/src/utils/entry-slim.js +563 -0
  134. package/src/utils/entryCache.js +144 -0
  135. package/src/utils/errorReport.js +6 -0
  136. package/src/utils/fileExpandedPathsStorage.js +70 -0
  137. package/src/utils/fileIcons.jsx +36 -0
  138. package/src/utils/fileOpen.js +37 -0
  139. package/src/utils/formatters.js +48 -0
  140. package/src/utils/gitApi.js +50 -0
  141. package/src/utils/gitTreeBuilder.js +19 -0
  142. package/src/utils/helpers.js +846 -0
  143. package/src/utils/identityHeal.js +99 -0
  144. package/src/utils/imConnState.js +59 -0
  145. package/src/utils/imOrigin.js +24 -0
  146. package/src/utils/imTr.js +7 -0
  147. package/src/utils/imageCompress.js +83 -0
  148. package/src/utils/imageDownscale.js +28 -0
  149. package/src/utils/imageResize.js +112 -0
  150. package/src/utils/ingestPipeline.js +47 -0
  151. package/src/utils/lazyWithReload.js +53 -0
  152. package/src/utils/markdown.js +30 -0
  153. package/src/utils/markdownIncremental.js +39 -0
  154. package/src/utils/markdownProfiler.js +113 -0
  155. package/src/utils/mdExtensionDetect.js +64 -0
  156. package/src/utils/memoryLinkParser.js +44 -0
  157. package/src/utils/modalMask.js +15 -0
  158. package/src/utils/monotime.js +6 -0
  159. package/src/utils/pinnedMenu.js +55 -0
  160. package/src/utils/presetShortcuts.js +14 -0
  161. package/src/utils/projectAlias.js +135 -0
  162. package/src/utils/promptClassifier.js +112 -0
  163. package/src/utils/promptDetect.js +196 -0
  164. package/src/utils/promptNav.js +60 -0
  165. package/src/utils/ptyChunkBuilder.js +251 -0
  166. package/src/utils/quickMenuHoverIntent.js +35 -0
  167. package/src/utils/rateLimitParser.js +119 -0
  168. package/src/utils/readResultPool.js +106 -0
  169. package/src/utils/refreshCachedItemProp.js +30 -0
  170. package/src/utils/requestType.js +208 -0
  171. package/src/utils/resizeCalc.js +21 -0
  172. package/src/utils/resolveLocalized.js +17 -0
  173. package/src/utils/searchApi.js +47 -0
  174. package/src/utils/searchReplace.js +51 -0
  175. package/src/utils/seqResourceLoaders.js +79 -0
  176. package/src/utils/sessionManager.js +471 -0
  177. package/src/utils/sessionMerge.js +180 -0
  178. package/src/utils/skillModalController.js +111 -0
  179. package/src/utils/skillsParser.js +116 -0
  180. package/src/utils/slashCommandLabels.js +80 -0
  181. package/src/utils/splitDragCalc.js +73 -0
  182. package/src/utils/stickyBottomController.js +643 -0
  183. package/src/utils/svgSanitize.js +48 -0
  184. package/src/utils/systemTags.js +47 -0
  185. package/src/utils/tClaude.js +20 -0
  186. package/src/utils/teamModalBuilder.js +351 -0
  187. package/src/utils/teamSessionParser.js +190 -0
  188. package/src/utils/teammateAvatars.js +194 -0
  189. package/src/utils/teammateDetector.js +121 -0
  190. package/src/utils/termDiag.js +114 -0
  191. package/src/utils/terminalClipboard.js +117 -0
  192. package/src/utils/terminalWriteQueue.js +398 -0
  193. package/src/utils/thinkingMerge.js +49 -0
  194. package/src/utils/toolCatalog.js +29 -0
  195. package/src/utils/toolResultBuilder.js +393 -0
  196. package/src/utils/toolResultClassifier.js +16 -0
  197. package/src/utils/toolResultCore.js +167 -0
  198. package/src/utils/toolsDiff.js +44 -0
  199. package/src/utils/toolsXmlFormatter.js +3 -0
  200. package/src/utils/ultraplanController.js +109 -0
  201. package/src/utils/ultraplanExperts.js +86 -0
  202. package/src/utils/ultraplanTemplates.js +154 -0
  203. package/src/utils/userImageRefs.js +49 -0
  204. package/src/utils/v3Assembler.js +112 -0
  205. package/src/utils/v3Rows.js +48 -0
  206. package/src/utils/voicePackPlayer.js +233 -0
  207. package/src/utils/webSearchGrouping.js +111 -0
  208. package/src/utils/workflowFormat.js +52 -0
  209. package/src/utils/workflowRuns.js +118 -0
  210. package/src/utils/workflowStore.js +94 -0
  211. package/dist/assets/App-VTo4NVdo.js +0 -2
  212. package/dist/assets/Mobile-CcmYvMm_.js +0 -1
  213. package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
  214. package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
  215. package/dist/assets/clone-md-Mc1-T.js +0 -1
  216. package/dist/assets/index-DCo650PG.js +0 -2
  217. package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
  218. package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
  219. package/server/lib/instance-registry.js +0 -57
  220. package/server/lib/jsonl-archive.js +0 -180
  221. package/server/lib/teammate-detect.js +0 -32
  222. package/server/lib/zip-safety.js +0 -68
@@ -36,8 +36,19 @@ export class AsyncWriteQueue {
36
36
  get pendingBytes() { return this._pendingBytes; }
37
37
 
38
38
  append(data, onDone) {
39
+ return this.appendTo(this._getPath(), data, onDone);
40
+ }
41
+
42
+ /**
43
+ * Explicit-path variant (wire-v2 S2): same queue, same ordering/drain
44
+ * machinery, caller supplies the target file. Within one drain batch, path
45
+ * groups are written in first-enqueue order, so a caller enqueuing
46
+ * conversation lines before the journal line keeps that relative order on
47
+ * disk (WIRE_FORMAT_V2.md §1.3 write-order protocol). Empty/falsy path stays
48
+ * a silent no-op — workspace mode and leaderless teammates rely on it.
49
+ */
50
+ appendTo(path, data, onDone) {
39
51
  if (this._closed) return;
40
- const path = this._getPath();
41
52
  if (!path) {
42
53
  if (onDone) try { onDone(); } catch {}
43
54
  return;
@@ -88,7 +88,7 @@ function _seqGuardCheck(entry, st) {
88
88
  /**
89
89
  * 标记乱序条目并尽量就地补偿:accumulated 已包含更新的真值时,用其前缀
90
90
  * (= 截至该条目声称长度的最新内容)回填 body.messages,避免裸 delta 切片
91
- * 残留在请求详情面板 / mergeLogFiles 落盘产物中。
91
+ * 残留在请求详情面板中。
92
92
  * 返回 true 表示已补偿(messages 为一致全量),false 表示需后续 checkpoint 补偿。
93
93
  */
94
94
  function _markStaleEntry(entry, accumulated) {
@@ -142,7 +142,7 @@ function _integrityCheck(entry, accumulated, st) {
142
142
  * 2. 真值前缀 —— stale 就地补偿、超长 slice 修复(_staleReorder 置位,merge 跳过);
143
143
  * 3. 裸 delta 切片待补偿 —— stale 就地补偿失败 / poisoned 冻结(_staleReorder 或
144
144
  * _reconstructBroken 置位):由第二遍 checkpoint 补偿回填,无第二遍的路径靠
145
- * isMergeBlockedEntry 阻断 merge、mergeLogFiles 丢弃兜底。
145
+ * isMergeBlockedEntry 阻断 merge 兜底。
146
146
  *
147
147
  * @param {object} entry - 单条日志条目(原地修改)
148
148
  * @param {{accumulated: Array, intState: {baselineSeen: boolean, poisoned: boolean},
@@ -239,7 +239,7 @@ export function reconstructEntries(entries) {
239
239
 
240
240
  /**
241
241
  * 用候选全量条目(checkpoint/旧格式,排除 teammate)补偿一条断裂条目。
242
- * 内容已是真值前缀,清除标记让批量 merge / mergeLogFiles 正常消费。
242
+ * 内容已是真值前缀,清除标记让批量 merge 正常消费。
243
243
  * @returns {boolean} true = 已补偿
244
244
  */
245
245
  function _tryRepairFromCandidate(brokenEntry, expectedCount, candidate) {
@@ -0,0 +1,45 @@
1
+ // CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
2
+ /**
3
+ * Error-reporting convention for swallowed catch blocks.
4
+ *
5
+ * Canonical home since wire-v2 S2 (moved verbatim from src/utils/errorReport.js,
6
+ * which remains as a re-export shell): server/lib/v2/* writers follow the same
7
+ * convention, and src/ is not shipped in the npm package.
8
+ *
9
+ * The rule (see CLAUDE.md): a `catch` whose failure has diagnostic value —
10
+ * anything where a throw means lost data or broken UI (SSE/WS event parsing,
11
+ * entry ingest, state updates, fetch response handling) — must call
12
+ * `reportSwallowed(tag, err)` instead of swallowing silently. Purely-benign
13
+ * best-effort guards (localStorage get/set, tabBridge IPC, revokeObjectURL,
14
+ * listener-cleanup `off()`) stay bare `catch {}`.
15
+ *
16
+ * Tag grammar: `<channel>.<event-name-verbatim>`, e.g. `sse.load_chunk`,
17
+ * `sse.stream-progress`, `ws.terminal-msg`. Keep the event name exactly as it
18
+ * appears in the code/protocol so a grep for the tag round-trips to the site.
19
+ * All reports share the greppable `[ccv:` console prefix.
20
+ *
21
+ * Dedup: identical failures usually repeat (e.g. every SSE event of a changed
22
+ * payload shape fails the same way), so each tag logs at most
23
+ * MAX_REPORTS_PER_TAG occurrences per session, then one suppression notice,
24
+ * then goes silent. Counter-based on purpose — no Date/timers, so this module
25
+ * is statically loadable and deterministic under `node --test`.
26
+ */
27
+ export const MAX_REPORTS_PER_TAG = 5;
28
+
29
+ const _counts = new Map();
30
+
31
+ export function reportSwallowed(tag, err, extra) {
32
+ const n = (_counts.get(tag) || 0) + 1;
33
+ _counts.set(tag, n);
34
+ if (n > MAX_REPORTS_PER_TAG + 1) return;
35
+ if (n === MAX_REPORTS_PER_TAG + 1) {
36
+ console.warn(`[ccv:${tag}] further occurrences suppressed`);
37
+ return;
38
+ }
39
+ if (extra !== undefined) console.warn(`[ccv:${tag}]`, err, extra);
40
+ else console.warn(`[ccv:${tag}]`, err);
41
+ }
42
+
43
+ export function _resetForTest() {
44
+ _counts.clear();
45
+ }
@@ -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
 
@@ -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。