Skip to content

Commit ee1d325

Browse files
authored
Codex/fix desktop build oom (deepagent-ltd#102)
### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._
1 parent c97659e commit ee1d325

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)