oasis_test 0.1.97 → 0.1.99

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.
Files changed (2) hide show
  1. package/dist/index.js +1456 -726
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -209,8 +209,13 @@ var init_content_type_handler = __esm({
209
209
  const repoLines = repos.map((r) => ` - \`${r.id}\`\uFF1A${r.remote ?? "<remote>"}`);
210
210
  const short = (s2) => s2.slice(0, 12);
211
211
  const versionLine = (() => {
212
- if (!(ctx.hasHead && ctx.headExternalRef)) return [];
212
+ if (!ctx.hasHead) return [];
213
213
  const ref2 = ctx.headExternalRef;
214
+ if (!ref2) {
215
+ return [
216
+ `> **\u4F60\u4E0A\u4E00\u7248\u6CA1\u6709\u53EF\u8FFD\u6EAF\u7684\u63D0\u4EA4\u53F7**\uFF08\u4E0A\u4E00\u7248\u662F\u7A7A\u8DD1 / \u5D29\u5728\u4EA4\u4ED8\u524D / \u53EA\u7559\u7ED3\u8BBA\u6CA1\u4EA7\u6B63\u6587\uFF0C\u8D26\u672C\u91CC\u8FD9\u7248\u662F**\u4EA4\u4ED8\u90A3\u4E00\u523B\u7684\u5FEB\u7167**\u3001\u6070\u597D\u4E3A\u7A7A\uFF09\u2014\u2014\u4F46**\u8D26\u672C\u4E3A\u7A7A\u4E0D\u4EE3\u8868 git \u91CC\u6CA1\u4E1C\u897F**\uFF08\u4E0A\u4E00\u8F6E\u82E5\u8DD1\u8FC7\uFF0C\u53EF\u80FD\u5DF2 push \u5206\u652F\uFF09\uFF1B**\u4EE5\u4F60 \`git fetch\` \u5230\u7684\u5B9E\u9645\u72B6\u6001\u4E3A\u51C6**\uFF08\u89C1\u4E0B\u65B9\u7B2C 1 \u6B65\uFF09\uFF0C\u522B\u636E\u6B64\u4ECE\u96F6\u91CD\u5199\u3002`
217
+ ];
218
+ }
214
219
  if (ref2.trim().startsWith("{")) {
215
220
  try {
216
221
  const m2 = JSON.parse(ref2);
@@ -808,11 +813,6 @@ function fold(model, op) {
808
813
  });
809
814
  break;
810
815
  }
811
- case "reopen": {
812
- if (!model.seals.has(op.artifactId)) throw new CorruptLogError(op, "reopen on unsealed");
813
- model.seals.delete(op.artifactId);
814
- break;
815
- }
816
816
  case "hold": {
817
817
  const p2 = op.payload;
818
818
  if (!model.artifacts.has(op.artifactId)) throw new CorruptLogError(op, "hold on missing artifact");
@@ -1086,7 +1086,9 @@ function repliedAfter(a, who, sinceSeq) {
1086
1086
  }
1087
1087
  function hasUnansweredMention(model, a, who) {
1088
1088
  const seq = latestMentionSeq(model, a, who);
1089
- return seq >= 0 && !repliedAfter(a, who, seq);
1089
+ if (seq < 0) return false;
1090
+ if (seq <= (a.resolvedAtSeq ?? Number.NEGATIVE_INFINITY)) return false;
1091
+ return !repliedAfter(a, who, seq);
1090
1092
  }
1091
1093
  function defaultRecipientOf(model, a, owner) {
1092
1094
  if (a.mentions.length === 1) {
@@ -1377,6 +1379,39 @@ function unresolvedEscalationsOf(model, id) {
1377
1379
  ...e.gapId !== void 0 ? { gapId: e.gapId } : {}
1378
1380
  }));
1379
1381
  }
1382
+ function escalationsOf(model, id) {
1383
+ return (model.escalations.get(id) ?? []).map((e) => ({
1384
+ escalationId: e.escalationId,
1385
+ part: e.part,
1386
+ reason: e.reason,
1387
+ by: e.by,
1388
+ ...e.hand !== void 0 ? { hand: e.hand } : {},
1389
+ at: e.at,
1390
+ resolved: e.resolved,
1391
+ ...e.gapId !== void 0 ? { gapId: e.gapId } : {},
1392
+ ...e.resolvedReason !== void 0 ? { resolvedReason: e.resolvedReason } : {}
1393
+ }));
1394
+ }
1395
+ function escalationById(model, escalationId) {
1396
+ for (const [artifactId, list] of model.escalations) {
1397
+ const e = list.find((x2) => x2.escalationId === escalationId);
1398
+ if (e) {
1399
+ return {
1400
+ artifactId,
1401
+ escalationId: e.escalationId,
1402
+ part: e.part,
1403
+ reason: e.reason,
1404
+ by: e.by,
1405
+ ...e.hand !== void 0 ? { hand: e.hand } : {},
1406
+ at: e.at,
1407
+ resolved: e.resolved,
1408
+ ...e.gapId !== void 0 ? { gapId: e.gapId } : {},
1409
+ ...e.resolvedReason !== void 0 ? { resolvedReason: e.resolvedReason } : {}
1410
+ };
1411
+ }
1412
+ }
1413
+ return null;
1414
+ }
1380
1415
  function listPendingReviews(model, workspace) {
1381
1416
  const all = [...model.pendingReviews.values()];
1382
1417
  return workspace ? all.filter((r) => r.workspace === workspace) : all;
@@ -1869,9 +1904,14 @@ var init_artifact = __esm({
1869
1904
  });
1870
1905
 
1871
1906
  // ../contract/src/revision.ts
1907
+ function isEmptyContent(contentKind) {
1908
+ return contentKind === EMPTY_CONTENT_KIND;
1909
+ }
1910
+ var EMPTY_CONTENT_KIND;
1872
1911
  var init_revision = __esm({
1873
1912
  "../contract/src/revision.ts"() {
1874
1913
  "use strict";
1914
+ EMPTY_CONTENT_KIND = "empty";
1875
1915
  }
1876
1916
  });
1877
1917
 
@@ -3116,6 +3156,9 @@ function truncateReviewNote(note) {
3116
3156
  \u2026\uFF08\u8BC4\u8BED\u8FC7\u957F\u5DF2\u622A\u65AD\uFF0C\u539F\u6587\u5171 ${note.length} \u5B57\uFF09`;
3117
3157
  }
3118
3158
  async function spreadRevisionContent(blobs, rev, out, prefix) {
3159
+ if (isEmptyContent(rev.contentKind)) {
3160
+ return { inlineText: null, listing: "\uFF08\u8FD9\u4E00\u8F6E\u6CA1\u6709\u4EA4\u4ED8\u5185\u5BB9\uFF09" };
3161
+ }
3119
3162
  if (rev.contentKind === "external-pin") {
3120
3163
  return { inlineText: null, listing: `external-pin \u2192 ${rev.contentRef}\uFF08\u672C\u4F53\u5728\u5916\u90E8\u7CFB\u7EDF\uFF0C\u6863\u6848\u67DC\u672A\u5B58\u6B63\u6587\uFF09` };
3121
3164
  }
@@ -3263,8 +3306,9 @@ async function assembleContext(args) {
3263
3306
  }
3264
3307
  if (args.materializeSkills) Object.assign(files, await args.materializeSkills().catch(() => ({})));
3265
3308
  const replyMemories = args.resolveActorMemoryIndex && replyMemoryEnabled ? await args.resolveActorMemoryIndex(memoryQueryTerms([a.body, artifact.title, artifact.description])).catch(() => []) : [];
3309
+ const ownerReply = args.actorId !== void 0 && args.actorId === artifact.owner;
3266
3310
  files["TASK.md"] = [
3267
- `# \u56DE\u590D\u8F6E\uFF1A\u4F60\u88AB @ \u8FDB\u4E00\u5C01\u4FE1\u91CC\u56DE\u8BDD`,
3311
+ ownerReply ? `# \u56DE\u4FE1\u8F6E\uFF1A\u4F60\u4EA4\u4ED8\u7684\u4EA7\u7269\u6536\u5230\u4E00\u6761\u8BC4\u8BBA\uFF0C\u7B49\u4F60\u7B54\u590D` : `# \u56DE\u590D\u8F6E\uFF1A\u4F60\u88AB @ \u8FDB\u4E00\u5C01\u4FE1\u91CC\u56DE\u8BDD`,
3268
3312
  ``,
3269
3313
  `artifact_id: ${artifactId}`,
3270
3314
  `annotation_id: ${a.id}`,
@@ -3272,8 +3316,13 @@ async function assembleContext(args) {
3272
3316
  `your_actor_id: ${args.actorId ?? ""}`,
3273
3317
  ``,
3274
3318
  `## \u672C\u6B21\u4E3A\u4EC0\u4E48\u5524\u8D77\u4F60`,
3275
- `\u6709\u4EBA\u5728\u4EA7\u7269 ${artifactId} \u7684\u4E00\u5C01\u4FE1\u91CC @ \u4E86\u4F60\uFF0C\u7B49\u4F60\u56DE\u8BDD\u3002\u4F60**\u4E0D\u662F**\u8FD9\u4E2A\u4EA7\u7269\u7684 owner\u2014\u2014`,
3276
- `\u672C\u8F6E\u4F60\u53EA\u6709\u53D1\u8A00\u6743\uFF1A\u8BFB\u4E0A\u4E0B\u6587\u3001\u56DE\u4E00\u5E16\uFF0C\u522B\u7684\u90FD\u4E0D\u7528\uFF08\u4E5F\u4E0D\u80FD\uFF09\u505A\u3002`,
3319
+ ...ownerReply ? [
3320
+ `\u4F60\u6B64\u524D\u5B8C\u6210\u5E76\u4EA4\u4ED8\u4E86\u4EA7\u7269 ${artifactId}\uFF08\u300C${artifact.title}\u300D\uFF09\u3002\u73B0\u5728\u6709\u4EBA\u5BF9\u5B83\u7559\u4E86\u4E00\u6761\u8BC4\u8BBA\uFF0C\u7B49\u4F60\u7B54\u590D\u3002`,
3321
+ `\u672C\u8F6E**\u53EA\u56DE\u8BDD\uFF0C\u4E0D\u6539\u4EA7\u7269**\uFF1A\u8BFB\u8BC4\u8BBA\u3001\u7ED3\u5408\u4F60\u4EA4\u4ED8\u65F6\u7684\u4E0A\u4E0B\u6587\uFF0C\u56DE\u4E00\u5E16\u628A\u95EE\u9898\u8BF4\u6E05\u695A\u3002`
3322
+ ] : [
3323
+ `\u6709\u4EBA\u5728\u4EA7\u7269 ${artifactId} \u7684\u4E00\u5C01\u4FE1\u91CC @ \u4E86\u4F60\uFF0C\u7B49\u4F60\u56DE\u8BDD\u3002\u4F60**\u4E0D\u662F**\u8FD9\u4E2A\u4EA7\u7269\u7684 owner\u2014\u2014`,
3324
+ `\u672C\u8F6E\u4F60\u53EA\u6709\u53D1\u8A00\u6743\uFF1A\u8BFB\u4E0A\u4E0B\u6587\u3001\u56DE\u4E00\u5E16\uFF0C\u522B\u7684\u90FD\u4E0D\u7528\uFF08\u4E5F\u4E0D\u80FD\uFF09\u505A\u3002`
3325
+ ],
3277
3326
  ``,
3278
3327
  `## \u8981\u4F60\u56DE\u7684\u65B0\u8BDD\uFF08\u4F5C\u8005 ${actorLabel(a.author)}${a.state === "open" ? "" : `\uFF0C\u72B6\u6001 ${a.state}`}\uFF09`,
3279
3328
  ...newPostLines,
@@ -3287,12 +3336,17 @@ async function assembleContext(args) {
3287
3336
  `- \`oasis reply ${a.id} --body "<\u4F60\u7684\u56DE\u8BDD>"\` \u2014\u2014 **\u672C\u8F6E\u552F\u4E00\u7684\u5199\u52A8\u4F5C**\u3002`,
3288
3337
  ``,
3289
3338
  `\u6536\u5BB9\u8FB9\u754C\uFF08\u7CFB\u7EDF\u5728\u670D\u52A1\u7AEF\u786C\u62E6\uFF0C\u4E0D\u662F\u5BA2\u5957\uFF09\uFF1A\u4E0D\u80FD\u6539\u4EA7\u7269\uFF08propose/merge/conclude \u90FD\u4E0D\u53EF\u7528\uFF09\u3001`,
3290
- `\u4E0D\u80FD\u518D @ \u4EFB\u4F55\u4EBA\uFF08@ \u6743\u53EA\u5C5E\u4E8E\u4EBA\uFF09\u3001\u4E0D\u80FD resolve \u5173\u4FE1\uFF08\u90A3\u662F owner/\u4F5C\u8005\u7684\u4E8B\uFF09\u3002\u56DE\u5B8C\u5E16\u5373\u6536\u5DE5\u3002`,
3339
+ ownerReply ? `\u4E0D\u80FD\u518D @ \u4EFB\u4F55\u4EBA\uFF08@ \u6743\u53EA\u5C5E\u4E8E\u4EBA\uFF09\u3001\u4E0D\u7528 resolve \u5173\u4FE1\uFF08\u56DE\u5E16\u540E\u7531\u7CFB\u7EDF\u81EA\u52A8\u5173\uFF09\u3002\u56DE\u5B8C\u5E16\u5373\u6536\u5DE5\u3002` : `\u4E0D\u80FD\u518D @ \u4EFB\u4F55\u4EBA\uFF08@ \u6743\u53EA\u5C5E\u4E8E\u4EBA\uFF09\u3001\u4E0D\u80FD resolve \u5173\u4FE1\uFF08\u90A3\u662F owner/\u4F5C\u8005\u7684\u4E8B\uFF09\u3002\u56DE\u5B8C\u5E16\u5373\u6536\u5DE5\u3002`,
3291
3340
  ``,
3292
- // 5:给「非 owner agent」一条明确出路——不能改产物时别硬扛/别空承诺,请提问人 @ owner。
3293
- `**\u672C\u8F6E\u4F60\u53EA\u80FD\u56DE\u8BDD\uFF0C\u6539\u4E0D\u4E86\u4EA7\u7269\u3002** \u5982\u679C\u4F60\u7684\u7B54\u590D\u610F\u5473\u7740\u4EA7\u7269\u9700\u8981\u6539\uFF08\u8865\u4E00\u6BB5\u3001\u6539\u4E00\u4E2A\u5224\u636E\u3001\u6362\u4E2A\u65B9\u6848\uFF09\uFF0C`,
3294
- `**\u4E0D\u8981\u81EA\u5DF1\u52A8\u624B\u3001\u4E5F\u4E0D\u8981\u627F\u8BFA"\u6211\u53BB\u6539"**\u2014\u2014\u5728\u56DE\u5E16\u91CC\u8BF4\u6E05\u300C\u8981\u6539\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u300D\uFF0C\u5E76\u8BF7\u63D0\u95EE\u7684\u4EBA`,
3295
- `**@ \u672C\u4EA7\u7269\u7684\u8D1F\u8D23\u4EBA\uFF08owner\uFF09** \u6765\u5904\u7F6E\uFF1A\u53EA\u6709 owner \u88AB\u5524\u8D77\u624D\u80FD\u6539\u4EA7\u7269\uFF0C\u5B83\u80FD\u770B\u5230\u8FD9\u6761\u4FE1\u7684\u5B8C\u6574\u4E0A\u4E0B\u6587\u3002`,
3341
+ ...ownerReply ? [
3342
+ `**\u672C\u8F6E\u4F60\u53EA\u80FD\u56DE\u8BDD\uFF0C\u6539\u4E0D\u4E86\u4EA7\u7269\u3002** \u5982\u679C\u8BC4\u8BBA\u610F\u5473\u7740\u4EA7\u7269\u9700\u8981\u6539\uFF08\u8865\u4E00\u6BB5\u3001\u6539\u4E00\u4E2A\u5224\u636E\u3001\u6362\u4E2A\u65B9\u6848\uFF09\uFF0C`,
3343
+ `**\u4E0D\u8981\u627F\u8BFA"\u6211\u8FD9\u8F6E\u53BB\u6539"**\u2014\u2014\u5728\u56DE\u5E16\u91CC\u8BF4\u6E05\u300C\u8981\u6539\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u300D\uFF0C\u4EBA\u770B\u5230\u540E\u4F1A\u51B3\u5B9A\u662F\u5426\u8BA9\u8282\u70B9\u91CD\u8DD1\uFF08\u91CD\u8DD1\u4F1A\u5E26\u4E0A\u8FD9\u6761\u8BC4\u8BBA\u7684\u4E0A\u4E0B\u6587\uFF09\u3002`
3344
+ ] : [
3345
+ // 改 5:给「非 owner agent」一条明确出路——不能改产物时别硬扛/别空承诺,请提问人 @ owner。
3346
+ `**\u672C\u8F6E\u4F60\u53EA\u80FD\u56DE\u8BDD\uFF0C\u6539\u4E0D\u4E86\u4EA7\u7269\u3002** \u5982\u679C\u4F60\u7684\u7B54\u590D\u610F\u5473\u7740\u4EA7\u7269\u9700\u8981\u6539\uFF08\u8865\u4E00\u6BB5\u3001\u6539\u4E00\u4E2A\u5224\u636E\u3001\u6362\u4E2A\u65B9\u6848\uFF09\uFF0C`,
3347
+ `**\u4E0D\u8981\u81EA\u5DF1\u52A8\u624B\u3001\u4E5F\u4E0D\u8981\u627F\u8BFA"\u6211\u53BB\u6539"**\u2014\u2014\u5728\u56DE\u5E16\u91CC\u8BF4\u6E05\u300C\u8981\u6539\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u300D\uFF0C\u5E76\u8BF7\u63D0\u95EE\u7684\u4EBA`,
3348
+ `**@ \u672C\u4EA7\u7269\u7684\u8D1F\u8D23\u4EBA\uFF08owner\uFF09** \u6765\u5904\u7F6E\uFF1A\u53EA\u6709 owner \u88AB\u5524\u8D77\u624D\u80FD\u6539\u4EA7\u7269\uFF0C\u5B83\u80FD\u770B\u5230\u8FD9\u6761\u4FE1\u7684\u5B8C\u6574\u4E0A\u4E0B\u6587\u3002`
3349
+ ],
3296
3350
  ...memoryIndexLines(replyMemories)
3297
3351
  ].join("\n");
3298
3352
  return { files };
@@ -3341,6 +3395,7 @@ async function assembleContext(args) {
3341
3395
  }
3342
3396
  const contentMapOf = async (rev) => {
3343
3397
  if (!rev) return /* @__PURE__ */ new Map();
3398
+ if (isEmptyContent(rev.contentKind)) return null;
3344
3399
  if (rev.contentKind === "external-pin") return null;
3345
3400
  if (rev.contentKind === "manifest") {
3346
3401
  const m2 = /* @__PURE__ */ new Map();
@@ -4004,6 +4059,7 @@ ${manifestDiff(base, next)}`
4004
4059
  }
4005
4060
  async function revContentMap(blobs, rev) {
4006
4061
  if (!rev) return /* @__PURE__ */ new Map();
4062
+ if (isEmptyContent(rev.contentKind)) return null;
4007
4063
  if (rev.contentKind === "external-pin") return null;
4008
4064
  if (rev.contentKind === "manifest") {
4009
4065
  const m2 = /* @__PURE__ */ new Map();
@@ -5187,7 +5243,18 @@ var init_dispatcher = __esm({
5187
5243
  actor: args.actor,
5188
5244
  action: "produce",
5189
5245
  ...args.part !== void 0 ? { part: args.part } : {}
5190
- }, args.bypassScheduling === true);
5246
+ }, args.bypassScheduling === true, args.dispatchId);
5247
+ }
5248
+ /** 新引擎回信 work(设计-最终 §回信轮)的 effect 调到此——派发收容回信会话(唯一写动作 `oasis reply`)。
5249
+ * jobKey 锚在 issue(annotationId)上,与既有回复轮(信箱 ④ 被 @)同一套装配与写围栏。 */
5250
+ async requestReplyWork(args) {
5251
+ const jobKey = `reply::${args.annotationId}::${args.actor}`;
5252
+ return this.spawnJob(jobKey, {
5253
+ artifactId: args.artifactId,
5254
+ actor: args.actor,
5255
+ action: "reply",
5256
+ annotationId: args.annotationId
5257
+ }, args.bypassScheduling === true, args.dispatchId);
5191
5258
  }
5192
5259
  /** 新引擎 review.create 的 effect 调到此——派发 AI 评审会话。 */
5193
5260
  async requestReview(args) {
@@ -5196,7 +5263,7 @@ var init_dispatcher = __esm({
5196
5263
  artifactId: args.artifactId,
5197
5264
  actor: args.actor,
5198
5265
  action: "review"
5199
- }, args.bypassScheduling === true);
5266
+ }, args.bypassScheduling === true, args.dispatchId);
5200
5267
  }
5201
5268
  /** 该产物是否有在途**生产**会话(部件或整体)——集成须等所有 producer 退场后才抢。 */
5202
5269
  hasInFlightProduce(artifactId) {
@@ -5220,9 +5287,9 @@ var init_dispatcher = __esm({
5220
5287
  * 后者答「哪个运行时进程」(节点铸、spawn 才有)。二者一一对应,映射记在 run 上,不合并。
5221
5288
  */
5222
5289
  spawnAttemptEpoch = /* @__PURE__ */ new Map();
5223
- async spawnJob(jobKey, spec, bypassScheduling = false) {
5290
+ async spawnJob(jobKey, spec, bypassScheduling = false, dispatchId) {
5224
5291
  if (this.spawning.has(jobKey)) return { kind: "already-in-flight", jobKey };
5225
- const attempt = `dispatch:${(0, import_node_crypto2.randomUUID)()}`;
5292
+ const attempt = dispatchId ?? `dispatch:${(0, import_node_crypto2.randomUUID)()}`;
5226
5293
  this.spawnAttemptEpoch.set(jobKey, attempt);
5227
5294
  this.spawning.add(jobKey);
5228
5295
  const work = this.spawnJobInner(jobKey, spec, attempt, bypassScheduling).then(
@@ -6079,19 +6146,18 @@ var init_dispatcher = __esm({
6079
6146
  });
6080
6147
 
6081
6148
  // ../engine/src/model.ts
6082
- var DEFAULT_CONFIG, MAX_WORK_DURATION_MS, MAX_REVIEW_DURATION_MS, MAX_REVIEW_RETRIES, deriveWorkId, deriveReviewId, deriveReplyId;
6149
+ var DEFAULT_CONFIG, MAX_WORK_DURATION_MS, MAX_REVIEW_DURATION_MS, MAX_REVIEW_RETRIES, deriveWorkId, deriveReplyWorkId, deriveReviewId, deriveReplyId;
6083
6150
  var init_model = __esm({
6084
6151
  "../engine/src/model.ts"() {
6085
6152
  "use strict";
6086
6153
  DEFAULT_CONFIG = {
6087
- maxRetries: 3,
6088
- dispatchWaitMs: 5 * 6e4,
6089
- leaseMs: 2 * 6e4
6154
+ maxRetries: 3
6090
6155
  };
6091
6156
  MAX_WORK_DURATION_MS = 30 * 6e4;
6092
6157
  MAX_REVIEW_DURATION_MS = 15 * 6e4;
6093
6158
  MAX_REVIEW_RETRIES = 3;
6094
6159
  deriveWorkId = (nodeId, version2) => `wk:${nodeId}:${version2}`;
6160
+ deriveReplyWorkId = (issueId, n) => `wk:reply:${issueId}:${n}`;
6095
6161
  deriveReviewId = (workId, reviewer, group) => `rv:${workId}:${reviewer}:${group}`;
6096
6162
  deriveReplyId = (issueId, salt) => `ir:${issueId}:${salt}`;
6097
6163
  }
@@ -6177,7 +6243,23 @@ function isReviewOpen(r) {
6177
6243
  return !r.cancelledAt && !r.endedAt && !r.verdict;
6178
6244
  }
6179
6245
  function countFailedForNode(works) {
6180
- const sorted = [...works].sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6246
+ const sorted = [...works].filter((w2) => !w2.replyToIssueId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6247
+ let count2 = 0;
6248
+ for (const w2 of sorted) {
6249
+ if (w2.retryAt) break;
6250
+ const s2 = w2.status ?? (w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
6251
+ if (s2 === "running") continue;
6252
+ if (s2 === "dead" && !w2.endedAt) continue;
6253
+ if (s2 === "failed" || s2 === "dead" && w2.endedAt) {
6254
+ count2++;
6255
+ continue;
6256
+ }
6257
+ break;
6258
+ }
6259
+ return count2;
6260
+ }
6261
+ function countFailedForReplyIssue(issueId, works) {
6262
+ const sorted = works.filter((w2) => w2.replyToIssueId === issueId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6181
6263
  let count2 = 0;
6182
6264
  for (const w2 of sorted) {
6183
6265
  if (w2.retryAt) break;
@@ -6468,15 +6550,7 @@ var init_state = __esm({
6468
6550
  function scan(state, ctx) {
6469
6551
  const events = [];
6470
6552
  const now = ctx.at;
6471
- const resumeMarker = state.workorder.resumeMarker;
6472
6553
  for (const node of state.liveNodes()) {
6473
- if (node.lastResumeMarker < resumeMarker) {
6474
- const rlw = node.latestWorkId ? state.work(node.latestWorkId) : null;
6475
- if (rlw && rlw.deadAt !== null && rlw.endedAt === null) {
6476
- tryToRun(state, node.id, ctx, events);
6477
- continue;
6478
- }
6479
- }
6480
6554
  const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
6481
6555
  if (!lw) {
6482
6556
  tryToRun(state, node.id, ctx, events);
@@ -6496,6 +6570,11 @@ function scan(state, ctx) {
6496
6570
  continue;
6497
6571
  }
6498
6572
  if (wstate === "running") {
6573
+ const runningBlocking = findOpenBlockingIssueFromOthers(node.id, state.allIssues());
6574
+ if (runningBlocking && runningBlocking.createdAt > (lw.startedAt ?? lw.createdAt)) {
6575
+ tryToRun(state, node.id, ctx, events);
6576
+ continue;
6577
+ }
6499
6578
  const start = lw.startedAt ?? lw.createdAt;
6500
6579
  const elapsed = new Date(now).getTime() - new Date(start).getTime();
6501
6580
  if (elapsed > MAX_WORK_DURATION_MS) {
@@ -6545,6 +6624,10 @@ function scan(state, ctx) {
6545
6624
  }
6546
6625
  continue;
6547
6626
  }
6627
+ if (wstate === "success" && node.latestAcceptId === lw.id) {
6628
+ processCommentReplies(state, node.id, ctx, events);
6629
+ continue;
6630
+ }
6548
6631
  }
6549
6632
  const terminated = events.length === 0 && checkTerminated(state);
6550
6633
  return { events, terminated };
@@ -6552,7 +6635,6 @@ function scan(state, ctx) {
6552
6635
  function tryToRun(state, nodeId, ctx, events) {
6553
6636
  const node = state.node(nodeId);
6554
6637
  if (!node) return;
6555
- if (state.workorder.dispatchPaused) return;
6556
6638
  if (!node.assigneeActorId) return;
6557
6639
  const inbound = state.edgesInto(nodeId);
6558
6640
  for (const e of inbound) {
@@ -6669,6 +6751,92 @@ function processReviews(state, nodeId, workId, ctx, events) {
6669
6751
  });
6670
6752
  }
6671
6753
  }
6754
+ function processCommentReplies(state, nodeId, ctx, events) {
6755
+ const node = state.node(nodeId);
6756
+ if (!node?.assigneeActorId) return;
6757
+ const comments = state.allIssues().filter((i) => i.kind === "comment" && i.state === "open" && i.aboutNodeId === nodeId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
6758
+ if (comments.length === 0) return;
6759
+ if (state.hasQueuedWorkFor(nodeId)) return;
6760
+ const replyWorks = state.worksOf(nodeId).filter((w2) => w2.replyToIssueId);
6761
+ const inflight = replyWorks.find((w2) => !w2.endedAt && !w2.deadAt);
6762
+ if (inflight) {
6763
+ const start = inflight.startedAt ?? inflight.createdAt;
6764
+ const elapsed = new Date(ctx.at).getTime() - new Date(start).getTime();
6765
+ if (elapsed > MAX_WORK_DURATION_MS) {
6766
+ events.push({ kind: "work.timeout", workorderId: state.workorder.id, workId: inflight.id });
6767
+ }
6768
+ return;
6769
+ }
6770
+ for (const issue2 of comments) {
6771
+ const mine = replyWorks.filter((w2) => w2.replyToIssueId === issue2.id).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
6772
+ const latest = mine[mine.length - 1];
6773
+ if (latest) {
6774
+ const s2 = workState(latest, hasOutput(latest, 0));
6775
+ if (s2 === "success" && latest.acceptanceState !== "accepted") {
6776
+ events.push({
6777
+ kind: "work.accept",
6778
+ workorderId: state.workorder.id,
6779
+ workId: latest.id,
6780
+ by: "actor:system:engine",
6781
+ override: null
6782
+ });
6783
+ return;
6784
+ }
6785
+ if (s2 === "failed") {
6786
+ if (countFailedForReplyIssue(issue2.id, replyWorks) > (ctx.config?.maxRetries ?? DEFAULT_CONFIG.maxRetries)) {
6787
+ events.push({
6788
+ kind: "work.kill",
6789
+ workorderId: state.workorder.id,
6790
+ nodeId,
6791
+ workId: latest.id,
6792
+ reason: "reply failed exhausted"
6793
+ });
6794
+ } else {
6795
+ events.push({
6796
+ kind: "work.create",
6797
+ workorderId: state.workorder.id,
6798
+ nodeId,
6799
+ workId: deriveReplyWorkId(issue2.id, mine.length + 1),
6800
+ reasons: [],
6801
+ continuesWorkId: node.latestAcceptId,
6802
+ replyToIssueId: issue2.id
6803
+ });
6804
+ }
6805
+ return;
6806
+ }
6807
+ if (s2 === "retry") {
6808
+ events.push({
6809
+ kind: "work.create",
6810
+ workorderId: state.workorder.id,
6811
+ nodeId,
6812
+ workId: deriveReplyWorkId(issue2.id, mine.length + 1),
6813
+ reasons: [],
6814
+ continuesWorkId: node.latestAcceptId,
6815
+ replyToIssueId: issue2.id
6816
+ });
6817
+ return;
6818
+ }
6819
+ if (s2 === "dead") {
6820
+ const acceptedWork = node.latestAcceptId ? state.work(node.latestAcceptId) : null;
6821
+ const reFinished = !!acceptedWork?.acceptedAt && latest.createdAt < acceptedWork.acceptedAt;
6822
+ if (!reFinished) continue;
6823
+ }
6824
+ }
6825
+ const replies = state.repliesOf(issue2.id).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
6826
+ const lastAuthor = replies.length > 0 ? replies[replies.length - 1].authorActorId : issue2.authorActorId;
6827
+ if (lastAuthor === node.assigneeActorId) continue;
6828
+ events.push({
6829
+ kind: "work.create",
6830
+ workorderId: state.workorder.id,
6831
+ nodeId,
6832
+ workId: deriveReplyWorkId(issue2.id, mine.length + 1),
6833
+ reasons: [],
6834
+ continuesWorkId: node.latestAcceptId,
6835
+ replyToIssueId: issue2.id
6836
+ });
6837
+ return;
6838
+ }
6839
+ }
6672
6840
  function nextReviewId(state, workId, reviewerActorId, group) {
6673
6841
  const existing = state.reviewsOf(workId).filter(
6674
6842
  (r) => r.reviewerActorId === reviewerActorId && r.reviewGroup === group
@@ -6677,6 +6845,7 @@ function nextReviewId(state, workId, reviewerActorId, group) {
6677
6845
  return `rv:${workId}:${reviewerActorId}:${group}:${existing.length + 1}`;
6678
6846
  }
6679
6847
  function reviewStatus(r) {
6848
+ if (r.status) return r.status;
6680
6849
  if (r.retryAt) return "retry";
6681
6850
  if (r.cancelledAt) return "dead";
6682
6851
  if (r.verdict === "approve") return "accept";
@@ -6689,15 +6858,15 @@ function checkTerminated(state) {
6689
6858
  if (state.allWorks().some(isWorkOpen)) return false;
6690
6859
  if (state.allIssues().some((i) => i.state === "open")) return false;
6691
6860
  const live = state.liveNodes();
6692
- if (live.length === 0) return false;
6861
+ if (live.length === 0) return true;
6693
6862
  return live.every((n) => n.latestWorkId !== null && n.latestAcceptId === n.latestWorkId);
6694
6863
  }
6695
6864
  function activate(state, nodeId, _ctx) {
6696
6865
  const node = state.node(nodeId);
6697
6866
  if (!node) return "cancelled";
6698
- if (node.cancelledAt || state.workorder.lifecycle === "sealed") return "cancelled";
6867
+ if (node.cancelledAt) return "cancelled";
6699
6868
  if (state.workorder.lifecycle === "draft") return "paused";
6700
- if (node.paused || state.workorder.dispatchPaused) return "paused";
6869
+ if (state.workorder.dispatchPaused) return "paused";
6701
6870
  if (!node.assigneeActorId) return "no_assignee";
6702
6871
  if (state.openWorkOf(nodeId)) return "already_running";
6703
6872
  if (state.hasQueuedWorkFor(nodeId)) return "already_running";
@@ -6744,12 +6913,11 @@ CREATE TABLE IF NOT EXISTS ${schema}.workorders (
6744
6913
  owner_actor_id text NOT NULL,
6745
6914
  manager_actor_id text,
6746
6915
  root_node_id text,
6747
- lifecycle text NOT NULL CHECK (lifecycle IN ('draft','active','sealed')),
6748
- seal_reason text CHECK (seal_reason IN ('accepted','cancelled','frozen','converged')),
6916
+ lifecycle text NOT NULL CHECK (lifecycle IN ('draft','active')),
6749
6917
  dispatch_paused boolean NOT NULL DEFAULT false,
6750
6918
  paused_by text,
6751
6919
  paused_reason text,
6752
- resume_marker integer NOT NULL DEFAULT 0,
6920
+ done_at timestamptz,
6753
6921
  structural_version integer NOT NULL DEFAULT 0,
6754
6922
  row_version integer NOT NULL DEFAULT 0,
6755
6923
  created_at timestamptz NOT NULL,
@@ -6765,19 +6933,11 @@ CREATE TABLE IF NOT EXISTS ${schema}.workflow_nodes (
6765
6933
  title text NOT NULL,
6766
6934
  spec text,
6767
6935
  version integer NOT NULL DEFAULT 0,
6768
- spec_version integer NOT NULL DEFAULT 0,
6769
- consumed_spec_version integer NOT NULL DEFAULT 0,
6770
6936
  assignee_actor_id text,
6771
6937
  assignee_role text,
6772
- current_work_id text,
6773
6938
  latest_work_id text,
6774
6939
  latest_accept_id text,
6775
- last_resume_marker integer NOT NULL DEFAULT 0,
6776
6940
  is_final_output boolean NOT NULL DEFAULT false,
6777
- retry_count integer NOT NULL DEFAULT 0,
6778
- paused boolean NOT NULL DEFAULT false,
6779
- paused_by text,
6780
- paused_reason text,
6781
6941
  fields jsonb,
6782
6942
  cancelled_at timestamptz,
6783
6943
  row_version integer NOT NULL DEFAULT 0,
@@ -6829,6 +6989,8 @@ CREATE TABLE IF NOT EXISTS ${schema}.works (
6829
6989
  outcome_detail jsonb,
6830
6990
  session_ref text,
6831
6991
  continues_work_id text,
6992
+ -- \u56DE\u4FE1 work\uFF08\u8BBE\u8BA1-\u6700\u7EC8 \xA7\u56DE\u4FE1\u8F6E\uFF09\uFF1A\u975E\u7A7A = \u5BF9\u8BE5 comment issue \u7684\u56DE\u4FE1\u8F6E\uFF0C\u4E0D\u8FDB\u8282\u70B9\u4E3B\u94FE
6993
+ reply_to_issue_id text,
6832
6994
  output_version_no integer,
6833
6995
  conclusion text,
6834
6996
  acceptance_state text CHECK (acceptance_state IN ('draft','accepted','rejected')),
@@ -6971,7 +7133,6 @@ CREATE TABLE IF NOT EXISTS ${schema}.events (
6971
7133
  status text NOT NULL CHECK (status IN ('pending','applied','done','failed')),
6972
7134
  attempts integer NOT NULL DEFAULT 0,
6973
7135
  available_at timestamptz,
6974
- effect_lease_until timestamptz,
6975
7136
  failed_reason text,
6976
7137
  created_at timestamptz NOT NULL,
6977
7138
  applied_at timestamptz,
@@ -6995,7 +7156,46 @@ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS retry_at timestamptz;
6995
7156
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS retry_at timestamptz;
6996
7157
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS status text;
6997
7158
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS status text;
7159
+ -- \u56DE\u4FE1 work\uFF08\u8BBE\u8BA1-\u6700\u7EC8 \xA7\u56DE\u4FE1\u8F6E\uFF09\uFF1A\u975E\u7A7A = \u5BF9\u8BE5 comment issue \u7684\u56DE\u4FE1\u8F6E\uFF0C\u4E0D\u8FDB\u8282\u70B9\u4E3B\u94FE
7160
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
6998
7161
  ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS fields jsonb;
7162
+ -- \u2605 \u5148\u5220\u89C6\u56FE\u518D\u5220\u5217\uFF1Awork_view/node_view/workorder_view \u7528 SELECT *\uFF0C\u5BF9\u57FA\u8868\u5217\u6709\u4F9D\u8D56\u2014\u2014\u5B58\u91CF\u5E93\u4E0A
7163
+ -- DROP COLUMN \u4F1A\u62A5 "cannot drop column ... because other objects depend on it"\uFF0C\u4E14\u672C\u6BB5\u662F\u5355\u6761
7164
+ -- \u591A\u8BED\u53E5 Query\uFF08\u4E00\u4E2A\u9690\u5F0F\u4E8B\u52A1\uFF09\uFF0C\u4E00\u5904\u5931\u8D25\u5168\u6BB5\u56DE\u6EDA\u3001migrate \u629B\u9519 \u2192 serve \u8D77\u4E0D\u6765\u3002\u89C6\u56FE\u968F\u540E\u7531
7165
+ -- ENGINE_VIEWS \u91CD\u5EFA\uFF08migrate \u987A\u5E8F\uFF1AENGINE_DDL \u2192 ENGINE_VIEWS\uFF09\u3002
7166
+ DROP VIEW IF EXISTS ${schema}.work_view;
7167
+ DROP VIEW IF EXISTS ${schema}.review_view;
7168
+ DROP VIEW IF EXISTS ${schema}.node_view;
7169
+ DROP VIEW IF EXISTS ${schema}.workorder_view;
7170
+ -- \u8C03\u5EA6\u5668\u5B8C\u7ED3\u6807\u8BB0\uFF08mark_done/unmark_done \u843D\u70B9\uFF0C2026-08-12\uFF09\u3002
7171
+ ALTER TABLE ${schema}.workorders ADD COLUMN IF NOT EXISTS done_at timestamptz;
7172
+ -- \u2605 \u5DE5\u5355\u7EA7\u5C01\u5B58\u6574\u4F53\u79FB\u9664\uFF08ADR 0137\uFF09\uFF1Alifecycle \u53EA\u5269 draft/active\uFF0Cseal_reason \u5217\u5220\u9664\u3002
7173
+ -- \u5B58\u91CF sealed \u5DE5\u5355\u8FC1\u79FB\uFF1A\u5148\u628A\u5176\u5728\u518C\u8282\u70B9\u5168\u90E8\u4F5C\u5E9F\uFF08cancelled_at\u2014\u2014\u4FDD\u4F4F\u300C\u5DF2\u5F52\u6863/\u5DF2\u5E9F\u5F03\u300D\u7684
7174
+ -- \u4E1A\u52A1\u8BED\u4E49\uFF0Cper-node \u4F5C\u5E9F\u662F\u552F\u4E00\u4FDD\u7559\u7684\u4F5C\u5E9F\u673A\u5236\uFF09\uFF0C\u518D\u8FC1\u56DE active + done_at\uFF08\u79FB\u51FA\u8C03\u5EA6\u626B\u63CF\uFF09\u3002
7175
+ UPDATE ${schema}.workflow_nodes SET cancelled_at = COALESCE(cancelled_at, now())
7176
+ WHERE cancelled_at IS NULL
7177
+ AND workorder_id IN (SELECT id FROM ${schema}.workorders WHERE lifecycle = 'sealed');
7178
+ UPDATE ${schema}.workorders SET lifecycle = 'active', done_at = COALESCE(done_at, updated_at)
7179
+ WHERE lifecycle = 'sealed';
7180
+ -- CHECK \u7EA6\u675F\u6536\u7A84\u4E3A draft/active\uFF08\u5148\u5220\u540E\u52A0\uFF0C\u6210\u5BF9\u5E42\u7B49\uFF09\uFF1Bseal_reason \u968F\u540E\u5220\u5217\u3002
7181
+ ALTER TABLE ${schema}.workorders DROP CONSTRAINT IF EXISTS workorders_lifecycle_check;
7182
+ ALTER TABLE ${schema}.workorders ADD CONSTRAINT workorders_lifecycle_check
7183
+ CHECK (lifecycle IN ('draft','active'));
7184
+ ALTER TABLE ${schema}.workorders DROP COLUMN IF EXISTS seal_reason;
7185
+ -- \u65B0\u5F15\u64CE\u5DF2\u4E0D\u7528\u7684\u65E7\u5217\uFF08node.version \u7EDF\u4E00\u8BA1\u6570\u5668\u53D6\u4EE3 spec_version/consumed_spec_version\uFF1B
7186
+ -- latest_work_id/latest_accept_id \u53D6\u4EE3 current_work_id\uFF1B\u5DE5\u5355\u7EA7\u6682\u505C\u53D6\u4EE3\u8282\u70B9\u7EA7 paused*\uFF1B
7187
+ -- retry \u662F work \u72B6\u6001\u4E4B\u4E00\uFF0C\u4E0D\u662F\u8282\u70B9\u5217\uFF1BL2 \u79DF\u7EA6\u8F6E\u8BE2\u5DF2\u5220\uFF0Ceffect_lease_until \u6210\u6B7B\u5217\uFF1B
7188
+ -- resume_marker/last_resume_marker \u5DF2\u88AB\u300Cresume=\u6279\u91CF node_retry\u300D\u53D6\u4EE3\uFF0C2026-08-12\uFF09\u3002
7189
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS spec_version;
7190
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS consumed_spec_version;
7191
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS current_work_id;
7192
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS retry_count;
7193
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS paused;
7194
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS paused_by;
7195
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS paused_reason;
7196
+ ALTER TABLE ${schema}.workflow_nodes DROP COLUMN IF EXISTS last_resume_marker;
7197
+ ALTER TABLE ${schema}.workorders DROP COLUMN IF EXISTS resume_marker;
7198
+ ALTER TABLE ${schema}.events DROP COLUMN IF EXISTS effect_lease_until;
6999
7199
  -- \u5B58\u91CF\u56DE\u586B\uFF1Astatus \u5217=null \u7684\u884C\u6309\u65F6\u95F4\u6233/\u4EA7\u51FA\u56DE\u586B\uFF08\u4E0E views.ts \u56DE\u9000\u6D3E\u751F\u540C\u8BED\u4E49\uFF0C\u56DE\u586B\u540E\u72B6\u6001\u5373\u5B58\u50A8\u6001\uFF09
7000
7200
  UPDATE ${schema}.works SET status = CASE
7001
7201
  WHEN retry_at IS NOT NULL THEN 'retry'
@@ -7020,43 +7220,29 @@ function ENGINE_VIEWS(schema) {
7020
7220
  return `
7021
7221
  DROP VIEW IF EXISTS ${schema}.work_view;
7022
7222
  CREATE VIEW ${schema}.work_view AS
7023
- SELECT w.*,
7024
- CASE WHEN w.cancelled_at IS NOT NULL THEN 'cancelled'
7025
- WHEN w.ended_at IS NOT NULL THEN 'finished'
7026
- WHEN w.started_at IS NOT NULL THEN 'running'
7027
- ELSE 'waiting' END AS state
7223
+ SELECT w.*, w.status AS state
7028
7224
  FROM ${schema}.works w;
7029
7225
 
7030
7226
  DROP VIEW IF EXISTS ${schema}.review_view;
7031
7227
  CREATE VIEW ${schema}.review_view AS
7032
- SELECT r.*,
7033
- CASE WHEN r.cancelled_at IS NOT NULL THEN 'cancelled'
7034
- WHEN r.ended_at IS NOT NULL THEN 'finished'
7035
- WHEN r.started_at IS NOT NULL THEN 'running'
7036
- ELSE 'waiting' END AS state
7228
+ SELECT r.*, r.status AS state
7037
7229
  FROM ${schema}.reviews r;
7038
7230
 
7039
7231
  DROP VIEW IF EXISTS ${schema}.node_view;
7040
7232
  CREATE VIEW ${schema}.node_view AS
7041
7233
  SELECT n.*,
7042
7234
  CASE
7043
- WHEN n.cancelled_at IS NOT NULL OR o.lifecycle = 'sealed' THEN 'cancelled'
7044
- WHEN EXISTS (SELECT 1 FROM ${schema}.works w
7045
- WHERE w.node_id = n.id AND w.ended_at IS NULL AND w.cancelled_at IS NULL) THEN 'running'
7046
- WHEN EXISTS (SELECT 1 FROM ${schema}.works w
7047
- WHERE w.node_id = n.id AND w.acceptance_state = 'draft') THEN 'review'
7048
- -- \u2605 failed \u7684\u5224\u636E\u662F\u90A3\u6761 open \u7684 escalation\uFF0C\u4E0D\u662F retry_count \u8D85\u9650\uFF08\u540E\u8005\u65E0\u6E05\u96F6\u8DEF\u5F84 = \u6B7B\u9501\uFF09
7049
- WHEN EXISTS (SELECT 1 FROM ${schema}.issues i
7050
- WHERE i.raised_by_node_id = n.id AND i.kind = 'escalation'
7051
- AND i.state = 'open' AND i.blocking)
7052
- OR n.assignee_actor_id IS NULL THEN 'failed'
7053
- WHEN EXISTS (SELECT 1 FROM ${schema}.workflow_edges e
7054
- WHERE e.to_node_id = n.id AND e.required AND e.kind = 'data'
7055
- AND e.pinned_work_id IS NULL) THEN 'pending'
7056
- WHEN n.current_work_id IS NOT NULL THEN 'done'
7057
- ELSE 'pending' END AS state
7235
+ WHEN n.cancelled_at IS NOT NULL THEN 'dead' -- \u5DF2\u4F5C\u5E9F\uFF08\u79FB\u51FA\u8BA1\u5212\uFF09
7236
+ WHEN lw.id IS NULL THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08latest_work \u4E3A\u7A7A\uFF09
7237
+ WHEN lw.status = 'retry' THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08\u7528\u6237\u4ECB\u5165\u91CD\u8BD5\uFF0C\u7B49 try_to_run\uFF09
7238
+ WHEN lw.status = 'running' THEN 'running' -- \u5728\u8FDB\u884C
7239
+ WHEN lw.status = 'success' AND n.latest_accept_id IS DISTINCT FROM lw.id THEN 'reviewing' -- \u8BC4\u5BA1\u4E2D
7240
+ WHEN lw.status = 'success' AND n.latest_accept_id = lw.id THEN 'done' -- \u5DF2\u5B8C\u6210
7241
+ WHEN lw.status IN ('failed','dead') THEN 'dead' -- \u5361\u4F4F\u4E86
7242
+ ELSE 'waiting'
7243
+ END AS state
7058
7244
  FROM ${schema}.workflow_nodes n
7059
- JOIN ${schema}.workorders o ON o.id = n.workorder_id;
7245
+ LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id;
7060
7246
 
7061
7247
  DROP VIEW IF EXISTS ${schema}.workorder_view;
7062
7248
  CREATE VIEW ${schema}.workorder_view AS
@@ -7064,7 +7250,7 @@ SELECT o.*,
7064
7250
  (SELECT count(*) FROM ${schema}.workflow_nodes n
7065
7251
  WHERE n.workorder_id = o.id AND n.cancelled_at IS NULL) AS total_count,
7066
7252
  (SELECT count(*) FROM ${schema}.workflow_nodes n
7067
- WHERE n.workorder_id = o.id AND n.cancelled_at IS NULL AND n.current_work_id IS NOT NULL) AS done_count
7253
+ WHERE n.workorder_id = o.id AND n.cancelled_at IS NULL AND n.latest_accept_id IS NOT NULL) AS done_count
7068
7254
  FROM ${schema}.workorders o;
7069
7255
  `;
7070
7256
  }
@@ -12212,7 +12398,6 @@ function toRecord(r) {
12212
12398
  status: r.status,
12213
12399
  attempts: Number(r.attempts ?? 0),
12214
12400
  availableAt: iso(r.available_at),
12215
- effectLeaseUntil: iso(r.effect_lease_until),
12216
12401
  failedReason: r.failed_reason ?? null,
12217
12402
  createdAt: iso(r.created_at),
12218
12403
  appliedAt: iso(r.applied_at),
@@ -12317,11 +12502,10 @@ var init_store_postgres = __esm({
12317
12502
  managerActorId: r.manager_actor_id,
12318
12503
  rootNodeId: r.root_node_id,
12319
12504
  lifecycle: r.lifecycle,
12320
- sealReason: r.seal_reason,
12321
12505
  dispatchPaused: r.dispatch_paused,
12322
12506
  pausedBy: r.paused_by,
12323
12507
  pausedReason: r.paused_reason,
12324
- resumeMarker: r.resume_marker ?? 0,
12508
+ doneAt: iso(r.done_at),
12325
12509
  structuralVersion: r.structural_version,
12326
12510
  rowVersion: r.row_version,
12327
12511
  createdAt: iso(r.created_at),
@@ -12334,20 +12518,12 @@ var init_store_postgres = __esm({
12334
12518
  type: n.type,
12335
12519
  title: n.title,
12336
12520
  spec: n.spec,
12337
- version: n.version ?? n.spec_version ?? 0,
12338
- specVersion: n.spec_version,
12339
- consumedSpecVersion: n.consumed_spec_version,
12521
+ version: n.version ?? 0,
12340
12522
  assigneeActorId: n.assignee_actor_id,
12341
12523
  assigneeRole: n.assignee_role,
12342
- currentWorkId: n.current_work_id,
12343
12524
  latestWorkId: n.latest_work_id,
12344
- latestAcceptId: n.latest_accept_id ?? n.current_work_id,
12345
- lastResumeMarker: n.last_resume_marker ?? 0,
12525
+ latestAcceptId: n.latest_accept_id,
12346
12526
  isFinalOutput: n.is_final_output,
12347
- retryCount: n.retry_count,
12348
- paused: n.paused,
12349
- pausedBy: n.paused_by,
12350
- pausedReason: n.paused_reason,
12351
12527
  cancelledAt: iso(n.cancelled_at),
12352
12528
  fields: n.fields ?? void 0,
12353
12529
  rowVersion: n.row_version,
@@ -12389,6 +12565,7 @@ var init_store_postgres = __esm({
12389
12565
  outcomeDetail: w2.outcome_detail,
12390
12566
  sessionRef: w2.session_ref,
12391
12567
  continuesWorkId: w2.continues_work_id,
12568
+ replyToIssueId: w2.reply_to_issue_id ?? null,
12392
12569
  outputVersionNo: w2.output_version_no,
12393
12570
  conclusion: w2.conclusion,
12394
12571
  acceptanceState: w2.acceptance_state,
@@ -12481,9 +12658,9 @@ var init_store_postgres = __esm({
12481
12658
  await this.c.query(
12482
12659
  `INSERT INTO ${this.s}.workorders
12483
12660
  (id,company_id,project_id,title,description,goal,acceptance_criteria,owner_actor_id,
12484
- manager_actor_id,root_node_id,lifecycle,seal_reason,dispatch_paused,paused_by,paused_reason,
12485
- resume_marker,structural_version,row_version,created_at,created_by,updated_at)
12486
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
12661
+ manager_actor_id,root_node_id,lifecycle,dispatch_paused,paused_by,paused_reason,
12662
+ done_at,structural_version,row_version,created_at,created_by,updated_at)
12663
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)
12487
12664
  ON CONFLICT (id) DO NOTHING`,
12488
12665
  [
12489
12666
  r.id,
@@ -12497,11 +12674,10 @@ var init_store_postgres = __esm({
12497
12674
  r.managerActorId,
12498
12675
  r.rootNodeId,
12499
12676
  r.lifecycle,
12500
- r.sealReason,
12501
12677
  r.dispatchPaused,
12502
12678
  r.pausedBy,
12503
12679
  r.pausedReason,
12504
- r.resumeMarker ?? 0,
12680
+ r.doneAt,
12505
12681
  r.structuralVersion,
12506
12682
  r.rowVersion,
12507
12683
  r.createdAt,
@@ -12518,10 +12694,10 @@ var init_store_postgres = __esm({
12518
12694
  const n = m2.row;
12519
12695
  await this.c.query(
12520
12696
  `INSERT INTO ${this.s}.workflow_nodes
12521
- (id,workorder_id,type,title,spec,version,spec_version,consumed_spec_version,assignee_actor_id,assignee_role,
12522
- current_work_id,latest_work_id,latest_accept_id,last_resume_marker,is_final_output,retry_count,paused,paused_by,paused_reason,
12697
+ (id,workorder_id,type,title,spec,version,assignee_actor_id,assignee_role,
12698
+ latest_work_id,latest_accept_id,is_final_output,
12523
12699
  fields,cancelled_at,row_version,created_at,updated_at)
12524
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
12700
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
12525
12701
  ON CONFLICT (id) DO NOTHING`,
12526
12702
  [
12527
12703
  n.id,
@@ -12530,19 +12706,11 @@ var init_store_postgres = __esm({
12530
12706
  n.title,
12531
12707
  n.spec,
12532
12708
  n.version,
12533
- n.specVersion ?? 0,
12534
- n.consumedSpecVersion ?? 0,
12535
12709
  n.assigneeActorId,
12536
12710
  n.assigneeRole,
12537
- n.currentWorkId ?? null,
12538
12711
  n.latestWorkId,
12539
12712
  n.latestAcceptId,
12540
- n.lastResumeMarker,
12541
12713
  n.isFinalOutput,
12542
- n.retryCount ?? 0,
12543
- n.paused ?? false,
12544
- n.pausedBy ?? null,
12545
- n.pausedReason ?? null,
12546
12714
  n.fields ? JSON.stringify(n.fields) : null,
12547
12715
  n.cancelledAt,
12548
12716
  n.rowVersion,
@@ -12602,9 +12770,9 @@ var init_store_postgres = __esm({
12602
12770
  await this.c.query(
12603
12771
  `INSERT INTO ${this.s}.works
12604
12772
  (id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
12605
- outcome_detail,session_ref,continues_work_id,output_version_no,
12773
+ outcome_detail,session_ref,continues_work_id,reply_to_issue_id,output_version_no,
12606
12774
  node_version,conclusion,acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason)
12607
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
12775
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25)
12608
12776
  ON CONFLICT (id) DO NOTHING`,
12609
12777
  [
12610
12778
  w2.id,
@@ -12622,6 +12790,7 @@ var init_store_postgres = __esm({
12622
12790
  w2.outcomeDetail ? JSON.stringify(w2.outcomeDetail) : null,
12623
12791
  w2.sessionRef,
12624
12792
  w2.continuesWorkId,
12793
+ w2.replyToIssueId ?? null,
12625
12794
  w2.outputVersionNo,
12626
12795
  w2.nodeVersion,
12627
12796
  w2.conclusion,
@@ -12877,7 +13046,7 @@ var init_store_postgres = __esm({
12877
13046
  }
12878
13047
  async listActiveWorkorders() {
12879
13048
  const res = await this.c.query(
12880
- `SELECT id FROM ${this.s}.workorders WHERE lifecycle != 'sealed' ORDER BY created_at`
13049
+ `SELECT id FROM ${this.s}.workorders WHERE done_at IS NULL ORDER BY created_at`
12881
13050
  );
12882
13051
  return res.rows.map((r) => r.id);
12883
13052
  }
@@ -12919,41 +13088,61 @@ var init_types = __esm({
12919
13088
  });
12920
13089
 
12921
13090
  // ../engine/src/handlers/kill.ts
13091
+ function isWorkKillable(w2) {
13092
+ if (w2.status === "retry") return true;
13093
+ if (w2.status === "dead") return false;
13094
+ if (w2.retryAt && w2.deadAt) return true;
13095
+ return isWorkOpen(w2) || isWorkFailed(w2);
13096
+ }
13097
+ function isReviewKillable(r) {
13098
+ if (r.verdict) return false;
13099
+ if (r.status === "retry") return true;
13100
+ if (r.status === "dead") return false;
13101
+ if (r.retryAt && r.cancelledAt) return true;
13102
+ if (r.cancelledAt) return false;
13103
+ return true;
13104
+ }
13105
+ function killWorkRow(state, w2, at) {
13106
+ state.updateWork(w2.id, {
13107
+ deadAt: w2.deadAt ?? at,
13108
+ cancelledAt: w2.cancelledAt ?? at,
13109
+ status: "dead"
13110
+ });
13111
+ }
13112
+ function killReviewRow(state, r, at) {
13113
+ state.updateReview(r.id, { cancelledAt: r.cancelledAt ?? at, status: "dead" });
13114
+ }
12922
13115
  function killLatestWork(state, nodeId, at) {
12923
13116
  const node = state.node(nodeId);
12924
13117
  if (!node) return;
12925
13118
  const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
12926
- if (!lw || lw.deadAt) return;
12927
- if (isWorkOpen(lw)) {
12928
- state.updateWork(lw.id, { deadAt: at, cancelledAt: at, status: "dead" });
12929
- } else if (isWorkFailed(lw)) {
12930
- state.updateWork(lw.id, { deadAt: at, cancelledAt: at, status: "dead" });
12931
- }
13119
+ if (!lw) return;
13120
+ if (isWorkKillable(lw)) killWorkRow(state, lw, at);
12932
13121
  }
12933
13122
  function killNodeReviews(state, nodeId, at) {
12934
13123
  for (const r of state.allReviews()) {
12935
- if (r.nodeId === nodeId && isReviewOpen(r)) {
12936
- state.updateReview(r.id, { cancelledAt: at, status: "dead" });
13124
+ if (r.nodeId === nodeId && isReviewKillable(r)) {
13125
+ killReviewRow(state, r, at);
12937
13126
  }
12938
13127
  }
12939
13128
  }
12940
13129
  function killReview(state, reviewId, at) {
12941
13130
  const r = state.review(reviewId);
12942
- if (!r || r.cancelledAt || r.verdict) return;
12943
- state.updateReview(reviewId, { cancelledAt: at, status: "dead" });
13131
+ if (!r || !isReviewKillable(r)) return;
13132
+ killReviewRow(state, r, at);
12944
13133
  }
12945
13134
  function killAllWorks(state, at) {
12946
13135
  for (const w2 of state.allWorks()) {
12947
- if (!w2.endedAt && !w2.deadAt) {
12948
- state.updateWork(w2.id, { deadAt: at, cancelledAt: at, status: "dead" });
12949
- }
13136
+ if (!w2.endedAt && !w2.deadAt) killWorkRow(state, w2, at);
13137
+ }
13138
+ for (const n of state.liveNodes()) {
13139
+ const lw = n.latestWorkId ? state.work(n.latestWorkId) : null;
13140
+ if (lw && isWorkKillable(lw)) killWorkRow(state, lw, at);
12950
13141
  }
12951
13142
  }
12952
13143
  function killAllReviews(state, at) {
12953
13144
  for (const r of state.allReviews()) {
12954
- if (!r.endedAt && !r.cancelledAt && !r.verdict) {
12955
- state.updateReview(r.id, { cancelledAt: at, status: "dead" });
12956
- }
13145
+ if (isReviewKillable(r)) killReviewRow(state, r, at);
12957
13146
  }
12958
13147
  }
12959
13148
  var init_kill = __esm({
@@ -12963,101 +13152,6 @@ var init_kill = __esm({
12963
13152
  }
12964
13153
  });
12965
13154
 
12966
- // ../engine/src/handlers/workorder.ts
12967
- var workorderCreate, workorderMeta, workorderRoot, workorderSeal, workorderPause, workorderResume, workorderPing;
12968
- var init_workorder = __esm({
12969
- "../engine/src/handlers/workorder.ts"() {
12970
- "use strict";
12971
- init_kill();
12972
- workorderCreate = {
12973
- name: "workorder/create",
12974
- kind: "workorder.created",
12975
- apply(e, state, ctx) {
12976
- if (state.workorder.id === e.workorderId && state.workorder.createdAt) return;
12977
- state.insertWorkorder({
12978
- id: e.workorderId,
12979
- companyId: state.workorder.companyId,
12980
- projectId: e.projectId,
12981
- title: e.title,
12982
- description: e.description,
12983
- goal: e.goal,
12984
- acceptanceCriteria: e.acceptanceCriteria,
12985
- ownerActorId: e.ownerActorId || ctx.actorId || "actor:system:engine",
12986
- managerActorId: e.managerActorId,
12987
- rootNodeId: null,
12988
- lifecycle: "draft",
12989
- sealReason: null,
12990
- dispatchPaused: false,
12991
- pausedBy: null,
12992
- pausedReason: null,
12993
- resumeMarker: 0,
12994
- structuralVersion: 0,
12995
- rowVersion: 0,
12996
- createdAt: ctx.at,
12997
- createdBy: ctx.actorId,
12998
- updatedAt: ctx.at
12999
- });
13000
- }
13001
- };
13002
- workorderMeta = {
13003
- name: "workorder/meta",
13004
- kind: "workorder.meta_changed",
13005
- apply(e, state, ctx) {
13006
- state.updateWorkorder({ ...e.patch, updatedAt: ctx.at, rowVersion: state.workorder.rowVersion + 1 });
13007
- }
13008
- };
13009
- workorderRoot = {
13010
- name: "workorder/root",
13011
- kind: "workorder.root_set",
13012
- apply(e, state, ctx) {
13013
- if (!state.node(e.nodeId)) throw new Error(`root node ${e.nodeId} not in workorder ${e.workorderId}`);
13014
- state.updateWorkorder({ rootNodeId: e.nodeId, updatedAt: ctx.at });
13015
- }
13016
- };
13017
- workorderSeal = {
13018
- name: "workorder/seal",
13019
- kind: "workorder.sealed",
13020
- apply(e, state, ctx) {
13021
- if (state.workorder.lifecycle === "sealed") return;
13022
- state.updateWorkorder({ lifecycle: "sealed", sealReason: e.reason, updatedAt: ctx.at });
13023
- killAllWorks(state, ctx.at);
13024
- killAllReviews(state, ctx.at);
13025
- },
13026
- // ★ 设计 §workorder.sealed「按 work.kill 逻辑杀」:apply 只置状态,这里取消被杀会话
13027
- async effect(e, ctx) {
13028
- await ctx.cancelKilledSessions(e.workorderId);
13029
- }
13030
- };
13031
- workorderPause = {
13032
- name: "workorder/pause",
13033
- kind: "workorder.paused",
13034
- apply(e, state, ctx) {
13035
- state.updateWorkorder({ dispatchPaused: true, pausedBy: e.by, pausedReason: e.reason, updatedAt: ctx.at });
13036
- killAllWorks(state, ctx.at);
13037
- killAllReviews(state, ctx.at);
13038
- },
13039
- // ★ 设计 §workorder.paused「按 work.kill 逻辑杀」:apply 只置状态,这里取消被杀会话
13040
- async effect(e, ctx) {
13041
- await ctx.cancelKilledSessions(e.workorderId);
13042
- }
13043
- };
13044
- workorderResume = {
13045
- name: "workorder/resume",
13046
- kind: "workorder.resumed",
13047
- apply(e, state, ctx) {
13048
- const newMarker = state.workorder.resumeMarker + 1;
13049
- state.updateWorkorder({ dispatchPaused: false, pausedBy: null, pausedReason: null, resumeMarker: newMarker, updatedAt: ctx.at });
13050
- }
13051
- };
13052
- workorderPing = {
13053
- name: "workorder/ping",
13054
- kind: "workorder.ping",
13055
- apply(_e, _state, _ctx) {
13056
- }
13057
- };
13058
- }
13059
- });
13060
-
13061
13155
  // ../engine/src/handlers/plan.ts
13062
13156
  function newNode(id, workorderId, at, version2) {
13063
13157
  return {
@@ -13071,11 +13165,6 @@ function newNode(id, workorderId, at, version2) {
13071
13165
  assigneeRole: null,
13072
13166
  latestWorkId: null,
13073
13167
  latestAcceptId: null,
13074
- lastResumeMarker: 0,
13075
- retryCount: 0,
13076
- paused: false,
13077
- pausedBy: null,
13078
- pausedReason: null,
13079
13168
  isFinalOutput: false,
13080
13169
  cancelledAt: null,
13081
13170
  rowVersion: 0,
@@ -13083,6 +13172,31 @@ function newNode(id, workorderId, at, version2) {
13083
13172
  updatedAt: at
13084
13173
  };
13085
13174
  }
13175
+ function markNodeForRetry(state, nodeId, at) {
13176
+ const node = state.node(nodeId);
13177
+ if (!node) return;
13178
+ const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
13179
+ if (lw && lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "dead") {
13180
+ state.updateWork(lw.id, { retryAt: at, status: "retry" });
13181
+ }
13182
+ if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
13183
+ for (const req of state.requirementsOf(node.id)) {
13184
+ if (!req.latestReviewId) continue;
13185
+ const r = state.review(req.latestReviewId);
13186
+ if (r && r.cancelledAt && reviewState(r) === "dead") {
13187
+ state.updateReview(r.id, { retryAt: at, status: "retry" });
13188
+ }
13189
+ }
13190
+ }
13191
+ for (const issue2 of state.allIssues()) {
13192
+ if (issue2.kind !== "comment" || issue2.state !== "open" || issue2.aboutNodeId !== nodeId) continue;
13193
+ const replies = state.worksOf(nodeId).filter((w2) => w2.replyToIssueId === issue2.id).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
13194
+ const latest = replies[replies.length - 1];
13195
+ if (latest && latest.deadAt && workState(latest, hasOutput(latest, 0)) === "dead") {
13196
+ state.updateWork(latest.id, { retryAt: at, status: "retry" });
13197
+ }
13198
+ }
13199
+ }
13086
13200
  var planUpdateSpec, planAddNode, planDeleteNode, planAddEdge, planDeleteEdge, planAssignActor, planUpdateReviewRequirements, planNodeRetry, planUpdateFields, planApply;
13087
13201
  var init_plan = __esm({
13088
13202
  "../engine/src/handlers/plan.ts"() {
@@ -13184,8 +13298,17 @@ var init_plan = __esm({
13184
13298
  name: "plan/assign-actor",
13185
13299
  kind: "plan.assign_actor",
13186
13300
  apply(e, state, ctx) {
13187
- if (!state.node(e.nodeId)) return;
13188
- state.updateNode(e.nodeId, { assigneeActorId: e.assigneeActorId, assigneeRole: e.assigneeRole, updatedAt: ctx.at });
13301
+ const node = state.node(e.nodeId);
13302
+ if (!node) return;
13303
+ const changed = node.assigneeActorId !== e.assigneeActorId;
13304
+ const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
13305
+ const settled = lw !== null && lw !== void 0 && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success";
13306
+ state.updateNode(e.nodeId, {
13307
+ assigneeActorId: e.assigneeActorId,
13308
+ assigneeRole: e.assigneeRole,
13309
+ ...changed && !settled ? { version: node.version + 1 } : {},
13310
+ updatedAt: ctx.at
13311
+ });
13189
13312
  }
13190
13313
  };
13191
13314
  planUpdateReviewRequirements = {
@@ -13209,21 +13332,7 @@ var init_plan = __esm({
13209
13332
  name: "plan/node-retry",
13210
13333
  kind: "plan.node_retry",
13211
13334
  apply(e, state, ctx) {
13212
- const node = state.node(e.nodeId);
13213
- if (!node) return;
13214
- const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
13215
- if (lw && lw.deadAt && !lw.retryAt) {
13216
- state.updateWork(lw.id, { retryAt: ctx.at, status: "retry" });
13217
- }
13218
- if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
13219
- for (const req of state.requirementsOf(node.id)) {
13220
- if (!req.latestReviewId) continue;
13221
- const r = state.review(req.latestReviewId);
13222
- if (r && r.cancelledAt && !r.retryAt) {
13223
- state.updateReview(r.id, { retryAt: ctx.at, status: "retry" });
13224
- }
13225
- }
13226
- }
13335
+ markNodeForRetry(state, e.nodeId, ctx.at);
13227
13336
  }
13228
13337
  };
13229
13338
  planUpdateFields = {
@@ -13296,6 +13405,89 @@ var init_plan = __esm({
13296
13405
  }
13297
13406
  });
13298
13407
 
13408
+ // ../engine/src/handlers/workorder.ts
13409
+ var workorderCreate, workorderMeta, workorderRoot, workorderPause, workorderResume, workorderPing;
13410
+ var init_workorder = __esm({
13411
+ "../engine/src/handlers/workorder.ts"() {
13412
+ "use strict";
13413
+ init_kill();
13414
+ init_plan();
13415
+ workorderCreate = {
13416
+ name: "workorder/create",
13417
+ kind: "workorder.created",
13418
+ apply(e, state, ctx) {
13419
+ if (state.workorder.id === e.workorderId && state.workorder.createdAt) return;
13420
+ state.insertWorkorder({
13421
+ id: e.workorderId,
13422
+ companyId: state.workorder.companyId,
13423
+ projectId: e.projectId,
13424
+ title: e.title,
13425
+ description: e.description,
13426
+ goal: e.goal,
13427
+ acceptanceCriteria: e.acceptanceCriteria,
13428
+ ownerActorId: e.ownerActorId || ctx.actorId || "actor:system:engine",
13429
+ managerActorId: e.managerActorId,
13430
+ rootNodeId: null,
13431
+ lifecycle: "draft",
13432
+ dispatchPaused: false,
13433
+ pausedBy: null,
13434
+ pausedReason: null,
13435
+ doneAt: null,
13436
+ structuralVersion: 0,
13437
+ rowVersion: 0,
13438
+ createdAt: ctx.at,
13439
+ createdBy: ctx.actorId,
13440
+ updatedAt: ctx.at
13441
+ });
13442
+ }
13443
+ };
13444
+ workorderMeta = {
13445
+ name: "workorder/meta",
13446
+ kind: "workorder.meta_changed",
13447
+ apply(e, state, ctx) {
13448
+ state.updateWorkorder({ ...e.patch, updatedAt: ctx.at, rowVersion: state.workorder.rowVersion + 1 });
13449
+ }
13450
+ };
13451
+ workorderRoot = {
13452
+ name: "workorder/root",
13453
+ kind: "workorder.root_set",
13454
+ apply(e, state, ctx) {
13455
+ if (!state.node(e.nodeId)) throw new Error(`root node ${e.nodeId} not in workorder ${e.workorderId}`);
13456
+ state.updateWorkorder({ rootNodeId: e.nodeId, updatedAt: ctx.at });
13457
+ }
13458
+ };
13459
+ workorderPause = {
13460
+ name: "workorder/pause",
13461
+ kind: "workorder.paused",
13462
+ apply(e, state, ctx) {
13463
+ state.updateWorkorder({ dispatchPaused: true, pausedBy: e.by, pausedReason: e.reason, updatedAt: ctx.at });
13464
+ killAllWorks(state, ctx.at);
13465
+ killAllReviews(state, ctx.at);
13466
+ },
13467
+ // ★ 设计 §workorder.paused「按 work.kill 逻辑杀」:apply 只置状态,这里取消被杀会话
13468
+ async effect(e, ctx) {
13469
+ await ctx.cancelKilledSessions(e.workorderId);
13470
+ }
13471
+ };
13472
+ workorderResume = {
13473
+ name: "workorder/resume",
13474
+ kind: "workorder.resumed",
13475
+ apply(e, state, ctx) {
13476
+ state.updateWorkorder({ dispatchPaused: false, pausedBy: null, pausedReason: null, updatedAt: ctx.at });
13477
+ for (const node of state.liveNodes()) {
13478
+ planNodeRetry.apply({ kind: "plan.node_retry", workorderId: e.workorderId, nodeId: node.id, by: e.by }, state, ctx);
13479
+ }
13480
+ }
13481
+ };
13482
+ workorderPing = {
13483
+ name: "workorder/ping",
13484
+ kind: "workorder.ping",
13485
+ apply(_e, _state, _ctx) {
13486
+ }
13487
+ };
13488
+ }
13489
+ });
13490
+
13299
13491
  // ../engine/src/handlers/work.ts
13300
13492
  var workCreate, workKill, workResponse, workStart, workSubmitOutput, workTimeout;
13301
13493
  var init_work = __esm({
@@ -13310,15 +13502,41 @@ var init_work = __esm({
13310
13502
  if (state.work(e.workId)) return;
13311
13503
  const node = state.node(e.nodeId);
13312
13504
  if (!node) return;
13313
- const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
13314
- if (lw && !lw.deadAt) {
13315
- if (isWorkOpen(lw)) {
13316
- state.updateWork(lw.id, { deadAt: ctx.at, cancelledAt: ctx.at, status: "dead" });
13317
- } else if (isWorkFailed(lw)) {
13318
- state.updateWork(lw.id, { deadAt: ctx.at, cancelledAt: ctx.at, status: "dead" });
13319
- }
13505
+ if (e.replyToIssueId) {
13506
+ if (state.worksOf(e.nodeId).some((w2) => !w2.endedAt && !w2.deadAt)) return;
13507
+ state.insertWork({
13508
+ id: e.workId,
13509
+ workorderId: e.workorderId,
13510
+ nodeId: e.nodeId,
13511
+ assigneeActorId: node.assigneeActorId,
13512
+ createdAt: ctx.at,
13513
+ startedAt: null,
13514
+ endedAt: null,
13515
+ deadAt: null,
13516
+ cancelledAt: null,
13517
+ status: "running",
13518
+ outcome: null,
13519
+ outcomeDetail: null,
13520
+ sessionRef: null,
13521
+ continuesWorkId: e.continuesWorkId,
13522
+ replyToIssueId: e.replyToIssueId,
13523
+ outputVersionNo: null,
13524
+ conclusion: null,
13525
+ acceptanceState: null,
13526
+ acceptedAt: null,
13527
+ acceptedBy: null,
13528
+ rejectedReason: null,
13529
+ overrideBy: null,
13530
+ overrideReason: null,
13531
+ nodeVersion: node.version
13532
+ });
13533
+ return;
13320
13534
  }
13535
+ killLatestWork(state, e.nodeId, ctx.at);
13321
13536
  killNodeReviews(state, e.nodeId, ctx.at);
13537
+ for (const w2 of state.worksOf(e.nodeId)) {
13538
+ if (w2.replyToIssueId && !w2.endedAt && !w2.deadAt) killWorkRow(state, w2, ctx.at);
13539
+ }
13322
13540
  state.insertWork({
13323
13541
  id: e.workId,
13324
13542
  workorderId: e.workorderId,
@@ -13344,10 +13562,7 @@ var init_work = __esm({
13344
13562
  overrideReason: null,
13345
13563
  nodeVersion: node.version
13346
13564
  });
13347
- state.updateNode(e.nodeId, {
13348
- latestWorkId: e.workId,
13349
- lastResumeMarker: state.workorder.resumeMarker
13350
- });
13565
+ state.updateNode(e.nodeId, { latestWorkId: e.workId });
13351
13566
  for (const req of state.requirementsOf(e.nodeId)) {
13352
13567
  if (req.latestReviewId) {
13353
13568
  state.replaceRequirements(e.nodeId, state.requirementsOf(e.nodeId).map((r) => ({
@@ -13368,8 +13583,9 @@ var init_work = __esm({
13368
13583
  },
13369
13584
  // ★ 设计 §work.create step 1「kill 逻辑与 work.kill 完全一致」——apply 置旧 work deadAt 后,
13370
13585
  // 会话侧也要杀(旧 agent 可能还挂着)。cancelPreviousWork 找不到/会话已退则 no-op。
13586
+ // 回信 work 不杀前会话(apply 不 kill 主链,节点已完结、无在跑会话),只派发。
13371
13587
  async effect(e, ctx) {
13372
- await ctx.cancelPreviousWork(e.nodeId, e.workId);
13588
+ if (!e.replyToIssueId) await ctx.cancelPreviousWork(e.nodeId, e.workId);
13373
13589
  await ctx.dispatchWork(e.workId);
13374
13590
  }
13375
13591
  };
@@ -13381,14 +13597,12 @@ var init_work = __esm({
13381
13597
  if (!node) return;
13382
13598
  const lw = node.latestWorkId ? state.work(node.latestWorkId) : null;
13383
13599
  const target = e.workId ? state.work(e.workId) : null;
13384
- const targetKillable = target !== null && target !== void 0 && (isWorkOpen(target) || isWorkFailed(target) || target.endedAt !== null && target.endedAt !== void 0 && state.node(target.nodeId)?.latestAcceptId !== target.id);
13385
- if (lw && !lw.deadAt) {
13386
- if (isWorkOpen(lw)) {
13387
- state.updateWork(lw.id, { deadAt: ctx.at, cancelledAt: ctx.at, status: "dead" });
13388
- } else if (isWorkFailed(lw)) {
13389
- state.updateWork(lw.id, { deadAt: ctx.at, cancelledAt: ctx.at, status: "dead" });
13390
- } else if (node.latestAcceptId !== lw.id) {
13391
- state.updateWork(lw.id, { deadAt: ctx.at, cancelledAt: ctx.at, status: "dead" });
13600
+ const targetKillable = target !== null && target !== void 0 && (isWorkKillable(target) || target.endedAt !== null && target.endedAt !== void 0 && !target.deadAt && state.node(target.nodeId)?.latestAcceptId !== target.id);
13601
+ if (lw) {
13602
+ if (isWorkKillable(lw)) {
13603
+ killWorkRow(state, lw, ctx.at);
13604
+ } else if (!lw.deadAt && lw.endedAt && node.latestAcceptId !== lw.id) {
13605
+ killWorkRow(state, lw, ctx.at);
13392
13606
  }
13393
13607
  }
13394
13608
  if (targetKillable) {
@@ -13410,6 +13624,19 @@ var init_work = __esm({
13410
13624
  apply(e, state, ctx) {
13411
13625
  const w2 = state.work(e.workId);
13412
13626
  if (!w2 || w2.endedAt || w2.deadAt) return;
13627
+ if (w2.replyToIssueId) {
13628
+ const myReplies = state.repliesOf(w2.replyToIssueId).filter((r) => r.authorActorId === w2.assigneeActorId && r.createdAt >= w2.createdAt).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
13629
+ const replied = myReplies.length > 0;
13630
+ const ok = e.outcome === "completed" && (replied || hasOutput(w2, 0));
13631
+ state.updateWork(e.workId, {
13632
+ endedAt: ctx.at,
13633
+ outcome: e.outcome,
13634
+ outcomeDetail: e.detail,
13635
+ status: ok ? "success" : "failed",
13636
+ ...ok && !w2.conclusion && replied ? { conclusion: myReplies[myReplies.length - 1].body } : {}
13637
+ });
13638
+ return;
13639
+ }
13413
13640
  const status = e.outcome === "completed" && hasOutput(w2, state.artifactsOf(e.workId).length) ? "success" : "failed";
13414
13641
  state.updateWork(e.workId, { endedAt: ctx.at, outcome: e.outcome, outcomeDetail: e.detail, status });
13415
13642
  }
@@ -13432,6 +13659,9 @@ var init_work = __esm({
13432
13659
  apply(e, state, ctx) {
13433
13660
  const w2 = state.work(e.workId);
13434
13661
  if (!w2 || w2.deadAt) return;
13662
+ if (e.artifacts.length === 0 && w2.acceptanceState === "accepted" && state.artifactsOf(e.workId).length > 0) {
13663
+ return;
13664
+ }
13435
13665
  state.updateWork(e.workId, { conclusion: e.conclusion });
13436
13666
  state.replaceArtifacts(e.workId, e.artifacts.map((a) => ({
13437
13667
  id: a.id,
@@ -13476,8 +13706,7 @@ var issueCreate, issueReply, issueResolve, issueProposeResolution, issueRejectRe
13476
13706
  var init_issue = __esm({
13477
13707
  "../engine/src/handlers/issue.ts"() {
13478
13708
  "use strict";
13479
- init_views2();
13480
- init_kill();
13709
+ init_plan();
13481
13710
  issueCreate = {
13482
13711
  name: "issue/create",
13483
13712
  kind: "issue.create",
@@ -13507,15 +13736,6 @@ var init_issue = __esm({
13507
13736
  });
13508
13737
  for (const actorId of e.recipients) state.addRecipient({ issueId: e.issueId, actorId });
13509
13738
  for (const a of e.attachments) state.addAttachment({ issueId: e.issueId, ...a });
13510
- const targetNodeId = e.aboutNodeId ?? (e.aboutWorkId ? state.work(e.aboutWorkId)?.nodeId : null);
13511
- if (blocking && targetNodeId) {
13512
- const node = state.node(targetNodeId);
13513
- const lw = node?.latestWorkId ? state.work(node.latestWorkId) : null;
13514
- if (lw && (isWorkOpen(lw) || isWorkFailed(lw))) {
13515
- killLatestWork(state, targetNodeId, ctx.at);
13516
- killNodeReviews(state, targetNodeId, ctx.at);
13517
- }
13518
- }
13519
13739
  },
13520
13740
  async effect(e, ctx) {
13521
13741
  if (e.recipients.length) {
@@ -13571,6 +13791,10 @@ var init_issue = __esm({
13571
13791
  resolvedNote: e.note,
13572
13792
  resolvedViaWorkId: e.viaWorkId
13573
13793
  });
13794
+ if (issue2.kind === "gap" || issue2.kind === "escalation") {
13795
+ const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
13796
+ if (nodeId) markNodeForRetry(state, nodeId, ctx.at);
13797
+ }
13574
13798
  }
13575
13799
  };
13576
13800
  issueProposeResolution = {
@@ -13613,12 +13837,7 @@ var init_review = __esm({
13613
13837
  if (dup) return;
13614
13838
  const reqs = state.requirementsOf(e.nodeId);
13615
13839
  const lrId = reqs.find((r) => r.reviewGroup === e.reviewGroup)?.latestReviewId;
13616
- if (lrId) {
13617
- const lr = state.review(lrId);
13618
- if (lr && !lr.verdict && !lr.cancelledAt && (isReviewOpen(lr) || lr.endedAt)) {
13619
- state.updateReview(lrId, { cancelledAt: ctx.at });
13620
- }
13621
- }
13840
+ if (lrId) killReview(state, lrId, ctx.at);
13622
13841
  state.insertReview({
13623
13842
  id: e.reviewId,
13624
13843
  workorderId: e.workorderId,
@@ -13638,6 +13857,11 @@ var init_review = __esm({
13638
13857
  decidedAt: null
13639
13858
  });
13640
13859
  state.replaceRequirements(e.nodeId, reqs.map((r) => r.reviewGroup === e.reviewGroup ? { ...r, latestReviewId: e.reviewId } : r));
13860
+ const node = state.node(e.nodeId);
13861
+ if (node && node.latestAcceptId === e.targetWorkId) {
13862
+ state.updateNode(e.nodeId, { latestAcceptId: null, updatedAt: ctx.at });
13863
+ state.updateWork(e.targetWorkId, { acceptanceState: null, acceptedAt: null, acceptedBy: null });
13864
+ }
13641
13865
  },
13642
13866
  async effect(e, ctx) {
13643
13867
  if (e.reviewerIsAgent) await ctx.dispatchReview(e.reviewId, e.nodeId, e.reviewerActorId);
@@ -13739,6 +13963,48 @@ var init_review = __esm({
13739
13963
  if (!w2 || w2.deadAt || w2.acceptanceState === "accepted") return;
13740
13964
  const node = state.node(w2.nodeId);
13741
13965
  if (!node) return;
13966
+ if (w2.replyToIssueId) {
13967
+ const issue2 = state.issue(w2.replyToIssueId);
13968
+ state.updateWork(e.workId, {
13969
+ acceptanceState: "accepted",
13970
+ acceptedAt: ctx.at,
13971
+ acceptedBy: e.by,
13972
+ overrideBy: e.override?.by ?? null,
13973
+ overrideReason: e.override?.reason ?? null,
13974
+ ...w2.endedAt ? {} : { endedAt: ctx.at, outcome: "completed" }
13975
+ });
13976
+ if (issue2) {
13977
+ const already = state.repliesOf(issue2.id).some(
13978
+ (r) => r.authorActorId === w2.assigneeActorId && r.createdAt >= w2.createdAt
13979
+ );
13980
+ if (!already && w2.conclusion) {
13981
+ const replyId = deriveReplyId(issue2.id, e.workId);
13982
+ if (!state.repliesOf(issue2.id).some((r) => r.id === replyId)) {
13983
+ state.addReply({
13984
+ id: replyId,
13985
+ issueId: issue2.id,
13986
+ authorActorId: w2.assigneeActorId,
13987
+ handActorId: null,
13988
+ body: w2.conclusion,
13989
+ createdAt: ctx.at,
13990
+ mentions: null
13991
+ });
13992
+ }
13993
+ }
13994
+ if (issue2.state === "open") {
13995
+ issueResolve.apply({
13996
+ kind: "issue.resolve",
13997
+ workorderId: state.workorder.id,
13998
+ issueId: issue2.id,
13999
+ state: "resolved",
14000
+ by: e.by,
14001
+ note: null,
14002
+ viaWorkId: e.workId
14003
+ }, state, ctx);
14004
+ }
14005
+ }
14006
+ return;
14007
+ }
13742
14008
  state.updateNode(w2.nodeId, { latestAcceptId: e.workId, updatedAt: ctx.at });
13743
14009
  state.updateWork(e.workId, {
13744
14010
  acceptanceState: "accepted",
@@ -13755,7 +14021,7 @@ var init_review = __esm({
13755
14021
  state.updateWork(e.workId, { outputVersionNo: Math.max(0, ...versions) + 1 });
13756
14022
  }
13757
14023
  for (const r of state.openReviewsOf(e.workId)) {
13758
- state.updateReview(r.id, { cancelledAt: ctx.at });
14024
+ state.updateReview(r.id, { cancelledAt: ctx.at, status: "dead" });
13759
14025
  }
13760
14026
  const inputs = state.workInputsOf(e.workId);
13761
14027
  for (const edge of state.edgesInto(w2.nodeId)) {
@@ -13800,11 +14066,10 @@ var init_handlers = __esm({
13800
14066
  init_issue();
13801
14067
  init_types();
13802
14068
  HANDLERS = [
13803
- /* ── 工作区级(5)── */
14069
+ /* ── 工作区级(4)── */
13804
14070
  erase(workorderCreate),
13805
14071
  erase(workorderMeta),
13806
14072
  erase(workorderRoot),
13807
- erase(workorderSeal),
13808
14073
  erase(workorderPause),
13809
14074
  erase(workorderResume),
13810
14075
  erase(workorderPing),
@@ -13859,11 +14124,10 @@ function emptySnapshotPlaceholder(record8) {
13859
14124
  managerActorId: null,
13860
14125
  rootNodeId: null,
13861
14126
  lifecycle: "draft",
13862
- sealReason: null,
13863
14127
  dispatchPaused: false,
13864
14128
  pausedBy: null,
13865
14129
  pausedReason: null,
13866
- resumeMarker: 0,
14130
+ doneAt: null,
13867
14131
  structuralVersion: 0,
13868
14132
  rowVersion: 0,
13869
14133
  createdAt: "",
@@ -13892,7 +14156,6 @@ var init_bus = __esm({
13892
14156
  init_model();
13893
14157
  init_handlers();
13894
14158
  init_state();
13895
- init_views2();
13896
14159
  SYSTEM_ACTOR2 = "actor:system:engine";
13897
14160
  NOOP_IO = {
13898
14161
  async dispatchWork() {
@@ -14003,13 +14266,13 @@ var init_bus = __esm({
14003
14266
  * batch = event_queue.drain() ← 一次取空全部 pending
14004
14267
  * touched = set()
14005
14268
  * for e in batch:
14006
- * unmark_done(e.workorder_id) ← 事件到达 → 唤醒工单(sealed→active)
14269
+ * unmark_done(e.workorder_id) ← 事件到达 → 唤醒工单(清 done_at,重新纳入扫描集合)
14007
14270
  * touched.add(e.workorder_id)
14008
14271
  * handle(e) ← apply + effect
14009
14272
  * for wo in touched:
14010
14273
  * terminated = scan(wo) ← 全图遍历,emit 级联事件
14011
14274
  * if terminated:
14012
- * mark_done(wo) ← 自动封单
14275
+ * mark_done(wo) ← 调度器记账(done_at),不是封存——sealed 仅人显式触发
14013
14276
  * ```
14014
14277
  *
14015
14278
  * scan **只在循环内调用**——外部需要触发 scan 时发 workorder.ping 事件。
@@ -14054,8 +14317,8 @@ var init_bus = __esm({
14054
14317
  try {
14055
14318
  const result = await this.scan(woId);
14056
14319
  if (result.terminated) {
14057
- await this.markDone(woId);
14058
- console.log(`[bus] \u2705 \u5DE5\u5355 ${woId} \u5DF2\u5B8C\u7ED3\uFF0C\u81EA\u52A8\u5C01\u5355`);
14320
+ const transitioned = await this.markDone(woId);
14321
+ if (transitioned) console.log(`[bus] \u2705 \u5DE5\u5355 ${woId} \u5DF2\u5B8C\u7ED3\uFF08mark done\uFF0C\u7B49\u5F85\u65B0\u4E8B\u4EF6\u5524\u9192\uFF1B\u4E0D\u5C01\u5B58\uFF09`);
14059
14322
  }
14060
14323
  } catch (err) {
14061
14324
  console.error(`[bus] scan ${woId}:`, err);
@@ -14084,28 +14347,34 @@ var init_bus = __esm({
14084
14347
  }
14085
14348
  /* ═══════════════════════ mark_done / unmark_done ═══════════════════════ */
14086
14349
  /**
14087
- * 调度器基础设施:标记工单为"已完结"——直接设 lifecycle='sealed'。
14088
- * ★ 不是业务事件,不走 handler——这是调度器管理自己的扫描集合。
14350
+ * 调度器基础设施:标记工单为"已完结"——写 done_at(调度器记账字段),把工单移出空闲全扫集合。
14351
+ * ★ 不是业务事件,不走 handler。done_at 可逆:任何新事件经 unmarkDone 唤醒(工单级封存已移除,
14352
+ * ADR 0137——曾经的实现把这里写成 lifecycle=sealed:converged,「跑完即封存」是设计外行为)。
14353
+ * @returns 是否发生了状态迁移(已 done 的工单重复 markDone 返回 false,调用方据此避免重复刷日志)。
14089
14354
  */
14090
14355
  async markDone(workorderId) {
14091
- await this.store.transaction(async (tx) => {
14356
+ return this.store.transaction(async (tx) => {
14357
+ const snap = await tx.loadWorkorder(workorderId);
14358
+ if (!snap || snap.workorder.doneAt) return false;
14092
14359
  await tx.applyMutations(workorderId, [{
14093
14360
  t: "workorder.update",
14094
- patch: { lifecycle: "sealed", sealReason: "converged", updatedAt: this.clock.now() }
14361
+ patch: { doneAt: this.clock.now() }
14095
14362
  }]);
14363
+ return true;
14096
14364
  });
14097
14365
  }
14098
14366
  /**
14099
- * 调度器基础设施:事件到达 → 唤醒工单。如果工单处于 sealed 状态,设回 active。
14367
+ * 调度器基础设施:事件到达 → 唤醒工单(清 done_at,重新纳入扫描集合)。
14100
14368
  * ★ 不是业务事件——这是调度器在循环内的统一入口,保证任何事件都能触发 scan。
14369
+ * 不碰 lifecycle(draft 的建图闸由 scan 门自己判)。
14101
14370
  */
14102
14371
  async unmarkDone(workorderId) {
14103
14372
  await this.store.transaction(async (tx) => {
14104
14373
  const snap = await tx.loadWorkorder(workorderId);
14105
- if (!snap || snap.workorder.lifecycle !== "sealed") return;
14374
+ if (!snap || !snap.workorder.doneAt) return;
14106
14375
  await tx.applyMutations(workorderId, [{
14107
14376
  t: "workorder.update",
14108
- patch: { lifecycle: "active", sealReason: null, updatedAt: this.clock.now() }
14377
+ patch: { doneAt: null }
14109
14378
  }]);
14110
14379
  });
14111
14380
  }
@@ -14332,10 +14601,10 @@ var init_bus = __esm({
14332
14601
  const record8 = await tx.claimNextEvent(this.clock.now());
14333
14602
  if (!record8) return null;
14334
14603
  const snapshot = await tx.loadWorkorder(record8.workorderId);
14335
- if (snapshot?.workorder.lifecycle === "sealed") {
14604
+ if (snapshot?.workorder.doneAt) {
14336
14605
  await tx.applyMutations(record8.workorderId, [{
14337
14606
  t: "workorder.update",
14338
- patch: { lifecycle: "active", sealReason: null, updatedAt: this.clock.now() }
14607
+ patch: { doneAt: null }
14339
14608
  }]);
14340
14609
  }
14341
14610
  const state = new WorkorderState(snapshot ?? emptySnapshotPlaceholder(record8));
@@ -14522,8 +14791,8 @@ function workToRevision(w2, workInputs, artifactFiles) {
14522
14791
  contentRef = files[0].contentRef;
14523
14792
  contentKind = files[0].contentKind === "manifest" ? "manifest" : files[0].contentKind === "external-pin" ? "external-pin" : "inline-blob";
14524
14793
  } else {
14525
- contentRef = w2.conclusion ?? `\u8FD0\u884C\u4E2D\uFF08${w2.id}\uFF09`;
14526
- contentKind = "external-pin";
14794
+ contentRef = "";
14795
+ contentKind = "empty";
14527
14796
  }
14528
14797
  const revision = {
14529
14798
  id: w2.id,
@@ -14545,6 +14814,7 @@ function workToRevision(w2, workInputs, artifactFiles) {
14545
14814
  resolves: void 0
14546
14815
  };
14547
14816
  revision.rejectedReason = w2.rejectedReason;
14817
+ revision.replyToIssueId = w2.replyToIssueId ?? null;
14548
14818
  return revision;
14549
14819
  }
14550
14820
  function issueToAnnotation(issue2, replies, recipients) {
@@ -14758,12 +15028,7 @@ function bridgeLoadReadModel(input) {
14758
15028
  const seals = /* @__PURE__ */ new Map();
14759
15029
  for (const snap of snapshots2) {
14760
15030
  for (const node of snap.nodes) {
14761
- if (snap.workorder.lifecycle === "sealed") {
14762
- seals.set(node.id, {
14763
- reason: snap.workorder.sealReason ?? "cancelled",
14764
- at: snap.workorder.updatedAt
14765
- });
14766
- } else if (node.cancelledAt) {
15031
+ if (node.cancelledAt) {
14767
15032
  seals.set(node.id, { reason: "cancelled", at: node.cancelledAt });
14768
15033
  }
14769
15034
  }
@@ -15095,13 +15360,6 @@ function buildConstituentsRaw(model, plan, schema, roleIndex) {
15095
15360
  target: op.artifactId,
15096
15361
  payload: { reason: op.reason, ...op.note !== void 0 ? { note: op.note } : {} }
15097
15362
  };
15098
- case "reopen":
15099
- return {
15100
- artifactId: op.artifactId,
15101
- kind: "reopen",
15102
- target: op.artifactId,
15103
- payload: op.note !== void 0 ? { note: op.note } : {}
15104
- };
15105
15363
  case "link":
15106
15364
  return {
15107
15365
  artifactId: op.artifactId,
@@ -15690,7 +15948,7 @@ var init_kernel_bridge = __esm({
15690
15948
  await this.refreshTracked();
15691
15949
  const wid = this.wo(args.artifactId);
15692
15950
  let workId;
15693
- const openWorks = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId && r.state === "working").sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
15951
+ const openWorks = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId && r.state === "working" && !r.replyToIssueId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
15694
15952
  workId = openWorks[0]?.id;
15695
15953
  const artifact = this.model.artifacts.get(args.artifactId);
15696
15954
  if (!workId) {
@@ -15846,8 +16104,10 @@ var init_kernel_bridge = __esm({
15846
16104
  }
15847
16105
  }
15848
16106
  if (workId && args.note) {
15849
- const rev = this.model.revisions.get(workId);
15850
- if (rev && !rev.conclusion) {
16107
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16108
+ const w2 = snap?.works.find((x2) => x2.id === workId);
16109
+ const alreadyProduced = !w2 || w2.acceptanceState === "accepted" || (snap?.artifacts.some((a) => a.workId === workId) ?? false);
16110
+ if (w2 && !alreadyProduced) {
15851
16111
  await this.commit({
15852
16112
  companyId: "",
15853
16113
  workorderId: wid,
@@ -16242,18 +16502,106 @@ var init_kernel_bridge = __esm({
16242
16502
  }
16243
16503
  async seal(args) {
16244
16504
  const wid = this.wo(args.artifactId);
16505
+ await this.commit({
16506
+ companyId: "",
16507
+ workorderId: wid,
16508
+ actorId: args.actor,
16509
+ event: { kind: "plan.delete_node", workorderId: wid, nodeId: args.artifactId }
16510
+ });
16511
+ }
16512
+ /**
16513
+ * 强制重新运行节点(UI「重新运行」按钮,ADR 0137):直发 work.create——handler 自带
16514
+ * kill 旧 latest_work(running/failed/retry → dead)、清空各组评审指针、派发新会话;
16515
+ * 已完成节点重跑 = 产出新一版,新版被 accept 后下游经 pinnedWorkId 变更进入返工波。
16516
+ * 前置:节点在册且已指派执行人(work.create 落库 assigneeActorId 非空)。
16517
+ */
16518
+ async rerunNode(args) {
16519
+ const wid = this.wo(args.artifactId);
16520
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16521
+ const node = snap?.nodes.find((n) => n.id === args.artifactId);
16522
+ if (!node || node.cancelledAt) throw new KernelError(`\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u4F5C\u5E9F\uFF1A${args.artifactId}`, "not-found");
16523
+ if (!node.assigneeActorId) throw new KernelError("\u8282\u70B9\u672A\u6307\u6D3E\u6267\u884C\u4EBA\u2014\u2014\u5148\u6307\u6D3E\u518D\u91CD\u65B0\u8FD0\u884C", "no-assignee");
16524
+ const count2 = snap.works.filter((w2) => w2.nodeId === args.artifactId).length;
16245
16525
  await this.commit({
16246
16526
  companyId: "",
16247
16527
  workorderId: wid,
16248
16528
  actorId: args.actor,
16249
16529
  event: {
16250
- kind: "workorder.sealed",
16530
+ kind: "work.create",
16251
16531
  workorderId: wid,
16252
- reason: args.reason,
16253
- note: args.note ?? null
16532
+ nodeId: args.artifactId,
16533
+ workId: deriveWorkId(args.artifactId, count2 + 1),
16534
+ reasons: [{ kind: "forced", actorId: args.actor }],
16535
+ continuesWorkId: null
16254
16536
  }
16255
16537
  });
16256
16538
  }
16539
+ /** 停止评审(UI「停止评审」按钮):对节点各组未表态的 latest_review 发 review.kill(→ dead,杀会话)。 */
16540
+ async killNodeReviews(args) {
16541
+ const wid = this.wo(args.artifactId);
16542
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16543
+ const reqs = snap?.requirements.filter((r) => r.nodeId === args.artifactId) ?? [];
16544
+ for (const req of reqs) {
16545
+ if (!req.latestReviewId) continue;
16546
+ const r = snap.reviews.find((x2) => x2.id === req.latestReviewId);
16547
+ if (!r || r.verdict || r.status === "dead" || r.cancelledAt && r.status !== "retry") continue;
16548
+ await this.commit({
16549
+ companyId: "",
16550
+ workorderId: wid,
16551
+ actorId: args.actor,
16552
+ event: { kind: "review.kill", workorderId: wid, reviewId: r.id, reason: args.reason ?? "\u624B\u52A8\u505C\u6B62\u8BC4\u5BA1" }
16553
+ });
16554
+ }
16555
+ }
16556
+ /**
16557
+ * 重新评审(UI「重新评审」按钮,ADR 0137):对 success 的 latest_work 逐组直发 review.create。
16558
+ * 在跑的 latest_review 先 review.kill(create 的幂等守卫会跳过同 reviewer+group 的 open review,
16559
+ * 同工单事件严格有序保证 kill 先落)。已收口节点:review.create handler 会撤销收口
16560
+ *(清 latestAcceptId + 复位 acceptance 字段)→ 节点回「评审中」,scan 恢复评审判定。
16561
+ */
16562
+ async rereviewNode(args) {
16563
+ const wid = this.wo(args.artifactId);
16564
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16565
+ const node = snap?.nodes.find((n) => n.id === args.artifactId);
16566
+ if (!node || node.cancelledAt) throw new KernelError(`\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u4F5C\u5E9F\uFF1A${args.artifactId}`, "not-found");
16567
+ const lw = node.latestWorkId ? snap.works.find((w2) => w2.id === node.latestWorkId) : null;
16568
+ const lwState = lw ? workState(lw, hasOutput(lw, snap.artifacts.filter((a) => a.workId === lw.id).length)) : null;
16569
+ if (!lw || lwState !== "success") {
16570
+ throw new KernelError("\u8282\u70B9\u5C1A\u65E0\u6210\u529F\u4EA7\u51FA\uFF08latest_work \u975E success\uFF09\u2014\u2014\u8BC4\u5BA1\u4E2D/\u5DF2\u5B8C\u6210\u72B6\u6001\u624D\u80FD\u91CD\u65B0\u8BC4\u5BA1", "not-reviewable");
16571
+ }
16572
+ const reqs = snap.requirements.filter((r) => r.nodeId === args.artifactId);
16573
+ if (reqs.length === 0) throw new KernelError("\u8BE5\u8282\u70B9\u672A\u914D\u7F6E\u8BC4\u5BA1\u7EC4\u2014\u2014\u65E0\u8BC4\u5BA1\u53EF\u91CD\u5F00", "no-reviewers");
16574
+ for (const req of reqs) {
16575
+ if (req.latestReviewId) {
16576
+ const r = snap.reviews.find((x2) => x2.id === req.latestReviewId);
16577
+ if (r && !r.verdict && !r.cancelledAt && !r.endedAt) {
16578
+ await this.commit({
16579
+ companyId: "",
16580
+ workorderId: wid,
16581
+ actorId: args.actor,
16582
+ event: { kind: "review.kill", workorderId: wid, reviewId: r.id, reason: "\u91CD\u65B0\u8BC4\u5BA1" }
16583
+ });
16584
+ }
16585
+ }
16586
+ const existing = snap.reviews.filter((x2) => x2.targetWorkId === lw.id && x2.reviewerActorId === req.reviewerActorId && x2.reviewGroup === req.reviewGroup);
16587
+ const reviewId = existing.length === 0 ? deriveReviewId(lw.id, req.reviewerActorId, req.reviewGroup) : `rv:${lw.id}:${req.reviewerActorId}:${req.reviewGroup}:${existing.length + 1}`;
16588
+ await this.commit({
16589
+ companyId: "",
16590
+ workorderId: wid,
16591
+ actorId: args.actor,
16592
+ event: {
16593
+ kind: "review.create",
16594
+ workorderId: wid,
16595
+ nodeId: args.artifactId,
16596
+ reviewId,
16597
+ targetWorkId: lw.id,
16598
+ reviewerActorId: req.reviewerActorId,
16599
+ reviewGroup: req.reviewGroup,
16600
+ reviewerIsAgent: req.reviewerActorId.startsWith("actor:agent:")
16601
+ }
16602
+ });
16603
+ }
16604
+ }
16257
16605
  /** 工单级暂停:workorder.paused handler 杀全部在途会话 + 挡新派发。 */
16258
16606
  async pauseWorkorder(workorderId, actor) {
16259
16607
  this.trackedWorkorders.add(workorderId);
@@ -16378,6 +16726,18 @@ var init_kernel_bridge = __esm({
16378
16726
  actor: args.actor,
16379
16727
  reason: typeof card.commandArgs?.["reason"] === "string" ? card.commandArgs["reason"] : "\u7BA1\u7406\u8005\u8BF7\u6C42\u3001\u4EBA\u5DF2\u6388\u6743"
16380
16728
  });
16729
+ } else if (card.command === "resolveEscalation") {
16730
+ const escalationId = typeof card.commandArgs?.["escalationId"] === "string" ? card.commandArgs["escalationId"] : void 0;
16731
+ if (!escalationId) {
16732
+ throw new Error(`resolveEscalation \u6388\u6743\u5361\u7F3A escalationId\uFF1A${args.draftId}`);
16733
+ }
16734
+ const reason = typeof card.commandArgs?.["reason"] === "string" ? card.commandArgs["reason"] : void 0;
16735
+ await this.resolveEscalation({
16736
+ artifactId,
16737
+ actor: args.actor,
16738
+ escalationId,
16739
+ ...reason !== void 0 ? { reason } : {}
16740
+ });
16381
16741
  } else {
16382
16742
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u4E0D\u652F\u6301\u6267\u884C ${card.command}`);
16383
16743
  }
@@ -16526,6 +16886,20 @@ var init_kernel_bridge = __esm({
16526
16886
  applied++;
16527
16887
  break;
16528
16888
  }
16889
+ case "resolveEscalation": {
16890
+ const escOp = op;
16891
+ if (!escOp.escalationId) {
16892
+ throw new Error(`resolveEscalation op \u7F3A escalationId\uFF08${escOp.artifactId}\uFF09\u2014\u2014\u4E0D\u652F\u6301\u6574\u8282\u70B9\u5168\u6E05\uFF0C\u8BF7\u6307\u660E\u5173\u54EA\u4E00\u6761`);
16893
+ }
16894
+ await this.resolveEscalation({
16895
+ artifactId: escOp.artifactId,
16896
+ actor: _actor,
16897
+ escalationId: escOp.escalationId,
16898
+ ...escOp.reason !== void 0 ? { reason: escOp.reason } : {}
16899
+ });
16900
+ applied++;
16901
+ break;
16902
+ }
16529
16903
  default:
16530
16904
  break;
16531
16905
  }
@@ -16663,6 +17037,14 @@ var init_command_policy = __esm({
16663
17037
  idempotent: true,
16664
17038
  effect: (a) => `\u628A\u8282\u70B9 ${shortId(a.artifactId)} \u8F6C\u4E3A\u6301\u4E45\uFF08temp \u2192 persistent\uFF09`
16665
17039
  },
17040
+ resolveEscalation: {
17041
+ risk: "authorize",
17042
+ idempotent: true,
17043
+ // 重复关同一条是幂等的(kernel.resolveEscalation 内部按 escalationId 去重)
17044
+ // 「关错=静默停摆」的唯一防线是人在卡上一眼看出「关的是哪一条」——escalationId 与 reason 摘要必须上卡。
17045
+ // a 里 escalationId/artifactId 由 server.ts 起草时冻进 commandArgs(已四级解析),保证是真值、非 `?`。
17046
+ effect: (a) => `\u5173\u95ED\u4E0A\u62A5 ${shortId(a.escalationId)}\uFF08\u8282\u70B9 ${shortId(a.artifactId)}${typeof a.reason === "string" ? `\uFF0C\u7406\u7531\uFF1A${a.reason.slice(0, 60)}` : ""}\uFF09`
17047
+ },
16666
17048
  gc: {
16667
17049
  risk: "forbidden",
16668
17050
  forbiddenReason: "gc \u662F\u56DE\u6536\u5B64\u513F blob \u7684\u57FA\u7840\u8BBE\u65BD\u7EF4\u62A4\uFF0C\u7531 serve \u8FDB\u7A0B/\u4EBA\u7C7B operator \u6267\u884C\uFF0C\u4E0D\u5C5E\u4E8E\u7BA1\u7406\u8005\u80FD\u529B\u8303\u56F4\u3002"
@@ -129259,11 +129641,15 @@ function verifyOneSha(repos, sha2, opts) {
129259
129641
  }
129260
129642
  const reachable = repos.filter((r) => r.remote);
129261
129643
  if (reachable.length === 0) return { ok: true };
129644
+ const attempts = [];
129262
129645
  for (const r of reachable) {
129263
129646
  try {
129264
129647
  const ls = (0, import_node_child_process2.execFileSync)("git", ["ls-remote", r.remote], { encoding: "utf8", ...opts?.env ? { env: opts.env } : {} });
129265
129648
  if (ls.split("\n").some((l) => l.split(" ")[0]?.toLowerCase() === sha2.toLowerCase())) return { ok: true };
129266
- } catch {
129649
+ attempts.push({ repo: r, kind: "not-found" });
129650
+ } catch (e) {
129651
+ const err = e;
129652
+ attempts.push({ repo: r, kind: "access-error", err: String(err.stderr ?? err.message ?? e).trim() });
129267
129653
  }
129268
129654
  }
129269
129655
  if (opts?.mirrorDirOf) {
@@ -129278,6 +129664,15 @@ function verifyOneSha(repos, sha2, opts) {
129278
129664
  }
129279
129665
  }
129280
129666
  }
129667
+ if (attempts.length > 0 && attempts.every((a) => a.kind === "access-error")) {
129668
+ const lines = attempts.map((a) => ` - \u4ED3\u5E93 \`${a.repo.id}\`\uFF08${a.repo.remote}\uFF09\uFF1A${(a.err || "\u672A\u77E5\u9519\u8BEF").split("\n").slice(0, 3).join(" ")}`).join("\n");
129669
+ return {
129670
+ ok: false,
129671
+ reason: `\u65E0\u6CD5\u8BBF\u95EE\u7528\u4E8E\u6838\u5BF9\u8BE5 sha \u7684 git \u4ED3\u5E93\u2014\u2014**\u8FD9\u662F\u670D\u52A1\u7AEF\u73AF\u5883\u95EE\u9898\uFF08\u51ED\u636E / \u7F51\u7EDC / \u6743\u9650\uFF09\uFF0C\u4E0D\u662F sha \u7684\u95EE\u9898**\uFF1A
129672
+ ${lines}
129673
+ \u5904\u7406\u65B9\u5F0F\uFF1A\u5148\u8BA9\u8FD0\u7EF4\u6392\u67E5\uFF08GIT_ASKPASS / credential helper / DNS / \u4ED3\u5E93\u6743\u9650\uFF09\uFF0C\u6062\u590D\u540E\u518D\u91CD\u8BD5 propose\uFF1B\u5728\u4ED3\u5E93\u80FD\u8BBF\u95EE\u4E4B\u524D\uFF0C**\u4E0D\u8981\u6539 sha\u3001\u4E5F\u4E0D\u7528\u91CD\u53D6 sha**\u3002`
129674
+ };
129675
+ }
129281
129676
  return {
129282
129677
  ok: false,
129283
129678
  reason: `commit \`${sha2.slice(0, 12)}\u2026\` \u5728\u672C\u9879\u76EE\u7684\u4EFB\u4F55\u4ED3\u5E93\u91CC\u90FD\u627E\u4E0D\u5230\uFF08\u65E2\u4E0D\u662F\u4EFB\u4F55\u5206\u652F\u7684\u6700\u65B0\u63D0\u4EA4\uFF0C\u672C\u5730\u955C\u50CF\u91CC\u4E5F\u6CA1\u6709\uFF09\u3002**\u522B\u624B\u586B sha**\u2014\u2014push \u5230\u5DE5\u5355\u5206\u652F\u540E\u76F4\u63A5 \`oasis propose <id> --reason "\u2026"\`\uFF0C\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u6536\u96C6\uFF1B\u786E\u9700\u624B\u52A8\u6307\u5B9A\u65F6\uFF0Csha \u5FC5\u987B\u7528 \`git rev-parse HEAD\` \u53D6\u3001\u4E14\u5DF2\u7ECF push \u4E0A\u53BB\u3002`
@@ -129346,6 +129741,190 @@ var init_ephemeral_project = __esm({
129346
129741
  }
129347
129742
  });
129348
129743
 
129744
+ // ../server/src/domains/collab/timeline.ts
129745
+ function buildNodeTimeline(artifactId, snap, runIdByTarget) {
129746
+ if (!snap) return { artifactId, items: [] };
129747
+ const runIdOf = (kind, targetId) => runIdByTarget?.get(`${kind}:${targetId}`);
129748
+ const items = [];
129749
+ const artifactCountByWork = /* @__PURE__ */ new Map();
129750
+ for (const a of snap.artifacts) {
129751
+ artifactCountByWork.set(a.workId, (artifactCountByWork.get(a.workId) ?? 0) + 1);
129752
+ }
129753
+ const nodeWorkIds = /* @__PURE__ */ new Set();
129754
+ for (const w2 of snap.works) {
129755
+ if (w2.nodeId !== artifactId) continue;
129756
+ if (w2.replyToIssueId) continue;
129757
+ nodeWorkIds.add(w2.id);
129758
+ const status = w2.status ?? (w2.retryAt ? "retry" : w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
129759
+ items.push({
129760
+ type: "work",
129761
+ id: w2.id,
129762
+ createdAt: w2.createdAt,
129763
+ status,
129764
+ assignee: w2.assigneeActorId,
129765
+ ...w2.startedAt ? { startedAt: w2.startedAt } : {},
129766
+ ...w2.endedAt ? { endedAt: w2.endedAt } : {},
129767
+ ...w2.conclusion ? { conclusion: w2.conclusion } : {},
129768
+ ...w2.outcome ? { outcome: w2.outcome } : {},
129769
+ ...w2.rejectedReason ? { rejectedReason: w2.rejectedReason } : {},
129770
+ ...w2.acceptedAt ? { acceptedAt: w2.acceptedAt } : {},
129771
+ ...w2.outputVersionNo !== null ? { outputVersionNo: w2.outputVersionNo } : {},
129772
+ .../* @__PURE__ */ ((r0) => r0 ? { runId: r0 } : {})(runIdOf("work", w2.id)),
129773
+ ...w2.sessionRef ? { sessionRef: w2.sessionRef } : {},
129774
+ artifactCount: artifactCountByWork.get(w2.id) ?? 0
129775
+ });
129776
+ }
129777
+ for (const r of snap.reviews) {
129778
+ if (r.nodeId !== artifactId) continue;
129779
+ const status = r.status ?? (r.retryAt ? "retry" : r.cancelledAt ? "dead" : r.verdict === "approve" ? "accept" : r.verdict === "request_changes" ? "reject" : r.endedAt ? "failed" : "running");
129780
+ items.push({
129781
+ type: "review",
129782
+ id: r.id,
129783
+ createdAt: r.createdAt,
129784
+ status,
129785
+ reviewer: r.reviewerActorId,
129786
+ reviewGroup: r.reviewGroup,
129787
+ targetWorkId: r.targetWorkId,
129788
+ ...r.verdict ? { verdict: r.verdict } : {},
129789
+ ...r.note ? { note: r.note } : {},
129790
+ ...r.decidedAt ? { decidedAt: r.decidedAt } : {},
129791
+ .../* @__PURE__ */ ((r0) => r0 ? { runId: r0 } : {})(runIdOf("review", r.id)),
129792
+ ...r.sessionRef ? { sessionRef: r.sessionRef } : {}
129793
+ });
129794
+ }
129795
+ for (const i of snap.issues) {
129796
+ const related = i.aboutNodeId === artifactId || i.aboutWorkId !== null && nodeWorkIds.has(i.aboutWorkId) || i.aboutNodeId === null && i.aboutWorkId === null && i.raisedByNodeId === artifactId;
129797
+ if (!related) continue;
129798
+ const replies = snap.issueReplies.filter((r) => r.issueId === i.id).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt)).map((r) => ({ author: r.authorActorId, body: r.body, createdAt: r.createdAt }));
129799
+ items.push({
129800
+ type: "issue",
129801
+ id: i.id,
129802
+ createdAt: i.createdAt,
129803
+ kind: i.kind,
129804
+ state: i.state,
129805
+ blocking: i.blocking,
129806
+ body: i.body,
129807
+ author: i.authorActorId,
129808
+ ...i.raisedByNodeId ? { raisedByNodeId: i.raisedByNodeId } : {},
129809
+ ...i.aboutWorkId ? { aboutWorkId: i.aboutWorkId } : {},
129810
+ ...i.resolvedAt ? { resolvedAt: i.resolvedAt } : {},
129811
+ ...i.resolvedNote ? { resolvedNote: i.resolvedNote } : {},
129812
+ ...i.proposedResolution ? { proposedResolution: i.proposedResolution } : {},
129813
+ replies
129814
+ });
129815
+ }
129816
+ items.sort((a, b2) => a.createdAt < b2.createdAt ? -1 : a.createdAt > b2.createdAt ? 1 : TYPE_ORDER[a.type] - TYPE_ORDER[b2.type]);
129817
+ return { artifactId, items };
129818
+ }
129819
+ var TYPE_ORDER;
129820
+ var init_timeline = __esm({
129821
+ "../server/src/domains/collab/timeline.ts"() {
129822
+ "use strict";
129823
+ TYPE_ORDER = { work: 0, review: 1, issue: 2 };
129824
+ }
129825
+ });
129826
+
129827
+ // ../server/src/domains/collab/node-state.ts
129828
+ function nodeLabel(a) {
129829
+ if (a.title) return a.title;
129830
+ if (a.description) return a.description;
129831
+ const tail = a.id.split(":").slice(2).join(":");
129832
+ return tail ? `\u672A\u547D\u540D ${a.type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${a.type}`;
129833
+ }
129834
+ function workspaceTitleMap(model) {
129835
+ const titleOf = /* @__PURE__ */ new Map();
129836
+ for (const a of model.artifacts.values()) {
129837
+ if (a.type === "brief") {
129838
+ const title = a.title ?? a.description;
129839
+ if (title) titleOf.set(a.workspace, title);
129840
+ }
129841
+ }
129842
+ return titleOf;
129843
+ }
129844
+ function isCurrentHeadConcluded2(model, a) {
129845
+ return a.currentRev !== null && concludedHead(model, a.id) === a.currentRev;
129846
+ }
129847
+ function nodeInfo(model, a) {
129848
+ const lifecycle = lifecycleOf(model, a.id);
129849
+ const sealed = lifecycle !== "active";
129850
+ const concluded = isCurrentHeadConcluded2(model, a);
129851
+ const queueLen = queueOf(model, a.id).length;
129852
+ const revisions = revisionsOf(model, a.id);
129853
+ const hasWorking = revisions.some((r) => r.state === "working");
129854
+ const producedParts = new Set(revisions.filter((r) => r.part !== void 0 && r.state !== "rejected").map((r) => r.part));
129855
+ const hasUnproducedParts = (a.parts ?? []).some((p2) => !producedParts.has(p2.name));
129856
+ let mark;
129857
+ if (a.latestWorkState !== void 0) {
129858
+ const st = a.latestWorkState;
129859
+ if (sealed) {
129860
+ mark = lifecycle;
129861
+ } else if (st === null) {
129862
+ mark = "embryo";
129863
+ } else if (st === "running") {
129864
+ mark = "active";
129865
+ } else if (st === "success") {
129866
+ mark = a.latestAccepted ? "concluded" : "gate-pending";
129867
+ } else {
129868
+ mark = "blocked";
129869
+ }
129870
+ } else {
129871
+ mark = sealed ? lifecycle : pendingConcludeAttempt(model, a.id) ? "gate-pending" : blockedOf(model, a.id).blocked ? "blocked" : concluded ? "concluded" : a.currentRev || queueLen > 0 ? "active" : "embryo";
129872
+ }
129873
+ return { mark, queueLen, concluded, sealed, hasWorking, hasUnproducedParts, latestWorkState: a.latestWorkState ?? null, latestAccepted: a.latestAccepted ?? false };
129874
+ }
129875
+ function deriveStage(members) {
129876
+ if (members.length > 0 && members.every((m2) => m2.sealed)) return "sealed";
129877
+ const live = members.filter((m2) => !m2.sealed);
129878
+ if (live.length > 0 && live.every((m2) => m2.concluded)) return "concluded";
129879
+ const reviewing = live.some((m2) => m2.mark === "gate-pending" || m2.mark === "active" && m2.queueLen > 0 && !m2.hasUnproducedParts);
129880
+ if (reviewing) return "review";
129881
+ const progressing = live.some((m2) => m2.mark === "active" || m2.mark === "embryo" || m2.hasUnproducedParts);
129882
+ const blocked = live.some((m2) => m2.mark === "blocked");
129883
+ if (blocked && !progressing) return "blocked";
129884
+ return "executing";
129885
+ }
129886
+ function nodeRunState(info) {
129887
+ if (info.sealed) return void 0;
129888
+ switch (info.mark) {
129889
+ case "embryo":
129890
+ return "not_run";
129891
+ case "active":
129892
+ return "running";
129893
+ case "gate-pending":
129894
+ return "reviewing";
129895
+ case "concluded":
129896
+ return "done";
129897
+ case "blocked":
129898
+ return "stuck";
129899
+ default:
129900
+ return void 0;
129901
+ }
129902
+ }
129903
+ function deriveNodeStage(info) {
129904
+ if (info.sealed) return "sealed";
129905
+ switch (info.mark) {
129906
+ case "concluded":
129907
+ return "concluded";
129908
+ case "gate-pending":
129909
+ return "review";
129910
+ case "blocked":
129911
+ return "blocked";
129912
+ case "embryo":
129913
+ return "embryo";
129914
+ default:
129915
+ if (info.latestWorkState === "running") return "editing";
129916
+ if (info.hasWorking || info.hasUnproducedParts) return "editing";
129917
+ if (info.queueLen > 0) return "review";
129918
+ return "merged";
129919
+ }
129920
+ }
129921
+ var init_node_state = __esm({
129922
+ "../server/src/domains/collab/node-state.ts"() {
129923
+ "use strict";
129924
+ init_src3();
129925
+ }
129926
+ });
129927
+
129349
129928
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
129350
129929
  var util, objectUtil, ZodParsedType, getParsedType;
129351
129930
  var init_util = __esm({
@@ -146190,6 +146769,7 @@ var init_mcp2 = __esm({
146190
146769
  "../server/src/mcp.ts"() {
146191
146770
  "use strict";
146192
146771
  init_mcp();
146772
+ init_src();
146193
146773
  init_src3();
146194
146774
  }
146195
146775
  });
@@ -147328,90 +147908,6 @@ var init_assistants = __esm({
147328
147908
  }
147329
147909
  });
147330
147910
 
147331
- // ../server/src/domains/collab/node-state.ts
147332
- function nodeLabel(a) {
147333
- if (a.title) return a.title;
147334
- if (a.description) return a.description;
147335
- const tail = a.id.split(":").slice(2).join(":");
147336
- return tail ? `\u672A\u547D\u540D ${a.type}\uFF08${tail}\uFF09` : `\u672A\u547D\u540D ${a.type}`;
147337
- }
147338
- function workspaceTitleMap(model) {
147339
- const titleOf = /* @__PURE__ */ new Map();
147340
- for (const a of model.artifacts.values()) {
147341
- if (a.type === "brief") {
147342
- const title = a.title ?? a.description;
147343
- if (title) titleOf.set(a.workspace, title);
147344
- }
147345
- }
147346
- return titleOf;
147347
- }
147348
- function isCurrentHeadConcluded2(model, a) {
147349
- return a.currentRev !== null && concludedHead(model, a.id) === a.currentRev;
147350
- }
147351
- function nodeInfo(model, a) {
147352
- const lifecycle = lifecycleOf(model, a.id);
147353
- const sealed = lifecycle !== "active";
147354
- const concluded = isCurrentHeadConcluded2(model, a);
147355
- const queueLen = queueOf(model, a.id).length;
147356
- const revisions = revisionsOf(model, a.id);
147357
- const hasWorking = revisions.some((r) => r.state === "working");
147358
- const producedParts = new Set(revisions.filter((r) => r.part !== void 0 && r.state !== "rejected").map((r) => r.part));
147359
- const hasUnproducedParts = (a.parts ?? []).some((p2) => !producedParts.has(p2.name));
147360
- let mark;
147361
- if (a.latestWorkState !== void 0) {
147362
- const st = a.latestWorkState;
147363
- if (sealed) {
147364
- mark = lifecycle;
147365
- } else if (st === null) {
147366
- mark = "embryo";
147367
- } else if (st === "running") {
147368
- mark = "active";
147369
- } else if (st === "success") {
147370
- mark = a.latestAccepted ? "concluded" : "gate-pending";
147371
- } else {
147372
- mark = "blocked";
147373
- }
147374
- } else {
147375
- mark = sealed ? lifecycle : pendingConcludeAttempt(model, a.id) ? "gate-pending" : blockedOf(model, a.id).blocked ? "blocked" : concluded ? "concluded" : a.currentRev || queueLen > 0 ? "active" : "embryo";
147376
- }
147377
- return { mark, queueLen, concluded, sealed, hasWorking, hasUnproducedParts, latestWorkState: a.latestWorkState ?? null, latestAccepted: a.latestAccepted ?? false };
147378
- }
147379
- function deriveStage(members) {
147380
- if (members.length > 0 && members.every((m2) => m2.sealed)) return "sealed";
147381
- const live = members.filter((m2) => !m2.sealed);
147382
- if (live.length > 0 && live.every((m2) => m2.concluded)) return "concluded";
147383
- const reviewing = live.some((m2) => m2.mark === "gate-pending" || m2.mark === "active" && m2.queueLen > 0 && !m2.hasUnproducedParts);
147384
- if (reviewing) return "review";
147385
- const progressing = live.some((m2) => m2.mark === "active" || m2.mark === "embryo" || m2.hasUnproducedParts);
147386
- const blocked = live.some((m2) => m2.mark === "blocked");
147387
- if (blocked && !progressing) return "blocked";
147388
- return "executing";
147389
- }
147390
- function deriveNodeStage(info) {
147391
- if (info.sealed) return "sealed";
147392
- switch (info.mark) {
147393
- case "concluded":
147394
- return "concluded";
147395
- case "gate-pending":
147396
- return "review";
147397
- case "blocked":
147398
- return "blocked";
147399
- case "embryo":
147400
- return "embryo";
147401
- default:
147402
- if (info.latestWorkState === "running") return "editing";
147403
- if (info.hasWorking || info.hasUnproducedParts) return "editing";
147404
- if (info.queueLen > 0) return "review";
147405
- return "merged";
147406
- }
147407
- }
147408
- var init_node_state = __esm({
147409
- "../server/src/domains/collab/node-state.ts"() {
147410
- "use strict";
147411
- init_src3();
147412
- }
147413
- });
147414
-
147415
147911
  // ../server/src/domains/collab/stall.ts
147416
147912
  function shortId2(id) {
147417
147913
  return id.length > 22 ? `${id.slice(0, 14)}\u2026` : id;
@@ -147420,16 +147916,16 @@ function stallOf(model, id, runtime, dispatcherSignals) {
147420
147916
  const a = model.artifacts.get(id);
147421
147917
  if (!a) return null;
147422
147918
  const ownerIsAgent = a.owner.startsWith("actor:agent:");
147919
+ const escs = unresolvedEscalationsOf(model, id);
147920
+ if (escs.length > 0) {
147921
+ return { kind: "waiting_human", action: "discuss", ref: id, detail: `\u5DF2\u4E0A\u62A5\u5F85\u4EBA\u4ECB\u5165\uFF1A${escs.at(-1).reason}`, since: escs.at(-1).at };
147922
+ }
147423
147923
  if (runtime?.status === "running") {
147424
147924
  return { kind: "running", detail: `${runtime.action ?? "produce"} \u5728\u8DD1`, ...runtime.startedAt ? { since: runtime.startedAt } : {} };
147425
147925
  }
147426
147926
  if (hasOpenHumanChangeRequest(model, id) || headRejected(model, id)) {
147427
147927
  return ownerIsAgent ? { kind: "waiting_agent", action: "rework", actorId: a.owner, detail: "\u6709\u672A\u5904\u7406\u7684\u53D8\u66F4\u8BF7\u6C42\uFF0C\u5F85\u8FD4\u5DE5" } : { kind: "waiting_human", action: "rework", actorId: a.owner, detail: "\u6709\u672A\u5904\u7406\u7684\u53D8\u66F4\u8BF7\u6C42\uFF0C\u5F85\u4F60\u8FD4\u5DE5" };
147428
147928
  }
147429
- const escs = unresolvedEscalationsOf(model, id);
147430
- if (escs.length > 0) {
147431
- return { kind: "waiting_human", action: "discuss", ref: id, detail: `\u5DF2\u4E0A\u62A5\u5F85\u4EBA\u4ECB\u5165\uFF1A${escs.at(-1).reason}`, since: escs.at(-1).at };
147432
- }
147433
147929
  const gaps = unresolvedGapsOf(model, id);
147434
147930
  if (gaps.length > 0) {
147435
147931
  return { kind: "waiting_human", action: "resolve_gap", detail: `\u5361\u5728\u7F3A\u53E3\u4E0A\u5F85\u4EBA\u8865\uFF1A${gaps[0].description}` };
@@ -148537,7 +149033,7 @@ function requiresMeReasonOf(model, artifact, viewer, facts) {
148537
149033
  function computeRequiresMe(model, artifact, viewer, facts) {
148538
149034
  return requiresMeReasonOf(model, artifact, viewer, facts) !== null;
148539
149035
  }
148540
- function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject, dispatchJournal = [], dispatchPausedOf, ops = [], reviewerPreview, dispatcherSignalsOf, viewer, roleDefaultOwnerOf, roleLabelOf, resolveRoleExecutor, coordinatorEvents) {
149036
+ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject, dispatchJournal = [], dispatchPausedOf, _ops = [], reviewerPreview, dispatcherSignalsOf, viewer, roleDefaultOwnerOf, roleLabelOf, resolveRoleExecutor, coordinatorEvents) {
148541
149037
  const arts = [...model.artifacts.values()].filter((a) => a.workspace === workspaceId);
148542
149038
  if (arts.length === 0) return null;
148543
149039
  const ref2 = (id) => {
@@ -148596,16 +149092,23 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
148596
149092
  const mail = lifecycle === "active" ? openMailOf(model, a.id) : [];
148597
149093
  const requiresMeReason = lifecycle === "active" && a.id !== overviewRootId ? requiresMeReasonOf(model, a, viewer, requiresMeFacts) : null;
148598
149094
  const requiresMe = requiresMeReason !== null;
149095
+ const runState = nodeRunState(info);
148599
149096
  return {
148600
149097
  id: a.id,
148601
149098
  type: a.type,
148602
149099
  label: nodeLabel(a),
148603
149100
  stage,
149101
+ ...runState ? { state: runState } : {},
148604
149102
  currentRev: a.currentRev,
148605
149103
  queue: info.queueLen,
148606
149104
  // ★ 新引擎统一:node 状态唯一由 latest_work 派生(node-state.ts 五态)——mark==="blocked" 覆盖
148607
149105
  // 旧 fold 的 blockedOf(原字段)与新引擎 latest_work failed/dead(blockedOf 恒 false、不可靠)。
148608
149106
  blocked: info.mark === "blocked",
149107
+ // ADR 0079:未解上报数——分诊/巡检看整单时一眼看到「哪个节点挂着人还没处置的上报」。无则不带。
149108
+ ...(() => {
149109
+ const n = unresolvedEscalationsOf(model, a.id).length;
149110
+ return n > 0 ? { unresolvedEscalations: n } : {};
149111
+ })(),
148609
149112
  owner: ref2(a.owner),
148610
149113
  ...(hasDeliverySummary ? a.type === DELIVERY_SUMMARY_ARTIFACT_TYPE : a.isFinalOutput === true) ? { isFinalOutput: true } : {},
148611
149114
  // 封存原因透出:stage 只到 "sealed" 粒度,原因(accepted/cancelled/frozen)另给,供前端区分展示。
@@ -148671,8 +149174,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
148671
149174
  const total = counts2.running + counts2.held + counts2.failed + counts2.waitingHuman + counts2.waitingAgent + counts2.waitingSystem + counts2.waitingConvergence;
148672
149175
  return total > 0 ? { counts: counts2, stuck } : void 0;
148673
149176
  })();
148674
- const activity = buildActivity(model, arts, ref2);
148675
- const coordinatorActivity = coordinatorEvents ? buildCoordinatorActivityFromEvents(model, arts, coordinatorEvents, ref2) : buildCoordinatorActivity(arts, ops, ref2);
149177
+ const coordinatorActivity = buildCoordinatorActivityFromEvents(model, arts, coordinatorEvents ?? [], ref2);
148676
149178
  const acceptance = buildAcceptance(model, arts);
148677
149179
  const planning = planningStatusOf(model, arts, resolveRoleExecutor);
148678
149180
  const spec = workorderSpecOf(model, arts);
@@ -148682,7 +149184,6 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
148682
149184
  return {
148683
149185
  summary: summaryWithRequiresMe,
148684
149186
  graph: { nodes, edges },
148685
- activity,
148686
149187
  coordinatorActivity,
148687
149188
  acceptance,
148688
149189
  ...managerActorId ? { managerActorId } : {},
@@ -148823,7 +149324,6 @@ function buildWorkorderDraftDetail(draft, resolveActor, typeReviewerPreview, res
148823
149324
  return {
148824
149325
  summary,
148825
149326
  graph: { nodes, edges },
148826
- activity: [],
148827
149327
  coordinatorActivity: [],
148828
149328
  acceptance: [],
148829
149329
  ...draft.seed.acceptanceCriteria ? { acceptanceCriteria: draft.seed.acceptanceCriteria } : {},
@@ -148970,93 +149470,6 @@ function buildRuntimeByArtifact(entries, ref2) {
148970
149470
  return [id, runtime];
148971
149471
  }));
148972
149472
  }
148973
- function activitySummaryNote(note) {
148974
- return note.length <= ACTIVITY_NOTE_MAX_CHARS ? note : `${note.slice(0, ACTIVITY_NOTE_MAX_CHARS)}\u2026\uFF08\u5DF2\u622A\u65AD\uFF0C\u539F\u6587 ${note.length} \u5B57\uFF0C\u5168\u6587\u89C1 oplog\uFF09`;
148975
- }
148976
- function buildActivity(model, arts, ref2) {
148977
- const events = [];
148978
- for (const a of arts) {
148979
- for (const r of revisionsOf(model, a.id)) {
148980
- if (r.state !== "working") {
148981
- events.push({
148982
- seq: model.proposeSeq.get(r.id) ?? 0,
148983
- at: r.createdAt,
148984
- actor: ref2(r.author),
148985
- kind: "propose",
148986
- artifactId: a.id,
148987
- revisionId: r.id,
148988
- summary: `\u63D0\u51FA\u4FEE\u8BA2 ${r.id}\uFF08${r.reason}\uFF09`
148989
- });
148990
- }
148991
- const latestVoteByAuthor = /* @__PURE__ */ new Map();
148992
- for (const vote of model.reviews.get(r.id) ?? []) latestVoteByAuthor.set(vote.author, vote);
148993
- for (const vote of latestVoteByAuthor.values()) {
148994
- events.push({
148995
- seq: vote.seq,
148996
- at: vote.at,
148997
- actor: ref2(vote.author),
148998
- kind: vote.verdict === "approve" ? "review-approve" : "review-request-changes",
148999
- artifactId: a.id,
149000
- revisionId: r.id,
149001
- summary: vote.verdict === "approve" ? "\u8BC4\u5BA1\u901A\u8FC7" : `\u8981\u6C42\u4FEE\u6539${vote.note ? `\uFF1A${activitySummaryNote(vote.note)}` : ""}`
149002
- });
149003
- }
149004
- }
149005
- for (const m2 of concludeMilestones(model, a.id)) {
149006
- events.push({
149007
- seq: m2.seq,
149008
- at: m2.at,
149009
- actor: ref2(m2.forced?.by ?? a.owner),
149010
- kind: "conclude",
149011
- artifactId: a.id,
149012
- revisionId: m2.head,
149013
- summary: m2.forced ? `\u5F3A\u5236\u5B9A\u7A3F\uFF08${m2.forced.reason}\uFF09` : "\u5B9A\u7A3F"
149014
- });
149015
- }
149016
- for (const annotation of [...model.annotations.values()].filter((x2) => x2.anchor.target === a.id)) {
149017
- events.push({
149018
- seq: 0,
149019
- // ★ 修复(实证 ws:wo-3945cc70):新引擎 bridge 的 annotation.thread 只含**回复**,无回复的 issue
149020
- // thread=[] → thread[0]?.ts=undefined → at="" → buildActivity 排序时空串排最前、annotate 全堆顶,
149021
- // 协作轨迹时间顺序错乱。fallback 到 issue 创建时间(createdAt 必有值)。
149022
- at: annotation.thread[0]?.ts ?? annotation.createdAt ?? "",
149023
- actor: ref2(annotation.author),
149024
- kind: "annotate",
149025
- artifactId: a.id,
149026
- summary: annotation.body.split("\n").find((line) => line.trim() && !line.startsWith("OASIS_PLANNING_ISSUES ")) ?? "\u6279\u6CE8"
149027
- });
149028
- }
149029
- }
149030
- return events.sort((x2, y) => x2.at < y.at ? -1 : x2.at > y.at ? 1 : x2.seq - y.seq);
149031
- }
149032
- function buildCoordinatorActivity(arts, ops, ref2) {
149033
- const inWorkspace = new Set(arts.map((a) => a.id));
149034
- const managerRaw = arts.find((a) => a.type === "brief")?.fields?.["manager"];
149035
- const managerActor = typeof managerRaw === "string" ? managerRaw : null;
149036
- const firstRevisionAt = ops.filter((op) => op.kind === "propose_revision" && inWorkspace.has(op.artifactId)).map((op) => op.timestamp).sort()[0];
149037
- const GAP_FAMILY = /* @__PURE__ */ new Set(["report_gap", "resolve_gap", "escalate", "resolve_escalation"]);
149038
- const edits = [];
149039
- for (const op of ops) {
149040
- if (!inWorkspace.has(op.artifactId)) continue;
149041
- if (!COORDINATOR_EDIT_KINDS.has(op.kind)) continue;
149042
- if (!GAP_FAMILY.has(op.kind) && (!managerActor || op.actor !== managerActor)) continue;
149043
- const kind = op.kind;
149044
- if ((kind === "spawn_artifact" || kind === "link_input") && (!firstRevisionAt || op.timestamp < firstRevisionAt)) {
149045
- continue;
149046
- }
149047
- const detail = coordinatorEditDetail(kind, op.payload, ref2);
149048
- edits.push({
149049
- seq: op.seq,
149050
- at: op.timestamp,
149051
- actor: ref2(op.actor),
149052
- kind,
149053
- artifactId: op.artifactId,
149054
- label: COORDINATOR_EDIT_LABEL[kind],
149055
- ...detail ? { detail } : {}
149056
- });
149057
- }
149058
- return edits.sort((x2, y) => x2.at < y.at ? -1 : x2.at > y.at ? 1 : x2.seq - y.seq);
149059
- }
149060
149473
  function buildCoordinatorActivityFromEvents(model, arts, events, ref2) {
149061
149474
  const inWorkspace = new Set(arts.map((a) => a.id));
149062
149475
  const managerRaw = arts.find((a) => a.type === "brief")?.fields?.["manager"];
@@ -149095,11 +149508,6 @@ function buildCoordinatorActivityFromEvents(model, arts, events, ref2) {
149095
149508
  else push("resolve_gap", target, rec.seq, rec.createdAt, rec.actorId, "\u89E3\u9664\u7F3A\u53E3", str2(ev.note) ?? void 0);
149096
149509
  continue;
149097
149510
  }
149098
- if (kind === "workorder.sealed") {
149099
- const target = arts.find((a) => a.type === "brief")?.id ?? arts[0]?.id ?? "";
149100
- push("seal", target, rec.seq, rec.createdAt, rec.actorId, "\u5C01\u5B58", [str2(ev.reason), str2(ev.note)].filter(Boolean).join("\uFF1A") || void 0);
149101
- continue;
149102
- }
149103
149511
  if (managerActor && rec.actorId !== managerActor) continue;
149104
149512
  if (kind === "plan.changed") {
149105
149513
  const ev2 = ev;
@@ -149145,46 +149553,6 @@ function buildCoordinatorActivityFromEvents(model, arts, events, ref2) {
149145
149553
  }
149146
149554
  return edits.sort((x2, y) => x2.at < y.at ? -1 : x2.at > y.at ? 1 : x2.seq - y.seq);
149147
149555
  }
149148
- function coordinatorEditDetail(kind, payload, ref2) {
149149
- const str2 = (v2) => typeof v2 === "string" && v2.trim() ? v2 : void 0;
149150
- switch (kind) {
149151
- case "link_input":
149152
- case "unlink_input":
149153
- return str2(payload.to);
149154
- case "spawn_artifact":
149155
- return str2(payload.title) ?? str2(payload.type);
149156
- case "assign_owner": {
149157
- const owner = str2(payload.owner);
149158
- return owner ? ref2(owner).name ?? owner : void 0;
149159
- }
149160
- case "bump_pin": {
149161
- const to = str2(payload.to);
149162
- const pinned = str2(payload.pinned);
149163
- return to && pinned ? `${to} \u2192 ${pinned}` : to ?? pinned;
149164
- }
149165
- case "report_gap":
149166
- return str2(payload.description);
149167
- case "resolve_gap":
149168
- return str2(payload.note) ?? str2(payload.gapId);
149169
- case "escalate":
149170
- return str2(payload.reason);
149171
- case "resolve_escalation":
149172
- return str2(payload.note);
149173
- case "edit_artifact": {
149174
- const changed = ["title", "description", "docType", "fields", "reviewers"].filter((k2) => payload[k2] !== void 0);
149175
- return changed.length ? changed.join("\u3001") : void 0;
149176
- }
149177
- case "seal": {
149178
- const reason = str2(payload.reason);
149179
- const note = str2(payload.note);
149180
- return [reason, note].filter(Boolean).join("\uFF1A") || void 0;
149181
- }
149182
- case "reopen":
149183
- return str2(payload.note);
149184
- default:
149185
- return void 0;
149186
- }
149187
- }
149188
149556
  function buildAcceptance(model, arts) {
149189
149557
  return arts.map((a) => ({
149190
149558
  artifactId: a.id,
@@ -149227,7 +149595,7 @@ function buildNodeStatus(model, artifactId, dispatchJournal, resolve9, dispatche
149227
149595
  })() : void 0;
149228
149596
  return { ...runtime ? { runtime } : {}, ...stall ? { stall } : {} };
149229
149597
  }
149230
- var REQUIRES_ME_REASON_RANK, ACTIVITY_NOTE_MAX_CHARS, COORDINATOR_EDIT_KINDS, COORDINATOR_EDIT_LABEL;
149598
+ var REQUIRES_ME_REASON_RANK;
149231
149599
  var init_workorder_detail = __esm({
149232
149600
  "../server/src/domains/collab/workorder-detail.ts"() {
149233
149601
  "use strict";
@@ -149242,35 +149610,6 @@ var init_workorder_detail = __esm({
149242
149610
  backpressure: 2,
149243
149611
  "owner-mail": 3
149244
149612
  };
149245
- ACTIVITY_NOTE_MAX_CHARS = 500;
149246
- COORDINATOR_EDIT_KINDS = /* @__PURE__ */ new Set([
149247
- "link_input",
149248
- "unlink_input",
149249
- "spawn_artifact",
149250
- "assign_owner",
149251
- "bump_pin",
149252
- "report_gap",
149253
- "resolve_gap",
149254
- "escalate",
149255
- "resolve_escalation",
149256
- "edit_artifact",
149257
- "seal",
149258
- "reopen"
149259
- ]);
149260
- COORDINATOR_EDIT_LABEL = {
149261
- link_input: "\u63A5\u5165\u4F9D\u8D56",
149262
- unlink_input: "\u79FB\u9664\u4F9D\u8D56",
149263
- spawn_artifact: "\u65B0\u5EFA\u4EA4\u4ED8\u7269",
149264
- assign_owner: "\u6539\u6D3E",
149265
- bump_pin: "\u89E6\u53D1\u8FD4\u5DE5",
149266
- report_gap: "\u62A5\u544A\u7F3A\u53E3",
149267
- resolve_gap: "\u89E3\u9664\u7F3A\u53E3",
149268
- escalate: "\u4E0A\u62A5\u9700\u4EBA\u4ECB\u5165",
149269
- resolve_escalation: "\u5173\u95ED\u4E0A\u62A5",
149270
- edit_artifact: "\u6539\u4EA4\u4ED8\u7269\u4FE1\u606F",
149271
- seal: "\u5C01\u5B58",
149272
- reopen: "\u91CD\u542F"
149273
- };
149274
149613
  }
149275
149614
  });
149276
149615
 
@@ -160384,6 +160723,8 @@ function authorizedCommandReceipt(command, args, actor) {
160384
160723
  return { message: `force_conclude ${str(args, "artifactId")}\u2014\u2014\u91CC\u7A0B\u7891\u5E26 forced(by=${actor}) \u6807\u8BB0\uFF0C\u6D88\u8D39\u8005\u53EF\u89C1` };
160385
160724
  case "promote":
160386
160725
  return { message: "promoted to persistent" };
160726
+ case "resolveEscalation":
160727
+ return { message: `\u5DF2\u5173\u95ED\u8BE5\u6761\u4E0A\u62A5\uFF08${str(args, "escalationId")}\uFF09\u3002` };
160387
160728
  default:
160388
160729
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u65E0\u56DE\u6267\u6784\u9020 ${command}`);
160389
160730
  }
@@ -160391,36 +160732,61 @@ function authorizedCommandReceipt(command, args, actor) {
160391
160732
  async function runCommand(kernel, blobs, oplog, engineStore, actor, command, args, ctx = {}) {
160392
160733
  if (isAgent(actor) && GOVERNANCE_COMMANDS.has(command)) {
160393
160734
  if (command === "resolveEscalation") {
160394
- const artifactId = str(args, "artifactId");
160395
160735
  const why = optStr(args, "reason");
160396
- const escalationId = optStr(args, "escalationId");
160397
- if (escalationId === void 0) {
160398
- throw new Error(
160399
- `\u5173\u95ED\u4E0A\u62A5\u5FC5\u987B\u6307\u660E\u5173\u54EA\u4E00\u6761\uFF08--escalation <id>\uFF09\u3002\u6D3E\u7ED9\u4F60\u7684\u4EFB\u52A1\u91CC\u5DF2\u7ECF\u5E26\u4E86\u8FD9\u4E2A id\uFF1B\u4E5F\u53EF\u4EE5\u7528 oasis status ${artifactId} \u770B\u672A\u89E3\u4E0A\u62A5\u7684 id\u3002`
160400
- );
160736
+ let escalationId = optStr(args, "escalationId");
160737
+ if (escalationId === void 0 && ctx.sessionEscalationId !== void 0) {
160738
+ escalationId = ctx.sessionEscalationId;
160739
+ }
160740
+ let artifactId;
160741
+ if (escalationId !== void 0) {
160742
+ const found = escalationById(kernel.model, escalationId);
160743
+ if (!found) throw new Error(`\u4E0A\u62A5 ${escalationId} \u4E0D\u5B58\u5728\uFF08id \u89C1 \`oasis status <\u8282\u70B9>\` \u7684\u300C\u4E0A\u62A5\u300D\u6BB5\uFF09\u3002`);
160744
+ if (found.resolved) throw new Error(`\u4E0A\u62A5 ${escalationId} \u5DF2\u7ECF\u5173\u95ED\uFF0C\u65E0\u9700\u518D\u5173\u3002`);
160745
+ artifactId = found.artifactId;
160746
+ } else {
160747
+ const argArtifactId = optStr(args, "artifactId");
160748
+ if (argArtifactId === void 0) {
160749
+ throw new Error(
160750
+ `\u6CA1\u80FD\u786E\u5B9A\u8981\u5173\u54EA\u6761\u4E0A\u62A5\u2014\u2014\u8FD9\u4E0D\u662F\u4ECE\u67D0\u6761\u4E0A\u62A5\u5361\u70B9\u5F00\u7684\u4F1A\u8BDD\u3002\u8BF7\u6307\u660E\u5173\u54EA\u4E00\u6761\uFF1Aoasis resolve-escalation --escalation <id>\uFF08id \u89C1 \`oasis status <\u8282\u70B9>\` \u7684\u300C\u4E0A\u62A5\u300D\u6BB5\uFF09\u3002`
160751
+ );
160752
+ }
160753
+ const open2 = unresolvedEscalationsOf(kernel.model, argArtifactId);
160754
+ if (open2.length === 1) {
160755
+ escalationId = open2[0].escalationId;
160756
+ artifactId = argArtifactId;
160757
+ } else if (open2.length === 0) {
160758
+ throw new Error(`\u300C${argArtifactId}\u300D\u4E0A\u6CA1\u6709\u672A\u89E3\u4E0A\u62A5\u53EF\u5173\u3002`);
160759
+ } else {
160760
+ throw new Error(
160761
+ `\u300C${argArtifactId}\u300D\u4E0A\u6709 ${open2.length} \u6761\u672A\u89E3\u4E0A\u62A5\uFF0C\u8BF7\u6307\u660E\u5173\u54EA\u4E00\u6761\uFF08--escalation <id>\uFF09\uFF1A
160762
+ ` + open2.map((e) => ` --escalation ${e.escalationId} \uFF08${e.reason.split("\n")[0]}\uFF09`).join("\n")
160763
+ );
160764
+ }
160401
160765
  }
160402
- const { draftId } = await kernel.proposeIntervention(
160403
- {
160404
- reason: why ?? "\u7BA1\u7406\u8005\u5224\u65AD\u8BE5\u4E0A\u62A5\u53EF\u4EE5\u5173\u95ED",
160405
- ops: [
160406
- {
160407
- action: "resolveEscalation",
160408
- artifactId,
160409
- escalationId,
160410
- ...why !== void 0 ? { reason: why } : {}
160411
- }
160412
- ]
160413
- },
160766
+ const normalizedArgs = {
160767
+ escalationId,
160768
+ // 已四级解析出的具体一条(绝非 undefined、绝非「全清」)
160769
+ artifactId,
160770
+ // ← escalationById 反查出的所属节点
160771
+ ...why !== void 0 ? { reason: why } : {}
160772
+ };
160773
+ const escPolicy = classifyCommand("resolveEscalation");
160774
+ const effectLabel = escPolicy.effect(normalizedArgs);
160775
+ const { draftId } = await kernel.proposeCommandAuthorization({
160776
+ command: "resolveEscalation",
160777
+ args: normalizedArgs,
160778
+ artifactId,
160779
+ effectLabel,
160414
160780
  actor,
160415
160781
  // 人正在对话里 → 卡当场弹在这段 chat;自主会话(没人在场)→ 缺省,落 owner inbox。
160416
- ctx.chatOrigin !== void 0 ? { origin: ctx.chatOrigin } : void 0
160417
- );
160782
+ ...ctx.chatOrigin !== void 0 ? { origin: ctx.chatOrigin } : {}
160783
+ });
160418
160784
  return {
160419
- message: `\u5DF2\u63D0\u4EA4\u300C\u5173\u95ED\u8FD9\u6761\u4E0A\u62A5\u300D\u7684\u5F85\u786E\u8BA4\u5361\uFF08${draftId}\uFF09\u2014\u2014**\u8FD8\u6CA1\u751F\u6548**\uFF0C\u7B49\u4EBA\u70B9\u786E\u8BA4\u3002
160785
+ message: `\u5DF2\u63D0\u4EA4\u300C${effectLabel}\u300D\u7684\u5F85\u6388\u6743\u5361\uFF08${draftId}\uFF09\u2014\u2014**\u8FD8\u6CA1\u6267\u884C**\uFF0C\u7B49\u4EBA\u70B9\u786E\u8BA4\u3002
160420
160786
  \u628A\u4F60\u7684\u5224\u65AD\u4F9D\u636E\u8BB2\u6E05\u695A\uFF08\u505A\u4E86\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u73B0\u5728\u53EF\u4EE5\u5173\uFF09\uFF0C\u4EBA\u5728\u5361\u4E0A\u770B\u5230\u7684\u5C31\u662F\u5B83\u3002`,
160421
- // outcome 是**结构化**的生效标记(对齐 /api/stage/apply "applied" | "drafted"):别让 agent
160422
- // 靠读中文判断生没生效——读漏了就会当成已经关掉往下走,正是本线要治的「假报告」。
160423
- data: { outcome: "drafted", draftId }
160787
+ // outcome 结构化生效标记(与 forceConclude 同构):CLI api.cmd 内部据它接管同步等待 / 异步分流
160788
+ // (packages/cli/src/cli.ts:441-448),别让 agent 靠读中文判断生没生效。
160789
+ data: { outcome: "authorizing", draftId }
160424
160790
  };
160425
160791
  }
160426
160792
  const policy = classifyCommand(command);
@@ -161046,8 +161412,9 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
161046
161412
  }
161047
161413
  case "resolveEscalation": {
161048
161414
  const escId = optStr(args, "escalationId");
161415
+ const artId = optStr(args, "artifactId");
161049
161416
  await kernel.resolveEscalation({
161050
- artifactId: str(args, "artifactId"),
161417
+ ...artId !== void 0 ? { artifactId: artId } : {},
161051
161418
  actor,
161052
161419
  ...optStr(args, "reason") !== void 0 ? { reason: optStr(args, "reason") } : {},
161053
161420
  ...escId !== void 0 ? { escalationId: escId } : {}
@@ -161110,10 +161477,6 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
161110
161477
  await kernel.promote({ artifactId: str(args, "artifactId"), actor });
161111
161478
  return authorizedCommandReceipt(command, args, actor);
161112
161479
  }
161113
- case "reopen": {
161114
- await kernel.reopen({ artifactId: str(args, "artifactId"), actor, note: optStr(args, "note") });
161115
- return { message: `reopened ${str(args, "artifactId")}\uFF08lifecycle \u2192 active\uFF09` };
161116
- }
161117
161480
  case "hold": {
161118
161481
  await kernel.hold({
161119
161482
  artifactId: str(args, "artifactId"),
@@ -161121,7 +161484,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
161121
161484
  source: optStr(args, "source") ?? "human",
161122
161485
  ...optStr(args, "reason") !== void 0 ? { reason: optStr(args, "reason") } : {}
161123
161486
  });
161124
- return { message: `\u8282\u70B9\u5DF2\u6682\u505C\u6D3E\u53D1\uFF08hold\uFF09\uFF1A${str(args, "artifactId")}\u3002\u6062\u590D\u7528 release\u3002` };
161487
+ return { message: `\u8282\u70B9\u5DF2\u505C\u6B62\u8FD0\u884C\uFF08work.kill\uFF09\uFF1A${str(args, "artifactId")}\u3002\u91CD\u8DD1\u7528 rerun / release\u3002` };
161125
161488
  }
161126
161489
  case "release": {
161127
161490
  await kernel.release({
@@ -161131,6 +161494,22 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
161131
161494
  });
161132
161495
  return { message: `\u8282\u70B9\u5DF2\u89E3\u963B\uFF08release\uFF09\uFF1A${str(args, "artifactId")}\u2014\u2014\u6295\u5F71\u4E0B\u4E00\u62CD\u4F1A\u91CD\u63A8\u8BE5\u8282\u70B9\u5F53\u4E0B\u6B20\u7684\u6D3B\u3002` };
161133
161496
  }
161497
+ case "rerun": {
161498
+ await kernel.rerunNode({ artifactId: str(args, "artifactId"), actor });
161499
+ return { message: `\u8282\u70B9\u5DF2\u91CD\u65B0\u8FD0\u884C\uFF08work.create\uFF09\uFF1A${str(args, "artifactId")}` };
161500
+ }
161501
+ case "rereview": {
161502
+ await kernel.rereviewNode({ artifactId: str(args, "artifactId"), actor });
161503
+ return { message: `\u8282\u70B9\u5DF2\u91CD\u65B0\u8BC4\u5BA1\uFF08review.create\uFF09\uFF1A${str(args, "artifactId")}` };
161504
+ }
161505
+ case "killReview": {
161506
+ await kernel.killNodeReviews({
161507
+ artifactId: str(args, "artifactId"),
161508
+ actor,
161509
+ ...optStr(args, "reason") !== void 0 ? { reason: optStr(args, "reason") } : {}
161510
+ });
161511
+ return { message: `\u8282\u70B9\u8BC4\u5BA1\u5DF2\u505C\u6B62\uFF08review.kill\uFF09\uFF1A${str(args, "artifactId")}` };
161512
+ }
161134
161513
  case "forceConclude": {
161135
161514
  await kernel.forceConclude({
161136
161515
  artifactId: str(args, "artifactId"),
@@ -161203,13 +161582,14 @@ ${body}`);
161203
161582
  }
161204
161583
  return parts.join("\n\n");
161205
161584
  }
161206
- async function runView(kernel, oplog, engineStore, blobs, name, artifactId, params, ciGateStatus, reviewAssignee, nodeStatus, engineRecentEvents) {
161585
+ async function runView(kernel, oplog, engineStore, blobs, name, artifactId, params, ciGateStatus, reviewAssignee, nodeStatus, engineRecentEvents, dispatchesOfWorkorder) {
161207
161586
  const model = kernel.model;
161208
161587
  const need2 = () => {
161209
161588
  if (!artifactId) throw new Error("\u8BE5\u89C6\u56FE\u9700\u8981 artifact \u53C2\u6570");
161210
161589
  return artifactId;
161211
161590
  };
161212
161591
  const renderRevisionContent = async (rev) => {
161592
+ if (isEmptyContent(rev.contentKind)) return null;
161213
161593
  if (rev.contentKind === "external-pin") return `external-pin \u2192 ${rev.contentRef}\uFF08\u5185\u5BB9\u5728\u5916\u90E8\u7CFB\u7EDF/git\uFF0C\u6863\u6848\u67DC\u672A\u5B58\u6B63\u6587\uFF09`;
161214
161594
  if (rev.contentKind === "manifest") return renderManifestText(blobs, rev.contentRef);
161215
161595
  return new TextDecoder().decode(await blobs.get(rev.contentRef));
@@ -161277,15 +161657,62 @@ async function runView(kernel, oplog, engineStore, blobs, name, artifactId, para
161277
161657
  ),
161278
161658
  gaps: model.gaps.get(id) ?? [],
161279
161659
  // 决策 0030 D1:未解的带 gapId(resolve_gap 定位用)、已解的带 note
161660
+ escalations: escalationsOf(model, id),
161661
+ // ADR 0079:上报全列(含已解)——status 的「上报」段读它
161280
161662
  ciGate: ciGateStatus?.get(id) ?? null,
161281
161663
  // B 批:CI 闸在途实时进度(旁账,无则 null)
161282
161664
  // 「此刻在干嘛」:与工单图节点同源(buildNodeStatus)。缺 provider 时两字段一并缺省。
161283
- ...await nodeStatus?.(id) ?? {}
161665
+ ...await nodeStatus?.(id) ?? {},
161666
+ // 节点运行五态(ADR 0137):与 WorkorderNode.state 同源同口径(nodeRunState),已作废节点缺省。
161667
+ ...(() => {
161668
+ const s2 = nodeRunState(nodeInfo(model, artifact));
161669
+ return s2 ? { state: s2 } : {};
161670
+ })()
161284
161671
  };
161285
161672
  }
161673
+ case "timeline": {
161674
+ const id = need2();
161675
+ const artifact = model.artifacts.get(id);
161676
+ if (!artifact) throw new Error(`artifact \u4E0D\u5B58\u5728: ${id}`);
161677
+ if (!engineStore) return { artifactId: id, items: [] };
161678
+ const ws = artifact.workspace;
161679
+ const snap = await engineStore.transaction((tx) => tx.loadWorkorder(ws));
161680
+ let runIdByTarget;
161681
+ if (dispatchesOfWorkorder) {
161682
+ try {
161683
+ const rows = await dispatchesOfWorkorder(ws);
161684
+ runIdByTarget = /* @__PURE__ */ new Map();
161685
+ for (const d of [...rows].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt))) {
161686
+ runIdByTarget.set(`${d.targetKind}:${d.targetId}`, d.id);
161687
+ }
161688
+ } catch (err) {
161689
+ console.warn(`[timeline] \u6D3E\u53D1\u53F0\u8D26\u67E5\u8BE2\u5931\u8D25\uFF0C\u672C\u6B21\u4E0D\u51FA runId: ${String(err)}`);
161690
+ }
161691
+ }
161692
+ return buildNodeTimeline(id, snap, runIdByTarget);
161693
+ }
161286
161694
  case "content": {
161287
161695
  const id = need2();
161288
161696
  const path26 = params?.get("path") ?? null;
161697
+ if (id.startsWith("esc:")) {
161698
+ const e = escalationById(model, id);
161699
+ if (!e) throw new Error(`\u4E0A\u62A5\u4E0D\u5B58\u5728: ${id}\uFF08\u4E0A\u62A5 id \u89C1 \`oasis status <artifactId>\` \u7684\u300C\u4E0A\u62A5\u300D\u6BB5\uFF09`);
161700
+ const node = model.artifacts.get(e.artifactId);
161701
+ return {
161702
+ kind: "escalation",
161703
+ escalationId: e.escalationId,
161704
+ artifactId: e.artifactId,
161705
+ nodeLabel: node?.title ?? node?.description ?? null,
161706
+ reason: e.reason,
161707
+ by: e.by,
161708
+ ...e.hand !== void 0 ? { hand: e.hand } : {},
161709
+ at: e.at,
161710
+ ...e.part != null ? { part: e.part } : {},
161711
+ ...e.gapId !== void 0 ? { gapId: e.gapId } : {},
161712
+ resolved: e.resolved,
161713
+ ...e.resolvedReason !== void 0 ? { resolvedReason: e.resolvedReason } : {}
161714
+ };
161715
+ }
161289
161716
  if (id.startsWith("annotation:") || id.startsWith("ann:")) {
161290
161717
  const aid = id.startsWith("annotation:") ? id.slice("annotation:".length) : id;
161291
161718
  const a = model.annotations.get(aid);
@@ -161343,7 +161770,7 @@ async function runView(kernel, oplog, engineStore, blobs, name, artifactId, para
161343
161770
  const targetId = pinnedRev ?? artifact.currentRev;
161344
161771
  if (!targetId) return { kind: "empty" };
161345
161772
  const headRev = model.revisions.get(targetId);
161346
- if (headRev.contentKind === "external-pin" && String(headRev.contentRef ?? "").startsWith("\u8FD0\u884C\u4E2D\uFF08")) {
161773
+ if (isEmptyContent(headRev.contentKind)) {
161347
161774
  return { kind: "empty" };
161348
161775
  }
161349
161776
  const at = {
@@ -161772,12 +162199,14 @@ async function startOasisServer(opts) {
161772
162199
  let actingForHuman;
161773
162200
  let deputyChatRt;
161774
162201
  let chatOrigin;
162202
+ let sessionEscalationId;
161775
162203
  if (tokenClaims?.action === "chat" && tokenClaims.chatSessionId && opts.chatSession) {
161776
162204
  const cs = await opts.chatSession.getSession(tokenClaims.chatSessionId).catch(() => null);
161777
162205
  if (cs && cs.aiActorId === actor && cs.humanActorId.startsWith("actor:human:")) {
161778
162206
  actingForHuman = cs.humanActorId;
161779
162207
  deputyChatRt = cs.runtimeSessionId ?? void 0;
161780
162208
  chatOrigin = tokenClaims.chatSessionId;
162209
+ sessionEscalationId = cs.escalationId ?? void 0;
161781
162210
  }
161782
162211
  }
161783
162212
  const result = await runCommand(
@@ -161794,6 +162223,7 @@ async function startOasisServer(opts) {
161794
162223
  ...tokenClaims?.artifactId !== void 0 ? { sessionArtifactId: tokenClaims.artifactId } : {},
161795
162224
  ...actingForHuman !== void 0 ? { actingForHuman } : {},
161796
162225
  ...chatOrigin !== void 0 ? { chatOrigin } : {},
162226
+ ...sessionEscalationId !== void 0 ? { sessionEscalationId } : {},
161797
162227
  ...opts.projectState ? { projectState: opts.projectState } : {},
161798
162228
  ...opts.artifactState ? { artifactState: opts.artifactState } : {},
161799
162229
  ...opts.schema ? { schema: opts.schema } : {},
@@ -162173,7 +162603,7 @@ async function startOasisServer(opts) {
162173
162603
  const body = JSON.parse(rawBody || "{}");
162174
162604
  const ctx = resolveDiscussContext(engine.kernel.model, body, actor ?? void 0);
162175
162605
  const chatStore = opts.chatSession;
162176
- const baseItemKey = body.draftId ? `draft:${body.draftId}` : body.annotationId ? `ann:${body.annotationId}` : `esc:${body.artifactId}`;
162606
+ const baseItemKey = body.draftId ? `draft:${body.draftId}` : body.annotationId ? `ann:${body.annotationId}` : body.escalationId ? `esc:${body.escalationId}` : `esc:${body.artifactId}`;
162177
162607
  const itemKey = `${baseItemKey}:${ctx.ownerId}`;
162178
162608
  let chatSessionId;
162179
162609
  if (chatStore) {
@@ -162188,7 +162618,7 @@ async function startOasisServer(opts) {
162188
162618
  const mgrBinding = reg0 ? await reg0.getBinding(ctx.managerId).catch(() => null) : null;
162189
162619
  const managerNodeId = mgrBinding?.status === "active" ? mgrBinding.nodeId : "";
162190
162620
  const managerRuntimeKind = mgrBinding?.status === "active" ? mgrBinding.runtimeKind : void 0;
162191
- await chatStore.createSession({ id: chatSessionId, humanActorId: ctx.ownerId, aiActorId: ctx.managerId, runtimeId: managerNodeId, ...managerRuntimeKind ? { runtimeKind: managerRuntimeKind } : {}, runtimeSessionId: null, title, touchedAt: now, createdAt: now, workspace: ctx.workspace }).catch(() => void 0);
162621
+ await chatStore.createSession({ id: chatSessionId, humanActorId: ctx.ownerId, aiActorId: ctx.managerId, runtimeId: managerNodeId, ...managerRuntimeKind ? { runtimeKind: managerRuntimeKind } : {}, runtimeSessionId: null, title, touchedAt: now, createdAt: now, workspace: ctx.workspace, ...body.escalationId ? { escalationId: body.escalationId } : {} }).catch(() => void 0);
162192
162622
  await chatStore.linkWorkOrder(chatSessionId, ctx.workspace).catch(() => void 0);
162193
162623
  }
162194
162624
  discussSessions.set(itemKey, chatSessionId);
@@ -162494,7 +162924,8 @@ async function startOasisServer(opts) {
162494
162924
  opts.ciGateStatus,
162495
162925
  opts.reviewAssignee,
162496
162926
  opts.nodeStatus,
162497
- engineRecentEvents
162927
+ engineRecentEvents,
162928
+ opts.dispatchesOfWorkorder
162498
162929
  );
162499
162930
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ data }));
162500
162931
  } catch (err) {
@@ -163619,6 +164050,8 @@ var init_server3 = __esm({
163619
164050
  init_collect();
163620
164051
  init_remote_util();
163621
164052
  init_ephemeral_project();
164053
+ init_timeline();
164054
+ init_node_state();
163622
164055
  init_src3();
163623
164056
  init_mcp2();
163624
164057
  init_router();
@@ -163641,7 +164074,7 @@ var init_server3 = __esm({
163641
164074
  CHAT_OUTPUT_LIMIT = 16e3;
163642
164075
  STREAMING_PATHS = ["/api/events", "/api/chat", "/api/trajectory/events", "/api/intervention/wait"];
163643
164076
  WILL_VERBS = /* @__PURE__ */ new Set(["annotate", "reply", "resolve", "review", "merge", "escalate", "hold", "release"]);
163644
- GOVERNANCE_COMMANDS = /* @__PURE__ */ new Set(["seal", "assign", "promote", "reopen", "forceConclude", "unlink", "gc", "requestChange", "resolveEscalation"]);
164077
+ GOVERNANCE_COMMANDS = /* @__PURE__ */ new Set(["seal", "assign", "promote", "forceConclude", "unlink", "gc", "requestChange", "resolveEscalation"]);
163645
164078
  isAgent = (actor) => actor.startsWith("actor:agent:");
163646
164079
  }
163647
164080
  });
@@ -177505,6 +177938,8 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
177505
177938
  actor: actorRef(gap.by),
177506
177939
  ...gapReportCount > 1 ? { impact: `\u5DF2\u91CD\u590D\u4E0A\u62A5 ${gapReportCount} \u6B21` } : {},
177507
177940
  overdue: exactEscalation ? true : overdueSince(gapSince) ?? false,
177941
+ // ADR 0079 D1:把挂在这条 gap 上的那条上报 id 带到**顶层**(前端「沟通聊聊」据它绑定会话 → 关上报可省 id)。
177942
+ ...exactEscalation?.escalationId ? { escalationId: exactEscalation.escalationId } : {},
177508
177943
  gap: {
177509
177944
  gapId: gap.gapId,
177510
177945
  // 契约:reporter 永远是 GapView.by(原始上报人);诊断者另见 diagnosis 文案,不覆盖署名。
@@ -177513,27 +177948,31 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
177513
177948
  problem: gap.description,
177514
177949
  ...exactEscalation ? { diagnosis: exactEscalation.reason } : {},
177515
177950
  priority: exactEscalation ? "decision_required" : "blocked",
177516
- reportCount: gap.reportCount ?? 1
177951
+ reportCount: gap.reportCount ?? 1,
177952
+ ...exactEscalation?.escalationId ? { escalationId: exactEscalation.escalationId } : {}
177517
177953
  }
177518
177954
  });
177519
177955
  }
177520
177956
  if (gaps.length === 0 && escalations.length > 0) {
177521
- const last = escalations.at(-1);
177522
- gapItems.push({
177523
- kind: "gap-escalation",
177524
- artifactId: a.id,
177525
- workspace: a.workspace,
177526
- since: last.at,
177527
- actionRequired: true,
177528
- title: nodeLabel(a),
177529
- actor: actorRef(last.by),
177530
- overdue: true,
177531
- // 无 gap 的熔断升级卡本就是「已卡住需人介入」,恒置顶催办
177532
- // ADR-0108 D10:**不再截到 100 字**。熔断升级卡走的正是这条「无 gap」分支,而 D10
177533
- // 往里放进了真正有用的东西(末次失败原文、哪台机器死了多久、去哪改绑)——一刀切到 100 字
177534
- // 会把它们全砍掉,只剩「连败 N 次」这种人早就知道的部分。前端按卡片排版自己收,不在这里截。
177535
- summary: `\u300C${nodeLabel(a)}\u300D\u88AB\u4E0A\u62A5\u9700\u4F60\u4ECB\u5165${who}\uFF1A${last.reason}${escalations.length > 1 ? `\uFF08\u5171 ${escalations.length} \u6761\uFF09` : ""}`
177536
- });
177957
+ for (const e of escalations) {
177958
+ gapItems.push({
177959
+ kind: "gap-escalation",
177960
+ artifactId: a.id,
177961
+ workspace: a.workspace,
177962
+ since: e.at,
177963
+ actionRequired: true,
177964
+ title: nodeLabel(a),
177965
+ actor: actorRef(e.by),
177966
+ overdue: true,
177967
+ // 无 gap 的熔断升级卡本就是「已卡住需人介入」,恒置顶催办
177968
+ // ADR 0079 D1:每张卡各代表一条上报 顶层带上它的 escalationId(会话绑定用);summary 不再拼「(共 N 条)」。
177969
+ escalationId: e.escalationId,
177970
+ // ADR-0108 D10:**不再截到 100 字**。熔断升级卡走的正是这条「无 gap」分支,而 D10 刚
177971
+ // 往里放进了真正有用的东西(末次失败原文、哪台机器死了多久、去哪改绑)——一刀切到 100
177972
+ // 会把它们全砍掉,只剩「连败 N 次」这种人早就知道的部分。前端按卡片排版自己收,不在这里截。
177973
+ summary: `\u300C${nodeLabel(a)}\u300D\u88AB\u4E0A\u62A5\u9700\u4F60\u4ECB\u5165${who}\uFF1A${e.reason}`
177974
+ });
177975
+ }
177537
177976
  }
177538
177977
  }
177539
177978
  for (const r of listPendingReviews(model)) {
@@ -178715,14 +179154,6 @@ var init_letter_read_backfill = __esm({
178715
179154
  }
178716
179155
  });
178717
179156
 
178718
- // ../server/src/domains/collab/workorder-seal.ts
178719
- var init_workorder_seal = __esm({
178720
- "../server/src/domains/collab/workorder-seal.ts"() {
178721
- "use strict";
178722
- init_src3();
178723
- }
178724
- });
178725
-
178726
179157
  // ../server/src/domains/collab/index.ts
178727
179158
  async function resolveLead(registry2, me) {
178728
179159
  const rec = await registry2.getActor(me);
@@ -178784,6 +179215,7 @@ async function renderOverviewText(blobs, model, arts) {
178784
179215
  if (!head) return void 0;
178785
179216
  const rev = model.revisions.get(head);
178786
179217
  if (!rev) return void 0;
179218
+ if (isEmptyContent(rev.contentKind)) return "";
178787
179219
  if (rev.contentKind === "external-pin") return `external-pin \u2192 ${rev.contentRef}`;
178788
179220
  if (rev.contentKind === "manifest") {
178789
179221
  const entries = await readManifest(blobs, rev.contentRef);
@@ -178938,13 +179370,7 @@ function collabDomain(opts) {
178938
179370
  const resolveProject = await buildProjectResolver(artifacts);
178939
179371
  const dispatchPausedOf = await buildDispatchPausedResolver(kernel.model, artifacts);
178940
179372
  const wsArts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === req.params.id);
178941
- let ops = [];
178942
- let coordinatorEvents;
178943
- if (engineStore) {
178944
- coordinatorEvents = await engineStore.transaction((tx) => tx.listEvents(req.params.id));
178945
- } else {
178946
- ops = (await Promise.all(wsArts.map((a) => oplog.read(a.id)))).flat();
178947
- }
179373
+ const coordinatorEvents = engineStore ? await engineStore.transaction((tx) => tx.listEvents(req.params.id)) : void 0;
178948
179374
  const signalsForThisCompany = opts.dispatcherSignalsOf ? (id) => opts.dispatcherSignalsOf(req.auth.companyId, id) : void 0;
178949
179375
  const roleDefsByName = opts.roles ? new Map((await opts.roles.list()).map((r) => [r.name, { label: r.label, defaultOwner: r.defaultOwner }])) : void 0;
178950
179376
  const roleDefaultOwnerOf = roleDefsByName ? (role) => {
@@ -178959,7 +179385,7 @@ function collabDomain(opts) {
178959
179385
  resolveProject,
178960
179386
  opts.dispatchJournal?.() ?? [],
178961
179387
  dispatchPausedOf,
178962
- ops,
179388
+ [],
178963
179389
  (id) => kernel.reviewerPreview(id),
178964
179390
  signalsForThisCompany,
178965
179391
  req.auth.actor,
@@ -179321,7 +179747,6 @@ var init_collab = __esm({
179321
179747
  init_organization_usage();
179322
179748
  init_planner();
179323
179749
  init_closure_report();
179324
- init_workorder_seal();
179325
179750
  }
179326
179751
  });
179327
179752
 
@@ -183552,7 +183977,7 @@ function humanExitCause(exit) {
183552
183977
  const base = (exit.reason ? label[exit.reason] : void 0) ?? `\u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F\uFF08${exit.reason ?? "\u65E0\u9000\u51FA\u4FE1\u606F"}\uFF09`;
183553
183978
  return exit.errorMessage ? `${base}\uFF1A${exit.errorMessage.slice(0, 200)}` : base;
183554
183979
  }
183555
- var import_node_crypto36, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
183980
+ var import_node_crypto36, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_ESCALATION_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
183556
183981
  var init_worker = __esm({
183557
183982
  "../server/src/coordinator/worker.ts"() {
183558
183983
  "use strict";
@@ -183635,12 +184060,18 @@ ${HIGH_RISK_COMMANDS}`;
183635
184060
  - \`oasis hold <id> --reason "<\u4E3A\u4EC0\u4E48\u5148\u51BB>"\` \u2014\u2014 \u6682\u505C\u8BE5\u8282\u70B9\u7684 agent \u6D3E\u53D1\u3002\u53CD\u590D\u5931\u8D25\u3001\u6216\u4EBA\u8981\u5148\u67E5/\u5148\u624B\u52A8\u6539\u73AF\u5883\uFF08\u6362 runtime\u3001\u8865\u51ED\u8BC1\uFF09\u65F6\uFF0C\u5148\u628A\u5B83\u51BB\u4F4F\u522B\u518D\u7A7A\u8F6C\u91CD\u6D3E\uFF1Bowner \u4E0D\u53D8\u3001\u5185\u5BB9\u4ECD\u53EF\u6539\u3002
183636
184061
  - \`oasis release <id>\` \u2014\u2014 \u6062\u590D\u6D3E\u53D1\uFF1A\u89E3\u963B\u540E\u7CFB\u7EDF\u91CD\u63A8\u8BE5\u8282\u70B9**\u5F53\u4E0B\u771F\u6B63\u6B20\u7684\u6D3B**\uFF08\u5BF9\u88AB\u5347\u7EA7\u51BB\u7ED3\u7684\u8282\u70B9\uFF0C\u8FD9\u5C31\u662F"\u8BA9\u5B83\u91CD\u65B0\u8DD1"\uFF09\u3002**\u53EA\u5728\u786E\u8BA4\u6839\u56E0\u5DF2\u5904\u7406\u540E\u624D\u653E**\uFF08\u4EBA\u5DF2\u6362 runtime / \u8865\u4E86\u51ED\u8BC1 / \u6539\u4E86\u4EFB\u52A1\u4E66\uFF09\uFF0C\u5426\u5219\u4E00\u653E\u53C8\u683D\u540C\u4E00\u4E2A\u5751\u3002
183637
184062
  \u5224\u65AD\u8FB9\u754C\uFF1A\u8FD9\u4FE9\u662F"\u6709\u4EE3\u4EF7\u3001\u9700\u5224\u65AD"\u7684\u52A8\u4F5C\u2014\u2014**\u4EBA\u6279\u51C6\u4E86\u518D\u505A**\uFF0C\u522B\u81EA\u4F5C\u4E3B\u5F20\u628A\u4EBA\u6B63\u5728\u67E5\u7684\u8282\u70B9\u653E\u51FA\u53BB\u3002`;
184063
+ CONVERSATIONAL_ESCALATION_TOOLS = `\u3010\u5173\u4E0A\u62A5\uFF08\u5BF9\u8BDD\u4E13\u5C5E\uFF09\uFF1A\u4ECE\u67D0\u6761\u4E0A\u62A5\u5361\u300C\u6C9F\u901A\u804A\u804A\u300D\u8FDB\u6765\u3001\u628A\u4E8B\u60C5\u529E\u5B8C\u540E\uFF0C\u4E3B\u52A8\u5173\u6389\u8FD9\u6761\u4E0A\u62A5\u2014\u2014\u522B\u628A\u5DF2\u7ECF\u89E3\u51B3\u7684\u4E8B\u4E00\u76F4\u6302\u5728\u4EBA\u7684\u6536\u4EF6\u7BB1\u91CC\u3011\uFF1A
184064
+ - \`oasis resolve-escalation --reason "<\u4F60\u505A\u4E86\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u73B0\u5728\u53EF\u4EE5\u5173>"\` \u2014\u2014 **\u4E0D\u7528\u5E26 id**\uFF1A\u8FD9\u573A\u4F1A\u8BDD\u5C31\u662F\u4E3A\u67D0\u6761\u4E0A\u62A5\u5F00\u7684\uFF0C\u7CFB\u7EDF\u77E5\u9053\u5173\u54EA\u4E00\u6761\u3002
184065
+ - \u5B83**\u4E0D\u4F1A\u7ACB\u523B\u751F\u6548**\uFF1A\u4F1A\u8F6C\u6210\u4E00\u5F20\u300C\u5F85\u786E\u8BA4\u300D\u5361\u7B49\u4EBA\u70B9\u786E\u8BA4\uFF08\u5173\u9519\u7684\u540E\u679C\u662F\u5361\u6D88\u5931\u3001\u8282\u70B9\u8FD8\u51BB\u7740 \u2192 \u9759\u9ED8\u6C38\u4E45\u505C\u6446\uFF0C\u6240\u4EE5\u8981\u4EBA\u70B9\u5934\uFF09\u3002\u7167\u53D1\u5373\u53EF\uFF0C\u522B\u56E0\u4E3A\u4E0D\u7ACB\u523B\u751F\u6548\u5C31\u8DF3\u8FC7\u3001\u4E5F\u522B\u5F53\u6210\u5DF2\u7ECF\u5173\u6389\u5F80\u4E0B\u8D70\u3002
184066
+ - \`--reason\` \u5199\u6E05\u4F9D\u636E\uFF08\u505A\u4E86\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u73B0\u5728\u53EF\u4EE5\u5173\uFF09\u2014\u2014\u5B83\u8FDB\u64CD\u4F5C\u65E5\u5FD7\u3001\u4E5F\u662F\u4EBA\u5728\u5361\u4E0A\u770B\u5230\u7684\u5224\u65AD\u6750\u6599\u3002`;
183638
184067
  CONVERSATIONAL_MANAGER_BODY = `${CONVERSATIONAL_ETHOS}
183639
184068
 
183640
184069
  ${SHARED_GRAPH_BODY}
183641
184070
 
183642
184071
  ${CONVERSATIONAL_RECOVERY_TOOLS}
183643
184072
 
184073
+ ${CONVERSATIONAL_ESCALATION_TOOLS}
184074
+
183644
184075
  ${HIGH_RISK_COMMANDS}`;
183645
184076
  MACHINE_EXIT_CODES = /* @__PURE__ */ new Set([
183646
184077
  "output-limit",
@@ -184112,7 +184543,8 @@ ${ctx.nodeFault}
184112
184543
  ...openEscalations.length > 0 ? [
184113
184544
  `- \u8FD9\u4E2A\u8282\u70B9\u4E0A\u8FD8\u6302\u7740 **${openEscalations.length}** \u6761\u6CA1\u5173\u7684\u4E0A\u62A5\uFF08\u4EBA\u7684 inbox \u91CC\u8FD8\u770B\u5F97\u5230\uFF09\u3002**\u6839\u56E0\u771F\u9664\u4E86\u5C31\u987A\u624B\u5173\u6389**\uFF0C\u522B\u8BA9\u4EBA\u5BF9\u7740\u4E00\u4EF6\u5DF2\u7ECF\u4E0D\u5B58\u5728\u7684\u4E8B\u53D1\u6101\uFF1A`,
184114
184545
  ...openEscalations.map(
184115
- (e) => ` - \`oasis resolve-escalation ${artifactId} --escalation ${e.escalationId} --reason "<\u4E3A\u4EC0\u4E48\u73B0\u5728\u53EF\u4EE5\u5173>"\``
184546
+ // ADR 0079 B:位置参 artifactId 已去掉——escalationId 自描述所属节点,`--escalation` 就够了。
184547
+ (e) => ` - \`oasis resolve-escalation --escalation ${e.escalationId} --reason "<\u4E3A\u4EC0\u4E48\u73B0\u5728\u53EF\u4EE5\u5173>"\``
184116
184548
  ),
184117
184549
  ` \u8FD9\u6761**\u4E0D\u4F1A\u7ACB\u523B\u751F\u6548**\uFF1A\u7CFB\u7EDF\u4F1A\u8F6C\u6210\u4E00\u5F20\u5361\u7247\u3001\u7B49\u4EBA\u70B9\u786E\u8BA4\uFF08\u5173\u9519\u4E86\u7684\u540E\u679C\u662F\u5361\u6D88\u5931\u3001\u8282\u70B9\u8FD8\u51BB\u7740\uFF0C\u6240\u4EE5\u8981\u4EBA\u70B9\u5934\uFF09\u3002\u7167\u53D1\u5373\u53EF\uFF0C\u522B\u56E0\u4E3A\u5B83\u4E0D\u7ACB\u523B\u751F\u6548\u5C31\u8DF3\u8FC7\u3002`
184118
184550
  ] : [],
@@ -184442,7 +184874,6 @@ var init_src8 = __esm({
184442
184874
  init_tokens();
184443
184875
  init_collab();
184444
184876
  init_closure_report();
184445
- init_workorder_seal();
184446
184877
  init_workorders();
184447
184878
  init_workorder_detail();
184448
184879
  init_letter_read_backfill();
@@ -186683,6 +187114,14 @@ function createPgPool(dsn, opts) {
186683
187114
  connectionTimeoutMillis: opts?.connectionTimeoutMillis ?? 5e3
186684
187115
  });
186685
187116
  }
187117
+ async function dropSchemaIfExists(dsn, schema) {
187118
+ const pool = createPgPool(dsn, { max: 1 });
187119
+ try {
187120
+ await pool.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
187121
+ } finally {
187122
+ await pool.end();
187123
+ }
187124
+ }
186686
187125
  var init_pool = __esm({
186687
187126
  "../storage/src/pool.ts"() {
186688
187127
  "use strict";
@@ -189045,6 +189484,7 @@ var init_postgres_chat_sessions = __esm({
189045
189484
  await pool.query(`CREATE INDEX IF NOT EXISTS chat_sessions_analyzed_run_idx ON "${s2}".chat_sessions (analyzed_run_id, touched_at DESC)`);
189046
189485
  await pool.query(`ALTER TABLE "${s2}".chat_sessions ADD COLUMN IF NOT EXISTS runtime_kind text`);
189047
189486
  await pool.query(`ALTER TABLE "${s2}".chat_sessions ADD COLUMN IF NOT EXISTS project_id text`);
189487
+ await pool.query(`ALTER TABLE "${s2}".chat_sessions ADD COLUMN IF NOT EXISTS escalation_id text`);
189048
189488
  await pool.query(`
189049
189489
  WITH ranked AS (
189050
189490
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -189063,9 +189503,9 @@ var init_postgres_chat_sessions = __esm({
189063
189503
  }
189064
189504
  async createSession(s2) {
189065
189505
  await this.pool.query(
189066
- `INSERT INTO ${this.s}.chat_sessions (id, human_actor_id, ai_actor_id, runtime_id, runtime_kind, runtime_session_id, title, touched_at, created_at, workspace, analyzed_run_id, project_id)
189067
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)`,
189068
- [s2.id, s2.humanActorId, s2.aiActorId, s2.runtimeId, s2.runtimeKind ?? null, s2.runtimeSessionId ?? null, s2.title ?? null, s2.touchedAt, s2.createdAt, s2.workspace ?? null, s2.analyzedRunId ?? null, s2.projectId ?? null]
189506
+ `INSERT INTO ${this.s}.chat_sessions (id, human_actor_id, ai_actor_id, runtime_id, runtime_kind, runtime_session_id, title, touched_at, created_at, workspace, analyzed_run_id, project_id, escalation_id)
189507
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
189508
+ [s2.id, s2.humanActorId, s2.aiActorId, s2.runtimeId, s2.runtimeKind ?? null, s2.runtimeSessionId ?? null, s2.title ?? null, s2.touchedAt, s2.createdAt, s2.workspace ?? null, s2.analyzedRunId ?? null, s2.projectId ?? null, s2.escalationId ?? null]
189069
189509
  );
189070
189510
  }
189071
189511
  async listSessions(humanActorId) {
@@ -189286,7 +189726,8 @@ var init_postgres_chat_sessions = __esm({
189286
189726
  createdAt: new Date(row.created_at).toISOString(),
189287
189727
  workspace: row.workspace ?? null,
189288
189728
  analyzedRunId: row.analyzed_run_id ?? null,
189289
- projectId: row.project_id ?? null
189729
+ projectId: row.project_id ?? null,
189730
+ escalationId: row.escalation_id ?? null
189290
189731
  });
189291
189732
  rowToSessionWithQuality = (row) => {
189292
189733
  const quality = deriveLastTurnQuality({
@@ -190043,6 +190484,165 @@ var init_postgres_inbox_read = __esm({
190043
190484
  }
190044
190485
  });
190045
190486
 
190487
+ // ../storage/src/postgres-dispatches.ts
190488
+ function rowToRecord2(r) {
190489
+ return {
190490
+ id: r.id,
190491
+ targetKind: r.target_kind,
190492
+ targetId: r.target_id,
190493
+ workorderId: r.workorder_id,
190494
+ nodeId: r.node_id,
190495
+ actorId: r.actor_id,
190496
+ daemonId: r.daemon_id,
190497
+ jobKey: r.job_key,
190498
+ sessionRef: r.session_ref,
190499
+ createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : String(r.created_at),
190500
+ startedAt: r.started_at ? r.started_at instanceof Date ? r.started_at.toISOString() : String(r.started_at) : null,
190501
+ endedAt: r.ended_at ? r.ended_at instanceof Date ? r.ended_at.toISOString() : String(r.ended_at) : null,
190502
+ outcome: r.outcome,
190503
+ exitCode: r.exit_code,
190504
+ reason: r.reason
190505
+ };
190506
+ }
190507
+ var ident17, PostgresDispatchStore;
190508
+ var init_postgres_dispatches = __esm({
190509
+ "../storage/src/postgres-dispatches.ts"() {
190510
+ "use strict";
190511
+ ident17 = (schema) => schema.replace(/[^a-zA-Z0-9_]/g, "");
190512
+ PostgresDispatchStore = class _PostgresDispatchStore {
190513
+ constructor(pool, s2) {
190514
+ this.pool = pool;
190515
+ this.s = s2;
190516
+ }
190517
+ static async open(pool, schema = "public") {
190518
+ const s2 = ident17(schema);
190519
+ await pool.query(`CREATE SCHEMA IF NOT EXISTS "${s2}"`);
190520
+ await pool.query(`
190521
+ CREATE TABLE IF NOT EXISTS "${s2}".dispatches (
190522
+ id text PRIMARY KEY,
190523
+ target_kind text NOT NULL CHECK (target_kind IN ('work','review','chat')),
190524
+ target_id text NOT NULL,
190525
+ workorder_id text,
190526
+ node_id text,
190527
+ actor_id text NOT NULL,
190528
+ daemon_id text,
190529
+ job_key text,
190530
+ session_ref text,
190531
+ created_at timestamptz NOT NULL,
190532
+ started_at timestamptz,
190533
+ ended_at timestamptz,
190534
+ outcome text CHECK (outcome IN ('completed','failed','refused','superseded','cancelled')),
190535
+ exit_code integer,
190536
+ reason text,
190537
+ -- \u5F62\u72B6\u81EA\u6821\u9A8C\uFF1Akind \u4E0E target id \u524D\u7F00\u4E00\u4E00\u5BF9\u5E94\uFF08chat \u6D3E\u53D1\u7684\u76EE\u6807\u662F artifact:chat:<sessionId>\uFF0C
190538
+ -- \u89C1\u63D0\u6848 \xA74\uFF1Bchat \u4E3A P0 \u7559\u4F4D\u3001\u6682\u4E0D\u5199\u5165\uFF09
190539
+ CONSTRAINT dispatches_target_shape CHECK (
190540
+ (target_kind = 'work' AND target_id LIKE 'wk:%') OR
190541
+ (target_kind = 'review' AND target_id LIKE 'rv:%') OR
190542
+ (target_kind = 'chat' AND target_id LIKE 'artifact:chat:%')
190543
+ ),
190544
+ -- \u5173\u4E86\u884C\u5FC5\u6709\u8BF4\u6CD5\uFF1Aended_at \u4E0E outcome \u540C\u751F\u540C\u6B7B\uFF08\u540C works \u8868 I3 \u7684\u7EAA\u5F8B\uFF09
190545
+ CONSTRAINT dispatches_ended_needs_outcome CHECK ((ended_at IS NULL) = (outcome IS NULL))
190546
+ )`);
190547
+ await pool.query(`CREATE INDEX IF NOT EXISTS dispatches_target_idx ON "${s2}".dispatches (target_kind, target_id, created_at DESC)`);
190548
+ await pool.query(`CREATE INDEX IF NOT EXISTS dispatches_workorder_idx ON "${s2}".dispatches (workorder_id, created_at DESC)`);
190549
+ return new _PostgresDispatchStore(pool, s2);
190550
+ }
190551
+ /** write-ahead:派发意图产生的那一刻落 open 行(提案 I-D1)。同 id 重放安全(ON CONFLICT DO NOTHING)。 */
190552
+ async insertOpen(rec) {
190553
+ await this.pool.query(
190554
+ `INSERT INTO "${this.s}".dispatches (id, target_kind, target_id, workorder_id, node_id, actor_id, job_key, created_at)
190555
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (id) DO NOTHING`,
190556
+ [
190557
+ rec.id,
190558
+ rec.targetKind,
190559
+ rec.targetId,
190560
+ rec.workorderId ?? null,
190561
+ rec.nodeId ?? null,
190562
+ rec.actorId,
190563
+ rec.jobKey ?? null,
190564
+ rec.at ?? (/* @__PURE__ */ new Date()).toISOString()
190565
+ ]
190566
+ );
190567
+ }
190568
+ /** session_started 帧:回填 started_at / session_ref / daemon_id(幂等:只填第一次)。 */
190569
+ async markStarted(id, opts) {
190570
+ await this.pool.query(
190571
+ `UPDATE "${this.s}".dispatches
190572
+ SET started_at = COALESCE(started_at, $2),
190573
+ session_ref = COALESCE(session_ref, $3),
190574
+ daemon_id = COALESCE(daemon_id, $4)
190575
+ WHERE id = $1`,
190576
+ [id, opts.at ?? (/* @__PURE__ */ new Date()).toISOString(), opts.sessionRef ?? null, opts.daemonId ?? null]
190577
+ );
190578
+ }
190579
+ /**
190580
+ * 关行(幂等):只有仍 open 的行会写 ended_at/outcome/reason;已关的行只补 exit_code
190581
+ * (提案 §6 注:kill 请求时刻先关行、晚到的 exit 帧只补退出码,不改 outcome)。
190582
+ */
190583
+ async close(id, opts) {
190584
+ await this.pool.query(
190585
+ `UPDATE "${this.s}".dispatches
190586
+ SET ended_at = CASE WHEN ended_at IS NULL THEN $2::timestamptz ELSE ended_at END,
190587
+ outcome = CASE WHEN outcome IS NULL THEN $3 ELSE outcome END,
190588
+ reason = CASE WHEN ended_at IS NULL THEN $4 ELSE reason END,
190589
+ exit_code = COALESCE(exit_code, $5)
190590
+ WHERE id = $1`,
190591
+ [id, opts.at ?? (/* @__PURE__ */ new Date()).toISOString(), opts.outcome, opts.reason ?? null, opts.exitCode ?? null]
190592
+ );
190593
+ }
190594
+ /** cancel 漏斗兜底:只知道 target(workId/reviewId)时按 target 关掉在途行;返回被关的行。 */
190595
+ async closeOpenByTarget(targetKind, targetId, opts) {
190596
+ const res = await this.pool.query(
190597
+ `UPDATE "${this.s}".dispatches
190598
+ SET ended_at = $3::timestamptz, outcome = $4, reason = $5
190599
+ WHERE target_kind = $1 AND target_id = $2 AND ended_at IS NULL
190600
+ RETURNING *`,
190601
+ [targetKind, targetId, opts.at ?? (/* @__PURE__ */ new Date()).toISOString(), opts.outcome, opts.reason ?? null]
190602
+ );
190603
+ return res.rows.map(rowToRecord2);
190604
+ }
190605
+ /**
190606
+ * 按工单取全部派发行——timeline 视图一次捞整单,在内存里把 work/review 映射到 run
190607
+ * (`dispatches.id` = `agent_runs.id`),避免逐行查库。同一 target 多轮取**最新一条**由调用方决定。
190608
+ */
190609
+ async listByWorkorder(workorderId) {
190610
+ const res = await this.pool.query(
190611
+ `SELECT * FROM "${this.s}".dispatches WHERE workorder_id = $1 ORDER BY created_at`,
190612
+ [workorderId]
190613
+ );
190614
+ return res.rows.map(rowToRecord2);
190615
+ }
190616
+ async getById(id) {
190617
+ const res = await this.pool.query(`SELECT * FROM "${this.s}".dispatches WHERE id = $1`, [id]);
190618
+ return res.rows[0] ? rowToRecord2(res.rows[0]) : null;
190619
+ }
190620
+ /** 从 target 找在途派发(P1 起 cancelSession 的映射兜底:内存 Map 丢了还能从表里找回 daemon_id 去杀)。 */
190621
+ async findOpenByTarget(targetKind, targetId) {
190622
+ const res = await this.pool.query(
190623
+ `SELECT * FROM "${this.s}".dispatches WHERE target_kind = $1 AND target_id = $2 AND ended_at IS NULL ORDER BY created_at`,
190624
+ [targetKind, targetId]
190625
+ );
190626
+ return res.rows.map(rowToRecord2);
190627
+ }
190628
+ /**
190629
+ * 对账兜底(提案 §6 末行):派发意图落了行、却从未 started 也从未关行的(如人类节点的派发——
190630
+ * 无 daemon 会话、exit 帧永不来,正是 dispatch-to-work.json 死账泄漏的形状)→ 关成 failed/never-started。
190631
+ * 返回关掉的行数,调用方决定要不要告警。
190632
+ */
190633
+ async closeStaleNeverStarted(olderThanMs, at) {
190634
+ const res = await this.pool.query(
190635
+ `UPDATE "${this.s}".dispatches
190636
+ SET ended_at = $1::timestamptz, outcome = 'failed', reason = 'never-started'
190637
+ WHERE started_at IS NULL AND ended_at IS NULL AND created_at < $1::timestamptz - ($2 || ' milliseconds')::interval`,
190638
+ [at ?? (/* @__PURE__ */ new Date()).toISOString(), String(olderThanMs)]
190639
+ );
190640
+ return res.rowCount ?? 0;
190641
+ }
190642
+ };
190643
+ }
190644
+ });
190645
+
190046
190646
  // ../storage/src/index.ts
190047
190647
  var src_exports = {};
190048
190648
  __export(src_exports, {
@@ -190054,6 +190654,7 @@ __export(src_exports, {
190054
190654
  PostgresChannelStore: () => PostgresChannelStore,
190055
190655
  PostgresChatSessionStore: () => PostgresChatSessionStore,
190056
190656
  PostgresControlPlaneStore: () => PostgresControlPlaneStore,
190657
+ PostgresDispatchStore: () => PostgresDispatchStore,
190057
190658
  PostgresHumanPrefsStore: () => PostgresHumanPrefsStore,
190058
190659
  PostgresModelPriceStore: () => PostgresModelPriceStore,
190059
190660
  PostgresNodeStore: () => PostgresNodeStore,
@@ -190066,6 +190667,7 @@ __export(src_exports, {
190066
190667
  PostgresTypeRegistryStore: () => PostgresTypeRegistryStore,
190067
190668
  backfillTypeRegistryFromFile: () => backfillTypeRegistryFromFile,
190068
190669
  createPgPool: () => createPgPool,
190670
+ dropSchemaIfExists: () => dropSchemaIfExists,
190069
190671
  hasPgUnstorable: () => hasPgUnstorable,
190070
190672
  scrubPgJson: () => scrubPgJson,
190071
190673
  scrubPgString: () => scrubPgString
@@ -190089,6 +190691,7 @@ var init_src9 = __esm({
190089
190691
  init_postgres_type_registry();
190090
190692
  init_postgres_actor_memory();
190091
190693
  init_postgres_inbox_read();
190694
+ init_postgres_dispatches();
190092
190695
  }
190093
190696
  });
190094
190697
 
@@ -190838,6 +191441,16 @@ async function startServe(opts) {
190838
191441
  convergenceUnified
190839
191442
  });
190840
191443
  const kernel = newEngine.kernel;
191444
+ const dispatchLedger = await PostgresDispatchStore.open(pgPool, pgSchema);
191445
+ const ledgerWarn = (label) => (err) => console.warn(`[dispatch-ledger] ${label}: ${String(err)}`);
191446
+ {
191447
+ const t = setInterval(() => {
191448
+ dispatchLedger.closeStaleNeverStarted(10 * 6e4).then((n) => {
191449
+ if (n > 0) console.log(`[dispatch-ledger] \u5BF9\u8D26\uFF1A\u5173\u95ED ${n} \u6761 never-started \u6D3E\u53D1\u884C`);
191450
+ }).catch(ledgerWarn("\u5BF9\u8D26"));
191451
+ }, 5 * 6e4);
191452
+ t.unref();
191453
+ }
190841
191454
  console.log(`[serve] \u2605 \u65B0\u5F15\u64CE\u5DF2\u542F\u7528\uFF08schema=engine\uFF09\u2014\u2014 14 \u8868\u5173\u7CFB\u6A21\u578B + scan \u8C03\u5EA6 + \u4E09\u5C42\u81EA\u6108`);
190842
191455
  console.log(
190843
191456
  `[serve] \u6536\u655B\u95F8\uFF08convergence-unified\uFF09\uFF1A${convergenceUnified ? `\u26A0 \u5DF2\u542F\u7528 \u2014\u2014 manual conclude \u6321\u7EAF\u6765\u4FE1\u3001\u9690\u5F62\u6765\u4FE1\u6FC0\u6D3B\u3001\u56DB\u8F74\u5BF9\u79F0\u3001park \u5168\u8F74\u8C41\u514D\uFF08${process.env.NODE_ENV === "production" ? "prod \u663E\u5F0F\u5F00\u542F" : "\u672C\u5730\u9ED8\u8BA4\u5F00"}\uFF09` : "\u5173 \u2014\u2014 \u73B0\u72B6\u884C\u4E3A\uFF08prod \u9996\u5468\u9ED8\u8BA4\u5173\uFF0C\u8BBE OASIS_CONVERGENCE_UNIFIED=1 \u663E\u5F0F\u5F00\u542F\uFF09"}`
@@ -191485,6 +192098,8 @@ async function startServe(opts) {
191485
192098
  ...engine.assets ? { assets: engine.assets } : {},
191486
192099
  // ★ 新引擎 store:blob GC 从 work_artifacts 扫根(事实源=实体表,oplog 已退役)。
191487
192100
  engineStore: engine.kernel.getStore?.(),
192101
+ // ★ 派发台账(proposals/dispatch-ledger):timeline 据此把 work/review 硬关联到 agent_runs。
192102
+ dispatchesOfWorkorder: (wid) => dispatchLedger.listByWorkorder(wid),
191488
192103
  // ★ 事件流:bus.onEvent 驱动 /api/events SSE(替代 oplog.subscribe);recentEvents 回放/调试。
191489
192104
  engineEvents: (fn) => {
191490
192105
  const bus = engine.kernel.getBus?.();
@@ -192331,6 +192946,7 @@ async function startServe(opts) {
192331
192946
  if (msg.type === "hello" && msg.meta.activeSessions?.length) {
192332
192947
  enqueueChatRecovery("hello \u5BF9\u8D26", () => recoverChatTurns(daemonId, msg.meta.activeSessions));
192333
192948
  } else if (msg.type === "session_started") {
192949
+ dispatchLedger.markStarted(msg.dispatchId, { sessionRef: msg.sessionId, daemonId }).catch(ledgerWarn("markStarted"));
192334
192950
  const prodMapping = dispatchToWork.get(msg.dispatchId);
192335
192951
  if (prodMapping && busRef) {
192336
192952
  workToSession.set(prodMapping.workId, { dispatchId: msg.dispatchId, daemonId });
@@ -192362,6 +192978,11 @@ async function startServe(opts) {
192362
192978
  }).catch((err) => console.error(`[new-engine] review.started submit \u5931\u8D25:`, err));
192363
192979
  }
192364
192980
  } else if (msg.type === "session_exited") {
192981
+ dispatchLedger.close(msg.dispatchId, {
192982
+ outcome: msg.info.code === 0 ? "completed" : "failed",
192983
+ exitCode: msg.info.code ?? null,
192984
+ reason: msg.info.reason ?? null
192985
+ }).catch(ledgerWarn("close(exit)"));
192365
192986
  const dispatchOwnedAtReceipt = allDispatchers().some((d) => d.inFlightSessions().some((x2) => x2.sessionId === msg.dispatchId));
192366
192987
  enqueueChatRecovery("exit \u5BF9\u8D26", async () => {
192367
192988
  const chatHandled = await reconcileChatExitFrame({
@@ -192391,7 +193012,6 @@ async function startServe(opts) {
192391
193012
  }
192392
193013
  }).catch((err) => console.error(`[new-engine] work.response submit \u5931\u8D25:`, err));
192393
193014
  dispatchToWork.delete(msg.dispatchId);
192394
- saveDispatchToWork();
192395
193015
  if (mapping) workToSession.delete(mapping.workId);
192396
193016
  } else if (busRef) {
192397
193017
  await fallbackDispatchExitToWork(msg, busRef);
@@ -192452,27 +193072,34 @@ async function startServe(opts) {
192452
193072
  chatRemoteAdapter = new DaemonHubAdapter(hub, { ...hubAdapterOpts, stashUnknownFrames: true });
192453
193073
  dispatchRemoteAdapter = new DaemonHubAdapter(hub, { ...hubAdapterOpts, stashUnknownFrames: true });
192454
193074
  let newEngineProduce = null;
193075
+ let newEngineReplyWork = null;
192455
193076
  let newEngineReview = null;
192456
193077
  const dispatchedWorks = /* @__PURE__ */ new Set();
192457
193078
  const dispatchedReviews = /* @__PURE__ */ new Set();
192458
193079
  const dispatchToWork = /* @__PURE__ */ new Map();
192459
- const dispatchToWorkFile = path19.join(opts.dir, "dispatch-to-work.json");
192460
- function loadDispatchToWork() {
193080
+ async function rebuildDispatchMappings(store) {
192461
193081
  try {
192462
- if (fs25.existsSync(dispatchToWorkFile)) {
192463
- const data = JSON.parse(fs25.readFileSync(dispatchToWorkFile, "utf8"));
192464
- for (const [k2, v2] of Object.entries(data)) dispatchToWork.set(k2, v2);
192465
- console.log(`[new-engine] \u4ECE\u65C1\u8D26\u6062\u590D ${Object.keys(data).length} \u6761 dispatch\u2192work \u6620\u5C04`);
193082
+ const ids2 = await store.transaction((tx) => tx.listActiveWorkorders());
193083
+ let works = 0, reviews = 0;
193084
+ for (const wid of ids2) {
193085
+ const snap = await store.transaction((tx) => tx.loadWorkorder(wid));
193086
+ if (!snap) continue;
193087
+ for (const w2 of snap.works) {
193088
+ if (!w2.endedAt && !w2.deadAt && w2.sessionRef) {
193089
+ dispatchToWork.set(w2.sessionRef, { workId: w2.id, workorderId: wid });
193090
+ works++;
193091
+ }
193092
+ }
193093
+ for (const r of snap.reviews) {
193094
+ if (!r.endedAt && !r.cancelledAt && !r.verdict && r.sessionRef) {
193095
+ dispatchToReview.set(r.sessionRef, { reviewId: r.id, workorderId: wid, nodeId: r.nodeId });
193096
+ reviews++;
193097
+ }
193098
+ }
192466
193099
  }
193100
+ if (works || reviews) console.log(`[new-engine] \u4ECE\u5B9E\u4F53\u8868\u91CD\u5EFA dispatch \u6620\u5C04\uFF1Awork ${works} \u6761\u3001review ${reviews} \u6761`);
192467
193101
  } catch (err) {
192468
- console.warn(`[new-engine] \u6062\u590D dispatch\u2192work \u6620\u5C04\u5931\u8D25: ${String(err)}`);
192469
- }
192470
- }
192471
- function saveDispatchToWork() {
192472
- try {
192473
- fs25.writeFileSync(dispatchToWorkFile, JSON.stringify(Object.fromEntries(dispatchToWork), null, 2), { mode: 384 });
192474
- } catch (err) {
192475
- console.warn(`[new-engine] \u4FDD\u5B58 dispatch\u2192work \u6620\u5C04\u5931\u8D25: ${String(err)}`);
193102
+ console.warn(`[new-engine] \u91CD\u5EFA dispatch \u6620\u5C04\u5931\u8D25: ${String(err)}`);
192476
193103
  }
192477
193104
  }
192478
193105
  const workToSession = /* @__PURE__ */ new Map();
@@ -192481,7 +193108,7 @@ async function startServe(opts) {
192481
193108
  let busRef = null;
192482
193109
  async function fallbackDispatchExitToWork(msg, bus) {
192483
193110
  try {
192484
- const entry = readMetricJournal().find(
193111
+ const entry = journalRing.find(
192485
193112
  (e) => {
192486
193113
  if (e.kind !== "dispatched" && e.kind !== "recovered") return false;
192487
193114
  return e.dispatchId === msg.dispatchId;
@@ -192523,7 +193150,10 @@ async function startServe(opts) {
192523
193150
  {
192524
193151
  const newKernel = newEngine.kernel;
192525
193152
  if (newKernel && newKernel.setEngineIO) {
192526
- loadDispatchToWork();
193153
+ {
193154
+ const engStore = newKernel.getStore?.();
193155
+ if (engStore) void rebuildDispatchMappings(engStore);
193156
+ }
192527
193157
  newKernel.setEngineIO({
192528
193158
  async dispatchWork(workId) {
192529
193159
  if (!newEngineProduce) return;
@@ -192534,13 +193164,29 @@ async function startServe(opts) {
192534
193164
  const rev = kernelModel.revisions.get(workId);
192535
193165
  if (!rev) return;
192536
193166
  if (rev.state === "merged") return;
192537
- console.log(`[new-engine] dispatchWork ${workId} \u2192 ${rev.artifactId} / ${rev.author}`);
193167
+ const replyIssueId = rev.replyToIssueId ?? null;
193168
+ console.log(`[new-engine] dispatchWork ${workId} \u2192 ${rev.artifactId} / ${rev.author}${replyIssueId ? `\uFF08\u56DE\u4FE1 ${replyIssueId}\uFF09` : ""}`);
192538
193169
  const art = kernelModel.artifacts.get(rev.artifactId);
192539
193170
  const wid = art?.workspace ?? "";
193171
+ const ledgerId = `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
193172
+ try {
193173
+ await dispatchLedger.insertOpen({
193174
+ id: ledgerId,
193175
+ targetKind: "work",
193176
+ targetId: workId,
193177
+ workorderId: wid || null,
193178
+ nodeId: rev.artifactId,
193179
+ actorId: rev.author,
193180
+ jobKey: replyIssueId ? `reply::${replyIssueId}::${rev.author}` : `produce::${rev.artifactId}`
193181
+ });
193182
+ } catch (err) {
193183
+ ledgerWarn("insertOpen(work)")(err);
193184
+ }
192540
193185
  let result;
192541
193186
  try {
192542
- result = await newEngineProduce(rev.artifactId, rev.author);
193187
+ result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, ledgerId);
192543
193188
  } catch (err) {
193189
+ dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` }).catch(ledgerWarn("close(error)"));
192544
193190
  console.log(`[new-engine] dispatchWork ${workId} \u6D3E\u53D1\u5F02\u5E38\uFF1A${String(err)} \u2192 \u7F6E work failed`);
192545
193191
  if (busRef) {
192546
193192
  await busRef.submit({
@@ -192560,9 +193206,9 @@ async function startServe(opts) {
192560
193206
  }
192561
193207
  if (result?.dispatchId) {
192562
193208
  dispatchToWork.set(result.dispatchId, { workId, workorderId: wid });
192563
- saveDispatchToWork();
192564
193209
  dispatchedWorks.add(workId);
192565
193210
  } else if (result?.kind === "spawn-failed" || result?.kind === "predispatch-timeout") {
193211
+ dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch ${result.kind}` }).catch(ledgerWarn("close(spawn-failed)"));
192566
193212
  console.log(`[new-engine] dispatchWork ${workId} \u6D3E\u53D1\u5931\u8D25\uFF08${result.kind}\uFF09\u2192 \u7F6E work failed`);
192567
193213
  if (busRef) {
192568
193214
  await busRef.submit({
@@ -192579,6 +193225,7 @@ async function startServe(opts) {
192579
193225
  });
192580
193226
  }
192581
193227
  } else if (result?.kind) {
193228
+ dispatchLedger.close(ledgerId, { outcome: "superseded", reason: result.kind }).catch(ledgerWarn("close(skipped)"));
192582
193229
  console.log(`[new-engine] dispatchWork ${workId} \u672C\u8F6E\u672A\u6D3E\u51FA\uFF08${result.kind}\uFF09\u2014\u2014\u4FDD\u6301 running\uFF0Cscan \u4E0B\u8F6E\u91CD\u8BD5`);
192583
193230
  }
192584
193231
  rev.state = "working";
@@ -192588,11 +193235,36 @@ async function startServe(opts) {
192588
193235
  if (dispatchedReviews.has(reviewId)) return;
192589
193236
  dispatchedReviews.add(reviewId);
192590
193237
  console.log(`[new-engine] dispatchReview ${reviewId} \u2192 ${nodeId} / ${reviewerActorId}`);
192591
- const result = await newEngineReview(nodeId, reviewerActorId);
193238
+ const kernelModel = newKernel.model;
193239
+ const art = kernelModel.artifacts.get(nodeId);
193240
+ const revWid = art?.workspace ?? "";
193241
+ const ledgerId = `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
193242
+ try {
193243
+ await dispatchLedger.insertOpen({
193244
+ id: ledgerId,
193245
+ targetKind: "review",
193246
+ targetId: reviewId,
193247
+ workorderId: revWid || null,
193248
+ nodeId,
193249
+ actorId: reviewerActorId,
193250
+ jobKey: `review::${nodeId}::${reviewerActorId}`
193251
+ });
193252
+ } catch (err) {
193253
+ ledgerWarn("insertOpen(review)")(err);
193254
+ }
193255
+ let result;
193256
+ try {
193257
+ result = await newEngineReview(nodeId, reviewerActorId, ledgerId);
193258
+ } catch (err) {
193259
+ dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` }).catch(ledgerWarn("close(review-error)"));
193260
+ throw err;
193261
+ }
192592
193262
  if (result?.dispatchId) {
192593
- const kernelModel = newKernel.model;
192594
- const art = kernelModel.artifacts.get(nodeId);
192595
- dispatchToReview.set(result.dispatchId, { reviewId, workorderId: art?.workspace ?? "", nodeId });
193263
+ dispatchToReview.set(result.dispatchId, { reviewId, workorderId: revWid, nodeId });
193264
+ } else if (result?.kind === "spawn-failed" || result?.kind === "predispatch-timeout") {
193265
+ dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch ${result.kind}` }).catch(ledgerWarn("close(review-spawn-failed)"));
193266
+ } else if (result?.kind) {
193267
+ dispatchLedger.close(ledgerId, { outcome: "superseded", reason: result.kind }).catch(ledgerWarn("close(review-skipped)"));
192596
193268
  }
192597
193269
  },
192598
193270
  async cancelSession(ref2) {
@@ -192601,6 +193273,7 @@ async function startServe(opts) {
192601
193273
  console.log(`[new-engine] cancelSession workId=${ref2} dispatchId=${workSess.dispatchId} daemon=${workSess.daemonId}`);
192602
193274
  hub.dispatch(workSess.daemonId, { type: "kill", dispatchId: workSess.dispatchId });
192603
193275
  workToSession.delete(ref2);
193276
+ dispatchLedger.close(workSess.dispatchId, { outcome: "cancelled", reason: "session-cancelled" }).catch(ledgerWarn("close(cancel-work)"));
192604
193277
  return;
192605
193278
  }
192606
193279
  const reviewSess = reviewToSession.get(ref2);
@@ -192608,6 +193281,18 @@ async function startServe(opts) {
192608
193281
  console.log(`[new-engine] cancelSession reviewId=${ref2} dispatchId=${reviewSess.dispatchId} daemon=${reviewSess.daemonId}`);
192609
193282
  hub.dispatch(reviewSess.daemonId, { type: "kill", dispatchId: reviewSess.dispatchId });
192610
193283
  reviewToSession.delete(ref2);
193284
+ dispatchLedger.close(reviewSess.dispatchId, { outcome: "cancelled", reason: "session-cancelled" }).catch(ledgerWarn("close(cancel-review)"));
193285
+ return;
193286
+ }
193287
+ try {
193288
+ const kind = ref2.startsWith("rv:") ? "review" : "work";
193289
+ const closed = await dispatchLedger.closeOpenByTarget(kind, ref2, { outcome: "cancelled", reason: "session-cancelled (map-miss)" });
193290
+ for (const row of closed) {
193291
+ if (row.daemonId && hub) hub.dispatch(row.daemonId, { type: "kill", dispatchId: row.id });
193292
+ }
193293
+ if (closed.length) console.log(`[dispatch-ledger] cancelSession map-miss\uFF0C\u6309\u53F0\u8D26\u5173\u884C ${closed.length} \u6761\uFF08${ref2}\uFF09`);
193294
+ } catch (err) {
193295
+ ledgerWarn("cancel(map-miss)")(err);
192611
193296
  }
192612
193297
  },
192613
193298
  async notify(_actorIds, _payload) {
@@ -192892,12 +193577,16 @@ async function startServe(opts) {
192892
193577
  });
192893
193578
  const slot = { dispatcher: d, logged: 0, ticking: false };
192894
193579
  dispatchers.set(companyId, slot);
192895
- newEngineProduce = async (artifactId, actorId) => {
192896
- const result = await d.requestProduce({ artifactId, actor: actorId, bypassScheduling: true });
193580
+ newEngineProduce = async (artifactId, actorId, dispatchId) => {
193581
+ const result = await d.requestProduce({ artifactId, actor: actorId, bypassScheduling: true, ...dispatchId !== void 0 ? { dispatchId } : {} });
193582
+ return { kind: result.kind, dispatchId: result.kind === "dispatched" ? result.dispatchId : void 0 };
193583
+ };
193584
+ newEngineReplyWork = async (artifactId, actorId, issueId, dispatchId) => {
193585
+ const result = await d.requestReplyWork({ artifactId, actor: actorId, annotationId: issueId, bypassScheduling: true, ...dispatchId !== void 0 ? { dispatchId } : {} });
192897
193586
  return { kind: result.kind, dispatchId: result.kind === "dispatched" ? result.dispatchId : void 0 };
192898
193587
  };
192899
- newEngineReview = async (artifactId, actorId) => {
192900
- const result = await d.requestReview({ artifactId, actor: actorId, bypassScheduling: true });
193588
+ newEngineReview = async (artifactId, actorId, dispatchId) => {
193589
+ const result = await d.requestReview({ artifactId, actor: actorId, bypassScheduling: true, ...dispatchId !== void 0 ? { dispatchId } : {} });
192901
193590
  return { kind: result.kind, dispatchId: result.kind === "dispatched" ? result.dispatchId : void 0 };
192902
193591
  };
192903
193592
  };
@@ -193874,6 +194563,9 @@ var HANDSHAKE_TIMEOUT_MS = 15e3;
193874
194563
  var OUTBOX_TTL_MS = 5 * 6e4;
193875
194564
  var OUTBOX_MAX = 500;
193876
194565
  var PENDING_STREAM_MAX = 2e3;
194566
+ var AUTH_REJECT_BACKOFF_MAX_MS = 15 * 6e4;
194567
+ var AUTH_REJECT_GIVE_UP_AFTER = 3;
194568
+ var AUTH_REJECT_RE = /Unexpected server response:\s*(401|403)/;
193877
194569
  async function preflightOasisAccess(job) {
193878
194570
  const base = job.server.url.replace(/\/+$/, "");
193879
194571
  try {
@@ -193910,6 +194602,8 @@ var DaemonWsClient = class {
193910
194602
  this.handshakeTimeoutMs = opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS;
193911
194603
  this.outboxMax = opts.outboxMaxFrames ?? OUTBOX_MAX;
193912
194604
  this.workRoot = opts.workRoot;
194605
+ this.authRejectGiveUpAfter = opts.authRejectGiveUpAfter ?? AUTH_REJECT_GIVE_UP_AFTER;
194606
+ this.onAuthGiveUp = opts.onAuthGiveUp ?? (() => process.exit(0));
193913
194607
  }
193914
194608
  ws = null;
193915
194609
  pingTimer = null;
@@ -193917,11 +194611,20 @@ var DaemonWsClient = class {
193917
194611
  /** 收到 update 但有活跃会话时置真:延后到所有会话结束再自更新,避免打断执行中的 agent。 */
193918
194612
  pendingUpdate = false;
193919
194613
  /**
193920
- * 回执 outbox(proposal run-ledger §4.2):闭合类关键帧(session_exited)发出后先留一份,收到
194614
+ * 回执 outbox(proposal run-ledger §4.2):**服务端据以判决的帧**发出后先留一份,收到
193921
194615
  * 服务端 ack 才删;ws 抖动坏窗口发失败/丢失的帧,在重连时全量重发——投递升级 at-least-once,
193922
194616
  * 幽灵派发/身份拔河那类"帧掉进缝里"不再永久丢。内存态即可根治 ws 闪断(节点仍在);节点进程
193923
194617
  * 重启丢 outbox 的更窄场景本就被服务端 L3 时钟扫描兜底(重启会话已死),故不落盘。按 dispatchId 键
193924
- * (一次派发至多一个闭合帧,重复 ack 无害)。
194618
+ * (一次派发的判决帧按生命周期先后覆盖:started → exited,后者信息严格更强,重复 ack 无害)。
194619
+ *
194620
+ * **范围为什么不只是 session_exited**(2026-08-13 生产实证):本 outbox 落地时(§4.2,07-07 17:17)
194621
+ * 的问题定义是「退出帧丢 → run 永久 running → 僵尸」,故只收闭合类。但就在两小时前(§4.4,07-07 15:13)
194622
+ * 落地的 dispatch-ack 为了**不动契约**,选了「复用既有帧」变体——超时未见 `session_started` 即判派发
194623
+ * 丢失。这一步把 session_started 从"可有可无的正向信号"升格成"判定这次派发死活的唯一凭据",而当时
194624
+ * 没人回头改这里的范围。后果实测:节点 0.4~8 秒就起了会话并发了 started 帧,因闪断丢在路上,服务端
194625
+ * 15s 判幽灵、用户看到"对话失败",而那条会话又干净地跑了 260 秒(chat:7bf874f7)。
194626
+ * 判据一句话:**凡是服务端拿来做判决的帧,都必须走 outbox**;只做遥测/展示的(output/telemetry)走
194627
+ * sendStream 即可。
193925
194628
  */
193926
194629
  outbox = /* @__PURE__ */ new Map();
193927
194630
  /** ADR-0053 D4:在途的 GC 产物状态查询(requestId → resolver)。 */
@@ -193934,6 +194637,13 @@ var DaemonWsClient = class {
193934
194637
  silenceTimeoutMs;
193935
194638
  reconnectDelayMs;
193936
194639
  handshakeTimeoutMs;
194640
+ /** 连续被认证拒绝的次数;任何一次成功 open 即清零(见 AUTH_REJECT_* 注释)。 */
194641
+ authRejectStreak = 0;
194642
+ /** 本次连接尝试是否因握手 401/403 失败——error 事件里置、close 事件里消费。 */
194643
+ lastErrorWasAuthReject = false;
194644
+ authRejectGiveUpAfter;
194645
+ /** 放弃时的动作;默认干净退出(exit 0),测试注入以免打死测试进程。 */
194646
+ onAuthGiveUp;
193937
194647
  /**
193938
194648
  * 工作区根,供 chat 文件浏览 handler 定位会话目录。缺省 undefined = 走 adapter 同款默认根
193939
194649
  * (`~/.oasis/work`)——production 节点 adapter 也不带自定义 workRoot,两者恒同根。
@@ -193966,6 +194676,7 @@ var DaemonWsClient = class {
193966
194676
  this.ws = ws;
193967
194677
  ws.on("open", () => {
193968
194678
  log2("[node-cli]", `connected id=${this.daemonId}`);
194679
+ this.authRejectStreak = 0;
193969
194680
  this.lastReceivedAt = Date.now();
193970
194681
  this.send({ type: "hello", daemonId: this.daemonId, meta: this.buildMeta() });
193971
194682
  this.flushPendingStream();
@@ -193994,12 +194705,32 @@ var DaemonWsClient = class {
193994
194705
  clearInterval(this.pingTimer);
193995
194706
  this.pingTimer = null;
193996
194707
  }
193997
- if (!this.stopped) {
194708
+ if (this.stopped) return;
194709
+ const authRejected = this.lastErrorWasAuthReject;
194710
+ this.lastErrorWasAuthReject = false;
194711
+ if (!authRejected) {
194712
+ this.authRejectStreak = 0;
193998
194713
  log2("[node-cli]", `disconnected \u2014 reconnecting in ${this.reconnectDelayMs}ms`);
193999
194714
  setTimeout(() => this.connect(), this.reconnectDelayMs);
194715
+ return;
194716
+ }
194717
+ this.authRejectStreak += 1;
194718
+ if (this.authRejectStreak >= this.authRejectGiveUpAfter) {
194719
+ log2("[node-cli]", `\u670D\u52A1\u7AEF\u8FDE\u7EED ${this.authRejectStreak} \u6B21\u62D2\u7EDD\u672C\u8282\u70B9\u7684\u51ED\u636E\uFF08\u63E1\u624B 401/403\uFF09\u2014\u2014\u672C\u673A\u7684 node token \u5DF2\u5931\u6548\uFF0C\u6216\u8BE5\u8282\u70B9\u5DF2\u5728\u63A7\u5236\u53F0\u88AB\u79FB\u9664\u3002`);
194720
+ log2("[node-cli]", ` \u91CD\u8BD5\u4E0D\u4F1A\u8BA9\u5B83\u53D8\u597D\uFF1A\u8BF7\u5728 Oasis \u63A7\u5236\u53F0\u91CD\u65B0\u751F\u6210\u672C\u8282\u70B9\u7684\u5165\u7F51\u811A\u672C\uFF0C\u5E76\u5728\u672C\u673A\u91CD\u65B0\u8DD1\u4E00\u904D\u3002`);
194721
+ log2("[node-cli]", ` \u5B88\u62A4\u8FDB\u7A0B\u5C31\u6B64\u5E72\u51C0\u9000\u51FA\uFF08exit 0\uFF09\uFF0Csystemd \u7684 Restart=on-failure \u4E0D\u4F1A\u628A\u5B83\u62C9\u8D77\u6765\u3002`);
194722
+ this.stopped = true;
194723
+ this.onAuthGiveUp();
194724
+ return;
194000
194725
  }
194726
+ const delay = Math.min(this.reconnectDelayMs * 2 ** this.authRejectStreak, AUTH_REJECT_BACKOFF_MAX_MS);
194727
+ log2("[node-cli]", `\u8BA4\u8BC1\u88AB\u62D2\uFF08\u7B2C ${this.authRejectStreak} \u6B21\uFF09\u2014\u2014\u51ED\u636E\u53EF\u80FD\u5DF2\u5931\u6548\uFF1Breconnecting in ${delay}ms`);
194728
+ setTimeout(() => this.connect(), delay);
194729
+ });
194730
+ ws.on("error", (err) => {
194731
+ log2("[node-cli]", `ws error: ${err.message}`);
194732
+ if (AUTH_REJECT_RE.test(err.message)) this.lastErrorWasAuthReject = true;
194001
194733
  });
194002
- ws.on("error", (err) => log2("[node-cli]", `ws error: ${err.message}`));
194003
194734
  }
194004
194735
  async handleMessage(msg) {
194005
194736
  switch (msg.type) {
@@ -194033,7 +194764,7 @@ var DaemonWsClient = class {
194033
194764
  ...job.action ? { action: job.action } : {}
194034
194765
  });
194035
194766
  log2("[node-cli]", ` \u2192 session_started id=${handle.id} setup=${Date.now() - t0}ms`);
194036
- this.send({ type: "session_started", dispatchId, sessionId: handle.id, jobArtifactId: job.artifactId });
194767
+ this.sendKeyFrame(dispatchId, { type: "session_started", dispatchId, sessionId: handle.id, jobArtifactId: job.artifactId });
194037
194768
  handle.onOutput?.((chunk) => this.sendStream({ type: "session_output", dispatchId, chunk }));
194038
194769
  handle.onTelemetry?.((event) => this.sendStream({ type: "session_event", dispatchId, event }));
194039
194770
  handle.onExit((info) => {
@@ -194165,8 +194896,14 @@ var DaemonWsClient = class {
194165
194896
  for (const msg of this.pendingStream.splice(0)) this.send(msg);
194166
194897
  }
194167
194898
  /**
194168
- * 发送闭合类关键帧(session_exited):先入 outbox(收到 ack 前不删),再尝试发。ws 非 OPEN 时 send
194169
- * 静默丢弃、但帧留在 outbox,重连时 resendOutbox 全量重发;收到服务端 ack 才 outbox.delete。
194899
+ * 发送判决类关键帧(session_started / session_exited):先入 outbox(收到 ack 前不删),再尝试发。
194900
+ * ws 非 OPEN 时 send 静默丢弃、但帧留在 outbox,重连时 resendOutbox 全量重发;收到服务端 ack 才
194901
+ * outbox.delete。
194902
+ *
194903
+ * 服务端当前只对 session_exited 回 ack(daemon-hub),故 started 那条要么被同 dispatchId 的 exited
194904
+ * 覆盖(正常路径,秒级到分钟级),要么在重连时被 TTL 淘汰——**不给 started 单独加 ack 是有意的**:
194905
+ * ack 帧只带 dispatchId、不带类型,服务端若为 started 回 ack,节点会误删同槽位上排队的 exited,
194906
+ * 把一个窄的丢帧窗口换成另一个。等价能力已由"覆盖 + TTL"提供,不值得为它动契约。
194170
194907
  */
194171
194908
  sendKeyFrame(dispatchId, frame) {
194172
194909
  if (this.outbox.size >= this.outboxMax && !this.outbox.has(dispatchId)) {
@@ -195408,16 +196145,16 @@ var COMMAND_DECLS = {
195408
196145
  examples: ["oasis resolve-gap artifact:dev:abc --gap gap-001"]
195409
196146
  },
195410
196147
  "resolve-escalation": {
195411
- usage: "oasis resolve-escalation <artifactId> --escalation <id> [--reason <t>]",
195412
- description: "\u89E3\u51B3\u4E00\u4E2A\u5347\u7EA7\u3002",
195413
- positional: [
195414
- { name: "<artifactId>", required: true, desc: "\u4EA7\u7269 id" }
195415
- ],
196148
+ usage: "oasis resolve-escalation [--escalation <id>] [--reason <t>]",
196149
+ description: "\u5173\u95ED\u4E00\u6761\u4E0A\u62A5\uFF08escalation\uFF09\u3002\u8F6C\u6210\u5F85\u4EBA\u786E\u8BA4\u7684\u5361\u7247\u3001\u4E0D\u76F4\u63A5\u751F\u6548\u3002",
195416
196150
  flags: [
195417
- { name: "escalation", desc: "\u5347\u7EA7 id", required: true },
195418
- { name: "reason", desc: "\u89E3\u51B3\u539F\u56E0" }
196151
+ { name: "escalation", desc: "\u4E0A\u62A5 id\uFF08\u5F62\u5982 esc:<artifactId>:\u2026\uFF0C\u89C1 `oasis status` \u7684\u300C\u4E0A\u62A5\u300D\u6BB5\uFF09\u3002\u4ECE\u4E0A\u62A5\u5361\u300C\u6C9F\u901A\u804A\u804A\u300D\u5F00\u7684\u4F1A\u8BDD\u91CC\u53EF\u7701\u7565\u2014\u2014\u670D\u52A1\u7AEF\u636E\u4F1A\u8BDD\u89E3\u6790\uFF1B\u81EA\u4E3B\u5206\u8BCA\u4EFB\u52A1\u4E66\u4F1A\u76F4\u63A5\u7ED9\u4F60" },
196152
+ { name: "reason", desc: "\u4E3A\u4EC0\u4E48\u73B0\u5728\u53EF\u4EE5\u5173\uFF08\u8FDB\u64CD\u4F5C\u65E5\u5FD7\uFF0C\u4E5F\u662F\u4EBA\u5728\u786E\u8BA4\u5361\u4E0A\u770B\u5230\u7684\u5224\u65AD\u6750\u6599\uFF09" }
195419
196153
  ],
195420
- examples: ["oasis resolve-escalation artifact:dev:abc --escalation esc-001"]
196154
+ examples: [
196155
+ 'oasis resolve-escalation --reason "\u51ED\u8BC1\u8865\u4E0A\u4E86"',
196156
+ 'oasis resolve-escalation --escalation esc:artifact:cd:abc:9f2c1e --reason "\u5DF2\u6362\u673A\u5668"'
196157
+ ]
195421
196158
  },
195422
196159
  // —— 治理(§11) ——
195423
196160
  hold: {
@@ -195442,17 +196179,6 @@ var COMMAND_DECLS = {
195442
196179
  ],
195443
196180
  examples: ['oasis release artifact:dev:abc --reason "\u4E0A\u6E38\u5DF2\u66F4\u65B0"']
195444
196181
  },
195445
- reopen: {
195446
- usage: "oasis reopen <artifactId> [--note <t>]",
195447
- description: "\u89E3\u5C01\u4EA7\u7269\uFF08lifecycle \u2192 active\uFF09\u3002",
195448
- positional: [
195449
- { name: "<artifactId>", required: true, desc: "\u4EA7\u7269 id" }
195450
- ],
195451
- flags: [
195452
- { name: "note", desc: "\u5907\u6CE8" }
195453
- ],
195454
- examples: ["oasis reopen artifact:dev:abc"]
195455
- },
195456
196182
  "force-conclude": {
195457
196183
  usage: "oasis force-conclude <artifactId> --reason <t>",
195458
196184
  description: "\u8C41\u514D gate \u5F3A\u5236\u6536\u5C3E\u2014\u2014\u673A\u68B0\u68C0\u67E5\u7167\u8DD1\u3001\u91CC\u7A0B\u7891\u5E26 forced \u6807\u8BB0\u3002",
@@ -195913,7 +196639,6 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
195913
196639
  var <reveal|list|set|rm> ... # reveal=\u53D6\u660E\u6587\u5165\u53E3\uFF1Blist/set/rm=\u7BA1\u7406\u9762\uFF08\u4EC5\u4EBA\u7C7B operator\uFF09
195914
196640
 
195915
196641
  \u6CBB\u7406\uFF08\xA711\uFF09
195916
- reopen <artifactId> [--note t] # \u89E3\u5C01\uFF08lifecycle \u2192 active\uFF09
195917
196642
  force-conclude <artifactId> --reason <t> # \u8C41\u514D gate\uFF1B\u673A\u68B0\u68C0\u67E5\u7167\u8DD1\u3001\u91CC\u7A0B\u7891\u5E26 forced \u6807\u8BB0
195918
196643
  intervene --file <plan.json> [--apply true] # \u7ED3\u6784\u53D8\u66F4\u539F\u5B50\u6279\uFF1Apreview\uFF08\u7F3A\u7701\uFF09\u2192 --apply \u843D\u5730
195919
196644
  intervention draft --file <plan.json> # \xA711.8 \u8D77\u8349\u901A\u9053\uFF1A\u534F\u8C03\u8005\u63D0\u4EA4 C \u7C7B\u8349\u6848\u5F85\u4EBA\u786E\u8BA4\uFF08B \u7C7B\u53EF\u76F4\u63A5 intervene\uFF09
@@ -195923,7 +196648,7 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
195923
196648
  plan --file <plan.json> [--apply true] # \u540C\u7BA1\u7EBF\u7684\u6210\u56FE\u4E8B\u52A1\uFF1Aspawn/link/annotate \u539F\u5B50\u843D\u5730\uFF08A3\uFF0C\u4EFB\u52A1\u4E66\u968F\u56FE\u8D70\uFF09
195924
196649
  rework <artifactId> # A2 \u4EBA\u95F8"\u8FD4\u5DE5"\u6863\uFF1Abump \u6700\u65B0\uFF08\u65E0 ack\uFF09\u2192 \u70B9\u540D\u8FD4\u5DE5\u3001\u91CD\u65B0\u53EF\u6D3E
195925
196650
  ack <artifactId> [--note t] # "\u786E\u8BA4\u6CBF\u7528"\uFF1A\u770B\u8FC7\u4E0A\u6E38\u65B0\u7248\u3001\u786E\u8BA4\u672C\u4EA7\u7269\u4E0D\u53D7\u5F71\u54CD \u2192 \u4E0D\u8FD4\u5DE5\uFF0C\u7EA7\u8054\u6B62\u4E8E\u6B64\u679D\uFF08--note \u5199\u6E05\u4E3A\u4EC0\u4E48\u4E0D\u53D7\u5F71\u54CD\uFF09
195926
- # plan.json: { "reason": "...", "ops": [ {"action":"spawn"|"seal"|"reopen"|"link"|"unlink"|"pin"|"assign", ...} ] }
196651
+ # plan.json: { "reason": "...", "ops": [ {"action":"spawn"|"seal"|"link"|"unlink"|"pin"|"assign", ...} ] }
195927
196652
 
195928
196653
  \u4EA4\u4ED8\u7269\u6587\u6863\u4F53\u7CFB
195929
196654
  project create --name <name> [--goal <text>] [--slug <slug>] [--members <json-array>]
@@ -196019,7 +196744,7 @@ function renderGraphLines(graph) {
196019
196744
  for (const n of graph.nodes) {
196020
196745
  out.push(` ${n.id}`);
196021
196746
  const systemNote = closureIds.has(n.id) ? " [\u672C\u5355\u672B\u7AEF\xB7\u7CFB\u7EDF\u7EF4\u62A4\uFF0C\u8FDE\u7EBF\u4E0E\u4EFB\u52A1\u4E66\u90FD\u4E0D\u7528\u4F60\u7BA1]" : "";
196022
- out.push(` type=${n.type}${n.stage ? ` \u9636\u6BB5=${n.stage}` : ""}${n.blocked ? " [\u5361\u4F4F]" : ""} owner=${n.owner?.name ?? n.owner?.id ?? "?"}${n.label ? ` ${n.label}` : ""}${systemNote}`);
196747
+ out.push(` type=${n.type}${n.stage ? ` \u9636\u6BB5=${n.stage}` : ""}${n.blocked ? " [\u5361\u4F4F]" : ""}${n.unresolvedEscalations ? ` [\u4E0A\u62A5 ${n.unresolvedEscalations}]` : ""} owner=${n.owner?.name ?? n.owner?.id ?? "?"}${n.label ? ` ${n.label}` : ""}${systemNote}`);
196023
196748
  }
196024
196749
  const shown = graph.edges.filter((e) => !closureIds.has(e.from));
196025
196750
  const folded = [...closureIds].filter((id) => graph.edges.some((e) => e.from === id));
@@ -196148,7 +196873,7 @@ function makeClient(base, token) {
196148
196873
  }
196149
196874
  };
196150
196875
  }
196151
- var STAGE_COMMANDS = /* @__PURE__ */ new Set(["link", "spawn", "unlink", "cancel", "freeze", "cancel-part", "edit", "assign", "reopen"]);
196876
+ var STAGE_COMMANDS = /* @__PURE__ */ new Set(["link", "spawn", "unlink", "cancel", "freeze", "cancel-part", "edit", "assign"]);
196152
196877
  function fieldsFromFlags(flags, ownFlags) {
196153
196878
  const fields = {};
196154
196879
  for (const [k2, v2] of flags) {
@@ -196210,8 +196935,6 @@ function buildStageOp(cmd, flags, positional, readFile6 = (file) => fs29.readFil
196210
196935
  return { action: "cancelPart", artifactId: needPos(positional, 0, "oasis cancel-part <id> --part <name>"), part: need(flags, "part") };
196211
196936
  case "assign":
196212
196937
  return { action: "assign", artifactId: needPos(positional, 0, "oasis assign <id> --owner <actorId>"), owner: need(flags, "owner") };
196213
- case "reopen":
196214
- return { action: "reopen", artifactId: needPos(positional, 0, "oasis reopen <id> [--note <\u539F\u56E0>]"), ...flags.get("note") !== void 0 ? { note: flags.get("note") } : {} };
196215
196938
  case "edit": {
196216
196939
  const fields = fieldsFromFlags(flags, ownFlagsFromDecl("edit"));
196217
196940
  return {
@@ -196241,8 +196964,6 @@ function describeStageOp(op) {
196241
196964
  return `cancel-part ${op["artifactId"]} part=${op["part"]}`;
196242
196965
  case "assign":
196243
196966
  return `assign ${op["artifactId"]} \u2192 owner=${op["owner"]}`;
196244
- case "reopen":
196245
- return `reopen ${op["artifactId"]}\uFF08\u89E3\u5C01 \u2192 active\uFF09`;
196246
196967
  case "edit":
196247
196968
  return `edit ${op["artifactId"]}\uFF08${["title", "description"].filter((k2) => op[k2] !== void 0).join("/") || "?"}\uFF09`;
196248
196969
  default:
@@ -197619,8 +198340,7 @@ ${res.warning}`);
197619
198340
  }
197620
198341
  case "resolve-escalation": {
197621
198342
  const { message, data } = await api.cmd("resolveEscalation", {
197622
- artifactId: needPos(positional, 0, "oasis resolve-escalation <artifactId> --escalation <id> [--reason t]"),
197623
- escalationId: need(flags, "escalation"),
198343
+ ...flags.get("escalation") !== void 0 ? { escalationId: flags.get("escalation") } : {},
197624
198344
  ...flags.get("reason") !== void 0 ? { reason: flags.get("reason") } : {}
197625
198345
  });
197626
198346
  println(message);
@@ -197872,14 +198592,6 @@ ${res.warning}`);
197872
198592
  println(message);
197873
198593
  break;
197874
198594
  }
197875
- case "reopen": {
197876
- const { message } = await api.cmd("reopen", {
197877
- artifactId: needPos(positional, 0, "oasis reopen <artifactId>"),
197878
- note: flags.get("note")
197879
- });
197880
- println(message);
197881
- break;
197882
- }
197883
198595
  case "force-conclude": {
197884
198596
  const { message } = await api.cmd("forceConclude", {
197885
198597
  artifactId: needPos(positional, 0, "oasis force-conclude <artifactId> --reason t"),
@@ -198245,7 +198957,7 @@ ${res.warning}`);
198245
198957
  break;
198246
198958
  }
198247
198959
  case "content": {
198248
- const id = needPos(positional, 0, "oasis content <artifactId|revisionId|annotationId|reviewId> [path]");
198960
+ const id = needPos(positional, 0, "oasis content <artifactId|revisionId|annotationId|reviewId|escalationId> [path]");
198249
198961
  const path26 = positional[1];
198250
198962
  const c = await api.view("content", id, path26 ? { path: path26 } : void 0);
198251
198963
  if (c.kind === "empty") {
@@ -198284,6 +198996,14 @@ ${res.warning}`);
198284
198996
  }
198285
198997
  break;
198286
198998
  }
198999
+ if (c.kind === "escalation") {
199000
+ println(`# ${c.escalationId} ${c.resolved ? "resolved" : "open"} by ${c.by}${c.hand ? `\uFF08\u7ECF ${c.hand} \u4EE3\u5F55\uFF09` : ""} ${c.at}`);
199001
+ println(`# \u6240\u5728\u8282\u70B9 ${c.artifactId}${c.nodeLabel ? `\uFF08${c.nodeLabel}\uFF09` : ""}${c.part ? ` part=${c.part}` : ""}${c.gapId ? ` \u5173\u8054 gap ${c.gapId}` : ""}`);
199002
+ if (c.resolved) println(`# \u5DF2\u5173\u95ED${c.resolvedReason ? `\uFF1A${c.resolvedReason}` : "\uFF08\u672A\u8BB0\u5F55\u539F\u56E0\uFF09"}`);
199003
+ println(`---`);
199004
+ println(c.reason);
199005
+ break;
199006
+ }
198287
199007
  const at = c.at;
198288
199008
  const which = at.isCurrentHead ? "current head" : "\u5386\u53F2\u7248\u672C";
198289
199009
  println(`# ${at.artifactId} @ ${at.revisionId}\uFF08${which}\uFF09 by ${at.author} "${at.reason}"`);
@@ -198308,7 +199028,7 @@ ${res.warning}`);
198308
199028
  const id = needPos(positional, 0, "oasis status <artifactId>");
198309
199029
  const v2 = await api.view("show", id);
198310
199030
  const headRev = v2.revisions.find((r) => r.id === v2.artifact.currentRev);
198311
- const kind = !headRev ? "\uFF08\u7A7A\uFF0C\u5C1A\u65E0\u5185\u5BB9\uFF09" : headRev.contentKind === "external-pin" ? "\u4EE3\u7801/\u5916\u90E8(git)" : headRev.contentKind === "manifest" ? "\u591A\u6587\u4EF6" : "\u5355\u6587\u4EF6\u6587\u672C";
199031
+ const kind = !headRev || headRev.contentKind === "empty" ? "\uFF08\u7A7A\uFF0C\u5C1A\u65E0\u5185\u5BB9\uFF09" : headRev.contentKind === "external-pin" ? "\u4EE3\u7801/\u5916\u90E8(git)" : headRev.contentKind === "manifest" ? "\u591A\u6587\u4EF6" : "\u5355\u6587\u4EF6\u6587\u672C";
198312
199032
  println(`${v2.artifact.id} type=${v2.artifact.type} owner=${v2.artifact.owner} lifecycle=${v2.lifecycle}`);
198313
199033
  println(`current head : ${v2.artifact.currentRev ?? "\u2205"}`);
198314
199034
  if (v2.concludedHead === null) {
@@ -198367,6 +199087,16 @@ ${res.warning}`);
198367
199087
  println(`\u672A\u51B3 gap(${openGaps.length}):`);
198368
199088
  for (const g2 of openGaps) println(` ${g2.gapId}${g2.part ? ` part=${g2.part}` : ""} by ${g2.by} \u300C${g2.description}\u300D`);
198369
199089
  }
199090
+ const escs = v2.escalations ?? [];
199091
+ if (escs.length > 0) {
199092
+ const openN = escs.filter((e) => !e.resolved).length;
199093
+ println(`\u4E0A\u62A5(${escs.length}\uFF0C\u5176\u4E2D\u672A\u89E3 ${openN}) \u2190 \u53D6\u5168\u6587\u7528 \`oasis content <escalationId>\`\uFF1B\u5173\u4E00\u6761\u7528 \`oasis resolve-escalation --escalation <id> --reason "\u2026"\`\uFF1A`);
199094
+ for (const e of [...escs].reverse()) {
199095
+ const first = e.reason.split("\n")[0] ?? "";
199096
+ const closed = e.resolved ? ` \u2190 \u5DF2\u5173\u95ED${e.resolvedReason ? `\uFF1A${e.resolvedReason.split("\n")[0]}` : ""}` : "";
199097
+ println(` ${e.escalationId} ${e.resolved ? "resolved" : "open"} by ${e.by}${e.hand ? `\uFF08\u7ECF ${e.hand} \u4EE3\u5F55\uFF09` : ""} ${e.at}${e.gapId ? ` \u5173\u8054 gap ${e.gapId}` : ""} \u300C${first}\u300D${closed}`);
199098
+ }
199099
+ }
198370
199100
  if (v2.blocked.blocked) println(`BLOCKED: ${v2.blocked.reasons.join("\uFF1B")}`);
198371
199101
  if (v2.pendingGate) println(`\u5F85\u5BA1 gate: quorum=${v2.pendingGate.gate.quorum}`);
198372
199102
  break;
@@ -198608,7 +199338,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
198608
199338
  }
198609
199339
 
198610
199340
  // src/index.ts
198611
- var PKG_VERSION = true ? "0.1.97" : "dev";
199341
+ var PKG_VERSION = true ? "0.1.99" : "dev";
198612
199342
  var OASIS_DIR = path25.join(os10.homedir(), ".oasis");
198613
199343
  var CONFIG_FILE = path25.join(OASIS_DIR, "node-config.json");
198614
199344
  var PID_FILE = path25.join(OASIS_DIR, "node.pid");