@adhdev/daemon-core 0.9.82-rc.183 → 0.9.82-rc.185
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/dist/index.js +105 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +105 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/providers/spec/native-history-executor.ts +170 -3
package/package.json
CHANGED
|
@@ -101,16 +101,27 @@ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput):
|
|
|
101
101
|
// otherwise look for a matching file; the session-start floor wins
|
|
102
102
|
// when it's later.
|
|
103
103
|
const sessionFloor = typeof input.sessionStartedAtMs === 'number' ? input.sessionStartedAtMs : 0;
|
|
104
|
+
// When multiple concurrent CLI sessions in the same workspace each create
|
|
105
|
+
// their own rollout (e.g. two codex-cli sessions both writing into
|
|
106
|
+
// ~/.codex/sessions/{date}), `newestRecentFile` picks the same file for
|
|
107
|
+
// every reader and the daemon sessions cross-alias each other. Prefer
|
|
108
|
+
// session-meta-aware matching: the candidate whose meta.cwd matches the
|
|
109
|
+
// workspace AND whose meta.timestamp is closest to (or within
|
|
110
|
+
// spawnGraceMs of) the daemon's spawn time wins. Falls back to mtime
|
|
111
|
+
// ordering when no candidate exposes a usable session_meta.
|
|
112
|
+
const workspaceHint = typeof input.workspace === 'string' && input.workspace.trim() ? input.workspace.trim() : '';
|
|
104
113
|
let sourcePath: string | null = null;
|
|
105
114
|
if (resolved.includes('*')) {
|
|
106
|
-
sourcePath =
|
|
115
|
+
sourcePath = pickSessionBoundFileAcrossGlob(resolved, filePat, windowMs, sessionFloor, workspaceHint)
|
|
116
|
+
|| newestRecentFileAcrossGlob(resolved, filePat, windowMs, sessionFloor);
|
|
107
117
|
} else {
|
|
108
118
|
let stat: fs.Stats | null = null;
|
|
109
119
|
try { stat = fs.statSync(resolved); } catch { /* fall through to date-walk fallback */ }
|
|
110
120
|
if (stat && stat.isFile()) {
|
|
111
121
|
sourcePath = resolved;
|
|
112
122
|
} else if (stat && stat.isDirectory()) {
|
|
113
|
-
sourcePath =
|
|
123
|
+
sourcePath = pickSessionBoundFile(resolved, filePat, windowMs, sessionFloor, workspaceHint)
|
|
124
|
+
|| newestRecentFile(resolved, filePat, windowMs, sessionFloor);
|
|
114
125
|
}
|
|
115
126
|
// Date-templated directories (e.g. ~/.codex/sessions/{yyyy}/{mm}/{dd})
|
|
116
127
|
// expand to today's UTC dir. A session started before UTC midnight
|
|
@@ -119,7 +130,8 @@ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput):
|
|
|
119
130
|
// or doesn't exist yet, walk the last 3 UTC days at the same depth
|
|
120
131
|
// and pick the newest matching rollout across all of them.
|
|
121
132
|
if (!sourcePath && hasDateTemplateSegment(src.path)) {
|
|
122
|
-
sourcePath =
|
|
133
|
+
sourcePath = pickSessionBoundFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor, workspaceHint)
|
|
134
|
+
|| newestRecentFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor);
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
if (!sourcePath) return null;
|
|
@@ -469,6 +481,161 @@ function safeMtimeMs(p: string): number {
|
|
|
469
481
|
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
470
482
|
}
|
|
471
483
|
|
|
484
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
485
|
+
// Per-session rollout binding
|
|
486
|
+
//
|
|
487
|
+
// Reads the first JSONL line of a candidate file and returns a `session_meta`
|
|
488
|
+
// payload if present. Codex-cli writes
|
|
489
|
+
// {"timestamp":"...","type":"session_meta","payload":{"id":...,"cwd":...,
|
|
490
|
+
// "timestamp":"..."}}
|
|
491
|
+
// as the first record. Other providers that don't follow this convention
|
|
492
|
+
// return null and fall back to the mtime-based picker.
|
|
493
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
interface CandidateMeta {
|
|
496
|
+
cwd?: string;
|
|
497
|
+
sessionTimestampMs?: number;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function readCandidateSessionMeta(filePath: string): CandidateMeta | null {
|
|
501
|
+
try {
|
|
502
|
+
// Read only the first line — meta is always the first JSONL record
|
|
503
|
+
// and full file reads here would scale O(files × file_size).
|
|
504
|
+
const fd = fs.openSync(filePath, 'r');
|
|
505
|
+
try {
|
|
506
|
+
const buf = Buffer.alloc(8192);
|
|
507
|
+
const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
508
|
+
if (bytes <= 0) return null;
|
|
509
|
+
const text = buf.subarray(0, bytes).toString('utf8');
|
|
510
|
+
const nl = text.indexOf('\n');
|
|
511
|
+
const firstLine = (nl >= 0 ? text.slice(0, nl) : text).trim();
|
|
512
|
+
if (!firstLine) return null;
|
|
513
|
+
const parsed = JSON.parse(firstLine) as Record<string, unknown>;
|
|
514
|
+
if (String(parsed.type ?? '') !== 'session_meta') return null;
|
|
515
|
+
const payload = parsed.payload && typeof parsed.payload === 'object'
|
|
516
|
+
? (parsed.payload as Record<string, unknown>)
|
|
517
|
+
: null;
|
|
518
|
+
if (!payload) return null;
|
|
519
|
+
const cwd = typeof payload.cwd === 'string' ? payload.cwd : undefined;
|
|
520
|
+
const tsRaw = payload.timestamp;
|
|
521
|
+
const tsMs = typeof tsRaw === 'string'
|
|
522
|
+
? Date.parse(tsRaw)
|
|
523
|
+
: typeof tsRaw === 'number'
|
|
524
|
+
? (tsRaw < 1e12 ? Math.floor(tsRaw * 1000) : Math.floor(tsRaw))
|
|
525
|
+
: NaN;
|
|
526
|
+
return {
|
|
527
|
+
cwd,
|
|
528
|
+
sessionTimestampMs: Number.isFinite(tsMs) ? tsMs : undefined,
|
|
529
|
+
};
|
|
530
|
+
} finally {
|
|
531
|
+
fs.closeSync(fd);
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Spawn-bind grace window: an on-disk rollout whose session_meta.timestamp
|
|
540
|
+
* lands within ±SPAWN_BIND_GRACE_MS of the daemon's spawnedAtMs is treated
|
|
541
|
+
* as belonging to that daemon session. 10s is long enough to absorb codex
|
|
542
|
+
* binary startup latency on cold caches and short enough that two
|
|
543
|
+
* back-to-back launches don't both fall inside the same window.
|
|
544
|
+
*/
|
|
545
|
+
const SPAWN_BIND_GRACE_MS = 10_000;
|
|
546
|
+
|
|
547
|
+
function pickBoundFromEntries(
|
|
548
|
+
candidatePaths: string[],
|
|
549
|
+
sessionFloorMs: number,
|
|
550
|
+
workspaceHint: string,
|
|
551
|
+
): string | null {
|
|
552
|
+
if (!sessionFloorMs || !workspaceHint || candidatePaths.length === 0) return null;
|
|
553
|
+
// Resolve workspaceHint to handle macOS /tmp → /private/tmp aliasing, the
|
|
554
|
+
// same way expandPath does for the template substitution. Without this
|
|
555
|
+
// the daemon's `/Users/foo/repo` and codex's `/private/Users/foo/repo`
|
|
556
|
+
// never compare equal and disambiguation silently fails.
|
|
557
|
+
let workspaceResolved = workspaceHint;
|
|
558
|
+
try { workspaceResolved = fs.realpathSync(workspaceHint); } catch { /* keep raw */ }
|
|
559
|
+
let best: { p: string; diff: number } | null = null;
|
|
560
|
+
for (const p of candidatePaths) {
|
|
561
|
+
const meta = readCandidateSessionMeta(p);
|
|
562
|
+
if (!meta || !meta.cwd || meta.sessionTimestampMs == null) continue;
|
|
563
|
+
let candidateCwd = meta.cwd;
|
|
564
|
+
try { candidateCwd = fs.realpathSync(meta.cwd); } catch { /* keep raw */ }
|
|
565
|
+
if (candidateCwd !== workspaceResolved && meta.cwd !== workspaceHint) continue;
|
|
566
|
+
const diff = Math.abs(meta.sessionTimestampMs - sessionFloorMs);
|
|
567
|
+
if (diff > SPAWN_BIND_GRACE_MS) continue;
|
|
568
|
+
if (!best || diff < best.diff) best = { p, diff };
|
|
569
|
+
}
|
|
570
|
+
return best ? best.p : null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function listMatchingFiles(dir: string, pattern: RegExp): string[] {
|
|
574
|
+
let entries: fs.Dirent[];
|
|
575
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
576
|
+
const out: string[] = [];
|
|
577
|
+
for (const e of entries) {
|
|
578
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
579
|
+
out.push(path.join(dir, e.name));
|
|
580
|
+
}
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function pickSessionBoundFile(
|
|
585
|
+
dir: string,
|
|
586
|
+
pattern: RegExp,
|
|
587
|
+
windowMs: number,
|
|
588
|
+
sessionFloorMs: number,
|
|
589
|
+
workspaceHint: string,
|
|
590
|
+
): string | null {
|
|
591
|
+
if (!sessionFloorMs || !workspaceHint) return null;
|
|
592
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
|
|
593
|
+
const files = listMatchingFiles(dir, pattern).filter(p => safeMtimeMs(p) >= cutoff);
|
|
594
|
+
return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function pickSessionBoundFileAcrossGlob(
|
|
598
|
+
template: string,
|
|
599
|
+
pattern: RegExp,
|
|
600
|
+
windowMs: number,
|
|
601
|
+
sessionFloorMs: number,
|
|
602
|
+
workspaceHint: string,
|
|
603
|
+
): string | null {
|
|
604
|
+
if (!sessionFloorMs || !workspaceHint) return null;
|
|
605
|
+
const dirs = expandDirGlob(template);
|
|
606
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
|
|
607
|
+
const files: string[] = [];
|
|
608
|
+
for (const d of dirs) {
|
|
609
|
+
for (const p of listMatchingFiles(d, pattern)) {
|
|
610
|
+
if (safeMtimeMs(p) >= cutoff) files.push(p);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function pickSessionBoundFileAcrossDateWindow(
|
|
617
|
+
template: string,
|
|
618
|
+
input: NativeHistoryInput,
|
|
619
|
+
pattern: RegExp,
|
|
620
|
+
windowMs: number,
|
|
621
|
+
sessionFloorMs: number,
|
|
622
|
+
workspaceHint: string,
|
|
623
|
+
): string | null {
|
|
624
|
+
if (!sessionFloorMs || !workspaceHint) return null;
|
|
625
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs - SPAWN_BIND_GRACE_MS);
|
|
626
|
+
const files: string[] = [];
|
|
627
|
+
for (let dayOffset = 0; dayOffset < 3; dayOffset += 1) {
|
|
628
|
+
const dayMs = sessionFloorMs - dayOffset * 24 * 60 * 60 * 1000;
|
|
629
|
+
const dayInput: NativeHistoryInput = { ...input, sessionStartedAtMs: sessionFloorMs };
|
|
630
|
+
const resolved = expandPathForDate(template, dayInput, new Date(dayMs));
|
|
631
|
+
if (!resolved) continue;
|
|
632
|
+
for (const p of listMatchingFiles(resolved, pattern)) {
|
|
633
|
+
if (safeMtimeMs(p) >= cutoff) files.push(p);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return pickBoundFromEntries(files, sessionFloorMs, workspaceHint);
|
|
637
|
+
}
|
|
638
|
+
|
|
472
639
|
// ────────────────────────────────────────────────────────────────────────────
|
|
473
640
|
// jsonpath-lite + projection
|
|
474
641
|
// ────────────────────────────────────────────────────────────────────────────
|