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
|
@@ -36,8 +36,19 @@ export class AsyncWriteQueue {
|
|
|
36
36
|
get pendingBytes() { return this._pendingBytes; }
|
|
37
37
|
|
|
38
38
|
append(data, onDone) {
|
|
39
|
+
return this.appendTo(this._getPath(), data, onDone);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Explicit-path variant (wire-v2 S2): same queue, same ordering/drain
|
|
44
|
+
* machinery, caller supplies the target file. Within one drain batch, path
|
|
45
|
+
* groups are written in first-enqueue order, so a caller enqueuing
|
|
46
|
+
* conversation lines before the journal line keeps that relative order on
|
|
47
|
+
* disk (WIRE_FORMAT_V2.md §1.3 write-order protocol). Empty/falsy path stays
|
|
48
|
+
* a silent no-op — workspace mode and leaderless teammates rely on it.
|
|
49
|
+
*/
|
|
50
|
+
appendTo(path, data, onDone) {
|
|
39
51
|
if (this._closed) return;
|
|
40
|
-
const path = this._getPath();
|
|
41
52
|
if (!path) {
|
|
42
53
|
if (onDone) try { onDone(); } catch {}
|
|
43
54
|
return;
|
|
@@ -88,7 +88,7 @@ function _seqGuardCheck(entry, st) {
|
|
|
88
88
|
/**
|
|
89
89
|
* 标记乱序条目并尽量就地补偿:accumulated 已包含更新的真值时,用其前缀
|
|
90
90
|
* (= 截至该条目声称长度的最新内容)回填 body.messages,避免裸 delta 切片
|
|
91
|
-
*
|
|
91
|
+
* 残留在请求详情面板中。
|
|
92
92
|
* 返回 true 表示已补偿(messages 为一致全量),false 表示需后续 checkpoint 补偿。
|
|
93
93
|
*/
|
|
94
94
|
function _markStaleEntry(entry, accumulated) {
|
|
@@ -142,7 +142,7 @@ function _integrityCheck(entry, accumulated, st) {
|
|
|
142
142
|
* 2. 真值前缀 —— stale 就地补偿、超长 slice 修复(_staleReorder 置位,merge 跳过);
|
|
143
143
|
* 3. 裸 delta 切片待补偿 —— stale 就地补偿失败 / poisoned 冻结(_staleReorder 或
|
|
144
144
|
* _reconstructBroken 置位):由第二遍 checkpoint 补偿回填,无第二遍的路径靠
|
|
145
|
-
* isMergeBlockedEntry 阻断 merge
|
|
145
|
+
* isMergeBlockedEntry 阻断 merge 兜底。
|
|
146
146
|
*
|
|
147
147
|
* @param {object} entry - 单条日志条目(原地修改)
|
|
148
148
|
* @param {{accumulated: Array, intState: {baselineSeen: boolean, poisoned: boolean},
|
|
@@ -239,7 +239,7 @@ export function reconstructEntries(entries) {
|
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
241
|
* 用候选全量条目(checkpoint/旧格式,排除 teammate)补偿一条断裂条目。
|
|
242
|
-
* 内容已是真值前缀,清除标记让批量 merge
|
|
242
|
+
* 内容已是真值前缀,清除标记让批量 merge 正常消费。
|
|
243
243
|
* @returns {boolean} true = 已补偿
|
|
244
244
|
*/
|
|
245
245
|
function _tryRepairFromCandidate(brokenEntry, expectedCount, candidate) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
|
|
2
|
+
/**
|
|
3
|
+
* Error-reporting convention for swallowed catch blocks.
|
|
4
|
+
*
|
|
5
|
+
* Canonical home since wire-v2 S2 (moved verbatim from src/utils/errorReport.js,
|
|
6
|
+
* which remains as a re-export shell): server/lib/v2/* writers follow the same
|
|
7
|
+
* convention, and src/ is not shipped in the npm package.
|
|
8
|
+
*
|
|
9
|
+
* The rule (see CLAUDE.md): a `catch` whose failure has diagnostic value —
|
|
10
|
+
* anything where a throw means lost data or broken UI (SSE/WS event parsing,
|
|
11
|
+
* entry ingest, state updates, fetch response handling) — must call
|
|
12
|
+
* `reportSwallowed(tag, err)` instead of swallowing silently. Purely-benign
|
|
13
|
+
* best-effort guards (localStorage get/set, tabBridge IPC, revokeObjectURL,
|
|
14
|
+
* listener-cleanup `off()`) stay bare `catch {}`.
|
|
15
|
+
*
|
|
16
|
+
* Tag grammar: `<channel>.<event-name-verbatim>`, e.g. `sse.load_chunk`,
|
|
17
|
+
* `sse.stream-progress`, `ws.terminal-msg`. Keep the event name exactly as it
|
|
18
|
+
* appears in the code/protocol so a grep for the tag round-trips to the site.
|
|
19
|
+
* All reports share the greppable `[ccv:` console prefix.
|
|
20
|
+
*
|
|
21
|
+
* Dedup: identical failures usually repeat (e.g. every SSE event of a changed
|
|
22
|
+
* payload shape fails the same way), so each tag logs at most
|
|
23
|
+
* MAX_REPORTS_PER_TAG occurrences per session, then one suppression notice,
|
|
24
|
+
* then goes silent. Counter-based on purpose — no Date/timers, so this module
|
|
25
|
+
* is statically loadable and deterministic under `node --test`.
|
|
26
|
+
*/
|
|
27
|
+
export const MAX_REPORTS_PER_TAG = 5;
|
|
28
|
+
|
|
29
|
+
const _counts = new Map();
|
|
30
|
+
|
|
31
|
+
export function reportSwallowed(tag, err, extra) {
|
|
32
|
+
const n = (_counts.get(tag) || 0) + 1;
|
|
33
|
+
_counts.set(tag, n);
|
|
34
|
+
if (n > MAX_REPORTS_PER_TAG + 1) return;
|
|
35
|
+
if (n === MAX_REPORTS_PER_TAG + 1) {
|
|
36
|
+
console.warn(`[ccv:${tag}] further occurrences suppressed`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (extra !== undefined) console.warn(`[ccv:${tag}]`, err, extra);
|
|
40
|
+
else console.warn(`[ccv:${tag}]`, err);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function _resetForTest() {
|
|
44
|
+
_counts.clear();
|
|
45
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// 变化时触发——与 findRecentLog 的「最新真实日志」语义对齐,过滤流式临时文件噪声。
|
|
13
13
|
// - watchImpl / mkdirImpl 可注入,便于确定性单测(无需真实 FS 时序)。
|
|
14
14
|
|
|
15
|
-
import { watch as fsWatch, mkdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { watch as fsWatch, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
|
|
18
18
|
// fs.watch 一次文件写会抖多次(macOS/Linux 尤甚),通常 50~300ms 内合并;选 250ms 平衡实时性与稳定性。
|
|
@@ -21,14 +21,17 @@ const DEFAULT_DEBOUNCE_MS = 250;
|
|
|
21
21
|
// 杜绝被污染的 id 经 `IM_${id}` 拼进路径造成穿越(与路由正则 /^[a-z0-9_-]+$/ 同集)。
|
|
22
22
|
const PLATFORM_ID_RE = /^[a-z0-9_-]+$/;
|
|
23
23
|
|
|
24
|
-
export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl } = {}) {
|
|
24
|
+
export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl, readdirImpl } = {}) {
|
|
25
25
|
const _watch = watchImpl || fsWatch;
|
|
26
26
|
const _mkdir = mkdirImpl || ((d) => { try { mkdirSync(d, { recursive: true }); } catch { /* ignore */ } });
|
|
27
27
|
const _exists = existsImpl || existsSync;
|
|
28
|
+
const _readdir = readdirImpl || ((d) => { try { return readdirSync(d); } catch { return []; } });
|
|
28
29
|
const _onChange = typeof onChange === 'function' ? onChange : () => {};
|
|
29
30
|
const _getLogDir = typeof getLogDir === 'function' ? getLogDir : () => '';
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// platformId -> { w, dir, sessionsW, sidWatchers: Map<sid, FSWatcher> }
|
|
33
|
+
// 连同所属目录登记,便于 LOG_DIR 切换检测。
|
|
34
|
+
const watchers = new Map();
|
|
32
35
|
const timers = new Map(); // platformId -> timeout
|
|
33
36
|
let _disposed = false;
|
|
34
37
|
|
|
@@ -51,6 +54,52 @@ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_D
|
|
|
51
54
|
return true;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
function _closeReg(reg) {
|
|
58
|
+
try { reg.w.close(); } catch { /* ignore */ }
|
|
59
|
+
if (reg.sessionsW) { try { reg.sessionsW.close(); } catch { /* ignore */ } }
|
|
60
|
+
for (const sw of reg.sidWatchers.values()) { try { sw.close(); } catch { /* ignore */ } }
|
|
61
|
+
reg.sidWatchers.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// wire-v2: session appends land at IM_<id>/sessions/<sid>/journal.jsonl —
|
|
65
|
+
// two levels below the platform dir, invisible to its non-recursive watch.
|
|
66
|
+
// Per-sid watchers see journal appends (direct children of the sid dir);
|
|
67
|
+
// the sessions/-level watcher sees new sid dirs appear.
|
|
68
|
+
function _watchSid(platformId, reg, sid) {
|
|
69
|
+
if (reg.sidWatchers.has(sid)) return;
|
|
70
|
+
const sidDir = join(reg.dir, 'sessions', sid);
|
|
71
|
+
let sw;
|
|
72
|
+
try {
|
|
73
|
+
sw = _watch(sidDir, (_eventType, filename) => {
|
|
74
|
+
if (_relevant(filename)) _schedule(platformId);
|
|
75
|
+
});
|
|
76
|
+
} catch { return; }
|
|
77
|
+
if (sw && typeof sw.on === 'function') {
|
|
78
|
+
sw.on('error', () => { try { sw.close(); } catch { /* ignore */ } reg.sidWatchers.delete(sid); });
|
|
79
|
+
}
|
|
80
|
+
reg.sidWatchers.set(sid, sw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _ensureSessionsWatch(platformId, reg) {
|
|
84
|
+
const sessionsDir = join(reg.dir, 'sessions');
|
|
85
|
+
if (!_exists(sessionsDir)) return; // re-tried on the next platform-dir event
|
|
86
|
+
if (!reg.sessionsW) {
|
|
87
|
+
let sw;
|
|
88
|
+
try {
|
|
89
|
+
sw = _watch(sessionsDir, (_eventType, filename) => {
|
|
90
|
+
// New session dir → a fresh conversation exists; watch it and refresh.
|
|
91
|
+
if (filename) _watchSid(platformId, reg, String(filename));
|
|
92
|
+
_schedule(platformId);
|
|
93
|
+
});
|
|
94
|
+
} catch { return; }
|
|
95
|
+
if (sw && typeof sw.on === 'function') {
|
|
96
|
+
sw.on('error', () => { try { sw.close(); } catch { /* ignore */ } reg.sessionsW = null; });
|
|
97
|
+
}
|
|
98
|
+
reg.sessionsW = sw;
|
|
99
|
+
}
|
|
100
|
+
for (const sid of _readdir(sessionsDir)) _watchSid(platformId, reg, sid);
|
|
101
|
+
}
|
|
102
|
+
|
|
54
103
|
function ensure(platformId) {
|
|
55
104
|
if (_disposed || !platformId) return;
|
|
56
105
|
if (!PLATFORM_ID_RE.test(platformId)) return; // 纵深防御:非白名单字符不放行(防 `IM_${id}` 路径穿越)
|
|
@@ -62,29 +111,40 @@ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_D
|
|
|
62
111
|
// 幂等仅当「目录路径未变且仍存在」时成立;否则关旧重建:
|
|
63
112
|
// - reg.dir !== dir:LOG_DIR 运行时切换(切项目),旧 watcher 还盯着旧目录,新目录无人监听;
|
|
64
113
|
// - !exists(dir):目录被删(某些平台 fs.watch 删目录不报 error,留下永不恢复的幽灵监听)。
|
|
65
|
-
if (reg.dir === dir && _exists(dir))
|
|
66
|
-
|
|
114
|
+
if (reg.dir === dir && _exists(dir)) {
|
|
115
|
+
_ensureSessionsWatch(platformId, reg); // sessions/ may have appeared since
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
_closeReg(reg);
|
|
67
119
|
watchers.delete(platformId);
|
|
68
120
|
}
|
|
69
121
|
_mkdir(dir);
|
|
70
122
|
let w;
|
|
123
|
+
const newReg = { w: null, dir, sessionsW: null, sidWatchers: new Map() };
|
|
71
124
|
try {
|
|
72
125
|
w = _watch(dir, (_eventType, filename) => {
|
|
126
|
+
// v2: 'sessions' appearing (or any event) is the moment to arm the
|
|
127
|
+
// deeper watchers; legacy v1 .jsonl writes keep firing directly.
|
|
128
|
+
if (String(filename || '') === 'sessions' || !filename) {
|
|
129
|
+
_ensureSessionsWatch(platformId, newReg);
|
|
130
|
+
}
|
|
73
131
|
if (_relevant(filename)) _schedule(platformId);
|
|
74
132
|
});
|
|
75
133
|
} catch { return; }
|
|
76
134
|
// watcher 出错(目录被删等)→ 关闭并撤销登记,下次 ensure 可重建。
|
|
77
135
|
if (w && typeof w.on === 'function') {
|
|
78
|
-
w.on('error', () => {
|
|
136
|
+
w.on('error', () => { _closeReg(newReg); watchers.delete(platformId); });
|
|
79
137
|
}
|
|
80
|
-
|
|
138
|
+
newReg.w = w;
|
|
139
|
+
watchers.set(platformId, newReg);
|
|
140
|
+
_ensureSessionsWatch(platformId, newReg);
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
function dispose() {
|
|
84
144
|
_disposed = true;
|
|
85
145
|
for (const t of timers.values()) clearTimeout(t);
|
|
86
146
|
timers.clear();
|
|
87
|
-
for (const reg of watchers.values())
|
|
147
|
+
for (const reg of watchers.values()) _closeReg(reg);
|
|
88
148
|
watchers.clear();
|
|
89
149
|
}
|
|
90
150
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join, basename } from 'node:path';
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
4
3
|
|
|
5
4
|
const SUBAGENT_SYSTEM_RE = /(?:command execution|file search|planning) specialist|general-purpose agent|security monitor|performing a web search/i;
|
|
6
5
|
|
|
@@ -15,7 +14,9 @@ const SUBAGENT_BILLING_RE = /cc_is_subagent=true\b/;
|
|
|
15
14
|
// 两处服务端实现(本文件 + kv-cache-analyzer)由 test/interceptor-core-mainagent.test.js 互校防漂移;
|
|
16
15
|
// 前端 contentFilter 那份由 test/content-filter-unit.test.js 单测覆盖。
|
|
17
16
|
const TEAMMATE_SYSTEM_RE = /running as an agent in a team|Agent Teammate Communication/i;
|
|
18
|
-
//
|
|
17
|
+
// 1.7.0: the last external consumer (teammate-detect.js) retired with the
|
|
18
|
+
// prev-segment backfill; kept unexported-in-spirit for the KEEP-IN-SYNC trio
|
|
19
|
+
// above (tests still cross-check the three copies).
|
|
19
20
|
export { TEAMMATE_SYSTEM_RE, SUBAGENT_BILLING_RE };
|
|
20
21
|
|
|
21
22
|
// Rotation carry-forward: prompt-prefix → teammate-name pairs extracted from a
|
|
@@ -40,20 +41,6 @@ export function extractAgentSpawnPairs(responseBody) {
|
|
|
40
41
|
return pairs;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// Parses the first frame of a log-file head string; returns the entry when it
|
|
44
|
-
// is a rotation-context sentinel, else null. Callers pass a BOUNDED head read
|
|
45
|
-
// (never a whole segment).
|
|
46
|
-
export function parseRotationContextHead(headString) {
|
|
47
|
-
try {
|
|
48
|
-
const frameEnd = headString.indexOf('\n---\n');
|
|
49
|
-
if (frameEnd <= 0) return null;
|
|
50
|
-
const entry = JSON.parse(headString.slice(0, frameEnd));
|
|
51
|
-
return entry && entry.ccvRotationContext ? entry : null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
export function getSystemText(body) {
|
|
58
45
|
const system = body?.system;
|
|
59
46
|
if (typeof system === 'string') return system;
|
|
@@ -298,29 +285,27 @@ export function createStreamAssembler() {
|
|
|
298
285
|
};
|
|
299
286
|
}
|
|
300
287
|
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
288
|
+
// Log filename prefix (single source): v1 files were always `<project>_<ts>.jsonl`
|
|
289
|
+
// (`<pid>__`-prefixed variants came from the removed multi-instance feature and
|
|
290
|
+
// are excluded by the matcher below). Kept for the v1 lookups that still exist
|
|
291
|
+
// (migration-era readers, e.g. findRecentLog for IM).
|
|
292
|
+
export function logFilePrefix(projectName) {
|
|
293
|
+
return `${projectName}_`;
|
|
305
294
|
}
|
|
306
295
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const p = logFilePrefix(projectName, instanceId);
|
|
314
|
-
if (instanceId) return (f) => f.startsWith(p);
|
|
296
|
+
// v1 log filename ownership test: starts with `<project>_` and does NOT carry
|
|
297
|
+
// the legacy `<pid>__<project>_` instance mark (any instance-tagged file
|
|
298
|
+
// contains `__<project>_` by construction; untagged files never do, even when
|
|
299
|
+
// the project name itself contains `__`).
|
|
300
|
+
export function logFileMatcher(projectName) {
|
|
301
|
+
const p = logFilePrefix(projectName);
|
|
315
302
|
const pidMark = `__${projectName}_`;
|
|
316
303
|
return (f) => f.startsWith(p) && !f.includes(pidMark);
|
|
317
304
|
}
|
|
318
305
|
|
|
319
|
-
|
|
320
|
-
// 现有 2 参调用方与单测零改动。
|
|
321
|
-
export function findRecentLog(dir, projectName, instanceId = null) {
|
|
306
|
+
export function findRecentLog(dir, projectName) {
|
|
322
307
|
try {
|
|
323
|
-
const owns = logFileMatcher(projectName
|
|
308
|
+
const owns = logFileMatcher(projectName);
|
|
324
309
|
const files = readdirSync(dir)
|
|
325
310
|
// 排除 *_temp.jsonl:临时文件是未完成的写入态(resume 流程中途产物),
|
|
326
311
|
// 不应被当作"最近完整日志"(否则 _temp 因 sort 排在正式文件之后会被误选)。
|
|
@@ -333,114 +318,6 @@ export function findRecentLog(dir, projectName, instanceId = null) {
|
|
|
333
318
|
return null;
|
|
334
319
|
}
|
|
335
320
|
|
|
336
|
-
// 首启接管(仅 --pid 实例):本 pid 还没有自己的日志时,把项目里最近的【无标签】日志原子 rename 成
|
|
337
|
-
// `<pid>__<project>_<ts>.jsonl`,并入该 pid 血脉(move 而非 copy:不双计入统计)。返回接管后的新路径,或 null。
|
|
338
|
-
//
|
|
339
|
-
// 这是 best-effort 便利特性,不是核心隔离(核心隔离=各 pid 读自己的日志,不依赖接管)。为不误抢一个仍存活的
|
|
340
|
-
// 无 pid 实例(评审标记的 live-writer / 卡在 resume 的窄边角),双重守卫:① 项目里若存在任何无标签 `*_temp.jsonl`
|
|
341
|
-
// (某个无 pid 实例正卡在 resume / 在写)→ 放弃;② 无标签日志 mtime 必须早于 freshnessMs(默认 5min)。仍有理论窄缝
|
|
342
|
-
// (进程挂起 >5min 且无 temp),属可接受的便利取舍。并发下原子 rename 只一个赢、输者 throw→吞→null(回退全新)。
|
|
343
|
-
export function claimUntaggedLog(dir, projectName, instanceId, { freshnessMs = 300000 } = {}) {
|
|
344
|
-
if (!instanceId) return null;
|
|
345
|
-
try {
|
|
346
|
-
const untagged = findRecentLog(dir, projectName, null); // 最近无标签日志(已排除 pid 文件 + temp)
|
|
347
|
-
if (!untagged) return null;
|
|
348
|
-
// ① 存在无标签 temp ⇒ 有无 pid 实例正活动/卡在 resume → 不接管。
|
|
349
|
-
const owns = logFileMatcher(projectName, null);
|
|
350
|
-
const hasUntaggedTemp = readdirSync(dir).some(f => owns(f) && f.endsWith('_temp.jsonl'));
|
|
351
|
-
if (hasUntaggedTemp) return null;
|
|
352
|
-
let st;
|
|
353
|
-
try { st = statSync(untagged); } catch { return null; }
|
|
354
|
-
if (Date.now() - st.mtimeMs < freshnessMs) return null; // ② 可能是活动写者 → 不碰
|
|
355
|
-
const claimed = join(dir, `${instanceId}__${basename(untagged)}`);
|
|
356
|
-
if (existsSync(claimed)) return null; // 极端撞名 → 放弃
|
|
357
|
-
renameSyncWithRetry(untagged, claimed);
|
|
358
|
-
return claimed;
|
|
359
|
-
} catch { return null; }
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export function cleanupTempFiles(dir, projectName, instanceId = null) {
|
|
363
|
-
try {
|
|
364
|
-
const owns = logFileMatcher(projectName, instanceId);
|
|
365
|
-
const tempFiles = readdirSync(dir)
|
|
366
|
-
.filter(f => owns(f) && f.endsWith('_temp.jsonl'));
|
|
367
|
-
for (const f of tempFiles) {
|
|
368
|
-
try {
|
|
369
|
-
const tempPath = join(dir, f);
|
|
370
|
-
const newPath = tempPath.replace('_temp.jsonl', '.jsonl');
|
|
371
|
-
if (existsSync(newPath)) {
|
|
372
|
-
const tempContent = readFileSync(tempPath, 'utf-8');
|
|
373
|
-
if (tempContent.trim()) {
|
|
374
|
-
appendFileSync(newPath, tempContent);
|
|
375
|
-
}
|
|
376
|
-
unlinkSync(tempPath);
|
|
377
|
-
} else {
|
|
378
|
-
// 只有非空 temp 文件才 rename,空文件直接删除
|
|
379
|
-
const sz = statSync(tempPath).size;
|
|
380
|
-
if (sz > 0) {
|
|
381
|
-
renameSyncWithRetry(tempPath, newPath);
|
|
382
|
-
} else {
|
|
383
|
-
unlinkSync(tempPath);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} catch { }
|
|
387
|
-
}
|
|
388
|
-
} catch { }
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
export function migrateConversationContext(oldFile, newFile) {
|
|
392
|
-
try {
|
|
393
|
-
const content = readFileSync(oldFile, 'utf-8');
|
|
394
|
-
if (!content.trim()) return;
|
|
395
|
-
|
|
396
|
-
const parts = content.split('\n---\n').filter(p => p.trim());
|
|
397
|
-
if (parts.length === 0) return;
|
|
398
|
-
|
|
399
|
-
let originIndex = -1;
|
|
400
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
401
|
-
if (!/"mainAgent"\s*:\s*true/.test(parts[i])) continue;
|
|
402
|
-
try {
|
|
403
|
-
const entry = JSON.parse(parts[i]);
|
|
404
|
-
if (entry.mainAgent) {
|
|
405
|
-
const msgs = entry.body?.messages;
|
|
406
|
-
// Delta storage: 使用 _totalMessageCount(delta 条目)或 msgs.length(旧格式)
|
|
407
|
-
const msgCount = entry._totalMessageCount || (Array.isArray(msgs) ? msgs.length : 0);
|
|
408
|
-
if (msgCount === 1) {
|
|
409
|
-
originIndex = i;
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
} catch { }
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (originIndex < 0) return;
|
|
417
|
-
|
|
418
|
-
let migrationStart = originIndex;
|
|
419
|
-
if (originIndex > 0) {
|
|
420
|
-
try {
|
|
421
|
-
const prevContent = parts[originIndex - 1];
|
|
422
|
-
if (prevContent.trim().startsWith('{')) {
|
|
423
|
-
const prev = JSON.parse(prevContent);
|
|
424
|
-
if (isPreflightEntry(prev)) {
|
|
425
|
-
migrationStart = originIndex - 1;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
} catch { }
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const migratedParts = parts.slice(migrationStart);
|
|
432
|
-
writeFileSync(newFile, migratedParts.join('\n---\n') + '\n---\n');
|
|
433
|
-
|
|
434
|
-
const remainingParts = parts.slice(0, migrationStart);
|
|
435
|
-
if (remainingParts.length > 0) {
|
|
436
|
-
writeFileSync(oldFile, remainingParts.join('\n---\n') + '\n---\n');
|
|
437
|
-
} else {
|
|
438
|
-
// 所有内容已迁移到新文件,清空旧文件(不能删除,watcher 需要检测 truncation 来触发轮转)
|
|
439
|
-
writeFileSync(oldFile, '');
|
|
440
|
-
}
|
|
441
|
-
} catch { }
|
|
442
|
-
}
|
|
443
|
-
|
|
444
321
|
/**
|
|
445
322
|
* 计算单条 message 的轻量身份指纹,用于 delta storage 的 in-place last-msg replace 检测。
|
|
446
323
|
* 仅服务端 interceptor 使用 —— 触发 Plan C checkpoint 让客户端拿到 wire 真实内容。
|
|
@@ -479,38 +356,6 @@ export function fingerprintMsg(m) {
|
|
|
479
356
|
return (m.role || '?') + ':' + snip.replace(/\s+/g, ' ').slice(0, 80);
|
|
480
357
|
}
|
|
481
358
|
|
|
482
|
-
/**
|
|
483
|
-
* Rotate log file when it exceeds maxSize.
|
|
484
|
-
* Creates a new file (no content migration) and appends '\n' to old file
|
|
485
|
-
* to trigger fs.watchFile callback for watcher migration.
|
|
486
|
-
*
|
|
487
|
-
* initialContent (optional) is written INTO the new file at creation time —
|
|
488
|
-
* used for the rotation-context sentinel. It must be baked into creation
|
|
489
|
-
* rather than queued afterwards: the old-file trigger byte below fires the
|
|
490
|
-
* watcher's rotation-follow after a short debounce, and a late-flushing
|
|
491
|
-
* queued write can land between the new file's initial stream read and its
|
|
492
|
-
* lastByteOffset snapshot, in which case it is never delivered to clients.
|
|
493
|
-
*
|
|
494
|
-
* @param {string} currentFile - current log file path
|
|
495
|
-
* @param {string} newFile - new log file path to rotate to
|
|
496
|
-
* @param {number} maxSize - max file size in bytes
|
|
497
|
-
* @param {string} [initialContent] - content the new file is created with
|
|
498
|
-
* @returns {{ rotated: boolean, oldFile?: string, newFile?: string }}
|
|
499
|
-
*/
|
|
500
|
-
export function rotateLogFile(currentFile, newFile, maxSize, initialContent = '') {
|
|
501
|
-
try {
|
|
502
|
-
if (!existsSync(currentFile)) return { rotated: false };
|
|
503
|
-
const size = statSync(currentFile).size;
|
|
504
|
-
if (size < maxSize) return { rotated: false };
|
|
505
|
-
// 不迁移旧内容,创建新文件(立即创建,避免 watcher 时序窗口)
|
|
506
|
-
try { writeFileSync(newFile, initialContent); } catch { }
|
|
507
|
-
// 触发旧文件 watcher 回调,使其检测到文件变更并切换到新文件
|
|
508
|
-
try { appendFileSync(currentFile, '\n'); } catch { }
|
|
509
|
-
return { rotated: true, oldFile: currentFile, newFile };
|
|
510
|
-
} catch { }
|
|
511
|
-
return { rotated: false };
|
|
512
|
-
}
|
|
513
|
-
|
|
514
359
|
/**
|
|
515
360
|
* 在原始 JSON 字符串上定向替换顶层 "model" 字段的值,避免对巨型 wire body
|
|
516
361
|
* (`-c` 重启后的全量 checkpoint 请求可达数十 MB)做二次 JSON.parse + 全量 re-stringify。
|