deskbuddy 0.2.0-beta.36 → 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);
@@ -13852,6 +13852,7 @@ function removeCodexRemote() {
13852
13852
  import { closeSync as closeSync8, openSync as openSync8, readdirSync as readdirSync9, readSync as readSync7, statSync as statSync11 } from "fs";
13853
13853
  import { dirname as dirname6, join as join22 } from "path";
13854
13854
  var INTERRUPT_TOOLS = ["claude_code", "qoder_cn", "qoder"];
13855
+ var CODEX_INTERRUPT_TOOL = "codex";
13855
13856
  function transcriptHome(tool) {
13856
13857
  if (tool === "claude_code") return claudeHome();
13857
13858
  if (tool === "qoder_cn") return dirname6(qoderCnSettingsPath());
@@ -13974,14 +13975,53 @@ function claudeTranscriptFile(sessionId, cwd, home = claudeHome()) {
13974
13975
  var INTERRUPT_SETTLE_MS = 2e3;
13975
13976
  var INTERRUPT_TOLERANCE_MS = 5e3;
13976
13977
  function shouldSettleInterrupt(st, markerAt, now) {
13977
- if (markerAt === null || !INTERRUPT_TOOLS.includes(st.tool)) return false;
13978
+ const codex = st.tool === CODEX_INTERRUPT_TOOL;
13979
+ if (markerAt === null || !INTERRUPT_TOOLS.includes(st.tool) && !codex) return false;
13978
13980
  if (st.state !== "watch" && st.state !== "ask_approval") return false;
13979
- if (st.interactive !== true && st.embedded !== true) return false;
13981
+ if (!codex && st.interactive !== true && st.embedded !== true) return false;
13980
13982
  if (now - markerAt < INTERRUPT_SETTLE_MS) return false;
13981
13983
  if (st.updatedAt > markerAt + INTERRUPT_TOLERANCE_MS) return false;
13982
13984
  if ((st.recent ?? []).some((r) => r.e === "UserPromptSubmit" && r.at > markerAt)) return false;
13983
13985
  return true;
13984
13986
  }
13987
+ function codexInterruptedAt(file) {
13988
+ let fd;
13989
+ try {
13990
+ const st = statSync11(file);
13991
+ const len = Math.min(st.size, TAIL);
13992
+ const buf = Buffer.alloc(len);
13993
+ fd = openSync8(file, "r");
13994
+ readSync7(fd, buf, 0, len, st.size - len);
13995
+ const lines3 = buf.toString("utf8").split("\n");
13996
+ for (let i = lines3.length - 1; i >= 0; i--) {
13997
+ const line = lines3[i];
13998
+ if (line.indexOf('"event_msg"') === -1) continue;
13999
+ let o;
14000
+ try {
14001
+ o = JSON.parse(line);
14002
+ } catch {
14003
+ continue;
14004
+ }
14005
+ if (!o || o.type !== "event_msg" || !o.payload) continue;
14006
+ const t = o.payload.type;
14007
+ if (t === "turn_aborted") {
14008
+ const at = typeof o.timestamp === "string" ? Date.parse(o.timestamp) : NaN;
14009
+ return Number.isFinite(at) ? at : st.mtimeMs;
14010
+ }
14011
+ if (t === "task_complete" || t === "turn_complete" || t === "task_started" || t === "turn_started" || t === "user_message") return null;
14012
+ }
14013
+ return null;
14014
+ } catch {
14015
+ return null;
14016
+ } finally {
14017
+ if (fd !== void 0) {
14018
+ try {
14019
+ closeSync8(fd);
14020
+ } catch {
14021
+ }
14022
+ }
14023
+ }
14024
+ }
13985
14025
 
13986
14026
  // src/daemon/qoderAppDb.ts
13987
14027
  import { execFile as execFile4 } from "child_process";
@@ -14246,9 +14286,37 @@ function describe2(r) {
14246
14286
  return [r.status, r.n !== void 0 ? `n=${r.n}` : "", r.detail ?? ""].filter(Boolean).join(" ");
14247
14287
  }
14248
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
+
14249
14317
  // src/daemon/dshVersion.ts
14250
14318
  import { readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
14251
- import { dirname as dirname8, join as join24 } from "path";
14319
+ import { dirname as dirname8, join as join25 } from "path";
14252
14320
  var DSH_HEADLESS_MIN_VERSION = "0.0.1-rc.1";
14253
14321
  var DSH_PKG = "@deepseek-ai/dsh";
14254
14322
  var DSH_VERSION_UNSUPPORTED = "tool_version_unsupported";
@@ -14280,7 +14348,7 @@ function dshSupport(d = defaultDeps2) {
14280
14348
  }
14281
14349
  let dir2 = dirname8(real2);
14282
14350
  for (let i = 0; i < 4; i++) {
14283
- const raw = d.readFile(join24(dir2, "package.json"));
14351
+ const raw = d.readFile(join25(dir2, "package.json"));
14284
14352
  if (raw !== null) {
14285
14353
  let pkg = {};
14286
14354
  try {
@@ -14798,17 +14866,17 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
14798
14866
 
14799
14867
  // src/core/appended.ts
14800
14868
  import { chmodSync as chmodSync4, mkdirSync as mkdirSync11, readFileSync as readFileSync12, renameSync as renameSync8, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
14801
- import { join as join25 } from "path";
14869
+ import { join as join26 } from "path";
14802
14870
  var APPENDED_MAX_ITEMS = 5;
14803
14871
  var APPENDED_TEXT_MAX = 500;
14804
14872
  var APPENDED_RESULT_MAX = 800;
14805
14873
  var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14806
14874
  function appendedDir() {
14807
- return join25(deskbuddyHome(), "appended");
14875
+ return join26(deskbuddyHome(), "appended");
14808
14876
  }
14809
14877
  function appendedPath(sessionId) {
14810
14878
  const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
14811
- return safe2 ? join25(appendedDir(), `${safe2}.json`) : null;
14879
+ return safe2 ? join26(appendedDir(), `${safe2}.json`) : null;
14812
14880
  }
14813
14881
  function readAppended(sessionId) {
14814
14882
  const file = appendedPath(sessionId);
@@ -14838,13 +14906,13 @@ function writeAppended(sessionId, item) {
14838
14906
  var RUNNING_TTL_MS = 60 * 60 * 1e3;
14839
14907
  function runningPath(sessionId) {
14840
14908
  const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
14841
- return safe2 ? join25(deskbuddyHome(), "running", `${safe2}.json`) : null;
14909
+ return safe2 ? join26(deskbuddyHome(), "running", `${safe2}.json`) : null;
14842
14910
  }
14843
14911
  function writeRunning(sessionId, now = Date.now()) {
14844
14912
  const file = runningPath(sessionId);
14845
14913
  if (!file) return;
14846
14914
  try {
14847
- mkdirSync11(join25(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
14915
+ mkdirSync11(join26(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
14848
14916
  writeFileSync10(`${file}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
14849
14917
  renameSync8(`${file}.tmp`, file);
14850
14918
  } catch {
@@ -14892,7 +14960,7 @@ function resultSummary(tool, stdout6) {
14892
14960
 
14893
14961
  // src/core/dshMailbox.ts
14894
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";
14895
- import { join as join26 } from "path";
14963
+ import { join as join27 } from "path";
14896
14964
  var DSH_HEARTBEAT_FRESH_MS = 5 * 1e3;
14897
14965
  var DSH_ACK_TIMEOUT_MS = 8 * 1e3;
14898
14966
  var ACK_POLL_MS = 200;
@@ -14903,10 +14971,10 @@ var REVOKE_RETRY_MS = 50;
14903
14971
  var SID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
14904
14972
  var IID_RE = /^[A-Za-z0-9_-]{1,128}$/;
14905
14973
  function dshMailboxDir(home = deskbuddyHome(), channel = "dsh") {
14906
- return join26(home, channel);
14974
+ return join27(home, channel);
14907
14975
  }
14908
14976
  function sub(home, name, channel = "dsh") {
14909
- return join26(dshMailboxDir(home, channel), name);
14977
+ return join27(dshMailboxDir(home, channel), name);
14910
14978
  }
14911
14979
  function ensureDir2(p) {
14912
14980
  mkdirSync12(p, { recursive: true, mode: 448 });
@@ -14946,7 +15014,7 @@ function liveHeartbeats(sessionId, d0 = {}) {
14946
15014
  if (!/^\d+\.json$/.test(n)) continue;
14947
15015
  let hb;
14948
15016
  try {
14949
- hb = JSON.parse(readFileSync13(join26(dir2, n), "utf8"));
15017
+ hb = JSON.parse(readFileSync13(join27(dir2, n), "utf8"));
14950
15018
  } catch {
14951
15019
  continue;
14952
15020
  }
@@ -14970,8 +15038,8 @@ async function dshDeliver(sessionId, msg, d0 = {}) {
14970
15038
  const ackDir = sub(d.home, "ack", d.channel);
14971
15039
  ensureDir2(inbox);
14972
15040
  ensureDir2(ackDir);
14973
- const letter = join26(inbox, `${sessionId}.${msg.instructionId}.json`);
14974
- const ackPath = join26(ackDir, `${msg.instructionId}.json`);
15041
+ const letter = join27(inbox, `${sessionId}.${msg.instructionId}.json`);
15042
+ const ackPath = join27(ackDir, `${msg.instructionId}.json`);
14975
15043
  if (existsSync6(letter)) return { ok: false, reason: "inbox_busy" };
14976
15044
  const writtenAt = d.now();
14977
15045
  writeJson(letter, {
@@ -15040,7 +15108,7 @@ function dshClearInbox(home = deskbuddyHome(), channel = "dsh") {
15040
15108
  var DSH_REPLY_WAIT_MS = 4 * 60 * 1e3;
15041
15109
  function takeMailboxReply(instructionId, home = deskbuddyHome(), channel = "dsh") {
15042
15110
  if (!IID_RE.test(instructionId)) return null;
15043
- const p = join26(sub(home, "reply", channel), `${instructionId}.json`);
15111
+ const p = join27(sub(home, "reply", channel), `${instructionId}.json`);
15044
15112
  let v = null;
15045
15113
  try {
15046
15114
  v = JSON.parse(readFileSync13(p, "utf8"));
@@ -15069,7 +15137,7 @@ function clearDir(inbox) {
15069
15137
  let n = 0;
15070
15138
  for (const f of names) {
15071
15139
  try {
15072
- rmSync10(join26(inbox, f), { force: true });
15140
+ rmSync10(join27(inbox, f), { force: true });
15073
15141
  n++;
15074
15142
  } catch {
15075
15143
  }
@@ -15144,7 +15212,7 @@ function defaultCodexAppServers() {
15144
15212
  }
15145
15213
  function opencodeReady() {
15146
15214
  const mod = ocPluginModule(installedHookPath());
15147
- return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(join27(dirname9(mod), "ocTranslate.mjs"));
15215
+ return isOcPluginInstalled(opencodePluginPath()) && existsSync7(mod) && existsSync7(join28(dirname9(mod), "ocTranslate.mjs"));
15148
15216
  }
15149
15217
  var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
15150
15218
  function orphanResumeText(summary2) {
@@ -15211,6 +15279,7 @@ var Daemon = class {
15211
15279
  #ws = null;
15212
15280
  #idleTimer = null;
15213
15281
  #windowScan = null;
15282
+ #memLog = null;
15214
15283
  #aliveReport = null;
15215
15284
  #reportRetryTimer = null;
15216
15285
  #reportRetry = new ReportRetry({
@@ -15564,6 +15633,10 @@ var Daemon = class {
15564
15633
  async startGate() {
15565
15634
  const port = await this.#gate.start();
15566
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();
15567
15640
  this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
15568
15641
  this.#windowScan.unref();
15569
15642
  this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
@@ -15752,7 +15825,7 @@ var Daemon = class {
15752
15825
  #settleInterrupted() {
15753
15826
  if (this.#stopped) return;
15754
15827
  const enabled = loadConfig().enabledTools;
15755
- const tools = INTERRUPT_TOOLS.filter((t) => enabled.includes(t));
15828
+ const tools = [...INTERRUPT_TOOLS, CODEX_INTERRUPT_TOOL].filter((t) => enabled.includes(t));
15756
15829
  if (tools.length === 0) return;
15757
15830
  const now = this.#deps.now();
15758
15831
  const alive = /* @__PURE__ */ new Set();
@@ -15760,7 +15833,7 @@ var Daemon = class {
15760
15833
  let changed = false;
15761
15834
  for (const st of this.#aliveStates(tool)) {
15762
15835
  alive.add(st.sessionId);
15763
- if (!BUSY_STATES3.has(st.state) || st.interactive !== true && st.embedded !== true) {
15836
+ if (!BUSY_STATES3.has(st.state) || tool !== CODEX_INTERRUPT_TOOL && st.interactive !== true && st.embedded !== true) {
15764
15837
  this.#transcriptSeen.delete(st.sessionId);
15765
15838
  continue;
15766
15839
  }
@@ -15772,16 +15845,16 @@ var Daemon = class {
15772
15845
  if (!file) continue;
15773
15846
  const sig = transcriptSig(file);
15774
15847
  if (sig && this.#transcriptSeen.get(st.sessionId) === sig) continue;
15775
- const at = interruptedAt(file);
15848
+ const at = tool === CODEX_INTERRUPT_TOOL ? codexInterruptedAt(file) : interruptedAt(file);
15776
15849
  if (sig && (at === null || now - at >= INTERRUPT_SETTLE_MS)) this.#transcriptSeen.set(st.sessionId, sig);
15777
15850
  if (at === null || !shouldSettleInterrupt(st, at, now)) continue;
15778
- if (this.#settleBusy(st.sessionId, tool, st.updatedAt, `\u5728\u7A97\u53E3\u91CC\u88AB\u6253\u65AD\uFF08${tool} \u4E0D\u53D1 Stop\uFF09`)) {
15851
+ if (this.#settleBusy(st.sessionId, tool, st.updatedAt, `${tool === CODEX_INTERRUPT_TOOL ? "\u88AB\u7528\u6237\u505C\u6B62" : "\u5728\u7A97\u53E3\u91CC\u88AB\u6253\u65AD"}\uFF08${tool} \u4E0D\u53D1 Stop\uFF09`)) {
15779
15852
  changed = true;
15780
15853
  this.#idleWindowAfterInterrupt(st.sessionId, tool, at);
15781
15854
  this.#coord.settleInterruptedWindows(st.sessionId, at);
15782
15855
  }
15783
15856
  }
15784
- if (changed) void this.#reportTool(tool);
15857
+ if (changed) void (tool === CODEX_INTERRUPT_TOOL ? this.#reportCodex() : this.#reportTool(tool));
15785
15858
  }
15786
15859
  for (const m of [this.#transcripts, this.#transcriptMiss, this.#transcriptSeen]) for (const k of [...m.keys()]) if (!alive.has(k)) m.delete(k);
15787
15860
  }
@@ -15802,7 +15875,7 @@ var Daemon = class {
15802
15875
  const missAt = this.#transcriptMiss.get(st.sessionId);
15803
15876
  if (missAt !== void 0 && now - missAt < TRANSCRIPT_MISS_TTL_MS) return null;
15804
15877
  const home = transcriptHome(st.tool);
15805
- const f = home ? claudeTranscriptFile(st.sessionId, st.cwd, home) : null;
15878
+ const f = st.tool === CODEX_INTERRUPT_TOOL ? findCodexRollout(codexHome(), st.sessionId, 7) : home ? claudeTranscriptFile(st.sessionId, st.cwd, home) : null;
15806
15879
  if (f) {
15807
15880
  this.#transcripts.set(st.sessionId, f);
15808
15881
  this.#transcriptMiss.delete(st.sessionId);
@@ -15962,6 +16035,7 @@ var Daemon = class {
15962
16035
  this.#clearIdle();
15963
16036
  if (this.#windowScan) clearInterval(this.#windowScan);
15964
16037
  if (this.#aliveReport) clearInterval(this.#aliveReport);
16038
+ if (this.#memLog) clearInterval(this.#memLog);
15965
16039
  if (this.#reportRetryTimer) clearInterval(this.#reportRetryTimer);
15966
16040
  if (this.#codexTimer) clearInterval(this.#codexTimer);
15967
16041
  if (this.#wakeTimer) clearInterval(this.#wakeTimer);
@@ -16542,8 +16616,8 @@ var Daemon = class {
16542
16616
  }
16543
16617
  const sid = resolveAlias(cmd.sessionId);
16544
16618
  for (let p = this.#opening.get(sid); p; p = this.#opening.get(sid)) await p;
16545
- let release;
16546
- this.#opening.set(sid, new Promise((r) => release = r));
16619
+ let release2;
16620
+ this.#opening.set(sid, new Promise((r) => release2 = r));
16547
16621
  try {
16548
16622
  if (this.#queue.busy(sid)) await this.#queue.enqueue(sid, async () => {
16549
16623
  });
@@ -16565,7 +16639,7 @@ var Daemon = class {
16565
16639
  await this.#openWindowFor(cmd, sid, r.cwd);
16566
16640
  } finally {
16567
16641
  this.#opening.delete(sid);
16568
- release();
16642
+ release2();
16569
16643
  }
16570
16644
  }
16571
16645
  #guiAvailable() {
@@ -17004,7 +17078,7 @@ var Daemon = class {
17004
17078
  for (const n of names) {
17005
17079
  if (!n.endsWith(".json")) continue;
17006
17080
  try {
17007
- const s2 = JSON.parse(readFileSync14(join27(stateDir(), n), "utf8"));
17081
+ const s2 = JSON.parse(readFileSync14(join28(stateDir(), n), "utf8"));
17008
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 : "" });
17009
17083
  } catch {
17010
17084
  }
@@ -17038,12 +17112,12 @@ var Daemon = class {
17038
17112
  }
17039
17113
  case "deepseek_harness": {
17040
17114
  if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
17041
- const p = join27(deskbuddyHome(), "dsh", "transcript", `${s2.sessionId}.jsonl`);
17115
+ const p = join28(deskbuddyHome(), "dsh", "transcript", `${s2.sessionId}.jsonl`);
17042
17116
  return existsSync7(p) ? { kind: "file", path: p, format: "dsh" } : null;
17043
17117
  }
17044
17118
  case "opencode": {
17045
17119
  if (!TRANSCRIPT_SID_RE.test(s2.sessionId)) return null;
17046
- 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`));
17047
17121
  }
17048
17122
  case "dumate":
17049
17123
  return { kind: "http", key: "dumate", fetch: () => this.#dumate.transcriptMessages(s2.sessionId) };
@@ -17991,7 +18065,7 @@ async function runDisable(tools, opts = {}) {
17991
18065
 
17992
18066
  // src/commands/install.ts
17993
18067
  import { mkdirSync as mkdirSync13, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
17994
- import { join as join28 } from "path";
18068
+ import { join as join29 } from "path";
17995
18069
 
17996
18070
  // src/commands/setup.ts
17997
18071
  import qrcode from "qrcode-terminal";
@@ -18208,7 +18282,7 @@ function inSandbox(env) {
18208
18282
  }
18209
18283
  function precheckWritable(dirs) {
18210
18284
  for (const dir2 of dirs) {
18211
- const probe = join28(dir2, `.deskbuddy-probe-${process.pid}`);
18285
+ const probe = join29(dir2, `.deskbuddy-probe-${process.pid}`);
18212
18286
  try {
18213
18287
  mkdirSync13(dir2, { recursive: true });
18214
18288
  writeFileSync13(probe, "");
@@ -18618,7 +18692,7 @@ async function runAutoSwitch(opts) {
18618
18692
  // src/wrap/run.ts
18619
18693
  import { randomBytes as randomBytes10 } from "crypto";
18620
18694
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync15, statSync as statSync16, truncateSync as truncateSync3 } from "fs";
18621
- import { join as join31 } from "path";
18695
+ import { join as join32 } from "path";
18622
18696
  import { StringDecoder as StringDecoder2 } from "string_decoder";
18623
18697
 
18624
18698
  // src/wrap/injector.ts
@@ -19118,7 +19192,7 @@ import { execFileSync as execFileSync4, spawn as spawn4 } from "child_process";
19118
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";
19119
19193
  import { request as request2 } from "http";
19120
19194
  import { createServer as createServer5 } from "net";
19121
- import { join as join29 } from "path";
19195
+ import { join as join30 } from "path";
19122
19196
  var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
19123
19197
  var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
19124
19198
  var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
@@ -19287,10 +19361,10 @@ async function runCodexWindow(real2, args, d) {
19287
19361
  }
19288
19362
  var MAX_LOG_BYTES2 = 1024 * 1024;
19289
19363
  function appServerLogPath() {
19290
- return join29(deskbuddyHome(), "codex-app-server.log");
19364
+ return join30(deskbuddyHome(), "codex-app-server.log");
19291
19365
  }
19292
19366
  function windowsDir2() {
19293
- return join29(deskbuddyHome(), "codex-windows");
19367
+ return join30(deskbuddyHome(), "codex-windows");
19294
19368
  }
19295
19369
  function pidAlive5(pid) {
19296
19370
  try {
@@ -19380,7 +19454,7 @@ function killOwnServer(pid, url, args = freshArgs, kill = (p) => killServer(p))
19380
19454
  if (a && isAppServerArgs(a, url)) kill(pid);
19381
19455
  }
19382
19456
  function recordPath(rec) {
19383
- return join29(windowsDir2(), `${rec.server}.json`);
19457
+ return join30(windowsDir2(), `${rec.server}.json`);
19384
19458
  }
19385
19459
  function writeRecord(rec) {
19386
19460
  try {
@@ -19405,7 +19479,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
19405
19479
  return;
19406
19480
  }
19407
19481
  for (const n of names) {
19408
- const p = join29(dir2, n);
19482
+ const p = join30(dir2, n);
19409
19483
  let rec;
19410
19484
  try {
19411
19485
  rec = JSON.parse(readFileSync16(p, "utf8"));
@@ -19428,7 +19502,7 @@ function reapWindows(dir2 = windowsDir2(), alive = pidAlive5, args = freshArgs,
19428
19502
  function launchLog(line) {
19429
19503
  try {
19430
19504
  mkdirSync14(deskbuddyHome(), { recursive: true });
19431
- 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}
19432
19506
  `, { flag: "a" });
19433
19507
  } catch {
19434
19508
  }
@@ -19453,7 +19527,7 @@ function defaultCodexWindowDeps(base) {
19453
19527
  // src/wrap/promptLaunch.ts
19454
19528
  import { randomBytes as randomBytes8, randomUUID as randomUUID3 } from "crypto";
19455
19529
  import { homedir as homedir10 } from "os";
19456
- import { join as join30 } from "path";
19530
+ import { join as join31 } from "path";
19457
19531
  var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
19458
19532
  function promptArgs(text, tool = "") {
19459
19533
  if (!text) return [];
@@ -19463,7 +19537,7 @@ function promptArgs(text, tool = "") {
19463
19537
  function promptEnv(tool, env, launcherPid = process.pid, answerWindowId = "") {
19464
19538
  const out = { ...env };
19465
19539
  if (tool === "workbuddy") {
19466
- 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");
19467
19541
  out.DESKBUDDY_LAUNCHER_PID = String(launcherPid);
19468
19542
  out.DESKBUDDY_LAUNCHED_TOOL = tool;
19469
19543
  if (answerWindowId) out.DESKBUDDY_LAUNCHED_WINDOW = answerWindowId;
@@ -20047,7 +20121,7 @@ function childEnv(src, windowId, takeover = true) {
20047
20121
  }
20048
20122
  function wrapLog(line) {
20049
20123
  try {
20050
- const p = join31(deskbuddyHome(), "wrap.log");
20124
+ const p = join32(deskbuddyHome(), "wrap.log");
20051
20125
  mkdirSync15(deskbuddyHome(), { recursive: true });
20052
20126
  try {
20053
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
  }
@@ -362,3 +362,45 @@
362
362
  2026-10-12T09:01:12.293Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
363
363
  2026-10-12T09:01:12.293Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
364
364
  2026-10-12T09:01:14.613Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
365
+ 2026-10-12T10:52:32.632Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
366
+ 2026-10-12T10:52:37.625Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
367
+ 2026-10-12T10:52:37.625Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
368
+ 2026-10-12T10:52:37.718Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
369
+ 2026-10-12T10:52:37.719Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
370
+ 2026-10-12T10:52:40.546Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
371
+ 2026-10-12T10:56:52.376Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
372
+ 2026-10-12T10:56:57.350Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
373
+ 2026-10-12T10:56:57.350Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
374
+ 2026-10-12T10:56:57.454Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
375
+ 2026-10-12T10:56:57.454Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
376
+ 2026-10-12T10:56:59.731Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
377
+ 2026-10-12T11:00:08.567Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
378
+ 2026-10-12T11:00:13.510Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
379
+ 2026-10-12T11:00:13.510Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
380
+ 2026-10-12T11:00:13.614Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
381
+ 2026-10-12T11:00:13.614Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
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.36",
3
+ "version": "0.2.0-beta.38",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",