dual-brain 0.3.26 → 0.3.27
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 -30
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -56,7 +56,26 @@ function _codexResumeArgs(sessionId, cwd) {
|
|
|
56
56
|
return ['--dangerously-bypass-approvals-and-sandbox', 'resume', sessionId];
|
|
57
57
|
}
|
|
58
58
|
const approvalMode = getEffectiveAutomode(loadProfile(workspace), workspace) ? 'never' : 'on-request';
|
|
59
|
-
return [
|
|
59
|
+
return [..._codexApprovalArgs(workspace, approvalMode), 'resume', sessionId];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _isReplitWorkspace(cwd) {
|
|
63
|
+
const workspace = cwd || process.cwd();
|
|
64
|
+
return !!(
|
|
65
|
+
process.env.REPL_ID ||
|
|
66
|
+
process.env.REPL_SLUG ||
|
|
67
|
+
process.env.REPLIT_CLUSTER ||
|
|
68
|
+
existsSync(join(workspace, '.replit')) ||
|
|
69
|
+
existsSync(join(workspace, '.replit-tools'))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _codexApprovalArgs(cwd, approvalMode = 'on-request') {
|
|
74
|
+
// Codex workspace-write depends on bubblewrap/user namespaces. Replit's
|
|
75
|
+
// container frequently blocks that, so use the Replit container boundary and
|
|
76
|
+
// Codex approvals instead of launching a HEAD that cannot run any command.
|
|
77
|
+
const sandboxMode = _isReplitWorkspace(cwd) ? 'danger-full-access' : 'workspace-write';
|
|
78
|
+
return ['--sandbox', sandboxMode, '--ask-for-approval', approvalMode];
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
function _sessionTool(session) {
|
|
@@ -2399,8 +2418,7 @@ function runtimeLaunchArgsForPending(session, cwd, pending) {
|
|
|
2399
2418
|
if (tool === 'codex') {
|
|
2400
2419
|
if (pending?.bypassPermissions) return ['--dangerously-bypass-approvals-and-sandbox', 'resume', session.id];
|
|
2401
2420
|
return [
|
|
2402
|
-
'
|
|
2403
|
-
'--ask-for-approval', pending?.automode ? 'never' : 'on-request',
|
|
2421
|
+
..._codexApprovalArgs(cwd, pending?.automode ? 'never' : 'on-request'),
|
|
2404
2422
|
'resume', session.id,
|
|
2405
2423
|
];
|
|
2406
2424
|
}
|
|
@@ -2446,29 +2464,25 @@ async function processPendingRuntimeSwitch(cwd) {
|
|
|
2446
2464
|
clearPendingRuntimeSwitch(cwd);
|
|
2447
2465
|
|
|
2448
2466
|
if (targetTool !== currentTool) {
|
|
2449
|
-
|
|
2467
|
+
const brief = pending.handoffBrief || _sessionBrief(sess, targetTool);
|
|
2468
|
+
writeHandoffConversationLease(cwd, sess, currentTool, targetTool, brief);
|
|
2450
2469
|
markSessionSuperseded(cwd, sess, targetTool, pending.reason || 'runtime-switch');
|
|
2451
|
-
await
|
|
2470
|
+
await launchSupervisedHandoff(sess, cwd, currentTool, targetTool, brief, pending);
|
|
2452
2471
|
return true;
|
|
2453
2472
|
}
|
|
2454
2473
|
|
|
2455
|
-
const { spawnSync } = await import('node:child_process');
|
|
2456
2474
|
const launchArgs = runtimeLaunchArgsForPending(sess, cwd, pending);
|
|
2457
2475
|
process.stdout.write(` Launching: ${targetTool} ${launchArgs.join(' ')}\n\n`);
|
|
2458
2476
|
writeActiveConversation(cwd, sess, targetTool);
|
|
2459
|
-
const env = targetTool === 'codex' && (!process.env.TERM || process.env.TERM === 'dumb')
|
|
2460
|
-
? { ...process.env, TERM: 'xterm-256color' }
|
|
2461
|
-
: process.env;
|
|
2462
2477
|
try {
|
|
2463
|
-
|
|
2478
|
+
await launchSupervisedHead(targetTool, launchArgs, cwd, sess);
|
|
2464
2479
|
} finally {
|
|
2465
|
-
saveTerminalState(cwd, getTerminalId(), sess.id, sess.tool || targetTool);
|
|
2466
2480
|
clearActiveConversation(cwd, sess.id);
|
|
2467
2481
|
}
|
|
2468
2482
|
return true;
|
|
2469
2483
|
}
|
|
2470
2484
|
|
|
2471
|
-
function writeActiveConversation(cwd, session, tool) {
|
|
2485
|
+
function writeActiveConversation(cwd, session, tool, extra = {}) {
|
|
2472
2486
|
const dir = join(cwd, '.dualbrain');
|
|
2473
2487
|
const terminalId = getTerminalId();
|
|
2474
2488
|
const lease = {
|
|
@@ -2477,6 +2491,7 @@ function writeActiveConversation(cwd, session, tool) {
|
|
|
2477
2491
|
provider: tool,
|
|
2478
2492
|
terminalId,
|
|
2479
2493
|
ownerPid: process.pid,
|
|
2494
|
+
childPid: extra.childPid || null,
|
|
2480
2495
|
startedAt: new Date().toISOString(),
|
|
2481
2496
|
lastHeartbeat: new Date().toISOString(),
|
|
2482
2497
|
mode: 'active-head',
|
|
@@ -2486,6 +2501,18 @@ function writeActiveConversation(cwd, session, tool) {
|
|
|
2486
2501
|
return lease;
|
|
2487
2502
|
}
|
|
2488
2503
|
|
|
2504
|
+
function updateActiveConversation(cwd, sessionId, updates = {}) {
|
|
2505
|
+
try {
|
|
2506
|
+
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
2507
|
+
if (sessionId && lease.sessionId !== sessionId) return;
|
|
2508
|
+
writeFileSync(activeConversationPath(cwd), JSON.stringify({
|
|
2509
|
+
...lease,
|
|
2510
|
+
...updates,
|
|
2511
|
+
lastHeartbeat: new Date().toISOString(),
|
|
2512
|
+
}, null, 2) + '\n');
|
|
2513
|
+
} catch {}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2489
2516
|
function writeHandoffConversationLease(cwd, session, fromTool, targetTool, taskBrief = '') {
|
|
2490
2517
|
const dir = join(cwd, '.dualbrain');
|
|
2491
2518
|
const terminalId = getTerminalId();
|
|
@@ -2562,6 +2589,124 @@ function clearActiveConversation(cwd, sessionId) {
|
|
|
2562
2589
|
} catch {}
|
|
2563
2590
|
}
|
|
2564
2591
|
|
|
2592
|
+
function pendingSwitchMatchesSession(pending, session) {
|
|
2593
|
+
if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
|
|
2594
|
+
if (!session?.id) return true;
|
|
2595
|
+
return pending.sessionId === session.id;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
async function launchSupervisedHead(tool, launchArgs, cwd, session) {
|
|
2599
|
+
const { spawn } = await import('node:child_process');
|
|
2600
|
+
const env = tool === 'codex' && (!process.env.TERM || process.env.TERM === 'dumb')
|
|
2601
|
+
? { ...process.env, TERM: 'xterm-256color' }
|
|
2602
|
+
: process.env;
|
|
2603
|
+
let switching = false;
|
|
2604
|
+
let forceTimer = null;
|
|
2605
|
+
|
|
2606
|
+
return await new Promise((resolve) => {
|
|
2607
|
+
const child = spawn(tool, launchArgs, { stdio: 'inherit', cwd, env });
|
|
2608
|
+
updateActiveConversation(cwd, session?.id, { childPid: child.pid, provider: tool });
|
|
2609
|
+
|
|
2610
|
+
const stopChildForSwitch = () => {
|
|
2611
|
+
if (switching) return;
|
|
2612
|
+
switching = true;
|
|
2613
|
+
process.stdout.write('\n Switch confirmed — restarting HEAD with updated settings...\n');
|
|
2614
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
2615
|
+
forceTimer = setTimeout(() => {
|
|
2616
|
+
try {
|
|
2617
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
2618
|
+
} catch {}
|
|
2619
|
+
}, 2500);
|
|
2620
|
+
};
|
|
2621
|
+
|
|
2622
|
+
const watcher = setInterval(() => {
|
|
2623
|
+
const pending = readPendingRuntimeSwitch(cwd);
|
|
2624
|
+
if (pendingSwitchMatchesSession(pending, session)) stopChildForSwitch();
|
|
2625
|
+
}, 500);
|
|
2626
|
+
|
|
2627
|
+
child.on('exit', async (code, signal) => {
|
|
2628
|
+
clearInterval(watcher);
|
|
2629
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
2630
|
+
saveTerminalState(cwd, getTerminalId(), session?.id, session?.tool || tool);
|
|
2631
|
+
|
|
2632
|
+
if (switching) {
|
|
2633
|
+
const launched = await processPendingRuntimeSwitch(cwd);
|
|
2634
|
+
resolve({ switched: launched, code, signal });
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
clearActiveConversation(cwd, session?.id);
|
|
2639
|
+
resolve({ switched: false, code, signal });
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
child.on('error', (err) => {
|
|
2643
|
+
clearInterval(watcher);
|
|
2644
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
2645
|
+
process.stderr.write(`\n Could not launch ${tool}: ${err.message}\n`);
|
|
2646
|
+
clearActiveConversation(cwd, session?.id);
|
|
2647
|
+
resolve({ switched: false, error: err });
|
|
2648
|
+
});
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
async function launchSupervisedHandoff(session, cwd, currentTool, targetTool, brief, pending = {}) {
|
|
2653
|
+
let autoHandoff;
|
|
2654
|
+
try {
|
|
2655
|
+
autoHandoff = await import('../dist/src/auto-handoff.js');
|
|
2656
|
+
} catch (e) {
|
|
2657
|
+
process.stderr.write(`\n Could not load auto-handoff module: ${e.message}\n`);
|
|
2658
|
+
return { switched: false };
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
const fromProvider = currentTool === 'codex' ? 'openai' : 'anthropic';
|
|
2662
|
+
const result = autoHandoff.executeHandoff({
|
|
2663
|
+
fromProvider,
|
|
2664
|
+
cwd,
|
|
2665
|
+
auto: !!pending.automode,
|
|
2666
|
+
force: true,
|
|
2667
|
+
taskBrief: brief,
|
|
2668
|
+
});
|
|
2669
|
+
if (!result.success || !result.contextFile) {
|
|
2670
|
+
process.stderr.write(`\n ${result.message || 'Handoff failed.'}\n`);
|
|
2671
|
+
return { switched: false };
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
let prompt = '';
|
|
2675
|
+
try {
|
|
2676
|
+
const data = JSON.parse(readFileSync(result.contextFile, 'utf8'));
|
|
2677
|
+
prompt = data?.prompt || '';
|
|
2678
|
+
} catch {}
|
|
2679
|
+
if (!prompt) {
|
|
2680
|
+
process.stderr.write('\n Handoff context was created, but no prompt was available to launch the target provider.\n');
|
|
2681
|
+
return { switched: false };
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
let launchArgs;
|
|
2685
|
+
if (targetTool === 'codex') {
|
|
2686
|
+
launchArgs = pending?.bypassPermissions
|
|
2687
|
+
? ['--dangerously-bypass-approvals-and-sandbox', prompt.slice(0, 4000)]
|
|
2688
|
+
: [
|
|
2689
|
+
..._codexApprovalArgs(cwd, pending?.automode ? 'never' : 'on-request'),
|
|
2690
|
+
prompt.slice(0, 4000),
|
|
2691
|
+
];
|
|
2692
|
+
} else {
|
|
2693
|
+
const permissionArgs = pending?.bypassPermissions
|
|
2694
|
+
? ['--dangerously-skip-permissions']
|
|
2695
|
+
: pending?.automode
|
|
2696
|
+
? ['--permission-mode', 'auto']
|
|
2697
|
+
: [];
|
|
2698
|
+
launchArgs = [...permissionArgs, prompt.slice(0, 4000)];
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
process.stdout.write(` Launching: ${targetTool} ${launchArgs.slice(0, -1).join(' ')} [handoff context]\n\n`);
|
|
2702
|
+
writeActiveConversation(cwd, session, targetTool);
|
|
2703
|
+
try {
|
|
2704
|
+
return await launchSupervisedHead(targetTool, launchArgs, cwd, { ...session, tool: targetTool });
|
|
2705
|
+
} finally {
|
|
2706
|
+
clearActiveConversation(cwd, session?.id);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2565
2710
|
async function confirmConversationTakeover(sess, cwd, ask) {
|
|
2566
2711
|
const active = readActiveConversation(cwd);
|
|
2567
2712
|
if (!active || active.sessionId !== sess.id || active.ownerPid === process.pid) return 'launch';
|
|
@@ -2602,9 +2747,8 @@ async function launchSessionWithLease(sess, cwd, ask = null) {
|
|
|
2602
2747
|
writeActiveConversation(cwd, sess, tool);
|
|
2603
2748
|
process.stdout.write(`\n Launching: ${tool} ${launchArgs.join(' ')}\n\n`);
|
|
2604
2749
|
try {
|
|
2605
|
-
|
|
2750
|
+
await launchSupervisedHead(tool, launchArgs, cwd, sess);
|
|
2606
2751
|
} finally {
|
|
2607
|
-
saveTerminalState(cwd, getTerminalId(), sess.id, sess.tool || tool);
|
|
2608
2752
|
clearActiveConversation(cwd, sess.id);
|
|
2609
2753
|
}
|
|
2610
2754
|
return { next: 'main' };
|
|
@@ -6232,16 +6376,7 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
|
|
|
6232
6376
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
6233
6377
|
|
|
6234
6378
|
if (choice === 'c' || choice === 'r') {
|
|
6235
|
-
|
|
6236
|
-
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
6237
|
-
console.log(`\n Launching: ${tool} ${launchArgs.join(' ')}\n`);
|
|
6238
|
-
try {
|
|
6239
|
-
const { spawnSync } = await import('node:child_process');
|
|
6240
|
-
spawnSync(tool, launchArgs, { stdio: 'inherit' });
|
|
6241
|
-
} catch {
|
|
6242
|
-
console.log(` Could not launch ${tool} CLI.`);
|
|
6243
|
-
}
|
|
6244
|
-
return { next: 'dashboard' };
|
|
6379
|
+
return await launchSessionWithLease(sess, cwd, ask);
|
|
6245
6380
|
}
|
|
6246
6381
|
|
|
6247
6382
|
if (choice === 'g') {
|
|
@@ -6542,12 +6677,7 @@ async function sessionManageScreen(rl, ask, ctx = {}) {
|
|
|
6542
6677
|
}
|
|
6543
6678
|
|
|
6544
6679
|
if (choice === 'o') {
|
|
6545
|
-
|
|
6546
|
-
const tool = _sessionTool(sess);
|
|
6547
|
-
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
6548
|
-
console.log(`\n Launching: ${tool} ${launchArgs.join(' ')}\n`);
|
|
6549
|
-
spawnSync(tool, launchArgs, { stdio: 'inherit' });
|
|
6550
|
-
return { next: 'sessions' };
|
|
6680
|
+
return await launchSessionWithLease(sess, cwd, ask);
|
|
6551
6681
|
}
|
|
6552
6682
|
|
|
6553
6683
|
if (choice === 'g') {
|
package/package.json
CHANGED