Skip to content

Commit 81ee96c

Browse files
authored
Merge pull request #103 from deepagent-ltd/dev
Codex/fix desktop build oom (#102)
2 parents 8f96f9c + ee1d325 commit 81ee96c

11 files changed

Lines changed: 128 additions & 14 deletions

File tree

.github/workflows/desktop-build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ jobs:
142142
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
143143
run: npx electron-builder ${{ matrix.platform_flag }} --publish never --config electron-builder.config.ts
144144

145+
- name: Smoke test packaged macOS app
146+
if: matrix.group == 'mac'
147+
working-directory: packages/desktop
148+
run: |
149+
executable=$(find dist/mac-arm64 -maxdepth 4 -type f -path '*/Contents/MacOS/DeepAgent Code*' -perm +111 -print -quit)
150+
test -n "$executable"
151+
DEEPAGENT_CODE_DESKTOP_EXECUTABLE="$executable" bun run test:subagents-cold-start
152+
145153
- name: Upload package artifacts
146154
uses: actions/upload-artifact@v4
147155
with:

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/deepagent/atomic-write.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, writeSync } from "node:fs"
1+
import {
2+
closeSync,
3+
fsyncSync,
4+
linkSync,
5+
mkdirSync,
6+
openSync,
7+
renameSync,
8+
rmSync,
9+
writeFileSync,
10+
writeSync,
11+
} from "node:fs"
212
import { randomUUID } from "node:crypto"
313
import path from "node:path"
414

@@ -69,6 +79,20 @@ export const writeFileExclusive = (file: string, content: string): void => {
6979
}
7080
}
7181

82+
// Built-in domain-pack seeds are immutable packaged inputs and can be regenerated after a crash.
83+
// Keep each visible file atomic, but avoid two fsyncs per seed during first-run corpus installation.
84+
export const writeRecoverableFileExclusive = (file: string, content: string): void => {
85+
const dir = path.dirname(file)
86+
mkdirSync(dir, { recursive: true })
87+
const tmp = path.join(dir, `.${path.basename(file)}.seed-${process.pid}-${randomUUID()}`)
88+
try {
89+
writeFileSync(tmp, content, "utf8")
90+
linkSync(tmp, file)
91+
} finally {
92+
rmSync(tmp, { force: true })
93+
}
94+
}
95+
7296
// Best-effort directory fsync so a rename/create is durable. Not all platforms/filesystems permit
7397
// opening a directory for fsync (Windows throws EPERM/EISDIR, some FUSE mounts reject it); a failure
7498
// here does not compromise the file body (already fsync'd) so it is intentionally swallowed.

packages/core/src/deepagent/document-store.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdirSync, readdirSync, readFileSync, existsSync } from "node:fs"
22
import { createHash } from "node:crypto"
33
import path from "node:path"
4-
import { writeFileAtomic, writeFileExclusive } from "./atomic-write"
4+
import { writeFileAtomic, writeFileExclusive, writeRecoverableFileExclusive } from "./atomic-write"
55

66
// V3 Document System (docs/28): the bedrock. All persistent state is a typed-document
77
// graph — small files, content-addressed, append-only with a supersede chain, bidirectional
@@ -368,6 +368,26 @@ export class DocumentStore {
368368
return next
369369
}
370370

371+
// Trusted built-in corpus files are recoverable from the packaged domain packs. New seeds can
372+
// therefore skip per-file fsync while retaining atomic visibility; updates still use the normal
373+
// append-only durable path so shipped corpus revisions preserve version history.
374+
seedActive(input: CreateDocInput): Doc {
375+
const cur = this.findLogical(input)
376+
if (cur) {
377+
const doc = this.upsert(input)
378+
if (doc.status !== "active") this.setStatus(doc.id, "active")
379+
return this.get(doc.id)!
380+
}
381+
382+
const id = this.allocateId(input.type, input.domain ?? null, input.idSlug, input.description)
383+
let doc = { ...this.docFromInput(id, 1, input), status: "active" as const }
384+
this.assertKnowledgeConfidence(doc)
385+
this.assertLinkTargets(doc.links)
386+
doc = { ...doc, hash: computeHash(doc) }
387+
this.persistRecoverable(doc)
388+
return doc
389+
}
390+
371391
update(id: string, body: string, links?: readonly DocLink[]): Doc {
372392
const cur = this.get(id)
373393
if (!cur) throw new Error(`update: unknown doc ${id}`)
@@ -607,6 +627,20 @@ export class DocumentStore {
607627
}
608628
this.indexDoc(doc)
609629
}
630+
private persistRecoverable(doc: Doc): void {
631+
const dir = path.join(this.root, "docs", doc.type)
632+
mkdirSync(dir, { recursive: true })
633+
const file = path.join(dir, `${idToFile(doc.id)}@v${doc.version}.json`)
634+
try {
635+
writeRecoverableFileExclusive(file, JSON.stringify(doc, null, 2))
636+
} catch (error) {
637+
if ((error as NodeJS.ErrnoException)?.code !== "EEXIST") throw error
638+
const existing = this.readVersionFile(file)
639+
if (!existing || existing.hash !== doc.hash)
640+
throw new DocumentConflictError(doc.id, doc.version, existing?.hash ?? "<unreadable>", doc.hash)
641+
}
642+
this.indexDoc(doc)
643+
}
610644
private replace(doc: Doc): void {
611645
// rewrites the SAME version in place with new status/superseded_by; rehash so INV-2 holds. This
612646
// is an intentional overwrite (not a new version), so it uses the crash-safe atomic OVERWRITE
-92 Bytes
Binary file not shown.

packages/core/test/deepagent/document-store.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,26 @@ describe("V3 DocumentStore", () => {
174174
// overwrites the same version atomically (temp+fsync+rename). These tests pin the CAS + durability
175175
// behavior that H32-1 (v4.0.4) builds on.
176176
describe("F30-1 DocumentStore CAS + atomic durability", () => {
177+
test("recoverable built-in seeds retain active status and exclusive-create CAS", () => {
178+
const h1 = new DocumentStore(root)
179+
const h2 = new DocumentStore(root)
180+
const input = {
181+
type: "strategy" as const,
182+
scope: "durable",
183+
body: "trusted built-in strategy",
184+
description: "built-in strategy",
185+
idSlug: "built-in-strategy",
186+
confidence: { evidence_strength: "strong" as const, support_count: 1 },
187+
provenance: { source: "human" as const },
188+
}
189+
const first = h1.seedActive(input)
190+
const concurrent = h2.seedActive(input)
191+
expect(first.status).toBe("active")
192+
expect(concurrent.hash).toBe(first.hash)
193+
expect(new DocumentStore(root).get(first.id)).toEqual(first)
194+
expect(readdirSync(path.join(root, "docs", "strategy"))).toEqual([`${first.id.replaceAll(":", "__")}@v1.json`])
195+
})
196+
177197
test("normal single-writer flow is unchanged (create + updates land byte-identically)", () => {
178198
const a = store.create(design("v1"))
179199
const a2 = store.update(a.id, "v2")

packages/desktop/electron-builder.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const getBase = (): Configuration => ({
6262
"resources/*.metainfo.xml",
6363
"resources/deepagent-code-cli*",
6464
],
65+
asarUnpack: ["out/main/chunks/node.js"],
6566
beforePack: () => auditPackageInputs(path.dirname(fileURLToPath(import.meta.url))),
6667
extraResources: [
6768
{

packages/desktop/electron.vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default defineConfig(({ command }) => ({
3939
rollupOptions: {
4040
input: { index: "src/main/index.ts", sidecar: "src/main/sidecar.ts" },
4141
},
42-
externalizeDeps: { include: ["@lydell/node-pty"] },
42+
externalizeDeps: { exclude: ["@deepagent-code/core"], include: ["@lydell/node-pty"] },
4343
},
4444
plugins: [
4545
{

packages/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
},
3737
"main": "./out/main/index.js",
3838
"dependencies": {
39-
"@deepagent-code/core": "workspace:*",
4039
"@lydell/node-pty": "catalog:",
4140
"@zip.js/zip.js": "2.7.62",
4241
"effect": "catalog:",
@@ -51,6 +50,7 @@
5150
"devDependencies": {
5251
"@actions/artifact": "4.0.0",
5352
"@deepagent-code/app": "workspace:*",
53+
"@deepagent-code/core": "workspace:*",
5454
"@deepagent-code/ui": "workspace:*",
5555
"@jridgewell/trace-mapping": "0.3.31",
5656
"@playwright/test": "catalog:",

packages/desktop/scripts/audit-server-bundle.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { strict as assert } from "node:assert"
44
import { readdir } from "node:fs/promises"
55
import path from "node:path"
66

7-
const chunks = path.resolve("out/main/chunks")
8-
const sidecar = await Bun.file(path.resolve("out/main/sidecar.js")).text()
7+
const main = path.resolve("out/main")
8+
const chunks = path.join(main, "chunks")
9+
const sidecar = await Bun.file(path.join(main, "sidecar.js")).text()
910
const server = Bun.file(path.join(chunks, "node.js"))
1011
const sourceMap = Bun.file(path.join(chunks, "node.js.map"))
1112
const files = await readdir(chunks)
@@ -18,6 +19,22 @@ assert.deepEqual(
1819
[],
1920
"Rollup must not emit a transformed copy of the server bundle",
2021
)
22+
assert.deepEqual(
23+
(
24+
await Promise.all(
25+
[path.join(main, "index.js"), path.join(main, "sidecar.js"), ...files.map((file) => path.join(chunks, file))]
26+
.filter((file) => file.endsWith(".js"))
27+
.map(async (file) =>
28+
new Bun.Transpiler({ loader: "js" })
29+
.scanImports(await Bun.file(file).text())
30+
.filter((item) => item.path.startsWith("@deepagent-code/"))
31+
.map((item) => `${path.relative(main, file)} -> ${item.path}`),
32+
),
33+
)
34+
).flat(),
35+
[],
36+
"Packaged main process must not import TypeScript workspace packages",
37+
)
2138

2239
const source = await server.text()
2340
assert.match(source, /sourceMappingURL=node\.js\.map/, "external server bundle is not linked to its source map")

0 commit comments

Comments
 (0)