imprint-mcp 0.6.3 → 0.6.4
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 +8 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -3
- package/src/imprint/audit.ts +43 -22
- package/src/imprint/claude-cli-compile.ts +43 -7
- package/src/imprint/codex-cli-compile.ts +205 -129
- package/src/imprint/inject-listener.ts +31 -3
- package/src/imprint/llm.ts +3 -3
- package/src/imprint/react-flight-limits.ts +31 -0
- package/src/imprint/record.ts +657 -46
- package/src/imprint/redact.ts +240 -30
- package/src/imprint/response-body-intent.ts +73 -0
- package/src/imprint/response-body-lifecycle.ts +32 -0
- package/src/imprint/response-body-stream.ts +704 -0
- package/src/imprint/tracing.ts +60 -238
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { type ChildProcess, spawn, spawnSync } from 'node:child_process';
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { isAbsolute as pathIsAbsolute, join as pathJoin } from 'node:path';
|
|
13
|
-
import type
|
|
13
|
+
import { type Span, context as otelContext } from '@opentelemetry/api';
|
|
14
14
|
import type { AuthCliCompileMode } from './auth-compile-tools.ts';
|
|
15
15
|
import { type SharedModuleManifestEntry, resolvePlanSliceFromFile } from './build-plan.ts';
|
|
16
16
|
import type {
|
|
@@ -25,13 +25,12 @@ import { COMPILE_SENTINELS, compileDeadlineAfterVerification } from './mcp-compi
|
|
|
25
25
|
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
26
26
|
import {
|
|
27
27
|
endTraceSpan,
|
|
28
|
-
|
|
29
|
-
resolveTraceTokenCount,
|
|
28
|
+
recordLlmUsageSpan,
|
|
30
29
|
setSpanAttributes,
|
|
31
30
|
startTraceSpan,
|
|
31
|
+
traceInputOutputAttributes,
|
|
32
32
|
traceJsonInputOutputAttributes,
|
|
33
33
|
traceLlmIoEnabled,
|
|
34
|
-
traceLlmMessages,
|
|
35
34
|
traceToolIoEnabled,
|
|
36
35
|
traced,
|
|
37
36
|
} from './tracing.ts';
|
|
@@ -44,6 +43,80 @@ const CLI_PATH = pathJoin(REPO_ROOT, 'src', 'cli.ts');
|
|
|
44
43
|
const MCP_SERVER_NAME = 'imprint-compile';
|
|
45
44
|
const MAX_VERIFICATION_CYCLES = 5;
|
|
46
45
|
const MAX_MCP_TOOL_TIMEOUT_SEC = 1800;
|
|
46
|
+
const SENTINEL_USAGE_GRACE_MS = 15_000;
|
|
47
|
+
|
|
48
|
+
interface SentinelGraceController {
|
|
49
|
+
observeSentinel(): void;
|
|
50
|
+
observeTurnCompleted(): void;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface TurnActivityTracker {
|
|
55
|
+
isActive(): boolean;
|
|
56
|
+
started(): void;
|
|
57
|
+
completed(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Track Codex turn lifecycle without depending on optional tracing spans. */
|
|
61
|
+
export function createTurnActivityTracker(): TurnActivityTracker {
|
|
62
|
+
let active = false;
|
|
63
|
+
return {
|
|
64
|
+
isActive: () => active,
|
|
65
|
+
started: () => {
|
|
66
|
+
active = true;
|
|
67
|
+
},
|
|
68
|
+
completed: () => {
|
|
69
|
+
active = false;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Wait just long enough for the terminal Codex usage event after done/give_up.
|
|
76
|
+
* A completed turn terminates immediately; the timer is only a bounded fallback
|
|
77
|
+
* for CLI versions that never emit turn.completed.
|
|
78
|
+
*/
|
|
79
|
+
export function createSentinelGraceController(opts: {
|
|
80
|
+
hasActiveTurn: () => boolean;
|
|
81
|
+
terminate: () => void;
|
|
82
|
+
fallbackMs?: number;
|
|
83
|
+
schedule?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
84
|
+
cancel?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
85
|
+
}): SentinelGraceController {
|
|
86
|
+
const schedule = opts.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
87
|
+
const cancel = opts.cancel ?? clearTimeout;
|
|
88
|
+
let sentinelObserved = false;
|
|
89
|
+
let terminated = false;
|
|
90
|
+
let fallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
91
|
+
|
|
92
|
+
const terminateOnce = (): void => {
|
|
93
|
+
if (terminated) return;
|
|
94
|
+
terminated = true;
|
|
95
|
+
if (fallbackTimer) cancel(fallbackTimer);
|
|
96
|
+
fallbackTimer = undefined;
|
|
97
|
+
opts.terminate();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
observeSentinel(): void {
|
|
102
|
+
sentinelObserved = true;
|
|
103
|
+
if (!opts.hasActiveTurn()) {
|
|
104
|
+
terminateOnce();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (fallbackTimer || terminated) return;
|
|
108
|
+
fallbackTimer = schedule(terminateOnce, opts.fallbackMs ?? SENTINEL_USAGE_GRACE_MS);
|
|
109
|
+
fallbackTimer.unref?.();
|
|
110
|
+
},
|
|
111
|
+
observeTurnCompleted(): void {
|
|
112
|
+
if (sentinelObserved) terminateOnce();
|
|
113
|
+
},
|
|
114
|
+
dispose(): void {
|
|
115
|
+
if (fallbackTimer) cancel(fallbackTimer);
|
|
116
|
+
fallbackTimer = undefined;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
47
120
|
|
|
48
121
|
function formatRevisionMode(enabled: boolean | undefined): string {
|
|
49
122
|
return enabled
|
|
@@ -127,6 +200,7 @@ export async function compileViaCodexCli(
|
|
|
127
200
|
},
|
|
128
201
|
async (span) => {
|
|
129
202
|
const result = await compileViaCodexCliImpl(opts, span);
|
|
203
|
+
const model = opts.model ?? preferredAgentModel('codex-cli');
|
|
130
204
|
setSpanAttributes(span, {
|
|
131
205
|
'imprint.compile.outcome': result.outcome,
|
|
132
206
|
'imprint.compile.success': result.success,
|
|
@@ -136,6 +210,20 @@ export async function compileViaCodexCli(
|
|
|
136
210
|
'imprint.compile.output_tokens': result.outputTokens,
|
|
137
211
|
'imprint.compile.conversation_log': result.conversationLogPath,
|
|
138
212
|
});
|
|
213
|
+
// Codex reports input_tokens as the total prompt; cached_input_tokens is
|
|
214
|
+
// a subset, not an additional token bucket.
|
|
215
|
+
recordLlmUsageSpan(
|
|
216
|
+
'compile.codex_cli_usage',
|
|
217
|
+
{
|
|
218
|
+
provider: 'codex-cli',
|
|
219
|
+
model,
|
|
220
|
+
inputTokens: result.inputTokens,
|
|
221
|
+
outputTokens: result.outputTokens,
|
|
222
|
+
cacheReadTokens: result.cacheReadInputTokens,
|
|
223
|
+
cacheWriteTokens: result.cacheCreationInputTokens,
|
|
224
|
+
},
|
|
225
|
+
{ 'imprint.compile.turns': result.turns },
|
|
226
|
+
);
|
|
139
227
|
return result;
|
|
140
228
|
},
|
|
141
229
|
);
|
|
@@ -237,30 +325,16 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
237
325
|
}
|
|
238
326
|
|
|
239
327
|
const model = opts.model ?? preferredAgentModel('codex-cli');
|
|
240
|
-
const initialTokenCount = resolveTraceTokenCount(null, initialPrompt);
|
|
241
328
|
const captureLlmIo = traceLlmIoEnabled();
|
|
242
329
|
// done() owns a separately bounded live-verification phase, so its transport
|
|
243
330
|
// timeout cannot be derived from the compiler's remaining reasoning budget.
|
|
244
331
|
const mcpToolTimeoutSec = MAX_MCP_TOOL_TIMEOUT_SEC;
|
|
245
332
|
setSpanAttributes(traceSpan, {
|
|
246
|
-
...
|
|
247
|
-
provider: 'codex-cli',
|
|
248
|
-
model,
|
|
249
|
-
inputTokens: initialTokenCount.tokens,
|
|
250
|
-
tokenCountsEstimated: true,
|
|
251
|
-
inputTokenSource: initialTokenCount.source,
|
|
252
|
-
inputMessages: captureLlmIo
|
|
253
|
-
? traceLlmMessages([{ role: 'user', content: initialPrompt }])
|
|
254
|
-
: undefined,
|
|
255
|
-
inputValue: captureLlmIo ? initialPrompt : undefined,
|
|
256
|
-
invocationParameters: {
|
|
257
|
-
command: 'codex exec',
|
|
258
|
-
json: true,
|
|
259
|
-
sandbox: 'workspace-write',
|
|
260
|
-
tool_timeout_sec: mcpToolTimeoutSec,
|
|
261
|
-
},
|
|
262
|
-
}),
|
|
333
|
+
...(captureLlmIo ? traceInputOutputAttributes('input', initialPrompt) : {}),
|
|
263
334
|
'imprint.compile.initial_prompt_chars': initialPrompt.length,
|
|
335
|
+
'imprint.compile.command': 'codex exec',
|
|
336
|
+
'imprint.compile.sandbox': 'workspace-write',
|
|
337
|
+
'imprint.compile.tool_timeout_sec': mcpToolTimeoutSec,
|
|
264
338
|
});
|
|
265
339
|
|
|
266
340
|
const execArgs = opts.resume
|
|
@@ -352,27 +426,9 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
352
426
|
} finally {
|
|
353
427
|
process.removeListener('exit', terminateOnParentExit);
|
|
354
428
|
}
|
|
355
|
-
const hasActualUsage = result.inputTokens > 0 || result.outputTokens > 0;
|
|
356
|
-
const inputTokenCount = resolveTraceTokenCount(
|
|
357
|
-
hasActualUsage ? result.inputTokens : null,
|
|
358
|
-
initialPrompt,
|
|
359
|
-
);
|
|
360
|
-
const outputTokenCount = resolveTraceTokenCount(
|
|
361
|
-
hasActualUsage ? result.outputTokens : null,
|
|
362
|
-
result.message,
|
|
363
|
-
);
|
|
364
429
|
setSpanAttributes(traceSpan, {
|
|
365
|
-
...llmSpanAttributes({
|
|
366
|
-
provider: 'codex-cli',
|
|
367
|
-
model,
|
|
368
|
-
inputTokens: inputTokenCount.tokens,
|
|
369
|
-
outputTokens: outputTokenCount.tokens,
|
|
370
|
-
tokenCountsEstimated:
|
|
371
|
-
inputTokenCount.source === 'estimated' || outputTokenCount.source === 'estimated',
|
|
372
|
-
inputTokenSource: inputTokenCount.source,
|
|
373
|
-
outputTokenSource: outputTokenCount.source,
|
|
374
|
-
}),
|
|
375
430
|
'imprint.compile.message': result.message,
|
|
431
|
+
...(captureLlmIo ? traceInputOutputAttributes('output', result.message) : {}),
|
|
376
432
|
});
|
|
377
433
|
return result;
|
|
378
434
|
}
|
|
@@ -392,6 +448,10 @@ async function driveJsonl(
|
|
|
392
448
|
opts: CompileViaCodexCliOptions,
|
|
393
449
|
traceSpan?: Span,
|
|
394
450
|
): Promise<CompileAgentResult> {
|
|
451
|
+
// Bun's child-process event emitters do not preserve AsyncLocalStorage.
|
|
452
|
+
// Restore the compile span context inside stdout callbacks so turn/tool
|
|
453
|
+
// spans remain children of compile.codex_cli_agent in Phoenix.
|
|
454
|
+
const parentCtx = otelContext.active();
|
|
395
455
|
const conversationLog: unknown[] = [];
|
|
396
456
|
const conversationLogPath = pathJoin(opts.absoluteToolDir, '.compile-log.json');
|
|
397
457
|
const rawStdoutPath = pathJoin(opts.absoluteToolDir, '.codex-stdout.jsonl');
|
|
@@ -414,6 +474,7 @@ async function driveJsonl(
|
|
|
414
474
|
const rawStderrChunks: string[] = [];
|
|
415
475
|
let inputTokens = 0;
|
|
416
476
|
let outputTokens = 0;
|
|
477
|
+
let cacheReadInputTokens = 0;
|
|
417
478
|
let turn = 0;
|
|
418
479
|
let lastErrorMessage = '';
|
|
419
480
|
let stderrBuf = '';
|
|
@@ -421,6 +482,8 @@ async function driveJsonl(
|
|
|
421
482
|
let capturedSessionId: string | undefined;
|
|
422
483
|
const toolSpans = new Map<string, Span>();
|
|
423
484
|
let currentTurnSpan: Span | null = null;
|
|
485
|
+
const turnActivity = createTurnActivityTracker();
|
|
486
|
+
let onTurnCompletedAfterSentinel: (() => void) | undefined;
|
|
424
487
|
|
|
425
488
|
const budgetMs = Math.max(0, opts.deadlineMs - Date.now());
|
|
426
489
|
const fireProgress = (phase: 'thinking' | 'tool', toolName?: string): void => {
|
|
@@ -481,103 +544,106 @@ async function driveJsonl(
|
|
|
481
544
|
|
|
482
545
|
let stdoutBuf = '';
|
|
483
546
|
child.stdout?.on('data', (chunk: Buffer) => {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
547
|
+
otelContext.with(parentCtx, () => {
|
|
548
|
+
const chunkText = chunk.toString('utf8');
|
|
549
|
+
rawStdoutChunks.push(chunkText);
|
|
550
|
+
stdoutBuf += chunkText;
|
|
551
|
+
while (true) {
|
|
552
|
+
const nl = stdoutBuf.indexOf('\n');
|
|
553
|
+
if (nl < 0) break;
|
|
554
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
555
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
556
|
+
if (!line) continue;
|
|
557
|
+
|
|
558
|
+
let evt: CodexJsonEvent;
|
|
559
|
+
try {
|
|
560
|
+
evt = JSON.parse(line) as CodexJsonEvent;
|
|
561
|
+
} catch (err) {
|
|
562
|
+
log(`unparseable jsonl line: ${errMsg(err)}`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
501
565
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
566
|
+
conversationLog.push(evt);
|
|
567
|
+
// Keep the human-readable compile log current during a long agent turn.
|
|
568
|
+
// Rewriting is throttled so streamed bursts do not cause one full-file
|
|
569
|
+
// write per JSONL event, while a stalled/crashed turn still leaves the
|
|
570
|
+
// latest tool activity visible on disk.
|
|
571
|
+
scheduleLogFlush();
|
|
572
|
+
|
|
573
|
+
if (evt.type === 'thread.started') {
|
|
574
|
+
log(`thread_id=${evt.thread_id ?? '(none)'}`);
|
|
575
|
+
setSpanAttributes(traceSpan, { 'codex.thread_id': evt.thread_id });
|
|
576
|
+
if (evt.thread_id) capturedSessionId = evt.thread_id;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
515
579
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
580
|
+
if (evt.type === 'turn.started') {
|
|
581
|
+
turnActivity.started();
|
|
582
|
+
if (currentTurnSpan) endTraceSpan(currentTurnSpan);
|
|
583
|
+
flushLog();
|
|
584
|
+
turn++;
|
|
585
|
+
currentTurnSpan = startTraceSpan(`agent.turn.${turn}`, 'CHAIN', {
|
|
586
|
+
'imprint.agent.turn': turn,
|
|
587
|
+
'imprint.agent.cumulative_input_tokens': inputTokens,
|
|
588
|
+
'imprint.agent.cumulative_output_tokens': outputTokens,
|
|
589
|
+
});
|
|
590
|
+
fireProgress('thinking');
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
528
593
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
594
|
+
const normalizedToolEvt = normalizeCodexToolEvent(evt);
|
|
595
|
+
if (normalizedToolEvt) {
|
|
596
|
+
const { eventType, item } = normalizedToolEvt;
|
|
597
|
+
const toolName = codexToolName(item);
|
|
598
|
+
if (toolName) {
|
|
599
|
+
traceCodexToolEvent(toolSpans, eventType, item, toolName);
|
|
600
|
+
fireProgress(eventType === 'item.started' ? 'tool' : 'thinking', toolName);
|
|
601
|
+
}
|
|
602
|
+
continue;
|
|
536
603
|
}
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
604
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
})
|
|
554
|
-
: {}),
|
|
555
|
-
});
|
|
605
|
+
if ((evt.type === 'item.started' || evt.type === 'item.completed') && evt.item) {
|
|
606
|
+
const agentMessage = codexAgentMessageText(evt.item);
|
|
607
|
+
if (agentMessage && evt.type === 'item.completed') {
|
|
608
|
+
agentMessageCount++;
|
|
609
|
+
setSpanAttributes(traceSpan, {
|
|
610
|
+
'imprint.codex.agent_messages': agentMessageCount,
|
|
611
|
+
'imprint.codex.last_agent_message_chars': agentMessage.length,
|
|
612
|
+
...(traceLlmIoEnabled()
|
|
613
|
+
? traceInputOutputAttributes('output', agentMessage, undefined, 'agent_message')
|
|
614
|
+
: {}),
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
556
618
|
continue;
|
|
557
619
|
}
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
620
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
621
|
+
if (evt.type === 'turn.completed') {
|
|
622
|
+
const turnInput = evt.usage?.input_tokens ?? 0;
|
|
623
|
+
const turnOutput = evt.usage?.output_tokens ?? 0;
|
|
624
|
+
const turnCacheRead = evt.usage?.cached_input_tokens ?? 0;
|
|
625
|
+
inputTokens += turnInput;
|
|
626
|
+
outputTokens += turnOutput;
|
|
627
|
+
cacheReadInputTokens += turnCacheRead;
|
|
628
|
+
if (currentTurnSpan) {
|
|
629
|
+
setSpanAttributes(currentTurnSpan, {
|
|
630
|
+
'imprint.agent.turn_input_tokens': turnInput,
|
|
631
|
+
'imprint.agent.turn_output_tokens': turnOutput,
|
|
632
|
+
'imprint.agent.turn_cache_read_input_tokens': turnCacheRead,
|
|
633
|
+
});
|
|
634
|
+
endTraceSpan(currentTurnSpan);
|
|
635
|
+
currentTurnSpan = null;
|
|
636
|
+
}
|
|
637
|
+
turnActivity.completed();
|
|
638
|
+
onTurnCompletedAfterSentinel?.();
|
|
639
|
+
continue;
|
|
573
640
|
}
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
641
|
|
|
577
|
-
|
|
578
|
-
|
|
642
|
+
if (evt.type === 'error' || evt.type === 'turn.failed') {
|
|
643
|
+
lastErrorMessage = evt.message ?? evt.error?.message ?? JSON.stringify(evt);
|
|
644
|
+
}
|
|
579
645
|
}
|
|
580
|
-
}
|
|
646
|
+
});
|
|
581
647
|
});
|
|
582
648
|
|
|
583
649
|
child.stderr?.on('data', (chunk: Buffer) => {
|
|
@@ -593,6 +659,8 @@ async function driveJsonl(
|
|
|
593
659
|
const finish = (code: number): void => {
|
|
594
660
|
if (resolved) return;
|
|
595
661
|
resolved = true;
|
|
662
|
+
sentinelGrace.dispose();
|
|
663
|
+
onTurnCompletedAfterSentinel = undefined;
|
|
596
664
|
if (forcedKillTimer) clearTimeout(forcedKillTimer);
|
|
597
665
|
clearInterval(sentinelTimer);
|
|
598
666
|
resolve(code);
|
|
@@ -616,10 +684,17 @@ async function driveJsonl(
|
|
|
616
684
|
}, 2000);
|
|
617
685
|
forcedKillTimer.unref?.();
|
|
618
686
|
};
|
|
687
|
+
const sentinelGrace = createSentinelGraceController({
|
|
688
|
+
// Turn lifecycle is independent of tracing. startTraceSpan() returns null
|
|
689
|
+
// when tracing is disabled, but terminal usage still must be captured.
|
|
690
|
+
hasActiveTurn: turnActivity.isActive,
|
|
691
|
+
terminate: terminateForSentinel,
|
|
692
|
+
});
|
|
693
|
+
onTurnCompletedAfterSentinel = sentinelGrace.observeTurnCompleted;
|
|
619
694
|
const sentinelTimer = setInterval(() => {
|
|
620
695
|
const checkpointReached = opts.authMode && existsSync(checkpointSentinel);
|
|
621
696
|
if (!existsSync(doneSentinel) && !existsSync(giveUpSentinel) && !checkpointReached) return;
|
|
622
|
-
|
|
697
|
+
sentinelGrace.observeSentinel();
|
|
623
698
|
}, 500);
|
|
624
699
|
child.once('close', (code) => {
|
|
625
700
|
finish(code ?? -1);
|
|
@@ -629,6 +704,7 @@ async function driveJsonl(
|
|
|
629
704
|
});
|
|
630
705
|
});
|
|
631
706
|
childExited = true;
|
|
707
|
+
turnActivity.completed();
|
|
632
708
|
clearTimeout(deadlineTimer);
|
|
633
709
|
if (currentTurnSpan) endTraceSpan(currentTurnSpan);
|
|
634
710
|
for (const span of toolSpans.values()) endTraceSpan(span);
|
|
@@ -687,7 +763,7 @@ async function driveJsonl(
|
|
|
687
763
|
durationMs: Date.now() - opts.startTime,
|
|
688
764
|
inputTokens,
|
|
689
765
|
outputTokens,
|
|
690
|
-
cacheReadInputTokens
|
|
766
|
+
cacheReadInputTokens,
|
|
691
767
|
cacheCreationInputTokens: 0,
|
|
692
768
|
sessionId: capturedSessionId,
|
|
693
769
|
};
|
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
* `[IMPRINT]` is exact-match — keep stable.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export function createInjectedListenerSource(eventToken: string): string {
|
|
9
|
+
return `
|
|
9
10
|
(function imprintInjector() {
|
|
10
11
|
if (window.__imprint_injected__) return;
|
|
11
12
|
window.__imprint_injected__ = true;
|
|
12
13
|
|
|
13
14
|
const SENTINEL = '[IMPRINT]';
|
|
15
|
+
const EVENT_TOKEN = ${JSON.stringify(eventToken)};
|
|
16
|
+
const emitLog = console.log.bind(console);
|
|
17
|
+
const stringify = JSON.stringify.bind(JSON);
|
|
14
18
|
const MAX_VAL = 200;
|
|
19
|
+
let lastIntentTarget = null;
|
|
20
|
+
let lastIntentAt = 0;
|
|
21
|
+
let lastIntentKind = null;
|
|
15
22
|
|
|
16
23
|
function safeStr(v) {
|
|
17
24
|
try {
|
|
@@ -53,6 +60,7 @@ export const INJECTED_LISTENER_SOURCE = `
|
|
|
53
60
|
text: safeStr((el.textContent || '').trim()),
|
|
54
61
|
ariaLabel: el.getAttribute && el.getAttribute('aria-label') || null,
|
|
55
62
|
href: el.tagName === 'A' ? el.getAttribute('href') : null,
|
|
63
|
+
absoluteHref: el.tagName === 'A' ? el.href : null,
|
|
56
64
|
selector: selectorFor(el),
|
|
57
65
|
};
|
|
58
66
|
} catch (e) { return {}; }
|
|
@@ -60,17 +68,33 @@ export const INJECTED_LISTENER_SOURCE = `
|
|
|
60
68
|
|
|
61
69
|
function emit(type, payload) {
|
|
62
70
|
try {
|
|
63
|
-
|
|
71
|
+
emitLog(SENTINEL, EVENT_TOKEN, type, stringify(payload));
|
|
64
72
|
} catch (e) { /* ignore */ }
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
function onClick(ev) {
|
|
68
76
|
try {
|
|
69
|
-
|
|
77
|
+
onIntent(ev, 'click');
|
|
78
|
+
const tgt = ev.target && ev.target.closest ? ev.target.closest('a,button,input,form') || ev.target : ev.target;
|
|
70
79
|
emit('click', describe(tgt));
|
|
71
80
|
} catch (e) { /* ignore */ }
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
function onIntent(ev, intentKind) {
|
|
84
|
+
try {
|
|
85
|
+
if (!ev.isTrusted) return;
|
|
86
|
+
const tgt = ev.target && ev.target.closest ? ev.target.closest('a,button,input,form') : ev.target;
|
|
87
|
+
if (!tgt) return;
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const activation = intentKind === 'pointerdown' || intentKind === 'click';
|
|
90
|
+
if (!activation && tgt === lastIntentTarget && intentKind === lastIntentKind && now - lastIntentAt < 250) return;
|
|
91
|
+
lastIntentTarget = tgt;
|
|
92
|
+
lastIntentAt = now;
|
|
93
|
+
lastIntentKind = intentKind;
|
|
94
|
+
emit('intent', { ...describe(tgt), intentAt: now, intentKind: intentKind });
|
|
95
|
+
} catch (e) { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
function onInput(ev) {
|
|
75
99
|
try {
|
|
76
100
|
const tgt = ev.target;
|
|
@@ -128,10 +152,14 @@ export const INJECTED_LISTENER_SOURCE = `
|
|
|
128
152
|
// Capture phase = true so we see the event before the site has a chance to
|
|
129
153
|
// stopPropagation it.
|
|
130
154
|
document.addEventListener('click', onClick, true);
|
|
155
|
+
document.addEventListener('pointerover', (ev) => onIntent(ev, 'pointerover'), true);
|
|
156
|
+
document.addEventListener('pointerdown', (ev) => onIntent(ev, 'pointerdown'), true);
|
|
157
|
+
document.addEventListener('focusin', (ev) => onIntent(ev, 'focusin'), true);
|
|
131
158
|
document.addEventListener('input', onInput, true);
|
|
132
159
|
document.addEventListener('change', onChange, true);
|
|
133
160
|
document.addEventListener('submit', onSubmit, true);
|
|
134
161
|
})();
|
|
135
162
|
`;
|
|
163
|
+
}
|
|
136
164
|
|
|
137
165
|
export const IMPRINT_SENTINEL = '[IMPRINT]';
|
package/src/imprint/llm.ts
CHANGED
|
@@ -627,9 +627,9 @@ async function traceAnalyze(
|
|
|
627
627
|
throw err;
|
|
628
628
|
}
|
|
629
629
|
// Providers report `inputTokens` as the *uncached* input only; the cached
|
|
630
|
-
// portion lives in the cache fields.
|
|
631
|
-
//
|
|
632
|
-
//
|
|
630
|
+
// portion lives in the cache fields. Phoenix expects the TOTAL prompt plus
|
|
631
|
+
// the cache breakdown so it can calculate cost server-side, so sum them
|
|
632
|
+
// here. A real total is also large enough to clear the
|
|
633
633
|
// resolveTraceTokenCount sanity check, so cache-hit calls stop falling back
|
|
634
634
|
// to the chars/4 estimate.
|
|
635
635
|
const cacheReadTokens = result.cacheReadInputTokens ?? undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bound per-row metadata for untrusted React Flight bodies below the byte cap. */
|
|
2
|
+
export const MAX_REACT_FLIGHT_ROWS = 16_384;
|
|
3
|
+
|
|
4
|
+
/** Bound aggregate parsed JSON width across an untrusted React Flight body. */
|
|
5
|
+
export const MAX_REACT_FLIGHT_JSON_NODES = 32_768;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Estimate JSON node count before JSON.parse allocates the object graph.
|
|
9
|
+
* For valid JSON, one root plus each container opener and comma is a
|
|
10
|
+
* conservative upper approximation of containers and values. Delimiters in
|
|
11
|
+
* strings are ignored, including escaped quotes.
|
|
12
|
+
*/
|
|
13
|
+
export function boundedJsonNodeCount(json: string, limit: number): number | null {
|
|
14
|
+
if (limit < 1) return null;
|
|
15
|
+
let count = 1;
|
|
16
|
+
let inString = false;
|
|
17
|
+
for (let index = 0; index < json.length; index++) {
|
|
18
|
+
const char = json.charCodeAt(index);
|
|
19
|
+
if (inString) {
|
|
20
|
+
if (char === 0x5c) index++;
|
|
21
|
+
else if (char === 0x22) inString = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (char === 0x22) {
|
|
25
|
+
inString = true;
|
|
26
|
+
} else if (char === 0x7b || char === 0x5b || char === 0x2c) {
|
|
27
|
+
if (++count > limit) return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return count;
|
|
31
|
+
}
|