orchestrix-yuri 2.4.0 → 2.6.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/lib/gateway/engine/claude-tmux.js +108 -87
- package/package.json +1 -1
|
@@ -174,15 +174,39 @@ const PROCESSING_RE = /●/;
|
|
|
174
174
|
*/
|
|
175
175
|
function stripChrome(raw) {
|
|
176
176
|
return raw
|
|
177
|
-
|
|
178
|
-
.replace(/\x1B\]
|
|
179
|
-
.replace(
|
|
180
|
-
|
|
181
|
-
.replace(/[
|
|
182
|
-
.replace(
|
|
183
|
-
.replace(
|
|
184
|
-
|
|
185
|
-
.replace(
|
|
177
|
+
// ANSI escape sequences
|
|
178
|
+
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
|
|
179
|
+
.replace(/\x1B\].*?\x07/g, '')
|
|
180
|
+
// TUI indicators and decorations
|
|
181
|
+
.replace(/[○●◐◑⏺]/g, '')
|
|
182
|
+
.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, '')
|
|
183
|
+
.replace(/[⏵━─█·…→]/g, '')
|
|
184
|
+
// Claude Code banner
|
|
185
|
+
.replace(/^.*▐▛.*$/gm, '')
|
|
186
|
+
.replace(/^.*▝▜.*$/gm, '')
|
|
187
|
+
.replace(/^.*▘▘.*$/gm, '')
|
|
188
|
+
.replace(/^.*Claude Code v[\d.]+.*$/gm, '')
|
|
189
|
+
.replace(/^.*Opus.*context.*$/gm, '')
|
|
190
|
+
// Status line elements
|
|
191
|
+
.replace(/^.*bypass permissions.*$/gm, '')
|
|
192
|
+
.replace(/^.*shift\+tab to cycle.*$/gm, '')
|
|
193
|
+
.replace(/^.*◐\s*(min|medium|max|low|high).*$/gm, '')
|
|
194
|
+
.replace(/^.*\/effort.*$/gm, '')
|
|
195
|
+
.replace(/^.*Proxy\s*-\s*(On|Off).*$/gm, '')
|
|
196
|
+
// Shell commands that leaked
|
|
197
|
+
.replace(/^.*export\s+CLAUDE_AUTOCOMPACT.*$/gm, '')
|
|
198
|
+
.replace(/^.*dangerously-skip-permissions.*$/gm, '')
|
|
199
|
+
// Completion stats and spinner verbs
|
|
200
|
+
.replace(/^.*[A-Z][a-z]*ed for \d+.*$/gm, '')
|
|
201
|
+
.replace(/^.*[A-Z][a-z]*ing\.{3}.*$/gm, '')
|
|
202
|
+
// Prompt cursor and line decorations
|
|
203
|
+
.replace(/^❯\s*$/gm, '')
|
|
204
|
+
.replace(/^─+$/gm, '')
|
|
205
|
+
// Line-number gutter
|
|
206
|
+
.replace(/^\s*\d+\s*[│|]\s*/gm, '')
|
|
207
|
+
// Collapse blank lines
|
|
208
|
+
.replace(/^\s*$/gm, '')
|
|
209
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
186
210
|
.trim();
|
|
187
211
|
}
|
|
188
212
|
|
|
@@ -204,39 +228,31 @@ function paneTail(name, n) {
|
|
|
204
228
|
}
|
|
205
229
|
|
|
206
230
|
/**
|
|
207
|
-
*
|
|
231
|
+
* Check if Claude Code has started up and is ready for input.
|
|
232
|
+
* Used ONLY during session initialization — looks for the ❯ input prompt
|
|
233
|
+
* which appears once Claude Code has fully loaded.
|
|
208
234
|
*
|
|
209
|
-
*
|
|
235
|
+
* DO NOT use this for response completion detection — ❯ is always visible.
|
|
210
236
|
*/
|
|
211
|
-
function
|
|
237
|
+
function isStarted(name) {
|
|
212
238
|
const tail = paneTail(name, 15);
|
|
239
|
+
return IDLE_RE.test(tail);
|
|
240
|
+
}
|
|
213
241
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (IDLE_RE.test(tail)) {
|
|
222
|
-
return 'idle';
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Priority 3: Processing indicator — still working
|
|
226
|
-
if (PROCESSING_RE.test(tail) || BRAILLE_SPINNER.test(tail)) {
|
|
227
|
-
return 'processing';
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return 'unknown';
|
|
242
|
+
/**
|
|
243
|
+
* Check if a completion message is present in the pane output.
|
|
244
|
+
* e.g. "Baked for 31s", "Worked for 2m 45s"
|
|
245
|
+
* This is the most reliable signal that Claude has finished responding.
|
|
246
|
+
*/
|
|
247
|
+
function hasCompletionMessage(text) {
|
|
248
|
+
return COMPLETION_RE.test(text);
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
/**
|
|
234
|
-
*
|
|
235
|
-
* Checks for ○ idle indicator or completion message.
|
|
252
|
+
* Check if Claude Code is actively processing (● spinner visible).
|
|
236
253
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
return state === 'idle' || state === 'complete';
|
|
254
|
+
function isProcessing(text) {
|
|
255
|
+
return PROCESSING_RE.test(text) || BRAILLE_SPINNER.test(text);
|
|
240
256
|
}
|
|
241
257
|
|
|
242
258
|
// ── Context Management ─────────────────────────────────────────────────────────
|
|
@@ -304,7 +320,7 @@ async function proactiveCompact(name) {
|
|
|
304
320
|
log.tmux('Proactive /compact triggered');
|
|
305
321
|
injectMessage(name, '/compact focus on the most recent user conversation and any pending operations');
|
|
306
322
|
|
|
307
|
-
const ok = await
|
|
323
|
+
const ok = await waitForReady(name, 120000); // compact can take up to 2min
|
|
308
324
|
if (ok) {
|
|
309
325
|
_messageCount = 0;
|
|
310
326
|
log.tmux('Proactive /compact completed');
|
|
@@ -343,46 +359,45 @@ async function createSession(engineConfig) {
|
|
|
343
359
|
tmux(`set-option -t ${sessionName} history-limit ${HISTORY_LIMIT}`);
|
|
344
360
|
log.tmux(`Session "${sessionName}" created, launching Claude Code...`);
|
|
345
361
|
|
|
346
|
-
// Set
|
|
362
|
+
// Set env var and launch Claude Code in a single command to keep pane clean.
|
|
363
|
+
// Using && chains avoids separate shell prompt lines polluting capture output.
|
|
347
364
|
const compactPct = engineConfig.autocompact_pct || 80;
|
|
348
|
-
tmux(`send-keys -t ${sessionName}:0 'export CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=${compactPct}' Enter`);
|
|
349
|
-
|
|
350
|
-
// Launch Claude Code in interactive mode
|
|
351
|
-
tmux(`send-keys -t ${sessionName}:0 '"${binary}" --dangerously-skip-permissions' Enter`);
|
|
365
|
+
tmux(`send-keys -t ${sessionName}:0 'export CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=${compactPct} && "${binary}" --dangerously-skip-permissions' Enter`);
|
|
352
366
|
|
|
353
|
-
// Wait for Claude Code to initialize (detect
|
|
354
|
-
// Default 60s — Claude Code needs time to load CLAUDE.md, connect MCP servers, etc.
|
|
367
|
+
// Wait for Claude Code to initialize (detect ❯ prompt)
|
|
355
368
|
const startupTimeout = engineConfig.startup_timeout || 60000;
|
|
356
|
-
log.tmux(`Waiting for Claude Code to
|
|
357
|
-
const started = await
|
|
369
|
+
log.tmux(`Waiting for Claude Code to start (timeout: ${startupTimeout / 1000}s)...`);
|
|
370
|
+
const started = await waitForReady(sessionName, startupTimeout);
|
|
358
371
|
if (!started) {
|
|
359
|
-
// Don't kill session on failure — let user debug with tmux attach
|
|
360
372
|
const tail = paneTail(sessionName, 10);
|
|
361
|
-
log.error(`Claude Code did not
|
|
373
|
+
log.error(`Claude Code did not start within ${startupTimeout / 1000}s`);
|
|
362
374
|
log.error(`Last pane output:\n${tail}`);
|
|
363
375
|
log.info(`Debug: tmux attach -t ${sessionName}`);
|
|
364
|
-
throw new Error(`Claude Code did not
|
|
376
|
+
throw new Error(`Claude Code did not start within ${startupTimeout / 1000}s`);
|
|
365
377
|
}
|
|
366
378
|
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
379
|
+
// Clear tmux scrollback so session setup commands don't pollute response capture.
|
|
380
|
+
// The pane currently contains: shell commands, banner, status line.
|
|
381
|
+
// We want captureResponse to only see content from user messages onward.
|
|
382
|
+
await new Promise((r) => setTimeout(r, 1000)); // let TUI fully render
|
|
383
|
+
tmuxSafe(`clear-history -t ${sessionName}:0`);
|
|
384
|
+
|
|
385
|
+
// NOTE: We do NOT inject L1 context here. Instead, composePrompt() prepends
|
|
386
|
+
// L1 to the first user message. This avoids: (1) scrollback pollution from
|
|
387
|
+
// a huge YAML block, (2) waitForReady returning immediately because ❯ is
|
|
388
|
+
// always visible, (3) race conditions between L1 processing and first message.
|
|
376
389
|
|
|
377
390
|
_sessionReady = true;
|
|
378
391
|
log.tmux(`Session "${sessionName}" ready (cwd: ${projectRoot})`);
|
|
379
392
|
}
|
|
380
393
|
|
|
381
394
|
/**
|
|
382
|
-
* Wait for Claude Code to
|
|
383
|
-
*
|
|
395
|
+
* Wait for Claude Code to be ready (❯ prompt visible).
|
|
396
|
+
* Used for session init and after /compact — NOT for response capture.
|
|
397
|
+
*
|
|
398
|
+
* @returns {Promise<boolean>} true if ready detected, false if timeout
|
|
384
399
|
*/
|
|
385
|
-
function
|
|
400
|
+
function waitForReady(name, timeoutMs) {
|
|
386
401
|
const pollInterval = 2000;
|
|
387
402
|
return new Promise((resolve) => {
|
|
388
403
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -394,7 +409,7 @@ function waitForIdle(name, timeoutMs) {
|
|
|
394
409
|
return resolve(false);
|
|
395
410
|
}
|
|
396
411
|
|
|
397
|
-
if (
|
|
412
|
+
if (isStarted(name)) {
|
|
398
413
|
return resolve(true);
|
|
399
414
|
}
|
|
400
415
|
setTimeout(poll, pollInterval);
|
|
@@ -422,10 +437,14 @@ function injectMessage(name, text) {
|
|
|
422
437
|
/**
|
|
423
438
|
* Capture the response after injecting a message.
|
|
424
439
|
*
|
|
425
|
-
* Detection
|
|
440
|
+
* Detection strategy (❯ prompt is always visible, so we CANNOT use idle detection):
|
|
426
441
|
* P1: Completion message — "[Verb]ed for [N]s/m" (e.g. "Baked for 31s")
|
|
427
|
-
*
|
|
428
|
-
*
|
|
442
|
+
* Most reliable signal. Appears exactly once when Claude finishes.
|
|
443
|
+
* P2: Content stability — pane output unchanged for N consecutive polls.
|
|
444
|
+
* Fallback for edge cases where completion message is missed.
|
|
445
|
+
*
|
|
446
|
+
* We also track whether content has changed since injection (via marker)
|
|
447
|
+
* to avoid returning before Claude has even started responding.
|
|
429
448
|
*/
|
|
430
449
|
async function captureResponse(name, marker, engineConfig) {
|
|
431
450
|
const timeout = engineConfig.timeout || 300000;
|
|
@@ -435,7 +454,12 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
435
454
|
const deadline = Date.now() + timeout;
|
|
436
455
|
let lastHash = '';
|
|
437
456
|
let stableCount = 0;
|
|
438
|
-
let
|
|
457
|
+
let contentChanged = false;
|
|
458
|
+
|
|
459
|
+
// Capture baseline right after injection
|
|
460
|
+
const baselineRaw = capturePaneRaw(name, 500);
|
|
461
|
+
const baselineHash = crypto.createHash('md5').update(baselineRaw).digest('hex');
|
|
462
|
+
lastHash = baselineHash;
|
|
439
463
|
|
|
440
464
|
return new Promise((resolve) => {
|
|
441
465
|
const poll = () => {
|
|
@@ -451,31 +475,22 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
451
475
|
return resolve({ reply: '❌ Claude Code session terminated unexpectedly.', raw: '' });
|
|
452
476
|
}
|
|
453
477
|
|
|
454
|
-
const state = detectState(name);
|
|
455
478
|
const raw = capturePaneRaw(name, 500);
|
|
456
479
|
const hash = crypto.createHash('md5').update(raw).digest('hex');
|
|
457
480
|
|
|
458
|
-
// Track
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (state === 'processing') {
|
|
462
|
-
sawProcessing = true;
|
|
463
|
-
stableCount = 0;
|
|
464
|
-
lastHash = hash;
|
|
465
|
-
return setTimeout(poll, pollInterval);
|
|
481
|
+
// Track if content has changed since injection
|
|
482
|
+
if (hash !== baselineHash) {
|
|
483
|
+
contentChanged = true;
|
|
466
484
|
}
|
|
467
485
|
|
|
468
486
|
// P1: Completion message — most reliable done signal
|
|
469
|
-
|
|
487
|
+
// Only check after content has changed (Claude has started responding)
|
|
488
|
+
if (contentChanged && hasCompletionMessage(paneTail(name, 15))) {
|
|
470
489
|
return resolve(extractResponse(raw, marker));
|
|
471
490
|
}
|
|
472
491
|
|
|
473
|
-
// P2:
|
|
474
|
-
|
|
475
|
-
return resolve(extractResponse(raw, marker));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// P4: Content stability fallback
|
|
492
|
+
// P2: Content stability — pane unchanged for N polls
|
|
493
|
+
// Only trigger after content has changed from baseline
|
|
479
494
|
if (hash === lastHash) {
|
|
480
495
|
stableCount++;
|
|
481
496
|
} else {
|
|
@@ -483,7 +498,7 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
483
498
|
lastHash = hash;
|
|
484
499
|
}
|
|
485
500
|
|
|
486
|
-
if (stableCount >= stableThreshold
|
|
501
|
+
if (contentChanged && stableCount >= stableThreshold) {
|
|
487
502
|
log.tmux('Response detected via content stability');
|
|
488
503
|
return resolve(extractResponse(raw, marker));
|
|
489
504
|
}
|
|
@@ -492,7 +507,6 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
492
507
|
};
|
|
493
508
|
|
|
494
509
|
// Initial delay: give Claude time to start processing
|
|
495
|
-
// before first poll (avoids false-positive idle detection)
|
|
496
510
|
setTimeout(poll, Math.max(pollInterval, 3000));
|
|
497
511
|
});
|
|
498
512
|
}
|
|
@@ -515,12 +529,12 @@ function extractResponse(raw, marker) {
|
|
|
515
529
|
|
|
516
530
|
let responseText;
|
|
517
531
|
if (markerIdx >= 0) {
|
|
518
|
-
|
|
532
|
+
log.tmux(`Marker found at line ${markerIdx}/${lines.length}`);
|
|
519
533
|
const afterMarker = lines.slice(markerIdx + 1).join('\n');
|
|
520
534
|
responseText = stripChrome(afterMarker);
|
|
521
535
|
} else {
|
|
522
|
-
|
|
523
|
-
const tail = lines.slice(-
|
|
536
|
+
log.warn(`Marker not found in pane output, using last 50 lines as fallback`);
|
|
537
|
+
const tail = lines.slice(-50).join('\n');
|
|
524
538
|
responseText = stripChrome(tail);
|
|
525
539
|
}
|
|
526
540
|
|
|
@@ -626,14 +640,21 @@ async function callClaude(opts) {
|
|
|
626
640
|
|
|
627
641
|
/**
|
|
628
642
|
* Compose prompt for the persistent session.
|
|
629
|
-
*
|
|
630
|
-
*
|
|
643
|
+
* First message includes L1 context to prime the session.
|
|
644
|
+
* Subsequent messages send only the raw user text.
|
|
631
645
|
*
|
|
632
646
|
* @param {string} userMessage - The user's message text
|
|
633
647
|
* @param {Array} _chatHistory - Unused (Claude keeps its own context)
|
|
634
648
|
* @returns {string}
|
|
635
649
|
*/
|
|
636
650
|
function composePrompt(userMessage, _chatHistory) {
|
|
651
|
+
// First message: prepend L1 context so Claude knows who it is
|
|
652
|
+
if (_messageCount === 0) {
|
|
653
|
+
const l1 = loadL1Context();
|
|
654
|
+
if (l1) {
|
|
655
|
+
return `${l1}\n\n---\n\nUser message: ${userMessage}`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
637
658
|
return userMessage;
|
|
638
659
|
}
|
|
639
660
|
|