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.
- package/bin/dual-brain.mjs +160 -36
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -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
|
|
1466
|
-
const
|
|
1467
|
-
|
|
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
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
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
|
|
1602
|
-
const sess =
|
|
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
|
|
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
|
-
|
|
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) ||
|
|
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