@agentdeck/bridge 1.0.18 → 1.0.20
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/adapters/claude-code.d.ts +4 -2
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +9 -10
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex-cli.d.ts +3 -4
- package/dist/adapters/codex-cli.d.ts.map +1 -1
- package/dist/adapters/codex-cli.js +8 -12
- package/dist/adapters/codex-cli.js.map +1 -1
- package/dist/adapters/pty-adapter.d.ts +2 -2
- package/dist/adapters/pty-adapter.js +2 -2
- package/dist/apme/adapters/codex-turn-manager.d.ts +13 -77
- package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -1
- package/dist/apme/adapters/codex-turn-manager.js +102 -403
- package/dist/apme/adapters/codex-turn-manager.js.map +1 -1
- package/dist/apme/claude-transcript-reader.d.ts +22 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
- package/dist/apme/claude-transcript-reader.js +75 -1
- package/dist/apme/claude-transcript-reader.js.map +1 -1
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +6 -1
- package/dist/bridge-core.js.map +1 -1
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +3 -1
- package/dist/check-deps.js.map +1 -1
- package/dist/claude-turn-watchdog.d.ts +70 -0
- package/dist/claude-turn-watchdog.d.ts.map +1 -0
- package/dist/claude-turn-watchdog.js +147 -0
- package/dist/claude-turn-watchdog.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -3
- package/dist/cli.js.map +1 -1
- package/dist/codex-hook-silence.d.ts +52 -0
- package/dist/codex-hook-silence.d.ts.map +1 -0
- package/dist/codex-hook-silence.js +80 -0
- package/dist/codex-hook-silence.js.map +1 -0
- package/dist/codex-rate-limits.d.ts +1 -1
- package/dist/codex-rate-limits.d.ts.map +1 -1
- package/dist/codex-rate-limits.js +27 -17
- package/dist/codex-rate-limits.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -192
- package/dist/index.js.map +1 -1
- package/dist/state-machine.d.ts +43 -1
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +146 -12
- package/dist/state-machine.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version-check.d.ts +8 -0
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +27 -0
- package/dist/version-check.js.map +1 -1
- package/package.json +4 -3
- package/dist/apme/adapters/claude-pty.d.ts +0 -28
- package/dist/apme/adapters/claude-pty.d.ts.map +0 -1
- package/dist/apme/adapters/claude-pty.js +0 -88
- package/dist/apme/adapters/claude-pty.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,6 @@ import { readLastTurn as readClaudeTranscriptLastTurn } from './apme/claude-tran
|
|
|
14
14
|
import { claudeHookToSpans } from './apme/adapters/claude-hook.js';
|
|
15
15
|
import { codexHookToSpans } from './apme/adapters/codex-hook.js';
|
|
16
16
|
import { CodexTurnManager } from './apme/adapters/codex-turn-manager.js';
|
|
17
|
-
import { claudePtyParserEventToSpans, claudePtyResponseToSpan } from './apme/adapters/claude-pty.js';
|
|
18
17
|
import { timelineEntryToSpans } from './apme/adapters/timeline.js';
|
|
19
18
|
import { classifyAndEnqueueTurn } from './apme/classify-turn.js';
|
|
20
19
|
import { VoiceManager } from './voice.js';
|
|
@@ -23,6 +22,8 @@ import { enableDebugLog, log, debug, setPtyMode } from './logger.js';
|
|
|
23
22
|
import { EventJournal } from './event-journal.js';
|
|
24
23
|
import { PtyRingBuffer } from './pty-ringbuffer.js';
|
|
25
24
|
import { isSubagentOnlyHook, SubagentTimelineTracker } from './subagent-timeline.js';
|
|
25
|
+
import { ClaudeTurnWatchdog } from './claude-turn-watchdog.js';
|
|
26
|
+
import { CodexHookSilenceWarning } from './codex-hook-silence.js';
|
|
26
27
|
import { createDiagDump } from './diag-analyzer.js';
|
|
27
28
|
import { createAdapter, ClaudeCodeAdapter, CodexCliAdapter, OpenCodeAdapter } from './adapters/index.js';
|
|
28
29
|
import { MonitorAdapter } from './adapters/monitor.js';
|
|
@@ -106,6 +107,14 @@ export async function startSession(opts) {
|
|
|
106
107
|
process.exit(1);
|
|
107
108
|
for (const w of deps.warnings)
|
|
108
109
|
log(`WARNING: ${w}`);
|
|
110
|
+
if (agentType === 'codex-cli' && deps.agentVersion) {
|
|
111
|
+
const { satisfiesRange, getCompatibleCodexRange } = await import('./version-check.js');
|
|
112
|
+
const codexRange = getCompatibleCodexRange() ?? '>=0.141.0';
|
|
113
|
+
if (!satisfiesRange(deps.agentVersion, codexRange)) {
|
|
114
|
+
log(`WARNING: Codex ${deps.agentVersion} predates the lifecycle-hook baseline (${codexRange}). ` +
|
|
115
|
+
'The terminal can start, but AgentDeck lifecycle monitoring requires a newer Codex CLI.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
109
118
|
// Version compatibility check (Claude Code only — uses npm registry + compatibleClaudeCode range)
|
|
110
119
|
if (agentType === 'claude-code' && !opts.noUpdateCheck) {
|
|
111
120
|
const { checkVersionCompatibility } = await import('./version-check.js');
|
|
@@ -157,6 +166,15 @@ export async function startSession(opts) {
|
|
|
157
166
|
if (adapter instanceof ClaudeCodeAdapter) {
|
|
158
167
|
adapter.seedProjectName(projectName);
|
|
159
168
|
}
|
|
169
|
+
// Codex fires codex_session_start within seconds of the PTY spawning —
|
|
170
|
+
// before the main wiring below registers its listeners — so capture the
|
|
171
|
+
// fact early or the hook-silence warning would misread a healthy install
|
|
172
|
+
// as lifecycle-blind.
|
|
173
|
+
let codexHookSeenEarly = false;
|
|
174
|
+
adapter.on('event', (evt) => {
|
|
175
|
+
if (evt.source === 'hook' && evt.event.startsWith('codex_'))
|
|
176
|
+
codexHookSeenEarly = true;
|
|
177
|
+
});
|
|
160
178
|
// ===== Start adapter (creates HTTP server, spawns process) =====
|
|
161
179
|
try {
|
|
162
180
|
await adapter.start({ port, command: opts.command, gatewayUrl: opts.gatewayUrl });
|
|
@@ -443,20 +461,60 @@ export async function startSession(opts) {
|
|
|
443
461
|
});
|
|
444
462
|
// Claude Code hook events → timeline entries
|
|
445
463
|
if (agentType === 'claude-code') {
|
|
446
|
-
wireClaudeCodeTimeline(adapter, core
|
|
464
|
+
wireClaudeCodeTimeline(adapter, core);
|
|
465
|
+
// Missed-Stop recovery: when the hook channel goes quiet mid-turn, the
|
|
466
|
+
// transcript JSONL tail (the Stop branch's own authority) is probed and a
|
|
467
|
+
// proven-finished turn is closed by injecting a synthetic Stop through
|
|
468
|
+
// this same event pipe — state machine, timeline, and APME all close via
|
|
469
|
+
// their existing Stop paths, and a late real Stop dedups downstream.
|
|
470
|
+
const turnWatchdog = new ClaudeTurnWatchdog({
|
|
471
|
+
onMissedStop: ({ transcript_path }) => {
|
|
472
|
+
adapter.emit('event', {
|
|
473
|
+
source: 'hook',
|
|
474
|
+
event: 'Stop',
|
|
475
|
+
data: { transcript_path, synthetic_stop: true },
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
adapter.on('event', (evt) => {
|
|
480
|
+
if (evt.source === 'hook')
|
|
481
|
+
turnWatchdog.noteHookEvent(evt.event, evt.data ?? {});
|
|
482
|
+
});
|
|
483
|
+
core.onShutdown(() => turnWatchdog.stop());
|
|
484
|
+
}
|
|
485
|
+
// Codex lifecycle-channel dead-on-arrival warning: hooks and notify share
|
|
486
|
+
// the same curl/port mechanism, so when both are silent the session is
|
|
487
|
+
// lifecycle-blind with no signal at all. Warn once if the PTY is clearly in
|
|
488
|
+
// use but no codex_* event ever arrived. `--no-codex-hooks` opted out.
|
|
489
|
+
if (agentType === 'codex-cli' && opts.codexHooksExpected !== false) {
|
|
490
|
+
const hookSilence = new CodexHookSilenceWarning({
|
|
491
|
+
onSilent: () => {
|
|
492
|
+
log('WARNING: no Codex lifecycle event has reached this bridge — state/timeline/eval are blind for this session.');
|
|
493
|
+
log('Repair: rerun `agentdeck codex` (reinstalls hooks) or `agentdeck daemon install`, and check [hooks]/notify in ~/.codex/config.toml.');
|
|
494
|
+
core.bridgeTimeline.addEntry({
|
|
495
|
+
ts: Date.now(),
|
|
496
|
+
type: 'error',
|
|
497
|
+
raw: 'Codex lifecycle hooks silent — session is lifecycle-blind',
|
|
498
|
+
detail: 'The terminal is active but no codex_* hook or notify event arrived. Hooks and the notify fallback share one curl/port path in ~/.codex/config.toml, so a stale port or config-layer conflict kills both. Rerun `agentdeck codex` or `agentdeck daemon install` to repair.',
|
|
499
|
+
agentType: 'codex-cli',
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
if (codexHookSeenEarly)
|
|
504
|
+
hookSilence.noteHookEvent();
|
|
505
|
+
adapter.on('event', (evt) => {
|
|
506
|
+
if (evt.source === 'hook' && evt.event.startsWith('codex_'))
|
|
507
|
+
hookSilence.noteHookEvent();
|
|
508
|
+
else if (evt.source === 'activity')
|
|
509
|
+
hookSilence.noteActivity();
|
|
510
|
+
});
|
|
511
|
+
core.onShutdown(() => hookSilence.stop());
|
|
447
512
|
}
|
|
448
513
|
// APME wiring for non-Claude-Code agents (OpenCode, Codex, OpenClaw)
|
|
449
514
|
if (apme && agentType !== 'claude-code' && agentType !== 'monitor') {
|
|
450
|
-
wireAgentApme(adapter, agentType, apme, core
|
|
515
|
+
wireAgentApme(adapter, agentType, apme, core);
|
|
451
516
|
}
|
|
452
517
|
// ===== Wire adapter events → StateMachine + journal =====
|
|
453
|
-
// APME: PTY response fallback — captures response from terminal output when
|
|
454
|
-
// the Stop hook doesn't fire (unreliable in Claude Code v2.1+).
|
|
455
|
-
// Three capture paths handle the hook/PTY race condition:
|
|
456
|
-
// Path A: idle fires, turn exists → apply directly
|
|
457
|
-
// Path B: idle fires before hook (fast responses) → buffer, apply on hook arrival
|
|
458
|
-
// Path C: UserPromptSubmit closes prev turn → apply PTY text to closed turn
|
|
459
|
-
let pendingPtyResponse = null;
|
|
460
518
|
adapter.on('event', (evt) => {
|
|
461
519
|
switch (evt.source) {
|
|
462
520
|
case 'hook':
|
|
@@ -479,28 +537,6 @@ export async function startSession(opts) {
|
|
|
479
537
|
apme.collector.ingestSpan(core.sessionId, span);
|
|
480
538
|
}
|
|
481
539
|
}
|
|
482
|
-
if (evt.event === 'UserPromptSubmit' && apme) {
|
|
483
|
-
// Path B: apply buffered response to the NEW turn (idle fired before hook)
|
|
484
|
-
if (pendingPtyResponse) {
|
|
485
|
-
debug('APME', `hook:UPS Path B: applying pending (${pendingPtyResponse.length} chars)`);
|
|
486
|
-
const ctx = makeApmeAdapterCtx(apme, core.sessionId, agentType);
|
|
487
|
-
const span = claudePtyResponseToSpan(ctx, pendingPtyResponse);
|
|
488
|
-
if (span)
|
|
489
|
-
apme.collector.ingestSpan(core.sessionId, span);
|
|
490
|
-
pendingPtyResponse = null;
|
|
491
|
-
}
|
|
492
|
-
// Path C: prev turn was just closed by ingestHook — apply pending response
|
|
493
|
-
if (pendingPtyResponse) {
|
|
494
|
-
debug('APME', `hook:UPS Path C: applying pending to closed turn (${pendingPtyResponse.length} chars)`);
|
|
495
|
-
const ctx = makeApmeAdapterCtx(apme, core.sessionId, agentType);
|
|
496
|
-
const span = claudePtyResponseToSpan(ctx, pendingPtyResponse, { fallbackToLastClosed: true });
|
|
497
|
-
if (span)
|
|
498
|
-
apme.collector.ingestSpan(core.sessionId, span);
|
|
499
|
-
pendingPtyResponse = null;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (evt.event === 'Stop')
|
|
503
|
-
pendingPtyResponse = null; // Stop has cleaner response
|
|
504
540
|
// APME-off fallback: task rows from boundary hooks alone.
|
|
505
541
|
if (fallbackTasks)
|
|
506
542
|
fallbackTasks.ingestHook(core.sessionId, evt.event, evt.data ?? {});
|
|
@@ -511,67 +547,14 @@ export async function startSession(opts) {
|
|
|
511
547
|
core.stateMachine.handleHookEvent(evt.event, evt.data);
|
|
512
548
|
break;
|
|
513
549
|
case 'parser':
|
|
514
|
-
|
|
550
|
+
// Structured adapters (OpenCode SSE / OpenClaw Gateway) normalize
|
|
551
|
+
// their native event streams into the legacy state vocabulary.
|
|
552
|
+
journal.write('adapter_signal', agentType, { event: evt.event, ...evt.data });
|
|
515
553
|
core.stateMachine.handleParserEvent(evt.event, evt.data);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
for (const span of claudePtyParserEventToSpans(ctx, evt.event, evt.data ?? {})) {
|
|
521
|
-
apme.collector.ingestSpan(core.sessionId, span);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
// APME: Path A — spinner_stop fires when Claude finishes responding.
|
|
525
|
-
// Delay 500ms to let response render in PTY, then extract text after ⏺ marker.
|
|
526
|
-
if (apme && evt.event === 'spinner_stop') {
|
|
527
|
-
const sid = core.sessionId;
|
|
528
|
-
setTimeout(async () => {
|
|
529
|
-
if (!apme)
|
|
530
|
-
return;
|
|
531
|
-
const tail = ptyRingBuffer.getTail(5000);
|
|
532
|
-
// Claude's response starts with ⏺ — extract content after last ⏺ marker.
|
|
533
|
-
// Take only meaningful lines (stop at spinner chars ✢✻⏸, prompt ❯, or separator ──).
|
|
534
|
-
const marker = tail.lastIndexOf('⏺');
|
|
535
|
-
let response = '';
|
|
536
|
-
if (marker >= 0) {
|
|
537
|
-
const afterMarker = tail.slice(marker + 1).trim();
|
|
538
|
-
const lines = afterMarker.split('\n');
|
|
539
|
-
const clean = [];
|
|
540
|
-
for (const line of lines) {
|
|
541
|
-
const trimmed = line.trim();
|
|
542
|
-
if (!trimmed)
|
|
543
|
-
continue;
|
|
544
|
-
// Stop at UI artifacts: spinner chars (✢✳✶✻✽), plan mode (⏸), prompt (❯), separator (─)
|
|
545
|
-
if (/^[✢✳✶✻✽⏸⏵❯─>]/.test(trimmed))
|
|
546
|
-
break;
|
|
547
|
-
if (/planmode|plan\s*mode|shift\+tab|accept\s*edits/i.test(trimmed))
|
|
548
|
-
break;
|
|
549
|
-
// Status text: "Whirring…", "Finagling…", token count, shortcuts hint
|
|
550
|
-
if (/^\S+…(\s|$)/.test(trimmed) && /tokens|shortcuts|\d+[ms]\s/i.test(trimmed))
|
|
551
|
-
break;
|
|
552
|
-
if (/\?\s*for\s*shortcuts/.test(trimmed))
|
|
553
|
-
break;
|
|
554
|
-
clean.push(trimmed);
|
|
555
|
-
}
|
|
556
|
-
response = clean.join('\n').trim();
|
|
557
|
-
}
|
|
558
|
-
debug('APME', `spinner_stop+500ms: tailLen=${tail.length} marker=${marker} respLen=${response.length}`);
|
|
559
|
-
if (response.length > 2) {
|
|
560
|
-
const turnId = apme.collector.getActiveTurnId(sid);
|
|
561
|
-
if (turnId) {
|
|
562
|
-
const ctx = makeApmeAdapterCtx(apme, sid, agentType);
|
|
563
|
-
const span = claudePtyResponseToSpan(ctx, response);
|
|
564
|
-
if (span)
|
|
565
|
-
apme.collector.ingestSpan(sid, span);
|
|
566
|
-
pendingPtyResponse = null;
|
|
567
|
-
await classifyAndEnqueueTurn(apme, sid);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
pendingPtyResponse = response;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}, 500);
|
|
574
|
-
}
|
|
554
|
+
break;
|
|
555
|
+
case 'terminal_ui':
|
|
556
|
+
journal.write('terminal_ui', 'pty', { event: evt.event, ...evt.data });
|
|
557
|
+
core.stateMachine.handleTerminalUiEvent(evt.event, evt.data);
|
|
575
558
|
break;
|
|
576
559
|
case 'metadata':
|
|
577
560
|
switch (evt.event) {
|
|
@@ -587,11 +570,6 @@ export async function startSession(opts) {
|
|
|
587
570
|
const text = evt.data?.text;
|
|
588
571
|
if (text)
|
|
589
572
|
core.broadcast({ type: 'user_prompt', text });
|
|
590
|
-
// APME: user_prompt is the PTY-detected prompt — use it to open a turn
|
|
591
|
-
// and capture the prompt text (fallback for when hooks don't fire).
|
|
592
|
-
if (apme && text) {
|
|
593
|
-
apme.collector.ingestHook(core.sessionId, 'UserPromptSubmit', { message: { content: text } });
|
|
594
|
-
}
|
|
595
573
|
break;
|
|
596
574
|
}
|
|
597
575
|
case 'model_catalog': {
|
|
@@ -1266,14 +1244,12 @@ export async function startSession(opts) {
|
|
|
1266
1244
|
// ===== Non-Claude agent APME wiring =====
|
|
1267
1245
|
// For OpenCode, Codex, OpenClaw: intercepts adapter timeline events to create
|
|
1268
1246
|
// APME turns with prompts and responses. Claude Code uses hooks instead.
|
|
1269
|
-
function wireAgentApme(adapter, agentType, apme, core
|
|
1247
|
+
function wireAgentApme(adapter, agentType, apme, core) {
|
|
1270
1248
|
const sid = core.sessionId;
|
|
1271
|
-
// Codex turn boundaries
|
|
1272
|
-
// module
|
|
1273
|
-
// machine plus the hook-fresh shortcut so non-codex sessions allocate
|
|
1274
|
-
// nothing here.
|
|
1249
|
+
// Codex turn boundaries and rollout response capture live in their own
|
|
1250
|
+
// hook-owned module so non-Codex sessions allocate nothing here.
|
|
1275
1251
|
const codexManager = agentType === 'codex-cli'
|
|
1276
|
-
? new CodexTurnManager(core, apme,
|
|
1252
|
+
? new CodexTurnManager(core, apme, sid, agentType)
|
|
1277
1253
|
: null;
|
|
1278
1254
|
adapter.on('event', (evt) => {
|
|
1279
1255
|
if (evt.source === 'hook' && isSubagentOnlyHook(evt.event, evt.data ?? {})) {
|
|
@@ -1309,12 +1285,10 @@ function wireAgentApme(adapter, agentType, apme, core, ptyRingBuffer) {
|
|
|
1309
1285
|
void classifyAndEnqueueTurn(apme, sid);
|
|
1310
1286
|
}
|
|
1311
1287
|
}
|
|
1312
|
-
// ── Codex:
|
|
1288
|
+
// ── Codex: lifecycle hooks + notify turn-complete fallback ──
|
|
1313
1289
|
if (codexManager) {
|
|
1314
1290
|
if (evt.source === 'hook')
|
|
1315
1291
|
codexManager.onHookEvent(evt);
|
|
1316
|
-
else if (evt.source === 'parser')
|
|
1317
|
-
codexManager.onParserEvent(evt);
|
|
1318
1292
|
}
|
|
1319
1293
|
});
|
|
1320
1294
|
if (codexManager) {
|
|
@@ -1338,14 +1312,12 @@ function isClaudeTaskNotificationPrompt(text) {
|
|
|
1338
1312
|
return text.trim().toLowerCase().startsWith('<task-notification>');
|
|
1339
1313
|
}
|
|
1340
1314
|
// ===== Claude Code timeline wiring =====
|
|
1341
|
-
function wireClaudeCodeTimeline(adapter, core
|
|
1315
|
+
function wireClaudeCodeTimeline(adapter, core) {
|
|
1342
1316
|
let ccChatStart = null;
|
|
1343
1317
|
let ccPendingChatStart = false;
|
|
1344
1318
|
let ccPendingChatStartTimer = null;
|
|
1345
1319
|
let ccLastPromptText = null;
|
|
1346
|
-
let
|
|
1347
|
-
let ccPendingCompletion = false; // Change 2: track pending chat_end for PTY fallback
|
|
1348
|
-
let ccPendingCompletionTimer = null;
|
|
1320
|
+
let ccPendingCompletion = false;
|
|
1349
1321
|
const emitChatStart = (text) => {
|
|
1350
1322
|
if (ccPendingChatStartTimer) {
|
|
1351
1323
|
clearTimeout(ccPendingChatStartTimer);
|
|
@@ -1376,33 +1348,8 @@ function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
|
1376
1348
|
startedAt: ccChatStart ?? Date.now(),
|
|
1377
1349
|
});
|
|
1378
1350
|
};
|
|
1379
|
-
/** Extract response text from PTY ringbuffer after ⏺ marker (same pattern as APME) */
|
|
1380
|
-
const extractPtyResponse = () => {
|
|
1381
|
-
const tail = ptyRingBuffer.getTail(5000);
|
|
1382
|
-
const marker = tail.lastIndexOf('⏺');
|
|
1383
|
-
if (marker < 0)
|
|
1384
|
-
return '';
|
|
1385
|
-
const afterMarker = tail.slice(marker + 1).trim();
|
|
1386
|
-
const lines = afterMarker.split('\n');
|
|
1387
|
-
const clean = [];
|
|
1388
|
-
for (const line of lines) {
|
|
1389
|
-
const trimmed = line.trim();
|
|
1390
|
-
if (!trimmed)
|
|
1391
|
-
continue;
|
|
1392
|
-
if (/^[✢✳✶✻✽⏸⏵❯─>]/.test(trimmed))
|
|
1393
|
-
break;
|
|
1394
|
-
if (/planmode|plan\s*mode|shift\+tab|accept\s*edits/i.test(trimmed))
|
|
1395
|
-
break;
|
|
1396
|
-
if (/^\S+…(\s|$)/.test(trimmed) && /tokens|shortcuts|\d+[ms]\s/i.test(trimmed))
|
|
1397
|
-
break;
|
|
1398
|
-
if (/\?\s*for\s*shortcuts/.test(trimmed))
|
|
1399
|
-
break;
|
|
1400
|
-
clean.push(trimmed);
|
|
1401
|
-
}
|
|
1402
|
-
return clean.join('\n').trim();
|
|
1403
|
-
};
|
|
1404
1351
|
/**
|
|
1405
|
-
* Emit a single Claude turn-close row
|
|
1352
|
+
* Emit a single Claude turn-close row from the authoritative Stop hook:
|
|
1406
1353
|
* a `chat_response` when there is meaningful response text, otherwise a
|
|
1407
1354
|
* `chat_end`. The two are mutually exclusive so a turn never produces both —
|
|
1408
1355
|
* the paired dimmed `chat_end` used to fragment every turn into three rows on
|
|
@@ -1501,48 +1448,6 @@ function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
|
1501
1448
|
}
|
|
1502
1449
|
}
|
|
1503
1450
|
});
|
|
1504
|
-
// Change 1: PTY tool_action → tool_exec timeline entry (fills gap when hooks don't fire)
|
|
1505
|
-
adapter.on('event', (evt) => {
|
|
1506
|
-
if (evt.source !== 'parser' || evt.event !== 'tool_action')
|
|
1507
|
-
return;
|
|
1508
|
-
const now = Date.now();
|
|
1509
|
-
// Skip if hook already emitted tool_request within 2s (avoid duplicates)
|
|
1510
|
-
if (now - ccLastHookToolTs < 2000)
|
|
1511
|
-
return;
|
|
1512
|
-
const toolName = evt.data?.toolName || 'tool';
|
|
1513
|
-
const toolArgs = evt.data?.toolArgs || '';
|
|
1514
|
-
const raw = toolArgs ? `${toolName} ${toolArgs}` : toolName;
|
|
1515
|
-
core.bridgeTimeline.addEntry({
|
|
1516
|
-
ts: now, type: 'tool_exec',
|
|
1517
|
-
raw: raw.length > 500 ? raw.slice(0, 497) + '...' : raw,
|
|
1518
|
-
agentType: 'claude-code',
|
|
1519
|
-
});
|
|
1520
|
-
});
|
|
1521
|
-
// Change 2: PTY fallback chat_end from spinner_stop/idle (when Stop hook doesn't fire)
|
|
1522
|
-
adapter.on('event', (evt) => {
|
|
1523
|
-
if (evt.source !== 'parser')
|
|
1524
|
-
return;
|
|
1525
|
-
if ((evt.event === 'spinner_stop' || evt.event === 'idle') && ccPendingCompletion) {
|
|
1526
|
-
// Wait 1.5s — Stop hook may still arrive
|
|
1527
|
-
if (ccPendingCompletionTimer)
|
|
1528
|
-
clearTimeout(ccPendingCompletionTimer);
|
|
1529
|
-
ccPendingCompletionTimer = setTimeout(() => {
|
|
1530
|
-
ccPendingCompletionTimer = null;
|
|
1531
|
-
if (!ccPendingCompletion)
|
|
1532
|
-
return; // Stop hook already handled it
|
|
1533
|
-
ccPendingCompletion = false;
|
|
1534
|
-
if (ccPendingChatStart)
|
|
1535
|
-
emitChatStart('');
|
|
1536
|
-
const now = Date.now();
|
|
1537
|
-
const duration = ccChatStart ? Math.round((now - ccChatStart) / 1000) : null;
|
|
1538
|
-
const toolSummary = core.usageTracker.getToolSummary();
|
|
1539
|
-
// Extract response from PTY ringbuffer
|
|
1540
|
-
const responseText = extractPtyResponse();
|
|
1541
|
-
debug('timeline', `CC PTY fallback chat_end: respLen=${responseText.length} duration=${duration}`);
|
|
1542
|
-
emitCompletion(responseText, duration, toolSummary);
|
|
1543
|
-
}, 1500);
|
|
1544
|
-
}
|
|
1545
|
-
});
|
|
1546
1451
|
adapter.on('event', (evt) => {
|
|
1547
1452
|
if (evt.source !== 'hook')
|
|
1548
1453
|
return;
|
|
@@ -1554,7 +1459,7 @@ function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
|
1554
1459
|
ccChatStart = now;
|
|
1555
1460
|
core.usageTracker.resetToolCounts();
|
|
1556
1461
|
ccPendingChatStart = true;
|
|
1557
|
-
ccPendingCompletion = true;
|
|
1462
|
+
ccPendingCompletion = true;
|
|
1558
1463
|
// Claude Code v2.1+ sends { message: { content: "..." } }, not { prompt: "..." }
|
|
1559
1464
|
const msg = evt.data?.message;
|
|
1560
1465
|
const hookText = evt.data?.prompt
|
|
@@ -1569,7 +1474,6 @@ function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
|
1569
1474
|
break;
|
|
1570
1475
|
}
|
|
1571
1476
|
case 'PreToolUse': {
|
|
1572
|
-
ccLastHookToolTs = now; // Change 1: mark hook tool time for dedup
|
|
1573
1477
|
const toolName = evt.data?.tool_name || 'tool';
|
|
1574
1478
|
const formatted = formatToolInputForTimeline(toolName, evt.data?.tool_input);
|
|
1575
1479
|
core.bridgeTimeline.addEntry({
|
|
@@ -1586,18 +1490,10 @@ function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
|
1586
1490
|
break;
|
|
1587
1491
|
}
|
|
1588
1492
|
case 'Stop': {
|
|
1589
|
-
// Race guard: if ccPendingCompletion is already false, the PTY fallback
|
|
1590
|
-
// timer fired first (Stop hook arrived >1.5s after spinner_stop) and
|
|
1591
|
-
// already emitted chat_response/chat_end. Skipping here prevents the
|
|
1592
|
-
// duplicate-line bug visible on the dashboard timeline.
|
|
1593
1493
|
const wasPending = ccPendingCompletion;
|
|
1594
|
-
ccPendingCompletion = false;
|
|
1595
|
-
if (ccPendingCompletionTimer) {
|
|
1596
|
-
clearTimeout(ccPendingCompletionTimer);
|
|
1597
|
-
ccPendingCompletionTimer = null;
|
|
1598
|
-
}
|
|
1494
|
+
ccPendingCompletion = false;
|
|
1599
1495
|
if (!wasPending) {
|
|
1600
|
-
debug('timeline', '
|
|
1496
|
+
debug('timeline', 'duplicate Claude Stop hook ignored');
|
|
1601
1497
|
break;
|
|
1602
1498
|
}
|
|
1603
1499
|
if (ccPendingChatStart)
|
|
@@ -1612,8 +1508,6 @@ function wireClaudeCodeTimeline(adapter, core, journal, ptyRingBuffer) {
|
|
|
1612
1508
|
// payload points at it.
|
|
1613
1509
|
// 2. last_assistant_message — ~18% reliable field on the hook
|
|
1614
1510
|
// payload; honoured when the transcript isn't reachable.
|
|
1615
|
-
// 3. PTY ringbuffer — last-resort fallback already handled by
|
|
1616
|
-
// the existing spinner_stop pathway.
|
|
1617
1511
|
const transcriptPath = evt.data?.transcript_path || '';
|
|
1618
1512
|
let lastAssistantMsg = evt.data?.last_assistant_message || '';
|
|
1619
1513
|
if (transcriptPath) {
|