@arcships/morula-runtime 0.1.0-alpha.15 → 0.1.0-alpha.16

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/index.js CHANGED
@@ -740,7 +740,7 @@ var BUILD_ID, BUILD_CONTEXTS, BUILD_DEFAULT_CONTEXT;
740
740
  var init_build_env = __esm({
741
741
  "../morula-cli-core/dist/build-env.js"() {
742
742
  "use strict";
743
- BUILD_ID = "20260924065153.0fc1a372";
743
+ BUILD_ID = "20260929103157.019b0b1c";
744
744
  BUILD_CONTEXTS = {
745
745
  "local": "http://localhost:8080/api",
746
746
  "test": "https://morula.basecastle.com/api",
@@ -2588,9 +2588,9 @@ async function setupAutostart(context) {
2588
2588
  await removeLegacyAutostart();
2589
2589
  const { command, args } = resolvePortalCommand();
2590
2590
  const stateDir2 = process.env.MORULA_CONFIG_DIR ?? join3(homedir2(), ".morula-cli");
2591
- const daemonDir2 = join3(stateDir2, "daemon");
2592
- mkdirSync2(daemonDir2, { recursive: true, mode: 448 });
2593
- const logFile = join3(daemonDir2, "portal.log");
2591
+ const daemonDir = join3(stateDir2, "daemon");
2592
+ mkdirSync2(daemonDir, { recursive: true, mode: 448 });
2593
+ const logFile = join3(daemonDir, "portal.log");
2594
2594
  const plist = buildLaunchAgentPlist({ label: LABEL, command, args, logFile });
2595
2595
  const plistPath = launchAgentPath();
2596
2596
  mkdirSync2(join3(plistPath, ".."), { recursive: true });
@@ -2626,7 +2626,13 @@ var init_portal_autostart = __esm({
2626
2626
  // src/daemon/daemon-config.ts
2627
2627
  import { randomUUID } from "node:crypto";
2628
2628
  import { homedir as homedir4 } from "node:os";
2629
- import { join as join5 } from "node:path";
2629
+ import { dirname as dirname4, join as join5 } from "node:path";
2630
+ function configFilePathForEnv(env = process.env) {
2631
+ return env.MORULA_CONFIG ?? join5(env.MORULA_CONFIG_DIR ?? join5(homedir4(), ".morula-cli"), "config.yaml");
2632
+ }
2633
+ function daemonDirForEnv(env = process.env) {
2634
+ return join5(dirname4(configFilePathForEnv(env)), "daemon");
2635
+ }
2630
2636
  function loadDaemonConfig(store) {
2631
2637
  const config = store.load();
2632
2638
  const d = config?.[DAEMON_KEY];
@@ -2666,7 +2672,7 @@ var init_daemon_config = __esm({
2666
2672
  // src/daemon/daemon-lock.ts
2667
2673
  import { execFileSync as execFileSync3 } from "node:child_process";
2668
2674
  import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync2, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync3, writeSync } from "node:fs";
2669
- import { dirname as dirname4, join as join6 } from "node:path";
2675
+ import { dirname as dirname5, join as join6 } from "node:path";
2670
2676
  function daemonPidPath(configDir) {
2671
2677
  return join6(configDir, "daemon", DAEMON_PID_FILE);
2672
2678
  }
@@ -2769,7 +2775,7 @@ function acquireDaemonLock(lockPath, input) {
2769
2775
  startedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
2770
2776
  workdir: input.workdir
2771
2777
  };
2772
- mkdirSync4(dirname4(lockPath), { recursive: true, mode: 448 });
2778
+ mkdirSync4(dirname5(lockPath), { recursive: true, mode: 448 });
2773
2779
  try {
2774
2780
  const fd = openSync2(lockPath, "wx", 384);
2775
2781
  try {
@@ -2854,7 +2860,7 @@ __export(portal_instances_exports, {
2854
2860
  });
2855
2861
  import { readdirSync, statSync } from "node:fs";
2856
2862
  import { homedir as homedir5 } from "node:os";
2857
- import { dirname as dirname5, join as join7 } from "node:path";
2863
+ import { dirname as dirname6, join as join7 } from "node:path";
2858
2864
  function parseBaseUrl(raw) {
2859
2865
  if (!raw)
2860
2866
  return null;
@@ -2922,7 +2928,7 @@ function createPortalEnvironmentResolver(options = {}) {
2922
2928
  const env = options.env ?? process.env;
2923
2929
  const log = options.log ?? (() => {
2924
2930
  });
2925
- const defaultConfigDir = dirname5(env.MORULA_CONFIG ?? join7(env.MORULA_CONFIG_DIR ?? join7(options.homeDir ?? homedir5(), ".morula-cli"), "config.yaml"));
2931
+ const defaultConfigDir = dirname6(env.MORULA_CONFIG ?? join7(env.MORULA_CONFIG_DIR ?? join7(options.homeDir ?? homedir5(), ".morula-cli"), "config.yaml"));
2926
2932
  let index = [];
2927
2933
  let signature = "";
2928
2934
  function buildIndex() {
@@ -2939,7 +2945,7 @@ function createPortalEnvironmentResolver(options = {}) {
2939
2945
  const normalized = normalizeBaseUrl2(entry.url);
2940
2946
  if (!normalized)
2941
2947
  continue;
2942
- const configDir = dirname5(file);
2948
+ const configDir = dirname6(file);
2943
2949
  built.push({
2944
2950
  configDir,
2945
2951
  context,
@@ -3482,7 +3488,7 @@ var init_dim_locate = __esm({
3482
3488
  // src/daemon/acp/acp-process-registry.ts
3483
3489
  import { execFileSync as execFileSync4 } from "node:child_process";
3484
3490
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
3485
- import { dirname as dirname6 } from "node:path";
3491
+ import { dirname as dirname7 } from "node:path";
3486
3492
  function parseLstartToMs(tokens) {
3487
3493
  if (tokens.length < 5)
3488
3494
  return null;
@@ -3595,7 +3601,7 @@ function persistRecords() {
3595
3601
  if (!recordFile)
3596
3602
  return;
3597
3603
  try {
3598
- mkdirSync5(dirname6(recordFile), { recursive: true });
3604
+ mkdirSync5(dirname7(recordFile), { recursive: true });
3599
3605
  writeFileSync5(recordFile, JSON.stringify({
3600
3606
  v: RECORD_FILE_VERSION,
3601
3607
  children: [...ownedChildren.values()]
@@ -3838,9 +3844,9 @@ var init_acp_connection = __esm({
3838
3844
  process.stderr.write(`[daemon] dim spawn \u5931\u8D25 ${dimPath}\uFF08\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u5019\u9009\uFF09: ${message}
3839
3845
  `);
3840
3846
  try {
3841
- const { existsSync: existsSync19 } = await import("node:fs");
3847
+ const { existsSync: existsSync21 } = await import("node:fs");
3842
3848
  process.stderr.write(
3843
- `[daemon] diag: fileExists=${existsSync19(dimPath)} cwdExists=${this.options.cwd ? existsSync19(this.options.cwd) : "n/a"} PATH=${(process.env.PATH ?? "").slice(0, 140)} DYLD=${process.env.DYLD_LIBRARY_PATH ?? "-"} NODE_OPTIONS=${process.env.NODE_OPTIONS ?? "-"}
3849
+ `[daemon] diag: fileExists=${existsSync21(dimPath)} cwdExists=${this.options.cwd ? existsSync21(this.options.cwd) : "n/a"} PATH=${(process.env.PATH ?? "").slice(0, 140)} DYLD=${process.env.DYLD_LIBRARY_PATH ?? "-"} NODE_OPTIONS=${process.env.NODE_OPTIONS ?? "-"}
3844
3850
  `
3845
3851
  );
3846
3852
  } catch {
@@ -4424,14 +4430,13 @@ init_dist2();
4424
4430
  init_dist2();
4425
4431
 
4426
4432
  // src/build-env.ts
4427
- var BUILD_VERSION2 = "0.1.0-alpha.15";
4433
+ var BUILD_VERSION2 = "0.1.0-alpha.16";
4428
4434
 
4429
4435
  // src/commands/daemon.ts
4430
4436
  init_dist();
4431
4437
  init_dist2();
4432
- import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync12, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
4433
- import { homedir as homedir13 } from "node:os";
4434
- import { dirname as dirname11, join as join22 } from "node:path";
4438
+ import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync12, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
4439
+ import { dirname as dirname13, join as join23 } from "node:path";
4435
4440
 
4436
4441
  // src/daemon/daemon-autostart.ts
4437
4442
  init_portal_autostart();
@@ -4500,9 +4505,9 @@ init_portal_server();
4500
4505
  // src/daemon/runner.ts
4501
4506
  init_dist2();
4502
4507
  import { execFileSync as execFileSync11 } from "node:child_process";
4503
- import { existsSync as existsSync16, mkdirSync as mkdirSync14, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
4508
+ import { existsSync as existsSync17, mkdirSync as mkdirSync15, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
4504
4509
  import { arch, hostname } from "node:os";
4505
- import { dirname as dirname9, join as join20 } from "node:path";
4510
+ import { dirname as dirname11, join as join21 } from "node:path";
4506
4511
 
4507
4512
  // src/daemon/acp/acp-connection-pool.ts
4508
4513
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
@@ -4637,6 +4642,10 @@ var DaemonApi = class {
4637
4642
  * D1(daemon-server-connection-unstable):心跳是「设备存活」的唯一信号,显式 10s 超时——
4638
4643
  * 慢请求快速失败,避免占住单飞槽位(客户端默认 30s,对心跳过松)。
4639
4644
  * 说明:`client.post()` 的第三参是 query,不支持 RequestOptions,故直接走 `client.request`。
4645
+ *
4646
+ * `upgradeRequested`(runtime-self-upgrade,沿用 stop 的一次性认领模式):服务端置了
4647
+ * upgradeRequestedAt → 本次心跳认领(服务端已清指令)→ daemon 触发自升级;`upgradeVersion`
4648
+ * 为置指令时的目标版本(可空 = daemon 自行解析 latest)。
4640
4649
  */
4641
4650
  heartbeat(runtimeId, runningTaskCount = 0) {
4642
4651
  return this.client.request(
@@ -4647,6 +4656,13 @@ var DaemonApi = class {
4647
4656
  pollTasks(runtimeId, max = 1) {
4648
4657
  return this.client.get("/agent-tasks/poll", { runtimeId, max });
4649
4658
  }
4659
+ /**
4660
+ * runtime-self-upgrade:自升级结果回报(机器协议,设备级凭证)。
4661
+ * best-effort 语义由调用方掌握(失败只 log,不影响本地升级结果)。
4662
+ */
4663
+ reportUpgradeResult(runtimeId, body) {
4664
+ return this.client.post(`/agent-runtimes/${runtimeId}/upgrade-result`, body);
4665
+ }
4650
4666
  claim(taskId, runtimeId) {
4651
4667
  const res = this.client.post(`/agent-tasks/${taskId}/claim`, void 0, { runtimeId });
4652
4668
  res.then((r) => {
@@ -5205,8 +5221,9 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
5205
5221
  - **git / gh \u547D\u4EE4\u4E00\u5F8B\u5728\u5BF9\u5E94\u4ED3\u5E93\u76EE\u5F55\u5185\u6267\u884C**\uFF08\`cd <\u5DE5\u4F5C\u533A\u6839>/<\u4ED3\u5E93\u76EE\u5F55>\`\uFF0C\u6216 \`git -C <\u4ED3\u5E93\u76EE\u5F55> ...\`\uFF09\uFF1A
5206
5222
  \u5DE5\u4F5C\u533A\u6839\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u5728\u6839\u76EE\u5F55\u8DD1 git \u547D\u4EE4\u5FC5\u7136\u5931\u8D25\u3002\`morula\` CLI \u53EF\u5728\u5DE5\u4F5C\u533A\u6839\u6216\u5176\u4EFB\u610F\u5B50\u76EE\u5F55\u6267\u884C\uFF08\u89C1 0.4\uFF09\u3002
5207
5223
  - **\u4E3B\u4ED3\u5E93**\uFF1A\u5E73\u53F0\u5DF2\u89E3\u6790\u51FA\u7684\u4ED3\u5E93\u5728**\u5DE5\u4F5C\u533A\u6839\u4E0B\u5C31\u7EEA**\uFF08\u76EE\u5F55\u540D = \u4ED3\u5E93\u5168\u540D\u628A \`/\` \u6362\u6210 \`-\`\uFF0C\u5982 \`human-resources/xxx\` \u2192 \`human-resources-xxx\`\uFF09\uFF1B\`ls\` \u5DE5\u4F5C\u533A\u6839\u5373\u53EF\u770B\u5230\uFF0C\u76F4\u63A5\u5728\u8BE5\u76EE\u5F55\u5185\u5EFA\u5206\u652F/\u63D0\u4EA4/\u63A8\u5206\u652F/\u63D0 PR\u3002**\u9700\u8981\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u54EA\u4E9B\u4ED3\u5E93\u65F6\u81EA\u5DF1\u67E5**\uFF1A\`morula repo list --space <spaceId>\`\uFF08spaceId \u89C1\u7CFB\u7EDF\u63D0\u793A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF09\u3002
5208
- - **\u26A0\uFE0F \`morula repo checkout\` \u7684\u8BED\u4E49 = \u300C\u58F0\u660E/\u5207\u6362\u672C\u4EFB\u52A1\u7684**\u4E3B\u4ED3\u5E93**\u300D**\uFF08\u670D\u52A1\u7AEF generation+1 + daemon \u7269\u5316\u5DE5\u4F5C\u533A\uFF09\uFF1A
5209
- **\u5B83\u51B3\u5B9A\u540E\u7EED\u5F00\u53D1\u9636\u6BB5\u5728\u54EA\u4E2A\u4ED3\u5E93\u5E72\u6D3B**\u3002\u56E0\u6B64\u53EA\u5728**\u5224\u5B9A\u51FA\u4E3B\u4ED3\u5E93\u4E4B\u540E**\u8C03\u7528\uFF0C**\u4E00\u6B21\u4EFB\u52A1\u53EA\u58F0\u660E\u4E00\u6B21**\uFF08\u5224\u9519\u8981\u7EA0\u6B63\u65F6\u624D\u518D\u8C03\u4E00\u6B21\uFF09\u3002
5224
+ - **\u26A0\uFE0F \`morula repo checkout\` \u7684\u8BED\u4E49 = \u300C\u58F0\u660E/\u5207\u6362\u672C\u4EFB\u52A1\u7684\u4E3B\u4ED3\u5E93\u300D**\uFF08\u670D\u52A1\u7AEF generation+1 + daemon \u7269\u5316\u5DE5\u4F5C\u533A\uFF09\uFF1A
5225
+ \u58F0\u660E\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u4EFB\u52A1\uFF08\u5F53\u524D\u9636\u6BB5\u7684\u6267\u884C\u8005\uFF09\uFF1B**\u5F00\u53D1\u9636\u6BB5\u4EFB\u52A1\u4E0D\u7EE7\u627F\u5206\u6790\u9636\u6BB5\u7684\u58F0\u660E**\u2014\u2014\u9700\u8981\u6539\u4EE3\u7801\u65F6\u7531\u4F60\u81EA\u5DF1
5226
+ \u6309\u4EFB\u52A1\u63CF\u8FF0/\u5206\u6790\u7ED3\u8BBA checkout\u3002\u56E0\u6B64\u53EA\u5728**\u5224\u5B9A\u51FA\u4E3B\u4ED3\u5E93\u4E4B\u540E**\u8C03\u7528\uFF0C**\u4E00\u6B21\u4EFB\u52A1\u53EA\u58F0\u660E\u4E00\u6B21**\uFF08\u5224\u9519\u8981\u7EA0\u6B63\u65F6\u624D\u518D\u8C03\u4E00\u6B21\uFF09\u3002
5210
5227
  \u4E0D\u8981\u7528\u5B83\u300C\u628A\u7A7A\u95F4\u91CC\u7684\u4ED3\u5E93\u9010\u4E2A\u901B\u4E00\u904D\u300D\u2014\u2014\u6BCF\u8C03\u4E00\u6B21\u4E3B\u4ED3\u5E93\u5C31\u8DDF\u7740\u6362\u4E00\u6B21\uFF08\u771F\u673A\u8E29\u5230\uFF1A\u5206\u6790\u9636\u6BB5\u4E3A\u67E5\u8BC1 checkout \u4E86 3 \u4E2A\u4ED3\u5E93\uFF0C
5211
5228
  \u4E3B\u4ED3\u5E93\u6700\u7EC8\u505C\u5728\u6700\u540E\u90A3\u4E2A\u300C\u53EA\u770B\u4E86\u4E00\u773C\u300D\u7684\u4ED3\u5E93\u4E0A\uFF09\u3002
5212
5229
  - **\u82E5\u4EFB\u52A1\u9700\u8981\u4E86\u89E3\u4ED3\u5E93\u5185\u5BB9/\u4EE3\u7801/\u63D0\u4EA4\u72B6\u6001\uFF08\u8BFB\u573A\u666F\uFF0C\u542B\u7EAF\u95EE\u7B54\uFF09**\uFF1A
@@ -5778,7 +5795,7 @@ function outboxAge(entry) {
5778
5795
  // src/daemon/plugin-register.ts
5779
5796
  import { chmodSync as chmodSync3, cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync8, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
5780
5797
  import { homedir as homedir10 } from "node:os";
5781
- import { dirname as dirname7, join as join13 } from "node:path";
5798
+ import { dirname as dirname8, join as join13 } from "node:path";
5782
5799
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5783
5800
  var PLUGIN_NAME = "morula-cli";
5784
5801
  var MANIFEST_DIR_NAME = ".codex-plugin";
@@ -5793,9 +5810,9 @@ function defaultPluginsRoot(env = process.env, home = homedir10()) {
5793
5810
  return join13(resolveDimcodeHome(env, home), "plugins");
5794
5811
  }
5795
5812
  function resolvePackagePaths(entryUrl) {
5796
- const distDir = dirname7(fileURLToPath2(entryUrl));
5813
+ const distDir = dirname8(fileURLToPath2(entryUrl));
5797
5814
  return {
5798
- packageRoot: dirname7(distDir),
5815
+ packageRoot: dirname8(distDir),
5799
5816
  distDir,
5800
5817
  assetsDir: join13(distDir, "plugin")
5801
5818
  };
@@ -5811,7 +5828,25 @@ if [ -z "$NODE" ]; then
5811
5828
  echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
5812
5829
  exit 127
5813
5830
  fi
5814
- exec "$NODE" ${JSON.stringify(entry)} "$@"
5831
+ ENTRY=${JSON.stringify(entry)}
5832
+ # \u81EA\u6108\u56DE\u9000\uFF1A\u70D8\u7119\u7684\u5B89\u88C5\u76EE\u5F55\u88AB\u6E05\u7406/\u632A\u52A8\u65F6\uFF0C\u6309 daemon \u6CE8\u5165\u7684\u540C\u6E90\u5165\u53E3\u6216\u5F53\u524D morula-runtime \u91CD\u65B0\u5B9A\u4F4D
5833
+ if [ ! -f "$ENTRY" ] && [ -n "$MORULA_CLI" ] && [ -f "$MORULA_CLI" ]; then
5834
+ ENTRY="$MORULA_CLI"
5835
+ fi
5836
+ if [ ! -f "$ENTRY" ]; then
5837
+ RT="$(command -v morula-runtime 2>/dev/null || true)"
5838
+ if [ -n "$RT" ]; then
5839
+ ALT="$("$NODE" -e 'const{realpathSync}=require("fs");const{dirname,join}=require("path");try{const p=realpathSync(process.argv[1]);process.stdout.write(join(dirname(dirname(p)),"dist","morula.js"))}catch(e){}' "$RT" 2>/dev/null || true)"
5840
+ if [ -n "$ALT" ] && [ -f "$ALT" ]; then
5841
+ ENTRY="$ALT"
5842
+ fi
5843
+ fi
5844
+ fi
5845
+ if [ ! -f "$ENTRY" ]; then
5846
+ echo '{"code":"ERROR","message":"morula.js not found: the install dir was removed or moved; fix: morula-runtime setup (or npm i -g @arcships/morula-runtime)"}' >&2
5847
+ exit 127
5848
+ fi
5849
+ exec "$NODE" "$ENTRY" "$@"
5815
5850
  `;
5816
5851
  }
5817
5852
  function assetsReady(paths) {
@@ -6068,7 +6103,7 @@ function isAcpInitRetryableError(error) {
6068
6103
  async function withAcpInitRetry(setupOnce, deps = {}) {
6069
6104
  const delays = deps.delaysMs ?? ACP_INIT_RETRY_DELAYS_MS;
6070
6105
  const isRetryable = deps.isRetryable ?? isAcpInitRetryableError;
6071
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6106
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6072
6107
  const maxAttempts = delays.length + 1;
6073
6108
  let lastError;
6074
6109
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -6083,7 +6118,7 @@ async function withAcpInitRetry(setupOnce, deps = {}) {
6083
6118
  error: lastError instanceof Error ? lastError.message : String(lastError ?? "")
6084
6119
  });
6085
6120
  if (delayMs > 0)
6086
- await sleep2(delayMs);
6121
+ await sleep(delayMs);
6087
6122
  if (deps.isAborted?.())
6088
6123
  throw new Error("\u4EFB\u52A1\u88AB\u53D6\u6D88");
6089
6124
  }
@@ -6835,8 +6870,8 @@ var DimAgentAdapter = class {
6835
6870
  }
6836
6871
  }
6837
6872
  async probe() {
6838
- const { existsSync: existsSync19 } = await import("node:fs");
6839
- const installed = existsSync19(this.dimPath) || this.dimPath === "dim";
6873
+ const { existsSync: existsSync21 } = await import("node:fs");
6874
+ const installed = existsSync21(this.dimPath) || this.dimPath === "dim";
6840
6875
  let authStatus = installed ? "unknown" : "error";
6841
6876
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
6842
6877
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -7129,7 +7164,7 @@ function pickLegacyDimModels(inventory) {
7129
7164
  // src/daemon/repo-cache.ts
7130
7165
  import { execFileSync as execFileSync8 } from "node:child_process";
7131
7166
  import { existsSync as existsSync12, mkdirSync as mkdirSync10 } from "node:fs";
7132
- import { dirname as dirname8, join as join15 } from "node:path";
7167
+ import { dirname as dirname9, join as join15 } from "node:path";
7133
7168
  function ownerOf(repoFullName) {
7134
7169
  const idx = repoFullName.indexOf("/");
7135
7170
  return idx > 0 ? repoFullName.slice(0, idx) : "_default";
@@ -7164,7 +7199,7 @@ function openRepoCache(workdir, tenantId, runtimeId, repoFullName, env, agentRem
7164
7199
  syncRemoteUrl(repoCachePath, originUrl(), env);
7165
7200
  return;
7166
7201
  }
7167
- mkdirSync10(dirname8(repoCachePath), { recursive: true });
7202
+ mkdirSync10(dirname9(repoCachePath), { recursive: true });
7168
7203
  const cloneUrl = fetchUrl();
7169
7204
  try {
7170
7205
  execFileSync8("git", ["clone", "--filter=blob:none", cloneUrl, repoCachePath], { env, stdio: "pipe" });
@@ -8282,11 +8317,333 @@ function materializeAdditionalRepo(input) {
8282
8317
  return input.targetPath;
8283
8318
  }
8284
8319
 
8320
+ // src/daemon/self-upgrade.ts
8321
+ import { spawn as spawn4 } from "node:child_process";
8322
+ import { closeSync as closeSync4, existsSync as existsSync16, mkdirSync as mkdirSync13, openSync as openSync4, writeFileSync as writeFileSync11 } from "node:fs";
8323
+ import { dirname as dirname10, join as join19 } from "node:path";
8324
+ var RUNTIME_PACKAGE = "@arcships/morula-runtime";
8325
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
8326
+ var REGISTRY_TIMEOUT_MS = 1e4;
8327
+ var NPM_INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
8328
+ var NPM_ROLLBACK_TIMEOUT_MS = 10 * 60 * 1e3;
8329
+ var RESTART_LOCK_WAIT_MS = 6e4;
8330
+ var RESTART_LIVENESS_CHECK_MS = 5e3;
8331
+ function resolveRegistry(env = process.env) {
8332
+ return (env.MORULA_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY).replace(/\/+$/, "");
8333
+ }
8334
+ function parseVersion(input) {
8335
+ if (typeof input !== "string")
8336
+ return null;
8337
+ const match = input.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
8338
+ if (!match)
8339
+ return null;
8340
+ const prerelease = match[4] ? match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part) : [];
8341
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease };
8342
+ }
8343
+ function compareVersions(a, b) {
8344
+ const pa = parseVersion(a);
8345
+ const pb = parseVersion(b);
8346
+ if (!pa || !pb)
8347
+ return a === b ? 0 : a < b ? -1 : 1;
8348
+ for (const key of ["major", "minor", "patch"]) {
8349
+ if (pa[key] !== pb[key])
8350
+ return pa[key] < pb[key] ? -1 : 1;
8351
+ }
8352
+ if (pa.prerelease.length === 0 && pb.prerelease.length === 0)
8353
+ return 0;
8354
+ if (pa.prerelease.length === 0)
8355
+ return 1;
8356
+ if (pb.prerelease.length === 0)
8357
+ return -1;
8358
+ for (let i = 0; i < Math.max(pa.prerelease.length, pb.prerelease.length); i++) {
8359
+ const sa = pa.prerelease[i];
8360
+ const sb = pb.prerelease[i];
8361
+ if (sa === void 0)
8362
+ return -1;
8363
+ if (sb === void 0)
8364
+ return 1;
8365
+ if (sa === sb)
8366
+ continue;
8367
+ if (typeof sa === "number" && typeof sb === "number")
8368
+ return sa < sb ? -1 : 1;
8369
+ if (typeof sa === "number")
8370
+ return -1;
8371
+ if (typeof sb === "number")
8372
+ return 1;
8373
+ return sa < sb ? -1 : 1;
8374
+ }
8375
+ return 0;
8376
+ }
8377
+ function isUpgradeAvailable(current, latest) {
8378
+ if (!parseVersion(current) || !parseVersion(latest))
8379
+ return false;
8380
+ return compareVersions(current, latest) < 0;
8381
+ }
8382
+ function run2(command, args, opts) {
8383
+ return new Promise((resolve2) => {
8384
+ let child;
8385
+ try {
8386
+ child = spawn4(command, args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
8387
+ } catch (error) {
8388
+ resolve2({ ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) });
8389
+ return;
8390
+ }
8391
+ let stdout = "";
8392
+ let stderr = "";
8393
+ let settled = false;
8394
+ const timer = setTimeout(() => {
8395
+ if (!settled) {
8396
+ settled = true;
8397
+ try {
8398
+ child.kill("SIGKILL");
8399
+ } catch {
8400
+ }
8401
+ resolve2({ ok: false, code: null, stdout, stderr, error: `timeout after ${Math.round(opts.timeoutMs / 1e3)}s` });
8402
+ }
8403
+ }, opts.timeoutMs);
8404
+ timer.unref?.();
8405
+ child.stdout?.on("data", (d) => {
8406
+ stdout += d.toString();
8407
+ });
8408
+ child.stderr?.on("data", (d) => {
8409
+ stderr += d.toString();
8410
+ });
8411
+ child.on("error", (error) => {
8412
+ if (settled)
8413
+ return;
8414
+ settled = true;
8415
+ clearTimeout(timer);
8416
+ resolve2({ ok: false, code: null, stdout, stderr, error: error.message });
8417
+ });
8418
+ child.on("close", (code) => {
8419
+ if (settled)
8420
+ return;
8421
+ settled = true;
8422
+ clearTimeout(timer);
8423
+ resolve2({ ok: code === 0, code, stdout, stderr });
8424
+ });
8425
+ });
8426
+ }
8427
+ async function resolveNpmCommand() {
8428
+ const nodeDir = dirname10(process.execPath);
8429
+ const candidates = [
8430
+ { command: "npm", args: [] },
8431
+ { command: join19(nodeDir, "npm"), args: [] },
8432
+ { command: process.execPath, args: [join19(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")] }
8433
+ ];
8434
+ for (const candidate of candidates) {
8435
+ const res = await run2(candidate.command, [...candidate.args, "--version"], { timeoutMs: 15e3 });
8436
+ if (res.ok && /^\d+\.\d+\.\d+/.test(res.stdout.trim()))
8437
+ return candidate;
8438
+ }
8439
+ return null;
8440
+ }
8441
+ async function resolveNpmGlobalLayout() {
8442
+ const npm = await resolveNpmCommand();
8443
+ if (!npm)
8444
+ throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 npm\uFF08\u5347\u7EA7\u9700\u8981 npm install -g\uFF1B\u8BF7\u786E\u8BA4 node/npm \u5B89\u88C5\u5B8C\u6574\uFF09");
8445
+ const prefixRes = await run2(npm.command, [...npm.args, "prefix", "-g"], { timeoutMs: 3e4 });
8446
+ const rootRes = await run2(npm.command, [...npm.args, "root", "-g"], { timeoutMs: 3e4 });
8447
+ const prefix = prefixRes.ok ? prefixRes.stdout.trim() : "";
8448
+ const nodeModulesRoot = rootRes.ok ? rootRes.stdout.trim() : "";
8449
+ if (!prefix || !nodeModulesRoot)
8450
+ throw new Error(`\u89E3\u6790 npm \u5168\u5C40\u5E03\u5C40\u5931\u8D25\uFF08prefix=${prefix || "?"} root=${nodeModulesRoot || "?"}\uFF09`);
8451
+ return {
8452
+ prefix,
8453
+ nodeModulesRoot,
8454
+ runtimePackageRoot: join19(nodeModulesRoot, RUNTIME_PACKAGE),
8455
+ runtimeBinPath: process.platform === "win32" ? join19(prefix, `${RUNTIME_PACKAGE.split("/")[1] ?? "morula-runtime"}.cmd`) : join19(prefix, "bin", "morula-runtime")
8456
+ };
8457
+ }
8458
+ async function fetchLatestRuntimeVersion(opts = {}) {
8459
+ const registry2 = opts.registry ?? resolveRegistry();
8460
+ const url = `${registry2}/${encodeURIComponent(RUNTIME_PACKAGE)}/latest`;
8461
+ const controller = new AbortController();
8462
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? REGISTRY_TIMEOUT_MS);
8463
+ timer.unref?.();
8464
+ try {
8465
+ const res = await fetch(url, { signal: controller.signal, headers: { Accept: "application/json" } });
8466
+ if (!res.ok)
8467
+ throw new Error(`registry ${res.status}`);
8468
+ const body = await res.json();
8469
+ const version = typeof body.version === "string" ? body.version.trim() : "";
8470
+ if (!version)
8471
+ throw new Error("registry \u54CD\u5E94\u7F3A\u5C11 version");
8472
+ return version;
8473
+ } catch (error) {
8474
+ const message = error instanceof Error ? error.message : String(error);
8475
+ throw new Error(`\u67E5\u8BE2 npm registry \u5931\u8D25\uFF08${url}\uFF09: ${message}`);
8476
+ } finally {
8477
+ clearTimeout(timer);
8478
+ }
8479
+ }
8480
+ async function npmContext(registry2) {
8481
+ const npm = await resolveNpmCommand();
8482
+ if (!npm)
8483
+ throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 npm\uFF08\u5347\u7EA7\u9700\u8981 npm install -g\uFF1B\u8BF7\u786E\u8BA4 node/npm \u5B89\u88C5\u5B8C\u6574\uFF09");
8484
+ const layout = await resolveNpmGlobalLayout();
8485
+ return { npm, layout };
8486
+ }
8487
+ async function npmInstallGlobal(ctx, version, registry2, timeoutMs) {
8488
+ const args = [...ctx.npm.args, "install", "-g", `${RUNTIME_PACKAGE}@${version}`];
8489
+ if (registry2 && registry2 !== DEFAULT_NPM_REGISTRY)
8490
+ args.push("--registry", registry2);
8491
+ return run2(ctx.npm.command, args, { timeoutMs });
8492
+ }
8493
+ async function verifyRuntimeInstall(binPath, expected) {
8494
+ if (!existsSync16(binPath))
8495
+ return { ok: false, version: null, reason: `bin \u4E0D\u5B58\u5728\uFF1A${binPath}` };
8496
+ const res = await run2(binPath, ["--version"], { timeoutMs: 6e4 });
8497
+ if (!res.ok)
8498
+ return { ok: false, version: null, reason: `--version \u6267\u884C\u5931\u8D25\uFF08${res.error ?? `exit ${res.code}`}\uFF09\uFF1A${res.stderr.trim().slice(0, 300) || "\u65E0\u8F93\u51FA"}` };
8499
+ try {
8500
+ const parsed = JSON.parse(res.stdout.trim());
8501
+ const version = typeof parsed.version === "string" ? parsed.version : null;
8502
+ if (!version)
8503
+ return { ok: false, version: null, reason: `--version \u8F93\u51FA\u7F3A\u5C11 version\uFF1A${res.stdout.trim().slice(0, 200)}` };
8504
+ if (expected && version !== expected)
8505
+ return { ok: false, version, reason: `\u5B89\u88C5\u540E\u7248\u672C\u4E0D\u7B26\uFF1A\u671F\u671B ${expected}\uFF0C\u5B9E\u9645 ${version}` };
8506
+ return { ok: true, version };
8507
+ } catch {
8508
+ return { ok: false, version: null, reason: `--version \u8F93\u51FA\u975E JSON\uFF1A${res.stdout.trim().slice(0, 200)}` };
8509
+ }
8510
+ }
8511
+ async function performRuntimeUpgrade(opts) {
8512
+ const log = opts.log ?? (() => {
8513
+ });
8514
+ const registry2 = opts.registry ?? resolveRegistry();
8515
+ const steps = [];
8516
+ const from = opts.currentVersion;
8517
+ let target;
8518
+ if (opts.targetVersion) {
8519
+ target = opts.targetVersion;
8520
+ } else {
8521
+ target = await fetchLatestRuntimeVersion({ registry: registry2 });
8522
+ steps.push(`registry latest: ${target}`);
8523
+ }
8524
+ if (!isUpgradeAvailable(from, target)) {
8525
+ steps.push(`\u5F53\u524D ${from} \u5DF2\u4E0D\u4F4E\u4E8E ${target}\uFF0C\u65E0\u9700\u5347\u7EA7`);
8526
+ return { status: "up_to_date", fromVersion: from, targetVersion: target, steps };
8527
+ }
8528
+ log(`[upgrade] ${from} \u2192 ${target}\uFF08${registry2}\uFF09`);
8529
+ const ctx = await npmContext(registry2);
8530
+ steps.push(`npm install -g ${RUNTIME_PACKAGE}@${target}`);
8531
+ const install = await npmInstallGlobal(ctx, target, registry2, NPM_INSTALL_TIMEOUT_MS);
8532
+ if (!install.ok) {
8533
+ const error = `\u5B89\u88C5\u5931\u8D25\uFF1A${install.error ?? `exit ${install.code}`} ${install.stderr.trim().slice(0, 300)}`.trim();
8534
+ steps.push(error);
8535
+ log(`[upgrade] ${error}`);
8536
+ const still = await verifyRuntimeInstall(ctx.layout.runtimeBinPath);
8537
+ if (still.ok) {
8538
+ steps.push(`\u539F\u7248\u672C ${still.version ?? from} \u5B89\u88C5\u5B8C\u597D\uFF0C\u4FDD\u6301\u4E0D\u53D8`);
8539
+ return { status: "failed", fromVersion: from, toVersion: null, error, steps };
8540
+ }
8541
+ const rollback = await npmInstallGlobal(ctx, from, registry2, NPM_ROLLBACK_TIMEOUT_MS);
8542
+ if (rollback.ok) {
8543
+ steps.push(`\u5DF2\u56DE\u88C5\u539F\u7248\u672C ${from}`);
8544
+ return { status: "rolled_back", fromVersion: from, toVersion: null, error, steps };
8545
+ }
8546
+ steps.push(`\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF1A${rollback.error ?? `exit ${rollback.code}`}`);
8547
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u4E14\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF0C\u5F53\u524D\u5168\u5C40\u5B89\u88C5\u53EF\u80FD\u635F\u574F\uFF08\u8BF7\u624B\u52A8\u6267\u884C npm i -g ${RUNTIME_PACKAGE}@${from}\uFF09`, steps };
8548
+ }
8549
+ const verified = await verifyRuntimeInstall(ctx.layout.runtimeBinPath, target);
8550
+ if (!verified.ok) {
8551
+ const error = `\u65B0\u5B89\u88C5\u9A8C\u8BC1\u5931\u8D25\uFF1A${verified.reason ?? "\u672A\u77E5\u539F\u56E0"}`;
8552
+ steps.push(error);
8553
+ log(`[upgrade] ${error}`);
8554
+ const rollback = await npmInstallGlobal(ctx, from, registry2, NPM_ROLLBACK_TIMEOUT_MS);
8555
+ if (rollback.ok) {
8556
+ const rollbackVerified = await verifyRuntimeInstall(ctx.layout.runtimeBinPath, from);
8557
+ if (rollbackVerified.ok) {
8558
+ steps.push(`\u5DF2\u56DE\u88C5\u539F\u7248\u672C ${from} \u5E76\u9A8C\u8BC1\u53EF\u7528`);
8559
+ return { status: "rolled_back", fromVersion: from, toVersion: null, error, steps };
8560
+ }
8561
+ steps.push(`\u56DE\u88C5\u540E\u9A8C\u8BC1\u5931\u8D25\uFF1A${rollbackVerified.reason ?? "\u672A\u77E5"}\uFF08\u8BF7\u624B\u52A8\u6267\u884C npm i -g ${RUNTIME_PACKAGE}@${from}\uFF09`);
8562
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u56DE\u88C5 ${from} \u540E\u9A8C\u8BC1\u4ECD\u5931\u8D25`, steps };
8563
+ }
8564
+ steps.push(`\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF1A${rollback.error ?? `exit ${rollback.code}`}\uFF08\u8BF7\u624B\u52A8\u6267\u884C npm i -g ${RUNTIME_PACKAGE}@${from}\uFF09`);
8565
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u4E14\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25`, steps };
8566
+ }
8567
+ steps.push(`\u65B0\u7248\u672C ${verified.version} \u9A8C\u8BC1\u901A\u8FC7`);
8568
+ const warnings = [];
8569
+ const setup = await run2(ctx.layout.runtimeBinPath, ["setup"], { timeoutMs: 6e4 });
8570
+ if (setup.ok) {
8571
+ steps.push("\u63D2\u4EF6\u76EE\u5F55\u5DF2\u91CD\u70D8\u7119\uFF08morula-runtime setup\uFF09");
8572
+ } else {
8573
+ const warning = `\u63D2\u4EF6\u76EE\u5F55\u91CD\u70D8\u7119\u5931\u8D25\uFF08${setup.error ?? `exit ${setup.code}`}\uFF09\u2014\u2014 \u4E0D\u5F71\u54CD\u672C\u6B21\u5347\u7EA7\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C morula-runtime setup`;
8574
+ warnings.push(warning);
8575
+ steps.push(warning);
8576
+ log(`[upgrade] ${warning}`);
8577
+ }
8578
+ log(`[upgrade] \u5347\u7EA7\u5B8C\u6210\uFF1A${from} \u2192 ${verified.version}`);
8579
+ return { status: "succeeded", fromVersion: from, toVersion: verified.version ?? target, warnings, steps };
8580
+ }
8581
+ async function spawnUpgradedDaemon(opts) {
8582
+ let packageRoot = opts.runtimePackageRoot;
8583
+ if (!packageRoot || !existsSync16(join19(packageRoot, "dist", "index.js"))) {
8584
+ const layout = await resolveNpmGlobalLayout();
8585
+ packageRoot = layout.runtimePackageRoot;
8586
+ }
8587
+ const entry = join19(packageRoot, "dist", "index.js");
8588
+ if (!existsSync16(entry))
8589
+ return { pid: 0, entry, error: `\u65B0\u5305\u5165\u53E3\u4E0D\u5B58\u5728\uFF1A${entry}`, waitExit: async () => ({ exited: true, code: null }) };
8590
+ mkdirSync13(dirname10(opts.logFile), { recursive: true, mode: 448 });
8591
+ let logFd;
8592
+ try {
8593
+ logFd = openSync4(opts.logFile, "a", 384);
8594
+ } catch (error) {
8595
+ return { pid: 0, entry, error: `\u65E0\u6CD5\u6253\u5F00\u65E5\u5FD7\u6587\u4EF6 ${opts.logFile}: ${error instanceof Error ? error.message : String(error)}`, waitExit: async () => ({ exited: true, code: null }) };
8596
+ }
8597
+ try {
8598
+ const child = spawn4(process.execPath, [entry, "daemon", "run"], {
8599
+ cwd: process.cwd(),
8600
+ detached: true,
8601
+ stdio: ["ignore", logFd, logFd],
8602
+ env: {
8603
+ ...opts.env,
8604
+ MORULA_DAEMON_NAME: opts.daemonConfig?.name ?? "",
8605
+ MORULA_DAEMON_WORKDIR: opts.daemonConfig?.workdir ?? "",
8606
+ MORULA_DAEMON_MAX_CONCURRENT: String(opts.daemonConfig?.maxConcurrent ?? ""),
8607
+ MORULA_DAEMON_RESTART_WAIT_MS: String(RESTART_LOCK_WAIT_MS)
8608
+ }
8609
+ });
8610
+ child.unref();
8611
+ if (!child.pid)
8612
+ return { pid: 0, entry, error: "spawn \u672A\u8FD4\u56DE pid", waitExit: async () => ({ exited: true, code: null }) };
8613
+ const exitPromise = new Promise((resolve2) => {
8614
+ child.on("exit", (code) => {
8615
+ resolve2(code ?? null);
8616
+ });
8617
+ });
8618
+ const waitExit = async (ms) => {
8619
+ return await Promise.race([
8620
+ exitPromise.then((code) => ({ exited: true, code })),
8621
+ new Promise((resolve2) => {
8622
+ const timer = setTimeout(() => {
8623
+ resolve2({ exited: false, code: null });
8624
+ }, ms);
8625
+ timer.unref?.();
8626
+ })
8627
+ ]);
8628
+ };
8629
+ mkdirSync13(dirname10(opts.pidFile), { recursive: true, mode: 448 });
8630
+ writeFileSync11(opts.pidFile, String(child.pid), { mode: 384 });
8631
+ return { pid: child.pid, entry, waitExit };
8632
+ } catch (error) {
8633
+ return { pid: 0, entry, error: error instanceof Error ? error.message : String(error), waitExit: async () => ({ exited: true, code: null }) };
8634
+ } finally {
8635
+ try {
8636
+ closeSync4(logFd);
8637
+ } catch {
8638
+ }
8639
+ }
8640
+ }
8641
+
8285
8642
  // src/daemon/session-host.ts
8286
8643
  init_dist2();
8287
8644
  init_acp_connection();
8288
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "node:fs";
8289
- import { join as join19 } from "node:path";
8645
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync12 } from "node:fs";
8646
+ import { join as join20 } from "node:path";
8290
8647
 
8291
8648
  // src/daemon/steward-instructions.ts
8292
8649
  var STEWARD_SYSTEM_INSTRUCTIONS = `\u4F60\u662F\u300C\u7A7A\u95F4\u7BA1\u5BB6\u667A\u80FD\u4F53\u300D\uFF08Project Steward\uFF09\u3002
@@ -8540,8 +8897,8 @@ var SessionHost = class {
8540
8897
  async open(cmd) {
8541
8898
  if (!cmd?.sessionId || !cmd?.tenantId || !cmd?.projectId)
8542
8899
  return { ok: false, error: "SESSION_OPEN_INVALID_ARGS" };
8543
- const workspaceDir = join19(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8544
- mkdirSync13(workspaceDir, { recursive: true });
8900
+ const workspaceDir = join20(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8901
+ mkdirSync14(workspaceDir, { recursive: true });
8545
8902
  this.writeInjections(workspaceDir, cmd);
8546
8903
  const existing = this.sessions.get(cmd.sessionId);
8547
8904
  if (existing?.conn.isAlive) {
@@ -8827,7 +9184,7 @@ var SessionHost = class {
8827
9184
  ---
8828
9185
 
8829
9186
  ${cmd.prompt}`;
8830
- const run3 = (async () => {
9187
+ const run5 = (async () => {
8831
9188
  try {
8832
9189
  const { blocks, fileRefs } = await this.materializeAttachments(managed.workspaceDir, cmd.attachments ?? []);
8833
9190
  const promptBlocks = [{ type: "text", text: fileRefs.length > 0 ? `${text}
@@ -8874,8 +9231,8 @@ ${fileRefs.join("\n")}` : text }];
8874
9231
  this.touch(managed);
8875
9232
  }
8876
9233
  })();
8877
- managed.promptInFlight = run3;
8878
- return run3;
9234
+ managed.promptInFlight = run5;
9235
+ return run5;
8879
9236
  }
8880
9237
  /**
8881
9238
  * 附件物化(§五 P2-3):
@@ -8890,7 +9247,7 @@ ${fileRefs.join("\n")}` : text }];
8890
9247
  const fileRefs = [];
8891
9248
  if (attachments.length === 0)
8892
9249
  return { blocks, fileRefs };
8893
- const dir = join19(workspaceDir, "attachments");
9250
+ const dir = join20(workspaceDir, "attachments");
8894
9251
  for (const attachment of attachments) {
8895
9252
  try {
8896
9253
  const res = await fetch(attachment.downloadUrl, { signal: AbortSignal.timeout(ATTACHMENT_FETCH_TIMEOUT_MS) });
@@ -8900,9 +9257,9 @@ ${fileRefs.join("\n")}` : text }];
8900
9257
  if (attachment.isImage) {
8901
9258
  blocks.push({ type: "image", data: bytes.toString("base64"), mimeType: attachment.fileType || "image/png" });
8902
9259
  } else {
8903
- mkdirSync13(dir, { recursive: true });
8904
- const target = join19(dir, safeAttachmentName(attachment.fileName));
8905
- writeFileSync11(target, bytes);
9260
+ mkdirSync14(dir, { recursive: true });
9261
+ const target = join20(dir, safeAttachmentName(attachment.fileName));
9262
+ writeFileSync12(target, bytes);
8906
9263
  fileRefs.push(`- ${attachment.fileName} \u2192 ${target}`);
8907
9264
  }
8908
9265
  } catch (err) {
@@ -8998,13 +9355,13 @@ ${fileRefs.join("\n")}` : text }];
8998
9355
  /** 工作区注入物(幂等覆盖;daemon 侧组装,P1-6) */
8999
9356
  writeInjections(workspaceDir, cmd) {
9000
9357
  writeProjectAgentsMd(workspaceDir, cmd.agentsMd, this.log);
9001
- const skillDir = join19(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
9002
- mkdirSync13(skillDir, { recursive: true });
9003
- writeFileSync11(join19(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
9004
- const contextDir = join19(workspaceDir, ".agent_context");
9005
- mkdirSync13(contextDir, { recursive: true });
9006
- writeFileSync11(
9007
- join19(contextDir, "steward-context.md"),
9358
+ const skillDir = join20(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
9359
+ mkdirSync14(skillDir, { recursive: true });
9360
+ writeFileSync12(join20(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
9361
+ const contextDir = join20(workspaceDir, ".agent_context");
9362
+ mkdirSync14(contextDir, { recursive: true });
9363
+ writeFileSync12(
9364
+ join20(contextDir, "steward-context.md"),
9008
9365
  buildStewardContextMd(cmd.facts ?? { projectId: cmd.projectId, tenantId: cmd.tenantId }),
9009
9366
  "utf8"
9010
9367
  );
@@ -9548,6 +9905,12 @@ var DaemonRunner = class {
9548
9905
  outboxConsecutiveFailures = 0;
9549
9906
  outboxPausedUntil = 0;
9550
9907
  processing = /* @__PURE__ */ new Set();
9908
+ /**
9909
+ * runtime-self-upgrade:远程升级执行中标志。
9910
+ * 置位期间 tick / WS 派发**不领新任务**——升级完成后 daemon 会重启,中途接的任务会被打断
9911
+ * (心跳超时 → 服务端回收重派),违背「升级不破坏正在执行的任务」的约束。
9912
+ */
9913
+ upgradeInProgress = false;
9551
9914
  /** M1-A1:任务级取消信号(WS cancelled 事件 / 状态轮询触发 abort) */
9552
9915
  cancels = /* @__PURE__ */ new Map();
9553
9916
  /** Phase 2:审批决策等待表(approvalId → resolver;WS 事件 resolve,轮询兜底) */
@@ -9689,7 +10052,10 @@ var DaemonRunner = class {
9689
10052
  models: dimModels,
9690
10053
  // LOCAL-AGENT-COLLAB-001 Phase 1:宿主协议 + Provider inventory
9691
10054
  hostProtocolVersion: 1,
9692
- hostCapabilities: { worktreeV2: true, workspaceCleanupV1: false },
10055
+ // selfUpgrade(runtime-self-upgrade 评审 P2):本 daemon 认识心跳响应里的 upgradeRequested
10056
+ // 指令并具备自升级能力。管理页据此门控远程升级按钮——旧版 daemon 会静默吞掉指令,
10057
+ // 对它显示按钮只会误导用户(能力自报比服务端维护最低版本号更准确,register 即刷新)
10058
+ hostCapabilities: { worktreeV2: true, workspaceCleanupV1: false, selfUpgrade: true },
9693
10059
  inventoryRevision: `daemon-v${BUILD_VERSION2}`,
9694
10060
  providers: inventory
9695
10061
  });
@@ -9707,10 +10073,33 @@ var DaemonRunner = class {
9707
10073
  await this.tick();
9708
10074
  if (!this.running)
9709
10075
  break;
9710
- await sleep(POLL_INTERVAL_MS);
10076
+ await this.waitForPollInterval();
9711
10077
  }
9712
10078
  this.stop();
9713
10079
  }
10080
+ /** 下一拍等待(POLL_INTERVAL_MS,可被 wakeMainLoop 提前唤醒) */
10081
+ pollWake = null;
10082
+ waitForPollInterval() {
10083
+ return new Promise((resolve2) => {
10084
+ const timer = setTimeout(() => {
10085
+ this.pollWake = null;
10086
+ resolve2();
10087
+ }, POLL_INTERVAL_MS);
10088
+ timer.unref?.();
10089
+ this.pollWake = () => {
10090
+ clearTimeout(timer);
10091
+ this.pollWake = null;
10092
+ resolve2();
10093
+ };
10094
+ });
10095
+ }
10096
+ /**
10097
+ * 唤醒主循环的 poll 等待(评审 P4 配套):远程升级完成(running=false)后立即生效,
10098
+ * 不必最多干等 15s 的 poll 间隔——旧进程越快退出释放锁,新进程越快接管,自重启窗口越小。
10099
+ */
10100
+ wakeMainLoop() {
10101
+ this.pollWake?.();
10102
+ }
9714
10103
  stop() {
9715
10104
  this.running = false;
9716
10105
  this.stopHeartbeatTimer();
@@ -9775,6 +10164,10 @@ var DaemonRunner = class {
9775
10164
  this.running = false;
9776
10165
  return;
9777
10166
  }
10167
+ if (res?.upgradeRequested) {
10168
+ this.log(`[daemon] \u6536\u5230\u670D\u52A1\u7AEF\u8FDC\u7A0B\u5347\u7EA7\u6307\u4EE4${res.upgradeVersion ? `\uFF08\u76EE\u6807 ${res.upgradeVersion}\uFF09` : ""}`);
10169
+ void this.handleRemoteUpgrade(res.upgradeVersion ?? null);
10170
+ }
9778
10171
  if (!res.ok && res.status === "unknown") {
9779
10172
  this.log("[daemon] \u672C\u8BBE\u5907\u5DF2\u5728\u670D\u52A1\u7AEF\u6CE8\u9500\uFF08runtime \u8BB0\u5F55\u4E0D\u5B58\u5728\uFF09\uFF0C\u81EA\u52A8\u9000\u51FA");
9780
10173
  this.running = false;
@@ -9793,6 +10186,95 @@ var DaemonRunner = class {
9793
10186
  this.heartbeatInFlight = false;
9794
10187
  }
9795
10188
  }
10189
+ /**
10190
+ * runtime-self-upgrade:远程升级指令执行(心跳认领后异步触发,见 sendHeartbeat)。
10191
+ *
10192
+ * 边界:
10193
+ * - 有任务在跑(processing 非空)→ 回报 busy 拒绝(指令已消耗,页面可重新下发);
10194
+ * - 空闲 → performRuntimeUpgrade(安装/验证/失败回装/重烘焙插件目录);期间心跳照常、不领新任务;
10195
+ * - 成功 → 用**新包入口** detached 拉起新 daemon(等旧进程释放单实例锁)→ 回报 → 本实例退出;
10196
+ * - 失败/回退 → 回报后**继续用原版本运行**(升级失败不中断服务)。
10197
+ */
10198
+ async handleRemoteUpgrade(targetVersion) {
10199
+ if (!this.runtimeId)
10200
+ return;
10201
+ if (this.upgradeInProgress) {
10202
+ this.log("[daemon] \u5347\u7EA7\u5DF2\u5728\u8FDB\u884C\uFF0C\u5FFD\u7565\u91CD\u590D\u6307\u4EE4");
10203
+ return;
10204
+ }
10205
+ this.upgradeInProgress = true;
10206
+ const fromVersion = BUILD_VERSION2;
10207
+ try {
10208
+ if (this.processing.size > 0) {
10209
+ const error = `${this.processing.size} \u4E2A\u4EFB\u52A1\u6267\u884C\u4E2D`;
10210
+ this.log(`[daemon] \u5347\u7EA7\u6307\u4EE4\u88AB\u62D2\u7EDD\uFF1A${error}`);
10211
+ await this.reportUpgradeResultSafe({ status: "busy", fromVersion, error });
10212
+ return;
10213
+ }
10214
+ const report = await performRuntimeUpgrade({
10215
+ currentVersion: fromVersion,
10216
+ targetVersion,
10217
+ log: (line) => this.log(line)
10218
+ });
10219
+ if (report.status === "up_to_date") {
10220
+ this.log(`[daemon] \u5F53\u524D ${fromVersion} \u5DF2\u4E0D\u4F4E\u4E8E ${report.targetVersion}\uFF0C\u65E0\u9700\u5347\u7EA7`);
10221
+ await this.reportUpgradeResultSafe({ status: "succeeded", fromVersion, toVersion: fromVersion, restart: false });
10222
+ return;
10223
+ }
10224
+ if (report.status !== "succeeded") {
10225
+ this.log(`[daemon] \u5347\u7EA7\u5931\u8D25\uFF08${report.status}\uFF09\uFF1A${report.error}\u2014\u2014\u7EE7\u7EED\u7528 ${fromVersion} \u8FD0\u884C`);
10226
+ await this.reportUpgradeResultSafe({
10227
+ status: report.status,
10228
+ fromVersion,
10229
+ toVersion: report.toVersion ?? void 0,
10230
+ error: report.error
10231
+ });
10232
+ return;
10233
+ }
10234
+ const cfg = loadDaemonConfig(this.store);
10235
+ const dir = daemonDirForEnv(process.env);
10236
+ const spawned = await spawnUpgradedDaemon({
10237
+ env: process.env,
10238
+ logFile: join21(dir, "daemon.log"),
10239
+ pidFile: join21(dir, "daemon.pid"),
10240
+ daemonConfig: { name: cfg?.name, workdir: cfg?.workdir, maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT }
10241
+ });
10242
+ if (spawned.pid <= 0) {
10243
+ const error = `\u65B0\u7248\u672C ${report.toVersion} \u5DF2\u5B89\u88C5\uFF0C\u4F46\u81EA\u52A8\u91CD\u542F\u5931\u8D25\uFF1A${spawned.error ?? "\u672A\u77E5\u539F\u56E0"}\uFF08\u672C\u5B9E\u4F8B\u7EE7\u7EED\u8FD0\u884C\u65E7\u4EE3\u7801\uFF0C\u8BF7\u624B\u52A8\u6267\u884C morula-runtime daemon start \u91CD\u542F\uFF09`;
10244
+ this.log(`[daemon] ${error}`);
10245
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, toVersion: report.toVersion, error });
10246
+ return;
10247
+ }
10248
+ this.log(`[daemon] \u65B0 daemon \u5DF2\u62C9\u8D77\uFF08pid=${spawned.pid}\uFF09\uFF0C\u89C2\u5BDF ${Math.round(RESTART_LIVENESS_CHECK_MS / 1e3)}s \u786E\u8BA4\u63A5\u7BA1\u2026`);
10249
+ const liveness = await spawned.waitExit(RESTART_LIVENESS_CHECK_MS);
10250
+ if (liveness.exited) {
10251
+ const error = `\u65B0\u7248\u672C ${report.toVersion} \u5DF2\u5B89\u88C5\uFF0C\u4F46\u65B0 daemon \u8FDB\u7A0B\u7ACB\u5373\u9000\u51FA\uFF08code=${liveness.code ?? "\u672A\u77E5"}\uFF0C\u8BE6\u89C1 ${join21(dir, "daemon.log")}\uFF09\u2014\u2014\u672C\u5B9E\u4F8B\u7EE7\u7EED\u8FD0\u884C\u65E7\u4EE3\u7801\uFF0C\u8BF7\u68C0\u67E5\u65E5\u5FD7\u540E\u624B\u52A8 morula-runtime daemon start`;
10252
+ this.log(`[daemon] ${error}`);
10253
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, toVersion: report.toVersion, error });
10254
+ return;
10255
+ }
10256
+ this.log("[daemon] \u65B0\u8FDB\u7A0B\u8FD0\u884C\u4E2D\uFF08\u7B49\u672C\u5B9E\u4F8B\u9000\u51FA\u91CA\u653E\u9501\u540E\u63A5\u7BA1\uFF09\uFF0C\u56DE\u62A5\u6210\u529F\u5E76\u9000\u51FA");
10257
+ await this.reportUpgradeResultSafe({ status: "succeeded", fromVersion, toVersion: report.toVersion, restart: true });
10258
+ this.running = false;
10259
+ this.wakeMainLoop();
10260
+ } catch (error) {
10261
+ const message = error instanceof Error ? error.message : String(error);
10262
+ this.log(`[daemon] \u81EA\u5347\u7EA7\u5F02\u5E38\uFF1A${message}\u2014\u2014\u7EE7\u7EED\u7528 ${fromVersion} \u8FD0\u884C`);
10263
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, error: message });
10264
+ } finally {
10265
+ this.upgradeInProgress = false;
10266
+ }
10267
+ }
10268
+ /** 升级结果回报(best-effort:服务端不可达不影响本地升级流程) */
10269
+ async reportUpgradeResultSafe(body) {
10270
+ if (!this.runtimeId)
10271
+ return;
10272
+ try {
10273
+ await this.api.reportUpgradeResult(this.runtimeId, body);
10274
+ } catch (error) {
10275
+ this.log(`[daemon] \u5347\u7EA7\u7ED3\u679C\u56DE\u62A5\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u5730\u6D41\u7A0B\uFF09: ${error instanceof Error ? error.message : String(error)}`);
10276
+ }
10277
+ }
9796
10278
  /**
9797
10279
  * tick:poll 兜底 + 取消轮询 + cleanup 补拉(心跳与 outbox 补投均已独立于本循环)。
9798
10280
  * D1:整轮加时间预算——超预算记 WARN 并丢弃本轮等待下一拍,避免单个慢请求把 tick 拖死。
@@ -9821,6 +10303,8 @@ var DaemonRunner = class {
9821
10303
  async tickOnce() {
9822
10304
  if (!this.runtimeId)
9823
10305
  return;
10306
+ if (this.upgradeInProgress)
10307
+ return;
9824
10308
  try {
9825
10309
  const tasks = await this.api.pollTasks(this.runtimeId);
9826
10310
  for (const task of tasks) {
@@ -10220,6 +10704,10 @@ var DaemonRunner = class {
10220
10704
  async handleTask(taskId) {
10221
10705
  if (this.processing.has(taskId))
10222
10706
  return;
10707
+ if (this.upgradeInProgress) {
10708
+ this.log(`[daemon] \u5347\u7EA7\u8FDB\u884C\u4E2D\uFF0C\u5FFD\u7565\u4EFB\u52A1\u6D3E\u53D1 task=${taskId}`);
10709
+ return;
10710
+ }
10223
10711
  this.processing.add(taskId);
10224
10712
  const ctrl = new AbortController();
10225
10713
  this.cancels.set(taskId, ctrl);
@@ -10489,12 +10977,12 @@ var DaemonRunner = class {
10489
10977
  });
10490
10978
  if (spaceToken?.token)
10491
10979
  env = { ...env, ...buildRepoEnv(spaceToken.token, spaceToken.baseUrl) };
10492
- repoDir = join20(cfg.workdir, "scratch", taskId);
10980
+ repoDir = join21(cfg.workdir, "scratch", taskId);
10493
10981
  taskRoot = repoDir;
10494
- mkdirSync14(repoDir, { recursive: true });
10495
- const readme = join20(repoDir, "README.md");
10496
- if (!existsSync16(readme)) {
10497
- writeFileSync12(
10982
+ mkdirSync15(repoDir, { recursive: true });
10983
+ const readme = join21(repoDir, "README.md");
10984
+ if (!existsSync17(readme)) {
10985
+ writeFileSync13(
10498
10986
  readme,
10499
10987
  `# \u9694\u79BB\u5DE5\u4F5C\u533A\uFF08\u4EFB\u52A1 ${taskId}\uFF09
10500
10988
 
@@ -10792,7 +11280,7 @@ var DaemonRunner = class {
10792
11280
  }, { log: (line) => this.log(`[daemon] ${line}`) });
10793
11281
  const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
10794
11282
  const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
10795
- const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync16(resumeWorkDir);
11283
+ const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync17(resumeWorkDir);
10796
11284
  const input = {
10797
11285
  executionId: `${taskId}:1`,
10798
11286
  // task-workspace-layout:新会话 cwd = 任务工作区根(AGENTS.md/.agents 在根下,仓库在子目录)
@@ -11053,7 +11541,7 @@ var DaemonRunner = class {
11053
11541
  await this.api.submitCleanupResult(item.id, this.runtimeId, { ok: true });
11054
11542
  return;
11055
11543
  }
11056
- const managedRoot = join20(cfg.workdir, "worktrees", this.tenantId);
11544
+ const managedRoot = join21(cfg.workdir, "worktrees", this.tenantId);
11057
11545
  if (!wc.worktreePath.startsWith(`${managedRoot}/`)) {
11058
11546
  await this.api.submitCleanupResult(item.id, this.runtimeId, {
11059
11547
  ok: false,
@@ -11092,47 +11580,44 @@ var DaemonRunner = class {
11092
11580
  }
11093
11581
  }
11094
11582
  };
11095
- function sleep(ms) {
11096
- return new Promise((r) => setTimeout(r, ms));
11097
- }
11098
11583
  function isDebugLogEnabled() {
11099
11584
  return isFlagEnabled("MORULA_DAEMON_DEBUG_LOG", false);
11100
11585
  }
11101
11586
  var spaceTokenFailureLogged = false;
11102
11587
  function writeCustomSkills(targetDir, skills, log) {
11103
11588
  const NAME_RE = /^[\w-]{1,64}$/;
11104
- const base = join20(targetDir, ".agents", "skills");
11589
+ const base = join21(targetDir, ".agents", "skills");
11105
11590
  for (const skill of skills) {
11106
11591
  const name = skill.name.trim();
11107
11592
  if (!NAME_RE.test(name)) {
11108
11593
  log?.(`[daemon] \u8DF3\u8FC7\u975E\u6CD5 skill \u540D\u79F0: ${JSON.stringify(name)}`);
11109
11594
  continue;
11110
11595
  }
11111
- const dir = join20(base, name);
11112
- if (dirname9(dir) !== base) {
11596
+ const dir = join21(base, name);
11597
+ if (dirname11(dir) !== base) {
11113
11598
  log?.(`[daemon] \u8DF3\u8FC7\u8D8A\u754C skill \u8DEF\u5F84: ${JSON.stringify(name)}`);
11114
11599
  continue;
11115
11600
  }
11116
- mkdirSync14(dir, { recursive: true });
11601
+ mkdirSync15(dir, { recursive: true });
11117
11602
  const meta = ["---", `name: ${name}`];
11118
11603
  if (skill.description?.trim()) {
11119
11604
  meta.push(`description: ${skill.description.trim().replace(/\n/g, " ")}`);
11120
11605
  }
11121
11606
  meta.push("---", "");
11122
- writeFileSync12(join20(dir, "SKILL.md"), `${meta.join("\n")}
11607
+ writeFileSync13(join21(dir, "SKILL.md"), `${meta.join("\n")}
11123
11608
  ${skill.content ?? ""}`, "utf8");
11124
11609
  }
11125
11610
  }
11126
11611
  function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
11127
- const dir = join20(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
11128
- const refsDir = join20(dir, "references");
11129
- mkdirSync14(refsDir, { recursive: true });
11612
+ const dir = join21(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
11613
+ const refsDir = join21(dir, "references");
11614
+ mkdirSync15(refsDir, { recursive: true });
11130
11615
  const isGitlab = provider === "gitlab" && !!repoBaseUrl;
11131
11616
  const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
11132
11617
  const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
11133
- writeFileSync12(join20(dir, "SKILL.md"), `${front}
11618
+ writeFileSync13(join21(dir, "SKILL.md"), `${front}
11134
11619
  ${body}`, "utf8");
11135
- writeFileSync12(join20(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
11620
+ writeFileSync13(join21(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
11136
11621
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u534F\u4F5C\u5951\u7EA6 skill ${COLLABORATION_SKILL_NAME}\uFF08${isGitlab ? "GitLab" : "GitHub"} \u7248\uFF0C\u4ED3\u5E93=${repo ?? "\u65E0"}\uFF09\u5230 .agents/skills/`);
11137
11622
  }
11138
11623
  function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
@@ -11163,8 +11648,8 @@ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundB
11163
11648
  };
11164
11649
  }
11165
11650
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
11166
- const briefDir = join20(targetDir, ".agent_context");
11167
- mkdirSync14(briefDir, { recursive: true });
11651
+ const briefDir = join21(targetDir, ".agent_context");
11652
+ mkdirSync15(briefDir, { recursive: true });
11168
11653
  const brief = `# \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08Task Context\uFF09
11169
11654
 
11170
11655
  - \u672C AgentTask \u4EFB\u52A1 ID\uFF1A\`${taskId}\`
@@ -11174,7 +11659,7 @@ function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
11174
11659
  ---
11175
11660
 
11176
11661
  ${buildTaskPromptWithContract(detail, void 0)}`;
11177
- writeFileSync12(join20(briefDir, "task-context.md"), brief, "utf8");
11662
+ writeFileSync13(join21(briefDir, "task-context.md"), brief, "utf8");
11178
11663
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u4EFB\u52A1\u4E0A\u4E0B\u6587 brief \u5230 .agent_context/task-context.md`);
11179
11664
  }
11180
11665
  var PROJECT_AGENTS_MD_MAX_CHARS = 1e5;
@@ -11188,8 +11673,8 @@ function writeProjectAgentsMd(targetDir, agentsMd, log) {
11188
11673
  log?.(`[daemon] \u7A7A\u95F4 AGENTS.md \u5185\u5BB9\u8D85\u957F\uFF08${content.length} \u5B57\u7B26 > ${PROJECT_AGENTS_MD_MAX_CHARS}\uFF09\uFF0C\u5DF2\u622A\u65AD\u5199\u5165`);
11189
11674
  content = content.slice(0, PROJECT_AGENTS_MD_MAX_CHARS);
11190
11675
  }
11191
- mkdirSync14(targetDir, { recursive: true });
11192
- writeFileSync12(join20(targetDir, "AGENTS.md"), `${content}
11676
+ mkdirSync15(targetDir, { recursive: true });
11677
+ writeFileSync13(join21(targetDir, "AGENTS.md"), `${content}
11193
11678
  `, "utf8");
11194
11679
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u7A7A\u95F4\u9879\u76EE\u8BF4\u660E\u5230 AGENTS.md\uFF08${content.length} \u5B57\u7B26\uFF0C\u4F5C\u4E3A project.instructions \u81EA\u52A8\u52A0\u8F7D\uFF09`);
11195
11680
  return true;
@@ -11248,7 +11733,7 @@ async function reportRepoTargets(taskId, targets, env, ops) {
11248
11733
  }
11249
11734
  async function reportOneRepoTarget(taskId, target, env, ops) {
11250
11735
  const execEnv = mergeExecEnv(env);
11251
- if (!existsSync16(target.dir) || !existsSync16(join20(target.dir, ".git"))) {
11736
+ if (!existsSync17(target.dir) || !existsSync17(join21(target.dir, ".git"))) {
11252
11737
  ops.log(`[daemon] \u4EFB\u52A1 ${taskId} \u4ED3\u5E93 ${target.repoFullName} \u4EA4\u4ED8\u8DF3\u8FC7\uFF1A\u5DE5\u4F5C\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728\uFF08cleanup/\u5206\u652F\u6539\u540D\u65F6\u5E8F\uFF09`);
11253
11738
  return;
11254
11739
  }
@@ -11416,9 +11901,9 @@ Task ref: ${taskCode}\uFF08\u7531 morula \u81EA\u52A8\u8865\u5145\uFF0C\u7528\u4
11416
11901
 
11417
11902
  // src/commands/doctor.ts
11418
11903
  init_dist2();
11419
- import { existsSync as existsSync17, readFileSync as readFileSync11, realpathSync } from "node:fs";
11904
+ import { existsSync as existsSync18, readFileSync as readFileSync11, realpathSync } from "node:fs";
11420
11905
  import { homedir as homedir12 } from "node:os";
11421
- import { dirname as dirname10, join as join21 } from "node:path";
11906
+ import { dirname as dirname12, join as join22 } from "node:path";
11422
11907
  init_daemon_config();
11423
11908
  init_daemon_lock();
11424
11909
  init_dim_locate();
@@ -11438,16 +11923,16 @@ async function doctorCommand(context) {
11438
11923
  async function collectDoctor(context, probe) {
11439
11924
  const checks = [];
11440
11925
  const env = context.env;
11441
- const configDir = env.MORULA_CONFIG_DIR ?? join21(homedir12(), ".morula-cli");
11442
- const configPath = env.MORULA_CONFIG ?? join21(configDir, "config.yaml");
11443
- const daemonPath = join21(dirname10(configPath), "daemon");
11444
- const logFile = join21(daemonPath, "daemon.log");
11926
+ const configDir = env.MORULA_CONFIG_DIR ?? join22(homedir12(), ".morula-cli");
11927
+ const configPath = env.MORULA_CONFIG ?? join22(configDir, "config.yaml");
11928
+ const daemonPath = join22(dirname12(configPath), "daemon");
11929
+ const logFile = join22(daemonPath, "daemon.log");
11445
11930
  const contextName = context.runtime.context ?? null;
11446
11931
  const baseUrl = context.runtime.baseUrl ?? null;
11447
11932
  const tokenPresent = Boolean(context.runtime.token);
11448
11933
  const tokenSource = context.runtime.tokenSource ?? null;
11449
11934
  let configReadable = false;
11450
- if (existsSync17(configPath)) {
11935
+ if (existsSync18(configPath)) {
11451
11936
  try {
11452
11937
  readFileSync11(configPath, "utf8");
11453
11938
  configReadable = true;
@@ -11455,9 +11940,9 @@ async function collectDoctor(context, probe) {
11455
11940
  configReadable = false;
11456
11941
  }
11457
11942
  }
11458
- const lockPath = join21(daemonPath, DAEMON_LOCK_FILE);
11943
+ const lockPath = join22(daemonPath, DAEMON_LOCK_FILE);
11459
11944
  const lock = probeDaemonLock(lockPath);
11460
- const pidFile = join21(daemonPath, DAEMON_PID_FILE);
11945
+ const pidFile = join22(daemonPath, DAEMON_PID_FILE);
11461
11946
  const filePid = readPidFile(pidFile);
11462
11947
  const daemonPid = probeDaemonInstance(configDir).pid;
11463
11948
  const cfg = loadDaemonConfig(context.store);
@@ -11509,8 +11994,8 @@ async function collectDoctor(context, probe) {
11509
11994
  hint: "morula-runtime auth login"
11510
11995
  });
11511
11996
  } else if (probe) {
11512
- const check = await probeAuth(context);
11513
- checks.push(check);
11997
+ const check2 = await probeAuth(context);
11998
+ checks.push(check2);
11514
11999
  } else {
11515
12000
  checks.push({
11516
12001
  id: "auth.token",
@@ -11519,7 +12004,7 @@ async function collectDoctor(context, probe) {
11519
12004
  hint: "morula-runtime daemon doctor --probe\uFF08\u8054\u7F51\u786E\u8BA4\u767B\u5F55\u6001\uFF09"
11520
12005
  });
11521
12006
  }
11522
- checks.push(configReadable ? { id: "config.readable", status: "ok", message: `\u914D\u7F6E\u53EF\u8BFB\uFF1A${configPath}` } : existsSync17(configPath) ? { id: "config.readable", status: "fail", message: `\u914D\u7F6E\u5B58\u5728\u4F46\u4E0D\u53EF\u8BFB\uFF08\u6743\u9650\uFF1F\uFF09\uFF1A${configPath}`, hint: `ls -l ${configPath}` } : { id: "config.readable", status: "warn", message: `\u914D\u7F6E\u6587\u4EF6\u5C1A\u4E0D\u5B58\u5728\uFF1A${configPath}`, hint: "morula-runtime auth login\uFF08\u767B\u5F55\u540E\u751F\u6210\uFF09" });
12007
+ checks.push(configReadable ? { id: "config.readable", status: "ok", message: `\u914D\u7F6E\u53EF\u8BFB\uFF1A${configPath}` } : existsSync18(configPath) ? { id: "config.readable", status: "fail", message: `\u914D\u7F6E\u5B58\u5728\u4F46\u4E0D\u53EF\u8BFB\uFF08\u6743\u9650\uFF1F\uFF09\uFF1A${configPath}`, hint: `ls -l ${configPath}` } : { id: "config.readable", status: "warn", message: `\u914D\u7F6E\u6587\u4EF6\u5C1A\u4E0D\u5B58\u5728\uFF1A${configPath}`, hint: "morula-runtime auth login\uFF08\u767B\u5F55\u540E\u751F\u6210\uFF09" });
11523
12008
  if (!dimFound) {
11524
12009
  checks.push({
11525
12010
  id: "dim.present",
@@ -11626,19 +12111,13 @@ function detectInstallMode() {
11626
12111
  }
11627
12112
 
11628
12113
  // src/commands/daemon.ts
11629
- function configFilePath(env) {
11630
- return env.MORULA_CONFIG ?? join22(env.MORULA_CONFIG_DIR ?? join22(homedir13(), ".morula-cli"), "config.yaml");
11631
- }
11632
- function daemonDir(configPath) {
11633
- return join22(dirname11(configPath), "daemon");
11634
- }
11635
12114
  var PID_FILE = DAEMON_PID_FILE;
11636
12115
  var LOG_FILE = "daemon.log";
11637
12116
  var DAEMON_EXIT_FALLBACK_MS = 3e3;
11638
12117
  async function daemonCommand(context) {
11639
12118
  const { action } = context.parsed;
11640
12119
  if (action === "run")
11641
- return run2(context);
12120
+ return run3(context);
11642
12121
  if (action === "start")
11643
12122
  return start(context);
11644
12123
  if (action === "stop")
@@ -11815,7 +12294,7 @@ async function models(context) {
11815
12294
  contextWindow: m.contextWindow
11816
12295
  }));
11817
12296
  }
11818
- async function run2(context) {
12297
+ async function run3(context) {
11819
12298
  rejectUnknownFlags(context.parsed.flags, ["name", "workdir", "max-concurrent"]);
11820
12299
  const armBoundedExit = () => {
11821
12300
  setTimeout(() => process.exit(0), DAEMON_EXIT_FALLBACK_MS).unref();
@@ -11828,9 +12307,18 @@ async function run2(context) {
11828
12307
  saveDaemonConfig(context.store, { name, workdir, maxConcurrent });
11829
12308
  }
11830
12309
  const resolvedWorkdir = workdir ?? loadDaemonConfig(context.store)?.workdir ?? DEFAULT_WORKDIR;
11831
- mkdirSync15(resolvedWorkdir, { recursive: true });
11832
- const lockPath = join22(daemonDir(configFilePath(context.env)), DAEMON_LOCK_FILE);
11833
- const acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12310
+ mkdirSync16(resolvedWorkdir, { recursive: true });
12311
+ const lockPath = join23(daemonDirForEnv(context.env), DAEMON_LOCK_FILE);
12312
+ let acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12313
+ const restartWaitMs = Number(context.env.MORULA_DAEMON_RESTART_WAIT_MS ?? 0) || 0;
12314
+ if (!acquired.ok && restartWaitMs > 0) {
12315
+ const deadline = Date.now() + restartWaitMs;
12316
+ context.err(`[daemon] \u81EA\u5347\u7EA7\u91CD\u542F\uFF1A\u7B49\u5F85\u65E7\u5B9E\u4F8B\u91CA\u653E\u5355\u5B9E\u4F8B\u9501\uFF08\u6700\u591A ${Math.round(restartWaitMs / 1e3)}s\uFF09`);
12317
+ while (!acquired.ok && Date.now() < deadline) {
12318
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
12319
+ acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12320
+ }
12321
+ }
11834
12322
  if (!acquired.ok) {
11835
12323
  const pid = acquired.existing?.pid;
11836
12324
  if (context.env.MORULA_DAEMON_SUPERVISED === "1") {
@@ -11856,7 +12344,7 @@ async function run2(context) {
11856
12344
  log: (line) => context.err(line),
11857
12345
  // dim acp 子进程治理(issues/dim-acp-process-leak-shared-space-contention.md):
11858
12346
  // spawn 记录落盘(供 daemon 重启后的启动自愈认领自己的孤儿)+ 无归属子进程周期巡检
11859
- acpChildRecordFile: join22(daemonDir(configFilePath(context.env)), "dim-acp-children.json"),
12347
+ acpChildRecordFile: join23(daemonDirForEnv(context.env), "dim-acp-children.json"),
11860
12348
  // D6:认证失败时重新读 config 换票(不传则退回「启动即快照」旧行为)。
11861
12349
  // 只重算凭证,不重建 client——WS 用返回值更新 socket.auth 后重连。
11862
12350
  resolveToken: () => {
@@ -11907,12 +12395,12 @@ async function start(context) {
11907
12395
  throw error;
11908
12396
  identityError = error instanceof Error ? error.message : String(error);
11909
12397
  }
11910
- const dir = daemonDir(configFilePath(context.env));
12398
+ const dir = daemonDirForEnv(context.env);
11911
12399
  if (!dryRun)
11912
- mkdirSync15(dir, { recursive: true, mode: 448 });
11913
- const pidFile = join22(dir, PID_FILE);
11914
- const logFile = join22(dir, LOG_FILE);
11915
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12400
+ mkdirSync16(dir, { recursive: true, mode: 448 });
12401
+ const pidFile = join23(dir, PID_FILE);
12402
+ const logFile = join23(dir, LOG_FILE);
12403
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
11916
12404
  const lock = probeDaemonLock(lockPath);
11917
12405
  if (lock.state === "active") {
11918
12406
  throw new CliError(
@@ -11923,7 +12411,7 @@ async function start(context) {
11923
12411
  { hint: "morula-runtime daemon status" }
11924
12412
  );
11925
12413
  }
11926
- if (existsSync18(pidFile)) {
12414
+ if (existsSync19(pidFile)) {
11927
12415
  const pid = Number(readFileSync12(pidFile, "utf8").trim());
11928
12416
  if (isProcessAlive(pid)) {
11929
12417
  throw new CliError("ALREADY_RUNNING", EXIT.CONFLICT, `daemon \u5DF2\u5728\u8FD0\u884C\uFF08pid=${pid}\uFF09\uFF1B\u5982\u9700\u5F3A\u5236\u91CD\u542F\u8BF7\u5148\u6267\u884C kill -9 ${pid}`, false, {
@@ -11974,19 +12462,19 @@ async function start(context) {
11974
12462
  hint: `tail -n 50 ${logFile}`
11975
12463
  });
11976
12464
  }
11977
- writeFileSync13(pidFile, String(result.pid), { mode: 384 });
12465
+ writeFileSync14(pidFile, String(result.pid), { mode: 384 });
11978
12466
  context.err(`[daemon] \u5DF2\u540E\u53F0\u542F\u52A8\uFF08pid=${result.pid}\uFF09\uFF0C\u65E5\u5FD7: ${logFile}`);
11979
12467
  return { running: true, pid: result.pid, logFile, lockFile: lockPath };
11980
12468
  }
11981
12469
  async function stop(context) {
11982
12470
  rejectUnknownFlags(context.parsed.flags, ["dry-run"]);
11983
12471
  const dryRun = isDryRun(context);
11984
- const dir = daemonDir(configFilePath(context.env));
11985
- const pidFile = join22(dir, PID_FILE);
11986
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12472
+ const dir = daemonDirForEnv(context.env);
12473
+ const pidFile = join23(dir, PID_FILE);
12474
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
11987
12475
  const lock = probeDaemonLock(lockPath);
11988
12476
  const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
11989
- const filePid = existsSync18(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : null;
12477
+ const filePid = existsSync19(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : null;
11990
12478
  const pid = [lockPid, filePid].find((p) => p !== null && isProcessAlive(p)) ?? null;
11991
12479
  if (dryRun) {
11992
12480
  return {
@@ -12013,9 +12501,9 @@ async function stop(context) {
12013
12501
  async function status2(context) {
12014
12502
  rejectUnknownFlags(context.parsed.flags, []);
12015
12503
  const cfg = loadDaemonConfig(context.store);
12016
- const dir = daemonDir(configFilePath(context.env));
12017
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12018
- const probe = probeDaemonInstance(dirname11(configFilePath(context.env)));
12504
+ const dir = daemonDirForEnv(context.env);
12505
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
12506
+ const probe = probeDaemonInstance(dirname13(configFilePathForEnv(context.env)));
12019
12507
  return {
12020
12508
  running: probe.running,
12021
12509
  pid: probe.pid,
@@ -12024,7 +12512,7 @@ async function status2(context) {
12024
12512
  name: cfg?.name ?? null,
12025
12513
  workdir: cfg?.workdir ?? DEFAULT_WORKDIR,
12026
12514
  maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
12027
- logFile: join22(dir, LOG_FILE),
12515
+ logFile: join23(dir, LOG_FILE),
12028
12516
  lockFile: lockPath
12029
12517
  };
12030
12518
  }
@@ -12081,11 +12569,228 @@ async function setupCommand(context) {
12081
12569
  };
12082
12570
  }
12083
12571
 
12572
+ // src/commands/upgrade.ts
12573
+ init_dist2();
12574
+ import { existsSync as existsSync20, mkdirSync as mkdirSync17, readFileSync as readFileSync13, rmSync as rmSync11 } from "node:fs";
12575
+ import { join as join24 } from "node:path";
12576
+ init_daemon_config();
12577
+ init_daemon_lock();
12578
+ var PID_FILE2 = DAEMON_PID_FILE;
12579
+ var LOG_FILE2 = "daemon.log";
12580
+ async function upgradeCommand(context) {
12581
+ const { action } = context.parsed;
12582
+ if (action === "check")
12583
+ return check(context);
12584
+ if (action === "run")
12585
+ return run4(context);
12586
+ throw usage("usage: morula-runtime upgrade <check|run>", {
12587
+ hint: "morula-runtime upgrade --help"
12588
+ });
12589
+ }
12590
+ async function check(context) {
12591
+ rejectUnknownFlags(context.parsed.flags, ["registry"]);
12592
+ const registry2 = flagString(context.parsed.flags, "registry") ?? resolveRegistry(context.env);
12593
+ let latest = null;
12594
+ let error = null;
12595
+ try {
12596
+ latest = await fetchLatestRuntimeVersion({ registry: registry2 });
12597
+ } catch (e) {
12598
+ error = e instanceof Error ? e.message : String(e);
12599
+ }
12600
+ if (latest === null) {
12601
+ context.setExitCode(EXIT.UPSTREAM);
12602
+ return {
12603
+ ok: false,
12604
+ current: BUILD_VERSION2,
12605
+ latest: null,
12606
+ updateAvailable: null,
12607
+ registry: registry2,
12608
+ error,
12609
+ hint: "\u7F51\u7EDC\u4E0D\u53EF\u8FBE\u6216 registry \u4E0D\u53EF\u7528\uFF1B\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u76F4\u63A5\u6267\u884C morula-runtime upgrade run --yes\uFF08run \u4F1A\u81EA\u884C\u89E3\u6790\u76EE\u6807\u7248\u672C\uFF09"
12610
+ };
12611
+ }
12612
+ const updateAvailable = isUpgradeAvailable(BUILD_VERSION2, latest);
12613
+ return {
12614
+ ok: true,
12615
+ current: BUILD_VERSION2,
12616
+ latest,
12617
+ updateAvailable,
12618
+ registry: registry2,
12619
+ ...updateAvailable ? { hint: "morula-runtime upgrade run --yes\uFF08\u5347\u7EA7\u4F1A\u5148\u786E\u8BA4\u65E0\u4EFB\u52A1\u6267\u884C\u4E2D\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u88C5\u539F\u7248\u672C\uFF09" } : {}
12620
+ };
12621
+ }
12622
+ function probeDaemonPid(env) {
12623
+ const dir = daemonDirForEnv(env);
12624
+ const pidFile = join24(dir, PID_FILE2);
12625
+ const lockPath = join24(dir, DAEMON_LOCK_FILE);
12626
+ const lock = probeDaemonLock(lockPath);
12627
+ const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
12628
+ const filePid = existsSync20(pidFile) ? Number(readFileSync13(pidFile, "utf8").trim()) : null;
12629
+ const pid = [lockPid, filePid].find((p) => p !== null && isProcessAlive(p)) ?? null;
12630
+ return { pid, pidFile, lockPath, lockState: lock.state };
12631
+ }
12632
+ async function reportUpgradeResult(context, body) {
12633
+ const cfg = loadDaemonConfig(context.store);
12634
+ if (!context.runtime.token || !cfg?.runtimeId)
12635
+ return;
12636
+ try {
12637
+ await context.client.http.post(`/agent-runtimes/${cfg.runtimeId}/upgrade-result`, body);
12638
+ } catch (error) {
12639
+ context.err(`[upgrade] \u5347\u7EA7\u7ED3\u679C\u56DE\u62A5\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u5730\u7ED3\u679C\uFF09: ${error instanceof Error ? error.message : String(error)}`);
12640
+ }
12641
+ }
12642
+ async function countActiveTasks(context, runtimeId) {
12643
+ const res = await context.client.http.get(
12644
+ "/agent-tasks",
12645
+ { runtimeId, statusGroup: "active", page: 1, pageSize: 100 }
12646
+ );
12647
+ return (res?.data ?? []).filter((t) => ["dispatched", "running", "awaiting_approval"].includes(t.status)).length;
12648
+ }
12649
+ async function run4(context) {
12650
+ rejectUnknownFlags(context.parsed.flags, ["yes", "version", "registry"]);
12651
+ const yes = flagBoolean(context.parsed.flags, "yes");
12652
+ const targetVersion = flagString(context.parsed.flags, "version") ?? null;
12653
+ const registry2 = flagString(context.parsed.flags, "registry") ?? resolveRegistry(context.env);
12654
+ if (!yes) {
12655
+ throw usage("upgrade run \u9700\u8981 --yes \u663E\u5F0F\u786E\u8BA4\uFF08\u5148\u8DD1 morula-runtime upgrade check \u67E5\u770B\u5DEE\u8DDD\uFF09", {
12656
+ hint: "morula-runtime upgrade run --yes"
12657
+ });
12658
+ }
12659
+ const dir = daemonDirForEnv(context.env);
12660
+ const pidFile = join24(dir, PID_FILE2);
12661
+ const lockPath = join24(dir, DAEMON_LOCK_FILE);
12662
+ const probe = probeDaemonPid(context.env);
12663
+ const wasRunning = probe.pid !== null;
12664
+ const cfgForSafety = loadDaemonConfig(context.store);
12665
+ let activeTasks = 0;
12666
+ if (wasRunning && cfgForSafety?.runtimeId) {
12667
+ if (!context.runtime.token) {
12668
+ throw new CliError(
12669
+ "TASKS_UNKNOWN",
12670
+ EXIT.UNAUTHENTICATED,
12671
+ "daemon \u8FD0\u884C\u4E2D\u4F46\u5F53\u524D\u672A\u767B\u5F55\uFF0C\u65E0\u6CD5\u786E\u8BA4\u662F\u5426\u6709\u4EFB\u52A1\u6267\u884C\u4E2D\uFF0C\u5DF2\u62D2\u7EDD\u5347\u7EA7",
12672
+ false,
12673
+ { hint: "morula-runtime auth login \u540E\u91CD\u8BD5\uFF1B\u6216\u786E\u8BA4\u53EF\u4E2D\u65AD\u540E\u5148 morula-runtime daemon stop" }
12674
+ );
12675
+ }
12676
+ try {
12677
+ activeTasks = await countActiveTasks(context, cfgForSafety.runtimeId);
12678
+ } catch (error) {
12679
+ throw new CliError(
12680
+ "TASKS_UNKNOWN",
12681
+ EXIT.UPSTREAM,
12682
+ `daemon \u8FD0\u884C\u4E2D\u4E14\u65E0\u6CD5\u786E\u8BA4\u4EFB\u52A1\u72B6\u6001\uFF08${error instanceof Error ? error.message : String(error)}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5347\u7EA7`,
12683
+ false,
12684
+ { hint: "\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u5148 morula-runtime daemon stop \u518D\u5347\u7EA7\uFF08\u5347\u7EA7\u540E daemon \u4E0D\u4F1A\u81EA\u52A8\u62C9\u8D77\uFF09" }
12685
+ );
12686
+ }
12687
+ }
12688
+ if (activeTasks > 0) {
12689
+ throw new CliError(
12690
+ "TASKS_RUNNING",
12691
+ EXIT.CONFLICT,
12692
+ `daemon \u6709 ${activeTasks} \u4E2A\u4EFB\u52A1\u6267\u884C\u4E2D\uFF0C\u62D2\u7EDD\u5347\u7EA7\uFF08\u5347\u7EA7\u4F1A\u7834\u574F\u6B63\u5728\u6267\u884C\u7684\u4EFB\u52A1\uFF09`,
12693
+ false,
12694
+ { hint: "\u7B49\u4EFB\u52A1\u5B8C\u6210\u540E\u518D\u5347\u7EA7\uFF1B\u6216\u786E\u8BA4\u53EF\u4E2D\u65AD\u540E\u5148 morula-runtime daemon stop" }
12695
+ );
12696
+ }
12697
+ const report = await performRuntimeUpgrade({
12698
+ currentVersion: BUILD_VERSION2,
12699
+ targetVersion,
12700
+ registry: registry2,
12701
+ log: (line) => context.err(line)
12702
+ });
12703
+ if (report.status === "up_to_date") {
12704
+ await reportUpgradeResult(context, { status: "succeeded", fromVersion: BUILD_VERSION2, toVersion: BUILD_VERSION2, restart: false });
12705
+ return { ok: true, upToDate: true, current: BUILD_VERSION2, target: report.targetVersion, steps: report.steps };
12706
+ }
12707
+ if (report.status !== "succeeded") {
12708
+ await reportUpgradeResult(context, { status: report.status, fromVersion: report.fromVersion, toVersion: report.toVersion ?? void 0, error: report.error });
12709
+ context.setExitCode(EXIT.OTHER);
12710
+ return {
12711
+ ok: false,
12712
+ status: report.status,
12713
+ from: report.fromVersion,
12714
+ to: report.toVersion,
12715
+ error: report.error,
12716
+ steps: report.steps,
12717
+ hint: report.status === "rolled_back" ? "\u5DF2\u56DE\u88C5\u539F\u7248\u672C\uFF1B\u53EF\u67E5\u770B steps \u5B9A\u4F4D\u5931\u8D25\u539F\u56E0\u540E\u91CD\u8BD5" : "\u539F\u7248\u672C\u4FDD\u6301\u4E0D\u53D8\uFF1B\u53EF\u67E5\u770B steps \u5B9A\u4F4D\u5931\u8D25\u539F\u56E0\u540E\u91CD\u8BD5"
12718
+ };
12719
+ }
12720
+ const cfg = loadDaemonConfig(context.store);
12721
+ let restart = null;
12722
+ let deferredRestart = false;
12723
+ if (wasRunning) {
12724
+ if (cfgForSafety?.runtimeId && context.runtime.token) {
12725
+ const stillBusy = await countActiveTasks(context, cfgForSafety.runtimeId);
12726
+ if (stillBusy > 0) {
12727
+ deferredRestart = true;
12728
+ context.err(`[upgrade] daemon \u6709 ${stillBusy} \u4E2A\u4EFB\u52A1\u5728\u5347\u7EA7\u671F\u95F4\u88AB\u6D3E\u53D1\uFF0C\u4FDD\u7559\u5176\u8FD0\u884C\u2014\u2014\u65B0\u7248\u672C ${report.toVersion} \u5DF2\u5B89\u88C5\uFF0C\u4E0B\u6B21 daemon \u91CD\u542F\u65F6\u751F\u6548\uFF08morula-runtime daemon stop && morula-runtime daemon start\uFF09`);
12729
+ }
12730
+ }
12731
+ if (!deferredRestart) {
12732
+ if (probe.pid !== null) {
12733
+ const stopped = await stopDaemonByPid(probe.pid);
12734
+ context.err(`[upgrade] \u65E7 daemon\uFF08pid=${probe.pid}\uFF09\u5DF2\u505C\u6B62\uFF08${stopped.outcome}\uFF09`);
12735
+ }
12736
+ if (probe.lockState === "stale")
12737
+ releaseDaemonLock(lockPath);
12738
+ else if (probe.pid !== null)
12739
+ releaseDaemonLock(lockPath, probe.pid);
12740
+ rmSync11(pidFile, { force: true });
12741
+ mkdirSync17(dir, { recursive: true, mode: 448 });
12742
+ restart = await spawnUpgradedDaemon({
12743
+ env: context.env,
12744
+ logFile: join24(dir, LOG_FILE2),
12745
+ pidFile,
12746
+ daemonConfig: { name: cfg?.name, workdir: cfg?.workdir, maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT }
12747
+ });
12748
+ if (restart.pid > 0) {
12749
+ const liveness = await restart.waitExit(RESTART_LIVENESS_CHECK_MS);
12750
+ if (liveness.exited) {
12751
+ context.err(`[upgrade] \u65B0 daemon \u8FDB\u7A0B\u7ACB\u5373\u9000\u51FA\uFF08code=${liveness.code ?? "\u672A\u77E5"}\uFF09\uFF0C\u8BE6\u89C1 ${join24(dir, LOG_FILE2)}\u2014\u2014\u8BF7\u68C0\u67E5\u65E5\u5FD7\u540E\u624B\u52A8 morula-runtime daemon start`);
12752
+ restart = { pid: 0, entry: restart.entry, error: `\u65B0 daemon \u8FDB\u7A0B\u7ACB\u5373\u9000\u51FA\uFF08code=${liveness.code ?? "\u672A\u77E5"}\uFF09`, waitExit: async () => ({ exited: true, code: liveness.code ?? null }) };
12753
+ } else {
12754
+ context.err(`[upgrade] \u65B0 daemon \u5DF2\u62C9\u8D77\uFF08pid=${restart.pid}\uFF09\uFF0C\u65E5\u5FD7: ${join24(dir, LOG_FILE2)}`);
12755
+ }
12756
+ } else {
12757
+ context.err(`[upgrade] \u65B0 daemon \u62C9\u8D77\u5931\u8D25\uFF1A${restart.error ?? "\u672A\u77E5\u539F\u56E0"}\u2014\u2014\u8BF7\u624B\u52A8\u6267\u884C morula-runtime daemon start`);
12758
+ }
12759
+ }
12760
+ } else {
12761
+ context.err("[upgrade] daemon \u672A\u5728\u8FD0\u884C\uFF0C\u4FDD\u6301\u505C\u6B62\u72B6\u6001\uFF08\u9700\u8981\u65F6\u624B\u52A8 morula-runtime daemon start\uFF09");
12762
+ }
12763
+ const spawnFailed = wasRunning && !deferredRestart && restart !== null && restart.pid <= 0;
12764
+ await reportUpgradeResult(context, {
12765
+ status: spawnFailed ? "failed" : "succeeded",
12766
+ fromVersion: report.fromVersion,
12767
+ toVersion: report.toVersion,
12768
+ restart: !spawnFailed && wasRunning && !deferredRestart,
12769
+ ...spawnFailed ? { error: `\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\u4F46\u81EA\u52A8\u91CD\u542F\u5931\u8D25\uFF1A${restart?.error ?? "\u672A\u77E5\u539F\u56E0"}` } : {}
12770
+ });
12771
+ if (spawnFailed) {
12772
+ context.setExitCode(EXIT.OTHER);
12773
+ }
12774
+ return {
12775
+ ok: !spawnFailed,
12776
+ status: spawnFailed ? "restart_failed" : deferredRestart ? "installed_pending_restart" : "succeeded",
12777
+ from: report.fromVersion,
12778
+ to: report.toVersion,
12779
+ // deferredRestart(评审 P3):安装期有任务被派发 → 不打断 daemon,新版本下次重启生效
12780
+ restarted: !spawnFailed && wasRunning && !deferredRestart,
12781
+ pid: !spawnFailed && wasRunning && !deferredRestart ? restart?.pid ?? null : null,
12782
+ ...deferredRestart ? { note: "\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\uFF1Bdaemon \u6709\u4EFB\u52A1\u5728\u6267\u884C\u672A\u91CD\u542F\uFF0C\u4E0B\u6B21 daemon \u91CD\u542F\u65F6\u751F\u6548" } : {},
12783
+ warnings: report.warnings,
12784
+ steps: report.steps
12785
+ };
12786
+ }
12787
+
12084
12788
  // src/commands/registry-runtime.ts
12085
12789
  var COMMANDS = {
12086
12790
  auth: authCommand,
12087
12791
  daemon: daemonCommand,
12088
- setup: setupCommand
12792
+ setup: setupCommand,
12793
+ upgrade: upgradeCommand
12089
12794
  };
12090
12795
  var EXIT_CODES = {
12091
12796
  0: "success",
@@ -12107,7 +12812,7 @@ var GLOBAL_FLAGS2 = {
12107
12812
  };
12108
12813
  var SPEC = {
12109
12814
  name: "morula-runtime",
12110
- summary: "Device-level CLI for the Morula local agent runtime: authentication, daemon lifecycle (run/start/stop/status), local assistant portal, model inventory, and device diagnostics. Success output is JSON on stdout; errors are JSON on stderr.",
12815
+ summary: "Device-level CLI for the Morula local agent runtime: authentication, daemon lifecycle (run/start/stop/status), local assistant portal, model inventory, device diagnostics, and runtime self-upgrade (check + confirmed run). Success output is JSON on stdout; errors are JSON on stderr.",
12111
12816
  resources: [
12112
12817
  {
12113
12818
  resource: "auth",
@@ -12278,6 +12983,44 @@ var SPEC = {
12278
12983
  "canonical form is the bare `morula-runtime setup`; postinstall runs the same registration automatically",
12279
12984
  "only writes <root>/morula-cli; never deletes other plugins or the legacy ~/.agents/plugins/morula-cli (reported as legacyRoot)"
12280
12985
  ]
12986
+ },
12987
+ {
12988
+ resource: "upgrade",
12989
+ summary: "runtime self-upgrade: compare the installed version against the latest npm release, then upgrade this device after explicit confirmation (installs, verifies, rolls back on failure, and re-bakes the plugin launcher)",
12990
+ actions: [
12991
+ {
12992
+ action: "check",
12993
+ summary: "read-only: compare current version with the latest published version on the npm registry (one network call)",
12994
+ flags: [
12995
+ { name: "registry", type: "string", valueHint: "<url>", description: "npm registry to query (default https://registry.npmjs.org, or $MORULA_NPM_REGISTRY)" }
12996
+ ],
12997
+ examples: ["morula-runtime upgrade check"],
12998
+ writes: "none",
12999
+ interactive: false,
13000
+ idempotent: true,
13001
+ emits: ["ok", "current", "latest", "updateAvailable", "registry"]
13002
+ },
13003
+ {
13004
+ action: "run",
13005
+ summary: "upgrade this device to the target version (default: registry latest). REQUIRES --yes (no silent auto-upgrade). Refuses while the daemon has tasks running; stops an idle daemon, upgrades, verifies (rolls back on failure), re-runs setup, then restarts the daemon",
13006
+ flags: [
13007
+ { name: "yes", type: "boolean", description: "REQUIRED: explicit confirmation; without it the command refuses (exit 2). No silent auto-upgrade" },
13008
+ { name: "version", type: "string", valueHint: "<semver>", description: "target version (default: latest from the registry)" },
13009
+ { name: "registry", type: "string", valueHint: "<url>", description: "npm registry to install from (default https://registry.npmjs.org, or $MORULA_NPM_REGISTRY)" }
13010
+ ],
13011
+ examples: ["morula-runtime upgrade run --yes", "morula-runtime upgrade run --yes --version 0.1.0-alpha.16"],
13012
+ writes: "local",
13013
+ interactive: false,
13014
+ idempotent: false,
13015
+ emits: ["ok", "status", "from", "to", "restarted", "pid", "steps"]
13016
+ }
13017
+ ],
13018
+ notes: [
13019
+ "check: exit 0 = compared (see updateAvailable); 7 = registry unreachable (see error/hint)",
13020
+ "run: exit 0 = upgraded or already up to date; 6 (conflict) = daemon has running tasks (upgrade refused); 1 = install/verify failed (previous version kept or re-installed, see steps/error)",
13021
+ "the daemon restart only happens when the daemon was running before the upgrade; a stopped daemon stays stopped",
13022
+ "a stopped daemon is upgraded but not started \u2014 start it manually with morula-runtime daemon start"
13023
+ ]
12281
13024
  }
12282
13025
  ]
12283
13026
  };