oasis_test_v2 2.2.6 → 2.2.7

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 +712 -141
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -366,12 +366,16 @@ var init_annotation = __esm({
366
366
  function commentThreadSessionId(a, b2) {
367
367
  return `${COMMENT_THREAD_SESSION_PREFIX}${[a, b2].sort().join("|")}`;
368
368
  }
369
- var GAP_THREAD_SESSION_PREFIX, COMMENT_THREAD_SESSION_PREFIX;
369
+ function reviewThreadSessionId(a, b2) {
370
+ return `${REVIEW_THREAD_SESSION_PREFIX}${[a, b2].sort().join("|")}`;
371
+ }
372
+ var GAP_THREAD_SESSION_PREFIX, COMMENT_THREAD_SESSION_PREFIX, REVIEW_THREAD_SESSION_PREFIX;
370
373
  var init_collab_view = __esm({
371
374
  "../contract/src/collab-view.ts"() {
372
375
  "use strict";
373
376
  GAP_THREAD_SESSION_PREFIX = "gapline:";
374
377
  COMMENT_THREAD_SESSION_PREFIX = "commentline:";
378
+ REVIEW_THREAD_SESSION_PREFIX = "reviewline:";
375
379
  }
376
380
  });
377
381
 
@@ -1340,27 +1344,21 @@ function trimRoundSummariesForReturn(summaries, recentRounds = DELEGATION_RETURN
1340
1344
  });
1341
1345
  }
1342
1346
  function collectDelegationArtifacts(roundSummaries) {
1343
- const artifacts = /* @__PURE__ */ new Map();
1347
+ const out = [];
1344
1348
  for (const round of roundSummaries) {
1345
1349
  for (const file of round.files ?? []) {
1346
- const seen = artifacts.get(file.name);
1347
- if (seen) {
1348
- if (!seen.blobRef && file.blobRef) seen.blobRef = file.blobRef;
1349
- continue;
1350
- }
1351
- artifacts.set(file.name, {
1350
+ out.push({
1352
1351
  name: file.name,
1353
1352
  ...file.blobRef ? { blobRef: file.blobRef } : {},
1354
1353
  round: round.round,
1355
- // settledSeq 透传自这个名字**第一次出现**的那一轮。未收口的轮次(pending 续跑那一支)
1356
- // 与本字段落地前的存量行都没有值——前端凭 `settledSeq` 是否缺失走两条路径:
1357
- // → 按 settledSeq 各自定位对应 `chat-msg:delegation-done|<id>:<seq>`;
1358
- // 无 → 该委派整条退回「单委派单锚点」降级(`ann:36bfa04b`)。
1354
+ // 这一轮收口时的 settledSeq。未收口的轮次(pending 续跑那一支)与本字段落地前的存量行
1355
+ // 都没有值——前端凭它是否缺失走两条路径:有 → 按 settledSeq 各自定位对应
1356
+ // `chat-msg:delegation-done|<id>:<seq>`;无 → 该委派整条退回单锚点降级(`ann:36bfa04b`)。
1359
1357
  ...typeof round.settledSeq === "number" ? { settledSeq: round.settledSeq } : {}
1360
1358
  });
1361
1359
  }
1362
1360
  }
1363
- return [...artifacts.values()];
1361
+ return out;
1364
1362
  }
1365
1363
  function delegationOutboxFingerprints(roundSummaries) {
1366
1364
  const out = /* @__PURE__ */ new Map();
@@ -12271,7 +12269,7 @@ function latestProposedOf(state, nodeId, deliveriesOnly) {
12271
12269
  const successes = state.mainWorksOf(nodeId).filter((w2) => w2.status === "success" && (!deliveriesOnly || state.artifactsOf(w2.id).length > 0 || w2.conclusion != null)).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
12272
12270
  return successes.length > 0 ? successes[successes.length - 1].id : null;
12273
12271
  }
12274
- var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workContentEdited, workConclude, nodeLatestProposedRebased, workTimeout;
12272
+ var workCreate, workRedispatch, workSnapshotRecorded, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workContentEdited, workConclude, nodeLatestProposedRebased, workTimeout;
12275
12273
  var init_work = __esm({
12276
12274
  "../engine/src/handlers/work.ts"() {
12277
12275
  "use strict";
@@ -12384,11 +12382,78 @@ var init_work = __esm({
12384
12382
  // 会话侧也要杀(旧 agent 可能还挂着)。cancelPreviousWork 找不到/会话已退则 no-op。
12385
12383
  // 回信 work 不杀前会话(apply 不 kill 主链,节点已完结、无在跑会话),只派发。
12386
12384
  async effect(e, ctx) {
12387
- if (e.pauseGateVersion === 1 && !await ctx.canDispatchWork(e.workorderId, e.workId)) return;
12385
+ if (!await ctx.canDispatchWork(e.workorderId, e.workId)) return;
12388
12386
  if (!e.replyToIssueId) await ctx.cancelPreviousWork(e.nodeId, e.workId);
12389
12387
  await ctx.dispatchWork(e.workId, e.resumeEligible !== void 0 ? { resumeEligible: e.resumeEligible } : void 0);
12390
12388
  }
12391
12389
  };
12390
+ workRedispatch = {
12391
+ name: "work/redispatch",
12392
+ kind: "work.redispatch",
12393
+ apply() {
12394
+ },
12395
+ async effect(e, ctx) {
12396
+ if (!await ctx.canDispatchWork(e.workorderId, e.workId, { neverStarted: true })) return;
12397
+ await ctx.dispatchWork(e.workId, { resumeEligible: false });
12398
+ }
12399
+ };
12400
+ workSnapshotRecorded = {
12401
+ name: "work/snapshot-recorded",
12402
+ kind: "work.snapshot_recorded",
12403
+ apply(e, state, ctx) {
12404
+ const node2 = state.node(e.nodeId);
12405
+ if (!ctx.actorId.startsWith("actor:human:") || !state.workorder.dispatchPaused || !node2 || node2.cancelledAt || node2.latestWorkId !== e.expectedLatestWorkId || state.work(e.workId) || !e.contentRef || !e.conclusion.trim()) return;
12406
+ if (state.allWorksIncludingReplies(e.nodeId).some((w2) => !w2.endedAt && !w2.deadAt) || state.allReviews().some((r) => r.nodeId === e.nodeId && !r.endedAt && !r.cancelledAt)) return;
12407
+ state.insertWork({
12408
+ id: e.workId,
12409
+ workorderId: e.workorderId,
12410
+ nodeId: e.nodeId,
12411
+ assigneeActorId: ctx.actorId,
12412
+ createdAt: ctx.at,
12413
+ startedAt: null,
12414
+ endedAt: ctx.at,
12415
+ deadAt: null,
12416
+ cancelledAt: null,
12417
+ status: "success",
12418
+ lane: "main",
12419
+ outcome: "completed",
12420
+ outcomeDetail: null,
12421
+ sessionRef: null,
12422
+ continuesWorkId: node2.latestAcceptId,
12423
+ parentWorkId: node2.latestProposedWorkId,
12424
+ outputVersionNo: null,
12425
+ conclusion: e.conclusion,
12426
+ proposalReason: e.conclusion,
12427
+ lastActivityAt: ctx.at,
12428
+ handledIssueIds: null,
12429
+ agentHandoffAttemptId: null,
12430
+ businessHandoffStatus: null,
12431
+ businessHandoffRecordedAt: null,
12432
+ acceptanceState: null,
12433
+ acceptedAt: null,
12434
+ acceptedBy: null,
12435
+ rejectedReason: null,
12436
+ overrideBy: null,
12437
+ overrideReason: null,
12438
+ nodeVersion: node2.version
12439
+ });
12440
+ state.replaceArtifacts(e.workId, [{
12441
+ id: `${e.workId}:out`,
12442
+ workId: e.workId,
12443
+ nodeId: e.nodeId,
12444
+ type: null,
12445
+ name: "output",
12446
+ logicalPath: "/",
12447
+ contentRef: e.contentRef,
12448
+ contentKind: "inline",
12449
+ ordinal: 0,
12450
+ createdAt: ctx.at
12451
+ }]);
12452
+ state.replaceWorkInputs(e.workId, state.edgesInto(e.nodeId).filter((edge) => edge.kind === "data" && edge.pinnedWorkId).map((edge) => ({ workId: e.workId, upstreamNodeId: edge.fromNodeId, upstreamWorkId: edge.pinnedWorkId })));
12453
+ state.replaceRequirements(e.nodeId, state.requirementsOf(e.nodeId).map((r) => ({ ...r, latestReviewId: null })));
12454
+ state.updateNode(e.nodeId, { latestWorkId: e.workId, latestProposedWorkId: e.workId, updatedAt: ctx.at });
12455
+ }
12456
+ };
12392
12457
  workKill = {
12393
12458
  name: "work/kill",
12394
12459
  kind: "work.kill",
@@ -12810,7 +12875,7 @@ var init_review = __esm({
12810
12875
  }
12811
12876
  },
12812
12877
  async effect(e, ctx) {
12813
- if (e.pauseGateVersion === 1 && !await ctx.canDispatchReview(e.workorderId, e.reviewId)) return;
12878
+ if (!await ctx.canDispatchReview(e.workorderId, e.reviewId)) return;
12814
12879
  if (e.reviewerIsAgent) await ctx.dispatchReview(e.reviewId, e.nodeId, e.reviewerActorId);
12815
12880
  else await ctx.notify([e.reviewerActorId], { kind: "review-requested", reviewId: e.reviewId });
12816
12881
  }
@@ -13089,6 +13154,8 @@ var init_handlers = __esm({
13089
13154
  erase(planUpdateFields),
13090
13155
  /* ── 节点级 work(7)── */
13091
13156
  erase(workCreate),
13157
+ erase(workRedispatch),
13158
+ erase(workSnapshotRecorded),
13092
13159
  erase(workStart),
13093
13160
  erase(workSubmitOutput),
13094
13161
  erase(workContentEdited),
@@ -13663,11 +13730,15 @@ var init_bus = __esm({
13663
13730
  // ★ 注入 workorderId:派发侧读模型没追上时,据它指名重读那张工单(见 EngineIO.dispatchWork 注释)。
13664
13731
  dispatchWork: (id, opts) => this.io.dispatchWork(id, { ...opts, workorderId: record8.workorderId }),
13665
13732
  dispatchReview: (id, nodeId, reviewerActorId) => this.io.dispatchReview(id, nodeId, reviewerActorId),
13666
- canDispatchWork: async (workorderId, workId) => {
13733
+ canDispatchWork: async (workorderId, workId, opts) => {
13667
13734
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(workorderId));
13668
13735
  if (!snap || snap.workorder.dispatchPaused) return false;
13669
13736
  const work = snap.works.find((row) => row.id === workId);
13670
- return work?.status === "running" && !work.deadAt && !work.endedAt;
13737
+ if (opts?.neverStarted && work) {
13738
+ const node2 = snap.nodes.find((row) => row.id === work.nodeId);
13739
+ if (work.startedAt || work.sessionRef || !node2 || node2.cancelledAt || node2.latestWorkId !== workId) return false;
13740
+ }
13741
+ return work?.status === "running" && !work.deadAt && !work.cancelledAt && !work.endedAt;
13671
13742
  },
13672
13743
  canDispatchReview: async (workorderId, reviewId) => {
13673
13744
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(workorderId));
@@ -19708,6 +19779,39 @@ var init_kernel_bridge = __esm({
19708
19779
  throw new KernelError("\u5185\u5BB9\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u6587\u4EF6\u540E\u518D\u4FDD\u5B58", "content-conflict");
19709
19780
  }
19710
19781
  }
19782
+ /** 暂停时保存人工完整稿:不借 work.create 开运行,不改暂停位,验收/下游调度留待恢复。 */
19783
+ async recordPausedRevision(args) {
19784
+ if (!args.actor.startsWith("actor:human:")) {
19785
+ throw new KernelError("\u6682\u505C\u671F\u95F4\u5F55\u5165\u5B8C\u6574\u7A3F\u4EC5\u9650\u4EBA\u7C7B\u64CD\u4F5C", "forbidden");
19786
+ }
19787
+ await this.refreshTracked();
19788
+ const wid = this.wo(args.artifactId);
19789
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
19790
+ const node2 = snap?.nodes.find((n) => n.id === args.artifactId);
19791
+ if (!snap?.workorder.dispatchPaused || !node2 || node2.cancelledAt) {
19792
+ throw new KernelError("\u5DE5\u5355\u6216\u8282\u70B9\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u4FDD\u5B58", "revision-conflict");
19793
+ }
19794
+ const workId = deriveWorkId(node2.id, snap.works.filter((w2) => w2.nodeId === node2.id).length + 1);
19795
+ await this.commit({
19796
+ companyId: "",
19797
+ workorderId: wid,
19798
+ actorId: args.actor,
19799
+ event: {
19800
+ kind: "work.snapshot_recorded",
19801
+ workorderId: wid,
19802
+ nodeId: node2.id,
19803
+ workId,
19804
+ expectedLatestWorkId: node2.latestWorkId,
19805
+ contentRef: args.contentRef,
19806
+ conclusion: args.note
19807
+ }
19808
+ });
19809
+ const after = await this.store.transaction((tx) => tx.loadWorkorder(wid));
19810
+ if (!after?.artifacts.some((a) => a.workId === workId && a.contentRef === args.contentRef)) {
19811
+ throw new KernelError("\u4FDD\u5B58\u672A\u843D\u8D26\uFF1A\u8282\u70B9\u5DF2\u88AB\u4FEE\u6539\u6216\u5DE5\u5355\u5DF2\u6062\u590D\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "revision-conflict");
19812
+ }
19813
+ return this.model.revisions.get(workId);
19814
+ }
19711
19815
  async proposeRevision(args) {
19712
19816
  await this.refreshTracked();
19713
19817
  const wid = this.wo(args.artifactId);
@@ -29417,11 +29521,22 @@ async function sweepStaleChatTurns(deps) {
29417
29521
  deps.log?.(
29418
29522
  `[chat-turn-sweep] \u8F6E\u6B21 ${turn.id}\uFF08\u4F1A\u8BDD ${turn.chatSessionId}\uFF09\u6536\u53E3\u4E3A ${outcome.status}\uFF08${outcome.reason}\uFF09\uFF0C\u4F1A\u8BDD\u69FD\u5DF2\u91CA\u653E`
29419
29523
  );
29524
+ const settledStatus = outcome.settled ? outcome.status : void 0;
29525
+ if (deps.onTurnSettled && settledStatus && settledStatus !== "succeeded") {
29526
+ await deps.onTurnSettled({
29527
+ chatSessionId: turn.chatSessionId,
29528
+ turnId: turn.id,
29529
+ status: settledStatus,
29530
+ reason: outcome.reason ?? `\u8F6E\u6B21\u6536\u53E3\u4E3A ${settledStatus}`
29531
+ }).catch((e) => {
29532
+ deps.log?.(`[chat-turn-sweep] \u8F6E\u6B21 ${turn.id} \u6536\u53E3\u540E\u7684\u56DE\u8C03\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u62CD\uFF09: ${String(e)}`);
29533
+ });
29534
+ }
29420
29535
  }
29421
29536
  return settled;
29422
29537
  }
29423
29538
  async function sweepStaleChatTurnsAcrossCompanies(deps) {
29424
- const { listCompanies, storesFor, onCompanyError, log: log3, ...rest } = deps;
29539
+ const { listCompanies, storesFor, onCompanyError, log: log3, onTurnSettled, ...rest } = deps;
29425
29540
  const settled = [];
29426
29541
  for (const companyId of await listCompanies()) {
29427
29542
  try {
@@ -29431,7 +29546,9 @@ async function sweepStaleChatTurnsAcrossCompanies(deps) {
29431
29546
  turns: stores.turns,
29432
29547
  ...stores.assistantRowStatus ? { assistantRowStatus: stores.assistantRowStatus } : {},
29433
29548
  ...stores.settleAssistantRow ? { settleAssistantRow: stores.settleAssistantRow } : {},
29434
- ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {}
29549
+ ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {},
29550
+ // 把这一家的 companyId 补进去再往外报——下游(委派台账)按公司分 store,少了它就查错库。
29551
+ ...onTurnSettled ? { onTurnSettled: (info) => onTurnSettled({ ...info, ...companyId ? { companyId } : {} }) } : {}
29435
29552
  });
29436
29553
  settled.push(...outcomes);
29437
29554
  } catch (err) {
@@ -159619,7 +159736,7 @@ function normalizedOpToV3Op(op, kind) {
159619
159736
  if (op === "set_text") return "set_text";
159620
159737
  if (op === "patch_input") return "patch_input";
159621
159738
  if (op === "set_status") {
159622
- return kind === "tool_call" || kind === "tool_result" ? "completed" : void 0;
159739
+ return "completed";
159623
159740
  }
159624
159741
  return void 0;
159625
159742
  }
@@ -159803,7 +159920,7 @@ var init_live_chat = __esm({
159803
159920
  if (!turn.items) return;
159804
159921
  const itemId = outcome.itemId;
159805
159922
  const itemType = normalizedKindToItemType(event.kind);
159806
- const operation = normalizedOpToV3Op(event.opShape.op, event.kind);
159923
+ let operation = normalizedOpToV3Op(event.opShape.op, event.kind);
159807
159924
  if (!operation) return;
159808
159925
  const perItem = turn.v3Items.get(itemId) ?? { textOffset: 0, started: false };
159809
159926
  const isTextual2 = event.kind === "text" || event.kind === "thinking";
@@ -159839,6 +159956,7 @@ var init_live_chat = __esm({
159839
159956
  payload = { input: opShape.input };
159840
159957
  } else if (opShape.op === "set_status") {
159841
159958
  if (opShape.status === "streaming") return;
159959
+ operation = opShape.status === "completed" ? "completed" : "failed";
159842
159960
  }
159843
159961
  turn.v3Items.set(itemId, perItem);
159844
159962
  const frame = {
@@ -159969,6 +160087,24 @@ var init_live_chat = __esm({
159969
160087
  this.publishV3(turn, this.assignV3Seq(turn, frame));
159970
160088
  return true;
159971
160089
  }
160090
+ /** 恢复路在 DB 收口旧 item 后,将同一身份/版本的终态发给已挂上的观众。 */
160091
+ publishItemStatus(chatSessionId, item) {
160092
+ const turn = this.turns.get(chatSessionId);
160093
+ if (!turn || turn.status !== "running" || item.sessionId !== chatSessionId || item.status === "streaming") return false;
160094
+ if (turn.items?.turnId !== item.turnId) return false;
160095
+ this.flushPendingV3(turn);
160096
+ this.publishV3(turn, this.assignV3Seq(turn, {
160097
+ protocolVersion: 3,
160098
+ streamId: turn.liveStreamId,
160099
+ turnId: item.turnId,
160100
+ itemId: item.id,
160101
+ itemType: normalizedKindToItemType(item.kind),
160102
+ operation: item.status === "completed" ? "completed" : "failed",
160103
+ itemVersion: item.version,
160104
+ ...item.ord !== null ? { ord: item.ord } : {}
160105
+ }));
160106
+ return true;
160107
+ }
159972
160108
  emitV3TurnTerminal(turn, status) {
159973
160109
  if (status !== "done" && status !== "error") return;
159974
160110
  const operation = status === "done" ? "turn_completed" : "turn_failed";
@@ -195686,6 +195822,7 @@ var init_service3 = __esm({
195686
195822
  const store = await this.options.resolveStore(companyId).catch(() => null);
195687
195823
  await store?.patch(record8.id, { childRunId: anchor }).catch(() => null);
195688
195824
  }
195825
+ await this.bindChildRunToPlaceholder(record8, companyId, claim.turn.assistantRunId ?? void 0);
195689
195826
  await notDispatched("resumed-existing-run");
195690
195827
  return;
195691
195828
  }
@@ -195719,6 +195856,7 @@ var init_service3 = __esm({
195719
195856
  const store = await this.options.resolveStore(companyId);
195720
195857
  await store.patch(record8.id, { childRunId: anchor }).catch(() => null);
195721
195858
  }
195859
+ await this.bindChildRunToPlaceholder(record8, companyId, dispatched.runId);
195722
195860
  this.track(this.settleRound(record8, dispatched, companyId, () => output, turnId, turns));
195723
195861
  } catch (error2) {
195724
195862
  const failedAt = this.now();
@@ -195835,6 +195973,29 @@ var init_service3 = __esm({
195835
195973
  * 委派台账是同一类账,只是多一层——它还得回流。所以这里只做两件:把行拍成 `failed/interrupted`、
195836
195974
  * 然后走同一个 {@link notifySettled}。已经是终态的行原样返回(幂等,重复扫不重复回流)。
195837
195975
  */
195976
+ /**
195977
+ * 把这一轮的 run 锚点写到子会话那条**回答占位行**上。
195978
+ *
195979
+ * 为什么非它不可:serve 重启后,daemon 的 hello 对账与会话进程自报身份**共用**
195980
+ * `planChatTurnRecovery`,而那个函数第一行就是 `if (!row.runId) continue` —— 占位行没有 runId,
195981
+ * 这条子会话就永远配不出恢复计划,答 `unknown`,然后被 hub evict(杀掉)。
195982
+ * 轮次是第几轮按 `roundSummaries.length + 1` 算,与 `seedChildRoundRows` 那两处口径逐字一致。
195983
+ *
195984
+ * 失败只记日志:这是加固,不是这一轮派发成立的前提。
195985
+ */
195986
+ async bindChildRunToPlaceholder(record8, companyId, runId) {
195987
+ if (!runId) return;
195988
+ const round = record8.roundSummaries.length + 1;
195989
+ const { placeholderId } = childRoundMessageIds(record8.childSessionId, round);
195990
+ try {
195991
+ const chatStore = await this.options.resolveChatSessions(companyId);
195992
+ await chatStore.updateMessage(placeholderId, { runId });
195993
+ } catch (err) {
195994
+ console.warn(
195995
+ `[delegation] \u5B50\u4F1A\u8BDD\u5360\u4F4D\u884C\u5199 runId \u5931\u8D25\uFF08${record8.id} child=${record8.childSessionId} round=${round} run=${runId}\uFF09: ${String(err)}\u2014\u2014\u91CD\u542F\u540E\u8FD9\u6761\u5B50\u4F1A\u8BDD\u4F1A\u88AB\u5224 unknown \u5E76 evict`
195996
+ );
195997
+ }
195998
+ }
195838
195999
  async settleFromSweep(childSessionId, reason, companyId) {
195839
196000
  const store = await this.options.resolveStore(companyId).catch(() => null);
195840
196001
  if (!store) return null;
@@ -196957,12 +197118,15 @@ var init_dispatch_adapter = __esm({
196957
197118
  // ../server/src/domains/delegations/sweep-hook.ts
196958
197119
  function delegationSweepHook(deps) {
196959
197120
  const log3 = deps.log ?? ((m2) => console.warn(m2));
196960
- return async (chatSessionId, reason) => {
196961
- const session = await deps.sessions.getSession(chatSessionId).catch((err) => {
196962
- log3(`[chat-sweep] \u4F1A\u8BDD ${chatSessionId} \u7684\u516C\u53F8\u5F52\u5C5E\u8BFB\u4E0D\u5230\uFF08${String(err)}\uFF09\u2014\u2014\u6309\u9ED8\u8BA4\u516C\u53F8\u8BD5\u4E00\u6B21`);
196963
- return null;
196964
- });
196965
- const companyId = session?.companyId ?? void 0;
197121
+ return async (chatSessionId, reason, _messageId, knownCompanyId) => {
197122
+ let companyId = knownCompanyId;
197123
+ if (companyId === void 0) {
197124
+ const session = await deps.sessions.getSession(chatSessionId).catch((err) => {
197125
+ log3(`[chat-sweep] \u4F1A\u8BDD ${chatSessionId} \u7684\u516C\u53F8\u5F52\u5C5E\u8BFB\u4E0D\u5230\uFF08${String(err)}\uFF09\u2014\u2014\u6309\u9ED8\u8BA4\u516C\u53F8\u8BD5\u4E00\u6B21`);
197126
+ return null;
197127
+ });
197128
+ companyId = session?.companyId ?? void 0;
197129
+ }
196966
197130
  await deps.settle(chatSessionId, reason, companyId).catch((err) => {
196967
197131
  log3(`[chat-sweep] \u59D4\u6D3E\u53F0\u8D26\u6536\u655B\u5931\u8D25\uFF08\u4F1A\u8BDD ${chatSessionId} \u516C\u53F8 ${companyId ?? "\u9ED8\u8BA4"}\uFF09: ${String(err)}`);
196968
197132
  });
@@ -214524,7 +214688,7 @@ async function prepareDroppedDispatch(work, deps) {
214524
214688
  const node2 = kernel.model.artifacts.get(work.nodeId);
214525
214689
  const snap = await kernel.getStore().transaction((tx) => tx.loadWorkorder(work.workorderId));
214526
214690
  const current = snap?.works.find((w2) => w2.id === work.workId);
214527
- if (!snap || !current || current.status !== "running" || current.startedAt || current.sessionRef || current.endedAt || !snap.nodes.some((n) => n.id === work.nodeId && n.latestWorkId === current.id && !n.cancelledAt) || !node2 || node2.workspace !== work.workorderId || lifecycleOf(kernel.model, node2.id) !== "active" || kernel.model.pausedWorkorders.has(work.workorderId) || isHeld(kernel.model, node2.id)) return false;
214691
+ if (!snap || snap.workorder.dispatchPaused || !current || current.status !== "running" || current.deadAt || current.cancelledAt || current.startedAt || current.sessionRef || current.endedAt || !snap.nodes.some((n) => n.id === work.nodeId && n.latestWorkId === current.id && !n.cancelledAt) || !node2 || node2.workspace !== work.workorderId || lifecycleOf(kernel.model, node2.id) !== "active" || isHeld(kernel.model, node2.id)) return false;
214528
214692
  if (snap.issues.some((i) => i.kind === "escalation" && i.aboutNodeId === node2.id && !i.resolvedAt && i.intent !== "advisory")) return false;
214529
214693
  if (!work.assigneeActorId) return false;
214530
214694
  const binding = await deps.bindingFor(work.assigneeActorId);
@@ -216135,13 +216299,14 @@ async function planChatTurnRecovery(deps) {
216135
216299
  const plans = [];
216136
216300
  for (const row of rows) {
216137
216301
  if (!row.runId) continue;
216138
- const run = await deps.getRun(row.runId).catch(() => null);
216302
+ const run = await deps.getRun(row.runId);
216139
216303
  if (!run || run.status !== "running" || run.action !== "chat" || !run.artifactId) continue;
216140
216304
  const active = byArtifact.get(run.artifactId);
216141
216305
  if (!active || deps.isWired(run.artifactId)) continue;
216142
216306
  const meta = asObj3(run.metadata);
216143
216307
  const runtimeSessionId = typeof meta?.runtimeSessionId === "string" ? meta.runtimeSessionId : run.jobKey ?? void 0;
216144
- plans.push({
216308
+ const turn = await deps.getTurnForRun?.(row.sessionId, run.id);
216309
+ const plan = {
216145
216310
  dispatchId: active.dispatchId,
216146
216311
  chatSessionId: row.sessionId,
216147
216312
  assistantMsgId: row.id,
@@ -216152,18 +216317,42 @@ async function planChatTurnRecovery(deps) {
216152
216317
  ...runtimeSessionId ? { runtimeSessionId } : {},
216153
216318
  startedAt: run.startedAt,
216154
216319
  seedContent: row.content,
216155
- seedParts: partsOf(row.parts)
216156
- });
216320
+ seedParts: partsOf(row.parts),
216321
+ ...turn ? { turnId: turn.id } : {}
216322
+ };
216323
+ if (turn && turn.status !== "reserved" && turn.status !== "running") {
216324
+ deps.onSettled?.({ plan, turn });
216325
+ continue;
216326
+ }
216327
+ plans.push(plan);
216157
216328
  }
216158
216329
  return plans;
216159
216330
  }
216331
+ async function finishPersistedRecoveryItems(items, sessionId, turnId, status, log3) {
216332
+ if (!items || !chatItemsDoubleWriteEnabled()) return [];
216333
+ try {
216334
+ const rows = await items.listItemsByTurn(sessionId, turnId);
216335
+ let version2 = await items.sessionVersionCursor(sessionId);
216336
+ const closed = [];
216337
+ for (const row of rows) {
216338
+ if (row.status !== "streaming") continue;
216339
+ const updated = await items.applyOp(row.id, { op: "set_status", status }, { version: ++version2 });
216340
+ if (!updated) throw new Error(`item ${row.id} disappeared`);
216341
+ closed.push(updated);
216342
+ }
216343
+ return closed;
216344
+ } catch (err) {
216345
+ log3(`[chat-recovery] items \u6536\u5C3E\u5931\u8D25\uFF1Asession=${sessionId} turn=${turnId} ${String(err)}`);
216346
+ return [];
216347
+ }
216348
+ }
216160
216349
  function wireRecoveredChatTurn(deps) {
216161
216350
  const { plan, handle, liveChat, chatStore, trace } = deps;
216162
216351
  const log3 = deps.log ?? (() => void 0);
216163
216352
  const itemLedger = new ChatItemLedger({
216164
216353
  ...deps.items ? { items: deps.items } : {},
216165
216354
  sessionId: plan.chatSessionId,
216166
- turnId: deps.itemsTurnId ?? fallbackTurnId(plan.runId),
216355
+ turnId: deps.itemsTurnId ?? plan.turnId ?? fallbackTurnId(plan.runId),
216167
216356
  runId: plan.runId,
216168
216357
  messageId: plan.assistantMsgId,
216169
216358
  log: log3
@@ -216329,6 +216518,14 @@ function wireRecoveredChatTurn(deps) {
216329
216518
  normalizedSource.finishTurn(failed ? "failed" : "completed");
216330
216519
  itemLedger.finish(failed ? "failed" : "completed");
216331
216520
  await itemLedger.drain();
216521
+ const closedItems = await finishPersistedRecoveryItems(
216522
+ deps.items,
216523
+ plan.chatSessionId,
216524
+ itemLedger.turnId,
216525
+ failed ? "failed" : "completed",
216526
+ log3
216527
+ );
216528
+ for (const item of closedItems) liveChat.publishItemStatus(plan.chatSessionId, item);
216332
216529
  await chatStore.updateMessage(currentMsgId, {
216333
216530
  content: itemLedger.projectContent(),
216334
216531
  status: failed ? "error" : "done",
@@ -216426,6 +216623,29 @@ async function reconcileChatExitFrame(deps) {
216426
216623
  const meta = asObj3(run.metadata);
216427
216624
  if (meta?.dispatchId !== deps.dispatchId) continue;
216428
216625
  if (deps.ownedRunId?.(row.sessionId) === row.runId) return false;
216626
+ const turn = await deps.getTurnForRun?.(row.sessionId, run.id);
216627
+ if (turn && turn.status !== "reserved" && turn.status !== "running") {
216628
+ await reconcileSettledChatTurn({
216629
+ plan: {
216630
+ dispatchId: deps.dispatchId,
216631
+ chatSessionId: row.sessionId,
216632
+ assistantMsgId: row.id,
216633
+ runId: run.id,
216634
+ artifactId: run.artifactId,
216635
+ actor: run.actorId,
216636
+ startedAt: run.startedAt,
216637
+ seedContent: row.content,
216638
+ seedParts: partsOf(row.parts),
216639
+ turnId: turn.id
216640
+ },
216641
+ turn,
216642
+ chatStore: deps.chatStore,
216643
+ trace: deps.trace,
216644
+ ...deps.items ? { items: deps.items } : {},
216645
+ ...deps.log ? { log: deps.log } : {}
216646
+ });
216647
+ return true;
216648
+ }
216429
216649
  const { info } = deps;
216430
216650
  const failed = exitFailed(info);
216431
216651
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -216467,6 +216687,13 @@ async function reconcileChatExitFrame(deps) {
216467
216687
  normalizer.turnFinished(failed ? "failed" : "completed");
216468
216688
  itemLedger.finish(failed ? "failed" : "completed");
216469
216689
  await itemLedger.drain();
216690
+ await finishPersistedRecoveryItems(
216691
+ deps.items,
216692
+ row.sessionId,
216693
+ itemLedger.turnId,
216694
+ failed ? "failed" : "completed",
216695
+ deps.log ?? (() => void 0)
216696
+ );
216470
216697
  await deps.chatStore.updateMessage(row.id, {
216471
216698
  content: itemLedger.projectContent(),
216472
216699
  status: failed ? "error" : "done",
@@ -216505,6 +216732,156 @@ async function reconcileChatExitFrame(deps) {
216505
216732
  }
216506
216733
  return false;
216507
216734
  }
216735
+ async function planChatTurnRecoveryAcrossCompanies(deps) {
216736
+ const groups = [];
216737
+ const settled = [];
216738
+ const skipped = [];
216739
+ let scanned = 0;
216740
+ for (const companyId of await deps.listCompanies()) {
216741
+ try {
216742
+ const stores = await deps.storesFor(companyId);
216743
+ const companySettled = [];
216744
+ const plans = await planChatTurnRecovery({
216745
+ activeSessions: deps.activeSessions,
216746
+ listRunningAssistantMessages: stores.listRunningAssistantMessages,
216747
+ getRun: deps.getRun,
216748
+ isWired: deps.isWired,
216749
+ ...stores.getTurnForRun ? { getTurnForRun: stores.getTurnForRun } : {},
216750
+ onSettled: (hit) => companySettled.push(hit)
216751
+ });
216752
+ scanned += 1;
216753
+ settled.push(...companySettled.map((hit) => ({ ...hit, companyId, stores })));
216754
+ if (plans.length) groups.push({ companyId, stores, plans });
216755
+ if (deps.stopOnFirstHit && (plans.length || companySettled.length)) break;
216756
+ } catch (err) {
216757
+ skipped.push({ companyId, error: String(err) });
216758
+ deps.onCompanyError?.(companyId, err);
216759
+ }
216760
+ }
216761
+ return { groups, settled, scanned, skipped };
216762
+ }
216763
+ async function sweepDanglingChatTurnsAcrossCompanies(deps) {
216764
+ const { listCompanies, storesFor, onCompanyError, onSessionClosed, log: log3, ...rest } = deps;
216765
+ const closed = [];
216766
+ for (const companyId of await listCompanies()) {
216767
+ try {
216768
+ const stores = await storesFor(companyId);
216769
+ const ids2 = await sweepDanglingChatTurns({
216770
+ ...rest,
216771
+ listRunningAssistantMessages: stores.listRunningAssistantMessages,
216772
+ listMessages: (sid) => stores.chatStore.listMessages(sid),
216773
+ updateMessage: (id, patch) => stores.chatStore.updateMessage(id, patch),
216774
+ ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {},
216775
+ ...onSessionClosed ? { onSessionClosed: (sid, reason, messageId) => onSessionClosed(sid, reason, messageId, companyId) } : {}
216776
+ });
216777
+ closed.push(...ids2);
216778
+ } catch (err) {
216779
+ onCompanyError?.(companyId, err);
216780
+ }
216781
+ }
216782
+ return closed;
216783
+ }
216784
+ async function reconcileChatExitFrameAcrossCompanies(deps) {
216785
+ const { listCompanies, storesFor, onCompanyError, log: log3, ...rest } = deps;
216786
+ for (const companyId of await listCompanies()) {
216787
+ try {
216788
+ const stores = await storesFor(companyId);
216789
+ const handled2 = await reconcileChatExitFrame({
216790
+ ...rest,
216791
+ listRunningAssistantMessages: stores.listRunningAssistantMessages,
216792
+ chatStore: stores.chatStore,
216793
+ ...stores.getTurnForRun ? { getTurnForRun: stores.getTurnForRun } : {},
216794
+ ...stores.items ? { items: stores.items } : {},
216795
+ ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {}
216796
+ });
216797
+ if (handled2) return true;
216798
+ } catch (err) {
216799
+ onCompanyError?.(companyId, err);
216800
+ }
216801
+ }
216802
+ return false;
216803
+ }
216804
+ async function reconcileSettledChatTurn(deps) {
216805
+ const { plan, turn } = deps;
216806
+ if (turn.status === "reserved" || turn.status === "running") return;
216807
+ const succeeded = turn.status === "succeeded";
216808
+ const completedAt = turn.completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
216809
+ const turnId = deps.items ? await resolveChatItemsTurnId(deps.items, plan.assistantMsgId, turn.id) : turn.id;
216810
+ await finishPersistedRecoveryItems(
216811
+ deps.items,
216812
+ plan.chatSessionId,
216813
+ turnId,
216814
+ succeeded ? "completed" : "failed",
216815
+ deps.log ?? (() => void 0)
216816
+ );
216817
+ await deps.chatStore.updateMessage(plan.assistantMsgId, {
216818
+ status: succeeded ? "done" : "error",
216819
+ completedAt
216820
+ });
216821
+ const run = await deps.trace.getRun(plan.runId);
216822
+ if (run?.status === "running") {
216823
+ await deps.trace.updateRun(plan.runId, {
216824
+ status: turn.status,
216825
+ endedAt: completedAt
216826
+ });
216827
+ }
216828
+ }
216829
+ async function recoverChatTurnsAcrossCompanies(deps) {
216830
+ const result = await planChatTurnRecoveryAcrossCompanies(deps);
216831
+ const recovered = [];
216832
+ const log3 = deps.log ?? (() => void 0);
216833
+ const retire = async (hit) => {
216834
+ const { plan, turn, companyId, stores } = hit;
216835
+ log3(`[chat-claim] company=${companyId ?? "<default>"} session=${plan.chatSessionId} run=${plan.runId} dispatch=${plan.dispatchId} \u7ED3\u8BBA=settled\uFF08\u4F9D\u636E\uFF1A\u540C\u516C\u53F8\u8F6E\u6B21 ${turn.id} \u5DF2 ${turn.status}\uFF0CcompletedAt=${turn.completedAt ?? "-"}\uFF09`);
216836
+ deps.evict(plan.dispatchId);
216837
+ try {
216838
+ await reconcileSettledChatTurn({
216839
+ plan,
216840
+ turn,
216841
+ chatStore: stores.chatStore,
216842
+ trace: deps.trace,
216843
+ ...stores.items ? { items: stores.items } : {},
216844
+ log: log3
216845
+ });
216846
+ } catch (err) {
216847
+ deps.onCompanyError?.(companyId, err);
216848
+ }
216849
+ };
216850
+ for (const hit of result.settled) await retire(hit);
216851
+ for (const { companyId, stores, plans } of result.groups) {
216852
+ for (const plan of plans) {
216853
+ try {
216854
+ const itemsTurnId = stores.items ? await resolveChatItemsTurnId(stores.items, plan.assistantMsgId, plan.turnId ?? fallbackTurnId(plan.runId)) : plan.turnId ?? fallbackTurnId(plan.runId);
216855
+ const turn = await stores.getTurnForRun?.(plan.chatSessionId, plan.runId);
216856
+ if (turn && turn.status !== "reserved" && turn.status !== "running") {
216857
+ const hit = { companyId, stores, plan, turn };
216858
+ result.settled.push(hit);
216859
+ await retire(hit);
216860
+ continue;
216861
+ }
216862
+ const handle = deps.recover(plan);
216863
+ const { done } = wireRecoveredChatTurn({
216864
+ plan,
216865
+ handle,
216866
+ liveChat: deps.liveChat,
216867
+ chatStore: stores.chatStore,
216868
+ trace: deps.trace,
216869
+ register: () => deps.register(plan, handle),
216870
+ unregister: () => deps.unregister(plan),
216871
+ ...deps.traceHealth ? { traceHealth: deps.traceHealth } : {},
216872
+ ...stores.items ? { items: stores.items, itemsTurnId } : {},
216873
+ log: log3
216874
+ });
216875
+ recovered.push({ companyId, plan, done });
216876
+ log3(`[chat-recovery] \u91CD\u6302 chat \u8F6E\uFF1Acompany=${companyId ?? "<default>"} session=${plan.chatSessionId} run=${plan.runId} dispatch=${plan.dispatchId}`);
216877
+ } catch (err) {
216878
+ result.skipped.push({ companyId, error: String(err) });
216879
+ deps.onCompanyError?.(companyId, err);
216880
+ }
216881
+ }
216882
+ }
216883
+ return { ...result, recovered };
216884
+ }
216508
216885
  var import_node_crypto45, asObj3, partsOf, maxPartSeq, exitFailed, exitErrorText, runTerminalPatch;
216509
216886
  var init_chat_recovery = __esm({
216510
216887
  "../server/src/chat-recovery.ts"() {
@@ -225719,7 +226096,7 @@ function reviewSummary(snap, reviews, events) {
225719
226096
  return { phase: "done", status: `\u300A${name}\u300B\u5BA1\u6838\u901A\u8FC7\u3002`, latest, pending: [] };
225720
226097
  }
225721
226098
  if (work?.acceptanceState === "rejected" || judgement === "rejected") {
225722
- return { phase: "done", status: "\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002", latest, pending: [] };
226099
+ return { phase: "done", status: `\u300A${name}\u300B\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002`, latest, pending: [] };
225723
226100
  }
225724
226101
  const endedWork = !!(work?.cancelledAt || work?.deadAt);
225725
226102
  if (endedWork && latest.every((r) => !!r.cancelledAt && reviewState(r) !== "retry")) {
@@ -225772,6 +226149,9 @@ var init_workorder_manager = __esm({
225772
226149
  function isPendingActor(id) {
225773
226150
  return typeof id === "string" && id.startsWith("pending:");
225774
226151
  }
226152
+ function isAgentActor(id) {
226153
+ return typeof id === "string" && id.startsWith("actor:agent:");
226154
+ }
225775
226155
  function isDispatchLayerFailure(detail) {
225776
226156
  const reason = detail?.reason;
225777
226157
  return typeof reason === "string" && reason.startsWith("dispatch ");
@@ -226307,6 +226687,22 @@ function buildWorkorderActivity(input) {
226307
226687
  const created = events.filter((r) => r.event.kind === "review.create" && ids2.has(r.event.reviewId));
226308
226688
  const dates = summary.latest.flatMap((r) => [r.createdAt, r.startedAt, r.endedAt, r.decidedAt, r.cancelledAt, r.retryAt].filter((v2) => !!v2));
226309
226689
  const pending = summary.pending.filter((r) => r.reviewerActorId.startsWith("actor:human:") && (!input.viewerActorId || r.reviewerActorId === input.viewerActorId));
226690
+ const groups = [...new Set(summary.latest.map((r) => r.reviewGroup))];
226691
+ const reviewNotes = summary.latest.flatMap((r) => {
226692
+ const text5 = r.note?.trim();
226693
+ if (!text5) return [];
226694
+ const rejected = r.verdict === "request_changes";
226695
+ const label = ACTIVITY_COPY.reviewNoteLabel(rejected);
226696
+ return [{
226697
+ id: r.id,
226698
+ // 同一个人担多个评审组时才标组号——单组标了纯属噪声(卡上本来就只有这一个人)。
226699
+ label: groups.length > 1 ? `\u8BC4\u5BA1\u7EC4 ${groups.indexOf(r.reviewGroup) + 1} \xB7 ${label}` : label,
226700
+ text: text5,
226701
+ tone: rejected ? "reject" : "note"
226702
+ }];
226703
+ });
226704
+ const revieweeId = workById.get(workId)?.assigneeActorId;
226705
+ const reviewLine = reviewNotes.length && revieweeId && revieweeId !== reviewerId ? reviewThreadSessionId(revieweeId, reviewerId) : void 0;
226310
226706
  visibleReviews.set(cardId, {
226311
226707
  id: cardId,
226312
226708
  reviewWorkId: workId,
@@ -226320,8 +226716,13 @@ function buildWorkorderActivity(input) {
226320
226716
  executorId: reviewerId,
226321
226717
  phase: summary.phase,
226322
226718
  status: summary.status,
226323
- detail: summary.latest.length === 1 ? summary.latest[0].note ? `${summary.latest[0].verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1"}\uFF1A${summary.latest[0].note}` : void 0 : summary.latest.map((r) => `${nameOf(r.reviewerActorId)}\uFF08\u8BC4\u5BA1\u7EC4 ${[...new Set(summary.latest.map((r2) => r2.reviewGroup))].indexOf(r.reviewGroup) + 1}\uFF09\uFF1A${reviewLabel(r)}${r.note ? `
226324
- ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1"}\uFF1A${r.note}` : ""}`).join("\n\n"),
226719
+ /* 同一人担多个责任组时,展开区留一份**逐组结论**(只有判定词、没有意见正文)——小状态只有
226720
+ 一句总判定,说不清哪个组过了哪个组没过。意见正文一律走下面的意见条。 */
226721
+ ...summary.latest.length > 1 ? {
226722
+ detail: summary.latest.map((r) => `${nameOf(r.reviewerActorId)}\uFF08\u8BC4\u5BA1\u7EC4 ${groups.indexOf(r.reviewGroup) + 1}\uFF09\uFF1A${reviewLabel(r)}`).join("\n")
226723
+ } : {},
226724
+ ...reviewNotes.length ? { reviewNotes } : {},
226725
+ ...reviewLine ? { reviewThreadSessionId: reviewLine } : {},
226325
226726
  artifacts: artifactsByWork.get(workId) ?? [],
226326
226727
  actions: pending.length ? [
226327
226728
  { kind: "review", label: BUTTON.approve, target: first.nodeId, revisionId: workId, verdict: "approve" },
@@ -226361,7 +226762,7 @@ ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u
226361
226762
  if (issueById.get(issueId)?.kind === "gap") {
226362
226763
  d.traceAvailable = true;
226363
226764
  d.traceWorkIds = gapOrigins.get(issueId);
226364
- } else {
226765
+ } else if (!issueId.startsWith(REWORK_ANNOTATION_PREFIX)) {
226365
226766
  d.traceWorkIds = issueWorks.get(issueId);
226366
226767
  }
226367
226768
  }
@@ -226382,24 +226783,29 @@ ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u
226382
226783
  }
226383
226784
  return void 0;
226384
226785
  };
226385
- const cards = drafts.map((d) => ({
226386
- id: d.id,
226387
- seq: d.seq,
226388
- at: d.at,
226389
- updatedAt: d.updatedAt,
226390
- ...d.nodeId ? { nodeId: d.nodeId, nodeTitle: nodeName(d.nodeId) } : {},
226391
- executor: ref2(d.executorId),
226392
- ...d.reviewWorkId ? { reviewWorkId: d.reviewWorkId, reviewers: d.reviewerIds?.map(ref2) } : {},
226393
- ...d.handActorId ? { handActor: ref2(d.handActorId) } : {},
226394
- phase: d.phase,
226395
- status: d.status,
226396
- ...d.detail ? { detail: d.detail } : {},
226397
- artifacts: d.artifacts,
226398
- actions: d.actions,
226399
- ...d.traceAvailable ? { traceAvailable: true } : {},
226400
- ...d.traceWorkIds?.length ? { traceWorkIds: d.traceWorkIds, traceAvailable: true } : {},
226401
- .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(runIdOfCard(d))
226402
- }));
226786
+ const cards = drafts.map((d) => {
226787
+ const traced = isAgentActor(d.executorId);
226788
+ return {
226789
+ id: d.id,
226790
+ seq: d.seq,
226791
+ at: d.at,
226792
+ updatedAt: d.updatedAt,
226793
+ ...d.nodeId ? { nodeId: d.nodeId, nodeTitle: nodeName(d.nodeId) } : {},
226794
+ executor: ref2(d.executorId),
226795
+ ...d.reviewWorkId ? { reviewWorkId: d.reviewWorkId, reviewers: d.reviewerIds?.map(ref2) } : {},
226796
+ ...d.handActorId ? { handActor: ref2(d.handActorId) } : {},
226797
+ phase: d.phase,
226798
+ status: d.status,
226799
+ ...d.detail ? { detail: d.detail } : {},
226800
+ ...d.reviewNotes?.length ? { reviewNotes: d.reviewNotes } : {},
226801
+ ...d.reviewThreadSessionId ? { reviewThreadSessionId: d.reviewThreadSessionId } : {},
226802
+ artifacts: d.artifacts,
226803
+ actions: d.actions,
226804
+ ...traced && d.traceAvailable ? { traceAvailable: true } : {},
226805
+ ...d.traceWorkIds?.length ? { traceWorkIds: d.traceWorkIds, ...traced ? { traceAvailable: true } : {} } : {},
226806
+ .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(traced ? runIdOfCard(d) : void 0)
226807
+ };
226808
+ });
226403
226809
  return { workorderId, cards, truncated };
226404
226810
  }
226405
226811
  var ACTIVITY_EVENT_KINDS, ACTIVITY_EVENT_LIMIT, ACTIVITY_COPY, REWORK_ANNOTATION_PREFIX, ENGINE_RETRY_EXHAUSTED, BUTTON;
@@ -226465,15 +226871,13 @@ var init_activity2 = __esm({
226465
226871
  doneAwaitingReview: (name) => `\u300A${name}\u300B\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u5BA1\u6838\u3002`,
226466
226872
  done: (name) => `\u300A${name}\u300B\u5DF2\u5B8C\u6210\u3002`,
226467
226873
  finalDelivered: (acceptor) => `\u6700\u7EC8\u4EA4\u4ED8\u5DF2\u4E0A\u4F20\uFF0C\u7B49\u5F85 ${acceptor} \u9A8C\u6536\u3002`,
226468
- reviewPending: (name) => `\u8BF7\u786E\u8BA4\u300A${name}\u300B\u662F\u5426\u5BA1\u6838\u901A\u8FC7\u3002`,
226469
- reviewing: (name) => `\u6B63\u5728\u5BA1\u6838 \u300A${name}\u300B\u3002`,
226470
- reviewApproved: (name) => `\u300A${name}\u300B\u5BA1\u6838\u901A\u8FC7\u3002`,
226471
- reviewRejected: "\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002",
226472
- reviewRejectedDetail: (note) => `\u5BA1\u6838\u4E0D\u901A\u8FC7\uFF1A${note}`,
226473
- reviewTimedOut: (name) => `\u300A${name}\u300B\u7684\u5BA1\u6838\u672A\u5728\u65F6\u9650\u5185\u5B8C\u6210\u3002`,
226474
- /** 评审**连败熔断**(`review.kill`,`engine/activate.ts` 只对 agent 评审员)——不是超时,
226475
- 此前与 {@link reviewTimedOut} 共用一句,把被熔断的评审说成「未在时限内完成」。 */
226476
- reviewKilled: (name) => `\u300A${name}\u300B\u7684\u8BC4\u5BA1\u8FDE\u7EED\u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u3002`,
226874
+ /* 🚨 审核卡那一族文案(待审 / 正在审 / 通过 / 不通过 / 超时 / 熔断)**不在这里**:
226875
+ 审核卡改成按「被审交付 × 评审人」合卡之后,它的小状态由 `review-activity.ts`
226876
+ `reviewSummary()` 按该评审人的**有效判定**整句给出,不是逐事件套模板。这里此前留着一份
226877
+ 同名同义的副本,六个 key 全是零引用——我 2026-09-09 改「审核不通过」文案时先改到了这份
226878
+ 死副本上,页面纹丝不动。删掉,别再留第二个事实源。
226879
+ 留在这里的只有下面这一句——它不是小状态,是**审核意见条**上那个判定词,由本文件拼卡时用。 */
226880
+ reviewNoteLabel: (rejected) => rejected ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1",
226477
226881
  acceptPending: "\u8BF7\u9A8C\u6536\u6700\u7EC8\u4EA4\u4ED8\u3002",
226478
226882
  accepted: "\u9A8C\u6536\u6700\u7EC8\u4EA4\u4ED8\u5DF2\u5B8C\u6210\u9A8C\u6536\u3002",
226479
226883
  acceptedNode: (name) => `\u300A${name}\u300B\u5DF2\u9A8C\u6536\u3002`,
@@ -229337,7 +229741,15 @@ function collabDomain(opts) {
229337
229741
  }
229338
229742
  const text5 = raw;
229339
229743
  try {
229744
+ const snap = await kernel.getStore().transaction((tx) => tx.loadWorkorder(ws));
229745
+ if (snap?.workorder.dispatchPaused && !isHumanActor(req.auth.actor)) {
229746
+ return { status: 403, body: { error: { code: "forbidden", message: "\u6682\u505C\u671F\u95F4\u5F55\u5165\u5DE5\u5355\u6B63\u6587\u4EC5\u9650\u4EBA\u7C7B\u64CD\u4F5C" } } };
229747
+ }
229340
229748
  const contentRef = await blobs.put(new TextEncoder().encode(text5));
229749
+ if (snap?.workorder.dispatchPaused) {
229750
+ await kernel.recordPausedRevision({ artifactId: rootId, actor: req.auth.actor, contentRef, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
229751
+ return { status: 200, body: { overview: text5, concluded: false } };
229752
+ }
229341
229753
  await kernel.proposeRevision({ artifactId: rootId, actor: req.auth.actor, contentRef, reason: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
229342
229754
  await kernel.advanceQueue(rootId, req.auth.actor);
229343
229755
  const result = await kernel.conclude({ artifactId: rootId, actor: req.auth.actor, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
@@ -229345,7 +229757,7 @@ function collabDomain(opts) {
229345
229757
  } catch (err) {
229346
229758
  if (err instanceof KernelError) {
229347
229759
  return {
229348
- status: 400,
229760
+ status: err.code === "revision-conflict" ? 409 : 400,
229349
229761
  body: { error: { code: err.code ?? "kernel_error", message: err.message } }
229350
229762
  };
229351
229763
  }
@@ -232404,12 +232816,83 @@ function buildCommentThread(input) {
232404
232816
  bubbles
232405
232817
  };
232406
232818
  }
232819
+ function isReviewThreadSessionId(sessionId) {
232820
+ return typeof sessionId === "string" && sessionId.startsWith(REVIEW_THREAD_SESSION_PREFIX);
232821
+ }
232822
+ function partiesOfReviewThreadSessionId(sessionId) {
232823
+ if (!isReviewThreadSessionId(sessionId)) return null;
232824
+ const parts = sessionId.slice(REVIEW_THREAD_SESSION_PREFIX.length).split("|");
232825
+ return parts.length === 2 && parts[0] && parts[1] ? [parts[0], parts[1]] : null;
232826
+ }
232827
+ function buildReviewThread(input) {
232828
+ const parties = partiesOfReviewThreadSessionId(input.sessionId);
232829
+ if (!parties || !input.snap) return null;
232830
+ const [a, b2] = parties;
232831
+ const works = new Map(input.snap.works.map((w2) => [w2.id, w2]));
232832
+ const rows = input.snap.reviews.filter((r) => {
232833
+ const reviewee = works.get(r.targetWorkId)?.assigneeActorId;
232834
+ if (!reviewee || reviewee === r.reviewerActorId) return false;
232835
+ const pair = [r.reviewerActorId, reviewee].sort();
232836
+ return pair[0] === a && pair[1] === b2;
232837
+ });
232838
+ if (rows.length === 0) return null;
232839
+ const { ref: ref2 } = input;
232840
+ const nameOf = (id) => ref2(id).name?.trim() || "\u2014";
232841
+ const mention = (author) => nameOf(author === a ? b2 : a);
232842
+ const bubble = (o) => ({
232843
+ id: o.id,
232844
+ at: o.at,
232845
+ author: ref2(o.author),
232846
+ text: `@${mention(o.author)} ${o.body}`,
232847
+ issueId: o.issueId,
232848
+ kind: o.kind
232849
+ });
232850
+ const bubbles = [];
232851
+ for (const workId of new Set(rows.map((r) => r.targetWorkId))) {
232852
+ const w2 = works.get(workId);
232853
+ const body2 = w2?.conclusion?.trim();
232854
+ if (!w2 || !body2) continue;
232855
+ bubbles.push(bubble({
232856
+ id: `${workId}:delivered`,
232857
+ at: w2.endedAt || w2.createdAt,
232858
+ author: w2.assigneeActorId,
232859
+ body: body2,
232860
+ issueId: workId,
232861
+ kind: "raised"
232862
+ }));
232863
+ }
232864
+ for (const r of rows) {
232865
+ const note = r.note?.trim();
232866
+ if (!note) continue;
232867
+ const label = r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1";
232868
+ bubbles.push(bubble({
232869
+ id: `${r.id}:verdict`,
232870
+ at: r.decidedAt || r.endedAt || r.createdAt,
232871
+ author: r.reviewerActorId,
232872
+ body: `${label}\uFF1A${note}`,
232873
+ issueId: r.targetWorkId,
232874
+ kind: "reply"
232875
+ }));
232876
+ }
232877
+ if (bubbles.length === 0) return null;
232878
+ bubbles.sort((x2, y) => x2.at.localeCompare(y.at) || x2.id.localeCompare(y.id));
232879
+ const first = rows[0];
232880
+ const revieweeId = works.get(first.targetWorkId).assigneeActorId;
232881
+ return {
232882
+ sessionId: input.sessionId,
232883
+ workorderId: input.workorderId,
232884
+ executor: ref2(revieweeId),
232885
+ coordinator: ref2(first.reviewerActorId),
232886
+ bubbles
232887
+ };
232888
+ }
232407
232889
  var init_gap_thread = __esm({
232408
232890
  "../server/src/domains/collab/gap-thread.ts"() {
232409
232891
  "use strict";
232410
232892
  init_src();
232411
232893
  init_workorder_manager();
232412
232894
  init_src();
232895
+ init_src();
232413
232896
  }
232414
232897
  });
232415
232898
 
@@ -233047,12 +233530,12 @@ function createChatSessionsDomain(opts) {
233047
233530
  router.get("/api/workorders/:workorderId/gap-thread", async (req) => {
233048
233531
  const workorderId = req.params["workorderId"];
233049
233532
  const sessionId = req.query.get("session") ?? "";
233050
- if (!isGapThreadSessionId(sessionId) && !isCommentThreadSessionId(sessionId)) {
233533
+ if (!isGapThreadSessionId(sessionId) && !isCommentThreadSessionId(sessionId) && !isReviewThreadSessionId(sessionId)) {
233051
233534
  throw new ApiError(404, "NOT_FOUND", "thread not found");
233052
233535
  }
233053
233536
  const { snap, ref: ref2 } = await gapThreadInputsFor(req, workorderId);
233054
233537
  const nodeOwner = (nodeId) => snap?.nodes?.find((n) => String(n.id) === nodeId)?.assigneeActorId ?? snap?.workorder.ownerActorId;
233055
- const view = !snap ? null : isCommentThreadSessionId(sessionId) ? buildCommentThread({ workorderId, snap, ref: ref2, sessionId, nodeOwner }) : buildGapThread({ workorderId, snap, ref: ref2, sessionId });
233538
+ const view = !snap ? null : isReviewThreadSessionId(sessionId) ? buildReviewThread({ workorderId, snap, ref: ref2, sessionId }) : isCommentThreadSessionId(sessionId) ? buildCommentThread({ workorderId, snap, ref: ref2, sessionId, nodeOwner }) : buildGapThread({ workorderId, snap, ref: ref2, sessionId });
233056
233539
  if (!view) throw new ApiError(404, "NOT_FOUND", "gap thread not found");
233057
233540
  return { status: 200, body: view };
233058
233541
  });
@@ -246157,6 +246640,21 @@ var init_postgres_chat_sessions = __esm({
246157
246640
  );
246158
246641
  return r.rows.map(rowToMessage);
246159
246642
  }
246643
+ /** 按本 schema 中的会话 + run 精确找轮,不能用 activeTurn:已取消的轮已不占 active slot。 */
246644
+ async getTurnForRun(sessionId, runId) {
246645
+ const r = await this.pool.query(
246646
+ `SELECT id, status, completed_at, last_error FROM ${this.s}.chat_session_turns
246647
+ WHERE chat_session_id = $1 AND assistant_run_id = $2 ORDER BY reserved_at DESC LIMIT 1`,
246648
+ [sessionId, runId]
246649
+ );
246650
+ const row = r.rows[0];
246651
+ return row ? {
246652
+ id: row.id,
246653
+ status: row.status,
246654
+ completedAt: row.completed_at == null ? null : new Date(row.completed_at).toISOString(),
246655
+ lastError: row.last_error ?? null
246656
+ } : null;
246657
+ }
246160
246658
  async getSession(id) {
246161
246659
  const r = await this.pool.query(`SELECT * FROM ${this.s}.chat_sessions WHERE id = $1`, [id]);
246162
246660
  return r.rows[0] ? rowToSession(r.rows[0]) : null;
@@ -256992,6 +257490,7 @@ async function startServe(opts) {
256992
257490
  if (!cid || cid === defaultCompanyId) return "";
256993
257491
  return ` [${cid}]`;
256994
257492
  };
257493
+ const engineSchemaFor = (companyId) => companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId);
256995
257494
  let getEngine = async () => {
256996
257495
  throw new Error("company engine router not initialized");
256997
257496
  };
@@ -257097,7 +257596,7 @@ async function startServe(opts) {
257097
257596
  if (seeded > 0) console.log(`[serve] \u6536\u4EF6\u7BB1\u5DF2\u8BFB\u6C34\u4F4D\uFF1A\u64AD\u79CD ${seeded} \u6761\u5386\u53F2\u4F1A\u8BDD\uFF08ADR-0086 \u9996\u6B21\u4E0A\u7EBF\uFF09`);
257098
257597
  nodeStore = await PostgresNodeStore.open(pgPool, sharedSchema);
257099
257598
  nodeTokens = await PostgresNodeTokenStore.open(pgPool, sharedSchema);
257100
- console.log(`[serve] \u4E8B\u5B9E/\u72B6\u6001\u4F53\u7CFB\uFF1APostgres \u9ED8\u8BA4\u516C\u53F8 schema=${defaultCompanySchema}\uFF08oplog / registry / blobs / artifacts / chat\uFF09\uFF1B\u5171\u4EAB\u63A7\u5236\u9762 schema=${sharedSchema}\uFF08companies / nodes / model_prices / tenant_storage_*\uFF09`);
257599
+ console.log(`[serve] \u4E8B\u5B9E/\u72B6\u6001\u4F53\u7CFB\uFF1APostgres \u9ED8\u8BA4\u516C\u53F8 schema=${defaultCompanySchema}\uFF08oplog / registry / blobs / artifacts / chat\uFF09\uFF1B\u5171\u4EAB\u63A7\u5236\u9762 schema=${sharedSchema}\uFF08companies / nodes / model_prices / tenant_storage_*\uFF09\uFF1Bchat \u6062\u590D/\u8BA4\u9886/\u6E05\u626B\uFF1A\u6309 active \u516C\u53F8\u9010\u5BB6\u6247\u51FA\uFF08\u9ED8\u8BA4\u516C\u53F8\u4F18\u5148\uFF09\uFF0C\u6BCF\u6B21\u5224\u5B9A\u7684\u516C\u53F8\u4E0E\u4F9D\u636E\u89C1 [chat-claim] / [chat-recovery] \u884C`);
257101
257600
  } else {
257102
257601
  projectStateStore = await FileProjectStateStore.open(path29.join(opts.dir, "artifact-document-projects.json"));
257103
257602
  artifactStateStore = await FileArtifactStateStore.open(path29.join(opts.dir, "artifact-document-state.json"));
@@ -257108,7 +257607,6 @@ async function startServe(opts) {
257108
257607
  console.log("[serve] \u8282\u70B9/\u8FD0\u884C\u65F6\u4F53\u7CFB\uFF1A\u672C\u5730 JSON dev store");
257109
257608
  }
257110
257609
  const chatRecoveryStore = platformChatRecovery ?? chatSessionStore;
257111
- const chatRecoveryItems = pgPool ? await PostgresChatItemStore.open(pgPool, defaultCompanySchema) : void 0;
257112
257610
  const knowledgeConfigStore = await FileKnowledgeConfigStore.open(path29.join(opts.dir, "knowledge.json"));
257113
257611
  const knowledgeRunStore = await FileKnowledgeRunStore.open(path29.join(opts.dir, "knowledge-runs.json"));
257114
257612
  const knowledgeDeploymentId = await loadOrCreateKnowledgeDeploymentId(path29.join(opts.dir, "knowledge-deployment-id"));
@@ -257813,7 +258311,7 @@ async function startServe(opts) {
257813
258311
  id: c.id,
257814
258312
  // 默认公司的引擎表仍在共享 schema(ADR 0164 §D5 的 RP0,搬迁另立单)——这条映射
257815
258313
  // 搞错,默认公司的工单会一张都对不上,回填会把它们全判成「无证据」。
257816
- engineSchema: c.id === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(c.id)
258314
+ engineSchema: engineSchemaFor(c.id)
257817
258315
  }));
257818
258316
  const res = await backfillTenancyColumns(tenancyPool, { schema: defaultCompanySchema, companies: companies2, apply: true });
257819
258317
  const wrote = res.rows.reduce((n, r) => n + r.updated, 0);
@@ -257988,28 +258486,28 @@ async function startServe(opts) {
257988
258486
  const assistants2 = await PostgresAssistantBindStore.open(pgPool, loc.schemaName, { companyId });
257989
258487
  const humanPrefs2 = await PostgresHumanPrefsStore.open(pgPool, loc.schemaName);
257990
258488
  const ownedReadMarkers2 = await PostgresReadMarkerStore.open(pgPool, loc.schemaName, {
257991
- chatSessionsSchema: companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258489
+ chatSessionsSchema: engineSchemaFor(companyId)
257992
258490
  });
257993
258491
  const ownedMemory2 = await PostgresActorMemoryStore.open(pgPool, loc.schemaName);
257994
258492
  const ownedDrafts2 = await PostgresWorkorderDraftStore.open(
257995
258493
  pgPool,
257996
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258494
+ engineSchemaFor(companyId)
257997
258495
  );
257998
258496
  const ownedPlannerIssues2 = await PostgresWorkorderDraftPlannerIssuesStore.open(
257999
258497
  pgPool,
258000
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258498
+ engineSchemaFor(companyId)
258001
258499
  );
258002
258500
  const ownedLedger2 = await PostgresDispatchStore.open(
258003
258501
  pgPool,
258004
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258502
+ engineSchemaFor(companyId)
258005
258503
  );
258006
258504
  const ownedBlobs2 = await PostgresBlobStore.open(
258007
258505
  pgPool,
258008
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258506
+ engineSchemaFor(companyId)
258009
258507
  );
258010
258508
  const ownedTrace2 = await PostgresTraceStore.open(
258011
258509
  tracePool,
258012
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258510
+ engineSchemaFor(companyId)
258013
258511
  );
258014
258512
  return { owned: owned2, ownedOrgRegistry: ownedOrgRegistry2, types: types3, assistants: assistants2, humanPrefs: humanPrefs2, ownedReadMarkers: ownedReadMarkers2, ownedMemory: ownedMemory2, ownedDrafts: ownedDrafts2, ownedPlannerIssues: ownedPlannerIssues2, ownedLedger: ownedLedger2, ownedBlobs: ownedBlobs2, ownedTrace: ownedTrace2 };
258015
258513
  }
@@ -258265,7 +258763,7 @@ async function startServe(opts) {
258265
258763
  const id = companyId ?? defaultCompanyId;
258266
258764
  const hit = delegationStores.get(id);
258267
258765
  if (hit) return hit;
258268
- const schemaFor = id === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(id);
258766
+ const schemaFor = engineSchemaFor(id);
258269
258767
  const store = pgPool ? await PostgresDelegationStore.open(pgPool, schemaFor) : await FileDelegationStore.open(path29.join(opts.dir, `delegations-${companyEngineDirName(id)}.json`));
258270
258768
  delegationStores.set(id, store);
258271
258769
  return store;
@@ -258516,7 +259014,7 @@ async function startServe(opts) {
258516
259014
  const knowledgeGovernanceStoreFor = (companyId) => {
258517
259015
  const cached3 = knowledgeGovernanceStores.get(companyId);
258518
259016
  if (cached3) return cached3;
258519
- const knowledgeSchema = companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId);
259017
+ const knowledgeSchema = engineSchemaFor(companyId);
258520
259018
  const opened = pgDsn && pgPool ? withSchemaDdlLock(pgPool, knowledgeSchema, () => PostgresKnowledgeGovernanceStore.open(pgPool, knowledgeSchema, knowledgeSnapshotKey)) : FileKnowledgeGovernanceStore.open(path29.join(
258521
259019
  opts.dir,
258522
259020
  companyId === defaultCompanyId ? "knowledge-governance.json" : path29.join("companies", companyEngineDirName(companyId), "knowledge-governance.json")
@@ -259991,60 +260489,101 @@ async function startServe(opts) {
259991
260489
  });
259992
260490
  };
259993
260491
  {
259994
- const listRunningChatRows = chatRecoveryStore.listRunningAssistantMessages?.bind(chatRecoveryStore);
259995
- if (listRunningChatRows) {
260492
+ const defaultListRunningChatRows = chatRecoveryStore.listRunningAssistantMessages?.bind(chatRecoveryStore);
260493
+ if (defaultListRunningChatRows) {
260494
+ const chatRecoveryCompanies = async () => {
260495
+ if (!chatStoreRouter) return [void 0];
260496
+ const active = (await controlPlaneStore.listCompanies()).filter((c) => c.status === "active").map((c) => c.id);
260497
+ return [defaultCompanyId, ...active.filter((id) => id !== defaultCompanyId)];
260498
+ };
260499
+ const chatRecoveryPlanes = /* @__PURE__ */ new Map();
260500
+ const chatRecoveryFor = async (companyId) => {
260501
+ if (!pgPool || !platformChatRecovery) {
260502
+ return { listRunningAssistantMessages: defaultListRunningChatRows, chatStore: chatSessionStore };
260503
+ }
260504
+ const id = companyId ?? defaultCompanyId;
260505
+ const hit = chatRecoveryPlanes.get(id);
260506
+ if (hit) return hit;
260507
+ const sessions = id === defaultCompanyId ? platformChatRecovery : PlatformChatRecoveryStore.open(pgPool, engineSchemaFor(id));
260508
+ const items = await chatItemsFor(id);
260509
+ const plane = {
260510
+ listRunningAssistantMessages: () => sessions.listRunningAssistantMessages(),
260511
+ chatStore: sessions,
260512
+ getTurnForRun: (sid, rid) => sessions.getTurnForRun(sid, rid),
260513
+ ...items ? { items } : {}
260514
+ };
260515
+ chatRecoveryPlanes.set(id, plane);
260516
+ return plane;
260517
+ };
260518
+ const chatRecoveryLastError = /* @__PURE__ */ new Map();
260519
+ const chatRecoveryCompanyWarn = (label) => (companyId, err) => {
260520
+ const key = `${label}::${companyId ?? "<default>"}`;
260521
+ const msg = String(err);
260522
+ if (chatRecoveryLastError.get(key) === msg) return;
260523
+ chatRecoveryLastError.set(key, msg);
260524
+ console.warn(`[chat-recovery] ${label}\uFF1A\u516C\u53F8 ${companyId ?? "<default>"} \u8FD9\u4E00\u8F6E\u5931\u8D25\uFF08\u5176\u4F59\u516C\u53F8\u7EE7\u7EED\uFF09\uFF1A${msg}`);
260525
+ };
259996
260526
  let chatRecoveryChain = Promise.resolve();
259997
260527
  const enqueueChatRecovery = (label, step) => {
259998
260528
  chatRecoveryChain = chatRecoveryChain.then(step).catch((err) => console.error(`[chat-recovery] ${label}\u5931\u8D25: ${String(err)}`));
259999
260529
  };
260000
- const recoverChatTurns = async (daemonId, activeSessions) => {
260001
- if (!chatRemoteAdapter) return;
260002
- const plans = await planChatTurnRecovery({
260003
- activeSessions,
260004
- listRunningAssistantMessages: listRunningChatRows,
260005
- getRun: (id) => fanOutTrace.getRun(id),
260006
- isWired: (artifactId) => chatLiveSessions.has(`chat::${artifactId}`)
260007
- });
260008
- for (const plan of plans) {
260009
- const handle = chatRemoteAdapter.recover(plan.dispatchId, daemonId, plan.runtimeKind, plan.actor);
260010
- const jobKey = `chat::${plan.artifactId}`;
260011
- const itemsTurnId = chatRecoveryItems ? await resolveChatItemsTurnId(chatRecoveryItems, plan.assistantMsgId, fallbackTurnId(plan.runId)) : fallbackTurnId(plan.runId);
260012
- wireRecoveredChatTurn({
260013
- plan,
260014
- handle,
260015
- liveChat,
260016
- chatStore: chatRecoveryStore,
260017
- trace: runRoutedTrace,
260018
- register: () => chatLiveSessions.set(jobKey, {
260019
- artifactId: plan.artifactId,
260020
- sessionId: plan.runId,
260021
- dispatchId: plan.dispatchId,
260022
- kill: () => handle.kill()
260023
- }),
260024
- unregister: () => {
260025
- chatLiveSessions.delete(jobKey);
260026
- },
260027
- traceHealth,
260028
- ...chatRecoveryItems ? { items: chatRecoveryItems, itemsTurnId } : {},
260029
- log: (m2) => console.log(m2)
260030
- });
260031
- console.log(`[chat-recovery] \u91CD\u6302 chat \u8F6E\uFF1Asession=${plan.chatSessionId} run=${plan.runId} node=${daemonId}`);
260032
- }
260033
- };
260530
+ const recoverChatTurns = async (daemonId, activeSessions, opts2) => recoverChatTurnsAcrossCompanies({
260531
+ activeSessions,
260532
+ listCompanies: chatRecoveryCompanies,
260533
+ storesFor: chatRecoveryFor,
260534
+ getRun: (id) => fanOutTrace.getRun(id),
260535
+ isWired: (artifactId) => chatLiveSessions.has(`chat::${artifactId}`),
260536
+ ...opts2.stopOnFirstHit ? { stopOnFirstHit: true } : {},
260537
+ onCompanyError: chatRecoveryCompanyWarn(opts2.label),
260538
+ liveChat,
260539
+ trace: runRoutedTrace,
260540
+ traceHealth,
260541
+ recover: (plan) => chatRemoteAdapter.recover(plan.dispatchId, daemonId, plan.runtimeKind, plan.actor),
260542
+ register: (plan, handle) => chatLiveSessions.set(`chat::${plan.artifactId}`, {
260543
+ artifactId: plan.artifactId,
260544
+ sessionId: plan.runId,
260545
+ dispatchId: plan.dispatchId,
260546
+ kill: () => handle.kill()
260547
+ }),
260548
+ unregister: (plan) => {
260549
+ chatLiveSessions.delete(`chat::${plan.artifactId}`);
260550
+ },
260551
+ evict: (dispatchId) => {
260552
+ if (!hub.evictSession(dispatchId, "reaped")) hub.dispatch(daemonId, { type: "kill", dispatchId });
260553
+ },
260554
+ log: (message) => console.log(message)
260555
+ });
260034
260556
  hub.registerSessionClaimResolver?.(async (id) => {
260035
260557
  if (!id.jobArtifactId || !chatRemoteAdapter) return "unknown";
260036
260558
  const jobKey = `chat::${id.jobArtifactId}`;
260037
- if (chatLiveSessions.has(jobKey)) return "live";
260559
+ if (chatLiveSessions.has(jobKey)) {
260560
+ console.log(
260561
+ `[chat-claim] dispatch=${id.dispatchId} artifact=${id.jobArtifactId} \u7ED3\u8BBA=live\uFF08\u4F9D\u636E\uFF1A\u672C\u8FDB\u7A0B\u5728\u9014\u8868\u91CC\u5DF2\u6709\u8FD9\u4E00\u8F6E\uFF0C\u65E0\u9700\u91CD\u6302\uFF09`
260562
+ );
260563
+ return "live";
260564
+ }
260565
+ let outcome;
260038
260566
  await new Promise((resolve10) => {
260039
- chatRecoveryChain = chatRecoveryChain.then(() => recoverChatTurns(id.nodeId, [
260040
- { dispatchId: id.dispatchId, sessionId: id.sessionId, jobArtifactId: id.jobArtifactId }
260041
- ])).catch((err) => {
260567
+ chatRecoveryChain = chatRecoveryChain.then(async () => {
260568
+ outcome = await recoverChatTurns(
260569
+ id.nodeId,
260570
+ [{ dispatchId: id.dispatchId, sessionId: id.sessionId, jobArtifactId: id.jobArtifactId }],
260571
+ { label: "\u8BA4\u9886", stopOnFirstHit: true }
260572
+ );
260573
+ }).catch((err) => {
260042
260574
  console.error(`[chat-recovery] session_hello \u8BA4\u9886\u5931\u8D25: ${String(err)}`);
260043
260575
  }).then(() => {
260044
260576
  resolve10();
260045
260577
  });
260046
260578
  });
260047
- return chatLiveSessions.has(jobKey) ? "live" : "unknown";
260579
+ if (outcome?.settled.some((hit2) => hit2.plan.dispatchId === id.dispatchId)) return "settled";
260580
+ const live = chatLiveSessions.has(jobKey);
260581
+ const hit = outcome?.recovered.find((r) => r.plan.artifactId === id.jobArtifactId);
260582
+ const skipped = outcome?.skipped ?? [];
260583
+ console.log(
260584
+ `[chat-claim] dispatch=${id.dispatchId} artifact=${id.jobArtifactId} company=${hit ? hit.companyId ?? "<default>" : "-"} session=${hit?.plan.chatSessionId ?? "-"} run=${hit?.plan.runId ?? "-"} \u7ED3\u8BBA=${live ? "live" : "unknown"}\uFF08\u4F9D\u636E\uFF1A${live ? `\u5728\u8BE5\u516C\u53F8 schema \u91CC\u5BF9\u4E0A\u4E00\u6761 running assistant \u884C\u3001\u5176 run \u4ECD running\uFF0C\u5DF2\u91CD\u6302` : `\u626B\u8FC7 ${outcome?.scanned ?? 0} \u5BB6\u516C\u53F8\uFF0C\u65E0 running \u884C\u4E0E\u5B83\u5BF9\u4E0A` + (skipped.length ? `\uFF1B\u53E6\u6709 ${skipped.length} \u5BB6\u6CA1\u67E5\u6210\uFF08${skipped.map((x2) => x2.companyId ?? "<default>").join("\u3001")}\uFF09` : "")}\uFF09` + (live ? "" : "\u3002unknown \u53EA\u662F\u300C\u6211\u4E0D\u77E5\u9053\u300D\uFF0C\u5904\u7F6E\u4EA4\u51B7\u542F\u52A8\u5BBD\u9650\u90A3\u4E00\u7EA7")
260585
+ );
260586
+ return live ? "live" : "unknown";
260048
260587
  });
260049
260588
  hub.addMessageListener((daemonId, msg) => {
260050
260589
  if (msg.type !== "runtime_models") return;
@@ -260089,7 +260628,14 @@ async function startServe(opts) {
260089
260628
  });
260090
260629
  hub.addMessageListener((daemonId, msg) => {
260091
260630
  if (msg.type === "hello" && msg.meta.activeSessions?.length) {
260092
- enqueueChatRecovery("hello \u5BF9\u8D26", () => recoverChatTurns(daemonId, msg.meta.activeSessions));
260631
+ enqueueChatRecovery("hello \u5BF9\u8D26", async () => {
260632
+ const outcome = await recoverChatTurns(daemonId, msg.meta.activeSessions, { label: "hello \u5BF9\u8D26" });
260633
+ if (outcome.recovered.length === 0 && outcome.skipped.length === 0) return;
260634
+ const byCompany = outcome.recovered.map((r) => `${r.companyId ?? "<default>"}:${r.plan.chatSessionId}`).join("\u3001");
260635
+ console.log(
260636
+ `[chat-recovery] hello \u5BF9\u8D26\uFF1Anode=${daemonId} \u626B\u8FC7 ${outcome.scanned} \u5BB6\u516C\u53F8\uFF0C\u91CD\u6302 ${outcome.recovered.length} \u8F6E${byCompany ? `\uFF08${byCompany}\uFF09` : ""}` + (outcome.skipped.length ? `\uFF1B${outcome.skipped.length} \u5BB6\u6CA1\u67E5\u6210\uFF08${outcome.skipped.map((x2) => x2.companyId ?? "<default>").join("\u3001")}\uFF09` : "")
260637
+ );
260638
+ });
260093
260639
  } else if (msg.type === "session_started") {
260094
260640
  void ledgerForDispatch(msg.dispatchId).then((l) => l.markStarted(msg.dispatchId, { sessionRef: msg.sessionId, daemonId })).catch(ledgerWarn("markStarted"));
260095
260641
  const prodMapping = dispatchToWork.get(msg.dispatchId);
@@ -260135,16 +260681,16 @@ async function startServe(opts) {
260135
260681
  })).catch(ledgerWarn("close(exit)"));
260136
260682
  const dispatchOwnedAtReceipt = allDispatchers().some((d) => d.inFlightSessions().some((x2) => x2.sessionId === msg.dispatchId));
260137
260683
  enqueueChatRecovery("exit \u5BF9\u8D26", async () => {
260138
- const chatHandled = await reconcileChatExitFrame({
260684
+ const chatHandled = await reconcileChatExitFrameAcrossCompanies({
260139
260685
  dispatchId: msg.dispatchId,
260140
260686
  info: msg.info,
260141
260687
  isOwned: (id) => [...chatLiveSessions.values()].some((c) => c.dispatchId === id),
260142
260688
  ownedRunId: (sid) => liveChat.runFor(sid),
260143
- listRunningAssistantMessages: listRunningChatRows,
260144
260689
  trace: runRoutedTrace,
260145
- chatStore: chatRecoveryStore,
260690
+ listCompanies: chatRecoveryCompanies,
260691
+ storesFor: chatRecoveryFor,
260692
+ onCompanyError: chatRecoveryCompanyWarn("exit \u5BF9\u8D26"),
260146
260693
  drainStash: (id) => chatRemoteAdapter?.drainStash(id) ?? { frames: [] },
260147
- ...chatRecoveryItems ? { items: chatRecoveryItems } : {},
260148
260694
  log: (m2) => console.log(m2)
260149
260695
  });
260150
260696
  if (!chatHandled) {
@@ -260208,21 +260754,22 @@ async function startServe(opts) {
260208
260754
  }
260209
260755
  });
260210
260756
  const chatSweepTimer = setInterval(() => {
260211
- void sweepDanglingChatTurns({
260212
- listRunningAssistantMessages: listRunningChatRows,
260757
+ void sweepDanglingChatTurnsAcrossCompanies({
260758
+ listCompanies: chatRecoveryCompanies,
260759
+ storesFor: chatRecoveryFor,
260760
+ onCompanyError: chatRecoveryCompanyWarn("chat-sweep"),
260213
260761
  getRun: (id) => fanOutTrace.getRun(id),
260214
260762
  ownedRunId: (sid) => liveChat.runFor(sid),
260215
- listMessages: (sid) => chatRecoveryStore.listMessages(sid),
260216
- updateMessage: chatRecoveryStore.updateMessage.bind(chatRecoveryStore),
260217
260763
  now: Date.now(),
260218
260764
  // 提案 §6.2.9:委派台账**接进这一拍,不新写一套扫描**。
260219
260765
  // 这一拍闭合的是「run 已终态、assistant 行却还挂 running」的行;委派台账是同一类账,
260220
260766
  // 只是多一层——它还得回流。不接的话那条委派会永远显示「进行中」(刷新也不变),
260221
260767
  // 且「队列非空 ⇒ 不算完成」变成死锁:既看不到结果、也等不到失败。
260222
- // ⚠️ **必须带公司**:这一拍是进程级维护路径(跨公司扫本 schema 所有 running 行),
260223
- // 而委派台账按公司分 store。不带的话非默认公司那条委派会被拿去默认公司的库里查、
260224
- // 查不到、于是永远收敛不掉——症状是「刷新也不变的进行中」,且只在非默认公司出现。
260225
- // 反查那一步收在 `delegationSweepHook` 里(有它自己的测试),这里不再手写。
260768
+ // ⚠️ **必须带公司**:委派台账按公司分 store,不带的话非默认公司那条委派会被拿去默认公司的
260769
+ // 库里查、查不到、于是永远收敛不掉——症状是「刷新也不变的进行中」,且只在非默认公司出现。
260770
+ // 扇出之后**公司是已知量**(这一行就是从那家的 schema 读出来的),由扇出层作为第四个参数
260771
+ // 直接传给钩子,不再按会话反查一次——少一次点查,也少一条「读不到就按默认公司试一次」的
260772
+ // 错误回落。回落那一跳仍收在 `delegationSweepHook` 里(dev 文件模式还需要它,有自己的测试)。
260226
260773
  onSessionClosed: delegationSweepHook({
260227
260774
  sessions: chatRecoveryStore,
260228
260775
  settle: (sid, reason, companyId) => server.settleDelegationFromSweep(sid, reason, companyId),
@@ -260251,6 +260798,20 @@ async function startServe(opts) {
260251
260798
  isOwned: (turn) => Boolean(turn.assistantRunId && liveChat.runFor(turn.chatSessionId) === turn.assistantRunId),
260252
260799
  now: Date.now(),
260253
260800
  noRunGraceMs: chatTurnOrphanGraceMs,
260801
+ /* 轮次被判死之后把委派台账也收敛掉(2026-09-09 生产事故的第二道口子)。
260802
+ 此前只有 `sweepDanglingChatTurns` 那一拍挂了委派钩子,而它要先拿 assistant 行的 `run_id`
260803
+ 去查 run —— 子会话占位行的 `run_id` 一直是空的,那一拍整条跳过、钩子一次没调用:
260804
+ 三条委派的轮次 12:27–12:35 就 failed 了,台账到 13:10 还是 running,父会话永远"处理中"。
260805
+ 本拍是从轮次账本走的(`assistant_run_id` 一直有值),是"这一轮死没死"的权威来源。
260806
+ `companyId` 由扇出那层填好 —— 台账按公司分 store,少了它会查到默认公司的库。
260807
+ 非委派会话调进去是空转:`settleFromSweep` 按 childSessionId 查不到记录就原样返回。 */
260808
+ onTurnSettled: async ({ chatSessionId, status, reason, companyId }) => {
260809
+ await server.settleDelegationFromSweep(
260810
+ chatSessionId,
260811
+ `\u5B50\u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u88AB\u6062\u590D\u5668\u6536\u53E3\u4E3A ${status}\uFF08${reason}\uFF09`,
260812
+ companyId
260813
+ );
260814
+ },
260254
260815
  log: (m2) => console.log(m2),
260255
260816
  onCompanyError: (companyId, err) => {
260256
260817
  const key = companyId ?? "<default>";
@@ -260724,14 +261285,22 @@ async function startServe(opts) {
260724
261285
  //(activate.ts:`latestAcceptId == null && !lastProposedRejected`),本模块拿不到,
260725
261286
  // 而两个方向的代价不对称——该续没续只是少一点上下文,不该续却续了就把返工要隔离的
260726
261287
  // 旧会话又接了回来。何况这条 work 从来没跑过,本就没有属于它的会话可续。
260727
- redispatch: (workId, workorderId) => engineIO.dispatchWork(workId, { workorderId, resumeEligible: false }),
261288
+ // 补派进入同一工单事件队列:与 pause apply/effect 串行,不能在检查后绕到 IO 直派。
261289
+ redispatch: async (workId, workorderId) => {
261290
+ await newKernel.getBus().submit({
261291
+ companyId,
261292
+ workorderId,
261293
+ actorId: SYSTEM_ACTOR2,
261294
+ event: { kind: "work.redispatch", workorderId, workId }
261295
+ });
261296
+ },
260728
261297
  now: Date.now(),
260729
261298
  graceMs: DISPATCH_DROP_GRACE_MS,
260730
261299
  backoffMs: DISPATCH_DROP_BACKOFF_MS,
260731
261300
  maxPerTick: DISPATCH_DROP_MAX_PER_TICK
260732
261301
  }).then((r) => {
260733
261302
  if (r.redispatched.length > 0) {
260734
- console.warn(`[dispatch-drop] \u8865\u6D3E ${r.redispatched.length} \u6761\u4ECE\u672A\u5F00\u5DE5\u7684 work\uFF1A${r.redispatched.join(", ")}`);
261303
+ console.warn(`[dispatch-drop] \u5DF2\u8BF7\u6C42\u8865\u6D3E ${r.redispatched.length} \u6761\u4ECE\u672A\u5F00\u5DE5\u7684 work\uFF1A${r.redispatched.join(", ")}`);
260735
261304
  }
260736
261305
  }).catch((err) => console.error(`[dispatch-drop] tick \u5931\u8D25: ${String(err)}`));
260737
261306
  }, DISPATCH_DROP_SWEEP_TICK_MS);
@@ -262832,17 +263401,19 @@ var SessionProcessState = class {
262832
263401
  }
262833
263402
  let done = false;
262834
263403
  const finish = () => {
262835
- if (!done) {
262836
- done = true;
262837
- resolve10();
262838
- }
263404
+ if (done) return;
263405
+ done = true;
263406
+ clearTimeout(timer);
263407
+ this.exitAckResolve = null;
263408
+ resolve10();
262839
263409
  };
263410
+ const timer = setTimeout(finish, EXIT_ACK_WAIT_MS);
262840
263411
  this.exitAckResolve = finish;
262841
- setTimeout(finish, EXIT_ACK_WAIT_MS).unref?.();
262842
263412
  });
262843
263413
  }
262844
263414
  teardown() {
262845
263415
  this.stopping = true;
263416
+ this.exitAckResolve?.();
262846
263417
  if (this.watchdog) {
262847
263418
  clearInterval(this.watchdog);
262848
263419
  this.watchdog = null;
@@ -269579,7 +270150,7 @@ function shimScript() {
269579
270150
  }
269580
270151
 
269581
270152
  // src/index.ts
269582
- var PKG_VERSION = true ? "2.2.6" : "dev";
270153
+ var PKG_VERSION = true ? "2.2.7" : "dev";
269583
270154
  var LOCAL_BIN = localBin();
269584
270155
  var NPM_PREFIX = npmPrefix();
269585
270156
  var INSTANCE = DEFAULT_INSTANCE;