@ai-devkit/agent-manager 0.3.0 → 0.4.0

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.
@@ -1,10 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * Claude Code Adapter
4
- *
5
- * Detects running Claude Code agents by reading session files
6
- * from ~/.claude/ directory and correlating with running processes.
7
- */
8
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
3
  if (k2 === undefined) k2 = k;
10
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -42,337 +36,427 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
36
  exports.ClaudeCodeAdapter = void 0;
43
37
  const fs = __importStar(require("fs"));
44
38
  const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
45
40
  const AgentAdapter_1 = require("./AgentAdapter");
46
41
  const process_1 = require("../utils/process");
47
42
  const file_1 = require("../utils/file");
48
- var SessionEntryType;
49
- (function (SessionEntryType) {
50
- SessionEntryType["ASSISTANT"] = "assistant";
51
- SessionEntryType["USER"] = "user";
52
- SessionEntryType["PROGRESS"] = "progress";
53
- SessionEntryType["THINKING"] = "thinking";
54
- SessionEntryType["SYSTEM"] = "system";
55
- SessionEntryType["MESSAGE"] = "message";
56
- SessionEntryType["TEXT"] = "text";
57
- })(SessionEntryType || (SessionEntryType = {}));
58
43
  /**
59
44
  * Claude Code Adapter
60
45
  *
61
46
  * Detects Claude Code agents by:
62
47
  * 1. Finding running claude processes
63
- * 2. Reading session files from ~/.claude/projects/
64
- * 3. Matching sessions to processes via CWD
65
- * 4. Extracting status from session JSONL
66
- * 5. Extracting summary from history.jsonl
48
+ * 2. Getting process start times for accurate session matching
49
+ * 3. Reading bounded session files from ~/.claude/projects/
50
+ * 4. Matching sessions to processes via CWD then start time ranking
51
+ * 5. Extracting summary from last user message in session JSONL
67
52
  */
68
53
  class ClaudeCodeAdapter {
69
54
  constructor() {
70
55
  this.type = 'claude';
71
56
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
72
- this.claudeDir = path.join(homeDir, '.claude');
73
- this.projectsDir = path.join(this.claudeDir, 'projects');
74
- this.historyPath = path.join(this.claudeDir, 'history.jsonl');
57
+ this.projectsDir = path.join(homeDir, '.claude', 'projects');
75
58
  }
76
59
  /**
77
60
  * Check if this adapter can handle a given process
78
61
  */
79
62
  canHandle(processInfo) {
80
- return processInfo.command.toLowerCase().includes('claude');
63
+ return this.isClaudeExecutable(processInfo.command);
64
+ }
65
+ isClaudeExecutable(command) {
66
+ const executable = command.trim().split(/\s+/)[0] || '';
67
+ const base = path.basename(executable).toLowerCase();
68
+ return base === 'claude' || base === 'claude.exe';
81
69
  }
82
70
  /**
83
71
  * Detect running Claude Code agents
84
72
  */
85
73
  async detectAgents() {
86
- const claudeProcesses = (0, process_1.listProcesses)({ namePattern: 'claude' }).filter((processInfo) => this.canHandle(processInfo));
74
+ const claudeProcesses = this.listClaudeProcesses();
87
75
  if (claudeProcesses.length === 0) {
88
76
  return [];
89
77
  }
90
- const sessions = this.readSessions();
91
- const history = this.readHistory();
92
- const historyByProjectPath = this.indexHistoryByProjectPath(history);
93
- const historyBySessionId = new Map();
94
- for (const entry of history) {
95
- historyBySessionId.set(entry.sessionId, entry);
96
- }
97
- const sortedSessions = [...sessions].sort((a, b) => {
98
- const timeA = a.lastActive?.getTime() || 0;
99
- const timeB = b.lastActive?.getTime() || 0;
100
- return timeB - timeA;
101
- });
78
+ const processStartByPid = this.getProcessStartTimes(claudeProcesses.map((p) => p.pid));
79
+ const sessionScanLimit = this.calculateSessionScanLimit(claudeProcesses.length);
80
+ const sessions = this.readSessions(sessionScanLimit);
81
+ if (sessions.length === 0) {
82
+ return claudeProcesses.map((p) => this.mapProcessOnlyAgent(p, []));
83
+ }
84
+ const sortedSessions = [...sessions].sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime());
102
85
  const usedSessionIds = new Set();
103
86
  const assignedPids = new Set();
104
87
  const agents = [];
105
- this.assignSessionsForMode('cwd', claudeProcesses, sortedSessions, usedSessionIds, assignedPids, historyBySessionId, agents);
106
- this.assignHistoryEntriesForExactProcessCwd(claudeProcesses, assignedPids, historyByProjectPath, usedSessionIds, agents);
107
- this.assignSessionsForMode('project-parent', claudeProcesses, sortedSessions, usedSessionIds, assignedPids, historyBySessionId, agents);
88
+ const modes = ['cwd', 'missing-cwd', 'parent-child'];
89
+ for (const mode of modes) {
90
+ this.assignSessionsForMode(mode, claudeProcesses, sortedSessions, usedSessionIds, assignedPids, processStartByPid, agents);
91
+ }
108
92
  for (const processInfo of claudeProcesses) {
109
93
  if (assignedPids.has(processInfo.pid)) {
110
94
  continue;
111
95
  }
112
96
  assignedPids.add(processInfo.pid);
113
- agents.push(this.mapProcessOnlyAgent(processInfo, agents, historyByProjectPath, usedSessionIds));
97
+ agents.push(this.mapProcessOnlyAgent(processInfo, agents));
114
98
  }
115
99
  return agents;
116
100
  }
117
- assignHistoryEntriesForExactProcessCwd(claudeProcesses, assignedPids, historyByProjectPath, usedSessionIds, agents) {
118
- for (const processInfo of claudeProcesses) {
119
- if (assignedPids.has(processInfo.pid)) {
120
- continue;
121
- }
122
- const historyEntry = this.selectHistoryForProcess(processInfo.cwd || '', historyByProjectPath, usedSessionIds);
123
- if (!historyEntry) {
124
- continue;
125
- }
126
- assignedPids.add(processInfo.pid);
127
- usedSessionIds.add(historyEntry.sessionId);
128
- agents.push(this.mapHistoryToAgent(processInfo, historyEntry, agents));
129
- }
101
+ listClaudeProcesses() {
102
+ return (0, process_1.listProcesses)({ namePattern: 'claude' }).filter((p) => this.canHandle(p));
103
+ }
104
+ calculateSessionScanLimit(processCount) {
105
+ return Math.min(Math.max(processCount * ClaudeCodeAdapter.SESSION_SCAN_MULTIPLIER, ClaudeCodeAdapter.MIN_SESSION_SCAN), ClaudeCodeAdapter.MAX_SESSION_SCAN);
130
106
  }
131
- assignSessionsForMode(mode, claudeProcesses, sessions, usedSessionIds, assignedPids, historyBySessionId, agents) {
107
+ assignSessionsForMode(mode, claudeProcesses, sessions, usedSessionIds, assignedPids, processStartByPid, agents) {
132
108
  for (const processInfo of claudeProcesses) {
133
109
  if (assignedPids.has(processInfo.pid)) {
134
110
  continue;
135
111
  }
136
- const session = this.selectBestSession(processInfo, sessions, usedSessionIds, mode);
112
+ const session = this.selectBestSession(processInfo, sessions, usedSessionIds, processStartByPid, mode);
137
113
  if (!session) {
138
114
  continue;
139
115
  }
140
116
  usedSessionIds.add(session.sessionId);
141
117
  assignedPids.add(processInfo.pid);
142
- agents.push(this.mapSessionToAgent(session, processInfo, historyBySessionId, agents));
118
+ agents.push(this.mapSessionToAgent(session, processInfo, agents));
143
119
  }
144
120
  }
145
- selectBestSession(processInfo, sessions, usedSessionIds, mode) {
146
- const candidates = sessions.filter((session) => {
147
- if (usedSessionIds.has(session.sessionId)) {
148
- return false;
149
- }
150
- if (mode === 'cwd') {
151
- return this.pathEquals(processInfo.cwd, session.lastCwd)
152
- || this.pathEquals(processInfo.cwd, session.projectPath);
153
- }
154
- if (mode === 'project-parent') {
155
- return this.isChildPath(processInfo.cwd, session.projectPath)
156
- || this.isChildPath(processInfo.cwd, session.lastCwd);
157
- }
158
- return false;
159
- });
160
- if (candidates.length === 0) {
161
- return null;
162
- }
163
- if (mode !== 'project-parent') {
164
- return candidates[0];
165
- }
166
- return candidates.sort((a, b) => {
167
- const depthA = Math.max(this.pathDepth(a.projectPath), this.pathDepth(a.lastCwd));
168
- const depthB = Math.max(this.pathDepth(b.projectPath), this.pathDepth(b.lastCwd));
169
- if (depthA !== depthB) {
170
- return depthB - depthA;
171
- }
172
- const lastActiveA = a.lastActive?.getTime() || 0;
173
- const lastActiveB = b.lastActive?.getTime() || 0;
174
- return lastActiveB - lastActiveA;
175
- })[0];
176
- }
177
- mapSessionToAgent(session, processInfo, historyBySessionId, existingAgents) {
178
- const historyEntry = historyBySessionId.get(session.sessionId);
121
+ mapSessionToAgent(session, processInfo, existingAgents) {
179
122
  return {
180
123
  name: this.generateAgentName(session, existingAgents),
181
124
  type: this.type,
182
125
  status: this.determineStatus(session),
183
- summary: historyEntry?.display || 'Session started',
126
+ summary: session.lastUserMessage || 'Session started',
184
127
  pid: processInfo.pid,
185
128
  projectPath: session.projectPath || processInfo.cwd || '',
186
129
  sessionId: session.sessionId,
187
130
  slug: session.slug,
188
- lastActive: session.lastActive || new Date(),
131
+ lastActive: session.lastActive,
189
132
  };
190
133
  }
191
- mapProcessOnlyAgent(processInfo, existingAgents, historyByProjectPath, usedSessionIds) {
192
- const projectPath = processInfo.cwd || '';
193
- const historyEntry = this.selectHistoryForProcess(projectPath, historyByProjectPath, usedSessionIds);
194
- const sessionId = historyEntry?.sessionId || `pid-${processInfo.pid}`;
195
- const lastActive = historyEntry ? new Date(historyEntry.timestamp) : new Date();
196
- if (historyEntry) {
197
- usedSessionIds.add(historyEntry.sessionId);
198
- }
199
- const processSession = {
200
- sessionId,
201
- projectPath,
202
- lastCwd: projectPath,
203
- sessionLogPath: '',
204
- lastActive,
205
- };
134
+ mapProcessOnlyAgent(processInfo, existingAgents) {
135
+ const processCwd = processInfo.cwd || '';
136
+ const projectName = path.basename(processCwd) || 'claude';
137
+ const hasDuplicate = existingAgents.some((a) => a.projectPath === processCwd);
206
138
  return {
207
- name: this.generateAgentName(processSession, existingAgents),
139
+ name: hasDuplicate ? `${projectName} (pid-${processInfo.pid})` : projectName,
208
140
  type: this.type,
209
- status: AgentAdapter_1.AgentStatus.RUNNING,
210
- summary: historyEntry?.display || 'Claude process running',
141
+ status: AgentAdapter_1.AgentStatus.IDLE,
142
+ summary: 'Unknown',
211
143
  pid: processInfo.pid,
212
- projectPath,
213
- sessionId: processSession.sessionId,
214
- lastActive: processSession.lastActive || new Date(),
144
+ projectPath: processCwd,
145
+ sessionId: `pid-${processInfo.pid}`,
146
+ lastActive: new Date(),
215
147
  };
216
148
  }
217
- mapHistoryToAgent(processInfo, historyEntry, existingAgents) {
218
- const projectPath = processInfo.cwd || historyEntry.project;
219
- const historySession = {
220
- sessionId: historyEntry.sessionId,
221
- projectPath,
222
- lastCwd: projectPath,
223
- sessionLogPath: '',
224
- lastActive: new Date(historyEntry.timestamp),
225
- };
226
- return {
227
- name: this.generateAgentName(historySession, existingAgents),
228
- type: this.type,
229
- status: AgentAdapter_1.AgentStatus.RUNNING,
230
- summary: historyEntry.display || 'Claude process running',
231
- pid: processInfo.pid,
232
- projectPath,
233
- sessionId: historySession.sessionId,
234
- lastActive: historySession.lastActive || new Date(),
235
- };
149
+ selectBestSession(processInfo, sessions, usedSessionIds, processStartByPid, mode) {
150
+ const candidates = this.filterCandidateSessions(processInfo, sessions, usedSessionIds, mode);
151
+ if (candidates.length === 0) {
152
+ return undefined;
153
+ }
154
+ const processStart = processStartByPid.get(processInfo.pid);
155
+ if (!processStart) {
156
+ return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
157
+ }
158
+ const best = this.rankCandidatesByStartTime(candidates, processStart)[0];
159
+ if (!best) {
160
+ return undefined;
161
+ }
162
+ // In early modes (cwd/missing-cwd), defer assignment when the best
163
+ // candidate is outside start-time tolerance — a closer match may
164
+ // exist in parent-child mode (e.g., worktree sessions).
165
+ if (mode !== 'parent-child') {
166
+ const diffMs = Math.abs(best.sessionStart.getTime() - processStart.getTime());
167
+ if (diffMs > ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS) {
168
+ return undefined;
169
+ }
170
+ }
171
+ return best;
236
172
  }
237
- indexHistoryByProjectPath(historyEntries) {
238
- const grouped = new Map();
239
- for (const entry of historyEntries) {
240
- const key = this.normalizePath(entry.project);
241
- const list = grouped.get(key) || [];
242
- list.push(entry);
243
- grouped.set(key, list);
244
- }
245
- for (const [key, list] of grouped.entries()) {
246
- grouped.set(key, [...list].sort((a, b) => b.timestamp - a.timestamp));
247
- }
248
- return grouped;
173
+ filterCandidateSessions(processInfo, sessions, usedSessionIds, mode) {
174
+ return sessions.filter((session) => {
175
+ if (usedSessionIds.has(session.sessionId)) {
176
+ return false;
177
+ }
178
+ if (mode === 'cwd') {
179
+ return (this.pathEquals(processInfo.cwd, session.projectPath) ||
180
+ this.pathEquals(processInfo.cwd, session.lastCwd));
181
+ }
182
+ if (mode === 'missing-cwd') {
183
+ return !session.projectPath;
184
+ }
185
+ // parent-child mode: match if process CWD equals, is under, or is
186
+ // a parent of session project/lastCwd. This also catches exact CWD
187
+ // matches that were deferred from `cwd` mode due to start-time tolerance.
188
+ return (this.pathRelated(processInfo.cwd, session.projectPath) ||
189
+ this.pathRelated(processInfo.cwd, session.lastCwd));
190
+ });
249
191
  }
250
- selectHistoryForProcess(processCwd, historyByProjectPath, usedSessionIds) {
251
- if (!processCwd) {
252
- return undefined;
192
+ rankCandidatesByStartTime(candidates, processStart) {
193
+ const toleranceMs = ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
194
+ return candidates
195
+ .map((session) => {
196
+ const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
197
+ const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
198
+ return {
199
+ session,
200
+ rank: outsideTolerance,
201
+ diffMs,
202
+ recency: session.lastActive.getTime(),
203
+ };
204
+ })
205
+ .sort((a, b) => {
206
+ if (a.rank !== b.rank)
207
+ return a.rank - b.rank;
208
+ // Within tolerance (rank 0): prefer most recently active session.
209
+ // The exact diff is noise — a 6s vs 45s difference is meaningless,
210
+ // but the session with recent activity is more likely the real one.
211
+ if (a.rank === 0)
212
+ return b.recency - a.recency;
213
+ // Outside tolerance: prefer smallest time difference, then recency.
214
+ if (a.diffMs !== b.diffMs)
215
+ return a.diffMs - b.diffMs;
216
+ return b.recency - a.recency;
217
+ })
218
+ .map((ranked) => ranked.session);
219
+ }
220
+ getProcessStartTimes(pids) {
221
+ if (pids.length === 0 || process.env.JEST_WORKER_ID) {
222
+ return new Map();
253
223
  }
254
- const candidates = historyByProjectPath.get(this.normalizePath(processCwd)) || [];
255
- return candidates.find((entry) => !usedSessionIds.has(entry.sessionId));
224
+ try {
225
+ const output = (0, child_process_1.execSync)(`ps -o pid=,etime= -p ${pids.join(',')}`, { encoding: 'utf-8' });
226
+ const nowMs = Date.now();
227
+ const startTimes = new Map();
228
+ for (const rawLine of output.split('\n')) {
229
+ const line = rawLine.trim();
230
+ if (!line)
231
+ continue;
232
+ const parts = line.split(/\s+/);
233
+ if (parts.length < 2)
234
+ continue;
235
+ const pid = Number.parseInt(parts[0], 10);
236
+ const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
237
+ if (!Number.isFinite(pid) || elapsedSeconds === null)
238
+ continue;
239
+ startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
240
+ }
241
+ return startTimes;
242
+ }
243
+ catch {
244
+ return new Map();
245
+ }
246
+ }
247
+ parseElapsedSeconds(etime) {
248
+ const match = etime
249
+ .trim()
250
+ .match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
251
+ if (!match) {
252
+ return null;
253
+ }
254
+ const days = Number.parseInt(match[1] || '0', 10);
255
+ const hours = Number.parseInt(match[2] || '0', 10);
256
+ const minutes = Number.parseInt(match[3] || '0', 10);
257
+ const seconds = Number.parseInt(match[4] || '0', 10);
258
+ return ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
256
259
  }
257
260
  /**
258
- * Read all Claude Code sessions
261
+ * Read Claude Code sessions with bounded scanning
259
262
  */
260
- readSessions() {
263
+ readSessions(limit) {
264
+ const sessionFiles = this.findSessionFiles(limit);
265
+ const sessions = [];
266
+ for (const file of sessionFiles) {
267
+ try {
268
+ const session = this.readSession(file.filePath, file.projectPath);
269
+ if (session) {
270
+ sessions.push(session);
271
+ }
272
+ }
273
+ catch (error) {
274
+ console.error(`Failed to parse Claude session ${file.filePath}:`, error);
275
+ }
276
+ }
277
+ return sessions;
278
+ }
279
+ /**
280
+ * Find session files bounded by mtime, sorted most-recent first
281
+ */
282
+ findSessionFiles(limit) {
261
283
  if (!fs.existsSync(this.projectsDir)) {
262
284
  return [];
263
285
  }
264
- const sessions = [];
265
- const projectDirs = fs.readdirSync(this.projectsDir);
266
- for (const dirName of projectDirs) {
286
+ const files = [];
287
+ for (const dirName of fs.readdirSync(this.projectsDir)) {
267
288
  if (dirName.startsWith('.')) {
268
289
  continue;
269
290
  }
270
291
  const projectDir = path.join(this.projectsDir, dirName);
271
- if (!fs.statSync(projectDir).isDirectory()) {
272
- continue;
273
- }
274
- // Read sessions-index.json to get original project path
275
- const indexPath = path.join(projectDir, 'sessions-index.json');
276
- if (!fs.existsSync(indexPath)) {
277
- continue;
292
+ try {
293
+ if (!fs.statSync(projectDir).isDirectory())
294
+ continue;
278
295
  }
279
- const sessionsIndex = (0, file_1.readJson)(indexPath);
280
- if (!sessionsIndex) {
281
- console.error(`Failed to parse ${indexPath}`);
296
+ catch {
282
297
  continue;
283
298
  }
284
- const sessionFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
285
- for (const sessionFile of sessionFiles) {
286
- const sessionId = sessionFile.replace('.jsonl', '');
287
- const sessionLogPath = path.join(projectDir, sessionFile);
299
+ const indexPath = path.join(projectDir, 'sessions-index.json');
300
+ const index = (0, file_1.readJson)(indexPath);
301
+ const projectPath = index?.originalPath || '';
302
+ for (const entry of fs.readdirSync(projectDir)) {
303
+ if (!entry.endsWith('.jsonl')) {
304
+ continue;
305
+ }
306
+ const filePath = path.join(projectDir, entry);
288
307
  try {
289
- const sessionData = this.readSessionLog(sessionLogPath);
290
- sessions.push({
291
- sessionId,
292
- projectPath: sessionsIndex.originalPath,
293
- lastCwd: sessionData.lastCwd,
294
- slug: sessionData.slug,
295
- sessionLogPath,
296
- lastEntry: sessionData.lastEntry,
297
- lastActive: sessionData.lastActive,
308
+ files.push({
309
+ filePath,
310
+ projectPath,
311
+ mtimeMs: fs.statSync(filePath).mtimeMs,
298
312
  });
299
313
  }
300
- catch (error) {
301
- console.error(`Failed to read session ${sessionId}:`, error);
314
+ catch {
302
315
  continue;
303
316
  }
304
317
  }
305
318
  }
306
- return sessions;
319
+ // Ensure breadth: include at least the most recent session per project,
320
+ // then fill remaining slots with globally most-recent sessions.
321
+ const sorted = files.sort((a, b) => b.mtimeMs - a.mtimeMs);
322
+ const result = [];
323
+ const seenProjects = new Set();
324
+ // First pass: one most-recent session per project directory
325
+ for (const file of sorted) {
326
+ const projDir = path.dirname(file.filePath);
327
+ if (!seenProjects.has(projDir)) {
328
+ seenProjects.add(projDir);
329
+ result.push(file);
330
+ }
331
+ }
332
+ // Second pass: fill remaining slots with globally most-recent
333
+ if (result.length < limit) {
334
+ const resultSet = new Set(result.map((f) => f.filePath));
335
+ for (const file of sorted) {
336
+ if (result.length >= limit)
337
+ break;
338
+ if (!resultSet.has(file.filePath)) {
339
+ result.push(file);
340
+ }
341
+ }
342
+ }
343
+ return result.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
307
344
  }
308
345
  /**
309
- * Read a session JSONL file
310
- * Only reads last 100 lines for performance with large files
346
+ * Parse a single session file into ClaudeSession
311
347
  */
312
- readSessionLog(logPath) {
313
- const lines = (0, file_1.readLastLines)(logPath, 100);
348
+ readSession(filePath, projectPath) {
349
+ const sessionId = path.basename(filePath, '.jsonl');
350
+ let content;
351
+ try {
352
+ content = fs.readFileSync(filePath, 'utf-8');
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ const allLines = content.trim().split('\n');
358
+ if (allLines.length === 0) {
359
+ return null;
360
+ }
361
+ // Parse first line for sessionStart.
362
+ // Claude Code may emit a "file-history-snapshot" as the first entry, which
363
+ // stores its timestamp inside "snapshot.timestamp" rather than at the root.
364
+ let sessionStart = null;
365
+ try {
366
+ const firstEntry = JSON.parse(allLines[0]);
367
+ const rawTs = firstEntry.timestamp || firstEntry.snapshot?.timestamp;
368
+ if (rawTs) {
369
+ const ts = new Date(rawTs);
370
+ if (!Number.isNaN(ts.getTime())) {
371
+ sessionStart = ts;
372
+ }
373
+ }
374
+ }
375
+ catch {
376
+ /* skip */
377
+ }
378
+ // Parse all lines for session state (file already in memory)
314
379
  let slug;
315
- let lastEntry;
380
+ let lastEntryType;
316
381
  let lastActive;
317
382
  let lastCwd;
318
- for (const line of lines) {
383
+ let isInterrupted = false;
384
+ let lastUserMessage;
385
+ for (const line of allLines) {
319
386
  try {
320
387
  const entry = JSON.parse(line);
388
+ if (entry.timestamp) {
389
+ const ts = new Date(entry.timestamp);
390
+ if (!Number.isNaN(ts.getTime())) {
391
+ lastActive = ts;
392
+ }
393
+ }
321
394
  if (entry.slug && !slug) {
322
395
  slug = entry.slug;
323
396
  }
324
- lastEntry = entry;
325
- if (entry.timestamp) {
326
- lastActive = new Date(entry.timestamp);
327
- }
328
397
  if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
329
398
  lastCwd = entry.cwd;
330
399
  }
400
+ if (entry.type && !this.isMetadataEntryType(entry.type)) {
401
+ lastEntryType = entry.type;
402
+ if (entry.type === 'user') {
403
+ const msgContent = entry.message?.content;
404
+ isInterrupted =
405
+ Array.isArray(msgContent) &&
406
+ msgContent.some((c) => (c.type === 'text' &&
407
+ c.text?.includes('[Request interrupted')) ||
408
+ (c.type === 'tool_result' &&
409
+ c.content?.includes('[Request interrupted')));
410
+ // Extract user message text for summary fallback
411
+ const text = this.extractUserMessageText(msgContent);
412
+ if (text) {
413
+ lastUserMessage = text;
414
+ }
415
+ }
416
+ else {
417
+ isInterrupted = false;
418
+ }
419
+ }
331
420
  }
332
- catch (error) {
421
+ catch {
333
422
  continue;
334
423
  }
335
424
  }
336
- return { slug, lastEntry, lastActive, lastCwd };
337
- }
338
- /**
339
- * Read history.jsonl for user prompts
340
- * Only reads last 100 lines for performance
341
- */
342
- readHistory() {
343
- return (0, file_1.readJsonLines)(this.historyPath, 100);
425
+ return {
426
+ sessionId,
427
+ projectPath: projectPath || lastCwd || '',
428
+ lastCwd,
429
+ slug,
430
+ sessionStart: sessionStart || lastActive || new Date(),
431
+ lastActive: lastActive || new Date(),
432
+ lastEntryType,
433
+ isInterrupted,
434
+ lastUserMessage,
435
+ };
344
436
  }
345
437
  /**
346
- * Determine agent status from session entry
438
+ * Determine agent status from session state
347
439
  */
348
440
  determineStatus(session) {
349
- if (!session.lastEntry) {
441
+ if (!session.lastEntryType) {
350
442
  return AgentAdapter_1.AgentStatus.UNKNOWN;
351
443
  }
352
- const entryType = session.lastEntry.type;
353
- const lastActive = session.lastActive || new Date(0);
354
- const ageMinutes = (Date.now() - lastActive.getTime()) / 1000 / 60;
355
- if (ageMinutes > ClaudeCodeAdapter.IDLE_THRESHOLD_MINUTES) {
356
- return AgentAdapter_1.AgentStatus.IDLE;
444
+ // No age-based IDLE override: every agent in the list is backed by
445
+ // a running process (found via ps), so the entry type is the best
446
+ // indicator of actual state.
447
+ if (session.lastEntryType === 'user') {
448
+ return session.isInterrupted
449
+ ? AgentAdapter_1.AgentStatus.WAITING
450
+ : AgentAdapter_1.AgentStatus.RUNNING;
357
451
  }
358
- if (entryType === SessionEntryType.USER) {
359
- // Check if user interrupted manually - this puts agent back in waiting state
360
- const content = session.lastEntry.message?.content;
361
- if (Array.isArray(content)) {
362
- const isInterrupted = content.some(c => (c.type === SessionEntryType.TEXT && c.text?.includes('[Request interrupted')) ||
363
- (c.type === 'tool_result' && c.content?.includes('[Request interrupted')));
364
- if (isInterrupted)
365
- return AgentAdapter_1.AgentStatus.WAITING;
366
- }
452
+ if (session.lastEntryType === 'progress' ||
453
+ session.lastEntryType === 'thinking') {
367
454
  return AgentAdapter_1.AgentStatus.RUNNING;
368
455
  }
369
- if (entryType === SessionEntryType.PROGRESS || entryType === SessionEntryType.THINKING) {
370
- return AgentAdapter_1.AgentStatus.RUNNING;
371
- }
372
- else if (entryType === SessionEntryType.ASSISTANT) {
456
+ if (session.lastEntryType === 'assistant') {
373
457
  return AgentAdapter_1.AgentStatus.WAITING;
374
458
  }
375
- else if (entryType === SessionEntryType.SYSTEM) {
459
+ if (session.lastEntryType === 'system') {
376
460
  return AgentAdapter_1.AgentStatus.IDLE;
377
461
  }
378
462
  return AgentAdapter_1.AgentStatus.UNKNOWN;
@@ -383,21 +467,22 @@ class ClaudeCodeAdapter {
383
467
  */
384
468
  generateAgentName(session, existingAgents) {
385
469
  const projectName = path.basename(session.projectPath) || 'claude';
386
- const sameProjectAgents = existingAgents.filter(a => a.projectPath === session.projectPath);
470
+ const sameProjectAgents = existingAgents.filter((a) => a.projectPath === session.projectPath);
387
471
  if (sameProjectAgents.length === 0) {
388
472
  return projectName;
389
473
  }
390
- // Multiple sessions for same project, append slug
391
474
  if (session.slug) {
392
- // Use first word of slug for brevity (with safety check for format)
393
475
  const slugPart = session.slug.includes('-')
394
476
  ? session.slug.split('-')[0]
395
477
  : session.slug.slice(0, 8);
396
478
  return `${projectName} (${slugPart})`;
397
479
  }
398
- // No slug available, use session ID prefix
399
480
  return `${projectName} (${session.sessionId.slice(0, 8)})`;
400
481
  }
482
+ /** Check if two paths are equal, or one is a parent/child of the other. */
483
+ pathRelated(a, b) {
484
+ return this.pathEquals(a, b) || this.isChildPath(a, b) || this.isChildPath(b, a);
485
+ }
401
486
  pathEquals(a, b) {
402
487
  if (!a || !b) {
403
488
  return false;
@@ -410,7 +495,73 @@ class ClaudeCodeAdapter {
410
495
  }
411
496
  const normalizedChild = this.normalizePath(child);
412
497
  const normalizedParent = this.normalizePath(parent);
413
- return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
498
+ return normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
499
+ }
500
+ /**
501
+ * Extract meaningful text from a user message content.
502
+ * Handles string and array formats, skill command expansion, and noise filtering.
503
+ */
504
+ extractUserMessageText(content) {
505
+ if (!content) {
506
+ return undefined;
507
+ }
508
+ let raw;
509
+ if (typeof content === 'string') {
510
+ raw = content.trim();
511
+ }
512
+ else if (Array.isArray(content)) {
513
+ for (const block of content) {
514
+ if (block.type === 'text' && block.text?.trim()) {
515
+ raw = block.text.trim();
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ if (!raw) {
521
+ return undefined;
522
+ }
523
+ // Skill slash-command: extract /command-name and args
524
+ if (raw.startsWith('<command-message>')) {
525
+ return this.parseCommandMessage(raw);
526
+ }
527
+ // Expanded skill content: extract ARGUMENTS line if present, skip otherwise
528
+ if (raw.startsWith('Base directory for this skill:')) {
529
+ const argsMatch = raw.match(/\nARGUMENTS:\s*(.+)/);
530
+ return argsMatch?.[1]?.trim() || undefined;
531
+ }
532
+ // Filter noise
533
+ if (this.isNoiseMessage(raw)) {
534
+ return undefined;
535
+ }
536
+ return raw;
537
+ }
538
+ /**
539
+ * Parse a <command-message> string into "/command args" format.
540
+ */
541
+ parseCommandMessage(raw) {
542
+ const nameMatch = raw.match(/<command-name>([^<]+)<\/command-name>/);
543
+ const argsMatch = raw.match(/<command-args>([^<]+)<\/command-args>/);
544
+ const name = nameMatch?.[1]?.trim();
545
+ if (!name) {
546
+ return undefined;
547
+ }
548
+ const args = argsMatch?.[1]?.trim();
549
+ return args ? `${name} ${args}` : name;
550
+ }
551
+ /**
552
+ * Check if a message is noise (not a meaningful user intent).
553
+ */
554
+ isNoiseMessage(text) {
555
+ return (text.startsWith('[Request interrupted') ||
556
+ text === 'Tool loaded.' ||
557
+ text.startsWith('This session is being continued'));
558
+ }
559
+ /**
560
+ * Check if an entry type is metadata (not conversation state).
561
+ * These should not overwrite lastEntryType used for status determination.
562
+ */
563
+ isMetadataEntryType(type) {
564
+ return type === 'last-prompt' || type === 'file-history-snapshot';
414
565
  }
415
566
  normalizePath(value) {
416
567
  const resolved = path.resolve(value);
@@ -419,14 +570,12 @@ class ClaudeCodeAdapter {
419
570
  }
420
571
  return resolved;
421
572
  }
422
- pathDepth(value) {
423
- if (!value) {
424
- return 0;
425
- }
426
- return this.normalizePath(value).split(path.sep).filter(Boolean).length;
427
- }
428
573
  }
429
574
  exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
430
- /** Threshold in minutes before considering a session idle */
431
- ClaudeCodeAdapter.IDLE_THRESHOLD_MINUTES = 5;
575
+ /** Limit session parsing per run to keep list latency bounded. */
576
+ ClaudeCodeAdapter.MIN_SESSION_SCAN = 12;
577
+ ClaudeCodeAdapter.MAX_SESSION_SCAN = 40;
578
+ ClaudeCodeAdapter.SESSION_SCAN_MULTIPLIER = 4;
579
+ /** Matching tolerance between process start time and session start time. */
580
+ ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS = 2 * 60 * 1000;
432
581
  //# sourceMappingURL=ClaudeCodeAdapter.js.map