@lightcone-ai/daemon 0.9.62 → 0.9.63
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/package.json +1 -1
- package/src/agent-manager.js +4 -3
- package/src/browser-login.js +30 -13
- package/src/index.js +16 -2
- package/src/profile-lock.js +39 -3
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import { buildSystemPrompt } from './drivers/claude.js';
|
|
6
6
|
import { buildCodexSpawn, buildCodexSystemPrompt, parseCodexLine } from './drivers/codex.js';
|
|
7
7
|
import { buildKimiSpawn, buildKimiInitMessages, parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
|
|
8
|
-
import { startSession, stopSession } from './browser-login.js';
|
|
8
|
+
import { startSession, stopSession, stopAllSessions } from './browser-login.js';
|
|
9
9
|
import { buildSkillMcpServers } from './mcp-config.js';
|
|
10
10
|
|
|
11
11
|
export class AgentManager {
|
|
@@ -35,11 +35,12 @@ export class AgentManager {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
stopAll() {
|
|
38
|
+
async stopAll() {
|
|
39
39
|
for (const [, agent] of this.agents) {
|
|
40
40
|
if (agent.proc) agent.proc.kill();
|
|
41
41
|
}
|
|
42
42
|
this.agents.clear();
|
|
43
|
+
await stopAllSessions();
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// ── private ───────────────────────────────────────────────────────────────
|
|
@@ -355,7 +356,7 @@ export class AgentManager {
|
|
|
355
356
|
_stopBrowserLogin(msg) {
|
|
356
357
|
const platform = msg.platform ?? 'xhs';
|
|
357
358
|
console.log(`[AgentManager] Stopping browser login for platform=${platform}`);
|
|
358
|
-
stopSession(platform);
|
|
359
|
+
return stopSession(platform);
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
_stopAgent(agentId, teamId, connection) {
|
package/src/browser-login.js
CHANGED
|
@@ -135,6 +135,7 @@ export class BrowserLoginSession {
|
|
|
135
135
|
this._loginCheckTimer = null;
|
|
136
136
|
this._profileDir = null;
|
|
137
137
|
this._profileLock = null;
|
|
138
|
+
this._closing = null;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
async start(connection, userId) {
|
|
@@ -344,7 +345,7 @@ export class BrowserLoginSession {
|
|
|
344
345
|
console.error(`[BrowserLogin][${this._platform}] Failed to save cookies: ${err.message}`);
|
|
345
346
|
}
|
|
346
347
|
connection.send({ type: 'browser:login_complete', platform: this._platform, profileDir: this._profileDir });
|
|
347
|
-
this.close();
|
|
348
|
+
await this.close();
|
|
348
349
|
}
|
|
349
350
|
} catch (err) {
|
|
350
351
|
console.error(`[BrowserLogin][${this._platform}] Login check error:`, err.message);
|
|
@@ -367,13 +368,17 @@ export class BrowserLoginSession {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
async close() {
|
|
370
|
-
this.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
371
|
+
if (this._closing) return this._closing;
|
|
372
|
+
this._closing = (async () => {
|
|
373
|
+
this._stopTimers();
|
|
374
|
+
// Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
|
|
375
|
+
try { await this.send('Browser.close', {}, 3000); } catch {}
|
|
376
|
+
await sleep(1000);
|
|
377
|
+
if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
|
|
378
|
+
if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
|
|
379
|
+
if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
|
|
380
|
+
})();
|
|
381
|
+
return this._closing;
|
|
377
382
|
}
|
|
378
383
|
}
|
|
379
384
|
|
|
@@ -385,14 +390,26 @@ export function getSession(platform) { return _sessions.get(platform) ?? null; }
|
|
|
385
390
|
|
|
386
391
|
export async function startSession(platform, connection, userId) {
|
|
387
392
|
const existing = _sessions.get(platform);
|
|
388
|
-
if (existing) { existing.close(); _sessions.delete(platform); }
|
|
393
|
+
if (existing) { await existing.close(); _sessions.delete(platform); }
|
|
389
394
|
const session = new BrowserLoginSession(platform);
|
|
390
395
|
_sessions.set(platform, session);
|
|
391
|
-
|
|
392
|
-
|
|
396
|
+
try {
|
|
397
|
+
await session.start(connection, userId);
|
|
398
|
+
return session;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
_sessions.delete(platform);
|
|
401
|
+
await session.close();
|
|
402
|
+
throw err;
|
|
403
|
+
}
|
|
393
404
|
}
|
|
394
405
|
|
|
395
|
-
export function stopSession(platform) {
|
|
406
|
+
export async function stopSession(platform) {
|
|
396
407
|
const session = _sessions.get(platform);
|
|
397
|
-
if (session) { session.close(); _sessions.delete(platform); }
|
|
408
|
+
if (session) { await session.close(); _sessions.delete(platform); }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function stopAllSessions() {
|
|
412
|
+
const sessions = [..._sessions.values()];
|
|
413
|
+
_sessions.clear();
|
|
414
|
+
await Promise.allSettled(sessions.map(session => session.close()));
|
|
398
415
|
}
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import 'dotenv/config';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { DaemonConnection } from './connection.js';
|
|
5
5
|
import { AgentManager } from './agent-manager.js';
|
|
6
|
+
import { releaseProfileLocksForProcess } from './profile-lock.js';
|
|
6
7
|
|
|
7
8
|
const { version } = createRequire(import.meta.url)('../package.json');
|
|
8
9
|
|
|
@@ -42,5 +43,18 @@ const connection = new DaemonConnection({
|
|
|
42
43
|
|
|
43
44
|
connection.connect();
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
let shuttingDown = false;
|
|
47
|
+
async function shutdown(signal) {
|
|
48
|
+
if (shuttingDown) return;
|
|
49
|
+
shuttingDown = true;
|
|
50
|
+
console.log(`[Daemon] Shutting down (${signal})`);
|
|
51
|
+
connection.stop();
|
|
52
|
+
try { await agentManager.stopAll(); } catch (err) { console.error('[Daemon] Shutdown error:', err.message); }
|
|
53
|
+
releaseProfileLocksForProcess();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.on('SIGINT', () => { shutdown('SIGINT'); });
|
|
58
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM'); });
|
|
59
|
+
process.on('SIGHUP', () => { shutdown('SIGHUP'); });
|
|
60
|
+
process.on('exit', () => { releaseProfileLocksForProcess(); });
|
package/src/profile-lock.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync, statSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
|
|
5
6
|
const LOCK_ROOT = path.join(homedir(), '.lightcone', 'profile-locks');
|
|
6
7
|
const DEFAULT_STALE_MS = 20 * 60 * 1000;
|
|
@@ -37,6 +38,28 @@ function isStale(dir, staleMs) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function pidIsAlive(pid) {
|
|
42
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return true;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return err.code === 'EPERM';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAbandoned(dir) {
|
|
52
|
+
const info = lockInfo(dir);
|
|
53
|
+
if (!info?.pid) return false;
|
|
54
|
+
return !pidIsAlive(info.pid);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeLockDir(dir) {
|
|
58
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const heldLocks = new Map();
|
|
62
|
+
|
|
40
63
|
export async function acquireProfileLock(platform, profileDir, {
|
|
41
64
|
owner = 'unknown',
|
|
42
65
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
@@ -50,21 +73,26 @@ export async function acquireProfileLock(platform, profileDir, {
|
|
|
50
73
|
while (true) {
|
|
51
74
|
try {
|
|
52
75
|
mkdirSync(dir);
|
|
76
|
+
const token = randomUUID();
|
|
53
77
|
const release = () => {
|
|
54
|
-
|
|
78
|
+
const info = lockInfo(dir);
|
|
79
|
+
heldLocks.delete(dir);
|
|
80
|
+
if (!info || info.token === token || info.pid === process.pid) removeLockDir(dir);
|
|
55
81
|
};
|
|
56
82
|
writeFileSync(path.join(dir, 'owner.json'), JSON.stringify({
|
|
57
83
|
platform,
|
|
58
84
|
profileDir,
|
|
59
85
|
owner,
|
|
60
86
|
pid: process.pid,
|
|
87
|
+
token,
|
|
61
88
|
createdAt: new Date().toISOString(),
|
|
62
89
|
}, null, 2));
|
|
90
|
+
heldLocks.set(dir, token);
|
|
63
91
|
return { release, dir };
|
|
64
92
|
} catch (err) {
|
|
65
93
|
if (err.code !== 'EEXIST') throw err;
|
|
66
|
-
if (existsSync(dir) && isStale(dir, staleMs)) {
|
|
67
|
-
|
|
94
|
+
if (existsSync(dir) && (isAbandoned(dir) || isStale(dir, staleMs))) {
|
|
95
|
+
removeLockDir(dir);
|
|
68
96
|
continue;
|
|
69
97
|
}
|
|
70
98
|
if (Date.now() >= deadline) {
|
|
@@ -76,6 +104,14 @@ export async function acquireProfileLock(platform, profileDir, {
|
|
|
76
104
|
}
|
|
77
105
|
}
|
|
78
106
|
|
|
107
|
+
export function releaseProfileLocksForProcess() {
|
|
108
|
+
for (const [dir, token] of heldLocks) {
|
|
109
|
+
const info = lockInfo(dir);
|
|
110
|
+
if (!info || info.token === token || info.pid === process.pid) removeLockDir(dir);
|
|
111
|
+
heldLocks.delete(dir);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
79
115
|
export async function withProfileLock(platform, profileDir, options, fn) {
|
|
80
116
|
const lock = await acquireProfileLock(platform, profileDir, options);
|
|
81
117
|
try {
|