dsh-edge 0.15.0-alpha.1 → 0.15.0-alpha.2

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # last confirmed-consistent state. Both languages carry equal authority.
3
3
  # After editing either side, update both and re-record every pair with:
4
4
  # pnpm run doc-pairs -- --write
5
- README.md: df37b120d64d39186c91bdcb811cf2ac1f1745ac
6
- README.zh.md: 2348c8e25572a915c0423ed940abc1a80638381e
5
+ README.md: 313b3877f1f962865c6a8d8ba3e3a05f11599346
6
+ README.zh.md: 786e48302fb57a8d7b9646991a1b600eace86fec
package/README.md CHANGED
@@ -325,8 +325,13 @@ pnpm --filter dsh-edge example:install
325
325
  - After each committed inbox splice, mux publishes a complete `session/queue` snapshot. Reconnecting clients receive pending live-inbox baselines.
326
326
  - `POST /api/commands/list` uses the upstream generated-Remote envelope with an empty catalog because the Edge preset registers no human commands.
327
327
  - `GET /api/health` returns the public release/mode identifier and configured attachment default (`private-r2` or `temporary-do`). It validates owner authentication, deployment-scoped DeepSeek credentials, model/transport choices, and command timeouts before reporting ready.
328
+ - `GET /api/ready` requires the owner cookie and waits for session migration, workspace registration, the browser's session/workspace controllers, and registration of the DeepSeek model adapter. It does not make a provider request. It returns HTTP 503 with `runtime-initialization-failed` if startup fails; public health alone does not prove runtime readiness. Ordinary authenticated routes wait for base initialization without requiring the model adapter, preserving history and recovery access when only model initialization failed.
328
329
  - Health does not call the provider, Durable Object, R2, VFS, or shell. The authenticated agent-preset projection reports the pinned backend, temporary cap, deployment-default model, and runtime-derived upstream catalog with session selection scope.
329
330
 
331
+ Release upgrades automatically normalize the known legacy Edge cancellation records and migrate all outdated session logs, summaries, and blank headers in one SQLite transaction. All old logs are decoded and validated before the first migration write, so incompatible later logs cannot repeatedly consume earlier migration writes on restart. Preflight discards each decoded artifact; migration decodes again in the same synchronous transaction to avoid retaining every history in memory. If any session is incompatible or a write fails, that startup migration is rolled back in full. No session is silently skipped or deleted. This transaction covers session-format migration, not arbitrary plugin storage changes or external side effects. The installer matches the Cloudflare Worker version ID from upload output against both public health and authenticated readiness, so an earlier deployment of the same package cannot satisfy the check. The version metadata binding is included in both modes. The installer checks runtime readiness before reporting success; a timeout is reported as unverified, and a confirmed startup failure reports an error with recovery details. It never automatically rolls back Worker code after a schema upgrade. Release tests boot both prebuilt modes with legacy cancellation data, verify continuation, and reject false readiness on incompatible data; the packed npm artifact is also checked for authenticated runtime readiness.
332
+
333
+ SQLite `DELETE` operations consume the daily row-write allowance too. During format migration, Edge compares the number of legacy and current event rows: a single index-backed probe stops once copying current rows would cost more; when copying is cheaper, it writes the migrated compact logs into a replacement table and swaps tables inside the same transaction, avoiding a billed delete for every legacy token delta. Otherwise it replaces only the affected sessions. Current session logs are preserved, and failed migrations roll back the table swap. This reduces migration writes but cannot guarantee that every database fits the Free plan's 100,000 daily row-write allowance; sufficiently large upgrades or earlier daily usage can still exhaust it. Point-in-time recovery restores data, not the account's consumed daily quota.
334
+
330
335
  ### Diagnostic REST routes
331
336
 
332
337
  - `PUT /api/workspace/file?path=/workspace/...` writes a UTF-8 file.
package/README.zh.md CHANGED
@@ -325,8 +325,13 @@ pnpm --filter dsh-edge example:install
325
325
  - 每次 inbox splice 提交后,mux 都发布完整的 `session/queue` snapshot。客户端重连时会收到待处理的 live-inbox baseline。
326
326
  - `POST /api/commands/list` 使用上游 generated-Remote envelope 返回空 catalog,因为 Edge preset 没有注册 human command。
327
327
  - `GET /api/health` 返回公开 release/mode identifier 与配置的 attachment 默认值(`private-r2` 或 `temporary-do`)。它先验证 owner authentication、部署级 DeepSeek 凭据、模型/传输选择与命令 timeout,再报告 ready。
328
+ - `GET /api/ready` 要求 owner cookie,等待会话迁移、工作区注册、浏览器所需的会话/工作区控制器就绪,以及 DeepSeek 模型适配器完成注册。此检查不会向模型服务发起请求。启动失败时返回 HTTP 503 和 `runtime-initialization-failed`;公开 health 响应不能单独证明运行时可用。普通认证路由只等待基础初始化,不要求模型适配器可用;仅模型初始化失败时,仍保留历史记录和恢复访问。
328
329
  - Health 不调用 provider、Durable Object、R2、VFS 或 shell。认证后的 agent-preset projection 会报告固定 backend、临时存储上限、部署默认模型,以及 runtime 实际读取的上游 catalog 与 session 选择范围。
329
330
 
331
+ Release 升级会自动兼容已知的旧版 Edge 取消记录,并在一个 SQLite 事务中迁移所有旧格式会话日志、摘要和空会话头。首次迁移写入前会解码并校验所有旧日志,避免后面的不兼容日志导致每次重启都重复消耗前面会话的迁移写入。预检会释放每份解码结果;迁移阶段在同一同步事务内再次解码,避免把所有历史同时保留在内存中。任一会话不兼容或写入失败时,本次启动的迁移会整体回滚,不会静默跳过或删除会话。此事务覆盖会话格式迁移,不涵盖任意插件存储改动或外部副作用。安装器会把上传输出中的 Cloudflare Worker 版本 ID 与公开 health 和经过认证的就绪响应分别比对,同一软件包的旧部署不能满足该检查;两种模式都包含版本元数据绑定。安装器检查运行时就绪状态,通过后才报告成功;超时会报告尚未验证,确认启动失败则报告错误和恢复信息。它不会在存储格式升级后自动回滚 Worker 代码。发布测试使用含旧取消记录的数据启动两种预构建模式,验证继续聊天,并确认不兼容数据不会误报就绪;打包后的 npm 产物也会验证经过认证的运行时就绪状态。
332
+
333
+ SQLite 的 `DELETE` 操作也会消耗每日行写入额度。迁移格式时,Edge 会比较旧格式与当前格式的事件行数:单条利用索引的探测查询在确认复制成本更高时即停止;如果复制当前事件的成本更低,就把迁移后的压缩日志写入替代表,并在同一事务内交换表,避免为每个旧 token 片段支付逐行删除费用;否则只替换需要迁移的会话。已有当前格式日志会保留,迁移失败也会回滚表交换。这会降低迁移写入量,但不能保证所有数据库都满足 Free 计划每日 100,000 行写入额度;数据库足够大或当天已使用较多额度时,仍可能触及上限。时间点恢复恢复的是数据,不会恢复账户已经消耗的每日额度。
334
+
330
335
  ### 诊断 REST 路由
331
336
 
332
337
  - `PUT /api/workspace/file?path=/workspace/...` 写入 UTF-8 文件。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-edge",
3
- "version": "0.15.0-alpha.1",
3
+ "version": "0.15.0-alpha.2",
4
4
  "description": "Your DeepSeek Harness, anywhere — deploy a persistent personal coding agent to Cloudflare Workers in one command",
5
5
  "author": "pawaca",
6
6
  "license": "MIT",
@@ -7,6 +7,7 @@ export interface ActivationObservation {
7
7
  }
8
8
 
9
9
  export interface ExpectedHealth {
10
+ workerVersionId: string
10
11
  deploymentId: string
11
12
  shell: 'just-bash-direct' | 'just-bash-isolated'
12
13
  }
@@ -18,6 +19,8 @@ export const ACTIVATION_RETRY_MS: number
18
19
  export function observePublicActivation(options: {
19
20
  publicUrl: string
20
21
  mode: RuntimeMode
22
+ ownerSecret?: string
23
+ versionId?: string
21
24
  fetchImpl?: typeof fetch
22
25
  now?: () => number
23
26
  requestTimeoutMs?: number
@@ -32,3 +35,5 @@ export function observePublicActivation(options: {
32
35
  }): Promise<ActivationObservation>
33
36
 
34
37
  export function isExpectedHealth(value: unknown, expected: ExpectedHealth): boolean
38
+
39
+ export class RuntimeActivationError extends Error {}
@@ -7,10 +7,12 @@ export const ACTIVATION_RETRY_MS = 1_500
7
7
 
8
8
  const MAX_HEALTH_BYTES = 64 * 1024
9
9
 
10
- /** Observe when Cloudflare serves the exact Worker release without making it an install gate. */
10
+ /** Verify the exact uploaded release and its authenticated runtime before reporting ready. */
11
11
  export async function observePublicActivation({
12
12
  publicUrl,
13
13
  mode,
14
+ ownerSecret,
15
+ versionId,
14
16
  fetchImpl = globalThis.fetch,
15
17
  now = Date.now,
16
18
  requestTimeoutMs = ACTIVATION_REQUEST_TIMEOUT_MS,
@@ -29,6 +31,7 @@ export async function observePublicActivation({
29
31
 
30
32
  const healthUrl = publicHealthUrl(publicUrl)
31
33
  const expected = {
34
+ workerVersionId: versionId,
32
35
  deploymentId: `dsh-edge@${edgePackage.version}/${mode}`,
33
36
  shell: mode === 'direct' ? 'just-bash-direct' : 'just-bash-isolated',
34
37
  }
@@ -59,12 +62,18 @@ export async function observePublicActivation({
59
62
  if (response.ok) {
60
63
  const health = await readBoundedJson(response, MAX_HEALTH_BYTES)
61
64
  if (isExpectedHealth(health, expected)) {
62
- return activationResult('ready', attempts, startedAt, now())
65
+ if (typeof ownerSecret !== 'string' || Buffer.byteLength(ownerSecret, 'utf8') < 32 || Buffer.byteLength(ownerSecret, 'utf8') > 512) {
66
+ throw new RuntimeActivationError('Runtime verification requires the owner access key.')
67
+ }
68
+ if (await verifyRuntime({ publicUrl, ownerSecret, fetchImpl, signal: requestSignal, expected })) {
69
+ return activationResult('ready', attempts, startedAt, now())
70
+ }
63
71
  }
64
72
  } else {
65
73
  await response.body?.cancel()
66
74
  }
67
- } catch {
75
+ } catch (error) {
76
+ if (error instanceof RuntimeActivationError) throw error
68
77
  if (signal?.aborted) signal.throwIfAborted()
69
78
  // DNS, routing, challenge, timeout, and placeholder responses are all
70
79
  // transient observations until the bounded wait expires.
@@ -85,6 +94,8 @@ export function isExpectedHealth(value, expected) {
85
94
  && value.deploymentId === expected.deploymentId
86
95
  && value.shell === expected.shell
87
96
  && value.version === edgePackage.version
97
+ && typeof expected.workerVersionId === 'string' && expected.workerVersionId.length > 0
98
+ && value.workerVersionId === expected.workerVersionId
88
99
  }
89
100
 
90
101
  function publicHealthUrl(publicUrl) {
@@ -131,3 +142,31 @@ function activationResult(status, attempts, startedAt, finishedAt) {
131
142
  status,
132
143
  }
133
144
  }
145
+
146
+ /** An upload succeeded, but its application is definitively not ready. */
147
+ export class RuntimeActivationError extends Error {}
148
+
149
+ async function verifyRuntime({ publicUrl, ownerSecret, fetchImpl, signal, expected }) {
150
+ // Login and readiness use the same validated origin. Never follow redirects
151
+ // with owner credentials, and keep the short-lived probe cookie in memory.
152
+ const login = await fetchImpl(new URL('/api/auth/login', publicUrl).href, {
153
+ method: 'POST', redirect: 'manual', signal,
154
+ headers: { 'content-type': 'application/x-www-form-urlencoded', origin: new URL(publicUrl).origin },
155
+ body: new URLSearchParams({ accessKey: ownerSecret }).toString(),
156
+ })
157
+ const cookie = login.headers.get('set-cookie')?.split(';', 1)[0]
158
+ await login.body?.cancel()
159
+ // A same-version deployment may still serve the previous owner key during propagation.
160
+ if (login.status === 401) return false
161
+ if (login.status !== 303 || !/^__Host-dsh_edge_owner=v1\.[0-9]+\.[A-Za-z0-9_-]+$/u.test(cookie ?? '')) return false
162
+ const response = await fetchImpl(new URL('/api/ready', publicUrl).href, {
163
+ redirect: 'manual', signal,
164
+ headers: { accept: 'application/json', 'cache-control': 'no-cache', cookie },
165
+ })
166
+ const state = await readBoundedJson(response, MAX_HEALTH_BYTES)
167
+ if (response.status === 503 && state?.code === 'runtime-initialization-failed'
168
+ && state.workerVersionId === expected.workerVersionId) {
169
+ throw new RuntimeActivationError('Worker uploaded, but session or workspace initialization failed. Upgrade is not ready. Do not delete stored data; install a compatible release. No automatic rollback was attempted.')
170
+ }
171
+ return response.ok && state?.runtime === true && isExpectedHealth(state, expected)
172
+ }
package/scripts/cli.mjs CHANGED
@@ -279,9 +279,9 @@ export function createInstallerUi(
279
279
  },
280
280
  activationFinish(result) {
281
281
  if (activationSpinner === undefined) return
282
- if (result?.status === 'ready') activationSpinner.stop('Public URL is ready.')
282
+ if (result?.status === 'ready') activationSpinner.stop('Chat and workspace services are ready.')
283
283
  else if (result?.status === 'pending') {
284
- activationSpinner.stop('Worker uploaded; public URL activation is still pending.')
284
+ activationSpinner.stop('Worker uploaded; application readiness is not yet verified.')
285
285
  } else {
286
286
  activationSpinner.stop('Stopped waiting for public URL activation.')
287
287
  }
@@ -319,9 +319,9 @@ export function createInstallerUi(
319
319
  ...(result.activation?.status === 'ready'
320
320
  ? ['Status: Ready']
321
321
  : [
322
- 'Status: Cloudflare is still activating the public URL.',
323
- 'First-time workers.dev activation can take about a minute.',
324
- 'If the URL shows a placeholder, wait a moment and refresh.',
322
+ 'Status: Application readiness has not been verified.',
323
+ 'The public URL or application may still be starting.',
324
+ 'Open the URL and confirm your chats and workspaces load before using it.',
325
325
  ]),
326
326
  '',
327
327
  `URL: ${result.publicUrl}`,
@@ -349,11 +349,11 @@ export function createInstallerUi(
349
349
  const ready = result.activation?.status === 'ready'
350
350
  const title = ready
351
351
  ? (command === 'upgrade' ? 'dsh-edge upgrade is live' : 'dsh-edge is ready')
352
- : 'Worker uploaded — activation pending'
352
+ : 'Worker uploaded — readiness unverified'
353
353
  note(lines.join('\n'), title)
354
354
  const outro = ready
355
355
  ? (command === 'upgrade' ? 'Your dsh-edge upgrade is live.' : 'Your dsh-edge is ready.')
356
- : `${command === 'upgrade' ? 'Upgrade' : 'Installation'} succeeded; Cloudflare is still activating the public URL.`
356
+ : 'Worker uploaded; application readiness remains unverified.'
357
357
  log(clack.outro, outro)
358
358
  },
359
359
  }
@@ -166,6 +166,8 @@ export function installEdge(options: {
166
166
  createTemporaryDirectory?: () => Promise<string>
167
167
  removePath?: typeof import('node:fs/promises').rm
168
168
  observeActivation?: (options: {
169
+ ownerSecret: string
170
+ versionId?: string
169
171
  publicUrl: string
170
172
  mode: RuntimeMode
171
173
  signal?: AbortSignal
@@ -813,6 +813,8 @@ export async function installEdge({
813
813
  const activation = await observeActivation({
814
814
  mode,
815
815
  publicUrl: result.publicUrl,
816
+ ownerSecret: result.ownerSecret,
817
+ versionId: result.versionId,
816
818
  signal,
817
819
  })
818
820
  result = { ...result, activation }
@@ -42,6 +42,16 @@ try {
42
42
  assert.equal(health.service, 'dsh-edge')
43
43
  assert.equal(health.shell, mode === 'direct' ? 'just-bash-direct' : 'just-bash-isolated')
44
44
  assert.equal(health.deploymentId, `dsh-edge@${edgePackage.version}/${mode}`)
45
+ const login = await worker.fetch('http://dsh-edge.test/api/auth/login', {
46
+ method: 'POST', redirect: 'manual', headers: { 'content-type': 'application/x-www-form-urlencoded' },
47
+ body: new URLSearchParams({ accessKey: ACCESS_KEY }).toString(),
48
+ })
49
+ assert.equal(login.status, 303)
50
+ const cookie = login.headers.get('set-cookie')?.split(';', 1)[0]
51
+ assert.ok(cookie)
52
+ const ready = await worker.fetch('http://dsh-edge.test/api/ready', { headers: { cookie } })
53
+ assert.equal(ready.status, 200)
54
+ assert.equal((await ready.json()).runtime, true)
45
55
  await worker.stop()
46
56
  worker = undefined
47
57
  process.stdout.write(`Installed ${mode} Worker artifact started successfully.\n`)