oasis_test_v2 2.2.17 → 2.2.19

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 +497 -63
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5737,6 +5737,26 @@ CREATE TABLE IF NOT EXISTS ${q2}.event_handler_log (
5737
5737
  PRIMARY KEY (event_seq, handler_name)
5738
5738
  );
5739
5739
 
5740
+ -- \u5F85\u4EBA\u786E\u8BA4\u7684 intervention \u8349\u6848\u5361\uFF08\u547D\u4EE4\u6388\u6743\u5361 / \u6539\u56FE\u5361\uFF09\u3002
5741
+ --
5742
+ -- \u4E3A\u4EC0\u4E48\u8981\u843D\u5E93\uFF082026-09-11 \u5B9E\u5F55\uFF09\uFF1A\u65E7\u5F15\u64CE\u628A\u8D77\u8349\u8BB0\u6210\u4E00\u6761 propose_intervention op \u8FDB oplog\uFF0C
5743
+ -- \u91CD\u542F replay \u5C31 fold \u56DE pendingReviews\uFF1B\u51B3\u7B56 0130 \u9000\u5F79 oplog \u65F6\u8FD9\u4E00\u6BB5\u53EA\u642C\u4E86\u5185\u5B58 Map\uFF0C
5744
+ -- \u6CE8\u91CA\u8FD8\u5199\u7740\u300C\u77ED\u751F\u547D\u5468\u671F\u3001\u91CD\u542F\u4E22\u53EF\u63A5\u53D7\u300D\u3002\u4F46\u8FD9\u5F20\u5361\u7684\u8BED\u4E49\u662F**\u7B49\u4E00\u4E2A\u771F\u4EBA\u6765\u70B9**\uFF0C\u5BFF\u547D\u7531\u4EBA\u51B3\u5B9A\uFF0C
5745
+ -- \u4E0D\u7531\u8FDB\u7A0B\u51B3\u5B9A\u3002v2 \u4E0A 31 \u5C0F\u65F6\u91CD\u542F\u4E86 158 \u6B21\uFF08\u22485 \u6B21/\u5C0F\u65F6\uFF09\uFF0C\u5B9E\u6D4B\u4E00\u5F20\u5361\u8D77\u8349 24 \u79D2\u540E\u5C31\u88AB\u91CD\u542F\u62B9\u6389\uFF0C
5746
+ -- \u534F\u8C03\u8005\u5374\u4ECD\u5728\u5BF9\u8BDD\u91CC\u8BA9\u4EBA\u300C\u53BB\u6536\u4EF6\u7BB1\u627E\u90A3\u5F20\u5361\u300D\u2014\u2014\u804A\u5929\u9875\u548C\u6536\u4EF6\u7BB1\u8BFB\u7684\u662F\u540C\u4E00\u4EFD\u5185\u5B58\uFF0C\u4E00\u8D77\u53D8\u7A7A\u3002
5747
+ --
5748
+ -- card \u5217\u7528 jsonb \u6574\u5B58\uFF1A\u5361\u4F53\u7684\u5F62\u72B6\u5F52 core\uFF08KernelBridge.draftCards\uFF09\u7BA1\uFF0C\u5F15\u64CE\u53EA\u8D1F\u8D23\u5B58\u53D6\uFF0C
5749
+ -- \u4E0D\u8BA4\u91CC\u9762\u7684\u4E1A\u52A1\u5B57\u6BB5\u2014\u2014\u5426\u5219\u6BCF\u52A0\u4E00\u4E2A\u5B57\u6BB5\u90FD\u8981\u52A8 DDL\uFF0C\u800C\u5F15\u64CE\u4E0D\u8BE5\u77E5\u9053 InterventionPlan \u662F\u4EC0\u4E48\u3002
5750
+ -- \u7D22\u5F15\u4F4D\u53EA\u7559\u6392\u969C\u8981\u7528\u7684\u90A3\u4E24\u5217\u3002\u4E0D\u52A0 workorder_id \u5916\u952E\uFF1A\u5361\u53EF\u4EE5\u6307\u5411\u4E00\u4E2A\u521A\u88AB\u4F5C\u5E9F\u7684\u8282\u70B9\uFF0C
5751
+ -- \u5916\u952E\u4F1A\u8BA9\u300C\u8D77\u8349\u300D\u8FD9\u4E00\u6B65\u76F4\u63A5\u5931\u8D25\uFF0C\u6BD4\u7559\u4E00\u6761\u5B64\u513F\u5361\u7CDF\u5F97\u591A\u3002
5752
+ CREATE TABLE IF NOT EXISTS ${q2}.intervention_drafts (
5753
+ draft_id text PRIMARY KEY,
5754
+ workorder_id text,
5755
+ created_at timestamptz NOT NULL,
5756
+ card jsonb NOT NULL
5757
+ );
5758
+ CREATE INDEX IF NOT EXISTS intervention_drafts_wo_idx ON ${q2}.intervention_drafts (workorder_id);
5759
+
5740
5760
  -- \u2500\u2500 \u8FC1\u79FB\uFF1A\u65E2\u6709\u90E8\u7F72\u7684\u5217\u589E\u91CF\uFF08CREATE TABLE IF NOT EXISTS \u4E0D\u52A0\u5217\uFF0C\u8FD9\u91CC\u8865\uFF09\u2500\u2500
5741
5761
  -- \u2605 review_requirements.source \u6269\u4E3A\u56DB\u503C\uFF08+'closure'\uFF09\uFF1A\u7ED3\u6848\u4EBA\u95F8\u7531 scan \u5728\u6536\u53E3\u90A3\u4E00\u523B\u5199\u5165\uFF0C\u4E0E manual \u5206\u5BB6\u3002
5742
5762
  -- \u5199\u6CD5\u4E0E\u4E0B\u65B9 works_lane_check \u540C\u6B3E\uFF08\u5148\u5220\u540E\u52A0\uFF0C\u6210\u5BF9\u5E42\u7B49\uFF09\u3002
@@ -11278,6 +11298,38 @@ var init_store_postgres = __esm({
11278
11298
  createdAt: iso(r.createdAt)
11279
11299
  }));
11280
11300
  }
11301
+ /* ── intervention 草案卡(见 store.ts 的接口注释)。与上面两口钟同类:直连 pool 的窄读写,
11302
+ 标识符位一律经 quoteIdent——带连字符的公司 schema 裸拼会 syntax error。 ── */
11303
+ async listInterventionDrafts() {
11304
+ const res = await this.pool.query(
11305
+ `SELECT draft_id AS "draftId", workorder_id AS "workorderId", created_at AS "createdAt", card
11306
+ FROM ${quoteIdent(this.schema)}.intervention_drafts
11307
+ ORDER BY created_at`
11308
+ );
11309
+ return res.rows.map((r) => ({
11310
+ draftId: r.draftId,
11311
+ workorderId: r.workorderId ?? null,
11312
+ createdAt: iso(r.createdAt),
11313
+ card: r.card
11314
+ }));
11315
+ }
11316
+ async saveInterventionDraft(record8) {
11317
+ await this.pool.query(
11318
+ `INSERT INTO ${quoteIdent(this.schema)}.intervention_drafts (draft_id, workorder_id, created_at, card)
11319
+ VALUES ($1, $2, $3::timestamptz, $4::jsonb)
11320
+ ON CONFLICT (draft_id) DO UPDATE
11321
+ SET workorder_id = EXCLUDED.workorder_id,
11322
+ created_at = EXCLUDED.created_at,
11323
+ card = EXCLUDED.card`,
11324
+ [record8.draftId, record8.workorderId, record8.createdAt, JSON.stringify(record8.card ?? null)]
11325
+ );
11326
+ }
11327
+ async deleteInterventionDraft(draftId) {
11328
+ await this.pool.query(
11329
+ `DELETE FROM ${quoteIdent(this.schema)}.intervention_drafts WHERE draft_id = $1`,
11330
+ [draftId]
11331
+ );
11332
+ }
11281
11333
  };
11282
11334
  PgTx = class {
11283
11335
  constructor(c, s2) {
@@ -15588,7 +15640,7 @@ ${m2.spec.split("\n").map((l) => ` > ${l}`).join("\n")}`);
15588
15640
  `- \u7F3A\u4E00\u4E2A**\u8FD8\u4E0D\u5B58\u5728\u7684\u524D\u7F6E** \u2192 \`oasis gap\` \u4EA4\u4E0A\u53BB\uFF0C**\u522B\u786C\u7F16**\uFF1B\u5DF2\u505A\u5B8C\u7684\u90E8\u5206\u7167\u5E38 \`propose\`\uFF0C\u4F46\u8FD9\u4E00\u8F6E \`conclude\` \u4F1A\u88AB\u62D2\uFF0C\u63D0\u4EA4\u5B8C\u5373\u6536\u5DE5\u2014\u2014\u524D\u7F6E\u5C31\u7EEA\u540E\u7CFB\u7EDF\u4F1A\u5E26\u7740\u89E3\u6CD5\u8BF4\u660E\u91CD\u65B0\u5524\u8D77\u4F60\u3002`,
15589
15641
  `- \u5BF9**\u4E0A\u6E38**\u6709\u610F\u89C1\u2014\u2014\u6709\u7591\u95EE\u3001\u8981\u5B83\u6539\u3001\u5224\u5B83\u6CA1\u8FBE\u6807 \u2192 \`oasis annotate <\u4E0A\u6E38id> --body "<\u8BF4\u6E05\u695A>"\` \u540E**\u6536\u5DE5**\u3002\u5199\u7ED9\u4E0A\u6E38\u7684\u8BC4\u8BBA\u81EA\u52A8\u5E26\u963B\u585E\u529B\uFF1A**\u5B83\u7B54\u590D\u524D\u4E0D\u80FD\u6536\u5C3E**\uFF0C\u7B54\u590D\u540E\u7CFB\u7EDF\u91CD\u6D3E\u4F60\u3002`,
15590
15642
  ...args.part === void 0 ? [`- \u6D3B\u592A\u5927 / \u53EF\u5E76\u884C \u2192 \`oasis add-revision\` \u62C6\u6210\u5B50\u90E8\u4EF6\u6D3E\u51FA\u53BB\uFF0C\u522B\u4E00\u4EBA\u4E00\u4F1A\u8BDD\u786C\u625B\u3002`] : [],
15591
- ...batchedContinuation && args.part === void 0 ? [`- \u957F\u4EFB\u52A1\u5DF2\u5141\u8BB8\u5206\u6279\u7EED\u8DD1\uFF1A\u4E00\u6279\u6709\u8FDB\u5C55\u4F46\u6574\u4F53\u672A\u5B8C\u6210\u65F6\uFF0C\u6309\u300C\u600E\u4E48\u63D0\u4EA4\u300D\u91CC\u7684 propose \u547D\u4EE4\u8FFD\u52A0 \`--checkpoint --continue\`\uFF0Creason \u5199\u6E05\u672C\u6279\u5B8C\u6210\u4E86\u4EC0\u4E48\u3001\u5269\u4EC0\u4E48\u3001\u4E0B\u6279\u4ECE\u54EA\u7EE7\u7EED\uFF0C\u7136\u540E\u6536\u5DE5\uFF1B\u7CFB\u7EDF\u4F1A\u6D3E\u53D1\u4E0B\u4E00\u8F6E\u6B63\u5F0F session\u3002\u5168\u90E8\u5B8C\u6210\u65F6\u7167\u5E38 propose \u540E \`oasis conclude ${artifactId}\`\u3002`] : [],
15643
+ ...batchedContinuation && args.part === void 0 ? [`- \u957F\u4EFB\u52A1\u5DF2\u5141\u8BB8\u5206\u6279\u7EED\u8DD1\uFF1A\u4E00\u6279\u6709\u8FDB\u5C55\u4F46\u6574\u4F53\u672A\u5B8C\u6210\u65F6\uFF0C\u6309\u300C\u600E\u4E48\u63D0\u4EA4\u300D\u91CC\u7684 propose \u547D\u4EE4\u8FFD\u52A0 \`--checkpoint --continue\`\uFF0Creason \u5199\u6E05\u672C\u6279\u5B8C\u6210\u4E86\u4EC0\u4E48\u3001\u5269\u4EC0\u4E48\u3001\u4E0B\u6279\u4ECE\u54EA\u7EE7\u7EED\uFF0C\u7136\u540E\u6536\u5DE5\uFF1B\u7CFB\u7EDF\u4F1A\u6D3E\u53D1\u4E0B\u4E00\u8F6E\u6B63\u5F0F session\u3002\u5168\u90E8\u5B8C\u6210\u65F6\u7167\u5E38 propose \u540E \`oasis conclude ${artifactId} --note "<\u4EA4\u4ED8\u7ED3\u8BBA>"\`\u3002`] : [],
15592
15644
  `- \u5199\u5165\u53EA\u8D70 \`oasis propose\`\uFF0C\u771F\u6B63\u5B8C\u6574\u4E86\u624D \`oasis conclude\`\uFF1B\u5361\u6CA1\u5361\u4F4F\u3001\u8FDB\u5EA6\u90FD\u662F\u7CFB\u7EDF\u7B97\u7684\uFF0C\u522B\u81EA\u5DF1\u7EF4\u62A4\u3002`,
15593
15645
  `- \u4F46**\u7B80\u5355\u3001\u81EA\u8DB3\u3001\u4E0D\u4F9D\u8D56\u672A\u5B9A\u51B3\u7B56\u7684\u4EFB\u52A1**\uFF08\u5982\u6539\u4E2A\u80CC\u666F\u8272\uFF09\u2192 \u76F4\u63A5\u505A\uFF0C\u5224\u65AD\u6743\u5728\u4F60\u3002`,
15594
15646
  ...typeDef?.recommendedInputs && typeDef.recommendedInputs.length > 0 ? [`- \u672C\u7C7B\u578B\u5E38\u89C1\u4E0A\u6E38\u4F9D\u8D56\uFF08\u63A8\u8350\uFF0C\u975E\u5F3A\u5236\uFF09\uFF1A${typeDef.recommendedInputs.join("\u3001")}\u2014\u2014\u4F9D\u8D56\u5176\u4E2D\u67D0\u4E2A\u800C\u5B83\u8FD8\u6CA1\u6709\uFF0C\u6309\u4E0A\u9762 gap \u89C4\u5219\u62A5\u7F3A\u53E3\u3002`] : []
@@ -19353,8 +19405,14 @@ var init_kernel_bridge = __esm({
19353
19405
  /** 下一轮刷新是否顺带重扫一小片"不脏"的工单。只由 `refreshModel({ resync: true })`(读模型时钟)置位。 */
19354
19406
  resyncRequested = false;
19355
19407
  /**
19356
- * intervention 待授权卡(内存态)。旧引擎存 oplog + fold 出 pendingReviews;新引擎无 oplog,
19357
- * 用内存 map 承载同样的「propose → 人等确认 → apply-draft/reject」流。短生命周期、重启丢可接受。
19408
+ * intervention 待授权卡。内存 map 是**读路径的缓存**,真源在库里(`intervention_drafts` 表,
19409
+ * engine/schema.ts)——`open()` 启动时 rehydrate,propose 写库,apply/reject 删库。
19410
+ *
19411
+ * 旧引擎把起草记成一条 `propose_intervention` op 进 oplog,重启 replay 就 fold 回 pendingReviews;
19412
+ * 决策 0130 退役 oplog 时这一段只搬了内存 Map,注释写着「短生命周期、重启丢可接受」。
19413
+ * 2026-09-11 实录证伪了这句:v2 上 31 小时重启 158 次,一张卡起草 24 秒后就被抹掉,
19414
+ * 而协调者仍在对话里让人「去收件箱找那张卡」——聊天页和收件箱读的是同一份 `pendingReviews`,
19415
+ * 一起变空。这张卡的寿命由**等它的那个人**决定,不由进程决定,所以必须落库。
19358
19416
  */
19359
19417
  draftCards = /* @__PURE__ */ new Map();
19360
19418
  model;
@@ -19374,6 +19432,14 @@ var init_kernel_bridge = __esm({
19374
19432
  for (const record8 of records) kernel.markWorkorderDirty(record8.workorderId);
19375
19433
  });
19376
19434
  bus.onApplied((wid) => kernel.markWorkorderDirty(wid));
19435
+ try {
19436
+ for (const rec of await store.listInterventionDrafts()) {
19437
+ const card2 = rec.card;
19438
+ kernel.draftCards.set(rec.draftId, { ...card2, at: card2?.at || rec.createdAt });
19439
+ }
19440
+ } catch (err) {
19441
+ console.warn(`[kernel-bridge] open \u6062\u590D\u5F85\u6388\u6743\u5361\u5931\u8D25\uFF08\u672C\u6B21\u91CD\u542F\u7684\u5B58\u91CF\u5361\u4F1A\u4E22\uFF09\uFF1A${String(err)}`);
19442
+ }
19377
19443
  try {
19378
19444
  const all2 = await store.transaction((tx) => tx.listAllWorkorders());
19379
19445
  for (const id of all2) kernel.trackedWorkorders.add(id);
@@ -19384,6 +19450,39 @@ var init_kernel_bridge = __esm({
19384
19450
  return kernel;
19385
19451
  }
19386
19452
  /* ─────────────────────── 内部工具 ─────────────────────── */
19453
+ /**
19454
+ * 起草落库。**写失败不阻断起草**——卡已经在内存里、已经显示给人了,这时候抛错会让
19455
+ * `oasis force-conclude` 整条命令失败,比"这张卡撑不过下次重启"糟得多。退化成本次改动之前的行为。
19456
+ */
19457
+ async persistDraftCard(draftId, card2) {
19458
+ try {
19459
+ await this.store.saveInterventionDraft({
19460
+ draftId,
19461
+ workorderId: this.workorderOfCard(card2),
19462
+ createdAt: card2.at,
19463
+ card: card2
19464
+ });
19465
+ } catch (err) {
19466
+ console.warn(`[kernel-bridge] \u5F85\u6388\u6743\u5361\u843D\u5E93\u5931\u8D25 ${draftId}\uFF08\u91CD\u542F\u540E\u4F1A\u4E22\uFF09\uFF1A${String(err)}`);
19467
+ }
19468
+ }
19469
+ /** 卡已终局(人点了确认或驳回)→ 删库。失败只记日志:内存已出队,残留行会在下次 rehydrate 后
19470
+ * 被 apply/reject 的「卡不存在」分支挡掉,不会让已执行的命令被再执行一次。 */
19471
+ async forgetDraftCard(draftId) {
19472
+ try {
19473
+ await this.store.deleteInterventionDraft(draftId);
19474
+ } catch (err) {
19475
+ console.warn(`[kernel-bridge] \u5F85\u6388\u6743\u5361\u5220\u5E93\u5931\u8D25 ${draftId}\uFF1A${String(err)}`);
19476
+ }
19477
+ }
19478
+ /** 卡挂在哪张工单下(排障列用,允许为空)。锚点可能指向一个模型里已经没有的产物。 */
19479
+ workorderOfCard(card2) {
19480
+ const fromPlan = card2.plan?.workspace;
19481
+ if (fromPlan) return fromPlan;
19482
+ const anchor = card2.anchor;
19483
+ if (!anchor) return null;
19484
+ return this.model.artifacts.get(anchor)?.workspace ?? null;
19485
+ }
19387
19486
  /** artifactId → 所属 workorderId(从 model 查)。 */
19388
19487
  wo(artifactId) {
19389
19488
  const a = this.model.artifacts.get(artifactId);
@@ -19554,7 +19653,8 @@ var init_kernel_bridge = __esm({
19554
19653
  workspace: cardWs,
19555
19654
  advisory: [],
19556
19655
  downstreamClosure: [],
19557
- at: "",
19656
+ // 真实起草时刻。曾经恒为 "":收件箱那行因此时间列印 "-"、排序键空串最小、恒沉到列表最后一行。
19657
+ at: card2.at ?? "",
19558
19658
  // ★ anchor 也兜底到 plan ops 的产物 id——inbox change-confirm 用 r.anchor 找「anchor owner 的
19559
19659
  // 人类上级」(leadOf(r.anchor));graph-edit 卡之前恒空、boss 收不到卡(collab-inbox-audit 实测)。
19560
19660
  anchor: anchorId || card2.anchor || "",
@@ -21007,30 +21107,33 @@ var init_kernel_bridge = __esm({
21007
21107
  async proposeIntervention(plan, actor, opts) {
21008
21108
  const preview2 = await previewIntervention(this.model, plan, { schema: this.schema, roleIndex: this.roleIndex });
21009
21109
  const draftId = `draft:${(0, import_node_crypto3.randomUUID)()}`;
21010
- this.draftCards.set(draftId, {
21110
+ const card2 = {
21011
21111
  kind: "graph-edit",
21012
21112
  plan,
21013
21113
  proposedBy: actor,
21014
21114
  origin: opts?.origin,
21015
21115
  parentMessageId: opts?.parentMessageId,
21016
21116
  changeClass: preview2.changeClass,
21017
- nonMonotonicOps: preview2.nonMonotonicOps
21018
- });
21117
+ nonMonotonicOps: preview2.nonMonotonicOps,
21118
+ at: (/* @__PURE__ */ new Date()).toISOString()
21119
+ };
21120
+ this.draftCards.set(draftId, card2);
21121
+ await this.persistDraftCard(draftId, card2);
21019
21122
  await this.refreshModel();
21020
21123
  return { draftId, changeClass: preview2.changeClass, nonMonotonicOps: preview2.nonMonotonicOps };
21021
21124
  }
21022
21125
  async proposeCommandAuthorization(args) {
21023
21126
  const originNorm = args.origin ?? null;
21024
21127
  const argsJson = JSON.stringify(args.args ?? {});
21025
- for (const [id, card2] of this.draftCards) {
21026
- if (card2.kind !== "command" || card2.command !== args.command) continue;
21027
- if ((card2.origin ?? null) !== originNorm) continue;
21028
- if (JSON.stringify(card2.commandArgs ?? {}) !== argsJson) continue;
21128
+ for (const [id, card3] of this.draftCards) {
21129
+ if (card3.kind !== "command" || card3.command !== args.command) continue;
21130
+ if ((card3.origin ?? null) !== originNorm) continue;
21131
+ if (JSON.stringify(card3.commandArgs ?? {}) !== argsJson) continue;
21029
21132
  await this.refreshModel();
21030
21133
  return { draftId: id };
21031
21134
  }
21032
21135
  const draftId = `draft:${(0, import_node_crypto3.randomUUID)()}`;
21033
- this.draftCards.set(draftId, {
21136
+ const card2 = {
21034
21137
  kind: "command",
21035
21138
  command: args.command,
21036
21139
  commandArgs: args.args,
@@ -21038,8 +21141,11 @@ var init_kernel_bridge = __esm({
21038
21141
  anchor: args.artifactId,
21039
21142
  proposedBy: args.actor,
21040
21143
  origin: args.origin,
21041
- parentMessageId: args.parentMessageId
21042
- });
21144
+ parentMessageId: args.parentMessageId,
21145
+ at: (/* @__PURE__ */ new Date()).toISOString()
21146
+ };
21147
+ this.draftCards.set(draftId, card2);
21148
+ await this.persistDraftCard(draftId, card2);
21043
21149
  await this.refreshModel();
21044
21150
  return { draftId };
21045
21151
  }
@@ -21068,11 +21174,13 @@ var init_kernel_bridge = __esm({
21068
21174
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u4E0D\u652F\u6301\u6267\u884C ${card2.command}`);
21069
21175
  }
21070
21176
  this.draftCards.delete(args.draftId);
21177
+ await this.forgetDraftCard(args.draftId);
21071
21178
  await this.refreshModel();
21072
21179
  return { command: card2.command, artifactId };
21073
21180
  }
21074
21181
  async rejectIntervention(args) {
21075
21182
  this.draftCards.delete(args.draftId);
21183
+ await this.forgetDraftCard(args.draftId);
21076
21184
  await this.refreshModel();
21077
21185
  }
21078
21186
  /**
@@ -21383,6 +21491,7 @@ var init_kernel_bridge = __esm({
21383
21491
  }
21384
21492
  if (typeof plan.resolvesProposal === "string") {
21385
21493
  this.draftCards.delete(plan.resolvesProposal);
21494
+ await this.forgetDraftCard(plan.resolvesProposal);
21386
21495
  await this.refreshModel();
21387
21496
  }
21388
21497
  return {
@@ -22814,7 +22923,7 @@ function normalizeClaudeStreamLine(line) {
22814
22923
  for (const block of content3) {
22815
22924
  switch (block["type"]) {
22816
22925
  case "text":
22817
- out.push({ kind: "message", payload: { text: block["text"] } });
22926
+ if (type === "assistant") out.push({ kind: "message", payload: { text: block["text"] } });
22818
22927
  break;
22819
22928
  case "thinking":
22820
22929
  out.push({ kind: "thought", payload: { text: block["thinking"] } });
@@ -23014,6 +23123,8 @@ var init_claude_code = __esm({
23014
23123
  // 同上
23015
23124
  ["--verbose", "boolean"],
23016
23125
  // 与 --output-format stream-json 配对;单独无意义、成对是重复
23126
+ ["--replay-user-messages", "boolean"],
23127
+ // Input consumption receipts are adapter-owned.
23017
23128
  ["--include-partial-messages", "boolean"]
23018
23129
  // 同上
23019
23130
  ]);
@@ -23102,7 +23213,7 @@ var init_claude_code = __esm({
23102
23213
  // 据此喂 onOutput 实现 chat 逐字流式(同时 telemetry 仍由整条 assistant 消息归一)。
23103
23214
  ...this.opts.streamJson ? ["--output-format", "stream-json", "--verbose", "--include-partial-messages"] : [],
23104
23215
  // ADR-0093:stream-json 输入模式——让 claude 逐条从 stdin 读 user 消息(支持多轮追加)。
23105
- ...appendMode ? ["--input-format", "stream-json"] : [],
23216
+ ...appendMode ? ["--input-format", "stream-json", "--replay-user-messages"] : [],
23106
23217
  ...job.model ?? this.opts.model ? ["--model", job.model ?? this.opts.model] : [],
23107
23218
  // ADR「agent CLI 启动参数员工级可配置化」:员工配置 job.extraArgs(在前)+ opts.extraArgs(静态默认,在后),
23108
23219
  // 过 CLAUDE_BLOCKED_FLAGS 拦截打破协议契约的 flag(剔除 + warn),追加到 argv 末尾。
@@ -23170,10 +23281,11 @@ var init_claude_code = __esm({
23170
23281
  pendingClose = null;
23171
23282
  }
23172
23283
  };
23173
- const writeUserMessage = (text5) => {
23284
+ const pendingInputReceipts = /* @__PURE__ */ new Map();
23285
+ const writeUserMessage = (text5, uuid2) => {
23174
23286
  try {
23175
23287
  child.stdin.write(
23176
- JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: text5 }] } }) + "\n"
23288
+ JSON.stringify({ type: "user", ...uuid2 ? { uuid: uuid2 } : {}, message: { role: "user", content: [{ type: "text", text: text5 }] } }) + "\n"
23177
23289
  );
23178
23290
  writes++;
23179
23291
  cancelPendingClose();
@@ -23248,6 +23360,22 @@ var init_claude_code = __esm({
23248
23360
  out.write(line + "\n");
23249
23361
  try {
23250
23362
  const p2 = JSON.parse(line);
23363
+ if (p2["type"] === "user" && p2["isReplay"] === true && !p2["parent_tool_use_id"] && typeof p2["uuid"] === "string") {
23364
+ const key = pendingInputReceipts.get(p2["uuid"]);
23365
+ if (key) {
23366
+ pendingInputReceipts.delete(p2["uuid"]);
23367
+ diag("input_consumed", { key, uuid: p2["uuid"] });
23368
+ for (const cb of liveEventCbs) cb({
23369
+ protocolVersion: 2,
23370
+ streamId: `claude-live:${id}`,
23371
+ turnId: id,
23372
+ itemId: `input:${p2["uuid"]}`,
23373
+ itemType: "control",
23374
+ operation: "completed",
23375
+ payload: { code: "input_consumed", clientMessageKey: key }
23376
+ });
23377
+ }
23378
+ }
23251
23379
  if (p2["type"] === "result") {
23252
23380
  lastResult = p2;
23253
23381
  results++;
@@ -23423,6 +23551,9 @@ var init_claude_code = __esm({
23423
23551
  }
23424
23552
  };
23425
23553
  function doAppend(input) {
23554
+ if (input.requireConsumptionReceipt && (!input.clientMessageKey || !appendMode)) {
23555
+ return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
23556
+ }
23426
23557
  if (!appendMode) {
23427
23558
  diag("append_rejected", { reason: "unsupported", writes, results });
23428
23559
  return Promise.resolve({ accepted: false, reason: "unsupported", ...input.interrupt ? { interrupt: "unsupported" } : {} });
@@ -23436,9 +23567,12 @@ var init_claude_code = __esm({
23436
23567
  const outgoing = report.failed.length ? appendMaterializeWarning(input.text, report) : input.text;
23437
23568
  if (input.interrupt) writeInterrupt();
23438
23569
  diag("append_accepted", { writes, results, interrupt: !!input.interrupt, chars: input.text.length });
23439
- const ok3 = writeUserMessage(outgoing);
23570
+ const receiptId = input.requireConsumptionReceipt ? (0, import_node_crypto8.randomUUID)() : void 0;
23571
+ if (receiptId) pendingInputReceipts.set(receiptId, input.clientMessageKey);
23572
+ const ok3 = writeUserMessage(outgoing, receiptId);
23573
+ if (!ok3 && receiptId) pendingInputReceipts.delete(receiptId);
23440
23574
  if (!ok3) return Promise.resolve({ accepted: false, reason: "rejected", ...hasFiles ? { files: report } : {}, ...input.interrupt ? { interrupt: "unknown" } : {} });
23441
- return Promise.resolve({ accepted: true, ...hasFiles ? { files: report } : {}, interrupt: input.interrupt ? "interrupted" : "queued_at_boundary" });
23575
+ return Promise.resolve({ accepted: true, ...receiptId ? { consumption: "pending" } : {}, ...hasFiles ? { files: report } : {}, interrupt: input.interrupt ? "interrupted" : "queued_at_boundary" });
23442
23576
  }
23443
23577
  }
23444
23578
  };
@@ -24918,6 +25052,7 @@ async function runProtocolSession(job, cfg) {
24918
25052
  return !closing && started && !exited;
24919
25053
  },
24920
25054
  appendInput(input) {
25055
+ if (input.requireConsumptionReceipt) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
24921
25056
  appendChain = appendChain.then(async () => {
24922
25057
  if (exited || closing) return { accepted: false, reason: "session-closing" };
24923
25058
  const files = materializeFiles(dir, input.files, input.binaryFiles);
@@ -26743,6 +26878,7 @@ ${task}` : task;
26743
26878
  }
26744
26879
  };
26745
26880
  function doAppend(input) {
26881
+ if (input.requireConsumptionReceipt) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
26746
26882
  if (exited || closingForExit) return Promise.resolve({ accepted: false, reason: "session-closing" });
26747
26883
  const report = materializeFiles(dir, input.files, input.binaryFiles);
26748
26884
  const queued = report.failed.length ? { ...input, text: appendMaterializeWarning(input.text, report) } : input;
@@ -28398,6 +28534,18 @@ var init_normalizer = __esm({
28398
28534
  onLiveEvent(event) {
28399
28535
  this.turnId = event.turnId || this.turnId;
28400
28536
  const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
28537
+ if (event.itemType === "control" && payload.code === "input_consumed" && typeof payload.clientMessageKey === "string") {
28538
+ this.closeOpen("completed");
28539
+ this.emitItem({
28540
+ key: providerItemKey(this.providerName, event.itemId),
28541
+ kind: "control",
28542
+ role: "system",
28543
+ origin: "live",
28544
+ opShape: { op: "set_status", status: "completed" },
28545
+ attrs: { code: "input_consumed", clientMessageKey: payload.clientMessageKey }
28546
+ });
28547
+ return;
28548
+ }
28401
28549
  if (event.operation === "turn_completed" || event.operation === "turn_failed") {
28402
28550
  this.turnFinished(event.operation === "turn_failed" ? "failed" : "completed");
28403
28551
  return;
@@ -28813,7 +28961,7 @@ var init_chat_item_ledger = __esm({
28813
28961
  const payload = { role: "user", text: text5 };
28814
28962
  if (attachments?.length) payload.attachments = attachments;
28815
28963
  if (clientKeys?.length) payload.clientSubmitIds = clientKeys;
28816
- this.insert({
28964
+ return this.insert({
28817
28965
  kind: "text",
28818
28966
  role: "user",
28819
28967
  status: "completed",
@@ -28963,10 +29111,10 @@ var init_chat_item_ledger = __esm({
28963
29111
  * 返回落下的那一行(`null` = 本段有输出、或还没有 assistant 行可归属,不需要注解),
28964
29112
  * 调用方据它把这条推上 v3 流——不推的话它只在下一次快照才出现。
28965
29113
  */
28966
- closeSegmentBeforeUserInput() {
29114
+ closeSegmentBeforeUserInput(allowUnboundSegment = false) {
28967
29115
  this.closeStreamingRows("completed");
28968
29116
  if (this.segmentEndEmitted) return null;
28969
- if (this.segmentItemCount !== 0 || !this.messageId) return null;
29117
+ if (this.segmentItemCount !== 0 || !this.messageId && !allowUnboundSegment) return null;
28970
29118
  const id = this.insertSegmentEnd();
28971
29119
  this.segmentEndEmitted = true;
28972
29120
  const row = this.rows.get(id);
@@ -159948,7 +160096,8 @@ var init_live_chat = __esm({
159948
160096
  v3Seq: 0,
159949
160097
  v3Buffer: [],
159950
160098
  v3Subscribers: prev?.recovering ? prev.v3Subscribers : /* @__PURE__ */ new Set(),
159951
- v3Items: /* @__PURE__ */ new Map()
160099
+ v3Items: /* @__PURE__ */ new Map(),
160100
+ inputReceipts: /* @__PURE__ */ new Map()
159952
160101
  };
159953
160102
  this.turns.set(chatSessionId, turn);
159954
160103
  this.flushPendingUserItems(chatSessionId);
@@ -160002,6 +160151,8 @@ var init_live_chat = __esm({
160002
160151
  };
160003
160152
  },
160004
160153
  finish: (status) => {
160154
+ for (const receipt of turn.inputReceipts.values()) receipt.abandon();
160155
+ turn.inputReceipts.clear();
160005
160156
  try {
160006
160157
  turn.items?.finish(status === "error" ? "failed" : "completed");
160007
160158
  } catch {
@@ -160063,6 +160214,11 @@ var init_live_chat = __esm({
160063
160214
  * 账本坏掉不影响对话;v3 emit 抛异常也不影响 v2 通路与账本落盘——两条独立通路。
160064
160215
  */
160065
160216
  applyNormalizedEvent(turn, event) {
160217
+ if (event.type === "item" && event.kind === "control" && event.attrs?.code === "input_consumed") {
160218
+ const key = event.attrs.clientMessageKey;
160219
+ if (key) turn.inputReceipts.get(key)?.consume();
160220
+ return;
160221
+ }
160066
160222
  let outcome = null;
160067
160223
  try {
160068
160224
  outcome = turn.items?.apply(event) ?? null;
@@ -160586,7 +160742,7 @@ var init_live_chat = __esm({
160586
160742
  if (turn.recovering) return say({ accepted: false, reason: "recovering" }, { recovering: true });
160587
160743
  const fn = turn.handle.appendInput;
160588
160744
  if (typeof fn !== "function") return say({ accepted: false, reason: "unsupported" });
160589
- const pendingItemId = !opts?.system && opts?.clientMessageKey ? `oasis-user:${opts.clientMessageKey}` : void 0;
160745
+ const pendingItemId = !opts?.requireConsumptionReceipt && !opts?.system && opts?.clientMessageKey ? `oasis-user:${opts.clientMessageKey}` : void 0;
160590
160746
  const pendingAttachments = opts?.attachments?.map((a) => ({
160591
160747
  name: a.name,
160592
160748
  ...a.blobRef ? { blobRef: a.blobRef } : {},
@@ -160617,17 +160773,75 @@ var init_live_chat = __esm({
160617
160773
  payload: { role: "user" }
160618
160774
  });
160619
160775
  };
160776
+ let confirmed2 = false;
160777
+ let consumedItemId;
160778
+ let receiptPromise;
160779
+ const receiptKey = opts?.requireConsumptionReceipt && !opts.system ? opts.clientMessageKey : void 0;
160780
+ if (opts?.requireConsumptionReceipt && !opts.system && !receiptKey) {
160781
+ return say({ accepted: false, reason: "consumption-unsupported" });
160782
+ }
160783
+ if (receiptKey) {
160784
+ const existing = turn.inputReceipts.get(receiptKey);
160785
+ if (existing) {
160786
+ const consumed = await existing.result;
160787
+ await turn.items?.drain();
160788
+ return consumed && existing.itemId && turn.items?.ordFor(existing.itemId) !== void 0 ? { accepted: true } : { accepted: false, reason: "unconfirmed" };
160789
+ }
160790
+ let resolveReceipt;
160791
+ receiptPromise = new Promise((resolve10) => {
160792
+ resolveReceipt = resolve10;
160793
+ });
160794
+ turn.inputReceipts.set(receiptKey, {
160795
+ get itemId() {
160796
+ return consumedItemId;
160797
+ },
160798
+ result: receiptPromise,
160799
+ consume: () => {
160800
+ if (confirmed2 || turn.status !== "running") return;
160801
+ confirmed2 = true;
160802
+ consumedItemId = this.commitUserInput(turn, text5, opts);
160803
+ resolveReceipt(true);
160804
+ },
160805
+ abandon: () => {
160806
+ resolveReceipt(false);
160807
+ }
160808
+ });
160809
+ }
160810
+ const clearReceipt = () => {
160811
+ if (!receiptKey) return;
160812
+ turn.inputReceipts.get(receiptKey)?.abandon();
160813
+ turn.inputReceipts.delete(receiptKey);
160814
+ };
160620
160815
  let res;
160621
160816
  try {
160622
160817
  res = await fn.call(turn.handle, {
160623
160818
  text: text5,
160819
+ ...receiptKey ? { requireConsumptionReceipt: true } : {},
160624
160820
  ...opts?.clientMessageKey ? { clientMessageKey: opts.clientMessageKey } : {},
160625
160821
  ...opts?.files && Object.keys(opts.files).length ? { files: opts.files } : {},
160626
160822
  ...opts?.binaryFiles && Object.keys(opts.binaryFiles).length ? { binaryFiles: opts.binaryFiles } : {}
160627
160823
  });
160628
160824
  } catch (e) {
160825
+ if (!receiptKey) clearReceipt();
160629
160826
  failPending();
160630
- return say({ accepted: false, reason: "rejected" }, { err: String(e) });
160827
+ if (confirmed2) await turn.items?.drain();
160828
+ confirmed2 = confirmed2 && !!consumedItemId && turn.items?.ordFor(consumedItemId) !== void 0;
160829
+ return say({ accepted: confirmed2, ...!confirmed2 ? { reason: receiptKey ? "unconfirmed" : "rejected" } : {} }, { err: String(e) });
160830
+ }
160831
+ if (receiptPromise) {
160832
+ const notSent = ["unsupported", "consumption-unsupported", "session-closing", "no-live-turn", "recovering"];
160833
+ if (!confirmed2 && !res.accepted && notSent.includes(res.reason ?? "")) {
160834
+ clearReceipt();
160835
+ failPending();
160836
+ return say(res);
160837
+ }
160838
+ if (!await receiptPromise) {
160839
+ clearReceipt();
160840
+ failPending();
160841
+ return say({ accepted: false, reason: "unconfirmed" });
160842
+ }
160843
+ await turn.items?.drain();
160844
+ return say(!consumedItemId || turn.items?.ordFor(consumedItemId) === void 0 ? { accepted: false, reason: "unconfirmed" } : { accepted: true });
160631
160845
  }
160632
160846
  if (!res.accepted) {
160633
160847
  failPending();
@@ -160638,6 +160852,10 @@ var init_live_chat = __esm({
160638
160852
  failPending();
160639
160853
  return say({ accepted: false, reason: "turn-finished" }, { turnStatus: turn.status, replaced: this.turns.get(chatSessionId) !== turn });
160640
160854
  }
160855
+ this.commitUserInput(turn, text5, opts);
160856
+ return { accepted: true };
160857
+ }
160858
+ commitUserInput(turn, text5, opts) {
160641
160859
  const shown = opts?.displayText ?? text5;
160642
160860
  const userPart = {
160643
160861
  type: "user",
@@ -160646,7 +160864,7 @@ var init_live_chat = __esm({
160646
160864
  };
160647
160865
  const segmentEnd = (() => {
160648
160866
  try {
160649
- return turn.items?.closeSegmentBeforeUserInput() ?? null;
160867
+ return turn.items?.closeSegmentBeforeUserInput(!!opts?.requireConsumptionReceipt && !!turn.onUserInput) ?? null;
160650
160868
  } catch {
160651
160869
  return null;
160652
160870
  }
@@ -160662,8 +160880,9 @@ var init_live_chat = __esm({
160662
160880
  payload: { code: "segment_end", note: SEGMENT_END_NOTE }
160663
160881
  });
160664
160882
  }
160883
+ let userItemId;
160665
160884
  try {
160666
- turn.items?.recordUserInput(shown, opts?.attachments, opts?.clientKeys);
160885
+ userItemId = turn.items?.recordUserInput(shown, opts?.attachments, opts?.clientKeys);
160667
160886
  } catch {
160668
160887
  }
160669
160888
  const withSeq = { ...userPart, seq: ++turn.seq };
@@ -160697,7 +160916,7 @@ var init_live_chat = __esm({
160697
160916
  turn.onUserInput?.(shown, opts?.attachments);
160698
160917
  } catch {
160699
160918
  }
160700
- return { accepted: true };
160919
+ return userItemId;
160701
160920
  }
160702
160921
  /** 反查该会话当前驻留那一轮的 runId(无进行中/grace 轮,或该轮无 runId 时 undefined)。
160703
160922
  * 建单草案 submit 发生在这一轮内:用它给草案盖上「产出轮次」标记,供 friday 重进对话页把每版
@@ -160843,10 +161062,10 @@ var init_chat_attachment_files = __esm({
160843
161062
  });
160844
161063
 
160845
161064
  // ../server/src/domains/chat-sessions/append-now.ts
160846
- async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, attachments) {
161065
+ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, attachments, retainLocallyOnFailure = false) {
160847
161066
  const newId = deps.newId ?? import_node_crypto20.randomUUID;
160848
161067
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
160849
- await deps.store.enqueuePendingMessage({
161068
+ const submitted = await deps.store.enqueuePendingMessage({
160850
161069
  id: newId(),
160851
161070
  chatSessionId,
160852
161071
  text: text5,
@@ -160879,10 +161098,19 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
160879
161098
  ...displayText !== merged ? { displayText } : {},
160880
161099
  ...attachmentRefs.length ? { attachments: attachmentRefs } : {}
160881
161100
  });
160882
- const DEFINITELY_NOT_DELIVERED = /* @__PURE__ */ new Set(["unsupported", "no-live-turn", "session-closing", "turn-finished", "recovering"]);
161101
+ const DEFINITELY_NOT_DELIVERED = /* @__PURE__ */ new Set(["unsupported", "no-live-turn", "session-closing", "turn-finished", "recovering", "consumption-unsupported"]);
160883
161102
  const nextState = res.accepted ? "delivered" : DEFINITELY_NOT_DELIVERED.has(res.reason ?? "") ? "queued" : "unconfirmed";
160884
- await deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason).catch(() => void 0);
160885
- return { ...res, queued: true, delivered: merged, batch, ...prepared.paths.length ? { attachmentPaths: prepared.paths } : {} };
161103
+ const returnToClient = retainLocallyOnFailure && !!clientKey && nextState === "queued";
161104
+ if (returnToClient && deps.store.markPendingMessages) {
161105
+ await deps.store.markPendingMessages(chatSessionId, [submitted.id], "cancelled", "client-retained");
161106
+ await deps.store.markPendingMessages(chatSessionId, ids2.filter((id) => id !== submitted.id), nextState, res.reason);
161107
+ } else {
161108
+ const transition = deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason);
161109
+ if (retainLocallyOnFailure) await transition;
161110
+ else await transition?.catch(() => void 0);
161111
+ if (returnToClient) await deps.store.removePendingMessage(chatSessionId, submitted.id);
161112
+ }
161113
+ return { ...res, queued: !returnToClient, delivered: merged, batch, ...prepared.paths.length ? { attachmentPaths: prepared.paths } : {} };
160886
161114
  }
160887
161115
  var import_node_crypto20;
160888
161116
  var init_append_now = __esm({
@@ -203944,7 +204172,7 @@ async function startOasisServer(opts) {
203944
204172
  }
203945
204173
  return void 0;
203946
204174
  }
203947
- const appendNowThroughQueue2 = async (sid, text5, clientKey, attachments, perCompanyStore) => {
204175
+ const appendNowThroughQueue2 = async (sid, text5, clientKey, attachments, perCompanyStore, retainLocallyOnFailure = false) => {
203948
204176
  if (perCompanyStore === null) {
203949
204177
  throw new Error(
203950
204178
  `appendNowThroughQueue: per-company chat store unresolved (sid=${sid})`
@@ -203958,7 +204186,7 @@ async function startOasisServer(opts) {
203958
204186
  store,
203959
204187
  // ADR-0166 D9.1:附件与正文**同一次**送出——拆开就等于让 agent 拿着「见附件」而
203960
204188
  // 附件不存在的描述去干活。`files` 空时这次调用与从前逐字相同。
203961
- deliver: (merged, clientMessageKey, extra) => liveChat.append(sid, merged, { clientMessageKey, ...extra }),
204189
+ deliver: (merged, clientMessageKey, extra) => liveChat.append(sid, merged, { clientMessageKey, ...extra, requireConsumptionReceipt: true }),
203962
204190
  // **先查资产柜再查事实柜**——chat 附件是 `uploadChatAttachment` 存进**资产柜**的
203963
204191
  // (serve 那侧 `assets.put`),两个柜是**物理隔离**的(「资产柜与事实柜分开」)。
203964
204192
  // 只查 `opts.blobs` 必然取不到,于是每张图都被报成「附件没能取回来」。
@@ -203973,8 +204201,8 @@ async function startOasisServer(opts) {
203973
204201
  }
203974
204202
  throw new Error(`blob ${blobRef} \u4E24\u4E2A\u67DC\u91CC\u90FD\u6CA1\u6709`);
203975
204203
  }
203976
- }, clientKey, attachments);
203977
- return { accepted: r.accepted, ...r.reason ? { reason: r.reason } : {}, queued: true };
204204
+ }, clientKey, attachments, retainLocallyOnFailure);
204205
+ return { accepted: r.accepted, ...r.reason ? { reason: r.reason } : {}, queued: r.queued };
203978
204206
  };
203979
204207
  const GRAPH_EDIT_SUMMARY_MAX = 60;
203980
204208
  const truncateSummary = (s2) => {
@@ -206131,9 +206359,11 @@ async function startOasisServer(opts) {
206131
206359
  let text5 = "";
206132
206360
  let clientKey;
206133
206361
  let insertAttachments = [];
206362
+ let retainLocallyOnFailure = false;
206134
206363
  try {
206135
206364
  const b2 = JSON.parse(rawBody);
206136
206365
  text5 = String(b2.text ?? "").trim();
206366
+ retainLocallyOnFailure = b2.retainLocallyOnFailure === true;
206137
206367
  if (typeof b2.clientKey === "string" && b2.clientKey) clientKey = b2.clientKey;
206138
206368
  insertAttachments = (Array.isArray(b2.attachments) ? b2.attachments : []).filter((a) => !!a && typeof a === "object" && typeof a["name"] === "string" && (typeof a["text"] === "string" || typeof a["blobRef"] === "string")).map((a) => ({
206139
206369
  name: a["name"],
@@ -206147,7 +206377,7 @@ async function startOasisServer(opts) {
206147
206377
  res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: "EMPTY_TEXT" }));
206148
206378
  return;
206149
206379
  }
206150
- const result = await appendNowThroughQueue2(sid, text5, clientKey, insertAttachments, perCompanyStore);
206380
+ const result = await appendNowThroughQueue2(sid, text5, clientKey, insertAttachments, perCompanyStore, retainLocallyOnFailure);
206151
206381
  res.writeHead(result.accepted ? 200 : 409, { "content-type": "application/json" }).end(JSON.stringify(result));
206152
206382
  return;
206153
206383
  }
@@ -218337,6 +218567,10 @@ var init_daemon_hub = __esm({
218337
218567
  this.onHandshakeRejected?.(rejection);
218338
218568
  cb(false, code2, message);
218339
218569
  }
218570
+ /** Ask the running session: an upgrade leaves existing sessions alive. */
218571
+ supportsInputConsumptionReceipts(dispatchId) {
218572
+ return this.hasSession(dispatchId) && this.sessionMeta.get(dispatchId)?.supportsInputConsumptionReceipts === true;
218573
+ }
218340
218574
  dispatch(daemonId, msg) {
218341
218575
  const daemon = this.daemons.get(daemonId);
218342
218576
  if (!daemon) return false;
@@ -218406,6 +218640,7 @@ var init_daemon_hub = __esm({
218406
218640
  nodeId,
218407
218641
  sessionId: msg.sessionId,
218408
218642
  connectedAt: Date.now(),
218643
+ supportsInputConsumptionReceipts: msg.supportsInputConsumptionReceipts === true,
218409
218644
  ...msg.jobArtifactId ? { jobArtifactId: msg.jobArtifactId } : {}
218410
218645
  });
218411
218646
  for (const l of this.sessionConnectListeners) l({ dispatchId, sessionId: msg.sessionId, nodeId });
@@ -221047,7 +221282,7 @@ async function actorStats(actorId, opts = {}) {
221047
221282
  const now = opts.now ?? /* @__PURE__ */ new Date();
221048
221283
  const from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3).toISOString();
221049
221284
  const to = now.toISOString();
221050
- const weeklyUsage = opts.usage?.listRuns ? summarizeWeeklyRuns(await listWeeklyRuns(opts.usage, actorId, from, to)) : summarizeWeeklyUsage(opts.usage?.queryUsageStats ? await opts.usage.queryUsageStats({
221285
+ const weeklyUsage = opts.usage?.listRuns ? summarizeWeeklyRuns(await listWeeklyRuns(opts.usage, actorId, from, to, opts.artifactIds)) : summarizeWeeklyUsage(!opts.artifactIds && opts.usage?.queryUsageStats ? await opts.usage.queryUsageStats({
221051
221286
  actorIds: [actorId],
221052
221287
  from,
221053
221288
  to,
@@ -221055,6 +221290,8 @@ async function actorStats(actorId, opts = {}) {
221055
221290
  }) : []);
221056
221291
  return {
221057
221292
  actorId,
221293
+ usageAvailable: !!(opts.usage?.listRuns || !opts.artifactIds && opts.usage?.queryUsageStats),
221294
+ ...opts.model ? { participatingTaskCount: participatingTaskCount(opts.model, actorId) } : {},
221058
221295
  tokenWeek: weeklyUsage.tokenWeek,
221059
221296
  tokenWeekBreakdown: weeklyUsage.tokenWeekBreakdown,
221060
221297
  costWeekUsdMicros: weeklyUsage.costWeekUsdMicros,
@@ -221064,12 +221301,12 @@ async function actorStats(actorId, opts = {}) {
221064
221301
  unpricedModels: weeklyUsage.unpricedModels
221065
221302
  };
221066
221303
  }
221067
- async function listWeeklyRuns(trace, actorId, from, to) {
221304
+ async function listWeeklyRuns(trace, actorId, from, to, artifactIds) {
221068
221305
  const out = [];
221069
221306
  let cursor;
221070
221307
  do {
221071
221308
  const page = await trace.listRuns({ actorId, from, to, limit: 500, ...cursor ? { cursor } : {} });
221072
- out.push(...page.items);
221309
+ out.push(...page.items.filter((run) => !artifactIds || run.artifactId != null && artifactIds.has(run.artifactId)));
221073
221310
  cursor = page.nextCursor ?? void 0;
221074
221311
  } while (cursor);
221075
221312
  return out;
@@ -221080,6 +221317,13 @@ function summarizeWeeklyRuns(runs) {
221080
221317
  function summarizeWeeklyUsage(rows) {
221081
221318
  return summarize2(rows.map((row) => ({ model: row.effectiveModel, usage: row })));
221082
221319
  }
221320
+ function participatingTaskCount(model, actorId) {
221321
+ const workorders = /* @__PURE__ */ new Set();
221322
+ for (const artifact of model.artifacts.values()) {
221323
+ if (artifact.owner === actorId) workorders.add(artifact.workspace);
221324
+ }
221325
+ return workorders.size;
221326
+ }
221083
221327
  var init_stats = __esm({
221084
221328
  "../server/src/domains/actors/stats.ts"() {
221085
221329
  "use strict";
@@ -222415,9 +222659,16 @@ function actorsDomain(opts) {
222415
222659
  throw new ApiError(400, "BAD_REQUEST", "\u4F20 enabled\uFF08\u5F00\u5173\uFF09/ appliedToAll\uFF08\u5168\u5458\u751F\u6548\uFF09/ name\xB7description\uFF08\u66F4\u65B0\u5143\u6570\u636E\uFF09");
222416
222660
  });
222417
222661
  router.get("/api/actors/:id/stats", async (req) => {
222662
+ const ctx = await resolveCtx(req.auth.companyId);
222663
+ if (!await ctx.service.getActor(req.params.id)) throw new ApiError(404, "NOT_FOUND", "\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5");
222664
+ const model = ctx.getModel?.();
222418
222665
  return {
222419
222666
  status: 200,
222420
- body: await actorStats(req.params.id, { usage: opts.trace?.() })
222667
+ body: await actorStats(req.params.id, {
222668
+ usage: req.auth.companyId && !model ? void 0 : opts.trace?.(),
222669
+ model,
222670
+ ...req.auth.companyId ? { artifactIds: new Set(model?.artifacts.keys() ?? []) } : {}
222671
+ })
222421
222672
  };
222422
222673
  });
222423
222674
  router.get("/api/actors/:id/running-tasks", async (req) => {
@@ -222975,6 +223226,7 @@ function createActorsDomain(opts) {
222975
223226
  const engineStoreOf = (k2) => k2?.getStore?.();
222976
223227
  const defaultCtx = {
222977
223228
  service: buildService(opts.store, opts.kernel, opts.blobs),
223229
+ ...opts.kernel?.model ? { getModel: () => opts.kernel.model } : {},
222978
223230
  ...opts.oplog !== void 0 ? { oplog: opts.oplog } : {},
222979
223231
  ...engineStoreOf(opts.kernel) !== void 0 ? { engineStore: engineStoreOf(opts.kernel) } : {},
222980
223232
  ...opts.blobs !== void 0 ? { blobs: opts.blobs } : {}
@@ -222988,6 +223240,7 @@ function createActorsDomain(opts) {
222988
223240
  const blobs = engine.assets ?? engine.blobs;
222989
223241
  const ctx = {
222990
223242
  service: buildService(engine.registry, engine.kernel, blobs),
223243
+ getModel: () => engine.kernel.model,
222991
223244
  oplog: engine.oplog,
222992
223245
  ...engineStoreOf(engine.kernel) !== void 0 ? { engineStore: engineStoreOf(engine.kernel) } : {},
222993
223246
  blobs
@@ -223173,6 +223426,16 @@ var init_assistant_tier_dispatch = __esm({
223173
223426
  // ../server/src/domains/projects/routes.ts
223174
223427
  function projectsDomain(opts) {
223175
223428
  const svc = async (req) => opts.resolveService ? opts.resolveService(req.auth.companyId) : opts.service;
223429
+ const requireProjectManagePermission = async (req, projectId2, action) => {
223430
+ if (!opts.resolveMemberRole) return;
223431
+ if (isUncategorizedProjectId(projectId2)) return;
223432
+ const project = await (await svc(req)).getProject(projectId2);
223433
+ if (!project) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u9879\u76EE\uFF1A${projectId2}`);
223434
+ if (project.createdBy && project.createdBy.id === req.auth.actor) return;
223435
+ const role = await opts.resolveMemberRole(req.auth.companyId, req.auth.actor);
223436
+ if (role === "owner" || role === "admin") return;
223437
+ throw new ApiError(403, action.code, `\u4F60\u6CA1\u6709${action.verb}\u8BE5\u9879\u76EE\u7684\u6743\u9650\uFF0C\u8BF7\u8054\u7CFB\u7EC4\u7EC7\u7BA1\u7406\u5458`);
223438
+ };
223176
223439
  return (router) => {
223177
223440
  router.post("/api/projects", async (req) => {
223178
223441
  const body2 = req.body;
@@ -223214,6 +223477,7 @@ function projectsDomain(opts) {
223214
223477
  return { status: 200, body: toApiProject(project) };
223215
223478
  });
223216
223479
  router.patch("/api/projects/:id", async (req) => {
223480
+ await requireProjectManagePermission(req, req.params.id, { verb: "\u7F16\u8F91", code: "PROJECT_EDIT_FORBIDDEN" });
223217
223481
  const body2 = req.body ?? {};
223218
223482
  for (const immutable of ["id", "project_id", "projectId", "createdBy", "created_by", "ownerId", "owner_id"]) {
223219
223483
  if (Object.prototype.hasOwnProperty.call(body2, immutable)) {
@@ -223233,6 +223497,7 @@ function projectsDomain(opts) {
223233
223497
  }
223234
223498
  });
223235
223499
  router.delete("/api/projects/:id", async (req) => {
223500
+ await requireProjectManagePermission(req, req.params.id, { verb: "\u5220\u9664", code: "PROJECT_DELETE_FORBIDDEN" });
223236
223501
  try {
223237
223502
  const result = await (await svc(req)).deleteProject(req.params.id);
223238
223503
  return {
@@ -224873,8 +225138,10 @@ function buildWorkorderActivity(input) {
224873
225138
  ] : []
224874
225139
  });
224875
225140
  }
225141
+ const openedSeq = events.find((rec) => rec.event.kind === "work.create")?.seq;
225142
+ const beforeOpen = (seq) => openedSeq !== void 0 && seq < openedSeq;
224876
225143
  const drafts = [
224877
- ...standalone,
225144
+ ...standalone.filter((d) => !beforeOpen(d.seq)),
224878
225145
  ...executionCards,
224879
225146
  ...visibleReviews.values(),
224880
225147
  ...issueCards.values(),
@@ -224950,7 +225217,7 @@ function buildWorkorderActivity(input) {
224950
225217
  .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(traced ? runIdOfCard(d) : void 0)
224951
225218
  };
224952
225219
  });
224953
- cards.push(...reviewRequirementActivity(snap, events, ref2));
225220
+ cards.push(...reviewRequirementActivity(snap, events, ref2).filter((c) => !beforeOpen(c.seq)));
224954
225221
  cards.sort((a, b2) => a.updatedAt.localeCompare(b2.updatedAt) || a.seq - b2.seq || a.id.localeCompare(b2.id));
224955
225222
  return { workorderId, cards, truncated };
224956
225223
  }
@@ -225620,6 +225887,12 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
225620
225887
  actionRequired: true,
225621
225888
  summary: changeConfirmSummary(r),
225622
225889
  draftId: r.draftId,
225890
+ // ★ 落点(2026-09-11):带上起草它的那段对话,让前端把这一行跳回**卡真正渲染着的地方**。
225891
+ // 此前这一行恒跳「任务详情 ?tab=activity」,而全仓库渲染待审卡的只有 chat 页与任务页的协调者
225892
+ // 对话(都按 origin=会话 id 查),动态 tab 里一行渲染代码都没有——于是收件箱这条路
225893
+ // 「找得到那一行、却点不到确认键」。origin 缺省(自主会话起草、没人在场)时不带,
225894
+ // 仍回落工单详情。
225895
+ ...r.origin ? { chatSessionId: r.origin } : {},
225623
225896
  ...anchorArt ? { title: nodeLabel(anchorArt) } : {},
225624
225897
  detail: `${actorName(r.proposedBy) || "\u7BA1\u7406\u8005"}\uFF1A${r.command?.effectLabel ?? firstSentence(r.plan.reason)}`,
225625
225898
  ...r.kind !== "command" && opCount > 0 ? { impact: `\u6D89\u53CA ${opCount} \u5904\u6539\u52A8${r.nonMonotonicOps.length > 0 ? " \xB7 \u542B\u9500\u6BC1\u6027" : ""}` } : {},
@@ -228506,7 +228779,8 @@ function createProjectsDomain(opts) {
228506
228779
  register: projectsDomain({
228507
228780
  service,
228508
228781
  ...opts.resolveCompany ? { resolveService } : {},
228509
- ...opts.defaultBranchResolver ? { defaultBranchResolver: opts.defaultBranchResolver } : {}
228782
+ ...opts.defaultBranchResolver ? { defaultBranchResolver: opts.defaultBranchResolver } : {},
228783
+ ...opts.resolveMemberRole ? { resolveMemberRole: opts.resolveMemberRole } : {}
228510
228784
  }),
228511
228785
  ...opts.resolveCompany ? { resolveService } : {}
228512
228786
  };
@@ -239159,6 +239433,9 @@ function routeToSession(hub, nodeId, dispatchId, msg) {
239159
239433
  function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
239160
239434
  const unknownIfInterrupting = input.interrupt ? { interrupt: "unknown" } : {};
239161
239435
  if (entry.exited) return Promise.resolve({ accepted: false, reason: "session-closing", ...unknownIfInterrupting });
239436
+ if (input.requireConsumptionReceipt && !hub.supportsInputConsumptionReceipts?.(dispatchId)) {
239437
+ return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
239438
+ }
239162
239439
  const requestId = (0, import_node_crypto65.randomUUID)();
239163
239440
  const sent = routeToSession(hub, nodeId, dispatchId, { type: "append_input", dispatchId, input, requestId });
239164
239441
  if (!sent) return Promise.resolve({ accepted: false, reason: "session-closing", ...unknownIfInterrupting });
@@ -239166,10 +239443,10 @@ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
239166
239443
  const timer = setTimeout(() => {
239167
239444
  const i = entry.appendWaiters.findIndex((w2) => w2.timer === timer);
239168
239445
  if (i >= 0) entry.appendWaiters.splice(i, 1);
239169
- resolve10({ accepted: false, reason: "unsupported", ...unknownIfInterrupting });
239446
+ resolve10({ accepted: false, reason: input.requireConsumptionReceipt ? "unconfirmed" : "unsupported", ...unknownIfInterrupting });
239170
239447
  }, timeoutMs);
239171
239448
  timer.unref?.();
239172
- entry.appendWaiters.push({ requestId, resolve: resolve10, timer });
239449
+ entry.appendWaiters.push({ requestId, requireConsumptionReceipt: input.requireConsumptionReceipt, resolve: resolve10, timer });
239173
239450
  });
239174
239451
  }
239175
239452
  var import_node_crypto65, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
@@ -239316,7 +239593,7 @@ var init_daemon_adapter = __esm({
239316
239593
  entry.exited = info;
239317
239594
  for (const w2 of entry.appendWaiters.splice(0)) {
239318
239595
  clearTimeout(w2.timer);
239319
- w2.resolve({ accepted: false, reason: "session-closing" });
239596
+ w2.resolve({ accepted: false, reason: w2.requireConsumptionReceipt ? "unconfirmed" : "session-closing" });
239320
239597
  }
239321
239598
  if (info.reason === "server-unreachable") this.health?.recordUnreachable(entry.nodeId, info.reason);
239322
239599
  else this.health?.recordReachable(entry.nodeId);
@@ -253204,6 +253481,17 @@ var init_cutover_aware_dispatches = __esm({
253204
253481
  if (row.state === "frozen") throw new BundleFrozenError("dispatch");
253205
253482
  return this.o.cutover.readsCompanySchema(row) ? this.o.owned : this.o.legacy;
253206
253483
  }
253484
+ /**
253485
+ * 这家公司的派发台账当前**物理落在哪**:切走了就是它自己的引擎 schema,没切走就是那个
253486
+ * 所有未切公司共用的 legacy 台账。
253487
+ *
253488
+ * 给 {@link FanOutDispatchLedger} 去重用。判据**不在调用方重算**——就是 {@link pick}
253489
+ * 那一条(`cutover.get` + `readsCompanySchema`)。与 `CutoverAwareTraceStore.storageKey` 同构。
253490
+ */
253491
+ async storageKey() {
253492
+ const row = await this.o.cutover.get(this.o.companyId, "dispatch");
253493
+ return this.o.cutover.readsCompanySchema(row) ? `owned:${this.o.companyId}` : "legacy";
253494
+ }
253207
253495
  async insertOpen(rec) {
253208
253496
  return (await this.pickWritable()).insertOpen(rec);
253209
253497
  }
@@ -253239,11 +253527,48 @@ var init_cutover_aware_dispatches = __esm({
253239
253527
  constructor(o) {
253240
253528
  this.o = o;
253241
253529
  }
253242
- async each(op, fn) {
253530
+ /**
253531
+ * 本次要作用的物理落点清单——**同一个台账只出现一次**。
253532
+ *
253533
+ * 为什么必须去重:`dispatch` 是按 (公司, bundle) 逐个切的,没切走的公司**全部共用同一个
253534
+ * legacy 台账**(读写同一张 `public.dispatches`)。此前 `each` 直接按公司逐家来,于是同一张
253535
+ * 表被访问 N 次(N = 未切走的公司数):
253536
+ *
253537
+ * - **纯读直接出重复行**(`listOpen` / `listByWorkorder` / `findOpenByTarget`)。
253538
+ * 2026-09-11 v2 生产实证(`ws:wo-9f4652cd`):35 家公司、其中 33 家 dispatch 未切走,
253539
+ * `listByWorkorder` 返回 33 份同一条派发,动态卡「执行轨迹」抽屉里同一轮 attempt
253540
+ * 就出现 33 次(`attempts` 由它喂,见 server 的 `activity-trace.ts`)。
253541
+ * - 写侧**没有被放大**,但原因是运气好而不是设计:`markStarted` / `close` 是同值 UPDATE;
253542
+ * `closeStaleNeverStarted` / `closeOpenByTarget` 的 WHERE 带 `ended_at IS NULL`,
253543
+ * 第一遍改完第二遍就匹配不上,返回 0,`reduce` 出来仍是真实值。
253544
+ * - 无论读写,同一条 SQL 都实打实打了 N 遍。
253545
+ *
253546
+ * 去重键由台账自己报,不在这里按 cutover 状态重算(见 `storageKey` 的注释)。也不能按
253547
+ * `ledgerFor` 返回的对象做身份去重:每家各有一个 `CutoverAwareDispatchStore` 包装实例,
253548
+ * N 个不同对象包着同一个 legacy 台账。
253549
+ */
253550
+ async targets(op) {
253243
253551
  const out = [];
253552
+ const seen = /* @__PURE__ */ new Set();
253244
253553
  for (const cid of await this.o.listCompanies()) {
253245
253554
  try {
253246
- out.push(await fn(await this.o.ledgerFor(cid)));
253555
+ if (this.o.storageKeyFor) {
253556
+ const key = await this.o.storageKeyFor(cid);
253557
+ if (seen.has(key)) continue;
253558
+ seen.add(key);
253559
+ }
253560
+ out.push({ cid, ledger: await this.o.ledgerFor(cid) });
253561
+ } catch (err) {
253562
+ this.o.onError?.(cid, op, err);
253563
+ }
253564
+ }
253565
+ return out;
253566
+ }
253567
+ async each(op, fn) {
253568
+ const out = [];
253569
+ for (const { cid, ledger } of await this.targets(op)) {
253570
+ try {
253571
+ out.push(await fn(ledger));
253247
253572
  } catch (err) {
253248
253573
  this.o.onError?.(cid, op, err);
253249
253574
  }
@@ -253379,6 +253704,17 @@ var init_cutover_aware_trace = __esm({
253379
253704
  if (row.state === "frozen") throw new BundleFrozenError("trace");
253380
253705
  return this.o.cutover.readsCompanySchema(row) ? this.o.owned : this.o.legacy;
253381
253706
  }
253707
+ /**
253708
+ * 这家公司的 trace 当前**物理落在哪**:切走了就是它自己的引擎 schema,没切走就是那个
253709
+ * 所有未切公司共用的 legacy store。
253710
+ *
253711
+ * 给 {@link FanOutTraceStore} 去重用。判据**不在调用方重算**——就是 {@link pick} 那一条
253712
+ * (`cutover.get` + `readsCompanySchema`),照抄一遍就是又造一处会分叉的实现。
253713
+ */
253714
+ async storageKey() {
253715
+ const row = await this.o.cutover.get(this.o.companyId, "trace");
253716
+ return this.o.cutover.readsCompanySchema(row) ? `owned:${this.o.companyId}` : "legacy";
253717
+ }
253382
253718
  // ── run ──────────────────────────────────────────────────────────────────
253383
253719
  async createRun(input) {
253384
253720
  return (await this.pickWritable()).createRun(input);
@@ -253496,10 +253832,43 @@ var init_cutover_aware_trace = __esm({
253496
253832
  constructor(o) {
253497
253833
  this.o = o;
253498
253834
  }
253499
- async first(op, fn) {
253835
+ /**
253836
+ * 本次要查的物理落点清单——**同一个 store 只出现一次**。
253837
+ *
253838
+ * 为什么必须去重:`trace` 是按 (公司, bundle) 逐个切的,没切走的公司**全部共用同一个
253839
+ * legacy store**(读同一张 `public.agent_runs`)。此前 `union` 直接按公司逐家拼接,于是
253840
+ * 一条 run 在库里只有一行,却被取回 N 份(N = 未切走的公司数)。
253841
+ *
253842
+ * 2026-09-11 v2 生产实证(`ws:wo-9f4652cd`):35 家公司、其中 33 家 trace 未切走,
253843
+ * 动态卡的「执行轨迹」抽屉里同一轮 attempt 出现 **33 次**;`listEvents` 更狠——那一轮
253844
+ * `agent_run_events` 有 1158 行,扇出后返回 ≈3.8 万行,还附带 33 条一模一样的 SQL。
253845
+ * 不是显示层重复,是响应体和查询量一起炸。
253846
+ *
253847
+ * 去重键由 store 自己报,不在这里按 cutover 状态重算(见 `storageKey` 的注释)。
253848
+ * 也不能按 `storeFor` 返回的对象做身份去重:`openCompanyPlane` 给每家各造了一个
253849
+ * `CutoverAwareTraceStore` 包装实例,33 个不同对象包着同一个 legacy store。
253850
+ */
253851
+ async targets(op) {
253852
+ const out = [];
253853
+ const seen = /* @__PURE__ */ new Set();
253500
253854
  for (const cid of await this.o.listCompanies()) {
253501
253855
  try {
253502
- const r = await fn(await this.o.storeFor(cid));
253856
+ if (this.o.storageKeyFor) {
253857
+ const key = await this.o.storageKeyFor(cid);
253858
+ if (seen.has(key)) continue;
253859
+ seen.add(key);
253860
+ }
253861
+ out.push({ cid, store: await this.o.storeFor(cid) });
253862
+ } catch (err) {
253863
+ this.o.onError?.(cid, op, err);
253864
+ }
253865
+ }
253866
+ return out;
253867
+ }
253868
+ async first(op, fn) {
253869
+ for (const { cid, store } of await this.targets(op)) {
253870
+ try {
253871
+ const r = await fn(store);
253503
253872
  if (r != null) return r;
253504
253873
  } catch (err) {
253505
253874
  this.o.onError?.(cid, op, err);
@@ -253509,20 +253878,26 @@ var init_cutover_aware_trace = __esm({
253509
253878
  }
253510
253879
  async union(op, fn) {
253511
253880
  const out = [];
253512
- for (const cid of await this.o.listCompanies()) {
253881
+ for (const { cid, store } of await this.targets(op)) {
253513
253882
  try {
253514
- out.push(...await fn(await this.o.storeFor(cid)));
253883
+ out.push(...await fn(store));
253515
253884
  } catch (err) {
253516
253885
  this.o.onError?.(cid, op, err);
253517
253886
  }
253518
253887
  }
253519
253888
  return out;
253520
253889
  }
253521
- /** 这个 run 属于哪一家。写侧要先拿它,别直接写。 */
253890
+ /**
253891
+ * 这个 run 属于哪一家。写侧要先拿它,别直接写。
253892
+ *
253893
+ * ⚠ 未切走的那批公司共用一张 legacy 表,这里**判不出真正的归属**——它们都「有」这条 run,
253894
+ * 返回的是名单里第一家。去重不改变这个答案(第一家仍是第一家),只是把 N 次重复查询收成一次。
253895
+ * 要真正判准归属,得等 `trace` bundle 切完、每家各有自己的 schema。
253896
+ */
253522
253897
  async companyOfRun(runId) {
253523
- for (const cid of await this.o.listCompanies()) {
253898
+ for (const { cid, store } of await this.targets("companyOfRun")) {
253524
253899
  try {
253525
- if (await (await this.o.storeFor(cid)).getRun(runId)) return cid;
253900
+ if (await store.getRun(runId)) return cid;
253526
253901
  } catch (err) {
253527
253902
  this.o.onError?.(cid, "companyOfRun", err);
253528
253903
  }
@@ -256328,6 +256703,11 @@ var init_default_company_schema_move = __esm({
256328
256703
  "chat_sessions",
256329
256704
  "event_handler_log",
256330
256705
  "events",
256706
+ // 待人确认的 intervention 草案卡(2026-09-11 落库)。与 events/workorders 同族:锚在本公司的
256707
+ // 工单与产物上、不跨公司读,所以**整表搬**。不进 ENGINE_SCHEMA_BUNDLE_TABLES——那份是
256708
+ // legacy 里混着所有公司行的表(dispatches/blobs/trace 一族),只能逐行按归属复制;
256709
+ // 这张表在默认公司 schema 里只有总部自己的卡。
256710
+ "intervention_drafts",
256331
256711
  "issue_attachments",
256332
256712
  "issue_recipients",
256333
256713
  "issue_replies",
@@ -259405,6 +259785,14 @@ async function startServe(opts) {
259405
259785
  actorDirectory: registryStore,
259406
259786
  // 与 collab 同源;闭包避开下方 listCompanyMembers 声明的 TDZ,实际请求时才读取。
259407
259787
  listMembers: (companyId) => listCompanyMembers(companyId),
259788
+ // 改/删项目的门禁判据之一(另一半是项目创建人)——见 domains/projects/routes.ts 的
259789
+ // `requireProjectManagePermission`。同样用闭包避开 listCompanyMembers 的 TDZ。
259790
+ // 成员表按 company_id 有索引、量级几十行,与 `listMembers` 同一次读的成本量级;
259791
+ // 只在 PATCH/DELETE 这两个低频写端点上调,不在任何读热路径上。
259792
+ resolveMemberRole: async (companyId, actorId) => {
259793
+ const members = await listCompanyMembers(companyId);
259794
+ return members.find((m2) => m2.actorId === actorId)?.role;
259795
+ },
259408
259796
  resolveActorContext: (actorId) => buildActorContext(actors.service, actorId),
259409
259797
  defaultBranchResolver,
259410
259798
  // 展示:GET content-locations 给 git 仓补 resolvedDefaultBranch
@@ -260362,6 +260750,15 @@ async function startServe(opts) {
260362
260750
  const fanOutTrace = new FanOutTraceStore({
260363
260751
  listCompanies: async () => pgPool ? (await controlPlaneStore.listCompanies()).filter((c) => c.status === "active").map((c) => c.id) : [defaultCompanyId],
260364
260752
  storeFor: (cid) => traceStoreFor(cid),
260753
+ /**
260754
+ * 去重键:`trace` 未切走的公司全部共用同一个 legacy store,扇出只该查它一次。
260755
+ * 由 store 自己报落点,这里不按 cutover 状态重算(见 FanOutTraceStore.targets 的注释)。
260756
+ * 非 PG / 非 cutover-aware 的路径没有这个方法 ⇒ 退回按公司逐家查,行为不变。
260757
+ */
260758
+ storageKeyFor: async (cid) => {
260759
+ const s2 = await traceStoreFor(cid);
260760
+ return typeof s2.storageKey === "function" ? s2.storageKey() : cid;
260761
+ },
260365
260762
  onError: (cid, op, err) => console.warn(`[trace] ${cid} \u7684 ${op} \u5931\u8D25\uFF08\u5176\u4F59\u516C\u53F8\u7EE7\u7EED\uFF09\uFF1A${String(err)}`)
260366
260763
  });
260367
260764
  const traceSinkCache = /* @__PURE__ */ new Map();
@@ -260412,6 +260809,15 @@ async function startServe(opts) {
260412
260809
  // 只扫 active 的公司:停用公司的台账不该再被兜底扫改写(与本文件其它按公司扇出的地方同口径)。
260413
260810
  listCompanies: async () => (await controlPlaneStore.listCompanies()).filter((c) => c.status === "active").map((c) => c.id),
260414
260811
  ledgerFor: (cid) => dispatchLedgerFor(cid),
260812
+ /**
260813
+ * 去重键:`dispatch` 未切走的公司全部共用同一张 legacy 台账,扇出只该作用一次。
260814
+ * 由台账自己报落点,这里不按 cutover 状态重算(见 FanOutDispatchLedger.targets 的注释)。
260815
+ * 非 PG / 非 cutover-aware 的路径没有这个方法 ⇒ 退回按公司逐家来,行为不变。
260816
+ */
260817
+ storageKeyFor: async (cid) => {
260818
+ const l = await dispatchLedgerFor(cid);
260819
+ return typeof l.storageKey === "function" ? l.storageKey() : cid;
260820
+ },
260415
260821
  onError: (cid, op, err) => console.warn(`[dispatch-ledger] ${cid} \u7684 ${op} \u5931\u8D25\uFF08\u5176\u4F59\u516C\u53F8\u7EE7\u7EED\uFF09\uFF1A${String(err)}`)
260416
260822
  }) : dispatchLedger;
260417
260823
  const companyRegistryFor = async (companyId) => pgPool ? (await openCompanyPlane(companyId)).overlay : scopedRegistryFor(companyId);
@@ -265063,7 +265469,8 @@ var SessionProcessState = class {
265063
265469
  // 只报 dispatchId 的话 serve 重启后它认不出这是条活会话,会当成野进程 evict 掉。
265064
265470
  ...this.spec.job.artifactId ? { jobArtifactId: this.spec.job.artifactId } : {},
265065
265471
  // ADR 0300 D2:自报支持帧号;新服务端据此回带 ackSeq 的 hello-ack,届时才把 serverSupportsDedup 置真。
265066
- supportsFrameSeq: true
265472
+ supportsFrameSeq: true,
265473
+ supportsInputConsumptionReceipts: true
265067
265474
  });
265068
265475
  const toResend = this.serverSupportsDedup ? this.outbox : this.outbox.filter(isKeyFrame);
265069
265476
  for (const m2 of toResend) this.rawSend(m2);
@@ -268370,6 +268777,28 @@ var ASYNC_AUTH_MESSAGE = "\u5DF2\u63D0\u4EA4\u4EBA\u5DE5\u5BA1\u6838\uFF1B\u5148
268370
268777
  function asyncAuthorizationReturn(data) {
268371
268778
  return data !== void 0 ? { message: ASYNC_AUTH_MESSAGE, data } : { message: ASYNC_AUTH_MESSAGE };
268372
268779
  }
268780
+ var AUTH_CARD_GONE_MESSAGE = "\u8FD9\u5F20\u5F85\u6388\u6743\u5361\u5DF2\u7ECF\u4E0D\u5728\u5F85\u5904\u7406\u5217\u8868\u91CC\u4E86\u2014\u2014\u53EF\u80FD\u5DF2\u88AB\u4EBA\u5904\u7F6E\uFF0C\u4E5F\u53EF\u80FD\u5DF2\u5931\u6548\u3002**\u522B\u518D\u8BA9\u7528\u6237\u53BB\u6536\u4EF6\u7BB1\u627E\u8FD9\u5F20\u5361**\uFF1A\u5148\u7528\u53EA\u8BFB\u547D\u4EE4\u6838\u5BF9\u5F53\u524D\u72B6\u6001\uFF08\u5982 `oasis status <\u8282\u70B9>`\uFF09\uFF0C\u786E\u8BA4\u8FD9\u4EF6\u4E8B\u6CA1\u751F\u6548\uFF0C\u518D\u91CD\u65B0\u53D1\u8D77\u540C\u4E00\u6761\u547D\u4EE4\u3002";
268781
+ function authorizationGoneReturn(draftId) {
268782
+ return { message: AUTH_CARD_GONE_MESSAGE, data: { outcome: "authorization-gone", draftId } };
268783
+ }
268784
+ async function draftStillPending(base, headers, draftId) {
268785
+ try {
268786
+ const ctrl = new AbortController();
268787
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
268788
+ timer.unref?.();
268789
+ try {
268790
+ const res = await fetch(`${base}/api/view?name=intervention-drafts`, { headers, signal: ctrl.signal });
268791
+ if (!res.ok) return null;
268792
+ const body2 = await res.json();
268793
+ if (!Array.isArray(body2.data)) return null;
268794
+ return body2.data.some((d) => d?.id === draftId);
268795
+ } finally {
268796
+ clearTimeout(timer);
268797
+ }
268798
+ } catch {
268799
+ return null;
268800
+ }
268801
+ }
268373
268802
  function renderGraphLines(graph) {
268374
268803
  const closureIds = new Set(graph.nodes.filter((n) => n.type === DELIVERY_SUMMARY_ARTIFACT_TYPE).map((n) => n.id));
268375
268804
  const out = ["\n\u8282\u70B9\uFF08\u6539\u56FE\u5F15\u7528\u7528\u8FD9\u4E9B id\uFF09:"];
@@ -268391,6 +268820,10 @@ function resolveAuthWaitMs() {
268391
268820
  }
268392
268821
  async function waitForCommandAuthorization(base, headers, draftId, fallback) {
268393
268822
  const asyncFallback = asyncAuthorizationReturn(fallback.data);
268823
+ const giveUp = async () => {
268824
+ const pending = await draftStillPending(base, headers, draftId);
268825
+ return pending === false ? authorizationGoneReturn(draftId) : asyncFallback;
268826
+ };
268394
268827
  const timeoutMs = resolveAuthWaitMs();
268395
268828
  const controller = new AbortController();
268396
268829
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -268448,12 +268881,13 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
268448
268881
  data: { outcome: "denied", draftId }
268449
268882
  };
268450
268883
  }
268451
- return asyncFallback;
268884
+ if (p2.status === "gone") return authorizationGoneReturn(draftId);
268885
+ return await giveUp();
268452
268886
  }
268453
268887
  }
268454
- return asyncFallback;
268888
+ return await giveUp();
268455
268889
  } catch {
268456
- return asyncFallback;
268890
+ return await giveUp();
268457
268891
  } finally {
268458
268892
  clearTimeout(timer);
268459
268893
  clearInterval(progress);
@@ -271958,7 +272392,7 @@ function shimScript() {
271958
272392
  }
271959
272393
 
271960
272394
  // src/index.ts
271961
- var PKG_VERSION = true ? "2.2.17" : "dev";
272395
+ var PKG_VERSION = true ? "2.2.19" : "dev";
271962
272396
  var LOCAL_BIN = localBin();
271963
272397
  var NPM_PREFIX = npmPrefix();
271964
272398
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.17",
3
+ "version": "2.2.19",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "1c93b971ada0fb88e327a81413365e770998710a"
29
+ "sourceHead": "2b179b35f141bdd0e3785e0cf069d22b76fc8a37"
30
30
  }
31
31
  }