@imdeadpool/guardex 7.0.22 → 7.0.23

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.
@@ -3,7 +3,7 @@
3
3
  "displayName": "GitGuardex Active Agents",
4
4
  "description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
5
5
  "publisher": "recodeee",
6
- "version": "0.0.3",
6
+ "version": "0.0.7",
7
7
  "license": "MIT",
8
8
  "icon": "icon.png",
9
9
  "engines": {
@@ -36,6 +36,11 @@
36
36
  "title": "Commit Selected Session",
37
37
  "icon": "$(check)"
38
38
  },
39
+ {
40
+ "command": "gitguardex.activeAgents.inspect",
41
+ "title": "Inspect Session",
42
+ "icon": "$(info)"
43
+ },
39
44
  {
40
45
  "command": "gitguardex.activeAgents.openWorktree",
41
46
  "title": "Open Agent Worktree"
@@ -73,14 +78,14 @@
73
78
  "viewsWelcome": [
74
79
  {
75
80
  "view": "gitguardex.activeAgents",
76
- "contents": "No active Guardex agents are visible in this workspace yet.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)"
81
+ "contents": "No live Guardex agents are visible in this workspace yet.\n\nThis view tracks Guardex session files and managed worktree telemetry, not every repo visible in Source Control.\n\n[Start agent](command:gitguardex.activeAgents.startAgent)\n[Open guide](https://github.com/recodeee/gitguardex/blob/main/vscode/guardex-active-agents/README.md#quick-start)\n[Refresh](command:gitguardex.activeAgents.refresh)"
77
82
  }
78
83
  ],
79
84
  "menus": {
80
85
  "view/title": [
81
86
  {
82
87
  "command": "gitguardex.activeAgents.commitSelectedSession",
83
- "when": "view == gitguardex.activeAgents",
88
+ "when": "view == gitguardex.activeAgents && guardex.hasAgents",
84
89
  "group": "navigation@1"
85
90
  },
86
91
  {
@@ -95,6 +100,11 @@
95
100
  "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
96
101
  "group": "inline"
97
102
  },
103
+ {
104
+ "command": "gitguardex.activeAgents.inspect",
105
+ "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
106
+ "group": "inline"
107
+ },
98
108
  {
99
109
  "command": "gitguardex.activeAgents.finishSession",
100
110
  "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
@@ -5,6 +5,7 @@ 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 LOGS_RELATIVE_DIR = path.join('.omx', 'logs');
8
9
  const AGENT_WORKTREE_LOCK_FILE = 'AGENT.lock';
9
10
  const MANAGED_WORKTREE_ROOTS = [
10
11
  path.join('.omx', 'agent-worktrees'),
@@ -13,8 +14,42 @@ const MANAGED_WORKTREE_ROOTS = [
13
14
  const MAX_CHANGED_PATH_PREVIEW = 3;
14
15
  const ACTIVE_SESSIONS_FILTER_PREFIX = ACTIVE_SESSIONS_RELATIVE_DIR.split(path.sep).join('/');
15
16
  const LOCK_FILE_FILTER_PATH = LOCK_FILE_RELATIVE.split(path.sep).join('/');
17
+ const MANAGED_WORKTREE_FILTER_PREFIXES = MANAGED_WORKTREE_ROOTS
18
+ .map((relativeRoot) => relativeRoot.split(path.sep).join('/').replace(/\/+$/, ''));
16
19
  const IDLE_ACTIVITY_WINDOW_MS = 2 * 60 * 1000;
17
20
  const STALLED_ACTIVITY_WINDOW_MS = 15 * 60 * 1000;
21
+ const HEARTBEAT_STALE_MS = 5 * 60 * 1000;
22
+ const DEFAULT_BASE_BRANCH = 'dev';
23
+ const DEFAULT_LOG_TAIL_LINE_COUNT = 200;
24
+ const ADVISORY_SESSION_STATES = new Set(['working', 'thinking', 'idle']);
25
+ const WORKTREE_ACTIVITY_CACHE_TTL_MS = 3_000;
26
+ const MAX_WORKTREE_ACTIVITY_STAT_PATHS = 200;
27
+ const WORKTREE_ACTIVITY_SKIP_PREFIXES = [
28
+ '.git/',
29
+ '.omx/',
30
+ '.omc/',
31
+ 'node_modules/',
32
+ 'dist/',
33
+ 'build/',
34
+ 'coverage/',
35
+ '.next/',
36
+ 'out/',
37
+ 'vendor/',
38
+ ];
39
+ const WORKTREE_ACTIVITY_PRIORITY_PREFIXES = [
40
+ 'src/',
41
+ 'app/',
42
+ 'apps/',
43
+ 'lib/',
44
+ 'packages/',
45
+ 'scripts/',
46
+ 'test/',
47
+ 'tests/',
48
+ 'vscode/',
49
+ 'templates/',
50
+ 'openspec/',
51
+ 'docs/',
52
+ ];
18
53
  const BLOCKING_GIT_STATES = [
19
54
  {
20
55
  label: 'Rebase in progress.',
@@ -29,6 +64,7 @@ const BLOCKING_GIT_STATES = [
29
64
  markers: ['CHERRY_PICK_HEAD'],
30
65
  },
31
66
  ];
67
+ const worktreeActivityCache = new Map();
32
68
 
33
69
  function toNonEmptyString(value, fallback = '') {
34
70
  const normalized = typeof value === 'string' ? value.trim() : String(value || '').trim();
@@ -50,6 +86,11 @@ function normalizeOpenSpecTier(value) {
50
86
  return ['T0', 'T1', 'T2', 'T3'].includes(normalized) ? normalized : '';
51
87
  }
52
88
 
89
+ function normalizeAdvisoryState(value, fallback = 'working') {
90
+ const normalized = toNonEmptyString(value).toLowerCase();
91
+ return ADVISORY_SESSION_STATES.has(normalized) ? normalized : fallback;
92
+ }
93
+
53
94
  function sanitizeBranchForFile(branch) {
54
95
  const normalized = toNonEmptyString(branch, 'session');
55
96
  return normalized.replace(/[^a-zA-Z0-9._-]+/g, '__').replace(/^_+|_+$/g, '') || 'session';
@@ -93,6 +134,138 @@ function readJsonFile(filePath) {
93
134
  }
94
135
  }
95
136
 
137
+ function readConfiguredBaseBranch(repoRoot) {
138
+ const lines = runGitLines(path.resolve(repoRoot), ['config', '--get', 'multiagent.baseBranch']);
139
+ if (Array.isArray(lines) && typeof lines[0] === 'string' && lines[0].trim()) {
140
+ return lines[0].trim();
141
+ }
142
+ return DEFAULT_BASE_BRANCH;
143
+ }
144
+
145
+ function readAheadBehindCounts(worktreePath, branch, baseBranch) {
146
+ const normalizedWorktreePath = toNonEmptyString(worktreePath);
147
+ const normalizedBranch = toNonEmptyString(branch);
148
+ const normalizedBaseBranch = toNonEmptyString(baseBranch, DEFAULT_BASE_BRANCH);
149
+ const compareRef = `origin/${normalizedBaseBranch}`;
150
+
151
+ if (!normalizedWorktreePath || !normalizedBranch) {
152
+ return {
153
+ compareRef,
154
+ aheadCount: null,
155
+ behindCount: null,
156
+ };
157
+ }
158
+
159
+ const lines = runGitLines(normalizedWorktreePath, [
160
+ 'rev-list',
161
+ '--left-right',
162
+ '--count',
163
+ `${normalizedBranch}...${compareRef}`,
164
+ ]);
165
+ const match = Array.isArray(lines) && typeof lines[0] === 'string'
166
+ ? lines[0].trim().match(/^(\d+)\s+(\d+)$/)
167
+ : null;
168
+ if (!match) {
169
+ return {
170
+ compareRef,
171
+ aheadCount: null,
172
+ behindCount: null,
173
+ };
174
+ }
175
+
176
+ return {
177
+ compareRef,
178
+ aheadCount: Number.parseInt(match[1], 10),
179
+ behindCount: Number.parseInt(match[2], 10),
180
+ };
181
+ }
182
+
183
+ function sessionLogPath(repoRoot, branch) {
184
+ const normalizedRepoRoot = toNonEmptyString(repoRoot);
185
+ const normalizedBranch = toNonEmptyString(branch);
186
+ if (!normalizedRepoRoot || !normalizedBranch) {
187
+ return '';
188
+ }
189
+
190
+ return path.join(
191
+ path.resolve(normalizedRepoRoot),
192
+ LOGS_RELATIVE_DIR,
193
+ `agent-${sanitizeBranchForFile(normalizedBranch)}.log`,
194
+ );
195
+ }
196
+
197
+ function readLogTail(filePath, maxLines = DEFAULT_LOG_TAIL_LINE_COUNT) {
198
+ const normalizedFilePath = toNonEmptyString(filePath);
199
+ const normalizedMaxLines = toPositiveInteger(maxLines) || DEFAULT_LOG_TAIL_LINE_COUNT;
200
+ if (!normalizedFilePath || !fs.existsSync(normalizedFilePath)) {
201
+ return [];
202
+ }
203
+
204
+ try {
205
+ const lines = fs.readFileSync(normalizedFilePath, 'utf8').split(/\r?\n/);
206
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
207
+ lines.pop();
208
+ }
209
+ return lines.slice(-normalizedMaxLines);
210
+ } catch (_error) {
211
+ return [];
212
+ }
213
+ }
214
+
215
+ function readSessionHeldLocks(repoRoot, branch) {
216
+ const normalizedRepoRoot = toNonEmptyString(repoRoot);
217
+ const normalizedBranch = toNonEmptyString(branch);
218
+ if (!normalizedRepoRoot || !normalizedBranch) {
219
+ return [];
220
+ }
221
+
222
+ const parsed = readJsonFile(path.join(path.resolve(normalizedRepoRoot), LOCK_FILE_RELATIVE));
223
+ const locks = parsed?.locks;
224
+ if (!locks || typeof locks !== 'object' || Array.isArray(locks)) {
225
+ return [];
226
+ }
227
+
228
+ return Object.entries(locks)
229
+ .map(([rawRelativePath, entry]) => {
230
+ if (!entry || typeof entry !== 'object') {
231
+ return null;
232
+ }
233
+
234
+ const relativePath = normalizeRelativePath(rawRelativePath);
235
+ const ownerBranch = toNonEmptyString(entry.branch);
236
+ if (!relativePath || ownerBranch !== normalizedBranch) {
237
+ return null;
238
+ }
239
+
240
+ return {
241
+ relativePath,
242
+ claimedAt: toNonEmptyString(entry.claimed_at),
243
+ allowDelete: Boolean(entry.allow_delete),
244
+ };
245
+ })
246
+ .filter(Boolean)
247
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
248
+ }
249
+
250
+ function readSessionInspectData(session, options = {}) {
251
+ const repoRoot = toNonEmptyString(session?.repoRoot);
252
+ const branch = toNonEmptyString(session?.branch);
253
+ const worktreePath = toNonEmptyString(session?.worktreePath);
254
+ const baseBranch = readConfiguredBaseBranch(repoRoot);
255
+ const logPath = sessionLogPath(repoRoot, branch);
256
+ const logTailLines = readLogTail(logPath, options.logLines);
257
+
258
+ return {
259
+ baseBranch,
260
+ logPath,
261
+ logExists: Boolean(logPath) && fs.existsSync(logPath),
262
+ logTailLines,
263
+ logTailText: logTailLines.join('\n'),
264
+ heldLocks: readSessionHeldLocks(repoRoot, branch),
265
+ ...readAheadBehindCounts(worktreePath, branch, baseBranch),
266
+ };
267
+ }
268
+
96
269
  function normalizeIsoString(value, fallback = '') {
97
270
  const normalized = toNonEmptyString(value);
98
271
  if (!normalized) {
@@ -212,6 +385,9 @@ function parseRepoChangeLine(repoRoot, line) {
212
385
  || normalizedRelativePath.startsWith(`${LOCK_FILE_FILTER_PATH}/`)
213
386
  || normalizedRelativePath === ACTIVE_SESSIONS_FILTER_PREFIX
214
387
  || normalizedRelativePath.startsWith(`${ACTIVE_SESSIONS_FILTER_PREFIX}/`)
388
+ || MANAGED_WORKTREE_FILTER_PREFIXES.some((prefix) => (
389
+ normalizedRelativePath === prefix || normalizedRelativePath.startsWith(`${prefix}/`)
390
+ ))
215
391
  ) {
216
392
  return null;
217
393
  }
@@ -294,14 +470,80 @@ function collectWorktreeTrackedPaths(worktreePath) {
294
470
  .sort((left, right) => left.localeCompare(right));
295
471
  }
296
472
 
297
- function deriveLatestWorktreeFileActivity(worktreePath) {
473
+ function shouldSkipWorktreeActivityPath(relativePath) {
474
+ const normalized = normalizeRelativePath(relativePath);
475
+ if (!normalized || normalized === LOCK_FILE_RELATIVE || normalized === AGENT_WORKTREE_LOCK_FILE) {
476
+ return true;
477
+ }
478
+
479
+ return WORKTREE_ACTIVITY_SKIP_PREFIXES.some((prefix) => (
480
+ normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
481
+ ));
482
+ }
483
+
484
+ function worktreeActivityPathPriority(relativePath, recentPathsSet) {
485
+ if (recentPathsSet.has(relativePath)) {
486
+ return 0;
487
+ }
488
+ if (!relativePath.includes('/')) {
489
+ return 1;
490
+ }
491
+ if (WORKTREE_ACTIVITY_PRIORITY_PREFIXES.some((prefix) => relativePath.startsWith(prefix))) {
492
+ return 2;
493
+ }
494
+ return 3;
495
+ }
496
+
497
+ function collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths) {
498
+ const recentPaths = runGitLines(worktreePath, ['log', '-1', '--name-only', '--pretty=format:', '--', '.']) || [];
499
+ const filteredRecentPaths = [...new Set(recentPaths.map(normalizeRelativePath).filter(Boolean))]
500
+ .filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath));
501
+ const recentPathSet = new Set(filteredRecentPaths);
502
+ const prioritizedTrackedPaths = trackedPaths
503
+ .map(normalizeRelativePath)
504
+ .filter(Boolean)
505
+ .filter((relativePath) => !shouldSkipWorktreeActivityPath(relativePath))
506
+ .sort((left, right) => {
507
+ const priorityDelta = worktreeActivityPathPriority(left, recentPathSet)
508
+ - worktreeActivityPathPriority(right, recentPathSet);
509
+ if (priorityDelta !== 0) {
510
+ return priorityDelta;
511
+ }
512
+ return left.localeCompare(right);
513
+ });
514
+
515
+ return [...new Set([...filteredRecentPaths, ...prioritizedTrackedPaths])]
516
+ .slice(0, MAX_WORKTREE_ACTIVITY_STAT_PATHS);
517
+ }
518
+
519
+ function clearWorktreeActivityCache(worktreePath = '') {
520
+ const normalizedWorktreePath = toNonEmptyString(worktreePath);
521
+ if (!normalizedWorktreePath) {
522
+ worktreeActivityCache.clear();
523
+ return;
524
+ }
525
+ worktreeActivityCache.delete(path.resolve(normalizedWorktreePath));
526
+ }
527
+
528
+ function deriveLatestWorktreeFileActivity(worktreePath, options = {}) {
529
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
530
+ const useCache = options.useCache !== false;
531
+ const cacheKey = path.resolve(worktreePath);
532
+ if (useCache) {
533
+ const cached = worktreeActivityCache.get(cacheKey);
534
+ if (cached && (now - cached.checkedAtMs) < WORKTREE_ACTIVITY_CACHE_TTL_MS) {
535
+ return cached.latestMtimeMs;
536
+ }
537
+ }
538
+
298
539
  const trackedPaths = collectWorktreeTrackedPaths(worktreePath);
299
540
  if (!trackedPaths) {
300
541
  return null;
301
542
  }
302
543
 
544
+ const candidatePaths = collectWorktreeActivityCandidatePaths(worktreePath, trackedPaths);
303
545
  let latestMtimeMs = null;
304
- for (const relativePath of trackedPaths) {
546
+ for (const relativePath of candidatePaths) {
305
547
  const absolutePath = path.join(worktreePath, relativePath);
306
548
  try {
307
549
  const stats = fs.statSync(absolutePath);
@@ -316,6 +558,13 @@ function deriveLatestWorktreeFileActivity(worktreePath) {
316
558
  }
317
559
  }
318
560
 
561
+ if (useCache) {
562
+ worktreeActivityCache.set(cacheKey, {
563
+ checkedAtMs: now,
564
+ latestMtimeMs,
565
+ });
566
+ }
567
+
319
568
  return latestMtimeMs;
320
569
  }
321
570
 
@@ -323,6 +572,23 @@ function deriveSessionActivity(session, options = {}) {
323
572
  const now = Number.isFinite(options.now) ? options.now : Date.now();
324
573
  const pid = toPositiveInteger(session?.pid);
325
574
  const pidAlive = pid ? isPidAlive(pid) : null;
575
+ const heartbeatAt = normalizeIsoString(session?.lastHeartbeatAt);
576
+ const heartbeatMs = Date.parse(heartbeatAt);
577
+ if (heartbeatAt && Number.isFinite(heartbeatMs) && now - heartbeatMs > HEARTBEAT_STALE_MS) {
578
+ return {
579
+ activityKind: 'dead',
580
+ activityLabel: 'dead',
581
+ activityCountLabel: '',
582
+ activitySummary: `Heartbeat stale for ${formatElapsedFrom(heartbeatAt, now)}.`,
583
+ changeCount: 0,
584
+ changedPaths: [],
585
+ worktreeChangedPaths: [],
586
+ pidAlive,
587
+ lastFileActivityAt: '',
588
+ lastFileActivityLabel: '',
589
+ };
590
+ }
591
+
326
592
  const blockingLabel = deriveBlockingGitLabel(session.worktreePath);
327
593
  if (blockingLabel) {
328
594
  return {
@@ -332,6 +598,7 @@ function deriveSessionActivity(session, options = {}) {
332
598
  activitySummary: blockingLabel,
333
599
  changeCount: 0,
334
600
  changedPaths: [],
601
+ worktreeChangedPaths: [],
335
602
  pidAlive,
336
603
  lastFileActivityAt: '',
337
604
  lastFileActivityLabel: '',
@@ -346,6 +613,7 @@ function deriveSessionActivity(session, options = {}) {
346
613
  activitySummary: 'Recorded PID is not alive.',
347
614
  changeCount: 0,
348
615
  changedPaths: [],
616
+ worktreeChangedPaths: [],
349
617
  pidAlive,
350
618
  lastFileActivityAt: '',
351
619
  lastFileActivityLabel: '',
@@ -361,6 +629,7 @@ function deriveSessionActivity(session, options = {}) {
361
629
  activitySummary: 'Worktree activity unavailable.',
362
630
  changeCount: 0,
363
631
  changedPaths: [],
632
+ worktreeChangedPaths: [],
364
633
  pidAlive,
365
634
  lastFileActivityAt: '',
366
635
  lastFileActivityLabel: '',
@@ -368,6 +637,11 @@ function deriveSessionActivity(session, options = {}) {
368
637
  }
369
638
 
370
639
  if (worktreeChangedPaths.length > 0) {
640
+ const worktreeRelativePaths = [...new Set(worktreeChangedPaths
641
+ .map((relativePath) => normalizeRelativePath(relativePath))
642
+ .filter(Boolean))]
643
+ .sort((left, right) => left.localeCompare(right));
644
+ clearWorktreeActivityCache(session.worktreePath);
371
645
  const changedPaths = [...new Set(worktreeChangedPaths
372
646
  .map((relativePath) => normalizeRelativePath(
373
647
  path.relative(session.repoRoot, path.resolve(session.worktreePath, relativePath)),
@@ -382,13 +656,17 @@ function deriveSessionActivity(session, options = {}) {
382
656
  activitySummary: previewChangedPaths(worktreeChangedPaths),
383
657
  changeCount: worktreeChangedPaths.length,
384
658
  changedPaths,
659
+ worktreeChangedPaths: worktreeRelativePaths,
385
660
  pidAlive,
386
661
  lastFileActivityAt: '',
387
662
  lastFileActivityLabel: '',
388
663
  };
389
664
  }
390
665
 
391
- const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath);
666
+ const latestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, {
667
+ now,
668
+ useCache: options.useCache,
669
+ });
392
670
  const lastFileActivityAt = Number.isFinite(latestFileActivityMs)
393
671
  ? new Date(latestFileActivityMs).toISOString()
394
672
  : '';
@@ -407,6 +685,7 @@ function deriveSessionActivity(session, options = {}) {
407
685
  activitySummary: `Worktree clean. No file activity for ${lastFileActivityLabel}.`,
408
686
  changeCount: 0,
409
687
  changedPaths: [],
688
+ worktreeChangedPaths: [],
410
689
  pidAlive,
411
690
  lastFileActivityAt,
412
691
  lastFileActivityLabel,
@@ -424,6 +703,7 @@ function deriveSessionActivity(session, options = {}) {
424
703
  : 'Worktree clean.',
425
704
  changeCount: 0,
426
705
  changedPaths: [],
706
+ worktreeChangedPaths: [],
427
707
  pidAlive,
428
708
  lastFileActivityAt,
429
709
  lastFileActivityLabel,
@@ -436,6 +716,7 @@ function buildSessionRecord(input) {
436
716
  const branch = toNonEmptyString(input.branch);
437
717
  const pid = toPositiveInteger(input.pid);
438
718
  const startedAt = input.startedAt ? new Date(input.startedAt) : new Date();
719
+ const lastHeartbeatAt = input.lastHeartbeatAt ? new Date(input.lastHeartbeatAt) : new Date();
439
720
 
440
721
  if (!branch) {
441
722
  throw new Error('branch is required');
@@ -452,6 +733,9 @@ function buildSessionRecord(input) {
452
733
  if (Number.isNaN(startedAt.getTime())) {
453
734
  throw new Error('startedAt must be a valid date');
454
735
  }
736
+ if (Number.isNaN(lastHeartbeatAt.getTime())) {
737
+ throw new Error('lastHeartbeatAt must be a valid date');
738
+ }
455
739
 
456
740
  return {
457
741
  schemaVersion: SESSION_SCHEMA_VERSION,
@@ -467,6 +751,8 @@ function buildSessionRecord(input) {
467
751
  openspecTier: normalizeOpenSpecTier(input.openspecTier),
468
752
  taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
469
753
  startedAt: startedAt.toISOString(),
754
+ lastHeartbeatAt: lastHeartbeatAt.toISOString(),
755
+ state: normalizeAdvisoryState(input.state),
470
756
  };
471
757
  }
472
758
 
@@ -487,9 +773,17 @@ function normalizeSessionRecord(input, options = {}) {
487
773
  const branch = toNonEmptyString(input.branch);
488
774
  const worktreePath = toNonEmptyString(input.worktreePath);
489
775
  const startedAt = new Date(input.startedAt);
776
+ const lastHeartbeatAt = new Date(input.lastHeartbeatAt || input.startedAt);
490
777
  const pid = toPositiveInteger(input.pid);
491
778
 
492
- if (!repoRoot || !branch || !worktreePath || !pid || Number.isNaN(startedAt.getTime())) {
779
+ if (
780
+ !repoRoot
781
+ || !branch
782
+ || !worktreePath
783
+ || !pid
784
+ || Number.isNaN(startedAt.getTime())
785
+ || Number.isNaN(lastHeartbeatAt.getTime())
786
+ ) {
493
787
  return null;
494
788
  }
495
789
 
@@ -507,9 +801,12 @@ function normalizeSessionRecord(input, options = {}) {
507
801
  openspecTier: normalizeOpenSpecTier(input.openspecTier),
508
802
  taskRoutingReason: toNonEmptyString(input.taskRoutingReason),
509
803
  startedAt: startedAt.toISOString(),
804
+ lastHeartbeatAt: lastHeartbeatAt.toISOString(),
805
+ state: normalizeAdvisoryState(input.state, 'idle'),
510
806
  filePath: toNonEmptyString(options.filePath),
511
807
  label: deriveSessionLabel(branch, worktreePath),
512
808
  changedPaths: [],
809
+ worktreeChangedPaths: [],
513
810
  sourceKind: 'active-session',
514
811
  telemetryUpdatedAt: '',
515
812
  telemetrySource: '',
@@ -646,9 +943,12 @@ function buildWorktreeLockSession(repoRoot, worktreePath, lockPayload, options =
646
943
  openspecTier: '',
647
944
  taskRoutingReason: '',
648
945
  startedAt,
946
+ lastHeartbeatAt: '',
947
+ state: '',
649
948
  filePath: path.join(worktreePath, AGENT_WORKTREE_LOCK_FILE),
650
949
  label,
651
950
  changedPaths: [],
951
+ worktreeChangedPaths: [],
652
952
  sourceKind: 'worktree-lock',
653
953
  telemetryUpdatedAt: telemetryUpdatedAt || startedAt,
654
954
  telemetrySource: toNonEmptyString(lockPayload?.source, 'worktree-lock'),
@@ -782,6 +1082,7 @@ module.exports = {
782
1082
  SESSION_SCHEMA_VERSION,
783
1083
  activeSessionsDirForRepo,
784
1084
  buildSessionRecord,
1085
+ clearWorktreeActivityCache,
785
1086
  collectWorktreeChangedPaths,
786
1087
  collectWorktreeTrackedPaths,
787
1088
  deriveBlockingGitLabel,
@@ -798,7 +1099,13 @@ module.exports = {
798
1099
  readWorktreeLockSessions,
799
1100
  readRepoChanges,
800
1101
  deriveRepoChangeStatus,
1102
+ readAheadBehindCounts,
1103
+ readConfiguredBaseBranch,
1104
+ readLogTail,
801
1105
  resolveWorktreeGitDir,
1106
+ readSessionHeldLocks,
1107
+ readSessionInspectData,
1108
+ sessionLogPath,
802
1109
  sanitizeBranchForFile,
803
1110
  sessionFileNameForBranch,
804
1111
  sessionFilePathForBranch,