deskbuddy 0.2.0-beta.37 → 0.2.0-beta.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -469,9 +469,9 @@ var defaultExec2 = (file, args, opts) => new Promise((resolve8, reject) => {
469
469
  } else resolve8();
470
470
  });
471
471
  });
472
- function ptyCandidatesIn(dir2, platform = process.platform, arch = process.arch) {
472
+ function ptyCandidatesIn(dir2, platform = process.platform, arch2 = process.arch) {
473
473
  const pkg = join2(dir2, "node_modules", PACKAGE_NAME);
474
- return [...hasVendoredPty(platform, arch) ? [join2(pkg, "vendor", "node-pty")] : [], join2(dir2, "node_modules", "node-pty")];
474
+ return [...hasVendoredPty(platform, arch2) ? [join2(pkg, "vendor", "node-pty")] : [], join2(dir2, "node_modules", "node-pty")];
475
475
  }
476
476
  function ptyLoadsIn(dir2, execPath = process.execPath) {
477
477
  const script = "for(const p of process.argv.slice(1)){try{const m=require(p);if(typeof m.spawn==='function')process.exit(0)}catch{}}process.exit(3)";
@@ -797,7 +797,7 @@ var AutoEnabler = class {
797
797
  import { randomBytes as randomBytes7 } from "crypto";
798
798
  import { existsSync as existsSync7, readdirSync as readdirSync11, readFileSync as readFileSync14, statSync as statSync13 } from "fs";
799
799
  import { homedir as homedir8 } from "os";
800
- import { basename as basename9, dirname as dirname9, join as join27 } from "path";
800
+ import { basename as basename9, dirname as dirname9, join as join28 } from "path";
801
801
  import WebSocket2 from "ws";
802
802
 
803
803
  // src/core/transcript/locate.ts
@@ -6924,10 +6924,10 @@ var SNAPSHOT = `(async () => {${PRELUDE2}
6924
6924
  if (JSON.stringify(out).length > ${SNAPSHOT_MAX}) return { ok: false, why: 'too_big' };
6925
6925
  return out;
6926
6926
  })()`;
6927
- function answerExpr2(sessionId, interruptId, type, body) {
6928
- const payload = { type, ...body.tool_use_id ? { tool_use_id: body.tool_use_id } : {}, answers: body.answers, state: body.state };
6927
+ function answerExpr2(sessionId, interruptId, type2, body) {
6928
+ const payload = { type: type2, ...body.tool_use_id ? { tool_use_id: body.tool_use_id } : {}, answers: body.answers, state: body.state };
6929
6929
  return `(async () => {${PRELUDE2}
6930
- const sid = ${JSON.stringify(sessionId)}, iid = ${JSON.stringify(interruptId)}, want = ${JSON.stringify(type)};
6930
+ const sid = ${JSON.stringify(sessionId)}, iid = ${JSON.stringify(interruptId)}, want = ${JSON.stringify(type2)};
6931
6931
  const payload = ${JSON.stringify(payload)};
6932
6932
  const list = (S().agentInterruptCards || {})[sid] || [];
6933
6933
  const c = list.find((x) => x && String(x.interruptId) === iid);
@@ -13108,11 +13108,11 @@ var CodexWatch = class {
13108
13108
  }
13109
13109
  /** 会话状态变了(广播 / 补查):active 订阅,其余退订 */
13110
13110
  #applyStatus(id, status) {
13111
- const type = status.type;
13112
- if (typeof type !== "string") return;
13111
+ const type2 = status.type;
13112
+ if (typeof type2 !== "string") return;
13113
13113
  const prev = this.#status.get(id);
13114
- this.#status.set(id, type);
13115
- if (type === "idle" && prev !== "idle") {
13114
+ this.#status.set(id, type2);
13115
+ if (type2 === "idle" && prev !== "idle") {
13116
13116
  try {
13117
13117
  this.#d.onIdle?.(id);
13118
13118
  } catch {
@@ -13121,7 +13121,7 @@ var CodexWatch = class {
13121
13121
  const flags = Array.isArray(status.activeFlags) ? status.activeFlags : [];
13122
13122
  if (flags.includes("waitingOnApproval")) this.#waiting.add(id);
13123
13123
  else this.#waiting.delete(id);
13124
- if (type === "active") {
13124
+ if (type2 === "active") {
13125
13125
  this.#idle.delete(id);
13126
13126
  this.#subscribe(id);
13127
13127
  } else {
@@ -13220,8 +13220,8 @@ var CodexWatch = class {
13220
13220
  };
13221
13221
  this.#own.set(tid, (this.#own.get(tid) ?? 0) + 1);
13222
13222
  const prev = this.#chain.get(tid);
13223
- let release;
13224
- const mine = new Promise((r) => release = r);
13223
+ let release2;
13224
+ const mine = new Promise((r) => release2 = r);
13225
13225
  const tail4 = (prev ?? Promise.resolve()).then(() => mine);
13226
13226
  this.#chain.set(tid, tail4);
13227
13227
  try {
@@ -13234,7 +13234,7 @@ var CodexWatch = class {
13234
13234
  this.#d.log(`Codex \u53D1\u4E00\u8F6E\u51FA\u9519\uFF1A${e.message}`);
13235
13235
  return { sent: false, reason: "start_failed" };
13236
13236
  } finally {
13237
- release();
13237
+ release2();
13238
13238
  if (this.#chain.get(tid) === tail4) this.#chain.delete(tid);
13239
13239
  const n = (this.#own.get(tid) ?? 1) - 1;
13240
13240
  if (n > 0) this.#own.set(tid, n);
@@ -13425,11 +13425,11 @@ var CodexWatch = class {
13425
13425
  case "thread/status/changed": {
13426
13426
  const id = typeof p.threadId === "string" ? p.threadId : "";
13427
13427
  const status = p.status ?? {};
13428
- const type = status.type;
13428
+ const type2 = status.type;
13429
13429
  if (!id) return;
13430
- if (type === "notLoaded") this.#setLoaded(id, false);
13431
- else if (typeof type === "string") this.#setLoaded(id, true);
13432
- if (typeof type === "string") this.#applyStatus(id, status);
13430
+ if (type2 === "notLoaded") this.#setLoaded(id, false);
13431
+ else if (typeof type2 === "string") this.#setLoaded(id, true);
13432
+ if (typeof type2 === "string") this.#applyStatus(id, status);
13433
13433
  else {
13434
13434
  this.#idle.add(id);
13435
13435
  this.#unsubscribe(id);
@@ -14286,9 +14286,37 @@ function describe2(r) {
14286
14286
  return [r.status, r.n !== void 0 ? `n=${r.n}` : "", r.detail ?? ""].filter(Boolean).join(" ");
14287
14287
  }
14288
14288
 
14289
+ // src/daemon/health.ts
14290
+ import { arch, release, type } from "os";
14291
+ import { join as join24 } from "path";
14292
+ var MEM_LOG_MS = 30 * 60 * 1e3;
14293
+ function envLine() {
14294
+ return `\u8FD0\u884C\u73AF\u5883\uFF1A${type()} ${release()} ${arch()}\uFF1BNode ${process.version}\uFF1BDeskBuddy ${packageVersion()}`;
14295
+ }
14296
+ var mb = (n) => Math.round(n / 1024 / 1024);
14297
+ function memLine() {
14298
+ const m = process.memoryUsage();
14299
+ let handles = -1;
14300
+ try {
14301
+ handles = process.getActiveResourcesInfo().length;
14302
+ } catch {
14303
+ }
14304
+ return `\u5185\u5B58\uFF1Arss ${mb(m.rss)}MB\uFF0C\u5806 ${mb(m.heapUsed)}/${mb(m.heapTotal)}MB\uFF0Cexternal ${mb(m.external)}MB\uFF0CarrayBuffers ${mb(m.arrayBuffers)}MB\uFF1B\u6D3B\u52A8\u53E5\u67C4 ${handles}`;
14305
+ }
14306
+ function enableFatalReport(home) {
14307
+ try {
14308
+ const r = process.report;
14309
+ if (!r) return;
14310
+ r.directory = join24(home, "reports");
14311
+ r.reportOnFatalError = true;
14312
+ r.compact = true;
14313
+ } catch {
14314
+ }
14315
+ }
14316
+
14289
14317
  // src/daemon/dshVersion.ts
14290
14318
  import { readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
14291
- import { dirname as dirname8, join as join24 } from "path";
14319
+ import { dirname as dirname8, join as join25 } from "path";
14292
14320
  var DSH_HEADLESS_MIN_VERSION = "0.0.1-rc.1";
14293
14321
  var DSH_PKG = "@deepseek-ai/dsh";
14294
14322
  var DSH_VERSION_UNSUPPORTED = "tool_version_unsupported";
@@ -14320,7 +14348,7 @@ function dshSupport(d = defaultDeps2) {
14320
14348
  }
14321
14349
  let dir2 = dirname8(real2);
14322
14350
  for (let i = 0; i < 4; i++) {
14323
- const raw = d.readFile(join24(dir2, "package.json"));
14351
+ const raw = d.readFile(join25(dir2, "package.json"));
14324
14352
  if (raw !== null) {
14325
14353
  let pkg = {};
14326
14354
  try {
@@ -14838,17 +14866,17 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
14838
14866
 
14839
14867
  // src/core/appended.ts
14840
14868
  import { chmodSync as chmodSync4, mkdirSync as mkdirSync11, readFileSync as readFileSync12, renameSync as renameSync8, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
14841
- import { join as join25 } from "path";
14869
+ import { join as join26 } from "path";
14842
14870
  var APPENDED_MAX_ITEMS = 5;
14843
14871
  var APPENDED_TEXT_MAX = 500;
14844
14872
  var APPENDED_RESULT_MAX = 800;
14845
14873
  var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14846
14874
  function appendedDir() {
14847
- return join25(deskbuddyHome(), "appended");
14875
+ return join26(deskbuddyHome(), "appended");
14848
14876
  }
14849
14877
  function appendedPath(sessionId) {
14850
14878
  const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
14851
- return safe2 ? join25(appendedDir(), `${safe2}.json`) : null;
14879
+ return safe2 ? join26(appendedDir(), `${safe2}.json`) : null;
14852
14880
  }
14853
14881
  function readAppended(sessionId) {
14854
14882
  const file = appendedPath(sessionId);
@@ -14878,13 +14906,13 @@ function writeAppended(sessionId, item) {
14878
14906
  var RUNNING_TTL_MS = 60 * 60 * 1e3;
14879
14907
  function runningPath(sessionId) {
14880
14908
  const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
14881
- return safe2 ? join25(deskbuddyHome(), "running", `${safe2}.json`) : null;
14909
+ return safe2 ? join26(deskbuddyHome(), "running", `${safe2}.json`) : null;
14882
14910
  }
14883
14911
  function writeRunning(sessionId, now = Date.now()) {
14884
14912
  const file = runningPath(sessionId);
14885
14913
  if (!file) return;
14886
14914
  try {
14887
- mkdirSync11(join25(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
14915
+ mkdirSync11(join26(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
14888
14916
  writeFileSync10(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
14889
14917
  renameSync8(`${file}.tmp`, file);
14890
14918
  } catch {
@@ -14932,7 +14960,7 @@ function resultSummary(tool, stdout6) {
14932
14960
 
14933
14961
  // src/core/dshMailbox.ts
14934
14962
  import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync13, renameSync as renameSync9, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
14935
- import { join as join26 } from "path";
14963
+ import { join as join27 } from "path";
14936
14964
  var DSH_HEARTBEAT_FRESH_MS = 5 * 1e3;
14937
14965
  var DSH_ACK_TIMEOUT_MS = 8 * 1e3;
14938
14966
  var ACK_POLL_MS = 200;
@@ -14943,10 +14971,10 @@ var REVOKE_RETRY_MS = 50;
14943
14971
  var SID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
14944
14972
  var IID_RE = /^[A-Za-z0-9_-]{1,128}$/;
14945
14973
  function dshMailboxDir(home = deskbuddyHome(), channel = "dsh") {
14946
- return join26(home, channel);
14974
+ return join27(home, channel);
14947
14975
  }
14948
14976
  function sub(home, name, channel = "dsh") {
14949
- return join26(dshMailboxDir(home, channel), name);
14977
+ return join27(dshMailboxDir(home, channel), name);
14950
14978
  }
14951
14979
  function ensureDir2(p) {
14952
14980
  mkdirSync12(p, { recursive: true, mode: 448 });
@@ -14986,7 +15014,7 @@ function liveHeartbeats(sessionId, d0 = {}) {
14986
15014
  if (!/^\d+\.json$/.test(n)) continue;
14987
15015
  let hb;
14988
15016
  try {
14989
- hb = JSON.parse(readFileSync13(join26(dir2, n), "utf8"));
15017
+ hb = JSON.parse(readFileSync13(join27(dir2, n), "utf8"));
14990
15018
  } catch {
14991
15019
  continue;
14992
15020
  }
@@ -15010,8 +15038,8 @@ async function dshDeliver(sessionId, msg, d0 = {}) {
15010
15038
  const ackDir = sub(d.home, "ack", d.channel);
15011
15039
  ensureDir2(inbox);
15012
15040
  ensureDir2(ackDir);
15013
- const letter = join26(inbox, `${sessionId}.${msg.instructionId}.json`);
15014
- const ackPath = join26(ackDir, `${msg.instructionId}.json`);
15041
+ const letter = join27(inbox, `${sessionId}.${msg.instructionId}.json`);
15042
+ const ackPath = join27(ackDir, `${msg.instructionId}.json`);
15015
15043
  if (existsSync6(letter)) return { ok: false, reason: "inbox_busy" };
15016
15044
  const writtenAt = d.now();
15017
15045
  writeJson(letter, {
@@ -15080,7 +15108,7 @@ function dshClearInbox(home = deskbuddyHome(), channel = "dsh") {
15080
15108
  var DSH_REPLY_WAIT_MS = 4 * 60 * 1e3;
15081
15109
  function takeMailboxReply(instructionId, home = deskbuddyHome(), channel = "dsh") {
15082
15110
  if (!IID_RE.test(instructionId)) return null;
15083
- const p = join26(sub(home, "reply", channel), `${instructionId}.json`);
15111
+ const p = join27(sub(home, "reply", channel), `${instructionId}.json`);
15084
15112
  let v = null;
15085
15113
  try {
15086
15114
  v = JSON.parse(readFileSync13(p, "utf8"));
@@ -15109,7 +15137,7 @@ function clearDir(inbox) {
15109
15137
  let n = 0;
15110
15138
  for (const f of names) {
15111
15139
  try {
15112
- rmSync10(join26(inbox, f), { force: true });
15140
+ rmSync10(join27(inbox, f), { force: true });
15113
15141
  n++;
15114
15142
  } catch {
15115
15143
  }
@@ -15184,7 +15212,7 @@ function defaultCodexAppServers() {
15184
15212
  }
15185
15213
  function opencodeReady() {
15186
15214
  const mod = ocPluginModule(installedHookPath());
15187
- return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(join27(dirname9(mod), "ocTranslate.mjs"));
15215
+ return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(join28(dirname9(mod), "ocTranslate.mjs"));
15188
15216
  }
15189
15217
  var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
15190
15218
  function orphanResumeText(summary2) {
@@ -15251,6 +15279,7 @@ var Daemon = class {
15251
15279
  #ws = null;
15252
15280
  #idleTimer = null;
15253
15281
  #windowScan = null;
15282
+ #memLog = null;
15254
15283
  #aliveReport = null;
15255
15284
  #reportRetryTimer = null;
15256
15285
  #reportRetry = new ReportRetry({
@@ -15604,6 +15633,10 @@ var Daemon = class {
15604
15633
  async startGate() {
15605
15634
  const port = await this.#gate.start();
15606
15635
  log(`\u6388\u6743\u7AEF\u70B9\u5DF2\u5C31\u7EEA 127.0.0.1:${port}`);
15636
+ enableFatalReport(deskbuddyHome());
15637
+ log(`${envLine()}\uFF1B${memLine()}`);
15638
+ this.#memLog = setInterval(() => log(memLine()), MEM_LOG_MS);
15639
+ this.#memLog.unref();
15607
15640
  this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
15608
15641
  this.#windowScan.unref();
15609
15642
  this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
@@ -16002,6 +16035,7 @@ var Daemon = class {
16002
16035
  this.#clearIdle();
16003
16036
  if (this.#windowScan) clearInterval(this.#windowScan);
16004
16037
  if (this.#aliveReport) clearInterval(this.#aliveReport);
16038
+ if (this.#memLog) clearInterval(this.#memLog);
16005
16039
  if (this.#reportRetryTimer) clearInterval(this.#reportRetryTimer);
16006
16040
  if (this.#codexTimer) clearInterval(this.#codexTimer);
16007
16041
  if (this.#wakeTimer) clearInterval(this.#wakeTimer);
@@ -16582,8 +16616,8 @@ var Daemon = class {
16582
16616
  }
16583
16617
  const sid = resolveAlias(cmd.sessionId);
16584
16618
  for (let p = this.#opening.get(sid); p; p = this.#opening.get(sid)) await p;
16585
- let release;
16586
- this.#opening.set(sid, new Promise((r) => release = r));
16619
+ let release2;
16620
+ this.#opening.set(sid, new Promise((r) => release2 = r));
16587
16621
  try {
16588
16622
  if (this.#queue.busy(sid)) await this.#queue.enqueue(sid, async () => {
16589
16623
  });
@@ -16605,7 +16639,7 @@ var Daemon = class {
16605
16639
  await this.#openWindowFor(cmd, sid, r.cwd);
16606
16640
  } finally {
16607
16641
  this.#opening.delete(sid);
16608
- release();
16642
+ release2();
16609
16643
  }
16610
16644
  }
16611
16645
  #guiAvailable() {
@@ -17044,7 +17078,7 @@ var Daemon = class {
17044
17078
  for (const n of names) {
17045
17079
  if (!n.endsWith(".json")) continue;
17046
17080
  try {
17047
- const s2 = JSON.parse(readFileSync14(join27(stateDir(), n), "utf8"));
17081
+ const s2 = JSON.parse(readFileSync14(join28(stateDir(), n), "utf8"));
17048
17082
  if (typeof s2.sessionId === "string" && typeof s2.tool === "string" && enabled.has(s2.tool)) out.push({ tool: s2.tool, sessionId: s2.sessionId, state: typeof s2.state === "string" ? s2.state : "", cwd: typeof s2.cwd === "string" ? s2.cwd : "" });
17049
17083
  } catch {
17050
17084
  }
@@ -17078,12 +17112,12 @@ var Daemon = class {
17078
17112
  }
17079
17113
  case "deepseek_harness": {
17080
17114
  if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
17081
- const p = join27(deskbuddyHome(), "dsh", "transcript", `${s2.sessionId}.jsonl`);
17115
+ const p = join28(deskbuddyHome(), "dsh", "transcript", `${s2.sessionId}.jsonl`);
17082
17116
  return existsSync7(p) ? { kind: "file", path: p, format: "dsh" } : null;
17083
17117
  }
17084
17118
  case "opencode": {
17085
17119
  if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
17086
- return snapshotSource(`opencode-snap:${s2.sessionId}`, join27(deskbuddyHome(), "opencode", "transcript", `${s2.sessionId}.json`));
17120
+ return snapshotSource(`opencode-snap:${s2.sessionId}`, join28(deskbuddyHome(), "opencode", "transcript", `${s2.sessionId}.json`));
17087
17121
  }
17088
17122
  case "dumate":
17089
17123
  return { kind: "http", key: "dumate", fetch: () => this.#dumate.transcriptMessages(s2.sessionId) };
@@ -18031,7 +18065,7 @@ async function runDisable(tools, opts = {}) {
18031
18065
 
18032
18066
  // src/commands/install.ts
18033
18067
  import { mkdirSync as mkdirSync13, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
18034
- import { join as join28 } from "path";
18068
+ import { join as join29 } from "path";
18035
18069
 
18036
18070
  // src/commands/setup.ts
18037
18071
  import qrcode from "qrcode-terminal";
@@ -18248,7 +18282,7 @@ function inSandbox(env) {
18248
18282
  }
18249
18283
  function precheckWritable(dirs) {
18250
18284
  for (const dir2 of dirs) {
18251
- const probe = join28(dir2, `.deskbuddy-probe-${process.pid}`);
18285
+ const probe = join29(dir2, `.deskbuddy-probe-${process.pid}`);
18252
18286
  try {
18253
18287
  mkdirSync13(dir2, { recursive: true });
18254
18288
  writeFileSync13(probe, "");
@@ -18658,7 +18692,7 @@ async function runAutoSwitch(opts) {
18658
18692
  // src/wrap/run.ts
18659
18693
  import { randomBytes as randomBytes10 } from "crypto";
18660
18694
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync15, statSync as statSync16, truncateSync as truncateSync3 } from "fs";
18661
- import { join as join31 } from "path";
18695
+ import { join as join32 } from "path";
18662
18696
  import { StringDecoder as StringDecoder2 } from "string_decoder";
18663
18697
 
18664
18698
  // src/wrap/injector.ts
@@ -19158,7 +19192,7 @@ import { execFileSync as execFileSync4, spawn as spawn4 } from "child_process";
19158
19192
  import { closeSync as closeSync9, mkdirSync as mkdirSync14, openSync as openSync9, readdirSync as readdirSync12, readFileSync as readFileSync16, rmSync as rmSync14, statSync as statSync15, truncateSync as truncateSync2, writeFileSync as writeFileSync14, renameSync as renameSync10 } from "fs";
19159
19193
  import { request as request2 } from "http";
19160
19194
  import { createServer as createServer5 } from "net";
19161
- import { join as join29 } from "path";
19195
+ import { join as join30 } from "path";
19162
19196
  var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
19163
19197
  var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
19164
19198
  var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
@@ -19327,10 +19361,10 @@ async function runCodexWindow(real2, args, d) {
19327
19361
  }
19328
19362
  var MAX_LOG_BYTES2 = 1024 * 1024;
19329
19363
  function appServerLogPath() {
19330
- return join29(deskbuddyHome(), "codex-app-server.log");
19364
+ return join30(deskbuddyHome(), "codex-app-server.log");
19331
19365
  }
19332
19366
  function windowsDir2() {
19333
- return join29(deskbuddyHome(), "codex-windows");
19367
+ return join30(deskbuddyHome(), "codex-windows");
19334
19368
  }
19335
19369
  function pidAlive5(pid) {
19336
19370
  try {
@@ -19420,7 +19454,7 @@ function killOwnServer(pid, url, args = freshArgs, kill = (p) => killServer(p))
19420
19454
  if (a && isAppServerArgs(a, url)) kill(pid);
19421
19455
  }
19422
19456
  function recordPath(rec) {
19423
- return join29(windowsDir2(), `${rec.server}.json`);
19457
+ return join30(windowsDir2(), `${rec.server}.json`);
19424
19458
  }
19425
19459
  function writeRecord(rec) {
19426
19460
  try {
@@ -19445,7 +19479,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
19445
19479
  return;
19446
19480
  }
19447
19481
  for (const n of names) {
19448
- const p = join29(dir2, n);
19482
+ const p = join30(dir2, n);
19449
19483
  let rec;
19450
19484
  try {
19451
19485
  rec = JSON.parse(readFileSync16(p, "utf8"));
@@ -19468,7 +19502,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
19468
19502
  function launchLog(line) {
19469
19503
  try {
19470
19504
  mkdirSync14(deskbuddyHome(), { recursive: true });
19471
- writeFileSync14(join29(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
19505
+ writeFileSync14(join30(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
19472
19506
  `, { flag: "a" });
19473
19507
  } catch {
19474
19508
  }
@@ -19493,7 +19527,7 @@ function defaultCodexWindowDeps(base) {
19493
19527
  // src/wrap/promptLaunch.ts
19494
19528
  import { randomBytes as randomBytes8, randomUUID as randomUUID3 } from "crypto";
19495
19529
  import { homedir as homedir10 } from "os";
19496
- import { join as join30 } from "path";
19530
+ import { join as join31 } from "path";
19497
19531
  var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
19498
19532
  function promptArgs(text, tool = "") {
19499
19533
  if (!text) return [];
@@ -19503,7 +19537,7 @@ function promptArgs(text, tool = "") {
19503
19537
  function promptEnv(tool, env, launcherPid = process.pid, answerWindowId = "") {
19504
19538
  const out = { ...env };
19505
19539
  if (tool === "workbuddy") {
19506
- if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join30(homedir10(), ".workbuddy");
19540
+ if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join31(homedir10(), ".workbuddy");
19507
19541
  out.DESKBUDDY_LAUNCHER_PID = String(launcherPid);
19508
19542
  out.DESKBUDDY_LAUNCHED_TOOL = tool;
19509
19543
  if (answerWindowId) out.DESKBUDDY_LAUNCHED_WINDOW = answerWindowId;
@@ -20087,7 +20121,7 @@ function childEnv(src, windowId, takeover = true) {
20087
20121
  }
20088
20122
  function wrapLog(line) {
20089
20123
  try {
20090
- const p = join31(deskbuddyHome(), "wrap.log");
20124
+ const p = join32(deskbuddyHome(), "wrap.log");
20091
20125
  mkdirSync15(deskbuddyHome(), { recursive: true });
20092
20126
  try {
20093
20127
  if (statSync16(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
package/dist/gate.cjs CHANGED
@@ -64,7 +64,7 @@ function readStdin() {
64
64
  })
65
65
  }
66
66
 
67
- function post(port, token, body) {
67
+ function post(port, token, body, info) {
68
68
  return new Promise((resolve, reject) => {
69
69
  const req = http.request(
70
70
  {
@@ -87,10 +87,75 @@ function post(port, token, body) {
87
87
  }
88
88
  )
89
89
  req.on('error', reject)
90
+ // 连上过(守护进程收下了请求)还是根本没连上:窗口模式据此决定要不要等守护进程重启(问题表 268)
91
+ if (info) req.on('socket', (sock) => (sock.connecting ? sock.once('connect', () => (info.connected = true)) : (info.connected = true)))
90
92
  req.end(body)
91
93
  })
92
94
  }
93
95
 
96
+ /** 守护进程等待中途没了(崩溃 / 被杀),launcher 10 秒后重拉:最多等这么久,等它换了新端口再提交一次 */
97
+ const RESTART_WAIT_MS = 3 * 60 * 1000
98
+ const RESTART_POLL_MS = 2000
99
+
100
+ /** 与 src/daemon/autostart.ts STOP_MARK_FILE / STOP_MARK_TTL_MS 一致:DeskBuddy 自己的停止流程留的标记 */
101
+ const STOP_MARK_FILE = 'daemon.stopping'
102
+ const STOP_MARK_TTL_MS = 2 * 60 * 1000
103
+ function stopMarkFresh(now) {
104
+ try {
105
+ const t = Number(fs.readFileSync(path.join(deskbuddyHome(), STOP_MARK_FILE), 'utf8').trim())
106
+ return Number.isFinite(t) && now - t >= 0 && now - t < STOP_MARK_TTL_MS
107
+ } catch (_) {
108
+ return false
109
+ }
110
+ }
111
+
112
+ function readGateConf() {
113
+ try {
114
+ const c = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
115
+ return c && Number.isInteger(c.port) && typeof c.token === 'string' && c.token ? c : null
116
+ } catch (_) {
117
+ return null
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 窗口模式的 POST:守护进程在等设备答复期间没了(连上过、之后连接断开),不放弃 ——
123
+ * 等它重启(gate.json 换了端口 / 令牌)后把同一个授权再提交一次,设备上重新出卡(问题表 268:
124
+ * 崩溃后窗口里那道授权被撤、再也推不到设备)。🔴 一开始就连不上(守护进程根本不在)照旧立刻失败,不卡窗口
125
+ */
126
+ async function postSurviving(conf, body, deps) {
127
+ const d = deps || {}
128
+ const send = d.post || post
129
+ const now = d.now || Date.now
130
+ const sleep = d.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
131
+ const read = d.readConf || readGateConf
132
+ const stopping = d.stopping || stopMarkFresh
133
+ let cur = conf
134
+ let deadline = 0
135
+ for (;;) {
136
+ const info = { connected: false }
137
+ try {
138
+ return await send(cur.port, cur.token, body, info)
139
+ } catch (e) {
140
+ // 连上过又断了 = 守护进程这次没了,重新计时(重提交后又崩一次也能再等);没连上且不在等重启 = 守护进程根本不在
141
+ if (info.connected) deadline = now() + RESTART_WAIT_MS
142
+ else if (!deadline) throw e
143
+ // 等新的守护进程起来:gate.json 换了才再提交(同一个端口还连不上就接着等)
144
+ for (;;) {
145
+ // 🔴 正常停止(disable / 升级 / 未授权退出)不等:停止流程先删 gate.json,Windows 结束进程时留停止标记(评审必修)
146
+ if (now() >= deadline || stopping(now())) throw e
147
+ await sleep(RESTART_POLL_MS)
148
+ const c = read()
149
+ if (!c || stopping(now())) throw e
150
+ if (c.port !== cur.port || c.token !== cur.token) {
151
+ cur = c
152
+ break
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+
94
159
  /**
95
160
  * 这个 hook 是不是由【交互窗口】里的 Claude Code 触发的。
96
161
  * 🔴 实测(2026-09-24,2.1.259):`claude -p` 碰到要授权的工具**也会**触发 PermissionRequest,
@@ -792,7 +857,7 @@ async function windowMain() {
792
857
  ...(lang ? { lang } : {}),
793
858
  ...(tool === 'claude_code' ? { ccAutoMode: ccAutoMode(hook) } : {}),
794
859
  }
795
- const res = await post(conf.port, conf.token, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
860
+ const res = await postSurviving(conf, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
796
861
  if (res.status !== 200) return fail()
797
862
  const out = JSON.parse(res.data)
798
863
  if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
@@ -925,5 +990,5 @@ if (require.main === module) {
925
990
  } else {
926
991
  // 只给单测(智能体永远是 `node gate.cjs` 直接跑)。
927
992
  // 🔴 禁止通过 require 调用本关卡:main 不跑,进程会 exit 0 = 放行
928
- module.exports = { preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
993
+ module.exports = { postSurviving, RESTART_WAIT_MS, preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
929
994
  }
package/dist/launcher.cjs CHANGED
@@ -54,6 +54,77 @@ function paths(h) {
54
54
  crashes: path.join(versions, 'crashes.json'),
55
55
  log: path.join(h, 'daemon.log'),
56
56
  pid: path.join(h, 'launcher.pid'),
57
+ stderr: path.join(h, 'daemon.stderr.log'),
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 子进程 stderr 落盘(问题表 268):Windows 上 Node 的致命错误(V8 内存耗尽、libuv 断言、原生模块 abort)
63
+ * 退出码都是 0xC0000409,原因只打在 stderr 上;launcher 是隐藏启动的,stderr 没人接 → 日志里什么都没有。
64
+ * 超过 1MB 先转存成 .1(只留一份)。打不开返回 null(照旧 inherit)
65
+ */
66
+ const STDERR_MAX_BYTES = 1024 * 1024
67
+ function openStderr(h) {
68
+ const p = paths(h).stderr
69
+ try {
70
+ fs.mkdirSync(h, { recursive: true })
71
+ try {
72
+ if (fs.statSync(p).size > STDERR_MAX_BYTES) fs.renameSync(p, p + '.1')
73
+ } catch {
74
+ /* 还没有这个文件 */
75
+ }
76
+ const fd = fs.openSync(p, 'a')
77
+ return { fd, start: fs.fstatSync(fd).size }
78
+ } catch {
79
+ return null
80
+ }
81
+ }
82
+
83
+ const FATAL_LINE_RE = /FATAL ERROR|Fatal error|Assertion failed|Check failed|Uncaught|Error:|Exception|out of memory|abort/i
84
+
85
+ /** 这次运行期间写进 stderr 文件的末尾(最多 1500 字符,去掉空行);没有返回 '' */
86
+ function stderrTail(h, start) {
87
+ try {
88
+ const p = paths(h).stderr
89
+ const size = fs.statSync(p).size
90
+ const from = Math.max(start, size - 16384)
91
+ if (size <= from) return ''
92
+ const buf = Buffer.alloc(size - from)
93
+ const fd = fs.openSync(p, 'r')
94
+ try {
95
+ fs.readSync(fd, buf, 0, buf.length, from)
96
+ } finally {
97
+ fs.closeSync(fd)
98
+ }
99
+ const lines = buf.toString('utf8').split(/\r?\n/).filter((l) => l.trim())
100
+ // 关键行(V8 / libuv / Node 的致命错误头、未捕获异常)放前面:原生调用栈在后面,只留末尾几行
101
+ const key = lines.filter((l) => FATAL_LINE_RE.test(l) && !/^\s*\d+: 0x/.test(l)).slice(0, 4) // 原生调用栈帧(` 1: 0x… node::Abort()`)不算
102
+ const tail = lines.filter((l) => !key.includes(l)).slice(-4)
103
+ return [...key, ...(tail.length ? ['…', ...tail] : [])].join(' | ').slice(0, 1500)
104
+ } catch {
105
+ return ''
106
+ }
107
+ }
108
+
109
+ /** Node 致命错误时写诊断报告(JS / 原生调用栈、堆、句柄)的目录;只留最新 5 份 */
110
+ const REPORT_KEEP = 5
111
+ function reportDir(h) {
112
+ const d = path.join(h, 'reports')
113
+ try {
114
+ fs.mkdirSync(d, { recursive: true })
115
+ const old = fs.readdirSync(d).filter((n) => n.endsWith('.json')).sort()
116
+ for (const n of old.slice(0, Math.max(0, old.length - REPORT_KEEP))) fs.rmSync(path.join(d, n), { force: true })
117
+ } catch {
118
+ /* 清不了不影响启动 */
119
+ }
120
+ return d
121
+ }
122
+
123
+ function newReports(dir, since) {
124
+ try {
125
+ return fs.readdirSync(dir).filter((n) => n.endsWith('.json') && fs.statSync(path.join(dir, n)).mtimeMs >= since)
126
+ } catch {
127
+ return []
57
128
  }
58
129
  }
59
130
 
@@ -255,7 +326,25 @@ const AFTER_STOP_POLL_MS = 1000
255
326
  function runOnce(h, cur, args) {
256
327
  return new Promise((resolve) => {
257
328
  let stopping = false
258
- const child = spawn(process.execPath, [cur.cliPath, ...args], { stdio: 'inherit', windowsHide: true, env: childEnv(process.env) })
329
+ const err = openStderr(h)
330
+ const reports = reportDir(h)
331
+ const startedAt = Date.now()
332
+ let child
333
+ try {
334
+ child = spawn(process.execPath, ['--report-on-fatalerror', '--report-compact', '--report-directory=' + reports, cur.cliPath, ...args], {
335
+ stdio: ['inherit', 'inherit', err ? err.fd : 'inherit'],
336
+ windowsHide: true,
337
+ env: childEnv(process.env),
338
+ })
339
+ } finally {
340
+ if (err) {
341
+ try {
342
+ fs.closeSync(err.fd) // 子进程已经拿到一份
343
+ } catch {
344
+ /* ignore */
345
+ }
346
+ }
347
+ }
259
348
  const forward = (sig) => {
260
349
  stopping = true
261
350
  try {
@@ -269,9 +358,9 @@ function runOnce(h, cur, args) {
269
358
  handlers[sig] = () => forward(sig)
270
359
  process.on(sig, handlers[sig])
271
360
  }
272
- const done = (code, signal, err) => {
361
+ const done = (code, signal, e) => {
273
362
  for (const sig of Object.keys(handlers)) process.removeListener(sig, handlers[sig])
274
- resolve({ code, signal, stopping, err })
363
+ resolve({ code, signal, stopping, err: e, stderr: err ? stderrTail(h, err.start) : '', reports: newReports(reports, startedAt) })
275
364
  }
276
365
  child.on('error', (e) => done(null, null, e))
277
366
  child.on('exit', (code, signal) => done(code, signal, null))
@@ -323,6 +412,10 @@ async function main(argv, deps) {
323
412
  }
324
413
  const startedAt = now()
325
414
  const r = await run(cur)
415
+ // 🔴 退出后重读一次:跑着的时候 daemon 可能已把 verifying 改成 good(连满 3 分钟)。
416
+ // 用启动时的旧副本会把验证通过之后的崩溃也算成「验证期内」、不记退出码(问题表 268 的 4 次「验证期内异常退出」)
417
+ const latest = readCurrent(h)
418
+ const ran = latest && latest.version === cur.version ? latest : cur
326
419
  const abnormal = !r.stopping && (r.err || r.signal || r.code !== 0)
327
420
  if (r.err) log(h, '起不来版本 ' + cur.version + ':' + r.err.message)
328
421
  if (!abnormal) return r.code == null ? 0 : r.code
@@ -340,16 +433,23 @@ async function main(argv, deps) {
340
433
  log(h, '守护进程已退出(DeskBuddy 停止流程),launcher 不再重拉')
341
434
  return 0
342
435
  }
436
+ // 崩溃现场(问题表 268):退出码、stderr 末尾、诊断报告文件名记进 daemon.log
437
+ log(
438
+ h,
439
+ '守护进程退出码 ' + (r.code == null ? r.signal || '?' : r.code) +
440
+ (r.stderr ? ';stderr 末尾:' + r.stderr : ';stderr 没有输出') +
441
+ (r.reports && r.reports.length ? ';诊断报告:reports/' + r.reports.join(', reports/') : '')
442
+ )
343
443
  const killed = terminated(r)
344
444
  // 被外部 SIGTERM 不是这一版的毛病:不计崩溃、不触发回滚
345
- const rolledBack = killed ? false : recordCrash(h, cur, now())
445
+ const rolledBack = killed ? false : recordCrash(h, ran, now())
346
446
  if (killed) log(h, '守护进程被外部结束(SIGTERM,不是 DeskBuddy 的停止流程),交给' + (platform === 'win32' ? ' launcher ' : '开机自启') + '重新拉起')
347
447
  // 非 Windows:交给 launchd / systemd 按「非正常退出」重拉(它们会重新执行本 launcher,读到回滚后的 current.json)
348
448
  if (platform !== 'win32') return r.code == null ? 1 : r.code || 1
349
449
  // Windows:「启动」文件夹只在登录时跑一次,退了没人拉 —— 本进程自己重拉。
350
450
  // 🔴 DeskBuddy 自己的停止流程先结束本 launcher 再结束 daemon(autostart.ts stopPidFileDaemon),并留了标记,不会走到这里
351
451
  let wait = WIN_RESTART_MS
352
- if (cur.state !== 'verifying' && !rolledBack) {
452
+ if (ran.state !== 'verifying' && !rolledBack) {
353
453
  winFails = now() - startedAt >= WIN_STABLE_MS ? 1 : winFails + 1
354
454
  wait = Math.min(WIN_RESTART_MS * Math.pow(2, winFails - 1), WIN_RESTART_MAX_MS)
355
455
  log(h, '守护进程异常退出(退出码 ' + (r.code == null ? r.signal || '?' : r.code) + '),' + Math.round(wait / 1000) + ' 秒后重拉')
@@ -358,7 +458,7 @@ async function main(argv, deps) {
358
458
  }
359
459
  }
360
460
 
361
- module.exports = { main, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
461
+ module.exports = { main, runOnce, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
362
462
 
363
463
  if (require.main === module) {
364
464
  main(process.argv.slice(2)).then(
package/hook/gate.cjs CHANGED
@@ -64,7 +64,7 @@ function readStdin() {
64
64
  })
65
65
  }
66
66
 
67
- function post(port, token, body) {
67
+ function post(port, token, body, info) {
68
68
  return new Promise((resolve, reject) => {
69
69
  const req = http.request(
70
70
  {
@@ -87,10 +87,75 @@ function post(port, token, body) {
87
87
  }
88
88
  )
89
89
  req.on('error', reject)
90
+ // 连上过(守护进程收下了请求)还是根本没连上:窗口模式据此决定要不要等守护进程重启(问题表 268)
91
+ if (info) req.on('socket', (sock) => (sock.connecting ? sock.once('connect', () => (info.connected = true)) : (info.connected = true)))
90
92
  req.end(body)
91
93
  })
92
94
  }
93
95
 
96
+ /** 守护进程等待中途没了(崩溃 / 被杀),launcher 10 秒后重拉:最多等这么久,等它换了新端口再提交一次 */
97
+ const RESTART_WAIT_MS = 3 * 60 * 1000
98
+ const RESTART_POLL_MS = 2000
99
+
100
+ /** 与 src/daemon/autostart.ts STOP_MARK_FILE / STOP_MARK_TTL_MS 一致:DeskBuddy 自己的停止流程留的标记 */
101
+ const STOP_MARK_FILE = 'daemon.stopping'
102
+ const STOP_MARK_TTL_MS = 2 * 60 * 1000
103
+ function stopMarkFresh(now) {
104
+ try {
105
+ const t = Number(fs.readFileSync(path.join(deskbuddyHome(), STOP_MARK_FILE), 'utf8').trim())
106
+ return Number.isFinite(t) && now - t >= 0 && now - t < STOP_MARK_TTL_MS
107
+ } catch (_) {
108
+ return false
109
+ }
110
+ }
111
+
112
+ function readGateConf() {
113
+ try {
114
+ const c = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'gate.json'), 'utf8'))
115
+ return c && Number.isInteger(c.port) && typeof c.token === 'string' && c.token ? c : null
116
+ } catch (_) {
117
+ return null
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 窗口模式的 POST:守护进程在等设备答复期间没了(连上过、之后连接断开),不放弃 ——
123
+ * 等它重启(gate.json 换了端口 / 令牌)后把同一个授权再提交一次,设备上重新出卡(问题表 268:
124
+ * 崩溃后窗口里那道授权被撤、再也推不到设备)。🔴 一开始就连不上(守护进程根本不在)照旧立刻失败,不卡窗口
125
+ */
126
+ async function postSurviving(conf, body, deps) {
127
+ const d = deps || {}
128
+ const send = d.post || post
129
+ const now = d.now || Date.now
130
+ const sleep = d.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
131
+ const read = d.readConf || readGateConf
132
+ const stopping = d.stopping || stopMarkFresh
133
+ let cur = conf
134
+ let deadline = 0
135
+ for (;;) {
136
+ const info = { connected: false }
137
+ try {
138
+ return await send(cur.port, cur.token, body, info)
139
+ } catch (e) {
140
+ // 连上过又断了 = 守护进程这次没了,重新计时(重提交后又崩一次也能再等);没连上且不在等重启 = 守护进程根本不在
141
+ if (info.connected) deadline = now() + RESTART_WAIT_MS
142
+ else if (!deadline) throw e
143
+ // 等新的守护进程起来:gate.json 换了才再提交(同一个端口还连不上就接着等)
144
+ for (;;) {
145
+ // 🔴 正常停止(disable / 升级 / 未授权退出)不等:停止流程先删 gate.json,Windows 结束进程时留停止标记(评审必修)
146
+ if (now() >= deadline || stopping(now())) throw e
147
+ await sleep(RESTART_POLL_MS)
148
+ const c = read()
149
+ if (!c || stopping(now())) throw e
150
+ if (c.port !== cur.port || c.token !== cur.token) {
151
+ cur = c
152
+ break
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+
94
159
  /**
95
160
  * 这个 hook 是不是由【交互窗口】里的 Claude Code 触发的。
96
161
  * 🔴 实测(2026-09-24,2.1.259):`claude -p` 碰到要授权的工具**也会**触发 PermissionRequest,
@@ -792,7 +857,7 @@ async function windowMain() {
792
857
  ...(lang ? { lang } : {}),
793
858
  ...(tool === 'claude_code' ? { ccAutoMode: ccAutoMode(hook) } : {}),
794
859
  }
795
- const res = await post(conf.port, conf.token, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
860
+ const res = await postSurviving(conf, JSON.stringify({ mode: 'window', tool, hook, ...extra }))
796
861
  if (res.status !== 200) return fail()
797
862
  const out = JSON.parse(res.data)
798
863
  if (!out || (out.decision !== 'allow' && out.decision !== 'deny')) return fail()
@@ -925,5 +990,5 @@ if (require.main === module) {
925
990
  } else {
926
991
  // 只给单测(智能体永远是 `node gate.cjs` 直接跑)。
927
992
  // 🔴 禁止通过 require 调用本关卡:main 不跑,进程会 exit 0 = 放行
928
- module.exports = { preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
993
+ module.exports = { postSurviving, RESTART_WAIT_MS, preToolOutput, PRETOOL_MAX_MS, traeNoticeHook, qoderCliMatches, qoderNodeCliMatches, isInteractiveAgentWin, claudeArgsInteractive, qoderArgsInteractive, isInteractiveQoder, qoderUi, ccAutoMode, CC_POLICY_DOMAIN, CC_REG_KEYS, codebuddyAgent, codebuddyCliArgs, codebuddyAgentWin, winScriptOf, findCodebuddy, codebuddyProduct }
929
994
  }
@@ -380,3 +380,27 @@
380
380
  2026-10-12T11:00:13.614Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
381
381
  2026-10-12T11:00:13.614Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
382
382
  2026-10-12T11:00:15.993Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
383
+ 2026-10-12T11:15:58.702Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
384
+ 2026-10-12T11:16:03.652Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
385
+ 2026-10-12T11:16:03.652Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
386
+ 2026-10-12T11:16:03.752Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
387
+ 2026-10-12T11:16:03.753Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
388
+ 2026-10-12T11:16:06.045Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
389
+ 2026-10-12T11:21:53.902Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
390
+ 2026-10-12T11:21:58.896Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
391
+ 2026-10-12T11:21:58.896Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
392
+ 2026-10-12T11:21:58.995Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
393
+ 2026-10-12T11:21:58.995Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
394
+ 2026-10-12T11:22:01.397Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
395
+ 2026-10-12T11:24:28.256Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
396
+ 2026-10-12T11:24:33.247Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
397
+ 2026-10-12T11:24:33.247Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
398
+ 2026-10-12T11:24:33.342Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
399
+ 2026-10-12T11:24:33.342Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
400
+ 2026-10-12T11:24:35.695Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
401
+ 2026-10-12T11:26:33.586Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
402
+ 2026-10-12T11:26:38.587Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
403
+ 2026-10-12T11:26:38.588Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
404
+ 2026-10-12T11:26:38.709Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
405
+ 2026-10-12T11:26:38.710Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
406
+ 2026-10-12T11:26:41.188Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
package/hook/launcher.cjs CHANGED
@@ -54,6 +54,77 @@ function paths(h) {
54
54
  crashes: path.join(versions, 'crashes.json'),
55
55
  log: path.join(h, 'daemon.log'),
56
56
  pid: path.join(h, 'launcher.pid'),
57
+ stderr: path.join(h, 'daemon.stderr.log'),
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 子进程 stderr 落盘(问题表 268):Windows 上 Node 的致命错误(V8 内存耗尽、libuv 断言、原生模块 abort)
63
+ * 退出码都是 0xC0000409,原因只打在 stderr 上;launcher 是隐藏启动的,stderr 没人接 → 日志里什么都没有。
64
+ * 超过 1MB 先转存成 .1(只留一份)。打不开返回 null(照旧 inherit)
65
+ */
66
+ const STDERR_MAX_BYTES = 1024 * 1024
67
+ function openStderr(h) {
68
+ const p = paths(h).stderr
69
+ try {
70
+ fs.mkdirSync(h, { recursive: true })
71
+ try {
72
+ if (fs.statSync(p).size > STDERR_MAX_BYTES) fs.renameSync(p, p + '.1')
73
+ } catch {
74
+ /* 还没有这个文件 */
75
+ }
76
+ const fd = fs.openSync(p, 'a')
77
+ return { fd, start: fs.fstatSync(fd).size }
78
+ } catch {
79
+ return null
80
+ }
81
+ }
82
+
83
+ const FATAL_LINE_RE = /FATAL ERROR|Fatal error|Assertion failed|Check failed|Uncaught|Error:|Exception|out of memory|abort/i
84
+
85
+ /** 这次运行期间写进 stderr 文件的末尾(最多 1500 字符,去掉空行);没有返回 '' */
86
+ function stderrTail(h, start) {
87
+ try {
88
+ const p = paths(h).stderr
89
+ const size = fs.statSync(p).size
90
+ const from = Math.max(start, size - 16384)
91
+ if (size <= from) return ''
92
+ const buf = Buffer.alloc(size - from)
93
+ const fd = fs.openSync(p, 'r')
94
+ try {
95
+ fs.readSync(fd, buf, 0, buf.length, from)
96
+ } finally {
97
+ fs.closeSync(fd)
98
+ }
99
+ const lines = buf.toString('utf8').split(/\r?\n/).filter((l) => l.trim())
100
+ // 关键行(V8 / libuv / Node 的致命错误头、未捕获异常)放前面:原生调用栈在后面,只留末尾几行
101
+ const key = lines.filter((l) => FATAL_LINE_RE.test(l) && !/^\s*\d+: 0x/.test(l)).slice(0, 4) // 原生调用栈帧(` 1: 0x… node::Abort()`)不算
102
+ const tail = lines.filter((l) => !key.includes(l)).slice(-4)
103
+ return [...key, ...(tail.length ? ['…', ...tail] : [])].join(' | ').slice(0, 1500)
104
+ } catch {
105
+ return ''
106
+ }
107
+ }
108
+
109
+ /** Node 致命错误时写诊断报告(JS / 原生调用栈、堆、句柄)的目录;只留最新 5 份 */
110
+ const REPORT_KEEP = 5
111
+ function reportDir(h) {
112
+ const d = path.join(h, 'reports')
113
+ try {
114
+ fs.mkdirSync(d, { recursive: true })
115
+ const old = fs.readdirSync(d).filter((n) => n.endsWith('.json')).sort()
116
+ for (const n of old.slice(0, Math.max(0, old.length - REPORT_KEEP))) fs.rmSync(path.join(d, n), { force: true })
117
+ } catch {
118
+ /* 清不了不影响启动 */
119
+ }
120
+ return d
121
+ }
122
+
123
+ function newReports(dir, since) {
124
+ try {
125
+ return fs.readdirSync(dir).filter((n) => n.endsWith('.json') && fs.statSync(path.join(dir, n)).mtimeMs >= since)
126
+ } catch {
127
+ return []
57
128
  }
58
129
  }
59
130
 
@@ -255,7 +326,25 @@ const AFTER_STOP_POLL_MS = 1000
255
326
  function runOnce(h, cur, args) {
256
327
  return new Promise((resolve) => {
257
328
  let stopping = false
258
- const child = spawn(process.execPath, [cur.cliPath, ...args], { stdio: 'inherit', windowsHide: true, env: childEnv(process.env) })
329
+ const err = openStderr(h)
330
+ const reports = reportDir(h)
331
+ const startedAt = Date.now()
332
+ let child
333
+ try {
334
+ child = spawn(process.execPath, ['--report-on-fatalerror', '--report-compact', '--report-directory=' + reports, cur.cliPath, ...args], {
335
+ stdio: ['inherit', 'inherit', err ? err.fd : 'inherit'],
336
+ windowsHide: true,
337
+ env: childEnv(process.env),
338
+ })
339
+ } finally {
340
+ if (err) {
341
+ try {
342
+ fs.closeSync(err.fd) // 子进程已经拿到一份
343
+ } catch {
344
+ /* ignore */
345
+ }
346
+ }
347
+ }
259
348
  const forward = (sig) => {
260
349
  stopping = true
261
350
  try {
@@ -269,9 +358,9 @@ function runOnce(h, cur, args) {
269
358
  handlers[sig] = () => forward(sig)
270
359
  process.on(sig, handlers[sig])
271
360
  }
272
- const done = (code, signal, err) => {
361
+ const done = (code, signal, e) => {
273
362
  for (const sig of Object.keys(handlers)) process.removeListener(sig, handlers[sig])
274
- resolve({ code, signal, stopping, err })
363
+ resolve({ code, signal, stopping, err: e, stderr: err ? stderrTail(h, err.start) : '', reports: newReports(reports, startedAt) })
275
364
  }
276
365
  child.on('error', (e) => done(null, null, e))
277
366
  child.on('exit', (code, signal) => done(code, signal, null))
@@ -323,6 +412,10 @@ async function main(argv, deps) {
323
412
  }
324
413
  const startedAt = now()
325
414
  const r = await run(cur)
415
+ // 🔴 退出后重读一次:跑着的时候 daemon 可能已把 verifying 改成 good(连满 3 分钟)。
416
+ // 用启动时的旧副本会把验证通过之后的崩溃也算成「验证期内」、不记退出码(问题表 268 的 4 次「验证期内异常退出」)
417
+ const latest = readCurrent(h)
418
+ const ran = latest && latest.version === cur.version ? latest : cur
326
419
  const abnormal = !r.stopping && (r.err || r.signal || r.code !== 0)
327
420
  if (r.err) log(h, '起不来版本 ' + cur.version + ':' + r.err.message)
328
421
  if (!abnormal) return r.code == null ? 0 : r.code
@@ -340,16 +433,23 @@ async function main(argv, deps) {
340
433
  log(h, '守护进程已退出(DeskBuddy 停止流程),launcher 不再重拉')
341
434
  return 0
342
435
  }
436
+ // 崩溃现场(问题表 268):退出码、stderr 末尾、诊断报告文件名记进 daemon.log
437
+ log(
438
+ h,
439
+ '守护进程退出码 ' + (r.code == null ? r.signal || '?' : r.code) +
440
+ (r.stderr ? ';stderr 末尾:' + r.stderr : ';stderr 没有输出') +
441
+ (r.reports && r.reports.length ? ';诊断报告:reports/' + r.reports.join(', reports/') : '')
442
+ )
343
443
  const killed = terminated(r)
344
444
  // 被外部 SIGTERM 不是这一版的毛病:不计崩溃、不触发回滚
345
- const rolledBack = killed ? false : recordCrash(h, cur, now())
445
+ const rolledBack = killed ? false : recordCrash(h, ran, now())
346
446
  if (killed) log(h, '守护进程被外部结束(SIGTERM,不是 DeskBuddy 的停止流程),交给' + (platform === 'win32' ? ' launcher ' : '开机自启') + '重新拉起')
347
447
  // 非 Windows:交给 launchd / systemd 按「非正常退出」重拉(它们会重新执行本 launcher,读到回滚后的 current.json)
348
448
  if (platform !== 'win32') return r.code == null ? 1 : r.code || 1
349
449
  // Windows:「启动」文件夹只在登录时跑一次,退了没人拉 —— 本进程自己重拉。
350
450
  // 🔴 DeskBuddy 自己的停止流程先结束本 launcher 再结束 daemon(autostart.ts stopPidFileDaemon),并留了标记,不会走到这里
351
451
  let wait = WIN_RESTART_MS
352
- if (cur.state !== 'verifying' && !rolledBack) {
452
+ if (ran.state !== 'verifying' && !rolledBack) {
353
453
  winFails = now() - startedAt >= WIN_STABLE_MS ? 1 : winFails + 1
354
454
  wait = Math.min(WIN_RESTART_MS * Math.pow(2, winFails - 1), WIN_RESTART_MAX_MS)
355
455
  log(h, '守护进程异常退出(退出码 ' + (r.code == null ? r.signal || '?' : r.code) + '),' + Math.round(wait / 1000) + ' 秒后重拉')
@@ -358,7 +458,7 @@ async function main(argv, deps) {
358
458
  }
359
459
  }
360
460
 
361
- module.exports = { main, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
461
+ module.exports = { main, runOnce, childEnv, daemonLockName, lockOwnerAlive, intentionalStop, AFTER_STOP_ENV, SWITCH_ENV_KEYS, pickTarget, recordCrash, rollback, readCurrent, paths, CRASH_WINDOW_MS, CRASH_LIMIT, TERM_EXIT_CODE, UNBOUND_EXIT_CODE, STOP_MARK_FILE, STOP_MARK_TTL_MS, WIN_RESTART_MS, WIN_RESTART_MAX_MS }
362
462
 
363
463
  if (require.main === module) {
364
464
  main(process.argv.slice(2)).then(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskbuddy",
3
- "version": "0.2.0-beta.37",
3
+ "version": "0.2.0-beta.38",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",