dsh-lark-bot 0.19.10 → 0.19.12

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
@@ -296,6 +296,16 @@ var init_profile_package = __esm({
296
296
  }
297
297
  });
298
298
 
299
+ // src/adapters/model-choice.ts
300
+ function resolveModelChoice(preferenceModel, envModel) {
301
+ return preferenceModel?.trim() || envModel;
302
+ }
303
+ var init_model_choice = __esm({
304
+ "src/adapters/model-choice.ts"() {
305
+ "use strict";
306
+ }
307
+ });
308
+
299
309
  // src/adapters/dsh/acp-runtime.ts
300
310
  import { spawn as spawn4 } from "cross-spawn";
301
311
  import { existsSync as existsSync4, realpathSync as realpathSync3 } from "fs";
@@ -666,7 +676,7 @@ async function buildAcpAgentAdapter(env, preferences) {
666
676
  home: homedir2(),
667
677
  env: process.env,
668
678
  provider: env.provider,
669
- model: preferences.model ?? env.model,
679
+ model: resolveModelChoice(preferences.model, env.model),
670
680
  ...env.dshExplicit ? { command: env.dshCommand, args: env.dshArgs } : {}
671
681
  };
672
682
  const ensure = await ensureAcpProfile(runtimeOptions);
@@ -679,7 +689,7 @@ async function buildAcpAgentAdapter(env, preferences) {
679
689
  return new AcpDshAdapter({
680
690
  launch,
681
691
  provider: env.provider,
682
- model: preferences.model ?? env.model
692
+ model: resolveModelChoice(preferences.model, env.model)
683
693
  });
684
694
  }
685
695
  var AcpDshAdapter;
@@ -688,6 +698,7 @@ var init_acp_adapter = __esm({
688
698
  "use strict";
689
699
  init_acp_runtime();
690
700
  init_event_channel();
701
+ init_model_choice();
691
702
  AcpDshAdapter = class {
692
703
  id = "dsh-acp";
693
704
  displayName = "DeepSeek Harness (ACP)";
@@ -972,12 +983,18 @@ async function listProcesses(platform = process.platform, run = captureOutput) {
972
983
  return listProcessesPosix(run);
973
984
  }
974
985
  async function listProcessesPosix(run) {
975
- const { stdout } = await run("ps", ["-axo", "pid=,args="], 1e4);
986
+ const { stdout } = await run("ps", ["-axo", "pid=,ppid=,args="], 1e4);
976
987
  const result = [];
977
988
  for (const line of stdout.split("\n")) {
978
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
979
- if (match) {
980
- result.push({ pid: Number(match[1]), cmdline: match[2] ?? "" });
989
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
990
+ if (!match) continue;
991
+ const pid = Number(match[1]);
992
+ const ppid = Number(match[2]);
993
+ if (!Number.isInteger(pid) || pid <= 0) continue;
994
+ if (Number.isInteger(ppid) && ppid > 0) {
995
+ result.push({ pid, ppid, cmdline: match[3] ?? "" });
996
+ } else {
997
+ result.push({ pid, cmdline: match[3] ?? "" });
981
998
  }
982
999
  }
983
1000
  return result;
@@ -989,7 +1006,7 @@ async function listProcessesWindows(run) {
989
1006
  "-NoProfile",
990
1007
  "-NonInteractive",
991
1008
  "-Command",
992
- "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"
1009
+ "Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, CommandLine | ConvertTo-Json -Compress"
993
1010
  ],
994
1011
  1e4
995
1012
  );
@@ -1000,8 +1017,11 @@ async function listProcessesWindows(run) {
1000
1017
  if (typeof row !== "object" || row === null) return [];
1001
1018
  const record = row;
1002
1019
  const pid = Number(record.ProcessId);
1020
+ const ppid = Number(record.ParentProcessId);
1003
1021
  const cmdline = record.CommandLine;
1004
- return Number.isInteger(pid) && pid > 0 && typeof cmdline === "string" ? [{ pid, cmdline }] : [];
1022
+ if (!Number.isInteger(pid) || pid <= 0 || typeof cmdline !== "string") return [];
1023
+ if (Number.isInteger(ppid) && ppid > 0) return [{ pid, ppid, cmdline }];
1024
+ return [{ pid, cmdline }];
1005
1025
  });
1006
1026
  } catch {
1007
1027
  return [];
@@ -1703,7 +1723,7 @@ var DshProviderManager = class {
1703
1723
  try {
1704
1724
  catalogProviders = await this.catalog.listProviders();
1705
1725
  } catch (error) {
1706
- log.warn("model-catalog", "refresh-failed", {
1726
+ log.info("model-catalog", "refresh-failed", {
1707
1727
  error: error instanceof Error ? error.message : String(error)
1708
1728
  });
1709
1729
  }
@@ -3442,6 +3462,7 @@ var WebDshAdapter = class {
3442
3462
  };
3443
3463
 
3444
3464
  // src/adapters/index.ts
3465
+ init_model_choice();
3445
3466
  async function resolveAdapterRoute(input, source) {
3446
3467
  const provider = input.provider?.trim() || void 0;
3447
3468
  const model = input.model?.trim() || void 0;
@@ -3461,7 +3482,7 @@ async function resolveAdapterRoute(input, source) {
3461
3482
  return void 0;
3462
3483
  }
3463
3484
  async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model: void 0 }) {
3464
- const configuredModel = preferences.model ?? env.model;
3485
+ const configuredModel = resolveModelChoice(preferences.model, env.model);
3465
3486
  const managedRoute = env.adapterMode === "sdk" || env.adapterMode === "acp" ? await resolveAdapterRoute(
3466
3487
  { provider: env.provider, model: configuredModel },
3467
3488
  new DshProviderManager({ env: process.env })
@@ -3493,7 +3514,7 @@ async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model
3493
3514
  return new WebDshAdapter({
3494
3515
  baseUrl: env.webBaseUrl,
3495
3516
  provider: env.provider,
3496
- model: preferences.model ?? env.model
3517
+ model: resolveModelChoice(preferences.model, env.model)
3497
3518
  });
3498
3519
  }
3499
3520
  case "sdk":
@@ -3700,6 +3721,10 @@ var DEFAULTS = {
3700
3721
  guardianBridgeProfile: "default",
3701
3722
  upgradeNotify: false,
3702
3723
  upgradeCheckIntervalMs: 6 * 60 * 6e4,
3724
+ channelPingTimeoutSec: 30,
3725
+ channelKeepalive: true,
3726
+ channelKeepaliveMs: 15e3,
3727
+ channelHealthPollMs: 5e3,
3703
3728
  sessionBackfillMessages: 20,
3704
3729
  sessionBackfillBytes: 64 * 1024,
3705
3730
  sessionStreamUpdateMs: 800
@@ -3930,6 +3955,22 @@ function loadRuntimeEnv(source = process.env) {
3930
3955
  source.DSH_LARK_GUARDIAN_ENGINE_DEAD_MS,
3931
3956
  DEFAULTS.guardianEngineDeadMs,
3932
3957
  "DSH_LARK_GUARDIAN_ENGINE_DEAD_MS"
3958
+ ),
3959
+ channelPingTimeoutSec: parsePositiveIntMin(
3960
+ source.DSH_LARK_CHANNEL_PING_TIMEOUT_SEC,
3961
+ DEFAULTS.channelPingTimeoutSec,
3962
+ "DSH_LARK_CHANNEL_PING_TIMEOUT_SEC"
3963
+ ),
3964
+ channelKeepalive: parseBoolean(source.DSH_LARK_CHANNEL_KEEPALIVE, DEFAULTS.channelKeepalive),
3965
+ channelKeepaliveMs: parsePositiveIntMin(
3966
+ source.DSH_LARK_CHANNEL_KEEPALIVE_MS,
3967
+ DEFAULTS.channelKeepaliveMs,
3968
+ "DSH_LARK_CHANNEL_KEEPALIVE_MS"
3969
+ ),
3970
+ channelHealthPollMs: parsePositiveIntMin(
3971
+ source.DSH_LARK_CHANNEL_HEALTH_POLL_MS,
3972
+ DEFAULTS.channelHealthPollMs,
3973
+ "DSH_LARK_CHANNEL_HEALTH_POLL_MS"
3933
3974
  )
3934
3975
  };
3935
3976
  }
@@ -4169,17 +4210,36 @@ var ConfigStore = class {
4169
4210
  };
4170
4211
 
4171
4212
  // src/guardian/install.ts
4172
- init_own_package();
4173
- init_dsh_runtime();
4174
- init_process();
4175
4213
  import { mkdir as mkdir8, readFile as readFile8, rm as rm3, writeFile as writeFile5 } from "fs/promises";
4176
4214
  import { existsSync as existsSync5 } from "fs";
4177
4215
  import { homedir as homedir6 } from "os";
4178
- import { delimiter, dirname as dirname9, join as join11 } from "path";
4216
+ import { dirname as dirname10, join as join11 } from "path";
4217
+
4218
+ // src/platform/path.ts
4219
+ import { delimiter, dirname as dirname8 } from "path";
4220
+ function sanitizeServicePath(nodeBin, inheritedPath = process.env.PATH) {
4221
+ const entries = [dirname8(nodeBin), ...inheritedPath?.split(delimiter) ?? []];
4222
+ const seen = /* @__PURE__ */ new Set();
4223
+ return entries.filter((entry) => {
4224
+ if (!entry) return false;
4225
+ const normalized = entry.replaceAll("\\", "/");
4226
+ if (normalized.includes("/node_modules/.bin") || /\/_npx(?:\/|$)/.test(normalized) || /\/guardian\/update-worker\/npm-cache(?:\/|$)/.test(normalized)) {
4227
+ return false;
4228
+ }
4229
+ if (seen.has(entry)) return false;
4230
+ seen.add(entry);
4231
+ return true;
4232
+ }).join(delimiter);
4233
+ }
4234
+
4235
+ // src/guardian/install.ts
4236
+ init_own_package();
4237
+ init_dsh_runtime();
4238
+ init_process();
4179
4239
 
4180
4240
  // src/guardian/state.ts
4181
4241
  import { readFile as readFile7 } from "fs/promises";
4182
- import { dirname as dirname8 } from "path";
4242
+ import { dirname as dirname9 } from "path";
4183
4243
  import { mkdir as mkdir7 } from "fs/promises";
4184
4244
  var DEFAULT_DSH_PROFILE = "dsh-lark";
4185
4245
  var DEFAULT_BRIDGE_PROFILE = "default";
@@ -4221,7 +4281,7 @@ async function loadGuardianState(file, fallback) {
4221
4281
  }
4222
4282
  async function saveGuardianState(file, state) {
4223
4283
  const next = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
4224
- await mkdir7(dirname8(file), { recursive: true });
4284
+ await mkdir7(dirname9(file), { recursive: true });
4225
4285
  await writeFileAtomic(file, `${JSON.stringify(next, null, 2)}
4226
4286
  `, { mode: 384 });
4227
4287
  }
@@ -4275,7 +4335,7 @@ function guardianServiceFilePath(platform, root) {
4275
4335
  }
4276
4336
  function systemdUnit(nodeBin, cliEntry, env = {}) {
4277
4337
  const renderedEnv = {
4278
- PATH: env.PATH ?? dirname9(nodeBin),
4338
+ PATH: env.PATH ?? dirname10(nodeBin),
4279
4339
  ...env
4280
4340
  };
4281
4341
  const envLines = Object.entries(renderedEnv).map(([key, value]) => `Environment=${key}=${value}`).join("\n");
@@ -4298,18 +4358,7 @@ function systemdUnit(nodeBin, cliEntry, env = {}) {
4298
4358
  ].join("\n");
4299
4359
  }
4300
4360
  function stableGuardianServicePath(nodeBin, inheritedPath = process.env.PATH) {
4301
- const entries = [dirname9(nodeBin), ...inheritedPath?.split(delimiter) ?? []];
4302
- const seen = /* @__PURE__ */ new Set();
4303
- return entries.filter((entry) => {
4304
- if (!entry) return false;
4305
- const normalized = entry.replaceAll("\\", "/");
4306
- if (normalized.includes("/node_modules/.bin") || /\/_npx(?:\/|$)/.test(normalized) || /\/guardian\/update-worker\/npm-cache(?:\/|$)/.test(normalized)) {
4307
- return false;
4308
- }
4309
- if (seen.has(entry)) return false;
4310
- seen.add(entry);
4311
- return true;
4312
- }).join(delimiter);
4361
+ return sanitizeServicePath(nodeBin, inheritedPath);
4313
4362
  }
4314
4363
  function launchdPlist(nodeBin, cliEntry, label2 = `io.dsh-lark.${GUARDIAN_LABEL}`, logPath = "/tmp/dsh-lark-guardian.log") {
4315
4364
  return [
@@ -4477,15 +4526,6 @@ function guardianStatePath(root) {
4477
4526
  return join11(root, ".dsh-lark", "guardian.json");
4478
4527
  }
4479
4528
 
4480
- // src/upgrade/detect.ts
4481
- init_own_package();
4482
- import { readFile as readFile10 } from "fs/promises";
4483
- import { join as join12 } from "path";
4484
- import { homedir as homedir7 } from "os";
4485
- init_dsh_runtime();
4486
- init_process();
4487
- import { existsSync as existsSync6 } from "fs";
4488
-
4489
4529
  // src/guardian/heartbeat.ts
4490
4530
  import { readFile as readFile9 } from "fs/promises";
4491
4531
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 5e3;
@@ -4496,15 +4536,25 @@ async function readHeartbeat(file) {
4496
4536
  if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string" || typeof parsed.ts !== "number") {
4497
4537
  return void 0;
4498
4538
  }
4499
- return {
4539
+ const payload = {
4500
4540
  pid: parsed.pid,
4501
4541
  startedAt: parsed.startedAt,
4502
4542
  ts: parsed.ts
4503
4543
  };
4544
+ const channel = parsed.channel;
4545
+ if (isChannelSnapshot(channel)) {
4546
+ payload.channel = channel;
4547
+ }
4548
+ return payload;
4504
4549
  } catch {
4505
4550
  return void 0;
4506
4551
  }
4507
4552
  }
4553
+ function isChannelSnapshot(value) {
4554
+ if (!value || typeof value !== "object") return false;
4555
+ const snapshot = value;
4556
+ return typeof snapshot.state === "string" && typeof snapshot.ready === "boolean";
4557
+ }
4508
4558
  function heartbeatAgeMs(payload, now = Date.now()) {
4509
4559
  return Math.max(0, now - payload.ts);
4510
4560
  }
@@ -4512,12 +4562,34 @@ function isHeartbeatFresh(payload, maxAgeMs, now = Date.now()) {
4512
4562
  if (payload === void 0) return false;
4513
4563
  return heartbeatAgeMs(payload, now) < maxAgeMs;
4514
4564
  }
4515
- function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS) {
4565
+ function channelHealthLabel(channel) {
4566
+ if (channel === void 0) return "\u672A\u4E0A\u62A5";
4567
+ if (channel.ready) {
4568
+ return `ready (generation ${channel.generation ?? "-"})`;
4569
+ }
4570
+ switch (channel.state) {
4571
+ case "connecting":
4572
+ return "connecting";
4573
+ case "reconnecting":
4574
+ return `reconnecting (attempt ${channel.reconnectAttempts ?? "-"})`;
4575
+ case "failed":
4576
+ return "failed";
4577
+ case "stopped":
4578
+ return "stopped";
4579
+ default:
4580
+ return channel.state;
4581
+ }
4582
+ }
4583
+ function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, getChannelHealth) {
4516
4584
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4517
4585
  let stopped = false;
4518
4586
  const beat = async () => {
4519
4587
  if (stopped) return;
4520
4588
  const payload = { pid, startedAt, ts: Date.now() };
4589
+ const channel = getChannelHealth?.();
4590
+ if (channel !== void 0) {
4591
+ payload.channel = channel;
4592
+ }
4521
4593
  try {
4522
4594
  await writeFileAtomic(file, `${JSON.stringify(payload)}
4523
4595
  `, {
@@ -4540,6 +4612,13 @@ function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS) {
4540
4612
  }
4541
4613
 
4542
4614
  // src/upgrade/detect.ts
4615
+ init_own_package();
4616
+ import { readFile as readFile10 } from "fs/promises";
4617
+ import { join as join12 } from "path";
4618
+ import { homedir as homedir7 } from "os";
4619
+ init_dsh_runtime();
4620
+ init_process();
4621
+ import { existsSync as existsSync6 } from "fs";
4543
4622
  async function readInstalledPackage(dshHome, profile, packageName) {
4544
4623
  const manifest = join12(
4545
4624
  dshHome,
@@ -4616,7 +4695,7 @@ async function detectUpgradeState(options = {}) {
4616
4695
 
4617
4696
  // src/upgrade/state.ts
4618
4697
  import { mkdir as mkdir9, readFile as readFile11 } from "fs/promises";
4619
- import { dirname as dirname10, join as join13 } from "path";
4698
+ import { dirname as dirname11, join as join13 } from "path";
4620
4699
  function upgradeStatePath(root) {
4621
4700
  return join13(root, "upgrade-state.json");
4622
4701
  }
@@ -4632,7 +4711,7 @@ async function loadUpgradeState(file) {
4632
4711
  }
4633
4712
  }
4634
4713
  async function saveUpgradeState(file, state) {
4635
- await mkdir9(dirname10(file), { recursive: true });
4714
+ await mkdir9(dirname11(file), { recursive: true });
4636
4715
  await writeFileAtomic(file, JSON.stringify(state, null, 2));
4637
4716
  }
4638
4717
 
@@ -4715,13 +4794,13 @@ function packageSpecFor(name, version) {
4715
4794
 
4716
4795
  // src/service/manager.ts
4717
4796
  import { mkdir as mkdir14, readFile as readFile14, rename as rename2, rm as rm7, stat as stat5 } from "fs/promises";
4718
- import { dirname as dirname13, join as join19 } from "path";
4797
+ import { dirname as dirname14, join as join19 } from "path";
4719
4798
  import { homedir as homedir11 } from "os";
4720
4799
  init_dsh_runtime();
4721
4800
 
4722
4801
  // src/service/command.ts
4723
4802
  import { execFile } from "child_process";
4724
- import { basename as basename3, dirname as dirname11, join as join14 } from "path";
4803
+ import { basename as basename3, dirname as dirname12, join as join14 } from "path";
4725
4804
  import { fileURLToPath as fileURLToPath2 } from "url";
4726
4805
  var runCommand = (command, args, options) => new Promise((resolve6) => {
4727
4806
  execFile(
@@ -4747,7 +4826,7 @@ function resolveCliJsPath(metaUrl = import.meta.url) {
4747
4826
  const current = fileURLToPath2(metaUrl);
4748
4827
  const name = basename3(current);
4749
4828
  if (name === "cli.js" || name === "cli.mjs" || name === "cli.cjs") return current;
4750
- return join14(dirname11(current), "..", "dist", "cli.js");
4829
+ return join14(dirname12(current), "..", "dist", "cli.js");
4751
4830
  }
4752
4831
  function slugify(value) {
4753
4832
  const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -4790,6 +4869,9 @@ function snapshotServiceEnv(source = process.env, extraKeys = [], inherited = {}
4790
4869
  }
4791
4870
  }
4792
4871
  }
4872
+ if (typeof env.PATH === "string" && env.PATH.length > 0) {
4873
+ env.PATH = sanitizeServicePath(process.execPath, env.PATH);
4874
+ }
4793
4875
  return env;
4794
4876
  }
4795
4877
  function formatEnvFile(env) {
@@ -5375,7 +5457,7 @@ var PortableServiceController = class {
5375
5457
 
5376
5458
  // src/service/windows-task.ts
5377
5459
  import { mkdir as mkdir13, writeFile as writeFile7 } from "fs/promises";
5378
- import { dirname as dirname12, join as join18 } from "path";
5460
+ import { dirname as dirname13, join as join18 } from "path";
5379
5461
  function psQuote(value) {
5380
5462
  return `'${value.replace(/'/g, "''")}'`;
5381
5463
  }
@@ -5386,7 +5468,7 @@ function buildScheduledTaskScript(action, spec) {
5386
5468
  `$arguments = ${psQuote(spec.commandArgs.map((arg) => `"${arg.replace(/"/g, '\\"')}"`).join(" "))}`,
5387
5469
  `$log = ${psQuote(spec.logFile)}`,
5388
5470
  `$profile = ${psQuote(spec.profile)}`,
5389
- `$workDir = ${psQuote(dirname12(spec.commandPath))}`
5471
+ `$workDir = ${psQuote(dirname13(spec.commandPath))}`
5390
5472
  ].join("\n");
5391
5473
  const install = [
5392
5474
  `$inner = '"' + $command + '" ' + $arguments + ' >> "' + $log + '" 2>&1'`,
@@ -5605,7 +5687,7 @@ var ServiceManager = class {
5605
5687
  }
5606
5688
  async withLifecycleLock(operation) {
5607
5689
  const lockDir = this.paths.serviceLockDir(this.profile);
5608
- await mkdir14(dirname13(lockDir), { recursive: true });
5690
+ await mkdir14(dirname14(lockDir), { recursive: true });
5609
5691
  try {
5610
5692
  await mkdir14(lockDir);
5611
5693
  } catch (error) {
@@ -5645,11 +5727,39 @@ var ServiceManager = class {
5645
5727
  if (serviceStatus.state === "running") return;
5646
5728
  const process2 = await this.findProcess(this.profile);
5647
5729
  if (process2) {
5730
+ if (await this.isGuardianSpawned(process2)) {
5731
+ await this.stopGuardianSpawnedProcess(process2.pid);
5732
+ return;
5733
+ }
5648
5734
  throw new Error(
5649
5735
  `\u68C0\u6D4B\u5230\u672A\u53D7\u7BA1\u7684 dsh --profile ${this.profile}\uFF08pid ${process2.pid}\uFF09\u3002\u8BF7\u5148\u5728\u539F\u7EC8\u7AEF\u505C\u6B62\u5B83\uFF0C\u518D\u91CD\u8BD5\u3002`
5650
5736
  );
5651
5737
  }
5652
5738
  }
5739
+ /** True when the profile process was spawned by the resident guardian. */
5740
+ async isGuardianSpawned(process2) {
5741
+ const ppid = process2.ppid;
5742
+ if (!ppid) return false;
5743
+ const parent = (await listProcesses()).find((entry) => entry.pid === ppid);
5744
+ return parent !== void 0 && matchGuardianProcess(parent.cmdline);
5745
+ }
5746
+ /** Best-effort stop of a guardian-spawned profile process (grace, then kill). */
5747
+ async stopGuardianSpawnedProcess(pid) {
5748
+ try {
5749
+ process.kill(pid, "SIGTERM");
5750
+ } catch {
5751
+ return;
5752
+ }
5753
+ const deadline = Date.now() + 5e3;
5754
+ while (Date.now() < deadline && isPidAlive2(pid)) {
5755
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
5756
+ }
5757
+ if (!isPidAlive2(pid)) return;
5758
+ try {
5759
+ process.kill(pid, "SIGKILL");
5760
+ } catch {
5761
+ }
5762
+ }
5653
5763
  async buildSpec(requireProfile = false) {
5654
5764
  const previous = await this.readMetadata();
5655
5765
  const dshBin = this.dshBinOverride ?? discoverDshBin(this.home, this.sourceEnv) ?? previous?.dshBin;
@@ -5746,8 +5856,8 @@ var ServiceManager = class {
5746
5856
  }
5747
5857
  }
5748
5858
  async prepareFiles(spec, platform) {
5749
- await mkdir14(dirname13(spec.logFile), { recursive: true });
5750
- await mkdir14(dirname13(spec.envFile), { recursive: true });
5859
+ await mkdir14(dirname14(spec.logFile), { recursive: true });
5860
+ await mkdir14(dirname14(spec.envFile), { recursive: true });
5751
5861
  await writeServiceEnv(spec.envFile, spec.env);
5752
5862
  if (platform === "win32-task") {
5753
5863
  try {
@@ -5918,6 +6028,18 @@ async function runDoctorChecks(options) {
5918
6028
  } catch (error) {
5919
6029
  lines.push(`service: \u26A0\uFE0F \u72B6\u6001\u68C0\u67E5\u5931\u8D25\uFF08${error instanceof Error ? error.message : String(error)}\uFF09`);
5920
6030
  }
6031
+ try {
6032
+ const heartbeat = await readHeartbeat(
6033
+ paths.profilePath(env.guardianBridgeProfile, "guardian", "heartbeat.json")
6034
+ );
6035
+ lines.push(`channel: ${channelHealthLabel(heartbeat?.channel)}`);
6036
+ if (heartbeat?.channel && !heartbeat.channel.ready) {
6037
+ lines.push(
6038
+ "channel: \u26A0\uFE0F \u5F15\u64CE\u8FDB\u7A0B\u5B58\u6D3B\u4F46\u901A\u9053\u672A\u5C31\u7EEA\uFF08\u534A\u5F00/\u91CD\u8FDE/\u5931\u8D25\uFF09\uFF1B\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8DEF\u7531\u6216\u91CD\u542F managed engine"
6039
+ );
6040
+ }
6041
+ } catch {
6042
+ }
5921
6043
  try {
5922
6044
  const state = await loadUpgradeState(upgradeStatePath(env.home));
5923
6045
  if (state?.lastUpgrade.pendingRestart === true) {
@@ -7057,6 +7179,106 @@ var LanguagePolicyStore = class {
7057
7179
  // src/bridge/channel.ts
7058
7180
  import { createLarkChannel as createLarkChannel2 } from "@larksuite/channel";
7059
7181
 
7182
+ // src/bridge/channel-health.ts
7183
+ function mapState(state) {
7184
+ switch (state) {
7185
+ case "connected":
7186
+ return "ready";
7187
+ case "reconnecting":
7188
+ return "reconnecting";
7189
+ case "failed":
7190
+ return "failed";
7191
+ case "connecting":
7192
+ return "connecting";
7193
+ case "idle":
7194
+ default:
7195
+ return "connecting";
7196
+ }
7197
+ }
7198
+ var ChannelHealthMonitor = class {
7199
+ channel;
7200
+ pollMs;
7201
+ onUpdate;
7202
+ generation = 0;
7203
+ connectedAt;
7204
+ lastInboundAt;
7205
+ lastReconnectAt;
7206
+ lastError;
7207
+ observedState = "connecting";
7208
+ reconnectAttempts = 0;
7209
+ at = 0;
7210
+ pollTimer;
7211
+ constructor(channel, options = {}) {
7212
+ this.channel = channel;
7213
+ this.pollMs = options.pollMs ?? 5e3;
7214
+ this.onUpdate = options.onUpdate;
7215
+ }
7216
+ /** Latest snapshot (always a copy; safe to persist). */
7217
+ snapshot() {
7218
+ return {
7219
+ state: this.observedState,
7220
+ ready: this.observedState === "ready",
7221
+ generation: this.generation,
7222
+ ...this.connectedAt !== void 0 ? { connectedAt: this.connectedAt } : {},
7223
+ reconnectAttempts: this.reconnectAttempts,
7224
+ ...this.lastInboundAt !== void 0 ? { lastInboundAt: this.lastInboundAt } : {},
7225
+ ...this.lastReconnectAt !== void 0 ? { lastReconnectAt: this.lastReconnectAt } : {},
7226
+ ...this.lastError !== void 0 ? { lastError: this.lastError } : {},
7227
+ at: this.at
7228
+ };
7229
+ }
7230
+ /** Bind transport hooks from the bridge event subscription. */
7231
+ observeMessage() {
7232
+ this.lastInboundAt = Date.now();
7233
+ this.markChanged();
7234
+ }
7235
+ observeReconnecting() {
7236
+ this.observedState = "reconnecting";
7237
+ this.markChanged();
7238
+ }
7239
+ observeReconnected() {
7240
+ this.generation += 1;
7241
+ const now = Date.now();
7242
+ this.connectedAt = now;
7243
+ this.lastReconnectAt = now;
7244
+ this.observedState = "ready";
7245
+ this.markChanged();
7246
+ }
7247
+ observeError(error) {
7248
+ this.lastError = error instanceof Error ? error.message : String(error);
7249
+ if (this.observedState !== "reconnecting" && this.observedState !== "failed") {
7250
+ this.observedState = "failed";
7251
+ }
7252
+ this.markChanged();
7253
+ }
7254
+ /** Poll the SDK's connection-status snapshot. */
7255
+ start() {
7256
+ if (this.pollTimer) return;
7257
+ this.refresh();
7258
+ this.pollTimer = setInterval(() => this.refresh(), this.pollMs);
7259
+ this.pollTimer.unref?.();
7260
+ }
7261
+ stop() {
7262
+ if (this.pollTimer) {
7263
+ clearInterval(this.pollTimer);
7264
+ this.pollTimer = void 0;
7265
+ }
7266
+ this.observedState = "stopped";
7267
+ this.markChanged();
7268
+ }
7269
+ refresh() {
7270
+ const status = this.channel.getConnectionStatus?.();
7271
+ this.observedState = mapState(status?.state);
7272
+ this.reconnectAttempts = status?.reconnectAttempts ?? this.reconnectAttempts;
7273
+ this.markChanged();
7274
+ }
7275
+ markChanged() {
7276
+ const now = Date.now();
7277
+ this.at = now;
7278
+ this.onUpdate?.(this.snapshot());
7279
+ }
7280
+ };
7281
+
7060
7282
  // src/commands/index.ts
7061
7283
  import { stat as stat8 } from "fs/promises";
7062
7284
  import { homedir as homedir14 } from "os";
@@ -10299,7 +10521,7 @@ async function handleConfigHubAction(action, ctx, value = {}) {
10299
10521
  }
10300
10522
 
10301
10523
  // src/commands/archive.ts
10302
- import { dirname as dirname14 } from "path";
10524
+ import { dirname as dirname15 } from "path";
10303
10525
 
10304
10526
  // src/media/outbound-files.ts
10305
10527
  import { constants } from "fs";
@@ -10495,7 +10717,7 @@ async function sendArchiveFiles(ctx, record, destination = {
10495
10717
  [record.jsonlPath, `${record.archiveId}.jsonl`]
10496
10718
  ]) {
10497
10719
  try {
10498
- const prepared = await prepareOutboundFile({ path, baseDir: dirname14(path), allowedRoots: [dirname14(path)], fileName });
10720
+ const prepared = await prepareOutboundFile({ path, baseDir: dirname15(path), allowedRoots: [dirname15(path)], fileName });
10499
10721
  await ctx.channel.sendFile(destination.chatId, prepared.fileName, prepared.content, {
10500
10722
  ...destination.messageId ? { replyTo: destination.messageId } : {},
10501
10723
  ...destination.threadId ? { threadId: destination.threadId } : {}
@@ -13751,6 +13973,8 @@ The value is written only by the local bridge and is not sent to the agent.` },
13751
13973
  }
13752
13974
 
13753
13975
  // src/bridge/channel.ts
13976
+ var DEFAULT_CHANNEL_PING_TIMEOUT_SEC = 30;
13977
+ var DEFAULT_CHANNEL_KEEPALIVE_MS = 15e3;
13754
13978
  async function startChannel(deps) {
13755
13979
  const channel = (deps.createChannel ?? createLarkChannel2)({
13756
13980
  appId: deps.appId,
@@ -13777,7 +14001,26 @@ async function startChannel(deps) {
13777
14001
  resolveChatMode: true,
13778
14002
  handshakeTimeoutMs: 8e3,
13779
14003
  httpTimeoutMs: 3e4,
13780
- respectProxyEnv: true
14004
+ respectProxyEnv: true,
14005
+ // Issue #108: detect a half-open WebSocket (TCP ESTABLISHED but Feishu no
14006
+ // longer delivering). `pingTimeout` force-reconnects when no inbound frame
14007
+ // arrives after the last ping; the app-level `keepalive` probes and
14008
+ // force-reconnects, and `onUnrecoverable` fires when even that fails so the
14009
+ // engine can exit and let the managed service / guardian restart it.
14010
+ wsConfig: { pingTimeout: deps.channelPingTimeoutSec ?? DEFAULT_CHANNEL_PING_TIMEOUT_SEC },
14011
+ keepalive: {
14012
+ enabled: deps.channelKeepalive ?? true,
14013
+ intervalMs: deps.channelKeepaliveMs ?? DEFAULT_CHANNEL_KEEPALIVE_MS,
14014
+ onUnrecoverable: (error) => {
14015
+ log.fail("channel", "unrecoverable", {
14016
+ error: error instanceof Error ? error.message : String(error)
14017
+ });
14018
+ deps.onChannelUnrecoverable?.(error);
14019
+ }
14020
+ }
14021
+ });
14022
+ const channelHealth = new ChannelHealthMonitor(channel, {
14023
+ ...deps.channelHealthPollMs !== void 0 ? { pollMs: deps.channelHealthPollMs } : {}
13781
14024
  });
13782
14025
  const streaming = adaptLarkChannel(channel);
13783
14026
  const commandChannel = streaming;
@@ -13818,6 +14061,7 @@ async function startChannel(deps) {
13818
14061
  const isolationStore = deps.isolationStore ?? EMPTY_ISOLATION_STORE;
13819
14062
  let groupPoller;
13820
14063
  const processMessage = async (msg, alreadyClaimed = false) => {
14064
+ channelHealth.observeMessage();
13821
14065
  if (groupPoller && !alreadyClaimed && !groupPoller.claim(msg.messageId)) return;
13822
14066
  const chatMode = msg.chatMode ?? msg.chatType;
13823
14067
  const botSender = msg.senderType === "bot";
@@ -14394,21 +14638,25 @@ ${msg.content}`,
14394
14638
  },
14395
14639
  reconnecting: () => {
14396
14640
  log.warn("channel", "reconnecting", {});
14641
+ channelHealth.observeReconnecting();
14397
14642
  void reconnectNotifier.reconnecting().catch((error) => {
14398
14643
  log.fail("channel-reconnect-notice", error);
14399
14644
  });
14400
14645
  },
14401
14646
  reconnected: () => {
14402
14647
  log.info("channel", "reconnected", {});
14648
+ channelHealth.observeReconnected();
14403
14649
  void reconnectNotifier.reconnected().catch((error) => {
14404
14650
  log.fail("channel-reconnect-notice", error);
14405
14651
  });
14406
14652
  },
14407
14653
  error: (error) => {
14408
14654
  log.fail("channel", error);
14655
+ channelHealth.observeError(error);
14409
14656
  }
14410
14657
  });
14411
14658
  await channel.connect();
14659
+ channelHealth.start();
14412
14660
  if (sessionProjectionBridge) {
14413
14661
  void sessionProjectionBridge.start().catch((error) => {
14414
14662
  log.fail("session-projection", error, { step: "start" });
@@ -14425,8 +14673,10 @@ ${msg.content}`,
14425
14673
  }
14426
14674
  return {
14427
14675
  channel,
14676
+ channelHealth: () => channelHealth.snapshot(),
14428
14677
  disconnect: async () => {
14429
14678
  await groupPoller?.stop();
14679
+ channelHealth.stop();
14430
14680
  sessionProjection?.close();
14431
14681
  await sessionProjectionBridge?.close();
14432
14682
  await channel.disconnect();
@@ -16475,7 +16725,7 @@ async function safeRemove(path) {
16475
16725
  import { execFile as execFile3 } from "child_process";
16476
16726
  import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
16477
16727
  import { access as access2, copyFile, mkdir as mkdir18, realpath as realpath2 } from "fs/promises";
16478
- import { dirname as dirname15, join as join25 } from "path";
16728
+ import { dirname as dirname16, join as join25 } from "path";
16479
16729
  import { promisify as promisify2 } from "util";
16480
16730
  var execFileAsync2 = promisify2(execFile3);
16481
16731
  async function defaultRunGit2(args, cwd) {
@@ -16494,7 +16744,7 @@ async function exists2(path) {
16494
16744
  }
16495
16745
  }
16496
16746
  async function defaultCopyRulesFile(source, target) {
16497
- await mkdir18(dirname15(target), { recursive: true });
16747
+ await mkdir18(dirname16(target), { recursive: true });
16498
16748
  await copyFile(source, target);
16499
16749
  }
16500
16750
  function slugify2(scope) {
@@ -16712,7 +16962,7 @@ var UpdateNotifier = class {
16712
16962
 
16713
16963
  // src/bot/fleet-store.ts
16714
16964
  import { mkdir as mkdir19, readFile as readFile28 } from "fs/promises";
16715
- import { dirname as dirname16 } from "path";
16965
+ import { dirname as dirname17 } from "path";
16716
16966
  function validBotInstanceName(name) {
16717
16967
  return /^[a-z][a-z0-9-]{0,31}$/u.test(name);
16718
16968
  }
@@ -16833,7 +17083,7 @@ var BotFleetStore = class {
16833
17083
  })).sort((a, b) => a.name.localeCompare(b.name));
16834
17084
  }
16835
17085
  async persist() {
16836
- await mkdir19(dirname16(this.path), { recursive: true });
17086
+ await mkdir19(dirname17(this.path), { recursive: true });
16837
17087
  await writeFileAtomic(this.path, `${JSON.stringify(this.data, null, 2)}
16838
17088
  `, { mode: 384 });
16839
17089
  }
@@ -16853,7 +17103,7 @@ function isFleetEntry(value) {
16853
17103
 
16854
17104
  // src/bot/handoff-guard.ts
16855
17105
  import { mkdir as mkdir20, readFile as readFile29 } from "fs/promises";
16856
- import { dirname as dirname17 } from "path";
17106
+ import { dirname as dirname18 } from "path";
16857
17107
  var BotHandoffGuard = class {
16858
17108
  constructor(path) {
16859
17109
  this.path = path;
@@ -16901,7 +17151,7 @@ var BotHandoffGuard = class {
16901
17151
  }
16902
17152
  }
16903
17153
  async write(data) {
16904
- await mkdir20(dirname17(this.path), { recursive: true });
17154
+ await mkdir20(dirname18(this.path), { recursive: true });
16905
17155
  await writeFileAtomic(this.path, `${JSON.stringify(data, null, 2)}
16906
17156
  `, { mode: 384 });
16907
17157
  }
@@ -17684,7 +17934,7 @@ init_own_package();
17684
17934
  import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
17685
17935
  import { spawn as spawn7 } from "child_process";
17686
17936
  import { chmod, mkdir as mkdir21, readFile as readFile31, rm as rm10 } from "fs/promises";
17687
- import { dirname as dirname18, join as join26 } from "path";
17937
+ import { dirname as dirname19, join as join26 } from "path";
17688
17938
  init_own_package();
17689
17939
  init_process();
17690
17940
  function guardianUpdateFailureHint(code) {
@@ -17721,7 +17971,7 @@ async function loadState(file) {
17721
17971
  }
17722
17972
  }
17723
17973
  async function saveState(file, state) {
17724
- await mkdir21(dirname18(file), { recursive: true });
17974
+ await mkdir21(dirname19(file), { recursive: true });
17725
17975
  await writeFileAtomic(file, `${JSON.stringify(state, null, 2)}
17726
17976
  `, { mode: 384 });
17727
17977
  }
@@ -17796,7 +18046,7 @@ async function runGuardianUpdateWorker(request, options = {}) {
17796
18046
  if (delayMs > 0) await new Promise((resolve6) => setTimeout(resolve6, delayMs));
17797
18047
  const spec = `${state.packageName}@${state.targetVersion}`;
17798
18048
  const run = options.run ?? captureOutput;
17799
- const workerRoot = join26(dirname18(request.stateFile), "update-worker");
18049
+ const workerRoot = join26(dirname19(request.stateFile), "update-worker");
17800
18050
  const cacheRoot = join26(workerRoot, "npm-cache");
17801
18051
  const cacheDir = join26(
17802
18052
  cacheRoot,
@@ -18586,7 +18836,18 @@ async function startBridgeEngine(options) {
18586
18836
  },
18587
18837
  allowedUsers: activeProfile.access.allowedUsers,
18588
18838
  allowedChats: activeProfile.access.allowedChats,
18589
- ...options.createChannel ? { createChannel: options.createChannel } : {}
18839
+ ...options.createChannel ? { createChannel: options.createChannel } : {},
18840
+ channelPingTimeoutSec: env.channelPingTimeoutSec,
18841
+ channelKeepalive: env.channelKeepalive,
18842
+ channelKeepaliveMs: env.channelKeepaliveMs,
18843
+ channelHealthPollMs: env.channelHealthPollMs,
18844
+ onChannelUnrecoverable: (error) => {
18845
+ log.fail("engine", "channel-unrecoverable", {
18846
+ error: error instanceof Error ? error.message : String(error)
18847
+ });
18848
+ process.exitCode = 1;
18849
+ setTimeout(() => process.exit(1), 300);
18850
+ }
18590
18851
  };
18591
18852
  if (activeProfile.preferences.stopGraceMs !== void 0) {
18592
18853
  channelInput.stopGraceMs = activeProfile.preferences.stopGraceMs;
@@ -18651,7 +18912,8 @@ async function startBridgeEngine(options) {
18651
18912
  const heartbeat = startHeartbeat(
18652
18913
  paths.profilePath(profileName, "guardian", "heartbeat.json"),
18653
18914
  process.pid,
18654
- env.heartbeatMs
18915
+ env.heartbeatMs,
18916
+ () => bridge.channelHealth?.()
18655
18917
  );
18656
18918
  await updateHandoff.reconcile(currentVersion());
18657
18919
  void deliverUpdateResult().catch((error) => log.fail("upgrade", error, { step: "deliver-result" }));
@@ -19730,6 +19992,7 @@ var GuardianService = class {
19730
19992
  dshUp: this.dshUp,
19731
19993
  heartbeatAgeMs: this.lastHeartbeatAgeMs,
19732
19994
  channelConnected: this.channel !== void 0,
19995
+ channelState: this.lastHeartbeatChannel?.state,
19733
19996
  safeEngine: this.safeEngine?.kind,
19734
19997
  safeRuns: this.safeRuns.size,
19735
19998
  pid: process.pid,
@@ -19740,6 +20003,9 @@ var GuardianService = class {
19740
20003
  }
19741
20004
  dshUp = false;
19742
20005
  lastHeartbeatAgeMs;
20006
+ lastHeartbeatChannel;
20007
+ /** When the engine reported a non-ready channel while its heartbeat was fresh. */
20008
+ channelUnhealthySinceMs;
19743
20009
  log() {
19744
20010
  return this.options.logger ?? log;
19745
20011
  }
@@ -19774,7 +20040,24 @@ var GuardianService = class {
19774
20040
  const now = (this.options.now ?? Date.now)();
19775
20041
  const heartbeatFresh = isHeartbeatFresh(heartbeat, this.options.staleMs, now);
19776
20042
  this.lastHeartbeatAgeMs = heartbeat ? heartbeatAgeMs(heartbeat, now) : void 0;
20043
+ this.lastHeartbeatChannel = heartbeat?.channel;
19777
20044
  if (heartbeatFresh) this.lastHeartbeatFreshAt = now;
20045
+ const channelSnapshot = this.lastHeartbeatChannel;
20046
+ const channelUnhealthy = heartbeatFresh && channelSnapshot !== void 0 && channelSnapshot.ready === false;
20047
+ if (channelUnhealthy) {
20048
+ if (this.channelUnhealthySinceMs === void 0) {
20049
+ this.channelUnhealthySinceMs = now;
20050
+ this.log().warn("guardian", "channel-unhealthy", {
20051
+ dshProfile: this.state.dshProfile,
20052
+ state: channelSnapshot.state,
20053
+ generation: channelSnapshot.generation,
20054
+ reconnectAttempts: channelSnapshot.reconnectAttempts,
20055
+ lastError: channelSnapshot.lastError
20056
+ });
20057
+ }
20058
+ } else {
20059
+ this.channelUnhealthySinceMs = void 0;
20060
+ }
19778
20061
  const processFound = await (this.options.findProcess ?? findProfileProcess)(
19779
20062
  this.state.dshProfile
19780
20063
  );
@@ -20822,6 +21105,7 @@ async function statusGuardianCommand(options = {}, deps = {}) {
20822
21105
  `\u6865\u63A5 profile\uFF1A${state.bridgeProfile}`,
20823
21106
  `dsh \u662F\u5426\u5728\u7EBF\uFF1A${up ? "\u662F" : "\u5426"}${processFound ? `\uFF08pid ${processFound.pid}\uFF09` : ""}`,
20824
21107
  `\u5FC3\u8DF3\u9F84\uFF1A${heartbeat ? `${heartbeatAgeMs(heartbeat)}ms` : "\u65E0"}`,
21108
+ `\u98DE\u4E66\u901A\u9053\uFF1A${channelHealthLabel(heartbeat?.channel)}`,
20825
21109
  `\u5DF2\u89C2\u5BDF\u8FC7 dsh \u8FD0\u884C\uFF1A${state.profileSeenUp ? "\u662F" : "\u5426"}`,
20826
21110
  guardianProcess === void 0 ? "\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A\u672A\u53D1\u73B0\uFF08\u65E0\u6CD5\u552F\u4E00\u8BC1\u660E resident guardian \u8EAB\u4EFD\uFF09" : `\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A${guardianProcess.pid}`,
20827
21111
  `\u72B6\u6001\u6587\u4EF6\uFF1A${layout.stateFile}`,
@@ -20860,7 +21144,7 @@ function managerFor(options, version) {
20860
21144
  version
20861
21145
  });
20862
21146
  }
20863
- function formatServiceStatus(status, heartbeatAge2) {
21147
+ function formatServiceStatus(status, heartbeatAge2, channel) {
20864
21148
  return [
20865
21149
  "dsh-lark-bot \u6B63\u5E38\u5F15\u64CE\u670D\u52A1",
20866
21150
  ` name: ${status.name}`,
@@ -20871,7 +21155,8 @@ function formatServiceStatus(status, heartbeatAge2) {
20871
21155
  ` detail: ${status.detail}`,
20872
21156
  ` pid: ${status.pid ?? "-"}`,
20873
21157
  ` restarts: ${status.restarts ?? "-"}`,
20874
- ` heartbeat: ${heartbeatAge2 === void 0 ? "\u6682\u65E0" : `${heartbeatAge2}ms`}`
21158
+ ` heartbeat: ${heartbeatAge2 === void 0 ? "\u6682\u65E0" : `${heartbeatAge2}ms`}`,
21159
+ ` channel: ${channelHealthLabel(channel)}`
20875
21160
  ].join("\n");
20876
21161
  }
20877
21162
  async function heartbeatAge() {
@@ -20884,6 +21169,16 @@ async function heartbeatAge() {
20884
21169
  const heartbeat = await readHeartbeat(file);
20885
21170
  return heartbeat ? heartbeatAgeMs(heartbeat) : void 0;
20886
21171
  }
21172
+ async function heartbeatChannel() {
21173
+ const env = loadRuntimeEnv(process.env);
21174
+ const file = resolveAppPaths(env.home).profilePath(
21175
+ env.guardianBridgeProfile,
21176
+ "guardian",
21177
+ "heartbeat.json"
21178
+ );
21179
+ const heartbeat = await readHeartbeat(file);
21180
+ return heartbeat?.channel;
21181
+ }
20887
21182
  async function runServiceCommand(action, options = {}, deps = {}) {
20888
21183
  const manager = deps.manager ?? managerFor(options, deps.version ?? "0.0.0");
20889
21184
  const output = deps.output ?? ((text) => process.stdout.write(text));
@@ -20900,7 +21195,11 @@ ${logs.text || "\uFF08\u6682\u65E0\u65E5\u5FD7\uFF09"}
20900
21195
  return;
20901
21196
  }
20902
21197
  const status = action === "install" ? await manager.install() : action === "start" ? await manager.start() : action === "restart" ? await manager.restart() : action === "stop" ? await manager.stop() : action === "uninstall" ? await manager.uninstall() : await manager.status();
20903
- output(`${formatServiceStatus(status, await (deps.heartbeatAge ?? heartbeatAge)())}
21198
+ const [age, channel] = await Promise.all([
21199
+ (deps.heartbeatAge ?? heartbeatAge)(),
21200
+ (deps.heartbeatChannel ?? heartbeatChannel)()
21201
+ ]);
21202
+ output(`${formatServiceStatus(status, age, channel)}
20904
21203
  `);
20905
21204
  if ((action === "install" || action === "start" || action === "restart") && status.state !== "running") process.exitCode = 1;
20906
21205
  if (action === "status" && (!status.installed || status.state === "error")) {
@@ -20927,7 +21226,7 @@ function followLogFile(path, lines) {
20927
21226
  // src/cli/commands/supervise.ts
20928
21227
  import { spawn as spawn10 } from "child_process";
20929
21228
  import { closeSync, mkdirSync, openSync } from "fs";
20930
- import { dirname as dirname19 } from "path";
21229
+ import { dirname as dirname20 } from "path";
20931
21230
  import { homedir as homedir22 } from "os";
20932
21231
  init_dsh_runtime();
20933
21232
  var BASE_BACKOFF_MS = 5e3;
@@ -20961,8 +21260,8 @@ async function runSupervise(options, deps = {}) {
20961
21260
  const logFile = paths.serviceLogFile(profile);
20962
21261
  const dshBin = deps.dshBin ?? discoverDshBin(homedir22(), childEnv);
20963
21262
  if (!dshBin) throw new Error("\u672A\u627E\u5230 dsh CLI\uFF0Cportable supervisor \u65E0\u6CD5\u542F\u52A8 profile\u3002");
20964
- mkdirSync(dirname19(statusFile), { recursive: true });
20965
- mkdirSync(dirname19(logFile), { recursive: true });
21263
+ mkdirSync(dirname20(statusFile), { recursive: true });
21264
+ mkdirSync(dirname20(logFile), { recursive: true });
20966
21265
  const logFd = openSync(logFile, "a");
20967
21266
  const writeStatus = async (state, childPid, restarts2) => {
20968
21267
  const processIdentity = await (deps.readProcessIdentity ?? readLinuxProcessIdentity)(process.pid);