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
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// Wire Format v2 — live feed (S6b): streams v2 session-dir appends to the SSE
|
|
2
|
+
// clients, replacing the v1 log-file tail as the live channel.
|
|
3
|
+
//
|
|
4
|
+
// Architecture (plan 1.2):
|
|
5
|
+
// - One incremental cursor per live session dir, built on SessionSynthesizer —
|
|
6
|
+
// the SAME synthesis engine cold reads use, so live emission is
|
|
7
|
+
// byte-identical to a later cold read by construction.
|
|
8
|
+
// - Two-level watcher topology: a watcher on `sessions/` sees new session dirs
|
|
9
|
+
// appear (cross-process producers: teammates, IM workers); a per-session-dir
|
|
10
|
+
// watcher sees journal.jsonl / responses.jsonl appends (direct children).
|
|
11
|
+
// Conversation event files live two levels deeper where non-recursive
|
|
12
|
+
// fs.watch cannot see them — but the writer appends the conv line BEFORE the
|
|
13
|
+
// journal line, so the journal append is the tick; the synthesizer's
|
|
14
|
+
// defer/retry covers the rare flush inversion, and a safety poll covers
|
|
15
|
+
// missed events entirely (fs.watch is lossy under load).
|
|
16
|
+
// - The leader process's own writer nudges the feed in-process (`tick()`)
|
|
17
|
+
// for zero-latency delivery — the nudge only schedules a cursor read; the
|
|
18
|
+
// DATA always comes from the files, keeping one synthesis path.
|
|
19
|
+
//
|
|
20
|
+
// Emission: every synthesized item is round-tripped through JSON (protecting
|
|
21
|
+
// the synthesizer's internal objects from downstream mutation) and pushed
|
|
22
|
+
// through log-watcher's processWatchedEntry pipeline — the same enrichment,
|
|
23
|
+
// reconstruction and side-events the v1 tail produced.
|
|
24
|
+
|
|
25
|
+
import { watch, existsSync, readdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { reportSwallowed } from '../error-report.js';
|
|
28
|
+
import { createIncrementalReconstructor } from '../delta-reconstructor.js';
|
|
29
|
+
import { processWatchedEntry, sendEventToClients, sendEventRawToClients } from '../log-watcher.js';
|
|
30
|
+
import { SessionSynthesizer } from './adapter.js';
|
|
31
|
+
import { computeCacheLoss } from './meta-rows.js';
|
|
32
|
+
import { classifyRequest } from '../../../src/utils/requestType.js';
|
|
33
|
+
|
|
34
|
+
const FSWATCH_DEBOUNCE_MS = 80;
|
|
35
|
+
const SAFETY_POLL_MS = 5000;
|
|
36
|
+
const DEFER_MS = 3000; // synthesizer park deadline (lagging lines)
|
|
37
|
+
const ATTACH_RECENT_MS = 5 * 60 * 1000; // pre-existing dirs considered "live"
|
|
38
|
+
const TICK_RETRY_MS = 250; // in-process tick raced the first queue drain
|
|
39
|
+
const TICK_RETRY_MAX = 8;
|
|
40
|
+
|
|
41
|
+
/** Read newly appended complete lines from a file cursor {path, offset,
|
|
42
|
+
* pending}. Returns an array of raw line strings (possibly empty). A file
|
|
43
|
+
* that shrank (should never happen — v2 files are append-only) returns null
|
|
44
|
+
* so the caller can rebuild the cursor. */
|
|
45
|
+
function readNewLines(cursor) {
|
|
46
|
+
let size;
|
|
47
|
+
try {
|
|
48
|
+
size = statSync(cursor.path).size;
|
|
49
|
+
} catch {
|
|
50
|
+
return []; // not created yet
|
|
51
|
+
}
|
|
52
|
+
if (size < cursor.offset) return null;
|
|
53
|
+
if (size === cursor.offset) return [];
|
|
54
|
+
const toRead = size - cursor.offset;
|
|
55
|
+
const buf = Buffer.alloc(toRead);
|
|
56
|
+
let fd;
|
|
57
|
+
try {
|
|
58
|
+
fd = openSync(cursor.path, 'r');
|
|
59
|
+
readSync(fd, buf, 0, toRead, cursor.offset);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
reportSwallowed('v2-live.read', err);
|
|
62
|
+
return [];
|
|
63
|
+
} finally {
|
|
64
|
+
if (fd !== undefined) { try { closeSync(fd); } catch { /* already closed */ } }
|
|
65
|
+
}
|
|
66
|
+
cursor.offset = size;
|
|
67
|
+
const chunk = cursor.pending + buf.toString('utf-8');
|
|
68
|
+
const parts = chunk.split('\n');
|
|
69
|
+
cursor.pending = parts.pop() || '';
|
|
70
|
+
return parts.map((l) => l.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class V2LiveFeed {
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} opts
|
|
76
|
+
* @param {Array} opts.clients - shared SSE client array (server.js)
|
|
77
|
+
* @param {Function} opts.getClaudePid
|
|
78
|
+
* @param {Function} opts.runParallelHook
|
|
79
|
+
* @param {Function} [opts.notifyStatsWorker] - called with the session dir
|
|
80
|
+
* after a batch of entries was emitted from it
|
|
81
|
+
* @param {Function} [opts.watchImpl] - fs.watch injection (tests)
|
|
82
|
+
* @param {number} [opts.safetyPollMs] - 0 disables the timer (tests drive
|
|
83
|
+
* _safetyTick manually)
|
|
84
|
+
* @param {number} [opts.deferMs]
|
|
85
|
+
* @param {Function} [opts.now]
|
|
86
|
+
*/
|
|
87
|
+
constructor(opts = {}) {
|
|
88
|
+
this._clients = opts.clients || [];
|
|
89
|
+
this._getClaudePid = opts.getClaudePid || (() => null);
|
|
90
|
+
this._runParallelHook = opts.runParallelHook || (() => Promise.resolve());
|
|
91
|
+
this._notifyStatsWorker = opts.notifyStatsWorker || null;
|
|
92
|
+
this._watchImpl = opts.watchImpl || watch;
|
|
93
|
+
this._safetyPollMs = typeof opts.safetyPollMs === 'number' ? opts.safetyPollMs : SAFETY_POLL_MS;
|
|
94
|
+
this._deferMs = typeof opts.deferMs === 'number' ? opts.deferMs : DEFER_MS;
|
|
95
|
+
this._now = opts.now || Date.now;
|
|
96
|
+
// Wire v3 (V3.S2): when on, every emitted item ALSO broadcasts a metadata
|
|
97
|
+
// row (v2_requests_delta). Explicit ctor param — this module has no access
|
|
98
|
+
// to the server deps object.
|
|
99
|
+
this._wireV3 = !!opts.wireV3;
|
|
100
|
+
this._sessions = new Map(); // dir → cursor bundle
|
|
101
|
+
this._seenDirs = new Map(); // dir → last observed journal mtimeMs (attached or not)
|
|
102
|
+
this._sessionsRoot = null;
|
|
103
|
+
this._rootWatcher = null;
|
|
104
|
+
this._safetyTimer = null;
|
|
105
|
+
this._startedAt = 0;
|
|
106
|
+
this._active = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Begin (or switch to) live-following a project's sessions/ root. */
|
|
110
|
+
start(projectDir) {
|
|
111
|
+
this.stop();
|
|
112
|
+
this._active = true;
|
|
113
|
+
this._startedAt = this._now();
|
|
114
|
+
this._sessionsRoot = join(projectDir, 'sessions');
|
|
115
|
+
this._armRootWatcher();
|
|
116
|
+
this._initialScan();
|
|
117
|
+
if (this._safetyPollMs > 0) {
|
|
118
|
+
this._safetyTimer = setInterval(() => this._safetyTick(), this._safetyPollMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
stop() {
|
|
123
|
+
this._active = false;
|
|
124
|
+
if (this._safetyTimer) { clearInterval(this._safetyTimer); this._safetyTimer = null; }
|
|
125
|
+
if (this._rootWatcher) { try { this._rootWatcher.close(); } catch { /* already closed */ } this._rootWatcher = null; }
|
|
126
|
+
for (const cur of this._sessions.values()) this._closeCursor(cur);
|
|
127
|
+
this._sessions.clear();
|
|
128
|
+
this._seenDirs.clear();
|
|
129
|
+
this._sessionsRoot = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** In-process nudge from the local V2Writer after an ingest: attach the
|
|
133
|
+
* session on first sight and schedule a cursor read. Zero-latency path for
|
|
134
|
+
* the leader's own traffic. The write queue drains asynchronously, so the
|
|
135
|
+
* very first tick can race the journal's creation — retry briefly (the
|
|
136
|
+
* per-dir watcher and safety poll are the durable fallbacks). */
|
|
137
|
+
tick(sessionDir, _retries = 0) {
|
|
138
|
+
if (!this._active || !sessionDir) return;
|
|
139
|
+
let cur = this._sessions.get(sessionDir);
|
|
140
|
+
if (!cur) {
|
|
141
|
+
cur = this._attach(sessionDir, { suppressExisting: false });
|
|
142
|
+
if (!cur) {
|
|
143
|
+
if (_retries < TICK_RETRY_MAX) {
|
|
144
|
+
setTimeout(() => this.tick(sessionDir, _retries + 1), TICK_RETRY_MS);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this._scheduleRead(cur);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** True when a session dir is being followed (tests / diagnostics). */
|
|
153
|
+
isFollowing(sessionDir) {
|
|
154
|
+
return this._sessions.has(sessionDir);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── watcher topology ───────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
_armRootWatcher() {
|
|
160
|
+
if (this._rootWatcher || !this._sessionsRoot) return;
|
|
161
|
+
if (!existsSync(this._sessionsRoot)) return; // re-tried by the safety tick
|
|
162
|
+
try {
|
|
163
|
+
const watcher = this._watchImpl(this._sessionsRoot, (eventType, filename) => {
|
|
164
|
+
// A new (or renamed) direct child is potentially a fresh session dir.
|
|
165
|
+
if (!this._active) return;
|
|
166
|
+
if (filename) {
|
|
167
|
+
const dir = join(this._sessionsRoot, filename);
|
|
168
|
+
this._maybeAttachNew(dir);
|
|
169
|
+
} else {
|
|
170
|
+
this._scanRoot();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
watcher.on('error', (err) => {
|
|
174
|
+
reportSwallowed('v2-live.root-watch', err);
|
|
175
|
+
try { watcher.close(); } catch { /* already closed */ }
|
|
176
|
+
if (this._rootWatcher === watcher) this._rootWatcher = null; // safety poll re-arms
|
|
177
|
+
});
|
|
178
|
+
this._rootWatcher = watcher;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
reportSwallowed('v2-live.root-watch', err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_initialScan() {
|
|
185
|
+
if (!existsSync(this._sessionsRoot)) return;
|
|
186
|
+
let names = [];
|
|
187
|
+
try {
|
|
188
|
+
names = readdirSync(this._sessionsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
reportSwallowed('v2-live.scan', err);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
for (const name of names) {
|
|
194
|
+
const dir = join(this._sessionsRoot, name);
|
|
195
|
+
const mtime = this._journalMtime(dir);
|
|
196
|
+
this._seenDirs.set(dir, mtime);
|
|
197
|
+
// Pre-existing dirs with recent activity are live producers that predate
|
|
198
|
+
// this feed (mid-run server start / workspace switch): follow them but
|
|
199
|
+
// suppress their history — the cold reload channel covers it.
|
|
200
|
+
if (mtime > 0 && this._now() - mtime <= ATTACH_RECENT_MS) {
|
|
201
|
+
this._attach(dir, { suppressExisting: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_scanRoot() {
|
|
207
|
+
if (!existsSync(this._sessionsRoot)) return;
|
|
208
|
+
let names = [];
|
|
209
|
+
try {
|
|
210
|
+
names = readdirSync(this._sessionsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
211
|
+
} catch {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const name of names) this._maybeAttachNew(join(this._sessionsRoot, name));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Attach a dir first seen after feed start (root watcher / safety scan). */
|
|
218
|
+
_maybeAttachNew(dir) {
|
|
219
|
+
if (this._sessions.has(dir)) return;
|
|
220
|
+
const mtime = this._journalMtime(dir);
|
|
221
|
+
if (mtime === 0) return; // no journal yet — the next event/tick retries
|
|
222
|
+
const seenBefore = this._seenDirs.has(dir);
|
|
223
|
+
this._seenDirs.set(dir, mtime);
|
|
224
|
+
if (!seenBefore) {
|
|
225
|
+
// Genuinely new session dir: everything in it is new — emit from byte 0
|
|
226
|
+
// when it was born after the feed started, otherwise treat as history.
|
|
227
|
+
// birthtime ≤ 0 (Linux filesystems without btime) must count as new:
|
|
228
|
+
// suppressing would swallow a cross-process session's first entries
|
|
229
|
+
// (review P2).
|
|
230
|
+
const bornAfterStart = (() => {
|
|
231
|
+
try {
|
|
232
|
+
const bt = statSync(dir).birthtimeMs;
|
|
233
|
+
return !(bt > 0) || bt >= this._startedAt - 1000;
|
|
234
|
+
} catch { return true; }
|
|
235
|
+
})();
|
|
236
|
+
this._attach(dir, { suppressExisting: !bornAfterStart });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_journalMtime(dir) {
|
|
241
|
+
try { return statSync(join(dir, 'journal.jsonl')).mtimeMs; } catch { return 0; }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── per-session cursors ────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
_attach(dir, { suppressExisting }) {
|
|
247
|
+
if (this._sessions.has(dir)) return this._sessions.get(dir);
|
|
248
|
+
if (!existsSync(join(dir, 'journal.jsonl'))) return null;
|
|
249
|
+
const cur = {
|
|
250
|
+
dir,
|
|
251
|
+
synth: new SessionSynthesizer(dir, { deferMs: this._deferMs, now: this._now }),
|
|
252
|
+
// Per-session reconstructor (review P2): two concurrently-followed main
|
|
253
|
+
// producers in one project (a second ccv process, IM worker) must not
|
|
254
|
+
// rebase each other's accumulated baseline — v1's tail followed exactly
|
|
255
|
+
// one file, so a single shared reconstructor was a new interleave surface.
|
|
256
|
+
reconstructor: createIncrementalReconstructor(),
|
|
257
|
+
journal: { path: join(dir, 'journal.jsonl'), offset: 0, pending: '' },
|
|
258
|
+
responses: { path: join(dir, 'responses.jsonl'), offset: 0, pending: '' },
|
|
259
|
+
convFiles: new Map(), // path → {key, offset, pending}
|
|
260
|
+
watcher: null,
|
|
261
|
+
debounce: null,
|
|
262
|
+
reading: false,
|
|
263
|
+
dirty: false,
|
|
264
|
+
suppress: !!suppressExisting,
|
|
265
|
+
};
|
|
266
|
+
this._sessions.set(dir, cur);
|
|
267
|
+
this._seenDirs.set(dir, this._journalMtime(dir));
|
|
268
|
+
try {
|
|
269
|
+
cur.watcher = this._watchImpl(dir, () => this._scheduleRead(cur));
|
|
270
|
+
cur.watcher.on('error', (err) => {
|
|
271
|
+
reportSwallowed('v2-live.session-watch', err);
|
|
272
|
+
try { cur.watcher.close(); } catch { /* already closed */ }
|
|
273
|
+
cur.watcher = null; // safety poll keeps the cursor alive
|
|
274
|
+
});
|
|
275
|
+
} catch (err) {
|
|
276
|
+
reportSwallowed('v2-live.session-watch', err);
|
|
277
|
+
}
|
|
278
|
+
this._readCursor(cur); // seed (suppressed history or brand-new content)
|
|
279
|
+
cur.suppress = false;
|
|
280
|
+
return cur;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_closeCursor(cur) {
|
|
284
|
+
if (cur.debounce) { clearTimeout(cur.debounce); cur.debounce = null; }
|
|
285
|
+
if (cur.watcher) { try { cur.watcher.close(); } catch { /* already closed */ } cur.watcher = null; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_scheduleRead(cur) {
|
|
289
|
+
if (!this._active) return;
|
|
290
|
+
if (cur.debounce) return;
|
|
291
|
+
cur.debounce = setTimeout(() => {
|
|
292
|
+
cur.debounce = null;
|
|
293
|
+
this._readCursor(cur);
|
|
294
|
+
}, FSWATCH_DEBOUNCE_MS);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Read all new bytes of one session (conv events → responses → journal —
|
|
298
|
+
* the order that maximizes gate success), feed the synthesizer, emit. */
|
|
299
|
+
_readCursor(cur) {
|
|
300
|
+
if (cur.reading) { cur.dirty = true; return; }
|
|
301
|
+
cur.reading = true;
|
|
302
|
+
try {
|
|
303
|
+
do {
|
|
304
|
+
cur.dirty = false;
|
|
305
|
+
this._feedConvFiles(cur);
|
|
306
|
+
const respLines = readNewLines(cur.responses);
|
|
307
|
+
if (respLines === null) { this._rebuildCursor(cur); return; }
|
|
308
|
+
for (const raw of respLines) {
|
|
309
|
+
cur.synth.ingestResponseLine(raw);
|
|
310
|
+
// Wire v3 (V3.S4): forward the raw stored line — the flagged client
|
|
311
|
+
// assembles v1-shape entries from native lines locally. Suppressed
|
|
312
|
+
// during seed (history is covered by the cold-load channel).
|
|
313
|
+
if (this._wireV3 && !cur.suppress) this._forwardNative(cur, 'resp', null, raw);
|
|
314
|
+
}
|
|
315
|
+
const journalLines = readNewLines(cur.journal);
|
|
316
|
+
if (journalLines === null) { this._rebuildCursor(cur); return; }
|
|
317
|
+
for (const raw of journalLines) {
|
|
318
|
+
let line = null;
|
|
319
|
+
try { line = JSON.parse(raw); } catch (err) {
|
|
320
|
+
reportSwallowed('v2-live.journal-parse', new Error(`${cur.dir}: ${err.message}`));
|
|
321
|
+
}
|
|
322
|
+
if (line) cur.synth.ingestJournalLine(line);
|
|
323
|
+
}
|
|
324
|
+
this._emitDrained(cur);
|
|
325
|
+
} while (cur.dirty);
|
|
326
|
+
} finally {
|
|
327
|
+
cur.reading = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_feedConvFiles(cur) {
|
|
332
|
+
const convRoot = join(cur.dir, 'conversations');
|
|
333
|
+
if (!existsSync(convRoot)) return;
|
|
334
|
+
let keys = [];
|
|
335
|
+
try {
|
|
336
|
+
keys = readdirSync(convRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
337
|
+
} catch {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const key of keys) {
|
|
341
|
+
let files = [];
|
|
342
|
+
try {
|
|
343
|
+
files = readdirSync(join(convRoot, key)).filter((f) => /^e\d+\.jsonl$/.test(f));
|
|
344
|
+
} catch {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// Numeric epoch order. NB: file order is NOT guaranteed to be globally
|
|
348
|
+
// seq-ordered (a pre-fix writer restart appended newer seqs into an
|
|
349
|
+
// older epoch file) — the synthesizer's ingestConvLine keeps its event
|
|
350
|
+
// window seq-sorted on insert, so feed order only needs to be stable.
|
|
351
|
+
files.sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0]));
|
|
352
|
+
for (const f of files) {
|
|
353
|
+
const p = join(convRoot, key, f);
|
|
354
|
+
let fc = cur.convFiles.get(p);
|
|
355
|
+
if (!fc) {
|
|
356
|
+
fc = { key, path: p, offset: 0, pending: '' };
|
|
357
|
+
cur.convFiles.set(p, fc);
|
|
358
|
+
}
|
|
359
|
+
const lines = readNewLines(fc);
|
|
360
|
+
if (lines === null) { this._rebuildCursor(cur); return; }
|
|
361
|
+
for (const raw of lines) {
|
|
362
|
+
let ev = null;
|
|
363
|
+
try { ev = JSON.parse(raw); } catch (err) {
|
|
364
|
+
reportSwallowed('v2-live.conv-parse', new Error(`${cur.dir}/${key}: ${err.message}`));
|
|
365
|
+
}
|
|
366
|
+
if (ev) {
|
|
367
|
+
cur.synth.ingestConvLine(key, ev);
|
|
368
|
+
if (this._wireV3 && !cur.suppress) this._forwardNative(cur, 'conv', key, raw);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Append-only invariant broke (shrunk file): rebuild the whole cursor with
|
|
376
|
+
* a fresh synthesizer, replaying silently to the current state. */
|
|
377
|
+
_rebuildCursor(cur) {
|
|
378
|
+
reportSwallowed('v2-live.cursor-rebuild', new Error(`${cur.dir}: file shrank — rebuilding cursor`));
|
|
379
|
+
this._closeCursor(cur);
|
|
380
|
+
this._sessions.delete(cur.dir);
|
|
381
|
+
const fresh = this._attach(cur.dir, { suppressExisting: true });
|
|
382
|
+
if (fresh) this._scheduleRead(fresh);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
_emitDrained(cur) {
|
|
386
|
+
cur.synth.sweepDeadlines();
|
|
387
|
+
const items = cur.synth.drain();
|
|
388
|
+
if (items.length === 0) return;
|
|
389
|
+
if (cur.suppress) {
|
|
390
|
+
// Seed phase: history is already covered by the cold-load channel, so
|
|
391
|
+
// nothing is broadcast — but the shared reconstructor still consumes it,
|
|
392
|
+
// giving later deltas a baseline (v1's tail restarted with no baseline
|
|
393
|
+
// and passed bare slices through; the cold reload had to cover them).
|
|
394
|
+
for (const item of items) {
|
|
395
|
+
try {
|
|
396
|
+
cur.reconstructor.reconstruct(JSON.parse(JSON.stringify(item.entry)));
|
|
397
|
+
} catch (err) {
|
|
398
|
+
reportSwallowed('v2-live.seed', err);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let emitted = 0;
|
|
404
|
+
for (const item of items) {
|
|
405
|
+
let parsed = null;
|
|
406
|
+
try {
|
|
407
|
+
// JSON round-trip: downstream (reconstructor/enrichers) mutate entries
|
|
408
|
+
// in place — the synthesizer's own objects must stay pristine so the
|
|
409
|
+
// placeholder→completed mutation stays byte-true.
|
|
410
|
+
parsed = JSON.parse(JSON.stringify(item.entry));
|
|
411
|
+
} catch (err) {
|
|
412
|
+
reportSwallowed('v2-live.emit', err);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
processWatchedEntry(parsed, {
|
|
417
|
+
reconstructor: cur.reconstructor,
|
|
418
|
+
clients: this._clients,
|
|
419
|
+
getClaudePid: this._getClaudePid,
|
|
420
|
+
runParallelHook: this._runParallelHook,
|
|
421
|
+
suppressEntryBroadcast: this._wireV3,
|
|
422
|
+
});
|
|
423
|
+
emitted++;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
reportSwallowed('v2-live.emit', err);
|
|
426
|
+
}
|
|
427
|
+
// Wire v3: derive the metadata row from the post-reconstruction entry
|
|
428
|
+
// (main deltas carry full messages here — classification/cacheLoss get
|
|
429
|
+
// the same inputs the legacy list sees). Row-level errors must not
|
|
430
|
+
// disturb the legacy broadcast above.
|
|
431
|
+
if (this._wireV3) {
|
|
432
|
+
try {
|
|
433
|
+
this._emitRow(cur, item, parsed);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
reportSwallowed('v2-live.row', err);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (emitted > 0 && this._notifyStatsWorker) {
|
|
440
|
+
try { this._notifyStatsWorker(cur.dir); } catch (err) { reportSwallowed('v2-live.stats-notify', err); }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Wire v3 (V3.S4): broadcast one raw native line (conv event or responses
|
|
445
|
+
* line) — the flagged client's assembler replays these into v1-shape
|
|
446
|
+
* entries. `raw` is forwarded verbatim inside a JSON envelope carrying the
|
|
447
|
+
* session identity + channel. sessionId comes from the synthesizer (the
|
|
448
|
+
* UUID, matching the rows' sessionId and the entries' _seqEpoch). */
|
|
449
|
+
_forwardNative(cur, kind, channel, raw) {
|
|
450
|
+
try {
|
|
451
|
+
const payload = kind === 'conv'
|
|
452
|
+
? `{"sessionId":${JSON.stringify(cur.synth.sessionId)},"channel":${JSON.stringify(channel)},"line":${raw}}`
|
|
453
|
+
: `{"sessionId":${JSON.stringify(cur.synth.sessionId)},"line":${raw}}`;
|
|
454
|
+
const eventName = kind === 'conv' ? 'v3_conv' : 'v3_resp';
|
|
455
|
+
sendEventRawToClients(this._clients, eventName, payload);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
reportSwallowed('v2-live.native-forward', err);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Wire v3 (V3.S2): one metadata row per emitted item, derived from the
|
|
462
|
+
* post-reconstruction entry. Rows upsert client-side by (sessionId, seq):
|
|
463
|
+
* a placeholder row precedes its completed row, and a correction re-send
|
|
464
|
+
* follows when the NEXT request's arrival changes the previous row's
|
|
465
|
+
* classification (classifyRequest's Preflight/Plan cases read nextReq). */
|
|
466
|
+
_rowFrom(item, parsed) {
|
|
467
|
+
const usage = parsed.response?.body?.usage || null;
|
|
468
|
+
return {
|
|
469
|
+
seq: item.seq,
|
|
470
|
+
sessionId: item.sessionId,
|
|
471
|
+
timestamp: parsed.timestamp || '',
|
|
472
|
+
url: parsed.url || '',
|
|
473
|
+
method: parsed.method || 'POST',
|
|
474
|
+
kind: item.isMain ? 'main' : (parsed.teammate ? 'teammate' : 'sub'),
|
|
475
|
+
mainAgent: parsed.mainAgent === true,
|
|
476
|
+
teammate: parsed.teammate || undefined,
|
|
477
|
+
model: parsed.body?.model,
|
|
478
|
+
proxyUrl: parsed.proxyUrl || undefined,
|
|
479
|
+
status: parsed.response?.status,
|
|
480
|
+
duration: parsed.duration,
|
|
481
|
+
usage: usage ? {
|
|
482
|
+
input_tokens: usage.input_tokens || 0,
|
|
483
|
+
output_tokens: usage.output_tokens || 0,
|
|
484
|
+
cache_read_input_tokens: usage.cache_read_input_tokens || 0,
|
|
485
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens || 0,
|
|
486
|
+
} : null,
|
|
487
|
+
inProgress: !!parsed.inProgress,
|
|
488
|
+
typeTag: null,
|
|
489
|
+
cacheLoss: null,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
_emitRow(cur, item, parsed) {
|
|
494
|
+
const row = this._rowFrom(item, parsed);
|
|
495
|
+
try {
|
|
496
|
+
const tag = classifyRequest(parsed, null);
|
|
497
|
+
row.typeTag = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
498
|
+
} catch (err) { reportSwallowed('v2-live.row-classify', err); }
|
|
499
|
+
if (row.mainAgent && !row.inProgress) {
|
|
500
|
+
const u = parsed.response?.body?.usage;
|
|
501
|
+
const cw = (u && u.cache_creation_input_tokens) || 0;
|
|
502
|
+
const cr = (u && u.cache_read_input_tokens) || 0;
|
|
503
|
+
if (cur._v3PrevMain && cw > 0 && cw > cr) {
|
|
504
|
+
try { row.cacheLoss = computeCacheLoss(cur._v3PrevMain, parsed); } catch (err) { reportSwallowed('v2-live.row-cacheloss', err); }
|
|
505
|
+
}
|
|
506
|
+
cur._v3PrevMain = parsed; // one full entry retained per cursor (bounded)
|
|
507
|
+
}
|
|
508
|
+
// Lookahead correction: this entry is the previous row's nextReq.
|
|
509
|
+
const prev = cur._v3Last;
|
|
510
|
+
if (prev && (prev.row.sessionId !== row.sessionId || prev.row.seq !== row.seq)) {
|
|
511
|
+
try {
|
|
512
|
+
const tag = classifyRequest(prev.entry, parsed);
|
|
513
|
+
const corrected = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
514
|
+
if (JSON.stringify(corrected) !== JSON.stringify(prev.row.typeTag)) {
|
|
515
|
+
prev.row.typeTag = corrected;
|
|
516
|
+
sendEventToClients(this._clients, 'v2_requests_delta', prev.row);
|
|
517
|
+
}
|
|
518
|
+
} catch (err) { reportSwallowed('v2-live.row-classify', err); }
|
|
519
|
+
}
|
|
520
|
+
cur._v3Last = { row, entry: parsed };
|
|
521
|
+
sendEventToClients(this._clients, 'v2_requests_delta', row);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ── safety poll ────────────────────────────────────────────────────────────
|
|
525
|
+
|
|
526
|
+
_safetyTick() {
|
|
527
|
+
if (!this._active) return;
|
|
528
|
+
this._armRootWatcher(); // re-arm after errors / late directory creation
|
|
529
|
+
// Attached sessions: unconditional slow re-read (fs.watch is lossy).
|
|
530
|
+
for (const cur of [...this._sessions.values()]) {
|
|
531
|
+
this._readCursor(cur);
|
|
532
|
+
}
|
|
533
|
+
// Unattached dirs: attach brand-new ones (missed root events) and
|
|
534
|
+
// re-attach previously idle ones whose journal moved again.
|
|
535
|
+
if (!existsSync(this._sessionsRoot)) return;
|
|
536
|
+
let names = [];
|
|
537
|
+
try {
|
|
538
|
+
names = readdirSync(this._sessionsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
539
|
+
} catch {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
for (const name of names) {
|
|
543
|
+
const dir = join(this._sessionsRoot, name);
|
|
544
|
+
if (this._sessions.has(dir)) continue;
|
|
545
|
+
const mtime = this._journalMtime(dir);
|
|
546
|
+
if (mtime === 0) continue;
|
|
547
|
+
const seen = this._seenDirs.get(dir);
|
|
548
|
+
if (seen === undefined) {
|
|
549
|
+
this._maybeAttachNew(dir);
|
|
550
|
+
} else if (mtime > seen) {
|
|
551
|
+
// A dir skipped at the initial scan (stale then) has resumed activity:
|
|
552
|
+
// follow it now, history suppressed (it predates this feed).
|
|
553
|
+
this._seenDirs.set(dir, mtime);
|
|
554
|
+
this._attach(dir, { suppressExisting: true });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|