dual-brain 0.3.33 → 0.3.35

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.
Files changed (2) hide show
  1. package/bin/dual-brain.mjs +160 -36
  2. package/package.json +1 -1
@@ -1462,21 +1462,16 @@ async function cmdRuntimeSwitch(args = []) {
1462
1462
  const cwd = process.cwd();
1463
1463
  let sessions = [];
1464
1464
  try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
1465
- const active = readActiveConversation(cwd);
1466
- const sess = active
1467
- ? (sessions.find(s => s.id === active.sessionId) || {
1468
- id: active.sessionId,
1469
- tool: active.provider,
1470
- smartName: active.sessionName || active.conversationId || active.sessionId,
1471
- })
1472
- : sessions[0];
1465
+ const resolved = resolveCurrentConversationSession(cwd, sessions);
1466
+ const active = resolved.active;
1467
+ const sess = resolved.session;
1473
1468
  if (!sess) {
1474
- console.log('No resumable session found.');
1469
+ console.log('No current logical conversation found. Resume the intended chat through dual-brain first.');
1475
1470
  return;
1476
1471
  }
1477
1472
 
1478
1473
  const profile = loadProfile(cwd);
1479
- const settingsTerminalId = active?.terminalId || getTerminalId();
1474
+ const settingsTerminalId = resolved.terminalId || getTerminalId();
1480
1475
  const settings = loadSessionSettings(cwd, settingsTerminalId);
1481
1476
  let provider = _sessionTool(sess);
1482
1477
  let reason = 'head-runtime-switch';
@@ -1522,6 +1517,11 @@ async function cmdRuntimeSwitch(args = []) {
1522
1517
  settings.headModel = existingPending.model;
1523
1518
  if (!explicitEffort && existingPending.effort) settings.effort = existingPending.effort;
1524
1519
  }
1520
+ const runningHead = inferRunningHeadConfig(cwd, sess.id, provider);
1521
+ if (!explicitModel && !explicitLevel && _modelMatchesProvider(runningHead.model, provider)) {
1522
+ settings.headModel = runningHead.model;
1523
+ if (!explicitEffort && runningHead.effort) settings.effort = runningHead.effort;
1524
+ }
1525
1525
 
1526
1526
  const headPolicy = _headPolicyFor(provider, profile, settings);
1527
1527
  if (!settings.headModel || !_modelMatchesProvider(settings.headModel, provider)) {
@@ -1549,9 +1549,13 @@ async function cmdRuntimeSwitch(args = []) {
1549
1549
  });
1550
1550
 
1551
1551
  const launchArgs = provider === _sessionTool(sess)
1552
- ? _sessionLaunchArgs(sess, cwd)
1552
+ ? runtimeLaunchArgsForPending(sess, cwd, pending)
1553
1553
  : ['handoff', '--to', provider];
1554
1554
 
1555
+ const activeForSwitch = confirmed ? readActiveConversation(cwd) : null;
1556
+ const activeMatches = activeForSwitch?.sessionId === sess.id;
1557
+ let restartSignaled = false;
1558
+
1555
1559
  console.log('');
1556
1560
  console.log(`Runtime switch ${confirmed ? 'confirmed' : 'prepared'} for ${pending?.sessionName || sess.id}`);
1557
1561
  console.log(`Provider: ${provider}`);
@@ -1561,10 +1565,16 @@ async function cmdRuntimeSwitch(args = []) {
1561
1565
  console.log(`Reason: ${reason}`);
1562
1566
  console.log(`Launch: ${provider} ${launchArgs.join(' ')}`);
1563
1567
  if (confirmed) {
1564
- const activeForSwitch = readActiveConversation(cwd);
1565
- if (activeForSwitch?.sessionId === sess.id) {
1566
- console.log('Active supervisor detected: HEAD will restart with these settings.');
1567
- signalActiveConversation(cwd, sess.id);
1568
+ if (activeMatches) {
1569
+ restartSignaled = signalActiveConversation(cwd, sess.id);
1570
+ if (restartSignaled) {
1571
+ const terminalNote = activeForSwitch.terminalId && activeForSwitch.terminalId !== getTerminalId()
1572
+ ? ` in ${activeForSwitch.terminalId}`
1573
+ : '';
1574
+ console.log(`Active supervisor detected${terminalNote}: restart signal sent.`);
1575
+ } else {
1576
+ console.log('Active supervisor was detected, but the restart signal could not be delivered.');
1577
+ }
1568
1578
  } else if (!args.includes('--apply')) {
1569
1579
  console.log('No active supervisor detected: saved for the next dual-brain resume/switch, not live yet.');
1570
1580
  }
@@ -1573,7 +1583,12 @@ async function cmdRuntimeSwitch(args = []) {
1573
1583
 
1574
1584
  if (args.includes('--apply')) {
1575
1585
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
1576
- console.log('Non-interactive shell detected: saved for the active dual-brain terminal to apply.');
1586
+ if (restartSignaled) {
1587
+ console.log('Non-interactive shell detected: command applied by signaling the active dual-brain terminal.');
1588
+ } else {
1589
+ console.log('Non-interactive shell detected: saved for the next dual-brain resume/switch.');
1590
+ console.log('This command cannot mutate an unattached API/chat surface that is already running under another provider.');
1591
+ }
1577
1592
  } else {
1578
1593
  const applied = await processPendingRuntimeSwitch(cwd);
1579
1594
  if (!applied) console.log('Could not apply live here; resume the session through dual-brain to use these settings.');
@@ -1598,16 +1613,10 @@ async function cmdSwitchover(args = []) {
1598
1613
  const cwd = process.cwd();
1599
1614
  let sessions = [];
1600
1615
  try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
1601
- const active = readActiveConversation(cwd);
1602
- const sess = active
1603
- ? (sessions.find(s => s.id === active.sessionId) || {
1604
- id: active.sessionId,
1605
- tool: active.provider,
1606
- smartName: active.sessionName || active.conversationId || active.sessionId,
1607
- })
1608
- : sessions[0];
1616
+ const resolved = resolveCurrentConversationSession(cwd, sessions);
1617
+ const sess = resolved.session;
1609
1618
  if (!sess) {
1610
- console.log('No resumable session found.');
1619
+ console.log('No current logical conversation found. Resume the intended chat through dual-brain first.');
1611
1620
  return;
1612
1621
  }
1613
1622
 
@@ -2428,6 +2437,87 @@ function loadTerminalState(cwd, terminalId) {
2428
2437
  } catch { return null; }
2429
2438
  }
2430
2439
 
2440
+ function loadLatestTerminalState(cwd) {
2441
+ try {
2442
+ const dir = join(cwd, '.dualbrain');
2443
+ const files = readdirSync(dir)
2444
+ .filter(f => /^terminal-.*\.json$/.test(f))
2445
+ .map(f => {
2446
+ const full = join(dir, f);
2447
+ const state = JSON.parse(readFileSync(full, 'utf8'));
2448
+ return { state, mtime: statSync(full).mtimeMs };
2449
+ })
2450
+ .filter(x => x.state?.sessionId)
2451
+ .sort((a, b) => (b.state.timestamp || b.mtime || 0) - (a.state.timestamp || a.mtime || 0));
2452
+ return files[0]?.state || null;
2453
+ } catch { return null; }
2454
+ }
2455
+
2456
+ function sessionFromState(state, sessions = []) {
2457
+ if (!state?.sessionId) return null;
2458
+ return sessions.find(s => s.id === state.sessionId) || {
2459
+ id: state.sessionId,
2460
+ tool: state.tool || state.provider || 'claude',
2461
+ smartName: state.sessionName || state.conversationId || state.sessionId,
2462
+ };
2463
+ }
2464
+
2465
+ function resolveCurrentConversationSession(cwd, sessions = [], { allowLatestTerminal = true } = {}) {
2466
+ const terminalId = getTerminalId();
2467
+ const active = readActiveConversation(cwd);
2468
+ if (active?.terminalId === terminalId) {
2469
+ return {
2470
+ session: sessionFromState({
2471
+ sessionId: active.sessionId,
2472
+ tool: active.provider,
2473
+ sessionName: active.sessionName,
2474
+ conversationId: active.conversationId,
2475
+ }, sessions),
2476
+ terminalId,
2477
+ source: 'active-terminal',
2478
+ active,
2479
+ };
2480
+ }
2481
+
2482
+ const terminalState = loadTerminalState(cwd, terminalId);
2483
+ if (terminalState?.sessionId) {
2484
+ return {
2485
+ session: sessionFromState(terminalState, sessions),
2486
+ terminalId,
2487
+ source: 'terminal-state',
2488
+ active: null,
2489
+ };
2490
+ }
2491
+
2492
+ if (allowLatestTerminal) {
2493
+ const latestState = loadLatestTerminalState(cwd);
2494
+ if (latestState?.sessionId) {
2495
+ return {
2496
+ session: sessionFromState(latestState, sessions),
2497
+ terminalId: latestState.terminalId || null,
2498
+ source: 'latest-terminal-state',
2499
+ active: null,
2500
+ };
2501
+ }
2502
+ }
2503
+
2504
+ if (active?.sessionId) {
2505
+ return {
2506
+ session: sessionFromState({
2507
+ sessionId: active.sessionId,
2508
+ tool: active.provider,
2509
+ sessionName: active.sessionName,
2510
+ conversationId: active.conversationId,
2511
+ }, sessions),
2512
+ terminalId: active.terminalId || null,
2513
+ source: 'active-other-terminal',
2514
+ active,
2515
+ };
2516
+ }
2517
+
2518
+ return { session: null, terminalId, source: 'none', active: null };
2519
+ }
2520
+
2431
2521
  function sessionSettingsPath(cwd, terminalId = getTerminalId()) {
2432
2522
  return join(cwd, '.dualbrain', `session-settings-${terminalId}.json`);
2433
2523
  }
@@ -2597,15 +2687,7 @@ function parseProviderSwitchCommand(input) {
2597
2687
  }
2598
2688
 
2599
2689
  function getActionSession(cwd, recentSessions = []) {
2600
- const active = readActiveConversation(cwd);
2601
- if (active) {
2602
- return recentSessions.find(s => s.id === active.sessionId) || {
2603
- id: active.sessionId,
2604
- tool: active.provider,
2605
- smartName: active.sessionName || active.conversationId || active.sessionId,
2606
- };
2607
- }
2608
- return recentSessions[0] || null;
2690
+ return resolveCurrentConversationSession(cwd, recentSessions).session;
2609
2691
  }
2610
2692
 
2611
2693
  async function switchProviderNow(cwd, session, target = null) {
@@ -2781,13 +2863,46 @@ function readActiveConversation(cwd) {
2781
2863
  } catch { return null; }
2782
2864
  }
2783
2865
 
2866
+ function inferRunningHeadConfig(cwd, sessionId, provider) {
2867
+ const active = readActiveConversation(cwd);
2868
+ if (!active?.childPid || (sessionId && active.sessionId !== sessionId)) return {};
2869
+ const fallback = {
2870
+ provider: active.provider || provider,
2871
+ model: _modelMatchesProvider(active.model, provider) ? active.model : null,
2872
+ effort: active.effort || null,
2873
+ };
2874
+ try {
2875
+ const args = execSync(`ps -o args= -p ${Number(active.childPid)}`, {
2876
+ cwd,
2877
+ encoding: 'utf8',
2878
+ timeout: 1000,
2879
+ stdio: ['pipe', 'pipe', 'pipe'],
2880
+ }).trim();
2881
+ if (!args) return {};
2882
+ const parts = args.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(s => s.replace(/^"|"$/g, '')) || [];
2883
+ const modelIdx = parts.indexOf('--model');
2884
+ const effortIdx = parts.indexOf('--effort');
2885
+ const model = modelIdx !== -1 ? parts[modelIdx + 1] : null;
2886
+ const effort = effortIdx !== -1 ? parts[effortIdx + 1] : null;
2887
+ return {
2888
+ provider: active.provider || provider,
2889
+ model: _modelMatchesProvider(model, provider) ? model : fallback.model,
2890
+ effort: effort || fallback.effort || null,
2891
+ };
2892
+ } catch { return fallback; }
2893
+ }
2894
+
2784
2895
  async function processPendingRuntimeSwitch(cwd) {
2785
2896
  const pending = readPendingRuntimeSwitch(cwd);
2786
2897
  if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
2787
2898
 
2788
2899
  let sessions = [];
2789
2900
  try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
2790
- const sess = sessions.find(s => s.id === pending.sessionId) || sessions[0];
2901
+ const sess = sessions.find(s => s.id === pending.sessionId) || {
2902
+ id: pending.sessionId,
2903
+ tool: pending.fromProvider || pending.provider || 'claude',
2904
+ smartName: pending.sessionName || pending.sessionId,
2905
+ };
2791
2906
  if (!sess) {
2792
2907
  clearPendingRuntimeSwitch(cwd);
2793
2908
  return false;
@@ -2811,7 +2926,12 @@ async function processPendingRuntimeSwitch(cwd) {
2811
2926
 
2812
2927
  const launchArgs = runtimeLaunchArgsForPending(sess, cwd, pending);
2813
2928
  process.stdout.write(` Launching: ${targetTool} ${launchArgs.join(' ')}\n\n`);
2814
- writeActiveConversation(cwd, sess, targetTool);
2929
+ writeActiveConversation(cwd, sess, targetTool, {
2930
+ model: pending.model || null,
2931
+ effort: pending.effort || null,
2932
+ automode: pending.automode === true,
2933
+ bypassPermissions: pending.bypassPermissions === true,
2934
+ });
2815
2935
  try {
2816
2936
  await launchSupervisedHead(targetTool, launchArgs, cwd, sess);
2817
2937
  } finally {
@@ -2831,6 +2951,10 @@ function writeActiveConversation(cwd, session, tool, extra = {}) {
2831
2951
  terminalId,
2832
2952
  ownerPid: process.pid,
2833
2953
  childPid: extra.childPid || null,
2954
+ model: extra.model || null,
2955
+ effort: extra.effort || null,
2956
+ automode: extra.automode === true,
2957
+ bypassPermissions: extra.bypassPermissions === true,
2834
2958
  startedAt: new Date().toISOString(),
2835
2959
  lastHeartbeat: new Date().toISOString(),
2836
2960
  mode: 'active-head',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.3.33",
3
+ "version": "0.3.35",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {