Skip to content
Merged

1.4.7 #120

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/deepagent-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- uses: ./.github/actions/setup-bun

- name: Run deepagent-code
uses: deepagent-ltd/deepagent-code/github@github-v1.4.5
uses: deepagent-ltd/deepagent-code/github@github-v1.4.7
env:
DEEPAGENT_CODE_API_KEY: ${{ secrets.DEEPAGENT_CODE_API_KEY }}
DEEPAGENT_CODE_PERMISSION: '{"bash": "deny"}'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

- name: Run deepagent-code
if: steps.commits.outputs.has_commits == 'true'
uses: deepagent-ltd/deepagent-code/github@github-v1.4.5
uses: deepagent-ltd/deepagent-code/github@github-v1.4.7
env:
DEEPAGENT_CODE_API_KEY: ${{ secrets.DEEPAGENT_CODE_API_KEY }}
with:
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
test-fixture-gate:
name: test-fixture-gate
# QUAL-003: forbid test fixtures from bypassing the durable session state
# machine via direct inserts into authority tables. Pure grep gate, no deps.
runs-on: blacksmith-4vcpu-ubuntu-2404
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Bun
uses: ./.github/actions/setup-bun

- name: Run test fixture gate
run: bun script/check-test-fixtures.ts

unit:
name: unit (${{ matrix.settings.name }})
strategy:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ name: typecheck
on:
push:
branches: [dev]
# Gate every PR regardless of target branch: the typecheck baseline is easily polluted by stale
# worktrees/generated files, so keep it required and branch-agnostic.
pull_request:
branches: [dev]
workflow_dispatch:

jobs:
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Desktop 1.4.7 / DeepAgent Core V4.0.8 - Release hardening and data governance

- Recover the two incident sessions through the formal abandoned path with a zero-provider-invocation oracle and fork smoke on production-snapshot copies; fence pre-successor binaries via a protocol-3 database capability so older builds refuse to open databases carrying successor ownership.
- Fix the desktop startup crash in the session_intent rebuild migration on SQLite 3.51 (node runtime) and retry transient schema-lock failures in the migration runner.
- Stop the infinite compaction-continuation recovery loop on startup: recovery failures now land fail-closed (pending → failed) through the widened continuation state machine, serialized per session, with warning-level telemetry.
- Add durable turn stage evidence and a pre-dispatch watchdog for hung activities; order app timelines by (time, id) so wrapped opaque IDs never reorder messages.
- Persist encrypted remote-compaction content per session and commit remote compaction runs with an opaque blob pointer instead of a text summary, exempt from information-hole validation.
- Land RISK-003 data governance: runtime diff-artifact capture (staged behind a flag), a legacy event canonicalizer daemon, portable aggregate snapshot bundles, and a gate-ordered maintenance drill that reclaimed hundreds of MiB on production-snapshot copies (33,168 events deleted, VACUUM 5075 → 4311 MiB).
- Add the release-evidence manifest infrastructure and archive the 2026-08-19/20 release gates; lock REL-002 wire-budget and discovery-budget regressions; retire the legacy activity authority in favor of the durable path.
- Align Desktop 1.4.7, DeepAgent Core V4.0.8, and GitHub Action `github-v1.4.7` across package metadata, updater tags, and workflow references.

## Desktop 1.4.5 / DeepAgent Core V4.0.6 - Bounded planning and runtime hardening

- Make compaction durable through explicit run/attempt lifecycle records and Prompt Epoch authority, with bounded text-only summary retries and typed terminal failures.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zh.md">中文</a> |
<a href="https://github.com/deepagent-ltd/deepagent-code-enterprise">Enterprise</a>
</p>

Expand Down
2 changes: 1 addition & 1 deletion github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
persist-credentials: false

- name: Run deepagent-code
uses: deepagent-ltd/deepagent-code/github@github-v1.4.5
uses: deepagent-ltd/deepagent-code/github@github-v1.4.7
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion github/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.5
1.4.7
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@deepagent-code/app",
"version": "1.4.6",
"version": "1.4.7",
"description": "",
"type": "module",
"exports": {
Expand Down
105 changes: 105 additions & 0 deletions packages/app/src/components/dialog-console-account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { createResource, createSignal, type Component, For, Show } from "solid-js"
import { useDialog } from "@deepagent-code/ui/context/dialog"
import { Dialog } from "@deepagent-code/ui/dialog"
import { Spinner } from "@deepagent-code/ui/spinner"
import { showToast } from "@/utils/toast"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"

// PARITY-004 / GUI 批 — Console 账号接线:消费 /experimental/console/orgs 与 /switch,
// 列出可切换的 Console 组织并切换活跃组织(server-side truth,见 worklist GUI 批)。
type Org = {
accountID: string
accountEmail: string
accountUrl: string
orgID: string
orgName: string
active: boolean
}

export const DialogConsoleAccount: Component = () => {
const dialog = useDialog()
const language = useLanguage()
const serverSDK = useServerSDK()
const serverSync = useServerSync()
const [switching, setSwitching] = createSignal<string | undefined>()

const [orgs] = createResource(async () => {
const result = await serverSDK.client.experimental.console.listOrgs(undefined, { throwOnError: true })
return (result.data?.orgs ?? []) as Org[]
})

const switchOrg = async (org: Org) => {
if (switching()) return
setSwitching(`${org.accountID}:${org.orgID}`)
await serverSDK.client.experimental.console
.switchOrg({ accountID: org.accountID, orgID: org.orgID }, { throwOnError: true })
.then(() => {
serverSync.refreshProviders()
showToast({
variant: "success",
icon: "circle-check",
title: language.t("settings.providers.console.switch.toast.title"),
description: language.t("settings.providers.console.switch.toast.description", { org: org.orgName }),
})
dialog.close()
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
.finally(() => setSwitching(undefined))
}

return (
<Dialog title={language.t("settings.providers.console.switchOrg")} transition>
<Show
when={!orgs.loading}
fallback={
<div class="flex items-center justify-center py-8">
<Spinner />
</div>
}
>
<Show
when={(orgs() ?? []).length > 0}
fallback={
<div class="px-4 py-6 text-14-regular text-text-weak">
{language.t("settings.providers.console.empty")}
</div>
}
>
<div class="flex flex-col px-2 py-1">
<For each={orgs() ?? []}>
{(org) => {
const key = `${org.accountID}:${org.orgID}`
return (
<button
type="button"
disabled={!!switching()}
class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left hover:bg-fill-weak-base disabled:opacity-60"
onClick={() => void switchOrg(org)}
>
<span class="flex min-w-0 flex-col">
<span class="truncate text-14-medium text-text-strong">{org.orgName}</span>
<span class="truncate text-12-regular text-text-weak">{org.accountEmail}</span>
</span>
<Show when={switching() === key}>
<Spinner />
</Show>
<Show when={org.active && switching() !== key}>
<span class="text-12-medium text-text-interactive-base">
{language.t("common.active")}
</span>
</Show>
</button>
)
}}
</For>
</div>
</Show>
</Show>
</Dialog>
)
}
42 changes: 41 additions & 1 deletion packages/app/src/components/settings-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { ProviderIcon } from "@deepagent-code/ui/provider-icon"
import { Tag } from "@deepagent-code/ui/tag"
import { showToast } from "@/utils/toast"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js"
import { createMemo, createResource, type Component, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogCustomProvider } from "./dialog-custom-provider"
import { DialogConsoleAccount } from "./dialog-console-account"
import { SettingsList } from "./settings-list"
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
import { canRefreshProviderModels } from "./provider-model-refresh"
Expand Down Expand Up @@ -50,6 +51,12 @@ const SettingsProvidersContent: Component = () => {
const providers = useProviders()
const [refreshing, setRefreshing] = createStore<Record<string, boolean>>({})

// PARITY-004 / GUI 批 — Console 账号:消费 /experimental/console,显示活跃组织与可切换数量。
const [consoleState] = createResource(async () => {
const result = await serverSDK.client.experimental.console.get().catch(() => undefined)
return result?.data
})

const isConfigCustom = (providerID: string) => {
const provider = serverSync.data.config.provider?.[providerID]
if (!provider) return false
Expand Down Expand Up @@ -180,6 +187,39 @@ const SettingsProvidersContent: Component = () => {
</div>

<div class="flex flex-col gap-8 max-w-[720px]">
<Show
when={(consoleState()?.switchableOrgCount ?? 0) > 0 || (consoleState()?.consoleManagedProviders.length ?? 0) > 0}
>
<div class="flex flex-col gap-1" data-component="console-account-section">
<h3 class="text-14-medium text-text-strong pb-2">
{language.t("settings.providers.section.consoleAccount")}
</h3>
<SettingsList>
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col min-w-0">
<span class="text-14-medium text-text-strong truncate">
{consoleState()?.activeOrgName ?? language.t("settings.providers.console.noOrg")}
</span>
<span class="text-12-regular text-text-weak">
{language.t("settings.providers.console.managedCount", {
count: consoleState()?.consoleManagedProviders.length ?? 0,
})}
</span>
</div>
<Button
size="large"
variant="ghost"
onClick={() => {
dialog.show(() => <DialogConsoleAccount />)
}}
>
{language.t("settings.providers.console.switchOrg")}
</Button>
</div>
</SettingsList>
</div>
</Show>

<div class="flex flex-col gap-1" data-component="connected-providers-section">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
<SettingsList>
Expand Down
67 changes: 66 additions & 1 deletion packages/app/src/components/settings-v2/import-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import "./settings-v2.css"

type SourceFormat = "codex" | "claude"
type SourceFormat = "codex" | "claude" | "deepagent"
type SourceMode = SourceFormat | "custom"
type Scope = "session" | "memory" | "skill"
type ProgressEvent =
Expand Down Expand Up @@ -90,6 +90,7 @@ export const ImportSection: Component = () => {
const [dryRun, setDryRun] = createSignal(false)
const [copyLiveDb, setCopyLiveDb] = createSignal(true)
const [cwdFilter, setCwdFilter] = createSignal("")
const [snapshotFile, setSnapshotFile] = createSignal<File | undefined>(undefined)
const [running, setRunning] = createSignal(false)
const [logs, setLogs] = createSignal<string[]>([])
const [summary, setSummary] = createSignal<string>("")
Expand All @@ -113,6 +114,7 @@ export const ImportSection: Component = () => {
const modeOptions = createMemo(() => [
{ value: "codex" as const, label: t("settings.import.preset.codex", "Codex") },
{ value: "claude" as const, label: t("settings.import.preset.claude", "Claude Code") },
{ value: "deepagent" as const, label: t("settings.import.preset.deepagent", "DeepAgent Code (snapshot)") },
{ value: "custom" as const, label: t("settings.import.preset.custom", "Custom") },
])
const formatOptions = createMemo(() => [
Expand All @@ -125,6 +127,42 @@ export const ImportSection: Component = () => {
{ id: "skill", icon: "bookmark" },
]

// DeepAgent Code snapshot import: read the chosen bundle file and POST it to
// /session/import-snapshot (server rebuilds a fresh, continuable session).
const runSnapshotImport = async (h: HttpBase | undefined) => {
try {
const file = snapshotFile()
if (!file) {
push(` [error] ${t("settings.import.snapshot.noFile", "Choose a snapshot bundle file first")}`)
return
}
const bundle = await file.text()
const res = await fetch(`${h?.url ?? ""}/session/import-snapshot`, {
method: "POST",
headers: { "content-type": "application/json", ...authHeader(h) },
body: JSON.stringify({ bundle }),
signal: abort?.signal,
})
if (!res.ok) {
const text = await res.text().catch(() => res.statusText)
push(` [error] HTTP ${res.status}: ${text.slice(0, 200)}`)
return
}
const result = (await res.json().catch(() => undefined)) as
| { sessionID?: string; messages?: number; parts?: number }
| undefined
push(
` [session] imported ${result?.sessionID?.slice(0, 16) ?? ""}… · ${result?.messages ?? 0} messages · ${result?.parts ?? 0} parts`,
)
setSummary(t("settings.import.snapshot.done", "Snapshot imported as a new continuable session"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
push(` [error] ${message}`)
} finally {
setRunning(false)
}
}

const run = async () => {
if (running()) return
setLogs([])
Expand All @@ -134,6 +172,10 @@ export const ImportSection: Component = () => {
const h = http()
const src = format()
push(`→ ${src}${mode() === "custom" ? ` (custom: ${sourcePath() || "default"})` : ` (default)`} · scopes: ${scopes().join(",")}${dryRun() ? " · dry-run" : ""}`)
if (src === "deepagent") {
await runSnapshotImport(h)
return
}
try {
await streamImport(
h?.url ?? "",
Expand Down Expand Up @@ -218,6 +260,29 @@ export const ImportSection: Component = () => {
/>
</SettingsRowV2>

<Show when={mode() === "deepagent"}>
<SettingsRowV2
title={t("settings.import.snapshot.file", "Snapshot bundle")}
description={t(
"settings.import.snapshot.file.hint",
"Choose an exported session snapshot bundle (.json). It imports as a new continuable session.",
)}
>
<div class="flex flex-col gap-2 w-full sm:w-[260px]">
<input
data-action="settings-import-snapshot-file"
type="file"
accept=".json,application/json"
disabled={running()}
onChange={(e) => setSnapshotFile(e.currentTarget.files?.[0])}
/>
<Show when={snapshotFile()}>
{(file) => <span class="text-12-regular text-text-weak truncate">{file().name}</span>}
</Show>
</div>
</SettingsRowV2>
</Show>

<Show when={mode() === "custom"}>
<SettingsRowV2 title={t("settings.import.customPath", "Path")} description={t("settings.import.customPath.hint", "Absolute path to the agent data directory.")}>
<div class="flex flex-col gap-2 w-full sm:w-[260px]">
Expand Down
Loading
Loading