oasis_test_v2 2.2.10 → 2.2.12

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 +777 -373
  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;
@@ -28721,6 +28783,8 @@ var init_chat_item_ledger = __esm({
28721
28783
  * 也不算进本段的 assistant item 计数。
28722
28784
  */
28723
28785
  recordUserInput(text5, attachments) {
28786
+ const payload = { role: "user", text: text5 };
28787
+ if (attachments?.length) payload.attachments = attachments;
28724
28788
  this.insert({
28725
28789
  kind: "text",
28726
28790
  role: "user",
@@ -28731,6 +28795,7 @@ var init_chat_item_ledger = __esm({
28731
28795
  inContent: false,
28732
28796
  persist: true,
28733
28797
  forceNullMessage: true,
28798
+ payload,
28734
28799
  metadata: {
28735
28800
  [CHAT_ITEM_META_LEGACY_CONTENT]: false,
28736
28801
  ...attachments?.length ? { attachments } : {}
@@ -29049,7 +29114,8 @@ var init_chat_item_ledger = __esm({
29049
29114
  ord: created.ord,
29050
29115
  version: writeVersion,
29051
29116
  role: input.role,
29052
- text: input.text
29117
+ text: input.text,
29118
+ payload
29053
29119
  });
29054
29120
  } catch {
29055
29121
  }
@@ -29324,6 +29390,8 @@ function runBackgroundAssistantChatTurn(deps) {
29324
29390
  ...assistantMsgId ? { messageId: assistantMsgId } : {},
29325
29391
  log: log3
29326
29392
  });
29393
+ const liveTurn = deps.liveChat && deps.liveHandle ? deps.liveChat.start(deps.chatSessionId, deps.liveHandle, { items: itemLedger }) : void 0;
29394
+ if (assistantMsgId) liveTurn?.setAssistantMessageId(assistantMsgId);
29327
29395
  const normalizedSource = typeof deps.session.onNormalizedProviderEvent === "function" ? {
29328
29396
  onNormalizedProviderEvent: deps.session.onNormalizedProviderEvent.bind(deps.session),
29329
29397
  finishTurn: (signal) => deps.session.finishNormalizedTurn?.(signal)
@@ -29349,7 +29417,8 @@ function runBackgroundAssistantChatTurn(deps) {
29349
29417
  let ckptDirty = false;
29350
29418
  normalizedSource.onNormalizedProviderEvent((event) => {
29351
29419
  try {
29352
- itemLedger.apply(event);
29420
+ if (liveTurn) liveTurn.applyNormalizedEvent(event);
29421
+ else itemLedger.apply(event);
29353
29422
  } catch {
29354
29423
  }
29355
29424
  ckptDirty = true;
@@ -29387,34 +29456,59 @@ function runBackgroundAssistantChatTurn(deps) {
29387
29456
  }
29388
29457
  const finalize = async (status) => {
29389
29458
  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()
29459
+ try {
29460
+ await checkpointChain.catch(() => void 0);
29461
+ normalizedSource.finishTurn(status === "done" ? "completed" : "failed");
29462
+ itemLedger.finish(status === "done" ? "completed" : "failed");
29463
+ await itemLedger.drain();
29464
+ const projectedContent = itemLedger.projectContent();
29465
+ const parts = collectedParts();
29466
+ await finalizeAssistantRow({
29467
+ chatStore: deps.chatStore,
29468
+ chatSessionId: deps.chatSessionId,
29469
+ assistantMsgId,
29470
+ content: projectedContent,
29471
+ parts,
29472
+ status,
29473
+ ...runId ? { runId } : {},
29474
+ now
29475
+ });
29476
+ if (typeof deps.chatStore.updateSession === "function") {
29477
+ try {
29478
+ await deps.chatStore.updateSession(deps.chatSessionId, {
29479
+ runtimeSessionId: deps.session.nativeSessionId ?? null,
29480
+ touchedAt: now()
29481
+ });
29482
+ } catch {
29483
+ }
29484
+ }
29485
+ if (liveTurn) {
29486
+ if (projectedContent) liveTurn.emit({ type: "text", text: projectedContent });
29487
+ liveTurn.emitLive({
29488
+ protocolVersion: 2,
29489
+ streamId: `background:${deps.chatSessionId}`,
29490
+ turnId: deps.turnId ?? fallbackTurnId(runId),
29491
+ itemId: `background:${assistantMsgId ?? runId}`,
29492
+ itemType: "message",
29493
+ operation: "completed",
29494
+ payload: { role: "assistant", text: projectedContent }
29411
29495
  });
29496
+ }
29497
+ try {
29498
+ deps.onSettled?.();
29412
29499
  } catch {
29413
29500
  }
29414
- }
29415
- try {
29416
- deps.onSettled?.();
29417
- } catch {
29501
+ } finally {
29502
+ liveTurn?.emit({ type: "done" });
29503
+ liveTurn?.emitLive({
29504
+ protocolVersion: 2,
29505
+ streamId: `background:${deps.chatSessionId}`,
29506
+ turnId: deps.turnId ?? fallbackTurnId(runId),
29507
+ itemId: "background:terminal",
29508
+ itemType: "control",
29509
+ operation: status === "done" ? "turn_completed" : "turn_failed"
29510
+ });
29511
+ liveTurn?.finish(status);
29418
29512
  }
29419
29513
  };
29420
29514
  void Promise.resolve(deps.session.done).then(() => finalize("done").catch(() => void 0)).catch((err) => {
@@ -142333,71 +142427,6 @@ var init_collect = __esm({
142333
142427
  }
142334
142428
  });
142335
142429
 
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
142430
  // ../server/src/domains/collab/timeline.ts
142402
142431
  function buildNodeTimeline(artifactId, snap, runIdByTarget) {
142403
142432
  if (!snap) return { artifactId, items: [] };
@@ -159818,8 +159847,9 @@ var init_live_chat = __esm({
159818
159847
  DEFAULT_LIVE_COALESCE_MS = 24;
159819
159848
  DEFAULT_LIVE_COALESCE_MAX_CHUNKS = 32;
159820
159849
  DEFAULT_LIVE_COALESCE_MAX_CHARS = 64 * 1024;
159821
- LiveChatRegistry = class {
159850
+ LiveChatRegistry = class _LiveChatRegistry {
159822
159851
  turns = /* @__PURE__ */ new Map();
159852
+ turnStartWaiters = /* @__PURE__ */ new Map();
159823
159853
  epochs = /* @__PURE__ */ new Map();
159824
159854
  bufferMax;
159825
159855
  graceMs;
@@ -159865,6 +159895,15 @@ var init_live_chat = __esm({
159865
159895
  v3Items: /* @__PURE__ */ new Map()
159866
159896
  };
159867
159897
  this.turns.set(chatSessionId, turn);
159898
+ this.flushPendingUserItems(chatSessionId);
159899
+ const waiters = this.turnStartWaiters.get(chatSessionId);
159900
+ this.turnStartWaiters.delete(chatSessionId);
159901
+ for (const wake of waiters ?? []) {
159902
+ try {
159903
+ wake();
159904
+ } catch {
159905
+ }
159906
+ }
159868
159907
  return {
159869
159908
  runtimeSessionId: turn.runtimeSessionId,
159870
159909
  epoch: turn.liveEpoch,
@@ -159937,6 +159976,26 @@ var init_live_chat = __esm({
159937
159976
  ...opts.items ? { items: opts.items } : {}
159938
159977
  };
159939
159978
  }
159979
+ /** 只通知开轮,不承载内容。先检查再登记均为同步,避免空闲检查与订阅之间漏掉开轮。 */
159980
+ onNextTurn(chatSessionId, wake) {
159981
+ if (this.isRunning(chatSessionId)) {
159982
+ wake();
159983
+ return () => {
159984
+ };
159985
+ }
159986
+ let waiters = this.turnStartWaiters.get(chatSessionId);
159987
+ if (!waiters) {
159988
+ waiters = /* @__PURE__ */ new Set();
159989
+ this.turnStartWaiters.set(chatSessionId, waiters);
159990
+ }
159991
+ waiters.add(wake);
159992
+ return () => {
159993
+ waiters.delete(wake);
159994
+ if (waiters.size === 0 && this.turnStartWaiters.get(chatSessionId) === waiters) {
159995
+ this.turnStartWaiters.delete(chatSessionId);
159996
+ }
159997
+ };
159998
+ }
159940
159999
  /**
159941
160000
  * 归一层中间事件 → 带 `itemId` 的 item op(ADR D4 第 2 跳)+ v3 帧 dual-emit(S3)。
159942
160001
  *
@@ -160164,9 +160223,34 @@ var init_live_chat = __esm({
160164
160223
  * (`server.ts` 里 `recordUserSubmission` 在 `liveChat.start` 之前)。拿不到就不发——
160165
160224
  * 前端退回「等下一次快照」,与修复前一致,不会更坏。
160166
160225
  */
160226
+ /**
160227
+ * 「user item 已落库、但这条会话的 live 轮还没 start」时的暂存位(见 {@link publishUserItem})。
160228
+ *
160229
+ * 每会话只留最近几条:它的唯一用途是补发**当前这一次提交**,不是可靠队列。留太多等于把
160230
+ * 一条早就过期的 user 帧在下一轮开头推出去,那比丢了更糟(用户会看到上一轮的话冒出来)。
160231
+ */
160232
+ pendingUserItems = /* @__PURE__ */ new Map();
160233
+ /** 单会话暂存上限。正常只会有 1 条(一次提交一条);给到 4 是为了并发提交时不至于丢。 */
160234
+ static PENDING_USER_ITEM_MAX = 4;
160235
+ bufferUserItem(chatSessionId, item) {
160236
+ const list2 = this.pendingUserItems.get(chatSessionId) ?? [];
160237
+ list2.push(item);
160238
+ while (list2.length > _LiveChatRegistry.PENDING_USER_ITEM_MAX) list2.shift();
160239
+ this.pendingUserItems.set(chatSessionId, list2);
160240
+ }
160241
+ /** `start` 之后立刻调:把暂存的 user item 补发出去。发不出去就丢掉,不再往下一轮带。 */
160242
+ flushPendingUserItems(chatSessionId) {
160243
+ const list2 = this.pendingUserItems.get(chatSessionId);
160244
+ if (!list2?.length) return;
160245
+ this.pendingUserItems.delete(chatSessionId);
160246
+ for (const item of list2) this.publishUserItem(chatSessionId, item);
160247
+ }
160167
160248
  publishUserItem(chatSessionId, item) {
160168
160249
  const turn = this.turns.get(chatSessionId);
160169
- if (!turn) return false;
160250
+ if (!turn) {
160251
+ this.bufferUserItem(chatSessionId, item);
160252
+ return false;
160253
+ }
160170
160254
  const payload = { role: "user", text: item.text };
160171
160255
  if (item.clientSubmitId) payload.clientSubmitId = item.clientSubmitId;
160172
160256
  if (item.attachments?.length) payload.attachments = item.attachments;
@@ -160442,6 +160526,7 @@ var init_live_chat = __esm({
160442
160526
  if (!turn || turn.status !== "running") {
160443
160527
  return say({ accepted: false, reason: "no-live-turn" }, { turnStatus: turn?.status ?? "<no-turn>" });
160444
160528
  }
160529
+ if (turn.recovering) return say({ accepted: false, reason: "recovering" }, { recovering: true });
160445
160530
  const fn = turn.handle.appendInput;
160446
160531
  if (typeof fn !== "function") return say({ accepted: false, reason: "unsupported" });
160447
160532
  const pendingItemId = !opts?.system && opts?.clientMessageKey ? `oasis-user:${opts.clientMessageKey}` : void 0;
@@ -160587,6 +160672,40 @@ var init_live_chat = __esm({
160587
160672
  }
160588
160673
  });
160589
160674
 
160675
+ // ../server/src/chat-turn-wait.ts
160676
+ function waitForChatTurn(res, liveChat, sessionId, startKeepalive, ready = false) {
160677
+ res.writeHead(200, {
160678
+ "content-type": "application/x-oasis-live-v3+ndjson; charset=utf-8",
160679
+ "cache-control": "no-cache, no-transform",
160680
+ "x-accel-buffering": "no",
160681
+ "access-control-expose-headers": "X-Chat-Session-Watch, X-Chat-Waiting, X-Can-Append",
160682
+ "x-chat-session-watch": "1",
160683
+ "x-chat-waiting": "1",
160684
+ "x-can-append": "0"
160685
+ });
160686
+ res.flushHeaders();
160687
+ const stopKeepalive = startKeepalive(res);
160688
+ let detach = () => {
160689
+ };
160690
+ const cleanup = () => {
160691
+ stopKeepalive();
160692
+ detach();
160693
+ };
160694
+ res.once("close", cleanup);
160695
+ const wake = () => {
160696
+ cleanup();
160697
+ if (!res.destroyed) res.end('{"type":"turn_available"}\n');
160698
+ };
160699
+ if (ready) wake();
160700
+ else detach = liveChat.onNextTurn(sessionId, wake);
160701
+ if (res.destroyed) cleanup();
160702
+ }
160703
+ var init_chat_turn_wait = __esm({
160704
+ "../server/src/chat-turn-wait.ts"() {
160705
+ "use strict";
160706
+ }
160707
+ });
160708
+
160590
160709
  // ../server/src/chat-attachment-files.ts
160591
160710
  function safeAttachmentName(raw) {
160592
160711
  return (raw || "attachment").replace(/[^\w.\-]+/g, "_").replace(/^\.+/, "").slice(0, 80) || "attachment";
@@ -160683,7 +160802,7 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
160683
160802
  ...displayText !== merged ? { displayText } : {},
160684
160803
  ...attachmentRefs.length ? { attachments: attachmentRefs } : {}
160685
160804
  });
160686
- const DEFINITELY_NOT_DELIVERED = /* @__PURE__ */ new Set(["unsupported", "no-live-turn", "session-closing", "turn-finished"]);
160805
+ const DEFINITELY_NOT_DELIVERED = /* @__PURE__ */ new Set(["unsupported", "no-live-turn", "session-closing", "turn-finished", "recovering"]);
160687
160806
  const nextState = res.accepted ? "delivered" : DEFINITELY_NOT_DELIVERED.has(res.reason ?? "") ? "queued" : "unconfirmed";
160688
160807
  await deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason).catch(() => void 0);
160689
160808
  return { ...res, queued: true, delivered: merged, batch, ...prepared.paths.length ? { attachmentPaths: prepared.paths } : {} };
@@ -160784,13 +160903,24 @@ var init_closure_report = __esm({
160784
160903
  });
160785
160904
 
160786
160905
  // ../server/src/governance/workorder-drafts.ts
160787
- var import_node_fs5, DRAFT_EDIT_LOCK_TTL_MS, WorkorderDraftStore, ident, seqNoOf, PostgresWorkorderDraftStore;
160906
+ var import_node_fs5, DRAFT_EDIT_LOCK_TTL_MS, WorkorderDraftTagMismatchError, WorkorderDraftStore, ident, seqNoOf, PostgresWorkorderDraftStore;
160788
160907
  var init_workorder_drafts = __esm({
160789
160908
  "../server/src/governance/workorder-drafts.ts"() {
160790
160909
  "use strict";
160791
160910
  import_node_fs5 = __toESM(require("node:fs"), 1);
160792
160911
  init_closure_report();
160793
160912
  DRAFT_EDIT_LOCK_TTL_MS = 12e4;
160913
+ WorkorderDraftTagMismatchError = class extends Error {
160914
+ constructor(draftId, expectedWorkspace, actualWorkspace) {
160915
+ super(
160916
+ 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`
160917
+ );
160918
+ this.draftId = draftId;
160919
+ this.expectedWorkspace = expectedWorkspace;
160920
+ this.actualWorkspace = actualWorkspace;
160921
+ this.name = "WorkorderDraftTagMismatchError";
160922
+ }
160923
+ };
160794
160924
  WorkorderDraftStore = class {
160795
160925
  constructor(file) {
160796
160926
  this.file = file;
@@ -160894,14 +161024,28 @@ var init_workorder_drafts = __esm({
160894
161024
  }
160895
161025
  /** 给草案打上 chat 会话标(建单经 chat 路时,/api/cmd 拿到 X-Chat-Session-Id 后调)。
160896
161026
  * 同时(若拿得到)盖上产出它的那一轮 runId,供 friday 重进对话页把该版草案归位到那条助手消息。
160897
- * sourceRunId 一旦盖上不再改(同一版草案就属于那一轮)。 */
160898
- async tagSession(draftId, chatSessionId, sourceRunId) {
161027
+ * sourceRunId 一旦盖上不再改(同一版草案就属于那一轮)。
161028
+ *
161029
+ * **`expectedWorkspace` 是防跨库改绑的那道闸(2026-09-10 线上事故)**:草案 id 形如
161030
+ * `wodraft:<n>`,而**每个 schema 各有一台发号器**(共享库一台、每家公司各一台,见
161031
+ * `PostgresWorkorderDraftStore.open` 建的 `workorder_draft_seq`)。于是「共享库的 70 号」和
161032
+ * 「didi 库的 70 号」是两份毫不相干的草案。一旦建草案与打标落在**不同的 store**上
161033
+ * (公司上下文解析不出来 → 建落共享库;打标按公司解析 → 打进公司库),这里就会拿着同一个
161034
+ * 号在另一个库里翻出**别人的草案**,把它的 chatSessionId / sourceRunId 覆盖成本次会话——
161035
+ * 别人的建单卡就这样出现在你的对话里,而原归属被覆盖后再也找不回来。
161036
+ *
161037
+ * 所以调用方**必须**把「我刚建的那张单的 workspace」一起传进来:对不上就抛,绝不写。
161038
+ * 查不到 id 同理——旧实现在这里是 `if (d) {}` 的静默 no-op,那正是这次事故连一行日志都没留下的原因。
161039
+ * 抛出的是 {@link WorkorderDraftTagMismatchError},调用方该**记账后继续**(草案本身已经建好,
161040
+ * 不该因为打标失败把整个建单回执变成失败),见 `/api/cmd` 与 playbooks 两处调用点。 */
161041
+ async tagSession(draftId, chatSessionId, sourceRunId, expectedWorkspace) {
160899
161042
  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 });
161043
+ if (!d || expectedWorkspace !== void 0 && d.workspace !== expectedWorkspace) {
161044
+ throw new WorkorderDraftTagMismatchError(draftId, expectedWorkspace, d?.workspace);
160904
161045
  }
161046
+ d.chatSessionId = chatSessionId;
161047
+ if (sourceRunId && !d.sourceRunId) d.sourceRunId = sourceRunId;
161048
+ await this.persist({ kind: "upsert", draft: d });
160905
161049
  }
160906
161050
  /** 草案被就地编辑后(editDraftPlan 改了 plan.ops)调用:bump `at` 让 friday 重载据此察觉,
160907
161051
  * 并**落盘**——否则编辑过的草案在 apply 前进程重启就丢改动(草案落盘的初衷正是扛重启)。 */
@@ -161694,9 +161838,14 @@ var init_assistants = __esm({
161694
161838
  await this.opts.actors.disableActor(agentId, by).catch(() => {
161695
161839
  });
161696
161840
  }
161841
+ /** 本服务的公司 scope;未启用公司维度(单租户装配/单测)时 undefined = 不过滤,行为不变。 */
161842
+ scope() {
161843
+ const companyId = this.opts.companyId();
161844
+ return companyId ? { companyId } : void 0;
161845
+ }
161697
161846
  /* ---------- runtime 挑选(ADR §3.3.a):负载均衡 + 幂等 tie-break ---------- */
161698
161847
  async pickAssistantRuntime() {
161699
- const runtimes = (await this.opts.listRuntimes()).filter((r) => r.status === "online");
161848
+ const runtimes = (await this.opts.listRuntimes(void 0, this.scope())).filter((r) => r.status === "online");
161700
161849
  if (runtimes.length === 0) return null;
161701
161850
  let defaultKind = DEFAULT_ASSISTANT_RUNTIME_KIND;
161702
161851
  for (const r of runtimes) {
@@ -161723,7 +161872,7 @@ var init_assistants = __esm({
161723
161872
  if (!actor.roles.includes("assistant")) return false;
161724
161873
  const binding = await this.opts.registry.getBinding(agentId);
161725
161874
  if (!binding || binding.status !== "active") return false;
161726
- const runtime = (await this.opts.listRuntimes(binding.nodeId)).find((r) => r.kind === binding.runtimeKind);
161875
+ const runtime = (await this.opts.listRuntimes(binding.nodeId, this.scope())).find((r) => r.kind === binding.runtimeKind);
161727
161876
  if (!runtime || runtime.status !== "online") return false;
161728
161877
  return true;
161729
161878
  }
@@ -163729,7 +163878,8 @@ async function linkChatAttachmentsToWorkOrder(input) {
163729
163878
  ...candidate.contentType ?? meta?.contentType ? { contentType: candidate.contentType ?? meta?.contentType } : {},
163730
163879
  sourceSessionId: input.chatSessionId,
163731
163880
  sourceMessageId: candidate.messageId,
163732
- linkedAt: input.linkedAt
163881
+ linkedAt: input.linkedAt,
163882
+ ...input.uploadedBy ? { uploadedBy: input.uploadedBy } : {}
163733
163883
  };
163734
163884
  try {
163735
163885
  await input.artifacts.addWorkOrderFile(file);
@@ -186756,12 +186906,13 @@ var init_governance_service = __esm({
186756
186906
  const binding = await this.options.projects.getWorkspaceBinding(ctx.organizationId, workorderId);
186757
186907
  if (!binding) {
186758
186908
  if (claimedProjectId) throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_BINDING_MISSING", "\u5DE5\u5355\u6CA1\u6709\u53EF\u4FE1\u9879\u76EE\u7ED1\u5B9A");
186909
+ await this.requireOrganizationActor(ctx);
186759
186910
  return void 0;
186760
186911
  }
186761
186912
  if (claimedProjectId && binding.projectId !== claimedProjectId) {
186762
186913
  throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_MISMATCH", "\u8BF7\u6C42\u9879\u76EE\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u4E0D\u4E00\u81F4");
186763
186914
  }
186764
- if (this.options.projects.isEphemeralProject(ctx.organizationId, binding.projectId)) {
186915
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, binding.projectId)) {
186765
186916
  await this.requireOrganizationActor(ctx);
186766
186917
  return void 0;
186767
186918
  }
@@ -186969,8 +187120,8 @@ var init_governance_service = __esm({
186969
187120
  const projectId2 = input.scopeType === "project" ? input.scopeId : void 0;
186970
187121
  if (input.scopeType === "project") {
186971
187122
  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");
187123
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) {
187124
+ throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u8BE5\u9879\u76EE\u4E0D\u652F\u6301\u72EC\u7ACB\u77E5\u8BC6 Space");
186974
187125
  }
186975
187126
  await this.authorizeProjectResource(ctx, projectId2, "configure");
186976
187127
  } else if (input.scopeId) {
@@ -187096,7 +187247,7 @@ var init_governance_service = __esm({
187096
187247
  async reconfigureKnowledgeTree(ctx, store, organizationSpace, connection, _config) {
187097
187248
  const projectEntries = [];
187098
187249
  for (const project of await this.options.projects.listProjects(ctx.organizationId)) {
187099
- if (this.options.projects.isEphemeralProject(ctx.organizationId, project.id)) continue;
187250
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, project.id)) continue;
187100
187251
  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
187252
  if (projectSpace) projectEntries.push({ project, space: projectSpace });
187102
187253
  }
@@ -187315,8 +187466,8 @@ var init_governance_service = __esm({
187315
187466
  async resolveRunTarget(ctx, projectId2) {
187316
187467
  await this.requireOrganizationActor(ctx);
187317
187468
  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");
187469
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) {
187470
+ 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
187471
  }
187321
187472
  await this.authorizeProjectResource(ctx, projectId2, "publish");
187322
187473
  }
@@ -187532,7 +187683,7 @@ var init_governance_service = __esm({
187532
187683
  if (actor.kind !== "system") await this.requireOrganizationAdmin(ctx);
187533
187684
  const project = await this.options.projects.getProject(ctx.organizationId, projectId2);
187534
187685
  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;
187686
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) return null;
187536
187687
  const store = await this.options.storeFor(ctx.organizationId);
187537
187688
  const scope = this.projectScope(ctx, projectId2);
187538
187689
  const existingSpaces = await store.list(scope, "space");
@@ -190232,7 +190383,7 @@ var init_governance_service = __esm({
190232
190383
  if (event.claimedProjectId !== trusted.projectId) {
190233
190384
  throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_MISMATCH", "\u8BF7\u6C42\u9879\u76EE\u4E0E\u53EF\u4FE1\u6765\u6E90\u5F52\u5C5E\u4E0D\u4E00\u81F4");
190234
190385
  }
190235
- const targetProjectId = trusted.projectId && !this.options.projects.isEphemeralProject(ctx.organizationId, trusted.projectId) ? trusted.projectId : void 0;
190386
+ const targetProjectId = trusted.projectId && !this.options.projects.isProjectScopeUnsupported(ctx.organizationId, trusted.projectId) ? trusted.projectId : void 0;
190236
190387
  const discoveryActor = await this.requireActiveActor(ctx.actorId, ctx.organizationId);
190237
190388
  if (discoveryActor.kind !== "system") await this.requireOrganizationActor(ctx);
190238
190389
  if (targetProjectId) {
@@ -190498,7 +190649,7 @@ var init_governance_service = __esm({
190498
190649
  runs.push(...await this.enqueueDiscoveryEvent(ctx, event, runtime ?? void 0));
190499
190650
  } catch (error2) {
190500
190651
  const store = await this.options.storeFor(ctx.organizationId);
190501
- const taskProjectId = projectId2 && !this.options.projects.isEphemeralProject(ctx.organizationId, projectId2) ? projectId2 : void 0;
190652
+ const taskProjectId = projectId2 && !this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2) ? projectId2 : void 0;
190502
190653
  const scope = this.scopeFor(ctx, taskProjectId);
190503
190654
  const task = this.governanceTask(ctx, "discovery_failed", event.sourceType, `${event.sourceId}:${event.sourceVersionId}`, {
190504
190655
  eventType: event.eventType,
@@ -196615,6 +196766,7 @@ var init_continue_outcome = __esm({
196615
196766
  "use strict";
196616
196767
  REASON_TEXT = {
196617
196768
  "no-live-turn": "\u4E13\u5BB6\u6B64\u523B\u6CA1\u6709\u6B63\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E\uFF08\u591A\u534A\u662F\u521A\u6536\u5C3E\u3001\u4E0B\u4E00\u8F6E\u8FD8\u6CA1\u8D77\u6765\uFF09",
196769
+ recovering: "\u670D\u52A1\u521A\u91CD\u542F\uFF0C\u4E13\u5BB6\u90A3\u4E00\u8F6E\u8FD8\u5728\u6062\u590D\u4E2D\uFF08\u8282\u70B9\u91CD\u8FDE\u540E\u5C31\u80FD\u63D2\uFF1B\u8FD9\u53E5\u8BDD\u5DF2\u6392\u961F\uFF09",
196618
196770
  unsupported: "\u8FD9\u4E2A\u4E13\u5BB6\u7528\u7684\u6A21\u578B\u4E0D\u652F\u6301\u4E2D\u9014\u63D2\u8BDD\uFF08\u8981\u7B49\u5B83\u8FD9\u4E00\u8F6E\u8DD1\u5B8C\uFF09",
196619
196771
  "session-closing": "\u4E13\u5BB6\u90A3\u4E00\u8F6E\u6B63\u5728\u6536\u5C3E\uFF0C\u8FD9\u53E5\u8BDD\u6765\u665A\u4E86\u4E00\u6B65",
196620
196772
  "turn-finished": "\u6295\u9012\u8FC7\u7A0B\u4E2D\u4E13\u5BB6\u90A3\u4E00\u8F6E\u5DF2\u7ECF\u6536\u5C3E\u4E86",
@@ -198699,9 +198851,13 @@ async function mintInstallationToken(args, deps = {}) {
198699
198851
  const token = typeof body2["token"] === "string" ? body2["token"] : "";
198700
198852
  if (!token) throw new Error("github app: token missing in response");
198701
198853
  const parsed = typeof body2["expires_at"] === "string" ? Date.parse(body2["expires_at"]) : NaN;
198854
+ const rawPerms = body2["permissions"];
198855
+ const permissions = {};
198856
+ for (const [k2, v2] of Object.entries(rawPerms ?? {})) if (typeof v2 === "string") permissions[k2] = v2;
198702
198857
  return {
198703
198858
  token,
198704
- expiresAtMs: Number.isFinite(parsed) ? parsed : nowMs + 60 * 60 * 1e3
198859
+ expiresAtMs: Number.isFinite(parsed) ? parsed : nowMs + 60 * 60 * 1e3,
198860
+ permissions
198705
198861
  };
198706
198862
  }
198707
198863
  async function listAppInstallations(args, deps = {}) {
@@ -198725,7 +198881,10 @@ async function listAppInstallations(args, deps = {}) {
198725
198881
  const id = item["id"] === void 0 ? "" : String(item["id"]);
198726
198882
  if (!id) return [];
198727
198883
  const account = item["account"];
198728
- return [{ installationId: id, account: String(account?.["login"] ?? "") }];
198884
+ const rawPerms = item["permissions"];
198885
+ const permissions = {};
198886
+ for (const [k2, v2] of Object.entries(rawPerms ?? {})) if (typeof v2 === "string") permissions[k2] = v2;
198887
+ return [{ installationId: id, account: String(account?.["login"] ?? ""), permissions }];
198729
198888
  });
198730
198889
  }
198731
198890
  async function fetchAppSelfMetadata(args, deps = {}) {
@@ -202522,20 +202681,6 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
202522
202681
  const project = await ctx.projectState.getProject(projectId2);
202523
202682
  if (!project) throw new Error(`project not found: ${projectId2}`);
202524
202683
  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
202684
  }
202540
202685
  if (schemaForSeed) {
202541
202686
  const title = optStr(args, "title") ?? optStr(args, "description") ?? workspace;
@@ -204027,6 +204172,18 @@ async function startOasisServer(opts) {
204027
204172
  chatStore: store,
204028
204173
  chatSessionId: sessionId,
204029
204174
  session: bgSession,
204175
+ liveChat,
204176
+ liveHandle: {
204177
+ runtimeSessionId: session.id,
204178
+ ...session.runId ? { runId: session.runId } : {},
204179
+ kill: () => {
204180
+ void session.kill?.();
204181
+ },
204182
+ ...session.appendInput ? { appendInput: (input) => session.appendInput(input) } : {},
204183
+ get canAppendInput() {
204184
+ return typeof session.appendInput === "function" && session.canAppendInput !== false;
204185
+ }
204186
+ },
204030
204187
  ...itemStore ? { itemStore } : {},
204031
204188
  // 广播/委派回流带上 held turn id(B3);授权卡入口在别处,那条路本轮不接账本。
204032
204189
  ...turnId ? { turnId } : {}
@@ -204585,7 +204742,15 @@ async function startOasisServer(opts) {
204585
204742
  if (body2.command === "spawn" && wodrafts) {
204586
204743
  const draftId = result?.data?.draftId;
204587
204744
  const chatSid = req.headers["x-chat-session-id"];
204588
- if (draftId && typeof chatSid === "string") await wodrafts.tagSession(draftId, chatSid, liveChat.runFor(chatSid));
204745
+ if (draftId && typeof chatSid === "string") {
204746
+ const draftWorkspace = result?.data?.workspace;
204747
+ await wodrafts.tagSession(draftId, chatSid, liveChat.runFor(chatSid), draftWorkspace).catch((err) => {
204748
+ console.error(
204749
+ `[workorder-draft] \u8349\u6848 ${draftId}\uFF08ws=${String(draftWorkspace)}\uFF09\u672A\u80FD\u5173\u8054\u4F1A\u8BDD ${chatSid}\uFF1A`,
204750
+ err
204751
+ );
204752
+ });
204753
+ }
204589
204754
  }
204590
204755
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
204591
204756
  } catch (err) {
@@ -204914,7 +205079,9 @@ async function startOasisServer(opts) {
204914
205079
  blobMeta: blobMetaAcrossCabinets,
204915
205080
  // 工单就是这一刻建出来的,所以「现在」就是任务创建时间。取一次、全部文件共用,
204916
205081
  // 保证同一次建单带进来的文件在任务侧显示的是同一个时刻(排期序 28 原文口径)。
204917
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
205082
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
205083
+ // 建单发起人就是把这些附件带进任务的人——项目页「创建人」列与预览副标据此署名。
205084
+ uploadedBy: actor
204918
205085
  }).catch((err) => ({
204919
205086
  linked: 0,
204920
205087
  warnings: [`\u672A\u80FD\u628A\u672C\u6B21\u5BF9\u8BDD\u7684\u9644\u4EF6\u5E26\u5165\u4EFB\u52A1\u6587\u4EF6\uFF1A${err instanceof Error ? err.message : String(err)}`]
@@ -205592,6 +205759,13 @@ async function startOasisServer(opts) {
205592
205759
  };
205593
205760
  const wantsV3 = url.searchParams.get("protocol") === "v3";
205594
205761
  if (wantsV3) {
205762
+ const watchSession = url.searchParams.get("watch") === "1";
205763
+ const fromEpoch = url.searchParams.has("epoch") ? Number(url.searchParams.get("epoch")) : void 0;
205764
+ const cursor = liveChat.cursorFor(sid);
205765
+ if (watchSession && fromEpoch !== void 0 && cursor && cursor.epoch !== fromEpoch) {
205766
+ waitForChatTurn(res, liveChat, sid, startStreamKeepalive, true);
205767
+ return;
205768
+ }
205595
205769
  const sub2 = (frame) => {
205596
205770
  if (res.destroyed) return;
205597
205771
  res.write(`${JSON.stringify(frame)}
@@ -205603,6 +205777,11 @@ async function startOasisServer(opts) {
205603
205777
  await reopenRecoveringTurn();
205604
205778
  att2 = liveChat.attachLiveV3(sid, fromSeq, sub2);
205605
205779
  }
205780
+ if (watchSession && (!att2 || att2.status !== "running" && att2.replay.length === 0)) {
205781
+ att2?.detach();
205782
+ waitForChatTurn(res, liveChat, sid, startStreamKeepalive);
205783
+ return;
205784
+ }
205606
205785
  if (!att2) {
205607
205786
  res.writeHead(204).end();
205608
205787
  return;
@@ -205610,7 +205789,8 @@ async function startOasisServer(opts) {
205610
205789
  res.writeHead(200, {
205611
205790
  "content-type": "application/x-oasis-live-v3+ndjson; charset=utf-8",
205612
205791
  "transfer-encoding": "chunked",
205613
- "access-control-expose-headers": "X-Session-Id, X-Replay-Gap, X-Live-Epoch, X-Can-Append",
205792
+ "access-control-expose-headers": "X-Session-Id, X-Replay-Gap, X-Live-Epoch, X-Can-Append, X-Chat-Session-Watch",
205793
+ ...watchSession ? { "x-chat-session-watch": "1" } : {},
205614
205794
  "x-session-id": cs.runtimeSessionId ?? "",
205615
205795
  "x-live-epoch": String(att2.epoch),
205616
205796
  "x-can-append": liveChat.canAppend(sid) ? "1" : "0",
@@ -206062,13 +206242,16 @@ ${composed}`;
206062
206242
  只推 user:assistant 的每一条本来就走 `emitV3`。 */
206063
206243
  onPersisted: (info) => {
206064
206244
  if (info.role !== "user") return;
206245
+ const rowPayload = info.payload && typeof info.payload === "object" && !Array.isArray(info.payload) ? info.payload : void 0;
206246
+ const rowCsid = typeof rowPayload?.["clientSubmitId"] === "string" ? rowPayload["clientSubmitId"] : void 0;
206247
+ const rowAttachments = Array.isArray(rowPayload?.["attachments"]) ? rowPayload["attachments"] : void 0;
206065
206248
  opts.liveChat?.publishUserItem(persistTarget?.id ?? session.id, {
206066
206249
  itemId: info.itemId,
206067
206250
  ord: info.ord,
206068
206251
  version: info.version,
206069
206252
  text: info.text,
206070
- ...clientSubmitId ? { clientSubmitId } : {},
206071
- ...effectiveAttachments.length ? { attachments: effectiveAttachments } : {}
206253
+ ...rowCsid ? { clientSubmitId: rowCsid } : {},
206254
+ ...rowAttachments?.length ? { attachments: rowAttachments } : {}
206072
206255
  });
206073
206256
  },
206074
206257
  sessionId: persistTarget?.id ?? session.id,
@@ -206990,19 +207173,60 @@ ${composed}`;
206990
207173
  return;
206991
207174
  }
206992
207175
  const meta = await fetchAppSelfMetadata({ appId, privateKeyPem });
206993
- const installationId = await svc.revealResolvedVariable("GITHUB_APP_INSTALLATION_ID", actorId);
206994
- const effective = installationId ? await fetchInstallationPermissions({ appId, privateKeyPem, installationId }) : meta.permissions;
207176
+ const storedInstallationId = await svc.revealResolvedVariable("GITHUB_APP_INSTALLATION_ID", actorId);
207177
+ const installs = await listAppInstallations({ appId, privateKeyPem }).catch(() => []);
207178
+ const chosen = installs.find((i) => i.installationId === storedInstallationId) ?? (installs.length === 1 ? installs[0] : void 0);
207179
+ const installationStale = Boolean(storedInstallationId) && installs.length > 0 && !chosen;
207180
+ const installationId = chosen?.installationId || storedInstallationId;
207181
+ let grantedFrom = "app";
207182
+ let effective = meta.permissions;
207183
+ if (installationId) {
207184
+ const minted = await mintInstallationToken({ appId, privateKeyPem, installationId }).catch(() => null);
207185
+ if (minted && Object.keys(minted.permissions).length > 0) {
207186
+ effective = minted.permissions;
207187
+ grantedFrom = "token";
207188
+ } else if (chosen && Object.keys(chosen.permissions).length > 0) {
207189
+ effective = chosen.permissions;
207190
+ grantedFrom = "installation";
207191
+ } else {
207192
+ effective = await fetchInstallationPermissions({ appId, privateKeyPem, installationId });
207193
+ grantedFrom = "installation";
207194
+ }
207195
+ }
206995
207196
  const missing = missingAppPermissions(effective);
207197
+ 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));
207198
+ const grantedKeys = new Set(
207199
+ Object.entries(effective).filter(([, level]) => Boolean(level)).map(([k2]) => k2)
207200
+ );
206996
207201
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
206997
207202
  configured: true,
206998
207203
  slug: meta.slug,
206999
207204
  ownerLogin: meta.ownerLogin,
207205
+ /** App 申请的全集(英文原名 + 级别)。界面用它回答「我配了多少项」。 */
207206
+ requested: requestedAll,
207000
207207
  /** true=读的是这次安装真正被授予的权限;false=只拿到 App 申请的,可能偏乐观。 */
207001
207208
  fromInstallation: Boolean(installationId),
207002
- /** App 申请了、但这次安装还没被授予——多半是权限升级还等着组织 owner 批准。 */
207003
- awaitingApproval: missingAppPermissions(effective).filter((m2) => meta.permissions[m2.key]).map((m2) => m2.key),
207209
+ /** 「已授予」这一列是从哪问出来的:token(最准)/ 安装记录 / 只有 App 申请。 */
207210
+ grantedFrom,
207211
+ /** 读的是哪一处安装——同一个 App 装多处时,人得看得见自己批的是不是这一处。 */
207212
+ installationAccount: chosen?.account ?? "",
207213
+ /** 库里存的 installation 已不在这个 App 的安装列表里(卸载重装过)。 */
207214
+ installationStale,
207215
+ /**
207216
+ * 这个 App 的**其它**安装及各自的权限项数。只在装了不止一处时才有内容。
207217
+ * 它专门回答那句「我明明已经批过了」:批在哪一处,这里一眼看得见。
207218
+ */
207219
+ otherInstallations: installs.filter((i) => i.installationId !== installationId).map((i) => ({ account: i.account, count: Object.keys(i.permissions).length })),
207004
207220
  /**
207005
- * 这次安装**真正被授予**的全部权限,一律用 GitHub 的英文原名 + 级别
207221
+ * App 申请了、但**这一处安装铸出来的 token 里没有**。
207222
+ * **在全集上做差**,不再只看本仓必需的那八项(否则人另外勾的永远报不出来)。
207223
+ *
207224
+ * ⚠ 差集只说明「这一处安装现在给不了」,不等于「人没批」——同一个 App 装多处时
207225
+ * 批准是逐处的(见 `otherInstallations`)。界面别把这层推断说死。
207226
+ */
207227
+ awaitingApproval: requestedAll.filter((r) => !grantedKeys.has(r.key)).map((r) => r.key),
207228
+ /**
207229
+ * agent **真正拿得到**的全部权限(优先读 token 自己的权限位),一律用 GitHub 的英文原名 + 级别
207006
207230
  * (2026-09-10 发起人:「所有权限都用英文原文,不区分是不是八项权限」)。
207007
207231
  *
207008
207232
  * 此前这里是 `GITHUB_APP_PERMISSION_LABELS.filter(...)` —— 那张表只有本仓必需的
@@ -207152,7 +207376,8 @@ ${composed}`;
207152
207376
  return;
207153
207377
  }
207154
207378
  try {
207155
- const appSecret = opts.actors ? await opts.actors.service.revealVariable("FEISHU_APP_SECRET", actorId) : null;
207379
+ const startSvc = await connectorActorsService();
207380
+ const appSecret = startSvc ? await startSvc.revealResolvedVariable("FEISHU_APP_SECRET", actorId) : null;
207156
207381
  if (!appSecret) throw new Error("FEISHU_APP_SECRET not found \u2014 please complete app setup first");
207157
207382
  const basicAuth = Buffer.from(`${appId}:${appSecret}`).toString("base64");
207158
207383
  const resp = await fetch("https://accounts.feishu.cn/oauth/v1/device_authorization", {
@@ -207182,7 +207407,8 @@ ${composed}`;
207182
207407
  return;
207183
207408
  }
207184
207409
  try {
207185
- const appSecret = opts.actors ? await opts.actors.service.revealVariable("FEISHU_APP_SECRET", actorId) : null;
207410
+ const pollSvc = await connectorActorsService();
207411
+ const appSecret = pollSvc ? await pollSvc.revealResolvedVariable("FEISHU_APP_SECRET", actorId) : null;
207186
207412
  if (!appSecret) throw new Error("FEISHU_APP_SECRET not found \u2014 please complete app setup first");
207187
207413
  const resp = await fetch("https://open.feishu.cn/open-apis/authen/v2/oauth/token", {
207188
207414
  method: "POST",
@@ -207208,6 +207434,7 @@ ${composed}`;
207208
207434
  if (svc) {
207209
207435
  if (actorId) {
207210
207436
  await svc.putVariable({ key: "FEISHU_APP_ID", value: appId, scope: "personal", actorId, connectorId: "feishu", encrypted: true });
207437
+ await svc.putVariable({ key: "FEISHU_APP_SECRET", value: appSecret, scope: "personal", actorId, connectorId: "feishu", encrypted: true });
207211
207438
  } else {
207212
207439
  await svc.putVariable({ key: "FEISHU_APP_ID", value: appId, scope: "global", connectorId: "feishu", encrypted: true });
207213
207440
  await svc.upsertConnector({ id: "feishu", name: "\u98DE\u4E66", mode: "oauth", status: "connected", account: appId });
@@ -207318,7 +207545,9 @@ ${composed}`;
207318
207545
  const svc = await connectorActorsService();
207319
207546
  if (svc) {
207320
207547
  const connId = "github";
207321
- await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
207548
+ if (!actorIdParam) {
207549
+ await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
207550
+ }
207322
207551
  for (const [k2, v2] of [["GIT_AUTHOR_NAME", actorNameParam], ["GIT_AUTHOR_EMAIL", ""], ["GIT_COMMITTER_NAME", actorNameParam], ["GIT_COMMITTER_EMAIL", ""], ["EMAIL", ""]])
207323
207552
  await svc.putVariable({ key: k2, value: v2, scope: "personal", actorId: actorIdParam, connectorId: connId, encrypted: false });
207324
207553
  await svc.putVariable({ key: "GITHUB_TOKEN", value: p2.access_token, scope: "personal", actorId: actorIdParam, connectorId: connId, encrypted: true });
@@ -207453,7 +207682,6 @@ var init_server3 = __esm({
207453
207682
  init_src7();
207454
207683
  init_collect();
207455
207684
  init_remote_util();
207456
- init_ephemeral_project();
207457
207685
  init_timeline();
207458
207686
  init_node_state();
207459
207687
  init_src5();
@@ -207462,6 +207690,7 @@ var init_server3 = __esm({
207462
207690
  init_router();
207463
207691
  init_infra_error();
207464
207692
  init_live_chat();
207693
+ init_chat_turn_wait();
207465
207694
  init_append_now();
207466
207695
  init_workorder_drafts();
207467
207696
  init_draft_edit();
@@ -208068,10 +208297,15 @@ var init_memory_registry_store = __esm({
208068
208297
  const mk = this._varKey(v2.key, v2.actorId, v2.projectId);
208069
208298
  const ex = this.variables.get(mk);
208070
208299
  const keptName = v2.name ?? ex?.name;
208300
+ const keptCreatedBy = ex?.createdBy ?? v2.createdBy;
208301
+ const keptCreatedAt = ex?.createdAt ?? v2.createdAt;
208071
208302
  this.variables.set(mk, {
208072
208303
  ...v2,
208073
208304
  ...ex?.lastUsedAt !== void 0 ? { lastUsedAt: ex.lastUsedAt } : {},
208074
- ...keptName ? { name: keptName } : {}
208305
+ ...keptName ? { name: keptName } : {},
208306
+ ...keptCreatedBy ? { createdBy: keptCreatedBy } : {},
208307
+ ...keptCreatedAt ? { createdAt: keptCreatedAt } : {},
208308
+ ...v2.updatedBy ? { updatedBy: v2.updatedBy } : {}
208075
208309
  });
208076
208310
  }
208077
208311
  async touchVariableUsage(key, actorId, projectId2, when) {
@@ -210527,6 +210761,32 @@ var init_projection = __esm({
210527
210761
  }
210528
210762
  });
210529
210763
 
210764
+ // ../server/src/domains/projects/uncategorized-project.ts
210765
+ function isUncategorizedProjectId(projectId2) {
210766
+ return projectId2 === UNCATEGORIZED_PROJECT_ID;
210767
+ }
210768
+ async function bindWorkorderProject(deps) {
210769
+ const { workOrderId, projectId: projectId2 } = deps;
210770
+ if (!projectId2) return null;
210771
+ try {
210772
+ await deps.upsertBinding({ workOrderId, projectId: projectId2 });
210773
+ return projectId2;
210774
+ } catch (e) {
210775
+ deps.onWarn?.(`[project] \u5DE5\u5355 ${workOrderId} \u7ED1\u5B9A\u9879\u76EE\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u5EFA\u5355\uFF09\uFF1A${String(e)}`);
210776
+ return null;
210777
+ }
210778
+ }
210779
+ var UNCATEGORIZED_PROJECT_ID, UNCATEGORIZED_PROJECT_NAME, UNCATEGORIZED_PROJECT_DESCRIPTION, UNCATEGORIZED_UNKNOWN_TIME;
210780
+ var init_uncategorized_project = __esm({
210781
+ "../server/src/domains/projects/uncategorized-project.ts"() {
210782
+ "use strict";
210783
+ UNCATEGORIZED_PROJECT_ID = "proj:uncategorized";
210784
+ UNCATEGORIZED_PROJECT_NAME = "\u65E0\u5206\u7C7B\u9879\u76EE";
210785
+ 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";
210786
+ UNCATEGORIZED_UNKNOWN_TIME = "1970-01-01T00:00:00.000Z";
210787
+ }
210788
+ });
210789
+
210530
210790
  // ../server/src/domains/projects/service.ts
210531
210791
  function sanitizeCreatorActorId(raw) {
210532
210792
  const id = raw?.trim();
@@ -210562,8 +210822,14 @@ function foldSingleNodeTasks(items) {
210562
210822
  paths.add(item.path);
210563
210823
  nodeFoldersByTask.set(item.taskId, paths);
210564
210824
  }
210825
+ const tasksWithRootFiles = /* @__PURE__ */ new Set();
210826
+ for (const item of items) {
210827
+ if (item.kind === "file" && item.taskId && item.path?.startsWith(`wo:${item.taskId}/file:`)) {
210828
+ tasksWithRootFiles.add(item.taskId);
210829
+ }
210830
+ }
210565
210831
  const folded = new Set(
210566
- [...nodeFoldersByTask].filter(([, paths]) => paths.size === 1).map(([taskId]) => taskId)
210832
+ [...nodeFoldersByTask].filter(([taskId, paths]) => paths.size === 1 && !tasksWithRootFiles.has(taskId)).map(([taskId]) => taskId)
210567
210833
  );
210568
210834
  if (folded.size === 0) return folded;
210569
210835
  for (const item of items) {
@@ -210799,7 +211065,7 @@ var init_service4 = __esm({
210799
211065
  init_src();
210800
211066
  init_src5();
210801
211067
  init_projection();
210802
- init_ephemeral_project();
211068
+ init_uncategorized_project();
210803
211069
  init_workorders();
210804
211070
  init_node_state();
210805
211071
  DEFAULT_AGENT_ACTOR = "actor:agent:system";
@@ -210935,6 +211201,9 @@ var init_service4 = __esm({
210935
211201
  async listWorkspaceBindings() {
210936
211202
  return [...this.workspaceBindings.values()].map((binding) => ({ ...binding }));
210937
211203
  }
211204
+ async deleteWorkspaceBinding(workOrderId) {
211205
+ this.workspaceBindings.delete(workOrderId);
211206
+ }
210938
211207
  async setWorkspaceDispatchHold(workOrderId, held) {
210939
211208
  if (held) this.dispatchHeld.add(workOrderId);
210940
211209
  else this.dispatchHeld.delete(workOrderId);
@@ -211061,15 +211330,8 @@ var init_service4 = __esm({
211061
211330
  return { project, artifacts, files };
211062
211331
  }
211063
211332
  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));
211333
+ if (isUncategorizedProjectId(projectId2)) return [];
211334
+ return this.projects.listProjectFiles(projectId2);
211073
211335
  }
211074
211336
  /**
211075
211337
  * B2 投影同步:写命令落 oplog 后,从 oplog 重放重建 `this.artifacts` 文档层投影。
@@ -211126,23 +211388,18 @@ var init_service4 = __esm({
211126
211388
  return project ? this.withProjectDerivedState(project) : null;
211127
211389
  }
211128
211390
  /**
211129
- * 「无分类项目」虚拟卡:把所有 `proj_tmp_*` 一族的**读侧聚合**成一张固定项目,供列表/详情/关联任务/相关文件消费。
211391
+ * 「无分类项目」虚拟卡:把所有**没有项目绑定**的工单聚合成一张固定项目,供列表/详情/关联任务/相关文件消费。
211130
211392
  * 存储里没有真行——这就是本卡与真项目最大的区别:
211131
211393
  * - 写侧(成员、内容位置、上传文件)一律拒收(写到真项目 or 走真工单流程);
211132
- * - 派生态(artifactSummary / workOrderCount)跨所有 ephemeral 项目算并集;
211394
+ * - 派生态(artifactSummary / workOrderCount)按「无绑定工单」算并集;
211133
211395
  * - 帧上的「项目负责人:系统」由前端自选文案渲染,本层只给一个稳定的空 members 名单。
211396
+ *
211397
+ * ⚠ `workOrderCount` **走 listProjectWorkorders 同一条路**,不另写一份 bindings 过滤——
211398
+ * 两处各算各的正是「卡片计数与点进去的列表条数对不上」那类缺陷的来路。
211134
211399
  */
211135
211400
  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();
211401
+ const workorders = await this.listProjectWorkorders(UNCATEGORIZED_PROJECT_ID);
211402
+ const artifactList = await this.aggregateUncategorizedArtifacts();
211146
211403
  return {
211147
211404
  id: UNCATEGORIZED_PROJECT_ID,
211148
211405
  name: UNCATEGORIZED_PROJECT_NAME,
@@ -211150,46 +211407,33 @@ var init_service4 = __esm({
211150
211407
  memberCount: 0,
211151
211408
  members: [],
211152
211409
  artifactSummary: artifactList.summary,
211153
- workOrderCount,
211410
+ workOrderCount: workorders.length,
211154
211411
  // ADR 0205:本卡的「项目负责人:系统」是**前端按帧自选的文案**,后端不出 `"system"` 哨兵——
211155
211412
  // 出 null 保持「取不到就 null」的域内红线,也不把一个不存在的 actor id 塞进契约。
211156
211413
  createdBy: null,
211157
- createdAt: createdAts[0] ?? UNCATEGORIZED_UNKNOWN_TIME,
211158
- updatedAt: updatedAts[updatedAts.length - 1] ?? UNCATEGORIZED_UNKNOWN_TIME
211414
+ createdAt: UNCATEGORIZED_UNKNOWN_TIME,
211415
+ updatedAt: UNCATEGORIZED_UNKNOWN_TIME
211159
211416
  };
211160
211417
  }
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
- }
211418
+ async aggregateUncategorizedArtifacts() {
211419
+ const artifacts = await this.listAllProjectArtifacts(UNCATEGORIZED_PROJECT_ID);
211168
211420
  artifacts.sort((a, b2) => b2.updatedAt.localeCompare(a.updatedAt));
211169
211421
  return { artifacts, summary: summarizeArtifacts(artifacts) };
211170
211422
  }
211171
211423
  /**
211172
211424
  * 项目列表(带派生态)。
211173
211425
  *
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`。
211426
+ * 库里的每一行都是**人建的真项目**,全部返回——临时项目(`proj_tmp_*`)退役后,这里不再有
211427
+ * "算完就扔"的过滤:旧版 dev 环境 294 个项目里 272 个是一单一个的临时项目,
211428
+ * `withProjectDerivedState` 对它们逐个跑 `buildWorkorderSummaries`(全量工单)+ 遍历全部 artifact,
211429
+ * 93% 的计算白算,还把单线程事件循环整块堵住 3~11 秒(并发时 /api/automations 从 0.24s 被拖到 7.1s,
211430
+ * 前端看到的是「项目列表加载失败:timeout」——像数据库挂了,其实是被自己堵死的)。
211184
211431
  */
211185
211432
  async listProjects(opts) {
211186
211433
  const stored = await this.projects.listProjects();
211187
- const wanted = opts?.includeEphemeral === true ? stored : stored.filter((project) => !isEphemeralProject(project.id));
211188
211434
  const creatorCache = /* @__PURE__ */ new Map();
211189
- const derived = await Promise.all(wanted.map((project) => this.withProjectDerivedState(project, creatorCache)));
211435
+ const derived = await Promise.all(stored.map((project) => this.withProjectDerivedState(project, creatorCache)));
211190
211436
  if (opts?.includeUncategorized === false) return derived;
211191
- const wantUncategorized = opts?.includeUncategorized ?? !(opts?.includeEphemeral === true);
211192
- if (!wantUncategorized) return derived;
211193
211437
  const virtual = await this.buildUncategorizedProject();
211194
211438
  return [virtual, ...derived];
211195
211439
  }
@@ -211268,10 +211512,10 @@ var init_service4 = __esm({
211268
211512
  * 项目创建人(ADR 0205)——**读时解析**,三条来路按优先级:
211269
211513
  *
211270
211514
  * 1. 库里落了 `created_by` → 按它解析 name/avatar;
211271
- * 2. 没落且是**临时项目**(`proj_tmp_*`)→ 从它绑定的那张工单的 `brief.owner`(决策 0026 的
211272
- * 「发起人真人」)派生。这是老数据免回填的口径:派生每次都新鲜,回填反而会造出
211273
- * 「工单发起人换过、项目创建人却挂着旧值」的伪相;
211274
- * 3. 都取不到 `null`(老的手建项目),前端诚实显示「—」。
211515
+ * 2. 没落 `null`(老的手建项目),前端诚实显示「—」。
211516
+ *
211517
+ * 旧版还有一档「临时项目从它绑定的那张工单 `brief.owner` 派生」——临时项目退役后没有这一档了
211518
+ * (见 uncategorized-project.ts 文件头)。
211275
211519
  *
211276
211520
  * **绝不拿别的字段顶替**(`members[0]`、项目名、updatedAt 都不行)——未指定成员时全组织都是成员,
211277
211521
  * 按 actorId 排序等于随机点一个 agent 当负责人。这是本域红线,见 resolveActorName 的注释。
@@ -211279,9 +211523,7 @@ var init_service4 = __esm({
211279
211523
  async resolveProjectCreator(project, workorders, cache) {
211280
211524
  const stored = project.createdBy?.id;
211281
211525
  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;
211526
+ return null;
211285
211527
  }
211286
211528
  /**
211287
211529
  * actorId → {@link ProjectActorRef}:`kind` 按 id 前缀派生,`name` / `avatar` 走本公司名册现取。
@@ -211344,7 +211586,8 @@ var init_service4 = __esm({
211344
211586
  */
211345
211587
  async reassignWorkorder(workOrderId, projectId2) {
211346
211588
  if (isUncategorizedProjectId(projectId2)) {
211347
- return this.unlinkWorkorder(workOrderId);
211589
+ await this.unlinkWorkorder(workOrderId);
211590
+ return { workOrderId, projectId: null };
211348
211591
  }
211349
211592
  const project = await this.projects.getProject(projectId2);
211350
211593
  if (!project) throw new Error(`project not found: ${projectId2}`);
@@ -211353,77 +211596,46 @@ var init_service4 = __esm({
211353
211596
  return binding;
211354
211597
  }
211355
211598
  /**
211356
- * 「取消任务关联」——把工单从当前项目解绑,回落到它专属的临时项目(proj_tmp_<slug>)。
211599
+ * 「取消任务关联」——**删掉工单的项目绑定行**,工单从此没有项目(读侧落进「无分类项目」桶)。
211357
211600
  *
211358
- * **不是真正解绑成 null**:契约里 `Workspace.projectId` 是必填(见 ephemeral-project.ts 的历史事故链),
211359
- * 无绑定态会让下游 provision / 自动收集 / propose 走向连环破坏。回落到临时项目 =「在读侧进入
211360
- * 无分类桶」+「写侧仍保持"每工单必属项目"的合法态」。
211601
+ * 2026-09-10 之前这里是"回落到工单专属的临时项目 `proj_tmp_<slug>`",理由是「契约里每个工单
211602
+ * 都该属于某项目」。那套兜底已退役(见 uncategorized-project.ts 文件头):兜底容器让每张未挂靠
211603
+ * 工单变成一个只有它自己的假项目,代码仓/项目变量/记忆作用域全挂在一次性 id 上。
211361
211604
  *
211362
- * 幂等:目标已是自己的 ephemeral 项目 → 直接返回既有绑定,不重复 upsert;ephemeral 项目本身
211363
- * 不需要预先存在(`ensureWorkorderProject` 后台建,也可能是本工单第一次被"解绑"——按需建)。
211605
+ * 幂等:没有绑定行时静默返回(store delete 本身幂等)。
211364
211606
  */
211365
211607
  async unlinkWorkorder(workOrderId) {
211366
211608
  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;
211609
+ await this.artifacts.deleteWorkspaceBinding(workOrderId);
211383
211610
  }
211384
211611
  /**
211385
211612
  * 删除项目(帧 `444:1792` 三点菜单第二项,口径由发起人 2026-09-09 拍板)。
211386
211613
  *
211387
211614
  * **只删项目这个壳**——项目主记录 + 成员名单 + 上传文件记录 + 内容位置。任务和会话一条都不删:
211388
211615
  *
211389
- * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 回落到自己的
211390
- * `proj_tmp_<slug>`,也就是读侧的「无分类项目」。发起人明确不要「有任务在跑就不让删」
211391
- * 这道闸,所以这里没有任何在跑判断。
211392
- * 2. 搬工单的同时**把本项目的 git 类内容位置复制一份到每个工单的临时项目上**。
211393
- * 这条不是可选的:`git/resolve.ts` 的 `resolveCodeRepo` 是**派发时**沿
211394
- * `binding project → listContentLocations` 实时解的,不带过去的话,正在跑的任务
211395
- * 下一个节点就取不到代码仓——正是 `ephemeral-project.ts` 顶部那条五环事故链。
211616
+ * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 删掉绑定行 → 落进读侧的
211617
+ * 「无分类项目」。发起人明确不要「有任务在跑就不让删」这道闸,所以这里没有任何在跑判断。
211618
+ * 2. **代码仓不跟着搬**(发起人 2026-09-10 裁定)。`git/resolve.ts` 的 `resolveCodeRepo` 是
211619
+ * 派发时沿 `绑定 项目 → 内容位置` 实时解的,所以删完项目,这些任务下一个节点就取不到代码仓——
211620
+ * 这是**期望行为**:由执行中的数字员工发现并引导人给任务指定新项目,而不是由系统兜一个假容器
211621
+ * (旧版把 git 位置逐单复制到 `proj_tmp_*` 上,正是那套已退役的兜底)。
211396
211622
  * 3. 会话的 `project_id` 置空、项目作用域的变量/密钥清理,走 {@link OnProjectDeleted} 钩子
211397
211623
  * (本 service 对会话域与凭据域零耦合,同 `onProjectNameChanged` 的注入模式)。
211398
211624
  *
211399
- * 拒绝两类 id:`proj:uncategorized`(读侧虚拟卡,存储里没有真行)与 `proj_tmp_*`
211400
- * (工单的兜底归属,删掉等于把工单变成无绑定态——契约里 `Workspace.projectId` 必填)。
211625
+ * 拒绝 `proj:uncategorized`(读侧虚拟卡,存储里没有真行,删它没有意义)。
211401
211626
  */
211402
211627
  async deleteProject(id) {
211403
211628
  const projectId2 = id?.trim();
211404
211629
  if (!projectId2) throw new Error("project id is required");
211405
211630
  if (isUncategorizedProjectId(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
211406
- if (isEphemeralProject(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
211407
211631
  const project = await this.projects.getProject(projectId2);
211408
211632
  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
211633
  const bindings = await this.artifacts.listWorkspaceBindings();
211412
211634
  const movedWorkOrderIds = [];
211413
211635
  for (const binding of bindings) {
211414
211636
  if (binding.projectId !== projectId2) continue;
211415
- const moved = await this.unlinkWorkorder(binding.workOrderId);
211637
+ await this.unlinkWorkorder(binding.workOrderId);
211416
211638
  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
211639
  }
211428
211640
  await this.projects.deleteProject(projectId2);
211429
211641
  if (this.onProjectDeleted) {
@@ -211433,7 +211645,7 @@ var init_service4 = __esm({
211433
211645
  console.warn(`[projects] project-deleted follow-up failed for ${projectId2}: ${error2 instanceof Error ? error2.message : String(error2)}`);
211434
211646
  }
211435
211647
  }
211436
- return { projectId: projectId2, movedWorkOrderIds, carriedGitLocationCount: gitLocations.length };
211648
+ return { projectId: projectId2, movedWorkOrderIds };
211437
211649
  }
211438
211650
  /**
211439
211651
  * 项目关联工单(帧 `1355:4918`):把内部私有的 `listProjectWorkorders` 暴露给路由层。
@@ -211558,6 +211770,33 @@ var init_service4 = __esm({
211558
211770
  });
211559
211771
  }
211560
211772
  }
211773
+ if (this.artifacts.listWorkOrderFiles) {
211774
+ for (const workOrderId of workorderSummaries.map((w2) => w2.id)) {
211775
+ const workOrderFiles = await this.artifacts.listWorkOrderFiles(workOrderId).catch(() => []);
211776
+ const taskTitle = workorderTitleById.get(workOrderId) ?? null;
211777
+ for (const file of workOrderFiles) {
211778
+ const creatorId = file.uploadedBy ?? null;
211779
+ items.push({
211780
+ kind: "file",
211781
+ id: file.fileId,
211782
+ name: file.name,
211783
+ path: `wo:${workOrderId}/file:${file.fileId}`,
211784
+ filePath: null,
211785
+ depth: 1,
211786
+ type: file.contentType ?? inferTypeFromName(file.name),
211787
+ blobRef: file.blobId,
211788
+ contentType: file.contentType ?? null,
211789
+ size: file.size,
211790
+ taskId: workOrderId,
211791
+ taskTitle,
211792
+ creatorKind: creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null,
211793
+ creatorId,
211794
+ creatorName: creatorId ? this.resolveActorName(creatorId) : null,
211795
+ updatedAt: file.linkedAt
211796
+ });
211797
+ }
211798
+ }
211799
+ }
211561
211800
  const foldedTasks = foldSingleNodeTasks(items);
211562
211801
  const taskFolders = /* @__PURE__ */ new Set();
211563
211802
  for (const item of [...items]) {
@@ -211596,6 +211835,8 @@ var init_service4 = __esm({
211596
211835
  filePath: null,
211597
211836
  depth: path41 ? path41.replace(/^\/+/, "").split("/").length - 1 : 0,
211598
211837
  type: file.contentType ?? inferTypeFromName(file.name),
211838
+ blobRef: file.blobId,
211839
+ contentType: file.contentType ?? null,
211599
211840
  size: file.size,
211600
211841
  taskId: null,
211601
211842
  taskTitle: null,
@@ -212063,7 +212304,7 @@ var init_service4 = __esm({
212063
212304
  if (!snap) return [];
212064
212305
  const worksById = new Map(snap.works.map((w2) => [w2.id, w2]));
212065
212306
  const binding = this.artifacts.getWorkspaceBinding ? await this.artifacts.getWorkspaceBinding(workOrderId).catch(() => null) : null;
212066
- const projectId2 = binding?.projectId ?? ephemeralProjectIdFor(workOrderId);
212307
+ const projectId2 = binding?.projectId ?? UNCATEGORIZED_PROJECT_ID;
212067
212308
  const out = [];
212068
212309
  for (const node2 of snap.nodes) {
212069
212310
  if (!node2.latestAcceptId) continue;
@@ -212193,20 +212434,8 @@ var init_service4 = __esm({
212193
212434
  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
212435
  return { artifacts: filtered, summary: summarizeArtifacts(filtered) };
212195
212436
  }
212196
- /** 内部:虚拟卡 跨全部 ephemeral 项目聚合;真项目 → 走原路径。 */
212437
+ /** 内部:虚拟卡与真项目走同一条路——{@link listAllProjectArtifacts} 内部按「无绑定 / 绑到本项目」分流。 */
212197
212438
  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
212439
  return this.listAllProjectArtifacts(projectId2);
212211
212440
  }
212212
212441
  async listProjectWorkorders(projectId2) {
@@ -212215,17 +212444,19 @@ var init_service4 = __esm({
212215
212444
  const uncategorized = isUncategorizedProjectId(projectId2);
212216
212445
  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
212446
  const bound = workorder.projectId;
212218
- if (uncategorized) return typeof bound === "string" && isEphemeralProject(bound);
212447
+ if (uncategorized) return bound == null;
212219
212448
  return bound === projectId2;
212220
212449
  });
212221
212450
  }
212222
212451
  async listAllProjectArtifacts(projectId2) {
212223
- const stored = await this.artifacts.listArtifacts({ projectId: projectId2 });
212452
+ const uncategorized = isUncategorizedProjectId(projectId2);
212453
+ const stored = uncategorized ? [] : await this.artifacts.listArtifacts({ projectId: projectId2 });
212224
212454
  const byArtifact = new Map(stored.map((artifact) => [artifact.artifactId, artifact]));
212225
212455
  const bindings = await this.artifacts.listWorkspaceBindings();
212226
212456
  const projectByWorkorder = new Map(bindings.map((binding) => [binding.workOrderId, binding.projectId]));
212227
212457
  for (const artifact of this.kernel.model.artifacts.values()) {
212228
- if (projectByWorkorder.get(artifact.workspace) !== projectId2) continue;
212458
+ const bound = projectByWorkorder.get(artifact.workspace);
212459
+ if (uncategorized ? bound !== void 0 : bound !== projectId2) continue;
212229
212460
  const descriptor = decodeDocumentDescription(artifact.description);
212230
212461
  const info = nodeInfo(this.kernel.model, artifact);
212231
212462
  const revisions = revisionsOf(this.kernel.model, artifact.id);
@@ -216890,8 +217121,18 @@ function wireRecoveredChatTurn(deps) {
216890
217121
  clearInterval(ckptTimer);
216891
217122
  const failed = exitFailed(info);
216892
217123
  if (failed) {
216893
- live.emit({ type: "error", text: exitErrorText(info) });
216894
- itemLedger.recordError(exitErrorText(info));
217124
+ const errText2 = exitErrorText(info);
217125
+ live.emit({ type: "error", text: errText2 });
217126
+ const errorOutcome = itemLedger.recordError(errText2, { ...info.reason ? { reason: info.reason } : {} });
217127
+ live.publishServerItem({
217128
+ itemId: errorOutcome.itemId,
217129
+ itemType: "control",
217130
+ startedVersion: errorOutcome.startedVersion,
217131
+ version: errorOutcome.version,
217132
+ ord: errorOutcome.ord,
217133
+ status: "failed",
217134
+ payload: { code: "error", note: errText2, ...info.reason ? { reason: info.reason } : {} }
217135
+ });
216895
217136
  }
216896
217137
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
216897
217138
  const parts = collectedParts();
@@ -216918,6 +217159,11 @@ function wireRecoveredChatTurn(deps) {
216918
217159
  ...nextRuntimeSessionId ? { runtimeSessionId: nextRuntimeSessionId } : {},
216919
217160
  touchedAt: finishedAt
216920
217161
  }).catch(() => void 0);
217162
+ if (deps.onTurnSettled) {
217163
+ await deps.onTurnSettled(failed ? "failed" : "succeeded", finishedAt).catch((err) => {
217164
+ log3(`[chat-recovery] \u4F1A\u8BDD ${plan.chatSessionId} \u6536\u53E3\u8BB0\u8D26\u5931\u8D25\uFF08\u9000\u56DE\u6E05\u626B\u62CD\u515C\u5E95\uFF09: ${String(err)}`);
217165
+ });
217166
+ }
216921
217167
  if (!liveTerminalEmitted) {
216922
217168
  live.emitLive({
216923
217169
  protocolVersion: 2,
@@ -217241,6 +217487,22 @@ async function recoverChatTurnsAcrossCompanies(deps) {
217241
217487
  continue;
217242
217488
  }
217243
217489
  const handle = deps.recover(plan);
217490
+ const settleTurnId = turn?.id;
217491
+ const onTurnSettled = settleTurnId && stores.settleTurn ? async (status, completedAt) => {
217492
+ await stores.settleTurn(settleTurnId, {
217493
+ status,
217494
+ completedAt,
217495
+ ...status === "failed" ? { lastError: `\u91CD\u6302\u7684\u8F6E\u4EE5 ${status} \u6536\u5C3E\uFF08run ${plan.runId}\uFF09` } : {}
217496
+ });
217497
+ log3(`[chat-recovery] \u8F6E\u6B21 ${settleTurnId}\uFF08\u4F1A\u8BDD ${plan.chatSessionId}\uFF09\u968F\u91CD\u6302\u6536\u53E3\u4E3A ${status}\uFF0C\u4F1A\u8BDD\u69FD\u5DF2\u91CA\u653E`);
217498
+ await deps.onTurnSettled?.({
217499
+ chatSessionId: plan.chatSessionId,
217500
+ turnId: settleTurnId,
217501
+ status,
217502
+ reason: `\u91CD\u6302\u7684\u8F6E\u4EE5 ${status} \u6536\u5C3E`,
217503
+ ...companyId ? { companyId } : {}
217504
+ });
217505
+ } : void 0;
217244
217506
  const { done } = wireRecoveredChatTurn({
217245
217507
  plan,
217246
217508
  handle,
@@ -217250,6 +217512,7 @@ async function recoverChatTurnsAcrossCompanies(deps) {
217250
217512
  register: () => deps.register(plan, handle),
217251
217513
  unregister: () => deps.unregister(plan),
217252
217514
  ...deps.traceHealth ? { traceHealth: deps.traceHealth } : {},
217515
+ ...onTurnSettled ? { onTurnSettled } : {},
217253
217516
  ...stores.items ? { items: stores.items, itemsTurnId } : {},
217254
217517
  log: log3
217255
217518
  });
@@ -219317,7 +219580,14 @@ ${input.description}
219317
219580
  vars.filter((v2) => v2.scope === "personal" && v2.actorId === actorId && v2.connectorId).map((v2) => v2.connectorId)
219318
219581
  )];
219319
219582
  const effective = await this.effectiveConnectorEntries(actorId, config2, connectors);
219320
- return { connections, globalConnected, actorConnected, effective };
219583
+ const flat = {};
219584
+ for (const v2 of vars) if (v2.scope === "global" && !v2.actorId) flat[v2.key] = this.decodeCiphertext(v2.valueEncrypted);
219585
+ for (const v2 of vars) if (v2.scope === "personal" && (!v2.actorId || v2.actorId === actorId)) flat[v2.key] = this.decodeCiphertext(v2.valueEncrypted);
219586
+ const credentialReady = collectAllConnectorCredentials({
219587
+ vars: flat,
219588
+ actorName: flat["GIT_AUTHOR_NAME"] ?? actorId.split(":").pop() ?? "oasis-agent"
219589
+ }).map((c) => c.slug);
219590
+ return { connections, globalConnected, actorConnected, effective, credentialReady };
219321
219591
  }
219322
219592
  /**
219323
219593
  * 该员工每个连接器的「有效启用」集合(变量/技能注入闸门):
@@ -219430,6 +219700,11 @@ ${input.description}
219430
219700
  deliveryMode: v2.deliveryMode ?? "env",
219431
219701
  valueEncrypted: v2.encrypted === false ? `plain:${v2.value}` : this.encrypt(v2.value),
219432
219702
  updatedAt: this.now(),
219703
+ /* 创建者 / 创建时间 / 更新者:**由服务端落,不接受调用方自报**。
219704
+ createdBy/createdAt 两格存储层按「第一次落行才写」处理(COALESCE 旧值),所以这里每次都传,
219705
+ 写不写得进去由存储层决定;updatedBy 每次覆盖。
219706
+ `by` 缺省(连接器授权流程等系统写入)时三格都不传——**不拿一个假身份填上去**。 */
219707
+ ...v2.by ? { createdBy: v2.by, createdAt: this.now(), updatedBy: v2.by } : {},
219433
219708
  // ADR 凭据保险库补章 §D2.2:expiresAt 写侧穿透——**显式带 key**(含 undefined),使清空(传空→undefined)
219434
219709
  // 也落到底(store 两实现都按此覆盖)。lastUsedAt 从不由写侧传,取用时另刷(§D2.3)。
219435
219710
  expiresAt: v2.expiresAt
@@ -219502,7 +219777,12 @@ ${input.description}
219502
219777
  updatedAt: r.updatedAt,
219503
219778
  // ADR 凭据保险库补章 §D2.1:到期/取用时间戳透传(展示层据此染色;补章 §D2.4)。
219504
219779
  ...r.expiresAt !== void 0 ? { expiresAt: r.expiresAt } : {},
219505
- ...r.lastUsedAt !== void 0 ? { lastUsedAt: r.lastUsedAt } : {}
219780
+ ...r.lastUsedAt !== void 0 ? { lastUsedAt: r.lastUsedAt } : {},
219781
+ // 创建者 / 创建时间 / 更新者(子 PRD《密钥管理》§2 + 2026-09-10 追加)。
219782
+ // 存量行没有这三格,原样不带出去——展示侧出「—」,不拿 updatedAt 冒充 createdAt。
219783
+ ...r.createdBy !== void 0 ? { createdBy: r.createdBy } : {},
219784
+ ...r.createdAt !== void 0 ? { createdAt: r.createdAt } : {},
219785
+ ...r.updatedBy !== void 0 ? { updatedBy: r.updatedBy } : {}
219506
219786
  };
219507
219787
  });
219508
219788
  }
@@ -219789,13 +220069,6 @@ var init_memory = __esm({
219789
220069
  async write(ctx, input) {
219790
220070
  this.assertWritable(input, "agent");
219791
220071
  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
220072
  return this.writeResolved(ctx, input, input.scope === "project" ? projectId2 : null, "agent");
219800
220073
  }
219801
220074
  /** 控制台管理面写入:actorId/projectId 由受保护的管理路由明确给出,不复用 agent 自用入口。 */
@@ -220905,6 +221178,21 @@ var init_connector_skills = __esm({
220905
221178
  }
220906
221179
  });
220907
221180
 
221181
+ // ../server/src/domains/nodes/runtime-tenancy.ts
221182
+ function foreignNodeBlock(node2, expectedCompanyId) {
221183
+ if (!expectedCompanyId) return null;
221184
+ if (!node2) return null;
221185
+ const owner = node2.companyId;
221186
+ if (!owner) return null;
221187
+ if (owner === expectedCompanyId) return null;
221188
+ 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`;
221189
+ }
221190
+ var init_runtime_tenancy = __esm({
221191
+ "../server/src/domains/nodes/runtime-tenancy.ts"() {
221192
+ "use strict";
221193
+ }
221194
+ });
221195
+
220908
221196
  // ../server/src/domains/actors/routes.ts
220909
221197
  async function asApiError(run) {
220910
221198
  try {
@@ -221227,6 +221515,9 @@ function actorsDomain(opts) {
221227
221515
  const { service } = await resolveCtx(req.auth.companyId);
221228
221516
  const b2 = req.body;
221229
221517
  if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
221518
+ const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
221519
+ const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
221520
+ if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
221230
221521
  const { clearedModel } = await service.bind({ actorId: b2.actorId, nodeId: b2.nodeId, runtimeKind: b2.runtimeKind, status: b2.status ?? "active" }, req.auth.actor);
221231
221522
  return { status: 201, body: { ok: true, ...clearedModel ? { clearedModel } : {} } };
221232
221523
  });
@@ -221345,6 +221636,8 @@ function actorsDomain(opts) {
221345
221636
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
221346
221637
  ...b2.overrides !== void 0 ? { overrides: b2.overrides } : {},
221347
221638
  ...b2.encrypted !== void 0 ? { encrypted: b2.encrypted } : {},
221639
+ // 创建者/更新者取**当前登录人**,不接受 body 传入——自报的「谁建的」等于没有这一格。
221640
+ by: req.auth.actor,
221348
221641
  expiresAt: expiresAt2
221349
221642
  });
221350
221643
  await auditVariableChange(
@@ -221417,6 +221710,7 @@ function actorsDomain(opts) {
221417
221710
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
221418
221711
  ...b2.overrides !== void 0 ? { overrides: b2.overrides } : {},
221419
221712
  ...b2.encrypted !== void 0 ? { encrypted: b2.encrypted } : {},
221713
+ by: req.auth.actor,
221420
221714
  expiresAt: expiresAt2
221421
221715
  });
221422
221716
  await auditVariableChange(req, "credential_write", b2.key, `personal:${actorId}`);
@@ -222036,6 +222330,7 @@ var init_routes3 = __esm({
222036
222330
  init_image_upload();
222037
222331
  init_skill_materializer();
222038
222332
  init_connector_skills();
222333
+ init_runtime_tenancy();
222039
222334
  CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
222040
222335
  RevealRateLimiter = class {
222041
222336
  constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
@@ -222232,7 +222527,7 @@ function createActorsDomain(opts) {
222232
222527
  return {
222233
222528
  service: defaultCtx.service,
222234
222529
  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 } : {} })
222530
+ 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
222531
  };
222237
222532
  }
222238
222533
  var import_node_crypto49;
@@ -222434,12 +222729,9 @@ function projectsDomain(opts) {
222434
222729
  }
222435
222730
  });
222436
222731
  router.get("/api/projects", async (req) => {
222437
- const inc = req.query?.get("includeEphemeral");
222438
- const includeEphemeral = inc === "1" || inc === "true";
222439
222732
  const uncatRaw = req.query?.get("includeUncategorized");
222440
222733
  const includeUncategorized = uncatRaw === null || uncatRaw === void 0 ? void 0 : !(uncatRaw === "0" || uncatRaw === "false");
222441
222734
  const projects = await (await svc(req)).listProjects({
222442
- includeEphemeral,
222443
222735
  ...includeUncategorized !== void 0 ? { includeUncategorized } : {}
222444
222736
  });
222445
222737
  const page = paginate(projects.map(toApiProject), req.query);
@@ -222477,8 +222769,7 @@ function projectsDomain(opts) {
222477
222769
  body: {
222478
222770
  ok: true,
222479
222771
  project_id: result.projectId,
222480
- moved_work_order_ids: result.movedWorkOrderIds,
222481
- carried_git_location_count: result.carriedGitLocationCount
222772
+ moved_work_order_ids: result.movedWorkOrderIds
222482
222773
  }
222483
222774
  };
222484
222775
  } catch (err) {
@@ -222720,8 +223011,8 @@ ${errs.join("\n")}`);
222720
223011
  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
223012
  if (nulledPatch) {
222722
223013
  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 } };
223014
+ await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
223015
+ return { status: 200, body: { work_order_id: req.params.workOrderId, project_id: null } };
222725
223016
  } catch (err) {
222726
223017
  throw mapProjectError(err);
222727
223018
  }
@@ -222737,8 +223028,8 @@ ${errs.join("\n")}`);
222737
223028
  });
222738
223029
  router.delete("/api/work-orders/:workOrderId/project", async (req) => {
222739
223030
  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 } };
223031
+ await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
223032
+ return { status: 200, body: { work_order_id: req.params.workOrderId, project_id: null } };
222742
223033
  } catch (err) {
222743
223034
  throw mapProjectError(err);
222744
223035
  }
@@ -223222,6 +223513,9 @@ function toApiProjectFilesView(view) {
223222
223513
  // 其它外部引用→链接)。交付物整份产物行才有,上传文件与文件夹为 null。
223223
223514
  content_kind: item.contentKind ?? null,
223224
223515
  content_ref: item.contentRef ?? null,
223516
+ // 上传文件行(项目级上传 / 工单级文件)的 blob 引用 + MIME:前端据此直读 blob 预览。交付物与文件夹为 null。
223517
+ blob_ref: item.blobRef ?? null,
223518
+ content_type: item.contentType ?? null,
223225
223519
  size: item.size,
223226
223520
  task_id: item.taskId,
223227
223521
  task_title: item.taskTitle,
@@ -223256,7 +223550,7 @@ var init_routes5 = __esm({
223256
223550
  "use strict";
223257
223551
  init_router();
223258
223552
  init_src();
223259
- init_ephemeral_project();
223553
+ init_uncategorized_project();
223260
223554
  }
223261
223555
  });
223262
223556
 
@@ -228322,13 +228616,15 @@ async function buildChatPreview(chatStore, sessionId, who) {
228322
228616
  if (lastAsst) lines.push({ label: who, text: clip2(lastAsst.content) });
228323
228617
  return lines.length ? { lines } : void 0;
228324
228618
  }
228325
- async function buildChatInbox(chatStore, markers, me, resolveActor) {
228619
+ async function buildChatInbox(chatStore, markers, me, resolveActor, excludeChildSessions) {
228326
228620
  const sessions = await chatStore.listSessions(me);
228327
228621
  const byScope = new Map(markers.map((m2) => [m2.scope, m2]));
228328
228622
  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);
228623
+ const excluded = excludeChildSessions ? new Set(await excludeChildSessions(unread.map((s2) => s2.id)).catch(() => [])) : /* @__PURE__ */ new Set();
228329
228624
  const out = [];
228330
228625
  let previewsFetched = 0;
228331
228626
  for (const s2 of unread) {
228627
+ if (excluded.has(s2.id)) continue;
228332
228628
  const who = resolveActor?.(s2.aiActorId)?.name ?? "\u52A9\u7406";
228333
228629
  const abnormal = s2.lastTurnQuality === "abnormal";
228334
228630
  const preview2 = previewsFetched < PREVIEW_FETCH_CAP ? await buildChatPreview(chatStore, s2.id, who) : void 0;
@@ -228537,6 +228833,9 @@ function toAuditEntry(op) {
228537
228833
  actor: op.actor,
228538
228834
  op: op.kind,
228539
228835
  artifactId: op.artifactId,
228836
+ /* 动作名单独发一份(见契约 `AuditEntry.actionLabel`):`summary` 拼着裸 artifactId,
228837
+ 不能直接给人看,而「已处理」那一行需要「干了什么 + 在哪个节点」两截并排。 */
228838
+ actionLabel: KIND_LABEL2[op.kind] ?? op.kind,
228540
228839
  summary: `${KIND_LABEL2[op.kind] ?? op.kind} \xB7 ${op.artifactId}`
228541
228840
  };
228542
228841
  }
@@ -228573,6 +228872,7 @@ function eventToAuditEntry(rec) {
228573
228872
  actor: rec.actorId,
228574
228873
  op: ev.kind,
228575
228874
  artifactId,
228875
+ actionLabel: KIND_LABEL2[ev.kind] ?? ev.kind,
228576
228876
  summary: `${KIND_LABEL2[ev.kind] ?? ev.kind}${artifactId ? ` \xB7 ${artifactId}` : ""}`
228577
228877
  };
228578
228878
  }
@@ -229523,13 +229823,32 @@ async function buildLeadResolver(registry2) {
229523
229823
  return null;
229524
229824
  };
229525
229825
  }
229526
- async function buildResolver(registry2) {
229826
+ async function memberNameResolver(listMembers, companyId) {
229827
+ if (!listMembers) return void 0;
229828
+ let members;
229829
+ try {
229830
+ members = await listMembers(companyId);
229831
+ } catch (err) {
229832
+ console.warn(`[collab] \u6210\u5458\u6635\u79F0\u8BFB\u53D6\u5931\u8D25\uFF0C\u672C\u6B21\u56DE\u843D\u5458\u5DE5\u6863\u6848\u59D3\u540D: ${String(err)}`);
229833
+ return void 0;
229834
+ }
229835
+ const byActor2 = /* @__PURE__ */ new Map();
229836
+ for (const m2 of members) {
229837
+ const name = m2.displayName?.trim();
229838
+ if (name) byActor2.set(m2.actorId, name);
229839
+ }
229840
+ return (id) => byActor2.get(id);
229841
+ }
229842
+ async function buildResolver(registry2, memberName) {
229527
229843
  if (!registry2) return void 0;
229528
229844
  const actors = await registry2.listActors();
229529
229845
  const byId = new Map(actors.map((a) => [a.id, a]));
229530
229846
  return (id) => {
229531
229847
  const a = byId.get(id);
229532
- return a ? actorRefFieldsOf(a) : actorRefFallbackOf(id);
229848
+ if (!a) return actorRefFallbackOf(id);
229849
+ const fields = actorRefFieldsOf(a);
229850
+ const nickname = memberName?.(id);
229851
+ return nickname ? { ...fields, name: nickname } : fields;
229533
229852
  };
229534
229853
  }
229535
229854
  async function buildProjectResolver(artifacts) {
@@ -229651,6 +229970,7 @@ function collabDomain(opts) {
229651
229970
  cache.set(companyId, ctx);
229652
229971
  return ctx;
229653
229972
  }
229973
+ const resolverFor = async (companyId, registry2) => buildResolver(registry2, await memberNameResolver(opts.listMembers, companyId));
229654
229974
  return (router) => {
229655
229975
  router.get("/api/organization/usage-summary", async (req) => {
229656
229976
  if (!opts.trace) {
@@ -229695,7 +230015,7 @@ function collabDomain(opts) {
229695
230015
  });
229696
230016
  router.get("/api/workorders", async (req) => {
229697
230017
  const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
229698
- const resolve10 = await buildResolver(registry2);
230018
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
229699
230019
  const resolveProject = await buildProjectResolver(artifacts);
229700
230020
  const dispatchPausedOf = await buildDispatchPausedResolver(kernel.model, artifacts);
229701
230021
  const ref2 = (id) => {
@@ -229718,7 +230038,7 @@ function collabDomain(opts) {
229718
230038
  if (projectFilter) {
229719
230039
  items = items.filter((wo) => {
229720
230040
  const bound = wo.projectId;
229721
- if (isUncategorizedProjectId(projectFilter)) return typeof bound === "string" && isEphemeralProject(bound);
230041
+ if (isUncategorizedProjectId(projectFilter)) return bound == null;
229722
230042
  return bound === projectFilter;
229723
230043
  });
229724
230044
  }
@@ -229726,7 +230046,7 @@ function collabDomain(opts) {
229726
230046
  });
229727
230047
  router.get("/api/workorder-metrics", async (req) => {
229728
230048
  const { kernel, registry: registry2, artifacts, engineStore } = await resolveCtx(req.auth.companyId);
229729
- const resolve10 = await buildResolver(registry2);
230049
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
229730
230050
  const resolveProject = await buildProjectResolver(artifacts);
229731
230051
  const dispatchPausedOf = await buildDispatchPausedResolver(kernel.model, artifacts);
229732
230052
  const workorders = buildWorkorderSummaries(kernel.model, resolve10, resolveProject, dispatchPausedOf);
@@ -229773,7 +230093,7 @@ function collabDomain(opts) {
229773
230093
  });
229774
230094
  router.get("/api/workorders/:id", async (req) => {
229775
230095
  const { kernel, oplog, blobs, registry: registry2, artifacts, engineStore } = await resolveCtx(req.auth.companyId);
229776
- const resolve10 = await buildResolver(registry2);
230096
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
229777
230097
  const resolveProject = await buildProjectResolver(artifacts);
229778
230098
  const dispatchPausedOf = await buildDispatchPausedResolver(kernel.model, artifacts);
229779
230099
  const wsArts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === req.params.id);
@@ -229848,7 +230168,7 @@ function collabDomain(opts) {
229848
230168
  if (![...kernel.model.artifacts.values()].some((a) => a.workspace === workorderId)) {
229849
230169
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${workorderId}` } } };
229850
230170
  }
229851
- const resolve10 = await buildResolver(registry2);
230171
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
229852
230172
  const ref2 = (id) => {
229853
230173
  const extra = resolve10?.(id);
229854
230174
  return {
@@ -229891,7 +230211,7 @@ function collabDomain(opts) {
229891
230211
  if (!cardId) {
229892
230212
  return { status: 400, body: { error: { code: "bad_request", message: "\u7F3A\u5C11 card \u53C2\u6570\uFF08\u52A8\u6001\u5361 id\uFF09" } } };
229893
230213
  }
229894
- const resolve10 = await buildResolver(registry2);
230214
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
229895
230215
  const ref2 = (id) => {
229896
230216
  const extra = resolve10?.(id);
229897
230217
  return {
@@ -229931,7 +230251,7 @@ function collabDomain(opts) {
229931
230251
  if (!ledger) {
229932
230252
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${req.params.id}` } } };
229933
230253
  }
229934
- const resolve10 = await buildResolver(registry2);
230254
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
229935
230255
  const ids2 = /* @__PURE__ */ new Set();
229936
230256
  if (ledger.owner) ids2.add(ledger.owner);
229937
230257
  if (ledger.manager) ids2.add(ledger.manager);
@@ -229950,13 +230270,14 @@ function collabDomain(opts) {
229950
230270
  return { status: 200, body: { ...ledger, actors } };
229951
230271
  });
229952
230272
  router.get("/api/workorders/:id/files", async (req) => {
229953
- const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
230273
+ const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
229954
230274
  const ws = req.params.id;
229955
230275
  const exists = [...kernel.model.artifacts.values()].some((a) => a.workspace === ws);
229956
230276
  if (!exists) {
229957
230277
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
229958
230278
  }
229959
230279
  const files = artifacts?.listWorkOrderFiles ? await artifacts.listWorkOrderFiles(ws) : [];
230280
+ const resolve10 = await buildResolver(registry2);
229960
230281
  const items = files.map((f2) => ({
229961
230282
  id: f2.fileId,
229962
230283
  name: f2.name,
@@ -229966,12 +230287,14 @@ function collabDomain(opts) {
229966
230287
  updated_at: f2.linkedAt,
229967
230288
  blob_ref: f2.blobId,
229968
230289
  source_session_id: f2.sourceSessionId ?? null,
229969
- source_message_id: f2.sourceMessageId ?? null
230290
+ source_message_id: f2.sourceMessageId ?? null,
230291
+ uploaded_by: f2.uploadedBy ?? null,
230292
+ uploader_name: f2.uploadedBy ? resolve10?.(f2.uploadedBy)?.name ?? null : null
229970
230293
  }));
229971
230294
  return { status: 200, body: { total: items.length, items } };
229972
230295
  });
229973
230296
  router.post("/api/workorders/:id/files", async (req) => {
229974
- const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
230297
+ const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
229975
230298
  const ws = req.params.id;
229976
230299
  const exists = [...kernel.model.artifacts.values()].some((a) => a.workspace === ws);
229977
230300
  if (!exists) {
@@ -230005,7 +230328,9 @@ function collabDomain(opts) {
230005
230328
  blobId,
230006
230329
  size: bytes2.byteLength,
230007
230330
  ...contentType ? { contentType } : {},
230008
- linkedAt
230331
+ linkedAt,
230332
+ // 上传者:项目页「创建人」列与预览副标据此署名(建单带入的那条路记的是建单发起人)。
230333
+ uploadedBy: req.auth.actor
230009
230334
  };
230010
230335
  try {
230011
230336
  await artifacts.addWorkOrderFile(file);
@@ -230015,6 +230340,7 @@ function collabDomain(opts) {
230015
230340
  body: { error: { code: "write_failed", message: `\u5199 work_order_files \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}` } }
230016
230341
  };
230017
230342
  }
230343
+ const resolve10 = await buildResolver(registry2);
230018
230344
  const item = {
230019
230345
  id: fileId,
230020
230346
  name,
@@ -230024,7 +230350,9 @@ function collabDomain(opts) {
230024
230350
  updated_at: linkedAt,
230025
230351
  blob_ref: blobId,
230026
230352
  source_session_id: null,
230027
- source_message_id: null
230353
+ source_message_id: null,
230354
+ uploaded_by: req.auth.actor,
230355
+ uploader_name: resolve10?.(req.auth.actor)?.name ?? null
230028
230356
  };
230029
230357
  return { status: 201, body: { item } };
230030
230358
  });
@@ -230098,7 +230426,7 @@ function collabDomain(opts) {
230098
230426
  const [leadOf, resolveActor, initialMarkers] = await Promise.all([
230099
230427
  buildLeadResolver(registry2),
230100
230428
  // 决策 0026 (c):gap 升级也发卡住节点 owner 的人类上级
230101
- buildResolver(registry2),
230429
+ resolverFor(req.auth.companyId, registry2),
230102
230430
  // 决策 0042:gap reporter 名册 join
230103
230431
  myReadMarkers ? myReadMarkers.listMarkers(me) : Promise.resolve([])
230104
230432
  // ADR-0086:知会型条目的已读水位
@@ -230124,7 +230452,8 @@ function collabDomain(opts) {
230124
230452
  return void 0;
230125
230453
  }
230126
230454
  })();
230127
- const chatItems = chatStore && myReadMarkers ? await buildChatInbox(chatStore, markers, me, resolveActor) : [];
230455
+ const excludeChildSessions = opts.resolveDelegationChildIds ? (ids2) => opts.resolveDelegationChildIds(req.auth.companyId, ids2) : void 0;
230456
+ const chatItems = chatStore && myReadMarkers ? await buildChatInbox(chatStore, markers, me, resolveActor, excludeChildSessions) : [];
230128
230457
  let waitingItems = [];
230129
230458
  if (engineStore) {
230130
230459
  const snaps = (await Promise.all(
@@ -230370,7 +230699,7 @@ function collabDomain(opts) {
230370
230699
  let teamMembers;
230371
230700
  let lead;
230372
230701
  if (registry2) {
230373
- const resolve10 = await buildResolver(registry2);
230702
+ const resolve10 = await resolverFor(req.auth.companyId, registry2);
230374
230703
  const meRec = await registry2.getActor(me);
230375
230704
  if (meRec?.teamId) {
230376
230705
  teamMembers = new Set((await registry2.listActors({ teamId: meRec.teamId })).map((m2) => m2.id));
@@ -230419,7 +230748,7 @@ var init_collab = __esm({
230419
230748
  init_workorder_metrics();
230420
230749
  init_chat_workorder_files();
230421
230750
  init_organization_usage();
230422
- init_ephemeral_project();
230751
+ init_uncategorized_project();
230423
230752
  init_tags();
230424
230753
  init_activity2();
230425
230754
  init_node_state();
@@ -230706,24 +231035,16 @@ function makeSeededWorkorderCreator(deps) {
230706
231035
  const blobs = company?.blobs ?? deps.blobs;
230707
231036
  const registry2 = company?.registry ?? deps.registry;
230708
231037
  const artifactState = company ? company.artifactState : deps.artifactState;
230709
- const createProject = company ? company.createProject : deps.createProject;
231038
+ const workorderDrafts = company ? company.workorderDrafts : deps.workorderDrafts;
230710
231039
  const workspace = input.idempotencyKey ? workspaceForKey(input.idempotencyKey) : genWorkspace();
230711
231040
  if (input.idempotencyKey) {
230712
231041
  const existing = existingWorkorder(kernel, workspace);
230713
231042
  if (existing) return existing;
230714
231043
  }
230715
- if (artifactState && (input.projectId || createProject)) {
230716
- await ensureWorkorderProject({
231044
+ if (artifactState && input.projectId) {
231045
+ await bindWorkorderProject({
230717
231046
  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
- // 上面的守卫保证走不到这个分支
231047
+ projectId: input.projectId,
230727
231048
  upsertBinding: (b2) => artifactState.upsertWorkspaceBinding(b2),
230728
231049
  ...deps.onWarn ? { onWarn: deps.onWarn } : {}
230729
231050
  });
@@ -230755,8 +231076,8 @@ function makeSeededWorkorderCreator(deps) {
230755
231076
  if (!preview2.endState.ok) {
230756
231077
  throw new Error(`\u5267\u672C\u5EFA\u5355\u7EC8\u6001\u6821\u9A8C\u4E0D\u8FC7\uFF1A${preview2.endState.violations.join("\uFF1B")}`);
230757
231078
  }
230758
- if (input.stage === true && deps.workorderDrafts) {
230759
- const draft = await deps.workorderDrafts.submit({
231079
+ if (input.stage === true && workorderDrafts) {
231080
+ const draft = await workorderDrafts.submit({
230760
231081
  workspace,
230761
231082
  plan: planned.plan,
230762
231083
  rootArtifactId: planned.rootArtifactId,
@@ -230818,7 +231139,7 @@ var init_create_seeded_workorder = __esm({
230818
231139
  "../server/src/domains/collab/create-seeded-workorder.ts"() {
230819
231140
  "use strict";
230820
231141
  import_node_crypto57 = require("node:crypto");
230821
- init_ephemeral_project();
231142
+ init_uncategorized_project();
230822
231143
  init_planner();
230823
231144
  enc2 = (s2) => new TextEncoder().encode(s2);
230824
231145
  }
@@ -233630,7 +233951,10 @@ function createChatSessionsDomain(opts) {
233630
233951
  const kernel = await kernelFor(req);
233631
233952
  const engineStore = kernel?.getStore?.();
233632
233953
  const snap = engineStore ? await engineStore.transaction((tx) => tx.loadWorkorder(workorderId)) : null;
233633
- const resolve10 = await buildResolver(await registryFor(req));
233954
+ const resolve10 = await buildResolver(
233955
+ await registryFor(req),
233956
+ await memberNameResolver(opts.listMembers, req.auth.companyId)
233957
+ );
233634
233958
  const ref2 = (id) => {
233635
233959
  const extra = resolve10?.(id);
233636
233960
  return { id, ...extra?.name ? { name: extra.name } : {}, ...extra?.role ? { role: extra.role } : {}, ...extra?.avatar ? { avatar: extra.avatar } : {} };
@@ -234058,6 +234382,9 @@ function createChatSessionsDomain(opts) {
234058
234382
  childSessionId: record8.childSessionId,
234059
234383
  expertActorId: record8.expertActorId,
234060
234384
  ...expert?.name ? { expertName: expert.name } : {},
234385
+ /* 头像与名字同一本名册、同一次查询取出来。缺了它,委派卡与输入框上方状态条只能退回
234386
+ 姓名首字的灰圆(发起人 2026-09-10 报的「灰底 D」)——那两处画的是「[头像] 名字 进行中」。 */
234387
+ ...expert?.avatar ? { expertAvatar: expert.avatar } : {},
234061
234388
  state: record8.state,
234062
234389
  ...record8.failureKind ? { failureKind: record8.failureKind } : {},
234063
234390
  ...record8.label ? { label: record8.label } : {},
@@ -237750,7 +238077,7 @@ function playbooksDomain(opts) {
237750
238077
  const drafts = opts.resolveWorkorderDrafts ? await opts.resolveWorkorderDrafts(req.auth?.companyId) : opts.workorderDrafts;
237751
238078
  if (result.draftId && chatSid && drafts) {
237752
238079
  try {
237753
- await drafts.tagSession(result.draftId, chatSid, opts.liveChat?.runFor(chatSid));
238080
+ await drafts.tagSession(result.draftId, chatSid, opts.liveChat?.runFor(chatSid), result.workspace);
237754
238081
  } catch (err) {
237755
238082
  console.warn(`[playbooks] tagSession(${result.draftId}) \u5931\u8D25\uFF0C\u4E0D\u963B\u585E\u5EFA\u5355\u54CD\u5E94:`, err);
237756
238083
  }
@@ -238497,7 +238824,10 @@ var init_daemon_adapter = __esm({
238497
238824
  } else if (msg.type === "session_normalized_event") {
238498
238825
  this.confirmDelivery(msg.dispatchId, "started");
238499
238826
  const entry = this.pending.get(msg.dispatchId);
238500
- if (!entry) return;
238827
+ if (!entry) {
238828
+ if (this.stashEnabled) this.stashFrame(msg.dispatchId, { type: "normalized", event: msg.event });
238829
+ return;
238830
+ }
238501
238831
  if (entry.normalizedCbs.length === 0) entry.normalizedBacklog.push(msg.event);
238502
238832
  else for (const cb of entry.normalizedCbs) cb(msg.event);
238503
238833
  } else if (msg.type === "session_output") {
@@ -238630,6 +238960,7 @@ var init_daemon_adapter = __esm({
238630
238960
  let replayOutput;
238631
238961
  let replayTelemetry;
238632
238962
  let replayLiveEvent;
238963
+ let replayNormalized;
238633
238964
  const flushReplay = () => {
238634
238965
  while (stashed.frames.length > 0) {
238635
238966
  const frame = stashed.frames[0];
@@ -238641,6 +238972,10 @@ var init_daemon_adapter = __esm({
238641
238972
  if (!replayTelemetry) return;
238642
238973
  stashed.frames.shift();
238643
238974
  replayTelemetry(frame.event);
238975
+ } else if (frame.type === "normalized") {
238976
+ if (!replayNormalized) return;
238977
+ stashed.frames.shift();
238978
+ replayNormalized(frame.event);
238644
238979
  } else {
238645
238980
  if (!replayLiveEvent) return;
238646
238981
  stashed.frames.shift();
@@ -238679,9 +239014,11 @@ var init_daemon_adapter = __esm({
238679
239014
  },
238680
239015
  ...supportsNormalizedEvents ? {
238681
239016
  onNormalizedProviderEvent(cb) {
238682
- entry.normalizedCbs.push(cb);
239017
+ replayNormalized ??= cb;
239018
+ flushReplay();
238683
239019
  const backlog = entry.normalizedBacklog.splice(0, entry.normalizedBacklog.length);
238684
239020
  for (const e of backlog) cb(e);
239021
+ entry.normalizedCbs.push(cb);
238685
239022
  }
238686
239023
  } : {},
238687
239024
  // ADR-0093 对话追加(重挂句柄同 spawn 句柄)。
@@ -240228,7 +240565,11 @@ function rowToVariable(row) {
240228
240565
  ...row.expires_at ? { expiresAt: row.expires_at } : {},
240229
240566
  ...row.last_used_at ? { lastUsedAt: row.last_used_at } : {},
240230
240567
  // 展示名(存量行没有 → 不带这一格,展示侧回落成 key)。
240231
- ...row.name ? { name: row.name } : {}
240568
+ ...row.name ? { name: row.name } : {},
240569
+ // 创建者 / 创建时间 / 更新者:2026-09-10 之前的行没有 → 不带这一格,展示侧出「—」。
240570
+ ...row.created_by ? { createdBy: row.created_by } : {},
240571
+ ...row.created_at ? { createdAt: row.created_at } : {},
240572
+ ...row.updated_by ? { updatedBy: row.updated_by } : {}
240232
240573
  };
240233
240574
  }
240234
240575
  var ident4, PostgresRegistryStore, rowToActor, rowToConfig, rowToAgentPrefs;
@@ -240399,6 +240740,9 @@ var init_postgres_registry = __esm({
240399
240740
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS name text`);
240400
240741
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS expires_at text`);
240401
240742
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS last_used_at text`);
240743
+ await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS created_by text`);
240744
+ await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS created_at text`);
240745
+ await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS updated_by text`);
240402
240746
  await pool.query(`DROP INDEX IF EXISTS "${s2}"."${s2}_variables_key_actor_uq"`);
240403
240747
  await pool.query(`DROP INDEX IF EXISTS "${s2}"."${s2}_variables_scope_uq"`);
240404
240748
  await pool.query(`
@@ -240900,11 +241244,17 @@ var init_postgres_registry = __esm({
240900
241244
  await this.pool.query(
240901
241245
  /* `name` 走 COALESCE:**没传 = 不动**。连接器授权流程写变量时手上没有「名称」,
240902
241246
  普通赋值会把人在密钥表单里填的名字抹掉——同 connectors.connected_by 那条。 */
240903
- `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at, name)
240904
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
241247
+ /* created_by / created_at COALESCE(**旧值**, 新值):**第一次落行才写**,之后每次 upsert
241248
+ 都保留原值——否则「谁建的」会被最后一次改的人顶掉,这一格就没有意义了。
241249
+ updated_by 反过来:每次都覆盖。三者与 name 的 COALESCE 方向相反,别照抄。 */
241250
+ `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at, name, created_by, created_at, updated_by)
241251
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
240905
241252
  ON CONFLICT (key, COALESCE(actor_id, ''), COALESCE(project_id, '')) DO UPDATE
240906
241253
  SET scope=$2, actor_id=$3, project_id=$4, connector_id=$5, overrides=$6, delivery_mode=$7, value_encrypted=$8, updated_at=$9, expires_at=$10,
240907
- name=COALESCE($11, ${this.s}.variables.name)`,
241254
+ name=COALESCE($11, ${this.s}.variables.name),
241255
+ created_by=COALESCE(${this.s}.variables.created_by, $12),
241256
+ created_at=COALESCE(${this.s}.variables.created_at, $13),
241257
+ updated_by=$14`,
240908
241258
  [
240909
241259
  v2.key,
240910
241260
  v2.scope,
@@ -240916,7 +241266,10 @@ var init_postgres_registry = __esm({
240916
241266
  v2.valueEncrypted,
240917
241267
  v2.updatedAt,
240918
241268
  v2.expiresAt ?? null,
240919
- v2.name ?? null
241269
+ v2.name ?? null,
241270
+ v2.createdBy ?? null,
241271
+ v2.createdAt ?? null,
241272
+ v2.updatedBy ?? null
240920
241273
  ]
240921
241274
  );
240922
241275
  }
@@ -241887,8 +242240,10 @@ var init_postgres_projects = __esm({
241887
242240
  content_type text,
241888
242241
  source_session_id text,
241889
242242
  source_message_id text,
241890
- linked_at timestamptz NOT NULL
242243
+ linked_at timestamptz NOT NULL,
242244
+ uploaded_by text
241891
242245
  )`);
242246
+ await pool.query(`ALTER TABLE "${s2}".work_order_files ADD COLUMN IF NOT EXISTS uploaded_by text`);
241892
242247
  await pool.query(`CREATE INDEX IF NOT EXISTS work_order_files_wo_idx ON "${s2}".work_order_files (work_order_id)`);
241893
242248
  return new _PostgresArtifactStateStore(pool, schema);
241894
242249
  }
@@ -242164,15 +242519,19 @@ var init_postgres_projects = __esm({
242164
242519
  const r = await this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`);
242165
242520
  return r.rows.map(rowToWorkspaceBinding);
242166
242521
  }
242522
+ async deleteWorkspaceBinding(workOrderId) {
242523
+ await this.pool.query(`DELETE FROM ${this.s}.workspace_bindings WHERE work_order_id = $1`, [workOrderId]);
242524
+ }
242167
242525
  /* ---------- 任务输入文件(PRD §2.6.3 建单交接;薄旁存,非 oplog) ---------- */
242168
242526
  async addWorkOrderFile(file) {
242169
242527
  await this.pool.query(
242170
242528
  `INSERT INTO ${this.s}.work_order_files
242171
- (file_id, work_order_id, name, blob_id, size, content_type, source_session_id, source_message_id, linked_at)
242172
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
242529
+ (file_id, work_order_id, name, blob_id, size, content_type, source_session_id, source_message_id, linked_at, uploaded_by)
242530
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
242173
242531
  ON CONFLICT (file_id) DO UPDATE SET
242174
242532
  work_order_id=$2, name=$3, blob_id=$4, size=$5, content_type=$6,
242175
- source_session_id=$7, source_message_id=$8, linked_at=$9`,
242533
+ source_session_id=$7, source_message_id=$8, linked_at=$9,
242534
+ uploaded_by=COALESCE($10, ${this.s}.work_order_files.uploaded_by)`,
242176
242535
  [
242177
242536
  file.fileId,
242178
242537
  file.workOrderId,
@@ -242182,7 +242541,8 @@ var init_postgres_projects = __esm({
242182
242541
  file.contentType ?? null,
242183
242542
  file.sourceSessionId ?? null,
242184
242543
  file.sourceMessageId ?? null,
242185
- file.linkedAt
242544
+ file.linkedAt,
242545
+ file.uploadedBy ?? null
242186
242546
  ]
242187
242547
  );
242188
242548
  }
@@ -242374,6 +242734,7 @@ var init_postgres_projects = __esm({
242374
242734
  ...row.content_type !== null ? { contentType: row.content_type } : {},
242375
242735
  ...row.source_session_id !== null ? { sourceSessionId: row.source_session_id } : {},
242376
242736
  ...row.source_message_id !== null ? { sourceMessageId: row.source_message_id } : {},
242737
+ ...row.uploaded_by !== null && row.uploaded_by !== void 0 ? { uploadedBy: row.uploaded_by } : {},
242377
242738
  linkedAt: iso2(row.linked_at)
242378
242739
  });
242379
242740
  }
@@ -255641,8 +256002,8 @@ var init_cutover_aware_workorder_drafts = __esm({
255641
256002
  async snapshotForNewRun(d, runId) {
255642
256003
  return (await this.pickWritable()).snapshotForNewRun(d, runId);
255643
256004
  }
255644
- async tagSession(id, s2, runId) {
255645
- return (await this.pickWritable()).tagSession(id, s2, runId);
256005
+ async tagSession(id, s2, runId, expectedWorkspace) {
256006
+ return (await this.pickWritable()).tagSession(id, s2, runId, expectedWorkspace);
255646
256007
  }
255647
256008
  async markEdited(id) {
255648
256009
  return (await this.pickWritable()).markEdited(id);
@@ -256613,6 +256974,7 @@ var init_src11 = __esm({
256613
256974
  init_sender();
256614
256975
  init_routes7();
256615
256976
  init_runtime_drift();
256977
+ init_runtime_tenancy();
256616
256978
  init_types4();
256617
256979
  init_roles();
256618
256980
  init_node_store();
@@ -256664,7 +257026,7 @@ var init_src11 = __esm({
256664
257026
  init_connector_skills();
256665
257027
  init_connector_tool_versions();
256666
257028
  init_board_watch();
256667
- init_ephemeral_project();
257029
+ init_uncategorized_project();
256668
257030
  init_run_settlement();
256669
257031
  init_reconcile_terminal_runs();
256670
257032
  init_run_settlement_wiring();
@@ -258176,6 +258538,9 @@ async function startServe(opts) {
258176
258538
  let memStoreShared;
258177
258539
  let isKnowledgeQueryAvailable = async (_actorId) => false;
258178
258540
  const actors = createActorsDomain({
258541
+ // 租户闸(ADR-0164 §5):`POST /api/bindings` 据它拦「把本公司员工绑到别家公司的机器上」。
258542
+ // 闭包读当前 nodeStore(下方 PG 分支会重赋值),与 `listBindingModelIds` 同一写法。
258543
+ resolveNodeTenancy: async (nodeId) => nodeStore.getNode(nodeId),
258179
258544
  store: registryStore,
258180
258545
  oplog,
258181
258546
  kernel: defaultCompanyKernel,
@@ -258244,7 +258609,10 @@ async function startServe(opts) {
258244
258609
  prefsStore: humanPrefsStore,
258245
258610
  registry: registryStore,
258246
258611
  actors: actors.service,
258247
- listRuntimes: (nodeId) => nodeStore.listRuntimes(nodeId),
258612
+ // 默认公司那一份。**闭包求值**:`defaultCompanyId` let,startup 后才被库里的真实 id 覆盖
258613
+ // (同 :1575 那句「必须是调用时求值」)。
258614
+ companyId: () => defaultCompanyId,
258615
+ listRuntimes: (nodeId, scope) => nodeStore.listRuntimes(nodeId, scope),
258248
258616
  isPlatformAdmin: async () => false
258249
258617
  // 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
258250
258618
  });
@@ -258536,8 +258904,6 @@ async function startServe(opts) {
258536
258904
  getSchema: () => schema,
258537
258905
  registry: registryStore,
258538
258906
  artifactState: artifactStateStore,
258539
- // 决策 C:剧本建单没给 projectId 时,兜底建一个由工单派生的临时项目并绑上。
258540
- createProject: makeEnsureProjectFromStore(projectStateStore),
258541
258907
  onWarn: (m2) => console.warn(m2),
258542
258908
  workorderDrafts,
258543
258909
  buildDraftGraph: async (draft) => buildWorkorderDraftDetail(draft, await buildResolver(registryStore), buildTypeReviewerResolver(schema, await registryStore.listActors())).graph,
@@ -258559,8 +258925,12 @@ async function startServe(opts) {
258559
258925
  blobs: engine2.blobs,
258560
258926
  registry: engine2.registry ?? registryStore,
258561
258927
  artifactState: stores.artifacts,
258562
- // 兜底临时项目也要建在本公司的库里,否则绑定两端分处两个 schema。
258563
- createProject: makeEnsureProjectFromStore(stores.projects)
258928
+ // 草案 store 必须按公司拿(2026-09-10 事故):此前它是装配期捕获的共享库实例,
258929
+ // `stage: true` 建出来的草案落大通铺、读侧却按公司解析,本公司永远查不到自己的草案。
258930
+ // ⚠ 这个 helper 的 const 声明在本处之后,只能在**请求时**才求值的闭包里引用
258931
+ // (同上方 resolveChatSession 的写法),直接在装配期引用会撞 TDZ。
258932
+ // planner 阻断项旁账**不在本次范围**,理由见 create-seeded-workorder.ts 里那段注释。
258933
+ workorderDrafts: await workorderDraftsFor(companyId)
258564
258934
  };
258565
258935
  }
258566
258936
  } : {}
@@ -259716,6 +260086,7 @@ async function startServe(opts) {
259716
260086
  }
259717
260087
  return resolved;
259718
260088
  };
260089
+ const listCompanyMembers = (companyId) => controlPlaneStore.listMembers(companyId ?? defaultCompanyId);
259719
260090
  const server = await startOasisServer({
259720
260091
  deployTargets,
259721
260092
  kernel: engine.kernel,
@@ -259871,7 +260242,9 @@ async function startServe(opts) {
259871
260242
  prefsStore: plane?.humanPrefs ?? humanPrefsStore,
259872
260243
  registry: engine2.registry,
259873
260244
  actors: ctx.service,
259874
- listRuntimes: (nodeId) => nodeStore.listRuntimes(nodeId),
260245
+ // 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
260246
+ companyId: () => cid,
260247
+ listRuntimes: (nodeId, scope) => nodeStore.listRuntimes(nodeId, scope),
259875
260248
  isPlatformAdmin: async (caller) => {
259876
260249
  const member = await controlPlaneStore.getMember(cid, caller);
259877
260250
  return member?.role === "owner";
@@ -259961,10 +260334,20 @@ async function startServe(opts) {
259961
260334
  // 草案图算「类型默认审核人」用(与 /api/cmd 同源类型目录)
259962
260335
  roles: roleStore,
259963
260336
  // 诊断 f1641dd3-abdaa869:概览角色桶补「默认承接人」(读 RoleDef.defaultOwner)
260337
+ // 真人展示名的第一顺位:本公司成员昵称压过员工档案 name(档案上那份缺省是邮箱)。
260338
+ listMembers: listCompanyMembers,
259964
260339
  chatSession: chatSessionStore,
259965
260340
  // ADR-0086:chat 知会条目(与 readMarkers 成对)
259966
260341
  // ADR「多租户数据面收口」§D2:知会条目按**当前公司**取。装了路由才给(dev 文件模式沿用单实例)。
259967
260342
  ...chatStoreRouter ? { resolveChatSession: chatStoreFor } : {},
260343
+ /* 委派子会话不进铃铛(发起人 2026-09-10):专家跑完回流到父会话,父会话那条已经通知过一次。
260344
+ 判据与左侧会话列表那条路同一个读口(`listChildSessionIds`,见下面 chat-sessions 域的
260345
+ `delegations` 接线),不另造。灰度关着 → 回空集合 = 不滤,与接入前逐字一致。 */
260346
+ resolveDelegationChildIds: async (companyId, sessionIds) => {
260347
+ if (!chatDelegationEnabledFromEnv() || sessionIds.length === 0) return [];
260348
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
260349
+ return ledger ? ledger.listChildSessionIds(sessionIds) : [];
260350
+ },
259968
260351
  readMarkers: readMarkerStore,
259969
260352
  // bundle=`inbox`:水位按当前公司取(与上面 resolveChatSession 成对)。
259970
260353
  ...pgPool ? { resolveReadMarkers: readMarkerStoreFor } : {},
@@ -259991,6 +260374,8 @@ async function startServe(opts) {
259991
260374
  // 装了路由(有 PG)就只走路由,解析不出公司一律 403,不回落默认公司。
259992
260375
  ...chatStoreRouter ? { resolveStore: chatStoreFor } : {},
259993
260376
  registry: registryStore,
260377
+ // 缺口线署名的真人名字与 collab 读面同一口径:本公司成员昵称压过员工档案 name。
260378
+ listMembers: listCompanyMembers,
259994
260379
  // ★ wo:acfee725 评审 blocker ①:两个 `/workdir/*` 读口与 files-view 段按**当前公司**解 binding,
259995
260380
  // 与派发、与委派回流搬产物同一处(`resolveCompanyBinding`)。上面那个 `registry` 是全局
259996
260381
  // store(`public.actor_bindings`),org-registry cutover 之后它是冻结旧镜像——只留给
@@ -260176,7 +260561,9 @@ async function startServe(opts) {
260176
260561
  listProjects: (companyId) => companyId === defaultCompanyId ? projectStateStore.listProjects() : Promise.resolve([]),
260177
260562
  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
260563
  getWorkspaceBinding: (companyId, workorderId) => companyId === defaultCompanyId ? artifactStateStore.getWorkspaceBinding(workorderId) : Promise.resolve(null),
260179
- isEphemeralProject: (companyId, projectId2) => companyId !== defaultCompanyId || isEphemeralProject(projectId2)
260564
+ // 非默认公司暂不支持项目级知识作用域(这里只有默认公司的 projectStateStore/名册可读),
260565
+ // 一律降级到组织作用域。旧版这个谓词还兼着"是不是 proj_tmp_ 临时项目",临时项目退役后只剩这一半。
260566
+ isProjectScopeUnsupported: (companyId) => companyId !== defaultCompanyId
260180
260567
  },
260181
260568
  actor: async (actorId, organizationId) => {
260182
260569
  if (actorId === SYSTEM_ACTOR2) return { id: actorId, kind: "system", status: "active" };
@@ -260681,7 +261068,9 @@ async function startServe(opts) {
260681
261068
  }
260682
261069
  binding = currentBinding;
260683
261070
  }
260684
- const requireDispatchNode = (target) => {
261071
+ const requireDispatchNode = async (target) => {
261072
+ const foreign = foreignNodeBlock(await nodeStore.getNode(target.nodeId).catch(() => null), runCompanyId);
261073
+ if (foreign) throw new ApiError(409, "NODE_NOT_IN_COMPANY", foreign);
260685
261074
  const connected = hub?.connectedDaemons().find((d) => d.daemonId === target.nodeId);
260686
261075
  if (!connected) {
260687
261076
  throw new ApiError(409, "NODE_OFFLINE", `\u7ED1\u5B9A\u8282\u70B9 ${target.nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF`);
@@ -260691,7 +261080,7 @@ async function startServe(opts) {
260691
261080
  }
260692
261081
  return connected;
260693
261082
  };
260694
- let node2 = requireDispatchNode(binding);
261083
+ let node2 = await requireDispatchNode(binding);
260695
261084
  if (!chatRemoteAdapter) {
260696
261085
  throw new ApiError(503, "NODE_GATEWAY_NOT_READY", "node-gateway \u5C1A\u672A\u5C31\u7EEA");
260697
261086
  }
@@ -260771,7 +261160,7 @@ async function startServe(opts) {
260771
261160
  }
260772
261161
  if (stableDispatch.target.nodeId !== binding.nodeId || stableDispatch.target.runtimeKind !== binding.runtimeKind) {
260773
261162
  binding = { actorId, ...stableDispatch.target, status: "active" };
260774
- node2 = requireDispatchNode(binding);
261163
+ node2 = await requireDispatchNode(binding);
260775
261164
  requireNodeConnectors();
260776
261165
  }
260777
261166
  }
@@ -261132,6 +261521,14 @@ async function startServe(opts) {
261132
261521
  requestId: `project-created:${project.id}`
261133
261522
  }, project.id);
261134
261523
  });
261524
+ const notifyChatTurnSettled = async (info) => {
261525
+ await server.settleDelegationFromSweep(
261526
+ info.chatSessionId,
261527
+ `\u5B50\u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u88AB\u6062\u590D\u5668\u6536\u53E3\u4E3A ${info.status}\uFF08${info.reason}\uFF09`,
261528
+ info.companyId,
261529
+ { outcome: info.status === "succeeded" ? "succeeded" : "failed" }
261530
+ );
261531
+ };
261135
261532
  const handshakeThrottleOpts = {};
261136
261533
  {
261137
261534
  const num3 = (name) => {
@@ -261194,10 +261591,12 @@ async function startServe(opts) {
261194
261591
  if (hit) return hit;
261195
261592
  const sessions = id === defaultCompanyId ? platformChatRecovery : PlatformChatRecoveryStore.open(pgPool, engineSchemaFor(id));
261196
261593
  const items = await chatItemsFor(id);
261594
+ const turnsForRecovery = await chatTurnsFor(id).catch(() => void 0);
261197
261595
  const plane = {
261198
261596
  listRunningAssistantMessages: () => sessions.listRunningAssistantMessages(),
261199
261597
  chatStore: sessions,
261200
261598
  getTurnForRun: (sid, rid) => sessions.getTurnForRun(sid, rid),
261599
+ ...turnsForRecovery ? { settleTurn: (turnId, patch) => turnsForRecovery.settleTurn(turnId, patch) } : {},
261201
261600
  ...items ? { items } : {}
261202
261601
  };
261203
261602
  chatRecoveryPlanes.set(id, plane);
@@ -261223,6 +261622,8 @@ async function startServe(opts) {
261223
261622
  isWired: (artifactId) => chatLiveSessions.has(`chat::${artifactId}`),
261224
261623
  ...opts2.stopOnFirstHit ? { stopOnFirstHit: true } : {},
261225
261624
  onCompanyError: chatRecoveryCompanyWarn(opts2.label),
261625
+ // 重挂的那一轮收口时当场外报,与清扫拍同一个接收方(见 `notifyChatTurnSettled`)。
261626
+ onTurnSettled: (info) => notifyChatTurnSettled(info),
261226
261627
  liveChat,
261227
261628
  trace: runRoutedTrace,
261228
261629
  traceHealth,
@@ -261493,14 +261894,12 @@ async function startServe(opts) {
261493
261894
  本拍是从轮次账本走的(`assistant_run_id` 一直有值),是"这一轮死没死"的权威来源。
261494
261895
  `companyId` 由扇出那层填好 —— 台账按公司分 store,少了它会查到默认公司的库。
261495
261896
  非委派会话调进去是空转:`settleFromSweep` 按 childSessionId 查不到记录就原样返回。 */
261496
- onTurnSettled: async ({ chatSessionId, status, reason, companyId }) => {
261497
- await server.settleDelegationFromSweep(
261498
- chatSessionId,
261499
- `\u5B50\u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u88AB\u6062\u590D\u5668\u6536\u53E3\u4E3A ${status}\uFF08${reason}\uFF09`,
261500
- companyId,
261501
- { outcome: status === "succeeded" ? "succeeded" : "failed" }
261502
- );
261503
- },
261897
+ /* `status` 必须带下去分流。跨重启完成的那一轮走的正是 `succeeded`:
261898
+ `settleRound` 是派发那一刻建的闭包,serve 一重启就随老进程没了。
261899
+ 不分流的话,一条**干完活**的委派会被按时钟闭合拍成 failed。
261900
+ 接收方与重挂路**同一个**(`notifyChatTurnSettled`)——两条路一处收敛,
261901
+ 不会出现「清扫拍收的能通知、重挂收的不能」这种只在重启后显形的差异。 */
261902
+ onTurnSettled: (info) => notifyChatTurnSettled(info),
261504
261903
  log: (m2) => console.log(m2),
261505
261904
  onCompanyError: (companyId, err) => {
261506
261905
  const key = companyId ?? "<default>";
@@ -262124,6 +262523,8 @@ async function startServe(opts) {
262124
262523
  checkDispatchable: async ({ actor, binding }) => {
262125
262524
  try {
262126
262525
  if (!binding.nodeId) return null;
262526
+ const foreign = foreignNodeBlock(await nodeStore.getNode(binding.nodeId).catch(() => null), companyId);
262527
+ if (foreign) return { reason: "node-not-in-company", errorMessage: foreign };
262127
262528
  const drift = runtimeDriftBlock(await nodeStore.listRuntimes(binding.nodeId), binding.nodeId, binding.runtimeKind);
262128
262529
  if (drift) return drift;
262129
262530
  const health = healthOfNode(binding.nodeId);
@@ -269282,6 +269683,9 @@ ${res.warning}`);
269282
269683
  ...expect !== void 0 ? { expectedVersion: Number(expect) } : {}
269283
269684
  });
269284
269685
  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`);
269686
+ if ((flags.get("scope") ?? "actor") === "project" && rec.projectId === null) {
269687
+ 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");
269688
+ }
269285
269689
  } catch (err) {
269286
269690
  const body2 = err instanceof ApiRequestError ? err.body : void 0;
269287
269691
  if (body2?.current) {
@@ -270840,7 +271244,7 @@ function shimScript() {
270840
271244
  }
270841
271245
 
270842
271246
  // src/index.ts
270843
- var PKG_VERSION = true ? "2.2.10" : "dev";
271247
+ var PKG_VERSION = true ? "2.2.12" : "dev";
270844
271248
  var LOCAL_BIN = localBin();
270845
271249
  var NPM_PREFIX = npmPrefix();
270846
271250
  var INSTANCE = DEFAULT_INSTANCE;