Skip to content

Commit 4c8692a

Browse files
committed
Release v0.2.40
1 parent 931f16d commit 4c8692a

67 files changed

Lines changed: 2751 additions & 149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ permissions:
1010

1111
jobs:
1212
verify:
13-
runs-on: ubuntu-latest
13+
# Pinned: Playwright 1.55 cannot install Chromium on ubuntu-latest (26.04).
14+
runs-on: ubuntu-24.04
1415
steps:
1516
- uses: actions/checkout@v4
1617
- uses: pnpm/action-setup@v6.1.0

‎.github/workflows/deploy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
deploy:
2424
name: Deploy
2525
environment: production
26-
runs-on: ubuntu-latest
26+
runs-on: ubuntu-24.04
2727
timeout-minutes: 60
2828
steps:
2929
- uses: actions/checkout@v4

‎.github/workflows/orb-image.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ concurrency:
2525

2626
jobs:
2727
publish:
28-
runs-on: ubuntu-latest
28+
runs-on: ubuntu-24.04
2929
steps:
3030
- uses: actions/checkout@v4
3131
- uses: docker/setup-buildx-action@v3

‎RELEASE.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
2-
"version": "0.2.39",
2+
"version": "0.2.40",
33
"license": "FSL-1.1-ALv2",
44
"baselineSha256": "7847f65a9b20b366a60d52d19ca3cd6bdd1bb09b8cb506659a270f27a21d8877",
55
"e2b": {
66
"recipeSha256": "761809ee2efa2b6c750451ace29283cbb8e55b6ce3b5b14c2bb4509df71df801",
77
"profileSha256": "414dbecee2d47de36016ea05c736e2edff3a0deb298babf923887ea184a01c10"
88
},
99
"dxd": {
10-
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.39/dxd-linux-x64",
11-
"sha256": "d3a85385666b547620a62444b8267b84d484e551266e5b272b0ab3d7646b1455"
10+
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.40/dxd-linux-x64",
11+
"sha256": "ec3cc56921fc136a3d36fc2dbaf19d22dd70d27b12912317a0cef5cd170914f7"
1212
},
1313
"toolchain": {
1414
"node": ">=22.19.0",

‎apps/core/package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@dx/core",
33
"private": true,
4-
"version": "0.2.39",
4+
"version": "0.2.40",
55
"license": "FSL-1.1-ALv2",
66
"type": "module",
77
"scripts": {
@@ -20,7 +20,7 @@
2020
"@dx/api": "workspace:^",
2121
"@dx/domain": "workspace:^",
2222
"@effect/sql-d1": "4.0.5",
23-
"@flue/runtime": "file:../../third-party/flue/runtime-7047c601e7923609e65a9ae09d25eefd9081b2df.tgz",
23+
"@flue/runtime": "file:../../third-party/flue/runtime-de1dda9ae8b4791dea431cbdcbc818f4b5f22f71.tgz",
2424
"@jitl/quickjs-wasmfile-release-sync": "0.32.0",
2525
"@logtape/logtape": "2.3.1",
2626
"@modelcontextprotocol/client": "2.0.0",
@@ -43,8 +43,8 @@
4343
"@cloudflare/vitest-plugin": "1.0.0",
4444
"@cloudflare/workers-types": "5.20260820.1",
4545
"@earendil-works/pi-ai": "1.1.0",
46-
"@flue/sdk": "file:../../third-party/flue/sdk-7047c601e7923609e65a9ae09d25eefd9081b2df.tgz",
47-
"@flue/vite": "file:../../third-party/flue/vite-7047c601e7923609e65a9ae09d25eefd9081b2df.tgz",
46+
"@flue/sdk": "file:../../third-party/flue/sdk-de1dda9ae8b4791dea431cbdcbc818f4b5f22f71.tgz",
47+
"@flue/vite": "file:../../third-party/flue/vite-de1dda9ae8b4791dea431cbdcbc818f4b5f22f71.tgz",
4848
"@msw/cloudflare": "^0.0.1",
4949
"@types/ws": "8.18.1",
5050
"@valibot/to-json-schema": "1.7.1",

‎apps/core/src/agents/dx-agent.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import {
2727
import { rememberClaudeCodeSandbox } from "../claude-code/runs.js";
2828
import { claudeCodeMirrorTools } from "../claude-code/tools.js";
2929
import { traceSpan } from "../observability/tracing.js";
30+
import {
31+
takeWorkspaceDisk,
32+
WORKSPACE_DISK_SIGNAL,
33+
workspaceDiskNote,
34+
} from "../orb-disk/note.js";
3035
import {
3136
agentPluginHostContext,
3237
agentThreadToolsContext,
@@ -184,8 +189,9 @@ const agentSandbox: SandboxFactory | undefined =
184189
/**
185190
* Installed plugins' agent-start and agent-finish hooks. The agent-start
186191
* hook first writes dx's built-in skills into the workspace, for the dx
187-
* agent and Claude Code alike; Flue runs separate start hooks concurrently,
188-
* so both stay in one.
192+
* agent and Claude Code alike, then tells the agent when its disk is nearly
193+
* or completely full; Flue runs separate start hooks concurrently, so all
194+
* stay in one.
189195
*/
190196
const usePluginLifecycle = (
191197
threadId: string,
@@ -218,7 +224,16 @@ const usePluginLifecycle = (
218224
}
219225
};
220226
useAgentStart(async (context) => {
227+
// Opening the sandbox runs this turn's activation and its disk guard.
221228
await installBuiltInSkills(context.harness.sandbox);
229+
const disk = takeWorkspaceDisk(threadId);
230+
const note = disk === undefined ? undefined : workspaceDiskNote(disk);
231+
if (note !== undefined)
232+
context.append({
233+
kind: "signal",
234+
type: WORKSPACE_DISK_SIGNAL,
235+
body: note,
236+
});
222237
await hook("agent-start")(context);
223238
});
224239
if (plugins.some(({ lifecycle }) => lifecycle.includes("agent-finish")))

‎apps/core/src/app.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from "./notifications/routes.js";
2020
import { installFlueObservation } from "./observability/flue.js";
2121
import { installRuntimeTracing } from "./observability/platform-tracing.js";
22+
import { threadOrbDiskRoutes } from "./orb-disk/routes.js";
2223
import { threadOrbMemoryRoutes } from "./orb-memory/routes.js";
2324
import { portalAuthRoutes } from "./portals/auth-routes.js";
2425
import { portalHostRouting } from "./portals/host-routing.js";
@@ -123,6 +124,8 @@ app.route("/v1/threads", threadPortalRoutes);
123124
// `/memory/*` also matches `/memory`; see Portals above.
124125
app.use("/v1/threads/:threadId/memory/*", authorizeThread);
125126
app.route("/v1/threads", threadOrbMemoryRoutes);
127+
app.use("/v1/threads/:threadId/disk/*", authorizeThread);
128+
app.route("/v1/threads", threadOrbDiskRoutes);
126129
app.use("/v1/threads/:threadId/desktop/*", authorizeThread);
127130
app.use("/v1/threads/:threadId/desktop/*", enforceThreadActive);
128131
app.route("/v1/threads", threadDesktopRoutes);

‎apps/core/src/claude-code/claude-code.test.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ exec /usr/bin/awk "$@"
165165
"curl",
166166
options.installerWorks === true
167167
? `#!/bin/sh\necho 'mkdir -p "$HOME/.local/bin" && cp ${join(root, "claude-latest")} "$HOME/.local/bin/claude"'\n`
168-
: "#!/bin/sh\necho 'echo \"Installation failed: no space left on device\" >&2; exit 1'\n",
168+
: "#!/bin/sh\necho 'echo \"Installation failed: checksum mismatch\" >&2; exit 1'\n",
169169
);
170170
const installClaude = () => tool("claude", FAKE_CLAUDE);
171171
if (options.oldClaude !== undefined)
@@ -397,7 +397,8 @@ const waitFor = async (ready: () => boolean, ms = 10_000) => {
397397
const runDirectories = (ops: ReadonlyArray<GuestOp>) =>
398398
new Set(
399399
ops.flatMap(({ path }) => {
400-
const match = /dx-claude-code\/runs\/([^/'"\s]+)/.exec(path);
400+
// A launch's disk guard names the runs directory with a glob.
401+
const match = /dx-claude-code\/runs\/([^/'"\s*]+)/.exec(path);
401402
return match?.[1] === undefined ? [] : [match[1]];
402403
}),
403404
);
@@ -532,7 +533,7 @@ describe("Claude Code mode in a workspace", { timeout: 30_000 }, () => {
532533
type: "error",
533534
error: {
534535
errorMessage: expect.stringMatching(
535-
/Claude Code could not be installed\.[\s\S]*no space left on device/,
536+
/Claude Code could not be installed\.[\s\S]*checksum mismatch/,
536537
),
537538
},
538539
});
@@ -580,7 +581,7 @@ describe("Claude Code mode in a workspace", { timeout: 30_000 }, () => {
580581
type: "error",
581582
error: {
582583
errorMessage: expect.stringMatching(
583-
/Claude Code 1\.0\.108 is older than 2\.1\.310[\s\S]*claude update[\s\S]*no space left on device/,
584+
/Claude Code 1\.0\.108 is older than 2\.1\.310[\s\S]*claude update[\s\S]*checksum mismatch/,
584585
),
585586
},
586587
});

‎apps/core/src/claude-code/workspace-run.ts‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { Sandbox } from "@flue/runtime";
22
import { BUILT_IN_SKILL_NAMES } from "../agents/built-in-skill-names.js";
33
import { encodeBase64 } from "../encoding/base64.js";
4-
import { markRunFailure } from "../runtime/run-failure.js";
4+
import {
5+
isDiskFullText,
6+
parseWorkspaceDiskReading,
7+
withoutWorkspaceDiskReading,
8+
workspaceDiskFullFailure,
9+
workspaceDiskGuard,
10+
} from "../orb-disk/guard.js";
11+
import { classifyRunFailure, markRunFailure } from "../runtime/run-failure.js";
512
import { environmentSecretsOf } from "../settings/environment-variables/sandbox.js";
613
import { CLAUDE_CODE_MCP_SERVER } from "./mcp-server.js";
714
import { CHUNK_END_LINE, ClaudeCodeStream } from "./stream.js";
@@ -126,6 +133,11 @@ const CLI_ENVIRONMENT = {
126133
*/
127134
const CLAUDE_CODE_MIN_VERSION = "2.1.310";
128135

136+
const isWorkspaceReset = (error: unknown) =>
137+
classifyRunFailure({
138+
message: error instanceof Error ? error.message : String(error),
139+
}).code === "WORKSPACE_RESET";
140+
129141
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
130142

131143
/** The read's answer, or a failure once it has waited READ_TIMEOUT_MS. */
@@ -226,6 +238,13 @@ const launchFiles = (files: Readonly<Record<string, string>>) =>
226238

227239
const LAUNCH_FILES = "launch-files.sh";
228240

241+
/**
242+
* Added to an attempt's system prompt when its launch found the disk full:
243+
* a Claude Code run that died of it is restarted mid-turn, before the next
244+
* turn's note can tell it.
245+
*/
246+
const CLAUDE_CODE_DISK_FULL_NOTE = `dx note: this Orb's disk filled up completely during this turn, which stopped Claude Code. dx freed its own emergency space so it could restart. Before anything else, free regenerable space (build outputs and caches such as node_modules, target, dist, build, .cache, and package manager caches; find them with \`du -xh -d 2 ~ 2>/dev/null | sort -h | tail -n 20\`), never source files or uncommitted work, then continue the task.`;
247+
229248
/** Moves the plan token from the command's environment to descriptor 3. */
230249
const TOKEN_TO_DESCRIPTOR = `# The plan token arrives in this command's environment, which every process
231250
# of the workspace user can read through /proc while the process lives, and
@@ -276,7 +295,9 @@ const launchScript = (
276295
.join(" ");
277296
return `${TOKEN_TO_DESCRIPTOR}
278297
set -eu
279-
D=${quote(dir)}
298+
# A full disk must not keep Claude Code from starting: the guard frees dx's
299+
# reserve and leftovers first, and the agent is told to free space.
300+
${workspaceDiskGuard(false)}D=${quote(dir)}
280301
S=${quote(STATE_ROOT)}
281302
mkdir -p "$D"
282303
if [ -e "$D/pid" ]; then echo running; exit 0; fi
@@ -291,6 +312,7 @@ finish() {
291312
}
292313
trap finish EXIT
293314
${files ?? `. "$D/${LAUNCH_FILES}"`}
315+
if [ "$dx_disk_full" = 1 ]; then printf '\n\n%s\n' ${quote(CLAUDE_CODE_DISK_FULL_NOTE)} >> "$D/prompt.txt"; fi
294316
# A recorded PID is killed only while it is still that run's runner: after a
295317
# container restart the number can belong to any process.
296318
ours() { tr '\\0' ' ' < "/proc/$2/cmdline" 2>/dev/null | grep -qF "$1/runner.sh"; }
@@ -625,12 +647,16 @@ export class ClaudeCodeRun {
625647
signal,
626648
},
627649
);
628-
if (result.exitCode !== 0)
650+
if (result.exitCode !== 0) {
651+
const stderr = withoutWorkspaceDiskReading(result.stderr).trim();
652+
const disk = parseWorkspaceDiskReading(result.stderr);
653+
// Retrying frees nothing: the person or the agent must.
654+
if (isDiskFullText(`${stderr}\n${result.stdout}`))
655+
throw new Error(workspaceDiskFullFailure(disk?.totalBytes));
629656
throw new Error(
630-
result.stderr.trim() ||
631-
result.stdout.trim() ||
632-
"Claude Code could not start.",
657+
stderr || result.stdout.trim() || "Claude Code could not start.",
633658
);
659+
}
634660
}
635661

636662
async #read(path: string) {
@@ -718,6 +744,9 @@ export class ClaudeCodeRun {
718744
this.#forgetReadFailures();
719745
return value;
720746
} catch (error) {
747+
// The workspace lost its files: no read will answer, and Flue
748+
// re-attempts the submission for it at once.
749+
if (isWorkspaceReset(error)) throw error;
721750
let failure = error;
722751
if (!this.#reconnected && this.reconnect !== undefined) {
723752
this.#reconnected = true;

‎apps/core/src/cloudflare.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ configureRetryPolicy({
5353
retryDelay(errorMessage, attempt),
5454
submissionRetryPolicy: ({ errorMessage, attempt }) =>
5555
retryDelay(errorMessage, attempt),
56+
// A workspace that lost its files fails every model or tool retry in
57+
// place; re-attempting the submission runs activation, which sets it up
58+
// again.
59+
reattemptSubmission: ({ errorMessage }) =>
60+
classifyRunFailure({ message: errorMessage }).code === "WORKSPACE_RESET",
5661
});
5762

5863
const bindings = env as Bindings;

0 commit comments

Comments
 (0)