@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.62",
3
+ "version": "0.9.63",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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) {
@@ -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._stopTimers();
371
- // Use CDP Browser.close for graceful shutdown so Chrome flushes cookies to disk
372
- try { await this.send('Browser.close', {}, 3000); } catch {}
373
- await sleep(1000);
374
- if (this._ws) { try { this._ws.close(); } catch {} this._ws = null; }
375
- if (this._proc) { try { this._proc.kill('SIGKILL'); } catch {} this._proc = null; }
376
- if (this._profileLock) { this._profileLock.release(); this._profileLock = null; }
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
- await session.start(connection, userId);
392
- return session;
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
- process.on('SIGINT', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
46
- process.on('SIGTERM', () => { connection.stop(); agentManager.stopAll(); process.exit(0); });
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(); });
@@ -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
- try { rmSync(dir, { recursive: true, force: true }); } catch {}
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
- try { rmSync(dir, { recursive: true, force: true }); } catch {}
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 {