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
@@ -13,7 +13,7 @@
13
13
  import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
14
14
  import { open as fsOpen, stat as fsStat } from 'node:fs/promises';
15
15
  import { isCheckpointEntry, isDeltaEntry, reconstructSegment } from './delta-reconstructor.js';
16
- import { resolveJsonlPath } from './jsonl-archive.js';
16
+ import { isV2SessionDir, iterateV2RawEntries, iterateV2RawEntriesAsync, readV2WindowedEntries, streamV2WindowedEntries } from './v2/adapter.js';
17
17
 
18
18
  const READ_CHUNK_SIZE = 1024 * 1024; // 1MB
19
19
  const SEPARATOR = '\n---\n';
@@ -23,7 +23,12 @@ const SEPARATOR = '\n---\n';
23
23
  * 内存 = 1MB buffer + pending。
24
24
  */
25
25
  function* iterateRawEntries(filePath) {
26
- filePath = resolveJsonlPath(filePath);
26
+ // wire-v2 S5: a session DIRECTORY resolves to the v2→v1 adapter stream — the
27
+ // synthesized entries feed every downstream consumer unchanged (spec §11).
28
+ if (isV2SessionDir(filePath)) {
29
+ yield* iterateV2RawEntries(filePath);
30
+ return;
31
+ }
27
32
  const fileSize = statSync(filePath).size;
28
33
  if (fileSize === 0) return;
29
34
 
@@ -64,8 +69,17 @@ function* iterateRawEntries(filePath) {
64
69
  * @param {string} filePath
65
70
  * @param {{ startOffset?: number }} [opts] - startOffset>0 时从该偏移量开始读取并跳过首条(可能被截断)
66
71
  */
67
- async function* iterateRawEntriesAsync(filePath, { startOffset = 0 } = {}) {
68
- filePath = resolveJsonlPath(filePath);
72
+ // Exported since wire-v2 S4: the verify tool needs a SINGLE streaming pass —
73
+ // streamRawEntriesAsync's dedup map (~half the file resident) plus its second
74
+ // send pass are pure waste for a scan-only consumer (review P2).
75
+ export async function* iterateRawEntriesAsync(filePath, { startOffset = 0 } = {}) {
76
+ // wire-v2 S5: adapter branch. startOffset is a BYTE offset into a physical
77
+ // file — meaningless for a synthesized stream, so v2 always reads in full
78
+ // (readTailEntries routes v2 through its full-read path for the same reason).
79
+ if (isV2SessionDir(filePath)) {
80
+ yield* iterateV2RawEntriesAsync(filePath);
81
+ return;
82
+ }
69
83
  let fh;
70
84
  try {
71
85
  const st = await fsStat(filePath);
@@ -109,7 +123,6 @@ async function* iterateRawEntriesAsync(filePath, { startOffset = 0 } = {}) {
109
123
  * 用于 SSE load_start 的 total 字段(进度显示)。
110
124
  */
111
125
  export async function countLogEntries(filePath) {
112
- filePath = resolveJsonlPath(filePath);
113
126
  if (!existsSync(filePath)) return 0;
114
127
  let count = 0;
115
128
  for await (const _ of iterateRawEntriesAsync(filePath)) { count++; }
@@ -149,11 +162,10 @@ function isSegmentBoundary(entry) {
149
162
  }
150
163
 
151
164
  // ============================================================================
152
- // 同步 API — 用于 mergeLogFiles(合并需要重建为全量格式写入磁盘)
165
+ // 同步 API — 重建为全量格式的同步变体(当前无生产消费者,保留给测试与未来批处理;退役评估归 wire-v2 S6c)
153
166
  // ============================================================================
154
167
 
155
168
  export function streamReconstructedEntries(filePath, onSegment, opts = {}) {
156
- filePath = resolveJsonlPath(filePath);
157
169
  if (!existsSync(filePath)) return 0;
158
170
  const stat = statSync(filePath);
159
171
  if (stat.size === 0) return 0;
@@ -208,7 +220,6 @@ export function streamReconstructedEntries(filePath, onSegment, opts = {}) {
208
220
  }
209
221
 
210
222
  export async function streamReconstructedEntriesAsync(filePath, onSegment, opts = {}) {
211
- filePath = resolveJsonlPath(filePath);
212
223
  if (!existsSync(filePath)) return 0;
213
224
  try {
214
225
  const st = await fsStat(filePath);
@@ -289,16 +300,44 @@ export async function streamReconstructedEntriesAsync(filePath, onSegment, opts
289
300
  * @returns {Promise<{sentCount: number, totalCount: number}>}
290
301
  */
291
302
  export async function streamRawEntriesAsync(filePath, onRawEntry, opts = {}) {
292
- filePath = resolveJsonlPath(filePath);
293
303
  const empty = { sentCount: 0, totalCount: 0 };
294
304
  if (!existsSync(filePath)) { if (opts.onReady) opts.onReady({ totalCount: 0 }); return empty; }
295
- const stat = statSync(filePath);
296
- if (stat.size === 0) { if (opts.onReady) opts.onReady({ totalCount: 0 }); return empty; }
305
+ // v2 session dirs skip the size probe: a directory's st.size is 0 on some
306
+ // filesystems (Windows) and never reflects the synthesized stream anyway.
307
+ if (!isV2SessionDir(filePath)) {
308
+ const stat = statSync(filePath);
309
+ if (stat.size === 0) { if (opts.onReady) opts.onReady({ totalCount: 0 }); return empty; }
310
+ }
297
311
 
298
312
  const sinceFilter = opts.since || null;
299
313
  const onScan = opts.onScan || null;
300
314
  const onReady = opts.onReady || null;
301
315
 
316
+ // v2 windowed path: the adapter guarantees a reconstructable baseline at the
317
+ // window start (synthesized checkpoint), which the byte-tail heuristics below
318
+ // cannot — expand-to-checkpoint over adapter output could walk arbitrarily
319
+ // far back on a session with sparse organic snapshots.
320
+ // S10a: streaming two-pass — `since` is pushed into window membership so the
321
+ // /events incremental reconnect path materializes only the entries it will
322
+ // send (the historical full materialization here was the primary OOM path).
323
+ // onScan is retired on this branch: the newest-mainAgent raws come back as
324
+ // `mainAgentRing` in the result instead (adapter P0-2).
325
+ if (isV2SessionDir(filePath)) {
326
+ // cached defaults FALSE here: streamRawEntriesAsync serves the /events
327
+ // cold-load / live-attach and the workspaces live-source reload — these
328
+ // suppress the ≤ttl cache READ (they must not start from a stale window).
329
+ // Level-1 scan coalescing still collapses concurrent reconnects; the join
330
+ // staleness is bounded by one in-flight scan (not the ttl) — see
331
+ // singleflight.js cached=false note.
332
+ const res = await streamV2WindowedEntries(
333
+ filePath,
334
+ { limit: opts.limit, since: sinceFilter, onReady, cached: opts.cached === true },
335
+ onRawEntry,
336
+ );
337
+ await new Promise(resolve => setImmediate(resolve));
338
+ return { sentCount: res.sentCount, totalCount: res.totalCount, mainAgentRing: res.mainAgentRing };
339
+ }
340
+
302
341
  // 第一遍:异步 generator 逐条读取 → dedup Map 存原始字符串(不 parse)
303
342
  // 内存 = 去重后的原始字符串总量 ≈ 文件大小的一半(inProgress 被 completed 覆盖)
304
343
  const dedup = new Map();
@@ -417,10 +456,17 @@ function _sliceToCheckpoint(entries, limit) {
417
456
  * @param {{ limit?: number }} opts
418
457
  * @returns {Promise<{ entries: string[], hasMore: boolean, oldestTimestamp: string, estimatedTotal: number }>}
419
458
  */
420
- export async function readTailEntries(filePath, { limit = 300 } = {}) {
421
- filePath = resolveJsonlPath(filePath);
459
+ export async function readTailEntries(filePath, { limit = 300, cached = true } = {}) {
422
460
  const emptyResult = { entries: [], hasMore: false, oldestTimestamp: '', estimatedTotal: 0 };
423
461
  if (!existsSync(filePath)) return emptyResult;
462
+ // v2 session dirs have no byte-offset tail window — the adapter windows by
463
+ // entry count and synthesizes a baseline checkpoint at the window start.
464
+ // cached defaults TRUE: the /api/local-log tail + IM popup are historical
465
+ // read-only reads (S10b) and may consume a ≤ttl-stale window.
466
+ if (isV2SessionDir(filePath)) {
467
+ const win = await readV2WindowedEntries(filePath, { limit, cached });
468
+ return { entries: win.entries, hasMore: win.hasMore, oldestTimestamp: win.oldestTimestamp, estimatedTotal: win.totalCount };
469
+ }
424
470
  const st = statSync(filePath);
425
471
  if (st.size === 0) return emptyResult;
426
472
 
@@ -473,98 +519,3 @@ async function _readTailFull(filePath, limit, fileSize) {
473
519
  const oldestTimestamp = extractTimestamp(sliced[0]) || '';
474
520
  return { entries: sliced, hasMore, oldestTimestamp, estimatedTotal: totalCount };
475
521
  }
476
-
477
- /**
478
- * 读取分页历史条目(用于 /api/entries/page REST 端点)。
479
- *
480
- * 复用 iterateRawEntries + extractDedupKey 去重,
481
- * 过滤 timestamp < before 的条目,从末尾取最后 limit 条,
482
- * 向前扩展到 checkpoint 边界。
483
- *
484
- * @param {string} filePath
485
- * @param {{ before: string, limit: number }} opts
486
- * @returns {{ entries: string[], hasMore: boolean, oldestTimestamp: string, count: number }}
487
- */
488
- export async function readPagedEntries(filePath, { before, limit }) {
489
- filePath = resolveJsonlPath(filePath);
490
- if (!existsSync(filePath)) return { entries: [], hasMore: false, oldestTimestamp: '', count: 0 };
491
- const stat = statSync(filePath);
492
- if (stat.size === 0) return { entries: [], hasMore: false, oldestTimestamp: '', count: 0 };
493
-
494
- const dedup = await _collectDedup(iterateRawEntriesAsync(filePath));
495
-
496
- // 过滤 timestamp < before
497
- const filtered = [];
498
- for (const [key, raw] of dedup) {
499
- if (key.startsWith('__nokey_')) continue;
500
- const ts = extractTimestamp(raw);
501
- if (ts && ts < before) {
502
- filtered.push(raw);
503
- }
504
- }
505
-
506
- if (filtered.length === 0) {
507
- return { entries: [], hasMore: false, oldestTimestamp: '', count: 0 };
508
- }
509
-
510
- const { sliced, hasMore } = _sliceToCheckpoint(filtered, limit);
511
- const oldestTimestamp = extractTimestamp(sliced[0]) || '';
512
- return { entries: sliced, hasMore, oldestTimestamp, count: sliced.length };
513
- }
514
-
515
- /**
516
- * Scan a whole segment and collect the parsed entries matching `predicate`,
517
- * for the post-rotation teammate backfill (/api/prev-segment-teammates).
518
- *
519
- * - `rawPrefilter(rawString)`: cheap substring gate evaluated BEFORE
520
- * JSON.parse, so multi-MB MainAgent checkpoint frames are skipped without
521
- * parsing.
522
- * - Last-write-wins dedup by timestamp|url (a request's inProgress and final
523
- * writes share one key; the later frame replaces the earlier).
524
- * - `maxBytes` budget applied NEWEST-first over the raw sizes: when the
525
- * matches exceed the budget, the oldest are dropped and `truncated` is set.
526
- *
527
- * Returns { entries, truncated } with entries in chronological (file) order.
528
- */
529
- export async function collectFilteredRawEntriesAsync(filePath, predicate, {
530
- maxBytes = 64 * 1024 * 1024,
531
- rawPrefilter = null,
532
- yieldEvery = 50,
533
- } = {}) {
534
- const matched = new Map(); // dedupKey -> { size, entry }, insertion order = file order
535
- let nokey = 0;
536
- let totalBytes = 0;
537
- let truncated = false;
538
- let parsedCount = 0;
539
- for await (const raw of iterateRawEntriesAsync(filePath)) {
540
- if (rawPrefilter && !rawPrefilter(raw)) continue;
541
- // Real team logs defeat the prefilter for most BYTES (leader checkpoints
542
- // embed the SendMessage tool text), so parses can total >1s on a 300MB
543
- // segment — yield periodically to keep the live proxy's event loop
544
- // responsive during the scan.
545
- if (yieldEvery > 0 && ++parsedCount % yieldEvery === 0) {
546
- await new Promise((resolve) => setImmediate(resolve));
547
- }
548
- let entry;
549
- try { entry = JSON.parse(raw); } catch { continue; }
550
- if (!entry || !predicate(entry)) continue;
551
- const key = extractDedupKey(raw) || `__nokey_${nokey++}`;
552
- const prev = matched.get(key);
553
- if (prev) totalBytes -= prev.size;
554
- matched.set(key, { size: raw.length, entry });
555
- totalBytes += raw.length;
556
- // Enforce the budget DURING the scan (newest wins): evicting the oldest
557
- // keys keeps transient memory bounded even on teammate-heavy segments —
558
- // not just the response size.
559
- while (totalBytes > maxBytes && matched.size > 1) {
560
- const oldestKey = matched.keys().next().value;
561
- totalBytes -= matched.get(oldestKey).size;
562
- matched.delete(oldestKey);
563
- truncated = true;
564
- }
565
- }
566
- return {
567
- entries: [...matched.values()].map((it) => it.entry),
568
- truncated,
569
- };
570
- }
@@ -1,35 +1,23 @@
1
- import { readFileSync, existsSync, watch, watchFile, unwatchFile, statSync } from 'node:fs';
2
- import { open as fsOpen, stat as fsStat } from 'node:fs/promises';
3
- import { dirname, basename } from 'node:path';
1
+ import { readFileSync, existsSync } from 'node:fs';
4
2
  import { isMainAgentEntry, extractCachedContent } from './kv-cache-analyzer.js';
5
3
  import { buildContextWindowEvent, getContextSizeForModel } from './context-watcher.js';
6
- import { reconstructEntries, createIncrementalReconstructor } from './delta-reconstructor.js';
7
- import { countLogEntries, streamReconstructedEntriesAsync } from './log-stream.js';
4
+ import { reconstructEntries } from './delta-reconstructor.js';
8
5
  import { enrichEntry } from './enrich-plan-input.js';
9
6
  import { enrichEntry as enrichWorkflowEntry } from './enrich-workflow.js';
10
- import { resolveJsonlPath } from './jsonl-archive.js';
11
-
12
- // 跟踪所有被 watch 的日志文件。value: fileState 对象(外部只用 .has()/.keys())
13
- const watchedFiles = new Map();
14
-
15
- // 目录级 fs.watch 实例注册表(事件驱动,替代 per-file 轮询)
16
- const _dirWatchers = new Map();
17
-
18
- // Windows 单次 appendFileSync 触发 2+ 事件,防抖合并
19
- const FSWATCH_DEBOUNCE_MS = 80;
20
-
21
- // 安全网慢轮询:fs.watch 可能漏事件(buffer overflow 等),冷 fallback 兜底
22
- const SAFETY_POLL_MS = 5000;
23
-
24
- const FORCE_POLL = process.env.CCV_FORCE_POLL === '1';
25
-
7
+ import { sseWrite } from './wire-compress.js';
8
+
9
+ // 1.7.0: the v1 log-file tail (fs.watch + byte cursors + rotation follow)
10
+ // retired with the v1 write path — the live channel is server/lib/v2/
11
+ // live-feed.js. This module keeps what both eras share: the SSE client
12
+ // plumbing (sendToClients & friends), the per-entry broadcast pipeline
13
+ // (processWatchedEntry, used by the live feed), and readLogFile — the legacy
14
+ // v1 reader (no production caller; kept for tests and ad-hoc legacy reads).
26
15
  /**
27
16
  * Read and parse a JSONL log file.
28
17
  * @param {string} logFile - absolute path to the log file
29
18
  * @returns {Array} parsed and deduplicated entries
30
19
  */
31
20
  export function readLogFile(logFile) {
32
- logFile = resolveJsonlPath(logFile);
33
21
  if (!existsSync(logFile)) {
34
22
  return [];
35
23
  }
@@ -73,7 +61,11 @@ function _safeSseWrite(clients, client, payload) {
73
61
  }
74
62
  let ok;
75
63
  try {
76
- ok = client.write(payload);
64
+ // sseWrite routes through the client's negotiated Content-Encoding
65
+ // (wire-compress.js) — a bare client.write here would corrupt compressed
66
+ // streams. The drain listener below stays on the res: the encoder is
67
+ // piped into it, so socket drain still propagates.
68
+ ok = sseWrite(client, payload);
77
69
  } catch {
78
70
  _removeClient(clients, client);
79
71
  return false;
@@ -81,9 +73,18 @@ function _safeSseWrite(clients, client, payload) {
81
73
  if (!ok) {
82
74
  if (!client._sseBackpressureSince) {
83
75
  client._sseBackpressureSince = Date.now();
84
- client.once('drain', () => { client._sseBackpressureSince = 0; });
76
+ // On compressed clients the false write() came from the ENCODER (its
77
+ // input buffer), whose own 'drain' fires when compression catches up —
78
+ // the res may never emit 'drain' at all (compressed output is 20-80x
79
+ // smaller than what the socket can absorb). Arming the reset on the
80
+ // wrong stream would let the timeout below kill healthy clients.
81
+ const pressured = client._wireEnc || client;
82
+ pressured.once('drain', () => { client._sseBackpressureSince = 0; });
85
83
  } else if (Date.now() - client._sseBackpressureSince > SSE_BACKPRESSURE_TIMEOUT_MS) {
86
84
  _removeClient(clients, client);
85
+ // Destroy the encoder first: end() alone leaves the 16MB brotli window
86
+ // alive until TCP teardown of the already-stuck socket.
87
+ try { if (client._wireEnc) client._wireEnc.destroy(); } catch {}
87
88
  try { client.end(); } catch {}
88
89
  return false;
89
90
  }
@@ -105,264 +106,51 @@ export function sendEventToClients(clients, eventName, data) {
105
106
  }
106
107
  }
107
108
 
108
- export function sendChunkToClients(clients, dataJson) {
109
- const payload = `event: load_chunk\ndata: ${dataJson}\n\n`;
109
+ /** Like sendEventToClients but `json` is an ALREADY-serialized JSON string —
110
+ * wire v3 forwards raw stored lines verbatim without a parse/stringify trip. */
111
+ export function sendEventRawToClients(clients, eventName, json) {
112
+ const payload = `event: ${eventName}\ndata: ${json}\n\n`;
110
113
  for (let i = clients.length - 1; i >= 0; i--) {
111
114
  _safeSseWrite(clients, clients[i], payload);
112
115
  }
113
116
  }
114
117
 
115
- // --- 轮转切换(抽取公共逻辑) ---
116
-
117
- async function _switchToRotatedFile(logFile, currentLogFile, clients, opts) {
118
- // Deliberately KEEP the old file watched: external-process teammates resolve
119
- // the leader log once at boot and continue appending to the previous segment
120
- // after rotation. Unwatching it would make their post-rotation entries
121
- // invisible live; client dedup absorbs any replayed frames. watchedFiles is
122
- // a multi-file Map, so watching both segments is supported.
123
- const total = await countLogEntries(currentLogFile);
124
- sendEventToClients(clients, 'load_start', { total, incremental: false });
125
- await streamReconstructedEntriesAsync(currentLogFile, (segment) => {
126
- sendChunkToClients(clients, JSON.stringify(segment));
127
- });
128
- sendEventToClients(clients, 'load_end', {});
129
- watchLogFile({ ...opts, logFile: currentLogFile });
130
- }
131
-
132
- // --- 增量读 + 解析 + 广播(独立于触发机制) ---
133
-
134
- async function _readDelta(state) {
135
- if (state._reading) return; // 防止并发调用(debounce + safetyTimer 可能重叠)
136
- state._reading = true;
137
- const { logFile, opts, reconstructor } = state;
138
- const { clients, getClaudePid, runParallelHook, notifyStatsWorker, getLogFile } = opts;
139
- try {
140
- const st = await fsStat(logFile);
141
- const currentSize = st.size;
142
-
143
- if (currentSize < state.lastByteOffset) {
144
- state.lastByteOffset = 0;
145
- state.pendingTail = '';
146
- reconstructor.reset();
147
-
148
- const currentLogFile = getLogFile();
149
- if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
150
- await _switchToRotatedFile(logFile, currentLogFile, clients, opts);
151
- return;
152
- }
153
- }
154
-
155
- if (currentSize <= state.lastByteOffset) return;
156
-
157
- const bytesToRead = currentSize - state.lastByteOffset;
158
- const buf = Buffer.alloc(bytesToRead);
159
- const fh = await fsOpen(logFile, 'r');
160
- try {
161
- await fh.read(buf, 0, bytesToRead, state.lastByteOffset);
162
- } finally {
163
- await fh.close();
164
- }
165
- state.lastByteOffset = currentSize;
166
-
167
- const raw = state.pendingTail + buf.toString('utf-8');
168
- const parts = raw.split('\n---\n');
169
- state.pendingTail = parts.pop() || '';
170
-
171
- if (parts.length === 0 && state.pendingTail.trim()) {
172
- try {
173
- JSON.parse(state.pendingTail);
174
- parts.push(state.pendingTail);
175
- state.pendingTail = '';
176
- } catch {}
177
- }
178
-
179
- const validParts = parts.filter(p => p.trim());
180
- if (validParts.length > 0) {
181
- validParts.forEach(entry => {
182
- try {
183
- const parsed = JSON.parse(entry);
184
- if (!parsed.pid) parsed.pid = getClaudePid();
185
- reconstructor.reconstruct(parsed);
186
- try { enrichEntry(parsed); } catch {}
187
- try { enrichWorkflowEntry(parsed); } catch {}
188
- sendToClients(clients, parsed);
189
- runParallelHook('onNewEntry', parsed).catch(() => {});
190
- if (isMainAgentEntry(parsed) && !parsed.inProgress) {
191
- const cached = extractCachedContent(parsed);
192
- if (cached) sendEventToClients(clients, 'kv_cache_content', cached);
193
- const usage = parsed.response?.body?.usage;
194
- if (usage) {
195
- const contextSize = getContextSizeForModel(parsed.body?.model);
196
- const cwData = buildContextWindowEvent(usage, contextSize);
197
- if (cwData) sendEventToClients(clients, 'context_window', cwData);
198
- }
199
- }
200
- } catch {}
201
- });
202
- notifyStatsWorker(logFile);
203
- }
204
-
205
- const currentLogFile = getLogFile();
206
- if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
207
- await _switchToRotatedFile(logFile, currentLogFile, clients, opts);
208
- }
209
- } catch {
210
- // File not yet created or transient read error
211
- } finally {
212
- state._reading = false;
213
- }
214
- }
215
-
216
- // --- 目录级 fs.watch 管理 ---
217
-
218
- function _getOrCreateDirWatcher(dir) {
219
- if (_dirWatchers.has(dir)) return _dirWatchers.get(dir);
220
-
221
- try {
222
- const files = new Map();
223
- const watcher = watch(dir, (eventType, filename) => {
224
- if (!filename) {
225
- for (const [, fileState] of files) {
226
- _scheduleDebouncedRead(fileState);
227
- }
228
- return;
229
- }
230
- const fileState = files.get(filename);
231
- if (fileState) _scheduleDebouncedRead(fileState);
232
- });
233
-
234
- watcher.on('error', () => {
235
- for (const [, fileState] of files) {
236
- _fallbackToPolling(fileState);
237
- }
238
- try { watcher.close(); } catch {}
239
- _dirWatchers.delete(dir);
240
- });
241
-
242
- const entry = { watcher, files };
243
- _dirWatchers.set(dir, entry);
244
- return entry;
245
- } catch {
246
- return null;
247
- }
248
- }
249
-
250
- function _scheduleDebouncedRead(fileState) {
251
- if (fileState.debounceTimer) return;
252
- fileState.debounceTimer = setTimeout(() => {
253
- fileState.debounceTimer = null;
254
- _readDelta(fileState);
255
- }, FSWATCH_DEBOUNCE_MS);
256
- }
257
-
258
- // 测试注入缝(仿 updater fetchImpl 惯例):替换轮询分支的 watchFile 实现。
259
- // 真实 watchFile 的 stat 基线与 500ms 间隔在 CI 慢机上不可确定性驱动(基线竞态曾致
260
- // 25s 全程静默 flake),单测注入假实现手动触发回调即可零时序覆盖。生产恒为 node:fs 原版。
261
- let _watchFileImpl = watchFile;
262
- export function __setWatchFileImplForTests(fn) { _watchFileImpl = fn || watchFile; }
263
-
264
- function _fallbackToPolling(fileState) {
265
- if (fileState.polling) return;
266
- fileState.polling = true;
267
- _watchFileImpl(fileState.logFile, { interval: 500 }, () => {
268
- _readDelta(fileState);
269
- });
270
- }
271
-
272
- function _unwatchSingleFile(logFile) {
273
- const fileState = watchedFiles.get(logFile);
274
- watchedFiles.delete(logFile);
275
-
276
- if (!fileState) return;
277
-
278
- if (fileState.debounceTimer) clearTimeout(fileState.debounceTimer);
279
- if (fileState.safetyTimer) clearInterval(fileState.safetyTimer);
280
-
281
- if (fileState.polling) {
282
- try { unwatchFile(logFile); } catch {}
283
- return;
284
- }
285
-
286
- const dir = dirname(logFile);
287
- const filename = basename(logFile);
288
- const dirEntry = _dirWatchers.get(dir);
289
- if (dirEntry) {
290
- dirEntry.files.delete(filename);
291
- if (dirEntry.files.size === 0) {
292
- try { dirEntry.watcher.close(); } catch {}
293
- _dirWatchers.delete(dir);
294
- }
118
+ export function sendChunkToClients(clients, dataJson) {
119
+ const payload = `event: load_chunk\ndata: ${dataJson}\n\n`;
120
+ for (let i = clients.length - 1; i >= 0; i--) {
121
+ _safeSseWrite(clients, clients[i], payload);
295
122
  }
296
123
  }
297
124
 
298
- // --- 公开 API ---
299
-
300
- export function watchLogFile(opts) {
301
- const { logFile } = opts;
302
- if (watchedFiles.has(logFile)) return;
303
-
304
- let lastByteOffset = 0;
305
- const _reconstructor = createIncrementalReconstructor();
306
- try {
307
- if (existsSync(logFile)) {
308
- lastByteOffset = statSync(logFile).size;
125
+ /**
126
+ * Per-entry broadcast pipeline — the single processing path every live entry
127
+ * takes on its way to the SSE clients, regardless of source (v1 file tail or
128
+ * the v2 live feed): pid stamping, server-side delta reconstruction, plan/
129
+ * workflow enrichment, client broadcast, plugin hook, kv-cache and
130
+ * context-window side events.
131
+ * @param {object} parsed - one parsed log entry (mutated in place)
132
+ * @param {{reconstructor: object, clients: Array, getClaudePid: Function,
133
+ * runParallelHook: Function}} ctx
134
+ */
135
+ export function processWatchedEntry(parsed, ctx) {
136
+ const { reconstructor, clients, getClaudePid, runParallelHook } = ctx;
137
+ if (!parsed.pid) parsed.pid = getClaudePid();
138
+ reconstructor.reconstruct(parsed);
139
+ try { enrichEntry(parsed); } catch {}
140
+ try { enrichWorkflowEntry(parsed); } catch {}
141
+ // Wire v3 (V3.S5): the flagged v2 live feed suppresses the full-entry
142
+ // broadcast — clients rebuild entries from native lines + rows. The kv/
143
+ // context side events below still fire (they are slim and file-agnostic).
144
+ if (!ctx.suppressEntryBroadcast) sendToClients(clients, parsed);
145
+ runParallelHook('onNewEntry', parsed).catch(() => {});
146
+ if (isMainAgentEntry(parsed) && !parsed.inProgress) {
147
+ const cached = extractCachedContent(parsed);
148
+ if (cached) sendEventToClients(clients, 'kv_cache_content', cached);
149
+ const usage = parsed.response?.body?.usage;
150
+ if (usage) {
151
+ const contextSize = getContextSizeForModel(parsed.body?.model);
152
+ const cwData = buildContextWindowEvent(usage, contextSize);
153
+ if (cwData) sendEventToClients(clients, 'context_window', cwData);
309
154
  }
310
- } catch {}
311
-
312
- const fileState = {
313
- logFile,
314
- opts,
315
- reconstructor: _reconstructor,
316
- lastByteOffset,
317
- pendingTail: '',
318
- debounceTimer: null,
319
- safetyTimer: null,
320
- polling: false,
321
- };
322
-
323
- watchedFiles.set(logFile, fileState);
324
-
325
- if (FORCE_POLL) {
326
- _fallbackToPolling(fileState);
327
- return;
328
- }
329
-
330
- const dir = dirname(logFile);
331
- const filename = basename(logFile);
332
- const dirEntry = _getOrCreateDirWatcher(dir);
333
-
334
- if (!dirEntry) {
335
- _fallbackToPolling(fileState);
336
- return;
337
- }
338
-
339
- dirEntry.files.set(filename, fileState);
340
-
341
- fileState.safetyTimer = setInterval(() => {
342
- _readDelta(fileState);
343
- }, SAFETY_POLL_MS);
344
- }
345
-
346
- export function unwatchLogFile(logFile) {
347
- _unwatchSingleFile(logFile);
348
- }
349
-
350
- export function unwatchAll() {
351
- for (const logFile of watchedFiles.keys()) {
352
- _unwatchSingleFile(logFile);
353
155
  }
354
- for (const [, entry] of _dirWatchers) {
355
- try { entry.watcher.close(); } catch {}
356
- }
357
- _dirWatchers.clear();
358
- watchedFiles.clear();
359
- }
360
-
361
- export function startWatching(opts) {
362
- const { clients, ...watchOpts } = opts;
363
- watchLogFile({ ...watchOpts, clients });
364
- }
365
-
366
- export function getWatchedFiles() {
367
- return watchedFiles;
368
156
  }