dual-brain 0.3.33 → 0.3.34

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 +140 -31
  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,7 +1549,7 @@ 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
1555
  console.log('');
@@ -1598,16 +1598,10 @@ async function cmdSwitchover(args = []) {
1598
1598
  const cwd = process.cwd();
1599
1599
  let sessions = [];
1600
1600
  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];
1601
+ const resolved = resolveCurrentConversationSession(cwd, sessions);
1602
+ const sess = resolved.session;
1609
1603
  if (!sess) {
1610
- console.log('No resumable session found.');
1604
+ console.log('No current logical conversation found. Resume the intended chat through dual-brain first.');
1611
1605
  return;
1612
1606
  }
1613
1607
 
@@ -2428,6 +2422,87 @@ function loadTerminalState(cwd, terminalId) {
2428
2422
  } catch { return null; }
2429
2423
  }
2430
2424
 
2425
+ function loadLatestTerminalState(cwd) {
2426
+ try {
2427
+ const dir = join(cwd, '.dualbrain');
2428
+ const files = readdirSync(dir)
2429
+ .filter(f => /^terminal-.*\.json$/.test(f))
2430
+ .map(f => {
2431
+ const full = join(dir, f);
2432
+ const state = JSON.parse(readFileSync(full, 'utf8'));
2433
+ return { state, mtime: statSync(full).mtimeMs };
2434
+ })
2435
+ .filter(x => x.state?.sessionId)
2436
+ .sort((a, b) => (b.state.timestamp || b.mtime || 0) - (a.state.timestamp || a.mtime || 0));
2437
+ return files[0]?.state || null;
2438
+ } catch { return null; }
2439
+ }
2440
+
2441
+ function sessionFromState(state, sessions = []) {
2442
+ if (!state?.sessionId) return null;
2443
+ return sessions.find(s => s.id === state.sessionId) || {
2444
+ id: state.sessionId,
2445
+ tool: state.tool || state.provider || 'claude',
2446
+ smartName: state.sessionName || state.conversationId || state.sessionId,
2447
+ };
2448
+ }
2449
+
2450
+ function resolveCurrentConversationSession(cwd, sessions = [], { allowLatestTerminal = true } = {}) {
2451
+ const terminalId = getTerminalId();
2452
+ const active = readActiveConversation(cwd);
2453
+ if (active?.terminalId === terminalId) {
2454
+ return {
2455
+ session: sessionFromState({
2456
+ sessionId: active.sessionId,
2457
+ tool: active.provider,
2458
+ sessionName: active.sessionName,
2459
+ conversationId: active.conversationId,
2460
+ }, sessions),
2461
+ terminalId,
2462
+ source: 'active-terminal',
2463
+ active,
2464
+ };
2465
+ }
2466
+
2467
+ const terminalState = loadTerminalState(cwd, terminalId);
2468
+ if (terminalState?.sessionId) {
2469
+ return {
2470
+ session: sessionFromState(terminalState, sessions),
2471
+ terminalId,
2472
+ source: 'terminal-state',
2473
+ active: null,
2474
+ };
2475
+ }
2476
+
2477
+ if (allowLatestTerminal) {
2478
+ const latestState = loadLatestTerminalState(cwd);
2479
+ if (latestState?.sessionId) {
2480
+ return {
2481
+ session: sessionFromState(latestState, sessions),
2482
+ terminalId: latestState.terminalId || null,
2483
+ source: 'latest-terminal-state',
2484
+ active: null,
2485
+ };
2486
+ }
2487
+ }
2488
+
2489
+ if (active?.sessionId) {
2490
+ return {
2491
+ session: sessionFromState({
2492
+ sessionId: active.sessionId,
2493
+ tool: active.provider,
2494
+ sessionName: active.sessionName,
2495
+ conversationId: active.conversationId,
2496
+ }, sessions),
2497
+ terminalId: active.terminalId || null,
2498
+ source: 'active-other-terminal',
2499
+ active,
2500
+ };
2501
+ }
2502
+
2503
+ return { session: null, terminalId, source: 'none', active: null };
2504
+ }
2505
+
2431
2506
  function sessionSettingsPath(cwd, terminalId = getTerminalId()) {
2432
2507
  return join(cwd, '.dualbrain', `session-settings-${terminalId}.json`);
2433
2508
  }
@@ -2597,15 +2672,7 @@ function parseProviderSwitchCommand(input) {
2597
2672
  }
2598
2673
 
2599
2674
  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;
2675
+ return resolveCurrentConversationSession(cwd, recentSessions).session;
2609
2676
  }
2610
2677
 
2611
2678
  async function switchProviderNow(cwd, session, target = null) {
@@ -2781,13 +2848,46 @@ function readActiveConversation(cwd) {
2781
2848
  } catch { return null; }
2782
2849
  }
2783
2850
 
2851
+ function inferRunningHeadConfig(cwd, sessionId, provider) {
2852
+ const active = readActiveConversation(cwd);
2853
+ if (!active?.childPid || (sessionId && active.sessionId !== sessionId)) return {};
2854
+ const fallback = {
2855
+ provider: active.provider || provider,
2856
+ model: _modelMatchesProvider(active.model, provider) ? active.model : null,
2857
+ effort: active.effort || null,
2858
+ };
2859
+ try {
2860
+ const args = execSync(`ps -o args= -p ${Number(active.childPid)}`, {
2861
+ cwd,
2862
+ encoding: 'utf8',
2863
+ timeout: 1000,
2864
+ stdio: ['pipe', 'pipe', 'pipe'],
2865
+ }).trim();
2866
+ if (!args) return {};
2867
+ const parts = args.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(s => s.replace(/^"|"$/g, '')) || [];
2868
+ const modelIdx = parts.indexOf('--model');
2869
+ const effortIdx = parts.indexOf('--effort');
2870
+ const model = modelIdx !== -1 ? parts[modelIdx + 1] : null;
2871
+ const effort = effortIdx !== -1 ? parts[effortIdx + 1] : null;
2872
+ return {
2873
+ provider: active.provider || provider,
2874
+ model: _modelMatchesProvider(model, provider) ? model : fallback.model,
2875
+ effort: effort || fallback.effort || null,
2876
+ };
2877
+ } catch { return fallback; }
2878
+ }
2879
+
2784
2880
  async function processPendingRuntimeSwitch(cwd) {
2785
2881
  const pending = readPendingRuntimeSwitch(cwd);
2786
2882
  if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
2787
2883
 
2788
2884
  let sessions = [];
2789
2885
  try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
2790
- const sess = sessions.find(s => s.id === pending.sessionId) || sessions[0];
2886
+ const sess = sessions.find(s => s.id === pending.sessionId) || {
2887
+ id: pending.sessionId,
2888
+ tool: pending.fromProvider || pending.provider || 'claude',
2889
+ smartName: pending.sessionName || pending.sessionId,
2890
+ };
2791
2891
  if (!sess) {
2792
2892
  clearPendingRuntimeSwitch(cwd);
2793
2893
  return false;
@@ -2811,7 +2911,12 @@ async function processPendingRuntimeSwitch(cwd) {
2811
2911
 
2812
2912
  const launchArgs = runtimeLaunchArgsForPending(sess, cwd, pending);
2813
2913
  process.stdout.write(` Launching: ${targetTool} ${launchArgs.join(' ')}\n\n`);
2814
- writeActiveConversation(cwd, sess, targetTool);
2914
+ writeActiveConversation(cwd, sess, targetTool, {
2915
+ model: pending.model || null,
2916
+ effort: pending.effort || null,
2917
+ automode: pending.automode === true,
2918
+ bypassPermissions: pending.bypassPermissions === true,
2919
+ });
2815
2920
  try {
2816
2921
  await launchSupervisedHead(targetTool, launchArgs, cwd, sess);
2817
2922
  } finally {
@@ -2831,6 +2936,10 @@ function writeActiveConversation(cwd, session, tool, extra = {}) {
2831
2936
  terminalId,
2832
2937
  ownerPid: process.pid,
2833
2938
  childPid: extra.childPid || null,
2939
+ model: extra.model || null,
2940
+ effort: extra.effort || null,
2941
+ automode: extra.automode === true,
2942
+ bypassPermissions: extra.bypassPermissions === true,
2834
2943
  startedAt: new Date().toISOString(),
2835
2944
  lastHeartbeat: new Date().toISOString(),
2836
2945
  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.34",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {