Skip to content

feat(desktop): capture app and server heap snapshots into one folder - #58259

Merged
Hona merged 9 commits into
anomalyco:devfrom
Hona:desktop-heap-dump
Oct 23, 2026
Merged

Hona merged 9 commits into
anomalyco:devfrom
Hona:desktop-heap-dump

Conversation

@Hona

@Hona Hona commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

A desktop command palette command, Capture heap snapshots, writes snapshots of the desktop app and its local server into one folder. To get the server's snapshot there without moving a large file, opencode debug heap-dump and its endpoint now take an optional directory.

Palette Done

CLI: --directory

$ opencode debug heap-dump --directory ./snapshots
/home/me/project/snapshots/heap-30616-2026-10-22T234311651Z.heapsnapshot
  • Default unchanged. Without the flag, the server writes into its log directory, the same place as SIGUSR1.
  • Created when missing. The server creates the directory, nested levels included.
  • Relative paths resolve against the shell running the CLI. An absolute path passes through untouched, so a POSIX path for a remote --server stays intact on Windows.
  • Query parameter, not a body. POST /api/debug/heap-dump?directory=…. A request without it, like opencode api post /api/debug/heap-dump or the previous CLI, works as before. A relative directory is rejected with InvalidRequestError (400).
  • Failures say why. A directory the server cannot create or write into returns the filesystem's reason, e.g. Failed to write heap snapshot: EEXIST: file already exists, mkdir '/home/me/snapshots', instead of only pointing at the server log.
  • Older servers ignore the parameter and write to their log directory. The CLI notices and warns: The server ignored --directory. Update and restart it to choose where snapshots go.

Desktop: one folder for the whole app

~/Downloads/opencode-heap-2026-10-22T234211397Z/
  heap-17116-2026-10-22T234211405Z.heapsnapshot   desktop's own server, written here by the server
  window-1-21984.heapsnapshot                     one per app window
  main-21448.heapsnapshot                         Electron main
  manifest.json                                   written last
sequenceDiagram
  participant W as window (debug renderer)
  participant M as main (debug/main.ts)
  participant S as desktop server (sidecar)
  W->>M: HeapSnapshots.write()
  M->>M: mkdir folder (0700)
  par
    M->>S: POST /api/debug/heap-dump?directory=folder
    S-->>M: { path, pid } once the file is complete
  and
    M->>W: webContents.takeHeapSnapshot(file), one window at a time
  end
  M->>M: v8.writeHeapSnapshot(main), last
  M->>M: manifest.json
  M-->>W: { directory, complete }
Loading
  • Local server only. Always the desktop's own server (sidecar). Remote servers (SSH, WSL, paired machines) get no request; use opencode debug heap-dump there.
  • Nothing large is moved or copied. The server writes straight into the folder, which main creates before asking. An older server that ignores directory keeps the file in its log directory, and the manifest records that path.
  • Order. The server serializes while the windows do, since it is a separate process. Windows go one at a time, because a snapshot can double a process's memory and a leaking app may not have room for all of them at once. Main goes last, because writing its own heap blocks the loop that receives the window snapshots.
  • Failures don't stop the capture. Each process gets a file, path or error entry in the manifest, and the toast says when something failed.
  • Manifest. App and runtime versions, the snapshots with PIDs, app.getAppMetrics() (metricsMemoryUnit: "KiB"), and main's process.memoryUsage(). A process that grows while its JS heap does not leaks native memory.
  • Private. The folder is created 0700 and the manifest 0600. Downloads is created when missing.
  • Not included. Browser tabs (third-party page content; the agent's browser.heap tool covers them) and Web Workers.
  • Lives in the debug GUI extension. New HeapSnapshots Ipc and a main entry. The command registers through createKeyed(ctx.uses.heap, …), so it shows only while main answers, never on the web. One capture runs at a time.
// manifest.json
{
  "snapshots": [
    { "process": "server", "pid": 17116, "file": "heap-17116-2026-10-22T234211405Z.heapsnapshot" },
    { "process": "window", "pid": 21984, "window": 1, "file": "window-1-21984.heapsnapshot" },
    { "process": "main", "pid": 21448, "file": "main-21448.heapsnapshot" }
  ],
  "metrics": [{ "pid": 21448, "type": "Browser", "memory": { "privateBytes": 137452 } }, "…"],
  "metricsMemoryUnit": "KiB"
}

New copy is English only; translations land separately. The OpenAPI documents include only the heap-dump change; unrelated drift from dev is left out. Docs: troubleshooting.mdx and cli/commands.mdx cover --directory and the desktop command, and the opencode-dev skill explains both.


Added by the opencode bot (AI-generated).

Related: #56058, #36677

Public API: adds an optional directory query parameter to POST /api/debug/heap-dump, --directory to opencode debug heap-dump, and { directory } to the clients' debug.heapDump() (backwards compatible)

Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design is sound, but I could only partly verify it. Against a real opencode serve from this branch, with auth on, captureServer fetched the server's heap dump and moved it into the capture folder. The new tests pass and gui-extensions type-checks cleanly. I couldn't run the palette command, the window and main snapshots or the manifest without the Electron app; a screenshot of the toast and the resulting folder in the PR already show that part.

Public API is unchanged: this uses the existing /api/debug/heap-dump endpoint and adds an internal debug.heap Ipc between the desktop's main process and its windows.

One small edge case: if the Downloads folder doesn't exist, the whole capture fails with a generic error (see inline).

Related: #56058, #36677

)

// Snapshots hold prompts, file contents and credentials.
await mkdir(directory, { mode: 0o700 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app.getPath("downloads") can point to a folder that doesn't exist yet (e.g. minimal Linux setups with no ~/Downloads). Without recursive: true, this mkdir fails with ENOENT and the user only sees "Could not capture heap snapshots". mkdir(directory, { recursive: true, mode: 0o700 }) avoids that, and the folder name is already unique.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The missing-Downloads point is fixed: the folder is now created with recursive: true. The other new commits look sound and I have nothing new to flag: the command always captures the desktop's own server, the server's snapshot is only renamed into the folder (never copied), and windows are captured one at a time. The updated tests pass and gui-extensions type-checks cleanly. I still couldn't run the Electron side. Public API is unchanged.

@Hona Hona changed the title feat(desktop): capture heap snapshots from the command palette feat(desktop): capture app and server heap snapshots into one folder Oct 22, 2026
@Hona
Hona force-pushed the desktop-heap-dump branch from 7023be4 to 51d97e2 Compare October 22, 2026 23:48

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The missing-Downloads point is fixed (recursive: true). The new commits work against a real opencode serve from this branch: ?directory= with an absolute path writes there and creates missing folders, a relative path gets a 400, and opencode debug heap-dump --directory works with absolute and relative paths. The server, CLI and desktop heap tests pass, and tsc -b is clean in cli, server and gui-extensions. I couldn't run the Electron side.

Public API: two additions, both backwards compatible (requests without directory behave as before, and older servers ignore it):

  • POST /api/debug/heap-dump takes an optional directory query parameter (absolute, server-local, created if missing). A relative path returns 400 InvalidRequestError, and its 500 error message now includes the filesystem's reason.
  • opencode debug heap-dump --directory, and an optional { directory } for debug.heapDump() in the clients.

One new bug: an absolute --directory with a trailing slash wrongly prints 'The server ignored --directory' (see inline).

: cause,
})
// Servers released before `directory` existed ignore it and write into their log directory.
if (directory !== undefined && path.dirname(snapshot.path) !== path.normalize(directory))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path.normalize keeps a trailing separator, so --directory /tmp/snaps/ compares /tmp/snaps with /tmp/snaps/ and prints "The server ignored --directory…" even though the server wrote there. I reproduced this against a server from this branch. Resolving absolute paths too (path.resolve(value), which drops the trailing slash) or comparing with path.resolve on both sides avoids it. Only a POSIX path sent to a remote server from Windows needs to skip resolve, and path.posix.normalize won't strip the slash there either.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a8db41f. The CLI now checks the result the way the server builds it: it joins the directory and file name with the path style of the server's machine (win32 for a drive or UNC path, POSIX otherwise). That accepts a trailing separator and keeps a POSIX path for a remote server intact on Windows. The desktop side uses the same join. The CLI test now passes --directory

/\ and fails without the fix.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trailing-slash point is fixed: against a real opencode serve from this branch, --directory /tmp/…/d/ writes there without the wrong warning, and plain absolute and relative directories still work. The new comparison in the CLI and desktop is sound, and I have nothing new to flag. The CLI and desktop heap tests pass, and tsc -b is clean in cli and gui-extensions. I couldn't run the Electron side. Public API is unchanged since my last review.

@Hona
Hona merged commit 199ad46 into anomalyco:dev Oct 23, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants