oasis_test 0.1.99 → 0.1.101

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 +1137 -209
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2607,7 +2607,53 @@ var init_trace = __esm({
2607
2607
  });
2608
2608
 
2609
2609
  // ../contract/src/execution-continuity.ts
2610
- var ExecutionAttemptNotFoundError;
2610
+ function isUserHandoffDeliverable(ref2) {
2611
+ const kind = ref2.kind.trim().toLowerCase();
2612
+ const id = ref2.id.trim();
2613
+ const label = ref2.label?.trim().toLowerCase();
2614
+ if (!id || !USER_HANDOFF_DELIVERABLE_KINDS.has(kind)) return false;
2615
+ if (label && INTERNAL_HANDOFF_LABELS.has(label)) return false;
2616
+ return !["attempt:", "checkpoint:", "handoff:", "op:", "run:", "runtime:"].some((prefix) => id.toLowerCase().startsWith(prefix));
2617
+ }
2618
+ function userHandoffDeliverables(refs) {
2619
+ const seen = /* @__PURE__ */ new Set();
2620
+ return refs.flatMap((ref2) => {
2621
+ if (!isUserHandoffDeliverable(ref2)) return [];
2622
+ const normalized4 = {
2623
+ ...ref2,
2624
+ kind: ref2.kind.trim(),
2625
+ id: ref2.id.trim(),
2626
+ ...ref2.label === void 0 ? {} : { label: ref2.label.trim() }
2627
+ };
2628
+ const key = `${normalized4.kind.toLowerCase()}\0${normalized4.id}\0${normalized4.version ?? ""}`;
2629
+ if (seen.has(key)) return [];
2630
+ seen.add(key);
2631
+ return [normalized4];
2632
+ });
2633
+ }
2634
+ function normalizeUserHandoff(outcome, _changedOutputs, authored) {
2635
+ if (!authored) return null;
2636
+ const status = outcome === "succeeded" ? "delivered" : "not_delivered";
2637
+ if (authored.status !== status) {
2638
+ throw new Error(`user handoff status ${authored.status} does not match outcome ${outcome}`);
2639
+ }
2640
+ const summary = authored.summary.trim();
2641
+ if (!summary) throw new Error("user handoff summary is required");
2642
+ if ([...summary].length > 400) {
2643
+ throw new Error("user handoff summary exceeds 400 characters");
2644
+ }
2645
+ const nextStep = authored.nextStep.trim();
2646
+ if ([...nextStep].length > 400) {
2647
+ throw new Error("user handoff next step exceeds 400 characters");
2648
+ }
2649
+ return {
2650
+ status,
2651
+ summary,
2652
+ deliverables: status === "delivered" ? userHandoffDeliverables(authored.deliverables) : [],
2653
+ nextStep
2654
+ };
2655
+ }
2656
+ var ExecutionAttemptNotFoundError, USER_HANDOFF_DELIVERABLE_KINDS, INTERNAL_HANDOFF_LABELS;
2611
2657
  var init_execution_continuity = __esm({
2612
2658
  "../contract/src/execution-continuity.ts"() {
2613
2659
  "use strict";
@@ -2618,6 +2664,40 @@ var init_execution_continuity = __esm({
2618
2664
  this.name = "ExecutionAttemptNotFoundError";
2619
2665
  }
2620
2666
  };
2667
+ USER_HANDOFF_DELIVERABLE_KINDS = /* @__PURE__ */ new Set([
2668
+ "artifact",
2669
+ "audio",
2670
+ "blob",
2671
+ "code",
2672
+ "commit",
2673
+ "data",
2674
+ "dataset",
2675
+ "diff",
2676
+ "document",
2677
+ "file",
2678
+ "html",
2679
+ "image",
2680
+ "link",
2681
+ "markdown",
2682
+ "markdown-document",
2683
+ "output",
2684
+ "report",
2685
+ "repository",
2686
+ "revision",
2687
+ "text",
2688
+ "url",
2689
+ "video"
2690
+ ]);
2691
+ INTERNAL_HANDOFF_LABELS = /* @__PURE__ */ new Set([
2692
+ "checkpoint",
2693
+ "conclude",
2694
+ "continuity-checkpoint",
2695
+ "continuity-handoff",
2696
+ "control",
2697
+ "gap",
2698
+ "op",
2699
+ "operation"
2700
+ ]);
2621
2701
  }
2622
2702
  });
2623
2703
 
@@ -3289,6 +3369,22 @@ async function assembleContext(args) {
3289
3369
  const actorLabels = args.resolveActorLabels ? await args.resolveActorLabels().catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
3290
3370
  const actorLabel = (id) => id === void 0 ? "" : actorLabels.get(id) ?? id;
3291
3371
  const files = {};
3372
+ const upstreamHandoffFiles = /* @__PURE__ */ new Map();
3373
+ if (args.resolveUpstreamHandoff && artifact.workspace) {
3374
+ for (const edge of artifact.inputs) {
3375
+ if (edge.pinned === null) continue;
3376
+ const content = await args.resolveUpstreamHandoff({
3377
+ workOrderId: artifact.workspace,
3378
+ downstreamNodeId: artifact.id,
3379
+ upstreamNodeId: edge.to,
3380
+ pinnedWorkId: edge.pinned
3381
+ });
3382
+ if (content) {
3383
+ const dir = `inputs/${slug2(edge.to)}@${edge.pinned.slice(9, 17)}`;
3384
+ upstreamHandoffFiles.set(`${dir}/${UPSTREAM_HANDOFF_BASENAME}`, content);
3385
+ }
3386
+ }
3387
+ }
3292
3388
  if (canvasMode && artifact.description) files["DESIGN_BRIEF.md"] = artifact.description;
3293
3389
  if (closureReportMode && artifact.workspace) {
3294
3390
  const ledger = buildWorkorderLedger(model, artifact.workspace);
@@ -3928,7 +4024,16 @@ ${m2.spec.split("\n").map((l) => ` > ${l}`).join("\n")}`);
3928
4024
  ``,
3929
4025
  `## \u4F60\u7684\u8F93\u5165`,
3930
4026
  args.action === "review" ? `\u8BFB \`inputs/\` \u2014\u2014 \u5404\u4E0A\u6E38\u4E00\u4E2A\u6587\u4EF6\u5939\uFF1B**\u636E\u5B83\u6838\u5BF9\u5F53\u524D\u4EA7\u7269\u4E0E\u4E0A\u6E38\u662F\u5426\u4E00\u81F4**\uFF08\u6587\u672C\u7C7B=\u6587\u4EF6\uFF0C\u4EE3\u7801/\u5916\u90E8\u7C7B=\u6307\u9488\uFF09\uFF1A` : `\u8BFB \`inputs/\` \u2014\u2014 \u6BCF\u4E2A\u4E0A\u6E38\u4E00\u4E2A\u6587\u4EF6\u5939\uFF0C\u6309\u91CC\u9762\u5185\u5BB9\u529E\uFF08**\u6587\u672C\u7C7B=\u6587\u4EF6\uFF0C\u4EE3\u7801/\u5916\u90E8\u7C7B=\u6307\u9488**\uFF09\uFF1A`,
3931
- ...artifact.inputs.filter((e) => e.pinned !== null).length === 0 ? [`- \uFF08\u672C\u4EA7\u7269\u6682\u65E0\u5DF2\u5C31\u7EEA\u7684\u4E0A\u6E38\u8F93\u5165\uFF09`] : artifact.inputs.filter((e) => e.pinned !== null).map((e) => `- \`inputs/${slug2(e.to)}@${e.pinned.slice(9, 17)}/\` \u2190 ${e.to}${e.required ? "\uFF08required\uFF09" : ""}`)
4027
+ ...artifact.inputs.filter((e) => e.pinned !== null).length === 0 ? [`- \uFF08\u672C\u4EA7\u7269\u6682\u65E0\u5DF2\u5C31\u7EEA\u7684\u4E0A\u6E38\u8F93\u5165\uFF09`] : artifact.inputs.filter((e) => e.pinned !== null).map((e) => `- \`inputs/${slug2(e.to)}@${e.pinned.slice(9, 17)}/\` \u2190 ${e.to}${e.required ? "\uFF08required\uFF09" : ""}`),
4028
+ ...upstreamHandoffFiles.size === 0 ? [] : [
4029
+ ``,
4030
+ `### \u4E0A\u6E38\u4E1A\u52A1\u4EA4\u63A5\uFF08\u5F00\u5DE5\u524D\u5FC5\u8BFB\uFF09`,
4031
+ `1. \u5148\u8BFB\u6BCF\u4E2A\u4E0A\u6E38\u76EE\u5F55\u4E2D\u7684 \`${UPSTREAM_HANDOFF_BASENAME}\`\u3002`,
4032
+ `2. \u518D\u8BFB\u540C\u76EE\u5F55\u7684 \`CONTENT.md\`\u3001\`PIN.md\` \u548C\u5B9E\u9645\u4E1A\u52A1\u6587\u4EF6\u3002`,
4033
+ `3. \u7528 handoff \u7406\u89E3\u4E0A\u6E38\u5B8C\u6210\u60C5\u51B5\u3001\u53D8\u66F4\u8F93\u51FA\u3001\u672A\u51B3\u4E8B\u9879\u3001\u98CE\u9669\u4E0E\u5EFA\u8BAE\u52A8\u4F5C\u3002`,
4034
+ `4. \u7528 pinned inputs \u4E0E handoff \u4E2D\u7684 durable output \u5F15\u7528\u6838\u9A8C\u5B9E\u9645\u4EA7\u7269\uFF1Bhandoff \u662F\u4EA4\u63A5\u8BF4\u660E\uFF0C\u4E0D\u66FF\u4EE3\u4EA7\u7269\u4E8B\u5B9E\u3002`,
4035
+ ...[...upstreamHandoffFiles.keys()].map((path26) => `- \u4EA4\u63A5\uFF1A\`${path26}\``)
4036
+ ]
3932
4037
  ],
3933
4038
  ...skillsLines.length > 0 ? [``, `## \u4F60\u7684\u6280\u80FD\uFF08\u5DF2\u4E3A\u4F60\u542F\u7528\uFF0C\u6309\u9700\u8C03\u7528\uFF09`, ...skillsLines] : [],
3934
4039
  ``,
@@ -4029,6 +4134,17 @@ ${m2.spec.split("\n").map((l) => ` > ${l}`).join("\n")}`);
4029
4134
  required=${edge.required}
4030
4135
  ` + (latest && latest !== edge.pinned ? `\u6CE8\u610F\uFF1A\u4E0A\u6E38\u5DF2\u6709\u66F4\u65B0\u91CC\u7A0B\u7891 ${latest}\uFF08\u4F60 pin \u7684\u662F\u65E7\u7248\uFF09
4031
4136
  ` : "");
4137
+ const handoffPath = `${dir}/${UPSTREAM_HANDOFF_BASENAME}`;
4138
+ const handoff = upstreamHandoffFiles.get(handoffPath);
4139
+ if (handoff !== void 0) {
4140
+ const collision = Object.keys(files).find((file) => file.toLowerCase() === handoffPath.toLowerCase());
4141
+ if (collision) {
4142
+ throw new Error(
4143
+ `upstream input ${edge.to}@${edge.pinned} uses system-reserved filename ${UPSTREAM_HANDOFF_BASENAME} (${collision})`
4144
+ );
4145
+ }
4146
+ files[handoffPath] = handoff;
4147
+ }
4032
4148
  }
4033
4149
  const limit = args.historyLimit ?? 20;
4034
4150
  const opLog = args.oplog ? (await args.oplog.read(artifactId)).slice(-limit).map((op) => `#${op.seq} ${op.timestamp} ${op.kind} by ${op.actor}`).join("\n") : "";
@@ -4100,7 +4216,7 @@ required=${c.required}
4100
4216
  }
4101
4217
  return { text: lines.join("\n"), files };
4102
4218
  }
4103
- var slug2, WORKDIR_KEEP_BASENAME, WORKDIR_KEEP_DELIVERABLE, WORKDIR_KEEP_INPUTS, WORKDIR_KEEP_BODY, REVIEW_NOTE_MAX_CHARS, decodeText, REVIEW_ROLE_LENS_PLACEHOLDER, REVIEW_GATE_DIVISION_PLACEHOLDER;
4219
+ var slug2, UPSTREAM_HANDOFF_BASENAME, WORKDIR_KEEP_BASENAME, WORKDIR_KEEP_DELIVERABLE, WORKDIR_KEEP_INPUTS, WORKDIR_KEEP_BODY, REVIEW_NOTE_MAX_CHARS, decodeText, REVIEW_ROLE_LENS_PLACEHOLDER, REVIEW_GATE_DIVISION_PLACEHOLDER;
4104
4220
  var init_assembler = __esm({
4105
4221
  "../core/src/assembler.ts"() {
4106
4222
  "use strict";
@@ -4112,6 +4228,7 @@ var init_assembler = __esm({
4112
4228
  init_diff();
4113
4229
  init_workorder_ledger();
4114
4230
  slug2 = (id) => id.replace(/[^a-zA-Z0-9_-]+/g, "_");
4231
+ UPSTREAM_HANDOFF_BASENAME = "_HANDOFF.md";
4115
4232
  WORKDIR_KEEP_BASENAME = ".oasis-keep";
4116
4233
  WORKDIR_KEEP_DELIVERABLE = `deliverable/${WORKDIR_KEEP_BASENAME}`;
4117
4234
  WORKDIR_KEEP_INPUTS = `inputs/${WORKDIR_KEEP_BASENAME}`;
@@ -5242,7 +5359,9 @@ var init_dispatcher = __esm({
5242
5359
  artifactId: args.artifactId,
5243
5360
  actor: args.actor,
5244
5361
  action: "produce",
5245
- ...args.part !== void 0 ? { part: args.part } : {}
5362
+ ...args.part !== void 0 ? { part: args.part } : {},
5363
+ ...args.engineWorkId !== void 0 ? { engineWorkId: args.engineWorkId } : {},
5364
+ ...args.engineCompanyId !== void 0 ? { engineCompanyId: args.engineCompanyId } : {}
5246
5365
  }, args.bypassScheduling === true, args.dispatchId);
5247
5366
  }
5248
5367
  /** 新引擎回信 work(设计-最终 §回信轮)的 effect 调到此——派发收容回信会话(唯一写动作 `oasis reply`)。
@@ -5517,6 +5636,7 @@ var init_dispatcher = __esm({
5517
5636
  ...prevBriefSeq !== void 0 ? { prevBriefSeq } : {},
5518
5637
  ...this.opts.schema !== void 0 ? { schema: this.opts.schema } : {},
5519
5638
  ...codeRepo ? { codeRepo } : {},
5639
+ ...this.opts.resolveUpstreamHandoff !== void 0 ? { resolveUpstreamHandoff: this.opts.resolveUpstreamHandoff } : {},
5520
5640
  ...this.opts.resolveActorContext !== void 0 ? { resolveActorContext: this.opts.resolveActorContext } : {},
5521
5641
  ...this.opts.resolveActorLabels !== void 0 ? { resolveActorLabels: this.opts.resolveActorLabels } : {},
5522
5642
  ...this.opts.resolveRoleBriefs !== void 0 ? { resolveRoleBriefs: this.opts.resolveRoleBriefs } : {},
@@ -5533,7 +5653,18 @@ var init_dispatcher = __esm({
5533
5653
  continuityArtifact.workspace,
5534
5654
  { jobKey, ...spec.part !== void 0 ? { part: spec.part } : {} }
5535
5655
  );
5536
- if (resumePack) bundle.files["execution/RESUME.md"] = resumePack;
5656
+ if (resumePack) {
5657
+ bundle.files["execution/RESUME.md"] = resumePack;
5658
+ bundle.files["TASK.md"] += [
5659
+ "",
5660
+ "## \u6267\u884C\u6062\u590D\uFF08\u4EC5\u5F53\u524D\u8282\u70B9\uFF09",
5661
+ "\u672C\u8F6E\u662F\u540C\u4E00\u8282\u70B9\u4E0A\u4E00\u6B21 Attempt \u4E2D\u65AD\u540E\u7684\u7EE7\u7EED\u6267\u884C\u3002\u5F00\u5DE5\u524D\u5148\u8BFB `execution/RESUME.md`\uFF0C",
5662
+ "\u6838\u5BF9\u6700\u65B0 Checkpoint\u3001\u5DF2\u5B8C\u6210\u6B65\u9AA4\u3001durable outputs\u3001ExternalEffects \u4E0E\u672A\u51B3\u6B65\u9AA4\uFF1B",
5663
+ "\u4ECE\u5C1A\u672A\u5B8C\u6210\u7684\u90E8\u5206\u7EE7\u7EED\uFF0C\u907F\u514D\u91CD\u590D\u5DF2\u7ECF\u786E\u8BA4\u5B8C\u6210\u7684\u6B65\u9AA4\u6216\u5916\u90E8\u526F\u4F5C\u7528\u3002",
5664
+ "\u8FD9\u4EFD\u6062\u590D\u6750\u6599\u4E0D\u662F\u4E0A\u6E38\u8282\u70B9\u7684\u4E1A\u52A1\u4EA4\u63A5\uFF1B\u4E0A\u6E38\u4EA4\u63A5\u4ECD\u4EE5 `inputs/*/_HANDOFF.md` \u4E3A\u51C6\u3002",
5665
+ ""
5666
+ ].join("\n");
5667
+ }
5537
5668
  }
5538
5669
  this.assertSpawnAttemptCurrent(jobKey, attempt);
5539
5670
  const limits = { wallClockMs: this.wallClockFor(spec.artifactId) };
@@ -5634,6 +5765,8 @@ var init_dispatcher = __esm({
5634
5765
  }
5635
5766
  const tsess = {
5636
5767
  runId: session.id,
5768
+ ...spec.engineWorkId !== void 0 ? { engineWorkId: spec.engineWorkId } : {},
5769
+ ...spec.engineCompanyId !== void 0 ? { engineCompanyId: spec.engineCompanyId } : {},
5637
5770
  // 运行时会话号落账:它是 cwd 目录名、也是 --resume 的键。不记的话 run 与它跑在哪个
5638
5771
  // 工作目录之间就断了链(只能靠 grep 包内容 + 比对时间戳去猜)。
5639
5772
  ...runtimeSessionId !== void 0 ? { runtimeSessionId } : {},
@@ -6146,10 +6279,12 @@ var init_dispatcher = __esm({
6146
6279
  });
6147
6280
 
6148
6281
  // ../engine/src/model.ts
6149
- var DEFAULT_CONFIG, MAX_WORK_DURATION_MS, MAX_REVIEW_DURATION_MS, MAX_REVIEW_RETRIES, deriveWorkId, deriveReplyWorkId, deriveReviewId, deriveReplyId;
6282
+ var BUSINESS_HANDOFF_POLICY_FIELD, BUSINESS_HANDOFF_POLICY_EXEMPT, DEFAULT_CONFIG, MAX_WORK_DURATION_MS, MAX_REVIEW_DURATION_MS, MAX_REVIEW_RETRIES, deriveWorkId, deriveReplyWorkId, deriveReviewId, deriveReplyId;
6150
6283
  var init_model = __esm({
6151
6284
  "../engine/src/model.ts"() {
6152
6285
  "use strict";
6286
+ BUSINESS_HANDOFF_POLICY_FIELD = "businessHandoffPolicy";
6287
+ BUSINESS_HANDOFF_POLICY_EXEMPT = "exempt";
6153
6288
  DEFAULT_CONFIG = {
6154
6289
  maxRetries: 3
6155
6290
  };
@@ -6185,6 +6320,7 @@ function eventAnchors(e) {
6185
6320
  return { nodeId: e.nodeId, workId: e.workId };
6186
6321
  case "work.started":
6187
6322
  case "work.response":
6323
+ case "work.handoff_recorded":
6188
6324
  case "work.timeout":
6189
6325
  case "work.accept":
6190
6326
  case "work.submit_output":
@@ -6612,6 +6748,7 @@ function scan(state, ctx) {
6612
6748
  if (wstate === "success" && node.latestAcceptId !== lw.id) {
6613
6749
  const reqs = state.requirementsOf(node.id);
6614
6750
  if (reqs.length === 0) {
6751
+ if (!businessHandoffBarrierSatisfied(state, node.id, lw.id)) continue;
6615
6752
  events.push({
6616
6753
  kind: "work.accept",
6617
6754
  workorderId: state.workorder.id,
@@ -6641,6 +6778,7 @@ function tryToRun(state, nodeId, ctx, events) {
6641
6778
  if (e.kind !== "data" || !e.required) continue;
6642
6779
  const upstream = state.node(e.fromNodeId);
6643
6780
  if (!upstream?.latestAcceptId) return;
6781
+ if (isBusinessDependencyEdge(state, e) && !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestAcceptId)) return;
6644
6782
  }
6645
6783
  if (state.hasQueuedWorkFor(nodeId)) return;
6646
6784
  const workId = deriveWorkId(nodeId, state.worksOf(nodeId).length + 1);
@@ -6742,6 +6880,7 @@ function processReviews(state, nodeId, workId, ctx, events) {
6742
6880
  }
6743
6881
  }
6744
6882
  if (shouldAccept) {
6883
+ if (!businessHandoffBarrierSatisfied(state, nodeId, workId)) return;
6745
6884
  events.push({
6746
6885
  kind: "work.accept",
6747
6886
  workorderId: state.workorder.id,
@@ -6859,8 +6998,28 @@ function checkTerminated(state) {
6859
6998
  if (state.allIssues().some((i) => i.state === "open")) return false;
6860
6999
  const live = state.liveNodes();
6861
7000
  if (live.length === 0) return true;
7001
+ for (const edge of state.allEdges()) {
7002
+ if (!isBusinessDependencyEdge(state, edge)) continue;
7003
+ const upstream = state.node(edge.fromNodeId);
7004
+ if (!upstream?.latestWorkId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestWorkId)) return false;
7005
+ }
6862
7006
  return live.every((n) => n.latestWorkId !== null && n.latestAcceptId === n.latestWorkId);
6863
7007
  }
7008
+ function isBusinessDependencyEdge(state, edge) {
7009
+ const downstream = state.node(edge.toNodeId);
7010
+ return edge.kind === "data" && edge.required && downstream?.fields?.[BUSINESS_HANDOFF_POLICY_FIELD] !== BUSINESS_HANDOFF_POLICY_EXEMPT;
7011
+ }
7012
+ function businessHandoffBarrierSatisfied(state, nodeId, workId) {
7013
+ const work = state.work(workId);
7014
+ if (!requiresBusinessHandoff(state, nodeId, work)) return true;
7015
+ const node = state.node(nodeId);
7016
+ return node?.latestWorkId === workId && work?.agentHandoffAttemptId !== null && work?.businessHandoffStatus === "delivered";
7017
+ }
7018
+ function requiresBusinessHandoff(state, nodeId, work) {
7019
+ if (!state.edgesFrom(nodeId).some((edge) => isBusinessDependencyEdge(state, edge))) return false;
7020
+ if (typeof work?.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) return false;
7021
+ return true;
7022
+ }
6864
7023
  function activate(state, nodeId, _ctx) {
6865
7024
  const node = state.node(nodeId);
6866
7025
  if (!node) return "cancelled";
@@ -6993,6 +7152,10 @@ CREATE TABLE IF NOT EXISTS ${schema}.works (
6993
7152
  reply_to_issue_id text,
6994
7153
  output_version_no integer,
6995
7154
  conclusion text,
7155
+ -- NodeHandoff \u4E8B\u5B9E\u6E90\u7684\u6700\u5C0F\u8C03\u5EA6\u6295\u5F71\uFF1B\u4EC5 agent-authored handoff \u53EF\u5199\uFF0Csystem reconstruction \u4E0D\u5199\u3002
7156
+ agent_handoff_attempt_id text,
7157
+ business_handoff_status text CHECK (business_handoff_status IN ('delivered','not_delivered')),
7158
+ business_handoff_recorded_at timestamptz,
6996
7159
  acceptance_state text CHECK (acceptance_state IN ('draft','accepted','rejected')),
6997
7160
  accepted_at timestamptz,
6998
7161
  accepted_by text,
@@ -7155,6 +7318,12 @@ CREATE TABLE IF NOT EXISTS ${schema}.event_handler_log (
7155
7318
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS retry_at timestamptz;
7156
7319
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS retry_at timestamptz;
7157
7320
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS status text;
7321
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS agent_handoff_attempt_id text;
7322
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_status text;
7323
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_recorded_at timestamptz;
7324
+ ALTER TABLE ${schema}.works DROP CONSTRAINT IF EXISTS works_business_handoff_status_check;
7325
+ ALTER TABLE ${schema}.works ADD CONSTRAINT works_business_handoff_status_check
7326
+ CHECK (business_handoff_status IS NULL OR business_handoff_status IN ('delivered','not_delivered'));
7158
7327
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS status text;
7159
7328
  -- \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
7160
7329
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
@@ -12568,6 +12737,9 @@ var init_store_postgres = __esm({
12568
12737
  replyToIssueId: w2.reply_to_issue_id ?? null,
12569
12738
  outputVersionNo: w2.output_version_no,
12570
12739
  conclusion: w2.conclusion,
12740
+ agentHandoffAttemptId: w2.agent_handoff_attempt_id ?? null,
12741
+ businessHandoffStatus: w2.business_handoff_status ?? null,
12742
+ businessHandoffRecordedAt: iso(w2.business_handoff_recorded_at),
12571
12743
  acceptanceState: w2.acceptance_state,
12572
12744
  acceptedAt: iso(w2.accepted_at),
12573
12745
  acceptedBy: w2.accepted_by,
@@ -12771,8 +12943,9 @@ var init_store_postgres = __esm({
12771
12943
  `INSERT INTO ${this.s}.works
12772
12944
  (id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
12773
12945
  outcome_detail,session_ref,continues_work_id,reply_to_issue_id,output_version_no,
12774
- node_version,conclusion,acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason)
12775
- 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)
12946
+ node_version,conclusion,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
12947
+ acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason)
12948
+ 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)
12776
12949
  ON CONFLICT (id) DO NOTHING`,
12777
12950
  [
12778
12951
  w2.id,
@@ -12794,6 +12967,9 @@ var init_store_postgres = __esm({
12794
12967
  w2.outputVersionNo,
12795
12968
  w2.nodeVersion,
12796
12969
  w2.conclusion,
12970
+ w2.agentHandoffAttemptId,
12971
+ w2.businessHandoffStatus,
12972
+ w2.businessHandoffRecordedAt,
12797
12973
  w2.acceptanceState,
12798
12974
  w2.acceptedAt,
12799
12975
  w2.acceptedBy,
@@ -13489,7 +13665,7 @@ var init_workorder = __esm({
13489
13665
  });
13490
13666
 
13491
13667
  // ../engine/src/handlers/work.ts
13492
- var workCreate, workKill, workResponse, workStart, workSubmitOutput, workTimeout;
13668
+ var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workTimeout;
13493
13669
  var init_work = __esm({
13494
13670
  "../engine/src/handlers/work.ts"() {
13495
13671
  "use strict";
@@ -13522,6 +13698,9 @@ var init_work = __esm({
13522
13698
  replyToIssueId: e.replyToIssueId,
13523
13699
  outputVersionNo: null,
13524
13700
  conclusion: null,
13701
+ agentHandoffAttemptId: null,
13702
+ businessHandoffStatus: null,
13703
+ businessHandoffRecordedAt: null,
13525
13704
  acceptanceState: null,
13526
13705
  acceptedAt: null,
13527
13706
  acceptedBy: null,
@@ -13554,6 +13733,9 @@ var init_work = __esm({
13554
13733
  continuesWorkId: e.continuesWorkId,
13555
13734
  outputVersionNo: null,
13556
13735
  conclusion: null,
13736
+ agentHandoffAttemptId: null,
13737
+ businessHandoffStatus: null,
13738
+ businessHandoffRecordedAt: null,
13557
13739
  acceptanceState: null,
13558
13740
  acceptedAt: null,
13559
13741
  acceptedBy: null,
@@ -13637,8 +13819,34 @@ var init_work = __esm({
13637
13819
  });
13638
13820
  return;
13639
13821
  }
13640
- const status = e.outcome === "completed" && hasOutput(w2, state.artifactsOf(e.workId).length) ? "success" : "failed";
13641
- state.updateWork(e.workId, { endedAt: ctx.at, outcome: e.outcome, outcomeDetail: e.detail, status });
13822
+ const businessFailed = w2.businessHandoffStatus === "not_delivered";
13823
+ const status = e.outcome === "completed" && hasOutput(w2, state.artifactsOf(e.workId).length) && !businessFailed ? "success" : "failed";
13824
+ state.updateWork(e.workId, {
13825
+ endedAt: ctx.at,
13826
+ outcome: businessFailed ? "failed" : e.outcome,
13827
+ outcomeDetail: businessFailed ? { ...e.detail ?? {}, reason: "agent business handoff not delivered" } : e.detail,
13828
+ status
13829
+ });
13830
+ }
13831
+ };
13832
+ workHandoffRecorded = {
13833
+ name: "work/handoff-recorded",
13834
+ kind: "work.handoff_recorded",
13835
+ apply(e, state, ctx) {
13836
+ const w2 = state.work(e.workId);
13837
+ if (!w2 || e.producedBy !== "agent") return;
13838
+ if (w2.agentHandoffAttemptId && w2.agentHandoffAttemptId !== e.attemptId) return;
13839
+ state.updateWork(e.workId, {
13840
+ agentHandoffAttemptId: e.attemptId,
13841
+ businessHandoffStatus: e.status,
13842
+ businessHandoffRecordedAt: ctx.at,
13843
+ // handoff 可以在 work.response 后到达。明确未交付必须回到现有失败/重试路径。
13844
+ ...e.status === "not_delivered" && w2.endedAt && !w2.deadAt ? {
13845
+ status: "failed",
13846
+ outcome: "failed",
13847
+ outcomeDetail: { ...w2.outcomeDetail ?? {}, reason: "agent business handoff not delivered" }
13848
+ } : {}
13849
+ });
13642
13850
  }
13643
13851
  };
13644
13852
  workStart = {
@@ -13825,6 +14033,7 @@ var init_review = __esm({
13825
14033
  init_views2();
13826
14034
  init_kill();
13827
14035
  init_issue();
14036
+ init_activate();
13828
14037
  reviewCreate = {
13829
14038
  name: "review/create",
13830
14039
  kind: "review.create",
@@ -13963,6 +14172,7 @@ var init_review = __esm({
13963
14172
  if (!w2 || w2.deadAt || w2.acceptanceState === "accepted") return;
13964
14173
  const node = state.node(w2.nodeId);
13965
14174
  if (!node) return;
14175
+ if (!businessHandoffBarrierSatisfied(state, w2.nodeId, w2.id)) return;
13966
14176
  if (w2.replyToIssueId) {
13967
14177
  const issue2 = state.issue(w2.replyToIssueId);
13968
14178
  state.updateWork(e.workId, {
@@ -14089,6 +14299,7 @@ var init_handlers = __esm({
14089
14299
  erase(workCreate),
14090
14300
  erase(workStart),
14091
14301
  erase(workSubmitOutput),
14302
+ erase(workHandoffRecorded),
14092
14303
  erase(workResponse),
14093
14304
  erase(workTimeout),
14094
14305
  erase(workKill),
@@ -14499,13 +14710,9 @@ var init_bus = __esm({
14499
14710
  // 这里按 store 找它 cancel。找不到 / 不是刚被 kill / 会话已退 → no-op。这是设计「节点重新运行 = 自动
14500
14711
  // kill 旧任务」的会话侧落地,否则改版重跑时旧 agent 会空转到墙钟超时、和 bypassScheduling 组合出双会话。
14501
14712
  cancelPreviousWork: async (nodeId, currentWorkId) => {
14502
- try {
14503
- const snap = await this.store.transaction((tx) => tx.loadWorkorder(record8.workorderId));
14504
- const prev = snap?.works.filter((w2) => w2.nodeId === nodeId && w2.id !== currentWorkId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt))[0];
14505
- if (prev && prev.deadAt) await this.io.cancelSession(prev.id);
14506
- } catch (err) {
14507
- console.warn(`[bus] cancelPreviousWork ${nodeId}: ${String(err)}`);
14508
- }
14713
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(record8.workorderId));
14714
+ const prev = snap?.works.filter((w2) => w2.nodeId === nodeId && w2.id !== currentWorkId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt))[0];
14715
+ if (prev && prev.deadAt) await this.io.cancelSession(prev.id);
14509
14716
  },
14510
14717
  // ★ 设计 §workorder.sealed/paused「按 work.kill 逻辑杀」:killAllWorks 只置状态,会话取消在这里做——
14511
14718
  // apply 已把被杀 work/review 置 deadAt/cancelledAt,这里按 store 找它们仍挂着的会话 cancel
@@ -14757,6 +14964,7 @@ function nodeToArtifact(node, edges, requirements) {
14757
14964
  if (cleanFields) {
14758
14965
  if ("reviewRoles" in cleanFields) delete cleanFields.reviewRoles;
14759
14966
  if ("docType" in cleanFields) delete cleanFields.docType;
14967
+ if (BUSINESS_HANDOFF_POLICY_FIELD in cleanFields) delete cleanFields[BUSINESS_HANDOFF_POLICY_FIELD];
14760
14968
  }
14761
14969
  return {
14762
14970
  id: node.id,
@@ -16779,7 +16987,11 @@ var init_kernel_bridge = __esm({
16779
16987
  const spawnTypeDef = this.schema.get(op.type);
16780
16988
  const spawnOwner = op.owner ?? (spawnTypeDef?.ownerRole ? this.actorForRole(spawnTypeDef.ownerRole) : _actor);
16781
16989
  const opFields = op.fields;
16782
- const mergedFields = opReviewRoles !== void 0 ? { ...opFields ?? {}, reviewRoles: opReviewRoles } : opFields;
16990
+ const mergedFields = opReviewRoles !== void 0 || op.businessHandoffPolicy !== void 0 ? {
16991
+ ...opFields ?? {},
16992
+ ...opReviewRoles !== void 0 ? { reviewRoles: opReviewRoles } : {},
16993
+ ...op.businessHandoffPolicy !== void 0 ? { [BUSINESS_HANDOFF_POLICY_FIELD]: op.businessHandoffPolicy } : {}
16994
+ } : opFields;
16783
16995
  await this.commit({
16784
16996
  companyId: "",
16785
16997
  workorderId: wid,
@@ -38635,7 +38847,7 @@ var require_websocket = __commonJS({
38635
38847
  var http2 = require("http");
38636
38848
  var net = require("net");
38637
38849
  var tls = require("tls");
38638
- var { randomBytes: randomBytes6, createHash: createHash15 } = require("crypto");
38850
+ var { randomBytes: randomBytes6, createHash: createHash16 } = require("crypto");
38639
38851
  var { Duplex, Readable } = require("stream");
38640
38852
  var { URL: URL2 } = require("url");
38641
38853
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -39303,7 +39515,7 @@ var require_websocket = __commonJS({
39303
39515
  abortHandshake(websocket, socket, "Invalid Upgrade header");
39304
39516
  return;
39305
39517
  }
39306
- const digest = createHash15("sha1").update(key + GUID).digest("base64");
39518
+ const digest = createHash16("sha1").update(key + GUID).digest("base64");
39307
39519
  if (res.headers["sec-websocket-accept"] !== digest) {
39308
39520
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
39309
39521
  return;
@@ -39672,7 +39884,7 @@ var require_websocket_server = __commonJS({
39672
39884
  var EventEmitter = require("events");
39673
39885
  var http2 = require("http");
39674
39886
  var { Duplex } = require("stream");
39675
- var { createHash: createHash15 } = require("crypto");
39887
+ var { createHash: createHash16 } = require("crypto");
39676
39888
  var extension2 = require_extension();
39677
39889
  var PerMessageDeflate2 = require_permessage_deflate();
39678
39890
  var subprotocol2 = require_subprotocol();
@@ -39979,7 +40191,7 @@ var require_websocket_server = __commonJS({
39979
40191
  );
39980
40192
  }
39981
40193
  if (this._state > RUNNING) return abortHandshake(socket, 503);
39982
- const digest = createHash15("sha1").update(key + GUID).digest("base64");
40194
+ const digest = createHash16("sha1").update(key + GUID).digest("base64");
39983
40195
  const headers = [
39984
40196
  "HTTP/1.1 101 Switching Protocols",
39985
40197
  "Upgrade: websocket",
@@ -147081,7 +147293,8 @@ function withClosureReport(plan, opts) {
147081
147293
  title: CLOSURE_REPORT_TITLE,
147082
147294
  description: closureReportTaskBook(),
147083
147295
  inputs: business.map((op) => ({ to: op.id, required: true })),
147084
- isFinalOutput: true
147296
+ isFinalOutput: true,
147297
+ businessHandoffPolicy: BUSINESS_HANDOFF_POLICY_EXEMPT
147085
147298
  }
147086
147299
  ]
147087
147300
  };
@@ -147091,6 +147304,7 @@ var init_closure_report = __esm({
147091
147304
  "../server/src/domains/collab/closure-report.ts"() {
147092
147305
  "use strict";
147093
147306
  init_src();
147307
+ init_src2();
147094
147308
  init_src3();
147095
147309
  init_src3();
147096
147310
  CLOSURE_SPAWN = (op) => op.action === "spawn" && op.type === DELIVERY_SUMMARY_ARTIFACT_TYPE;
@@ -163072,7 +163286,8 @@ async function startOasisServer(opts) {
163072
163286
  { nodeId: currentRuntimeId, runtimeKind: currentRuntimeKind }
163073
163287
  );
163074
163288
  const persistedUserMessage = persistTarget ? stripInjectedChatContext(body.message) : body.message;
163075
- const history = runtimeChanged && chatStore && persistTarget ? (await chatStore.listMessages(persistTarget.id, 20)).filter((message) => message.role !== "system" && !(message.role === "assistant" && message.status === "running")).map((message) => ({
163289
+ const needsHistoryInjection = Boolean(persistTarget && (runtimeChanged || !persistTarget.runtimeSessionId));
163290
+ const history = needsHistoryInjection && chatStore && persistTarget ? (await chatStore.listMessages(persistTarget.id, 20)).filter((message) => message.role !== "system" && !(message.role === "assistant" && message.status === "running")).map((message) => ({
163076
163291
  role: message.role,
163077
163292
  content: message.role === "user" ? stripInjectedChatContext(message.content) : message.content
163078
163293
  })) : [];
@@ -164761,8 +164976,12 @@ function mergeContinuityRefs(...lists) {
164761
164976
  }
164762
164977
  return [...refs.values()];
164763
164978
  }
164764
- function systemHandoffFor(run) {
164979
+ function systemHandoffFor(run, checkpoint = null, effects = []) {
164765
164980
  const outcome = handoffOutcomeOf(run.status);
164981
+ const terminalReason = run.errorMessage ?? run.exitReason ?? `status=${outcome}`;
164982
+ const pendingEffects = effects.filter((effect) => effect.status === "pending").length;
164983
+ const failedEffects = effects.filter((effect) => effect.status === "failed").length;
164984
+ const progress = checkpoint ? `checkpoint=${checkpoint.checkpointId} completed=${checkpoint.completedStepIds.length} pending=${checkpoint.pendingSteps.length} recoverability=${checkpoint.recoverability}` : "no valid pre-terminal checkpoint";
164766
164985
  return {
164767
164986
  handoffId: `handoff:${run.id}`,
164768
164987
  attemptId: run.id,
@@ -164770,12 +164989,16 @@ function systemHandoffFor(run) {
164770
164989
  part: run.part,
164771
164990
  producedBy: "system_reconstructed",
164772
164991
  outcome,
164773
- summary: outcome === "succeeded" ? `Execution completed with ${run.outputRefs.length} durable output reference(s).` : outcome === "no-output" ? "Execution ended cleanly without a durable output." : `Execution ended with status ${outcome}.`,
164992
+ summary: `[system_reconstructed] attempt=${run.id} outcome=${outcome} reason=${terminalReason}; ${progress}`.slice(0, 400),
164774
164993
  changedOutputs: structuredClone(run.outputRefs),
164775
164994
  unresolvedIssues: [],
164776
- risks: run.errorMessage ? [run.errorMessage.slice(0, 400)] : [],
164777
- nextActions: [],
164778
- verificationGaps: [],
164995
+ risks: [
164996
+ ...run.errorMessage ? [run.errorMessage.slice(0, 400)] : [],
164997
+ ...pendingEffects || failedEffects ? [`external effects: pending=${pendingEffects}, failed=${failedEffects}`] : []
164998
+ ],
164999
+ nextActions: structuredClone(checkpoint?.pendingSteps ?? []),
165000
+ verificationGaps: checkpoint ? [] : ["No valid pre-terminal checkpoint was available."],
165001
+ userHandoff: null,
164779
165002
  createdAt: run.endedAt ?? run.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
164780
165003
  };
164781
165004
  }
@@ -164877,11 +165100,9 @@ var init_memory_trace_store = __esm({
164877
165100
  if (merged.nodeId && isTerminalStatus(merged.status)) {
164878
165101
  this.ensureTerminalCheckpointFor(merged);
164879
165102
  const existingHandoff = this.handoffs.get(id);
164880
- const handoff = existingHandoff ? {
164881
- ...existingHandoff,
164882
- outcome: handoffOutcomeOf(merged.status),
164883
- changedOutputs: structuredClone(existingHandoff.producedBy === "agent" ? existingHandoff.changedOutputs : merged.outputRefs)
164884
- } : systemHandoffFor(merged);
165103
+ const checkpoint = (this.checkpoints.get(id) ?? []).filter((item) => item.trigger !== "attempt_terminal").at(-1) ?? null;
165104
+ const effects = [...this.externalEffects.values()].filter((effect) => effect.attemptId === id);
165105
+ const handoff = existingHandoff?.producedBy === "agent" ? existingHandoff : systemHandoffFor(merged, checkpoint, effects);
164885
165106
  this.handoffs.set(id, handoff);
164886
165107
  merged.handoffId = handoff.handoffId;
164887
165108
  }
@@ -165074,6 +165295,46 @@ var init_memory_trace_store = __esm({
165074
165295
  async listExternalEffects(nodeId) {
165075
165296
  return [...this.externalEffects.values()].filter((effect) => effect.nodeId === nodeId).sort((a, b2) => (a.occurredAt ?? "").localeCompare(b2.occurredAt ?? "") || a.idempotencyKey.localeCompare(b2.idempotencyKey)).map((effect) => structuredClone(effect));
165076
165297
  }
165298
+ sortedExternalEffects(nodeId, status) {
165299
+ const recency = (effect) => {
165300
+ if (effect.occurredAt) return effect.occurredAt;
165301
+ const run = this.runs.get(effect.attemptId);
165302
+ return run?.endedAt ?? run?.updatedAt ?? run?.startedAt ?? "";
165303
+ };
165304
+ return [...this.externalEffects.values()].filter((effect) => effect.nodeId === nodeId && (status === void 0 || effect.status === status)).sort((a, b2) => recency(b2).localeCompare(recency(a)) || b2.idempotencyKey.localeCompare(a.idempotencyKey)).map((effect) => structuredClone(effect));
165305
+ }
165306
+ async getExternalEffectsForResume(nodeId, jobKey, part, succeededLimit, failedLimit) {
165307
+ const inScope = (effect) => {
165308
+ const run = this.runs.get(effect.attemptId);
165309
+ return Boolean(run && executionJobKey(run) === jobKey && run.part === part);
165310
+ };
165311
+ const pending = this.sortedExternalEffects(nodeId, "pending").filter(inScope);
165312
+ const succeeded = this.sortedExternalEffects(nodeId, "succeeded").filter(inScope);
165313
+ const failed = this.sortedExternalEffects(nodeId, "failed").filter(inScope);
165314
+ const includedSucceeded = succeeded.slice(0, succeededLimit);
165315
+ const includedFailed = failed.slice(0, failedLimit);
165316
+ return {
165317
+ effects: [...pending, ...includedSucceeded, ...includedFailed],
165318
+ summary: {
165319
+ total: pending.length + succeeded.length + failed.length,
165320
+ totalByStatus: { pending: pending.length, succeeded: succeeded.length, failed: failed.length },
165321
+ includedByStatus: {
165322
+ pending: pending.length,
165323
+ succeeded: includedSucceeded.length,
165324
+ failed: includedFailed.length
165325
+ }
165326
+ }
165327
+ };
165328
+ }
165329
+ async queryExternalEffects(nodeId, options) {
165330
+ const effects = this.sortedExternalEffects(nodeId, options.status);
165331
+ return {
165332
+ total: effects.length,
165333
+ offset: options.offset,
165334
+ limit: options.limit,
165335
+ items: effects.slice(options.offset, options.offset + options.limit)
165336
+ };
165337
+ }
165077
165338
  async putHandoff(input) {
165078
165339
  const run = this.runs.get(input.attemptId);
165079
165340
  if (!run?.nodeId) throw new ExecutionAttemptNotFoundError(input.attemptId);
@@ -165095,6 +165356,11 @@ var init_memory_trace_store = __esm({
165095
165356
  risks: structuredClone(input.risks ?? []),
165096
165357
  nextActions: structuredClone(input.nextActions ?? []),
165097
165358
  verificationGaps: structuredClone(input.verificationGaps ?? []),
165359
+ userHandoff: normalizeUserHandoff(
165360
+ input.outcome,
165361
+ input.changedOutputs ?? run.outputRefs,
165362
+ input.userHandoff
165363
+ ),
165098
165364
  createdAt: input.createdAt ?? this.now()
165099
165365
  };
165100
165366
  this.handoffs.set(input.attemptId, handoff);
@@ -165145,13 +165411,17 @@ var init_memory_trace_store = __esm({
165145
165411
  this.handoffs.set(handoff.attemptId, structuredClone({
165146
165412
  ...handoff,
165147
165413
  jobKey: handoff.jobKey ?? (run ? executionJobKey(run) : `attempt:${handoff.attemptId}`),
165148
- part: handoff.part ?? run?.part ?? null
165414
+ part: handoff.part ?? run?.part ?? null,
165415
+ userHandoff: handoff.userHandoff ?? null
165149
165416
  }));
165150
165417
  }
165151
165418
  for (const [id, run] of this.runs) {
165152
165419
  if (!run.nodeId || !isTerminalStatus(run.status)) continue;
165153
165420
  this.ensureTerminalCheckpointFor(run);
165154
- const handoff = this.handoffs.get(id) ?? systemHandoffFor(run);
165421
+ const existingHandoff = this.handoffs.get(id);
165422
+ const checkpoint = (this.checkpoints.get(id) ?? []).filter((item) => item.trigger !== "attempt_terminal").at(-1) ?? null;
165423
+ const effects = [...this.externalEffects.values()].filter((effect) => effect.attemptId === id);
165424
+ const handoff = existingHandoff?.producedBy === "agent" ? existingHandoff : systemHandoffFor(run, checkpoint, effects);
165155
165425
  this.handoffs.set(id, handoff);
165156
165426
  this.runs.set(id, { ...run, handoffId: handoff.handoffId });
165157
165427
  }
@@ -165936,6 +166206,13 @@ var init_test_engine = __esm({
165936
166206
  }
165937
166207
  });
165938
166208
 
166209
+ // ../testkit/src/engine-handoff.ts
166210
+ var init_engine_handoff = __esm({
166211
+ "../testkit/src/engine-handoff.ts"() {
166212
+ "use strict";
166213
+ }
166214
+ });
166215
+
165939
166216
  // ../testkit/src/index.ts
165940
166217
  var init_src7 = __esm({
165941
166218
  "../testkit/src/index.ts"() {
@@ -165952,6 +166229,7 @@ var init_src7 = __esm({
165952
166229
  init_memory_control_plane_store();
165953
166230
  init_memory_actor_memory_store();
165954
166231
  init_test_engine();
166232
+ init_engine_handoff();
165955
166233
  }
165956
166234
  });
165957
166235
 
@@ -170212,7 +170490,9 @@ var init_sink = __esm({
170212
170490
  // 「这条 run 跑在哪个 cwd」才有据可查,Session 列也不再对 dispatch run 恒空。
170213
170491
  baseMetadata: {
170214
170492
  bundleManifest: session.bundleManifest,
170215
- ...session.runtimeSessionId ? { runtimeSessionId: session.runtimeSessionId } : {}
170493
+ ...session.runtimeSessionId ? { runtimeSessionId: session.runtimeSessionId } : {},
170494
+ ...session.engineWorkId ? { engineWorkId: session.engineWorkId } : {},
170495
+ ...session.engineCompanyId ? { engineCompanyId: session.engineCompanyId } : {}
170216
170496
  },
170217
170497
  observedModel: null,
170218
170498
  checkpointState: {},
@@ -170427,7 +170707,21 @@ var init_sink = __esm({
170427
170707
  stream: "runtime",
170428
170708
  message: `exit code=${update.exit.code ?? "killed"} ops=${update.opRefs.length}`
170429
170709
  }]);
170430
- const status = update.exit.wallclockKilled ? "timeout" : update.exit.reason === "cancelled" ? "cancelled" : update.opRefs.length > 0 ? "succeeded" : update.exit.reason === "clean" ? "no-output" : "failed";
170710
+ let terminalDurableRefs = [];
170711
+ let checkpointPrevious = null;
170712
+ if (this.continuityMode !== "off" && this.store.appendCheckpoint && this.store.listCheckpoints) {
170713
+ checkpointPrevious = await this.latestCheckpoint(st);
170714
+ const terminalInput = this.checkpointInput(st, checkpointPrevious, "attempt_terminal", update.exit.at);
170715
+ terminalDurableRefs = terminalInput.durableRefs;
170716
+ }
170717
+ const opOutputRefs = update.opRefs.map((ref2) => ({
170718
+ kind: "op",
170719
+ id: ref2.id,
170720
+ version: String(ref2.seq),
170721
+ label: ref2.kind
170722
+ }));
170723
+ const hasDurableOutput = update.opRefs.length > 0 || terminalDurableRefs.length > 0;
170724
+ const status = update.exit.wallclockKilled ? "timeout" : update.exit.reason === "timeout" ? "timeout" : update.exit.reason === "cancelled" ? "cancelled" : update.exit.reason === "output-limit" && hasDurableOutput ? "partial" : update.exit.reason && update.exit.reason !== "clean" || update.exit.code !== null && update.exit.code !== 0 ? "failed" : hasDurableOutput ? "succeeded" : update.exit.reason === "clean" ? "no-output" : "failed";
170431
170725
  const isErr = status === "failed" || status === "timeout";
170432
170726
  const errorFields = isErr ? { errorCode: update.exit.reason ?? "error", errorMessage: formatExitError(update.exit) } : {};
170433
170727
  const model = typeof update.exit.model === "string" && update.exit.model.trim() ? update.exit.model.trim() : st.observedModel;
@@ -170435,17 +170729,16 @@ var init_sink = __esm({
170435
170729
  if (exitRtId) st.baseMetadata = { ...st.baseMetadata, runtimeSessionId: exitRtId };
170436
170730
  const exitWorkdir = typeof update.exit.workdirRef === "string" && update.exit.workdirRef.trim() ? update.exit.workdirRef.trim() : void 0;
170437
170731
  if (exitWorkdir) st.baseMetadata = { ...st.baseMetadata, workdirRef: exitWorkdir };
170438
- const outputRefs = update.opRefs.map((ref2) => ({
170439
- kind: "op",
170440
- id: ref2.id,
170441
- version: String(ref2.seq),
170442
- label: ref2.kind
170443
- }));
170732
+ const outputRefs = opOutputRefs.slice();
170733
+ for (const ref2 of terminalDurableRefs) {
170734
+ if (!outputRefs.some((item) => item.kind === ref2.kind && item.id === ref2.id && item.version === ref2.version)) {
170735
+ outputRefs.push(ref2);
170736
+ }
170737
+ }
170444
170738
  if (this.continuityMode !== "off" && this.store.appendCheckpoint && this.store.listCheckpoints) {
170445
- const previous = await this.latestCheckpoint(st);
170446
- const terminalInput = this.checkpointInput(st, previous, "attempt_terminal", update.exit.at);
170739
+ const terminalInput = this.checkpointInput(st, checkpointPrevious, "attempt_terminal", update.exit.at);
170447
170740
  const durableRefs = [...terminalInput.durableRefs];
170448
- for (const ref2 of outputRefs) {
170741
+ for (const ref2 of opOutputRefs) {
170449
170742
  if (!durableRefs.some((item) => item.kind === ref2.kind && item.id === ref2.id && item.version === ref2.version)) {
170450
170743
  durableRefs.push(ref2);
170451
170744
  }
@@ -170455,7 +170748,7 @@ var init_sink = __esm({
170455
170748
  pendingSteps: terminalInput.pendingSteps,
170456
170749
  completedStepIds: terminalInput.completedStepIds,
170457
170750
  durableRefs,
170458
- recoverability: outputRefs.length > 0 ? "full" : terminalInput.recoverability,
170751
+ recoverability: hasDurableOutput ? "full" : terminalInput.recoverability,
170459
170752
  trigger: "attempt_terminal",
170460
170753
  createdAt: update.exit.at
170461
170754
  });
@@ -170465,7 +170758,10 @@ var init_sink = __esm({
170465
170758
  endedAt: update.exit.at,
170466
170759
  durationMs: update.exit.durationMs,
170467
170760
  exitCode: update.exit.code,
170468
- ...this.continuityMode !== "off" ? { outputRefs } : {},
170761
+ // Attempt terminal => NodeHandoff is unconditional, including the `off` cohort. Keep the
170762
+ // durable output facts on the Attempt in every cohort so storage can reconstruct an honest
170763
+ // system handoff after an abnormal exit without depending on the continuity experiment.
170764
+ outputRefs,
170469
170765
  // 退出事件带回的真实 runtime 会话 id 属血统,不随实验开关丢弃(ADR-0131)。
170470
170766
  ...exitRtId ? { runtimeSessionId: exitRtId } : {},
170471
170767
  ...errorFields,
@@ -180602,6 +180898,11 @@ function isRefArray(value) {
180602
180898
  return typeof ref2["kind"] === "string" && typeof ref2["id"] === "string" && (ref2["version"] === void 0 || typeof ref2["version"] === "string") && (ref2["label"] === void 0 || typeof ref2["label"] === "string");
180603
180899
  });
180604
180900
  }
180901
+ function isUserHandoff(value) {
180902
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
180903
+ const handoff = value;
180904
+ return (handoff["status"] === "delivered" || handoff["status"] === "not_delivered") && typeof handoff["summary"] === "string" && handoff["summary"].trim().length > 0 && typeof handoff["nextStep"] === "string" && isRefArray(handoff["deliverables"]);
180905
+ }
180605
180906
  function resolveAttemptWriter(req, requestedAttemptId) {
180606
180907
  const dispatchId = req.auth.dispatch?.dispatchId;
180607
180908
  if (requestedAttemptId === "current") {
@@ -180621,6 +180922,11 @@ function executionContinuityDomain(service) {
180621
180922
  if (!view) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5DE5\u5355\uFF1A${req.params.id}`);
180622
180923
  return { status: 200, body: view };
180623
180924
  });
180925
+ router.get("/api/execution-continuity/attempts/:id/recovery", async (req) => {
180926
+ const view = await service.recoveryView(req.params.id, req.auth.companyId);
180927
+ if (!view) throw new ApiError(404, "NOT_FOUND", `\u8BE5 Attempt \u6CA1\u6709\u5DF2\u6CE8\u5165\u7684\u91CD\u6D3E ResumePack\uFF1A${req.params.id}`);
180928
+ return { status: 200, body: view };
180929
+ });
180624
180930
  router.get("/api/execution-continuity/workorders/:id/nodes/:nodeId/resume-pack", async (req) => {
180625
180931
  const pack = await service.resumePack(
180626
180932
  req.params.id,
@@ -180631,6 +180937,25 @@ function executionContinuityDomain(service) {
180631
180937
  if (!pack) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8282\u70B9\uFF1A${req.params.nodeId}`);
180632
180938
  return { status: 200, body: pack };
180633
180939
  });
180940
+ router.get("/api/execution-continuity/workorders/:id/nodes/:nodeId/external-effects", async (req) => {
180941
+ const statusRaw = req.query.get("status");
180942
+ if (statusRaw !== null && !EXTERNAL_EFFECT_STATUSES.includes(statusRaw)) {
180943
+ throw new ApiError(400, "BAD_REQUEST", "status \u5FC5\u987B\u662F pending / succeeded / failed");
180944
+ }
180945
+ const limit = Number(req.query.get("limit") ?? "50");
180946
+ const offset = Number(req.query.get("offset") ?? "0");
180947
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200 || !Number.isInteger(offset) || offset < 0) {
180948
+ throw new ApiError(400, "BAD_REQUEST", "limit \u5FC5\u987B\u662F 1..200\uFF0Coffset \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
180949
+ }
180950
+ const page = await service.externalEffectHistory(
180951
+ req.params.id,
180952
+ req.params.nodeId,
180953
+ req.auth.companyId,
180954
+ { ...statusRaw ? { status: statusRaw } : {}, limit, offset }
180955
+ );
180956
+ if (!page) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8282\u70B9\uFF1A${req.params.nodeId}`);
180957
+ return { status: 200, body: page };
180958
+ });
180634
180959
  router.post("/api/execution-continuity/attempts/:id/checkpoints", async (req) => {
180635
180960
  const attemptId = resolveAttemptWriter(req, req.params.id);
180636
180961
  const b2 = req.body ?? {};
@@ -180705,6 +181030,9 @@ function executionContinuityDomain(service) {
180705
181030
  if (b2.changedOutputs !== void 0 && !isRefArray(b2.changedOutputs) || b2.unresolvedIssues !== void 0 && !isStringArray(b2.unresolvedIssues) || b2.risks !== void 0 && !isStringArray(b2.risks) || b2.nextActions !== void 0 && !isStringArray(b2.nextActions) || b2.verificationGaps !== void 0 && !isStringArray(b2.verificationGaps)) {
180706
181031
  throw new ApiError(400, "BAD_REQUEST", "handoff arrays contain invalid values");
180707
181032
  }
181033
+ if (!isUserHandoff(b2.userHandoff)) {
181034
+ throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11\u5408\u6CD5\u7684 userHandoff\uFF1BAgent \u5FC5\u987B\u63D0\u4EA4\u672C\u6B21\u5DE5\u4F5C\u7684\u4E1A\u52A1\u4EA4\u63A5\u6458\u8981");
181035
+ }
180708
181036
  const handoff = await service.handoff({
180709
181037
  attemptId,
180710
181038
  producedBy: "agent",
@@ -180714,13 +181042,14 @@ function executionContinuityDomain(service) {
180714
181042
  unresolvedIssues: Array.isArray(b2.unresolvedIssues) ? b2.unresolvedIssues : [],
180715
181043
  risks: Array.isArray(b2.risks) ? b2.risks : [],
180716
181044
  nextActions: Array.isArray(b2.nextActions) ? b2.nextActions : [],
180717
- verificationGaps: Array.isArray(b2.verificationGaps) ? b2.verificationGaps : []
181045
+ verificationGaps: Array.isArray(b2.verificationGaps) ? b2.verificationGaps : [],
181046
+ userHandoff: b2.userHandoff
180718
181047
  });
180719
181048
  return { status: 200, body: handoff };
180720
181049
  });
180721
181050
  };
180722
181051
  }
180723
- var HANDOFF_OUTCOMES, AGENT_CHECKPOINT_TRIGGERS;
181052
+ var HANDOFF_OUTCOMES, AGENT_CHECKPOINT_TRIGGERS, EXTERNAL_EFFECT_STATUSES;
180724
181053
  var init_routes8 = __esm({
180725
181054
  "../server/src/domains/execution-continuity/routes.ts"() {
180726
181055
  "use strict";
@@ -180734,6 +181063,7 @@ var init_routes8 = __esm({
180734
181063
  "orphaned"
180735
181064
  ];
180736
181065
  AGENT_CHECKPOINT_TRIGGERS = ["agent", "plan_completed", "stage_completed"];
181066
+ EXTERNAL_EFFECT_STATUSES = ["pending", "succeeded", "failed"];
180737
181067
  }
180738
181068
  });
180739
181069
 
@@ -180746,15 +181076,8 @@ function renderResumePack(pack) {
180746
181076
  `Node: ${pack.node.title} (${pack.node.nodeId})`,
180747
181077
  `Job: ${pack.jobKey ?? "unspecified"}`,
180748
181078
  `Part: ${pack.part ?? "whole-node"}`,
180749
- pack.node.description ? `Goal: ${pack.node.description}` : "",
180750
- "",
180751
- "## Upstream handoffs"
181079
+ pack.node.description ? `Goal: ${pack.node.description}` : ""
180752
181080
  ].filter(Boolean);
180753
- if (pack.upstream.length === 0) lines.push("- None");
180754
- for (const item of pack.upstream) {
180755
- lines.push(`- ${item.node.title}: ${item.handoff?.summary || "No terminal handoff yet"}`);
180756
- for (const issue2 of item.handoff?.unresolvedIssues ?? []) lines.push(` - unresolved: ${issue2}`);
180757
- }
180758
181081
  lines.push("", "## Previous attempt");
180759
181082
  if (!pack.previousAttempt) lines.push("- This is the first attempt.");
180760
181083
  else {
@@ -180766,14 +181089,40 @@ function renderResumePack(pack) {
180766
181089
  }
180767
181090
  }
180768
181091
  lines.push("", "## External effects");
180769
- if (pack.externalEffects.length === 0) lines.push("- None recorded");
181092
+ const effectSummary = pack.externalEffectSummary;
181093
+ if (effectSummary.total === 0) lines.push("- None recorded");
181094
+ else {
181095
+ const totals = effectSummary.totalByStatus;
181096
+ const included = effectSummary.includedByStatus;
181097
+ lines.push(
181098
+ `- ${effectSummary.total} total; showing all ${included.pending} pending, latest ${included.succeeded} of ${totals.succeeded} succeeded, and latest ${included.failed} of ${totals.failed} retryable failed. Older records remain available through the history query.`
181099
+ );
181100
+ }
180770
181101
  for (const effect of pack.externalEffects) {
180771
181102
  const action = effect.status === "succeeded" ? "do not repeat" : effect.status === "pending" ? "execution uncertain; reconcile before retry" : "failed; retry allowed";
180772
181103
  lines.push(`- [${effect.status}/${effect.source}] ${effect.kind} \u2192 ${effect.target} (${action}; key ${effect.idempotencyKey})`);
180773
181104
  }
181105
+ const snapshot = Buffer.from(JSON.stringify(pack), "utf8").toString("base64url");
181106
+ lines.push("", `${RESUME_SNAPSHOT_PREFIX}${snapshot}${RESUME_SNAPSHOT_SUFFIX}`);
180774
181107
  return `${lines.join("\n")}
180775
181108
  `;
180776
181109
  }
181110
+ function parseResumePackSnapshot(content) {
181111
+ const start = content.lastIndexOf(RESUME_SNAPSHOT_PREFIX);
181112
+ if (start < 0) return null;
181113
+ const encodedStart = start + RESUME_SNAPSHOT_PREFIX.length;
181114
+ const end = content.indexOf(RESUME_SNAPSHOT_SUFFIX, encodedStart);
181115
+ if (end < 0) return null;
181116
+ try {
181117
+ const value = JSON.parse(Buffer.from(content.slice(encodedStart, end), "base64url").toString("utf8"));
181118
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
181119
+ const pack = value;
181120
+ if (pack.scenario !== "first-run" && pack.scenario !== "redispatch" || !pack.node || typeof pack.node.nodeId !== "string" || !Array.isArray(pack.node.dependsOn) || !Array.isArray(pack.upstream) || !Array.isArray(pack.externalEffects) || !pack.externalEffectSummary) return null;
181121
+ return pack;
181122
+ } catch {
181123
+ return null;
181124
+ }
181125
+ }
180777
181126
  function renderCoordinatorView(view) {
180778
181127
  const lines = [
180779
181128
  "# Coordinator View",
@@ -180793,6 +181142,22 @@ function renderCoordinatorView(view) {
180793
181142
  `- State: ${node.nodeState}; attempt status: ${node.status}; attempt: ${node.attemptNo || "none"}`,
180794
181143
  `- Actor: ${node.actorId ?? "unassigned"}; role: ${node.role ?? "unassigned"}`
180795
181144
  );
181145
+ if (node.recoverySource) {
181146
+ const source = node.recoverySource;
181147
+ lines.push(
181148
+ `- Recovery source: Attempt ${source.recoveredFromAttempt.attemptNo ?? "?"} \u2192 Attempt ${source.currentAttempt.attemptNo ?? "?"}`
181149
+ );
181150
+ if (source.recoveryCheckpoint) {
181151
+ lines.push(` - Recovery checkpoint: #${source.recoveryCheckpoint.seq} (${source.recoveryCheckpoint.recoverability})`);
181152
+ for (const step of source.recoveryCheckpoint.pendingSteps) lines.push(` - Recovery pending: ${step}`);
181153
+ }
181154
+ if (source.recoveryHandoff?.producedBy === "agent") {
181155
+ lines.push(` - Recovery Agent handoff: ${source.recoveryHandoff.summary}`);
181156
+ } else if (source.recoveryHandoff?.producedBy === "system_reconstructed") {
181157
+ lines.push(` - Recovery system reconstruction (not an Agent conclusion): ${source.recoveryHandoff.summary}`);
181158
+ }
181159
+ for (const risk of source.recoveryHandoff?.risks ?? []) lines.push(` - Recovery risk: ${risk}`);
181160
+ }
180796
181161
  if (node.blockedBy.length > 0) lines.push(`- Blocked by: ${node.blockedBy.join(", ")}`);
180797
181162
  if (node.progress) {
180798
181163
  lines.push(`- Latest checkpoint: ${node.progress.recoverability} at ${node.progress.lastCheckpointAt}`);
@@ -180847,12 +181212,42 @@ succeeds, use \`oasis continuity-effect\` with a stable key that the same node w
180847
181212
 
180848
181213
  ## Handoff
180849
181214
 
180850
- Before terminal exit, run \`oasis continuity-handoff\` with a concise summary (maximum 400
181215
+ Before terminal exit, run \`oasis continuity-handoff\` with a concise technical summary (maximum 400
180851
181216
  characters), changed durable outputs, unresolved issues, risks, next actions, and verification gaps.
180852
- The system reconstructs a minimal handoff if the process dies first; an agent-authored handoff is
180853
- more useful.
181217
+ You must also provide \`--user-summary\` as short business-facing language for the people receiving
181218
+ the work. Say what you completed, what result you formed, and what downstream work can use; if not
181219
+ delivered, say only why and what must happen next. Use \`--user-next-step\` when downstream needs an
181220
+ instruction. List only openable business results in \`--user-deliverables\`; keep technical refs in
181221
+ \`--outputs\`, and never list conclude, gap, checkpoint, or op as a user deliverable. Do not use a
181222
+ generic success/output-count template or mention attempts, checkpoints, runtimes, recovery,
181223
+ infrastructure paths, or internal diagnostics there.
181224
+ The system reconstructs an internal handoff if the process dies first, but reconstructed content is
181225
+ never shown as a business handoff.
181226
+ `;
181227
+ }
181228
+ function renderBusinessHandoffProtocol() {
181229
+ return `# Business handoff protocol
181230
+
181231
+ Before terminal exit, run \`oasis continuity-handoff\`. The NodeHandoff and its userHandoff are a
181232
+ required completion barrier for business downstream work. A successful handoff must use outcome
181233
+ \`succeeded\`, include a non-empty \`--user-summary\` saying what was done, what result was formed,
181234
+ and what downstream can use, and may have zero files. If the work cannot be delivered, submit a
181235
+ non-success outcome with a user summary explaining why delivery failed and \`--user-next-step\`;
181236
+ that records the handoff but does not release downstream work. List only openable business results
181237
+ in \`--user-deliverables\`. Never use a generic success/output-count template or expose attempts,
181238
+ checkpoints, runtimes, recovery, infrastructure paths, or internal diagnostics in userHandoff.
180854
181239
  `;
180855
181240
  }
181241
+ function emptyExternalEffectProjection() {
181242
+ return {
181243
+ effects: [],
181244
+ summary: {
181245
+ total: 0,
181246
+ totalByStatus: { pending: 0, succeeded: 0, failed: 0 },
181247
+ includedByStatus: { pending: 0, succeeded: 0, failed: 0 }
181248
+ }
181249
+ };
181250
+ }
180856
181251
  function latestAttemptByNode(attempts) {
180857
181252
  const out = /* @__PURE__ */ new Map();
180858
181253
  for (const attempt of attempts) {
@@ -180862,10 +181257,11 @@ function latestAttemptByNode(attempts) {
180862
181257
  }
180863
181258
  return out;
180864
181259
  }
180865
- function stateFromAttempt(attempt, handoff, checkpoint, now, stalledAfterMs) {
181260
+ function stateFromAttempt(attempt, handoff, checkpoint, now, stalledAfterMs, openToolCall) {
180866
181261
  if (attempt.status === "running" || attempt.status === "queued") {
180867
- const last = checkpoint?.createdAt ?? attempt.startedAt;
180868
- return now.getTime() - Date.parse(last) <= stalledAfterMs ? "running" : "stalled";
181262
+ const lastActivityAt = attempt.lastProgressAt ?? checkpoint?.createdAt ?? attempt.startedAt;
181263
+ if (openToolCall) return "running";
181264
+ return now.getTime() - Date.parse(lastActivityAt) <= stalledAfterMs ? "running" : "stalled";
180869
181265
  }
180870
181266
  if (attempt.status === "succeeded") return "succeeded";
180871
181267
  if (attempt.status === "no-output") return handoff?.unresolvedIssues.length ? "blocked" : "no_change";
@@ -180888,22 +181284,26 @@ function waitingDescendants(source, reverse, states) {
180888
181284
  return [...found].sort();
180889
181285
  }
180890
181286
  function todoAction(node) {
180891
- if (node.handoffProducer === "agent" && node.nextActions[0]) return `Agent Handoff \u5EFA\u8BAE\u5173\u6CE8\uFF1A${node.nextActions[0]}`;
180892
- if (node.nodeState === "ready") return "\u8BE5\u8282\u70B9\u5DF2 ready\uFF0C\u7B49\u5F85 Oasis \u539F\u6709\u8C03\u5EA6\u673A\u5236\u5904\u7406";
180893
- if (node.nodeState === "blocked" && node.unresolvedIssues[0]) return `\u8BE5\u8282\u70B9\u5DF2 blocked\uFF1B\u672A\u51B3\u95EE\u9898\uFF1A${node.unresolvedIssues[0]}`;
180894
- if (!node.progress) return `\u8BE5\u8282\u70B9\u5DF2 ${node.nodeState}\uFF0C\u6CA1\u6709\u53EF\u7528 checkpoint`;
180895
- if (node.progress.recoverability === "full") return `\u8BE5\u8282\u70B9\u5DF2 ${node.nodeState}\uFF0C\u5B58\u5728\u6700\u65B0\u5B8C\u6574 checkpoint\uFF0C\u53EF\u7531\u534F\u8C03\u8005\u5224\u65AD\u540E\u7EED\u5904\u7406`;
180896
- if (node.progress.recoverability === "partial") return `\u8BE5\u8282\u70B9\u5DF2 ${node.nodeState}\uFF0C\u5B58\u5728\u6700\u65B0\u90E8\u5206\u53EF\u6062\u590D checkpoint\uFF0C\u9700\u8981\u5173\u6CE8\u5DF2\u6709\u4EA7\u51FA\u590D\u6838`;
180897
- return `\u8BE5\u8282\u70B9\u5DF2 ${node.nodeState}\uFF0C\u6700\u65B0 checkpoint \u4E0D\u53EF\u6062\u590D\uFF0C\u9700\u8981\u534F\u8C03\u8005\u5173\u6CE8`;
180898
- }
180899
- var DONE_STATES, BLOCKING_STATES, HANDOFF_OUTCOMES2, TODO_RANK, ExecutionContinuityService;
181287
+ if (node.handoffProducer === "agent" && node.nextActions[0]) return `Agent Handoff: ${node.nextActions[0]}`;
181288
+ if (node.nodeState === "ready") return "\u8282\u70B9 ready\uFF0C\u7B49\u5F85\u8C03\u5EA6";
181289
+ if (node.nodeState === "blocked" && node.unresolvedIssues[0]) return `\u8282\u70B9 blocked\uFF1A${node.unresolvedIssues[0]}`;
181290
+ if (!node.progress) return `\u8282\u70B9 ${node.nodeState}\uFF0C\u65E0 checkpoint`;
181291
+ if (node.progress.recoverability === "full") return `\u8282\u70B9 ${node.nodeState}\uFF0Ccheckpoint \u5B8C\u6574\u53EF\u6062\u590D`;
181292
+ if (node.progress.recoverability === "partial") return `\u8282\u70B9 ${node.nodeState}\uFF0Ccheckpoint \u90E8\u5206\u53EF\u6062\u590D\uFF0C\u9700\u5173\u6CE8\u5DF2\u6709\u4EA7\u51FA`;
181293
+ return `\u8282\u70B9 ${node.nodeState}\uFF0Ccheckpoint \u4E0D\u53EF\u6062\u590D`;
181294
+ }
181295
+ var import_node_crypto32, DONE_STATES, BLOCKING_STATES, HANDOFF_OUTCOMES2, RESUME_EXTERNAL_EFFECT_LIMIT, RESUME_SNAPSHOT_PREFIX, RESUME_SNAPSHOT_SUFFIX, TODO_RANK, ExecutionContinuityService;
180900
181296
  var init_service7 = __esm({
180901
181297
  "../server/src/domains/execution-continuity/service.ts"() {
180902
181298
  "use strict";
180903
181299
  init_src();
181300
+ import_node_crypto32 = require("node:crypto");
180904
181301
  DONE_STATES = /* @__PURE__ */ new Set(["succeeded", "no_change"]);
180905
181302
  BLOCKING_STATES = /* @__PURE__ */ new Set(["stalled", "failed", "blocked"]);
180906
181303
  HANDOFF_OUTCOMES2 = /* @__PURE__ */ new Set(["succeeded", "no-output", "failed", "cancelled", "timeout", "orphaned"]);
181304
+ RESUME_EXTERNAL_EFFECT_LIMIT = 20;
181305
+ RESUME_SNAPSHOT_PREFIX = "<!-- oasis-resume-pack-snapshot:v1:";
181306
+ RESUME_SNAPSHOT_SUFFIX = " -->";
180907
181307
  TODO_RANK = {
180908
181308
  stalled: 0,
180909
181309
  failed: 1,
@@ -180918,6 +181318,8 @@ var init_service7 = __esm({
180918
181318
  stalledAfterMs;
180919
181319
  attemptVisibilityWaitMs;
180920
181320
  onCheckpointState;
181321
+ readBundleFile;
181322
+ onAgentHandoffPersisted;
180921
181323
  constructor(opts) {
180922
181324
  this.store = opts.store;
180923
181325
  this.mode = opts.mode;
@@ -180926,6 +181328,8 @@ var init_service7 = __esm({
180926
181328
  this.stalledAfterMs = opts.stalledAfterMs ?? 25 * 6e4;
180927
181329
  this.attemptVisibilityWaitMs = opts.attemptVisibilityWaitMs ?? 500;
180928
181330
  this.onCheckpointState = opts.onCheckpointState;
181331
+ this.readBundleFile = opts.readBundleFile;
181332
+ this.onAgentHandoffPersisted = opts.onAgentHandoffPersisted;
180929
181333
  }
180930
181334
  assertWritable() {
180931
181335
  if (this.mode === "off") throw new Error("execution continuity is disabled");
@@ -181001,12 +181405,22 @@ var init_service7 = __esm({
181001
181405
  });
181002
181406
  }
181003
181407
  async handoff(input) {
181004
- this.assertWritable();
181005
181408
  if (!HANDOFF_OUTCOMES2.has(input.outcome)) throw new Error(`invalid handoff outcome: ${input.outcome}`);
181006
181409
  if (!input.summary.trim()) throw new Error("handoff summary is required");
181007
181410
  if ([...input.summary].length > 400) throw new Error("handoff summary exceeds 400 characters");
181008
- await this.awaitAttempt(input.attemptId);
181009
- return this.afterAttemptVisible(() => this.store.putHandoff(input));
181411
+ if (input.producedBy === "agent" && !input.userHandoff) {
181412
+ throw new Error("agent-authored handoff requires userHandoff");
181413
+ }
181414
+ const userHandoff = input.userHandoff ? normalizeUserHandoff(input.outcome, input.changedOutputs ?? [], input.userHandoff) : void 0;
181415
+ const attempt = await this.awaitAttempt(input.attemptId);
181416
+ const handoff = await this.afterAttemptVisible(() => this.store.putHandoff({
181417
+ ...input,
181418
+ ...userHandoff ? { userHandoff } : {}
181419
+ }));
181420
+ if (handoff.producedBy === "agent" && handoff.userHandoff) {
181421
+ await this.onAgentHandoffPersisted?.(attempt, handoff);
181422
+ }
181423
+ return handoff;
181010
181424
  }
181011
181425
  async afterAttemptVisible(write) {
181012
181426
  const deadline = Date.now() + this.attemptVisibilityWaitMs;
@@ -181028,38 +181442,86 @@ var init_service7 = __esm({
181028
181442
  const jobKey = scope.jobKey ?? currentAttempt?.jobKey ?? null;
181029
181443
  const part = scope.part !== void 0 ? scope.part : currentAttempt?.part ?? null;
181030
181444
  const attempts = allAttempts.filter((attempt) => attempt.id !== scope.excludeAttemptId);
181031
- const latest = latestAttemptByNode(attempts);
181032
- const previousAttempt = attempts.filter((attempt) => attempt.nodeId === nodeId).filter((attempt) => jobKey === null || attempt.jobKey === jobKey && attempt.part === part).sort((a, b2) => (b2.attemptNo ?? 0) - (a.attemptNo ?? 0))[0] ?? null;
181033
- const [previousHandoff, latestCheckpoint, externalEffects] = await Promise.all([
181034
- previousAttempt ? this.store.getHandoff(previousAttempt.id) : Promise.resolve(null),
181035
- previousAttempt ? this.store.getLatestCheckpoint(previousAttempt.id) : Promise.resolve(null),
181036
- this.store.listExternalEffects(nodeId)
181445
+ const previousAttempt = attempts.filter((attempt) => attempt.nodeId === nodeId).filter((attempt) => !currentAttempt || (attempt.attemptNo ?? 0) < (currentAttempt.attemptNo ?? 0)).filter((attempt) => jobKey === null || attempt.jobKey === jobKey && attempt.part === part).sort((a, b2) => (b2.attemptNo ?? 0) - (a.attemptNo ?? 0))[0] ?? null;
181446
+ if (!previousAttempt) return null;
181447
+ const [previousHandoff, latestCheckpoint, externalEffectProjection] = await Promise.all([
181448
+ this.store.getHandoff(previousAttempt.id),
181449
+ this.store.getLatestCheckpoint(previousAttempt.id),
181450
+ jobKey === null ? Promise.resolve(emptyExternalEffectProjection()) : this.store.getExternalEffectsForResume(
181451
+ nodeId,
181452
+ jobKey,
181453
+ part,
181454
+ RESUME_EXTERNAL_EFFECT_LIMIT,
181455
+ RESUME_EXTERNAL_EFFECT_LIMIT
181456
+ )
181037
181457
  ]);
181038
- const upstream = await Promise.all(node.dependsOn.map(async (upstreamId) => {
181039
- const upstreamNode = nodes.find((item) => item.nodeId === upstreamId) ?? {
181040
- nodeId: upstreamId,
181041
- title: upstreamId,
181042
- dependsOn: []
181043
- };
181044
- const attempt = latest.get(upstreamId) ?? null;
181045
- return {
181046
- node: upstreamNode,
181047
- attempt,
181048
- handoff: attempt ? await this.store.getHandoff(attempt.id) : null
181049
- };
181050
- }));
181051
181458
  return {
181052
- scenario: previousAttempt ? "redispatch" : "first-run",
181459
+ scenario: "redispatch",
181053
181460
  jobKey,
181054
181461
  part,
181055
181462
  node,
181056
- upstream,
181463
+ upstream: [],
181057
181464
  previousAttempt,
181058
181465
  previousHandoff,
181059
181466
  latestCheckpoint,
181060
- externalEffects
181467
+ externalEffects: externalEffectProjection.effects,
181468
+ externalEffectSummary: externalEffectProjection.summary
181061
181469
  };
181062
181470
  }
181471
+ async recoveryView(attemptId, companyId) {
181472
+ const attempt = await this.store.getAttempt(attemptId);
181473
+ if (!attempt?.workOrderId || !attempt.nodeId || attempt.continuityMode !== "resume") return null;
181474
+ const recoverySnapshot = this.readRecoverySnapshot(attempt);
181475
+ if (!recoverySnapshot) return null;
181476
+ const graph = await this.resolveGraph(attempt.workOrderId, companyId);
181477
+ if (!graph.some((node) => node.nodeId === attempt.nodeId)) return null;
181478
+ return {
181479
+ workOrderId: attempt.workOrderId,
181480
+ node: recoverySnapshot.node,
181481
+ attempt,
181482
+ recoverySnapshot
181483
+ };
181484
+ }
181485
+ /**
181486
+ * Resolves a Chat Session's durable WorkOrder links to the newest current node Attempt that has
181487
+ * a verified historical recovery input. This deliberately does not inspect Chat run ids: Chat
181488
+ * runs and Dispatcher Attempts are separate identities.
181489
+ */
181490
+ async latestRecoveryAttemptForWorkOrders(workOrderIds, companyId) {
181491
+ const candidates = [];
181492
+ for (const workOrderId of [...new Set(workOrderIds)]) {
181493
+ const graph = await this.resolveGraph(workOrderId, companyId);
181494
+ if (graph.length === 0) continue;
181495
+ const nodeIds = new Set(graph.map((node) => node.nodeId));
181496
+ const latest = latestAttemptByNode(await this.store.listAttempts(workOrderId));
181497
+ for (const attempt of latest.values()) {
181498
+ if (!attempt.nodeId || !nodeIds.has(attempt.nodeId) || !this.readRecoverySnapshot(attempt)) continue;
181499
+ candidates.push({ workOrderId, nodeId: attempt.nodeId, attemptId: attempt.id, startedAt: attempt.startedAt });
181500
+ }
181501
+ }
181502
+ candidates.sort((a, b2) => b2.startedAt.localeCompare(a.startedAt) || b2.attemptId.localeCompare(a.attemptId));
181503
+ const candidate = candidates[0];
181504
+ return candidate ? { workOrderId: candidate.workOrderId, nodeId: candidate.nodeId, attemptId: candidate.attemptId } : null;
181505
+ }
181506
+ readRecoverySnapshot(attempt) {
181507
+ if (!attempt.nodeId || attempt.continuityMode !== "resume") return null;
181508
+ const metadata = attempt.metadata;
181509
+ const manifest = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata["bundleManifest"] : null;
181510
+ const expectedHash = manifest && typeof manifest === "object" && !Array.isArray(manifest) ? manifest["execution/RESUME.md"] : null;
181511
+ if (typeof expectedHash !== "string" || !this.readBundleFile) return null;
181512
+ const injectedResume = this.readBundleFile(attempt.id, "execution/RESUME.md");
181513
+ if (!injectedResume) return null;
181514
+ const actualHash = (0, import_node_crypto32.createHash)("sha256").update(injectedResume).digest("hex");
181515
+ if (actualHash !== expectedHash) return null;
181516
+ const recoverySnapshot = parseResumePackSnapshot(injectedResume);
181517
+ if (!recoverySnapshot || recoverySnapshot.scenario !== "redispatch" || !recoverySnapshot.previousAttempt || recoverySnapshot.node.nodeId !== attempt.nodeId || recoverySnapshot.jobKey !== attempt.jobKey || recoverySnapshot.part !== attempt.part) return null;
181518
+ return recoverySnapshot;
181519
+ }
181520
+ async externalEffectHistory(workOrderId, nodeId, companyId, options) {
181521
+ const nodes = await this.resolveGraph(workOrderId, companyId);
181522
+ if (!nodes.some((node) => node.nodeId === nodeId)) return null;
181523
+ return this.store.queryExternalEffects(nodeId, options);
181524
+ }
181063
181525
  async coordinatorView(workOrderId, companyId) {
181064
181526
  const graph = await this.resolveGraph(workOrderId, companyId);
181065
181527
  if (graph.length === 0) return null;
@@ -181067,6 +181529,7 @@ var init_service7 = __esm({
181067
181529
  const latest = latestAttemptByNode(attempts);
181068
181530
  const handoffs = /* @__PURE__ */ new Map();
181069
181531
  const checkpoints = /* @__PURE__ */ new Map();
181532
+ const recoverySnapshots = /* @__PURE__ */ new Map();
181070
181533
  await Promise.all([...latest.values()].map(async (attempt) => {
181071
181534
  const [handoff, points] = await Promise.all([
181072
181535
  this.store.getHandoff(attempt.id),
@@ -181074,6 +181537,7 @@ var init_service7 = __esm({
181074
181537
  ]);
181075
181538
  handoffs.set(attempt.id, handoff);
181076
181539
  checkpoints.set(attempt.id, points);
181540
+ recoverySnapshots.set(attempt.id, this.readRecoverySnapshot(attempt));
181077
181541
  }));
181078
181542
  const stateByNode = /* @__PURE__ */ new Map();
181079
181543
  for (const node of graph) {
@@ -181088,11 +181552,21 @@ var init_service7 = __esm({
181088
181552
  const attempt = latest.get(node.nodeId) ?? null;
181089
181553
  const handoff = attempt ? handoffs.get(attempt.id) ?? null : null;
181090
181554
  const checkpoint = attempt ? checkpoints.get(attempt.id) ?? null : null;
181555
+ const recoverySnapshot = attempt ? recoverySnapshots.get(attempt.id) ?? null : null;
181556
+ const recoverySource = attempt && recoverySnapshot?.previousAttempt ? {
181557
+ currentAttempt: attempt,
181558
+ recoveredFromAttempt: recoverySnapshot.previousAttempt,
181559
+ recoveryCheckpoint: recoverySnapshot.latestCheckpoint,
181560
+ recoveryHandoff: recoverySnapshot.previousHandoff
181561
+ } : null;
181091
181562
  const nodeState = stateByNode.get(node.nodeId);
181563
+ const wasRecovered = recoverySource !== null;
181564
+ const executionState = wasRecovered && nodeState === "failed" ? "recovery_failed" : BLOCKING_STATES.has(nodeState) ? "awaiting_coordinator" : wasRecovered ? "recovered" : nodeState === "running" ? "running" : null;
181092
181565
  const includeProgress = checkpoint && ["running", "stalled", "blocked", "failed"].includes(nodeState);
181093
181566
  return {
181094
181567
  nodeId: node.nodeId,
181095
181568
  title: node.title,
181569
+ attemptId: attempt?.id ?? null,
181096
181570
  nodeState,
181097
181571
  status: attempt?.status ?? "not_started",
181098
181572
  actorId: attempt?.actorId ?? null,
@@ -181105,6 +181579,10 @@ var init_service7 = __esm({
181105
181579
  risks: handoff?.risks ?? [],
181106
181580
  verificationGaps: handoff?.verificationGaps ?? [],
181107
181581
  changedOutputs: handoff?.changedOutputs ?? [],
181582
+ executionState,
181583
+ latestCheckpoint: checkpoint,
181584
+ handoff,
181585
+ recoverySource,
181108
181586
  ...includeProgress ? { progress: {
181109
181587
  pendingSteps: checkpoint.pendingSteps,
181110
181588
  recoverability: checkpoint.recoverability,
@@ -181126,9 +181604,132 @@ var init_service7 = __esm({
181126
181604
  "blocked",
181127
181605
  "failed"
181128
181606
  ].map((state) => [state, nodes.filter((node) => node.nodeState === state).length]));
181129
- return { workOrderId, mode: this.mode, progress, nodes, dependencyBlocks, todo: todos };
181130
- }
181131
- };
181607
+ const collaborationStatus = nodes.length > 0 && nodes.every((node) => DONE_STATES.has(node.nodeState)) ? "completed" : nodes.some((node) => BLOCKING_STATES.has(node.nodeState)) ? "needs_coordination" : "in_progress";
181608
+ const handoffSummary = [...nodes].sort((a, b2) => Number(BLOCKING_STATES.has(b2.nodeState)) - Number(BLOCKING_STATES.has(a.nodeState))).map((node) => node.summary.trim()).find(Boolean) ?? "";
181609
+ return { workOrderId, mode: this.mode, collaborationStatus, handoffSummary, progress, nodes, dependencyBlocks, todo: todos };
181610
+ }
181611
+ };
181612
+ }
181613
+ });
181614
+
181615
+ // ../server/src/domains/execution-continuity/handoff-work-bridge.ts
181616
+ function createAgentHandoffWorkBridge(opts) {
181617
+ return async (attempt, handoff) => {
181618
+ const metadata = attempt.metadata && typeof attempt.metadata === "object" && !Array.isArray(attempt.metadata) ? attempt.metadata : {};
181619
+ const workId = typeof metadata["engineWorkId"] === "string" ? metadata["engineWorkId"] : null;
181620
+ const companyId = typeof metadata["engineCompanyId"] === "string" ? metadata["engineCompanyId"] : opts.getDefaultCompanyId();
181621
+ if (!workId || !attempt.workOrderId || !handoff.userHandoff) {
181622
+ throw new Error(`agent handoff ${attempt.id} is missing engine Work lineage`);
181623
+ }
181624
+ const companyEngine = await opts.resolveEngine(companyId);
181625
+ const engineBus = companyEngine.kernel.getBus?.();
181626
+ if (!engineBus) throw new Error(`engine bus unavailable for ${companyId}`);
181627
+ await engineBus.submitAndProcess({
181628
+ companyId,
181629
+ workorderId: attempt.workOrderId,
181630
+ actorId: attempt.actorId,
181631
+ event: {
181632
+ kind: "work.handoff_recorded",
181633
+ workorderId: attempt.workOrderId,
181634
+ workId,
181635
+ attemptId: attempt.id,
181636
+ producedBy: "agent",
181637
+ status: handoff.userHandoff.status
181638
+ },
181639
+ dedupKey: `agent-handoff:${attempt.id}:${handoff.userHandoff.status}`
181640
+ });
181641
+ };
181642
+ }
181643
+ var init_handoff_work_bridge = __esm({
181644
+ "../server/src/domains/execution-continuity/handoff-work-bridge.ts"() {
181645
+ "use strict";
181646
+ }
181647
+ });
181648
+
181649
+ // ../server/src/domains/execution-continuity/upstream-handoff-input.ts
181650
+ function renderRefs(refs) {
181651
+ return refs.map(
181652
+ (ref2) => `- ${ref2.kind}:${ref2.id}${ref2.version ? `@${ref2.version}` : ""}${ref2.label ? ` \u2014 ${ref2.label}` : ""}`
181653
+ );
181654
+ }
181655
+ function renderList(title, values) {
181656
+ return [
181657
+ `## ${title}`,
181658
+ ...values.length > 0 ? values.map((value) => `- ${value}`) : ["- None"],
181659
+ ""
181660
+ ];
181661
+ }
181662
+ function renderUpstreamNodeHandoff(handoff, source) {
181663
+ if (handoff.producedBy !== "agent") {
181664
+ throw new Error(`upstream business handoff ${handoff.handoffId} must be Agent-authored`);
181665
+ }
181666
+ return [
181667
+ "# Upstream NodeHandoff",
181668
+ "",
181669
+ `Upstream node: ${source.upstreamNodeId}`,
181670
+ `Pinned work: ${source.pinnedWorkId}`,
181671
+ `Attempt: ${handoff.attemptId}`,
181672
+ `Handoff: ${handoff.handoffId}`,
181673
+ `Job: ${handoff.jobKey}`,
181674
+ `Part: ${handoff.part ?? "whole-node"}`,
181675
+ `Outcome: ${handoff.outcome}`,
181676
+ "Produced by: agent",
181677
+ `Created at: ${handoff.createdAt}`,
181678
+ "",
181679
+ "## Summary",
181680
+ handoff.summary,
181681
+ "",
181682
+ "## Changed durable outputs",
181683
+ ...handoff.changedOutputs.length > 0 ? renderRefs(handoff.changedOutputs) : ["- None"],
181684
+ "",
181685
+ ...renderList("Unresolved issues", handoff.unresolvedIssues),
181686
+ ...renderList("Risks", handoff.risks),
181687
+ ...renderList("Next actions", handoff.nextActions),
181688
+ ...renderList("Verification gaps", handoff.verificationGaps),
181689
+ "This handoff explains the upstream Agent's work. Verify referenced outputs against the pinned files in this directory and their authoritative durable systems.",
181690
+ ""
181691
+ ].join("\n");
181692
+ }
181693
+ function createUpstreamHandoffInputResolver(opts) {
181694
+ return async (input) => {
181695
+ const snapshot = await opts.loadWorkorder(input.workOrderId);
181696
+ if (!snapshot) throw new Error(`workorder not found while assembling upstream handoff: ${input.workOrderId}`);
181697
+ const edge = snapshot.edges.find(
181698
+ (candidate) => candidate.fromNodeId === input.upstreamNodeId && candidate.toNodeId === input.downstreamNodeId
181699
+ );
181700
+ const downstream = snapshot.nodes.find((node) => node.id === input.downstreamNodeId);
181701
+ const businessEdge = edge?.kind === "data" && edge.required && downstream?.fields?.[BUSINESS_HANDOFF_POLICY_FIELD] !== BUSINESS_HANDOFF_POLICY_EXEMPT;
181702
+ if (!businessEdge) return null;
181703
+ if (edge.pinnedWorkId !== input.pinnedWorkId) {
181704
+ throw new Error(
181705
+ `upstream handoff pin drift for ${input.upstreamNodeId}\u2192${input.downstreamNodeId}: bundle=${input.pinnedWorkId}, edge=${edge.pinnedWorkId ?? "none"}`
181706
+ );
181707
+ }
181708
+ const work = snapshot.works.find(
181709
+ (candidate) => candidate.id === input.pinnedWorkId && candidate.nodeId === input.upstreamNodeId
181710
+ );
181711
+ if (!work) throw new Error(`pinned upstream Work not found: ${input.pinnedWorkId}`);
181712
+ if (typeof work.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) {
181713
+ return null;
181714
+ }
181715
+ if (!work.agentHandoffAttemptId || work.businessHandoffStatus !== "delivered") {
181716
+ throw new Error(`pinned upstream Work ${work.id} has no delivered Agent handoff projection`);
181717
+ }
181718
+ const handoff = await opts.getHandoff(work.agentHandoffAttemptId);
181719
+ if (!handoff) throw new Error(`Agent NodeHandoff not found for Attempt ${work.agentHandoffAttemptId}`);
181720
+ if (handoff.attemptId !== work.agentHandoffAttemptId || handoff.producedBy !== "agent" || handoff.outcome !== "succeeded" || handoff.userHandoff?.status !== "delivered") {
181721
+ throw new Error(`invalid Agent NodeHandoff for pinned Work ${work.id}`);
181722
+ }
181723
+ return renderUpstreamNodeHandoff(handoff, {
181724
+ upstreamNodeId: input.upstreamNodeId,
181725
+ pinnedWorkId: input.pinnedWorkId
181726
+ });
181727
+ };
181728
+ }
181729
+ var init_upstream_handoff_input = __esm({
181730
+ "../server/src/domains/execution-continuity/upstream-handoff-input.ts"() {
181731
+ "use strict";
181732
+ init_src2();
181132
181733
  }
181133
181734
  });
181134
181735
 
@@ -181144,6 +181745,8 @@ var init_execution_continuity2 = __esm({
181144
181745
  init_service7();
181145
181746
  init_service7();
181146
181747
  init_routes8();
181748
+ init_handoff_work_bridge();
181749
+ init_upstream_handoff_input();
181147
181750
  }
181148
181751
  });
181149
181752
 
@@ -181257,7 +181860,7 @@ function createChatSessionsDomain(opts) {
181257
181860
  if (!text.trim()) throw new ApiError(400, "BAD_REQUEST", "text required");
181258
181861
  const now = Date.now();
181259
181862
  sweepDrafts(now);
181260
- const id = `cd_${(0, import_node_crypto32.randomUUID)()}`;
181863
+ const id = `cd_${(0, import_node_crypto33.randomUUID)()}`;
181261
181864
  drafts.set(id, { text, actor: req.auth.actor, expiresAt: now + DRAFT_TTL_MS });
181262
181865
  return { status: 201, body: { id, expiresInMs: DRAFT_TTL_MS } };
181263
181866
  });
@@ -181290,7 +181893,7 @@ function createChatSessionsDomain(opts) {
181290
181893
  const runtimeId = await currentRuntimeId(body.aiActorId) ?? "unknown";
181291
181894
  const now = (/* @__PURE__ */ new Date()).toISOString();
181292
181895
  const session = {
181293
- id: (0, import_node_crypto32.randomUUID)(),
181896
+ id: (0, import_node_crypto33.randomUUID)(),
181294
181897
  humanActorId: req.auth.actor,
181295
181898
  aiActorId: body.aiActorId,
181296
181899
  runtimeId,
@@ -181363,7 +181966,7 @@ function createChatSessionsDomain(opts) {
181363
181966
  if (body.role !== "user" && body.role !== "assistant") throw new ApiError(400, "BAD_REQUEST", "role must be user or assistant");
181364
181967
  const role = body.role;
181365
181968
  const msg = await store.appendMessage({
181366
- id: (0, import_node_crypto32.randomUUID)(),
181969
+ id: (0, import_node_crypto33.randomUUID)(),
181367
181970
  sessionId: req.params.id,
181368
181971
  role,
181369
181972
  content: role === "user" ? stripInjectedChatContext(body.content) : body.content,
@@ -181400,6 +182003,18 @@ function createChatSessionsDomain(opts) {
181400
182003
  const items = allSummaries.filter((s2) => workOrderIds.includes(s2.id));
181401
182004
  return { status: 200, body: { items } };
181402
182005
  });
182006
+ router.get("/api/chat-sessions/:id/execution-recovery-attempt", async (req) => {
182007
+ const session = await store.getSession(req.params.id);
182008
+ if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
182009
+ const workOrderIds = await store.listWorkOrdersForSession(req.params.id);
182010
+ const candidate = workOrderIds.length && opts.resolveExecutionRecoveryAttempt ? await opts.resolveExecutionRecoveryAttempt(workOrderIds, req.auth.companyId) : null;
182011
+ return {
182012
+ status: 200,
182013
+ body: {
182014
+ attempt: candidate ? { chatSessionId: session.id, ...candidate } : null
182015
+ }
182016
+ };
182017
+ });
181403
182018
  async function locateWorkdir(sessionId, actor) {
181404
182019
  const session = await store.getSession(sessionId);
181405
182020
  if (!session || session.humanActorId !== actor) throw new ApiError(404, "NOT_FOUND", "session not found");
@@ -181456,11 +182071,11 @@ function createChatSessionsDomain(opts) {
181456
182071
  });
181457
182072
  };
181458
182073
  }
181459
- var import_node_crypto32, WORKDIR_READ_MAX_BYTES;
182074
+ var import_node_crypto33, WORKDIR_READ_MAX_BYTES;
181460
182075
  var init_chat_sessions = __esm({
181461
182076
  "../server/src/domains/chat-sessions/index.ts"() {
181462
182077
  "use strict";
181463
- import_node_crypto32 = require("node:crypto");
182078
+ import_node_crypto33 = require("node:crypto");
181464
182079
  init_chat_session();
181465
182080
  init_router();
181466
182081
  init_workorders();
@@ -183197,7 +183812,7 @@ function playbooksDomain(opts) {
183197
183812
  opts.overrides.set(companyOf(req), ref2, nodeKey, nextOverride);
183198
183813
  try {
183199
183814
  await opts.audit?.({
183200
- id: `reg_${(0, import_node_crypto33.randomUUID)()}`,
183815
+ id: `reg_${(0, import_node_crypto34.randomUUID)()}`,
183201
183816
  actor: req.auth.actor,
183202
183817
  kind: "registry_change",
183203
183818
  target: `playbook:${ref2}#${nodeKey}`,
@@ -183214,11 +183829,11 @@ function playbooksDomain(opts) {
183214
183829
  });
183215
183830
  };
183216
183831
  }
183217
- var import_node_crypto33;
183832
+ var import_node_crypto34;
183218
183833
  var init_routes10 = __esm({
183219
183834
  "../server/src/domains/playbooks/routes.ts"() {
183220
183835
  "use strict";
183221
- import_node_crypto33 = require("node:crypto");
183836
+ import_node_crypto34 = require("node:crypto");
183222
183837
  init_router();
183223
183838
  init_registry3();
183224
183839
  init_planner();
@@ -183351,11 +183966,11 @@ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
183351
183966
  entry.appendWaiters.push({ resolve: resolve9, timer });
183352
183967
  });
183353
183968
  }
183354
- var import_node_crypto34, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
183969
+ var import_node_crypto35, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
183355
183970
  var init_daemon_adapter = __esm({
183356
183971
  "../server/src/daemon-adapter.ts"() {
183357
183972
  "use strict";
183358
- import_node_crypto34 = require("node:crypto");
183973
+ import_node_crypto35 = require("node:crypto");
183359
183974
  STASH_TTL_MS = 5 * 6e4;
183360
183975
  STASH_MAX_FRAMES_PER_ID = 500;
183361
183976
  STASH_MAX_IDS = 50;
@@ -183370,7 +183985,7 @@ var init_daemon_adapter = __esm({
183370
183985
  this.stashEnabled = opts.stashUnknownFrames ?? false;
183371
183986
  hub.addMessageListener((_daemonId, msg) => this.onDaemonMessage(msg));
183372
183987
  hub.addDisconnectListener((daemonId) => this.onNodeDown(daemonId));
183373
- hub.addConnectListener((daemon) => this.onNodeUp(daemon.daemonId));
183988
+ hub.addConnectListener((daemon) => this.onNodeUp(daemon));
183374
183989
  }
183375
183990
  pending = /* @__PURE__ */ new Map();
183376
183991
  nodeLostGraceMs;
@@ -183456,48 +184071,73 @@ var init_daemon_adapter = __esm({
183456
184071
  }
183457
184072
  return byActor;
183458
184073
  }
183459
- /** 收到任何回帧(started/event/output/exited)即视为派发已达,取消 ack 超时。 */
183460
- cancelAck(dispatchId) {
184074
+ /** 派发送达/启动观测。供新 Engine session_started 之前保存 nodeId 并建立 fencing 路由。 */
184075
+ deliveryStatus(dispatchId) {
184076
+ const entry = this.pending.get(dispatchId);
184077
+ return entry ? { state: entry.deliveryState, nodeId: entry.nodeId } : void 0;
184078
+ }
184079
+ /** 收到 dispatch_received(或兼容旧节点的更强 started/event/output 证据)即取消送达超时。 */
184080
+ confirmDelivery(dispatchId, state) {
183461
184081
  const entry = this.pending.get(dispatchId);
183462
184082
  if (entry?.ackTimer) {
183463
184083
  clearTimeout(entry.ackTimer);
183464
184084
  entry.ackTimer = void 0;
183465
184085
  }
184086
+ if (entry) entry.deliveryState = state;
183466
184087
  }
183467
184088
  onNodeDown(daemonId) {
183468
184089
  if (this.reapTimers.has(daemonId)) return;
183469
184090
  if (![...this.pending.values()].some((e) => e.nodeId === daemonId && !e.exited)) return;
183470
184091
  const timer = setTimeout(() => {
183471
184092
  this.reapTimers.delete(daemonId);
183472
- this.reapNode(daemonId);
184093
+ this.markNodeUnknown(daemonId);
183473
184094
  }, this.nodeLostGraceMs);
183474
184095
  timer.unref?.();
183475
184096
  this.reapTimers.set(daemonId, timer);
183476
184097
  }
183477
- onNodeUp(daemonId) {
184098
+ onNodeUp(daemon) {
184099
+ const daemonId = daemon.daemonId;
183478
184100
  const timer = this.reapTimers.get(daemonId);
183479
184101
  if (timer) {
183480
184102
  clearTimeout(timer);
183481
184103
  this.reapTimers.delete(daemonId);
183482
184104
  }
183483
- }
183484
- /** 节点失联超宽限期:收割其全部在途会话——合成可重试 exit,走 dispatcher 既有退避/重派。 */
183485
- reapNode(daemonId) {
183486
- const doomed = [];
184105
+ if (!daemon.meta || daemon.meta.pendingDispatchIds === void 0) return;
184106
+ for (const dispatchId of daemon.meta.pendingDispatchIds) this.confirmDelivery(dispatchId, "received");
184107
+ for (const session of daemon.meta.activeSessions ?? []) this.confirmDelivery(session.dispatchId, "started");
184108
+ const present = /* @__PURE__ */ new Set([
184109
+ ...daemon.meta.pendingDispatchIds ?? [],
184110
+ ...(daemon.meta.activeSessions ?? []).map((s2) => s2.dispatchId)
184111
+ ]);
184112
+ const absent = [];
183487
184113
  for (const [dispatchId, entry] of this.pending) {
183488
- if (entry.nodeId === daemonId && !entry.exited) doomed.push(dispatchId);
184114
+ if (entry.nodeId === daemonId && !entry.exited && !present.has(dispatchId)) absent.push(dispatchId);
184115
+ }
184116
+ for (const dispatchId of absent) {
184117
+ this.log(`[dispatch-delivery] ${dispatchId}\uFF1A\u8282\u70B9 ${daemonId} \u91CD\u8FDE\u5E76\u660E\u786E\u786E\u8BA4 session/pending \u5747\u4E0D\u5B58\u5728 \u2192 terminal`);
184118
+ this.settle(dispatchId, {
184119
+ code: null,
184120
+ reason: "server-unreachable",
184121
+ errorMessage: `\u8282\u70B9 ${daemonId} \u91CD\u8FDE\u540E\u7684\u5B58\u5728\u6027\u5FEB\u7167\u786E\u8BA4\u539F dispatch \u4E0D\u5B58\u5728`
184122
+ });
183489
184123
  }
183490
- if (doomed.length === 0) return;
183491
- console.warn(
183492
- `[hub-adapter] \u8282\u70B9 ${daemonId} \u5931\u8054\u8D85 ${this.nodeLostGraceMs}ms\u2014\u2014\u6536\u5272\u5176 ${doomed.length} \u4E2A\u5728\u9014\u4F1A\u8BDD\uFF08\u5F52\u56E0 server-unreachable \u2192 \u9000\u907F/\u91CD\u6D3E\uFF09\uFF1A${doomed.join(", ")}`
183493
- );
183494
- for (const dispatchId of doomed) {
183495
- this.settle(dispatchId, { code: null, reason: "server-unreachable", errorMessage: `\u8282\u70B9 ${daemonId} \u5931\u8054\u8D85\u5BBD\u9650\u671F\uFF08${this.nodeLostGraceMs}ms\uFF09\uFF0C\u4F1A\u8BDD\u88AB\u6536\u5272\uFF08\u51B3\u7B56 0033\uFF09` });
184124
+ }
184125
+ /** 节点失联超宽限期:只报警,不把「暂时够不到」升级成「runtime 已死」。 */
184126
+ markNodeUnknown(daemonId) {
184127
+ const unknown2 = [];
184128
+ for (const [dispatchId, entry] of this.pending) {
184129
+ if (entry.nodeId === daemonId && !entry.exited) unknown2.push(dispatchId);
183496
184130
  }
184131
+ if (unknown2.length === 0) return;
184132
+ this.log(`[dispatch-delivery] \u8282\u70B9 ${daemonId} \u5931\u8054\u8D85 ${this.nodeLostGraceMs}ms\uFF1B${unknown2.length} \u4E2A\u6D3E\u53D1\u4FDD\u6301\u975E\u7EC8\u6001\uFF0C\u7B49\u5F85\u5B58\u5728\u6027\u786E\u8BA4\u6216 Work SLA\uFF1A${unknown2.join(", ")}`);
183497
184133
  }
183498
184134
  onDaemonMessage(msg) {
183499
- if (msg.type === "session_started") {
183500
- this.cancelAck(msg.dispatchId);
184135
+ if (msg.type === "dispatch_received") {
184136
+ this.confirmDelivery(msg.dispatchId, "received");
184137
+ const entry = this.pending.get(msg.dispatchId);
184138
+ if (entry) this.health?.recordReachable(entry.nodeId);
184139
+ } else if (msg.type === "session_started") {
184140
+ this.confirmDelivery(msg.dispatchId, "started");
183501
184141
  const entry = this.pending.get(msg.dispatchId);
183502
184142
  if (entry) this.health?.recordReachable(entry.nodeId);
183503
184143
  } else if (msg.type === "session_exited") {
@@ -183505,12 +184145,12 @@ var init_daemon_adapter = __esm({
183505
184145
  if (this.pending.has(msg.dispatchId)) this.settle(msg.dispatchId, info);
183506
184146
  else if (this.stashEnabled) this.stashFrame(msg.dispatchId, { type: "exit", info });
183507
184147
  } else if (msg.type === "session_event") {
183508
- this.cancelAck(msg.dispatchId);
184148
+ this.confirmDelivery(msg.dispatchId, "started");
183509
184149
  const entry = this.pending.get(msg.dispatchId);
183510
184150
  if (!entry && this.stashEnabled) this.stashFrame(msg.dispatchId, { type: "event", event: msg.event });
183511
184151
  for (const cb of entry?.telemetryCbs ?? []) cb(msg.event);
183512
184152
  } else if (msg.type === "session_output") {
183513
- this.cancelAck(msg.dispatchId);
184153
+ this.confirmDelivery(msg.dispatchId, "started");
183514
184154
  const entry = this.pending.get(msg.dispatchId);
183515
184155
  if (!entry && this.stashEnabled) this.stashFrame(msg.dispatchId, { type: "output", chunk: msg.chunk });
183516
184156
  for (const cb of entry?.outputCbs ?? []) cb(msg.chunk);
@@ -183526,7 +184166,7 @@ var init_daemon_adapter = __esm({
183526
184166
  async spawn(job) {
183527
184167
  const nodeId = job.binding?.nodeId;
183528
184168
  if (!nodeId) throw new Error("DaemonHubAdapter \u9700\u8981 job.binding.nodeId\uFF08\u8DEF\u7531\u9519\u8BEF\uFF09");
183529
- const dispatchId = job.dispatchId ?? `dispatch:${(0, import_node_crypto34.randomUUID)()}`;
184169
+ const dispatchId = job.dispatchId ?? `dispatch:${(0, import_node_crypto35.randomUUID)()}`;
183530
184170
  const entry = {
183531
184171
  nodeId,
183532
184172
  ...job.binding?.runtimeKind ? { runtimeKind: job.binding.runtimeKind } : {},
@@ -183535,6 +184175,7 @@ var init_daemon_adapter = __esm({
183535
184175
  telemetryCbs: [],
183536
184176
  outputCbs: [],
183537
184177
  exited: void 0,
184178
+ deliveryState: "awaiting_ack",
183538
184179
  ackTimer: void 0,
183539
184180
  appendWaiters: []
183540
184181
  };
@@ -183545,9 +184186,10 @@ var init_daemon_adapter = __esm({
183545
184186
  } else if (this.ackTimeoutMs > 0) {
183546
184187
  const timer = setTimeout(() => {
183547
184188
  const e = this.pending.get(dispatchId);
183548
- if (!e || e.exited) return;
183549
- this.log(`[dispatch-ack] ${dispatchId}\uFF08${job.artifactId}\uFF09\u6D3E\u53D1\u540E ${this.ackTimeoutMs}ms \u672A\u89C1\u56DE\u6267 \u2192 \u5224\u6D3E\u53D1\u4E22\u5931\uFF0C\u5408\u6210\u5931\u8D25\u91CD\u6D3E`);
183550
- this.settle(dispatchId, { code: null, reason: "server-unreachable", errorMessage: `\u6D3E\u53D1\u540E ${this.ackTimeoutMs}ms \u672A\u89C1\u4EFB\u4F55\u56DE\u6267\uFF08session_started/\u9000\u51FA\u5E27\uFF09\uFF0C\u5224\u6D3E\u53D1\u4E22\u5931\uFF08\u5E7D\u7075\u6D3E\u53D1\uFF0Cproposal \xA74.4\uFF09` });
184189
+ if (!e || e.exited || e.deliveryState !== "awaiting_ack") return;
184190
+ e.ackTimer = void 0;
184191
+ e.deliveryState = "delivery_unknown";
184192
+ this.log(`[dispatch-delivery] ${dispatchId}\uFF08${job.artifactId}\uFF09\u6D3E\u53D1\u540E ${this.ackTimeoutMs}ms \u672A\u89C1 dispatch_received \u2192 delivery_unknown\uFF08\u975E\u7EC8\u6001\uFF0C\u4E0D\u91CD\u6D3E\uFF09`);
183551
184193
  }, this.ackTimeoutMs);
183552
184194
  timer.unref?.();
183553
184195
  entry.ackTimer = timer;
@@ -183586,6 +184228,7 @@ var init_daemon_adapter = __esm({
183586
184228
  telemetryCbs: [],
183587
184229
  outputCbs: [],
183588
184230
  exited: stashed.exit,
184231
+ deliveryState: "started",
183589
184232
  ackTimer: void 0,
183590
184233
  appendWaiters: []
183591
184234
  };
@@ -183689,7 +184332,7 @@ var init_daemon_adapter = __esm({
183689
184332
  }));
183690
184333
  }
183691
184334
  request(nodeId, buildFrame) {
183692
- const requestId = (0, import_node_crypto34.randomUUID)();
184335
+ const requestId = (0, import_node_crypto35.randomUUID)();
183693
184336
  const sent = this.hub.dispatch(nodeId, buildFrame(requestId));
183694
184337
  if (!sent) return Promise.resolve({ ok: false, code: "NODE_OFFLINE" });
183695
184338
  return new Promise((resolve9) => {
@@ -183721,6 +184364,31 @@ var init_daemon_adapter = __esm({
183721
184364
  }
183722
184365
  });
183723
184366
 
184367
+ // ../server/src/dispatch-fence.ts
184368
+ function isExplicitlyDispatchFenced(run) {
184369
+ const fence = metadataObject(run.metadata).dispatchFence;
184370
+ return fence !== null && typeof fence === "object" && !Array.isArray(fence);
184371
+ }
184372
+ async function fenceDispatch(trace, dispatchId, reason, at = (/* @__PURE__ */ new Date()).toISOString()) {
184373
+ const run = await trace.getRun(dispatchId);
184374
+ if (!run) return false;
184375
+ if (isExplicitlyDispatchFenced(run)) return true;
184376
+ await trace.updateRun(dispatchId, {
184377
+ metadata: {
184378
+ ...metadataObject(run.metadata),
184379
+ dispatchFence: { at, reason }
184380
+ }
184381
+ });
184382
+ return true;
184383
+ }
184384
+ var metadataObject;
184385
+ var init_dispatch_fence = __esm({
184386
+ "../server/src/dispatch-fence.ts"() {
184387
+ "use strict";
184388
+ metadataObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
184389
+ }
184390
+ });
184391
+
183724
184392
  // ../server/src/side-map.ts
183725
184393
  var import_node_fs16, import_node_path21, FileSideMap;
183726
184394
  var init_side_map = __esm({
@@ -183772,13 +184440,13 @@ function classifyPage(page, lastPushedHash, serviceAccount) {
183772
184440
  if (sha(body) === lastPushedHash) return { kind: "echo" };
183773
184441
  return { kind: "human-edit", content: body, updatedBy: page.updatedBy };
183774
184442
  }
183775
- var import_node_crypto35, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
184443
+ var import_node_crypto36, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
183776
184444
  var init_engine = __esm({
183777
184445
  "../server/src/mirror/engine.ts"() {
183778
184446
  "use strict";
183779
- import_node_crypto35 = require("node:crypto");
184447
+ import_node_crypto36 = require("node:crypto");
183780
184448
  init_src();
183781
- sha = (s2) => (0, import_node_crypto35.createHash)("sha256").update(s2, "utf8").digest("hex");
184449
+ sha = (s2) => (0, import_node_crypto36.createHash)("sha256").update(s2, "utf8").digest("hex");
183782
184450
  enc3 = (s2) => new TextEncoder().encode(s2);
183783
184451
  dec = (b2) => new TextDecoder().decode(b2);
183784
184452
  MirrorEngine = class {
@@ -183977,11 +184645,11 @@ function humanExitCause(exit) {
183977
184645
  const base = (exit.reason ? label[exit.reason] : void 0) ?? `\u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F\uFF08${exit.reason ?? "\u65E0\u9000\u51FA\u4FE1\u606F"}\uFF09`;
183978
184646
  return exit.errorMessage ? `${base}\uFF1A${exit.errorMessage.slice(0, 200)}` : base;
183979
184647
  }
183980
- var import_node_crypto36, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_ESCALATION_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
184648
+ var import_node_crypto37, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_ESCALATION_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
183981
184649
  var init_worker = __esm({
183982
184650
  "../server/src/coordinator/worker.ts"() {
183983
184651
  "use strict";
183984
- import_node_crypto36 = require("node:crypto");
184652
+ import_node_crypto37 = require("node:crypto");
183985
184653
  init_src3();
183986
184654
  init_identity();
183987
184655
  AUTONOMOUS_ETHOS = `\u4F60\u662F\u8FD9\u4E2A\u5DE5\u5355\u7684**\u7BA1\u7406\u8005**\u2014\u2014\u804C\u8D23\u662F\u8BA9\u5B83\u987A\u7545\u8DD1\u5B8C\u3002\u7CFB\u7EDF\u5728\u67D0\u4E2A\u8282\u70B9\u5361\u4F4F\u3001\u673A\u68B0\u5206\u8BCA\u786E\u8BA4"\u9700\u8981\u4F60"\u65F6\u5524\u8D77\u4F60\uFF08\u65E0\u4EBA\u5728\u573A\uFF0C\u4F60\u8FD9\u4E00\u8F6E\u628A\u80FD\u505A\u7684\u505A\u6389\uFF09\u3002\u4F60\u7684\u624B\u6BB5\u5F88\u5BBD\uFF1A
@@ -184227,7 +184895,7 @@ ${HIGH_RISK_COMMANDS}`;
184227
184895
  return this.deps.kernel.model.lastSeq.get(artifactId) ?? 0;
184228
184896
  }
184229
184897
  evaluationKey(kind, artifactId, evidence) {
184230
- const fingerprint = (0, import_node_crypto36.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
184898
+ const fingerprint = (0, import_node_crypto37.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
184231
184899
  return `${kind}:${artifactId}:${fingerprint}`;
184232
184900
  }
184233
184901
  artifactEvidence(id) {
@@ -184601,7 +185269,7 @@ ${ctx.nodeFault}
184601
185269
  this.deps.log?.(`[coordinator] ${workspace} \u5168\u5C40\u4E0A\u4E0B\u6587\u8BFB\u53D6\u5931\u8D25\uFF0C\u7EE7\u7EED\u6CBF\u7528\u539F\u534F\u8C03\u8005\u4EFB\u52A1\uFF1A${String(error2)}`);
184602
185270
  }
184603
185271
  }
184604
- const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto36.randomUUID)();
185272
+ const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto37.randomUUID)();
184605
185273
  const job = {
184606
185274
  actor: actorId,
184607
185275
  actorToken: token,
@@ -184889,6 +185557,7 @@ var init_src8 = __esm({
184889
185557
  init_overrides();
184890
185558
  init_scheduler();
184891
185559
  init_daemon_adapter();
185560
+ init_dispatch_fence();
184892
185561
  init_node_health();
184893
185562
  init_side_map();
184894
185563
  init_engine();
@@ -184907,11 +185576,11 @@ var init_src8 = __esm({
184907
185576
  });
184908
185577
 
184909
185578
  // ../storage/src/postgres.ts
184910
- var import_node_crypto37, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
185579
+ var import_node_crypto38, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
184911
185580
  var init_postgres = __esm({
184912
185581
  "../storage/src/postgres.ts"() {
184913
185582
  "use strict";
184914
- import_node_crypto37 = require("node:crypto");
185583
+ import_node_crypto38 = require("node:crypto");
184915
185584
  init_esm();
184916
185585
  init_src();
184917
185586
  ident3 = (s2) => {
@@ -185090,7 +185759,7 @@ var init_postgres = __esm({
185090
185759
  return new _PostgresBlobStore(pool, schema);
185091
185760
  }
185092
185761
  async put(bytes) {
185093
- const hash = (0, import_node_crypto37.createHash)("sha256").update(bytes).digest("hex");
185762
+ const hash = (0, import_node_crypto38.createHash)("sha256").update(bytes).digest("hex");
185094
185763
  await this.pool.query(
185095
185764
  `INSERT INTO ${this.t} (hash, bytes, size, content_type) VALUES ($1, $2, $3, $4) ON CONFLICT (hash) DO NOTHING`,
185096
185765
  [hash, Buffer.from(bytes), bytes.byteLength, sniffContentType(bytes) ?? null]
@@ -187711,10 +188380,12 @@ var init_postgres_trace = __esm({
187711
188380
  risks jsonb NOT NULL DEFAULT '[]'::jsonb,
187712
188381
  next_actions jsonb NOT NULL DEFAULT '[]'::jsonb,
187713
188382
  verification_gaps jsonb NOT NULL DEFAULT '[]'::jsonb,
188383
+ user_handoff jsonb,
187714
188384
  created_at text NOT NULL
187715
188385
  )`);
187716
188386
  await pool.query(`ALTER TABLE "${s2}".node_handoffs ADD COLUMN IF NOT EXISTS job_key text NOT NULL DEFAULT ''`);
187717
188387
  await pool.query(`ALTER TABLE "${s2}".node_handoffs ADD COLUMN IF NOT EXISTS part text`);
188388
+ await pool.query(`ALTER TABLE "${s2}".node_handoffs ADD COLUMN IF NOT EXISTS user_handoff jsonb`);
187718
188389
  await pool.query(`
187719
188390
  UPDATE "${s2}".node_handoffs h
187720
188391
  SET job_key=COALESCE(NULLIF(h.job_key, ''), r.job_key, 'attempt:' || h.attempt_id), part=r.part
@@ -187775,7 +188446,19 @@ var init_postgres_trace = __esm({
187775
188446
  FOR EACH ROW EXECUTE FUNCTION "${s2}".ensure_terminal_execution_checkpoint()`);
187776
188447
  await pool.query(`
187777
188448
  CREATE OR REPLACE FUNCTION "${s2}".ensure_terminal_node_handoff() RETURNS trigger AS $$
187778
- DECLARE mapped_outcome text; generated_handoff_id text;
188449
+ DECLARE
188450
+ mapped_outcome text;
188451
+ generated_handoff_id text;
188452
+ terminal_reason text;
188453
+ latest_checkpoint_id text;
188454
+ latest_pending jsonb;
188455
+ latest_completed jsonb;
188456
+ latest_recoverability text;
188457
+ pending_effects integer;
188458
+ failed_effects integer;
188459
+ reconstructed_summary text;
188460
+ reconstructed_risks jsonb;
188461
+ reconstructed_gaps jsonb;
187779
188462
  BEGIN
187780
188463
  IF NEW.node_id IS NULL OR NEW.status IN ('queued', 'running') THEN RETURN NEW; END IF;
187781
188464
  mapped_outcome := CASE NEW.status
@@ -187788,24 +188471,82 @@ var init_postgres_trace = __esm({
187788
188471
  ELSE 'failed'
187789
188472
  END;
187790
188473
  generated_handoff_id := 'handoff:' || NEW.id;
188474
+ terminal_reason := COALESCE(NEW.error_message, NEW.exit_reason, 'status=' || mapped_outcome);
188475
+ -- attempt_terminal \u662F\u7CFB\u7EDF\u5728\u540C\u4E00\u7EC8\u6001\u4E8B\u52A1\u5185\u8865\u7684\u5FEB\u7167\uFF1B\u91CD\u5EFA\u4F9D\u636E\u53EA\u8BA4\u7EC8\u6001\u4E4B\u524D\u5DF2\u7ECF\u5B58\u5728\u7684\u6709\u6548 checkpoint\u3002
188476
+ SELECT checkpoint_id, pending_steps, completed_step_ids, recoverability
188477
+ INTO latest_checkpoint_id, latest_pending, latest_completed, latest_recoverability
188478
+ FROM "${s2}".execution_checkpoints
188479
+ WHERE attempt_id=NEW.id AND trigger <> 'attempt_terminal'
188480
+ ORDER BY seq DESC LIMIT 1;
188481
+ SELECT
188482
+ COUNT(*) FILTER (WHERE status='pending')::int,
188483
+ COUNT(*) FILTER (WHERE status='failed')::int
188484
+ INTO pending_effects, failed_effects
188485
+ FROM "${s2}".external_effects WHERE attempt_id=NEW.id;
188486
+ reconstructed_summary := left(
188487
+ '[system_reconstructed] attempt=' || NEW.id || ' outcome=' || mapped_outcome ||
188488
+ ' reason=' || terminal_reason || '; ' ||
188489
+ CASE WHEN latest_checkpoint_id IS NULL
188490
+ THEN 'no valid pre-terminal checkpoint'
188491
+ ELSE 'checkpoint=' || latest_checkpoint_id ||
188492
+ ' completed=' || jsonb_array_length(COALESCE(latest_completed, '[]'::jsonb)) ||
188493
+ ' pending=' || jsonb_array_length(COALESCE(latest_pending, '[]'::jsonb)) ||
188494
+ ' recoverability=' || latest_recoverability
188495
+ END,
188496
+ 400
188497
+ );
188498
+ reconstructed_risks :=
188499
+ CASE WHEN NEW.error_message IS NULL THEN '[]'::jsonb
188500
+ ELSE jsonb_build_array(left(NEW.error_message, 400)) END ||
188501
+ CASE WHEN COALESCE(pending_effects, 0) = 0 AND COALESCE(failed_effects, 0) = 0 THEN '[]'::jsonb
188502
+ ELSE jsonb_build_array('external effects: pending=' || COALESCE(pending_effects, 0) ||
188503
+ ', failed=' || COALESCE(failed_effects, 0)) END;
188504
+ reconstructed_gaps := CASE WHEN latest_checkpoint_id IS NULL
188505
+ THEN jsonb_build_array('No valid pre-terminal checkpoint was available.')
188506
+ ELSE '[]'::jsonb END;
187791
188507
  INSERT INTO "${s2}".node_handoffs AS current_handoff
187792
188508
  (handoff_id, attempt_id, job_key, part, produced_by, outcome, summary, changed_outputs,
187793
- unresolved_issues, risks, next_actions, verification_gaps, created_at)
188509
+ unresolved_issues, risks, next_actions, verification_gaps, user_handoff, created_at)
187794
188510
  VALUES
187795
188511
  (generated_handoff_id, NEW.id, COALESCE(NEW.job_key, NEW.trigger_ref, 'attempt:' || NEW.id), NEW.part,
187796
188512
  'system_reconstructed', mapped_outcome,
187797
- CASE WHEN mapped_outcome = 'succeeded'
187798
- THEN 'Execution completed with ' || jsonb_array_length(NEW.output_refs) || ' durable output reference(s).'
187799
- WHEN mapped_outcome = 'no-output' THEN 'Execution ended cleanly without a durable output.'
187800
- ELSE 'Execution ended with status ' || mapped_outcome || '.' END,
187801
- NEW.output_refs, '[]'::jsonb,
187802
- CASE WHEN NEW.error_message IS NULL THEN '[]'::jsonb ELSE jsonb_build_array(left(NEW.error_message, 400)) END,
187803
- '[]'::jsonb, '[]'::jsonb, COALESCE(NEW.ended_at, NEW.updated_at, NEW.started_at))
188513
+ reconstructed_summary, NEW.output_refs, '[]'::jsonb, reconstructed_risks,
188514
+ COALESCE(latest_pending, '[]'::jsonb), reconstructed_gaps, NULL,
188515
+ COALESCE(NEW.ended_at, NEW.updated_at, NEW.started_at))
187804
188516
  ON CONFLICT (attempt_id) DO UPDATE SET
187805
- outcome=EXCLUDED.outcome,
188517
+ produced_by=CASE
188518
+ WHEN current_handoff.produced_by = 'agent'
188519
+ THEN current_handoff.produced_by
188520
+ ELSE EXCLUDED.produced_by
188521
+ END,
188522
+ outcome=CASE WHEN current_handoff.produced_by = 'agent'
188523
+ THEN current_handoff.outcome ELSE EXCLUDED.outcome END,
188524
+ summary=CASE
188525
+ WHEN current_handoff.produced_by = 'agent'
188526
+ THEN current_handoff.summary
188527
+ ELSE EXCLUDED.summary
188528
+ END,
187806
188529
  changed_outputs=CASE
187807
- WHEN current_handoff.produced_by = 'agent' THEN current_handoff.changed_outputs
188530
+ WHEN current_handoff.produced_by = 'agent'
188531
+ THEN current_handoff.changed_outputs
187808
188532
  ELSE EXCLUDED.changed_outputs
188533
+ END,
188534
+ unresolved_issues=CASE
188535
+ WHEN current_handoff.produced_by = 'agent'
188536
+ THEN current_handoff.unresolved_issues ELSE EXCLUDED.unresolved_issues END,
188537
+ risks=CASE
188538
+ WHEN current_handoff.produced_by = 'agent'
188539
+ THEN current_handoff.risks ELSE EXCLUDED.risks END,
188540
+ next_actions=CASE
188541
+ WHEN current_handoff.produced_by = 'agent'
188542
+ THEN current_handoff.next_actions ELSE EXCLUDED.next_actions END,
188543
+ verification_gaps=CASE
188544
+ WHEN current_handoff.produced_by = 'agent'
188545
+ THEN current_handoff.verification_gaps ELSE EXCLUDED.verification_gaps END,
188546
+ user_handoff=CASE
188547
+ WHEN current_handoff.produced_by = 'agent'
188548
+ THEN current_handoff.user_handoff
188549
+ ELSE EXCLUDED.user_handoff
187809
188550
  END;
187810
188551
  NEW.handoff_id := COALESCE(NEW.handoff_id, generated_handoff_id);
187811
188552
  RETURN NEW;
@@ -188511,6 +189252,101 @@ var init_postgres_trace = __esm({
188511
189252
  );
188512
189253
  return r.rows.map(rowToExternalEffect);
188513
189254
  }
189255
+ async getExternalEffectsForResume(nodeId, jobKey, part, succeededLimit, failedLimit) {
189256
+ const recency = "COALESCE(e.occurred_at, r.ended_at, r.updated_at, r.started_at, '')";
189257
+ const client = await this.pool.connect();
189258
+ try {
189259
+ await client.query("BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY");
189260
+ const counts2 = await client.query(
189261
+ `SELECT COUNT(*)::int AS total,
189262
+ COUNT(*) FILTER (WHERE e.status='pending')::int AS pending,
189263
+ COUNT(*) FILTER (WHERE e.status='succeeded')::int AS succeeded,
189264
+ COUNT(*) FILTER (WHERE e.status='failed')::int AS failed
189265
+ FROM ${this.s}.external_effects e
189266
+ INNER JOIN ${this.s}.agent_runs r ON r.id=e.attempt_id
189267
+ WHERE e.node_id=$1
189268
+ AND COALESCE(r.job_key, r.trigger_ref, 'attempt:' || r.id)=$2
189269
+ AND r.part IS NOT DISTINCT FROM $3`,
189270
+ [nodeId, jobKey, part]
189271
+ );
189272
+ const pending = await client.query(
189273
+ `SELECT e.* FROM ${this.s}.external_effects e
189274
+ LEFT JOIN ${this.s}.agent_runs r ON r.id=e.attempt_id
189275
+ WHERE e.node_id=$1 AND e.status='pending'
189276
+ AND COALESCE(r.job_key, r.trigger_ref, 'attempt:' || r.id)=$2
189277
+ AND r.part IS NOT DISTINCT FROM $3
189278
+ ORDER BY ${recency} DESC, e.idempotency_key DESC`,
189279
+ [nodeId, jobKey, part]
189280
+ );
189281
+ const succeeded = await client.query(
189282
+ `SELECT e.* FROM ${this.s}.external_effects e
189283
+ LEFT JOIN ${this.s}.agent_runs r ON r.id=e.attempt_id
189284
+ WHERE e.node_id=$1 AND e.status='succeeded'
189285
+ AND COALESCE(r.job_key, r.trigger_ref, 'attempt:' || r.id)=$2
189286
+ AND r.part IS NOT DISTINCT FROM $3
189287
+ ORDER BY ${recency} DESC, e.idempotency_key DESC LIMIT $4`,
189288
+ [nodeId, jobKey, part, succeededLimit]
189289
+ );
189290
+ const failed = await client.query(
189291
+ `SELECT e.* FROM ${this.s}.external_effects e
189292
+ LEFT JOIN ${this.s}.agent_runs r ON r.id=e.attempt_id
189293
+ WHERE e.node_id=$1 AND e.status='failed'
189294
+ AND COALESCE(r.job_key, r.trigger_ref, 'attempt:' || r.id)=$2
189295
+ AND r.part IS NOT DISTINCT FROM $3
189296
+ ORDER BY ${recency} DESC, e.idempotency_key DESC LIMIT $4`,
189297
+ [nodeId, jobKey, part, failedLimit]
189298
+ );
189299
+ const totals = counts2.rows[0];
189300
+ const totalByStatus = {
189301
+ pending: Number(totals?.["pending"] ?? 0),
189302
+ succeeded: Number(totals?.["succeeded"] ?? 0),
189303
+ failed: Number(totals?.["failed"] ?? 0)
189304
+ };
189305
+ const projection = {
189306
+ effects: [...pending.rows, ...succeeded.rows, ...failed.rows].map(rowToExternalEffect),
189307
+ summary: {
189308
+ total: Number(totals?.["total"] ?? 0),
189309
+ totalByStatus,
189310
+ includedByStatus: {
189311
+ pending: pending.rowCount ?? 0,
189312
+ succeeded: succeeded.rowCount ?? 0,
189313
+ failed: failed.rowCount ?? 0
189314
+ }
189315
+ }
189316
+ };
189317
+ await client.query("COMMIT");
189318
+ return projection;
189319
+ } catch (error2) {
189320
+ await client.query("ROLLBACK").catch(() => {
189321
+ });
189322
+ throw error2;
189323
+ } finally {
189324
+ client.release();
189325
+ }
189326
+ }
189327
+ async queryExternalEffects(nodeId, options) {
189328
+ const recency = "COALESCE(e.occurred_at, r.ended_at, r.updated_at, r.started_at, '')";
189329
+ const [count2, page] = await Promise.all([
189330
+ this.pool.query(
189331
+ `SELECT COUNT(*)::int AS total FROM ${this.s}.external_effects
189332
+ WHERE node_id=$1 AND ($2::text IS NULL OR status=$2)`,
189333
+ [nodeId, options.status ?? null]
189334
+ ),
189335
+ this.pool.query(
189336
+ `SELECT e.* FROM ${this.s}.external_effects e
189337
+ LEFT JOIN ${this.s}.agent_runs r ON r.id=e.attempt_id
189338
+ WHERE e.node_id=$1 AND ($2::text IS NULL OR e.status=$2)
189339
+ ORDER BY ${recency} DESC, e.idempotency_key DESC LIMIT $3 OFFSET $4`,
189340
+ [nodeId, options.status ?? null, options.limit, options.offset]
189341
+ )
189342
+ ]);
189343
+ return {
189344
+ total: Number(count2.rows[0]?.total ?? 0),
189345
+ offset: options.offset,
189346
+ limit: options.limit,
189347
+ items: page.rows.map(rowToExternalEffect)
189348
+ };
189349
+ }
188514
189350
  async putHandoff(input) {
188515
189351
  if (!input.summary.trim()) throw new Error("handoff summary is required");
188516
189352
  if ([...input.summary].length > 400) throw new Error("handoff summary exceeds 400 characters");
@@ -188537,18 +189373,24 @@ var init_postgres_trace = __esm({
188537
189373
  risks: input.risks ?? [],
188538
189374
  nextActions: input.nextActions ?? [],
188539
189375
  verificationGaps: input.verificationGaps ?? [],
189376
+ userHandoff: normalizeUserHandoff(
189377
+ input.outcome,
189378
+ input.changedOutputs ?? run.outputRefs,
189379
+ input.userHandoff
189380
+ ),
188540
189381
  createdAt: input.createdAt ?? this.now()
188541
189382
  };
188542
189383
  const saved = await client.query(
188543
189384
  `INSERT INTO ${this.s}.node_handoffs AS current_handoff
188544
189385
  (handoff_id, attempt_id, job_key, part, produced_by, outcome, summary, changed_outputs,
188545
- unresolved_issues, risks, next_actions, verification_gaps, created_at)
188546
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9::jsonb,$10::jsonb,$11::jsonb,$12::jsonb,$13)
189386
+ unresolved_issues, risks, next_actions, verification_gaps, user_handoff, created_at)
189387
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9::jsonb,$10::jsonb,$11::jsonb,$12::jsonb,$13::jsonb,$14)
188547
189388
  ON CONFLICT (attempt_id) DO UPDATE SET
188548
189389
  produced_by=EXCLUDED.produced_by, outcome=EXCLUDED.outcome, summary=EXCLUDED.summary,
188549
189390
  changed_outputs=EXCLUDED.changed_outputs, unresolved_issues=EXCLUDED.unresolved_issues,
188550
189391
  risks=EXCLUDED.risks, next_actions=EXCLUDED.next_actions,
188551
- verification_gaps=EXCLUDED.verification_gaps, created_at=EXCLUDED.created_at
189392
+ verification_gaps=EXCLUDED.verification_gaps, user_handoff=EXCLUDED.user_handoff,
189393
+ created_at=EXCLUDED.created_at
188552
189394
  WHERE current_handoff.produced_by <> 'agent' OR EXCLUDED.produced_by = 'agent'
188553
189395
  RETURNING *`,
188554
189396
  [
@@ -188564,6 +189406,7 @@ var init_postgres_trace = __esm({
188564
189406
  JSON.stringify(handoff.risks),
188565
189407
  JSON.stringify(handoff.nextActions),
188566
189408
  JSON.stringify(handoff.verificationGaps),
189409
+ JSON.stringify(handoff.userHandoff),
188567
189410
  handoff.createdAt
188568
189411
  ]
188569
189412
  );
@@ -189074,6 +189917,7 @@ var init_postgres_trace = __esm({
189074
189917
  risks: row.risks ?? [],
189075
189918
  nextActions: row.next_actions ?? [],
189076
189919
  verificationGaps: row.verification_gaps ?? [],
189920
+ userHandoff: row.user_handoff ?? null,
189077
189921
  createdAt: row.created_at
189078
189922
  });
189079
189923
  rowToEvent2 = (row) => ({
@@ -189753,11 +190597,11 @@ var init_postgres_chat_sessions = __esm({
189753
190597
  });
189754
190598
 
189755
190599
  // ../storage/src/postgres-nodes.ts
189756
- var import_node_crypto38, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
190600
+ var import_node_crypto39, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
189757
190601
  var init_postgres_nodes = __esm({
189758
190602
  "../storage/src/postgres-nodes.ts"() {
189759
190603
  "use strict";
189760
- import_node_crypto38 = require("node:crypto");
190604
+ import_node_crypto39 = require("node:crypto");
189761
190605
  init_esm();
189762
190606
  ident12 = (s2) => {
189763
190607
  if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
@@ -189896,7 +190740,7 @@ var init_postgres_nodes = __esm({
189896
190740
  return store;
189897
190741
  }
189898
190742
  issue(nodeId) {
189899
- const token = `ont_${(0, import_node_crypto38.randomBytes)(24).toString("base64url")}`;
190743
+ const token = `ont_${(0, import_node_crypto39.randomBytes)(24).toString("base64url")}`;
189900
190744
  this.cache.set(token, nodeId);
189901
190745
  void this.pool.query(`INSERT INTO ${this.s}.node_tokens (token,node_id) VALUES ($1,$2)`, [token, nodeId]);
189902
190746
  return token;
@@ -190107,11 +190951,11 @@ var init_postgres_type_registry = __esm({
190107
190951
  });
190108
190952
 
190109
190953
  // ../storage/src/postgres-actor-memory.ts
190110
- var import_node_crypto39, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
190954
+ var import_node_crypto40, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
190111
190955
  var init_postgres_actor_memory = __esm({
190112
190956
  "../storage/src/postgres-actor-memory.ts"() {
190113
190957
  "use strict";
190114
- import_node_crypto39 = require("node:crypto");
190958
+ import_node_crypto40 = require("node:crypto");
190115
190959
  init_src();
190116
190960
  matchClause = (q) => q.requireMatch && q.keywords.length > 0 ? " WHERE m > 0" : "";
190117
190961
  ident15 = (s2) => {
@@ -190316,7 +191160,7 @@ var init_postgres_actor_memory = __esm({
190316
191160
  }
190317
191161
  async write(input, now) {
190318
191162
  if (input.memId === void 0) {
190319
- const memId = `mem:${(0, import_node_crypto39.randomUUID)()}`;
191163
+ const memId = `mem:${(0, import_node_crypto40.randomUUID)()}`;
190320
191164
  const r2 = await this.pool.query(
190321
191165
  `INSERT INTO ${this.s}.actor_memories
190322
191166
  (mem_id, actor_id, project_id, keywords, content, version, created_at, updated_at, accessed_at, source_artifact_id, source_session_id)
@@ -190832,14 +191676,14 @@ var import_node_child_process16 = require("node:child_process");
190832
191676
  var fs29 = __toESM(require("node:fs"), 1);
190833
191677
  var os9 = __toESM(require("node:os"), 1);
190834
191678
  var path23 = __toESM(require("node:path"), 1);
190835
- var import_node_crypto44 = require("node:crypto");
191679
+ var import_node_crypto45 = require("node:crypto");
190836
191680
 
190837
191681
  // ../cli/src/serve.ts
190838
191682
  var fs25 = __toESM(require("node:fs"), 1);
190839
191683
  var os5 = __toESM(require("node:os"), 1);
190840
191684
  var path19 = __toESM(require("node:path"), 1);
190841
191685
  var import_node_child_process12 = require("node:child_process");
190842
- var import_node_crypto40 = require("node:crypto");
191686
+ var import_node_crypto41 = require("node:crypto");
190843
191687
  var import_node_url6 = require("node:url");
190844
191688
  init_src3();
190845
191689
  init_src8();
@@ -191692,6 +192536,7 @@ async function startServe(opts) {
191692
192536
  const executionContinuity = createExecutionContinuityDomain({
191693
192537
  store: traceStore,
191694
192538
  mode: continuityMode,
192539
+ readBundleFile: (attemptId, relativePath) => trajReader.readBundleFile(attemptId, relativePath),
191695
192540
  onCheckpointState: async (attemptId, state) => {
191696
192541
  try {
191697
192542
  traceStoreSink.checkpointState(attemptId, state);
@@ -191700,6 +192545,10 @@ async function startServe(opts) {
191700
192545
  console.error(`[execution-continuity] checkpoint state mirror failed for ${attemptId}:`, error2);
191701
192546
  }
191702
192547
  },
192548
+ onAgentHandoffPersisted: createAgentHandoffWorkBridge({
192549
+ getDefaultCompanyId: () => defaultCompanyId,
192550
+ resolveEngine: (companyId) => getEngine(companyId)
192551
+ }),
191703
192552
  resolveGraph: async (workOrderId, companyId) => {
191704
192553
  const companyEngine = await getEngine(companyId ?? defaultCompanyId);
191705
192554
  return [...companyEngine.kernel.model.artifacts.values()].filter((artifact) => artifact.workspace === workOrderId).map((artifact) => ({
@@ -192178,7 +193027,7 @@ async function startServe(opts) {
192178
193027
  }));
192179
193028
  }
192180
193029
  const limits = { wallClockMs: SESSION_WALL_CLOCK_MS.planner };
192181
- const artifactId = `artifact:planner:${(0, import_node_crypto40.randomUUID)()}`;
193030
+ const artifactId = `artifact:planner:${(0, import_node_crypto41.randomUUID)()}`;
192182
193031
  const handle = await chatRemoteAdapter.spawn({
192183
193032
  actor: planner.id,
192184
193033
  actorToken: issueSessionToken(planner.id, { artifactId, action: "plan-workorder", limits, binding }),
@@ -192265,7 +193114,15 @@ async function startServe(opts) {
192265
193114
  executionContinuity.register,
192266
193115
  automations.register,
192267
193116
  playbooks.register,
192268
- ...chatSessionStore ? [createChatSessionsDomain({ store: chatSessionStore, registry: registryStore, trace: traceStore, kernel, artifactState: artifactStateStore, workdir: () => workdirBridge ?? void 0 })] : []
193117
+ ...chatSessionStore ? [createChatSessionsDomain({
193118
+ store: chatSessionStore,
193119
+ registry: registryStore,
193120
+ trace: traceStore,
193121
+ kernel,
193122
+ artifactState: artifactStateStore,
193123
+ workdir: () => workdirBridge ?? void 0,
193124
+ resolveExecutionRecoveryAttempt: (workOrderIds, companyId) => executionContinuity.service.latestRecoveryAttemptForWorkOrders(workOrderIds, companyId)
193125
+ })] : []
192269
193126
  ],
192270
193127
  workorderDrafts,
192271
193128
  assistants: assistantsService,
@@ -192496,7 +193353,9 @@ async function startServe(opts) {
192496
193353
  */
192497
193354
  isDispatchFenced: async (dispatchId) => {
192498
193355
  const r = await traceStore.getRun(dispatchId).catch(() => null);
192499
- if (!r || r.status === "running") return false;
193356
+ if (!r) return false;
193357
+ if (isExplicitlyDispatchFenced(r)) return true;
193358
+ if (r.status === "running") return false;
192500
193359
  return INFERRED_DEATH.has(r.exitReason ?? "");
192501
193360
  },
192502
193361
  retainedSession: (key) => key.annotationId && key.artifactId ? allDispatchers().map((d) => d.retainedProduceSessionFor(key.artifactId)).find(Boolean) : coordinatorRef?.retainedSessionFor(key),
@@ -192552,10 +193411,10 @@ async function startServe(opts) {
192552
193411
  { nodeId: priorChatSession?.runtimeId, runtimeKind: priorChatSession?.runtimeKind },
192553
193412
  { nodeId: binding.nodeId, runtimeKind: binding.runtimeKind }
192554
193413
  ) : false;
192555
- const runtimeSessionId = sessionId ?? (0, import_node_crypto40.randomUUID)();
193414
+ const runtimeSessionId = sessionId ?? (0, import_node_crypto41.randomUUID)();
192556
193415
  const resumeRuntimeSession = Boolean(sessionId);
192557
- const traceRunId = `chat-run:${(0, import_node_crypto40.randomUUID)()}`;
192558
- const artifactId = `artifact:chat:${(0, import_node_crypto40.randomUUID)()}`;
193416
+ const traceRunId = `chat-run:${(0, import_node_crypto41.randomUUID)()}`;
193417
+ const artifactId = `artifact:chat:${(0, import_node_crypto41.randomUUID)()}`;
192559
193418
  const traceStartedAt = (/* @__PURE__ */ new Date()).toISOString();
192560
193419
  let lastProgressTouchMs = 0;
192561
193420
  const PROGRESS_TOUCH_THROTTLE_MS2 = 2e4;
@@ -192950,6 +193809,9 @@ async function startServe(opts) {
192950
193809
  const prodMapping = dispatchToWork.get(msg.dispatchId);
192951
193810
  if (prodMapping && busRef) {
192952
193811
  workToSession.set(prodMapping.workId, { dispatchId: msg.dispatchId, daemonId });
193812
+ if (prodMapping.nodeId !== daemonId) {
193813
+ dispatchToWork.set(msg.dispatchId, { ...prodMapping, nodeId: daemonId });
193814
+ }
192953
193815
  busRef.submit({
192954
193816
  companyId: "",
192955
193817
  workorderId: prodMapping.workorderId,
@@ -193077,6 +193939,7 @@ async function startServe(opts) {
193077
193939
  const dispatchedWorks = /* @__PURE__ */ new Set();
193078
193940
  const dispatchedReviews = /* @__PURE__ */ new Set();
193079
193941
  const dispatchToWork = /* @__PURE__ */ new Map();
193942
+ const workToSession = /* @__PURE__ */ new Map();
193080
193943
  async function rebuildDispatchMappings(store) {
193081
193944
  try {
193082
193945
  const ids2 = await store.transaction((tx) => tx.listActiveWorkorders());
@@ -193102,7 +193965,6 @@ async function startServe(opts) {
193102
193965
  console.warn(`[new-engine] \u91CD\u5EFA dispatch \u6620\u5C04\u5931\u8D25: ${String(err)}`);
193103
193966
  }
193104
193967
  }
193105
- const workToSession = /* @__PURE__ */ new Map();
193106
193968
  const reviewToSession = /* @__PURE__ */ new Map();
193107
193969
  const dispatchToReview = /* @__PURE__ */ new Map();
193108
193970
  let busRef = null;
@@ -193168,7 +194030,7 @@ async function startServe(opts) {
193168
194030
  console.log(`[new-engine] dispatchWork ${workId} \u2192 ${rev.artifactId} / ${rev.author}${replyIssueId ? `\uFF08\u56DE\u4FE1 ${replyIssueId}\uFF09` : ""}`);
193169
194031
  const art = kernelModel.artifacts.get(rev.artifactId);
193170
194032
  const wid = art?.workspace ?? "";
193171
- const ledgerId = `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
194033
+ const ledgerId = `dispatch:${(0, import_node_crypto41.randomUUID)()}`;
193172
194034
  try {
193173
194035
  await dispatchLedger.insertOpen({
193174
194036
  id: ledgerId,
@@ -193184,7 +194046,7 @@ async function startServe(opts) {
193184
194046
  }
193185
194047
  let result;
193186
194048
  try {
193187
- result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, ledgerId);
194049
+ result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, workId, ledgerId);
193188
194050
  } catch (err) {
193189
194051
  dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` }).catch(ledgerWarn("close(error)"));
193190
194052
  console.log(`[new-engine] dispatchWork ${workId} \u6D3E\u53D1\u5F02\u5E38\uFF1A${String(err)} \u2192 \u7F6E work failed`);
@@ -193205,7 +194067,13 @@ async function startServe(opts) {
193205
194067
  return;
193206
194068
  }
193207
194069
  if (result?.dispatchId) {
193208
- dispatchToWork.set(result.dispatchId, { workId, workorderId: wid });
194070
+ const delivery = dispatchRemoteAdapter?.deliveryStatus(result.dispatchId);
194071
+ dispatchToWork.set(result.dispatchId, {
194072
+ workId,
194073
+ workorderId: wid,
194074
+ ...delivery ? { nodeId: delivery.nodeId } : {}
194075
+ });
194076
+ if (delivery) workToSession.set(workId, { dispatchId: result.dispatchId, daemonId: delivery.nodeId });
193209
194077
  dispatchedWorks.add(workId);
193210
194078
  } else if (result?.kind === "spawn-failed" || result?.kind === "predispatch-timeout") {
193211
194079
  dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch ${result.kind}` }).catch(ledgerWarn("close(spawn-failed)"));
@@ -193238,7 +194106,7 @@ async function startServe(opts) {
193238
194106
  const kernelModel = newKernel.model;
193239
194107
  const art = kernelModel.artifacts.get(nodeId);
193240
194108
  const revWid = art?.workspace ?? "";
193241
- const ledgerId = `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
194109
+ const ledgerId = `dispatch:${(0, import_node_crypto41.randomUUID)()}`;
193242
194110
  try {
193243
194111
  await dispatchLedger.insertOpen({
193244
194112
  id: ledgerId,
@@ -193269,11 +194137,16 @@ async function startServe(opts) {
193269
194137
  },
193270
194138
  async cancelSession(ref2) {
193271
194139
  const workSess = workToSession.get(ref2);
193272
- if (workSess && hub) {
193273
- console.log(`[new-engine] cancelSession workId=${ref2} dispatchId=${workSess.dispatchId} daemon=${workSess.daemonId}`);
193274
- hub.dispatch(workSess.daemonId, { type: "kill", dispatchId: workSess.dispatchId });
194140
+ const persistedWorkDispatch = [...dispatchToWork.entries()].find(([, mapping]) => mapping.workId === ref2);
194141
+ if ((workSess || persistedWorkDispatch) && hub) {
194142
+ const dispatchId = workSess?.dispatchId ?? persistedWorkDispatch[0];
194143
+ const daemonId = workSess?.daemonId ?? persistedWorkDispatch[1].nodeId;
194144
+ console.log(`[new-engine] cancelSession workId=${ref2} dispatchId=${dispatchId} daemon=${daemonId ?? "unknown"}`);
194145
+ const fenced = await fenceDispatch(traceStore, dispatchId, "engine-cancel-before-successor");
194146
+ if (!fenced) throw new Error(`\u627E\u4E0D\u5230 dispatch run ${dispatchId}\uFF0C\u62D2\u7EDD\u5728\u65E0 fencing \u65F6\u521B\u5EFA\u540E\u7EE7`);
194147
+ if (daemonId) hub.dispatch(daemonId, { type: "kill", dispatchId });
193275
194148
  workToSession.delete(ref2);
193276
- dispatchLedger.close(workSess.dispatchId, { outcome: "cancelled", reason: "session-cancelled" }).catch(ledgerWarn("close(cancel-work)"));
194149
+ dispatchLedger.close(dispatchId, { outcome: "cancelled", reason: "session-cancelled" }).catch(ledgerWarn("close(cancel-work)"));
193277
194150
  return;
193278
194151
  }
193279
194152
  const reviewSess = reviewToSession.get(ref2);
@@ -193496,7 +194369,15 @@ async function startServe(opts) {
193496
194369
  const out = await actorMemoryService.list({ actorId, artifactId }, { scope: "all", keywords: queryTerms, limit: MEMORY_INJECT_MAX, offset: 0 }).catch(() => null);
193497
194370
  return out?.items ?? [];
193498
194371
  },
193499
- ...continuityMode !== "off" ? { executionContinuityInstructions: renderContinuityProtocol() } : {},
194372
+ resolveUpstreamHandoff: createUpstreamHandoffInputResolver({
194373
+ loadWorkorder: async (workOrderId) => {
194374
+ const store = companyEngine.kernel.getStore?.();
194375
+ if (!store) throw new Error(`engine store unavailable for ${companyId}`);
194376
+ return store.transaction((tx) => tx.loadWorkorder(workOrderId));
194377
+ },
194378
+ getHandoff: (attemptId) => traceStore.getHandoff(attemptId)
194379
+ }),
194380
+ executionContinuityInstructions: continuityMode === "off" ? renderBusinessHandoffProtocol() : renderContinuityProtocol(),
193500
194381
  ...continuityMode === "resume" ? {
193501
194382
  resolveExecutionResumePack: async (artifactId, workOrderId, scope) => {
193502
194383
  const pack = await executionContinuity.service.resumePack(workOrderId, artifactId, companyId, {
@@ -193577,8 +194458,15 @@ async function startServe(opts) {
193577
194458
  });
193578
194459
  const slot = { dispatcher: d, logged: 0, ticking: false };
193579
194460
  dispatchers.set(companyId, slot);
193580
- newEngineProduce = async (artifactId, actorId, dispatchId) => {
193581
- const result = await d.requestProduce({ artifactId, actor: actorId, bypassScheduling: true, ...dispatchId !== void 0 ? { dispatchId } : {} });
194461
+ newEngineProduce = async (artifactId, actorId, workId, dispatchId) => {
194462
+ const result = await d.requestProduce({
194463
+ artifactId,
194464
+ actor: actorId,
194465
+ bypassScheduling: true,
194466
+ engineWorkId: workId,
194467
+ engineCompanyId: companyId,
194468
+ ...dispatchId !== void 0 ? { dispatchId } : {}
194469
+ });
193582
194470
  return { kind: result.kind, dispatchId: result.kind === "dispatched" ? result.dispatchId : void 0 };
193583
194471
  };
193584
194472
  newEngineReplyWork = async (artifactId, actorId, issueId, dispatchId) => {
@@ -194258,7 +195146,7 @@ var SessionManager = class {
194258
195146
  // ../cli/src/daemon/ws-client.ts
194259
195147
  init_wrapper();
194260
195148
  var import_node_os9 = require("node:os");
194261
- var import_node_crypto41 = require("node:crypto");
195149
+ var import_node_crypto42 = require("node:crypto");
194262
195150
  init_src6();
194263
195151
 
194264
195152
  // ../cli/src/daemon/detect-adapters.ts
@@ -194610,6 +195498,10 @@ var DaemonWsClient = class {
194610
195498
  stopped = false;
194611
195499
  /** 收到 update 但有活跃会话时置真:延后到所有会话结束再自更新,避免打断执行中的 agent。 */
194612
195500
  pendingUpdate = false;
195501
+ /** 已回 dispatch_received、尚未完成 runtime spawn 的派发;也随 hello 上报供重连对账。 */
195502
+ pendingDispatchIds = /* @__PURE__ */ new Set();
195503
+ /** setup 期间收到 kill 的派发。spawn 返回后先杀句柄,不得再宣告 session_started。 */
195504
+ cancelledPendingDispatchIds = /* @__PURE__ */ new Set();
194613
195505
  /**
194614
195506
  * 回执 outbox(proposal run-ledger §4.2):**服务端据以判决的帧**发出后先留一份,收到
194615
195507
  * 服务端 ack 才删;ws 抖动坏窗口发失败/丢失的帧,在重连时全量重发——投递升级 at-least-once,
@@ -194738,11 +195630,19 @@ var DaemonWsClient = class {
194738
195630
  const { dispatchId, job } = msg;
194739
195631
  const t0 = Date.now();
194740
195632
  log2("[node-cli]", `\u2190 dispatch ${dispatchId} artifact=${job.artifactId}`);
195633
+ if (this.pendingDispatchIds.has(dispatchId) || this.sessions.get(dispatchId)) {
195634
+ this.send({ type: "dispatch_received", dispatchId });
195635
+ break;
195636
+ }
195637
+ this.pendingDispatchIds.add(dispatchId);
195638
+ this.send({ type: "dispatch_received", dispatchId });
194741
195639
  try {
194742
195640
  if (this.preflight) {
194743
195641
  const preflightFailure = await preflightOasisAccess(job);
194744
195642
  if (preflightFailure) {
194745
195643
  log2("[node-cli]", ` preflight failed: ${preflightFailure.reason}`);
195644
+ this.pendingDispatchIds.delete(dispatchId);
195645
+ this.cancelledPendingDispatchIds.delete(dispatchId);
194746
195646
  this.sendKeyFrame(dispatchId, { type: "session_exited", dispatchId, sessionId: "preflight-failed", info: preflightFailure });
194747
195647
  return;
194748
195648
  }
@@ -194763,8 +195663,8 @@ var DaemonWsClient = class {
194763
195663
  actor: job.actor,
194764
195664
  ...job.action ? { action: job.action } : {}
194765
195665
  });
194766
- log2("[node-cli]", ` \u2192 session_started id=${handle.id} setup=${Date.now() - t0}ms`);
194767
- this.sendKeyFrame(dispatchId, { type: "session_started", dispatchId, sessionId: handle.id, jobArtifactId: job.artifactId });
195666
+ this.pendingDispatchIds.delete(dispatchId);
195667
+ const cancelledBeforeStart = this.cancelledPendingDispatchIds.delete(dispatchId);
194768
195668
  handle.onOutput?.((chunk) => this.sendStream({ type: "session_output", dispatchId, chunk }));
194769
195669
  handle.onTelemetry?.((event) => this.sendStream({ type: "session_event", dispatchId, event }));
194770
195670
  handle.onExit((info) => {
@@ -194773,7 +195673,16 @@ var DaemonWsClient = class {
194773
195673
  this.sendKeyFrame(dispatchId, { type: "session_exited", dispatchId, sessionId: handle.id, info });
194774
195674
  this.maybeRunPendingUpdate();
194775
195675
  });
195676
+ if (cancelledBeforeStart) {
195677
+ log2("[node-cli]", ` dispatch ${dispatchId} \u5728 setup \u671F\u95F4\u5DF2\u88AB fencing\uFF0Cspawn \u540E\u7ACB\u5373 kill`);
195678
+ await handle.kill();
195679
+ } else {
195680
+ log2("[node-cli]", ` \u2192 session_started id=${handle.id} setup=${Date.now() - t0}ms`);
195681
+ this.sendKeyFrame(dispatchId, { type: "session_started", dispatchId, sessionId: handle.id, jobArtifactId: job.artifactId });
195682
+ }
194776
195683
  } catch (err) {
195684
+ this.pendingDispatchIds.delete(dispatchId);
195685
+ this.cancelledPendingDispatchIds.delete(dispatchId);
194777
195686
  log2("[node-cli]", ` spawn failed: ${err}`);
194778
195687
  const errorMessage = String(err?.stack ?? err).slice(0, 8e3);
194779
195688
  this.sendKeyFrame(dispatchId, { type: "session_exited", dispatchId, sessionId: "spawn-failed", info: { code: null, reason: "error", errorMessage } });
@@ -194783,7 +195692,9 @@ var DaemonWsClient = class {
194783
195692
  case "kill": {
194784
195693
  log2("[node-cli]", `\u2190 kill dispatch=${msg.dispatchId}`);
194785
195694
  const killed = await this.sessions.kill(msg.dispatchId);
194786
- if (!killed) {
195695
+ if (!killed && this.pendingDispatchIds.has(msg.dispatchId)) {
195696
+ this.cancelledPendingDispatchIds.add(msg.dispatchId);
195697
+ } else if (!killed) {
194787
195698
  this.sendKeyFrame(msg.dispatchId, { type: "session_exited", dispatchId: msg.dispatchId, sessionId: "missing-session", info: { code: null, reason: "timeout" } });
194788
195699
  }
194789
195700
  break;
@@ -194856,7 +195767,7 @@ var DaemonWsClient = class {
194856
195767
  */
194857
195768
  async queryArtifactStates(artifactIds, timeoutMs = 15e3) {
194858
195769
  if (artifactIds.length === 0) return {};
194859
- const requestId = (0, import_node_crypto41.randomUUID)();
195770
+ const requestId = (0, import_node_crypto42.randomUUID)();
194860
195771
  return new Promise((resolve9, reject) => {
194861
195772
  const timer = setTimeout(() => {
194862
195773
  this.gcPending.delete(requestId);
@@ -194939,7 +195850,9 @@ var DaemonWsClient = class {
194939
195850
  ...this.daemonVersion ? { daemonVersion: this.daemonVersion } : {},
194940
195851
  connectorReadiness: inspectConnectorRuntimeReadiness(),
194941
195852
  ...this.nodeName ? { nodeName: this.nodeName } : {},
194942
- ...activeSessions.length > 0 ? { activeSessions } : {}
195853
+ ...activeSessions.length > 0 ? { activeSessions } : {},
195854
+ // 始终带字段(空数组也带)作为新协议能力标志;服务端仅对支持该字段的节点做缺席确认。
195855
+ pendingDispatchIds: [...this.pendingDispatchIds]
194943
195856
  };
194944
195857
  }
194945
195858
  };
@@ -195143,7 +196056,7 @@ function forwardSignal(child, sig, killGroup = (pgid, signal) => {
195143
196056
  // ../cli/src/daemon/machine-id.ts
195144
196057
  var import_node_child_process14 = require("node:child_process");
195145
196058
  var import_node_fs20 = require("node:fs");
195146
- var import_node_crypto42 = require("node:crypto");
196059
+ var import_node_crypto43 = require("node:crypto");
195147
196060
  var import_node_os12 = require("node:os");
195148
196061
  function linuxMachineId() {
195149
196062
  for (const p2 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
@@ -195209,7 +196122,7 @@ var defaultSources = {
195209
196122
  function resolveNodeId(sources = {}) {
195210
196123
  const s2 = { ...defaultSources, ...sources };
195211
196124
  const material = `${s2.machineFingerprint()}:${s2.osUser()}`;
195212
- const digest = (0, import_node_crypto42.createHash)("sha256").update(material).digest("hex").slice(0, 12);
196125
+ const digest = (0, import_node_crypto43.createHash)("sha256").update(material).digest("hex").slice(0, 12);
195213
196126
  return `node-${digest}`;
195214
196127
  }
195215
196128
 
@@ -195655,18 +196568,21 @@ var COMMAND_DECLS = {
195655
196568
  examples: ["oasis continuity-effect --kind send-message --target chat:release --key release-v1"]
195656
196569
  },
195657
196570
  "continuity-handoff": {
195658
- usage: "oasis continuity-handoff --outcome <status> --summary <text> [--outputs <json-array>] [--unresolved <json-string-array>] [--risks <json-string-array>] [--next <json-string-array>] [--verification-gaps <json-string-array>]",
195659
- description: "\u4E3A\u5F53\u524D\u6D3E\u53D1 attempt \u5199\u7ED3\u6784\u5316 NodeHandoff\uFF1B\u7CFB\u7EDF\u4ECD\u4F1A\u4E3A\u6F0F\u5199\u7684\u7EC8\u6001 attempt \u515C\u5E95\u91CD\u5EFA\u3002",
196571
+ usage: "oasis continuity-handoff --outcome <status> --summary <text> --user-summary <text> [--user-deliverables <json-array>] [--user-next-step <text>] [--outputs <json-array>] [--unresolved <json-string-array>] [--risks <json-string-array>] [--next <json-string-array>] [--verification-gaps <json-string-array>]",
196572
+ description: "\u4E3A\u5F53\u524D\u6D3E\u53D1 attempt \u5199\u6280\u672F\u578B NodeHandoff\uFF0C\u5E76\u5355\u72EC\u5199\u4F9B\u4F7F\u7528\u8005\u67E5\u770B\u7684 userHandoff\u3002",
195660
196573
  flags: [
195661
196574
  { name: "outcome", desc: "succeeded | no-output | failed | cancelled | timeout | orphaned", required: true },
195662
196575
  { name: "summary", desc: "\u4E0D\u8D85\u8FC7 400 \u5B57\u7684\u6458\u8981", required: true },
196576
+ { name: "user-summary", desc: "\u505A\u4E86\u4EC0\u4E48\u3001\u5F62\u6210\u4EC0\u4E48\u7ED3\u679C\u3001\u4E0B\u6E38\u53EF\u57FA\u4E8E\u4EC0\u4E48\u7EE7\u7EED\uFF1B\u5931\u8D25\u65F6\u5199\u672A\u4EA4\u4ED8\u539F\u56E0", required: true },
196577
+ { name: "user-deliverables", desc: "\u53EF\u6253\u5F00\u7684\u4E1A\u52A1\u4EA4\u4ED8\u7269\u5F15\u7528 JSON array\uFF1B\u4E0D\u8981\u586B conclude/gap/checkpoint/op" },
196578
+ { name: "user-next-step", desc: "\u7ED9\u4E0B\u6E38\u6216\u5904\u7406\u963B\u585E\u8005\u7684\u7B80\u77ED\u4E0B\u4E00\u6B65" },
195663
196579
  { name: "outputs", desc: "\u53D8\u5316\u8F93\u51FA\u5F15\u7528 JSON array" },
195664
196580
  { name: "unresolved", desc: "\u672A\u89E3\u51B3\u95EE\u9898 JSON string array" },
195665
196581
  { name: "risks", desc: "\u98CE\u9669 JSON string array" },
195666
196582
  { name: "next", desc: "\u4E0B\u4E00\u6B65 JSON string array" },
195667
196583
  { name: "verification-gaps", desc: "\u9A8C\u8BC1\u7F3A\u53E3 JSON string array" }
195668
196584
  ],
195669
- examples: [`oasis continuity-handoff --outcome succeeded --summary "implemented and tested" --next '[]'`]
196585
+ examples: [`oasis continuity-handoff --outcome succeeded --summary "implemented and tested" --user-summary "\u5F00\u53D1\u5DF2\u5B8C\u6210\u5E76\u901A\u8FC7\u6D4B\u8BD5\u3002" --user-deliverables '[{"kind":"artifact","id":"artifact:dev:x","label":"\u5F00\u53D1\u7ED3\u679C"}]' --user-next-step "\u4E0B\u6E38\u53EF\u5F00\u59CB\u9A8C\u6536\u3002" --next '[]'`]
195670
196586
  },
195671
196587
  "continuity-view": {
195672
196588
  usage: "oasis continuity-view <workOrderId>",
@@ -196443,13 +197359,13 @@ function ownFlagsFromDecl(command) {
196443
197359
  }
196444
197360
 
196445
197361
  // ../cli/src/connector-effect-runner.ts
196446
- var import_node_crypto43 = require("node:crypto");
197362
+ var import_node_crypto44 = require("node:crypto");
196447
197363
  var import_node_child_process15 = require("node:child_process");
196448
197364
  init_src6();
196449
197365
  function stableKey(effect) {
196450
197366
  const keyFields = Object.fromEntries(Object.entries(effect.keyFields).sort(([a], [b2]) => a.localeCompare(b2)));
196451
197367
  const canonical = JSON.stringify({ kind: effect.kind, target: effect.target, keyFields });
196452
- return `sha256:${(0, import_node_crypto43.createHash)("sha256").update(canonical).digest("hex")}`;
197368
+ return `sha256:${(0, import_node_crypto44.createHash)("sha256").update(canonical).digest("hex")}`;
196453
197369
  }
196454
197370
  async function runConnectorEffect(connector, toolArgs, deps) {
196455
197371
  const effect = connector.describeExternalEffect?.(toolArgs) ?? null;
@@ -196579,7 +197495,7 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
196579
197495
  record-action <artifactId> --name <\u52A8\u4F5C\u540D> [--pass true] [--result <json>] [--params <json>] # \xA79.2 grounding \u7559\u75D5\uFF1A\u4E3A action_vs_oplog \u8BB0\u4E00\u6761\u901A\u8FC7\u7684 tool_call\uFF08\u771F\u8DD1\u8FC7\u5065\u5EB7\u63A2\u9488\u540E record-action --name health_probe --pass true\uFF09\uFF1B\u58F0\u660E\u4E86 requiredActions \u7684\u7C7B\u578B\u6CA1\u6709\u5B83 conclude \u4F1A\u88AB\u673A\u68B0\u62E6
196580
197496
  continuity-checkpoint --pending <json-string-array> --recoverability full|partial|none [--trigger plan_completed|stage_completed] [--refs <json-array>] [--completed <json-string-array>]
196581
197497
  continuity-effect --kind <kind> --target <target> --key <stable-key>
196582
- continuity-handoff --outcome <status> --summary <text> [--outputs <json-array>] [--unresolved <json-string-array>] [--risks <json-string-array>] [--next <json-string-array>] [--verification-gaps <json-string-array>]
197498
+ continuity-handoff --outcome <status> --summary <text> --user-summary <text> [--user-deliverables <json-array>] [--user-next-step <text>] [--outputs <json-array>] [--unresolved <json-string-array>] [--risks <json-string-array>] [--next <json-string-array>] [--verification-gaps <json-string-array>]
196583
197499
  continuity-view <workOrderId> # \u534F\u8C03\u89C6\u56FE\uFF1A\u516B\u6001\u8FDB\u5EA6\u3001\u4F9D\u8D56\u963B\u585E\u3001\u6309\u4F18\u5148\u7EA7 todo
196584
197500
  continuity-resume <workOrderId> <nodeId> # \u8C03\u8BD5\u5F53\u524D\u8282\u70B9\u4F1A\u6536\u5230\u7684\u7ED3\u6784\u5316 ResumePack
196585
197501
  resolve <annotationId> [--as resolved|wontfix|acknowledged]
@@ -197159,7 +198075,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
197159
198075
  const store = createNodeTokenStore(path23.join(dir, "node-tokens.json"));
197160
198076
  const sub = positional[0];
197161
198077
  if (sub === "issue") {
197162
- const id = flags.get("id") ?? `node-${(0, import_node_crypto44.randomUUID)()}`;
198078
+ const id = flags.get("id") ?? `node-${(0, import_node_crypto45.randomUUID)()}`;
197163
198079
  const token2 = store.issue(id);
197164
198080
  println(token2);
197165
198081
  process.stderr.write(
@@ -197596,14 +198512,26 @@ Trace: ${run.traceRunId}`);
197596
198512
  break;
197597
198513
  }
197598
198514
  case "continuity-handoff": {
198515
+ const outcome = need(flags, "outcome");
198516
+ const changedOutputs = flags.has("outputs") ? parseArrayFlag(flags.get("outputs"), "outputs") : [];
198517
+ const userSummary = need(flags, "user-summary").trim();
198518
+ if (!userSummary) throw new Error("--user-summary \u4E0D\u80FD\u4E3A\u7A7A");
198519
+ const userDeliverables = flags.has("user-deliverables") ? parseArrayFlag(flags.get("user-deliverables"), "user-deliverables") : [];
198520
+ const userNextStep = flags.get("user-next-step")?.trim() ?? "";
197599
198521
  const result = await api.request("PUT", "/api/execution-continuity/attempts/current/handoff", {
197600
- outcome: need(flags, "outcome"),
198522
+ outcome,
197601
198523
  summary: need(flags, "summary"),
197602
- changedOutputs: flags.has("outputs") ? parseArrayFlag(flags.get("outputs"), "outputs") : [],
198524
+ changedOutputs,
197603
198525
  unresolvedIssues: flags.has("unresolved") ? parseStringArrayFlag(flags.get("unresolved"), "unresolved") : [],
197604
198526
  risks: flags.has("risks") ? parseStringArrayFlag(flags.get("risks"), "risks") : [],
197605
198527
  nextActions: flags.has("next") ? parseStringArrayFlag(flags.get("next"), "next") : [],
197606
- verificationGaps: flags.has("verification-gaps") ? parseStringArrayFlag(flags.get("verification-gaps"), "verification-gaps") : []
198528
+ verificationGaps: flags.has("verification-gaps") ? parseStringArrayFlag(flags.get("verification-gaps"), "verification-gaps") : [],
198529
+ userHandoff: {
198530
+ status: outcome === "succeeded" ? "delivered" : "not_delivered",
198531
+ summary: userSummary,
198532
+ deliverables: outcome === "succeeded" ? userDeliverables : [],
198533
+ nextStep: userNextStep
198534
+ }
197607
198535
  });
197608
198536
  println(JSON.stringify(result, null, 2));
197609
198537
  break;
@@ -199338,7 +200266,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
199338
200266
  }
199339
200267
 
199340
200268
  // src/index.ts
199341
- var PKG_VERSION = true ? "0.1.99" : "dev";
200269
+ var PKG_VERSION = true ? "0.1.101" : "dev";
199342
200270
  var OASIS_DIR = path25.join(os10.homedir(), ".oasis");
199343
200271
  var CONFIG_FILE = path25.join(OASIS_DIR, "node-config.json");
199344
200272
  var PID_FILE = path25.join(OASIS_DIR, "node.pid");
@@ -199507,7 +200435,7 @@ function selfUpdate() {
199507
200435
  const inner = `{ echo "[oasis] self-update \u2192 ${PKG_NAME}@latest (registry ${OFFICIAL}, \u5931\u8D25\u56DE\u843D\u9ED8\u8BA4\u6E90)"; sleep 2; { ${installOfficial} || ${installFallback}; } && ${printVersion} && ${startCmd}; } >> '${LOG_FILE}' 2>&1`;
199508
200436
  const hasSystemd = process.platform === "linux" && (() => {
199509
200437
  try {
199510
- (0, import_node_child_process16.execSync)("command -v systemd-run", { stdio: "ignore" });
200438
+ (0, import_node_child_process16.execSync)("systemd-run --user --scope --quiet -- true", { stdio: "ignore" });
199511
200439
  return true;
199512
200440
  } catch {
199513
200441
  return false;