Skip to content

Commit d509ede

Browse files
committed
Release v0.2.23
1 parent 3c1731b commit d509ede

97 files changed

Lines changed: 5642 additions & 1514 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/deploy.yml‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: Deploy
2+
3+
# Deploys a dx release to your Cloudflare account, on demand. Made for forks:
4+
# it fetches release tags from upstream, checks out the one you name (the
5+
# newest by default), and deploys it with the fork's own settings. See
6+
# https://dxcode.dev/docs/deployment/fork/ for the setup.
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
tag:
11+
description: Release tag to deploy (empty for the newest vX.Y.Z)
12+
type: string
13+
default: ""
14+
15+
permissions:
16+
contents: read
17+
18+
concurrency:
19+
group: deploy
20+
cancel-in-progress: false
21+
22+
jobs:
23+
deploy:
24+
name: Deploy
25+
environment: production
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 60
28+
steps:
29+
- uses: actions/checkout@v4
30+
with:
31+
fetch-depth: 0
32+
persist-credentials: false
33+
- name: Check out the release tag
34+
id: tag
35+
env:
36+
REQUESTED: ${{ inputs.tag }}
37+
UPSTREAM: ${{ vars.DX_UPSTREAM_REPOSITORY || 'dxcode-dev/dxcode' }}
38+
# Only while the upstream repository is private.
39+
UPSTREAM_TOKEN: ${{ secrets.DX_UPSTREAM_TOKEN }}
40+
run: |
41+
auth=()
42+
[ -z "$UPSTREAM_TOKEN" ] || auth=(-c "http.extraheader=AUTHORIZATION: basic $(printf 'x-access-token:%s' "$UPSTREAM_TOKEN" | base64 -w0)")
43+
git "${auth[@]}" fetch --quiet --force "https://github.com/$UPSTREAM.git" 'refs/tags/v*:refs/tags/v*'
44+
tag="${REQUESTED:-$(git tag --list 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)}"
45+
if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || ! git rev-parse --verify --quiet "refs/tags/$tag" > /dev/null; then
46+
echo "::error::Upstream has no release tag $tag."
47+
exit 1
48+
fi
49+
git checkout --quiet --detach "$tag"
50+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
51+
- name: Restore the deployment configuration and credentials
52+
id: config
53+
env:
54+
SELFHOST_CONFIG: ${{ vars.DX_SELFHOST_CONFIG_JSON }}
55+
SELFHOST_CREDENTIALS: ${{ secrets.DX_SELFHOST_CREDENTIALS_JSON }}
56+
run: |
57+
[ -n "$SELFHOST_CONFIG" ] || { echo "::error::Set the DX_SELFHOST_CONFIG_JSON variable."; exit 1; }
58+
umask 077
59+
mkdir -p .dx
60+
printf '%s\n' "$SELFHOST_CONFIG" > .dx/deploy.selfhost.json
61+
name="$(jq -er .name .dx/deploy.selfhost.json)"
62+
# Required once the deployment exists: the deploy will not reset an
63+
# existing administrator.
64+
if [ -n "$SELFHOST_CREDENTIALS" ]; then
65+
mkdir -p ".dx/alchemy/selfhost-$name"
66+
printf '%s\n' "$SELFHOST_CREDENTIALS" > ".dx/alchemy/selfhost-$name/credentials.json"
67+
fi
68+
echo "domain=$(jq -r '.domain // empty' .dx/deploy.selfhost.json)" >> "$GITHUB_OUTPUT"
69+
- uses: pnpm/action-setup@v4
70+
with:
71+
version: 9.15.0
72+
- uses: actions/setup-node@v4
73+
with:
74+
node-version: 22.19.0
75+
cache: pnpm
76+
- run: pnpm install --frozen-lockfile
77+
- name: Deploy ${{ steps.tag.outputs.tag }}
78+
env:
79+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
80+
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
81+
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
82+
DX_ADMIN_PASSWORD: ${{ secrets.DX_ADMIN_PASSWORD }}
83+
DX_DXD_GITHUB_TOKEN: ${{ secrets.DX_UPSTREAM_TOKEN }}
84+
DX_SELFHOST_CONFIG: .dx/deploy.selfhost.json
85+
DX_DEPLOY_APPROVE: "1"
86+
run: node scripts/run-alchemy-deployment.mjs deploy selfhost
87+
- name: Check /healthz and /readyz
88+
if: steps.config.outputs.domain != ''
89+
env:
90+
DOMAIN: ${{ steps.config.outputs.domain }}
91+
run: |
92+
for path in healthz readyz; do
93+
curl -fsS --retry 6 --retry-delay 10 --retry-all-errors --max-time 20 "https://$DOMAIN/$path" > /dev/null
94+
echo "/$path answers."
95+
done

‎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.22",
2+
"version": "0.2.23",
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.22/dxd-linux-x64",
11-
"sha256": "5372a8faaecccb039b5af17760a03890ddd9ad831c588a5c3b33f0fc3272bdcd"
10+
"url": "https://github.com/dxcode-dev/dxcode/releases/download/v0.2.23/dxd-linux-x64",
11+
"sha256": "11f6dde286174a0d6a376d908462e9b17023e5cb1d15fa227ab0b5617dfe22dd"
1212
},
1313
"toolchain": {
1414
"node": ">=22.19.0",

‎alchemy.run.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,12 @@ export default Alchemy.Stack(
509509
: yield* Effect.die(
510510
"DX_DEPLOYMENT_ORB_WORKER_NAME is required for Cloudflare Containers.",
511511
);
512-
const defaultOrbProfileId = e2bInstalled
513-
? defaultE2BOrbProfile.id
514-
: defaultCloudflareOrbProfile.id;
512+
// Hosted stages run on Containers whenever they install it; a self-host
513+
// keeps E2B first (scripts/alchemy-deployment.mjs checks the same rule).
514+
const defaultOrbProfileId =
515+
e2bInstalled && (selfhost || !cloudflareInstalled)
516+
? defaultE2BOrbProfile.id
517+
: defaultCloudflareOrbProfile.id;
515518
const runnerProfileCatalog = (e2bProfiles: ReadonlyArray<unknown>) =>
516519
JSON.stringify({
517520
version: 1,

‎apps/core/migration-manifest.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
"filename": "0011_release_v0.2.22.sql",
6868
"sha256": "bddc4afba97e70af8b9b77b9a889b331569852ac67b5eba4022a7b763fb00340",
6969
"classification": "non-destructive"
70+
},
71+
{
72+
"filename": "0012_release_v0.2.23.sql",
73+
"sha256": "cefa0b2bf09c97668d19c7f81fc53f7915d8b86a2ea321aa27dde33a2d72db7e",
74+
"classification": "non-destructive"
7075
}
7176
],
7277
"durableObjects": [
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- A person's sidebar layout, shared by every device they sign in on. A row
2+
-- records one choice: a parent Thread whose children are shown (`thread`;
3+
-- only open parents are kept, closed is the default), or a Project, Inactive,
4+
-- or Archived section opened or closed (`section`). Without a row the
5+
-- sidebar's default applies. Rows past the newest 500 per person are pruned.
6+
CREATE TABLE sidebar_layout_entry (
7+
user_id TEXT NOT NULL,
8+
kind TEXT NOT NULL CHECK (kind IN ('thread', 'section')),
9+
item_id TEXT NOT NULL CHECK (length(item_id) BETWEEN 1 AND 128),
10+
open INTEGER NOT NULL CHECK (open IN (0, 1)),
11+
updated_at TEXT NOT NULL,
12+
PRIMARY KEY (user_id, kind, item_id)
13+
) WITHOUT ROWID;

‎apps/core/package.json‎

Lines changed: 1 addition & 2 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.22",
4+
"version": "0.2.23",
55
"license": "FSL-1.1-ALv2",
66
"type": "module",
77
"scripts": {
@@ -33,7 +33,6 @@
3333
"hono": "4.12.32",
3434
"jose": "6.2.3",
3535
"quickjs-emscripten-core": "0.32.0",
36-
"sarvamai": "1.1.10",
3736
"tldts": "7.4.11",
3837
"valibot": "1.4.2",
3938
"yaml": "2.9.0",

‎apps/core/src/app.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
import { settingsRoutes } from "./settings/routes.js";
3535
import { pluginTriggerIngressRoutes } from "./settings/triggers/routes.js";
3636
import { scheduleUsageRetention } from "./settings/usage/retention.js";
37+
import { sidebarLayoutRoutes } from "./sidebar-layout/routes.js";
3738
import { bitbucketGitGatewayRoutes } from "./source-control/bitbucket/git-gateway.js";
3839
import { bitbucketControlPlaneRoutes } from "./source-control/bitbucket/routes.js";
3940
import {
@@ -90,6 +91,7 @@ app.route("/v1/projects", projectRoutes);
9091
app.route("/v1/realtime", realtimeRoutes);
9192
app.route("/v1/settings", settingsRoutes);
9293
app.route("/v1/shared-threads", sharedThreadRoutes);
94+
app.route("/v1/sidebar-layout", sidebarLayoutRoutes);
9395
app.route("/v1/thread-search", threadSearchRoutes);
9496
app.use("/v1/threads/:threadId", shareThreadReads);
9597
app.use("/v1/threads/:threadId/readiness", shareThreadReads);

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {
2+
type ClaudePlanLimitCause,
23
ClaudePlanLimitStatus,
34
ClaudePlanReading,
45
ClaudePlanUsage,
56
type ClaudePlanWindow,
67
ClaudePlanWindowId,
78
claudePlanBlockedUntil,
9+
claudePlanExtraUsage,
810
claudePlanWindowFamily,
911
mergeClaudePlanUsage,
1012
} from "@dx/domain";
@@ -14,8 +16,9 @@ import { Option, Schema } from "effect";
1416
* A Claude Pro/Max plan's limits as Anthropic reports them. The OAuth token
1517
* from `claude setup-token` carries only the `user:inference` scope, so dx
1618
* cannot ask Anthropic's usage endpoint. Instead, every response on the plan
17-
* carries `anthropic-ratelimit-unified-*` headers: the title call reads them
18-
* directly, and the Claude Code CLI reports them as `rate_limit_event` lines
19+
* carries `anthropic-ratelimit-unified-*` headers: the title call and the
20+
* connection's access check read them directly, and the Claude Code CLI
21+
* reports them as `rate_limit_event` lines
1922
* on its stream-json output.
2023
*/
2124

@@ -38,6 +41,11 @@ const seconds = (value: unknown) => {
3841
: undefined;
3942
};
4043

44+
const reason = (value: unknown) =>
45+
typeof value === "string" && /^[a-z_]{1,64}$/.test(value)
46+
? { disabledReason: value }
47+
: {};
48+
4149
const fraction = (value: unknown) => {
4250
const number = typeof value === "string" ? Number(value) : value;
4351
return typeof number === "number" && Number.isFinite(number)
@@ -89,6 +97,7 @@ export const claudePlanReadingFromEvent = (
8997
? {}
9098
: { resetsAt: seconds(value.overageResetsAt) }),
9199
inUse: value.isUsingOverage === true || value.overageInUse === true,
100+
...reason(value.overageDisabledReason),
92101
},
93102
});
94103
};
@@ -120,6 +129,7 @@ export const claudePlanReadingFromHeaders = (
120129
...(isStatus(overageStatus) ? { status: overageStatus } : {}),
121130
...(overageReset === undefined ? {} : { resetsAt: overageReset }),
122131
inUse: header("overage-in-use") === "true",
132+
...reason(header("overage-disabled-reason")),
123133
},
124134
});
125135
};
@@ -172,7 +182,8 @@ const limitWindowFor = (
172182

173183
/**
174184
* The plan state for one launch on `model` (a Claude model id): the extra
175-
* usage choice and, when the plan is used up for that model, until when.
185+
* usage choice, whether the account has extra usage off, which no choice
186+
* changes, and, when the plan is used up for that model, until when.
176187
*/
177188
export const claudePlanAccess = (state: ClaudePlanState, model: string) => {
178189
const blockedUntil = claudePlanBlockedUntil(
@@ -184,11 +195,31 @@ export const claudePlanAccess = (state: ClaudePlanState, model: string) => {
184195
const limitWindow = limitWindowFor(state.usage, model);
185196
return {
186197
allowExtraUsage: state.allowExtraUsage,
198+
...(claudePlanExtraUsage(state.usage) === "off"
199+
? { extraUsageOff: true }
200+
: {}),
187201
...(blockedUntil === undefined ? {} : { blockedUntil }),
188202
...(limitWindow === undefined ? {} : { limitWindow }),
189203
};
190204
};
191205

206+
/**
207+
* Why dx does not start Claude Code on a blocked plan: its owner keeps it
208+
* off extra usage, or the account has none to keep it off.
209+
*/
210+
export const claudePlanBlockedCause = (
211+
plan: ClaudePlanAccess,
212+
): ClaudePlanLimitCause => (plan.extraUsageOff ? "exhausted" : "not-started");
213+
214+
/**
215+
* Whether a one-token Messages call may read the plan's limits now. While
216+
* the plan is used up, a call could draw on paid extra usage unless the
217+
* account has none, so dx waits for the limit to reset.
218+
*/
219+
export const claudePlanReadable = (state: ClaudePlanState) =>
220+
claudePlanExtraUsage(state.usage) === "off" ||
221+
claudePlanBlockedUntil(state.usage, false, nowSeconds()) === undefined;
222+
192223
export type ClaudePlanAccess = ReturnType<typeof claudePlanAccess>;
193224

194225
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
claudeCodeDxToolList,
2929
deliverClaudeCodeDxToolResults,
3030
} from "./dx-tools.js";
31-
import type { ClaudePlanAccess } from "./plan.js";
31+
import { type ClaudePlanAccess, claudePlanBlockedCause } from "./plan.js";
3232
import { claudeCodeRuns } from "./runs.js";
3333
import type { ClaudeCodeBlock, ClaudeCodeSegment } from "./stream.js";
3434
import { mirroredToolCall } from "./tools.js";
@@ -609,7 +609,7 @@ export const createClaudeCodeProvider = (deps: ClaudeCodeProviderDeps) => {
609609
? {}
610610
: { window: credential.plan.limitWindow }),
611611
resetsAt: credential.plan.blockedUntil,
612-
cause: "not-started",
612+
cause: claudePlanBlockedCause(credential.plan),
613613
}),
614614
);
615615
const next = claudeCodeRuns.create(

0 commit comments

Comments
 (0)