orchestrix-yuri 2.6.0 → 2.7.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.
@@ -347,10 +347,9 @@ async function createSession(engineConfig) {
347
347
  // Ensure CLAUDE.md has channel mode instructions (survives compact)
348
348
  ensureClaudeMd(projectRoot);
349
349
 
350
- // Only kill existing session if it exists don't kill on retry
351
- // so the user can `tmux attach -t yuri-gateway` to debug
350
+ // Kill existing broken session (ensureSession already checked if it was healthy)
352
351
  if (hasSession(sessionName)) {
353
- log.tmux(`Killing existing session "${sessionName}"`);
352
+ log.tmux(`Replacing unhealthy session "${sessionName}"`);
354
353
  tmuxSafe(`kill-session -t ${sessionName}`);
355
354
  }
356
355
 
@@ -446,7 +445,7 @@ function injectMessage(name, text) {
446
445
  * We also track whether content has changed since injection (via marker)
447
446
  * to avoid returning before Claude has even started responding.
448
447
  */
449
- async function captureResponse(name, marker, engineConfig) {
448
+ async function captureResponse(name, engineConfig) {
450
449
  const timeout = engineConfig.timeout || 300000;
451
450
  const pollInterval = engineConfig.poll_interval || 2000;
452
451
  const stableThreshold = engineConfig.stable_count || 3;
@@ -467,7 +466,7 @@ async function captureResponse(name, marker, engineConfig) {
467
466
  if (Date.now() > deadline) {
468
467
  log.warn('Response capture timed out');
469
468
  const raw = capturePaneRaw(name, 500);
470
- return resolve(extractResponse(raw, marker));
469
+ return resolve(extractResponse(raw, baselineRaw));
471
470
  }
472
471
 
473
472
  // Session died
@@ -484,13 +483,11 @@ async function captureResponse(name, marker, engineConfig) {
484
483
  }
485
484
 
486
485
  // P1: Completion message — most reliable done signal
487
- // Only check after content has changed (Claude has started responding)
488
486
  if (contentChanged && hasCompletionMessage(paneTail(name, 15))) {
489
- return resolve(extractResponse(raw, marker));
487
+ return resolve(extractResponse(raw, baselineRaw));
490
488
  }
491
489
 
492
490
  // P2: Content stability — pane unchanged for N polls
493
- // Only trigger after content has changed from baseline
494
491
  if (hash === lastHash) {
495
492
  stableCount++;
496
493
  } else {
@@ -500,7 +497,7 @@ async function captureResponse(name, marker, engineConfig) {
500
497
 
501
498
  if (contentChanged && stableCount >= stableThreshold) {
502
499
  log.tmux('Response detected via content stability');
503
- return resolve(extractResponse(raw, marker));
500
+ return resolve(extractResponse(raw, baselineRaw));
504
501
  }
505
502
 
506
503
  setTimeout(poll, pollInterval);
@@ -512,49 +509,60 @@ async function captureResponse(name, marker, engineConfig) {
512
509
  }
513
510
 
514
511
  /**
515
- * Extract the assistant's response from captured pane output.
516
- * Finds the marker, takes everything after it, strips chrome.
512
+ * Extract the assistant's response by diffing current pane against baseline.
513
+ *
514
+ * Strategy: the baseline was captured right after message injection (before
515
+ * Claude started responding). The current capture has Claude's response + TUI
516
+ * chrome. We diff the two to find only the new content.
517
+ *
518
+ * @param {string} currentRaw - Current pane capture
519
+ * @param {string} baselineRaw - Pane capture from right after injection
520
+ * @returns {{reply: string, raw: string}}
517
521
  */
518
- function extractResponse(raw, marker) {
519
- const lines = raw.split('\n');
520
- let markerIdx = -1;
521
-
522
- // Find the last occurrence of the marker (in case of scrollback)
523
- for (let i = lines.length - 1; i >= 0; i--) {
524
- if (lines[i].includes(marker)) {
525
- markerIdx = i;
526
- break;
527
- }
528
- }
522
+ function extractResponse(currentRaw, baselineRaw) {
523
+ const currentLines = currentRaw.split('\n');
524
+ const baselineLines = new Set(baselineRaw.split('\n'));
529
525
 
530
- let responseText;
531
- if (markerIdx >= 0) {
532
- log.tmux(`Marker found at line ${markerIdx}/${lines.length}`);
533
- const afterMarker = lines.slice(markerIdx + 1).join('\n');
534
- responseText = stripChrome(afterMarker);
535
- } else {
536
- log.warn(`Marker not found in pane output, using last 50 lines as fallback`);
537
- const tail = lines.slice(-50).join('\n');
538
- responseText = stripChrome(tail);
539
- }
526
+ // Find lines that are new (not in baseline)
527
+ const newLines = currentLines.filter((line) => !baselineLines.has(line));
528
+ let responseText = stripChrome(newLines.join('\n'));
540
529
 
541
- // Trim trailing idle indicators and empty lines
530
+ // Remove trailing indicators and collapse blank lines
542
531
  responseText = responseText
543
532
  .replace(/[○●◐◑]\s*$/g, '')
544
533
  .replace(/\n{3,}/g, '\n\n')
545
534
  .trim();
546
535
 
547
- return { reply: responseText || '(no response captured)', raw };
536
+ if (!responseText) {
537
+ log.warn('No new content found after diffing pane output');
538
+ // Fallback: strip the whole current output
539
+ responseText = stripChrome(currentLines.slice(-30).join('\n')).trim();
540
+ }
541
+
542
+ return { reply: responseText || '(no response captured)', raw: currentRaw };
548
543
  }
549
544
 
550
545
  // ── Public API ─────────────────────────────────────────────────────────────────
551
546
 
552
547
  /**
553
548
  * Ensure the tmux session is alive and ready.
554
- * Lazy-initializes on first call. Restarts if session died.
549
+ * - If session already exists with Claude Code running → reuse it
550
+ * - If session doesn't exist → create fresh
551
+ * - If session exists but Claude Code crashed → recreate
555
552
  */
556
553
  async function ensureSession(engineConfig) {
557
- if (_sessionName && hasSession(_sessionName) && _sessionReady) {
554
+ const sessionName = engineConfig.tmux_session || DEFAULT_SESSION;
555
+
556
+ // Fast path: session alive and marked ready in this process
557
+ if (_sessionName === sessionName && hasSession(sessionName) && _sessionReady) {
558
+ return;
559
+ }
560
+
561
+ // Check if session exists from a previous gateway run
562
+ if (hasSession(sessionName) && isStarted(sessionName)) {
563
+ log.tmux(`Reusing existing session "${sessionName}" (Claude Code is running)`);
564
+ _sessionName = sessionName;
565
+ _sessionReady = true;
558
566
  return;
559
567
  }
560
568
 
@@ -573,12 +581,10 @@ async function ensureSession(engineConfig) {
573
581
  log.warn(`Session init attempt ${attempt}/${maxRetries} failed: ${err.message}`);
574
582
  if (attempt === maxRetries) {
575
583
  log.error('All init attempts failed. Check Claude Code installation and tmux.');
576
- log.info(`Debug: tmux attach -t ${engineConfig.tmux_session || DEFAULT_SESSION}`);
584
+ log.info(`Debug: tmux attach -t ${sessionName}`);
577
585
  throw err;
578
586
  }
579
- // Kill session before retry so createSession starts fresh
580
- const sn = engineConfig.tmux_session || DEFAULT_SESSION;
581
- if (hasSession(sn)) tmuxSafe(`kill-session -t ${sn}`);
587
+ if (hasSession(sessionName)) tmuxSafe(`kill-session -t ${sessionName}`);
582
588
  await new Promise((r) => setTimeout(r, 3000));
583
589
  }
584
590
  }
@@ -617,13 +623,9 @@ async function callClaude(opts) {
617
623
  await proactiveCompact(_sessionName);
618
624
  }
619
625
 
620
- // Generate a unique marker for boundary detection
621
- const marker = `YURI-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
622
- const markedPrompt = `[${marker}] ${prompt}`;
623
-
624
- // Inject and capture
625
- injectMessage(_sessionName, markedPrompt);
626
- const result = await captureResponse(_sessionName, marker, config);
626
+ // Inject user message (no marker extraction uses completion message position)
627
+ injectMessage(_sessionName, prompt);
628
+ const result = await captureResponse(_sessionName, config);
627
629
 
628
630
  _messageCount++;
629
631
  resolve(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Yuri — Meta-Orchestrator for Orchestrix. Drive your entire project lifecycle with natural language.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {