@arcships/morula-runtime 0.1.0-alpha.3 → 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 = "20260914094501.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.3";
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]);
3261
3358
  }
3262
- /** M2-B4:执行事件(工具调用等)→ agent_events(taskRunId+seq 唯一,前端时间线消费) */
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
+ });
3373
+ }
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
@@ -3677,6 +3896,8 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
3677
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
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
 
@@ -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) {
@@ -4135,9 +4317,18 @@ function normalizeOptionId(decision) {
4135
4317
  var DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1e3;
4136
4318
  var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
4137
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;
4138
4323
  var PROMPT_RPC_TIMEOUT_GRACE_MS = 1e4;
4139
4324
  var WATCHDOG_TICK_MS = 15e3;
4140
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
+ }
4141
4332
  var ACP_INIT_RETRY_DELAYS_MS = [2e3, 8e3];
4142
4333
  function isAcpInitRetryableError(error) {
4143
4334
  const message = error instanceof Error ? error.message : String(error ?? "");
@@ -4232,6 +4423,11 @@ function extractProviderError(text) {
4232
4423
  }
4233
4424
  return null;
4234
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
+ }
4235
4431
  function resolveAcpModelId(models2, requested, configOptions) {
4236
4432
  if (!requested.trim())
4237
4433
  return null;
@@ -4278,7 +4474,9 @@ var DimAcpExecutor = class {
4278
4474
  let ownsConnection = false;
4279
4475
  let sessionId = "";
4280
4476
  let finalText = "";
4281
- let inFlightTools = 0;
4477
+ let turnNo = 1;
4478
+ const inFlightToolCalls = /* @__PURE__ */ new Map();
4479
+ const inFlightTools = () => inFlightToolCalls.size;
4282
4480
  let lastActivityAt = Date.now();
4283
4481
  let watchdogTimer = null;
4284
4482
  let watchdogFired = false;
@@ -4336,10 +4534,12 @@ var DimAcpExecutor = class {
4336
4534
  if (idle <= 0)
4337
4535
  return;
4338
4536
  lastActivityAt = Date.now();
4339
- inFlightTools = 0;
4537
+ inFlightToolCalls.clear();
4340
4538
  watchdogTimer = setInterval(() => {
4539
+ maybeInterruptAtSafePoint();
4540
+ enforceTaskWallClock();
4341
4541
  const toolBudget = Number(process.env.MORULA_DIM_TOOL_IDLE_TIMEOUT_MS) || DEFAULT_TOOL_IDLE_TIMEOUT_MS;
4342
- const budget = inFlightTools > 0 ? toolBudget : idle;
4542
+ const budget = inFlightTools() > 0 ? toolBudget : idle;
4343
4543
  if (Date.now() - lastActivityAt > budget) {
4344
4544
  watchdogFired = true;
4345
4545
  if (sessionId && conn)
@@ -4348,7 +4548,8 @@ var DimAcpExecutor = class {
4348
4548
  finish({
4349
4549
  ok: false,
4350
4550
  error: `daemon: idle watchdog fired\uFF08${Math.round(budget / 1e3)}s \u65E0\u6D3B\u52A8\uFF09`,
4351
- sessionId: sessionId || void 0
4551
+ sessionId: sessionId || void 0,
4552
+ turns: turnNo
4352
4553
  });
4353
4554
  }
4354
4555
  }, WATCHDOG_TICK_MS);
@@ -4357,6 +4558,60 @@ var DimAcpExecutor = class {
4357
4558
  const markActivity = () => {
4358
4559
  lastActivityAt = Date.now();
4359
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
+ }
4360
4615
  const setupOnce = async () => {
4361
4616
  if (pool) {
4362
4617
  conn = await pool.acquire();
@@ -4382,12 +4637,23 @@ var DimAcpExecutor = class {
4382
4637
  }
4383
4638
  });
4384
4639
  setup().then(async () => {
4385
- timer = setTimeout(() => {
4386
- if (sessionId && conn)
4387
- conn.cancel(sessionId);
4388
- killTree(conn);
4389
- finish({ ok: false, error: `dim \u6267\u884C\u8D85\u65F6\uFF08${Math.round(timeout / 1e3)}s\uFF09`, sessionId: sessionId || void 0 });
4390
- }, 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();
4391
4657
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
4392
4658
  startWatchdog();
4393
4659
  const bridge = new AcpPermissionBridge();
@@ -4451,56 +4717,142 @@ var DimAcpExecutor = class {
4451
4717
  pool?.touch();
4452
4718
  markActivity();
4453
4719
  opts.onEvent?.(mapper.readyMessage(sessionId), "");
4454
- const result = await conn.prompt(sessionId, opts.userPrompt, (update) => {
4455
- 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;
4456
4785
  pool?.touch();
4457
- const { messages } = mapper.map(sessionId, update);
4458
- for (const msg of messages) {
4459
- if (msg.eventType === "text:delta") {
4460
- const delta = typeof msg.payload?.delta === "string" ? msg.payload.delta : "";
4461
- finalText += delta;
4462
- opts.onProgress({ stage: "output", summary: delta });
4463
- opts.onEvent?.(msg, finalText);
4464
- } else if (msg.eventType === "thinking:delta") {
4465
- const delta = typeof msg.payload?.delta === "string" ? msg.payload.delta : "";
4466
- opts.onProgress({ stage: "thinking", summary: delta });
4467
- opts.onEvent?.(msg, finalText);
4468
- } else if (msg.eventType === "tool:started") {
4469
- inFlightTools += 1;
4470
- opts.onEvent?.(msg, finalText);
4471
- opts.onProgress({
4472
- stage: "tool_call",
4473
- toolName: typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool",
4474
- toolInput: typeof msg.payload?.toolInput === "object" && msg.payload?.toolInput !== null ? msg.payload.toolInput : void 0,
4475
- summary: typeof msg.payload?.toolName === "string" ? msg.payload.toolName : "tool"
4476
- });
4477
- } else {
4478
- 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;
4479
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;
4480
4845
  }
4481
- }, timeout + PROMPT_RPC_TIMEOUT_GRACE_MS);
4482
- stopWatchdog();
4483
- pool?.touch();
4484
- const usage2 = result.usage ?? mapper.latestUsage;
4485
- if (result.stopReason === "cancelled") {
4486
- finish({ ok: false, error: "\u4EFB\u52A1\u88AB\u53D6\u6D88", sessionId });
4487
- } else if (result.stopReason === "max_tokens") {
4488
- opts.onEvent?.(mapper.runEndedMessage({ stopReason: "max_tokens", usage: usage2 }), finalText);
4489
- finish({ ok: false, error: "dim \u8FBE\u5230 max_tokens \u4E0A\u9650", tokenUsage: usage2, sessionId });
4490
- } else if (result.stopReason === "refusal") {
4491
- opts.onEvent?.(mapper.runEndedMessage({ stopReason: "refusal", usage: usage2 }), finalText);
4492
- const providerError = extractProviderError(finalText);
4493
- finish({ ok: false, error: providerError ?? "\u6A21\u578B\u62D2\u7EDD\u6267\u884C", tokenUsage: usage2, sessionId });
4494
- } else {
4495
4846
  opts.onEvent?.(mapper.runEndedMessage({ stopReason: result.stopReason, usage: usage2 }), finalText);
4496
- finish({ ok: true, text: finalText, tokenUsage: usage2, sessionId });
4847
+ finish({ ...base, ok: true, text: finalText, tokenUsage: usage2 });
4848
+ return;
4497
4849
  }
4498
4850
  }).catch((err) => {
4499
4851
  const message = err instanceof Error ? err.message : String(err);
4500
4852
  if (sessionId && conn && message.includes("\u8D85\u65F6")) {
4501
4853
  conn.cancel(sessionId);
4502
4854
  }
4503
- finish({ ok: false, error: message, sessionId: sessionId || void 0 });
4855
+ finish({ ok: false, error: message, sessionId: sessionId || void 0, turns: turnNo, interrupted: interruptedTurns > 0 });
4504
4856
  });
4505
4857
  });
4506
4858
  }
@@ -4653,6 +5005,8 @@ function createDimEventNormalizer(opts) {
4653
5005
  type: "tool_call_completed",
4654
5006
  callId: String(callId),
4655
5007
  toolName: typeof p.toolName === "string" ? p.toolName : void 0,
5008
+ // P3:运行中打断留下的孤儿调用(无真实结果)→ 时间线标中断点,不伪装成正常完成
5009
+ ...p.aborted === true ? { aborted: true } : {},
4656
5010
  ...outputSummary !== void 0 ? { outputSummary } : {}
4657
5011
  });
4658
5012
  break;
@@ -4733,6 +5087,16 @@ var DIM_CAPABILITIES = {
4733
5087
  authentication: "required",
4734
5088
  sessionResume: true,
4735
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,
4736
5100
  modelSelection: "workspace_config",
4737
5101
  systemPromptMode: "native",
4738
5102
  skillInjection: "agents_skills",
@@ -4824,6 +5188,7 @@ var DimAgentAdapter = class {
4824
5188
  return await new Promise((resolve2) => {
4825
5189
  const events = [];
4826
5190
  let sessionId = null;
5191
+ let reportedSessionId = null;
4827
5192
  const normalizer = createDimEventNormalizer({
4828
5193
  onEvent: (event) => {
4829
5194
  events.push(event);
@@ -4836,6 +5201,10 @@ var DimAgentAdapter = class {
4836
5201
  const onEvent = (msg, finalText) => {
4837
5202
  if (msg.type === "ready" && typeof msg.sessionId === "string" && msg.sessionId) {
4838
5203
  sessionId = msg.sessionId;
5204
+ if (reportedSessionId !== sessionId) {
5205
+ reportedSessionId = sessionId;
5206
+ input.onEvent({ type: "session", providerSessionId: sessionId });
5207
+ }
4839
5208
  }
4840
5209
  normalizer.handle(msg, finalText);
4841
5210
  };
@@ -4868,7 +5237,9 @@ var DimAgentAdapter = class {
4868
5237
  }
4869
5238
  },
4870
5239
  timeoutMs: input.timeoutMs,
4871
- signal: input.signal
5240
+ signal: input.signal,
5241
+ // P3:运行中打断端口由上层(runner)提供;未提供时执行器保持单回合语义
5242
+ steer: input.steer ?? null
4872
5243
  }).then((result) => {
4873
5244
  const finalText = finalAssistantSelector(events);
4874
5245
  resolve2({
@@ -4878,7 +5249,9 @@ var DimAgentAdapter = class {
4878
5249
  usage: result.tokenUsage ? normalizeUsage(result.tokenUsage) : null,
4879
5250
  exitCode: result.ok ? 0 : null,
4880
5251
  error: result.ok ? null : { code: "DIM_RUN_FAILED", message: result.error ?? "dim \u6267\u884C\u5931\u8D25" },
4881
- resumeFailed: result.resumeFailed
5252
+ resumeFailed: result.resumeFailed,
5253
+ turns: result.turns,
5254
+ interrupted: result.interrupted
4882
5255
  });
4883
5256
  }).catch((err) => {
4884
5257
  resolve2({
@@ -4907,6 +5280,8 @@ var FAKE_CAPABILITIES = {
4907
5280
  approvalMode: "none",
4908
5281
  authentication: "none",
4909
5282
  sessionResume: false,
5283
+ midRunSteer: false,
5284
+ checkpointInbox: false,
4910
5285
  modelSelection: "explicit",
4911
5286
  systemPromptMode: "native",
4912
5287
  skillInjection: "prompt_only",
@@ -6105,6 +6480,144 @@ function materializeAdditionalRepo(input) {
6105
6480
  return input.targetPath;
6106
6481
  }
6107
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
+
6108
6621
  // src/daemon/ws-client.ts
6109
6622
  import { createHash as createHash2 } from "node:crypto";
6110
6623
  import { io } from "socket.io-client";
@@ -6135,10 +6648,49 @@ var DaemonWsClient = class {
6135
6648
  authReloads = 0;
6136
6649
  reconnectAttempts = 0;
6137
6650
  pollOnly = false;
6651
+ /**
6652
+ * P1:已订阅的任务房间(taskId → afterSeq)。**订阅意图独立于连接状态**——
6653
+ * 连接未就绪(初始连接中 / 重连中 / 凭证重载重建 socket)时先登记,连上后由 `joinTaskRooms()` 重放,
6654
+ * 否则「连上之前 claim 到的任务」永远收不到消息(服务端不发 tenant 广播)。
6655
+ */
6656
+ taskRooms = /* @__PURE__ */ new Map();
6138
6657
  /** 是否已降级为 poll-only(认证长期失败;runner 的 poll 兜底继续工作) */
6139
6658
  get isPollOnly() {
6140
6659
  return this.pollOnly;
6141
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
+ }
6142
6694
  /** 当前凭证指纹(日志用;不泄露本体) */
6143
6695
  get tokenFingerprint() {
6144
6696
  return tokenFingerprint(this.currentToken);
@@ -6176,6 +6728,9 @@ var DaemonWsClient = class {
6176
6728
  this.reconnectAttempts = 0;
6177
6729
  this.authReloads = 0;
6178
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);
6179
6734
  });
6180
6735
  socket.on("agent-task:dispatched", (payload) => {
6181
6736
  handlers.onTaskDispatched(payload.taskId, payload.runtimeId);
@@ -6194,6 +6749,11 @@ var DaemonWsClient = class {
6194
6749
  void reason;
6195
6750
  handlers.onDisconnected();
6196
6751
  });
6752
+ socket.on("agent-task:message", (payload) => {
6753
+ const event = parseTaskMessageFrame(payload);
6754
+ if (event)
6755
+ handlers.onTaskMessage(event);
6756
+ });
6197
6757
  socket.on("connect_error", (error) => {
6198
6758
  this.onConnectError(socket, error);
6199
6759
  });
@@ -6291,30 +6851,6 @@ var DaemonWsClient = class {
6291
6851
  }
6292
6852
  };
6293
6853
 
6294
- // src/daemon/stage-contracts.ts
6295
- var STAGE_CONTRACTS = {
6296
- todo: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1A\u5F85\u5206\u6790\uFF08\u9700\u6C42\u5206\u6790\uFF09
6297
-
6298
- \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
6299
-
6300
- \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`,
6301
- in_progress: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1A\u5F00\u53D1\u5B9E\u73B0
6302
-
6303
- \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
6304
-
6305
- \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`,
6306
- review: `## \u672C\u4EFB\u52A1\u9636\u6BB5\uFF1ACode Review
6307
-
6308
- \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
6309
-
6310
- \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`
6311
- };
6312
- function buildStageContract(stageCode) {
6313
- if (!stageCode)
6314
- return "";
6315
- return STAGE_CONTRACTS[stageCode] ?? "";
6316
- }
6317
-
6318
6854
  // src/daemon/runner.ts
6319
6855
  var HEARTBEAT_INTERVAL_MS = 3e4;
6320
6856
  var POLL_INTERVAL_MS = 15e3;
@@ -6322,6 +6858,30 @@ var TICK_BUDGET_MS = 6e4;
6322
6858
  var OUTBOX_REPLAY_MAX_FAILURES = 5;
6323
6859
  var OUTBOX_REPLAY_PAUSE_MS = 5 * 6e4;
6324
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
+ }
6325
6885
  var OUTBOX_PERMANENT_CODES = ["NOT_FOUND", "CONFLICT"];
6326
6886
  var DEFAULT_MODEL = "deepseek-v4-flash";
6327
6887
  var DaemonRunner = class {
@@ -6360,6 +6920,16 @@ var DaemonRunner = class {
6360
6920
  cancels = /* @__PURE__ */ new Map();
6361
6921
  /** Phase 2:审批决策等待表(approvalId → resolver;WS 事件 resolve,轮询兜底) */
6362
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();
6363
6933
  dimPath;
6364
6934
  model;
6365
6935
  /** 独立 dim 数据空间 env 覆盖(DIMCODE_HOME=~/.morula/dimcode;空对象=初始化失败回退共享空间) */
@@ -6454,31 +7024,7 @@ var DaemonRunner = class {
6454
7024
  this.log(`[daemon] \u5DF2\u6CE8\u518C runtime=${registered.id} daemonId=${cfg.daemonId}`);
6455
7025
  this.repoCheckout?.setRuntimeId(registered.id);
6456
7026
  this.startHeartbeatTimer();
6457
- this.ws.connect({
6458
- onTaskDispatched: (taskId, runtimeId) => {
6459
- this.onTaskDispatched(taskId, runtimeId);
6460
- },
6461
- // M1-A1:取消信号 → 中断执行中的 dim 进程
6462
- onTaskCancelled: (taskId) => {
6463
- const ctrl = this.cancels.get(taskId);
6464
- if (ctrl) {
6465
- this.log(`[daemon] \u6536\u5230\u53D6\u6D88\u4FE1\u53F7 task=${taskId}\uFF0C\u4E2D\u65AD\u6267\u884C`);
6466
- ctrl.abort();
6467
- this.cancels.delete(taskId);
6468
- }
6469
- },
6470
- onDisconnected: () => {
6471
- },
6472
- // Phase 2:审批决策 → resolve 等待者(daemon 侧 ACP permission/response)
6473
- onApprovalDecision: (event) => {
6474
- const waiter = this.pendingApprovals.get(event.approvalId);
6475
- if (waiter) {
6476
- clearTimeout(waiter.timer);
6477
- this.pendingApprovals.delete(event.approvalId);
6478
- waiter.resolve(event.decision);
6479
- }
6480
- }
6481
- });
7027
+ this.ws.connect(this.buildWsHandlers());
6482
7028
  void this.replayOutbox();
6483
7029
  while (this.running) {
6484
7030
  await this.tick();
@@ -6608,6 +7154,7 @@ var DaemonRunner = class {
6608
7154
  }
6609
7155
  } catch {
6610
7156
  }
7157
+ void this.catchUpTaskMessages(taskId);
6611
7158
  }
6612
7159
  void this.replayOutbox();
6613
7160
  try {
@@ -6797,6 +7344,48 @@ var DaemonRunner = class {
6797
7344
  }, Math.max(0, nextAt - Date.now()));
6798
7345
  this.outboxTimer.unref?.();
6799
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
+ }
6800
7389
  /**
6801
7390
  * 派发事件入口(P0-b,issues/agent-task-false-failure-on-daemon-fetch-error.md):
6802
7391
  * 服务端把派发事件发到 **tenant 房间**(daemon-executor.service.ts `sendToTenant`,WS 无 per-runtime
@@ -6828,6 +7417,8 @@ var DaemonRunner = class {
6828
7417
  return;
6829
7418
  }
6830
7419
  claimed = true;
7420
+ this.ws.subscribeTask(taskId, 0);
7421
+ void this.catchUpTaskMessages(taskId);
6831
7422
  await this.executeTask(taskId, ctrl.signal);
6832
7423
  } catch (error) {
6833
7424
  if (ctrl.signal.aborted) {
@@ -6845,9 +7436,148 @@ var DaemonRunner = class {
6845
7436
  } finally {
6846
7437
  this.cancels.delete(taskId);
6847
7438
  this.processing.delete(taskId);
7439
+ this.ws.unsubscribeTask(taskId);
7440
+ this.pendingMessages.delete(taskId);
6848
7441
  this.api.clearTaskToken(taskId);
6849
7442
  }
6850
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
+ }
6851
7581
  async executeTask(taskId, signal) {
6852
7582
  const detail = await this.api.getTask(taskId);
6853
7583
  this.log(`[daemon] \u5F00\u59CB\u4EFB\u52A1 ${taskId}\uFF08${detail.executor.executorName}\uFF09`);
@@ -6961,7 +7691,15 @@ var DaemonRunner = class {
6961
7691
  writeCustomSkills(taskRoot, customSkills, this.log);
6962
7692
  this.log(`[daemon] \u5DF2\u6CE8\u5165 ${customSkills.length} \u4E2A\u81EA\u5B9A\u4E49 skill \u5230 .agents/skills/`);
6963
7693
  }
6964
- 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
+ );
6965
7703
  writeTaskContextBrief(taskRoot, taskId, detail, repo, this.log);
6966
7704
  writeProjectAgentsMd(taskRoot, detail.projectAgentsMd, this.log);
6967
7705
  return { taskRoot, repoDir, worktreePath, env, repoBaseUrl };
@@ -7120,6 +7858,20 @@ var DaemonRunner = class {
7120
7858
  let thoughtBuf = "";
7121
7859
  let thoughtTimer = null;
7122
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
+ };
7123
7875
  const progressBuf = [];
7124
7876
  let progressTimer = null;
7125
7877
  let progressChain = Promise.resolve();
@@ -7129,13 +7881,28 @@ var DaemonRunner = class {
7129
7881
  if (batch.length === 0)
7130
7882
  return progressChain;
7131
7883
  progressChain = progressChain.then(async () => {
7132
- for (const p of batch) {
7133
- 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) => ({
7134
7892
  stage: p.stage,
7135
7893
  summary: p.summary ? String(redactValue(p.summary)) : void 0,
7136
7894
  toolName: p.toolName,
7137
7895
  toolInput: p.toolInput !== void 0 ? redactInputSummary(p.toolInput) : void 0
7138
- }).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);
7139
7906
  });
7140
7907
  }
7141
7908
  });
@@ -7153,9 +7920,23 @@ var DaemonRunner = class {
7153
7920
  return;
7154
7921
  streamFlushing = true;
7155
7922
  const batch = streamChunks.splice(0);
7923
+ streamInFlight += batch.length;
7156
7924
  try {
7157
7925
  for (const c of batch) {
7158
- 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);
7159
7940
  });
7160
7941
  }
7161
7942
  } finally {
@@ -7172,17 +7953,19 @@ var DaemonRunner = class {
7172
7953
  void flushStreamChunks(false);
7173
7954
  }
7174
7955
  };
7175
- 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);
7176
7959
  const input = {
7177
7960
  executionId: `${taskId}:1`,
7178
7961
  // task-workspace-layout:新会话 cwd = 任务工作区根(AGENTS.md/.agents 在根下,仓库在子目录)
7179
- cwd: resumeSessionId && detail.lastRun?.dimWorkDir ? detail.lastRun.dimWorkDir : taskRoot,
7962
+ cwd: canResume ? resumeWorkDir : taskRoot,
7180
7963
  // LOCAL-AGENT-FIX-003:把协作契约前置到任务上下文(userPrompt = Multica 的 messages[0]);
7181
7964
  // dim 是 systemPromptMode:'native',systemPrompt 不进 LLM,契约/al上下文均以 userPrompt 承载。
7182
7965
  userPrompt: buildTaskPromptWithContract(detail, repoBaseUrl),
7183
7966
  env,
7184
7967
  modelId: requestedModelId ?? this.model,
7185
- resumeSessionId,
7968
+ resumeSessionId: canResume ? resumeSessionId : null,
7186
7969
  approvalPolicy: {
7187
7970
  version: 1,
7188
7971
  readonly: false,
@@ -7195,6 +7978,8 @@ var DaemonRunner = class {
7195
7978
  approvalDecider: isFlagEnabled("MORULA_DIM_APPROVAL_MODE", false) && detail.executor.approvalMode === "manual" ? this.createApprovalDecider(taskId, `${taskId}:1`) : void 0,
7196
7979
  // M1-A1:取消信号透传(WS cancelled / 轮询兜底 → abort → 进程组中断)
7197
7980
  signal,
7981
+ // P3:运行中打断端口(安全点中断 → 同会话续开一轮;能力门控见 §三 E4)
7982
+ steer: this.buildSteerPort(taskId, detail),
7198
7983
  onEvent: (event) => {
7199
7984
  if (event.type === "progress") {
7200
7985
  if (event.summary || event.toolName)
@@ -7255,6 +8040,15 @@ var DaemonRunner = class {
7255
8040
  streamChunks.push({ chunkType: "usage", seqHint: nextChunkSeq(), data: event.usage });
7256
8041
  void flushStreamChunks(true);
7257
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
+ }
7258
8052
  }
7259
8053
  };
7260
8054
  const result = await provider.run(input);
@@ -7278,10 +8072,24 @@ var DaemonRunner = class {
7278
8072
  clearTimeout(progressTimer);
7279
8073
  progressTimer = null;
7280
8074
  }
7281
- await flushProgress().catch(() => {
7282
- });
7283
- 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
7284
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
+ }
7285
8093
  return result;
7286
8094
  }
7287
8095
  /**
@@ -7491,16 +8299,44 @@ function writeCustomSkills(targetDir, skills, log) {
7491
8299
  ${skill.content ?? ""}`, "utf8");
7492
8300
  }
7493
8301
  }
7494
- function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch) {
8302
+ function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
7495
8303
  const dir = join15(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
7496
8304
  const refsDir = join15(dir, "references");
7497
8305
  mkdirSync9(refsDir, { recursive: true });
7498
- const front = repoBaseUrl ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
7499
- 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);
7500
8309
  writeFileSync8(join15(dir, "SKILL.md"), `${front}
7501
8310
  ${body}`, "utf8");
7502
8311
  writeFileSync8(join15(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
7503
- 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
+ };
7504
8340
  }
7505
8341
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
7506
8342
  const briefDir = join15(targetDir, ".agent_context");
@@ -7596,7 +8432,8 @@ ${multiRepoList}
7596
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
7597
8433
 
7598
8434
  `;
7599
- const stageContract = buildStageContract(detail.stageCode);
8435
+ const taskSource = detail.contextSnapshot?.source;
8436
+ const stageContract = buildStageContract(detail.stageCode, typeof taskSource === "string" ? taskSource : void 0);
7600
8437
  const stageContractBlock = stageContract ? `${stageContract}
7601
8438
 
7602
8439
  ` : "";
@@ -7796,6 +8633,7 @@ import { existsSync as existsSync13, readFileSync as readFileSync9, realpathSync
7796
8633
  import { homedir as homedir8 } from "node:os";
7797
8634
  import { dirname as dirname6, join as join16 } from "node:path";
7798
8635
  init_daemon_config();
8636
+ init_dim_locate();
7799
8637
  async function doctorCommand(context) {
7800
8638
  const probe = context.parsed.flags.probe === true || context.parsed.flags.probe === "true";
7801
8639
  const { facts, checks } = await collectDoctor(context, probe);