cc-viewer 1.7.0 → 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/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-qfRsRth0.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/server/lib/log-watcher.js +29 -3
- package/server/lib/v2/adapter.js +71 -6
- package/server/lib/v2/live-feed.js +110 -3
- package/server/lib/v2/meta-rows.js +360 -0
- package/server/lib/v2/v2-writer.js +15 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +96 -26
- package/server/routes/logs.js +15 -12
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +7 -6
- package/server/server.js +23 -1
- 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-DTkacfRW.js +0 -2
- package/dist/assets/index-Ac8vnV3v.js +0 -2
- package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +0 -2
package/dist/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
|
|
22
22
|
// electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
|
|
23
23
|
</script>
|
|
24
|
-
<script type="module" crossorigin src="./assets/index-
|
|
24
|
+
<script type="module" crossorigin src="./assets/index-B51Ci0GL.js"></script>
|
|
25
25
|
<link rel="modulepreload" crossorigin href="./assets/vendor-antd-DI7JL-mE.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-B9c49dtM.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-B6cpBtIE.js">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"findcc.js",
|
|
67
67
|
"interceptor.js",
|
|
68
68
|
"server/",
|
|
69
|
+
"src/utils/",
|
|
69
70
|
"plugins/",
|
|
70
71
|
"concepts/",
|
|
71
72
|
"ultraAgents/"
|
|
@@ -4,6 +4,7 @@ import { buildContextWindowEvent, getContextSizeForModel } from './context-watch
|
|
|
4
4
|
import { reconstructEntries } from './delta-reconstructor.js';
|
|
5
5
|
import { enrichEntry } from './enrich-plan-input.js';
|
|
6
6
|
import { enrichEntry as enrichWorkflowEntry } from './enrich-workflow.js';
|
|
7
|
+
import { sseWrite } from './wire-compress.js';
|
|
7
8
|
|
|
8
9
|
// 1.7.0: the v1 log-file tail (fs.watch + byte cursors + rotation follow)
|
|
9
10
|
// retired with the v1 write path — the live channel is server/lib/v2/
|
|
@@ -60,7 +61,11 @@ function _safeSseWrite(clients, client, payload) {
|
|
|
60
61
|
}
|
|
61
62
|
let ok;
|
|
62
63
|
try {
|
|
63
|
-
|
|
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);
|
|
64
69
|
} catch {
|
|
65
70
|
_removeClient(clients, client);
|
|
66
71
|
return false;
|
|
@@ -68,9 +73,18 @@ function _safeSseWrite(clients, client, payload) {
|
|
|
68
73
|
if (!ok) {
|
|
69
74
|
if (!client._sseBackpressureSince) {
|
|
70
75
|
client._sseBackpressureSince = Date.now();
|
|
71
|
-
|
|
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; });
|
|
72
83
|
} else if (Date.now() - client._sseBackpressureSince > SSE_BACKPRESSURE_TIMEOUT_MS) {
|
|
73
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 {}
|
|
74
88
|
try { client.end(); } catch {}
|
|
75
89
|
return false;
|
|
76
90
|
}
|
|
@@ -92,6 +106,15 @@ export function sendEventToClients(clients, eventName, data) {
|
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|
|
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`;
|
|
113
|
+
for (let i = clients.length - 1; i >= 0; i--) {
|
|
114
|
+
_safeSseWrite(clients, clients[i], payload);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
export function sendChunkToClients(clients, dataJson) {
|
|
96
119
|
const payload = `event: load_chunk\ndata: ${dataJson}\n\n`;
|
|
97
120
|
for (let i = clients.length - 1; i >= 0; i--) {
|
|
@@ -115,7 +138,10 @@ export function processWatchedEntry(parsed, ctx) {
|
|
|
115
138
|
reconstructor.reconstruct(parsed);
|
|
116
139
|
try { enrichEntry(parsed); } catch {}
|
|
117
140
|
try { enrichWorkflowEntry(parsed); } catch {}
|
|
118
|
-
|
|
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);
|
|
119
145
|
runParallelHook('onNewEntry', parsed).catch(() => {});
|
|
120
146
|
if (isMainAgentEntry(parsed) && !parsed.inProgress) {
|
|
121
147
|
const cached = extractCachedContent(parsed);
|
package/server/lib/v2/adapter.js
CHANGED
|
@@ -402,12 +402,30 @@ export class SessionSynthesizer {
|
|
|
402
402
|
// ---- body (blob backfill is per-request by journal ref — never carried) --
|
|
403
403
|
const tools = this._loadBlob(req.blobs && req.blobs.tools);
|
|
404
404
|
const system = this._loadBlob(req.blobs && req.blobs.sys);
|
|
405
|
+
// req.params carries every residual body field (params-era journals);
|
|
406
|
+
// dedicated fields override it so precedence is deterministic. user_id is
|
|
407
|
+
// ALWAYS meta.userIdRaw — the client does equality-based session-boundary
|
|
408
|
+
// detection on it, and a `-c` adopted folder mixes real user_ids that
|
|
409
|
+
// would otherwise split the timeline mid-session.
|
|
410
|
+
const params = req.params && typeof req.params === 'object' ? req.params : null;
|
|
405
411
|
const body = {
|
|
412
|
+
...params,
|
|
406
413
|
...(req.model && { model: req.model }),
|
|
407
414
|
...(system !== undefined && { system }),
|
|
408
415
|
...(tools !== undefined && { tools }),
|
|
409
|
-
...(meta.userIdRaw && { metadata: { user_id: meta.userIdRaw } }),
|
|
410
416
|
};
|
|
417
|
+
if (meta.userIdRaw) {
|
|
418
|
+
const extra = body.metadata && typeof body.metadata === 'object' ? body.metadata : null;
|
|
419
|
+
body.metadata = { ...extra, user_id: meta.userIdRaw };
|
|
420
|
+
} else if (body.metadata && typeof body.metadata === 'object' && body.metadata.user_id !== undefined) {
|
|
421
|
+
// noid session (no parseable user_id at write time): a raw params
|
|
422
|
+
// user_id may vary per request, and surfacing it would re-open the
|
|
423
|
+
// spurious client boundary-split this block exists to prevent. Drop
|
|
424
|
+
// ONLY user_id; other metadata keys keep full fidelity.
|
|
425
|
+
const { user_id, ...restMeta } = body.metadata;
|
|
426
|
+
if (Object.keys(restMeta).length > 0) body.metadata = restMeta;
|
|
427
|
+
else delete body.metadata;
|
|
428
|
+
}
|
|
411
429
|
|
|
412
430
|
const isTeammateEntry = isTeammate;
|
|
413
431
|
const isMainKind = isMain;
|
|
@@ -523,6 +541,7 @@ export class SessionSynthesizer {
|
|
|
523
541
|
}
|
|
524
542
|
entry.response = {
|
|
525
543
|
...(typeof done.http === 'number' && { status: done.http }),
|
|
544
|
+
...(resp && resp.statusText && { statusText: resp.statusText }),
|
|
526
545
|
...(resp && resp.headers && { headers: resp.headers }),
|
|
527
546
|
body: resp ? resp.body : null,
|
|
528
547
|
};
|
|
@@ -646,7 +665,7 @@ export function findTeammateSessionDirs(sessionDir, leaderUuid) {
|
|
|
646
665
|
* Item-level iteration of one v2 session with teammate re-join — the shared
|
|
647
666
|
* core of the raw-string generators and the window reader below.
|
|
648
667
|
*/
|
|
649
|
-
function* iterateV2Items(sessionDir, opts = {}) {
|
|
668
|
+
export function* iterateV2Items(sessionDir, opts = {}) {
|
|
650
669
|
const streams = [iterateSessionItems(sessionDir, opts)];
|
|
651
670
|
const ownMeta = (() => {
|
|
652
671
|
try { return JSON.parse(readFileSync(join(sessionDir, 'meta.json'), 'utf-8')); } catch { return null; }
|
|
@@ -735,7 +754,7 @@ export function _v2WindowStatsForTest(reset = false) {
|
|
|
735
754
|
* The result is IMMUTABLE and shared across coalesced callers — window
|
|
736
755
|
* selection must not mutate `recs`/`ring`.
|
|
737
756
|
*/
|
|
738
|
-
async function scanV2Descriptors(sessionDir) {
|
|
757
|
+
export async function scanV2Descriptors(sessionDir) {
|
|
739
758
|
_windowStats.scanRuns++;
|
|
740
759
|
const dedup = new Map(); // key → descriptor (insertion order = output order)
|
|
741
760
|
const ring = []; // newest ≤3 isMain descriptors, pre-dedup iteration order
|
|
@@ -796,7 +815,7 @@ function selectV2Window(scan, opts = {}) {
|
|
|
796
815
|
|
|
797
816
|
/** Level-1 shared Pass A + per-caller selection. `cached` gates the TTL
|
|
798
817
|
* micro-cache read (F5b: /events live-attach must never consume it). */
|
|
799
|
-
async function computeV2Window(sessionDir, opts = {}) {
|
|
818
|
+
export async function computeV2Window(sessionDir, opts = {}) {
|
|
800
819
|
const scan = await _scanFlight.run(`scan:${sessionDir}`, () => scanV2Descriptors(sessionDir), { cached: !!opts.cached });
|
|
801
820
|
return selectV2Window(scan, opts);
|
|
802
821
|
}
|
|
@@ -813,7 +832,7 @@ async function computeV2Window(sessionDir, opts = {}) {
|
|
|
813
832
|
* equivalent (algorithm review F6). `collect` mode fills a slot array in
|
|
814
833
|
* Pass A output order instead: byte-identical to the historical single pass.
|
|
815
834
|
*/
|
|
816
|
-
async function materializeV2Window(sessionDir, win, { collect = false, onEntry = null } = {}) {
|
|
835
|
+
async function materializeV2Window(sessionDir, win, { collect = false, onEntry = null, promoteKeys = null } = {}) {
|
|
817
836
|
const memberIdx = new Map();
|
|
818
837
|
win.members.forEach((d, i) => memberIdx.set(itemKey(d.sessionId, d.seq), i));
|
|
819
838
|
const ringKeys = new Set(win.ring.map((d) => itemKey(d.sessionId, d.seq)));
|
|
@@ -837,7 +856,12 @@ async function materializeV2Window(sessionDir, win, { collect = false, onEntry =
|
|
|
837
856
|
// replayed state (item.stateRef aliases the live conversation state at
|
|
838
857
|
// this seq — used transiently, never retained). Same construction as the
|
|
839
858
|
// historical end-of-window rebuild, byte-for-byte.
|
|
840
|
-
|
|
859
|
+
// `promoteKeys` (V3.S1): per-member force-checkpoint for single-entry
|
|
860
|
+
// reads — a mid-session main delta carries only its appended slice, but
|
|
861
|
+
// the detail view needs the full replayed state for BOTH the target and
|
|
862
|
+
// its prevMain (Body Diff / cacheLoss compare whole messages arrays).
|
|
863
|
+
// The window baseline promotes only win.baselineKey (the window start).
|
|
864
|
+
if (k === win.baselineKey || (promoteKeys && promoteKeys.has(k))) {
|
|
841
865
|
const state = item.stateRef || [];
|
|
842
866
|
const rebuilt = { ...item.entry, body: { ...item.entry.body, messages: state } };
|
|
843
867
|
rebuilt._isCheckpoint = true;
|
|
@@ -889,6 +913,47 @@ async function materializeV2Window(sessionDir, win, { collect = false, onEntry =
|
|
|
889
913
|
* @param {{limit?: number, before?: string|null, cached?: boolean}} [opts]
|
|
890
914
|
* @returns {Promise<{entries: string[], hasMore: boolean, oldestTimestamp: string, totalCount: number, mainAgentRing: string[]}>}
|
|
891
915
|
*/
|
|
916
|
+
/**
|
|
917
|
+
* V3.S1: on-demand single-entry read — the detail view's data source once the
|
|
918
|
+
* request list is metadata rows. Locates the target (sessionId, seq) in the
|
|
919
|
+
* shared descriptor scan, pairs it with its preceding isMain descriptor
|
|
920
|
+
* (Body Diff / Context tab need the previous mainAgent, raw-seq adjacency),
|
|
921
|
+
* and materializes exactly those members with per-member checkpoint promotion
|
|
922
|
+
* (main deltas come back with the full replayed state, matching what the
|
|
923
|
+
* client holds after reconstruction on the legacy channel).
|
|
924
|
+
*
|
|
925
|
+
* @param {string} sessionDir - leader session dir (teammate rows resolve
|
|
926
|
+
* through the leader's k-way fold so tags/order match the list)
|
|
927
|
+
* @param {{seq: number, sessionId?: string|null, cached?: boolean}} opts -
|
|
928
|
+
* sessionId (UUID) disambiguates teammate rows; null = any session's seq
|
|
929
|
+
* @returns {Promise<{entry: string, prevMain: string|null}|null>} raw JSON
|
|
930
|
+
* strings (never re-parsed here), or null when the seq is unknown / gated
|
|
931
|
+
* out by synthesis (crash-orphan) — callers answer 404.
|
|
932
|
+
*/
|
|
933
|
+
export async function readV2SingleEntry(sessionDir, opts = {}) {
|
|
934
|
+
const seq = opts.seq;
|
|
935
|
+
const sessionId = opts.sessionId || null;
|
|
936
|
+
const key = `entry:${sessionDir}|${sessionId || ''}|${seq}`;
|
|
937
|
+
return _windowFlight.run(key, async () => {
|
|
938
|
+
const scan = await _scanFlight.run(`scan:${sessionDir}`, () => scanV2Descriptors(sessionDir), { cached: true });
|
|
939
|
+
const recs = scan.recs;
|
|
940
|
+
const idx = recs.findIndex((d) => d.seq === seq && (!sessionId || d.sessionId === sessionId));
|
|
941
|
+
if (idx === -1) return null;
|
|
942
|
+
const target = recs[idx];
|
|
943
|
+
let prevMain = null;
|
|
944
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
945
|
+
if (recs[i].isMain) { prevMain = recs[i]; break; }
|
|
946
|
+
}
|
|
947
|
+
const members = prevMain ? [prevMain, target] : [target];
|
|
948
|
+
const win = { members, ring: [], baselineKey: null };
|
|
949
|
+
const promoteKeys = new Set(members.filter((d) => d.isMainDelta).map((d) => itemKey(d.sessionId, d.seq)));
|
|
950
|
+
const { slots } = await materializeV2Window(sessionDir, win, { collect: true, promoteKeys });
|
|
951
|
+
const entry = slots[members.length - 1];
|
|
952
|
+
if (entry === undefined) return null; // synthesis gate dropped it between passes
|
|
953
|
+
return { entry, prevMain: prevMain ? (slots[0] ?? null) : null };
|
|
954
|
+
}, { cached: opts.cached !== false });
|
|
955
|
+
}
|
|
956
|
+
|
|
892
957
|
export async function readV2WindowedEntries(sessionDir, opts = {}) {
|
|
893
958
|
const cached = !!opts.cached;
|
|
894
959
|
const key = `win:${sessionDir}|${opts.limit || 0}|${opts.before || ''}`;
|
|
@@ -26,8 +26,10 @@ import { watch, existsSync, readdirSync, statSync, openSync, readSync, closeSync
|
|
|
26
26
|
import { join } from 'node:path';
|
|
27
27
|
import { reportSwallowed } from '../error-report.js';
|
|
28
28
|
import { createIncrementalReconstructor } from '../delta-reconstructor.js';
|
|
29
|
-
import { processWatchedEntry } from '../log-watcher.js';
|
|
29
|
+
import { processWatchedEntry, sendEventToClients, sendEventRawToClients } from '../log-watcher.js';
|
|
30
30
|
import { SessionSynthesizer } from './adapter.js';
|
|
31
|
+
import { computeCacheLoss } from './meta-rows.js';
|
|
32
|
+
import { classifyRequest } from '../../../src/utils/requestType.js';
|
|
31
33
|
|
|
32
34
|
const FSWATCH_DEBOUNCE_MS = 80;
|
|
33
35
|
const SAFETY_POLL_MS = 5000;
|
|
@@ -91,6 +93,10 @@ export class V2LiveFeed {
|
|
|
91
93
|
this._safetyPollMs = typeof opts.safetyPollMs === 'number' ? opts.safetyPollMs : SAFETY_POLL_MS;
|
|
92
94
|
this._deferMs = typeof opts.deferMs === 'number' ? opts.deferMs : DEFER_MS;
|
|
93
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;
|
|
94
100
|
this._sessions = new Map(); // dir → cursor bundle
|
|
95
101
|
this._seenDirs = new Map(); // dir → last observed journal mtimeMs (attached or not)
|
|
96
102
|
this._sessionsRoot = null;
|
|
@@ -299,7 +305,13 @@ export class V2LiveFeed {
|
|
|
299
305
|
this._feedConvFiles(cur);
|
|
300
306
|
const respLines = readNewLines(cur.responses);
|
|
301
307
|
if (respLines === null) { this._rebuildCursor(cur); return; }
|
|
302
|
-
for (const raw of respLines)
|
|
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
|
+
}
|
|
303
315
|
const journalLines = readNewLines(cur.journal);
|
|
304
316
|
if (journalLines === null) { this._rebuildCursor(cur); return; }
|
|
305
317
|
for (const raw of journalLines) {
|
|
@@ -351,7 +363,10 @@ export class V2LiveFeed {
|
|
|
351
363
|
try { ev = JSON.parse(raw); } catch (err) {
|
|
352
364
|
reportSwallowed('v2-live.conv-parse', new Error(`${cur.dir}/${key}: ${err.message}`));
|
|
353
365
|
}
|
|
354
|
-
if (ev)
|
|
366
|
+
if (ev) {
|
|
367
|
+
cur.synth.ingestConvLine(key, ev);
|
|
368
|
+
if (this._wireV3 && !cur.suppress) this._forwardNative(cur, 'conv', key, raw);
|
|
369
|
+
}
|
|
355
370
|
}
|
|
356
371
|
}
|
|
357
372
|
}
|
|
@@ -403,17 +418,109 @@ export class V2LiveFeed {
|
|
|
403
418
|
clients: this._clients,
|
|
404
419
|
getClaudePid: this._getClaudePid,
|
|
405
420
|
runParallelHook: this._runParallelHook,
|
|
421
|
+
suppressEntryBroadcast: this._wireV3,
|
|
406
422
|
});
|
|
407
423
|
emitted++;
|
|
408
424
|
} catch (err) {
|
|
409
425
|
reportSwallowed('v2-live.emit', err);
|
|
410
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
|
+
}
|
|
411
438
|
}
|
|
412
439
|
if (emitted > 0 && this._notifyStatsWorker) {
|
|
413
440
|
try { this._notifyStatsWorker(cur.dir); } catch (err) { reportSwallowed('v2-live.stats-notify', err); }
|
|
414
441
|
}
|
|
415
442
|
}
|
|
416
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
|
+
|
|
417
524
|
// ── safety poll ────────────────────────────────────────────────────────────
|
|
418
525
|
|
|
419
526
|
_safetyTick() {
|