cc-viewer 1.7.0 → 1.7.2
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-B224r-8M.js +2 -0
- package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-BUubiE6Y.js} +1 -1
- package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-CKmfRRvo.js} +1 -1
- package/dist/assets/index-CjsQEQc2.js +2 -0
- package/dist/assets/seqResourceLoaders-C-T_EMiO.js +2 -0
- package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/server/lib/log-management.js +1 -0
- package/server/lib/log-watcher.js +29 -3
- package/server/lib/stats-worker.js +21 -2
- package/server/lib/v2/adapter.js +123 -18
- package/server/lib/v2/journal.js +9 -5
- package/server/lib/v2/jsonl-read.js +127 -0
- package/server/lib/v2/live-feed.js +222 -20
- package/server/lib/v2/meta-rows.js +361 -0
- package/server/lib/v2/replay.js +5 -5
- package/server/lib/v2/session-select.js +69 -2
- 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/im.js +1 -1
- 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/server/workspace-registry.js +5 -0
- 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
|
@@ -26,8 +26,11 @@ 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 { isDiscardableSession } from './session-select.js';
|
|
32
|
+
import { computeCacheLoss } from './meta-rows.js';
|
|
33
|
+
import { classifyRequest } from '../../../src/utils/requestType.js';
|
|
31
34
|
|
|
32
35
|
const FSWATCH_DEBOUNCE_MS = 80;
|
|
33
36
|
const SAFETY_POLL_MS = 5000;
|
|
@@ -36,11 +39,26 @@ const ATTACH_RECENT_MS = 5 * 60 * 1000; // pre-existing dirs considered "live"
|
|
|
36
39
|
const TICK_RETRY_MS = 250; // in-process tick raced the first queue drain
|
|
37
40
|
const TICK_RETRY_MAX = 8;
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const READ_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
43
|
+
// Node's max string length (~512MiB) — a longer line can never be decoded.
|
|
44
|
+
const MAX_LINE_BYTES = 0x1fffffe8;
|
|
45
|
+
|
|
46
|
+
/** Read newly appended complete lines from a file cursor {path, offset}
|
|
47
|
+
* (partial-line carry lives on lazily-initialized _pendBufs/_pendBytes/
|
|
48
|
+
* _skipLine cursor fields). Returns an array of raw line strings (possibly
|
|
49
|
+
* empty). A file
|
|
41
50
|
* that shrank (should never happen — v2 files are append-only) returns null
|
|
42
|
-
* so the caller can rebuild the cursor.
|
|
43
|
-
|
|
51
|
+
* so the caller can rebuild the cursor.
|
|
52
|
+
*
|
|
53
|
+
* Chunked + byte-level newline split (issue #129 twin): the first attach to
|
|
54
|
+
* a session seeds from offset 0, so one whole-file read + decode would throw
|
|
55
|
+
* ERR_STRING_TOO_LONG on an oversized journal and crash-loop the boot path
|
|
56
|
+
* exactly like the cold-scan crash the streaming reader fixed. The partial
|
|
57
|
+
* tail line is carried on the cursor as Buffer fragments (never decoded
|
|
58
|
+
* until its newline lands); a single line past the string cap is skipped
|
|
59
|
+
* with a report instead of thrown. Exported for tests (chunkBytes seam).
|
|
60
|
+
*/
|
|
61
|
+
export function readNewLines(cursor, chunkBytes = READ_CHUNK_BYTES, maxLineBytes = MAX_LINE_BYTES) {
|
|
44
62
|
let size;
|
|
45
63
|
try {
|
|
46
64
|
size = statSync(cursor.path).size;
|
|
@@ -49,23 +67,74 @@ function readNewLines(cursor) {
|
|
|
49
67
|
}
|
|
50
68
|
if (size < cursor.offset) return null;
|
|
51
69
|
if (size === cursor.offset) return [];
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
if (cursor._pendBufs === undefined) {
|
|
71
|
+
cursor._pendBufs = [];
|
|
72
|
+
cursor._pendBytes = 0;
|
|
73
|
+
cursor._skipLine = false;
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
54
76
|
let fd;
|
|
55
77
|
try {
|
|
56
78
|
fd = openSync(cursor.path, 'r');
|
|
57
|
-
|
|
79
|
+
const chunk = Buffer.alloc(Math.min(chunkBytes, size - cursor.offset));
|
|
80
|
+
while (cursor.offset < size) {
|
|
81
|
+
const toRead = Math.min(chunk.length, size - cursor.offset);
|
|
82
|
+
const n = readSync(fd, chunk, 0, toRead, cursor.offset);
|
|
83
|
+
if (n === 0) break;
|
|
84
|
+
cursor.offset += n;
|
|
85
|
+
const view = chunk.subarray(0, n);
|
|
86
|
+
let from = 0;
|
|
87
|
+
while (from < n) {
|
|
88
|
+
const nl = view.indexOf(10, from);
|
|
89
|
+
if (nl === -1) {
|
|
90
|
+
if (!cursor._skipLine) {
|
|
91
|
+
const restLen = n - from;
|
|
92
|
+
if (cursor._pendBytes + restLen > maxLineBytes) {
|
|
93
|
+
cursor._skipLine = true;
|
|
94
|
+
cursor._pendBufs = [];
|
|
95
|
+
cursor._pendBytes = 0;
|
|
96
|
+
reportSwallowed('v2-live.read-line-too-long', new Error(`${cursor.path}: line exceeds ${maxLineBytes} bytes — skipped`));
|
|
97
|
+
} else {
|
|
98
|
+
// chunk is reused next readSync — the carried slice must own its bytes
|
|
99
|
+
cursor._pendBufs.push(Buffer.from(view.subarray(from)));
|
|
100
|
+
cursor._pendBytes += restLen;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (cursor._skipLine) {
|
|
106
|
+
cursor._skipLine = false; // the oversized line ends at this newline
|
|
107
|
+
} else {
|
|
108
|
+
const seg = view.subarray(from, nl);
|
|
109
|
+
let text = null;
|
|
110
|
+
try {
|
|
111
|
+
if (cursor._pendBufs.length > 0) {
|
|
112
|
+
cursor._pendBufs.push(seg);
|
|
113
|
+
text = Buffer.concat(cursor._pendBufs).toString('utf-8');
|
|
114
|
+
} else {
|
|
115
|
+
text = seg.toString('utf-8');
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
reportSwallowed('v2-live.read-line-too-long', err);
|
|
119
|
+
}
|
|
120
|
+
cursor._pendBufs = [];
|
|
121
|
+
cursor._pendBytes = 0;
|
|
122
|
+
if (text !== null) {
|
|
123
|
+
const t = text.trim();
|
|
124
|
+
if (t) out.push(t);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
from = nl + 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
58
130
|
} catch (err) {
|
|
131
|
+
// Deliver what was already parsed; offset only advanced past read bytes,
|
|
132
|
+
// so the next poll resumes from the failure point.
|
|
59
133
|
reportSwallowed('v2-live.read', err);
|
|
60
|
-
return [];
|
|
61
134
|
} finally {
|
|
62
135
|
if (fd !== undefined) { try { closeSync(fd); } catch { /* already closed */ } }
|
|
63
136
|
}
|
|
64
|
-
|
|
65
|
-
const chunk = cursor.pending + buf.toString('utf-8');
|
|
66
|
-
const parts = chunk.split('\n');
|
|
67
|
-
cursor.pending = parts.pop() || '';
|
|
68
|
-
return parts.map((l) => l.trim()).filter(Boolean);
|
|
137
|
+
return out;
|
|
69
138
|
}
|
|
70
139
|
|
|
71
140
|
export class V2LiveFeed {
|
|
@@ -91,8 +160,13 @@ export class V2LiveFeed {
|
|
|
91
160
|
this._safetyPollMs = typeof opts.safetyPollMs === 'number' ? opts.safetyPollMs : SAFETY_POLL_MS;
|
|
92
161
|
this._deferMs = typeof opts.deferMs === 'number' ? opts.deferMs : DEFER_MS;
|
|
93
162
|
this._now = opts.now || Date.now;
|
|
163
|
+
// Wire v3 (V3.S2): when on, every emitted item ALSO broadcasts a metadata
|
|
164
|
+
// row (v2_requests_delta). Explicit ctor param — this module has no access
|
|
165
|
+
// to the server deps object.
|
|
166
|
+
this._wireV3 = !!opts.wireV3;
|
|
94
167
|
this._sessions = new Map(); // dir → cursor bundle
|
|
95
168
|
this._seenDirs = new Map(); // dir → last observed journal mtimeMs (attached or not)
|
|
169
|
+
this._discardGated = new Set(); // dirs refused by the discard gate — first real attach must NOT suppress history
|
|
96
170
|
this._sessionsRoot = null;
|
|
97
171
|
this._rootWatcher = null;
|
|
98
172
|
this._safetyTimer = null;
|
|
@@ -120,6 +194,7 @@ export class V2LiveFeed {
|
|
|
120
194
|
for (const cur of this._sessions.values()) this._closeCursor(cur);
|
|
121
195
|
this._sessions.clear();
|
|
122
196
|
this._seenDirs.clear();
|
|
197
|
+
this._discardGated.clear();
|
|
123
198
|
this._sessionsRoot = null;
|
|
124
199
|
}
|
|
125
200
|
|
|
@@ -240,6 +315,23 @@ export class V2LiveFeed {
|
|
|
240
315
|
_attach(dir, { suppressExisting }) {
|
|
241
316
|
if (this._sessions.has(dir)) return this._sessions.get(dir);
|
|
242
317
|
if (!existsSync(join(dir, 'journal.jsonl'))) return null;
|
|
318
|
+
// Discardable sessions (quota-probe orphans — no main/teammate req, no
|
|
319
|
+
// meta.leader) are never followed: single choke point, every attach path
|
|
320
|
+
// (_initialScan / _maybeAttachNew / tick / _safetyTick / _rebuildCursor)
|
|
321
|
+
// funnels here and handles null. Self-healing when a dir later gains its
|
|
322
|
+
// first main: the leader's own dir re-attaches via tick's retry chain
|
|
323
|
+
// (sub-second); cross-process dirs via the 5s safety poll's mtime-bump
|
|
324
|
+
// scan (_seenDirs bookkeeping stays in the callers, so the bump fires).
|
|
325
|
+
if (isDiscardableSession(dir)) {
|
|
326
|
+
this._discardGated.add(dir);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
// A dir previously refused by the discard gate is attaching for the FIRST
|
|
330
|
+
// time — its first renderable turn was never broadcast, so the safety
|
|
331
|
+
// poll's suppressExisting:true (meant for "old stale dir resumed") must
|
|
332
|
+
// not swallow it: cross-process producers (IM worker, second ccv) have no
|
|
333
|
+
// cold-load fallback for a connected client.
|
|
334
|
+
if (this._discardGated.delete(dir)) suppressExisting = false;
|
|
243
335
|
const cur = {
|
|
244
336
|
dir,
|
|
245
337
|
synth: new SessionSynthesizer(dir, { deferMs: this._deferMs, now: this._now }),
|
|
@@ -248,9 +340,9 @@ export class V2LiveFeed {
|
|
|
248
340
|
// rebase each other's accumulated baseline — v1's tail followed exactly
|
|
249
341
|
// one file, so a single shared reconstructor was a new interleave surface.
|
|
250
342
|
reconstructor: createIncrementalReconstructor(),
|
|
251
|
-
journal: { path: join(dir, 'journal.jsonl'), offset: 0
|
|
252
|
-
responses: { path: join(dir, 'responses.jsonl'), offset: 0
|
|
253
|
-
convFiles: new Map(), // path → {key, offset
|
|
343
|
+
journal: { path: join(dir, 'journal.jsonl'), offset: 0 },
|
|
344
|
+
responses: { path: join(dir, 'responses.jsonl'), offset: 0 },
|
|
345
|
+
convFiles: new Map(), // path → {key, offset} (+ lazy _pendBufs carry)
|
|
254
346
|
watcher: null,
|
|
255
347
|
debounce: null,
|
|
256
348
|
reading: false,
|
|
@@ -299,7 +391,13 @@ export class V2LiveFeed {
|
|
|
299
391
|
this._feedConvFiles(cur);
|
|
300
392
|
const respLines = readNewLines(cur.responses);
|
|
301
393
|
if (respLines === null) { this._rebuildCursor(cur); return; }
|
|
302
|
-
for (const raw of respLines)
|
|
394
|
+
for (const raw of respLines) {
|
|
395
|
+
cur.synth.ingestResponseLine(raw);
|
|
396
|
+
// Wire v3 (V3.S4): forward the raw stored line — the flagged client
|
|
397
|
+
// assembles v1-shape entries from native lines locally. Suppressed
|
|
398
|
+
// during seed (history is covered by the cold-load channel).
|
|
399
|
+
if (this._wireV3 && !cur.suppress) this._forwardNative(cur, 'resp', null, raw);
|
|
400
|
+
}
|
|
303
401
|
const journalLines = readNewLines(cur.journal);
|
|
304
402
|
if (journalLines === null) { this._rebuildCursor(cur); return; }
|
|
305
403
|
for (const raw of journalLines) {
|
|
@@ -341,7 +439,7 @@ export class V2LiveFeed {
|
|
|
341
439
|
const p = join(convRoot, key, f);
|
|
342
440
|
let fc = cur.convFiles.get(p);
|
|
343
441
|
if (!fc) {
|
|
344
|
-
fc = { key, path: p, offset: 0
|
|
442
|
+
fc = { key, path: p, offset: 0 };
|
|
345
443
|
cur.convFiles.set(p, fc);
|
|
346
444
|
}
|
|
347
445
|
const lines = readNewLines(fc);
|
|
@@ -351,7 +449,10 @@ export class V2LiveFeed {
|
|
|
351
449
|
try { ev = JSON.parse(raw); } catch (err) {
|
|
352
450
|
reportSwallowed('v2-live.conv-parse', new Error(`${cur.dir}/${key}: ${err.message}`));
|
|
353
451
|
}
|
|
354
|
-
if (ev)
|
|
452
|
+
if (ev) {
|
|
453
|
+
cur.synth.ingestConvLine(key, ev);
|
|
454
|
+
if (this._wireV3 && !cur.suppress) this._forwardNative(cur, 'conv', key, raw);
|
|
455
|
+
}
|
|
355
456
|
}
|
|
356
457
|
}
|
|
357
458
|
}
|
|
@@ -403,17 +504,118 @@ export class V2LiveFeed {
|
|
|
403
504
|
clients: this._clients,
|
|
404
505
|
getClaudePid: this._getClaudePid,
|
|
405
506
|
runParallelHook: this._runParallelHook,
|
|
507
|
+
suppressEntryBroadcast: this._wireV3,
|
|
406
508
|
});
|
|
407
509
|
emitted++;
|
|
408
510
|
} catch (err) {
|
|
409
511
|
reportSwallowed('v2-live.emit', err);
|
|
410
512
|
}
|
|
513
|
+
// Wire v3: derive the metadata row from the post-reconstruction entry
|
|
514
|
+
// (main deltas carry full messages here — classification/cacheLoss get
|
|
515
|
+
// the same inputs the legacy list sees). Row-level errors must not
|
|
516
|
+
// disturb the legacy broadcast above.
|
|
517
|
+
if (this._wireV3) {
|
|
518
|
+
try {
|
|
519
|
+
this._emitRow(cur, item, parsed);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
reportSwallowed('v2-live.row', err);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
411
524
|
}
|
|
412
525
|
if (emitted > 0 && this._notifyStatsWorker) {
|
|
413
526
|
try { this._notifyStatsWorker(cur.dir); } catch (err) { reportSwallowed('v2-live.stats-notify', err); }
|
|
414
527
|
}
|
|
415
528
|
}
|
|
416
529
|
|
|
530
|
+
/** Wire v3 (V3.S4): broadcast one raw native line (conv event or responses
|
|
531
|
+
* line) — the flagged client's assembler replays these into v1-shape
|
|
532
|
+
* entries. `raw` is forwarded verbatim inside a JSON envelope carrying the
|
|
533
|
+
* session identity + channel. sessionId comes from the synthesizer (the
|
|
534
|
+
* UUID, matching the rows' sessionId and the entries' _seqEpoch). */
|
|
535
|
+
_forwardNative(cur, kind, channel, raw) {
|
|
536
|
+
try {
|
|
537
|
+
const payload = kind === 'conv'
|
|
538
|
+
? `{"sessionId":${JSON.stringify(cur.synth.sessionId)},"channel":${JSON.stringify(channel)},"line":${raw}}`
|
|
539
|
+
: `{"sessionId":${JSON.stringify(cur.synth.sessionId)},"line":${raw}}`;
|
|
540
|
+
const eventName = kind === 'conv' ? 'v3_conv' : 'v3_resp';
|
|
541
|
+
sendEventRawToClients(this._clients, eventName, payload);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
reportSwallowed('v2-live.native-forward', err);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Wire v3 (V3.S2): one metadata row per emitted item, derived from the
|
|
548
|
+
* post-reconstruction entry. Rows upsert client-side by (sessionId, seq):
|
|
549
|
+
* a placeholder row precedes its completed row, and a correction re-send
|
|
550
|
+
* follows when the NEXT request's arrival changes the previous row's
|
|
551
|
+
* classification (classifyRequest's Preflight/Plan cases read nextReq). */
|
|
552
|
+
_rowFrom(item, parsed) {
|
|
553
|
+
const usage = parsed.response?.body?.usage || null;
|
|
554
|
+
return {
|
|
555
|
+
seq: item.seq,
|
|
556
|
+
sessionId: item.sessionId,
|
|
557
|
+
timestamp: parsed.timestamp || '',
|
|
558
|
+
url: parsed.url || '',
|
|
559
|
+
method: parsed.method || 'POST',
|
|
560
|
+
// conv/evt/kind/mainAgent mirror the cold fold (meta-rows.js foldDir) —
|
|
561
|
+
// journal truth, NOT re-derivation. conv is load-bearing: the client
|
|
562
|
+
// assembler's buildEntry is `if (row.conv)`-gated, so a conv-less live
|
|
563
|
+
// row rebuilt every entry with EMPTY messages (chat vanished at stream
|
|
564
|
+
// end). mainAgent must be kind-derived like cold: parsed.mainAgent
|
|
565
|
+
// re-derives from the body and mis-tags countTokens (main system/tools
|
|
566
|
+
// aboard) as true, which would merge its turn into the chat live-only.
|
|
567
|
+
conv: item.conv,
|
|
568
|
+
evt: item.evt,
|
|
569
|
+
kind: item.kind || (item.isMain ? 'main' : (parsed.teammate ? 'teammate' : 'sub')),
|
|
570
|
+
mainAgent: item.isMain === true,
|
|
571
|
+
teammate: parsed.teammate || undefined,
|
|
572
|
+
model: parsed.body?.model,
|
|
573
|
+
proxyUrl: parsed.proxyUrl || undefined,
|
|
574
|
+
status: parsed.response?.status,
|
|
575
|
+
duration: parsed.duration,
|
|
576
|
+
usage: usage ? {
|
|
577
|
+
input_tokens: usage.input_tokens || 0,
|
|
578
|
+
output_tokens: usage.output_tokens || 0,
|
|
579
|
+
cache_read_input_tokens: usage.cache_read_input_tokens || 0,
|
|
580
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens || 0,
|
|
581
|
+
} : null,
|
|
582
|
+
inProgress: !!parsed.inProgress,
|
|
583
|
+
typeTag: null,
|
|
584
|
+
cacheLoss: null,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_emitRow(cur, item, parsed) {
|
|
589
|
+
const row = this._rowFrom(item, parsed);
|
|
590
|
+
try {
|
|
591
|
+
const tag = classifyRequest(parsed, null);
|
|
592
|
+
row.typeTag = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
593
|
+
} catch (err) { reportSwallowed('v2-live.row-classify', err); }
|
|
594
|
+
if (row.mainAgent && !row.inProgress) {
|
|
595
|
+
const u = parsed.response?.body?.usage;
|
|
596
|
+
const cw = (u && u.cache_creation_input_tokens) || 0;
|
|
597
|
+
const cr = (u && u.cache_read_input_tokens) || 0;
|
|
598
|
+
if (cur._v3PrevMain && cw > 0 && cw > cr) {
|
|
599
|
+
try { row.cacheLoss = computeCacheLoss(cur._v3PrevMain, parsed); } catch (err) { reportSwallowed('v2-live.row-cacheloss', err); }
|
|
600
|
+
}
|
|
601
|
+
cur._v3PrevMain = parsed; // one full entry retained per cursor (bounded)
|
|
602
|
+
}
|
|
603
|
+
// Lookahead correction: this entry is the previous row's nextReq.
|
|
604
|
+
const prev = cur._v3Last;
|
|
605
|
+
if (prev && (prev.row.sessionId !== row.sessionId || prev.row.seq !== row.seq)) {
|
|
606
|
+
try {
|
|
607
|
+
const tag = classifyRequest(prev.entry, parsed);
|
|
608
|
+
const corrected = tag ? { type: tag.type, subType: tag.subType ?? null } : null;
|
|
609
|
+
if (JSON.stringify(corrected) !== JSON.stringify(prev.row.typeTag)) {
|
|
610
|
+
prev.row.typeTag = corrected;
|
|
611
|
+
sendEventToClients(this._clients, 'v2_requests_delta', prev.row);
|
|
612
|
+
}
|
|
613
|
+
} catch (err) { reportSwallowed('v2-live.row-classify', err); }
|
|
614
|
+
}
|
|
615
|
+
cur._v3Last = { row, entry: parsed };
|
|
616
|
+
sendEventToClients(this._clients, 'v2_requests_delta', row);
|
|
617
|
+
}
|
|
618
|
+
|
|
417
619
|
// ── safety poll ────────────────────────────────────────────────────────────
|
|
418
620
|
|
|
419
621
|
_safetyTick() {
|