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.
- package/dist/index.js +767 -182
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
135973
|
-
|
|
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
|
-
|
|
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)
|
|
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))
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
* ②
|
|
146856
|
-
*
|
|
146857
|
-
* `source
|
|
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 (
|
|
148263
|
+
if (opts?.awaitingDispatch) return "awaiting_dispatch";
|
|
148070
148264
|
if (opts?.dispatchPaused) return "held";
|
|
148071
|
-
if (members.some((m2) => m2.state === "
|
|
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
|
-
|
|
148079
|
-
|
|
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 (
|
|
148101
|
-
if (
|
|
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 "
|
|
148105
|
-
if (
|
|
148106
|
-
if (
|
|
148107
|
-
if (
|
|
148108
|
-
if (
|
|
148109
|
-
if (hasCause("
|
|
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 (
|
|
148121
|
-
if (rt.status === "
|
|
148122
|
-
if (stall?.kind === "
|
|
148123
|
-
if (
|
|
148124
|
-
if (
|
|
148125
|
-
if (info.latestAccepted) return "
|
|
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
|
|
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 {
|
|
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
|
-
|
|
167539
|
-
|
|
167540
|
-
|
|
167541
|
-
|
|
167542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167591
|
-
|
|
167592
|
-
|
|
167593
|
-
|
|
167594
|
-
|
|
167595
|
-
|
|
167596
|
-
|
|
167597
|
-
|
|
167598
|
-
|
|
167599
|
-
|
|
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: "
|
|
167853
|
+
kind: "assistant_bond",
|
|
167604
167854
|
humanId,
|
|
167605
|
-
agentId
|
|
167855
|
+
agentId,
|
|
167606
167856
|
by,
|
|
167607
167857
|
at: this.now(),
|
|
167608
|
-
detail: {
|
|
167858
|
+
detail: { origin: "claim", via: "rebind", keepOriginalPosition: true }
|
|
167609
167859
|
});
|
|
167610
|
-
|
|
167611
|
-
|
|
167612
|
-
|
|
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 ?
|
|
167723
|
-
const nextName = opts.newName && opts.newName !==
|
|
167724
|
-
if (JSON.stringify(nextRoles) !== JSON.stringify(
|
|
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:
|
|
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
|
-
|
|
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:
|
|
168942
|
+
state: verdict.state,
|
|
168943
|
+
...verdict.blockedBy ? { blockedBy: verdict.blockedBy } : {},
|
|
168709
168944
|
...a.id === overviewRootId ? { overviewRoot: true } : {}
|
|
168710
168945
|
};
|
|
168711
168946
|
});
|
|
168712
|
-
const
|
|
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
|
-
|
|
219376
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
233476
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
233729
|
-
await
|
|
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
|
|
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
|
-
|
|
242548
|
-
|
|
242549
|
-
|
|
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
|
|
243207
|
-
|
|
243208
|
-
|
|
243209
|
-
|
|
243210
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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.
|
|
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.
|
|
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;
|