imcodes 2026.4.1875-dev.1861 → 2026.4.1881-dev.1865
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/shared/effort-levels.d.ts +4 -2
- package/dist/shared/effort-levels.d.ts.map +1 -1
- package/dist/shared/effort-levels.js +16 -2
- package/dist/shared/effort-levels.js.map +1 -1
- package/dist/src/context/materialization-coordinator.d.ts.map +1 -1
- package/dist/src/context/materialization-coordinator.js +94 -51
- package/dist/src/context/materialization-coordinator.js.map +1 -1
- package/dist/src/daemon/jsonl-parse-core.d.ts +79 -0
- package/dist/src/daemon/jsonl-parse-core.d.ts.map +1 -0
- package/dist/src/daemon/jsonl-parse-core.js +400 -0
- package/dist/src/daemon/jsonl-parse-core.js.map +1 -0
- package/dist/src/daemon/jsonl-parse-pool.d.ts +52 -0
- package/dist/src/daemon/jsonl-parse-pool.d.ts.map +1 -0
- package/dist/src/daemon/jsonl-parse-pool.js +173 -0
- package/dist/src/daemon/jsonl-parse-pool.js.map +1 -0
- package/dist/src/daemon/jsonl-parse-worker-bootstrap.mjs +30 -0
- package/dist/src/daemon/jsonl-parse-worker-types.d.ts +36 -0
- package/dist/src/daemon/jsonl-parse-worker-types.d.ts.map +1 -0
- package/dist/src/daemon/jsonl-parse-worker-types.js +2 -0
- package/dist/src/daemon/jsonl-parse-worker-types.js.map +1 -0
- package/dist/src/daemon/jsonl-parse-worker.d.ts +12 -0
- package/dist/src/daemon/jsonl-parse-worker.d.ts.map +1 -0
- package/dist/src/daemon/jsonl-parse-worker.js +47 -0
- package/dist/src/daemon/jsonl-parse-worker.js.map +1 -0
- package/dist/src/daemon/jsonl-watcher.d.ts.map +1 -1
- package/dist/src/daemon/jsonl-watcher.js +72 -412
- package/dist/src/daemon/jsonl-watcher.js.map +1 -1
- package/package.json +2 -2
|
@@ -21,8 +21,8 @@ import logger from '../util/logger.js';
|
|
|
21
21
|
import { resolveContextWindow } from '../util/model-context.js';
|
|
22
22
|
import { getSessionContextWindow } from './cc-presets.js';
|
|
23
23
|
import { registerWatcherControl, unregisterWatcherControl } from './watcher-controls.js';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { createParseContext, forgetSession as forgetSessionInCtx, parseLines as parseLinesInCtx, } from './jsonl-parse-core.js';
|
|
25
|
+
import { jsonlParsePool, isJsonlWorkerEnabled } from './jsonl-parse-pool.js';
|
|
26
26
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
27
27
|
/** Compute a filesystem-safe project key from an absolute path.
|
|
28
28
|
* Replaces / \ and : with '-'. Used by Claude Code and Codex watchers.
|
|
@@ -134,364 +134,26 @@ async function findLatestJsonl(dir) {
|
|
|
134
134
|
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
135
135
|
return join(dir, withStats[0].f);
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
?? '';
|
|
158
|
-
const text = String(val);
|
|
159
|
-
return text.split('\n')[0] ?? '';
|
|
160
|
-
}
|
|
161
|
-
const pendingClaudeToolCalls = new Map();
|
|
162
|
-
function getPendingClaudeTools(sessionName) {
|
|
163
|
-
let pending = pendingClaudeToolCalls.get(sessionName);
|
|
164
|
-
if (!pending) {
|
|
165
|
-
pending = new Map();
|
|
166
|
-
pendingClaudeToolCalls.set(sessionName, pending);
|
|
167
|
-
}
|
|
168
|
-
return pending;
|
|
169
|
-
}
|
|
170
|
-
function rememberClaudeToolCall(sessionName, pending) {
|
|
171
|
-
getPendingClaudeTools(sessionName).set(pending.id, pending);
|
|
172
|
-
}
|
|
173
|
-
function takeClaudeToolCall(sessionName, toolUseId) {
|
|
174
|
-
if (!toolUseId)
|
|
175
|
-
return undefined;
|
|
176
|
-
const pending = pendingClaudeToolCalls.get(sessionName);
|
|
177
|
-
if (!pending)
|
|
178
|
-
return undefined;
|
|
179
|
-
const tool = pending.get(toolUseId);
|
|
180
|
-
if (tool)
|
|
181
|
-
pending.delete(toolUseId);
|
|
182
|
-
if (pending.size === 0)
|
|
183
|
-
pendingClaudeToolCalls.delete(sessionName);
|
|
184
|
-
return tool;
|
|
185
|
-
}
|
|
186
|
-
function emitClaudeFileChange(sessionName, batch, eventId, ts) {
|
|
187
|
-
timelineEmitter.emit(sessionName, TIMELINE_EVENT_FILE_CHANGE, { batch }, {
|
|
188
|
-
source: 'daemon',
|
|
189
|
-
confidence: 'high',
|
|
190
|
-
eventId,
|
|
191
|
-
...(ts ? { ts } : {}),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
function buildClaudeToolEventId(sessionName, toolUseId, phase) {
|
|
195
|
-
return `cc-tool:${sessionName}:${toolUseId}:${phase}`;
|
|
196
|
-
}
|
|
197
|
-
function isClaudeFileChangeTool(name) {
|
|
198
|
-
return name === 'Edit' || name === 'MultiEdit' || name === 'Write' || name === 'NotebookEdit';
|
|
199
|
-
}
|
|
200
|
-
/** Patterns for system-injected messages that should not display as user messages. */
|
|
201
|
-
const SYSTEM_INJECT_RE = /<task-notification|<system-reminder|<command-name>|<command-message>|<local-command-|<bash-input>|<bash-stdout>|<bash-stderr>/;
|
|
202
|
-
function emitUserStringContent(sessionName, text, stableId, ts) {
|
|
203
|
-
if (!text.trim())
|
|
204
|
-
return;
|
|
205
|
-
// System-injected messages: don't show as user message, emit working signal instead
|
|
206
|
-
if (SYSTEM_INJECT_RE.test(text)) {
|
|
207
|
-
timelineEmitter.emit(sessionName, 'agent.status', {
|
|
208
|
-
status: 'processing',
|
|
209
|
-
label: 'Processing system event...',
|
|
210
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
timelineEmitter.emit(sessionName, 'user.message', {
|
|
214
|
-
text,
|
|
215
|
-
}, { source: 'daemon', confidence: 'high', ...(stableId ? { eventId: stableId('um') } : {}), ...(ts ? { ts } : {}) });
|
|
216
|
-
}
|
|
217
|
-
function emitAssistantStringContent(sessionName, text, stableId, ts) {
|
|
218
|
-
if (!text.trim())
|
|
219
|
-
return;
|
|
220
|
-
timelineEmitter.emit(sessionName, 'assistant.text', {
|
|
221
|
-
text,
|
|
222
|
-
streaming: false,
|
|
223
|
-
}, { source: 'daemon', confidence: 'high', ...(stableId ? { eventId: stableId('at') } : {}), ...(ts ? { ts } : {}) });
|
|
224
|
-
}
|
|
225
|
-
function emitClaudeToolCallBlock(sessionName, block, stableId, ts) {
|
|
226
|
-
if (!block.name)
|
|
227
|
-
return;
|
|
228
|
-
if (block.name === 'AskUserQuestion') {
|
|
229
|
-
const inp = block.input;
|
|
230
|
-
timelineEmitter.emit(sessionName, 'ask.question', {
|
|
231
|
-
toolUseId: block.id,
|
|
232
|
-
questions: inp?.['questions'] ?? [],
|
|
233
|
-
}, { source: 'daemon', confidence: 'high', ...(stableId ? { eventId: stableId('aq') } : {}), ...(ts ? { ts } : {}) });
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const input = block.input;
|
|
237
|
-
const toolUseId = block.id;
|
|
238
|
-
const isDeferredFileTool = isClaudeFileChangeTool(block.name) && !!toolUseId;
|
|
239
|
-
if (toolUseId) {
|
|
240
|
-
rememberClaudeToolCall(sessionName, {
|
|
241
|
-
id: toolUseId,
|
|
242
|
-
name: block.name,
|
|
243
|
-
input,
|
|
244
|
-
...(ts ? { ts } : {}),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (isDeferredFileTool)
|
|
248
|
-
return;
|
|
249
|
-
const callEventId = toolUseId ? buildClaudeToolEventId(sessionName, toolUseId, 'call') : (stableId ? stableId('tc') : undefined);
|
|
250
|
-
const summaryInput = extractToolInput(block.name, input);
|
|
251
|
-
timelineEmitter.emit(sessionName, 'tool.call', {
|
|
252
|
-
tool: block.name,
|
|
253
|
-
...(summaryInput ? { input: summaryInput } : (input ? { input } : {})),
|
|
254
|
-
}, {
|
|
255
|
-
source: 'daemon',
|
|
256
|
-
confidence: 'high',
|
|
257
|
-
...(callEventId ? { eventId: callEventId } : {}),
|
|
258
|
-
...(ts ? { ts } : {}),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
function emitClaudeToolResultBlock(sessionName, block, stableId, ts) {
|
|
262
|
-
const toolUseId = block.tool_use_id;
|
|
263
|
-
const pending = takeClaudeToolCall(sessionName, toolUseId);
|
|
264
|
-
const toolUseResult = asRecord(block.toolUseResult);
|
|
265
|
-
const contentResult = asRecord(block.content);
|
|
266
|
-
const normalized = pending
|
|
267
|
-
&& !block.is_error
|
|
268
|
-
? normalizeClaudeFileChange({
|
|
269
|
-
toolName: pending.name,
|
|
270
|
-
toolCallId: pending.id,
|
|
271
|
-
input: pending.input,
|
|
272
|
-
toolResult: toolUseResult ?? contentResult ?? undefined,
|
|
273
|
-
})
|
|
274
|
-
: null;
|
|
275
|
-
if (pending && isClaudeFileChangeTool(pending.name)) {
|
|
276
|
-
const summaryInput = extractToolInput(pending.name, pending.input);
|
|
277
|
-
timelineEmitter.emit(sessionName, 'tool.call', {
|
|
278
|
-
tool: pending.name,
|
|
279
|
-
...(summaryInput ? { input: summaryInput } : (pending.input ? { input: pending.input } : {})),
|
|
280
|
-
}, {
|
|
281
|
-
source: 'daemon',
|
|
282
|
-
confidence: 'high',
|
|
283
|
-
eventId: buildClaudeToolEventId(sessionName, pending.id, 'call'),
|
|
284
|
-
...(pending.ts ? { ts: pending.ts } : {}),
|
|
285
|
-
...(normalized ? { hidden: true } : {}),
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
if (normalized && pending) {
|
|
289
|
-
timelineEmitter.emit(sessionName, 'tool.result', {
|
|
290
|
-
...(block.is_error ? { error: String(block.content ?? 'error') } : {}),
|
|
291
|
-
...(toolUseResult?.content ? { output: toolUseResult.content } : {}),
|
|
292
|
-
}, {
|
|
293
|
-
source: 'daemon',
|
|
294
|
-
confidence: 'high',
|
|
295
|
-
eventId: buildClaudeToolEventId(sessionName, pending.id, 'result'),
|
|
296
|
-
...(ts ? { ts } : {}),
|
|
297
|
-
hidden: true,
|
|
298
|
-
});
|
|
299
|
-
emitClaudeFileChange(sessionName, normalized, `cc-file-change:${sessionName}:${pending.id}`, ts);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
const error = block.is_error ? String(block.content ?? 'error') : undefined;
|
|
303
|
-
const output = !error ? extractToolResultOutput(block) : undefined;
|
|
304
|
-
timelineEmitter.emit(sessionName, 'tool.result', {
|
|
305
|
-
...(error ? { error } : {}),
|
|
306
|
-
...(output ? { output } : {}),
|
|
307
|
-
}, {
|
|
308
|
-
source: 'daemon',
|
|
309
|
-
confidence: 'high',
|
|
310
|
-
...(toolUseId ? { eventId: buildClaudeToolEventId(sessionName, toolUseId, 'result') } : stableId ? { eventId: stableId('tr') } : {}),
|
|
311
|
-
...(ts ? { ts } : {}),
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Parse one JSONL line and emit timeline events.
|
|
316
|
-
* - assistant: emit assistant.text, assistant.thinking, tool.call
|
|
317
|
-
* - user: emit user.message, tool.result
|
|
318
|
-
*
|
|
319
|
-
* lineByteOffset: byte offset of this line in the file — used to generate stable
|
|
320
|
-
* eventIds so the same line always produces the same ID regardless of whether it
|
|
321
|
-
* arrives via real-time streaming or history replay.
|
|
322
|
-
*/
|
|
323
|
-
function parseLine(sessionName, line, lineByteOffset) {
|
|
324
|
-
if (!line.trim())
|
|
325
|
-
return;
|
|
326
|
-
let raw;
|
|
327
|
-
try {
|
|
328
|
-
raw = JSON.parse(line);
|
|
329
|
-
}
|
|
330
|
-
catch {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
// Extract original event timestamp from JSONL (CC writes ISO timestamp on each entry).
|
|
334
|
-
const lineTs = raw['timestamp'] ? new Date(raw['timestamp']).getTime() : undefined;
|
|
335
|
-
const ts = lineTs && isFinite(lineTs) ? lineTs : undefined;
|
|
336
|
-
// Stable ID generator: same line always gets same eventId across restarts.
|
|
337
|
-
let blockIdx = 0;
|
|
338
|
-
const stableId = lineByteOffset !== undefined
|
|
339
|
-
? (suffix) => `cc:${sessionName}:${lineByteOffset}:${suffix}:${blockIdx++}`
|
|
340
|
-
: undefined;
|
|
341
|
-
// Progress events — transient status for status bar display (no message.content)
|
|
342
|
-
if (raw['type'] === 'progress') {
|
|
343
|
-
const data = raw['data'];
|
|
344
|
-
if (!data)
|
|
345
|
-
return;
|
|
346
|
-
const progressType = String(data['type'] ?? '');
|
|
347
|
-
switch (progressType) {
|
|
348
|
-
case 'bash_progress': {
|
|
349
|
-
const elapsed = data['elapsedTimeSeconds'];
|
|
350
|
-
timelineEmitter.emit(sessionName, 'agent.status', {
|
|
351
|
-
status: 'bash_running',
|
|
352
|
-
label: `Bash running${elapsed ? ` (${Math.round(elapsed)}s)` : ''}...`,
|
|
353
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
case 'agent_progress': {
|
|
357
|
-
const raw = data['message'];
|
|
358
|
-
let msg = 'working';
|
|
359
|
-
if (typeof raw === 'string') {
|
|
360
|
-
msg = raw;
|
|
361
|
-
}
|
|
362
|
-
else if (raw && typeof raw === 'object') {
|
|
363
|
-
const role = raw.type ?? raw.role ?? '';
|
|
364
|
-
msg = String(role) || 'working';
|
|
365
|
-
}
|
|
366
|
-
timelineEmitter.emit(sessionName, 'agent.status', {
|
|
367
|
-
status: 'agent_working',
|
|
368
|
-
label: `Sub-agent: ${msg}`,
|
|
369
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
case 'mcp_progress': {
|
|
373
|
-
const toolName = String(data['toolName'] ?? 'tool');
|
|
374
|
-
const server = String(data['serverName'] ?? '');
|
|
375
|
-
const mStatus = String(data['status'] ?? 'started');
|
|
376
|
-
if (mStatus === 'started') {
|
|
377
|
-
timelineEmitter.emit(sessionName, 'agent.status', {
|
|
378
|
-
status: 'mcp_running',
|
|
379
|
-
label: `MCP: ${server ? server + '/' : ''}${toolName}...`,
|
|
380
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
381
|
-
}
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
case 'waiting_for_task': {
|
|
385
|
-
const desc = String(data['taskDescription'] ?? 'task');
|
|
386
|
-
timelineEmitter.emit(sessionName, 'agent.status', {
|
|
387
|
-
status: 'waiting',
|
|
388
|
-
label: `Waiting: ${desc}`,
|
|
389
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// Result events — end-of-turn summary with total cost
|
|
396
|
-
if (raw['type'] === 'result') {
|
|
397
|
-
const costUsd = raw['total_cost_usd'];
|
|
398
|
-
if (typeof costUsd === 'number' && costUsd > 0) {
|
|
399
|
-
timelineEmitter.emit(sessionName, 'usage.update', { costUsd }, { source: 'daemon', confidence: 'high' });
|
|
400
|
-
}
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
// System events — compact_boundary etc. (no message.content)
|
|
404
|
-
if (raw['type'] === 'system') {
|
|
405
|
-
const subtype = String(raw['subtype'] ?? '');
|
|
406
|
-
if (subtype === 'compact_boundary') {
|
|
407
|
-
timelineEmitter.emit(sessionName, 'agent.status', {
|
|
408
|
-
status: 'compacting',
|
|
409
|
-
label: 'Compacting conversation...',
|
|
410
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
411
|
-
}
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const msg = raw['message'];
|
|
415
|
-
if (!msg)
|
|
416
|
-
return;
|
|
417
|
-
const content = msg['content'];
|
|
418
|
-
if (raw['type'] === 'assistant') {
|
|
419
|
-
if (typeof content === 'string') {
|
|
420
|
-
emitAssistantStringContent(sessionName, content, stableId, ts);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
if (!Array.isArray(content))
|
|
424
|
-
return;
|
|
425
|
-
for (const block of content) {
|
|
426
|
-
if (block.type === 'text' && block.text) {
|
|
427
|
-
timelineEmitter.emit(sessionName, 'assistant.text', {
|
|
428
|
-
text: block.text,
|
|
429
|
-
streaming: false,
|
|
430
|
-
}, { source: 'daemon', confidence: 'high', ...(stableId ? { eventId: stableId('at') } : {}), ...(ts ? { ts } : {}) });
|
|
431
|
-
}
|
|
432
|
-
else if (block.type === 'thinking') {
|
|
433
|
-
timelineEmitter.emit(sessionName, 'assistant.thinking', {
|
|
434
|
-
text: block.thinking,
|
|
435
|
-
}, { source: 'daemon', confidence: 'high', ...(stableId ? { eventId: stableId('th') } : {}), ...(ts ? { ts } : {}) });
|
|
436
|
-
}
|
|
437
|
-
else if (block.type === 'tool_use' && block.name) {
|
|
438
|
-
emitClaudeToolCallBlock(sessionName, block, stableId, ts);
|
|
439
|
-
}
|
|
440
|
-
else if (block.type === 'tool_result') {
|
|
441
|
-
emitClaudeToolResultBlock(sessionName, block, stableId, ts);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Emit token usage + model for context bar (transient, no stable ID needed)
|
|
445
|
-
const usage = msg['usage'];
|
|
446
|
-
const model = msg['model'];
|
|
447
|
-
if (usage && typeof usage.input_tokens === 'number') {
|
|
448
|
-
const presetCtx = watchers.get(sessionName)?.ccSessionId ? getSessionContextWindow(watchers.get(sessionName).ccSessionId) : undefined;
|
|
449
|
-
timelineEmitter.emit(sessionName, 'usage.update', {
|
|
450
|
-
inputTokens: usage.input_tokens + (usage.cache_creation_input_tokens ?? 0),
|
|
451
|
-
cacheTokens: usage.cache_read_input_tokens ?? 0,
|
|
452
|
-
contextWindow: resolveContextWindow(presetCtx, model),
|
|
453
|
-
...(model ? { model } : {}),
|
|
454
|
-
}, { source: 'daemon', confidence: 'high' });
|
|
455
|
-
}
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (raw['type'] === 'user') {
|
|
459
|
-
if (typeof content === 'string') {
|
|
460
|
-
emitUserStringContent(sessionName, content, stableId, ts);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (!Array.isArray(content))
|
|
464
|
-
return;
|
|
465
|
-
for (const block of content) {
|
|
466
|
-
if (block.type === 'text' && block.text?.trim()) {
|
|
467
|
-
emitUserStringContent(sessionName, block.text, stableId, ts);
|
|
468
|
-
}
|
|
469
|
-
else if (block.type === 'tool_result') {
|
|
470
|
-
emitClaudeToolResultBlock(sessionName, block, stableId, ts);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
/** Extract a truncated summary of tool result content for display. */
|
|
476
|
-
function extractToolResultOutput(block) {
|
|
477
|
-
const raw = block.content;
|
|
478
|
-
if (!raw)
|
|
479
|
-
return undefined;
|
|
480
|
-
let text;
|
|
481
|
-
if (typeof raw === 'string') {
|
|
482
|
-
text = raw;
|
|
483
|
-
}
|
|
484
|
-
else if (Array.isArray(raw)) {
|
|
485
|
-
text = raw.map((b) => b.text ?? '').join('\n');
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
return undefined;
|
|
489
|
-
}
|
|
490
|
-
text = text.trim();
|
|
491
|
-
if (!text)
|
|
492
|
-
return undefined;
|
|
493
|
-
return text.length > 200 ? text.slice(0, 197) + '...' : text;
|
|
494
|
-
}
|
|
137
|
+
// ── JSONL parsing ─────────────────────────────────────────────────────────────
|
|
138
|
+
//
|
|
139
|
+
// The heavy parsing (JSON.parse + regex + block interpretation) lives in the
|
|
140
|
+
// pure `jsonl-parse-core` module so it can run either on the main thread or
|
|
141
|
+
// in the `jsonl-parse-worker` thread without diverging. The watcher itself
|
|
142
|
+
// only owns I/O and event dispatch.
|
|
143
|
+
//
|
|
144
|
+
// Strategy:
|
|
145
|
+
// - Default: drained batches are parsed synchronously on main using
|
|
146
|
+
// `mainParseCtx`. Code path identical to pre-worker behaviour.
|
|
147
|
+
// - Opt-in `IM4CODES_JSONL_WORKER=1`: drained batches ship to the worker
|
|
148
|
+
// pool; crash / timeout automatically falls back to the main-thread
|
|
149
|
+
// context (same pure parser). Intended for deployments that observe
|
|
150
|
+
// main-loop pressure from heavy Claude JSONL streams.
|
|
151
|
+
//
|
|
152
|
+
// `emitRecentHistory` (one-shot at session start) always runs on main using a
|
|
153
|
+
// throwaway context, because (a) it's infrequent and (b) we want to filter
|
|
154
|
+
// `usage.update` events and emit the snapshot at the end instead.
|
|
155
|
+
/** Main-thread fallback state (only used when worker disabled or failed). */
|
|
156
|
+
const mainParseCtx = createParseContext();
|
|
495
157
|
const watchers = new Map();
|
|
496
158
|
function watcherControl(sessionName) {
|
|
497
159
|
return {
|
|
@@ -630,58 +292,27 @@ export async function emitRecentHistory(sessionName, filePath) {
|
|
|
630
292
|
const content = msg?.['content'];
|
|
631
293
|
if (!(Array.isArray(content) || typeof content === 'string'))
|
|
632
294
|
continue;
|
|
633
|
-
allEntries.push({ lineBytePos,
|
|
295
|
+
allEntries.push({ lineBytePos, rawLine: line });
|
|
634
296
|
}
|
|
635
|
-
// Take last HISTORY_LINES entries
|
|
297
|
+
// Take last HISTORY_LINES entries and replay them through the shared parse
|
|
298
|
+
// core. A scratch ParseContext keeps history's tool-call correlation state
|
|
299
|
+
// isolated from the live watcher — no leakage into subsequent drains.
|
|
636
300
|
const recentEntries = allEntries.slice(-HISTORY_LINES);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
timelineEmitter.emit(sessionName, 'assistant.text', {
|
|
653
|
-
text: block.text, streaming: false,
|
|
654
|
-
}, { source: 'daemon', confidence: 'high', eventId: stableId('at'), ...(ts ? { ts } : {}) });
|
|
655
|
-
}
|
|
656
|
-
else if (block.type === 'thinking') {
|
|
657
|
-
timelineEmitter.emit(sessionName, 'assistant.thinking', {
|
|
658
|
-
text: block.thinking,
|
|
659
|
-
}, { source: 'daemon', confidence: 'high', eventId: stableId('th'), ...(ts ? { ts } : {}) });
|
|
660
|
-
}
|
|
661
|
-
else if (block.type === 'tool_use' && block.name) {
|
|
662
|
-
emitClaudeToolCallBlock(sessionName, block, stableId, ts);
|
|
663
|
-
}
|
|
664
|
-
else if (block.type === 'tool_result') {
|
|
665
|
-
emitClaudeToolResultBlock(sessionName, block, stableId, ts);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
else if (raw['type'] === 'user') {
|
|
670
|
-
const msg = raw['message'];
|
|
671
|
-
const content = msg?.['content'];
|
|
672
|
-
if (typeof content === 'string') {
|
|
673
|
-
emitUserStringContent(sessionName, content, stableId, ts);
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
for (const block of content) {
|
|
677
|
-
if (block.type === 'text' && block.text?.trim()) {
|
|
678
|
-
emitUserStringContent(sessionName, block.text, stableId, ts);
|
|
679
|
-
}
|
|
680
|
-
else if (block.type === 'tool_result') {
|
|
681
|
-
emitClaudeToolResultBlock(sessionName, block, stableId, ts);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
301
|
+
const historyCtx = createParseContext();
|
|
302
|
+
const presetContextWindow = watchers.get(sessionName)?.ccSessionId
|
|
303
|
+
? getSessionContextWindow(watchers.get(sessionName).ccSessionId)
|
|
304
|
+
: undefined;
|
|
305
|
+
const { emits } = parseLinesInCtx(historyCtx, {
|
|
306
|
+
sessionName,
|
|
307
|
+
items: recentEntries.map((e) => ({ line: e.rawLine, lineByteOffset: e.lineBytePos })),
|
|
308
|
+
...(presetContextWindow !== undefined ? { presetContextWindow } : {}),
|
|
309
|
+
});
|
|
310
|
+
// History handles the usage snapshot separately below — drop per-line
|
|
311
|
+
// usage events so we don't spam the context bar during replay.
|
|
312
|
+
for (const em of emits) {
|
|
313
|
+
if (em.type === 'usage.update')
|
|
314
|
+
continue;
|
|
315
|
+
timelineEmitter.emit(em.sessionName, em.type, em.payload, em.metadata);
|
|
685
316
|
}
|
|
686
317
|
// Emit the most recent usage snapshot so the context bar populates on load
|
|
687
318
|
if (lastUsagePayload) {
|
|
@@ -779,7 +410,10 @@ export function stopWatching(sessionName) {
|
|
|
779
410
|
unregisterWatcherControl(sessionName);
|
|
780
411
|
releaseFiles(sessionName);
|
|
781
412
|
releaseOwnership(sessionName);
|
|
782
|
-
|
|
413
|
+
// Drop per-session pending tool-call state in both the main-thread fallback
|
|
414
|
+
// context and the worker (best-effort; worker call is async and unawaited).
|
|
415
|
+
forgetSessionInCtx(mainParseCtx, sessionName);
|
|
416
|
+
void jsonlParsePool.forgetSession(sessionName);
|
|
783
417
|
}
|
|
784
418
|
/**
|
|
785
419
|
* Start watching a specific JSONL file (CC sub-sessions with known --session-id path).
|
|
@@ -1102,12 +736,38 @@ async function drainNewLines(sessionName, state) {
|
|
|
1102
736
|
// pendingPartialLine bytes were already read in a prior drain, so we subtract them.
|
|
1103
737
|
const prevPendingByteLen = Buffer.byteLength(fullChunk.slice(0, fullChunk.length - chunk.length), 'utf8');
|
|
1104
738
|
let lineByteOffset = chunkStartOffset - prevPendingByteLen;
|
|
739
|
+
// Batch complete lines and route to worker (if enabled) or parse on main.
|
|
740
|
+
const items = [];
|
|
1105
741
|
for (const line of lines) {
|
|
1106
742
|
if (state.stopped)
|
|
1107
743
|
break;
|
|
1108
|
-
|
|
744
|
+
items.push({ line, lineByteOffset });
|
|
1109
745
|
lineByteOffset += Buffer.byteLength(line, 'utf8') + 1; // +1 for \n
|
|
1110
746
|
}
|
|
747
|
+
if (items.length === 0)
|
|
748
|
+
return;
|
|
749
|
+
const presetContextWindow = state.ccSessionId
|
|
750
|
+
? getSessionContextWindow(state.ccSessionId)
|
|
751
|
+
: undefined;
|
|
752
|
+
const request = {
|
|
753
|
+
sessionName,
|
|
754
|
+
items,
|
|
755
|
+
...(state.ccSessionId ? { ccSessionId: state.ccSessionId } : {}),
|
|
756
|
+
...(presetContextWindow !== undefined ? { presetContextWindow } : {}),
|
|
757
|
+
};
|
|
758
|
+
let emits = null;
|
|
759
|
+
if (isJsonlWorkerEnabled() && jsonlParsePool.isAvailable()) {
|
|
760
|
+
const result = await jsonlParsePool.parseLines(request);
|
|
761
|
+
if (result)
|
|
762
|
+
emits = result.emits;
|
|
763
|
+
}
|
|
764
|
+
if (!emits) {
|
|
765
|
+
// Main-thread fallback: either worker disabled, unavailable, or returned null.
|
|
766
|
+
emits = parseLinesInCtx(mainParseCtx, request).emits;
|
|
767
|
+
}
|
|
768
|
+
for (const em of emits) {
|
|
769
|
+
timelineEmitter.emit(em.sessionName, em.type, em.payload, em.metadata);
|
|
770
|
+
}
|
|
1111
771
|
}
|
|
1112
772
|
catch (err) {
|
|
1113
773
|
if (!state.stopped) {
|