orchestrix-yuri 2.3.4 → 2.5.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.
|
@@ -143,24 +143,28 @@ function tmuxSafe(cmd) {
|
|
|
143
143
|
|
|
144
144
|
// ── Claude Code TUI Indicators ─────────────────────────────────────────────────
|
|
145
145
|
//
|
|
146
|
-
// Claude Code
|
|
146
|
+
// Claude Code TUI state indicators vary by version and statusline config:
|
|
147
147
|
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
148
|
+
// Idle indicators (any of these = ready for input):
|
|
149
|
+
// ○ (U+25CB) — circle idle indicator (shown with certain statusline configs)
|
|
150
|
+
// ❯ — prompt cursor (always shown when idle, most reliable)
|
|
151
151
|
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
152
|
+
// Processing indicators:
|
|
153
|
+
// ● (U+25CF) — filled circle, active generation
|
|
154
|
+
// Braille spinner: ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
|
|
154
155
|
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
156
|
+
// Status line elements (NOT state indicators):
|
|
157
|
+
// ◐ — effort level indicator (e.g. "◐ medium · /effort"), NOT approval prompt
|
|
158
|
+
//
|
|
159
|
+
// Completion message (past-tense verb + duration):
|
|
160
|
+
// "Baked for 31s", "Worked for 2m 45s"
|
|
161
|
+
// Pattern: /[A-Z][a-z]*ed for \d+/
|
|
158
162
|
//
|
|
159
163
|
// ────────────────────────────────────────────────────────────────────────────────
|
|
160
164
|
|
|
161
165
|
const BRAILLE_SPINNER = /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/;
|
|
162
166
|
const COMPLETION_RE = /[A-Z][a-z]*ed for \d+/;
|
|
163
|
-
const IDLE_RE =
|
|
167
|
+
const IDLE_RE = /[○❯]/;
|
|
164
168
|
const PROCESSING_RE = /●/;
|
|
165
169
|
|
|
166
170
|
/**
|
|
@@ -200,43 +204,31 @@ function paneTail(name, n) {
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* should never appear. The detection is kept for robustness but no auto-approve
|
|
207
|
-
* action is taken — sending blind 'y' keystrokes is dangerous.
|
|
207
|
+
* Check if Claude Code has started up and is ready for input.
|
|
208
|
+
* Used ONLY during session initialization — looks for the ❯ input prompt
|
|
209
|
+
* which appears once Claude Code has fully loaded.
|
|
208
210
|
*
|
|
209
|
-
*
|
|
211
|
+
* DO NOT use this for response completion detection — ❯ is always visible.
|
|
210
212
|
*/
|
|
211
|
-
function
|
|
213
|
+
function isStarted(name) {
|
|
212
214
|
const tail = paneTail(name, 15);
|
|
215
|
+
return IDLE_RE.test(tail);
|
|
216
|
+
}
|
|
213
217
|
|
|
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';
|
|
218
|
+
/**
|
|
219
|
+
* Check if a completion message is present in the pane output.
|
|
220
|
+
* e.g. "Baked for 31s", "Worked for 2m 45s"
|
|
221
|
+
* This is the most reliable signal that Claude has finished responding.
|
|
222
|
+
*/
|
|
223
|
+
function hasCompletionMessage(text) {
|
|
224
|
+
return COMPLETION_RE.test(text);
|
|
231
225
|
}
|
|
232
226
|
|
|
233
227
|
/**
|
|
234
|
-
*
|
|
235
|
-
* Checks for ○ idle indicator or completion message.
|
|
228
|
+
* Check if Claude Code is actively processing (● spinner visible).
|
|
236
229
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
return state === 'idle' || state === 'complete';
|
|
230
|
+
function isProcessing(text) {
|
|
231
|
+
return PROCESSING_RE.test(text) || BRAILLE_SPINNER.test(text);
|
|
240
232
|
}
|
|
241
233
|
|
|
242
234
|
// ── Context Management ─────────────────────────────────────────────────────────
|
|
@@ -304,7 +296,7 @@ async function proactiveCompact(name) {
|
|
|
304
296
|
log.tmux('Proactive /compact triggered');
|
|
305
297
|
injectMessage(name, '/compact focus on the most recent user conversation and any pending operations');
|
|
306
298
|
|
|
307
|
-
const ok = await
|
|
299
|
+
const ok = await waitForReady(name, 120000); // compact can take up to 2min
|
|
308
300
|
if (ok) {
|
|
309
301
|
_messageCount = 0;
|
|
310
302
|
log.tmux('Proactive /compact completed');
|
|
@@ -354,7 +346,7 @@ async function createSession(engineConfig) {
|
|
|
354
346
|
// Default 60s — Claude Code needs time to load CLAUDE.md, connect MCP servers, etc.
|
|
355
347
|
const startupTimeout = engineConfig.startup_timeout || 60000;
|
|
356
348
|
log.tmux(`Waiting for Claude Code to become idle (timeout: ${startupTimeout / 1000}s)...`);
|
|
357
|
-
const started = await
|
|
349
|
+
const started = await waitForReady(sessionName, startupTimeout);
|
|
358
350
|
if (!started) {
|
|
359
351
|
// Don't kill session on failure — let user debug with tmux attach
|
|
360
352
|
const tail = paneTail(sessionName, 10);
|
|
@@ -371,7 +363,7 @@ async function createSession(engineConfig) {
|
|
|
371
363
|
if (l1) {
|
|
372
364
|
log.tmux('Injecting L1 context...');
|
|
373
365
|
await injectMessage(sessionName, l1);
|
|
374
|
-
await
|
|
366
|
+
await waitForReady(sessionName, 120000); // allow up to 2min for L1 processing
|
|
375
367
|
}
|
|
376
368
|
|
|
377
369
|
_sessionReady = true;
|
|
@@ -379,10 +371,12 @@ async function createSession(engineConfig) {
|
|
|
379
371
|
}
|
|
380
372
|
|
|
381
373
|
/**
|
|
382
|
-
* Wait for Claude Code to
|
|
383
|
-
*
|
|
374
|
+
* Wait for Claude Code to be ready (❯ prompt visible).
|
|
375
|
+
* Used for session init and after /compact — NOT for response capture.
|
|
376
|
+
*
|
|
377
|
+
* @returns {Promise<boolean>} true if ready detected, false if timeout
|
|
384
378
|
*/
|
|
385
|
-
function
|
|
379
|
+
function waitForReady(name, timeoutMs) {
|
|
386
380
|
const pollInterval = 2000;
|
|
387
381
|
return new Promise((resolve) => {
|
|
388
382
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -394,7 +388,7 @@ function waitForIdle(name, timeoutMs) {
|
|
|
394
388
|
return resolve(false);
|
|
395
389
|
}
|
|
396
390
|
|
|
397
|
-
if (
|
|
391
|
+
if (isStarted(name)) {
|
|
398
392
|
return resolve(true);
|
|
399
393
|
}
|
|
400
394
|
setTimeout(poll, pollInterval);
|
|
@@ -422,10 +416,14 @@ function injectMessage(name, text) {
|
|
|
422
416
|
/**
|
|
423
417
|
* Capture the response after injecting a message.
|
|
424
418
|
*
|
|
425
|
-
* Detection
|
|
419
|
+
* Detection strategy (❯ prompt is always visible, so we CANNOT use idle detection):
|
|
426
420
|
* P1: Completion message — "[Verb]ed for [N]s/m" (e.g. "Baked for 31s")
|
|
427
|
-
*
|
|
428
|
-
*
|
|
421
|
+
* Most reliable signal. Appears exactly once when Claude finishes.
|
|
422
|
+
* P2: Content stability — pane output unchanged for N consecutive polls.
|
|
423
|
+
* Fallback for edge cases where completion message is missed.
|
|
424
|
+
*
|
|
425
|
+
* We also track whether content has changed since injection (via marker)
|
|
426
|
+
* to avoid returning before Claude has even started responding.
|
|
429
427
|
*/
|
|
430
428
|
async function captureResponse(name, marker, engineConfig) {
|
|
431
429
|
const timeout = engineConfig.timeout || 300000;
|
|
@@ -435,7 +433,12 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
435
433
|
const deadline = Date.now() + timeout;
|
|
436
434
|
let lastHash = '';
|
|
437
435
|
let stableCount = 0;
|
|
438
|
-
let
|
|
436
|
+
let contentChanged = false;
|
|
437
|
+
|
|
438
|
+
// Capture baseline right after injection
|
|
439
|
+
const baselineRaw = capturePaneRaw(name, 500);
|
|
440
|
+
const baselineHash = crypto.createHash('md5').update(baselineRaw).digest('hex');
|
|
441
|
+
lastHash = baselineHash;
|
|
439
442
|
|
|
440
443
|
return new Promise((resolve) => {
|
|
441
444
|
const poll = () => {
|
|
@@ -451,31 +454,22 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
451
454
|
return resolve({ reply: '❌ Claude Code session terminated unexpectedly.', raw: '' });
|
|
452
455
|
}
|
|
453
456
|
|
|
454
|
-
const state = detectState(name);
|
|
455
457
|
const raw = capturePaneRaw(name, 500);
|
|
456
458
|
const hash = crypto.createHash('md5').update(raw).digest('hex');
|
|
457
459
|
|
|
458
|
-
// Track
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (state === 'processing') {
|
|
462
|
-
sawProcessing = true;
|
|
463
|
-
stableCount = 0;
|
|
464
|
-
lastHash = hash;
|
|
465
|
-
return setTimeout(poll, pollInterval);
|
|
460
|
+
// Track if content has changed since injection
|
|
461
|
+
if (hash !== baselineHash) {
|
|
462
|
+
contentChanged = true;
|
|
466
463
|
}
|
|
467
464
|
|
|
468
465
|
// P1: Completion message — most reliable done signal
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// P2: Idle indicator — done if we saw processing start
|
|
474
|
-
if (state === 'idle' && sawProcessing) {
|
|
466
|
+
// Only check after content has changed (Claude has started responding)
|
|
467
|
+
if (contentChanged && hasCompletionMessage(paneTail(name, 15))) {
|
|
475
468
|
return resolve(extractResponse(raw, marker));
|
|
476
469
|
}
|
|
477
470
|
|
|
478
|
-
//
|
|
471
|
+
// P2: Content stability — pane unchanged for N polls
|
|
472
|
+
// Only trigger after content has changed from baseline
|
|
479
473
|
if (hash === lastHash) {
|
|
480
474
|
stableCount++;
|
|
481
475
|
} else {
|
|
@@ -483,7 +477,7 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
483
477
|
lastHash = hash;
|
|
484
478
|
}
|
|
485
479
|
|
|
486
|
-
if (stableCount >= stableThreshold
|
|
480
|
+
if (contentChanged && stableCount >= stableThreshold) {
|
|
487
481
|
log.tmux('Response detected via content stability');
|
|
488
482
|
return resolve(extractResponse(raw, marker));
|
|
489
483
|
}
|
|
@@ -492,7 +486,6 @@ async function captureResponse(name, marker, engineConfig) {
|
|
|
492
486
|
};
|
|
493
487
|
|
|
494
488
|
// Initial delay: give Claude time to start processing
|
|
495
|
-
// before first poll (avoids false-positive idle detection)
|
|
496
489
|
setTimeout(poll, Math.max(pollInterval, 3000));
|
|
497
490
|
});
|
|
498
491
|
}
|