oasis_test 0.1.106 → 0.1.108

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 +623 -175
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -993,20 +993,27 @@ function pinnedReviewerOf(model, artifactId, role) {
993
993
  }
994
994
  return best?.actor ?? null;
995
995
  }
996
+ function latestAttempt(attempts) {
997
+ let best = null;
998
+ for (const a of attempts) {
999
+ if (best === null || a.at > best.at || a.at === best.at && a.seq >= best.seq) best = a;
1000
+ }
1001
+ return best;
1002
+ }
996
1003
  function concludeMilestones(model, id) {
997
1004
  return (model.concludeAttempts.get(id) ?? []).filter(
998
1005
  (a) => attemptStatus(model, a) === "satisfied"
999
1006
  );
1000
1007
  }
1001
1008
  function pendingConcludeAttempt(model, id) {
1002
- const last = model.concludeAttempts.get(id)?.at(-1);
1009
+ const last = latestAttempt(model.concludeAttempts.get(id) ?? []);
1003
1010
  return last && attemptStatus(model, last) === "pending" ? last : null;
1004
1011
  }
1005
1012
  function isConcluded(model, id) {
1006
1013
  return concludeMilestones(model, id).length > 0;
1007
1014
  }
1008
1015
  function concludedHead(model, id) {
1009
- return concludeMilestones(model, id).at(-1)?.head ?? null;
1016
+ return latestAttempt(concludeMilestones(model, id))?.head ?? null;
1010
1017
  }
1011
1018
  function revisionsOf(model, id) {
1012
1019
  return [...model.revisions.values()].filter((r) => r.artifactId === id);
@@ -3429,12 +3436,19 @@ async function assembleContext(args) {
3429
3436
  if (args.resolveUpstreamHandoff && artifact.workspace) {
3430
3437
  for (const edge of artifact.inputs) {
3431
3438
  if (edge.pinned === null) continue;
3432
- const content = await args.resolveUpstreamHandoff({
3433
- workOrderId: artifact.workspace,
3434
- downstreamNodeId: artifact.id,
3435
- upstreamNodeId: edge.to,
3436
- pinnedWorkId: edge.pinned
3437
- });
3439
+ let content = null;
3440
+ try {
3441
+ content = await args.resolveUpstreamHandoff({
3442
+ workOrderId: artifact.workspace,
3443
+ downstreamNodeId: artifact.id,
3444
+ upstreamNodeId: edge.to,
3445
+ pinnedWorkId: edge.pinned
3446
+ });
3447
+ } catch (err) {
3448
+ console.warn(
3449
+ `[assemble] ${artifact.id} \u4E0A\u6E38 ${edge.to} \u4EA4\u63A5\u89E3\u6790\u5931\u8D25\uFF0C\u672C\u8F6E\u4E0D\u6CE8\u5165 _HANDOFF.md\uFF1A` + (err instanceof Error ? err.message : String(err))
3450
+ );
3451
+ }
3438
3452
  if (content) {
3439
3453
  const dir = `inputs/${slug2(edge.to)}@${edge.pinned.slice(9, 17)}`;
3440
3454
  upstreamHandoffFiles.set(`${dir}/${UPSTREAM_HANDOFF_BASENAME}`, content);
@@ -4061,6 +4075,25 @@ ${m2.spec.split("\n").map((l) => ` > ${l}`).join("\n")}`);
4061
4075
  });
4062
4076
  })()
4063
4077
  ),
4078
+ // ★ 2026-08-18:交稿时把「系统看不出来的四件事」一并带上。
4079
+ //
4080
+ // 为什么必须写在**任务书**里:adapter 只把 TASK.md 当提示词喂给模型,其余 bundle 文件(包括
4081
+ // execution/CONTINUITY.md)只是躺在工作目录里、没人读。`--reason` 之所以交付过的节点 100% 都有,
4082
+ // 正因为它既进了任务书又是必填;这四项此前只活在 `oasis help propose` 里——next 上节点全部更新到
4083
+ // 认得它们之后的 10 小时里,两次真实交付**一次都没带**(实测 0/2)。
4084
+ //
4085
+ // 给 `none` 出口是刻意的:逼着写自由文本只会得到「暂无」这种假保证,而「确认没有」与「没人写」
4086
+ // 对下游是两条完全不同的信息(渲染侧据此分流,见 renderDeliveredWorkHandoff)。
4087
+ ...args.action === "review" ? [] : [
4088
+ ``,
4089
+ `### \u4EA4\u7A3F\u65F6\u4E00\u5E76\u5E26\u4E0A\uFF1A\u4E0B\u6E38\u8981\u7528\u7684\u56DB\u4EF6\u4E8B`,
4090
+ `\u8FD9\u56DB\u9879\u4F1A\u539F\u6837\u51FA\u73B0\u5728\u4E0B\u6E38\u8282\u70B9\u7684 \`inputs/<\u672C\u8282\u70B9>@<\u7248\u672C>/${UPSTREAM_HANDOFF_BASENAME}\` \u91CC\u3002\u5206\u53F7\u5206\u9694\uFF1B**\u786E\u5B9E\u6CA1\u6709\u5C31\u5199 \`none\`\uFF0C\u522B\u7F16\u300C\u6682\u65E0\u300D**\uFF1A`,
4091
+ `- \`--gaps\` **\u4F60\u6CA1\u80FD\u6838\u9A8C\u7684\u90E8\u5206** \u2014\u2014 \u6700\u8981\u7D27\u7684\u4E00\u9879\u3002\u4E0B\u6E38\u9ED8\u8BA4\u300C\u4E0A\u6E38\u4EA4\u4E86\u5C31\u662F\u9A8C\u8FC7\u4E86\u300D\uFF0C\u4F60\u4E0D\u5199\uFF0C\u6CA1\u9A8C\u7684\u90A3\u5757\u5C31\u76F4\u63A5\u8FDB\u4E0B\u4E00\u73AF\u3002\u4F8B\uFF1A\`--gaps "\u6CA1\u505A\u538B\u6D4B;\u53EA\u5728 dev \u9A8C\u8FC7"\``,
4092
+ `- \`--unresolved\` **\u4F60\u77E5\u9053\u4F46\u6CA1\u89E3\u51B3\u7684** \u2014\u2014 \u514D\u5F97\u4E0B\u6E38\u91CD\u590D\u8E29\u4F60\u5DF2\u7ECF\u8E29\u8FC7\u7684\u5751\u3002\u4F8B\uFF1A\`--unresolved "\u7EBF\u4E0A\u914D\u989D\u6CA1\u786E\u8BA4"\``,
4093
+ `- \`--risks\` **\u4EA4\u4ED8\u7269\u672C\u8EAB\u6CA1\u9519\u3001\u4F46\u7528\u7684\u65F6\u5019\u8981\u5F53\u5FC3\u7684** \u2014\u2014 \u4F8B\uFF1A\`--risks "\u5E76\u53D1\u8C03\u7528\u53EF\u80FD\u89E6\u53D1\u9650\u6D41"\``,
4094
+ `- \`--next\` **\u5EFA\u8BAE\u4E0B\u6E38\u5148\u505A\u54EA\u4E00\u6B65**\uFF08\u53EF\u9009\uFF09\u2014\u2014 \u4F8B\uFF1A\`--next "\u5148\u8DD1\u4E00\u6B21\u5C0F\u6D41\u91CF\u7070\u5EA6"\``,
4095
+ `\u5199 \`none\` = \u300C\u6211\u786E\u8BA4\u6CA1\u6709\u300D\uFF1B\u4E0D\u5199 = \u300C\u6CA1\u4EBA\u5199\u8FC7\u300D\u3002**\u8FD9\u4E24\u4EF6\u4E8B\u5BF9\u4E0B\u6E38\u5B8C\u5168\u4E0D\u540C\uFF0C\u522B\u6DF7\u3002**`
4096
+ ],
4064
4097
  // 兜底(§10):上次失败了 → 明确告诉它发生了什么 + 怎么破(首选 add-revision 拆出去)
4065
4098
  ...args.lastFailure ? args.lastFailure.resumed ? [
4066
4099
  // 续跑分支:上文还在它眼前,别再说"没留下任何产出"(自相矛盾),改成"接着干 + 先核现场"。
@@ -6024,11 +6057,18 @@ var init_dispatcher = __esm({
6024
6057
  bundle.files["execution/CONTINUITY.md"] = this.opts.executionContinuityInstructions;
6025
6058
  }
6026
6059
  if (this.opts.resolveExecutionResumePack && (spec.action === "produce" || spec.action === "integrate") && continuityArtifact) {
6027
- const resumePack = await this.opts.resolveExecutionResumePack(
6028
- spec.artifactId,
6029
- continuityArtifact.workspace,
6030
- { jobKey, ...spec.part !== void 0 ? { part: spec.part } : {} }
6031
- );
6060
+ let resumePack = null;
6061
+ try {
6062
+ resumePack = await this.opts.resolveExecutionResumePack(
6063
+ spec.artifactId,
6064
+ continuityArtifact.workspace,
6065
+ { jobKey, ...spec.part !== void 0 ? { part: spec.part } : {} }
6066
+ );
6067
+ } catch (err) {
6068
+ console.warn(
6069
+ `[dispatch] ${spec.artifactId} \u6062\u590D\u5305\u89E3\u6790\u5931\u8D25\uFF0C\u672C\u8F6E\u6309\u9996\u6B21\u6267\u884C\u6D3E\u53D1\uFF08\u4E0D\u6CE8\u5165 execution/RESUME.md\uFF09\uFF1A` + (err instanceof Error ? err.message : String(err))
6070
+ );
6071
+ }
6032
6072
  if (resumePack) {
6033
6073
  bundle.files["execution/RESUME.md"] = resumePack;
6034
6074
  bundle.files["TASK.md"] += [
@@ -6700,6 +6740,15 @@ var init_session_limits = __esm({
6700
6740
  });
6701
6741
 
6702
6742
  // ../engine/src/model.ts
6743
+ function isMainWork(w2) {
6744
+ return w2.lane === "main";
6745
+ }
6746
+ function isReplyWork(w2) {
6747
+ return w2.lane === "reply";
6748
+ }
6749
+ function replyIssueIdOf(w2) {
6750
+ return w2.lane === "reply" ? w2.replyToIssueId ?? null : null;
6751
+ }
6703
6752
  var BUSINESS_HANDOFF_POLICY_FIELD, BUSINESS_HANDOFF_POLICY_EXEMPT, DEFAULT_CONFIG, deriveWorkId, deriveReplyWorkId, deriveReviewId, deriveReplyId;
6704
6753
  var init_model = __esm({
6705
6754
  "../engine/src/model.ts"() {
@@ -6798,7 +6847,7 @@ function isReviewOpen(r) {
6798
6847
  return !r.cancelledAt && !r.endedAt && !r.verdict;
6799
6848
  }
6800
6849
  function countFailedForNode(works) {
6801
- const sorted = [...works].filter((w2) => !w2.replyToIssueId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6850
+ const sorted = works.filter(isMainWork).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6802
6851
  let count2 = 0;
6803
6852
  for (const w2 of sorted) {
6804
6853
  if (w2.retryAt) break;
@@ -6814,7 +6863,7 @@ function countFailedForNode(works) {
6814
6863
  return count2;
6815
6864
  }
6816
6865
  function countFailedForReplyRound(works) {
6817
- const sorted = works.filter((w2) => w2.replyToIssueId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6866
+ const sorted = works.filter(isReplyWork).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
6818
6867
  let count2 = 0;
6819
6868
  for (const w2 of sorted) {
6820
6869
  if (w2.retryAt) break;
@@ -6858,6 +6907,7 @@ function findUnresolvedCRFromOthers(nodeId, issues) {
6858
6907
  var init_views2 = __esm({
6859
6908
  "../engine/src/views.ts"() {
6860
6909
  "use strict";
6910
+ init_model();
6861
6911
  }
6862
6912
  });
6863
6913
 
@@ -6866,6 +6916,7 @@ var WorkorderState;
6866
6916
  var init_state = __esm({
6867
6917
  "../engine/src/state.ts"() {
6868
6918
  "use strict";
6919
+ init_model();
6869
6920
  init_views2();
6870
6921
  WorkorderState = class {
6871
6922
  workorder;
@@ -6925,12 +6976,25 @@ var init_state = __esm({
6925
6976
  allWorks() {
6926
6977
  return [...this.workMap.values()];
6927
6978
  }
6928
- worksOf(nodeId) {
6979
+ /**
6980
+ * ★ 该节点的**全部** work——**主链与回信轮都在里面**(刺眼命名:调它 = 显式声明「我不按 lane 过滤」)。
6981
+ * 只有确实需要「所有运行」的入口才用它(work 编号 = 现有数 +1、I1 占位判定、连败链计数——那些函数自己再按
6982
+ * lane 分)。要主链就 {@link mainWorksOf}、要回信就 {@link replyWorksOf},别在调用点手写 `!replyToIssueId`。
6983
+ */
6984
+ allWorksIncludingReplies(nodeId) {
6929
6985
  return this.allWorks().filter((w2) => w2.nodeId === nodeId);
6930
6986
  }
6931
- /** 该节点有没有还占着名额的运行(I1 判据的一半,另一半见 hasQueuedWorkFor)。 */
6987
+ /** 该节点的**主链** work(派发/验收/发版本号那条道)。判据 = lane({@link isMainWork})。 */
6988
+ mainWorksOf(nodeId) {
6989
+ return this.allWorksIncludingReplies(nodeId).filter(isMainWork);
6990
+ }
6991
+ /** 该节点的**回信轮** work(对 comment 的答话那条道)。判据 = lane({@link isReplyWork})。 */
6992
+ replyWorksOf(nodeId) {
6993
+ return this.allWorksIncludingReplies(nodeId).filter(isReplyWork);
6994
+ }
6995
+ /** 该节点有没有还占着名额的运行(I1 判据的一半,另一半见 hasQueuedWorkFor)。主链或回信都占名额。 */
6932
6996
  openWorkOf(nodeId) {
6933
- return this.worksOf(nodeId).find(isWorkOpen);
6997
+ return this.allWorksIncludingReplies(nodeId).find(isWorkOpen);
6934
6998
  }
6935
6999
  artifactsOf(workId) {
6936
7000
  return this.artifactList.filter((a) => a.workId === workId);
@@ -6972,7 +7036,7 @@ var init_state = __esm({
6972
7036
  }
6973
7037
  /** 本事务是否已对该节点的某个 work 排了 work.accept——activate 据此避让。 */
6974
7038
  hasAcceptedWorkFor(nodeId) {
6975
- const myWorks = new Set(this.worksOf(nodeId).map((w2) => w2.id));
7039
+ const myWorks = new Set(this.allWorksIncludingReplies(nodeId).map((w2) => w2.id));
6976
7040
  return this.emittedList.some(
6977
7041
  (e) => e.kind === "work.accept" && myWorks.has(e.workId)
6978
7042
  );
@@ -7135,7 +7199,7 @@ function scan(state, ctx) {
7135
7199
  continue;
7136
7200
  }
7137
7201
  if (wstate === "failed") {
7138
- const cf = countFailedForNode(state.worksOf(node.id));
7202
+ const cf = countFailedForNode(state.allWorksIncludingReplies(node.id));
7139
7203
  const overLimit = cf > (ctx.config?.maxRetries ?? DEFAULT_CONFIG.maxRetries);
7140
7204
  if (overLimit && classify(node.assigneeActorId) === "agent") {
7141
7205
  events.push({
@@ -7194,7 +7258,7 @@ function tryToRun(state, nodeId, ctx, events) {
7194
7258
  if (isBusinessDependencyEdge(state, e) && !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestAcceptId)) return;
7195
7259
  }
7196
7260
  if (state.hasQueuedWorkFor(nodeId)) return;
7197
- const workId = deriveWorkId(nodeId, state.worksOf(nodeId).length + 1);
7261
+ const workId = deriveWorkId(nodeId, state.allWorksIncludingReplies(nodeId).length + 1);
7198
7262
  events.push({
7199
7263
  kind: "work.create",
7200
7264
  workorderId: state.workorder.id,
@@ -7318,7 +7382,7 @@ function processCommentReplies(state, nodeId, ctx, events) {
7318
7382
  const comments = state.allIssues().filter((i) => i.kind === "comment" && i.resolvedAt === null && i.aboutNodeId === nodeId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
7319
7383
  if (comments.length === 0) return;
7320
7384
  if (state.hasQueuedWorkFor(nodeId)) return;
7321
- const replyWorks = state.worksOf(nodeId).filter((w2) => w2.replyToIssueId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
7385
+ const replyWorks = state.replyWorksOf(nodeId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
7322
7386
  const inflight = replyWorks.find((w2) => !w2.endedAt && !w2.deadAt);
7323
7387
  if (inflight) {
7324
7388
  const start = inflight.startedAt ?? inflight.createdAt;
@@ -7483,16 +7547,24 @@ function handoffBlock(state, nodeId, workId) {
7483
7547
  overridable: false
7484
7548
  };
7485
7549
  }
7486
- const delivered = work?.businessHandoffStatus === "delivered" || work?.status === "success" && work.agentHandoffAttemptId === null && work.businessHandoffStatus === null;
7487
- if (delivered) return null;
7488
- return {
7489
- code: "handoff-undeclared",
7490
- detail: `\u4E1A\u52A1\u4EA4\u63A5\u672A\u5B8C\u6210\uFF1Awork.status=${work?.status ?? "?"}\u3001handoff=${work?.businessHandoffStatus ?? "\u672A\u58F0\u660E"}${work?.agentHandoffAttemptId ? `\uFF08attempt ${work.agentHandoffAttemptId}\uFF09` : ""}\u2014\u2014\u7B49\u6267\u884C\u4EBA\u8865\u4EA4\u63A5\uFF0C\u6216\u7531\u4EBA force-conclude \u7B7E\u5B57\u7A7F\u900F`,
7491
- overridable: true
7492
- };
7550
+ if (work?.businessHandoffStatus === "not_delivered") {
7551
+ return {
7552
+ code: "handoff-undeclared",
7553
+ detail: `\u6267\u884C\u4EBA\u660E\u786E\u58F0\u660E\u672C\u8F6E\u672A\u4EA4\u4ED8\uFF08not_delivered${work.agentHandoffAttemptId ? `\uFF0Cattempt ${work.agentHandoffAttemptId}` : ""}\uFF09\u2014\u2014\u7B49\u8FD9\u4E00\u7248\u91CD\u8DD1\u4EA4\u4ED8\uFF0C\u6216\u7531\u4EBA force-conclude \u7B7E\u5B57\u7A7F\u900F`,
7554
+ overridable: true
7555
+ };
7556
+ }
7557
+ if (work?.status !== "success" && work?.businessHandoffStatus !== "delivered") {
7558
+ return {
7559
+ code: "work-not-success",
7560
+ detail: `\u8FD9\u4E00\u7248\u4E0D\u662F\u6709\u6548\u4EA7\u51FA\uFF08work.status=${work?.status ?? "?"}\uFF09\u2014\u2014\u6539\u5BF9\u6700\u8FD1\u4E00\u7248\u6210\u529F\u7684\u4EA7\u51FA\u6536\u53E3\uFF0C\u6216\u7531\u4EBA force-conclude \u7B7E\u5B57\u7A7F\u900F`,
7561
+ overridable: true
7562
+ };
7563
+ }
7564
+ return null;
7493
7565
  }
7494
7566
  function requiresBusinessHandoff(state, nodeId, work) {
7495
- if (work?.replyToIssueId) return false;
7567
+ if (work && isReplyWork(work)) return false;
7496
7568
  if (!state.edgesFrom(nodeId).some((edge) => isBusinessDependencyEdge(state, edge))) return false;
7497
7569
  if (typeof work?.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) return false;
7498
7570
  return true;
@@ -7506,12 +7578,12 @@ function activate(state, nodeId, _ctx) {
7506
7578
  if (!node.assigneeActorId) return "no_assignee";
7507
7579
  if (state.openWorkOf(nodeId)) return "already_running";
7508
7580
  if (state.hasQueuedWorkFor(nodeId)) return "already_running";
7509
- if (state.worksOf(nodeId).some((w2) => w2.acceptanceState === "draft")) return "in_review";
7581
+ if (state.allWorksIncludingReplies(nodeId).some((w2) => w2.acceptanceState === "draft")) return "in_review";
7510
7582
  const unresolvedSelf = findUnresolvedSelfIssue(nodeId, state.allIssues());
7511
7583
  if (unresolvedSelf) return unresolvedSelf.kind === "escalation" ? "failed" : "waiting_answer";
7512
7584
  const inbound = state.edgesInto(nodeId);
7513
7585
  if (inbound.some((e) => e.required && e.kind === "data" && !e.pinnedWorkId && !isEdgeInputUnobtainable(state, e))) return "upstream_not_ready";
7514
- if (inbound.some((e) => state.worksOf(e.fromNodeId).some(isWorkOpen))) return "upstream_unstable";
7586
+ if (inbound.some((e) => state.allWorksIncludingReplies(e.fromNodeId).some(isWorkOpen))) return "upstream_unstable";
7515
7587
  return "dispatched";
7516
7588
  }
7517
7589
  function assessConvergence(state, ctx) {
@@ -7528,6 +7600,7 @@ var init_activate = __esm({
7528
7600
  "use strict";
7529
7601
  init_src2();
7530
7602
  init_model();
7603
+ init_model();
7531
7604
  init_views2();
7532
7605
  }
7533
7606
  });
@@ -7626,7 +7699,10 @@ CREATE TABLE IF NOT EXISTS ${schema}.works (
7626
7699
  outcome_detail jsonb,
7627
7700
  session_ref text,
7628
7701
  continues_work_id text,
7629
- -- \u56DE\u4FE1 work\uFF08\u8BBE\u8BA1-\u6700\u7EC8 \xA7\u56DE\u4FE1\u8F6E\uFF09\uFF1A\u975E\u7A7A = \u5BF9\u8BE5 comment issue \u7684\u56DE\u4FE1\u8F6E\uFF0C\u4E0D\u8FDB\u8282\u70B9\u4E3B\u94FE
7702
+ -- \u2605 work \u5C5E\u4E8E\u54EA\u6761\u9053\uFF08\u6B63\u5411\u3001\u663E\u5F0F\u4E8B\u5B9E\uFF09\uFF1A'main'=\u8282\u70B9\u4E3B\u94FE\u4EA7\u51FA / 'reply'=\u56DE\u4FE1\u8F6E\u3002\u53D6\u4EE3\u65E7\u7684\u5426\u5B9A\u5F0F\u5224\u636E
7703
+ -- "reply_to_issue_id IS NULL"\u3002\u5B58\u91CF\u884C\u6309 reply_to_issue_id \u56DE\u586B\uFF08\u89C1\u4E0B\u65B9\u8FC1\u79FB\u6BB5\uFF09\u3002
7704
+ lane text CHECK (lane IN ('main','reply')),
7705
+ -- \u56DE\u4FE1\u8F6E\u6307\u5411\u54EA\u6761 comment issue\uFF08**\u7EAF\u6570\u636E**\uFF1A\u975E\u7A7A\u4EC5\u8868\u793A\u300C\u7B54\u54EA\u6761\u4FE1\u300D\uFF0C\u662F\u4E0D\u662F\u56DE\u4FE1\u8F6E\u7531 lane \u5224\uFF09
7630
7706
  reply_to_issue_id text,
7631
7707
  output_version_no integer,
7632
7708
  conclusion text,
@@ -7807,8 +7883,15 @@ ALTER TABLE ${schema}.works DROP CONSTRAINT IF EXISTS works_business_handoff_sta
7807
7883
  ALTER TABLE ${schema}.works ADD CONSTRAINT works_business_handoff_status_check
7808
7884
  CHECK (business_handoff_status IS NULL OR business_handoff_status IN ('delivered','not_delivered'));
7809
7885
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS status text;
7810
- -- \u56DE\u4FE1 work\uFF08\u8BBE\u8BA1-\u6700\u7EC8 \xA7\u56DE\u4FE1\u8F6E\uFF09\uFF1A\u975E\u7A7A = \u56DE\u4FE1\u8F6E\uFF08\u951A\u6700\u65E9\u672A\u89E3 comment\uFF09\uFF0C\u4E0D\u8FDB\u8282\u70B9\u4E3B\u94FE
7886
+ -- \u56DE\u4FE1\u8F6E\u6307\u5411\u54EA\u6761 comment issue\uFF08\u7EAF\u6570\u636E\uFF09
7811
7887
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
7888
+ -- \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
7889
+ -- \u53EA\u586B\u5F53\u524D\u4E3A NULL \u7684\u884C \u2192 \u5E42\u7B49\uFF08\u91CD\u542F\u4E0D\u8986\u76D6 handler \u5DF2\u5199\u7684\u503C\uFF09\u3002\u8BFB\u9762\u53E6\u6709\u515C\u5E95\uFF08store-postgres \u8BFB\u6620\u5C04\uFF09\u3002
7890
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS lane text;
7891
+ UPDATE ${schema}.works SET lane = CASE WHEN reply_to_issue_id IS NULL THEN 'main' ELSE 'reply' END
7892
+ WHERE lane IS NULL;
7893
+ ALTER TABLE ${schema}.works DROP CONSTRAINT IF EXISTS works_lane_check;
7894
+ ALTER TABLE ${schema}.works ADD CONSTRAINT works_lane_check CHECK (lane IS NULL OR lane IN ('main','reply'));
7812
7895
  -- \u2605 issue \u5B58\u6D3B\u5224\u636E\u552F\u4E00\u5316\uFF08ADR 0144\uFF09\uFF1Aresolution \u627F\u63A5\u65E7 state \u7684 closed\uFF08=wontfix\uFF09\uFF1B
7813
7896
  -- \u968F\u540E state / blocking \u5217\u5220\u9664\uFF08\u4F9D\u8D56 state \u7684 CHECK \u4E0E\u4E09\u4E2A\u65E7\u90E8\u5206\u7D22\u5F15\u968F\u5217\u81EA\u52A8\u5220\u9664\uFF09\u3002
7814
7897
  -- \u56DE\u586B\u5FC5\u987B\u5728\u5220\u5217\u524D\u3001\u4E14\u53EA\u5728 state \u5217\u8FD8\u5B58\u5728\u65F6\u6267\u884C\uFF08DO \u5757\u5224 information_schema\uFF0C\u91CD\u590D\u542F\u52A8\u5E42\u7B49\uFF09\u3002
@@ -7937,14 +8020,13 @@ FROM ${schema}.workflow_nodes n
7937
8020
  LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id
7938
8021
  LEFT JOIN ${schema}.works ls ON ls.id = n.latest_success_work_id;
7939
8022
 
8023
+ -- workorder_view \u5DF2\u4E0B\u7EBF\uFF08\u5DE5\u5355 1aa1f35b\uFF09\uFF1A\u5B83\u53EA\u989D\u5916\u7ED9\u51FA total_count / done_count \u4E24\u4E2A\u8FDB\u5EA6\u6570\uFF0C\u5168\u4ED3\u96F6\u6D88\u8D39\u65B9\uFF0C
8024
+ -- \u4E14\u53E3\u5F84\u4E0E\u540E\u7AEF\u771F\u6E90\uFF08server summaryOfWorkspace \u7684 progressExcludingBrief\uFF09\u4E0D\u540C\u2014\u2014total \u53EA\u6392 cancelled\u3001done \u7528
8025
+ -- latest_accept_id IS NOT NULL\uFF08\u66FE\u88AB\u9A8C\u6536\u8FC7\u81F3\u5C11\u4E00\u6B21\uFF09\uFF0C\u800C\u771F\u6E90\u6392\u9664 brief + \u5DF2\u4F5C\u5E9F\u3001\u6309\u300C\u9A8C\u6536\u843D\u5728\u5F53\u524D\u7248\u300D\u7B97\u3002
8026
+ -- \u4FDD\u7559\u4E00\u4EFD\u89C6\u56FE\u65E2\u6CA1\u4EBA\u8BFB\u3001\u53E3\u5F84\u53C8\u5BF9\u4E0D\u4E0A\uFF0C\u53EA\u4F1A\u662F\u7B2C\u4E09\u5904\u5BF9\u4E0D\u4E0A\u7684\u8FDB\u5EA6\u6570\u3002\u8FD9\u91CC\u4E0D\u518D CREATE\uFF1B\u4E0B\u9762\u7684 DROP \u8BA9\u5B58\u91CF\u5E93
8027
+ -- \u5728\u4E0B\u6B21 migrate \u65F6\u628A\u65E7\u89C6\u56FE\u5220\u6389\uFF08ENGINE_DDL \u6BB5\u4EA6\u6709\u4E00\u5904 DROP\uFF0C\u4E8C\u8005\u90FD\u7559\u4EE5\u9632\u91CD\u6392\uFF09\u3002\u5DE5\u5355\u8FDB\u5EA6\u7EDF\u4E00\u8D70\u540E\u7AEF\u8BFB\u6A21\u578B\uFF0C
8028
+ -- \u9700\u8981 SQL \u4FA7\u7EDF\u8BA1\u65F6\u53E6\u7ACB\u4E0E\u771F\u6E90\u540C\u53E3\u5F84\u7684\u89C6\u56FE\u5E76\u6CE8\u660E\u670D\u52A1\u5BF9\u8C61\uFF0C\u522B\u590D\u6D3B\u8FD9\u4E24\u4E2A\u6570\u3002
7940
8029
  DROP VIEW IF EXISTS ${schema}.workorder_view;
7941
- CREATE VIEW ${schema}.workorder_view AS
7942
- SELECT o.*,
7943
- (SELECT count(*) FROM ${schema}.workflow_nodes n
7944
- WHERE n.workorder_id = o.id AND n.cancelled_at IS NULL) AS total_count,
7945
- (SELECT count(*) FROM ${schema}.workflow_nodes n
7946
- WHERE n.workorder_id = o.id AND n.cancelled_at IS NULL AND n.latest_accept_id IS NOT NULL) AS done_count
7947
- FROM ${schema}.workorders o;
7948
8030
  `;
7949
8031
  }
7950
8032
  var init_schema2 = __esm({
@@ -13158,7 +13240,10 @@ var init_store_postgres = __esm({
13158
13240
  JOIN ${this.s}.workflow_nodes n ON n.id = r.node_id WHERE n.workorder_id = $1`,
13159
13241
  [id]
13160
13242
  );
13161
- const works = await this.c.query(`SELECT * FROM ${this.s}.works WHERE workorder_id = $1`, [id]);
13243
+ const works = await this.c.query(
13244
+ `SELECT * FROM ${this.s}.works WHERE workorder_id = $1 ORDER BY created_at ASC, id ASC`,
13245
+ [id]
13246
+ );
13162
13247
  const arts = await this.c.query(
13163
13248
  `SELECT a.* FROM ${this.s}.work_artifacts a JOIN ${this.s}.works w ON w.id = a.work_id WHERE w.workorder_id = $1`,
13164
13249
  [id]
@@ -13259,6 +13344,9 @@ var init_store_postgres = __esm({
13259
13344
  outcomeDetail: w2.outcome_detail,
13260
13345
  sessionRef: w2.session_ref,
13261
13346
  continuesWorkId: w2.continues_work_id,
13347
+ // ★ lane 兜底(存量行迁移前 / 迁移与回填之间的竞态窗):无值时按 reply_to_issue_id 派生,
13348
+ // 保证 TS 侧 `Work.lane` 恒非空。reply_to_issue_id 在读写映射层可直接读(护栏白名单)。
13349
+ lane: w2.lane ?? (w2.reply_to_issue_id == null ? "main" : "reply"),
13262
13350
  replyToIssueId: w2.reply_to_issue_id ?? null,
13263
13351
  outputVersionNo: w2.output_version_no,
13264
13352
  conclusion: w2.conclusion,
@@ -13470,8 +13558,8 @@ var init_store_postgres = __esm({
13470
13558
  (id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
13471
13559
  outcome_detail,session_ref,continues_work_id,reply_to_issue_id,output_version_no,
13472
13560
  node_version,conclusion,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
13473
- acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason)
13474
- 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)
13561
+ acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason,lane)
13562
+ 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)
13475
13563
  ON CONFLICT (id) DO NOTHING`,
13476
13564
  [
13477
13565
  w2.id,
@@ -13501,7 +13589,8 @@ var init_store_postgres = __esm({
13501
13589
  w2.acceptedBy,
13502
13590
  w2.rejectedReason,
13503
13591
  w2.overrideBy,
13504
- w2.overrideReason
13592
+ w2.overrideReason,
13593
+ w2.lane
13505
13594
  ]
13506
13595
  );
13507
13596
  break;
@@ -13935,7 +14024,7 @@ function markNodeForRetry(state, nodeId, at) {
13935
14024
  (i) => i.kind === "comment" && i.resolvedAt === null && i.aboutNodeId === nodeId
13936
14025
  );
13937
14026
  if (hasUnresolvedComment) {
13938
- const replyWorks = state.worksOf(nodeId).filter((w2) => w2.replyToIssueId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14027
+ const replyWorks = state.replyWorksOf(nodeId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
13939
14028
  const latest = replyWorks[replyWorks.length - 1];
13940
14029
  if (latest && latest.deadAt && workState(latest, hasOutput(latest, 0)) === "dead") {
13941
14030
  state.updateWork(latest.id, { retryAt: at, status: "retry" });
@@ -14002,6 +14091,11 @@ var init_plan = __esm({
14002
14091
  if (!node || node.cancelledAt) return;
14003
14092
  killLatestWork(state, e.nodeId, ctx.at);
14004
14093
  killNodeReviews(state, e.nodeId, ctx.at);
14094
+ for (const issue2 of state.allIssues()) {
14095
+ if (issue2.resolvedAt !== null) continue;
14096
+ if (issue2.aboutNodeId !== e.nodeId && issue2.raisedByNodeId !== e.nodeId) continue;
14097
+ state.updateIssue(issue2.id, { resolvedAt: ctx.at, resolution: "wontfix" });
14098
+ }
14005
14099
  state.updateNode(e.nodeId, { cancelledAt: ctx.at, updatedAt: ctx.at });
14006
14100
  for (const edge of state.edgesFrom(e.nodeId)) {
14007
14101
  const dn = state.node(edge.toNodeId);
@@ -14236,7 +14330,7 @@ var init_workorder = __esm({
14236
14330
 
14237
14331
  // ../engine/src/handlers/work.ts
14238
14332
  function latestSuccessOf(state, nodeId) {
14239
- const successes = state.worksOf(nodeId).filter((w2) => !w2.replyToIssueId && w2.status === "success").sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14333
+ const successes = state.mainWorksOf(nodeId).filter((w2) => w2.status === "success").sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14240
14334
  return successes.length > 0 ? successes[successes.length - 1].id : null;
14241
14335
  }
14242
14336
  var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workTimeout;
@@ -14245,6 +14339,7 @@ var init_work = __esm({
14245
14339
  "use strict";
14246
14340
  init_views2();
14247
14341
  init_kill();
14342
+ init_model();
14248
14343
  workCreate = {
14249
14344
  name: "work/create",
14250
14345
  kind: "work.create",
@@ -14253,7 +14348,7 @@ var init_work = __esm({
14253
14348
  const node = state.node(e.nodeId);
14254
14349
  if (!node) return;
14255
14350
  if (e.replyToIssueId) {
14256
- if (state.worksOf(e.nodeId).some((w2) => !w2.endedAt && !w2.deadAt)) return;
14351
+ if (state.allWorksIncludingReplies(e.nodeId).some((w2) => !w2.endedAt && !w2.deadAt)) return;
14257
14352
  state.insertWork({
14258
14353
  id: e.workId,
14259
14354
  workorderId: e.workorderId,
@@ -14265,6 +14360,7 @@ var init_work = __esm({
14265
14360
  deadAt: null,
14266
14361
  cancelledAt: null,
14267
14362
  status: "running",
14363
+ lane: "reply",
14268
14364
  outcome: null,
14269
14365
  outcomeDetail: null,
14270
14366
  sessionRef: null,
@@ -14287,8 +14383,8 @@ var init_work = __esm({
14287
14383
  }
14288
14384
  supersedeLatestWork(state, e.nodeId, ctx.at);
14289
14385
  killNodeReviews(state, e.nodeId, ctx.at);
14290
- for (const w2 of state.worksOf(e.nodeId)) {
14291
- if (w2.replyToIssueId && !w2.endedAt && !w2.deadAt) killWorkRow(state, w2, ctx.at);
14386
+ for (const w2 of state.replyWorksOf(e.nodeId)) {
14387
+ if (!w2.endedAt && !w2.deadAt) killWorkRow(state, w2, ctx.at);
14292
14388
  }
14293
14389
  state.insertWork({
14294
14390
  id: e.workId,
@@ -14301,6 +14397,7 @@ var init_work = __esm({
14301
14397
  deadAt: null,
14302
14398
  cancelledAt: null,
14303
14399
  status: "running",
14400
+ lane: "main",
14304
14401
  outcome: null,
14305
14402
  outcomeDetail: null,
14306
14403
  sessionRef: null,
@@ -14386,7 +14483,7 @@ var init_work = __esm({
14386
14483
  apply(e, state, ctx) {
14387
14484
  const w2 = state.work(e.workId);
14388
14485
  if (!w2 || w2.endedAt || w2.deadAt) return;
14389
- if (w2.replyToIssueId) {
14486
+ if (isReplyWork(w2)) {
14390
14487
  const myReplies = state.allIssues().filter((i) => i.kind === "comment" && i.aboutNodeId === w2.nodeId).flatMap((i) => state.repliesOf(i.id)).filter((r) => r.authorActorId === w2.assigneeActorId && r.createdAt >= w2.createdAt).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14391
14488
  const replied = myReplies.length > 0;
14392
14489
  const ok = e.outcome === "completed" && (replied || hasOutput(w2, 0));
@@ -14771,7 +14868,7 @@ var init_review = __esm({
14771
14868
  const block = acceptBlock(state, e.workId);
14772
14869
  if (block && !(block.overridable && e.override)) return;
14773
14870
  const w2 = state.work(e.workId);
14774
- if (w2.replyToIssueId) {
14871
+ if (isReplyWork(w2)) {
14775
14872
  state.updateWork(e.workId, {
14776
14873
  acceptanceState: "accepted",
14777
14874
  acceptedAt: ctx.at,
@@ -14827,7 +14924,7 @@ var init_review = __esm({
14827
14924
  ...w2.endedAt ? {} : { endedAt: ctx.at, outcome: "completed" }
14828
14925
  });
14829
14926
  if (w2.outputVersionNo === null) {
14830
- const versions = state.worksOf(w2.nodeId).map((x2) => x2.outputVersionNo ?? 0);
14927
+ const versions = state.allWorksIncludingReplies(w2.nodeId).map((x2) => x2.outputVersionNo ?? 0);
14831
14928
  state.updateWork(e.workId, { outputVersionNo: Math.max(0, ...versions) + 1 });
14832
14929
  }
14833
14930
  for (const r of state.openReviewsOf(e.workId)) {
@@ -15665,7 +15762,8 @@ function workToRevision(w2, workInputs, artifactFiles) {
15665
15762
  resolves: void 0
15666
15763
  };
15667
15764
  revision.rejectedReason = w2.rejectedReason;
15668
- revision.replyToIssueId = w2.replyToIssueId ?? null;
15765
+ revision.lane = w2.lane;
15766
+ revision.replyIssueId = replyIssueIdOf(w2);
15669
15767
  return revision;
15670
15768
  }
15671
15769
  function issueToAnnotation(issue2, replies, recipients) {
@@ -15780,7 +15878,7 @@ function workToConcludeAttempt(w2, requirements, reviews) {
15780
15878
  gate: buildGateFromRequirements(reqs)
15781
15879
  };
15782
15880
  }
15783
- if (w2.acceptanceState === "accepted") {
15881
+ if (w2.acceptanceState === "accepted" && w2.outputVersionNo !== null) {
15784
15882
  const reqs = requirements.filter((r) => r.nodeId === w2.nodeId);
15785
15883
  return {
15786
15884
  head: w2.id,
@@ -15942,6 +16040,7 @@ function bridgeLoadReadModel(input) {
15942
16040
  const holds = /* @__PURE__ */ new Map();
15943
16041
  for (const node of allNodes) {
15944
16042
  if (!node.latestWorkId) continue;
16043
+ if (node.cancelledAt) continue;
15945
16044
  const lw = allWorks.find((w2) => w2.id === node.latestWorkId);
15946
16045
  if (!lw || !lw.deadAt) continue;
15947
16046
  if (node.latestAcceptId === node.latestWorkId) continue;
@@ -16861,7 +16960,7 @@ var init_kernel_bridge = __esm({
16861
16960
  await this.refreshTracked();
16862
16961
  const wid = this.wo(args.artifactId);
16863
16962
  let workId;
16864
- const openWorks = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId && r.state === "working" && !r.replyToIssueId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
16963
+ const openWorks = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId && r.state === "working" && r.lane !== "reply").sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
16865
16964
  workId = openWorks[0]?.id;
16866
16965
  const artifact = this.model.artifacts.get(args.artifactId);
16867
16966
  if (!workId) {
@@ -130836,7 +130935,7 @@ function buildNodeTimeline(artifactId, snap, runIdByTarget) {
130836
130935
  const nodeWorkIds = /* @__PURE__ */ new Set();
130837
130936
  for (const w2 of snap.works) {
130838
130937
  if (w2.nodeId !== artifactId) continue;
130839
- if (w2.replyToIssueId) continue;
130938
+ if (isReplyWork(w2)) continue;
130840
130939
  nodeWorkIds.add(w2.id);
130841
130940
  const status = w2.status ?? (w2.retryAt ? "retry" : w2.deadAt ? "dead" : w2.endedAt ? w2.outcome === "failed" ? "failed" : "success" : "running");
130842
130941
  items.push({
@@ -130904,6 +131003,7 @@ var TYPE_ORDER;
130904
131003
  var init_timeline = __esm({
130905
131004
  "../server/src/domains/collab/timeline.ts"() {
130906
131005
  "use strict";
131006
+ init_src3();
130907
131007
  TYPE_ORDER = { work: 0, review: 1, issue: 2 };
130908
131008
  }
130909
131009
  });
@@ -149035,6 +149135,7 @@ function stallOf(model, id, runtime, dispatcherSignals) {
149035
149135
  if (gaps.length > 0) {
149036
149136
  return { kind: "waiting_human", action: "resolve_gap", detail: `\u5361\u5728\u7F3A\u53E3\u4E0A\u5F85\u4EBA\u8865\uFF1A${gaps[0].description}` };
149037
149137
  }
149138
+ if (isDeliveredSettled(model, id)) return null;
149038
149139
  if (isHeld(model, id)) {
149039
149140
  const h = holdOf(model, id);
149040
149141
  return { kind: "held", action: "resume", detail: `\u5DF2\u6682\u505C\u6D3E\u53D1${h?.reason ? `\uFF1A${h.reason}` : ""}\u2014\u2014\u5F85\u6062\u590D`, ...h?.at ? { since: h.at } : {} };
@@ -149080,7 +149181,7 @@ function stallOf(model, id, runtime, dispatcherSignals) {
149080
149181
  if (dep) return { kind: "waiting_system", ref: dep.to, detail: `\u7B49\u4E0A\u6E38\u6536\u53E3\uFF1A${dep.to}` };
149081
149182
  return { kind: "waiting_system", detail: `\u963B\u585E\u4E2D\uFF1A${b2.reasons[0] ?? "\u5F85\u524D\u7F6E\u5C31\u7EEA"}` };
149082
149183
  }
149083
- const queueCause = lifecycleOf(model, id) === "active" ? queueBlockCauseOf(model, id) : null;
149184
+ const queueCause = queueBlockCauseOf(model, id);
149084
149185
  if (queueCause !== null) {
149085
149186
  const pending = queueOf(model, id);
149086
149187
  const since = pending[0]?.createdAt;
@@ -149907,9 +150008,10 @@ function summaryOfWorkspace(model, workspace, arts, ref2, resolveProject, dispat
149907
150008
  owner: ref2(seed.owner),
149908
150009
  participants: participantIds.map(ref2),
149909
150010
  artifactCount: arts.length,
150011
+ // 原始计数(含 brief、含已作废)——仅作旧客户端回退;界面真源是下面的 progressExcludingBrief。
149910
150012
  progress: { concluded, total: arts.length },
149911
- // ADR 0088 D1:两个派生位内联,前端不再按行拉详情。口径逐字对齐它替代的旧前端函数。
149912
- progressExcludingBrief: progressExcludingBrief(arts, members, { concluded, total: arts.length }),
150013
+ // ADR 0088 D1:派生位内联,前端不再按行拉详情。这是进度的唯一真源(排除 brief + 已作废,见函数注释)。
150014
+ progressExcludingBrief: progressExcludingBrief(arts, members),
149913
150015
  ...runtimeAlertOf(arts, members, runtimeOf) ?? {},
149914
150016
  updatedAt: lastOps[lastOps.length - 1] ?? "",
149915
150017
  projectId: resolveProject?.(workspace) ?? null,
@@ -149924,20 +150026,17 @@ function tagsOf(seed) {
149924
150026
  if (!Array.isArray(raw)) return [];
149925
150027
  return [...new Set(raw.map((v2) => typeof v2 === "string" ? v2.trim() : "").filter(Boolean))];
149926
150028
  }
149927
- function progressExcludingBrief(arts, members, progress) {
149928
- let briefCount = 0;
149929
- let briefConcluded = 0;
150029
+ function progressExcludingBrief(arts, members) {
150030
+ let total = 0;
150031
+ let concluded = 0;
149930
150032
  for (let i = 0; i < arts.length; i++) {
149931
- if (arts[i].type !== "brief") continue;
149932
- briefCount++;
149933
- const s2 = deriveNodeStage(members[i]);
149934
- if (s2 === "concluded" || s2 === "sealed") briefConcluded++;
150033
+ const info = members[i];
150034
+ if (arts[i].type === "brief") continue;
150035
+ if (info.sealed) continue;
150036
+ total++;
150037
+ if (info.concluded) concluded++;
149935
150038
  }
149936
- if (briefCount === 0) return progress;
149937
- return {
149938
- concluded: Math.max(0, progress.concluded - briefConcluded),
149939
- total: Math.max(0, progress.total - briefCount)
149940
- };
150039
+ return { concluded, total };
149941
150040
  }
149942
150041
  function runtimeAlertOf(arts, members, runtimeOf) {
149943
150042
  if (!runtimeOf) return void 0;
@@ -157272,6 +157371,8 @@ function executionEnvHash(p2) {
157272
157371
  workRoot: p2.workRoot,
157273
157372
  home: p2.homeHostDir,
157274
157373
  bin: p2.binHostPath,
157374
+ // 包根目录进哈希:它决定挂载形态(整包 vs 单文件),换了就得重建 persist 容器
157375
+ binPkg: p2.binPackageDir ?? null,
157275
157376
  extraRo: [...p2.extraRoBindDirs ?? []].sort()
157276
157377
  });
157277
157378
  return (0, import_node_crypto13.createHash)("sha256").update(material).digest("hex").slice(0, 16);
@@ -157312,9 +157413,9 @@ function commonArgs(p2, name) {
157312
157413
  // 不变量 1:workdir 根同路径挂入(rw)
157313
157414
  "-v",
157314
157415
  `${p2.workRoot}:${p2.workRoot}`,
157315
- // 不变量 4:agent CLI 只读挂入
157316
- "-v",
157317
- `${p2.binHostPath}:${binMount}:ro`,
157416
+ // 不变量 4:agent CLI 只读挂入。自包含二进制挂单文件到 /usr/local/bin/<name>;
157417
+ // 脚本型 CLI 改挂整包(同路径),执行走 binHostPath 原路径——见 WrapParams.binPackageDir。
157418
+ ...p2.binPackageDir ? ["-v", `${p2.binPackageDir}:${p2.binPackageDir}:ro`] : ["-v", `${p2.binHostPath}:${binMount}:ro`],
157318
157419
  // 不变量 5:HOME 持久
157319
157420
  "-v",
157320
157421
  `${p2.homeHostDir}:${CONTAINER_HOME}`
@@ -157338,7 +157439,7 @@ function envFlags(env) {
157338
157439
  }
157339
157440
  function wrapForContainer(p2) {
157340
157441
  const ee = p2.executionEnv;
157341
- const binInContainer = `/usr/local/bin/${p2.binName ?? path6.basename(p2.binHostPath)}`;
157442
+ const binInContainer = p2.binPackageDir ? p2.binHostPath : `/usr/local/bin/${p2.binName ?? path6.basename(p2.binHostPath)}`;
157342
157443
  const eeHash = executionEnvHash(p2);
157343
157444
  if (ee.persist && p2.job.workdirKey) {
157344
157445
  const name2 = persistContainerName(p2.job.workdirKey);
@@ -157437,6 +157538,20 @@ function resolveBinHostPath(bin) {
157437
157538
  }
157438
157539
  throw new Error(`executionEnv\uFF1A\u5728 PATH \u4E0A\u627E\u4E0D\u5230\u53EF\u6267\u884C\u7684 ${bin}\uFF0C\u65E0\u6CD5\u6302\u5165\u5BB9\u5668`);
157439
157540
  }
157541
+ function resolveBinPackageDir(binHostPath) {
157542
+ if (!/\.(js|mjs|cjs)$/i.test(binHostPath)) return null;
157543
+ let dir = path7.dirname(binHostPath);
157544
+ for (let i = 0; i < 8; i++) {
157545
+ if (fs7.existsSync(path7.join(dir, "package.json"))) {
157546
+ if (dir === "/" || dir === os3.tmpdir() || dir.split(path7.sep).length <= 2) return null;
157547
+ return dir;
157548
+ }
157549
+ const parent = path7.dirname(dir);
157550
+ if (parent === dir) break;
157551
+ dir = parent;
157552
+ }
157553
+ return null;
157554
+ }
157440
157555
  function containerHomeHostDir(workRoot) {
157441
157556
  const dir = path7.join(resolveWorkRoot(workRoot), "container-home");
157442
157557
  fs7.mkdirSync(dir, { recursive: true });
@@ -157525,10 +157640,13 @@ async function maybeContainerize(params) {
157525
157640
  return [];
157526
157641
  }
157527
157642
  }))];
157643
+ const binHostPath = resolveBinHostPath(params.bin);
157644
+ const binPackageDir = resolveBinPackageDir(binHostPath);
157528
157645
  const plan = wrapForContainer({
157529
- binHostPath: resolveBinHostPath(params.bin),
157646
+ binHostPath,
157530
157647
  binName: path7.basename(params.bin),
157531
157648
  // realpath 前的名字("claude")——argv[0] 语义要保住
157649
+ ...binPackageDir ? { binPackageDir } : {},
157532
157650
  args: params.args,
157533
157651
  cwd: params.cwd,
157534
157652
  env,
@@ -159291,6 +159409,7 @@ var init_codex = __esm({
159291
159409
  "../adapters/src/codex/index.ts"() {
159292
159410
  "use strict";
159293
159411
  import_node_child_process9 = require("node:child_process");
159412
+ init_container_runtime();
159294
159413
  import_node_crypto15 = require("node:crypto");
159295
159414
  fs10 = __toESM(require("node:fs"), 1);
159296
159415
  os5 = __toESM(require("node:os"), 1);
@@ -159336,9 +159455,6 @@ var init_codex = __esm({
159336
159455
  */
159337
159456
  capabilities = { appendInput: false };
159338
159457
  async spawn(job) {
159339
- if (job.executionEnv) {
159340
- throw new Error("executionEnv\uFF1Acodex runtime \u5C1A\u672A\u652F\u6301\u5BB9\u5668\u5316\u6267\u884C\uFF08ADR-0147\uFF09\uFF0C\u8BF7\u6539\u6D3E claude-code \u5458\u5DE5\u6216\u53BB\u6389\u8BE5\u8282\u70B9\u7684 executionEnv \u58F0\u660E");
159341
- }
159342
159458
  const slug6 = job.artifactId.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32);
159343
159459
  const id = `codex-${slug6}-${(0, import_node_crypto15.randomUUID)().slice(0, 8)}`;
159344
159460
  const startedAtMs = Date.now();
@@ -159398,7 +159514,21 @@ var init_codex = __esm({
159398
159514
  ),
159399
159515
  promptViaStdin ? "-" : prompt
159400
159516
  ];
159401
- const child = (0, import_node_child_process9.spawn)(this.opts.codexBin ?? "codex", args, {
159517
+ const containerized = await maybeContainerize({
159518
+ bin: this.opts.codexBin ?? "codex",
159519
+ args,
159520
+ cwd: dir,
159521
+ job,
159522
+ ...this.opts.workRoot !== void 0 ? { workRoot: this.opts.workRoot } : {},
159523
+ ...this.opts.env !== void 0 ? { extraEnvEarly: this.opts.env } : {}
159524
+ });
159525
+ const child = containerized ? (0, import_node_child_process9.spawn)(containerized.bin, containerized.args, {
159526
+ cwd: dir,
159527
+ env: scrubLeakyEnv(process.env),
159528
+ // stdin 的开合口径与宿主分支一致:codex 只在走 stdin 时才该收到管道(见下方长注释)。
159529
+ // 容器包装本身带 `-i`,podman 会把这条管道透传给容器内进程。
159530
+ stdio: [promptViaStdin ? "pipe" : "ignore", "pipe", "pipe"]
159531
+ }) : (0, import_node_child_process9.spawn)(this.opts.codexBin ?? "codex", args, {
159402
159532
  cwd: dir,
159403
159533
  env: {
159404
159534
  ...scrubLeakyEnv(process.env),
@@ -159513,6 +159643,7 @@ var init_codex = __esm({
159513
159643
  };
159514
159644
  const timer = setTimeout(() => {
159515
159645
  killReason ??= "timeout";
159646
+ if (containerized) killContainerBestEffort(containerized.containerName, containerized.mode);
159516
159647
  child.kill("SIGKILL");
159517
159648
  }, job.limits.wallClockMs);
159518
159649
  child.on("error", () => {
@@ -159540,13 +159671,16 @@ var init_codex = __esm({
159540
159671
  ...telemetry.usage ? { usage: telemetry.usage } : {},
159541
159672
  ...model ? { model } : {},
159542
159673
  ...runtimeSessionId ? { runtimeSessionId } : {},
159543
- ...errorMessage ? { errorMessage } : {}
159674
+ ...errorMessage ? { errorMessage } : {},
159675
+ // 跑在哪个镜像上要落账:同一题换了镜像,分数就不可比(ADR-0147 M5)
159676
+ ...containerized?.imageDigest ? { imageDigest: containerized.imageDigest } : {}
159544
159677
  });
159545
159678
  });
159546
159679
  return {
159547
159680
  id,
159548
159681
  async kill() {
159549
159682
  killReason ??= "cancelled";
159683
+ if (containerized) killContainerBestEffort(containerized.containerName, containerized.mode);
159550
159684
  child.kill("SIGKILL");
159551
159685
  },
159552
159686
  onExit(cb) {
@@ -163799,14 +163933,36 @@ async function startOasisServer(opts) {
163799
163933
  ...currentCompanyId !== void 0 ? { companyId: currentCompanyId } : {}
163800
163934
  }
163801
163935
  );
163802
- if (tokenClaims?.dispatchId && opts.onDispatchCommandCompleted) {
163936
+ if (tokenClaims?.dispatchId && opts.onDispatchCommandCompleted && !READ_ONLY_COMMANDS.has(body.command)) {
163803
163937
  try {
163938
+ const revision = body.command === "propose" || body.command === "integrate" ? result?.data : void 0;
163939
+ const deliveredArtifactId = typeof revision?.artifactId === "string" ? revision.artifactId : tokenClaims.artifactId;
163940
+ const listArg = (key) => {
163941
+ const raw = body.args?.[key];
163942
+ if (!Array.isArray(raw)) return void 0;
163943
+ const items = raw.filter((x2) => typeof x2 === "string" && x2.trim() !== "");
163944
+ return items.length > 0 ? items : void 0;
163945
+ };
163946
+ const delivered = revision && typeof revision.id === "string" && typeof revision.reason === "string" && revision.reason.trim() && deliveredArtifactId !== void 0 ? {
163947
+ summary: revision.reason,
163948
+ refs: [{
163949
+ kind: "artifact",
163950
+ id: deliveredArtifactId,
163951
+ version: revision.id,
163952
+ ...engine.kernel.model.artifacts.get(deliveredArtifactId)?.title ? { label: engine.kernel.model.artifacts.get(deliveredArtifactId).title } : {}
163953
+ }],
163954
+ ...listArg("unresolvedIssues") ? { unresolvedIssues: listArg("unresolvedIssues") } : {},
163955
+ ...listArg("risks") ? { risks: listArg("risks") } : {},
163956
+ ...listArg("nextActions") ? { nextActions: listArg("nextActions") } : {},
163957
+ ...listArg("verificationGaps") ? { verificationGaps: listArg("verificationGaps") } : {}
163958
+ } : void 0;
163804
163959
  await opts.onDispatchCommandCompleted({
163805
163960
  dispatchId: tokenClaims.dispatchId,
163806
163961
  command: body.command,
163807
163962
  ...tokenClaims.artifactId !== void 0 ? { artifactId: tokenClaims.artifactId } : {},
163808
163963
  durableRefs: tokenClaims.artifactId !== void 0 ? [{ kind: "artifact", id: tokenClaims.artifactId, label: body.command }] : [],
163809
- at: (/* @__PURE__ */ new Date()).toISOString()
163964
+ at: (/* @__PURE__ */ new Date()).toISOString(),
163965
+ ...delivered ? { delivered } : {}
163810
163966
  });
163811
163967
  } catch (error2) {
163812
163968
  console.error(`[execution-continuity] important-output checkpoint failed for ${tokenClaims.dispatchId}:`, error2);
@@ -165650,7 +165806,7 @@ async function startOasisServer(opts) {
165650
165806
  await new Promise((resolve10) => httpServer.listen(opts.port ?? 0, "0.0.0.0", resolve10));
165651
165807
  const address = httpServer.address();
165652
165808
  if (address === null || typeof address === "string") throw new Error("listen failed");
165653
- const baseUrl = `http://0.0.0.0:${address.port}`;
165809
+ const baseUrl = `http://127.0.0.1:${address.port}`;
165654
165810
  if (channelWsSupervisor) void channelWsSupervisor.start();
165655
165811
  return {
165656
165812
  baseUrl,
@@ -165662,7 +165818,7 @@ async function startOasisServer(opts) {
165662
165818
  })
165663
165819
  };
165664
165820
  }
165665
- var http, githubAppPending, defaultResolveActor, enc, CHAT_OUTPUT_LIMIT, STREAMING_PATHS, WILL_VERBS, GOVERNANCE_COMMANDS, isAgent, isHuman;
165821
+ var http, githubAppPending, defaultResolveActor, enc, CHAT_OUTPUT_LIMIT, STREAMING_PATHS, WILL_VERBS, GOVERNANCE_COMMANDS, READ_ONLY_COMMANDS, isAgent, isHuman;
165666
165822
  var init_server3 = __esm({
165667
165823
  "../server/src/server.ts"() {
165668
165824
  "use strict";
@@ -165702,6 +165858,21 @@ var init_server3 = __esm({
165702
165858
  STREAMING_PATHS = ["/api/events", "/api/chat", "/api/trajectory/events", "/api/intervention/wait"];
165703
165859
  WILL_VERBS = /* @__PURE__ */ new Set(["annotate", "reply", "resolve", "review", "merge", "escalate", "hold", "release"]);
165704
165860
  GOVERNANCE_COMMANDS = /* @__PURE__ */ new Set(["seal", "assign", "promote", "forceConclude", "unlink", "gc", "requestChange", "resolveEscalation"]);
165861
+ READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
165862
+ "ls",
165863
+ "show",
165864
+ "timeline",
165865
+ "content",
165866
+ "queue",
165867
+ "stale",
165868
+ "blocked",
165869
+ "graph",
165870
+ "log",
165871
+ "working",
165872
+ "proposed",
165873
+ "rejected",
165874
+ "superseded"
165875
+ ]);
165705
165876
  isAgent = (actor) => actor.startsWith("actor:agent:");
165706
165877
  isHuman = (actor) => actor.startsWith("actor:human:");
165707
165878
  }
@@ -166416,9 +166587,25 @@ function systemHandoffFor(run, checkpoint = null, effects = []) {
166416
166587
  createdAt: run.endedAt ?? run.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
166417
166588
  };
166418
166589
  }
166590
+ function mergeSystemHandoff(existing, reconstructed) {
166591
+ if (!existing) return reconstructed;
166592
+ const authored = (text) => text.trim() !== "" && !text.startsWith("[system_reconstructed]");
166593
+ const keepList = (current, next) => current.length > 0 ? structuredClone(current) : structuredClone(next);
166594
+ return {
166595
+ ...reconstructed,
166596
+ // outcome 描述这趟 Attempt 的命运(交稿后仍可能失败/超时)→ 以终态为准。
166597
+ summary: authored(existing.summary) ? existing.summary : reconstructed.summary,
166598
+ changedOutputs: keepList(existing.changedOutputs, reconstructed.changedOutputs),
166599
+ unresolvedIssues: keepList(existing.unresolvedIssues, reconstructed.unresolvedIssues),
166600
+ risks: keepList(existing.risks, reconstructed.risks),
166601
+ nextActions: keepList(existing.nextActions, reconstructed.nextActions),
166602
+ verificationGaps: keepList(existing.verificationGaps, reconstructed.verificationGaps),
166603
+ userHandoff: existing.userHandoff ?? reconstructed.userHandoff,
166604
+ createdAt: existing.createdAt
166605
+ };
166606
+ }
166419
166607
  function validateHandoffInput(input, status) {
166420
166608
  if (!input.summary.trim()) throw new Error("handoff summary is required");
166421
- if ([...input.summary].length > 400) throw new Error("handoff summary exceeds 400 characters");
166422
166609
  if (isTerminalStatus(status) && input.outcome !== handoffOutcomeOf(status)) {
166423
166610
  throw new Error(`handoff outcome ${input.outcome} does not match attempt status ${status}`);
166424
166611
  }
@@ -166516,7 +166703,7 @@ var init_memory_trace_store = __esm({
166516
166703
  const existingHandoff = this.handoffs.get(id);
166517
166704
  const checkpoint = (this.checkpoints.get(id) ?? []).filter((item) => item.trigger !== "attempt_terminal").at(-1) ?? null;
166518
166705
  const effects = [...this.externalEffects.values()].filter((effect) => effect.attemptId === id);
166519
- const handoff = existingHandoff?.producedBy === "agent" ? existingHandoff : systemHandoffFor(merged, checkpoint, effects);
166706
+ const handoff = existingHandoff?.producedBy === "agent" ? existingHandoff : mergeSystemHandoff(existingHandoff ?? null, systemHandoffFor(merged, checkpoint, effects));
166520
166707
  this.handoffs.set(id, handoff);
166521
166708
  merged.handoffId = handoff.handoffId;
166522
166709
  }
@@ -171891,21 +172078,38 @@ var init_sink = __esm({
171891
172078
  };
171892
172079
  }
171893
172080
  checkpointInput(st, previous, trigger, createdAt) {
172081
+ const durableRefs = structuredClone(st.checkpointState.durableRefs ?? previous?.durableRefs ?? []);
171894
172082
  return {
171895
172083
  attemptId: st.runId,
171896
172084
  pendingSteps: [...st.checkpointState.pendingSteps ?? previous?.pendingSteps ?? []],
171897
- durableRefs: structuredClone(st.checkpointState.durableRefs ?? previous?.durableRefs ?? []),
172085
+ durableRefs,
171898
172086
  completedStepIds: [...st.checkpointState.completedStepIds ?? previous?.completedStepIds ?? []],
171899
- recoverability: st.checkpointState.recoverability ?? previous?.recoverability ?? "none",
172087
+ // recoverability 只有 Agent 自己说得准,但「一条持久化产出都没有」与「已经落了东西」这两种局面
172088
+ // 系统分得清:有 durableRefs 就至少是 partial(下一趟能接着用这些产出),别一律记成 none——
172089
+ // 那会让恢复包看起来「什么都没留下」,与账本里的 refs 自相矛盾。
172090
+ recoverability: st.checkpointState.recoverability ?? previous?.recoverability ?? (durableRefs.length > 0 ? "partial" : "none"),
171900
172091
  trigger,
171901
172092
  createdAt
171902
172093
  };
171903
172094
  }
172095
+ /**
172096
+ * 两条 checkpoint 的内容是否一字不差(trigger/时间不算内容)——用于跳过无意义的重复写。
172097
+ *
172098
+ * refs 必须逐字段比,**不能 JSON.stringify 整个对象**:写进 PG 的 jsonb 读回来键序是重排过的
172099
+ * (`{kind,id,version,label}` 回来是 `{id,kind,label,version}`),按字符串比会把「完全一样」判成
172100
+ * 「变了」,去重直接失效——线上的表现就是每个工具结果照样灌一行。
172101
+ */
172102
+ sameCheckpointContent(input, previous) {
172103
+ if (!previous) return false;
172104
+ const refKeys = (refs) => refs.map((ref2) => `${ref2.kind}\0${ref2.id}\0${ref2.version ?? ""}\0${ref2.label ?? ""}`).join("");
172105
+ return JSON.stringify(input.pendingSteps) === JSON.stringify(previous.pendingSteps) && refKeys(input.durableRefs) === refKeys(previous.durableRefs) && JSON.stringify(input.completedStepIds) === JSON.stringify(previous.completedStepIds) && input.recoverability === previous.recoverability;
172106
+ }
171904
172107
  clearObservedCheckpointState(st) {
171905
172108
  st.checkpointState = {};
171906
172109
  }
171907
172110
  async appendSystemCheckpoint(st, trigger, createdAt, appendDurableRefs = []) {
171908
172111
  if (!this.canCheckpoint()) return;
172112
+ if (trigger === "tool_completed" && Object.keys(st.checkpointState).length === 0) return;
171909
172113
  const previous = await this.latestCheckpoint(st);
171910
172114
  const input = this.checkpointInput(st, previous, trigger, createdAt);
171911
172115
  if (appendDurableRefs.length > 0) {
@@ -171915,6 +172119,14 @@ var init_sink = __esm({
171915
172119
  }
171916
172120
  input.durableRefs = [...refs.values()];
171917
172121
  }
172122
+ if (input.durableRefs.length > 0 && st.checkpointState.recoverability === void 0 && previous?.recoverability === void 0) {
172123
+ input.recoverability = "partial";
172124
+ }
172125
+ const observedTrigger = trigger === "tool_completed" || trigger === "important_output";
172126
+ if (observedTrigger && this.sameCheckpointContent(input, previous)) {
172127
+ this.clearObservedCheckpointState(st);
172128
+ return;
172129
+ }
171918
172130
  await this.store.appendCheckpoint(input);
171919
172131
  this.clearObservedCheckpointState(st);
171920
172132
  this.scheduleCheckpointTimer(st);
@@ -171951,8 +172163,16 @@ var init_sink = __esm({
171951
172163
  if (!st || st.ending || !this.canCheckpoint()) return;
171952
172164
  this.enqueue(sessionId, async (current) => {
171953
172165
  if (signal.trigger === "important_output") {
171954
- const { durableRefs = [], ...state } = signal;
172166
+ const { durableRefs = [], completedStepIds = [], ...state } = signal;
171955
172167
  this.mergeCheckpointState(current, state);
172168
+ if (completedStepIds.length > 0) {
172169
+ const previous = await this.latestCheckpoint(current);
172170
+ const merged = /* @__PURE__ */ new Set([
172171
+ ...current.checkpointState.completedStepIds ?? previous?.completedStepIds ?? [],
172172
+ ...completedStepIds
172173
+ ]);
172174
+ current.checkpointState = { ...current.checkpointState, completedStepIds: [...merged] };
172175
+ }
171956
172176
  await this.appendSystemCheckpoint(
171957
172177
  current,
171958
172178
  signal.trigger,
@@ -180335,7 +180555,7 @@ function collectWaitingRows(snapshots2, me) {
180335
180555
  const latestReply = /* @__PURE__ */ new Map();
180336
180556
  for (const w2 of snap.works) {
180337
180557
  if (w2.assigneeActorId !== me || cancelled.has(w2.nodeId)) continue;
180338
- latestBy(w2.replyToIssueId ? latestReply : latestMain, w2);
180558
+ latestBy(isReplyWork(w2) ? latestReply : latestMain, w2);
180339
180559
  }
180340
180560
  for (const w2 of [...latestMain.values(), ...latestReply.values()]) {
180341
180561
  const st = workState(w2, hasOutput(w2, outputCountOf(w2.id)));
@@ -182982,7 +183202,9 @@ function renderCoordinatorView(view) {
182982
183202
  if (node.handoffProducer === "agent") {
182983
183203
  lines.push(`- Agent handoff: ${node.summary || "No summary"}`);
182984
183204
  } else if (node.handoffProducer === "system_reconstructed") {
182985
- lines.push(`- System reconstruction (not an Agent conclusion): ${node.summary || "No summary"}`);
183205
+ lines.push(
183206
+ node.summary.startsWith("[system_reconstructed]") ? `- System reconstruction (not an Agent conclusion): ${node.summary || "No summary"}` : `- Delivery note recorded at submit time (the executor's own wording, captured by the system): ${node.summary}`
183207
+ );
182986
183208
  }
182987
183209
  for (const output of node.changedOutputs) {
182988
183210
  lines.push(` - Durable output: ${output.kind}:${output.id}${output.version ? `@${output.version}` : ""}`);
@@ -183005,6 +183227,11 @@ function renderCoordinatorView(view) {
183005
183227
  return `${lines.join("\n")}
183006
183228
  `;
183007
183229
  }
183230
+ function shouldInjectResumePack(pack) {
183231
+ const status = pack?.previousAttempt?.status;
183232
+ if (!status) return false;
183233
+ return status !== "succeeded" && status !== "no-output";
183234
+ }
183008
183235
  function renderContinuityProtocol() {
183009
183236
  return `# Execution continuity protocol
183010
183237
 
@@ -183016,8 +183243,13 @@ authoritative systems. Record only durable facts and stable references here.
183016
183243
  Run \`oasis continuity-checkpoint --trigger plan_completed\` after planning and
183017
183244
  \`oasis continuity-checkpoint --trigger stage_completed\` after a key stage so the system knows
183018
183245
  what remains, stable completed-step ids, recoverability, and only references that another agent can open.
183019
- The system silently adds checkpoints after successful tool results, durable Oasis command writes,
183020
- changed long-task state, and immediately before terminal status; do not emit timer-only checkpoints.
183246
+ Your explicit checkpoints are the only source of pendingSteps, completedStepIds and recoverability.
183247
+ The system appends checkpoints on its own after each successful tool result, after a durable Oasis
183248
+ command succeeds (carrying a coarse reference to the artifact you wrote), after a captured connector
183249
+ external effect settles, and once immediately before terminal status \u2014 but every one of those merely
183250
+ re-persists what you already declared plus references it can see; none of them can infer what remains
183251
+ to be done. If you never checkpoint, a later attempt on this node has nothing to resume from. Do not
183252
+ emit timer-only checkpoints.
183021
183253
 
183022
183254
  ## External effects
183023
183255
 
@@ -183028,30 +183260,35 @@ succeeds, use \`oasis continuity-effect\` with a stable key that the same node w
183028
183260
 
183029
183261
  ## Handoff
183030
183262
 
183031
- Before terminal exit, run \`oasis continuity-handoff\` with a concise technical summary (maximum 400
183032
- characters), changed durable outputs, unresolved issues, risks, next actions, and verification gaps.
183033
- You must also provide \`--user-summary\` as short business-facing language for the people receiving
183034
- the work. Say what you completed, what result you formed, and what downstream work can use; if not
183035
- delivered, say only why and what must happen next. Use \`--user-next-step\` when downstream needs an
183036
- instruction. List only openable business results in \`--user-deliverables\`; keep technical refs in
183037
- \`--outputs\`, and never list conclude, gap, checkpoint, or op as a user deliverable. Do not use a
183038
- generic success/output-count template or mention attempts, checkpoints, runtimes, recovery,
183039
- infrastructure paths, or internal diagnostics there.
183040
- The system reconstructs an internal handoff if the process dies first, but reconstructed content is
183041
- never shown as a business handoff.
183263
+ Before terminal exit, run \`oasis continuity-handoff\` with a technical summary, changed durable
183264
+ outputs, unresolved issues, risks, next actions, and verification gaps. Submitting work already
183265
+ records the delivered revision and your \`--reason\` as this node's handoff summary, so the value you
183266
+ add here is what the system cannot observe: what is still unresolved, what is risky, what the next
183267
+ agent should do, and what you could not verify. You must also provide \`--user-summary\` as short
183268
+ business-facing language for the people receiving the work. Say what you completed, what result you
183269
+ formed, and what downstream work can use; if not delivered, say only why and what must happen next.
183270
+ Use \`--user-next-step\` when downstream needs an instruction. List only openable business results in
183271
+ \`--user-deliverables\`; keep technical refs in \`--outputs\`, and never list conclude, gap,
183272
+ checkpoint, or op as a user deliverable. Do not use a generic success/output-count template or
183273
+ mention attempts, checkpoints, runtimes, recovery, infrastructure paths, or internal diagnostics
183274
+ there. Only an explicit non-success outcome holds downstream work back; a missing handoff does not,
183275
+ so never claim non-delivery merely because you ran out of time to write one.
183042
183276
  `;
183043
183277
  }
183044
183278
  function renderBusinessHandoffProtocol() {
183045
183279
  return `# Business handoff protocol
183046
183280
 
183047
- Before terminal exit, run \`oasis continuity-handoff\`. The NodeHandoff and its userHandoff are a
183048
- required completion barrier for business downstream work. A successful handoff must use outcome
183049
- \`succeeded\`, include a non-empty \`--user-summary\` saying what was done, what result was formed,
183050
- and what downstream can use, and may have zero files. If the work cannot be delivered, submit a
183051
- non-success outcome with a user summary explaining why delivery failed and \`--user-next-step\`;
183052
- that records the handoff but does not release downstream work. List only openable business results
183053
- in \`--user-deliverables\`. Never use a generic success/output-count template or expose attempts,
183054
- checkpoints, runtimes, recovery, infrastructure paths, or internal diagnostics in userHandoff.
183281
+ Before terminal exit, run \`oasis continuity-handoff\`. Submitting work already records the delivered
183282
+ revision and your \`--reason\` as this node's handoff summary; this command adds what the system
183283
+ cannot observe \u2014 unresolved issues, risks, next actions, verification gaps \u2014 plus a business-facing
183284
+ \`--user-summary\`. A successful handoff must use outcome \`succeeded\`, include a non-empty
183285
+ \`--user-summary\` saying what was done, what result was formed, and what downstream can use, and may
183286
+ have zero files. If the work genuinely cannot be delivered, submit a non-success outcome with a user
183287
+ summary explaining why and \`--user-next-step\`: that is the one signal which holds downstream work
183288
+ back, so never use it just because you ran out of time to write a handoff. Omitting this command does
183289
+ not block downstream. List only openable business results in \`--user-deliverables\`. Never use a
183290
+ generic success/output-count template or expose attempts, checkpoints, runtimes, recovery,
183291
+ infrastructure paths, or internal diagnostics in userHandoff.
183055
183292
  `;
183056
183293
  }
183057
183294
  function emptyExternalEffectProjection() {
@@ -183225,7 +183462,6 @@ var init_service7 = __esm({
183225
183462
  async handoff(input) {
183226
183463
  if (!HANDOFF_OUTCOMES2.has(input.outcome)) throw new Error(`invalid handoff outcome: ${input.outcome}`);
183227
183464
  if (!input.summary.trim()) throw new Error("handoff summary is required");
183228
- if ([...input.summary].length > 400) throw new Error("handoff summary exceeds 400 characters");
183229
183465
  if (input.producedBy === "agent" && !input.userHandoff) {
183230
183466
  throw new Error("agent-authored handoff requires userHandoff");
183231
183467
  }
@@ -183288,7 +183524,7 @@ var init_service7 = __esm({
183288
183524
  }
183289
183525
  async recoveryView(attemptId, companyId) {
183290
183526
  const attempt = await this.store.getAttempt(attemptId);
183291
- if (!attempt?.workOrderId || !attempt.nodeId || attempt.continuityMode !== "resume") return null;
183527
+ if (!attempt?.workOrderId || !attempt.nodeId || !attempt.continuityMode || attempt.continuityMode === "off") return null;
183292
183528
  const recoverySnapshot = this.readRecoverySnapshot(attempt);
183293
183529
  if (!recoverySnapshot) return null;
183294
183530
  const graph = await this.resolveGraph(attempt.workOrderId, companyId);
@@ -183322,7 +183558,7 @@ var init_service7 = __esm({
183322
183558
  return candidate ? { workOrderId: candidate.workOrderId, nodeId: candidate.nodeId, attemptId: candidate.attemptId } : null;
183323
183559
  }
183324
183560
  readRecoverySnapshot(attempt) {
183325
- if (!attempt.nodeId || attempt.continuityMode !== "resume") return null;
183561
+ if (!attempt.nodeId || attempt.continuityMode === "off" || attempt.continuityMode === null) return null;
183326
183562
  const metadata = attempt.metadata;
183327
183563
  const manifest = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata["bundleManifest"] : null;
183328
183564
  const expectedHash = manifest && typeof manifest === "object" && !Array.isArray(manifest) ? manifest["execution/RESUME.md"] : null;
@@ -183475,11 +183711,20 @@ var init_handoff_work_bridge = __esm({
183475
183711
 
183476
183712
  // ../server/src/domains/execution-continuity/upstream-handoff-input.ts
183477
183713
  function renderRefs(refs) {
183478
- return refs.map(
183479
- (ref2) => `- ${ref2.kind}:${ref2.id}${ref2.version ? `@${ref2.version}` : ""}${ref2.label ? ` \u2014 ${ref2.label}` : ""}`
183480
- );
183714
+ return refs.map((ref2) => {
183715
+ const label = ref2.id.startsWith(`${ref2.kind}:`) ? ref2.id : `${ref2.kind}:${ref2.id}`;
183716
+ return `- ${label}${ref2.version ? `@${ref2.version}` : ""}${ref2.label ? ` \u2014 ${ref2.label}` : ""}`;
183717
+ });
183718
+ }
183719
+ function bounded(text) {
183720
+ const t = text.trim();
183721
+ return [...t].length <= INJECTED_TEXT_LIMIT ? t : `${[...t].slice(0, INJECTED_TEXT_LIMIT).join("")}\u2026\uFF08\u539F\u6587\u8FC7\u957F\uFF0C\u6B64\u5904\u622A\u65AD\uFF1B\u5B8C\u6574\u5185\u5BB9\u89C1\u8BE5\u4E0A\u6E38\u8282\u70B9\u7684\u4EA4\u4ED8\u8BF4\u660E\uFF09`;
183722
+ }
183723
+ function isExplicitNone(values) {
183724
+ return values.length === 1 && NONE_TOKENS.has((values[0] ?? "").trim().toLowerCase());
183481
183725
  }
183482
183726
  function renderList(title, values) {
183727
+ if (isExplicitNone(values)) return [`## ${title}`, "- \u6267\u884C\u4EBA\u786E\u8BA4\uFF1A\u65E0", ""];
183483
183728
  return [
183484
183729
  `## ${title}`,
183485
183730
  ...values.length > 0 ? values.map((value) => `- ${value}`) : ["- None"],
@@ -183504,8 +183749,9 @@ function renderUpstreamNodeHandoff(handoff, source) {
183504
183749
  `Created at: ${handoff.createdAt}`,
183505
183750
  "",
183506
183751
  "## Summary",
183507
- handoff.summary,
183752
+ bounded(handoff.summary),
183508
183753
  "",
183754
+ ...source.conclusion ? ["## Delivery note (upstream conclusion)", bounded(source.conclusion), ""] : [],
183509
183755
  "## Changed durable outputs",
183510
183756
  ...handoff.changedOutputs.length > 0 ? renderRefs(handoff.changedOutputs) : ["- None"],
183511
183757
  "",
@@ -183517,49 +183763,117 @@ function renderUpstreamNodeHandoff(handoff, source) {
183517
183763
  ""
183518
183764
  ].join("\n");
183519
183765
  }
183766
+ function renderDeliveredWorkHandoff(source) {
183767
+ const authored = [
183768
+ ...source.unresolvedIssues ?? [],
183769
+ ...source.risks ?? [],
183770
+ ...source.nextActions ?? [],
183771
+ ...source.verificationGaps ?? []
183772
+ ];
183773
+ return [
183774
+ "# Upstream NodeHandoff",
183775
+ "",
183776
+ `Upstream node: ${source.upstreamNodeId}${source.upstreamTitle ? `\uFF08${source.upstreamTitle}\uFF09` : ""}`,
183777
+ `Pinned work: ${source.pinnedWorkId}`,
183778
+ "Produced by: system\uFF08\u636E\u4E0A\u6E38\u4EA4\u4F5C\u4E1A\u4E8B\u5B9E\u751F\u6210\uFF0C\u975E Agent \u624B\u5199\u7684\u6280\u672F\u4EA4\u63A5\uFF09",
183779
+ "",
183780
+ "## Summary",
183781
+ source.conclusion && source.conclusion.trim() ? bounded(source.conclusion) : "\u4E0A\u6E38\u672A\u7559\u6536\u5C3E\u7ED3\u8BBA\uFF1B\u4EE5\u672C\u76EE\u5F55\u4E2D\u9489\u4F4F\u7684\u4EA4\u4ED8\u7269\u5185\u5BB9\u4E3A\u51C6\u3002",
183782
+ "",
183783
+ "## Changed durable outputs",
183784
+ ...source.changedOutputs.length > 0 ? renderRefs(source.changedOutputs) : ["- None"],
183785
+ "",
183786
+ ...renderList("Unresolved issues", [...source.unresolvedIssues ?? []]),
183787
+ ...renderList("Risks", [...source.risks ?? []]),
183788
+ ...renderList("Next actions", [...source.nextActions ?? []]),
183789
+ ...renderList("Verification gaps", [...source.verificationGaps ?? []]),
183790
+ authored.length > 0 ? "\u4EE5\u4E0A\u56DB\u8282\u662F\u4E0A\u6E38\u6267\u884C\u4EBA\u4EA4\u4F5C\u4E1A\u65F6\u81EA\u5DF1\u5199\u7684\u3002" : "\u4E0A\u6E38\u8FD9\u4E00\u8F6E\u6CA1\u5199\u672A\u51B3 / \u98CE\u9669 / \u4E0B\u4E00\u6B65 / \u6838\u9A8C\u7F3A\u53E3\u2014\u2014**\u7A7A\u4E0D\u7B49\u4E8E\u300C\u4E0A\u6E38\u786E\u8BA4\u65E0\u98CE\u9669\u300D**\uFF0C\u53EA\u7B49\u4E8E\u6CA1\u4EBA\u5199\u3002",
183791
+ "\u4EE5\u672C\u76EE\u5F55\u4E2D\u9489\u4F4F\u7684\u90A3\u4E00\u7248\u4EA4\u4ED8\u7269\u4E3A\u51C6\uFF1B\u6709\u7591\u95EE\u56DE\u4E0A\u6E38\u8282\u70B9\u63D0\u56DE\u538B\uFF0C\u4E0D\u8981\u51ED\u672C\u6587\u4EF6\u63A8\u65AD\u672A\u5199\u660E\u7684\u4E8B\u3002",
183792
+ ""
183793
+ ].join("\n");
183794
+ }
183520
183795
  function createUpstreamHandoffInputResolver(opts) {
183796
+ const warn = opts.warn ?? ((message) => console.warn(`[upstream-handoff] ${message}`));
183521
183797
  return async (input) => {
183522
- const snapshot = await opts.loadWorkorder(input.workOrderId);
183523
- if (!snapshot) throw new Error(`workorder not found while assembling upstream handoff: ${input.workOrderId}`);
183524
- const edge = snapshot.edges.find(
183525
- (candidate) => candidate.fromNodeId === input.upstreamNodeId && candidate.toNodeId === input.downstreamNodeId
183526
- );
183527
- const downstream = snapshot.nodes.find((node) => node.id === input.downstreamNodeId);
183528
- const businessEdge = edge?.kind === "data" && edge.required && downstream?.fields?.[BUSINESS_HANDOFF_POLICY_FIELD] !== BUSINESS_HANDOFF_POLICY_EXEMPT;
183529
- if (!businessEdge) return null;
183530
- if (edge.pinnedWorkId !== input.pinnedWorkId) {
183531
- throw new Error(
183532
- `upstream handoff pin drift for ${input.upstreamNodeId}\u2192${input.downstreamNodeId}: bundle=${input.pinnedWorkId}, edge=${edge.pinnedWorkId ?? "none"}`
183798
+ try {
183799
+ const snapshot = await opts.loadWorkorder(input.workOrderId);
183800
+ if (!snapshot) {
183801
+ warn(`\u5DE5\u5355\u5FEB\u7167\u8BFB\u4E0D\u5230\uFF0C\u8DF3\u8FC7\u4E0A\u6E38\u4EA4\u63A5\u6CE8\u5165\uFF1A${input.workOrderId}`);
183802
+ return null;
183803
+ }
183804
+ const edge = snapshot.edges.find(
183805
+ (candidate) => candidate.fromNodeId === input.upstreamNodeId && candidate.toNodeId === input.downstreamNodeId
183533
183806
  );
183534
- }
183535
- const work = snapshot.works.find(
183536
- (candidate) => candidate.id === input.pinnedWorkId && candidate.nodeId === input.upstreamNodeId
183537
- );
183538
- if (!work) throw new Error(`pinned upstream Work not found: ${input.pinnedWorkId}`);
183539
- if (typeof work.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) {
183540
- return null;
183541
- }
183542
- if (work.status === "success" && work.agentHandoffAttemptId === null && work.businessHandoffStatus === null) {
183807
+ const downstream = snapshot.nodes.find((node) => node.id === input.downstreamNodeId);
183808
+ const businessEdge = edge?.kind === "data" && edge.required && downstream?.fields?.[BUSINESS_HANDOFF_POLICY_FIELD] !== BUSINESS_HANDOFF_POLICY_EXEMPT;
183809
+ if (!businessEdge) return null;
183810
+ if (edge.pinnedWorkId !== input.pinnedWorkId) {
183811
+ warn(
183812
+ `pin \u6F02\u79FB\uFF0C\u8DF3\u8FC7\u6CE8\u5165 ${input.upstreamNodeId}\u2192${input.downstreamNodeId}\uFF1Abundle=${input.pinnedWorkId}\u3001edge=${edge.pinnedWorkId ?? "none"}`
183813
+ );
183814
+ return null;
183815
+ }
183816
+ const work = snapshot.works.find(
183817
+ (candidate) => candidate.id === input.pinnedWorkId && candidate.nodeId === input.upstreamNodeId
183818
+ );
183819
+ if (!work) {
183820
+ warn(`\u9489\u4F4F\u7684\u4E0A\u6E38 Work \u4E0D\u5728\u5FEB\u7167\u91CC\uFF0C\u8DF3\u8FC7\u6CE8\u5165\uFF1A${input.pinnedWorkId}`);
183821
+ return null;
183822
+ }
183823
+ if (typeof work.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) {
183824
+ return null;
183825
+ }
183826
+ const upstreamNode = snapshot.nodes.find((node) => node.id === input.upstreamNodeId);
183827
+ const conclusion = typeof work.conclusion === "string" ? work.conclusion : null;
183828
+ const changedOutputs = [{
183829
+ kind: "artifact",
183830
+ id: input.upstreamNodeId,
183831
+ version: input.pinnedWorkId,
183832
+ ...upstreamNode?.title ? { label: upstreamNode.title } : {}
183833
+ }];
183834
+ const deliveredRow = typeof work.sessionRef === "string" && work.sessionRef ? await opts.getHandoff(work.sessionRef).catch(() => null) : null;
183835
+ const systemHandoff = () => renderDeliveredWorkHandoff({
183836
+ upstreamNodeId: input.upstreamNodeId,
183837
+ upstreamTitle: upstreamNode?.title,
183838
+ pinnedWorkId: input.pinnedWorkId,
183839
+ conclusion,
183840
+ changedOutputs: (deliveredRow?.changedOutputs.length ?? 0) > 0 ? deliveredRow.changedOutputs : changedOutputs,
183841
+ ...deliveredRow?.unresolvedIssues.length ? { unresolvedIssues: deliveredRow.unresolvedIssues } : {},
183842
+ ...deliveredRow?.risks.length ? { risks: deliveredRow.risks } : {},
183843
+ ...deliveredRow?.nextActions.length ? { nextActions: deliveredRow.nextActions } : {},
183844
+ ...deliveredRow?.verificationGaps.length ? { verificationGaps: deliveredRow.verificationGaps } : {}
183845
+ });
183846
+ if (!work.agentHandoffAttemptId || work.businessHandoffStatus !== "delivered") {
183847
+ return systemHandoff();
183848
+ }
183849
+ const handoff = await opts.getHandoff(work.agentHandoffAttemptId).catch((error2) => {
183850
+ warn(`\u8BFB Agent NodeHandoff \u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u7CFB\u7EDF\u751F\u6210\u7248\uFF1A${error2 instanceof Error ? error2.message : String(error2)}`);
183851
+ return null;
183852
+ });
183853
+ if (!handoff || handoff.attemptId !== work.agentHandoffAttemptId || handoff.producedBy !== "agent" || handoff.outcome !== "succeeded" || handoff.userHandoff?.status !== "delivered") {
183854
+ warn(
183855
+ `Agent NodeHandoff \u4E0E\u6295\u5F71\u5BF9\u4E0D\u4E0A\uFF08work=${work.id}\u3001attempt=${work.agentHandoffAttemptId}\uFF09\uFF0C\u964D\u7EA7\u4E3A\u7CFB\u7EDF\u751F\u6210\u7248`
183856
+ );
183857
+ return systemHandoff();
183858
+ }
183859
+ return renderUpstreamNodeHandoff(handoff, {
183860
+ upstreamNodeId: input.upstreamNodeId,
183861
+ pinnedWorkId: input.pinnedWorkId,
183862
+ conclusion
183863
+ });
183864
+ } catch (error2) {
183865
+ warn(`\u4E0A\u6E38\u4EA4\u63A5\u89E3\u6790\u5F02\u5E38\uFF0C\u672C\u8F6E\u4E0D\u6CE8\u5165\uFF1A${error2 instanceof Error ? error2.message : String(error2)}`);
183543
183866
  return null;
183544
183867
  }
183545
- if (!work.agentHandoffAttemptId || work.businessHandoffStatus !== "delivered") {
183546
- throw new Error(`pinned upstream Work ${work.id} has no delivered Agent handoff projection`);
183547
- }
183548
- const handoff = await opts.getHandoff(work.agentHandoffAttemptId);
183549
- if (!handoff) throw new Error(`Agent NodeHandoff not found for Attempt ${work.agentHandoffAttemptId}`);
183550
- if (handoff.attemptId !== work.agentHandoffAttemptId || handoff.producedBy !== "agent" || handoff.outcome !== "succeeded" || handoff.userHandoff?.status !== "delivered") {
183551
- throw new Error(`invalid Agent NodeHandoff for pinned Work ${work.id}`);
183552
- }
183553
- return renderUpstreamNodeHandoff(handoff, {
183554
- upstreamNodeId: input.upstreamNodeId,
183555
- pinnedWorkId: input.pinnedWorkId
183556
- });
183557
183868
  };
183558
183869
  }
183870
+ var INJECTED_TEXT_LIMIT, NONE_TOKENS;
183559
183871
  var init_upstream_handoff_input = __esm({
183560
183872
  "../server/src/domains/execution-continuity/upstream-handoff-input.ts"() {
183561
183873
  "use strict";
183562
183874
  init_src3();
183875
+ INJECTED_TEXT_LIMIT = 4e3;
183876
+ NONE_TOKENS = /* @__PURE__ */ new Set(["none", "n/a", "na", "nil", "\u65E0", "\u6CA1\u6709", "\u6682\u65E0"]);
183563
183877
  }
183564
183878
  });
183565
183879
 
@@ -183580,6 +183894,51 @@ var init_node_conclusions = __esm({
183580
183894
  }
183581
183895
  });
183582
183896
 
183897
+ // ../server/src/domains/execution-continuity/delivered-handoff.ts
183898
+ function mergeRefs(primary, extra) {
183899
+ const out = [...primary];
183900
+ for (const ref2 of extra) {
183901
+ if (!out.some((item) => item.kind === ref2.kind && item.id === ref2.id && item.version === ref2.version)) {
183902
+ out.push(ref2);
183903
+ }
183904
+ }
183905
+ return out;
183906
+ }
183907
+ async function recordDeliveredOutputHandoff(deps, facts) {
183908
+ const warn = deps.warn ?? ((message) => console.warn(`[execution-continuity] ${message}`));
183909
+ if (!facts.summary.trim() || facts.refs.length === 0) return null;
183910
+ try {
183911
+ if (deps.getAttempt) {
183912
+ const attempt = await deps.getAttempt(facts.attemptId).catch(() => null);
183913
+ if (!attempt?.nodeId) return null;
183914
+ }
183915
+ const existing = await deps.getHandoff(facts.attemptId).catch(() => null);
183916
+ if (existing?.producedBy === "agent") return existing;
183917
+ return await deps.handoff({
183918
+ attemptId: facts.attemptId,
183919
+ producedBy: "system_reconstructed",
183920
+ outcome: "succeeded",
183921
+ summary: facts.summary,
183922
+ changedOutputs: mergeRefs(facts.refs, existing?.changedOutputs ?? []),
183923
+ // 本次带来的优先;没带就把已有的机器派生内容原样带回——本次只补「交了什么」,
183924
+ // 不该顺手抹掉终态触发器算出来的风险项。
183925
+ unresolvedIssues: [...facts.unresolvedIssues ?? existing?.unresolvedIssues ?? []],
183926
+ risks: [...facts.risks ?? existing?.risks ?? []],
183927
+ nextActions: [...facts.nextActions ?? existing?.nextActions ?? []],
183928
+ verificationGaps: [...facts.verificationGaps ?? existing?.verificationGaps ?? []],
183929
+ ...facts.at ? { createdAt: facts.at } : {}
183930
+ });
183931
+ } catch (error2) {
183932
+ warn(`\u4EA4\u4F5C\u4E1A\u4EA4\u63A5\u843D\u5E93\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u6B21\u4EA4\u7A3F\uFF09${facts.attemptId}: ${error2 instanceof Error ? error2.message : String(error2)}`);
183933
+ return null;
183934
+ }
183935
+ }
183936
+ var init_delivered_handoff = __esm({
183937
+ "../server/src/domains/execution-continuity/delivered-handoff.ts"() {
183938
+ "use strict";
183939
+ }
183940
+ });
183941
+
183583
183942
  // ../server/src/domains/execution-continuity/index.ts
183584
183943
  function createExecutionContinuityDomain(opts) {
183585
183944
  const service = new ExecutionContinuityService(opts);
@@ -183595,6 +183954,7 @@ var init_execution_continuity2 = __esm({
183595
183954
  init_handoff_work_bridge();
183596
183955
  init_upstream_handoff_input();
183597
183956
  init_node_conclusions();
183957
+ init_delivered_handoff();
183598
183958
  }
183599
183959
  });
183600
183960
 
@@ -187121,13 +187481,24 @@ ${ctx.nodeFault}
187121
187481
  }
187122
187482
  }
187123
187483
  const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto38.randomUUID)();
187484
+ const taskWithContext = coordinatorContext ? [
187485
+ task,
187486
+ ``,
187487
+ `## \u5168\u5C40\u6267\u884C\u72B6\u6001\uFF08\u5148\u8BFB \`execution/COORDINATOR_VIEW.md\`\uFF09`,
187488
+ `\u90A3\u4EFD\u6587\u4EF6\u662F\u672C\u5DE5\u5355**\u6240\u6709\u8282\u70B9**\u7684\u4E00\u6B21\u6027\u5FEB\u7167\uFF1A\u5404\u8282\u70B9\u72B6\u6001\u3001\u5361\u5728\u8C01\u8EAB\u4E0A\u3001\u5DF2\u4EA4\u4ED8\u7684\u8282\u70B9\u4EA4\u4E86\u4EC0\u4E48`,
187489
+ `\uFF08Delivery note = \u6267\u884C\u4EBA\u4EA4\u4F5C\u4E1A\u65F6\u5199\u4E0B\u7684\u539F\u8BDD\uFF09\u3001\u52A8\u4E86\u54EA\u4E9B\u4EA7\u7269\u4E0E\u7248\u672C\u3001\u4EE5\u53CA\u672A\u51B3\u9879 / \u98CE\u9669 / \u6838\u9A8C\u7F3A\u53E3\u3002`,
187490
+ `\u5224\u300C\u771F\u6B7B\u9501\u8FD8\u662F\u5065\u5EB7\u7B49\u5F85\u300D\u4E4B\u524D\u5148\u770B\u5B83\u2014\u2014\u53EA\u770B\u672C\u8282\u70B9\u90BB\u57DF\u5BB9\u6613\u628A\u300C\u4E0A\u6E38\u6B63\u5E38\u5728\u4EA7\u300D\u8BEF\u5224\u6210\u5361\u4F4F\u3002`,
187491
+ `\u6CE8\u610F\u533A\u5206\u4E24\u79CD\u6458\u8981\uFF1A**Delivery note** \u662F\u6267\u884C\u4EBA\u81EA\u5DF1\u7684\u8BDD\uFF0C\u53EF\u4FE1\uFF1B**System reconstruction** \u662F\u8FDB\u7A0B`,
187492
+ `\u7EC8\u6001\u62FC\u51FA\u6765\u7684\u673A\u5668\u4E32\uFF0C\u53EA\u8BF4\u660E\u4F1A\u8BDD\u600E\u4E48\u7ED3\u675F\u7684\uFF0C\u4E0D\u4EE3\u8868\u4E1A\u52A1\u7ED3\u8BBA\u3002`,
187493
+ ``
187494
+ ].join("\n") : task;
187124
187495
  const job = {
187125
187496
  actor: actorId,
187126
187497
  actorToken: token,
187127
187498
  artifactId,
187128
187499
  bundle: {
187129
187500
  files: {
187130
- "TASK.md": task,
187501
+ "TASK.md": taskWithContext,
187131
187502
  ...coordinatorContext ? { "execution/COORDINATOR_VIEW.md": coordinatorContext } : {}
187132
187503
  }
187133
187504
  },
@@ -190284,7 +190655,7 @@ var init_postgres_trace = __esm({
190284
190655
  part text,
190285
190656
  produced_by text NOT NULL CHECK (produced_by IN ('agent','system_reconstructed')),
190286
190657
  outcome text NOT NULL CHECK (outcome IN ('succeeded','no-output','failed','cancelled','timeout','orphaned')),
190287
- summary text NOT NULL CHECK (char_length(summary) <= 400),
190658
+ summary text NOT NULL,
190288
190659
  changed_outputs jsonb NOT NULL DEFAULT '[]'::jsonb,
190289
190660
  unresolved_issues jsonb NOT NULL DEFAULT '[]'::jsonb,
190290
190661
  risks jsonb NOT NULL DEFAULT '[]'::jsonb,
@@ -190296,6 +190667,7 @@ var init_postgres_trace = __esm({
190296
190667
  await pool.query(`ALTER TABLE "${s2}".node_handoffs ADD COLUMN IF NOT EXISTS job_key text NOT NULL DEFAULT ''`);
190297
190668
  await pool.query(`ALTER TABLE "${s2}".node_handoffs ADD COLUMN IF NOT EXISTS part text`);
190298
190669
  await pool.query(`ALTER TABLE "${s2}".node_handoffs ADD COLUMN IF NOT EXISTS user_handoff jsonb`);
190670
+ await pool.query(`ALTER TABLE "${s2}".node_handoffs DROP CONSTRAINT IF EXISTS node_handoffs_summary_check`);
190299
190671
  await pool.query(`
190300
190672
  UPDATE "${s2}".node_handoffs h
190301
190673
  SET job_key=COALESCE(NULLIF(h.job_key, ''), r.job_key, 'attempt:' || h.attempt_id), part=r.part
@@ -190423,6 +190795,15 @@ var init_postgres_trace = __esm({
190423
190795
  reconstructed_summary, NEW.output_refs, '[]'::jsonb, reconstructed_risks,
190424
190796
  COALESCE(latest_pending, '[]'::jsonb), reconstructed_gaps, NULL,
190425
190797
  COALESCE(NEW.ended_at, NEW.updated_at, NEW.started_at))
190798
+ -- \u2605 2026-08-17\uFF1A\u7EC8\u6001\u91CD\u5EFA\u4ECE\u300C\u6574\u884C\u8986\u76D6\u300D\u6539\u6210\u300C**\u53EA\u586B\u7A7A**\u300D\u3002
190799
+ --
190800
+ -- \u75C5\u6839\uFF1A\u8FD9\u91CC\u539F\u672C\u5BF9\u4EFB\u4F55 produced_by <> 'agent' \u7684\u884C\u4E00\u5F8B\u7528 EXCLUDED \u8986\u76D6\u3002\u4EA4\u4F5C\u4E1A\u90A3\u4E00\u523B\u7531\u670D\u52A1\u7AEF
190801
+ -- \u843D\u7684\u771F\u5B9E\u6458\u8981\uFF08work.conclusion\uFF09\u4E0E\u4EA4\u4ED8\u7248\u672C\uFF0C\u4F1A\u5728\u4F1A\u8BDD\u7EC8\u6001\u88AB\u6362\u6210
190802
+ -- "[system_reconstructed] attempt=\u2026 reason=\u2026" \u8FD9\u4E32\u673A\u5668\u8BDD\u2014\u2014\u5199\u4E86\u7B49\u4E8E\u6CA1\u5199\u3002
190803
+ --
190804
+ -- \u65B0\u89C4\u5219\uFF1AAgent \u4EB2\u624B\u5199\u7684\u6700\u9AD8\u4F18\u5148\uFF08\u539F\u6837\u4FDD\u7559\uFF09\uFF1B\u7CFB\u7EDF\u884C\u53EA\u5728**\u8BE5\u5B57\u6BB5\u8FD8\u662F\u7A7A\u7684**\u65F6\u5019\u624D\u586B\uFF1B\u673A\u5668\u4E32
190805
+ -- \uFF08\u4EE5 [system_reconstructed] \u5F00\u5934\uFF09\u89C6\u540C\u7A7A\uFF0C\u53EF\u88AB\u771F\u5B9E\u6458\u8981\u66FF\u6362\u3001\u4E5F\u53EF\u88AB\u65B0\u7684\u673A\u5668\u4E32\u5237\u65B0\u3002
190806
+ -- outcome \u4F8B\u5916\uFF1A\u5B83\u63CF\u8FF0\u7684\u662F\u8FD9\u8D9F Attempt \u7684\u547D\u8FD0\uFF08\u53EF\u80FD\u5728\u4EA4\u7A3F\u540E\u624D\u5931\u8D25/\u8D85\u65F6\uFF09\uFF0C\u4EE5\u7EC8\u6001\u4E3A\u51C6\u3002
190426
190807
  ON CONFLICT (attempt_id) DO UPDATE SET
190427
190808
  produced_by=CASE
190428
190809
  WHEN current_handoff.produced_by = 'agent'
@@ -190434,29 +190815,46 @@ var init_postgres_trace = __esm({
190434
190815
  summary=CASE
190435
190816
  WHEN current_handoff.produced_by = 'agent'
190436
190817
  THEN current_handoff.summary
190818
+ WHEN COALESCE(current_handoff.summary, '') <> ''
190819
+ AND current_handoff.summary NOT LIKE '[system_reconstructed]%'
190820
+ THEN current_handoff.summary
190437
190821
  ELSE EXCLUDED.summary
190438
190822
  END,
190439
190823
  changed_outputs=CASE
190440
190824
  WHEN current_handoff.produced_by = 'agent'
190441
190825
  THEN current_handoff.changed_outputs
190826
+ WHEN jsonb_array_length(COALESCE(current_handoff.changed_outputs, '[]'::jsonb)) > 0
190827
+ THEN current_handoff.changed_outputs
190442
190828
  ELSE EXCLUDED.changed_outputs
190443
190829
  END,
190444
190830
  unresolved_issues=CASE
190445
190831
  WHEN current_handoff.produced_by = 'agent'
190446
- THEN current_handoff.unresolved_issues ELSE EXCLUDED.unresolved_issues END,
190832
+ THEN current_handoff.unresolved_issues
190833
+ WHEN jsonb_array_length(COALESCE(current_handoff.unresolved_issues, '[]'::jsonb)) > 0
190834
+ THEN current_handoff.unresolved_issues
190835
+ ELSE EXCLUDED.unresolved_issues END,
190447
190836
  risks=CASE
190448
190837
  WHEN current_handoff.produced_by = 'agent'
190449
- THEN current_handoff.risks ELSE EXCLUDED.risks END,
190838
+ THEN current_handoff.risks
190839
+ WHEN jsonb_array_length(COALESCE(current_handoff.risks, '[]'::jsonb)) > 0
190840
+ THEN current_handoff.risks
190841
+ ELSE EXCLUDED.risks END,
190450
190842
  next_actions=CASE
190451
190843
  WHEN current_handoff.produced_by = 'agent'
190452
- THEN current_handoff.next_actions ELSE EXCLUDED.next_actions END,
190844
+ THEN current_handoff.next_actions
190845
+ WHEN jsonb_array_length(COALESCE(current_handoff.next_actions, '[]'::jsonb)) > 0
190846
+ THEN current_handoff.next_actions
190847
+ ELSE EXCLUDED.next_actions END,
190453
190848
  verification_gaps=CASE
190454
190849
  WHEN current_handoff.produced_by = 'agent'
190455
- THEN current_handoff.verification_gaps ELSE EXCLUDED.verification_gaps END,
190850
+ THEN current_handoff.verification_gaps
190851
+ WHEN jsonb_array_length(COALESCE(current_handoff.verification_gaps, '[]'::jsonb)) > 0
190852
+ THEN current_handoff.verification_gaps
190853
+ ELSE EXCLUDED.verification_gaps END,
190456
190854
  user_handoff=CASE
190457
190855
  WHEN current_handoff.produced_by = 'agent'
190458
190856
  THEN current_handoff.user_handoff
190459
- ELSE EXCLUDED.user_handoff
190857
+ ELSE COALESCE(current_handoff.user_handoff, EXCLUDED.user_handoff)
190460
190858
  END;
190461
190859
  NEW.handoff_id := COALESCE(NEW.handoff_id, generated_handoff_id);
190462
190860
  RETURN NEW;
@@ -191259,7 +191657,6 @@ var init_postgres_trace = __esm({
191259
191657
  }
191260
191658
  async putHandoff(input) {
191261
191659
  if (!input.summary.trim()) throw new Error("handoff summary is required");
191262
- if ([...input.summary].length > 400) throw new Error("handoff summary exceeds 400 characters");
191263
191660
  const client = await this.pool.connect();
191264
191661
  try {
191265
191662
  await client.query("BEGIN");
@@ -194218,11 +194615,11 @@ async function startServe(opts) {
194218
194615
  fs28.writeFile(serverHeartbeatFile, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid }) + "\n", () => {
194219
194616
  });
194220
194617
  };
194618
+ const lock = path22.join(opts.dir, "serve.lock");
194619
+ acquireServeLock(lock);
194221
194620
  writeServerHeartbeat();
194222
194621
  const serverHeartbeatTimer = setInterval(writeServerHeartbeat, 5e3);
194223
194622
  serverHeartbeatTimer.unref?.();
194224
- const lock = path22.join(opts.dir, "serve.lock");
194225
- acquireServeLock(lock);
194226
194623
  let activeTimeoutPolicy;
194227
194624
  try {
194228
194625
  activeTimeoutPolicy = loadPolicyFromFile(resolvePolicyFilePath());
@@ -194583,7 +194980,7 @@ async function startServe(opts) {
194583
194980
  const trajReader = new FsTrajectorySink(opts.dir);
194584
194981
  const trace = createTraceDomain({ store: traceStore, readBundleFile: (id, rel) => trajReader.readBundleFile(id, rel) });
194585
194982
  const traceHealth = createTraceHealth();
194586
- const continuityModeRaw = process.env["OASIS_EXECUTION_CONTINUITY_MODE"]?.trim() || "off";
194983
+ const continuityModeRaw = process.env["OASIS_EXECUTION_CONTINUITY_MODE"]?.trim() || "record";
194587
194984
  if (!(continuityModeRaw === "off" || continuityModeRaw === "record" || continuityModeRaw === "resume")) {
194588
194985
  throw new Error(`OASIS_EXECUTION_CONTINUITY_MODE must be off|record|resume, received ${continuityModeRaw}`);
194589
194986
  }
@@ -194632,7 +195029,9 @@ async function startServe(opts) {
194632
195029
  return snap ? nodeConclusionsFromSnapshot(snap) : {};
194633
195030
  }
194634
195031
  });
194635
- console.log(`[serve] execution-continuity mode=${continuityMode}\uFF08record=\u5BF9\u7167\u7EC4\uFF0Cresume=\u6062\u590D\u5305\u5B9E\u9A8C\u7EC4\uFF09`);
195032
+ console.log(
195033
+ `[serve] execution-continuity mode=${continuityMode}\uFF08off=\u5168\u5173\uFF1Brecord/resume \u884C\u4E3A\u4E00\u81F4\uFF1A\u8BB0 checkpoint + \u4E2D\u65AD\u65F6\u6CE8\u6062\u590D\u5305\u3002A/B \u5B9E\u9A8C\u5DF2\u7ED3\u675F\uFF0Cresume \u4EC5\u4E3A\u517C\u5BB9\u4FDD\u7559\uFF09${process.env["OASIS_EXTERNAL_EFFECT_CAPTURE"] === "1" ? "\uFF1B\u5916\u90E8\u526F\u4F5C\u7528\u62E6\u622A\uFF1A\u5F00" : ""}`
195034
+ );
194636
195035
  const automationStore = pgPool ? await PostgresAutomationStore.open(pgPool, pgSchema) : new MemoryAutomationStore();
194637
195036
  console.log(`[serve] \u81EA\u52A8\u5316\u4F53\u7CFB\uFF1A${pgPool ? `Postgres schema=${pgSchema}` : "\u5185\u5B58 dev store"}`);
194638
195037
  const automations = createAutomationsDomain({ store: automationStore, kernel });
@@ -195382,9 +195781,36 @@ async function startServe(opts) {
195382
195781
  ...await materializeSkillFiles({ service: actors.service, actorId, runtimeKind })
195383
195782
  }),
195384
195783
  uploadChatAttachment: async ({ contentBase64 }) => ({ blobRef: await assets.put(new Uint8Array(Buffer.from(contentBase64, "base64"))) }),
195385
- onDispatchCommandCompleted: async ({ dispatchId, durableRefs, at }) => {
195386
- traceStoreSink.checkpoint(dispatchId, { trigger: "important_output", durableRefs, at });
195784
+ onDispatchCommandCompleted: async ({ dispatchId, command, durableRefs, at, delivered }) => {
195785
+ traceStoreSink.checkpoint(dispatchId, {
195786
+ trigger: "important_output",
195787
+ // 交作业的那条命令带着版本号(`wk:<节点>:<n>`),比笼统的「动过这个产物」有用得多——
195788
+ // 下一趟接手时据它知道上一趟已经交到哪一版。其余持久化命令仍记粗粒度引用。
195789
+ durableRefs: delivered?.refs ?? durableRefs,
195790
+ // 稳定步骤 id = 命令名:同一步骤重跑时 id 一致(checkpoint 契约对 stable id 的要求),
195791
+ // 恢复时下一趟能看出「上一趟已经 propose 过 / 已经 gap 过」。
195792
+ completedStepIds: [command],
195793
+ at
195794
+ });
195387
195795
  await traceStoreSink.flush(dispatchId);
195796
+ if (!delivered) return;
195797
+ await recordDeliveredOutputHandoff(
195798
+ {
195799
+ getAttempt: (attemptId) => traceStore.getAttempt(attemptId),
195800
+ getHandoff: (attemptId) => traceStore.getHandoff(attemptId),
195801
+ handoff: (input) => executionContinuity.service.handoff(input)
195802
+ },
195803
+ {
195804
+ attemptId: dispatchId,
195805
+ summary: delivered.summary,
195806
+ refs: delivered.refs,
195807
+ ...delivered.unresolvedIssues ? { unresolvedIssues: delivered.unresolvedIssues } : {},
195808
+ ...delivered.risks ? { risks: delivered.risks } : {},
195809
+ ...delivered.nextActions ? { nextActions: delivered.nextActions } : {},
195810
+ ...delivered.verificationGaps ? { verificationGaps: delivered.verificationGaps } : {},
195811
+ at
195812
+ }
195813
+ );
195388
195814
  },
195389
195815
  dispatchProduce: async ({ artifactId, actorId, part, companyId }) => {
195390
195816
  const d = dispatchers.get(companyId ?? defaultCompanyId);
@@ -196128,7 +196554,7 @@ async function startServe(opts) {
196128
196554
  const rev = kernelModel.revisions.get(workId);
196129
196555
  if (!rev) return;
196130
196556
  if (rev.state === "merged") return;
196131
- const replyIssueId = rev.replyToIssueId ?? null;
196557
+ const replyIssueId = rev.replyIssueId ?? null;
196132
196558
  console.log(`[new-engine] dispatchWork ${workId} \u2192 ${rev.artifactId} / ${rev.author}${replyIssueId ? `\uFF08\u56DE\u4FE1 ${replyIssueId}\uFF09` : ""}`);
196133
196559
  const art = kernelModel.artifacts.get(rev.artifactId);
196134
196560
  const wid = art?.workspace ?? "";
@@ -196432,7 +196858,7 @@ async function startServe(opts) {
196432
196858
  artifactId ? await resolveDispatchScope(artifactId) : void 0,
196433
196859
  (key) => emitBrokerRefused(actorId, artifactId, key)
196434
196860
  );
196435
- return continuityMode === "off" ? provision : { ...provision, env: { ...provision.env, OASIS_EXTERNAL_EFFECT_CAPTURE: "1" } };
196861
+ return process.env["OASIS_EXTERNAL_EFFECT_CAPTURE"] === "1" ? { ...provision, env: { ...provision.env, OASIS_EXTERNAL_EFFECT_CAPTURE: "1" } } : provision;
196436
196862
  },
196437
196863
  resolveActorContext: async (actorId) => buildActorContext((await runActors()).service, actorId),
196438
196864
  // 收尾评审的角色分工段(reviewer 此前彼此不知道对方存在 → 越界 / 重复劳动)。
@@ -196486,13 +196912,15 @@ async function startServe(opts) {
196486
196912
  getHandoff: (attemptId) => traceStore.getHandoff(attemptId)
196487
196913
  }),
196488
196914
  executionContinuityInstructions: continuityMode === "off" ? renderBusinessHandoffProtocol() : renderContinuityProtocol(),
196489
- ...continuityMode === "resume" ? {
196915
+ // 恢复包注入(2026-08-17):从「只有 resume 实验组才给」改成「只要账本开着就给」——
196916
+ // A/B 实验早已结束,record 与 resume 现在行为一致(resume 值保留只为兼容既有部署变量)。
196917
+ ...continuityMode !== "off" ? {
196490
196918
  resolveExecutionResumePack: async (artifactId, workOrderId, scope) => {
196491
196919
  const pack = await executionContinuity.service.resumePack(workOrderId, artifactId, companyId, {
196492
196920
  jobKey: scope.jobKey,
196493
196921
  part: scope.part ?? null
196494
196922
  });
196495
- return pack ? renderResumePack(pack) : null;
196923
+ return shouldInjectResumePack(pack) ? renderResumePack(pack) : null;
196496
196924
  }
196497
196925
  } : {},
196498
196926
  schema: liveSchemaMap,
@@ -198612,9 +199040,16 @@ var COMMAND_DECLS = {
198612
199040
  { name: "rebase-of", desc: "\u628A\u4E00\u6761 queued revision \u6539 base \u540E\u91CD\u6302" },
198613
199041
  { name: "checkpoint", desc: "\u65AD\u70B9\u7EED\u4F20\u6807\u8BB0" },
198614
199042
  { name: "continue", desc: "\u7EE7\u7EED\u4E0A\u6B21\u65AD\u70B9" },
198615
- { name: "resolves", desc: "\u672C\u7248\u663E\u5F0F\u7B54\u590D\u7684\u4FE1\uFF08\u9017\u53F7\u5206\u9694 annotationId\uFF09\u3002\u5E38\u89C4\u4E0D\u7528\u586B\u2014\u2014\u7CFB\u7EDF\u6309\u6D3E\u53D1\u70B9\u540D\u96C6\u81EA\u52A8\u76D6\u7AE0\uFF1B\u53EA\u5728\u4E2D\u9014\u7ECF status \u53D1\u73B0\u7684\u4FE1\u7B49\u7279\u4F8B\u7528" }
199043
+ { name: "resolves", desc: "\u672C\u7248\u663E\u5F0F\u7B54\u590D\u7684\u4FE1\uFF08\u9017\u53F7\u5206\u9694 annotationId\uFF09\u3002\u5E38\u89C4\u4E0D\u7528\u586B\u2014\u2014\u7CFB\u7EDF\u6309\u6D3E\u53D1\u70B9\u540D\u96C6\u81EA\u52A8\u76D6\u7AE0\uFF1B\u53EA\u5728\u4E2D\u9014\u7ECF status \u53D1\u73B0\u7684\u4FE1\u7B49\u7279\u4F8B\u7528" },
199044
+ { name: "unresolved", desc: "\u7559\u7ED9\u4E0B\u6E38\u7684\u672A\u51B3\u4E8B\u9879\uFF08\u5206\u53F7\u5206\u9694\uFF09\u3002\u5199\u8FDB\u672C\u8282\u70B9\u7684\u6280\u672F\u4EA4\u63A5\uFF0C\u4E0B\u6E38\u5F00\u5DE5\u524D\u4F1A\u8BFB\u5230" },
199045
+ { name: "risks", desc: "\u4E0B\u6E38\u8BE5\u77E5\u9053\u7684\u98CE\u9669\uFF08\u5206\u53F7\u5206\u9694\uFF09" },
199046
+ { name: "next", desc: "\u5EFA\u8BAE\u4E0B\u6E38\u63A5\u7740\u505A\u4EC0\u4E48\uFF08\u5206\u53F7\u5206\u9694\uFF09" },
199047
+ { name: "gaps", desc: "\u4F60\u6CA1\u80FD\u6838\u9A8C\u7684\u90E8\u5206\uFF08\u5206\u53F7\u5206\u9694\uFF09\u2014\u2014\u522B\u8BA9\u4E0B\u6E38\u4EE5\u4E3A\u5DF2\u9A8C\u8FC7" }
198616
199048
  ],
198617
- examples: ['oasis propose artifact:dev:abc-123 --from-dir deliverable/ --reason "\u4FEE\u590D\u767B\u5F55\u9875\u767D\u5C4F"']
199049
+ examples: [
199050
+ 'oasis propose artifact:dev:abc-123 --from-dir deliverable/ --reason "\u4FEE\u590D\u767B\u5F55\u9875\u767D\u5C4F"',
199051
+ 'oasis propose artifact:dev:abc-123 --reason "\u63A5\u53E3\u8054\u8C03\u5B8C\u6210" --unresolved "\u7EBF\u4E0A\u914D\u989D\u6CA1\u786E\u8BA4" --gaps "\u6CA1\u8DD1\u538B\u6D4B"'
199052
+ ]
198618
199053
  },
198619
199054
  queue: {
198620
199055
  group: "\u7248\u672C\u673A\uFF08\xA75\uFF09",
@@ -199896,6 +200331,11 @@ function needPos(positional, i, usage) {
199896
200331
  if (v2 === void 0) throw new Error(`\u7528\u6CD5: ${usage}`);
199897
200332
  return v2;
199898
200333
  }
200334
+ function semicolonList(raw, field) {
200335
+ if (raw === void 0) return {};
200336
+ const items = raw.split(";").map((x2) => x2.trim()).filter(Boolean);
200337
+ return items.length > 0 ? { [field]: items } : {};
200338
+ }
199899
200339
  async function readStdin() {
199900
200340
  if (process.stdin.isTTY) return "";
199901
200341
  const chunks = [];
@@ -201282,7 +201722,15 @@ ${res.warning}`);
201282
201722
  ...checkpoint ? { continuation: { checkpoint: true, continue: true } } : {},
201283
201723
  // 申报单(B 部):显式增补"本版答复的信"(逗号分隔 annotationId)。常规不用填——系统按派发点名集
201284
201724
  // 自动盖章;只在中途经 status 发现的信等特例用。
201285
- ...flags.get("resolves") !== void 0 ? { resolves: flags.get("resolves").split(",").map((x2) => x2.trim()).filter(Boolean) } : {}
201725
+ ...flags.get("resolves") !== void 0 ? { resolves: flags.get("resolves").split(",").map((x2) => x2.trim()).filter(Boolean) } : {},
201726
+ // 技术交接的四项「系统看不见的东西」(2026-08-17):交作业时顺手带上,服务端写进本节点的
201727
+ // NodeHandoff。**不必再等会话终态另跑 `oasis continuity-handoff`**——那条命令的说明书在
201728
+ // execution/CONTINUITY.md 里,而 adapter 只把 TASK.md 当提示词,线上因此只有 3.6% 的节点写到了。
201729
+ // 分号分隔;一条都不填也完全正常(摘要与交付版本系统自己会记)。
201730
+ ...semicolonList(flags.get("unresolved"), "unresolvedIssues"),
201731
+ ...semicolonList(flags.get("risks"), "risks"),
201732
+ ...semicolonList(flags.get("next"), "nextActions"),
201733
+ ...semicolonList(flags.get("gaps"), "verificationGaps")
201286
201734
  });
201287
201735
  println(message);
201288
201736
  break;
@@ -202556,7 +203004,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
202556
203004
  }
202557
203005
 
202558
203006
  // src/index.ts
202559
- var PKG_VERSION = true ? "0.1.106" : "dev";
203007
+ var PKG_VERSION = true ? "0.1.108" : "dev";
202560
203008
  var OASIS_DIR = path28.join(os11.homedir(), ".oasis");
202561
203009
  var CONFIG_FILE = path28.join(OASIS_DIR, "node-config.json");
202562
203010
  var PID_FILE = path28.join(OASIS_DIR, "node.pid");