oasis_test_v2 2.2.9 → 2.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +725 -362
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -665,7 +665,20 @@ function isAgentAvatarPresetRef(ref2) {
665
665
  const id = ref2.slice(AGENT_AVATAR_PRESET_PREFIX.length);
666
666
  return AGENT_AVATAR_PRESET_IDS.includes(id);
667
667
  }
668
- var AGENT_AVATAR_PRESET_PREFIX, AGENT_AVATAR_LEGACY_PRESET_IDS, AGENT_AVATAR_V2_PRESET_IDS, AGENT_AVATAR_PRESET_IDS, SYSTEM_AVATAR_REF;
668
+ function humanAvatarToneOf(seed) {
669
+ let hash2 = 2166136261;
670
+ for (const ch of seed.trim()) {
671
+ hash2 = Math.imul(hash2 ^ (ch.codePointAt(0) ?? 0), 16777619) >>> 0;
672
+ }
673
+ return hash2 % HUMAN_AVATAR_TONE_COUNT;
674
+ }
675
+ function humanAvatarPresetRef(tone) {
676
+ return `${HUMAN_AVATAR_PRESET_PREFIX}${tone}`;
677
+ }
678
+ function defaultHumanAvatarRef(actorId) {
679
+ return humanAvatarPresetRef(humanAvatarToneOf(actorId));
680
+ }
681
+ var AGENT_AVATAR_PRESET_PREFIX, AGENT_AVATAR_LEGACY_PRESET_IDS, AGENT_AVATAR_V2_PRESET_IDS, AGENT_AVATAR_PRESET_IDS, SYSTEM_AVATAR_REF, HUMAN_AVATAR_PRESET_PREFIX, HUMAN_AVATAR_TONE_COUNT;
669
682
  var init_avatar = __esm({
670
683
  "../contract/src/avatar.ts"() {
671
684
  "use strict";
@@ -696,6 +709,8 @@ var init_avatar = __esm({
696
709
  ...AGENT_AVATAR_V2_PRESET_IDS
697
710
  ];
698
711
  SYSTEM_AVATAR_REF = "preset:system";
712
+ HUMAN_AVATAR_PRESET_PREFIX = "preset:human:";
713
+ HUMAN_AVATAR_TONE_COUNT = 8;
699
714
  }
700
715
  });
701
716
 
@@ -905,7 +920,17 @@ var init_link_windows = __esm({
905
920
  /** orphan-sweep 判「账上在跑、四张认领表都不认」为孤儿的宽限(`serve.ts`)。 */
906
921
  orphanGraceMs: 12e4,
907
922
  /** orphan-sweep 在 serve 冷启动后推迟判孤儿的窗口(`serve.ts`)。 */
908
- orphanColdStartGraceMs: 18e4
923
+ orphanColdStartGraceMs: 18e4,
924
+ /**
925
+ * `/items` 读时把「账上还 running、本进程 live 注册表里却没有」的一轮**显示**成 `interrupted`
926
+ * 的宽限(ADR-0510 D6 表格第二行)。
927
+ *
928
+ * 取值必须 **≥ {@link LINK_WINDOWS.orphanGraceMs}**:这条判定只写 wire、不写账本,它的作用是
929
+ * 让页面别在权威扫描还没开始看的时候就一直转圈;但它也**不该比权威更早下结论**——早于
930
+ * `orphanGraceMs` 就等于拿时间当判据抢在核实前面说话(ADR-0166)。两个数写在同一处,
931
+ * 谁单边调参 `tests/adr-0300-link-windows.test.ts` 当场变红。
932
+ */
933
+ chatTurnPhaseStaleAfterMs: 12e4
909
934
  };
910
935
  }
911
936
  });
@@ -914,12 +939,16 @@ var init_link_windows = __esm({
914
939
  function isSystemActorId(id) {
915
940
  return typeof id === "string" && id.startsWith(SYSTEM_ACTOR_ID_PREFIX);
916
941
  }
917
- var SYSTEM_ACTOR_ID_PREFIX, SYSTEM_ACTOR_DISPLAY_NAME, AGENT_PREFS_DEFAULTS, SkillOwnedByAnotherCompanyError;
942
+ function isHumanActorId(id) {
943
+ return typeof id === "string" && id.startsWith(HUMAN_ACTOR_ID_PREFIX);
944
+ }
945
+ var SYSTEM_ACTOR_ID_PREFIX, SYSTEM_ACTOR_DISPLAY_NAME, HUMAN_ACTOR_ID_PREFIX, AGENT_PREFS_DEFAULTS, SkillOwnedByAnotherCompanyError;
918
946
  var init_registry = __esm({
919
947
  "../contract/src/registry.ts"() {
920
948
  "use strict";
921
949
  SYSTEM_ACTOR_ID_PREFIX = "actor:system";
922
950
  SYSTEM_ACTOR_DISPLAY_NAME = "\u7CFB\u7EDF";
951
+ HUMAN_ACTOR_ID_PREFIX = "actor:human:";
923
952
  AGENT_PREFS_DEFAULTS = { memoryEnabled: true };
924
953
  SkillOwnedByAnotherCompanyError = class extends Error {
925
954
  constructor(skillId, companyId) {
@@ -4637,8 +4666,6 @@ var init_kill = __esm({
4637
4666
  function scan(state, ctx) {
4638
4667
  if (state.workorder.dispatchPaused) return { events: [], terminated: false };
4639
4668
  const events = [];
4640
- const now = ctx.at;
4641
- const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
4642
4669
  for (const node2 of state.liveNodes()) {
4643
4670
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
4644
4671
  if (!lw) {
@@ -4672,12 +4699,7 @@ function scan(state, ctx) {
4672
4699
  continue;
4673
4700
  }
4674
4701
  if (wstate === "running") {
4675
- const start = lw.lastActivityAt ?? lw.startedAt ?? lw.createdAt;
4676
- const elapsed = new Date(now).getTime() - new Date(start).getTime();
4677
- const wcell = policy.resolve({ executor: classify(node2.assigneeActorId), scenario: "work" });
4678
- if (wcell.timeoutAction.kind === "kill-rebuild" && elapsed > wcell.deadlineMs) {
4679
- events.push({ kind: "work.timeout", workorderId: state.workorder.id, workId: lw.id });
4680
- }
4702
+ checkRunningWorkTimeout(state, node2, lw, ctx, events);
4681
4703
  continue;
4682
4704
  }
4683
4705
  const blockers = scanBlockersForNode(state, node2, lw);
@@ -4742,10 +4764,9 @@ function scan(state, ctx) {
4742
4764
  workorderId: state.workorder.id,
4743
4765
  nodeId: node2.id,
4744
4766
  // ★ 合并写:保留 source ∈ {type, manual, model} 的原有行,覆写 source='closure' 的行。
4745
- // `plan.update_review_requirements` handler 是**全量替换**(killNodeReviews + replaceRequirements),
4767
+ // `plan.update_review_requirements` 的名单仍是**全量替换**,
4746
4768
  // 所以必须把手动 / 类型 / 模型来源的行一并带上——漏一条即误删协调者手动指派的审核人。
4747
- // killNodeReviews 会杀本节点在跑的评审:candidate 分支进入时该节点尚未派出评审(reqs 为空
4748
- // or 只含 closure 之外的组),本轮内 kill 无影响。
4769
+ // 总线为新事件写 preserveReviews:未变组的当前审核继续,只有新增 closure 组待派。
4749
4770
  reviewers: mergedClosureRequirements(state, node2.id, closureActors)
4750
4771
  });
4751
4772
  continue;
@@ -4848,6 +4869,15 @@ function scan(state, ctx) {
4848
4869
  const terminated = events.length === 0 && checkTerminated(state);
4849
4870
  return { events, terminated };
4850
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
+ }
4851
4881
  function openCommentBlockers(state, node2) {
4852
4882
  if (!node2.assigneeActorId) return [];
4853
4883
  return state.allIssues().filter((i) => i.kind === "comment" && i.resolvedAt === null && i.aboutNodeId === node2.id).map((i) => ({
@@ -4932,6 +4962,13 @@ function tryToRun(state, nodeId, ctx, events) {
4932
4962
  const node2 = state.node(nodeId);
4933
4963
  if (!node2) return;
4934
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
+ }
4935
4972
  const inbound = state.edgesInto(nodeId);
4936
4973
  for (const e of inbound) {
4937
4974
  if (e.kind !== "data" || !e.required) continue;
@@ -5560,6 +5597,7 @@ CREATE TABLE IF NOT EXISTS ${q2}.work_artifacts (
5560
5597
  content_kind text NOT NULL CHECK (content_kind IN ('inline','external','manifest')),
5561
5598
  ordinal integer,
5562
5599
  created_at timestamptz NOT NULL,
5600
+ content_edited_at timestamptz,
5563
5601
  UNIQUE (work_id, logical_path)
5564
5602
  );
5565
5603
  CREATE INDEX IF NOT EXISTS work_artifacts_node_idx ON ${q2}.work_artifacts (node_id);
@@ -5725,6 +5763,9 @@ ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS proposal_reason text;
5725
5763
  -- \u2605 \u8FD0\u884C\u6D3B\u8DC3\u5FC3\u8DF3\uFF1Av2 submit_output / work.conclude \u5199\u5165\uFF1Bscan \u8D85\u65F6\u4EE5\u5B83\u8D77\u7B97\uFF08work \u751F\u547D\u5468\u671F\u5EF6\u957F\u540E
5726
5764
  -- 30min \u949F\u4E0D\u80FD\u518D\u7F69\u6B7B\u6574\u4E2A\u4F1A\u8BDD\uFF09\u3002\u5B58\u91CF\u4E3A NULL \u2192 \u56DE\u9000 started_at/created_at\uFF0C\u884C\u4E3A\u4E0D\u53D8\u3002
5727
5765
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS last_activity_at timestamptz;
5766
+ -- \u4EA7\u7269\u6B63\u6587\u5C31\u5730\u6539\u5199\uFF08work.content_edited\uFF09\u7684\u65F6\u523B\u3002\u5C31\u5730\u6539\u5199\u4E0D\u65B0\u5F00 work\u3001\u4E0D\u94F8\u65B0\u7248\uFF0C
5767
+ -- \u6CA1\u6709\u8FD9\u4E00\u5217\uFF0C\u300C\u76F8\u5173\u6587\u4EF6\u300D\u8868\u7684\u65F6\u95F4\u5217\u5C31\u53EA\u80FD\u505C\u5728\u9A8C\u6536\u65F6\u523B\u3002\u5B58\u91CF\u884C\u4E3A NULL = \u4ECE\u672A\u88AB\u6539\u5199\u8FC7\u3002
5768
+ ALTER TABLE ${q2}.work_artifacts ADD COLUMN IF NOT EXISTS content_edited_at timestamptz;
5728
5769
  -- \u2605 \u51BB\u7ED3\u6765\u4FE1\u96C6\u5408\uFF08\u8BA1\u5212 \xA76.5\uFF0C\u9636\u6BB5 E\uFF09\uFF1Awork.create \u6309 reasons \u7684 issue \u9879\u51BB\u7ED3\u7684 issue id \u6279\u6B21\uFF08jsonb \u6570\u7EC4\uFF09\u3002
5729
5770
  -- reply \u5F52\u5C5E/\u9500\u8D26/\u5BA1\u8BA1\u53EA\u8BA4\u5B83\uFF0C\u4E0D\u9760\u65F6\u95F4\u7A97\u731C\u6D4B\u3002\u5B58\u91CF\u4E3A NULL\uFF08\u65E0\u6279\u6B21\u8BED\u4E49\uFF09\u3002
5730
5771
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS frozen_issue_ids jsonb;
@@ -11388,7 +11429,11 @@ var init_store_postgres = __esm({
11388
11429
  contentRef: a.content_ref,
11389
11430
  contentKind: a.content_kind,
11390
11431
  ordinal: a.ordinal,
11391
- createdAt: iso(a.created_at)
11432
+ createdAt: iso(a.created_at),
11433
+ // 只在真被改写过时才带这个键:从未改写的行在内存态里是**没有这个字段**的,
11434
+ // PG 回读时补一个 `null` 会让「事件重放出来的态」和「从库里读出来的态」深比不相等
11435
+ // (packages/engine/tests/store-conformance.postgres.test.ts 就是钉这条一致性的)。
11436
+ ...iso(a.content_edited_at) ? { contentEditedAt: iso(a.content_edited_at) } : {}
11392
11437
  })),
11393
11438
  workInputs: inputs.rows.map((i) => ({
11394
11439
  workId: i.work_id,
@@ -11623,9 +11668,9 @@ var init_store_postgres = __esm({
11623
11668
  for (const a of m2.rows) {
11624
11669
  await this.c.query(
11625
11670
  `INSERT INTO ${this.s}.work_artifacts
11626
- (id,work_id,node_id,type,name,logical_path,content_ref,content_kind,ordinal,created_at)
11627
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT (id) DO NOTHING`,
11628
- [a.id, a.workId, a.nodeId, a.type, a.name, a.logicalPath, a.contentRef, a.contentKind, a.ordinal, a.createdAt]
11671
+ (id,work_id,node_id,type,name,logical_path,content_ref,content_kind,ordinal,created_at,content_edited_at)
11672
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (id) DO NOTHING`,
11673
+ [a.id, a.workId, a.nodeId, a.type, a.name, a.logicalPath, a.contentRef, a.contentKind, a.ordinal, a.createdAt, a.contentEditedAt ?? null]
11629
11674
  );
11630
11675
  }
11631
11676
  break;
@@ -11974,7 +12019,7 @@ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
11974
12019
  }
11975
12020
  }
11976
12021
  }
11977
- var planUpdateSpec, planAddNode, planDeleteNode, planAddEdge, planDeleteEdge, planAssignActor, planUpdateReviewRequirements, planNodeRetry, planUpdateFields, planApply;
12022
+ var planUpdateSpec, planAddNode, planDeleteNode, planAddEdge, planDeleteEdge, planAssignActor, reviewerKey, planUpdateReviewRequirements, planNodeRetry, planUpdateFields, planApply;
11978
12023
  var init_plan = __esm({
11979
12024
  "../engine/src/handlers/plan.ts"() {
11980
12025
  "use strict";
@@ -12097,11 +12142,51 @@ var init_plan = __esm({
12097
12142
  });
12098
12143
  }
12099
12144
  };
12145
+ reviewerKey = (r) => JSON.stringify([r.reviewerActorId, r.reviewGroup]);
12100
12146
  planUpdateReviewRequirements = {
12101
12147
  name: "plan/update-review-requirements",
12102
12148
  kind: "plan.update_review_requirements",
12103
12149
  apply(e, state, ctx) {
12104
- 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
+ }
12105
12190
  killNodeReviews(state, e.nodeId, ctx.at);
12106
12191
  state.replaceRequirements(e.nodeId, e.reviewers.map((r) => ({
12107
12192
  nodeId: e.nodeId,
@@ -12112,6 +12197,9 @@ var init_plan = __esm({
12112
12197
  assignedAt: ctx.at,
12113
12198
  latestReviewId: null
12114
12199
  })));
12200
+ },
12201
+ async effect(e, ctx) {
12202
+ if (e.preserveReviews) await ctx.cancelKilledSessions(e.workorderId, { nodeId: e.nodeId, retained: e.reviewers });
12115
12203
  }
12116
12204
  };
12117
12205
  planNodeRetry = {
@@ -12626,7 +12714,7 @@ var init_work = __esm({
12626
12714
  workContentEdited = {
12627
12715
  name: "work/content-edited",
12628
12716
  kind: "work.content_edited",
12629
- apply(e, state) {
12717
+ apply(e, state, ctx) {
12630
12718
  const work = state.work(e.workId);
12631
12719
  if (!work) return;
12632
12720
  const node2 = state.node(work.nodeId);
@@ -12634,7 +12722,7 @@ var init_work = __esm({
12634
12722
  const artifacts = state.artifactsOf(e.workId);
12635
12723
  const target = artifacts.find((a) => a.id === e.artifactId);
12636
12724
  if (!target || target.contentRef !== e.previousRef || target.contentKind === "external") return;
12637
- state.replaceArtifacts(e.workId, artifacts.map((a) => a.id === target.id ? { ...a, contentRef: e.contentRef } : a));
12725
+ state.replaceArtifacts(e.workId, artifacts.map((a) => a.id === target.id ? { ...a, contentRef: e.contentRef, contentEditedAt: ctx.at } : a));
12638
12726
  }
12639
12727
  };
12640
12728
  workConclude = {
@@ -13199,6 +13287,8 @@ function versionCommandEvent(event) {
13199
13287
  return { ...event, semanticsVersion: 3 };
13200
13288
  }
13201
13289
  switch (event.kind) {
13290
+ case "plan.update_review_requirements":
13291
+ return { ...event, preserveReviews: true };
13202
13292
  case "work.accept":
13203
13293
  case "work.kill":
13204
13294
  case "work.handoff_recorded":
@@ -13764,7 +13854,7 @@ var init_bus = __esm({
13764
13854
  // ★ 设计 §workorder.sealed/paused「按 work.kill 逻辑杀」:killAllWorks 只置状态,会话取消在这里做——
13765
13855
  // apply 已把被杀 work/review 置 deadAt/cancelledAt,这里按 store 找它们仍挂着的会话 cancel
13766
13856
  //(deadAt 且 endedAt 未置 = 刚被杀未退出的在途会话)。没会话 / 已退 → no-op。
13767
- cancelKilledSessions: async () => {
13857
+ cancelKilledSessions: async (_workorderId, reviewScope) => {
13768
13858
  let snap;
13769
13859
  try {
13770
13860
  snap = await this.store.transaction((tx) => tx.loadWorkorder(record8.workorderId));
@@ -13780,10 +13870,13 @@ var init_bus = __esm({
13780
13870
  console.warn(`[bus] cancelKilledSessions ${what} ${id}: ${String(err)}`);
13781
13871
  }
13782
13872
  };
13783
- for (const w2 of snap.works) {
13873
+ for (const w2 of reviewScope === void 0 ? snap.works : []) {
13784
13874
  if (w2.deadAt && w2.endedAt === null) await cancel(w2.id, "work");
13785
13875
  }
13786
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;
13787
13880
  if (r.cancelledAt && r.endedAt === null) await cancel(r.id, "review");
13788
13881
  }
13789
13882
  },
@@ -16236,6 +16329,10 @@ var init_dispatcher = __esm({
16236
16329
  // 凭证是执行者属性,要人改(等待策略:熔断)
16237
16330
  [NO_BINDING_REASON]: "A",
16238
16331
  // 僵尸账号:只有人给它一个有效绑定才可能变
16332
+ // 绑到了**别家公司**的机器上(ADR-0164 §5 的租户闸)。与上一条同构:绑定本身就是错的,
16333
+ // 只有人改绑才可能变——而「改绑」正是 A 轴的第一条触发(executorSignature 含 nodeId),
16334
+ // 改完自动复位、不必再等一次人工干预。落 C 也不算错(安全默认),但会让人多做一步。
16335
+ "node-not-in-company": "A",
16239
16336
  "clean": "B",
16240
16337
  // 机器好好的、就是没交东西 → 换个东西给它做才有意义
16241
16338
  "timeout": "B",
@@ -21158,16 +21255,18 @@ var init_kernel_bridge = __esm({
21158
21255
  const artId = op.artifactId;
21159
21256
  const opReviewers = op.reviewers;
21160
21257
  const editReviewRoles = op.reviewRoles;
21258
+ const closureReviewers = (artifacts.get(artId)?.reviewers ?? []).filter((r) => r.source === "closure");
21161
21259
  if (artId && (opReviewers || editReviewRoles !== void 0)) {
21162
21260
  const art = artifacts.get(artId);
21163
21261
  const effectiveRoles = editReviewRoles !== void 0 ? editReviewRoles === null ? void 0 : editReviewRoles : art?.reviewRoles;
21164
- 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 }));
21165
21263
  const { reviewers: nextReqs, staffingIssues } = resolveNodeReviewers({
21166
21264
  typeName: art?.type ?? "",
21167
21265
  typeDef: art ? this.schema.get(art.type) : void 0,
21168
21266
  ...effectiveRoles !== void 0 ? { reviewRoles: effectiveRoles } : {},
21169
21267
  explicitReviewers: effectiveExplicit,
21170
- roleIndex: this.roleIndex
21268
+ roleIndex: this.roleIndex,
21269
+ closureGate: closureReviewers.map((r) => r.actor)
21171
21270
  });
21172
21271
  for (const issue2 of staffingIssues) {
21173
21272
  reviewerStaffing.push({ nodeId: artId, type: art?.type ?? "", role: issue2.role, from: issue2.from });
@@ -21226,7 +21325,7 @@ var init_kernel_bridge = __esm({
21226
21325
  ...opDescription !== void 0 ? { description: opDescription } : {},
21227
21326
  ...fields ? { fields: { ...previous3.fields, ...fields } } : {},
21228
21327
  ...editReviewRoles !== void 0 ? { reviewRoles: editReviewRoles ?? void 0 } : {},
21229
- ...opReviewers ? { reviewers: opReviewers } : {}
21328
+ ...opReviewers ? { reviewers: [...opReviewers, ...closureReviewers] } : {}
21230
21329
  });
21231
21330
  applied++;
21232
21331
  break;
@@ -28739,9 +28838,22 @@ var init_chat_item_ledger = __esm({
28739
28838
  ...deterministicId ? { deterministicId } : {}
28740
28839
  });
28741
28840
  }
28742
- /** 服务端合成的错误帧(session.done 抛 / exit 判失败)。不入正文,归 control。 */
28841
+ /**
28842
+ * 服务端合成的错误帧(session.done 抛 / exit 判失败)。不入正文,归 control。
28843
+ *
28844
+ * **返回 outcome 是为了让调用方把它推上 v3 流**(ADR-0510 D5/D6,2026-09-10 修)。
28845
+ * 在此之前这条行只落库、不发帧:客户端在这一轮里唯一收到的终态信号是 `emitV3TurnTerminal`
28846
+ * 发的 `turn_failed`,而那一帧不带任何正文、客户端的 `applyFrame` 又明说「不建 item、
28847
+ * 仅推进 frameSeq」——于是失败原因整条飞不到还开着的页面上,只有下一次 `/items` 快照
28848
+ * 才补得上。生产现场(v2.openfriday.ai,2026-09-10 05:39 那一轮):runtime 报
28849
+ * 「401 Authentication Fails」,库里三处都记着,页面上从头到尾是空的。
28850
+ *
28851
+ * 号取的是**建行时那两格钟**(`startedVersion` 给 wire 的 `started` 帧、`version` 给这一行
28852
+ * 本身的写),与 provider item 走 {@link ChatItemLedger.apply} 那条路逐字同构——
28853
+ * 快照与增量流因此还是同一套版本空间。
28854
+ */
28743
28855
  recordError(text5, extra) {
28744
- this.insert({
28856
+ const id = this.insert({
28745
28857
  kind: "control",
28746
28858
  role: "assistant",
28747
28859
  status: "failed",
@@ -28757,6 +28869,8 @@ var init_chat_item_ledger = __esm({
28757
28869
  ...extra?.detail ? { detail: extra.detail } : {}
28758
28870
  }
28759
28871
  });
28872
+ const row = this.rows.get(id);
28873
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
28760
28874
  }
28761
28875
  /**
28762
28876
  * 回灌重启前已经落库的那段正文(`chat-recovery` 重挂 / 迟到对账用)。
@@ -29272,6 +29386,8 @@ function runBackgroundAssistantChatTurn(deps) {
29272
29386
  ...assistantMsgId ? { messageId: assistantMsgId } : {},
29273
29387
  log: log3
29274
29388
  });
29389
+ const liveTurn = deps.liveChat && deps.liveHandle ? deps.liveChat.start(deps.chatSessionId, deps.liveHandle, { items: itemLedger }) : void 0;
29390
+ if (assistantMsgId) liveTurn?.setAssistantMessageId(assistantMsgId);
29275
29391
  const normalizedSource = typeof deps.session.onNormalizedProviderEvent === "function" ? {
29276
29392
  onNormalizedProviderEvent: deps.session.onNormalizedProviderEvent.bind(deps.session),
29277
29393
  finishTurn: (signal) => deps.session.finishNormalizedTurn?.(signal)
@@ -29297,7 +29413,8 @@ function runBackgroundAssistantChatTurn(deps) {
29297
29413
  let ckptDirty = false;
29298
29414
  normalizedSource.onNormalizedProviderEvent((event) => {
29299
29415
  try {
29300
- itemLedger.apply(event);
29416
+ if (liveTurn) liveTurn.applyNormalizedEvent(event);
29417
+ else itemLedger.apply(event);
29301
29418
  } catch {
29302
29419
  }
29303
29420
  ckptDirty = true;
@@ -29335,34 +29452,59 @@ function runBackgroundAssistantChatTurn(deps) {
29335
29452
  }
29336
29453
  const finalize = async (status) => {
29337
29454
  if (ckptTimer) clearInterval(ckptTimer);
29338
- await checkpointChain.catch(() => void 0);
29339
- normalizedSource.finishTurn(status === "done" ? "completed" : "failed");
29340
- itemLedger.finish(status === "done" ? "completed" : "failed");
29341
- await itemLedger.drain();
29342
- const projectedContent = itemLedger.projectContent();
29343
- const parts = collectedParts();
29344
- await finalizeAssistantRow({
29345
- chatStore: deps.chatStore,
29346
- chatSessionId: deps.chatSessionId,
29347
- assistantMsgId,
29348
- content: projectedContent,
29349
- parts,
29350
- status,
29351
- ...runId ? { runId } : {},
29352
- now
29353
- });
29354
- if (typeof deps.chatStore.updateSession === "function") {
29355
- try {
29356
- await deps.chatStore.updateSession(deps.chatSessionId, {
29357
- runtimeSessionId: deps.session.nativeSessionId ?? null,
29358
- touchedAt: now()
29455
+ try {
29456
+ await checkpointChain.catch(() => void 0);
29457
+ normalizedSource.finishTurn(status === "done" ? "completed" : "failed");
29458
+ itemLedger.finish(status === "done" ? "completed" : "failed");
29459
+ await itemLedger.drain();
29460
+ const projectedContent = itemLedger.projectContent();
29461
+ const parts = collectedParts();
29462
+ await finalizeAssistantRow({
29463
+ chatStore: deps.chatStore,
29464
+ chatSessionId: deps.chatSessionId,
29465
+ assistantMsgId,
29466
+ content: projectedContent,
29467
+ parts,
29468
+ status,
29469
+ ...runId ? { runId } : {},
29470
+ now
29471
+ });
29472
+ if (typeof deps.chatStore.updateSession === "function") {
29473
+ try {
29474
+ await deps.chatStore.updateSession(deps.chatSessionId, {
29475
+ runtimeSessionId: deps.session.nativeSessionId ?? null,
29476
+ touchedAt: now()
29477
+ });
29478
+ } catch {
29479
+ }
29480
+ }
29481
+ if (liveTurn) {
29482
+ if (projectedContent) liveTurn.emit({ type: "text", text: projectedContent });
29483
+ liveTurn.emitLive({
29484
+ protocolVersion: 2,
29485
+ streamId: `background:${deps.chatSessionId}`,
29486
+ turnId: deps.turnId ?? fallbackTurnId(runId),
29487
+ itemId: `background:${assistantMsgId ?? runId}`,
29488
+ itemType: "message",
29489
+ operation: "completed",
29490
+ payload: { role: "assistant", text: projectedContent }
29359
29491
  });
29492
+ }
29493
+ try {
29494
+ deps.onSettled?.();
29360
29495
  } catch {
29361
29496
  }
29362
- }
29363
- try {
29364
- deps.onSettled?.();
29365
- } catch {
29497
+ } finally {
29498
+ liveTurn?.emit({ type: "done" });
29499
+ liveTurn?.emitLive({
29500
+ protocolVersion: 2,
29501
+ streamId: `background:${deps.chatSessionId}`,
29502
+ turnId: deps.turnId ?? fallbackTurnId(runId),
29503
+ itemId: "background:terminal",
29504
+ itemType: "control",
29505
+ operation: status === "done" ? "turn_completed" : "turn_failed"
29506
+ });
29507
+ liveTurn?.finish(status);
29366
29508
  }
29367
29509
  };
29368
29510
  void Promise.resolve(deps.session.done).then(() => finalize("done").catch(() => void 0)).catch((err) => {
@@ -142281,71 +142423,6 @@ var init_collect = __esm({
142281
142423
  }
142282
142424
  });
142283
142425
 
142284
- // ../server/src/domains/projects/ephemeral-project.ts
142285
- function isEphemeralProject(projectId2) {
142286
- return projectId2.startsWith(EPHEMERAL_PROJECT_PREFIX);
142287
- }
142288
- function isUncategorizedProjectId(projectId2) {
142289
- return projectId2 === UNCATEGORIZED_PROJECT_ID;
142290
- }
142291
- function ephemeralProjectIdFor(workOrderId) {
142292
- return `${EPHEMERAL_PROJECT_PREFIX}${workOrderId.replace(/[^a-zA-Z0-9_-]+/g, "_")}`;
142293
- }
142294
- function ephemeralProjectNameFor(workOrderId, title) {
142295
- return `\u4E34\u65F6\u9879\u76EE \xB7 ${title?.trim() || workOrderId}`;
142296
- }
142297
- async function ensureWorkorderProject(deps) {
142298
- const { workOrderId, projectId: projectId2, title, createdBy } = deps;
142299
- try {
142300
- if (projectId2) {
142301
- await deps.upsertBinding({ workOrderId, projectId: projectId2 });
142302
- return projectId2;
142303
- }
142304
- const id = ephemeralProjectIdFor(workOrderId);
142305
- await deps.createProject({
142306
- id,
142307
- name: ephemeralProjectNameFor(workOrderId, title),
142308
- 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`,
142309
- ...createdBy ? { createdBy } : {}
142310
- });
142311
- await deps.upsertBinding({ workOrderId, projectId: id });
142312
- return id;
142313
- } catch (e) {
142314
- deps.onWarn?.(`[project] \u5DE5\u5355 ${workOrderId} \u7ED1\u5B9A\u9879\u76EE\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u5EFA\u5355\uFF09\uFF1A${String(e)}`);
142315
- return null;
142316
- }
142317
- }
142318
- function makeEnsureProjectFromStore(store) {
142319
- return async (input) => {
142320
- const existing = await store.getProject(input.id);
142321
- if (existing) return { id: existing.id };
142322
- const now = (/* @__PURE__ */ new Date()).toISOString();
142323
- const createdById = input.createdBy?.trim();
142324
- const createdBy = createdById && (createdById.startsWith("actor:human:") || createdById.startsWith("actor:agent:")) ? { id: createdById, kind: projectActorKind(createdById), name: null, avatar: null } : void 0;
142325
- await store.upsertProject({
142326
- id: input.id,
142327
- name: input.name,
142328
- ...input.description !== void 0 ? { description: input.description } : {},
142329
- ...createdBy ? { createdBy } : {},
142330
- createdAt: now,
142331
- updatedAt: now
142332
- });
142333
- return { id: input.id };
142334
- };
142335
- }
142336
- var EPHEMERAL_PROJECT_PREFIX, UNCATEGORIZED_PROJECT_ID, UNCATEGORIZED_PROJECT_NAME, UNCATEGORIZED_PROJECT_DESCRIPTION, UNCATEGORIZED_UNKNOWN_TIME;
142337
- var init_ephemeral_project = __esm({
142338
- "../server/src/domains/projects/ephemeral-project.ts"() {
142339
- "use strict";
142340
- init_src();
142341
- EPHEMERAL_PROJECT_PREFIX = "proj_tmp_";
142342
- UNCATEGORIZED_PROJECT_ID = "proj:uncategorized";
142343
- UNCATEGORIZED_PROJECT_NAME = "\u65E0\u5206\u7C7B\u9879\u76EE";
142344
- 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";
142345
- UNCATEGORIZED_UNKNOWN_TIME = "1970-01-01T00:00:00.000Z";
142346
- }
142347
- });
142348
-
142349
142426
  // ../server/src/domains/collab/timeline.ts
142350
142427
  function buildNodeTimeline(artifactId, snap, runIdByTarget) {
142351
142428
  if (!snap) return { artifactId, items: [] };
@@ -159766,8 +159843,9 @@ var init_live_chat = __esm({
159766
159843
  DEFAULT_LIVE_COALESCE_MS = 24;
159767
159844
  DEFAULT_LIVE_COALESCE_MAX_CHUNKS = 32;
159768
159845
  DEFAULT_LIVE_COALESCE_MAX_CHARS = 64 * 1024;
159769
- LiveChatRegistry = class {
159846
+ LiveChatRegistry = class _LiveChatRegistry {
159770
159847
  turns = /* @__PURE__ */ new Map();
159848
+ turnStartWaiters = /* @__PURE__ */ new Map();
159771
159849
  epochs = /* @__PURE__ */ new Map();
159772
159850
  bufferMax;
159773
159851
  graceMs;
@@ -159813,6 +159891,15 @@ var init_live_chat = __esm({
159813
159891
  v3Items: /* @__PURE__ */ new Map()
159814
159892
  };
159815
159893
  this.turns.set(chatSessionId, turn);
159894
+ this.flushPendingUserItems(chatSessionId);
159895
+ const waiters = this.turnStartWaiters.get(chatSessionId);
159896
+ this.turnStartWaiters.delete(chatSessionId);
159897
+ for (const wake of waiters ?? []) {
159898
+ try {
159899
+ wake();
159900
+ } catch {
159901
+ }
159902
+ }
159816
159903
  return {
159817
159904
  runtimeSessionId: turn.runtimeSessionId,
159818
159905
  epoch: turn.liveEpoch,
@@ -159835,6 +159922,7 @@ var init_live_chat = __esm({
159835
159922
  },
159836
159923
  emitLive: (event) => this.emitLive(turn, event),
159837
159924
  applyNormalizedEvent: (event) => this.applyNormalizedEvent(turn, event),
159925
+ publishServerItem: (item) => this.publishServerItem(turn, item),
159838
159926
  subscribe: (sub) => {
159839
159927
  turn.subscribers.add(sub);
159840
159928
  return () => {
@@ -159884,6 +159972,26 @@ var init_live_chat = __esm({
159884
159972
  ...opts.items ? { items: opts.items } : {}
159885
159973
  };
159886
159974
  }
159975
+ /** 只通知开轮,不承载内容。先检查再登记均为同步,避免空闲检查与订阅之间漏掉开轮。 */
159976
+ onNextTurn(chatSessionId, wake) {
159977
+ if (this.isRunning(chatSessionId)) {
159978
+ wake();
159979
+ return () => {
159980
+ };
159981
+ }
159982
+ let waiters = this.turnStartWaiters.get(chatSessionId);
159983
+ if (!waiters) {
159984
+ waiters = /* @__PURE__ */ new Set();
159985
+ this.turnStartWaiters.set(chatSessionId, waiters);
159986
+ }
159987
+ waiters.add(wake);
159988
+ return () => {
159989
+ waiters.delete(wake);
159990
+ if (waiters.size === 0 && this.turnStartWaiters.get(chatSessionId) === waiters) {
159991
+ this.turnStartWaiters.delete(chatSessionId);
159992
+ }
159993
+ };
159994
+ }
159887
159995
  /**
159888
159996
  * 归一层中间事件 → 带 `itemId` 的 item op(ADR D4 第 2 跳)+ v3 帧 dual-emit(S3)。
159889
159997
  *
@@ -159981,6 +160089,45 @@ var init_live_chat = __esm({
159981
160089
  };
159982
160090
  this.publishV3Coalesced(turn, frame);
159983
160091
  }
160092
+ /**
160093
+ * 服务端合成 item → 两帧(`started` + 终态),与 {@link emitV3} 给 provider item 发的那两帧
160094
+ * **逐字同构**:先用建行预留的那一格发 `started`(客户端以它为该 item 首次亮相、payload 首次成形),
160095
+ * 再用这一行本身的写号发 `completed` / `failed`。
160096
+ *
160097
+ * 为什么不学 {@link publishUserItem} 用一帧 `set_text`:那条路是给「一次性成形的**正文**」用的,
160098
+ * 落在客户端桶里 `status` 恒为 `streaming`——用户那条消息无所谓,失败收尾的 control 行却必须是
160099
+ * 终态,否则这一轮在客户端看来永远没关。
160100
+ */
160101
+ publishServerItem(turn, item) {
160102
+ if (turn.status !== "running") return;
160103
+ const perItem = turn.v3Items.get(item.itemId) ?? { textOffset: 0, started: false };
160104
+ const ord = turn.items?.ordFor(item.itemId) ?? item.ord ?? void 0;
160105
+ const base = {
160106
+ protocolVersion: 3,
160107
+ streamId: turn.liveStreamId,
160108
+ turnId: this.wireTurnId(turn),
160109
+ itemId: item.itemId,
160110
+ itemType: item.itemType,
160111
+ ...ord !== void 0 ? { ord } : {},
160112
+ offset: 0
160113
+ };
160114
+ if (!perItem.started && item.startedVersion !== void 0) {
160115
+ this.publishV3Coalesced(turn, {
160116
+ ...base,
160117
+ operation: "started",
160118
+ itemVersion: item.startedVersion,
160119
+ payload: item.payload
160120
+ });
160121
+ perItem.started = true;
160122
+ }
160123
+ turn.v3Items.set(item.itemId, perItem);
160124
+ this.publishV3Coalesced(turn, {
160125
+ ...base,
160126
+ operation: item.status === "failed" ? "failed" : "completed",
160127
+ itemVersion: item.version,
160128
+ payload: item.payload
160129
+ });
160130
+ }
159984
160131
  assignV3Seq(turn, frame) {
159985
160132
  return { ...frame, epoch: turn.liveEpoch, seq: ++turn.v3Seq };
159986
160133
  }
@@ -160072,9 +160219,34 @@ var init_live_chat = __esm({
160072
160219
  * (`server.ts` 里 `recordUserSubmission` 在 `liveChat.start` 之前)。拿不到就不发——
160073
160220
  * 前端退回「等下一次快照」,与修复前一致,不会更坏。
160074
160221
  */
160222
+ /**
160223
+ * 「user item 已落库、但这条会话的 live 轮还没 start」时的暂存位(见 {@link publishUserItem})。
160224
+ *
160225
+ * 每会话只留最近几条:它的唯一用途是补发**当前这一次提交**,不是可靠队列。留太多等于把
160226
+ * 一条早就过期的 user 帧在下一轮开头推出去,那比丢了更糟(用户会看到上一轮的话冒出来)。
160227
+ */
160228
+ pendingUserItems = /* @__PURE__ */ new Map();
160229
+ /** 单会话暂存上限。正常只会有 1 条(一次提交一条);给到 4 是为了并发提交时不至于丢。 */
160230
+ static PENDING_USER_ITEM_MAX = 4;
160231
+ bufferUserItem(chatSessionId, item) {
160232
+ const list2 = this.pendingUserItems.get(chatSessionId) ?? [];
160233
+ list2.push(item);
160234
+ while (list2.length > _LiveChatRegistry.PENDING_USER_ITEM_MAX) list2.shift();
160235
+ this.pendingUserItems.set(chatSessionId, list2);
160236
+ }
160237
+ /** `start` 之后立刻调:把暂存的 user item 补发出去。发不出去就丢掉,不再往下一轮带。 */
160238
+ flushPendingUserItems(chatSessionId) {
160239
+ const list2 = this.pendingUserItems.get(chatSessionId);
160240
+ if (!list2?.length) return;
160241
+ this.pendingUserItems.delete(chatSessionId);
160242
+ for (const item of list2) this.publishUserItem(chatSessionId, item);
160243
+ }
160075
160244
  publishUserItem(chatSessionId, item) {
160076
160245
  const turn = this.turns.get(chatSessionId);
160077
- if (!turn) return false;
160246
+ if (!turn) {
160247
+ this.bufferUserItem(chatSessionId, item);
160248
+ return false;
160249
+ }
160078
160250
  const payload = { role: "user", text: item.text };
160079
160251
  if (item.clientSubmitId) payload.clientSubmitId = item.clientSubmitId;
160080
160252
  if (item.attachments?.length) payload.attachments = item.attachments;
@@ -160118,11 +160290,17 @@ var init_live_chat = __esm({
160118
160290
  const frame = {
160119
160291
  protocolVersion: 3,
160120
160292
  streamId: turn.liveStreamId,
160121
- turnId: turn.liveTurnId,
160293
+ // 账本轮次 id,与 item 帧同一套(#1249 把 emitV3 改成了 wireTurnId,收尾帧当时漏了)。
160294
+ // 客户端此前把收尾帧整条丢弃(`applyFrame`:「不建 item,仅推进 frameSeq」),所以这个
160295
+ // 号是对是错都看不出来;相位一挂上去它就得对——否则相位会记到一个桶里根本不存在的 turnId 上。
160296
+ turnId: this.wireTurnId(turn),
160122
160297
  itemId: `oasis-turn:${turn.liveEpoch}:${status}`,
160123
160298
  itemType: "control",
160124
160299
  operation,
160125
160300
  itemVersion: 0,
160301
+ // 相位被携带,不被推导(ADR-0510 D6):这一轮到底是成了还是败了,服务端此刻知道,
160302
+ // 客户端替换即可,不用再去把一轮里每条 item 的 status 取或来猜。
160303
+ phase: status === "done" ? "succeeded" : "failed",
160126
160304
  epoch: turn.liveEpoch,
160127
160305
  seq: ++turn.v3Seq
160128
160306
  };
@@ -160489,6 +160667,40 @@ var init_live_chat = __esm({
160489
160667
  }
160490
160668
  });
160491
160669
 
160670
+ // ../server/src/chat-turn-wait.ts
160671
+ function waitForChatTurn(res, liveChat, sessionId, startKeepalive, ready = false) {
160672
+ res.writeHead(200, {
160673
+ "content-type": "application/x-oasis-live-v3+ndjson; charset=utf-8",
160674
+ "cache-control": "no-cache, no-transform",
160675
+ "x-accel-buffering": "no",
160676
+ "access-control-expose-headers": "X-Chat-Session-Watch, X-Chat-Waiting, X-Can-Append",
160677
+ "x-chat-session-watch": "1",
160678
+ "x-chat-waiting": "1",
160679
+ "x-can-append": "0"
160680
+ });
160681
+ res.flushHeaders();
160682
+ const stopKeepalive = startKeepalive(res);
160683
+ let detach = () => {
160684
+ };
160685
+ const cleanup = () => {
160686
+ stopKeepalive();
160687
+ detach();
160688
+ };
160689
+ res.once("close", cleanup);
160690
+ const wake = () => {
160691
+ cleanup();
160692
+ if (!res.destroyed) res.end('{"type":"turn_available"}\n');
160693
+ };
160694
+ if (ready) wake();
160695
+ else detach = liveChat.onNextTurn(sessionId, wake);
160696
+ if (res.destroyed) cleanup();
160697
+ }
160698
+ var init_chat_turn_wait = __esm({
160699
+ "../server/src/chat-turn-wait.ts"() {
160700
+ "use strict";
160701
+ }
160702
+ });
160703
+
160492
160704
  // ../server/src/chat-attachment-files.ts
160493
160705
  function safeAttachmentName(raw) {
160494
160706
  return (raw || "attachment").replace(/[^\w.\-]+/g, "_").replace(/^\.+/, "").slice(0, 80) || "attachment";
@@ -160686,13 +160898,24 @@ var init_closure_report = __esm({
160686
160898
  });
160687
160899
 
160688
160900
  // ../server/src/governance/workorder-drafts.ts
160689
- var import_node_fs5, DRAFT_EDIT_LOCK_TTL_MS, WorkorderDraftStore, ident, seqNoOf, PostgresWorkorderDraftStore;
160901
+ var import_node_fs5, DRAFT_EDIT_LOCK_TTL_MS, WorkorderDraftTagMismatchError, WorkorderDraftStore, ident, seqNoOf, PostgresWorkorderDraftStore;
160690
160902
  var init_workorder_drafts = __esm({
160691
160903
  "../server/src/governance/workorder-drafts.ts"() {
160692
160904
  "use strict";
160693
160905
  import_node_fs5 = __toESM(require("node:fs"), 1);
160694
160906
  init_closure_report();
160695
160907
  DRAFT_EDIT_LOCK_TTL_MS = 12e4;
160908
+ WorkorderDraftTagMismatchError = class extends Error {
160909
+ constructor(draftId, expectedWorkspace, actualWorkspace) {
160910
+ super(
160911
+ actualWorkspace === void 0 ? `\u5EFA\u5355\u8349\u6848 ${draftId} \u4E0D\u5728\u672C store\uFF08\u5F88\u53EF\u80FD\u5EFA\u5728\u53E6\u4E00\u4E2A schema \u7684\u5E93\u91CC\uFF09\u2014\u2014\u62D2\u7EDD\u6253\u6807` : `\u5EFA\u5355\u8349\u6848 ${draftId} \u7684 workspace \u662F ${actualWorkspace}\uFF0C\u4E0E\u672C\u6B21\u5EFA\u5355\u7684 ${String(expectedWorkspace)} \u4E0D\u7B26\u2014\u2014\u62D2\u7EDD\u6253\u6807\uFF08\u8DE8\u5E93\u649E\u53F7\u4F1A\u6539\u7ED1\u522B\u4EBA\u7684\u8349\u6848\uFF09`
160912
+ );
160913
+ this.draftId = draftId;
160914
+ this.expectedWorkspace = expectedWorkspace;
160915
+ this.actualWorkspace = actualWorkspace;
160916
+ this.name = "WorkorderDraftTagMismatchError";
160917
+ }
160918
+ };
160696
160919
  WorkorderDraftStore = class {
160697
160920
  constructor(file) {
160698
160921
  this.file = file;
@@ -160796,14 +161019,28 @@ var init_workorder_drafts = __esm({
160796
161019
  }
160797
161020
  /** 给草案打上 chat 会话标(建单经 chat 路时,/api/cmd 拿到 X-Chat-Session-Id 后调)。
160798
161021
  * 同时(若拿得到)盖上产出它的那一轮 runId,供 friday 重进对话页把该版草案归位到那条助手消息。
160799
- * sourceRunId 一旦盖上不再改(同一版草案就属于那一轮)。 */
160800
- async tagSession(draftId, chatSessionId, sourceRunId) {
161022
+ * sourceRunId 一旦盖上不再改(同一版草案就属于那一轮)。
161023
+ *
161024
+ * **`expectedWorkspace` 是防跨库改绑的那道闸(2026-09-10 线上事故)**:草案 id 形如
161025
+ * `wodraft:<n>`,而**每个 schema 各有一台发号器**(共享库一台、每家公司各一台,见
161026
+ * `PostgresWorkorderDraftStore.open` 建的 `workorder_draft_seq`)。于是「共享库的 70 号」和
161027
+ * 「didi 库的 70 号」是两份毫不相干的草案。一旦建草案与打标落在**不同的 store**上
161028
+ * (公司上下文解析不出来 → 建落共享库;打标按公司解析 → 打进公司库),这里就会拿着同一个
161029
+ * 号在另一个库里翻出**别人的草案**,把它的 chatSessionId / sourceRunId 覆盖成本次会话——
161030
+ * 别人的建单卡就这样出现在你的对话里,而原归属被覆盖后再也找不回来。
161031
+ *
161032
+ * 所以调用方**必须**把「我刚建的那张单的 workspace」一起传进来:对不上就抛,绝不写。
161033
+ * 查不到 id 同理——旧实现在这里是 `if (d) {}` 的静默 no-op,那正是这次事故连一行日志都没留下的原因。
161034
+ * 抛出的是 {@link WorkorderDraftTagMismatchError},调用方该**记账后继续**(草案本身已经建好,
161035
+ * 不该因为打标失败把整个建单回执变成失败),见 `/api/cmd` 与 playbooks 两处调用点。 */
161036
+ async tagSession(draftId, chatSessionId, sourceRunId, expectedWorkspace) {
160801
161037
  const d = this.drafts.get(draftId);
160802
- if (d) {
160803
- d.chatSessionId = chatSessionId;
160804
- if (sourceRunId && !d.sourceRunId) d.sourceRunId = sourceRunId;
160805
- await this.persist({ kind: "upsert", draft: d });
161038
+ if (!d || expectedWorkspace !== void 0 && d.workspace !== expectedWorkspace) {
161039
+ throw new WorkorderDraftTagMismatchError(draftId, expectedWorkspace, d?.workspace);
160806
161040
  }
161041
+ d.chatSessionId = chatSessionId;
161042
+ if (sourceRunId && !d.sourceRunId) d.sourceRunId = sourceRunId;
161043
+ await this.persist({ kind: "upsert", draft: d });
160807
161044
  }
160808
161045
  /** 草案被就地编辑后(editDraftPlan 改了 plan.ops)调用:bump `at` 让 friday 重载据此察觉,
160809
161046
  * 并**落盘**——否则编辑过的草案在 apply 前进程重启就丢改动(草案落盘的初衷正是扛重启)。 */
@@ -161596,9 +161833,14 @@ var init_assistants = __esm({
161596
161833
  await this.opts.actors.disableActor(agentId, by).catch(() => {
161597
161834
  });
161598
161835
  }
161836
+ /** 本服务的公司 scope;未启用公司维度(单租户装配/单测)时 undefined = 不过滤,行为不变。 */
161837
+ scope() {
161838
+ const companyId = this.opts.companyId();
161839
+ return companyId ? { companyId } : void 0;
161840
+ }
161599
161841
  /* ---------- runtime 挑选(ADR §3.3.a):负载均衡 + 幂等 tie-break ---------- */
161600
161842
  async pickAssistantRuntime() {
161601
- const runtimes = (await this.opts.listRuntimes()).filter((r) => r.status === "online");
161843
+ const runtimes = (await this.opts.listRuntimes(void 0, this.scope())).filter((r) => r.status === "online");
161602
161844
  if (runtimes.length === 0) return null;
161603
161845
  let defaultKind = DEFAULT_ASSISTANT_RUNTIME_KIND;
161604
161846
  for (const r of runtimes) {
@@ -161625,7 +161867,7 @@ var init_assistants = __esm({
161625
161867
  if (!actor.roles.includes("assistant")) return false;
161626
161868
  const binding = await this.opts.registry.getBinding(agentId);
161627
161869
  if (!binding || binding.status !== "active") return false;
161628
- const runtime = (await this.opts.listRuntimes(binding.nodeId)).find((r) => r.kind === binding.runtimeKind);
161870
+ const runtime = (await this.opts.listRuntimes(binding.nodeId, this.scope())).find((r) => r.kind === binding.runtimeKind);
161629
161871
  if (!runtime || runtime.status !== "online") return false;
161630
161872
  return true;
161631
161873
  }
@@ -162356,14 +162598,25 @@ var init_planner = __esm({
162356
162598
  function systemActorFallback(id) {
162357
162599
  return isSystemActorId(id) ? { name: SYSTEM_ACTOR_DISPLAY_NAME, avatar: SYSTEM_AVATAR_REF } : void 0;
162358
162600
  }
162601
+ function defaultAvatarRefOf(id, kind) {
162602
+ if (kind === "system" || isSystemActorId(id)) return SYSTEM_AVATAR_REF;
162603
+ if (kind === "human" || isHumanActorId(id)) return defaultHumanAvatarRef(id);
162604
+ return void 0;
162605
+ }
162359
162606
  function actorRefFieldsOf(a) {
162360
- const avatar = a.avatar ?? (a.kind === "system" ? SYSTEM_AVATAR_REF : void 0);
162607
+ const avatar = a.avatar ?? defaultAvatarRefOf(a.id, a.kind);
162361
162608
  return {
162362
162609
  name: a.name,
162363
162610
  ...a.roles[0] ? { role: a.roles[0] } : {},
162364
162611
  ...avatar ? { avatar } : {}
162365
162612
  };
162366
162613
  }
162614
+ function actorRefFallbackOf(id) {
162615
+ const system = systemActorFallback(id);
162616
+ if (system) return system;
162617
+ const avatar = defaultAvatarRefOf(id);
162618
+ return avatar ? { avatar } : void 0;
162619
+ }
162367
162620
  function overviewRootIdOf(arts) {
162368
162621
  const roots = arts.filter((a) => a.inputs.length === 0);
162369
162622
  if (roots.length <= 1) return roots[0]?.id;
@@ -186647,12 +186900,13 @@ var init_governance_service = __esm({
186647
186900
  const binding = await this.options.projects.getWorkspaceBinding(ctx.organizationId, workorderId);
186648
186901
  if (!binding) {
186649
186902
  if (claimedProjectId) throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_BINDING_MISSING", "\u5DE5\u5355\u6CA1\u6709\u53EF\u4FE1\u9879\u76EE\u7ED1\u5B9A");
186903
+ await this.requireOrganizationActor(ctx);
186650
186904
  return void 0;
186651
186905
  }
186652
186906
  if (claimedProjectId && binding.projectId !== claimedProjectId) {
186653
186907
  throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_MISMATCH", "\u8BF7\u6C42\u9879\u76EE\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u4E0D\u4E00\u81F4");
186654
186908
  }
186655
- if (this.options.projects.isEphemeralProject(ctx.organizationId, binding.projectId)) {
186909
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, binding.projectId)) {
186656
186910
  await this.requireOrganizationActor(ctx);
186657
186911
  return void 0;
186658
186912
  }
@@ -186860,8 +187114,8 @@ var init_governance_service = __esm({
186860
187114
  const projectId2 = input.scopeType === "project" ? input.scopeId : void 0;
186861
187115
  if (input.scopeType === "project") {
186862
187116
  if (!projectId2) throw new KnowledgeGovernanceError(400, "KNOWLEDGE_PROJECT_REQUIRED", "\u9879\u76EE Space \u5FC5\u987B\u6307\u5B9A\u9879\u76EE");
186863
- if (this.options.projects.isEphemeralProject(ctx.organizationId, projectId2)) {
186864
- throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u4E34\u65F6\u9879\u76EE\u4E0D\u521B\u5EFA\u77E5\u8BC6 Space");
187117
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) {
187118
+ throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u8BE5\u9879\u76EE\u4E0D\u652F\u6301\u72EC\u7ACB\u77E5\u8BC6 Space");
186865
187119
  }
186866
187120
  await this.authorizeProjectResource(ctx, projectId2, "configure");
186867
187121
  } else if (input.scopeId) {
@@ -186987,7 +187241,7 @@ var init_governance_service = __esm({
186987
187241
  async reconfigureKnowledgeTree(ctx, store, organizationSpace, connection, _config) {
186988
187242
  const projectEntries = [];
186989
187243
  for (const project of await this.options.projects.listProjects(ctx.organizationId)) {
186990
- if (this.options.projects.isEphemeralProject(ctx.organizationId, project.id)) continue;
187244
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, project.id)) continue;
186991
187245
  const projectSpace = (await store.list(this.projectScope(ctx, project.id), "space")).filter((space) => space.status !== "archived").sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
186992
187246
  if (projectSpace) projectEntries.push({ project, space: projectSpace });
186993
187247
  }
@@ -187206,8 +187460,8 @@ var init_governance_service = __esm({
187206
187460
  async resolveRunTarget(ctx, projectId2) {
187207
187461
  await this.requireOrganizationActor(ctx);
187208
187462
  if (projectId2) {
187209
- if (this.options.projects.isEphemeralProject(ctx.organizationId, projectId2)) {
187210
- throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u4E34\u65F6\u9879\u76EE\u6CA1\u6709\u72EC\u7ACB\u9879\u76EE\u77E5\u8BC6\u5E93\uFF0C\u8BF7\u9009\u62E9\u7EC4\u7EC7\u77E5\u8BC6\u5E93");
187463
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) {
187464
+ throw new KnowledgeGovernanceError(409, "KNOWLEDGE_EPHEMERAL_PROJECT", "\u8BE5\u9879\u76EE\u6CA1\u6709\u72EC\u7ACB\u9879\u76EE\u77E5\u8BC6\u5E93\uFF0C\u8BF7\u9009\u62E9\u7EC4\u7EC7\u77E5\u8BC6\u5E93");
187211
187465
  }
187212
187466
  await this.authorizeProjectResource(ctx, projectId2, "publish");
187213
187467
  }
@@ -187423,7 +187677,7 @@ var init_governance_service = __esm({
187423
187677
  if (actor.kind !== "system") await this.requireOrganizationAdmin(ctx);
187424
187678
  const project = await this.options.projects.getProject(ctx.organizationId, projectId2);
187425
187679
  if (!project) throw new KnowledgeGovernanceError(404, "KNOWLEDGE_PROJECT_NOT_FOUND", "\u9879\u76EE\u4E0D\u5B58\u5728");
187426
- if (this.options.projects.isEphemeralProject(ctx.organizationId, projectId2)) return null;
187680
+ if (this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2)) return null;
187427
187681
  const store = await this.options.storeFor(ctx.organizationId);
187428
187682
  const scope = this.projectScope(ctx, projectId2);
187429
187683
  const existingSpaces = await store.list(scope, "space");
@@ -190123,7 +190377,7 @@ var init_governance_service = __esm({
190123
190377
  if (event.claimedProjectId !== trusted.projectId) {
190124
190378
  throw new KnowledgeGovernanceError(409, "KNOWLEDGE_PROJECT_MISMATCH", "\u8BF7\u6C42\u9879\u76EE\u4E0E\u53EF\u4FE1\u6765\u6E90\u5F52\u5C5E\u4E0D\u4E00\u81F4");
190125
190379
  }
190126
- const targetProjectId = trusted.projectId && !this.options.projects.isEphemeralProject(ctx.organizationId, trusted.projectId) ? trusted.projectId : void 0;
190380
+ const targetProjectId = trusted.projectId && !this.options.projects.isProjectScopeUnsupported(ctx.organizationId, trusted.projectId) ? trusted.projectId : void 0;
190127
190381
  const discoveryActor = await this.requireActiveActor(ctx.actorId, ctx.organizationId);
190128
190382
  if (discoveryActor.kind !== "system") await this.requireOrganizationActor(ctx);
190129
190383
  if (targetProjectId) {
@@ -190389,7 +190643,7 @@ var init_governance_service = __esm({
190389
190643
  runs.push(...await this.enqueueDiscoveryEvent(ctx, event, runtime ?? void 0));
190390
190644
  } catch (error2) {
190391
190645
  const store = await this.options.storeFor(ctx.organizationId);
190392
- const taskProjectId = projectId2 && !this.options.projects.isEphemeralProject(ctx.organizationId, projectId2) ? projectId2 : void 0;
190646
+ const taskProjectId = projectId2 && !this.options.projects.isProjectScopeUnsupported(ctx.organizationId, projectId2) ? projectId2 : void 0;
190393
190647
  const scope = this.scopeFor(ctx, taskProjectId);
190394
190648
  const task = this.governanceTask(ctx, "discovery_failed", event.sourceType, `${event.sourceId}:${event.sourceVersionId}`, {
190395
190649
  eventType: event.eventType,
@@ -198960,8 +199214,11 @@ var init_github = __esm({
198960
199214
  sessionEnv.set("GH_CONFIG_DIR", configDir);
198961
199215
  const appSlug = credentials["app_slug"];
198962
199216
  if (appSlug) sessionEnv.set("GH_APP_SLUG", appSlug);
198963
- const botEmail = credentials["app_bot_email"] ?? "";
198964
- const botName = credentials["app_bot_name"] ?? "";
199217
+ const rawBotEmail = credentials["app_bot_email"] ?? "";
199218
+ const rawBotName = credentials["app_bot_name"] ?? "";
199219
+ const staleBot = Boolean(appSlug && rawBotEmail && !rawBotEmail.includes(`+${appSlug}[bot]@`));
199220
+ const botEmail = staleBot ? "" : rawBotEmail;
199221
+ const botName = staleBot ? appSlug ? `${appSlug}[bot]` : "" : rawBotName;
198965
199222
  const signName = botName || name;
198966
199223
  sessionEnv.set("GIT_AUTHOR_NAME", signName);
198967
199224
  sessionEnv.set("GIT_AUTHOR_EMAIL", botEmail);
@@ -201251,10 +201508,8 @@ async function adoptGithubApp(store, args, deps = {}) {
201251
201508
  await put3("GITHUB_APP_TOKEN_EXPIRES_AT", String(minted.expiresAtMs), false);
201252
201509
  if (args.slug) {
201253
201510
  const bot = await fetchAppBotIdentity(args.slug, { token: minted.token }, deps);
201254
- if (bot) {
201255
- await put3("GITHUB_APP_BOT_EMAIL", bot.email, false);
201256
- await put3("GITHUB_APP_BOT_NAME", bot.login, false);
201257
- }
201511
+ await put3("GITHUB_APP_BOT_EMAIL", bot?.email ?? "", false);
201512
+ await put3("GITHUB_APP_BOT_NAME", bot?.login ?? `${args.slug}[bot]`, false);
201258
201513
  }
201259
201514
  return {
201260
201515
  account: args.slug ? `${args.slug}[bot]` : args.appId,
@@ -201922,7 +202177,7 @@ async function draftActorResolver(registry2) {
201922
202177
  const byId = new Map(actors.map((a) => [a.id, a]));
201923
202178
  return (id) => {
201924
202179
  const a = byId.get(id);
201925
- return a ? actorRefFieldsOf(a) : systemActorFallback(id);
202180
+ return a ? actorRefFieldsOf(a) : actorRefFallbackOf(id);
201926
202181
  };
201927
202182
  }
201928
202183
  async function draftRoleStaffingResolver(registry2) {
@@ -202412,20 +202667,6 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
202412
202667
  const project = await ctx.projectState.getProject(projectId2);
202413
202668
  if (!project) throw new Error(`project not found: ${projectId2}`);
202414
202669
  await bindingState.upsertWorkspaceBinding({ workOrderId: workspace, projectId: projectId2 });
202415
- } else if (createdViaArg === "seed" && ctx.projectState && bindingState) {
202416
- await ensureWorkorderProject({
202417
- workOrderId: workspace,
202418
- ...optStr(args, "title") ? { title: optStr(args, "title") } : {},
202419
- // ADR 0207「对话中创建则为对话人」:优先记**陪聊解析出的那个真人**
202420
- // (`ctx.workorderInitiator.humanActorId`,与 brief.owner 同源),
202421
- // 没有会话上下文时退到调用者本人——但**只在他是人时**;
202422
- // agent 替人跑 spawn 不该把自己写成项目创建人,那种情况落 NULL,
202423
- // 由读侧从工单 brief.owner 派生(service.resolveProjectCreator)。
202424
- ...ctx.workorderInitiator?.humanActorId ? { createdBy: ctx.workorderInitiator.humanActorId } : !isAgent(actor) ? { createdBy: actor } : {},
202425
- createProject: makeEnsureProjectFromStore(ctx.projectState),
202426
- upsertBinding: (b2) => bindingState.upsertWorkspaceBinding(b2),
202427
- onWarn: (m2) => console.warn(m2)
202428
- });
202429
202670
  }
202430
202671
  if (schemaForSeed) {
202431
202672
  const title = optStr(args, "title") ?? optStr(args, "description") ?? workspace;
@@ -203154,18 +203395,27 @@ async function runView(kernel, oplog, engineStore, blobs, name, artifactId, para
203154
203395
  );
203155
203396
  const pendingGate = pendingConcludeAttempt(model, id);
203156
203397
  const rejectionByRevision = /* @__PURE__ */ new Map();
203398
+ const contentEditedByRevision = /* @__PURE__ */ new Map();
203157
203399
  if (engineStore) {
203158
203400
  const snap = await engineStore.transaction((tx) => tx.loadWorkorder(artifact.workspace));
203159
203401
  for (const w2 of snap?.works ?? []) {
203160
203402
  if (w2.rejectedReason) rejectionByRevision.set(w2.id, w2.rejectedReason);
203161
203403
  }
203404
+ for (const a of snap?.artifacts ?? []) {
203405
+ const at = a.contentEditedAt;
203406
+ if (!at) continue;
203407
+ const workId = a.workId;
203408
+ const prev = contentEditedByRevision.get(workId);
203409
+ if (!prev || at > prev) contentEditedByRevision.set(workId, at);
203410
+ }
203162
203411
  }
203163
203412
  const revisionViews = revisions.map((rev) => ({
203164
203413
  ...rev,
203165
203414
  stage: revisionStage(artifact, rev),
203166
203415
  approvals: (model.reviews.get(rev.id) ?? []).filter((v2) => v2.verdict === "approve").length,
203167
203416
  required: pendingGate?.head === rev.id ? pendingGate.gate?.quorum ?? 0 : 0,
203168
- ...rejectionByRevision.has(rev.id) ? { rejection: { reason: rejectionByRevision.get(rev.id) } } : {}
203417
+ ...rejectionByRevision.has(rev.id) ? { rejection: { reason: rejectionByRevision.get(rev.id) } } : {},
203418
+ ...contentEditedByRevision.has(rev.id) ? { contentEditedAt: contentEditedByRevision.get(rev.id) } : {}
203169
203419
  }));
203170
203420
  return {
203171
203421
  artifact,
@@ -203908,6 +204158,18 @@ async function startOasisServer(opts) {
203908
204158
  chatStore: store,
203909
204159
  chatSessionId: sessionId,
203910
204160
  session: bgSession,
204161
+ liveChat,
204162
+ liveHandle: {
204163
+ runtimeSessionId: session.id,
204164
+ ...session.runId ? { runId: session.runId } : {},
204165
+ kill: () => {
204166
+ void session.kill?.();
204167
+ },
204168
+ ...session.appendInput ? { appendInput: (input) => session.appendInput(input) } : {},
204169
+ get canAppendInput() {
204170
+ return typeof session.appendInput === "function" && session.canAppendInput !== false;
204171
+ }
204172
+ },
203911
204173
  ...itemStore ? { itemStore } : {},
203912
204174
  // 广播/委派回流带上 held turn id(B3);授权卡入口在别处,那条路本轮不接账本。
203913
204175
  ...turnId ? { turnId } : {}
@@ -204466,7 +204728,15 @@ async function startOasisServer(opts) {
204466
204728
  if (body2.command === "spawn" && wodrafts) {
204467
204729
  const draftId = result?.data?.draftId;
204468
204730
  const chatSid = req.headers["x-chat-session-id"];
204469
- if (draftId && typeof chatSid === "string") await wodrafts.tagSession(draftId, chatSid, liveChat.runFor(chatSid));
204731
+ if (draftId && typeof chatSid === "string") {
204732
+ const draftWorkspace = result?.data?.workspace;
204733
+ await wodrafts.tagSession(draftId, chatSid, liveChat.runFor(chatSid), draftWorkspace).catch((err) => {
204734
+ console.error(
204735
+ `[workorder-draft] \u8349\u6848 ${draftId}\uFF08ws=${String(draftWorkspace)}\uFF09\u672A\u80FD\u5173\u8054\u4F1A\u8BDD ${chatSid}\uFF1A`,
204736
+ err
204737
+ );
204738
+ });
204739
+ }
204470
204740
  }
204471
204741
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
204472
204742
  } catch (err) {
@@ -205473,6 +205743,13 @@ async function startOasisServer(opts) {
205473
205743
  };
205474
205744
  const wantsV3 = url.searchParams.get("protocol") === "v3";
205475
205745
  if (wantsV3) {
205746
+ const watchSession = url.searchParams.get("watch") === "1";
205747
+ const fromEpoch = url.searchParams.has("epoch") ? Number(url.searchParams.get("epoch")) : void 0;
205748
+ const cursor = liveChat.cursorFor(sid);
205749
+ if (watchSession && fromEpoch !== void 0 && cursor && cursor.epoch !== fromEpoch) {
205750
+ waitForChatTurn(res, liveChat, sid, startStreamKeepalive, true);
205751
+ return;
205752
+ }
205476
205753
  const sub2 = (frame) => {
205477
205754
  if (res.destroyed) return;
205478
205755
  res.write(`${JSON.stringify(frame)}
@@ -205484,6 +205761,11 @@ async function startOasisServer(opts) {
205484
205761
  await reopenRecoveringTurn();
205485
205762
  att2 = liveChat.attachLiveV3(sid, fromSeq, sub2);
205486
205763
  }
205764
+ if (watchSession && (!att2 || att2.status !== "running" && att2.replay.length === 0)) {
205765
+ att2?.detach();
205766
+ waitForChatTurn(res, liveChat, sid, startStreamKeepalive);
205767
+ return;
205768
+ }
205487
205769
  if (!att2) {
205488
205770
  res.writeHead(204).end();
205489
205771
  return;
@@ -205491,7 +205773,8 @@ async function startOasisServer(opts) {
205491
205773
  res.writeHead(200, {
205492
205774
  "content-type": "application/x-oasis-live-v3+ndjson; charset=utf-8",
205493
205775
  "transfer-encoding": "chunked",
205494
- "access-control-expose-headers": "X-Session-Id, X-Replay-Gap, X-Live-Epoch, X-Can-Append",
205776
+ "access-control-expose-headers": "X-Session-Id, X-Replay-Gap, X-Live-Epoch, X-Can-Append, X-Chat-Session-Watch",
205777
+ ...watchSession ? { "x-chat-session-watch": "1" } : {},
205495
205778
  "x-session-id": cs.runtimeSessionId ?? "",
205496
205779
  "x-live-epoch": String(att2.epoch),
205497
205780
  "x-can-append": liveChat.canAppend(sid) ? "1" : "0",
@@ -206250,10 +206533,26 @@ ${composed}`;
206250
206533
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
206251
206534
  ...chatExit?.detail ? { detail: chatExit.detail } : {}
206252
206535
  });
206253
- itemLedger.recordError(errText2, {
206536
+ const errorOutcome = itemLedger.recordError(errText2, {
206254
206537
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
206255
206538
  ...chatExit?.detail ? { detail: chatExit.detail } : {}
206256
206539
  });
206540
+ if (live) {
206541
+ live.publishServerItem({
206542
+ itemId: errorOutcome.itemId,
206543
+ itemType: "control",
206544
+ startedVersion: errorOutcome.startedVersion,
206545
+ version: errorOutcome.version,
206546
+ ord: errorOutcome.ord,
206547
+ status: "failed",
206548
+ payload: {
206549
+ code: "error",
206550
+ note: errText2,
206551
+ ...chatExit?.reason ? { reason: chatExit.reason } : {},
206552
+ ...chatExit?.detail ? { detail: chatExit.detail } : {}
206553
+ }
206554
+ });
206555
+ }
206257
206556
  }
206258
206557
  finished = true;
206259
206558
  clearInterval(ckptTimer);
@@ -206858,15 +207157,36 @@ ${composed}`;
206858
207157
  const installationId = await svc.revealResolvedVariable("GITHUB_APP_INSTALLATION_ID", actorId);
206859
207158
  const effective = installationId ? await fetchInstallationPermissions({ appId, privateKeyPem, installationId }) : meta.permissions;
206860
207159
  const missing = missingAppPermissions(effective);
207160
+ const requestedAll = Object.entries(meta.permissions).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key));
207161
+ const grantedKeys = new Set(
207162
+ Object.entries(effective).filter(([, level]) => Boolean(level)).map(([k2]) => k2)
207163
+ );
206861
207164
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
206862
207165
  configured: true,
206863
207166
  slug: meta.slug,
206864
207167
  ownerLogin: meta.ownerLogin,
207168
+ /** App 申请的全集(英文原名 + 级别)。界面用它回答「我配了多少项」。 */
207169
+ requested: requestedAll,
206865
207170
  /** true=读的是这次安装真正被授予的权限;false=只拿到 App 申请的,可能偏乐观。 */
206866
207171
  fromInstallation: Boolean(installationId),
206867
- /** App 申请了、但这次安装还没被授予——多半是权限升级还等着组织 owner 批准。 */
206868
- awaitingApproval: missingAppPermissions(effective).filter((m2) => meta.permissions[m2.key]).map((m2) => m2.key),
206869
- granted: GITHUB_APP_PERMISSION_LABELS.filter((r) => effective[r.key]),
207172
+ /**
207173
+ * App 申请了、但这次安装还没被授予——多半是权限升级还等着组织 owner 批准。
207174
+ * **在全集上做差**,不再只看本仓必需的那八项(否则人另外勾的永远报不出来)。
207175
+ */
207176
+ awaitingApproval: requestedAll.filter((r) => !grantedKeys.has(r.key)).map((r) => r.key),
207177
+ /**
207178
+ * 这次安装**真正被授予**的全部权限,一律用 GitHub 的英文原名 + 级别
207179
+ * (2026-09-10 发起人:「所有权限都用英文原文,不区分是不是八项权限」)。
207180
+ *
207181
+ * 此前这里是 `GITHUB_APP_PERMISSION_LABELS.filter(...)` —— 那张表只有本仓必需的
207182
+ * 八项中文标签,于是这一格实际上是「必备清单打勾」,人在 GitHub 上另外勾的一个都不显示,
207183
+ * 看上去像没生效。中间还试过「八项中文 + 额外项英文」两段式,也被否了:
207184
+ * 同一个列表两套命名,反而要人先分辨「这条为什么是中文那条为什么是英文」。
207185
+ *
207186
+ * **不编中文名**:GitHub 的权限键是开放集合且还在增,写死对照表迟早漏;
207187
+ * 而英文原名与 GitHub 设置页上的字**逐字一致**,对着改权限时不用做心理翻译。
207188
+ */
207189
+ granted: Object.entries(effective).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key)),
206870
207190
  missing,
206871
207191
  settingsUrl: meta.slug ? appPermissionSettingsUrl(meta) : null
206872
207192
  }));
@@ -206942,7 +207262,8 @@ ${composed}`;
206942
207262
  const perms = pending.permissions ?? {};
206943
207263
  const missing = missingAppPermissions(perms);
206944
207264
  return {
206945
- granted: GITHUB_APP_PERMISSION_LABELS.filter((r) => perms[r.key]),
207265
+ // app/permissions 那一处同口径:英文原名 + 级别,不分是不是本仓必需的八项。
207266
+ granted: Object.entries(perms).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key)),
206946
207267
  missing,
206947
207268
  ...pending.slug ? { settingsUrl: appPermissionSettingsUrl({
206948
207269
  slug: pending.slug,
@@ -206985,10 +207306,8 @@ ${composed}`;
206985
207306
  }
206986
207307
  if (pending.slug) {
206987
207308
  const bot = await fetchAppBotIdentity(pending.slug, { token: minted.token });
206988
- if (bot) {
206989
- await put3("GITHUB_APP_BOT_EMAIL", bot.email, false);
206990
- await put3("GITHUB_APP_BOT_NAME", bot.login, false);
206991
- }
207309
+ await put3("GITHUB_APP_BOT_EMAIL", bot?.email ?? "", false);
207310
+ await put3("GITHUB_APP_BOT_NAME", bot?.login ?? `${pending.slug}[bot]`, false);
206992
207311
  }
206993
207312
  pending.done = true;
206994
207313
  delete pending.privateKeyPem;
@@ -207172,7 +207491,9 @@ ${composed}`;
207172
207491
  const svc = await connectorActorsService();
207173
207492
  if (svc) {
207174
207493
  const connId = "github";
207175
- await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
207494
+ if (!actorIdParam) {
207495
+ await svc.upsertConnector({ id: connId, name: "GitHub", mode: "oauth", status: "connected", account: "github" });
207496
+ }
207176
207497
  for (const [k2, v2] of [["GIT_AUTHOR_NAME", actorNameParam], ["GIT_AUTHOR_EMAIL", ""], ["GIT_COMMITTER_NAME", actorNameParam], ["GIT_COMMITTER_EMAIL", ""], ["EMAIL", ""]])
207177
207498
  await svc.putVariable({ key: k2, value: v2, scope: "personal", actorId: actorIdParam, connectorId: connId, encrypted: false });
207178
207499
  await svc.putVariable({ key: "GITHUB_TOKEN", value: p2.access_token, scope: "personal", actorId: actorIdParam, connectorId: connId, encrypted: true });
@@ -207307,7 +207628,6 @@ var init_server3 = __esm({
207307
207628
  init_src7();
207308
207629
  init_collect();
207309
207630
  init_remote_util();
207310
- init_ephemeral_project();
207311
207631
  init_timeline();
207312
207632
  init_node_state();
207313
207633
  init_src5();
@@ -207316,6 +207636,7 @@ var init_server3 = __esm({
207316
207636
  init_router();
207317
207637
  init_infra_error();
207318
207638
  init_live_chat();
207639
+ init_chat_turn_wait();
207319
207640
  init_append_now();
207320
207641
  init_workorder_drafts();
207321
207642
  init_draft_edit();
@@ -210381,6 +210702,32 @@ var init_projection = __esm({
210381
210702
  }
210382
210703
  });
210383
210704
 
210705
+ // ../server/src/domains/projects/uncategorized-project.ts
210706
+ function isUncategorizedProjectId(projectId2) {
210707
+ return projectId2 === UNCATEGORIZED_PROJECT_ID;
210708
+ }
210709
+ async function bindWorkorderProject(deps) {
210710
+ const { workOrderId, projectId: projectId2 } = deps;
210711
+ if (!projectId2) return null;
210712
+ try {
210713
+ await deps.upsertBinding({ workOrderId, projectId: projectId2 });
210714
+ return projectId2;
210715
+ } catch (e) {
210716
+ deps.onWarn?.(`[project] \u5DE5\u5355 ${workOrderId} \u7ED1\u5B9A\u9879\u76EE\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u5EFA\u5355\uFF09\uFF1A${String(e)}`);
210717
+ return null;
210718
+ }
210719
+ }
210720
+ var UNCATEGORIZED_PROJECT_ID, UNCATEGORIZED_PROJECT_NAME, UNCATEGORIZED_PROJECT_DESCRIPTION, UNCATEGORIZED_UNKNOWN_TIME;
210721
+ var init_uncategorized_project = __esm({
210722
+ "../server/src/domains/projects/uncategorized-project.ts"() {
210723
+ "use strict";
210724
+ UNCATEGORIZED_PROJECT_ID = "proj:uncategorized";
210725
+ UNCATEGORIZED_PROJECT_NAME = "\u65E0\u5206\u7C7B\u9879\u76EE";
210726
+ UNCATEGORIZED_PROJECT_DESCRIPTION = "\u6240\u6709\u672A\u9009\u62E9\u9879\u76EE\u7684\u5BF9\u8BDD\u76F8\u5173\u6587\u4EF6\u548C\u4EFB\u52A1\u81EA\u52A8\u5E76\u5165\u8FD9\u91CC\uFF0C\u7EC4\u7EC7\u5185\u6240\u6709\u4EBA\u53EF\u89C1\u3002";
210727
+ UNCATEGORIZED_UNKNOWN_TIME = "1970-01-01T00:00:00.000Z";
210728
+ }
210729
+ });
210730
+
210384
210731
  // ../server/src/domains/projects/service.ts
210385
210732
  function sanitizeCreatorActorId(raw) {
210386
210733
  const id = raw?.trim();
@@ -210642,6 +210989,10 @@ function buildRevisionDag(revisions) {
210642
210989
  }))
210643
210990
  };
210644
210991
  }
210992
+ function laterOf(a, b2) {
210993
+ if (!b2) return a;
210994
+ return b2 > a ? b2 : a;
210995
+ }
210645
210996
  var DEFAULT_AGENT_ACTOR, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
210646
210997
  var init_service4 = __esm({
210647
210998
  "../server/src/domains/projects/service.ts"() {
@@ -210649,7 +211000,7 @@ var init_service4 = __esm({
210649
211000
  init_src();
210650
211001
  init_src5();
210651
211002
  init_projection();
210652
- init_ephemeral_project();
211003
+ init_uncategorized_project();
210653
211004
  init_workorders();
210654
211005
  init_node_state();
210655
211006
  DEFAULT_AGENT_ACTOR = "actor:agent:system";
@@ -210785,6 +211136,9 @@ var init_service4 = __esm({
210785
211136
  async listWorkspaceBindings() {
210786
211137
  return [...this.workspaceBindings.values()].map((binding) => ({ ...binding }));
210787
211138
  }
211139
+ async deleteWorkspaceBinding(workOrderId) {
211140
+ this.workspaceBindings.delete(workOrderId);
211141
+ }
210788
211142
  async setWorkspaceDispatchHold(workOrderId, held) {
210789
211143
  if (held) this.dispatchHeld.add(workOrderId);
210790
211144
  else this.dispatchHeld.delete(workOrderId);
@@ -210911,15 +211265,8 @@ var init_service4 = __esm({
210911
211265
  return { project, artifacts, files };
210912
211266
  }
210913
211267
  async collectProjectFiles(projectId2) {
210914
- if (!isUncategorizedProjectId(projectId2)) return this.projects.listProjectFiles(projectId2);
210915
- const stored = await this.projects.listProjects();
210916
- const ephemeralIds = stored.filter((p2) => isEphemeralProject(p2.id)).map((p2) => p2.id);
210917
- const out = [];
210918
- for (const id of ephemeralIds) {
210919
- const files = await this.projects.listProjectFiles(id);
210920
- out.push(...files.map((f2) => ({ ...f2, projectId: UNCATEGORIZED_PROJECT_ID })));
210921
- }
210922
- return out.sort((a, b2) => b2.uploadedAt.localeCompare(a.uploadedAt));
211268
+ if (isUncategorizedProjectId(projectId2)) return [];
211269
+ return this.projects.listProjectFiles(projectId2);
210923
211270
  }
210924
211271
  /**
210925
211272
  * B2 投影同步:写命令落 oplog 后,从 oplog 重放重建 `this.artifacts` 文档层投影。
@@ -210976,23 +211323,18 @@ var init_service4 = __esm({
210976
211323
  return project ? this.withProjectDerivedState(project) : null;
210977
211324
  }
210978
211325
  /**
210979
- * 「无分类项目」虚拟卡:把所有 `proj_tmp_*` 一族的**读侧聚合**成一张固定项目,供列表/详情/关联任务/相关文件消费。
211326
+ * 「无分类项目」虚拟卡:把所有**没有项目绑定**的工单聚合成一张固定项目,供列表/详情/关联任务/相关文件消费。
210980
211327
  * 存储里没有真行——这就是本卡与真项目最大的区别:
210981
211328
  * - 写侧(成员、内容位置、上传文件)一律拒收(写到真项目 or 走真工单流程);
210982
- * - 派生态(artifactSummary / workOrderCount)跨所有 ephemeral 项目算并集;
211329
+ * - 派生态(artifactSummary / workOrderCount)按「无绑定工单」算并集;
210983
211330
  * - 帧上的「项目负责人:系统」由前端自选文案渲染,本层只给一个稳定的空 members 名单。
211331
+ *
211332
+ * ⚠ `workOrderCount` **走 listProjectWorkorders 同一条路**,不另写一份 bindings 过滤——
211333
+ * 两处各算各的正是「卡片计数与点进去的列表条数对不上」那类缺陷的来路。
210984
211334
  */
210985
211335
  async buildUncategorizedProject() {
210986
- const stored = await this.projects.listProjects();
210987
- const ephemeralIds = new Set(
210988
- stored.filter((project) => isEphemeralProject(project.id)).map((project) => project.id)
210989
- );
210990
- const bindings = await this.artifacts.listWorkspaceBindings();
210991
- const workOrderCount = bindings.filter((b2) => ephemeralIds.has(b2.projectId)).length;
210992
- const artifactList = await this.aggregateUncategorizedArtifacts(ephemeralIds, bindings);
210993
- const ephemeralProjects = stored.filter((project) => ephemeralIds.has(project.id));
210994
- const createdAts = ephemeralProjects.map((p2) => p2.createdAt).sort();
210995
- const updatedAts = ephemeralProjects.map((p2) => p2.updatedAt).sort();
211336
+ const workorders = await this.listProjectWorkorders(UNCATEGORIZED_PROJECT_ID);
211337
+ const artifactList = await this.aggregateUncategorizedArtifacts();
210996
211338
  return {
210997
211339
  id: UNCATEGORIZED_PROJECT_ID,
210998
211340
  name: UNCATEGORIZED_PROJECT_NAME,
@@ -211000,46 +211342,33 @@ var init_service4 = __esm({
211000
211342
  memberCount: 0,
211001
211343
  members: [],
211002
211344
  artifactSummary: artifactList.summary,
211003
- workOrderCount,
211345
+ workOrderCount: workorders.length,
211004
211346
  // ADR 0205:本卡的「项目负责人:系统」是**前端按帧自选的文案**,后端不出 `"system"` 哨兵——
211005
211347
  // 出 null 保持「取不到就 null」的域内红线,也不把一个不存在的 actor id 塞进契约。
211006
211348
  createdBy: null,
211007
- createdAt: createdAts[0] ?? UNCATEGORIZED_UNKNOWN_TIME,
211008
- updatedAt: updatedAts[updatedAts.length - 1] ?? UNCATEGORIZED_UNKNOWN_TIME
211349
+ createdAt: UNCATEGORIZED_UNKNOWN_TIME,
211350
+ updatedAt: UNCATEGORIZED_UNKNOWN_TIME
211009
211351
  };
211010
211352
  }
211011
- async aggregateUncategorizedArtifacts(ephemeralIds, bindings) {
211012
- const _bindings = bindings ?? await this.artifacts.listWorkspaceBindings();
211013
- const artifacts = [];
211014
- for (const projectId2 of ephemeralIds) {
211015
- const list2 = await this.listAllProjectArtifacts(projectId2);
211016
- artifacts.push(...list2.map((a) => ({ ...a, projectId: UNCATEGORIZED_PROJECT_ID })));
211017
- }
211353
+ async aggregateUncategorizedArtifacts() {
211354
+ const artifacts = await this.listAllProjectArtifacts(UNCATEGORIZED_PROJECT_ID);
211018
211355
  artifacts.sort((a, b2) => b2.updatedAt.localeCompare(a.updatedAt));
211019
211356
  return { artifacts, summary: summarizeArtifacts(artifacts) };
211020
211357
  }
211021
211358
  /**
211022
211359
  * 项目列表(带派生态)。
211023
211360
  *
211024
- * ★ `includeEphemeral` 必须在**算派生态之前**生效,不能让调用方拿到全量再自己 filter。
211025
- * `withProjectDerivedState` 每算一个项目就要跑一遍 `buildWorkorderSummaries`(全量工单)+
211026
- * 遍历模型里全部 artifact——成本是 O(项目数 × 全模型),而且是同步的。实测 dev:294 个项目里
211027
- * 272 个是 `proj_tmp_*` 临时项目(工单没指定项目时的兜底归属,一单一个),列表只返回剩下的 22 个,
211028
- * **93% 的计算算完就扔**。代价不是"多花点 CPU":这段同步活会把单线程的事件循环整块堵住 3~11 秒,
211029
- * 期间别的请求从 pg 连接池取连接排不上号、撞 `connectionTimeoutMillis`(5s),
211030
- * 前端看到的是「项目列表加载失败:timeout exceeded when trying to connect」——像数据库挂了,
211031
- * 其实是被自己堵死的(实测并发时 /api/automations 从 0.24s 被拖到 7.1s,自动化页面因此空白)。
211032
- *
211033
- * 只对**真要返回的那些**算派生态。默认口径与路由一致(不含临时项目),排查用 `includeEphemeral`。
211361
+ * 库里的每一行都是**人建的真项目**,全部返回——临时项目(`proj_tmp_*`)退役后,这里不再有
211362
+ * "算完就扔"的过滤:旧版 dev 环境 294 个项目里 272 个是一单一个的临时项目,
211363
+ * `withProjectDerivedState` 对它们逐个跑 `buildWorkorderSummaries`(全量工单)+ 遍历全部 artifact,
211364
+ * 93% 的计算白算,还把单线程事件循环整块堵住 3~11 秒(并发时 /api/automations 从 0.24s 被拖到 7.1s,
211365
+ * 前端看到的是「项目列表加载失败:timeout」——像数据库挂了,其实是被自己堵死的)。
211034
211366
  */
211035
211367
  async listProjects(opts) {
211036
211368
  const stored = await this.projects.listProjects();
211037
- const wanted = opts?.includeEphemeral === true ? stored : stored.filter((project) => !isEphemeralProject(project.id));
211038
211369
  const creatorCache = /* @__PURE__ */ new Map();
211039
- const derived = await Promise.all(wanted.map((project) => this.withProjectDerivedState(project, creatorCache)));
211370
+ const derived = await Promise.all(stored.map((project) => this.withProjectDerivedState(project, creatorCache)));
211040
211371
  if (opts?.includeUncategorized === false) return derived;
211041
- const wantUncategorized = opts?.includeUncategorized ?? !(opts?.includeEphemeral === true);
211042
- if (!wantUncategorized) return derived;
211043
211372
  const virtual = await this.buildUncategorizedProject();
211044
211373
  return [virtual, ...derived];
211045
211374
  }
@@ -211118,10 +211447,10 @@ var init_service4 = __esm({
211118
211447
  * 项目创建人(ADR 0205)——**读时解析**,三条来路按优先级:
211119
211448
  *
211120
211449
  * 1. 库里落了 `created_by` → 按它解析 name/avatar;
211121
- * 2. 没落且是**临时项目**(`proj_tmp_*`)→ 从它绑定的那张工单的 `brief.owner`(决策 0026 的
211122
- * 「发起人真人」)派生。这是老数据免回填的口径:派生每次都新鲜,回填反而会造出
211123
- * 「工单发起人换过、项目创建人却挂着旧值」的伪相;
211124
- * 3. 都取不到 `null`(老的手建项目),前端诚实显示「—」。
211450
+ * 2. 没落 `null`(老的手建项目),前端诚实显示「—」。
211451
+ *
211452
+ * 旧版还有一档「临时项目从它绑定的那张工单 `brief.owner` 派生」——临时项目退役后没有这一档了
211453
+ * (见 uncategorized-project.ts 文件头)。
211125
211454
  *
211126
211455
  * **绝不拿别的字段顶替**(`members[0]`、项目名、updatedAt 都不行)——未指定成员时全组织都是成员,
211127
211456
  * 按 actorId 排序等于随机点一个 agent 当负责人。这是本域红线,见 resolveActorName 的注释。
@@ -211129,9 +211458,7 @@ var init_service4 = __esm({
211129
211458
  async resolveProjectCreator(project, workorders, cache) {
211130
211459
  const stored = project.createdBy?.id;
211131
211460
  if (stored) return this.resolveActorRef(stored, cache);
211132
- if (!isEphemeralProject(project.id)) return null;
211133
- const ownerId = workorders.map((wo) => wo.owner?.id).find((id) => typeof id === "string" && id.startsWith("actor:"));
211134
- return ownerId ? this.resolveActorRef(ownerId, cache) : null;
211461
+ return null;
211135
211462
  }
211136
211463
  /**
211137
211464
  * actorId → {@link ProjectActorRef}:`kind` 按 id 前缀派生,`name` / `avatar` 走本公司名册现取。
@@ -211194,7 +211521,8 @@ var init_service4 = __esm({
211194
211521
  */
211195
211522
  async reassignWorkorder(workOrderId, projectId2) {
211196
211523
  if (isUncategorizedProjectId(projectId2)) {
211197
- return this.unlinkWorkorder(workOrderId);
211524
+ await this.unlinkWorkorder(workOrderId);
211525
+ return { workOrderId, projectId: null };
211198
211526
  }
211199
211527
  const project = await this.projects.getProject(projectId2);
211200
211528
  if (!project) throw new Error(`project not found: ${projectId2}`);
@@ -211203,77 +211531,46 @@ var init_service4 = __esm({
211203
211531
  return binding;
211204
211532
  }
211205
211533
  /**
211206
- * 「取消任务关联」——把工单从当前项目解绑,回落到它专属的临时项目(proj_tmp_<slug>)。
211534
+ * 「取消任务关联」——**删掉工单的项目绑定行**,工单从此没有项目(读侧落进「无分类项目」桶)。
211207
211535
  *
211208
- * **不是真正解绑成 null**:契约里 `Workspace.projectId` 是必填(见 ephemeral-project.ts 的历史事故链),
211209
- * 无绑定态会让下游 provision / 自动收集 / propose 走向连环破坏。回落到临时项目 =「在读侧进入
211210
- * 无分类桶」+「写侧仍保持"每工单必属项目"的合法态」。
211536
+ * 2026-09-10 之前这里是"回落到工单专属的临时项目 `proj_tmp_<slug>`",理由是「契约里每个工单
211537
+ * 都该属于某项目」。那套兜底已退役(见 uncategorized-project.ts 文件头):兜底容器让每张未挂靠
211538
+ * 工单变成一个只有它自己的假项目,代码仓/项目变量/记忆作用域全挂在一次性 id 上。
211211
211539
  *
211212
- * 幂等:目标已是自己的 ephemeral 项目 → 直接返回既有绑定,不重复 upsert;ephemeral 项目本身
211213
- * 不需要预先存在(`ensureWorkorderProject` 后台建,也可能是本工单第一次被"解绑"——按需建)。
211540
+ * 幂等:没有绑定行时静默返回(store delete 本身幂等)。
211214
211541
  */
211215
211542
  async unlinkWorkorder(workOrderId) {
211216
211543
  if (!workOrderId.trim()) throw new Error("work_order_id is required");
211217
- const ephemeralId = ephemeralProjectIdFor(workOrderId);
211218
- const existing = await this.artifacts.getWorkspaceBinding(workOrderId);
211219
- if (!await this.projects.getProject(ephemeralId)) {
211220
- const now = (/* @__PURE__ */ new Date()).toISOString();
211221
- await this.projects.upsertProject({
211222
- id: ephemeralId,
211223
- name: `\u4E34\u65F6\u9879\u76EE \xB7 ${workOrderId}`,
211224
- 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`,
211225
- createdAt: now,
211226
- updatedAt: now
211227
- });
211228
- }
211229
- if (existing?.projectId === ephemeralId) return existing;
211230
- const binding = { workOrderId, projectId: ephemeralId };
211231
- await this.artifacts.upsertWorkspaceBinding(binding);
211232
- return binding;
211544
+ await this.artifacts.deleteWorkspaceBinding(workOrderId);
211233
211545
  }
211234
211546
  /**
211235
211547
  * 删除项目(帧 `444:1792` 三点菜单第二项,口径由发起人 2026-09-09 拍板)。
211236
211548
  *
211237
211549
  * **只删项目这个壳**——项目主记录 + 成员名单 + 上传文件记录 + 内容位置。任务和会话一条都不删:
211238
211550
  *
211239
- * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 回落到自己的
211240
- * `proj_tmp_<slug>`,也就是读侧的「无分类项目」。发起人明确不要「有任务在跑就不让删」
211241
- * 这道闸,所以这里没有任何在跑判断。
211242
- * 2. 搬工单的同时**把本项目的 git 类内容位置复制一份到每个工单的临时项目上**。
211243
- * 这条不是可选的:`git/resolve.ts` 的 `resolveCodeRepo` 是**派发时**沿
211244
- * `binding project → listContentLocations` 实时解的,不带过去的话,正在跑的任务
211245
- * 下一个节点就取不到代码仓——正是 `ephemeral-project.ts` 顶部那条五环事故链。
211551
+ * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 删掉绑定行 → 落进读侧的
211552
+ * 「无分类项目」。发起人明确不要「有任务在跑就不让删」这道闸,所以这里没有任何在跑判断。
211553
+ * 2. **代码仓不跟着搬**(发起人 2026-09-10 裁定)。`git/resolve.ts` 的 `resolveCodeRepo` 是
211554
+ * 派发时沿 `绑定 项目 → 内容位置` 实时解的,所以删完项目,这些任务下一个节点就取不到代码仓——
211555
+ * 这是**期望行为**:由执行中的数字员工发现并引导人给任务指定新项目,而不是由系统兜一个假容器
211556
+ * (旧版把 git 位置逐单复制到 `proj_tmp_*` 上,正是那套已退役的兜底)。
211246
211557
  * 3. 会话的 `project_id` 置空、项目作用域的变量/密钥清理,走 {@link OnProjectDeleted} 钩子
211247
211558
  * (本 service 对会话域与凭据域零耦合,同 `onProjectNameChanged` 的注入模式)。
211248
211559
  *
211249
- * 拒绝两类 id:`proj:uncategorized`(读侧虚拟卡,存储里没有真行)与 `proj_tmp_*`
211250
- * (工单的兜底归属,删掉等于把工单变成无绑定态——契约里 `Workspace.projectId` 必填)。
211560
+ * 拒绝 `proj:uncategorized`(读侧虚拟卡,存储里没有真行,删它没有意义)。
211251
211561
  */
211252
211562
  async deleteProject(id) {
211253
211563
  const projectId2 = id?.trim();
211254
211564
  if (!projectId2) throw new Error("project id is required");
211255
211565
  if (isUncategorizedProjectId(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
211256
- if (isEphemeralProject(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
211257
211566
  const project = await this.projects.getProject(projectId2);
211258
211567
  if (!project) throw new Error(`project not found: ${projectId2}`);
211259
- const gitLocations = (await this.projects.listContentLocations(projectId2)).filter((loc) => loc.kind === "external-home" && loc.externalHome?.system === "git");
211260
- const now = (/* @__PURE__ */ new Date()).toISOString();
211261
211568
  const bindings = await this.artifacts.listWorkspaceBindings();
211262
211569
  const movedWorkOrderIds = [];
211263
211570
  for (const binding of bindings) {
211264
211571
  if (binding.projectId !== projectId2) continue;
211265
- const moved = await this.unlinkWorkorder(binding.workOrderId);
211572
+ await this.unlinkWorkorder(binding.workOrderId);
211266
211573
  movedWorkOrderIds.push(binding.workOrderId);
211267
- for (const loc of gitLocations) {
211268
- await this.projects.addContentLocation({
211269
- ...loc,
211270
- projectId: moved.projectId,
211271
- // 派生 id:同一条位置搬到同一个临时项目永远算出同一个 id(addContentLocation 是
211272
- // upsert),重跑不会堆出重复的仓库行。
211273
- locationId: `pcl-${moved.projectId}-carried-${loc.locationId}`,
211274
- updatedAt: now
211275
- });
211276
- }
211277
211574
  }
211278
211575
  await this.projects.deleteProject(projectId2);
211279
211576
  if (this.onProjectDeleted) {
@@ -211283,7 +211580,7 @@ var init_service4 = __esm({
211283
211580
  console.warn(`[projects] project-deleted follow-up failed for ${projectId2}: ${error2 instanceof Error ? error2.message : String(error2)}`);
211284
211581
  }
211285
211582
  }
211286
- return { projectId: projectId2, movedWorkOrderIds, carriedGitLocationCount: gitLocations.length };
211583
+ return { projectId: projectId2, movedWorkOrderIds };
211287
211584
  }
211288
211585
  /**
211289
211586
  * 项目关联工单(帧 `1355:4918`):把内部私有的 `listProjectWorkorders` 暴露给路由层。
@@ -211498,7 +211795,11 @@ var init_service4 = __esm({
211498
211795
  if (arts.length === 0) continue;
211499
211796
  const work = worksById.get(acceptedId);
211500
211797
  const folderPath = `wo:${workOrderId}/node:${node2.id}`;
211501
- const folderUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211798
+ const workUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211799
+ const folderUpdatedAt = arts.reduce(
211800
+ (acc, a) => laterOf(acc, a.contentEditedAt),
211801
+ workUpdatedAt
211802
+ );
211502
211803
  const creatorId = work?.assigneeActorId ?? null;
211503
211804
  const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
211504
211805
  const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
@@ -211522,7 +211823,7 @@ var init_service4 = __esm({
211522
211823
  for (const a of sortedArts) {
211523
211824
  const artifactId = node2.id;
211524
211825
  const artifactType = a.type ?? node2.type;
211525
- const rowUpdatedAt = folderUpdatedAt;
211826
+ const rowUpdatedAt = laterOf(workUpdatedAt, a.contentEditedAt);
211526
211827
  if (a.contentKind === "manifest" && a.contentRef?.trim()) {
211527
211828
  const content3 = await this.readContentRefText(a.contentRef);
211528
211829
  const files = content3 !== null ? tryParseManifestFiles(content3) : void 0;
@@ -211909,7 +212210,7 @@ var init_service4 = __esm({
211909
212210
  if (!snap) return [];
211910
212211
  const worksById = new Map(snap.works.map((w2) => [w2.id, w2]));
211911
212212
  const binding = this.artifacts.getWorkspaceBinding ? await this.artifacts.getWorkspaceBinding(workOrderId).catch(() => null) : null;
211912
- const projectId2 = binding?.projectId ?? ephemeralProjectIdFor(workOrderId);
212213
+ const projectId2 = binding?.projectId ?? UNCATEGORIZED_PROJECT_ID;
211913
212214
  const out = [];
211914
212215
  for (const node2 of snap.nodes) {
211915
212216
  if (!node2.latestAcceptId) continue;
@@ -211918,6 +212219,7 @@ var init_service4 = __esm({
211918
212219
  const arts = snap.artifacts.filter((a) => a.workId === acceptedId);
211919
212220
  if (arts.length === 0) continue;
211920
212221
  const work = worksById.get(acceptedId);
212222
+ const workUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211921
212223
  const documents = arts.slice().sort((a, b2) => a.ordinal - b2.ordinal).map((a) => ({
211922
212224
  // collab 层 artifactId === engine nodeId(见 bridge-readmodel.ts:215)——预览端读的
211923
212225
  // useArtifactView 也走这个 id。
@@ -211929,7 +212231,9 @@ var init_service4 = __esm({
211929
212231
  contentRef: a.contentRef,
211930
212232
  title: node2.title || a.name,
211931
212233
  // 节点标题优先;否则用 engine artifact 的 "output" 兜底
211932
- contentKind: engineContentKind(a.contentKind)
212234
+ contentKind: engineContentKind(a.contentKind),
212235
+ // 文档级时间:被人在预览里就地改写过就跳到改写时刻,否则就是这轮执行的时刻。
212236
+ updatedAt: laterOf(workUpdatedAt, a.contentEditedAt)
211933
212237
  }));
211934
212238
  out.push({
211935
212239
  outputId: `engine:${acceptedId}`,
@@ -211940,7 +212244,9 @@ var init_service4 = __esm({
211940
212244
  documents,
211941
212245
  payload: {},
211942
212246
  createdAt: work?.createdAt ?? node2.createdAt,
211943
- updatedAt: work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt
212247
+ // 节点(= 文件夹行)的时间取「这轮执行的时刻」与「其下任一文档最后被就地改写的时刻」里更晚的那个:
212248
+ // 改过某个文件之后,文件夹不能还显示比自己的孩子更早的时间。
212249
+ updatedAt: documents.reduce((acc, d) => laterOf(acc, d.updatedAt), workUpdatedAt)
211944
212250
  });
211945
212251
  }
211946
212252
  return out;
@@ -212034,20 +212340,8 @@ var init_service4 = __esm({
212034
212340
  const filtered = artifacts.filter((artifact) => input.type === void 0 || artifact.type === input.type).filter((artifact) => input.status === void 0 || artifact.status === input.status).sort((a, b2) => b2.updatedAt.localeCompare(a.updatedAt));
212035
212341
  return { artifacts: filtered, summary: summarizeArtifacts(filtered) };
212036
212342
  }
212037
- /** 内部:虚拟卡 跨全部 ephemeral 项目聚合;真项目 → 走原路径。 */
212343
+ /** 内部:虚拟卡与真项目走同一条路——{@link listAllProjectArtifacts} 内部按「无绑定 / 绑到本项目」分流。 */
212038
212344
  async collectProjectArtifacts(projectId2) {
212039
- if (isUncategorizedProjectId(projectId2)) {
212040
- const stored = await this.projects.listProjects();
212041
- const ephemeralIds = new Set(
212042
- stored.filter((project) => isEphemeralProject(project.id)).map((project) => project.id)
212043
- );
212044
- const artifacts = [];
212045
- for (const id of ephemeralIds) {
212046
- const list2 = await this.listAllProjectArtifacts(id);
212047
- artifacts.push(...list2.map((a) => ({ ...a, projectId: UNCATEGORIZED_PROJECT_ID })));
212048
- }
212049
- return artifacts;
212050
- }
212051
212345
  return this.listAllProjectArtifacts(projectId2);
212052
212346
  }
212053
212347
  async listProjectWorkorders(projectId2) {
@@ -212056,17 +212350,19 @@ var init_service4 = __esm({
212056
212350
  const uncategorized = isUncategorizedProjectId(projectId2);
212057
212351
  return buildWorkorderSummaries(this.kernel.model, void 0, (workOrderId) => projectByWorkorder.get(workOrderId) ?? null, void 0, void 0, void 0, (role) => this.kernel.actorForRole(role)).filter((workorder) => {
212058
212352
  const bound = workorder.projectId;
212059
- if (uncategorized) return typeof bound === "string" && isEphemeralProject(bound);
212353
+ if (uncategorized) return bound == null;
212060
212354
  return bound === projectId2;
212061
212355
  });
212062
212356
  }
212063
212357
  async listAllProjectArtifacts(projectId2) {
212064
- const stored = await this.artifacts.listArtifacts({ projectId: projectId2 });
212358
+ const uncategorized = isUncategorizedProjectId(projectId2);
212359
+ const stored = uncategorized ? [] : await this.artifacts.listArtifacts({ projectId: projectId2 });
212065
212360
  const byArtifact = new Map(stored.map((artifact) => [artifact.artifactId, artifact]));
212066
212361
  const bindings = await this.artifacts.listWorkspaceBindings();
212067
212362
  const projectByWorkorder = new Map(bindings.map((binding) => [binding.workOrderId, binding.projectId]));
212068
212363
  for (const artifact of this.kernel.model.artifacts.values()) {
212069
- if (projectByWorkorder.get(artifact.workspace) !== projectId2) continue;
212364
+ const bound = projectByWorkorder.get(artifact.workspace);
212365
+ if (uncategorized ? bound !== void 0 : bound !== projectId2) continue;
212070
212366
  const descriptor = decodeDocumentDescription(artifact.description);
212071
212367
  const info = nodeInfo(this.kernel.model, artifact);
212072
212368
  const revisions = revisionsOf(this.kernel.model, artifact.id);
@@ -216731,8 +217027,18 @@ function wireRecoveredChatTurn(deps) {
216731
217027
  clearInterval(ckptTimer);
216732
217028
  const failed = exitFailed(info);
216733
217029
  if (failed) {
216734
- live.emit({ type: "error", text: exitErrorText(info) });
216735
- itemLedger.recordError(exitErrorText(info));
217030
+ const errText2 = exitErrorText(info);
217031
+ live.emit({ type: "error", text: errText2 });
217032
+ const errorOutcome = itemLedger.recordError(errText2, { ...info.reason ? { reason: info.reason } : {} });
217033
+ live.publishServerItem({
217034
+ itemId: errorOutcome.itemId,
217035
+ itemType: "control",
217036
+ startedVersion: errorOutcome.startedVersion,
217037
+ version: errorOutcome.version,
217038
+ ord: errorOutcome.ord,
217039
+ status: "failed",
217040
+ payload: { code: "error", note: errText2, ...info.reason ? { reason: info.reason } : {} }
217041
+ });
216736
217042
  }
216737
217043
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
216738
217044
  const parts = collectedParts();
@@ -219630,13 +219936,6 @@ var init_memory = __esm({
219630
219936
  async write(ctx, input) {
219631
219937
  this.assertWritable(input, "agent");
219632
219938
  const projectId2 = await this.projectOf(ctx);
219633
- if (input.scope === "project" && projectId2 === null) {
219634
- throw new ApiError(
219635
- 400,
219636
- "MEMORY_NO_PROJECT",
219637
- "\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"
219638
- );
219639
- }
219640
219939
  return this.writeResolved(ctx, input, input.scope === "project" ? projectId2 : null, "agent");
219641
219940
  }
219642
219941
  /** 控制台管理面写入:actorId/projectId 由受保护的管理路由明确给出,不复用 agent 自用入口。 */
@@ -220746,6 +221045,21 @@ var init_connector_skills = __esm({
220746
221045
  }
220747
221046
  });
220748
221047
 
221048
+ // ../server/src/domains/nodes/runtime-tenancy.ts
221049
+ function foreignNodeBlock(node2, expectedCompanyId) {
221050
+ if (!expectedCompanyId) return null;
221051
+ if (!node2) return null;
221052
+ const owner = node2.companyId;
221053
+ if (!owner) return null;
221054
+ if (owner === expectedCompanyId) return null;
221055
+ return `\u8FD0\u884C\u65F6\u8282\u70B9 ${node2.id} \u5C5E\u4E8E\u516C\u53F8 ${owner}\uFF0C\u4E0D\u5C5E\u4E8E ${expectedCompanyId}\u2014\u2014\u5458\u5DE5\u53EA\u80FD\u7ED1\u5B9A/\u6D3E\u53D1\u5230\u672C\u516C\u53F8\u7684\u8282\u70B9\uFF08ADR-0164 \xA75\uFF1A\u8DE8\u516C\u53F8\u5171\u4EAB\u540C\u4E00\u53F0\u7269\u7406\u673A\u7684\u6B63\u89E3\u662F\u5404\u81EA\u767B\u8BB0\u4E0D\u540C node id\uFF09\u3002`;
221056
+ }
221057
+ var init_runtime_tenancy = __esm({
221058
+ "../server/src/domains/nodes/runtime-tenancy.ts"() {
221059
+ "use strict";
221060
+ }
221061
+ });
221062
+
220749
221063
  // ../server/src/domains/actors/routes.ts
220750
221064
  async function asApiError(run) {
220751
221065
  try {
@@ -221068,6 +221382,9 @@ function actorsDomain(opts) {
221068
221382
  const { service } = await resolveCtx(req.auth.companyId);
221069
221383
  const b2 = req.body;
221070
221384
  if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
221385
+ const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
221386
+ const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
221387
+ if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
221071
221388
  const { clearedModel } = await service.bind({ actorId: b2.actorId, nodeId: b2.nodeId, runtimeKind: b2.runtimeKind, status: b2.status ?? "active" }, req.auth.actor);
221072
221389
  return { status: 201, body: { ok: true, ...clearedModel ? { clearedModel } : {} } };
221073
221390
  });
@@ -221877,6 +222194,7 @@ var init_routes3 = __esm({
221877
222194
  init_image_upload();
221878
222195
  init_skill_materializer();
221879
222196
  init_connector_skills();
222197
+ init_runtime_tenancy();
221880
222198
  CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
221881
222199
  RevealRateLimiter = class {
221882
222200
  constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
@@ -222073,7 +222391,7 @@ function createActorsDomain(opts) {
222073
222391
  return {
222074
222392
  service: defaultCtx.service,
222075
222393
  resolveCtx,
222076
- register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {} })
222394
+ register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
222077
222395
  };
222078
222396
  }
222079
222397
  var import_node_crypto49;
@@ -222275,12 +222593,9 @@ function projectsDomain(opts) {
222275
222593
  }
222276
222594
  });
222277
222595
  router.get("/api/projects", async (req) => {
222278
- const inc = req.query?.get("includeEphemeral");
222279
- const includeEphemeral = inc === "1" || inc === "true";
222280
222596
  const uncatRaw = req.query?.get("includeUncategorized");
222281
222597
  const includeUncategorized = uncatRaw === null || uncatRaw === void 0 ? void 0 : !(uncatRaw === "0" || uncatRaw === "false");
222282
222598
  const projects = await (await svc(req)).listProjects({
222283
- includeEphemeral,
222284
222599
  ...includeUncategorized !== void 0 ? { includeUncategorized } : {}
222285
222600
  });
222286
222601
  const page = paginate(projects.map(toApiProject), req.query);
@@ -222318,8 +222633,7 @@ function projectsDomain(opts) {
222318
222633
  body: {
222319
222634
  ok: true,
222320
222635
  project_id: result.projectId,
222321
- moved_work_order_ids: result.movedWorkOrderIds,
222322
- carried_git_location_count: result.carriedGitLocationCount
222636
+ moved_work_order_ids: result.movedWorkOrderIds
222323
222637
  }
222324
222638
  };
222325
222639
  } catch (err) {
@@ -222561,8 +222875,8 @@ ${errs.join("\n")}`);
222561
222875
  const nulledPatch = "project_id" in b2 && b2.project_id === null || "projectId" in b2 && b2.projectId === null || b2.project_id === "null" || b2.project_id === "" || b2.projectId === "null" || b2.projectId === "";
222562
222876
  if (nulledPatch) {
222563
222877
  try {
222564
- const binding = await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222565
- return { status: 200, body: { work_order_id: binding.workOrderId, project_id: binding.projectId } };
222878
+ await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222879
+ return { status: 200, body: { work_order_id: req.params.workOrderId, project_id: null } };
222566
222880
  } catch (err) {
222567
222881
  throw mapProjectError(err);
222568
222882
  }
@@ -222578,8 +222892,8 @@ ${errs.join("\n")}`);
222578
222892
  });
222579
222893
  router.delete("/api/work-orders/:workOrderId/project", async (req) => {
222580
222894
  try {
222581
- const binding = await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222582
- return { status: 200, body: { work_order_id: binding.workOrderId, project_id: binding.projectId } };
222895
+ await (await svc(req)).unlinkWorkorder(req.params.workOrderId);
222896
+ return { status: 200, body: { work_order_id: req.params.workOrderId, project_id: null } };
222583
222897
  } catch (err) {
222584
222898
  throw mapProjectError(err);
222585
222899
  }
@@ -223012,7 +223326,8 @@ function toApiNodeOutputDocument(doc) {
223012
223326
  summary: doc.summary,
223013
223327
  ...doc.contentKind !== void 0 ? { content_kind: doc.contentKind } : {},
223014
223328
  ...doc.size !== void 0 ? { size: doc.size } : {},
223015
- ...doc.files !== void 0 ? { files: doc.files.map((f2) => ({ path: f2.path, ...f2.size !== void 0 ? { size: f2.size } : {} })) } : {}
223329
+ ...doc.files !== void 0 ? { files: doc.files.map((f2) => ({ path: f2.path, ...f2.size !== void 0 ? { size: f2.size } : {} })) } : {},
223330
+ ...doc.updatedAt !== void 0 ? { updated_at: doc.updatedAt } : {}
223016
223331
  };
223017
223332
  }
223018
223333
  function toApiNodeOutputValidation(validation) {
@@ -223096,7 +223411,7 @@ var init_routes5 = __esm({
223096
223411
  "use strict";
223097
223412
  init_router();
223098
223413
  init_src();
223099
- init_ephemeral_project();
223414
+ init_uncategorized_project();
223100
223415
  }
223101
223416
  });
223102
223417
 
@@ -228162,13 +228477,15 @@ async function buildChatPreview(chatStore, sessionId, who) {
228162
228477
  if (lastAsst) lines.push({ label: who, text: clip2(lastAsst.content) });
228163
228478
  return lines.length ? { lines } : void 0;
228164
228479
  }
228165
- async function buildChatInbox(chatStore, markers, me, resolveActor) {
228480
+ async function buildChatInbox(chatStore, markers, me, resolveActor, excludeChildSessions) {
228166
228481
  const sessions = await chatStore.listSessions(me);
228167
228482
  const byScope = new Map(markers.map((m2) => [m2.scope, m2]));
228168
228483
  const unread = sessions.filter((s2) => s2.lastTurnQuality !== void 0 && isUnread(s2.touchedAt, byScope.get(chatScope(s2.id)))).sort((a, b2) => a.touchedAt < b2.touchedAt ? 1 : a.touchedAt > b2.touchedAt ? -1 : 0);
228484
+ const excluded = excludeChildSessions ? new Set(await excludeChildSessions(unread.map((s2) => s2.id)).catch(() => [])) : /* @__PURE__ */ new Set();
228169
228485
  const out = [];
228170
228486
  let previewsFetched = 0;
228171
228487
  for (const s2 of unread) {
228488
+ if (excluded.has(s2.id)) continue;
228172
228489
  const who = resolveActor?.(s2.aiActorId)?.name ?? "\u52A9\u7406";
228173
228490
  const abnormal = s2.lastTurnQuality === "abnormal";
228174
228491
  const preview2 = previewsFetched < PREVIEW_FETCH_CAP ? await buildChatPreview(chatStore, s2.id, who) : void 0;
@@ -229369,7 +229686,7 @@ async function buildResolver(registry2) {
229369
229686
  const byId = new Map(actors.map((a) => [a.id, a]));
229370
229687
  return (id) => {
229371
229688
  const a = byId.get(id);
229372
- return a ? actorRefFieldsOf(a) : systemActorFallback(id);
229689
+ return a ? actorRefFieldsOf(a) : actorRefFallbackOf(id);
229373
229690
  };
229374
229691
  }
229375
229692
  async function buildProjectResolver(artifacts) {
@@ -229558,7 +229875,7 @@ function collabDomain(opts) {
229558
229875
  if (projectFilter) {
229559
229876
  items = items.filter((wo) => {
229560
229877
  const bound = wo.projectId;
229561
- if (isUncategorizedProjectId(projectFilter)) return typeof bound === "string" && isEphemeralProject(bound);
229878
+ if (isUncategorizedProjectId(projectFilter)) return bound == null;
229562
229879
  return bound === projectFilter;
229563
229880
  });
229564
229881
  }
@@ -229964,7 +230281,8 @@ function collabDomain(opts) {
229964
230281
  return void 0;
229965
230282
  }
229966
230283
  })();
229967
- const chatItems = chatStore && myReadMarkers ? await buildChatInbox(chatStore, markers, me, resolveActor) : [];
230284
+ const excludeChildSessions = opts.resolveDelegationChildIds ? (ids2) => opts.resolveDelegationChildIds(req.auth.companyId, ids2) : void 0;
230285
+ const chatItems = chatStore && myReadMarkers ? await buildChatInbox(chatStore, markers, me, resolveActor, excludeChildSessions) : [];
229968
230286
  let waitingItems = [];
229969
230287
  if (engineStore) {
229970
230288
  const snaps = (await Promise.all(
@@ -230259,7 +230577,7 @@ var init_collab = __esm({
230259
230577
  init_workorder_metrics();
230260
230578
  init_chat_workorder_files();
230261
230579
  init_organization_usage();
230262
- init_ephemeral_project();
230580
+ init_uncategorized_project();
230263
230581
  init_tags();
230264
230582
  init_activity2();
230265
230583
  init_node_state();
@@ -230546,24 +230864,16 @@ function makeSeededWorkorderCreator(deps) {
230546
230864
  const blobs = company?.blobs ?? deps.blobs;
230547
230865
  const registry2 = company?.registry ?? deps.registry;
230548
230866
  const artifactState = company ? company.artifactState : deps.artifactState;
230549
- const createProject = company ? company.createProject : deps.createProject;
230867
+ const workorderDrafts = company ? company.workorderDrafts : deps.workorderDrafts;
230550
230868
  const workspace = input.idempotencyKey ? workspaceForKey(input.idempotencyKey) : genWorkspace();
230551
230869
  if (input.idempotencyKey) {
230552
230870
  const existing = existingWorkorder(kernel, workspace);
230553
230871
  if (existing) return existing;
230554
230872
  }
230555
- if (artifactState && (input.projectId || createProject)) {
230556
- await ensureWorkorderProject({
230873
+ if (artifactState && input.projectId) {
230874
+ await bindWorkorderProject({
230557
230875
  workOrderId: workspace,
230558
- ...input.projectId ? { projectId: input.projectId } : {},
230559
- ...input.title ? { title: input.title } : {},
230560
- // ADR 0207:临时项目的创建人 = 建这张单的**真人**。`humanActorId` 是 B 路径 handler
230561
- // 从 `x-chat-session-id` 反查填进来的「陪发起人聊出此单的人」,与 brief.owner 同源;
230562
- // 没有它时只在 `input.actor` 本身是人时才用——剧本由 agent 触发时落 NULL,
230563
- // 不把执行者写成负责人。
230564
- ...input.humanActorId ? { createdBy: input.humanActorId } : input.actor.startsWith("actor:human:") ? { createdBy: input.actor } : {},
230565
- createProject: createProject ?? (async ({ id }) => ({ id })),
230566
- // 上面的守卫保证走不到这个分支
230876
+ projectId: input.projectId,
230567
230877
  upsertBinding: (b2) => artifactState.upsertWorkspaceBinding(b2),
230568
230878
  ...deps.onWarn ? { onWarn: deps.onWarn } : {}
230569
230879
  });
@@ -230595,8 +230905,8 @@ function makeSeededWorkorderCreator(deps) {
230595
230905
  if (!preview2.endState.ok) {
230596
230906
  throw new Error(`\u5267\u672C\u5EFA\u5355\u7EC8\u6001\u6821\u9A8C\u4E0D\u8FC7\uFF1A${preview2.endState.violations.join("\uFF1B")}`);
230597
230907
  }
230598
- if (input.stage === true && deps.workorderDrafts) {
230599
- const draft = await deps.workorderDrafts.submit({
230908
+ if (input.stage === true && workorderDrafts) {
230909
+ const draft = await workorderDrafts.submit({
230600
230910
  workspace,
230601
230911
  plan: planned.plan,
230602
230912
  rootArtifactId: planned.rootArtifactId,
@@ -230658,7 +230968,7 @@ var init_create_seeded_workorder = __esm({
230658
230968
  "../server/src/domains/collab/create-seeded-workorder.ts"() {
230659
230969
  "use strict";
230660
230970
  import_node_crypto57 = require("node:crypto");
230661
- init_ephemeral_project();
230971
+ init_uncategorized_project();
230662
230972
  init_planner();
230663
230973
  enc2 = (s2) => new TextEncoder().encode(s2);
230664
230974
  }
@@ -233772,12 +234082,20 @@ function createChatSessionsDomain(opts) {
233772
234082
  const limitRaw = Number(req.query?.get("limit") ?? "0") || 0;
233773
234083
  const limit = limitRaw > 0 ? Math.min(limitRaw, MAX_SNAPSHOT_ITEMS) : DEFAULT_SNAPSHOT_ITEMS;
233774
234084
  const cursor = opts.liveChat?.cursorFor(chatSessionId) ?? null;
233775
- let turnId = cursor?.turnId ?? null;
233776
- if (!turnId) {
233777
- const turns = await turnsFor(req);
233778
- const active = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
233779
- turnId = active?.id ?? null;
233780
- }
234085
+ const turns = await turnsFor(req);
234086
+ const activeTurnRow = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
234087
+ const turnId = cursor?.turnId ?? activeTurnRow?.id ?? null;
234088
+ const activeTurn = (() => {
234089
+ if (!activeTurnRow) return null;
234090
+ const settled = activeTurnRow.status !== "reserved" && activeTurnRow.status !== "running";
234091
+ if (settled) return { turnId: activeTurnRow.id, phase: activeTurnRow.status };
234092
+ if (opts.liveChat?.isRunning(chatSessionId)) {
234093
+ return { turnId: activeTurnRow.id, phase: "running" };
234094
+ }
234095
+ const startedAt = Date.parse(activeTurnRow.startedAt ?? activeTurnRow.reservedAt);
234096
+ const stale = Number.isFinite(startedAt) && Date.now() - startedAt >= LINK_WINDOWS.chatTurnPhaseStaleAfterMs;
234097
+ return { turnId: activeTurnRow.id, phase: stale ? "interrupted" : "running" };
234098
+ })();
233781
234099
  const snapshotEntries = [];
233782
234100
  let nextSinceVersion = sinceVersion;
233783
234101
  let hasMoreBefore = false;
@@ -233809,6 +234127,7 @@ function createChatSessionsDomain(opts) {
233809
234127
  nextSinceVersion,
233810
234128
  frameCursor: cursor ? { epoch: cursor.epoch, seq: cursor.seq } : { epoch: 0, seq: 0 },
233811
234129
  canAppend: cursor?.canAppend ?? false,
234130
+ activeTurn,
233812
234131
  oldestOrd,
233813
234132
  hasMoreBefore
233814
234133
  };
@@ -233889,6 +234208,9 @@ function createChatSessionsDomain(opts) {
233889
234208
  childSessionId: record8.childSessionId,
233890
234209
  expertActorId: record8.expertActorId,
233891
234210
  ...expert?.name ? { expertName: expert.name } : {},
234211
+ /* 头像与名字同一本名册、同一次查询取出来。缺了它,委派卡与输入框上方状态条只能退回
234212
+ 姓名首字的灰圆(发起人 2026-09-10 报的「灰底 D」)——那两处画的是「[头像] 名字 进行中」。 */
234213
+ ...expert?.avatar ? { expertAvatar: expert.avatar } : {},
233892
234214
  state: record8.state,
233893
234215
  ...record8.failureKind ? { failureKind: record8.failureKind } : {},
233894
234216
  ...record8.label ? { label: record8.label } : {},
@@ -237581,7 +237903,7 @@ function playbooksDomain(opts) {
237581
237903
  const drafts = opts.resolveWorkorderDrafts ? await opts.resolveWorkorderDrafts(req.auth?.companyId) : opts.workorderDrafts;
237582
237904
  if (result.draftId && chatSid && drafts) {
237583
237905
  try {
237584
- await drafts.tagSession(result.draftId, chatSid, opts.liveChat?.runFor(chatSid));
237906
+ await drafts.tagSession(result.draftId, chatSid, opts.liveChat?.runFor(chatSid), result.workspace);
237585
237907
  } catch (err) {
237586
237908
  console.warn(`[playbooks] tagSession(${result.draftId}) \u5931\u8D25\uFF0C\u4E0D\u963B\u585E\u5EFA\u5355\u54CD\u5E94:`, err);
237587
237909
  }
@@ -238328,7 +238650,10 @@ var init_daemon_adapter = __esm({
238328
238650
  } else if (msg.type === "session_normalized_event") {
238329
238651
  this.confirmDelivery(msg.dispatchId, "started");
238330
238652
  const entry = this.pending.get(msg.dispatchId);
238331
- if (!entry) return;
238653
+ if (!entry) {
238654
+ if (this.stashEnabled) this.stashFrame(msg.dispatchId, { type: "normalized", event: msg.event });
238655
+ return;
238656
+ }
238332
238657
  if (entry.normalizedCbs.length === 0) entry.normalizedBacklog.push(msg.event);
238333
238658
  else for (const cb of entry.normalizedCbs) cb(msg.event);
238334
238659
  } else if (msg.type === "session_output") {
@@ -238461,6 +238786,7 @@ var init_daemon_adapter = __esm({
238461
238786
  let replayOutput;
238462
238787
  let replayTelemetry;
238463
238788
  let replayLiveEvent;
238789
+ let replayNormalized;
238464
238790
  const flushReplay = () => {
238465
238791
  while (stashed.frames.length > 0) {
238466
238792
  const frame = stashed.frames[0];
@@ -238472,6 +238798,10 @@ var init_daemon_adapter = __esm({
238472
238798
  if (!replayTelemetry) return;
238473
238799
  stashed.frames.shift();
238474
238800
  replayTelemetry(frame.event);
238801
+ } else if (frame.type === "normalized") {
238802
+ if (!replayNormalized) return;
238803
+ stashed.frames.shift();
238804
+ replayNormalized(frame.event);
238475
238805
  } else {
238476
238806
  if (!replayLiveEvent) return;
238477
238807
  stashed.frames.shift();
@@ -238510,9 +238840,11 @@ var init_daemon_adapter = __esm({
238510
238840
  },
238511
238841
  ...supportsNormalizedEvents ? {
238512
238842
  onNormalizedProviderEvent(cb) {
238513
- entry.normalizedCbs.push(cb);
238843
+ replayNormalized ??= cb;
238844
+ flushReplay();
238514
238845
  const backlog = entry.normalizedBacklog.splice(0, entry.normalizedBacklog.length);
238515
238846
  for (const e of backlog) cb(e);
238847
+ entry.normalizedCbs.push(cb);
238516
238848
  }
238517
238849
  } : {},
238518
238850
  // ADR-0093 对话追加(重挂句柄同 spawn 句柄)。
@@ -241995,6 +242327,9 @@ var init_postgres_projects = __esm({
241995
242327
  const r = await this.pool.query(`SELECT * FROM ${this.s}.workspace_bindings`);
241996
242328
  return r.rows.map(rowToWorkspaceBinding);
241997
242329
  }
242330
+ async deleteWorkspaceBinding(workOrderId) {
242331
+ await this.pool.query(`DELETE FROM ${this.s}.workspace_bindings WHERE work_order_id = $1`, [workOrderId]);
242332
+ }
241998
242333
  /* ---------- 任务输入文件(PRD §2.6.3 建单交接;薄旁存,非 oplog) ---------- */
241999
242334
  async addWorkOrderFile(file) {
242000
242335
  await this.pool.query(
@@ -255472,8 +255807,8 @@ var init_cutover_aware_workorder_drafts = __esm({
255472
255807
  async snapshotForNewRun(d, runId) {
255473
255808
  return (await this.pickWritable()).snapshotForNewRun(d, runId);
255474
255809
  }
255475
- async tagSession(id, s2, runId) {
255476
- return (await this.pickWritable()).tagSession(id, s2, runId);
255810
+ async tagSession(id, s2, runId, expectedWorkspace) {
255811
+ return (await this.pickWritable()).tagSession(id, s2, runId, expectedWorkspace);
255477
255812
  }
255478
255813
  async markEdited(id) {
255479
255814
  return (await this.pickWritable()).markEdited(id);
@@ -256444,6 +256779,7 @@ var init_src11 = __esm({
256444
256779
  init_sender();
256445
256780
  init_routes7();
256446
256781
  init_runtime_drift();
256782
+ init_runtime_tenancy();
256447
256783
  init_types4();
256448
256784
  init_roles();
256449
256785
  init_node_store();
@@ -256495,7 +256831,7 @@ var init_src11 = __esm({
256495
256831
  init_connector_skills();
256496
256832
  init_connector_tool_versions();
256497
256833
  init_board_watch();
256498
- init_ephemeral_project();
256834
+ init_uncategorized_project();
256499
256835
  init_run_settlement();
256500
256836
  init_reconcile_terminal_runs();
256501
256837
  init_run_settlement_wiring();
@@ -258007,6 +258343,9 @@ async function startServe(opts) {
258007
258343
  let memStoreShared;
258008
258344
  let isKnowledgeQueryAvailable = async (_actorId) => false;
258009
258345
  const actors = createActorsDomain({
258346
+ // 租户闸(ADR-0164 §5):`POST /api/bindings` 据它拦「把本公司员工绑到别家公司的机器上」。
258347
+ // 闭包读当前 nodeStore(下方 PG 分支会重赋值),与 `listBindingModelIds` 同一写法。
258348
+ resolveNodeTenancy: async (nodeId) => nodeStore.getNode(nodeId),
258010
258349
  store: registryStore,
258011
258350
  oplog,
258012
258351
  kernel: defaultCompanyKernel,
@@ -258075,7 +258414,10 @@ async function startServe(opts) {
258075
258414
  prefsStore: humanPrefsStore,
258076
258415
  registry: registryStore,
258077
258416
  actors: actors.service,
258078
- listRuntimes: (nodeId) => nodeStore.listRuntimes(nodeId),
258417
+ // 默认公司那一份。**闭包求值**:`defaultCompanyId` let,startup 后才被库里的真实 id 覆盖
258418
+ // (同 :1575 那句「必须是调用时求值」)。
258419
+ companyId: () => defaultCompanyId,
258420
+ listRuntimes: (nodeId, scope) => nodeStore.listRuntimes(nodeId, scope),
258079
258421
  isPlatformAdmin: async () => false
258080
258422
  // 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
258081
258423
  });
@@ -258367,8 +258709,6 @@ async function startServe(opts) {
258367
258709
  getSchema: () => schema,
258368
258710
  registry: registryStore,
258369
258711
  artifactState: artifactStateStore,
258370
- // 决策 C:剧本建单没给 projectId 时,兜底建一个由工单派生的临时项目并绑上。
258371
- createProject: makeEnsureProjectFromStore(projectStateStore),
258372
258712
  onWarn: (m2) => console.warn(m2),
258373
258713
  workorderDrafts,
258374
258714
  buildDraftGraph: async (draft) => buildWorkorderDraftDetail(draft, await buildResolver(registryStore), buildTypeReviewerResolver(schema, await registryStore.listActors())).graph,
@@ -258390,8 +258730,12 @@ async function startServe(opts) {
258390
258730
  blobs: engine2.blobs,
258391
258731
  registry: engine2.registry ?? registryStore,
258392
258732
  artifactState: stores.artifacts,
258393
- // 兜底临时项目也要建在本公司的库里,否则绑定两端分处两个 schema。
258394
- createProject: makeEnsureProjectFromStore(stores.projects)
258733
+ // 草案 store 必须按公司拿(2026-09-10 事故):此前它是装配期捕获的共享库实例,
258734
+ // `stage: true` 建出来的草案落大通铺、读侧却按公司解析,本公司永远查不到自己的草案。
258735
+ // ⚠ 这个 helper 的 const 声明在本处之后,只能在**请求时**才求值的闭包里引用
258736
+ // (同上方 resolveChatSession 的写法),直接在装配期引用会撞 TDZ。
258737
+ // planner 阻断项旁账**不在本次范围**,理由见 create-seeded-workorder.ts 里那段注释。
258738
+ workorderDrafts: await workorderDraftsFor(companyId)
258395
258739
  };
258396
258740
  }
258397
258741
  } : {}
@@ -259702,7 +260046,9 @@ async function startServe(opts) {
259702
260046
  prefsStore: plane?.humanPrefs ?? humanPrefsStore,
259703
260047
  registry: engine2.registry,
259704
260048
  actors: ctx.service,
259705
- listRuntimes: (nodeId) => nodeStore.listRuntimes(nodeId),
260049
+ // 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
260050
+ companyId: () => cid,
260051
+ listRuntimes: (nodeId, scope) => nodeStore.listRuntimes(nodeId, scope),
259706
260052
  isPlatformAdmin: async (caller) => {
259707
260053
  const member = await controlPlaneStore.getMember(cid, caller);
259708
260054
  return member?.role === "owner";
@@ -259796,6 +260142,14 @@ async function startServe(opts) {
259796
260142
  // ADR-0086:chat 知会条目(与 readMarkers 成对)
259797
260143
  // ADR「多租户数据面收口」§D2:知会条目按**当前公司**取。装了路由才给(dev 文件模式沿用单实例)。
259798
260144
  ...chatStoreRouter ? { resolveChatSession: chatStoreFor } : {},
260145
+ /* 委派子会话不进铃铛(发起人 2026-09-10):专家跑完回流到父会话,父会话那条已经通知过一次。
260146
+ 判据与左侧会话列表那条路同一个读口(`listChildSessionIds`,见下面 chat-sessions 域的
260147
+ `delegations` 接线),不另造。灰度关着 → 回空集合 = 不滤,与接入前逐字一致。 */
260148
+ resolveDelegationChildIds: async (companyId, sessionIds) => {
260149
+ if (!chatDelegationEnabledFromEnv() || sessionIds.length === 0) return [];
260150
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
260151
+ return ledger ? ledger.listChildSessionIds(sessionIds) : [];
260152
+ },
259799
260153
  readMarkers: readMarkerStore,
259800
260154
  // bundle=`inbox`:水位按当前公司取(与上面 resolveChatSession 成对)。
259801
260155
  ...pgPool ? { resolveReadMarkers: readMarkerStoreFor } : {},
@@ -260007,7 +260361,9 @@ async function startServe(opts) {
260007
260361
  listProjects: (companyId) => companyId === defaultCompanyId ? projectStateStore.listProjects() : Promise.resolve([]),
260008
260362
  listProjectMembers: async (companyId, projectId2) => companyId === defaultCompanyId ? (await projectStateStore.listProjectMembers(projectId2)).flatMap((member) => member.kind === "system" ? [] : [{ actorId: member.actorId, kind: member.kind, status: member.status === "active" ? "active" : "inactive" }]) : [],
260009
260363
  getWorkspaceBinding: (companyId, workorderId) => companyId === defaultCompanyId ? artifactStateStore.getWorkspaceBinding(workorderId) : Promise.resolve(null),
260010
- isEphemeralProject: (companyId, projectId2) => companyId !== defaultCompanyId || isEphemeralProject(projectId2)
260364
+ // 非默认公司暂不支持项目级知识作用域(这里只有默认公司的 projectStateStore/名册可读),
260365
+ // 一律降级到组织作用域。旧版这个谓词还兼着"是不是 proj_tmp_ 临时项目",临时项目退役后只剩这一半。
260366
+ isProjectScopeUnsupported: (companyId) => companyId !== defaultCompanyId
260011
260367
  },
260012
260368
  actor: async (actorId, organizationId) => {
260013
260369
  if (actorId === SYSTEM_ACTOR2) return { id: actorId, kind: "system", status: "active" };
@@ -260512,7 +260868,9 @@ async function startServe(opts) {
260512
260868
  }
260513
260869
  binding = currentBinding;
260514
260870
  }
260515
- const requireDispatchNode = (target) => {
260871
+ const requireDispatchNode = async (target) => {
260872
+ const foreign = foreignNodeBlock(await nodeStore.getNode(target.nodeId).catch(() => null), runCompanyId);
260873
+ if (foreign) throw new ApiError(409, "NODE_NOT_IN_COMPANY", foreign);
260516
260874
  const connected = hub?.connectedDaemons().find((d) => d.daemonId === target.nodeId);
260517
260875
  if (!connected) {
260518
260876
  throw new ApiError(409, "NODE_OFFLINE", `\u7ED1\u5B9A\u8282\u70B9 ${target.nodeId} \u5F53\u524D\u4E0D\u5728\u7EBF`);
@@ -260522,7 +260880,7 @@ async function startServe(opts) {
260522
260880
  }
260523
260881
  return connected;
260524
260882
  };
260525
- let node2 = requireDispatchNode(binding);
260883
+ let node2 = await requireDispatchNode(binding);
260526
260884
  if (!chatRemoteAdapter) {
260527
260885
  throw new ApiError(503, "NODE_GATEWAY_NOT_READY", "node-gateway \u5C1A\u672A\u5C31\u7EEA");
260528
260886
  }
@@ -260602,7 +260960,7 @@ async function startServe(opts) {
260602
260960
  }
260603
260961
  if (stableDispatch.target.nodeId !== binding.nodeId || stableDispatch.target.runtimeKind !== binding.runtimeKind) {
260604
260962
  binding = { actorId, ...stableDispatch.target, status: "active" };
260605
- node2 = requireDispatchNode(binding);
260963
+ node2 = await requireDispatchNode(binding);
260606
260964
  requireNodeConnectors();
260607
260965
  }
260608
260966
  }
@@ -261955,6 +262313,8 @@ async function startServe(opts) {
261955
262313
  checkDispatchable: async ({ actor, binding }) => {
261956
262314
  try {
261957
262315
  if (!binding.nodeId) return null;
262316
+ const foreign = foreignNodeBlock(await nodeStore.getNode(binding.nodeId).catch(() => null), companyId);
262317
+ if (foreign) return { reason: "node-not-in-company", errorMessage: foreign };
261958
262318
  const drift = runtimeDriftBlock(await nodeStore.listRuntimes(binding.nodeId), binding.nodeId, binding.runtimeKind);
261959
262319
  if (drift) return drift;
261960
262320
  const health = healthOfNode(binding.nodeId);
@@ -269113,6 +269473,9 @@ ${res.warning}`);
269113
269473
  ...expect !== void 0 ? { expectedVersion: Number(expect) } : {}
269114
269474
  });
269115
269475
  println(`\u5DF2\u8BB0\u4E0B ${rec.memId}\uFF08v${rec.version}\uFF0C${rec.projectId === null ? "actor \u7EA7\xB7\u8DE8\u9879\u76EE\u901A\u7528" : `\u9879\u76EE ${rec.projectId}`}\uFF09`);
269476
+ if ((flags.get("scope") ?? "actor") === "project" && rec.projectId === null) {
269477
+ println(" \u24D8 \u672C\u8F6E\u89E3\u6790\u4E0D\u51FA\u6240\u5C5E\u9879\u76EE\uFF08\u8FD9\u5F20\u4EFB\u52A1\u6CA1\u6302\u9879\u76EE\uFF09\uFF0C\u5DF2\u6309 actor \u7EA7\u8BB0\u4E0B\u2014\u2014\u60F3\u8BA9\u5B83\u843D\u5230\u9879\u76EE\u540D\u4E0B\uFF0C\u5148\u7ED9\u4EFB\u52A1\u6307\u5B9A\u9879\u76EE\u3002");
269478
+ }
269116
269479
  } catch (err) {
269117
269480
  const body2 = err instanceof ApiRequestError ? err.body : void 0;
269118
269481
  if (body2?.current) {
@@ -270671,7 +271034,7 @@ function shimScript() {
270671
271034
  }
270672
271035
 
270673
271036
  // src/index.ts
270674
- var PKG_VERSION = true ? "2.2.9" : "dev";
271037
+ var PKG_VERSION = true ? "2.2.11" : "dev";
270675
271038
  var LOCAL_BIN = localBin();
270676
271039
  var NPM_PREFIX = npmPrefix();
270677
271040
  var INSTANCE = DEFAULT_INSTANCE;