Skip to content

Commit a99d338

Browse files
DevelopmentCats35C4n0rmatifali
authored
refactor(claude-code): simplify session resumption logic for standalone and task mode (#579)
## Description Brings session resumption up to speed with current claude-code capabilities, and should make session resumption less prone to errors across the board. I am still testing this further to ensure that all logic path's are verified. <!-- Briefly describe what this PR does and why --> ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information <!-- Delete this section if not applicable --> **Path:** `registry/coder/modules/claude-code` **New version:** `v4.2.5` **Breaking change:** [ ] Yes [ ] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues <!-- Link related issues or write "None" if not applicable --> --------- Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Co-authored-by: Atif Ali <atif@coder.com>
1 parent c62fe56 commit a99d338

File tree

6 files changed

+323
-194
lines changed

6 files changed

+323
-194
lines changed

registry/coder/modules/claude-code/README.md

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
1313
```tf
1414
module "claude-code" {
1515
source = "registry.coder.com/coder/claude-code/coder"
16-
version = "4.2.4"
17-
agent_id = coder_agent.example.id
16+
version = "4.2.5"
17+
agent_id = coder_agent.main.id
1818
workdir = "/home/coder/project"
1919
claude_api_key = "xxxx-xxxxx-xxxx"
2020
}
@@ -45,13 +45,15 @@ This example shows how to configure the Claude Code module to run the agent behi
4545
```tf
4646
module "claude-code" {
4747
source = "dev.registry.coder.com/coder/claude-code/coder"
48+
version = "4.2.5"
49+
agent_id = coder_agent.main.id
50+
workdir = "/home/coder/project"
4851
enable_boundary = true
4952
boundary_version = "main"
5053
boundary_log_dir = "/tmp/boundary_logs"
5154
boundary_log_level = "WARN"
5255
boundary_additional_allowed_urls = ["GET *google.com"]
5356
boundary_proxy_port = "8087"
54-
version = "4.2.4"
5557
}
5658
```
5759

@@ -70,8 +72,8 @@ data "coder_parameter" "ai_prompt" {
7072
7173
module "claude-code" {
7274
source = "registry.coder.com/coder/claude-code/coder"
73-
version = "4.2.4"
74-
agent_id = coder_agent.example.id
75+
version = "4.2.5"
76+
agent_id = coder_agent.main.id
7577
workdir = "/home/coder/project"
7678
7779
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -106,13 +108,12 @@ Run and configure Claude Code as a standalone CLI in your workspace.
106108
```tf
107109
module "claude-code" {
108110
source = "registry.coder.com/coder/claude-code/coder"
109-
version = "4.2.4"
110-
agent_id = coder_agent.example.id
111-
workdir = "/home/coder"
111+
version = "4.2.5"
112+
agent_id = coder_agent.main.id
113+
workdir = "/home/coder/project"
112114
install_claude_code = true
113115
claude_code_version = "2.0.62"
114116
report_tasks = false
115-
cli_app = true
116117
}
117118
```
118119

@@ -129,8 +130,8 @@ variable "claude_code_oauth_token" {
129130
130131
module "claude-code" {
131132
source = "registry.coder.com/coder/claude-code/coder"
132-
version = "4.2.4"
133-
agent_id = coder_agent.example.id
133+
version = "4.2.5"
134+
agent_id = coder_agent.main.id
134135
workdir = "/home/coder/project"
135136
claude_code_oauth_token = var.claude_code_oauth_token
136137
}
@@ -146,13 +147,13 @@ Configure Claude Code to use AWS Bedrock for accessing Claude models through you
146147

147148
```tf
148149
resource "coder_env" "bedrock_use" {
149-
agent_id = coder_agent.example.id
150+
agent_id = coder_agent.main.id
150151
name = "CLAUDE_CODE_USE_BEDROCK"
151152
value = "1"
152153
}
153154
154155
resource "coder_env" "aws_region" {
155-
agent_id = coder_agent.example.id
156+
agent_id = coder_agent.main.id
156157
name = "AWS_REGION"
157158
value = "us-east-1" # Choose your preferred region
158159
}
@@ -174,13 +175,13 @@ variable "aws_secret_access_key" {
174175
}
175176
176177
resource "coder_env" "aws_access_key_id" {
177-
agent_id = coder_agent.example.id
178+
agent_id = coder_agent.main.id
178179
name = "AWS_ACCESS_KEY_ID"
179180
value = var.aws_access_key_id
180181
}
181182
182183
resource "coder_env" "aws_secret_access_key" {
183-
agent_id = coder_agent.example.id
184+
agent_id = coder_agent.main.id
184185
name = "AWS_SECRET_ACCESS_KEY"
185186
value = var.aws_secret_access_key
186187
}
@@ -195,15 +196,15 @@ variable "aws_bearer_token_bedrock" {
195196
}
196197
197198
resource "coder_env" "bedrock_api_key" {
198-
agent_id = coder_agent.example.id
199+
agent_id = coder_agent.main.id
199200
name = "AWS_BEARER_TOKEN_BEDROCK"
200201
value = var.aws_bearer_token_bedrock
201202
}
202203
203204
module "claude-code" {
204205
source = "registry.coder.com/coder/claude-code/coder"
205-
version = "4.2.4"
206-
agent_id = coder_agent.example.id
206+
version = "4.2.5"
207+
agent_id = coder_agent.main.id
207208
workdir = "/home/coder/project"
208209
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
209210
}
@@ -228,39 +229,39 @@ variable "vertex_sa_json" {
228229
}
229230
230231
resource "coder_env" "vertex_use" {
231-
agent_id = coder_agent.example.id
232+
agent_id = coder_agent.main.id
232233
name = "CLAUDE_CODE_USE_VERTEX"
233234
value = "1"
234235
}
235236
236237
resource "coder_env" "vertex_project_id" {
237-
agent_id = coder_agent.example.id
238+
agent_id = coder_agent.main.id
238239
name = "ANTHROPIC_VERTEX_PROJECT_ID"
239240
value = "your-gcp-project-id"
240241
}
241242
242243
resource "coder_env" "cloud_ml_region" {
243-
agent_id = coder_agent.example.id
244+
agent_id = coder_agent.main.id
244245
name = "CLOUD_ML_REGION"
245246
value = "global"
246247
}
247248
248249
resource "coder_env" "vertex_sa_json" {
249-
agent_id = coder_agent.example.id
250+
agent_id = coder_agent.main.id
250251
name = "VERTEX_SA_JSON"
251252
value = var.vertex_sa_json
252253
}
253254
254255
resource "coder_env" "google_application_credentials" {
255-
agent_id = coder_agent.example.id
256+
agent_id = coder_agent.main.id
256257
name = "GOOGLE_APPLICATION_CREDENTIALS"
257258
value = "/tmp/gcp-sa.json"
258259
}
259260
260261
module "claude-code" {
261262
source = "registry.coder.com/coder/claude-code/coder"
262-
version = "4.2.4"
263-
agent_id = coder_agent.example.id
263+
version = "4.2.5"
264+
agent_id = coder_agent.main.id
264265
workdir = "/home/coder/project"
265266
model = "claude-sonnet-4@20250514"
266267

registry/coder/modules/claude-code/main.test.ts

Lines changed: 142 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,17 @@ describe("claude-code", async () => {
208208
});
209209

210210
// Create a mock task session file with the hardcoded task session ID
211+
// Note: Claude CLI creates files without "session-" prefix when using --session-id
211212
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
212213
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
213214
await execContainer(id, ["mkdir", "-p", sessionDir]);
214215
await execContainer(id, [
215216
"bash",
216217
"-c",
217-
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
218+
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF'
219+
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
220+
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
221+
SESSIONEOF`,
218222
]);
219223

220224
await execModuleScript(id);
@@ -226,46 +230,10 @@ describe("claude-code", async () => {
226230
]);
227231
expect(startLog.stdout).toContain("--resume");
228232
expect(startLog.stdout).toContain(taskSessionId);
229-
expect(startLog.stdout).toContain("Resuming existing task session");
233+
expect(startLog.stdout).toContain("Resuming task session");
230234
expect(startLog.stdout).toContain("--dangerously-skip-permissions");
231235
});
232236

233-
test("claude-continue-resume-standalone-session", async () => {
234-
const { id } = await setup({
235-
moduleVariables: {
236-
continue: "true",
237-
report_tasks: "false",
238-
ai_prompt: "test prompt",
239-
},
240-
});
241-
242-
const sessionId = "some-random-session-id";
243-
const workdir = "/home/coder/project";
244-
const claudeJson = {
245-
projects: {
246-
[workdir]: {
247-
lastSessionId: sessionId,
248-
},
249-
},
250-
};
251-
252-
await execContainer(id, [
253-
"bash",
254-
"-c",
255-
`echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`,
256-
]);
257-
258-
await execModuleScript(id);
259-
260-
const startLog = await execContainer(id, [
261-
"bash",
262-
"-c",
263-
"cat /home/coder/.claude-module/agentapi-start.log",
264-
]);
265-
expect(startLog.stdout).toContain("--continue");
266-
expect(startLog.stdout).toContain("Resuming existing session");
267-
});
268-
269237
test("pre-post-install-scripts", async () => {
270238
const { id } = await setup({
271239
moduleVariables: {
@@ -360,4 +328,140 @@ describe("claude-code", async () => {
360328
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
361329
);
362330
});
331+
332+
test("partial-initialization-detection", async () => {
333+
const { id } = await setup({
334+
moduleVariables: {
335+
continue: "true",
336+
report_tasks: "true",
337+
ai_prompt: "test prompt",
338+
},
339+
});
340+
341+
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
342+
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
343+
await execContainer(id, ["mkdir", "-p", sessionDir]);
344+
345+
await execContainer(id, [
346+
"bash",
347+
"-c",
348+
`echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`,
349+
]);
350+
351+
await execModuleScript(id);
352+
353+
const startLog = await execContainer(id, [
354+
"bash",
355+
"-c",
356+
"cat /home/coder/.claude-module/agentapi-start.log",
357+
]);
358+
359+
// Should start new session, not try to resume invalid one
360+
expect(startLog.stdout).toContain("Starting new task session");
361+
expect(startLog.stdout).toContain("--session-id");
362+
});
363+
364+
test("standalone-first-build-no-sessions", async () => {
365+
const { id } = await setup({
366+
moduleVariables: {
367+
continue: "true",
368+
report_tasks: "false",
369+
},
370+
});
371+
372+
await execModuleScript(id);
373+
374+
const startLog = await execContainer(id, [
375+
"bash",
376+
"-c",
377+
"cat /home/coder/.claude-module/agentapi-start.log",
378+
]);
379+
380+
// Should start fresh, not try to continue
381+
expect(startLog.stdout).toContain("No sessions found");
382+
expect(startLog.stdout).toContain("starting fresh standalone session");
383+
expect(startLog.stdout).not.toContain("--continue");
384+
});
385+
386+
test("standalone-with-sessions-continues", async () => {
387+
const { id } = await setup({
388+
moduleVariables: {
389+
continue: "true",
390+
report_tasks: "false",
391+
},
392+
});
393+
394+
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
395+
await execContainer(id, ["mkdir", "-p", sessionDir]);
396+
await execContainer(id, [
397+
"bash",
398+
"-c",
399+
`cat > ${sessionDir}/generic-123.jsonl << 'EOF'
400+
{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"}
401+
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
402+
EOF`,
403+
]);
404+
405+
await execModuleScript(id);
406+
407+
const startLog = await execContainer(id, [
408+
"bash",
409+
"-c",
410+
"cat /home/coder/.claude-module/agentapi-start.log",
411+
]);
412+
413+
// Should continue existing session
414+
expect(startLog.stdout).toContain("Sessions found");
415+
expect(startLog.stdout).toContain(
416+
"Continuing most recent standalone session",
417+
);
418+
expect(startLog.stdout).toContain("--continue");
419+
});
420+
421+
test("task-mode-ignores-manual-sessions", async () => {
422+
const { id } = await setup({
423+
moduleVariables: {
424+
continue: "true",
425+
report_tasks: "true",
426+
ai_prompt: "test prompt",
427+
},
428+
});
429+
430+
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
431+
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
432+
await execContainer(id, ["mkdir", "-p", sessionDir]);
433+
434+
// Create task session (without "session-" prefix, as CLI does)
435+
await execContainer(id, [
436+
"bash",
437+
"-c",
438+
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF'
439+
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
440+
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
441+
EOF`,
442+
]);
443+
444+
// Create manual session (newer)
445+
await execContainer(id, [
446+
"bash",
447+
"-c",
448+
`cat > ${sessionDir}/manual-456.jsonl << 'EOF'
449+
{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"}
450+
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"}
451+
EOF`,
452+
]);
453+
454+
await execModuleScript(id);
455+
456+
const startLog = await execContainer(id, [
457+
"bash",
458+
"-c",
459+
"cat /home/coder/.claude-module/agentapi-start.log",
460+
]);
461+
462+
// Should resume task session, not manual session
463+
expect(startLog.stdout).toContain("Resuming task session");
464+
expect(startLog.stdout).toContain(taskSessionId);
465+
expect(startLog.stdout).not.toContain("manual-456");
466+
});
363467
});

0 commit comments

Comments
 (0)