oasis_test_v2 2.2.10 → 2.2.11

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 +516 -322
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4666,8 +4666,6 @@ var init_kill = __esm({
4666
4666
  function scan(state, ctx) {
4667
4667
  if (state.workorder.dispatchPaused) return { events: [], terminated: false };
4668
4668
  const events = [];
4669
- const now = ctx.at;
4670
- const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
4671
4669
  for (const node2 of state.liveNodes()) {
4672
4670
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
4673
4671
  if (!lw) {
@@ -4701,12 +4699,7 @@ function scan(state, ctx) {
4701
4699
  continue;
4702
4700
  }
4703
4701
  if (wstate === "running") {
4704
- const start = lw.lastActivityAt ?? lw.startedAt ?? lw.createdAt;
4705
- const elapsed = new Date(now).getTime() - new Date(start).getTime();
4706
- const wcell = policy.resolve({ executor: classify(node2.assigneeActorId), scenario: "work" });
4707
- if (wcell.timeoutAction.kind === "kill-rebuild" && elapsed > wcell.deadlineMs) {
4708
- events.push({ kind: "work.timeout", workorderId: state.workorder.id, workId: lw.id });
4709
- }
4702
+ checkRunningWorkTimeout(state, node2, lw, ctx, events);
4710
4703
  continue;
4711
4704
  }
4712
4705
  const blockers = scanBlockersForNode(state, node2, lw);
@@ -4771,10 +4764,9 @@ function scan(state, ctx) {
4771
4764
  workorderId: state.workorder.id,
4772
4765
  nodeId: node2.id,
4773
4766
  // ★ 合并写:保留 source ∈ {type, manual, model} 的原有行,覆写 source='closure' 的行。
4774
- // `plan.update_review_requirements` handler 是**全量替换**(killNodeReviews + replaceRequirements),
4767
+ // `plan.update_review_requirements` 的名单仍是**全量替换**,
4775
4768
  // 所以必须把手动 / 类型 / 模型来源的行一并带上——漏一条即误删协调者手动指派的审核人。
4776
- // killNodeReviews 会杀本节点在跑的评审:candidate 分支进入时该节点尚未派出评审(reqs 为空
4777
- // or 只含 closure 之外的组),本轮内 kill 无影响。
4769
+ // 总线为新事件写 preserveReviews:未变组的当前审核继续,只有新增 closure 组待派。
4778
4770
  reviewers: mergedClosureRequirements(state, node2.id, closureActors)
4779
4771
  });
4780
4772
  continue;
@@ -4877,6 +4869,15 @@ function scan(state, ctx) {
4877
4869
  const terminated = events.length === 0 && checkTerminated(state);
4878
4870
  return { events, terminated };
4879
4871
  }
4872
+ function checkRunningWorkTimeout(state, node2, work, ctx, events) {
4873
+ const start = work.lastActivityAt ?? work.startedAt ?? work.createdAt;
4874
+ const elapsed = new Date(ctx.at).getTime() - new Date(start).getTime();
4875
+ const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
4876
+ const wcell = policy.resolve({ executor: classify(node2.assigneeActorId), scenario: "work" });
4877
+ if (wcell.timeoutAction.kind === "kill-rebuild" && elapsed > wcell.deadlineMs) {
4878
+ events.push({ kind: "work.timeout", workorderId: state.workorder.id, workId: work.id });
4879
+ }
4880
+ }
4880
4881
  function openCommentBlockers(state, node2) {
4881
4882
  if (!node2.assigneeActorId) return [];
4882
4883
  return state.allIssues().filter((i) => i.kind === "comment" && i.resolvedAt === null && i.aboutNodeId === node2.id).map((i) => ({
@@ -4961,6 +4962,13 @@ function tryToRun(state, nodeId, ctx, events) {
4961
4962
  const node2 = state.node(nodeId);
4962
4963
  if (!node2) return;
4963
4964
  if (!node2.assigneeActorId) return;
4965
+ const latest = (node2.latestWorkId ? state.work(node2.latestWorkId) : null) ?? null;
4966
+ if (unexemptedVetoBlockers(scanBlockersForNode(state, node2, latest), "dispatch").length > 0) {
4967
+ if (latest && workState(latest, hasOutput(latest, state.artifactsOf(latest.id).length)) === "running") {
4968
+ checkRunningWorkTimeout(state, node2, latest, ctx, events);
4969
+ }
4970
+ return;
4971
+ }
4964
4972
  const inbound = state.edgesInto(nodeId);
4965
4973
  for (const e of inbound) {
4966
4974
  if (e.kind !== "data" || !e.required) continue;
@@ -12011,7 +12019,7 @@ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
12011
12019
  }
12012
12020
  }
12013
12021
  }
12014
- var planUpdateSpec, planAddNode, planDeleteNode, planAddEdge, planDeleteEdge, planAssignActor, planUpdateReviewRequirements, planNodeRetry, planUpdateFields, planApply;
12022
+ var planUpdateSpec, planAddNode, planDeleteNode, planAddEdge, planDeleteEdge, planAssignActor, reviewerKey, planUpdateReviewRequirements, planNodeRetry, planUpdateFields, planApply;
12015
12023
  var init_plan = __esm({
12016
12024
  "../engine/src/handlers/plan.ts"() {
12017
12025
  "use strict";
@@ -12134,11 +12142,51 @@ var init_plan = __esm({
12134
12142
  });
12135
12143
  }
12136
12144
  };
12145
+ reviewerKey = (r) => JSON.stringify([r.reviewerActorId, r.reviewGroup]);
12137
12146
  planUpdateReviewRequirements = {
12138
12147
  name: "plan/update-review-requirements",
12139
12148
  kind: "plan.update_review_requirements",
12140
12149
  apply(e, state, ctx) {
12141
- if (!state.node(e.nodeId)) return;
12150
+ const node2 = state.node(e.nodeId);
12151
+ if (!node2) return;
12152
+ if (e.preserveReviews) {
12153
+ const previous3 = state.requirementsOf(e.nodeId);
12154
+ const previousByKey = new Map(previous3.map((r) => [reviewerKey(r), r]));
12155
+ const nextByKey = new Map(e.reviewers.map((r) => [reviewerKey(r), r]));
12156
+ const membershipChanged = previousByKey.size !== nextByKey.size || [...previousByKey.keys()].some((key) => !nextByKey.has(key));
12157
+ const currentByGroup = /* @__PURE__ */ new Map();
12158
+ for (const req of previous3) {
12159
+ const r = req.latestReviewId ? state.review(req.latestReviewId) : void 0;
12160
+ if (r && r.nodeId === e.nodeId && r.targetWorkId === node2.latestProposedWorkId && r.reviewGroup === req.reviewGroup && previousByKey.has(reviewerKey(r)) && nextByKey.has(reviewerKey(r))) {
12161
+ currentByGroup.set(req.reviewGroup, r.id);
12162
+ }
12163
+ }
12164
+ const next = [...nextByKey.values()].map((r) => {
12165
+ const old = previousByKey.get(reviewerKey(r));
12166
+ return {
12167
+ nodeId: e.nodeId,
12168
+ reviewerActorId: r.reviewerActorId,
12169
+ reviewGroup: r.reviewGroup,
12170
+ source: r.source,
12171
+ assignedBy: old?.assignedBy ?? ctx.actorId,
12172
+ // assignedAt 同时是历史判决回填的名单纪元。名单变更必须推进,避免拿缩减后的组集
12173
+ // 去补判旧 work;本轮有效票由 durable 指针保留,不靠时间戳继承(ADR 0533)。
12174
+ assignedAt: membershipChanged ? ctx.at : old?.assignedAt ?? ctx.at,
12175
+ latestReviewId: currentByGroup.get(r.reviewGroup) ?? null
12176
+ };
12177
+ });
12178
+ for (const r of state.allReviews()) {
12179
+ if (r.nodeId === e.nodeId && !nextByKey.has(reviewerKey(r)) && isReviewKillable(r)) {
12180
+ killReviewRow(state, r, ctx.at);
12181
+ }
12182
+ }
12183
+ const unchanged = previous3.length === next.length && next.every((r) => {
12184
+ const old = previousByKey.get(reviewerKey(r));
12185
+ return old?.source === r.source && old.latestReviewId === r.latestReviewId;
12186
+ });
12187
+ if (!unchanged) state.replaceRequirements(e.nodeId, next);
12188
+ return;
12189
+ }
12142
12190
  killNodeReviews(state, e.nodeId, ctx.at);
12143
12191
  state.replaceRequirements(e.nodeId, e.reviewers.map((r) => ({
12144
12192
  nodeId: e.nodeId,
@@ -12149,6 +12197,9 @@ var init_plan = __esm({
12149
12197
  assignedAt: ctx.at,
12150
12198
  latestReviewId: null
12151
12199
  })));
12200
+ },
12201
+ async effect(e, ctx) {
12202
+ if (e.preserveReviews) await ctx.cancelKilledSessions(e.workorderId, { nodeId: e.nodeId, retained: e.reviewers });
12152
12203
  }
12153
12204
  };
12154
12205
  planNodeRetry = {
@@ -13236,6 +13287,8 @@ function versionCommandEvent(event) {
13236
13287
  return { ...event, semanticsVersion: 3 };
13237
13288
  }
13238
13289
  switch (event.kind) {
13290
+ case "plan.update_review_requirements":
13291
+ return { ...event, preserveReviews: true };
13239
13292
  case "work.accept":
13240
13293
  case "work.kill":
13241
13294
  case "work.handoff_recorded":
@@ -13801,7 +13854,7 @@ var init_bus = __esm({
13801
13854
  // ★ 设计 §workorder.sealed/paused「按 work.kill 逻辑杀」:killAllWorks 只置状态,会话取消在这里做——
13802
13855
  // apply 已把被杀 work/review 置 deadAt/cancelledAt,这里按 store 找它们仍挂着的会话 cancel
13803
13856
  //(deadAt 且 endedAt 未置 = 刚被杀未退出的在途会话)。没会话 / 已退 → no-op。
13804
- cancelKilledSessions: async () => {
13857
+ cancelKilledSessions: async (_workorderId, reviewScope) => {
13805
13858
  let snap;
13806
13859
  try {
13807
13860
  snap = await this.store.transaction((tx) => tx.loadWorkorder(record8.workorderId));
@@ -13817,10 +13870,13 @@ var init_bus = __esm({
13817
13870
  console.warn(`[bus] cancelKilledSessions ${what} ${id}: ${String(err)}`);
13818
13871
  }
13819
13872
  };
13820
- for (const w2 of snap.works) {
13873
+ for (const w2 of reviewScope === void 0 ? snap.works : []) {
13821
13874
  if (w2.deadAt && w2.endedAt === null) await cancel(w2.id, "work");
13822
13875
  }
13823
13876
  for (const r of snap.reviews) {
13877
+ if (reviewScope && (r.nodeId !== reviewScope.nodeId || reviewScope.retained.some(
13878
+ (req) => req.reviewerActorId === r.reviewerActorId && req.reviewGroup === r.reviewGroup
13879
+ ))) continue;
13824
13880
  if (r.cancelledAt && r.endedAt === null) await cancel(r.id, "review");
13825
13881
  }
13826
13882
  },
@@ -16273,6 +16329,10 @@ var init_dispatcher = __esm({
16273
16329
  // 凭证是执行者属性,要人改(等待策略:熔断)
16274
16330
  [NO_BINDING_REASON]: "A",
16275
16331
  // 僵尸账号:只有人给它一个有效绑定才可能变
16332
+ // 绑到了**别家公司**的机器上(ADR-0164 §5 的租户闸)。与上一条同构:绑定本身就是错的,
16333
+ // 只有人改绑才可能变——而「改绑」正是 A 轴的第一条触发(executorSignature 含 nodeId),
16334
+ // 改完自动复位、不必再等一次人工干预。落 C 也不算错(安全默认),但会让人多做一步。
16335
+ "node-not-in-company": "A",
16276
16336
  "clean": "B",
16277
16337
  // 机器好好的、就是没交东西 → 换个东西给它做才有意义
16278
16338
  "timeout": "B",
@@ -21195,16 +21255,18 @@ var init_kernel_bridge = __esm({
21195
21255
  const artId = op.artifactId;
21196
21256
  const opReviewers = op.reviewers;
21197
21257
  const editReviewRoles = op.reviewRoles;
21258
+ const closureReviewers = (artifacts.get(artId)?.reviewers ?? []).filter((r) => r.source === "closure");
21198
21259
  if (artId && (opReviewers || editReviewRoles !== void 0)) {
21199
21260
  const art = artifacts.get(artId);
21200
21261
  const effectiveRoles = editReviewRoles !== void 0 ? editReviewRoles === null ? void 0 : editReviewRoles : art?.reviewRoles;
21201
- const effectiveExplicit = opReviewers ?? (art?.reviewers ?? []).map((r) => ({ actor: r.actor, source: r.source }));
21262
+ const effectiveExplicit = opReviewers ?? (art?.reviewers ?? []).filter((r) => r.source !== "closure").map((r) => ({ actor: r.actor, source: r.source }));
21202
21263
  const { reviewers: nextReqs, staffingIssues } = resolveNodeReviewers({
21203
21264
  typeName: art?.type ?? "",
21204
21265
  typeDef: art ? this.schema.get(art.type) : void 0,
21205
21266
  ...effectiveRoles !== void 0 ? { reviewRoles: effectiveRoles } : {},
21206
21267
  explicitReviewers: effectiveExplicit,
21207
- roleIndex: this.roleIndex
21268
+ roleIndex: this.roleIndex,
21269
+ closureGate: closureReviewers.map((r) => r.actor)
21208
21270
  });
21209
21271
  for (const issue2 of staffingIssues) {
21210
21272
  reviewerStaffing.push({ nodeId: artId, type: art?.type ?? "", role: issue2.role, from: issue2.from });
@@ -21263,7 +21325,7 @@ var init_kernel_bridge = __esm({
21263
21325
  ...opDescription !== void 0 ? { description: opDescription } : {},
21264
21326
  ...fields ? { fields: { ...previous3.fields, ...fields } } : {},
21265
21327
  ...editReviewRoles !== void 0 ? { reviewRoles: editReviewRoles ?? void 0 } : {},
21266
- ...opReviewers ? { reviewers: opReviewers } : {}
21328
+ ...opReviewers ? { reviewers: [...opReviewers, ...closureReviewers] } : {}
21267
21329
  });
21268
21330
  applied++;
21269
21331
  break;
@@ -29324,6 +29386,8 @@ function runBackgroundAssistantChatTurn(deps) {
29324
29386
  ...assistantMsgId ? { messageId: assistantMsgId } : {},
29325
29387
  log: log3
29326
29388
  });
29389
+ const liveTurn = deps.liveChat && deps.liveHandle ? deps.liveChat.start(deps.chatSessionId, deps.liveHandle, { items: itemLedger }) : void 0;
29390
+ if (assistantMsgId) liveTurn?.setAssistantMessageId(assistantMsgId);
29327
29391
  const normalizedSource = typeof deps.session.onNormalizedProviderEvent === "function" ? {
29328
29392
  onNormalizedProviderEvent: deps.session.onNormalizedProviderEvent.bind(deps.session),
29329
29393
  finishTurn: (signal) => deps.session.finishNormalizedTurn?.(signal)
@@ -29349,7 +29413,8 @@ function runBackgroundAssistantChatTurn(deps) {
29349
29413
  let ckptDirty = false;
29350
29414
  normalizedSource.onNormalizedProviderEvent((event) => {
29351
29415
  try {
29352
- itemLedger.apply(event);
29416
+ if (liveTurn) liveTurn.applyNormalizedEvent(event);
29417
+ else itemLedger.apply(event);
29353
29418
  } catch {
29354
29419
  }
29355
29420
  ckptDirty = true;
@@ -29387,34 +29452,59 @@ function runBackgroundAssistantChatTurn(deps) {
29387
29452
  }
29388
29453
  const finalize = async (status) => {
29389
29454
  if (ckptTimer) clearInterval(ckptTimer);
29390
- await checkpointChain.catch(() => void 0);
29391
- normalizedSource.finishTurn(status === "done" ? "completed" : "failed");
29392
- itemLedger.finish(status === "done" ? "completed" : "failed");
29393
- await itemLedger.drain();
29394
- const projectedContent = itemLedger.projectContent();
29395
- const parts = collectedParts();
29396
- await finalizeAssistantRow({
29397
- chatStore: deps.chatStore,
29398
- chatSessionId: deps.chatSessionId,
29399
- assistantMsgId,
29400
- content: projectedContent,
29401
- parts,
29402
- status,
29403
- ...runId ? { runId } : {},
29404
- now
29405
- });
29406
- if (typeof deps.chatStore.updateSession === "function") {
29407
- try {
29408
- await deps.chatStore.updateSession(deps.chatSessionId, {
29409
- runtimeSessionId: deps.session.nativeSessionId ?? null,
29410
- touchedAt: now()
29455
+ try {
29456
+ await checkpointChain.catch(() => void 0);
29457
+ normalizedSource.finishTurn(status === "done" ? "completed" : "failed");
29458
+ itemLedger.finish(status === "done" ? "completed" : "failed");
29459
+ await itemLedger.drain();
29460
+ const projectedContent = itemLedger.projectContent();
29461
+ const parts = collectedParts();
29462
+ await finalizeAssistantRow({
29463
+ chatStore: deps.chatStore,
29464
+ chatSessionId: deps.chatSessionId,
29465
+ assistantMsgId,
29466
+ content: projectedContent,
29467
+ parts,
29468
+ status,
29469
+ ...runId ? { runId } : {},
29470
+ now
29471
+ });
29472
+ if (typeof deps.chatStore.updateSession === "function") {
29473
+ try {
29474
+ await deps.chatStore.updateSession(deps.chatSessionId, {
29475
+ runtimeSessionId: deps.session.nativeSessionId ?? null,
29476
+ touchedAt: now()
29477
+ });
29478
+ } catch {
29479
+ }
29480
+ }
29481
+ if (liveTurn) {
29482
+ if (projectedContent) liveTurn.emit({ type: "text", text: projectedContent });
29483
+ liveTurn.emitLive({
29484
+ protocolVersion: 2,
29485
+ streamId: `background:${deps.chatSessionId}`,
29486
+ turnId: deps.turnId ?? fallbackTurnId(runId),
29487
+ itemId: `background:${assistantMsgId ?? runId}`,
29488
+ itemType: "message",
29489
+ operation: "completed",
29490
+ payload: { role: "assistant", text: projectedContent }
29411
29491
  });
29492
+ }
29493
+ try {
29494
+ deps.onSettled?.();
29412
29495
  } catch {
29413
29496
  }
29414
- }
29415
- try {
29416
- deps.onSettled?.();
29417
- } catch {
29497
+ } finally {
29498
+ liveTurn?.emit({ type: "done" });
29499
+ liveTurn?.emitLive({
29500
+ protocolVersion: 2,
29501
+ streamId: `background:${deps.chatSessionId}`,
29502
+ turnId: deps.turnId ?? fallbackTurnId(runId),
29503
+ itemId: "background:terminal",
29504
+ itemType: "control",
29505
+ operation: status === "done" ? "turn_completed" : "turn_failed"
29506
+ });
29507
+ liveTurn?.finish(status);
29418
29508
  }
29419
29509
  };
29420
29510
  void Promise.resolve(deps.session.done).then(() => finalize("done").catch(() => void 0)).catch((err) => {
@@ -142333,71 +142423,6 @@ var init_collect = __esm({
142333
142423
  }
142334
142424
  });
142335
142425
 
142336
- // ../server/src/domains/projects/ephemeral-project.ts
142337
- function isEphemeralProject(projectId2) {
142338
- return projectId2.startsWith(EPHEMERAL_PROJECT_PREFIX);
142339
- }
142340
- function isUncategorizedProjectId(projectId2) {
142341
- return projectId2 === UNCATEGORIZED_PROJECT_ID;
142342
- }
142343
- function ephemeralProjectIdFor(workOrderId) {
142344
- return `${EPHEMERAL_PROJECT_PREFIX}${workOrderId.replace(/[^a-zA-Z0-9_-]+/g, "_")}`;
142345
- }
142346
- function ephemeralProjectNameFor(workOrderId, title) {
142347
- return `\u4E34\u65F6\u9879\u76EE \xB7 ${title?.trim() || workOrderId}`;
142348
- }
142349
- async function ensureWorkorderProject(deps) {
142350
- const { workOrderId, projectId: projectId2, title, createdBy } = deps;
142351
- try {
142352
- if (projectId2) {
142353
- await deps.upsertBinding({ workOrderId, projectId: projectId2 });
142354
- return projectId2;
142355
- }
142356
- const id = ephemeralProjectIdFor(workOrderId);
142357
- await deps.createProject({
142358
- id,
142359
- name: ephemeralProjectNameFor(workOrderId, title),
142360
- description: `\u5EFA\u5355\u65F6\u672A\u6307\u5B9A\u9879\u76EE\uFF0C\u7CFB\u7EDF\u81EA\u52A8\u4E3A\u5DE5\u5355 ${workOrderId} \u521B\u5EFA\u7684\u4E34\u65F6\u9879\u76EE\u3002\u53EF\u7528 \`PATCH /api/work-orders/${workOrderId}/project\` \u6539\u6302\u5230\u6B63\u5F0F\u9879\u76EE\u3002`,
142361
- ...createdBy ? { createdBy } : {}
142362
- });
142363
- await deps.upsertBinding({ workOrderId, projectId: id });
142364
- return id;
142365
- } catch (e) {
142366
- deps.onWarn?.(`[project] \u5DE5\u5355 ${workOrderId} \u7ED1\u5B9A\u9879\u76EE\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u5EFA\u5355\uFF09\uFF1A${String(e)}`);
142367
- return null;
142368
- }
142369
- }
142370
- function makeEnsureProjectFromStore(store) {
142371
- return async (input) => {
142372
- const existing = await store.getProject(input.id);
142373
- if (existing) return { id: existing.id };
142374
- const now = (/* @__PURE__ */ new Date()).toISOString();
142375
- const createdById = input.createdBy?.trim();
142376
- const createdBy = createdById && (createdById.startsWith("actor:human:") || createdById.startsWith("actor:agent:")) ? { id: createdById, kind: projectActorKind(createdById), name: null, avatar: null } : void 0;
142377
- await store.upsertProject({
142378
- id: input.id,
142379
- name: input.name,
142380
- ...input.description !== void 0 ? { description: input.description } : {},
142381
- ...createdBy ? { createdBy } : {},
142382
- createdAt: now,
142383
- updatedAt: now
142384
- });
142385
- return { id: input.id };
142386
- };
142387
- }
142388
- var EPHEMERAL_PROJECT_PREFIX, UNCATEGORIZED_PROJECT_ID, UNCATEGORIZED_PROJECT_NAME, UNCATEGORIZED_PROJECT_DESCRIPTION, UNCATEGORIZED_UNKNOWN_TIME;
142389
- var init_ephemeral_project = __esm({
142390
- "../server/src/domains/projects/ephemeral-project.ts"() {
142391
- "use strict";
142392
- init_src();
142393
- EPHEMERAL_PROJECT_PREFIX = "proj_tmp_";
142394
- UNCATEGORIZED_PROJECT_ID = "proj:uncategorized";
142395
- UNCATEGORIZED_PROJECT_NAME = "\u65E0\u5206\u7C7B\u9879\u76EE";
142396
- UNCATEGORIZED_PROJECT_DESCRIPTION = "\u6240\u6709\u672A\u9009\u62E9\u9879\u76EE\u7684\u5BF9\u8BDD\u76F8\u5173\u6587\u4EF6\u548C\u4EFB\u52A1\u81EA\u52A8\u5E76\u5165\u8FD9\u91CC\uFF0C\u7EC4\u7EC7\u5185\u6240\u6709\u4EBA\u53EF\u89C1\u3002";
142397
- UNCATEGORIZED_UNKNOWN_TIME = "1970-01-01T00:00:00.000Z";
142398
- }
142399
- });
142400
-
142401
142426
  // ../server/src/domains/collab/timeline.ts
142402
142427
  function buildNodeTimeline(artifactId, snap, runIdByTarget) {
142403
142428
  if (!snap) return { artifactId, items: [] };
@@ -159818,8 +159843,9 @@ var init_live_chat = __esm({
159818
159843
  DEFAULT_LIVE_COALESCE_MS = 24;
159819
159844
  DEFAULT_LIVE_COALESCE_MAX_CHUNKS = 32;
159820
159845
  DEFAULT_LIVE_COALESCE_MAX_CHARS = 64 * 1024;
159821
- LiveChatRegistry = class {
159846
+ LiveChatRegistry = class _LiveChatRegistry {
159822
159847
  turns = /* @__PURE__ */ new Map();
159848
+ turnStartWaiters = /* @__PURE__ */ new Map();
159823
159849
  epochs = /* @__PURE__ */ new Map();
159824
159850
  bufferMax;
159825
159851
  graceMs;
@@ -159865,6 +159891,15 @@ var init_live_chat = __esm({
159865
159891
  v3Items: /* @__PURE__ */ new Map()
159866
159892
  };
159867
159893
  this.turns.set(chatSessionId, turn);
159894
+ this.flushPendingUserItems(chatSessionId);
159895
+ const waiters = this.turnStartWaiters.get(chatSessionId);
159896
+ this.turnStartWaiters.delete(chatSessionId);
159897
+ for (const wake of waiters ?? []) {
159898
+ try {
159899
+ wake();
159900
+ } catch {
159901
+ }
159902
+ }
159868
159903
  return {
159869
159904
  runtimeSessionId: turn.runtimeSessionId,
159870
159905
  epoch: turn.liveEpoch,
@@ -159937,6 +159972,26 @@ var init_live_chat = __esm({
159937
159972
  ...opts.items ? { items: opts.items } : {}
159938
159973
  };
159939
159974
  }
159975
+ /** 只通知开轮,不承载内容。先检查再登记均为同步,避免空闲检查与订阅之间漏掉开轮。 */
159976
+ onNextTurn(chatSessionId, wake) {
159977
+ if (this.isRunning(chatSessionId)) {
159978
+ wake();
159979
+ return () => {
159980
+ };
159981
+ }
159982
+ let waiters = this.turnStartWaiters.get(chatSessionId);
159983
+ if (!waiters) {
159984
+ waiters = /* @__PURE__ */ new Set();
159985
+ this.turnStartWaiters.set(chatSessionId, waiters);
159986
+ }
159987
+ waiters.add(wake);
159988
+ return () => {
159989
+ waiters.delete(wake);
159990
+ if (waiters.size === 0 && this.turnStartWaiters.get(chatSessionId) === waiters) {
159991
+ this.turnStartWaiters.delete(chatSessionId);
159992
+ }
159993
+ };
159994
+ }
159940
159995
  /**
159941
159996
  * 归一层中间事件 → 带 `itemId` 的 item op(ADR D4 第 2 跳)+ v3 帧 dual-emit(S3)。
159942
159997
  *
@@ -160164,9 +160219,34 @@ var init_live_chat = __esm({
160164
160219
  * (`server.ts` 里 `recordUserSubmission` 在 `liveChat.start` 之前)。拿不到就不发——
160165
160220
  * 前端退回「等下一次快照」,与修复前一致,不会更坏。
160166
160221
  */
160222
+ /**
160223
+ * 「user item 已落库、但这条会话的 live 轮还没 start」时的暂存位(见 {@link publishUserItem})。
160224
+ *
160225
+ * 每会话只留最近几条:它的唯一用途是补发**当前这一次提交**,不是可靠队列。留太多等于把
160226
+ * 一条早就过期的 user 帧在下一轮开头推出去,那比丢了更糟(用户会看到上一轮的话冒出来)。
160227
+ */
160228
+ pendingUserItems = /* @__PURE__ */ new Map();
160229
+ /** 单会话暂存上限。正常只会有 1 条(一次提交一条);给到 4 是为了并发提交时不至于丢。 */
160230
+ static PENDING_USER_ITEM_MAX = 4;
160231
+ bufferUserItem(chatSessionId, item) {
160232
+ const list2 = this.pendingUserItems.get(chatSessionId) ?? [];
160233
+ list2.push(item);
160234
+ while (list2.length > _LiveChatRegistry.PENDING_USER_ITEM_MAX) list2.shift();
160235
+ this.pendingUserItems.set(chatSessionId, list2);
160236
+ }
160237
+ /** `start` 之后立刻调:把暂存的 user item 补发出去。发不出去就丢掉,不再往下一轮带。 */
160238
+ flushPendingUserItems(chatSessionId) {
160239
+ const list2 = this.pendingUserItems.get(chatSessionId);
160240
+ if (!list2?.length) return;
160241
+ this.pendingUserItems.delete(chatSessionId);
160242
+ for (const item of list2) this.publishUserItem(chatSessionId, item);
160243
+ }
160167
160244
  publishUserItem(chatSessionId, item) {
160168
160245
  const turn = this.turns.get(chatSessionId);
160169
- if (!turn) return false;
160246
+ if (!turn) {
160247
+ this.bufferUserItem(chatSessionId, item);
160248
+ return false;
160249
+ }
160170
160250
  const payload = { role: "user", text: item.text };
160171
160251
  if (item.clientSubmitId) payload.clientSubmitId = item.clientSubmitId;
160172
160252
  if (item.attachments?.length) payload.attachments = item.attachments;
@@ -160587,6 +160667,40 @@ var init_live_chat = __esm({
160587
160667
  }
160588
160668
  });
160589
160669
 
160670
+ // ../server/src/chat-turn-wait.ts
160671
+ function waitForChatTurn(res, liveChat, sessionId, startKeepalive, ready = false) {
160672
+ res.writeHead(200, {
160673
+ "content-type": "application/x-oasis-live-v3+ndjson; charset=utf-8",
160674
+ "cache-control": "no-cache, no-transform",
160675
+ "x-accel-buffering": "no",
160676
+ "access-control-expose-headers": "X-Chat-Session-Watch, X-Chat-Waiting, X-Can-Append",
160677
+ "x-chat-session-watch": "1",
160678
+ "x-chat-waiting": "1",
160679
+ "x-can-append": "0"
160680
+ });
160681
+ res.flushHeaders();
160682
+ const stopKeepalive = startKeepalive(res);
160683
+ let detach = () => {
160684
+ };
160685
+ const cleanup = () => {
160686
+ stopKeepalive();
160687
+ detach();
160688
+ };
160689
+ res.once("close", cleanup);
160690
+ const wake = () => {
160691
+ cleanup();
160692
+ if (!res.destroyed) res.end('{"type":"turn_available"}\n');
160693
+ };
160694
+ if (ready) wake();
160695
+ else detach = liveChat.onNextTurn(sessionId, wake);
160696
+ if (res.destroyed) cleanup();
160697
+ }
160698
+ var init_chat_turn_wait = __esm({
160699
+ "../server/src/chat-turn-wait.ts"() {
160700
+ "use strict";
160701
+ }
160702
+ });
160703
+
160590
160704
  // ../server/src/chat-attachment-files.ts
160591
160705
  function safeAttachmentName(raw) {
160592
160706
  return (raw || "attachment").replace(/[^\w.\-]+/g, "_").replace(/^\.+/, "").slice(0, 80) || "attachment";
@@ -160784,13 +160898,24 @@ var init_closure_report = __esm({
160784
160898
  });
160785
160899
 
160786
160900
  // ../server/src/governance/workorder-drafts.ts
160787
- var import_node_fs5, DRAFT_EDIT_LOCK_TTL_MS, WorkorderDraftStore, ident, seqNoOf, PostgresWorkorderDraftStore;
160901
+ var import_node_fs5, DRAFT_EDIT_LOCK_TTL_MS, WorkorderDraftTagMismatchError, WorkorderDraftStore, ident, seqNoOf, PostgresWorkorderDraftStore;
160788
160902
  var init_workorder_drafts = __esm({
160789
160903
  "../server/src/governance/workorder-drafts.ts"() {
160790
160904
  "use strict";
160791
160905
  import_node_fs5 = __toESM(require("node:fs"), 1);
160792
160906
  init_closure_report();
160793
160907
  DRAFT_EDIT_LOCK_TTL_MS = 12e4;
160908
+ WorkorderDraftTagMismatchError = class extends Error {
160909
+ constructor(draftId, expectedWorkspace, actualWorkspace) {
160910
+ super(
160911
+ actualWorkspace === void 0 ? `\u5EFA\u5355\u8349\u6848 ${draftId} \u4E0D\u5728\u672C store\uFF08\u5F88\u53EF\u80FD\u5EFA\u5728\u53E6\u4E00\u4E2A schema \u7684\u5E93\u91CC\uFF09\u2014\u2014\u62D2\u7EDD\u6253\u6807` : `\u5EFA\u5355\u8349\u6848 ${draftId} \u7684 workspace \u662F ${actualWorkspace}\uFF0C\u4E0E\u672C\u6B21\u5EFA\u5355\u7684 ${String(expectedWorkspace)} \u4E0D\u7B26\u2014\u2014\u62D2\u7EDD\u6253\u6807\uFF08\u8DE8\u5E93\u649E\u53F7\u4F1A\u6539\u7ED1\u522B\u4EBA\u7684\u8349\u6848\uFF09`
160912
+ );
160913
+ this.draftId = draftId;
160914
+ this.expectedWorkspace = expectedWorkspace;
160915
+ this.actualWorkspace = actualWorkspace;
160916
+ this.name = "WorkorderDraftTagMismatchError";
160917
+ }
160918
+ };
160794
160919
  WorkorderDraftStore = class {
160795
160920
  constructor(file) {
160796
160921
  this.file = file;
@@ -160894,14 +161019,28 @@ var init_workorder_drafts = __esm({
160894
161019
  }
160895
161020
  /** 给草案打上 chat 会话标(建单经 chat 路时,/api/cmd 拿到 X-Chat-Session-Id 后调)。
160896
161021
  * 同时(若拿得到)盖上产出它的那一轮 runId,供 friday 重进对话页把该版草案归位到那条助手消息。
160897
- * sourceRunId 一旦盖上不再改(同一版草案就属于那一轮)。 */
160898
- async tagSession(draftId, chatSessionId, sourceRunId) {
161022
+ * sourceRunId 一旦盖上不再改(同一版草案就属于那一轮)。
161023
+ *
161024
+ * **`expectedWorkspace` 是防跨库改绑的那道闸(2026-09-10 线上事故)**:草案 id 形如
161025
+ * `wodraft:<n>`,而**每个 schema 各有一台发号器**(共享库一台、每家公司各一台,见
161026
+ * `PostgresWorkorderDraftStore.open` 建的 `workorder_draft_seq`)。于是「共享库的 70 号」和
161027
+ * 「didi 库的 70 号」是两份毫不相干的草案。一旦建草案与打标落在**不同的 store**上
161028
+ * (公司上下文解析不出来 → 建落共享库;打标按公司解析 → 打进公司库),这里就会拿着同一个
161029
+ * 号在另一个库里翻出**别人的草案**,把它的 chatSessionId / sourceRunId 覆盖成本次会话——
161030
+ * 别人的建单卡就这样出现在你的对话里,而原归属被覆盖后再也找不回来。
161031
+ *
161032
+ * 所以调用方**必须**把「我刚建的那张单的 workspace」一起传进来:对不上就抛,绝不写。
161033
+ * 查不到 id 同理——旧实现在这里是 `if (d) {}` 的静默 no-op,那正是这次事故连一行日志都没留下的原因。
161034
+ * 抛出的是 {@link WorkorderDraftTagMismatchError},调用方该**记账后继续**(草案本身已经建好,
161035
+ * 不该因为打标失败把整个建单回执变成失败),见 `/api/cmd` 与 playbooks 两处调用点。 */
161036
+ async tagSession(draftId, chatSessionId, sourceRunId, expectedWorkspace) {
160899
161037
  const d = this.drafts.get(draftId);
160900
- if (d) {
160901
- d.chatSessionId = chatSessionId;
160902
- if (sourceRunId && !d.sourceRunId) d.sourceRunId = sourceRunId;
160903
- await this.persist({ kind: "upsert", draft: d });
161038
+ if (!d || expectedWorkspace !== void 0 && d.workspace !== expectedWorkspace) {
161039
+ throw new WorkorderDraftTagMismatchError(draftId, expectedWorkspace, d?.workspace);
160904
161040
  }
161041
+ d.chatSessionId = chatSessionId;
161042
+ if (sourceRunId && !d.sourceRunId) d.sourceRunId = sourceRunId;
161043
+ await this.persist({ kind: "upsert", draft: d });
160905
161044
  }
160906
161045
  /** 草案被就地编辑后(editDraftPlan 改了 plan.ops)调用:bump `at` 让 friday 重载据此察觉,
160907
161046
  * 并**落盘**——否则编辑过的草案在 apply 前进程重启就丢改动(草案落盘的初衷正是扛重启)。 */
@@ -161694,9 +161833,14 @@ var init_assistants = __esm({
161694
161833
  await this.opts.actors.disableActor(agentId, by).catch(() => {
161695
161834
  });
161696
161835
  }
161836
+ /** 本服务的公司 scope;未启用公司维度(单租户装配/单测)时 undefined = 不过滤,行为不变。 */
161837
+ scope() {
161838
+ const companyId = this.opts.companyId();
161839
+ return companyId ? { companyId } : void 0;
161840
+ }
161697
161841
  /* ---------- runtime 挑选(ADR §3.3.a):负载均衡 + 幂等 tie-break ---------- */
161698
161842
  async pickAssistantRuntime() {
161699
- const runtimes = (await this.opts.listRuntimes()).filter((r) => r.status === "online");
161843
+ const runtimes = (await this.opts.listRuntimes(void 0, this.scope())).filter((r) => r.status === "online");
161700
161844
  if (runtimes.length === 0) return null;
161701
161845
  let defaultKind = DEFAULT_ASSISTANT_RUNTIME_KIND;
161702
161846
  for (const r of runtimes) {
@@ -161723,7 +161867,7 @@ var init_assistants = __esm({
161723
161867
  if (!actor.roles.includes("assistant")) return false;
161724
161868
  const binding = await this.opts.registry.getBinding(agentId);
161725
161869
  if (!binding || binding.status !== "active") return false;
161726
- const runtime = (await this.opts.listRuntimes(binding.nodeId)).find((r) => r.kind === binding.runtimeKind);
161870
+ const runtime = (await this.opts.listRuntimes(binding.nodeId, this.scope())).find((r) => r.kind === binding.runtimeKind);
161727
161871
  if (!runtime || runtime.status !== "online") return false;
161728
161872
  return true;
161729
161873
  }
@@ -186756,12 +186900,13 @@ var init_governance_service = __esm({
186756
186900
  const binding = await this.options.projects.getWorkspaceBinding(ctx.organizationId, workorderId);
186757
186901
  if (!binding) {
186758
186902
  if (claimedProjectId) throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_BINDING_MISSING", "\u5DE5\u5355\u6CA1\u6709\u53EF\u4FE1\u9879\u76EE\u7ED1\u5B9A");
186903
+ await this.requireOrganizationActor(ctx);
186759
186904
  return void 0;
186760
186905
  }
186761
186906
  if (claimedProjectId && binding.projectId !== claimedProjectId) {
186762
186907
  throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_MISMATCH", "\u8BF7\u6C42\u9879\u76EE\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u4E0D\u4E00\u81F4");
186763
186908
  }
186764
- if (this.options.projects.isEphemeralProject(ctx.organizationId, binding.projectId)) {
186909
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, binding.projectId)) {
186765
186910
  await this.requireOrganizationActor(ctx);
186766
186911
  return void 0;
186767
186912
  }
@@ -186969,8 +187114,8 @@ var init_governance_service = __esm({
186969
187114
  const projectId2 = input.scopeType === "project" ? input.scopeId : void 0;
186970
187115
  if (input.scopeType === "project") {
186971
187116
  if (!projectId2) throw new KnowledgeGovernanceError(400, "KNOWLEDGE_PROJECT_REQUIRED", "\u9879\u76EE Space \u5FC5\u987B\u6307\u5B9A\u9879\u76EE");
186972
- if (this.options.projects.isEphemeralProject(ctx.organizationId, projectId2)) {
186973
- throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u4E34\u65F6\u9879\u76EE\u4E0D\u521B\u5EFA\u77E5\u8BC6 Space");
187117
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) {
187118
+ throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u8BE5\u9879\u76EE\u4E0D\u652F\u6301\u72EC\u7ACB\u77E5\u8BC6 Space");
186974
187119
  }
186975
187120
  await this.authorizeProjectResource(ctx, projectId2, "configure");
186976
187121
  } else if (input.scopeId) {
@@ -187096,7 +187241,7 @@ var init_governance_service = __esm({
187096
187241
  async reconfigureKnowledgeTree(ctx, store, organizationSpace, connection, _config) {
187097
187242
  const projectEntries = [];
187098
187243
  for (const project of await this.options.projects.listProjects(ctx.organizationId)) {
187099
- if (this.options.projects.isEphemeralProject(ctx.organizationId, project.id)) continue;
187244
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, project.id)) continue;
187100
187245
  const projectSpace = (await store.list(this.projectScope(ctx, project.id), "space")).filter((space) => space.status !== "archived").sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
187101
187246
  if (projectSpace) projectEntries.push({ project, space: projectSpace });
187102
187247
  }
@@ -187315,8 +187460,8 @@ var init_governance_service = __esm({
187315
187460
  async resolveRunTarget(ctx, projectId2) {
187316
187461
  await this.requireOrganizationActor(ctx);
187317
187462
  if (projectId2) {
187318
- if (this.options.projects.isEphemeralProject(ctx.organizationId, projectId2)) {
187319
- throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u4E34\u65F6\u9879\u76EE\u6CA1\u6709\u72EC\u7ACB\u9879\u76EE\u77E5\u8BC6\u5E93\uFF0C\u8BF7\u9009\u62E9\u7EC4\u7EC7\u77E5\u8BC6\u5E93");
187463
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) {
187464
+ throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u8BE5\u9879\u76EE\u6CA1\u6709\u72EC\u7ACB\u9879\u76EE\u77E5\u8BC6\u5E93\uFF0C\u8BF7\u9009\u62E9\u7EC4\u7EC7\u77E5\u8BC6\u5E93");
187320
187465
  }
187321
187466
  await this.authorizeProjectResource(ctx, projectId2, "publish");
187322
187467
  }
@@ -187532,7 +187677,7 @@ var init_governance_service = __esm({
187532
187677
  if (actor.kind !== "system") await this.requireOrganizationAdmin(ctx);
187533
187678
  const project = await this.options.projects.getProject(ctx.organizationId, projectId2);
187534
187679
  if (!project) throw new KnowledgeGovernanceError(404, "KNOWLEDGE_PROJECT_NOT_FOUND", "\u9879\u76EE\u4E0D\u5B58\u5728");
187535
- if (this.options.projects.isEphemeralProject(ctx.organizationId, projectId2)) return null;
187680
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) return null;
187536
187681
  const store = await this.options.storeFor(ctx.organizationId);
187537
187682
  const scope = this.projectScope(ctx, projectId2);
187538
187683
  const existingSpaces = await store.list(scope, "space");
@@ -190232,7 +190377,7 @@ var init_governance_service = __esm({
190232
190377
  if (event.claimedProjectId !== trusted.projectId) {
190233
190378
  throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_MISMATCH", "\u8BF7\u6C42\u9879\u76EE\u4E0E\u53EF\u4FE1\u6765\u6E90\u5F52\u5C5E\u4E0D\u4E00\u81F4");
190234
190379
  }
190235
- const targetProjectId = trusted.projectId && !this.options.projects.isEphemeralProject(ctx.organizationId, trusted.projectId) ? trusted.projectId : void 0;
190380
+ const targetProjectId = trusted.projectId && !this.options.projects.isProjectScopeUnsupported(ctx.organizationId, trusted.projectId) ? trusted.projectId : void 0;
190236
190381
  const discoveryActor = await this.requireActiveActor(ctx.actorId, ctx.organizationId);
190237
190382
  if (discoveryActor.kind !== "system") await this.requireOrganizationActor(ctx);
190238
190383
  if (targetProjectId) {
@@ -190498,7 +190643,7 @@ var init_governance_service = __esm({
190498
190643
  runs.push(...await this.enqueueDiscoveryEvent(ctx, event, runtime ?? void 0));
190499
190644
  } catch (error2) {
190500
190645
  const store = await this.options.storeFor(ctx.organizationId);
190501
- const taskProjectId = projectId2 && !this.options.projects.isEphemeralProject(ctx.organizationId, projectId2) ? projectId2 : void 0;
190646
+ const taskProjectId = projectId2 && !this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2) ? projectId2 : void 0;
190502
190647
  const scope = this.scopeFor(ctx, taskProjectId);
190503
190648
  const task = this.governanceTask(ctx, "discovery_failed", event.sourceType, `${event.sourceId}:${event.sourceVersionId}`, {
190504
190649
  eventType: event.eventType,
@@ -202522,20 +202667,6 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
202522
202667
  const project = await ctx.projectState.getProject(projectId2);
202523
202668
  if (!project) throw new Error(`project not found: ${projectId2}`);
202524
202669
  await bindingState.upsertWorkspaceBinding({ workOrderId: workspace, projectId: projectId2 });
202525
- } else if (createdViaArg === "seed" && ctx.projectState && bindingState) {
202526
- await ensureWorkorderProject({
202527
- workOrderId: workspace,
202528
- ...optStr(args, "title") ? { title: optStr(args, "title") } : {},
202529
- // ADR 0207「对话中创建则为对话人」:优先记**陪聊解析出的那个真人**
202530
- // (`ctx.workorderInitiator.humanActorId`,与 brief.owner 同源),
202531
- // 没有会话上下文时退到调用者本人——但**只在他是人时**;
202532
- // agent 替人跑 spawn 不该把自己写成项目创建人,那种情况落 NULL,
202533
- // 由读侧从工单 brief.owner 派生(service.resolveProjectCreator)。
202534
- ...ctx.workorderInitiator?.humanActorId ? { createdBy: ctx.workorderInitiator.humanActorId } : !isAgent(actor) ? { createdBy: actor } : {},
202535
- createProject: makeEnsureProjectFromStore(ctx.projectState),
202536
- upsertBinding: (b2) => bindingState.upsertWorkspaceBinding(b2),
202537
- onWarn: (m2) => console.warn(m2)
202538
- });
202539
202670
  }
202540
202671
  if (schemaForSeed) {
202541
202672
  const title = optStr(args, "title") ?? optStr(args, "description") ?? workspace;
@@ -204027,6 +204158,18 @@ async function startOasisServer(opts) {
204027
204158
  chatStore: store,
204028
204159
  chatSessionId: sessionId,
204029
204160
  session: bgSession,
204161
+ liveChat,
204162
+ liveHandle: {
204163
+ runtimeSessionId: session.id,
204164
+ ...session.runId ? { runId: session.runId } : {},
204165
+ kill: () => {
204166
+ void session.kill?.();
204167
+ },
204168
+ ...session.appendInput ? { appendInput: (input) => session.appendInput(input) } : {},
204169
+ get canAppendInput() {
204170
+ return typeof session.appendInput === "function" && session.canAppendInput !== false;
204171
+ }
204172
+ },
204030
204173
  ...itemStore ? { itemStore } : {},
204031
204174
  // 广播/委派回流带上 held turn id(B3);授权卡入口在别处,那条路本轮不接账本。
204032
204175
  ...turnId ? { turnId } : {}
@@ -204585,7 +204728,15 @@ async function startOasisServer(opts) {
204585
204728
  if (body2.command === "spawn" && wodrafts) {
204586
204729
  const draftId = result?.data?.draftId;
204587
204730
  const chatSid = req.headers["x-chat-session-id"];
204588
- if (draftId && typeof chatSid === "string") await wodrafts.tagSession(draftId, chatSid, liveChat.runFor(chatSid));
204731
+ if (draftId && typeof chatSid === "string") {
204732
+ const draftWorkspace = result?.data?.workspace;
204733
+ await wodrafts.tagSession(draftId, chatSid, liveChat.runFor(chatSid), draftWorkspace).catch((err) => {
204734
+ console.error(
204735
+ `[workorder-draft] \u8349\u6848 ${draftId}\uFF08ws=${String(draftWorkspace)}\uFF09\u672A\u80FD\u5173\u8054\u4F1A\u8BDD ${chatSid}\uFF1A`,
204736
+ err
204737
+ );
204738
+ });
204739
+ }
204589
204740
  }
204590
204741
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
204591
204742
  } catch (err) {
@@ -205592,6 +205743,13 @@ async function startOasisServer(opts) {
205592
205743
  };
205593
205744
  const wantsV3 = url.searchParams.get("protocol") === "v3";
205594
205745
  if (wantsV3) {
205746
+ const watchSession = url.searchParams.get("watch") === "1";
205747
+ const fromEpoch = url.searchParams.has("epoch") ? Number(url.searchParams.get("epoch")) : void 0;
205748
+ const cursor = liveChat.cursorFor(sid);
205749
+ if (watchSession && fromEpoch !== void 0 && cursor && cursor.epoch !== fromEpoch) {
205750
+ waitForChatTurn(res, liveChat, sid, startStreamKeepalive, true);
205751
+ return;
205752
+ }
205595
205753
  const sub2 = (frame) => {
205596
205754
  if (res.destroyed) return;
205597
205755
  res.write(`${JSON.stringify(frame)}
@@ -205603,6 +205761,11 @@ async function startOasisServer(opts) {
205603
205761
  await reopenRecoveringTurn();
205604
205762
  att2 = liveChat.attachLiveV3(sid, fromSeq, sub2);
205605
205763
  }
205764
+ if (watchSession && (!att2 || att2.status !== "running" && att2.replay.length === 0)) {
205765
+ att2?.detach();
205766
+ waitForChatTurn(res, liveChat, sid, startStreamKeepalive);
205767
+ return;
205768
+ }
205606
205769
  if (!att2) {
205607
205770
  res.writeHead(204).end();
205608
205771
  return;
@@ -205610,7 +205773,8 @@ async function startOasisServer(opts) {
205610
205773
  res.writeHead(200, {
205611
205774
  "content-type": "application/x-oasis-live-v3+ndjson; charset=utf-8",
205612
205775
  "transfer-encoding": "chunked",
205613
- "access-control-expose-headers": "X-Session-Id, X-Replay-Gap, X-Live-Epoch, X-Can-Append",
205776
+ "access-control-expose-headers": "X-Session-Id, X-Replay-Gap, X-Live-Epoch, X-Can-Append, X-Chat-Session-Watch",
205777
+ ...watchSession ? { "x-chat-session-watch": "1" } : {},
205614
205778
  "x-session-id": cs.runtimeSessionId ?? "",
205615
205779
  "x-live-epoch": String(att2.epoch),
205616
205780
  "x-can-append": liveChat.canAppend(sid) ? "1" : "0",
@@ -206993,14 +207157,23 @@ ${composed}`;
206993
207157
  const installationId = await svc.revealResolvedVariable("GITHUB_APP_INSTALLATION_ID", actorId);
206994
207158
  const effective = installationId ? await fetchInstallationPermissions({ appId, privateKeyPem, installationId }) : meta.permissions;
206995
207159
  const missing = missingAppPermissions(effective);
207160
+ const requestedAll = Object.entries(meta.permissions).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key));
207161
+ const grantedKeys = new Set(
207162
+ Object.entries(effective).filter(([, level]) => Boolean(level)).map(([k2]) => k2)
207163
+ );
206996
207164
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
206997
207165
  configured: true,
206998
207166
  slug: meta.slug,
206999
207167
  ownerLogin: meta.ownerLogin,
207168
+ /** App 申请的全集(英文原名 + 级别)。界面用它回答「我配了多少项」。 */
207169
+ requested: requestedAll,
207000
207170
  /** true=读的是这次安装真正被授予的权限;false=只拿到 App 申请的,可能偏乐观。 */
207001
207171
  fromInstallation: Boolean(installationId),
207002
- /** App 申请了、但这次安装还没被授予——多半是权限升级还等着组织 owner 批准。 */
207003
- awaitingApproval: missingAppPermissions(effective).filter((m2) => meta.permissions[m2.key]).map((m2) => m2.key),
207172
+ /**
207173
+ * App 申请了、但这次安装还没被授予——多半是权限升级还等着组织 owner 批准。
207174
+ * **在全集上做差**,不再只看本仓必需的那八项(否则人另外勾的永远报不出来)。
207175
+ */
207176
+ awaitingApproval: requestedAll.filter((r) => !grantedKeys.has(r.key)).map((r) => r.key),
207004
207177
  /**
207005
207178
  * 这次安装**真正被授予**的全部权限,一律用 GitHub 的英文原名 + 级别
207006
207179
  * (2026-09-10 发起人:「所有权限都用英文原文,不区分是不是八项权限」)。
@@ -207318,7 +207491,9 @@ ${composed}`;
207318
207491
  const svc = await connectorActorsService();
207319
207492
  if (svc) {
207320
207493
  const connId = "github";
207321
- await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
207494
+ if (!actorIdParam) {
207495
+ await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
207496
+ }
207322
207497
  for (const [k2, v2] of [["GIT_AUTHOR_NAME", actorNameParam], ["GIT_AUTHOR_EMAIL", ""], ["GIT_COMMITTER_NAME", actorNameParam], ["GIT_COMMITTER_EMAIL", ""], ["EMAIL", ""]])
207323
207498
  await svc.putVariable({ key: k2, value: v2, scope: "personal", actorId: actorIdParam, connectorId: connId, encrypted: false });
207324
207499
  await svc.putVariable({ key: "GITHUB_TOKEN", value: p2.access_token, scope: "personal", actorId: actorIdParam, connectorId: connId, encrypted: true });
@@ -207453,7 +207628,6 @@ var init_server3 = __esm({
207453
207628
  init_src7();
207454
207629
  init_collect();
207455
207630
  init_remote_util();
207456
- init_ephemeral_project();
207457
207631
  init_timeline();
207458
207632
  init_node_state();
207459
207633
  init_src5();
@@ -207462,6 +207636,7 @@ var init_server3 = __esm({
207462
207636
  init_router();
207463
207637
  init_infra_error();
207464
207638
  init_live_chat();
207639
+ init_chat_turn_wait();
207465
207640
  init_append_now();
207466
207641
  init_workorder_drafts();
207467
207642
  init_draft_edit();
@@ -210527,6 +210702,32 @@ var init_projection = __esm({
210527
210702
  }
210528
210703
  });
210529
210704
 
210705
+ // ../server/src/domains/projects/uncategorized-project.ts
210706
+ function isUncategorizedProjectId(projectId2) {
210707
+ return projectId2 === UNCATEGORIZED_PROJECT_ID;
210708
+ }
210709
+ async function bindWorkorderProject(deps) {
210710
+ const { workOrderId, projectId: projectId2 } = deps;
210711
+ if (!projectId2) return null;
210712
+ try {
210713
+ await deps.upsertBinding({ workOrderId, projectId: projectId2 });
210714
+ return projectId2;
210715
+ } catch (e) {
210716
+ deps.onWarn?.(`[project] \u5DE5\u5355 ${workOrderId} \u7ED1\u5B9A\u9879\u76EE\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u5EFA\u5355\uFF09\uFF1A${String(e)}`);
210717
+ return null;
210718
+ }
210719
+ }
210720
+ var UNCATEGORIZED_PROJECT_ID, UNCATEGORIZED_PROJECT_NAME, UNCATEGORIZED_PROJECT_DESCRIPTION, UNCATEGORIZED_UNKNOWN_TIME;
210721
+ var init_uncategorized_project = __esm({
210722
+ "../server/src/domains/projects/uncategorized-project.ts"() {
210723
+ "use strict";
210724
+ UNCATEGORIZED_PROJECT_ID = "proj:uncategorized";
210725
+ UNCATEGORIZED_PROJECT_NAME = "\u65E0\u5206\u7C7B\u9879\u76EE";
210726
+ UNCATEGORIZED_PROJECT_DESCRIPTION = "\u6240\u6709\u672A\u9009\u62E9\u9879\u76EE\u7684\u5BF9\u8BDD\u76F8\u5173\u6587\u4EF6\u548C\u4EFB\u52A1\u81EA\u52A8\u5E76\u5165\u8FD9\u91CC\uFF0C\u7EC4\u7EC7\u5185\u6240\u6709\u4EBA\u53EF\u89C1\u3002";
210727
+ UNCATEGORIZED_UNKNOWN_TIME = "1970-01-01T00:00:00.000Z";
210728
+ }
210729
+ });
210730
+
210530
210731
  // ../server/src/domains/projects/service.ts
210531
210732
  function sanitizeCreatorActorId(raw) {
210532
210733
  const id = raw?.trim();
@@ -210799,7 +211000,7 @@ var init_service4 = __esm({
210799
211000
  init_src();
210800
211001
  init_src5();
210801
211002
  init_projection();
210802
- init_ephemeral_project();
211003
+ init_uncategorized_project();
210803
211004
  init_workorders();
210804
211005
  init_node_state();
210805
211006
  DEFAULT_AGENT_ACTOR = "actor:agent:system";
@@ -210935,6 +211136,9 @@ var init_service4 = __esm({
210935
211136
  async listWorkspaceBindings() {
210936
211137
  return [...this.workspaceBindings.values()].map((binding) => ({ ...binding }));
210937
211138
  }
211139
+ async deleteWorkspaceBinding(workOrderId) {
211140
+ this.workspaceBindings.delete(workOrderId);
211141
+ }
210938
211142
  async setWorkspaceDispatchHold(workOrderId, held) {
210939
211143
  if (held) this.dispatchHeld.add(workOrderId);
210940
211144
  else this.dispatchHeld.delete(workOrderId);
@@ -211061,15 +211265,8 @@ var init_service4 = __esm({
211061
211265
  return { project, artifacts, files };
211062
211266
  }
211063
211267
  async collectProjectFiles(projectId2) {
211064
- if (!isUncategorizedProjectId(projectId2)) return this.projects.listProjectFiles(projectId2);
211065
- const stored = await this.projects.listProjects();
211066
- const ephemeralIds = stored.filter((p2) => isEphemeralProject(p2.id)).map((p2) => p2.id);
211067
- const out = [];
211068
- for (const id of ephemeralIds) {
211069
- const files = await this.projects.listProjectFiles(id);
211070
- out.push(...files.map((f2) => ({ ...f2, projectId: UNCATEGORIZED_PROJECT_ID })));
211071
- }
211072
- return out.sort((a, b2) => b2.uploadedAt.localeCompare(a.uploadedAt));
211268
+ if (isUncategorizedProjectId(projectId2)) return [];
211269
+ return this.projects.listProjectFiles(projectId2);
211073
211270
  }
211074
211271
  /**
211075
211272
  * B2 投影同步:写命令落 oplog 后,从 oplog 重放重建 `this.artifacts` 文档层投影。
@@ -211126,23 +211323,18 @@ var init_service4 = __esm({
211126
211323
  return project ? this.withProjectDerivedState(project) : null;
211127
211324
  }
211128
211325
  /**
211129
- * 「无分类项目」虚拟卡:把所有 `proj_tmp_*` 一族的**读侧聚合**成一张固定项目,供列表/详情/关联任务/相关文件消费。
211326
+ * 「无分类项目」虚拟卡:把所有**没有项目绑定**的工单聚合成一张固定项目,供列表/详情/关联任务/相关文件消费。
211130
211327
  * 存储里没有真行——这就是本卡与真项目最大的区别:
211131
211328
  * - 写侧(成员、内容位置、上传文件)一律拒收(写到真项目 or 走真工单流程);
211132
- * - 派生态(artifactSummary / workOrderCount)跨所有 ephemeral 项目算并集;
211329
+ * - 派生态(artifactSummary / workOrderCount)按「无绑定工单」算并集;
211133
211330
  * - 帧上的「项目负责人:系统」由前端自选文案渲染,本层只给一个稳定的空 members 名单。
211331
+ *
211332
+ * ⚠ `workOrderCount` **走 listProjectWorkorders 同一条路**,不另写一份 bindings 过滤——
211333
+ * 两处各算各的正是「卡片计数与点进去的列表条数对不上」那类缺陷的来路。
211134
211334
  */
211135
211335
  async buildUncategorizedProject() {
211136
- const stored = await this.projects.listProjects();
211137
- const ephemeralIds = new Set(
211138
- stored.filter((project) => isEphemeralProject(project.id)).map((project) => project.id)
211139
- );
211140
- const bindings = await this.artifacts.listWorkspaceBindings();
211141
- const workOrderCount = bindings.filter((b2) => ephemeralIds.has(b2.projectId)).length;
211142
- const artifactList = await this.aggregateUncategorizedArtifacts(ephemeralIds, bindings);
211143
- const ephemeralProjects = stored.filter((project) => ephemeralIds.has(project.id));
211144
- const createdAts = ephemeralProjects.map((p2) => p2.createdAt).sort();
211145
- const updatedAts = ephemeralProjects.map((p2) => p2.updatedAt).sort();
211336
+ const workorders = await this.listProjectWorkorders(UNCATEGORIZED_PROJECT_ID);
211337
+ const artifactList = await this.aggregateUncategorizedArtifacts();
211146
211338
  return {
211147
211339
  id: UNCATEGORIZED_PROJECT_ID,
211148
211340
  name: UNCATEGORIZED_PROJECT_NAME,
@@ -211150,46 +211342,33 @@ var init_service4 = __esm({
211150
211342
  memberCount: 0,
211151
211343
  members: [],
211152
211344
  artifactSummary: artifactList.summary,
211153
- workOrderCount,
211345
+ workOrderCount: workorders.length,
211154
211346
  // ADR 0205:本卡的「项目负责人:系统」是**前端按帧自选的文案**,后端不出 `"system"` 哨兵——
211155
211347
  // 出 null 保持「取不到就 null」的域内红线,也不把一个不存在的 actor id 塞进契约。
211156
211348
  createdBy: null,
211157
- createdAt: createdAts[0] ?? UNCATEGORIZED_UNKNOWN_TIME,
211158
- updatedAt: updatedAts[updatedAts.length - 1] ?? UNCATEGORIZED_UNKNOWN_TIME
211349
+ createdAt: UNCATEGORIZED_UNKNOWN_TIME,
211350
+ updatedAt: UNCATEGORIZED_UNKNOWN_TIME
211159
211351
  };
211160
211352
  }
211161
- async aggregateUncategorizedArtifacts(ephemeralIds, bindings) {
211162
- const _bindings = bindings ?? await this.artifacts.listWorkspaceBindings();
211163
- const artifacts = [];
211164
- for (const projectId2 of ephemeralIds) {
211165
- const list2 = await this.listAllProjectArtifacts(projectId2);
211166
- artifacts.push(...list2.map((a) => ({ ...a, projectId: UNCATEGORIZED_PROJECT_ID })));
211167
- }
211353
+ async aggregateUncategorizedArtifacts() {
211354
+ const artifacts = await this.listAllProjectArtifacts(UNCATEGORIZED_PROJECT_ID);
211168
211355
  artifacts.sort((a, b2) => b2.updatedAt.localeCompare(a.updatedAt));
211169
211356
  return { artifacts, summary: summarizeArtifacts(artifacts) };
211170
211357
  }
211171
211358
  /**
211172
211359
  * 项目列表(带派生态)。
211173
211360
  *
211174
- * ★ `includeEphemeral` 必须在**算派生态之前**生效,不能让调用方拿到全量再自己 filter。
211175
- * `withProjectDerivedState` 每算一个项目就要跑一遍 `buildWorkorderSummaries`(全量工单)+
211176
- * 遍历模型里全部 artifact——成本是 O(项目数 × 全模型),而且是同步的。实测 dev:294 个项目里
211177
- * 272 个是 `proj_tmp_*` 临时项目(工单没指定项目时的兜底归属,一单一个),列表只返回剩下的 22 个,
211178
- * **93% 的计算算完就扔**。代价不是"多花点 CPU":这段同步活会把单线程的事件循环整块堵住 3~11 秒,
211179
- * 期间别的请求从 pg 连接池取连接排不上号、撞 `connectionTimeoutMillis`(5s),
211180
- * 前端看到的是「项目列表加载失败:timeout exceeded when trying to connect」——像数据库挂了,
211181
- * 其实是被自己堵死的(实测并发时 /api/automations 从 0.24s 被拖到 7.1s,自动化页面因此空白)。
211182
- *
211183
- * 只对**真要返回的那些**算派生态。默认口径与路由一致(不含临时项目),排查用 `includeEphemeral`。
211361
+ * 库里的每一行都是**人建的真项目**,全部返回——临时项目(`proj_tmp_*`)退役后,这里不再有
211362
+ * "算完就扔"的过滤:旧版 dev 环境 294 个项目里 272 个是一单一个的临时项目,
211363
+ * `withProjectDerivedState` 对它们逐个跑 `buildWorkorderSummaries`(全量工单)+ 遍历全部 artifact,
211364
+ * 93% 的计算白算,还把单线程事件循环整块堵住 3~11 秒(并发时 /api/automations 从 0.24s 被拖到 7.1s,
211365
+ * 前端看到的是「项目列表加载失败:timeout」——像数据库挂了,其实是被自己堵死的)。
211184
211366
  */
211185
211367
  async listProjects(opts) {
211186
211368
  const stored = await this.projects.listProjects();
211187
- const wanted = opts?.includeEphemeral === true ? stored : stored.filter((project) => !isEphemeralProject(project.id));
211188
211369
  const creatorCache = /* @__PURE__ */ new Map();
211189
- const derived = await Promise.all(wanted.map((project) => this.withProjectDerivedState(project, creatorCache)));
211370
+ const derived = await Promise.all(stored.map((project) => this.withProjectDerivedState(project, creatorCache)));
211190
211371
  if (opts?.includeUncategorized === false) return derived;
211191
- const wantUncategorized = opts?.includeUncategorized ?? !(opts?.includeEphemeral === true);
211192
- if (!wantUncategorized) return derived;
211193
211372
  const virtual = await this.buildUncategorizedProject();
211194
211373
  return [virtual, ...derived];
211195
211374
  }
@@ -211268,10 +211447,10 @@ var init_service4 = __esm({
211268
211447
  * 项目创建人(ADR 0205)——**读时解析**,三条来路按优先级:
211269
211448
  *
211270
211449
  * 1. 库里落了 `created_by` → 按它解析 name/avatar;
211271
- * 2. 没落且是**临时项目**(`proj_tmp_*`)→ 从它绑定的那张工单的 `brief.owner`(决策 0026 的
211272
- * 「发起人真人」)派生。这是老数据免回填的口径:派生每次都新鲜,回填反而会造出
211273
- * 「工单发起人换过、项目创建人却挂着旧值」的伪相;
211274
- * 3. 都取不到 `null`(老的手建项目),前端诚实显示「—」。
211450
+ * 2. 没落 `null`(老的手建项目),前端诚实显示「—」。
211451
+ *
211452
+ * 旧版还有一档「临时项目从它绑定的那张工单 `brief.owner` 派生」——临时项目退役后没有这一档了
211453
+ * (见 uncategorized-project.ts 文件头)。
211275
211454
  *
211276
211455
  * **绝不拿别的字段顶替**(`members[0]`、项目名、updatedAt 都不行)——未指定成员时全组织都是成员,
211277
211456
  * 按 actorId 排序等于随机点一个 agent 当负责人。这是本域红线,见 resolveActorName 的注释。
@@ -211279,9 +211458,7 @@ var init_service4 = __esm({
211279
211458
  async resolveProjectCreator(project, workorders, cache) {
211280
211459
  const stored = project.createdBy?.id;
211281
211460
  if (stored) return this.resolveActorRef(stored, cache);
211282
- if (!isEphemeralProject(project.id)) return null;
211283
- const ownerId = workorders.map((wo) => wo.owner?.id).find((id) => typeof id === "string" && id.startsWith("actor:"));
211284
- return ownerId ? this.resolveActorRef(ownerId, cache) : null;
211461
+ return null;
211285
211462
  }
211286
211463
  /**
211287
211464
  * actorId → {@link ProjectActorRef}:`kind` 按 id 前缀派生,`name` / `avatar` 走本公司名册现取。
@@ -211344,7 +211521,8 @@ var init_service4 = __esm({
211344
211521
  */
211345
211522
  async reassignWorkorder(workOrderId, projectId2) {
211346
211523
  if (isUncategorizedProjectId(projectId2)) {
211347
- return this.unlinkWorkorder(workOrderId);
211524
+ await this.unlinkWorkorder(workOrderId);
211525
+ return { workOrderId, projectId: null };
211348
211526
  }
211349
211527
  const project = await this.projects.getProject(projectId2);
211350
211528
  if (!project) throw new Error(`project not found: ${projectId2}`);
@@ -211353,77 +211531,46 @@ var init_service4 = __esm({
211353
211531
  return binding;
211354
211532
  }
211355
211533
  /**
211356
- * 「取消任务关联」——把工单从当前项目解绑,回落到它专属的临时项目(proj_tmp_<slug>)。
211534
+ * 「取消任务关联」——**删掉工单的项目绑定行**,工单从此没有项目(读侧落进「无分类项目」桶)。
211357
211535
  *
211358
- * **不是真正解绑成 null**:契约里 `Workspace.projectId` 是必填(见 ephemeral-project.ts 的历史事故链),
211359
- * 无绑定态会让下游 provision / 自动收集 / propose 走向连环破坏。回落到临时项目 =「在读侧进入
211360
- * 无分类桶」+「写侧仍保持"每工单必属项目"的合法态」。
211536
+ * 2026-09-10 之前这里是"回落到工单专属的临时项目 `proj_tmp_<slug>`",理由是「契约里每个工单
211537
+ * 都该属于某项目」。那套兜底已退役(见 uncategorized-project.ts 文件头):兜底容器让每张未挂靠
211538
+ * 工单变成一个只有它自己的假项目,代码仓/项目变量/记忆作用域全挂在一次性 id 上。
211361
211539
  *
211362
- * 幂等:目标已是自己的 ephemeral 项目 → 直接返回既有绑定,不重复 upsert;ephemeral 项目本身
211363
- * 不需要预先存在(`ensureWorkorderProject` 后台建,也可能是本工单第一次被"解绑"——按需建)。
211540
+ * 幂等:没有绑定行时静默返回(store delete 本身幂等)。
211364
211541
  */
211365
211542
  async unlinkWorkorder(workOrderId) {
211366
211543
  if (!workOrderId.trim()) throw new Error("work_order_id is required");
211367
- const ephemeralId = ephemeralProjectIdFor(workOrderId);
211368
- const existing = await this.artifacts.getWorkspaceBinding(workOrderId);
211369
- if (!await this.projects.getProject(ephemeralId)) {
211370
- const now = (/* @__PURE__ */ new Date()).toISOString();
211371
- await this.projects.upsertProject({
211372
- id: ephemeralId,
211373
- name: `\u4E34\u65F6\u9879\u76EE \xB7 ${workOrderId}`,
211374
- description: `\u5DE5\u5355 ${workOrderId} \u4ECE\u5176\u5B83\u9879\u76EE\u53D6\u6D88\u5173\u8054\u540E\u843D\u56DE\u7684\u4E34\u65F6\u9879\u76EE\u3002\u53EF\u7528 PATCH /api/work-orders/${workOrderId}/project \u6539\u6302\u5230\u6B63\u5F0F\u9879\u76EE\u3002`,
211375
- createdAt: now,
211376
- updatedAt: now
211377
- });
211378
- }
211379
- if (existing?.projectId === ephemeralId) return existing;
211380
- const binding = { workOrderId, projectId: ephemeralId };
211381
- await this.artifacts.upsertWorkspaceBinding(binding);
211382
- return binding;
211544
+ await this.artifacts.deleteWorkspaceBinding(workOrderId);
211383
211545
  }
211384
211546
  /**
211385
211547
  * 删除项目(帧 `444:1792` 三点菜单第二项,口径由发起人 2026-09-09 拍板)。
211386
211548
  *
211387
211549
  * **只删项目这个壳**——项目主记录 + 成员名单 + 上传文件记录 + 内容位置。任务和会话一条都不删:
211388
211550
  *
211389
- * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 回落到自己的
211390
- * `proj_tmp_<slug>`,也就是读侧的「无分类项目」。发起人明确不要「有任务在跑就不让删」
211391
- * 这道闸,所以这里没有任何在跑判断。
211392
- * 2. 搬工单的同时**把本项目的 git 类内容位置复制一份到每个工单的临时项目上**。
211393
- * 这条不是可选的:`git/resolve.ts` 的 `resolveCodeRepo` 是**派发时**沿
211394
- * `binding project → listContentLocations` 实时解的,不带过去的话,正在跑的任务
211395
- * 下一个节点就取不到代码仓——正是 `ephemeral-project.ts` 顶部那条五环事故链。
211551
+ * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 删掉绑定行 → 落进读侧的
211552
+ * 「无分类项目」。发起人明确不要「有任务在跑就不让删」这道闸,所以这里没有任何在跑判断。
211553
+ * 2. **代码仓不跟着搬**(发起人 2026-09-10 裁定)。`git/resolve.ts` 的 `resolveCodeRepo` 是
211554
+ * 派发时沿 `绑定 项目 → 内容位置` 实时解的,所以删完项目,这些任务下一个节点就取不到代码仓——
211555
+ * 这是**期望行为**:由执行中的数字员工发现并引导人给任务指定新项目,而不是由系统兜一个假容器
211556
+ * (旧版把 git 位置逐单复制到 `proj_tmp_*` 上,正是那套已退役的兜底)。
211396
211557
  * 3. 会话的 `project_id` 置空、项目作用域的变量/密钥清理,走 {@link OnProjectDeleted} 钩子
211397
211558
  * (本 service 对会话域与凭据域零耦合,同 `onProjectNameChanged` 的注入模式)。
211398
211559
  *
211399
- * 拒绝两类 id:`proj:uncategorized`(读侧虚拟卡,存储里没有真行)与 `proj_tmp_*`
211400
- * (工单的兜底归属,删掉等于把工单变成无绑定态——契约里 `Workspace.projectId` 必填)。
211560
+ * 拒绝 `proj:uncategorized`(读侧虚拟卡,存储里没有真行,删它没有意义)。
211401
211561
  */
211402
211562
  async deleteProject(id) {
211403
211563
  const projectId2 = id?.trim();
211404
211564
  if (!projectId2) throw new Error("project id is required");
211405
211565
  if (isUncategorizedProjectId(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
211406
- if (isEphemeralProject(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
211407
211566
  const project = await this.projects.getProject(projectId2);
211408
211567
  if (!project) throw new Error(`project not found: ${projectId2}`);
211409
- const gitLocations = (await this.projects.listContentLocations(projectId2)).filter((loc) => loc.kind === "external-home" && loc.externalHome?.system === "git");
211410
- const now = (/* @__PURE__ */ new Date()).toISOString();
211411
211568
  const bindings = await this.artifacts.listWorkspaceBindings();
211412
211569
  const movedWorkOrderIds = [];
211413
211570
  for (const binding of bindings) {
211414
211571
  if (binding.projectId !== projectId2) continue;
211415
- const moved = await this.unlinkWorkorder(binding.workOrderId);
211572
+ await this.unlinkWorkorder(binding.workOrderId);
211416
211573
  movedWorkOrderIds.push(binding.workOrderId);
211417
- for (const loc of gitLocations) {
211418
- await this.projects.addContentLocation({
211419
- ...loc,
211420
- projectId: moved.projectId,
211421
- // 派生 id:同一条位置搬到同一个临时项目永远算出同一个 id(addContentLocation 是
211422
- // upsert),重跑不会堆出重复的仓库行。
211423
- locationId: `pcl-${moved.projectId}-carried-${loc.locationId}`,
211424
- updatedAt: now
211425
- });
211426
- }
211427
211574
  }
211428
211575
  await this.projects.deleteProject(projectId2);
211429
211576
  if (this.onProjectDeleted) {
@@ -211433,7 +211580,7 @@ var init_service4 = __esm({
211433
211580
  console.warn(`[projects] project-deleted follow-up failed for ${projectId2}: ${error2 instanceof Error ? error2.message : String(error2)}`);
211434
211581
  }
211435
211582
  }
211436
- return { projectId: projectId2, movedWorkOrderIds, carriedGitLocationCount: gitLocations.length };
211583
+ return { projectId: projectId2, movedWorkOrderIds };
211437
211584
  }
211438
211585
  /**
211439
211586
  * 项目关联工单(帧 `1355:4918`):把内部私有的 `listProjectWorkorders` 暴露给路由层。
@@ -212063,7 +212210,7 @@ var init_service4 = __esm({
212063
212210
  if (!snap) return [];
212064
212211
  const worksById = new Map(snap.works.map((w2) => [w2.id, w2]));
212065
212212
  const binding = this.artifacts.getWorkspaceBinding ? await this.artifacts.getWorkspaceBinding(workOrderId).catch(() => null) : null;
212066
- const projectId2 = binding?.projectId ?? ephemeralProjectIdFor(workOrderId);
212213
+ const projectId2 = binding?.projectId ?? UNCATEGORIZED_PROJECT_ID;
212067
212214
  const out = [];
212068
212215
  for (const node2 of snap.nodes) {
212069
212216
  if (!node2.latestAcceptId) continue;
@@ -212193,20 +212340,8 @@ var init_service4 = __esm({
212193
212340
  const filtered = artifacts.filter((artifact) => input.type === void 0 || artifact.type === input.type).filter((artifact) => input.status === void 0 || artifact.status === input.status).sort((a, b2) => b2.updatedAt.localeCompare(a.updatedAt));
212194
212341
  return { artifacts: filtered, summary: summarizeArtifacts(filtered) };
212195
212342
  }
212196
- /** 内部:虚拟卡 跨全部 ephemeral 项目聚合;真项目 → 走原路径。 */
212343
+ /** 内部:虚拟卡与真项目走同一条路——{@link listAllProjectArtifacts} 内部按「无绑定 / 绑到本项目」分流。 */
212197
212344
  async collectProjectArtifacts(projectId2) {
212198
- if (isUncategorizedProjectId(projectId2)) {
212199
- const stored = await this.projects.listProjects();
212200
- const ephemeralIds = new Set(
212201
- stored.filter((project) => isEphemeralProject(project.id)).map((project) => project.id)
212202
- );
212203
- const artifacts = [];
212204
- for (const id of ephemeralIds) {
212205
- const list2 = await this.listAllProjectArtifacts(id);
212206
- artifacts.push(...list2.map((a) => ({ ...a, projectId: UNCATEGORIZED_PROJECT_ID })));
212207
- }
212208
- return artifacts;
212209
- }
212210
212345
  return this.listAllProjectArtifacts(projectId2);
212211
212346
  }
212212
212347
  async listProjectWorkorders(projectId2) {
@@ -212215,17 +212350,19 @@ var init_service4 = __esm({
212215
212350
  const uncategorized = isUncategorizedProjectId(projectId2);
212216
212351
  return buildWorkorderSummaries(this.kernel.model, void 0, (workOrderId) => projectByWorkorder.get(workOrderId) ?? null, void 0, void 0, void 0, (role) => this.kernel.actorForRole(role)).filter((workorder) => {
212217
212352
  const bound = workorder.projectId;
212218
- if (uncategorized) return typeof bound === "string" && isEphemeralProject(bound);
212353
+ if (uncategorized) return bound == null;
212219
212354
  return bound === projectId2;
212220
212355
  });
212221
212356
  }
212222
212357
  async listAllProjectArtifacts(projectId2) {
212223
- const stored = await this.artifacts.listArtifacts({ projectId: projectId2 });
212358
+ const uncategorized = isUncategorizedProjectId(projectId2);
212359
+ const stored = uncategorized ? [] : await this.artifacts.listArtifacts({ projectId: projectId2 });
212224
212360
  const byArtifact = new Map(stored.map((artifact) => [artifact.artifactId, artifact]));
212225
212361
  const bindings = await this.artifacts.listWorkspaceBindings();
212226
212362
  const projectByWorkorder = new Map(bindings.map((binding) => [binding.workOrderId, binding.projectId]));
212227
212363
  for (const artifact of this.kernel.model.artifacts.values()) {
212228
- if (projectByWorkorder.get(artifact.workspace) !== projectId2) continue;
212364
+ const bound = projectByWorkorder.get(artifact.workspace);
212365
+ if (uncategorized ? bound !== void 0 : bound !== projectId2) continue;
212229
212366
  const descriptor = decodeDocumentDescription(artifact.description);
212230
212367
  const info = nodeInfo(this.kernel.model, artifact);
212231
212368
  const revisions = revisionsOf(this.kernel.model, artifact.id);
@@ -216890,8 +217027,18 @@ function wireRecoveredChatTurn(deps) {
216890
217027
  clearInterval(ckptTimer);
216891
217028
  const failed = exitFailed(info);
216892
217029
  if (failed) {
216893
- live.emit({ type: "error", text: exitErrorText(info) });
216894
- itemLedger.recordError(exitErrorText(info));
217030
+ const errText2 = exitErrorText(info);
217031
+ live.emit({ type: "error", text: errText2 });
217032
+ const errorOutcome = itemLedger.recordError(errText2, { ...info.reason ? { reason: info.reason } : {} });
217033
+ live.publishServerItem({
217034
+ itemId: errorOutcome.itemId,
217035
+ itemType: "control",
217036
+ startedVersion: errorOutcome.startedVersion,
217037
+ version: errorOutcome.version,
217038
+ ord: errorOutcome.ord,
217039
+ status: "failed",
217040
+ payload: { code: "error", note: errText2, ...info.reason ? { reason: info.reason } : {} }
217041
+ });
216895
217042
  }
216896
217043
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
216897
217044
  const parts = collectedParts();
@@ -219789,13 +219936,6 @@ var init_memory = __esm({
219789
219936
  async write(ctx, input) {
219790
219937
  this.assertWritable(input, "agent");
219791
219938
  const projectId2 = await this.projectOf(ctx);
219792
- if (input.scope === "project" && projectId2 === null) {
219793
- throw new ApiError(
219794
- 400,
219795
- "MEMORY_NO_PROJECT",
219796
- "\u672C\u8F6E\u89E3\u6790\u4E0D\u51FA\u6240\u5C5E\u9879\u76EE\uFF08\u975E\u6D3E\u53D1\u4F1A\u8BDD\u6216\u4EA7\u7269\u672A\u7ED1\u5B9A\u5DE5\u5355\uFF09\uFF0C\u65E0\u6CD5\u5199\u9879\u76EE\u7EA7\u8BB0\u5FC6\u2014\u2014\u6539\u7528 --scope actor \u5199\u8DE8\u9879\u76EE\u901A\u7528\u7684\u7ECF\u9A8C\u3002"
219797
- );
219798
- }
219799
219939
  return this.writeResolved(ctx, input, input.scope === "project" ? projectId2 : null, "agent");
219800
219940
  }
219801
219941
  /** 控制台管理面写入:actorId/projectId 由受保护的管理路由明确给出,不复用 agent 自用入口。 */
@@ -220905,6 +221045,21 @@ var init_connector_skills = __esm({
220905
221045
  }
220906
221046
  });
220907
221047
 
221048
+ // ../server/src/domains/nodes/runtime-tenancy.ts
221049
+ function foreignNodeBlock(node2, expectedCompanyId) {
221050
+ if (!expectedCompanyId) return null;
221051
+ if (!node2) return null;
221052
+ const owner = node2.companyId;
221053
+ if (!owner) return null;
221054
+ if (owner === expectedCompanyId) return null;
221055
+ return `\u8FD0\u884C\u65F6\u8282\u70B9 ${node2.id} \u5C5E\u4E8E\u516C\u53F8 ${owner}\uFF0C\u4E0D\u5C5E\u4E8E ${expectedCompanyId}\u2014\u2014\u5458\u5DE5\u53EA\u80FD\u7ED1\u5B9A/\u6D3E\u53D1\u5230\u672C\u516C\u53F8\u7684\u8282\u70B9\uFF08ADR-0164 \xA75\uFF1A\u8DE8\u516C\u53F8\u5171\u4EAB\u540C\u4E00\u53F0\u7269\u7406\u673A\u7684\u6B63\u89E3\u662F\u5404\u81EA\u767B\u8BB0\u4E0D\u540C node id\uFF09\u3002`;
221056
+ }
221057
+ var init_runtime_tenancy = __esm({
221058
+ "../server/src/domains/nodes/runtime-tenancy.ts"() {
221059
+ "use strict";
221060
+ }
221061
+ });
221062
+
220908
221063
  // ../server/src/domains/actors/routes.ts
220909
221064
  async function asApiError(run) {
220910
221065
  try {
@@ -221227,6 +221382,9 @@ function actorsDomain(opts) {
221227
221382
  const { service } = await resolveCtx(req.auth.companyId);
221228
221383
  const b2 = req.body;
221229
221384
  if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
221385
+ const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
221386
+ const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
221387
+ if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
221230
221388
  const { clearedModel } = await service.bind({ actorId: b2.actorId, nodeId: b2.nodeId, runtimeKind: b2.runtimeKind, status: b2.status ?? "active" }, req.auth.actor);
221231
221389
  return { status: 201, body: { ok: true, ...clearedModel ? { clearedModel } : {} } };
221232
221390
  });
@@ -222036,6 +222194,7 @@ var init_routes3 = __esm({
222036
222194
  init_image_upload();
222037
222195
  init_skill_materializer();
222038
222196
  init_connector_skills();
222197
+ init_runtime_tenancy();
222039
222198
  CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
222040
222199
  RevealRateLimiter = class {
222041
222200
  constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
@@ -222232,7 +222391,7 @@ function createActorsDomain(opts) {
222232
222391
  return {
222233
222392
  service: defaultCtx.service,
222234
222393
  resolveCtx,
222235
- register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {} })
222394
+ register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
222236
222395
  };
222237
222396
  }
222238
222397
  var import_node_crypto49;
@@ -222434,12 +222593,9 @@ function projectsDomain(opts) {
222434
222593
  }
222435
222594
  });
222436
222595
  router.get("/api/projects", async (req) => {
222437
- const inc = req.query?.get("includeEphemeral");
222438
- const includeEphemeral = inc === "1" || inc === "true";
222439
222596
  const uncatRaw = req.query?.get("includeUncategorized");
222440
222597
  const includeUncategorized = uncatRaw === null || uncatRaw === void 0 ? void 0 : !(uncatRaw === "0" || uncatRaw === "false");
222441
222598
  const projects = await (await svc(req)).listProjects({
222442
- includeEphemeral,
222443
222599
  ...includeUncategorized !== void 0 ? { includeUncategorized } : {}
222444
222600
  });
222445
222601
  const page = paginate(projects.map(toApiProject), req.query);
@@ -222477,8 +222633,7 @@ function projectsDomain(opts) {
222477
222633
  body: {
222478
222634
  ok: true,
222479
222635
  project_id: result.projectId,
222480
- moved_work_order_ids: result.movedWorkOrderIds,
222481
- carried_git_location_count: result.carriedGitLocationCount
222636
+ moved_work_order_ids: result.movedWorkOrderIds
222482
222637
  }
222483
222638
  };
222484
222639
  } catch (err) {
@@ -222720,8 +222875,8 @@ ${errs.join("\n")}`);
222720
222875
  const nulledPatch = "project_id" in b2 && b2.project_id === null || "projectId" in b2 && b2.projectId === null || b2.project_id === "null" || b2.project_id === "" || b2.projectId === "null" || b2.projectId === "";
222721
222876
  if (nulledPatch) {
222722
222877
  try {
222723
- const binding = await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222724
- return { status: 200, body: { work_order_id: binding.workOrderId, project_id: binding.projectId } };
222878
+ await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222879
+ return { status: 200, body: { work_order_id: req.params.workOrderId, project_id: null } };
222725
222880
  } catch (err) {
222726
222881
  throw mapProjectError(err);
222727
222882
  }
@@ -222737,8 +222892,8 @@ ${errs.join("\n")}`);
222737
222892
  });
222738
222893
  router.delete("/api/work-orders/:workOrderId/project", async (req) => {
222739
222894
  try {
222740
- const binding = await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222741
- return { status: 200, body: { work_order_id: binding.workOrderId, project_id: binding.projectId } };
222895
+ await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222896
+ return { status: 200, body: { work_order_id: req.params.workOrderId, project_id: null } };
222742
222897
  } catch (err) {
222743
222898
  throw mapProjectError(err);
222744
222899
  }
@@ -223256,7 +223411,7 @@ var init_routes5 = __esm({
223256
223411
  "use strict";
223257
223412
  init_router();
223258
223413
  init_src();
223259
- init_ephemeral_project();
223414
+ init_uncategorized_project();
223260
223415
  }
223261
223416
  });
223262
223417
 
@@ -228322,13 +228477,15 @@ async function buildChatPreview(chatStore, sessionId, who) {
228322
228477
  if (lastAsst) lines.push({ label: who, text: clip2(lastAsst.content) });
228323
228478
  return lines.length ? { lines } : void 0;
228324
228479
  }
228325
- async function buildChatInbox(chatStore, markers, me, resolveActor) {
228480
+ async function buildChatInbox(chatStore, markers, me, resolveActor, excludeChildSessions) {
228326
228481
  const sessions = await chatStore.listSessions(me);
228327
228482
  const byScope = new Map(markers.map((m2) => [m2.scope, m2]));
228328
228483
  const unread = sessions.filter((s2) => s2.lastTurnQuality !== void 0 && isUnread(s2.touchedAt, byScope.get(chatScope(s2.id)))).sort((a, b2) => a.touchedAt < b2.touchedAt ? 1 : a.touchedAt > b2.touchedAt ? -1 : 0);
228484
+ const excluded = excludeChildSessions ? new Set(await excludeChildSessions(unread.map((s2) => s2.id)).catch(() => [])) : /* @__PURE__ */ new Set();
228329
228485
  const out = [];
228330
228486
  let previewsFetched = 0;
228331
228487
  for (const s2 of unread) {
228488
+ if (excluded.has(s2.id)) continue;
228332
228489
  const who = resolveActor?.(s2.aiActorId)?.name ?? "\u52A9\u7406";
228333
228490
  const abnormal = s2.lastTurnQuality === "abnormal";
228334
228491
  const preview2 = previewsFetched < PREVIEW_FETCH_CAP ? await buildChatPreview(chatStore, s2.id, who) : void 0;
@@ -229718,7 +229875,7 @@ function collabDomain(opts) {
229718
229875
  if (projectFilter) {
229719
229876
  items = items.filter((wo) => {
229720
229877
  const bound = wo.projectId;
229721
- if (isUncategorizedProjectId(projectFilter)) return typeof bound === "string" && isEphemeralProject(bound);
229878
+ if (isUncategorizedProjectId(projectFilter)) return bound == null;
229722
229879
  return bound === projectFilter;
229723
229880
  });
229724
229881
  }
@@ -230124,7 +230281,8 @@ function collabDomain(opts) {
230124
230281
  return void 0;
230125
230282
  }
230126
230283
  })();
230127
- const chatItems = chatStore && myReadMarkers ? await buildChatInbox(chatStore, markers, me, resolveActor) : [];
230284
+ const excludeChildSessions = opts.resolveDelegationChildIds ? (ids2) => opts.resolveDelegationChildIds(req.auth.companyId, ids2) : void 0;
230285
+ const chatItems = chatStore && myReadMarkers ? await buildChatInbox(chatStore, markers, me, resolveActor, excludeChildSessions) : [];
230128
230286
  let waitingItems = [];
230129
230287
  if (engineStore) {
230130
230288
  const snaps = (await Promise.all(
@@ -230419,7 +230577,7 @@ var init_collab = __esm({
230419
230577
  init_workorder_metrics();
230420
230578
  init_chat_workorder_files();
230421
230579
  init_organization_usage();
230422
- init_ephemeral_project();
230580
+ init_uncategorized_project();
230423
230581
  init_tags();
230424
230582
  init_activity2();
230425
230583
  init_node_state();
@@ -230706,24 +230864,16 @@ function makeSeededWorkorderCreator(deps) {
230706
230864
  const blobs = company?.blobs ?? deps.blobs;
230707
230865
  const registry2 = company?.registry ?? deps.registry;
230708
230866
  const artifactState = company ? company.artifactState : deps.artifactState;
230709
- const createProject = company ? company.createProject : deps.createProject;
230867
+ const workorderDrafts = company ? company.workorderDrafts : deps.workorderDrafts;
230710
230868
  const workspace = input.idempotencyKey ? workspaceForKey(input.idempotencyKey) : genWorkspace();
230711
230869
  if (input.idempotencyKey) {
230712
230870
  const existing = existingWorkorder(kernel, workspace);
230713
230871
  if (existing) return existing;
230714
230872
  }
230715
- if (artifactState && (input.projectId || createProject)) {
230716
- await ensureWorkorderProject({
230873
+ if (artifactState && input.projectId) {
230874
+ await bindWorkorderProject({
230717
230875
  workOrderId: workspace,
230718
- ...input.projectId ? { projectId: input.projectId } : {},
230719
- ...input.title ? { title: input.title } : {},
230720
- // ADR 0207:临时项目的创建人 = 建这张单的**真人**。`humanActorId` 是 B 路径 handler
230721
- // 从 `x-chat-session-id` 反查填进来的「陪发起人聊出此单的人」,与 brief.owner 同源;
230722
- // 没有它时只在 `input.actor` 本身是人时才用——剧本由 agent 触发时落 NULL,
230723
- // 不把执行者写成负责人。
230724
- ...input.humanActorId ? { createdBy: input.humanActorId } : input.actor.startsWith("actor:human:") ? { createdBy: input.actor } : {},
230725
- createProject: createProject ?? (async ({ id }) => ({ id })),
230726
- // 上面的守卫保证走不到这个分支
230876
+ projectId: input.projectId,
230727
230877
  upsertBinding: (b2) => artifactState.upsertWorkspaceBinding(b2),
230728
230878
  ...deps.onWarn ? { onWarn: deps.onWarn } : {}
230729
230879
  });
@@ -230755,8 +230905,8 @@ function makeSeededWorkorderCreator(deps) {
230755
230905
  if (!preview2.endState.ok) {
230756
230906
  throw new Error(`\u5267\u672C\u5EFA\u5355\u7EC8\u6001\u6821\u9A8C\u4E0D\u8FC7\uFF1A${preview2.endState.violations.join("\uFF1B")}`);
230757
230907
  }
230758
- if (input.stage === true && deps.workorderDrafts) {
230759
- const draft = await deps.workorderDrafts.submit({
230908
+ if (input.stage === true && workorderDrafts) {
230909
+ const draft = await workorderDrafts.submit({
230760
230910
  workspace,
230761
230911
  plan: planned.plan,
230762
230912
  rootArtifactId: planned.rootArtifactId,
@@ -230818,7 +230968,7 @@ var init_create_seeded_workorder = __esm({
230818
230968
  "../server/src/domains/collab/create-seeded-workorder.ts"() {
230819
230969
  "use strict";
230820
230970
  import_node_crypto57 = require("node:crypto");
230821
- init_ephemeral_project();
230971
+ init_uncategorized_project();
230822
230972
  init_planner();
230823
230973
  enc2 = (s2) => new TextEncoder().encode(s2);
230824
230974
  }
@@ -234058,6 +234208,9 @@ function createChatSessionsDomain(opts) {
234058
234208
  childSessionId: record8.childSessionId,
234059
234209
  expertActorId: record8.expertActorId,
234060
234210
  ...expert?.name ? { expertName: expert.name } : {},
234211
+ /* 头像与名字同一本名册、同一次查询取出来。缺了它,委派卡与输入框上方状态条只能退回
234212
+ 姓名首字的灰圆(发起人 2026-09-10 报的「灰底 D」)——那两处画的是「[头像] 名字 进行中」。 */
234213
+ ...expert?.avatar ? { expertAvatar: expert.avatar } : {},
234061
234214
  state: record8.state,
234062
234215
  ...record8.failureKind ? { failureKind: record8.failureKind } : {},
234063
234216
  ...record8.label ? { label: record8.label } : {},
@@ -237750,7 +237903,7 @@ function playbooksDomain(opts) {
237750
237903
  const drafts = opts.resolveWorkorderDrafts ? await opts.resolveWorkorderDrafts(req.auth?.companyId) : opts.workorderDrafts;
237751
237904
  if (result.draftId && chatSid && drafts) {
237752
237905
  try {
237753
- await drafts.tagSession(result.draftId, chatSid, opts.liveChat?.runFor(chatSid));
237906
+ await drafts.tagSession(result.draftId, chatSid, opts.liveChat?.runFor(chatSid), result.workspace);
237754
237907
  } catch (err) {
237755
237908
  console.warn(`[playbooks] tagSession(${result.draftId}) \u5931\u8D25\uFF0C\u4E0D\u963B\u585E\u5EFA\u5355\u54CD\u5E94:`, err);
237756
237909
  }
@@ -238497,7 +238650,10 @@ var init_daemon_adapter = __esm({
238497
238650
  } else if (msg.type === "session_normalized_event") {
238498
238651
  this.confirmDelivery(msg.dispatchId, "started");
238499
238652
  const entry = this.pending.get(msg.dispatchId);
238500
- if (!entry) return;
238653
+ if (!entry) {
238654
+ if (this.stashEnabled) this.stashFrame(msg.dispatchId, { type: "normalized", event: msg.event });
238655
+ return;
238656
+ }
238501
238657
  if (entry.normalizedCbs.length === 0) entry.normalizedBacklog.push(msg.event);
238502
238658
  else for (const cb of entry.normalizedCbs) cb(msg.event);
238503
238659
  } else if (msg.type === "session_output") {
@@ -238630,6 +238786,7 @@ var init_daemon_adapter = __esm({
238630
238786
  let replayOutput;
238631
238787
  let replayTelemetry;
238632
238788
  let replayLiveEvent;
238789
+ let replayNormalized;
238633
238790
  const flushReplay = () => {
238634
238791
  while (stashed.frames.length > 0) {
238635
238792
  const frame = stashed.frames[0];
@@ -238641,6 +238798,10 @@ var init_daemon_adapter = __esm({
238641
238798
  if (!replayTelemetry) return;
238642
238799
  stashed.frames.shift();
238643
238800
  replayTelemetry(frame.event);
238801
+ } else if (frame.type === "normalized") {
238802
+ if (!replayNormalized) return;
238803
+ stashed.frames.shift();
238804
+ replayNormalized(frame.event);
238644
238805
  } else {
238645
238806
  if (!replayLiveEvent) return;
238646
238807
  stashed.frames.shift();
@@ -238679,9 +238840,11 @@ var init_daemon_adapter = __esm({
238679
238840
  },
238680
238841
  ...supportsNormalizedEvents ? {
238681
238842
  onNormalizedProviderEvent(cb) {
238682
- entry.normalizedCbs.push(cb);
238843
+ replayNormalized ??= cb;
238844
+ flushReplay();
238683
238845
  const backlog = entry.normalizedBacklog.splice(0, entry.normalizedBacklog.length);
238684
238846
  for (const e of backlog) cb(e);
238847
+ entry.normalizedCbs.push(cb);
238685
238848
  }
238686
238849
  } : {},
238687
238850
  // ADR-0093 对话追加(重挂句柄同 spawn 句柄)。
@@ -242164,6 +242327,9 @@ var init_postgres_projects = __esm({
242164
242327
  const r = await this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`);
242165
242328
  return r.rows.map(rowToWorkspaceBinding);
242166
242329
  }
242330
+ async deleteWorkspaceBinding(workOrderId) {
242331
+ await this.pool.query(`DELETE FROM ${this.s}.workspace_bindings WHERE work_order_id = $1`, [workOrderId]);
242332
+ }
242167
242333
  /* ---------- 任务输入文件(PRD §2.6.3 建单交接;薄旁存,非 oplog) ---------- */
242168
242334
  async addWorkOrderFile(file) {
242169
242335
  await this.pool.query(
@@ -255641,8 +255807,8 @@ var init_cutover_aware_workorder_drafts = __esm({
255641
255807
  async snapshotForNewRun(d, runId) {
255642
255808
  return (await this.pickWritable()).snapshotForNewRun(d, runId);
255643
255809
  }
255644
- async tagSession(id, s2, runId) {
255645
- return (await this.pickWritable()).tagSession(id, s2, runId);
255810
+ async tagSession(id, s2, runId, expectedWorkspace) {
255811
+ return (await this.pickWritable()).tagSession(id, s2, runId, expectedWorkspace);
255646
255812
  }
255647
255813
  async markEdited(id) {
255648
255814
  return (await this.pickWritable()).markEdited(id);
@@ -256613,6 +256779,7 @@ var init_src11 = __esm({
256613
256779
  init_sender();
256614
256780
  init_routes7();
256615
256781
  init_runtime_drift();
256782
+ init_runtime_tenancy();
256616
256783
  init_types4();
256617
256784
  init_roles();
256618
256785
  init_node_store();
@@ -256664,7 +256831,7 @@ var init_src11 = __esm({
256664
256831
  init_connector_skills();
256665
256832
  init_connector_tool_versions();
256666
256833
  init_board_watch();
256667
- init_ephemeral_project();
256834
+ init_uncategorized_project();
256668
256835
  init_run_settlement();
256669
256836
  init_reconcile_terminal_runs();
256670
256837
  init_run_settlement_wiring();
@@ -258176,6 +258343,9 @@ async function startServe(opts) {
258176
258343
  let memStoreShared;
258177
258344
  let isKnowledgeQueryAvailable = async (_actorId) => false;
258178
258345
  const actors = createActorsDomain({
258346
+ // 租户闸(ADR-0164 §5):`POST /api/bindings` 据它拦「把本公司员工绑到别家公司的机器上」。
258347
+ // 闭包读当前 nodeStore(下方 PG 分支会重赋值),与 `listBindingModelIds` 同一写法。
258348
+ resolveNodeTenancy: async (nodeId) => nodeStore.getNode(nodeId),
258179
258349
  store: registryStore,
258180
258350
  oplog,
258181
258351
  kernel: defaultCompanyKernel,
@@ -258244,7 +258414,10 @@ async function startServe(opts) {
258244
258414
  prefsStore: humanPrefsStore,
258245
258415
  registry: registryStore,
258246
258416
  actors: actors.service,
258247
- listRuntimes: (nodeId) => nodeStore.listRuntimes(nodeId),
258417
+ // 默认公司那一份。**闭包求值**:`defaultCompanyId` let,startup 后才被库里的真实 id 覆盖
258418
+ // (同 :1575 那句「必须是调用时求值」)。
258419
+ companyId: () => defaultCompanyId,
258420
+ listRuntimes: (nodeId, scope) => nodeStore.listRuntimes(nodeId, scope),
258248
258421
  isPlatformAdmin: async () => false
258249
258422
  // 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
258250
258423
  });
@@ -258536,8 +258709,6 @@ async function startServe(opts) {
258536
258709
  getSchema: () => schema,
258537
258710
  registry: registryStore,
258538
258711
  artifactState: artifactStateStore,
258539
- // 决策 C:剧本建单没给 projectId 时,兜底建一个由工单派生的临时项目并绑上。
258540
- createProject: makeEnsureProjectFromStore(projectStateStore),
258541
258712
  onWarn: (m2) => console.warn(m2),
258542
258713
  workorderDrafts,
258543
258714
  buildDraftGraph: async (draft) => buildWorkorderDraftDetail(draft, await buildResolver(registryStore), buildTypeReviewerResolver(schema, await registryStore.listActors())).graph,
@@ -258559,8 +258730,12 @@ async function startServe(opts) {
258559
258730
  blobs: engine2.blobs,
258560
258731
  registry: engine2.registry ?? registryStore,
258561
258732
  artifactState: stores.artifacts,
258562
- // 兜底临时项目也要建在本公司的库里,否则绑定两端分处两个 schema。
258563
- createProject: makeEnsureProjectFromStore(stores.projects)
258733
+ // 草案 store 必须按公司拿(2026-09-10 事故):此前它是装配期捕获的共享库实例,
258734
+ // `stage: true` 建出来的草案落大通铺、读侧却按公司解析,本公司永远查不到自己的草案。
258735
+ // ⚠ 这个 helper 的 const 声明在本处之后,只能在**请求时**才求值的闭包里引用
258736
+ // (同上方 resolveChatSession 的写法),直接在装配期引用会撞 TDZ。
258737
+ // planner 阻断项旁账**不在本次范围**,理由见 create-seeded-workorder.ts 里那段注释。
258738
+ workorderDrafts: await workorderDraftsFor(companyId)
258564
258739
  };
258565
258740
  }
258566
258741
  } : {}
@@ -259871,7 +260046,9 @@ async function startServe(opts) {
259871
260046
  prefsStore: plane?.humanPrefs ?? humanPrefsStore,
259872
260047
  registry: engine2.registry,
259873
260048
  actors: ctx.service,
259874
- listRuntimes: (nodeId) => nodeStore.listRuntimes(nodeId),
260049
+ // 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
260050
+ companyId: () => cid,
260051
+ listRuntimes: (nodeId, scope) => nodeStore.listRuntimes(nodeId, scope),
259875
260052
  isPlatformAdmin: async (caller) => {
259876
260053
  const member = await controlPlaneStore.getMember(cid, caller);
259877
260054
  return member?.role === "owner";
@@ -259965,6 +260142,14 @@ async function startServe(opts) {
259965
260142
  // ADR-0086:chat 知会条目(与 readMarkers 成对)
259966
260143
  // ADR「多租户数据面收口」§D2:知会条目按**当前公司**取。装了路由才给(dev 文件模式沿用单实例)。
259967
260144
  ...chatStoreRouter ? { resolveChatSession: chatStoreFor } : {},
260145
+ /* 委派子会话不进铃铛(发起人 2026-09-10):专家跑完回流到父会话,父会话那条已经通知过一次。
260146
+ 判据与左侧会话列表那条路同一个读口(`listChildSessionIds`,见下面 chat-sessions 域的
260147
+ `delegations` 接线),不另造。灰度关着 → 回空集合 = 不滤,与接入前逐字一致。 */
260148
+ resolveDelegationChildIds: async (companyId, sessionIds) => {
260149
+ if (!chatDelegationEnabledFromEnv() || sessionIds.length === 0) return [];
260150
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
260151
+ return ledger ? ledger.listChildSessionIds(sessionIds) : [];
260152
+ },
259968
260153
  readMarkers: readMarkerStore,
259969
260154
  // bundle=`inbox`:水位按当前公司取(与上面 resolveChatSession 成对)。
259970
260155
  ...pgPool ? { resolveReadMarkers: readMarkerStoreFor } : {},
@@ -260176,7 +260361,9 @@ async function startServe(opts) {
260176
260361
  listProjects: (companyId) => companyId === defaultCompanyId ? projectStateStore.listProjects() : Promise.resolve([]),
260177
260362
  listProjectMembers: async (companyId, projectId2) => companyId === defaultCompanyId ? (await projectStateStore.listProjectMembers(projectId2)).flatMap((member) => member.kind === "system" ? [] : [{ actorId: member.actorId, kind: member.kind, status: member.status === "active" ? "active" : "inactive" }]) : [],
260178
260363
  getWorkspaceBinding: (companyId, workorderId) => companyId === defaultCompanyId ? artifactStateStore.getWorkspaceBinding(workorderId) : Promise.resolve(null),
260179
- isEphemeralProject: (companyId, projectId2) => companyId !== defaultCompanyId || isEphemeralProject(projectId2)
260364
+ // 非默认公司暂不支持项目级知识作用域(这里只有默认公司的 projectStateStore/名册可读),
260365
+ // 一律降级到组织作用域。旧版这个谓词还兼着"是不是 proj_tmp_ 临时项目",临时项目退役后只剩这一半。
260366
+ isProjectScopeUnsupported: (companyId) => companyId !== defaultCompanyId
260180
260367
  },
260181
260368
  actor: async (actorId, organizationId) => {
260182
260369
  if (actorId === SYSTEM_ACTOR2) return { id: actorId, kind: "system", status: "active" };
@@ -260681,7 +260868,9 @@ async function startServe(opts) {
260681
260868
  }
260682
260869
  binding = currentBinding;
260683
260870
  }
260684
- const requireDispatchNode = (target) => {
260871
+ const requireDispatchNode = async (target) => {
260872
+ const foreign = foreignNodeBlock(await nodeStore.getNode(target.nodeId).catch(() => null), runCompanyId);
260873
+ if (foreign) throw new ApiError(409, "NODE_NOT_IN_COMPANY", foreign);
260685
260874
  const connected = hub?.connectedDaemons().find((d) => d.daemonId === target.nodeId);
260686
260875
  if (!connected) {
260687
260876
  throw new ApiError(409, "NODE_OFFLINE", `\u7ED1\u5B9A\u8282\u70B9 ${target.nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF`);
@@ -260691,7 +260880,7 @@ async function startServe(opts) {
260691
260880
  }
260692
260881
  return connected;
260693
260882
  };
260694
- let node2 = requireDispatchNode(binding);
260883
+ let node2 = await requireDispatchNode(binding);
260695
260884
  if (!chatRemoteAdapter) {
260696
260885
  throw new ApiError(503, "NODE_GATEWAY_NOT_READY", "node-gateway \u5C1A\u672A\u5C31\u7EEA");
260697
260886
  }
@@ -260771,7 +260960,7 @@ async function startServe(opts) {
260771
260960
  }
260772
260961
  if (stableDispatch.target.nodeId !== binding.nodeId || stableDispatch.target.runtimeKind !== binding.runtimeKind) {
260773
260962
  binding = { actorId, ...stableDispatch.target, status: "active" };
260774
- node2 = requireDispatchNode(binding);
260963
+ node2 = await requireDispatchNode(binding);
260775
260964
  requireNodeConnectors();
260776
260965
  }
260777
260966
  }
@@ -262124,6 +262313,8 @@ async function startServe(opts) {
262124
262313
  checkDispatchable: async ({ actor, binding }) => {
262125
262314
  try {
262126
262315
  if (!binding.nodeId) return null;
262316
+ const foreign = foreignNodeBlock(await nodeStore.getNode(binding.nodeId).catch(() => null), companyId);
262317
+ if (foreign) return { reason: "node-not-in-company", errorMessage: foreign };
262127
262318
  const drift = runtimeDriftBlock(await nodeStore.listRuntimes(binding.nodeId), binding.nodeId, binding.runtimeKind);
262128
262319
  if (drift) return drift;
262129
262320
  const health = healthOfNode(binding.nodeId);
@@ -269282,6 +269473,9 @@ ${res.warning}`);
269282
269473
  ...expect !== void 0 ? { expectedVersion: Number(expect) } : {}
269283
269474
  });
269284
269475
  println(`\u5DF2\u8BB0\u4E0B ${rec.memId}\uFF08v${rec.version}\uFF0C${rec.projectId === null ? "actor \u7EA7\xB7\u8DE8\u9879\u76EE\u901A\u7528" : `\u9879\u76EE ${rec.projectId}`}\uFF09`);
269476
+ if ((flags.get("scope") ?? "actor") === "project" && rec.projectId === null) {
269477
+ println(" \u24D8 \u672C\u8F6E\u89E3\u6790\u4E0D\u51FA\u6240\u5C5E\u9879\u76EE\uFF08\u8FD9\u5F20\u4EFB\u52A1\u6CA1\u6302\u9879\u76EE\uFF09\uFF0C\u5DF2\u6309 actor \u7EA7\u8BB0\u4E0B\u2014\u2014\u60F3\u8BA9\u5B83\u843D\u5230\u9879\u76EE\u540D\u4E0B\uFF0C\u5148\u7ED9\u4EFB\u52A1\u6307\u5B9A\u9879\u76EE\u3002");
269478
+ }
269285
269479
  } catch (err) {
269286
269480
  const body2 = err instanceof ApiRequestError ? err.body : void 0;
269287
269481
  if (body2?.current) {
@@ -270840,7 +271034,7 @@ function shimScript() {
270840
271034
  }
270841
271035
 
270842
271036
  // src/index.ts
270843
- var PKG_VERSION = true ? "2.2.10" : "dev";
271037
+ var PKG_VERSION = true ? "2.2.11" : "dev";
270844
271038
  var LOCAL_BIN = localBin();
270845
271039
  var NPM_PREFIX = npmPrefix();
270846
271040
  var INSTANCE = DEFAULT_INSTANCE;