oasis_test_v2 2.2.2 → 2.2.4

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 +1578 -338
  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
@@ -11289,6 +11363,7 @@ var init_store_postgres = __esm({
11289
11363
  proposalReason: w2.proposal_reason ?? null,
11290
11364
  lastActivityAt: iso(w2.last_activity_at),
11291
11365
  frozenIssueIds: w2.frozen_issue_ids ?? null,
11366
+ handledIssueIds: w2.handled_issue_ids ?? null,
11292
11367
  agentHandoffAttemptId: w2.agent_handoff_attempt_id ?? null,
11293
11368
  businessHandoffStatus: w2.business_handoff_status ?? null,
11294
11369
  businessHandoffRecordedAt: iso(w2.business_handoff_recorded_at),
@@ -11494,8 +11569,8 @@ var init_store_postgres = __esm({
11494
11569
  (id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
11495
11570
  outcome_detail,session_ref,continues_work_id,parent_work_id,reply_to_issue_id,output_version_no,
11496
11571
  node_version,conclusion,proposal_reason,last_activity_at,frozen_issue_ids,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
11497
- acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason,lane)
11498
- 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)
11499
11574
  ON CONFLICT (id) DO NOTHING`,
11500
11575
  [
11501
11576
  w2.id,
@@ -11530,13 +11605,14 @@ var init_store_postgres = __esm({
11530
11605
  w2.rejectedReason,
11531
11606
  w2.overrideBy,
11532
11607
  w2.overrideReason,
11533
- w2.lane
11608
+ w2.lane,
11609
+ w2.handledIssueIds ? JSON.stringify(w2.handledIssueIds) : null
11534
11610
  ]
11535
11611
  );
11536
11612
  break;
11537
11613
  }
11538
11614
  case "work.update":
11539
- 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 });
11540
11616
  break;
11541
11617
  case "artifact.replace":
11542
11618
  await this.c.query(`DELETE FROM ${this.s}.work_artifacts WHERE work_id = $1`, [m2.workId]);
@@ -11866,7 +11942,7 @@ function newNode(id, workorderId, at, version2) {
11866
11942
  updatedAt: at
11867
11943
  };
11868
11944
  }
11869
- function markNodeForRetry(state, nodeId, at) {
11945
+ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
11870
11946
  const node2 = state.node(nodeId);
11871
11947
  if (!node2) return;
11872
11948
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
@@ -11874,7 +11950,7 @@ function markNodeForRetry(state, nodeId, at) {
11874
11950
  state.updateWork(lw.id, { retryAt: at, status: "retry" });
11875
11951
  }
11876
11952
  if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
11877
- if (lw.acceptanceState === "rejected") state.updateWork(lw.id, { retryAt: at });
11953
+ if (resetRejectedBudget && lw.acceptanceState === "rejected") state.updateWork(lw.id, { retryAt: at });
11878
11954
  for (const req of state.requirementsOf(node2.id)) {
11879
11955
  if (!req.latestReviewId) continue;
11880
11956
  const r = state.review(req.latestReviewId);
@@ -12006,6 +12082,9 @@ var init_plan = __esm({
12006
12082
  const changed = node2.assigneeActorId !== e.assigneeActorId;
12007
12083
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
12008
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
+ }
12009
12088
  state.updateNode(e.nodeId, {
12010
12089
  assigneeActorId: e.assigneeActorId,
12011
12090
  assigneeRole: e.assigneeRole,
@@ -12035,7 +12114,7 @@ var init_plan = __esm({
12035
12114
  name: "plan/node-retry",
12036
12115
  kind: "plan.node_retry",
12037
12116
  apply(e, state, ctx) {
12038
- markNodeForRetry(state, e.nodeId, ctx.at);
12117
+ markNodeForRetry(state, e.nodeId, ctx.at, e.resetRejectedBudget === true);
12039
12118
  }
12040
12119
  };
12041
12120
  planUpdateFields = {
@@ -12168,7 +12247,13 @@ var init_workorder = __esm({
12168
12247
  apply(e, state, ctx) {
12169
12248
  state.updateWorkorder({ dispatchPaused: false, pausedBy: null, pausedReason: null, updatedAt: ctx.at });
12170
12249
  for (const node2 of state.liveNodes()) {
12171
- 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);
12172
12257
  }
12173
12258
  }
12174
12259
  };
@@ -12182,8 +12267,8 @@ var init_workorder = __esm({
12182
12267
  });
12183
12268
 
12184
12269
  // ../engine/src/handlers/work.ts
12185
- function latestProposedOf(state, nodeId) {
12186
- 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));
12187
12272
  return successes.length > 0 ? successes[successes.length - 1].id : null;
12188
12273
  }
12189
12274
  var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workContentEdited, workConclude, nodeLatestProposedRebased, workTimeout;
@@ -12198,6 +12283,7 @@ var init_work = __esm({
12198
12283
  name: "work/create",
12199
12284
  kind: "work.create",
12200
12285
  apply(e, state, ctx) {
12286
+ if (e.pauseGateVersion === 1 && state.workorder.dispatchPaused) return;
12201
12287
  if (state.work(e.workId)) return;
12202
12288
  const node2 = state.node(e.nodeId);
12203
12289
  if (!node2) return;
@@ -12223,6 +12309,7 @@ var init_work = __esm({
12223
12309
  replyToIssueId: e.replyToIssueId,
12224
12310
  outputVersionNo: null,
12225
12311
  conclusion: null,
12312
+ handledIssueIds: null,
12226
12313
  agentHandoffAttemptId: null,
12227
12314
  businessHandoffStatus: null,
12228
12315
  businessHandoffRecordedAt: null,
@@ -12262,6 +12349,7 @@ var init_work = __esm({
12262
12349
  outputVersionNo: null,
12263
12350
  conclusion: null,
12264
12351
  frozenIssueIds: frozen.length > 0 ? frozen : null,
12352
+ handledIssueIds: null,
12265
12353
  agentHandoffAttemptId: null,
12266
12354
  businessHandoffStatus: null,
12267
12355
  businessHandoffRecordedAt: null,
@@ -12296,6 +12384,7 @@ var init_work = __esm({
12296
12384
  // 会话侧也要杀(旧 agent 可能还挂着)。cancelPreviousWork 找不到/会话已退则 no-op。
12297
12385
  // 回信 work 不杀前会话(apply 不 kill 主链,节点已完结、无在跑会话),只派发。
12298
12386
  async effect(e, ctx) {
12387
+ if (e.pauseGateVersion === 1 && !await ctx.canDispatchWork(e.workorderId, e.workId)) return;
12299
12388
  if (!e.replyToIssueId) await ctx.cancelPreviousWork(e.nodeId, e.workId);
12300
12389
  await ctx.dispatchWork(e.workId, e.resumeEligible !== void 0 ? { resumeEligible: e.resumeEligible } : void 0);
12301
12390
  }
@@ -12326,7 +12415,7 @@ var init_work = __esm({
12326
12415
  if (node2.latestProposedWorkId) {
12327
12416
  const ls = state.work(node2.latestProposedWorkId);
12328
12417
  if (!ls || ls.status !== "success") {
12329
- state.updateNode(e.nodeId, { latestProposedWorkId: latestProposedOf(state, e.nodeId) });
12418
+ state.updateNode(e.nodeId, { latestProposedWorkId: latestProposedOf(state, e.nodeId, e.semanticsVersion === 2) });
12330
12419
  }
12331
12420
  }
12332
12421
  killNodeReviews(state, e.nodeId, ctx.at);
@@ -12356,17 +12445,34 @@ var init_work = __esm({
12356
12445
  }
12357
12446
  const businessFailed = w2.businessHandoffStatus === "not_delivered";
12358
12447
  const artifactCount = state.artifactsOf(e.workId).length;
12359
- 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));
12360
- 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));
12361
12450
  const status = succeeded ? "success" : "failed";
12362
- 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;
12363
12469
  state.updateWork(e.workId, {
12364
12470
  endedAt: ctx.at,
12365
12471
  outcome: businessFailed ? "failed" : e.outcome,
12366
12472
  outcomeDetail: businessFailed ? { ...e.detail ?? {}, reason: "agent business handoff not delivered" } : zeroOutputReason ? { ...e.detail ?? {}, reason: e.detail?.reason ?? zeroOutputReason } : e.detail,
12367
12473
  status
12368
12474
  });
12369
- if (status === "success" && !(e.semanticsVersion === 2 && artifactCount === 0)) {
12475
+ if (status === "success" && !((e.semanticsVersion ?? 1) >= 2 && artifactCount === 0)) {
12370
12476
  state.updateNode(w2.nodeId, { latestProposedWorkId: e.workId });
12371
12477
  }
12372
12478
  }
@@ -12390,7 +12496,7 @@ var init_work = __esm({
12390
12496
  } : {}
12391
12497
  });
12392
12498
  if (flipsToFailed && state.node(w2.nodeId)?.latestProposedWorkId === e.workId) {
12393
- state.updateNode(w2.nodeId, { latestProposedWorkId: latestProposedOf(state, w2.nodeId) });
12499
+ state.updateNode(w2.nodeId, { latestProposedWorkId: latestProposedOf(state, w2.nodeId, e.semanticsVersion === 2) });
12394
12500
  }
12395
12501
  }
12396
12502
  };
@@ -12508,6 +12614,7 @@ var init_issue = __esm({
12508
12614
  "../engine/src/handlers/issue.ts"() {
12509
12615
  "use strict";
12510
12616
  init_plan();
12617
+ init_letters();
12511
12618
  issueCreate = {
12512
12619
  name: "issue/create",
12513
12620
  kind: "issue.create",
@@ -12578,6 +12685,11 @@ var init_issue = __esm({
12578
12685
  // 缺失(人 / chat 会话 / 存量事件)落 null,判据按"不是节点发言"处理。
12579
12686
  viaWorkId: e.viaNodeId ? state.node(e.viaNodeId)?.latestWorkId ?? null : null
12580
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
+ }
12581
12693
  }
12582
12694
  };
12583
12695
  issueResolve = {
@@ -12585,7 +12697,13 @@ var init_issue = __esm({
12585
12697
  kind: "issue.resolve",
12586
12698
  apply(e, state, ctx) {
12587
12699
  const issue2 = state.issue(e.issueId);
12588
- 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;
12589
12707
  const humanResolution = e.by.startsWith("actor:human:") && e.by === ctx.actorId;
12590
12708
  if (e.semanticsVersion === 2 && issue2.kind === "escalation" && !humanResolution) {
12591
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");
@@ -12600,7 +12718,7 @@ var init_issue = __esm({
12600
12718
  resolvedNote: e.note,
12601
12719
  resolvedViaWorkId: e.viaWorkId
12602
12720
  });
12603
- if (issue2.kind === "gap" && (e.semanticsVersion !== 2 || humanResolution)) {
12721
+ if (issue2.kind === "gap" && e.closeRelatedEscalations !== false && (e.semanticsVersion !== 2 || humanResolution)) {
12604
12722
  for (const other of state.allIssues()) {
12605
12723
  if (other.kind === "escalation" && other.resolvedAt === null && other.workorderId === issue2.workorderId && other.gapId === issue2.id) {
12606
12724
  state.updateIssue(other.id, {
@@ -12618,7 +12736,7 @@ var init_issue = __esm({
12618
12736
  }
12619
12737
  if (issue2.kind === "gap" || issue2.kind === "escalation") {
12620
12738
  const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
12621
- if (nodeId) markNodeForRetry(state, nodeId, ctx.at);
12739
+ if (nodeId) markNodeForRetry(state, nodeId, ctx.at, e.resetRejectedBudget === true);
12622
12740
  }
12623
12741
  }
12624
12742
  };
@@ -12656,6 +12774,7 @@ var init_review = __esm({
12656
12774
  name: "review/create",
12657
12775
  kind: "review.create",
12658
12776
  apply(e, state, ctx) {
12777
+ if (e.pauseGateVersion === 1 && state.workorder.dispatchPaused) return;
12659
12778
  const work = state.work(e.targetWorkId);
12660
12779
  if (!work) return;
12661
12780
  const dup = state.reviewsOf(e.targetWorkId).some(
@@ -12691,6 +12810,7 @@ var init_review = __esm({
12691
12810
  }
12692
12811
  },
12693
12812
  async effect(e, ctx) {
12813
+ if (e.pauseGateVersion === 1 && !await ctx.canDispatchReview(e.workorderId, e.reviewId)) return;
12694
12814
  if (e.reviewerIsAgent) await ctx.dispatchReview(e.reviewId, e.nodeId, e.reviewerActorId);
12695
12815
  else await ctx.notify([e.reviewerActorId], { kind: "review-requested", reviewId: e.reviewId });
12696
12816
  }
@@ -12997,11 +13117,28 @@ var init_handlers = __esm({
12997
13117
  });
12998
13118
 
12999
13119
  // ../engine/src/bus.ts
13000
- function versionEscalationResolution(event) {
13001
- if (event.kind === "work.accept" || event.kind === "issue.resolve") {
13002
- 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;
13003
13141
  }
13004
- return event;
13005
13142
  }
13006
13143
  function emptySnapshotPlaceholder(record8) {
13007
13144
  return {
@@ -13209,7 +13346,7 @@ var init_bus = __esm({
13209
13346
  workorderId: input.workorderId,
13210
13347
  actorId: input.actorId,
13211
13348
  handActorId: input.handActorId ?? null,
13212
- event: versionEscalationResolution(input.event),
13349
+ event: versionCommandEvent(input.event),
13213
13350
  causedBySeq: null,
13214
13351
  origin: "command",
13215
13352
  dedupKey: input.dedupKey ?? null,
@@ -13234,6 +13371,32 @@ var init_bus = __esm({
13234
13371
  }
13235
13372
  }
13236
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
+ }
13237
13400
  /* ═══════════════════════ 调度器主循环 ═══════════════════════ */
13238
13401
  /**
13239
13402
  * 启动调度器——永不返回。
@@ -13469,7 +13632,7 @@ var init_bus = __esm({
13469
13632
  workorderId: record8.workorderId,
13470
13633
  actorId: SYSTEM_ACTOR,
13471
13634
  handActorId: null,
13472
- event: versionEscalationResolution(event),
13635
+ event: versionCommandEvent(event),
13473
13636
  causedBySeq: record8.seq,
13474
13637
  origin: "apply",
13475
13638
  dedupKey: null,
@@ -13500,6 +13663,18 @@ var init_bus = __esm({
13500
13663
  // ★ 注入 workorderId:派发侧读模型没追上时,据它指名重读那张工单(见 EngineIO.dispatchWork 注释)。
13501
13664
  dispatchWork: (id, opts) => this.io.dispatchWork(id, { ...opts, workorderId: record8.workorderId }),
13502
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
+ },
13503
13678
  cancelSession: (ref2) => this.io.cancelSession(ref2),
13504
13679
  // ★ work.create 的「kill 旧会话」:apply 已把该节点上一个 latest_work 置 deadAt(会话可能还挂着),
13505
13680
  // 这里按 store 找它 cancel。找不到 / 不是刚被 kill / 会话已退 → no-op。这是设计「节点重新运行 = 自动
@@ -13566,7 +13741,7 @@ var init_bus = __esm({
13566
13741
  workorderId: record8.workorderId,
13567
13742
  actorId: b2.actorId ?? SYSTEM_ACTOR,
13568
13743
  handActorId: null,
13569
- event: versionEscalationResolution(b2.event),
13744
+ event: versionCommandEvent(b2.event),
13570
13745
  causedBySeq: record8.seq,
13571
13746
  origin: "effect",
13572
13747
  dedupKey: b2.dedupKey ?? null,
@@ -13604,7 +13779,7 @@ var init_bus = __esm({
13604
13779
  workorderId,
13605
13780
  actorId: SYSTEM_ACTOR,
13606
13781
  handActorId: null,
13607
- event: versionEscalationResolution(event),
13782
+ event: versionCommandEvent(event),
13608
13783
  causedBySeq: null,
13609
13784
  origin: "apply",
13610
13785
  dedupKey: null,
@@ -13656,7 +13831,7 @@ var init_bus = __esm({
13656
13831
  workorderId: record8.workorderId,
13657
13832
  actorId: SYSTEM_ACTOR,
13658
13833
  handActorId: null,
13659
- event: versionEscalationResolution(event),
13834
+ event: versionCommandEvent(event),
13660
13835
  causedBySeq: record8.seq,
13661
13836
  origin: "apply",
13662
13837
  dedupKey: null,
@@ -14502,6 +14677,7 @@ async function assembleContext(args) {
14502
14677
  const siblingParts = args.part !== void 0 ? (artifact.parts ?? []).filter((p2) => p2.name !== args.part) : [];
14503
14678
  const consumers = consumersOf(model, artifactId).filter((c) => c.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE);
14504
14679
  const staleSet = staleSetOf(model, artifactId);
14680
+ const acceptedHead = concludedHead(model, artifactId);
14505
14681
  const head = artifact.currentRev;
14506
14682
  const headRev = head !== null ? model.revisions.get(head) : null;
14507
14683
  const headSpread = headRev !== null ? await spreadRevisionContent(blobs, headRev, files, "deliverable") : null;
@@ -14817,10 +14993,19 @@ ${manifestDiff(baseC, newC)}
14817
14993
  } else if (head === null) {
14818
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`);
14819
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");
14820
14999
  } else {
14821
15000
  whyLines.push(`**\u7EE7\u7EED\u5B8C\u5584**\uFF1A\u4E0A\u6B21\u672A conclude\u3002`);
14822
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`);
14823
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
+ }
14824
15009
  if (pendingConcludeAttempt(model, artifactId) !== null) {
14825
15010
  whyLines.push(
14826
15011
  ``,
@@ -18050,7 +18235,7 @@ function bridgeLoadReadModel(input) {
18050
18235
  const art = nodeToArtifact(node2, edgesByTarget.get(node2.id) ?? [], requirementsByNode.get(node2.id) ?? []);
18051
18236
  const lw = node2.latestWorkId ? workById.get(node2.latestWorkId) : void 0;
18052
18237
  if (lw) {
18053
- 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");
18054
18239
  const hasConclusionWithoutResponse = lw.conclusion !== null && lw.conclusion !== void 0 && st === "running" && !lw.endedAt;
18055
18240
  let uiSt;
18056
18241
  if (st === "retry") uiSt = null;
@@ -18553,22 +18738,18 @@ ${res.candidates.map((c) => ` - ${c.type}:${c.title ?? "(\u65E0title)"} \u2192
18553
18738
  return { ...plan, ops };
18554
18739
  }
18555
18740
  function buildConstituents(model, plan, schema, roleIndex) {
18556
- const already = new Set(
18557
- plan.ops.filter((o) => o.action === "unlink").map((o) => `${o.artifactId} ${o.to}`)
18558
- );
18741
+ const inputs = new Map([...model.artifacts].map(([id, art]) => [id, new Set(art.inputs.map((i) => i.to))]));
18559
18742
  const out = [];
18560
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);
18561
18747
  if (c.kind === "seal" && c.payload.reason === "cancelled") {
18562
- for (const edge of edgesTouching(model, c.artifactId)) {
18563
- const key = `${edge.artifactId} ${edge.to}`;
18564
- if (already.has(key)) continue;
18565
- already.add(key);
18566
- out.push({
18567
- artifactId: edge.artifactId,
18568
- kind: "unlink_input",
18569
- target: edge.artifactId,
18570
- payload: { to: edge.to }
18571
- });
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
+ }
18572
18753
  }
18573
18754
  }
18574
18755
  out.push(c);
@@ -18907,6 +19088,10 @@ function filterReadModelByWorkspace(model, workspace) {
18907
19088
  pinMeta: new Map([...model.pinMeta].filter(([artId]) => myArtIds.has(artId)))
18908
19089
  };
18909
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
+ }
18910
19095
  async function mapWithConcurrency(items, limit, fn) {
18911
19096
  if (items.length === 0) return [];
18912
19097
  const out = new Array(items.length);
@@ -19542,6 +19727,7 @@ var init_kernel_bridge = __esm({
19542
19727
  }
19543
19728
  if (!workId) {
19544
19729
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
19730
+ assertWorkorderDispatchActive(snap);
19545
19731
  const nodeRow = snap?.nodes.find((n) => n.id === args.artifactId);
19546
19732
  const headId = nodeRow?.latestWorkId ?? null;
19547
19733
  if (headId) {
@@ -19963,6 +20149,30 @@ var init_kernel_bridge = __esm({
19963
20149
  });
19964
20150
  }
19965
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
+ }
19966
20176
  const ann = this.model.annotations.get(args.annotationId);
19967
20177
  if (!ann) throw new Error(`annotation not found: ${args.annotationId}`);
19968
20178
  if (isHumanChangeRequest(ann) && args.actor.startsWith("actor:agent:") && args.resolution === "resolved" && !args.via) {
@@ -19981,7 +20191,9 @@ var init_kernel_bridge = __esm({
19981
20191
  state: args.resolution === "wontfix" ? "closed" : "resolved",
19982
20192
  by: args.actor,
19983
20193
  note: args.note ?? null,
19984
- viaWorkId: args.via ?? null
20194
+ viaWorkId: args.via ?? null,
20195
+ ...args.viaNode ? { viaNodeId: args.viaNode } : {},
20196
+ ...args.resolution === "acknowledged" ? { acknowledged: true } : {}
19985
20197
  }
19986
20198
  });
19987
20199
  }
@@ -20047,7 +20259,7 @@ var init_kernel_bridge = __esm({
20047
20259
  for (const [artifactId, gaps] of this.model.gaps) {
20048
20260
  if (gaps.some((g2) => g2.gapId === args.gapId && !g2.resolved)) {
20049
20261
  const wid = this.wo(artifactId);
20050
- await this.commit({
20262
+ const events = [{
20051
20263
  companyId: "",
20052
20264
  workorderId: wid,
20053
20265
  actorId: args.actor,
@@ -20060,16 +20272,31 @@ var init_kernel_bridge = __esm({
20060
20272
  note: args.note ?? null,
20061
20273
  viaWorkId: null
20062
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
+ }
20063
20296
  });
20064
- if (args.rework) {
20065
- await this.annotate({
20066
- artifactId,
20067
- actor: args.actor,
20068
- kind: "comment",
20069
- annotationId: `ann:rework:${args.gapId}`,
20070
- body: args.note ?? "\u7F3A\u53E3\u5DF2\u89E3\u51B3\uFF0C\u8BF7\u6309\u7B54\u590D\u91CD\u505A"
20071
- });
20072
- }
20297
+ await this.bus.submitBatchAndProcess(events);
20298
+ this.markWorkorderDirty(wid);
20299
+ await this.refreshTracked();
20073
20300
  return;
20074
20301
  }
20075
20302
  }
@@ -20118,6 +20345,57 @@ var init_kernel_bridge = __esm({
20118
20345
  }
20119
20346
  });
20120
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
+ }
20121
20399
  async resolveEscalation(args) {
20122
20400
  if (!args.actor.startsWith("actor:human:")) {
20123
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");
@@ -20322,6 +20600,7 @@ var init_kernel_bridge = __esm({
20322
20600
  async rerunNode(args) {
20323
20601
  const wid = this.wo(args.artifactId);
20324
20602
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
20603
+ assertWorkorderDispatchActive(snap);
20325
20604
  const node2 = snap?.nodes.find((n) => n.id === args.artifactId);
20326
20605
  if (!node2 || node2.cancelledAt) throw new KernelError(`\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u4F5C\u5E9F\uFF1A${args.artifactId}`, "not-found");
20327
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");
@@ -20371,6 +20650,7 @@ var init_kernel_bridge = __esm({
20371
20650
  async rereviewNode(args) {
20372
20651
  const wid = this.wo(args.artifactId);
20373
20652
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
20653
+ assertWorkorderDispatchActive(snap);
20374
20654
  const node2 = snap?.nodes.find((n) => n.id === args.artifactId);
20375
20655
  if (!node2 || node2.cancelledAt) throw new KernelError(`\u8282\u70B9\u4E0D\u5B58\u5728\u6216\u5DF2\u4F5C\u5E9F\uFF1A${args.artifactId}`, "not-found");
20376
20656
  const lw = node2.latestWorkId ? snap.works.find((w2) => w2.id === node2.latestWorkId) : null;
@@ -20573,12 +20853,9 @@ var init_kernel_bridge = __esm({
20573
20853
  throw new Error(`resolveEscalation \u6388\u6743\u5361\u7F3A escalationId\uFF1A${args.draftId}`);
20574
20854
  }
20575
20855
  const reason = typeof card2.commandArgs?.["reason"] === "string" ? card2.commandArgs["reason"] : void 0;
20576
- await this.resolveEscalation({
20577
- artifactId,
20578
- actor: args.actor,
20579
- escalationId,
20580
- ...reason !== void 0 ? { reason } : {}
20581
- });
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 } : {} });
20582
20859
  } else {
20583
20860
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u4E0D\u652F\u6301\u6267\u884C ${card2.command}`);
20584
20861
  }
@@ -20622,11 +20899,29 @@ var init_kernel_bridge = __esm({
20622
20899
  if (preview2.changeClass === "C" && !_actor.startsWith("actor:human:")) {
20623
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");
20624
20901
  }
20625
- 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";
20626
20903
  let applied = 0;
20627
20904
  const unsupported = [];
20628
20905
  const reviewerStaffing = [];
20629
- 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 });
20630
20925
  for (const op of plan.ops) {
20631
20926
  switch (op.action) {
20632
20927
  case "spawn": {
@@ -20658,7 +20953,7 @@ var init_kernel_bridge = __esm({
20658
20953
  // ★ executionEnv 同走 fields jsonb(与路径 A 对称——两条 spawn 路径必须一致,前车之鉴:类型默认评审闸)。
20659
20954
  ...op.executionEnv !== void 0 ? { executionEnv: op.executionEnv } : {}
20660
20955
  } : opFields;
20661
- await this.commit({
20956
+ await queue({
20662
20957
  companyId: "",
20663
20958
  workorderId: wid,
20664
20959
  actorId: _actor,
@@ -20688,27 +20983,64 @@ var init_kernel_bridge = __esm({
20688
20983
  activateWorkorder: false
20689
20984
  }
20690
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
+ });
20691
20998
  applied++;
20692
20999
  break;
20693
21000
  }
20694
- case "link":
20695
- 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 });
20696
21005
  applied++;
20697
21006
  break;
20698
- case "unlink":
20699
- 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);
20700
21012
  applied++;
20701
21013
  break;
21014
+ }
20702
21015
  case "assign":
20703
- 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 });
20704
21017
  applied++;
20705
21018
  break;
20706
21019
  case "seal":
20707
- 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 });
20708
21027
  applied++;
20709
21028
  break;
20710
21029
  case "annotate":
20711
- 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
+ });
20712
21044
  applied++;
20713
21045
  break;
20714
21046
  case "edit": {
@@ -20716,7 +21048,7 @@ var init_kernel_bridge = __esm({
20716
21048
  const opReviewers = op.reviewers;
20717
21049
  const editReviewRoles = op.reviewRoles;
20718
21050
  if (artId && (opReviewers || editReviewRoles !== void 0)) {
20719
- const art = this.model.artifacts.get(artId);
21051
+ const art = artifacts.get(artId);
20720
21052
  const effectiveRoles = editReviewRoles !== void 0 ? editReviewRoles === null ? void 0 : editReviewRoles : art?.reviewRoles;
20721
21053
  const effectiveExplicit = opReviewers ?? (art?.reviewers ?? []).map((r) => ({ actor: r.actor, source: r.source }));
20722
21054
  const { reviewers: nextReqs, staffingIssues } = resolveNodeReviewers({
@@ -20730,13 +21062,13 @@ var init_kernel_bridge = __esm({
20730
21062
  reviewerStaffing.push({ nodeId: artId, type: art?.type ?? "", role: issue2.role, from: issue2.from });
20731
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`);
20732
21064
  }
20733
- await this.commit({
21065
+ await queue({
20734
21066
  companyId: "",
20735
- workorderId: this.wo(artId),
21067
+ workorderId: woFor(artId),
20736
21068
  actorId: _actor,
20737
21069
  event: {
20738
21070
  kind: "plan.update_review_requirements",
20739
- workorderId: this.wo(artId),
21071
+ workorderId: woFor(artId),
20740
21072
  nodeId: artId,
20741
21073
  reviewers: nextReqs
20742
21074
  }
@@ -20745,15 +21077,15 @@ var init_kernel_bridge = __esm({
20745
21077
  const opTitle = op.title;
20746
21078
  const opDescription = op.description;
20747
21079
  if (artId && (opTitle !== void 0 || opDescription !== void 0)) {
20748
- await this.commit({
21080
+ await queue({
20749
21081
  companyId: "",
20750
- workorderId: this.wo(artId),
21082
+ workorderId: woFor(artId),
20751
21083
  actorId: _actor,
20752
21084
  event: {
20753
21085
  kind: "plan.update_spec",
20754
- workorderId: this.wo(artId),
21086
+ workorderId: woFor(artId),
20755
21087
  nodeId: artId,
20756
- spec: opDescription ?? this.model.artifacts.get(artId)?.description ?? null,
21088
+ spec: opDescription ?? artifacts.get(artId)?.description ?? null,
20757
21089
  ...opTitle !== void 0 ? { title: opTitle } : {},
20758
21090
  policy: "auto"
20759
21091
  }
@@ -20761,21 +21093,30 @@ var init_kernel_bridge = __esm({
20761
21093
  }
20762
21094
  const fields = op.fields;
20763
21095
  if (artId && fields) {
20764
- const editDef = this.schema.get(this.model.artifacts.get(artId)?.type ?? "");
21096
+ const editDef = this.schema.get(artifacts.get(artId)?.type ?? "");
20765
21097
  const validated = editDef?.fieldDefs ? validateArtifactFields(fields, editDef.fieldDefs, "edit") : fields;
20766
- await this.commit({
21098
+ await queue({
20767
21099
  companyId: "",
20768
- workorderId: this.wo(artId),
21100
+ workorderId: woFor(artId),
20769
21101
  actorId: _actor,
20770
21102
  event: {
20771
21103
  kind: "plan.update_fields",
20772
- workorderId: this.wo(artId),
21104
+ workorderId: woFor(artId),
20773
21105
  nodeId: artId,
20774
21106
  fields: validated ?? {},
20775
21107
  merge: true
20776
21108
  }
20777
21109
  });
20778
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
+ });
20779
21120
  applied++;
20780
21121
  break;
20781
21122
  }
@@ -20784,11 +21125,16 @@ var init_kernel_bridge = __esm({
20784
21125
  if (!escOp.escalationId) {
20785
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`);
20786
21127
  }
20787
- await this.resolveEscalation({
20788
- artifactId: escOp.artifactId,
20789
- actor: _actor,
20790
- escalationId: escOp.escalationId,
20791
- ...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
20792
21138
  });
20793
21139
  applied++;
20794
21140
  break;
@@ -20798,7 +21144,7 @@ var init_kernel_bridge = __esm({
20798
21144
  break;
20799
21145
  }
20800
21146
  }
20801
- await this.commit({
21147
+ await queue({
20802
21148
  companyId: "",
20803
21149
  workorderId: wid,
20804
21150
  actorId: _actor,
@@ -20813,17 +21159,22 @@ var init_kernel_bridge = __esm({
20813
21159
  activateWorkorder: true
20814
21160
  }
20815
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();
20816
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
+ }
20817
21174
  if (typeof plan.resolvesProposal === "string") {
20818
21175
  this.draftCards.delete(plan.resolvesProposal);
20819
21176
  await this.refreshModel();
20820
21177
  }
20821
- if (unsupported.length > 0) {
20822
- throw new KernelError(
20823
- `\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`,
20824
- "unsupported-op"
20825
- );
20826
- }
20827
21178
  return {
20828
21179
  interventionId: `intervention:${(0, import_node_crypto3.randomUUID)()}`,
20829
21180
  applied,
@@ -20875,67 +21226,6 @@ var init_chat_system_message = __esm({
20875
21226
  }
20876
21227
  });
20877
21228
 
20878
- // ../server/src/build-info.ts
20879
- function findRepoRoot() {
20880
- let dir;
20881
- try {
20882
- dir = path.dirname((0, import_node_url2.fileURLToPath)(__esm_import_meta_url));
20883
- } catch {
20884
- return null;
20885
- }
20886
- for (let i = 0; i < 12; i++) {
20887
- if (fs.existsSync(path.join(dir, ".git"))) return dir;
20888
- const parent = path.dirname(dir);
20889
- if (parent === dir) break;
20890
- dir = parent;
20891
- }
20892
- return null;
20893
- }
20894
- function git(root, args) {
20895
- try {
20896
- const out = (0, import_node_child_process.execFileSync)("git", args, {
20897
- cwd: root,
20898
- encoding: "utf8",
20899
- timeout: 3e3,
20900
- stdio: ["ignore", "pipe", "ignore"]
20901
- });
20902
- const v2 = out.trim();
20903
- return v2 || null;
20904
- } catch {
20905
- return null;
20906
- }
20907
- }
20908
- function getBuildInfo() {
20909
- if (cached) return cached;
20910
- const envSha = process.env["OASIS_GIT_SHA"]?.trim();
20911
- const envBranch = process.env["OASIS_GIT_BRANCH"]?.trim();
20912
- let gitSha = envSha || null;
20913
- let gitBranch = envBranch || null;
20914
- if (!gitSha || !gitBranch) {
20915
- const root = findRepoRoot();
20916
- if (root) {
20917
- if (!gitSha) gitSha = git(root, ["rev-parse", "HEAD"]);
20918
- if (!gitBranch) {
20919
- const b2 = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
20920
- gitBranch = b2 === "HEAD" ? null : b2;
20921
- }
20922
- }
20923
- }
20924
- cached = { gitSha, gitBranch, startedAt: STARTED_AT };
20925
- return cached;
20926
- }
20927
- var import_node_child_process, fs, path, import_node_url2, STARTED_AT, cached;
20928
- var init_build_info = __esm({
20929
- "../server/src/build-info.ts"() {
20930
- "use strict";
20931
- import_node_child_process = require("node:child_process");
20932
- fs = __toESM(require("node:fs"), 1);
20933
- path = __toESM(require("node:path"), 1);
20934
- import_node_url2 = require("node:url");
20935
- STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
20936
- }
20937
- });
20938
-
20939
21229
  // ../server/src/facades/api/router.ts
20940
21230
  var ApiError, Router;
20941
21231
  var init_router = __esm({
@@ -21081,6 +21371,32 @@ async function saveArtifactContent(kernel, blobs, actor, input) {
21081
21371
  }
21082
21372
  return { ...view, contentRef, text: input.text };
21083
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
+ }
21084
21400
  var init_artifact_content2 = __esm({
21085
21401
  "../server/src/artifact-content.ts"() {
21086
21402
  "use strict";
@@ -21089,6 +21405,110 @@ var init_artifact_content2 = __esm({
21089
21405
  }
21090
21406
  });
21091
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
+
21092
21512
  // ../server/src/command-policy.ts
21093
21513
  function classifyCommand(command) {
21094
21514
  return COMMAND_POLICY[command] ?? { risk: "direct" };
@@ -21115,7 +21535,7 @@ var init_command_policy = __esm({
21115
21535
  // 重复关同一条是幂等的(kernel.resolveEscalation 内部按 escalationId 去重)
21116
21536
  // 「关错=静默停摆」的唯一防线是人在卡上一眼看出「关的是哪一条」——escalationId 与 reason 摘要必须上卡。
21117
21537
  // a 里 escalationId/artifactId 由 server.ts 起草时冻进 commandArgs(已四级解析),保证是真值、非 `?`。
21118
- 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`
21119
21539
  },
21120
21540
  gc: {
21121
21541
  risk: "forbidden",
@@ -23588,6 +24008,13 @@ function applyModelOverrides(base, overrides) {
23588
24008
  }
23589
24009
  return kept;
23590
24010
  }
24011
+ function clearModelDiscoveryCache(runtime, executablePath) {
24012
+ if (!runtime) {
24013
+ modelCache.clear();
24014
+ return;
24015
+ }
24016
+ modelCache.delete(discoveryCacheKey(runtime, executablePath));
24017
+ }
23591
24018
  function discoveryCacheKey(runtime, executablePath) {
23592
24019
  return `${runtime}:${executablePath ?? ""}`;
23593
24020
  }
@@ -23612,8 +24039,11 @@ async function withFallback(runtime, executablePath, discover, fallback) {
23612
24039
  }
23613
24040
  function claudeStaticModels() {
23614
24041
  return [
23615
- { id: "claude-opus-4-7", label: "Claude Opus 4.7", provider: "Anthropic", default: true },
24042
+ { id: "claude-opus-5", label: "Claude Opus 5", provider: "Anthropic" },
24043
+ { id: "claude-fable-5-1", label: "Claude Fable 5.1", provider: "Anthropic" },
24044
+ { id: "claude-fable-5", label: "Claude Fable 5", provider: "Anthropic" },
23616
24045
  { id: "claude-opus-4-8", label: "Claude Opus 4.8", provider: "Anthropic" },
24046
+ { id: "claude-opus-4-7", label: "Claude Opus 4.7", provider: "Anthropic" },
23617
24047
  { id: "claude-opus-4-6", label: "Claude Opus 4.6", provider: "Anthropic" },
23618
24048
  { id: "claude-sonnet-5", label: "Claude Sonnet 5", provider: "Anthropic" },
23619
24049
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "Anthropic" },
@@ -23623,14 +24053,12 @@ function claudeStaticModels() {
23623
24053
  }
23624
24054
  function codexStaticModels() {
23625
24055
  return [
23626
- { id: "gpt-5.5", label: "GPT-5.5", provider: "OpenAI", default: true },
23627
- { id: "gpt-5.5-mini", label: "GPT-5.5 mini", provider: "OpenAI" },
23628
- { id: "gpt-5.4", label: "GPT-5.4", provider: "OpenAI" },
23629
- { id: "gpt-5.4-mini", label: "GPT-5.4 mini", provider: "OpenAI" },
23630
- { id: "gpt-5.3-codex", label: "GPT-5.3 Codex", provider: "OpenAI" },
23631
- { id: "gpt-5", label: "GPT-5", provider: "OpenAI" },
23632
- { id: "o3", label: "o3", provider: "OpenAI" },
23633
- { id: "o3-mini", label: "o3-mini", provider: "OpenAI" }
24056
+ { id: "gpt-6-astra", label: "GPT-6-Astra", provider: "OpenAI", default: true },
24057
+ { id: "gpt-5.6-sol", label: "GPT-5.6-Sol", provider: "OpenAI" },
24058
+ { id: "gpt-5.6-terra", label: "GPT-5.6-Terra", provider: "OpenAI" },
24059
+ { id: "gpt-5.6-luna", label: "GPT-5.6-Luna", provider: "OpenAI" },
24060
+ { id: "gpt-5.5", label: "GPT-5.5", provider: "OpenAI" },
24061
+ { id: "gpt-5.2", label: "GPT-5.2", provider: "OpenAI" }
23634
24062
  ];
23635
24063
  }
23636
24064
  function geminiStaticModels() {
@@ -28452,6 +28880,16 @@ var init_chat_item_ledger = __esm({
28452
28880
  version: writeVersion
28453
28881
  });
28454
28882
  row.ord = created.ord;
28883
+ try {
28884
+ this.deps.onPersisted?.({
28885
+ itemId: id,
28886
+ ord: created.ord,
28887
+ version: writeVersion,
28888
+ role: input.role,
28889
+ text: input.text
28890
+ });
28891
+ } catch {
28892
+ }
28455
28893
  }, `create:${input.kind}`);
28456
28894
  return id;
28457
28895
  }
@@ -28580,17 +29018,21 @@ async function openAssistantRunningRow(deps) {
28580
29018
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
28581
29019
  if (typeof deps.chatStore.appendMessage !== "function") return void 0;
28582
29020
  const id = (0, import_node_crypto16.randomUUID)();
29021
+ const at = now();
28583
29022
  try {
28584
29023
  await deps.chatStore.appendMessage({
28585
29024
  id,
28586
29025
  sessionId: deps.chatSessionId,
28587
29026
  role: "assistant",
28588
29027
  content: "",
28589
- createdAt: now(),
29028
+ createdAt: at,
28590
29029
  status: "running",
28591
29030
  ...deps.runId ? { runId: deps.runId } : {},
28592
29031
  ...deps.turnId ? { turnId: deps.turnId } : {}
28593
29032
  });
29033
+ if (typeof deps.chatStore.updateSession === "function") {
29034
+ await deps.chatStore.updateSession(deps.chatSessionId, { touchedAt: at }).catch(() => void 0);
29035
+ }
28594
29036
  return id;
28595
29037
  } catch {
28596
29038
  return void 0;
@@ -141794,7 +142236,7 @@ function buildNodeTimeline(artifactId, snap, runIdByTarget) {
141794
142236
  if (w2.nodeId !== artifactId) continue;
141795
142237
  if (isReplyWork(w2)) continue;
141796
142238
  nodeWorkIds.add(w2.id);
141797
- const status = w2.status ?? (w2.retryAt ? "retry" : w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
142239
+ const status = w2.status ?? (w2.retryAt && w2.deadAt ? "retry" : w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
141798
142240
  items.push({
141799
142241
  type: "work",
141800
142242
  id: w2.id,
@@ -159475,6 +159917,58 @@ var init_live_chat = __esm({
159475
159917
  }
159476
159918
  }
159477
159919
  }
159920
+ /**
159921
+ * 把**用户自己那条消息**推上 v3 流(ADR-0510 D2 的收口)。
159922
+ *
159923
+ * ## 为什么需要
159924
+ *
159925
+ * 用户点发送时前端先画一条「乐观条」,它**没有服务端展示序**(D2:不许前端发明号),
159926
+ * 渲染时钉在列表末尾。这条规矩对「刚发完、还没有任何回复」是对的。
159927
+ *
159928
+ * 但服务端落账本时**立刻**给了这条 user item 一个 ord(它是这一轮第一个建的行,号最小),
159929
+ * 而这个号此前**只走快照**——`emitV3` 只服务 provider 事件(`server.ts` 那句注释:
159930
+ * 「不走 v3 流帧(那条只服务 assistant)」)。于是有个几秒的窗口:
159931
+ * agent 的正文带着更大的号从直播流到了、进入「有号」段,而用户那条还在「无号」段被钉在最后
159932
+ * ⇒ **agent 的回复显示在用户消息上面**,等下一次快照才跳回去。
159933
+ * 2026-09-09 用户现场原话:「过几秒钟页面刷新一下,agent 回复内容会回到用户消息下面」。
159934
+ *
159935
+ * 修法就是把这条也推上去:前端按 itemId(`oasis-user-optim:<csid>`,与乐观桶位同名)覆盖,
159936
+ * 拿到真号立刻归位,窗口从「到下一次快照」压到「一次 INSERT 往返」。
159937
+ *
159938
+ * ## 为什么不在前端算
159939
+ *
159940
+ * 试过的三种都被证伪(见 `chat-items-merge.ts` 里那段注释)。这次也不能用「记下创建时的最大号」:
159941
+ * 空对话首次发送时那个最大号是 0,随后到达的历史(号更大)会全部排到乐观条**后面**——
159942
+ * 正是 ADR D9 点名必须验的「空桶先发送」那一档。号只能由服务端给。
159943
+ *
159944
+ * ## 拿不到轮就静默跳过
159945
+ *
159946
+ * 本方法由账本的落库链回调触发,而那一刻这条会话的 live 轮可能还没 `start`
159947
+ * (`server.ts` 里 `recordUserSubmission` 在 `liveChat.start` 之前)。拿不到就不发——
159948
+ * 前端退回「等下一次快照」,与修复前一致,不会更坏。
159949
+ */
159950
+ publishUserItem(chatSessionId, item) {
159951
+ const turn = this.turns.get(chatSessionId);
159952
+ if (!turn) return false;
159953
+ const payload = { role: "user", text: item.text };
159954
+ if (item.clientSubmitId) payload.clientSubmitId = item.clientSubmitId;
159955
+ if (item.attachments?.length) payload.attachments = item.attachments;
159956
+ const frame = {
159957
+ protocolVersion: 3,
159958
+ streamId: turn.liveStreamId,
159959
+ turnId: this.wireTurnId(turn),
159960
+ itemId: item.itemId,
159961
+ itemType: "message",
159962
+ operation: "set_text",
159963
+ itemVersion: item.version,
159964
+ ...item.ord !== null ? { ord: item.ord } : {},
159965
+ offset: 0,
159966
+ payload
159967
+ };
159968
+ this.flushPendingV3(turn);
159969
+ this.publishV3(turn, this.assignV3Seq(turn, frame));
159970
+ return true;
159971
+ }
159478
159972
  emitV3TurnTerminal(turn, status) {
159479
159973
  if (status !== "done" && status !== "error") return;
159480
159974
  const operation = status === "done" ? "turn_completed" : "turn_failed";
@@ -194349,8 +194843,15 @@ function isTextual(contentType) {
194349
194843
  const t = contentType.toLowerCase();
194350
194844
  return t.startsWith("text/") || t.includes("json") || t.includes("xml") || t.includes("yaml") || t.includes("javascript");
194351
194845
  }
194352
- function toPrepared(rel, source, read) {
194353
- const base = { rel, source, size: read.size, contentType: read.contentType };
194846
+ function toPrepared(rel, source, read, src) {
194847
+ const base = {
194848
+ rel,
194849
+ source,
194850
+ size: read.size,
194851
+ contentType: read.contentType,
194852
+ ...src ? { srcSize: src.size } : {},
194853
+ ...src?.mtime ? { srcMtime: src.mtime } : {}
194854
+ };
194354
194855
  if (!isTextual(read.contentType)) return { ...base, base64: read.contentBase64 };
194355
194856
  return { ...base, text: Buffer.from(read.contentBase64, "base64").toString("utf8") };
194356
194857
  }
@@ -194387,7 +194888,7 @@ async function collectInboundFiles(port, parent, paths, limits, lenient) {
194387
194888
  if (!lenient) throw error2;
194388
194889
  skipped.push({ source, code: error2.code, message: error2.message });
194389
194890
  };
194390
- if (wanted.length === 0) return { prepared: [], skipped, totalBytes: 0 };
194891
+ if (wanted.length === 0) return { prepared: [], skipped, totalBytes: 0, unchanged: [] };
194391
194892
  if (!port) {
194392
194893
  const error2 = new DelegationFileError(
194393
194894
  "FILE_UNREADABLE",
@@ -194396,7 +194897,7 @@ async function collectInboundFiles(port, parent, paths, limits, lenient) {
194396
194897
  );
194397
194898
  if (!lenient) throw error2;
194398
194899
  for (const source of wanted) skipped.push({ source, code: error2.code, message: error2.message });
194399
- return { prepared: [], skipped, totalBytes: 0 };
194900
+ return { prepared: [], skipped, totalBytes: 0, unchanged: [] };
194400
194901
  }
194401
194902
  let accepted = wanted;
194402
194903
  if (wanted.length > limits.maxFiles) {
@@ -194474,10 +194975,10 @@ async function collectInboundFiles(port, parent, paths, limits, lenient) {
194474
194975
  used.add(rel);
194475
194976
  prepared.push(toPrepared(rel, source, read));
194476
194977
  }
194477
- return { prepared, skipped, totalBytes };
194978
+ return { prepared, skipped, totalBytes, unchanged: [] };
194478
194979
  }
194479
- async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FILE_LIMITS) {
194480
- const out = { prepared: [], skipped: [], totalBytes: 0 };
194980
+ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FILE_LIMITS, known) {
194981
+ const out = { prepared: [], skipped: [], totalBytes: 0, unchanged: [] };
194481
194982
  if (!port) return out;
194482
194983
  if (port.locate) {
194483
194984
  const located = await port.locate(child).catch((err) => ({
@@ -194537,7 +195038,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
194537
195038
  for (const entry of level.entries) {
194538
195039
  const childPath = `${dir}/${entry.name}`;
194539
195040
  if (entry.type === "dir") queue.push(childPath);
194540
- else files.push({ rel: childPath, size: entry.size });
195041
+ else files.push({ rel: childPath, size: entry.size, ...entry.mtime ? { mtime: entry.mtime } : {} });
194541
195042
  }
194542
195043
  }
194543
195044
  for (const dir of queue) {
@@ -194553,6 +195054,12 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
194553
195054
  out.skipped.push({ source: file.rel, code: "PATH_REJECTED", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF0C\u6CA1\u642C" });
194554
195055
  continue;
194555
195056
  }
195057
+ const target = `${DELEGATION_OUTBOUND_DIR}/${dirName}/${relative5}`;
195058
+ const seen = known?.get(target);
195059
+ if (seen && file.mtime && seen.size === file.size && seen.mtime === file.mtime) {
195060
+ out.unchanged.push(target);
195061
+ continue;
195062
+ }
194556
195063
  if (out.prepared.length >= limits.maxFiles) {
194557
195064
  out.skipped.push({ source: file.rel, code: "FILE_QUOTA_EXCEEDED", message: `\u8D85\u8FC7\u4E00\u6B21 ${limits.maxFiles} \u4E2A\u7684\u4E0A\u9650\uFF0C\u6CA1\u642C` });
194558
195065
  continue;
@@ -194571,7 +195078,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
194571
195078
  continue;
194572
195079
  }
194573
195080
  out.totalBytes += read.size;
194574
- out.prepared.push(toPrepared(`${DELEGATION_OUTBOUND_DIR}/${dirName}/${relative5}`, file.rel, read));
195081
+ out.prepared.push(toPrepared(target, file.rel, read, { size: file.size, ...file.mtime ? { mtime: file.mtime } : {} }));
194575
195082
  }
194576
195083
  return out;
194577
195084
  }
@@ -194628,6 +195135,31 @@ function digestOf(text5) {
194628
195135
  const line = text5.trim().replace(/\s+/g, " ");
194629
195136
  return line.length > DIGEST_CHARS ? `${line.slice(0, DIGEST_CHARS)}\u2026` : line;
194630
195137
  }
195138
+ function childRoundMessageIds(childSessionId, round) {
195139
+ const suffix = round <= 1 ? "" : `:${round}`;
195140
+ return {
195141
+ inputId: deterministicUuid(`delegation-input:${childSessionId}${suffix}`),
195142
+ placeholderId: deterministicUuid(`delegation-placeholder:${childSessionId}${suffix}`)
195143
+ };
195144
+ }
195145
+ async function seedChildRoundRows(chatStore, childSessionId, round, prompt2, now) {
195146
+ const ids2 = childRoundMessageIds(childSessionId, round);
195147
+ await chatStore.appendMessageOnce({
195148
+ id: ids2.inputId,
195149
+ sessionId: childSessionId,
195150
+ role: "user",
195151
+ content: prompt2,
195152
+ createdAt: now
195153
+ });
195154
+ await chatStore.appendMessageOnce({
195155
+ id: ids2.placeholderId,
195156
+ sessionId: childSessionId,
195157
+ role: "assistant",
195158
+ content: "",
195159
+ status: "running",
195160
+ createdAt: now
195161
+ });
195162
+ }
194631
195163
  var import_node_crypto32, DelegationError, TITLE_TASK_CHARS, DIGEST_CHARS, PENDING_QUEUE_OWNER, DISPATCH_LEASE_MS, CONSECUTIVE_FAIL_LIMIT, DelegationService;
194632
195164
  var init_service3 = __esm({
194633
195165
  "../server/src/domains/delegations/service.ts"() {
@@ -195053,21 +195585,7 @@ var init_service3 = __esm({
195053
195585
  createdAt: now
195054
195586
  });
195055
195587
  }
195056
- await chatStore.appendMessageOnce({
195057
- id: deterministicUuid(`delegation-input:${record8.childSessionId}`),
195058
- sessionId: record8.childSessionId,
195059
- role: "user",
195060
- content: task,
195061
- createdAt: now
195062
- });
195063
- await chatStore.appendMessageOnce({
195064
- id: deterministicUuid(`delegation-placeholder:${record8.childSessionId}`),
195065
- sessionId: record8.childSessionId,
195066
- role: "assistant",
195067
- content: "",
195068
- status: "running",
195069
- createdAt: now
195070
- });
195588
+ await seedChildRoundRows(chatStore, record8.childSessionId, 1, task, now);
195071
195589
  }
195072
195590
  /**
195073
195591
  * 去程文件:读父会话工作区、翻成 §8.2 的同步错误码。
@@ -195227,7 +195745,7 @@ var init_service3 = __esm({
195227
195745
  settledAt: failedAt
195228
195746
  }).catch(() => null);
195229
195747
  const chatStore = await this.options.resolveChatSessions(companyId).catch(() => null);
195230
- await chatStore?.updateMessage(deterministicUuid(`delegation-placeholder:${record8.childSessionId}`), {
195748
+ await chatStore?.updateMessage(childRoundMessageIds(record8.childSessionId, record8.roundSummaries.length + 1).placeholderId, {
195231
195749
  content: `\u6D3E\u53D1\u6CA1\u80FD\u8D77\u6765\uFF1A${message}`,
195232
195750
  status: "error",
195233
195751
  completedAt: failedAt
@@ -195276,7 +195794,7 @@ var init_service3 = __esm({
195276
195794
  const pending = await store.countPendingTouches(fresh.id).catch(() => 0);
195277
195795
  const stopLoss = consecutiveFails >= CONSECUTIVE_FAIL_LIMIT;
195278
195796
  const chatStore = await this.options.resolveChatSessions(companyId).catch(() => null);
195279
- await chatStore?.updateMessage(deterministicUuid(`delegation-placeholder:${fresh.childSessionId}`), {
195797
+ await chatStore?.updateMessage(childRoundMessageIds(fresh.childSessionId, round).placeholderId, {
195280
195798
  content: text5 || (failureKind ? `\u8FD9\u4E00\u8F6E\u6CA1\u8DD1\u5B8C\uFF1A${error2}` : ""),
195281
195799
  status: failureKind ? "error" : "done",
195282
195800
  completedAt: at
@@ -195288,11 +195806,13 @@ var init_service3 = __esm({
195288
195806
  });
195289
195807
  return;
195290
195808
  }
195291
- const settled = await store.appendRoundSummary(fresh.id, newSummary, {
195809
+ const settledSeqAtRound = fresh.settledSeq + 1;
195810
+ const settledSummary = { ...newSummary, settledSeq: settledSeqAtRound };
195811
+ const settled = await store.appendRoundSummary(fresh.id, settledSummary, {
195292
195812
  consecutiveFails,
195293
195813
  state: failureKind ? "failed" : "done",
195294
195814
  failureKind: failureKind ?? null,
195295
- settledSeq: fresh.settledSeq + 1,
195815
+ settledSeq: settledSeqAtRound,
195296
195816
  settledAt: at
195297
195817
  }).catch(() => null);
195298
195818
  if (settled) await this.notifySettled(settled, companyId);
@@ -195321,17 +195841,19 @@ var init_service3 = __esm({
195321
195841
  const record8 = await store.getByChildSession(childSessionId).catch(() => null);
195322
195842
  if (!record8 || record8.state !== "running") return record8;
195323
195843
  const at = this.now();
195844
+ const settledSeqAtRound = record8.settledSeq + 1;
195324
195845
  const newSummary = {
195325
195846
  round: (record8.roundSummaries[record8.roundSummaries.length - 1]?.round ?? 0) + 1,
195326
195847
  text: `\u8FD9\u4E00\u8F6E\u88AB\u65F6\u949F\u95ED\u5408\uFF1A${reason}`,
195327
- at
195848
+ at,
195849
+ settledSeq: settledSeqAtRound
195328
195850
  };
195329
195851
  const settled = await store.appendRoundSummary(record8.id, newSummary, {
195330
195852
  state: "failed",
195331
195853
  // 退出帧丢失后被时钟闭合 = `interrupted`(§9),不是 `not-started`——它确实起来过。
195332
195854
  failureKind: "interrupted",
195333
195855
  consecutiveFails: record8.consecutiveFails + 1,
195334
- settledSeq: record8.settledSeq + 1,
195856
+ settledSeq: settledSeqAtRound,
195335
195857
  settledAt: at
195336
195858
  }).catch(() => null);
195337
195859
  if (settled) await this.notifySettled(settled, companyId);
@@ -195417,6 +195939,15 @@ var init_service3 = __esm({
195417
195939
  onDispatched: async () => {
195418
195940
  await queue.confirm();
195419
195941
  await store.markTouchesConsumed(record8.id, touches.map((touch) => touch.messageId), this.now());
195942
+ await seedChildRoundRows(
195943
+ chatStore,
195944
+ record8.childSessionId,
195945
+ record8.roundSummaries.length + 1,
195946
+ body2,
195947
+ this.now()
195948
+ ).catch((err) => {
195949
+ console.warn(`[delegation] \u5B50\u4F1A\u8BDD\u8865\u8F6E\u6B21\u884C\u5931\u8D25\uFF08${record8.id} round=${record8.roundSummaries.length + 1}\uFF09: ${String(err)}`);
195950
+ });
195420
195951
  },
195421
195952
  /**
195422
195953
  * **没送到就把正文放回可读态**(见 `dispatchExpert` 的 `onNotDispatched`)。
@@ -195515,6 +196046,10 @@ var init_service3 = __esm({
195515
196046
  const inbound = await this.readInboundFiles(parent, files);
195516
196047
  const inboundBundle = toBundleFiles(inbound.prepared);
195517
196048
  let delivered = false;
196049
+ let deliveryReasonCode;
196050
+ if (!this.options.appendToChild) {
196051
+ deliveryReasonCode = "no-append-port";
196052
+ }
195518
196053
  if (this.options.appendToChild) {
195519
196054
  const result = await this.options.appendToChild(record8.childSessionId, {
195520
196055
  // 文件是静默落盘的,append 这条路没有 TASK.md 可改——**必须自己在正文里说一声**,
@@ -195530,6 +196065,7 @@ ${inboundBundle.paths.map((path41) => `- ${path41}`).join("\n")}` : text5,
195530
196065
  ...Object.keys(inboundBundle.binaryFiles).length ? { binaryFiles: inboundBundle.binaryFiles } : {}
195531
196066
  }).catch(() => ({ accepted: false, reason: "append-threw" }));
195532
196067
  delivered = result.accepted === true;
196068
+ if (!delivered) deliveryReasonCode = result.reason ?? "rejected";
195533
196069
  }
195534
196070
  if (!delivered) {
195535
196071
  if (chatStore.enqueuePendingMessage) {
@@ -195588,6 +196124,7 @@ ${inboundBundle.paths.map((path41) => `- ${path41}`).join("\n")}` : text5,
195588
196124
  }
195589
196125
  return {
195590
196126
  delivered,
196127
+ ...delivered ? {} : { deliveryReasonCode: deliveryReasonCode ?? "rejected" },
195591
196128
  startedRound,
195592
196129
  pendingInputs: await this.pendingInputs(store, record8.id),
195593
196130
  // 起了新的一轮 = 快照与文件都随那一轮重新送过去了,没有「等下一轮再刷新」这回事。
@@ -195710,6 +196247,34 @@ ${inboundBundle.paths.map((path41) => `- ${path41}`).join("\n")}` : text5,
195710
196247
  }
195711
196248
  });
195712
196249
 
196250
+ // ../server/src/domains/delegations/continue-outcome.ts
196251
+ function describeContinueOutcome(input) {
196252
+ if (input.delivered) return { outcome: "inserted" };
196253
+ if (input.startedRound) return { outcome: "started-round" };
196254
+ const code2 = input.deliveryReasonCode ?? "rejected";
196255
+ return {
196256
+ outcome: "queued",
196257
+ // 认不出的码**原样带出来**,不折成「未知原因」——排障时那串字符本身就是线索。
196258
+ reason: REASON_TEXT[code2] ?? `\u672A\u80FD\u63D2\u5165\uFF08${code2}\uFF09`,
196259
+ reasonCode: code2
196260
+ };
196261
+ }
196262
+ var REASON_TEXT;
196263
+ var init_continue_outcome = __esm({
196264
+ "../server/src/domains/delegations/continue-outcome.ts"() {
196265
+ "use strict";
196266
+ REASON_TEXT = {
196267
+ "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",
196268
+ 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",
196269
+ "session-closing": "\u4E13\u5BB6\u90A3\u4E00\u8F6E\u6B63\u5728\u6536\u5C3E\uFF0C\u8FD9\u53E5\u8BDD\u6765\u665A\u4E86\u4E00\u6B65",
196270
+ "turn-finished": "\u6295\u9012\u8FC7\u7A0B\u4E2D\u4E13\u5BB6\u90A3\u4E00\u8F6E\u5DF2\u7ECF\u6536\u5C3E\u4E86",
196271
+ rejected: "\u4E13\u5BB6\u90A3\u4E00\u4FA7\u62D2\u6536\u4E86\u8FD9\u6B21\u63D2\u5165",
196272
+ "append-threw": "\u6295\u9012\u8FC7\u7A0B\u4E2D\u51FA\u9519\uFF08\u7F51\u7EDC\u6216\u8282\u70B9\u5F02\u5E38\uFF09",
196273
+ "no-append-port": "\u8FD9\u5957\u90E8\u7F72\u6CA1\u6709\u63A5\u4E2D\u9014\u63D2\u5165\u901A\u9053"
196274
+ };
196275
+ }
196276
+ });
196277
+
195713
196278
  // ../server/src/domains/delegations/routes.ts
195714
196279
  function trustedDelegationInvocationId(body2, turn) {
195715
196280
  const scope = turn?.artifactId ?? turn?.dispatchId;
@@ -195778,8 +196343,16 @@ function delegationRoutes(service) {
195778
196343
  const text5 = requireString(raw["text"], "--text");
195779
196344
  const files = requireStringArray(raw["files"], "--file");
195780
196345
  try {
195781
- await service.continueDelegation(req.params.id, text5, callerOf(req), files);
195782
- return { status: 204, body: void 0 };
196346
+ const res = await service.continueDelegation(req.params.id, text5, callerOf(req), files);
196347
+ const view = describeContinueOutcome(res);
196348
+ const body2 = {
196349
+ outcome: view.outcome,
196350
+ ...view.reason ? { reason: view.reason } : {},
196351
+ ...view.reasonCode ? { reasonCode: view.reasonCode } : {},
196352
+ pendingInputs: res.pendingInputs,
196353
+ ...res.attachedFiles.length ? { attachedFiles: res.attachedFiles } : {}
196354
+ };
196355
+ return { status: 200, body: body2 };
195783
196356
  } catch (error2) {
195784
196357
  rethrow(error2);
195785
196358
  }
@@ -195807,6 +196380,7 @@ var init_routes2 = __esm({
195807
196380
  "use strict";
195808
196381
  import_node_crypto33 = require("node:crypto");
195809
196382
  init_router();
196383
+ init_continue_outcome();
195810
196384
  init_service3();
195811
196385
  }
195812
196386
  });
@@ -196178,6 +196752,9 @@ function delegationReturnText(terminal, opts) {
196178
196752
  if (opts.movedPaths.length > 0) {
196179
196753
  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}`));
196180
196754
  }
196755
+ if ((opts.unchangedPaths?.length ?? 0) > 0) {
196756
+ 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`);
196757
+ }
196181
196758
  const locationFailures = opts.skipped.filter((s2) => s2.code !== void 0 && isLocationFailureCode(s2.code));
196182
196759
  const contentSkipped = opts.skipped.filter((s2) => s2.code === void 0 || !isLocationFailureCode(s2.code));
196183
196760
  if (locationFailures.length > 0) {
@@ -196217,17 +196794,21 @@ async function deliverDelegationReturn(deps, terminal) {
196217
196794
  };
196218
196795
  }
196219
196796
  const dirName = delegationDirName(record8.label, record8.expertActorId, record8.id);
196797
+ const known = delegationOutboxFingerprints(record8.roundSummaries);
196220
196798
  const outbound = await prepareOutboundFiles(
196221
196799
  deps.workdir,
196222
196800
  // 子会话与父会话同公司(§6.1.2 隔离二),所以这里的 companyId 取父会话那个。
196223
196801
  { chatSessionId: record8.childSessionId, ...parentSession.companyId ? { companyId: parentSession.companyId } : {} },
196224
- dirName
196802
+ dirName,
196803
+ void 0,
196804
+ known
196225
196805
  ).catch((err) => {
196226
196806
  log3(`[delegation-return] \u4EA7\u7269\u642C\u8FD0\u5931\u8D25\uFF08delegation=${record8.id}\uFF09: ${String(err)}`);
196227
196807
  return {
196228
196808
  prepared: [],
196229
196809
  skipped: [{ source: "outputs/", code: "INTERNAL", message: "\u642C\u8FD0\u5931\u8D25" }],
196230
196810
  totalBytes: 0,
196811
+ unchanged: [],
196231
196812
  listDirCode: "INTERNAL"
196232
196813
  };
196233
196814
  });
@@ -196247,8 +196828,18 @@ async function deliverDelegationReturn(deps, terminal) {
196247
196828
  const text5 = delegationReturnText(terminal, {
196248
196829
  movedPaths: bundleFiles.paths,
196249
196830
  skipped: outbound.skipped,
196250
- recentRounds: deps.recentRounds ?? DELEGATION_RETURN_RECENT_ROUNDS
196831
+ recentRounds: deps.recentRounds ?? DELEGATION_RETURN_RECENT_ROUNDS,
196832
+ unchangedPaths: outbound.unchanged
196251
196833
  });
196834
+ const movedFileRefs = outbound.prepared.map((f2) => ({
196835
+ name: f2.rel,
196836
+ ...typeof f2.srcSize === "number" ? { size: f2.srcSize } : {},
196837
+ ...f2.srcMtime ? { mtime: f2.srcMtime } : {}
196838
+ }));
196839
+ const fingerprints = {};
196840
+ for (const f2 of movedFileRefs) {
196841
+ if (typeof f2.size === "number" || f2.mtime) fingerprints[f2.name] = { ...typeof f2.size === "number" ? { size: f2.size } : {}, ...f2.mtime ? { mtime: f2.mtime } : {} };
196842
+ }
196252
196843
  const source = delegationReturnSource(record8);
196253
196844
  const messageId = deriveSystemMessageId(source);
196254
196845
  let written;
@@ -196279,6 +196870,8 @@ async function deliverDelegationReturn(deps, terminal) {
196279
196870
  const bundle = bundleFiles.paths.length ? {
196280
196871
  files: bundleFiles.files,
196281
196872
  binaryFiles: bundleFiles.binaryFiles,
196873
+ // queued 那一档要靠它把指纹带到 flush 落账那一刻(`PendingChatAttachment.srcSize`)。
196874
+ fingerprints,
196282
196875
  taskLines: [
196283
196876
  "",
196284
196877
  "## \u4E13\u5BB6\u4EA4\u56DE\u7684\u6587\u4EF6",
@@ -196313,6 +196906,8 @@ async function deliverDelegationReturn(deps, terminal) {
196313
196906
  via: delivery.via,
196314
196907
  movedFiles: bundleFiles.paths.length,
196315
196908
  movedPaths: bundleFiles.paths,
196909
+ movedFileRefs,
196910
+ unchangedPaths: outbound.unchanged,
196316
196911
  skippedFiles: outbound.skipped.length
196317
196912
  };
196318
196913
  }
@@ -196324,6 +196919,7 @@ var init_return_flow = __esm({
196324
196919
  init_chat_system_message();
196325
196920
  init_chat_broadcast();
196326
196921
  init_files();
196922
+ init_src();
196327
196923
  }
196328
196924
  });
196329
196925
 
@@ -196467,8 +197063,10 @@ function assembleDelegationsDomain(deps) {
196467
197063
  );
196468
197064
  const targetRound = record8.roundSummaries[record8.roundSummaries.length - 1]?.round;
196469
197065
  let movedPaths = null;
197066
+ let movedRefs = [];
196470
197067
  if (outcome.status === "delivered" && (outcome.via === "turn" || outcome.via === "append") && outcome.movedPaths.length > 0) {
196471
197068
  movedPaths = outcome.movedPaths;
197069
+ movedRefs = outcome.movedFileRefs;
196472
197070
  }
196473
197071
  if (movedPaths === null || targetRound === void 0) return;
196474
197072
  const store = await deps.resolveStore(companyId).catch(() => null);
@@ -196481,7 +197079,7 @@ function assembleDelegationsDomain(deps) {
196481
197079
  `[delegation-return] \u4EA7\u7269\u6E05\u5355\u843D\u8D26\u672A\u6210 (immediate) delegation=${record8.id} round=${targetRound} status=resolve-failed moved=${movedPaths.length} paths=${pathsField}`
196482
197080
  );
196483
197081
  } else {
196484
- outcomeCode = await registerRoundFiles(store, record8.id, targetRound, movedPaths);
197082
+ outcomeCode = await registerRoundFiles(store, record8.id, targetRound, movedRefs.length ? movedRefs : movedPaths);
196485
197083
  if (outcomeCode === "ok") return;
196486
197084
  deps.metrics?.recordFilesRegisterFailure?.(outcomeCode);
196487
197085
  warn(
@@ -196495,6 +197093,11 @@ function assembleDelegationsDomain(deps) {
196495
197093
  chatSessionId: record8.parentSessionId,
196496
197094
  delegationRef: { delegationId: record8.id, round: targetRound },
196497
197095
  paths: movedPaths,
197096
+ // 补账那一趟同样要带指纹,否则「首次登记失败 → 补账成功」的路径会留下无指纹条目。
197097
+ fingerprints: Object.fromEntries(movedRefs.map((f2) => [f2.name, {
197098
+ ...typeof f2.size === "number" ? { size: f2.size } : {},
197099
+ ...f2.mtime ? { mtime: f2.mtime } : {}
197100
+ }])),
196498
197101
  reason: `immediate-register-failed:${outcomeCode}`
196499
197102
  }).catch((err) => {
196500
197103
  warn(
@@ -196517,16 +197120,20 @@ async function registerDelegationFilesOnFlush(deps, row, companyId) {
196517
197120
  const warn = deps.log ?? ((m2) => console.warn(m2));
196518
197121
  const ref2 = row.delegationRef;
196519
197122
  if (!ref2) return "no-ref";
196520
- const paths = (row.attachments ?? []).map((a) => a.path).filter((p2) => !!p2);
196521
- if (paths.length === 0) return "no-files";
197123
+ const rows = (row.attachments ?? []).filter((a) => !!a.path);
197124
+ if (rows.length === 0) return "no-files";
196522
197125
  const store = await deps.resolveStore(companyId).catch(() => null);
196523
197126
  if (!store) return "failed";
196524
- const files = paths.map((name) => ({ name }));
197127
+ const files = rows.map((a) => ({
197128
+ name: a.path,
197129
+ ...typeof a.srcSize === "number" ? { size: a.srcSize } : {},
197130
+ ...a.srcMtime ? { mtime: a.srcMtime } : {}
197131
+ }));
196525
197132
  const outcome = await registerRoundFiles(store, ref2.delegationId, ref2.round, files);
196526
197133
  if (outcome !== "ok") {
196527
197134
  deps.metrics?.recordFilesRegisterFailure?.(outcome);
196528
197135
  warn(
196529
- `[delegation-return] flush \u843D\u8D26\u672A\u6210 delegation=${ref2.delegationId} round=${ref2.round} status=${outcome} files=${paths.length}`
197136
+ `[delegation-return] flush \u843D\u8D26\u672A\u6210 delegation=${ref2.delegationId} round=${ref2.round} status=${outcome} files=${files.length}`
196530
197137
  );
196531
197138
  }
196532
197139
  return outcome;
@@ -196549,6 +197156,31 @@ var init_assembly = __esm({
196549
197156
  }
196550
197157
  });
196551
197158
 
197159
+ // ../server/src/domains/delegations/child-live-turn.ts
197160
+ function registerDelegatedChildTurn(liveChat, childSessionId, session) {
197161
+ const appendInput = session.appendInput;
197162
+ if (typeof appendInput !== "function" || session.canAppendInput === false) return;
197163
+ const ctrl = liveChat.start(childSessionId, {
197164
+ runtimeSessionId: session.id,
197165
+ ...session.runId ? { runId: session.runId } : {},
197166
+ kill: () => {
197167
+ void session.kill?.();
197168
+ },
197169
+ appendInput: (input) => appendInput.call(session, input),
197170
+ // **每次现读**,不快照:一轮跑到收尾时 stdin 会先关,那之后 runtime 自己会回
197171
+ // `session-closing`,判断权本就该留在它那儿(同 `/api/chat` 那条路的写法)。
197172
+ get canAppendInput() {
197173
+ return session.canAppendInput !== false;
197174
+ }
197175
+ });
197176
+ void session.done.then(() => ctrl.finish("done"), () => ctrl.finish("error"));
197177
+ }
197178
+ var init_child_live_turn = __esm({
197179
+ "../server/src/domains/delegations/child-live-turn.ts"() {
197180
+ "use strict";
197181
+ }
197182
+ });
197183
+
196552
197184
  // ../connectors/src/_base/wrapper-assets.ts
196553
197185
  function isUsableWrapper(scriptPath) {
196554
197186
  if (!(0, import_node_fs7.existsSync)(scriptPath)) return false;
@@ -201298,7 +201930,7 @@ function authorizedCommandReceipt(command, args, actor) {
201298
201930
  case "promote":
201299
201931
  return { message: "promoted to persistent" };
201300
201932
  case "resolveEscalation":
201301
- return { message: `\u5DF2\u5173\u95ED\u8BE5\u6761\u4E0A\u62A5\uFF08${str(args, "escalationId")}\uFF09\u3002` };
201933
+ 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` };
201302
201934
  default:
201303
201935
  throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u65E0\u56DE\u6267\u6784\u9020 ${command}`);
201304
201936
  }
@@ -201337,7 +201969,11 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
201337
201969
  );
201338
201970
  }
201339
201971
  }
201972
+ const linkedGapId = escalationById(kernel.model, escalationId)?.gapId;
201973
+ const linkedGap = linkedGapId ? (kernel.model.gaps.get(artifactId) ?? []).find((g2) => g2.gapId === linkedGapId) : void 0;
201974
+ 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");
201340
201975
  const normalizedArgs = {
201976
+ ...linkedGap ? { gapId: linkedGap.gapId, continueExecution: true } : {},
201341
201977
  escalationId,
201342
201978
  // ← 已四级解析出的具体一条(绝非 undefined、绝非「全清」)
201343
201979
  artifactId,
@@ -201935,10 +202571,10 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}`] : []
201935
202571
  const note = optStr(args, "note");
201936
202572
  const via = optStr(args, "via");
201937
202573
  const target = kernel.model.annotations.get(annotationId);
201938
- if (target && target.state !== "open") {
202574
+ if (target && target.state !== "open" && resolution !== "acknowledged") {
201939
202575
  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" : ""}` };
201940
202576
  }
201941
- await kernel.resolveAnnotation({ annotationId, actor, resolution, note, ...via !== void 0 ? { via } : {}, ...hand !== void 0 ? { hand } : {} });
202577
+ await kernel.resolveAnnotation({ annotationId, actor, resolution, note, ...via !== void 0 ? { via } : {}, ...hand !== void 0 ? { hand } : {}, ...ctx.sessionArtifactId ? { viaNode: ctx.sessionArtifactId } : {} });
201942
202578
  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" : ""}` };
201943
202579
  }
201944
202580
  case "gap": {
@@ -202489,7 +203125,15 @@ async function startOasisServer(opts) {
202489
203125
  resolveStore: opts.delegationStoreFor,
202490
203126
  resolveChatSessions: resolveChatSessionsForDelegation,
202491
203127
  resolveActors: async (companyId) => (await actorsDomain2.resolveCtx(companyId)).service,
202492
- dispatchChat,
203128
+ // **派发完顺手把子会话这一轮登记进 live 注册表**(v2 bug 0088)。缺这一步时下面那行
203129
+ // `appendToChild` 恒回 `no-live-turn`——注册表里从来没有以 `childSessionId` 为键的轮,
203130
+ // 于是每一条 `--continue` 转达都落队列,专家要等本轮跑完才看见「手上这段作废」。
203131
+ // 理由与两条纪律见 `child-live-turn.ts` 的文件头。
203132
+ dispatchChat: async (request2) => {
203133
+ const session = await dispatchChat(request2);
203134
+ registerDelegatedChildTurn(liveChat, request2.chatSessionId, session);
203135
+ return session;
203136
+ },
202493
203137
  appendToChild: (childSessionId, text5, extra) => liveChat.append(childSessionId, text5, extra),
202494
203138
  // 用 lambda 转一层:`broadcastDepsFor` 在本文件里声明得更靠后(回流是异步发生的,
202495
203139
  // 那时它早就赋过值了)。直接把值递进去会撞 TDZ。
@@ -202730,16 +203374,25 @@ async function startOasisServer(opts) {
202730
203374
  ...store?.enqueuePendingMessage ? {
202731
203375
  enqueuePending: async (input) => {
202732
203376
  const bundle = input.bundle;
203377
+ const fp = (p2) => {
203378
+ const f2 = bundle?.fingerprints?.[p2];
203379
+ return {
203380
+ ...typeof f2?.size === "number" ? { srcSize: f2.size } : {},
203381
+ ...f2?.mtime ? { srcMtime: f2.mtime } : {}
203382
+ };
203383
+ };
202733
203384
  const attachments = bundle ? [
202734
203385
  ...Object.entries(bundle.files ?? {}).map(([p2, text5]) => ({
202735
203386
  name: p2.slice(p2.lastIndexOf("/") + 1) || p2,
202736
203387
  text: text5,
202737
- path: p2
203388
+ path: p2,
203389
+ ...fp(p2)
202738
203390
  })),
202739
203391
  ...Object.entries(bundle.binaryFiles ?? {}).map(([p2, b64]) => ({
202740
203392
  name: p2.slice(p2.lastIndexOf("/") + 1) || p2,
202741
203393
  bytesBase64: b64,
202742
- path: p2
203394
+ path: p2,
203395
+ ...fp(p2)
202743
203396
  }))
202744
203397
  ] : [];
202745
203398
  const { randomUUID: randomUUID40 } = await import("node:crypto");
@@ -202769,7 +203422,15 @@ async function startOasisServer(opts) {
202769
203422
  // ——生产 `deliverToAgent:512` 会开轮/append,用户听到空消息且下一场 UPDATE 再把已完成
202770
203423
  // 的行改回 delivering,违背「只补账、不投递」语义。
202771
203424
  enqueueRegisterPending: async (input) => {
202772
- const attachments = input.paths.map((p2) => ({ name: p2.slice(p2.lastIndexOf("/") + 1) || p2, path: p2 }));
203425
+ const attachments = input.paths.map((p2) => {
203426
+ const f2 = input.fingerprints?.[p2];
203427
+ return {
203428
+ name: p2.slice(p2.lastIndexOf("/") + 1) || p2,
203429
+ path: p2,
203430
+ ...typeof f2?.size === "number" ? { srcSize: f2.size } : {},
203431
+ ...f2?.mtime ? { srcMtime: f2.mtime } : {}
203432
+ };
203433
+ });
202773
203434
  const { randomUUID: randomUUID40 } = await import("node:crypto");
202774
203435
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
202775
203436
  try {
@@ -203203,6 +203864,22 @@ async function startOasisServer(opts) {
203203
203864
  }));
203204
203865
  return;
203205
203866
  }
203867
+ if (url.pathname.startsWith("/api/files/")) {
203868
+ const company = opts.resolveCompanyContext ? await opts.resolveCompanyContext(actor, req.headers, url.pathname) : void 0;
203869
+ if (company && company.kind !== "ok") {
203870
+ res.writeHead(403, { "content-type": "application/json" }).end(JSON.stringify({
203871
+ error: { code: "FORBIDDEN", message: "\u65E0\u6743\u8BFB\u53D6\u8BE5\u516C\u53F8\u7684\u6587\u4EF6" }
203872
+ }));
203873
+ return;
203874
+ }
203875
+ await serveFileRequest(
203876
+ req,
203877
+ res,
203878
+ url,
203879
+ await resolveEngine(company?.kind === "ok" ? company.companyId : void 0)
203880
+ );
203881
+ return;
203882
+ }
203206
203883
  if (url.pathname === "/api/whoami" && req.method === "GET") {
203207
203884
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ actor }));
203208
203885
  return;
@@ -204909,6 +205586,22 @@ ${composed}`;
204909
205586
  const itemLedger = new ChatItemLedger({
204910
205587
  ...itemStore ? { items: itemStore } : {},
204911
205588
  versionSeed,
205589
+ /* 用户那条消息一落库就推上 v3 流(ADR-0510 D2 的收口)。
205590
+ 不推的话:乐观条没有服务端号、被钉在列表末尾,而 agent 的正文带着更大的号从
205591
+ 直播流到达、进「有号」段 ⇒ **回复显示在用户消息上面**,直到下一次快照才跳回去
205592
+ (2026-09-09 用户现场)。这里把窗口从「等下一次快照」压到「一次 INSERT 往返」。
205593
+ 只推 user:assistant 的每一条本来就走 `emitV3`。 */
205594
+ onPersisted: (info) => {
205595
+ if (info.role !== "user") return;
205596
+ opts.liveChat?.publishUserItem(persistTarget?.id ?? session.id, {
205597
+ itemId: info.itemId,
205598
+ ord: info.ord,
205599
+ version: info.version,
205600
+ text: info.text,
205601
+ ...clientSubmitId ? { clientSubmitId } : {},
205602
+ ...effectiveAttachments.length ? { attachments: effectiveAttachments } : {}
205603
+ });
205604
+ },
204912
205605
  sessionId: persistTarget?.id ?? session.id,
204913
205606
  // 优先账本 id(`chat_session_turns.id`,跨进程唯一、重启不失忆);账本没接入才兜底。
204914
205607
  turnId: heldTurn?.id ?? fallbackTurnId(session.runId),
@@ -206219,6 +206912,7 @@ var init_server3 = __esm({
206219
206912
  init_src();
206220
206913
  init_src();
206221
206914
  init_src();
206915
+ init_file_http();
206222
206916
  init_build_info();
206223
206917
  init_artifact_content2();
206224
206918
  init_command_policy();
@@ -206260,6 +206954,7 @@ var init_server3 = __esm({
206260
206954
  init_workorder_terminal();
206261
206955
  init_knowledge2();
206262
206956
  init_assembly();
206957
+ init_child_live_turn();
206263
206958
  init_src8();
206264
206959
  init_continuation();
206265
206960
  githubAppPending = new PendingAppCreations();
@@ -209346,6 +210041,27 @@ function foldSingleNodeTasks(items) {
209346
210041
  }
209347
210042
  return folded;
209348
210043
  }
210044
+ function fillFolderSizes(items) {
210045
+ const folders = items.filter((i) => i.kind === "folder" && i.path);
210046
+ if (folders.length === 0) return;
210047
+ const files = items.filter((i) => i.kind !== "folder" && i.path);
210048
+ for (const folder of folders) {
210049
+ const prefix = `${folder.path}/`;
210050
+ let sum = 0;
210051
+ let seen = 0;
210052
+ let complete = true;
210053
+ for (const file of files) {
210054
+ if (!file.path.startsWith(prefix)) continue;
210055
+ seen += 1;
210056
+ if (file.size == null) {
210057
+ complete = false;
210058
+ break;
210059
+ }
210060
+ sum += file.size;
210061
+ }
210062
+ folder.size = seen > 0 && complete ? sum : null;
210063
+ }
210064
+ }
209349
210065
  function engineContentKind(k2) {
209350
210066
  return k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
209351
210067
  }
@@ -210243,6 +210959,7 @@ var init_service4 = __esm({
210243
210959
  const projectByWorkOrder = new Map(bindings.map((b2) => [b2.workOrderId, b2.projectId]));
210244
210960
  const workorderSummaries = await this.listProjectWorkorders(projectId2);
210245
210961
  const workorderTitleById = new Map(workorderSummaries.map((w2) => [w2.id, w2.title]));
210962
+ const workorderOwnerById = new Map(workorderSummaries.map((w2) => [w2.id, w2.owner?.id ?? null]));
210246
210963
  const engineRows = await this.collectEngineFilesForProject(
210247
210964
  projectId2,
210248
210965
  workorderSummaries.map((w2) => w2.id),
@@ -210261,6 +210978,8 @@ var init_service4 = __esm({
210261
210978
  const taskId = record8.createdFromWorkOrderId ?? artifact?.workspace ?? null;
210262
210979
  const taskTitle = taskId ? workorderTitleById.get(taskId) ?? null : null;
210263
210980
  const creatorId = revision.author || null;
210981
+ const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
210982
+ const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
210264
210983
  const folderPath = taskId ? `wo:${taskId}/node:${record8.artifactId}` : null;
210265
210984
  if (folderPath) items.push({
210266
210985
  kind: "folder",
@@ -210273,13 +210992,14 @@ var init_service4 = __esm({
210273
210992
  size: null,
210274
210993
  taskId,
210275
210994
  taskTitle,
210276
- creatorKind: null,
210277
- creatorId: null,
210278
- creatorName: null,
210995
+ creatorKind,
210996
+ creatorId,
210997
+ creatorName,
210279
210998
  updatedAt: record8.updatedAt
210280
210999
  });
210281
211000
  const content3 = revision.contentKind === "manifest" ? await this.readContentRefText(revision.contentRef) : null;
210282
211001
  const files2 = content3 !== null ? tryParseManifestFiles(content3) : void 0;
211002
+ const wholeSize = files2?.length ? null : content3 !== null ? Buffer.byteLength(content3, "utf8") : await this.contentByteSize(revision.contentRef, revision.contentKind);
210283
211003
  for (const file of files2?.length ? files2 : [null]) {
210284
211004
  items.push({
210285
211005
  kind: "artifact",
@@ -210289,15 +211009,15 @@ var init_service4 = __esm({
210289
211009
  filePath: file?.path ?? null,
210290
211010
  depth: folderPath ? 2 : 0,
210291
211011
  type: record8.type,
210292
- size: file?.size ?? null,
211012
+ size: file ? file.size ?? null : wholeSize,
210293
211013
  taskId,
210294
211014
  taskTitle,
210295
211015
  // 内容形态/引用只挂在**整份产物**那一行;装箱单摊出来的成员行有自己的后缀,
210296
211016
  // 按后缀走图标即可,挂上 manifest 反而会把 `Dockerfile` 这种无后缀成员画成压缩包。
210297
211017
  ...file ? {} : { contentKind: revision.contentKind, contentRef: revision.contentRef },
210298
- creatorKind: creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null,
211018
+ creatorKind,
210299
211019
  creatorId,
210300
- creatorName: creatorId ? this.resolveActorName(creatorId) : null,
211020
+ creatorName,
210301
211021
  updatedAt: record8.updatedAt
210302
211022
  });
210303
211023
  }
@@ -210309,6 +211029,7 @@ var init_service4 = __esm({
210309
211029
  if (foldedTasks.has(item.taskId)) continue;
210310
211030
  taskFolders.add(item.taskId);
210311
211031
  const path41 = `wo:${item.taskId}`;
211032
+ const ownerId = workorderOwnerById.get(item.taskId) ?? null;
210312
211033
  items.push({
210313
211034
  kind: "folder",
210314
211035
  id: `folder:${path41}`,
@@ -210320,9 +211041,9 @@ var init_service4 = __esm({
210320
211041
  size: null,
210321
211042
  taskId: item.taskId,
210322
211043
  taskTitle: item.taskTitle,
210323
- creatorKind: null,
210324
- creatorId: null,
210325
- creatorName: null,
211044
+ creatorKind: ownerId ? ownerId.startsWith("actor:human:") ? "human" : "agent" : null,
211045
+ creatorId: ownerId,
211046
+ creatorName: ownerId ? this.resolveActorName(ownerId) : null,
210326
211047
  updatedAt: item.updatedAt
210327
211048
  });
210328
211049
  }
@@ -210348,6 +211069,7 @@ var init_service4 = __esm({
210348
211069
  updatedAt: file.uploadedAt
210349
211070
  });
210350
211071
  }
211072
+ fillFolderSizes(items);
210351
211073
  items.sort((a, b2) => {
210352
211074
  const pathA = a.path ?? "";
210353
211075
  const pathB = b2.path ?? "";
@@ -210391,6 +211113,9 @@ var init_service4 = __esm({
210391
211113
  const work = worksById.get(acceptedId);
210392
211114
  const folderPath = `wo:${workOrderId}/node:${node2.id}`;
210393
211115
  const folderUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211116
+ const creatorId = work?.assigneeActorId ?? null;
211117
+ const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
211118
+ const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
210394
211119
  out.push({
210395
211120
  kind: "folder",
210396
211121
  id: `folder:${folderPath}`,
@@ -210402,14 +211127,11 @@ var init_service4 = __esm({
210402
211127
  size: null,
210403
211128
  taskId: workOrderId,
210404
211129
  taskTitle,
210405
- creatorKind: null,
210406
- creatorId: null,
210407
- creatorName: null,
211130
+ creatorKind,
211131
+ creatorId,
211132
+ creatorName,
210408
211133
  updatedAt: folderUpdatedAt
210409
211134
  });
210410
- const creatorId = work?.assigneeActorId ?? null;
210411
- const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
210412
- const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
210413
211135
  const sortedArts = arts.slice().sort((a, b2) => a.ordinal - b2.ordinal);
210414
211136
  for (const a of sortedArts) {
210415
211137
  const artifactId = node2.id;
@@ -210448,7 +211170,7 @@ var init_service4 = __esm({
210448
211170
  filePath: null,
210449
211171
  depth: 2,
210450
211172
  type: artifactType || "",
210451
- size: null,
211173
+ size: await this.contentByteSize(a.contentRef, engineContentKind(a.contentKind)),
210452
211174
  contentKind: engineContentKind(a.contentKind),
210453
211175
  ...a.contentRef ? { contentRef: a.contentRef } : {},
210454
211176
  taskId: workOrderId,
@@ -210867,6 +211589,19 @@ var init_service4 = __esm({
210867
211589
  }
210868
211590
  return { ...withKind, size: Buffer.byteLength(content3, "utf8") };
210869
211591
  }
211592
+ /**
211593
+ * 一份产物正文的字节数——**与任务页「相关产物」共用同一条判据**(见 {@link enrichNodeOutputDocument}):
211594
+ * 不看 `contentKind` 名义,只看正文能不能从 BlobStore 读到;读不到就是 null。
211595
+ *
211596
+ * 于是 git 提交号 / Figma version 这类真·外部钉、未纳管、`empty` 都诚实无大小(前端显「—」),
211597
+ * 而 `inline-blob` 的任务书、PRD、ADR 会报出真实字节数——此前 files-view 这条路径**恒发 null**,
211598
+ * 同一份任务书在任务页有 8.0 KB、在项目页却是「—」。
211599
+ */
211600
+ async contentByteSize(contentRef, contentKind) {
211601
+ if (contentKind === "empty" || !contentRef?.trim()) return null;
211602
+ const content3 = await this.readContentRefText(contentRef);
211603
+ return content3 === null ? null : Buffer.byteLength(content3, "utf8");
211604
+ }
210870
211605
  /** 正文读取的唯一入口:只认 BlobStore;没配 / 没纳管 / 读失败一律 null(调用方据此诚实缺 size)。 */
210871
211606
  async readContentRefText(contentRef) {
210872
211607
  if (!this.blobs) return null;
@@ -217756,6 +218491,31 @@ ${input.description}
217756
218491
  listActorConnectorConnections(actorId) {
217757
218492
  return this.opts.store.listActorConnectorConnections(actorId);
217758
218493
  }
218494
+ /**
218495
+ * 移除某员工与某连接器的连接(组织页连接器详情的「移除连接」,2026-09-09 原型 1600:5765)。
218496
+ *
218497
+ * 删两样东西,缺一不可:
218498
+ * ① 这名员工**自己**那份连接器凭据(`scope=personal` 且 `connectorId` 命中的变量行)——
218499
+ * 不删的话,「移除」之后 `actorConnected` 里还有它,卡片照样在,读作「删不掉」;
218500
+ * ② 「员工×连接器」的连接记录——不删的话 `effective` 里还有它(enabled=false),
218501
+ * 卡片变成一张「已停用」的僵尸卡,而人要的是它消失。
218502
+ *
218503
+ * **不碰组织那条 connector 行**(决策 0080 修订五的同一条理由):组织级连接是别人配的资产,
218504
+ * 一名员工点「移除连接」不该把全组织的连接拆掉。组织级的断开在「管理 > 连接器」。
218505
+ * 于是:组织已连接时,移除个人连接后这名员工会**回落到组织默认**——卡片仍在,但身份那一行
218506
+ * 变回组织账号。这是对的,不是没删干净。
218507
+ *
218508
+ * 幂等:没有个人凭据、没有记录时照样返回成功(`variablesDeleted: 0`)——重复点、并发点都不该报错。
218509
+ */
218510
+ async removeActorConnector(actorId, connectorId) {
218511
+ const rows = await this.opts.store.listVariables(actorId);
218512
+ const mine = rows.filter(
218513
+ (v2) => v2.scope === "personal" && v2.actorId === actorId && v2.connectorId === connectorId
218514
+ );
218515
+ for (const v2 of mine) await this.opts.store.deleteVariable(v2.key, actorId);
218516
+ await this.opts.store.deleteActorConnectorConnection(actorId, connectorId);
218517
+ return { removed: true, variablesDeleted: mine.length };
218518
+ }
217759
218519
  /**
217760
218520
  * 员工连接器面板所需状态:连接记录 + 组织级已连接集合 + 该员工已授权(有个人 connector 变量)集合。
217761
218521
  * 前端据此算每个 connector 的 hasRecord / 有效 enabled / connected(actor 或 global)。
@@ -218555,15 +219315,28 @@ var init_model_pricing = __esm({
218555
219315
  "gpt-4.1-nano": usd(0.1, 0.4, 0.025),
218556
219316
  "gpt-4.1-nano-2025-04-14": usd(0.1, 0.4, 0.025),
218557
219317
  "o4-mini": usd(1.1, 4.4, 0.275),
219318
+ // o3 / o3-mini 在 codex 静态目录里可选,却一直不在价目快照里(费用记 0)。
219319
+ // 这两行按 OpenAI 公开价填,不是智增增快照里的行;供应商价目表同步到时以 DB 为准。
219320
+ "o3": usd(2, 8),
219321
+ "o3-mini": usd(1.1, 4.4),
219322
+ "claude-fable-5-1": usd(10, 50),
218558
219323
  "claude-fable-5": usd(10, 50),
219324
+ "claude-mythos-5-1": usd(10, 50),
218559
219325
  "claude-mythos-5": usd(10, 50),
219326
+ // claude-opus-5 是 Claude Code 当代默认款,却一直不在这张快照里:查不到价 → 费用记 0
219327
+ // (与 2026-09-04 那次 `claude-opus-5[1m]` 记 0 同一类坑)。按 Opus 档 $5/$25 补上。
219328
+ "claude-opus-5": usd(5, 25),
218560
219329
  "claude-opus-4-8": usd(5, 25),
218561
219330
  "claude-opus-4-7": usd(5, 25),
218562
219331
  "claude-sonnet-5": usd(2, 10),
218563
219332
  "claude-sonnet-4-6": usd(3, 15),
218564
219333
  "claude-opus-4-6": usd(5, 25),
218565
219334
  "claude-opus-4-5-20251101": usd(5, 25),
219335
+ "claude-opus-4-5": usd(5, 25),
218566
219336
  "claude-sonnet-4-5-20250929": usd(3, 15),
219337
+ // 短名同样要有:前缀比对是「表里的键是模型名的前缀」,带日期的那行匹配不上不带日期的短名。
219338
+ // claude-sonnet-4-5 从 2026-07-04 起就在目录里可选,却一直查不到价(费用记 0),本次一并补。
219339
+ "claude-sonnet-4-5": usd(3, 15),
218567
219340
  "claude-opus-4-1-20250805": usd(15, 75),
218568
219341
  "claude-opus-4-20250514": usd(15, 75),
218569
219342
  "claude-sonnet-4-20250514": usd(3, 15),
@@ -218572,6 +219345,14 @@ var init_model_pricing = __esm({
218572
219345
  "claude-3-5-sonnet-20240620": usd(3, 15),
218573
219346
  "claude-3-sonnet-20240229": usd(3, 15),
218574
219347
  "claude-haiku-4-5-20251001": usd(1, 5),
219348
+ // codebuddy 目录用点号写法(claude-sonnet-4.6),与上面的连字符写法是同一批模型,
219349
+ // 但字符串对不上就查不到价 → 记 0。同价另起别名行。
219350
+ "claude-sonnet-4.6": usd(3, 15),
219351
+ "claude-opus-4.7": usd(5, 25),
219352
+ "gemini-3.1-pro": usd(2, 12),
219353
+ "gemini-3-pro-preview": usd(2, 12),
219354
+ // Claude Code 回报的可能是不带日期的短名,短名匹配不上带日期的那行(前缀比对是「表里的键是模型名的前缀」)。
219355
+ "claude-haiku-4-5": usd(1, 5),
218575
219356
  "gemini-3.5-flash": usd(1.5, 9),
218576
219357
  "gemini-3.1-pro-preview": usd(2, 12),
218577
219358
  "gemini-3.1-pro-preview-customtools": usd(2, 12),
@@ -219705,6 +220486,11 @@ function actorsDomain(opts) {
219705
220486
  const conn = await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, b2.enabled);
219706
220487
  return { status: 200, body: conn };
219707
220488
  });
220489
+ router.delete("/api/actors/:id/connectors/:connectorId", async (req) => {
220490
+ const { service } = await resolveCtx(req.auth.companyId);
220491
+ const r = await service.removeActorConnector(req.params.id, req.params.connectorId);
220492
+ return { status: 200, body: r };
220493
+ });
219708
220494
  const requireVariableManager = (req) => {
219709
220495
  if (!isHumanActor(req.auth.actor)) {
219710
220496
  throw new ApiError(
@@ -223996,11 +224782,13 @@ async function resolveRuntimeModels(deps, runtimeId) {
223996
224782
  const rt = (await deps.nodeStore.listRuntimes()).find((r) => r.id === runtimeId);
223997
224783
  if (!rt) return [];
223998
224784
  try {
223999
- const [base, overrides] = await Promise.all([
224000
- listModels(rt.kind, rt.binary),
224785
+ const reported = deps.nodeReportedModels?.(rt.nodeId, rt.kind);
224786
+ const [discovered, overrides] = await Promise.all([
224787
+ // 节点报过就不必再在控制面上白 spawn 一次(跨机时那次 spawn 注定 ENOENT)。
224788
+ reported && reported.length > 0 ? Promise.resolve(reported) : listModels(rt.kind, rt.binary),
224001
224789
  deps.registry.listRuntimeModelOverrides(runtimeId)
224002
224790
  ]);
224003
- return applyModelOverrides(base, overrides);
224791
+ return applyModelOverrides(discovered, overrides);
224004
224792
  } catch {
224005
224793
  const overrides = await deps.registry.listRuntimeModelOverrides(runtimeId).catch(() => []);
224006
224794
  return applyModelOverrides([], overrides);
@@ -224392,6 +225180,21 @@ function nodesDomain(deps) {
224392
225180
  if (!rt) return { status: 404, body: { error: "runtime not found" } };
224393
225181
  return { status: 200, body: { items: await resolveRuntimeModels(deps, runtimeId) } };
224394
225182
  });
225183
+ router.post("/api/runtimes/:runtimeId/models/refresh", async (req) => {
225184
+ const runtimeId = req.params["runtimeId"];
225185
+ if (!runtimeId) return { status: 400, body: { error: "missing runtimeId" } };
225186
+ const rt = (await deps.nodeStore.listRuntimes()).find((r) => r.id === runtimeId);
225187
+ if (!rt) return { status: 404, body: { error: "runtime not found" } };
225188
+ const outcome = deps.refreshNodeModels ? await deps.refreshNodeModels(rt.nodeId) : { ok: false, reason: "\u672C\u90E8\u7F72\u6CA1\u6709\u8282\u70B9\u7F51\u5173\uFF0C\u62FF\u4E0D\u5230\u8FD9\u53F0\u673A\u5668\u4E0A\u7684\u771F\u5B9E\u6E05\u5355" };
225189
+ return {
225190
+ status: 200,
225191
+ body: {
225192
+ items: await resolveRuntimeModels(deps, runtimeId),
225193
+ refreshed: outcome.ok,
225194
+ ...outcome.reason ? { reason: outcome.reason } : {}
225195
+ }
225196
+ };
225197
+ });
224395
225198
  router.get("/api/runtimes/:runtimeId/model-overrides", async (req) => {
224396
225199
  const runtimeId = req.params["runtimeId"];
224397
225200
  if (!runtimeId) return { status: 400, body: { error: "missing runtimeId" } };
@@ -224806,6 +225609,146 @@ var init_node_store = __esm({
224806
225609
  }
224807
225610
  });
224808
225611
 
225612
+ // ../server/src/domains/collab/review-activity.ts
225613
+ function reviewCardId(targetWorkId, reviewerActorId) {
225614
+ return `${REVIEW_CARD_PREFIX}${encodeURIComponent(targetWorkId)}:${encodeURIComponent(reviewerActorId)}`;
225615
+ }
225616
+ function activityReviews(snap, events) {
225617
+ const rows = /* @__PURE__ */ new Map();
225618
+ for (const r of snap.reviews) rows.set(r.id, { ...r });
225619
+ const authoritative = new Set(snap.reviews.filter((r) => r.status).map((r) => r.id));
225620
+ for (const rec of [...events].sort((a, b2) => a.seq - b2.seq)) {
225621
+ const e = rec.event;
225622
+ if (e.kind === "review.create" && !rows.has(e.reviewId)) {
225623
+ rows.set(e.reviewId, {
225624
+ id: e.reviewId,
225625
+ workorderId: snap.workorder.id,
225626
+ nodeId: e.nodeId,
225627
+ targetWorkId: e.targetWorkId,
225628
+ reviewerActorId: e.reviewerActorId,
225629
+ reviewGroup: e.reviewGroup ?? e.reviewerActorId,
225630
+ createdAt: rec.createdAt,
225631
+ startedAt: null,
225632
+ endedAt: null,
225633
+ cancelledAt: null,
225634
+ verdict: null,
225635
+ note: null,
225636
+ handActorId: null,
225637
+ decidedAt: null,
225638
+ sessionRef: null
225639
+ });
225640
+ }
225641
+ if (e.kind === "review.create" && !authoritative.has(e.reviewId)) {
225642
+ const r = rows.get(e.reviewId);
225643
+ Object.assign(r, {
225644
+ nodeId: e.nodeId,
225645
+ targetWorkId: e.targetWorkId,
225646
+ reviewerActorId: e.reviewerActorId,
225647
+ reviewGroup: e.reviewGroup ?? r.reviewGroup ?? e.reviewerActorId,
225648
+ createdAt: r.createdAt ?? rec.createdAt
225649
+ });
225650
+ }
225651
+ if (!e.kind.startsWith("review.")) continue;
225652
+ 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)) : [];
225653
+ 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;
225654
+ for (const r of matches) {
225655
+ if (!r || authoritative.has(r.id)) continue;
225656
+ if (e.kind === "review.started") r.startedAt = rec.createdAt;
225657
+ if (e.kind === "review.timeout") {
225658
+ r.endedAt = rec.createdAt;
225659
+ }
225660
+ if (e.kind === "review.kill") r.cancelledAt = rec.createdAt;
225661
+ if (e.kind === "review.response") {
225662
+ r.verdict = e.verdict;
225663
+ r.note = e.note ?? null;
225664
+ r.decidedAt = rec.createdAt;
225665
+ r.endedAt = rec.createdAt;
225666
+ r.handActorId = e.handActorId ?? null;
225667
+ }
225668
+ }
225669
+ }
225670
+ const createdSeq = new Map(events.flatMap((r) => r.event.kind === "review.create" ? [[r.event.reviewId, r.seq]] : []));
225671
+ 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));
225672
+ }
225673
+ function reviewCardTarget(cardId, reviews) {
225674
+ if (cardId.startsWith(REVIEW_CARD_PREFIX)) {
225675
+ const encoded = cardId.slice(REVIEW_CARD_PREFIX.length);
225676
+ const separator = encoded.indexOf(":");
225677
+ if (separator < 1 || separator === encoded.length - 1) return void 0;
225678
+ try {
225679
+ return {
225680
+ targetWorkId: decodeURIComponent(encoded.slice(0, separator)),
225681
+ reviewerActorId: decodeURIComponent(encoded.slice(separator + 1))
225682
+ };
225683
+ } catch {
225684
+ return void 0;
225685
+ }
225686
+ }
225687
+ if (cardId.startsWith("review:")) {
225688
+ const review = reviews.find((r) => r.id === cardId.slice(7));
225689
+ if (review) return { targetWorkId: review.targetWorkId, reviewerActorId: review.reviewerActorId };
225690
+ }
225691
+ return void 0;
225692
+ }
225693
+ function latestActivityReviews(reviews, events) {
225694
+ const createSeq = new Map(events.flatMap((r) => r.event.kind === "review.create" ? [[r.event.reviewId, r.seq]] : []));
225695
+ 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));
225696
+ const latest = /* @__PURE__ */ new Map();
225697
+ for (const r of rows) latest.set(JSON.stringify([r.reviewGroup, r.reviewerActorId]), r);
225698
+ return [...latest.values()];
225699
+ }
225700
+ function reviewLabel(r) {
225701
+ const state = reviewState(r);
225702
+ if (state === "accept") return "\u5BA1\u6838\u901A\u8FC7";
225703
+ if (state === "reject") return "\u5BA1\u6838\u4E0D\u901A\u8FC7";
225704
+ if (state === "dead") return "\u5BA1\u6838\u5DF2\u505C\u6B62";
225705
+ if (state === "failed") return "\u5BA1\u6838\u8FD0\u884C\u5931\u8D25";
225706
+ if (state === "retry") return "\u6B63\u5728\u91CD\u8BD5\u5BA1\u6838";
225707
+ return r.reviewerActorId.startsWith("actor:human:") ? "\u5F85\u5BA1\u6838" : "\u6B63\u5728\u5BA1\u6838";
225708
+ }
225709
+ function reviewSummary(snap, reviews, events) {
225710
+ const first = reviews[0];
225711
+ const work = snap.works.find((w2) => w2.id === first.targetWorkId);
225712
+ const node2 = snap.nodes.find((n) => n.id === first.nodeId);
225713
+ const name = node2?.title ?? first.nodeId;
225714
+ const latest = latestActivityReviews(reviews, events);
225715
+ const requirements = snap.requirements.filter((r) => r.nodeId === first.nodeId && r.reviewerActorId === first.reviewerActorId);
225716
+ const judgement = judgeWork({ requirements: requirements.length ? requirements : reviews, reviews });
225717
+ const pending = latest.filter((r) => !r.cancelledAt && !r.verdict && reviewState(r) === "running");
225718
+ if (work?.acceptanceState === "accepted" || work?.acceptedAt || work?.acceptanceState !== "rejected" && judgement === "passed") {
225719
+ return { phase: "done", status: `\u300A${name}\u300B\u5BA1\u6838\u901A\u8FC7\u3002`, latest, pending: [] };
225720
+ }
225721
+ if (work?.acceptanceState === "rejected" || judgement === "rejected") {
225722
+ return { phase: "done", status: "\u5BA1\u6838\u4E0D\u901A\u8FC7\u3002", latest, pending: [] };
225723
+ }
225724
+ const endedWork = !!(work?.cancelledAt || work?.deadAt);
225725
+ if (endedWork && latest.every((r) => !!r.cancelledAt && reviewState(r) !== "retry")) {
225726
+ return { phase: "done", status: `\u300A${name}\u300B\u7684\u672C\u6B21\u5BA1\u6838\u5DF2\u53D6\u6D88\u3002`, latest, pending: [] };
225727
+ }
225728
+ const interrupted = latest.some((r) => ["failed", "dead"].includes(reviewState(r)));
225729
+ 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 };
225730
+ if (latest.some((r) => r.reviewerActorId.startsWith("actor:agent:") && ["running", "retry"].includes(reviewState(r)))) {
225731
+ return { phase: "running", status: `\u6B63\u5728\u5BA1\u6838 \u300A${name}\u300B\u3002`, latest, pending };
225732
+ }
225733
+ return { phase: "not_started", status: `\u8BF7\u786E\u8BA4\u300A${name}\u300B\u662F\u5426\u5BA1\u6838\u901A\u8FC7\u3002`, latest, pending };
225734
+ }
225735
+ var REVIEW_CARD_PREFIX, REVIEW_TRACE_EVENT_KINDS;
225736
+ var init_review_activity = __esm({
225737
+ "../server/src/domains/collab/review-activity.ts"() {
225738
+ "use strict";
225739
+ init_src4();
225740
+ REVIEW_CARD_PREFIX = "review-work:";
225741
+ REVIEW_TRACE_EVENT_KINDS = [
225742
+ "review.create",
225743
+ "review.started",
225744
+ "review.response",
225745
+ "review.timeout",
225746
+ "review.kill",
225747
+ "plan.node_retry"
225748
+ ];
225749
+ }
225750
+ });
225751
+
224809
225752
  // ../server/src/domains/collab/workorder-manager.ts
224810
225753
  function resolveWorkorderManager(snap) {
224811
225754
  const nodes = snap.nodes;
@@ -224844,8 +225787,29 @@ function buildWorkorderActivity(input) {
224844
225787
  if (!snap) return { workorderId, cards: [], truncated: false };
224845
225788
  const nodeById = new Map(snap.nodes.map((n) => [n.id, n]));
224846
225789
  const workById = new Map(snap.works.map((w2) => [w2.id, w2]));
224847
- const reviewById = new Map(snap.reviews.map((r) => [r.id, r]));
224848
225790
  const issueById = new Map(snap.issues.map((i) => [i.id, i]));
225791
+ const activeWorks = /* @__PURE__ */ new Set();
225792
+ const gapOrigins = /* @__PURE__ */ new Map();
225793
+ const knownStarts = new Set(events.flatMap((r) => r.event.kind === "work.create" || r.event.kind === "work.started" ? [r.event.workId] : []));
225794
+ for (const rec of events) {
225795
+ const event = rec.event;
225796
+ if (event.kind === "work.create" || event.kind === "work.started") activeWorks.add(event.workId);
225797
+ if (event.kind === "work.response" || event.kind === "work.timeout" || event.kind === "work.kill") activeWorks.delete(event.workId);
225798
+ if (event.kind !== "issue.create" || event.issueKind !== "gap") continue;
225799
+ const nodeId = event.raisedByNodeId ?? event.aboutNodeId;
225800
+ const matches = (id) => {
225801
+ const work = workById.get(id);
225802
+ return work?.nodeId === nodeId && work.assigneeActorId === event.authorActorId;
225803
+ };
225804
+ if (event.aboutWorkId && matches(event.aboutWorkId)) {
225805
+ gapOrigins.set(event.issueId, [event.aboutWorkId]);
225806
+ } else if (!event.aboutWorkId) {
225807
+ const candidates = [...activeWorks].filter(matches);
225808
+ const at = Date.parse(rec.createdAt);
225809
+ 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));
225810
+ if (candidates.length === 1 && !unobserved) gapOrigins.set(event.issueId, candidates);
225811
+ }
225812
+ }
224849
225813
  const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
224850
225814
  const rootBriefNodeIds = new Set(
224851
225815
  snap.nodes.filter((n) => n.type === "brief" && !nodesWithInEdges.has(n.id)).map((n) => n.id)
@@ -224896,7 +225860,6 @@ function buildWorkorderActivity(input) {
224896
225860
  const groupIndexOf = /* @__PURE__ */ new Map();
224897
225861
  const cardOfWork = /* @__PURE__ */ new Map();
224898
225862
  const groupKey = (nodeId) => `${nodeId}#${groupIndexOf.get(nodeId) ?? 0}`;
224899
- const reviewCards = /* @__PURE__ */ new Map();
224900
225863
  const issueCards = /* @__PURE__ */ new Map();
224901
225864
  const workOfIssue = /* @__PURE__ */ new Map();
224902
225865
  const plainCommentIssueIds = /* @__PURE__ */ new Set();
@@ -225118,72 +226081,6 @@ function buildWorkorderActivity(input) {
225118
226081
  }
225119
226082
  break;
225120
226083
  }
225121
- /* ── 审核(review)──────────────────────────────────────────── */
225122
- case "review.create": {
225123
- const reviewId = String(ev.reviewId ?? "");
225124
- const nodeId = String(ev.nodeId ?? "");
225125
- const targetWorkId = String(ev.targetWorkId ?? "");
225126
- const reviewerId = str4(ev.reviewerActorId) ?? rec.actorId;
225127
- const isAgent2 = ev.reviewerIsAgent === true;
225128
- const name = nodeName(nodeId);
225129
- reviewCards.set(reviewId, {
225130
- id: `review:${reviewId}`,
225131
- seq: rec.seq,
225132
- at: rec.createdAt,
225133
- updatedAt: rec.createdAt,
225134
- nodeId,
225135
- executorId: reviewerId,
225136
- ...rec.handActorId ? { handActorId: rec.handActorId } : {},
225137
- // agent 评审是「机器在跑」= 进行中;人审是待办 = 待开始(人没有「开始看了」这个事件)。
225138
- phase: isAgent2 ? "running" : "not_started",
225139
- status: isAgent2 ? ACTIVITY_COPY.reviewing(name) : ACTIVITY_COPY.reviewPending(name),
225140
- /* 挂上被审那一次交付的**全部产物**(发起人 2026-09-08 定):要人点「通过/不通过」,
225141
- 就得让他在同一张卡上看到到底在审什么。此前这里恒空,产物只挂在验收卡上——
225142
- 而验收卡与审核卡是同一件事的两张(见下方 `acceptCards` 那段的跳过判据)。 */
225143
- artifacts: artifactsByWork.get(targetWorkId) ?? [],
225144
- // 人审才出底栏按钮;agent 审无按钮(原型同)。
225145
- actions: isAgent2 ? [] : [
225146
- { kind: "review", label: BUTTON.approve, target: nodeId, verdict: "approve" },
225147
- { kind: "review", label: BUTTON.reject, target: nodeId, verdict: "request_changes" }
225148
- ]
225149
- });
225150
- break;
225151
- }
225152
- case "review.started": {
225153
- const d = reviewCards.get(String(ev.reviewId ?? ""));
225154
- if (d) touch(d, rec);
225155
- break;
225156
- }
225157
- case "review.response": {
225158
- const reviewId = str4(ev.reviewId);
225159
- const targetWorkId = String(ev.targetWorkId ?? "");
225160
- const d = reviewId ? reviewCards.get(reviewId) : [...reviewCards.values()].find((c) => reviewById.get(c.id.slice("review:".length))?.targetWorkId === targetWorkId);
225161
- if (!d) break;
225162
- touch(d, rec);
225163
- d.phase = "done";
225164
- d.executorId = str4(ev.reviewerActorId) ?? d.executorId;
225165
- d.actions = [];
225166
- const nodeId = d.nodeId;
225167
- const name = nodeName(nodeId);
225168
- if (ev.verdict === "approve") {
225169
- d.status = ACTIVITY_COPY.reviewApproved(name);
225170
- } else {
225171
- d.status = ACTIVITY_COPY.reviewRejected;
225172
- const note = str4(ev.note);
225173
- if (note) d.detail = ACTIVITY_COPY.reviewRejectedDetail(note);
225174
- }
225175
- break;
225176
- }
225177
- case "review.timeout":
225178
- case "review.kill": {
225179
- const d = reviewCards.get(String(ev.reviewId ?? ""));
225180
- if (!d) break;
225181
- touch(d, rec);
225182
- d.phase = "stuck";
225183
- d.status = ev.kind === "review.kill" ? ACTIVITY_COPY.reviewKilled(nodeName(d.nodeId)) : ACTIVITY_COPY.reviewTimedOut(nodeName(d.nodeId));
225184
- d.actions = [];
225185
- break;
225186
- }
225187
226084
  /* ── 沟通与异常(issue)────────────────────────────────────── */
225188
226085
  case "issue.create": {
225189
226086
  const issueId = String(ev.issueId ?? "");
@@ -225327,24 +226224,14 @@ function buildWorkorderActivity(input) {
225327
226224
  if (issue2?.kind === "escalation") {
225328
226225
  const linkedGap = str4(issue2.gapId);
225329
226226
  const stuckActor = (linkedGap ? issueById.get(linkedGap)?.authorActorId : void 0) ?? issue2.authorActorId;
226227
+ touch(d, rec);
226228
+ d.phase = "done";
226229
+ d.executorId = resolverId;
226230
+ d.handActorId = rec.handActorId ?? void 0;
226231
+ d.status = ACTIVITY_COPY.handledEscalation(nameOf(stuckActor));
225330
226232
  d.actions = [];
225331
226233
  const raisedCard = issueCards.get(`${issueId}:raised`);
225332
226234
  if (raisedCard) raisedCard.actions = [];
225333
- standalone.push({
225334
- id: `issue:${issueId}:resolved`,
225335
- seq: rec.seq,
225336
- at: rec.createdAt,
225337
- updatedAt: rec.createdAt,
225338
- ...d.nodeId ? { nodeId: d.nodeId } : {},
225339
- executorId: resolverId,
225340
- ...rec.handActorId ? { handActorId: rec.handActorId } : {},
225341
- phase: "done",
225342
- /* 收口正文不上卡(与开口那一支同一条口径):`ws:wo-635d7add` 的收口 note 是一整段
225343
- 处置说明,铺回展开区又是一屏字。 */
225344
- status: ACTIVITY_COPY.handledEscalation(nameOf(stuckActor)),
225345
- artifacts: [],
225346
- actions: []
225347
- });
225348
226235
  break;
225349
226236
  }
225350
226237
  touch(d, rec);
@@ -225371,9 +226258,7 @@ function buildWorkorderActivity(input) {
225371
226258
  break;
225372
226259
  }
225373
226260
  }
225374
- const reviewedWorkIds = new Set(
225375
- [...reviewCards.keys()].map((rid) => reviewById.get(rid)?.targetWorkId).filter(Boolean)
225376
- );
226261
+ const reviewedWorkIds = new Set([...snap.reviews, ...activityReviews(snap, events)].map((r) => r.targetWorkId));
225377
226262
  for (const w2 of snap.works) {
225378
226263
  if (acceptCards.has(w2.id)) continue;
225379
226264
  if (reviewedWorkIds.has(w2.id)) continue;
@@ -225397,15 +226282,57 @@ function buildWorkorderActivity(input) {
225397
226282
  status: ACTIVITY_COPY.acceptPending,
225398
226283
  artifacts: artifactsByWork.get(w2.id) ?? [],
225399
226284
  actions: [
225400
- { kind: "accept", label: BUTTON.acceptPass, target: node2.id, verdict: "approve" },
225401
- { kind: "accept", label: BUTTON.acceptFail, target: node2.id, verdict: "request_changes" }
226285
+ { kind: "accept", label: BUTTON.acceptPass, target: node2.id, revisionId: w2.id, verdict: "approve" },
226286
+ { kind: "accept", label: BUTTON.acceptFail, target: node2.id, revisionId: w2.id, verdict: "request_changes" }
225402
226287
  ]
225403
226288
  });
225404
226289
  }
226290
+ const visibleReviews = /* @__PURE__ */ new Map();
226291
+ const allReviews = activityReviews(snap, events);
226292
+ const reviewWorks = /* @__PURE__ */ new Map();
226293
+ const reviewCards = /* @__PURE__ */ new Map();
226294
+ for (const r of allReviews) {
226295
+ const rows = reviewWorks.get(r.targetWorkId) ?? [];
226296
+ rows.push(r);
226297
+ reviewWorks.set(r.targetWorkId, rows);
226298
+ const cardId = reviewCardId(r.targetWorkId, r.reviewerActorId);
226299
+ const group = reviewCards.get(cardId) ?? { workId: r.targetWorkId, reviewerId: r.reviewerActorId, reviews: [] };
226300
+ group.reviews.push(r);
226301
+ reviewCards.set(cardId, group);
226302
+ }
226303
+ for (const [cardId, { workId, reviewerId, reviews }] of reviewCards) {
226304
+ const summary = reviewSummary(snap, reviews, events);
226305
+ const first = reviews[0];
226306
+ const ids2 = new Set(reviews.map((r) => r.id));
226307
+ const created = events.filter((r) => r.event.kind === "review.create" && ids2.has(r.event.reviewId));
226308
+ const dates = summary.latest.flatMap((r) => [r.createdAt, r.startedAt, r.endedAt, r.decidedAt, r.cancelledAt, r.retryAt].filter((v2) => !!v2));
226309
+ const pending = summary.pending.filter((r) => r.reviewerActorId.startsWith("actor:human:") && (!input.viewerActorId || r.reviewerActorId === input.viewerActorId));
226310
+ visibleReviews.set(cardId, {
226311
+ id: cardId,
226312
+ reviewWorkId: workId,
226313
+ reviewerId,
226314
+ reviewerIds: [reviewerId],
226315
+ traceAvailable: true,
226316
+ seq: created.length ? Math.min(...created.map((r) => r.seq)) : 0,
226317
+ at: first.createdAt,
226318
+ updatedAt: dates.sort().at(-1) ?? first.createdAt,
226319
+ nodeId: first.nodeId,
226320
+ executorId: reviewerId,
226321
+ phase: summary.phase,
226322
+ status: summary.status,
226323
+ 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 ? `
226324
+ ${r.verdict === "request_changes" ? "\u5BA1\u6838\u4E0D\u901A\u8FC7" : "\u5BA1\u6838\u610F\u89C1"}\uFF1A${r.note}` : ""}`).join("\n\n"),
226325
+ artifacts: artifactsByWork.get(workId) ?? [],
226326
+ actions: pending.length ? [
226327
+ { kind: "review", label: BUTTON.approve, target: first.nodeId, revisionId: workId, verdict: "approve" },
226328
+ { kind: "review", label: BUTTON.reject, target: first.nodeId, revisionId: workId, verdict: "request_changes" }
226329
+ ] : []
226330
+ });
226331
+ }
225405
226332
  const drafts = [
225406
226333
  ...standalone,
225407
226334
  ...groupCards.values(),
225408
- ...reviewCards.values(),
226335
+ ...visibleReviews.values(),
225409
226336
  ...issueCards.values(),
225410
226337
  ...acceptCards.values()
225411
226338
  ].sort((a, b2) => a.updatedAt.localeCompare(b2.updatedAt) || a.seq - b2.seq || a.id.localeCompare(b2.id));
@@ -225429,7 +226356,14 @@ function buildWorkorderActivity(input) {
225429
226356
  }
225430
226357
  }
225431
226358
  for (const d of drafts) {
225432
- if (d.id.startsWith("issue:")) d.traceWorkIds = issueWorks.get(d.id.slice("issue:".length));
226359
+ if (!d.id.startsWith("issue:")) continue;
226360
+ const issueId = d.id.slice("issue:".length);
226361
+ if (issueById.get(issueId)?.kind === "gap") {
226362
+ d.traceAvailable = true;
226363
+ d.traceWorkIds = gapOrigins.get(issueId);
226364
+ } else {
226365
+ d.traceWorkIds = issueWorks.get(issueId);
226366
+ }
225433
226367
  }
225434
226368
  const runIdOfCard = (d) => {
225435
226369
  const map = input.runIdByTarget;
@@ -225439,7 +226373,14 @@ function buildWorkorderActivity(input) {
225439
226373
  if (run) return run;
225440
226374
  }
225441
226375
  }
225442
- return d.id.startsWith("review:") ? map?.get(d.id) : void 0;
226376
+ if (d.reviewWorkId) {
226377
+ for (const r of [...reviewWorks.get(d.reviewWorkId) ?? []].reverse()) {
226378
+ if (d.reviewerId && r.reviewerActorId !== d.reviewerId) continue;
226379
+ const run = map?.get(`review:${r.id}`);
226380
+ if (run) return run;
226381
+ }
226382
+ }
226383
+ return void 0;
225443
226384
  };
225444
226385
  const cards = drafts.map((d) => ({
225445
226386
  id: d.id,
@@ -225448,12 +226389,14 @@ function buildWorkorderActivity(input) {
225448
226389
  updatedAt: d.updatedAt,
225449
226390
  ...d.nodeId ? { nodeId: d.nodeId, nodeTitle: nodeName(d.nodeId) } : {},
225450
226391
  executor: ref2(d.executorId),
226392
+ ...d.reviewWorkId ? { reviewWorkId: d.reviewWorkId, reviewers: d.reviewerIds?.map(ref2) } : {},
225451
226393
  ...d.handActorId ? { handActor: ref2(d.handActorId) } : {},
225452
226394
  phase: d.phase,
225453
226395
  status: d.status,
225454
226396
  ...d.detail ? { detail: d.detail } : {},
225455
226397
  artifacts: d.artifacts,
225456
226398
  actions: d.actions,
226399
+ ...d.traceAvailable ? { traceAvailable: true } : {},
225457
226400
  ...d.traceWorkIds?.length ? { traceWorkIds: d.traceWorkIds, traceAvailable: true } : {},
225458
226401
  .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(runIdOfCard(d))
225459
226402
  }));
@@ -225465,6 +226408,7 @@ var init_activity2 = __esm({
225465
226408
  "use strict";
225466
226409
  init_src();
225467
226410
  init_src4();
226411
+ init_review_activity();
225468
226412
  init_workorder_manager();
225469
226413
  ACTIVITY_EVENT_KINDS = [
225470
226414
  "plan.changed",
@@ -225629,12 +226573,21 @@ function buildWorkorderActivityTrace(input) {
225629
226573
  const ref2 = input.ref ?? ((id) => ({ id }));
225630
226574
  const empty2 = { workorderId, cardId, attempts: [] };
225631
226575
  if (!snap) return empty2;
226576
+ const reviews = activityReviews(snap, input.events ?? []);
226577
+ const reviewTarget = reviewCardTarget(cardId, reviews);
226578
+ if (reviewTarget) return buildReviewHistory(input, reviewTarget.targetWorkId, reviewTarget.reviewerActorId, reviews);
225632
226579
  const card2 = input.events ? buildWorkorderActivity({
225633
226580
  workorderId,
225634
226581
  snap,
225635
226582
  events: input.events,
225636
226583
  ref: ref2
225637
226584
  }).cards.find((c) => c.id === cardId) : void 0;
226585
+ const gap = cardId.startsWith("issue:") ? snap.issues.find((i) => i.id === cardId.slice(6) && i.kind === "gap") : void 0;
226586
+ if (gap && !card2?.traceWorkIds?.length) return {
226587
+ ...empty2,
226588
+ works: [],
226589
+ 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"
226590
+ };
225638
226591
  const workIds = card2?.traceWorkIds ?? (cardId.startsWith("work:") ? [cardId.slice(5)] : []);
225639
226592
  if (workIds.length > 0) {
225640
226593
  let firstBriefing;
@@ -225666,6 +226619,7 @@ function buildWorkorderActivityTrace(input) {
225666
226619
  cardId,
225667
226620
  briefing: firstBriefing,
225668
226621
  works,
226622
+ ...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" } : {},
225669
226623
  attempts: works.flatMap((w2) => w2.attempts).sort((a, b2) => a.at.localeCompare(b2.at) || a.runId.localeCompare(b2.runId))
225670
226624
  };
225671
226625
  }
@@ -225781,10 +226735,98 @@ function buildSingleTrace(input) {
225781
226735
  });
225782
226736
  return { workorderId, cardId, briefing, attempts };
225783
226737
  }
226738
+ function buildReviewHistory(input, targetWorkId, reviewerActorId, allReviews) {
226739
+ const { workorderId, cardId } = input;
226740
+ const snap = input.snap;
226741
+ const ref2 = input.ref ?? ((id) => ({ id }));
226742
+ const rows = allReviews.filter((r) => r.targetWorkId === targetWorkId && r.reviewerActorId === reviewerActorId);
226743
+ if (!rows.length) return { workorderId, cardId, attempts: [] };
226744
+ const byId = new Map(rows.map((r) => [r.id, r]));
226745
+ const current = new Set(latestActivityReviews(rows, input.events ?? []).map((r) => r.id));
226746
+ const work = snap.works.find((w2) => w2.id === targetWorkId);
226747
+ const nodeId = rows[0].nodeId;
226748
+ const events = [];
226749
+ for (const rec of [...input.events ?? []].sort((a, b2) => a.seq - b2.seq)) {
226750
+ const e = rec.event;
226751
+ const r = "reviewId" in e && e.reviewId ? byId.get(e.reviewId) : void 0;
226752
+ const direct = e.kind === "review.response" && e.targetWorkId === targetWorkId && e.reviewerActorId === reviewerActorId;
226753
+ let retry = false;
226754
+ if (e.kind === "plan.node_retry" && e.nodeId === nodeId) {
226755
+ const preceding = snap.works.filter((w2) => w2.nodeId === nodeId && w2.createdAt <= rec.createdAt).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
226756
+ retry = preceding[0]?.id === targetWorkId && rows.some((r2) => r2.createdAt <= rec.createdAt);
226757
+ }
226758
+ if (!r && !direct && !retry) continue;
226759
+ let label;
226760
+ let detail;
226761
+ switch (e.kind) {
226762
+ case "review.create":
226763
+ label = "\u521B\u5EFA\u5BA1\u6838";
226764
+ break;
226765
+ case "review.started":
226766
+ label = "\u5F00\u59CB\u5BA1\u6838";
226767
+ break;
226768
+ case "review.timeout":
226769
+ label = "\u5BA1\u6838\u6267\u884C\u8D85\u65F6 / \u5931\u8D25";
226770
+ break;
226771
+ case "review.kill":
226772
+ label = "\u5BA1\u6838\u5DF2\u505C\u6B62";
226773
+ detail = e.reason;
226774
+ break;
226775
+ case "review.response":
226776
+ label = e.verdict === "approve" ? "\u5BA1\u6838\u901A\u8FC7" : "\u5BA1\u6838\u4E0D\u901A\u8FC7";
226777
+ detail = e.note ?? void 0;
226778
+ break;
226779
+ case "plan.node_retry":
226780
+ label = "\u4ECB\u5165\u91CD\u8BD5";
226781
+ break;
226782
+ default:
226783
+ continue;
226784
+ }
226785
+ const actorId = e.kind === "plan.node_retry" ? e.by : "reviewerActorId" in e ? e.reviewerActorId : rec.actorId;
226786
+ events.push({
226787
+ seq: rec.seq,
226788
+ at: rec.createdAt,
226789
+ kind: e.kind,
226790
+ label,
226791
+ actor: ref2(actorId),
226792
+ ...r ? { reviewId: r.id, reviewGroup: r.reviewGroup } : {},
226793
+ ...detail ? { detail } : {}
226794
+ });
226795
+ }
226796
+ const reviews = rows.map((r) => {
226797
+ const single = buildSingleTrace({ ...input, snap: { ...snap, reviews: allReviews }, cardId: `review:${r.id}` });
226798
+ return {
226799
+ reviewId: r.id,
226800
+ reviewGroup: r.reviewGroup,
226801
+ actor: ref2(r.reviewerActorId),
226802
+ at: r.createdAt,
226803
+ ...r.endedAt ?? r.decidedAt ?? r.cancelledAt ? { endedAt: r.endedAt ?? r.decidedAt ?? r.cancelledAt } : {},
226804
+ status: reviewLabel(r),
226805
+ current: current.has(r.id),
226806
+ ...r.note ? { note: r.note } : {},
226807
+ attempts: single.attempts
226808
+ };
226809
+ });
226810
+ return {
226811
+ workorderId,
226812
+ cardId,
226813
+ attempts: reviews.flatMap((r) => r.attempts).sort((a, b2) => a.at.localeCompare(b2.at) || a.runId.localeCompare(b2.runId)),
226814
+ reviewHistory: {
226815
+ targetWorkId,
226816
+ nodeTitle: snap.nodes.find((n) => n.id === nodeId)?.title ?? nodeId,
226817
+ ...work?.outputVersionNo != null ? { outputVersionNo: work.outputVersionNo } : {},
226818
+ artifacts: work ? artifactsOfWork(work, input) : [],
226819
+ reviews,
226820
+ events,
226821
+ ...input.dispatches === void 0 ? { recordsUnavailable: true } : {}
226822
+ }
226823
+ };
226824
+ }
225784
226825
  var init_activity_trace = __esm({
225785
226826
  "../server/src/domains/collab/activity-trace.ts"() {
225786
226827
  "use strict";
225787
226828
  init_src4();
226829
+ init_review_activity();
225788
226830
  init_activity2();
225789
226831
  init_workorder_manager();
225790
226832
  }
@@ -227870,6 +228912,7 @@ function collabDomain(opts) {
227870
228912
  return {
227871
228913
  status: 200,
227872
228914
  body: buildWorkorderActivity({
228915
+ viewerActorId: req.auth.actor,
227873
228916
  workorderId,
227874
228917
  snap,
227875
228918
  events,
@@ -227905,7 +228948,12 @@ function collabDomain(opts) {
227905
228948
  };
227906
228949
  const [snap, events] = engineStore ? await engineStore.transaction(async (tx) => [
227907
228950
  await tx.loadWorkorder(workorderId),
227908
- await tx.listEvents(workorderId, 0, ACTIVITY_EVENT_LIMIT, ACTIVITY_EVENT_KINDS)
228951
+ await tx.listEvents(
228952
+ workorderId,
228953
+ 0,
228954
+ cardId.startsWith(REVIEW_CARD_PREFIX) || cardId.startsWith("review:") ? void 0 : ACTIVITY_EVENT_LIMIT,
228955
+ cardId.startsWith(REVIEW_CARD_PREFIX) || cardId.startsWith("review:") ? REVIEW_TRACE_EVENT_KINDS : ACTIVITY_EVENT_KINDS
228956
+ )
227909
228957
  ]) : [null, []];
227910
228958
  const manifestFiles = snap ? await resolveActivityManifestFiles(snap.artifacts, blobs) : void 0;
227911
228959
  const dispatches = await listDispatchRows(workorderId, opts.dispatchesOfWorkorder);
@@ -228026,6 +229074,9 @@ function collabDomain(opts) {
228026
229074
  return { status: 201, body: { item } };
228027
229075
  });
228028
229076
  router.post("/api/workorders/:id/hold", async (req) => {
229077
+ if (!isHumanActor(req.auth.actor)) {
229078
+ 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" } } };
229079
+ }
228029
229080
  const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
228030
229081
  const ws = req.params.id;
228031
229082
  const exists = [...kernel.model.artifacts.values()].some((a) => a.workspace === ws);
@@ -228065,6 +229116,9 @@ function collabDomain(opts) {
228065
229116
  return { status: 200, body: { cancelled } };
228066
229117
  });
228067
229118
  router.post("/api/workorders/:id/dispatch", async (req) => {
229119
+ if (!isHumanActor(req.auth.actor)) {
229120
+ 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" } } };
229121
+ }
228068
229122
  const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
228069
229123
  const ws = req.params.id;
228070
229124
  const already = !kernel.model.pausedWorkorders.has(ws);
@@ -228390,6 +229444,7 @@ var init_collab = __esm({
228390
229444
  init_workorder_detail();
228391
229445
  init_workorders();
228392
229446
  init_activity2();
229447
+ init_review_activity();
228393
229448
  init_activity_trace();
228394
229449
  init_inbox();
228395
229450
  init_escalation_attention();
@@ -231745,7 +232800,8 @@ function createChatSessionsDomain(opts) {
231745
232800
  ...Object.prototype.hasOwnProperty.call(body2, "projectId") ? { projectId: body2.projectId } : {},
231746
232801
  // 会话级模型覆盖:显式传 null / 空串 = 清除覆盖(回到旧解析);不传这一项 = 不动它。
231747
232802
  ...Object.prototype.hasOwnProperty.call(body2, "model") ? { model: normalizeModel(body2.model) } : {},
231748
- touchedAt: body2.touchedAt ?? (/* @__PURE__ */ new Date()).toISOString()
232803
+ // 只有显式传了才写——改标题 / 换项目 / 换模型都不是「有新动静」,不该顶掉列表顺序与已读水位。
232804
+ ...body2.touchedAt !== void 0 ? { touchedAt: body2.touchedAt } : {}
231749
232805
  });
231750
232806
  return { status: 200, body: await store2.getSession(req.params.id) };
231751
232807
  });
@@ -231964,7 +233020,11 @@ function createChatSessionsDomain(opts) {
231964
233020
  ...record8.label ? { label: record8.label } : {},
231965
233021
  ...record8.taskDigest ? { taskDigest: record8.taskDigest } : {},
231966
233022
  parentMessageId: record8.parentMessageId,
231967
- touches: touches.map((t) => ({ messageId: t.messageId, kind: t.kind, at: t.at })),
233023
+ /* `messageId` 必须过 `touchAnchorMessageId`:流水行的主键是 `(delegation_id, message_id)`,
233024
+ 服务端为此在锚点后缀了一段随机 token(`DelegationService.touchAnchor`)。那段后缀是存储
233025
+ 的去重装置,直接发给前端等于给了一条 `messages[]` 里不存在的 id ——追加转达卡全部掉进
233026
+ floating 堆在流末尾,同一条消息上的两条委派还会因后缀不同被拆成两张单专家卡。 */
233027
+ touches: touches.map((t) => ({ messageId: touchAnchorMessageId(t.messageId), kind: t.kind, at: t.at })),
231968
233028
  artifacts: collectDelegationArtifacts(record8.roundSummaries),
231969
233029
  createdAt: record8.createdAt,
231970
233030
  ...record8.settledAt ? { settledAt: record8.settledAt } : {}
@@ -244470,6 +245530,64 @@ var init_postgres_chat_sessions = __esm({
244470
245530
  async appendMessage(m2, opts) {
244471
245531
  return (await this.appendMessageOnce(m2, opts)).message;
244472
245532
  }
245533
+ /**
245534
+ * 系统状态条的 `chat_items` 补行(ADR-0510 D3 的收口)。
245535
+ *
245536
+ * ## 为什么需要
245537
+ *
245538
+ * `role='system'` 的通告(委派回报「已完成 / 无法完成」、工单终态播报、授权卡状态条)走的是
245539
+ * `appendChatMessageOnce` → 本 store 直写 `chat_messages` 这条路,**不经 `ChatItemLedger`**
245540
+ * (`chat-system-message.ts` 文件头原话:「非 live 直写……不产生 chat_items」)。
245541
+ *
245542
+ * 在 D3 之前这没关系:`/items` 会给没有 item 行的消息合成一条快照条。D3 把历史收成
245543
+ * **只从 `chat_items` 出**、删掉了那条合成路之后,这些通告就从对话页上**凭空消失**了。
245544
+ *
245545
+ * 后果不只是「少看见一条通告」——委派产物的附件卡是**锚在那条回报通告之后的第一条 assistant**
245546
+ * 上的(ws:wo-4a2cd648 §①)。通告不在流里 ⇒ 找不到锚点 ⇒ 卡掉进 floating、渲染到末尾。
245547
+ * 2026-09-09 用户现场:`--continue` 续跑两个子会话,回报通告写进了 `chat_messages`
245548
+ * (07:29:56 / 07:30:12)但 `chat_items` 一行都没有,产物卡因此挂错位置。
245549
+ *
245550
+ * ## 为什么补在这一层
245551
+ *
245552
+ * `appendChatMessageOnce` 是所有系统消息的唯一入口,但那三个调用点(return-flow /
245553
+ * chat-broadcast / server.ts 授权卡)**都拿不到 `ChatItemStore`**,从上面递下来要动三份 deps 契约。
245554
+ * 而这里本来就握着同一个 schema 的连接池——把「用户能看见的消息必有 item 行」这条不变量
245555
+ * 落在存储边界上,比在三个业务处各接一次线更难漏。
245556
+ *
245557
+ * ## 失败了怎么办:吞掉
245558
+ *
245559
+ * item 写失败**不许**把消息本身带走——消息已经 INSERT 成功、幂等键已经占住,抛出去会让调用方
245560
+ * 当成「没写成」而重试,而重试会撞主键。所以这里只记日志。代价是那条通告在页面上仍然看不见,
245561
+ * 与修复前一致,不会更坏。
245562
+ */
245563
+ async writeSystemMessageItem(m2) {
245564
+ try {
245565
+ await this.pool.query(
245566
+ `INSERT INTO ${this.s}.chat_items
245567
+ (id, session_id, message_id, turn_id, run_id, seq, kind, role, status,
245568
+ version, turn_version, provider_item_key, payload, metadata,
245569
+ created_at, updated_at, completed_at)
245570
+ VALUES ($1, $2, $3, $4, NULL, 1, 'text', $5, 'completed',
245571
+ COALESCE((SELECT MAX(version) FROM ${this.s}.chat_items WHERE session_id = $2), 0) + 1,
245572
+ 1, NULL, $6::jsonb, '{"legacyContent": false}'::jsonb,
245573
+ $7::timestamptz, $7::timestamptz, $7::timestamptz)
245574
+ ON CONFLICT (id) DO NOTHING`,
245575
+ [
245576
+ // 与 D3 之前 `/items` 合成条、以及 backfill 补的行**同一套身份**:同一条消息永远只有
245577
+ // 这一个 itemId,重复调用(幂等重写、backfill 再跑)不会造出第二条。
245578
+ `chat-message:${m2.id}`,
245579
+ m2.sessionId,
245580
+ m2.id,
245581
+ `chat-message-turn:${m2.id}`,
245582
+ m2.role,
245583
+ JSON.stringify({ role: m2.role, text: m2.content ?? "" }),
245584
+ m2.createdAt
245585
+ ]
245586
+ );
245587
+ } catch (err) {
245588
+ console.warn(`[chat-items] \u7CFB\u7EDF\u72B6\u6001\u6761\u8865\u884C\u5931\u8D25\uFF08message=${m2.id} session=${m2.sessionId}\uFF09: ${String(err)}`);
245589
+ }
245590
+ }
244473
245591
  async appendMessageOnce(m2, opts) {
244474
245592
  await this.assertSessionInScope(m2.sessionId);
244475
245593
  const insert = async () => {
@@ -244486,7 +245604,9 @@ var init_postgres_chat_sessions = __esm({
244486
245604
  };
244487
245605
  for (let attempt = 0; ; attempt++) {
244488
245606
  try {
244489
- return { message: { ...m2, seq: await insert() }, inserted: true };
245607
+ const written = { message: { ...m2, seq: await insert() }, inserted: true };
245608
+ if ((opts?.source ?? "live") === "system") await this.writeSystemMessageItem(written.message);
245609
+ return written;
244490
245610
  } catch (err) {
244491
245611
  const isUniqueViolation5 = err && typeof err === "object" && err.code === "23505";
244492
245612
  const constraint = String(err.constraint ?? "");
@@ -245138,11 +246258,17 @@ var init_postgres_chat_sessions = __esm({
245138
246258
  model: row.model ?? null
245139
246259
  });
245140
246260
  rowToSessionWithQuality = (row) => {
246261
+ const status = row.last_assistant_status ?? void 0;
245141
246262
  const quality = deriveLastTurnQuality({
245142
- status: row.last_assistant_status ?? void 0,
246263
+ status,
245143
246264
  content: row.last_assistant_content ?? ""
245144
246265
  });
245145
- return { ...rowToSession(row), ...quality ? { lastTurnQuality: quality } : {} };
246266
+ const running = deriveSessionRunning({ status });
246267
+ return {
246268
+ ...rowToSession(row),
246269
+ ...quality ? { lastTurnQuality: quality } : {},
246270
+ ...running ? { running: true } : {}
246271
+ };
245146
246272
  };
245147
246273
  rowToMessage = (row) => ({
245148
246274
  id: row.id,
@@ -255770,6 +256896,41 @@ async function startServe(opts) {
255770
256896
  let nodeTokens = createNodeTokenStore(path29.join(opts.dir, "node-tokens.json"));
255771
256897
  const enrollTokens = createEnrollTokenStore(30 * 60 * 1e3, path29.join(opts.dir, "enroll-tokens.json"));
255772
256898
  let nodeStore = await FileNodeStore.open(path29.join(opts.dir, "nodes.json"));
256899
+ const nodeReportedModelsStore = /* @__PURE__ */ new Map();
256900
+ const modelRefreshWaiters = /* @__PURE__ */ new Map();
256901
+ const modelRefreshInFlight = /* @__PURE__ */ new Map();
256902
+ const modelRefreshLastAt = /* @__PURE__ */ new Map();
256903
+ const MODELS_STALE_MS = 5 * 6e4;
256904
+ const MODELS_BG_REFRESH_MIN_GAP_MS = 6e4;
256905
+ const MODELS_REFRESH_TIMEOUT_MS = 45e3;
256906
+ const refreshNodeModels = (nodeId) => {
256907
+ const running = modelRefreshInFlight.get(nodeId);
256908
+ if (running) return running;
256909
+ if (!hub) return Promise.resolve({ ok: false, reason: "\u672C\u8FDB\u7A0B\u6CA1\u6709\u8282\u70B9\u7F51\u5173" });
256910
+ const online = hub.connectedDaemons().some((d) => d.daemonId === nodeId);
256911
+ if (!online) return Promise.resolve({ ok: false, reason: "\u8282\u70B9\u5F53\u524D\u4E0D\u5728\u7EBF\uFF0C\u7528\u7684\u662F\u5B83\u4E0A\u6B21\u62A5\u7684\u6E05\u5355" });
256912
+ const requestId = (0, import_node_crypto75.randomUUID)();
256913
+ const task = new Promise((resolve10) => {
256914
+ const timer = setTimeout(() => {
256915
+ modelRefreshWaiters.delete(requestId);
256916
+ resolve10({ ok: false, reason: "\u8282\u70B9\u6CA1\u5728 45 \u79D2\u5185\u56DE\u8BDD\uFF08\u53EF\u80FD\u662F\u65E7\u7248\u8282\u70B9\uFF0C\u6216\u8FD9\u53F0\u673A\u5668\u4E0A\u7684 CLI \u63A2\u6D4B\u8D85\u65F6\uFF09" });
256917
+ }, MODELS_REFRESH_TIMEOUT_MS);
256918
+ modelRefreshWaiters.set(requestId, { resolve: () => resolve10({ ok: true }), timer });
256919
+ hub.dispatch(nodeId, { type: "list_models", requestId });
256920
+ }).finally(() => {
256921
+ modelRefreshInFlight.delete(nodeId);
256922
+ modelRefreshLastAt.set(nodeId, Date.now());
256923
+ });
256924
+ modelRefreshInFlight.set(nodeId, task);
256925
+ return task;
256926
+ };
256927
+ const nodeReportedModels = (nodeId, runtimeKind) => {
256928
+ const entry = nodeReportedModelsStore.get(nodeId);
256929
+ const stale = !entry || Date.now() - entry.fetchedAt > MODELS_STALE_MS;
256930
+ const gapOk = Date.now() - (modelRefreshLastAt.get(nodeId) ?? 0) > MODELS_BG_REFRESH_MIN_GAP_MS;
256931
+ if (stale && gapOk) void refreshNodeModels(nodeId).catch(() => ({ ok: false }));
256932
+ return entry?.byKind.get(runtimeKind);
256933
+ };
255773
256934
  const registryAuditFile = path29.join(opts.dir, "registry-audit.ndjson");
255774
256935
  const registryListeners = /* @__PURE__ */ new Set();
255775
256936
  const registryAudit = (record8) => {
@@ -255899,7 +257060,7 @@ async function startServe(opts) {
255899
257060
  listBindingModelIds: async (nodeId, runtimeKind) => {
255900
257061
  const rt = (await nodeStore.listRuntimes(nodeId)).find((r) => r.kind === runtimeKind);
255901
257062
  if (!rt) return [];
255902
- return (await resolveRuntimeModels({ nodeStore, registry: registryStore }, rt.id)).map((m2) => m2.id);
257063
+ return (await resolveRuntimeModels({ nodeStore, registry: registryStore, nodeReportedModels }, rt.id)).map((m2) => m2.id);
255903
257064
  }
255904
257065
  });
255905
257066
  assistantsService = new AssistantsService({
@@ -257437,7 +258598,7 @@ async function startServe(opts) {
257437
258598
  registry: registryStore,
257438
258599
  // 会话级模型覆盖的候选清单:与 `GET /api/runtimes/:id/models` **同一份**实现
257439
258600
  // (resolveRuntimeModels = discovery + 管理员 override),避免「下拉选得到、发出去被拒」。
257440
- listRuntimeModelIds: async (runtimeId) => (await resolveRuntimeModels({ nodeStore, registry: registryStore }, runtimeId)).map((m2) => m2.id),
258601
+ listRuntimeModelIds: async (runtimeId) => (await resolveRuntimeModels({ nodeStore, registry: registryStore, nodeReportedModels }, runtimeId)).map((m2) => m2.id),
257441
258602
  // 诊断 ab94f202 Fix A:add-revision --role 写入口校验用「可分配岗位」集合(岗位目录 AND assignable)。
257442
258603
  // 与 GET /api/roles 的 assignable 派生同式(domains/roles/index.ts):system=true / 已弃用一律不可分配。
257443
258604
  resolveAssignableRoles: async () => new Set(
@@ -257574,6 +258735,8 @@ async function startServe(opts) {
257574
258735
  nodeHealth,
257575
258736
  activeRunCountOf,
257576
258737
  activeRunsOf,
258738
+ nodeReportedModels,
258739
+ refreshNodeModels,
257577
258740
  fetchLatestNpmDaemonVersion
257578
258741
  }),
257579
258742
  collabDomain({
@@ -258883,6 +260046,24 @@ async function startServe(opts) {
258883
260046
  });
258884
260047
  return chatLiveSessions.has(jobKey) ? "live" : "unknown";
258885
260048
  });
260049
+ hub.addMessageListener((daemonId, msg) => {
260050
+ if (msg.type !== "runtime_models") return;
260051
+ const prev = nodeReportedModelsStore.get(daemonId);
260052
+ const byKind = prev?.byKind ?? /* @__PURE__ */ new Map();
260053
+ for (const entry of msg.models) {
260054
+ if (entry.models.length > 0) byKind.set(entry.kind, entry.models);
260055
+ }
260056
+ nodeReportedModelsStore.set(daemonId, { byKind, fetchedAt: Date.now() });
260057
+ if (msg.requestId) {
260058
+ const waiter = modelRefreshWaiters.get(msg.requestId);
260059
+ if (waiter) {
260060
+ clearTimeout(waiter.timer);
260061
+ modelRefreshWaiters.delete(msg.requestId);
260062
+ waiter.resolve();
260063
+ }
260064
+ }
260065
+ console.log(`[models] \u8282\u70B9 ${daemonId} \u4E0A\u62A5\u5019\u9009\u6A21\u578B\uFF1A${msg.models.map((m2) => `${m2.kind}=${m2.models.length}`).join(" ") || "\uFF08\u4E00\u6761\u90FD\u6CA1\u63A2\u5230\uFF09"}`);
260066
+ });
258886
260067
  hub.addMessageListener((daemonId, msg) => {
258887
260068
  if (msg.type !== "gc_query_artifacts") return;
258888
260069
  void (async () => {
@@ -261986,6 +263167,7 @@ var OUTBOX_MAX = 500;
261986
263167
  var PENDING_STREAM_MAX = 2e3;
261987
263168
  var AUTH_REJECT_BACKOFF_MAX_MS = 15 * 6e4;
261988
263169
  var AUTH_REJECT_GIVE_UP_AFTER = 3;
263170
+ var MODEL_REPORT_COOLDOWN_MS = 10 * 6e4;
261989
263171
  var AUTH_REJECT_RE = /Unexpected server response:\s*(401|403)/;
261990
263172
  var DRAIN_TIMEOUT_MS = 15e3;
261991
263173
  var DRAIN_POLL_MS = 50;
@@ -262079,6 +263261,12 @@ var DaemonWsClient = class {
262079
263261
  authRejectStreak = 0;
262080
263262
  /** 本次连接尝试是否因握手 401/403 失败——error 事件里置、close 事件里消费。 */
262081
263263
  lastErrorWasAuthReject = false;
263264
+ /** 模型上报的轮次号:重连会再拉一轮,用它作废上一轮的迟到结果(否则旧结果可能盖住新结果)。 */
263265
+ modelReportGeneration = 0;
263266
+ /** 上一次**探测成功**的时刻;节点频繁掉线重连时靠它避免每分钟把十几个 CLI 重起一遍。 */
263267
+ lastModelReportAt = 0;
263268
+ /** 缓存上一轮结果:冷却期内重连直接重发这份,服务端重启后也能立刻拿到清单。 */
263269
+ lastModelReport = [];
262082
263270
  authRejectGiveUpAfter;
262083
263271
  /** 放弃时的动作;默认干净退出(exit 0),测试注入以免打死测试进程。 */
262084
263272
  onAuthGiveUp;
@@ -262187,6 +263375,7 @@ var DaemonWsClient = class {
262187
263375
  this.flushPendingStream();
262188
263376
  this.resendOutbox();
262189
263377
  this.reportRuntimes = false;
263378
+ void this.reportRuntimeModels();
262190
263379
  this.pingTimer = setInterval(() => {
262191
263380
  this.send({ type: "pong" });
262192
263381
  if (Date.now() - this.lastReceivedAt > this.silenceTimeoutMs) {
@@ -262353,6 +263542,10 @@ var DaemonWsClient = class {
262353
263542
  this.send({ type: "workdir_read_result", requestId: msg.requestId, result });
262354
263543
  break;
262355
263544
  }
263545
+ case "list_models": {
263546
+ void this.reportRuntimeModels({ force: true, requestId: msg.requestId });
263547
+ break;
263548
+ }
262356
263549
  case "update": {
262357
263550
  this.pendingUpdatePkg = msg.pkg;
262358
263551
  if (!this.onUpdate) {
@@ -262470,6 +263663,42 @@ var DaemonWsClient = class {
262470
263663
  this.onUpdate?.(this.pendingUpdatePkg);
262471
263664
  }
262472
263665
  }
263666
+ /**
263667
+ * 拉一遍本机各 runtime 的候选模型清单并上报(`runtime_models` 帧)。
263668
+ *
263669
+ * 串行而不是并发:ACP 家族每探一个都要真起一个 CLI 进程(hermes 40s、oasis-agent 30s 的超时),
263670
+ * 十几个一起起会把节点的内存和文件句柄按住。`listModels` 自带 60 秒缓存,重连时基本是空跑。
263671
+ * 任何一个 runtime 抛错都只丢它自己那一条,不影响其余。全空就不发帧——让服务端走老路,
263672
+ * 而不是用一份空清单把它原本能给出的静态目录盖掉。
263673
+ */
263674
+ async reportRuntimeModels(opts = {}) {
263675
+ const generation = ++this.modelReportGeneration;
263676
+ const reply = (models) => this.send({ type: "runtime_models", models, ...opts.requestId ? { requestId: opts.requestId } : {} });
263677
+ if (!opts.force && this.lastModelReport.length > 0 && Date.now() - this.lastModelReportAt < MODEL_REPORT_COOLDOWN_MS) {
263678
+ reply(this.lastModelReport);
263679
+ return;
263680
+ }
263681
+ const out = [];
263682
+ for (const rt of this.runtimes) {
263683
+ if (generation !== this.modelReportGeneration) return;
263684
+ try {
263685
+ const kind = rt.kind;
263686
+ if (opts.force) clearModelDiscoveryCache(kind, rt.binary);
263687
+ const models = await listModels(kind, rt.binary);
263688
+ if (models.length > 0) out.push({ kind: rt.kind, models });
263689
+ } catch {
263690
+ }
263691
+ }
263692
+ if (generation !== this.modelReportGeneration) return;
263693
+ if (out.length === 0) {
263694
+ if (opts.requestId) reply([]);
263695
+ return;
263696
+ }
263697
+ this.lastModelReport = out;
263698
+ this.lastModelReportAt = Date.now();
263699
+ reply(out);
263700
+ log2("[node-cli]", `\u5DF2\u4E0A\u62A5\u5019\u9009\u6A21\u578B\uFF1A${out.map((r) => `${r.kind}=${r.models.length}`).join(" ")}`);
263701
+ }
262473
263702
  buildMeta() {
262474
263703
  const activeSessions = this.sessions.activeSessions();
262475
263704
  return {
@@ -263050,7 +264279,7 @@ var COMMAND_DECLS = {
263050
264279
  { 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**" },
263051
264280
  { 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" },
263052
264281
  { name: "continue", desc: "\u5F80\u5DF2\u6709\u59D4\u6D3E\u91CC\u518D\u8F6C\u8FBE\u4E00\u53E5\uFF1A\u7ED9 label / \u4E13\u5BB6\u540D / delegationId \u524D\u7F00" },
263053
- { 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" },
264282
+ { 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" },
263054
264283
  { name: "status", desc: "\u770B\u4E00\u4E2A\u59D4\u6D3E\u7684\u8BE6\u60C5\uFF1A\u5168\u91CF\u5404\u8F6E\u5C0F\u7ED3 + \u4EA7\u7269\u6E05\u5355" },
263055
264284
  { 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 },
263056
264285
  { name: "all", desc: "\u914D --list\uFF1A\u8FDE\u5386\u53F2\u59D4\u6D3E\u4E00\u8D77\u5217", boolean: true },
@@ -265536,15 +266765,26 @@ ${round.text}`);
265536
266765
  const text5 = flags.get("text");
265537
266766
  if (!text5) throw new Error('--continue \u8981\u914D --text "<\u8981\u8F6C\u8FBE\u7684\u8BDD>"');
265538
266767
  const target = await resolveRef2(continueRef);
265539
- await api.post(
266768
+ const outcome = await api.post(
265540
266769
  `/api/delegations/${encodeURIComponent(target.id)}/messages`,
265541
266770
  { text: text5, ...fileArgs.length ? { files: fileArgs } : {} }
265542
266771
  );
265543
266772
  if (asJson2) {
265544
- println(JSON.stringify({ delegationId: target.id, label: target.label, files: fileArgs }, null, 2));
266773
+ println(JSON.stringify({ delegationId: target.id, label: target.label, files: fileArgs, ...outcome }, null, 2));
265545
266774
  break;
265546
266775
  }
265547
- println(`\u5DF2\u8F6C\u8FBE\u7ED9 ${target.expertName ?? target.expertActorId}\uFF08${target.label ?? target.id.slice(0, 8)}\uFF09\u3002`);
266776
+ const who = `${target.expertName ?? target.expertActorId}\uFF08${target.label ?? target.id.slice(0, 8)}\uFF09`;
266777
+ if (outcome?.outcome === "inserted") {
266778
+ 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`);
266779
+ } else if (outcome?.outcome === "started-round") {
266780
+ 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`);
266781
+ } else if (outcome?.outcome === "queued") {
266782
+ 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`);
266783
+ 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`);
266784
+ 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");
266785
+ } else {
266786
+ println(`\u5DF2\u8F6C\u8FBE\u7ED9 ${who}\u3002`);
266787
+ }
265548
266788
  if (fileArgs.length) println(` \u4E00\u8D77\u5E26\u8FC7\u53BB\u7684\u6587\u4EF6\uFF1A${fileArgs.join("\u3001")}`);
265549
266789
  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");
265550
266790
  break;
@@ -268340,7 +269580,7 @@ function shimScript() {
268340
269580
  }
268341
269581
 
268342
269582
  // src/index.ts
268343
- var PKG_VERSION = true ? "2.2.2" : "dev";
269583
+ var PKG_VERSION = true ? "2.2.4" : "dev";
268344
269584
  var LOCAL_BIN = localBin();
268345
269585
  var NPM_PREFIX = npmPrefix();
268346
269586
  var INSTANCE = DEFAULT_INSTANCE;