oasis_test_v2 2.2.1 → 2.2.3

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 +2043 -513
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1190,6 +1190,9 @@ function deriveLastTurnQuality(lastAssistant) {
1190
1190
  if (status === "error") return "abnormal";
1191
1191
  return lastAssistant && lastAssistant.content.trim().length > 0 ? "clean" : "abnormal";
1192
1192
  }
1193
+ function deriveSessionRunning(lastAssistant) {
1194
+ return lastAssistant?.status === "running";
1195
+ }
1193
1196
  function windowMessagesByTurns(messages, turns) {
1194
1197
  if (turns <= 0) return { messages, hasMoreBefore: false };
1195
1198
  let seen = 0;
@@ -1340,11 +1343,39 @@ function collectDelegationArtifacts(roundSummaries) {
1340
1343
  const artifacts = /* @__PURE__ */ new Map();
1341
1344
  for (const round of roundSummaries) {
1342
1345
  for (const file of round.files ?? []) {
1343
- artifacts.set(file.name, { name: file.name, ...file.blobRef ? { blobRef: file.blobRef } : {}, round: round.round });
1346
+ const seen = artifacts.get(file.name);
1347
+ if (seen) {
1348
+ if (!seen.blobRef && file.blobRef) seen.blobRef = file.blobRef;
1349
+ continue;
1350
+ }
1351
+ artifacts.set(file.name, {
1352
+ name: file.name,
1353
+ ...file.blobRef ? { blobRef: file.blobRef } : {},
1354
+ round: round.round,
1355
+ // settledSeq 透传自这个名字**第一次出现**的那一轮。未收口的轮次(pending 续跑那一支)
1356
+ // 与本字段落地前的存量行都没有值——前端凭 `settledSeq` 是否缺失走两条路径:
1357
+ // 有 → 按 settledSeq 各自定位对应 `chat-msg:delegation-done|<id>:<seq>`;
1358
+ // 无 → 该委派整条退回「单委派单锚点」降级(`ann:36bfa04b`)。
1359
+ ...typeof round.settledSeq === "number" ? { settledSeq: round.settledSeq } : {}
1360
+ });
1344
1361
  }
1345
1362
  }
1346
1363
  return [...artifacts.values()];
1347
1364
  }
1365
+ function delegationOutboxFingerprints(roundSummaries) {
1366
+ const out = /* @__PURE__ */ new Map();
1367
+ for (const round of roundSummaries) {
1368
+ for (const file of round.files ?? []) {
1369
+ if (typeof file.size !== "number" || !file.mtime) continue;
1370
+ out.set(file.name, { size: file.size, mtime: file.mtime });
1371
+ }
1372
+ }
1373
+ return out;
1374
+ }
1375
+ function touchAnchorMessageId(messageId) {
1376
+ const hash2 = messageId.indexOf("#");
1377
+ return hash2 >= 0 ? messageId.slice(0, hash2) : messageId;
1378
+ }
1348
1379
  var DelegationLabelConflictError, DELEGATION_FILE_LIMITS, DELEGATION_INBOUND_DIR, DELEGATION_OUTBOUND_DIR, DELEGATION_OUTPUTS_DIR, DELEGATION_RETURN_RECENT_ROUNDS;
1349
1380
  var init_delegation = __esm({
1350
1381
  "../contract/src/delegation.ts"() {
@@ -1537,6 +1568,16 @@ var init_http_api = __esm({
1537
1568
  }
1538
1569
  });
1539
1570
 
1571
+ // ../contract/src/file-preview.ts
1572
+ var OFFICE_PREVIEW_MAX_BYTES, FILE_CONTENT_MAX_BYTES;
1573
+ var init_file_preview = __esm({
1574
+ "../contract/src/file-preview.ts"() {
1575
+ "use strict";
1576
+ OFFICE_PREVIEW_MAX_BYTES = 20 * 1024 * 1024;
1577
+ FILE_CONTENT_MAX_BYTES = 50 * 1024 * 1024;
1578
+ }
1579
+ });
1580
+
1540
1581
  // ../contract/src/index.ts
1541
1582
  var init_src = __esm({
1542
1583
  "../contract/src/index.ts"() {
@@ -1596,6 +1637,7 @@ var init_src = __esm({
1596
1637
  init_knowledge();
1597
1638
  init_knowledge_governance();
1598
1639
  init_http_api();
1640
+ init_file_preview();
1599
1641
  }
1600
1642
  });
1601
1643
 
@@ -4036,7 +4078,7 @@ var init_events = __esm({
4036
4078
  // ../engine/src/views.ts
4037
4079
  function workState(w2, hasOutput2) {
4038
4080
  if (w2.status) return w2.status;
4039
- if (w2.retryAt) return "retry";
4081
+ if (w2.retryAt && w2.deadAt) return "retry";
4040
4082
  if (w2.deadAt) return "dead";
4041
4083
  if (w2.endedAt) {
4042
4084
  if (w2.outcome === "failed") return "failed";
@@ -4458,6 +4500,36 @@ function lastPostIsFromNode(state, issue2, node2) {
4458
4500
  function repliedInWork(state, work, issueId) {
4459
4501
  return repliesSorted(state, issueId).some((r) => r.createdAt >= REPLY_ORIGIN_SINCE ? r.viaWorkId === work.id : r.authorActorId === work.assigneeActorId && r.createdAt >= work.createdAt);
4460
4502
  }
4503
+ function recordHandledNotification(state, work, issue2, acknowledged = false) {
4504
+ if (issue2.aboutNodeId !== work.nodeId || work.endedAt || work.deadAt) return;
4505
+ const gap = issue2.gapId ? state.issue(issue2.gapId) : void 0;
4506
+ const ids2 = [issue2.id, ...gap?.kind === "gap" && gap.resolvedAt && gap.raisedByNodeId === work.nodeId ? [gap.id] : []];
4507
+ state.updateWork(work.id, {
4508
+ frozenIssueIds: [.../* @__PURE__ */ new Set([...work.frozenIssueIds ?? [], ...ids2])],
4509
+ ...acknowledged ? { handledIssueIds: [.../* @__PURE__ */ new Set([...work.handledIssueIds ?? [], ...ids2])] } : {}
4510
+ });
4511
+ }
4512
+ function notificationsHandledInWork(state, work) {
4513
+ if (work.conclusion != null) return false;
4514
+ const node2 = state.node(work.nodeId);
4515
+ const accepted = node2?.latestAcceptId ? state.work(node2.latestAcceptId) : void 0;
4516
+ if (!node2 || !accepted || node2.latestAcceptId !== node2.latestProposedWorkId || accepted.nodeVersion !== work.nodeVersion) return false;
4517
+ const previousInputs = new Set(state.workInputsOf(accepted.id).map((i) => i.upstreamWorkId));
4518
+ const currentInputs = new Set(state.workInputsOf(work.id).map((i) => i.upstreamWorkId));
4519
+ if (previousInputs.size !== currentInputs.size || [...currentInputs].some((id) => !previousInputs.has(id))) return false;
4520
+ const ids2 = work.frozenIssueIds ?? [];
4521
+ const handledNow = (work.handledIssueIds ?? []).length > 0 || ids2.some((id) => repliedInWork(state, work, id));
4522
+ return handledNow && ids2.length > 0 && ids2.every((id) => {
4523
+ const issue2 = state.issue(id);
4524
+ if (!issue2) return false;
4525
+ if (issue2.kind === "gap") return issue2.resolvedAt !== null && ((work.handledIssueIds ?? []).includes(id) || ids2.some((relatedId) => {
4526
+ const related = state.issue(relatedId);
4527
+ return related?.kind === "comment" && related.gapId === id && repliedInWork(state, work, relatedId) && lastPostIsFromNode(state, related, node2);
4528
+ }) || state.mainWorksOf(work.nodeId).some((prior) => prior.id !== work.id && prior.status === "success" && ((prior.handledIssueIds ?? []).includes(id) || prior.acceptedAt && (prior.frozenIssueIds ?? []).includes(id))));
4529
+ if (issue2.kind !== "comment" && issue2.kind !== "change_request") return false;
4530
+ return issue2.resolvedAt !== null && (work.handledIssueIds ?? []).includes(id) || issue2.kind === "comment" && repliedInWork(state, work, id) && lastPostIsFromNode(state, issue2, node2);
4531
+ });
4532
+ }
4461
4533
  function threadLength(state, issueId) {
4462
4534
  return state.repliesOf(issueId).length + 1;
4463
4535
  }
@@ -4559,6 +4631,7 @@ var init_kill = __esm({
4559
4631
 
4560
4632
  // ../engine/src/activate.ts
4561
4633
  function scan(state, ctx) {
4634
+ if (state.workorder.dispatchPaused) return { events: [], terminated: false };
4562
4635
  const events = [];
4563
4636
  const now = ctx.at;
4564
4637
  const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
@@ -5651,6 +5724,7 @@ ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS last_activity_at timestamptz;
5651
5724
  -- \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
5652
5725
  -- 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
5653
5726
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS frozen_issue_ids jsonb;
5727
+ ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS handled_issue_ids jsonb;
5654
5728
  -- \u56DE\u4FE1\u8F6E\u6307\u5411\u54EA\u6761 comment issue\uFF08\u7EAF\u6570\u636E\uFF09
5655
5729
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
5656
5730
  -- \u2605 work lane\uFF08\u6B63\u5411\u5224\u636E\uFF0C\u53D6\u4EE3 "reply_to_issue_id IS NULL"\uFF09\uFF1A\u5B58\u91CF\u884C\u6309\u6709\u65E0 reply_to_issue_id \u56DE\u586B\u3002
@@ -5800,7 +5874,7 @@ ALTER TABLE ${q2}.workorders DROP COLUMN IF EXISTS resume_marker;
5800
5874
  ALTER TABLE ${q2}.events DROP COLUMN IF EXISTS effect_lease_until;
5801
5875
  -- \u5B58\u91CF\u56DE\u586B\uFF1Astatus \u5217=null \u7684\u884C\u6309\u65F6\u95F4\u6233/\u4EA7\u51FA\u56DE\u586B\uFF08\u4E0E views.ts \u56DE\u9000\u6D3E\u751F\u540C\u8BED\u4E49\uFF0C\u56DE\u586B\u540E\u72B6\u6001\u5373\u5B58\u50A8\u6001\uFF09
5802
5876
  UPDATE ${q2}.works SET status = CASE
5803
- WHEN retry_at IS NOT NULL THEN 'retry'
5877
+ WHEN retry_at IS NOT NULL AND dead_at IS NOT NULL THEN 'retry'
5804
5878
  WHEN dead_at IS NOT NULL THEN 'dead'
5805
5879
  WHEN ended_at IS NOT NULL AND outcome = 'failed' THEN 'failed'
5806
5880
  WHEN ended_at IS NOT NULL AND (conclusion IS NOT NULL OR
@@ -11107,13 +11181,22 @@ var init_store_postgres = __esm({
11107
11181
  async close() {
11108
11182
  await this.pool.end();
11109
11183
  }
11184
+ /**
11185
+ * ★ 与 {@link tx} 同一条纪律:schema 名进的是**标识符位**,必须 {@link quoteIdent}。
11186
+ *
11187
+ * 这两个直连 pool 的方法(本方法与 {@link listNeverStartedWorks})是上一轮修连字符 schema 时
11188
+ * 漏掉的两处——`tx()` 那条路已经引号化了,它们没有。表现不是「一读就 500」而是**两口钟在带
11189
+ * 连字符的公司上永远转不动**:2026-09-09 v2 生产日志里 `[deadline-clock] [company:test-ryzs]`
11190
+ * 与 `[dispatch-drop]` 每 5 秒 / 每分钟各报一次 `syntax error at or near "-"`,
11191
+ * 一分钟约 50 行;也就是说这些公司的 deadline 最迟触发保证与丢单补派**从来没生效过**。
11192
+ */
11110
11193
  async listOpenWorkDeadlines(opts) {
11111
11194
  const res = await this.pool.query(
11112
11195
  `SELECT workorder_id AS "workorderId",
11113
11196
  id AS "workId",
11114
11197
  assignee_actor_id AS "assigneeActorId",
11115
11198
  COALESCE(last_activity_at, started_at, created_at) AS "deadlineFrom"
11116
- FROM ${this.schema}.works
11199
+ FROM ${quoteIdent(this.schema)}.works
11117
11200
  WHERE ended_at IS NULL AND dead_at IS NULL AND cancelled_at IS NULL
11118
11201
  AND ($1::text IS NULL OR id > $1)
11119
11202
  ORDER BY id
@@ -11134,7 +11217,7 @@ var init_store_postgres = __esm({
11134
11217
  node_id AS "nodeId",
11135
11218
  assignee_actor_id AS "assigneeActorId",
11136
11219
  created_at AS "createdAt"
11137
- FROM ${this.schema}.works
11220
+ FROM ${quoteIdent(this.schema)}.works
11138
11221
  WHERE ended_at IS NULL AND dead_at IS NULL AND cancelled_at IS NULL
11139
11222
  AND started_at IS NULL AND session_ref IS NULL
11140
11223
  AND created_at < $1::timestamptz
@@ -11280,6 +11363,7 @@ var init_store_postgres = __esm({
11280
11363
  proposalReason: w2.proposal_reason ?? null,
11281
11364
  lastActivityAt: iso(w2.last_activity_at),
11282
11365
  frozenIssueIds: w2.frozen_issue_ids ?? null,
11366
+ handledIssueIds: w2.handled_issue_ids ?? null,
11283
11367
  agentHandoffAttemptId: w2.agent_handoff_attempt_id ?? null,
11284
11368
  businessHandoffStatus: w2.business_handoff_status ?? null,
11285
11369
  businessHandoffRecordedAt: iso(w2.business_handoff_recorded_at),
@@ -11485,8 +11569,8 @@ var init_store_postgres = __esm({
11485
11569
  (id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
11486
11570
  outcome_detail,session_ref,continues_work_id,parent_work_id,reply_to_issue_id,output_version_no,
11487
11571
  node_version,conclusion,proposal_reason,last_activity_at,frozen_issue_ids,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
11488
- acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason,lane)
11489
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
11572
+ acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason,lane,handled_issue_ids)
11573
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34)
11490
11574
  ON CONFLICT (id) DO NOTHING`,
11491
11575
  [
11492
11576
  w2.id,
@@ -11521,13 +11605,14 @@ var init_store_postgres = __esm({
11521
11605
  w2.rejectedReason,
11522
11606
  w2.overrideBy,
11523
11607
  w2.overrideReason,
11524
- w2.lane
11608
+ w2.lane,
11609
+ w2.handledIssueIds ? JSON.stringify(w2.handledIssueIds) : null
11525
11610
  ]
11526
11611
  );
11527
11612
  break;
11528
11613
  }
11529
11614
  case "work.update":
11530
- await this.patch("works", "id", m2.id, m2.patch, { outcomeDetail: true });
11615
+ await this.patch("works", "id", m2.id, m2.patch, { outcomeDetail: true, frozenIssueIds: true, handledIssueIds: true });
11531
11616
  break;
11532
11617
  case "artifact.replace":
11533
11618
  await this.c.query(`DELETE FROM ${this.s}.work_artifacts WHERE work_id = $1`, [m2.workId]);
@@ -11857,7 +11942,7 @@ function newNode(id, workorderId, at, version2) {
11857
11942
  updatedAt: at
11858
11943
  };
11859
11944
  }
11860
- function markNodeForRetry(state, nodeId, at) {
11945
+ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
11861
11946
  const node2 = state.node(nodeId);
11862
11947
  if (!node2) return;
11863
11948
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
@@ -11865,6 +11950,7 @@ function markNodeForRetry(state, nodeId, at) {
11865
11950
  state.updateWork(lw.id, { retryAt: at, status: "retry" });
11866
11951
  }
11867
11952
  if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
11953
+ if (resetRejectedBudget && lw.acceptanceState === "rejected") state.updateWork(lw.id, { retryAt: at });
11868
11954
  for (const req of state.requirementsOf(node2.id)) {
11869
11955
  if (!req.latestReviewId) continue;
11870
11956
  const r = state.review(req.latestReviewId);
@@ -11996,6 +12082,9 @@ var init_plan = __esm({
11996
12082
  const changed = node2.assigneeActorId !== e.assigneeActorId;
11997
12083
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
11998
12084
  const settled = lw !== null && lw !== void 0 && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success";
12085
+ if (changed && e.resetRejectedBudget === true && lw && !lw.deadAt && settled && lw.acceptanceState === "rejected") {
12086
+ state.updateWork(lw.id, { retryAt: ctx.at });
12087
+ }
11999
12088
  state.updateNode(e.nodeId, {
12000
12089
  assigneeActorId: e.assigneeActorId,
12001
12090
  assigneeRole: e.assigneeRole,
@@ -12025,7 +12114,7 @@ var init_plan = __esm({
12025
12114
  name: "plan/node-retry",
12026
12115
  kind: "plan.node_retry",
12027
12116
  apply(e, state, ctx) {
12028
- markNodeForRetry(state, e.nodeId, ctx.at);
12117
+ markNodeForRetry(state, e.nodeId, ctx.at, e.resetRejectedBudget === true);
12029
12118
  }
12030
12119
  };
12031
12120
  planUpdateFields = {
@@ -12158,7 +12247,13 @@ var init_workorder = __esm({
12158
12247
  apply(e, state, ctx) {
12159
12248
  state.updateWorkorder({ dispatchPaused: false, pausedBy: null, pausedReason: null, updatedAt: ctx.at });
12160
12249
  for (const node2 of state.liveNodes()) {
12161
- planNodeRetry.apply({ kind: "plan.node_retry", workorderId: e.workorderId, nodeId: node2.id, by: e.by }, state, ctx);
12250
+ planNodeRetry.apply({
12251
+ kind: "plan.node_retry",
12252
+ workorderId: e.workorderId,
12253
+ nodeId: node2.id,
12254
+ by: e.by,
12255
+ resetRejectedBudget: e.resetRejectedBudget
12256
+ }, state, ctx);
12162
12257
  }
12163
12258
  }
12164
12259
  };
@@ -12172,8 +12267,8 @@ var init_workorder = __esm({
12172
12267
  });
12173
12268
 
12174
12269
  // ../engine/src/handlers/work.ts
12175
- function latestProposedOf(state, nodeId) {
12176
- const successes = state.mainWorksOf(nodeId).filter((w2) => w2.status === "success").sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
12270
+ function latestProposedOf(state, nodeId, deliveriesOnly) {
12271
+ const successes = state.mainWorksOf(nodeId).filter((w2) => w2.status === "success" && (!deliveriesOnly || state.artifactsOf(w2.id).length > 0 || w2.conclusion != null)).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
12177
12272
  return successes.length > 0 ? successes[successes.length - 1].id : null;
12178
12273
  }
12179
12274
  var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workContentEdited, workConclude, nodeLatestProposedRebased, workTimeout;
@@ -12188,6 +12283,7 @@ var init_work = __esm({
12188
12283
  name: "work/create",
12189
12284
  kind: "work.create",
12190
12285
  apply(e, state, ctx) {
12286
+ if (e.pauseGateVersion === 1 && state.workorder.dispatchPaused) return;
12191
12287
  if (state.work(e.workId)) return;
12192
12288
  const node2 = state.node(e.nodeId);
12193
12289
  if (!node2) return;
@@ -12213,6 +12309,7 @@ var init_work = __esm({
12213
12309
  replyToIssueId: e.replyToIssueId,
12214
12310
  outputVersionNo: null,
12215
12311
  conclusion: null,
12312
+ handledIssueIds: null,
12216
12313
  agentHandoffAttemptId: null,
12217
12314
  businessHandoffStatus: null,
12218
12315
  businessHandoffRecordedAt: null,
@@ -12252,6 +12349,7 @@ var init_work = __esm({
12252
12349
  outputVersionNo: null,
12253
12350
  conclusion: null,
12254
12351
  frozenIssueIds: frozen.length > 0 ? frozen : null,
12352
+ handledIssueIds: null,
12255
12353
  agentHandoffAttemptId: null,
12256
12354
  businessHandoffStatus: null,
12257
12355
  businessHandoffRecordedAt: null,
@@ -12286,6 +12384,7 @@ var init_work = __esm({
12286
12384
  // 会话侧也要杀(旧 agent 可能还挂着)。cancelPreviousWork 找不到/会话已退则 no-op。
12287
12385
  // 回信 work 不杀前会话(apply 不 kill 主链,节点已完结、无在跑会话),只派发。
12288
12386
  async effect(e, ctx) {
12387
+ if (e.pauseGateVersion === 1 && !await ctx.canDispatchWork(e.workorderId, e.workId)) return;
12289
12388
  if (!e.replyToIssueId) await ctx.cancelPreviousWork(e.nodeId, e.workId);
12290
12389
  await ctx.dispatchWork(e.workId, e.resumeEligible !== void 0 ? { resumeEligible: e.resumeEligible } : void 0);
12291
12390
  }
@@ -12316,7 +12415,7 @@ var init_work = __esm({
12316
12415
  if (node2.latestProposedWorkId) {
12317
12416
  const ls = state.work(node2.latestProposedWorkId);
12318
12417
  if (!ls || ls.status !== "success") {
12319
- state.updateNode(e.nodeId, { latestProposedWorkId: latestProposedOf(state, e.nodeId) });
12418
+ state.updateNode(e.nodeId, { latestProposedWorkId: latestProposedOf(state, e.nodeId, e.semanticsVersion === 2) });
12320
12419
  }
12321
12420
  }
12322
12421
  killNodeReviews(state, e.nodeId, ctx.at);
@@ -12346,17 +12445,34 @@ var init_work = __esm({
12346
12445
  }
12347
12446
  const businessFailed = w2.businessHandoffStatus === "not_delivered";
12348
12447
  const artifactCount = state.artifactsOf(e.workId).length;
12349
- const pureReply = e.semanticsVersion === 2 && artifactCount === 0 && (w2.conclusion === null || w2.conclusion === void 0) && (w2.frozenIssueIds ?? []).some((issueId) => state.issue(issueId)?.kind === "comment" && repliedInWork(state, w2, issueId));
12350
- const succeeded = e.outcome === "completed" && !businessFailed && (e.semanticsVersion === 2 ? artifactCount > 0 || pureReply : hasOutput(w2, artifactCount));
12448
+ const pureReply = (e.semanticsVersion ?? 1) >= 2 && artifactCount === 0 && (w2.conclusion === null || w2.conclusion === void 0) && (w2.frozenIssueIds ?? []).some((issueId) => state.issue(issueId)?.kind === "comment" && repliedInWork(state, w2, issueId));
12449
+ const succeeded = e.outcome === "completed" && !businessFailed && ((e.semanticsVersion ?? 1) >= 2 ? artifactCount > 0 || (e.semanticsVersion === 3 ? notificationsHandledInWork(state, w2) : pureReply) : hasOutput(w2, artifactCount));
12351
12450
  const status = succeeded ? "success" : "failed";
12352
- const zeroOutputReason = e.semanticsVersion === 2 && !succeeded && e.outcome === "completed" && !businessFailed ? artifactCount === 0 && w2.conclusion ? "conclusion without artifacts (invariant violation)" : "zero output" : null;
12451
+ if (e.semanticsVersion === 3 && artifactCount === 0 && e.outcome === "completed") {
12452
+ const node2 = state.node(w2.nodeId);
12453
+ if (node2) for (const id of w2.frozenIssueIds ?? []) {
12454
+ const issue2 = state.issue(id);
12455
+ if (issue2?.kind === "comment" && !issue2.resolvedAt && repliedInWork(state, w2, id) && lastPostIsFromNode(state, issue2, node2)) {
12456
+ state.emit({
12457
+ kind: "issue.resolve",
12458
+ workorderId: e.workorderId,
12459
+ issueId: id,
12460
+ state: "resolved",
12461
+ by: w2.assigneeActorId,
12462
+ note: null,
12463
+ viaWorkId: w2.id
12464
+ });
12465
+ }
12466
+ }
12467
+ }
12468
+ const zeroOutputReason = (e.semanticsVersion ?? 1) >= 2 && !succeeded && e.outcome === "completed" && !businessFailed ? artifactCount === 0 && w2.conclusion ? "conclusion without artifacts (invariant violation)" : e.semanticsVersion === 3 && (w2.frozenIssueIds ?? []).length > 0 ? "notifications incomplete or delivery requirements changed" : "zero output" : null;
12353
12469
  state.updateWork(e.workId, {
12354
12470
  endedAt: ctx.at,
12355
12471
  outcome: businessFailed ? "failed" : e.outcome,
12356
12472
  outcomeDetail: businessFailed ? { ...e.detail ?? {}, reason: "agent business handoff not delivered" } : zeroOutputReason ? { ...e.detail ?? {}, reason: e.detail?.reason ?? zeroOutputReason } : e.detail,
12357
12473
  status
12358
12474
  });
12359
- if (status === "success" && !(e.semanticsVersion === 2 && artifactCount === 0)) {
12475
+ if (status === "success" && !((e.semanticsVersion ?? 1) >= 2 && artifactCount === 0)) {
12360
12476
  state.updateNode(w2.nodeId, { latestProposedWorkId: e.workId });
12361
12477
  }
12362
12478
  }
@@ -12380,7 +12496,7 @@ var init_work = __esm({
12380
12496
  } : {}
12381
12497
  });
12382
12498
  if (flipsToFailed && state.node(w2.nodeId)?.latestProposedWorkId === e.workId) {
12383
- state.updateNode(w2.nodeId, { latestProposedWorkId: latestProposedOf(state, w2.nodeId) });
12499
+ state.updateNode(w2.nodeId, { latestProposedWorkId: latestProposedOf(state, w2.nodeId, e.semanticsVersion === 2) });
12384
12500
  }
12385
12501
  }
12386
12502
  };
@@ -12498,6 +12614,7 @@ var init_issue = __esm({
12498
12614
  "../engine/src/handlers/issue.ts"() {
12499
12615
  "use strict";
12500
12616
  init_plan();
12617
+ init_letters();
12501
12618
  issueCreate = {
12502
12619
  name: "issue/create",
12503
12620
  kind: "issue.create",
@@ -12568,6 +12685,11 @@ var init_issue = __esm({
12568
12685
  // 缺失(人 / chat 会话 / 存量事件)落 null,判据按"不是节点发言"处理。
12569
12686
  viaWorkId: e.viaNodeId ? state.node(e.viaNodeId)?.latestWorkId ?? null : null
12570
12687
  });
12688
+ const workId = e.viaNodeId ? state.node(e.viaNodeId)?.latestWorkId : null;
12689
+ const work = workId ? state.work(workId) : void 0;
12690
+ if (e.handlingVersion === 1 && work && work.assigneeActorId === e.authorActorId && ctx.actorId === e.authorActorId) {
12691
+ recordHandledNotification(state, work, issue2);
12692
+ }
12571
12693
  }
12572
12694
  };
12573
12695
  issueResolve = {
@@ -12575,7 +12697,13 @@ var init_issue = __esm({
12575
12697
  kind: "issue.resolve",
12576
12698
  apply(e, state, ctx) {
12577
12699
  const issue2 = state.issue(e.issueId);
12578
- if (!issue2 || issue2.resolvedAt !== null) return;
12700
+ if (!issue2) return;
12701
+ const workId = e.viaNodeId ? state.node(e.viaNodeId)?.latestWorkId : null;
12702
+ const work = workId ? state.work(workId) : void 0;
12703
+ if (e.acknowledged && work && work.assigneeActorId === e.by && ctx.actorId === e.by && e.note?.trim() && issue2.kind !== "escalation" && (issue2.kind !== "gap" || issue2.resolvedAt !== null)) {
12704
+ recordHandledNotification(state, work, issue2, true);
12705
+ }
12706
+ if (issue2.resolvedAt !== null) return;
12579
12707
  const humanResolution = e.by.startsWith("actor:human:") && e.by === ctx.actorId;
12580
12708
  if (e.semanticsVersion === 2 && issue2.kind === "escalation" && !humanResolution) {
12581
12709
  throw new Error("\u5173\u95ED\u5347\u7EA7\u9700\u8981\u4EBA\u660E\u786E\u786E\u8BA4\uFF1Bagent \u8BF7\u63D0\u4EA4\u5173\u95ED\u6388\u6743\u7533\u8BF7");
@@ -12590,7 +12718,7 @@ var init_issue = __esm({
12590
12718
  resolvedNote: e.note,
12591
12719
  resolvedViaWorkId: e.viaWorkId
12592
12720
  });
12593
- if (issue2.kind === "gap" && (e.semanticsVersion !== 2 || humanResolution)) {
12721
+ if (issue2.kind === "gap" && e.closeRelatedEscalations !== false && (e.semanticsVersion !== 2 || humanResolution)) {
12594
12722
  for (const other of state.allIssues()) {
12595
12723
  if (other.kind === "escalation" && other.resolvedAt === null && other.workorderId === issue2.workorderId && other.gapId === issue2.id) {
12596
12724
  state.updateIssue(other.id, {
@@ -12608,7 +12736,7 @@ var init_issue = __esm({
12608
12736
  }
12609
12737
  if (issue2.kind === "gap" || issue2.kind === "escalation") {
12610
12738
  const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
12611
- if (nodeId) markNodeForRetry(state, nodeId, ctx.at);
12739
+ if (nodeId) markNodeForRetry(state, nodeId, ctx.at, e.resetRejectedBudget === true);
12612
12740
  }
12613
12741
  }
12614
12742
  };
@@ -12646,6 +12774,7 @@ var init_review = __esm({
12646
12774
  name: "review/create",
12647
12775
  kind: "review.create",
12648
12776
  apply(e, state, ctx) {
12777
+ if (e.pauseGateVersion === 1 && state.workorder.dispatchPaused) return;
12649
12778
  const work = state.work(e.targetWorkId);
12650
12779
  if (!work) return;
12651
12780
  const dup = state.reviewsOf(e.targetWorkId).some(
@@ -12681,6 +12810,7 @@ var init_review = __esm({
12681
12810
  }
12682
12811
  },
12683
12812
  async effect(e, ctx) {
12813
+ if (e.pauseGateVersion === 1 && !await ctx.canDispatchReview(e.workorderId, e.reviewId)) return;
12684
12814
  if (e.reviewerIsAgent) await ctx.dispatchReview(e.reviewId, e.nodeId, e.reviewerActorId);
12685
12815
  else await ctx.notify([e.reviewerActorId], { kind: "review-requested", reviewId: e.reviewId });
12686
12816
  }
@@ -12987,11 +13117,28 @@ var init_handlers = __esm({
12987
13117
  });
12988
13118
 
12989
13119
  // ../engine/src/bus.ts
12990
- function versionEscalationResolution(event) {
12991
- if (event.kind === "work.accept" || event.kind === "issue.resolve") {
12992
- return { ...event, semanticsVersion: 2 };
13120
+ function versionCommandEvent(event) {
13121
+ if (event.kind === "work.create" || event.kind === "review.create") {
13122
+ return { ...event, pauseGateVersion: 1 };
13123
+ }
13124
+ if (event.kind === "issue.reply") return { ...event, handlingVersion: 1 };
13125
+ if (event.kind === "work.response" && event.semanticsVersion === 2) {
13126
+ return { ...event, semanticsVersion: 3 };
13127
+ }
13128
+ switch (event.kind) {
13129
+ case "work.accept":
13130
+ case "work.kill":
13131
+ case "work.handoff_recorded":
13132
+ return { ...event, semanticsVersion: 2 };
13133
+ case "issue.resolve":
13134
+ return { ...event, semanticsVersion: 2, resetRejectedBudget: true };
13135
+ case "plan.node_retry":
13136
+ case "workorder.resumed":
13137
+ case "plan.assign_actor":
13138
+ return { ...event, resetRejectedBudget: true };
13139
+ default:
13140
+ return event;
12993
13141
  }
12994
- return event;
12995
13142
  }
12996
13143
  function emptySnapshotPlaceholder(record8) {
12997
13144
  return {
@@ -13199,7 +13346,7 @@ var init_bus = __esm({
13199
13346
  workorderId: input.workorderId,
13200
13347
  actorId: input.actorId,
13201
13348
  handActorId: input.handActorId ?? null,
13202
- event: versionEscalationResolution(input.event),
13349
+ event: versionCommandEvent(input.event),
13203
13350
  causedBySeq: null,
13204
13351
  origin: "command",
13205
13352
  dedupKey: input.dedupKey ?? null,
@@ -13224,6 +13371,32 @@ var init_bus = __esm({
13224
13371
  }
13225
13372
  }
13226
13373
  }
13374
+ /** 同一命令的一组最终修改一起入队。scan-on-empty 在末项之前始终看到 pending,不能派发中间状态。 */
13375
+ async submitBatchAndProcess(inputs) {
13376
+ if (!inputs.length) return;
13377
+ const seqs = await this.store.transaction(async (tx) => {
13378
+ for (const wid of [...new Set(inputs.map((i) => i.workorderId))].sort()) await tx.lockWorkorder(wid);
13379
+ return tx.appendEvents(inputs.map((input) => ({
13380
+ companyId: input.companyId,
13381
+ workorderId: input.workorderId,
13382
+ actorId: input.actorId,
13383
+ handActorId: input.handActorId ?? null,
13384
+ event: versionCommandEvent(input.event),
13385
+ causedBySeq: null,
13386
+ origin: "command",
13387
+ dedupKey: input.dedupKey ?? null,
13388
+ createdAt: this.clock.now(),
13389
+ ...eventAnchors(input.event)
13390
+ })));
13391
+ });
13392
+ if (this.running) {
13393
+ const last = /* @__PURE__ */ new Map();
13394
+ inputs.forEach((input, i) => last.set(input.workorderId, seqs[i]));
13395
+ for (const [wid, seq] of last) await this.waitForSettled(wid, seq);
13396
+ } else {
13397
+ for (let i = 0; i < 2e3; i++) if (!await this.drainOne()) break;
13398
+ }
13399
+ }
13227
13400
  /* ═══════════════════════ 调度器主循环 ═══════════════════════ */
13228
13401
  /**
13229
13402
  * 启动调度器——永不返回。
@@ -13459,7 +13632,7 @@ var init_bus = __esm({
13459
13632
  workorderId: record8.workorderId,
13460
13633
  actorId: SYSTEM_ACTOR,
13461
13634
  handActorId: null,
13462
- event: versionEscalationResolution(event),
13635
+ event: versionCommandEvent(event),
13463
13636
  causedBySeq: record8.seq,
13464
13637
  origin: "apply",
13465
13638
  dedupKey: null,
@@ -13490,6 +13663,18 @@ var init_bus = __esm({
13490
13663
  // ★ 注入 workorderId:派发侧读模型没追上时,据它指名重读那张工单(见 EngineIO.dispatchWork 注释)。
13491
13664
  dispatchWork: (id, opts) => this.io.dispatchWork(id, { ...opts, workorderId: record8.workorderId }),
13492
13665
  dispatchReview: (id, nodeId, reviewerActorId) => this.io.dispatchReview(id, nodeId, reviewerActorId),
13666
+ canDispatchWork: async (workorderId, workId) => {
13667
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(workorderId));
13668
+ if (!snap || snap.workorder.dispatchPaused) return false;
13669
+ const work = snap.works.find((row) => row.id === workId);
13670
+ return work?.status === "running" && !work.deadAt && !work.endedAt;
13671
+ },
13672
+ canDispatchReview: async (workorderId, reviewId) => {
13673
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(workorderId));
13674
+ if (!snap || snap.workorder.dispatchPaused) return false;
13675
+ const review = snap.reviews.find((row) => row.id === reviewId);
13676
+ return review?.status === "running" && !review.cancelledAt && !review.endedAt;
13677
+ },
13493
13678
  cancelSession: (ref2) => this.io.cancelSession(ref2),
13494
13679
  // ★ work.create 的「kill 旧会话」:apply 已把该节点上一个 latest_work 置 deadAt(会话可能还挂着),
13495
13680
  // 这里按 store 找它 cancel。找不到 / 不是刚被 kill / 会话已退 → no-op。这是设计「节点重新运行 = 自动
@@ -13556,7 +13741,7 @@ var init_bus = __esm({
13556
13741
  workorderId: record8.workorderId,
13557
13742
  actorId: b2.actorId ?? SYSTEM_ACTOR,
13558
13743
  handActorId: null,
13559
- event: versionEscalationResolution(b2.event),
13744
+ event: versionCommandEvent(b2.event),
13560
13745
  causedBySeq: record8.seq,
13561
13746
  origin: "effect",
13562
13747
  dedupKey: b2.dedupKey ?? null,
@@ -13594,7 +13779,7 @@ var init_bus = __esm({
13594
13779
  workorderId,
13595
13780
  actorId: SYSTEM_ACTOR,
13596
13781
  handActorId: null,
13597
- event: versionEscalationResolution(event),
13782
+ event: versionCommandEvent(event),
13598
13783
  causedBySeq: null,
13599
13784
  origin: "apply",
13600
13785
  dedupKey: null,
@@ -13646,7 +13831,7 @@ var init_bus = __esm({
13646
13831
  workorderId: record8.workorderId,
13647
13832
  actorId: SYSTEM_ACTOR,
13648
13833
  handActorId: null,
13649
- event: versionEscalationResolution(event),
13834
+ event: versionCommandEvent(event),
13650
13835
  causedBySeq: record8.seq,
13651
13836
  origin: "apply",
13652
13837
  dedupKey: null,
@@ -14492,6 +14677,7 @@ async function assembleContext(args) {
14492
14677
  const siblingParts = args.part !== void 0 ? (artifact.parts ?? []).filter((p2) => p2.name !== args.part) : [];
14493
14678
  const consumers = consumersOf(model, artifactId).filter((c) => c.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE);
14494
14679
  const staleSet = staleSetOf(model, artifactId);
14680
+ const acceptedHead = concludedHead(model, artifactId);
14495
14681
  const head = artifact.currentRev;
14496
14682
  const headRev = head !== null ? model.revisions.get(head) : null;
14497
14683
  const headSpread = headRev !== null ? await spreadRevisionContent(blobs, headRev, files, "deliverable") : null;
@@ -14807,10 +14993,19 @@ ${manifestDiff(baseC, newC)}
14807
14993
  } else if (head === null) {
14808
14994
  whyLines.push(typeDef?.contentType === "code" ? `**\u672C\u4EA7\u7269\u5728 Oasis \u91CC\u8FD8\u6CA1\u6709\u5DF2\u4EA4\u4ED8\u7684\u7248\u672C**\u2014\u2014\u4F46**\u8FD9\u4E0D\u7B49\u4E8E git \u91CC\u6CA1\u6709\u4E1C\u897F**\uFF1A\u4E0A\u4E00\u8F6E\u53EF\u80FD\u63A8\u4E86\u5206\u652F\u5374\u6CA1\u6765\u5F97\u53CA\u4EA4\u4ED8\u3002**\u4EE5\u4F60 fetch \u5230\u7684\u5206\u652F\u5B9E\u51B5\u4E3A\u51C6**\uFF08\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\u7B2C 1 \u6B65\uFF09\u3002` : canvasMode ? `**\u5168\u65B0\u4EA7\u7269**\uFF1A\u8FD9\u662F\u9996\u7248\u2014\u2014\u672C\u4EA7\u7269\u8FD8\u6CA1\u6709\u4EFB\u4F55\u63D0\u4EA4\uFF08\u600E\u4E48\u843D\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002` : `**\u5168\u65B0\u4EA7\u7269**\uFF1A\`deliverable/\` \u4E3A\u7A7A\uFF0C\u8FD8\u6CA1\u6709\u4EFB\u4F55\u5185\u5BB9\u3002`);
14809
14995
  whatLines.push(typeDef?.contentType === "code" ? `\u6309\u300C\u600E\u4E48\u63D0\u4EA4\u300D\u7B2C 1 \u6B65\u843D\u5230\u5DE5\u5355\u5206\u652F\u3001**\u5148\u770B\u6E05\u5B83\u4E0A\u9762\u5DF2\u7ECF\u6709\u4EC0\u4E48**\uFF0C\u518D\u63A5\u7740\u505A\u5B8C\uFF08\u600E\u4E48\u843D\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002` : `\u4EA7\u51FA**\u9996\u7248**\u5185\u5BB9\uFF08\u600E\u4E48\u843D\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002`);
14996
+ } else if (acceptedHead) {
14997
+ whyLines.push("**\u672C\u8F6E\u5DE5\u4F5C**\uFF1A\u6309\u5F53\u524D\u4EFB\u52A1\u8981\u6C42\u3001\u901A\u77E5\u53CA\u6062\u590D\u8BF4\u660E\u6838\u5BF9\u5DF2\u6709\u4EA4\u4ED8\u3002");
14998
+ whatLines.push("\u82E5\u6709\u771F\u5B9E\u7684\u65B0\u4FEE\u6539\u8981\u6C42\uFF0C\u5B8C\u6210\u4FEE\u6539\u540E\u518D\u4EA4\u4ED8\uFF1B\u82E5\u672C\u8F6E\u4EC5\u5904\u7406\u901A\u77E5\u4E14\u65E0\u9700\u6539\u7A3F\uFF0C\u9010\u6761\u660E\u786E\u6807\u8BB0\u5DF2\u5904\u7406\u540E\u6536\u5DE5\uFF0C\u4E0D\u5FC5\u91CD\u65B0 propose / conclude\u3002");
14810
14999
  } else {
14811
15000
  whyLines.push(`**\u7EE7\u7EED\u5B8C\u5584**\uFF1A\u4E0A\u6B21\u672A conclude\u3002`);
14812
15001
  whatLines.push(`**\u63A5\u7740\u628A\u5B83\u505A\u5B8C**\u3001\u771F\u6B63\u5B8C\u6574\u4E86\u518D conclude\uFF08\u600E\u4E48\u843D\u89C1\u300C\u600E\u4E48\u63D0\u4EA4\u300D\uFF09\u3002`);
14813
15002
  }
15003
+ if (acceptedHead) {
15004
+ whyLines.unshift(`**\u5DF2\u6709\u4EA4\u4ED8\u5DF2\u9A8C\u6536**\uFF1A${acceptedHead}\u3002\u6267\u884C\u8F6E\u6B21\u4E0E\u4EA4\u4ED8\u7248\u672C\u5206\u5F00\u8BA1\u6570\uFF0C\u672C\u8F6E\u88AB\u5524\u8D77\u4E0D\u4F1A\u64A4\u9500\u8BE5\u9A8C\u6536\u3002`);
15005
+ for (const gap of freshGaps) {
15006
+ whatLines.push(`\u6838\u5BF9\u7F3A\u53E3 ${gap.gapId} \u7684\u89E3\u6CD5\uFF1B\u786E\u8BA4\u65E0\u9700\u4FEE\u6539\u65F6\uFF1A\`oasis resolve ${gap.gapId} --as acknowledged --note "<\u6838\u5BF9\u4F9D\u636E\u53CA\u65E0\u9700\u4FEE\u6539\u7684\u539F\u56E0>"\`\u3002`);
15007
+ }
15008
+ }
14814
15009
  if (pendingConcludeAttempt(model, artifactId) !== null) {
14815
15010
  whyLines.push(
14816
15011
  ``,
@@ -18040,7 +18235,7 @@ function bridgeLoadReadModel(input) {
18040
18235
  const art = nodeToArtifact(node2, edgesByTarget.get(node2.id) ?? [], requirementsByNode.get(node2.id) ?? []);
18041
18236
  const lw = node2.latestWorkId ? workById.get(node2.latestWorkId) : void 0;
18042
18237
  if (lw) {
18043
- const st = lw.status ?? (lw.retryAt ? "retry" : lw.deadAt ? "dead" : lw.endedAt ? lw.outcome === "failed" ? "failed" : "success" : "running");
18238
+ const st = lw.status ?? (lw.retryAt && lw.deadAt ? "retry" : lw.deadAt ? "dead" : lw.endedAt ? lw.outcome === "failed" ? "failed" : "success" : "running");
18044
18239
  const hasConclusionWithoutResponse = lw.conclusion !== null && lw.conclusion !== void 0 && st === "running" && !lw.endedAt;
18045
18240
  let uiSt;
18046
18241
  if (st === "retry") uiSt = null;
@@ -18543,22 +18738,18 @@ ${res.candidates.map((c) => ` - ${c.type}:${c.title ?? "(\u65E0title)"} \u2192
18543
18738
  return { ...plan, ops };
18544
18739
  }
18545
18740
  function buildConstituents(model, plan, schema, roleIndex) {
18546
- const already = new Set(
18547
- plan.ops.filter((o) => o.action === "unlink").map((o) => `${o.artifactId} ${o.to}`)
18548
- );
18741
+ const inputs = new Map([...model.artifacts].map(([id, art]) => [id, new Set(art.inputs.map((i) => i.to))]));
18549
18742
  const out = [];
18550
18743
  for (const c of buildConstituentsRaw(model, plan, schema, roleIndex)) {
18744
+ if (c.kind === "spawn_artifact") inputs.set(c.artifactId, new Set(c.payload.inputs.map((i) => i.to)));
18745
+ if (c.kind === "link_input") inputs.get(c.artifactId)?.add(c.payload.to);
18746
+ if (c.kind === "unlink_input") inputs.get(c.artifactId)?.delete(c.payload.to);
18551
18747
  if (c.kind === "seal" && c.payload.reason === "cancelled") {
18552
- for (const edge of edgesTouching(model, c.artifactId)) {
18553
- const key = `${edge.artifactId} ${edge.to}`;
18554
- if (already.has(key)) continue;
18555
- already.add(key);
18556
- out.push({
18557
- artifactId: edge.artifactId,
18558
- kind: "unlink_input",
18559
- target: edge.artifactId,
18560
- payload: { to: edge.to }
18561
- });
18748
+ for (const [id, upstreams] of inputs) {
18749
+ for (const to of upstreams) if (id === c.artifactId || to === c.artifactId) {
18750
+ out.push({ artifactId: id, kind: "unlink_input", target: id, payload: { to } });
18751
+ upstreams.delete(to);
18752
+ }
18562
18753
  }
18563
18754
  }
18564
18755
  out.push(c);
@@ -18897,6 +19088,10 @@ function filterReadModelByWorkspace(model, workspace) {
18897
19088
  pinMeta: new Map([...model.pinMeta].filter(([artId]) => myArtIds.has(artId)))
18898
19089
  };
18899
19090
  }
19091
+ function assertWorkorderDispatchActive(snap) {
19092
+ if (!snap?.workorder.dispatchPaused) return;
19093
+ throw new KernelError("\u5DE5\u5355\u5DF2\u6682\u505C\u2014\u2014\u8BF7\u5148\u901A\u8FC7\u5DE5\u5355\u64CD\u4F5C\u6062\u590D\u8FD0\u884C\uFF0C\u518D\u6267\u884C\u8282\u70B9\u7EA7\u8C03\u5EA6\u64CD\u4F5C", "workorder-paused");
19094
+ }
18900
19095
  async function mapWithConcurrency(items, limit, fn) {
18901
19096
  if (items.length === 0) return [];
18902
19097
  const out = new Array(items.length);
@@ -19532,6 +19727,7 @@ var init_kernel_bridge = __esm({
19532
19727
  }
19533
19728
  if (!workId) {
19534
19729
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
19730
+ assertWorkorderDispatchActive(snap);
19535
19731
  const nodeRow = snap?.nodes.find((n) => n.id === args.artifactId);
19536
19732
  const headId = nodeRow?.latestWorkId ?? null;
19537
19733
  if (headId) {
@@ -19953,6 +20149,30 @@ var init_kernel_bridge = __esm({
19953
20149
  });
19954
20150
  }
19955
20151
  async resolveAnnotation(args) {
20152
+ if (args.resolution === "acknowledged" && args.viaNode) {
20153
+ const gap = (this.model.gaps.get(args.viaNode) ?? []).find((g2) => g2.gapId === args.annotationId);
20154
+ if (gap) {
20155
+ if (!gap.resolved || !args.note?.trim()) throw new Error("\u53EA\u80FD\u786E\u8BA4\u5DF2\u89E3\u51B3\u7684\u7F3A\u53E3\uFF0C\u4E14\u9700\u8BF4\u660E\u4E3A\u4F55\u65E0\u9700\u6539\u7A3F");
20156
+ const wid2 = this.wo(args.viaNode);
20157
+ await this.commit({
20158
+ companyId: "",
20159
+ workorderId: wid2,
20160
+ actorId: args.actor,
20161
+ event: {
20162
+ kind: "issue.resolve",
20163
+ workorderId: wid2,
20164
+ issueId: gap.gapId,
20165
+ state: "resolved",
20166
+ by: args.actor,
20167
+ note: args.note,
20168
+ viaWorkId: null,
20169
+ viaNodeId: args.viaNode,
20170
+ acknowledged: true
20171
+ }
20172
+ });
20173
+ return;
20174
+ }
20175
+ }
19956
20176
  const ann = this.model.annotations.get(args.annotationId);
19957
20177
  if (!ann) throw new Error(`annotation not found: ${args.annotationId}`);
19958
20178
  if (isHumanChangeRequest(ann) && args.actor.startsWith("actor:agent:") && args.resolution === "resolved" && !args.via) {
@@ -19971,7 +20191,9 @@ var init_kernel_bridge = __esm({
19971
20191
  state: args.resolution === "wontfix" ? "closed" : "resolved",
19972
20192
  by: args.actor,
19973
20193
  note: args.note ?? null,
19974
- viaWorkId: args.via ?? null
20194
+ viaWorkId: args.via ?? null,
20195
+ ...args.viaNode ? { viaNodeId: args.viaNode } : {},
20196
+ ...args.resolution === "acknowledged" ? { acknowledged: true } : {}
19975
20197
  }
19976
20198
  });
19977
20199
  }
@@ -20037,7 +20259,7 @@ var init_kernel_bridge = __esm({
20037
20259
  for (const [artifactId, gaps] of this.model.gaps) {
20038
20260
  if (gaps.some((g2) => g2.gapId === args.gapId && !g2.resolved)) {
20039
20261
  const wid = this.wo(artifactId);
20040
- await this.commit({
20262
+ const events = [{
20041
20263
  companyId: "",
20042
20264
  workorderId: wid,
20043
20265
  actorId: args.actor,
@@ -20050,16 +20272,31 @@ var init_kernel_bridge = __esm({
20050
20272
  note: args.note ?? null,
20051
20273
  viaWorkId: null
20052
20274
  }
20275
+ }];
20276
+ if (args.rework) events.push({
20277
+ companyId: "",
20278
+ workorderId: wid,
20279
+ actorId: args.actor,
20280
+ event: {
20281
+ kind: "issue.create",
20282
+ workorderId: wid,
20283
+ issueId: `ann:rework:${args.gapId}`,
20284
+ issueKind: "comment",
20285
+ body: args.note ?? "\u7F3A\u53E3\u5DF2\u89E3\u51B3\uFF0C\u8BF7\u6838\u5BF9\u89E3\u6CD5",
20286
+ aboutNodeId: artifactId,
20287
+ aboutWorkId: null,
20288
+ raisedByNodeId: null,
20289
+ authorActorId: args.actor,
20290
+ handActorId: null,
20291
+ recipients: [],
20292
+ attachments: [],
20293
+ gapId: args.gapId,
20294
+ source: "gap-recovery"
20295
+ }
20053
20296
  });
20054
- if (args.rework) {
20055
- await this.annotate({
20056
- artifactId,
20057
- actor: args.actor,
20058
- kind: "comment",
20059
- annotationId: `ann:rework:${args.gapId}`,
20060
- body: args.note ?? "\u7F3A\u53E3\u5DF2\u89E3\u51B3\uFF0C\u8BF7\u6309\u7B54\u590D\u91CD\u505A"
20061
- });
20062
- }
20297
+ await this.bus.submitBatchAndProcess(events);
20298
+ this.markWorkorderDirty(wid);
20299
+ await this.refreshTracked();
20063
20300
  return;
20064
20301
  }
20065
20302
  }
@@ -20108,6 +20345,57 @@ var init_kernel_bridge = __esm({
20108
20345
  }
20109
20346
  });
20110
20347
  }
20348
+ /** 人确认恢复卡后,只关闭卡中列出的缺口和关联上报。其他问题仍由各自责任人处理。 */
20349
+ async resolveRecovery(args) {
20350
+ if (!args.actor.startsWith("actor:human:")) throw new Error("\u6062\u590D\u51B3\u5B9A\u9700\u8981\u4EBA\u660E\u786E\u786E\u8BA4");
20351
+ if (!args.reason.trim()) throw new Error("\u6062\u590D\u51B3\u5B9A\u5FC5\u987B\u8BF4\u660E\u91C7\u7528\u7684\u65B9\u6848");
20352
+ const wid = this.wo(args.artifactId);
20353
+ const snapshot = await this.store.transaction((tx) => tx.loadWorkorder(wid));
20354
+ const gap = snapshot?.issues.find((i) => i.id === args.gapId && i.kind === "gap" && i.aboutNodeId === args.artifactId);
20355
+ const escalation = snapshot?.issues.find((i) => i.id === args.escalationId && i.kind === "escalation" && i.aboutNodeId === args.artifactId);
20356
+ if (!gap || !escalation || escalation.gapId !== gap.id) throw new Error("\u6062\u590D\u5361\u7684\u7F3A\u53E3\u4E0E\u4E0A\u62A5\u5173\u8054\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u786E\u8BA4");
20357
+ const events = [];
20358
+ for (const issue2 of [gap, escalation]) if (!issue2.resolvedAt) events.push({
20359
+ companyId: "",
20360
+ workorderId: wid,
20361
+ actorId: args.actor,
20362
+ event: {
20363
+ kind: "issue.resolve",
20364
+ workorderId: wid,
20365
+ issueId: issue2.id,
20366
+ state: "resolved",
20367
+ by: args.actor,
20368
+ note: args.reason,
20369
+ viaWorkId: null,
20370
+ closeRelatedEscalations: false
20371
+ }
20372
+ });
20373
+ if (!gap.resolvedAt) events.push({
20374
+ companyId: "",
20375
+ workorderId: wid,
20376
+ actorId: args.actor,
20377
+ event: {
20378
+ kind: "issue.create",
20379
+ workorderId: wid,
20380
+ issueId: `ann:recovery:${gap.id}`,
20381
+ issueKind: "comment",
20382
+ body: args.reason,
20383
+ aboutNodeId: args.artifactId,
20384
+ aboutWorkId: null,
20385
+ raisedByNodeId: null,
20386
+ authorActorId: args.actor,
20387
+ handActorId: null,
20388
+ recipients: [],
20389
+ attachments: [],
20390
+ gapId: gap.id,
20391
+ source: "gap-recovery"
20392
+ }
20393
+ });
20394
+ await this.bus.submitBatchAndProcess(events);
20395
+ this.markWorkorderDirty(wid);
20396
+ await this.refreshTracked();
20397
+ await this.releaseIfStillHeld(args.artifactId, args.actor);
20398
+ }
20111
20399
  async resolveEscalation(args) {
20112
20400
  if (!args.actor.startsWith("actor:human:")) {
20113
20401
  throw new Error("\u5173\u95ED\u5347\u7EA7\u9700\u8981\u4EBA\u660E\u786E\u786E\u8BA4\uFF1Bagent \u8BF7\u63D0\u4EA4\u5173\u95ED\u6388\u6743\u7533\u8BF7");
@@ -20312,6 +20600,7 @@ var init_kernel_bridge = __esm({
20312
20600
  async rerunNode(args) {
20313
20601
  const wid = this.wo(args.artifactId);
20314
20602
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
20603
+ assertWorkorderDispatchActive(snap);
20315
20604
  const node2 = snap?.nodes.find((n) => n.id === args.artifactId);
20316
20605
  if (!node2 || node2.cancelledAt) throw new KernelError(`\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u4F5C\u5E9F\uFF1A${args.artifactId}`, "not-found");
20317
20606
  if (!node2.assigneeActorId) throw new KernelError("\u8282\u70B9\u672A\u6307\u6D3E\u6267\u884C\u4EBA\u2014\u2014\u5148\u6307\u6D3E\u518D\u91CD\u65B0\u8FD0\u884C", "no-assignee");
@@ -20361,6 +20650,7 @@ var init_kernel_bridge = __esm({
20361
20650
  async rereviewNode(args) {
20362
20651
  const wid = this.wo(args.artifactId);
20363
20652
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
20653
+ assertWorkorderDispatchActive(snap);
20364
20654
  const node2 = snap?.nodes.find((n) => n.id === args.artifactId);
20365
20655
  if (!node2 || node2.cancelledAt) throw new KernelError(`\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u4F5C\u5E9F\uFF1A${args.artifactId}`, "not-found");
20366
20656
  const lw = node2.latestWorkId ? snap.works.find((w2) => w2.id === node2.latestWorkId) : null;
@@ -20563,12 +20853,9 @@ var init_kernel_bridge = __esm({
20563
20853
  throw new Error(`resolveEscalation \u6388\u6743\u5361\u7F3A escalationId\uFF1A${args.draftId}`);
20564
20854
  }
20565
20855
  const reason = typeof card2.commandArgs?.["reason"] === "string" ? card2.commandArgs["reason"] : void 0;
20566
- await this.resolveEscalation({
20567
- artifactId,
20568
- actor: args.actor,
20569
- escalationId,
20570
- ...reason !== void 0 ? { reason } : {}
20571
- });
20856
+ const gapId = typeof card2.commandArgs?.["gapId"] === "string" ? card2.commandArgs["gapId"] : void 0;
20857
+ if (gapId) await this.resolveRecovery({ artifactId, actor: args.actor, escalationId, gapId, reason: reason ?? "" });
20858
+ else await this.resolveEscalation({ artifactId, actor: args.actor, escalationId, ...reason !== void 0 ? { reason } : {} });
20572
20859
  } else {
20573
20860
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u4E0D\u652F\u6301\u6267\u884C ${card2.command}`);
20574
20861
  }
@@ -20612,11 +20899,29 @@ var init_kernel_bridge = __esm({
20612
20899
  if (preview2.changeClass === "C" && !_actor.startsWith("actor:human:")) {
20613
20900
  throw new Error("C \u7C7B\u6539\u52A8\uFF08\u975E\u5355\u8C03\u6539\u56FE\uFF09\u5FC5\u987B\u7531\u4EBA\u786E\u8BA4\uFF0Cagent \u4E0D\u80FD\u76F4\u63A5\u843D\u5730\u2014\u2014\u8BF7\u8D70 preview \u2192 draft \u2192 \u4EBA apply-draft");
20614
20901
  }
20615
- const wid = plan.workspace ?? plan.ops.find((o) => o.action === "spawn" && o.workspace)?.workspace ?? "ws:default";
20902
+ const wid = ws ?? plan.ops.find((o) => o.action === "spawn" && o.workspace)?.workspace ?? "ws:default";
20616
20903
  let applied = 0;
20617
20904
  const unsupported = [];
20618
20905
  const reviewerStaffing = [];
20619
- await this.ensureWorkorder(wid, "", _actor);
20906
+ const events = [];
20907
+ const artifacts = new Map([...this.model.artifacts].map(([id, art]) => [id, { ...art, fields: { ...art.fields }, inputs: [...art.inputs] }]));
20908
+ const woFor = (id) => {
20909
+ const art = artifacts.get(id);
20910
+ if (!art) throw new Error(`artifact not found: ${id}`);
20911
+ return art.workspace;
20912
+ };
20913
+ const queue = async (input) => {
20914
+ if (input.event.kind === "plan.update_spec") {
20915
+ const event = input.event;
20916
+ const previous3 = events.find((e) => e.workorderId === input.workorderId && e.event.kind === "plan.update_spec" && e.event.nodeId === event.nodeId);
20917
+ if (previous3?.event.kind === "plan.update_spec") {
20918
+ previous3.event = { ...previous3.event, ...event };
20919
+ return;
20920
+ }
20921
+ }
20922
+ events.push(input);
20923
+ };
20924
+ const enqueue = async (event) => queue({ companyId: "", workorderId: event.workorderId, actorId: _actor, event });
20620
20925
  for (const op of plan.ops) {
20621
20926
  switch (op.action) {
20622
20927
  case "spawn": {
@@ -20648,7 +20953,7 @@ var init_kernel_bridge = __esm({
20648
20953
  // ★ executionEnv 同走 fields jsonb(与路径 A 对称——两条 spawn 路径必须一致,前车之鉴:类型默认评审闸)。
20649
20954
  ...op.executionEnv !== void 0 ? { executionEnv: op.executionEnv } : {}
20650
20955
  } : opFields;
20651
- await this.commit({
20956
+ await queue({
20652
20957
  companyId: "",
20653
20958
  workorderId: wid,
20654
20959
  actorId: _actor,
@@ -20678,27 +20983,64 @@ var init_kernel_bridge = __esm({
20678
20983
  activateWorkorder: false
20679
20984
  }
20680
20985
  });
20986
+ artifacts.set(id, {
20987
+ id,
20988
+ type: op.type,
20989
+ title: op.title ?? op.type,
20990
+ description: op.description ?? "",
20991
+ workspace: wid,
20992
+ owner: spawnOwner,
20993
+ fields: mergedFields ?? {},
20994
+ reviewRoles: opReviewRoles,
20995
+ reviewers: opReviewers ?? [],
20996
+ inputs: (op.inputs ?? []).map((i) => ({ to: i.to, required: i.required ?? true, pinned: null }))
20997
+ });
20681
20998
  applied++;
20682
20999
  break;
20683
21000
  }
20684
- case "link":
20685
- await this.linkInput({ artifactId: op.artifactId, to: op.to, actor: _actor });
21001
+ case "link": {
21002
+ await enqueue({ kind: "plan.add_edge", workorderId: woFor(op.artifactId), fromNodeId: op.to, toNodeId: op.artifactId, edgeKind: "data", required: op.required ?? true });
21003
+ const art = artifacts.get(op.artifactId);
21004
+ if (!art.inputs.some((i) => i.to === op.to)) art.inputs.push({ to: op.to, required: op.required ?? true, pinned: null });
20686
21005
  applied++;
20687
21006
  break;
20688
- case "unlink":
20689
- await this.unlinkInput({ artifactId: op.artifactId, to: op.to, actor: _actor });
21007
+ }
21008
+ case "unlink": {
21009
+ await enqueue({ kind: "plan.delete_edge", workorderId: woFor(op.artifactId), fromNodeId: op.to, toNodeId: op.artifactId });
21010
+ const art = artifacts.get(op.artifactId);
21011
+ art.inputs = art.inputs.filter((i) => i.to !== op.to);
20690
21012
  applied++;
20691
21013
  break;
21014
+ }
20692
21015
  case "assign":
20693
- await this.assignOwner({ artifactId: op.artifactId, owner: op.owner, actor: _actor });
21016
+ await enqueue({ kind: "plan.assign_actor", workorderId: woFor(op.artifactId), nodeId: op.artifactId, assigneeActorId: op.owner, assigneeRole: null });
20694
21017
  applied++;
20695
21018
  break;
20696
21019
  case "seal":
20697
- await this.seal({ artifactId: op.artifactId, reason: op.reason, actor: _actor });
21020
+ if (op.reason === "cancelled") for (const art of artifacts.values()) {
21021
+ for (const edge of art.inputs.filter((i) => art.id === op.artifactId || i.to === op.artifactId)) {
21022
+ await enqueue({ kind: "plan.delete_edge", workorderId: woFor(art.id), fromNodeId: edge.to, toNodeId: art.id });
21023
+ }
21024
+ art.inputs = art.inputs.filter((i) => art.id !== op.artifactId && i.to !== op.artifactId);
21025
+ }
21026
+ await enqueue({ kind: "plan.delete_node", workorderId: woFor(op.artifactId), nodeId: op.artifactId, sealReason: op.reason });
20698
21027
  applied++;
20699
21028
  break;
20700
21029
  case "annotate":
20701
- await this.annotate({ artifactId: op.artifactId, body: op.body, actor: _actor });
21030
+ await enqueue({
21031
+ kind: "issue.create",
21032
+ workorderId: woFor(op.artifactId),
21033
+ issueId: `ann:${(0, import_node_crypto3.randomUUID)()}`,
21034
+ issueKind: "comment",
21035
+ body: op.body,
21036
+ aboutNodeId: op.artifactId,
21037
+ aboutWorkId: null,
21038
+ raisedByNodeId: null,
21039
+ authorActorId: _actor,
21040
+ handActorId: null,
21041
+ recipients: [],
21042
+ attachments: []
21043
+ });
20702
21044
  applied++;
20703
21045
  break;
20704
21046
  case "edit": {
@@ -20706,7 +21048,7 @@ var init_kernel_bridge = __esm({
20706
21048
  const opReviewers = op.reviewers;
20707
21049
  const editReviewRoles = op.reviewRoles;
20708
21050
  if (artId && (opReviewers || editReviewRoles !== void 0)) {
20709
- const art = this.model.artifacts.get(artId);
21051
+ const art = artifacts.get(artId);
20710
21052
  const effectiveRoles = editReviewRoles !== void 0 ? editReviewRoles === null ? void 0 : editReviewRoles : art?.reviewRoles;
20711
21053
  const effectiveExplicit = opReviewers ?? (art?.reviewers ?? []).map((r) => ({ actor: r.actor, source: r.source }));
20712
21054
  const { reviewers: nextReqs, staffingIssues } = resolveNodeReviewers({
@@ -20720,13 +21062,13 @@ var init_kernel_bridge = __esm({
20720
21062
  reviewerStaffing.push({ nodeId: artId, type: art?.type ?? "", role: issue2.role, from: issue2.from });
20721
21063
  console.warn(`[edit] ${artId} \u7684\u300C${issue2.role}\u300D\u8BC4\u5BA1\u95F8\u5728\u5185\u6838 roleIndex \u91CC\u6CA1\u6709\u6301\u6709\u8005\u2014\u2014\u8BE5\u6876\u6682\u7F3A\u3002`);
20722
21064
  }
20723
- await this.commit({
21065
+ await queue({
20724
21066
  companyId: "",
20725
- workorderId: this.wo(artId),
21067
+ workorderId: woFor(artId),
20726
21068
  actorId: _actor,
20727
21069
  event: {
20728
21070
  kind: "plan.update_review_requirements",
20729
- workorderId: this.wo(artId),
21071
+ workorderId: woFor(artId),
20730
21072
  nodeId: artId,
20731
21073
  reviewers: nextReqs
20732
21074
  }
@@ -20735,15 +21077,15 @@ var init_kernel_bridge = __esm({
20735
21077
  const opTitle = op.title;
20736
21078
  const opDescription = op.description;
20737
21079
  if (artId && (opTitle !== void 0 || opDescription !== void 0)) {
20738
- await this.commit({
21080
+ await queue({
20739
21081
  companyId: "",
20740
- workorderId: this.wo(artId),
21082
+ workorderId: woFor(artId),
20741
21083
  actorId: _actor,
20742
21084
  event: {
20743
21085
  kind: "plan.update_spec",
20744
- workorderId: this.wo(artId),
21086
+ workorderId: woFor(artId),
20745
21087
  nodeId: artId,
20746
- spec: opDescription ?? this.model.artifacts.get(artId)?.description ?? null,
21088
+ spec: opDescription ?? artifacts.get(artId)?.description ?? null,
20747
21089
  ...opTitle !== void 0 ? { title: opTitle } : {},
20748
21090
  policy: "auto"
20749
21091
  }
@@ -20751,21 +21093,30 @@ var init_kernel_bridge = __esm({
20751
21093
  }
20752
21094
  const fields = op.fields;
20753
21095
  if (artId && fields) {
20754
- const editDef = this.schema.get(this.model.artifacts.get(artId)?.type ?? "");
21096
+ const editDef = this.schema.get(artifacts.get(artId)?.type ?? "");
20755
21097
  const validated = editDef?.fieldDefs ? validateArtifactFields(fields, editDef.fieldDefs, "edit") : fields;
20756
- await this.commit({
21098
+ await queue({
20757
21099
  companyId: "",
20758
- workorderId: this.wo(artId),
21100
+ workorderId: woFor(artId),
20759
21101
  actorId: _actor,
20760
21102
  event: {
20761
21103
  kind: "plan.update_fields",
20762
- workorderId: this.wo(artId),
21104
+ workorderId: woFor(artId),
20763
21105
  nodeId: artId,
20764
21106
  fields: validated ?? {},
20765
21107
  merge: true
20766
21108
  }
20767
21109
  });
20768
21110
  }
21111
+ const previous3 = artifacts.get(artId);
21112
+ if (previous3) artifacts.set(artId, {
21113
+ ...previous3,
21114
+ ...opTitle !== void 0 ? { title: opTitle } : {},
21115
+ ...opDescription !== void 0 ? { description: opDescription } : {},
21116
+ ...fields ? { fields: { ...previous3.fields, ...fields } } : {},
21117
+ ...editReviewRoles !== void 0 ? { reviewRoles: editReviewRoles ?? void 0 } : {},
21118
+ ...opReviewers ? { reviewers: opReviewers } : {}
21119
+ });
20769
21120
  applied++;
20770
21121
  break;
20771
21122
  }
@@ -20774,11 +21125,16 @@ var init_kernel_bridge = __esm({
20774
21125
  if (!escOp.escalationId) {
20775
21126
  throw new Error(`resolveEscalation op \u7F3A escalationId\uFF08${escOp.artifactId}\uFF09\u2014\u2014\u4E0D\u652F\u6301\u6574\u8282\u70B9\u5168\u6E05\uFF0C\u8BF7\u6307\u660E\u5173\u54EA\u4E00\u6761`);
20776
21127
  }
20777
- await this.resolveEscalation({
20778
- artifactId: escOp.artifactId,
20779
- actor: _actor,
20780
- escalationId: escOp.escalationId,
20781
- ...escOp.reason !== void 0 ? { reason: escOp.reason } : {}
21128
+ const escalation = (this.model.escalations.get(escOp.artifactId) ?? []).find((e) => e.escalationId === escOp.escalationId);
21129
+ if (!escalation) throw new Error(`escalation not found: ${escOp.escalationId}`);
21130
+ await enqueue({
21131
+ kind: "issue.resolve",
21132
+ workorderId: woFor(escOp.artifactId),
21133
+ issueId: escOp.escalationId,
21134
+ state: "resolved",
21135
+ by: _actor,
21136
+ note: escOp.reason ?? null,
21137
+ viaWorkId: null
20782
21138
  });
20783
21139
  applied++;
20784
21140
  break;
@@ -20788,7 +21144,7 @@ var init_kernel_bridge = __esm({
20788
21144
  break;
20789
21145
  }
20790
21146
  }
20791
- await this.commit({
21147
+ await queue({
20792
21148
  companyId: "",
20793
21149
  workorderId: wid,
20794
21150
  actorId: _actor,
@@ -20803,17 +21159,22 @@ var init_kernel_bridge = __esm({
20803
21159
  activateWorkorder: true
20804
21160
  }
20805
21161
  });
21162
+ if (unsupported.length) throw new KernelError(`\u6574\u6279\u672A\u843D\u5730\uFF1B\u4EE5\u4E0B op \u672C\u5F15\u64CE\u4E0D\u652F\u6301\uFF1A${unsupported.join("\u3001")}`, "unsupported-op");
21163
+ await this.ensureWorkorder(wid, "", _actor);
21164
+ await this.bus.submitBatchAndProcess(events);
21165
+ for (const event of events) {
21166
+ this.trackedWorkorders.add(event.workorderId);
21167
+ this.markWorkorderDirty(event.workorderId);
21168
+ }
21169
+ await this.refreshTracked();
20806
21170
  await this.syncFromBus(wid);
21171
+ for (const id of new Set(plan.ops.filter((op) => op.action === "resolveEscalation").map((op) => op.artifactId))) {
21172
+ await this.releaseIfStillHeld(id, _actor);
21173
+ }
20807
21174
  if (typeof plan.resolvesProposal === "string") {
20808
21175
  this.draftCards.delete(plan.resolvesProposal);
20809
21176
  await this.refreshModel();
20810
21177
  }
20811
- if (unsupported.length > 0) {
20812
- throw new KernelError(
20813
- `\u5DF2\u843D\u5730 ${applied} \u6761 op\uFF1B\u4EE5\u4E0B op \u672C\u5F15\u64CE\u4E0D\u652F\u6301\u3001\u672A\u6267\u884C\uFF1A${unsupported.join("\u3001")}\uFF08pin \u5DF2\u5E9F\u5F03\u2014\u2014\u4E0B\u6E38\u7248\u672C\u7531\u9A8C\u6536\u4E0A\u6E38\u81EA\u52A8\u63A8\u8FDB\uFF09`,
20814
- "unsupported-op"
20815
- );
20816
- }
20817
21178
  return {
20818
21179
  interventionId: `intervention:${(0, import_node_crypto3.randomUUID)()}`,
20819
21180
  applied,
@@ -20865,67 +21226,6 @@ var init_chat_system_message = __esm({
20865
21226
  }
20866
21227
  });
20867
21228
 
20868
- // ../server/src/build-info.ts
20869
- function findRepoRoot() {
20870
- let dir;
20871
- try {
20872
- dir = path.dirname((0, import_node_url2.fileURLToPath)(__esm_import_meta_url));
20873
- } catch {
20874
- return null;
20875
- }
20876
- for (let i = 0; i < 12; i++) {
20877
- if (fs.existsSync(path.join(dir, ".git"))) return dir;
20878
- const parent = path.dirname(dir);
20879
- if (parent === dir) break;
20880
- dir = parent;
20881
- }
20882
- return null;
20883
- }
20884
- function git(root, args) {
20885
- try {
20886
- const out = (0, import_node_child_process.execFileSync)("git", args, {
20887
- cwd: root,
20888
- encoding: "utf8",
20889
- timeout: 3e3,
20890
- stdio: ["ignore", "pipe", "ignore"]
20891
- });
20892
- const v2 = out.trim();
20893
- return v2 || null;
20894
- } catch {
20895
- return null;
20896
- }
20897
- }
20898
- function getBuildInfo() {
20899
- if (cached) return cached;
20900
- const envSha = process.env["OASIS_GIT_SHA"]?.trim();
20901
- const envBranch = process.env["OASIS_GIT_BRANCH"]?.trim();
20902
- let gitSha = envSha || null;
20903
- let gitBranch = envBranch || null;
20904
- if (!gitSha || !gitBranch) {
20905
- const root = findRepoRoot();
20906
- if (root) {
20907
- if (!gitSha) gitSha = git(root, ["rev-parse", "HEAD"]);
20908
- if (!gitBranch) {
20909
- const b2 = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
20910
- gitBranch = b2 === "HEAD" ? null : b2;
20911
- }
20912
- }
20913
- }
20914
- cached = { gitSha, gitBranch, startedAt: STARTED_AT };
20915
- return cached;
20916
- }
20917
- var import_node_child_process, fs, path, import_node_url2, STARTED_AT, cached;
20918
- var init_build_info = __esm({
20919
- "../server/src/build-info.ts"() {
20920
- "use strict";
20921
- import_node_child_process = require("node:child_process");
20922
- fs = __toESM(require("node:fs"), 1);
20923
- path = __toESM(require("node:path"), 1);
20924
- import_node_url2 = require("node:url");
20925
- STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
20926
- }
20927
- });
20928
-
20929
21229
  // ../server/src/facades/api/router.ts
20930
21230
  var ApiError, Router;
20931
21231
  var init_router = __esm({
@@ -21071,6 +21371,32 @@ async function saveArtifactContent(kernel, blobs, actor, input) {
21071
21371
  }
21072
21372
  return { ...view, contentRef, text: input.text };
21073
21373
  }
21374
+ async function readArtifactFile(kernel, blobs, artifactId, path41, expectedRef) {
21375
+ await kernel.refreshModel();
21376
+ const artifact = kernel.model.artifacts.get(artifactId);
21377
+ const output = artifact?.currentRev ? kernel.model.revisions.get(artifact.currentRev) : void 0;
21378
+ if (!artifact || !output) throw new ApiError(404, "NOT_FOUND", "\u6B64\u4EA7\u7269\u6CA1\u6709\u53EF\u4E0B\u8F7D\u7684\u539F\u6587\u4EF6");
21379
+ if (expectedRef && output.contentRef !== expectedRef) throw new ApiError(409, "CONTENT_CONFLICT", "\u6587\u4EF6\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00");
21380
+ let hash2 = output.contentRef;
21381
+ let name = artifact.title || artifactId;
21382
+ if (output.contentKind === "manifest") {
21383
+ if (!path41) throw new ApiError(400, "PATH_REQUIRED", "\u8BF7\u9009\u62E9\u6587\u4EF6\u5939\u4E2D\u7684\u5177\u4F53\u6587\u4EF6");
21384
+ const entry = parseManifest(await blobs.get(output.contentRef)).entries.find((e) => e.path === path41);
21385
+ if (!entry) throw new ApiError(404, "NOT_FOUND", "\u5305\u5185\u6587\u4EF6\u4E0D\u5B58\u5728");
21386
+ hash2 = entry.hash;
21387
+ name = entry.path;
21388
+ if (entry.size !== void 0 && entry.size > FILE_CONTENT_MAX_BYTES) throw new ApiError(413, "FILE_TOO_LARGE", "\u6587\u4EF6\u8D85\u8FC7 50 MiB \u8BFB\u53D6\u9650\u5236");
21389
+ } else if (output.contentKind !== "inline-blob") {
21390
+ throw new ApiError(404, "NOT_FOUND", "\u6B64\u4EA7\u7269\u6CA1\u6709\u53EF\u4E0B\u8F7D\u7684\u539F\u6587\u4EF6");
21391
+ }
21392
+ const meta = await blobs.meta?.(hash2);
21393
+ if (meta && meta.size > FILE_CONTENT_MAX_BYTES) throw new ApiError(413, "FILE_TOO_LARGE", "\u6587\u4EF6\u8D85\u8FC7 50 MiB \u8BFB\u53D6\u9650\u5236");
21394
+ try {
21395
+ return { bytes: await blobs.get(hash2), name, contentType: meta?.contentType };
21396
+ } catch {
21397
+ throw new ApiError(404, "NOT_FOUND", "\u5F53\u524D\u516C\u53F8\u4E2D\u627E\u4E0D\u5230\u8BE5\u6587\u4EF6\u6B63\u6587");
21398
+ }
21399
+ }
21074
21400
  var init_artifact_content2 = __esm({
21075
21401
  "../server/src/artifact-content.ts"() {
21076
21402
  "use strict";
@@ -21079,6 +21405,110 @@ var init_artifact_content2 = __esm({
21079
21405
  }
21080
21406
  });
21081
21407
 
21408
+ // ../server/src/file-http.ts
21409
+ async function serveFileRequest(req, res, url, engine) {
21410
+ try {
21411
+ if (req.method !== "GET" || url.pathname !== "/api/files/content") throw new ApiError(404, "NOT_FOUND", "\u6587\u4EF6\u63A5\u53E3\u4E0D\u5B58\u5728");
21412
+ const { bytes: bytes2, name, contentType } = await readArtifactFile(
21413
+ engine.kernel,
21414
+ engine.blobs,
21415
+ url.searchParams.get("artifact") ?? "",
21416
+ url.searchParams.get("path"),
21417
+ url.searchParams.get("ref")
21418
+ );
21419
+ if (bytes2.length > FILE_CONTENT_MAX_BYTES) throw new ApiError(413, "FILE_TOO_LARGE", "\u6587\u4EF6\u8D85\u8FC7 50 MiB \u8BFB\u53D6\u9650\u5236");
21420
+ res.writeHead(200, {
21421
+ "content-type": contentType ?? sniffContentType(bytes2) ?? "application/octet-stream",
21422
+ "content-length": String(bytes2.length),
21423
+ "cache-control": "private, no-store",
21424
+ "x-content-type-options": "nosniff",
21425
+ "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(name.split(/[\\/]/).pop() || "file").replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`
21426
+ }).end(Buffer.from(bytes2));
21427
+ } catch (error2) {
21428
+ if (res.destroyed) return;
21429
+ const status = error2 instanceof ApiError ? error2.status : 500;
21430
+ if (!req.complete) {
21431
+ res.setHeader("connection", "close");
21432
+ req.resume();
21433
+ }
21434
+ res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" }).end(JSON.stringify({
21435
+ error: {
21436
+ code: error2 instanceof ApiError ? error2.code : "FILE_READ_FAILED",
21437
+ message: error2 instanceof ApiError ? error2.message : "\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"
21438
+ }
21439
+ }));
21440
+ }
21441
+ }
21442
+ var init_file_http = __esm({
21443
+ "../server/src/file-http.ts"() {
21444
+ "use strict";
21445
+ init_src();
21446
+ init_artifact_content2();
21447
+ init_router();
21448
+ }
21449
+ });
21450
+
21451
+ // ../server/src/build-info.ts
21452
+ function findRepoRoot() {
21453
+ let dir;
21454
+ try {
21455
+ dir = path.dirname((0, import_node_url2.fileURLToPath)(__esm_import_meta_url));
21456
+ } catch {
21457
+ return null;
21458
+ }
21459
+ for (let i = 0; i < 12; i++) {
21460
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
21461
+ const parent = path.dirname(dir);
21462
+ if (parent === dir) break;
21463
+ dir = parent;
21464
+ }
21465
+ return null;
21466
+ }
21467
+ function git(root, args) {
21468
+ try {
21469
+ const out = (0, import_node_child_process.execFileSync)("git", args, {
21470
+ cwd: root,
21471
+ encoding: "utf8",
21472
+ timeout: 3e3,
21473
+ stdio: ["ignore", "pipe", "ignore"]
21474
+ });
21475
+ const v2 = out.trim();
21476
+ return v2 || null;
21477
+ } catch {
21478
+ return null;
21479
+ }
21480
+ }
21481
+ function getBuildInfo() {
21482
+ if (cached) return cached;
21483
+ const envSha = process.env["OASIS_GIT_SHA"]?.trim();
21484
+ const envBranch = process.env["OASIS_GIT_BRANCH"]?.trim();
21485
+ let gitSha = envSha || null;
21486
+ let gitBranch = envBranch || null;
21487
+ if (!gitSha || !gitBranch) {
21488
+ const root = findRepoRoot();
21489
+ if (root) {
21490
+ if (!gitSha) gitSha = git(root, ["rev-parse", "HEAD"]);
21491
+ if (!gitBranch) {
21492
+ const b2 = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
21493
+ gitBranch = b2 === "HEAD" ? null : b2;
21494
+ }
21495
+ }
21496
+ }
21497
+ cached = { gitSha, gitBranch, startedAt: STARTED_AT };
21498
+ return cached;
21499
+ }
21500
+ var import_node_child_process, fs, path, import_node_url2, STARTED_AT, cached;
21501
+ var init_build_info = __esm({
21502
+ "../server/src/build-info.ts"() {
21503
+ "use strict";
21504
+ import_node_child_process = require("node:child_process");
21505
+ fs = __toESM(require("node:fs"), 1);
21506
+ path = __toESM(require("node:path"), 1);
21507
+ import_node_url2 = require("node:url");
21508
+ STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
21509
+ }
21510
+ });
21511
+
21082
21512
  // ../server/src/command-policy.ts
21083
21513
  function classifyCommand(command) {
21084
21514
  return COMMAND_POLICY[command] ?? { risk: "direct" };
@@ -21105,7 +21535,7 @@ var init_command_policy = __esm({
21105
21535
  // 重复关同一条是幂等的(kernel.resolveEscalation 内部按 escalationId 去重)
21106
21536
  // 「关错=静默停摆」的唯一防线是人在卡上一眼看出「关的是哪一条」——escalationId 与 reason 摘要必须上卡。
21107
21537
  // a 里 escalationId/artifactId 由 server.ts 起草时冻进 commandArgs(已四级解析),保证是真值、非 `?`。
21108
- effect: (a) => `\u5173\u95ED\u4E0A\u62A5 ${shortId(a.escalationId)}\uFF08\u8282\u70B9 ${shortId(a.artifactId)}${typeof a.reason === "string" ? `\uFF0C\u7406\u7531\uFF1A${a.reason.slice(0, 60)}` : ""}\uFF09`
21538
+ effect: (a) => typeof a.gapId === "string" ? `\u91C7\u7528\u65B9\u6848\uFF1A${String(a.reason ?? "")}\uFF1B\u89E3\u51B3\u7F3A\u53E3 ${a.gapId}\uFF1B\u5173\u95ED\u5173\u8054\u4E0A\u62A5 ${String(a.escalationId)}\uFF1B\u7EE7\u7EED\u6267\u884C\uFF1A\u662F\uFF08\u4ECD\u987B\u6EE1\u8DB3\u5176\u4ED6\u963B\u585E\u6761\u4EF6\uFF09` : `\u5173\u95ED\u4E0A\u62A5 ${shortId(a.escalationId)}\uFF08\u8282\u70B9 ${shortId(a.artifactId)}${typeof a.reason === "string" ? `\uFF0C\u7406\u7531\uFF1A${a.reason}` : ""}\uFF09`
21109
21539
  },
21110
21540
  gc: {
21111
21541
  risk: "forbidden",
@@ -28028,6 +28458,15 @@ var init_chat_item_ledger = __esm({
28028
28458
  this.messageId = deps.messageId ?? null;
28029
28459
  this.telemetryTextInContent = deps.telemetryTextInContent === true;
28030
28460
  this.persistEnabled = Boolean(deps.items) && chatItemsDoubleWriteEnabled();
28461
+ this.clock = Math.max(0, deps.versionSeed ?? 0);
28462
+ if (deps.versionSeed === void 0 && deps.items) {
28463
+ void deps.items.sessionVersionCursor(deps.sessionId).then(
28464
+ (seed) => {
28465
+ if (seed > this.clock) this.clock = seed;
28466
+ },
28467
+ () => void 0
28468
+ );
28469
+ }
28031
28470
  }
28032
28471
  /** 账本轮次 id(`chat_session_turns.id`;未接账本时是 {@link fallbackTurnId})。v3 wire 上 item 帧的 turnId 用它(ADR-0510 D1)。 */
28033
28472
  get turnId() {
@@ -28043,6 +28482,11 @@ var init_chat_item_ledger = __esm({
28043
28482
  /** 内存账本:itemId → 行。插入顺序即 seq 顺序。 */
28044
28483
  rows = /* @__PURE__ */ new Map();
28045
28484
  seqCounter = 0;
28485
+ /**
28486
+ * 会话逻辑时钟(ADR-0510 附录 A)。**一次写走一格**——不描述某条 item 改了第几次,描述
28487
+ * 「这条会话被写了第几次」。于是「一次建行要发两帧」这个死结不存在:想发几帧就取几个号。
28488
+ */
28489
+ clock;
28046
28490
  segment = 0;
28047
28491
  messageId;
28048
28492
  /** 已排队但还没执行的那一次 `append_text`——排队期间到的片段合并进它(见 queueAppend)。 */
@@ -28055,6 +28499,18 @@ var init_chat_item_ledger = __esm({
28055
28499
  finished = false;
28056
28500
  /** 上一次见到的归一层 turnId——变了就清映射(provider 开了新 turn)。 */
28057
28501
  lastTurnId = null;
28502
+ /** 会话逻辑时钟走一格。**唯一发号点**(ADR-0510 附录 A.4)——别在别处自己数。 */
28503
+ tick() {
28504
+ return ++this.clock;
28505
+ }
28506
+ /**
28507
+ * 该 item 的服务端展示序;INSERT 还没返回(或没开双写)时是 undefined。
28508
+ * 调用方(`emitV3`)拿不到就不在帧上带号——**不许自己编一个**。
28509
+ */
28510
+ ordFor(itemId) {
28511
+ const ord = this.rows.get(itemId)?.ord;
28512
+ return ord === null ? void 0 : ord;
28513
+ }
28058
28514
  get failures() {
28059
28515
  return this.failureCount;
28060
28516
  }
@@ -28091,20 +28547,17 @@ var init_chat_item_ledger = __esm({
28091
28547
  if (event.type === "turn") {
28092
28548
  this.itemKeyMap.clear();
28093
28549
  this.lastTurnId = null;
28094
- return;
28550
+ return null;
28095
28551
  }
28096
28552
  if (this.lastTurnId !== null && this.lastTurnId !== event.turnId) {
28097
28553
  this.itemKeyMap.clear();
28098
28554
  }
28099
28555
  this.lastTurnId = event.turnId;
28100
28556
  const existingId = this.itemKeyMap.get(event.providerItemKey);
28101
- if (existingId === void 0) {
28102
- this.createFrom(event);
28103
- return;
28104
- }
28557
+ if (existingId === void 0) return this.createFrom(event);
28105
28558
  const row = this.rows.get(existingId);
28106
- if (!row) return;
28107
- this.applyOpTo(row, event);
28559
+ if (!row) return null;
28560
+ return this.applyOpTo(row, event);
28108
28561
  }
28109
28562
  // ── 非 provider 来源(服务端自己产的行)────────────────────────────────────
28110
28563
  /**
@@ -28299,34 +28752,49 @@ var init_chat_item_ledger = __esm({
28299
28752
  metadata: { [CHAT_ITEM_META_LEGACY_CONTENT]: inContent }
28300
28753
  });
28301
28754
  this.itemKeyMap.set(event.providerItemKey, id);
28302
- if (event.opShape.op === "set_status") this.applyOpTo(this.rows.get(id), event);
28755
+ const row = this.rows.get(id);
28756
+ const outcome = {
28757
+ itemId: id,
28758
+ version: row.wireVersion,
28759
+ startedVersion: row.startedVersion,
28760
+ ord: row.ord
28761
+ };
28762
+ if (event.opShape.op === "set_status") {
28763
+ const followUp = this.applyOpTo(row, event);
28764
+ if (followUp) outcome.version = followUp.version;
28765
+ }
28766
+ return outcome;
28303
28767
  }
28304
28768
  applyOpTo(row, event) {
28305
28769
  const op = event.opShape;
28306
28770
  if (event.attrs) row.attrs = { ...row.attrs, ...event.attrs };
28307
28771
  switch (op.op) {
28308
28772
  case "append_text":
28309
- if (op.text === "") return;
28773
+ if (op.text === "") return null;
28310
28774
  row.text += op.text;
28311
28775
  this.queueAppend(row, op.text);
28312
- return;
28776
+ return this.outcomeOf(row);
28313
28777
  case "set_text":
28314
- if (row.text === op.text) return;
28778
+ if (row.text === op.text) return null;
28315
28779
  row.text = op.text;
28316
28780
  this.sealPendingAppend();
28317
28781
  this.persist(row, row.kind === "text" || row.kind === "thinking" ? { op: "set_text", text: op.text } : { op: "set_payload", payload: this.payloadOf(row) }, "set_text");
28318
- return;
28782
+ return this.outcomeOf(row);
28319
28783
  case "patch_input":
28320
28784
  row.attrs = { ...row.attrs, input: op.input };
28321
28785
  this.persist(row, { op: "patch_input", input: op.input }, "patch_input");
28322
- return;
28786
+ return this.outcomeOf(row);
28323
28787
  case "set_status":
28324
- if (row.status === op.status) return;
28788
+ if (row.status === op.status) return null;
28325
28789
  row.status = op.status;
28326
28790
  this.persist(row, { op: "set_status", status: op.status }, "set_status");
28327
- return;
28791
+ return this.outcomeOf(row);
28328
28792
  }
28329
28793
  }
28794
+ /** 这一次写的回执。`persist` 已经在里面走过钟了,这里只是把读数带出去给 wire。 */
28795
+ outcomeOf(row) {
28796
+ return { itemId: row.id, version: row.wireVersion, ord: row.ord };
28797
+ }
28330
28798
  /** 关掉所有还 streaming 的行(收尾/切段)。 */
28331
28799
  closeStreamingRows(status) {
28332
28800
  this.sealPendingAppend();
@@ -28361,9 +28829,14 @@ var init_chat_item_ledger = __esm({
28361
28829
  }
28362
28830
  insert(input) {
28363
28831
  const id = input.deterministicId ?? this.newId();
28832
+ const startedVersion = this.tick();
28833
+ const writeVersion = this.tick();
28364
28834
  const row = {
28365
28835
  id,
28366
28836
  seq: ++this.seqCounter,
28837
+ ord: null,
28838
+ wireVersion: writeVersion,
28839
+ startedVersion,
28367
28840
  kind: input.kind,
28368
28841
  role: input.role,
28369
28842
  status: input.status,
@@ -28383,7 +28856,7 @@ var init_chat_item_ledger = __esm({
28383
28856
  const payload = input.payload ?? this.payloadOf(row);
28384
28857
  const metadata = input.metadata ?? { [CHAT_ITEM_META_LEGACY_CONTENT]: input.inContent };
28385
28858
  this.enqueue(async () => {
28386
- await this.deps.items.createItem({
28859
+ const created = await this.deps.items.createItem({
28387
28860
  id,
28388
28861
  sessionId: this.deps.sessionId,
28389
28862
  messageId,
@@ -28395,8 +28868,20 @@ var init_chat_item_ledger = __esm({
28395
28868
  payload,
28396
28869
  metadata,
28397
28870
  providerItemKey: input.providerItemKey ?? null,
28398
- createdAt
28871
+ createdAt,
28872
+ version: writeVersion
28399
28873
  });
28874
+ row.ord = created.ord;
28875
+ try {
28876
+ this.deps.onPersisted?.({
28877
+ itemId: id,
28878
+ ord: created.ord,
28879
+ version: writeVersion,
28880
+ role: input.role,
28881
+ text: input.text
28882
+ });
28883
+ } catch {
28884
+ }
28400
28885
  }, `create:${input.kind}`);
28401
28886
  return id;
28402
28887
  }
@@ -28405,6 +28890,7 @@ var init_chat_item_ledger = __esm({
28405
28890
  * 语义没变(`version` 数的是落库次数),投影是拼接,合不合并结果逐字相同。
28406
28891
  */
28407
28892
  queueAppend(row, text5) {
28893
+ row.wireVersion = this.tick();
28408
28894
  if (!row.persist || !this.persistEnabled) return;
28409
28895
  const pending = this.pendingAppend;
28410
28896
  if (pending && pending.id === row.id) {
@@ -28418,7 +28904,11 @@ var init_chat_item_ledger = __esm({
28418
28904
  next.text = "";
28419
28905
  if (this.pendingAppend === next) this.pendingAppend = null;
28420
28906
  if (!merged) return;
28421
- const applied = await this.deps.items.applyOp(row.id, { op: "append_text", text: merged }, { at: this.now() });
28907
+ const applied = await this.deps.items.applyOp(
28908
+ row.id,
28909
+ { op: "append_text", text: merged },
28910
+ { at: this.now(), version: row.wireVersion }
28911
+ );
28422
28912
  if (!applied) throw new Error(`item ${row.id} \u4E0D\u5B58\u5728\uFF08append_text \u843D\u7A7A\uFF09`);
28423
28913
  }, "append_text");
28424
28914
  }
@@ -28427,9 +28917,11 @@ var init_chat_item_ledger = __esm({
28427
28917
  this.pendingAppend = null;
28428
28918
  }
28429
28919
  persist(row, op, what) {
28920
+ const version2 = this.tick();
28921
+ row.wireVersion = version2;
28430
28922
  if (!row.persist || !this.persistEnabled) return;
28431
28923
  this.enqueue(async () => {
28432
- await this.deps.items.applyOp(row.id, op, { at: this.now() });
28924
+ await this.deps.items.applyOp(row.id, op, { at: this.now(), version: version2 });
28433
28925
  }, what);
28434
28926
  }
28435
28927
  /** 串行链 + 失败吞掉计数。**串行是必须的**:append_text 依赖建行已经落地。 */
@@ -28518,17 +29010,21 @@ async function openAssistantRunningRow(deps) {
28518
29010
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
28519
29011
  if (typeof deps.chatStore.appendMessage !== "function") return void 0;
28520
29012
  const id = (0, import_node_crypto16.randomUUID)();
29013
+ const at = now();
28521
29014
  try {
28522
29015
  await deps.chatStore.appendMessage({
28523
29016
  id,
28524
29017
  sessionId: deps.chatSessionId,
28525
29018
  role: "assistant",
28526
29019
  content: "",
28527
- createdAt: now(),
29020
+ createdAt: at,
28528
29021
  status: "running",
28529
29022
  ...deps.runId ? { runId: deps.runId } : {},
28530
29023
  ...deps.turnId ? { turnId: deps.turnId } : {}
28531
29024
  });
29025
+ if (typeof deps.chatStore.updateSession === "function") {
29026
+ await deps.chatStore.updateSession(deps.chatSessionId, { touchedAt: at }).catch(() => void 0);
29027
+ }
28532
29028
  return id;
28533
29029
  } catch {
28534
29030
  return void 0;
@@ -141732,7 +142228,7 @@ function buildNodeTimeline(artifactId, snap, runIdByTarget) {
141732
142228
  if (w2.nodeId !== artifactId) continue;
141733
142229
  if (isReplyWork(w2)) continue;
141734
142230
  nodeWorkIds.add(w2.id);
141735
- const status = w2.status ?? (w2.retryAt ? "retry" : w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
142231
+ const status = w2.status ?? (w2.retryAt && w2.deadAt ? "retry" : w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
141736
142232
  items.push({
141737
142233
  type: "work",
141738
142234
  id: w2.id,
@@ -159267,13 +159763,15 @@ var init_live_chat = __esm({
159267
159763
  * 账本坏掉不影响对话;v3 emit 抛异常也不影响 v2 通路与账本落盘——两条独立通路。
159268
159764
  */
159269
159765
  applyNormalizedEvent(turn, event) {
159766
+ let outcome = null;
159270
159767
  try {
159271
- turn.items?.apply(event);
159768
+ outcome = turn.items?.apply(event) ?? null;
159272
159769
  } catch {
159273
159770
  }
159274
159771
  if (event.type === "turn") return;
159772
+ if (!outcome) return;
159275
159773
  try {
159276
- this.emitV3(turn, event);
159774
+ this.emitV3(turn, event, outcome);
159277
159775
  } catch {
159278
159776
  }
159279
159777
  }
@@ -159293,18 +159791,17 @@ var init_live_chat = __esm({
159293
159791
  wireTurnId(turn) {
159294
159792
  return turn.items?.turnId ?? turn.liveTurnId;
159295
159793
  }
159296
- emitV3(turn, event) {
159794
+ emitV3(turn, event, outcome) {
159297
159795
  if (!turn.items) return;
159298
- const itemId = turn.items.itemIdForKey(event.providerItemKey);
159299
- if (!itemId) return;
159796
+ const itemId = outcome.itemId;
159300
159797
  const itemType = normalizedKindToItemType(event.kind);
159301
159798
  const operation = normalizedOpToV3Op(event.opShape.op, event.kind);
159302
159799
  if (!operation) return;
159303
- const perItem = turn.v3Items.get(itemId) ?? { version: 0, textOffset: 0, started: false };
159800
+ const perItem = turn.v3Items.get(itemId) ?? { textOffset: 0, started: false };
159304
159801
  const isTextual2 = event.kind === "text" || event.kind === "thinking";
159305
159802
  const opShape = event.opShape;
159306
- if (!perItem.started) {
159307
- perItem.version += 1;
159803
+ const ord = turn.items.ordFor(itemId) ?? outcome.ord ?? void 0;
159804
+ if (!perItem.started && outcome.startedVersion !== void 0) {
159308
159805
  const startedFrame = {
159309
159806
  protocolVersion: 3,
159310
159807
  streamId: turn.liveStreamId,
@@ -159312,7 +159809,8 @@ var init_live_chat = __esm({
159312
159809
  itemId,
159313
159810
  itemType,
159314
159811
  operation: "started",
159315
- itemVersion: perItem.version,
159812
+ itemVersion: outcome.startedVersion,
159813
+ ...ord !== void 0 ? { ord } : {},
159316
159814
  offset: 0,
159317
159815
  payload: v3StartedPayload(event.role, event.attrs, itemType, event.kind)
159318
159816
  };
@@ -159334,7 +159832,6 @@ var init_live_chat = __esm({
159334
159832
  } else if (opShape.op === "set_status") {
159335
159833
  if (opShape.status === "streaming") return;
159336
159834
  }
159337
- perItem.version += 1;
159338
159835
  turn.v3Items.set(itemId, perItem);
159339
159836
  const frame = {
159340
159837
  protocolVersion: 3,
@@ -159343,7 +159840,9 @@ var init_live_chat = __esm({
159343
159840
  itemId,
159344
159841
  itemType,
159345
159842
  operation,
159346
- itemVersion: perItem.version,
159843
+ // 账本这一次写用掉的那一格钟——**库里那一行的 version 就是它**,快照与增量流同一套版本空间。
159844
+ itemVersion: outcome.version,
159845
+ ...ord !== void 0 ? { ord } : {},
159347
159846
  ...offset !== void 0 ? { offset } : {},
159348
159847
  ...payload !== void 0 ? { payload } : {}
159349
159848
  };
@@ -159369,6 +159868,7 @@ var init_live_chat = __esm({
159369
159868
  const payload = pending.frame.payload;
159370
159869
  pending.frame.payload = { ...payload, text: String(payload.text ?? "") + text5 };
159371
159870
  pending.frame.itemVersion = frame.itemVersion;
159871
+ if (frame.ord !== void 0) pending.frame.ord = frame.ord;
159372
159872
  pending.chunks += 1;
159373
159873
  pending.chars += text5.length;
159374
159874
  return;
@@ -159409,6 +159909,58 @@ var init_live_chat = __esm({
159409
159909
  }
159410
159910
  }
159411
159911
  }
159912
+ /**
159913
+ * 把**用户自己那条消息**推上 v3 流(ADR-0510 D2 的收口)。
159914
+ *
159915
+ * ## 为什么需要
159916
+ *
159917
+ * 用户点发送时前端先画一条「乐观条」,它**没有服务端展示序**(D2:不许前端发明号),
159918
+ * 渲染时钉在列表末尾。这条规矩对「刚发完、还没有任何回复」是对的。
159919
+ *
159920
+ * 但服务端落账本时**立刻**给了这条 user item 一个 ord(它是这一轮第一个建的行,号最小),
159921
+ * 而这个号此前**只走快照**——`emitV3` 只服务 provider 事件(`server.ts` 那句注释:
159922
+ * 「不走 v3 流帧(那条只服务 assistant)」)。于是有个几秒的窗口:
159923
+ * agent 的正文带着更大的号从直播流到了、进入「有号」段,而用户那条还在「无号」段被钉在最后
159924
+ * ⇒ **agent 的回复显示在用户消息上面**,等下一次快照才跳回去。
159925
+ * 2026-09-09 用户现场原话:「过几秒钟页面刷新一下,agent 回复内容会回到用户消息下面」。
159926
+ *
159927
+ * 修法就是把这条也推上去:前端按 itemId(`oasis-user-optim:<csid>`,与乐观桶位同名)覆盖,
159928
+ * 拿到真号立刻归位,窗口从「到下一次快照」压到「一次 INSERT 往返」。
159929
+ *
159930
+ * ## 为什么不在前端算
159931
+ *
159932
+ * 试过的三种都被证伪(见 `chat-items-merge.ts` 里那段注释)。这次也不能用「记下创建时的最大号」:
159933
+ * 空对话首次发送时那个最大号是 0,随后到达的历史(号更大)会全部排到乐观条**后面**——
159934
+ * 正是 ADR D9 点名必须验的「空桶先发送」那一档。号只能由服务端给。
159935
+ *
159936
+ * ## 拿不到轮就静默跳过
159937
+ *
159938
+ * 本方法由账本的落库链回调触发,而那一刻这条会话的 live 轮可能还没 `start`
159939
+ * (`server.ts` 里 `recordUserSubmission` 在 `liveChat.start` 之前)。拿不到就不发——
159940
+ * 前端退回「等下一次快照」,与修复前一致,不会更坏。
159941
+ */
159942
+ publishUserItem(chatSessionId, item) {
159943
+ const turn = this.turns.get(chatSessionId);
159944
+ if (!turn) return false;
159945
+ const payload = { role: "user", text: item.text };
159946
+ if (item.clientSubmitId) payload.clientSubmitId = item.clientSubmitId;
159947
+ if (item.attachments?.length) payload.attachments = item.attachments;
159948
+ const frame = {
159949
+ protocolVersion: 3,
159950
+ streamId: turn.liveStreamId,
159951
+ turnId: this.wireTurnId(turn),
159952
+ itemId: item.itemId,
159953
+ itemType: "message",
159954
+ operation: "set_text",
159955
+ itemVersion: item.version,
159956
+ ...item.ord !== null ? { ord: item.ord } : {},
159957
+ offset: 0,
159958
+ payload
159959
+ };
159960
+ this.flushPendingV3(turn);
159961
+ this.publishV3(turn, this.assignV3Seq(turn, frame));
159962
+ return true;
159963
+ }
159412
159964
  emitV3TurnTerminal(turn, status) {
159413
159965
  if (status !== "done" && status !== "error") return;
159414
159966
  const operation = status === "done" ? "turn_completed" : "turn_failed";
@@ -160175,7 +160727,7 @@ var init_workorder_drafts = __esm({
160175
160727
  }
160176
160728
  };
160177
160729
  ident = (s2) => {
160178
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160730
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160179
160731
  return s2;
160180
160732
  };
160181
160733
  seqNoOf = (id, fallback) => {
@@ -160433,7 +160985,7 @@ var init_workorder_draft_planner_issues = __esm({
160433
160985
  }
160434
160986
  };
160435
160987
  ident2 = (s2) => {
160436
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160988
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160437
160989
  return s2;
160438
160990
  };
160439
160991
  PostgresWorkorderDraftPlannerIssuesStore = class _PostgresWorkorderDraftPlannerIssuesStore extends MemoryWorkorderDraftPlannerIssuesStore {
@@ -194283,8 +194835,15 @@ function isTextual(contentType) {
194283
194835
  const t = contentType.toLowerCase();
194284
194836
  return t.startsWith("text/") || t.includes("json") || t.includes("xml") || t.includes("yaml") || t.includes("javascript");
194285
194837
  }
194286
- function toPrepared(rel, source, read) {
194287
- const base = { rel, source, size: read.size, contentType: read.contentType };
194838
+ function toPrepared(rel, source, read, src) {
194839
+ const base = {
194840
+ rel,
194841
+ source,
194842
+ size: read.size,
194843
+ contentType: read.contentType,
194844
+ ...src ? { srcSize: src.size } : {},
194845
+ ...src?.mtime ? { srcMtime: src.mtime } : {}
194846
+ };
194288
194847
  if (!isTextual(read.contentType)) return { ...base, base64: read.contentBase64 };
194289
194848
  return { ...base, text: Buffer.from(read.contentBase64, "base64").toString("utf8") };
194290
194849
  }
@@ -194321,7 +194880,7 @@ async function collectInboundFiles(port, parent, paths, limits, lenient) {
194321
194880
  if (!lenient) throw error2;
194322
194881
  skipped.push({ source, code: error2.code, message: error2.message });
194323
194882
  };
194324
- if (wanted.length === 0) return { prepared: [], skipped, totalBytes: 0 };
194883
+ if (wanted.length === 0) return { prepared: [], skipped, totalBytes: 0, unchanged: [] };
194325
194884
  if (!port) {
194326
194885
  const error2 = new DelegationFileError(
194327
194886
  "FILE_UNREADABLE",
@@ -194330,7 +194889,7 @@ async function collectInboundFiles(port, parent, paths, limits, lenient) {
194330
194889
  );
194331
194890
  if (!lenient) throw error2;
194332
194891
  for (const source of wanted) skipped.push({ source, code: error2.code, message: error2.message });
194333
- return { prepared: [], skipped, totalBytes: 0 };
194892
+ return { prepared: [], skipped, totalBytes: 0, unchanged: [] };
194334
194893
  }
194335
194894
  let accepted = wanted;
194336
194895
  if (wanted.length > limits.maxFiles) {
@@ -194408,10 +194967,10 @@ async function collectInboundFiles(port, parent, paths, limits, lenient) {
194408
194967
  used.add(rel);
194409
194968
  prepared.push(toPrepared(rel, source, read));
194410
194969
  }
194411
- return { prepared, skipped, totalBytes };
194970
+ return { prepared, skipped, totalBytes, unchanged: [] };
194412
194971
  }
194413
- async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FILE_LIMITS) {
194414
- const out = { prepared: [], skipped: [], totalBytes: 0 };
194972
+ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FILE_LIMITS, known) {
194973
+ const out = { prepared: [], skipped: [], totalBytes: 0, unchanged: [] };
194415
194974
  if (!port) return out;
194416
194975
  if (port.locate) {
194417
194976
  const located = await port.locate(child).catch((err) => ({
@@ -194471,7 +195030,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
194471
195030
  for (const entry of level.entries) {
194472
195031
  const childPath = `${dir}/${entry.name}`;
194473
195032
  if (entry.type === "dir") queue.push(childPath);
194474
- else files.push({ rel: childPath, size: entry.size });
195033
+ else files.push({ rel: childPath, size: entry.size, ...entry.mtime ? { mtime: entry.mtime } : {} });
194475
195034
  }
194476
195035
  }
194477
195036
  for (const dir of queue) {
@@ -194487,6 +195046,12 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
194487
195046
  out.skipped.push({ source: file.rel, code: "PATH_REJECTED", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF0C\u6CA1\u642C" });
194488
195047
  continue;
194489
195048
  }
195049
+ const target = `${DELEGATION_OUTBOUND_DIR}/${dirName}/${relative5}`;
195050
+ const seen = known?.get(target);
195051
+ if (seen && file.mtime && seen.size === file.size && seen.mtime === file.mtime) {
195052
+ out.unchanged.push(target);
195053
+ continue;
195054
+ }
194490
195055
  if (out.prepared.length >= limits.maxFiles) {
194491
195056
  out.skipped.push({ source: file.rel, code: "FILE_QUOTA_EXCEEDED", message: `\u8D85\u8FC7\u4E00\u6B21 ${limits.maxFiles} \u4E2A\u7684\u4E0A\u9650\uFF0C\u6CA1\u642C` });
194492
195057
  continue;
@@ -194505,7 +195070,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
194505
195070
  continue;
194506
195071
  }
194507
195072
  out.totalBytes += read.size;
194508
- out.prepared.push(toPrepared(`${DELEGATION_OUTBOUND_DIR}/${dirName}/${relative5}`, file.rel, read));
195073
+ out.prepared.push(toPrepared(target, file.rel, read, { size: file.size, ...file.mtime ? { mtime: file.mtime } : {} }));
194509
195074
  }
194510
195075
  return out;
194511
195076
  }
@@ -194562,6 +195127,31 @@ function digestOf(text5) {
194562
195127
  const line = text5.trim().replace(/\s+/g, " ");
194563
195128
  return line.length > DIGEST_CHARS ? `${line.slice(0, DIGEST_CHARS)}\u2026` : line;
194564
195129
  }
195130
+ function childRoundMessageIds(childSessionId, round) {
195131
+ const suffix = round <= 1 ? "" : `:${round}`;
195132
+ return {
195133
+ inputId: deterministicUuid(`delegation-input:${childSessionId}${suffix}`),
195134
+ placeholderId: deterministicUuid(`delegation-placeholder:${childSessionId}${suffix}`)
195135
+ };
195136
+ }
195137
+ async function seedChildRoundRows(chatStore, childSessionId, round, prompt2, now) {
195138
+ const ids2 = childRoundMessageIds(childSessionId, round);
195139
+ await chatStore.appendMessageOnce({
195140
+ id: ids2.inputId,
195141
+ sessionId: childSessionId,
195142
+ role: "user",
195143
+ content: prompt2,
195144
+ createdAt: now
195145
+ });
195146
+ await chatStore.appendMessageOnce({
195147
+ id: ids2.placeholderId,
195148
+ sessionId: childSessionId,
195149
+ role: "assistant",
195150
+ content: "",
195151
+ status: "running",
195152
+ createdAt: now
195153
+ });
195154
+ }
194565
195155
  var import_node_crypto32, DelegationError, TITLE_TASK_CHARS, DIGEST_CHARS, PENDING_QUEUE_OWNER, DISPATCH_LEASE_MS, CONSECUTIVE_FAIL_LIMIT, DelegationService;
194566
195156
  var init_service3 = __esm({
194567
195157
  "../server/src/domains/delegations/service.ts"() {
@@ -194987,21 +195577,7 @@ var init_service3 = __esm({
194987
195577
  createdAt: now
194988
195578
  });
194989
195579
  }
194990
- await chatStore.appendMessageOnce({
194991
- id: deterministicUuid(`delegation-input:${record8.childSessionId}`),
194992
- sessionId: record8.childSessionId,
194993
- role: "user",
194994
- content: task,
194995
- createdAt: now
194996
- });
194997
- await chatStore.appendMessageOnce({
194998
- id: deterministicUuid(`delegation-placeholder:${record8.childSessionId}`),
194999
- sessionId: record8.childSessionId,
195000
- role: "assistant",
195001
- content: "",
195002
- status: "running",
195003
- createdAt: now
195004
- });
195580
+ await seedChildRoundRows(chatStore, record8.childSessionId, 1, task, now);
195005
195581
  }
195006
195582
  /**
195007
195583
  * 去程文件:读父会话工作区、翻成 §8.2 的同步错误码。
@@ -195161,7 +195737,7 @@ var init_service3 = __esm({
195161
195737
  settledAt: failedAt
195162
195738
  }).catch(() => null);
195163
195739
  const chatStore = await this.options.resolveChatSessions(companyId).catch(() => null);
195164
- await chatStore?.updateMessage(deterministicUuid(`delegation-placeholder:${record8.childSessionId}`), {
195740
+ await chatStore?.updateMessage(childRoundMessageIds(record8.childSessionId, record8.roundSummaries.length + 1).placeholderId, {
195165
195741
  content: `\u6D3E\u53D1\u6CA1\u80FD\u8D77\u6765\uFF1A${message}`,
195166
195742
  status: "error",
195167
195743
  completedAt: failedAt
@@ -195210,7 +195786,7 @@ var init_service3 = __esm({
195210
195786
  const pending = await store.countPendingTouches(fresh.id).catch(() => 0);
195211
195787
  const stopLoss = consecutiveFails >= CONSECUTIVE_FAIL_LIMIT;
195212
195788
  const chatStore = await this.options.resolveChatSessions(companyId).catch(() => null);
195213
- await chatStore?.updateMessage(deterministicUuid(`delegation-placeholder:${fresh.childSessionId}`), {
195789
+ await chatStore?.updateMessage(childRoundMessageIds(fresh.childSessionId, round).placeholderId, {
195214
195790
  content: text5 || (failureKind ? `\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u5B8C\uFF1A${error2}` : ""),
195215
195791
  status: failureKind ? "error" : "done",
195216
195792
  completedAt: at
@@ -195222,11 +195798,13 @@ var init_service3 = __esm({
195222
195798
  });
195223
195799
  return;
195224
195800
  }
195225
- const settled = await store.appendRoundSummary(fresh.id, newSummary, {
195801
+ const settledSeqAtRound = fresh.settledSeq + 1;
195802
+ const settledSummary = { ...newSummary, settledSeq: settledSeqAtRound };
195803
+ const settled = await store.appendRoundSummary(fresh.id, settledSummary, {
195226
195804
  consecutiveFails,
195227
195805
  state: failureKind ? "failed" : "done",
195228
195806
  failureKind: failureKind ?? null,
195229
- settledSeq: fresh.settledSeq + 1,
195807
+ settledSeq: settledSeqAtRound,
195230
195808
  settledAt: at
195231
195809
  }).catch(() => null);
195232
195810
  if (settled) await this.notifySettled(settled, companyId);
@@ -195255,17 +195833,19 @@ var init_service3 = __esm({
195255
195833
  const record8 = await store.getByChildSession(childSessionId).catch(() => null);
195256
195834
  if (!record8 || record8.state !== "running") return record8;
195257
195835
  const at = this.now();
195836
+ const settledSeqAtRound = record8.settledSeq + 1;
195258
195837
  const newSummary = {
195259
195838
  round: (record8.roundSummaries[record8.roundSummaries.length - 1]?.round ?? 0) + 1,
195260
195839
  text: `\u8FD9\u4E00\u8F6E\u88AB\u65F6\u949F\u95ED\u5408\uFF1A${reason}`,
195261
- at
195840
+ at,
195841
+ settledSeq: settledSeqAtRound
195262
195842
  };
195263
195843
  const settled = await store.appendRoundSummary(record8.id, newSummary, {
195264
195844
  state: "failed",
195265
195845
  // 退出帧丢失后被时钟闭合 = `interrupted`(§9),不是 `not-started`——它确实起来过。
195266
195846
  failureKind: "interrupted",
195267
195847
  consecutiveFails: record8.consecutiveFails + 1,
195268
- settledSeq: record8.settledSeq + 1,
195848
+ settledSeq: settledSeqAtRound,
195269
195849
  settledAt: at
195270
195850
  }).catch(() => null);
195271
195851
  if (settled) await this.notifySettled(settled, companyId);
@@ -195351,6 +195931,15 @@ var init_service3 = __esm({
195351
195931
  onDispatched: async () => {
195352
195932
  await queue.confirm();
195353
195933
  await store.markTouchesConsumed(record8.id, touches.map((touch) => touch.messageId), this.now());
195934
+ await seedChildRoundRows(
195935
+ chatStore,
195936
+ record8.childSessionId,
195937
+ record8.roundSummaries.length + 1,
195938
+ body2,
195939
+ this.now()
195940
+ ).catch((err) => {
195941
+ console.warn(`[delegation] \u5B50\u4F1A\u8BDD\u8865\u8F6E\u6B21\u884C\u5931\u8D25\uFF08${record8.id} round=${record8.roundSummaries.length + 1}\uFF09: ${String(err)}`);
195942
+ });
195354
195943
  },
195355
195944
  /**
195356
195945
  * **没送到就把正文放回可读态**(见 `dispatchExpert` 的 `onNotDispatched`)。
@@ -195449,6 +196038,10 @@ var init_service3 = __esm({
195449
196038
  const inbound = await this.readInboundFiles(parent, files);
195450
196039
  const inboundBundle = toBundleFiles(inbound.prepared);
195451
196040
  let delivered = false;
196041
+ let deliveryReasonCode;
196042
+ if (!this.options.appendToChild) {
196043
+ deliveryReasonCode = "no-append-port";
196044
+ }
195452
196045
  if (this.options.appendToChild) {
195453
196046
  const result = await this.options.appendToChild(record8.childSessionId, {
195454
196047
  // 文件是静默落盘的,append 这条路没有 TASK.md 可改——**必须自己在正文里说一声**,
@@ -195464,6 +196057,7 @@ ${inboundBundle.paths.map((path41) => `- ${path41}`).join("\n")}` : text5,
195464
196057
  ...Object.keys(inboundBundle.binaryFiles).length ? { binaryFiles: inboundBundle.binaryFiles } : {}
195465
196058
  }).catch(() => ({ accepted: false, reason: "append-threw" }));
195466
196059
  delivered = result.accepted === true;
196060
+ if (!delivered) deliveryReasonCode = result.reason ?? "rejected";
195467
196061
  }
195468
196062
  if (!delivered) {
195469
196063
  if (chatStore.enqueuePendingMessage) {
@@ -195522,6 +196116,7 @@ ${inboundBundle.paths.map((path41) => `- ${path41}`).join("\n")}` : text5,
195522
196116
  }
195523
196117
  return {
195524
196118
  delivered,
196119
+ ...delivered ? {} : { deliveryReasonCode: deliveryReasonCode ?? "rejected" },
195525
196120
  startedRound,
195526
196121
  pendingInputs: await this.pendingInputs(store, record8.id),
195527
196122
  // 起了新的一轮 = 快照与文件都随那一轮重新送过去了,没有「等下一轮再刷新」这回事。
@@ -195644,6 +196239,34 @@ ${inboundBundle.paths.map((path41) => `- ${path41}`).join("\n")}` : text5,
195644
196239
  }
195645
196240
  });
195646
196241
 
196242
+ // ../server/src/domains/delegations/continue-outcome.ts
196243
+ function describeContinueOutcome(input) {
196244
+ if (input.delivered) return { outcome: "inserted" };
196245
+ if (input.startedRound) return { outcome: "started-round" };
196246
+ const code2 = input.deliveryReasonCode ?? "rejected";
196247
+ return {
196248
+ outcome: "queued",
196249
+ // 认不出的码**原样带出来**,不折成「未知原因」——排障时那串字符本身就是线索。
196250
+ reason: REASON_TEXT[code2] ?? `\u672A\u80FD\u63D2\u5165\uFF08${code2}\uFF09`,
196251
+ reasonCode: code2
196252
+ };
196253
+ }
196254
+ var REASON_TEXT;
196255
+ var init_continue_outcome = __esm({
196256
+ "../server/src/domains/delegations/continue-outcome.ts"() {
196257
+ "use strict";
196258
+ REASON_TEXT = {
196259
+ "no-live-turn": "\u4E13\u5BB6\u6B64\u523B\u6CA1\u6709\u6B63\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E\uFF08\u591A\u534A\u662F\u521A\u6536\u5C3E\u3001\u4E0B\u4E00\u8F6E\u8FD8\u6CA1\u8D77\u6765\uFF09",
196260
+ unsupported: "\u8FD9\u4E2A\u4E13\u5BB6\u7528\u7684\u6A21\u578B\u4E0D\u652F\u6301\u4E2D\u9014\u63D2\u8BDD\uFF08\u8981\u7B49\u5B83\u8FD9\u4E00\u8F6E\u8DD1\u5B8C\uFF09",
196261
+ "session-closing": "\u4E13\u5BB6\u90A3\u4E00\u8F6E\u6B63\u5728\u6536\u5C3E\uFF0C\u8FD9\u53E5\u8BDD\u6765\u665A\u4E86\u4E00\u6B65",
196262
+ "turn-finished": "\u6295\u9012\u8FC7\u7A0B\u4E2D\u4E13\u5BB6\u90A3\u4E00\u8F6E\u5DF2\u7ECF\u6536\u5C3E\u4E86",
196263
+ rejected: "\u4E13\u5BB6\u90A3\u4E00\u4FA7\u62D2\u6536\u4E86\u8FD9\u6B21\u63D2\u5165",
196264
+ "append-threw": "\u6295\u9012\u8FC7\u7A0B\u4E2D\u51FA\u9519\uFF08\u7F51\u7EDC\u6216\u8282\u70B9\u5F02\u5E38\uFF09",
196265
+ "no-append-port": "\u8FD9\u5957\u90E8\u7F72\u6CA1\u6709\u63A5\u4E2D\u9014\u63D2\u5165\u901A\u9053"
196266
+ };
196267
+ }
196268
+ });
196269
+
195647
196270
  // ../server/src/domains/delegations/routes.ts
195648
196271
  function trustedDelegationInvocationId(body2, turn) {
195649
196272
  const scope = turn?.artifactId ?? turn?.dispatchId;
@@ -195712,8 +196335,16 @@ function delegationRoutes(service) {
195712
196335
  const text5 = requireString(raw["text"], "--text");
195713
196336
  const files = requireStringArray(raw["files"], "--file");
195714
196337
  try {
195715
- await service.continueDelegation(req.params.id, text5, callerOf(req), files);
195716
- return { status: 204, body: void 0 };
196338
+ const res = await service.continueDelegation(req.params.id, text5, callerOf(req), files);
196339
+ const view = describeContinueOutcome(res);
196340
+ const body2 = {
196341
+ outcome: view.outcome,
196342
+ ...view.reason ? { reason: view.reason } : {},
196343
+ ...view.reasonCode ? { reasonCode: view.reasonCode } : {},
196344
+ pendingInputs: res.pendingInputs,
196345
+ ...res.attachedFiles.length ? { attachedFiles: res.attachedFiles } : {}
196346
+ };
196347
+ return { status: 200, body: body2 };
195717
196348
  } catch (error2) {
195718
196349
  rethrow(error2);
195719
196350
  }
@@ -195741,6 +196372,7 @@ var init_routes2 = __esm({
195741
196372
  "use strict";
195742
196373
  import_node_crypto33 = require("node:crypto");
195743
196374
  init_router();
196375
+ init_continue_outcome();
195744
196376
  init_service3();
195745
196377
  }
195746
196378
  });
@@ -196112,6 +196744,9 @@ function delegationReturnText(terminal, opts) {
196112
196744
  if (opts.movedPaths.length > 0) {
196113
196745
  lines.push("", "\u4E13\u5BB6\u4EA4\u56DE\u7684\u6587\u4EF6\uFF08\u5DF2\u5728\u4F60\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF0C\u53EF\u76F4\u63A5\u8BFB\u53D6\uFF09\uFF1A", ...opts.movedPaths.map((p2) => `- ${p2}`));
196114
196746
  }
196747
+ if ((opts.unchangedPaths?.length ?? 0) > 0) {
196748
+ lines.push("", `\u53E6\u6709 ${opts.unchangedPaths.length} \u4E2A\u6587\u4EF6\u4E0E\u4E0A\u6B21\u4EA4\u56DE\u65F6\u9010\u5B57\u4E00\u81F4\uFF0C\u672C\u8F6E\u6CA1\u6709\u91CD\u4F20\uFF08\u4ECD\u5728\u4F60\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF0C\u8DEF\u5F84\u4E0D\u53D8\uFF09\u3002`);
196749
+ }
196115
196750
  const locationFailures = opts.skipped.filter((s2) => s2.code !== void 0 && isLocationFailureCode(s2.code));
196116
196751
  const contentSkipped = opts.skipped.filter((s2) => s2.code === void 0 || !isLocationFailureCode(s2.code));
196117
196752
  if (locationFailures.length > 0) {
@@ -196151,17 +196786,21 @@ async function deliverDelegationReturn(deps, terminal) {
196151
196786
  };
196152
196787
  }
196153
196788
  const dirName = delegationDirName(record8.label, record8.expertActorId, record8.id);
196789
+ const known = delegationOutboxFingerprints(record8.roundSummaries);
196154
196790
  const outbound = await prepareOutboundFiles(
196155
196791
  deps.workdir,
196156
196792
  // 子会话与父会话同公司(§6.1.2 隔离二),所以这里的 companyId 取父会话那个。
196157
196793
  { chatSessionId: record8.childSessionId, ...parentSession.companyId ? { companyId: parentSession.companyId } : {} },
196158
- dirName
196794
+ dirName,
196795
+ void 0,
196796
+ known
196159
196797
  ).catch((err) => {
196160
196798
  log3(`[delegation-return] \u4EA7\u7269\u642C\u8FD0\u5931\u8D25\uFF08delegation=${record8.id}\uFF09: ${String(err)}`);
196161
196799
  return {
196162
196800
  prepared: [],
196163
196801
  skipped: [{ source: "outputs/", code: "INTERNAL", message: "\u642C\u8FD0\u5931\u8D25" }],
196164
196802
  totalBytes: 0,
196803
+ unchanged: [],
196165
196804
  listDirCode: "INTERNAL"
196166
196805
  };
196167
196806
  });
@@ -196181,8 +196820,18 @@ async function deliverDelegationReturn(deps, terminal) {
196181
196820
  const text5 = delegationReturnText(terminal, {
196182
196821
  movedPaths: bundleFiles.paths,
196183
196822
  skipped: outbound.skipped,
196184
- recentRounds: deps.recentRounds ?? DELEGATION_RETURN_RECENT_ROUNDS
196823
+ recentRounds: deps.recentRounds ?? DELEGATION_RETURN_RECENT_ROUNDS,
196824
+ unchangedPaths: outbound.unchanged
196185
196825
  });
196826
+ const movedFileRefs = outbound.prepared.map((f2) => ({
196827
+ name: f2.rel,
196828
+ ...typeof f2.srcSize === "number" ? { size: f2.srcSize } : {},
196829
+ ...f2.srcMtime ? { mtime: f2.srcMtime } : {}
196830
+ }));
196831
+ const fingerprints = {};
196832
+ for (const f2 of movedFileRefs) {
196833
+ if (typeof f2.size === "number" || f2.mtime) fingerprints[f2.name] = { ...typeof f2.size === "number" ? { size: f2.size } : {}, ...f2.mtime ? { mtime: f2.mtime } : {} };
196834
+ }
196186
196835
  const source = delegationReturnSource(record8);
196187
196836
  const messageId = deriveSystemMessageId(source);
196188
196837
  let written;
@@ -196213,6 +196862,8 @@ async function deliverDelegationReturn(deps, terminal) {
196213
196862
  const bundle = bundleFiles.paths.length ? {
196214
196863
  files: bundleFiles.files,
196215
196864
  binaryFiles: bundleFiles.binaryFiles,
196865
+ // queued 那一档要靠它把指纹带到 flush 落账那一刻(`PendingChatAttachment.srcSize`)。
196866
+ fingerprints,
196216
196867
  taskLines: [
196217
196868
  "",
196218
196869
  "## \u4E13\u5BB6\u4EA4\u56DE\u7684\u6587\u4EF6",
@@ -196247,6 +196898,8 @@ async function deliverDelegationReturn(deps, terminal) {
196247
196898
  via: delivery.via,
196248
196899
  movedFiles: bundleFiles.paths.length,
196249
196900
  movedPaths: bundleFiles.paths,
196901
+ movedFileRefs,
196902
+ unchangedPaths: outbound.unchanged,
196250
196903
  skippedFiles: outbound.skipped.length
196251
196904
  };
196252
196905
  }
@@ -196258,6 +196911,7 @@ var init_return_flow = __esm({
196258
196911
  init_chat_system_message();
196259
196912
  init_chat_broadcast();
196260
196913
  init_files();
196914
+ init_src();
196261
196915
  }
196262
196916
  });
196263
196917
 
@@ -196401,8 +197055,10 @@ function assembleDelegationsDomain(deps) {
196401
197055
  );
196402
197056
  const targetRound = record8.roundSummaries[record8.roundSummaries.length - 1]?.round;
196403
197057
  let movedPaths = null;
197058
+ let movedRefs = [];
196404
197059
  if (outcome.status === "delivered" && (outcome.via === "turn" || outcome.via === "append") && outcome.movedPaths.length > 0) {
196405
197060
  movedPaths = outcome.movedPaths;
197061
+ movedRefs = outcome.movedFileRefs;
196406
197062
  }
196407
197063
  if (movedPaths === null || targetRound === void 0) return;
196408
197064
  const store = await deps.resolveStore(companyId).catch(() => null);
@@ -196415,7 +197071,7 @@ function assembleDelegationsDomain(deps) {
196415
197071
  `[delegation-return] \u4EA7\u7269\u6E05\u5355\u843D\u8D26\u672A\u6210 (immediate) delegation=${record8.id} round=${targetRound} status=resolve-failed moved=${movedPaths.length} paths=${pathsField}`
196416
197072
  );
196417
197073
  } else {
196418
- outcomeCode = await registerRoundFiles(store, record8.id, targetRound, movedPaths);
197074
+ outcomeCode = await registerRoundFiles(store, record8.id, targetRound, movedRefs.length ? movedRefs : movedPaths);
196419
197075
  if (outcomeCode === "ok") return;
196420
197076
  deps.metrics?.recordFilesRegisterFailure?.(outcomeCode);
196421
197077
  warn(
@@ -196429,6 +197085,11 @@ function assembleDelegationsDomain(deps) {
196429
197085
  chatSessionId: record8.parentSessionId,
196430
197086
  delegationRef: { delegationId: record8.id, round: targetRound },
196431
197087
  paths: movedPaths,
197088
+ // 补账那一趟同样要带指纹,否则「首次登记失败 → 补账成功」的路径会留下无指纹条目。
197089
+ fingerprints: Object.fromEntries(movedRefs.map((f2) => [f2.name, {
197090
+ ...typeof f2.size === "number" ? { size: f2.size } : {},
197091
+ ...f2.mtime ? { mtime: f2.mtime } : {}
197092
+ }])),
196432
197093
  reason: `immediate-register-failed:${outcomeCode}`
196433
197094
  }).catch((err) => {
196434
197095
  warn(
@@ -196451,16 +197112,20 @@ async function registerDelegationFilesOnFlush(deps, row, companyId) {
196451
197112
  const warn = deps.log ?? ((m2) => console.warn(m2));
196452
197113
  const ref2 = row.delegationRef;
196453
197114
  if (!ref2) return "no-ref";
196454
- const paths = (row.attachments ?? []).map((a) => a.path).filter((p2) => !!p2);
196455
- if (paths.length === 0) return "no-files";
197115
+ const rows = (row.attachments ?? []).filter((a) => !!a.path);
197116
+ if (rows.length === 0) return "no-files";
196456
197117
  const store = await deps.resolveStore(companyId).catch(() => null);
196457
197118
  if (!store) return "failed";
196458
- const files = paths.map((name) => ({ name }));
197119
+ const files = rows.map((a) => ({
197120
+ name: a.path,
197121
+ ...typeof a.srcSize === "number" ? { size: a.srcSize } : {},
197122
+ ...a.srcMtime ? { mtime: a.srcMtime } : {}
197123
+ }));
196459
197124
  const outcome = await registerRoundFiles(store, ref2.delegationId, ref2.round, files);
196460
197125
  if (outcome !== "ok") {
196461
197126
  deps.metrics?.recordFilesRegisterFailure?.(outcome);
196462
197127
  warn(
196463
- `[delegation-return] flush \u843D\u8D26\u672A\u6210 delegation=${ref2.delegationId} round=${ref2.round} status=${outcome} files=${paths.length}`
197128
+ `[delegation-return] flush \u843D\u8D26\u672A\u6210 delegation=${ref2.delegationId} round=${ref2.round} status=${outcome} files=${files.length}`
196464
197129
  );
196465
197130
  }
196466
197131
  return outcome;
@@ -196483,6 +197148,31 @@ var init_assembly = __esm({
196483
197148
  }
196484
197149
  });
196485
197150
 
197151
+ // ../server/src/domains/delegations/child-live-turn.ts
197152
+ function registerDelegatedChildTurn(liveChat, childSessionId, session) {
197153
+ const appendInput = session.appendInput;
197154
+ if (typeof appendInput !== "function" || session.canAppendInput === false) return;
197155
+ const ctrl = liveChat.start(childSessionId, {
197156
+ runtimeSessionId: session.id,
197157
+ ...session.runId ? { runId: session.runId } : {},
197158
+ kill: () => {
197159
+ void session.kill?.();
197160
+ },
197161
+ appendInput: (input) => appendInput.call(session, input),
197162
+ // **每次现读**,不快照:一轮跑到收尾时 stdin 会先关,那之后 runtime 自己会回
197163
+ // `session-closing`,判断权本就该留在它那儿(同 `/api/chat` 那条路的写法)。
197164
+ get canAppendInput() {
197165
+ return session.canAppendInput !== false;
197166
+ }
197167
+ });
197168
+ void session.done.then(() => ctrl.finish("done"), () => ctrl.finish("error"));
197169
+ }
197170
+ var init_child_live_turn = __esm({
197171
+ "../server/src/domains/delegations/child-live-turn.ts"() {
197172
+ "use strict";
197173
+ }
197174
+ });
197175
+
196486
197176
  // ../connectors/src/_base/wrapper-assets.ts
196487
197177
  function isUsableWrapper(scriptPath) {
196488
197178
  if (!(0, import_node_fs7.existsSync)(scriptPath)) return false;
@@ -201232,7 +201922,7 @@ function authorizedCommandReceipt(command, args, actor) {
201232
201922
  case "promote":
201233
201923
  return { message: "promoted to persistent" };
201234
201924
  case "resolveEscalation":
201235
- return { message: `\u5DF2\u5173\u95ED\u8BE5\u6761\u4E0A\u62A5\uFF08${str(args, "escalationId")}\uFF09\u3002` };
201925
+ return { message: optStr(args, "gapId") ? `\u5DF2\u786E\u8BA4\u6062\u590D\u65B9\u6848\uFF0C\u89E3\u51B3\u7F3A\u53E3 ${str(args, "gapId")}\u3001\u5173\u95ED\u5173\u8054\u4E0A\u62A5 ${str(args, "escalationId")}\uFF1B\u8C03\u5EA6\u5668\u5C06\u5728\u5176\u4ED6\u963B\u585E\u89E3\u9664\u540E\u7EE7\u7EED\u6267\u884C\u3002` : `\u5DF2\u5173\u95ED\u8BE5\u6761\u4E0A\u62A5\uFF08${str(args, "escalationId")}\uFF09\u3002` };
201236
201926
  default:
201237
201927
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u65E0\u56DE\u6267\u6784\u9020 ${command}`);
201238
201928
  }
@@ -201271,7 +201961,11 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
201271
201961
  );
201272
201962
  }
201273
201963
  }
201964
+ const linkedGapId = escalationById(kernel.model, escalationId)?.gapId;
201965
+ const linkedGap = linkedGapId ? (kernel.model.gaps.get(artifactId) ?? []).find((g2) => g2.gapId === linkedGapId) : void 0;
201966
+ if (linkedGap && !why?.trim()) throw new Error("\u8BF7\u8BF4\u660E\u91C7\u7528\u4EC0\u4E48\u6062\u590D\u65B9\u6848\uFF0C\u518D\u63D0\u4EA4\u7F3A\u53E3\u4E0E\u4E0A\u62A5\u7684\u8054\u5408\u786E\u8BA4");
201274
201967
  const normalizedArgs = {
201968
+ ...linkedGap ? { gapId: linkedGap.gapId, continueExecution: true } : {},
201275
201969
  escalationId,
201276
201970
  // ← 已四级解析出的具体一条(绝非 undefined、绝非「全清」)
201277
201971
  artifactId,
@@ -201362,14 +202056,15 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
201362
202056
  }
201363
202057
  const projectId2 = optStr(args, "project_id") ?? optStr(args, "projectId");
201364
202058
  const schemaForSeed = (createdViaArg ?? "manual-spawn") === "seed" ? ctx.schema : void 0;
202059
+ const bindingState = ctx.workspaceBindingState ?? ctx.artifactState;
201365
202060
  if (projectId2 !== void 0) {
201366
- if (!ctx.projectState || !ctx.artifactState) {
202061
+ if (!ctx.projectState || !bindingState) {
201367
202062
  throw new Error("spawn \u5E26 project_id \u9700\u8981\u670D\u52A1\u7AEF\u6CE8\u5165\u9879\u76EE\u5B58\u50A8\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u5B58\u50A8");
201368
202063
  }
201369
202064
  const project = await ctx.projectState.getProject(projectId2);
201370
202065
  if (!project) throw new Error(`project not found: ${projectId2}`);
201371
- await ctx.artifactState.upsertWorkspaceBinding({ workOrderId: workspace, projectId: projectId2 });
201372
- } else if (createdViaArg === "seed" && ctx.projectState && ctx.artifactState) {
202066
+ await bindingState.upsertWorkspaceBinding({ workOrderId: workspace, projectId: projectId2 });
202067
+ } else if (createdViaArg === "seed" && ctx.projectState && bindingState) {
201373
202068
  await ensureWorkorderProject({
201374
202069
  workOrderId: workspace,
201375
202070
  ...optStr(args, "title") ? { title: optStr(args, "title") } : {},
@@ -201380,7 +202075,7 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
201380
202075
  // 由读侧从工单 brief.owner 派生(service.resolveProjectCreator)。
201381
202076
  ...ctx.workorderInitiator?.humanActorId ? { createdBy: ctx.workorderInitiator.humanActorId } : !isAgent(actor) ? { createdBy: actor } : {},
201382
202077
  createProject: makeEnsureProjectFromStore(ctx.projectState),
201383
- upsertBinding: (b2) => ctx.artifactState.upsertWorkspaceBinding(b2),
202078
+ upsertBinding: (b2) => bindingState.upsertWorkspaceBinding(b2),
201384
202079
  onWarn: (m2) => console.warn(m2)
201385
202080
  });
201386
202081
  }
@@ -201868,10 +202563,10 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
201868
202563
  const note = optStr(args, "note");
201869
202564
  const via = optStr(args, "via");
201870
202565
  const target = kernel.model.annotations.get(annotationId);
201871
- if (target && target.state !== "open") {
202566
+ if (target && target.state !== "open" && resolution !== "acknowledged") {
201872
202567
  return { message: `annotation \u5DF2\u5173\u95ED\uFF08${target.state}\uFF09\u2014\u2014\u65E0\u9700\u518D\u5904\u7406${isReactivatedLetter(target, kernel.model.convergenceUnified) ? "\uFF1B\u8FD9\u5C01\u4FE1\u5728\u590D\u6D3B\u4E2D\uFF0C\u56DE\u5E16\u7B54\u590D\u7528 `oasis reply`\uFF0C\u6216\u6539\u5185\u5BB9 propose\uFF08\u7533\u62A5\u4F1A\u5E26\u4E0A\u5B83\uFF09" : ""}` };
201873
202568
  }
201874
- await kernel.resolveAnnotation({ annotationId, actor, resolution, note, ...via !== void 0 ? { via } : {}, ...hand !== void 0 ? { hand } : {} });
202569
+ await kernel.resolveAnnotation({ annotationId, actor, resolution, note, ...via !== void 0 ? { via } : {}, ...hand !== void 0 ? { hand } : {}, ...ctx.sessionArtifactId ? { viaNode: ctx.sessionArtifactId } : {} });
201875
202570
  return { message: `annotation \u2192 ${resolution}${via ? `\uFF08via ${via}\uFF09` : ""}${isAgent(actor) && target?.author.startsWith("actor:human:") ? "\uFF1B\u4F5C\u8005\u4F1A\u6536\u5230\u901A\u77E5\uFF0C\u82E5\u4E0D\u8BA4\u53EF\u4F1A\u5728\u4FE1\u91CC\u56DE\u5E16\u5524\u4F60" : ""}` };
201876
202571
  }
201877
202572
  case "gap": {
@@ -202422,7 +203117,15 @@ async function startOasisServer(opts) {
202422
203117
  resolveStore: opts.delegationStoreFor,
202423
203118
  resolveChatSessions: resolveChatSessionsForDelegation,
202424
203119
  resolveActors: async (companyId) => (await actorsDomain2.resolveCtx(companyId)).service,
202425
- dispatchChat,
203120
+ // **派发完顺手把子会话这一轮登记进 live 注册表**(v2 bug 0088)。缺这一步时下面那行
203121
+ // `appendToChild` 恒回 `no-live-turn`——注册表里从来没有以 `childSessionId` 为键的轮,
203122
+ // 于是每一条 `--continue` 转达都落队列,专家要等本轮跑完才看见「手上这段作废」。
203123
+ // 理由与两条纪律见 `child-live-turn.ts` 的文件头。
203124
+ dispatchChat: async (request2) => {
203125
+ const session = await dispatchChat(request2);
203126
+ registerDelegatedChildTurn(liveChat, request2.chatSessionId, session);
203127
+ return session;
203128
+ },
202426
203129
  appendToChild: (childSessionId, text5, extra) => liveChat.append(childSessionId, text5, extra),
202427
203130
  // 用 lambda 转一层:`broadcastDepsFor` 在本文件里声明得更靠后(回流是异步发生的,
202428
203131
  // 那时它早就赋过值了)。直接把值递进去会撞 TDZ。
@@ -202663,16 +203366,25 @@ async function startOasisServer(opts) {
202663
203366
  ...store?.enqueuePendingMessage ? {
202664
203367
  enqueuePending: async (input) => {
202665
203368
  const bundle = input.bundle;
203369
+ const fp = (p2) => {
203370
+ const f2 = bundle?.fingerprints?.[p2];
203371
+ return {
203372
+ ...typeof f2?.size === "number" ? { srcSize: f2.size } : {},
203373
+ ...f2?.mtime ? { srcMtime: f2.mtime } : {}
203374
+ };
203375
+ };
202666
203376
  const attachments = bundle ? [
202667
203377
  ...Object.entries(bundle.files ?? {}).map(([p2, text5]) => ({
202668
203378
  name: p2.slice(p2.lastIndexOf("/") + 1) || p2,
202669
203379
  text: text5,
202670
- path: p2
203380
+ path: p2,
203381
+ ...fp(p2)
202671
203382
  })),
202672
203383
  ...Object.entries(bundle.binaryFiles ?? {}).map(([p2, b64]) => ({
202673
203384
  name: p2.slice(p2.lastIndexOf("/") + 1) || p2,
202674
203385
  bytesBase64: b64,
202675
- path: p2
203386
+ path: p2,
203387
+ ...fp(p2)
202676
203388
  }))
202677
203389
  ] : [];
202678
203390
  const { randomUUID: randomUUID40 } = await import("node:crypto");
@@ -202702,7 +203414,15 @@ async function startOasisServer(opts) {
202702
203414
  // ——生产 `deliverToAgent:512` 会开轮/append,用户听到空消息且下一场 UPDATE 再把已完成
202703
203415
  // 的行改回 delivering,违背「只补账、不投递」语义。
202704
203416
  enqueueRegisterPending: async (input) => {
202705
- const attachments = input.paths.map((p2) => ({ name: p2.slice(p2.lastIndexOf("/") + 1) || p2, path: p2 }));
203417
+ const attachments = input.paths.map((p2) => {
203418
+ const f2 = input.fingerprints?.[p2];
203419
+ return {
203420
+ name: p2.slice(p2.lastIndexOf("/") + 1) || p2,
203421
+ path: p2,
203422
+ ...typeof f2?.size === "number" ? { srcSize: f2.size } : {},
203423
+ ...f2?.mtime ? { srcMtime: f2.mtime } : {}
203424
+ };
203425
+ });
202706
203426
  const { randomUUID: randomUUID40 } = await import("node:crypto");
202707
203427
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
202708
203428
  try {
@@ -203136,6 +203856,22 @@ async function startOasisServer(opts) {
203136
203856
  }));
203137
203857
  return;
203138
203858
  }
203859
+ if (url.pathname.startsWith("/api/files/")) {
203860
+ const company = opts.resolveCompanyContext ? await opts.resolveCompanyContext(actor, req.headers, url.pathname) : void 0;
203861
+ if (company && company.kind !== "ok") {
203862
+ res.writeHead(403, { "content-type": "application/json" }).end(JSON.stringify({
203863
+ error: { code: "FORBIDDEN", message: "\u65E0\u6743\u8BFB\u53D6\u8BE5\u516C\u53F8\u7684\u6587\u4EF6" }
203864
+ }));
203865
+ return;
203866
+ }
203867
+ await serveFileRequest(
203868
+ req,
203869
+ res,
203870
+ url,
203871
+ await resolveEngine(company?.kind === "ok" ? company.companyId : void 0)
203872
+ );
203873
+ return;
203874
+ }
203139
203875
  if (url.pathname === "/api/whoami" && req.method === "GET") {
203140
203876
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ actor }));
203141
203877
  return;
@@ -203175,6 +203911,7 @@ async function startOasisServer(opts) {
203175
203911
  }
203176
203912
  };
203177
203913
  const artifactStateNow = async () => opts.artifactStateFor ? await opts.artifactStateFor(currentCompanyId) : opts.artifactState;
203914
+ const projectStateNow = async () => opts.projectStateFor ? await opts.projectStateFor(currentCompanyId) : opts.projectState;
203178
203915
  if (url.pathname.startsWith("/api/channels")) {
203179
203916
  const json = (status, body2) => {
203180
203917
  res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body2));
@@ -203273,6 +204010,10 @@ async function startOasisServer(opts) {
203273
204010
  }
203274
204011
  }
203275
204012
  const chatAnchorMessageId = await anchorMessageIdFor(chatOrigin, chatStoreNowOrNull);
204013
+ const spawnArgs = body2.args;
204014
+ const needsProject = body2.command === "spawn" && (spawnArgs?.["createdVia"] === "seed" || spawnArgs?.["project_id"] !== void 0 || spawnArgs?.["projectId"] !== void 0);
204015
+ const commandProjects = needsProject ? await projectStateNow() : opts.projectState;
204016
+ const commandBindings = needsProject && commandProjects ? await artifactStateNow() : void 0;
203276
204017
  const result = await runCommand(
203277
204018
  engine.kernel,
203278
204019
  engine.blobs,
@@ -203291,13 +204032,14 @@ async function startOasisServer(opts) {
203291
204032
  ...chatOrigin !== void 0 ? { chatOrigin } : {},
203292
204033
  ...chatAnchorMessageId !== void 0 ? { chatAnchorMessageId } : {},
203293
204034
  ...sessionEscalationId !== void 0 ? { sessionEscalationId } : {},
203294
- ...opts.projectState ? { projectState: opts.projectState } : {},
204035
+ ...commandProjects ? { projectState: commandProjects } : {},
204036
+ ...commandBindings ? { workspaceBindingState: commandBindings } : {},
203295
204037
  ...opts.artifactState ? { artifactState: opts.artifactState } : {},
203296
204038
  ...opts.schema ? { schema: opts.schema } : {},
203297
204039
  ...engine.registry ? { registry: engine.registry } : {},
203298
204040
  ...opts.resolveAssignableRoles ? { resolveAssignableRoles: opts.resolveAssignableRoles } : {},
203299
204041
  ...opts.planWorkorderWithAgent ? { planWorkorderWithAgent: opts.planWorkorderWithAgent } : {},
203300
- ...opts.resolveCodeRepo ? { resolveCodeRepo: opts.resolveCodeRepo } : {},
204042
+ ...opts.resolveCodeRepo ? { resolveCodeRepo: (ws) => opts.resolveCodeRepo(ws, currentCompanyId) } : {},
203301
204043
  ...opts.dispatchProduce ? { dispatchProduce: opts.dispatchProduce } : {},
203302
204044
  ...wodrafts ? { workorderDrafts: wodrafts } : {},
203303
204045
  workorderDraftPlannerIssues: wodraftPlannerIssues,
@@ -203585,6 +204327,7 @@ async function startOasisServer(opts) {
203585
204327
  }
203586
204328
  const resuming = resumeState.hasNodes;
203587
204329
  let sessionProjectId;
204330
+ let sessionBindingState;
203588
204331
  if (body2.chatSessionId && opts.chatSession) {
203589
204332
  const applyChatStore = await chatStoreNow();
203590
204333
  const session = await applyChatStore.getSession(body2.chatSessionId);
@@ -203593,10 +204336,12 @@ async function startOasisServer(opts) {
203593
204336
  }
203594
204337
  sessionProjectId = session.projectId ?? void 0;
203595
204338
  if (sessionProjectId) {
203596
- if (!opts.projectState || !opts.artifactState) {
204339
+ const projects = await projectStateNow();
204340
+ sessionBindingState = await artifactStateNow();
204341
+ if (!projects || !sessionBindingState) {
203597
204342
  throw new Error("chat session \u5E26 projectId \u65F6\u9700\u8981\u670D\u52A1\u7AEF\u6CE8\u5165\u9879\u76EE\u5B58\u50A8\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u5B58\u50A8");
203598
204343
  }
203599
- const project = await opts.projectState.getProject(sessionProjectId);
204344
+ const project = await projects.getProject(sessionProjectId);
203600
204345
  if (!project) throw new Error(`project not found: ${sessionProjectId}`);
203601
204346
  }
203602
204347
  }
@@ -203646,7 +204391,7 @@ async function startOasisServer(opts) {
203646
204391
  return;
203647
204392
  }
203648
204393
  if (targetWs && sessionProjectId) {
203649
- await opts.artifactState.upsertWorkspaceBinding({ workOrderId: targetWs, projectId: sessionProjectId });
204394
+ await sessionBindingState.upsertWorkspaceBinding({ workOrderId: targetWs, projectId: sessionProjectId });
203650
204395
  }
203651
204396
  const result = resuming ? { interventionId: "", applied: 0, resumed: true } : await engine.kernel.applyIntervention(body2.plan, body2.baseSeqs, actor);
203652
204397
  if (resuming) {
@@ -204829,8 +205574,26 @@ ${composed}`;
204829
205574
  });
204830
205575
  }
204831
205576
  const itemStore = persistTarget ? opts.resolveChatItems ? await opts.resolveChatItems(currentCompanyId).catch(() => void 0) : opts.chatItems : void 0;
205577
+ const versionSeed = itemStore ? await itemStore.sessionVersionCursor(persistTarget?.id ?? session.id).catch(() => 0) : 0;
204832
205578
  const itemLedger = new ChatItemLedger({
204833
205579
  ...itemStore ? { items: itemStore } : {},
205580
+ versionSeed,
205581
+ /* 用户那条消息一落库就推上 v3 流(ADR-0510 D2 的收口)。
205582
+ 不推的话:乐观条没有服务端号、被钉在列表末尾,而 agent 的正文带着更大的号从
205583
+ 直播流到达、进「有号」段 ⇒ **回复显示在用户消息上面**,直到下一次快照才跳回去
205584
+ (2026-09-09 用户现场)。这里把窗口从「等下一次快照」压到「一次 INSERT 往返」。
205585
+ 只推 user:assistant 的每一条本来就走 `emitV3`。 */
205586
+ onPersisted: (info) => {
205587
+ if (info.role !== "user") return;
205588
+ opts.liveChat?.publishUserItem(persistTarget?.id ?? session.id, {
205589
+ itemId: info.itemId,
205590
+ ord: info.ord,
205591
+ version: info.version,
205592
+ text: info.text,
205593
+ ...clientSubmitId ? { clientSubmitId } : {},
205594
+ ...effectiveAttachments.length ? { attachments: effectiveAttachments } : {}
205595
+ });
205596
+ },
204834
205597
  sessionId: persistTarget?.id ?? session.id,
204835
205598
  // 优先账本 id(`chat_session_turns.id`,跨进程唯一、重启不失忆);账本没接入才兜底。
204836
205599
  turnId: heldTurn?.id ?? fallbackTurnId(session.runId),
@@ -206141,6 +206904,7 @@ var init_server3 = __esm({
206141
206904
  init_src();
206142
206905
  init_src();
206143
206906
  init_src();
206907
+ init_file_http();
206144
206908
  init_build_info();
206145
206909
  init_artifact_content2();
206146
206910
  init_command_policy();
@@ -206182,6 +206946,7 @@ var init_server3 = __esm({
206182
206946
  init_workorder_terminal();
206183
206947
  init_knowledge2();
206184
206948
  init_assembly();
206949
+ init_child_live_turn();
206185
206950
  init_src8();
206186
206951
  init_continuation();
206187
206952
  githubAppPending = new PendingAppCreations();
@@ -206762,7 +207527,12 @@ var init_memory_registry_store = __esm({
206762
207527
  async putVariable(v2) {
206763
207528
  const mk = this._varKey(v2.key, v2.actorId, v2.projectId);
206764
207529
  const ex = this.variables.get(mk);
206765
- this.variables.set(mk, { ...v2, ...ex?.lastUsedAt !== void 0 ? { lastUsedAt: ex.lastUsedAt } : {} });
207530
+ const keptName = v2.name ?? ex?.name;
207531
+ this.variables.set(mk, {
207532
+ ...v2,
207533
+ ...ex?.lastUsedAt !== void 0 ? { lastUsedAt: ex.lastUsedAt } : {},
207534
+ ...keptName ? { name: keptName } : {}
207535
+ });
206766
207536
  }
206767
207537
  async touchVariableUsage(key, actorId, projectId2, when) {
206768
207538
  const mk = this._varKey(key, actorId, projectId2);
@@ -209263,6 +210033,30 @@ function foldSingleNodeTasks(items) {
209263
210033
  }
209264
210034
  return folded;
209265
210035
  }
210036
+ function fillFolderSizes(items) {
210037
+ const folders = items.filter((i) => i.kind === "folder" && i.path);
210038
+ if (folders.length === 0) return;
210039
+ const files = items.filter((i) => i.kind !== "folder" && i.path);
210040
+ for (const folder of folders) {
210041
+ const prefix = `${folder.path}/`;
210042
+ let sum = 0;
210043
+ let seen = 0;
210044
+ let complete = true;
210045
+ for (const file of files) {
210046
+ if (!file.path.startsWith(prefix)) continue;
210047
+ seen += 1;
210048
+ if (file.size == null) {
210049
+ complete = false;
210050
+ break;
210051
+ }
210052
+ sum += file.size;
210053
+ }
210054
+ folder.size = seen > 0 && complete ? sum : null;
210055
+ }
210056
+ }
210057
+ function engineContentKind(k2) {
210058
+ return k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
210059
+ }
209266
210060
  function namedError(name, message) {
209267
210061
  const err = new Error(message);
209268
210062
  err.name = name;
@@ -209484,6 +210278,13 @@ var init_service4 = __esm({
209484
210278
  async listProjects() {
209485
210279
  return [...this.projects.values()].map((project) => ({ ...project }));
209486
210280
  }
210281
+ /** 与 Postgres 同语义:主记录 + 成员 + 上传文件 + 内容位置一并清掉;不存在的 id 幂等返回。 */
210282
+ async deleteProject(id) {
210283
+ this.projects.delete(id);
210284
+ this.members.delete(id);
210285
+ for (const [fileId, f2] of this.files) if (f2.projectId === id) this.files.delete(fileId);
210286
+ for (const [locId, l] of this.contentLocations) if (l.projectId === id) this.contentLocations.delete(locId);
210287
+ }
209487
210288
  async replaceProjectMembers(projectId2, members) {
209488
210289
  this.members.set(projectId2, new Map(members.map((member) => [member.actorId, cloneProjectMember(member)])));
209489
210290
  }
@@ -209629,6 +210430,7 @@ var init_service4 = __esm({
209629
210430
  }
209630
210431
  projectCreatedHandler;
209631
210432
  onProjectNameChanged;
210433
+ onProjectDeleted;
209632
210434
  /**
209633
210435
  * Cross-domain follow-up for project-scoped resources. Project persistence stays authoritative:
209634
210436
  * a failed follow-up is reported but never turns an already-created project into a false 500.
@@ -209643,6 +210445,10 @@ var init_service4 = __esm({
209643
210445
  setOnProjectNameChanged(handler) {
209644
210446
  this.onProjectNameChanged = handler;
209645
210447
  }
210448
+ /** 删项目后的跨域清理钩子(会话解绑 + 项目变量清理)。装配层注入;缺省 = 无副作用。 */
210449
+ setOnProjectDeleted(handler) {
210450
+ this.onProjectDeleted = handler;
210451
+ }
209646
210452
  /** 工单→项目绑定查询(content-location add 的项目匹配守卫用)。 */
209647
210453
  async getWorkspaceBinding(workOrderId) {
209648
210454
  return this.artifacts.getWorkspaceBinding(workOrderId);
@@ -210031,6 +210837,60 @@ var init_service4 = __esm({
210031
210837
  await this.artifacts.upsertWorkspaceBinding(binding);
210032
210838
  return binding;
210033
210839
  }
210840
+ /**
210841
+ * 删除项目(帧 `444:1792` 三点菜单第二项,口径由发起人 2026-09-09 拍板)。
210842
+ *
210843
+ * **只删项目这个壳**——项目主记录 + 成员名单 + 上传文件记录 + 内容位置。任务和会话一条都不删:
210844
+ *
210845
+ * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 回落到自己的
210846
+ * `proj_tmp_<slug>`,也就是读侧的「无分类项目」。发起人明确不要「有任务在跑就不让删」
210847
+ * 这道闸,所以这里没有任何在跑判断。
210848
+ * 2. 搬工单的同时**把本项目的 git 类内容位置复制一份到每个工单的临时项目上**。
210849
+ * 这条不是可选的:`git/resolve.ts` 的 `resolveCodeRepo` 是**派发时**沿
210850
+ * `binding → project → listContentLocations` 实时解的,不带过去的话,正在跑的任务
210851
+ * 下一个节点就取不到代码仓——正是 `ephemeral-project.ts` 顶部那条五环事故链。
210852
+ * 3. 会话的 `project_id` 置空、项目作用域的变量/密钥清理,走 {@link OnProjectDeleted} 钩子
210853
+ * (本 service 对会话域与凭据域零耦合,同 `onProjectNameChanged` 的注入模式)。
210854
+ *
210855
+ * 拒绝两类 id:`proj:uncategorized`(读侧虚拟卡,存储里没有真行)与 `proj_tmp_*`
210856
+ * (工单的兜底归属,删掉等于把工单变成无绑定态——契约里 `Workspace.projectId` 必填)。
210857
+ */
210858
+ async deleteProject(id) {
210859
+ const projectId2 = id?.trim();
210860
+ if (!projectId2) throw new Error("project id is required");
210861
+ if (isUncategorizedProjectId(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
210862
+ if (isEphemeralProject(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
210863
+ const project = await this.projects.getProject(projectId2);
210864
+ if (!project) throw new Error(`project not found: ${projectId2}`);
210865
+ const gitLocations = (await this.projects.listContentLocations(projectId2)).filter((loc) => loc.kind === "external-home" && loc.externalHome?.system === "git");
210866
+ const now = (/* @__PURE__ */ new Date()).toISOString();
210867
+ const bindings = await this.artifacts.listWorkspaceBindings();
210868
+ const movedWorkOrderIds = [];
210869
+ for (const binding of bindings) {
210870
+ if (binding.projectId !== projectId2) continue;
210871
+ const moved = await this.unlinkWorkorder(binding.workOrderId);
210872
+ movedWorkOrderIds.push(binding.workOrderId);
210873
+ for (const loc of gitLocations) {
210874
+ await this.projects.addContentLocation({
210875
+ ...loc,
210876
+ projectId: moved.projectId,
210877
+ // 派生 id:同一条位置搬到同一个临时项目永远算出同一个 id(addContentLocation 是
210878
+ // upsert),重跑不会堆出重复的仓库行。
210879
+ locationId: `pcl-${moved.projectId}-carried-${loc.locationId}`,
210880
+ updatedAt: now
210881
+ });
210882
+ }
210883
+ }
210884
+ await this.projects.deleteProject(projectId2);
210885
+ if (this.onProjectDeleted) {
210886
+ try {
210887
+ await this.onProjectDeleted({ projectId: projectId2, projectName: project.name });
210888
+ } catch (error2) {
210889
+ console.warn(`[projects] project-deleted follow-up failed for ${projectId2}: ${error2 instanceof Error ? error2.message : String(error2)}`);
210890
+ }
210891
+ }
210892
+ return { projectId: projectId2, movedWorkOrderIds, carriedGitLocationCount: gitLocations.length };
210893
+ }
210034
210894
  /**
210035
210895
  * 项目关联工单(帧 `1355:4918`):把内部私有的 `listProjectWorkorders` 暴露给路由层。
210036
210896
  * `projectId="proj:uncategorized"` 时聚合所有 `proj_tmp_*` 绑定的工单。
@@ -210091,6 +210951,7 @@ var init_service4 = __esm({
210091
210951
  const projectByWorkOrder = new Map(bindings.map((b2) => [b2.workOrderId, b2.projectId]));
210092
210952
  const workorderSummaries = await this.listProjectWorkorders(projectId2);
210093
210953
  const workorderTitleById = new Map(workorderSummaries.map((w2) => [w2.id, w2.title]));
210954
+ const workorderOwnerById = new Map(workorderSummaries.map((w2) => [w2.id, w2.owner?.id ?? null]));
210094
210955
  const engineRows = await this.collectEngineFilesForProject(
210095
210956
  projectId2,
210096
210957
  workorderSummaries.map((w2) => w2.id),
@@ -210109,6 +210970,8 @@ var init_service4 = __esm({
210109
210970
  const taskId = record8.createdFromWorkOrderId ?? artifact?.workspace ?? null;
210110
210971
  const taskTitle = taskId ? workorderTitleById.get(taskId) ?? null : null;
210111
210972
  const creatorId = revision.author || null;
210973
+ const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
210974
+ const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
210112
210975
  const folderPath = taskId ? `wo:${taskId}/node:${record8.artifactId}` : null;
210113
210976
  if (folderPath) items.push({
210114
210977
  kind: "folder",
@@ -210121,13 +210984,14 @@ var init_service4 = __esm({
210121
210984
  size: null,
210122
210985
  taskId,
210123
210986
  taskTitle,
210124
- creatorKind: null,
210125
- creatorId: null,
210126
- creatorName: null,
210987
+ creatorKind,
210988
+ creatorId,
210989
+ creatorName,
210127
210990
  updatedAt: record8.updatedAt
210128
210991
  });
210129
210992
  const content3 = revision.contentKind === "manifest" ? await this.readContentRefText(revision.contentRef) : null;
210130
210993
  const files2 = content3 !== null ? tryParseManifestFiles(content3) : void 0;
210994
+ const wholeSize = files2?.length ? null : content3 !== null ? Buffer.byteLength(content3, "utf8") : await this.contentByteSize(revision.contentRef, revision.contentKind);
210131
210995
  for (const file of files2?.length ? files2 : [null]) {
210132
210996
  items.push({
210133
210997
  kind: "artifact",
@@ -210137,12 +211001,15 @@ var init_service4 = __esm({
210137
211001
  filePath: file?.path ?? null,
210138
211002
  depth: folderPath ? 2 : 0,
210139
211003
  type: record8.type,
210140
- size: file?.size ?? null,
211004
+ size: file ? file.size ?? null : wholeSize,
210141
211005
  taskId,
210142
211006
  taskTitle,
210143
- creatorKind: creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null,
211007
+ // 内容形态/引用只挂在**整份产物**那一行;装箱单摊出来的成员行有自己的后缀,
211008
+ // 按后缀走图标即可,挂上 manifest 反而会把 `Dockerfile` 这种无后缀成员画成压缩包。
211009
+ ...file ? {} : { contentKind: revision.contentKind, contentRef: revision.contentRef },
211010
+ creatorKind,
210144
211011
  creatorId,
210145
- creatorName: creatorId ? this.resolveActorName(creatorId) : null,
211012
+ creatorName,
210146
211013
  updatedAt: record8.updatedAt
210147
211014
  });
210148
211015
  }
@@ -210154,6 +211021,7 @@ var init_service4 = __esm({
210154
211021
  if (foldedTasks.has(item.taskId)) continue;
210155
211022
  taskFolders.add(item.taskId);
210156
211023
  const path41 = `wo:${item.taskId}`;
211024
+ const ownerId = workorderOwnerById.get(item.taskId) ?? null;
210157
211025
  items.push({
210158
211026
  kind: "folder",
210159
211027
  id: `folder:${path41}`,
@@ -210165,9 +211033,9 @@ var init_service4 = __esm({
210165
211033
  size: null,
210166
211034
  taskId: item.taskId,
210167
211035
  taskTitle: item.taskTitle,
210168
- creatorKind: null,
210169
- creatorId: null,
210170
- creatorName: null,
211036
+ creatorKind: ownerId ? ownerId.startsWith("actor:human:") ? "human" : "agent" : null,
211037
+ creatorId: ownerId,
211038
+ creatorName: ownerId ? this.resolveActorName(ownerId) : null,
210171
211039
  updatedAt: item.updatedAt
210172
211040
  });
210173
211041
  }
@@ -210193,6 +211061,7 @@ var init_service4 = __esm({
210193
211061
  updatedAt: file.uploadedAt
210194
211062
  });
210195
211063
  }
211064
+ fillFolderSizes(items);
210196
211065
  items.sort((a, b2) => {
210197
211066
  const pathA = a.path ?? "";
210198
211067
  const pathB = b2.path ?? "";
@@ -210236,6 +211105,9 @@ var init_service4 = __esm({
210236
211105
  const work = worksById.get(acceptedId);
210237
211106
  const folderPath = `wo:${workOrderId}/node:${node2.id}`;
210238
211107
  const folderUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211108
+ const creatorId = work?.assigneeActorId ?? null;
211109
+ const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
211110
+ const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
210239
211111
  out.push({
210240
211112
  kind: "folder",
210241
211113
  id: `folder:${folderPath}`,
@@ -210247,14 +211119,11 @@ var init_service4 = __esm({
210247
211119
  size: null,
210248
211120
  taskId: workOrderId,
210249
211121
  taskTitle,
210250
- creatorKind: null,
210251
- creatorId: null,
210252
- creatorName: null,
211122
+ creatorKind,
211123
+ creatorId,
211124
+ creatorName,
210253
211125
  updatedAt: folderUpdatedAt
210254
211126
  });
210255
- const creatorId = work?.assigneeActorId ?? null;
210256
- const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
210257
- const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
210258
211127
  const sortedArts = arts.slice().sort((a, b2) => a.ordinal - b2.ordinal);
210259
211128
  for (const a of sortedArts) {
210260
211129
  const artifactId = node2.id;
@@ -210293,7 +211162,9 @@ var init_service4 = __esm({
210293
211162
  filePath: null,
210294
211163
  depth: 2,
210295
211164
  type: artifactType || "",
210296
- size: null,
211165
+ size: await this.contentByteSize(a.contentRef, engineContentKind(a.contentKind)),
211166
+ contentKind: engineContentKind(a.contentKind),
211167
+ ...a.contentRef ? { contentRef: a.contentRef } : {},
210297
211168
  taskId: workOrderId,
210298
211169
  taskTitle,
210299
211170
  creatorKind,
@@ -210645,7 +211516,6 @@ var init_service4 = __esm({
210645
211516
  const worksById = new Map(snap.works.map((w2) => [w2.id, w2]));
210646
211517
  const binding = this.artifacts.getWorkspaceBinding ? await this.artifacts.getWorkspaceBinding(workOrderId).catch(() => null) : null;
210647
211518
  const projectId2 = binding?.projectId ?? ephemeralProjectIdFor(workOrderId);
210648
- const mapContentKind = (k2) => k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
210649
211519
  const out = [];
210650
211520
  for (const node2 of snap.nodes) {
210651
211521
  if (!node2.latestAcceptId) continue;
@@ -210665,7 +211535,7 @@ var init_service4 = __esm({
210665
211535
  contentRef: a.contentRef,
210666
211536
  title: node2.title || a.name,
210667
211537
  // 节点标题优先;否则用 engine artifact 的 "output" 兜底
210668
- contentKind: mapContentKind(a.contentKind)
211538
+ contentKind: engineContentKind(a.contentKind)
210669
211539
  }));
210670
211540
  out.push({
210671
211541
  outputId: `engine:${acceptedId}`,
@@ -210711,6 +211581,19 @@ var init_service4 = __esm({
210711
211581
  }
210712
211582
  return { ...withKind, size: Buffer.byteLength(content3, "utf8") };
210713
211583
  }
211584
+ /**
211585
+ * 一份产物正文的字节数——**与任务页「相关产物」共用同一条判据**(见 {@link enrichNodeOutputDocument}):
211586
+ * 不看 `contentKind` 名义,只看正文能不能从 BlobStore 读到;读不到就是 null。
211587
+ *
211588
+ * 于是 git 提交号 / Figma version 这类真·外部钉、未纳管、`empty` 都诚实无大小(前端显「—」),
211589
+ * 而 `inline-blob` 的任务书、PRD、ADR 会报出真实字节数——此前 files-view 这条路径**恒发 null**,
211590
+ * 同一份任务书在任务页有 8.0 KB、在项目页却是「—」。
211591
+ */
211592
+ async contentByteSize(contentRef, contentKind) {
211593
+ if (contentKind === "empty" || !contentRef?.trim()) return null;
211594
+ const content3 = await this.readContentRefText(contentRef);
211595
+ return content3 === null ? null : Buffer.byteLength(content3, "utf8");
211596
+ }
210714
211597
  /** 正文读取的唯一入口:只认 BlobStore;没配 / 没纳管 / 读失败一律 null(调用方据此诚实缺 size)。 */
210715
211598
  async readContentRefText(contentRef) {
210716
211599
  if (!this.blobs) return null;
@@ -217718,6 +218601,8 @@ ${input.description}
217718
218601
  }
217719
218602
  await this.opts.store.putVariable({
217720
218603
  key: v2.key,
218604
+ // 展示名(子 PRD《密钥管理》§2)。没传就不传下去——存储层按「不动」处理,不抹既有名字。
218605
+ ...v2.name !== void 0 && v2.name !== "" ? { name: v2.name } : {},
217721
218606
  scope: v2.scope,
217722
218607
  ...v2.actorId !== void 0 ? { actorId: v2.actorId } : {},
217723
218608
  ...v2.projectId !== void 0 ? { projectId: v2.projectId } : {},
@@ -217786,6 +218671,7 @@ ${input.description}
217786
218671
  const isPlain = r.valueEncrypted.startsWith("plain:");
217787
218672
  return {
217788
218673
  key: r.key,
218674
+ ...r.name !== void 0 ? { name: r.name } : {},
217789
218675
  scope: r.scope,
217790
218676
  ...r.actorId !== void 0 ? { actorId: r.actorId } : {},
217791
218677
  ...r.projectId !== void 0 ? { projectId: r.projectId } : {},
@@ -219613,6 +220499,8 @@ function actorsDomain(opts) {
219613
220499
  key: b2.key,
219614
220500
  value: b2.value,
219615
220501
  scope,
220502
+ // 展示名(子 PRD《密钥管理》§2「名称」);不传 = 不改既有名字。
220503
+ ...typeof b2.name === "string" && b2.name.trim() ? { name: b2.name.trim() } : {},
219616
220504
  ...scope === "project" ? { projectId: b2.projectId } : {},
219617
220505
  ...b2.deliveryMode !== void 0 ? { deliveryMode: b2.deliveryMode } : {},
219618
220506
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
@@ -219684,6 +220572,8 @@ function actorsDomain(opts) {
219684
220572
  value: b2.value,
219685
220573
  actorId,
219686
220574
  scope: "personal",
220575
+ // 展示名(同上)。组织页「添加密钥 → 新建」走的就是这条端点。
220576
+ ...typeof b2.name === "string" && b2.name.trim() ? { name: b2.name.trim() } : {},
219687
220577
  ...b2.deliveryMode !== void 0 ? { deliveryMode: b2.deliveryMode } : {},
219688
220578
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
219689
220579
  ...b2.overrides !== void 0 ? { overrides: b2.overrides } : {},
@@ -220740,6 +221630,22 @@ function projectsDomain(opts) {
220740
221630
  throw mapProjectError(err);
220741
221631
  }
220742
221632
  });
221633
+ router.delete("/api/projects/:id", async (req) => {
221634
+ try {
221635
+ const result = await (await svc(req)).deleteProject(req.params.id);
221636
+ return {
221637
+ status: 200,
221638
+ body: {
221639
+ ok: true,
221640
+ project_id: result.projectId,
221641
+ moved_work_order_ids: result.movedWorkOrderIds,
221642
+ carried_git_location_count: result.carriedGitLocationCount
221643
+ }
221644
+ };
221645
+ } catch (err) {
221646
+ throw mapProjectError(err);
221647
+ }
221648
+ });
220743
221649
  router.get("/api/projects/:id/members", async (req) => {
220744
221650
  try {
220745
221651
  const members = await (await svc(req)).listProjectMembers(req.params.id);
@@ -221162,6 +222068,9 @@ function mapProjectError(err) {
221162
222068
  if (err instanceof Error && err.message.startsWith("project not found:")) {
221163
222069
  return new ApiError(404, "NOT_FOUND", err.message);
221164
222070
  }
222071
+ if (err instanceof Error && err.message.startsWith("project not deletable:")) {
222072
+ return new ApiError(400, "PROJECT_NOT_DELETABLE", err.message);
222073
+ }
221165
222074
  if (err instanceof Error && err.message.startsWith("missing artifact revision registration:")) {
221166
222075
  return new ApiError(400, "MISSING_ARTIFACT_REVISION", err.message);
221167
222076
  }
@@ -221469,6 +222378,10 @@ function toApiProjectFilesView(view) {
221469
222378
  file_path: item.filePath ?? null,
221470
222379
  depth: item.depth,
221471
222380
  type: item.type,
222381
+ // 内容形态 + 内容引用:前端名称列给**没有后缀**的行选图标用(正文→文本、git 提交→代码、
222382
+ // 其它外部引用→链接)。交付物整份产物行才有,上传文件与文件夹为 null。
222383
+ content_kind: item.contentKind ?? null,
222384
+ content_ref: item.contentRef ?? null,
221472
222385
  size: item.size,
221473
222386
  task_id: item.taskId,
221474
222387
  task_title: item.taskTitle,
@@ -221524,6 +222437,10 @@ function createProjectsDomain(opts) {
221524
222437
  const handler = opts.onProjectNameChangedFor(companyId);
221525
222438
  if (handler) svc.setOnProjectNameChanged(handler);
221526
222439
  }
222440
+ if (opts.onProjectDeletedFor) {
222441
+ const handler = opts.onProjectDeletedFor(companyId);
222442
+ if (handler) svc.setOnProjectDeleted(handler);
222443
+ }
221527
222444
  return svc;
221528
222445
  };
221529
222446
  const service = build(void 0, void 0);
@@ -222349,8 +223266,8 @@ var init_offboarding = __esm({
222349
223266
  return { ownedAgents, inFlight };
222350
223267
  }
222351
223268
  /**
222352
- * 现在**能不能直接删**这个成员。DELETE 路由先问它:非空即 409 HANDOFF_REQUIRED。
222353
- * 这是「阻断直接移除」的唯一判据,DELETE 与 commit 共用它,不会两处各判一套。
223269
+ * 现在**能不能直接删**这个成员。DELETE commit 共用它,不会两处各判一套。
223270
+ * 交接删掉之后这里只剩两条:不是成员(NOT_FOUND)、公司最后一位 owner(LAST_OWNER)。
222354
223271
  */
222355
223272
  async blockersForRemoval(companyId, accountId) {
222356
223273
  const blockers = [];
@@ -222533,6 +223450,15 @@ function companiesDomain(opts) {
222533
223450
  if (!agentCredentials) throw new ApiError(501, "NOT_IMPLEMENTED", "\u672C\u90E8\u7F72\u672A\u63A5\u5165 agent \u51ED\u8BC1");
222534
223451
  return agentCredentials;
222535
223452
  };
223453
+ const syncActorNameToDisplayName = async (member, by) => {
223454
+ if (!resolveActorsCtx) return;
223455
+ const name = member.displayName?.trim();
223456
+ if (!name) return;
223457
+ const { service: actorsSvc } = await resolveActorsCtx(member.companyId);
223458
+ const existing = await actorsSvc.getActor(member.actorId);
223459
+ if (!existing || existing.name === name) return;
223460
+ await actorsSvc.upsertActor({ ...existing, name }, by);
223461
+ };
222536
223462
  const requireIfMatch = (req) => {
222537
223463
  const raw = req.raw.headers["if-match"];
222538
223464
  const text5 = (typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0)?.replace(/^"|"$/g, "");
@@ -222648,6 +223574,7 @@ function companiesDomain(opts) {
222648
223574
  throw new ApiError(400, "BAD_REQUEST", "displayName \u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
222649
223575
  }
222650
223576
  member = await service.setMemberDisplayName(cid, aid, b2.displayName).catch(mapErr);
223577
+ await syncActorNameToDisplayName(member, req.auth.actor);
222651
223578
  }
222652
223579
  return { status: 200, body: member };
222653
223580
  });
@@ -224606,6 +225533,146 @@ var init_node_store = __esm({
224606
225533
  }
224607
225534
  });
224608
225535
 
225536
+ // ../server/src/domains/collab/review-activity.ts
225537
+ function reviewCardId(targetWorkId, reviewerActorId) {
225538
+ return `${REVIEW_CARD_PREFIX}${encodeURIComponent(targetWorkId)}:${encodeURIComponent(reviewerActorId)}`;
225539
+ }
225540
+ function activityReviews(snap, events) {
225541
+ const rows = /* @__PURE__ */ new Map();
225542
+ for (const r of snap.reviews) rows.set(r.id, { ...r });
225543
+ const authoritative = new Set(snap.reviews.filter((r) => r.status).map((r) => r.id));
225544
+ for (const rec of [...events].sort((a, b2) => a.seq - b2.seq)) {
225545
+ const e = rec.event;
225546
+ if (e.kind === "review.create" && !rows.has(e.reviewId)) {
225547
+ rows.set(e.reviewId, {
225548
+ id: e.reviewId,
225549
+ workorderId: snap.workorder.id,
225550
+ nodeId: e.nodeId,
225551
+ targetWorkId: e.targetWorkId,
225552
+ reviewerActorId: e.reviewerActorId,
225553
+ reviewGroup: e.reviewGroup ?? e.reviewerActorId,
225554
+ createdAt: rec.createdAt,
225555
+ startedAt: null,
225556
+ endedAt: null,
225557
+ cancelledAt: null,
225558
+ verdict: null,
225559
+ note: null,
225560
+ handActorId: null,
225561
+ decidedAt: null,
225562
+ sessionRef: null
225563
+ });
225564
+ }
225565
+ if (e.kind === "review.create" && !authoritative.has(e.reviewId)) {
225566
+ const r = rows.get(e.reviewId);
225567
+ Object.assign(r, {
225568
+ nodeId: e.nodeId,
225569
+ targetWorkId: e.targetWorkId,
225570
+ reviewerActorId: e.reviewerActorId,
225571
+ reviewGroup: e.reviewGroup ?? r.reviewGroup ?? e.reviewerActorId,
225572
+ createdAt: r.createdAt ?? rec.createdAt
225573
+ });
225574
+ }
225575
+ if (!e.kind.startsWith("review.")) continue;
225576
+ const candidates = "reviewId" in e && e.reviewId ? [rows.get(e.reviewId)] : e.kind === "review.response" ? [...rows.values()].filter((r) => r.targetWorkId === e.targetWorkId && r.reviewerActorId === e.reviewerActorId && (!e.reviewGroup || r.reviewGroup === e.reviewGroup)) : [];
225577
+ const matches = e.kind === "review.response" && !e.reviewId ? latestActivityReviews(candidates.filter((r) => !!r && r.createdAt <= rec.createdAt), events.filter((r) => r.seq <= rec.seq)) : candidates;
225578
+ for (const r of matches) {
225579
+ if (!r || authoritative.has(r.id)) continue;
225580
+ if (e.kind === "review.started") r.startedAt = rec.createdAt;
225581
+ if (e.kind === "review.timeout") {
225582
+ r.endedAt = rec.createdAt;
225583
+ }
225584
+ if (e.kind === "review.kill") r.cancelledAt = rec.createdAt;
225585
+ if (e.kind === "review.response") {
225586
+ r.verdict = e.verdict;
225587
+ r.note = e.note ?? null;
225588
+ r.decidedAt = rec.createdAt;
225589
+ r.endedAt = rec.createdAt;
225590
+ r.handActorId = e.handActorId ?? null;
225591
+ }
225592
+ }
225593
+ }
225594
+ const createdSeq = new Map(events.flatMap((r) => r.event.kind === "review.create" ? [[r.event.reviewId, r.seq]] : []));
225595
+ return [...rows.values()].filter((r) => r.nodeId && r.targetWorkId && r.reviewerActorId && r.createdAt).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || (createdSeq.get(a.id) ?? 0) - (createdSeq.get(b2.id) ?? 0) || a.id.localeCompare(b2.id));
225596
+ }
225597
+ function reviewCardTarget(cardId, reviews) {
225598
+ if (cardId.startsWith(REVIEW_CARD_PREFIX)) {
225599
+ const encoded = cardId.slice(REVIEW_CARD_PREFIX.length);
225600
+ const separator = encoded.indexOf(":");
225601
+ if (separator < 1 || separator === encoded.length - 1) return void 0;
225602
+ try {
225603
+ return {
225604
+ targetWorkId: decodeURIComponent(encoded.slice(0, separator)),
225605
+ reviewerActorId: decodeURIComponent(encoded.slice(separator + 1))
225606
+ };
225607
+ } catch {
225608
+ return void 0;
225609
+ }
225610
+ }
225611
+ if (cardId.startsWith("review:")) {
225612
+ const review = reviews.find((r) => r.id === cardId.slice(7));
225613
+ if (review) return { targetWorkId: review.targetWorkId, reviewerActorId: review.reviewerActorId };
225614
+ }
225615
+ return void 0;
225616
+ }
225617
+ function latestActivityReviews(reviews, events) {
225618
+ const createSeq = new Map(events.flatMap((r) => r.event.kind === "review.create" ? [[r.event.reviewId, r.seq]] : []));
225619
+ const rows = [...reviews].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || (createSeq.get(a.id) ?? 0) - (createSeq.get(b2.id) ?? 0) || a.id.localeCompare(b2.id));
225620
+ const latest = /* @__PURE__ */ new Map();
225621
+ for (const r of rows) latest.set(JSON.stringify([r.reviewGroup, r.reviewerActorId]), r);
225622
+ return [...latest.values()];
225623
+ }
225624
+ function reviewLabel(r) {
225625
+ const state = reviewState(r);
225626
+ if (state === "accept") return "\u5BA1\u6838\u901A\u8FC7";
225627
+ if (state === "reject") return "\u5BA1\u6838\u4E0D\u901A\u8FC7";
225628
+ if (state === "dead") return "\u5BA1\u6838\u5DF2\u505C\u6B62";
225629
+ if (state === "failed") return "\u5BA1\u6838\u8FD0\u884C\u5931\u8D25";
225630
+ if (state === "retry") return "\u6B63\u5728\u91CD\u8BD5\u5BA1\u6838";
225631
+ return r.reviewerActorId.startsWith("actor:human:") ? "\u5F85\u5BA1\u6838" : "\u6B63\u5728\u5BA1\u6838";
225632
+ }
225633
+ function reviewSummary(snap, reviews, events) {
225634
+ const first = reviews[0];
225635
+ const work = snap.works.find((w2) => w2.id === first.targetWorkId);
225636
+ const node2 = snap.nodes.find((n) => n.id === first.nodeId);
225637
+ const name = node2?.title ?? first.nodeId;
225638
+ const latest = latestActivityReviews(reviews, events);
225639
+ const requirements = snap.requirements.filter((r) => r.nodeId === first.nodeId && r.reviewerActorId === first.reviewerActorId);
225640
+ const judgement = judgeWork({ requirements: requirements.length ? requirements : reviews, reviews });
225641
+ const pending = latest.filter((r) => !r.cancelledAt && !r.verdict && reviewState(r) === "running");
225642
+ if (work?.acceptanceState === "accepted" || work?.acceptedAt || work?.acceptanceState !== "rejected" && judgement === "passed") {
225643
+ return { phase: "done", status: `\u300A${name}\u300B\u5BA1\u6838\u901A\u8FC7\u3002`, latest, pending: [] };
225644
+ }
225645
+ if (work?.acceptanceState === "rejected" || judgement === "rejected") {
225646
+ return { phase: "done", status: "\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002", latest, pending: [] };
225647
+ }
225648
+ const endedWork = !!(work?.cancelledAt || work?.deadAt);
225649
+ if (endedWork && latest.every((r) => !!r.cancelledAt && reviewState(r) !== "retry")) {
225650
+ return { phase: "done", status: `\u300A${name}\u300B\u7684\u672C\u6B21\u5BA1\u6838\u5DF2\u53D6\u6D88\u3002`, latest, pending: [] };
225651
+ }
225652
+ const interrupted = latest.some((r) => ["failed", "dead"].includes(reviewState(r)));
225653
+ if (interrupted) return { phase: "stuck", status: latest.length === 1 ? reviewState(latest[0]) === "dead" ? events.some((r) => r.event.kind === "review.kill" && r.event.reviewId === latest[0].id) ? `\u300A${name}\u300B\u7684\u8BC4\u5BA1\u8FDE\u7EED\u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u3002` : `\u300A${name}\u300B\u7684\u5BA1\u6838\u5DF2\u505C\u6B62\u3002` : `\u300A${name}\u300B\u7684\u5BA1\u6838\u672A\u5728\u65F6\u9650\u5185\u5B8C\u6210\u3002` : `\u300A${name}\u300B\u7684\u90E8\u5206\u5BA1\u6838\u6267\u884C\u4E2D\u65AD\uFF0C\u5C1A\u672A\u5B8C\u6210\u5168\u90E8\u5BA1\u6838\u3002`, latest, pending };
225654
+ if (latest.some((r) => r.reviewerActorId.startsWith("actor:agent:") && ["running", "retry"].includes(reviewState(r)))) {
225655
+ return { phase: "running", status: `\u6B63\u5728\u5BA1\u6838 \u300A${name}\u300B\u3002`, latest, pending };
225656
+ }
225657
+ return { phase: "not_started", status: `\u8BF7\u786E\u8BA4\u300A${name}\u300B\u662F\u5426\u5BA1\u6838\u901A\u8FC7\u3002`, latest, pending };
225658
+ }
225659
+ var REVIEW_CARD_PREFIX, REVIEW_TRACE_EVENT_KINDS;
225660
+ var init_review_activity = __esm({
225661
+ "../server/src/domains/collab/review-activity.ts"() {
225662
+ "use strict";
225663
+ init_src4();
225664
+ REVIEW_CARD_PREFIX = "review-work:";
225665
+ REVIEW_TRACE_EVENT_KINDS = [
225666
+ "review.create",
225667
+ "review.started",
225668
+ "review.response",
225669
+ "review.timeout",
225670
+ "review.kill",
225671
+ "plan.node_retry"
225672
+ ];
225673
+ }
225674
+ });
225675
+
224609
225676
  // ../server/src/domains/collab/workorder-manager.ts
224610
225677
  function resolveWorkorderManager(snap) {
224611
225678
  const nodes = snap.nodes;
@@ -224644,8 +225711,29 @@ function buildWorkorderActivity(input) {
224644
225711
  if (!snap) return { workorderId, cards: [], truncated: false };
224645
225712
  const nodeById = new Map(snap.nodes.map((n) => [n.id, n]));
224646
225713
  const workById = new Map(snap.works.map((w2) => [w2.id, w2]));
224647
- const reviewById = new Map(snap.reviews.map((r) => [r.id, r]));
224648
225714
  const issueById = new Map(snap.issues.map((i) => [i.id, i]));
225715
+ const activeWorks = /* @__PURE__ */ new Set();
225716
+ const gapOrigins = /* @__PURE__ */ new Map();
225717
+ const knownStarts = new Set(events.flatMap((r) => r.event.kind === "work.create" || r.event.kind === "work.started" ? [r.event.workId] : []));
225718
+ for (const rec of events) {
225719
+ const event = rec.event;
225720
+ if (event.kind === "work.create" || event.kind === "work.started") activeWorks.add(event.workId);
225721
+ if (event.kind === "work.response" || event.kind === "work.timeout" || event.kind === "work.kill") activeWorks.delete(event.workId);
225722
+ if (event.kind !== "issue.create" || event.issueKind !== "gap") continue;
225723
+ const nodeId = event.raisedByNodeId ?? event.aboutNodeId;
225724
+ const matches = (id) => {
225725
+ const work = workById.get(id);
225726
+ return work?.nodeId === nodeId && work.assigneeActorId === event.authorActorId;
225727
+ };
225728
+ if (event.aboutWorkId && matches(event.aboutWorkId)) {
225729
+ gapOrigins.set(event.issueId, [event.aboutWorkId]);
225730
+ } else if (!event.aboutWorkId) {
225731
+ const candidates = [...activeWorks].filter(matches);
225732
+ const at = Date.parse(rec.createdAt);
225733
+ const unobserved = snap.works.some((work) => !knownStarts.has(work.id) && matches(work.id) && Date.parse(work.createdAt) <= at && (!work.endedAt || Date.parse(work.endedAt) >= at) && (!work.deadAt || Date.parse(work.deadAt) >= at) && (!work.cancelledAt || Date.parse(work.cancelledAt) >= at));
225734
+ if (candidates.length === 1 && !unobserved) gapOrigins.set(event.issueId, candidates);
225735
+ }
225736
+ }
224649
225737
  const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
224650
225738
  const rootBriefNodeIds = new Set(
224651
225739
  snap.nodes.filter((n) => n.type === "brief" && !nodesWithInEdges.has(n.id)).map((n) => n.id)
@@ -224696,8 +225784,9 @@ function buildWorkorderActivity(input) {
224696
225784
  const groupIndexOf = /* @__PURE__ */ new Map();
224697
225785
  const cardOfWork = /* @__PURE__ */ new Map();
224698
225786
  const groupKey = (nodeId) => `${nodeId}#${groupIndexOf.get(nodeId) ?? 0}`;
224699
- const reviewCards = /* @__PURE__ */ new Map();
224700
225787
  const issueCards = /* @__PURE__ */ new Map();
225788
+ const workOfIssue = /* @__PURE__ */ new Map();
225789
+ const plainCommentIssueIds = /* @__PURE__ */ new Set();
224701
225790
  const acceptCards = /* @__PURE__ */ new Map();
224702
225791
  const standalone = [];
224703
225792
  const touch = (d, rec) => {
@@ -224755,10 +225844,22 @@ function buildWorkorderActivity(input) {
224755
225844
  const executorId = w2?.assigneeActorId ?? rec.actorId;
224756
225845
  if (isPendingActor(executorId)) break;
224757
225846
  const status = reasonKinds.has("rework") ? ACTIVITY_COPY.reworking(nodeName(nodeId)) : reasonKinds.has("resumed") ? ACTIVITY_COPY.resumed(nameOf(executorId), nodeName(nodeId)) : reasonKinds.has("spec_changed") ? ACTIVITY_COPY.specChanged(nodeName(nodeId)) : reasonKinds.has("issue") ? ACTIVITY_COPY.issueDriven(nodeName(nodeId)) : ACTIVITY_COPY.working(nodeName(nodeId));
225847
+ const drivingIssueId = str4(reasons.find((r) => r.kind === "issue")?.issueId);
225848
+ if (drivingIssueId) {
225849
+ workOfIssue.set(drivingIssueId, workId);
225850
+ const ic = plainCommentIssueIds.has(drivingIssueId) ? issueCards.get(drivingIssueId) : void 0;
225851
+ const commentAuthor = issueById.get(drivingIssueId)?.authorActorId;
225852
+ if (ic && commentAuthor) {
225853
+ touch(ic, rec);
225854
+ ic.executorId = executorId;
225855
+ ic.status = ACTIVITY_COPY.commentHandling(nameOf(executorId), nameOf(commentAuthor));
225856
+ }
225857
+ }
224758
225858
  const reworkReason = str4(reasons.find((r) => r.kind === "rework")?.reason);
224759
225859
  const key = groupKey(nodeId);
224760
225860
  const existingGroup = groupCards.get(key);
224761
225861
  if (existingGroup) {
225862
+ if (!existingGroup.traceWorkIds.includes(workId)) existingGroup.traceWorkIds.push(workId);
224762
225863
  touch(existingGroup, rec);
224763
225864
  existingGroup.executorId = executorId;
224764
225865
  existingGroup.phase = "running";
@@ -224771,6 +225872,7 @@ function buildWorkorderActivity(input) {
224771
225872
  }
224772
225873
  const fresh = {
224773
225874
  id: `work:${workId}`,
225875
+ traceWorkIds: [workId],
224774
225876
  seq: rec.seq,
224775
225877
  at: rec.createdAt,
224776
225878
  updatedAt: rec.createdAt,
@@ -224813,8 +225915,9 @@ function buildWorkorderActivity(input) {
224813
225915
  }
224814
225916
  break;
224815
225917
  }
225918
+ const engineVerdict = workById.get(workId)?.status;
224816
225919
  touch(d, rec);
224817
- if (ev.outcome === "failed") {
225920
+ if (ev.outcome === "failed" && engineVerdict !== "success") {
224818
225921
  d.phase = "stuck";
224819
225922
  d.stuckReason = "failed";
224820
225923
  d.status = ACTIVITY_COPY.failed(nameOf(d.executorId));
@@ -224902,72 +226005,6 @@ function buildWorkorderActivity(input) {
224902
226005
  }
224903
226006
  break;
224904
226007
  }
224905
- /* ── 审核(review)──────────────────────────────────────────── */
224906
- case "review.create": {
224907
- const reviewId = String(ev.reviewId ?? "");
224908
- const nodeId = String(ev.nodeId ?? "");
224909
- const targetWorkId = String(ev.targetWorkId ?? "");
224910
- const reviewerId = str4(ev.reviewerActorId) ?? rec.actorId;
224911
- const isAgent2 = ev.reviewerIsAgent === true;
224912
- const name = nodeName(nodeId);
224913
- reviewCards.set(reviewId, {
224914
- id: `review:${reviewId}`,
224915
- seq: rec.seq,
224916
- at: rec.createdAt,
224917
- updatedAt: rec.createdAt,
224918
- nodeId,
224919
- executorId: reviewerId,
224920
- ...rec.handActorId ? { handActorId: rec.handActorId } : {},
224921
- // agent 评审是「机器在跑」= 进行中;人审是待办 = 待开始(人没有「开始看了」这个事件)。
224922
- phase: isAgent2 ? "running" : "not_started",
224923
- status: isAgent2 ? ACTIVITY_COPY.reviewing(name) : ACTIVITY_COPY.reviewPending(name),
224924
- /* 挂上被审那一次交付的**全部产物**(发起人 2026-09-08 定):要人点「通过/不通过」,
224925
- 就得让他在同一张卡上看到到底在审什么。此前这里恒空,产物只挂在验收卡上——
224926
- 而验收卡与审核卡是同一件事的两张(见下方 `acceptCards` 那段的跳过判据)。 */
224927
- artifacts: artifactsByWork.get(targetWorkId) ?? [],
224928
- // 人审才出底栏按钮;agent 审无按钮(原型同)。
224929
- actions: isAgent2 ? [] : [
224930
- { kind: "review", label: BUTTON.approve, target: nodeId, verdict: "approve" },
224931
- { kind: "review", label: BUTTON.reject, target: nodeId, verdict: "request_changes" }
224932
- ]
224933
- });
224934
- break;
224935
- }
224936
- case "review.started": {
224937
- const d = reviewCards.get(String(ev.reviewId ?? ""));
224938
- if (d) touch(d, rec);
224939
- break;
224940
- }
224941
- case "review.response": {
224942
- const reviewId = str4(ev.reviewId);
224943
- const targetWorkId = String(ev.targetWorkId ?? "");
224944
- const d = reviewId ? reviewCards.get(reviewId) : [...reviewCards.values()].find((c) => reviewById.get(c.id.slice("review:".length))?.targetWorkId === targetWorkId);
224945
- if (!d) break;
224946
- touch(d, rec);
224947
- d.phase = "done";
224948
- d.executorId = str4(ev.reviewerActorId) ?? d.executorId;
224949
- d.actions = [];
224950
- const nodeId = d.nodeId;
224951
- const name = nodeName(nodeId);
224952
- if (ev.verdict === "approve") {
224953
- d.status = ACTIVITY_COPY.reviewApproved(name);
224954
- } else {
224955
- d.status = ACTIVITY_COPY.reviewRejected;
224956
- const note = str4(ev.note);
224957
- if (note) d.detail = ACTIVITY_COPY.reviewRejectedDetail(note);
224958
- }
224959
- break;
224960
- }
224961
- case "review.timeout":
224962
- case "review.kill": {
224963
- const d = reviewCards.get(String(ev.reviewId ?? ""));
224964
- if (!d) break;
224965
- touch(d, rec);
224966
- d.phase = "stuck";
224967
- d.status = ev.kind === "review.kill" ? ACTIVITY_COPY.reviewKilled(nodeName(d.nodeId)) : ACTIVITY_COPY.reviewTimedOut(nodeName(d.nodeId));
224968
- d.actions = [];
224969
- break;
224970
- }
224971
226008
  /* ── 沟通与异常(issue)────────────────────────────────────── */
224972
226009
  case "issue.create": {
224973
226010
  const issueId = String(ev.issueId ?? "");
@@ -225046,6 +226083,7 @@ function buildWorkorderActivity(input) {
225046
226083
  });
225047
226084
  break;
225048
226085
  }
226086
+ if (!isGap && !isEscalation && !isBackpressure) plainCommentIssueIds.add(issueId);
225049
226087
  const nodeAssignee = nodeId ? nodeById.get(nodeId)?.assigneeActorId : void 0;
225050
226088
  const commentCounterpart = nodeAssignee ?? ownerId;
225051
226089
  const commentTargetId = commentCounterpart && commentCounterpart !== authorId ? commentCounterpart : void 0;
@@ -225110,31 +226148,24 @@ function buildWorkorderActivity(input) {
225110
226148
  if (issue2?.kind === "escalation") {
225111
226149
  const linkedGap = str4(issue2.gapId);
225112
226150
  const stuckActor = (linkedGap ? issueById.get(linkedGap)?.authorActorId : void 0) ?? issue2.authorActorId;
226151
+ touch(d, rec);
226152
+ d.phase = "done";
226153
+ d.executorId = resolverId;
226154
+ d.handActorId = rec.handActorId ?? void 0;
226155
+ d.status = ACTIVITY_COPY.handledEscalation(nameOf(stuckActor));
225113
226156
  d.actions = [];
225114
226157
  const raisedCard = issueCards.get(`${issueId}:raised`);
225115
226158
  if (raisedCard) raisedCard.actions = [];
225116
- standalone.push({
225117
- id: `issue:${issueId}:resolved`,
225118
- seq: rec.seq,
225119
- at: rec.createdAt,
225120
- updatedAt: rec.createdAt,
225121
- ...d.nodeId ? { nodeId: d.nodeId } : {},
225122
- executorId: resolverId,
225123
- ...rec.handActorId ? { handActorId: rec.handActorId } : {},
225124
- phase: "done",
225125
- /* 收口正文不上卡(与开口那一支同一条口径):`ws:wo-635d7add` 的收口 note 是一整段
225126
- 处置说明,铺回展开区又是一屏字。 */
225127
- status: ACTIVITY_COPY.handledEscalation(nameOf(stuckActor)),
225128
- artifacts: [],
225129
- actions: []
225130
- });
225131
226159
  break;
225132
226160
  }
225133
226161
  touch(d, rec);
225134
226162
  d.phase = "done";
225135
226163
  d.actions = [];
225136
226164
  d.executorId = resolverId;
225137
- d.status = ACTIVITY_COPY.commentResolved(nameOf(resolverId), nodeName(d.nodeId));
226165
+ const drivenWorkId = workOfIssue.get(issueId);
226166
+ const producedNewVersion = !!(drivenWorkId && workById.get(drivenWorkId)?.conclusion);
226167
+ const commentAuthorId = issue2?.authorActorId;
226168
+ d.status = producedNewVersion && commentAuthorId ? ACTIVITY_COPY.commentAddressed(nameOf(resolverId), nameOf(commentAuthorId), nodeName(d.nodeId)) : ACTIVITY_COPY.commentResolved(nameOf(resolverId), nodeName(d.nodeId));
225138
226169
  break;
225139
226170
  }
225140
226171
  case "plan.node_retry": {
@@ -225151,9 +226182,7 @@ function buildWorkorderActivity(input) {
225151
226182
  break;
225152
226183
  }
225153
226184
  }
225154
- const reviewedWorkIds = new Set(
225155
- [...reviewCards.keys()].map((rid) => reviewById.get(rid)?.targetWorkId).filter(Boolean)
225156
- );
226185
+ const reviewedWorkIds = new Set([...snap.reviews, ...activityReviews(snap, events)].map((r) => r.targetWorkId));
225157
226186
  for (const w2 of snap.works) {
225158
226187
  if (acceptCards.has(w2.id)) continue;
225159
226188
  if (reviewedWorkIds.has(w2.id)) continue;
@@ -225177,23 +226206,104 @@ function buildWorkorderActivity(input) {
225177
226206
  status: ACTIVITY_COPY.acceptPending,
225178
226207
  artifacts: artifactsByWork.get(w2.id) ?? [],
225179
226208
  actions: [
225180
- { kind: "accept", label: BUTTON.acceptPass, target: node2.id, verdict: "approve" },
225181
- { kind: "accept", label: BUTTON.acceptFail, target: node2.id, verdict: "request_changes" }
226209
+ { kind: "accept", label: BUTTON.acceptPass, target: node2.id, revisionId: w2.id, verdict: "approve" },
226210
+ { kind: "accept", label: BUTTON.acceptFail, target: node2.id, revisionId: w2.id, verdict: "request_changes" }
225182
226211
  ]
225183
226212
  });
225184
226213
  }
226214
+ const visibleReviews = /* @__PURE__ */ new Map();
226215
+ const allReviews = activityReviews(snap, events);
226216
+ const reviewWorks = /* @__PURE__ */ new Map();
226217
+ const reviewCards = /* @__PURE__ */ new Map();
226218
+ for (const r of allReviews) {
226219
+ const rows = reviewWorks.get(r.targetWorkId) ?? [];
226220
+ rows.push(r);
226221
+ reviewWorks.set(r.targetWorkId, rows);
226222
+ const cardId = reviewCardId(r.targetWorkId, r.reviewerActorId);
226223
+ const group = reviewCards.get(cardId) ?? { workId: r.targetWorkId, reviewerId: r.reviewerActorId, reviews: [] };
226224
+ group.reviews.push(r);
226225
+ reviewCards.set(cardId, group);
226226
+ }
226227
+ for (const [cardId, { workId, reviewerId, reviews }] of reviewCards) {
226228
+ const summary = reviewSummary(snap, reviews, events);
226229
+ const first = reviews[0];
226230
+ const ids2 = new Set(reviews.map((r) => r.id));
226231
+ const created = events.filter((r) => r.event.kind === "review.create" && ids2.has(r.event.reviewId));
226232
+ const dates = summary.latest.flatMap((r) => [r.createdAt, r.startedAt, r.endedAt, r.decidedAt, r.cancelledAt, r.retryAt].filter((v2) => !!v2));
226233
+ const pending = summary.pending.filter((r) => r.reviewerActorId.startsWith("actor:human:") && (!input.viewerActorId || r.reviewerActorId === input.viewerActorId));
226234
+ visibleReviews.set(cardId, {
226235
+ id: cardId,
226236
+ reviewWorkId: workId,
226237
+ reviewerId,
226238
+ reviewerIds: [reviewerId],
226239
+ traceAvailable: true,
226240
+ seq: created.length ? Math.min(...created.map((r) => r.seq)) : 0,
226241
+ at: first.createdAt,
226242
+ updatedAt: dates.sort().at(-1) ?? first.createdAt,
226243
+ nodeId: first.nodeId,
226244
+ executorId: reviewerId,
226245
+ phase: summary.phase,
226246
+ status: summary.status,
226247
+ detail: summary.latest.length === 1 ? summary.latest[0].note ? `${summary.latest[0].verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1"}\uFF1A${summary.latest[0].note}` : void 0 : summary.latest.map((r) => `${nameOf(r.reviewerActorId)}\uFF08\u8BC4\u5BA1\u7EC4 ${[...new Set(summary.latest.map((r2) => r2.reviewGroup))].indexOf(r.reviewGroup) + 1}\uFF09\uFF1A${reviewLabel(r)}${r.note ? `
226248
+ ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1"}\uFF1A${r.note}` : ""}`).join("\n\n"),
226249
+ artifacts: artifactsByWork.get(workId) ?? [],
226250
+ actions: pending.length ? [
226251
+ { kind: "review", label: BUTTON.approve, target: first.nodeId, revisionId: workId, verdict: "approve" },
226252
+ { kind: "review", label: BUTTON.reject, target: first.nodeId, revisionId: workId, verdict: "request_changes" }
226253
+ ] : []
226254
+ });
226255
+ }
225185
226256
  const drafts = [
225186
226257
  ...standalone,
225187
226258
  ...groupCards.values(),
225188
- ...reviewCards.values(),
226259
+ ...visibleReviews.values(),
225189
226260
  ...issueCards.values(),
225190
226261
  ...acceptCards.values()
225191
226262
  ].sort((a, b2) => a.updatedAt.localeCompare(b2.updatedAt) || a.seq - b2.seq || a.id.localeCompare(b2.id));
225192
- const runIdByTarget = input.runIdByTarget;
225193
- const runIdOfCard = (cardId) => {
225194
- if (!runIdByTarget) return void 0;
225195
- if (cardId.startsWith("work:")) return runIdByTarget.get(`work:${cardId.slice("work:".length)}`);
225196
- if (cardId.startsWith("review:")) return runIdByTarget.get(`review:${cardId.slice("review:".length)}`);
226263
+ const issueWorks = /* @__PURE__ */ new Map();
226264
+ const reasonIds = /* @__PURE__ */ new Map();
226265
+ for (const rec of input.events) {
226266
+ const ev = rec.event;
226267
+ if (ev.kind === "work.create") reasonIds.set(
226268
+ ev.workId,
226269
+ ev.reasons.filter((r) => r.kind === "issue").map((r) => r.issueId)
226270
+ );
226271
+ }
226272
+ for (const w2 of [...snap?.works ?? []].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id))) {
226273
+ const replyIssueId = replyIssueIdOf(w2);
226274
+ for (const id of new Set(w2.frozenIssueIds ?? reasonIds.get(w2.id) ?? (replyIssueId ? [replyIssueId] : []))) {
226275
+ const issue2 = issueById.get(id);
226276
+ if (issue2?.kind !== "comment" && issue2?.kind !== "change_request") continue;
226277
+ const ids2 = issueWorks.get(id) ?? [];
226278
+ ids2.push(w2.id);
226279
+ issueWorks.set(id, ids2);
226280
+ }
226281
+ }
226282
+ for (const d of drafts) {
226283
+ if (!d.id.startsWith("issue:")) continue;
226284
+ const issueId = d.id.slice("issue:".length);
226285
+ if (issueById.get(issueId)?.kind === "gap") {
226286
+ d.traceAvailable = true;
226287
+ d.traceWorkIds = gapOrigins.get(issueId);
226288
+ } else {
226289
+ d.traceWorkIds = issueWorks.get(issueId);
226290
+ }
226291
+ }
226292
+ const runIdOfCard = (d) => {
226293
+ const map = input.runIdByTarget;
226294
+ if (d.traceWorkIds?.length) {
226295
+ for (const id of [...d.traceWorkIds].reverse()) {
226296
+ const run = map?.get(`work:${id}`);
226297
+ if (run) return run;
226298
+ }
226299
+ }
226300
+ if (d.reviewWorkId) {
226301
+ for (const r of [...reviewWorks.get(d.reviewWorkId) ?? []].reverse()) {
226302
+ if (d.reviewerId && r.reviewerActorId !== d.reviewerId) continue;
226303
+ const run = map?.get(`review:${r.id}`);
226304
+ if (run) return run;
226305
+ }
226306
+ }
225197
226307
  return void 0;
225198
226308
  };
225199
226309
  const cards = drafts.map((d) => ({
@@ -225203,13 +226313,16 @@ function buildWorkorderActivity(input) {
225203
226313
  updatedAt: d.updatedAt,
225204
226314
  ...d.nodeId ? { nodeId: d.nodeId, nodeTitle: nodeName(d.nodeId) } : {},
225205
226315
  executor: ref2(d.executorId),
226316
+ ...d.reviewWorkId ? { reviewWorkId: d.reviewWorkId, reviewers: d.reviewerIds?.map(ref2) } : {},
225206
226317
  ...d.handActorId ? { handActor: ref2(d.handActorId) } : {},
225207
226318
  phase: d.phase,
225208
226319
  status: d.status,
225209
226320
  ...d.detail ? { detail: d.detail } : {},
225210
226321
  artifacts: d.artifacts,
225211
226322
  actions: d.actions,
225212
- .../* @__PURE__ */ ((r) => r ? { runId: r } : {})(runIdOfCard(d.id))
226323
+ ...d.traceAvailable ? { traceAvailable: true } : {},
226324
+ ...d.traceWorkIds?.length ? { traceWorkIds: d.traceWorkIds, traceAvailable: true } : {},
226325
+ .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(runIdOfCard(d))
225213
226326
  }));
225214
226327
  return { workorderId, cards, truncated };
225215
226328
  }
@@ -225218,6 +226331,8 @@ var init_activity2 = __esm({
225218
226331
  "../server/src/domains/collab/activity.ts"() {
225219
226332
  "use strict";
225220
226333
  init_src();
226334
+ init_src4();
226335
+ init_review_activity();
225221
226336
  init_workorder_manager();
225222
226337
  ACTIVITY_EVENT_KINDS = [
225223
226338
  "plan.changed",
@@ -225347,6 +226462,14 @@ var init_activity2 = __esm({
225347
226462
  * 留言 / 回压收口——**同样一句索引**。收口 note 与开口正文同源、同样能长到几千字:
225348
226463
  * 全库实测 `ws:wo-56bba3c1` 那条收口后小状态仍是 **8096 字**。全文去留言线看。
225349
226464
  */
226465
+ /** 有人接手这条留言、开工了——由 `work.create` 的 `reasons` 引用该 issueId 判定。 */
226466
+ commentHandling: (executor, author) => `${executor} \u6B63\u5728\u5904\u7406\u6765\u81EA ${author} \u7684\u7559\u8A00\u3002`,
226467
+ /**
226468
+ * 处理完**并且真出了新一版**。
226469
+ * ⚠ 只在那一轮真有 conclusion 时才说「更新了一版」——resolve 也可能是「看了但不改」,
226470
+ * 那种情况说出了新版就是一句卡面兑现不了的断言(同「未经 X 确认」那一族)。
226471
+ */
226472
+ commentAddressed: (executor, author, node2) => `${executor} \u5DF2\u57FA\u4E8E ${author} \u7684\u7559\u8A00\u66F4\u65B0\u4E86\u4E00\u7248\u300A${node2}\u300B\u3002`,
225350
226473
  commentResolved: (by, node2) => `${by} \u5DF2\u5904\u7406\u300A${node2}\u300B\u7684\u7559\u8A00\u3002`,
225351
226474
  issueResolved: (note) => note
225352
226475
  };
@@ -225370,6 +226493,135 @@ function targetOf(cardId) {
225370
226493
  return null;
225371
226494
  }
225372
226495
  function buildWorkorderActivityTrace(input) {
226496
+ const { workorderId, cardId, snap } = input;
226497
+ const ref2 = input.ref ?? ((id) => ({ id }));
226498
+ const empty2 = { workorderId, cardId, attempts: [] };
226499
+ if (!snap) return empty2;
226500
+ const reviews = activityReviews(snap, input.events ?? []);
226501
+ const reviewTarget = reviewCardTarget(cardId, reviews);
226502
+ if (reviewTarget) return buildReviewHistory(input, reviewTarget.targetWorkId, reviewTarget.reviewerActorId, reviews);
226503
+ const card2 = input.events ? buildWorkorderActivity({
226504
+ workorderId,
226505
+ snap,
226506
+ events: input.events,
226507
+ ref: ref2
226508
+ }).cards.find((c) => c.id === cardId) : void 0;
226509
+ const gap = cardId.startsWith("issue:") ? snap.issues.find((i) => i.id === cardId.slice(6) && i.kind === "gap") : void 0;
226510
+ if (gap && !card2?.traceWorkIds?.length) return {
226511
+ ...empty2,
226512
+ works: [],
226513
+ notice: "\u7F3A\u53E3\u8BB0\u5F55\u6CA1\u6709\u660E\u786E\u5173\u8054\u5230\u67D0\u6B21\u6267\u884C\uFF0C\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u8FD0\u884C\u8F68\u8FF9\u3002"
226514
+ };
226515
+ const workIds = card2?.traceWorkIds ?? (cardId.startsWith("work:") ? [cardId.slice(5)] : []);
226516
+ if (workIds.length > 0) {
226517
+ let firstBriefing;
226518
+ const works = [];
226519
+ for (const workId of workIds) {
226520
+ const work = snap.works.find((w2) => w2.id === workId);
226521
+ if (!work) continue;
226522
+ const single = buildSingleTrace({ ...input, cardId: `work:${workId}` });
226523
+ const briefing = single.briefing;
226524
+ if (!briefing) continue;
226525
+ firstBriefing ??= briefing;
226526
+ const sources = sourcesOfWork(work, input, briefing);
226527
+ const artifacts = single.attempts.at(-1)?.artifacts ?? artifactsOfWork(work, input);
226528
+ works.push({
226529
+ workId,
226530
+ at: work.createdAt,
226531
+ nodeTitle: briefing.nodeTitle,
226532
+ ...briefing.spec ? { spec: briefing.spec } : {},
226533
+ sources,
226534
+ attempts: single.attempts,
226535
+ ...work.conclusion?.trim() ? { conclusion: work.conclusion.trim() } : {},
226536
+ artifacts,
226537
+ ...single.attempts.length ? {} : { emptyReason: input.dispatches === void 0 ? "records_unavailable" : work.startedAt || work.endedAt || work.deadAt || work.cancelledAt || work.conclusion || work.status && work.status !== "running" ? "records_missing" : "not_dispatched" }
226538
+ });
226539
+ }
226540
+ if (works.length) {
226541
+ return {
226542
+ workorderId,
226543
+ cardId,
226544
+ briefing: firstBriefing,
226545
+ works,
226546
+ ...gap && !gap.aboutWorkId ? { notice: "\u6839\u636E\u7F3A\u53E3\u63D0\u51FA\u65F6\u7684\u8BB0\u5F55\uFF0C\u5173\u8054\u5230\u8BE5\u8282\u70B9\u3001\u8BE5\u6267\u884C\u4EBA\u5F53\u65F6\u552F\u4E00\u7684\u6267\u884C\u3002" } : {},
226547
+ attempts: works.flatMap((w2) => w2.attempts).sort((a, b2) => a.at.localeCompare(b2.at) || a.runId.localeCompare(b2.runId))
226548
+ };
226549
+ }
226550
+ }
226551
+ return buildSingleTrace(input);
226552
+ }
226553
+ function sourcesOfWork(work, input, briefing) {
226554
+ const snap = input.snap;
226555
+ const ref2 = input.ref ?? ((id) => ({ id }));
226556
+ const created = input.events?.find((r) => r.event.kind === "work.create" && r.event.workId === work.id);
226557
+ const event = created?.event;
226558
+ const replyIssueId = replyIssueIdOf(work);
226559
+ const ids2 = work.frozenIssueIds ?? (event?.kind === "work.create" ? event.reasons.filter((r) => r.kind === "issue").map((r) => r.issueId) : replyIssueId ? [replyIssueId] : void 0);
226560
+ if (!ids2) return [{ kind: "unavailable", at: work.createdAt }];
226561
+ const sources = [];
226562
+ for (const issueId of new Set(ids2)) {
226563
+ const issue2 = snap.issues.find((i) => i.id === issueId);
226564
+ if (!issue2 || issue2.createdAt > work.createdAt) {
226565
+ sources.push({ kind: "unavailable", at: work.createdAt, issueId });
226566
+ continue;
226567
+ }
226568
+ if (issue2.kind !== "comment" && issue2.kind !== "change_request") continue;
226569
+ const replySeq = (id) => input.events?.find((e) => e.event.kind === "issue.reply" && e.event.replyId === id)?.seq;
226570
+ if (snap.issueReplies.some((r) => r.issueId === issueId && r.createdAt === work.createdAt && (!created || replySeq(r.id) === void 0))) {
226571
+ sources.push({ kind: "unavailable", at: work.createdAt, issueId });
226572
+ continue;
226573
+ }
226574
+ const replies = snap.issueReplies.filter((r) => {
226575
+ if (r.issueId !== issueId || r.createdAt > work.createdAt) return false;
226576
+ const seq = replySeq(r.id);
226577
+ return created && seq !== void 0 ? seq < created.seq : r.createdAt < work.createdAt;
226578
+ }).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || (replySeq(a.id) ?? 0) - (replySeq(b2.id) ?? 0) || a.id.localeCompare(b2.id));
226579
+ const reply = replies.at(-1);
226580
+ if (reply?.viaWorkId && snap.works.some((w2) => w2.id === reply.viaWorkId && w2.nodeId === work.nodeId)) {
226581
+ sources.push({ kind: "unavailable", at: work.createdAt, issueId });
226582
+ continue;
226583
+ }
226584
+ sources.push({
226585
+ kind: issueId.startsWith("ann:rework:") ? "gap_resume" : "comment",
226586
+ actor: ref2(reply?.authorActorId ?? issue2.authorActorId),
226587
+ at: reply?.createdAt ?? issue2.createdAt,
226588
+ content: reply?.body ?? issue2.body,
226589
+ issueId,
226590
+ ...reply ? { replyId: reply.id } : {}
226591
+ });
226592
+ }
226593
+ return sources.length ? sources : [{
226594
+ kind: "assignment",
226595
+ actor: briefing.actor,
226596
+ at: work.createdAt,
226597
+ ...briefing.spec ? { content: briefing.spec } : {}
226598
+ }];
226599
+ }
226600
+ function artifactsOfWork(work, input) {
226601
+ const snap = input.snap;
226602
+ const workId = work.id;
226603
+ const nodeId = work.nodeId;
226604
+ const node2 = snap.nodes.find((n) => n.id === nodeId);
226605
+ const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
226606
+ const isRootBrief = node2?.type === "brief" && !nodesWithInEdges.has(nodeId);
226607
+ const list2 = [];
226608
+ for (const a of snap.artifacts) {
226609
+ if (a.workId !== workId) continue;
226610
+ const artifactNodeId = a.nodeId;
226611
+ const files = a.contentKind === "manifest" ? input.manifestFiles?.get(a.contentRef) : void 0;
226612
+ if (files && files.length > 0) {
226613
+ for (const path41 of files) {
226614
+ list2.push({ id: a.id, name: path41.split("/").pop() || path41, path: path41, nodeId: artifactNodeId });
226615
+ }
226616
+ } else if (isRootBrief) {
226617
+ list2.push({ id: a.id, name: "\u4EFB\u52A1\u76EE\u6807", path: "\u4EFB\u52A1\u76EE\u6807.md", nodeId: artifactNodeId });
226618
+ } else {
226619
+ list2.push({ id: a.id, name: a.name, nodeId: artifactNodeId });
226620
+ }
226621
+ }
226622
+ return list2;
226623
+ }
226624
+ function buildSingleTrace(input) {
225373
226625
  const { workorderId, cardId, snap } = input;
225374
226626
  const ref2 = input.ref ?? ((id) => ({ id }));
225375
226627
  const empty2 = { workorderId, cardId, attempts: [] };
@@ -225382,26 +226634,6 @@ function buildWorkorderActivityTrace(input) {
225382
226634
  const nodeId = work?.nodeId ?? review?.nodeId;
225383
226635
  if (!nodeId) return empty2;
225384
226636
  const node2 = nodeById.get(nodeId);
225385
- const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
225386
- const isRootBrief = node2?.type === "brief" && !nodesWithInEdges.has(nodeId);
225387
- const artifactsOfWork = (workId) => {
225388
- const list2 = [];
225389
- for (const a of snap.artifacts) {
225390
- if (a.workId !== workId) continue;
225391
- const artifactNodeId = a.nodeId;
225392
- const files = a.contentKind === "manifest" ? input.manifestFiles?.get(a.contentRef) : void 0;
225393
- if (files && files.length > 0) {
225394
- for (const path41 of files) {
225395
- list2.push({ id: a.id, name: path41.split("/").pop() || path41, path: path41, nodeId: artifactNodeId });
225396
- }
225397
- } else if (isRootBrief) {
225398
- list2.push({ id: a.id, name: "\u4EFB\u52A1\u76EE\u6807", path: "\u4EFB\u52A1\u76EE\u6807.md", nodeId: artifactNodeId });
225399
- } else {
225400
- list2.push({ id: a.id, name: a.name, nodeId: artifactNodeId });
225401
- }
225402
- }
225403
- return list2;
225404
- };
225405
226637
  const managerId = resolveWorkorderManager(snap) ?? snap.workorder.ownerActorId;
225406
226638
  const startedAt = work?.createdAt ?? review?.createdAt;
225407
226639
  const spec = node2?.spec?.trim();
@@ -225411,25 +226643,115 @@ function buildWorkorderActivityTrace(input) {
225411
226643
  nodeTitle: node2?.title || nodeId.split(":").slice(2).join(":") || nodeId,
225412
226644
  ...spec ? { spec } : {}
225413
226645
  };
225414
- const rows = (input.dispatches ?? []).filter((d) => d.targetKind === target.kind && d.targetId === target.id).slice().sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
226646
+ const rows = (input.dispatches ?? []).filter((d) => d.targetKind === target.kind && d.targetId === target.id).slice().sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id));
225415
226647
  const executorId = work?.assigneeActorId ?? review?.reviewerActorId;
225416
226648
  const conclusion = work?.conclusion?.trim() || review?.note?.trim() || void 0;
225417
226649
  const attempts = rows.map((d, i) => {
225418
226650
  const isLast = i === rows.length - 1;
225419
- const actorId = executorId ?? d.actorId;
226651
+ const actorId = d.actorId ?? executorId;
225420
226652
  return {
225421
226653
  runId: d.id,
225422
226654
  at: d.createdAt,
225423
226655
  actor: ref2(actorId),
225424
226656
  ...isLast && conclusion ? { conclusion } : {},
225425
- artifacts: isLast && work ? artifactsOfWork(work.id) : []
226657
+ artifacts: isLast && work ? artifactsOfWork(work, input) : []
225426
226658
  };
225427
226659
  });
225428
226660
  return { workorderId, cardId, briefing, attempts };
225429
226661
  }
226662
+ function buildReviewHistory(input, targetWorkId, reviewerActorId, allReviews) {
226663
+ const { workorderId, cardId } = input;
226664
+ const snap = input.snap;
226665
+ const ref2 = input.ref ?? ((id) => ({ id }));
226666
+ const rows = allReviews.filter((r) => r.targetWorkId === targetWorkId && r.reviewerActorId === reviewerActorId);
226667
+ if (!rows.length) return { workorderId, cardId, attempts: [] };
226668
+ const byId = new Map(rows.map((r) => [r.id, r]));
226669
+ const current = new Set(latestActivityReviews(rows, input.events ?? []).map((r) => r.id));
226670
+ const work = snap.works.find((w2) => w2.id === targetWorkId);
226671
+ const nodeId = rows[0].nodeId;
226672
+ const events = [];
226673
+ for (const rec of [...input.events ?? []].sort((a, b2) => a.seq - b2.seq)) {
226674
+ const e = rec.event;
226675
+ const r = "reviewId" in e && e.reviewId ? byId.get(e.reviewId) : void 0;
226676
+ const direct = e.kind === "review.response" && e.targetWorkId === targetWorkId && e.reviewerActorId === reviewerActorId;
226677
+ let retry = false;
226678
+ if (e.kind === "plan.node_retry" && e.nodeId === nodeId) {
226679
+ const preceding = snap.works.filter((w2) => w2.nodeId === nodeId && w2.createdAt <= rec.createdAt).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
226680
+ retry = preceding[0]?.id === targetWorkId && rows.some((r2) => r2.createdAt <= rec.createdAt);
226681
+ }
226682
+ if (!r && !direct && !retry) continue;
226683
+ let label;
226684
+ let detail;
226685
+ switch (e.kind) {
226686
+ case "review.create":
226687
+ label = "\u521B\u5EFA\u5BA1\u6838";
226688
+ break;
226689
+ case "review.started":
226690
+ label = "\u5F00\u59CB\u5BA1\u6838";
226691
+ break;
226692
+ case "review.timeout":
226693
+ label = "\u5BA1\u6838\u6267\u884C\u8D85\u65F6 / \u5931\u8D25";
226694
+ break;
226695
+ case "review.kill":
226696
+ label = "\u5BA1\u6838\u5DF2\u505C\u6B62";
226697
+ detail = e.reason;
226698
+ break;
226699
+ case "review.response":
226700
+ label = e.verdict === "approve" ? "\u5BA1\u6838\u901A\u8FC7" : "\u5BA1\u6838\u4E0D\u901A\u8FC7";
226701
+ detail = e.note ?? void 0;
226702
+ break;
226703
+ case "plan.node_retry":
226704
+ label = "\u4ECB\u5165\u91CD\u8BD5";
226705
+ break;
226706
+ default:
226707
+ continue;
226708
+ }
226709
+ const actorId = e.kind === "plan.node_retry" ? e.by : "reviewerActorId" in e ? e.reviewerActorId : rec.actorId;
226710
+ events.push({
226711
+ seq: rec.seq,
226712
+ at: rec.createdAt,
226713
+ kind: e.kind,
226714
+ label,
226715
+ actor: ref2(actorId),
226716
+ ...r ? { reviewId: r.id, reviewGroup: r.reviewGroup } : {},
226717
+ ...detail ? { detail } : {}
226718
+ });
226719
+ }
226720
+ const reviews = rows.map((r) => {
226721
+ const single = buildSingleTrace({ ...input, snap: { ...snap, reviews: allReviews }, cardId: `review:${r.id}` });
226722
+ return {
226723
+ reviewId: r.id,
226724
+ reviewGroup: r.reviewGroup,
226725
+ actor: ref2(r.reviewerActorId),
226726
+ at: r.createdAt,
226727
+ ...r.endedAt ?? r.decidedAt ?? r.cancelledAt ? { endedAt: r.endedAt ?? r.decidedAt ?? r.cancelledAt } : {},
226728
+ status: reviewLabel(r),
226729
+ current: current.has(r.id),
226730
+ ...r.note ? { note: r.note } : {},
226731
+ attempts: single.attempts
226732
+ };
226733
+ });
226734
+ return {
226735
+ workorderId,
226736
+ cardId,
226737
+ attempts: reviews.flatMap((r) => r.attempts).sort((a, b2) => a.at.localeCompare(b2.at) || a.runId.localeCompare(b2.runId)),
226738
+ reviewHistory: {
226739
+ targetWorkId,
226740
+ nodeTitle: snap.nodes.find((n) => n.id === nodeId)?.title ?? nodeId,
226741
+ ...work?.outputVersionNo != null ? { outputVersionNo: work.outputVersionNo } : {},
226742
+ artifacts: work ? artifactsOfWork(work, input) : [],
226743
+ reviews,
226744
+ events,
226745
+ ...input.dispatches === void 0 ? { recordsUnavailable: true } : {}
226746
+ }
226747
+ };
226748
+ }
225430
226749
  var init_activity_trace = __esm({
225431
226750
  "../server/src/domains/collab/activity-trace.ts"() {
225432
226751
  "use strict";
226752
+ init_src4();
226753
+ init_review_activity();
226754
+ init_activity2();
225433
226755
  init_workorder_manager();
225434
226756
  }
225435
226757
  });
@@ -227283,10 +228605,7 @@ function collabDomain(opts) {
227283
228605
  const hit = cache.get(companyId);
227284
228606
  if (hit) return hit;
227285
228607
  const e = await opts.resolveEngine(companyId);
227286
- if (e.kernel === opts.kernel) {
227287
- cache.set(companyId, defaultCtx);
227288
- return defaultCtx;
227289
- }
228608
+ const artifacts = opts.resolveArtifacts ? await opts.resolveArtifacts(companyId) : e.kernel === opts.kernel ? opts.artifacts : void 0;
227290
228609
  const ctx = {
227291
228610
  kernel: e.kernel,
227292
228611
  oplog: e.oplog,
@@ -227296,7 +228615,7 @@ function collabDomain(opts) {
227296
228615
  // ★ 产物旁存也要按公司取(见 resolveArtifacts 的注释):漏了这一行,非默认公司的
227297
228616
  // `/api/workorders/:id/files` 与 node_output 清单恒空——写侧写对了、读侧根本没接线。
227298
228617
  // 抛出来就抛出来:宁可 500 说"这家公司的数据面解析不了",也不能回落默认公司的库。
227299
- ...opts.resolveArtifacts ? { artifacts: await opts.resolveArtifacts(companyId) } : {}
228618
+ ...artifacts ? { artifacts } : {}
227300
228619
  };
227301
228620
  cache.set(companyId, ctx);
227302
228621
  return ctx;
@@ -227517,6 +228836,7 @@ function collabDomain(opts) {
227517
228836
  return {
227518
228837
  status: 200,
227519
228838
  body: buildWorkorderActivity({
228839
+ viewerActorId: req.auth.actor,
227520
228840
  workorderId,
227521
228841
  snap,
227522
228842
  events,
@@ -227550,7 +228870,15 @@ function collabDomain(opts) {
227550
228870
  ...extra?.avatar ? { avatar: extra.avatar } : {}
227551
228871
  };
227552
228872
  };
227553
- const snap = engineStore ? await engineStore.transaction((tx) => tx.loadWorkorder(workorderId)) : null;
228873
+ const [snap, events] = engineStore ? await engineStore.transaction(async (tx) => [
228874
+ await tx.loadWorkorder(workorderId),
228875
+ await tx.listEvents(
228876
+ workorderId,
228877
+ 0,
228878
+ cardId.startsWith(REVIEW_CARD_PREFIX) || cardId.startsWith("review:") ? void 0 : ACTIVITY_EVENT_LIMIT,
228879
+ cardId.startsWith(REVIEW_CARD_PREFIX) || cardId.startsWith("review:") ? REVIEW_TRACE_EVENT_KINDS : ACTIVITY_EVENT_KINDS
228880
+ )
228881
+ ]) : [null, []];
227554
228882
  const manifestFiles = snap ? await resolveActivityManifestFiles(snap.artifacts, blobs) : void 0;
227555
228883
  const dispatches = await listDispatchRows(workorderId, opts.dispatchesOfWorkorder);
227556
228884
  return {
@@ -227559,6 +228887,7 @@ function collabDomain(opts) {
227559
228887
  workorderId,
227560
228888
  cardId,
227561
228889
  snap,
228890
+ events,
227562
228891
  ref: ref2,
227563
228892
  ...dispatches ? { dispatches } : {},
227564
228893
  ...manifestFiles ? { manifestFiles } : {}
@@ -227669,6 +228998,9 @@ function collabDomain(opts) {
227669
228998
  return { status: 201, body: { item } };
227670
228999
  });
227671
229000
  router.post("/api/workorders/:id/hold", async (req) => {
229001
+ if (!isHumanActor(req.auth.actor)) {
229002
+ return { status: 403, body: { error: { code: "forbidden", message: "\u6682\u505C\u6574\u4E2A\u5DE5\u5355\u662F\u6CBB\u7406\u64CD\u4F5C\uFF0C\u4EC5\u4EBA\u7C7B\u53EF\u6267\u884C" } } };
229003
+ }
227672
229004
  const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
227673
229005
  const ws = req.params.id;
227674
229006
  const exists = [...kernel.model.artifacts.values()].some((a) => a.workspace === ws);
@@ -227708,6 +229040,9 @@ function collabDomain(opts) {
227708
229040
  return { status: 200, body: { cancelled } };
227709
229041
  });
227710
229042
  router.post("/api/workorders/:id/dispatch", async (req) => {
229043
+ if (!isHumanActor(req.auth.actor)) {
229044
+ return { status: 403, body: { error: { code: "forbidden", message: "\u6062\u590D\u6574\u4E2A\u5DE5\u5355\u662F\u6CBB\u7406\u64CD\u4F5C\uFF0C\u4EC5\u4EBA\u7C7B\u53EF\u6267\u884C" } } };
229045
+ }
227711
229046
  const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
227712
229047
  const ws = req.params.id;
227713
229048
  const already = !kernel.model.pausedWorkorders.has(ws);
@@ -228033,6 +229368,7 @@ var init_collab = __esm({
228033
229368
  init_workorder_detail();
228034
229369
  init_workorders();
228035
229370
  init_activity2();
229371
+ init_review_activity();
228036
229372
  init_activity_trace();
228037
229373
  init_inbox();
228038
229374
  init_escalation_attention();
@@ -231108,42 +232444,17 @@ function mapChatItemToSnapshotEntry(row) {
231108
232444
  itemType,
231109
232445
  role,
231110
232446
  status,
231111
- itemVersion: row.turnVersion,
232447
+ // ADR-0510 附录 A:版本轴 = 会话逻辑时钟(`chat_items.version`)。
232448
+ // 此前这里填的是 `turnVersion`(轮级 resync 游标),而增量流填的是进程内每 item 计数——
232449
+ // 两套编号空间,一轮中途拉快照就把随后每一帧判 stale,进 resync 死循环。
232450
+ itemVersion: row.version,
231112
232451
  seq: row.seq,
232452
+ ord: row.ord,
231113
232453
  messageId: row.messageId,
231114
232454
  updatedAt: row.updatedAt,
231115
232455
  payload: row.payload
231116
232456
  };
231117
232457
  }
231118
- function synthesizeMessageItem(msg, seq) {
231119
- const attachments = Array.isArray(msg.attachments) ? msg.attachments.map((a) => ({
231120
- name: a.name,
231121
- ...a.blobRef !== void 0 ? { blobRef: a.blobRef } : {},
231122
- ...a.contentType !== void 0 ? { contentType: a.contentType } : {}
231123
- })) : void 0;
231124
- const clientSubmitId = msg.role === "user" ? msg.clientSubmitId ?? clientSubmitIdFromMessageKey(msg.messageKey) : void 0;
231125
- const role = msg.role === "user" || msg.role === "system" ? msg.role : "assistant";
231126
- const status = msg.status === "running" ? "streaming" : msg.status === "error" ? "failed" : "completed";
231127
- return {
231128
- // ADR-0510 D1:有 csid 就用**与账本 user item、前端乐观桶位同名**的身份;没有(历史行 / 老客户端)
231129
- // 才退回合成 id。三方同名,桶按 itemId 合并才闭环——否则同一条 user 消息画两个气泡。
231130
- itemId: clientSubmitId ? optimisticUserItemId(clientSubmitId) : `chat-message:${msg.id}`,
231131
- turnId: msg.runId ?? `chat-message-turn:${msg.id}`,
231132
- itemType: "message",
231133
- role,
231134
- status,
231135
- itemVersion: 0,
231136
- seq,
231137
- messageId: msg.id,
231138
- updatedAt: msg.completedAt ?? msg.createdAt,
231139
- payload: {
231140
- role,
231141
- text: msg.content ?? "",
231142
- ...attachments?.length ? { attachments } : {},
231143
- ...clientSubmitId ? { clientSubmitId } : {}
231144
- }
231145
- };
231146
- }
231147
232458
  function throwWorkdirError(code2) {
231148
232459
  switch (code2) {
231149
232460
  case "PATH_ESCAPE":
@@ -231413,7 +232724,8 @@ function createChatSessionsDomain(opts) {
231413
232724
  ...Object.prototype.hasOwnProperty.call(body2, "projectId") ? { projectId: body2.projectId } : {},
231414
232725
  // 会话级模型覆盖:显式传 null / 空串 = 清除覆盖(回到旧解析);不传这一项 = 不动它。
231415
232726
  ...Object.prototype.hasOwnProperty.call(body2, "model") ? { model: normalizeModel(body2.model) } : {},
231416
- touchedAt: body2.touchedAt ?? (/* @__PURE__ */ new Date()).toISOString()
232727
+ // 只有显式传了才写——改标题 / 换项目 / 换模型都不是「有新动静」,不该顶掉列表顺序与已读水位。
232728
+ ...body2.touchedAt !== void 0 ? { touchedAt: body2.touchedAt } : {}
231417
232729
  });
231418
232730
  return { status: 200, body: await store2.getSession(req.params.id) };
231419
232731
  });
@@ -231505,57 +232817,50 @@ function createChatSessionsDomain(opts) {
231505
232817
  if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
231506
232818
  const chatSessionId = req.params.id;
231507
232819
  const sinceVersion = Math.max(0, Number(req.query?.get("sinceVersion") ?? "0") || 0);
232820
+ const beforeOrdRaw = Number(req.query?.get("beforeOrd") ?? "0") || 0;
232821
+ const beforeOrd = beforeOrdRaw > 0 ? beforeOrdRaw : void 0;
232822
+ const limitRaw = Number(req.query?.get("limit") ?? "0") || 0;
232823
+ const limit = limitRaw > 0 ? Math.min(limitRaw, MAX_SNAPSHOT_ITEMS) : DEFAULT_SNAPSHOT_ITEMS;
231508
232824
  const cursor = opts.liveChat?.cursorFor(chatSessionId) ?? null;
231509
232825
  let turnId = cursor?.turnId ?? null;
231510
- let inflightRunId = opts.liveChat?.runFor(chatSessionId) ?? null;
231511
232826
  if (!turnId) {
231512
232827
  const turns = await turnsFor(req);
231513
232828
  const active = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
231514
232829
  turnId = active?.id ?? null;
231515
- inflightRunId ??= active?.assistantRunId ?? null;
231516
232830
  }
231517
232831
  const snapshotEntries = [];
231518
232832
  let nextSinceVersion = sinceVersion;
231519
- if (turnId) {
231520
- const rows = await items.listItemsByTurn(chatSessionId, turnId, { sinceTurnVersion: sinceVersion }).catch(() => []);
232833
+ let hasMoreBefore = false;
232834
+ if (sinceVersion > 0) {
232835
+ if (turnId) {
232836
+ const rows = await items.listItemsByTurn(chatSessionId, turnId, { sinceTurnVersion: sinceVersion }).catch(() => []);
232837
+ for (const row of rows) {
232838
+ snapshotEntries.push(mapChatItemToSnapshotEntry(row));
232839
+ if (row.turnVersion > nextSinceVersion) nextSinceVersion = row.turnVersion;
232840
+ }
232841
+ }
232842
+ } else {
232843
+ const rows = await items.listSessionItems(chatSessionId, {
232844
+ limit,
232845
+ ...beforeOrd !== void 0 ? { beforeOrd } : {}
232846
+ }).catch(() => []);
232847
+ hasMoreBefore = rows.length >= limit;
231521
232848
  for (const row of rows) {
231522
232849
  snapshotEntries.push(mapChatItemToSnapshotEntry(row));
231523
232850
  if (row.turnVersion > nextSinceVersion) nextSinceVersion = row.turnVersion;
231524
232851
  }
231525
232852
  }
231526
- const activeTurnItemIds = new Set(snapshotEntries.map((e) => e.itemId));
231527
- const HISTORY_TURNS = 12;
231528
- const historicalEntries = [];
231529
- if (sinceVersion === 0) {
231530
- const listRecentTurns2 = store2.listRecentTurns;
231531
- const messages = listRecentTurns2 ? (await listRecentTurns2.call(store2, chatSessionId, HISTORY_TURNS).catch(() => ({ messages: [], hasMoreBefore: false }))).messages : windowMessagesByTurns(await store2.listMessages(chatSessionId).catch(() => []), HISTORY_TURNS).messages;
231532
- let historySeq = 0;
231533
- for (const msg of messages) {
231534
- if (!msg.id) continue;
231535
- const rows = await items.listItemsByMessage(msg.id).catch(() => []);
231536
- const historical = turnId ? rows.filter((row) => row.turnId !== turnId) : rows;
231537
- if (rows.length > 0) {
231538
- for (const row of [...historical].sort((a, b2) => a.seq - b2.seq)) {
231539
- historySeq += 1;
231540
- historicalEntries.push({ ...mapChatItemToSnapshotEntry(row), seq: historySeq });
231541
- }
231542
- continue;
231543
- }
231544
- if (msg.role === "assistant" && msg.status === "running" && inflightRunId && msg.runId === inflightRunId) continue;
231545
- const synthesized = synthesizeMessageItem(msg, historySeq + 1);
231546
- if (activeTurnItemIds.has(synthesized.itemId)) continue;
231547
- historySeq += 1;
231548
- historicalEntries.push(synthesized);
231549
- }
231550
- }
232853
+ const oldestOrd = snapshotEntries.find((e) => e.ord !== null)?.ord ?? null;
231551
232854
  const body2 = {
231552
232855
  chatSessionId,
231553
232856
  turnId,
231554
- // 历史合成条在前,active turn chat_items 在后——顺序即桶排序,桶按 seq 单调排列。
231555
- items: [...historicalEntries, ...snapshotEntries],
232857
+ // 已按 ord 升序——服务端给什么顺序,客户端就按 ord 排出什么顺序,两边同一把尺子。
232858
+ items: snapshotEntries,
231556
232859
  nextSinceVersion,
231557
232860
  frameCursor: cursor ? { epoch: cursor.epoch, seq: cursor.seq } : { epoch: 0, seq: 0 },
231558
- canAppend: cursor?.canAppend ?? false
232861
+ canAppend: cursor?.canAppend ?? false,
232862
+ oldestOrd,
232863
+ hasMoreBefore
231559
232864
  };
231560
232865
  return { status: 200, body: body2 };
231561
232866
  });
@@ -231639,7 +232944,11 @@ function createChatSessionsDomain(opts) {
231639
232944
  ...record8.label ? { label: record8.label } : {},
231640
232945
  ...record8.taskDigest ? { taskDigest: record8.taskDigest } : {},
231641
232946
  parentMessageId: record8.parentMessageId,
231642
- touches: touches.map((t) => ({ messageId: t.messageId, kind: t.kind, at: t.at })),
232947
+ /* `messageId` 必须过 `touchAnchorMessageId`:流水行的主键是 `(delegation_id, message_id)`,
232948
+ 服务端为此在锚点后缀了一段随机 token(`DelegationService.touchAnchor`)。那段后缀是存储
232949
+ 的去重装置,直接发给前端等于给了一条 `messages[]` 里不存在的 id ——追加转达卡全部掉进
232950
+ floating 堆在流末尾,同一条消息上的两条委派还会因后缀不同被拆成两张单专家卡。 */
232951
+ touches: touches.map((t) => ({ messageId: touchAnchorMessageId(t.messageId), kind: t.kind, at: t.at })),
231643
232952
  artifacts: collectDelegationArtifacts(record8.roundSummaries),
231644
232953
  createdAt: record8.createdAt,
231645
232954
  ...record8.settledAt ? { settledAt: record8.settledAt } : {}
@@ -231908,7 +233217,7 @@ function createChatSessionsDomain(opts) {
231908
233217
  });
231909
233218
  };
231910
233219
  }
231911
- var import_node_crypto59, WORKDIR_READ_MAX_BYTES;
233220
+ var import_node_crypto59, DEFAULT_SNAPSHOT_ITEMS, MAX_SNAPSHOT_ITEMS, WORKDIR_READ_MAX_BYTES;
231912
233221
  var init_chat_sessions = __esm({
231913
233222
  "../server/src/domains/chat-sessions/index.ts"() {
231914
233223
  "use strict";
@@ -231929,6 +233238,8 @@ var init_chat_sessions = __esm({
231929
233238
  init_workorder_terminal();
231930
233239
  init_session_workdir();
231931
233240
  init_project_bundle();
233241
+ DEFAULT_SNAPSHOT_ITEMS = 400;
233242
+ MAX_SNAPSHOT_ITEMS = 1e3;
231932
233243
  WORKDIR_READ_MAX_BYTES = 2 * 1024 * 1024;
231933
233244
  }
231934
233245
  });
@@ -236591,6 +237902,9 @@ function makeStoreCodeRepoResolver(deps) {
236591
237902
  }
236592
237903
  };
236593
237904
  }
237905
+ function makeCompanyCodeRepoResolver(storesFor) {
237906
+ return async (ws, companyId) => makeStoreCodeRepoResolver(await storesFor(companyId)).resolveCodeRepo(ws);
237907
+ }
236594
237908
  var init_resolve = __esm({
236595
237909
  "../server/src/git/resolve.ts"() {
236596
237910
  "use strict";
@@ -237541,16 +238855,28 @@ var init_fused_signal_router = __esm({
237541
238855
  }
237542
238856
  });
237543
238857
 
238858
+ // ../storage/src/pg-ident.ts
238859
+ function quoteIdent2(name) {
238860
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(name)) throw new Error(`invalid schema name: ${name}`);
238861
+ return `"${name}"`;
238862
+ }
238863
+ var init_pg_ident = __esm({
238864
+ "../storage/src/pg-ident.ts"() {
238865
+ "use strict";
238866
+ }
238867
+ });
238868
+
237544
238869
  // ../storage/src/postgres.ts
237545
238870
  var import_node_crypto67, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
237546
238871
  var init_postgres = __esm({
237547
238872
  "../storage/src/postgres.ts"() {
237548
238873
  "use strict";
237549
238874
  import_node_crypto67 = require("node:crypto");
238875
+ init_pg_ident();
237550
238876
  init_esm();
237551
238877
  init_src();
237552
238878
  ident3 = (s2) => {
237553
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema identifier: ${s2}`);
238879
+ quoteIdent2(s2);
237554
238880
  return s2;
237555
238881
  };
237556
238882
  isUniqueViolation = (err) => typeof err === "object" && err !== null && err.code === "23505";
@@ -237772,7 +239098,9 @@ function rowToVariable(row) {
237772
239098
  updatedAt: new Date(row.updated_at).toISOString(),
237773
239099
  // ADR 凭据保险库补章 §D2.1:到期/取用时间戳(text 列存 ISO 字符串,原样带出;null/空 = undefined)。
237774
239100
  ...row.expires_at ? { expiresAt: row.expires_at } : {},
237775
- ...row.last_used_at ? { lastUsedAt: row.last_used_at } : {}
239101
+ ...row.last_used_at ? { lastUsedAt: row.last_used_at } : {},
239102
+ // 展示名(存量行没有 → 不带这一格,展示侧回落成 key)。
239103
+ ...row.name ? { name: row.name } : {}
237776
239104
  };
237777
239105
  }
237778
239106
  var ident4, PostgresRegistryStore, rowToActor, rowToConfig, rowToAgentPrefs;
@@ -237780,9 +239108,10 @@ var init_postgres_registry = __esm({
237780
239108
  "../storage/src/postgres-registry.ts"() {
237781
239109
  "use strict";
237782
239110
  init_esm();
239111
+ init_pg_ident();
237783
239112
  init_src();
237784
239113
  ident4 = (s2) => {
237785
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
239114
+ quoteIdent2(s2);
237786
239115
  return s2;
237787
239116
  };
237788
239117
  PostgresRegistryStore = class _PostgresRegistryStore {
@@ -237938,6 +239267,7 @@ var init_postgres_registry = __esm({
237938
239267
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS project_id text`);
237939
239268
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS environment text`);
237940
239269
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS delivery_mode text`);
239270
+ await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS name text`);
237941
239271
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS expires_at text`);
237942
239272
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS last_used_at text`);
237943
239273
  await pool.query(`DROP INDEX IF EXISTS "${s2}"."${s2}_variables_key_actor_uq"`);
@@ -238436,10 +239766,13 @@ var init_postgres_registry = __esm({
238436
239766
  /* ---------- 变量 ---------- */
238437
239767
  async putVariable(v2) {
238438
239768
  await this.pool.query(
238439
- `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at)
238440
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
239769
+ /* `name` COALESCE:**没传 = 不动**。连接器授权流程写变量时手上没有「名称」,
239770
+ 普通赋值会把人在密钥表单里填的名字抹掉——同 connectors.connected_by 那条。 */
239771
+ `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at, name)
239772
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
238441
239773
  ON CONFLICT (key, COALESCE(actor_id, ''), COALESCE(project_id, '')) DO UPDATE
238442
- SET scope=$2, actor_id=$3, project_id=$4, connector_id=$5, overrides=$6, delivery_mode=$7, value_encrypted=$8, updated_at=$9, expires_at=$10`,
239774
+ SET scope=$2, actor_id=$3, project_id=$4, connector_id=$5, overrides=$6, delivery_mode=$7, value_encrypted=$8, updated_at=$9, expires_at=$10,
239775
+ name=COALESCE($11, ${this.s}.variables.name)`,
238443
239776
  [
238444
239777
  v2.key,
238445
239778
  v2.scope,
@@ -238450,7 +239783,8 @@ var init_postgres_registry = __esm({
238450
239783
  v2.deliveryMode ?? null,
238451
239784
  v2.valueEncrypted,
238452
239785
  v2.updatedAt,
238453
- v2.expiresAt ?? null
239786
+ v2.expiresAt ?? null,
239787
+ v2.name ?? null
238454
239788
  ]
238455
239789
  );
238456
239790
  }
@@ -238546,17 +239880,6 @@ var init_postgres_registry = __esm({
238546
239880
  }
238547
239881
  });
238548
239882
 
238549
- // ../storage/src/pg-ident.ts
238550
- function quoteIdent2(name) {
238551
- if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(name)) throw new Error(`invalid schema name: ${name}`);
238552
- return `"${name}"`;
238553
- }
238554
- var init_pg_ident = __esm({
238555
- "../storage/src/pg-ident.ts"() {
238556
- "use strict";
238557
- }
238558
- });
238559
-
238560
239883
  // ../storage/src/postgres-assistant-store.ts
238561
239884
  var ASSISTANT_LOCK_NAMESPACE, rowToBinding, rowToHistory, PostgresAssistantBindStore, PostgresHumanPrefsStore;
238562
239885
  var init_postgres_assistant_store = __esm({
@@ -238865,8 +240188,9 @@ var init_postgres_channels = __esm({
238865
240188
  "../storage/src/postgres-channels.ts"() {
238866
240189
  "use strict";
238867
240190
  init_esm();
240191
+ init_pg_ident();
238868
240192
  ident5 = (s2) => {
238869
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
240193
+ quoteIdent2(s2);
238870
240194
  return s2;
238871
240195
  };
238872
240196
  PostgresChannelStore = class _PostgresChannelStore {
@@ -239140,6 +240464,24 @@ var init_postgres_projects = __esm({
239140
240464
  const r = await this.pool.query(`SELECT * FROM ${this.s}.projects ORDER BY created_at`);
239141
240465
  return r.rows.map(rowToProject);
239142
240466
  }
240467
+ /** 主行 + 三张子表一并删,一个事务里做完——四张表之间没有外键,分开删失败一半就留孤儿行。 */
240468
+ async deleteProject(id) {
240469
+ const client = await this.pool.connect();
240470
+ try {
240471
+ await client.query("BEGIN");
240472
+ await client.query(`DELETE FROM ${this.s}.project_members WHERE project_id = $1`, [id]);
240473
+ await client.query(`DELETE FROM ${this.s}.project_files WHERE project_id = $1`, [id]);
240474
+ await client.query(`DELETE FROM ${this.s}.project_content_locations WHERE project_id = $1`, [id]);
240475
+ await client.query(`DELETE FROM ${this.s}.projects WHERE id = $1`, [id]);
240476
+ await client.query("COMMIT");
240477
+ } catch (e) {
240478
+ await client.query("ROLLBACK").catch(() => {
240479
+ });
240480
+ throw e;
240481
+ } finally {
240482
+ client.release();
240483
+ }
240484
+ }
239143
240485
  async replaceProjectMembers(projectId2, members) {
239144
240486
  const client = await this.pool.connect();
239145
240487
  try {
@@ -239934,8 +241276,9 @@ var init_postgres_control_plane = __esm({
239934
241276
  "../storage/src/postgres-control-plane.ts"() {
239935
241277
  "use strict";
239936
241278
  init_esm();
241279
+ init_pg_ident();
239937
241280
  ident7 = (s2) => {
239938
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
241281
+ quoteIdent2(s2);
239939
241282
  return s2;
239940
241283
  };
239941
241284
  PostgresControlPlaneStore = class _PostgresControlPlaneStore {
@@ -240860,11 +242203,12 @@ var ident8, mergeContinuityRefs3, PostgresTraceStore, rowToRun, terminalHandoffO
240860
242203
  var init_postgres_trace = __esm({
240861
242204
  "../storage/src/postgres-trace.ts"() {
240862
242205
  "use strict";
242206
+ init_pg_ident();
240863
242207
  init_esm();
240864
242208
  init_src();
240865
242209
  init_pg_sanitize();
240866
242210
  ident8 = (s2) => {
240867
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
242211
+ quoteIdent2(s2);
240868
242212
  return s2;
240869
242213
  };
240870
242214
  mergeContinuityRefs3 = (...lists) => {
@@ -242849,10 +244193,11 @@ var init_postgres_automations = __esm({
242849
244193
  "../storage/src/postgres-automations.ts"() {
242850
244194
  "use strict";
242851
244195
  init_esm();
244196
+ init_pg_ident();
242852
244197
  init_postgres_chat_turns();
242853
244198
  init_src();
242854
244199
  ident10 = (s2) => {
242855
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
244200
+ quoteIdent2(s2);
242856
244201
  return s2;
242857
244202
  };
242858
244203
  genId = (prefix) => `${prefix}_${Math.random().toString(36).slice(2, 12)}`;
@@ -244091,6 +245436,15 @@ var init_postgres_chat_sessions = __esm({
244091
245436
  args
244092
245437
  );
244093
245438
  }
245439
+ /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
245440
+ async clearProjectBinding(projectId2) {
245441
+ if (!projectId2) return 0;
245442
+ const r = await this.pool.query(
245443
+ `UPDATE ${this.s}.chat_sessions SET project_id = NULL WHERE project_id = $1 AND company_id = $2`,
245444
+ [projectId2, this.companyId]
245445
+ );
245446
+ return r.rowCount ?? 0;
245447
+ }
244094
245448
  async deleteSession(id) {
244095
245449
  if (!await this.scopedSessionExists(id)) return;
244096
245450
  await this.pool.query(`DELETE FROM ${this.s}.chat_messages WHERE session_id = $1`, [id]);
@@ -244100,6 +245454,64 @@ var init_postgres_chat_sessions = __esm({
244100
245454
  async appendMessage(m2, opts) {
244101
245455
  return (await this.appendMessageOnce(m2, opts)).message;
244102
245456
  }
245457
+ /**
245458
+ * 系统状态条的 `chat_items` 补行(ADR-0510 D3 的收口)。
245459
+ *
245460
+ * ## 为什么需要
245461
+ *
245462
+ * `role='system'` 的通告(委派回报「已完成 / 无法完成」、工单终态播报、授权卡状态条)走的是
245463
+ * `appendChatMessageOnce` → 本 store 直写 `chat_messages` 这条路,**不经 `ChatItemLedger`**
245464
+ * (`chat-system-message.ts` 文件头原话:「非 live 直写……不产生 chat_items」)。
245465
+ *
245466
+ * 在 D3 之前这没关系:`/items` 会给没有 item 行的消息合成一条快照条。D3 把历史收成
245467
+ * **只从 `chat_items` 出**、删掉了那条合成路之后,这些通告就从对话页上**凭空消失**了。
245468
+ *
245469
+ * 后果不只是「少看见一条通告」——委派产物的附件卡是**锚在那条回报通告之后的第一条 assistant**
245470
+ * 上的(ws:wo-4a2cd648 §①)。通告不在流里 ⇒ 找不到锚点 ⇒ 卡掉进 floating、渲染到末尾。
245471
+ * 2026-09-09 用户现场:`--continue` 续跑两个子会话,回报通告写进了 `chat_messages`
245472
+ * (07:29:56 / 07:30:12)但 `chat_items` 一行都没有,产物卡因此挂错位置。
245473
+ *
245474
+ * ## 为什么补在这一层
245475
+ *
245476
+ * `appendChatMessageOnce` 是所有系统消息的唯一入口,但那三个调用点(return-flow /
245477
+ * chat-broadcast / server.ts 授权卡)**都拿不到 `ChatItemStore`**,从上面递下来要动三份 deps 契约。
245478
+ * 而这里本来就握着同一个 schema 的连接池——把「用户能看见的消息必有 item 行」这条不变量
245479
+ * 落在存储边界上,比在三个业务处各接一次线更难漏。
245480
+ *
245481
+ * ## 失败了怎么办:吞掉
245482
+ *
245483
+ * item 写失败**不许**把消息本身带走——消息已经 INSERT 成功、幂等键已经占住,抛出去会让调用方
245484
+ * 当成「没写成」而重试,而重试会撞主键。所以这里只记日志。代价是那条通告在页面上仍然看不见,
245485
+ * 与修复前一致,不会更坏。
245486
+ */
245487
+ async writeSystemMessageItem(m2) {
245488
+ try {
245489
+ await this.pool.query(
245490
+ `INSERT INTO ${this.s}.chat_items
245491
+ (id, session_id, message_id, turn_id, run_id, seq, kind, role, status,
245492
+ version, turn_version, provider_item_key, payload, metadata,
245493
+ created_at, updated_at, completed_at)
245494
+ VALUES ($1, $2, $3, $4, NULL, 1, 'text', $5, 'completed',
245495
+ COALESCE((SELECT MAX(version) FROM ${this.s}.chat_items WHERE session_id = $2), 0) + 1,
245496
+ 1, NULL, $6::jsonb, '{"legacyContent": false}'::jsonb,
245497
+ $7::timestamptz, $7::timestamptz, $7::timestamptz)
245498
+ ON CONFLICT (id) DO NOTHING`,
245499
+ [
245500
+ // 与 D3 之前 `/items` 合成条、以及 backfill 补的行**同一套身份**:同一条消息永远只有
245501
+ // 这一个 itemId,重复调用(幂等重写、backfill 再跑)不会造出第二条。
245502
+ `chat-message:${m2.id}`,
245503
+ m2.sessionId,
245504
+ m2.id,
245505
+ `chat-message-turn:${m2.id}`,
245506
+ m2.role,
245507
+ JSON.stringify({ role: m2.role, text: m2.content ?? "" }),
245508
+ m2.createdAt
245509
+ ]
245510
+ );
245511
+ } catch (err) {
245512
+ console.warn(`[chat-items] \u7CFB\u7EDF\u72B6\u6001\u6761\u8865\u884C\u5931\u8D25\uFF08message=${m2.id} session=${m2.sessionId}\uFF09: ${String(err)}`);
245513
+ }
245514
+ }
244103
245515
  async appendMessageOnce(m2, opts) {
244104
245516
  await this.assertSessionInScope(m2.sessionId);
244105
245517
  const insert = async () => {
@@ -244116,7 +245528,9 @@ var init_postgres_chat_sessions = __esm({
244116
245528
  };
244117
245529
  for (let attempt = 0; ; attempt++) {
244118
245530
  try {
244119
- return { message: { ...m2, seq: await insert() }, inserted: true };
245531
+ const written = { message: { ...m2, seq: await insert() }, inserted: true };
245532
+ if ((opts?.source ?? "live") === "system") await this.writeSystemMessageItem(written.message);
245533
+ return written;
244120
245534
  } catch (err) {
244121
245535
  const isUniqueViolation5 = err && typeof err === "object" && err.code === "23505";
244122
245536
  const constraint = String(err.constraint ?? "");
@@ -244768,11 +246182,17 @@ var init_postgres_chat_sessions = __esm({
244768
246182
  model: row.model ?? null
244769
246183
  });
244770
246184
  rowToSessionWithQuality = (row) => {
246185
+ const status = row.last_assistant_status ?? void 0;
244771
246186
  const quality = deriveLastTurnQuality({
244772
- status: row.last_assistant_status ?? void 0,
246187
+ status,
244773
246188
  content: row.last_assistant_content ?? ""
244774
246189
  });
244775
- return { ...rowToSession(row), ...quality ? { lastTurnQuality: quality } : {} };
246190
+ const running = deriveSessionRunning({ status });
246191
+ return {
246192
+ ...rowToSession(row),
246193
+ ...quality ? { lastTurnQuality: quality } : {},
246194
+ ...running ? { running: true } : {}
246195
+ };
244776
246196
  };
244777
246197
  rowToMessage = (row) => ({
244778
246198
  id: row.id,
@@ -244914,12 +246334,15 @@ var init_postgres_chat_items = __esm({
244914
246334
  const r = await this.pool.query(
244915
246335
  `INSERT INTO ${this.s}.chat_items
244916
246336
  (id, session_id, message_id, turn_id, run_id, seq, kind, role, status,
244917
- version, turn_version, provider_item_key, payload, metadata, created_at, updated_at, completed_at)
246337
+ version, turn_version, provider_item_key, payload, metadata, created_at, updated_at, ord, completed_at)
244918
246338
  SELECT $1, $2, $3, $4, $5,
244919
246339
  COALESCE(MAX(c.seq), 0) + 1,
244920
- $6, $7, $8::text, 0,
246340
+ $6, $7, $8::text, COALESCE($13::bigint, 0),
244921
246341
  COALESCE(MAX(c.turn_version), 0) + 1,
244922
246342
  $9, $10::jsonb, $11::jsonb, $12::timestamptz, $12::timestamptz,
246343
+ -- ord\uFF1A\u8D26\u672C\u9884\u6D3E\u4E86\u5C31\u7528\u5B83\uFF08\u5EFA\u884C\u5373\u6709\u53F7\u3001\u7B2C\u4E00\u5E27\u5C31\u80FD\u5E26\u4E0A wire\uFF09\uFF1B\u6CA1\u7ED9\u624D\u8D70\u5217 DEFAULT
246344
+ -- \u7684 nextval\u2014\u2014\u4E24\u6761\u8DEF\u53D6\u7684\u662F\u540C\u4E00\u6761\u8868\u7EA7\u5E8F\u5217\uFF0C\u8BED\u4E49\u76F8\u540C\uFF0C\u53EA\u5DEE\u62FF\u53F7\u53D1\u751F\u5728\u54EA\u4E00\u4FA7\u3002
246345
+ COALESCE($14::bigint, nextval('${this.s}.chat_items_ord_seq'::regclass)),
244923
246346
  -- $12/$8 \u4E09\u5904\u90FD\u663E\u5F0F\u8F6C\u578B\uFF1A\u4E0D\u8F6C\u7684\u8BDD PG \u4F1A\u5728\u540C\u4E00\u6761\u8BED\u53E5\u91CC\u7ED9\u540C\u4E00\u4E2A\u53C2\u6570\u63A8\u51FA\u4E0D\u540C\u7C7B\u578B
244924
246347
  -- \uFF08\u5217\u4F4D\u662F timestamptz/text\u3001CASE \u91CC\u662F unknown\uFF09\uFF0C\u76F4\u63A5\u62A5
244925
246348
  -- inconsistent types deduced for parameter $12\u3002
@@ -244939,7 +246362,9 @@ var init_postgres_chat_items = __esm({
244939
246362
  scrubPgString(input.providerItemKey ?? null),
244940
246363
  payload,
244941
246364
  metadata,
244942
- at
246365
+ at,
246366
+ input.version ?? null,
246367
+ input.ord ?? null
244943
246368
  ]
244944
246369
  );
244945
246370
  return rowToItem(r.rows[0]);
@@ -244977,10 +246402,13 @@ var init_postgres_chat_items = __esm({
244977
246402
  setExpr = `status = $3::text, completed_at = CASE WHEN $3 IN ('completed','failed','interrupted') THEN $2::timestamptz ELSE c.completed_at END`;
244978
246403
  break;
244979
246404
  }
246405
+ const versionParamIndex = args.length + 1;
246406
+ args.push(opts?.version ?? null);
244980
246407
  const r = await this.pool.query(
244981
246408
  `UPDATE ${this.s}.chat_items c
244982
246409
  SET ${setExpr},
244983
- version = c.version + 1,
246410
+ version = CASE WHEN $${versionParamIndex}::bigint IS NULL THEN c.version + 1
246411
+ ELSE GREATEST(c.version, $${versionParamIndex}::bigint) END,
244984
246412
  turn_version = ${bumpTurnVersion},
244985
246413
  updated_at = $2
244986
246414
  WHERE c.id = $1
@@ -245028,6 +246456,49 @@ var init_postgres_chat_items = __esm({
245028
246456
  );
245029
246457
  return r.rows.map((row) => rowToItem(row));
245030
246458
  }
246459
+ /**
246460
+ * 一条会话的 item,按展示序升序(ADR-0510 D3:历史的唯一出口)。
246461
+ *
246462
+ * 排序键 `(ord IS NULL) DESC, ord DESC` 的意思是「取最新的一段」:带号的行按号从大到小、
246463
+ * 没号的存量行排在最后(= 最老)。截断之后再翻回升序返回。
246464
+ * 走索引 `chat_items_session_ord_idx (session_id, ord)`。
246465
+ */
246466
+ async listSessionItems(sessionId, opts) {
246467
+ const limit = Math.max(1, Math.min(opts?.limit ?? 500, 2e3));
246468
+ const before = opts?.beforeOrd;
246469
+ const r = before === void 0 ? await this.pool.query(
246470
+ `SELECT * FROM ${this.s}.chat_items WHERE session_id = $1
246471
+ ORDER BY (ord IS NULL) ASC, ord DESC, seq DESC LIMIT $2`,
246472
+ [sessionId, limit]
246473
+ ) : await this.pool.query(
246474
+ `SELECT * FROM ${this.s}.chat_items WHERE session_id = $1 AND ord IS NOT NULL AND ord < $2
246475
+ ORDER BY ord DESC LIMIT $3`,
246476
+ [sessionId, before, limit]
246477
+ );
246478
+ const rows = r.rows.map((row) => rowToItem(row));
246479
+ rows.reverse();
246480
+ return rows.sort((a, b2) => {
246481
+ if (a.ord === null && b2.ord === null) return a.seq - b2.seq;
246482
+ if (a.ord === null) return -1;
246483
+ if (b2.ord === null) return 1;
246484
+ return a.ord - b2.ord;
246485
+ });
246486
+ }
246487
+ async sessionVersionCursor(sessionId) {
246488
+ const r = await this.pool.query(
246489
+ `SELECT COALESCE(MAX(version), 0) AS v FROM ${this.s}.chat_items WHERE session_id = $1`,
246490
+ [sessionId]
246491
+ );
246492
+ return Number(r.rows[0]?.v ?? 0);
246493
+ }
246494
+ async reserveOrds(count2) {
246495
+ const n = Math.max(1, Math.min(Math.floor(count2), 1e3));
246496
+ const r = await this.pool.query(
246497
+ `SELECT nextval('${this.s}.chat_items_ord_seq'::regclass) AS ord FROM generate_series(1, $1)`,
246498
+ [n]
246499
+ );
246500
+ return r.rows.map((row) => Number(row.ord));
246501
+ }
245031
246502
  async turnCursor(sessionId, turnId) {
245032
246503
  const r = await this.pool.query(
245033
246504
  `SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq, COALESCE(MAX(turn_version), 0) + 1 AS next_turn_version
@@ -245364,8 +246835,14 @@ function planSessionOrd(sessionId, messages, items, turns, opts) {
245364
246835
  }
245365
246836
  async function listChatItemsSchemas(pool) {
245366
246837
  const r = await pool.query(
245367
- `SELECT n.nspname AS s FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
245368
- WHERE c.relname = 'chat_items' AND c.relkind = 'r' ORDER BY 1`
246838
+ `SELECT n.nspname AS s
246839
+ FROM pg_class c
246840
+ JOIN pg_namespace n ON n.oid = c.relnamespace
246841
+ WHERE c.relname = 'chat_items' AND c.relkind = 'r'
246842
+ AND (SELECT count(*) FROM information_schema.columns col
246843
+ WHERE col.table_schema = n.nspname AND col.table_name = 'chat_items'
246844
+ AND col.column_name IN ('session_id', 'turn_id', 'seq', 'kind', 'payload')) = 5
246845
+ ORDER BY 1`
245369
246846
  );
245370
246847
  return r.rows.map((x2) => x2.s);
245371
246848
  }
@@ -245959,9 +247436,10 @@ var init_postgres_nodes = __esm({
245959
247436
  "../storage/src/postgres-nodes.ts"() {
245960
247437
  "use strict";
245961
247438
  import_node_crypto68 = require("node:crypto");
247439
+ init_pg_ident();
245962
247440
  init_esm();
245963
247441
  ident12 = (s2) => {
245964
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
247442
+ quoteIdent2(s2);
245965
247443
  return s2;
245966
247444
  };
245967
247445
  PostgresNodeStore = class _PostgresNodeStore {
@@ -246226,8 +247704,9 @@ var init_postgres_model_prices = __esm({
246226
247704
  "../storage/src/postgres-model-prices.ts"() {
246227
247705
  "use strict";
246228
247706
  init_esm();
247707
+ init_pg_ident();
246229
247708
  ident13 = (s2) => {
246230
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
247709
+ quoteIdent2(s2);
246231
247710
  return s2;
246232
247711
  };
246233
247712
  PostgresModelPriceStore = class _PostgresModelPriceStore {
@@ -246437,10 +247916,11 @@ var init_postgres_actor_memory = __esm({
246437
247916
  "../storage/src/postgres-actor-memory.ts"() {
246438
247917
  "use strict";
246439
247918
  import_node_crypto69 = require("node:crypto");
247919
+ init_pg_ident();
246440
247920
  init_src();
246441
247921
  matchClause = (q2) => q2.requireMatch && q2.keywords.length > 0 ? " WHERE m > 0" : "";
246442
247922
  ident14 = (s2) => {
246443
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
247923
+ quoteIdent2(s2);
246444
247924
  return s2;
246445
247925
  };
246446
247926
  PostgresActorMemoryStore = class _PostgresActorMemoryStore {
@@ -246746,9 +248226,10 @@ var init_postgres_inbox_read = __esm({
246746
248226
  "../storage/src/postgres-inbox-read.ts"() {
246747
248227
  "use strict";
246748
248228
  init_esm();
248229
+ init_pg_ident();
246749
248230
  SEED_MARKER_ACTOR = "actor:system";
246750
248231
  ident15 = (s2) => {
246751
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
248232
+ quoteIdent2(s2);
246752
248233
  return s2;
246753
248234
  };
246754
248235
  PostgresReadMarkerStore = class _PostgresReadMarkerStore {
@@ -252000,10 +253481,11 @@ var init_postgres_knowledge = __esm({
252000
253481
  "../storage/src/postgres-knowledge.ts"() {
252001
253482
  "use strict";
252002
253483
  init_esm();
253484
+ init_pg_ident();
252003
253485
  init_pg_sanitize();
252004
253486
  init_knowledge_snapshot_crypto();
252005
253487
  ident17 = (value2) => {
252006
- if (!/^[a-z_][a-z0-9_]*$/.test(value2)) throw new Error(`invalid schema name: ${value2}`);
253488
+ quoteIdent2(value2);
252007
253489
  return value2;
252008
253490
  };
252009
253491
  TABLES = [
@@ -255625,6 +257107,36 @@ async function startServe(opts) {
255625
257107
  projectId2,
255626
257108
  afterName
255627
257109
  );
257110
+ },
257111
+ /**
257112
+ * 删项目后的跨域清理(发起人 2026-09-09 拍板的口径里,projects 域自己管不到的那两件):
257113
+ * ① 绑到该项目的会话 `project_id` 置空 —— 会话一条都不删,回到「不指定项目」。不清的话
257114
+ * 项目 chip 会按既定口径显示一串裸项目 id(`pages/chat/FRAME-DATA-MAP.md`)。
257115
+ * ② 该项目作用域下的变量/密钥删掉 —— 项目没了,`resolveProjectVariables` 再也解析不到它们,
257116
+ * 留着只是一堆谁也读不到的明文凭据。
257117
+ * 与改名钩子同款:`chatStoreFor` / `engineRouter` 的 const 都在本域构造之后,所以只能在
257118
+ * handler 体内(删除发生时)解析,不能在工厂里提前引用——那会撞 TDZ。
257119
+ */
257120
+ onProjectDeletedFor: (companyId) => async ({ projectId: projectId2 }) => {
257121
+ const failures = [];
257122
+ try {
257123
+ const sessions = await chatStoreFor(companyId);
257124
+ if (sessions.clearProjectBinding) await sessions.clearProjectBinding(projectId2);
257125
+ else failures.push("\u4F1A\u8BDD store \u4E0D\u652F\u6301 clearProjectBinding\uFF0C\u4F1A\u8BDD\u7684\u9879\u76EE\u7ED1\u5B9A\u672A\u6E05");
257126
+ } catch (e) {
257127
+ failures.push(`\u6E05\u4F1A\u8BDD\u9879\u76EE\u7ED1\u5B9A\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
257128
+ }
257129
+ try {
257130
+ const registry2 = companyId && pgPool ? (await engineRouter.getEngine(companyId)).registry : registryStore;
257131
+ if (registry2.resolveProjectVariables) {
257132
+ for (const row of await registry2.resolveProjectVariables(projectId2)) {
257133
+ await registry2.deleteVariable(row.key, void 0, { projectId: projectId2 });
257134
+ }
257135
+ }
257136
+ } catch (e) {
257137
+ failures.push(`\u6E05\u9879\u76EE\u53D8\u91CF\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
257138
+ }
257139
+ if (failures.length) throw new Error(failures.join("\uFF1B"));
255628
257140
  }
255629
257141
  });
255630
257142
  const currentArtifactProjection = async () => {
@@ -256611,6 +258123,16 @@ async function startServe(opts) {
256611
258123
  if (!stores.artifacts) throw new Error(`[work-order-files] \u516C\u53F8 ${companyId} \u6CA1\u6709 artifacts store\uFF0C\u62D2\u7EDD\u56DE\u843D\u9ED8\u8BA4\u516C\u53F8`);
256612
258124
  return stores.artifacts;
256613
258125
  };
258126
+ const projectStateFor = async (companyId) => {
258127
+ if (!chatStoreRouter) return projectStateStore;
258128
+ const stores = await chatStoreRouter.open(companyId, "work-order-projects");
258129
+ if (!stores.projects) throw new Error(`[work-order-projects] \u516C\u53F8 ${companyId} \u6CA1\u6709 projects store\uFF0C\u62D2\u7EDD\u56DE\u843D\u9ED8\u8BA4\u516C\u53F8`);
258130
+ return stores.projects;
258131
+ };
258132
+ const resolveCompanyCodeRepo = makeCompanyCodeRepoResolver(async (companyId) => ({
258133
+ projects: await projectStateFor(companyId),
258134
+ bindings: await artifactStateFor(companyId)
258135
+ }));
256614
258136
  if (pgDsn && pgPool && chatStoreRouter) {
256615
258137
  const router = chatStoreRouter;
256616
258138
  void (async () => {
@@ -256799,7 +258321,6 @@ async function startServe(opts) {
256799
258321
  }
256800
258322
  return token;
256801
258323
  };
256802
- let resolveCodeRepoRef;
256803
258324
  let coordinatorRef = null;
256804
258325
  let coordinatorSpawnProbe = null;
256805
258326
  const dispatchTimers = /* @__PURE__ */ new Set();
@@ -256959,9 +258480,9 @@ async function startServe(opts) {
256959
258480
  port: opts.port ?? 7320,
256960
258481
  projectState: projectStateStore,
256961
258482
  artifactState: artifactStateStore,
256962
- // 建单交接的 `work_order_files` 按**当前公司**落库;与 collab 读侧 `resolveArtifacts` 同一个闭包。
258483
+ // 建单的项目校验、绑定与输入文件按当前公司落库;与项目/任务读侧使用同一个 router。
256963
258484
  // 装了路由(有 PG)才给——dev 文件模式没有 schema 概念,沿用上面那个单实例。
256964
- ...chatStoreRouter ? { artifactStateFor } : {},
258485
+ ...chatStoreRouter ? { artifactStateFor, projectStateFor } : {},
256965
258486
  schema,
256966
258487
  registry: registryStore,
256967
258488
  // 会话级模型覆盖的候选清单:与 `GET /api/runtimes/:id/models` **同一份**实现
@@ -257031,7 +258552,7 @@ async function startServe(opts) {
257031
258552
  });
257032
258553
  return { plannerActor: planner.id, text: chunks.join("") };
257033
258554
  },
257034
- resolveCodeRepo: (ws) => resolveCodeRepoRef?.(ws) ?? null,
258555
+ resolveCodeRepo: resolveCompanyCodeRepo,
257035
258556
  // propose sha 校验兼镜像根:verifyExternalPinSha 在 ls-remote 未命中(祖先提交)时 `git fetch <remote> <sha>`
257036
258557
  // 进此目录留档(校验即留档,见 git/collect.ts)。目录按需 `init --bare`。(历史:曾是合并器/CI 闸的 git-mirrors 根,已删。)
257037
258558
  gitMirrorRoot: path29.join(opts.dir, "propose-mirrors"),
@@ -257108,8 +258629,8 @@ async function startServe(opts) {
257108
258629
  collabDomain({
257109
258630
  kernel: defaultCompanyKernel,
257110
258631
  oplog,
257111
- blobs,
257112
- // 诊断 3e3219e4-22bb19e4 §②:详情页取 seed brief head 正文作页头真源 + overview 写入口
258632
+ blobs: engine.blobs,
258633
+ // 页头正文读写与内容接口同源:默认公司的事实柜。
257113
258634
  // 资产柜:POST /api/workorders/:id/files 上传字节落这里(与 uploadChatAttachment 同一柜),
257114
258635
  // 再让 artifacts.addWorkOrderFile 写行——手动上传不进 oplog / events,删了不伤重放。
257115
258636
  assets,
@@ -259128,11 +260649,10 @@ async function startServe(opts) {
259128
260649
  for (const w2 of projectsCfg?.workspaces ?? []) {
259129
260650
  await artifactStateStore.upsertWorkspaceBinding({ workOrderId: w2.id, projectId: w2.projectId });
259130
260651
  }
259131
- const { resolveCodeRepo, resolveProject } = makeStoreCodeRepoResolver({
260652
+ const { resolveProject } = makeStoreCodeRepoResolver({
259132
260653
  projects: projectStateStore,
259133
260654
  bindings: artifactStateStore
259134
260655
  });
259135
- resolveCodeRepoRef = resolveCodeRepo;
259136
260656
  if (opts.dispatch) {
259137
260657
  const startDispatcher = (companyId, companyEngine) => {
259138
260658
  if (dispatchers.has(companyId)) return;
@@ -259408,8 +260928,7 @@ async function startServe(opts) {
259408
260928
  } : {},
259409
260929
  schema: liveSchemaMap,
259410
260930
  // 活视图:类型目录变更后由 rebuildLiveSchema 原地重填(Dispatcher 每轮现读 opts.schema)
259411
- resolveCodeRepo,
259412
- // §4/C:异步从权威源(store)解析;dispatcher 装配前 await
260931
+ resolveCodeRepo: (ws) => resolveCompanyCodeRepo(ws, companyId),
259413
260932
  // #2 派单开关:工单"待派发"(控制面旁存标 held)时不派其活。held 集通常空、表极小,每次查可接受。
259414
260933
  resolveDispatchHold: async (ws) => (await artifactStateStore.listWorkspaceDispatchHeld()).includes(ws),
259415
260934
  // §D3 评审人连续性:锁定审核人不可用(熔断/离岗)→ 系统身份幂等升级,交管理者改派(不静默换人)。
@@ -260631,7 +262150,7 @@ ${nodeFault}` : "");
260631
262150
  log: (m2) => console.log(m2),
260632
262151
  batchLimit: envNum("OASIS_AUTOMATION_EVENT_BATCH", 200),
260633
262152
  // 工单→项目绑定是合法薄旁存(B5),不是内核事实——列项目下的工单只认它。
260634
- listProjectWorkspaces: async ({ projectId: projectId2 }) => (await artifactStateStore.listWorkspaceBindings()).filter((b2) => b2.projectId === projectId2).map((b2) => b2.workOrderId),
262153
+ listProjectWorkspaces: async ({ companyId, projectId: projectId2 }) => (await (await artifactStateFor(companyId)).listWorkspaceBindings()).filter((b2) => b2.projectId === projectId2).map((b2) => b2.workOrderId),
260635
262154
  // 完成判据必须复用 D3 resolver(不复制 deriveStage),且与成员快照同一次读取。
260636
262155
  // ensureTracked:内核跟踪集只在启动时装载一次、此后只随本进程写入增长——多实例部署下,
260637
262156
  // 另一实例建出来的工单在这里会被读成 unknown(既不触发也不留痕,QA OBS-1 实测)。
@@ -260642,7 +262161,7 @@ ${nodeFault}` : "");
260642
262161
  return makeAutomationWorkorderSnapshotResolver(engine2.kernel)(workspace);
260643
262162
  },
260644
262163
  // 触发时复查归因主体的项目读取权(与配置期同一口径)。
260645
- projectReadable: async ({ projectId: projectId2 }) => await projectStateStore.getProject(projectId2) !== null
262164
+ projectReadable: async ({ companyId, projectId: projectId2 }) => await (await projectStateFor(companyId)).getProject(projectId2) !== null
260646
262165
  });
260647
262166
  automationEventTimer = setInterval(() => {
260648
262167
  void eventPoller.tick().catch((err) => console.error(`[project-event-poller] tick \u5931\u8D25: ${String(err)}`));
@@ -262581,7 +264100,7 @@ var COMMAND_DECLS = {
262581
264100
  { name: "as", desc: "\u7ED9\u8FD9\u6B21\u59D4\u6D3E\u8D77\u4E2A\u77ED\u540D\uFF0C\u4E4B\u540E\u7528\u5B83 --continue\uFF1B\u649E\u540D\u670D\u52A1\u7AEF\u81EA\u52A8\u52A0\u540E\u7F00\uFF0C**\u4EE5\u8F93\u51FA\u91CC\u90A3\u4E2A\u4E3A\u51C6**" },
262582
264101
  { name: "file", desc: "\u4E00\u8D77\u5E26\u8FC7\u53BB\u7684\u6587\u4EF6\uFF08\u4F60\u5DE5\u4F5C\u76EE\u5F55\u4E0B\u7684\u76F8\u5BF9\u8DEF\u5F84\uFF0C\u53EF\u7ED9\u591A\u6B21\uFF1B\u5355\u4E2A \u22642MB\u3001\u4E00\u6B21 \u226420 \u4E2A / 20MB\uFF09" },
262583
264102
  { name: "continue", desc: "\u5F80\u5DF2\u6709\u59D4\u6D3E\u91CC\u518D\u8F6C\u8FBE\u4E00\u53E5\uFF1A\u7ED9 label / \u4E13\u5BB6\u540D / delegationId \u524D\u7F00" },
262584
- { name: "text", desc: "\u8F6C\u8FBE\u5185\u5BB9\uFF08\u914D --continue\uFF09\u3002\u6539\u65B9\u5411\u5C31\u660E\u8BF4\u300C\u6539\u65B9\u5411\u300D\uFF0C\u4E13\u5BB6\u4F1A\u81EA\u5DF1\u5224\u65AD\u8981\u4E0D\u8981\u653E\u5F03\u624B\u4E0A\u90A3\u6BB5" },
264103
+ { name: "text", desc: "\u8F6C\u8FBE\u5185\u5BB9\uFF08\u914D --continue\uFF09\u3002\u6539\u65B9\u5411\u5C31\u660E\u8BF4\u300C\u6539\u65B9\u5411\u300D\uFF0C\u4E13\u5BB6\u4F1A\u81EA\u5DF1\u5224\u65AD\u8981\u4E0D\u8981\u653E\u5F03\u624B\u4E0A\u90A3\u6BB5\u3002**\u770B\u56DE\u6267**\uFF1A\u63D2\u8FDB\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E / \u8D77\u4E86\u65B0\u7684\u4E00\u8F6E / \u8FD8\u6392\u5728\u961F\u5217\u91CC\u2014\u2014\u6392\u961F\u65F6\u4E13\u5BB6\u8981\u7B49\u672C\u8F6E\u8DD1\u5B8C\u624D\u770B\u89C1\uFF0C\u8BE5\u8DDF\u7528\u6237\u8BF4\u4E00\u58F0" },
262585
264104
  { name: "status", desc: "\u770B\u4E00\u4E2A\u59D4\u6D3E\u7684\u8BE6\u60C5\uFF1A\u5168\u91CF\u5404\u8F6E\u5C0F\u7ED3 + \u4EA7\u7269\u6E05\u5355" },
262586
264105
  { name: "list", desc: "\u5217\u51FA\u8FD9\u6761\u5BF9\u8BDD\u6D3E\u51FA\u53BB\u7684\u6D3B\uFF1B\u9ED8\u8BA4\u53EA\u7ED9\u8FDB\u884C\u4E2D + \u6700\u8FD1 10 \u6761", boolean: true },
262587
264106
  { name: "all", desc: "\u914D --list\uFF1A\u8FDE\u5386\u53F2\u59D4\u6D3E\u4E00\u8D77\u5217", boolean: true },
@@ -265067,15 +266586,26 @@ ${round.text}`);
265067
266586
  const text5 = flags.get("text");
265068
266587
  if (!text5) throw new Error('--continue \u8981\u914D --text "<\u8981\u8F6C\u8FBE\u7684\u8BDD>"');
265069
266588
  const target = await resolveRef2(continueRef);
265070
- await api.post(
266589
+ const outcome = await api.post(
265071
266590
  `/api/delegations/${encodeURIComponent(target.id)}/messages`,
265072
266591
  { text: text5, ...fileArgs.length ? { files: fileArgs } : {} }
265073
266592
  );
265074
266593
  if (asJson2) {
265075
- println(JSON.stringify({ delegationId: target.id, label: target.label, files: fileArgs }, null, 2));
266594
+ println(JSON.stringify({ delegationId: target.id, label: target.label, files: fileArgs, ...outcome }, null, 2));
265076
266595
  break;
265077
266596
  }
265078
- println(`\u5DF2\u8F6C\u8FBE\u7ED9 ${target.expertName ?? target.expertActorId}\uFF08${target.label ?? target.id.slice(0, 8)}\uFF09\u3002`);
266597
+ const who = `${target.expertName ?? target.expertActorId}\uFF08${target.label ?? target.id.slice(0, 8)}\uFF09`;
266598
+ if (outcome?.outcome === "inserted") {
266599
+ println(`\u5DF2\u63D2\u8FDB ${who} \u6B63\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E\u2014\u2014\u5B83\u628A\u5F53\u524D\u8FD9\u4E00\u6B65\u7B54\u5B8C\u5C31\u4F1A\u770B\u5230\u3002`);
266600
+ } else if (outcome?.outcome === "started-round") {
266601
+ println(`\u5DF2\u8F6C\u8FBE\u7ED9 ${who}\uFF1A\u5B83\u4E0A\u4E00\u8F6E\u5DF2\u7ECF\u7ED3\u675F\uFF0C\u8FD9\u53E5\u8BDD\u5F53\u573A\u8D77\u4E86\u65B0\u7684\u4E00\u8F6E\u3002`);
266602
+ } else if (outcome?.outcome === "queued") {
266603
+ println(`\u5DF2\u6536\u4E0B\uFF0C\u4F46${who}**\u73B0\u5728\u8FD8\u770B\u4E0D\u5230**\uFF1A${outcome.reason ?? "\u63D2\u4E0D\u8FDB\u6B63\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E"}\u3002`);
266604
+ println(` \u8FD9\u53E5\u8BDD\u6392\u5728\u5F85\u8F6C\u8FBE\u961F\u5217\u91CC\uFF08\u8FD8\u6709 ${outcome.pendingInputs} \u53E5\u6CA1\u9001\u5230\uFF09\uFF0C\u672C\u8F6E\u8DD1\u5B8C\u4F1A\u4F5C\u4E3A\u4E0B\u4E00\u8F6E\u53D1\u51FA\u3002`);
266605
+ println(" \u5B83\u8FD9\u4E00\u8F6E\u53EF\u80FD\u8FD8\u5728\u6309\u65E7\u65B9\u5411\u505A\u2014\u2014\u8981\u4E0D\u8981\u5728\u8FD9\u6761\u5BF9\u8BDD\u91CC\u5148\u8DDF\u7528\u6237\u8BF4\u660E\uFF0C\u4F60\u81EA\u5DF1\u5224\u65AD\u3002");
266606
+ } else {
266607
+ println(`\u5DF2\u8F6C\u8FBE\u7ED9 ${who}\u3002`);
266608
+ }
265079
266609
  if (fileArgs.length) println(` \u4E00\u8D77\u5E26\u8FC7\u53BB\u7684\u6587\u4EF6\uFF1A${fileArgs.join("\u3001")}`);
265080
266610
  println("\u8FD9\u4E00\u8F6E\u5230\u6B64\u4E3A\u6B62\uFF1A\u7ED3\u679C\u4F1A\u81EA\u5DF1\u56DE\u5230\u8FD9\u6761\u5BF9\u8BDD\uFF0C\u4E0D\u8981\u5728\u8FD9\u91CC\u7B49\u3002");
265081
266611
  break;
@@ -267871,7 +269401,7 @@ function shimScript() {
267871
269401
  }
267872
269402
 
267873
269403
  // src/index.ts
267874
- var PKG_VERSION = true ? "2.2.1" : "dev";
269404
+ var PKG_VERSION = true ? "2.2.3" : "dev";
267875
269405
  var LOCAL_BIN = localBin();
267876
269406
  var NPM_PREFIX = npmPrefix();
267877
269407
  var INSTANCE = DEFAULT_INSTANCE;