@adhdev/daemon-core 0.9.82-rc.313 → 0.9.82-rc.315

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": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.313",
3
+ "version": "0.9.82-rc.315",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.313",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.315",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -321,42 +321,64 @@ function removeDaemonPidFile(): void {
321
321
  }
322
322
  }
323
323
 
324
- function cleanupStaleGlobalInstallDirs(pkgName: string, surface: CurrentGlobalInstallSurface): void {
325
- const prefixArgs = surface.installPrefix ? ['--prefix', surface.installPrefix] : [];
326
- const npmRoot = String(execNpmCommandSync(['root', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
327
- if (!npmRoot) return;
328
- const npmPrefix = surface.installPrefix
329
- || String(execNpmCommandSync(['prefix', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
330
- const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
331
- const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
332
- const binNames = new Set<string>([packageBaseName]);
333
- if (pkgName === '@adhdev/daemon-standalone') {
334
- binNames.add('adhdev-standalone');
324
+ /**
325
+ * Best-effort removal of a leftover npm staging entry.
326
+ *
327
+ * A stale staging dir can hold a locked native binary — e.g. `ghostty-vt.dll`
328
+ * from `@adhdev/ghostty-vt-node` still mapped by a lingering session-host
329
+ * process which makes `rmSync` throw `EPERM` on Windows. Staging cleanup is
330
+ * only housekeeping: the leftover is inert and npm creates its own fresh
331
+ * staging dir for the real install, so a lock on an old leftover must NOT abort
332
+ * the upgrade. Log and continue instead of letting the error propagate.
333
+ */
334
+ export function safeRemoveStaleEntry(target: string, label: string): void {
335
+ try {
336
+ fs.rmSync(target, { recursive: true, force: true });
337
+ appendUpgradeLog(`${label}: ${target}`);
338
+ } catch (error: any) {
339
+ appendUpgradeLog(`Skipped locked stale entry (${error?.code || 'error'}): ${target} — ${error?.message || String(error)}`);
335
340
  }
341
+ }
336
342
 
337
- if (pkgName.startsWith('@')) {
338
- const [scope, name] = pkgName.split('/');
339
- const scopeDir = path.join(npmRoot, scope);
340
- if (!fs.existsSync(scopeDir)) return;
341
- for (const entry of fs.readdirSync(scopeDir)) {
342
- if (!entry.startsWith(`.${name}-`)) continue;
343
- fs.rmSync(path.join(scopeDir, entry), { recursive: true, force: true });
344
- appendUpgradeLog(`Removed stale scoped staging dir: ${path.join(scopeDir, entry)}`);
343
+ export function cleanupStaleGlobalInstallDirs(pkgName: string, surface: CurrentGlobalInstallSurface): void {
344
+ // The whole routine is housekeeping — never let it throw out and abort the
345
+ // upgrade (npm root/prefix probing or readdir can fail for unrelated reasons).
346
+ try {
347
+ const prefixArgs = surface.installPrefix ? ['--prefix', surface.installPrefix] : [];
348
+ const npmRoot = String(execNpmCommandSync(['root', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
349
+ if (!npmRoot) return;
350
+ const npmPrefix = surface.installPrefix
351
+ || String(execNpmCommandSync(['prefix', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
352
+ const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
353
+ const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
354
+ const binNames = new Set<string>([packageBaseName]);
355
+ if (pkgName === '@adhdev/daemon-standalone') {
356
+ binNames.add('adhdev-standalone');
345
357
  }
346
- } else {
347
- for (const entry of fs.readdirSync(npmRoot)) {
348
- if (!entry.startsWith(`.${pkgName}-`)) continue;
349
- fs.rmSync(path.join(npmRoot, entry), { recursive: true, force: true });
350
- appendUpgradeLog(`Removed stale staging dir: ${path.join(npmRoot, entry)}`);
358
+
359
+ if (pkgName.startsWith('@')) {
360
+ const [scope, name] = pkgName.split('/');
361
+ const scopeDir = path.join(npmRoot, scope);
362
+ if (!fs.existsSync(scopeDir)) return;
363
+ for (const entry of fs.readdirSync(scopeDir)) {
364
+ if (!entry.startsWith(`.${name}-`)) continue;
365
+ safeRemoveStaleEntry(path.join(scopeDir, entry), 'Removed stale scoped staging dir');
366
+ }
367
+ } else {
368
+ for (const entry of fs.readdirSync(npmRoot)) {
369
+ if (!entry.startsWith(`.${pkgName}-`)) continue;
370
+ safeRemoveStaleEntry(path.join(npmRoot, entry), 'Removed stale staging dir');
371
+ }
351
372
  }
352
- }
353
373
 
354
- if (fs.existsSync(binDir)) {
355
- for (const entry of fs.readdirSync(binDir)) {
356
- if (!Array.from(binNames).some((name) => entry.startsWith(`.${name}-`))) continue;
357
- fs.rmSync(path.join(binDir, entry), { recursive: true, force: true });
358
- appendUpgradeLog(`Removed stale bin staging entry: ${path.join(binDir, entry)}`);
374
+ if (fs.existsSync(binDir)) {
375
+ for (const entry of fs.readdirSync(binDir)) {
376
+ if (!Array.from(binNames).some((name) => entry.startsWith(`.${name}-`))) continue;
377
+ safeRemoveStaleEntry(path.join(binDir, entry), 'Removed stale bin staging entry');
378
+ }
359
379
  }
380
+ } catch (error: any) {
381
+ appendUpgradeLog(`Stale staging cleanup skipped (${error?.code || 'error'}): ${error?.message || String(error)}`);
360
382
  }
361
383
  }
362
384
 
@@ -21,6 +21,7 @@
21
21
  import * as fs from 'node:fs';
22
22
  import * as os from 'node:os';
23
23
  import * as path from 'node:path';
24
+ import { LOG } from '../../logging/logger.js';
24
25
  import type {
25
26
  NativeHistoryConfig,
26
27
  NativeHistoryJsonlSource,
@@ -138,8 +139,48 @@ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput):
138
139
  || (requestedSessionId ? null : pickSessionBoundFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor, workspaceHint))
139
140
  || (requestedSessionId ? null : newestRecentFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor));
140
141
  }
142
+ // Raw-workspace slug candidate: `resolved` derives its {cwd*} slug from
143
+ // fs.realpathSync(workspace). On Windows realpath normalizes the path
144
+ // (drive-letter case D:↔d:, \\?\ long-path prefix, junction expansion)
145
+ // so the slug can diverge from the one the CLI actually wrote — and the
146
+ // concrete path above then misses with ENOENT. Retry with the slug built
147
+ // from the RAW workspace string before falling back to a scan; it's the
148
+ // cheap fix when realpath divergence is the only problem.
149
+ if (!sourcePath && !hasDateTemplateSegment(src.path)) {
150
+ const resolvedRaw = expandPath(src.path, input, { skipWorkspaceRealpath: true });
151
+ if (resolvedRaw && resolvedRaw !== resolved) {
152
+ try {
153
+ const rawStat = fs.statSync(resolvedRaw);
154
+ if (rawStat.isFile()) sourcePath = resolvedRaw;
155
+ else if (rawStat.isDirectory()) {
156
+ sourcePath = pickExactSessionFile(resolvedRaw, filePat, requestedSessionId)
157
+ || (requestedSessionId ? null : newestRecentFile(resolvedRaw, filePat, windowMs, sessionFloor));
158
+ }
159
+ } catch { /* raw slug also missed — fall through to scan */ }
160
+ }
161
+ }
162
+ // Last-resort scan: the slug-derived directory missed entirely (the
163
+ // dominant Windows failure: realpath/raw slug both diverge from the CLI's
164
+ // on-disk project dir → 0 messages, no PTY fallback for native-source
165
+ // providers). When we have an exact session id, walk the projects root
166
+ // for `<sessionId>.jsonl` regardless of which project subdir holds it.
167
+ // The session id is a UUID, so basename matching is unambiguous; mirrors
168
+ // the standalone reader's scan (claude-cli-transcript.ts resolveTranscriptPath).
169
+ if (!sourcePath && requestedSessionId) {
170
+ sourcePath = scanProjectsRootForSessionFile(src.path, input, requestedSessionId);
171
+ }
172
+ }
173
+ if (!sourcePath) {
174
+ // Was silent before — a slug miss produced 0 messages with no trace, so
175
+ // a live read_chat returning empty was indistinguishable from "no file"
176
+ // vs "wrong path". Log the attempted concrete path + both slug variants
177
+ // so the failure mode is greppable in daemon logs.
178
+ const wsRaw = typeof input.workspace === 'string' ? input.workspace : '';
179
+ let wsReal = wsRaw;
180
+ try { if (wsRaw) wsReal = fs.realpathSync(wsRaw); } catch { /* keep raw */ }
181
+ LOG.debug('NativeHistory', `jsonl unresolved: tried=${JSON.stringify(resolved)} sessionId=${requestedSessionId || '(none)'} wsRaw=${JSON.stringify(wsRaw)} wsReal=${JSON.stringify(wsReal)} rawSlug=${JSON.stringify(claudeProjectDirName(wsRaw))} realSlug=${JSON.stringify(claudeProjectDirName(wsReal))} (concrete miss + raw-slug retry + projects scan all failed)`);
182
+ return null;
141
183
  }
142
- if (!sourcePath) return null;
143
184
 
144
185
  const mtime = safeMtimeMs(sourcePath);
145
186
  const lines = readJsonlLines(sourcePath);
@@ -278,7 +319,7 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
278
319
  // Path expansion + globbing
279
320
  // ────────────────────────────────────────────────────────────────────────────
280
321
 
281
- function expandPath(template: string, input: NativeHistoryInput): string | null {
322
+ function expandPath(template: string, input: NativeHistoryInput, opts?: { skipWorkspaceRealpath?: boolean }): string | null {
282
323
  if (!template) return null;
283
324
  let out = template;
284
325
  if (out.startsWith('~/') || out === '~') {
@@ -301,7 +342,11 @@ function expandPath(template: string, input: NativeHistoryInput): string | null
301
342
  // `-`. Realpath also handles aliases such as /tmp -> /private/tmp.
302
343
  const workspaceRaw = input.workspace ?? '';
303
344
  let workspaceResolved = workspaceRaw;
304
- if (workspaceRaw) {
345
+ // The caller may request the RAW slug (skip realpath) to recover from
346
+ // Windows realpath normalization diverging the {cwd*} slug from the dir
347
+ // the CLI actually created. Default keeps realpath (handles /tmp ->
348
+ // /private/tmp aliasing that the CLI itself resolves on macOS).
349
+ if (workspaceRaw && !opts?.skipWorkspaceRealpath) {
305
350
  try { workspaceResolved = fs.realpathSync(workspaceRaw); }
306
351
  catch { /* path may not exist yet — keep the raw value */ }
307
352
  }
@@ -333,6 +378,62 @@ function claudeProjectDirName(workspace: string): string {
333
378
  return workspace.replace(/[^A-Za-z0-9_-]/g, '-');
334
379
  }
335
380
 
381
+ /**
382
+ * Last-resort lookup for a transcript by its exact session id, ignoring the
383
+ * per-cwd slug entirely. The slug-derived directory above can miss completely
384
+ * when the CLI's on-disk project dir disagrees with the slug we reconstruct
385
+ * (notably on Windows, where fs.realpathSync normalizes drive-letter case and
386
+ * adds a \\?\ prefix). Since the session id is a UUID, scanning the projects
387
+ * root for `<sessionId>.jsonl` is unambiguous.
388
+ *
389
+ * Derives the scan base from the template's segments up to (but excluding) the
390
+ * first one that references a per-session variable ({cwd*} or {session_id}) —
391
+ * e.g. `~/.claude/projects/{cwd_claude_project}/{session_id}.jsonl` → scan
392
+ * `~/.claude/projects`. Returns the matching file path, or null.
393
+ */
394
+ function scanProjectsRootForSessionFile(template: string, input: NativeHistoryInput, requestedSessionId: string): string | null {
395
+ if (!requestedSessionId) return null;
396
+ // Resolve the leading static portion of the template (everything before the
397
+ // first {var} segment) into a concrete base directory.
398
+ let head = template;
399
+ if (head.startsWith('~/') || head === '~') head = path.join(os.homedir(), head.slice(2));
400
+ const segs = head.split('/');
401
+ const baseParts: string[] = [];
402
+ for (const seg of segs) {
403
+ if (/[{}*?]/.test(seg)) break;
404
+ baseParts.push(seg);
405
+ }
406
+ const base = baseParts.join('/');
407
+ if (!base) return null;
408
+ let baseStat: fs.Stats | null = null;
409
+ try { baseStat = fs.statSync(base); } catch { return null; }
410
+ if (!baseStat.isDirectory()) return null;
411
+
412
+ const needle = `${requestedSessionId.toLowerCase()}.jsonl`;
413
+ // Bounded walk: project layouts are <root>/<projectDir>/<uuid>.jsonl, so a
414
+ // shallow scan (root + one level of subdirs) suffices and avoids walking an
415
+ // unbounded tree. Check the root itself first, then each immediate subdir.
416
+ const dirsToScan: string[] = [base];
417
+ try {
418
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
419
+ if (entry.isDirectory()) dirsToScan.push(path.join(base, entry.name));
420
+ }
421
+ } catch { /* readdir failed — fall back to scanning base only */ }
422
+
423
+ for (const dir of dirsToScan) {
424
+ let entries: fs.Dirent[];
425
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
426
+ for (const entry of entries) {
427
+ if (!entry.isFile()) continue;
428
+ if (entry.name.toLowerCase() !== needle) continue;
429
+ const found = path.join(dir, entry.name);
430
+ LOG.debug('NativeHistory', `jsonl scan-fallback hit: sessionId=${requestedSessionId} resolved via projects-root scan → ${JSON.stringify(found)} (slug-derived path missed; likely realpath/slug divergence)`);
431
+ return found;
432
+ }
433
+ }
434
+ return null;
435
+ }
436
+
336
437
  function globToRegex(pattern: string): RegExp {
337
438
  // Minimal glob: `*` → `[^/]*`, `?` → `[^/]`, `.` → `\.`. Anchored.
338
439
  const re = pattern