oasis_test_v2 2.2.89 → 2.2.90

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 +767 -182
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -707,7 +707,10 @@ function isDispatchUndecidable(error2) {
707
707
  function isDispatchFenced(error2) {
708
708
  return typeof error2 === "object" && error2 !== null && error2.dispatchFenced === true;
709
709
  }
710
- var SeqConflictError, BlobNotFoundError, AssistantBindConflictError, TraceSeqConflictError, DispatchUndecidableError, DispatchFencedError;
710
+ function isCompanyMembersLockLost(error2) {
711
+ return typeof error2 === "object" && error2 !== null && error2.companyMembersLockLost === true;
712
+ }
713
+ var SeqConflictError, BlobNotFoundError, AssistantBindConflictError, TraceSeqConflictError, DispatchUndecidableError, DispatchFencedError, CompanyMembersLockLostError;
711
714
  var init_errors = __esm({
712
715
  "../contract/src/errors.ts"() {
713
716
  "use strict";
@@ -771,6 +774,19 @@ var init_errors = __esm({
771
774
  /** 结构化标记,理由同 {@link DispatchUndecidableError.dispatchUndecidable}。 */
772
775
  dispatchFenced = true;
773
776
  };
777
+ CompanyMembersLockLostError = class extends Error {
778
+ constructor(companyId, reason, writeIssued = false) {
779
+ super(
780
+ `company members lock lost for ${companyId} (${reason}); ` + (writeIssued ? "the external write was already in flight and its outcome is undetermined" : "the pending external write was not issued")
781
+ );
782
+ this.companyId = companyId;
783
+ this.reason = reason;
784
+ this.writeIssued = writeIssued;
785
+ }
786
+ name = "CompanyMembersLockLostError";
787
+ /** 结构化标记,理由同 {@link DispatchUndecidableError.dispatchUndecidable}(跨包不靠 instanceof)。 */
788
+ companyMembersLockLost = true;
789
+ };
774
790
  }
775
791
  });
776
792
 
@@ -127579,7 +127595,9 @@ function scan(state, ctx) {
127579
127595
  workorderId: state.workorder.id,
127580
127596
  nodeId: node2.id,
127581
127597
  workId: lw.id,
127582
- reason: "failed exhausted"
127598
+ reason: ENGINE_RETRY_FUSE_KILL_REASON,
127599
+ source: "sla"
127600
+ // ADR 0626:引擎连败熔断 = 系统停的(节点主状态「待处理」,上报关掉也不掉成「已暂停」)
127583
127601
  });
127584
127602
  const retryEscId = `esc:retry-exhausted:${lw.id}`;
127585
127603
  const stuckActor = node2.assigneeActorId;
@@ -128154,7 +128172,9 @@ function processReviews(state, nodeId, workId, ctx, events) {
128154
128172
  kind: "review.kill",
128155
128173
  workorderId: state.workorder.id,
128156
128174
  reviewId: lrId,
128157
- reason: "exhausted"
128175
+ reason: "exhausted",
128176
+ source: "sla"
128177
+ // ADR 0626:评审熔断 = 系统停的
128158
128178
  });
128159
128179
  pushReviewExhaustedEscalation(state, nodeId, workId, lrId, req.reviewerActorId, exhausted.rounds, events);
128160
128180
  }
@@ -128348,7 +128368,7 @@ function assessConvergence(state, ctx) {
128348
128368
  const anyUnfinished = state.liveNodes().some((n) => !n.latestAcceptId);
128349
128369
  return { stuck: !anyRunning && !anyBlockingIssue && allIdle && anyUnfinished, codes };
128350
128370
  }
128351
- var SYSTEM_ENGINE_ACTOR;
128371
+ var ENGINE_RETRY_FUSE_KILL_REASON, SYSTEM_ENGINE_ACTOR;
128352
128372
  var init_activate = __esm({
128353
128373
  "../engine/src/activate.ts"() {
128354
128374
  "use strict";
@@ -128363,6 +128383,7 @@ var init_activate = __esm({
128363
128383
  init_model();
128364
128384
  init_views2();
128365
128385
  init_kill();
128386
+ ENGINE_RETRY_FUSE_KILL_REASON = "failed exhausted";
128366
128387
  SYSTEM_ENGINE_ACTOR = ENGINE_SYSTEM_ACTOR;
128367
128388
  }
128368
128389
  });
@@ -129041,6 +129062,9 @@ ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS handled_issue_ids jsonb;
129041
129062
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS protocol_recovery jsonb;
129042
129063
  -- ADR 0607\uFF1A\u6267\u884C\u4EBA\u786E\u8BA4\u300C\u4E0A\u6E38\u6362\u7248\u4E0D\u5F71\u54CD\u5DF2\u63A5\u53D7\u7684\u4EA4\u4ED8\u300D\uFF08note / \u786E\u8BA4\u4EBA / \u65F6\u523B / \u5F53\u65F6\u7684\u5DF2\u63A5\u53D7\u4EA4\u4ED8\uFF09\u3002
129043
129064
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS upstream_ack jsonb;
129065
+ -- ADR 0626\uFF1Awork.kill / review.kill \u58F0\u660E\u7684\u300C\u8C01\u505C\u7684\u300D\uFF08human / manager / sla\uFF09\u3002\u5B58\u91CF\u884C\u4E3A NULL = \u672A\u58F0\u660E\u3002
129066
+ ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS kill_source text;
129067
+ ALTER TABLE ${q2}.reviews ADD COLUMN IF NOT EXISTS kill_source text;
129044
129068
  -- \u56DE\u4FE1\u8F6E\u6307\u5411\u54EA\u6761 comment issue\uFF08\u7EAF\u6570\u636E\uFF09
129045
129069
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
129046
129070
  -- \u2605 work lane\uFF08\u6B63\u5411\u5224\u636E\uFF0C\u53D6\u4EE3 "reply_to_issue_id IS NULL"\uFF09\uFF1A\u5B58\u91CF\u884C\u6309\u6709\u65E0 reply_to_issue_id \u56DE\u586B\u3002
@@ -134789,7 +134813,9 @@ var init_store_postgres = __esm({
134789
134813
  acceptedBy: w2.accepted_by,
134790
134814
  rejectedReason: w2.rejected_reason,
134791
134815
  overrideBy: w2.override_by,
134792
- overrideReason: w2.override_reason
134816
+ overrideReason: w2.override_reason,
134817
+ // 可选位:只在真有值时带上,与内存态 / 重放出来的行同形(未被停的行没有这个键)
134818
+ ...w2.kill_source ? { killSource: w2.kill_source } : {}
134793
134819
  })),
134794
134820
  artifacts: arts.rows.map((a) => ({
134795
134821
  id: a.id,
@@ -134869,7 +134895,8 @@ var init_store_postgres = __esm({
134869
134895
  verdict: v2.verdict,
134870
134896
  note: v2.note,
134871
134897
  handActorId: v2.hand_actor_id,
134872
- decidedAt: iso(v2.decided_at)
134898
+ decidedAt: iso(v2.decided_at),
134899
+ ...v2.kill_source ? { killSource: v2.kill_source } : {}
134873
134900
  }))
134874
134901
  };
134875
134902
  }
@@ -135422,7 +135449,7 @@ var init_plan = __esm({
135422
135449
  const titleChanged = e.title !== void 0 && e.title !== node2.title;
135423
135450
  if (!specChanged && !titleChanged) return;
135424
135451
  state.updateNode(e.nodeId, {
135425
- ...specChanged ? { spec: e.spec, version: node2.version + 1 } : {},
135452
+ ...specChanged ? { spec: e.spec, ...e.contentSync ? {} : { version: node2.version + 1 } } : {},
135426
135453
  ...titleChanged ? { title: e.title } : {},
135427
135454
  updatedAt: ctx.at
135428
135455
  });
@@ -135969,17 +135996,18 @@ var init_work = __esm({
135969
135996
  const target = e.workId ? state.work(e.workId) : null;
135970
135997
  const targetKillable = target !== null && target !== void 0 && (isWorkKillable(target) || target.endedAt !== null && target.endedAt !== void 0 && !target.deadAt && state.node(target.nodeId)?.latestAcceptId !== target.id);
135971
135998
  if (lw) {
135972
- if (isWorkKillable(lw)) {
135973
- killWorkRow(state, lw, ctx.at);
135974
- } else if (!lw.deadAt && lw.endedAt && node2.latestAcceptId !== lw.id) {
135999
+ const lwKillable = isWorkKillable(lw) || !lw.deadAt && !!lw.endedAt && node2.latestAcceptId !== lw.id;
136000
+ if (lwKillable) {
135975
136001
  killWorkRow(state, lw, ctx.at);
136002
+ if (e.source) state.updateWork(lw.id, { killSource: e.source });
135976
136003
  }
135977
136004
  }
135978
136005
  if (targetKillable) {
135979
136006
  const w2 = state.work(e.workId);
135980
136007
  state.updateWork(e.workId, {
135981
136008
  ...w2.rejectedReason === null ? { rejectedReason: e.reason } : {},
135982
- ...w2.status !== "dead" ? { status: "dead" } : {}
136009
+ ...w2.status !== "dead" ? { status: "dead" } : {},
136010
+ ...e.source ? { killSource: e.source } : {}
135983
136011
  });
135984
136012
  }
135985
136013
  if (node2.latestProposedWorkId) {
@@ -136523,7 +136551,10 @@ var init_review = __esm({
136523
136551
  name: "review/kill",
136524
136552
  kind: "review.kill",
136525
136553
  apply(e, state, ctx) {
136554
+ const before = state.review(e.reviewId);
136555
+ const killable = !!before && isReviewKillable(before);
136526
136556
  killReview(state, e.reviewId, ctx.at);
136557
+ if (killable && e.source) state.updateReview(e.reviewId, { killSource: e.source });
136527
136558
  },
136528
136559
  async effect(e, ctx) {
136529
136560
  await ctx.cancelSession(e.reviewId);
@@ -138141,8 +138172,11 @@ function emptyReadModel() {
138141
138172
  escalations: /* @__PURE__ */ new Map(),
138142
138173
  pendingReviews: /* @__PURE__ */ new Map(),
138143
138174
  pausedWorkorders: /* @__PURE__ */ new Set(),
138144
- acceptBlockers: /* @__PURE__ */ new Map()
138175
+ creationHeldWorkorders: /* @__PURE__ */ new Set(),
138176
+ acceptBlockers: /* @__PURE__ */ new Map(),
138145
138177
  // 默认空 = 无挡板 = 历史行为;只有 bridge 从引擎快照刷新时才填
138178
+ reworkHalted: /* @__PURE__ */ new Map()
138179
+ // 同上:只有 bridge 从引擎快照刷新时才填
138146
138180
  };
138147
138181
  }
138148
138182
  function indexAdd(index2, key2, value2) {
@@ -144085,6 +144119,25 @@ var init_command_card_naming = __esm({
144085
144119
  }
144086
144120
  });
144087
144121
 
144122
+ // ../core/src/seed-content.ts
144123
+ function headerRootIdOf(arts) {
144124
+ const roots = arts.filter((a) => a.inputs.length === 0);
144125
+ if (roots.length <= 1) return roots[0]?.id;
144126
+ const consumed = new Set(arts.flatMap((a) => a.inputs.map((e) => e.to)));
144127
+ return (roots.find((a) => !consumed.has(a.id)) ?? roots[0])?.id;
144128
+ }
144129
+ function isSeedContentNode(arts, artifactId) {
144130
+ const humanRoots = arts.filter((a) => a.inputs.length === 0 && isHumanActorId(a.owner));
144131
+ if (!humanRoots.some((a) => a.id === artifactId)) return false;
144132
+ return humanRoots.length === 1 || headerRootIdOf(arts) === artifactId;
144133
+ }
144134
+ var init_seed_content = __esm({
144135
+ "../core/src/seed-content.ts"() {
144136
+ "use strict";
144137
+ init_src();
144138
+ }
144139
+ });
144140
+
144088
144141
  // ../core/src/queue-retirement.ts
144089
144142
  function isSettledProduction(w2) {
144090
144143
  if (w2.lane === "reply") return false;
@@ -144133,6 +144186,10 @@ var init_queue_retirement = __esm({
144133
144186
  });
144134
144187
 
144135
144188
  // ../core/src/bridge-readmodel.ts
144189
+ function stopSourceOfDeadWork(w2) {
144190
+ if (w2.killSource) return w2.killSource;
144191
+ return w2.rejectedReason === ENGINE_RETRY_FUSE_KILL_REASON ? "sla" : "human";
144192
+ }
144136
144193
  function mapWorkStateToRevisionState(w2, artifactCount) {
144137
144194
  const s2 = workState(w2, w2.conclusion !== null && w2.conclusion !== void 0 || artifactCount > 0);
144138
144195
  switch (s2) {
@@ -144144,7 +144201,9 @@ function mapWorkStateToRevisionState(w2, artifactCount) {
144144
144201
  case "success":
144145
144202
  if (w2.acceptanceState === "accepted") return "merged";
144146
144203
  if (w2.acceptanceState === "rejected") return "rejected";
144147
- if (w2.conclusion == null) return "working";
144204
+ if (w2.conclusion == null) {
144205
+ return artifactCount > 0 ? "working" : "superseded";
144206
+ }
144148
144207
  return "proposed";
144149
144208
  // 已 conclude、未裁决
144150
144209
  // ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
@@ -144157,6 +144216,9 @@ function mapWorkStateToRevisionState(w2, artifactCount) {
144157
144216
  return "superseded";
144158
144217
  }
144159
144218
  }
144219
+ function settledWithoutOutput(w2, artifactFiles) {
144220
+ return w2.status === "success" && !w2.deadAt && w2.conclusion == null && (artifactFiles.get(w2.id)?.length ?? 0) === 0;
144221
+ }
144160
144222
  function mapIssueStateToAnnotationState(i) {
144161
144223
  if (i.resolvedAt === null) return "open";
144162
144224
  return i.resolution === "wontfix" ? "wontfix" : "resolved";
@@ -144413,25 +144475,41 @@ function bridgeLoadReadModel(input) {
144413
144475
  const requirementsByNode = groupRows(allRequirements, (r) => r.nodeId);
144414
144476
  const repliesByIssue = groupRows(allReplies, (r) => r.issueId);
144415
144477
  const reviewById = new Map(allReviews.map((r) => [r.id, r]));
144478
+ const artifactFiles = /* @__PURE__ */ new Map();
144479
+ for (const a of allArtifacts) {
144480
+ const list2 = artifactFiles.get(a.workId) ?? [];
144481
+ list2.push(a);
144482
+ artifactFiles.set(a.workId, list2);
144483
+ }
144416
144484
  const acceptBlockers = /* @__PURE__ */ new Map();
144485
+ const reworkHalted = /* @__PURE__ */ new Map();
144486
+ const upstreamAckedBy = /* @__PURE__ */ new Map();
144417
144487
  for (const snap of snapshots2) {
144418
144488
  let state = null;
144419
144489
  const snapshotWorks = new Map(snap.works.map((w2) => [w2.id, w2]));
144420
144490
  for (const node2 of snap.nodes) {
144491
+ const accepted = node2.latestAcceptId ? snapshotWorks.get(node2.latestAcceptId) : void 0;
144492
+ if (accepted && !node2.cancelledAt) {
144493
+ const pins = new Set((edgesByTarget.get(node2.id) ?? []).filter((e) => e.kind === "data" && e.pinnedWorkId).map((e) => e.pinnedWorkId));
144494
+ const used = inputsByWork.get(accepted.id) ?? [];
144495
+ const inputsMoved = pins.size !== used.length || used.some((i) => !pins.has(i.upstreamWorkId));
144496
+ if (inputsMoved) {
144497
+ state ??= new WorkorderState(snap);
144498
+ const ack = acceptedInputsCurrent(state, node2, accepted) ? snap.works.filter((w2) => w2.nodeId === node2.id && w2.status === "success" && w2.upstreamAck?.acceptedWorkId === accepted.id).at(-1) : void 0;
144499
+ if (ack) upstreamAckedBy.set(node2.id, ack.id);
144500
+ }
144501
+ }
144421
144502
  if (!node2.latestWorkId || node2.latestAcceptId === node2.latestWorkId) continue;
144422
144503
  const lw = snapshotWorks.get(node2.latestWorkId);
144423
144504
  if (!lw || !lw.endedAt || lw.deadAt) continue;
144505
+ if (settledWithoutOutput(lw, artifactFiles) && node2.latestProposedWorkId === node2.latestAcceptId) continue;
144424
144506
  state ??= new WorkorderState(snap);
144425
144507
  const block = acceptBlock(state, lw.id);
144426
144508
  if (block) acceptBlockers.set(node2.id, block);
144509
+ const halted = gateRejectionsExhausted(state, node2.id, {});
144510
+ if (halted) reworkHalted.set(node2.id, halted);
144427
144511
  }
144428
144512
  }
144429
- const artifactFiles = /* @__PURE__ */ new Map();
144430
- for (const a of allArtifacts) {
144431
- const list2 = artifactFiles.get(a.workId) ?? [];
144432
- list2.push(a);
144433
- artifactFiles.set(a.workId, list2);
144434
- }
144435
144513
  const artifacts = /* @__PURE__ */ new Map();
144436
144514
  const workById = /* @__PURE__ */ new Map();
144437
144515
  for (const w2 of allWorks) workById.set(w2.id, w2);
@@ -144449,7 +144527,7 @@ function bridgeLoadReadModel(input) {
144449
144527
  else uiSt = st;
144450
144528
  art.latestWorkState = uiSt;
144451
144529
  art.latestAccepted = node2.latestProposedWorkId !== null && node2.latestAcceptId === node2.latestProposedWorkId;
144452
- if ((st === "failed" || st === "dead") && node2.latestProposedWorkId) {
144530
+ if ((st === "failed" || st === "dead" || settledWithoutOutput(lw, artifactFiles)) && node2.latestProposedWorkId) {
144453
144531
  art.currentRev = node2.latestProposedWorkId;
144454
144532
  }
144455
144533
  } else {
@@ -144600,6 +144678,7 @@ function bridgeLoadReadModel(input) {
144600
144678
  const reqs = requirementsByNode.get(node2.id) ?? [];
144601
144679
  if (reqs.length === 0) continue;
144602
144680
  let stoppedAt = null;
144681
+ let stoppedBy = null;
144603
144682
  let deadGroups = 0;
144604
144683
  let liveGroups = 0;
144605
144684
  for (const g2 of new Set(reqs.map((r) => r.reviewGroup))) {
@@ -144612,14 +144691,20 @@ function bridgeLoadReadModel(input) {
144612
144691
  const st = reviewState(r);
144613
144692
  if (st === "dead") {
144614
144693
  deadGroups += 1;
144615
- if (r.cancelledAt && (stoppedAt === null || r.cancelledAt > stoppedAt)) stoppedAt = r.cancelledAt;
144694
+ if (r.cancelledAt && (stoppedAt === null || r.cancelledAt > stoppedAt)) {
144695
+ stoppedAt = r.cancelledAt;
144696
+ stoppedBy = r.killSource ?? null;
144697
+ }
144616
144698
  } else if (st !== "accept" && st !== "reject") {
144617
144699
  liveGroups += 1;
144618
144700
  }
144619
144701
  }
144620
144702
  if (deadGroups === 0 || liveGroups > 0 || stoppedAt === null) continue;
144621
144703
  holds.set(node2.id, {
144622
- source: "sla",
144704
+ // ADR 0626:来源取 review.kill 声明的 source(最后停的那一组)。此前恒写 sla——人用 `oasis hold`
144705
+ // 连带停掉的评审(D11:已暂停)与系统止损停的(待处理)因此分不开。未声明来源 = 人停的
144706
+ // (停评审的系统路径——零产出保险丝 / 评审熔断 / 系统 hold——都已声明 sla)。
144707
+ source: stoppedBy ?? "human",
144623
144708
  by: "",
144624
144709
  at: stoppedAt,
144625
144710
  reason: "\u8BC4\u5BA1\u5DF2\u505C\xB7\u5F85\u6062\u590D\uFF08\u4EA4\u4ED8\u7248\u4FDD\u7559\uFF0C\u6062\u590D\u540E\u7EE7\u7EED\u6295\u7968\uFF09"
@@ -144629,7 +144714,7 @@ function bridgeLoadReadModel(input) {
144629
144714
  if (node2.latestAcceptId === node2.latestWorkId) continue;
144630
144715
  const reason = lw.rejectedReason ?? (lw.outcome === "failed" ? "\u8FD0\u884C\u5931\u8D25\u5DF2\u505C\uFF08\u8FDE\u8D25/\u8D85\u65F6\uFF09" : "\u624B\u52A8\u505C\u6B62");
144631
144716
  holds.set(node2.id, {
144632
- source: "human",
144717
+ source: stopSourceOfDeadWork(lw),
144633
144718
  by: "",
144634
144719
  at: lw.deadAt,
144635
144720
  reason
@@ -144708,9 +144793,13 @@ function bridgeLoadReadModel(input) {
144708
144793
  }
144709
144794
  const workspaceStructuralSeq = /* @__PURE__ */ new Map();
144710
144795
  const pausedWorkorders = /* @__PURE__ */ new Set();
144796
+ const creationHeldWorkorders = /* @__PURE__ */ new Set();
144711
144797
  for (const snap of snapshots2) {
144712
144798
  workspaceStructuralSeq.set(snap.workorder.id, snap.workorder.structuralVersion);
144713
144799
  if (snap.workorder.dispatchPaused) pausedWorkorders.add(snap.workorder.id);
144800
+ if (snap.workorder.dispatchPaused && snap.workorder.pausedReason === WORKORDER_HOLD_AT_CREATE_REASON) {
144801
+ creationHeldWorkorders.add(snap.workorder.id);
144802
+ }
144714
144803
  }
144715
144804
  const revisionsByArtifact = /* @__PURE__ */ new Map();
144716
144805
  for (const w2 of allWorks) {
@@ -144737,7 +144826,9 @@ function bridgeLoadReadModel(input) {
144737
144826
  for (const edge of allEdges) {
144738
144827
  if (!edge.pinnedWorkId) continue;
144739
144828
  const record9 = pinMeta.get(edge.toNodeId) ?? {};
144740
- record9[edge.fromNodeId] = { seq: 0, actor: "", via: "link" };
144829
+ const ackWorkId = upstreamAckedBy.get(edge.toNodeId);
144830
+ const ackWork = ackWorkId ? workById.get(ackWorkId) : void 0;
144831
+ record9[edge.fromNodeId] = ackWork ? { seq: proposeSeq.get(ackWork.id) ?? 0, actor: ackWork.assigneeActorId, via: "bump", ack: true } : { seq: 0, actor: "", via: "link" };
144741
144832
  pinMeta.set(edge.toNodeId, record9);
144742
144833
  }
144743
144834
  const pendingReviews = /* @__PURE__ */ new Map();
@@ -144818,18 +144909,21 @@ function bridgeLoadReadModel(input) {
144818
144909
  holds,
144819
144910
  workspaceStructuralSeq,
144820
144911
  pausedWorkorders,
144912
+ creationHeldWorkorders,
144821
144913
  lastOpAt,
144822
144914
  pinMeta,
144823
144915
  gaps,
144824
144916
  deliveredGapSolutions,
144825
144917
  escalations,
144826
144918
  pendingReviews,
144827
- acceptBlockers
144919
+ acceptBlockers,
144920
+ reworkHalted
144828
144921
  };
144829
144922
  }
144830
144923
  var init_bridge_readmodel = __esm({
144831
144924
  "../core/src/bridge-readmodel.ts"() {
144832
144925
  "use strict";
144926
+ init_src();
144833
144927
  init_src6();
144834
144928
  init_queue_retirement();
144835
144929
  }
@@ -145254,6 +145348,9 @@ function previewIntervention(model, plan, config2) {
145254
145348
  };
145255
145349
  for (const id of touched) expand(id);
145256
145350
  const nonMonotonicOps = classifyNonMonotonic(plan.ops);
145351
+ if (plan.ops.some((op) => op.action === "edit" && op.description !== void 0 && op.description !== (model.artifacts.get(op.artifactId)?.description ?? null) && isSeedContentNode([...model.artifacts.values()].filter((a) => a.workspace === model.artifacts.get(op.artifactId)?.workspace), op.artifactId))) {
145352
+ nonMonotonicOps.push("edit-seed-content");
145353
+ }
145257
145354
  const changeClass = violations.length > 0 || nonMonotonicOps.length > 0 ? "C" : "B";
145258
145355
  return {
145259
145356
  changeClass,
@@ -145295,6 +145392,7 @@ var init_preview_intervention = __esm({
145295
145392
  init_kernel();
145296
145393
  init_node_identity();
145297
145394
  init_governance();
145395
+ init_seed_content();
145298
145396
  MAX_PREREQ_DEPTH = 8;
145299
145397
  }
145300
145398
  });
@@ -145407,6 +145505,7 @@ var init_kernel_bridge = __esm({
145407
145505
  init_preview_intervention();
145408
145506
  init_errors2();
145409
145507
  init_errors2();
145508
+ init_seed_content();
145410
145509
  init_rejected_proposals();
145411
145510
  PENDING_ROLE_PREFIX2 = "pending:role:";
145412
145511
  PENDING_UNASSIGNED2 = "pending:unassigned";
@@ -145460,6 +145559,15 @@ var init_kernel_bridge = __esm({
145460
145559
  * 不接也不影响作废本身:台账的周期对账看到条目消失后仍会收口,只是说不出「是谁把上报关了」。
145461
145560
  */
145462
145561
  draftVoidedListeners = /* @__PURE__ */ new Set();
145562
+ /**
145563
+ * 内容柜(ADR 0625):工单总纲「改描述 = 出一版新内容」要把正文写成 blob,内核本身不持有 blobs,
145564
+ * 由装配方(serve 的公司引擎路由 / startOasisServer / 测试基座)注入同一家公司的字节柜。
145565
+ * 没注入时改总纲描述直接报错,而不是退回「升版本等人重交」的老路——那条路正是 Bug 0265 的卡死形状。
145566
+ */
145567
+ contentStore = null;
145568
+ useContentStore(store) {
145569
+ this.contentStore = store;
145570
+ }
145463
145571
  /** 作废扫描的重入闸:作废走的 {@link dropDraftCard} 不刷新模型,但仍要挡住并发刷新里的重复扫描。 */
145464
145572
  sweepingStaleDrafts = false;
145465
145573
  model;
@@ -146221,6 +146329,15 @@ var init_kernel_bridge = __esm({
146221
146329
  }
146222
146330
  // ── 里程碑 / 收尾 ──
146223
146331
  async conclude(args) {
146332
+ const result = await this.concludeCore(args);
146333
+ try {
146334
+ await this.syncSeedSpec(args.artifactId, args.actor);
146335
+ } catch (err) {
146336
+ console.warn(`[kernel-bridge] \u603B\u7EB2 spec \u955C\u50CF\u540C\u6B65\u5931\u8D25\uFF08${args.artifactId}\uFF09\uFF1A${String(err)}`);
146337
+ }
146338
+ return result;
146339
+ }
146340
+ async concludeCore(args) {
146224
146341
  await this.refreshTracked();
146225
146342
  const wid = this.wo(args.artifactId);
146226
146343
  const conv = convergenceState(this.model, args.artifactId);
@@ -146311,6 +146428,72 @@ var init_kernel_bridge = __esm({
146311
146428
  }
146312
146429
  return { kind: "milestone", head: headId };
146313
146430
  }
146431
+ /**
146432
+ * 工单总纲出一版新内容(ADR 0625):**出版本、收口、验收一步完成,改的人就是拍板的人**。
146433
+ *
146434
+ * 两个入口共用这一处:页头保存(`PUT /api/workorders/:id/overview`)与改图里对总纲的 `edit description`。
146435
+ * - 内容与当前最新一版相同 → 不出新版(同值不 bump),只把 spec 镜像对齐;
146436
+ * - 工单暂停 → 只记人工稿(`recordPausedRevision`,不开运行、不验收,恢复后由 scan 处理);
146437
+ * - 否则 propose → advanceQueue → conclude(与建单首版 `seedWorkorderBrief` 同一条链)。
146438
+ * 下游只按「输入换版」各派一轮,可用 `oasis ack` 确认沿用(ADR 0607),不因此扩大级联。
146439
+ */
146440
+ async publishSeedContent(args) {
146441
+ const store = this.contentStore;
146442
+ if (!store) throw new KernelError("\u5185\u5BB9\u67DC\u672A\u88C5\u914D\uFF0C\u65E0\u6CD5\u4FDD\u5B58\u5DE5\u5355\u603B\u7EB2\u5185\u5BB9", "content-store-missing");
146443
+ await this.refreshTracked();
146444
+ const wid = this.wo(args.artifactId);
146445
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
146446
+ const paused = !!snap?.workorder.dispatchPaused;
146447
+ const current = await this.seedContentText(snap, args.artifactId);
146448
+ if (current !== null && current.trim() === args.text.trim()) {
146449
+ await this.commitSeedSpec(args.artifactId, args.actor, current);
146450
+ return { published: false, concluded: !paused };
146451
+ }
146452
+ const contentRef = await store.put(new TextEncoder().encode(args.text));
146453
+ if (paused) {
146454
+ await this.recordPausedRevision({ artifactId: args.artifactId, actor: args.actor, contentRef, note: args.note });
146455
+ await this.commitSeedSpec(args.artifactId, args.actor, args.text);
146456
+ return { published: true, concluded: false };
146457
+ }
146458
+ await this.proposeRevision({ artifactId: args.artifactId, actor: args.actor, contentRef, reason: args.note });
146459
+ await this.advanceQueue(args.artifactId, args.actor);
146460
+ const result = await this.conclude({ artifactId: args.artifactId, actor: args.actor, note: args.note });
146461
+ await this.commitSeedSpec(args.artifactId, args.actor, args.text);
146462
+ return { published: true, concluded: result.kind !== "gate-pending" };
146463
+ }
146464
+ /** 总纲最新一版有内容的正文(单个 inline 交付物);取不到(无版本 / 装箱单 / 外链)→ null。 */
146465
+ async seedContentText(snap, artifactId) {
146466
+ const node2 = snap?.nodes.find((n) => n.id === artifactId);
146467
+ const workId = node2?.latestProposedWorkId ?? node2?.latestAcceptId ?? null;
146468
+ if (!workId || !this.contentStore) return null;
146469
+ const outs = snap.artifacts.filter((a) => a.workId === workId);
146470
+ if (outs.length !== 1 || outs[0].contentKind !== "inline") return null;
146471
+ return new TextDecoder().decode(await this.contentStore.get(outs[0].contentRef));
146472
+ }
146473
+ /** 定稿后把总纲 spec 对齐到它最新一版的正文(只对总纲;非总纲、无内容柜、内容相同都 no-op)。 */
146474
+ async syncSeedSpec(artifactId, actor) {
146475
+ if (!this.contentStore) return;
146476
+ const art = this.model.artifacts.get(artifactId);
146477
+ if (!art) return;
146478
+ const arts = [...this.model.artifacts.values()].filter((a) => a.workspace === art.workspace);
146479
+ if (!isSeedContentNode(arts, artifactId)) return;
146480
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(this.wo(artifactId)));
146481
+ const text7 = await this.seedContentText(snap, artifactId);
146482
+ if (text7 !== null) await this.commitSeedSpec(artifactId, actor, text7);
146483
+ }
146484
+ /** 发 `plan.update_spec{contentSync}`:只改 spec、不升版本(引擎侧语义见 handlers/plan.ts)。 */
146485
+ async commitSeedSpec(artifactId, actor, text7) {
146486
+ const wid = this.wo(artifactId);
146487
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
146488
+ const node2 = snap?.nodes.find((n) => n.id === artifactId);
146489
+ if (!node2 || node2.spec === text7) return;
146490
+ await this.commit({
146491
+ companyId: "",
146492
+ workorderId: wid,
146493
+ actorId: actor,
146494
+ event: { kind: "plan.update_spec", workorderId: wid, nodeId: artifactId, spec: text7, policy: "auto", contentSync: true }
146495
+ });
146496
+ }
146314
146497
  /** 「这个 work 是否有 runtime 会话负责回报 response」的权威判据(serve 注入 dispatchToWork 在途登记)。 */
146315
146498
  runtimeSessionAuthority = null;
146316
146499
  setRuntimeSessionAuthority(fn) {
@@ -146852,10 +147035,10 @@ var init_kernel_bridge = __esm({
146852
147035
  * ① `issue.resolve` 的 handler 已经会对 gap/escalation 跑 `markNodeForRetry`(dead → retry),
146853
147036
  * 多数情况下节点就此复活——本方法只补它没覆盖的那一段:retry 之后 scan 因故没能建出新 work,
146854
147037
  * `latest_work.deadAt` 还在 → 派生的 hold 仍然成立、节点继续冻着。
146855
- * ② 派生的 hold **不带真实来源**:`bridge-readmodel` 的 holds 投影把 source 硬写成 `"human"`
146856
- * (`kernel.hold` 只落 `work.kill`,`source` 参数根本没进存储)。所以这里**不能**按
146857
- * `source === "sla"` 分档——那个判据恒不成立。改用「已无未解上报」当分寸:球不在任何人手里了,
146858
- * 才解冻。
147038
+ * ② 这里**不按** hold 的 source 分档,用「已无未解上报」当分寸:球不在任何人手里了,才解冻。
147039
+ * 写这段时 source 根本没进存储(`bridge-readmodel` 硬写 `"human"`);ADR 0626 起 `work.kill` /
147040
+ * `review.kill` 带 source 落 `kill_source`,但存量行仍没有这一列,而且解冻要不要做本来就该看
147041
+ * 「还有没有人在等」,不看是谁停的——所以口径不变。
146859
147042
  */
146860
147043
  async releaseIfStillHeld(artifactId, actor) {
146861
147044
  await this.refreshModel();
@@ -146927,7 +147110,7 @@ var init_kernel_bridge = __esm({
146927
147110
  companyId: "",
146928
147111
  workorderId: wid,
146929
147112
  actorId: args.actor,
146930
- event: { kind: "review.kill", workorderId: wid, reviewId: r.id, reason }
147113
+ event: { kind: "review.kill", workorderId: wid, reviewId: r.id, reason, ...args.source ? { source: args.source } : {} }
146931
147114
  });
146932
147115
  }
146933
147116
  return { kind: "reviews-stopped", workId: headWork.id, reviewIds: killable.map((r) => r.id) };
@@ -146946,7 +147129,9 @@ var init_kernel_bridge = __esm({
146946
147129
  workorderId: wid,
146947
147130
  nodeId: args.artifactId,
146948
147131
  workId: latestWork.id,
146949
- reason
147132
+ reason,
147133
+ // ADR 0626:把「谁停的」落进事实——读面的 hold.source 从这里取(此前在这里被丢掉,桥一律记成 human)。
147134
+ ...args.source ? { source: args.source } : {}
146950
147135
  }
146951
147136
  });
146952
147137
  return { kind: "work-killed", workId: latestWork.id };
@@ -147026,7 +147211,7 @@ var init_kernel_bridge = __esm({
147026
147211
  companyId: "",
147027
147212
  workorderId: wid,
147028
147213
  actorId: args.actor,
147029
- event: { kind: "review.kill", workorderId: wid, reviewId: r.id, reason: args.reason ?? "\u624B\u52A8\u505C\u6B62\u8BC4\u5BA1" }
147214
+ event: { kind: "review.kill", workorderId: wid, reviewId: r.id, reason: args.reason ?? "\u624B\u52A8\u505C\u6B62\u8BC4\u5BA1", ...args.source ? { source: args.source } : {} }
147030
147215
  });
147031
147216
  killed.push(r.id);
147032
147217
  }
@@ -147567,6 +147752,8 @@ var init_kernel_bridge = __esm({
147567
147752
  events.push(input);
147568
147753
  };
147569
147754
  const enqueue = async (event) => queue({ companyId: "", workorderId: event.workorderId, actorId: _actor, event });
147755
+ const seedContentEdits = /* @__PURE__ */ new Map();
147756
+ const scopedArtifactsOf = (workspace) => [...this.model.artifacts.values()].filter((a) => a.workspace === workspace);
147570
147757
  for (const op of plan.ops) {
147571
147758
  switch (op.action) {
147572
147759
  case "spawn": {
@@ -147715,6 +147902,8 @@ var init_kernel_bridge = __esm({
147715
147902
  }
147716
147903
  const opTitle = op.title;
147717
147904
  const opDescription = op.description;
147905
+ const seedEdit = artId && opDescription !== void 0 && opDescription !== (artifacts.get(artId)?.description ?? null) && isSeedContentNode(scopedArtifactsOf(woFor(artId)), artId);
147906
+ if (seedEdit) seedContentEdits.set(artId, opDescription);
147718
147907
  if (artId && (opTitle !== void 0 || opDescription !== void 0)) {
147719
147908
  await queue({
147720
147909
  companyId: "",
@@ -147726,7 +147915,8 @@ var init_kernel_bridge = __esm({
147726
147915
  nodeId: artId,
147727
147916
  spec: opDescription ?? artifacts.get(artId)?.description ?? null,
147728
147917
  ...opTitle !== void 0 ? { title: opTitle } : {},
147729
- policy: "auto"
147918
+ policy: "auto",
147919
+ ...seedContentEdits.has(artId) ? { contentSync: true } : {}
147730
147920
  }
147731
147921
  });
147732
147922
  }
@@ -147798,6 +147988,9 @@ var init_kernel_bridge = __esm({
147798
147988
  }
147799
147989
  });
147800
147990
  if (unsupported.length) throw new KernelError(`\u6574\u6279\u672A\u843D\u5730\uFF1B\u4EE5\u4E0B op \u672C\u5F15\u64CE\u4E0D\u652F\u6301\uFF1A${unsupported.join("\u3001")}`, "unsupported-op");
147991
+ for (const [artifactId, text7] of seedContentEdits) {
147992
+ await this.publishSeedContent({ artifactId, actor: _actor, text: text7, note: plan.reason?.trim() || "\u7F16\u8F91\u5DE5\u5355\u63CF\u8FF0" });
147993
+ }
147801
147994
  await this.ensureWorkorder(wid, "", _actor);
147802
147995
  await this.bus.submitBatchAndProcess(events);
147803
147996
  for (const event of events) {
@@ -147848,6 +148041,7 @@ var init_src7 = __esm({
147848
148041
  init_session_limits();
147849
148042
  init_rejected_proposals();
147850
148043
  init_command_card_naming();
148044
+ init_seed_content();
147851
148045
  init_kernel_bridge();
147852
148046
  init_bridge_readmodel();
147853
148047
  init_queue_retirement();
@@ -148066,17 +148260,26 @@ function workorderClosure(members) {
148066
148260
  function deriveStage(members, opts) {
148067
148261
  const closure = workorderClosure(members.map((m2) => m2.info));
148068
148262
  if (closure === "cancelled") return "cancelled";
148069
- if (members.some((m2) => m2.state === "pending")) return "pending";
148263
+ if (opts?.awaitingDispatch) return "awaiting_dispatch";
148070
148264
  if (opts?.dispatchPaused) return "held";
148071
- if (members.some((m2) => m2.state === "blocked")) return "blocked";
148265
+ if (members.some((m2) => m2.state === "pending")) return "pending";
148266
+ const advancing = members.some(
148267
+ (m2) => m2.state === "running" || m2.state === "queued" || m2.state === "reviewing" || m2.state === "blocked" && m2.blockedBy === "gap"
148268
+ );
148269
+ if (!advancing && members.some((m2) => m2.state === "held")) return "held";
148270
+ if (members.some((m2) => m2.state === "blocked" && m2.blockedBy !== "upstream")) return "blocked";
148072
148271
  if (closure === "settled" || closure === "archived") return "concluded";
148073
148272
  const delivery = members.filter((m2) => !m2.overviewRoot && !m2.info.sealed);
148074
148273
  if (delivery.length > 0 && delivery.every((m2) => m2.state === "not_run")) return "not_started";
148075
148274
  return "executing";
148076
148275
  }
148077
148276
  function nodeRunState(model, artifact, info, runtime, opts) {
148078
- if (info.sealed) return void 0;
148079
- if (artifact.continuationPause) return artifact.continuationPause.decision === "pending" ? "pending" : "blocked";
148277
+ return nodeRunVerdict(model, artifact, info, runtime, opts).state;
148278
+ }
148279
+ function nodeRunVerdict(model, artifact, info, runtime, opts) {
148280
+ if (info.sealed) return { state: void 0 };
148281
+ if (artifact.continuationPause?.decision === "pending") return { state: "pending" };
148282
+ const continuationDenied = artifact.continuationPause?.decision === "denied";
148080
148283
  const id = artifact.id;
148081
148284
  const now = opts?.now ?? Date.now();
148082
148285
  const rt = runtime ?? {};
@@ -148090,51 +148293,57 @@ function nodeRunState(model, artifact, info, runtime, opts) {
148090
148293
  const blocked = opts?.blocked ?? blockedOf(model, id);
148091
148294
  const hasCause = (c) => diagnosis.causes.some((x2) => x2.category === c);
148092
148295
  const stallSaysHumanBall = stall?.kind === "waiting_human" && (stall.action === "discuss" || stall.action === "rework" || stall.action === "accept" || stall.action === "review");
148093
- if (stallSaysHumanBall) return "pending";
148296
+ if (stallSaysHumanBall) return { state: "pending" };
148297
+ if (continuationDenied && unresolvedEscalationsOf(model, id).length > 0) return { state: "pending" };
148094
148298
  const stallMasksHumanBall = stall === null || stall.kind === "running" || // 档 1
148095
148299
  stall.kind === "held" || // 档 4
148096
148300
  stall.kind === "failed" || // 档 5
148097
148301
  stall.kind === "waiting_agent" && stall.action === "rework" || // 档 2(owner=agent,P2 不适用)
148098
148302
  stall.kind === "waiting_human" && stall.action === "resolve_gap";
148099
- if (stallMasksHumanBall && humanBallPending(model, artifact)) return "pending";
148100
- if (hasCause("owner-pending") || hasCause("accept-barrier")) return "pending";
148101
- if (rt.status === "fused" || rt.fused || hasCause("runtime-fused")) return "pending";
148303
+ if (stallMasksHumanBall && humanBallPending(model, artifact)) return { state: "pending" };
148304
+ if (!artifact.owner.startsWith("actor:agent:") && openBackpressureOf(model, id).length > 0) return { state: "pending" };
148305
+ if (hasCause("owner-pending") || hasCause("accept-barrier")) return { state: "pending" };
148306
+ if (rt.status === "fused" || rt.fused || hasCause("runtime-fused")) return { state: "pending" };
148102
148307
  const heldEarlyReturn = hasCause("held");
148103
- if (heldEarlyReturn && (isPendingOwner(artifact.owner) || model.acceptBlockers.get(id))) return "pending";
148104
- if (heldEarlyReturn) return "held";
148105
- if (hasCause("graph-gap") || hasCause("graph-deadlock") || hasCause("residual")) return "blocked";
148106
- if (hasCause("runtime-backoff") || hasCause("runtime-silent")) return "blocked";
148107
- if (blocked.blocked && info.latestWorkState !== null) return "blocked";
148108
- if (stall?.kind === "failed" || rt.status === "failed") return "blocked";
148109
- if (hasCause("content-gate")) return "reviewing";
148308
+ if (heldEarlyReturn && (isPendingOwner(artifact.owner) || model.acceptBlockers.get(id))) return { state: "pending" };
148309
+ if (heldEarlyReturn && holdOf(model, id)?.source === "sla") return { state: "pending" };
148310
+ if (model.reworkHalted?.has(id)) return { state: "pending" };
148311
+ if (heldEarlyReturn || continuationDenied) return { state: "held" };
148312
+ if (hasCause("graph-gap")) return { state: "blocked", blockedBy: "gap" };
148313
+ if (hasCause("graph-deadlock")) return { state: "blocked", blockedBy: "deadlock" };
148314
+ if (hasCause("residual")) return { state: "blocked", blockedBy: "residual" };
148315
+ if (blocked.blocked && info.latestWorkState !== null) return { state: "blocked", blockedBy: "upstream" };
148316
+ if (hasCause("content-gate")) return { state: "reviewing" };
148110
148317
  if (hasCause("resource-queued") || rt.queued) {
148111
148318
  const minDwell = opts?.queuedMinDwellMs ?? 2e3;
148112
148319
  const since = rt.queued?.since;
148113
148320
  if (since !== void 0) {
148114
148321
  const dwell = now - Date.parse(since);
148115
- if (dwell >= minDwell) return "queued";
148322
+ if (dwell >= minDwell) return { state: "queued" };
148116
148323
  } else {
148117
- return "queued";
148324
+ return { state: "queued" };
148118
148325
  }
148119
148326
  }
148120
- if (stall?.kind === "running") return "running";
148121
- if (rt.status === "running" || info.latestWorkState === "running") return "running";
148122
- if (stall?.kind === "waiting_agent") return "running";
148123
- if (info.latestWorkState === "success" && !info.latestAccepted) return "running";
148124
- if (info.latestWorkState === null) return "not_run";
148125
- if (info.latestAccepted) return "done";
148126
- return "not_run";
148327
+ if (hasCause("runtime-backoff") || hasCause("runtime-silent")) return { state: "running" };
148328
+ if (stall?.kind === "failed" || rt.status === "failed" || info.latestWorkState === "failed") return { state: "running" };
148329
+ if (stall?.kind === "running") return { state: "running" };
148330
+ if (rt.status === "running" || info.latestWorkState === "running") return { state: "running" };
148331
+ if (stall?.kind === "waiting_agent") return { state: "running" };
148332
+ if (info.latestWorkState === "success" && !info.latestAccepted) return { state: "running" };
148333
+ if (info.latestWorkState === null) return { state: "not_run" };
148334
+ if (info.latestAccepted) return { state: "done" };
148335
+ return { state: "not_run" };
148127
148336
  }
148128
148337
  function evaluateNodeRunState(model, artifact, info, projection = {}, opts) {
148129
148338
  const stall = stallOf(model, artifact.id, projection.exposedRuntime, projection.signals);
148130
- const state = nodeRunState(
148339
+ const verdict = nodeRunVerdict(
148131
148340
  model,
148132
148341
  artifact,
148133
148342
  info,
148134
148343
  { ...projection.signals ?? { hasInFlight: false }, status: projection.runtime?.status },
148135
148344
  { stall, blocked: blockedOf(model, artifact.id), ...opts ?? {} }
148136
148345
  );
148137
- return { state, stall };
148346
+ return { ...verdict, stall };
148138
148347
  }
148139
148348
  function humanBallPending(model, artifact) {
148140
148349
  const id = artifact.id;
@@ -167513,6 +167722,18 @@ var init_assistants = __esm({
167513
167722
  setPlatformAdminChecker(fn) {
167514
167723
  this.opts.isPlatformAdmin = fn;
167515
167724
  }
167725
+ /* ---------- 写侧同步边界(借给删 Agent 那道闸) ---------- */
167726
+ /**
167727
+ * 「给这个助理建 / 换绑定」与「删这个助理」的同步边界(🔴 2026-09-26 代码评审 R8 阻断项)。
167728
+ *
167729
+ * 本服务是绑定的唯一写入口,所以这把锁只能由它往外借:删 Agent 的那道闸
167730
+ * (`governance/agent-manage-guard.ts` 的 `lockAgentBinding`)拿它把「重读绑定 → 判
167731
+ * `ASSISTANT_BOUND` → 下发那笔停用」整段排进与换绑 / 认领 / 回归复用同一条队。
167732
+ * 不把 `bindStore` 本身递出去:绑定的写面仍然只在这个类里。
167733
+ */
167734
+ withAgentBindingLock(agentId, fn) {
167735
+ return this.opts.bindStore.withAgentLock(agentId, fn);
167736
+ }
167516
167737
  /* ---------- 读 ---------- */
167517
167738
  getByHuman(humanId) {
167518
167739
  return this.opts.bindStore.getByHuman(humanId);
@@ -167535,19 +167756,23 @@ var init_assistants = __esm({
167535
167756
  const bond = await this.opts.bindStore.getByHuman(humanId);
167536
167757
  if (bond) return { agentId: bond.agentActorId, created: false, source: "existing" };
167537
167758
  const restored = await this.opts.bindStore.restoreArchivedBindingByHuman(humanId);
167538
- const restoredActor = restored ? await this.opts.registry.getActor(restored.agentActorId) : null;
167539
- if (restored && restoredActor?.status !== "active") {
167540
- await this.opts.bindStore.archiveBindingByHuman(humanId, "unbind");
167541
- } else if (restored) {
167542
- await this.audit({
167543
- kind: "assistant_bond",
167544
- humanId,
167545
- agentId: restored.agentActorId,
167546
- by,
167547
- at: this.now(),
167548
- detail: { origin: restored.origin, via: "rejoin-restore" }
167759
+ if (restored) {
167760
+ const alive2 = await this.opts.bindStore.withAgentLock(restored.agentActorId, async () => {
167761
+ if ((await this.opts.registry.getActor(restored.agentActorId))?.status === "active") return true;
167762
+ await this.opts.bindStore.archiveBindingByHuman(humanId, "unbind");
167763
+ return false;
167549
167764
  });
167550
- return { agentId: restored.agentActorId, created: false, source: "existing" };
167765
+ if (alive2) {
167766
+ await this.audit({
167767
+ kind: "assistant_bond",
167768
+ humanId,
167769
+ agentId: restored.agentActorId,
167770
+ by,
167771
+ at: this.now(),
167772
+ detail: { origin: restored.origin, via: "rejoin-restore" }
167773
+ });
167774
+ return { agentId: restored.agentActorId, created: false, source: "existing" };
167775
+ }
167551
167776
  }
167552
167777
  const agentId = await this._createLocked(humanId, by, "lazy-create");
167553
167778
  return { agentId, created: true, source: "lazy-created" };
@@ -167586,45 +167811,55 @@ var init_assistants = __esm({
167586
167811
  if (target.status !== "active") {
167587
167812
  throw Object.assign(new Error("\u76EE\u6807\u6570\u5B57\u5458\u5DE5\u672A\u5728\u5C97"), { code: "REBIND_AGENT_NOT_ELIGIBLE" });
167588
167813
  }
167589
- return this.opts.bindStore.withHumanLock(humanId, async () => {
167590
- if (await this.opts.bindStore.getByAgent(agentId)) {
167591
- throw Object.assign(new Error("\u8BE5\u6570\u5B57\u5458\u5DE5\u5DF2\u662F\u4ED6\u4EBA\u52A9\u7406"), { code: "REBIND_AGENT_NOT_ELIGIBLE" });
167592
- }
167593
- if (await this.opts.bindStore.getHistoryByAgent(agentId)) {
167594
- throw Object.assign(new Error("\u8BE5\u6570\u5B57\u5458\u5DE5\u66FE\u88AB\u8BA4\u9886\u8FC7\uFF0C\u4E0D\u53EF\u518D\u7ED1\u4E3A\u52A9\u7406"), { code: "REBIND_AGENT_NOT_ELIGIBLE" });
167595
- }
167596
- const archived = await this.opts.bindStore.archiveBindingByHuman(humanId, "unbind");
167597
- if (archived) {
167598
- if (archived.origin === "lazy-create") {
167599
- await this.opts.actors.disableActor(archived.agentActorId, by).catch(() => {
167814
+ return this.opts.bindStore.withHumanLock(humanId, async () => (
167815
+ /* 🔴 2026-09-26 代码评审 R8 阻断项:换绑与「删这个助理」必须在**同一把锁**里排队。
167816
+ 上面那次 `status !== "active"` 是锁外读的,而删除侧读绑定也在它自己的锁外——两边各自
167817
+ 看到「对方还没动」,结果就是绑定落在一个正被删掉的 agent 上:成员在组织内、助理没了
167818
+ (组织页 PRD v1.6 禁的那个终态)。取锁顺序固定 human → agent,删除侧只取 agent 这一把,
167819
+ 所以不会成环。 */
167820
+ this.opts.bindStore.withAgentLock(agentId, async () => {
167821
+ if (await this.opts.bindStore.getByAgent(agentId)) {
167822
+ throw Object.assign(new Error("\u8BE5\u6570\u5B57\u5458\u5DE5\u5DF2\u662F\u4ED6\u4EBA\u52A9\u7406"), { code: "REBIND_AGENT_NOT_ELIGIBLE" });
167823
+ }
167824
+ if (await this.opts.bindStore.getHistoryByAgent(agentId)) {
167825
+ throw Object.assign(new Error("\u8BE5\u6570\u5B57\u5458\u5DE5\u66FE\u88AB\u8BA4\u9886\u8FC7\uFF0C\u4E0D\u53EF\u518D\u7ED1\u4E3A\u52A9\u7406"), { code: "REBIND_AGENT_NOT_ELIGIBLE" });
167826
+ }
167827
+ if ((await this.opts.registry.getActor(agentId))?.status !== "active") {
167828
+ throw Object.assign(new Error("\u76EE\u6807\u6570\u5B57\u5458\u5DE5\u672A\u5728\u5C97"), { code: "REBIND_AGENT_NOT_ELIGIBLE" });
167829
+ }
167830
+ const archived = await this.opts.bindStore.archiveBindingByHuman(humanId, "unbind");
167831
+ if (archived) {
167832
+ if (archived.origin === "lazy-create") {
167833
+ await this.opts.actors.disableActor(archived.agentActorId, by).catch(() => {
167834
+ });
167835
+ }
167836
+ await this.audit({
167837
+ kind: "assistant_bond_archived",
167838
+ humanId,
167839
+ agentId: archived.agentActorId,
167840
+ by,
167841
+ at: this.now(),
167842
+ detail: { trigger: "rebind", nextAgentId: agentId, previousOrigin: archived.origin }
167600
167843
  });
167601
167844
  }
167845
+ await this.opts.bindStore.insertBinding({
167846
+ humanActorId: humanId,
167847
+ agentActorId: agentId,
167848
+ createdAt: this.now(),
167849
+ createdBy: by,
167850
+ origin: "claim"
167851
+ });
167602
167852
  await this.audit({
167603
- kind: "assistant_bond_archived",
167853
+ kind: "assistant_bond",
167604
167854
  humanId,
167605
- agentId: archived.agentActorId,
167855
+ agentId,
167606
167856
  by,
167607
167857
  at: this.now(),
167608
- detail: { trigger: "rebind", nextAgentId: agentId, previousOrigin: archived.origin }
167858
+ detail: { origin: "claim", via: "rebind", keepOriginalPosition: true }
167609
167859
  });
167610
- }
167611
- await this.opts.bindStore.insertBinding({
167612
- humanActorId: humanId,
167613
- agentActorId: agentId,
167614
- createdAt: this.now(),
167615
- createdBy: by,
167616
- origin: "claim"
167617
- });
167618
- await this.audit({
167619
- kind: "assistant_bond",
167620
- humanId,
167621
- agentId,
167622
- by,
167623
- at: this.now(),
167624
- detail: { origin: "claim", via: "rebind", keepOriginalPosition: true }
167625
- });
167626
- return { agentId, created: false, source: "existing" };
167627
- });
167860
+ return { agentId, created: false, source: "existing" };
167861
+ })
167862
+ ));
167628
167863
  }
167629
167864
  /**
167630
167865
  * 主动解除绑定。懒创建助理同步停用;claim 来的数字员工只撤绑定、不停用 actor。
@@ -167713,20 +167948,21 @@ var init_assistants = __esm({
167713
167948
  await this.assertWritePermission(by, humanId);
167714
167949
  const agent = await this.opts.registry.getActor(agentId);
167715
167950
  if (!agent || agent.kind !== "agent") throw Object.assign(new Error("\u76EE\u6807\u4E0D\u662F agent \u5458\u5DE5"), { code: "LEGACY_AGENT_NOT_ELIGIBLE_FOR_REBIND" });
167716
- return this.opts.bindStore.withHumanLock(humanId, async () => {
167951
+ return this.opts.bindStore.withHumanLock(humanId, async () => this.opts.bindStore.withAgentLock(agentId, async () => {
167717
167952
  if (await this.opts.bindStore.getByHuman(humanId)) throw Object.assign(new Error("\u8BE5\u771F\u4EBA\u5DF2\u6709\u7ED1\u5B9A\u52A9\u7406"), { code: "LEGACY_AGENT_NOT_ELIGIBLE_FOR_REBIND" });
167718
167953
  if (await this.opts.bindStore.getByAgent(agentId)) throw Object.assign(new Error("\u8BE5 agent \u5DF2\u88AB\u7ED1\u5B9A"), { code: "LEGACY_AGENT_NOT_ELIGIBLE_FOR_REBIND" });
167719
167954
  if (await this.opts.bindStore.getHistoryByAgent(agentId)) throw Object.assign(new Error("\u8BE5 agent \u5DF2\u88AB\u8BA4\u9886\u8FC7\uFF0C\u4E0D\u5165\u6362\u7ED1\u5019\u9009"), { code: "LEGACY_AGENT_NOT_ELIGIBLE_FOR_REBIND" });
167720
167955
  await this.opts.bindStore.insertBinding({ humanActorId: humanId, agentActorId: agentId, createdAt: this.now(), createdBy: by, origin: "claim" });
167956
+ const current = await this.opts.registry.getActor(agentId) ?? agent;
167721
167957
  const keepPos = opts.keepOriginalPosition === true;
167722
- const nextRoles = keepPos ? agent.roles : ["assistant"];
167723
- const nextName = opts.newName && opts.newName !== agent.name ? opts.newName : agent.name;
167724
- if (JSON.stringify(nextRoles) !== JSON.stringify(agent.roles) || nextName !== agent.name || agent.status !== "active") {
167958
+ const nextRoles = keepPos ? current.roles : ["assistant"];
167959
+ const nextName = opts.newName && opts.newName !== current.name ? opts.newName : current.name;
167960
+ if (JSON.stringify(nextRoles) !== JSON.stringify(current.roles) || nextName !== current.name || current.status !== "active") {
167725
167961
  await this.opts.actors.upsertActor({ id: agentId, kind: "agent", name: nextName, status: "active", roles: nextRoles }, by);
167726
167962
  }
167727
- await this.audit({ kind: "assistant_bond", humanId, agentId, by, at: this.now(), detail: { origin: "claim", previousRoles: agent.roles, keepOriginalPosition: keepPos } });
167963
+ await this.audit({ kind: "assistant_bond", humanId, agentId, by, at: this.now(), detail: { origin: "claim", previousRoles: current.roles, keepOriginalPosition: keepPos } });
167728
167964
  return { agentId, humanId };
167729
- });
167965
+ }));
167730
167966
  }
167731
167967
  /* ---------- opt-out 开关 ---------- */
167732
167968
  async setAutoAssign(humanId, on, by) {
@@ -168663,10 +168899,7 @@ function actorRefFallbackOf(id) {
168663
168899
  return avatar ? { avatar } : void 0;
168664
168900
  }
168665
168901
  function overviewRootIdOf(arts) {
168666
- const roots = arts.filter((a) => a.inputs.length === 0);
168667
- if (roots.length <= 1) return roots[0]?.id;
168668
- const consumed = new Set(arts.flatMap((a) => a.inputs.map((e) => e.to)));
168669
- return (roots.find((a) => !consumed.has(a.id)) ?? roots[0])?.id;
168902
+ return headerRootIdOf(arts);
168670
168903
  }
168671
168904
  function buildWorkorderSummaries(model, resolveActor, resolveProject, dispatchPausedOf, projectionOf, viewer, resolveRoleExecutor) {
168672
168905
  const byWorkspace = /* @__PURE__ */ new Map();
@@ -168703,13 +168936,16 @@ function summaryOfWorkspace(model, workspace, arts, ref2, resolveProject, dispat
168703
168936
  const overviewRootId = overviewRootIdOf(arts);
168704
168937
  const stageMembers = arts.map((a, i) => {
168705
168938
  const info = members[i];
168939
+ const verdict = nodeStateOf2 ? nodeStateOf2(a.id, info) : evaluateNodeRunState(model, a, info, projectionOf?.(a.id, info));
168706
168940
  return {
168707
168941
  info,
168708
- state: nodeStateOf2 ? nodeStateOf2(a.id, info) : evaluateNodeRunState(model, a, info, projectionOf?.(a.id, info)).state,
168942
+ state: verdict.state,
168943
+ ...verdict.blockedBy ? { blockedBy: verdict.blockedBy } : {},
168709
168944
  ...a.id === overviewRootId ? { overviewRoot: true } : {}
168710
168945
  };
168711
168946
  });
168712
- const stage = deriveStage(stageMembers, { dispatchPaused });
168947
+ const awaitingDispatch = model.creationHeldWorkorders?.has(workspace) ?? false;
168948
+ const stage = deriveStage(stageMembers, { dispatchPaused, awaitingDispatch });
168713
168949
  const requiresMeCount = viewer ? arts.reduce((n, a) => {
168714
168950
  if (a.id === overviewRootId) return n;
168715
168951
  if (lifecycleOf(model, a.id) !== "active") return n;
@@ -169057,7 +169293,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
169057
169293
  ...e.upstreamCancelled ? { upstreamCancelled: true } : {}
169058
169294
  })) : [];
169059
169295
  const pv = reviewerPreview?.(a.id);
169060
- const { state: runState, stall: stallRaw } = evaluateNodeRunState(model, a, info, { runtime, exposedRuntime, signals });
169296
+ const { state: runState, blockedBy, stall: stallRaw } = evaluateNodeRunState(model, a, info, { runtime, exposedRuntime, signals });
169061
169297
  const stall = stallRaw ? (() => {
169062
169298
  const { actorId, blockers: rawBlockers, ...rest } = stallRaw;
169063
169299
  const blockers = rawBlockers?.map((bl) => ({
@@ -169077,7 +169313,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
169077
169313
  const mail = lifecycle === "active" ? openMailOf(model, a.id) : [];
169078
169314
  const requiresMeReason = lifecycle === "active" && a.id !== overviewRootId ? requiresMeReasonOf(model, a, viewer, requiresMeFacts) : null;
169079
169315
  const requiresMe = requiresMeReason !== null;
169080
- runStateById.set(a.id, runState);
169316
+ runStateById.set(a.id, { state: runState, ...blockedBy ? { blockedBy } : {} });
169081
169317
  return {
169082
169318
  id: a.id,
169083
169319
  type: a.type,
@@ -169145,7 +169381,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
169145
169381
  void 0,
169146
169382
  void 0,
169147
169383
  resolveRoleExecutor,
169148
- (id) => runStateById.get(id)
169384
+ (id) => runStateById.get(id) ?? { state: void 0 }
169149
169385
  );
169150
169386
  const edges = arts.flatMap(
169151
169387
  (a) => a.inputs.map((e) => ({ from: a.id, to: e.to, pinned: e.pinned, required: e.required }))
@@ -211687,6 +211923,7 @@ async function startOasisServer(opts) {
211687
211923
  void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor, companyId);
211688
211924
  }
211689
211925
  };
211926
+ opts.kernel.useContentStore?.(opts.blobs);
211690
211927
  const resolveEngine = opts.resolveEngine ?? (async () => ({ kernel: opts.kernel, oplog: opts.oplog, blobs: opts.blobs, ...opts.registry ? { registry: opts.registry } : {} }));
211691
211928
  const engineBus = opts.kernel.getBus?.();
211692
211929
  const engineEvents = opts.engineEvents ?? (engineBus ? (fn) => {
@@ -217543,6 +217780,13 @@ var init_memory_assistant_store = __esm({
217543
217780
  async withHumanLock(humanId, fn) {
217544
217781
  return this.mutex.run(humanId, fn);
217545
217782
  }
217783
+ /**
217784
+ * 按 agent 的那把(🔴 R8 阻断项,语义见 contract `AssistantBindStore.withAgentLock`):
217785
+ * 同一个 mutex map,键加 `agent\x1F` 前缀——与 Postgres 版同一套键形状,两边行为才对得上。
217786
+ */
217787
+ async withAgentLock(agentId, fn) {
217788
+ return this.mutex.run(`agent${agentId}`, fn);
217789
+ }
217546
217790
  async insertBinding(b2) {
217547
217791
  const hist = this.history.get(b2.agentActorId);
217548
217792
  if (hist && hist.firstBoundHuman !== b2.humanActorId) {
@@ -219108,6 +219352,7 @@ var memberKey, scopedKey, bridgeKey, migrationRow, MemoryControlPlaneStore;
219108
219352
  var init_memory_control_plane_store = __esm({
219109
219353
  "../testkit/src/memory-control-plane-store.ts"() {
219110
219354
  "use strict";
219355
+ init_src();
219111
219356
  memberKey = (companyId, accountId) => `${companyId} ${accountId}`;
219112
219357
  scopedKey = (companyId, id) => `${companyId}\0${id}`;
219113
219358
  bridgeKey = (companyId, type, id) => `${companyId}\0${type}\0${id}`;
@@ -219148,6 +219393,19 @@ var init_memory_control_plane_store = __esm({
219148
219393
  // key = companyId\0operationId
219149
219394
  /** 存量身份映射审计(ADR D3.4):**只追加**的一条数组,插入序即写入序。 */
219150
219395
  roleMigrations = [];
219396
+ /**
219397
+ * 成员行锁(模拟 PG 的 `SELECT … WHERE company_id FOR UPDATE`):有人持有时为一个待决 Promise。
219398
+ *
219399
+ * 为什么内存版也需要它:本类的成员写方法**方法体里没有 await**,彼此天然原子;但
219400
+ * `withCompanyMembersLocked` 的 `run` 是外部传进来的异步回调(员工页那笔 registry 写入),
219401
+ * 它一 await 就给了其它请求穿插的机会——不拦的话内存版与 PG 版在**唯一一处会出事的地方**分叉。
219402
+ * 所以每个成员写方法开头都排一次队(`while (this.membersLock) await this.membersLock`):
219403
+ * 方法体是同步的,队列排空那一刻进入就等价于「拿到锁」,中途不可能被穿插。
219404
+ *
219405
+ * 与 PG 的差别只有粒度:这里是**一把全局队列**(不按 companyId 分),内存 / 文件版量小,
219406
+ * 跨公司多排一会儿无害;换来的是所有入口一行代码、不可能漏掉某个公司维度。
219407
+ */
219408
+ membersLock;
219151
219409
  // —— 公司 ——
219152
219410
  async createCompany(c) {
219153
219411
  if (this.companies.has(c.id)) throw new Error(`company already exists: ${c.id}`);
@@ -219226,6 +219484,7 @@ var init_memory_control_plane_store = __esm({
219226
219484
  this.members.delete(memberKey(companyId, accountId));
219227
219485
  }
219228
219486
  async removeMemberIfNotLastOwner(companyId, accountId) {
219487
+ while (this.membersLock) await this.membersLock;
219229
219488
  const key2 = memberKey(companyId, accountId);
219230
219489
  const target = this.members.get(key2);
219231
219490
  if (!target) return { ok: false, reason: "not_found" };
@@ -219240,6 +219499,7 @@ var init_memory_control_plane_store = __esm({
219240
219499
  return { ok: true, member: { ...target } };
219241
219500
  }
219242
219501
  async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
219502
+ while (this.membersLock) await this.membersLock;
219243
219503
  const target = this.members.get(memberKey(companyId, accountId));
219244
219504
  if (!target) return { ok: false, reason: "not_found" };
219245
219505
  if (target.role === "owner") return { ok: false, reason: "owner_protected" };
@@ -219255,6 +219515,7 @@ var init_memory_control_plane_store = __esm({
219255
219515
  return { ok: true, member: { ...target }, releasedAgents };
219256
219516
  }
219257
219517
  async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
219518
+ while (this.membersLock) await this.membersLock;
219258
219519
  const key2 = memberKey(companyId, accountId);
219259
219520
  const target = this.members.get(key2);
219260
219521
  if (!target) return { ok: false, reason: "not_found" };
@@ -219269,7 +219530,18 @@ var init_memory_control_plane_store = __esm({
219269
219530
  this.members.set(key2, updated);
219270
219531
  return { ok: true, member: { ...updated } };
219271
219532
  }
219533
+ async updateMemberDisplayName(companyId, accountId, displayName3, opts) {
219534
+ while (this.membersLock) await this.membersLock;
219535
+ const key2 = memberKey(companyId, accountId);
219536
+ const target = this.members.get(key2);
219537
+ if (!target) return { ok: false, reason: "not_found" };
219538
+ if (opts?.requireNotOwner && target.role === "owner") return { ok: false, reason: "owner_protected" };
219539
+ const updated = { ...target, displayName: displayName3 };
219540
+ this.members.set(key2, updated);
219541
+ return { ok: true, member: { ...updated } };
219542
+ }
219272
219543
  async setSoleCompanyOwner(companyId, toAccountId, opts) {
219544
+ while (this.membersLock) await this.membersLock;
219273
219545
  const target = this.members.get(memberKey(companyId, toAccountId));
219274
219546
  if (!target) return { ok: false, reason: "not_found" };
219275
219547
  if (opts?.requireCurrentOwner !== void 0) {
@@ -219297,8 +219569,31 @@ var init_memory_control_plane_store = __esm({
219297
219569
  this.members.set(memberKey(companyId, toAccountId), owner);
219298
219570
  return { ok: true, owner: { ...owner }, demoted };
219299
219571
  }
219572
+ async withCompanyMembersLocked(companyId, run) {
219573
+ while (this.membersLock) await this.membersLock;
219574
+ let release;
219575
+ this.membersLock = new Promise((resolve12) => {
219576
+ release = resolve12;
219577
+ });
219578
+ let ended = false;
219579
+ const readLocked = () => [...this.members.values()].filter((m2) => m2.companyId === companyId).map((m2) => ({ ...m2 }));
219580
+ const lease = {
219581
+ beginExternalWrite: async () => {
219582
+ if (ended) throw new CompanyMembersLockLostError(companyId, "section_ended", false);
219583
+ return readLocked();
219584
+ }
219585
+ };
219586
+ try {
219587
+ return await run(readLocked(), lease);
219588
+ } finally {
219589
+ ended = true;
219590
+ this.membersLock = void 0;
219591
+ release();
219592
+ }
219593
+ }
219300
219594
  // —— 存量身份映射(ADR D3)——
219301
219595
  async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
219596
+ while (this.membersLock) await this.membersLock;
219302
219597
  const target = this.members.get(memberKey(companyId, toAccountId));
219303
219598
  if (!target) return { ok: false, reason: "not_found" };
219304
219599
  const others = [];
@@ -219362,6 +219657,7 @@ var init_memory_control_plane_store = __esm({
219362
219657
  return true;
219363
219658
  }
219364
219659
  async commitAcceptedInvitation(invitationId, now, member) {
219660
+ while (this.membersLock) await this.membersLock;
219365
219661
  const inv = this.invitations.get(invitationId);
219366
219662
  if (!inv) return { ok: false, reason: "not_found" };
219367
219663
  if (inv.status !== "pending") return { ok: false, reason: "not_pending", current: { ...inv } };
@@ -219370,18 +219666,13 @@ var init_memory_control_plane_store = __esm({
219370
219666
  this.invitations.set(invitationId, inv);
219371
219667
  return { ok: false, reason: "expired", current: { ...inv } };
219372
219668
  }
219669
+ const key2 = memberKey(member.companyId, member.accountId);
219670
+ const seated = this.members.get(key2);
219671
+ if (seated) return { ok: false, reason: "already_member", member: { ...seated } };
219373
219672
  inv.status = "accepted";
219374
219673
  this.invitations.set(invitationId, inv);
219375
- const key2 = memberKey(member.companyId, member.accountId);
219376
- const prev = this.members.get(key2);
219377
- const stored = {
219378
- ...member,
219379
- ...prev?.displayName !== void 0 ? { displayName: prev.displayName } : {},
219380
- ...prev?.onboardingCompletedAt !== void 0 ? { onboardingCompletedAt: prev.onboardingCompletedAt } : {},
219381
- ...prev?.onboardingStep !== void 0 ? { onboardingStep: prev.onboardingStep } : {}
219382
- };
219383
- this.members.set(key2, stored);
219384
- return { ok: true, invitation: { ...inv }, member: { ...stored } };
219674
+ this.members.set(key2, { ...member });
219675
+ return { ok: true, invitation: { ...inv }, member: { ...member } };
219385
219676
  }
219386
219677
  // —— 共享邀请链接 ——
219387
219678
  async createInviteLink(link) {
@@ -224617,6 +224908,7 @@ var init_control_plane_file_store = __esm({
224617
224908
  "removeMember",
224618
224909
  "removeMemberIfNotLastOwner",
224619
224910
  "updateMemberRoleIfNotLastOwner",
224911
+ "updateMemberDisplayName",
224620
224912
  "setSoleCompanyOwner",
224621
224913
  "removeMemberAndReleaseOwnedAgents",
224622
224914
  "migrateSoleCompanyOwner",
@@ -224721,6 +225013,10 @@ var init_control_plane_file_store = __esm({
224721
225013
  });
224722
225014
 
224723
225015
  // ../server/src/company-engine-router.ts
225016
+ function withContentStore(engine) {
225017
+ engine.kernel.useContentStore?.(engine.blobs);
225018
+ return engine;
225019
+ }
224724
225020
  var CompanyEngineRouter;
224725
225021
  var init_company_engine_router = __esm({
224726
225022
  "../server/src/company-engine-router.ts"() {
@@ -224728,7 +225024,7 @@ var init_company_engine_router = __esm({
224728
225024
  CompanyEngineRouter = class {
224729
225025
  constructor(opts) {
224730
225026
  this.opts = opts;
224731
- this.cache.set(opts.defaultCompanyId, opts.defaultEngine);
225027
+ this.cache.set(opts.defaultCompanyId, withContentStore(opts.defaultEngine));
224732
225028
  }
224733
225029
  cache = /* @__PURE__ */ new Map();
224734
225030
  /**
@@ -224760,6 +225056,7 @@ var init_company_engine_router = __esm({
224760
225056
  if (pending) return pending;
224761
225057
  const building = buildEngine(companyId).then(
224762
225058
  (engine) => {
225059
+ withContentStore(engine);
224763
225060
  this.cache.set(companyId, engine);
224764
225061
  this.inflight.delete(companyId);
224765
225062
  return engine;
@@ -233446,7 +233743,13 @@ function mayManageAgent(caller, subject) {
233446
233743
  }
233447
233744
  async function assertMayManageAgent(guard, ctx, agent) {
233448
233745
  if (!guard) return;
233449
- const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
233746
+ const [role, subject] = await Promise.all([
233747
+ guard.memberRole(ctx.companyId, ctx.callerActorId),
233748
+ resolveAgentSubject(guard, ctx, agent)
233749
+ ]);
233750
+ assertManageAgentWithRole(ctx.callerActorId, role, subject);
233751
+ }
233752
+ async function resolveAgentSubject(guard, ctx, agent) {
233450
233753
  const subject = { positions: agent.positions };
233451
233754
  if (isAssistantAgent(subject)) {
233452
233755
  const human = await guard.boundHuman(ctx.companyId, agent.id);
@@ -233455,14 +233758,27 @@ async function assertMayManageAgent(guard, ctx, agent) {
233455
233758
  subject.boundHumanReportsTo = await guard.reportsTo(ctx.companyId, human) ?? void 0;
233456
233759
  }
233457
233760
  }
233458
- if (mayManageAgent({ role, actorId: ctx.callerActorId }, subject)) return;
233761
+ return subject;
233762
+ }
233763
+ function assertManageAgentWithRole(callerActorId, callerRole, subject) {
233764
+ if (mayManageAgent({ role: callerRole, actorId: callerActorId }, subject)) return;
233459
233765
  throw new AgentManageForbiddenError(
233460
233766
  isAssistantAgent(subject) ? "\u53EA\u6709\u672C\u4EBA\u3001\u5176\u76F4\u63A5\u4E0A\u7EA7\u6216\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u8FD9\u4E2A\u52A9\u7406" : "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u4E13\u5BB6 Agent"
233461
233767
  );
233462
233768
  }
233769
+ function assistantBoundToMember(subject, roleOf) {
233770
+ if (!isAssistantAgent(subject)) return false;
233771
+ const human = subject.boundHumanActorId;
233772
+ if (human === void 0) return false;
233773
+ return roleOf(human) !== void 0;
233774
+ }
233775
+ function assertAgentDeletable(subject, roleOf) {
233776
+ if (!assistantBoundToMember(subject, roleOf)) return;
233777
+ throw new AgentManageForbiddenError(ASSISTANT_BOUND_MESSAGE, "ASSISTANT_BOUND");
233778
+ }
233463
233779
  async function assertCompanyManager(guard, ctx, what) {
233464
233780
  if (!guard) return;
233465
- const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
233781
+ const role = await guard.memberRole(ctx.companyId, ctx.callerActorId);
233466
233782
  if (isCompanyManager(role)) return;
233467
233783
  throw new AgentManageForbiddenError(`\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5${what}`);
233468
233784
  }
@@ -233472,23 +233788,79 @@ async function assertMayCreateAgent(guard, ctx) {
233472
233788
  async function assertMaySelfOrManage(guard, ctx, targetActorId) {
233473
233789
  if (!guard) return;
233474
233790
  if (ctx.callerActorId === targetActorId) return;
233475
- const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
233476
- if (isCompanyManager(role)) return;
233791
+ const [role, targetRole] = await Promise.all([
233792
+ guard.memberRole(ctx.companyId, ctx.callerActorId),
233793
+ guard.memberRole(ctx.companyId, targetActorId)
233794
+ ]);
233795
+ assertSelfOrManageWithRoles(ctx.callerActorId, targetActorId, role, targetRole);
233796
+ }
233797
+ function assertSelfOrManageWithRoles(callerActorId, targetActorId, callerRole, targetRole) {
233798
+ if (callerActorId === targetActorId) return;
233799
+ if (targetRole === "owner") {
233800
+ throw new AgentManageForbiddenError(OWNER_PROTECTED_PROFILE_MESSAGE, "OWNER_PROTECTED");
233801
+ }
233802
+ if (isCompanyManager(callerRole)) return;
233477
233803
  throw new AgentManageForbiddenError("\u53EA\u80FD\u4FEE\u6539\u81EA\u5DF1\u7684\u6863\u6848\uFF1B\u6539\u4ED6\u4EBA\u7684\u9700\u8981\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u6743\u9650");
233478
233804
  }
233479
- var ASSISTANT_POSITION, AgentManageForbiddenError;
233805
+ async function runGuardedActorWrite(guard, ctx, target, write) {
233806
+ if (!guard) return write();
233807
+ if (target.kind !== "agent" && ctx.callerActorId === target.id) return write();
233808
+ const subject = target.kind === "agent" ? await resolveAgentSubject(guard, ctx, { id: target.id, positions: target.positions }) : void 0;
233809
+ const nextSubject = subject !== void 0 && target.nextPositions !== void 0 && isAssistantAgent(subject) !== isAssistantAgent({ positions: target.nextPositions }) ? await resolveAgentSubject(guard, ctx, { id: target.id, positions: target.nextPositions }) : void 0;
233810
+ const check2 = (roleOf, latest = subject) => {
233811
+ if (latest === void 0) {
233812
+ assertSelfOrManageWithRoles(ctx.callerActorId, target.id, roleOf(ctx.callerActorId), roleOf(target.id));
233813
+ return;
233814
+ }
233815
+ const callerRole = roleOf(ctx.callerActorId);
233816
+ assertManageAgentWithRole(ctx.callerActorId, callerRole, latest);
233817
+ if (nextSubject) assertManageAgentWithRole(ctx.callerActorId, callerRole, nextSubject);
233818
+ if (target.intent === "delete") assertAgentDeletable(latest, roleOf);
233819
+ };
233820
+ const deletingAssistant = subject !== void 0 && target.intent === "delete" && isAssistantAgent(subject);
233821
+ const withBindingPinned = async (run) => {
233822
+ if (!deletingAssistant) return run(subject);
233823
+ const pinned = async () => run(await resolveAgentSubject(guard, ctx, { id: target.id, positions: target.positions }));
233824
+ return guard.lockAgentBinding ? guard.lockAgentBinding(ctx.companyId, target.id, pinned) : pinned();
233825
+ };
233826
+ if (!guard.lockMembers) {
233827
+ return withBindingPinned(async (latest) => {
233828
+ const boundHuman = target.intent === "delete" && latest?.boundHumanActorId !== ctx.callerActorId ? latest?.boundHumanActorId : void 0;
233829
+ const [callerRole, targetRole, boundHumanRole] = await Promise.all([
233830
+ guard.memberRole(ctx.companyId, ctx.callerActorId),
233831
+ // agent 目标不看「目标的成员角色」(agent 不是成员,D8.5),少发一次点查。
233832
+ latest === void 0 ? guard.memberRole(ctx.companyId, target.id) : Promise.resolve(void 0),
233833
+ boundHuman !== void 0 ? guard.memberRole(ctx.companyId, boundHuman) : Promise.resolve(void 0)
233834
+ ]);
233835
+ check2((actorId) => actorId === ctx.callerActorId ? callerRole : actorId === target.id ? targetRole : actorId === boundHuman ? boundHumanRole : void 0, latest);
233836
+ return write();
233837
+ });
233838
+ }
233839
+ return guard.lockMembers(ctx.companyId, async (roleOf, lease) => {
233840
+ check2(roleOf);
233841
+ return withBindingPinned(async (latest) => {
233842
+ const fresh = await lease.beginExternalWrite();
233843
+ check2(fresh, latest);
233844
+ return write();
233845
+ });
233846
+ });
233847
+ }
233848
+ var ASSISTANT_POSITION, AgentManageForbiddenError, OWNER_PROTECTED_PROFILE_MESSAGE, ASSISTANT_BOUND_MESSAGE;
233480
233849
  var init_agent_manage_guard = __esm({
233481
233850
  "../server/src/governance/agent-manage-guard.ts"() {
233482
233851
  "use strict";
233483
233852
  ASSISTANT_POSITION = "assistant";
233484
233853
  AgentManageForbiddenError = class extends Error {
233485
233854
  status = 403;
233486
- code = "FORBIDDEN";
233487
- constructor(message) {
233855
+ code;
233856
+ constructor(message, code2 = "FORBIDDEN") {
233488
233857
  super(message);
233489
233858
  this.name = "AgentManageForbiddenError";
233859
+ this.code = code2;
233490
233860
  }
233491
233861
  };
233862
+ OWNER_PROTECTED_PROFILE_MESSAGE = "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u7F16\u8F91\u3001\u964D\u7EA7\u6216\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7";
233863
+ ASSISTANT_BOUND_MESSAGE = "\u8BE5\u52A9\u7406\u4ECD\u7ED1\u5B9A\u7740\u7EC4\u7EC7\u5185\u7684\u6210\u5458\uFF0C\u4E0D\u80FD\u5220\u9664\uFF1B\u8981\u5148\u89E3\u7ED1\uFF08\u6216\u7B49\u8BE5\u6210\u5458\u79BB\u5F00\u7EC4\u7EC7\uFF09";
233492
233864
  }
233493
233865
  });
233494
233866
 
@@ -233596,16 +233968,32 @@ function actorsDomain(opts) {
233596
233968
  }
233597
233969
  await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: actor.id, positions: actor.roles }));
233598
233970
  };
233599
- const guardPositionsChange = async (req, existing, nextPositions) => {
233600
- if (existing.kind !== "agent" || nextPositions === void 0) return;
233601
- if (isAssistantAgent({ positions: existing.roles }) === isAssistantAgent({ positions: nextPositions })) return;
233602
- const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
233603
- await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: existing.id, positions: nextPositions }));
233604
- };
233605
- const guardExistingActorWrite = async (req, service, actorId) => {
233971
+ const requireActor = async (service, actorId) => {
233606
233972
  const existing = await service.getActor(actorId);
233607
233973
  if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${actorId}`);
233974
+ return existing;
233975
+ };
233976
+ const guardExistingActorWrite = async (req, service, actorId) => {
233977
+ const existing = await requireActor(service, actorId);
233608
233978
  await guardAgentWrite(req, existing);
233979
+ return existing;
233980
+ };
233981
+ const guardedActorWrite = async (req, existing, nextPositions, write, intent = "update") => {
233982
+ try {
233983
+ return await runGuardedActorWrite(
233984
+ opts.agentManageGuard,
233985
+ { companyId: req.auth.companyId, callerActorId: req.auth.actor },
233986
+ { id: existing.id, kind: existing.kind, positions: existing.roles, nextPositions, intent },
233987
+ write
233988
+ );
233989
+ } catch (e) {
233990
+ if (e instanceof AgentManageForbiddenError) throw new ApiError(e.status, e.code, e.message);
233991
+ if (isCompanyMembersLockLost(e)) {
233992
+ const undetermined = e.writeIssued === true;
233993
+ throw new ApiError(503, "MEMBERS_LOCK_UNAVAILABLE", undetermined ? "\u7EC4\u7EC7\u6210\u5458\u6B63\u5728\u53D8\u66F4\u4E2D\uFF0C\u8FD9\u6B21\u4FEE\u6539\u662F\u5426\u751F\u6548\u672A\u80FD\u786E\u8BA4\uFF0C\u8BF7\u5237\u65B0\u540E\u67E5\u770B" : "\u7EC4\u7EC7\u6210\u5458\u6B63\u5728\u53D8\u66F4\u4E2D\uFF0C\u8FD9\u6B21\u4FEE\u6539\u6CA1\u6709\u751F\u6548\uFF0C\u8BF7\u91CD\u8BD5");
233994
+ }
233995
+ throw e;
233996
+ }
233609
233997
  };
233610
233998
  const revealLimits = opts.credentialRevealLimits ?? { perSession: 200, perSessionPerKey: 20 };
233611
233999
  const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
@@ -233663,11 +234051,12 @@ function actorsDomain(opts) {
233663
234051
  companyId: req.auth.companyId,
233664
234052
  callerActorId: req.auth.actor
233665
234053
  }));
234054
+ const existing = await service.getActor(b2.id);
233666
234055
  const { id, kind, name } = b2;
233667
234056
  const positions = positionsFromPayload(b2);
233668
234057
  const avatar = avatarRefFromPayload(b2);
233669
234058
  const tags = tagsFromPayload(b2);
233670
- const actor = await asApiError(() => service.upsertActor(
234059
+ const write = () => asApiError(() => service.upsertActor(
233671
234060
  {
233672
234061
  id,
233673
234062
  kind,
@@ -233683,18 +234072,16 @@ function actorsDomain(opts) {
233683
234072
  },
233684
234073
  req.auth.actor
233685
234074
  ));
234075
+ const actor = existing ? await guardedActorWrite(req, existing, positions, write) : await write();
233686
234076
  return { status: 201, body: actorApiRecord(actor) };
233687
234077
  });
233688
234078
  router.patch("/api/actors/:id", async (req) => {
233689
234079
  const { service } = await resolveCtx(req.auth.companyId);
233690
- const existing = await service.getActor(req.params.id);
233691
- if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${req.params.id}`);
233692
- await guardAgentWrite(req, existing);
234080
+ const existing = await requireActor(service, req.params.id);
233693
234081
  const b2 = req.body ?? {};
233694
234082
  const positions = positionsFromPayload(b2);
233695
234083
  const avatar = avatarRefFromPayload(b2);
233696
234084
  const tags = tagsFromPayload(b2);
233697
- await guardPositionsChange(req, existing, positions);
233698
234085
  const {
233699
234086
  positions: _positions,
233700
234087
  id: _id,
@@ -233706,7 +234093,8 @@ function actorsDomain(opts) {
233706
234093
  tags: _tags,
233707
234094
  ...patch
233708
234095
  } = b2;
233709
- const actor = await asApiError(() => service.upsertActor({
234096
+ const intent = b2.status === "disabled" ? "delete" : "update";
234097
+ const actor = await guardedActorWrite(req, existing, positions, () => asApiError(() => service.upsertActor({
233710
234098
  ...existing,
233711
234099
  ...patch,
233712
234100
  // 档案展示字段:显式给(含清空)——空串/null 归一为 undefined(写库落 NULL);未传则保留原值
@@ -233720,27 +234108,33 @@ function actorsDomain(opts) {
233720
234108
  ...positions !== void 0 ? { roles: positions } : {},
233721
234109
  id: existing.id,
233722
234110
  kind: existing.kind
233723
- }, req.auth.actor));
234111
+ }, req.auth.actor)), intent);
233724
234112
  return { status: 200, body: actorApiRecord(actor) };
233725
234113
  });
233726
234114
  router.delete("/api/actors/:id", async (req) => {
233727
234115
  const { service } = await resolveCtx(req.auth.companyId);
233728
- await guardExistingActorWrite(req, service, req.params.id);
233729
- await service.disableActor(req.params.id, req.auth.actor);
234116
+ const existing = await requireActor(service, req.params.id);
234117
+ await guardedActorWrite(
234118
+ req,
234119
+ existing,
234120
+ void 0,
234121
+ () => service.disableActor(req.params.id, req.auth.actor),
234122
+ "delete"
234123
+ );
233730
234124
  return { status: 200, body: { ok: true } };
233731
234125
  });
233732
234126
  router.post("/api/actors/:id/avatar", async (req) => {
233733
234127
  const { service, blobs } = await resolveCtx(req.auth.companyId);
233734
234128
  if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
233735
- await guardExistingActorWrite(req, service, req.params.id);
234129
+ const existing = await guardExistingActorWrite(req, service, req.params.id);
233736
234130
  const ref2 = await storeImageBlob(blobs, req.body?.data);
233737
- const actor = await service.setActorAvatar(req.params.id, ref2, req.auth.actor);
234131
+ const actor = await guardedActorWrite(req, existing, void 0, () => service.setActorAvatar(req.params.id, ref2, req.auth.actor));
233738
234132
  return { status: 201, body: { avatar: actor.avatar } };
233739
234133
  });
233740
234134
  router.delete("/api/actors/:id/avatar", async (req) => {
233741
234135
  const { service } = await resolveCtx(req.auth.companyId);
233742
- await guardExistingActorWrite(req, service, req.params.id);
233743
- await service.setActorAvatar(req.params.id, null, req.auth.actor);
234136
+ const existing = await requireActor(service, req.params.id);
234137
+ await guardedActorWrite(req, existing, void 0, () => service.setActorAvatar(req.params.id, null, req.auth.actor));
233744
234138
  return { status: 200, body: { ok: true } };
233745
234139
  });
233746
234140
  router.post("/api/teams/:id/icon", async (req) => {
@@ -242544,15 +242938,9 @@ function collabDomain(opts) {
242544
242938
  if (snap?.workorder.dispatchPaused && !isHumanActor(req.auth.actor)) {
242545
242939
  return { status: 403, body: { error: { code: "forbidden", message: "\u6682\u505C\u671F\u95F4\u5F55\u5165\u5DE5\u5355\u6B63\u6587\u4EC5\u9650\u4EBA\u7C7B\u64CD\u4F5C" } } };
242546
242940
  }
242547
- const contentRef = await blobs.put(new TextEncoder().encode(text7));
242548
- if (snap?.workorder.dispatchPaused) {
242549
- await kernel.recordPausedRevision({ artifactId: rootId, actor: req.auth.actor, contentRef, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
242550
- return { status: 200, body: { overview: text7, concluded: false } };
242551
- }
242552
- await kernel.proposeRevision({ artifactId: rootId, actor: req.auth.actor, contentRef, reason: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
242553
- await kernel.advanceQueue(rootId, req.auth.actor);
242554
- const result = await kernel.conclude({ artifactId: rootId, actor: req.auth.actor, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
242555
- return { status: 200, body: { overview: text7, concluded: result.kind !== "gate-pending" } };
242941
+ kernel.useContentStore(blobs);
242942
+ const result = await kernel.publishSeedContent({ artifactId: rootId, actor: req.auth.actor, text: text7, note: "\u7F16\u8F91\u5DE5\u5355\u9875\u5934\u6B63\u6587" });
242943
+ return { status: 200, body: { overview: text7, concluded: result.concluded } };
242556
242944
  } catch (err) {
242557
242945
  if (err instanceof KernelError) {
242558
242946
  return {
@@ -243198,16 +243586,34 @@ var init_service6 = __esm({
243198
243586
  * 改成员在本公司的昵称(组织模型 ADR §4.1 `company_member.display_name`)。
243199
243587
  * 每公司独立:**不回写 `account.name`**——同一个人在 A 公司叫「小爱」、在 B 公司叫「Alice L.」是常态。
243200
243588
  * trim 后不得为空(空昵称等于没有昵称,那应该用「不传」表达,而不是存一个空串进去)。
243589
+ *
243590
+ * 🔴 2026-09-24 QA 第 3 轮回压的 P0 越权(ADR D5.1 / 验收标准第 3 条):这条路径此前
243591
+ * **既不问调用者是谁、也不问目标是谁**,只 `getMember` 读一行再 `upsertMember` 写回去。
243592
+ * 于是管理员 A1 用自己的登录会话直接 `PATCH …/members/<唯一组织管理员>` 带 `displayName`
243593
+ * 就能改掉组织管理员的资料(HTTP 200,刷新后生效)——前端把编辑入口藏了,服务端没有闸。
243594
+ *
243595
+ * 修法与 `changeMemberRole` / `removeMember` 同一套,不另造判据:
243596
+ * - `callerAccountId !== accountId`(**改别人的**)→ 走原子原语并要求「目标不是 owner」,
243597
+ * 复核在**锁内**、读的是目标的最新身份(并发转交刚把目标升成组织管理员也拦得住),
243598
+ * 被拒时报与另外两条同一个 `403 OWNER_PROTECTED`;
243599
+ * - `callerAccountId === accountId`(**改自己的**)→ 不复核身份。自己永远能改自己的名字,
243600
+ * 组织管理员本人也在入职向导第 1 步走这条路(`useSetMemberDisplayName`),收紧会把他卡死。
243601
+ *
243602
+ * 两支都走 `updateMemberDisplayName`——它**只写 display_name 一列**。旧的整行 `upsertMember`
243603
+ * 还有第二个坑:`SET role = $4` 无条件回写,读到写之间若有一次转交落地,这一笔昵称会把
243604
+ * 新 owner 的 role 写回旧值,组织当场 0 位 owner(QA R7 / R12 同形状的竞态,换了个入口)。
243201
243605
  */
243202
- async setMemberDisplayName(companyId, accountId, displayName3) {
243606
+ async setMemberDisplayName(companyId, accountId, displayName3, callerAccountId) {
243203
243607
  const trimmed = displayName3.trim();
243204
243608
  if (!trimmed) throw new CompanyError(400, "BAD_REQUEST", "displayName \u4E0D\u80FD\u4E3A\u7A7A");
243205
243609
  await this.requireCompany(companyId);
243206
- const member = await this.store.getMember(companyId, accountId);
243207
- if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243208
- const next = { ...member, displayName: trimmed };
243209
- await this.store.upsertMember(next);
243210
- return next;
243610
+ const editingSomeoneElse = callerAccountId !== accountId;
243611
+ const res = await this.store.updateMemberDisplayName(companyId, accountId, trimmed, {
243612
+ requireNotOwner: editingSomeoneElse
243613
+ });
243614
+ if (res.ok) return res.member;
243615
+ if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243616
+ throw this.ownerProtected();
243211
243617
  }
243212
243618
  /**
243213
243619
  * 「移除前预检」:确认成员可被移除,返回其 Membership(含 actorId)——**不删**。
@@ -243664,6 +244070,8 @@ var init_service6 = __esm({
243664
244070
  if (mine !== void 0 && mine !== normalizeEmail(inv.email)) {
243665
244071
  throw new CompanyError(403, "EMAIL_MISMATCH", "\u767B\u5F55\u8EAB\u4EFD\u4E0E\u88AB\u9080\u8BF7\u90AE\u7BB1\u4E0D\u7B26");
243666
244072
  }
244073
+ const member = await this.store.getMember(inv.companyId, accepter.accountId);
244074
+ if (member) throw new CompanyError(409, "ALREADY_MEMBER", "\u4F60\u5DF2\u7ECF\u662F\u8FD9\u4E2A\u56E2\u961F\u7684\u6210\u5458");
243667
244075
  return inv;
243668
244076
  }
243669
244077
  /**
@@ -243697,6 +244105,7 @@ var init_service6 = __esm({
243697
244105
  if (res.ok) return res.member;
243698
244106
  if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u9080\u8BF7\uFF1A${inv.id}`);
243699
244107
  if (res.reason === "expired") throw new CompanyError(410, "EXPIRED", "\u9080\u8BF7\u5DF2\u8FC7\u671F");
244108
+ if (res.reason === "already_member") throw new CompanyError(409, "ALREADY_MEMBER", "\u4F60\u5DF2\u7ECF\u662F\u8FD9\u4E2A\u56E2\u961F\u7684\u6210\u5458");
243700
244109
  throw new CompanyError(409, "NOT_PENDING", `\u9080\u8BF7\u5DF2\u5904\u7406\uFF08${res.current.status}\uFF09`);
243701
244110
  }
243702
244111
  /**
@@ -244179,7 +244588,7 @@ function companiesDomain(opts) {
244179
244588
  if (typeof b2.displayName !== "string" || !b2.displayName.trim()) {
244180
244589
  throw new ApiError(400, "BAD_REQUEST", "displayName \u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
244181
244590
  }
244182
- member = await service.setMemberDisplayName(cid, aid, b2.displayName).catch(mapErr);
244591
+ member = await service.setMemberDisplayName(cid, aid, b2.displayName, me.accountId).catch(mapErr);
244183
244592
  await syncActorNameToDisplayName(member, req.auth.actor);
244184
244593
  }
244185
244594
  return { status: 200, body: member };
@@ -260271,10 +260680,24 @@ var init_postgres_assistant_store = __esm({
260271
260680
  return new _PostgresAssistantBindStore(pool, schema, opts?.companyId);
260272
260681
  }
260273
260682
  async withHumanLock(humanId, fn) {
260683
+ return this.withKeyLock(this.lockSubject ? `${this.lockSubject}${humanId}` : humanId, fn);
260684
+ }
260685
+ /**
260686
+ * 按 agent 的那把(🔴 R8 阻断项,语义见 contract `AssistantBindStore.withAgentLock`)。
260687
+ *
260688
+ * 键多带一段 `agent` 前缀:human 键的形状(`<company>\x1F<id>`)一个字节不动——那一段
260689
+ * 有真 PG 回归盯着(`postgres-assistant-store-lockkey.postgres.test.ts`)。ActorId 本来就分
260690
+ * `actor:human:` / `actor:agent:` 两个命名空间、撞不上,前缀是为了让锁键读起来就知道锁的是谁。
260691
+ */
260692
+ async withAgentLock(agentId, fn) {
260693
+ const key2 = `agent${agentId}`;
260694
+ return this.withKeyLock(this.lockSubject ? `${this.lockSubject}${key2}` : key2, fn);
260695
+ }
260696
+ /** 两把锁共用的临界区:一条独占连接上开事务、取 xact advisory lock、跑完即提交(锁随事务放)。 */
260697
+ async withKeyLock(lockKey, fn) {
260274
260698
  const client = await this.pool.connect();
260275
260699
  try {
260276
260700
  await client.query("BEGIN");
260277
- const lockKey = this.lockSubject ? `${this.lockSubject}${humanId}` : humanId;
260278
260701
  await client.query("SELECT pg_advisory_xact_lock($1::int4, hashtext($2)::int4)", [ASSISTANT_LOCK_NAMESPACE, lockKey]);
260279
260702
  const out = await fn();
260280
260703
  await client.query("COMMIT");
@@ -261648,17 +262071,22 @@ var init_postgres_control_plane = __esm({
261648
262071
  "use strict";
261649
262072
  init_esm();
261650
262073
  init_pg_ident();
262074
+ init_src();
261651
262075
  ident8 = (s2) => {
261652
262076
  quoteIdent2(s2);
261653
262077
  return s2;
261654
262078
  };
261655
262079
  PostgresControlPlaneStore = class _PostgresControlPlaneStore {
261656
- constructor(pool, schema) {
262080
+ constructor(pool, schema, opts = {}) {
261657
262081
  this.pool = pool;
261658
262082
  this.s = `"${ident8(schema)}"`;
262083
+ this.membersLockWriteBudgetMs = opts.membersLockWriteBudgetMs ?? 3e4;
262084
+ this.membersLockHeartbeatMs = opts.membersLockHeartbeatMs ?? 5e3;
261659
262085
  }
261660
262086
  s;
261661
- static async open(pool, schema = "public") {
262087
+ membersLockWriteBudgetMs;
262088
+ membersLockHeartbeatMs;
262089
+ static async open(pool, schema = "public", opts = {}) {
261662
262090
  const s2 = ident8(schema);
261663
262091
  await pool.query(`CREATE SCHEMA IF NOT EXISTS "${s2}"`);
261664
262092
  await pool.query(`
@@ -261875,7 +262303,7 @@ var init_postgres_control_plane = __esm({
261875
262303
  )`);
261876
262304
  await pool.query(`CREATE INDEX IF NOT EXISTS company_member_role_migrations_company_idx ON "${s2}".company_member_role_migrations (company_id, applied_at DESC)`);
261877
262305
  await ensureOneOwnerInvariant(pool, s2);
261878
- return new _PostgresControlPlaneStore(pool, schema);
262306
+ return new _PostgresControlPlaneStore(pool, schema, opts);
261879
262307
  }
261880
262308
  // —— 公司 ——
261881
262309
  async createCompany(c) {
@@ -262113,6 +262541,40 @@ var init_postgres_control_plane = __esm({
262113
262541
  client.release();
262114
262542
  }
262115
262543
  }
262544
+ async updateMemberDisplayName(companyId, accountId, displayName3, opts) {
262545
+ const client = await this.pool.connect();
262546
+ try {
262547
+ await client.query("BEGIN");
262548
+ const all2 = await client.query(
262549
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
262550
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
262551
+ [companyId]
262552
+ );
262553
+ const rows = all2.rows.map(rowToMember);
262554
+ const target = rows.find((m2) => m2.accountId === accountId);
262555
+ if (!target) {
262556
+ await client.query("ROLLBACK");
262557
+ return { ok: false, reason: "not_found" };
262558
+ }
262559
+ if (opts?.requireNotOwner && target.role === "owner") {
262560
+ await client.query("ROLLBACK");
262561
+ return { ok: false, reason: "owner_protected" };
262562
+ }
262563
+ await client.query(
262564
+ `UPDATE ${this.s}.company_members SET display_name = $3, updated_at = now()
262565
+ WHERE company_id = $1 AND account_id = $2`,
262566
+ [companyId, accountId, displayName3]
262567
+ );
262568
+ await client.query("COMMIT");
262569
+ return { ok: true, member: { ...target, displayName: displayName3 } };
262570
+ } catch (e) {
262571
+ await client.query("ROLLBACK").catch(() => {
262572
+ });
262573
+ throw e;
262574
+ } finally {
262575
+ client.release();
262576
+ }
262577
+ }
262116
262578
  async setSoleCompanyOwner(companyId, toAccountId, opts) {
262117
262579
  const client = await this.pool.connect();
262118
262580
  try {
@@ -262180,6 +262642,83 @@ var init_postgres_control_plane = __esm({
262180
262642
  client.release();
262181
262643
  }
262182
262644
  }
262645
+ async withCompanyMembersLocked(companyId, run) {
262646
+ const client = await this.pool.connect();
262647
+ let writeIssued = false;
262648
+ let sectionEnded = false;
262649
+ let lost;
262650
+ let broken;
262651
+ let abandon;
262652
+ const lose = (reason, err) => {
262653
+ if (reason === "budget_exceeded" && writeIssued) return;
262654
+ lost ??= { reason, ...err ? { err } : {} };
262655
+ if (err) broken ??= err;
262656
+ if (!writeIssued) abandon?.(new CompanyMembersLockLostError(companyId, reason, false));
262657
+ };
262658
+ const onClientError = (err) => lose("connection_lost", err);
262659
+ client.on("error", onClientError);
262660
+ let heartbeat;
262661
+ let budget;
262662
+ try {
262663
+ await client.query("BEGIN");
262664
+ await client.query("SET LOCAL idle_in_transaction_session_timeout = 0");
262665
+ const lockRows = async () => {
262666
+ const r = await client.query(
262667
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
262668
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
262669
+ [companyId]
262670
+ );
262671
+ return r.rows.map(rowToMember);
262672
+ };
262673
+ const all2 = await lockRows();
262674
+ const lease = {
262675
+ beginExternalWrite: async () => {
262676
+ const guardFlags = () => {
262677
+ if (sectionEnded) throw new CompanyMembersLockLostError(companyId, "section_ended", false);
262678
+ if (lost) throw new CompanyMembersLockLostError(companyId, lost.reason, false);
262679
+ };
262680
+ guardFlags();
262681
+ let fresh;
262682
+ try {
262683
+ fresh = await lockRows();
262684
+ } catch (err) {
262685
+ lose("connection_lost", err instanceof Error ? err : new Error(String(err)));
262686
+ throw new CompanyMembersLockLostError(companyId, "connection_lost", false);
262687
+ }
262688
+ guardFlags();
262689
+ writeIssued = true;
262690
+ return fresh;
262691
+ }
262692
+ };
262693
+ heartbeat = setInterval(() => {
262694
+ client.query("SELECT 1").catch((err) => lose("connection_lost", err instanceof Error ? err : new Error(String(err))));
262695
+ }, this.membersLockHeartbeatMs);
262696
+ heartbeat.unref?.();
262697
+ budget = setTimeout(() => lose("budget_exceeded"), this.membersLockWriteBudgetMs);
262698
+ budget.unref?.();
262699
+ const running = run(all2, lease);
262700
+ const out = await new Promise((resolve12, reject) => {
262701
+ abandon = reject;
262702
+ if (lost && !writeIssued) reject(new CompanyMembersLockLostError(companyId, lost.reason, false));
262703
+ running.then(resolve12, reject);
262704
+ });
262705
+ if (lost) {
262706
+ throw new CompanyMembersLockLostError(companyId, lost.reason, true);
262707
+ }
262708
+ await client.query("COMMIT");
262709
+ return out;
262710
+ } catch (e) {
262711
+ if (!broken) await client.query("ROLLBACK").catch(() => {
262712
+ });
262713
+ throw e;
262714
+ } finally {
262715
+ sectionEnded = true;
262716
+ if (heartbeat) clearInterval(heartbeat);
262717
+ if (budget) clearTimeout(budget);
262718
+ client.removeListener("error", onClientError);
262719
+ client.release(broken);
262720
+ }
262721
+ }
262183
262722
  // —— 存量身份映射(ADR D3)——
262184
262723
  async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
262185
262724
  const client = await this.pool.connect();
@@ -262339,6 +262878,16 @@ var init_postgres_control_plane = __esm({
262339
262878
  await client.query("ROLLBACK");
262340
262879
  return { ok: false, reason: "not_pending", current: cur };
262341
262880
  }
262881
+ const seats = await client.query(
262882
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
262883
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
262884
+ [member.companyId]
262885
+ );
262886
+ const seated = seats.rows.map(rowToMember).find((m2) => m2.accountId === member.accountId);
262887
+ if (seated) {
262888
+ await client.query("ROLLBACK");
262889
+ return { ok: false, reason: "already_member", member: seated };
262890
+ }
262342
262891
  if (Date.parse(cur.expiresAt) <= Date.parse(now)) {
262343
262892
  await client.query(
262344
262893
  `UPDATE ${this.s}.company_invitations SET status = 'expired' WHERE id = $1`,
@@ -262354,10 +262903,20 @@ var init_postgres_control_plane = __esm({
262354
262903
  const inserted = await client.query(
262355
262904
  `INSERT INTO ${this.s}.company_members (company_id, account_id, actor_id, role, created_at, updated_at)
262356
262905
  VALUES ($1,$2,$3,$4,now(),now())
262357
- ON CONFLICT (company_id, account_id) DO UPDATE SET actor_id = $3, role = $4, updated_at = now()
262906
+ ON CONFLICT (company_id, account_id) DO NOTHING
262358
262907
  RETURNING *`,
262359
262908
  [member.companyId, member.accountId, member.actorId, member.role]
262360
262909
  );
262910
+ if (inserted.rowCount === 0) {
262911
+ const raced = await client.query(
262912
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
262913
+ FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2`,
262914
+ [member.companyId, member.accountId]
262915
+ );
262916
+ await client.query("ROLLBACK");
262917
+ const current = raced.rows[0] ? rowToMember(raced.rows[0]) : member;
262918
+ return { ok: false, reason: "already_member", member: current };
262919
+ }
262361
262920
  await client.query("COMMIT");
262362
262921
  return { ok: true, invitation: { ...cur, status: "accepted" }, member: rowToMember(inserted.rows[0]) };
262363
262922
  } catch (e) {
@@ -278733,6 +279292,7 @@ __export(src_exports2, {
278733
279292
  ACTOR_DELETED_CHAT_MESSAGE: () => ACTOR_DELETED_CHAT_MESSAGE,
278734
279293
  ACTOR_DELETED_ERROR_CODE: () => ACTOR_DELETED_ERROR_CODE,
278735
279294
  ASSISTANT_ARCHIVED_CODE: () => ASSISTANT_ARCHIVED_CODE,
279295
+ ASSISTANT_BOUND_MESSAGE: () => ASSISTANT_BOUND_MESSAGE,
278736
279296
  ASSISTANT_POSITION: () => ASSISTANT_POSITION,
278737
279297
  ASSISTANT_TIER_RULES: () => ASSISTANT_TIER_RULES,
278738
279298
  AUTOMATION_TEMPLATES: () => AUTOMATION_TEMPLATES,
@@ -278880,6 +279440,7 @@ __export(src_exports2, {
278880
279440
  ORPHANED_TURN_ASSISTANT_NOTICE: () => ORPHANED_TURN_ASSISTANT_NOTICE,
278881
279441
  OUTPUT_COPY_LIMITS: () => OUTPUT_COPY_LIMITS,
278882
279442
  OWNER_PROTECTED_MESSAGE: () => OWNER_PROTECTED_MESSAGE,
279443
+ OWNER_PROTECTED_PROFILE_MESSAGE: () => OWNER_PROTECTED_PROFILE_MESSAGE,
278883
279444
  PARENT_CONVERSATION_PATH: () => PARENT_CONVERSATION_PATH,
278884
279445
  PARENT_CONVERSATION_RECENT_TURNS: () => PARENT_CONVERSATION_RECENT_TURNS,
278885
279446
  PostgresNotificationStore: () => PostgresNotificationStore,
@@ -278939,6 +279500,7 @@ __export(src_exports2, {
278939
279500
  assertMayCreateAgent: () => assertMayCreateAgent,
278940
279501
  assertMayManageAgent: () => assertMayManageAgent,
278941
279502
  assertMaySelfOrManage: () => assertMaySelfOrManage,
279503
+ assistantBoundToMember: () => assistantBoundToMember,
278942
279504
  assistantRowRecovery: () => assistantRowRecovery,
278943
279505
  assistantsDomain: () => assistantsDomain,
278944
279506
  atCapacity: () => atCapacity,
@@ -279302,6 +279864,7 @@ __export(src_exports2, {
279302
279864
  rollbackOrgIdentity: () => rollbackOrgIdentity,
279303
279865
  roundOfMessage: () => roundOfMessage,
279304
279866
  routeFusedSignal: () => routeFusedSignal,
279867
+ runGuardedActorWrite: () => runGuardedActorWrite,
279305
279868
  runtimeConnectionStatus: () => runtimeConnectionStatus,
279306
279869
  runtimeDriftBlock: () => runtimeDriftBlock,
279307
279870
  runtimeSwitchedNotice: () => runtimeSwitchedNotice,
@@ -283211,10 +283774,10 @@ async function startServe(opts) {
283211
283774
  `controlPlaneStore` / `getEngine` / `resolveAssistantsCtx` 在下方才就绪;这三个箭头只在
283212
283775
  请求时跑(同 `projectExists` 的惰性模式)。 */
283213
283776
  agentManageGuard: {
283214
- callerRole: async (companyId, callerActorId) => {
283777
+ memberRole: async (companyId, actorId) => {
283215
283778
  const cid = companyId ?? defaultCompanyId;
283216
283779
  const members = await controlPlaneStore.listMembers(cid);
283217
- const row = members.find((m2) => m2.actorId === callerActorId || m2.accountId === callerActorId);
283780
+ const row = members.find((m2) => m2.actorId === actorId || m2.accountId === actorId);
283218
283781
  return row?.role;
283219
283782
  },
283220
283783
  boundHuman: async (companyId, agentId) => {
@@ -283222,6 +283785,27 @@ async function startServe(opts) {
283222
283785
  if (!ctx) return null;
283223
283786
  return (await ctx.service.getByAgent(agentId))?.humanActorId ?? null;
283224
283787
  },
283788
+ /* 删助理那一笔的绑定锁(🔴 R8 阻断项):与换绑 / 认领 / 回归复用**同一把**——都是助理域
283789
+ 那份 bindStore 上按 agent 键的 advisory lock,经 `withAgentBindingLock` 借出(绑定的写面
283790
+ 仍只在助理服务里)。和 `boundHuman` 走同一个解析器,所以锁的和读的必然是同一家公司的
283791
+ 同一份 store;两处各解析一次就会出现「锁了 A 家的、读了 B 家的」。
283792
+ 解析器还没装好(启动期,没有请求)→ 不额外上锁,闸那边仍会重读绑定。 */
283793
+ lockAgentBinding: async (companyId, agentId, run) => {
283794
+ const ctx = await resolveAssistantsCtxLazy(companyId);
283795
+ return ctx ? ctx.service.withAgentBindingLock(agentId, run) : run();
283796
+ },
283797
+ /* 锁内复核(评审阻断①):借控制面那把成员行锁,把「最终判权 + 那一笔 registry 写入」
283798
+ 排进与转交 / 降级 / 移除 / 改昵称同一条队。`roleOf` 读的是**锁内**那份快照,不是
283799
+ 上面 memberRole 那一眼;找行的口径与它逐字一致(现阶段 actorId === accountId,
283800
+ 但仍两边都比,免得哪天分化后这道闸静默失效)。 */
283801
+ lockMembers: async (companyId, run) => {
283802
+ const cid = companyId ?? defaultCompanyId;
283803
+ const roleOfIn = (members) => (actorId) => members.find((m2) => m2.actorId === actorId || m2.accountId === actorId)?.role;
283804
+ return controlPlaneStore.withCompanyMembersLocked(cid, async (members, lease) => run(
283805
+ roleOfIn(members),
283806
+ { beginExternalWrite: async () => roleOfIn(await lease.beginExternalWrite()) }
283807
+ ));
283808
+ },
283225
283809
  reportsTo: async (companyId, humanActorId) => {
283226
283810
  const engine2 = await getEngine(companyId ?? defaultCompanyId);
283227
283811
  return (await engine2.registry.getActor(humanActorId))?.reportsTo ?? null;
@@ -289674,6 +290258,7 @@ ${nodeFault}` : "");
289674
290258
  const killed = await tk.killNodeReviews({
289675
290259
  artifactId: t.artifactId,
289676
290260
  actor: SYSTEM_ACTOR2,
290261
+ source: "sla",
289677
290262
  reviewerActorId: t.actorId,
289678
290263
  reason: notice
289679
290264
  });
@@ -294936,7 +295521,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
294936
295521
  }
294937
295522
  }
294938
295523
  }
294939
- var CLI_VERSION = "2.2.89" ? "2.2.89" : "dev";
295524
+ var CLI_VERSION = "2.2.90" ? "2.2.90" : "dev";
294940
295525
  function apiHeaders(token, company) {
294941
295526
  const headers = {
294942
295527
  authorization: `Bearer ${token}`,
@@ -298907,7 +299492,7 @@ function shimScript() {
298907
299492
  }
298908
299493
 
298909
299494
  // src/index.ts
298910
- var PKG_VERSION = true ? "2.2.89" : "dev";
299495
+ var PKG_VERSION = true ? "2.2.90" : "dev";
298911
299496
  var LOCAL_BIN = localBin();
298912
299497
  var NPM_PREFIX = npmPrefix();
298913
299498
  var INSTANCE = DEFAULT_INSTANCE;