@orgloop/agentctl 1.0.1 → 1.2.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.
package/dist/cli.js CHANGED
@@ -9,21 +9,34 @@ import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { Command } from "commander";
11
11
  import { ClaudeCodeAdapter } from "./adapters/claude-code.js";
12
+ import { CodexAdapter } from "./adapters/codex.js";
12
13
  import { OpenClawAdapter } from "./adapters/openclaw.js";
14
+ import { OpenCodeAdapter } from "./adapters/opencode.js";
15
+ import { PiAdapter } from "./adapters/pi.js";
16
+ import { PiRustAdapter } from "./adapters/pi-rust.js";
13
17
  import { DaemonClient } from "./client/daemon-client.js";
14
18
  import { runHook } from "./hooks.js";
19
+ import { orchestrateLaunch, parseAdapterSlots, } from "./launch-orchestrator.js";
20
+ import { expandMatrix, parseMatrixFile } from "./matrix-parser.js";
15
21
  import { mergeSession } from "./merge.js";
16
22
  import { createWorktree } from "./worktree.js";
17
23
  const adapters = {
18
24
  "claude-code": new ClaudeCodeAdapter(),
25
+ codex: new CodexAdapter(),
19
26
  openclaw: new OpenClawAdapter(),
27
+ opencode: new OpenCodeAdapter(),
28
+ pi: new PiAdapter(),
29
+ "pi-rust": new PiRustAdapter(),
20
30
  };
21
31
  const client = new DaemonClient();
22
32
  /**
23
33
  * Ensure the daemon is running. Auto-starts it if not.
24
34
  * Returns true if daemon is available after the call.
35
+ * Set AGENTCTL_NO_DAEMON=1 to skip daemon and use direct adapter mode.
25
36
  */
26
37
  async function ensureDaemon() {
38
+ if (process.env.AGENTCTL_NO_DAEMON === "1")
39
+ return false;
27
40
  if (await client.isRunning())
28
41
  return true;
29
42
  // Auto-start daemon in background
@@ -67,27 +80,33 @@ function getAllAdapters() {
67
80
  return Object.values(adapters);
68
81
  }
69
82
  // --- Formatters ---
70
- function formatSession(s) {
71
- return {
83
+ function formatSession(s, showGroup) {
84
+ const row = {
72
85
  ID: s.id.slice(0, 8),
73
86
  Status: s.status,
74
87
  Model: s.model || "-",
75
- CWD: s.cwd ? shortenPath(s.cwd) : "-",
76
- PID: s.pid?.toString() || "-",
77
- Started: timeAgo(s.startedAt),
78
- Prompt: (s.prompt || "-").slice(0, 60),
79
88
  };
89
+ if (showGroup)
90
+ row.Group = s.group || "-";
91
+ row.CWD = s.cwd ? shortenPath(s.cwd) : "-";
92
+ row.PID = s.pid?.toString() || "-";
93
+ row.Started = timeAgo(s.startedAt);
94
+ row.Prompt = (s.prompt || "-").slice(0, 60);
95
+ return row;
80
96
  }
81
- function formatRecord(s) {
82
- return {
97
+ function formatRecord(s, showGroup) {
98
+ const row = {
83
99
  ID: s.id.slice(0, 8),
84
100
  Status: s.status,
85
101
  Model: s.model || "-",
86
- CWD: s.cwd ? shortenPath(s.cwd) : "-",
87
- PID: s.pid?.toString() || "-",
88
- Started: timeAgo(new Date(s.startedAt)),
89
- Prompt: (s.prompt || "-").slice(0, 60),
90
102
  };
103
+ if (showGroup)
104
+ row.Group = s.group || "-";
105
+ row.CWD = s.cwd ? shortenPath(s.cwd) : "-";
106
+ row.PID = s.pid?.toString() || "-";
107
+ row.Started = timeAgo(new Date(s.startedAt));
108
+ row.Prompt = (s.prompt || "-").slice(0, 60);
109
+ return row;
91
110
  }
92
111
  function shortenPath(p) {
93
112
  const home = process.env.HOME || "";
@@ -153,6 +172,7 @@ function sessionToJson(s) {
153
172
  tokens: s.tokens,
154
173
  cost: s.cost,
155
174
  pid: s.pid,
175
+ group: s.group,
156
176
  meta: s.meta,
157
177
  };
158
178
  }
@@ -168,20 +188,27 @@ program
168
188
  .description("List agent sessions")
169
189
  .option("--adapter <name>", "Filter by adapter")
170
190
  .option("--status <status>", "Filter by status (running|stopped|idle|error)")
191
+ .option("--group <id>", "Filter by launch group (e.g. g-a1b2c3)")
171
192
  .option("-a, --all", "Include stopped sessions (last 7 days)")
172
193
  .option("--json", "Output as JSON")
173
194
  .action(async (opts) => {
174
195
  const daemonRunning = await ensureDaemon();
175
196
  if (daemonRunning) {
176
- const sessions = await client.call("session.list", {
197
+ let sessions = await client.call("session.list", {
177
198
  status: opts.status,
178
199
  all: opts.all,
200
+ adapter: opts.adapter,
201
+ group: opts.group,
179
202
  });
203
+ if (opts.adapter) {
204
+ sessions = sessions.filter((s) => s.adapter === opts.adapter);
205
+ }
180
206
  if (opts.json) {
181
207
  printJson(sessions);
182
208
  }
183
209
  else {
184
- printTable(sessions.map(formatRecord));
210
+ const hasGroups = sessions.some((s) => s.group);
211
+ printTable(sessions.map((s) => formatRecord(s, hasGroups)));
185
212
  }
186
213
  return;
187
214
  }
@@ -202,7 +229,8 @@ program
202
229
  printJson(sessions.map(sessionToJson));
203
230
  }
204
231
  else {
205
- printTable(sessions.map(formatSession));
232
+ const hasGroups = sessions.some((s) => s.group);
233
+ printTable(sessions.map((s) => formatSession(s, hasGroups)));
206
234
  }
207
235
  });
208
236
  // status
@@ -222,7 +250,7 @@ program
222
250
  printJson(session);
223
251
  }
224
252
  else {
225
- const fmt = formatRecord(session);
253
+ const fmt = formatRecord(session, !!session.group);
226
254
  for (const [k, v] of Object.entries(fmt)) {
227
255
  console.log(`${k.padEnd(10)} ${v}`);
228
256
  }
@@ -232,32 +260,37 @@ program
232
260
  }
233
261
  return;
234
262
  }
235
- catch (err) {
236
- console.error(err.message);
237
- process.exit(1);
263
+ catch {
264
+ // Daemon failed — fall through to direct adapter lookup
238
265
  }
239
266
  }
240
- // Direct fallback
241
- const adapter = getAdapter(opts.adapter);
242
- try {
243
- const session = await adapter.status(id);
244
- if (opts.json) {
245
- printJson(sessionToJson(session));
246
- }
247
- else {
248
- const fmt = formatSession(session);
249
- for (const [k, v] of Object.entries(fmt)) {
250
- console.log(`${k.padEnd(10)} ${v}`);
267
+ // Direct fallback: try specified adapter, or search all adapters
268
+ const statusAdapters = opts.adapter
269
+ ? [getAdapter(opts.adapter)]
270
+ : getAllAdapters();
271
+ for (const adapter of statusAdapters) {
272
+ try {
273
+ const session = await adapter.status(id);
274
+ if (opts.json) {
275
+ printJson(sessionToJson(session));
251
276
  }
252
- if (session.tokens) {
253
- console.log(`Tokens in: ${session.tokens.in}, out: ${session.tokens.out}`);
277
+ else {
278
+ const fmt = formatSession(session, !!session.group);
279
+ for (const [k, v] of Object.entries(fmt)) {
280
+ console.log(`${k.padEnd(10)} ${v}`);
281
+ }
282
+ if (session.tokens) {
283
+ console.log(`Tokens in: ${session.tokens.in}, out: ${session.tokens.out}`);
284
+ }
254
285
  }
286
+ return;
287
+ }
288
+ catch {
289
+ // Try next adapter
255
290
  }
256
291
  }
257
- catch (err) {
258
- console.error(err.message);
259
- process.exit(1);
260
- }
292
+ console.error(`Session not found: ${id}`);
293
+ process.exit(1);
261
294
  });
262
295
  // peek
263
296
  program
@@ -276,22 +309,39 @@ program
276
309
  console.log(output);
277
310
  return;
278
311
  }
312
+ catch {
313
+ // Daemon failed — fall through to direct adapter lookup
314
+ }
315
+ }
316
+ // Direct fallback: try specified adapter, or search all adapters
317
+ if (opts.adapter) {
318
+ const adapter = getAdapter(opts.adapter);
319
+ try {
320
+ const output = await adapter.peek(id, {
321
+ lines: Number.parseInt(opts.lines, 10),
322
+ });
323
+ console.log(output);
324
+ return;
325
+ }
279
326
  catch (err) {
280
327
  console.error(err.message);
281
328
  process.exit(1);
282
329
  }
283
330
  }
284
- const adapter = getAdapter(opts.adapter);
285
- try {
286
- const output = await adapter.peek(id, {
287
- lines: Number.parseInt(opts.lines, 10),
288
- });
289
- console.log(output);
290
- }
291
- catch (err) {
292
- console.error(err.message);
293
- process.exit(1);
331
+ for (const adapter of getAllAdapters()) {
332
+ try {
333
+ const output = await adapter.peek(id, {
334
+ lines: Number.parseInt(opts.lines, 10),
335
+ });
336
+ console.log(output);
337
+ return;
338
+ }
339
+ catch {
340
+ // Try next adapter
341
+ }
294
342
  }
343
+ console.error(`Session not found: ${id}`);
344
+ process.exit(1);
295
345
  });
296
346
  // stop
297
347
  program
@@ -356,7 +406,7 @@ program
356
406
  // launch
357
407
  program
358
408
  .command("launch [adapter]")
359
- .description("Launch a new agent session")
409
+ .description("Launch a new agent session (or multiple with --adapter flags)")
360
410
  .requiredOption("-p, --prompt <text>", "Prompt to send")
361
411
  .option("--spec <path>", "Spec file path")
362
412
  .option("--cwd <dir>", "Working directory")
@@ -364,13 +414,108 @@ program
364
414
  .option("--force", "Override directory locks")
365
415
  .option("--worktree <repo>", "Auto-create git worktree from this repo before launch")
366
416
  .option("--branch <name>", "Branch name for --worktree")
417
+ .option("--adapter <name>", "Adapter to launch (repeatable for parallel launch)", collectAdapter, [])
418
+ .option("--matrix <file>", "YAML matrix file for advanced sweep launch")
367
419
  .option("--on-create <script>", "Hook: run after session is created")
368
420
  .option("--on-complete <script>", "Hook: run after session completes")
369
421
  .option("--pre-merge <script>", "Hook: run before merge")
370
422
  .option("--post-merge <script>", "Hook: run after merge")
423
+ .allowUnknownOption() // Allow interleaved --adapter/--model for parseAdapterSlots
371
424
  .action(async (adapterName, opts) => {
372
425
  let cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
373
- const name = adapterName || "claude-code";
426
+ // Collect hooks
427
+ const hooks = opts.onCreate || opts.onComplete || opts.preMerge || opts.postMerge
428
+ ? {
429
+ onCreate: opts.onCreate,
430
+ onComplete: opts.onComplete,
431
+ preMerge: opts.preMerge,
432
+ postMerge: opts.postMerge,
433
+ }
434
+ : undefined;
435
+ // --- Multi-adapter / matrix detection ---
436
+ let slots = [];
437
+ if (opts.matrix) {
438
+ // Matrix file mode
439
+ try {
440
+ const matrixFile = await parseMatrixFile(opts.matrix);
441
+ slots = expandMatrix(matrixFile);
442
+ // Matrix can override cwd and prompt
443
+ if (matrixFile.cwd)
444
+ cwd = path.resolve(matrixFile.cwd);
445
+ }
446
+ catch (err) {
447
+ console.error(`Failed to parse matrix file: ${err.message}`);
448
+ process.exit(1);
449
+ }
450
+ }
451
+ else {
452
+ // Check for multi-adapter via raw argv parsing
453
+ // We need raw argv because commander can't handle interleaved
454
+ // --adapter A --model M1 --adapter B --model M2
455
+ const rawArgs = process.argv.slice(2);
456
+ const adapterCount = rawArgs.filter((a) => a === "--adapter" || a === "-A").length;
457
+ if (adapterCount > 1) {
458
+ // Multi-adapter mode: parse from raw args
459
+ slots = parseAdapterSlots(rawArgs);
460
+ }
461
+ else if (adapterCount === 1 && opts.adapter?.length === 1) {
462
+ // Single --adapter flag — could still be multi if model is specified
463
+ // but this is the normal single-adapter path via --adapter flag
464
+ }
465
+ }
466
+ // --- Parallel launch path ---
467
+ if (slots.length > 1) {
468
+ const daemonRunning = await ensureDaemon();
469
+ try {
470
+ let groupId = "";
471
+ const result = await orchestrateLaunch({
472
+ slots,
473
+ prompt: opts.prompt,
474
+ spec: opts.spec,
475
+ cwd,
476
+ hooks,
477
+ adapters,
478
+ onSessionLaunched: (slotResult) => {
479
+ // Track in daemon if available
480
+ if (daemonRunning && !slotResult.error) {
481
+ client
482
+ .call("session.launch.track", {
483
+ id: slotResult.sessionId,
484
+ adapter: slotResult.slot.adapter,
485
+ cwd: slotResult.cwd,
486
+ group: groupId,
487
+ })
488
+ .catch(() => {
489
+ // Best effort — session will be picked up by poll
490
+ });
491
+ }
492
+ },
493
+ onGroupCreated: (id) => {
494
+ groupId = id;
495
+ },
496
+ });
497
+ console.log(`\nLaunched ${result.results.length} sessions (group: ${result.groupId}):`);
498
+ for (const r of result.results) {
499
+ const label = r.slot.model
500
+ ? `${r.slot.adapter} (${r.slot.model})`
501
+ : r.slot.adapter;
502
+ if (r.error) {
503
+ console.log(` ✗ ${label} — ${r.error}`);
504
+ }
505
+ else {
506
+ console.log(` ${label} → ${shortenPath(r.cwd)} (${r.sessionId.slice(0, 8)})`);
507
+ }
508
+ }
509
+ }
510
+ catch (err) {
511
+ console.error(`Parallel launch failed: ${err.message}`);
512
+ process.exit(1);
513
+ }
514
+ return;
515
+ }
516
+ // --- Single adapter launch path (original behavior) ---
517
+ const name = slots.length === 1 ? slots[0].adapter : adapterName || "claude-code";
518
+ const model = slots.length === 1 && slots[0].model ? slots[0].model : opts.model;
374
519
  // FEAT-1: Worktree lifecycle
375
520
  let worktreeInfo;
376
521
  if (opts.worktree) {
@@ -391,15 +536,6 @@ program
391
536
  process.exit(1);
392
537
  }
393
538
  }
394
- // Collect hooks
395
- const hooks = opts.onCreate || opts.onComplete || opts.preMerge || opts.postMerge
396
- ? {
397
- onCreate: opts.onCreate,
398
- onComplete: opts.onComplete,
399
- preMerge: opts.preMerge,
400
- postMerge: opts.postMerge,
401
- }
402
- : undefined;
403
539
  const daemonRunning = await ensureDaemon();
404
540
  if (daemonRunning) {
405
541
  try {
@@ -408,7 +544,7 @@ program
408
544
  prompt: opts.prompt,
409
545
  cwd,
410
546
  spec: opts.spec,
411
- model: opts.model,
547
+ model,
412
548
  force: opts.force,
413
549
  worktree: worktreeInfo
414
550
  ? { repo: worktreeInfo.repo, branch: worktreeInfo.branch }
@@ -443,7 +579,7 @@ program
443
579
  prompt: opts.prompt,
444
580
  spec: opts.spec,
445
581
  cwd,
446
- model: opts.model,
582
+ model,
447
583
  hooks,
448
584
  });
449
585
  console.log(`Launched session ${session.id.slice(0, 8)} (PID: ${session.pid})`);
@@ -462,6 +598,10 @@ program
462
598
  process.exit(1);
463
599
  }
464
600
  });
601
+ /** Commander collect callback for repeatable --adapter */
602
+ function collectAdapter(value, previous) {
603
+ return previous.concat([value]);
604
+ }
465
605
  // events
466
606
  program
467
607
  .command("events")
@@ -560,6 +700,84 @@ program
560
700
  });
561
701
  }
562
702
  });
703
+ // --- Worktree subcommand ---
704
+ const worktreeCmd = new Command("worktree").description("Manage agentctl-created worktrees");
705
+ worktreeCmd
706
+ .command("list")
707
+ .description("List git worktrees for a repo")
708
+ .argument("<repo>", "Path to the main repo")
709
+ .option("--json", "Output as JSON")
710
+ .action(async (repo, opts) => {
711
+ const { listWorktrees } = await import("./worktree.js");
712
+ try {
713
+ const entries = await listWorktrees(repo);
714
+ // Filter to only non-bare worktrees (exclude the main worktree)
715
+ const worktrees = entries.filter((e) => !e.bare);
716
+ if (opts.json) {
717
+ printJson(worktrees);
718
+ return;
719
+ }
720
+ if (worktrees.length === 0) {
721
+ console.log("No worktrees found.");
722
+ return;
723
+ }
724
+ printTable(worktrees.map((e) => ({
725
+ Path: shortenPath(e.path),
726
+ Branch: e.branch || "-",
727
+ HEAD: e.head?.slice(0, 8) || "-",
728
+ })));
729
+ }
730
+ catch (err) {
731
+ console.error(err.message);
732
+ process.exit(1);
733
+ }
734
+ });
735
+ worktreeCmd
736
+ .command("clean")
737
+ .description("Remove a worktree and optionally its branch")
738
+ .argument("<path>", "Path to the worktree to remove")
739
+ .option("--repo <path>", "Main repo path (auto-detected if omitted)")
740
+ .option("--delete-branch", "Also delete the worktree's branch")
741
+ .action(async (worktreePath, opts) => {
742
+ const { cleanWorktree } = await import("./worktree.js");
743
+ const absPath = path.resolve(worktreePath);
744
+ let repo = opts.repo;
745
+ // Auto-detect repo from the worktree's .git file
746
+ if (!repo) {
747
+ try {
748
+ const gitFile = await fs.readFile(path.join(absPath, ".git"), "utf-8");
749
+ // .git file contains: gitdir: /path/to/repo/.git/worktrees/<name>
750
+ const match = gitFile.match(/gitdir:\s*(.+)/);
751
+ if (match) {
752
+ const gitDir = match[1].trim();
753
+ // Navigate up from .git/worktrees/<name> to the repo root
754
+ repo = path.resolve(gitDir, "..", "..", "..");
755
+ }
756
+ }
757
+ catch {
758
+ console.error("Cannot auto-detect repo. Use --repo to specify the main repository.");
759
+ process.exit(1);
760
+ }
761
+ }
762
+ if (!repo) {
763
+ console.error("Cannot determine repo path. Use --repo.");
764
+ process.exit(1);
765
+ }
766
+ try {
767
+ const result = await cleanWorktree(repo, absPath, {
768
+ deleteBranch: opts.deleteBranch,
769
+ });
770
+ console.log(`Removed worktree: ${shortenPath(result.removedPath)}`);
771
+ if (result.deletedBranch) {
772
+ console.log(`Deleted branch: ${result.deletedBranch}`);
773
+ }
774
+ }
775
+ catch (err) {
776
+ console.error(err.message);
777
+ process.exit(1);
778
+ }
779
+ });
780
+ program.addCommand(worktreeCmd);
563
781
  // --- Lock commands ---
564
782
  program
565
783
  .command("lock <directory>")
@@ -24,6 +24,7 @@ export interface AgentSession {
24
24
  };
25
25
  cost?: number;
26
26
  pid?: number;
27
+ group?: string;
27
28
  meta: Record<string, unknown>;
28
29
  }
29
30
  export interface LifecycleEvent {
@@ -5,7 +5,11 @@ import net from "node:net";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
8
+ import { CodexAdapter } from "../adapters/codex.js";
8
9
  import { OpenClawAdapter } from "../adapters/openclaw.js";
10
+ import { OpenCodeAdapter } from "../adapters/opencode.js";
11
+ import { PiAdapter } from "../adapters/pi.js";
12
+ import { PiRustAdapter } from "../adapters/pi-rust.js";
9
13
  import { migrateLocks } from "../migration/migrate-locks.js";
10
14
  import { FuseEngine } from "./fuse-engine.js";
11
15
  import { LockManager } from "./lock-manager.js";
@@ -32,7 +36,11 @@ export async function startDaemon(opts = {}) {
32
36
  // 5. Initialize subsystems
33
37
  const adapters = opts.adapters || {
34
38
  "claude-code": new ClaudeCodeAdapter(),
39
+ codex: new CodexAdapter(),
35
40
  openclaw: new OpenClawAdapter(),
41
+ opencode: new OpenCodeAdapter(),
42
+ pi: new PiAdapter(),
43
+ "pi-rust": new PiRustAdapter(),
36
44
  };
37
45
  const lockManager = new LockManager(state);
38
46
  const emitter = new EventEmitter();
@@ -145,11 +153,16 @@ function createRequestHandler(ctx) {
145
153
  return async (req) => {
146
154
  const params = (req.params || {});
147
155
  switch (req.method) {
148
- case "session.list":
149
- return ctx.sessionTracker.listSessions({
156
+ case "session.list": {
157
+ let sessions = ctx.sessionTracker.listSessions({
150
158
  status: params.status,
151
159
  all: params.all,
152
160
  });
161
+ if (params.group) {
162
+ sessions = sessions.filter((s) => s.group === params.group);
163
+ }
164
+ return sessions;
165
+ }
153
166
  case "session.status": {
154
167
  const session = ctx.sessionTracker.getSession(params.id);
155
168
  if (!session)
@@ -157,11 +170,15 @@ function createRequestHandler(ctx) {
157
170
  return session;
158
171
  }
159
172
  case "session.peek": {
160
- const adapterName = params.adapter || "claude-code";
173
+ // Auto-detect adapter from tracked session, fall back to param or claude-code
174
+ const tracked = ctx.sessionTracker.getSession(params.id);
175
+ const adapterName = params.adapter || tracked?.adapter || "claude-code";
161
176
  const adapter = ctx.adapters[adapterName];
162
177
  if (!adapter)
163
178
  throw new Error(`Unknown adapter: ${adapterName}`);
164
- return adapter.peek(params.id, {
179
+ // Use the full session ID if we resolved it from the tracker
180
+ const peekId = tracked?.id || params.id;
181
+ return adapter.peek(peekId, {
165
182
  lines: params.lines,
166
183
  });
167
184
  }
@@ -193,6 +210,10 @@ function createRequestHandler(ctx) {
193
210
  env: params.env,
194
211
  adapterOpts: params.adapterOpts,
195
212
  });
213
+ // Propagate group tag if provided
214
+ if (params.group) {
215
+ session.group = params.group;
216
+ }
196
217
  const record = ctx.sessionTracker.track(session, adapterName);
197
218
  // Auto-lock
198
219
  if (cwd) {
@@ -204,6 +225,15 @@ function createRequestHandler(ctx) {
204
225
  const session = ctx.sessionTracker.getSession(params.id);
205
226
  if (!session)
206
227
  throw new Error(`Session not found: ${params.id}`);
228
+ // Ghost pending entry with dead PID: remove from state with --force
229
+ if (session.id.startsWith("pending-") &&
230
+ params.force &&
231
+ session.pid &&
232
+ !isProcessAlive(session.pid)) {
233
+ ctx.lockManager.autoUnlock(session.id);
234
+ ctx.sessionTracker.removeSession(session.id);
235
+ return null;
236
+ }
207
237
  const adapter = ctx.adapters[session.adapter];
208
238
  if (!adapter)
209
239
  throw new Error(`Unknown adapter: ${session.adapter}`);
@@ -3,16 +3,33 @@ import type { SessionRecord, StateManager } from "./state.js";
3
3
  export interface SessionTrackerOpts {
4
4
  adapters: Record<string, AgentAdapter>;
5
5
  pollIntervalMs?: number;
6
+ /** Override PID liveness check for testing (default: process.kill(pid, 0)) */
7
+ isProcessAlive?: (pid: number) => boolean;
6
8
  }
7
9
  export declare class SessionTracker {
8
10
  private state;
9
11
  private adapters;
10
12
  private pollIntervalMs;
11
13
  private pollHandle;
14
+ private polling;
15
+ private readonly isProcessAlive;
12
16
  constructor(state: StateManager, opts: SessionTrackerOpts);
13
17
  startPolling(): void;
18
+ /** Run poll() with a guard to skip if the previous cycle is still running */
19
+ private guardedPoll;
14
20
  stopPolling(): void;
15
21
  private poll;
22
+ /**
23
+ * Clean up ghost sessions in the daemon state:
24
+ * - pending-* entries whose PID matches a resolved session → remove pending
25
+ * - Any "running"/"idle" session in state whose PID is dead → mark stopped
26
+ */
27
+ private reapStaleEntries;
28
+ /**
29
+ * Remove stopped sessions from state that have been stopped for more than 7 days.
30
+ * This reduces overhead from accumulating hundreds of historical sessions.
31
+ */
32
+ private pruneOldSessions;
16
33
  /** Track a newly launched session */
17
34
  track(session: AgentSession, adapterName: string): SessionRecord;
18
35
  /** Get session record by id (exact or prefix) */
@@ -21,8 +38,11 @@ export declare class SessionTracker {
21
38
  listSessions(opts?: {
22
39
  status?: string;
23
40
  all?: boolean;
41
+ adapter?: string;
24
42
  }): SessionRecord[];
25
43
  activeCount(): number;
44
+ /** Remove a session from state entirely (used for ghost cleanup) */
45
+ removeSession(sessionId: string): void;
26
46
  /** Called when a session stops — returns the cwd for fuse/lock processing */
27
47
  onSessionExit(sessionId: string): SessionRecord | undefined;
28
48
  }