oasis_test_v2 2.2.6 → 2.2.8

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 +1228 -256
  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));
@@ -17335,6 +17406,7 @@ ${supplemental.taskAppend.trim()}
17335
17406
  ...Object.keys(jobEnv).length > 0 ? { env: jobEnv } : {},
17336
17407
  ...provisioned?.wrapperPaths && provisioned.wrapperPaths.length > 0 ? { wrapperPaths: provisioned.wrapperPaths } : {},
17337
17408
  ...provisioned?.requiredTools && provisioned.requiredTools.length > 0 ? { requiredTools: provisioned.requiredTools } : {},
17409
+ ...provisioned?.requiredToolVersions && Object.keys(provisioned.requiredToolVersions).length > 0 ? { requiredToolVersions: provisioned.requiredToolVersions } : {},
17338
17410
  // 凭据本体随 job 下发,供**节点侧本机注入**(见上方 provision 注释)。
17339
17411
  ...provisioned?.connectorCreds && provisioned.connectorCreds.length > 0 ? { connectorCreds: provisioned.connectorCreds } : {},
17340
17412
  ...executionEnv !== void 0 ? { executionEnv } : {},
@@ -19708,6 +19780,39 @@ var init_kernel_bridge = __esm({
19708
19780
  throw new KernelError("\u5185\u5BB9\u5DF2\u88AB\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u6587\u4EF6\u540E\u518D\u4FDD\u5B58", "content-conflict");
19709
19781
  }
19710
19782
  }
19783
+ /** 暂停时保存人工完整稿:不借 work.create 开运行,不改暂停位,验收/下游调度留待恢复。 */
19784
+ async recordPausedRevision(args) {
19785
+ if (!args.actor.startsWith("actor:human:")) {
19786
+ throw new KernelError("\u6682\u505C\u671F\u95F4\u5F55\u5165\u5B8C\u6574\u7A3F\u4EC5\u9650\u4EBA\u7C7B\u64CD\u4F5C", "forbidden");
19787
+ }
19788
+ await this.refreshTracked();
19789
+ const wid = this.wo(args.artifactId);
19790
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
19791
+ const node2 = snap?.nodes.find((n) => n.id === args.artifactId);
19792
+ if (!snap?.workorder.dispatchPaused || !node2 || node2.cancelledAt) {
19793
+ throw new KernelError("\u5DE5\u5355\u6216\u8282\u70B9\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u4FDD\u5B58", "revision-conflict");
19794
+ }
19795
+ const workId = deriveWorkId(node2.id, snap.works.filter((w2) => w2.nodeId === node2.id).length + 1);
19796
+ await this.commit({
19797
+ companyId: "",
19798
+ workorderId: wid,
19799
+ actorId: args.actor,
19800
+ event: {
19801
+ kind: "work.snapshot_recorded",
19802
+ workorderId: wid,
19803
+ nodeId: node2.id,
19804
+ workId,
19805
+ expectedLatestWorkId: node2.latestWorkId,
19806
+ contentRef: args.contentRef,
19807
+ conclusion: args.note
19808
+ }
19809
+ });
19810
+ const after = await this.store.transaction((tx) => tx.loadWorkorder(wid));
19811
+ if (!after?.artifacts.some((a) => a.workId === workId && a.contentRef === args.contentRef)) {
19812
+ 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");
19813
+ }
19814
+ return this.model.revisions.get(workId);
19815
+ }
19711
19816
  async proposeRevision(args) {
19712
19817
  await this.refreshTracked();
19713
19818
  const wid = this.wo(args.artifactId);
@@ -29417,11 +29522,22 @@ async function sweepStaleChatTurns(deps) {
29417
29522
  deps.log?.(
29418
29523
  `[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
29524
  );
29525
+ const settledStatus = outcome.settled ? outcome.status : void 0;
29526
+ if (deps.onTurnSettled && settledStatus && settledStatus !== "succeeded") {
29527
+ await deps.onTurnSettled({
29528
+ chatSessionId: turn.chatSessionId,
29529
+ turnId: turn.id,
29530
+ status: settledStatus,
29531
+ reason: outcome.reason ?? `\u8F6E\u6B21\u6536\u53E3\u4E3A ${settledStatus}`
29532
+ }).catch((e) => {
29533
+ 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)}`);
29534
+ });
29535
+ }
29420
29536
  }
29421
29537
  return settled;
29422
29538
  }
29423
29539
  async function sweepStaleChatTurnsAcrossCompanies(deps) {
29424
- const { listCompanies, storesFor, onCompanyError, log: log3, ...rest } = deps;
29540
+ const { listCompanies, storesFor, onCompanyError, log: log3, onTurnSettled, ...rest } = deps;
29425
29541
  const settled = [];
29426
29542
  for (const companyId of await listCompanies()) {
29427
29543
  try {
@@ -29431,7 +29547,9 @@ async function sweepStaleChatTurnsAcrossCompanies(deps) {
29431
29547
  turns: stores.turns,
29432
29548
  ...stores.assistantRowStatus ? { assistantRowStatus: stores.assistantRowStatus } : {},
29433
29549
  ...stores.settleAssistantRow ? { settleAssistantRow: stores.settleAssistantRow } : {},
29434
- ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {}
29550
+ ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {},
29551
+ // 把这一家的 companyId 补进去再往外报——下游(委派台账)按公司分 store,少了它就查错库。
29552
+ ...onTurnSettled ? { onTurnSettled: (info) => onTurnSettled({ ...info, ...companyId ? { companyId } : {} }) } : {}
29435
29553
  });
29436
29554
  settled.push(...outcomes);
29437
29555
  } catch (err) {
@@ -159619,7 +159737,7 @@ function normalizedOpToV3Op(op, kind) {
159619
159737
  if (op === "set_text") return "set_text";
159620
159738
  if (op === "patch_input") return "patch_input";
159621
159739
  if (op === "set_status") {
159622
- return kind === "tool_call" || kind === "tool_result" ? "completed" : void 0;
159740
+ return "completed";
159623
159741
  }
159624
159742
  return void 0;
159625
159743
  }
@@ -159803,7 +159921,7 @@ var init_live_chat = __esm({
159803
159921
  if (!turn.items) return;
159804
159922
  const itemId = outcome.itemId;
159805
159923
  const itemType = normalizedKindToItemType(event.kind);
159806
- const operation = normalizedOpToV3Op(event.opShape.op, event.kind);
159924
+ let operation = normalizedOpToV3Op(event.opShape.op, event.kind);
159807
159925
  if (!operation) return;
159808
159926
  const perItem = turn.v3Items.get(itemId) ?? { textOffset: 0, started: false };
159809
159927
  const isTextual2 = event.kind === "text" || event.kind === "thinking";
@@ -159839,6 +159957,7 @@ var init_live_chat = __esm({
159839
159957
  payload = { input: opShape.input };
159840
159958
  } else if (opShape.op === "set_status") {
159841
159959
  if (opShape.status === "streaming") return;
159960
+ operation = opShape.status === "completed" ? "completed" : "failed";
159842
159961
  }
159843
159962
  turn.v3Items.set(itemId, perItem);
159844
159963
  const frame = {
@@ -159969,6 +160088,24 @@ var init_live_chat = __esm({
159969
160088
  this.publishV3(turn, this.assignV3Seq(turn, frame));
159970
160089
  return true;
159971
160090
  }
160091
+ /** 恢复路在 DB 收口旧 item 后,将同一身份/版本的终态发给已挂上的观众。 */
160092
+ publishItemStatus(chatSessionId, item) {
160093
+ const turn = this.turns.get(chatSessionId);
160094
+ if (!turn || turn.status !== "running" || item.sessionId !== chatSessionId || item.status === "streaming") return false;
160095
+ if (turn.items?.turnId !== item.turnId) return false;
160096
+ this.flushPendingV3(turn);
160097
+ this.publishV3(turn, this.assignV3Seq(turn, {
160098
+ protocolVersion: 3,
160099
+ streamId: turn.liveStreamId,
160100
+ turnId: item.turnId,
160101
+ itemId: item.id,
160102
+ itemType: normalizedKindToItemType(item.kind),
160103
+ operation: item.status === "completed" ? "completed" : "failed",
160104
+ itemVersion: item.version,
160105
+ ...item.ord !== null ? { ord: item.ord } : {}
160106
+ }));
160107
+ return true;
160108
+ }
159972
160109
  emitV3TurnTerminal(turn, status) {
159973
160110
  if (status !== "done" && status !== "error") return;
159974
160111
  const operation = status === "done" ? "turn_completed" : "turn_failed";
@@ -195686,6 +195823,7 @@ var init_service3 = __esm({
195686
195823
  const store = await this.options.resolveStore(companyId).catch(() => null);
195687
195824
  await store?.patch(record8.id, { childRunId: anchor }).catch(() => null);
195688
195825
  }
195826
+ await this.bindChildRunToPlaceholder(record8, companyId, claim.turn.assistantRunId ?? void 0);
195689
195827
  await notDispatched("resumed-existing-run");
195690
195828
  return;
195691
195829
  }
@@ -195700,7 +195838,15 @@ var init_service3 = __esm({
195700
195838
  ...companyId ? { companyId } : {},
195701
195839
  ...snapshot ? { parentConversationSnapshot: snapshot } : {},
195702
195840
  extraSystemPrompt: EXPERT_DELEGATION_SYSTEM_PROMPT,
195703
- ..._DelegationService.inboundBundle(inbound) ? { workspaceBundle: _DelegationService.inboundBundle(inbound) } : {}
195841
+ ..._DelegationService.inboundBundle(inbound) ? { workspaceBundle: _DelegationService.inboundBundle(inbound) } : {},
195842
+ /* 派发这一侧要拿它们建 `chat_items` 账本(见本文件 `DelegationDispatchRequest` 的注释)。
195843
+ 轮次口径与 `bindChildRunToPlaceholder` / `seedChildRoundRows` **逐字一致**:
195844
+ `record.roundSummaries.length + 1`。三处一旦不同源,item 就会挂到上一轮那条回答行上。 */
195845
+ assistantMessageId: childRoundMessageIds(
195846
+ record8.childSessionId,
195847
+ record8.roundSummaries.length + 1
195848
+ ).placeholderId,
195849
+ chatTurnId: turnId
195704
195850
  });
195705
195851
  outcomeSettled = true;
195706
195852
  await opts?.onDispatched?.().catch((err) => console.warn(`[delegation] \u5F85\u8F6C\u8FBE\u6807\u8BB0\u5DF2\u6D88\u8D39\u5931\u8D25\uFF08${record8.id}\uFF09: ${String(err)}`));
@@ -195719,6 +195865,7 @@ var init_service3 = __esm({
195719
195865
  const store = await this.options.resolveStore(companyId);
195720
195866
  await store.patch(record8.id, { childRunId: anchor }).catch(() => null);
195721
195867
  }
195868
+ await this.bindChildRunToPlaceholder(record8, companyId, dispatched.runId);
195722
195869
  this.track(this.settleRound(record8, dispatched, companyId, () => output, turnId, turns));
195723
195870
  } catch (error2) {
195724
195871
  const failedAt = this.now();
@@ -195835,6 +195982,29 @@ var init_service3 = __esm({
195835
195982
  * 委派台账是同一类账,只是多一层——它还得回流。所以这里只做两件:把行拍成 `failed/interrupted`、
195836
195983
  * 然后走同一个 {@link notifySettled}。已经是终态的行原样返回(幂等,重复扫不重复回流)。
195837
195984
  */
195985
+ /**
195986
+ * 把这一轮的 run 锚点写到子会话那条**回答占位行**上。
195987
+ *
195988
+ * 为什么非它不可:serve 重启后,daemon 的 hello 对账与会话进程自报身份**共用**
195989
+ * `planChatTurnRecovery`,而那个函数第一行就是 `if (!row.runId) continue` —— 占位行没有 runId,
195990
+ * 这条子会话就永远配不出恢复计划,答 `unknown`,然后被 hub evict(杀掉)。
195991
+ * 轮次是第几轮按 `roundSummaries.length + 1` 算,与 `seedChildRoundRows` 那两处口径逐字一致。
195992
+ *
195993
+ * 失败只记日志:这是加固,不是这一轮派发成立的前提。
195994
+ */
195995
+ async bindChildRunToPlaceholder(record8, companyId, runId) {
195996
+ if (!runId) return;
195997
+ const round = record8.roundSummaries.length + 1;
195998
+ const { placeholderId } = childRoundMessageIds(record8.childSessionId, round);
195999
+ try {
196000
+ const chatStore = await this.options.resolveChatSessions(companyId);
196001
+ await chatStore.updateMessage(placeholderId, { runId });
196002
+ } catch (err) {
196003
+ console.warn(
196004
+ `[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`
196005
+ );
196006
+ }
196007
+ }
195838
196008
  async settleFromSweep(childSessionId, reason, companyId) {
195839
196009
  const store = await this.options.resolveStore(companyId).catch(() => null);
195840
196010
  if (!store) return null;
@@ -196940,7 +197110,11 @@ function toChatDispatchRequest(request2) {
196940
197110
  delegatedChildTurn: true,
196941
197111
  // §6.1.3 去程:`--file` 带来的字节就在这一格。**丢了它命令照样回 202**,
196942
197112
  // 而专家的工作区里什么都没有——这正是上一版真实发生过的形态。
196943
- ...request2.workspaceBundle ? { workspaceBundle: request2.workspaceBundle } : {}
197113
+ ...request2.workspaceBundle ? { workspaceBundle: request2.workspaceBundle } : {},
197114
+ // 这两格漏了同样**不报错**:子会话照跑、正文照回,只是 `chat_items` 的行没有 message_id、
197115
+ // turnId 回落成 `chat-run-turn:<runId>`,右栏面板的增量流对不上轮——正是本文件存在的理由。
197116
+ ...request2.assistantMessageId ? { assistantMessageId: request2.assistantMessageId } : {},
197117
+ ...request2.chatTurnId ? { chatTurnId: request2.chatTurnId } : {}
196944
197118
  };
196945
197119
  }
196946
197120
  function applyWorkspaceBundle(target, bundle) {
@@ -196957,12 +197131,15 @@ var init_dispatch_adapter = __esm({
196957
197131
  // ../server/src/domains/delegations/sweep-hook.ts
196958
197132
  function delegationSweepHook(deps) {
196959
197133
  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;
197134
+ return async (chatSessionId, reason, _messageId, knownCompanyId) => {
197135
+ let companyId = knownCompanyId;
197136
+ if (companyId === void 0) {
197137
+ const session = await deps.sessions.getSession(chatSessionId).catch((err) => {
197138
+ 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`);
197139
+ return null;
197140
+ });
197141
+ companyId = session?.companyId ?? void 0;
197142
+ }
196966
197143
  await deps.settle(chatSessionId, reason, companyId).catch((err) => {
196967
197144
  log3(`[chat-sweep] \u59D4\u6D3E\u53F0\u8D26\u6536\u655B\u5931\u8D25\uFF08\u4F1A\u8BDD ${chatSessionId} \u516C\u53F8 ${companyId ?? "\u9ED8\u8BA4"}\uFF09: ${String(err)}`);
196968
197145
  });
@@ -197157,27 +197334,77 @@ var init_assembly = __esm({
197157
197334
  });
197158
197335
 
197159
197336
  // ../server/src/domains/delegations/child-live-turn.ts
197160
- function registerDelegatedChildTurn(liveChat, childSessionId, session) {
197337
+ function registerDelegatedChildTurn(liveChat, childSessionId, session, deps = {}) {
197338
+ const log3 = deps.log ?? ((m2) => console.warn(m2));
197161
197339
  const appendInput = session.appendInput;
197162
- if (typeof appendInput !== "function" || session.canAppendInput === false) return;
197340
+ const canAppend = typeof appendInput === "function" && session.canAppendInput !== false;
197341
+ const ledger = new ChatItemLedger({
197342
+ ...deps.items ? { items: deps.items } : {},
197343
+ sessionId: childSessionId,
197344
+ turnId: deps.turnId ?? fallbackTurnId(session.runId),
197345
+ ...session.runId ? { runId: session.runId } : {},
197346
+ ...deps.assistantMessageId ? { messageId: deps.assistantMessageId } : {},
197347
+ ...deps.versionSeed !== void 0 ? { versionSeed: deps.versionSeed } : {},
197348
+ log: log3
197349
+ });
197163
197350
  const ctrl = liveChat.start(childSessionId, {
197164
197351
  runtimeSessionId: session.id,
197165
197352
  ...session.runId ? { runId: session.runId } : {},
197166
197353
  kill: () => {
197167
197354
  void session.kill?.();
197168
197355
  },
197169
- appendInput: (input) => appendInput.call(session, input),
197356
+ // 纪律 ①:插不进去的 runtime 也登记,只是这两格照实报。
197357
+ ...canAppend && appendInput ? { appendInput: (input) => appendInput.call(session, input) } : {},
197170
197358
  // **每次现读**,不快照:一轮跑到收尾时 stdin 会先关,那之后 runtime 自己会回
197171
197359
  // `session-closing`,判断权本就该留在它那儿(同 `/api/chat` 那条路的写法)。
197172
197360
  get canAppendInput() {
197173
- return session.canAppendInput !== false;
197361
+ return typeof session.appendInput === "function" && session.canAppendInput !== false;
197362
+ }
197363
+ }, { items: ledger });
197364
+ if (deps.assistantMessageId) ctrl.setAssistantMessageId(deps.assistantMessageId);
197365
+ const hasChannels = typeof session.onOutput === "function" || typeof session.onTelemetry === "function" || typeof session.onLiveEvent === "function";
197366
+ const normalized4 = typeof session.onNormalizedProviderEvent === "function" ? {
197367
+ onNormalizedProviderEvent: session.onNormalizedProviderEvent.bind(session),
197368
+ finishTurn: (signal) => session.finishNormalizedTurn?.(signal)
197369
+ } : hasChannels ? attachStreamNormalizer(
197370
+ {
197371
+ id: session.id,
197372
+ onOutput: (cb) => session.onOutput?.(cb),
197373
+ onTelemetry: (cb) => session.onTelemetry?.(cb),
197374
+ onLiveEvent: (cb) => session.onLiveEvent?.(cb),
197375
+ ...session.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: session.supportsLiveProtocol } : {}
197376
+ },
197377
+ {
197378
+ providerName: deps.runtimeKind ?? "runtime",
197379
+ fallbackTurnId: `oasis-turn:${session.runId ?? session.id}`
197380
+ }
197381
+ ) : null;
197382
+ normalized4?.onNormalizedProviderEvent((event) => {
197383
+ try {
197384
+ ctrl.applyNormalizedEvent(event);
197385
+ } catch {
197174
197386
  }
197175
197387
  });
197176
- void session.done.then(() => ctrl.finish("done"), () => ctrl.finish("error"));
197388
+ const settle = async (signal) => {
197389
+ try {
197390
+ normalized4?.finishTurn(signal);
197391
+ } catch {
197392
+ }
197393
+ try {
197394
+ ledger.finish(signal);
197395
+ await ledger.drain();
197396
+ } catch (err) {
197397
+ log3(`[delegation] \u5B50\u4F1A\u8BDD ${childSessionId} \u8D26\u672C\u6536\u5C3E\u5931\u8D25: ${String(err)}`);
197398
+ }
197399
+ ctrl.finish(signal === "completed" ? "done" : "error");
197400
+ };
197401
+ void session.done.then(() => settle("completed"), () => settle("failed"));
197177
197402
  }
197178
197403
  var init_child_live_turn = __esm({
197179
197404
  "../server/src/domains/delegations/child-live-turn.ts"() {
197180
197405
  "use strict";
197406
+ init_src6();
197407
+ init_chat_item_ledger();
197181
197408
  }
197182
197409
  });
197183
197410
 
@@ -197291,10 +197518,12 @@ var init_tool_home = __esm({
197291
197518
  });
197292
197519
 
197293
197520
  // ../connectors/src/_base/install.ts
197294
- function installPlan(cmd, platform2, toolsPrefix) {
197295
- if (cmd === "lark-cli") {
197521
+ function installPlan(cmd, platform2, toolsPrefix, version2) {
197522
+ const npmPkg = NPM_TOOL_PACKAGES[cmd];
197523
+ if (npmPkg) {
197296
197524
  const npm = platform2 === "win32" ? "npm.cmd" : "npm";
197297
- return [{ file: npm, args: ["install", "-g", "--prefix", toolsPrefix, "@larksuite/cli"], ensureDir: toolsPrefix }];
197525
+ const spec = version2 ? `${npmPkg}@${version2}` : npmPkg;
197526
+ return [{ file: npm, args: ["install", "-g", "--prefix", toolsPrefix, spec], ensureDir: toolsPrefix }];
197298
197527
  }
197299
197528
  const pkg = cmd === "gh" ? { brew: "gh", apt: "gh", dnf: "gh", pacman: "github-cli", apk: "github-cli", winget: "GitHub.cli", choco: "gh", scoop: "gh" } : { brew: "git", apt: "git", dnf: "git", pacman: "git", apk: "git", winget: "Git.Git", choco: "git", scoop: "git" };
197300
197529
  if (platform2 === "darwin") {
@@ -197339,17 +197568,54 @@ function defaultDeps() {
197339
197568
  mkdirp: (dir) => {
197340
197569
  (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
197341
197570
  },
197342
- log: (msg) => console.warn(msg)
197571
+ log: (msg) => console.warn(msg),
197572
+ installedVersion: async (pkg, prefix) => readInstalledVersion(pkg, prefix)
197343
197573
  };
197344
197574
  }
197345
- async function ensureToolInstalled(cmd, deps = {}) {
197575
+ function readInstalledVersion(pkg, prefix) {
197576
+ const parts = pkg.split("/");
197577
+ for (const mid of [["lib", "node_modules"], ["node_modules"]]) {
197578
+ const file = (0, import_node_path11.join)(prefix, ...mid, ...parts, "package.json");
197579
+ if (!(0, import_node_fs9.existsSync)(file)) continue;
197580
+ try {
197581
+ const v2 = JSON.parse((0, import_node_fs9.readFileSync)(file, "utf8")).version;
197582
+ if (typeof v2 === "string" && v2) return v2;
197583
+ } catch {
197584
+ }
197585
+ }
197586
+ return null;
197587
+ }
197588
+ async function alignToWantedVersion(cmd, wanted, d) {
197589
+ if (alignedVersion.get(cmd) === wanted) return;
197590
+ const pkg = NPM_TOOL_PACKAGES[cmd];
197591
+ if (!pkg) return;
197592
+ const installed = await d.installedVersion(pkg, d.toolsPrefix);
197593
+ if (!installed) return;
197594
+ if (installed === wanted) {
197595
+ alignedVersion.set(cmd, wanted);
197596
+ return;
197597
+ }
197598
+ const npm = d.platform === "win32" ? "npm.cmd" : "npm";
197599
+ d.log(`[connector-install] ${cmd} ${installed} \u2192 ${wanted}\uFF08\u670D\u52A1\u7AEF\u767B\u8BB0\u7248\u672C\uFF09\uFF0C\u5B89\u88C5\u4E2D\u2026`);
197600
+ try {
197601
+ d.mkdirp(d.toolsPrefix);
197602
+ await d.run(npm, ["install", "-g", "--prefix", d.toolsPrefix, `${pkg}@${wanted}`]);
197603
+ alignedVersion.set(cmd, wanted);
197604
+ d.log(`[connector-install] ${cmd} \u5DF2\u5BF9\u9F50\u5230 ${wanted}`);
197605
+ } catch (err) {
197606
+ d.log(`[connector-install] ${cmd} \u5BF9\u9F50\u5230 ${wanted} \u5931\u8D25\uFF0C\u7EE7\u7EED\u7528 ${installed}\uFF1A${String(err)}`);
197607
+ }
197608
+ }
197609
+ async function ensureToolInstalled(cmd, opts = {}) {
197610
+ const { versions, ...deps } = opts;
197346
197611
  const d = { ...defaultDeps(), ...deps };
197347
- if (confirmed.has(cmd)) return true;
197348
- if (await d.isPresent(cmd)) {
197612
+ const wanted = versions?.[cmd];
197613
+ if (confirmed.has(cmd) || await d.isPresent(cmd)) {
197349
197614
  confirmed.add(cmd);
197615
+ if (wanted) await alignToWantedVersion(cmd, wanted, d);
197350
197616
  return true;
197351
197617
  }
197352
- const steps = installPlan(cmd, d.platform, d.toolsPrefix);
197618
+ const steps = installPlan(cmd, d.platform, d.toolsPrefix, wanted);
197353
197619
  if (steps.length === 0) {
197354
197620
  d.log(`[connector-install] ${cmd} \u7F3A\u5931\uFF0C\u4E14 ${d.platform} \u4E0B\u65E0\u5DF2\u77E5\u81EA\u52A8\u5B89\u88C5\u6CD5\u2014\u2014\u8BF7\u624B\u52A8\u5B89\u88C5`);
197355
197621
  return false;
@@ -197362,6 +197628,7 @@ async function ensureToolInstalled(cmd, deps = {}) {
197362
197628
  await d.run(step.file, step.args);
197363
197629
  if (await d.isPresent(cmd)) {
197364
197630
  confirmed.add(cmd);
197631
+ if (wanted) alignedVersion.set(cmd, wanted);
197365
197632
  d.log(`[connector-install] ${cmd} \u5B89\u88C5\u6210\u529F\uFF08${step.file} ${step.args.join(" ")}\uFF09`);
197366
197633
  return true;
197367
197634
  }
@@ -197372,15 +197639,15 @@ async function ensureToolInstalled(cmd, deps = {}) {
197372
197639
  d.log(`[connector-install] ${cmd} \u81EA\u52A8\u5B89\u88C5\u672A\u6210\u529F\u2014\u2014\u5C06\u56DE\u9000\u5230 wrapper/\u547D\u4EE4\u81EA\u8EAB\u62A5\u9519`);
197373
197640
  return false;
197374
197641
  }
197375
- async function ensureConnectorTools(tools, deps = {}) {
197642
+ async function ensureConnectorTools(tools, opts = {}) {
197376
197643
  if (!tools || tools.length === 0) return [];
197377
197644
  const missing = [];
197378
197645
  for (const cmd of tools) {
197379
- if (!await ensureToolInstalled(cmd, deps)) missing.push(cmd);
197646
+ if (!await ensureToolInstalled(cmd, opts)) missing.push(cmd);
197380
197647
  }
197381
197648
  return missing;
197382
197649
  }
197383
- var import_node_child_process12, import_node_util2, import_node_fs9, import_node_path11, execFileAsync, confirmed;
197650
+ var import_node_child_process12, import_node_util2, import_node_fs9, import_node_path11, execFileAsync, NPM_TOOL_PACKAGES, confirmed, alignedVersion;
197384
197651
  var init_install = __esm({
197385
197652
  "../connectors/src/_base/install.ts"() {
197386
197653
  "use strict";
@@ -197390,7 +197657,9 @@ var init_install = __esm({
197390
197657
  import_node_path11 = require("node:path");
197391
197658
  init_tool_home();
197392
197659
  execFileAsync = (0, import_node_util2.promisify)(import_node_child_process12.execFile);
197660
+ NPM_TOOL_PACKAGES = { "lark-cli": "@larksuite/cli" };
197393
197661
  confirmed = /* @__PURE__ */ new Set();
197662
+ alignedVersion = /* @__PURE__ */ new Map();
197394
197663
  }
197395
197664
  });
197396
197665
 
@@ -201461,7 +201730,7 @@ var init_connector_adapter = __esm({
201461
201730
  async spawn(job) {
201462
201731
  const t0 = Date.now();
201463
201732
  if (job.requiredTools?.length) {
201464
- const missing = await ensureConnectorTools(job.requiredTools);
201733
+ const missing = await ensureConnectorTools(job.requiredTools, { versions: job.requiredToolVersions });
201465
201734
  if (missing.length) log("[adapter]", ` connector tools still missing (agent may fail): ${missing.join(", ")}`);
201466
201735
  }
201467
201736
  const prepared = await prepareConnectorsForJob(job);
@@ -203125,13 +203394,22 @@ async function startOasisServer(opts) {
203125
203394
  resolveStore: opts.delegationStoreFor,
203126
203395
  resolveChatSessions: resolveChatSessionsForDelegation,
203127
203396
  resolveActors: async (companyId) => (await actorsDomain2.resolveCtx(companyId)).service,
203128
- // **派发完顺手把子会话这一轮登记进 live 注册表**(v2 bug 0088)。缺这一步时下面那行
203129
- // `appendToChild` 恒回 `no-live-turn`——注册表里从来没有以 `childSessionId` 为键的轮,
203130
- // 于是每一条 `--continue` 转达都落队列,专家要等本轮跑完才看见「手上这段作废」。
203131
- // 理由与两条纪律见 `child-live-turn.ts` 的文件头。
203397
+ /* **派发完顺手把子会话这一轮登记进 live 注册表,并给它接上 `chat_items` 账本。**
203398
+ 两件事的成因不同,都在 `child-live-turn.ts` 的文件头:
203399
+ · 不登记 → 下面那行 `appendToChild` 恒回 `no-live-turn`,每一条 `--follow-up` 转达
203400
+ 都落队列,专家要等本轮跑完才看见「手上这段作废」(v2 bug 0088);
203401
+ · 不接账本 → 子会话在 `chat_items` 里只有 storage 层镜像来的一问一答两行纯文字,
203402
+ 右栏面板只能靠详情接口拿 run_id 去轨迹表**现折**,跑的过程中一片空白。 */
203132
203403
  dispatchChat: async (request2) => {
203133
203404
  const session = await dispatchChat(request2);
203134
- registerDelegatedChildTurn(liveChat, request2.chatSessionId, session);
203405
+ const itemStore = opts.resolveChatItems ? await opts.resolveChatItems(request2.companyId).catch(() => void 0) : opts.chatItems;
203406
+ const versionSeed = itemStore ? await itemStore.sessionVersionCursor(request2.chatSessionId).catch(() => 0) : 0;
203407
+ registerDelegatedChildTurn(liveChat, request2.chatSessionId, session, {
203408
+ ...itemStore ? { items: itemStore } : {},
203409
+ ...request2.chatTurnId ? { turnId: request2.chatTurnId } : {},
203410
+ ...request2.assistantMessageId ? { assistantMessageId: request2.assistantMessageId } : {},
203411
+ versionSeed
203412
+ });
203135
203413
  return session;
203136
203414
  },
203137
203415
  appendToChild: (childSessionId, text5, extra) => liveChat.append(childSessionId, text5, extra),
@@ -203906,6 +204184,7 @@ async function startOasisServer(opts) {
203906
204184
  const rawBody = Buffer.concat(chunks).toString("utf8");
203907
204185
  const companyCtx = opts.resolveCompanyContext ? await opts.resolveCompanyContext(actor, req.headers, url.pathname) : void 0;
203908
204186
  const currentCompanyId = companyCtx?.kind === "ok" ? companyCtx.companyId : void 0;
204187
+ const connectorActorsService = async (companyId) => opts.actors ? (await opts.actors.resolveCtx(companyId ?? currentCompanyId)).service : void 0;
203909
204188
  const wodrafts = opts.resolveWorkorderDrafts ? await opts.resolveWorkorderDrafts(currentCompanyId) : opts.workorderDrafts;
203910
204189
  const wodraftPlannerIssues = (opts.resolveWorkorderDraftPlannerIssues && await opts.resolveWorkorderDraftPlannerIssues(currentCompanyId)) ?? opts.workorderDraftPlannerIssues ?? defaultDraftPlannerIssuesStore;
203911
204190
  const engine = await resolveEngine(currentCompanyId);
@@ -205969,12 +206248,12 @@ ${composed}`;
205969
206248
  const actorCtx = await opts.resolveActorContext(body2.actorId).catch(() => null);
205970
206249
  if (actorCtx) {
205971
206250
  const { mkdtempSync: mkdtempSync6, mkdirSync: mkdirSync25, writeFileSync: writeFileSync18 } = await import("node:fs");
205972
- const { join: join39, dirname: dirname36 } = await import("node:path");
206251
+ const { join: join40, dirname: dirname36 } = await import("node:path");
205973
206252
  const { tmpdir: tmpdir12 } = await import("node:os");
205974
- const dir = mkdtempSync6(join39(tmpdir12(), "oasis-chat-"));
206253
+ const dir = mkdtempSync6(join40(tmpdir12(), "oasis-chat-"));
205975
206254
  if (actorCtx.config?.prompt) {
205976
206255
  for (const [rel, content3] of Object.entries(splitIdentityFiles2(actorCtx.config.prompt))) {
205977
- const file = join39(dir, rel);
206256
+ const file = join40(dir, rel);
205978
206257
  mkdirSync25(dirname36(file), { recursive: true });
205979
206258
  writeFileSync18(file, content3);
205980
206259
  }
@@ -205986,12 +206265,12 @@ ${composed}`;
205986
206265
  const s2 = byId.get(id);
205987
206266
  return s2 ? `- **${s2.name}** (\`${s2.id}\`): ${s2.description}` : `- \`${id}\`\uFF08\u672A\u5728\u6280\u80FD\u5E93\u4E2D\uFF0C\u53EF\u80FD\u5DF2\u5378\u8F7D\uFF09`;
205988
206267
  });
205989
- writeFileSync18(join39(dir, "SKILLS.md"), ["# \u53EF\u7528\u6280\u80FD", "", "\u4EE5\u4E0B\u6280\u80FD\u5DF2\u4E3A\u4F60\u542F\u7528\uFF0C\u53EF\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u76F4\u63A5\u4F7F\u7528\uFF1A", "", ...lines].join("\n"));
206268
+ writeFileSync18(join40(dir, "SKILLS.md"), ["# \u53EF\u7528\u6280\u80FD", "", "\u4EE5\u4E0B\u6280\u80FD\u5DF2\u4E3A\u4F60\u542F\u7528\uFF0C\u53EF\u5728\u672C\u6B21\u4F1A\u8BDD\u4E2D\u76F4\u63A5\u4F7F\u7528\uFF1A", "", ...lines].join("\n"));
205990
206269
  }
205991
206270
  if (opts.materializeSkills) {
205992
206271
  const skillFiles = await opts.materializeSkills(body2.actorId, "claude").catch(() => ({}));
205993
206272
  for (const [rel, content3] of Object.entries(skillFiles)) {
205994
- const file = join39(dir, rel);
206273
+ const file = join40(dir, rel);
205995
206274
  mkdirSync25(dirname36(file), { recursive: true });
205996
206275
  writeFileSync18(file, content3);
205997
206276
  }
@@ -206005,7 +206284,7 @@ ${composed}`;
206005
206284
  const modeNote = c.mode === "oauth" ? "OAuth \xB7 \u51ED\u8BC1\u7531\u5E73\u53F0\u7BA1\u7406\uFF0C\u901A\u8FC7\u5BF9\u5E94 CLI wrapper \u8C03\u7528" : "\u76F4\u63A5\u5199\u5165 \xB7 \u51ED\u8BC1\u5DF2\u6CE8\u5165\u73AF\u5883\u53D8\u91CF";
206006
206285
  return `- **${c.name}** (\`${c.id}\`): ${statusNote} \xB7 ${modeNote}`;
206007
206286
  });
206008
- writeFileSync18(join39(dir, "CONNECTORS.md"), ["# \u53EF\u7528\u8FDE\u63A5\u5668", "", "\u4EE5\u4E0B\u8FDE\u63A5\u5668\u5DF2\u4E3A\u672C\u6B21\u4F1A\u8BDD\u914D\u7F6E\uFF0C\u51ED\u8BC1\u5DF2\u901A\u8FC7\u73AF\u5883\u53D8\u91CF\u6216 CLI wrapper \u6CE8\u5165\uFF0C\u65E0\u9700\u624B\u52A8\u914D\u7F6E\uFF1A", "", ...lines].join("\n"));
206287
+ writeFileSync18(join40(dir, "CONNECTORS.md"), ["# \u53EF\u7528\u8FDE\u63A5\u5668", "", "\u4EE5\u4E0B\u8FDE\u63A5\u5668\u5DF2\u4E3A\u672C\u6B21\u4F1A\u8BDD\u914D\u7F6E\uFF0C\u51ED\u8BC1\u5DF2\u901A\u8FC7\u73AF\u5883\u53D8\u91CF\u6216 CLI wrapper \u6CE8\u5165\uFF0C\u65E0\u9700\u624B\u52A8\u914D\u7F6E\uFF1A", "", ...lines].join("\n"));
206009
206288
  }
206010
206289
  spawnCwd = dir;
206011
206290
  }
@@ -206090,7 +206369,7 @@ ${composed}`;
206090
206369
  }
206091
206370
  if (req.method === "POST" && /^\/api\/connectors\/[^/]+\/disconnect$/.test(url.pathname)) {
206092
206371
  const connId = decodeURIComponent(url.pathname.split("/")[3] ?? "");
206093
- const svc = opts.actors?.service;
206372
+ const svc = await connectorActorsService();
206094
206373
  if (!svc) {
206095
206374
  res.writeHead(501).end(JSON.stringify({ error: "actors service disabled" }));
206096
206375
  return;
@@ -206116,7 +206395,7 @@ ${composed}`;
206116
206395
  }
206117
206396
  if (req.method === "DELETE" && /^\/api\/connectors\/[^/]+$/.test(url.pathname)) {
206118
206397
  const connId = decodeURIComponent(url.pathname.slice("/api/connectors/".length));
206119
- const svc = opts.actors?.service;
206398
+ const svc = await connectorActorsService();
206120
206399
  if (!svc) {
206121
206400
  res.writeHead(501).end(JSON.stringify({ error: "actors service disabled" }));
206122
206401
  return;
@@ -206135,6 +206414,31 @@ ${composed}`;
206135
206414
  }
206136
206415
  return;
206137
206416
  }
206417
+ if (req.method === "DELETE" && /^\/api\/actors\/[^/]+\/connectors\/[^/]+$/.test(url.pathname)) {
206418
+ const parts = url.pathname.split("/");
206419
+ const actorId = decodeURIComponent(parts[3] ?? "");
206420
+ const connId = decodeURIComponent(parts[5] ?? "");
206421
+ const svc = await connectorActorsService();
206422
+ if (!svc) {
206423
+ res.writeHead(501).end(JSON.stringify({ error: "actors service disabled" }));
206424
+ return;
206425
+ }
206426
+ try {
206427
+ const { variablesDeleted } = await svc.deleteActorConnector(actorId, connId);
206428
+ let channelDisabled = false;
206429
+ if (connId === "feishu" && channelService) {
206430
+ const b2 = await channelService.getBindingForActor(actorId);
206431
+ if (b2 && b2.status !== "revoked") {
206432
+ await channelService.disable(actorId);
206433
+ channelDisabled = true;
206434
+ }
206435
+ }
206436
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true, variablesDeleted, channelDisabled }));
206437
+ } catch (e) {
206438
+ res.writeHead(500).end(JSON.stringify({ error: String(e) }));
206439
+ }
206440
+ return;
206441
+ }
206138
206442
  if (url.pathname === "/api/connectors/feishu/setup" && req.method === "GET") {
206139
206443
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
206140
206444
  const send = (obj2) => res.write(`data: ${JSON.stringify(obj2)}
@@ -206184,8 +206488,8 @@ ${composed}`;
206184
206488
  if (!errStr && pollData["client_id"]) {
206185
206489
  const clientId = String(pollData["client_id"]);
206186
206490
  const clientSecret = pollData["client_secret"] ? String(pollData["client_secret"]) : void 0;
206187
- if (opts.actors && clientSecret) {
206188
- const svc = opts.actors.service;
206491
+ const svc = clientSecret ? await connectorActorsService() : void 0;
206492
+ if (svc && clientSecret) {
206189
206493
  const credScope = actorIdParam ? { scope: "personal", actorId: actorIdParam } : { scope: "global" };
206190
206494
  await svc.putVariable({ key: "FEISHU_APP_ID", value: clientId, ...credScope, connectorId: "feishu", encrypted: true });
206191
206495
  await svc.putVariable({ key: "FEISHU_APP_SECRET", value: clientSecret, ...credScope, connectorId: "feishu", encrypted: true });
@@ -206248,6 +206552,8 @@ ${composed}`;
206248
206552
  });
206249
206553
  githubAppPending.put(state, {
206250
206554
  ...actorId ? { actorId } : {},
206555
+ // 回调那一步没有鉴权头,解析不出公司——只能在这里记下来带过去。
206556
+ ...currentCompanyId ? { companyId: currentCompanyId } : {},
206251
206557
  employeeSlug: nameSlug,
206252
206558
  createdAtMs: Date.now(),
206253
206559
  // 存下这次选的归属——回调时要拿它跟 GitHub 返回的 owner 比对。
@@ -206268,7 +206574,7 @@ ${composed}`;
206268
206574
  if (url.pathname === "/api/connectors/github/app/orgs" && req.method === "POST") {
206269
206575
  try {
206270
206576
  const { actorId } = JSON.parse(rawBody || "{}");
206271
- const svc = opts.actors?.service;
206577
+ const svc = await connectorActorsService();
206272
206578
  if (!svc) throw new Error("actors service unavailable");
206273
206579
  const token = await svc.revealResolvedVariable("GITHUB_TOKEN", actorId);
206274
206580
  if (!token) {
@@ -206301,7 +206607,7 @@ ${composed}`;
206301
206607
  }
206302
206608
  if (url.pathname === "/api/connectors/github/app/available" && req.method === "POST") {
206303
206609
  try {
206304
- const svc = opts.actors?.service;
206610
+ const svc = await connectorActorsService();
206305
206611
  if (!svc) throw new Error("actors service unavailable");
206306
206612
  const rows = await svc.listVariables();
206307
206613
  const plain = (key, actorId) => rows.find((r) => r.key === key && r.actorId === actorId && !r.encrypted)?.maskedValue ?? "";
@@ -206364,7 +206670,7 @@ ${composed}`;
206364
206670
  res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "\u7F3A\u5C11 appId" }));
206365
206671
  return;
206366
206672
  }
206367
- const svc = opts.actors?.service;
206673
+ const svc = await connectorActorsService();
206368
206674
  if (!svc) throw new Error("actors service unavailable");
206369
206675
  const rows = await svc.listVariables();
206370
206676
  const owners = rows.filter((r) => r.key === "GITHUB_APP_ID" && !r.encrypted && r.maskedValue === appId);
@@ -206411,7 +206717,7 @@ ${composed}`;
206411
206717
  bad(400, "\u8FD9\u4E0D\u50CF\u4E00\u4E2A\u79C1\u94A5\u6587\u4EF6\u2014\u2014\u8BF7\u4E0A\u4F20 GitHub \u4E0B\u8F7D\u7684 .pem\uFF08\u5185\u5BB9\u4EE5 -----BEGIN ... PRIVATE KEY----- \u5F00\u5934\uFF09");
206412
206718
  return;
206413
206719
  }
206414
- const svc = opts.actors?.service;
206720
+ const svc = await connectorActorsService();
206415
206721
  if (!svc) throw new Error("actors service unavailable");
206416
206722
  let meta;
206417
206723
  try {
@@ -206469,7 +206775,7 @@ ${composed}`;
206469
206775
  if (url.pathname === "/api/connectors/github/app/permissions" && req.method === "POST") {
206470
206776
  try {
206471
206777
  const { actorId } = JSON.parse(rawBody || "{}");
206472
- const svc = opts.actors?.service;
206778
+ const svc = await connectorActorsService();
206473
206779
  if (!svc) throw new Error("actors service unavailable");
206474
206780
  const appId = await svc.revealResolvedVariable("GITHUB_APP_ID", actorId);
206475
206781
  const privateKeyPem = await svc.revealResolvedVariable("GITHUB_APP_PRIVATE_KEY", actorId);
@@ -206523,7 +206829,7 @@ ${composed}`;
206523
206829
  fail(problem.message);
206524
206830
  return;
206525
206831
  }
206526
- const svc = opts.actors?.service;
206832
+ const svc = await connectorActorsService(pending.companyId);
206527
206833
  if (!svc) throw new Error("actors service unavailable");
206528
206834
  const scope = pending.actorId ? { scope: "personal", actorId: pending.actorId } : { scope: "global" };
206529
206835
  const put3 = (key, value2, encrypted) => svc.putVariable({ key, value: value2, ...scope, connectorId: "github", encrypted });
@@ -206592,7 +206898,7 @@ ${composed}`;
206592
206898
  reply({ ok: false, error: "pending" });
206593
206899
  return;
206594
206900
  }
206595
- const svc = opts.actors?.service;
206901
+ const svc = await connectorActorsService();
206596
206902
  if (!svc) throw new Error("actors service unavailable");
206597
206903
  const scope = pending.actorId ? { scope: "personal", actorId: pending.actorId } : { scope: "global" };
206598
206904
  const put3 = (key, value2, encrypted) => svc.putVariable({ key, value: value2, ...scope, connectorId: "github", encrypted });
@@ -206681,8 +206987,8 @@ ${composed}`;
206681
206987
  throw new Error(String(data["error_description"] ?? errStr ?? "login failed"));
206682
206988
  }
206683
206989
  if (!data["access_token"]) throw new Error("no access_token in response");
206684
- if (opts.actors) {
206685
- const svc = opts.actors.service;
206990
+ const svc = await connectorActorsService();
206991
+ if (svc) {
206686
206992
  if (actorId) {
206687
206993
  await svc.putVariable({ key: "FEISHU_APP_ID", value: appId, scope: "personal", actorId, connectorId: "feishu", encrypted: true });
206688
206994
  } else {
@@ -206783,8 +207089,8 @@ ${composed}`;
206783
207089
  });
206784
207090
  const p2 = await pr.json();
206785
207091
  if (p2.access_token) {
206786
- if (opts.actors) {
206787
- const svc = opts.actors.service;
207092
+ const svc = await connectorActorsService();
207093
+ if (svc) {
206788
207094
  const connId = "github";
206789
207095
  await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
206790
207096
  for (const [k2, v2] of [["GIT_AUTHOR_NAME", actorNameParam], ["GIT_AUTHOR_EMAIL", ""], ["GIT_COMMITTER_NAME", actorNameParam], ["GIT_COMMITTER_EMAIL", ""], ["EMAIL", ""]])
@@ -214524,7 +214830,7 @@ async function prepareDroppedDispatch(work, deps) {
214524
214830
  const node2 = kernel.model.artifacts.get(work.nodeId);
214525
214831
  const snap = await kernel.getStore().transaction((tx) => tx.loadWorkorder(work.workorderId));
214526
214832
  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;
214833
+ 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
214834
  if (snap.issues.some((i) => i.kind === "escalation" && i.aboutNodeId === node2.id && !i.resolvedAt && i.intent !== "advisory")) return false;
214529
214835
  if (!work.assigneeActorId) return false;
214530
214836
  const binding = await deps.bindingFor(work.assigneeActorId);
@@ -216135,13 +216441,14 @@ async function planChatTurnRecovery(deps) {
216135
216441
  const plans = [];
216136
216442
  for (const row of rows) {
216137
216443
  if (!row.runId) continue;
216138
- const run = await deps.getRun(row.runId).catch(() => null);
216444
+ const run = await deps.getRun(row.runId);
216139
216445
  if (!run || run.status !== "running" || run.action !== "chat" || !run.artifactId) continue;
216140
216446
  const active = byArtifact.get(run.artifactId);
216141
216447
  if (!active || deps.isWired(run.artifactId)) continue;
216142
216448
  const meta = asObj3(run.metadata);
216143
216449
  const runtimeSessionId = typeof meta?.runtimeSessionId === "string" ? meta.runtimeSessionId : run.jobKey ?? void 0;
216144
- plans.push({
216450
+ const turn = await deps.getTurnForRun?.(row.sessionId, run.id);
216451
+ const plan = {
216145
216452
  dispatchId: active.dispatchId,
216146
216453
  chatSessionId: row.sessionId,
216147
216454
  assistantMsgId: row.id,
@@ -216152,18 +216459,42 @@ async function planChatTurnRecovery(deps) {
216152
216459
  ...runtimeSessionId ? { runtimeSessionId } : {},
216153
216460
  startedAt: run.startedAt,
216154
216461
  seedContent: row.content,
216155
- seedParts: partsOf(row.parts)
216156
- });
216462
+ seedParts: partsOf(row.parts),
216463
+ ...turn ? { turnId: turn.id } : {}
216464
+ };
216465
+ if (turn && turn.status !== "reserved" && turn.status !== "running") {
216466
+ deps.onSettled?.({ plan, turn });
216467
+ continue;
216468
+ }
216469
+ plans.push(plan);
216157
216470
  }
216158
216471
  return plans;
216159
216472
  }
216473
+ async function finishPersistedRecoveryItems(items, sessionId, turnId, status, log3) {
216474
+ if (!items || !chatItemsDoubleWriteEnabled()) return [];
216475
+ try {
216476
+ const rows = await items.listItemsByTurn(sessionId, turnId);
216477
+ let version2 = await items.sessionVersionCursor(sessionId);
216478
+ const closed = [];
216479
+ for (const row of rows) {
216480
+ if (row.status !== "streaming") continue;
216481
+ const updated = await items.applyOp(row.id, { op: "set_status", status }, { version: ++version2 });
216482
+ if (!updated) throw new Error(`item ${row.id} disappeared`);
216483
+ closed.push(updated);
216484
+ }
216485
+ return closed;
216486
+ } catch (err) {
216487
+ log3(`[chat-recovery] items \u6536\u5C3E\u5931\u8D25\uFF1Asession=${sessionId} turn=${turnId} ${String(err)}`);
216488
+ return [];
216489
+ }
216490
+ }
216160
216491
  function wireRecoveredChatTurn(deps) {
216161
216492
  const { plan, handle, liveChat, chatStore, trace } = deps;
216162
216493
  const log3 = deps.log ?? (() => void 0);
216163
216494
  const itemLedger = new ChatItemLedger({
216164
216495
  ...deps.items ? { items: deps.items } : {},
216165
216496
  sessionId: plan.chatSessionId,
216166
- turnId: deps.itemsTurnId ?? fallbackTurnId(plan.runId),
216497
+ turnId: deps.itemsTurnId ?? plan.turnId ?? fallbackTurnId(plan.runId),
216167
216498
  runId: plan.runId,
216168
216499
  messageId: plan.assistantMsgId,
216169
216500
  log: log3
@@ -216329,6 +216660,14 @@ function wireRecoveredChatTurn(deps) {
216329
216660
  normalizedSource.finishTurn(failed ? "failed" : "completed");
216330
216661
  itemLedger.finish(failed ? "failed" : "completed");
216331
216662
  await itemLedger.drain();
216663
+ const closedItems = await finishPersistedRecoveryItems(
216664
+ deps.items,
216665
+ plan.chatSessionId,
216666
+ itemLedger.turnId,
216667
+ failed ? "failed" : "completed",
216668
+ log3
216669
+ );
216670
+ for (const item of closedItems) liveChat.publishItemStatus(plan.chatSessionId, item);
216332
216671
  await chatStore.updateMessage(currentMsgId, {
216333
216672
  content: itemLedger.projectContent(),
216334
216673
  status: failed ? "error" : "done",
@@ -216426,6 +216765,29 @@ async function reconcileChatExitFrame(deps) {
216426
216765
  const meta = asObj3(run.metadata);
216427
216766
  if (meta?.dispatchId !== deps.dispatchId) continue;
216428
216767
  if (deps.ownedRunId?.(row.sessionId) === row.runId) return false;
216768
+ const turn = await deps.getTurnForRun?.(row.sessionId, run.id);
216769
+ if (turn && turn.status !== "reserved" && turn.status !== "running") {
216770
+ await reconcileSettledChatTurn({
216771
+ plan: {
216772
+ dispatchId: deps.dispatchId,
216773
+ chatSessionId: row.sessionId,
216774
+ assistantMsgId: row.id,
216775
+ runId: run.id,
216776
+ artifactId: run.artifactId,
216777
+ actor: run.actorId,
216778
+ startedAt: run.startedAt,
216779
+ seedContent: row.content,
216780
+ seedParts: partsOf(row.parts),
216781
+ turnId: turn.id
216782
+ },
216783
+ turn,
216784
+ chatStore: deps.chatStore,
216785
+ trace: deps.trace,
216786
+ ...deps.items ? { items: deps.items } : {},
216787
+ ...deps.log ? { log: deps.log } : {}
216788
+ });
216789
+ return true;
216790
+ }
216429
216791
  const { info } = deps;
216430
216792
  const failed = exitFailed(info);
216431
216793
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -216467,6 +216829,13 @@ async function reconcileChatExitFrame(deps) {
216467
216829
  normalizer.turnFinished(failed ? "failed" : "completed");
216468
216830
  itemLedger.finish(failed ? "failed" : "completed");
216469
216831
  await itemLedger.drain();
216832
+ await finishPersistedRecoveryItems(
216833
+ deps.items,
216834
+ row.sessionId,
216835
+ itemLedger.turnId,
216836
+ failed ? "failed" : "completed",
216837
+ deps.log ?? (() => void 0)
216838
+ );
216470
216839
  await deps.chatStore.updateMessage(row.id, {
216471
216840
  content: itemLedger.projectContent(),
216472
216841
  status: failed ? "error" : "done",
@@ -216505,6 +216874,156 @@ async function reconcileChatExitFrame(deps) {
216505
216874
  }
216506
216875
  return false;
216507
216876
  }
216877
+ async function planChatTurnRecoveryAcrossCompanies(deps) {
216878
+ const groups = [];
216879
+ const settled = [];
216880
+ const skipped = [];
216881
+ let scanned = 0;
216882
+ for (const companyId of await deps.listCompanies()) {
216883
+ try {
216884
+ const stores = await deps.storesFor(companyId);
216885
+ const companySettled = [];
216886
+ const plans = await planChatTurnRecovery({
216887
+ activeSessions: deps.activeSessions,
216888
+ listRunningAssistantMessages: stores.listRunningAssistantMessages,
216889
+ getRun: deps.getRun,
216890
+ isWired: deps.isWired,
216891
+ ...stores.getTurnForRun ? { getTurnForRun: stores.getTurnForRun } : {},
216892
+ onSettled: (hit) => companySettled.push(hit)
216893
+ });
216894
+ scanned += 1;
216895
+ settled.push(...companySettled.map((hit) => ({ ...hit, companyId, stores })));
216896
+ if (plans.length) groups.push({ companyId, stores, plans });
216897
+ if (deps.stopOnFirstHit && (plans.length || companySettled.length)) break;
216898
+ } catch (err) {
216899
+ skipped.push({ companyId, error: String(err) });
216900
+ deps.onCompanyError?.(companyId, err);
216901
+ }
216902
+ }
216903
+ return { groups, settled, scanned, skipped };
216904
+ }
216905
+ async function sweepDanglingChatTurnsAcrossCompanies(deps) {
216906
+ const { listCompanies, storesFor, onCompanyError, onSessionClosed, log: log3, ...rest } = deps;
216907
+ const closed = [];
216908
+ for (const companyId of await listCompanies()) {
216909
+ try {
216910
+ const stores = await storesFor(companyId);
216911
+ const ids2 = await sweepDanglingChatTurns({
216912
+ ...rest,
216913
+ listRunningAssistantMessages: stores.listRunningAssistantMessages,
216914
+ listMessages: (sid) => stores.chatStore.listMessages(sid),
216915
+ updateMessage: (id, patch) => stores.chatStore.updateMessage(id, patch),
216916
+ ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {},
216917
+ ...onSessionClosed ? { onSessionClosed: (sid, reason, messageId) => onSessionClosed(sid, reason, messageId, companyId) } : {}
216918
+ });
216919
+ closed.push(...ids2);
216920
+ } catch (err) {
216921
+ onCompanyError?.(companyId, err);
216922
+ }
216923
+ }
216924
+ return closed;
216925
+ }
216926
+ async function reconcileChatExitFrameAcrossCompanies(deps) {
216927
+ const { listCompanies, storesFor, onCompanyError, log: log3, ...rest } = deps;
216928
+ for (const companyId of await listCompanies()) {
216929
+ try {
216930
+ const stores = await storesFor(companyId);
216931
+ const handled2 = await reconcileChatExitFrame({
216932
+ ...rest,
216933
+ listRunningAssistantMessages: stores.listRunningAssistantMessages,
216934
+ chatStore: stores.chatStore,
216935
+ ...stores.getTurnForRun ? { getTurnForRun: stores.getTurnForRun } : {},
216936
+ ...stores.items ? { items: stores.items } : {},
216937
+ ...log3 ? { log: (m2) => log3(companyId ? `${m2}\uFF3B${companyId}\uFF3D` : m2) } : {}
216938
+ });
216939
+ if (handled2) return true;
216940
+ } catch (err) {
216941
+ onCompanyError?.(companyId, err);
216942
+ }
216943
+ }
216944
+ return false;
216945
+ }
216946
+ async function reconcileSettledChatTurn(deps) {
216947
+ const { plan, turn } = deps;
216948
+ if (turn.status === "reserved" || turn.status === "running") return;
216949
+ const succeeded = turn.status === "succeeded";
216950
+ const completedAt = turn.completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
216951
+ const turnId = deps.items ? await resolveChatItemsTurnId(deps.items, plan.assistantMsgId, turn.id) : turn.id;
216952
+ await finishPersistedRecoveryItems(
216953
+ deps.items,
216954
+ plan.chatSessionId,
216955
+ turnId,
216956
+ succeeded ? "completed" : "failed",
216957
+ deps.log ?? (() => void 0)
216958
+ );
216959
+ await deps.chatStore.updateMessage(plan.assistantMsgId, {
216960
+ status: succeeded ? "done" : "error",
216961
+ completedAt
216962
+ });
216963
+ const run = await deps.trace.getRun(plan.runId);
216964
+ if (run?.status === "running") {
216965
+ await deps.trace.updateRun(plan.runId, {
216966
+ status: turn.status,
216967
+ endedAt: completedAt
216968
+ });
216969
+ }
216970
+ }
216971
+ async function recoverChatTurnsAcrossCompanies(deps) {
216972
+ const result = await planChatTurnRecoveryAcrossCompanies(deps);
216973
+ const recovered = [];
216974
+ const log3 = deps.log ?? (() => void 0);
216975
+ const retire = async (hit) => {
216976
+ const { plan, turn, companyId, stores } = hit;
216977
+ 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`);
216978
+ deps.evict(plan.dispatchId);
216979
+ try {
216980
+ await reconcileSettledChatTurn({
216981
+ plan,
216982
+ turn,
216983
+ chatStore: stores.chatStore,
216984
+ trace: deps.trace,
216985
+ ...stores.items ? { items: stores.items } : {},
216986
+ log: log3
216987
+ });
216988
+ } catch (err) {
216989
+ deps.onCompanyError?.(companyId, err);
216990
+ }
216991
+ };
216992
+ for (const hit of result.settled) await retire(hit);
216993
+ for (const { companyId, stores, plans } of result.groups) {
216994
+ for (const plan of plans) {
216995
+ try {
216996
+ const itemsTurnId = stores.items ? await resolveChatItemsTurnId(stores.items, plan.assistantMsgId, plan.turnId ?? fallbackTurnId(plan.runId)) : plan.turnId ?? fallbackTurnId(plan.runId);
216997
+ const turn = await stores.getTurnForRun?.(plan.chatSessionId, plan.runId);
216998
+ if (turn && turn.status !== "reserved" && turn.status !== "running") {
216999
+ const hit = { companyId, stores, plan, turn };
217000
+ result.settled.push(hit);
217001
+ await retire(hit);
217002
+ continue;
217003
+ }
217004
+ const handle = deps.recover(plan);
217005
+ const { done } = wireRecoveredChatTurn({
217006
+ plan,
217007
+ handle,
217008
+ liveChat: deps.liveChat,
217009
+ chatStore: stores.chatStore,
217010
+ trace: deps.trace,
217011
+ register: () => deps.register(plan, handle),
217012
+ unregister: () => deps.unregister(plan),
217013
+ ...deps.traceHealth ? { traceHealth: deps.traceHealth } : {},
217014
+ ...stores.items ? { items: stores.items, itemsTurnId } : {},
217015
+ log: log3
217016
+ });
217017
+ recovered.push({ companyId, plan, done });
217018
+ log3(`[chat-recovery] \u91CD\u6302 chat \u8F6E\uFF1Acompany=${companyId ?? "<default>"} session=${plan.chatSessionId} run=${plan.runId} dispatch=${plan.dispatchId}`);
217019
+ } catch (err) {
217020
+ result.skipped.push({ companyId, error: String(err) });
217021
+ deps.onCompanyError?.(companyId, err);
217022
+ }
217023
+ }
217024
+ }
217025
+ return { ...result, recovered };
217026
+ }
216508
217027
  var import_node_crypto45, asObj3, partsOf, maxPartSeq, exitFailed, exitErrorText, runTerminalPatch;
216509
217028
  var init_chat_recovery = __esm({
216510
217029
  "../server/src/chat-recovery.ts"() {
@@ -217856,9 +218375,32 @@ var init_service5 = __esm({
217856
218375
  const a = await this.opts.store.getActor(id);
217857
218376
  if (!a) throw new Error(`actor not found: ${id}`);
217858
218377
  await this.upsertActor({ ...a, status: "disabled" }, by);
218378
+ await this.dropRuntimeBindingOfDeletedAgent(a.id, a.kind, by);
217859
218379
  if (a.status === "active") await this.opts.onActorDisabled?.(id, by);
217860
218380
  return a;
217861
218381
  }
218382
+ /**
218383
+ * 删掉 Agent 时把它跟 runtime 的绑定一起撤掉。
218384
+ *
218385
+ * 为什么必须做(2026-09-09 线上实测):删除只写 `status='disabled'`,`actor_bindings` 那行
218386
+ * 原样留着且仍是 `active`。而 `GET /api/actors` 会把已删 Agent 过滤掉、`GET /api/bindings` 不会——
218387
+ * 运行时管理页拿两份数据对着算「运行 Agent」,于是已删的 Agent 继续占着一格,
218388
+ * 名字还查不回来,页面上直接露出 `asst-xxx-cbf34f` 这种裸 id。didi 组织实测:yx_claude 上
218389
+ * 12 条 active 绑定里 7 条的 Agent 早被删了(其中 5 条是助理)。
218390
+ *
218391
+ * 顺带修好另一处:`pickAssistantRuntime` 用「(nodeId,kind) 上的 active 绑定数」当负载做均衡,
218392
+ * 已删 Agent 的残留绑定会把负载算高,把新助理往别的机器上赶。
218393
+ *
218394
+ * 只对 agent 生效:真人被移出组织是「离岗」不是删除,PRD 明确其助理照常在岗、账号回来还能接上。
218395
+ * 幂等:removeBinding 对没有绑定的 actor 是 no-op,所以重复删也安全(存量脏数据也能靠再删一次修好)。
218396
+ */
218397
+ async dropRuntimeBindingOfDeletedAgent(id, kind, by) {
218398
+ if (kind !== "agent") return;
218399
+ const existing = await this.opts.store.getBinding(id);
218400
+ if (!existing) return;
218401
+ await this.opts.store.removeBinding(id);
218402
+ await this.audit({ kind: "binding_change", actorId: id, by, at: this.now(), detail: { removed: true, reason: "agent_deleted" } });
218403
+ }
217862
218404
  /**
217863
218405
  * 原子「若当前 active 则停用」(QA R10):与 disableActor 的区别在于——
217864
218406
  * 只在 actor 当前**确为 active** 时才写,返回 `ok:true + before + seq`;已被并发者/别的路径先 disable
@@ -217882,6 +218424,7 @@ var init_service5 = __esm({
217882
218424
  if (!res.ok) return res;
217883
218425
  this.opts.onRolesChanged?.(id, []);
217884
218426
  await this.audit({ kind: "actor_upsert", actorId: id, by, at: this.now(), detail: { status: "disabled", roles: res.before.roles } });
218427
+ await this.dropRuntimeBindingOfDeletedAgent(id, res.before.kind, by);
217885
218428
  await this.opts.onActorDisabled?.(id, by);
217886
218429
  return { ok: true, before: res.before, seq };
217887
218430
  }
@@ -218482,39 +219025,42 @@ ${input.description}
218482
219025
  const c = cfg ?? await this.opts.store.latestConfig(actorId);
218483
219026
  return new Set(c?.connectorIds ?? []);
218484
219027
  }
218485
- /** 设置某员工×连接器的连接记录(启用/停用)。无则创建,有则翻转 enabled。 */
218486
- async setActorConnectorEnabled(actorId, connectorId, enabled) {
218487
- const conn = { actorId, connectorId, enabled, updatedAt: this.now() };
219028
+ /**
219029
+ * 设置某员工×连接器的连接记录(启用/停用)。无则创建,有则翻转 enabled
219030
+ *
219031
+ * `configuredBy` 只在**授权流程走完**时传(「谁为这名员工把它接上的」)。普通开关不传,
219032
+ * 此时把已有值原样带回——**只增不抹**:翻一次开关不该把授权时记下的人擦掉。
219033
+ * pg 侧另有 `COALESCE` 兜同一条,两层都做是因为内存 store 是整行替换。
219034
+ */
219035
+ async setActorConnectorEnabled(actorId, connectorId, enabled, configuredBy) {
219036
+ const keep = configuredBy ?? (await this.opts.store.listActorConnectorConnections(actorId)).find((c) => c.connectorId === connectorId)?.configuredBy;
219037
+ const conn = {
219038
+ actorId,
219039
+ connectorId,
219040
+ enabled,
219041
+ updatedAt: this.now(),
219042
+ ...keep ? { configuredBy: keep } : {}
219043
+ };
218488
219044
  await this.opts.store.upsertActorConnectorConnection(conn);
218489
219045
  return conn;
218490
219046
  }
218491
- listActorConnectorConnections(actorId) {
218492
- return this.opts.store.listActorConnectorConnections(actorId);
218493
- }
218494
219047
  /**
218495
- * 移除某员工与某连接器的连接(组织页连接器详情的「移除连接」,2026-09-09 原型 1600:5765)。
219048
+ * 删除**一条**「员工 × 连接器」连接:连接记录 + 该员工这个连接器的**个人凭据变量**。
218496
219049
  *
218497
- * 删两样东西,缺一不可:
218498
- * 这名员工**自己**那份连接器凭据(`scope=personal` 且 `connectorId` 命中的变量行)——
218499
- * 不删的话,「移除」之后 `actorConnected` 里还有它,卡片照样在,读作「删不掉」;
218500
- * ② 「员工×连接器」的连接记录——不删的话 `effective` 里还有它(enabled=false),
218501
- * 卡片变成一张「已停用」的僵尸卡,而人要的是它消失。
219050
+ * 与开关的区别(发起人 2026-09-09 定的,两颗按钮并存):开关是「暂时不用」,凭据留着、
219051
+ * 打开就能接着用;删除是「不要了」,凭据清掉、再要用得重走一遍授权。
218502
219052
  *
218503
- * **不碰组织那条 connector 行**(决策 0080 修订五的同一条理由):组织级连接是别人配的资产,
218504
- * 一名员工点「移除连接」不该把全组织的连接拆掉。组织级的断开在「管理 > 连接器」。
218505
- * 于是:组织已连接时,移除个人连接后这名员工会**回落到组织默认**——卡片仍在,但身份那一行
218506
- * 变回组织账号。这是对的,不是没删干净。
218507
- *
218508
- * 幂等:没有个人凭据、没有记录时照样返回成功(`variablesDeleted: 0`)——重复点、并发点都不该报错。
219053
+ * **只动这名员工自己的东西**:组织级变量(scope=global)一个不碰——那是别人也在用的。
219054
+ * 飞书对话通道的下线不在这里做(本服务够不到 channelService),由路由层在删完凭据后补一刀。
218509
219055
  */
218510
- async removeActorConnector(actorId, connectorId) {
218511
- const rows = await this.opts.store.listVariables(actorId);
218512
- const mine = rows.filter(
218513
- (v2) => v2.scope === "personal" && v2.actorId === actorId && v2.connectorId === connectorId
218514
- );
218515
- for (const v2 of mine) await this.opts.store.deleteVariable(v2.key, actorId);
219056
+ async deleteActorConnector(actorId, connectorId) {
219057
+ const personal = (await this.opts.store.listVariables(actorId)).filter((v2) => v2.scope === "personal" && v2.actorId === actorId && v2.connectorId === connectorId);
219058
+ for (const v2 of personal) await this.deleteVariable(v2.key, actorId);
218516
219059
  await this.opts.store.deleteActorConnectorConnection(actorId, connectorId);
218517
- return { removed: true, variablesDeleted: mine.length };
219060
+ return { variablesDeleted: personal.length };
219061
+ }
219062
+ listActorConnectorConnections(actorId) {
219063
+ return this.opts.store.listActorConnectorConnections(actorId);
218518
219064
  }
218519
219065
  /**
218520
219066
  * 员工连接器面板所需状态:连接记录 + 组织级已连接集合 + 该员工已授权(有个人 connector 变量)集合。
@@ -220483,14 +221029,9 @@ function actorsDomain(opts) {
220483
221029
  const { service } = await resolveCtx(req.auth.companyId);
220484
221030
  const b2 = req.body;
220485
221031
  if (typeof b2?.enabled !== "boolean") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 enabled (boolean)");
220486
- const conn = await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, b2.enabled);
221032
+ const conn = b2.enabled ? await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, true, req.auth.actor) : await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, false);
220487
221033
  return { status: 200, body: conn };
220488
221034
  });
220489
- router.delete("/api/actors/:id/connectors/:connectorId", async (req) => {
220490
- const { service } = await resolveCtx(req.auth.companyId);
220491
- const r = await service.removeActorConnector(req.params.id, req.params.connectorId);
220492
- return { status: 200, body: r };
220493
- });
220494
221035
  const requireVariableManager = (req) => {
220495
221036
  if (!isHumanActor(req.auth.actor)) {
220496
221037
  throw new ApiError(
@@ -225713,13 +226254,19 @@ function reviewSummary(snap, reviews, events) {
225713
226254
  const name = node2?.title ?? first.nodeId;
225714
226255
  const latest = latestActivityReviews(reviews, events);
225715
226256
  const requirements = snap.requirements.filter((r) => r.nodeId === first.nodeId && r.reviewerActorId === first.reviewerActorId);
225716
- const judgement = judgeWork({ requirements: requirements.length ? requirements : reviews, reviews });
226257
+ for (const requirement of requirements) {
226258
+ const current = reviews.find((r) => r.id === requirement.latestReviewId);
226259
+ if (!current) continue;
226260
+ const index2 = latest.findIndex((r) => r.reviewGroup === requirement.reviewGroup);
226261
+ if (index2 >= 0) latest[index2] = current;
226262
+ }
226263
+ const judgement = judgeWork({ requirements: requirements.length ? requirements : latest, reviews: latest });
225717
226264
  const pending = latest.filter((r) => !r.cancelledAt && !r.verdict && reviewState(r) === "running");
225718
226265
  if (work?.acceptanceState === "accepted" || work?.acceptedAt || work?.acceptanceState !== "rejected" && judgement === "passed") {
225719
226266
  return { phase: "done", status: `\u300A${name}\u300B\u5BA1\u6838\u901A\u8FC7\u3002`, latest, pending: [] };
225720
226267
  }
225721
226268
  if (work?.acceptanceState === "rejected" || judgement === "rejected") {
225722
- return { phase: "done", status: "\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002", latest, pending: [] };
226269
+ return { phase: "done", status: `\u300A${name}\u300B\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002`, latest, pending: [] };
225723
226270
  }
225724
226271
  const endedWork = !!(work?.cancelledAt || work?.deadAt);
225725
226272
  if (endedWork && latest.every((r) => !!r.cancelledAt && reviewState(r) !== "retry")) {
@@ -225749,6 +226296,66 @@ var init_review_activity = __esm({
225749
226296
  }
225750
226297
  });
225751
226298
 
226299
+ // ../server/src/domains/collab/review-requirement-activity.ts
226300
+ function reviewRequirementActivity(snap, events, ref2) {
226301
+ const previous3 = /* @__PURE__ */ new Map();
226302
+ const titles = new Map(snap.nodes.map((n) => [n.id, n.title]));
226303
+ const cards = [];
226304
+ const label = (r) => `${ref2(r.reviewerActorId).name || "\u672A\u547D\u540D\u5BA1\u6838\u4EBA"}${r.source === "closure" ? "\uFF08\u7ED3\u6848\u5BA1\u6838\uFF09" : ""}`;
226305
+ const names = (rows) => [...new Set(rows.map(label))].join("\u3001") || "\u65E0";
226306
+ for (const rec of [...events].sort((a, b2) => a.seq - b2.seq)) {
226307
+ if (rec.status === "pending" || rec.status === "failed") continue;
226308
+ const e = rec.event;
226309
+ if (e.kind === "plan.changed") {
226310
+ for (const node2 of e.addNodes ?? []) previous3.set(node2.id, node2.reviewers ?? []);
226311
+ for (const id of e.removeNodes ?? []) previous3.delete(id);
226312
+ continue;
226313
+ }
226314
+ if (e.kind === "plan.add_node") {
226315
+ previous3.set(e.nodeId, e.reviewers ?? []);
226316
+ continue;
226317
+ }
226318
+ if (e.kind !== "plan.update_review_requirements") continue;
226319
+ const before = previous3.get(e.nodeId);
226320
+ const after = e.reviewers;
226321
+ const beforeKeys = new Set(before?.map(requirementKey));
226322
+ const afterKeys = new Set(after.map(requirementKey));
226323
+ const added = before ? after.filter((r) => !beforeKeys.has(requirementKey(r))) : [];
226324
+ const removed = before?.filter((r) => !afterKeys.has(requirementKey(r))) ?? [];
226325
+ const nodeTitle = titles.get(e.nodeId) || "\u5DF2\u79FB\u9664\u7684\u8282\u70B9";
226326
+ const system = rec.actorId.startsWith("actor:system:");
226327
+ const closureAdded = system && added.length > 0 && !removed.length && added.every((r) => r.source === "closure");
226328
+ cards.push({
226329
+ id: `wo:${rec.seq}`,
226330
+ seq: rec.seq,
226331
+ at: rec.createdAt,
226332
+ updatedAt: rec.createdAt,
226333
+ nodeId: e.nodeId,
226334
+ nodeTitle,
226335
+ executor: system ? { ...ref2(rec.actorId), name: "\u7CFB\u7EDF" } : ref2(rec.actorId),
226336
+ ...rec.handActorId ? { handActor: ref2(rec.handActorId) } : {},
226337
+ phase: "done",
226338
+ status: closureAdded ? `\u4E3A\u300A${nodeTitle}\u300B\u8865\u5145\u4E86\u7ED3\u6848\u5BA1\u6838\u8981\u6C42\u3002` : `\u66F4\u65B0\u4E86\u300A${nodeTitle}\u300B\u7684\u5BA1\u6838\u5217\u8868\u3002`,
226339
+ detail: [
226340
+ ...added.length ? [`\u65B0\u589E\uFF1A${names(added)}`] : [],
226341
+ ...removed.length ? [`\u79FB\u9664\uFF1A${names(removed)}`] : [],
226342
+ `\u66F4\u65B0\u540E\uFF1A${names(after)}`
226343
+ ].join("\n"),
226344
+ artifacts: [],
226345
+ actions: []
226346
+ });
226347
+ previous3.set(e.nodeId, after);
226348
+ }
226349
+ return cards;
226350
+ }
226351
+ var requirementKey;
226352
+ var init_review_requirement_activity = __esm({
226353
+ "../server/src/domains/collab/review-requirement-activity.ts"() {
226354
+ "use strict";
226355
+ requirementKey = (r) => JSON.stringify([r.reviewerActorId, r.reviewGroup, r.source]);
226356
+ }
226357
+ });
226358
+
225752
226359
  // ../server/src/domains/collab/workorder-manager.ts
225753
226360
  function resolveWorkorderManager(snap) {
225754
226361
  const nodes = snap.nodes;
@@ -225772,6 +226379,9 @@ var init_workorder_manager = __esm({
225772
226379
  function isPendingActor(id) {
225773
226380
  return typeof id === "string" && id.startsWith("pending:");
225774
226381
  }
226382
+ function isAgentActor(id) {
226383
+ return typeof id === "string" && id.startsWith("actor:agent:");
226384
+ }
225775
226385
  function isDispatchLayerFailure(detail) {
225776
226386
  const reason = detail?.reason;
225777
226387
  return typeof reason === "string" && reason.startsWith("dispatch ");
@@ -226307,6 +226917,22 @@ function buildWorkorderActivity(input) {
226307
226917
  const created = events.filter((r) => r.event.kind === "review.create" && ids2.has(r.event.reviewId));
226308
226918
  const dates = summary.latest.flatMap((r) => [r.createdAt, r.startedAt, r.endedAt, r.decidedAt, r.cancelledAt, r.retryAt].filter((v2) => !!v2));
226309
226919
  const pending = summary.pending.filter((r) => r.reviewerActorId.startsWith("actor:human:") && (!input.viewerActorId || r.reviewerActorId === input.viewerActorId));
226920
+ const groups = [...new Set(summary.latest.map((r) => r.reviewGroup))];
226921
+ const reviewNotes = summary.latest.flatMap((r) => {
226922
+ const text5 = r.note?.trim();
226923
+ if (!text5) return [];
226924
+ const rejected = r.verdict === "request_changes";
226925
+ const label = ACTIVITY_COPY.reviewNoteLabel(rejected);
226926
+ return [{
226927
+ id: r.id,
226928
+ // 同一个人担多个评审组时才标组号——单组标了纯属噪声(卡上本来就只有这一个人)。
226929
+ label: groups.length > 1 ? `\u8BC4\u5BA1\u7EC4 ${groups.indexOf(r.reviewGroup) + 1} \xB7 ${label}` : label,
226930
+ text: text5,
226931
+ tone: rejected ? "reject" : "note"
226932
+ }];
226933
+ });
226934
+ const revieweeId = workById.get(workId)?.assigneeActorId;
226935
+ const reviewLine = reviewNotes.length && revieweeId && revieweeId !== reviewerId ? reviewThreadSessionId(revieweeId, reviewerId) : void 0;
226310
226936
  visibleReviews.set(cardId, {
226311
226937
  id: cardId,
226312
226938
  reviewWorkId: workId,
@@ -226320,8 +226946,13 @@ function buildWorkorderActivity(input) {
226320
226946
  executorId: reviewerId,
226321
226947
  phase: summary.phase,
226322
226948
  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"),
226949
+ /* 同一人担多个责任组时,展开区留一份**逐组结论**(只有判定词、没有意见正文)——小状态只有
226950
+ 一句总判定,说不清哪个组过了哪个组没过。意见正文一律走下面的意见条。 */
226951
+ ...summary.latest.length > 1 ? {
226952
+ detail: summary.latest.map((r) => `${nameOf(r.reviewerActorId)}\uFF08\u8BC4\u5BA1\u7EC4 ${groups.indexOf(r.reviewGroup) + 1}\uFF09\uFF1A${reviewLabel(r)}`).join("\n")
226953
+ } : {},
226954
+ ...reviewNotes.length ? { reviewNotes } : {},
226955
+ ...reviewLine ? { reviewThreadSessionId: reviewLine } : {},
226325
226956
  artifacts: artifactsByWork.get(workId) ?? [],
226326
226957
  actions: pending.length ? [
226327
226958
  { kind: "review", label: BUTTON.approve, target: first.nodeId, revisionId: workId, verdict: "approve" },
@@ -226361,7 +226992,7 @@ ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u
226361
226992
  if (issueById.get(issueId)?.kind === "gap") {
226362
226993
  d.traceAvailable = true;
226363
226994
  d.traceWorkIds = gapOrigins.get(issueId);
226364
- } else {
226995
+ } else if (!issueId.startsWith(REWORK_ANNOTATION_PREFIX)) {
226365
226996
  d.traceWorkIds = issueWorks.get(issueId);
226366
226997
  }
226367
226998
  }
@@ -226382,24 +227013,31 @@ ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u
226382
227013
  }
226383
227014
  return void 0;
226384
227015
  };
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
- }));
227016
+ const cards = drafts.map((d) => {
227017
+ const traced = isAgentActor(d.executorId);
227018
+ return {
227019
+ id: d.id,
227020
+ seq: d.seq,
227021
+ at: d.at,
227022
+ updatedAt: d.updatedAt,
227023
+ ...d.nodeId ? { nodeId: d.nodeId, nodeTitle: nodeName(d.nodeId) } : {},
227024
+ executor: ref2(d.executorId),
227025
+ ...d.reviewWorkId ? { reviewWorkId: d.reviewWorkId, reviewers: d.reviewerIds?.map(ref2) } : {},
227026
+ ...d.handActorId ? { handActor: ref2(d.handActorId) } : {},
227027
+ phase: d.phase,
227028
+ status: d.status,
227029
+ ...d.detail ? { detail: d.detail } : {},
227030
+ ...d.reviewNotes?.length ? { reviewNotes: d.reviewNotes } : {},
227031
+ ...d.reviewThreadSessionId ? { reviewThreadSessionId: d.reviewThreadSessionId } : {},
227032
+ artifacts: d.artifacts,
227033
+ actions: d.actions,
227034
+ ...traced && d.traceAvailable ? { traceAvailable: true } : {},
227035
+ ...d.traceWorkIds?.length ? { traceWorkIds: d.traceWorkIds, ...traced ? { traceAvailable: true } : {} } : {},
227036
+ .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(traced ? runIdOfCard(d) : void 0)
227037
+ };
227038
+ });
227039
+ cards.push(...reviewRequirementActivity(snap, events, ref2));
227040
+ cards.sort((a, b2) => a.updatedAt.localeCompare(b2.updatedAt) || a.seq - b2.seq || a.id.localeCompare(b2.id));
226403
227041
  return { workorderId, cards, truncated };
226404
227042
  }
226405
227043
  var ACTIVITY_EVENT_KINDS, ACTIVITY_EVENT_LIMIT, ACTIVITY_COPY, REWORK_ANNOTATION_PREFIX, ENGINE_RETRY_EXHAUSTED, BUTTON;
@@ -226409,9 +227047,12 @@ var init_activity2 = __esm({
226409
227047
  init_src();
226410
227048
  init_src4();
226411
227049
  init_review_activity();
227050
+ init_review_requirement_activity();
226412
227051
  init_workorder_manager();
226413
227052
  ACTIVITY_EVENT_KINDS = [
226414
227053
  "plan.changed",
227054
+ "plan.add_node",
227055
+ "plan.update_review_requirements",
226415
227056
  "plan.update_spec",
226416
227057
  "plan.node_retry",
226417
227058
  "work.create",
@@ -226465,15 +227106,13 @@ var init_activity2 = __esm({
226465
227106
  doneAwaitingReview: (name) => `\u300A${name}\u300B\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u5BA1\u6838\u3002`,
226466
227107
  done: (name) => `\u300A${name}\u300B\u5DF2\u5B8C\u6210\u3002`,
226467
227108
  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`,
227109
+ /* 🚨 审核卡那一族文案(待审 / 正在审 / 通过 / 不通过 / 超时 / 熔断)**不在这里**:
227110
+ 审核卡改成按「被审交付 × 评审人」合卡之后,它的小状态由 `review-activity.ts`
227111
+ `reviewSummary()` 按该评审人的**有效判定**整句给出,不是逐事件套模板。这里此前留着一份
227112
+ 同名同义的副本,六个 key 全是零引用——我 2026-09-09 改「审核不通过」文案时先改到了这份
227113
+ 死副本上,页面纹丝不动。删掉,别再留第二个事实源。
227114
+ 留在这里的只有下面这一句——它不是小状态,是**审核意见条**上那个判定词,由本文件拼卡时用。 */
227115
+ reviewNoteLabel: (rejected) => rejected ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1",
226477
227116
  acceptPending: "\u8BF7\u9A8C\u6536\u6700\u7EC8\u4EA4\u4ED8\u3002",
226478
227117
  accepted: "\u9A8C\u6536\u6700\u7EC8\u4EA4\u4ED8\u5DF2\u5B8C\u6210\u9A8C\u6536\u3002",
226479
227118
  acceptedNode: (name) => `\u300A${name}\u300B\u5DF2\u9A8C\u6536\u3002`,
@@ -227641,9 +228280,18 @@ function toAuditEntry(op) {
227641
228280
  summary: `${KIND_LABEL2[op.kind] ?? op.kind} \xB7 ${op.artifactId}`
227642
228281
  };
227643
228282
  }
228283
+ function nodeIdOfWorkId(workId) {
228284
+ if (!workId || !workId.startsWith("wk:")) return null;
228285
+ const body2 = workId.slice(3);
228286
+ const cut = body2.lastIndexOf(":");
228287
+ if (cut <= 0) return null;
228288
+ if (!/^\d+$/.test(body2.slice(cut + 1))) return null;
228289
+ const nodeId = body2.slice(0, cut);
228290
+ return nodeId.startsWith("artifact:") ? nodeId : null;
228291
+ }
227644
228292
  function eventToAuditEntry(rec) {
227645
228293
  const ev = rec.event;
227646
- const artifactId = rec.nodeId ?? rec.workId ?? "";
228294
+ const artifactId = rec.nodeId ?? nodeIdOfWorkId(rec.workId) ?? "";
227647
228295
  return {
227648
228296
  seq: rec.seq,
227649
228297
  at: rec.createdAt,
@@ -227653,7 +228301,11 @@ function eventToAuditEntry(rec) {
227653
228301
  summary: `${KIND_LABEL2[ev.kind] ?? ev.kind}${artifactId ? ` \xB7 ${artifactId}` : ""}`
227654
228302
  };
227655
228303
  }
227656
- function enrichAudit(entry, model, titleOf) {
228304
+ function enrichAudit(entry, model, titleOf, workorderId) {
228305
+ if (!entry.artifactId) {
228306
+ const workspaceTitle2 = workorderId ? titleOf.get(workorderId) : void 0;
228307
+ return { ...entry, ...workspaceTitle2 ? { workspaceTitle: workspaceTitle2 } : {}, visibility: "ok" };
228308
+ }
227657
228309
  const a = model.artifacts.get(entry.artifactId);
227658
228310
  if (!a) return { ...entry, visibility: "deleted" };
227659
228311
  const workspaceTitle = titleOf.get(a.workspace);
@@ -227671,8 +228323,8 @@ async function buildAudit(source, q2 = {}, model) {
227671
228323
  const items = [];
227672
228324
  let lastCursor = null;
227673
228325
  let truncated = false;
227674
- const push2 = (entry, cursor) => {
227675
- items.push(model && titleOf ? enrichAudit(entry, model, titleOf) : entry);
228326
+ const push2 = (entry, cursor, workorderId) => {
228327
+ items.push(model && titleOf ? enrichAudit(entry, model, titleOf, workorderId) : entry);
227676
228328
  lastCursor = cursor;
227677
228329
  };
227678
228330
  const match = (actor, kind, at) => {
@@ -227683,30 +228335,33 @@ async function buildAudit(source, q2 = {}, model) {
227683
228335
  return true;
227684
228336
  };
227685
228337
  if (q2.latest) {
227686
- let collected = [];
228338
+ const collected = [];
227687
228339
  if (source.engineStore) {
227688
228340
  const records = await source.engineStore.transaction((tx) => tx.listEvents(void 0, 0));
227689
228341
  for (const rec of records) {
227690
228342
  const ev = rec.event;
227691
228343
  if (!match(rec.actorId, ev.kind, rec.createdAt)) continue;
227692
- collected.push(eventToAuditEntry(rec));
228344
+ collected.push({ entry: eventToAuditEntry(rec), workorderId: rec.workorderId });
227693
228345
  }
227694
228346
  } else if (source.oplog) {
227695
228347
  const src = source.oplog.readFilteredOps ? source.oplog.readFilteredOps({ ...q2.actor ? { actor: q2.actor } : {}, ...q2.kind ? { kind: q2.kind } : {} }, void 0, { latest: true }) : source.oplog.readAll();
227696
228348
  for await (const { op } of src) {
227697
228349
  if (!match(op.actor, op.kind, op.timestamp)) continue;
227698
- collected.push(toAuditEntry(op));
228350
+ collected.push({ entry: toAuditEntry(op) });
227699
228351
  }
227700
228352
  }
227701
228353
  const tail = collected.slice(-limit).reverse();
227702
- return { items: model && titleOf ? tail.map((e) => enrichAudit(e, model, titleOf)) : tail, cursor: null };
228354
+ return {
228355
+ items: model && titleOf ? tail.map(({ entry, workorderId }) => enrichAudit(entry, model, titleOf, workorderId)) : tail.map(({ entry }) => entry),
228356
+ cursor: null
228357
+ };
227703
228358
  }
227704
228359
  if (source.engineStore) {
227705
228360
  const records = await source.engineStore.transaction((tx) => tx.listEvents(void 0, fromSeq));
227706
228361
  for (const rec of records) {
227707
228362
  const ev = rec.event;
227708
228363
  if (!match(rec.actorId, ev.kind, rec.createdAt)) continue;
227709
- push2(eventToAuditEntry(rec), String(rec.seq));
228364
+ push2(eventToAuditEntry(rec), String(rec.seq), rec.workorderId);
227710
228365
  if (items.length >= limit) {
227711
228366
  truncated = true;
227712
228367
  break;
@@ -227752,6 +228407,30 @@ var init_audit = __esm({
227752
228407
  "workorder.paused": "\u6682\u505C\u6D3E\u53D1",
227753
228408
  "workorder.resumed": "\u6062\u590D\u6D3E\u53D1",
227754
228409
  "workorder.sealed": "\u5C01\u5B58",
228410
+ // 其余新引擎事件 kind——缺标签时 summary 直接露英文 kind(生产实测:`plan.changed` 是量最大的
228411
+ // 一类审计条目,界面上就写着「plan.changed」)。这张表按 EngineEvent 的 kind 全集补齐。
228412
+ "plan.changed": "\u6539\u4EFB\u52A1\u56FE",
228413
+ "plan.add_edge": "\u63A5\u4F9D\u8D56",
228414
+ "plan.delete_edge": "\u65AD\u4F9D\u8D56",
228415
+ "plan.update_review_requirements": "\u6539\u8BC4\u5BA1\u8981\u6C42",
228416
+ "plan.update_fields": "\u6539\u5B57\u6BB5",
228417
+ "work.started": "\u5F00\u5DE5",
228418
+ "work.redispatch": "\u91CD\u6D3E",
228419
+ "work.snapshot_recorded": "\u5B58\u5FEB\u7167",
228420
+ "work.handoff_recorded": "\u8BB0\u4EA4\u63A5",
228421
+ "work.reject": "\u6253\u56DE",
228422
+ "work.content_edited": "\u6539\u5185\u5BB9",
228423
+ "work.conclude": "\u5B9A\u7A3F",
228424
+ "node.latest_proposed.rebased": "\u6362\u57FA\u7EBF",
228425
+ "review.started": "\u5F00\u59CB\u8BC4\u5BA1",
228426
+ "review.kill": "\u64A4\u8BC4\u5BA1",
228427
+ "review.timeout": "\u8BC4\u5BA1\u8D85\u65F6",
228428
+ "issue.resolution_proposed": "\u63D0\u89E3\u51B3\u65B9\u6848",
228429
+ "issue.resolution_rejected": "\u9A73\u56DE\u89E3\u51B3\u65B9\u6848",
228430
+ "workorder.created": "\u5EFA\u5DE5\u5355",
228431
+ "workorder.root_set": "\u5B9A\u6839\u8282\u70B9",
228432
+ "workorder.meta_changed": "\u6539\u5DE5\u5355\u4FE1\u606F",
228433
+ "workorder.ping": "\u5524\u9192",
227755
228434
  // 旧 op kind(oplog 回退路径)
227756
228435
  spawn_artifact: "\u5EFA\u4EA7\u7269",
227757
228436
  propose_revision: "\u63D0\u4FEE\u8BA2",
@@ -229337,7 +230016,15 @@ function collabDomain(opts) {
229337
230016
  }
229338
230017
  const text5 = raw;
229339
230018
  try {
230019
+ const snap = await kernel.getStore().transaction((tx) => tx.loadWorkorder(ws));
230020
+ if (snap?.workorder.dispatchPaused && !isHumanActor(req.auth.actor)) {
230021
+ return { status: 403, body: { error: { code: "forbidden", message: "\u6682\u505C\u671F\u95F4\u5F55\u5165\u5DE5\u5355\u6B63\u6587\u4EC5\u9650\u4EBA\u7C7B\u64CD\u4F5C" } } };
230022
+ }
229340
230023
  const contentRef = await blobs.put(new TextEncoder().encode(text5));
230024
+ if (snap?.workorder.dispatchPaused) {
230025
+ await kernel.recordPausedRevision({ artifactId: rootId, actor: req.auth.actor, contentRef, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
230026
+ return { status: 200, body: { overview: text5, concluded: false } };
230027
+ }
229341
230028
  await kernel.proposeRevision({ artifactId: rootId, actor: req.auth.actor, contentRef, reason: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
229342
230029
  await kernel.advanceQueue(rootId, req.auth.actor);
229343
230030
  const result = await kernel.conclude({ artifactId: rootId, actor: req.auth.actor, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
@@ -229345,7 +230032,7 @@ function collabDomain(opts) {
229345
230032
  } catch (err) {
229346
230033
  if (err instanceof KernelError) {
229347
230034
  return {
229348
- status: 400,
230035
+ status: err.code === "revision-conflict" ? 409 : 400,
229349
230036
  body: { error: { code: err.code ?? "kernel_error", message: err.message } }
229350
230037
  };
229351
230038
  }
@@ -232404,12 +233091,83 @@ function buildCommentThread(input) {
232404
233091
  bubbles
232405
233092
  };
232406
233093
  }
233094
+ function isReviewThreadSessionId(sessionId) {
233095
+ return typeof sessionId === "string" && sessionId.startsWith(REVIEW_THREAD_SESSION_PREFIX);
233096
+ }
233097
+ function partiesOfReviewThreadSessionId(sessionId) {
233098
+ if (!isReviewThreadSessionId(sessionId)) return null;
233099
+ const parts = sessionId.slice(REVIEW_THREAD_SESSION_PREFIX.length).split("|");
233100
+ return parts.length === 2 && parts[0] && parts[1] ? [parts[0], parts[1]] : null;
233101
+ }
233102
+ function buildReviewThread(input) {
233103
+ const parties = partiesOfReviewThreadSessionId(input.sessionId);
233104
+ if (!parties || !input.snap) return null;
233105
+ const [a, b2] = parties;
233106
+ const works = new Map(input.snap.works.map((w2) => [w2.id, w2]));
233107
+ const rows = input.snap.reviews.filter((r) => {
233108
+ const reviewee = works.get(r.targetWorkId)?.assigneeActorId;
233109
+ if (!reviewee || reviewee === r.reviewerActorId) return false;
233110
+ const pair = [r.reviewerActorId, reviewee].sort();
233111
+ return pair[0] === a && pair[1] === b2;
233112
+ });
233113
+ if (rows.length === 0) return null;
233114
+ const { ref: ref2 } = input;
233115
+ const nameOf = (id) => ref2(id).name?.trim() || "\u2014";
233116
+ const mention = (author) => nameOf(author === a ? b2 : a);
233117
+ const bubble = (o) => ({
233118
+ id: o.id,
233119
+ at: o.at,
233120
+ author: ref2(o.author),
233121
+ text: `@${mention(o.author)} ${o.body}`,
233122
+ issueId: o.issueId,
233123
+ kind: o.kind
233124
+ });
233125
+ const bubbles = [];
233126
+ for (const workId of new Set(rows.map((r) => r.targetWorkId))) {
233127
+ const w2 = works.get(workId);
233128
+ const body2 = w2?.conclusion?.trim();
233129
+ if (!w2 || !body2) continue;
233130
+ bubbles.push(bubble({
233131
+ id: `${workId}:delivered`,
233132
+ at: w2.endedAt || w2.createdAt,
233133
+ author: w2.assigneeActorId,
233134
+ body: body2,
233135
+ issueId: workId,
233136
+ kind: "raised"
233137
+ }));
233138
+ }
233139
+ for (const r of rows) {
233140
+ const note = r.note?.trim();
233141
+ if (!note) continue;
233142
+ const label = r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1";
233143
+ bubbles.push(bubble({
233144
+ id: `${r.id}:verdict`,
233145
+ at: r.decidedAt || r.endedAt || r.createdAt,
233146
+ author: r.reviewerActorId,
233147
+ body: `${label}\uFF1A${note}`,
233148
+ issueId: r.targetWorkId,
233149
+ kind: "reply"
233150
+ }));
233151
+ }
233152
+ if (bubbles.length === 0) return null;
233153
+ bubbles.sort((x2, y) => x2.at.localeCompare(y.at) || x2.id.localeCompare(y.id));
233154
+ const first = rows[0];
233155
+ const revieweeId = works.get(first.targetWorkId).assigneeActorId;
233156
+ return {
233157
+ sessionId: input.sessionId,
233158
+ workorderId: input.workorderId,
233159
+ executor: ref2(revieweeId),
233160
+ coordinator: ref2(first.reviewerActorId),
233161
+ bubbles
233162
+ };
233163
+ }
232407
233164
  var init_gap_thread = __esm({
232408
233165
  "../server/src/domains/collab/gap-thread.ts"() {
232409
233166
  "use strict";
232410
233167
  init_src();
232411
233168
  init_workorder_manager();
232412
233169
  init_src();
233170
+ init_src();
232413
233171
  }
232414
233172
  });
232415
233173
 
@@ -233047,12 +233805,12 @@ function createChatSessionsDomain(opts) {
233047
233805
  router.get("/api/workorders/:workorderId/gap-thread", async (req) => {
233048
233806
  const workorderId = req.params["workorderId"];
233049
233807
  const sessionId = req.query.get("session") ?? "";
233050
- if (!isGapThreadSessionId(sessionId) && !isCommentThreadSessionId(sessionId)) {
233808
+ if (!isGapThreadSessionId(sessionId) && !isCommentThreadSessionId(sessionId) && !isReviewThreadSessionId(sessionId)) {
233051
233809
  throw new ApiError(404, "NOT_FOUND", "thread not found");
233052
233810
  }
233053
233811
  const { snap, ref: ref2 } = await gapThreadInputsFor(req, workorderId);
233054
233812
  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 });
233813
+ 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
233814
  if (!view) throw new ApiError(404, "NOT_FOUND", "gap thread not found");
233057
233815
  return { status: 200, body: view };
233058
233816
  });
@@ -237267,6 +238025,11 @@ var init_daemon_adapter = __esm({
237267
238025
  clearTimeout(entry.ackTimer);
237268
238026
  entry.ackTimer = void 0;
237269
238027
  }
238028
+ const reapTimer = this.sessionReapTimers.get(dispatchId);
238029
+ if (reapTimer) {
238030
+ clearTimeout(reapTimer);
238031
+ this.sessionReapTimers.delete(dispatchId);
238032
+ }
237270
238033
  entry.exited = info;
237271
238034
  for (const w2 of entry.appendWaiters.splice(0)) {
237272
238035
  clearTimeout(w2.timer);
@@ -237362,6 +238125,10 @@ var init_daemon_adapter = __esm({
237362
238125
  for (const [dispatchId, entry] of this.pending) {
237363
238126
  if (entry.nodeId !== daemonId || entry.exited || present.has(dispatchId)) continue;
237364
238127
  if (this.hub.hasSession?.(dispatchId)) continue;
238128
+ if (this.sessionReapTimers.has(dispatchId)) {
238129
+ this.log(`[dispatch-delivery] ${dispatchId}\uFF1A\u8282\u70B9 ${daemonId} \u7A7A\u5E93\u5B58\u4E0D\u8986\u76D6\u72EC\u7ACB\u4F1A\u8BDD\u65AD\u7EBF\u5BBD\u9650\uFF0C\u7ED3\u8BBA=unknown\uFF08\u4F9D\u636E\uFF1A\u6570\u636E\u9762\u66FE\u8FDE\u63A5\uFF0C\u7B49\u5F85\u91CD\u8FDE\u6216\u539F ${this.sessionReapGraceMs}ms \u671F\u9650\uFF09`);
238130
+ continue;
238131
+ }
237365
238132
  absent.push(dispatchId);
237366
238133
  }
237367
238134
  for (const dispatchId of absent) {
@@ -237394,8 +238161,7 @@ var init_daemon_adapter = __esm({
237394
238161
  */
237395
238162
  onSessionDown(dispatchId) {
237396
238163
  if (this.sessionReapTimers.has(dispatchId)) return;
237397
- const entry = this.pending.get(dispatchId);
237398
- if (!entry || entry.exited) return;
238164
+ if (this.settledDispatchIds.has(dispatchId)) return;
237399
238165
  const timer = setTimeout(() => {
237400
238166
  this.sessionReapTimers.delete(dispatchId);
237401
238167
  const e = this.pending.get(dispatchId);
@@ -238731,6 +239497,7 @@ ${ctx.nodeFault}
238731
239497
  env: { ...prov?.env ?? {}, OASIS_STAGE: "1", ...workspace ? { OASIS_WORKSPACE: workspace } : {} },
238732
239498
  ...prov?.wrapperPaths && prov.wrapperPaths.length > 0 ? { wrapperPaths: prov.wrapperPaths } : {},
238733
239499
  ...prov?.requiredTools && prov.requiredTools.length > 0 ? { requiredTools: prov.requiredTools } : {},
239500
+ ...prov?.requiredToolVersions && Object.keys(prov.requiredToolVersions).length > 0 ? { requiredToolVersions: prov.requiredToolVersions } : {},
238734
239501
  ...prov?.connectorCreds && prov.connectorCreds.length > 0 ? { connectorCreds: prov.connectorCreds } : {}
238735
239502
  };
238736
239503
  this.deps.log?.(`[coordinator] ${logMsg}`);
@@ -239291,6 +240058,7 @@ var init_postgres_registry = __esm({
239291
240058
  updated_at timestamptz NOT NULL,
239292
240059
  PRIMARY KEY (actor_id, connector_id)
239293
240060
  )`);
240061
+ await pool.query(`ALTER TABLE "${s2}".actor_connector_connections ADD COLUMN IF NOT EXISTS configured_by text`);
239294
240062
  await pool.query(`
239295
240063
  CREATE TABLE IF NOT EXISTS "${s2}".skill_catalog (
239296
240064
  id text PRIMARY KEY, -- slug
@@ -239678,10 +240446,11 @@ var init_postgres_registry = __esm({
239678
240446
  }
239679
240447
  async upsertActorConnectorConnection(c) {
239680
240448
  await this.pool.query(
239681
- `INSERT INTO ${this.s}.actor_connector_connections (actor_id, connector_id, enabled, updated_at)
239682
- VALUES ($1, $2, $3, $4)
239683
- ON CONFLICT (actor_id, connector_id) DO UPDATE SET enabled = $3, updated_at = $4`,
239684
- [c.actorId, c.connectorId, c.enabled, c.updatedAt]
240449
+ `INSERT INTO ${this.s}.actor_connector_connections (actor_id, connector_id, enabled, updated_at, configured_by)
240450
+ VALUES ($1, $2, $3, $4, $5)
240451
+ ON CONFLICT (actor_id, connector_id) DO UPDATE SET enabled = $3, updated_at = $4,
240452
+ configured_by = COALESCE($5, ${this.s}.actor_connector_connections.configured_by)`,
240453
+ [c.actorId, c.connectorId, c.enabled, c.updatedAt, c.configuredBy ?? null]
239685
240454
  );
239686
240455
  }
239687
240456
  async listActorConnectorConnections(actorId) {
@@ -239693,7 +240462,8 @@ var init_postgres_registry = __esm({
239693
240462
  actorId: row.actor_id,
239694
240463
  connectorId: row.connector_id,
239695
240464
  enabled: row.enabled,
239696
- updatedAt: new Date(row.updated_at).toISOString()
240465
+ updatedAt: new Date(row.updated_at).toISOString(),
240466
+ ...row.configured_by ? { configuredBy: row.configured_by } : {}
239697
240467
  }));
239698
240468
  }
239699
240469
  async listAllActorConnectorConnections() {
@@ -239704,7 +240474,8 @@ var init_postgres_registry = __esm({
239704
240474
  actorId: row.actor_id,
239705
240475
  connectorId: row.connector_id,
239706
240476
  enabled: row.enabled,
239707
- updatedAt: new Date(row.updated_at).toISOString()
240477
+ updatedAt: new Date(row.updated_at).toISOString(),
240478
+ ...row.configured_by ? { configuredBy: row.configured_by } : {}
239708
240479
  }));
239709
240480
  }
239710
240481
  async deleteActorConnectorConnection(actorId, connectorId) {
@@ -246157,6 +246928,21 @@ var init_postgres_chat_sessions = __esm({
246157
246928
  );
246158
246929
  return r.rows.map(rowToMessage);
246159
246930
  }
246931
+ /** 按本 schema 中的会话 + run 精确找轮,不能用 activeTurn:已取消的轮已不占 active slot。 */
246932
+ async getTurnForRun(sessionId, runId) {
246933
+ const r = await this.pool.query(
246934
+ `SELECT id, status, completed_at, last_error FROM ${this.s}.chat_session_turns
246935
+ WHERE chat_session_id = $1 AND assistant_run_id = $2 ORDER BY reserved_at DESC LIMIT 1`,
246936
+ [sessionId, runId]
246937
+ );
246938
+ const row = r.rows[0];
246939
+ return row ? {
246940
+ id: row.id,
246941
+ status: row.status,
246942
+ completedAt: row.completed_at == null ? null : new Date(row.completed_at).toISOString(),
246943
+ lastError: row.last_error ?? null
246944
+ } : null;
246945
+ }
246160
246946
  async getSession(id) {
246161
246947
  const r = await this.pool.query(`SELECT * FROM ${this.s}.chat_sessions WHERE id = $1`, [id]);
246162
246948
  return r.rows[0] ? rowToSession(r.rows[0]) : null;
@@ -254703,6 +255489,98 @@ var init_chat_builtin_skills = __esm({
254703
255489
  }
254704
255490
  });
254705
255491
 
255492
+ // ../server/src/governance/connector-tool-versions.ts
255493
+ function connectorToolVersionsFile(dataDir) {
255494
+ return (0, import_node_path25.join)(dataDir, "connector-tools", "versions.json");
255495
+ }
255496
+ function registryBase() {
255497
+ const raw = process.env["npm_config_registry"] || process.env["NPM_CONFIG_REGISTRY"] || "https://registry.npmjs.org";
255498
+ return raw.replace(/\/+$/, "");
255499
+ }
255500
+ async function fetchLatestNpmVersion(pkg) {
255501
+ try {
255502
+ const url = `${registryBase()}/${pkg.split("/").map(encodeURIComponent).join("%2F")}/latest`;
255503
+ const res = await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(NPM_PROBE_TIMEOUT_MS) });
255504
+ if (!res.ok) return null;
255505
+ const v2 = (await res.json()).version;
255506
+ return typeof v2 === "string" && v2 ? v2 : null;
255507
+ } catch {
255508
+ return null;
255509
+ }
255510
+ }
255511
+ async function readRegisteredToolVersions(dataDir) {
255512
+ try {
255513
+ const raw = JSON.parse(await (0, import_promises14.readFile)(connectorToolVersionsFile(dataDir), "utf8"));
255514
+ if (!raw || typeof raw !== "object") return {};
255515
+ const out = {};
255516
+ for (const [k2, v2] of Object.entries(raw)) {
255517
+ if (typeof v2 === "string" && v2) out[k2] = v2;
255518
+ }
255519
+ return out;
255520
+ } catch {
255521
+ return {};
255522
+ }
255523
+ }
255524
+ async function writeRegisteredToolVersions(dataDir, versions) {
255525
+ const file = connectorToolVersionsFile(dataDir);
255526
+ await (0, import_promises14.mkdir)((0, import_node_path25.join)(dataDir, "connector-tools"), { recursive: true });
255527
+ const tmp = `${file}.${process.pid}.tmp`;
255528
+ await (0, import_promises14.writeFile)(tmp, `${JSON.stringify(versions, null, 2)}
255529
+ `, "utf8");
255530
+ await (0, import_promises14.rename)(tmp, file);
255531
+ }
255532
+ async function refreshConnectorToolVersions(opts) {
255533
+ const fetchLatest = opts.fetchLatest ?? fetchLatestNpmVersion;
255534
+ const log3 = opts.log ?? (() => {
255535
+ });
255536
+ const versions = await readRegisteredToolVersions(opts.dataDir);
255537
+ const results = [];
255538
+ let changed = false;
255539
+ for (const [tool, pkg] of Object.entries(NPM_TOOL_PACKAGES)) {
255540
+ const before = versions[tool];
255541
+ const latest = await fetchLatest(pkg);
255542
+ if (!latest) {
255543
+ results.push({ tool, pkg, ...before ? { from: before, to: before } : {}, outcome: "unreachable" });
255544
+ log3(`[connector-tools] ${pkg} \u95EE\u4E0D\u5230\u6700\u65B0\u7248\uFF0C\u6CBF\u7528\u767B\u8BB0\u7248\u672C ${before ?? "\uFF08\u65E0\uFF09"}`);
255545
+ continue;
255546
+ }
255547
+ if (before === latest) {
255548
+ results.push({ tool, pkg, from: before, to: latest, outcome: "unchanged" });
255549
+ continue;
255550
+ }
255551
+ versions[tool] = latest;
255552
+ changed = true;
255553
+ results.push({ tool, pkg, ...before ? { from: before } : {}, to: latest, outcome: "updated" });
255554
+ log3(`[connector-tools] ${pkg} \u767B\u8BB0\u7248\u672C ${before ?? "\uFF08\u65E0\uFF09"} \u2192 ${latest}`);
255555
+ }
255556
+ if (changed) {
255557
+ try {
255558
+ await writeRegisteredToolVersions(opts.dataDir, versions);
255559
+ } catch (err) {
255560
+ log3(`[connector-tools] \u767B\u8BB0\u7248\u672C\u843D\u76D8\u5931\u8D25\uFF08\u672C\u8FDB\u7A0B\u5185\u4ECD\u751F\u6548\uFF09\uFF1A${String(err)}`);
255561
+ }
255562
+ }
255563
+ return { versions, results };
255564
+ }
255565
+ function pickToolVersions(registered, tools) {
255566
+ const out = {};
255567
+ for (const t of tools) {
255568
+ const v2 = registered[t];
255569
+ if (v2) out[t] = v2;
255570
+ }
255571
+ return out;
255572
+ }
255573
+ var import_promises14, import_node_path25, NPM_PROBE_TIMEOUT_MS;
255574
+ var init_connector_tool_versions = __esm({
255575
+ "../server/src/governance/connector-tool-versions.ts"() {
255576
+ "use strict";
255577
+ import_promises14 = require("node:fs/promises");
255578
+ import_node_path25 = require("node:path");
255579
+ init_src8();
255580
+ NPM_PROBE_TIMEOUT_MS = 3e3;
255581
+ }
255582
+ });
255583
+
254706
255584
  // ../server/src/design/board-watch.ts
254707
255585
  function decideBoardLetter(c, nowMs, quietMs) {
254708
255586
  if (c.hasOpenWatchLetter) return false;
@@ -255498,6 +256376,7 @@ var init_src11 = __esm({
255498
256376
  init_builtin_skills();
255499
256377
  init_chat_builtin_skills();
255500
256378
  init_connector_skills();
256379
+ init_connector_tool_versions();
255501
256380
  init_board_watch();
255502
256381
  init_ephemeral_project();
255503
256382
  init_run_settlement();
@@ -256360,6 +257239,7 @@ function materializeBuiltins(builtins, runtimeKind, opts) {
256360
257239
  var CONNECTOR_SKILL_SOURCES = [
256361
257240
  { connectorId: "feishu", baseUrl: FEISHU_WELL_KNOWN_SKILLS_BASE }
256362
257241
  ];
257242
+ var registeredToolVersions = {};
256363
257243
  async function readAllConnectorSkills(dataDir) {
256364
257244
  const out = [];
256365
257245
  for (const src of CONNECTOR_SKILL_SOURCES) {
@@ -256391,11 +257271,18 @@ async function refreshConnectorSkillsNow(dataDir, apply) {
256391
257271
  }
256392
257272
  }
256393
257273
  if (out.length > 0) apply(out);
257274
+ try {
257275
+ const { versions } = await refreshConnectorToolVersions({ dataDir, log: (m2) => console.log(m2) });
257276
+ registeredToolVersions = versions;
257277
+ } catch (err) {
257278
+ console.warn(`[serve] connector CLI \u767B\u8BB0\u7248\u672C\u5237\u65B0\u5931\u8D25\uFF0C\u6CBF\u7528\u4E0A\u4E00\u4EFD\uFF1A${String(err)}`);
257279
+ }
256394
257280
  return results;
256395
257281
  }
256396
257282
  function refreshConnectorSkillsInBackground(dataDir, apply) {
256397
257283
  void refreshConnectorSkillsNow(dataDir, apply);
256398
257284
  }
257285
+ var CONNECTOR_SKILL_REFRESH_MS = 24 * 60 * 6e4;
256399
257286
  var SESSION_TOKEN_GRACE_MS = 15 * 6e4;
256400
257287
  function makeResumeRetryProxy(first, spawnFallback) {
256401
257288
  const relay = () => {
@@ -256589,6 +257476,8 @@ async function buildActorProvision(service, actorId, scope, onBrokerRefused) {
256589
257476
  env,
256590
257477
  wrapperPaths: [oasisWrapperScript(), ...provision.wrapperPaths],
256591
257478
  requiredTools: provision.requiredTools,
257479
+ // 只挑本次真要用的那几个:job 里不塞无关工具的版本号。
257480
+ requiredToolVersions: pickToolVersions(registeredToolVersions, provision.requiredTools),
256592
257481
  connectorCreds: provision.connectorCreds,
256593
257482
  cleanup: provision.cleanup
256594
257483
  };
@@ -256992,6 +257881,7 @@ async function startServe(opts) {
256992
257881
  if (!cid || cid === defaultCompanyId) return "";
256993
257882
  return ` [${cid}]`;
256994
257883
  };
257884
+ const engineSchemaFor = (companyId) => companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId);
256995
257885
  let getEngine = async () => {
256996
257886
  throw new Error("company engine router not initialized");
256997
257887
  };
@@ -257097,7 +257987,7 @@ async function startServe(opts) {
257097
257987
  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
257988
  nodeStore = await PostgresNodeStore.open(pgPool, sharedSchema);
257099
257989
  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`);
257990
+ 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
257991
  } else {
257102
257992
  projectStateStore = await FileProjectStateStore.open(path29.join(opts.dir, "artifact-document-projects.json"));
257103
257993
  artifactStateStore = await FileArtifactStateStore.open(path29.join(opts.dir, "artifact-document-state.json"));
@@ -257108,7 +257998,6 @@ async function startServe(opts) {
257108
257998
  console.log("[serve] \u8282\u70B9/\u8FD0\u884C\u65F6\u4F53\u7CFB\uFF1A\u672C\u5730 JSON dev store");
257109
257999
  }
257110
258000
  const chatRecoveryStore = platformChatRecovery ?? chatSessionStore;
257111
- const chatRecoveryItems = pgPool ? await PostgresChatItemStore.open(pgPool, defaultCompanySchema) : void 0;
257112
258001
  const knowledgeConfigStore = await FileKnowledgeConfigStore.open(path29.join(opts.dir, "knowledge.json"));
257113
258002
  const knowledgeRunStore = await FileKnowledgeRunStore.open(path29.join(opts.dir, "knowledge-runs.json"));
257114
258003
  const knowledgeDeploymentId = await loadOrCreateKnowledgeDeploymentId(path29.join(opts.dir, "knowledge-deployment-id"));
@@ -257777,10 +258666,17 @@ async function startServe(opts) {
257777
258666
  });
257778
258667
  console.log(`[serve] loaded ${builtinSkills.length} builtin skill(s): ${builtinSkills.map((s2) => s2.id).join(", ") || "(none)"}`);
257779
258668
  connectorSkills = await readAllConnectorSkills(opts.dir);
258669
+ registeredToolVersions = await readRegisteredToolVersions(opts.dir);
257780
258670
  console.log(`[serve] connector skills\uFF08\u843D\u76D8\u526F\u672C\uFF09\uFF1A${connectorSkills.length} \u4E2A`);
257781
258671
  refreshConnectorSkillsInBackground(opts.dir, (s2) => {
257782
258672
  connectorSkills = s2;
257783
258673
  });
258674
+ const connectorSkillTimer = setInterval(() => {
258675
+ refreshConnectorSkillsInBackground(opts.dir, (s2) => {
258676
+ connectorSkills = s2;
258677
+ });
258678
+ }, CONNECTOR_SKILL_REFRESH_MS);
258679
+ connectorSkillTimer.unref?.();
257784
258680
  if (defaultCompanyMigration.createdCompany) {
257785
258681
  const seededAt = (/* @__PURE__ */ new Date()).toISOString();
257786
258682
  for (const devActor of ["actor:human:yx"]) {
@@ -257813,7 +258709,7 @@ async function startServe(opts) {
257813
258709
  id: c.id,
257814
258710
  // 默认公司的引擎表仍在共享 schema(ADR 0164 §D5 的 RP0,搬迁另立单)——这条映射
257815
258711
  // 搞错,默认公司的工单会一张都对不上,回填会把它们全判成「无证据」。
257816
- engineSchema: c.id === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(c.id)
258712
+ engineSchema: engineSchemaFor(c.id)
257817
258713
  }));
257818
258714
  const res = await backfillTenancyColumns(tenancyPool, { schema: defaultCompanySchema, companies: companies2, apply: true });
257819
258715
  const wrote = res.rows.reduce((n, r) => n + r.updated, 0);
@@ -257988,28 +258884,28 @@ async function startServe(opts) {
257988
258884
  const assistants2 = await PostgresAssistantBindStore.open(pgPool, loc.schemaName, { companyId });
257989
258885
  const humanPrefs2 = await PostgresHumanPrefsStore.open(pgPool, loc.schemaName);
257990
258886
  const ownedReadMarkers2 = await PostgresReadMarkerStore.open(pgPool, loc.schemaName, {
257991
- chatSessionsSchema: companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258887
+ chatSessionsSchema: engineSchemaFor(companyId)
257992
258888
  });
257993
258889
  const ownedMemory2 = await PostgresActorMemoryStore.open(pgPool, loc.schemaName);
257994
258890
  const ownedDrafts2 = await PostgresWorkorderDraftStore.open(
257995
258891
  pgPool,
257996
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258892
+ engineSchemaFor(companyId)
257997
258893
  );
257998
258894
  const ownedPlannerIssues2 = await PostgresWorkorderDraftPlannerIssuesStore.open(
257999
258895
  pgPool,
258000
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258896
+ engineSchemaFor(companyId)
258001
258897
  );
258002
258898
  const ownedLedger2 = await PostgresDispatchStore.open(
258003
258899
  pgPool,
258004
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258900
+ engineSchemaFor(companyId)
258005
258901
  );
258006
258902
  const ownedBlobs2 = await PostgresBlobStore.open(
258007
258903
  pgPool,
258008
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258904
+ engineSchemaFor(companyId)
258009
258905
  );
258010
258906
  const ownedTrace2 = await PostgresTraceStore.open(
258011
258907
  tracePool,
258012
- companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId)
258908
+ engineSchemaFor(companyId)
258013
258909
  );
258014
258910
  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
258911
  }
@@ -258265,7 +259161,7 @@ async function startServe(opts) {
258265
259161
  const id = companyId ?? defaultCompanyId;
258266
259162
  const hit = delegationStores.get(id);
258267
259163
  if (hit) return hit;
258268
- const schemaFor = id === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(id);
259164
+ const schemaFor = engineSchemaFor(id);
258269
259165
  const store = pgPool ? await PostgresDelegationStore.open(pgPool, schemaFor) : await FileDelegationStore.open(path29.join(opts.dir, `delegations-${companyEngineDirName(id)}.json`));
258270
259166
  delegationStores.set(id, store);
258271
259167
  return store;
@@ -258516,7 +259412,7 @@ async function startServe(opts) {
258516
259412
  const knowledgeGovernanceStoreFor = (companyId) => {
258517
259413
  const cached3 = knowledgeGovernanceStores.get(companyId);
258518
259414
  if (cached3) return cached3;
258519
- const knowledgeSchema = companyId === defaultCompanyId ? defaultCompanySchema : companyEngineDirName(companyId);
259415
+ const knowledgeSchema = engineSchemaFor(companyId);
258520
259416
  const opened = pgDsn && pgPool ? withSchemaDdlLock(pgPool, knowledgeSchema, () => PostgresKnowledgeGovernanceStore.open(pgPool, knowledgeSchema, knowledgeSnapshotKey)) : FileKnowledgeGovernanceStore.open(path29.join(
258521
259417
  opts.dir,
258522
259418
  companyId === defaultCompanyId ? "knowledge-governance.json" : path29.join("companies", companyEngineDirName(companyId), "knowledge-governance.json")
@@ -258650,6 +259546,7 @@ async function startServe(opts) {
258650
259546
  env: provision.env,
258651
259547
  wrapperPaths: provision.wrapperPaths,
258652
259548
  ...provision.requiredTools.length > 0 ? { requiredTools: provision.requiredTools } : {},
259549
+ ...Object.keys(provision.requiredToolVersions).length > 0 ? { requiredToolVersions: provision.requiredToolVersions } : {},
258653
259550
  ...provision.connectorCreds.length > 0 ? { connectorCreds: provision.connectorCreds } : {}
258654
259551
  });
258655
259552
  const chunks = [];
@@ -259635,7 +260532,7 @@ async function startServe(opts) {
259635
260532
  console.warn(`[trace] chat run ${traceRunId} \u7EED\u8D26\u5F02\u5E38: ${String(err)}`);
259636
260533
  });
259637
260534
  };
259638
- const provision = isolatedKnowledgeTurn ? { env: {}, wrapperPaths: [], requiredTools: [], connectorCreds: [], cleanup: async () => void 0 } : await buildActorProvision(actorService, actorId);
260535
+ const provision = isolatedKnowledgeTurn ? { env: {}, wrapperPaths: [], requiredTools: [], requiredToolVersions: {}, connectorCreds: [], cleanup: async () => void 0 } : await buildActorProvision(actorService, actorId);
259639
260536
  const actorCtx = await buildActorContext(actorService, actorId).catch(() => null);
259640
260537
  const chatResolvedModel = sessionModelOverride ?? actorCtx?.config?.model ?? void 0;
259641
260538
  const chatModel = chatResolvedModel ?? await registryStore.getRuntimeConfig(`runtime:${binding.nodeId}:${binding.runtimeKind}`).then((c) => c?.model).catch(() => void 0) ?? void 0;
@@ -259775,6 +260672,7 @@ async function startServe(opts) {
259775
260672
  },
259776
260673
  wrapperPaths: provision.wrapperPaths,
259777
260674
  ...provision.requiredTools.length > 0 ? { requiredTools: provision.requiredTools } : {},
260675
+ ...Object.keys(provision.requiredToolVersions).length > 0 ? { requiredToolVersions: provision.requiredToolVersions } : {},
259778
260676
  ...provision.connectorCreds.length > 0 ? { connectorCreds: provision.connectorCreds } : {},
259779
260677
  ...requiredConnectors.length > 0 ? { requiredConnectorSlugs: requiredConnectors } : {}
259780
260678
  });
@@ -259991,60 +260889,101 @@ async function startServe(opts) {
259991
260889
  });
259992
260890
  };
259993
260891
  {
259994
- const listRunningChatRows = chatRecoveryStore.listRunningAssistantMessages?.bind(chatRecoveryStore);
259995
- if (listRunningChatRows) {
260892
+ const defaultListRunningChatRows = chatRecoveryStore.listRunningAssistantMessages?.bind(chatRecoveryStore);
260893
+ if (defaultListRunningChatRows) {
260894
+ const chatRecoveryCompanies = async () => {
260895
+ if (!chatStoreRouter) return [void 0];
260896
+ const active = (await controlPlaneStore.listCompanies()).filter((c) => c.status === "active").map((c) => c.id);
260897
+ return [defaultCompanyId, ...active.filter((id) => id !== defaultCompanyId)];
260898
+ };
260899
+ const chatRecoveryPlanes = /* @__PURE__ */ new Map();
260900
+ const chatRecoveryFor = async (companyId) => {
260901
+ if (!pgPool || !platformChatRecovery) {
260902
+ return { listRunningAssistantMessages: defaultListRunningChatRows, chatStore: chatSessionStore };
260903
+ }
260904
+ const id = companyId ?? defaultCompanyId;
260905
+ const hit = chatRecoveryPlanes.get(id);
260906
+ if (hit) return hit;
260907
+ const sessions = id === defaultCompanyId ? platformChatRecovery : PlatformChatRecoveryStore.open(pgPool, engineSchemaFor(id));
260908
+ const items = await chatItemsFor(id);
260909
+ const plane = {
260910
+ listRunningAssistantMessages: () => sessions.listRunningAssistantMessages(),
260911
+ chatStore: sessions,
260912
+ getTurnForRun: (sid, rid) => sessions.getTurnForRun(sid, rid),
260913
+ ...items ? { items } : {}
260914
+ };
260915
+ chatRecoveryPlanes.set(id, plane);
260916
+ return plane;
260917
+ };
260918
+ const chatRecoveryLastError = /* @__PURE__ */ new Map();
260919
+ const chatRecoveryCompanyWarn = (label) => (companyId, err) => {
260920
+ const key = `${label}::${companyId ?? "<default>"}`;
260921
+ const msg = String(err);
260922
+ if (chatRecoveryLastError.get(key) === msg) return;
260923
+ chatRecoveryLastError.set(key, msg);
260924
+ 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}`);
260925
+ };
259996
260926
  let chatRecoveryChain = Promise.resolve();
259997
260927
  const enqueueChatRecovery = (label, step) => {
259998
260928
  chatRecoveryChain = chatRecoveryChain.then(step).catch((err) => console.error(`[chat-recovery] ${label}\u5931\u8D25: ${String(err)}`));
259999
260929
  };
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
- };
260930
+ const recoverChatTurns = async (daemonId, activeSessions, opts2) => recoverChatTurnsAcrossCompanies({
260931
+ activeSessions,
260932
+ listCompanies: chatRecoveryCompanies,
260933
+ storesFor: chatRecoveryFor,
260934
+ getRun: (id) => fanOutTrace.getRun(id),
260935
+ isWired: (artifactId) => chatLiveSessions.has(`chat::${artifactId}`),
260936
+ ...opts2.stopOnFirstHit ? { stopOnFirstHit: true } : {},
260937
+ onCompanyError: chatRecoveryCompanyWarn(opts2.label),
260938
+ liveChat,
260939
+ trace: runRoutedTrace,
260940
+ traceHealth,
260941
+ recover: (plan) => chatRemoteAdapter.recover(plan.dispatchId, daemonId, plan.runtimeKind, plan.actor),
260942
+ register: (plan, handle) => chatLiveSessions.set(`chat::${plan.artifactId}`, {
260943
+ artifactId: plan.artifactId,
260944
+ sessionId: plan.runId,
260945
+ dispatchId: plan.dispatchId,
260946
+ kill: () => handle.kill()
260947
+ }),
260948
+ unregister: (plan) => {
260949
+ chatLiveSessions.delete(`chat::${plan.artifactId}`);
260950
+ },
260951
+ evict: (dispatchId) => {
260952
+ if (!hub.evictSession(dispatchId, "reaped")) hub.dispatch(daemonId, { type: "kill", dispatchId });
260953
+ },
260954
+ log: (message) => console.log(message)
260955
+ });
260034
260956
  hub.registerSessionClaimResolver?.(async (id) => {
260035
260957
  if (!id.jobArtifactId || !chatRemoteAdapter) return "unknown";
260036
260958
  const jobKey = `chat::${id.jobArtifactId}`;
260037
- if (chatLiveSessions.has(jobKey)) return "live";
260959
+ if (chatLiveSessions.has(jobKey)) {
260960
+ console.log(
260961
+ `[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`
260962
+ );
260963
+ return "live";
260964
+ }
260965
+ let outcome;
260038
260966
  await new Promise((resolve10) => {
260039
- chatRecoveryChain = chatRecoveryChain.then(() => recoverChatTurns(id.nodeId, [
260040
- { dispatchId: id.dispatchId, sessionId: id.sessionId, jobArtifactId: id.jobArtifactId }
260041
- ])).catch((err) => {
260967
+ chatRecoveryChain = chatRecoveryChain.then(async () => {
260968
+ outcome = await recoverChatTurns(
260969
+ id.nodeId,
260970
+ [{ dispatchId: id.dispatchId, sessionId: id.sessionId, jobArtifactId: id.jobArtifactId }],
260971
+ { label: "\u8BA4\u9886", stopOnFirstHit: true }
260972
+ );
260973
+ }).catch((err) => {
260042
260974
  console.error(`[chat-recovery] session_hello \u8BA4\u9886\u5931\u8D25: ${String(err)}`);
260043
260975
  }).then(() => {
260044
260976
  resolve10();
260045
260977
  });
260046
260978
  });
260047
- return chatLiveSessions.has(jobKey) ? "live" : "unknown";
260979
+ if (outcome?.settled.some((hit2) => hit2.plan.dispatchId === id.dispatchId)) return "settled";
260980
+ const live = chatLiveSessions.has(jobKey);
260981
+ const hit = outcome?.recovered.find((r) => r.plan.artifactId === id.jobArtifactId);
260982
+ const skipped = outcome?.skipped ?? [];
260983
+ console.log(
260984
+ `[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")
260985
+ );
260986
+ return live ? "live" : "unknown";
260048
260987
  });
260049
260988
  hub.addMessageListener((daemonId, msg) => {
260050
260989
  if (msg.type !== "runtime_models") return;
@@ -260089,7 +261028,14 @@ async function startServe(opts) {
260089
261028
  });
260090
261029
  hub.addMessageListener((daemonId, msg) => {
260091
261030
  if (msg.type === "hello" && msg.meta.activeSessions?.length) {
260092
- enqueueChatRecovery("hello \u5BF9\u8D26", () => recoverChatTurns(daemonId, msg.meta.activeSessions));
261031
+ enqueueChatRecovery("hello \u5BF9\u8D26", async () => {
261032
+ const outcome = await recoverChatTurns(daemonId, msg.meta.activeSessions, { label: "hello \u5BF9\u8D26" });
261033
+ if (outcome.recovered.length === 0 && outcome.skipped.length === 0) return;
261034
+ const byCompany = outcome.recovered.map((r) => `${r.companyId ?? "<default>"}:${r.plan.chatSessionId}`).join("\u3001");
261035
+ console.log(
261036
+ `[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` : "")
261037
+ );
261038
+ });
260093
261039
  } else if (msg.type === "session_started") {
260094
261040
  void ledgerForDispatch(msg.dispatchId).then((l) => l.markStarted(msg.dispatchId, { sessionRef: msg.sessionId, daemonId })).catch(ledgerWarn("markStarted"));
260095
261041
  const prodMapping = dispatchToWork.get(msg.dispatchId);
@@ -260135,16 +261081,16 @@ async function startServe(opts) {
260135
261081
  })).catch(ledgerWarn("close(exit)"));
260136
261082
  const dispatchOwnedAtReceipt = allDispatchers().some((d) => d.inFlightSessions().some((x2) => x2.sessionId === msg.dispatchId));
260137
261083
  enqueueChatRecovery("exit \u5BF9\u8D26", async () => {
260138
- const chatHandled = await reconcileChatExitFrame({
261084
+ const chatHandled = await reconcileChatExitFrameAcrossCompanies({
260139
261085
  dispatchId: msg.dispatchId,
260140
261086
  info: msg.info,
260141
261087
  isOwned: (id) => [...chatLiveSessions.values()].some((c) => c.dispatchId === id),
260142
261088
  ownedRunId: (sid) => liveChat.runFor(sid),
260143
- listRunningAssistantMessages: listRunningChatRows,
260144
261089
  trace: runRoutedTrace,
260145
- chatStore: chatRecoveryStore,
261090
+ listCompanies: chatRecoveryCompanies,
261091
+ storesFor: chatRecoveryFor,
261092
+ onCompanyError: chatRecoveryCompanyWarn("exit \u5BF9\u8D26"),
260146
261093
  drainStash: (id) => chatRemoteAdapter?.drainStash(id) ?? { frames: [] },
260147
- ...chatRecoveryItems ? { items: chatRecoveryItems } : {},
260148
261094
  log: (m2) => console.log(m2)
260149
261095
  });
260150
261096
  if (!chatHandled) {
@@ -260208,21 +261154,22 @@ async function startServe(opts) {
260208
261154
  }
260209
261155
  });
260210
261156
  const chatSweepTimer = setInterval(() => {
260211
- void sweepDanglingChatTurns({
260212
- listRunningAssistantMessages: listRunningChatRows,
261157
+ void sweepDanglingChatTurnsAcrossCompanies({
261158
+ listCompanies: chatRecoveryCompanies,
261159
+ storesFor: chatRecoveryFor,
261160
+ onCompanyError: chatRecoveryCompanyWarn("chat-sweep"),
260213
261161
  getRun: (id) => fanOutTrace.getRun(id),
260214
261162
  ownedRunId: (sid) => liveChat.runFor(sid),
260215
- listMessages: (sid) => chatRecoveryStore.listMessages(sid),
260216
- updateMessage: chatRecoveryStore.updateMessage.bind(chatRecoveryStore),
260217
261163
  now: Date.now(),
260218
261164
  // 提案 §6.2.9:委派台账**接进这一拍,不新写一套扫描**。
260219
261165
  // 这一拍闭合的是「run 已终态、assistant 行却还挂 running」的行;委派台账是同一类账,
260220
261166
  // 只是多一层——它还得回流。不接的话那条委派会永远显示「进行中」(刷新也不变),
260221
261167
  // 且「队列非空 ⇒ 不算完成」变成死锁:既看不到结果、也等不到失败。
260222
- // ⚠️ **必须带公司**:这一拍是进程级维护路径(跨公司扫本 schema 所有 running 行),
260223
- // 而委派台账按公司分 store。不带的话非默认公司那条委派会被拿去默认公司的库里查、
260224
- // 查不到、于是永远收敛不掉——症状是「刷新也不变的进行中」,且只在非默认公司出现。
260225
- // 反查那一步收在 `delegationSweepHook` 里(有它自己的测试),这里不再手写。
261168
+ // ⚠️ **必须带公司**:委派台账按公司分 store,不带的话非默认公司那条委派会被拿去默认公司的
261169
+ // 库里查、查不到、于是永远收敛不掉——症状是「刷新也不变的进行中」,且只在非默认公司出现。
261170
+ // 扇出之后**公司是已知量**(这一行就是从那家的 schema 读出来的),由扇出层作为第四个参数
261171
+ // 直接传给钩子,不再按会话反查一次——少一次点查,也少一条「读不到就按默认公司试一次」的
261172
+ // 错误回落。回落那一跳仍收在 `delegationSweepHook` 里(dev 文件模式还需要它,有自己的测试)。
260226
261173
  onSessionClosed: delegationSweepHook({
260227
261174
  sessions: chatRecoveryStore,
260228
261175
  settle: (sid, reason, companyId) => server.settleDelegationFromSweep(sid, reason, companyId),
@@ -260251,6 +261198,20 @@ async function startServe(opts) {
260251
261198
  isOwned: (turn) => Boolean(turn.assistantRunId && liveChat.runFor(turn.chatSessionId) === turn.assistantRunId),
260252
261199
  now: Date.now(),
260253
261200
  noRunGraceMs: chatTurnOrphanGraceMs,
261201
+ /* 轮次被判死之后把委派台账也收敛掉(2026-09-09 生产事故的第二道口子)。
261202
+ 此前只有 `sweepDanglingChatTurns` 那一拍挂了委派钩子,而它要先拿 assistant 行的 `run_id`
261203
+ 去查 run —— 子会话占位行的 `run_id` 一直是空的,那一拍整条跳过、钩子一次没调用:
261204
+ 三条委派的轮次 12:27–12:35 就 failed 了,台账到 13:10 还是 running,父会话永远"处理中"。
261205
+ 本拍是从轮次账本走的(`assistant_run_id` 一直有值),是"这一轮死没死"的权威来源。
261206
+ `companyId` 由扇出那层填好 —— 台账按公司分 store,少了它会查到默认公司的库。
261207
+ 非委派会话调进去是空转:`settleFromSweep` 按 childSessionId 查不到记录就原样返回。 */
261208
+ onTurnSettled: async ({ chatSessionId, status, reason, companyId }) => {
261209
+ await server.settleDelegationFromSweep(
261210
+ chatSessionId,
261211
+ `\u5B50\u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u88AB\u6062\u590D\u5668\u6536\u53E3\u4E3A ${status}\uFF08${reason}\uFF09`,
261212
+ companyId
261213
+ );
261214
+ },
260254
261215
  log: (m2) => console.log(m2),
260255
261216
  onCompanyError: (companyId, err) => {
260256
261217
  const key = companyId ?? "<default>";
@@ -260724,14 +261685,22 @@ async function startServe(opts) {
260724
261685
  //(activate.ts:`latestAcceptId == null && !lastProposedRejected`),本模块拿不到,
260725
261686
  // 而两个方向的代价不对称——该续没续只是少一点上下文,不该续却续了就把返工要隔离的
260726
261687
  // 旧会话又接了回来。何况这条 work 从来没跑过,本就没有属于它的会话可续。
260727
- redispatch: (workId, workorderId) => engineIO.dispatchWork(workId, { workorderId, resumeEligible: false }),
261688
+ // 补派进入同一工单事件队列:与 pause apply/effect 串行,不能在检查后绕到 IO 直派。
261689
+ redispatch: async (workId, workorderId) => {
261690
+ await newKernel.getBus().submit({
261691
+ companyId,
261692
+ workorderId,
261693
+ actorId: SYSTEM_ACTOR2,
261694
+ event: { kind: "work.redispatch", workorderId, workId }
261695
+ });
261696
+ },
260728
261697
  now: Date.now(),
260729
261698
  graceMs: DISPATCH_DROP_GRACE_MS,
260730
261699
  backoffMs: DISPATCH_DROP_BACKOFF_MS,
260731
261700
  maxPerTick: DISPATCH_DROP_MAX_PER_TICK
260732
261701
  }).then((r) => {
260733
261702
  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(", ")}`);
261703
+ console.warn(`[dispatch-drop] \u5DF2\u8BF7\u6C42\u8865\u6D3E ${r.redispatched.length} \u6761\u4ECE\u672A\u5F00\u5DE5\u7684 work\uFF1A${r.redispatched.join(", ")}`);
260735
261704
  }
260736
261705
  }).catch((err) => console.error(`[dispatch-drop] tick \u5931\u8D25: ${String(err)}`));
260737
261706
  }, DISPATCH_DROP_SWEEP_TICK_MS);
@@ -262325,6 +263294,7 @@ ${nodeFault}` : "");
262325
263294
  if (gitTimer) clearInterval(gitTimer);
262326
263295
  if (coordTimer) clearInterval(coordTimer);
262327
263296
  if (livenessTimer) clearInterval(livenessTimer);
263297
+ clearInterval(connectorSkillTimer);
262328
263298
  if (orphanSweepTimer) clearInterval(orphanSweepTimer);
262329
263299
  for (const t of dropSweepTimers) clearInterval(t);
262330
263300
  if (deadlineClockTimer) clearInterval(deadlineClockTimer);
@@ -262523,7 +263493,7 @@ async function runSession(dispatchId, job, deps) {
262523
263493
  }
262524
263494
  }
262525
263495
  if (job.requiredTools?.length) {
262526
- const missing = await ensureConnectorTools(job.requiredTools);
263496
+ const missing = await ensureConnectorTools(job.requiredTools, { versions: job.requiredToolVersions });
262527
263497
  if (missing.length) log2("[node-cli]", ` connector tools still missing (agent may fail): ${missing.join(", ")}`);
262528
263498
  }
262529
263499
  const prepared = await prepareConnectorsForJob(job);
@@ -262832,17 +263802,19 @@ var SessionProcessState = class {
262832
263802
  }
262833
263803
  let done = false;
262834
263804
  const finish = () => {
262835
- if (!done) {
262836
- done = true;
262837
- resolve10();
262838
- }
263805
+ if (done) return;
263806
+ done = true;
263807
+ clearTimeout(timer);
263808
+ this.exitAckResolve = null;
263809
+ resolve10();
262839
263810
  };
263811
+ const timer = setTimeout(finish, EXIT_ACK_WAIT_MS);
262840
263812
  this.exitAckResolve = finish;
262841
- setTimeout(finish, EXIT_ACK_WAIT_MS).unref?.();
262842
263813
  });
262843
263814
  }
262844
263815
  teardown() {
262845
263816
  this.stopping = true;
263817
+ this.exitAckResolve?.();
262846
263818
  if (this.watchdog) {
262847
263819
  clearInterval(this.watchdog);
262848
263820
  this.watchdog = null;
@@ -262924,7 +263896,7 @@ function detectRuntimes() {
262924
263896
 
262925
263897
  // ../cli/src/daemon/workdir-handler.ts
262926
263898
  var import_node_fs19 = __toESM(require("node:fs"), 1);
262927
- var import_node_path25 = __toESM(require("node:path"), 1);
263899
+ var import_node_path26 = __toESM(require("node:path"), 1);
262928
263900
  init_src6();
262929
263901
  init_src();
262930
263902
  var WORKDIR_READ_MAX_BYTES2 = 2 * 1024 * 1024;
@@ -262943,10 +263915,10 @@ function isSensitiveSegment(segment) {
262943
263915
  }
262944
263916
  function relPathIsSensitive(rel) {
262945
263917
  if (!rel) return false;
262946
- return rel.split(import_node_path25.default.sep).some((seg) => seg.length > 0 && isSensitiveSegment(seg));
263918
+ return rel.split(import_node_path26.default.sep).some((seg) => seg.length > 0 && isSensitiveSegment(seg));
262947
263919
  }
262948
263920
  function withinBase(p2, base) {
262949
- return p2 === base || p2.startsWith(base + import_node_path25.default.sep);
263921
+ return p2 === base || p2.startsWith(base + import_node_path26.default.sep);
262950
263922
  }
262951
263923
  function normalizeRel(raw) {
262952
263924
  const trimmed = (raw ?? "").trim();
@@ -262963,7 +263935,7 @@ async function trustedCanonicalContainer(req, logicalBase, dirKind) {
262963
263935
  } catch {
262964
263936
  return null;
262965
263937
  }
262966
- return isLegacy ? import_node_path25.default.join(trustedRootReal, "oasis-chat-sessions") : import_node_path25.default.join(trustedRootReal, "sessions", dirKind);
263938
+ return isLegacy ? import_node_path26.default.join(trustedRootReal, "oasis-chat-sessions") : import_node_path26.default.join(trustedRootReal, "sessions", dirKind);
262967
263939
  }
262968
263940
  async function resolveWithinWorkdir(req) {
262969
263941
  const dirKind = sessionDirKind(req.runtimeKind);
@@ -262981,11 +263953,11 @@ async function resolveWithinWorkdir(req) {
262981
263953
  } catch {
262982
263954
  return { ok: false, code: "NOT_FOUND" };
262983
263955
  }
262984
- if (import_node_path25.default.dirname(base) !== canonicalContainer) return { ok: false, code: "PATH_ESCAPE" };
263956
+ if (import_node_path26.default.dirname(base) !== canonicalContainer) return { ok: false, code: "PATH_ESCAPE" };
262985
263957
  const rel = normalizeRel(req.path);
262986
- const requested = import_node_path25.default.resolve(base, rel);
263958
+ const requested = import_node_path26.default.resolve(base, rel);
262987
263959
  if (!withinBase(requested, base)) return { ok: false, code: "PATH_ESCAPE" };
262988
- const cleanRel = base === requested ? "" : import_node_path25.default.relative(base, requested);
263960
+ const cleanRel = base === requested ? "" : import_node_path26.default.relative(base, requested);
262989
263961
  if (relPathIsSensitive(cleanRel)) return { ok: false, code: "SENSITIVE" };
262990
263962
  let real;
262991
263963
  try {
@@ -262995,7 +263967,7 @@ async function resolveWithinWorkdir(req) {
262995
263967
  return { ok: false, code: "PATH_ESCAPE" };
262996
263968
  }
262997
263969
  if (!withinBase(real, base)) return { ok: false, code: "PATH_ESCAPE" };
262998
- const realRel = base === real ? "" : import_node_path25.default.relative(base, real);
263970
+ const realRel = base === real ? "" : import_node_path26.default.relative(base, real);
262999
263971
  if (relPathIsSensitive(realRel)) return { ok: false, code: "SENSITIVE" };
263000
263972
  return { ok: true, base, real };
263001
263973
  }
@@ -263031,7 +264003,7 @@ async function handleWorkdirList(req) {
263031
264003
  const slice = truncated ? visible.slice(0, WORKDIR_LIST_MAX_ENTRIES) : visible;
263032
264004
  const entries = [];
263033
264005
  for (const d of slice) {
263034
- const abs = import_node_path25.default.join(anchor, d.name);
264006
+ const abs = import_node_path26.default.join(anchor, d.name);
263035
264007
  try {
263036
264008
  const st = await import_node_fs19.default.promises.lstat(abs);
263037
264009
  if (st.isSymbolicLink()) continue;
@@ -263063,7 +264035,7 @@ async function verifyOpenedFd(fh, base, fallback) {
263063
264035
  const fdReal = await fdCanonicalPath(fh);
263064
264036
  if (fdReal === null) return { anchor: fallback };
263065
264037
  if (!withinBase(fdReal, base)) return { error: { ok: false, code: "PATH_ESCAPE" } };
263066
- const fdRel = base === fdReal ? "" : import_node_path25.default.relative(base, fdReal);
264038
+ const fdRel = base === fdReal ? "" : import_node_path26.default.relative(base, fdReal);
263067
264039
  if (relPathIsSensitive(fdRel)) return { error: { ok: false, code: "SENSITIVE" } };
263068
264040
  return { anchor: `/proc/self/fd/${fh.fd}` };
263069
264041
  }
@@ -263152,7 +264124,7 @@ function looksBinary(bytes2) {
263152
264124
  function contentTypeFor(absPath, bytes2) {
263153
264125
  const sniffed = sniffContentType(bytes2);
263154
264126
  if (sniffed) return sniffed;
263155
- const ext = import_node_path25.default.extname(absPath).toLowerCase();
264127
+ const ext = import_node_path26.default.extname(absPath).toLowerCase();
263156
264128
  if (EXT_CONTENT_TYPE[ext]) return EXT_CONTENT_TYPE[ext];
263157
264129
  return looksBinary(bytes2) ? "application/octet-stream" : "text/plain; charset=utf-8";
263158
264130
  }
@@ -263748,17 +264720,17 @@ var RuntimeRouterAdapter = class {
263748
264720
  // ../cli/src/daemon/reap-claude-projects.ts
263749
264721
  var import_node_fs20 = __toESM(require("node:fs"), 1);
263750
264722
  var import_node_os10 = __toESM(require("node:os"), 1);
263751
- var import_node_path26 = __toESM(require("node:path"), 1);
264723
+ var import_node_path27 = __toESM(require("node:path"), 1);
263752
264724
  function claudeProjectSlug(cwd) {
263753
264725
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
263754
264726
  }
263755
264727
  function claudeProjectsRoot() {
263756
- const configDir = process.env["CLAUDE_CONFIG_DIR"] || import_node_path26.default.join(import_node_os10.default.homedir(), ".claude");
263757
- return import_node_path26.default.join(configDir, "projects");
264728
+ const configDir = process.env["CLAUDE_CONFIG_DIR"] || import_node_path27.default.join(import_node_os10.default.homedir(), ".claude");
264729
+ return import_node_path27.default.join(configDir, "projects");
263758
264730
  }
263759
264731
  function reapClaudeProjects(workdir, runtimeKind) {
263760
264732
  if (runtimeKind !== "claude" && runtimeKind !== "claude-code") return;
263761
- const dir = import_node_path26.default.join(claudeProjectsRoot(), claudeProjectSlug(workdir));
264733
+ const dir = import_node_path27.default.join(claudeProjectsRoot(), claudeProjectSlug(workdir));
263762
264734
  if (!import_node_fs20.default.existsSync(dir)) return;
263763
264735
  try {
263764
264736
  import_node_fs20.default.rmSync(dir, { recursive: true, force: true });
@@ -263767,7 +264739,7 @@ function reapClaudeProjects(workdir, runtimeKind) {
263767
264739
  }
263768
264740
  function measureClaudeProjects(workdir, runtimeKind) {
263769
264741
  if (runtimeKind !== "claude" && runtimeKind !== "claude-code") return 0;
263770
- const dir = import_node_path26.default.join(claudeProjectsRoot(), claudeProjectSlug(workdir));
264742
+ const dir = import_node_path27.default.join(claudeProjectsRoot(), claudeProjectSlug(workdir));
263771
264743
  return dirSizeBytes2(dir);
263772
264744
  }
263773
264745
  function dirSizeBytes2(dir) {
@@ -263779,7 +264751,7 @@ function dirSizeBytes2(dir) {
263779
264751
  return 0;
263780
264752
  }
263781
264753
  for (const e of entries) {
263782
- const p2 = import_node_path26.default.join(dir, e.name);
264754
+ const p2 = import_node_path27.default.join(dir, e.name);
263783
264755
  if (e.isSymbolicLink()) continue;
263784
264756
  if (e.isDirectory()) {
263785
264757
  total += dirSizeBytes2(p2);
@@ -266190,7 +267162,7 @@ function fieldsFromFlags(flags, ownFlags) {
266190
267162
  }
266191
267163
  return Object.keys(fields).length > 0 ? fields : void 0;
266192
267164
  }
266193
- function buildStageOp(cmd, flags, positional, readFile8 = (file) => fs39.readFileSync(file, "utf8")) {
267165
+ function buildStageOp(cmd, flags, positional, readFile9 = (file) => fs39.readFileSync(file, "utf8")) {
266194
267166
  switch (cmd) {
266195
267167
  case "link":
266196
267168
  return {
@@ -266212,7 +267184,7 @@ function buildStageOp(cmd, flags, positional, readFile8 = (file) => fs39.readFil
266212
267184
  ...flags.get("title") !== void 0 ? { title: flags.get("title") } : {},
266213
267185
  ...(flags.get("brief") ?? flags.get("description")) !== void 0 ? { description: flags.get("brief") ?? flags.get("description") } : {},
266214
267186
  ...flags.get("input") !== void 0 ? { inputs: flags.get("input").split(",").map((to) => ({ to: to.trim() })) } : {},
266215
- ...partsFile !== void 0 ? { parts: JSON.parse(readFile8(partsFile)) } : {},
267187
+ ...partsFile !== void 0 ? { parts: JSON.parse(readFile9(partsFile)) } : {},
266216
267188
  ...fields !== void 0 ? { fields } : {}
266217
267189
  };
266218
267190
  }
@@ -269579,7 +270551,7 @@ function shimScript() {
269579
270551
  }
269580
270552
 
269581
270553
  // src/index.ts
269582
- var PKG_VERSION = true ? "2.2.6" : "dev";
270554
+ var PKG_VERSION = true ? "2.2.8" : "dev";
269583
270555
  var LOCAL_BIN = localBin();
269584
270556
  var NPM_PREFIX = npmPrefix();
269585
270557
  var INSTANCE = DEFAULT_INSTANCE;