claude-code-session-manager 0.21.1 → 0.21.3
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/bin/cli.cjs +5 -0
- package/dist/assets/{TiptapBody-C46DacIO.js → TiptapBody-PdmsfUCQ.js} +2 -2
- package/dist/assets/cssMode-DfqZGMQs.js +1 -0
- package/dist/assets/{freemarker2-BxIPNQn-.js → freemarker2-XTPYh37h.js} +1 -1
- package/dist/assets/handlebars-DKUF5VyH.js +1 -0
- package/dist/assets/html-uqoqsIeI.js +1 -0
- package/dist/assets/htmlMode-aMTQs1su.js +1 -0
- package/dist/assets/index-DO3ROR11.js +3525 -0
- package/dist/assets/index-DeQI4oVI.css +32 -0
- package/dist/assets/javascript-BVxRZMds.js +1 -0
- package/dist/assets/{jsonMode-1FAJaHiX.js → jsonMode-D04xP2s5.js} +4 -4
- package/dist/assets/liquid-BkQHTH2P.js +1 -0
- package/dist/assets/lspLanguageFeatures-By9uLznH.js +4 -0
- package/dist/assets/mdx-Du1IlbjV.js +1 -0
- package/dist/assets/{index-oGyPFfYZ.css → monaco-editor-BTnBOi8r.css} +1 -32
- package/dist/assets/monaco-editor-BW5C4Iv1.js +908 -0
- package/dist/assets/python-DSlImqXd.js +1 -0
- package/dist/assets/razor-BmUVyvSK.js +1 -0
- package/dist/assets/{tsMode-CLQIVays.js → tsMode-Btj0TTH7.js} +1 -1
- package/dist/assets/typescript-Bzelq9vO.js +1 -0
- package/dist/assets/xml-Whd9EaSd.js +1 -0
- package/dist/assets/yaml-QYf0-IN8.js +1 -0
- package/dist/index.html +4 -2
- package/package.json +1 -1
- package/src/main/__tests__/runVerify.test.cjs +101 -0
- package/src/main/config.cjs +36 -4
- package/src/main/historyAggregator.cjs +400 -149
- package/src/main/index.cjs +8 -0
- package/src/main/ipcSchemas.cjs +42 -13
- package/src/main/kg.cjs +87 -30
- package/src/main/lib/credentials.cjs +7 -0
- package/src/main/lib/e2eStateMachine.cjs +39 -0
- package/src/main/runVerify.cjs +28 -5
- package/src/main/scheduler/prdParser.cjs +16 -1
- package/src/main/scheduler.cjs +97 -13
- package/src/main/transcripts.cjs +141 -19
- package/src/main/usageMatrix.cjs +7 -3
- package/src/main/webRemote.cjs +190 -29
- package/src/preload/api.d.ts +40 -0
- package/src/preload/index.cjs +7 -0
- package/dist/assets/cssMode-CauFS5Bp.js +0 -1
- package/dist/assets/handlebars-DnEVFUsu.js +0 -1
- package/dist/assets/html-S8NXUTqc.js +0 -1
- package/dist/assets/htmlMode-rSEyII9x.js +0 -1
- package/dist/assets/index-DMobTczM.js +0 -4431
- package/dist/assets/javascript-BiWR68QP.js +0 -1
- package/dist/assets/liquid-CEtOkbwI.js +0 -1
- package/dist/assets/lspLanguageFeatures-CRF3U0x3.js +0 -4
- package/dist/assets/mdx-C7C95Bzt.js +0 -1
- package/dist/assets/python-CXvKcjLk.js +0 -1
- package/dist/assets/razor-tzZHfRy2.js +0 -1
- package/dist/assets/typescript-LxhyM9W2.js +0 -1
- package/dist/assets/xml-VS_m20VE.js +0 -1
- package/dist/assets/yaml-BsjggdVD.js +0 -1
package/src/main/transcripts.cjs
CHANGED
|
@@ -43,6 +43,44 @@ function transcriptPath(cwd, sessionUuid) {
|
|
|
43
43
|
return path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd), `${sessionUuid}.jsonl`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const MAX_RAW_STR = 4096;
|
|
47
|
+
|
|
48
|
+
// Block types whose text/content fields are parsed structurally by
|
|
49
|
+
// orchestrator.ts / race.ts — truncating them produces mid-token "…" and
|
|
50
|
+
// unparseable JSON, so they are exempt from the size cap.
|
|
51
|
+
const EXEMPT_TYPES = new Set(['tool_result', 'tool_use']);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cap string fields in a content block array so arbitrary tool output doesn't
|
|
55
|
+
* bloat the ring buffer. Blocks whose type is in EXEMPT_TYPES are passed
|
|
56
|
+
* through intact so that structured result payloads survive to the digest
|
|
57
|
+
* parsers in race.ts / orchestrator.ts.
|
|
58
|
+
*/
|
|
59
|
+
function trimContentArray(content) {
|
|
60
|
+
if (!Array.isArray(content)) return content;
|
|
61
|
+
return content.map((block) => {
|
|
62
|
+
if (!block || typeof block !== 'object') return block;
|
|
63
|
+
if (EXEMPT_TYPES.has(block.type)) return block;
|
|
64
|
+
const b = { ...block };
|
|
65
|
+
if (typeof b.text === 'string' && b.text.length > MAX_RAW_STR) {
|
|
66
|
+
b.text = b.text.slice(0, MAX_RAW_STR) + '…';
|
|
67
|
+
}
|
|
68
|
+
if (typeof b.content === 'string' && b.content.length > MAX_RAW_STR) {
|
|
69
|
+
b.content = b.content.slice(0, MAX_RAW_STR) + '…';
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(b.content)) {
|
|
72
|
+
b.content = trimContentArray(b.content);
|
|
73
|
+
}
|
|
74
|
+
return b;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Build the slim raw projection used by race.ts and orchestrator.ts. */
|
|
79
|
+
function makeRaw(obj) {
|
|
80
|
+
const msgContent = obj?.message?.content;
|
|
81
|
+
return { message: { content: trimContentArray(msgContent) } };
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
* Parse one JSONL line defensively. Real schema drifts, so we pass through
|
|
48
86
|
* anything that parses and tag a coarse `kind`.
|
|
@@ -56,7 +94,7 @@ function classifyLine(obj) {
|
|
|
56
94
|
|
|
57
95
|
// Usage rollups arrive as summary events.
|
|
58
96
|
if (obj.usage || msg?.usage) {
|
|
59
|
-
return { kind: 'usage', data: obj.usage || msg.usage, raw: obj };
|
|
97
|
+
return { kind: 'usage', data: obj.usage || msg.usage, raw: makeRaw(obj) };
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
// Tool uses: scan content array for tool_use blocks.
|
|
@@ -64,31 +102,31 @@ function classifyLine(obj) {
|
|
|
64
102
|
for (const block of content) {
|
|
65
103
|
if (block?.type === 'tool_use') {
|
|
66
104
|
if (block.name === 'TodoWrite') {
|
|
67
|
-
return { kind: 'todo_write', data: block.input?.todos || block.input || [], raw: obj };
|
|
105
|
+
return { kind: 'todo_write', data: block.input?.todos || block.input || [], raw: makeRaw(obj) };
|
|
68
106
|
}
|
|
69
107
|
if (block.name === 'ExitPlanMode' || block.name === 'EnterPlanMode') {
|
|
70
|
-
return { kind: 'plan', data: block.input, raw: obj };
|
|
108
|
+
return { kind: 'plan', data: block.input, raw: makeRaw(obj) };
|
|
71
109
|
}
|
|
72
110
|
if (block.name === 'Agent' || block.name === 'Task') {
|
|
73
111
|
// Include block.id as toolUseId so the live store can match the
|
|
74
112
|
// corresponding tool_result and update per-agent lastActivityAt.
|
|
75
|
-
return { kind: 'agent_spawn', data: { ...block.input, toolUseId: block.id }, raw: obj };
|
|
113
|
+
return { kind: 'agent_spawn', data: { ...block.input, toolUseId: block.id }, raw: makeRaw(obj) };
|
|
76
114
|
}
|
|
77
115
|
return {
|
|
78
116
|
kind: 'tool_use',
|
|
79
117
|
data: { name: block.name, input: block.input, id: block.id },
|
|
80
|
-
raw: obj,
|
|
118
|
+
raw: makeRaw(obj),
|
|
81
119
|
};
|
|
82
120
|
}
|
|
83
121
|
// tool_result carries the tool_use_id of the completed Task/Agent call.
|
|
84
122
|
// The live store uses this to update the agent's lastActivityAt bookend.
|
|
85
123
|
if (block?.type === 'tool_result' && block.tool_use_id) {
|
|
86
|
-
return { kind: 'tool_result', data: { toolUseId: block.tool_use_id }, raw: obj };
|
|
124
|
+
return { kind: 'tool_result', data: { toolUseId: block.tool_use_id }, raw: makeRaw(obj) };
|
|
87
125
|
}
|
|
88
126
|
}
|
|
89
127
|
}
|
|
90
128
|
|
|
91
|
-
return { kind: type || 'message', data: obj, raw: obj };
|
|
129
|
+
return { kind: type || 'message', data: obj, raw: makeRaw(obj) };
|
|
92
130
|
}
|
|
93
131
|
|
|
94
132
|
/**
|
|
@@ -129,7 +167,7 @@ async function readDelta(sub) {
|
|
|
129
167
|
}
|
|
130
168
|
}
|
|
131
169
|
|
|
132
|
-
async function
|
|
170
|
+
async function doFlush(sub, { emit = true, replay = false } = {}) {
|
|
133
171
|
const lines = await readDelta(sub);
|
|
134
172
|
for (const line of lines) {
|
|
135
173
|
let obj;
|
|
@@ -150,6 +188,7 @@ async function flush(sub, { emit = true } = {}) {
|
|
|
150
188
|
cwd: sub.cwd,
|
|
151
189
|
sessionUuid: sub.sessionUuid,
|
|
152
190
|
ev,
|
|
191
|
+
replay,
|
|
153
192
|
});
|
|
154
193
|
if (emit) sendIfAlive(window, `transcript:event:${sub.tabId}`, ev);
|
|
155
194
|
// Mirror to OTEL — no-op when disabled. We emit on the initial drain too
|
|
@@ -164,10 +203,86 @@ async function flush(sub, { emit = true } = {}) {
|
|
|
164
203
|
}
|
|
165
204
|
}
|
|
166
205
|
|
|
206
|
+
// Serialised flush scheduler — at most one readDelta per sub in flight at a
|
|
207
|
+
// time. Uses a dirty flag for trailing-edge re-run: if a chokidar event fires
|
|
208
|
+
// while a flush is in progress, dirty stays true and the loop runs one more
|
|
209
|
+
// time after the current read completes, guaranteeing no event is dropped.
|
|
210
|
+
function scheduleFlush(sub) {
|
|
211
|
+
sub.dirty = true;
|
|
212
|
+
if (sub.flushing) return sub.flushing;
|
|
213
|
+
sub.flushing = (async () => {
|
|
214
|
+
while (sub.dirty) {
|
|
215
|
+
sub.dirty = false;
|
|
216
|
+
await doFlush(sub);
|
|
217
|
+
}
|
|
218
|
+
})()
|
|
219
|
+
.catch((e) => {
|
|
220
|
+
logs.writeLine({ level: 'warn', scope: 'transcripts', message: 'flush error', meta: { error: e?.message } });
|
|
221
|
+
})
|
|
222
|
+
.finally(() => {
|
|
223
|
+
sub.flushing = null;
|
|
224
|
+
});
|
|
225
|
+
return sub.flushing;
|
|
226
|
+
}
|
|
227
|
+
|
|
167
228
|
const MAX_TRANSCRIPT_SUBS = 20;
|
|
168
229
|
|
|
230
|
+
/**
|
|
231
|
+
* LRU pool of released-but-cached subscriptions. When a renderer consumer
|
|
232
|
+
* calls release(), the sub stays alive (offset + buffer preserved) so a
|
|
233
|
+
* subsequent tab-switch back resumes from the current offset instead of
|
|
234
|
+
* re-reading the entire transcript from byte 0. Oldest entries are evicted
|
|
235
|
+
* once the pool exceeds LRU_CAP.
|
|
236
|
+
*/
|
|
237
|
+
const LRU_CAP = 6;
|
|
238
|
+
const lruReleased = []; // tabIds with no active consumer, ordered oldest→newest
|
|
239
|
+
|
|
240
|
+
function _closeSub(tabId) {
|
|
241
|
+
const sub = subs.get(tabId);
|
|
242
|
+
if (!sub) return;
|
|
243
|
+
sub.watcher?.close().catch(() => {});
|
|
244
|
+
subs.delete(tabId);
|
|
245
|
+
usageMatrix.removeTab(tabId);
|
|
246
|
+
const i = lruReleased.indexOf(tabId);
|
|
247
|
+
if (i !== -1) lruReleased.splice(i, 1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* release(tabId) — called when the renderer's last consumer unmounts (view
|
|
252
|
+
* switch). Keeps the sub alive in the LRU cache so a quick revisit resumes
|
|
253
|
+
* from the persisted offset. Evicts the oldest cached sub if over LRU_CAP.
|
|
254
|
+
*/
|
|
255
|
+
function release(tabId) {
|
|
256
|
+
if (!subs.has(tabId)) return;
|
|
257
|
+
if (!lruReleased.includes(tabId)) {
|
|
258
|
+
lruReleased.push(tabId);
|
|
259
|
+
}
|
|
260
|
+
while (lruReleased.length > LRU_CAP) {
|
|
261
|
+
const oldest = lruReleased.shift();
|
|
262
|
+
_closeSub(oldest);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** closeTab(tabId) — genuine tab close; always destroys the sub immediately. */
|
|
267
|
+
function closeTab(tabId) {
|
|
268
|
+
_closeSub(tabId);
|
|
269
|
+
}
|
|
270
|
+
|
|
169
271
|
async function subscribe({ tabId, cwd, sessionUuid }) {
|
|
170
|
-
if (subs.has(tabId))
|
|
272
|
+
if (subs.has(tabId)) {
|
|
273
|
+
// Tab is in the LRU cache — promote it back to active.
|
|
274
|
+
const i = lruReleased.indexOf(tabId);
|
|
275
|
+
if (i !== -1) lruReleased.splice(i, 1);
|
|
276
|
+
return { ok: true, path: subs.get(tabId).filePath };
|
|
277
|
+
}
|
|
278
|
+
if (subs.size >= MAX_TRANSCRIPT_SUBS) {
|
|
279
|
+
// Before rejecting a genuinely new subscription, evict an idle LRU-cached
|
|
280
|
+
// entry — it occupies a slot but has no active consumer. Only reject if no
|
|
281
|
+
// idle entries are available to free.
|
|
282
|
+
if (lruReleased.length > 0) {
|
|
283
|
+
_closeSub(lruReleased[0]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
171
286
|
if (subs.size >= MAX_TRANSCRIPT_SUBS) {
|
|
172
287
|
logs.writeLine({
|
|
173
288
|
level: 'warn',
|
|
@@ -189,34 +304,33 @@ async function subscribe({ tabId, cwd, sessionUuid }) {
|
|
|
189
304
|
pending: '',
|
|
190
305
|
buffer: [],
|
|
191
306
|
watcher: null,
|
|
307
|
+
flushing: null,
|
|
308
|
+
dirty: false,
|
|
192
309
|
};
|
|
193
310
|
// If the file already exists, read current content as replay. Do not emit
|
|
194
311
|
// during this initial drain — the renderer drains sub.buffer via
|
|
195
312
|
// `transcript:buffer` after `transcript:subscribe` resolves. Emitting here
|
|
196
313
|
// would race the renderer's onEvent listener registration and drop events.
|
|
314
|
+
// replay:true prevents historical usage events from entering the 5-min window.
|
|
197
315
|
if (fs.existsSync(filePath)) {
|
|
198
|
-
await
|
|
316
|
+
await doFlush(sub, { emit: false, replay: true });
|
|
199
317
|
}
|
|
200
318
|
const watcher = chokidar.watch(filePath, {
|
|
201
319
|
ignoreInitial: false,
|
|
202
320
|
persistent: true,
|
|
203
321
|
awaitWriteFinish: { stabilityThreshold: 30, pollInterval: 20 },
|
|
204
322
|
});
|
|
205
|
-
watcher.on('add', () =>
|
|
206
|
-
watcher.on('change', () =>
|
|
323
|
+
watcher.on('add', () => scheduleFlush(sub));
|
|
324
|
+
watcher.on('change', () => scheduleFlush(sub));
|
|
207
325
|
watcher.on('error', (err) => logs.writeLine({ level: 'warn', scope: 'transcripts', message: 'chokidar watcher error', meta: { error: err?.message } }));
|
|
208
326
|
sub.watcher = watcher;
|
|
209
327
|
subs.set(tabId, sub);
|
|
210
328
|
return { ok: true, path: filePath };
|
|
211
329
|
}
|
|
212
330
|
|
|
331
|
+
/** @deprecated Use release() for view-switch, closeTab() for genuine close. */
|
|
213
332
|
function unsubscribe(tabId) {
|
|
214
|
-
|
|
215
|
-
if (!sub) return;
|
|
216
|
-
sub.watcher?.close().catch(() => {});
|
|
217
|
-
subs.delete(tabId);
|
|
218
|
-
// Drop the tab from the AgOps matrix — "active sessions" only.
|
|
219
|
-
usageMatrix.removeTab(tabId);
|
|
333
|
+
release(tabId);
|
|
220
334
|
}
|
|
221
335
|
|
|
222
336
|
function getBuffer(tabId) {
|
|
@@ -233,8 +347,14 @@ function closeAll() {
|
|
|
233
347
|
function registerTranscriptHandlers() {
|
|
234
348
|
const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
|
|
235
349
|
ipcMain.handle('transcript:subscribe', v(s.transcriptSubscribe, (payload) => subscribe(payload)));
|
|
350
|
+
// transcript:unsubscribe is now an alias for release (view-switch, not close).
|
|
236
351
|
ipcMain.handle('transcript:unsubscribe', v(s.transcriptTabId, ({ tabId }) => {
|
|
237
|
-
|
|
352
|
+
release(tabId);
|
|
353
|
+
return { ok: true };
|
|
354
|
+
}));
|
|
355
|
+
// transcript:close is the genuine close used when a tab is removed.
|
|
356
|
+
ipcMain.handle('transcript:close', v(s.transcriptTabId, ({ tabId }) => {
|
|
357
|
+
closeTab(tabId);
|
|
238
358
|
return { ok: true };
|
|
239
359
|
}));
|
|
240
360
|
ipcMain.handle('transcript:buffer', v(s.transcriptTabId, ({ tabId }) => getBuffer(tabId)));
|
|
@@ -245,6 +365,8 @@ module.exports = {
|
|
|
245
365
|
attachWindow,
|
|
246
366
|
registerTranscriptHandlers,
|
|
247
367
|
closeAll,
|
|
368
|
+
release,
|
|
369
|
+
closeTab,
|
|
248
370
|
encodeCwd,
|
|
249
371
|
transcriptPath,
|
|
250
372
|
classifyLine,
|
package/src/main/usageMatrix.cjs
CHANGED
|
@@ -85,7 +85,7 @@ function ensureTab(tabId, cwd, sessionUuid) {
|
|
|
85
85
|
* Feed one classified transcript event into the per-tab aggregator. Called
|
|
86
86
|
* from transcripts.cjs for every event, both during replay and live.
|
|
87
87
|
*/
|
|
88
|
-
function recordEvent({ tabId, cwd, sessionUuid, ev }) {
|
|
88
|
+
function recordEvent({ tabId, cwd, sessionUuid, ev, replay = false }) {
|
|
89
89
|
if (!tabId || !ev) return;
|
|
90
90
|
const t = ensureTab(tabId, cwd, sessionUuid);
|
|
91
91
|
const now = Date.now();
|
|
@@ -109,8 +109,12 @@ function recordEvent({ tabId, cwd, sessionUuid, ev }) {
|
|
|
109
109
|
t.perTurnInputTokens.push(inTok);
|
|
110
110
|
if (t.perTurnInputTokens.length > TURN_RING) t.perTurnInputTokens.shift();
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// Historical events must not enter the 5-min sliding window — doing so
|
|
113
|
+
// would make tokensPerMin spike to "critical" on every tab switch.
|
|
114
|
+
if (!replay) {
|
|
115
|
+
t.tokenWindow.push({ ts: now, tokens: inTok + outTok });
|
|
116
|
+
pruneWindow(t.tokenWindow, now);
|
|
117
|
+
}
|
|
114
118
|
dirty = true;
|
|
115
119
|
break;
|
|
116
120
|
}
|