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.
- package/README.md +13 -1
- package/cli.js +116 -37
- package/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/Mobile-qfRsRth0.js +1 -0
- package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
- package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
- package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
- package/dist/assets/clone-BQtvy9Ae.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
- package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
- package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
- package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
- package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
- package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
- package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
- package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
- package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
- package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
- package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
- package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
- package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
- package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
- package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
- package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
- package/dist/index.html +4 -4
- package/findcc.js +4 -3
- package/package.json +3 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-log-watcher.js +68 -8
- package/server/lib/interceptor-core.js +19 -174
- package/server/lib/log-management.js +214 -247
- package/server/lib/log-stream.js +59 -108
- package/server/lib/log-watcher.js +62 -274
- package/server/lib/log-zip.js +148 -0
- package/server/lib/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/stats-worker.js +138 -267
- package/server/lib/user-prompt-extract.js +250 -0
- package/server/lib/v2/adapter.js +1124 -0
- package/server/lib/v2/blob-store.js +44 -0
- package/server/lib/v2/conversation-store.js +217 -0
- package/server/lib/v2/convert-manager.js +79 -0
- package/server/lib/v2/convert-worker.js +42 -0
- package/server/lib/v2/convert.js +409 -0
- package/server/lib/v2/identity.js +204 -0
- package/server/lib/v2/journal.js +62 -0
- package/server/lib/v2/layout.js +167 -0
- package/server/lib/v2/live-feed.js +558 -0
- package/server/lib/v2/meta-rows.js +360 -0
- package/server/lib/v2/migrate-prompt.js +54 -0
- package/server/lib/v2/replay.js +191 -0
- package/server/lib/v2/session-select.js +222 -0
- package/server/lib/v2/singleflight.js +104 -0
- package/server/lib/v2/v2-writer.js +528 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +120 -150
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- package/server/routes/logs.js +209 -164
- package/server/routes/preferences.js +0 -3
- package/server/routes/project-meta.js +2 -3
- package/server/routes/session-pin.js +7 -8
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +50 -14
- package/server/server.js +70 -66
- package/server/workspace-registry.js +24 -4
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +27 -0
- package/src/utils/avatarAnimationPostPass.js +49 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +7 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +620 -0
- package/src/utils/contextRaw.js +18 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/contextTurns.js +117 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/effectiveModel.js +13 -0
- package/src/utils/entry-slim.js +563 -0
- package/src/utils/entryCache.js +144 -0
- package/src/utils/errorReport.js +6 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +846 -0
- package/src/utils/identityHeal.js +99 -0
- package/src/utils/imConnState.js +59 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +112 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +119 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +30 -0
- package/src/utils/requestType.js +208 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/searchApi.js +47 -0
- package/src/utils/searchReplace.js +51 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +471 -0
- package/src/utils/sessionMerge.js +180 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tClaude.js +20 -0
- package/src/utils/teamModalBuilder.js +351 -0
- package/src/utils/teamSessionParser.js +190 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +121 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/toolCatalog.js +29 -0
- package/src/utils/toolResultBuilder.js +393 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +167 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +154 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/v3Assembler.js +112 -0
- package/src/utils/v3Rows.js +48 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webSearchGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- package/dist/assets/App-VTo4NVdo.js +0 -2
- package/dist/assets/Mobile-CcmYvMm_.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
- package/dist/assets/clone-md-Mc1-T.js +0 -1
- package/dist/assets/index-DCo650PG.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
- package/server/lib/instance-registry.js +0 -57
- package/server/lib/jsonl-archive.js +0 -180
- package/server/lib/teammate-detect.js +0 -32
- package/server/lib/zip-safety.js +0 -68
package/server/lib/log-stream.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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 —
|
|
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
|
-
|
|
296
|
-
|
|
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
|
|
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
|
|
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 {
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
}
|