dual-brain 0.2.18 → 0.2.20
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 +8 -5
- package/hooks/head-guard.mjs +6 -10
- package/install.mjs +160 -69
- package/package.json +1 -1
- package/src/cognitive-loop.mjs +2 -0
- package/src/session-lock.mjs +6 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -1151,7 +1151,10 @@ async function installGlobal() {
|
|
|
1151
1151
|
|
|
1152
1152
|
// Resolve absolute path to hooks directory via import.meta.url
|
|
1153
1153
|
const pkgRoot = join(__dirname, '..');
|
|
1154
|
-
|
|
1154
|
+
// Hooks live at hooks/ in the published package, .claude/hooks/ in dev
|
|
1155
|
+
const hooksDir = existsSync(join(pkgRoot, 'hooks', 'head-guard.mjs'))
|
|
1156
|
+
? join(pkgRoot, 'hooks')
|
|
1157
|
+
: join(pkgRoot, '.claude', 'hooks');
|
|
1155
1158
|
|
|
1156
1159
|
// Warn if running from npx (ephemeral path)
|
|
1157
1160
|
if (pkgRoot.includes('.npm/_npx') || pkgRoot.includes('npx-')) {
|
|
@@ -1178,9 +1181,9 @@ async function installGlobal() {
|
|
|
1178
1181
|
})();
|
|
1179
1182
|
|
|
1180
1183
|
if (hasProjectLocalHooks) {
|
|
1181
|
-
console.log(' hooks
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
+
console.log(' project-local hooks detected (will take precedence in this workspace)');
|
|
1185
|
+
}
|
|
1186
|
+
{
|
|
1184
1187
|
// Load existing settings (merge, never clobber)
|
|
1185
1188
|
let existing = {};
|
|
1186
1189
|
if (existsSync(globalSettingsPath)) {
|
|
@@ -4479,7 +4482,7 @@ async function askDefaultShell(cwd, rl, fx) {
|
|
|
4479
4482
|
` ${DIM}modifies${RST} ${YLW}.replit onBoot${RST}`,
|
|
4480
4483
|
` ${DIM}undo${RST} Settings → System → Startup`,
|
|
4481
4484
|
'',
|
|
4482
|
-
` ${CYAN}[
|
|
4485
|
+
` ${CYAN}[Enter]${RST} Start on boot ${DIM}[n] Run manually${RST}`,
|
|
4483
4486
|
];
|
|
4484
4487
|
process.stdout.write('\n' + panel('dual-brain setup', setupContent) + '\n');
|
|
4485
4488
|
|
package/hooks/head-guard.mjs
CHANGED
|
@@ -31,16 +31,12 @@ try {
|
|
|
31
31
|
const raw = readFileSync('/dev/stdin', 'utf8');
|
|
32
32
|
input = JSON.parse(raw);
|
|
33
33
|
} catch {
|
|
34
|
-
// Can't parse input — fail
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
process.stdout.write(JSON.stringify(output));
|
|
43
|
-
process.exit(2);
|
|
34
|
+
// Can't parse input — fail open. This hook's purpose is to block HEAD from
|
|
35
|
+
// implementing directly. If we can't parse stdin (e.g. subagent context where
|
|
36
|
+
// Claude Code doesn't pipe parseable JSON), blocking would incorrectly deny
|
|
37
|
+
// work agents. Allowing is safer: worst case HEAD slips through once, but
|
|
38
|
+
// work agents aren't blocked.
|
|
39
|
+
process.exit(0);
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
const toolName = input.tool_name || '';
|
package/install.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import { dirname, join, resolve } from 'path';
|
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { spawnSync } from 'child_process';
|
|
17
17
|
import { createHash } from 'crypto';
|
|
18
|
+
import { spinner, success as fxSuccess, warn as fxWarn, error as fxError, info as fxInfo, banner, celebrate, colors, sleep, nl, getMode } from './src/fx.mjs';
|
|
19
|
+
import { panel, signalLine, headerBar } from './src/tui.mjs';
|
|
18
20
|
|
|
19
21
|
// Skip hook installation during global npm install — hooks are installed
|
|
20
22
|
// when the user runs 'dual-brain install' in their project directory.
|
|
@@ -103,14 +105,14 @@ if (subcommand && !SUBCOMMANDS.includes(subcommand)) {
|
|
|
103
105
|
process.exit(1);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
// ─── Box Drawing
|
|
108
|
+
// ─── Box Drawing (legacy compat — prefer panel() from tui.mjs) ─────────────
|
|
107
109
|
|
|
108
110
|
const W = 54;
|
|
109
|
-
const
|
|
111
|
+
const pad_legacy = (s, len = W - 2) => {
|
|
110
112
|
s = String(s);
|
|
111
113
|
return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
|
|
112
114
|
};
|
|
113
|
-
const ln = (s) => `║ ${
|
|
115
|
+
const ln = (s) => `║ ${pad_legacy(s)} ║`;
|
|
114
116
|
const br = (l, r) => l + '═'.repeat(W) + r;
|
|
115
117
|
const sep = () => '╠' + '═'.repeat(W) + '╣';
|
|
116
118
|
|
|
@@ -421,10 +423,8 @@ async function authGuidance(env) {
|
|
|
421
423
|
if (env.claude.authed && env.codex.authed) return env;
|
|
422
424
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return env;
|
|
423
425
|
|
|
424
|
-
|
|
425
|
-
console.log('
|
|
426
|
-
console.log(' │ 🔑 Auth Setup │');
|
|
427
|
-
console.log(' └────────────────────────────────────────────┘');
|
|
426
|
+
nl();
|
|
427
|
+
console.log(panel('Auth Setup', ['Checking provider authentication...'], { width: 50 }));
|
|
428
428
|
|
|
429
429
|
if (!env.claude.authed) {
|
|
430
430
|
console.log('');
|
|
@@ -654,27 +654,24 @@ function getAuthState() {
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
function printAuthStatusBox(state) {
|
|
657
|
-
const
|
|
657
|
+
const cl = state.claude;
|
|
658
658
|
const x = state.codex;
|
|
659
|
-
const cIcon = c.authed ? '✅' : c.installed ? '⚠️' : '❌';
|
|
660
|
-
const xIcon = x.authed ? '✅' : x.installed ? '⚠️' : '❌';
|
|
661
659
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
console.log('');
|
|
660
|
+
const lines = [];
|
|
661
|
+
lines.push(signalLine(cl.authed ? 'success' : 'warning', `Claude ${cl.authed ? 'authenticated' : cl.installed ? 'not authenticated' : 'not installed'}`));
|
|
662
|
+
lines.push(` Method: ${cl.method}`);
|
|
663
|
+
lines.push(` Expiry: ${cl.expiryText}`);
|
|
664
|
+
lines.push(` Storage: ${cl.storageText}`);
|
|
665
|
+
lines.push('');
|
|
666
|
+
lines.push(signalLine(x.authed ? 'success' : 'warning', `Codex ${x.authed ? 'authenticated' : x.installed ? 'not authenticated' : 'not installed'}`));
|
|
667
|
+
lines.push(` Method: ${x.method}`);
|
|
668
|
+
lines.push(` Expiry: ${x.expiryText}`);
|
|
669
|
+
lines.push(` Storage: ${x.storageText}`);
|
|
670
|
+
if (x.lastRefresh) lines.push(` Refreshed:${x.lastRefreshText}`);
|
|
671
|
+
|
|
672
|
+
nl();
|
|
673
|
+
console.log(panel('Auth Status', lines, { width: 60 }));
|
|
674
|
+
nl();
|
|
678
675
|
}
|
|
679
676
|
|
|
680
677
|
function runCodexDeviceAuth(codexPath) {
|
|
@@ -913,9 +910,9 @@ function install(workspace, env, mode) {
|
|
|
913
910
|
'gpt-work-dispatcher.mjs', 'profiles.mjs',
|
|
914
911
|
'summary-checkpoint.mjs', 'decision-ledger.mjs', 'control-panel.mjs',
|
|
915
912
|
'risk-classifier.mjs', 'failure-detector.mjs',
|
|
916
|
-
'
|
|
913
|
+
'plan-generator.mjs', 'vibe-memory.mjs',
|
|
917
914
|
'wave-orchestrator.mjs',
|
|
918
|
-
'
|
|
915
|
+
'model-registry.mjs',
|
|
919
916
|
'auto-update-wrapper.mjs',
|
|
920
917
|
'head-guard.mjs',
|
|
921
918
|
];
|
|
@@ -981,36 +978,38 @@ function install(workspace, env, mode) {
|
|
|
981
978
|
|
|
982
979
|
// ─── Status Report ──────────────────────────────────────────────────────────
|
|
983
980
|
|
|
984
|
-
function printReport(env, mode, actions, isDryRun) {
|
|
985
|
-
const
|
|
981
|
+
function printReport(env, mode, actions, isDryRun, { skipBanner = false } = {}) {
|
|
982
|
+
const m = getMode();
|
|
983
|
+
nl();
|
|
986
984
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
lines.push(sep());
|
|
985
|
+
// Gradient banner (skip if already shown in animated detection)
|
|
986
|
+
if (!skipBanner) banner(`v${VERSION}`);
|
|
990
987
|
|
|
991
|
-
|
|
992
|
-
const
|
|
993
|
-
|
|
988
|
+
// Provider status
|
|
989
|
+
const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? 'installed · not authed' : 'not installed';
|
|
990
|
+
const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'installed · not authed' : 'not installed';
|
|
991
|
+
const cHealthy = env.claude.authed;
|
|
992
|
+
const xHealthy = env.codex.authed;
|
|
994
993
|
|
|
994
|
+
const statusLines = [];
|
|
995
|
+
statusLines.push(signalLine(cHealthy ? 'success' : 'warning', `Claude ${cAuth}`));
|
|
996
|
+
statusLines.push(signalLine(xHealthy ? 'success' : 'warning', `Codex ${xAuth}`));
|
|
995
997
|
if (env.isReplit) {
|
|
996
|
-
|
|
998
|
+
statusLines.push(signalLine('info', `Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
|
|
997
999
|
}
|
|
998
1000
|
|
|
999
1001
|
if (actions) {
|
|
1000
|
-
|
|
1001
|
-
for (const a of actions)
|
|
1002
|
-
|
|
1003
|
-
|
|
1002
|
+
statusLines.push('');
|
|
1003
|
+
for (const a of actions) statusLines.push(` ${a}`);
|
|
1004
|
+
statusLines.push('');
|
|
1005
|
+
statusLines.push(signalLine('success', 'Installed — launching session manager...'));
|
|
1004
1006
|
} else if (isDryRun) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
+
statusLines.push('');
|
|
1008
|
+
statusLines.push(signalLine('info', 'Dry run — no files written'));
|
|
1007
1009
|
}
|
|
1008
1010
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
console.log('');
|
|
1012
|
-
for (const l of lines) console.log(` ${l}`);
|
|
1013
|
-
console.log('');
|
|
1011
|
+
console.log(panel('dual-brain status', statusLines, { width: 64 }));
|
|
1012
|
+
nl();
|
|
1014
1013
|
}
|
|
1015
1014
|
|
|
1016
1015
|
// ─── Profile System ────────────────────────────────────────────────────────
|
|
@@ -1223,17 +1222,19 @@ function cmdMode() {
|
|
|
1223
1222
|
const current = loadProfile(workspace);
|
|
1224
1223
|
const PEMOJIS = { auto: '🤖', balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
1225
1224
|
const UI_NAMES = { auto: 'Auto (default)', balanced: 'Balanced', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
1226
|
-
|
|
1227
|
-
console.log(
|
|
1228
|
-
|
|
1225
|
+
nl();
|
|
1226
|
+
console.log(` ${colors.bold}${colors.cyan}Routing modes:${colors.reset}`);
|
|
1227
|
+
nl();
|
|
1229
1228
|
for (const [name, p] of Object.entries(PROFILES)) {
|
|
1230
|
-
const
|
|
1229
|
+
const isActive = name === current.name;
|
|
1231
1230
|
const label = UI_NAMES[name] || name;
|
|
1232
|
-
|
|
1231
|
+
const activeMarker = isActive ? ` ${colors.green}● active${colors.reset}` : '';
|
|
1232
|
+
const style = isActive ? `${colors.cyan}${colors.bold}` : colors.dim;
|
|
1233
|
+
console.log(` ${PEMOJIS[name] || ' '} ${style}${label.padEnd(15)}${colors.reset} ${p.description}${activeMarker}`);
|
|
1233
1234
|
}
|
|
1234
|
-
|
|
1235
|
+
nl();
|
|
1235
1236
|
console.log(` Switch: ${cmd('npx dual-brain mode <name>')}`);
|
|
1236
|
-
|
|
1237
|
+
nl();
|
|
1237
1238
|
return;
|
|
1238
1239
|
}
|
|
1239
1240
|
|
|
@@ -1434,7 +1435,85 @@ async function main() {
|
|
|
1434
1435
|
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
1435
1436
|
if (subcommand === 'explain') { cmdExplain(); return; }
|
|
1436
1437
|
|
|
1437
|
-
|
|
1438
|
+
const mode_fx = getMode();
|
|
1439
|
+
const animate = mode_fx === 'full' || mode_fx === 'subtle';
|
|
1440
|
+
|
|
1441
|
+
// Animated detection phase
|
|
1442
|
+
nl();
|
|
1443
|
+
banner(`v${VERSION}`);
|
|
1444
|
+
|
|
1445
|
+
let env;
|
|
1446
|
+
if (animate) {
|
|
1447
|
+
const sp1 = spinner('Detecting environment...').start();
|
|
1448
|
+
await sleep(300);
|
|
1449
|
+
env = detectEnvironment();
|
|
1450
|
+
|
|
1451
|
+
// Workspace detection
|
|
1452
|
+
if (env.isReplit) {
|
|
1453
|
+
sp1.succeed(`Replit workspace detected${env.hasReplitTools ? ' + replit-tools' : ''}`);
|
|
1454
|
+
} else {
|
|
1455
|
+
sp1.succeed('Local workspace detected');
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Node version
|
|
1459
|
+
const sp2 = spinner('Checking Node.js...').start();
|
|
1460
|
+
await sleep(200);
|
|
1461
|
+
sp2.succeed(`Node ${process.version} found`);
|
|
1462
|
+
|
|
1463
|
+
// Git repo
|
|
1464
|
+
const sp3 = spinner('Checking git...').start();
|
|
1465
|
+
await sleep(200);
|
|
1466
|
+
const gitResult = run('git', ['rev-parse', '--show-toplevel']);
|
|
1467
|
+
const gitBranch = run('git', ['branch', '--show-current']);
|
|
1468
|
+
if (gitResult.status === 0) {
|
|
1469
|
+
const repoName = gitResult.stdout.trim().split('/').pop();
|
|
1470
|
+
const branch = gitBranch.stdout?.trim() || 'unknown';
|
|
1471
|
+
sp3.succeed(`Git repository: ${repoName} (${branch})`);
|
|
1472
|
+
} else {
|
|
1473
|
+
sp3.warn('Not a git repository');
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Claude CLI
|
|
1477
|
+
const sp4 = spinner('Checking Claude CLI...').start();
|
|
1478
|
+
await sleep(300);
|
|
1479
|
+
if (env.claude.installed) {
|
|
1480
|
+
const authLabel = env.claude.authed ? 'CLI OAuth' : 'not authenticated';
|
|
1481
|
+
sp4.succeed(`Claude CLI found · ${authLabel}`);
|
|
1482
|
+
} else {
|
|
1483
|
+
sp4.warn('Claude CLI not found');
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Codex CLI
|
|
1487
|
+
const sp5 = spinner('Checking Codex CLI...').start();
|
|
1488
|
+
await sleep(300);
|
|
1489
|
+
if (env.codex.installed) {
|
|
1490
|
+
const authLabel = env.codex.authed ? 'authenticated' : 'not authenticated';
|
|
1491
|
+
sp5.succeed(`OpenAI Codex CLI found · ${authLabel}`);
|
|
1492
|
+
} else {
|
|
1493
|
+
sp5.warn('Codex CLI not found');
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Sessions
|
|
1497
|
+
const sp6 = spinner('Checking sessions...').start();
|
|
1498
|
+
await sleep(200);
|
|
1499
|
+
const sessionsDir = resolve(process.cwd(), '.replit-tools', '.claude-persistent');
|
|
1500
|
+
if (existsSync(sessionsDir)) {
|
|
1501
|
+
sp6.succeed('Session persistence via replit-tools');
|
|
1502
|
+
} else {
|
|
1503
|
+
sp6.succeed('Standard session management');
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
nl();
|
|
1507
|
+
} else {
|
|
1508
|
+
// Non-animated fallback for CI/plain
|
|
1509
|
+
env = detectEnvironment();
|
|
1510
|
+
fxInfo(`Workspace: ${env.isReplit ? 'Replit' : 'local'}`);
|
|
1511
|
+
fxInfo(`Node ${process.version}`);
|
|
1512
|
+
fxInfo(`Claude: ${env.claude.installed ? (env.claude.authed ? 'authed' : 'installed') : 'missing'}`);
|
|
1513
|
+
fxInfo(`Codex: ${env.codex.installed ? (env.codex.authed ? 'authed' : 'installed') : 'missing'}`);
|
|
1514
|
+
nl();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1438
1517
|
const startupUpdateInfo = (subcommand === 'update' || dryRun || jsonOut)
|
|
1439
1518
|
? null
|
|
1440
1519
|
: checkForUpdate(env.workspace);
|
|
@@ -1445,9 +1524,9 @@ async function main() {
|
|
|
1445
1524
|
process.stdout.isTTY &&
|
|
1446
1525
|
!process.env.CI
|
|
1447
1526
|
) {
|
|
1448
|
-
|
|
1527
|
+
nl();
|
|
1449
1528
|
const shouldUpdate = await promptForUpdate(startupUpdateInfo);
|
|
1450
|
-
|
|
1529
|
+
nl();
|
|
1451
1530
|
if (shouldUpdate) {
|
|
1452
1531
|
env = healClaudeAuth(env);
|
|
1453
1532
|
env = healCodexAuth(env);
|
|
@@ -1477,29 +1556,38 @@ async function main() {
|
|
|
1477
1556
|
if (jsonOut) {
|
|
1478
1557
|
console.log(JSON.stringify({ version: VERSION, env, mode }, null, 2));
|
|
1479
1558
|
} else {
|
|
1480
|
-
printReport(env, mode, null, true);
|
|
1559
|
+
printReport(env, mode, null, true, { skipBanner: true });
|
|
1481
1560
|
}
|
|
1482
1561
|
process.exit(0);
|
|
1483
1562
|
}
|
|
1484
1563
|
|
|
1485
1564
|
if (subcommand === 'update') {
|
|
1486
1565
|
const actions = performUpdate(env.workspace, env, mode);
|
|
1487
|
-
printReport(env, mode, actions);
|
|
1566
|
+
printReport(env, mode, actions, false, { skipBanner: true });
|
|
1488
1567
|
process.exit(0);
|
|
1489
1568
|
}
|
|
1490
1569
|
|
|
1491
1570
|
// Check for replit-tools on Replit
|
|
1492
1571
|
if (env.isReplit && !env.hasReplitTools) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1572
|
+
nl();
|
|
1573
|
+
fxWarn('replit-tools not found — recommended for Replit environments.');
|
|
1574
|
+
fxInfo('Dual-brain works best alongside replit-tools for persistent auth,');
|
|
1575
|
+
fxInfo('session management, and shell integration.');
|
|
1576
|
+
nl();
|
|
1577
|
+
fxInfo(`Install: ${cmd('npx -y data-tools')}`);
|
|
1578
|
+
nl();
|
|
1500
1579
|
}
|
|
1501
1580
|
|
|
1502
|
-
|
|
1581
|
+
// Install hooks and config
|
|
1582
|
+
let actions;
|
|
1583
|
+
if (animate) {
|
|
1584
|
+
const spInstall = spinner('Installing dual-brain hooks...').start();
|
|
1585
|
+
await sleep(400);
|
|
1586
|
+
actions = install(env.workspace, env, mode);
|
|
1587
|
+
spInstall.succeed(`Installed ${actions.length} components`);
|
|
1588
|
+
} else {
|
|
1589
|
+
actions = install(env.workspace, env, mode);
|
|
1590
|
+
}
|
|
1503
1591
|
|
|
1504
1592
|
// Write a standalone shell-hook.sh so users can source it from .bashrc.
|
|
1505
1593
|
// Non-interactive installs (npm postinstall) just print the hint; interactive
|
|
@@ -1530,7 +1618,10 @@ async function main() {
|
|
|
1530
1618
|
}
|
|
1531
1619
|
}
|
|
1532
1620
|
|
|
1533
|
-
|
|
1621
|
+
nl();
|
|
1622
|
+
await celebrate('Setup complete');
|
|
1623
|
+
nl();
|
|
1624
|
+
printReport(env, mode, actions, false, { skipBanner: true });
|
|
1534
1625
|
|
|
1535
1626
|
// After install, launch the session manager (interactive TTY only)
|
|
1536
1627
|
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
package/package.json
CHANGED
package/src/cognitive-loop.mjs
CHANGED
|
@@ -197,6 +197,7 @@ export function enter(userMessage, context = {}) {
|
|
|
197
197
|
plan,
|
|
198
198
|
nextDispatch: prepared,
|
|
199
199
|
suggestion: prepared.blockers[0],
|
|
200
|
+
mode,
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
|
|
@@ -209,6 +210,7 @@ export function enter(userMessage, context = {}) {
|
|
|
209
210
|
plan,
|
|
210
211
|
nextDispatch: prepared,
|
|
211
212
|
estimatedCost: plan.estimatedCost,
|
|
213
|
+
mode,
|
|
212
214
|
};
|
|
213
215
|
}
|
|
214
216
|
|
package/src/session-lock.mjs
CHANGED
|
@@ -46,6 +46,12 @@ export function acquire({ force = false } = {}) {
|
|
|
46
46
|
return { acquired: true, sessionId: _sessionId, existingSession: null, mode: 'primary' };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Same process (re-entry within same session) — always grant
|
|
50
|
+
if (existing.pid === process.pid) {
|
|
51
|
+
_sessionId = existing.sessionId;
|
|
52
|
+
return { acquired: true, sessionId: _sessionId, existingSession: null, mode: 'primary' };
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
const age = Date.now() - existing.heartbeat;
|
|
50
56
|
|
|
51
57
|
if (age > STALE_THRESHOLD_MS || force) {
|