oasis_test_v2 2.2.20 → 2.2.21

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 +467 -157
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4082,7 +4082,8 @@ var init_model = __esm({
4082
4082
  BUSINESS_HANDOFF_POLICY_FIELD = "businessHandoffPolicy";
4083
4083
  BUSINESS_HANDOFF_POLICY_EXEMPT = "exempt";
4084
4084
  DEFAULT_CONFIG = {
4085
- maxRetries: 3
4085
+ maxRetries: 3,
4086
+ maxStagnantCheckpoints: 3
4086
4087
  };
4087
4088
  deriveWorkId = (nodeId, version2) => `wk:${nodeId}:${version2}`;
4088
4089
  deriveReviewId = (workId, reviewer, group) => `rv:${workId}:${reviewer}:${group}`;
@@ -4090,6 +4091,101 @@ var init_model = __esm({
4090
4091
  }
4091
4092
  });
4092
4093
 
4094
+ // ../engine/src/closure-gate.ts
4095
+ function closureSlotOfFacts(facts) {
4096
+ const report = facts.liveClosureReport();
4097
+ if (report) return { kind: "closure-report", id: report.id };
4098
+ const business = facts.liveBusinessNodes();
4099
+ if (business.length === 1) return { kind: "sole-artifact", id: business[0].id };
4100
+ if (business.length === 0) return { kind: "empty" };
4101
+ return { kind: "missing", businessCount: business.length };
4102
+ }
4103
+ function humanGateActorsOfFacts(facts, artifactId) {
4104
+ const slot = closureSlotOfFacts(facts);
4105
+ if (slot.kind !== "closure-report" && slot.kind !== "sole-artifact") return [];
4106
+ if (slot.id !== artifactId) return [];
4107
+ const owner = facts.humanOwner();
4108
+ return owner === null ? [] : [owner];
4109
+ }
4110
+ function closureReviewGroup(actor) {
4111
+ return `closure:${actor}`;
4112
+ }
4113
+ function closureReviewerSpecs(closureActors) {
4114
+ return closureActors.map((actor) => ({
4115
+ reviewerActorId: actor,
4116
+ reviewGroup: closureReviewGroup(actor),
4117
+ source: "closure"
4118
+ }));
4119
+ }
4120
+ function engineClosureFacts(state) {
4121
+ return {
4122
+ liveBusinessNodes: () => state.liveNodes().filter((n) => n.type !== BRIEF_TYPE && n.type !== DELIVERY_SUMMARY_TYPE).map((n) => ({ id: n.id, type: n.type })),
4123
+ liveClosureReport: () => {
4124
+ const r = state.liveNodes().find((n) => n.type === DELIVERY_SUMMARY_TYPE);
4125
+ return r ? { id: r.id, type: r.type } : null;
4126
+ },
4127
+ humanOwner: () => {
4128
+ const brief = state.liveNodes().find((n) => n.type === BRIEF_TYPE);
4129
+ const owner = brief?.assigneeActorId;
4130
+ if (!owner || !owner.startsWith(HUMAN_ACTOR_PREFIX)) return null;
4131
+ return owner;
4132
+ }
4133
+ };
4134
+ }
4135
+ function engineWorkorderHumanOwner(state) {
4136
+ return engineClosureFacts(state).humanOwner();
4137
+ }
4138
+ function engineHumanGateActors(state, nodeId) {
4139
+ return humanGateActorsOfFacts(engineClosureFacts(state), nodeId);
4140
+ }
4141
+ var DELIVERY_SUMMARY_TYPE, BRIEF_TYPE, HUMAN_ACTOR_PREFIX;
4142
+ var init_closure_gate = __esm({
4143
+ "../engine/src/closure-gate.ts"() {
4144
+ "use strict";
4145
+ DELIVERY_SUMMARY_TYPE = "delivery-summary";
4146
+ BRIEF_TYPE = "brief";
4147
+ HUMAN_ACTOR_PREFIX = "actor:human:";
4148
+ }
4149
+ });
4150
+
4151
+ // ../engine/src/continuation.ts
4152
+ function continuationApprover(state) {
4153
+ const owner = engineWorkorderHumanOwner(state);
4154
+ if (owner || state.liveNodes().some((node2) => node2.type === "brief")) return owner;
4155
+ return state.workorder.ownerActorId.startsWith("actor:human:") ? state.workorder.ownerActorId : null;
4156
+ }
4157
+ function continuationUsedRounds(state, nodeId) {
4158
+ const withOutput = new Set(state.artifactsOfNode(nodeId).map((a) => a.workId));
4159
+ return state.mainWorksOf(nodeId).filter((w2) => w2.assigneeActorId.startsWith("actor:agent:") && (w2.startedAt || w2.sessionRef || w2.lastActivityAt || withOutput.has(w2.id))).length;
4160
+ }
4161
+ function continuationAwaitingStart(state, work) {
4162
+ return work.status === "running" && !work.endedAt && !work.deadAt && !work.cancelledAt && !work.startedAt && !work.sessionRef && !work.lastActivityAt && state.artifactsOf(work.id).length === 0;
4163
+ }
4164
+ function continuationExhausted(state, node2) {
4165
+ const budget = node2.continuationBudget;
4166
+ return !!budget && (budget.request?.decision === "denied" || budget.request?.decision === "pending" || continuationUsedRounds(state, node2.id) >= budget.authorizedRounds);
4167
+ }
4168
+ function continuationRequestId(node2) {
4169
+ return `continuation:${node2.id}:${node2.continuationBudget.requestGeneration + 1}`;
4170
+ }
4171
+ function continuationSummary(state, node2, rounds) {
4172
+ const used = continuationUsedRounds(state, node2.id);
4173
+ const latest = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
4174
+ const output = node2.latestProposedWorkId ? state.work(node2.latestProposedWorkId) : latest;
4175
+ const files = output ? state.artifactsOf(output.id) : [];
4176
+ return `\u300A${node2.title}\u300B\u5C1A\u672A\u5B8C\u6210\uFF0C\u5DF2\u8FBE\u5230\u81EA\u52A8\u6D3E\u53D1\u4E0A\u9650\u3002\u7D2F\u8BA1\u5DF2\u8FD0\u884C ${used} \u8F6E\uFF0C\u5DF2\u6388\u6743 ${node2.continuationBudget.authorizedRounds} \u8F6E\u3002
4177
+ \u9636\u6BB5\u6210\u679C\uFF1A${files.length ? files.map((a) => a.logicalPath).join("\u3001") : "\u6682\u65E0\u5DF2\u63D0\u4EA4\u7684\u4EA4\u4ED8\u5185\u5BB9"}\u3002
4178
+ \u6700\u8FD1 checkpoint \u8BF4\u660E\uFF08\u6267\u884C\u4EBA\u539F\u6587\uFF09\uFF1A${output?.proposalReason || "\u672A\u77E5\uFF0C\u672A\u63D0\u4F9B"}
4179
+ \u5269\u4F59\u4E8B\u9879\u53CA\u4E0B\u4E00\u6279\u8BA1\u5212\uFF1A\u4EE5\u4EE5\u4E0A\u8BF4\u660E\u4E3A\u4F9D\u636E\uFF1B\u672A\u660E\u786E\u63D0\u4F9B\u7684\u90E8\u5206\u672A\u77E5\uFF0C\u9700\u4EBA\u5DE5\u786E\u8BA4\u3002
4180
+ \u540C\u610F\u540E\u6700\u591A\u518D\u8FD0\u884C ${rounds} \u8F6E\uFF1B\u62D2\u7EDD\u6216\u4E0D\u56DE\u590D\u5C06\u4FDD\u6301\u6682\u505C\u5E76\u4FDD\u7559\u6210\u679C\uFF0C\u4E0D\u81EA\u52A8\u5BA3\u544A\u5B8C\u6210\u3002`;
4181
+ }
4182
+ var init_continuation = __esm({
4183
+ "../engine/src/continuation.ts"() {
4184
+ "use strict";
4185
+ init_closure_gate();
4186
+ }
4187
+ });
4188
+
4093
4189
  // ../engine/src/events.ts
4094
4190
  function eventAnchors(e) {
4095
4191
  switch (e.kind) {
@@ -4334,6 +4430,10 @@ var init_state = __esm({
4334
4430
  artifactsOf(workId) {
4335
4431
  return this.artifactList.filter((a) => a.workId === workId);
4336
4432
  }
4433
+ /** 多批次分析一次读取本节点全部快照,避免每个 work 再扫整单产物。 */
4434
+ artifactsOfNode(nodeId) {
4435
+ return this.artifactList.filter((a) => a.nodeId === nodeId);
4436
+ }
4337
4437
  workInputsOf(workId) {
4338
4438
  return this.workInputList.filter((i) => i.workId === workId);
4339
4439
  }
@@ -4491,60 +4591,6 @@ var init_state = __esm({
4491
4591
  }
4492
4592
  });
4493
4593
 
4494
- // ../engine/src/closure-gate.ts
4495
- function closureSlotOfFacts(facts) {
4496
- const report = facts.liveClosureReport();
4497
- if (report) return { kind: "closure-report", id: report.id };
4498
- const business = facts.liveBusinessNodes();
4499
- if (business.length === 1) return { kind: "sole-artifact", id: business[0].id };
4500
- if (business.length === 0) return { kind: "empty" };
4501
- return { kind: "missing", businessCount: business.length };
4502
- }
4503
- function humanGateActorsOfFacts(facts, artifactId) {
4504
- const slot = closureSlotOfFacts(facts);
4505
- if (slot.kind !== "closure-report" && slot.kind !== "sole-artifact") return [];
4506
- if (slot.id !== artifactId) return [];
4507
- const owner = facts.humanOwner();
4508
- return owner === null ? [] : [owner];
4509
- }
4510
- function closureReviewGroup(actor) {
4511
- return `closure:${actor}`;
4512
- }
4513
- function closureReviewerSpecs(closureActors) {
4514
- return closureActors.map((actor) => ({
4515
- reviewerActorId: actor,
4516
- reviewGroup: closureReviewGroup(actor),
4517
- source: "closure"
4518
- }));
4519
- }
4520
- function engineClosureFacts(state) {
4521
- return {
4522
- liveBusinessNodes: () => state.liveNodes().filter((n) => n.type !== BRIEF_TYPE && n.type !== DELIVERY_SUMMARY_TYPE).map((n) => ({ id: n.id, type: n.type })),
4523
- liveClosureReport: () => {
4524
- const r = state.liveNodes().find((n) => n.type === DELIVERY_SUMMARY_TYPE);
4525
- return r ? { id: r.id, type: r.type } : null;
4526
- },
4527
- humanOwner: () => {
4528
- const brief = state.liveNodes().find((n) => n.type === BRIEF_TYPE);
4529
- const owner = brief?.assigneeActorId;
4530
- if (!owner || !owner.startsWith(HUMAN_ACTOR_PREFIX)) return null;
4531
- return owner;
4532
- }
4533
- };
4534
- }
4535
- function engineHumanGateActors(state, nodeId) {
4536
- return humanGateActorsOfFacts(engineClosureFacts(state), nodeId);
4537
- }
4538
- var DELIVERY_SUMMARY_TYPE, BRIEF_TYPE, HUMAN_ACTOR_PREFIX;
4539
- var init_closure_gate = __esm({
4540
- "../engine/src/closure-gate.ts"() {
4541
- "use strict";
4542
- DELIVERY_SUMMARY_TYPE = "delivery-summary";
4543
- BRIEF_TYPE = "brief";
4544
- HUMAN_ACTOR_PREFIX = "actor:human:";
4545
- }
4546
- });
4547
-
4548
4594
  // ../engine/src/letters.ts
4549
4595
  function repliesSorted(state, issueId) {
4550
4596
  return [...state.repliesOf(issueId)].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
@@ -4701,6 +4747,26 @@ function scan(state, ctx) {
4701
4747
  const events = [];
4702
4748
  for (const node2 of state.liveNodes()) {
4703
4749
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
4750
+ const continuationPolicy = node2.assigneeActorId ? ctx.continuationPolicies?.get(node2.assigneeActorId) : null;
4751
+ if (!node2.continuationBudget && continuationPolicy) {
4752
+ events.push({ kind: "continuation.initialized", workorderId: state.workorder.id, nodeId: node2.id, rounds: continuationPolicy.maxRounds });
4753
+ continue;
4754
+ }
4755
+ const delivered = node2.latestProposedWorkId ? state.work(node2.latestProposedWorkId) : null;
4756
+ const hasCurrentDelivery = delivered?.status === "success" && !delivered.deadAt && (delivered.conclusion != null || node2.latestAcceptId === delivered.id) && delivered.nodeVersion === node2.version && inputsMatch(state, node2, delivered);
4757
+ if (node2.continuationBudget && continuationExhausted(state, node2) && !hasCurrentDelivery && (!lw || lw.endedAt || lw.deadAt || continuationAwaitingStart(state, lw))) {
4758
+ const requestId = continuationRequestId(node2);
4759
+ if (node2.continuationBudget.request?.decision !== "pending" && node2.continuationBudget.request?.decision !== "denied") {
4760
+ events.push({
4761
+ kind: "continuation.requested",
4762
+ workorderId: state.workorder.id,
4763
+ nodeId: node2.id,
4764
+ requestId,
4765
+ rounds: continuationPolicy?.maxRounds ?? node2.continuationBudget.roundsPerGrant
4766
+ });
4767
+ }
4768
+ continue;
4769
+ }
4704
4770
  if (!lw) {
4705
4771
  tryToRun(state, node2.id, ctx, events);
4706
4772
  continue;
@@ -4994,6 +5060,20 @@ function scanBlockersForNode(state, node2, latest) {
4994
5060
  function tryToRun(state, nodeId, ctx, events) {
4995
5061
  const node2 = state.node(nodeId);
4996
5062
  if (!node2) return;
5063
+ if (continuationExhausted(state, node2)) {
5064
+ const latest2 = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
5065
+ const request2 = node2.continuationBudget?.request;
5066
+ if ((!latest2 || latest2.endedAt || latest2.deadAt) && request2?.decision !== "pending" && request2?.decision !== "denied") {
5067
+ events.push({
5068
+ kind: "continuation.requested",
5069
+ workorderId: state.workorder.id,
5070
+ nodeId: node2.id,
5071
+ requestId: continuationRequestId(node2),
5072
+ rounds: (node2.assigneeActorId ? ctx.continuationPolicies?.get(node2.assigneeActorId)?.maxRounds : null) ?? node2.continuationBudget.roundsPerGrant
5073
+ });
5074
+ } else if (latest2 && !latest2.endedAt && !latest2.deadAt) checkRunningWorkTimeout(state, node2, latest2, ctx, events);
5075
+ return;
5076
+ }
4997
5077
  if (!node2.assigneeActorId) return;
4998
5078
  const latest = (node2.latestWorkId ? state.work(node2.latestWorkId) : null) ?? null;
4999
5079
  if (unexemptedVetoBlockers(scanBlockersForNode(state, node2, latest), "dispatch").length > 0) {
@@ -5092,19 +5172,34 @@ function findHumanRecoveryPost(state, node2, deadAt) {
5092
5172
  const first = candidates[0];
5093
5173
  return first ? { actorId: first.actorId, at: first.at } : null;
5094
5174
  }
5095
- function countUnconcludedCheckpoints(state, nodeId) {
5096
- const works = state.mainWorksOf(nodeId).sort((a, b2) => b2.createdAt.localeCompare(a.createdAt));
5175
+ function countStagnantCheckpoints(state, nodeId) {
5176
+ const works = state.mainWorksOf(nodeId).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id, "en", { numeric: true }));
5177
+ const contents = /* @__PURE__ */ new Map();
5178
+ for (const a of state.artifactsOfNode(nodeId)) {
5179
+ const parts = contents.get(a.workId) ?? [];
5180
+ parts.push(JSON.stringify([a.logicalPath, a.contentKind, a.contentRef]));
5181
+ contents.set(a.workId, parts);
5182
+ }
5183
+ const seen = /* @__PURE__ */ new Set();
5097
5184
  let count2 = 0;
5185
+ let version2;
5098
5186
  for (const w2 of works) {
5099
- if (w2.retryAt) break;
5100
- if (w2.status === "success") {
5101
- if (w2.conclusion == null && state.artifactsOf(w2.id).length > 0) {
5102
- count2++;
5103
- continue;
5104
- }
5105
- break;
5106
- }
5107
- continue;
5187
+ if (w2.retryAt || w2.status === "success" && w2.conclusion != null) {
5188
+ seen.clear();
5189
+ count2 = 0;
5190
+ version2 = void 0;
5191
+ }
5192
+ if (w2.status !== "success") continue;
5193
+ const parts = contents.get(w2.id);
5194
+ if (!parts?.length) continue;
5195
+ if (version2 !== void 0 && version2 !== w2.nodeVersion) {
5196
+ seen.clear();
5197
+ count2 = 0;
5198
+ }
5199
+ version2 = w2.nodeVersion;
5200
+ const signature = JSON.stringify(parts.sort());
5201
+ count2 = !w2.retryAt && w2.conclusion == null && seen.has(signature) ? count2 + 1 : 0;
5202
+ seen.add(signature);
5108
5203
  }
5109
5204
  return count2;
5110
5205
  }
@@ -5123,8 +5218,8 @@ function countConsecutiveRejections(state, nodeId) {
5123
5218
  return count2;
5124
5219
  }
5125
5220
  function unconcludedCheckpointsExhausted(state, nodeId, ctx) {
5126
- const limit = ctx.config?.maxRetries ?? DEFAULT_CONFIG.maxRetries;
5127
- const rounds = countUnconcludedCheckpoints(state, nodeId);
5221
+ const limit = ctx.config?.maxStagnantCheckpoints ?? DEFAULT_CONFIG.maxStagnantCheckpoints;
5222
+ const rounds = countStagnantCheckpoints(state, nodeId);
5128
5223
  return rounds > limit ? { rounds, limit } : null;
5129
5224
  }
5130
5225
  function gateRejectionsExhausted(state, nodeId, ctx) {
@@ -5478,6 +5573,7 @@ function assessConvergence(state, ctx) {
5478
5573
  var init_activate = __esm({
5479
5574
  "../engine/src/activate.ts"() {
5480
5575
  "use strict";
5576
+ init_continuation();
5481
5577
  init_src3();
5482
5578
  init_src2();
5483
5579
  init_model();
@@ -5532,6 +5628,7 @@ CREATE TABLE IF NOT EXISTS ${q2}.workflow_nodes (
5532
5628
  latest_accept_id text,
5533
5629
  is_final_output boolean NOT NULL DEFAULT false,
5534
5630
  fields jsonb,
5631
+ continuation_budget jsonb,
5535
5632
  cancelled_at timestamptz,
5536
5633
  -- \u5C01\u5B58\u539F\u56E0\uFF08accepted/cancelled/frozen\uFF09\u3002NULL = \u5B58\u91CF\u884C\uFF0C\u8BFB\u6A21\u578B\u6309 cancelled \u515C\u5E95\u3002
5537
5634
  seal_reason text,
@@ -5539,6 +5636,7 @@ CREATE TABLE IF NOT EXISTS ${q2}.workflow_nodes (
5539
5636
  created_at timestamptz NOT NULL,
5540
5637
  updated_at timestamptz NOT NULL
5541
5638
  );
5639
+ ALTER TABLE ${q2}.workflow_nodes ADD COLUMN IF NOT EXISTS continuation_budget jsonb;
5542
5640
  CREATE INDEX IF NOT EXISTS nodes_workorder_idx ON ${q2}.workflow_nodes (workorder_id);
5543
5641
 
5544
5642
  CREATE TABLE IF NOT EXISTS ${q2}.workflow_edges (
@@ -11444,6 +11542,7 @@ var init_store_postgres = __esm({
11444
11542
  // NULL = 存量行(ADR 0137 迁移批量置的 cancelled_at):按 cancelled 读,不追溯改写历史。
11445
11543
  sealReason: n.seal_reason ?? null,
11446
11544
  fields: n.fields ?? void 0,
11545
+ continuationBudget: n.continuation_budget ?? null,
11447
11546
  rowVersion: n.row_version,
11448
11547
  createdAt: iso(n.created_at),
11449
11548
  updatedAt: iso(n.updated_at)
@@ -11625,8 +11724,8 @@ var init_store_postgres = __esm({
11625
11724
  `INSERT INTO ${this.s}.workflow_nodes
11626
11725
  (id,workorder_id,type,title,spec,version,assignee_actor_id,assignee_role,
11627
11726
  latest_work_id,latest_proposed_work_id,latest_accept_id,is_final_output,
11628
- fields,cancelled_at,seal_reason,row_version,created_at,updated_at)
11629
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
11727
+ fields,cancelled_at,seal_reason,row_version,created_at,updated_at,continuation_budget)
11728
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
11630
11729
  ON CONFLICT (id) DO NOTHING`,
11631
11730
  [
11632
11731
  n.id,
@@ -11646,13 +11745,14 @@ var init_store_postgres = __esm({
11646
11745
  n.sealReason ?? null,
11647
11746
  n.rowVersion,
11648
11747
  n.createdAt,
11649
- n.updatedAt
11748
+ n.updatedAt,
11749
+ n.continuationBudget ? JSON.stringify(n.continuationBudget) : null
11650
11750
  ]
11651
11751
  );
11652
11752
  break;
11653
11753
  }
11654
11754
  case "node.update":
11655
- await this.patch("workflow_nodes", "id", m2.id, m2.patch, { fields: true });
11755
+ await this.patch("workflow_nodes", "id", m2.id, m2.patch, { fields: true, continuationBudget: true });
11656
11756
  break;
11657
11757
  case "edge.insert": {
11658
11758
  const e = m2.row;
@@ -12076,7 +12176,7 @@ function newNode(id, workorderId, at, version2) {
12076
12176
  updatedAt: at
12077
12177
  };
12078
12178
  }
12079
- function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
12179
+ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false, resetCheckpointBudget = false) {
12080
12180
  const node2 = state.node(nodeId);
12081
12181
  if (!node2) return;
12082
12182
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
@@ -12085,6 +12185,9 @@ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
12085
12185
  }
12086
12186
  if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
12087
12187
  if (resetRejectedBudget && lw.acceptanceState === "rejected") state.updateWork(lw.id, { retryAt: at });
12188
+ if (resetCheckpointBudget && lw.conclusion == null && state.artifactsOf(lw.id).length > 0) {
12189
+ state.updateWork(lw.id, { retryAt: at });
12190
+ }
12088
12191
  for (const req of state.requirementsOf(node2.id)) {
12089
12192
  if (!req.latestReviewId) continue;
12090
12193
  const r = state.review(req.latestReviewId);
@@ -12216,7 +12319,7 @@ var init_plan = __esm({
12216
12319
  const changed = node2.assigneeActorId !== e.assigneeActorId;
12217
12320
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
12218
12321
  const settled = lw !== null && lw !== void 0 && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success";
12219
- if (changed && e.resetRejectedBudget === true && lw && !lw.deadAt && settled && lw.acceptanceState === "rejected") {
12322
+ if (changed && lw && !lw.deadAt && settled && (e.resetRejectedBudget === true && lw.acceptanceState === "rejected" || e.resetCheckpointBudget === true && lw.conclusion == null && state.artifactsOf(lw.id).length > 0)) {
12220
12323
  state.updateWork(lw.id, { retryAt: ctx.at });
12221
12324
  }
12222
12325
  state.updateNode(e.nodeId, {
@@ -12291,7 +12394,7 @@ var init_plan = __esm({
12291
12394
  name: "plan/node-retry",
12292
12395
  kind: "plan.node_retry",
12293
12396
  apply(e, state, ctx) {
12294
- markNodeForRetry(state, e.nodeId, ctx.at, e.resetRejectedBudget === true);
12397
+ markNodeForRetry(state, e.nodeId, ctx.at, e.resetRejectedBudget === true, e.resetCheckpointBudget === true);
12295
12398
  }
12296
12399
  };
12297
12400
  planUpdateFields = {
@@ -12429,7 +12532,8 @@ var init_workorder = __esm({
12429
12532
  workorderId: e.workorderId,
12430
12533
  nodeId: node2.id,
12431
12534
  by: e.by,
12432
- resetRejectedBudget: e.resetRejectedBudget
12535
+ resetRejectedBudget: e.resetRejectedBudget,
12536
+ resetCheckpointBudget: e.resetCheckpointBudget
12433
12537
  }, state, ctx);
12434
12538
  }
12435
12539
  }
@@ -12452,6 +12556,7 @@ var workCreate, workRedispatch, workSnapshotRecorded, workKill, workResponse, wo
12452
12556
  var init_work = __esm({
12453
12557
  "../engine/src/handlers/work.ts"() {
12454
12558
  "use strict";
12559
+ init_continuation();
12455
12560
  init_views2();
12456
12561
  init_kill();
12457
12562
  init_letters();
@@ -12500,6 +12605,7 @@ var init_work = __esm({
12500
12605
  });
12501
12606
  return;
12502
12607
  }
12608
+ if (e.continuationGateVersion === 1 && continuationExhausted(state, node2)) return;
12503
12609
  supersedeLatestWork(state, e.nodeId, ctx.at);
12504
12610
  killNodeReviews(state, e.nodeId, ctx.at);
12505
12611
  for (const w2 of state.replyWorksOf(e.nodeId)) {
@@ -12980,7 +13086,7 @@ var init_issue = __esm({
12980
13086
  }
12981
13087
  if (issue2.kind === "gap" || issue2.kind === "escalation") {
12982
13088
  const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
12983
- if (nodeId) markNodeForRetry(state, nodeId, ctx.at, e.resetRejectedBudget === true);
13089
+ if (nodeId) markNodeForRetry(state, nodeId, ctx.at, e.resetRejectedBudget === true, e.resetCheckpointBudget === true);
12984
13090
  }
12985
13091
  }
12986
13092
  };
@@ -13181,6 +13287,10 @@ var init_review = __esm({
13181
13287
  }
13182
13288
  return;
13183
13289
  }
13290
+ const budget = state.node(w2.nodeId)?.continuationBudget;
13291
+ if (budget?.request && (budget.request.decision === "pending" || budget.request.decision === "denied")) {
13292
+ state.updateNode(w2.nodeId, { continuationBudget: { ...budget, request: { ...budget.request, decision: "completed" } } });
13293
+ }
13184
13294
  state.updateNode(w2.nodeId, { latestAcceptId: e.workId, updatedAt: ctx.at });
13185
13295
  state.updateWork(e.workId, {
13186
13296
  acceptanceState: "accepted",
@@ -13300,6 +13410,67 @@ var init_review = __esm({
13300
13410
  }
13301
13411
  });
13302
13412
 
13413
+ // ../engine/src/handlers/continuation.ts
13414
+ var validRounds, continuationInitialize, continuationRequest, continuationDecide;
13415
+ var init_continuation2 = __esm({
13416
+ "../engine/src/handlers/continuation.ts"() {
13417
+ "use strict";
13418
+ init_continuation();
13419
+ validRounds = (n) => Number.isSafeInteger(n) && n >= 1 && n <= 100;
13420
+ continuationInitialize = {
13421
+ name: "continuation/initialize",
13422
+ kind: "continuation.initialized",
13423
+ apply(e, state) {
13424
+ const node2 = state.node(e.nodeId);
13425
+ if (!node2 || node2.continuationBudget || !validRounds(e.rounds)) return;
13426
+ state.updateNode(node2.id, { continuationBudget: { authorizedRounds: e.rounds, requestGeneration: 0, roundsPerGrant: e.rounds, request: null } });
13427
+ }
13428
+ };
13429
+ continuationRequest = {
13430
+ name: "continuation/request",
13431
+ kind: "continuation.requested",
13432
+ apply(e, state, ctx) {
13433
+ const node2 = state.node(e.nodeId);
13434
+ if (!node2 || node2.cancelledAt || !node2.continuationBudget || !validRounds(e.rounds) || continuationRequestId(node2) !== e.requestId || !continuationExhausted(state, node2) || node2.continuationBudget.request?.decision === "pending" || node2.continuationBudget.request?.decision === "denied") return;
13435
+ const latest = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
13436
+ if (latest && !latest.endedAt && !latest.deadAt && !continuationAwaitingStart(state, latest)) return;
13437
+ state.updateNode(node2.id, { continuationBudget: {
13438
+ ...node2.continuationBudget,
13439
+ requestGeneration: node2.continuationBudget.requestGeneration + 1,
13440
+ request: {
13441
+ id: e.requestId,
13442
+ at: ctx.at,
13443
+ rounds: e.rounds,
13444
+ usedRounds: continuationUsedRounds(state, node2.id),
13445
+ summary: continuationSummary(state, node2, e.rounds),
13446
+ decision: "pending"
13447
+ }
13448
+ } });
13449
+ }
13450
+ };
13451
+ continuationDecide = {
13452
+ name: "continuation/decide",
13453
+ kind: "continuation.decided",
13454
+ apply(e, state, ctx) {
13455
+ const node2 = state.node(e.nodeId);
13456
+ const budget = node2?.continuationBudget;
13457
+ const request2 = budget?.request;
13458
+ if (!node2 || node2.cancelledAt || !budget || !request2 || request2.id !== e.requestId || request2.decision !== "pending" || !ctx.actorId.startsWith("actor:human:") || ctx.actorId !== continuationApprover(state)) return;
13459
+ state.updateNode(node2.id, { continuationBudget: {
13460
+ ...budget,
13461
+ authorizedRounds: e.approve ? Math.max(budget.authorizedRounds, continuationUsedRounds(state, node2.id)) + request2.rounds : budget.authorizedRounds,
13462
+ roundsPerGrant: request2.rounds,
13463
+ request: { ...request2, decision: e.approve ? "approved" : "denied", decidedBy: ctx.actorId, decidedAt: ctx.at, reason: e.reason }
13464
+ } });
13465
+ const latest = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
13466
+ if (e.approve && latest && continuationAwaitingStart(state, latest)) {
13467
+ state.emit({ kind: "work.redispatch", workorderId: e.workorderId, workId: latest.id });
13468
+ }
13469
+ }
13470
+ };
13471
+ }
13472
+ });
13473
+
13303
13474
  // ../engine/src/handlers/index.ts
13304
13475
  var HANDLERS;
13305
13476
  var init_handlers = __esm({
@@ -13311,6 +13482,7 @@ var init_handlers = __esm({
13311
13482
  init_work();
13312
13483
  init_review();
13313
13484
  init_issue();
13485
+ init_continuation2();
13314
13486
  init_types();
13315
13487
  HANDLERS = [
13316
13488
  /* ── 工作区级(5)── */
@@ -13331,6 +13503,9 @@ var init_handlers = __esm({
13331
13503
  erase(planUpdateReviewRequirements),
13332
13504
  erase(planNodeRetry),
13333
13505
  erase(planUpdateFields),
13506
+ erase(continuationInitialize),
13507
+ erase(continuationRequest),
13508
+ erase(continuationDecide),
13334
13509
  /* ── 节点级 work(7)── */
13335
13510
  erase(workCreate),
13336
13511
  erase(workRedispatch),
@@ -13365,7 +13540,7 @@ var init_handlers = __esm({
13365
13540
  // ../engine/src/bus.ts
13366
13541
  function versionCommandEvent(event) {
13367
13542
  if (event.kind === "work.create" || event.kind === "review.create") {
13368
- return { ...event, pauseGateVersion: 1 };
13543
+ return { ...event, pauseGateVersion: 1, ...event.kind === "work.create" ? { continuationGateVersion: 1 } : {} };
13369
13544
  }
13370
13545
  if (event.kind === "issue.reply") return { ...event, handlingVersion: 1 };
13371
13546
  if (event.kind === "work.response" && event.semanticsVersion === 2) {
@@ -13379,11 +13554,11 @@ function versionCommandEvent(event) {
13379
13554
  case "work.handoff_recorded":
13380
13555
  return { ...event, semanticsVersion: 2 };
13381
13556
  case "issue.resolve":
13382
- return { ...event, semanticsVersion: 2, resetRejectedBudget: true };
13557
+ return { ...event, semanticsVersion: 2, resetRejectedBudget: true, resetCheckpointBudget: true };
13383
13558
  case "plan.node_retry":
13384
13559
  case "workorder.resumed":
13385
13560
  case "plan.assign_actor":
13386
- return { ...event, resetRejectedBudget: true };
13561
+ return { ...event, resetRejectedBudget: true, resetCheckpointBudget: true };
13387
13562
  default:
13388
13563
  return event;
13389
13564
  }
@@ -13429,6 +13604,7 @@ var init_bus = __esm({
13429
13604
  init_model();
13430
13605
  init_handlers();
13431
13606
  init_state();
13607
+ init_continuation();
13432
13608
  SYSTEM_ACTOR = "actor:system:engine";
13433
13609
  NOOP_IO = {
13434
13610
  async dispatchWork() {
@@ -13915,6 +14091,21 @@ var init_bus = __esm({
13915
14091
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(workorderId));
13916
14092
  if (!snap || snap.workorder.dispatchPaused) return false;
13917
14093
  const work = snap.works.find((row) => row.id === workId);
14094
+ if (work && work.lane === "main" && !work.startedAt && !work.sessionRef && !work.lastActivityAt) {
14095
+ const node2 = snap.nodes.find((row) => row.id === work.nodeId);
14096
+ if (node2 && !node2.continuationBudget && this.io.continuationPolicy) {
14097
+ const policy = await this.io.continuationPolicy(work.assigneeActorId);
14098
+ if (policy) {
14099
+ if (!Number.isSafeInteger(policy.maxRounds) || policy.maxRounds < 1 || policy.maxRounds > 100) {
14100
+ throw new Error(`invalid continuation round limit for ${work.assigneeActorId}`);
14101
+ }
14102
+ buffered.push({ event: { kind: "continuation.initialized", workorderId, nodeId: node2.id, rounds: policy.maxRounds } });
14103
+ buffered.push({ event: { kind: "work.redispatch", workorderId, workId } });
14104
+ return false;
14105
+ }
14106
+ }
14107
+ if (node2 && continuationExhausted(new WorkorderState(snap), node2)) return false;
14108
+ }
13918
14109
  if (opts?.neverStarted && work) {
13919
14110
  const node2 = snap.nodes.find((row) => row.id === work.nodeId);
13920
14111
  if (work.startedAt || work.sessionRef || !node2 || node2.cancelledAt || node2.latestWorkId !== workId) return false;
@@ -14024,7 +14215,21 @@ var init_bus = __esm({
14024
14215
  if (!snapshot) return { emitted: 0, terminated: false };
14025
14216
  if (snapshot.workorder.lifecycle !== "active") return { emitted: 0, terminated: false };
14026
14217
  const state = new WorkorderState(snapshot);
14027
- const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR, config: this.config, policy: this.policyOf() };
14218
+ const continuationPolicies = /* @__PURE__ */ new Map();
14219
+ if (this.io.continuationPolicy) {
14220
+ for (const node2 of state.liveNodes()) {
14221
+ const budget = node2.continuationBudget;
14222
+ if (budget && (!continuationExhausted(state, node2) || budget.request?.decision === "pending" || budget.request?.decision === "denied")) continue;
14223
+ if (node2.assigneeActorId && !continuationPolicies.has(node2.assigneeActorId)) {
14224
+ const policy = await this.io.continuationPolicy(node2.assigneeActorId);
14225
+ if (policy && (!Number.isSafeInteger(policy.maxRounds) || policy.maxRounds < 1 || policy.maxRounds > 100)) {
14226
+ throw new Error(`invalid continuation round limit for ${node2.assigneeActorId}`);
14227
+ }
14228
+ continuationPolicies.set(node2.assigneeActorId, policy);
14229
+ }
14230
+ }
14231
+ }
14232
+ const scanCtx = { continuationPolicies, at: this.clock.now(), actorId: SYSTEM_ACTOR, config: this.config, policy: this.policyOf() };
14028
14233
  const { events, terminated } = scan(state, scanCtx);
14029
14234
  await tx.applyMutations(workorderId, state.mutations);
14030
14235
  const allEvents = [...state.emitted, ...events];
@@ -14159,6 +14364,7 @@ var init_src4 = __esm({
14159
14364
  "../engine/src/index.ts"() {
14160
14365
  "use strict";
14161
14366
  init_model();
14367
+ init_continuation();
14162
14368
  init_events();
14163
14369
  init_views2();
14164
14370
  init_state();
@@ -15296,6 +15502,7 @@ ${manifestDiff(baseC, newC)}
15296
15502
  let skillsLines = [];
15297
15503
  let identityNote = null;
15298
15504
  let batchedContinuation = false;
15505
+ let continuationMaxRounds = 3;
15299
15506
  let memoryEnabled = true;
15300
15507
  let executorRole = null;
15301
15508
  const executionActor = args.actorId ?? artifact.owner;
@@ -15303,6 +15510,7 @@ ${manifestDiff(baseC, newC)}
15303
15510
  const actorCtx = await args.resolveActorContext(executionActor).catch(() => null);
15304
15511
  if (actorCtx) {
15305
15512
  batchedContinuation = actorCtx.config?.batchedContinuation === true;
15513
+ continuationMaxRounds = actorCtx.config?.continuationMaxRounds ?? 3;
15306
15514
  memoryEnabled = actorCtx.memoryEnabled !== false;
15307
15515
  executorRole = actorCtx.roles?.[0] ?? null;
15308
15516
  if (actorCtx.config?.prompt) {
@@ -15674,7 +15882,7 @@ ${m2.spec.split("\n").map((l) => ` > ${l}`).join("\n")}`);
15674
15882
  `- \u7F3A\u4E00\u4E2A**\u8FD8\u4E0D\u5B58\u5728\u7684\u524D\u7F6E** \u2192 \`oasis gap\` \u4EA4\u4E0A\u53BB\uFF0C**\u522B\u786C\u7F16**\uFF1B\u5DF2\u505A\u5B8C\u7684\u90E8\u5206\u7167\u5E38 \`propose\`\uFF0C\u4F46\u8FD9\u4E00\u8F6E \`conclude\` \u4F1A\u88AB\u62D2\uFF0C\u63D0\u4EA4\u5B8C\u5373\u6536\u5DE5\u2014\u2014\u524D\u7F6E\u5C31\u7EEA\u540E\u7CFB\u7EDF\u4F1A\u5E26\u7740\u89E3\u6CD5\u8BF4\u660E\u91CD\u65B0\u5524\u8D77\u4F60\u3002`,
15675
15883
  `- \u5BF9**\u4E0A\u6E38**\u6709\u610F\u89C1\u2014\u2014\u6709\u7591\u95EE\u3001\u8981\u5B83\u6539\u3001\u5224\u5B83\u6CA1\u8FBE\u6807 \u2192 \`oasis annotate <\u4E0A\u6E38id> --body "<\u8BF4\u6E05\u695A>"\` \u540E**\u6536\u5DE5**\u3002\u5199\u7ED9\u4E0A\u6E38\u7684\u8BC4\u8BBA\u81EA\u52A8\u5E26\u963B\u585E\u529B\uFF1A**\u5B83\u7B54\u590D\u524D\u4E0D\u80FD\u6536\u5C3E**\uFF0C\u7B54\u590D\u540E\u7CFB\u7EDF\u91CD\u6D3E\u4F60\u3002`,
15676
15884
  ...args.part === void 0 ? [`- \u6D3B\u592A\u5927 / \u53EF\u5E76\u884C \u2192 \`oasis add-revision\` \u62C6\u6210\u5B50\u90E8\u4EF6\u6D3E\u51FA\u53BB\uFF0C\u522B\u4E00\u4EBA\u4E00\u4F1A\u8BDD\u786C\u625B\u3002`] : [],
15677
- ...batchedContinuation && args.part === void 0 ? [`- \u957F\u4EFB\u52A1\u5DF2\u5141\u8BB8\u5206\u6279\u7EED\u8DD1\uFF1A\u4E00\u6279\u6709\u8FDB\u5C55\u4F46\u6574\u4F53\u672A\u5B8C\u6210\u65F6\uFF0C\u6309\u300C\u600E\u4E48\u63D0\u4EA4\u300D\u91CC\u7684 propose \u547D\u4EE4\u8FFD\u52A0 \`--checkpoint --continue\`\uFF0Creason \u5199\u6E05\u672C\u6279\u5B8C\u6210\u4E86\u4EC0\u4E48\u3001\u5269\u4EC0\u4E48\u3001\u4E0B\u6279\u4ECE\u54EA\u7EE7\u7EED\uFF0C\u7136\u540E\u6536\u5DE5\uFF1B\u7CFB\u7EDF\u4F1A\u6D3E\u53D1\u4E0B\u4E00\u8F6E\u6B63\u5F0F session\u3002\u5168\u90E8\u5B8C\u6210\u65F6\u7167\u5E38 propose \u540E \`oasis conclude ${artifactId} --note "<\u4EA4\u4ED8\u7ED3\u8BBA>"\`\u3002`] : [],
15885
+ ...batchedContinuation && args.part === void 0 ? [`- \u957F\u4EFB\u52A1\u5DF2\u5141\u8BB8\u5206\u6279\u7EED\u8DD1\uFF1A\u4E00\u6279\u6709\u8FDB\u5C55\u4F46\u6574\u4F53\u672A\u5B8C\u6210\u65F6\uFF0C\u6309\u300C\u600E\u4E48\u63D0\u4EA4\u300D\u91CC\u7684 propose \u547D\u4EE4\u8FFD\u52A0 \`--checkpoint --continue\`\uFF0Creason \u5199\u6E05\u672C\u6279\u5B8C\u6210\u4E86\u4EC0\u4E48\u3001\u5269\u4EC0\u4E48\u3001\u4E0B\u6279\u4ECE\u54EA\u7EE7\u7EED\uFF0C\u7136\u540E\u6536\u5DE5\uFF1B\u7CFB\u7EDF\u5728\u6BCF\u6B21\u6388\u6743\u7684 ${continuationMaxRounds} \u8F6E\u989D\u5EA6\u5185\u6D3E\u53D1\u4E0B\u4E00\u8F6E\u6B63\u5F0F session\uFF08\u9996\u6B21\u6267\u884C\u4E5F\u8BA1\u4E00\u8F6E\uFF09\uFF0C\u5230\u9650\u5C1A\u672A conclude \u65F6\u6682\u505C\u5E76\u7B49\u5F85\u4EBA\u5DE5\u5BA1\u6279\uFF0C\u540C\u610F\u540E\u8FFD\u52A0\u540C\u6837\u989D\u5EA6\u3002\u7A7A\u4EA7\u51FA\u6216\u8FDE\u7EED\u91CD\u590D\u5DF2\u6709\u5FEB\u7167\u4ECD\u4F1A\u63D0\u524D\u6682\u505C\uFF0C\u9047\u5230\u963B\u585E\u5E94\u5982\u5B9E\u4E0A\u62A5\u3002\u5168\u90E8\u5B8C\u6210\u65F6\u7167\u5E38 propose \u540E \`oasis conclude ${artifactId} --note "<\u4EA4\u4ED8\u7ED3\u8BBA>"\`\u3002`] : [],
15678
15886
  `- \u5199\u5165\u53EA\u8D70 \`oasis propose\`\uFF0C\u771F\u6B63\u5B8C\u6574\u4E86\u624D \`oasis conclude\`\uFF1B\u5361\u6CA1\u5361\u4F4F\u3001\u8FDB\u5EA6\u90FD\u662F\u7CFB\u7EDF\u7B97\u7684\uFF0C\u522B\u81EA\u5DF1\u7EF4\u62A4\u3002`,
15679
15887
  `- \u4F46**\u7B80\u5355\u3001\u81EA\u8DB3\u3001\u4E0D\u4F9D\u8D56\u672A\u5B9A\u51B3\u7B56\u7684\u4EFB\u52A1**\uFF08\u5982\u6539\u4E2A\u80CC\u666F\u8272\uFF09\u2192 \u76F4\u63A5\u505A\uFF0C\u5224\u65AD\u6743\u5728\u4F60\u3002`,
15680
15888
  ...typeDef?.recommendedInputs && typeDef.recommendedInputs.length > 0 ? [`- \u672C\u7C7B\u578B\u5E38\u89C1\u4E0A\u6E38\u4F9D\u8D56\uFF08\u63A8\u8350\uFF0C\u975E\u5F3A\u5236\uFF09\uFF1A${typeDef.recommendedInputs.join("\u3001")}\u2014\u2014\u4F9D\u8D56\u5176\u4E2D\u67D0\u4E2A\u800C\u5B83\u8FD8\u6CA1\u6709\uFF0C\u6309\u4E0A\u9762 gap \u89C4\u5219\u62A5\u7F3A\u53E3\u3002`] : []
@@ -18793,6 +19001,36 @@ function bridgeLoadReadModel(input) {
18793
19001
  pinMeta.set(edge.toNodeId, record8);
18794
19002
  }
18795
19003
  const pendingReviews = /* @__PURE__ */ new Map();
19004
+ for (const snap of snapshots2) {
19005
+ let approver;
19006
+ for (const node2 of snap.nodes) {
19007
+ const request2 = node2.continuationBudget?.request;
19008
+ const latest = node2.latestWorkId ? workById.get(node2.latestWorkId) : void 0;
19009
+ if (!request2 || node2.cancelledAt || latest && !latest.endedAt && !latest.deadAt && (latest.startedAt || latest.sessionRef || latest.lastActivityAt || snap.artifacts.some((a) => a.workId === latest.id))) continue;
19010
+ if (request2.decision === "pending" || request2.decision === "denied") {
19011
+ const artifact = artifacts.get(node2.id);
19012
+ if (artifact) artifact.continuationPause = { decision: request2.decision, detail: request2.summary, at: request2.at };
19013
+ }
19014
+ if (request2.decision !== "pending") continue;
19015
+ if (approver === void 0) approver = continuationApprover(new WorkorderState(snap));
19016
+ pendingReviews.set(request2.id, {
19017
+ draftId: request2.id,
19018
+ continuation: { nodeId: node2.id, requestId: request2.id, approverActorId: approver },
19019
+ plan: { workspace: snap.workorder.id, reason: request2.summary, ops: [] },
19020
+ changeClass: "C",
19021
+ nonMonotonicOps: [],
19022
+ proposedBy: node2.assigneeActorId ?? snap.workorder.ownerActorId,
19023
+ origin: null,
19024
+ workspace: snap.workorder.id,
19025
+ advisory: [],
19026
+ downstreamClosure: [],
19027
+ at: request2.at,
19028
+ anchor: node2.id,
19029
+ kind: "command",
19030
+ command: { command: "continueBatch", args: { artifactId: node2.id, rounds: request2.rounds }, effectLabel: request2.summary }
19031
+ });
19032
+ }
19033
+ }
18796
19034
  const pointedReviewIds = new Set(
18797
19035
  allRequirements.map((r) => r.latestReviewId).filter((x2) => x2 != null)
18798
19036
  );
@@ -19392,6 +19630,7 @@ var import_node_crypto3, PENDING_ROLE_PREFIX2, PENDING_UNASSIGNED2, EngineKernel
19392
19630
  var init_kernel_bridge = __esm({
19393
19631
  "../core/src/kernel-bridge.ts"() {
19394
19632
  "use strict";
19633
+ init_src4();
19395
19634
  import_node_crypto3 = require("node:crypto");
19396
19635
  init_read_model();
19397
19636
  init_closure_gate2();
@@ -21183,7 +21422,35 @@ var init_kernel_bridge = __esm({
21183
21422
  await this.refreshModel();
21184
21423
  return { draftId };
21185
21424
  }
21425
+ async decideContinuation(draftId, actor, approve, reason) {
21426
+ const review = this.model.pendingReviews.get(draftId);
21427
+ if (!review?.continuation || !review.workspace) throw new Error("\u7EED\u8DD1\u5BA1\u6279\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406");
21428
+ const snapshot = await this.store.transaction((tx) => tx.loadWorkorder(review.workspace));
21429
+ const node2 = snapshot?.nodes.find((n) => n.id === review.continuation.nodeId);
21430
+ const request2 = node2?.continuationBudget?.request;
21431
+ if (!actor.startsWith("actor:human:") || !snapshot || continuationApprover(new WorkorderState(snapshot)) !== actor) throw new Error("\u4EC5\u5DE5\u5355\u4EBA\u7C7B\u8D1F\u8D23\u4EBA\u53EF\u4EE5\u5BA1\u6279\u7EED\u8DD1\u989D\u5EA6");
21432
+ if (!request2 || request2.id !== draftId || request2.decision !== "pending" || node2?.cancelledAt) throw new Error("\u7EED\u8DD1\u5BA1\u6279\u5DF2\u5904\u7406\u6216\u5931\u6548");
21433
+ const expectedAuthorized = Math.max(
21434
+ node2.continuationBudget.authorizedRounds,
21435
+ continuationUsedRounds(new WorkorderState(snapshot), node2.id)
21436
+ ) + request2.rounds;
21437
+ await this.commit({
21438
+ companyId: snapshot.workorder.companyId,
21439
+ workorderId: snapshot.workorder.id,
21440
+ actorId: actor,
21441
+ event: { kind: "continuation.decided", workorderId: snapshot.workorder.id, nodeId: node2.id, requestId: draftId, approve, reason }
21442
+ });
21443
+ const after = await this.store.transaction((tx) => tx.loadWorkorder(snapshot.workorder.id));
21444
+ const afterBudget = after?.nodes.find((n) => n.id === node2.id)?.continuationBudget;
21445
+ const decided = afterBudget?.request;
21446
+ const applied = approve ? (afterBudget?.authorizedRounds ?? 0) >= expectedAuthorized : decided?.id === draftId && (decided.decision === "denied" || decided.decision === "completed");
21447
+ if (!applied) throw new Error("\u7EED\u8DD1\u5BA1\u6279\u53D1\u751F\u51B2\u7A81\uFF0C\u8BF7\u5237\u65B0\u540E\u67E5\u770B\u7ED3\u679C");
21448
+ return node2.id;
21449
+ }
21186
21450
  async applyAuthorizedCommand(args) {
21451
+ if (this.model.pendingReviews.get(args.draftId)?.continuation) {
21452
+ return { command: "continueBatch", artifactId: await this.decideContinuation(args.draftId, args.actor, true) };
21453
+ }
21187
21454
  const card2 = this.draftCards.get(args.draftId);
21188
21455
  if (!card2 || card2.kind !== "command" || !card2.command) {
21189
21456
  throw new Error(`\u4E0D\u662F\u547D\u4EE4\u6388\u6743\u8BF7\u6C42\u6216\u5DF2\u5904\u7406\uFF1A${args.draftId}`);
@@ -21213,6 +21480,10 @@ var init_kernel_bridge = __esm({
21213
21480
  return { command: card2.command, artifactId };
21214
21481
  }
21215
21482
  async rejectIntervention(args) {
21483
+ if (this.model.pendingReviews.get(args.draftId)?.continuation) {
21484
+ await this.decideContinuation(args.draftId, args.actor, false, args.reason);
21485
+ return;
21486
+ }
21216
21487
  this.draftCards.delete(args.draftId);
21217
21488
  await this.forgetDraftCard(args.draftId);
21218
21489
  await this.refreshModel();
@@ -142917,6 +143188,7 @@ function shortId2(id) {
142917
143188
  function stallOf(model, id, runtime, dispatcherSignals) {
142918
143189
  const a = model.artifacts.get(id);
142919
143190
  if (!a) return null;
143191
+ if (a.continuationPause) return a.continuationPause.decision === "pending" ? { kind: "waiting_human", action: "discuss", ref: id, detail: a.continuationPause.detail, since: a.continuationPause.at } : { kind: "held", detail: "\u5DF2\u62D2\u7EDD\u8FFD\u52A0\u81EA\u52A8\u7EED\u8DD1\u989D\u5EA6\uFF0C\u8282\u70B9\u4FDD\u6301\u6682\u505C\uFF1B\u6210\u679C\u5DF2\u4FDD\u7559\u3002", since: a.continuationPause.at };
142920
143192
  const ownerIsAgent = a.owner.startsWith("actor:agent:");
142921
143193
  const escs = unresolvedEscalationsOf(model, id);
142922
143194
  if (escs.length > 0) {
@@ -143072,7 +143344,7 @@ function nodeInfo(model, a) {
143072
143344
  const hasUnproducedParts = (a.parts ?? []).some((p2) => !producedParts.has(p2.name));
143073
143345
  const blockedByIssue = unresolvedGapsOf(model, a.id).length > 0 || unresolvedEscalationsOf(model, a.id).length > 0;
143074
143346
  let mark;
143075
- if (!sealed && blockedByIssue) {
143347
+ if (!sealed && (blockedByIssue || a.continuationPause)) {
143076
143348
  mark = "blocked";
143077
143349
  } else if (a.latestWorkState !== void 0) {
143078
143350
  const st = a.latestWorkState;
@@ -143129,6 +143401,7 @@ function deriveStage(members, opts) {
143129
143401
  }
143130
143402
  function nodeRunState(model, artifact, info, runtime, opts) {
143131
143403
  if (info.sealed) return void 0;
143404
+ if (artifact.continuationPause) return artifact.continuationPause.decision === "pending" ? "pending" : "blocked";
143132
143405
  const id = artifact.id;
143133
143406
  const now = opts?.now ?? Date.now();
143134
143407
  const rt = runtime ?? {};
@@ -161536,6 +161809,17 @@ var init_workorder_drafts = __esm({
161536
161809
  async forWorkspace(workspace) {
161537
161810
  return [...this.drafts.values()].reverse().find((d) => d.workspace === workspace);
161538
161811
  }
161812
+ /**
161813
+ * 按 workspace + **版本号**取某一版(2026-09-11:对话流里一版一张卡,右侧面板也按版本定位,
161814
+ * 于是「人在面板里改负责人/审核人再保存」这件事必须写回**他正在看的那一版**)。
161815
+ *
161816
+ * ⚠ 别用 `forWorkspace` 顶替它:那个取的是链上**最新**一版。看着 V1 改了一笔、却写进 V3,
161817
+ * 等于用旧图把新图整个盖掉——V2/V3 的内容会无声消失(2026-09-11 发起人报的「节点图要隔离」)。
161818
+ * 找不到那一版时回 undefined,由调用方判 404,不许静默回落到最新版。
161819
+ */
161820
+ async forWorkspaceVersion(workspace, version2) {
161821
+ return [...this.drafts.values()].find((d) => d.workspace === workspace && d.version === version2);
161822
+ }
161539
161823
  /** 按 chat 会话取最新一份:agent 不自带 ws(后端生成)时,friday 据本会话 id 取回它刚建的草案。 */
161540
161824
  async forSession(chatSessionId) {
161541
161825
  return [...this.drafts.values()].reverse().find((d) => d.chatSessionId === chatSessionId);
@@ -162582,8 +162866,8 @@ async function buildDynamicWorkorderPlan(input) {
162582
162866
  const explicit = node2.owner ? await validateExplicitOwner(input.registry, node2.owner, node2.type) : null;
162583
162867
  if (explicit?.issue) issues.push(explicit.issue);
162584
162868
  const owner = explicit?.actor ? { role: def.ownerRole ?? "", actor: explicit.actor } : def.ownerRole ? await resolveRole(def.ownerRole, node2.type) : null;
162585
- const staffed = owner?.actor && staffableTypes.has(node2.type);
162586
- if (enforceStaffing && def.ownerRole && !staffed && !input.retainUnstaffedNodes) continue;
162869
+ const staffable = staffableTypes.has(node2.type);
162870
+ if (enforceStaffing && def.ownerRole && !staffable && !input.retainUnstaffedNodes) continue;
162587
162871
  const ownerActor = owner?.actor ?? (def.ownerRole ? `pending:role:${def.ownerRole}` : PENDING_UNASSIGNED);
162588
162872
  availableNodes.push({
162589
162873
  type: node2.type,
@@ -162782,7 +163066,18 @@ async function validateExplicitOwner(registry2, owner, artifactType) {
162782
163066
  return { actor: owner };
162783
163067
  }
162784
163068
  function pickPreferred(actors) {
162785
- return [...actors].sort((a, b2) => a.kind !== b2.kind ? a.kind === "agent" ? -1 : 1 : a.id.localeCompare(b2.id))[0];
163069
+ return actors.filter((a) => a.kind === "agent").sort((a, b2) => a.id.localeCompare(b2.id))[0];
163070
+ }
163071
+ async function autoAssignableRoles(registry2) {
163072
+ const out = /* @__PURE__ */ new Set();
163073
+ if (!registry2) return out;
163074
+ for (const actor of await registry2.listActors({ status: "active" })) {
163075
+ if (actor.kind !== "agent") continue;
163076
+ const binding = await registry2.getBinding(actor.id);
163077
+ if (!binding || binding.status !== "active") continue;
163078
+ for (const role of actor.roles) out.add(role);
163079
+ }
163080
+ return out;
162786
163081
  }
162787
163082
  async function resolveActiveRole(registry2, role, artifactType, roles) {
162788
163083
  if (!registry2) {
@@ -162818,7 +163113,10 @@ async function resolveActiveRole(registry2, role, artifactType, roles) {
162818
163113
  };
162819
163114
  }
162820
163115
  const preferred = pickPreferred(actors);
163116
+ const nodeWord = artifactType ? `${artifactType} \u8282\u70B9` : "\u5BF9\u5E94\u8282\u70B9";
162821
163117
  if (!preferred) {
163118
+ const humans = actors.filter((a) => a.kind === "human");
163119
+ const message = humans.length > 0 ? `${role} \u5C97\u4F4D\u5F53\u524D\u53EA\u6709\u771F\u4EBA\uFF08${humans.map((h) => h.name).join("\u3001")}\uFF09\u3001\u6CA1\u6709\u53EF\u81EA\u52A8\u63A5\u5355\u7684 agent\u2014\u2014${nodeWord}\u843D\u5F85\u5C97\uFF0C\u8BF7\u5728\u56FE\u4E0A\u6307\u5B9A\u8D1F\u8D23\u4EBA\uFF0C\u6216\u7ED9\u8BE5\u5C97\u4F4D\u914D\u4E00\u4E2A agent\u3002` : `\u7F3A\u5C11 active \u7684 ${role} \u5458\u5DE5\uFF0C${nodeWord}\u4E0D\u4F1A\u81EA\u52A8\u6D3E\u53D1\u3002`;
162822
163120
  return {
162823
163121
  role,
162824
163122
  actor: null,
@@ -162827,27 +163125,25 @@ async function resolveActiveRole(registry2, role, artifactType, roles) {
162827
163125
  severity: "blocking",
162828
163126
  role,
162829
163127
  ...artifactType ? { artifactType } : {},
162830
- message: `\u7F3A\u5C11 active \u7684 ${role} \u5458\u5DE5\uFF0C${artifactType ? `${artifactType} \u8282\u70B9` : "\u5BF9\u5E94\u8282\u70B9"}\u4E0D\u4F1A\u81EA\u52A8\u6D3E\u53D1\u3002`
163128
+ message
162831
163129
  }
162832
163130
  };
162833
163131
  }
162834
- if (preferred.kind === "agent") {
162835
- const binding = await registry2.getBinding(preferred.id);
162836
- if (!binding || binding.status !== "active") {
162837
- return {
163132
+ const binding = await registry2.getBinding(preferred.id);
163133
+ if (!binding || binding.status !== "active") {
163134
+ return {
163135
+ role,
163136
+ actor: null,
163137
+ issue: {
163138
+ code: "missing_binding",
163139
+ severity: "blocking",
162838
163140
  role,
162839
- actor: null,
162840
- issue: {
162841
- code: "missing_binding",
162842
- severity: "blocking",
162843
- role,
162844
- ...artifactType ? { artifactType } : {},
162845
- // 措辞对两条路都成立:规则路会丢节点、agent/剧本路保留为待岗——**都是「暂不自动派发」**,
162846
- // 而非「暂不创建」(后者在保留待岗后就是假话)。
162847
- message: `${preferred.name}\uFF08${preferred.id}\uFF09\u7F3A\u5C11 active runtime \u7ED1\u5B9A\uFF0C${artifactType ? `${artifactType} \u8282\u70B9` : "\u5BF9\u5E94\u8282\u70B9"}\u6682\u4E0D\u81EA\u52A8\u6D3E\u53D1\uFF08\u5F85\u8865\u9F50\u540E\u81EA\u52A8\u63A5\u624B\uFF09\u3002`
162848
- }
162849
- };
162850
- }
163141
+ ...artifactType ? { artifactType } : {},
163142
+ // 措辞对两条路都成立:规则路会丢节点、agent/剧本路保留为待岗——**都是「暂不自动派发」**,
163143
+ // 而非「暂不创建」(后者在保留待岗后就是假话)。
163144
+ message: `${preferred.name}\uFF08${preferred.id}\uFF09\u7F3A\u5C11 active runtime \u7ED1\u5B9A\uFF0C${nodeWord}\u6682\u4E0D\u81EA\u52A8\u6D3E\u53D1\uFF08\u5F85\u8865\u9F50\u540E\u81EA\u52A8\u63A5\u624B\uFF09\u3002`
163145
+ }
163146
+ };
162851
163147
  }
162852
163148
  return { role, actor: preferred.id };
162853
163149
  }
@@ -163274,7 +163570,12 @@ function planningStatusOf(_model, arts, resolveRoleExecutor) {
163274
163570
  const role = a.owner.slice(PENDING_ROLE_PREFIX.length);
163275
163571
  if (seenRoles.has(role)) continue;
163276
163572
  seenRoles.add(role);
163277
- issues.push({ code: "missing_role", severity: "warning", role, message: `${role} \u89D2\u8272\u65E0 active \u5458\u5DE5\uFF0C\u76F8\u5173\u8282\u70B9\u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216 assign\u3002` });
163573
+ issues.push({
163574
+ code: "missing_role",
163575
+ severity: "warning",
163576
+ role,
163577
+ message: `${role} \u5C97\u4F4D\u6CA1\u6709\u53EF\u81EA\u52A8\u63A5\u5355\u7684 agent\uFF0C\u76F8\u5173\u8282\u70B9\u5F85\u6D3E\u2014\u2014\u8BF7 assign \u6307\u5B9A\u8D1F\u8D23\u4EBA\uFF0C\u6216\u7ED9\u8BE5\u5C97\u4F4D\u914D\u4E00\u4E2A agent\u3002`
163578
+ });
163278
163579
  }
163279
163580
  if (resolveRoleExecutor) {
163280
163581
  for (const a of arts) {
@@ -163322,7 +163623,12 @@ function draftPlanningStatusOf(spawns, isRoleStaffed, extraBlockingIssues = [])
163322
163623
  if (isRoleStaffed(role)) continue;
163323
163624
  if (seenRoles.has(role)) continue;
163324
163625
  seenRoles.add(role);
163325
- issues.push({ code: "missing_role", severity: "warning", role, message: `${role} \u89D2\u8272\u65E0 active \u5458\u5DE5\uFF0C\u76F8\u5173\u8282\u70B9\u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216 assign\u3002` });
163626
+ issues.push({
163627
+ code: "missing_role",
163628
+ severity: "warning",
163629
+ role,
163630
+ message: `${role} \u5C97\u4F4D\u6CA1\u6709\u53EF\u81EA\u52A8\u63A5\u5355\u7684 agent\uFF0C\u76F8\u5173\u8282\u70B9\u5F85\u6D3E\u2014\u2014\u8BF7\u5728\u56FE\u4E0A\u6307\u5B9A\u8D1F\u8D23\u4EBA\uFF0C\u6216\u7ED9\u8BE5\u5C97\u4F4D\u914D\u4E00\u4E2A agent\u3002`
163631
+ });
163326
163632
  }
163327
163633
  }
163328
163634
  for (const extra of extraBlockingIssues) {
@@ -164394,7 +164700,7 @@ async function planChatContinuation(input) {
164394
164700
  };
164395
164701
  }
164396
164702
  var UNFINISHED_OPEN, UNFINISHED_EMPTY;
164397
- var init_continuation = __esm({
164703
+ var init_continuation3 = __esm({
164398
164704
  "../server/src/domains/chat-sessions/continuation.ts"() {
164399
164705
  "use strict";
164400
164706
  init_injected_context();
@@ -194988,6 +195294,7 @@ var init_curator_skill = __esm({
194988
195294
  skills,
194989
195295
  connectorIds,
194990
195296
  maxConcurrent: 4,
195297
+ ...currentConfig?.continuationMaxRounds !== void 0 ? { continuationMaxRounds: currentConfig.continuationMaxRounds } : {},
194991
195298
  ...currentConfig?.batchedContinuation !== void 0 ? { batchedContinuation: currentConfig.batchedContinuation } : {}
194992
195299
  }, by);
194993
195300
  }
@@ -202780,33 +203087,13 @@ async function draftActorResolver(registry2) {
202780
203087
  }
202781
203088
  async function draftRoleStaffingResolver(registry2) {
202782
203089
  if (!registry2) return () => true;
202783
- const actors = await registry2.listActors();
202784
- const staffedRoles = /* @__PURE__ */ new Set();
202785
- for (const a of actors) if (a.status === "active") for (const r of a.roles) staffedRoles.add(r);
202786
- return (role) => staffedRoles.has(role);
203090
+ const assignable = await autoAssignableRoles(registry2);
203091
+ return (role) => assignable.has(role);
202787
203092
  }
202788
203093
  async function draftRolePlannableResolver(registry2) {
202789
203094
  if (!registry2) return async () => true;
202790
- const actors = await registry2.listActors();
202791
- const activeActorsByRole = /* @__PURE__ */ new Map();
202792
- for (const a of actors) {
202793
- if (a.status !== "active") continue;
202794
- for (const r of a.roles) {
202795
- const list2 = activeActorsByRole.get(r) ?? [];
202796
- list2.push(a);
202797
- activeActorsByRole.set(r, list2);
202798
- }
202799
- }
202800
- return async (role) => {
202801
- const list2 = activeActorsByRole.get(role);
202802
- if (!list2 || list2.length === 0) return false;
202803
- for (const a of list2) {
202804
- if (a.kind !== "agent") return true;
202805
- const binding = await registry2.getBinding(a.id);
202806
- if (binding && binding.status === "active") return true;
202807
- }
202808
- return false;
202809
- };
203095
+ const assignable = await autoAssignableRoles(registry2);
203096
+ return async (role) => assignable.has(role);
202810
203097
  }
202811
203098
  async function draftTypeReviewerResolver(schema, registry2) {
202812
203099
  if (!schema || !registry2) return void 0;
@@ -203118,6 +203405,8 @@ async function resolveProposeContent(kernel, blobs, args, artifactId, ctx) {
203118
203405
  }
203119
203406
  function authorizedCommandReceipt(command, args, actor) {
203120
203407
  switch (command) {
203408
+ case "continueBatch":
203409
+ return { message: `\u5DF2\u6388\u6743\u518D\u8FD0\u884C ${args.rounds} \u8F6E\uFF1B\u5176\u4ED6\u963B\u585E\u89E3\u9664\u540E\u7EE7\u7EED\u3002` };
203121
203410
  case "forceConclude":
203122
203411
  return { message: `force_conclude ${str(args, "artifactId")}\u2014\u2014\u91CC\u7A0B\u7891\u5E26 forced(by=${actor}) \u6807\u8BB0\uFF0C\u6D88\u8D39\u8005\u53EF\u89C1` };
203123
203412
  case "promote":
@@ -205608,11 +205897,7 @@ async function startOasisServer(opts) {
205608
205897
  return;
205609
205898
  }
205610
205899
  }
205611
- const staffedRoles = /* @__PURE__ */ new Set();
205612
- if (principalRegistry) {
205613
- const actorsForStaffing = await principalRegistry.listActors();
205614
- for (const a of actorsForStaffing) if (a.status === "active") for (const r of a.roles) staffedRoles.add(r);
205615
- }
205900
+ const staffedRoles = await autoAssignableRoles(principalRegistry);
205616
205901
  const spawnedForPlanning = body2.plan.ops.filter((op) => op.action === "spawn").filter((op) => op.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE).map((op) => {
205617
205902
  const title = op.title ?? op.description;
205618
205903
  return {
@@ -206021,7 +206306,7 @@ async function startOasisServer(opts) {
206021
206306
  }
206022
206307
  if (url.pathname === "/api/workorder/draft/plan" && req.method === "POST" && wodrafts) {
206023
206308
  try {
206024
- const { workspace, plan, holder } = JSON.parse(rawBody);
206309
+ const { workspace, plan, holder, version: version2 } = JSON.parse(rawBody);
206025
206310
  if (!workspace || !plan || !Array.isArray(plan.ops)) throw new Error("\u9700\u8981 { workspace, plan:{ops:[]} }");
206026
206311
  const planLockedBy = wodrafts.editLockHolder(workspace);
206027
206312
  if (planLockedBy && planLockedBy !== holder) {
@@ -206030,9 +206315,10 @@ async function startOasisServer(opts) {
206030
206315
  );
206031
206316
  return;
206032
206317
  }
206033
- const draft = await wodrafts.forWorkspace(workspace);
206318
+ const draft = version2 === void 0 ? await wodrafts.forWorkspace(workspace) : await wodrafts.forWorkspaceVersion(workspace, version2);
206034
206319
  if (!draft) {
206035
- res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: `no staged workorder draft for ${workspace}` }));
206320
+ const what = version2 === void 0 ? workspace : `${workspace} v${version2}`;
206321
+ res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: `no staged workorder draft for ${what}` }));
206036
206322
  return;
206037
206323
  }
206038
206324
  const pv = await engine.kernel.previewIntervention({ ...plan, reason: plan.reason || "(draft edit)" });
@@ -206302,7 +206588,7 @@ async function startOasisServer(opts) {
206302
206588
  if (viewName === "intervention-drafts") {
206303
206589
  const origin = url.searchParams.get("origin");
206304
206590
  const workspace = url.searchParams.get("workspace") ?? void 0;
206305
- const reviews = listPendingReviews(engine.kernel.model, origin === null ? workspace : void 0).filter((r) => origin === null ? true : r.origin === origin);
206591
+ const reviews = listPendingReviews(engine.kernel.model, origin === null ? workspace : void 0).filter((r) => origin === null ? true : r.origin === origin).filter((r) => !r.continuation || r.continuation.approverActorId === actor);
206306
206592
  const data2 = reviews.map((r) => ({
206307
206593
  id: r.draftId,
206308
206594
  plan: r.plan,
@@ -206320,7 +206606,8 @@ async function startOasisServer(opts) {
206320
206606
  anchor: r.anchor,
206321
206607
  // 提案 coordinator-command-authorization:命令授权卡的种类 + 命令本体(前端据此渲成命令卡)。
206322
206608
  ...r.kind !== void 0 ? { kind: r.kind } : {},
206323
- ...r.command !== void 0 ? { command: r.command } : {}
206609
+ ...r.command !== void 0 ? { command: r.command } : {},
206610
+ ...r.continuation ? { continuation: r.continuation } : {}
206324
206611
  }));
206325
206612
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ data: data2 }));
206326
206613
  return;
@@ -208456,7 +208743,7 @@ var init_server3 = __esm({
208456
208743
  init_planner();
208457
208744
  init_chat_parts();
208458
208745
  init_injected_context();
208459
- init_continuation();
208746
+ init_continuation3();
208460
208747
  init_chat_turn_gate();
208461
208748
  init_chat_workorder_files();
208462
208749
  init_chat_item_ledger();
@@ -208469,7 +208756,7 @@ var init_server3 = __esm({
208469
208756
  init_assembly();
208470
208757
  init_child_live_turn();
208471
208758
  init_src8();
208472
- init_continuation();
208759
+ init_continuation3();
208473
208760
  githubAppPending = new PendingAppCreations();
208474
208761
  connectorAuthSnapshots = /* @__PURE__ */ new Map();
208475
208762
  CONNECTOR_AUTH_SNAPSHOT_TTL_MS = 30 * 60 * 1e3;
@@ -220006,6 +220293,7 @@ var init_service5 = __esm({
220006
220293
  connectorIds: patch.connectorIds ?? latest?.connectorIds ?? [],
220007
220294
  maxConcurrent: patch.maxConcurrent ?? latest?.maxConcurrent ?? 4,
220008
220295
  batchedContinuation: patch.batchedContinuation ?? latest?.batchedContinuation ?? false,
220296
+ continuationMaxRounds: patch.continuationMaxRounds ?? latest?.continuationMaxRounds ?? 3,
220009
220297
  extraArgs: patch.extraArgs ?? latest?.extraArgs ?? [],
220010
220298
  createdBy: by,
220011
220299
  createdAt: this.now(),
@@ -220033,6 +220321,7 @@ var init_service5 = __esm({
220033
220321
  connectorIds: [...target.connectorIds],
220034
220322
  maxConcurrent: target.maxConcurrent,
220035
220323
  batchedContinuation: target.batchedContinuation ?? false,
220324
+ continuationMaxRounds: target.continuationMaxRounds ?? 3,
220036
220325
  extraArgs: [...target.extraArgs ?? []],
220037
220326
  createdBy: by,
220038
220327
  createdAt: this.now(),
@@ -220921,12 +221210,13 @@ ${input.description}
220921
221210
  };
220922
221211
  mask = (plain) => plain.length <= 4 ? "\u2022\u2022\u2022\u2022" : `${"\u2022".repeat(4)}${plain.slice(-4)}`;
220923
221212
  changedKeys = (prev, next) => {
220924
- if (!prev) return ["prompt", "model", "skills", "connectorIds", "maxConcurrent", "batchedContinuation", "extraArgs"];
221213
+ if (!prev) return ["prompt", "model", "skills", "connectorIds", "maxConcurrent", "batchedContinuation", "continuationMaxRounds", "extraArgs"];
220925
221214
  const keys = [];
220926
221215
  if (prev.prompt !== next.prompt) keys.push("prompt");
220927
221216
  if (prev.model !== next.model) keys.push("model");
220928
221217
  if (JSON.stringify(prev.skills) !== JSON.stringify(next.skills)) keys.push("skills");
220929
221218
  if (JSON.stringify(prev.connectorIds) !== JSON.stringify(next.connectorIds)) keys.push("connectorIds");
221219
+ if ((prev.continuationMaxRounds ?? 3) !== (next.continuationMaxRounds ?? 3)) keys.push("continuationMaxRounds");
220930
221220
  if ((prev.batchedContinuation ?? false) !== (next.batchedContinuation ?? false)) keys.push("batchedContinuation");
220931
221221
  if (prev.maxConcurrent !== next.maxConcurrent) keys.push("maxConcurrent");
220932
221222
  if (JSON.stringify(prev.extraArgs ?? []) !== JSON.stringify(next.extraArgs ?? [])) keys.push("extraArgs");
@@ -222450,6 +222740,9 @@ function actorsDomain(opts) {
222450
222740
  checkStringArray("skills");
222451
222741
  checkStringArray("connectorIds");
222452
222742
  checkBoolean("batchedContinuation");
222743
+ if (b2.continuationMaxRounds !== void 0 && (!Number.isSafeInteger(b2.continuationMaxRounds) || b2.continuationMaxRounds < 1 || b2.continuationMaxRounds > 100)) {
222744
+ throw new ApiError(400, "BAD_REQUEST", "continuationMaxRounds \u5FC5\u987B\u662F 1\u2013100 \u7684\u6574\u6570");
222745
+ }
222453
222746
  const rawExtra = b2.extraArgs;
222454
222747
  if (rawExtra !== void 0 && typeof rawExtra !== "string" && !(Array.isArray(rawExtra) && rawExtra.every((x2) => typeof x2 === "string"))) {
222455
222748
  throw new ApiError(400, "BAD_REQUEST", "extraArgs \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u5B57\u7B26\u4E32\u6570\u7EC4");
@@ -222471,6 +222764,7 @@ function actorsDomain(opts) {
222471
222764
  ...b2.skills !== void 0 ? { skills: b2.skills } : {},
222472
222765
  ...b2.connectorIds !== void 0 ? { connectorIds: b2.connectorIds } : {},
222473
222766
  ...typeof b2.maxConcurrent === "number" ? { maxConcurrent: b2.maxConcurrent } : {},
222767
+ ...b2.continuationMaxRounds !== void 0 ? { continuationMaxRounds: b2.continuationMaxRounds } : {},
222474
222768
  ...b2.batchedContinuation !== void 0 ? { batchedContinuation: b2.batchedContinuation } : {},
222475
222769
  ...extraArgs !== void 0 ? { extraArgs } : {}
222476
222770
  },
@@ -226034,6 +226328,7 @@ var init_activity_trace = __esm({
226034
226328
 
226035
226329
  // ../server/src/domains/collab/inbox.ts
226036
226330
  function changeConfirmSummary(r) {
226331
+ if (r.continuation) return `\u81EA\u52A8\u7EED\u8DD1\u5DF2\u5230\u9650\uFF0C\u7B49\u5F85\u4F60\u5BA1\u6838\uFF1A${r.plan.reason}`;
226037
226332
  const why = r.plan.reason.slice(0, 100);
226038
226333
  if (r.kind === "command" && r.command) {
226039
226334
  return `\u7BA1\u7406\u8005\u8BF7\u6C42\u6267\u884C\u4E00\u4E2A\u9AD8\u5371\u64CD\u4F5C\uFF0C\u7B49\u4F60\u6388\u6743\uFF1A${r.command.effectLabel}`;
@@ -226194,7 +226489,7 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
226194
226489
  const anchorOwner = model.artifacts.get(r.anchor)?.owner;
226195
226490
  const isOwnerRecipient = creatorOf.get(r.workspace) === me;
226196
226491
  const isLeadRecipient = !!anchorOwner && !!leadOf && leadOf(anchorOwner) === me;
226197
- if (!isOwnerRecipient && !isLeadRecipient) continue;
226492
+ if (r.continuation ? r.continuation.approverActorId !== me : !isOwnerRecipient && !isLeadRecipient) continue;
226198
226493
  const anchorArt = model.artifacts.get(r.anchor);
226199
226494
  const opCount = r.plan.ops?.length ?? 0;
226200
226495
  out.push({
@@ -241899,6 +242194,7 @@ var init_postgres_registry = __esm({
241899
242194
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS max_concurrent integer NOT NULL DEFAULT 4`);
241900
242195
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS reviewer_ids jsonb NOT NULL DEFAULT '[]'::jsonb`);
241901
242196
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS batched_continuation boolean NOT NULL DEFAULT false`);
242197
+ await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS continuation_max_rounds integer NOT NULL DEFAULT 3 CHECK (continuation_max_rounds BETWEEN 1 AND 100)`);
241902
242198
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS extra_args jsonb NOT NULL DEFAULT '[]'::jsonb`);
241903
242199
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS memory_enabled boolean NOT NULL DEFAULT true`);
241904
242200
  await pool.query(`
@@ -242150,8 +242446,8 @@ var init_postgres_registry = __esm({
242150
242446
  const r = await this.pool.query(
242151
242447
  // reviewer_ids 列保留(历史数据)但不再写入——决策 0029 删了 actor 级 reviewer override;列有 DEFAULT '[]' 自动填。
242152
242448
  `INSERT INTO ${this.s}.actor_configs
242153
- (actor_id, version, prompt, model, skills, connector_ids, max_concurrent, batched_continuation, extra_args, created_by, created_at, source, rolled_back_from)
242154
- SELECT $1,$2::int,$3,$4,$5::jsonb,$6::jsonb,$7::int,$8::boolean,$9::jsonb,$10,$11,$12,$13
242449
+ (actor_id, version, prompt, model, skills, connector_ids, max_concurrent, batched_continuation, extra_args, created_by, created_at, source, rolled_back_from, continuation_max_rounds)
242450
+ SELECT $1,$2::int,$3,$4,$5::jsonb,$6::jsonb,$7::int,$8::boolean,$9::jsonb,$10,$11,$12,$13,$14::int
242155
242451
  WHERE (SELECT COALESCE(MAX(version), 0) + 1 FROM ${this.s}.actor_configs WHERE actor_id = $1) = $2::int`,
242156
242452
  [
242157
242453
  c.actorId,
@@ -242166,7 +242462,8 @@ var init_postgres_registry = __esm({
242166
242462
  c.createdBy,
242167
242463
  c.createdAt,
242168
242464
  c.source,
242169
- c.rolledBackFrom ?? null
242465
+ c.rolledBackFrom ?? null,
242466
+ c.continuationMaxRounds ?? 3
242170
242467
  ]
242171
242468
  );
242172
242469
  if (r.rowCount !== 1) throw new Error(`config version conflict for ${c.actorId}: got ${c.version}`);
@@ -242618,6 +242915,7 @@ var init_postgres_registry = __esm({
242618
242915
  connectorIds: row.connector_ids,
242619
242916
  maxConcurrent: row.max_concurrent ?? 4,
242620
242917
  batchedContinuation: row.batched_continuation ?? false,
242918
+ continuationMaxRounds: row.continuation_max_rounds ?? 3,
242621
242919
  extraArgs: row.extra_args ?? [],
242622
242920
  // 注意:memory_enabled 是死列,不映射进 ActorConfig——记忆开关在 agent_prefs。
242623
242921
  createdBy: row.created_by,
@@ -252576,6 +252874,7 @@ async function provisionCompanyDataPlaneSchema(pool, schema) {
252576
252874
  reviewer_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
252577
252875
  max_concurrent integer NOT NULL DEFAULT 4,
252578
252876
  batched_continuation boolean NOT NULL DEFAULT false,
252877
+ continuation_max_rounds integer NOT NULL DEFAULT 3 CHECK (continuation_max_rounds BETWEEN 1 AND 100),
252579
252878
  extra_args jsonb NOT NULL DEFAULT '[]'::jsonb,
252580
252879
  memory_enabled boolean NOT NULL DEFAULT true,
252581
252880
  created_by text NOT NULL REFERENCES ${s2}.actors(id) ON DELETE RESTRICT,
@@ -252584,6 +252883,7 @@ async function provisionCompanyDataPlaneSchema(pool, schema) {
252584
252883
  rolled_back_from integer,
252585
252884
  PRIMARY KEY (actor_id, version)
252586
252885
  )`);
252886
+ await pool.query(`ALTER TABLE ${s2}.actor_configs ADD COLUMN IF NOT EXISTS continuation_max_rounds integer NOT NULL DEFAULT 3 CHECK (continuation_max_rounds BETWEEN 1 AND 100)`);
252587
252887
  await pool.query(`
252588
252888
  CREATE TABLE IF NOT EXISTS ${s2}.agent_prefs (
252589
252889
  actor_id text PRIMARY KEY REFERENCES ${s2}.actors(id) ON DELETE CASCADE,
@@ -252707,6 +253007,7 @@ function rowToConfig2(row) {
252707
253007
  connectorIds: row.connector_ids,
252708
253008
  maxConcurrent: row.max_concurrent,
252709
253009
  batchedContinuation: row.batched_continuation,
253010
+ continuationMaxRounds: row.continuation_max_rounds ?? 3,
252710
253011
  extraArgs: row.extra_args ?? [],
252711
253012
  createdBy: row.created_by,
252712
253013
  createdAt: new Date(row.created_at).toISOString(),
@@ -252989,8 +253290,8 @@ var init_company_owned_registry_store = __esm({
252989
253290
  async appendConfig(c) {
252990
253291
  const r = await this.pool.query(
252991
253292
  `INSERT INTO ${this.s}.actor_configs
252992
- (actor_id, version, prompt, model, skills, connector_ids, max_concurrent, batched_continuation, extra_args, created_by, created_at, source, rolled_back_from)
252993
- SELECT $1,$2::int,$3,$4,$5::jsonb,$6::jsonb,$7::int,$8::boolean,$9::jsonb,$10,$11,$12,$13
253293
+ (actor_id, version, prompt, model, skills, connector_ids, max_concurrent, batched_continuation, extra_args, created_by, created_at, source, rolled_back_from, continuation_max_rounds)
253294
+ SELECT $1,$2::int,$3,$4,$5::jsonb,$6::jsonb,$7::int,$8::boolean,$9::jsonb,$10,$11,$12,$13,$14::int
252994
253295
  WHERE (SELECT COALESCE(MAX(version), 0) + 1 FROM ${this.s}.actor_configs WHERE actor_id = $1) = $2::int`,
252995
253296
  [
252996
253297
  c.actorId,
@@ -253005,7 +253306,8 @@ var init_company_owned_registry_store = __esm({
253005
253306
  c.createdBy,
253006
253307
  c.createdAt,
253007
253308
  c.source,
253008
- c.rolledBackFrom ?? null
253309
+ c.rolledBackFrom ?? null,
253310
+ c.continuationMaxRounds ?? 3
253009
253311
  ]
253010
253312
  );
253011
253313
  if (r.rowCount !== 1) throw new Error(`config version conflict for ${c.actorId}: got ${c.version}`);
@@ -255061,8 +255363,8 @@ async function runIdentityMigration(input) {
255061
255363
  const ins = await client.query(
255062
255364
  `INSERT INTO ${cs}.actor_configs
255063
255365
  (actor_id,version,prompt,model,skills,connector_ids,reviewer_ids,max_concurrent,
255064
- batched_continuation,extra_args,memory_enabled,created_by,created_at,source,rolled_back_from)
255065
- VALUES ($1,$2,$3,$4,$5::jsonb,$6::jsonb,$7::jsonb,$8,$9,$10::jsonb,$11,$12,$13,$14,$15)
255366
+ batched_continuation,extra_args,memory_enabled,created_by,created_at,source,rolled_back_from,continuation_max_rounds)
255367
+ VALUES ($1,$2,$3,$4,$5::jsonb,$6::jsonb,$7::jsonb,$8,$9,$10::jsonb,$11,$12,$13,$14,$15,$16)
255066
255368
  ON CONFLICT (actor_id, version) DO NOTHING`,
255067
255369
  [
255068
255370
  r.actor_id,
@@ -255079,7 +255381,8 @@ async function runIdentityMigration(input) {
255079
255381
  r.created_by,
255080
255382
  r.created_at,
255081
255383
  r.source,
255082
- r.rolled_back_from ?? null
255384
+ r.rolled_back_from ?? null,
255385
+ r.continuation_max_rounds ?? 3
255083
255386
  ]
255084
255387
  );
255085
255388
  plan.configRows += ins.rowCount ?? 0;
@@ -257419,6 +257722,9 @@ var init_cutover_aware_workorder_drafts = __esm({
257419
257722
  async forWorkspace(w2) {
257420
257723
  return (await this.pick()).forWorkspace(w2);
257421
257724
  }
257725
+ async forWorkspaceVersion(w2, version2) {
257726
+ return (await this.pick()).forWorkspaceVersion(w2, version2);
257727
+ }
257422
257728
  async forSession(s2) {
257423
257729
  return (await this.pick()).forSession(s2);
257424
257730
  }
@@ -258431,7 +258737,7 @@ var init_src11 = __esm({
258431
258737
  init_execution_continuity2();
258432
258738
  init_chat_sessions();
258433
258739
  init_dev_store();
258434
- init_continuation();
258740
+ init_continuation3();
258435
258741
  init_knowledge2();
258436
258742
  init_delegations();
258437
258743
  init_automations();
@@ -258968,9 +259274,9 @@ function render(reason, facts) {
258968
259274
  switch (reason.kind) {
258969
259275
  case "checkpoints-exhausted":
258970
259276
  return {
258971
- headline: `\u8FD9\u4E00\u6B65**\u4EA4\u4E86 ${reason.rounds} \u7248\u7A3F\uFF0C\u6BCF\u4E00\u7248\u90FD\u6CA1\u8D70\u6536\u53E3**\u2014\u2014\u5F15\u64CE\u5DF2\u7ECF\u81EA\u5DF1\u91CD\u8BD5\u4E86 ${reason.rounds} \u8F6E\uFF08\u4E0A\u9650 ${reason.limit}\uFF09\uFF0C\u73B0\u5728\u505C\u4E0B\u7B49\u4EBA\u3002`,
259277
+ headline: `\u8FD9\u4E00\u6B65**\u8FDE\u7EED ${reason.rounds} \u8F6E\u6CA1\u6709\u65B0\u589E\u4EA4\u4ED8\u5185\u5BB9**\uFF0C\u91CD\u590D\u63D0\u4EA4\u5DF2\u6709\u5FEB\u7167\u4E14\u672A\u6536\u53E3\uFF08\u65E0\u8FDB\u5C55\u4E0A\u9650 ${reason.limit}\uFF09\u2014\u2014\u5F15\u64CE\u5DF2\u6682\u505C\u81EA\u52A8\u7EED\u8DD1\uFF0C\u7B49\u5F85\u4EBA\u5DE5\u5904\u7406\u3002`,
258972
259278
  detail: facts,
258973
- action: "\u9700\u8981\u4F60\u5904\u7406\uFF1A\u76F4\u63A5\u5F3A\u5236\u6536\u53E3\uFF0C\u6216\u91CD\u8DD1\u672C\u8282\u70B9\uFF08\u91CD\u8DD1\u4F1A\u91CD\u7F6E\u8FD9\u4E2A\u8BA1\u6570\uFF09\u3002"
259279
+ action: "\u9700\u8981\u4F60\u5904\u7406\uFF1A\u68C0\u67E5\u6700\u8FD1\u5FEB\u7167\u548C\u5269\u4F59\u5DE5\u4F5C\uFF0C\u8865\u5145\u6267\u884C\u65B9\u5411\u540E\u91CD\u8DD1\u672C\u8282\u70B9\uFF08\u91CD\u7F6E\u65E0\u8FDB\u5C55\u8BA1\u6570\uFF09\uFF1B\u5982\u679C\u5DF2\u7ECF\u5B8C\u6210\uFF0C\u8BA9\u6267\u884C\u4EBA\u63D0\u4EA4\u5B8C\u6574\u5FEB\u7167\u5E76 conclude\u3002"
258974
259280
  };
258975
259281
  case "gate-rejections-exhausted":
258976
259282
  return {
@@ -263671,6 +263977,10 @@ async function startServe(opts) {
263671
263977
  return false;
263672
263978
  });
263673
263979
  const engineIO = {
263980
+ async continuationPolicy(actorId) {
263981
+ const config2 = await (await companyRegistryFor(companyId)).latestConfig(actorId);
263982
+ return config2?.batchedContinuation ? { maxRounds: config2.continuationMaxRounds ?? 3 } : null;
263983
+ },
263674
263984
  async dispatchWork(workId, opts2) {
263675
263985
  if (engineDispatchBridges.size === 0) {
263676
263986
  console.warn(`[new-engine] dispatchWork ${workId} \u8DF3\u8FC7\uFF1A\u672C\u8FDB\u7A0B\u4E00\u5BB6\u516C\u53F8\u7684\u6D3E\u53D1\u6865\u90FD\u6CA1\u6709\uFF08--dispatch \u672A\u5F00\uFF1F\uFF09`);
@@ -272829,7 +273139,7 @@ function shimScript() {
272829
273139
  }
272830
273140
 
272831
273141
  // src/index.ts
272832
- var PKG_VERSION = true ? "2.2.20" : "dev";
273142
+ var PKG_VERSION = true ? "2.2.21" : "dev";
272833
273143
  var LOCAL_BIN = localBin();
272834
273144
  var NPM_PREFIX = npmPrefix();
272835
273145
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.20",
3
+ "version": "2.2.21",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "91acd24741c1142ec1f35a33f4f5d81790874975"
29
+ "sourceHead": "9d8339aadeb1d13c73ce23e2f4d03d4616c54a90"
30
30
  }
31
31
  }