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
@@ -1,11 +1,14 @@
1
1
  // SSE event stream + log-registration / resume / turn-end routes (moved verbatim from server.js).
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { LOG_FILE, _resumeState, resolveResumeChoice, _projectName } from '../interceptor.js';
4
+ import { _projectName, getLiveLogSource, isContinuedLaunch } from '../interceptor.js';
5
5
  import { LOG_DIR } from '../../findcc.js';
6
- import { watchLogFile } from '../lib/log-watcher.js';
7
- import { countLogEntries, streamRawEntriesAsync, readPagedEntries } from '../lib/log-stream.js';
8
- import { awaitDrainOrClose } from '../lib/sse-backpressure.js';
6
+ import { streamRawEntriesAsync } from '../lib/log-stream.js';
7
+ import { migrationStatus } from '../lib/v2/migrate-prompt.js';
8
+ import { reportSwallowed } from '../lib/error-report.js';
9
+ import { sseHead, sseWrite, needsDrain, wireEnd, awaitWireDrain } from '../lib/wire-compress.js';
10
+ import { readV2ColdBundle } from '../lib/v2/meta-rows.js';
11
+ import { readV2SingleEntry } from '../lib/v2/adapter.js';
9
12
  import { enrichRawIfNeeded } from '../lib/enrich-plan-input.js';
10
13
  import { validateLogPath } from '../lib/log-management.js';
11
14
  import { isMainAgentEntry, extractCachedContent } from '../lib/kv-cache-analyzer.js';
@@ -49,77 +52,6 @@ function turnEndNotify(req, res, parsedUrl, isLocal, deps) {
49
52
  });
50
53
  }
51
54
 
52
- // 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
53
- function registerLog(req, res, parsedUrl, isLocal, deps) {
54
- let body = '';
55
- req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
56
- req.on('end', () => {
57
- try {
58
- const { logFile } = JSON.parse(body);
59
- if (logFile && typeof logFile === 'string' && logFile.startsWith(LOG_DIR) && existsSync(logFile)) {
60
- watchLogFile(deps.logWatcherOpts(logFile));
61
- res.writeHead(200, { 'Content-Type': 'application/json' });
62
- res.end(JSON.stringify({ ok: true }));
63
- } else {
64
- res.writeHead(400, { 'Content-Type': 'application/json' });
65
- res.end(JSON.stringify({ error: 'Invalid log file path' }));
66
- }
67
- } catch {
68
- res.writeHead(400, { 'Content-Type': 'application/json' });
69
- res.end(JSON.stringify({ error: 'Invalid request body' }));
70
- }
71
- });
72
- }
73
-
74
- // 用户选择继续/新开日志
75
- function resumeChoice(req, res, parsedUrl, isLocal, deps) {
76
- let body = '';
77
- req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
78
- req.on('end', async () => {
79
- try {
80
- const { choice } = JSON.parse(body);
81
- if (choice !== 'continue' && choice !== 'new') {
82
- res.writeHead(400, { 'Content-Type': 'application/json' });
83
- res.end(JSON.stringify({ error: 'Invalid choice' }));
84
- return;
85
- }
86
- const result = resolveResumeChoice(choice);
87
- if (!result) {
88
- res.writeHead(409, { 'Content-Type': 'application/json' });
89
- res.end(JSON.stringify({ error: 'Already resolved' }));
90
- return;
91
- }
92
- // 重新 watch 最终的日志文件
93
- watchLogFile(deps.logWatcherOpts(result.logFile));
94
- // 广播 resume_resolved + full_reload
95
- const resolvedData = JSON.stringify({ logFile: result.logFile });
96
- deps.clients.forEach(client => {
97
- try {
98
- client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
99
- } catch { }
100
- });
101
- // 流式分段广播 full_reload,避免全量加载 OOM
102
- const reloadTotal = await countLogEntries(LOG_FILE);
103
- deps.clients.forEach(client => {
104
- try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: reloadTotal, incremental: false })}\n\n`); } catch { }
105
- });
106
- await streamRawEntriesAsync(LOG_FILE, (raw) => {
107
- deps.clients.forEach(client => {
108
- try { client.write('event: load_chunk\ndata: ['); client.write(raw.replace(/\n/g, '')); client.write(']\n\n'); } catch { }
109
- });
110
- });
111
- deps.clients.forEach(client => {
112
- try { client.write(`event: load_end\ndata: {}\n\n`); } catch { }
113
- });
114
- res.writeHead(200, { 'Content-Type': 'application/json' });
115
- res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
116
- } catch {
117
- res.writeHead(400, { 'Content-Type': 'application/json' });
118
- res.end(JSON.stringify({ error: 'Invalid request body' }));
119
- }
120
- });
121
- }
122
-
123
55
  // SSE endpoint
124
56
  // 构造「有新版」徽标的 SSE 帧(pending = {version, source})。pending 为空返回 null。
125
57
  // 抽成纯函数便于单测帧格式;events() 在新连接上调用它补推,使版本徽标跨刷新持续显示。
@@ -130,7 +62,10 @@ export function sseUpdateBadgeFrame(pending) {
130
62
  }
131
63
 
132
64
  async function events(req, res, parsedUrl, isLocal, deps) {
133
- res.writeHead(200, {
65
+ // Negotiated Content-Encoding (br|identity). From here on, every byte of
66
+ // this response MUST go through sseWrite — a bare res.write would corrupt
67
+ // the compressed stream (see server/lib/wire-compress.js).
68
+ sseHead(req, res, 200, {
134
69
  'Content-Type': 'text/event-stream',
135
70
  'Cache-Control': 'no-cache',
136
71
  'Connection': 'keep-alive',
@@ -144,7 +79,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
144
79
 
145
80
  // SSE 心跳保活:每 30s 发送 ping 事件,防止连接被 OS/代理/浏览器静默断开
146
81
  const pingTimer = setInterval(() => {
147
- try { res.write('event: ping\ndata: {}\n\n'); } catch {}
82
+ try { sseWrite(res, 'event: ping\ndata: {}\n\n'); } catch {}
148
83
  }, 30000);
149
84
 
150
85
  // server_config: 给前端推一次性的关键运行时常量,让前端 cooldown / debounce 等
@@ -152,20 +87,26 @@ async function events(req, res, parsedUrl, isLocal, deps) {
152
87
  // 见 server.js 顶部 turnEnd debounce 的 SUNSET-MARKER 注释。
153
88
  // write 失败要 warn:之前静默吞会让 env override 漂移到前端硬常量 10s 而无人发觉。
154
89
  try {
155
- res.write(`event: server_config\ndata: ${JSON.stringify({ turnEndDebounceMs: deps.turnEndDebounceMs })}\n\n`);
90
+ sseWrite(res, `event: server_config\ndata: ${JSON.stringify({ turnEndDebounceMs: deps.turnEndDebounceMs, wireV3: !!deps.wireV3, build: deps.serverBuild || '' })}\n\n`);
156
91
  } catch (err) {
157
92
  console.warn(`[server_config] SSE write failed (turnEndDebounceMs=${deps.turnEndDebounceMs}):`, err && err.message);
158
93
  }
159
94
 
160
- // 如果有待决的 resume 选择,发送 resume_prompt 事件
161
- if (_resumeState) {
162
- res.write(`event: resume_prompt\ndata: ${JSON.stringify({ recentFileName: _resumeState.recentFileName })}\n\n`);
163
- }
164
-
165
95
  // 补推「有新版」徽标:复用启动检查缓存的结果,让刷新/新标签页连上即恢复徽标。
166
96
  // 置于 deps.clients.push(res) 之前 → 新客户端仅得一份,不与一次性广播竞态。
167
97
  const updFrame = sseUpdateBadgeFrame(deps.pendingMajorUpdate);
168
- if (updFrame) res.write(updFrame);
98
+ if (updFrame) sseWrite(res, updFrame);
99
+
100
+ // 1.7.0 迁移引导(P2):当前项目仍有未转换的 v1 日志 → 连接即推 migrate_prompt。
101
+ // 是否弹窗由客户端决定(「不再提醒」偏好在客户端;continued=true 时无视 dismissed
102
+ // 再提醒一次——`-c` 续接的对话前半段在旧格式里,不迁移就看不到)。工作区切换后的
103
+ // 提示由 workspaces launch 路由对存量连接广播(本帧只覆盖新连接)。
104
+ try {
105
+ const mig = migrationStatus(LOG_DIR, _projectName || '');
106
+ if (mig.pending) {
107
+ sseWrite(res, `event: migrate_prompt\ndata: ${JSON.stringify({ ...mig, continued: isContinuedLaunch() })}\n\n`);
108
+ }
109
+ } catch (e) { reportSwallowed('sse.migrate_prompt', e); }
169
110
 
170
111
  // 增量加载参数:移动端带 since/cc/project 请求增量数据
171
112
  const sinceParam = parsedUrl.searchParams.get('since');
@@ -202,10 +143,21 @@ async function events(req, res, parsedUrl, isLocal, deps) {
202
143
  // ring=3 的容错意义:团队会话末尾常有连续 teammate 伪 mainAgent 条目,单记忆位会被
203
144
  // 挤掉真实 mainAgent;子串预过滤的理论误伤(键/值恰为 "teammate" 的真实条目)也由
204
145
  // 环内更早候选兜底。
146
+ // KEEP IN SYNC: server/lib/v2/adapter.js MAINAGENT_RING_DEPTH — on the v2 path
147
+ // the adapter's mainAgentRing (that depth) wholesale REPLACES this ring, so a
148
+ // mismatch would silently give v1 and v2 sessions different candidate depths.
205
149
  const MAINAGENT_SCAN_RING = 3;
206
150
  const mainAgentRawRing = [];
207
151
 
208
- await streamRawEntriesAsync(LOG_FILE, async (raw) => {
152
+ // Wire v3 (V3.S5): flagged + v2 source ⇒ the legacy full-entry cold stream
153
+ // is REPLACED by rows + native lines (the byte win); the client assembler
154
+ // rebuilds entries locally. v1 legacy files keep the entry pipeline.
155
+ const _v3Src = deps.wireV3 ? getLiveLogSource() : null;
156
+ const v3Cold = !!(_v3Src && existsSync(join(_v3Src, 'journal.jsonl')));
157
+
158
+ // S6b: the cold-load source is the current v2 session dir when the v2
159
+ // writer is active (adapter stream), else the v1 file.
160
+ const coldLoadResult = v3Cold ? null : await streamRawEntriesAsync(getLiveLogSource(), async (raw) => {
209
161
  // 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
210
162
  // ExitPlanMode V2 空 input 的条目按需补全 plan / planFilePath,其它原样透传
211
163
  if (res.destroyed || !res.writable) return;
@@ -215,9 +167,15 @@ async function events(req, res, parsedUrl, isLocal, deps) {
215
167
  // 把 EPIPE 抛穿 async callback;res.on('close'|'error') 已会做 clients 数组清理。
216
168
  let drained = true;
217
169
  try {
218
- res.write('event: load_chunk\ndata: [');
219
- drained = res.write(out.includes('\n') ? out.replace(/\n/g, '') : out);
220
- res.write(']\n\n');
170
+ sseWrite(res, 'event: load_chunk\ndata: [');
171
+ const ok = sseWrite(res, out.includes('\n') ? out.replace(/\n/g, '') : out);
172
+ sseWrite(res, ']\n\n');
173
+ // Two pressure signals: `ok` is the write target's input buffer (the
174
+ // ENCODER on compressed paths — its backlog would otherwise be invisible
175
+ // because compressed output is 20-80x smaller and rarely fills the
176
+ // socket), needsDrain(res) is the socket itself. awaitWireDrain below
177
+ // waits on whichever stream actually applies the pressure.
178
+ drained = ok && !needsDrain(res);
221
179
  } catch {
222
180
  return;
223
181
  }
@@ -225,7 +183,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
225
183
  // helper 内部会在 fulfill 时把另外两个监听器从 res 上摘掉,避免 N 次 backpressure
226
184
  // 累积出 N 个 stale close/error listener 触发 MaxListenersExceededWarning。
227
185
  if (!drained) {
228
- await awaitDrainOrClose(res, deps.SSE_BACKPRESSURE_TIMEOUT_MS);
186
+ await awaitWireDrain(res, deps.SSE_BACKPRESSURE_TIMEOUT_MS);
229
187
  }
230
188
  }, {
231
189
  since: useIncremental ? sinceParam : undefined,
@@ -249,11 +207,62 @@ async function events(req, res, parsedUrl, isLocal, deps) {
249
207
  loadStartData.hasMore = !!hasMore;
250
208
  loadStartData.oldestTs = oldestTs || '';
251
209
  }
252
- res.write(`event: load_start\ndata: ${JSON.stringify(loadStartData)}\n\n`);
210
+ sseWrite(res, `event: load_start\ndata: ${JSON.stringify(loadStartData)}\n\n`);
253
211
  },
254
212
  });
255
213
 
256
- res.write(`event: load_end\ndata: {}\n\n`);
214
+ // Wire v3 (V3.S2/S4/S5): cold rows + native lines land BEFORE load_end so
215
+ // the flagged client can assemble its window in the same load cycle. The
216
+ // legacy chunk stream was skipped above (v3Cold) — load_start is emitted
217
+ // here from the rows metadata instead.
218
+ if (v3Cold) {
219
+ try {
220
+ const src = _v3Src;
221
+ // Single-flighted bundle (review P2-b): a reconnect storm coalesces to
222
+ // one journal fold + one native read per (dir, window). `since` scopes
223
+ // an incremental reconnect to the delta window (review P1-2) — the
224
+ // client upserts those rows instead of resetting the list.
225
+ const { meta, native } = await readV2ColdBundle(src, {
226
+ limit: useLimit ? effectiveLimit : 0,
227
+ since: useIncremental ? sinceParam : null,
228
+ });
229
+ // Build every payload BEFORE load_start so it can carry the exact byte
230
+ // total — the client renders a real received/total progress (the legacy
231
+ // wire's per-entry count-up has no per-frame granularity here).
232
+ const rowsPayload = JSON.stringify({ rows: meta.rows, totalCount: meta.totalCount, hasMore: meta.hasMore, oldestTs: meta.oldestTimestamp, incremental: !!useIncremental });
233
+ let v3Bytes = rowsPayload.length;
234
+ for (const p of native.convPayloads) v3Bytes += p.length;
235
+ for (const p of native.respPayloads) v3Bytes += p.length;
236
+ const loadStartData = { total: meta.totalCount, incremental: !!useIncremental, v3Bytes };
237
+ if (useLimit) {
238
+ loadStartData.hasMore = !!meta.hasMore;
239
+ loadStartData.oldestTs = meta.oldestTimestamp || '';
240
+ }
241
+ sseWrite(res, `event: load_start\ndata: ${JSON.stringify(loadStartData)}\n\n`);
242
+ sseWrite(res, `event: v2_requests\ndata: ${rowsPayload}\n\n`);
243
+ for (const payload of native.convPayloads) sseWrite(res, `event: v3_conv\ndata: ${payload}\n\n`);
244
+ for (const payload of native.respPayloads) sseWrite(res, `event: v3_resp\ndata: ${payload}\n\n`);
245
+ // kv-cache / context_window sources: rebuild the newest completed
246
+ // mainAgent rows. Depth 3 mirrors the legacy scan-ring fallback (review
247
+ // P2-e): the newest main may lack usage/cached-content or be a
248
+ // synthesis-gated orphan — earlier candidates then provide the values.
249
+ const mains = meta.rows.filter((r) => r.mainAgent && !r.inProgress).slice(-3);
250
+ for (const m of mains) {
251
+ const detail = await readV2SingleEntry(src, { seq: m.seq, sessionId: m.sessionId });
252
+ if (detail && detail.entry) mainAgentRawRing.push(detail.entry);
253
+ }
254
+ } catch (err) { reportSwallowed('sse.v2_requests', err); }
255
+ }
256
+
257
+ sseWrite(res, `event: load_end\ndata: {}\n\n`);
258
+
259
+ // S10a: v2 sources no longer invoke onScan (the two-pass window never
260
+ // stringifies out-of-window entries) — the adapter returns the newest-main
261
+ // raws as `mainAgentRing` instead. v1 sources still fill the ring via onScan.
262
+ if (Array.isArray(coldLoadResult?.mainAgentRing) && coldLoadResult.mainAgentRing.length > 0) {
263
+ mainAgentRawRing.length = 0;
264
+ mainAgentRawRing.push(...coldLoadResult.mainAgentRing);
265
+ }
257
266
 
258
267
  // Pass 1 入环的候选 newest-first 校验 + parse(≤K 次)。kv_cache 与 context_window
259
268
  // 各自取"最新一条能提供该值的真实 mainAgent"—— 与旧版逐条覆盖语义等价(环深度内)。
@@ -278,10 +287,10 @@ async function events(req, res, parsedUrl, isLocal, deps) {
278
287
 
279
288
  // 发送最新 MainAgent 的 KV-Cache 和 context_window
280
289
  if (latestKvCache) {
281
- res.write(`event: kv_cache_content\ndata: ${JSON.stringify(latestKvCache)}\n\n`);
290
+ sseWrite(res, `event: kv_cache_content\ndata: ${JSON.stringify(latestKvCache)}\n\n`);
282
291
  }
283
292
  if (latestContextWindow) {
284
- res.write(`event: context_window\ndata: ${JSON.stringify(latestContextWindow)}\n\n`);
293
+ sseWrite(res, `event: context_window\ndata: ${JSON.stringify(latestContextWindow)}\n\n`);
285
294
  pushedContextWindow = true;
286
295
  }
287
296
  // Fallback: no MainAgent in log (e.g. fresh session after -c), read context-window.json
@@ -301,7 +310,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
301
310
  const effectiveSize = adaptContextWindow(contextSize, inputTokens);
302
311
  const usedPct = effectiveSize > 0 ? Math.round((totalTokens / effectiveSize) * 100) : 0;
303
312
  const data = { ...cw, context_window_size: effectiveSize, used_percentage: usedPct, remaining_percentage: 100 - usedPct };
304
- res.write(`event: context_window\ndata: ${JSON.stringify(data)}\n\n`);
313
+ sseWrite(res, `event: context_window\ndata: ${JSON.stringify(data)}\n\n`);
305
314
  }
306
315
  } catch { }
307
316
  }
@@ -325,69 +334,30 @@ async function events(req, res, parsedUrl, isLocal, deps) {
325
334
  // API endpoint
326
335
  async function requests(req, res) {
327
336
  // 异步流式 JSON 数组输出,不做 reconstruct,发原始条目
328
- res.writeHead(200, { 'Content-Type': 'application/json' });
329
- res.write('[');
330
- let first = true;
331
- await streamRawEntriesAsync(LOG_FILE, (raw) => {
332
- if (!first) res.write(',');
333
- res.write(enrichRawIfNeeded(raw));
334
- first = false;
335
- });
336
- res.write(']');
337
- res.end();
338
- }
339
-
340
- // 分页历史条目端点:移动端"加载更多"按需拉取
341
- // 支持可选 ?file= 参数指定目标文件(用于本地日志分页),默认使用活跃会话文件
342
- async function entriesPage(req, res, parsedUrl) {
343
- const before = parsedUrl.searchParams.get('before');
344
- const limitVal = Math.min(parseInt(parsedUrl.searchParams.get('limit'), 10) || 100, 500);
345
- if (!before || isNaN(new Date(before).getTime())) {
346
- res.writeHead(400, { 'Content-Type': 'application/json' });
347
- res.end(JSON.stringify({ error: 'missing or invalid "before" parameter' }));
348
- return;
349
- }
350
- const file = parsedUrl.searchParams.get('file');
351
- let targetFile = LOG_FILE;
352
- if (file) {
353
- if (file.includes('..') || (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.zip'))) {
354
- res.writeHead(400, { 'Content-Type': 'application/json' });
355
- res.end(JSON.stringify({ error: 'Invalid file name' }));
356
- return;
357
- }
358
- try { validateLogPath(LOG_DIR, file); } catch (e) {
359
- const status = e.code === 'NOT_FOUND' ? 404 : e.code === 'ACCESS_DENIED' ? 403 : 400;
360
- res.writeHead(status, { 'Content-Type': 'application/json' });
361
- res.end(JSON.stringify({ error: e.message }));
362
- return;
363
- }
364
- targetFile = join(LOG_DIR, file);
365
- }
337
+ // flush:false —— whole-stream response: the client parses the array only
338
+ // when complete, so per-macrotask flush boundaries would just cost ratio.
339
+ sseHead(req, res, 200, { 'Content-Type': 'application/json' }, { flush: false });
366
340
  try {
367
- const result = await readPagedEntries(targetFile, { before, limit: limitVal });
368
- // entries 是原始 JSON 字符串数组,parse 后返回给客户端
369
- // ExitPlanMode V2 input 的条目用 enrichRawIfNeeded 在 raw 阶段补全
370
- const entries = result.entries.map(raw => {
371
- try { return JSON.parse(enrichRawIfNeeded(raw)); } catch { return null; }
372
- }).filter(Boolean);
373
- res.writeHead(200, { 'Content-Type': 'application/json' });
374
- res.end(JSON.stringify({
375
- entries,
376
- hasMore: result.hasMore,
377
- oldestTimestamp: result.oldestTimestamp,
378
- count: entries.length,
379
- }));
341
+ sseWrite(res, '[');
342
+ let first = true;
343
+ await streamRawEntriesAsync(getLiveLogSource(), (raw) => {
344
+ if (!first) sseWrite(res, ',');
345
+ sseWrite(res, enrichRawIfNeeded(raw));
346
+ first = false;
347
+ });
348
+ sseWrite(res, ']');
349
+ wireEnd(res);
380
350
  } catch (err) {
381
- res.writeHead(500, { 'Content-Type': 'application/json' });
382
- res.end(JSON.stringify({ error: err.message }));
351
+ // Mid-stream failure (e.g. session dir converted/removed while streaming):
352
+ // without this catch the rejection propagates through the dispatcher and
353
+ // kills the process. Headers are already sent — close the stream cleanly.
354
+ console.error('[api-requests]', err && err.stack || err);
355
+ wireEnd(res);
383
356
  }
384
357
  }
385
358
 
386
359
  export const eventsRoutes = [
387
360
  { method: 'POST', match: 'exact', path: '/api/turn-end-notify', handler: turnEndNotify },
388
- { method: 'POST', match: 'exact', path: '/api/register-log', handler: registerLog },
389
- { method: 'POST', match: 'exact', path: '/api/resume-choice', handler: resumeChoice },
390
361
  { method: 'GET', match: 'exact', path: '/events', handler: events },
391
362
  { method: 'GET', match: 'exact', path: '/api/requests', handler: requests },
392
- { method: 'GET', match: 'exact', path: '/api/entries/page', handler: entriesPage },
393
363
  ];
@@ -8,7 +8,7 @@ import { homedir, tmpdir } from 'node:os';
8
8
  import { execFile, spawn } from 'node:child_process';
9
9
  import { bumpWorkspacesVersion } from '../lib/file-access-policy.js';
10
10
  import { validateImportDir } from '../lib/file-api.js';
11
- import { LOG_FILE, PROFILE_PATH, _projectName } from '../interceptor.js';
11
+ import { PROFILE_PATH, _projectName, _logDir } from '../interceptor.js';
12
12
  import { LOG_DIR, getClaudeConfigDir } from '../../findcc.js';
13
13
 
14
14
  function upload(req, res, parsedUrl, isLocal, deps) {
@@ -844,7 +844,9 @@ function createDir(req, res, parsedUrl, isLocal, deps) {
844
844
  }
845
845
 
846
846
  function openLogDir(req, res) {
847
- const dir = LOG_FILE ? dirname(LOG_FILE) : LOG_DIR;
847
+ // Project dir works for both formats (legacy v1 files and the v2 sessions/
848
+ // tree are siblings under it); LOG_DIR is the pre-workspace fallback.
849
+ const dir = _logDir || LOG_DIR;
848
850
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
849
851
  execFile(cmd, [dir], { windowsHide: true }, () => {});
850
852
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -21,6 +21,7 @@
21
21
  // reports its own in-process adapter (deps.im.getBridgeStatus) — that's what the manager probes.
22
22
  import { getDescriptor, loadConfig, loadState, saveConfig } from '../lib/im-config.js';
23
23
  import { findRecentLog } from '../lib/interceptor-core.js';
24
+ import { listV2Sessions } from '../lib/v2/adapter.js';
24
25
  import { readSenders } from '../lib/im-senders.js';
25
26
  import { readImAppendSystem, writeImAppendSystem, buildImAppendSystemPreset, MAX_IM_APPEND_SYSTEM_CHARS } from '../lib/im-append-system.js';
26
27
  import { imDir } from '../lib/im-lock.js';
@@ -256,8 +257,16 @@ function imLogs(req, res, parsedUrl, isLocal, deps) {
256
257
  const project = `IM_${id}`;
257
258
  let latest = null;
258
259
  try {
259
- const abs = findRecentLog(join(LOG_DIR, project), project); // 已排除 *_temp.jsonl
260
- if (abs) latest = `${project}/${basename(abs)}`; // 相对 LOG_DIR,直接喂给 /api/local-log?file=
260
+ // wire-v2: prefer the newest v2 session (by meta.startTs, journal mtime as
261
+ // tie-break); fall back to legacy v1 files until they are migrated.
262
+ const sessions = listV2Sessions(join(LOG_DIR, project)).filter((s) => !s.leader);
263
+ if (sessions.length > 0) {
264
+ sessions.sort((a, b) => (a.startTs < b.startTs ? 1 : a.startTs > b.startTs ? -1 : 0));
265
+ latest = `v2:${project}/${sessions[0].sid}`; // 直接喂给 /api/local-log?file=
266
+ } else {
267
+ const abs = findRecentLog(join(LOG_DIR, project), project); // 已排除 *_temp.jsonl
268
+ if (abs) latest = `${project}/${basename(abs)}`; // 相对 LOG_DIR
269
+ }
261
270
  } catch { /* 无目录/无日志 → latest=null */ }
262
271
  res.writeHead(200, JSON_HEADERS);
263
272
  res.end(JSON.stringify({ project, latest }));