@arcships/morula-runtime 0.1.0-alpha.2 → 0.1.0-alpha.4

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
@@ -362,6 +362,66 @@ var init_portal_server = __esm({
362
362
  }
363
363
  });
364
364
 
365
+ // src/daemon/dim-locate.ts
366
+ import { accessSync, constants } from "node:fs";
367
+ import { homedir as homedir4 } from "node:os";
368
+ import { join as join6 } from "node:path";
369
+ function isExecutable(p) {
370
+ try {
371
+ accessSync(p, constants.X_OK);
372
+ return true;
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+ function knownDimLocations() {
378
+ const home = homedir4();
379
+ return [
380
+ "/Applications/DimAgent.app/Contents/Resources/runtime/cli/dim",
381
+ "/Applications/Dim.app/Contents/Resources/runtime/cli/dim",
382
+ join6(home, "Applications", "DimAgent.app", "Contents", "Resources", "runtime", "cli", "dim"),
383
+ join6(home, ".dimcode", "bin", "dim"),
384
+ join6(home, ".local", "bin", "dim")
385
+ ];
386
+ }
387
+ function resolveDimCandidates(primary) {
388
+ const out = [];
389
+ const push = (p) => {
390
+ if (p && !out.includes(p))
391
+ out.push(p);
392
+ };
393
+ push(primary);
394
+ for (const c of knownDimLocations())
395
+ push(c);
396
+ for (const dir of (process.env.PATH ?? "").split(":").filter(Boolean))
397
+ push(join6(dir, "dim"));
398
+ return out;
399
+ }
400
+ function resolveDimBinary() {
401
+ const explicit = process.env.MORULA_DIM_PATH?.trim();
402
+ if (explicit) {
403
+ return isExecutable(explicit) ? explicit : "dim";
404
+ }
405
+ for (const candidate of knownDimLocations()) {
406
+ if (isExecutable(candidate)) {
407
+ return candidate;
408
+ }
409
+ }
410
+ const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
411
+ for (const dir of pathDirs) {
412
+ const candidate = join6(dir, "dim");
413
+ if (isExecutable(candidate)) {
414
+ return candidate;
415
+ }
416
+ }
417
+ return "dim";
418
+ }
419
+ var init_dim_locate = __esm({
420
+ "src/daemon/dim-locate.ts"() {
421
+ "use strict";
422
+ }
423
+ });
424
+
365
425
  // src/daemon/acp/acp-connection.ts
366
426
  var acp_connection_exports = {};
367
427
  __export(acp_connection_exports, {
@@ -400,6 +460,7 @@ var DEFAULT_ACP_INIT_TIMEOUT_MS, cachedDimVersion, requestSeq, AcpConnection;
400
460
  var init_acp_connection = __esm({
401
461
  "src/daemon/acp/acp-connection.ts"() {
402
462
  "use strict";
463
+ init_dim_locate();
403
464
  DEFAULT_ACP_INIT_TIMEOUT_MS = 45e3;
404
465
  cachedDimVersion = null;
405
466
  requestSeq = 0;
@@ -424,6 +485,46 @@ var init_acp_connection = __esm({
424
485
  setPermissionHandler(handler) {
425
486
  this.permissionHandler = handler;
426
487
  }
488
+ /**
489
+ * P5(issues/daemon-dim-spawn-enoent.md):按候选顺序尝试 spawn `dim acp` 子进程。
490
+ * 以 `spawn` 事件判成功(进程已创建);`error` 事件(ENOENT/EACCES 等,进程未起来)
491
+ * 则回收本次尝试并换下一个候选。全部失败抛最后一个错误。
492
+ */
493
+ async spawnFirstUsable(candidates) {
494
+ let lastError = null;
495
+ for (const dimPath of candidates) {
496
+ const child = spawn3(dimPath, ["acp"], {
497
+ cwd: this.options.cwd,
498
+ env: { ...process.env, ...this.options.env },
499
+ stdio: ["pipe", "pipe", "pipe"],
500
+ detached: process.platform !== "win32"
501
+ });
502
+ try {
503
+ await new Promise((resolve2, reject) => {
504
+ child.once("error", reject);
505
+ child.once("spawn", resolve2);
506
+ });
507
+ return child;
508
+ } catch (error) {
509
+ lastError = error;
510
+ const message = error instanceof Error ? error.message : String(error);
511
+ process.stderr.write(`[daemon] dim spawn \u5931\u8D25 ${dimPath}\uFF08\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u5019\u9009\uFF09: ${message}
512
+ `);
513
+ try {
514
+ const { existsSync: existsSync17 } = await import("node:fs");
515
+ process.stderr.write(
516
+ `[daemon] diag: fileExists=${existsSync17(dimPath)} cwdExists=${this.options.cwd ? existsSync17(this.options.cwd) : "n/a"} PATH=${(process.env.PATH ?? "").slice(0, 140)} DYLD=${process.env.DYLD_LIBRARY_PATH ?? "-"} NODE_OPTIONS=${process.env.NODE_OPTIONS ?? "-"}
517
+ `
518
+ );
519
+ } catch {
520
+ }
521
+ child.removeAllListeners("error");
522
+ child.removeAllListeners("spawn");
523
+ }
524
+ }
525
+ const detail = lastError instanceof Error ? lastError.message : String(lastError);
526
+ throw new Error(`dim acp \u8FDB\u7A0B\u9519\u8BEF: ${detail}\uFF08\u5DF2\u5C1D\u8BD5 ${candidates.length} \u4E2A\u5019\u9009\u8DEF\u5F84\uFF09`);
527
+ }
427
528
  get isAlive() {
428
529
  return this.child !== null && this.child.exitCode === null && !this.exited;
429
530
  }
@@ -435,12 +536,7 @@ var init_acp_connection = __esm({
435
536
  if (this.isAlive)
436
537
  return;
437
538
  this.exited = false;
438
- const child = spawn3(this.options.dimPath, ["acp"], {
439
- cwd: this.options.cwd,
440
- env: { ...process.env, ...this.options.env },
441
- stdio: ["pipe", "pipe", "pipe"],
442
- detached: process.platform !== "win32"
443
- });
539
+ const child = await this.spawnFirstUsable(resolveDimCandidates(this.options.dimPath));
444
540
  this.child = child;
445
541
  child.stderr.on("data", (d) => {
446
542
  const text = d.toString().trim();
@@ -691,10 +787,10 @@ __export(dim_home_exports, {
691
787
  });
692
788
  import { execFileSync as execFileSync3 } from "node:child_process";
693
789
  import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync4, readFileSync as readFileSync4, rmSync as rmSync3 } from "node:fs";
694
- import { homedir as homedir4 } from "node:os";
695
- import { join as join6 } from "node:path";
790
+ import { homedir as homedir5 } from "node:os";
791
+ import { join as join7 } from "node:path";
696
792
  function isolatedDimHome() {
697
- return join6(homedir4(), ".morula", "dimcode");
793
+ return join7(homedir5(), ".morula", "dimcode");
698
794
  }
699
795
  function resolveIsolatedDimEnv() {
700
796
  return {};
@@ -1715,7 +1811,7 @@ function page(ok, message) {
1715
1811
  }
1716
1812
 
1717
1813
  // ../morula-cli-core/dist/build-env.js
1718
- var BUILD_ID = "20260913141147.nogit";
1814
+ var BUILD_ID = "20260916074556.nogit";
1719
1815
  var BUILD_CONTEXTS = {
1720
1816
  "local": "http://localhost:8080/api",
1721
1817
  "test": "https://morula.basecastle.com/api",
@@ -2073,9 +2169,6 @@ var ReportApi = class {
2073
2169
  listByAutopilot(autopilotId, query = {}) {
2074
2170
  return this.client.get(`/agent-autopilots/${encodeURIComponent(requiredId2(autopilotId, "autopilot"))}/reports`, { ...query });
2075
2171
  }
2076
- archive(id) {
2077
- return this.client.patch(`/agent-reports/${encodeURIComponent(requiredId2(id))}`, { status: "archived" });
2078
- }
2079
2172
  };
2080
2173
  function requiredId2(id, label = "report") {
2081
2174
  const normalized = id.trim();
@@ -2873,7 +2966,7 @@ function redactInputSummary(value) {
2873
2966
  }
2874
2967
 
2875
2968
  // src/build-env.ts
2876
- var BUILD_VERSION2 = "0.1.0-alpha.2";
2969
+ var BUILD_VERSION2 = "0.1.0-alpha.4";
2877
2970
 
2878
2971
  // src/commands/daemon.ts
2879
2972
  import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
@@ -3256,10 +3349,34 @@ var DaemonApi = class {
3256
3349
  submitCleanupResult(id, runtimeId, body) {
3257
3350
  return this.client.post(`/agent-workspace-cleanups/${id}/result`, body, { runtimeId });
3258
3351
  }
3352
+ /**
3353
+ * 单条进度上报(兼容入口;runner 已改为批量 `recordProgressBatch`)。
3354
+ * 与批量走同一条端点/同一份契约:`items` 缺省时服务端按单条语义处理顶层字段。
3355
+ */
3259
3356
  recordProgress(taskId, body) {
3260
- return this.taskRequest(taskId, `/agent-tasks/${taskId}/progress`, { body });
3357
+ return this.recordProgressBatch(taskId, [body]);
3358
+ }
3359
+ /**
3360
+ * 批量进度上报(issues/daemon-stream-events-silently-stop-mid-run.md 根因 A):
3361
+ * 每 500ms 一批、**一次请求**提交,吞吐不再被单条 RTT 钉在 ~27 req/s
3362
+ * (绑定推理模型的任务实测产出 ~110 片段/s,逐条发必然积压数分钟,并把终态回传堵在后面)。
3363
+ *
3364
+ * 兼容性:顶层字段带该批首条 —— 旧服务端没有 `items` 支持(`whitelist` 会剥离该字段),
3365
+ * 收到后降级为「每批只落 1 条」,而不是整批 400 丢失。
3366
+ */
3367
+ recordProgressBatch(taskId, items) {
3368
+ if (items.length === 0)
3369
+ return Promise.resolve({ ok: true, inserted: 0 });
3370
+ return this.taskRequest(taskId, `/agent-tasks/${taskId}/progress`, {
3371
+ body: { ...items[0], items }
3372
+ });
3261
3373
  }
3262
- /** M2-B4:执行事件(工具调用等)→ agent_events(taskRunId+seq 唯一,前端时间线消费) */
3374
+ /**
3375
+ * M2-B4:执行事件(工具调用等)→ agent_events(taskRunId+seq 唯一,前端时间线消费)。
3376
+ *
3377
+ * P3:`turnNo` 为多回合预留位(`turn_started` / `turn_interrupted` 用它标记回合边界);
3378
+ * 服务端 `agent_events.turn_no` 直接落该值,也是「检查点消费标第几回合」的判据来源。
3379
+ */
3263
3380
  recordEvent(taskId, body) {
3264
3381
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/events`, { body });
3265
3382
  }
@@ -3267,6 +3384,46 @@ var DaemonApi = class {
3267
3384
  recordStreamChunk(taskId, body) {
3268
3385
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/stream-chunk`, { body });
3269
3386
  }
3387
+ /**
3388
+ * 会话指针回传(P0 §5.4 前置修复):只写 `dimSessionId`/`dimWorkDir`,不改任务状态。
3389
+ *
3390
+ * 取消路径下 daemon 跳过 submitResult,指针若只随 result 回传就永久丢失 → 续跑/接着聊不可能。
3391
+ * 失败不抛(调用方 fire-and-forget)——丢指针只影响续跑能力,不该反过来影响任务执行。
3392
+ */
3393
+ reportSessionPointer(taskId, body) {
3394
+ return this.taskRequest(
3395
+ taskId,
3396
+ `/agent-tasks/${taskId}/session`,
3397
+ { body }
3398
+ );
3399
+ }
3400
+ /**
3401
+ * P1(features/stage-runtime-live-chat.md §5.2 机器协议):对话消息回执。
3402
+ *
3403
+ * 语义边界(P1 必须严格遵守,误回执会丢话):
3404
+ * - 只对 `targetTaskId === 本任务` 的消息发 `consumed`——那才是被合并进本轮 prompt 的消息;
3405
+ * - `delivered` **不**由 daemon 发(服务端创建 follow-up run 时置位);
3406
+ * - `interrupting` 属 P3,P1 不发。
3407
+ */
3408
+ ackMessage(taskId, messageId, body) {
3409
+ return this.taskRequest(
3410
+ taskId,
3411
+ `/agent-tasks/${taskId}/messages/${messageId}/ack`,
3412
+ { body }
3413
+ );
3414
+ }
3415
+ /**
3416
+ * P1:对话补齐(WS 是 best-effort,拉取才是 correctness path)。
3417
+ *
3418
+ * `afterSeq` / `limit` 走 **query**(不是 token,见 `taskRequest` 注释);`limit` 缺省即不传,
3419
+ * 由服务端默认值决定。旧服务端无该端点 → 404,调用方只记日志(补齐失败不影响本轮执行)。
3420
+ */
3421
+ listTaskChat(taskId, options = {}) {
3422
+ return this.taskRequest(taskId, `/agent-tasks/${taskId}/chat`, {
3423
+ method: "GET",
3424
+ query: { afterSeq: options.afterSeq ?? 0, limit: options.limit }
3425
+ });
3426
+ }
3270
3427
  /** 任务级审批请求上报(Phase 2):返回 { ok, approvalId } */
3271
3428
  requestApproval(taskId, body) {
3272
3429
  return this.taskRequest(
@@ -3317,17 +3474,19 @@ function buildGithubSkillBody(baseBranch = "main") {
3317
3474
  return COLLABORATION_SKILL_BODY.replaceAll("__MORULA_BASE__", base);
3318
3475
  }
3319
3476
  function buildGitlabSkillBody(repoFullName, baseUrl, baseBranch = "main") {
3320
- const projectPath = encodeURIComponent(repoFullName);
3321
3477
  const host = new URL(baseUrl).host;
3322
3478
  const base = baseBranch?.trim() || "main";
3479
+ const repo = repoFullName?.trim();
3323
3480
  const body = COLLABORATION_SKILL_BODY.replaceAll("__MORULA_BASE__", base);
3481
+ const repoLine = repo ? `- **\u672C\u4EFB\u52A1\u4E3B\u4ED3\u5E93**\uFF1A\`${repo}\`\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\u6309\u4F60\u5B9E\u9645\u6539\u52A8\u7684\u4ED3\u5E93\u9010\u4E2A\u5EFA MR\uFF0C\u89C1 \xA70.5 \u4ED3\u5E93\u4E0E\u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002
3482
+ ` : "";
3324
3483
  return `${body}
3325
3484
 
3326
3485
  ## GitLab \u5E73\u53F0\u8BF4\u660E\uFF08\u672C\u4EFB\u52A1\u4ED3\u5E93\u662F GitLab\uFF0C\u8986\u76D6\u4E0A\u6587 gh \u76F8\u5173\u6307\u4EE4\uFF09
3327
3486
 
3328
3487
  \u672C\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6765\u81EA **GitLab**\uFF08${host}\uFF09\uFF0C\u4E0D\u662F GitHub\uFF1A
3329
3488
 
3330
- - **\u4E0D\u8981**\u4F7F\u7528 \`gh\`\uFF08GitHub CLI \u5728\u672C\u5E73\u53F0\u4E0D\u53EF\u7528\u3001\u4F1A\u5931\u8D25\uFF09\u3002
3489
+ ${repoLine}- **\u4E0D\u8981**\u4F7F\u7528 \`gh\`\uFF08GitHub CLI \u5728\u672C\u5E73\u53F0\u4E0D\u53EF\u7528\u3001\u4F1A\u5931\u8D25\uFF09\u3002
3331
3490
  - **\u8BA4\u8BC1**\uFF1A\u672C\u73AF\u5883 Git remote \u5DF2\u914D\u7F6E\u4E3A **SSH**\uFF08\`git@${host}:\`\uFF09\uFF0C\u8BA4\u8BC1\u8D70\u672C\u673A SSH agent / keychain\uFF0C**\u65E0\u9700\u5BC6\u7801\u3001\u65E0\u9700 HTTPS token\u3001\u65E0\u9700\u624B\u52A8\u8F93\u5165\u51ED\u636E**\u3002
3332
3491
  - \`git fetch origin\` / \`git pull\` / \`git push\` \u76F4\u63A5\u53EF\u7528\uFF1B**\u4E0D\u8981\u5207\u6362\u6210 HTTPS \u5730\u5740\u91CD\u8BD5**\uFF08HTTPS \u65E0\u51ED\u636E\u4F1A \`HTTP Basic: Access denied\`\uFF09\u3002
3333
3492
  - \u9700\u8981\u5237\u65B0\u8FDC\u7AEF\u6700\u65B0\u4EE3\u7801\u65F6\u76F4\u63A5 \`git fetch origin\`\uFF08\u6362\u5206\u652F\u7528 \`git fetch origin <branch>\` \u540E \`git switch -c\` \u6216 \`git checkout -b\`\uFF09\uFF0C**\u4E0D\u8981**\u5BFB\u627E \`MORULA_GITLAB_TOKEN\` \u6216\u7528 curl \u62C9\u53D6\u3002
@@ -3345,9 +3504,16 @@ function buildGitlabSkillBody(repoFullName, baseUrl, baseBranch = "main") {
3345
3504
  \`\`\`bash
3346
3505
  morula repo mr list <taskId> --state opened
3347
3506
  \`\`\`
3348
- \u8FD4\u56DE\u4EFB\u52A1\u7A7A\u95F4\u4ED3\u5E93\u7684 open MR\uFF08iid / title / sourceBranch / targetBranch / webUrl\uFF09\u3002\u5BA1\u67E5\u5355\u4E2A MR \u7684 diff \u4ECD\u7528 \`morula repo checkout\` \u540E \`git diff\`\u3002
3507
+ \u8FD4\u56DE\u4EFB\u52A1\u7A7A\u95F4\u4ED3\u5E93\u7684 open MR\uFF08iid / title / sourceBranch / targetBranch / webUrl / createdAt / updatedAt / author\uFF09\u3002\u5BA1\u67E5\u5355\u4E2A MR \u7684 diff \u4ECD\u7528 \`morula repo checkout\` \u540E \`git diff\`\u3002
3349
3508
  - \u672C\u547D\u4EE4\u53EA\u8BFB\uFF08\u4E0D approve / \u4E0D merge / \u4E0D\u8BC4\u8BBA\u2014\u2014**\u5408\u5E76\u7531\u4EBA\u51B3\u5B9A**\uFF0C\u89C1\u5951\u7EA6\u7B2C 4 \u6761\uFF09\uFF1B
3350
3509
  - \u82E5\u8FD4\u56DE \`NO_CONNECTION\`\uFF08\u4EFB\u52A1\u672A\u7ED1\u5B9A\u4ED3\u5E93\u8FDE\u63A5\uFF09\uFF0C\u8BF4\u660E\u672C\u4EFB\u52A1\u4E0D\u662F MR \u5DE1\u68C0\u573A\u666F\uFF0C\u76F4\u63A5\u6309\u4EFB\u52A1\u8981\u6C42\u56DE\u5E94\uFF0C\u4E0D\u8981\u81EA\u884C\u731C\u6D4B\u3002
3510
+ - **\u5728 MR \u4E0A\u7559\u8BC4\u5BA1\u610F\u89C1\uFF08\u5DE1\u68C0/\u5BA1\u67E5\u7C7B\u4EFB\u52A1\uFF0C\u975E\u8BC4\u5BA1\u4EFB\u52A1\u65F6\uFF09**\uFF1A\u7528 \`morula repo mr comment\`\uFF0C**\u4E00\u6761 MR \u6700\u591A\u4E00\u6761\u8BC4\u8BBA**\uFF1A
3511
+ \`\`\`bash
3512
+ morula repo mr get <taskId> --mr <iid> --notes # \u5199\u4E4B\u524D\u5148\u8BFB\u5DF2\u6709\u8BA8\u8BBA\uFF08\u5E73\u53F0\u4E0D\u505A\u53BB\u91CD\uFF0C\u91CD\u590D\u7531\u4F60\u907F\u514D\uFF09
3513
+ morula repo mr comment <taskId> --mr <iid> --content-file <path>
3514
+ \`\`\`
3515
+ \u5185\u5BB9\u4E3A\u7ED3\u8BBA + \u5173\u952E\u95EE\u9898\uFF08\u6587\u4EF6:\u884C + \u5EFA\u8BAE\uFF09\uFF0C\u672B\u5C3E\u5E26\u7F72\u540D\u884C \`> \u7531\u81EA\u52A8\u5316\u300C<\u81EA\u52A8\u5316\u540D\u79F0>\u300D\u4E8E <\u65E5\u671F> \u751F\u6210\`\uFF08\u4F9B\u4E0B\u4E00\u8F6E\u81EA\u8BC6\u522B\uFF09\uFF1B
3516
+ **\u4E0D\u8981\u7528 \`morula review submit\`**\uFF08\u90A3\u662F\u8BC4\u5BA1\u4EFB\u52A1\u7684\u673A\u5668\u534F\u8BAE\uFF0C\u4F1A\u9A71\u52A8\u4E1A\u52A1\u4EFB\u52A1\u72B6\u6001\u56DE\u9000\uFF09\u3002
3351
3517
  - \u5173\u8054/\u5173\u5355\u8BED\u4E49\u4E0E\u4E0A\u6587\u4E00\u81F4\uFF1AMR \u7684 title / body / \u6E90\u5206\u652F\u51FA\u73B0\u4EFB\u52A1\u7F16\u53F7 \u2192 \u81EA\u52A8\u5173\u8054\uFF1B\`Closes <\u4EFB\u52A1\u7F16\u53F7>\` \u2192 merge \u65F6\u81EA\u52A8\u6D41\u8F6C\u4EFB\u52A1\u3002
3352
3518
  `;
3353
3519
  }
@@ -3483,6 +3649,7 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
3483
3649
  morula repo checkout <taskId> <repoFullName> [--ref <branch|sha>] # \u62C9\u53D6/\u5207\u6362\u4ED3\u5E93\uFF08\u8FD4\u56DE\u7269\u5316\u8DEF\u5F84\uFF0C\u89C1 0.5\uFF09
3484
3650
  morula repo prepare <taskId> <repoFullName> # \u591A\u4ED3\u5E93\u4EFB\u52A1\uFF1A\u7269\u5316\u5176\u4ED6\u4ED3\u5E93\u5230\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\u4E0B\uFF08\u89C1 0.5\uFF09
3485
3651
  morula comment list --object <objectId> # \u8BFB\u8BC4\u8BBA\uFF08\u4EFB\u52A1=objectId\uFF09
3652
+ morula task inbox [--peek] # \u8FD0\u884C\u4E2D\u68C0\u67E5\u70B9\uFF1A\u53D6\u672C\u7EBF\u7A0B\u672A\u6D88\u8D39\u6D88\u606F\uFF0C\u7EB3\u5165\u672C\u56DE\u5408\uFF08\u89C1 1.3\uFF09
3486
3653
  # \u6CE8\uFF1A\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u53D1\u9001\uFF08\u4F5C\u8005=\u667A\u80FD\u4F53\uFF09\uFF0C\u4E0D\u8981\u8C03\u7528 comment add \u81EA\u884C\u7F6E\u8BC4\u3002
3487
3654
  \`\`\`
3488
3655
  - **\u53EF\u7528\u4ED3\u5E93\u6E05\u5355**\u7531\u7CFB\u7EDF\u63D0\u793A\u76F4\u63A5\u7ED9\u51FA\uFF08\u300C\u53EF\u7528\u4ED3\u5E93\u300D/\u300C\u591A\u4ED3\u5E93\u4EFB\u52A1\u300D\u6BB5\uFF09\uFF0C**\u4E0D\u9700\u8981**\u81EA\u5DF1\u67E5\uFF1B\u53EA\u6709\u5728\u7CFB\u7EDF\u63D0\u793A\u672A\u7ED9\u51FA\u3001\u53C8\u786E\u5B9E\u9700\u8981\u679A\u4E3E\u65F6\u624D\u7528
@@ -3528,6 +3695,44 @@ morula report create --title "<\u4E00\u53E5\u8BDD\u6807\u9898>" --content "<Mark
3528
3695
  - \u62A5\u544A\u843D\u5E93\u540E\u521B\u5EFA\u8005\u4E0E\u8BA2\u9605\u8005\u4F1A\u6536\u5230\u8F7B\u901A\u77E5\uFF08\u53EA\u542B\u6807\u9898\u4E0E\u8DF3\u8F6C\uFF09\uFF1B
3529
3696
  - \u4E0D\u4EA7\u51FA\u4E5F\u6CA1\u5173\u7CFB\uFF1A\u4EFB\u52A1\u7EC8\u6001\u65F6\u5E73\u53F0\u4F1A\u7528\u4F60\u7684\u6700\u7EC8\u8F93\u51FA\u515C\u5E95\u843D\u4E00\u4EFD\u62A5\u544A\uFF08\u4F46\u515C\u5E95\u4E0D\u5982\u4F60\u4E3B\u52A8\u7EC4\u7EC7\u7684\u5185\u5BB9\u6E05\u6670\uFF0C\u4FE1\u606F\u7C7B\u4EA7\u51FA**\u4F18\u5148\u4E3B\u52A8\u4EA7\u51FA**\uFF09\u3002
3530
3697
 
3698
+ ## 1.3 \u8FD0\u884C\u4E2D\u53CD\u9988\uFF1A\u68C0\u67E5\u70B9\u4E0E\u6253\u65AD\uFF08\u672C\u56DE\u5408\u5185\u751F\u6548\uFF09
3699
+
3700
+ \u7528\u6237\u53EF\u4EE5\u5728**\u4F60\u6B63\u5728\u8FD0\u884C\u7684\u8FD9\u4E00\u4E2A\u56DE\u5408\u5185**\u53D1\u6765\u610F\u89C1\u2014\u2014\u4E0D\u5FC5\u7B49\u672C\u8F6E\u7ED3\u675F\u3001\u4E5F\u4E0D\u5FC5\u91CD\u5F00\u4EFB\u52A1\u3002\u4F60\u8981\u5728\u4E24\u5904\u914D\u5408\uFF1A
3701
+
3702
+ **\u2460 \u4E3B\u52A8\u67E5\u68C0\u67E5\u70B9\uFF08\`morula task inbox\`\uFF09\u2014\u2014\u53EA\u5728\u8FD9\u56DB\u4E2A\u65F6\u673A\u67E5\uFF0C\u4E0D\u8981\u66F4\u9891\u7E41**
3703
+
3704
+ \`\`\`bash
3705
+ morula task inbox # \u53D6\u8D70\u672C\u7EBF\u7A0B\u672A\u6D88\u8D39\u7684\u6D88\u606F\uFF08\u670D\u52A1\u7AEF\u6807\u8BB0\u300C\u672C\u56DE\u5408\u5DF2\u6D88\u8D39\u300D\uFF09
3706
+ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\u6CA1\u6709\u65B0\u610F\u89C1\uFF0C\u4E0D\u6807\u8BB0\u6D88\u8D39\uFF09
3707
+ \`\`\`
3708
+
3709
+ \u56DB\u4E2A\u65F6\u673A\uFF08\u95ED\u73AF\u679A\u4E3E\uFF0C**\u4E0D\u8981**\u6BCF\u4E00\u6B65\u90FD\u67E5\uFF09\uFF1A
3710
+
3711
+ 1. \u4E00\u6279\u6587\u4EF6\u6539\u52A8**\u5B8C\u6210\u4E4B\u540E**\uFF08\u51C6\u5907\u8FDB\u5165\u4E0B\u4E00\u6279\u4E4B\u524D\uFF09\uFF1B
3712
+ 2. \`git commit\` **\u4E4B\u524D**\uFF1B
3713
+ 3. \`git push\` / \u5EFA PR **\u4E4B\u524D**\uFF1B
3714
+ 4. \u6536\u5230\u300C\u8FD0\u884C\u4E2D\u88AB\u6253\u65AD\u300D\u63D0\u793A\u65F6\uFF08\u89C1 \u2461\uFF09\u3002
3715
+
3716
+ \u62FF\u5230\u6D88\u606F\u540E\uFF1A
3717
+
3718
+ - **\u6709\u6D88\u606F** \u2192 \u9010\u6761\u7EB3\u5165\u672C\u56DE\u5408\uFF0C\u6309\u7528\u6237\u7684\u610F\u89C1\u8C03\u6574\uFF0C\u5E76\u5728\u6700\u7EC8\u603B\u7ED3\u91CC**\u56DE\u6267\u5F0F\u8BF4\u660E**\uFF08\u300C\u5DF2\u6309\u8981\u6C42\u6539\u4E3A X\u300D\uFF09\uFF1B
3719
+ - **\u6CA1\u6709\u6D88\u606F** \u2192 **\u7ACB\u5373\u7EE7\u7EED\u624B\u4E0A\u7684\u4E8B\uFF0C\u4E0D\u8981\u5C55\u5F00\u8BA8\u8BBA\u3001\u4E0D\u8981\u590D\u8FF0\u68C0\u67E5\u8FC7\u7A0B\u3001\u4E0D\u8981\u5728\u603B\u7ED3\u91CC\u63D0\u300C\u68C0\u67E5\u8FC7\u6CA1\u6709\u65B0\u610F\u89C1\u300D**\uFF1B
3720
+ - \u7528\u6237\u7684\u610F\u89C1**\u4E0D\u5F97\u63A8\u7FFB\u5B83\u6CA1\u6D89\u53CA\u7684\u90E8\u5206**\uFF1A\u5DF2\u5B8C\u6210\u7684\u3001\u4E0E\u610F\u89C1\u65E0\u5173\u7684\u6539\u52A8\u4FDD\u7559\uFF1B\u610F\u89C1\u4E0E\u539F\u59CB\u9700\u6C42\u51B2\u7A81\u65F6**\u4EE5\u610F\u89C1\u4E3A\u51C6**\u5E76\u5728\u603B\u7ED3\u91CC\u8BF4\u660E\u5DEE\u5F02\uFF1B
3721
+ - \u4E0D\u8981\u53CD\u590D\u8F6E\u8BE2\uFF08\u4E00\u6B21\u68C0\u67E5\u5C31\u662F\u4E00\u6B21\u5DE5\u5177\u8C03\u7528\uFF0C\u6709\u5F00\u9500\uFF09\u3002
3722
+
3723
+ **\u2461 \u88AB\u6253\u65AD\u65F6\uFF08\u7528\u6237\u6309\u4E86\u300C\u7ACB\u5373\u6253\u65AD\u300D\uFF09\u4F60\u4F1A\u5728\u540C\u4E00\u4F1A\u8BDD\u6536\u5230\u65B0\u7684\u6307\u4EE4**
3724
+
3725
+ - \u4E0A\u4E00\u56DE\u5408\u5728**\u5B89\u5168\u70B9**\u88AB\u4E2D\u65AD\uFF1A\u5176\u4E2D\u7684\u5DE5\u5177\u8FDB\u5EA6\u5DF2\u4E22\u5F03\uFF0C**\u88AB\u4E2D\u65AD\u7684\u5DE5\u5177\u8C03\u7528\u6CA1\u6709\u7ED3\u679C**\uFF08\u65F6\u95F4\u7EBF\u6807\u4E3A\u5DF2\u4E2D\u65AD\uFF09\u2014\u2014\u9700\u8981\u65F6\u91CD\u65B0\u6267\u884C\uFF0C\u4E0D\u8981\u5047\u5B9A\u5B83\u5DF2\u6210\u529F\uFF1B
3726
+ - \u65B0\u6D88\u606F\u5C31\u662F\u7528\u6237\u7684\u6700\u65B0\u610F\u89C1\uFF1A\u5148\u6309\u5B83\u8C03\u6574\u65B9\u5411\uFF0C\u518D\u7EE7\u7EED\u5B8C\u6210\u672C\u4EFB\u52A1\uFF1B\u5DE5\u4F5C\u533A\u6587\u4EF6\u4E0E git \u72B6\u6001\u4ECD\u5728\uFF0C\u4E0D\u7528\u91CD\u505A\u5DF2\u5B8C\u6210\u7684\u90E8\u5206\u3002
3727
+
3728
+ **\u2462 \u7528\u6237\u610F\u89C1\u4E0E\u300C\u8981\u4E0D\u8981\u8FD4\u5DE5\u300D\u7531\u4F60\u81EA\u5DF1\u5224\u65AD\uFF0C\u5E73\u53F0\u4E0D\u4EE3\u505A**
3729
+
3730
+ - \u5E73\u53F0**\u4E0D\u4F1A**\u56E0\u4E3A\u7528\u6237\u8BF4\u4E86\u53E5\u8BDD\u5C31\u56DE\u9000\u9636\u6BB5\u3001\u6539\u4EFB\u52A1\u72B6\u6001\u6216\u6D3E\u8FD4\u5DE5\u4EFB\u52A1\u2014\u2014**\u90A3\u662F\u4F60\u7684\u5224\u65AD**\uFF1A\u8BC4\u4F30\u540E\u8BA4\u4E3A\u5FC5\u987B\u63A8\u5012\u91CD\u505A\uFF0C\u5C31\u6309\u4F60\u81EA\u5DF1\u7684\u534F\u8BAE\u8868\u8FBE\uFF08\u8BC4\u5BA1\u9636\u6BB5\u7528 \`morula review submit\` \u7ED9\u51FA\u7ED3\u8BBA\uFF1B\u5206\u6790\u9636\u6BB5\u7528 \`morula task analysis-verdict\`\uFF09\uFF0C\u72B6\u6001\u6D41\u8F6C\u7531\u670D\u52A1\u7AEF\u6309\u65E2\u6709\u89C4\u5219\u9A71\u52A8\uFF1B
3731
+ - \u4F60\u53EA\u8DDF**\u5F53\u524D\u9636\u6BB5**\u5BF9\u8BDD\uFF1A\u6D88\u606F\u5C31\u662F\u53D1\u7ED9\u5F53\u524D\u9636\u6BB5\u8D1F\u8D23\u667A\u80FD\u4F53\u7684\u3002**\u4E0D\u8981**\u56E0\u4E3A\u6536\u5230\u610F\u89C1\u5C31\u53BB\u64CD\u4F5C\u522B\u7684\u9636\u6BB5\u6216\u522B\u7684\u5BF9\u8C61\u7684\u8D44\u6E90\uFF1B
3732
+ - **\u8BC4\u5BA1\u9636\u6BB5\u4F60\u53EA\u505A\u8BC4\u5BA1**\uFF08\u770B diff\u3001\u6838\u5BF9\u76EE\u6807\u3001\u51FA\u7ED3\u8BBA\uFF09\uFF0C**\u4E0D\u8981\u6539\u4EE3\u7801\u3001\u4E0D\u8981\u5EFA\u5206\u652F/\u63D0 PR**\u3002
3733
+
3734
+ > \u4E3A\u4EC0\u4E48\u662F\u8FD9\u56DB\u4E2A\u65F6\u673A\uFF1A\u68C0\u67E5\u662F**\u5DE5\u5177\u8C03\u7528**\uFF0C\u6709 token \u4E0E\u5EF6\u8FDF\u5F00\u9500\uFF1B\u300C\u65E0\u6D88\u606F\u7ACB\u5373\u7EE7\u7EED\u300D\u662F\u4E3A\u4E86\u4E0D\u8BA9\u68C0\u67E5\u70B9\u628A\u4E3B\u7EBF\u5E26\u504F\u3002
3735
+
3531
3736
  ## 2. PR \u94FE\u63A5\u4E0E\u5173\u5355\u610F\u56FE\u662F\u4E24\u5957\u4E0D\u540C\u7684\u5951\u7EA6
3532
3737
 
3533
3738
  - **\u94FE\u63A5\u901A\u9053**\uFF1APR \u7684 title / branch / body \u4E2D\u51FA\u73B0\u4EFB\u52A1\u7F16\u53F7 \u2192 \u670D\u52A1\u7AEF\u81EA\u52A8\u5173\u8054 PR \u5230\u4EFB\u52A1\u3002
@@ -3617,6 +3822,20 @@ morula report create --title "<\u4E00\u53E5\u8BDD\u6807\u9898>" --content "<Mark
3617
3822
  - **\u4E0D\u8981\u624B\u52A8\u6D41\u8F6C\u672C\u4EFB\u52A1\u72B6\u6001**\uFF08\u4E0D\u8981\u8C03 \`morula task state\`\uFF09\uFF1A\u8BC4\u5BA1\u7ED3\u8BBA\u7531\u670D\u52A1\u7AEF\u636E\u6B64\u63A8\u8FDB\u3002
3618
3823
  - **\u5FC5\u987B\u4E0A\u62A5**\uFF1A\u6F0F\u62A5\u4F1A\u8BA9\u4EFB\u52A1\u505C\u5728\u300C\u5F85\u8BC4\u5BA1\u300D\uFF0C\u5F00\u53D1\u65E0\u6CD5\u8FDB\u5165\u4E0B\u4E00\u8F6E\u3002
3619
3824
 
3825
+ ## 4.3 \u5728 MR/PR \u4E0A\u7559\u8BC4\u5BA1\u610F\u89C1\uFF08\u5DE1\u68C0/\u5BA1\u67E5\u7C7B\u4EFB\u52A1\uFF0C\u975E\u8BC4\u5BA1\u4EFB\u52A1\u65F6\uFF09
3826
+
3827
+ > **\u4EC5\u5F53\u672C\u4EFB\u52A1\u662F\u5DE1\u68C0/\u5BA1\u67E5\u7C7B\uFF08\u5982\u81EA\u52A8\u5316\u5B9A\u65F6 PR Review\uFF09\uFF0C\u4E14\u786E\u5B9E\u53D1\u73B0\u503C\u5F97\u63D0\u51FA\u7684\u95EE\u9898\u65F6\u9002\u7528**\uFF1B\u5F00\u53D1/\u5206\u6790/\u95EE\u7B54\u4EFB\u52A1\u8DF3\u8FC7\u672C\u8282\u3002
3828
+
3829
+ - **\u6B63\u786E\u59FF\u52BF**\uFF1A\u7528 \`morula repo mr comment\`\uFF08\u670D\u52A1\u7AEF\u673A\u5668\u534F\u8BAE\uFF0C\u53EA\u5199\u8BC4\u8BBA\uFF1A\u4E0D approve\u3001\u4E0D merge\u3001\u4E0D close\uFF09\uFF0C**\u4E00\u6761 MR \u6700\u591A\u4E00\u6761\u8BC4\u8BBA**\uFF1A
3830
+ \`\`\`bash
3831
+ morula repo mr get <taskId> --mr <iid> --notes # \u5199\u4E4B\u524D\u5148\u8BFB\u5DF2\u6709\u8BA8\u8BBA\uFF08\u5E73\u53F0\u4E0D\u505A\u53BB\u91CD\uFF0C\u91CD\u590D\u7531\u4F60\u907F\u514D\uFF09
3832
+ morula repo mr comment <taskId> --mr <iid> --content-file <path>
3833
+ \`\`\`
3834
+ - \u5185\u5BB9\u4E3A**\u7ED3\u8BBA + \u5173\u952E\u95EE\u9898**\uFF08\u6587\u4EF6:\u884C + \u95EE\u9898 + \u5EFA\u8BAE\uFF09\uFF0C\u4E0D\u8981\u8D34\u5927\u6BB5\u65E5\u5FD7\u6216\u590D\u8FF0 diff\uFF1B\u957F\u5185\u5BB9\u5199\u6210\u62A5\u544A\uFF08\`morula report create\`\uFF09\u540E\u7528\u6458\u8981\u5F15\u7528\uFF1B
3835
+ - \u672B\u5C3E\u5E26\u7F72\u540D\u884C \`> \u7531\u81EA\u52A8\u5316\u300C<\u81EA\u52A8\u5316\u540D\u79F0>\u300D\u4E8E <\u65E5\u671F> \u751F\u6210\`\u2014\u2014\u4F9B\u4EBA\u4E0E agent \u4E0B\u4E00\u8F6E\u8BC6\u522B\u8FD9\u662F\u672C\u81EA\u52A8\u5316\u7684\u5386\u53F2\u8BC4\u8BBA\uFF1B
3836
+ - \u82E5\u672C\u81EA\u52A8\u5316\u5DF2\u5C31\u540C\u4E00\u95EE\u9898\u7559\u8FC7\u8BC4\u8BBA\u3001\u7ED3\u8BBA\u4E0E\u8FD9\u6B21\u4E00\u81F4 \u2192 **\u4E0D\u8981\u91CD\u590D\u8BC4\u8BBA**\uFF1B
3837
+ - \u26D4 **\u4E0D\u8981\u7528 \`morula review submit\`**\uFF08\u90A3\u662F\u8BC4\u5BA1\u4EFB\u52A1\u7684\u673A\u5668\u534F\u8BAE\uFF0C\u4F1A\u9A71\u52A8\u4E1A\u52A1\u4EFB\u52A1\u72B6\u6001\u56DE\u9000\uFF09\u3002
3838
+
3620
3839
  ## 5. \u8BC4\u8BBA\u4EA4\u4ED8\u7EAA\u5F8B
3621
3840
 
3622
3841
  - **\u4F55\u65F6\u53D1\u8BC4\u8BBA**\uFF1A**\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u4EE5\u667A\u80FD\u4F53\u8EAB\u4EFD\u53D1\u9001**\uFF08\u4EFB\u52A1\u5B8C\u6210\u65F6\u81EA\u52A8\u53D1\u5E03\u5230\u4EFB\u52A1\u5BF9\u8C61\uFF0C\u4F5C\u8005=\u5F53\u524D\u667A\u80FD\u4F53\uFF09\u3002**\u4F60\u4E0D\u9700\u8981\u3001\u4E5F\u4E0D\u5E94\u8C03\u7528 \`morula comment add\`**\u2014\u2014\u53EA\u8981\u5728\u6700\u7EC8\u8F93\u51FA\u91CC\u7ED9\u51FA\u7528\u6237\u5173\u5FC3\u7684\u7ED3\u8BBA\u5373\u53EF\uFF0C\u670D\u52A1\u7AEF\u4F1A\u4F5C\u4E3A\u8BE5\u667A\u80FD\u4F53\u7684\u5B8C\u6210\u8BC4\u8BBA\u53D1\u5E03\u3002chat \u4F1A\u8BDD\u65E0 objectId \u7684\u4EFB\u52A1\uFF08\u7EAF\u5BF9\u8BDD\u6D3E\u53D1\uFF09\u4E0D\u56DE\u8BC4\u8BBA\u2014\u2014\u6700\u7EC8\u4EA4\u4ED8\u7531 daemon \u56DE\u586B assistant \u6D88\u606F\u3002
@@ -3672,11 +3891,13 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
3672
3891
 
3673
3892
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
3674
3893
  |------|------|------|
3675
- | CLI \u8BFB\u8BC4\u8BBA | \`morula comment list --object <objectId>\` | \`apps/morula-cli/src/commands/comment.ts\` |
3676
- | CLI \u5199\u8BC4\u8BBA\uFF08**\u5951\u7EA6\u7981\u6B62 agent \u81EA\u884C\u8C03\u7528**\uFF1A\u5B8C\u6210\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u53D1\u9001\uFF0C\u89C1\u672C\u6587\u4EF6\u7B2C 5 \u6761\uFF09 | \`morula comment add\`\uFF08\u547D\u4EE4\u5B58\u5728\uFF0C\u4F46\u672C\u5951\u7EA6\u8981\u6C42\u4E0D\u8C03\u7528\uFF09 | \`apps/morula-cli/src/commands/comment.ts\` |
3677
- | CLI \u4EFB\u52A1\u8BE6\u60C5\uFF08**\u65E0\u53C2**\u53D6\u672C\u4EFB\u52A1\uFF1B\u5E26\u53C2\u53D6\u4E1A\u52A1\u5BF9\u8C61\uFF09 | \`morula task get\` \u2192 \`GET /agent-tasks/:id\`\uFF1B\`morula task get <objectId>\` \u2192 \`GET /objects/:id\` | \`apps/morula-cli/src/commands/task.ts\` |
3894
+ | CLI \u8BFB\u8BC4\u8BBA | \`morula comment list --object <objectId>\` | \`packages/morula-cli/src/commands/comment.ts\` |
3895
+ | CLI \u5199\u8BC4\u8BBA\uFF08**\u5951\u7EA6\u7981\u6B62 agent \u81EA\u884C\u8C03\u7528**\uFF1A\u5B8C\u6210\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u53D1\u9001\uFF0C\u89C1\u672C\u6587\u4EF6\u7B2C 5 \u6761\uFF09 | \`morula comment add\`\uFF08\u547D\u4EE4\u5B58\u5728\uFF0C\u4F46\u672C\u5951\u7EA6\u8981\u6C42\u4E0D\u8C03\u7528\uFF09 | \`packages/morula-cli/src/commands/comment.ts\` |
3896
+ | CLI \u4EFB\u52A1\u8BE6\u60C5\uFF08**\u65E0\u53C2**\u53D6\u672C\u4EFB\u52A1\uFF1B\u5E26\u53C2\u53D6\u4E1A\u52A1\u5BF9\u8C61\uFF09 | \`morula task get\` \u2192 \`GET /agent-tasks/:id\`\uFF1B\`morula task get <objectId>\` \u2192 \`GET /objects/:id\` | \`packages/morula-cli/src/commands/task.ts\` |
3678
3897
  | daemon \u6CE8\u5165\u51ED\u636E\uFF08agent \u65E0\u9700\u81EA\u884C\u767B\u5F55\uFF09 | runner \u6CE8\u5165 \`env\` \u7ED9 Provider \u5B50\u8FDB\u7A0B | \`packages/runtime/src/daemon/runner.ts\` |
3679
- | \u7CFB\u7EDF\u63D0\u793A\u6CE8\u5165\uFF08\u9636\u6BB5\u804C\u8D23 / \u76EE\u6807 / \u4ED3\u5E93\u6E05\u5355 / \u5DE5\u5177\u6307\u5F15\uFF09\uFF1B\u4EFB\u52A1\u8BE6\u60C5\u7531 \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u83B7\u53D6 | \`buildTaskPromptWithContract\`\uFF08\`runner.ts\`\uFF0C\u552F\u4E00\u8FDB dim \u7684 messages[0]\uFF09\uFF1BCLI \`apps/morula-cli/src/commands/task.ts\`\uFF08resolveTaskId \u2192 \`GET /agent-tasks/:id\`\uFF09 | \`packages/runtime/src/daemon/runner.ts\` |
3898
+ | \u7CFB\u7EDF\u63D0\u793A\u6CE8\u5165\uFF08\u9636\u6BB5\u804C\u8D23 / \u76EE\u6807 / \u4ED3\u5E93\u6E05\u5355 / \u5DE5\u5177\u6307\u5F15\uFF09\uFF1B\u4EFB\u52A1\u8BE6\u60C5\u7531 \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u83B7\u53D6 | \`buildTaskPromptWithContract\`\uFF08\`runner.ts\`\uFF0C\u552F\u4E00\u8FDB dim \u7684 messages[0]\uFF09\uFF1BCLI \`packages/morula-cli/src/commands/task.ts\`\uFF08resolveTaskId \u2192 \`GET /agent-tasks/:id\`\uFF09 | \`packages/runtime/src/daemon/runner.ts\` |
3899
+ | \u8FD0\u884C\u4E2D\u68C0\u67E5\u70B9\uFF08\u672C\u56DE\u5408\u5185\u53D6\u7528\u6237\u610F\u89C1\uFF09 | CLI \`morula task inbox [--peek]\` \u2192 \`GET /agent-tasks/:id/thread/inbox\`\uFF08\u673A\u5668\u534F\u8BAE\uFF1B\u9ED8\u8BA4\u53D6\u8D70\u5373\u6D88\u8D39\uFF0C\u843D \`consumed\` + \u5F53\u56DE\u5408\u53F7\uFF09\uFF1B\u5951\u7EA6\u65F6\u673A\u89C1\u672C SKILL 1.3 | \`packages/morula-cli/src/commands/task.ts\`\u3001\`src/modules/agent-task/chat/object-agent-chat.controller.ts\`\u3001\`object-agent-message.service.ts\`\uFF08\`getThreadInbox\`\uFF09 |
3900
+ | \u8FD0\u884C\u4E2D\u6253\u65AD\uFF08\u540C\u4E00\u4F1A\u8BDD\u91CD\u5F00\u4E00\u8F6E\uFF09 | daemon \u5728\u5B89\u5168\u70B9\uFF08\`inFlightTools === 0\`\uFF09\`session/cancel\` \u2192 \`session/resume\` \u2192 \u7528\u65B0\u610F\u89C1\u91CD\u5F00 prompt\uFF1B\u56DE\u5408\u53F7\u4E0E\u4E2D\u65AD\u70B9\u4E0A\u62A5\u4E3A \`turn_started\` / \`turn_interrupted\` \u4E8B\u4EF6 | \`packages/runtime/src/daemon/acp/dim-acp-executor.ts\`\uFF08\u56DE\u5408\u5FAA\u73AF\uFF09\u3001\`runner.ts\`\uFF08\`buildSteerPort\`\uFF09\u3001\`src/modules/agent-task/chat/object-agent-message.service.ts\`\uFF08\`steerDeliveryStrategy\`\uFF09 |
3680
3901
 
3681
3902
  ## 3. \u6539\u4EE3\u7801\u9ED8\u8BA4\u4EA7 PR\uFF08\u5206\u652F\u7EAA\u5F8B\uFF0CAgent \u53D6\u540D\uFF09
3682
3903
 
@@ -3694,13 +3915,13 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
3694
3915
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
3695
3916
  |------|------|------|
3696
3917
  | merge \u81EA\u52A8\u5173\u5355\uFF08\u670D\u52A1\u7AEF\u9A71\u52A8\u72B6\u6001\uFF09 | webhook merged \u2192 closeIntent \u2192 Object \u5B8C\u6210 | \`src/modules/vcs-integration/webhook/vcs-event.service.ts\` |
3697
- | agent \u4E0D\u8C03 CLI \u6539\u72B6\u6001 | \u534F\u4F5C\u5951\u7EA6\u7B2C 4 \u6761\u3002**\u6CE8\u610F\uFF1ACLI \u6709 \`morula task state <id> --state <code>\`**\uFF08\u673A\u5668\u534F\u8BAE \`POST /agent-tasks/:id/state\`\uFF0C\u4F9B\u5E73\u53F0/\u81EA\u6D41\u8F6C\u4F7F\u7528\uFF09\u2014\u2014\u662F**\u5951\u7EA6\u8981\u6C42 agent \u4E0D\u8C03\u7528**\u5B83\uFF0C\u4E0D\u662F\u300CCLI \u6CA1\u6709\u8FD9\u4E2A\u547D\u4EE4\u300D | \`apps/morula-cli/src/commands/task.ts\`\uFF08action \`state\`\uFF09\u3001\`src/modules/agent-task/agent-task.controller.ts\`\uFF08\u7AEF\u70B9\uFF09 |
3918
+ | agent \u4E0D\u8C03 CLI \u6539\u72B6\u6001 | \u534F\u4F5C\u5951\u7EA6\u7B2C 4 \u6761\u3002**\u6CE8\u610F\uFF1ACLI \u6709 \`morula task state <id> --state <code>\`**\uFF08\u673A\u5668\u534F\u8BAE \`POST /agent-tasks/:id/state\`\uFF0C\u4F9B\u5E73\u53F0/\u81EA\u6D41\u8F6C\u4F7F\u7528\uFF09\u2014\u2014\u662F**\u5951\u7EA6\u8981\u6C42 agent \u4E0D\u8C03\u7528**\u5B83\uFF0C\u4E0D\u662F\u300CCLI \u6CA1\u6709\u8FD9\u4E2A\u547D\u4EE4\u300D | \`packages/morula-cli/src/commands/task.ts\`\uFF08action \`state\`\uFF09\u3001\`src/modules/agent-task/agent-task.controller.ts\`\uFF08\u7AEF\u70B9\uFF09 |
3698
3919
 
3699
3920
  ## 4.1 \u8BC4\u5BA1\u4EFB\u52A1\u7ED3\u8BBA\u56DE\u4F20\uFF08morula review submit\uFF09
3700
3921
 
3701
3922
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
3702
3923
  |------|------|------|
3703
- | \u8BC4\u5BA1\u7ED3\u8BBA\u673A\u5668\u534F\u8BAE\uFF08approved / changes_requested / commented\uFF0Cchanges_requested \u5FC5\u987B\u5E26 details\uFF09 | \`morula review submit <objectId> --verdict ... [--details] [--pr-number]\` \u2192 \`POST /agent-collaborations/review/submit\`\uFF08\u670D\u52A1\u7AEF\u6821\u9A8C\u8C03\u7528\u8005 = \u8BC4\u5BA1\u4EFB\u52A1\u540E\u843D\u5E93\u5E76\u9A71\u52A8\u56DE\u73AF\uFF09 | \`apps/morula-cli/src/commands/review.ts\`\u3001\`src/modules/agent-task/review-loop.service.ts\` |
3924
+ | \u8BC4\u5BA1\u7ED3\u8BBA\u673A\u5668\u534F\u8BAE\uFF08approved / changes_requested / commented\uFF0Cchanges_requested \u5FC5\u987B\u5E26 details\uFF09 | \`morula review submit <objectId> --verdict ... [--details] [--pr-number]\` \u2192 \`POST /agent-collaborations/review/submit\`\uFF08\u670D\u52A1\u7AEF\u6821\u9A8C\u8C03\u7528\u8005 = \u8BC4\u5BA1\u4EFB\u52A1\u540E\u843D\u5E93\u5E76\u9A71\u52A8\u56DE\u73AF\uFF09 | \`packages/morula-cli/src/commands/review.ts\`\u3001\`src/modules/agent-task/review-loop.service.ts\` |
3704
3925
  | \u8BC4\u5BA1\u5BF9\u8C61\uFF08objectId\uFF09\u7ECF AgentTask \u672C\u4F53\u4E0B\u53D1\uFF08\`morula task get\` \u65E0\u53C2\u8FD4\u56DE\uFF09 | AgentTask \u8BB0\u5F55 \`objectId\` \u5B57\u6BB5\uFF08enqueue \u65F6\u5199\u5165\uFF09 | \`src/modules/agent-task/review-loop.service.ts\` |
3705
3926
 
3706
3927
  ## 5. \u8BC4\u8BBA\u4EA4\u4ED8\u7EAA\u5F8B\uFF08\u4E00\u6761\u6700\u7EC8\u8BC4\u8BBA\uFF0C\u7ED3\u679C\u4E0A\u5E73\u53F0\uFF09
@@ -3738,46 +3959,7 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
3738
3959
  // src/daemon/runner.ts
3739
3960
  init_daemon_config();
3740
3961
  init_dim_home();
3741
-
3742
- // src/daemon/dim-locate.ts
3743
- import { accessSync, constants } from "node:fs";
3744
- import { homedir as homedir5 } from "node:os";
3745
- import { join as join7 } from "node:path";
3746
- function isExecutable(p) {
3747
- try {
3748
- accessSync(p, constants.X_OK);
3749
- return true;
3750
- } catch {
3751
- return false;
3752
- }
3753
- }
3754
- function resolveDimBinary() {
3755
- const explicit = process.env.MORULA_DIM_PATH?.trim();
3756
- if (explicit) {
3757
- return isExecutable(explicit) ? explicit : "dim";
3758
- }
3759
- const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
3760
- for (const dir of pathDirs) {
3761
- const candidate = join7(dir, "dim");
3762
- if (isExecutable(candidate)) {
3763
- return candidate;
3764
- }
3765
- }
3766
- const home = homedir5();
3767
- const knownLocations = [
3768
- "/Applications/DimAgent.app/Contents/Resources/runtime/cli/dim",
3769
- "/Applications/Dim.app/Contents/Resources/runtime/cli/dim",
3770
- join7(home, "Applications", "DimAgent.app", "Contents", "Resources", "runtime", "cli", "dim"),
3771
- join7(home, ".dimcode", "bin", "dim"),
3772
- join7(home, ".local", "bin", "dim")
3773
- ];
3774
- for (const candidate of knownLocations) {
3775
- if (isExecutable(candidate)) {
3776
- return candidate;
3777
- }
3778
- }
3779
- return "dim";
3780
- }
3962
+ init_dim_locate();
3781
3963
 
3782
3964
  // src/daemon/flags.ts
3783
3965
  function isFlagEnabled(name, defaultValue = true) {
@@ -3792,19 +3974,33 @@ import { execFileSync as execFileSync4 } from "node:child_process";
3792
3974
  import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
3793
3975
  import { join as join8 } from "node:path";
3794
3976
  var GIT_CANDIDATE_DIRS = ["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin", "/opt/local/bin"];
3977
+ function prependToPath(dirs) {
3978
+ const cur = process.env.PATH ?? "";
3979
+ const parts = new Set(cur.split(":").filter(Boolean));
3980
+ const missing = dirs.filter((d) => !parts.has(d));
3981
+ if (missing.length)
3982
+ process.env.PATH = [...missing, cur].join(":");
3983
+ return missing;
3984
+ }
3795
3985
  function ensureGitOnPath() {
3796
3986
  try {
3797
3987
  execFileSync4("git", ["--version"], { stdio: "pipe" });
3798
3988
  return;
3799
3989
  } catch {
3800
3990
  }
3801
- const cur = process.env.PATH ?? "";
3802
- const missing = GIT_CANDIDATE_DIRS.filter((d) => existsSync5(join8(d, "git")) && !cur.split(":").includes(d));
3991
+ const missing = prependToPath(GIT_CANDIDATE_DIRS.filter((d) => existsSync5(join8(d, "git"))));
3803
3992
  if (missing.length) {
3804
- process.env.PATH = [...missing, cur].join(":");
3805
3993
  console.warn(`[daemon] PATH \u7F3A\u5C11 git\uFF0C\u5DF2\u8865\u5145: ${missing.join(", ")}`);
3806
3994
  }
3807
3995
  }
3996
+ function ensureNodeOnPath() {
3997
+ const nodeDir = process.execPath ? join8(process.execPath, "..") : "";
3998
+ const candidates = [nodeDir, "/usr/local/bin", "/opt/homebrew/bin"].filter((d) => d && existsSync5(join8(d, "node")));
3999
+ const missing = prependToPath(candidates);
4000
+ if (missing.length) {
4001
+ console.warn(`[daemon] PATH \u7F3A\u5C11 node\uFF0C\u5DF2\u8865\u5145: ${missing.join(", ")}`);
4002
+ }
4003
+ }
3808
4004
  function buildRepoEnv(token, baseUrl) {
3809
4005
  if (baseUrl) {
3810
4006
  const host = new URL(baseUrl).host;
@@ -4121,9 +4317,18 @@ function normalizeOptionId(decision) {
4121
4317
  var DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1e3;
4122
4318
  var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
4123
4319
  var DEFAULT_TOOL_IDLE_TIMEOUT_MS = 20 * 60 * 1e3;
4320
+ var DEFAULT_MAX_TURNS = 8;
4321
+ var DEFAULT_TASK_WALL_CLOCK_MS = 2 * 60 * 60 * 1e3;
4322
+ var DEFAULT_STEER_SAFETY_MS = 30 * 1e3;
4124
4323
  var PROMPT_RPC_TIMEOUT_GRACE_MS = 1e4;
4125
4324
  var WATCHDOG_TICK_MS = 15e3;
4126
4325
  var KILL_GRACE_MS = 3e3;
4326
+ function resolveSteerSafetyMs(raw = process.env.MORULA_STEER_SAFETY_MS) {
4327
+ if (raw === void 0 || raw === "")
4328
+ return DEFAULT_STEER_SAFETY_MS;
4329
+ const parsed = Number(raw);
4330
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : DEFAULT_STEER_SAFETY_MS;
4331
+ }
4127
4332
  var ACP_INIT_RETRY_DELAYS_MS = [2e3, 8e3];
4128
4333
  function isAcpInitRetryableError(error) {
4129
4334
  const message = error instanceof Error ? error.message : String(error ?? "");
@@ -4218,6 +4423,11 @@ function extractProviderError(text) {
4218
4423
  }
4219
4424
  return null;
4220
4425
  }
4426
+ function payloadToolCallId(msg) {
4427
+ const payload = msg.payload ?? {};
4428
+ const raw = payload.toolCall?.id ?? payload.toolCallId ?? payload.id;
4429
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
4430
+ }
4221
4431
  function resolveAcpModelId(models2, requested, configOptions) {
4222
4432
  if (!requested.trim())
4223
4433
  return null;
@@ -4264,7 +4474,9 @@ var DimAcpExecutor = class {
4264
4474
  let ownsConnection = false;
4265
4475
  let sessionId = "";
4266
4476
  let finalText = "";
4267
- let inFlightTools = 0;
4477
+ let turnNo = 1;
4478
+ const inFlightToolCalls = /* @__PURE__ */ new Map();
4479
+ const inFlightTools = () => inFlightToolCalls.size;
4268
4480
  let lastActivityAt = Date.now();
4269
4481
  let watchdogTimer = null;
4270
4482
  let watchdogFired = false;
@@ -4322,10 +4534,12 @@ var DimAcpExecutor = class {
4322
4534
  if (idle <= 0)
4323
4535
  return;
4324
4536
  lastActivityAt = Date.now();
4325
- inFlightTools = 0;
4537
+ inFlightToolCalls.clear();
4326
4538
  watchdogTimer = setInterval(() => {
4539
+ maybeInterruptAtSafePoint();
4540
+ enforceTaskWallClock();
4327
4541
  const toolBudget = Number(process.env.MORULA_DIM_TOOL_IDLE_TIMEOUT_MS) || DEFAULT_TOOL_IDLE_TIMEOUT_MS;
4328
- const budget = inFlightTools > 0 ? toolBudget : idle;
4542
+ const budget = inFlightTools() > 0 ? toolBudget : idle;
4329
4543
  if (Date.now() - lastActivityAt > budget) {
4330
4544
  watchdogFired = true;
4331
4545
  if (sessionId && conn)
@@ -4334,7 +4548,8 @@ var DimAcpExecutor = class {
4334
4548
  finish({
4335
4549
  ok: false,
4336
4550
  error: `daemon: idle watchdog fired\uFF08${Math.round(budget / 1e3)}s \u65E0\u6D3B\u52A8\uFF09`,
4337
- sessionId: sessionId || void 0
4551
+ sessionId: sessionId || void 0,
4552
+ turns: turnNo
4338
4553
  });
4339
4554
  }
4340
4555
  }, WATCHDOG_TICK_MS);
@@ -4343,6 +4558,60 @@ var DimAcpExecutor = class {
4343
4558
  const markActivity = () => {
4344
4559
  lastActivityAt = Date.now();
4345
4560
  };
4561
+ let activeSteer = null;
4562
+ let steerWaitingSince = 0;
4563
+ let wallClockFired = false;
4564
+ let interruptedTurns = 0;
4565
+ let turnKind = "initial";
4566
+ const steerSafetyMs = resolveSteerSafetyMs();
4567
+ const taskStartedAt = Date.now();
4568
+ const maxTurns = Math.max(1, Math.trunc(opts.steer?.maxTurns ?? DEFAULT_MAX_TURNS));
4569
+ const taskWallClockMs = opts.steer?.wallClockMs ?? 0;
4570
+ const wallClockExceeded = () => taskWallClockMs > 0 && Date.now() - taskStartedAt > taskWallClockMs;
4571
+ function maybeInterruptAtSafePoint() {
4572
+ if (!opts.steer || activeSteer || !conn || !sessionId)
4573
+ return;
4574
+ const pending = opts.steer.peek();
4575
+ if (!pending) {
4576
+ steerWaitingSince = 0;
4577
+ return;
4578
+ }
4579
+ let waitedMs = 0;
4580
+ if (inFlightTools() > 0) {
4581
+ if (steerWaitingSince === 0)
4582
+ steerWaitingSince = Date.now();
4583
+ waitedMs = Date.now() - steerWaitingSince;
4584
+ if (steerSafetyMs === 0 || waitedMs < steerSafetyMs)
4585
+ return;
4586
+ }
4587
+ activeSteer = pending;
4588
+ steerWaitingSince = 0;
4589
+ opts.steer.onInterrupting({ turnNo, messageId: pending.messageId });
4590
+ process.stderr.write(`[daemon] \u5B89\u5168\u70B9\u6253\u65AD\uFF1Aturn=${turnNo} message=${pending.messageId} inFlightTools=${inFlightTools()} waited=${waitedMs}ms
4591
+ `);
4592
+ conn.cancel(sessionId);
4593
+ }
4594
+ function enforceTaskWallClock() {
4595
+ if (wallClockFired || !wallClockExceeded())
4596
+ return;
4597
+ wallClockFired = true;
4598
+ process.stderr.write(`[daemon] \u4EFB\u52A1\u5899\u949F\u8D85\u9650\uFF08${Math.round(taskWallClockMs / 1e3)}s\uFF09\uFF0C\u7EC8\u6B62\u672C\u8F6E\u5E76\u8981\u6C42\u4EBA\u4ECB\u5165
4599
+ `);
4600
+ if (sessionId && conn)
4601
+ conn.cancel(sessionId);
4602
+ }
4603
+ function emitAbortedTools() {
4604
+ const aborted = [];
4605
+ for (const [callId, toolName] of inFlightToolCalls) {
4606
+ aborted.push(toolName);
4607
+ opts.onEvent?.({
4608
+ eventType: "tool:completed",
4609
+ payload: { toolCallId: callId, toolName, toolResult: "\uFF08\u8FD0\u884C\u4E2D\u6253\u65AD\uFF1A\u8BE5\u5DE5\u5177\u8C03\u7528\u88AB\u4E2D\u65AD\uFF0C\u65E0\u7ED3\u679C\uFF09", aborted: true }
4610
+ }, finalText);
4611
+ }
4612
+ inFlightToolCalls.clear();
4613
+ return aborted;
4614
+ }
4346
4615
  const setupOnce = async () => {
4347
4616
  if (pool) {
4348
4617
  conn = await pool.acquire();
@@ -4368,12 +4637,23 @@ var DimAcpExecutor = class {
4368
4637
  }
4369
4638
  });
4370
4639
  setup().then(async () => {
4371
- timer = setTimeout(() => {
4372
- if (sessionId && conn)
4373
- conn.cancel(sessionId);
4374
- killTree(conn);
4375
- finish({ ok: false, error: `dim \u6267\u884C\u8D85\u65F6\uFF08${Math.round(timeout / 1e3)}s\uFF09`, sessionId: sessionId || void 0 });
4376
- }, timeout);
4640
+ const armTurnTimer = () => {
4641
+ if (timer)
4642
+ clearTimeout(timer);
4643
+ timer = setTimeout(() => {
4644
+ if (sessionId && conn)
4645
+ conn.cancel(sessionId);
4646
+ killTree(conn);
4647
+ finish({
4648
+ ok: false,
4649
+ error: `dim \u5355\u56DE\u5408\u8D85\u65F6\uFF08${Math.round(timeout / 1e3)}s\uFF09`,
4650
+ sessionId: sessionId || void 0,
4651
+ turns: turnNo,
4652
+ interrupted: interruptedTurns > 0
4653
+ });
4654
+ }, timeout);
4655
+ };
4656
+ armTurnTimer();
4377
4657
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
4378
4658
  startWatchdog();
4379
4659
  const bridge = new AcpPermissionBridge();
@@ -4437,56 +4717,142 @@ var DimAcpExecutor = class {
4437
4717
  pool?.touch();
4438
4718
  markActivity();
4439
4719
  opts.onEvent?.(mapper.readyMessage(sessionId), "");
4440
- const result = await conn.prompt(sessionId, opts.userPrompt, (update) => {
4441
- markActivity();
4720
+ const reopenSessionAfterInterrupt = async () => {
4721
+ try {
4722
+ await conn.resumeSession({ ...sessionParams, sessionId });
4723
+ return true;
4724
+ } catch (e1) {
4725
+ process.stderr.write(`[daemon] \u6253\u65AD\u540E\u4F1A\u8BDD\u7EED\u5F00\u5931\u8D25\uFF08resume\uFF09\uFF0C\u6539\u8BD5 load: ${errMessage(e1)}
4726
+ `);
4727
+ }
4728
+ try {
4729
+ await conn.loadSession({ ...sessionParams, sessionId });
4730
+ return true;
4731
+ } catch (e2) {
4732
+ process.stderr.write(`[daemon] \u6253\u65AD\u540E\u4F1A\u8BDD\u7EED\u5F00\u5931\u8D25\uFF08load\uFF09\uFF0C\u56DE\u9000\u65B0\u5EFA\u4F1A\u8BDD\uFF08\u4E0A\u4E0B\u6587\u672A\u4FDD\u7559\uFF09: ${errMessage(e2)}
4733
+ `);
4734
+ }
4735
+ const created = await conn.newSession(sessionParams);
4736
+ sessionId = created.sessionId;
4737
+ await conn.setConfigOption(sessionId, "permission", "workspace-write").catch(() => {
4738
+ });
4739
+ opts.onEvent?.(mapper.readyMessage(sessionId), "");
4740
+ return false;
4741
+ };
4742
+ let promptText = opts.userPrompt;
4743
+ let sessionReset = false;
4744
+ for (; ; ) {
4745
+ armTurnTimer();
4746
+ activeSteer = null;
4747
+ opts.steer?.onTurnStarted?.({ turnNo, kind: turnKind });
4748
+ const result = await conn.prompt(sessionId, promptText, (update) => {
4749
+ markActivity();
4750
+ pool?.touch();
4751
+ const { messages } = mapper.map(sessionId, update);
4752
+ for (const msg of messages) {
4753
+ if (msg.eventType === "text:delta") {
4754
+ const delta = typeof msg.payload?.delta === "string" ? msg.payload.delta : "";
4755
+ finalText += delta;
4756
+ opts.onProgress({ stage: "output", summary: delta });
4757
+ opts.onEvent?.(msg, finalText);
4758
+ } else if (msg.eventType === "thinking:delta") {
4759
+ const delta = typeof msg.payload?.delta === "string" ? msg.payload.delta : "";
4760
+ opts.onProgress({ stage: "thinking", summary: delta });
4761
+ opts.onEvent?.(msg, finalText);
4762
+ } else if (msg.eventType === "tool:started") {
4763
+ const callId = payloadToolCallId(msg) ?? `tool-${Date.now()}`;
4764
+ inFlightToolCalls.set(callId, typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool");
4765
+ opts.onEvent?.(msg, finalText);
4766
+ opts.onProgress({
4767
+ stage: "tool_call",
4768
+ toolName: typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool",
4769
+ toolInput: typeof msg.payload?.toolInput === "object" && msg.payload?.toolInput !== null ? msg.payload.toolInput : void 0,
4770
+ summary: typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool"
4771
+ });
4772
+ } else {
4773
+ if (msg.eventType === "tool:completed" || msg.eventType === "tool:failed") {
4774
+ const callId = payloadToolCallId(msg);
4775
+ if (callId)
4776
+ inFlightToolCalls.delete(callId);
4777
+ }
4778
+ opts.onEvent?.(msg, finalText);
4779
+ }
4780
+ }
4781
+ maybeInterruptAtSafePoint();
4782
+ }, timeout + PROMPT_RPC_TIMEOUT_GRACE_MS);
4783
+ if (settled)
4784
+ return;
4442
4785
  pool?.touch();
4443
- const { messages } = mapper.map(sessionId, update);
4444
- for (const msg of messages) {
4445
- if (msg.eventType === "text:delta") {
4446
- const delta = typeof msg.payload?.delta === "string" ? msg.payload.delta : "";
4447
- finalText += delta;
4448
- opts.onProgress({ stage: "output", summary: delta });
4449
- opts.onEvent?.(msg, finalText);
4450
- } else if (msg.eventType === "thinking:delta") {
4451
- const delta = typeof msg.payload?.delta === "string" ? msg.payload.delta : "";
4452
- opts.onProgress({ stage: "thinking", summary: delta });
4453
- opts.onEvent?.(msg, finalText);
4454
- } else if (msg.eventType === "tool:started") {
4455
- inFlightTools += 1;
4456
- opts.onEvent?.(msg, finalText);
4457
- opts.onProgress({
4458
- stage: "tool_call",
4459
- toolName: typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool",
4460
- toolInput: typeof msg.payload?.toolInput === "object" && msg.payload?.toolInput !== null ? msg.payload.toolInput : void 0,
4461
- summary: typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool"
4462
- });
4463
- } else {
4464
- opts.onEvent?.(msg, finalText);
4786
+ markActivity();
4787
+ const usage2 = result.usage ?? mapper.latestUsage;
4788
+ const cancelled = result.stopReason === "cancelled";
4789
+ const base = { sessionId, turns: turnNo, interrupted: interruptedTurns > 0 };
4790
+ if (wallClockFired || wallClockExceeded()) {
4791
+ const abortedTools = emitAbortedTools();
4792
+ opts.steer?.onTurnInterrupted?.({ turnNo, reason: "wall-clock", abortedTools });
4793
+ killTree(conn);
4794
+ finish({
4795
+ ...base,
4796
+ ok: false,
4797
+ error: `\u5DF2\u8FBE\u4EFB\u52A1\u5899\u949F\u4E0A\u9650\uFF08${Math.round(taskWallClockMs / 1e3)}s\uFF09\uFF0C\u5DF2\u7EC8\u6B62\u672C\u8F6E\uFF0C\u8BF7\u4EBA\u4ECB\u5165`
4798
+ });
4799
+ return;
4800
+ }
4801
+ if (cancelled && activeSteer) {
4802
+ interruptedTurns += 1;
4803
+ const abortedTools = emitAbortedTools();
4804
+ const nextTurn = turnNo + 1;
4805
+ const withinBudget = nextTurn <= maxTurns;
4806
+ opts.steer?.onTurnInterrupted?.({ turnNo, reason: withinBudget ? "steer" : "max-turns", abortedTools });
4807
+ if (!withinBudget) {
4808
+ process.stderr.write(`[daemon] \u5DF2\u8FBE\u6700\u5927\u56DE\u5408\u6570 ${maxTurns}\uFF0C\u5269\u4F59\u6253\u65AD\u8BF7\u6C42\u4EA4\u7531\u4E0B\u4E00\u8F6E
4809
+ `);
4810
+ finish({ ...base, ok: true, text: finalText, tokenUsage: usage2 });
4811
+ return;
4465
4812
  }
4813
+ turnKind = "after-interrupt";
4814
+ promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
4815
+ turnNo = nextTurn;
4816
+ sessionReset = !await reopenSessionAfterInterrupt();
4817
+ continue;
4818
+ }
4819
+ if (cancelled) {
4820
+ finish({ ...base, ok: false, error: "\u4EFB\u52A1\u88AB\u53D6\u6D88" });
4821
+ return;
4822
+ }
4823
+ if (result.stopReason === "max_tokens") {
4824
+ opts.onEvent?.(mapper.runEndedMessage({ stopReason: "max_tokens", usage: usage2 }), finalText);
4825
+ finish({ ...base, ok: false, error: "dim \u8FBE\u5230 max_tokens \u4E0A\u9650", tokenUsage: usage2 });
4826
+ return;
4827
+ }
4828
+ if (result.stopReason === "refusal") {
4829
+ opts.onEvent?.(mapper.runEndedMessage({ stopReason: "refusal", usage: usage2 }), finalText);
4830
+ const providerError = extractProviderError(finalText);
4831
+ finish({ ...base, ok: false, error: providerError ?? "\u6A21\u578B\u62D2\u7EDD\u6267\u884C", tokenUsage: usage2 });
4832
+ return;
4833
+ }
4834
+ const leftover = activeSteer ?? opts.steer?.peek() ?? null;
4835
+ if (leftover && turnNo + 1 <= maxTurns && !wallClockExceeded()) {
4836
+ const nextTurn = turnNo + 1;
4837
+ turnKind = "appended";
4838
+ promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
4839
+ turnNo = nextTurn;
4840
+ sessionReset = false;
4841
+ activeSteer = null;
4842
+ process.stderr.write(`[daemon] \u56DE\u5408\u5DF2\u81EA\u7136\u7ED3\u675F\uFF0C\u6253\u65AD\u8BF7\u6C42\u964D\u7EA7\u4E3A\u8FFD\u52A0\u4E00\u8F6E\uFF08turn=${nextTurn}\uFF09
4843
+ `);
4844
+ continue;
4466
4845
  }
4467
- }, timeout + PROMPT_RPC_TIMEOUT_GRACE_MS);
4468
- stopWatchdog();
4469
- pool?.touch();
4470
- const usage2 = result.usage ?? mapper.latestUsage;
4471
- if (result.stopReason === "cancelled") {
4472
- finish({ ok: false, error: "\u4EFB\u52A1\u88AB\u53D6\u6D88", sessionId });
4473
- } else if (result.stopReason === "max_tokens") {
4474
- opts.onEvent?.(mapper.runEndedMessage({ stopReason: "max_tokens", usage: usage2 }), finalText);
4475
- finish({ ok: false, error: "dim \u8FBE\u5230 max_tokens \u4E0A\u9650", tokenUsage: usage2, sessionId });
4476
- } else if (result.stopReason === "refusal") {
4477
- opts.onEvent?.(mapper.runEndedMessage({ stopReason: "refusal", usage: usage2 }), finalText);
4478
- const providerError = extractProviderError(finalText);
4479
- finish({ ok: false, error: providerError ?? "\u6A21\u578B\u62D2\u7EDD\u6267\u884C", tokenUsage: usage2, sessionId });
4480
- } else {
4481
4846
  opts.onEvent?.(mapper.runEndedMessage({ stopReason: result.stopReason, usage: usage2 }), finalText);
4482
- finish({ ok: true, text: finalText, tokenUsage: usage2, sessionId });
4847
+ finish({ ...base, ok: true, text: finalText, tokenUsage: usage2 });
4848
+ return;
4483
4849
  }
4484
4850
  }).catch((err) => {
4485
4851
  const message = err instanceof Error ? err.message : String(err);
4486
4852
  if (sessionId && conn && message.includes("\u8D85\u65F6")) {
4487
4853
  conn.cancel(sessionId);
4488
4854
  }
4489
- finish({ ok: false, error: message, sessionId: sessionId || void 0 });
4855
+ finish({ ok: false, error: message, sessionId: sessionId || void 0, turns: turnNo, interrupted: interruptedTurns > 0 });
4490
4856
  });
4491
4857
  });
4492
4858
  }
@@ -4639,6 +5005,8 @@ function createDimEventNormalizer(opts) {
4639
5005
  type: "tool_call_completed",
4640
5006
  callId: String(callId),
4641
5007
  toolName: typeof p.toolName === "string" ? p.toolName : void 0,
5008
+ // P3:运行中打断留下的孤儿调用(无真实结果)→ 时间线标中断点,不伪装成正常完成
5009
+ ...p.aborted === true ? { aborted: true } : {},
4642
5010
  ...outputSummary !== void 0 ? { outputSummary } : {}
4643
5011
  });
4644
5012
  break;
@@ -4719,6 +5087,16 @@ var DIM_CAPABILITIES = {
4719
5087
  authentication: "required",
4720
5088
  sessionResume: true,
4721
5089
  // ACP session/resume + session/load 跨任务续跑(2026-08-12 实测可用)
5090
+ // E4 能力矩阵(features/stage-runtime-live-chat.md §三):
5091
+ // - `checkpointInbox`(P2):合作式检查点已落地(`morula task inbox` + 契约 skill 的四处时机),
5092
+ // agent 在本回合内主动拉取未消费消息并纳入**本回合**;
5093
+ // - `midRunSteer`(P3):安全点打断已落地(等 `inFlightTools === 0` → session/cancel →
5094
+ // session/resume 同会话重开一轮,见 acp/dim-acp-executor.ts 的回合循环);
5095
+ // 破坏性档留一键回退:`MORULA_LIVE_CHAT_STEER=0` 时能力位同步为 false
5096
+ // (前端自动隐藏入口、后端 typed 拒绝),避免「按钮亮着但 daemon 不打断」的假能力。
5097
+ // 能力位只在实现真正可用后置 true —— 前端据此渲染入口,后端据此 typed 拒绝,同源判据。
5098
+ midRunSteer: isFlagEnabled("MORULA_LIVE_CHAT_STEER", true),
5099
+ checkpointInbox: true,
4722
5100
  modelSelection: "workspace_config",
4723
5101
  systemPromptMode: "native",
4724
5102
  skillInjection: "agents_skills",
@@ -4751,8 +5129,8 @@ var DimAgentAdapter = class {
4751
5129
  }
4752
5130
  }
4753
5131
  async probe() {
4754
- const { existsSync: existsSync16 } = await import("node:fs");
4755
- const installed = existsSync16(this.dimPath) || this.dimPath === "dim";
5132
+ const { existsSync: existsSync17 } = await import("node:fs");
5133
+ const installed = existsSync17(this.dimPath) || this.dimPath === "dim";
4756
5134
  let authStatus = installed ? "unknown" : "error";
4757
5135
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
4758
5136
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -4810,6 +5188,7 @@ var DimAgentAdapter = class {
4810
5188
  return await new Promise((resolve2) => {
4811
5189
  const events = [];
4812
5190
  let sessionId = null;
5191
+ let reportedSessionId = null;
4813
5192
  const normalizer = createDimEventNormalizer({
4814
5193
  onEvent: (event) => {
4815
5194
  events.push(event);
@@ -4822,6 +5201,10 @@ var DimAgentAdapter = class {
4822
5201
  const onEvent = (msg, finalText) => {
4823
5202
  if (msg.type === "ready" && typeof msg.sessionId === "string" && msg.sessionId) {
4824
5203
  sessionId = msg.sessionId;
5204
+ if (reportedSessionId !== sessionId) {
5205
+ reportedSessionId = sessionId;
5206
+ input.onEvent({ type: "session", providerSessionId: sessionId });
5207
+ }
4825
5208
  }
4826
5209
  normalizer.handle(msg, finalText);
4827
5210
  };
@@ -4854,7 +5237,9 @@ var DimAgentAdapter = class {
4854
5237
  }
4855
5238
  },
4856
5239
  timeoutMs: input.timeoutMs,
4857
- signal: input.signal
5240
+ signal: input.signal,
5241
+ // P3:运行中打断端口由上层(runner)提供;未提供时执行器保持单回合语义
5242
+ steer: input.steer ?? null
4858
5243
  }).then((result) => {
4859
5244
  const finalText = finalAssistantSelector(events);
4860
5245
  resolve2({
@@ -4864,7 +5249,9 @@ var DimAgentAdapter = class {
4864
5249
  usage: result.tokenUsage ? normalizeUsage(result.tokenUsage) : null,
4865
5250
  exitCode: result.ok ? 0 : null,
4866
5251
  error: result.ok ? null : { code: "DIM_RUN_FAILED", message: result.error ?? "dim \u6267\u884C\u5931\u8D25" },
4867
- resumeFailed: result.resumeFailed
5252
+ resumeFailed: result.resumeFailed,
5253
+ turns: result.turns,
5254
+ interrupted: result.interrupted
4868
5255
  });
4869
5256
  }).catch((err) => {
4870
5257
  resolve2({
@@ -4893,6 +5280,8 @@ var FAKE_CAPABILITIES = {
4893
5280
  approvalMode: "none",
4894
5281
  authentication: "none",
4895
5282
  sessionResume: false,
5283
+ midRunSteer: false,
5284
+ checkpointInbox: false,
4896
5285
  modelSelection: "explicit",
4897
5286
  systemPromptMode: "native",
4898
5287
  skillInjection: "prompt_only",
@@ -6091,6 +6480,144 @@ function materializeAdditionalRepo(input) {
6091
6480
  return input.targetPath;
6092
6481
  }
6093
6482
 
6483
+ // src/daemon/task-messages.ts
6484
+ var TASK_MESSAGE_QUEUE_MAX = 200;
6485
+ function asRecord(value) {
6486
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
6487
+ }
6488
+ function asNonEmptyString(value) {
6489
+ return typeof value === "string" && value.length > 0 ? value : null;
6490
+ }
6491
+ function normalizeAgentMessage(value) {
6492
+ const row = asRecord(value);
6493
+ if (!row)
6494
+ return null;
6495
+ const id = asNonEmptyString(row.id);
6496
+ if (!id)
6497
+ return null;
6498
+ const seq = row.seq;
6499
+ if (typeof seq !== "number" || !Number.isFinite(seq))
6500
+ return null;
6501
+ return {
6502
+ id,
6503
+ seq,
6504
+ authorType: typeof row.authorType === "string" ? row.authorType : "unknown",
6505
+ authorPrincipalId: asNonEmptyString(row.authorPrincipalId),
6506
+ authorName: asNonEmptyString(row.authorName),
6507
+ content: typeof row.content === "string" ? row.content : "",
6508
+ mode: typeof row.mode === "string" ? row.mode : "queue",
6509
+ deliveryState: typeof row.deliveryState === "string" ? row.deliveryState : "pending",
6510
+ deliveryError: asNonEmptyString(row.deliveryError),
6511
+ targetTaskId: asNonEmptyString(row.targetTaskId),
6512
+ anchorEventSeq: typeof row.anchorEventSeq === "number" ? row.anchorEventSeq : null,
6513
+ turnNo: typeof row.turnNo === "number" ? row.turnNo : null,
6514
+ createdAt: typeof row.createdAt === "string" ? row.createdAt : ""
6515
+ };
6516
+ }
6517
+ function parseTaskMessageFrame(value) {
6518
+ const frame = asRecord(value);
6519
+ if (!frame)
6520
+ return null;
6521
+ const taskId = asNonEmptyString(frame.taskId);
6522
+ if (!taskId || !Array.isArray(frame.messages))
6523
+ return null;
6524
+ const messages = frame.messages.map(normalizeAgentMessage).filter((message) => message !== null);
6525
+ if (messages.length === 0)
6526
+ return null;
6527
+ return {
6528
+ type: "agent-task:message",
6529
+ taskId,
6530
+ objectId: asNonEmptyString(frame.objectId),
6531
+ anchorKey: typeof frame.anchorKey === "string" ? frame.anchorKey : "",
6532
+ messages
6533
+ };
6534
+ }
6535
+ function upsertTaskMessages(existing, incoming, max = TASK_MESSAGE_QUEUE_MAX) {
6536
+ const byId = /* @__PURE__ */ new Map();
6537
+ for (const message of existing)
6538
+ byId.set(message.id, message);
6539
+ for (const message of incoming)
6540
+ byId.set(message.id, message);
6541
+ const merged = [...byId.values()].sort((a, b) => a.seq - b.seq || a.id.localeCompare(b.id));
6542
+ const dropped = Math.max(0, merged.length - max);
6543
+ return { messages: dropped > 0 ? merged.slice(dropped) : merged, dropped };
6544
+ }
6545
+ function selectConsumedMessages(messages, taskId) {
6546
+ return messages.filter((message) => message.targetTaskId === taskId);
6547
+ }
6548
+ function selectSteerMessages(messages, taskId) {
6549
+ return messages.filter((message) => message.mode === "steer").filter((message) => message.deliveryState !== "consumed" && message.deliveryState !== "withdrawn").filter((message) => message.targetTaskId === taskId).sort((a, b) => a.seq - b.seq);
6550
+ }
6551
+ function renderMessageList(messages) {
6552
+ return messages.map((message) => `${message.seq}. \u3010${message.authorType === "user" ? "\u4EBA" : message.authorType === "system" ? "\u5E73\u53F0" : "\u8FD0\u884C\u65F6"}\u3011${message.content}`).join("\n\n");
6553
+ }
6554
+ function buildSteerPrompt(input) {
6555
+ const opinions = input.messages.length > 0 ? renderMessageList(input.messages) : "\uFF08\u6253\u65AD\u8BF7\u6C42\u672A\u88AB\u53D6\u5230\uFF0C\u8BF7\u6309\u539F\u59CB\u9700\u6C42\u7EE7\u7EED\uFF09";
6556
+ const resetNote = input.sessionReset ? "\n- \u26A0\uFE0F \u4F1A\u8BDD\u5DF2\u91CD\u5EFA\uFF1A\u4E0A\u4E00\u56DE\u5408\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587**\u672A\u4FDD\u7559**\uFF0C\u8BF7\u4EE5\u672C\u56DE\u5408\u7ED9\u51FA\u7684\u4FE1\u606F\u4E3A\u51C6\uFF08\u5DE5\u4F5C\u533A\u6587\u4EF6\u4E0E git \u72B6\u6001\u4ECD\u5728\uFF09\u3002" : "";
6557
+ const originalPrompt = input.taskPrompt?.trim() ? `
6558
+
6559
+ \u3010\u539F\u59CB\u4EFB\u52A1\u9700\u6C42\uFF08\u672C\u8F6E\u662F\u540C\u4E00\u6B21\u5DE5\u4F5C\u7684\u5EF6\u7EED\uFF09\u3011
6560
+ ${input.taskPrompt.trim()}` : "";
6561
+ return `\u3010\u8FD0\u884C\u4E2D\u6253\u65AD \xB7 \u8BF7\u5148\u6309\u8FD9\u4E9B\u610F\u89C1\u8C03\u6574\uFF0C\u518D\u7EE7\u7EED\u3011
6562
+ \u4F60\u6B63\u5728\u6267\u884C\u7B2C ${input.turnNo - 1} \u56DE\u5408\uFF0C\u4EBA\u5728\u8FD0\u884C\u8FC7\u7A0B\u4E2D\u63D0\u51FA\u4E86\u4EE5\u4E0B\u610F\u89C1\uFF08\u6309\u63D0\u51FA\u987A\u5E8F\uFF09\uFF1A
6563
+
6564
+ ${opinions}
6565
+
6566
+ \u8981\u6C42\uFF1A
6567
+ 1. \u5148\u9010\u6761\u5904\u7406\u4E0A\u8FF0\u610F\u89C1\uFF0C\u518D\u7EE7\u7EED\u5B8C\u6210\u672C\u4EFB\u52A1\uFF1B\u610F\u89C1\u4E0E\u539F\u59CB\u9700\u6C42\u51B2\u7A81\u65F6**\u4EE5\u610F\u89C1\u4E3A\u51C6**\uFF0C\u5E76\u5728\u6700\u7EC8\u603B\u7ED3\u91CC\u8BF4\u660E\u5DEE\u5F02\u3002
6568
+ 2. \u4E0D\u8981\u63A8\u7FFB\u4E0A\u8FF0\u610F\u89C1\u672A\u6D89\u53CA\u7684\u90E8\u5206\uFF1B\u5DF2\u7ECF\u5B8C\u6210\u4E14\u4E0D\u5728\u610F\u89C1\u8303\u56F4\u5185\u7684\u6539\u52A8\u4FDD\u7559\u3002
6569
+ 3. \u4E0A\u4E00\u56DE\u5408\u5728\u5B89\u5168\u70B9\u88AB\u4E2D\u65AD\uFF0C\u5176\u4E2D\u7684\u5DE5\u5177\u8FDB\u5EA6\u5DF2\u4E22\u5F03\u3001\u88AB\u4E2D\u65AD\u7684\u8C03\u7528**\u6CA1\u6709\u7ED3\u679C**\uFF08\u65F6\u95F4\u7EBF\u4E0A\u6807\u4E3A\u5DF2\u4E2D\u65AD\uFF09\u2014\u2014\u9700\u8981\u65F6\u91CD\u65B0\u6267\u884C\uFF0C\u4E0D\u8981\u5047\u5B9A\u5B83\u5DF2\u6210\u529F\u3002${resetNote}${originalPrompt}`;
6570
+ }
6571
+
6572
+ // src/daemon/stage-contracts.ts
6573
+ var STAGE_CONTRACTS = {
6574
+ todo: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1A\u5F85\u5206\u6790\uFF08\u9700\u6C42\u5206\u6790\uFF09
6575
+
6576
+ \u53EA\u505A\u9700\u6C42\u5206\u6790\uFF0C\u4E0D\u5199\u4EE3\u7801\uFF1A\u5224\u65AD\u9700\u6C42\u662F\u5426\u6E05\u6670\u53EF\u5F00\u53D1\uFF0C\u4EA7\u51FA\u53EF\u6267\u884C\u7684\u65B9\u6848\u3001\u8303\u56F4\u4E0E\u98CE\u9669\u3001\u9A8C\u6536\u70B9\uFF0C\u4E3A\u5F00\u53D1\u63D0\u4F9B\u8F93\u5165\u3002\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u63D0 PR\u3002
6577
+
6578
+ \u7ED3\u8BBA\u5FC5\u987B\u4EE5\u673A\u5668\u534F\u8BAE\u4E0A\u62A5\uFF08\u547D\u4EE4\u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \u4E0E \`morula task --help\`\uFF09\uFF0C\u5426\u5219\u4EFB\u52A1\u4F1A\u505C\u5728\u300C\u5F85\u5206\u6790\u300D\u3002`,
6579
+ in_progress: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1A\u5F00\u53D1\u5B9E\u73B0
6580
+
6581
+ \u6309\u4EFB\u52A1\u63CF\u8FF0\u4E0E\u5206\u6790\u7ED3\u8BBA\u5B8C\u6210\u4EE3\u7801\u6539\u52A8\uFF0C\u672C\u5730\u81EA\u9A8C\u8BC1\uFF08\u6784\u5EFA/\u6D4B\u8BD5\uFF09\u901A\u8FC7\u540E\u63D0\u4EA4 MR\u3002\u53EA\u505A\u6700\u5C0F\u5FC5\u8981\u6539\u52A8\uFF0C\u4E0D\u505A\u65E0\u5173\u91CD\u6784\uFF1B\u4E0D\u5408\u5E76 MR\u3001\u4E0D\u505A\u8BC4\u5BA1\u7ED3\u8BBA\u3002
6582
+
6583
+ \u63D0\u4EA4 MR \u5FC5\u987B\u8D70\u5E73\u53F0\u901A\u9053\uFF08\u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \u4E0E \`morula repo --help\`\uFF09\uFF1B\u88AB\u73AF\u5883\u963B\u65AD\u65F6\u5982\u5B9E\u62A5\u544A\u963B\u585E\u539F\u56E0\uFF08PR_BLOCKED + \u539F\u56E0\uFF09\uFF0C\u4E0D\u8981\u5047\u88C5\u5B8C\u6210\u3002`,
6584
+ review: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1ACode Review
6585
+
6586
+ \u53EA\u8BFB\u8BC4\u5BA1\uFF0C\u4E0D\u4FEE\u6539\u4EE3\u7801\uFF1A\u57FA\u4E8E**\u771F\u5B9E diff**\uFF08PR \u5206\u652F\u76F8\u5BF9\u76EE\u6807\u5206\u652F\uFF09\u8BC4\u4F30\u6B63\u786E\u6027 / \u5B89\u5168\u6027 / \u98CE\u683C / \u6D4B\u8BD5\u8986\u76D6 / \u662F\u5426\u7B26\u5408\u539F\u59CB\u9700\u6C42\uFF0C\u7ED9\u51FA\u660E\u786E\u7ED3\u8BBA\u4E0E\u5177\u4F53\u610F\u89C1\u3002\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u5408\u5E76 MR\u3001\u4E0D\u8BC4\u5BA1\u81EA\u5DF1\u5F00\u53D1\u7684\u4EFB\u52A1\u3002
6587
+
6588
+ \u7ED3\u8BBA\u5FC5\u987B\u4EE5\u673A\u5668\u534F\u8BAE\u4E0A\u62A5\uFF08\u547D\u4EE4\u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \u4E0E \`morula review --help\`\uFF09\uFF0C\u672A\u4E0A\u62A5\u5219\u4EFB\u52A1\u505C\u5728\u300C\u5F85\u8BC4\u5BA1\u300D\u3002`
6589
+ };
6590
+ var AUTOPILOT_CONTRACT = `## \u672C\u4EFB\u52A1\uFF1A\u81EA\u52A8\u5316\u5DE1\u68C0\uFF08\u7531\u300C\u81EA\u52A8\u5316\u300D\u7684\u5B9A\u65F6 / Webhook / \u624B\u52A8\u89E6\u53D1\uFF09
6591
+
6592
+ \u6309\u4EFB\u52A1\u8BF4\u660E\uFF08runbook\uFF09\u6267\u884C\uFF0C\u4EA7\u51FA**\u62A5\u544A**\uFF1A\`morula report create --title <\u6807\u9898> --content-file <md>\`
6593
+ \uFF08\u7701\u7565 --autopilot \u65F6\u670D\u52A1\u7AEF\u81EA\u52A8\u5F52\u5C5E\u672C\u81EA\u52A8\u5316\uFF1B\u957F\u5185\u5BB9\u4E00\u5F8B\u7528 --content-file\uFF09\u3002
6594
+
6595
+ - \u53EA\u8BFB\u4E3A\u4E3B\uFF1A\u4E0D\u6539\u4E1A\u52A1\u6570\u636E\u3001\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u63D0 PR\uFF08\u9664\u975E\u4EFB\u52A1\u8BF4\u660E\u660E\u786E\u8981\u6C42\uFF09\u3002
6596
+ - \u7ED3\u8BBA\u5199\u8FDB\u6700\u7EC8\u8F93\u51FA\uFF0C\u5E76\u540C\u6B65\u843D\u4E00\u4EFD\u62A5\u544A\uFF1B\u62A5\u544A\u91CC\u7ED9\u300C\u6587\u4EF6:\u884C + \u4E25\u91CD\u5EA6 + \u5EFA\u8BAE\u300D\u8FD9\u7C7B\u53EF\u6267\u884C\u4FE1\u606F\u3002
6597
+ - \u4E0D\u8981\u8C03\u7528 \`morula task state\` / \`morula task analysis-verdict\` / \`morula review submit\`\uFF1A
6598
+ \u672C\u4EFB\u52A1\u6CA1\u6709\u5173\u8054\u4E1A\u52A1\u4EFB\u52A1\uFF0C\u8FD9\u4E9B\u8C03\u7528\u4F1A\u88AB\u5E73\u53F0\u62D2\u7EDD\u3002
6599
+ - \u9700\u8981\u5728 MR \u4E0A\u7559\u8BC4\u5BA1\u610F\u89C1\u65F6\u7528 \`morula repo mr comment\`\uFF08\u53EA\u5199\u8BC4\u8BBA\uFF0C\u4E0D approve\u3001\u4E0D merge\uFF09\u3002
6600
+ **\u5199\u4E4B\u524D\u5FC5\u987B\u5148 \`morula repo mr get <taskId> --mr <iid> --notes\` \u8BFB\u5DF2\u6709\u8BA8\u8BBA**\uFF1A
6601
+ \u5982\u679C\u672C\u81EA\u52A8\u5316\u5DF2\u5C31\u540C\u4E00\u95EE\u9898\u7559\u8FC7\u8BC4\u8BBA\u3001\u7ED3\u8BBA\u4E0E\u8FD9\u6B21\u4E00\u81F4\uFF0C\u5C31\u4E0D\u8981\u518D\u8BC4\u8BBA\uFF08\u5E73\u53F0\u4E0D\u505A\u53BB\u91CD\uFF0C\u91CD\u590D\u7531\u4F60\u907F\u514D\uFF09\uFF1B
6602
+ \u53EA\u6709\u51FA\u73B0\u65B0\u95EE\u9898\u6216\u7ED3\u8BBA\u53D8\u5316\u65F6\u624D\u5199\u65B0\u8BC4\u8BBA\u3002\u8BC4\u8BBA\u672B\u5C3E\u5E26\u7F72\u540D\u884C \`> \u7531\u81EA\u52A8\u5316\u300C<\u540D\u79F0>\u300D\u4E8E <\u65E5\u671F> \u751F\u6210\`\uFF0C
6603
+ \u4F9B\u4F60\u81EA\u5DF1\u5728\u4E0B\u4E00\u8F6E\u8BC6\u522B\u5386\u53F2\u8BC4\u8BBA\u3002
6604
+
6605
+ ## \u80FD\u529B\u5165\u53E3\uFF08\u6267\u884C\u65B9\u5F0F\u7531\u5E73\u53F0\u8D1F\u8D23\uFF0C\u4EFB\u52A1\u8BF4\u660E\u91CC\u4E0D\u4F1A\u5199\u8FD9\u4E9B\uFF09
6606
+
6607
+ - \u672C\u4EFB\u52A1\u4E0E\u5E73\u53F0\u4EA4\u4E92**\u53EA\u80FD**\u901A\u8FC7 \`morula\` CLI\uFF1A\u672C\u4EFB\u52A1**\u6CA1\u6709**\u5E73\u53F0\u5DE5\u5177 / MCP \u8C03\u7528\u9762
6608
+ \uFF08\`create_object\`\u3001\`create_report\` \u8FD9\u7C7B\u5DE5\u5177\u5728\u672C\u4EFB\u52A1\u5185\u4E0D\u53EF\u7528\uFF09\uFF1B\u547D\u4EE4\u53C2\u6570\u4EE5 \`morula <resource> --help\` \u4E3A\u51C6\u3002
6609
+ - \u4ED3\u5E93\u8BFB\u53D6\u4E0E\u6539\u52A8\u3001\u5DE5\u4F5C\u533A\u5E03\u5C40\u3001\u5206\u652F\u4E0E MR \u89C4\u5219\u89C1\u5DF2\u6CE8\u5165\u7684\u534F\u4F5C\u5951\u7EA6 skill\uFF08\xA70.5 \u4ED3\u5E93\u4E0E\u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002
6610
+ - \u8981\u7ED9\u4EBA\u751F\u6210\u5F85\u8DDF\u8FDB\u7684\u4EFB\u52A1\uFF1A\`morula task create --space <spaceId> --title <\u6807\u9898> --description <\u5185\u5BB9>\`
6611
+ \uFF08\`<spaceId>\` \u53D6 \`morula task get\` \u8FD4\u56DE\u7684 \`contextSnapshot\`\uFF09\uFF1B\u65B0\u5EFA\u4EFB\u52A1\u9ED8\u8BA4\u843D\u300C\u5F85\u89C4\u5212\u300D\uFF0C
6612
+ \u7531\u4EBA\u786E\u8BA4\u540E\u518D\u6D3E\u53D1\u4FEE\u590D\u3002\u62A5\u544A\u4E0E\u4EFB\u52A1\u662F\u4E24\u4E2A\u57DF\uFF0C\u4E0D\u8981\u628A\u5F85\u529E\u4E8B\u9879\u5199\u8FDB\u62A5\u544A\u6B63\u6587\u5F53\u4EFB\u52A1\u7528\u3002`;
6613
+ function buildStageContract(stageCode, source) {
6614
+ if (source === "autopilot")
6615
+ return AUTOPILOT_CONTRACT;
6616
+ if (!stageCode)
6617
+ return "";
6618
+ return STAGE_CONTRACTS[stageCode] ?? "";
6619
+ }
6620
+
6094
6621
  // src/daemon/ws-client.ts
6095
6622
  import { createHash as createHash2 } from "node:crypto";
6096
6623
  import { io } from "socket.io-client";
@@ -6121,10 +6648,49 @@ var DaemonWsClient = class {
6121
6648
  authReloads = 0;
6122
6649
  reconnectAttempts = 0;
6123
6650
  pollOnly = false;
6651
+ /**
6652
+ * P1:已订阅的任务房间(taskId → afterSeq)。**订阅意图独立于连接状态**——
6653
+ * 连接未就绪(初始连接中 / 重连中 / 凭证重载重建 socket)时先登记,连上后由 `joinTaskRooms()` 重放,
6654
+ * 否则「连上之前 claim 到的任务」永远收不到消息(服务端不发 tenant 广播)。
6655
+ */
6656
+ taskRooms = /* @__PURE__ */ new Map();
6124
6657
  /** 是否已降级为 poll-only(认证长期失败;runner 的 poll 兜底继续工作) */
6125
6658
  get isPollOnly() {
6126
6659
  return this.pollOnly;
6127
6660
  }
6661
+ /** 当前已订阅的任务房间(诊断/测试用只读快照) */
6662
+ get subscribedTaskIds() {
6663
+ return [...this.taskRooms.keys()];
6664
+ }
6665
+ /**
6666
+ * P1:订阅 `task:<taskId>` 房间(幂等)——本 run 的对话消息(`agent-task:message`)只发进该房间。
6667
+ *
6668
+ * 连接未就绪时只登记(连上后 `joinTaskRooms()` 补发),保证「先 claim 后连上」也不漏帧。
6669
+ * `afterSeq`:与前端同口径的订阅水位(补齐由 HTTP 拉取负责,见 runner 的 catch-up)。
6670
+ */
6671
+ subscribeTask(taskId, afterSeq = 0) {
6672
+ this.taskRooms.set(taskId, afterSeq);
6673
+ if (this.socket?.connected)
6674
+ this.socket.emit("subscribe:task", { taskId, afterSeq });
6675
+ }
6676
+ /**
6677
+ * P1:退订 `task:<taskId>` 房间(run 终态调用;未订阅时是 no-op)。
6678
+ * 意图表与 socket 两侧都要清——否则重连重放会把已结束任务的房间重新订上(泄漏:消息队列只增不减)。
6679
+ */
6680
+ unsubscribeTask(taskId) {
6681
+ if (!this.taskRooms.delete(taskId))
6682
+ return;
6683
+ if (this.socket?.connected)
6684
+ this.socket.emit("unsubscribe:task", { taskId });
6685
+ }
6686
+ /** 把已登记的任务房间订阅重放到当前 socket(首次连接 / 重连 / 换票重建都走这里) */
6687
+ joinTaskRooms() {
6688
+ const socket = this.socket;
6689
+ if (!socket)
6690
+ return;
6691
+ for (const [taskId, afterSeq] of this.taskRooms)
6692
+ socket.emit("subscribe:task", { taskId, afterSeq });
6693
+ }
6128
6694
  /** 当前凭证指纹(日志用;不泄露本体) */
6129
6695
  get tokenFingerprint() {
6130
6696
  return tokenFingerprint(this.currentToken);
@@ -6162,9 +6728,12 @@ var DaemonWsClient = class {
6162
6728
  this.reconnectAttempts = 0;
6163
6729
  this.authReloads = 0;
6164
6730
  console.log(`[daemon] WS \u5DF2\u8FDE\u63A5\uFF08${(/* @__PURE__ */ new Date()).toISOString()}\uFF09`);
6731
+ this.joinTaskRooms();
6732
+ for (const taskId of this.taskRooms.keys())
6733
+ handlers.onTaskResubscribed?.(taskId);
6165
6734
  });
6166
6735
  socket.on("agent-task:dispatched", (payload) => {
6167
- handlers.onTaskDispatched(payload.taskId);
6736
+ handlers.onTaskDispatched(payload.taskId, payload.runtimeId);
6168
6737
  });
6169
6738
  socket.on("agent-task:status-changed", (payload) => {
6170
6739
  if (payload?.status === "cancelled" && payload.taskId) {
@@ -6180,6 +6749,11 @@ var DaemonWsClient = class {
6180
6749
  void reason;
6181
6750
  handlers.onDisconnected();
6182
6751
  });
6752
+ socket.on("agent-task:message", (payload) => {
6753
+ const event = parseTaskMessageFrame(payload);
6754
+ if (event)
6755
+ handlers.onTaskMessage(event);
6756
+ });
6183
6757
  socket.on("connect_error", (error) => {
6184
6758
  this.onConnectError(socket, error);
6185
6759
  });
@@ -6277,30 +6851,6 @@ var DaemonWsClient = class {
6277
6851
  }
6278
6852
  };
6279
6853
 
6280
- // src/daemon/stage-contracts.ts
6281
- var STAGE_CONTRACTS = {
6282
- todo: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1A\u5F85\u5206\u6790\uFF08\u9700\u6C42\u5206\u6790\uFF09
6283
-
6284
- \u53EA\u505A\u9700\u6C42\u5206\u6790\uFF0C\u4E0D\u5199\u4EE3\u7801\uFF1A\u5224\u65AD\u9700\u6C42\u662F\u5426\u6E05\u6670\u53EF\u5F00\u53D1\uFF0C\u4EA7\u51FA\u53EF\u6267\u884C\u7684\u65B9\u6848\u3001\u8303\u56F4\u4E0E\u98CE\u9669\u3001\u9A8C\u6536\u70B9\uFF0C\u4E3A\u5F00\u53D1\u63D0\u4F9B\u8F93\u5165\u3002\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u63D0 PR\u3002
6285
-
6286
- \u7ED3\u8BBA\u5FC5\u987B\u4EE5\u673A\u5668\u534F\u8BAE\u4E0A\u62A5\uFF08\u547D\u4EE4\u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \u4E0E \`morula task --help\`\uFF09\uFF0C\u5426\u5219\u4EFB\u52A1\u4F1A\u505C\u5728\u300C\u5F85\u5206\u6790\u300D\u3002`,
6287
- in_progress: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1A\u5F00\u53D1\u5B9E\u73B0
6288
-
6289
- \u6309\u4EFB\u52A1\u63CF\u8FF0\u4E0E\u5206\u6790\u7ED3\u8BBA\u5B8C\u6210\u4EE3\u7801\u6539\u52A8\uFF0C\u672C\u5730\u81EA\u9A8C\u8BC1\uFF08\u6784\u5EFA/\u6D4B\u8BD5\uFF09\u901A\u8FC7\u540E\u63D0\u4EA4 MR\u3002\u53EA\u505A\u6700\u5C0F\u5FC5\u8981\u6539\u52A8\uFF0C\u4E0D\u505A\u65E0\u5173\u91CD\u6784\uFF1B\u4E0D\u5408\u5E76 MR\u3001\u4E0D\u505A\u8BC4\u5BA1\u7ED3\u8BBA\u3002
6290
-
6291
- \u63D0\u4EA4 MR \u5FC5\u987B\u8D70\u5E73\u53F0\u901A\u9053\uFF08\u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \u4E0E \`morula repo --help\`\uFF09\uFF1B\u88AB\u73AF\u5883\u963B\u65AD\u65F6\u5982\u5B9E\u62A5\u544A\u963B\u585E\u539F\u56E0\uFF08PR_BLOCKED + \u539F\u56E0\uFF09\uFF0C\u4E0D\u8981\u5047\u88C5\u5B8C\u6210\u3002`,
6292
- review: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1ACode Review
6293
-
6294
- \u53EA\u8BFB\u8BC4\u5BA1\uFF0C\u4E0D\u4FEE\u6539\u4EE3\u7801\uFF1A\u57FA\u4E8E**\u771F\u5B9E diff**\uFF08PR \u5206\u652F\u76F8\u5BF9\u76EE\u6807\u5206\u652F\uFF09\u8BC4\u4F30\u6B63\u786E\u6027 / \u5B89\u5168\u6027 / \u98CE\u683C / \u6D4B\u8BD5\u8986\u76D6 / \u662F\u5426\u7B26\u5408\u539F\u59CB\u9700\u6C42\uFF0C\u7ED9\u51FA\u660E\u786E\u7ED3\u8BBA\u4E0E\u5177\u4F53\u610F\u89C1\u3002\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u5408\u5E76 MR\u3001\u4E0D\u8BC4\u5BA1\u81EA\u5DF1\u5F00\u53D1\u7684\u4EFB\u52A1\u3002
6295
-
6296
- \u7ED3\u8BBA\u5FC5\u987B\u4EE5\u673A\u5668\u534F\u8BAE\u4E0A\u62A5\uFF08\u547D\u4EE4\u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \u4E0E \`morula review --help\`\uFF09\uFF0C\u672A\u4E0A\u62A5\u5219\u4EFB\u52A1\u505C\u5728\u300C\u5F85\u8BC4\u5BA1\u300D\u3002`
6297
- };
6298
- function buildStageContract(stageCode) {
6299
- if (!stageCode)
6300
- return "";
6301
- return STAGE_CONTRACTS[stageCode] ?? "";
6302
- }
6303
-
6304
6854
  // src/daemon/runner.ts
6305
6855
  var HEARTBEAT_INTERVAL_MS = 3e4;
6306
6856
  var POLL_INTERVAL_MS = 15e3;
@@ -6308,6 +6858,30 @@ var TICK_BUDGET_MS = 6e4;
6308
6858
  var OUTBOX_REPLAY_MAX_FAILURES = 5;
6309
6859
  var OUTBOX_REPLAY_PAUSE_MS = 5 * 6e4;
6310
6860
  var OUTBOX_MAX_ATTEMPTS_PER_ROUND = 2;
6861
+ var PROGRESS_BATCH_MAX = 500;
6862
+ var OBSERVATION_WARN_INTERVAL_MS = 3e4;
6863
+ var TERMINAL_OBSERVATION_DRAIN_BUDGET_MS = Number(process.env.MORULA_TERMINAL_DRAIN_BUDGET_MS ?? 2e3);
6864
+ async function drainObservationTail(args) {
6865
+ const startedAt = Date.now();
6866
+ const progressDrained = await waitWithin(args.progress, args.budgetMs);
6867
+ const remaining = args.budgetMs - (Date.now() - startedAt);
6868
+ await waitWithin(args.stream, Math.max(0, remaining));
6869
+ return { progressDrained, streamDrained: !args.streamPending() };
6870
+ }
6871
+ async function waitWithin(promise, ms) {
6872
+ if (ms <= 0)
6873
+ return false;
6874
+ let timer;
6875
+ const timeout = new Promise((resolve2) => {
6876
+ timer = setTimeout(resolve2, ms, false);
6877
+ timer.unref?.();
6878
+ });
6879
+ const settled = promise.then(() => true, () => true);
6880
+ const done = await Promise.race([settled, timeout]);
6881
+ if (timer)
6882
+ clearTimeout(timer);
6883
+ return done;
6884
+ }
6311
6885
  var OUTBOX_PERMANENT_CODES = ["NOT_FOUND", "CONFLICT"];
6312
6886
  var DEFAULT_MODEL = "deepseek-v4-flash";
6313
6887
  var DaemonRunner = class {
@@ -6346,6 +6920,16 @@ var DaemonRunner = class {
6346
6920
  cancels = /* @__PURE__ */ new Map();
6347
6921
  /** Phase 2:审批决策等待表(approvalId → resolver;WS 事件 resolve,轮询兜底) */
6348
6922
  pendingApprovals = /* @__PURE__ */ new Map();
6923
+ /**
6924
+ * P1(features/stage-runtime-live-chat.md §P1-9):本 run 的对话消息内存队列
6925
+ * (taskId → 按 seq 升序的消息)。与 `cancels` / `pendingApprovals` 同款:进程内 per-task 状态,
6926
+ * `handleTask` 收尾清空(跨任务不泄漏)。
6927
+ *
6928
+ * 用途只有两个:① ACP 会话建立瞬间对「本轮吃掉的」消息回执 `consumed`;
6929
+ * ② WS 帧与 HTTP 补齐的**去重合并**(同一条消息两条通道都会来)。
6930
+ * 消息的权威状态与投递都在服务端,这里只是镜像(不做投递决策)。
6931
+ */
6932
+ pendingMessages = /* @__PURE__ */ new Map();
6349
6933
  dimPath;
6350
6934
  model;
6351
6935
  /** 独立 dim 数据空间 env 覆盖(DIMCODE_HOME=~/.morula/dimcode;空对象=初始化失败回退共享空间) */
@@ -6359,6 +6943,7 @@ var DaemonRunner = class {
6359
6943
  return;
6360
6944
  this.running = true;
6361
6945
  ensureGitOnPath();
6946
+ ensureNodeOnPath();
6362
6947
  {
6363
6948
  const cfg2 = loadDaemonConfig(this.store);
6364
6949
  try {
@@ -6439,31 +7024,7 @@ var DaemonRunner = class {
6439
7024
  this.log(`[daemon] \u5DF2\u6CE8\u518C runtime=${registered.id} daemonId=${cfg.daemonId}`);
6440
7025
  this.repoCheckout?.setRuntimeId(registered.id);
6441
7026
  this.startHeartbeatTimer();
6442
- this.ws.connect({
6443
- onTaskDispatched: (taskId) => {
6444
- void this.handleTask(taskId);
6445
- },
6446
- // M1-A1:取消信号 → 中断执行中的 dim 进程
6447
- onTaskCancelled: (taskId) => {
6448
- const ctrl = this.cancels.get(taskId);
6449
- if (ctrl) {
6450
- this.log(`[daemon] \u6536\u5230\u53D6\u6D88\u4FE1\u53F7 task=${taskId}\uFF0C\u4E2D\u65AD\u6267\u884C`);
6451
- ctrl.abort();
6452
- this.cancels.delete(taskId);
6453
- }
6454
- },
6455
- onDisconnected: () => {
6456
- },
6457
- // Phase 2:审批决策 → resolve 等待者(daemon 侧 ACP permission/response)
6458
- onApprovalDecision: (event) => {
6459
- const waiter = this.pendingApprovals.get(event.approvalId);
6460
- if (waiter) {
6461
- clearTimeout(waiter.timer);
6462
- this.pendingApprovals.delete(event.approvalId);
6463
- waiter.resolve(event.decision);
6464
- }
6465
- }
6466
- });
7027
+ this.ws.connect(this.buildWsHandlers());
6467
7028
  void this.replayOutbox();
6468
7029
  while (this.running) {
6469
7030
  await this.tick();
@@ -6593,6 +7154,7 @@ var DaemonRunner = class {
6593
7154
  }
6594
7155
  } catch {
6595
7156
  }
7157
+ void this.catchUpTaskMessages(taskId);
6596
7158
  }
6597
7159
  void this.replayOutbox();
6598
7160
  try {
@@ -6782,6 +7344,62 @@ var DaemonRunner = class {
6782
7344
  }, Math.max(0, nextAt - Date.now()));
6783
7345
  this.outboxTimer.unref?.();
6784
7346
  }
7347
+ /**
7348
+ * WS 下行事件 → runner 逻辑的接线(§6.2 主通道)。
7349
+ *
7350
+ * 抽成方法而不是内联在 `start()` 里:`start()` 需要注册/网络才跑得起来,
7351
+ * 拆开后单测可直接拿到 handlers 驱动事件(不必起 socket、不必真连服务端)。
7352
+ */
7353
+ buildWsHandlers() {
7354
+ return {
7355
+ onTaskDispatched: (taskId, runtimeId) => {
7356
+ this.onTaskDispatched(taskId, runtimeId);
7357
+ },
7358
+ // M1-A1:取消信号 → 中断执行中的 dim 进程
7359
+ onTaskCancelled: (taskId) => {
7360
+ const ctrl = this.cancels.get(taskId);
7361
+ if (ctrl) {
7362
+ this.log(`[daemon] \u6536\u5230\u53D6\u6D88\u4FE1\u53F7 task=${taskId}\uFF0C\u4E2D\u65AD\u6267\u884C`);
7363
+ ctrl.abort();
7364
+ this.cancels.delete(taskId);
7365
+ }
7366
+ },
7367
+ onDisconnected: () => {
7368
+ },
7369
+ // Phase 2:审批决策 → resolve 等待者(daemon 侧 ACP permission/response)
7370
+ onApprovalDecision: (event) => {
7371
+ const waiter = this.pendingApprovals.get(event.approvalId);
7372
+ if (waiter) {
7373
+ clearTimeout(waiter.timer);
7374
+ this.pendingApprovals.delete(event.approvalId);
7375
+ waiter.resolve(event.decision);
7376
+ }
7377
+ },
7378
+ // P1:本 run 的对话消息(房间 task:<taskId>)→ 去重合并进内存队列;
7379
+ // 回执时机是「ACP 会话建立瞬间」(见 runOnce 的 session 分支),不是收到就回执。
7380
+ onTaskMessage: (event) => {
7381
+ this.enqueueTaskMessages(event);
7382
+ },
7383
+ // P1:重连后房间已重放 → 立即按 afterSeq 重新补齐(断连期间的帧不会补发,只能拉)
7384
+ onTaskResubscribed: (taskId) => {
7385
+ void this.catchUpTaskMessages(taskId);
7386
+ }
7387
+ };
7388
+ }
7389
+ /**
7390
+ * 派发事件入口(P0-b,issues/agent-task-false-failure-on-daemon-fetch-error.md):
7391
+ * 服务端把派发事件发到 **tenant 房间**(daemon-executor.service.ts `sendToTenant`,WS 无 per-runtime
7392
+ * 房间),同租户每台 daemon 都会收到,故**必须按 `runtimeId` 自过滤**——否则每台设备都会替每条任务
7393
+ * 去 claim,任一台设备的一次网络抖动都会把归属设备正在跑的任务判失败(`POST /agent-tasks/:id/result`
7394
+ * 不校验任务归属,见同 issue 根因 ③ / ④)。
7395
+ *
7396
+ * `runtimeId` 缺失(旧服务端)→ 保持旧行为不过滤;poll 兜底本就按 runtimeId 查询,不受影响。
7397
+ */
7398
+ onTaskDispatched(taskId, runtimeId) {
7399
+ if (runtimeId && this.runtimeId && runtimeId !== this.runtimeId)
7400
+ return;
7401
+ void this.handleTask(taskId);
7402
+ }
6785
7403
  /** 任务处理(幂等:已在处理中则跳过) */
6786
7404
  async handleTask(taskId) {
6787
7405
  if (this.processing.has(taskId))
@@ -6789,6 +7407,7 @@ var DaemonRunner = class {
6789
7407
  this.processing.add(taskId);
6790
7408
  const ctrl = new AbortController();
6791
7409
  this.cancels.set(taskId, ctrl);
7410
+ let claimed = false;
6792
7411
  try {
6793
7412
  if (!this.runtimeId)
6794
7413
  return;
@@ -6797,6 +7416,9 @@ var DaemonRunner = class {
6797
7416
  this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u9886\u53D6\u5931\u8D25: ${claim.code}`);
6798
7417
  return;
6799
7418
  }
7419
+ claimed = true;
7420
+ this.ws.subscribeTask(taskId, 0);
7421
+ void this.catchUpTaskMessages(taskId);
6800
7422
  await this.executeTask(taskId, ctrl.signal);
6801
7423
  } catch (error) {
6802
7424
  if (ctrl.signal.aborted) {
@@ -6804,15 +7426,158 @@ var DaemonRunner = class {
6804
7426
  return;
6805
7427
  }
6806
7428
  const message = error instanceof Error ? error.message : String(error);
7429
+ if (!claimed) {
7430
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u672A\u53D6\u5F97\u6267\u884C\u6743\uFF0C\u5FFD\u7565\u672C\u6B21\u5F02\u5E38\uFF08\u4E0D\u4E0A\u62A5\u5931\u8D25\uFF09: ${message}`);
7431
+ return;
7432
+ }
6807
7433
  this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u5904\u7406\u5F02\u5E38: ${message}`);
6808
7434
  await this.submitResultReliable(taskId, { ok: false, error: message }).catch(() => {
6809
7435
  });
6810
7436
  } finally {
6811
7437
  this.cancels.delete(taskId);
6812
7438
  this.processing.delete(taskId);
7439
+ this.ws.unsubscribeTask(taskId);
7440
+ this.pendingMessages.delete(taskId);
6813
7441
  this.api.clearTaskToken(taskId);
6814
7442
  }
6815
7443
  }
7444
+ /**
7445
+ * P1 消息入队(§P1-9 第 2/3 条):`agent-task:message` 帧与 `GET /chat` 补齐共用同一条合并路径
7446
+ * (合并规则见 `upsertTaskMessages`:按 id 去重、按 seq 升序、超上限丢最旧)。
7447
+ *
7448
+ * 只接受**正在本机处理**的任务:房间退订后仍可能到达迟到帧,别的任务/别的设备的帧也可能因重连重放而来。
7449
+ * 若不设这道闸,队列会出现无人回收的 taskId(收尾只清 `processing` 里的任务)→ 跨任务泄漏。
7450
+ */
7451
+ enqueueTaskMessages(frame) {
7452
+ if (!this.processing.has(frame.taskId))
7453
+ return;
7454
+ const { messages, dropped } = upsertTaskMessages(this.pendingMessages.get(frame.taskId) ?? [], frame.messages);
7455
+ this.pendingMessages.set(frame.taskId, messages);
7456
+ if (dropped > 0)
7457
+ this.log(`[daemon] \u4EFB\u52A1 ${frame.taskId} \u5BF9\u8BDD\u6D88\u606F\u961F\u5217\u8D85\u4E0A\u9650 ${TASK_MESSAGE_QUEUE_MAX}\uFF0C\u5DF2\u4E22\u5F03\u6700\u65E7 ${dropped} \u6761`);
7458
+ }
7459
+ /**
7460
+ * P1 补齐(§P1-9 第 3 条):订阅后 / 重连重订阅后拉本线程消息,与 WS 帧走同一条去重合并。
7461
+ *
7462
+ * **拉取才是 correctness path**(WS 只是唤醒,断了就不重发):这是「跑通期间收到的消息一条不丢」的保证。
7463
+ * 失败**只记日志**:补齐是兜底能力,不能反过来打断正在跑的 run(旧服务端无此端点 → 404 也走这里)。
7464
+ */
7465
+ async catchUpTaskMessages(taskId) {
7466
+ try {
7467
+ const chat = await this.api.listTaskChat(taskId, { afterSeq: 0 });
7468
+ const rows = chat?.messages ?? [];
7469
+ if (rows.length === 0)
7470
+ return;
7471
+ this.enqueueTaskMessages({
7472
+ type: "agent-task:message",
7473
+ taskId,
7474
+ objectId: chat?.thread?.objectId ?? null,
7475
+ anchorKey: chat?.thread?.anchorKey ?? "",
7476
+ messages: rows
7477
+ });
7478
+ } catch (error) {
7479
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u5BF9\u8BDD\u6D88\u606F\u8865\u9F50\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
7480
+ }
7481
+ }
7482
+ /**
7483
+ * P1/P3 消费回执:对**本轮真正进入执行上下文**的消息回执 `consumed{turnNo}`。
7484
+ *
7485
+ * 判据严格限定 `targetTaskId === taskId`:这类消息才是服务端合并进本轮 prompt 的。
7486
+ * `targetTaskId` 为 null / 别的 run 的消息**仍在服务端排队**,会由投递器合并进后续 follow-up run ——
7487
+ * 对它们回执等于把话丢掉。`delivered` 不回执(服务端创建 follow-up run 时自己置位)。
7488
+ *
7489
+ * `excludeSteer`(P3):首回合**不能**把打断请求算成「已消费」——它没进首回合的 prompt,
7490
+ * 而是要在下一个安全点打断后由 `consumeSteer` 注入下一回合(见 `buildSteerPort`)。
7491
+ *
7492
+ * fire-and-forget + `.catch(log)`:服务端行状态是权威,丢一次回执不影响消息本身,
7493
+ * 但绝不能因此拖慢或打断本轮执行。回执发出后即从内存队列移除(本 run 只消费一次)。
7494
+ */
7495
+ ackConsumedMessages(taskId, turnNo = 1, options = {}) {
7496
+ const queued = this.pendingMessages.get(taskId);
7497
+ if (!queued || queued.length === 0)
7498
+ return [];
7499
+ const consumed = selectConsumedMessages(queued, taskId).filter((message) => !options.excludeSteer || message.mode !== "steer");
7500
+ if (consumed.length === 0)
7501
+ return [];
7502
+ for (const message of consumed) {
7503
+ void this.api.ackMessage(taskId, message.id, { state: "consumed", turnNo }).catch((error) => {
7504
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u6D88\u606F\u56DE\u6267\u5931\u8D25 message=${message.id}: ${error instanceof Error ? error.message : String(error)}`);
7505
+ });
7506
+ }
7507
+ const consumedIds = new Set(consumed.map((message) => message.id));
7508
+ this.pendingMessages.set(taskId, queued.filter((message) => !consumedIds.has(message.id)));
7509
+ return consumed.map((message) => message.id);
7510
+ }
7511
+ /**
7512
+ * P3:本 run 待处理的打断请求(按 seq 升序,取一条用于触发;`consume` 时全部取走)。
7513
+ * 判据见 `selectSteerMessages`(严格要求已投给本 run)。
7514
+ */
7515
+ pendingSteerMessages(taskId) {
7516
+ return selectSteerMessages(this.pendingMessages.get(taskId) ?? [], taskId);
7517
+ }
7518
+ /**
7519
+ * 取走打断请求并给出下一回合的 prompt(P3)。取走即回执 `consumed{turnNo}`——
7520
+ * 此刻这句话**确实**进了下一回合的 prompt,语义诚实(不是「猜它进去了」)。
7521
+ */
7522
+ consumeSteer(taskId, detail, turnNo, sessionReset) {
7523
+ const messages = this.pendingSteerMessages(taskId);
7524
+ if (messages.length > 0) {
7525
+ const ids = new Set(messages.map((message) => message.id));
7526
+ this.pendingMessages.set(taskId, (this.pendingMessages.get(taskId) ?? []).filter((message) => !ids.has(message.id)));
7527
+ for (const message of messages) {
7528
+ void this.api.ackMessage(taskId, message.id, { state: "consumed", turnNo }).catch((error) => {
7529
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u6253\u65AD\u6D88\u606F\u56DE\u6267\u5931\u8D25 message=${message.id}: ${error instanceof Error ? error.message : String(error)}`);
7530
+ });
7531
+ }
7532
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u6253\u65AD\u8BF7\u6C42\u5DF2\u6CE8\u5165\u7B2C ${turnNo} \u56DE\u5408 prompt\uFF08${messages.length} \u6761\uFF09`);
7533
+ } else {
7534
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u8FDB\u5165\u7B2C ${turnNo} \u56DE\u5408\uFF0C\u4F46\u6253\u65AD\u8BF7\u6C42\u5DF2\u88AB\u6E05\u7A7A\uFF08\u6309\u539F\u59CB\u9700\u6C42\u7EE7\u7EED\uFF09`);
7535
+ }
7536
+ return buildSteerPrompt({
7537
+ turnNo,
7538
+ messages,
7539
+ taskPrompt: detail.taskPrompt,
7540
+ sessionReset
7541
+ });
7542
+ }
7543
+ /**
7544
+ * P3:运行中打断端口(provider 侧的「怎么停、下一轮说什么」全从这里取,见 `ProviderSteerPort`)。
7545
+ *
7546
+ * `MORULA_LIVE_CHAT_STEER=0` 时不提供端口 = 退回单回合(steer 消息留在队列里,
7547
+ * run 终态由服务端 D27 按 follow-up 轮次投递)——破坏性档必须有一键回退路径(§八)。
7548
+ */
7549
+ buildSteerPort(taskId, detail) {
7550
+ if (!isFlagEnabled("MORULA_LIVE_CHAT_STEER", true))
7551
+ return null;
7552
+ return {
7553
+ peek: () => {
7554
+ const [first] = this.pendingSteerMessages(taskId);
7555
+ return first ? { messageId: first.id } : null;
7556
+ },
7557
+ onInterrupting: ({ turnNo, messageId }) => {
7558
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u6253\u65AD\u4E2D\uFF08\u7B2C ${turnNo} \u56DE\u5408\uFF0C\u5B89\u5168\u70B9\u4E2D\u65AD\uFF09message=${messageId}`);
7559
+ void this.api.ackMessage(taskId, messageId, { state: "interrupting", turnNo }).catch((error) => {
7560
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u6253\u65AD\u4E2D\u56DE\u6267\u5931\u8D25 message=${messageId}: ${error instanceof Error ? error.message : String(error)}`);
7561
+ });
7562
+ },
7563
+ consume: ({ turnNo, sessionReset }) => this.consumeSteer(taskId, detail, turnNo, sessionReset === true),
7564
+ onTurnStarted: ({ turnNo, kind }) => {
7565
+ void this.api.recordEvent(taskId, { type: "turn_started", turnNo, output: { kind } }).catch(() => {
7566
+ });
7567
+ if (kind === "initial")
7568
+ this.ackConsumedMessages(taskId, turnNo, { excludeSteer: true });
7569
+ },
7570
+ onTurnInterrupted: ({ turnNo, reason, abortedTools }) => {
7571
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u7B2C ${turnNo} \u56DE\u5408\u88AB\u4E2D\u65AD\uFF08${reason}${abortedTools.length > 0 ? `\uFF0C\u5728\u98DE\u5DE5\u5177 ${abortedTools.length} \u4E2A\u5DF2\u6807 aborted` : ""}\uFF09`);
7572
+ void this.api.recordEvent(taskId, {
7573
+ type: "turn_interrupted",
7574
+ turnNo,
7575
+ output: { reason, abortedTools }
7576
+ }).catch(() => {
7577
+ });
7578
+ }
7579
+ };
7580
+ }
6816
7581
  async executeTask(taskId, signal) {
6817
7582
  const detail = await this.api.getTask(taskId);
6818
7583
  this.log(`[daemon] \u5F00\u59CB\u4EFB\u52A1 ${taskId}\uFF08${detail.executor.executorName}\uFF09`);
@@ -6926,7 +7691,15 @@ var DaemonRunner = class {
6926
7691
  writeCustomSkills(taskRoot, customSkills, this.log);
6927
7692
  this.log(`[daemon] \u5DF2\u6CE8\u5165 ${customSkills.length} \u4E2A\u81EA\u5B9A\u4E49 skill \u5230 .agents/skills/`);
6928
7693
  }
6929
- writeCollaborationSkill(taskRoot, repo, repoBaseUrl, this.log, detail.workContext?.baseBranch);
7694
+ const contractPlatform = resolveContractPlatform(detail.availableRepos, repo, repoBaseUrl, detail.workContext?.baseBranch);
7695
+ writeCollaborationSkill(
7696
+ taskRoot,
7697
+ contractPlatform.repoFullName,
7698
+ contractPlatform.baseUrl,
7699
+ this.log,
7700
+ contractPlatform.baseBranch,
7701
+ contractPlatform.provider
7702
+ );
6930
7703
  writeTaskContextBrief(taskRoot, taskId, detail, repo, this.log);
6931
7704
  writeProjectAgentsMd(taskRoot, detail.projectAgentsMd, this.log);
6932
7705
  return { taskRoot, repoDir, worktreePath, env, repoBaseUrl };
@@ -7085,6 +7858,20 @@ var DaemonRunner = class {
7085
7858
  let thoughtBuf = "";
7086
7859
  let thoughtTimer = null;
7087
7860
  let streamFlushing = false;
7861
+ const observationWarnAt = {};
7862
+ const observationStats = { progressFailed: 0, progressDropped: 0, streamFailed: 0, streamDropped: 0 };
7863
+ let progressDropAll = false;
7864
+ let streamDropAll = false;
7865
+ let progressPending = 0;
7866
+ let streamInFlight = 0;
7867
+ const streamPendingCount = () => streamChunks.length + streamInFlight;
7868
+ const logObservation = (channel, event, reason, total) => {
7869
+ const now = Date.now();
7870
+ if (now - (observationWarnAt[channel] ?? 0) < OBSERVATION_WARN_INTERVAL_MS)
7871
+ return;
7872
+ observationWarnAt[channel] = now;
7873
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} ${channel} ${event}\uFF08\u7D2F\u8BA1 ${total} \u6761\uFF09\uFF1A${reason}`);
7874
+ };
7088
7875
  const progressBuf = [];
7089
7876
  let progressTimer = null;
7090
7877
  let progressChain = Promise.resolve();
@@ -7094,13 +7881,28 @@ var DaemonRunner = class {
7094
7881
  if (batch.length === 0)
7095
7882
  return progressChain;
7096
7883
  progressChain = progressChain.then(async () => {
7097
- for (const p of batch) {
7098
- await this.api.recordProgress(taskId, {
7884
+ if (progressDropAll)
7885
+ return;
7886
+ progressPending += batch.length;
7887
+ for (let offset = 0; offset < batch.length; offset += PROGRESS_BATCH_MAX) {
7888
+ if (progressDropAll)
7889
+ return;
7890
+ const slice = batch.slice(offset, offset + PROGRESS_BATCH_MAX);
7891
+ await this.api.recordProgressBatch(taskId, slice.map((p) => ({
7099
7892
  stage: p.stage,
7100
7893
  summary: p.summary ? String(redactValue(p.summary)) : void 0,
7101
7894
  toolName: p.toolName,
7102
7895
  toolInput: p.toolInput !== void 0 ? redactInputSummary(p.toolInput) : void 0
7103
- }).catch(() => {
7896
+ }))).then((res) => {
7897
+ progressPending -= slice.length;
7898
+ if (res && res.ok === false) {
7899
+ observationStats.progressFailed += slice.length;
7900
+ logObservation("progress", "\u4E0A\u62A5\u672A\u843D\u5E93", String(res.code ?? "unknown"), observationStats.progressFailed);
7901
+ }
7902
+ }).catch((err) => {
7903
+ progressPending -= slice.length;
7904
+ observationStats.progressFailed += slice.length;
7905
+ logObservation("progress", "\u4E0A\u62A5\u5931\u8D25", err?.message ?? String(err), observationStats.progressFailed);
7104
7906
  });
7105
7907
  }
7106
7908
  });
@@ -7118,9 +7920,23 @@ var DaemonRunner = class {
7118
7920
  return;
7119
7921
  streamFlushing = true;
7120
7922
  const batch = streamChunks.splice(0);
7923
+ streamInFlight += batch.length;
7121
7924
  try {
7122
7925
  for (const c of batch) {
7123
- await this.api.recordStreamChunk(taskId, c).catch(() => {
7926
+ if (streamDropAll) {
7927
+ streamInFlight -= 1;
7928
+ continue;
7929
+ }
7930
+ await this.api.recordStreamChunk(taskId, c).then((res) => {
7931
+ streamInFlight -= 1;
7932
+ if (res && res.ok === false) {
7933
+ observationStats.streamFailed += 1;
7934
+ logObservation("stream", "\u4E0A\u62A5\u672A\u843D\u5E93", String(res.code ?? "unknown"), observationStats.streamFailed);
7935
+ }
7936
+ }).catch((err) => {
7937
+ streamInFlight -= 1;
7938
+ observationStats.streamFailed += 1;
7939
+ logObservation("stream", "\u4E0A\u62A5\u5931\u8D25", err?.message ?? String(err), observationStats.streamFailed);
7124
7940
  });
7125
7941
  }
7126
7942
  } finally {
@@ -7137,17 +7953,19 @@ var DaemonRunner = class {
7137
7953
  void flushStreamChunks(false);
7138
7954
  }
7139
7955
  };
7140
- const resumeSessionId = providerKey === "dim" ? detail.lastRun?.dimSessionId ?? null : null;
7956
+ const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
7957
+ const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
7958
+ const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync12(resumeWorkDir);
7141
7959
  const input = {
7142
7960
  executionId: `${taskId}:1`,
7143
7961
  // task-workspace-layout:新会话 cwd = 任务工作区根(AGENTS.md/.agents 在根下,仓库在子目录)
7144
- cwd: resumeSessionId && detail.lastRun?.dimWorkDir ? detail.lastRun.dimWorkDir : taskRoot,
7962
+ cwd: canResume ? resumeWorkDir : taskRoot,
7145
7963
  // LOCAL-AGENT-FIX-003:把协作契约前置到任务上下文(userPrompt = Multica 的 messages[0]);
7146
7964
  // dim 是 systemPromptMode:'native',systemPrompt 不进 LLM,契约/al上下文均以 userPrompt 承载。
7147
7965
  userPrompt: buildTaskPromptWithContract(detail, repoBaseUrl),
7148
7966
  env,
7149
7967
  modelId: requestedModelId ?? this.model,
7150
- resumeSessionId,
7968
+ resumeSessionId: canResume ? resumeSessionId : null,
7151
7969
  approvalPolicy: {
7152
7970
  version: 1,
7153
7971
  readonly: false,
@@ -7160,6 +7978,8 @@ var DaemonRunner = class {
7160
7978
  approvalDecider: isFlagEnabled("MORULA_DIM_APPROVAL_MODE", false) && detail.executor.approvalMode === "manual" ? this.createApprovalDecider(taskId, `${taskId}:1`) : void 0,
7161
7979
  // M1-A1:取消信号透传(WS cancelled / 轮询兜底 → abort → 进程组中断)
7162
7980
  signal,
7981
+ // P3:运行中打断端口(安全点中断 → 同会话续开一轮;能力门控见 §三 E4)
7982
+ steer: this.buildSteerPort(taskId, detail),
7163
7983
  onEvent: (event) => {
7164
7984
  if (event.type === "progress") {
7165
7985
  if (event.summary || event.toolName)
@@ -7220,6 +8040,15 @@ var DaemonRunner = class {
7220
8040
  streamChunks.push({ chunkType: "usage", seqHint: nextChunkSeq(), data: event.usage });
7221
8041
  void flushStreamChunks(true);
7222
8042
  }
8043
+ if (event.type === "session" && event.providerSessionId) {
8044
+ void this.api.reportSessionPointer(taskId, {
8045
+ dimSessionId: event.providerSessionId,
8046
+ // 与传给 provider 的 cwd 同源(session 绑定 cwd,续跑要求一致,见上方 resumeWorkDir 说明)
8047
+ dimWorkDir: canResume ? resumeWorkDir : taskRoot
8048
+ }).catch((error) => {
8049
+ this.log(`[daemon] \u4F1A\u8BDD\u6307\u9488\u56DE\u4F20\u5931\u8D25 task=${taskId}: ${error instanceof Error ? error.message : String(error)}`);
8050
+ });
8051
+ }
7223
8052
  }
7224
8053
  };
7225
8054
  const result = await provider.run(input);
@@ -7243,10 +8072,24 @@ var DaemonRunner = class {
7243
8072
  clearTimeout(progressTimer);
7244
8073
  progressTimer = null;
7245
8074
  }
7246
- await flushProgress().catch(() => {
7247
- });
7248
- await flushStreamChunks(true).catch(() => {
8075
+ const progressDrain = flushProgress();
8076
+ const streamDrain = flushStreamChunks(true);
8077
+ const drained = await drainObservationTail({
8078
+ progress: progressDrain,
8079
+ stream: streamDrain,
8080
+ streamPending: () => streamPendingCount() > 0,
8081
+ budgetMs: TERMINAL_OBSERVATION_DRAIN_BUDGET_MS
7249
8082
  });
8083
+ if (!drained.progressDrained) {
8084
+ progressDropAll = true;
8085
+ observationStats.progressDropped += progressPending;
8086
+ logObservation("progress", "\u6536\u5C3E\u4E22\u5F03\u6B8B\u4F59", `\u6392\u7A7A\u8D85\u9884\u7B97 ${TERMINAL_OBSERVATION_DRAIN_BUDGET_MS}ms\uFF0C\u6B8B\u4F59\u4E0D\u518D\u53D1\u9001`, observationStats.progressDropped);
8087
+ }
8088
+ if (!drained.streamDrained) {
8089
+ streamDropAll = true;
8090
+ observationStats.streamDropped += streamPendingCount();
8091
+ logObservation("stream", "\u6536\u5C3E\u4E22\u5F03\u6B8B\u4F59", `\u6392\u7A7A\u8D85\u9884\u7B97 ${TERMINAL_OBSERVATION_DRAIN_BUDGET_MS}ms\uFF0C\u6B8B\u4F59\u4E0D\u518D\u53D1\u9001`, observationStats.streamDropped);
8092
+ }
7250
8093
  return result;
7251
8094
  }
7252
8095
  /**
@@ -7456,16 +8299,44 @@ function writeCustomSkills(targetDir, skills, log) {
7456
8299
  ${skill.content ?? ""}`, "utf8");
7457
8300
  }
7458
8301
  }
7459
- function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch) {
8302
+ function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
7460
8303
  const dir = join15(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
7461
8304
  const refsDir = join15(dir, "references");
7462
8305
  mkdirSync9(refsDir, { recursive: true });
7463
- const front = repoBaseUrl ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
7464
- const body = repoBaseUrl ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
8306
+ const isGitlab = provider === "gitlab" && !!repoBaseUrl;
8307
+ const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
8308
+ const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
7465
8309
  writeFileSync8(join15(dir, "SKILL.md"), `${front}
7466
8310
  ${body}`, "utf8");
7467
8311
  writeFileSync8(join15(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
7468
- log?.(`[daemon] \u5DF2\u6CE8\u5165\u534F\u4F5C\u5951\u7EA6 skill ${COLLABORATION_SKILL_NAME}\uFF08${repoBaseUrl ? "GitLab" : "GitHub"} \u7248\uFF09\u5230 .agents/skills/`);
8312
+ 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/`);
8313
+ }
8314
+ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
8315
+ const repos = (availableRepos ?? []).filter((r) => !!r?.repoFullName);
8316
+ const bound = boundRepo ? repos.find((r) => r.repoFullName === boundRepo) ?? null : null;
8317
+ const preferred = bound ?? repos[0] ?? null;
8318
+ if (boundBaseUrl) {
8319
+ return {
8320
+ provider: "gitlab",
8321
+ repoFullName: boundRepo,
8322
+ baseUrl: boundBaseUrl,
8323
+ baseBranch: boundBaseBranch ?? preferred?.defaultBranch ?? void 0
8324
+ };
8325
+ }
8326
+ const gitlabRepo = repos.find((r) => (r.baseUrl ?? "").trim() !== "");
8327
+ if (gitlabRepo) {
8328
+ return {
8329
+ provider: "gitlab",
8330
+ repoFullName: gitlabRepo.repoFullName,
8331
+ baseUrl: gitlabRepo.baseUrl ?? void 0,
8332
+ baseBranch: boundBaseBranch ?? gitlabRepo.defaultBranch ?? void 0
8333
+ };
8334
+ }
8335
+ return {
8336
+ provider: "github",
8337
+ repoFullName: boundRepo ?? preferred?.repoFullName ?? null,
8338
+ baseBranch: boundBaseBranch ?? preferred?.defaultBranch ?? void 0
8339
+ };
7469
8340
  }
7470
8341
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
7471
8342
  const briefDir = join15(targetDir, ".agent_context");
@@ -7561,7 +8432,8 @@ ${multiRepoList}
7561
8432
  - \u4EFB\u52A1\u8BE6\u60C5\uFF08\u4EFB\u52A1\u7F16\u53F7 / \u5173\u8054\u5BF9\u8C61 / \u4E1A\u52A1\u76EE\u6807 / \u8BC4\u8BBA / PR\uFF09\uFF1A\`morula task get\`\uFF08\u65E0\u53C2\uFF0CtaskId \u7531\u6267\u884C\u73AF\u5883\u63D0\u4F9B\uFF09\u3002
7562
8433
 
7563
8434
  `;
7564
- const stageContract = buildStageContract(detail.stageCode);
8435
+ const taskSource = detail.contextSnapshot?.source;
8436
+ const stageContract = buildStageContract(detail.stageCode, typeof taskSource === "string" ? taskSource : void 0);
7565
8437
  const stageContractBlock = stageContract ? `${stageContract}
7566
8438
 
7567
8439
  ` : "";
@@ -7761,6 +8633,7 @@ import { existsSync as existsSync13, readFileSync as readFileSync9, realpathSync
7761
8633
  import { homedir as homedir8 } from "node:os";
7762
8634
  import { dirname as dirname6, join as join16 } from "node:path";
7763
8635
  init_daemon_config();
8636
+ init_dim_locate();
7764
8637
  async function doctorCommand(context) {
7765
8638
  const probe = context.parsed.flags.probe === true || context.parsed.flags.probe === "true";
7766
8639
  const { facts, checks } = await collectDoctor(context, probe);
@@ -8278,10 +9151,128 @@ function parseMaxConcurrent(context) {
8278
9151
  return value;
8279
9152
  }
8280
9153
 
9154
+ // src/daemon/plugin-register.ts
9155
+ import { chmodSync as chmodSync3, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync12, readdirSync as readdirSync4, readFileSync as readFileSync11, rmSync as rmSync8, statSync as statSync3, writeFileSync as writeFileSync10 } from "node:fs";
9156
+ import { homedir as homedir11 } from "node:os";
9157
+ import { dirname as dirname8, join as join19 } from "node:path";
9158
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
9159
+ var PLUGIN_NAME = "morula-cli";
9160
+ var MANIFEST_DIR_NAME = ".codex-plugin";
9161
+ function legacyAgentsPluginDir(home = homedir11()) {
9162
+ return join19(home, ".agents", "plugins", PLUGIN_NAME);
9163
+ }
9164
+ function resolveDimcodeHome(env = process.env, home = homedir11()) {
9165
+ const fromEnv = env.DIMCODE_HOME?.trim();
9166
+ return fromEnv || join19(home, ".dimcode", "v2");
9167
+ }
9168
+ function defaultPluginsRoot(env = process.env, home = homedir11()) {
9169
+ return join19(resolveDimcodeHome(env, home), "plugins");
9170
+ }
9171
+ function resolvePackagePaths(entryUrl) {
9172
+ const distDir = dirname8(fileURLToPath2(entryUrl));
9173
+ return {
9174
+ packageRoot: dirname8(distDir),
9175
+ distDir,
9176
+ assetsDir: join19(distDir, "plugin")
9177
+ };
9178
+ }
9179
+ function buildLauncherScript(opts) {
9180
+ const entry = join19(opts.packageRoot, "dist", "morula.js");
9181
+ return `#!/bin/sh
9182
+ # Morula \u4E1A\u52A1 CLI \u542F\u52A8\u5668\uFF08\u7531 \`morula-runtime setup\` \u751F\u6210\uFF1BNode \u5347\u7EA7\u540E\u91CD\u8DD1 setup \u5237\u65B0\uFF09\u3002
9183
+ # \u4E0D\u4F9D\u8D56\u8C03\u7528\u65B9 PATH \u91CC\u7684 node\uFF08agent \u6267\u884C\u73AF\u5883 PATH \u5E38\u88AB\u88C1\u526A\uFF09\uFF0C\u4EC5\u4F5C\u515C\u5E95\u3002
9184
+ NODE=${JSON.stringify(opts.nodePath)}
9185
+ [ -x "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
9186
+ if [ -z "$NODE" ]; then
9187
+ echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
9188
+ exit 127
9189
+ fi
9190
+ exec "$NODE" ${JSON.stringify(entry)} "$@"
9191
+ `;
9192
+ }
9193
+ function assetsReady(paths) {
9194
+ return existsSync16(join19(paths.assetsDir, "plugin.json")) && existsSync16(join19(paths.assetsDir, "skills"));
9195
+ }
9196
+ function listSkillNames(assetsDir) {
9197
+ const dir = join19(assetsDir, "skills");
9198
+ if (!existsSync16(dir))
9199
+ return [];
9200
+ return readdirSync4(dir).filter((name) => {
9201
+ try {
9202
+ return statSync3(join19(dir, name)).isDirectory();
9203
+ } catch {
9204
+ return false;
9205
+ }
9206
+ }).sort();
9207
+ }
9208
+ function registerPlugin(opts) {
9209
+ const { pluginsRoot, paths, version } = opts;
9210
+ const nodePath = opts.nodePath ?? process.execPath;
9211
+ const pluginDir = join19(pluginsRoot, PLUGIN_NAME);
9212
+ if (!assetsReady(paths))
9213
+ throw new Error(`plugin assets not found at ${paths.assetsDir}; run \`pnpm run build:dist\` (dev) or reinstall the package`);
9214
+ const skillsSrc = join19(paths.assetsDir, "skills");
9215
+ const skills = listSkillNames(paths.assetsDir);
9216
+ const manifestDir = join19(pluginDir, MANIFEST_DIR_NAME);
9217
+ mkdirSync12(manifestDir, { recursive: true });
9218
+ const manifest = JSON.parse(readFileSync11(join19(paths.assetsDir, "plugin.json"), "utf8"));
9219
+ writeFileSync10(join19(manifestDir, "plugin.json"), `${JSON.stringify({ ...manifest, version }, null, 2)}
9220
+ `);
9221
+ const skillsDest = join19(pluginDir, "skills");
9222
+ rmSync8(skillsDest, { recursive: true, force: true });
9223
+ mkdirSync12(skillsDest, { recursive: true });
9224
+ cpSync(skillsSrc, skillsDest, { recursive: true });
9225
+ const binDir = join19(pluginDir, "bin");
9226
+ mkdirSync12(binDir, { recursive: true });
9227
+ const binPath = join19(binDir, "morula");
9228
+ writeFileSync10(binPath, buildLauncherScript({ nodePath, packageRoot: paths.packageRoot }));
9229
+ chmodSync3(binPath, 493);
9230
+ const legacy = legacyAgentsPluginDir(opts.home);
9231
+ return {
9232
+ pluginDir,
9233
+ binPath,
9234
+ skills,
9235
+ version,
9236
+ node: nodePath,
9237
+ legacyDir: existsSync16(join19(legacy, MANIFEST_DIR_NAME)) ? legacy : null
9238
+ };
9239
+ }
9240
+
9241
+ // src/commands/setup.ts
9242
+ var KNOWN_FLAGS = ["root", "help"];
9243
+ async function setupCommand(context) {
9244
+ rejectUnknownFlags(context.parsed.flags, KNOWN_FLAGS);
9245
+ if (context.parsed.action && context.parsed.action !== "setup")
9246
+ throw usage("usage: morula-runtime setup [--root <dir>]", { hint: "morula-runtime setup --help" });
9247
+ const rootOverride = flagString(context.parsed.flags, "root");
9248
+ const pluginsRoot = rootOverride ?? defaultPluginsRoot(context.env);
9249
+ const paths = resolvePackagePaths(import.meta.url);
9250
+ let result;
9251
+ try {
9252
+ result = registerPlugin({ pluginsRoot, paths, version: BUILD_VERSION2 });
9253
+ } catch (error) {
9254
+ throw new CliError("ERROR", EXIT.OTHER, error instanceof Error ? error.message : String(error), false, {
9255
+ hint: "\u6E90\u7801\u6A21\u5F0F\u8BF7\u5148 `pnpm --filter @arcships/morula-runtime run build:dist`\uFF1B\u5168\u5C40\u5B89\u88C5\u8BF7\u91CD\u88C5\u8BE5\u5305"
9256
+ });
9257
+ }
9258
+ return {
9259
+ ok: true,
9260
+ pluginRoot: result.pluginDir,
9261
+ version: result.version,
9262
+ binPath: result.binPath,
9263
+ skills: result.skills,
9264
+ node: result.node,
9265
+ legacyRoot: result.legacyDir,
9266
+ ...result.legacyDir ? { hint: "\u68C0\u6D4B\u5230\u65E7\u7684\u540C\u540D\u63D2\u4EF6\u76EE\u5F55\uFF0C\u8BF7\u5220\u9664\u4EE5\u514D\u4E24\u4E2A\u63D2\u4EF6\u6839\u540C\u540D\u51B2\u7A81\uFF08Dim \u4F18\u5148\u547D\u4E2D <DIMCODE_HOME>/plugins\uFF09" } : {},
9267
+ note: "Dim \u5728**\u65B0 session** \u624D\u4F1A\u91CD\u65B0\u626B\u63CF\u63D2\u4EF6\u6839\uFF1B\u5DF2\u5F00\u7740\u7684\u4F1A\u8BDD\u4E0D\u4F1A\u52A0\u8F7D\u65B0 skills\u3002"
9268
+ };
9269
+ }
9270
+
8281
9271
  // src/commands/registry-runtime.ts
8282
9272
  var COMMANDS = {
8283
9273
  auth: authCommand,
8284
- daemon: daemonCommand
9274
+ daemon: daemonCommand,
9275
+ setup: setupCommand
8285
9276
  };
8286
9277
  var EXIT_CODES = {
8287
9278
  0: "success",
@@ -8437,6 +9428,28 @@ var SPEC = {
8437
9428
  notes: [
8438
9429
  'exit code 0 = all checks passed (warn is ok); 1 = at least one check failed -> see checks[] to tell "found problems" from "command failed"'
8439
9430
  ]
9431
+ },
9432
+ {
9433
+ resource: "setup",
9434
+ summary: "materialize the Dim plugin directory (skills + bin launcher) for this package",
9435
+ actions: [
9436
+ {
9437
+ action: "setup",
9438
+ summary: "write <DIMCODE_HOME>/plugins/morula-cli (manifest + skills + bin/morula launcher); offline and idempotent",
9439
+ flags: [
9440
+ { name: "root", type: "string", valueHint: "<dir>", description: "plugins root to install into (default: $DIMCODE_HOME/plugins or ~/.dimcode/v2/plugins)" }
9441
+ ],
9442
+ examples: ["morula-runtime setup", "morula-runtime setup --root /tmp/plugins"],
9443
+ writes: "local",
9444
+ interactive: false,
9445
+ idempotent: true,
9446
+ emits: ["ok", "pluginRoot", "version", "binPath", "skills", "node", "legacyRoot"]
9447
+ }
9448
+ ],
9449
+ notes: [
9450
+ "canonical form is the bare `morula-runtime setup`; postinstall runs the same registration automatically",
9451
+ "only writes <root>/morula-cli; never deletes other plugins or the legacy ~/.agents/plugins/morula-cli (reported as legacyRoot)"
9452
+ ]
8440
9453
  }
8441
9454
  ]
8442
9455
  };