@imdeadpool/guardex 7.0.20 → 7.0.21

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.
@@ -5,6 +5,11 @@ const cp = require('node:child_process');
5
5
  const ACTIVE_SESSIONS_RELATIVE_DIR = path.join('.omx', 'state', 'active-sessions');
6
6
  const SESSION_SCHEMA_VERSION = 1;
7
7
  const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
8
+ const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock';
9
+ const MANAGED_WORKTREE_ROOTS = [
10
+ path.join('.omx', 'agent-worktrees'),
11
+ path.join('.omc', 'agent-worktrees'),
12
+ ];
8
13
  const MAX_CHANGED_PATH_PREVIEW = 3;
9
14
  const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/');
10
15
  const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/');
@@ -62,6 +67,10 @@ function sessionFilePathForBranch(repoRoot, branch) {
62
67
  return path.join(activeSessionsDirForRepo(repoRoot), sessionFileNameForBranch(branch));
63
68
  }
64
69
 
70
+ function resolveManagedWorktreeRoots(repoRoot) {
71
+ return MANAGED_WORKTREE_ROOTS.map((relativeRoot) => path.join(path.resolve(repoRoot), relativeRoot));
72
+ }
73
+
65
74
  function splitOutputLines(output) {
66
75
  if (typeof output !== 'string') {
67
76
  return null;
@@ -76,6 +85,24 @@ function normalizeRelativePath(value) {
76
85
  return toNonEmptyString(value).replace(/\\/g, '/').replace(/^\.\//, '');
77
86
  }
78
87
 
88
+ function readJsonFile(filePath) {
89
+ try {
90
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
91
+ } catch (_error) {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function normalizeIsoString(value, fallback = '') {
97
+ const normalized = toNonEmptyString(value);
98
+ if (!normalized) {
99
+ return fallback;
100
+ }
101
+
102
+ const timestamp = Date.parse(normalized);
103
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
104
+ }
105
+
79
106
  function runGitLines(worktreePath, args) {
80
107
  try {
81
108
  const output = cp.execFileSync('git', ['-C', worktreePath, ...args], {
@@ -200,8 +227,8 @@ function parseRepoChangeLine(repoRoot, line) {
200
227
 
201
228
  function collectWorktreeChangedPaths(worktreePath) {
202
229
  const changedGroups = [
203
- runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]),
204
- runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]),
230
+ runGitLines(worktreePath, ['diff', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]),
231
+ runGitLines(worktreePath, ['diff', '--cached', '--name-only', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`, `:(exclude)${AGENT_WORKTREE_LOCK_FILE}`]),
205
232
  runGitLines(worktreePath, ['ls-files', '--others', '--exclude-standard']),
206
233
  ];
207
234
 
@@ -210,7 +237,11 @@ function collectWorktreeChangedPaths(worktreePath) {
210
237
  }
211
238
 
212
239
  return [...new Set(changedGroups.flat())]
213
- .filter((relativePath) => relativePath && relativePath !== LOCK_FILE_RELATIVE)
240
+ .filter((relativePath) => (
241
+ relativePath
242
+ && relativePath !== LOCK_FILE_RELATIVE
243
+ && relativePath !== AGENT_WORKTREE_LOCK_FILE
244
+ ))
214
245
  .sort((left, right) => left.localeCompare(right));
215
246
  }
216
247
 
@@ -290,6 +321,8 @@ function deriveLatestWorktreeFileActivity(worktreePath) {
290
321
 
291
322
  function deriveSessionActivity(session, options = {}) {
292
323
  const now = Number.isFinite(options.now) ? options.now : Date.now();
324
+ const pid = toPositiveInteger(session?.pid);
325
+ const pidAlive = pid ? isPidAlive(pid) : null;
293
326
  const blockingLabel = deriveBlockingGitLabel(session.worktreePath);
294
327
  if (blockingLabel) {
295
328
  return {
@@ -299,14 +332,13 @@ function deriveSessionActivity(session, options = {}) {
299
332
  activitySummary: blockingLabel,
300
333
  changeCount: 0,
301
334
  changedPaths: [],
302
- pidAlive: isPidAlive(session.pid),
335
+ pidAlive,
303
336
  lastFileActivityAt: '',
304
337
  lastFileActivityLabel: '',
305
338
  };
306
339
  }
307
340
 
308
- const pidAlive = isPidAlive(session.pid);
309
- if (!pidAlive) {
341
+ if (pid && !pidAlive) {
310
342
  return {
311
343
  activityKind: 'dead',
312
344
  activityLabel: 'dead',
@@ -426,6 +458,7 @@ function buildSessionRecord(input) {
426
458
  repoRoot,
427
459
  branch,
428
460
  taskName: toNonEmptyString(input.taskName, 'task'),
461
+ latestTaskPreview: '',
429
462
  agentName: toNonEmptyString(input.agentName, 'agent'),
430
463
  worktreePath,
431
464
  pid,
@@ -465,6 +498,7 @@ function normalizeSessionRecord(input, options = {}) {
465
498
  repoRoot: path.resolve(repoRoot),
466
499
  branch,
467
500
  taskName: toNonEmptyString(input.taskName, 'task'),
501
+ latestTaskPreview: '',
468
502
  agentName: toNonEmptyString(input.agentName, 'agent'),
469
503
  worktreePath: path.resolve(worktreePath),
470
504
  pid,
@@ -476,6 +510,12 @@ function normalizeSessionRecord(input, options = {}) {
476
510
  filePath: toNonEmptyString(options.filePath),
477
511
  label: deriveSessionLabel(branch, worktreePath),
478
512
  changedPaths: [],
513
+ sourceKind: 'active-session',
514
+ telemetryUpdatedAt: '',
515
+ telemetrySource: '',
516
+ lockSnapshotCount: 0,
517
+ lockSessionCount: 0,
518
+ collaboration: false,
479
519
  };
480
520
  }
481
521
 
@@ -517,49 +557,212 @@ function isPidAlive(pid) {
517
557
  }
518
558
  }
519
559
 
520
- function readActiveSessions(repoRoot, options = {}) {
521
- const activeSessionsDir = activeSessionsDirForRepo(repoRoot);
522
- if (!fs.existsSync(activeSessionsDir)) {
523
- return [];
560
+ function readWorktreeBranch(worktreePath) {
561
+ const lines = runGitLines(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
562
+ return Array.isArray(lines) && typeof lines[0] === 'string' ? lines[0].trim() : '';
563
+ }
564
+
565
+ function deriveAgentNameFromBranch(branch) {
566
+ const parts = toNonEmptyString(branch).split('/').filter(Boolean);
567
+ if (parts.length >= 2 && parts[0] === 'agent') {
568
+ return parts[1];
569
+ }
570
+ return 'agent';
571
+ }
572
+
573
+ function flattenTelemetrySnapshotSessions(lockPayload) {
574
+ const flattened = [];
575
+ const snapshots = Array.isArray(lockPayload?.snapshots) ? lockPayload.snapshots : [];
576
+ for (const snapshot of snapshots) {
577
+ const snapshotSessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : [];
578
+ for (const session of snapshotSessions) {
579
+ flattened.push({
580
+ taskPreview: toNonEmptyString(session?.taskPreview),
581
+ taskUpdatedAt: normalizeIsoString(session?.taskUpdatedAt),
582
+ projectName: toNonEmptyString(session?.projectName),
583
+ projectPath: toNonEmptyString(session?.projectPath),
584
+ snapshotName: toNonEmptyString(snapshot?.snapshotName),
585
+ email: toNonEmptyString(snapshot?.email),
586
+ });
587
+ }
524
588
  }
589
+ return flattened;
590
+ }
591
+
592
+ function sortSessionsByTimestamp(sessions) {
593
+ sessions.sort((left, right) => {
594
+ const timeDelta = Date.parse(right.startedAt) - Date.parse(left.startedAt);
595
+ if (timeDelta !== 0) {
596
+ return timeDelta;
597
+ }
598
+ return left.label.localeCompare(right.label);
599
+ });
600
+ return sessions;
601
+ }
525
602
 
603
+ function deriveLockTaskAnchor(entries, fallbackTaskName, fallbackTimestamp) {
604
+ const sortedEntries = [...entries].sort((left, right) => {
605
+ const timeDelta = Date.parse(right.taskUpdatedAt || '') - Date.parse(left.taskUpdatedAt || '');
606
+ if (timeDelta !== 0) {
607
+ return timeDelta;
608
+ }
609
+ if (Boolean(right.taskPreview) !== Boolean(left.taskPreview)) {
610
+ return Number(Boolean(right.taskPreview)) - Number(Boolean(left.taskPreview));
611
+ }
612
+ return (right.projectPath || '').localeCompare(left.projectPath || '');
613
+ });
614
+
615
+ const latestEntry = sortedEntries[0] || null;
616
+ return {
617
+ taskName: latestEntry?.taskPreview || fallbackTaskName || 'task',
618
+ latestTaskPreview: latestEntry?.taskPreview || '',
619
+ timestamp: latestEntry?.taskUpdatedAt || fallbackTimestamp || '',
620
+ };
621
+ }
622
+
623
+ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options = {}) {
526
624
  const now = options.now || Date.now();
625
+ const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload);
626
+ const telemetryUpdatedAt = normalizeIsoString(lockPayload?.updatedAt);
627
+ const branch = readWorktreeBranch(worktreePath);
628
+ const effectiveBranch = branch && branch !== 'HEAD'
629
+ ? branch
630
+ : `agent/telemetry/${path.basename(worktreePath)}`;
631
+ const label = deriveSessionLabel(effectiveBranch, worktreePath);
632
+ const taskAnchor = deriveLockTaskAnchor(telemetryEntries, label, telemetryUpdatedAt);
633
+ const startedAt = taskAnchor.timestamp || telemetryUpdatedAt || new Date(now).toISOString();
634
+
635
+ const session = {
636
+ schemaVersion: toPositiveInteger(lockPayload?.schemaVersion) || SESSION_SCHEMA_VERSION,
637
+ repoRoot: path.resolve(repoRoot),
638
+ branch: effectiveBranch,
639
+ taskName: taskAnchor.taskName,
640
+ latestTaskPreview: taskAnchor.latestTaskPreview,
641
+ agentName: deriveAgentNameFromBranch(effectiveBranch),
642
+ worktreePath: path.resolve(worktreePath),
643
+ pid: null,
644
+ cliName: 'codex',
645
+ taskMode: '',
646
+ openspecTier: '',
647
+ taskRoutingReason: '',
648
+ startedAt,
649
+ filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE),
650
+ label,
651
+ changedPaths: [],
652
+ sourceKind: 'worktree-lock',
653
+ telemetryUpdatedAt: telemetryUpdatedAt || startedAt,
654
+ telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'),
655
+ lockSnapshotCount: toPositiveInteger(lockPayload?.snapshotCount) || 0,
656
+ lockSessionCount: toPositiveInteger(lockPayload?.sessionCount) || telemetryEntries.length,
657
+ collaboration: Boolean(lockPayload?.collaboration),
658
+ };
659
+
660
+ session.elapsedLabel = formatElapsedFrom(session.startedAt, now);
661
+ Object.assign(session, deriveSessionActivity(session, { now }));
662
+ return session;
663
+ }
664
+
665
+ function readWorktreeLockSessions(repoRoot, options = {}) {
527
666
  const sessions = [];
528
- for (const entry of fs.readdirSync(activeSessionsDir, { withFileTypes: true })) {
529
- if (!entry.isFile() || !entry.name.endsWith('.json')) {
667
+ for (const managedRoot of resolveManagedWorktreeRoots(repoRoot)) {
668
+ if (!fs.existsSync(managedRoot)) {
530
669
  continue;
531
670
  }
532
671
 
533
- const filePath = path.join(activeSessionsDir, entry.name);
534
- let parsed;
672
+ let entries;
535
673
  try {
536
- parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
674
+ entries = fs.readdirSync(managedRoot, { withFileTypes: true });
537
675
  } catch (_error) {
538
676
  continue;
539
677
  }
540
678
 
541
- const normalized = normalizeSessionRecord(parsed, { filePath });
542
- if (!normalized) {
543
- continue;
679
+ for (const entry of entries) {
680
+ if (!entry.isDirectory()) {
681
+ continue;
682
+ }
683
+
684
+ const worktreePath = path.join(managedRoot, entry.name);
685
+ const lockPath = path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE);
686
+ if (!fs.existsSync(lockPath)) {
687
+ continue;
688
+ }
689
+
690
+ const lockPayload = readJsonFile(lockPath);
691
+ if (!lockPayload || typeof lockPayload !== 'object' || Array.isArray(lockPayload)) {
692
+ continue;
693
+ }
694
+
695
+ const telemetryEntries = flattenTelemetrySnapshotSessions(lockPayload);
696
+ if (telemetryEntries.length === 0 && !toPositiveInteger(lockPayload.sessionCount)) {
697
+ continue;
698
+ }
699
+
700
+ sessions.push(buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options));
544
701
  }
545
- if (!options.includeStale && !isPidAlive(normalized.pid)) {
702
+ }
703
+
704
+ return sortSessionsByTimestamp(sessions);
705
+ }
706
+
707
+ function mergeSessionSources(primarySessions, lockSessions) {
708
+ const lockSessionsByWorktree = new Map(
709
+ lockSessions.map((session) => [path.resolve(session.worktreePath), session]),
710
+ );
711
+ const consumedLockWorktrees = new Set();
712
+ const merged = [];
713
+
714
+ for (const session of primarySessions) {
715
+ const worktreeKey = path.resolve(session.worktreePath);
716
+ const lockSession = lockSessionsByWorktree.get(worktreeKey);
717
+ if (lockSession && session.activityKind === 'dead') {
546
718
  continue;
547
719
  }
720
+ if (lockSession) {
721
+ consumedLockWorktrees.add(worktreeKey);
722
+ }
723
+ merged.push(session);
724
+ }
548
725
 
549
- normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now);
550
- Object.assign(normalized, deriveSessionActivity(normalized, { now }));
551
- sessions.push(normalized);
726
+ for (const lockSession of lockSessions) {
727
+ const worktreeKey = path.resolve(lockSession.worktreePath);
728
+ if (!consumedLockWorktrees.has(worktreeKey)) {
729
+ merged.push(lockSession);
730
+ }
552
731
  }
553
732
 
554
- sessions.sort((left, right) => {
555
- const timeDelta = Date.parse(right.startedAt) - Date.parse(left.startedAt);
556
- if (timeDelta !== 0) {
557
- return timeDelta;
733
+ return sortSessionsByTimestamp(merged);
734
+ }
735
+
736
+ function readActiveSessions(repoRoot, options = {}) {
737
+ const activeSessionsDir = activeSessionsDirForRepo(repoRoot);
738
+ const now = options.now || Date.now();
739
+ const sessionFileSessions = [];
740
+ if (fs.existsSync(activeSessionsDir)) {
741
+ for (const entry of fs.readdirSync(activeSessionsDir, { withFileTypes: true })) {
742
+ if (!entry.isFile() || !entry.name.endsWith('.json')) {
743
+ continue;
744
+ }
745
+
746
+ const filePath = path.join(activeSessionsDir, entry.name);
747
+ const parsed = readJsonFile(filePath);
748
+ const normalized = normalizeSessionRecord(parsed, { filePath });
749
+ if (!normalized) {
750
+ continue;
751
+ }
752
+ if (!options.includeStale && !isPidAlive(normalized.pid)) {
753
+ continue;
754
+ }
755
+
756
+ normalized.elapsedLabel = formatElapsedFrom(normalized.startedAt, now);
757
+ Object.assign(normalized, deriveSessionActivity(normalized, { now }));
758
+ sessionFileSessions.push(normalized);
558
759
  }
559
- return left.label.localeCompare(right.label);
560
- });
760
+ }
561
761
 
562
- return sessions;
762
+ return mergeSessionSources(
763
+ sortSessionsByTimestamp(sessionFileSessions),
764
+ readWorktreeLockSessions(repoRoot, { now }),
765
+ );
563
766
  }
564
767
 
565
768
  function readRepoChanges(repoRoot) {
@@ -592,6 +795,7 @@ module.exports = {
592
795
  parseRepoChangeLine,
593
796
  previewChangedPaths,
594
797
  readActiveSessions,
798
+ readWorktreeLockSessions,
595
799
  readRepoChanges,
596
800
  deriveRepoChangeStatus,
597
801
  resolveWorktreeGitDir,