@agentmeshhq/agent 0.1.17 → 0.2.1

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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/__tests__/orphan-process.test.d.ts +11 -0
  4. package/dist/__tests__/orphan-process.test.js +286 -0
  5. package/dist/__tests__/orphan-process.test.js.map +1 -0
  6. package/dist/__tests__/runner.test.js +16 -0
  7. package/dist/__tests__/runner.test.js.map +1 -1
  8. package/dist/__tests__/watchdog.test.js +138 -12
  9. package/dist/__tests__/watchdog.test.js.map +1 -1
  10. package/dist/cli/index.js +2 -1
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/start.d.ts +2 -1
  13. package/dist/cli/start.js +6 -3
  14. package/dist/cli/start.js.map +1 -1
  15. package/dist/cli/status.js +11 -0
  16. package/dist/cli/status.js.map +1 -1
  17. package/dist/cli/stop.js +7 -2
  18. package/dist/cli/stop.js.map +1 -1
  19. package/dist/config/schema.d.ts +4 -2
  20. package/dist/core/daemon/assignment-message.d.ts +12 -0
  21. package/dist/core/daemon/assignment-message.js +36 -0
  22. package/dist/core/daemon/assignment-message.js.map +1 -0
  23. package/dist/core/daemon/bootstrap.d.ts +35 -0
  24. package/dist/core/daemon/bootstrap.js +52 -0
  25. package/dist/core/daemon/bootstrap.js.map +1 -0
  26. package/dist/core/daemon/crash-log.d.ts +16 -0
  27. package/dist/core/daemon/crash-log.js +24 -0
  28. package/dist/core/daemon/crash-log.js.map +1 -0
  29. package/dist/core/daemon/health-policy.d.ts +21 -0
  30. package/dist/core/daemon/health-policy.js +32 -0
  31. package/dist/core/daemon/health-policy.js.map +1 -0
  32. package/dist/core/daemon/sandbox-config.d.ts +9 -0
  33. package/dist/core/daemon/sandbox-config.js +17 -0
  34. package/dist/core/daemon/sandbox-config.js.map +1 -0
  35. package/dist/core/daemon/state.d.ts +33 -0
  36. package/dist/core/daemon/state.js +77 -0
  37. package/dist/core/daemon/state.js.map +1 -0
  38. package/dist/core/daemon/tmux-session.d.ts +17 -0
  39. package/dist/core/daemon/tmux-session.js +34 -0
  40. package/dist/core/daemon/tmux-session.js.map +1 -0
  41. package/dist/core/daemon/workspace.d.ts +10 -0
  42. package/dist/core/daemon/workspace.js +51 -0
  43. package/dist/core/daemon/workspace.js.map +1 -0
  44. package/dist/core/daemon.d.ts +4 -7
  45. package/dist/core/daemon.js +143 -259
  46. package/dist/core/daemon.js.map +1 -1
  47. package/dist/core/injector.js +6 -0
  48. package/dist/core/injector.js.map +1 -1
  49. package/dist/core/registry.js +1 -1
  50. package/dist/core/registry.js.map +1 -1
  51. package/dist/core/runner/build.d.ts +9 -0
  52. package/dist/core/runner/build.js +53 -0
  53. package/dist/core/runner/build.js.map +1 -0
  54. package/dist/core/runner/detect.d.ts +5 -0
  55. package/dist/core/runner/detect.js +14 -0
  56. package/dist/core/runner/detect.js.map +1 -0
  57. package/dist/core/runner/index.d.ts +5 -0
  58. package/dist/core/runner/index.js +5 -0
  59. package/dist/core/runner/index.js.map +1 -0
  60. package/dist/core/runner/model.d.ts +5 -0
  61. package/dist/core/runner/model.js +7 -0
  62. package/dist/core/runner/model.js.map +1 -0
  63. package/dist/core/runner/opencode-models.d.ts +15 -0
  64. package/dist/core/runner/opencode-models.js +70 -0
  65. package/dist/core/runner/opencode-models.js.map +1 -0
  66. package/dist/core/runner/types.d.ts +19 -0
  67. package/dist/core/runner/types.js +8 -0
  68. package/dist/core/runner/types.js.map +1 -0
  69. package/dist/core/runner.d.ts +5 -47
  70. package/dist/core/runner.js +5 -167
  71. package/dist/core/runner.js.map +1 -1
  72. package/dist/core/tmux-runtime.d.ts +13 -0
  73. package/dist/core/tmux-runtime.js +72 -0
  74. package/dist/core/tmux-runtime.js.map +1 -0
  75. package/dist/core/tmux.d.ts +7 -1
  76. package/dist/core/tmux.js +75 -45
  77. package/dist/core/tmux.js.map +1 -1
  78. package/dist/core/watchdog.d.ts +18 -1
  79. package/dist/core/watchdog.js +78 -29
  80. package/dist/core/watchdog.js.map +1 -1
  81. package/package.json +30 -11
  82. package/dist/cli/inbox.d.ts +0 -5
  83. package/dist/cli/inbox.js +0 -123
  84. package/dist/cli/inbox.js.map +0 -1
  85. package/dist/cli/issue.d.ts +0 -42
  86. package/dist/cli/issue.js +0 -297
  87. package/dist/cli/issue.js.map +0 -1
  88. package/dist/cli/ready.d.ts +0 -5
  89. package/dist/cli/ready.js +0 -131
  90. package/dist/cli/ready.js.map +0 -1
  91. package/dist/cli/sync.d.ts +0 -8
  92. package/dist/cli/sync.js +0 -154
  93. package/dist/cli/sync.js.map +0 -1
  94. package/dist/core/issue-cache.d.ts +0 -44
  95. package/dist/core/issue-cache.js +0 -75
  96. package/dist/core/issue-cache.js.map +0 -1
  97. package/src/__tests__/context.test.ts +0 -464
  98. package/src/__tests__/injector.test.ts +0 -29
  99. package/src/__tests__/jwt.test.ts +0 -112
  100. package/src/__tests__/loader.test.ts +0 -239
  101. package/src/__tests__/runner.test.ts +0 -104
  102. package/src/__tests__/sandbox.test.ts +0 -435
  103. package/src/__tests__/watchdog.test.ts +0 -368
  104. package/src/cli/attach.ts +0 -22
  105. package/src/cli/build.ts +0 -145
  106. package/src/cli/config.ts +0 -148
  107. package/src/cli/context.ts +0 -231
  108. package/src/cli/deploy.ts +0 -155
  109. package/src/cli/index.ts +0 -375
  110. package/src/cli/init.ts +0 -75
  111. package/src/cli/list.ts +0 -70
  112. package/src/cli/local.ts +0 -183
  113. package/src/cli/logs.ts +0 -64
  114. package/src/cli/migrate.ts +0 -212
  115. package/src/cli/nudge.ts +0 -81
  116. package/src/cli/restart.ts +0 -59
  117. package/src/cli/slack.ts +0 -70
  118. package/src/cli/start.ts +0 -115
  119. package/src/cli/status.ts +0 -91
  120. package/src/cli/stop.ts +0 -48
  121. package/src/cli/test.ts +0 -143
  122. package/src/cli/token.ts +0 -188
  123. package/src/cli/whoami.ts +0 -142
  124. package/src/config/loader.ts +0 -121
  125. package/src/config/schema.ts +0 -68
  126. package/src/context/handoff.ts +0 -122
  127. package/src/context/index.ts +0 -8
  128. package/src/context/schema.ts +0 -111
  129. package/src/context/storage.ts +0 -197
  130. package/src/core/daemon.ts +0 -1308
  131. package/src/core/heartbeat.ts +0 -129
  132. package/src/core/injector.ts +0 -292
  133. package/src/core/registry.ts +0 -159
  134. package/src/core/runner.ts +0 -225
  135. package/src/core/sandbox.ts +0 -547
  136. package/src/core/session-id.ts +0 -111
  137. package/src/core/tmux.ts +0 -405
  138. package/src/core/watchdog.ts +0 -238
  139. package/src/core/websocket.ts +0 -94
  140. package/src/index.ts +0 -10
  141. package/src/utils/jwt.ts +0 -87
  142. package/tsconfig.json +0 -8
  143. package/vitest.config.ts +0 -12
@@ -1,547 +0,0 @@
1
- /**
2
- * Docker Sandbox Runtime
3
- *
4
- * Provides containerized execution of agents with filesystem isolation.
5
- * Each agent runs in its own container with only the allowed workspace mounted.
6
- */
7
-
8
- import { type ChildProcess, execSync, spawn, spawnSync } from "node:child_process";
9
- import crypto from "node:crypto";
10
- import fs from "node:fs";
11
- import os from "node:os";
12
- import path from "node:path";
13
-
14
- export interface SandboxConfig {
15
- /** Agent name (used for container naming) */
16
- agentName: string;
17
- /** Image to use for the container */
18
- image: string;
19
- /** Workspace path on host to mount */
20
- workspacePath: string;
21
- /** Container mount point for workspace (default: /workspace) */
22
- containerWorkdir?: string;
23
- /** CPU limit (e.g., "0.5" for half a CPU) */
24
- cpuLimit?: string;
25
- /** Memory limit (e.g., "512m", "1g") */
26
- memoryLimit?: string;
27
- /** Environment variables to pass to container */
28
- env?: Record<string, string>;
29
- /** Network mode (default: bridge) */
30
- networkMode?: string;
31
- /** Additional mounts (host:container format) */
32
- additionalMounts?: string[];
33
- /** Run opencode serve instead of TUI */
34
- serveMode?: boolean;
35
- /** Port for serve mode */
36
- servePort?: number;
37
- /** Custom command to run in container (overrides default) */
38
- command?: string[];
39
- }
40
-
41
- export interface SandboxMountPolicy {
42
- /** Paths that are allowed to be mounted */
43
- allowedPaths: string[];
44
- /** Paths that are explicitly denied (checked first) */
45
- deniedPaths: string[];
46
- }
47
-
48
- // Default mount policy - deny sensitive paths
49
- const DEFAULT_DENY_PATHS = [
50
- "~/.ssh",
51
- "~/.gnupg",
52
- "~/.aws",
53
- "~/.config/gcloud",
54
- "~/.kube",
55
- "~/.docker",
56
- "/etc/passwd",
57
- "/etc/shadow",
58
- "/etc/hosts",
59
- "/var/run/docker.sock",
60
- ];
61
-
62
- export class DockerSandbox {
63
- private config: SandboxConfig;
64
- private containerId: string | null = null;
65
- private containerName: string;
66
- private process: ChildProcess | null = null;
67
-
68
- constructor(config: SandboxConfig) {
69
- this.config = {
70
- containerWorkdir: "/workspace",
71
- cpuLimit: "1",
72
- memoryLimit: "2g",
73
- networkMode: "bridge",
74
- ...config,
75
- };
76
- // Generate unique container name
77
- const suffix = crypto.randomBytes(4).toString("hex");
78
- this.containerName = `agentmesh-sandbox-${config.agentName}-${suffix}`;
79
- }
80
-
81
- /**
82
- * Validates that the workspace path is allowed based on policy
83
- */
84
- validateMountPolicy(policy?: SandboxMountPolicy): void {
85
- const workspacePath = this.expandPath(this.config.workspacePath);
86
-
87
- // Check denied paths first
88
- const deniedPaths = policy?.deniedPaths || DEFAULT_DENY_PATHS;
89
- for (const denied of deniedPaths) {
90
- const expandedDenied = this.expandPath(denied);
91
- if (workspacePath.startsWith(expandedDenied) || workspacePath === expandedDenied) {
92
- throw new Error(`Mount denied: ${workspacePath} matches denied path ${denied}`);
93
- }
94
- }
95
-
96
- // Check allowed paths if specified
97
- if (policy?.allowedPaths && policy.allowedPaths.length > 0) {
98
- const isAllowed = policy.allowedPaths.some((allowed) => {
99
- const expandedAllowed = this.expandPath(allowed);
100
- return workspacePath.startsWith(expandedAllowed);
101
- });
102
- if (!isAllowed) {
103
- throw new Error(`Mount denied: ${workspacePath} is not in allowed paths`);
104
- }
105
- }
106
-
107
- // Verify path exists
108
- if (!fs.existsSync(workspacePath)) {
109
- throw new Error(`Workspace path does not exist: ${workspacePath}`);
110
- }
111
- }
112
-
113
- /**
114
- * Expands ~ to home directory
115
- */
116
- private expandPath(p: string): string {
117
- if (p.startsWith("~")) {
118
- return path.join(os.homedir(), p.slice(1));
119
- }
120
- return path.resolve(p);
121
- }
122
-
123
- /**
124
- * Checks if Docker is available
125
- */
126
- static checkDockerAvailable(): boolean {
127
- try {
128
- const result = spawnSync("docker", ["--version"], {
129
- encoding: "utf-8",
130
- timeout: 5000,
131
- });
132
- return result.status === 0;
133
- } catch {
134
- return false;
135
- }
136
- }
137
-
138
- /**
139
- * Pulls the sandbox image if not present
140
- */
141
- async pullImage(): Promise<void> {
142
- console.log(`Checking for image: ${this.config.image}`);
143
-
144
- // Check if image exists locally
145
- const result = spawnSync("docker", ["image", "inspect", this.config.image], {
146
- encoding: "utf-8",
147
- });
148
-
149
- if (result.status === 0) {
150
- console.log("Image found locally");
151
- return;
152
- }
153
-
154
- console.log(`Pulling image: ${this.config.image}...`);
155
- const pullResult = spawnSync("docker", ["pull", this.config.image], {
156
- encoding: "utf-8",
157
- stdio: "inherit",
158
- });
159
-
160
- if (pullResult.status !== 0) {
161
- throw new Error(`Failed to pull image: ${this.config.image}`);
162
- }
163
- }
164
-
165
- /**
166
- * Creates and starts the sandbox container
167
- */
168
- async start(): Promise<string> {
169
- // Validate mount policy
170
- this.validateMountPolicy();
171
-
172
- const workspacePath = this.expandPath(this.config.workspacePath);
173
-
174
- // Build docker run arguments
175
- const args: string[] = [
176
- "run",
177
- "-d", // Detached
178
- "--name",
179
- this.containerName,
180
- "--hostname",
181
- `sandbox-${this.config.agentName}`,
182
-
183
- // Resource limits
184
- "--cpus",
185
- this.config.cpuLimit!,
186
- "--memory",
187
- this.config.memoryLimit!,
188
-
189
- // Security: run as non-root
190
- "--user",
191
- "1000:1000",
192
-
193
- // Network
194
- "--network",
195
- this.config.networkMode!,
196
-
197
- // Mount workspace
198
- "-v",
199
- `${workspacePath}:${this.config.containerWorkdir}:rw`,
200
-
201
- // Working directory
202
- "-w",
203
- this.config.containerWorkdir!,
204
- ];
205
-
206
- // Add environment variables
207
- if (this.config.env) {
208
- for (const [key, value] of Object.entries(this.config.env)) {
209
- args.push("-e", `${key}=${value}`);
210
- }
211
- }
212
-
213
- // Add additional mounts
214
- if (this.config.additionalMounts) {
215
- for (const mount of this.config.additionalMounts) {
216
- args.push("-v", mount);
217
- }
218
- }
219
-
220
- // Expose port for serve mode
221
- if (this.config.serveMode && this.config.servePort) {
222
- args.push("-p", `${this.config.servePort}:${this.config.servePort}`);
223
- }
224
-
225
- // Image and command
226
- args.push(this.config.image);
227
-
228
- // Command: either serve mode or keep container alive for tmux-style attach
229
- if (this.config.serveMode) {
230
- args.push(
231
- "opencode",
232
- "serve",
233
- "--port",
234
- String(this.config.servePort || 3001),
235
- "--hostname",
236
- "0.0.0.0",
237
- );
238
- } else {
239
- // Keep container running for attach capability
240
- args.push("tail", "-f", "/dev/null");
241
- }
242
-
243
- console.log(`Starting sandbox container: ${this.containerName}`);
244
-
245
- const result = spawnSync("docker", args, {
246
- encoding: "utf-8",
247
- });
248
-
249
- if (result.status !== 0) {
250
- throw new Error(`Failed to start container: ${result.stderr || result.stdout}`);
251
- }
252
-
253
- this.containerId = result.stdout.trim();
254
- console.log(`Container started: ${this.containerId.substring(0, 12)}`);
255
-
256
- return this.containerId;
257
- }
258
-
259
- /**
260
- * Executes a command in the running container
261
- */
262
- async exec(command: string[], options?: { interactive?: boolean }): Promise<string> {
263
- if (!this.containerId && !this.containerName) {
264
- throw new Error("Container not started");
265
- }
266
-
267
- const target = this.containerId || this.containerName;
268
- const args = ["exec"];
269
-
270
- if (options?.interactive) {
271
- args.push("-it");
272
- }
273
-
274
- args.push(target, ...command);
275
-
276
- const result = spawnSync("docker", args, {
277
- encoding: "utf-8",
278
- stdio: options?.interactive ? "inherit" : "pipe",
279
- });
280
-
281
- if (result.status !== 0 && !options?.interactive) {
282
- throw new Error(`Exec failed: ${result.stderr || result.stdout}`);
283
- }
284
-
285
- return result.stdout || "";
286
- }
287
-
288
- /**
289
- * Spawns opencode in the container (for non-serve mode)
290
- */
291
- async spawnOpencode(): Promise<ChildProcess> {
292
- if (!this.containerId && !this.containerName) {
293
- throw new Error("Container not started");
294
- }
295
-
296
- const target = this.containerId || this.containerName;
297
-
298
- // Build environment args
299
- const envArgs: string[] = [];
300
- if (this.config.env) {
301
- for (const [key, value] of Object.entries(this.config.env)) {
302
- envArgs.push("-e", `${key}=${value}`);
303
- }
304
- }
305
-
306
- this.process = spawn("docker", ["exec", "-it", ...envArgs, target, "opencode"], {
307
- stdio: "inherit",
308
- });
309
-
310
- return this.process;
311
- }
312
-
313
- /**
314
- * Attaches to the container's opencode process
315
- */
316
- attach(): void {
317
- if (!this.containerId && !this.containerName) {
318
- throw new Error("Container not started");
319
- }
320
-
321
- const target = this.containerId || this.containerName;
322
-
323
- // Exec into container interactively
324
- execSync(`docker exec -it ${target} opencode`, {
325
- stdio: "inherit",
326
- });
327
- }
328
-
329
- /**
330
- * Gets container logs
331
- */
332
- getLogs(lines = 100): string {
333
- if (!this.containerId && !this.containerName) {
334
- throw new Error("Container not started");
335
- }
336
-
337
- const target = this.containerId || this.containerName;
338
- const result = spawnSync("docker", ["logs", "--tail", String(lines), target], {
339
- encoding: "utf-8",
340
- });
341
-
342
- return result.stdout + result.stderr;
343
- }
344
-
345
- /**
346
- * Follows container logs
347
- */
348
- followLogs(): void {
349
- if (!this.containerId && !this.containerName) {
350
- throw new Error("Container not started");
351
- }
352
-
353
- const target = this.containerId || this.containerName;
354
- execSync(`docker logs -f ${target}`, { stdio: "inherit" });
355
- }
356
-
357
- /**
358
- * Checks if container is running
359
- */
360
- isRunning(): boolean {
361
- if (!this.containerId && !this.containerName) {
362
- return false;
363
- }
364
-
365
- const target = this.containerId || this.containerName;
366
- const result = spawnSync("docker", ["inspect", "-f", "{{.State.Running}}", target], {
367
- encoding: "utf-8",
368
- });
369
-
370
- return result.stdout.trim() === "true";
371
- }
372
-
373
- /**
374
- * Gets container status
375
- */
376
- getStatus(): { running: boolean; status: string; health?: string } {
377
- if (!this.containerId && !this.containerName) {
378
- return { running: false, status: "not created" };
379
- }
380
-
381
- const target = this.containerId || this.containerName;
382
- const result = spawnSync(
383
- "docker",
384
- ["inspect", "-f", "{{.State.Running}}|{{.State.Status}}|{{.State.Health.Status}}", target],
385
- { encoding: "utf-8" },
386
- );
387
-
388
- if (result.status !== 0) {
389
- return { running: false, status: "not found" };
390
- }
391
-
392
- const [running, status, health] = result.stdout.trim().split("|");
393
- return {
394
- running: running === "true",
395
- status,
396
- health: health || undefined,
397
- };
398
- }
399
-
400
- /**
401
- * Stops the container
402
- */
403
- async stop(): Promise<void> {
404
- if (!this.containerId && !this.containerName) {
405
- return;
406
- }
407
-
408
- const target = this.containerId || this.containerName;
409
- console.log(`Stopping container: ${target}`);
410
-
411
- spawnSync("docker", ["stop", "-t", "10", target], {
412
- encoding: "utf-8",
413
- });
414
- }
415
-
416
- /**
417
- * Removes the container
418
- */
419
- async destroy(): Promise<void> {
420
- if (!this.containerId && !this.containerName) {
421
- return;
422
- }
423
-
424
- const target = this.containerId || this.containerName;
425
- console.log(`Destroying container: ${target}`);
426
-
427
- // Stop if running
428
- await this.stop();
429
-
430
- // Remove
431
- spawnSync("docker", ["rm", "-f", target], {
432
- encoding: "utf-8",
433
- });
434
-
435
- this.containerId = null;
436
- }
437
-
438
- /**
439
- * Restarts the container (destroy + start)
440
- * Returns the new container ID
441
- */
442
- async restart(): Promise<string> {
443
- await this.destroy();
444
-
445
- // Generate a new container name
446
- const suffix = crypto.randomBytes(4).toString("hex");
447
- this.containerName = `agentmesh-sandbox-${this.config.agentName}-${suffix}`;
448
-
449
- return this.start();
450
- }
451
-
452
- /**
453
- * Force removes any existing containers for this agent
454
- */
455
- static forceCleanup(agentName: string): number {
456
- const result = spawnSync(
457
- "docker",
458
- ["ps", "-aq", "--filter", `name=agentmesh-sandbox-${agentName}-`],
459
- { encoding: "utf-8" },
460
- );
461
-
462
- if (result.status !== 0 || !result.stdout.trim()) {
463
- return 0;
464
- }
465
-
466
- const containers = result.stdout.trim().split("\n").filter(Boolean);
467
-
468
- for (const containerId of containers) {
469
- spawnSync("docker", ["rm", "-f", containerId], {
470
- encoding: "utf-8",
471
- timeout: 10000,
472
- });
473
- }
474
-
475
- return containers.length;
476
- }
477
-
478
- /**
479
- * Gets container name
480
- */
481
- getContainerName(): string {
482
- return this.containerName;
483
- }
484
-
485
- /**
486
- * Gets container ID
487
- */
488
- getContainerId(): string | null {
489
- return this.containerId;
490
- }
491
-
492
- /**
493
- * Finds existing sandbox container for an agent
494
- */
495
- static findExisting(agentName: string): string | null {
496
- const result = spawnSync(
497
- "docker",
498
- [
499
- "ps",
500
- "-aq",
501
- "--filter",
502
- `name=agentmesh-sandbox-${agentName}-`,
503
- "--filter",
504
- "status=running",
505
- ],
506
- { encoding: "utf-8" },
507
- );
508
-
509
- const containerId = result.stdout.trim().split("\n")[0];
510
- return containerId || null;
511
- }
512
-
513
- /**
514
- * Lists all sandbox containers
515
- */
516
- static listAll(): Array<{
517
- id: string;
518
- name: string;
519
- status: string;
520
- image: string;
521
- }> {
522
- const result = spawnSync(
523
- "docker",
524
- [
525
- "ps",
526
- "-a",
527
- "--filter",
528
- "name=agentmesh-sandbox-",
529
- "--format",
530
- "{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}",
531
- ],
532
- { encoding: "utf-8" },
533
- );
534
-
535
- if (result.status !== 0 || !result.stdout.trim()) {
536
- return [];
537
- }
538
-
539
- return result.stdout
540
- .trim()
541
- .split("\n")
542
- .map((line) => {
543
- const [id, name, status, image] = line.split("|");
544
- return { id, name, status, image };
545
- });
546
- }
547
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * OpenCode Session ID Utilities
3
- *
4
- * Parses OpenCode log files to extract the active session ID (ses_XXXXX).
5
- * Used for native session resumption via `opencode --session <id> --continue`.
6
- */
7
-
8
- import fs from "node:fs";
9
- import os from "node:os";
10
- import path from "node:path";
11
-
12
- const SESSION_ID_PATTERN = /service=session\s+id=(ses_[A-Za-z0-9]+)/;
13
-
14
- function getLogDir(agentName: string): string {
15
- return path.join(os.homedir(), ".agentmesh", "opencode-data", agentName, "opencode", "log");
16
- }
17
-
18
- /**
19
- * Gets the latest OpenCode session ID from agent log files.
20
- *
21
- * Parses log files in ~/.agentmesh/opencode-data/<agentName>/opencode/log/
22
- * for the pattern: `service=session id=ses_XXXXX ... created`
23
- *
24
- * @returns The session ID string (e.g. "ses_365c3ec5bffeAV7qWirhNr1sU9") or null
25
- */
26
- export function getLatestSessionId(agentName: string): string | null {
27
- const logDir = getLogDir(agentName);
28
-
29
- if (!fs.existsSync(logDir)) {
30
- return null;
31
- }
32
-
33
- try {
34
- // List log files sorted by name (they're timestamped, so newest last)
35
- const logFiles = fs
36
- .readdirSync(logDir)
37
- .filter((f) => f.endsWith(".log"))
38
- .sort();
39
-
40
- if (logFiles.length === 0) {
41
- return null;
42
- }
43
-
44
- // Search from newest log file backwards
45
- for (let i = logFiles.length - 1; i >= 0; i--) {
46
- const logPath = path.join(logDir, logFiles[i]);
47
- const content = fs.readFileSync(logPath, "utf-8");
48
-
49
- // Find all session creation lines and take the last one
50
- const lines = content.split("\n");
51
- let lastSessionId: string | null = null;
52
-
53
- for (const line of lines) {
54
- if (line.includes("service=session") && line.includes("created")) {
55
- const match = line.match(SESSION_ID_PATTERN);
56
- if (match) {
57
- lastSessionId = match[1];
58
- }
59
- }
60
- }
61
-
62
- if (lastSessionId) {
63
- return lastSessionId;
64
- }
65
- }
66
-
67
- return null;
68
- } catch (error) {
69
- console.error(`[SESSION-ID] Failed to parse logs for ${agentName}:`, error);
70
- return null;
71
- }
72
- }
73
-
74
- /**
75
- * Waits for OpenCode to write a NEW session ID to logs (different from `previousId`).
76
- * Polls log files up to `maxWaitMs` with `intervalMs` between checks.
77
- *
78
- * This solves the race condition where we read logs before OpenCode has started
79
- * and see the old session ID, falsely concluding resume succeeded.
80
- *
81
- * @returns The new session ID, or null if timeout or no new session appeared
82
- */
83
- export async function waitForNewSessionId(
84
- agentName: string,
85
- previousId: string | null,
86
- maxWaitMs = 15000,
87
- intervalMs = 1000,
88
- ): Promise<string | null> {
89
- const deadline = Date.now() + maxWaitMs;
90
-
91
- while (Date.now() < deadline) {
92
- const currentId = getLatestSessionId(agentName);
93
-
94
- // If we see a different session ID than before, OpenCode has started
95
- if (currentId && currentId !== previousId) {
96
- return currentId;
97
- }
98
-
99
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
100
- }
101
-
102
- return null;
103
- }
104
-
105
- /**
106
- * Snapshots the latest session ID BEFORE starting OpenCode.
107
- * Used to detect whether OpenCode created a new session or resumed the requested one.
108
- */
109
- export function snapshotSessionId(agentName: string): string | null {
110
- return getLatestSessionId(agentName);
111
- }