dual-brain 0.3.32 → 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.
- package/bin/dual-brain.mjs +161 -31
- 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,7 +1549,7 @@ 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
1555
|
console.log('');
|
|
@@ -1564,6 +1564,7 @@ async function cmdRuntimeSwitch(args = []) {
|
|
|
1564
1564
|
const activeForSwitch = readActiveConversation(cwd);
|
|
1565
1565
|
if (activeForSwitch?.sessionId === sess.id) {
|
|
1566
1566
|
console.log('Active supervisor detected: HEAD will restart with these settings.');
|
|
1567
|
+
signalActiveConversation(cwd, sess.id);
|
|
1567
1568
|
} else if (!args.includes('--apply')) {
|
|
1568
1569
|
console.log('No active supervisor detected: saved for the next dual-brain resume/switch, not live yet.');
|
|
1569
1570
|
}
|
|
@@ -1597,16 +1598,10 @@ async function cmdSwitchover(args = []) {
|
|
|
1597
1598
|
const cwd = process.cwd();
|
|
1598
1599
|
let sessions = [];
|
|
1599
1600
|
try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
|
|
1600
|
-
const
|
|
1601
|
-
const sess =
|
|
1602
|
-
? (sessions.find(s => s.id === active.sessionId) || {
|
|
1603
|
-
id: active.sessionId,
|
|
1604
|
-
tool: active.provider,
|
|
1605
|
-
smartName: active.sessionName || active.conversationId || active.sessionId,
|
|
1606
|
-
})
|
|
1607
|
-
: sessions[0];
|
|
1601
|
+
const resolved = resolveCurrentConversationSession(cwd, sessions);
|
|
1602
|
+
const sess = resolved.session;
|
|
1608
1603
|
if (!sess) {
|
|
1609
|
-
console.log('No
|
|
1604
|
+
console.log('No current logical conversation found. Resume the intended chat through dual-brain first.');
|
|
1610
1605
|
return;
|
|
1611
1606
|
}
|
|
1612
1607
|
|
|
@@ -2427,6 +2422,87 @@ function loadTerminalState(cwd, terminalId) {
|
|
|
2427
2422
|
} catch { return null; }
|
|
2428
2423
|
}
|
|
2429
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
|
+
|
|
2430
2506
|
function sessionSettingsPath(cwd, terminalId = getTerminalId()) {
|
|
2431
2507
|
return join(cwd, '.dualbrain', `session-settings-${terminalId}.json`);
|
|
2432
2508
|
}
|
|
@@ -2596,15 +2672,7 @@ function parseProviderSwitchCommand(input) {
|
|
|
2596
2672
|
}
|
|
2597
2673
|
|
|
2598
2674
|
function getActionSession(cwd, recentSessions = []) {
|
|
2599
|
-
|
|
2600
|
-
if (active) {
|
|
2601
|
-
return recentSessions.find(s => s.id === active.sessionId) || {
|
|
2602
|
-
id: active.sessionId,
|
|
2603
|
-
tool: active.provider,
|
|
2604
|
-
smartName: active.sessionName || active.conversationId || active.sessionId,
|
|
2605
|
-
};
|
|
2606
|
-
}
|
|
2607
|
-
return recentSessions[0] || null;
|
|
2675
|
+
return resolveCurrentConversationSession(cwd, recentSessions).session;
|
|
2608
2676
|
}
|
|
2609
2677
|
|
|
2610
2678
|
async function switchProviderNow(cwd, session, target = null) {
|
|
@@ -2780,13 +2848,46 @@ function readActiveConversation(cwd) {
|
|
|
2780
2848
|
} catch { return null; }
|
|
2781
2849
|
}
|
|
2782
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
|
+
|
|
2783
2880
|
async function processPendingRuntimeSwitch(cwd) {
|
|
2784
2881
|
const pending = readPendingRuntimeSwitch(cwd);
|
|
2785
2882
|
if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
|
|
2786
2883
|
|
|
2787
2884
|
let sessions = [];
|
|
2788
2885
|
try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
|
|
2789
|
-
const sess = sessions.find(s => s.id === pending.sessionId) ||
|
|
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
|
+
};
|
|
2790
2891
|
if (!sess) {
|
|
2791
2892
|
clearPendingRuntimeSwitch(cwd);
|
|
2792
2893
|
return false;
|
|
@@ -2810,7 +2911,12 @@ async function processPendingRuntimeSwitch(cwd) {
|
|
|
2810
2911
|
|
|
2811
2912
|
const launchArgs = runtimeLaunchArgsForPending(sess, cwd, pending);
|
|
2812
2913
|
process.stdout.write(` Launching: ${targetTool} ${launchArgs.join(' ')}\n\n`);
|
|
2813
|
-
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
|
+
});
|
|
2814
2920
|
try {
|
|
2815
2921
|
await launchSupervisedHead(targetTool, launchArgs, cwd, sess);
|
|
2816
2922
|
} finally {
|
|
@@ -2830,6 +2936,10 @@ function writeActiveConversation(cwd, session, tool, extra = {}) {
|
|
|
2830
2936
|
terminalId,
|
|
2831
2937
|
ownerPid: process.pid,
|
|
2832
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,
|
|
2833
2943
|
startedAt: new Date().toISOString(),
|
|
2834
2944
|
lastHeartbeat: new Date().toISOString(),
|
|
2835
2945
|
mode: 'active-head',
|
|
@@ -2851,6 +2961,18 @@ function updateActiveConversation(cwd, sessionId, updates = {}) {
|
|
|
2851
2961
|
} catch {}
|
|
2852
2962
|
}
|
|
2853
2963
|
|
|
2964
|
+
function signalActiveConversation(cwd, sessionId) {
|
|
2965
|
+
try {
|
|
2966
|
+
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
2967
|
+
if (sessionId && lease.sessionId !== sessionId) return false;
|
|
2968
|
+
if (!pidAlive(lease.ownerPid)) return false;
|
|
2969
|
+
process.kill(lease.ownerPid, 'SIGUSR1');
|
|
2970
|
+
return true;
|
|
2971
|
+
} catch {
|
|
2972
|
+
return false;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2854
2976
|
function writeHandoffConversationLease(cwd, session, fromTool, targetTool, taskBrief = '') {
|
|
2855
2977
|
const dir = join(cwd, '.dualbrain');
|
|
2856
2978
|
const terminalId = getTerminalId();
|
|
@@ -2957,6 +3079,12 @@ async function launchSupervisedHead(tool, launchArgs, cwd, session) {
|
|
|
2957
3079
|
}, 2500);
|
|
2958
3080
|
};
|
|
2959
3081
|
|
|
3082
|
+
const onSignalSwitch = () => {
|
|
3083
|
+
const pending = readPendingRuntimeSwitch(cwd);
|
|
3084
|
+
if (pendingSwitchMatchesSession(pending, session)) stopChildForSwitch();
|
|
3085
|
+
};
|
|
3086
|
+
process.on('SIGUSR1', onSignalSwitch);
|
|
3087
|
+
|
|
2960
3088
|
const watcher = setInterval(() => {
|
|
2961
3089
|
const pending = readPendingRuntimeSwitch(cwd);
|
|
2962
3090
|
if (pendingSwitchMatchesSession(pending, session)) stopChildForSwitch();
|
|
@@ -2964,6 +3092,7 @@ async function launchSupervisedHead(tool, launchArgs, cwd, session) {
|
|
|
2964
3092
|
|
|
2965
3093
|
child.on('exit', async (code, signal) => {
|
|
2966
3094
|
clearInterval(watcher);
|
|
3095
|
+
process.off('SIGUSR1', onSignalSwitch);
|
|
2967
3096
|
if (forceTimer) clearTimeout(forceTimer);
|
|
2968
3097
|
saveTerminalState(cwd, getTerminalId(), session?.id, session?.tool || tool);
|
|
2969
3098
|
|
|
@@ -2979,6 +3108,7 @@ async function launchSupervisedHead(tool, launchArgs, cwd, session) {
|
|
|
2979
3108
|
|
|
2980
3109
|
child.on('error', (err) => {
|
|
2981
3110
|
clearInterval(watcher);
|
|
3111
|
+
process.off('SIGUSR1', onSignalSwitch);
|
|
2982
3112
|
if (forceTimer) clearTimeout(forceTimer);
|
|
2983
3113
|
process.stderr.write(`\n Could not launch ${tool}: ${err.message}\n`);
|
|
2984
3114
|
clearActiveConversation(cwd, session?.id);
|
package/package.json
CHANGED