oasis_test_v2 2.2.81 → 2.2.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +325 -73
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2950,6 +2950,25 @@ async function fetchResultPackage(base, fetchImpl, runId) {
2950
2950
  }));
2951
2951
  return { artifacts, fileCount: body2.project?.fileCount ?? artifacts.length };
2952
2952
  }
2953
+ function toEpochMs(v2) {
2954
+ if (typeof v2 === "number" && Number.isFinite(v2)) return v2;
2955
+ if (typeof v2 === "string" && v2.length > 0) {
2956
+ const ms = Date.parse(v2);
2957
+ return Number.isNaN(ms) ? void 0 : ms;
2958
+ }
2959
+ return void 0;
2960
+ }
2961
+ function runTime(r) {
2962
+ return r.createdAt ?? r.updatedAt ?? Number.NEGATIVE_INFINITY;
2963
+ }
2964
+ function latestSucceededRun(runs) {
2965
+ let best;
2966
+ for (const r of runs) {
2967
+ if (r.status !== "succeeded") continue;
2968
+ if (best === void 0 || runTime(r) > runTime(best)) best = r;
2969
+ }
2970
+ return best;
2971
+ }
2953
2972
  async function fetchBoardStatus(base, fetchImpl, artifactId) {
2954
2973
  const boardKey = deriveBoardKey(artifactId);
2955
2974
  let boardExists = true;
@@ -2975,10 +2994,20 @@ async function fetchBoardStatus(base, fetchImpl, artifactId) {
2975
2994
  "GET",
2976
2995
  `/api/runs?projectId=${encodeURIComponent(boardKey)}`
2977
2996
  );
2978
- runs = (body2.runs ?? []).map((r) => ({
2979
- runId: String(r["runId"] ?? r["id"] ?? ""),
2980
- status: String(r["status"] ?? "unknown")
2981
- }));
2997
+ runs = (body2.runs ?? []).map((r) => {
2998
+ const run = {
2999
+ runId: String(r["runId"] ?? r["id"] ?? ""),
3000
+ status: String(r["status"] ?? "unknown")
3001
+ };
3002
+ if (typeof r["agentId"] === "string" && r["agentId"].length > 0) run.agentId = r["agentId"];
3003
+ if (typeof r["error"] === "string" && r["error"].trim().length > 0) run.error = r["error"];
3004
+ const createdAt = toEpochMs(r["createdAt"]);
3005
+ if (createdAt !== void 0) run.createdAt = createdAt;
3006
+ const updatedAt = toEpochMs(r["updatedAt"]);
3007
+ if (updatedAt !== void 0) run.updatedAt = updatedAt;
3008
+ return run;
3009
+ });
3010
+ runs.sort((a, b2) => runTime(b2) - runTime(a) || 0);
2982
3011
  } catch {
2983
3012
  }
2984
3013
  }
@@ -218354,13 +218383,22 @@ function cloneOffboarding(op) {
218354
218383
  assignmentDecisions: op.assignmentDecisions.map((d) => ({ ...d, toPrincipal: { ...d.toPrincipal } }))
218355
218384
  };
218356
218385
  }
218357
- var memberKey, scopedKey, bridgeKey, MemoryControlPlaneStore;
218386
+ var memberKey, scopedKey, bridgeKey, migrationRow, MemoryControlPlaneStore;
218358
218387
  var init_memory_control_plane_store = __esm({
218359
218388
  "../testkit/src/memory-control-plane-store.ts"() {
218360
218389
  "use strict";
218361
218390
  memberKey = (companyId, accountId) => `${companyId} ${accountId}`;
218362
218391
  scopedKey = (companyId, id) => `${companyId}\0${id}`;
218363
218392
  bridgeKey = (companyId, type, id) => `${companyId}\0${type}\0${id}`;
218393
+ migrationRow = (companyId, accountId, roleBefore, roleAfter, audit2) => ({
218394
+ companyId,
218395
+ accountId,
218396
+ roleBefore,
218397
+ roleAfter,
218398
+ evidence: audit2.evidence,
218399
+ ...audit2.evidence === "manifest" && audit2.manifestVersion !== void 0 ? { manifestVersion: audit2.manifestVersion } : {},
218400
+ appliedAt: audit2.appliedAt
218401
+ });
218364
218402
  MemoryControlPlaneStore = class {
218365
218403
  companies = /* @__PURE__ */ new Map();
218366
218404
  members = /* @__PURE__ */ new Map();
@@ -218480,6 +218518,21 @@ var init_memory_control_plane_store = __esm({
218480
218518
  this.members.delete(key2);
218481
218519
  return { ok: true, member: { ...target } };
218482
218520
  }
218521
+ async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
218522
+ const target = this.members.get(memberKey(companyId, accountId));
218523
+ if (!target) return { ok: false, reason: "not_found" };
218524
+ if (target.role === "owner") return { ok: false, reason: "owner_protected" };
218525
+ const releasedAgents = [];
218526
+ for (const [key2, e] of this.digitalEmployees) {
218527
+ if (e.companyId !== companyId || e.ownerAccountId !== accountId) continue;
218528
+ const { ownerAccountId: _cleared, ...rest } = e;
218529
+ this.digitalEmployees.set(key2, { ...rest, updatedAt: at2 });
218530
+ releasedAgents.push(e.id);
218531
+ }
218532
+ this.principalBridges.delete(bridgeKey(companyId, "human", accountId));
218533
+ this.members.delete(memberKey(companyId, accountId));
218534
+ return { ok: true, member: { ...target }, releasedAgents };
218535
+ }
218483
218536
  async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
218484
218537
  const key2 = memberKey(companyId, accountId);
218485
218538
  const target = this.members.get(key2);
@@ -218506,7 +218559,7 @@ var init_memory_control_plane_store = __esm({
218506
218559
  return { ok: false, reason: "target_not_listed" };
218507
218560
  }
218508
218561
  const acc = this.accounts.get(toAccountId);
218509
- if (acc && acc.status !== "active") return { ok: false, reason: "target_inactive" };
218562
+ if (!acc || acc.status !== "active") return { ok: false, reason: "target_inactive" };
218510
218563
  const others = [];
218511
218564
  for (const m2 of this.members.values()) {
218512
218565
  if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
@@ -218523,6 +218576,37 @@ var init_memory_control_plane_store = __esm({
218523
218576
  this.members.set(memberKey(companyId, toAccountId), owner);
218524
218577
  return { ok: true, owner: { ...owner }, demoted };
218525
218578
  }
218579
+ // —— 存量身份映射(ADR D3)——
218580
+ async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
218581
+ const target = this.members.get(memberKey(companyId, toAccountId));
218582
+ if (!target) return { ok: false, reason: "not_found" };
218583
+ const others = [];
218584
+ for (const m2 of this.members.values()) {
218585
+ if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
218586
+ }
218587
+ if (others.length === 0 && target.role === "owner") {
218588
+ return { ok: true, owner: { ...target }, demoted: [], auditRows: [] };
218589
+ }
218590
+ const auditRows = [];
218591
+ const demoted = others.map((m2) => {
218592
+ const next = { ...m2, role: "admin" };
218593
+ this.members.set(memberKey(companyId, m2.accountId), next);
218594
+ auditRows.push(migrationRow(companyId, m2.accountId, "owner", "admin", audit2));
218595
+ return { ...next };
218596
+ });
218597
+ if (target.role !== "owner") auditRows.push(migrationRow(companyId, toAccountId, target.role, "owner", audit2));
218598
+ const owner = { ...target, role: "owner" };
218599
+ this.members.set(memberKey(companyId, toAccountId), owner);
218600
+ for (const r of auditRows) this.roleMigrations.push({ ...r });
218601
+ return { ok: true, owner: { ...owner }, demoted, auditRows };
218602
+ }
218603
+ async setMemberRoleOnly(companyId, accountId, role) {
218604
+ const key2 = memberKey(companyId, accountId);
218605
+ const cur = this.members.get(key2);
218606
+ if (!cur) return false;
218607
+ this.members.set(key2, { ...cur, role });
218608
+ return true;
218609
+ }
218526
218610
  // —— 存量身份映射审计(ADR D3.4):只追加 ——
218527
218611
  async appendMemberRoleMigrations(rows) {
218528
218612
  for (const r of rows) this.roleMigrations.push({ ...r });
@@ -223813,6 +223897,9 @@ var init_control_plane_file_store = __esm({
223813
223897
  "removeMemberIfNotLastOwner",
223814
223898
  "updateMemberRoleIfNotLastOwner",
223815
223899
  "setSoleCompanyOwner",
223900
+ "removeMemberAndReleaseOwnedAgents",
223901
+ "migrateSoleCompanyOwner",
223902
+ "setMemberRoleOnly",
223816
223903
  "appendMemberRoleMigrations",
223817
223904
  "createInvitation",
223818
223905
  "updateInvitationStatus",
@@ -225260,38 +225347,17 @@ async function backfillOrgIdentity(deps, opts = {}) {
225260
225347
  const desc = row.changes.map((c) => `${c.accountId} ${c.roleBefore}\u2192${c.roleAfter}`).join("\uFF0C");
225261
225348
  log3(` ${row.companyId}\uFF1A${desc}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
225262
225349
  if (!opts.apply) continue;
225263
- const res = await deps.store.setSoleCompanyOwner(row.companyId, target);
225264
- if (!res.ok) {
225265
- throw new Error(
225266
- `${row.companyId} \u6536\u655B\u5230 ${target} \u5931\u8D25\uFF1A${res.reason}\uFF08target_not_listed = \u90A3\u4E2A\u4EBA\u8FD8\u6CA1\u8D70\u5B8C\u65B0\u624B\u6307\u5F15\uFF1Btarget_inactive = \u8D26\u53F7\u5DF2\u505C\u7528\uFF09`
225267
- );
225268
- }
225269
- applied += 1;
225270
- const at2 = now();
225271
225350
  const evidence = row.decision === "manifest" ? "manifest" : "E1";
225272
- const rows = res.demoted.map((m2) => ({
225273
- companyId: row.companyId,
225274
- accountId: m2.accountId,
225275
- roleBefore: "owner",
225276
- roleAfter: "admin",
225351
+ const res = await deps.store.migrateSoleCompanyOwner(row.companyId, target, {
225277
225352
  evidence,
225278
- ...opts.manifest?.manifestVersion !== void 0 && evidence === "manifest" ? { manifestVersion: opts.manifest.manifestVersion } : {},
225279
- appliedAt: at2
225280
- }));
225281
- const promoted = row.changes.find((c) => c.accountId === target);
225282
- if (promoted) {
225283
- rows.push({
225284
- companyId: row.companyId,
225285
- accountId: target,
225286
- roleBefore: promoted.roleBefore,
225287
- roleAfter: "owner",
225288
- evidence,
225289
- ...opts.manifest?.manifestVersion !== void 0 && evidence === "manifest" ? { manifestVersion: opts.manifest.manifestVersion } : {},
225290
- appliedAt: at2
225291
- });
225353
+ ...opts.manifest?.manifestVersion !== void 0 ? { manifestVersion: opts.manifest.manifestVersion } : {},
225354
+ appliedAt: now()
225355
+ });
225356
+ if (!res.ok) {
225357
+ throw new Error(`${row.companyId} \u6536\u655B\u5230 ${target} \u5931\u8D25\uFF1A${res.reason}\uFF08\u90A3\u4E00\u884C\u4E0D\u5728\u8BE5\u7EC4\u7EC7\u7684\u6210\u5458\u8868\u91CC\uFF09`);
225292
225358
  }
225293
- await deps.store.appendMemberRoleMigrations(rows);
225294
- auditRows += rows.length;
225359
+ applied += 1;
225360
+ auditRows += res.auditRows.length;
225295
225361
  }
225296
225362
  return { ...inv, applied, auditRows };
225297
225363
  }
@@ -225311,7 +225377,7 @@ async function rollbackOrgIdentity(deps, opts) {
225311
225377
  plan.push({ companyId: r.companyId, accountId: r.accountId, from: member.role, to: r.roleBefore });
225312
225378
  log3(` ${r.companyId} ${r.accountId} ${member.role}\u2192${r.roleBefore}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
225313
225379
  if (!opts.apply) continue;
225314
- await deps.store.upsertMember({ ...member, role: r.roleBefore });
225380
+ await deps.store.setMemberRoleOnly(r.companyId, r.accountId, r.roleBefore);
225315
225381
  reverted += 1;
225316
225382
  }
225317
225383
  return { plan, reverted };
@@ -232799,6 +232865,12 @@ function actorsDomain(opts) {
232799
232865
  }
232800
232866
  await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: actor.id, positions: actor.roles }));
232801
232867
  };
232868
+ const guardPositionsChange = async (req, existing, nextPositions) => {
232869
+ if (existing.kind !== "agent" || nextPositions === void 0) return;
232870
+ if (isAssistantAgent({ positions: existing.roles }) === isAssistantAgent({ positions: nextPositions })) return;
232871
+ const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
232872
+ await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: existing.id, positions: nextPositions }));
232873
+ };
232802
232874
  const guardExistingActorWrite = async (req, service, actorId) => {
232803
232875
  const existing = await service.getActor(actorId);
232804
232876
  if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${actorId}`);
@@ -232891,6 +232963,7 @@ function actorsDomain(opts) {
232891
232963
  const positions = positionsFromPayload(b2);
232892
232964
  const avatar = avatarRefFromPayload(b2);
232893
232965
  const tags = tagsFromPayload(b2);
232966
+ await guardPositionsChange(req, existing, positions);
232894
232967
  const {
232895
232968
  positions: _positions,
232896
232969
  id: _id,
@@ -242321,8 +242394,8 @@ var init_service6 = __esm({
242321
242394
  const target = await this.store.getMember(companyId, accountId);
242322
242395
  if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242323
242396
  if (target.role === "owner") throw this.ownerProtected();
242324
- const res = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
242325
- if (res.ok) return res.member;
242397
+ const res = await this.store.removeMemberAndReleaseOwnedAgents(companyId, accountId, this.now());
242398
+ if (res.ok) return { member: res.member, releasedAgents: res.releasedAgents };
242326
242399
  if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242327
242400
  throw this.ownerProtected();
242328
242401
  }
@@ -242846,12 +242919,14 @@ var init_offboarding = __esm({
242846
242919
  listInFlight;
242847
242920
  transferAssignment;
242848
242921
  disableActor;
242922
+ restoreActor;
242849
242923
  constructor(opts) {
242850
242924
  this.store = opts.store;
242851
242925
  this.now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
242852
242926
  this.listInFlight = opts.listInFlightAssignments;
242853
242927
  this.transferAssignment = opts.transferAssignment;
242854
242928
  this.disableActor = opts.disableActor;
242929
+ this.restoreActor = opts.restoreActor;
242855
242930
  }
242856
242931
  /** 该成员是不是公司最后一个 owner(最后一个 owner 立即阻断,先于任何交接讨论)。 */
242857
242932
  async isLastOwner(companyId, accountId) {
@@ -242877,14 +242952,13 @@ var init_offboarding = __esm({
242877
242952
  */
242878
242953
  async blockersForRemoval(companyId, accountId) {
242879
242954
  const blockers = [];
242880
- const member = await this.store.getMember(companyId, accountId);
242955
+ const members = await this.store.listMembers(companyId);
242956
+ const member = members.find((m2) => m2.accountId === accountId);
242881
242957
  if (!member) {
242882
242958
  return [{ code: "NOT_FOUND", message: `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}` }];
242883
242959
  }
242884
242960
  if (member.role === "owner") {
242885
242961
  blockers.push({ code: "OWNER_PROTECTED", message: OWNER_PROTECTED_MESSAGE });
242886
- } else if (await this.isLastOwner(companyId, accountId)) {
242887
- blockers.push({ code: "LAST_OWNER", message: "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner" });
242888
242962
  }
242889
242963
  return blockers;
242890
242964
  }
@@ -242953,12 +243027,9 @@ var init_offboarding = __esm({
242953
243027
  if (op.version !== expectedVersion) {
242954
243028
  throw new CompanyError(409, "STALE_PLAN", `\u8BA1\u5212\u7248\u672C\u5DF2\u53D8\uFF08\u5F53\u524D ${op.version}\uFF0C\u4F60\u7ED9\u7684\u662F ${expectedVersion}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D6\u8BA1\u5212`);
242955
243029
  }
242956
- const target = await this.store.getMember(companyId, accountId);
243030
+ const members = await this.store.listMembers(companyId);
243031
+ const target = members.find((m2) => m2.accountId === accountId);
242957
243032
  if (target?.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
242958
- if (await this.isLastOwner(companyId, accountId)) {
242959
- throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
242960
- }
242961
- const released = await this.releaseOwnedAgents(companyId, accountId);
242962
243033
  const remaining = await this.blockersForRemoval(companyId, accountId);
242963
243034
  if (remaining.length > 0) {
242964
243035
  await this.store.updateMemberOffboardingIfVersion(
@@ -242970,7 +243041,7 @@ var init_offboarding = __esm({
242970
243041
  );
242971
243042
  throw new CompanyError(409, remaining[0].code, `\u4ECD\u6709\u672A\u4EA4\u63A5\u9879\uFF1A${remaining.map((b2) => b2.message).join("; ")}`);
242972
243043
  }
242973
- await this.applyMemberRemoval(companyId, accountId);
243044
+ const released = await this.applyMemberRemoval(companyId, accountId);
242974
243045
  await this.store.updateMemberOffboardingIfVersion(
242975
243046
  companyId,
242976
243047
  operationId,
@@ -242981,35 +243052,63 @@ var init_offboarding = __esm({
242981
243052
  return { status: "removed", transferred: [], left: released };
242982
243053
  }
242983
243054
  /**
242984
- * 移除一条成员关系的**全套副作用**,顺序固定,三条移除路径共用:
242985
- * ① 清名下 agent 的负责人字段(`digital_employee.owner` 的复合 FK 是 ON DELETE RESTRICT,
242986
- * 不清就是一个裸 FK 违例);**只清 owner**——不改状态、不撤凭证、不解绑助理(PRD v1.5);
242987
- * ② 停用花名册里这个人(**还在别家公司在职就不停用**——停用打的是全局那一行);
242988
- * ③ 删 human principal bridge(对 company_member 是 CASCADE,但文件/内存实现没有级联,
242989
- * 显式删一次让「桥随主体走」在应用层也成立);
242990
- * ④ 删成员行(走 `removeMemberIfNotLastOwner`,组织管理员那一位在这里被纵深防御挡住)。
243055
+ * 移除一条成员关系的**全套副作用**,三条移除路径共用(管理员移除他人 `commit`、
243056
+ * 本人退出组织 `DELETE …/members/me`、注销账号 `DELETE /api/accounts/me`)。
242991
243057
  *
242992
- * 顺序不能反:成员行一删,本公司的 scoped registry 就不再认这个 actor,②会静默 no-op。
242993
- * 任何一步失败都停在「人已停用、成员还在」——可重试,每一步都幂等。
243058
+ * 顺序与原子性(2026-09-23 代码评审打回后重排):
243059
+ * ① 读一遍成员行:不存在 → 404;**已经是组织管理员 → 当场 403,一个副作用都不做**(D5.2)。
243060
+ * ② 停用花名册里这个人(**还在别家公司在职就不停用**——停用打的是全局那一行)。
243061
+ * 这一步必须在③之前:成员行一删,本公司的 scoped registry 就不再认这个 actor,
243062
+ * 停用会静默 no-op,人被移除了却还挂在花名册上(QA D3 修过一次的病)。
243063
+ * ③ **一个事务**做完控制面三件事:锁内复核目标不是组织管理员 → 清名下 agent 的负责人字段
243064
+ * (`digital_employee.owner` 的复合 FK 是 ON DELETE RESTRICT,不清就是裸 FK 违例;
243065
+ * **只清 owner**,不改状态、不撤凭证、不解绑助理,PRD v1.5)→ 删 human principal bridge
243066
+ * → 删成员行(`store.removeMemberAndReleaseOwnedAgents`)。
243067
+ * ④ ③失败(并发转交把这个人升成了组织管理员,A09)→ 把②CAS 还原回去。
242994
243068
  *
242995
- * 三个调用点:管理员移除他人(`commit`)、本人退出组织(`DELETE …/members/me`)、
242996
- * 注销账号(逐家执行,`DELETE /api/accounts/me`)。
243069
+ * **为什么③非要是一个事务**:以前①②③是「先清负责人、再停用、再删桥,最后才去拿锁删成员行」
243070
+ * 四段独立的写。A09 那条并发(`O` 转交给 `A1` 的同时另一位管理员删 `A1`)走到最后一步才被
243071
+ * `OWNER_PROTECTED` 拒——而此刻 `A1` 名下 Agent 的负责人已经被清空、桥已经被删、人已经被停用。
243072
+ * 请求失败了,破坏却留下了:新上任的组织管理员丢了名下全部 Agent 的归属、还从花名册上消失。
243073
+ * 现在控制面那三件事共一把锁(与转交锁同一组行),失败时一行都没写;跨不进这把锁的只有员工侧
243074
+ * 停用,用 CAS 补偿还原——「停用之后有别人写过这个 actor」就跳过,不撤销他人的合法写(QA R9)。
242997
243075
  */
242998
243076
  async applyMemberRemoval(companyId, accountId) {
242999
- const released = await this.releaseOwnedAgents(companyId, accountId);
243000
243077
  const member = await this.store.getMember(companyId, accountId);
243001
- if (member && !await isEmployedElsewhere(this.store, companyId, accountId)) {
243002
- await this.disableActor?.(companyId, member.actorId);
243078
+ if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243079
+ if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
243080
+ let ticket = null;
243081
+ if (!await isEmployedElsewhere(this.store, companyId, accountId)) {
243082
+ ticket = await this.disableActor?.(companyId, member.actorId) ?? null;
243003
243083
  }
243004
- await this.store.deletePrincipalBridge(companyId, "human", accountId);
243005
- const removed = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
243084
+ const removed = await this.store.removeMemberAndReleaseOwnedAgents(companyId, accountId, this.now()).catch(async (e) => {
243085
+ await this.compensateDisable(companyId, ticket);
243086
+ throw e;
243087
+ });
243006
243088
  if (!removed.ok) {
243089
+ await this.compensateDisable(companyId, ticket);
243007
243090
  if (removed.reason === "not_found") {
243008
243091
  throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243009
243092
  }
243010
243093
  throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
243011
243094
  }
243012
- return released;
243095
+ return removed.releasedAgents;
243096
+ }
243097
+ /**
243098
+ * 把②那次停用还原回去。**只还原本次自己造成的那一次**:`restoreActor` 走 CAS,
243099
+ * 停用之后但凡有别人写过这个 actor 就跳过——留下「成员还在 + actor 已停用」这个合法终态
243100
+ * (与员工页 `DELETE /api/actors/:id` 的终态同构),比静默撤销他人的合法停用安全。
243101
+ * 补偿本身失败不改写主错误:调用方要看到的是「为什么没删成」,不是补偿的二级故障。
243102
+ */
243103
+ async compensateDisable(companyId, ticket) {
243104
+ if (!ticket || !this.restoreActor) return;
243105
+ try {
243106
+ await this.restoreActor(companyId, ticket);
243107
+ } catch (e) {
243108
+ console.error(
243109
+ `[offboarding] restore actor ${ticket.actorId} failed after removal was rejected: ${e instanceof Error ? e.message : String(e)}`
243110
+ );
243111
+ }
243013
243112
  }
243014
243113
  /**
243015
243114
  * 把该成员名下所有 agent 的负责人字段清空,返回被清的 agent id。
@@ -243098,10 +243197,18 @@ function companiesDomain(opts) {
243098
243197
  }
243099
243198
  return n;
243100
243199
  };
243200
+ const requireActiveCompany = async (companyId) => {
243201
+ const company = await service.get(companyId);
243202
+ if (!company || company.status !== "active") {
243203
+ throw new ApiError(404, "COMPANY_NOT_FOUND", `\u7EC4\u7EC7\u4E0D\u5B58\u5728\u6216\u5DF2\u4E0D\u53EF\u7528\uFF1A${companyId}`);
243204
+ }
243205
+ };
243101
243206
  const requireOwnerOrAdmin = async (req, companyId) => {
243102
243207
  const me = await whoami(req);
243103
243208
  const member = await service.getMember(companyId, me.accountId);
243104
- if (!member || member.role !== "owner" && member.role !== "admin") {
243209
+ if (!member) throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
243210
+ await requireActiveCompany(companyId);
243211
+ if (member.role !== "owner" && member.role !== "admin") {
243105
243212
  throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
243106
243213
  }
243107
243214
  };
@@ -243109,6 +243216,7 @@ function companiesDomain(opts) {
243109
243216
  const me = await whoami(req);
243110
243217
  const member = await service.getMember(companyId, me.accountId);
243111
243218
  if (!member) throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
243219
+ await requireActiveCompany(companyId);
243112
243220
  return me;
243113
243221
  };
243114
243222
  return (router) => {
@@ -243232,7 +243340,6 @@ function companiesDomain(opts) {
243232
243340
  const aid = req.params.accountId;
243233
243341
  await requireOwnerOrAdmin(req, cid);
243234
243342
  const member = await service.peekRemovableMember(cid, aid).catch(mapErr);
243235
- if (offboarding) await offboarding.releaseOwnedAgents(cid, aid).catch(mapErr);
243236
243343
  let actorsSvc;
243237
243344
  let ownedDisableBefore = null;
243238
243345
  let disableSeq = 0;
@@ -243628,9 +243735,30 @@ function createCompaniesDomain(opts) {
243628
243735
  ...opts.resolveActorsCtx !== void 0 ? {
243629
243736
  // 未认领转 left 的 agent、以及 commit 移除掉的那位成员,都要把员工侧 ActorRecord
243630
243737
  // 一并软停用——否则控制面说「已离开/已移除」、花名册还显示在岗,两个事实源当场分叉。
243738
+ //
243739
+ // 用 `disableActorIfCurrentlyActive` 而不是 `disableActor`,并把 `{before, seq}` 回给
243740
+ // offboarding:只有**真的把这个 actor 从 active 拉到 disabled** 的那一次调用才拿得到
243741
+ // 返回值,也才「拥有」这次状态变更;已被并发请求或员工页先停用的一律 ok:false,
243742
+ // 本次就不 own、移除失败也不补偿(QA R10 的判据,两条移除路径同一套)。
243631
243743
  disableActor: async (companyId, actorId) => {
243632
243744
  const ctx = await opts.resolveActorsCtx(companyId);
243633
- await ctx.service.disableActorIfCurrentlyActive(actorId, "system:offboarding");
243745
+ const res = await ctx.service.disableActorIfCurrentlyActive(actorId, "system:offboarding");
243746
+ return res.ok ? { actorId, before: res.before, seq: res.seq } : null;
243747
+ },
243748
+ // 移除最终失败时的补偿:**CAS** 写回停用前那一行。`expectedSeq` 对不上(停用之后
243749
+ // 有任何第三方写过这个 actor)就跳过——不静默撤销他人的合法停用 / 改名(QA R9)。
243750
+ restoreActor: async (companyId, ticket) => {
243751
+ const ctx = await opts.resolveActorsCtx(companyId);
243752
+ const res = await ctx.service.casUpsertActor(
243753
+ { ...ticket.before, status: "active" },
243754
+ ticket.seq,
243755
+ "system:offboarding"
243756
+ );
243757
+ if (!res.ok) {
243758
+ console.info(
243759
+ `[companies] restore skipped for ${ticket.actorId}: another write intervened after the disable`
243760
+ );
243761
+ }
243634
243762
  }
243635
243763
  } : {}
243636
243764
  });
@@ -260652,7 +260780,7 @@ async function ensureOneOwnerInvariant(pool, s2) {
260652
260780
  );
260653
260781
  }
260654
260782
  }
260655
- var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToRoleMigration, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
260783
+ var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToRoleMigration, migrationRow2, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
260656
260784
  var init_postgres_control_plane = __esm({
260657
260785
  "../storage/src/postgres-control-plane.ts"() {
260658
260786
  "use strict";
@@ -261043,6 +261171,48 @@ var init_postgres_control_plane = __esm({
261043
261171
  client.release();
261044
261172
  }
261045
261173
  }
261174
+ async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
261175
+ const client = await this.pool.connect();
261176
+ try {
261177
+ await client.query("BEGIN");
261178
+ const all2 = await client.query(
261179
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
261180
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
261181
+ [companyId]
261182
+ );
261183
+ const target = all2.rows.map(rowToMember).find((m2) => m2.accountId === accountId);
261184
+ if (!target) {
261185
+ await client.query("ROLLBACK");
261186
+ return { ok: false, reason: "not_found" };
261187
+ }
261188
+ if (target.role === "owner") {
261189
+ await client.query("ROLLBACK");
261190
+ return { ok: false, reason: "owner_protected" };
261191
+ }
261192
+ const released = await client.query(
261193
+ `UPDATE ${this.s}.digital_employees SET owner_account_id = NULL, updated_at = $3
261194
+ WHERE company_id = $1 AND owner_account_id = $2
261195
+ RETURNING id`,
261196
+ [companyId, accountId, at2]
261197
+ );
261198
+ await client.query(
261199
+ `DELETE FROM ${this.s}.principal_bridge WHERE company_id = $1 AND principal_type = 'human' AND principal_id = $2`,
261200
+ [companyId, accountId]
261201
+ );
261202
+ await client.query(
261203
+ `DELETE FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2`,
261204
+ [companyId, accountId]
261205
+ );
261206
+ await client.query("COMMIT");
261207
+ return { ok: true, member: target, releasedAgents: released.rows.map((r) => r.id) };
261208
+ } catch (e) {
261209
+ await client.query("ROLLBACK").catch(() => {
261210
+ });
261211
+ throw e;
261212
+ } finally {
261213
+ client.release();
261214
+ }
261215
+ }
261046
261216
  async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
261047
261217
  const client = await this.pool.connect();
261048
261218
  try {
@@ -261111,7 +261281,7 @@ var init_postgres_control_plane = __esm({
261111
261281
  `SELECT status FROM ${this.s}.accounts WHERE id = $1`,
261112
261282
  [toAccountId]
261113
261283
  );
261114
- if (acc.rows[0] && acc.rows[0].status !== "active") {
261284
+ if (!acc.rows[0] || acc.rows[0].status !== "active") {
261115
261285
  await client.query("ROLLBACK");
261116
261286
  return { ok: false, reason: "target_inactive" };
261117
261287
  }
@@ -261148,6 +261318,73 @@ var init_postgres_control_plane = __esm({
261148
261318
  client.release();
261149
261319
  }
261150
261320
  }
261321
+ // —— 存量身份映射(ADR D3)——
261322
+ async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
261323
+ const client = await this.pool.connect();
261324
+ try {
261325
+ await client.query("BEGIN");
261326
+ const all2 = await client.query(
261327
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
261328
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
261329
+ [companyId]
261330
+ );
261331
+ const rows = all2.rows.map(rowToMember);
261332
+ const target = rows.find((m2) => m2.accountId === toAccountId);
261333
+ if (!target) {
261334
+ await client.query("ROLLBACK");
261335
+ return { ok: false, reason: "not_found" };
261336
+ }
261337
+ const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
261338
+ if (others.length === 0 && target.role === "owner") {
261339
+ await client.query("COMMIT");
261340
+ return { ok: true, owner: target, demoted: [], auditRows: [] };
261341
+ }
261342
+ if (others.length > 0) {
261343
+ await client.query(
261344
+ `UPDATE ${this.s}.company_members SET role = 'admin' WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
261345
+ [companyId, toAccountId]
261346
+ );
261347
+ }
261348
+ if (target.role !== "owner") {
261349
+ await client.query(
261350
+ `UPDATE ${this.s}.company_members SET role = 'owner' WHERE company_id = $1 AND account_id = $2`,
261351
+ [companyId, toAccountId]
261352
+ );
261353
+ }
261354
+ const auditRows = [
261355
+ ...others.map((m2) => migrationRow2(companyId, m2.accountId, "owner", "admin", audit2)),
261356
+ ...target.role !== "owner" ? [migrationRow2(companyId, toAccountId, target.role, "owner", audit2)] : []
261357
+ ];
261358
+ for (const r of auditRows) {
261359
+ await client.query(
261360
+ `INSERT INTO ${this.s}.company_member_role_migrations
261361
+ (company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
261362
+ VALUES ($1,$2,$3,$4,$5,$6,$7)`,
261363
+ [r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
261364
+ );
261365
+ }
261366
+ await client.query("COMMIT");
261367
+ return {
261368
+ ok: true,
261369
+ owner: { ...target, role: "owner" },
261370
+ demoted: others.map((m2) => ({ ...m2, role: "admin" })),
261371
+ auditRows
261372
+ };
261373
+ } catch (e) {
261374
+ await client.query("ROLLBACK").catch(() => {
261375
+ });
261376
+ throw e;
261377
+ } finally {
261378
+ client.release();
261379
+ }
261380
+ }
261381
+ async setMemberRoleOnly(companyId, accountId, role) {
261382
+ const r = await this.pool.query(
261383
+ `UPDATE ${this.s}.company_members SET role = $3 WHERE company_id = $1 AND account_id = $2`,
261384
+ [companyId, accountId, role]
261385
+ );
261386
+ return (r.rowCount ?? 0) > 0;
261387
+ }
261151
261388
  // —— 存量身份映射审计(ADR D3.4)——
261152
261389
  async appendMemberRoleMigrations(rows) {
261153
261390
  if (rows.length === 0) return;
@@ -261661,6 +261898,15 @@ var init_postgres_control_plane = __esm({
261661
261898
  ...row.manifest_version != null ? { manifestVersion: row.manifest_version } : {},
261662
261899
  appliedAt: toIso(row.applied_at)
261663
261900
  });
261901
+ migrationRow2 = (companyId, accountId, roleBefore, roleAfter, audit2) => ({
261902
+ companyId,
261903
+ accountId,
261904
+ roleBefore,
261905
+ roleAfter,
261906
+ evidence: audit2.evidence,
261907
+ ...audit2.evidence === "manifest" && audit2.manifestVersion !== void 0 ? { manifestVersion: audit2.manifestVersion } : {},
261908
+ appliedAt: audit2.appliedAt
261909
+ });
261664
261910
  rowToMember = (row) => ({
261665
261911
  companyId: row.company_id,
261666
261912
  accountId: row.account_id,
@@ -293490,7 +293736,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
293490
293736
  }
293491
293737
  }
293492
293738
  }
293493
- var CLI_VERSION = "2.2.81" ? "2.2.81" : "dev";
293739
+ var CLI_VERSION = "2.2.82" ? "2.2.82" : "dev";
293494
293740
  function apiHeaders(token, company) {
293495
293741
  const headers = {
293496
293742
  authorization: `Bearer ${token}`,
@@ -295287,7 +295533,13 @@ ${res.warning}`);
295287
295533
  if (live.length > 0) {
295288
295534
  for (const r of live) println(`[design] \u5F15\u64CE\u5728\u8DD1\uFF1Arun ${r.runId.slice(0, 8)} \u72B6\u6001=${r.status}\u2014\u2014\u522B\u518D\u8D77\u65B0\u8F6E\uFF1B\u8FC7\u51E0\u5206\u949F\u518D status\uFF0C\u6216\u672C\u8F6E\u6536\u5DE5\u7B49\u91CD\u6D3E\u3002`);
295289
295535
  } else if (st.runs.length > 0) {
295290
- for (const r of st.runs.slice(0, 3)) println(`[design] \u8FD1\u671F run\uFF1A${r.runId.slice(0, 8)} \u7EC8\u6001=${r.status}`);
295536
+ for (const r of st.runs.slice(0, 3)) {
295537
+ println(`[design] \u8FD1\u671F run\uFF1A${r.runId.slice(0, 8)} \u7EC8\u6001=${r.status}${r.agentId ? ` \u5F15\u64CE=${r.agentId}` : ""}`);
295538
+ if (r.status === "failed" && r.error) {
295539
+ const reason = r.error.length > 300 ? `${r.error.slice(0, 300)}\u2026` : r.error;
295540
+ println(`[design] \u5931\u8D25\u539F\u56E0\uFF08od \u539F\u6587\uFF09\uFF1A${reason}`);
295541
+ }
295542
+ }
295291
295543
  } else {
295292
295544
  println(`[design] \u65E0\u5728\u4E16 run \u8BB0\u5F55\uFF08od \u91CD\u542F\u540E\u4E3A\u7A7A\uFF0C\u5C5E\u6B63\u5E38\uFF09\u2014\u2014\u770B\u4E0B\u9762\u677F\u4E0A\u4EA7\u7269\u5373\u6301\u4E45\u771F\u76F8\u3002`);
295293
295545
  }
@@ -295299,7 +295551,7 @@ ${res.warning}`);
295299
295551
  for (const a of st.artifacts) println(` - ${a.path}\uFF08${a.kind ?? "?"}\uFF09status=${a.status ?? "?"}${a.mtime ? ` \xB7 ${new Date(a.mtime).toISOString()}` : ""}`);
295300
295552
  if (flags.get("pull") === "true") {
295301
295553
  const odPublic = (flags.get("od-public-url") ?? process.env["OASIS_OPEN_DESIGN_PUBLIC_URL"] ?? "https://design.open-friday.com").replace(/\/$/, "");
295302
- const { dir: dir2, entry } = await harvestBoard(odBase, odFetch, odPublic, os13.tmpdir(), st.boardKey, st.runs[0]?.runId ?? "status-pull");
295554
+ const { dir: dir2, entry } = await harvestBoard(odBase, odFetch, odPublic, os13.tmpdir(), st.boardKey, latestSucceededRun(st.runs)?.runId ?? "status-pull");
295303
295555
  println(`[design] \u5DF2\u62C9\u5230\u672C\u5730\uFF1A${dir2}\uFF08\u5165\u53E3 ${entry}\uFF09`);
295304
295556
  println(`[design] \u8BFB\u5B8C\u6EE1\u610F\u5373\u4EA4\u7A3F\uFF1Aoasis propose ${artifactId} --from-dir ${dir2} --reason "<\u8FD9\u7248\u505A\u4E86\u4EC0\u4E48>"`);
295305
295557
  } else {
@@ -297400,7 +297652,7 @@ function shimScript() {
297400
297652
  }
297401
297653
 
297402
297654
  // src/index.ts
297403
- var PKG_VERSION = true ? "2.2.81" : "dev";
297655
+ var PKG_VERSION = true ? "2.2.82" : "dev";
297404
297656
  var LOCAL_BIN = localBin();
297405
297657
  var NPM_PREFIX = npmPrefix();
297406
297658
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.81",
3
+ "version": "2.2.82",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "3aab21cfa53fd28b194371066125b2b81c9f846d"
29
+ "sourceHead": "6a195fdb899ac4fe62d0cb8a164be7840880a6a7"
30
30
  }
31
31
  }