cc-viewer 1.6.348 → 1.7.0
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-DTkacfRW.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-C5Q6uUyH.js} +1 -1
- package/dist/assets/Mobile-MoBp9c8k.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-Ac8vnV3v.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-Cd_TAXTb.js +2 -0
- package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-CxuD1CYl.css} +1 -1
- 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 +2 -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 +34 -272
- 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 +1059 -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 +451 -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 +513 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/routes/events.js +30 -130
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- package/server/routes/logs.js +194 -152
- 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/workspaces.js +44 -9
- package/server/server.js +47 -65
- package/server/workspace-registry.js +24 -4
- 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,44 @@
|
|
|
1
|
+
// Wire Format v2 — content-addressed blob store for body.tools / body.system
|
|
2
|
+
// (docs/refactor/WIRE_FORMAT_V2.md §7).
|
|
3
|
+
//
|
|
4
|
+
// key = sha256(JSON.stringify(value)) hex, first 16 chars. File content is the
|
|
5
|
+
// raw JSON value. Writes go through writeFileAtomicSync (tmp → fsync → rename):
|
|
6
|
+
// the blob is the ONE durability barrier of the write-order protocol — its
|
|
7
|
+
// journal reference may only become durable after the blob itself (spec §1.3).
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { blobPath, writeFileAtomicSync } from './layout.js';
|
|
12
|
+
|
|
13
|
+
export class BlobStore {
|
|
14
|
+
/** @param {object} paths - sessionPaths() object for one session */
|
|
15
|
+
constructor(paths) {
|
|
16
|
+
this._paths = paths;
|
|
17
|
+
this._written = new Set(); // refs confirmed on disk this process lifetime
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Store a JSON-serializable value, return its ref ('sha256-<hex16>').
|
|
22
|
+
* Idempotent: same content → same ref, at most one disk write per process
|
|
23
|
+
* (existsSync re-check makes restarts cheap too). Returns null for
|
|
24
|
+
* undefined/null values so callers can spread `...(ref && {tools: ref})`.
|
|
25
|
+
*
|
|
26
|
+
* KEEP IN SYNC: replay.js `blobRefOf` re-implements this exact formula
|
|
27
|
+
* (sha256 of JSON.stringify, hex, first 16) so the verifier can compare v1
|
|
28
|
+
* inline values against journal refs WITHOUT reading blob files — change the
|
|
29
|
+
* hash, the slice length, or the serialization here and the verify gate
|
|
30
|
+
* silently produces false diffs/OKs unless blobRefOf changes with it.
|
|
31
|
+
*/
|
|
32
|
+
put(value) {
|
|
33
|
+
if (value === undefined || value === null) return null;
|
|
34
|
+
const json = JSON.stringify(value);
|
|
35
|
+
const ref = 'sha256-' + createHash('sha256').update(json).digest('hex').slice(0, 16);
|
|
36
|
+
if (this._written.has(ref)) return ref;
|
|
37
|
+
const finalPath = blobPath(this._paths, ref);
|
|
38
|
+
if (!existsSync(finalPath)) {
|
|
39
|
+
writeFileAtomicSync(finalPath, json);
|
|
40
|
+
}
|
|
41
|
+
this._written.add(ref);
|
|
42
|
+
return ref;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Wire Format v2 — conversation event store (docs/refactor/WIRE_FORMAT_V2.md §6).
|
|
2
|
+
//
|
|
3
|
+
// The write side performs EXACTLY ONE cheap judgement — the prefix-extension
|
|
4
|
+
// test (same one v1's delta path does) — and stores wire events faithfully:
|
|
5
|
+
// append — wire messages prefix-extend the previous state → store the tail;
|
|
6
|
+
// snapshot — anything else (v1 §3.1 plan-mode window, §3.2 K-tail overlap,
|
|
7
|
+
// unexplained shrink, process restart) → store the FULL wire array
|
|
8
|
+
// and let the READ-side materializer resolve it with the shared
|
|
9
|
+
// reverse-anchor module. No merge logic ever runs at write time
|
|
10
|
+
// (plan decision F6: a write-side merge mistake would be permanent).
|
|
11
|
+
// ctl — replace-tail (v1 §3.3 in-place replace) / compact continuation.
|
|
12
|
+
//
|
|
13
|
+
// /clear (shared predicate, §3.4) closes the epoch and opens e<N+1>;
|
|
14
|
+
// /compact (§3.5) stays in the SAME epoch with a ctl marker.
|
|
15
|
+
//
|
|
16
|
+
// Every stored line carries the initiating seq + rid (plan risk F8): physical
|
|
17
|
+
// append order is completion order, semantic order is seq.
|
|
18
|
+
|
|
19
|
+
import { readdirSync } from 'node:fs';
|
|
20
|
+
import { isPostClearCheckpoint, isCompactContinuation, normalizeMsgForEquality } from '../session-boundary.js';
|
|
21
|
+
import { fingerprintMsg } from '../interceptor-core.js';
|
|
22
|
+
import { ensureConvDirSync, convEpochPath, convDir } from './layout.js';
|
|
23
|
+
|
|
24
|
+
/** FNV-1a over a string — cheap full-content discriminator for exact mode. */
|
|
25
|
+
function fnv1a(s) {
|
|
26
|
+
let h = 0x811c9dc5;
|
|
27
|
+
for (let i = 0; i < s.length; i++) {
|
|
28
|
+
h ^= s.charCodeAt(i);
|
|
29
|
+
h = Math.imul(h, 0x01000193);
|
|
30
|
+
}
|
|
31
|
+
return (h >>> 0).toString(36);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ConversationStore {
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} paths - sessionPaths() object
|
|
37
|
+
* @param {{appendTo: Function}} queue - AsyncWriteQueue (explicit-path API)
|
|
38
|
+
* @param {{exactFps?: boolean}} [opts] - exact mode: judgement fingerprints
|
|
39
|
+
* additionally hash the FULL message JSON. The offline converter needs
|
|
40
|
+
* byte-grade faithfulness (its golden gate replays byte-identical wire);
|
|
41
|
+
* fingerprintMsg's 80-char first-block snip cannot distinguish wires that
|
|
42
|
+
* share a long prefix (real data 2026-07-14: sub prompts identical in the
|
|
43
|
+
* first 80 chars, differing later, judged "unchanged" → replay stuck on
|
|
44
|
+
* the first wire). The live request path keeps the cheap default.
|
|
45
|
+
*/
|
|
46
|
+
constructor(paths, queue, opts = {}) {
|
|
47
|
+
this._paths = paths;
|
|
48
|
+
this._queue = queue;
|
|
49
|
+
this._convs = new Map(); // convKey → { epoch, count, fps, dirEnsured }
|
|
50
|
+
// Exact fps hash the NORMALIZED message (S4 decision 2026-07-14): the
|
|
51
|
+
// client migrates cache_control breakpoints onto old messages (string →
|
|
52
|
+
// block array), which is invisible to fingerprintMsg but flips the full
|
|
53
|
+
// JSON hash — without normalization the converter degrades to a snapshot
|
|
54
|
+
// per request on affected conversations (real-data storm, ~62% of corpus
|
|
55
|
+
// bytes). Storage still records the wire verbatim; only the judgement
|
|
56
|
+
// compares modulo cache_control/form.
|
|
57
|
+
this._fpOf = opts.exactFps
|
|
58
|
+
? (m) => { const n = normalizeMsgForEquality(m); return `${fingerprintMsg(n)}:${fnv1a(JSON.stringify(n))}`; }
|
|
59
|
+
: fingerprintMsg;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_state(convKey) {
|
|
63
|
+
let st = this._convs.get(convKey);
|
|
64
|
+
if (!st) {
|
|
65
|
+
// fps: per-message judgement fingerprints of the CURRENT wire state.
|
|
66
|
+
// Tail-only comparison proved insufficient on real data (2026-07-14):
|
|
67
|
+
// interleaved same-sid streams (pre-flag proxy teammates, dual writers)
|
|
68
|
+
// share prefixes/tails while differing in the middle — append/replace
|
|
69
|
+
// judgements must verify the WHOLE prefix or replay produces a
|
|
70
|
+
// franken-mix. Strings are ~90 chars × wire length: negligible memory.
|
|
71
|
+
//
|
|
72
|
+
// epoch is SEEDED from the epoch files already on disk (2026-07-15 fix):
|
|
73
|
+
// a fresh store on an existing conv dir (process restart / `-c`
|
|
74
|
+
// continuation onto the same session) must continue in the LATEST epoch.
|
|
75
|
+
// Starting back at e0 appended newer seqs to an older file, breaking the
|
|
76
|
+
// global "file order == seq order" the live reader relies on (the
|
|
77
|
+
// journal seq already reseeds from disk; the epoch did not).
|
|
78
|
+
st = { epoch: this._seedEpoch(convKey), count: 0, fps: [], dirEnsured: false, everWrote: false };
|
|
79
|
+
this._convs.set(convKey, st);
|
|
80
|
+
}
|
|
81
|
+
return st;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Highest e<N>.jsonl already on disk for this conv (0 when none/unreadable). */
|
|
85
|
+
_seedEpoch(convKey) {
|
|
86
|
+
let max = 0;
|
|
87
|
+
try {
|
|
88
|
+
for (const f of readdirSync(convDir(this._paths, convKey))) {
|
|
89
|
+
const m = f.match(/^e(\d+)\.jsonl$/);
|
|
90
|
+
if (m) { const n = Number(m[1]); if (n > max) max = n; }
|
|
91
|
+
}
|
|
92
|
+
} catch { /* no conv dir yet — brand-new conversation */ }
|
|
93
|
+
return max;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** All fps[0..n) pairwise equal between the stored state and the incoming wire. */
|
|
97
|
+
static _prefixEqual(prevFps, fpsIn, n) {
|
|
98
|
+
for (let i = 0; i < n; i++) {
|
|
99
|
+
if (prevFps[i] !== fpsIn[i]) return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ingest one request's wire messages for a conversation.
|
|
106
|
+
* @param {string} convKey
|
|
107
|
+
* @param {Array} messages - ORIGINAL wire messages (pre any v1 delta mutation)
|
|
108
|
+
* @param {{seq:number, rid:string}} ids
|
|
109
|
+
* @returns {{evt:'append'|'snapshot'|null, ctl:string|null, epoch:number,
|
|
110
|
+
* boundary:'clear'|'compact'|'replace-tail'|null,
|
|
111
|
+
* msgFrom:number, msgTo:number}}
|
|
112
|
+
* evt/ctl describe what was written (journal req line mirrors this, §4);
|
|
113
|
+
* msgFrom/msgTo are the [from,to) wire counts for integrity checks.
|
|
114
|
+
*/
|
|
115
|
+
ingest(convKey, messages, ids) {
|
|
116
|
+
const st = this._state(convKey);
|
|
117
|
+
const msgs = Array.isArray(messages) ? messages : [];
|
|
118
|
+
const len = msgs.length;
|
|
119
|
+
// Judgement fingerprint = v1's fingerprintMsg, NOT the client-shared
|
|
120
|
+
// messageFingerprint: fingerprintMsg is content-aware for tool_result
|
|
121
|
+
// bodies (40-char snippet) where messageFingerprint keys tool_result on
|
|
122
|
+
// tool_use_id ONLY. A same-id tool_result tail edit must be detected here
|
|
123
|
+
// exactly like v1's Plan C does, or the two dual-write sides legitimately
|
|
124
|
+
// diverge in a way the verify digest (built on messageFingerprint) is
|
|
125
|
+
// structurally blind to (review P2).
|
|
126
|
+
const fpsIn = msgs.map(this._fpOf);
|
|
127
|
+
const prev = st.count;
|
|
128
|
+
const prevFps = st.fps;
|
|
129
|
+
|
|
130
|
+
let evt = null;
|
|
131
|
+
let ctl = null;
|
|
132
|
+
let boundary = null;
|
|
133
|
+
const lines = [];
|
|
134
|
+
|
|
135
|
+
// /clear has priority over every other judgement (spec §6). The predicate
|
|
136
|
+
// wants the v1 entry shape; at write time this IS a full wire snapshot, so
|
|
137
|
+
// a synthetic _isCheckpoint:true carrier is faithful.
|
|
138
|
+
const isClear = prev > 0 && isPostClearCheckpoint({ _isCheckpoint: true, body: { messages: msgs } }, prev);
|
|
139
|
+
if (isClear) {
|
|
140
|
+
st.epoch += 1;
|
|
141
|
+
st.count = 0;
|
|
142
|
+
st.fps = [];
|
|
143
|
+
boundary = 'clear';
|
|
144
|
+
evt = 'snapshot';
|
|
145
|
+
lines.push({ seq: ids.seq, rid: ids.rid, t: 'snapshot', msgs, reason: 'first' });
|
|
146
|
+
} else if (len === prev && ConversationStore._prefixEqual(prevFps, fpsIn, len)) {
|
|
147
|
+
// Unchanged wire state → no conversation event (journal still records req/done).
|
|
148
|
+
evt = null;
|
|
149
|
+
} else if (len === prev && prev > 0 && ConversationStore._prefixEqual(prevFps, fpsIn, len - 1)
|
|
150
|
+
&& prevFps[len - 1] !== '' && fpsIn[len - 1] !== '' && fpsIn[len - 1] !== prevFps[len - 1]) {
|
|
151
|
+
// v1 §3.3 in-place last-message replace (same judgement family as the
|
|
152
|
+
// interceptor's `_sameLenInPlaceReplace`, same fingerprintMsg formula) —
|
|
153
|
+
// gated on FULL prefix equality: an equal-length but different wire array
|
|
154
|
+
// (real data: pre-flag proxy teammates / dual writers interleaved into
|
|
155
|
+
// one stream, §3.7 L104) must NOT be patched tail-only, or the replayed
|
|
156
|
+
// state becomes a franken-mix. Any prefix mismatch falls through to the
|
|
157
|
+
// snapshot branch (write-side never merges — snapshot is truth).
|
|
158
|
+
evt = 'ctl';
|
|
159
|
+
ctl = 'replace-tail';
|
|
160
|
+
boundary = 'replace-tail';
|
|
161
|
+
lines.push({ seq: ids.seq, rid: ids.rid, t: 'ctl', op: 'replace-tail', msg: msgs[len - 1] });
|
|
162
|
+
} else if (len > prev && (prev === 0 || ConversationStore._prefixEqual(prevFps, fpsIn, prev))) {
|
|
163
|
+
if (prev === 0 && st.count === 0 && !st.everWrote) {
|
|
164
|
+
// First event of a FRESH STORE → snapshot(first), so every epoch file
|
|
165
|
+
// is self-contained even when it starts mid-wire (teammate logs,
|
|
166
|
+
// process restart onto an existing session dir). No epoch===0 guard:
|
|
167
|
+
// a store seeded onto an existing epoch (restart continuation) must
|
|
168
|
+
// snapshot too — falling through to `append` would write the full
|
|
169
|
+
// wire as a tail onto the previous process's state (franken-mix).
|
|
170
|
+
evt = 'snapshot';
|
|
171
|
+
lines.push({ seq: ids.seq, rid: ids.rid, t: 'snapshot', msgs, reason: 'first' });
|
|
172
|
+
} else {
|
|
173
|
+
evt = 'append';
|
|
174
|
+
lines.push({ seq: ids.seq, rid: ids.rid, t: 'append', msgs: msgs.slice(prev) });
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Prefix test failed: §3.1 short window / §3.2 overlap / unexplained
|
|
178
|
+
// shrink / restart-onto-existing-state. Store the full wire truth.
|
|
179
|
+
const reason = prev === 0 ? 'first' : (len < prev ? 'shrunk' : 'tail-mismatch');
|
|
180
|
+
evt = 'snapshot';
|
|
181
|
+
lines.push({ seq: ids.seq, rid: ids.rid, t: 'snapshot', msgs, reason });
|
|
182
|
+
if (isCompactContinuation({ body: { messages: msgs } })) {
|
|
183
|
+
ctl = 'compact';
|
|
184
|
+
boundary = 'compact';
|
|
185
|
+
lines.push({ seq: ids.seq, rid: ids.rid, t: 'ctl', op: 'compact' });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (lines.length > 0) {
|
|
190
|
+
if (!st.dirEnsured) {
|
|
191
|
+
ensureConvDirSync(this._paths, convKey);
|
|
192
|
+
st.dirEnsured = true;
|
|
193
|
+
}
|
|
194
|
+
const path = convEpochPath(this._paths, convKey, st.epoch);
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
this._queue.appendTo(path, JSON.stringify(line) + '\n');
|
|
197
|
+
}
|
|
198
|
+
st.everWrote = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Advance state to this wire truth (eagerly, mirroring v1's eager-update
|
|
202
|
+
// fix for <30ms bursts — the next request must compare against THIS one).
|
|
203
|
+
st.count = len;
|
|
204
|
+
st.fps = fpsIn;
|
|
205
|
+
|
|
206
|
+
// [from,to) of the wire messages this event covers (spec §4):
|
|
207
|
+
// snapshot ⇒ [0,len); append ⇒ [prev,len); replace-tail ⇒ [len-1,len);
|
|
208
|
+
// no event ⇒ empty range at len.
|
|
209
|
+
const msgFrom = evt === 'snapshot' ? 0 : (ctl === 'replace-tail' ? Math.max(0, len - 1) : (evt === 'append' ? prev : len));
|
|
210
|
+
return { evt, ctl, epoch: st.epoch, boundary, msgFrom, msgTo: len };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Forget in-memory conversation states (workspace reset / resume hooks). */
|
|
214
|
+
reset() {
|
|
215
|
+
this._convs.clear();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Wire Format v2 — S8 conversion manager (resident-task supervision).
|
|
2
|
+
//
|
|
3
|
+
// Owns the single in-process conversion worker: start/stop/status for the
|
|
4
|
+
// routes, plus boot-time auto-resume — a state file left at 'running' or
|
|
5
|
+
// 'verifying' means the previous server died mid-conversion, so the task
|
|
6
|
+
// restarts itself until the project reaches 'done' (user decision: the
|
|
7
|
+
// migration is resident until complete). File-level checkpoints make the
|
|
8
|
+
// re-run safe and cheap (done files are skipped).
|
|
9
|
+
|
|
10
|
+
import { Worker } from 'node:worker_threads';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { readConvertState } from './convert.js';
|
|
13
|
+
|
|
14
|
+
let _running = null; // { project, logDir, worker, startedAt, progress }
|
|
15
|
+
let _lastError = null; // last worker-level failure (state file may lag on hard crashes)
|
|
16
|
+
|
|
17
|
+
export function isConvertRunning() {
|
|
18
|
+
return !!_running;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start converting a project in a background worker.
|
|
23
|
+
* @returns {{ok: true} | {ok: false, error: string}}
|
|
24
|
+
*/
|
|
25
|
+
export function startConvert(logDir, project) {
|
|
26
|
+
if (!logDir || !project) return { ok: false, error: 'logDir/project required' };
|
|
27
|
+
if (_running) return { ok: false, error: `conversion already running for project "${_running.project}"` };
|
|
28
|
+
let worker;
|
|
29
|
+
try {
|
|
30
|
+
worker = new Worker(new URL('./convert-worker.js', import.meta.url));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { ok: false, error: `failed to start worker: ${err.message}` };
|
|
33
|
+
}
|
|
34
|
+
_lastError = null;
|
|
35
|
+
_running = { project, logDir, worker, startedAt: new Date().toISOString(), progress: null };
|
|
36
|
+
worker.on('message', (msg) => {
|
|
37
|
+
if (!msg || !_running || _running.worker !== worker) return;
|
|
38
|
+
if (msg.type === 'progress') _running.progress = msg.progress;
|
|
39
|
+
else if (msg.type === 'final' && msg.error) _lastError = msg.error;
|
|
40
|
+
});
|
|
41
|
+
worker.on('error', (err) => {
|
|
42
|
+
_lastError = String(err && err.message || err);
|
|
43
|
+
if (_running && _running.worker === worker) _running = null;
|
|
44
|
+
});
|
|
45
|
+
worker.on('exit', () => {
|
|
46
|
+
if (_running && _running.worker === worker) _running = null;
|
|
47
|
+
});
|
|
48
|
+
worker.postMessage({ type: 'start', logDir, project });
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Request a graceful stop (checkpoints at a file/entry boundary). */
|
|
53
|
+
export function stopConvert() {
|
|
54
|
+
if (!_running) return { ok: false, error: 'no conversion running' };
|
|
55
|
+
try { _running.worker.postMessage({ type: 'stop' }); } catch { }
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Snapshot for GET /api/wire-v2-convert: in-memory liveness + on-disk state. */
|
|
60
|
+
export function convertStatus(logDir, project) {
|
|
61
|
+
const state = project ? readConvertState(join(logDir, project)) : null;
|
|
62
|
+
const runningHere = !!(_running && _running.project === project);
|
|
63
|
+
return {
|
|
64
|
+
running: runningHere,
|
|
65
|
+
...(runningHere && { startedAt: _running.startedAt, progress: _running.progress }),
|
|
66
|
+
...(_lastError && !runningHere && { lastWorkerError: _lastError }),
|
|
67
|
+
state,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Boot hook: resume a conversion the previous process left unfinished. */
|
|
72
|
+
export function maybeResumeConvert(logDir, project) {
|
|
73
|
+
if (!logDir || !project || _running) return false;
|
|
74
|
+
const state = readConvertState(join(logDir, project));
|
|
75
|
+
if (!state || (state.status !== 'running' && state.status !== 'verifying')) return false;
|
|
76
|
+
const res = startConvert(logDir, project);
|
|
77
|
+
if (res.ok) console.error(`[CC Viewer] wire-v2 convert: resuming unfinished migration for "${project}"`);
|
|
78
|
+
return res.ok;
|
|
79
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Wire Format v2 — S8 conversion worker (worker_threads entry).
|
|
2
|
+
//
|
|
3
|
+
// One conversion per worker lifetime: the manager spawns a worker per start
|
|
4
|
+
// (or boot-resume), sends {type:'start'}, and the worker exits after posting
|
|
5
|
+
// its final message. 'stop' flips a flag polled by convertProject, which
|
|
6
|
+
// checkpoints at a safe boundary and returns with status:'stopped'.
|
|
7
|
+
|
|
8
|
+
import { parentPort } from 'node:worker_threads';
|
|
9
|
+
import { convertProject } from './convert.js';
|
|
10
|
+
|
|
11
|
+
let stopRequested = false;
|
|
12
|
+
|
|
13
|
+
parentPort?.on('message', async (msg) => {
|
|
14
|
+
if (!msg || typeof msg !== 'object') return;
|
|
15
|
+
if (msg.type === 'stop') {
|
|
16
|
+
stopRequested = true;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (msg.type !== 'start') return;
|
|
20
|
+
|
|
21
|
+
const { logDir, project } = msg;
|
|
22
|
+
let lastProgressAt = 0;
|
|
23
|
+
try {
|
|
24
|
+
const state = await convertProject(logDir, project, {
|
|
25
|
+
shouldStop: () => stopRequested,
|
|
26
|
+
onProgress: (p) => {
|
|
27
|
+
// ≥1s throttle: progress is UI polish, not a firehose. Terminal phases
|
|
28
|
+
// always go through so the manager's snapshot ends accurate.
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (p.phase !== 'done' && now - lastProgressAt < 1000) return;
|
|
31
|
+
lastProgressAt = now;
|
|
32
|
+
parentPort?.postMessage({ type: 'progress', project, progress: p });
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
parentPort?.postMessage({ type: 'final', project, state });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// convertProject already persisted status:'error' to the state file.
|
|
38
|
+
parentPort?.postMessage({ type: 'final', project, error: String(err && err.message || err) });
|
|
39
|
+
}
|
|
40
|
+
// Let the queued postMessage drain, then end the thread.
|
|
41
|
+
setImmediate(() => process.exit(0));
|
|
42
|
+
});
|