Skip to content

Commit 4f0a263

Browse files
authored
Merge pull request #58 from diggerhq/fix/deploy-worker-images-dir
Add agent resume, fix exec/run route, dev deploy improvements
2 parents b1bc2b5 + 480001c commit 4f0a263

File tree

7 files changed

+95
-12
lines changed

7 files changed

+95
-12
lines changed

deploy/ec2/deploy-aws-dev.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,11 @@ cmd_deploy() {
377377
log "Step 4: Installing env files..."
378378
ssh -o StrictHostKeyChecking=no -i "$key_file" ubuntu@"$public_ip" \
379379
"sudo bash ~/opensandbox/deploy/ec2/setup-dev-env.sh $API_KEY"
380+
# Add sandbox domain for preview URLs (nip.io resolves to the public IP)
381+
ssh -o StrictHostKeyChecking=no -i "$key_file" ubuntu@"$public_ip" "
382+
echo 'OPENSANDBOX_SANDBOX_DOMAIN=${public_ip}.nip.io' | sudo tee -a /etc/opensandbox/server.env
383+
echo 'OPENSANDBOX_SANDBOX_DOMAIN=${public_ip}.nip.io' | sudo tee -a /etc/opensandbox/worker.env
384+
"
380385

381386
log "Step 5: Starting/restarting services..."
382387
ssh -o StrictHostKeyChecking=no -i "$key_file" ubuntu@"$public_ip" "

internal/api/agent_session.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func (s *Server) createAgentSession(c echo.Context) error {
4949

5050
// Send configure command if any config options provided
5151
hasConfig := req.Model != "" || req.SystemPrompt != "" || len(req.AllowedTools) > 0 ||
52-
req.PermissionMode != "" || req.MaxTurns > 0 || req.Cwd != "" || len(req.McpServers) > 0
52+
req.PermissionMode != "" || req.MaxTurns > 0 || req.Cwd != "" || len(req.McpServers) > 0 ||
53+
req.Resume != ""
5354
if hasConfig && session.StdinWriter != nil {
5455
configCmd := map[string]interface{}{"type": "configure"}
5556
if req.Model != "" {
@@ -73,6 +74,9 @@ func (s *Server) createAgentSession(c echo.Context) error {
7374
if len(req.McpServers) > 0 {
7475
configCmd["mcpServers"] = req.McpServers
7576
}
77+
if req.Resume != "" {
78+
configCmd["resume"] = req.Resume
79+
}
7680
configJSON, _ := json.Marshal(configCmd)
7781
session.StdinWriter.Write(append(configJSON, '\n'))
7882
}
@@ -83,6 +87,9 @@ func (s *Server) createAgentSession(c echo.Context) error {
8387
"type": "prompt",
8488
"text": req.Prompt,
8589
}
90+
if req.Resume != "" {
91+
promptCmd["resume"] = req.Resume
92+
}
8693
promptJSON, _ := json.Marshal(promptCmd)
8794
session.StdinWriter.Write(append(promptJSON, '\n'))
8895
}

internal/worker/handlers.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,8 @@ func (s *HTTPServer) createAgentSession(c echo.Context) error {
633633

634634
// Send configure command if any config options provided
635635
hasConfig := req.Model != "" || req.SystemPrompt != "" || len(req.AllowedTools) > 0 ||
636-
req.PermissionMode != "" || req.MaxTurns > 0 || req.Cwd != "" || len(req.McpServers) > 0
636+
req.PermissionMode != "" || req.MaxTurns > 0 || req.Cwd != "" || len(req.McpServers) > 0 ||
637+
req.Resume != ""
637638
if hasConfig && session.StdinWriter != nil {
638639
configCmd := map[string]interface{}{"type": "configure"}
639640
if req.Model != "" {
@@ -657,6 +658,9 @@ func (s *HTTPServer) createAgentSession(c echo.Context) error {
657658
if len(req.McpServers) > 0 {
658659
configCmd["mcpServers"] = req.McpServers
659660
}
661+
if req.Resume != "" {
662+
configCmd["resume"] = req.Resume
663+
}
660664
configJSON, _ := jsonMarshal(configCmd)
661665
session.StdinWriter.Write(append(configJSON, '\n'))
662666
}

internal/worker/http_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ func NewHTTPServer(mgr sandbox.Manager, ptyMgr *sandbox.PTYManager, execMgr *san
8282
api.GET("/sandboxes/:id", s.getSandbox)
8383

8484
// Exec sessions (replaces old /commands)
85+
api.POST("/sandboxes/:id/exec/run", s.execRun) // static path before parameterized
8586
api.POST("/sandboxes/:id/exec", s.createExecSession)
8687
api.GET("/sandboxes/:id/exec", s.listExecSessions)
8788
api.GET("/sandboxes/:id/exec/:sessionID", s.execSessionWebSocket)
8889
api.POST("/sandboxes/:id/exec/:sessionID/kill", s.killExecSession)
89-
api.POST("/sandboxes/:id/exec/run", s.execRun)
9090

9191
// Timeout
9292
api.POST("/sandboxes/:id/timeout", s.setTimeout)

pkg/types/agent_session.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ type AgentSessionCreateRequest struct {
1010
MaxTurns int `json:"maxTurns,omitempty"`
1111
Cwd string `json:"cwd,omitempty"`
1212
McpServers map[string]interface{} `json:"mcpServers,omitempty"`
13+
Resume string `json:"resume,omitempty"`
1314
}
1415

1516
// AgentSessionInfo is the response body for agent session metadata.
1617
type AgentSessionInfo struct {
17-
SessionID string `json:"sessionID"`
18-
SandboxID string `json:"sandboxID"`
19-
Running bool `json:"running"`
20-
StartedAt string `json:"startedAt"`
18+
SessionID string `json:"sessionID"`
19+
SandboxID string `json:"sandboxID"`
20+
Running bool `json:"running"`
21+
StartedAt string `json:"startedAt"`
22+
ClaudeSessionID string `json:"claudeSessionID,omitempty"`
2123
}
2224

2325
// AgentPromptRequest is the request body for sending a prompt to an agent session.

scripts/claude-agent-wrapper/index.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ interface ConfigureCommand {
3737
maxTurns?: number;
3838
cwd?: string;
3939
mcpServers?: Record<string, unknown>;
40+
resume?: string;
4041
}
4142

4243
interface PromptCommand {
4344
type: "prompt";
4445
text: string;
46+
resume?: string;
4547
}
4648

4749
interface InterruptCommand {
@@ -60,6 +62,8 @@ let config: ConfigureCommand = {
6062
};
6163

6264
let activeQuery: Query | null = null;
65+
// Track the Claude session ID so we can resume later
66+
let claudeSessionId: string | null = null;
6367

6468
// --- Helpers ---
6569

@@ -109,21 +113,69 @@ async function handlePrompt(cmd: PromptCommand): Promise<void> {
109113
options.mcpServers = config.mcpServers as Options["mcpServers"];
110114
}
111115

116+
// Resume from a previous Claude session if requested
117+
// Priority: prompt command resume > config resume > auto-captured session ID
118+
const resumeId = cmd.resume || config.resume || claudeSessionId;
119+
if (resumeId) {
120+
options.resume = resumeId;
121+
const source = cmd.resume ? "prompt" : config.resume ? "config" : "auto";
122+
logError(`resuming from session: ${resumeId} (from ${source})`);
123+
// Clear config.resume after first use so subsequent prompts don't re-resume from config
124+
if (config.resume) {
125+
config.resume = undefined;
126+
}
127+
}
128+
129+
logError(`handlePrompt: prompt="${cmd.text.slice(0, 100)}", resume=${resumeId || "none"}, model=${options.model}, cwd=${options.cwd}`);
130+
112131
try {
113132
activeQuery = query({ prompt: cmd.text, options });
114133

115134
for await (const message of activeQuery) {
135+
// Capture the Claude session ID from messages for future resumption
136+
if ("session_id" in message && typeof message.session_id === "string") {
137+
if (!claudeSessionId || claudeSessionId !== message.session_id) {
138+
claudeSessionId = message.session_id;
139+
logError(`captured claude session_id: ${claudeSessionId}`);
140+
}
141+
}
116142
emitMessage(message);
117143
}
118144

119-
emit({ type: "turn_complete" });
145+
logError(`turn complete, claude_session_id=${claudeSessionId}`);
146+
emit({ type: "turn_complete", claude_session_id: claudeSessionId });
120147
} catch (err: unknown) {
121-
const message = err instanceof Error ? err.message : String(err);
122-
if (message.includes("abort") || message.includes("interrupt")) {
148+
const errMsg = err instanceof Error ? err.message : String(err);
149+
const errStack = err instanceof Error ? err.stack : "";
150+
logError(`prompt error: ${errMsg}\n${errStack}`);
151+
if (errMsg.includes("abort") || errMsg.includes("interrupt")) {
123152
emit({ type: "interrupted" });
124153
} else {
125-
logError(`prompt error: ${message}`);
126-
emit({ type: "error", message });
154+
// If resume failed, try again without resume
155+
if (resumeId) {
156+
logError(`resume failed, retrying without resume...`);
157+
options.resume = undefined;
158+
try {
159+
activeQuery = query({ prompt: cmd.text, options });
160+
for await (const message of activeQuery) {
161+
if ("session_id" in message && typeof message.session_id === "string") {
162+
if (!claudeSessionId || claudeSessionId !== message.session_id) {
163+
claudeSessionId = message.session_id;
164+
logError(`captured claude session_id (retry): ${claudeSessionId}`);
165+
}
166+
}
167+
emitMessage(message);
168+
}
169+
logError(`retry turn complete, claude_session_id=${claudeSessionId}`);
170+
emit({ type: "turn_complete", claude_session_id: claudeSessionId });
171+
} catch (retryErr: unknown) {
172+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
173+
logError(`retry also failed: ${retryMsg}`);
174+
emit({ type: "error", message: retryMsg });
175+
}
176+
} else {
177+
emit({ type: "error", message: errMsg });
178+
}
127179
}
128180
} finally {
129181
activeQuery = null;
@@ -200,7 +252,18 @@ async function main(): Promise<void> {
200252
}
201253
}
202254

255+
process.on("uncaughtException", (err) => {
256+
logError(`uncaughtException: ${err.message}\n${err.stack}`);
257+
emit({ type: "error", message: `uncaughtException: ${err.message}` });
258+
});
259+
260+
process.on("unhandledRejection", (reason) => {
261+
logError(`unhandledRejection: ${reason}`);
262+
emit({ type: "error", message: `unhandledRejection: ${String(reason)}` });
263+
});
264+
203265
main().catch((err) => {
204266
logError(`fatal: ${err}`);
267+
emit({ type: "error", message: `fatal: ${err instanceof Error ? err.message : String(err)}` });
205268
process.exit(1);
206269
});

sdks/typescript/src/agent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface AgentConfig {
2727
maxTurns?: number;
2828
cwd?: string;
2929
mcpServers?: Record<string, McpServerConfig>;
30+
resume?: string;
3031
}
3132

3233
export interface AgentStartOpts extends AgentConfig {
@@ -75,6 +76,7 @@ export class Agent {
7576
if (opts.maxTurns != null) body.maxTurns = opts.maxTurns;
7677
if (opts.cwd) body.cwd = opts.cwd;
7778
if (opts.mcpServers) body.mcpServers = opts.mcpServers;
79+
if (opts.resume) body.resume = opts.resume;
7880

7981
const resp = await fetch(`${this.apiUrl}/sandboxes/${this.sandboxId}/agent`, {
8082
method: "POST",

0 commit comments

Comments
 (0)