anomalyco / anomalyco/opencode

Desktop renderer crashes: Solid store proxy Object.keys race + fatal 404/permission-server handling

未关闭
#46,292 1 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

@Hona 已经在做这个了。

开始于 2026年8月30日。

主要语言
TypeScript
星标
209k
派生
27.5k
平均合并
7 小时 2 分钟
30 天内合并 PR
384

描述

Summary

OpenCode desktop (v1.18.25, Windows, packaged app) crashes repeatedly at startup. The crashes are fatal renderer errors — the renderer dies, taking the app down. Three distinct failure modes observed, all renderer-side:

  1. Solid.js store proxy enumeration raceObject.keys() on the session_status store proxy throws V8 proxy-invariant errors.
  2. "Session not found" 404 treated as fatal — resolving/fetching messages for a session that no longer exists kills the renderer.
  3. "Permission server not found" treated as fatal — when the active server key is missing from the servers list, ensure() throws inside a memo during render.

All three are fatal renderer error entries in %APPDATA%\ai.opencode.desktop\logs\<session>\renderer.log followed by render process gone.


Bug 1: Object.keys() on Solid store proxy throws (primary crash, every startup)

Stack (identical code site across runs; V8 error message varies run to run):

TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
    at Object.keys (<anonymous>)
    at oc://renderer/assets/main-8t-QUXax.js:104444:19
    at Array.some (<anonymous>)
    at Object.fn (oc://renderer/assets/main-8t-QUXax.js:104443:45)
    at runComputation ...
    at batch ...
    at Object.setStore [as set] (oc://renderer/assets/main-8t-QUXax.js:34536:5)
    at oc://renderer/assets/main-8t-QUXax.js:61988:28
    at async retry (...)
    at async Promise.allSettled (index 3)

Variants seen on the same line: Getter must be a function: <number> and 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '...' which is either non-existent or configurable in the proxy target.

The code site (renderer bundle, main-*.js): an isWorking memo in the projects/home view:

const isWorking = createMemo(() => dirs().some((directory) => {
  return Object.keys(serverSync().session.data.session_status).some((id) => {
    if (serverSync().session.get(id)?.directory !== directory) return false;
    return serverSync().session.data.session_working(id);
  });
}));

Trigger: at bootstrap, the session_status store is written from two concurrent paths — the bootstrap slow-list (index 3 of Promise.allSettled: session.set("session_status", sessionID, reconcile(status))) and loadActiveSessionsQuery/seedActiveSessionStatuses — plus session events. The memo re-runs on every store update and enumerates the proxy while writers mutate the target → V8 proxy invariant violation → TypeError → fatal.

Amplifier: with a large session count (448 sessions in the shared opencode.db), the enumeration is large and the race hits on essentially every startup. It is timing/state dependent — the app can run fine for hours and then crash when the status map is written while the memo enumerates.

Mitigation that works (local patch): make the memo enumerate a stable snapshot instead of the live proxy:

const isWorking = createMemo(() => {
  let keys = [];
  try { keys = Object.keys({ ...serverSync().session.data.session_status }); } catch (e) { keys = []; }
  return dirs().some((directory) => keys.some((id) => {
    try {
      if (serverSync().session.get(id)?.directory !== directory) return false;
      return serverSync().session.data.session_working(id);
    } catch (e) { return false; }
  }));
});

(Note: spreading first — {...proxy} — avoids the getOwnPropertyDescriptor trap entirely.)

Bug 2: "Session not found" 404 is a fatal renderer error

Error: Session not found: ses_<id>
    at wrapClientError (main-8t-QUXax.js:57589:12)
    at request (main-8t-QUXax.js:52505:28)
    at async retry (...)
    at async fetchMessages (...)
    at async loadMessages (...)
    at async Promise.all (index 1)
Caused by: { "body": { "name": "NotFoundError", "data": { "message": "Session not found: ..." } }, "status": 404 }

The renderer resolves session IDs that appear in session status / lists but no longer exist server-side (stale local cache, deleted sessions, DB inconsistency between list and get). session.resolve() / fetchMessages() reject with a 404 and the rejection reaches the global handler → fatal renderer error.

Sessions in this crash were not present in the server's DB (SELECT ... FROM session returned nothing) but were still referenced by the app's persisted state (Local Storage / workspace state files). A deleted/stale session should never take down the UI.

Mitigation that works (local patch): resolve2 in the session store catches not-found errors and resolves null:

const request = (sessionApi ? sessionApi.get({ sessionID }).then(normalizeSessionInfo) : client2.session.get({ sessionID }).then((result) => {
  if (!result.data) throw sessionNotFoundError(sessionID);
  return result.data;
})).catch((err) => {
  if (isLocalSessionNotFoundError(err, sessionID) || isSessionNotFoundError(err, sessionID)) return null;
  const body = err?.body;
  if (body?.status === 404 || err?.status === 404 || body?.name === "NotFoundError") return null;
  throw err;
});
const resolved = request.then((result) => {
  if (result == null) return null;
  ...
});

Bug 3: "Permission server not found" is a fatal renderer error

Error: Permission server not found: http://100.126.12.120:8081
    at ensure (main-8t-QUXax.js:77458:24)
    at selected (main-8t-QUXax.js:77501:14)
    at Object.fn (main-8t-QUXax.js:77520:14)
    at runComputation ... (Solid memo during render)

When the app's active server (server.key, persisted from a previous session) is not present in global2.servers.list() (e.g., server config was cleared/updated, or a remote server became stale), ensure() throws inside a createMemo during render → fatal. This happens on startup when a remote server (Tailscale URL like http://100.126.12.120:8081) was the last active server but is not in the configured list.

The throw site should be defensive — a missing active server should fall back gracefully (e.g., to the local sidecar) rather than crash the renderer.


Environment

  • OpenCode desktop 1.18.25 (latest at time of writing), Windows 10/11, packaged (Electron + custom oc:// protocol)
  • Large shared storage: 448 sessions / 16.7k messages / 71.5k parts in opencode.db
  • Multiple remote servers configured (Tailscale/LAN http://host:8081), one of which was unreachable at crash time
  • Crash since v1.18.25; reproduced on every startup when session state is large and/or a stale remote server is the active one

Suggested fixes

  1. Do not run Object.keys() / Object.entries() directly on Solid store proxies that are being concurrently written; enumerate snapshots, or harden the store's getOwnPropertyDescriptor trap against V8 proxy invariant violations under concurrent mutation.
  2. Treat 404 / Session not found and Permission server not found as recoverable conditions — never fatal. Fall back (skip the session; use the local server) instead of letting the rejection reach the global error handler.
  3. Avoid restoring stale session references from persisted state (Local Storage / workspace caches) — validate against the server before use.

Happy to provide full log bundles / crashpad dumps if useful.

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。