@orgloop/agentctl 1.2.0 → 1.3.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,4 +1,4 @@
1
- import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
1
+ import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
2
  export interface PidInfo {
3
3
  pid: number;
4
4
  cwd: string;
@@ -38,6 +38,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
38
38
  private readonly getPids;
39
39
  private readonly isProcessAlive;
40
40
  constructor(opts?: ClaudeCodeAdapterOpts);
41
+ discover(): Promise<DiscoveredSession[]>;
42
+ isAlive(sessionId: string): Promise<boolean>;
41
43
  list(opts?: ListOpts): Promise<AgentSession[]>;
42
44
  peek(sessionId: string, opts?: PeekOpts): Promise<string>;
43
45
  status(sessionId: string): Promise<AgentSession>;
@@ -5,7 +5,9 @@ import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { promisify } from "node:util";
8
+ import { buildSpawnEnv } from "../utils/daemon-env.js";
8
9
  import { readHead, readTail } from "../utils/partial-read.js";
10
+ import { resolveBinaryPath } from "../utils/resolve-binary.js";
9
11
  const execFileAsync = promisify(execFile);
10
12
  const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), ".claude");
11
13
  // Default: only show stopped sessions from the last 7 days
@@ -30,6 +32,57 @@ export class ClaudeCodeAdapter {
30
32
  this.getPids = opts?.getPids || getClaudePids;
31
33
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
32
34
  }
35
+ async discover() {
36
+ const runningPids = await this.getPids();
37
+ const results = [];
38
+ let projectDirs;
39
+ try {
40
+ projectDirs = await fs.readdir(this.projectsDir);
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ for (const projDir of projectDirs) {
46
+ const projPath = path.join(this.projectsDir, projDir);
47
+ const stat = await fs.stat(projPath).catch(() => null);
48
+ if (!stat?.isDirectory())
49
+ continue;
50
+ const entries = await this.getEntriesForProject(projPath, projDir);
51
+ for (const { entry, index } of entries) {
52
+ if (entry.isSidechain)
53
+ continue;
54
+ const isRunning = await this.isSessionRunning(entry, index, runningPids);
55
+ const { model, tokens } = await this.parseSessionTail(entry.fullPath);
56
+ results.push({
57
+ id: entry.sessionId,
58
+ status: isRunning ? "running" : "stopped",
59
+ adapter: this.id,
60
+ cwd: index.originalPath || entry.projectPath,
61
+ model,
62
+ startedAt: new Date(entry.created),
63
+ stoppedAt: isRunning ? undefined : new Date(entry.modified),
64
+ pid: isRunning
65
+ ? await this.findMatchingPid(entry, index, runningPids)
66
+ : undefined,
67
+ prompt: entry.firstPrompt?.slice(0, 200),
68
+ tokens,
69
+ nativeMetadata: {
70
+ projectDir: index.originalPath || entry.projectPath,
71
+ gitBranch: entry.gitBranch,
72
+ messageCount: entry.messageCount,
73
+ },
74
+ });
75
+ }
76
+ }
77
+ return results;
78
+ }
79
+ async isAlive(sessionId) {
80
+ const runningPids = await this.getPids();
81
+ const entry = await this.findIndexEntry(sessionId);
82
+ if (!entry)
83
+ return false;
84
+ return this.isSessionRunning(entry.entry, entry.index, runningPids);
85
+ }
33
86
  async list(opts) {
34
87
  const runningPids = await this.getPids();
35
88
  const sessions = [];
@@ -123,7 +176,7 @@ export class ClaudeCodeAdapter {
123
176
  args.push("--model", opts.model);
124
177
  }
125
178
  args.push("-p", opts.prompt);
126
- const env = { ...process.env, ...opts.env };
179
+ const env = buildSpawnEnv(undefined, opts.env);
127
180
  const cwd = opts.cwd || process.cwd();
128
181
  // Write stdout to a log file so we can extract the session ID
129
182
  // without keeping a pipe open (which would prevent full detachment).
@@ -131,12 +184,17 @@ export class ClaudeCodeAdapter {
131
184
  const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
132
185
  const logFd = await fs.open(logPath, "w");
133
186
  // Capture stderr to the same log file for debugging launch failures
134
- const child = spawn("claude", args, {
187
+ const claudePath = await resolveBinaryPath("claude");
188
+ const child = spawn(claudePath, args, {
135
189
  cwd,
136
190
  env,
137
191
  stdio: ["ignore", logFd.fd, logFd.fd],
138
192
  detached: true,
139
193
  });
194
+ // Handle spawn errors (e.g. ENOENT) gracefully instead of crashing the daemon
195
+ child.on("error", (err) => {
196
+ console.error(`[claude-code] spawn error: ${err.message}`);
197
+ });
140
198
  // Fully detach: child runs in its own process group.
141
199
  // When the wrapper gets SIGTERM, the child keeps running.
142
200
  child.unref();
@@ -250,11 +308,15 @@ export class ClaudeCodeAdapter {
250
308
  ];
251
309
  const session = await this.status(sessionId).catch(() => null);
252
310
  const cwd = session?.cwd || process.cwd();
253
- const child = spawn("claude", args, {
311
+ const claudePath = await resolveBinaryPath("claude");
312
+ const child = spawn(claudePath, args, {
254
313
  cwd,
255
314
  stdio: ["pipe", "pipe", "pipe"],
256
315
  detached: true,
257
316
  });
317
+ child.on("error", (err) => {
318
+ console.error(`[claude-code] resume spawn error: ${err.message}`);
319
+ });
258
320
  child.unref();
259
321
  }
260
322
  async *events() {
@@ -1,4 +1,4 @@
1
- import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
1
+ import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
2
  export interface CodexPidInfo {
3
3
  pid: number;
4
4
  cwd: string;
@@ -34,6 +34,8 @@ export declare class CodexAdapter implements AgentAdapter {
34
34
  private readonly getPids;
35
35
  private readonly isProcessAlive;
36
36
  constructor(opts?: CodexAdapterOpts);
37
+ discover(): Promise<DiscoveredSession[]>;
38
+ isAlive(sessionId: string): Promise<boolean>;
37
39
  list(opts?: ListOpts): Promise<AgentSession[]>;
38
40
  peek(sessionId: string, opts?: PeekOpts): Promise<string>;
39
41
  status(sessionId: string): Promise<AgentSession>;
@@ -5,6 +5,8 @@ import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { promisify } from "node:util";
8
+ import { buildSpawnEnv } from "../utils/daemon-env.js";
9
+ import { resolveBinaryPath } from "../utils/resolve-binary.js";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const DEFAULT_CODEX_DIR = path.join(os.homedir(), ".codex");
10
12
  // Default: only show stopped sessions from the last 7 days
@@ -28,6 +30,41 @@ export class CodexAdapter {
28
30
  this.getPids = opts?.getPids || getCodexPids;
29
31
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
30
32
  }
33
+ async discover() {
34
+ const runningPids = await this.getPids();
35
+ const sessionInfos = await this.discoverSessions();
36
+ const results = [];
37
+ for (const info of sessionInfos) {
38
+ const isRunning = this.isSessionRunning(info, runningPids);
39
+ const pid = isRunning
40
+ ? this.findMatchingPid(info, runningPids)
41
+ : undefined;
42
+ results.push({
43
+ id: info.id,
44
+ status: isRunning ? "running" : "stopped",
45
+ adapter: this.id,
46
+ cwd: info.cwd,
47
+ model: info.model,
48
+ startedAt: info.created,
49
+ stoppedAt: isRunning ? undefined : info.modified,
50
+ pid,
51
+ prompt: info.firstPrompt,
52
+ tokens: info.tokens,
53
+ nativeMetadata: {
54
+ cliVersion: info.cliVersion,
55
+ lastMessage: info.lastMessage,
56
+ },
57
+ });
58
+ }
59
+ return results;
60
+ }
61
+ async isAlive(sessionId) {
62
+ const runningPids = await this.getPids();
63
+ const info = await this.findSession(sessionId);
64
+ if (!info)
65
+ return false;
66
+ return this.isSessionRunning(info, runningPids);
67
+ }
31
68
  async list(opts) {
32
69
  const runningPids = await this.getPids();
33
70
  const sessionInfos = await this.discoverSessions();
@@ -102,16 +139,20 @@ export class CodexAdapter {
102
139
  const cwd = opts.cwd || process.cwd();
103
140
  args.push("--cd", cwd);
104
141
  args.push(opts.prompt);
105
- const env = { ...process.env, ...opts.env };
142
+ const env = buildSpawnEnv(undefined, opts.env);
106
143
  await fs.mkdir(this.sessionsMetaDir, { recursive: true });
107
144
  const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
108
145
  const logFd = await fs.open(logPath, "w");
109
- const child = spawn("codex", args, {
146
+ const codexPath = await resolveBinaryPath("codex");
147
+ const child = spawn(codexPath, args, {
110
148
  cwd,
111
149
  env,
112
150
  stdio: ["ignore", logFd.fd, "ignore"],
113
151
  detached: true,
114
152
  });
153
+ child.on("error", (err) => {
154
+ console.error(`[codex] spawn error: ${err.message}`);
155
+ });
115
156
  child.unref();
116
157
  const pid = child.pid;
117
158
  const now = new Date();
@@ -219,11 +260,15 @@ export class CodexAdapter {
219
260
  sessionId,
220
261
  message,
221
262
  ];
222
- const child = spawn("codex", args, {
263
+ const codexPath = await resolveBinaryPath("codex");
264
+ const child = spawn(codexPath, args, {
223
265
  cwd,
224
266
  stdio: ["pipe", "pipe", "pipe"],
225
267
  detached: true,
226
268
  });
269
+ child.on("error", (err) => {
270
+ console.error(`[codex] resume spawn error: ${err.message}`);
271
+ });
227
272
  child.unref();
228
273
  }
229
274
  async *events() {
@@ -1,4 +1,4 @@
1
- import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
1
+ import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
2
  export interface DeviceIdentity {
3
3
  deviceId: string;
4
4
  publicKeyPem: string;
@@ -122,6 +122,8 @@ export declare class OpenClawAdapter implements AgentAdapter {
122
122
  private readonly rpcCall;
123
123
  constructor(opts?: OpenClawAdapterOpts);
124
124
  private tryLoadDeviceIdentity;
125
+ discover(): Promise<DiscoveredSession[]>;
126
+ isAlive(sessionId: string): Promise<boolean>;
125
127
  list(opts?: ListOpts): Promise<AgentSession[]>;
126
128
  peek(sessionId: string, opts?: PeekOpts): Promise<string>;
127
129
  status(sessionId: string): Promise<AgentSession>;
@@ -197,6 +197,62 @@ export class OpenClawAdapter {
197
197
  return null;
198
198
  }
199
199
  }
200
+ async discover() {
201
+ if (!this.authToken)
202
+ return [];
203
+ let result;
204
+ try {
205
+ result = (await this.rpcCall("sessions.list", {
206
+ includeDerivedTitles: true,
207
+ includeLastMessage: true,
208
+ }));
209
+ }
210
+ catch {
211
+ return [];
212
+ }
213
+ return result.sessions.map((row) => {
214
+ const updatedAt = row.updatedAt ?? 0;
215
+ const model = row.model || result.defaults.model || undefined;
216
+ const input = row.inputTokens ?? 0;
217
+ const output = row.outputTokens ?? 0;
218
+ // Gateway is the source of truth: if a session is in the list, it's alive.
219
+ // Don't guess from timestamps — sessions can be idle for hours between messages.
220
+ return {
221
+ id: row.sessionId || row.key,
222
+ status: "running",
223
+ adapter: this.id,
224
+ model,
225
+ startedAt: updatedAt > 0 ? new Date(updatedAt) : new Date(),
226
+ prompt: row.derivedTitle || row.displayName || row.label,
227
+ tokens: input || output ? { in: input, out: output } : undefined,
228
+ nativeMetadata: {
229
+ key: row.key,
230
+ kind: row.kind,
231
+ channel: row.channel,
232
+ displayName: row.displayName,
233
+ modelProvider: row.modelProvider || result.defaults.modelProvider,
234
+ lastMessagePreview: row.lastMessagePreview,
235
+ },
236
+ };
237
+ });
238
+ }
239
+ async isAlive(sessionId) {
240
+ if (!this.authToken)
241
+ return false;
242
+ try {
243
+ const result = (await this.rpcCall("sessions.list", {
244
+ search: sessionId,
245
+ }));
246
+ // Gateway is the source of truth: if a session is in the list, it's alive.
247
+ return result.sessions.some((s) => s.sessionId === sessionId ||
248
+ s.key === sessionId ||
249
+ s.sessionId?.startsWith(sessionId) ||
250
+ s.key.startsWith(sessionId));
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ }
200
256
  async list(opts) {
201
257
  if (!this.authToken) {
202
258
  console.warn("Warning: OPENCLAW_GATEWAY_TOKEN is not set — OpenClaw adapter cannot authenticate. " +
@@ -342,17 +398,18 @@ export class OpenClawAdapter {
342
398
  }
343
399
  // --- Private helpers ---
344
400
  mapRowToSession(row, defaults) {
345
- const now = Date.now();
346
401
  const updatedAt = row.updatedAt ?? 0;
347
- const ageMs = now - updatedAt;
348
- const isActive = updatedAt > 0 && ageMs < 5 * 60 * 1000;
402
+ const ageMs = Date.now() - updatedAt;
403
+ // Gateway is the source of truth: if a session is in the list, it's alive.
404
+ // Use "running" for recently active sessions, "idle" for quiet ones.
405
+ const status = updatedAt > 0 && ageMs < 5 * 60 * 1000 ? "running" : "idle";
349
406
  const model = row.model || defaults.model || undefined;
350
407
  const input = row.inputTokens ?? 0;
351
408
  const output = row.outputTokens ?? 0;
352
409
  return {
353
410
  id: row.sessionId || row.key,
354
411
  adapter: this.id,
355
- status: isActive ? "running" : "idle",
412
+ status,
356
413
  startedAt: updatedAt > 0 ? new Date(updatedAt) : new Date(),
357
414
  cwd: undefined,
358
415
  model,
@@ -1,4 +1,4 @@
1
- import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
1
+ import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
2
  export interface PidInfo {
3
3
  pid: number;
4
4
  cwd: string;
@@ -95,6 +95,8 @@ export declare class OpenCodeAdapter implements AgentAdapter {
95
95
  private readonly getPids;
96
96
  private readonly isProcessAlive;
97
97
  constructor(opts?: OpenCodeAdapterOpts);
98
+ discover(): Promise<DiscoveredSession[]>;
99
+ isAlive(sessionId: string): Promise<boolean>;
98
100
  list(opts?: ListOpts): Promise<AgentSession[]>;
99
101
  peek(sessionId: string, opts?: PeekOpts): Promise<string>;
100
102
  status(sessionId: string): Promise<AgentSession>;
@@ -5,6 +5,8 @@ import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { promisify } from "node:util";
8
+ import { buildSpawnEnv } from "../utils/daemon-env.js";
9
+ import { resolveBinaryPath } from "../utils/resolve-binary.js";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const DEFAULT_STORAGE_DIR = path.join(os.homedir(), ".local", "share", "opencode", "storage");
10
12
  // Default: only show stopped sessions from the last 7 days
@@ -37,6 +39,63 @@ export class OpenCodeAdapter {
37
39
  this.getPids = opts?.getPids || getOpenCodePids;
38
40
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
39
41
  }
42
+ async discover() {
43
+ const runningPids = await this.getPids();
44
+ const results = [];
45
+ let projectDirs;
46
+ try {
47
+ projectDirs = await fs.readdir(this.sessionDir);
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ for (const projHash of projectDirs) {
53
+ const projPath = path.join(this.sessionDir, projHash);
54
+ const stat = await fs.stat(projPath).catch(() => null);
55
+ if (!stat?.isDirectory())
56
+ continue;
57
+ const sessionFiles = await this.getSessionFilesForProject(projPath);
58
+ for (const sessionData of sessionFiles) {
59
+ const isRunning = await this.isSessionRunning(sessionData, runningPids);
60
+ const { model, tokens, cost } = await this.aggregateMessageStats(sessionData.id);
61
+ const createdAt = sessionData.time?.created
62
+ ? new Date(sessionData.time.created)
63
+ : new Date();
64
+ const updatedAt = sessionData.time?.updated
65
+ ? new Date(sessionData.time.updated)
66
+ : undefined;
67
+ results.push({
68
+ id: sessionData.id,
69
+ status: isRunning ? "running" : "stopped",
70
+ adapter: this.id,
71
+ cwd: sessionData.directory,
72
+ model,
73
+ startedAt: createdAt,
74
+ stoppedAt: isRunning ? undefined : updatedAt,
75
+ pid: isRunning
76
+ ? await this.findMatchingPid(sessionData, runningPids)
77
+ : undefined,
78
+ prompt: sessionData.title?.slice(0, 200),
79
+ tokens,
80
+ cost,
81
+ nativeMetadata: {
82
+ projectID: sessionData.projectID,
83
+ slug: sessionData.slug,
84
+ summary: sessionData.summary,
85
+ version: sessionData.version,
86
+ },
87
+ });
88
+ }
89
+ }
90
+ return results;
91
+ }
92
+ async isAlive(sessionId) {
93
+ const runningPids = await this.getPids();
94
+ const resolved = await this.resolveSessionId(sessionId);
95
+ if (!resolved)
96
+ return false;
97
+ return this.isSessionRunning(resolved, runningPids);
98
+ }
40
99
  async list(opts) {
41
100
  const runningPids = await this.getPids();
42
101
  const sessions = [];
@@ -116,15 +175,19 @@ export class OpenCodeAdapter {
116
175
  }
117
176
  async launch(opts) {
118
177
  const args = ["run", opts.prompt];
119
- const env = { ...process.env, ...opts.env };
178
+ const env = buildSpawnEnv(undefined, opts.env);
120
179
  const cwd = opts.cwd || process.cwd();
121
180
  await fs.mkdir(this.sessionsMetaDir, { recursive: true });
122
- const child = spawn("opencode", args, {
181
+ const opencodePath = await resolveBinaryPath("opencode");
182
+ const child = spawn(opencodePath, args, {
123
183
  cwd,
124
184
  env,
125
185
  stdio: ["ignore", "pipe", "pipe"],
126
186
  detached: true,
127
187
  });
188
+ child.on("error", (err) => {
189
+ console.error(`[opencode] spawn error: ${err.message}`);
190
+ });
128
191
  child.unref();
129
192
  const pid = child.pid;
130
193
  const now = new Date();
@@ -183,11 +246,15 @@ export class OpenCodeAdapter {
183
246
  if (!resolved)
184
247
  throw new Error(`Session not found for resume: ${sessionId}`);
185
248
  const cwd = resolved.directory || process.cwd();
186
- const child = spawn("opencode", ["run", message], {
249
+ const opencodePath = await resolveBinaryPath("opencode");
250
+ const child = spawn(opencodePath, ["run", message], {
187
251
  cwd,
188
252
  stdio: ["ignore", "pipe", "pipe"],
189
253
  detached: true,
190
254
  });
255
+ child.on("error", (err) => {
256
+ console.error(`[opencode] resume spawn error: ${err.message}`);
257
+ });
191
258
  child.unref();
192
259
  }
193
260
  async *events() {
@@ -1,4 +1,4 @@
1
- import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
1
+ import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
2
  export interface PidInfo {
3
3
  pid: number;
4
4
  cwd: string;
@@ -41,6 +41,8 @@ export declare class PiRustAdapter implements AgentAdapter {
41
41
  private readonly getPids;
42
42
  private readonly isProcessAlive;
43
43
  constructor(opts?: PiRustAdapterOpts);
44
+ discover(): Promise<DiscoveredSession[]>;
45
+ isAlive(sessionId: string): Promise<boolean>;
44
46
  list(opts?: ListOpts): Promise<AgentSession[]>;
45
47
  peek(sessionId: string, opts?: PeekOpts): Promise<string>;
46
48
  status(sessionId: string): Promise<AgentSession>;
@@ -5,6 +5,8 @@ import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { promisify } from "node:util";
8
+ import { buildSpawnEnv } from "../utils/daemon-env.js";
9
+ import { resolveBinaryPath } from "../utils/resolve-binary.js";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const DEFAULT_SESSION_DIR = path.join(os.homedir(), ".pi", "agent", "sessions");
10
12
  // Default: only show stopped sessions from the last 7 days
@@ -31,6 +33,80 @@ export class PiRustAdapter {
31
33
  this.getPids = opts?.getPids || getPiRustPids;
32
34
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
33
35
  }
36
+ async discover() {
37
+ const runningPids = await this.getPids();
38
+ const results = [];
39
+ let projectDirs;
40
+ try {
41
+ const entries = await fs.readdir(this.sessionDir);
42
+ projectDirs = entries.filter((e) => e.startsWith("--"));
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ for (const projDir of projectDirs) {
48
+ const projPath = path.join(this.sessionDir, projDir);
49
+ const stat = await fs.stat(projPath).catch(() => null);
50
+ if (!stat?.isDirectory())
51
+ continue;
52
+ const projectCwd = decodeProjDir(projDir);
53
+ const sessionFiles = await this.getSessionFiles(projPath);
54
+ for (const file of sessionFiles) {
55
+ const filePath = path.join(projPath, file);
56
+ const header = await this.readSessionHeader(filePath);
57
+ if (!header)
58
+ continue;
59
+ const isRunning = await this.isSessionRunning(header, projectCwd, runningPids);
60
+ const { model, tokens, cost } = await this.parseSessionTail(filePath);
61
+ const firstPrompt = await this.readFirstPrompt(filePath);
62
+ let fileStat;
63
+ try {
64
+ fileStat = await fs.stat(filePath);
65
+ }
66
+ catch {
67
+ // ignore
68
+ }
69
+ results.push({
70
+ id: header.id,
71
+ status: isRunning ? "running" : "stopped",
72
+ adapter: this.id,
73
+ cwd: header.cwd || projectCwd,
74
+ model: model || header.modelId,
75
+ startedAt: new Date(header.timestamp),
76
+ stoppedAt: isRunning
77
+ ? undefined
78
+ : fileStat
79
+ ? new Date(Number(fileStat.mtimeMs))
80
+ : undefined,
81
+ pid: isRunning
82
+ ? await this.findMatchingPid(header, projectCwd, runningPids)
83
+ : undefined,
84
+ prompt: firstPrompt?.slice(0, 200),
85
+ tokens,
86
+ cost: cost ?? undefined,
87
+ nativeMetadata: {
88
+ provider: header.provider,
89
+ thinkingLevel: header.thinkingLevel,
90
+ projectDir: projectCwd,
91
+ sessionFile: filePath,
92
+ },
93
+ });
94
+ }
95
+ }
96
+ return results;
97
+ }
98
+ async isAlive(sessionId) {
99
+ const runningPids = await this.getPids();
100
+ const filePath = await this.findSessionFile(sessionId);
101
+ if (!filePath)
102
+ return false;
103
+ const header = await this.readSessionHeader(filePath);
104
+ if (!header)
105
+ return false;
106
+ const projDir = path.basename(path.dirname(filePath));
107
+ const projectCwd = decodeProjDir(projDir);
108
+ return this.isSessionRunning(header, projectCwd, runningPids);
109
+ }
34
110
  async list(opts) {
35
111
  const runningPids = await this.getPids();
36
112
  const sessions = [];
@@ -129,18 +205,22 @@ export class PiRustAdapter {
129
205
  if (opts.model) {
130
206
  args.unshift("--model", opts.model);
131
207
  }
132
- const env = { ...process.env, ...opts.env };
208
+ const env = buildSpawnEnv(undefined, opts.env);
133
209
  const cwd = opts.cwd || process.cwd();
134
210
  // Write stdout to a log file so we can extract the session ID
135
211
  await fs.mkdir(this.sessionsMetaDir, { recursive: true });
136
212
  const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
137
213
  const logFd = await fs.open(logPath, "w");
138
- const child = spawn("pi-rust", args, {
214
+ const piRustPath = await resolveBinaryPath("pi-rust");
215
+ const child = spawn(piRustPath, args, {
139
216
  cwd,
140
217
  env,
141
218
  stdio: ["ignore", logFd.fd, "ignore"],
142
219
  detached: true,
143
220
  });
221
+ child.on("error", (err) => {
222
+ console.error(`[pi-rust] spawn error: ${err.message}`);
223
+ });
144
224
  child.unref();
145
225
  const pid = child.pid;
146
226
  const now = new Date();
@@ -251,11 +331,15 @@ export class PiRustAdapter {
251
331
  else {
252
332
  args.unshift("--continue");
253
333
  }
254
- const child = spawn("pi-rust", args, {
334
+ const piRustPath = await resolveBinaryPath("pi-rust");
335
+ const child = spawn(piRustPath, args, {
255
336
  cwd,
256
337
  stdio: ["pipe", "pipe", "pipe"],
257
338
  detached: true,
258
339
  });
340
+ child.on("error", (err) => {
341
+ console.error(`[pi-rust] resume spawn error: ${err.message}`);
342
+ });
259
343
  child.unref();
260
344
  }
261
345
  async *events() {
@@ -1,4 +1,4 @@
1
- import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
1
+ import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
2
  export interface PidInfo {
3
3
  pid: number;
4
4
  cwd: string;
@@ -41,6 +41,8 @@ export declare class PiAdapter implements AgentAdapter {
41
41
  private readonly getPids;
42
42
  private readonly isProcessAlive;
43
43
  constructor(opts?: PiAdapterOpts);
44
+ discover(): Promise<DiscoveredSession[]>;
45
+ isAlive(sessionId: string): Promise<boolean>;
44
46
  list(opts?: ListOpts): Promise<AgentSession[]>;
45
47
  peek(sessionId: string, opts?: PeekOpts): Promise<string>;
46
48
  /** Extract assistant messages from a JSONL session file */