@agentmeshhq/agent 0.1.16 → 0.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/attach.js +0 -19
- package/dist/cli/attach.js.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/nudge.js +0 -13
- package/dist/cli/nudge.js.map +1 -1
- package/dist/cli/start.d.ts +2 -1
- package/dist/cli/start.js +6 -3
- package/dist/cli/start.js.map +1 -1
- package/dist/config/schema.d.ts +2 -0
- package/dist/core/daemon.d.ts +6 -1
- package/dist/core/daemon.js +162 -30
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/registry.js +1 -1
- package/dist/core/registry.js.map +1 -1
- package/dist/core/runner.js +5 -26
- package/dist/core/runner.js.map +1 -1
- package/dist/core/sandbox.js +2 -6
- package/dist/core/sandbox.js.map +1 -1
- package/dist/core/session-id.d.ts +30 -0
- package/dist/core/session-id.js +90 -0
- package/dist/core/session-id.js.map +1 -0
- package/dist/core/tmux.d.ts +1 -1
- package/dist/core/tmux.js +9 -1
- package/dist/core/tmux.js.map +1 -1
- package/dist/core/watchdog.js +2 -7
- package/dist/core/watchdog.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/attach.ts +0 -21
- package/src/cli/index.ts +2 -1
- package/src/cli/nudge.ts +0 -18
- package/src/cli/start.ts +6 -3
- package/src/config/schema.ts +2 -0
- package/src/core/daemon.ts +183 -32
- package/src/core/registry.ts +1 -1
- package/src/core/runner.ts +5 -28
- package/src/core/sandbox.ts +2 -5
- package/src/core/session-id.ts +111 -0
- package/src/core/tmux.ts +12 -0
- package/src/core/watchdog.ts +2 -7
package/src/cli/attach.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
1
|
import pc from "picocolors";
|
|
3
|
-
import { getAgentState } from "../config/loader.js";
|
|
4
2
|
import { attachSession, getSessionName, sessionExists } from "../core/tmux.js";
|
|
5
3
|
|
|
6
4
|
export function attach(name: string): void {
|
|
@@ -9,25 +7,6 @@ export function attach(name: string): void {
|
|
|
9
7
|
process.exit(1);
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
// Check if this is a sandbox agent
|
|
13
|
-
const localAgent = getAgentState(name);
|
|
14
|
-
|
|
15
|
-
if (localAgent?.sandboxContainer) {
|
|
16
|
-
// Sandbox agent - attach via docker exec
|
|
17
|
-
console.log(`Attaching to sandbox container ${localAgent.sandboxContainer}...`);
|
|
18
|
-
console.log(pc.dim("Detach with: Ctrl+B, D\n"));
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
execSync(`docker exec -it ${localAgent.sandboxContainer} agentmesh attach ${name}`, {
|
|
22
|
-
stdio: "inherit",
|
|
23
|
-
});
|
|
24
|
-
} catch {
|
|
25
|
-
// execSync throws on non-zero exit, but that's expected when detaching
|
|
26
|
-
}
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Host agent - attach via tmux
|
|
31
10
|
const sessionName = getSessionName(name);
|
|
32
11
|
|
|
33
12
|
if (!sessionExists(sessionName)) {
|
package/src/cli/index.ts
CHANGED
|
@@ -53,7 +53,8 @@ program
|
|
|
53
53
|
.option("-w, --workdir <path>", "Working directory")
|
|
54
54
|
.option("-m, --model <model>", "Model identifier")
|
|
55
55
|
.option("-f, --foreground", "Run in foreground (blocking)")
|
|
56
|
-
.option("--
|
|
56
|
+
.option("--restore-context", "Restore context from previous session (default: disabled)")
|
|
57
|
+
.option("--worker", "Enable auto-nudge and restart for worker agents (default: disabled)")
|
|
57
58
|
.option("--auto-setup", "Auto-clone repository for project assignments")
|
|
58
59
|
.option("--serve", "Run opencode serve instead of tmux TUI (for Integration Service)")
|
|
59
60
|
.option("--serve-port <port>", "Port for opencode serve (default: 3001)", "3001")
|
package/src/cli/nudge.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import pc from "picocolors";
|
|
3
2
|
import { getAgentState, loadConfig, loadState } from "../config/loader.js";
|
|
4
3
|
import { registerAgent } from "../core/registry.js";
|
|
@@ -25,23 +24,6 @@ export async function nudge(name: string, message: string): Promise<void> {
|
|
|
25
24
|
// Check if this is a local agent
|
|
26
25
|
const localAgent = getAgentState(name);
|
|
27
26
|
|
|
28
|
-
// Sandbox agent - route nudge through docker exec
|
|
29
|
-
if (localAgent?.sandboxContainer) {
|
|
30
|
-
const result = spawnSync(
|
|
31
|
-
"docker",
|
|
32
|
-
["exec", localAgent.sandboxContainer, "agentmesh", "nudge", name, message],
|
|
33
|
-
{ encoding: "utf-8", stdio: "pipe" },
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
if (result.status === 0) {
|
|
37
|
-
console.log(pc.green(`Nudged "${name}" in sandbox.`));
|
|
38
|
-
} else {
|
|
39
|
-
console.log(pc.red(`Failed to nudge "${name}" in sandbox: ${result.stderr}`));
|
|
40
|
-
}
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Local agent with tmux session on host
|
|
45
27
|
if (localAgent && sessionExists(getSessionName(name))) {
|
|
46
28
|
// Local nudge via tmux send-keys
|
|
47
29
|
const formatted = `[AgentMesh] Nudge from CLI:
|
package/src/cli/start.ts
CHANGED
|
@@ -12,7 +12,8 @@ export interface StartOptions {
|
|
|
12
12
|
workdir?: string;
|
|
13
13
|
model?: string;
|
|
14
14
|
foreground?: boolean;
|
|
15
|
-
|
|
15
|
+
restoreContext?: boolean;
|
|
16
|
+
worker?: boolean;
|
|
16
17
|
autoSetup?: boolean;
|
|
17
18
|
/** Run opencode serve instead of tmux TUI (for Integration Service) */
|
|
18
19
|
serve?: boolean;
|
|
@@ -56,7 +57,8 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
56
57
|
try {
|
|
57
58
|
const daemon = new AgentDaemon({
|
|
58
59
|
...options,
|
|
59
|
-
restoreContext:
|
|
60
|
+
restoreContext: options.restoreContext,
|
|
61
|
+
worker: options.worker,
|
|
60
62
|
autoSetup: options.autoSetup,
|
|
61
63
|
});
|
|
62
64
|
await daemon.start();
|
|
@@ -80,7 +82,8 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
80
82
|
if (options.command) args.push("--command", options.command);
|
|
81
83
|
if (options.workdir) args.push("--workdir", options.workdir);
|
|
82
84
|
if (options.model) args.push("--model", options.model);
|
|
83
|
-
if (options.
|
|
85
|
+
if (options.restoreContext) args.push("--restore-context");
|
|
86
|
+
if (options.worker) args.push("--worker");
|
|
84
87
|
if (options.autoSetup) args.push("--auto-setup");
|
|
85
88
|
if (options.serve) {
|
|
86
89
|
args.push("--serve");
|
package/src/config/schema.ts
CHANGED
package/src/core/daemon.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } fro
|
|
|
16
16
|
import { checkInbox, fetchAssignments, registerAgent, type ServerContext } from "./registry.js";
|
|
17
17
|
import { buildRunnerConfig, getRunnerDisplayName, type RunnerConfig } from "./runner.js";
|
|
18
18
|
import { DockerSandbox } from "./sandbox.js";
|
|
19
|
+
import { getLatestSessionId, snapshotSessionId, waitForNewSessionId } from "./session-id.js";
|
|
19
20
|
import {
|
|
20
21
|
captureSessionContext,
|
|
21
22
|
captureSessionOutput,
|
|
@@ -56,8 +57,10 @@ export interface DaemonOptions {
|
|
|
56
57
|
workdir?: string;
|
|
57
58
|
model?: string;
|
|
58
59
|
daemonize?: boolean;
|
|
59
|
-
/** Whether to restore context from previous session (default:
|
|
60
|
+
/** Whether to restore context from previous session (default: false) */
|
|
60
61
|
restoreContext?: boolean;
|
|
62
|
+
/** Only send nudges/restart for worker agents (default: false) */
|
|
63
|
+
worker?: boolean;
|
|
61
64
|
/** Auto-clone repository for project assignments */
|
|
62
65
|
autoSetup?: boolean;
|
|
63
66
|
/** Run opencode serve instead of tmux TUI (for Integration Service) */
|
|
@@ -86,6 +89,7 @@ export class AgentDaemon {
|
|
|
86
89
|
private isRunning = false;
|
|
87
90
|
private assignedProject: string | undefined;
|
|
88
91
|
private shouldRestoreContext: boolean;
|
|
92
|
+
private isWorkerAgent: boolean;
|
|
89
93
|
private autoSetup: boolean;
|
|
90
94
|
private serveMode: boolean;
|
|
91
95
|
private servePort: number;
|
|
@@ -97,6 +101,9 @@ export class AgentDaemon {
|
|
|
97
101
|
private sandbox: DockerSandbox | null = null;
|
|
98
102
|
private healthCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
99
103
|
private serverContext: ServerContext | undefined;
|
|
104
|
+
// Session resume tracking
|
|
105
|
+
private _preStartSessionId: string | null | undefined;
|
|
106
|
+
private _attemptedResumeSessionId: string | undefined;
|
|
100
107
|
// Auto-restart tracking
|
|
101
108
|
private restartCount = 0;
|
|
102
109
|
private lastStableTime: Date | null = null;
|
|
@@ -111,11 +118,12 @@ export class AgentDaemon {
|
|
|
111
118
|
|
|
112
119
|
// Ensure config has required fields with defaults
|
|
113
120
|
if (!config.agents) config.agents = [];
|
|
114
|
-
if (!config.defaults) config.defaults = { command: "opencode", model: "claude-sonnet-4" };
|
|
121
|
+
if (!config.defaults) config.defaults = { command: "opencode", model: "claude-sonnet-4-5-20250929" };
|
|
115
122
|
|
|
116
123
|
this.config = config;
|
|
117
124
|
this.agentName = options.name;
|
|
118
|
-
this.shouldRestoreContext = options.restoreContext
|
|
125
|
+
this.shouldRestoreContext = options.restoreContext === true;
|
|
126
|
+
this.isWorkerAgent = options.worker === true;
|
|
119
127
|
this.autoSetup = options.autoSetup === true;
|
|
120
128
|
|
|
121
129
|
// Find or create agent config
|
|
@@ -231,6 +239,20 @@ export class AgentDaemon {
|
|
|
231
239
|
|
|
232
240
|
// Create tmux session if it doesn't exist
|
|
233
241
|
if (!sessionAlreadyExists) {
|
|
242
|
+
// Load saved context to check for OpenCode session ID (for native resume)
|
|
243
|
+
let savedSessionId: string | undefined;
|
|
244
|
+
if (this.shouldRestoreContext && this.agentId) {
|
|
245
|
+
const savedContext = loadContext(this.agentId);
|
|
246
|
+
savedSessionId = savedContext?.custom?.opencodeSessionId as string | undefined;
|
|
247
|
+
if (savedSessionId) {
|
|
248
|
+
console.log(`[CONTEXT] Found saved OpenCode session ID: ${savedSessionId}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Snapshot the latest session ID in logs BEFORE starting OpenCode.
|
|
253
|
+
// This lets us detect whether OpenCode actually resumed vs created a new session.
|
|
254
|
+
const preStartSessionId = snapshotSessionId(this.agentName);
|
|
255
|
+
|
|
234
256
|
console.log(`Creating tmux session: ${sessionName}`);
|
|
235
257
|
|
|
236
258
|
// Include runner env vars (e.g., OPENCODE_MODEL) at session creation
|
|
@@ -239,11 +261,16 @@ export class AgentDaemon {
|
|
|
239
261
|
this.agentConfig.command,
|
|
240
262
|
this.agentConfig.workdir,
|
|
241
263
|
this.runnerConfig.env, // Apply model env at process start
|
|
264
|
+
savedSessionId, // Resume OpenCode session if available
|
|
242
265
|
);
|
|
243
266
|
|
|
244
267
|
if (!created) {
|
|
245
268
|
throw new Error("Failed to create tmux session");
|
|
246
269
|
}
|
|
270
|
+
|
|
271
|
+
// Store pre-start snapshot for fallback detection later
|
|
272
|
+
this._preStartSessionId = preStartSessionId;
|
|
273
|
+
this._attemptedResumeSessionId = savedSessionId;
|
|
247
274
|
} else {
|
|
248
275
|
console.log(`Reconnecting to existing session: ${sessionName}`);
|
|
249
276
|
// Update environment for existing session
|
|
@@ -371,10 +398,61 @@ export class AgentDaemon {
|
|
|
371
398
|
console.log("Checking for previous context...");
|
|
372
399
|
const savedContext = loadContext(this.agentId);
|
|
373
400
|
if (savedContext) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
401
|
+
if (this._attemptedResumeSessionId && !this.serveMode && !this.sandboxMode) {
|
|
402
|
+
// Native session resume was attempted — verify it worked.
|
|
403
|
+
// Wait for OpenCode to write a NEW session entry to logs.
|
|
404
|
+
// If resume succeeded, it reuses the session (no new entry).
|
|
405
|
+
// If resume failed, OpenCode creates a new session (new entry appears).
|
|
406
|
+
const newSessionId = await waitForNewSessionId(
|
|
407
|
+
this.agentName,
|
|
408
|
+
this._preStartSessionId ?? null,
|
|
409
|
+
15000,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (!newSessionId) {
|
|
413
|
+
// No new session appeared in logs. Could mean:
|
|
414
|
+
// a) Resume succeeded (OpenCode reused session, no new "created" log)
|
|
415
|
+
// b) OpenCode is sitting at splash (session not found, no new session created)
|
|
416
|
+
const health = isSessionHealthy(this.agentName);
|
|
417
|
+
const currentSessionId = getLatestSessionId(this.agentName);
|
|
418
|
+
|
|
419
|
+
if (!health.healthy) {
|
|
420
|
+
// OpenCode died — clear stale session ID to prevent restart loop
|
|
421
|
+
console.log(`[CONTEXT] Fallback: OpenCode not healthy, injecting text summary.`);
|
|
422
|
+
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
423
|
+
saveContext(savedContext);
|
|
424
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
425
|
+
} else if (currentSessionId === this._attemptedResumeSessionId) {
|
|
426
|
+
// The session ID we tried to resume is still the latest — resume worked
|
|
427
|
+
console.log(`[CONTEXT] Resumed OpenCode session ${this._attemptedResumeSessionId}`);
|
|
428
|
+
} else {
|
|
429
|
+
// Pane is alive but no matching session ID in logs — OpenCode is at splash
|
|
430
|
+
console.log(
|
|
431
|
+
`[CONTEXT] Fallback: session not found in OpenCode. Injecting text summary.`,
|
|
432
|
+
);
|
|
433
|
+
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
434
|
+
saveContext(savedContext);
|
|
435
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
436
|
+
}
|
|
437
|
+
} else if (newSessionId === this._attemptedResumeSessionId) {
|
|
438
|
+
// OpenCode logged the same session ID — resume succeeded
|
|
439
|
+
console.log(`[CONTEXT] Resumed OpenCode session ${this._attemptedResumeSessionId}`);
|
|
440
|
+
} else {
|
|
441
|
+
// OpenCode created a different session — resume failed, fallback to text.
|
|
442
|
+
// Update saved context with new session ID to prevent restart loop.
|
|
443
|
+
console.log(
|
|
444
|
+
`[CONTEXT] Fallback: resume failed (expected ${this._attemptedResumeSessionId}, got ${newSessionId}). Injecting text summary.`,
|
|
445
|
+
);
|
|
446
|
+
savedContext.custom = { ...savedContext.custom, opencodeSessionId: newSessionId };
|
|
447
|
+
saveContext(savedContext);
|
|
448
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
// No session ID saved or non-tmux mode — use text injection
|
|
452
|
+
console.log(`Restoring context from ${savedContext.savedAt}`);
|
|
453
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
454
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
455
|
+
}
|
|
378
456
|
}
|
|
379
457
|
}
|
|
380
458
|
|
|
@@ -491,6 +569,11 @@ ${lastOutput}
|
|
|
491
569
|
|
|
492
570
|
fs.appendFileSync(logFile, crashLog);
|
|
493
571
|
|
|
572
|
+
// Save context (including session ID) before restart attempt
|
|
573
|
+
if (this.agentId) {
|
|
574
|
+
this.saveAgentContext();
|
|
575
|
+
}
|
|
576
|
+
|
|
494
577
|
// Check if we can restart
|
|
495
578
|
if (this.restartCount < MAX_RESTART_ATTEMPTS) {
|
|
496
579
|
this.restartCount++;
|
|
@@ -557,34 +640,39 @@ ${lastOutput}
|
|
|
557
640
|
});
|
|
558
641
|
}
|
|
559
642
|
|
|
560
|
-
//
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
this.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
643
|
+
// Only nudge worker agents - others restart immediately
|
|
644
|
+
if (this.isWorkerAgent) {
|
|
645
|
+
// If we haven't sent a nudge yet, send one
|
|
646
|
+
if (!this.nudgeSentAt) {
|
|
647
|
+
console.log(`[HEALTH] Sending nudge to worker agent...`);
|
|
648
|
+
|
|
649
|
+
const nudgeMessage =
|
|
650
|
+
progress.status === "permission_blocked"
|
|
651
|
+
? "Please continue with your task. If you see a permission prompt, try an alternative approach that doesn't require that permission."
|
|
652
|
+
: "Please continue with your current task.";
|
|
653
|
+
|
|
654
|
+
const sent = sendNudge(this.agentName, nudgeMessage);
|
|
655
|
+
if (sent) {
|
|
656
|
+
this.nudgeSentAt = now;
|
|
657
|
+
console.log(`[HEALTH] Nudge sent successfully`);
|
|
658
|
+
} else {
|
|
659
|
+
console.log(`[HEALTH] Failed to send nudge`);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
575
662
|
}
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
663
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
664
|
+
// Check if enough time has passed since nudge
|
|
665
|
+
const timeSinceNudge = now.getTime() - this.nudgeSentAt.getTime();
|
|
666
|
+
if (timeSinceNudge < NUDGE_WAIT_MS) {
|
|
667
|
+
// Still waiting for agent to respond to nudge
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
584
670
|
}
|
|
585
671
|
|
|
586
|
-
// Agent still stuck
|
|
587
|
-
console.log(
|
|
672
|
+
// Agent still stuck - trigger restart (or restart immediately if not a worker)
|
|
673
|
+
console.log(
|
|
674
|
+
`[HEALTH] Agent still stuck${this.isWorkerAgent ? " after nudge" : ""}, triggering restart...`,
|
|
675
|
+
);
|
|
588
676
|
this.stuckSince = null;
|
|
589
677
|
this.nudgeSentAt = null;
|
|
590
678
|
|
|
@@ -638,12 +726,25 @@ ${lastOutput}
|
|
|
638
726
|
sandboxContainer: containerName,
|
|
639
727
|
});
|
|
640
728
|
} else {
|
|
641
|
-
// Non-sandbox restart
|
|
729
|
+
// Non-sandbox restart — load saved session ID for native resume
|
|
730
|
+
let savedSessionId: string | undefined;
|
|
731
|
+
let savedContext = null;
|
|
732
|
+
if (this.agentId) {
|
|
733
|
+
savedContext = loadContext(this.agentId);
|
|
734
|
+
savedSessionId = savedContext?.custom?.opencodeSessionId as string | undefined;
|
|
735
|
+
if (savedSessionId) {
|
|
736
|
+
console.log(`[RESTART] Attempting to resume OpenCode session: ${savedSessionId}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const preRestartSessionId = snapshotSessionId(this.agentName);
|
|
741
|
+
|
|
642
742
|
const created = createSession(
|
|
643
743
|
this.agentName,
|
|
644
744
|
this.agentConfig.command,
|
|
645
745
|
this.agentConfig.workdir,
|
|
646
746
|
this.runnerConfig.env,
|
|
747
|
+
savedSessionId,
|
|
647
748
|
);
|
|
648
749
|
|
|
649
750
|
if (!created) {
|
|
@@ -656,6 +757,39 @@ ${lastOutput}
|
|
|
656
757
|
AGENTMESH_AGENT_ID: this.agentId!,
|
|
657
758
|
...this.runnerConfig.env,
|
|
658
759
|
});
|
|
760
|
+
|
|
761
|
+
// Verify native resume and fallback if needed
|
|
762
|
+
if (savedSessionId && savedContext) {
|
|
763
|
+
const newSessionId = await waitForNewSessionId(this.agentName, preRestartSessionId, 15000);
|
|
764
|
+
|
|
765
|
+
if (!newSessionId) {
|
|
766
|
+
const health = isSessionHealthy(this.agentName);
|
|
767
|
+
const currentSessionId = getLatestSessionId(this.agentName);
|
|
768
|
+
|
|
769
|
+
if (!health.healthy) {
|
|
770
|
+
console.log(`[RESTART] Fallback: OpenCode not healthy, injecting text summary.`);
|
|
771
|
+
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
772
|
+
saveContext(savedContext);
|
|
773
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
774
|
+
} else if (currentSessionId === savedSessionId) {
|
|
775
|
+
console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
|
|
776
|
+
} else {
|
|
777
|
+
console.log(`[RESTART] Fallback: session not found. Injecting text summary.`);
|
|
778
|
+
savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
|
|
779
|
+
saveContext(savedContext);
|
|
780
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
781
|
+
}
|
|
782
|
+
} else if (newSessionId === savedSessionId) {
|
|
783
|
+
console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
|
|
784
|
+
} else {
|
|
785
|
+
console.log(
|
|
786
|
+
`[RESTART] Fallback: resume failed (got ${newSessionId}). Injecting text summary.`,
|
|
787
|
+
);
|
|
788
|
+
savedContext.custom = { ...savedContext.custom, opencodeSessionId: newSessionId };
|
|
789
|
+
saveContext(savedContext);
|
|
790
|
+
injectRestoredContext(this.agentName, savedContext);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
659
793
|
}
|
|
660
794
|
|
|
661
795
|
// Wait for session to be ready
|
|
@@ -792,6 +926,16 @@ ${lastOutput}
|
|
|
792
926
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
793
927
|
|
|
794
928
|
console.log(`opencode serve started on http://0.0.0.0:${this.servePort}`);
|
|
929
|
+
|
|
930
|
+
// Store saved session ID for integration service reuse
|
|
931
|
+
if (this.shouldRestoreContext && this.agentId) {
|
|
932
|
+
const savedContext = loadContext(this.agentId);
|
|
933
|
+
const savedSessionId = savedContext?.custom?.opencodeSessionId as string | undefined;
|
|
934
|
+
if (savedSessionId) {
|
|
935
|
+
console.log(`[SERVE] Saved OpenCode session available for reuse: ${savedSessionId}`);
|
|
936
|
+
updateAgentInState(this.agentName, { opencodeSessionId: savedSessionId });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
795
939
|
}
|
|
796
940
|
|
|
797
941
|
/**
|
|
@@ -961,6 +1105,13 @@ Logs: docker logs ${containerName}
|
|
|
961
1105
|
};
|
|
962
1106
|
}
|
|
963
1107
|
|
|
1108
|
+
// Capture OpenCode session ID for native resume on restart
|
|
1109
|
+
const sessionId = getLatestSessionId(this.agentName);
|
|
1110
|
+
if (sessionId) {
|
|
1111
|
+
context.custom = { ...context.custom, opencodeSessionId: sessionId };
|
|
1112
|
+
console.log(`[CONTEXT] Captured OpenCode session ID: ${sessionId}`);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
964
1115
|
// Save updated context
|
|
965
1116
|
saveContext(context);
|
|
966
1117
|
console.log(`Context saved for agent ${this.agentName}`);
|
package/src/core/registry.ts
CHANGED
|
@@ -48,7 +48,7 @@ export async function registerAgent(options: RegisterOptions): Promise<RegisterR
|
|
|
48
48
|
model: options.model,
|
|
49
49
|
capabilities: options.capabilities || ["coding", "review", "debugging"],
|
|
50
50
|
workspace: options.workspace,
|
|
51
|
-
restore_context: options.restoreContext ??
|
|
51
|
+
restore_context: options.restoreContext ?? false,
|
|
52
52
|
}),
|
|
53
53
|
});
|
|
54
54
|
|
package/src/core/runner.ts
CHANGED
|
@@ -122,40 +122,17 @@ export function validateOpenCodeModel(model: string): { valid: boolean; error?:
|
|
|
122
122
|
|
|
123
123
|
// Common model aliases to their full OpenCode names
|
|
124
124
|
const MODEL_ALIASES: Record<string, string> = {
|
|
125
|
-
// Anthropic — Claude
|
|
126
125
|
"claude-sonnet-4": "anthropic/claude-sonnet-4-5",
|
|
127
|
-
"claude-sonnet-4-0": "anthropic/claude-sonnet-4-0",
|
|
128
126
|
"claude-sonnet-4-5": "anthropic/claude-sonnet-4-5",
|
|
129
|
-
"claude-sonnet-4-6": "anthropic/claude-sonnet-4-6",
|
|
130
127
|
"claude-opus-4": "anthropic/claude-opus-4-5",
|
|
131
|
-
"claude-opus-4-0": "anthropic/claude-opus-4-0",
|
|
132
|
-
"claude-opus-4-1": "anthropic/claude-opus-4-1",
|
|
133
128
|
"claude-opus-4-5": "anthropic/claude-opus-4-5",
|
|
134
|
-
"claude-opus-4-6": "anthropic/claude-opus-4-6",
|
|
135
129
|
"claude-haiku-4": "anthropic/claude-haiku-4-5",
|
|
136
130
|
"claude-haiku-4-5": "anthropic/claude-haiku-4-5",
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"gpt-5.2": "openai/gpt-5.2",
|
|
143
|
-
"gpt-5.3-codex": "openai/gpt-5.3-codex",
|
|
144
|
-
"gpt-5.2-codex": "openai/gpt-5.2-codex",
|
|
145
|
-
"gpt-5.1-codex": "openai/gpt-5.1-codex",
|
|
146
|
-
"gpt-5.1-codex-max": "openai/gpt-5.1-codex-max",
|
|
147
|
-
"gpt-5.1-codex-mini": "openai/gpt-5.1-codex-mini",
|
|
148
|
-
"gpt-5-codex": "openai/gpt-5-codex",
|
|
149
|
-
"codex-mini": "openai/codex-mini-latest",
|
|
150
|
-
codex: "openai/gpt-5.3-codex",
|
|
151
|
-
|
|
152
|
-
// xAI — Grok
|
|
153
|
-
"grok-4": "xai/grok-4",
|
|
154
|
-
"grok-4-fast": "xai/grok-4-fast",
|
|
155
|
-
"grok-3": "xai/grok-3",
|
|
156
|
-
"grok-3-fast": "xai/grok-3-fast",
|
|
157
|
-
"grok-3-mini": "xai/grok-3-mini",
|
|
158
|
-
"grok-code": "xai/grok-code-fast-1",
|
|
131
|
+
"gpt-4o": "openai/gpt-4o",
|
|
132
|
+
"gpt-4": "openai/gpt-4",
|
|
133
|
+
o3: "openai/o3",
|
|
134
|
+
"o3-mini": "openai/o3-mini",
|
|
135
|
+
codex: "openai/codex",
|
|
159
136
|
};
|
|
160
137
|
|
|
161
138
|
/**
|
package/src/core/sandbox.ts
CHANGED
|
@@ -225,11 +225,8 @@ export class DockerSandbox {
|
|
|
225
225
|
// Image and command
|
|
226
226
|
args.push(this.config.image);
|
|
227
227
|
|
|
228
|
-
// Command:
|
|
229
|
-
if (this.config.
|
|
230
|
-
// Custom command (e.g., agentmesh start inside container)
|
|
231
|
-
args.push(...this.config.command);
|
|
232
|
-
} else if (this.config.serveMode) {
|
|
228
|
+
// Command: either serve mode or keep container alive for tmux-style attach
|
|
229
|
+
if (this.config.serveMode) {
|
|
233
230
|
args.push(
|
|
234
231
|
"opencode",
|
|
235
232
|
"serve",
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
}
|
package/src/core/tmux.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function createSession(
|
|
|
31
31
|
command: string,
|
|
32
32
|
workdir?: string,
|
|
33
33
|
env?: SessionEnv,
|
|
34
|
+
opencodeSessionId?: string,
|
|
34
35
|
): boolean {
|
|
35
36
|
const sessionName = getSessionName(agentName);
|
|
36
37
|
|
|
@@ -92,6 +93,17 @@ export function createSession(
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
// Append --session --continue flags for native session resume
|
|
97
|
+
if (
|
|
98
|
+
opencodeSessionId &&
|
|
99
|
+
(finalCommand === "opencode" || finalCommand.startsWith("opencode ")) &&
|
|
100
|
+
!finalCommand.includes("--session") &&
|
|
101
|
+
!finalCommand.includes("--continue")
|
|
102
|
+
) {
|
|
103
|
+
finalCommand = `${finalCommand} --session ${opencodeSessionId} --continue`;
|
|
104
|
+
console.log(`[TMUX] Resuming OpenCode session: ${opencodeSessionId}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
const fullCommand = `${envPrefix}${finalCommand}`;
|
|
96
108
|
|
|
97
109
|
// Set reasonable terminal size for TUI applications
|
package/src/core/watchdog.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { execSync, spawnSync } from "node:child_process";
|
|
9
|
-
import fs from "node:fs";
|
|
10
9
|
import { captureSessionOutput } from "./tmux.js";
|
|
11
10
|
|
|
12
11
|
export type WatchdogStatus = "active" | "idle" | "stuck" | "permission_blocked";
|
|
@@ -139,12 +138,8 @@ export function getLastActivityTime(agentName: string, containerName?: string):
|
|
|
139
138
|
}
|
|
140
139
|
logLine = result.stdout.trim();
|
|
141
140
|
} else {
|
|
142
|
-
// Non-sandbox mode: read from
|
|
143
|
-
|
|
144
|
-
const agentLogDir = `${process.env.HOME}/.agentmesh/opencode-data/${agentName}/opencode/log`;
|
|
145
|
-
const sharedLogDir = `${process.env.HOME}/.local/share/opencode/log`;
|
|
146
|
-
// Prefer agent-specific logs, fall back to shared dir for backwards compatibility
|
|
147
|
-
const logDir = fs.existsSync(agentLogDir) ? agentLogDir : sharedLogDir;
|
|
141
|
+
// Non-sandbox mode: read from local logs
|
|
142
|
+
const logDir = `${process.env.HOME}/.local/share/opencode/log`;
|
|
148
143
|
const result = spawnSync(
|
|
149
144
|
"sh",
|
|
150
145
|
["-c", `ls -t ${logDir}/*.log 2>/dev/null | head -1 | xargs tail -1 2>/dev/null`],
|