oasis_test_v2 2.2.19 → 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 +1005 -258
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -243,7 +243,7 @@ function rankMemories(items, terms) {
243
243
  (a, b2) => b2.score - a.score || b2.accessedAt.localeCompare(a.accessedAt) || a.memId.localeCompare(b2.memId)
244
244
  );
245
245
  }
246
- var ActorMemoryNotFoundError, ActorMemoryVersionConflictError, MEMORY_CONTENT_MAX_CHARS, MEMORY_KEYWORDS_MAX, MEMORY_KEYWORD_MAX_CHARS, MEMORY_PER_ACTOR_MAX, MEMORY_DIGEST_CHARS, MEMORY_INJECT_MAX, MEMORY_KEYWORD_WEIGHT, MEMORY_QUERY_TERMS_MAX, MEMORY_READ_MAX_IDS, MEMORY_NGRAM_SCAN_MAX_CHARS, CJK_RE, ASCII_WORD_RE;
246
+ var ActorMemoryNotFoundError, ActorMemoryVersionConflictError, MEMORY_CONTENT_MAX_CHARS, MEMORY_KEYWORDS_MAX, MEMORY_KEYWORD_MAX_CHARS, MEMORY_PER_ACTOR_MAX, MEMORY_DIGEST_CHARS, MEMORY_INJECT_DIGEST_CHARS, MEMORY_INJECT_MAX, MEMORY_KEYWORD_WEIGHT, MEMORY_QUERY_TERMS_MAX, MEMORY_READ_MAX_IDS, MEMORY_NGRAM_SCAN_MAX_CHARS, CJK_RE, ASCII_WORD_RE;
247
247
  var init_actor_memory = __esm({
248
248
  "../contract/src/actor-memory.ts"() {
249
249
  "use strict";
@@ -268,7 +268,8 @@ var init_actor_memory = __esm({
268
268
  MEMORY_KEYWORDS_MAX = 16;
269
269
  MEMORY_KEYWORD_MAX_CHARS = 32;
270
270
  MEMORY_PER_ACTOR_MAX = 500;
271
- MEMORY_DIGEST_CHARS = 80;
271
+ MEMORY_DIGEST_CHARS = 140;
272
+ MEMORY_INJECT_DIGEST_CHARS = 80;
272
273
  MEMORY_INJECT_MAX = 30;
273
274
  MEMORY_KEYWORD_WEIGHT = 3;
274
275
  MEMORY_QUERY_TERMS_MAX = 32;
@@ -1219,6 +1220,35 @@ var init_chat = __esm({
1219
1220
  }
1220
1221
  });
1221
1222
 
1223
+ // ../contract/src/session-outputs.ts
1224
+ function diffSessionOutputs(scanned, previous3) {
1225
+ const prev = previous3 ?? {};
1226
+ const changed = [];
1227
+ const fingerprints = {};
1228
+ for (const entry of [...scanned].sort((a, b2) => a.path.localeCompare(b2.path))) {
1229
+ const { path: path41, size, mtime } = entry;
1230
+ if (!path41) continue;
1231
+ const usable = typeof size === "number" && Number.isFinite(size) && typeof mtime === "string" && mtime !== "";
1232
+ if (usable) fingerprints[path41] = { size, mtime };
1233
+ const before = prev[path41];
1234
+ const unchanged = usable && before !== void 0 && before.size === size && before.mtime === mtime;
1235
+ if (unchanged) continue;
1236
+ changed.push({ path: path41, name: basenameOf(path41), size: size ?? null, mtime: mtime ?? null });
1237
+ }
1238
+ return { changed, fingerprints };
1239
+ }
1240
+ function basenameOf(path41) {
1241
+ const i = path41.lastIndexOf("/");
1242
+ return i >= 0 ? path41.slice(i + 1) : path41;
1243
+ }
1244
+ var SESSION_OUTPUTS_DIR;
1245
+ var init_session_outputs = __esm({
1246
+ "../contract/src/session-outputs.ts"() {
1247
+ "use strict";
1248
+ SESSION_OUTPUTS_DIR = "outputs";
1249
+ }
1250
+ });
1251
+
1222
1252
  // ../contract/src/chat-session.ts
1223
1253
  function supportsChatModelOverride(runtimeKind) {
1224
1254
  return !!runtimeKind && CHAT_MODEL_OVERRIDE_RUNTIMES.includes(runtimeKind);
@@ -1409,10 +1439,11 @@ function touchAnchorMessageId(messageId) {
1409
1439
  const hash2 = messageId.indexOf("#");
1410
1440
  return hash2 >= 0 ? messageId.slice(0, hash2) : messageId;
1411
1441
  }
1412
- var DelegationLabelConflictError, DELEGATION_FILE_LIMITS, DELEGATION_INBOUND_DIR, DELEGATION_OUTBOUND_DIR, DELEGATION_OUTPUTS_DIR, DELEGATION_RETURN_RECENT_ROUNDS;
1442
+ var DelegationLabelConflictError, DELEGATION_FILE_LIMITS, DELEGATION_INBOUND_DIR, DELEGATION_OUTBOUND_DIR, DELEGATION_RETURN_RECENT_ROUNDS;
1413
1443
  var init_delegation = __esm({
1414
1444
  "../contract/src/delegation.ts"() {
1415
1445
  "use strict";
1446
+ init_session_outputs();
1416
1447
  DelegationLabelConflictError = class extends Error {
1417
1448
  constructor(parentSessionId, label) {
1418
1449
  super(`delegation label conflict in parent session: parent=${parentSessionId} label=${label ?? "<null>"}`);
@@ -1431,7 +1462,6 @@ var init_delegation = __esm({
1431
1462
  };
1432
1463
  DELEGATION_INBOUND_DIR = "inputs/attachments";
1433
1464
  DELEGATION_OUTBOUND_DIR = "delegations";
1434
- DELEGATION_OUTPUTS_DIR = "outputs";
1435
1465
  DELEGATION_RETURN_RECENT_ROUNDS = 3;
1436
1466
  }
1437
1467
  });
@@ -1452,6 +1482,8 @@ var init_delegation_prompts = __esm({
1452
1482
  "## \u5DE5\u4F5C\u533A\u7EA6\u5B9A",
1453
1483
  "",
1454
1484
  "- **\u4EA4\u4ED8\u7269\u653E `outputs/`**\uFF1A\u4F60\u8981\u4EA4\u56DE\u53BB\u7684\u6587\u4EF6\u653E\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E0B\uFF0C\u4FDD\u7559\u4F60\u81EA\u5DF1\u7684\u5B50\u76EE\u5F55\u7ED3\u6784\u3002",
1485
+ " \u8FD9\u91CC\u7684\u6587\u4EF6\u65E2\u4F1A\u4EA4\u56DE\u534F\u8C03\u8005\uFF0C\u4E5F\u4F1A\u5728**\u4F60\u8FD9\u6761\u5B50\u4F1A\u8BDD\u7684\u9875\u9762\u4E0A**\u663E\u793A\u6210\u53EF\u70B9\u5F00\u7684\u6587\u4EF6\u5361\u7247\uFF1B",
1486
+ " \u4E2D\u95F4\u4EA7\u7269\u548C\u8349\u7A3F\u522B\u653E\u8FD9\u513F\u3002",
1455
1487
  `- \`${PARENT_CONVERSATION_PATH}\` \u5982\u679C\u5B58\u5728\uFF0C\u662F\u4E3B\u4F1A\u8BDD\u7684\u5BF9\u8BDD\u5FEB\u7167\uFF0C\u53EF\u4EE5\u67E5\u9605\u3002`,
1456
1488
  " **\u5B83\u4E0D\u4FDD\u8BC1\u5B58\u5728**\uFF08\u4E3B\u4F1A\u8BDD\u6CA1\u6709\u53EF\u8F6C\u50A8\u7684\u5386\u53F2\u65F6\u5C31\u6CA1\u6709\u8FD9\u4E2A\u6587\u4EF6\uFF09\uFF0C\u522B\u628A\u5B83\u5F53\u524D\u63D0\u3002",
1457
1489
  "- \u534F\u8C03\u8005\u968F\u59D4\u6D3E\u5E26\u8FC7\u6765\u7684\u6587\u4EF6\u5728\u4F60\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF0C\u4EFB\u52A1\u4E66\u91CC\u4F1A\u70B9\u540D\u3002",
@@ -1658,6 +1690,7 @@ var init_src = __esm({
1658
1690
  init_playbook();
1659
1691
  init_experiments();
1660
1692
  init_chat();
1693
+ init_session_outputs();
1661
1694
  init_chat_session();
1662
1695
  init_chat_session();
1663
1696
  init_live_protocol();
@@ -4049,7 +4082,8 @@ var init_model = __esm({
4049
4082
  BUSINESS_HANDOFF_POLICY_FIELD = "businessHandoffPolicy";
4050
4083
  BUSINESS_HANDOFF_POLICY_EXEMPT = "exempt";
4051
4084
  DEFAULT_CONFIG = {
4052
- maxRetries: 3
4085
+ maxRetries: 3,
4086
+ maxStagnantCheckpoints: 3
4053
4087
  };
4054
4088
  deriveWorkId = (nodeId, version2) => `wk:${nodeId}:${version2}`;
4055
4089
  deriveReviewId = (workId, reviewer, group) => `rv:${workId}:${reviewer}:${group}`;
@@ -4057,6 +4091,101 @@ var init_model = __esm({
4057
4091
  }
4058
4092
  });
4059
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
+
4060
4189
  // ../engine/src/events.ts
4061
4190
  function eventAnchors(e) {
4062
4191
  switch (e.kind) {
@@ -4301,6 +4430,10 @@ var init_state = __esm({
4301
4430
  artifactsOf(workId) {
4302
4431
  return this.artifactList.filter((a) => a.workId === workId);
4303
4432
  }
4433
+ /** 多批次分析一次读取本节点全部快照,避免每个 work 再扫整单产物。 */
4434
+ artifactsOfNode(nodeId) {
4435
+ return this.artifactList.filter((a) => a.nodeId === nodeId);
4436
+ }
4304
4437
  workInputsOf(workId) {
4305
4438
  return this.workInputList.filter((i) => i.workId === workId);
4306
4439
  }
@@ -4458,60 +4591,6 @@ var init_state = __esm({
4458
4591
  }
4459
4592
  });
4460
4593
 
4461
- // ../engine/src/closure-gate.ts
4462
- function closureSlotOfFacts(facts) {
4463
- const report = facts.liveClosureReport();
4464
- if (report) return { kind: "closure-report", id: report.id };
4465
- const business = facts.liveBusinessNodes();
4466
- if (business.length === 1) return { kind: "sole-artifact", id: business[0].id };
4467
- if (business.length === 0) return { kind: "empty" };
4468
- return { kind: "missing", businessCount: business.length };
4469
- }
4470
- function humanGateActorsOfFacts(facts, artifactId) {
4471
- const slot = closureSlotOfFacts(facts);
4472
- if (slot.kind !== "closure-report" && slot.kind !== "sole-artifact") return [];
4473
- if (slot.id !== artifactId) return [];
4474
- const owner = facts.humanOwner();
4475
- return owner === null ? [] : [owner];
4476
- }
4477
- function closureReviewGroup(actor) {
4478
- return `closure:${actor}`;
4479
- }
4480
- function closureReviewerSpecs(closureActors) {
4481
- return closureActors.map((actor) => ({
4482
- reviewerActorId: actor,
4483
- reviewGroup: closureReviewGroup(actor),
4484
- source: "closure"
4485
- }));
4486
- }
4487
- function engineClosureFacts(state) {
4488
- return {
4489
- liveBusinessNodes: () => state.liveNodes().filter((n) => n.type !== BRIEF_TYPE && n.type !== DELIVERY_SUMMARY_TYPE).map((n) => ({ id: n.id, type: n.type })),
4490
- liveClosureReport: () => {
4491
- const r = state.liveNodes().find((n) => n.type === DELIVERY_SUMMARY_TYPE);
4492
- return r ? { id: r.id, type: r.type } : null;
4493
- },
4494
- humanOwner: () => {
4495
- const brief = state.liveNodes().find((n) => n.type === BRIEF_TYPE);
4496
- const owner = brief?.assigneeActorId;
4497
- if (!owner || !owner.startsWith(HUMAN_ACTOR_PREFIX)) return null;
4498
- return owner;
4499
- }
4500
- };
4501
- }
4502
- function engineHumanGateActors(state, nodeId) {
4503
- return humanGateActorsOfFacts(engineClosureFacts(state), nodeId);
4504
- }
4505
- var DELIVERY_SUMMARY_TYPE, BRIEF_TYPE, HUMAN_ACTOR_PREFIX;
4506
- var init_closure_gate = __esm({
4507
- "../engine/src/closure-gate.ts"() {
4508
- "use strict";
4509
- DELIVERY_SUMMARY_TYPE = "delivery-summary";
4510
- BRIEF_TYPE = "brief";
4511
- HUMAN_ACTOR_PREFIX = "actor:human:";
4512
- }
4513
- });
4514
-
4515
4594
  // ../engine/src/letters.ts
4516
4595
  function repliesSorted(state, issueId) {
4517
4596
  return [...state.repliesOf(issueId)].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
@@ -4668,6 +4747,26 @@ function scan(state, ctx) {
4668
4747
  const events = [];
4669
4748
  for (const node2 of state.liveNodes()) {
4670
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
+ }
4671
4770
  if (!lw) {
4672
4771
  tryToRun(state, node2.id, ctx, events);
4673
4772
  continue;
@@ -4961,6 +5060,20 @@ function scanBlockersForNode(state, node2, latest) {
4961
5060
  function tryToRun(state, nodeId, ctx, events) {
4962
5061
  const node2 = state.node(nodeId);
4963
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
+ }
4964
5077
  if (!node2.assigneeActorId) return;
4965
5078
  const latest = (node2.latestWorkId ? state.work(node2.latestWorkId) : null) ?? null;
4966
5079
  if (unexemptedVetoBlockers(scanBlockersForNode(state, node2, latest), "dispatch").length > 0) {
@@ -5059,19 +5172,34 @@ function findHumanRecoveryPost(state, node2, deadAt) {
5059
5172
  const first = candidates[0];
5060
5173
  return first ? { actorId: first.actorId, at: first.at } : null;
5061
5174
  }
5062
- function countUnconcludedCheckpoints(state, nodeId) {
5063
- 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();
5064
5184
  let count2 = 0;
5185
+ let version2;
5065
5186
  for (const w2 of works) {
5066
- if (w2.retryAt) break;
5067
- if (w2.status === "success") {
5068
- if (w2.conclusion == null && state.artifactsOf(w2.id).length > 0) {
5069
- count2++;
5070
- continue;
5071
- }
5072
- break;
5073
- }
5074
- 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);
5075
5203
  }
5076
5204
  return count2;
5077
5205
  }
@@ -5090,8 +5218,8 @@ function countConsecutiveRejections(state, nodeId) {
5090
5218
  return count2;
5091
5219
  }
5092
5220
  function unconcludedCheckpointsExhausted(state, nodeId, ctx) {
5093
- const limit = ctx.config?.maxRetries ?? DEFAULT_CONFIG.maxRetries;
5094
- const rounds = countUnconcludedCheckpoints(state, nodeId);
5221
+ const limit = ctx.config?.maxStagnantCheckpoints ?? DEFAULT_CONFIG.maxStagnantCheckpoints;
5222
+ const rounds = countStagnantCheckpoints(state, nodeId);
5095
5223
  return rounds > limit ? { rounds, limit } : null;
5096
5224
  }
5097
5225
  function gateRejectionsExhausted(state, nodeId, ctx) {
@@ -5445,6 +5573,7 @@ function assessConvergence(state, ctx) {
5445
5573
  var init_activate = __esm({
5446
5574
  "../engine/src/activate.ts"() {
5447
5575
  "use strict";
5576
+ init_continuation();
5448
5577
  init_src3();
5449
5578
  init_src2();
5450
5579
  init_model();
@@ -5499,6 +5628,7 @@ CREATE TABLE IF NOT EXISTS ${q2}.workflow_nodes (
5499
5628
  latest_accept_id text,
5500
5629
  is_final_output boolean NOT NULL DEFAULT false,
5501
5630
  fields jsonb,
5631
+ continuation_budget jsonb,
5502
5632
  cancelled_at timestamptz,
5503
5633
  -- \u5C01\u5B58\u539F\u56E0\uFF08accepted/cancelled/frozen\uFF09\u3002NULL = \u5B58\u91CF\u884C\uFF0C\u8BFB\u6A21\u578B\u6309 cancelled \u515C\u5E95\u3002
5504
5634
  seal_reason text,
@@ -5506,6 +5636,7 @@ CREATE TABLE IF NOT EXISTS ${q2}.workflow_nodes (
5506
5636
  created_at timestamptz NOT NULL,
5507
5637
  updated_at timestamptz NOT NULL
5508
5638
  );
5639
+ ALTER TABLE ${q2}.workflow_nodes ADD COLUMN IF NOT EXISTS continuation_budget jsonb;
5509
5640
  CREATE INDEX IF NOT EXISTS nodes_workorder_idx ON ${q2}.workflow_nodes (workorder_id);
5510
5641
 
5511
5642
  CREATE TABLE IF NOT EXISTS ${q2}.workflow_edges (
@@ -11411,6 +11542,7 @@ var init_store_postgres = __esm({
11411
11542
  // NULL = 存量行(ADR 0137 迁移批量置的 cancelled_at):按 cancelled 读,不追溯改写历史。
11412
11543
  sealReason: n.seal_reason ?? null,
11413
11544
  fields: n.fields ?? void 0,
11545
+ continuationBudget: n.continuation_budget ?? null,
11414
11546
  rowVersion: n.row_version,
11415
11547
  createdAt: iso(n.created_at),
11416
11548
  updatedAt: iso(n.updated_at)
@@ -11592,8 +11724,8 @@ var init_store_postgres = __esm({
11592
11724
  `INSERT INTO ${this.s}.workflow_nodes
11593
11725
  (id,workorder_id,type,title,spec,version,assignee_actor_id,assignee_role,
11594
11726
  latest_work_id,latest_proposed_work_id,latest_accept_id,is_final_output,
11595
- fields,cancelled_at,seal_reason,row_version,created_at,updated_at)
11596
- 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)
11597
11729
  ON CONFLICT (id) DO NOTHING`,
11598
11730
  [
11599
11731
  n.id,
@@ -11613,13 +11745,14 @@ var init_store_postgres = __esm({
11613
11745
  n.sealReason ?? null,
11614
11746
  n.rowVersion,
11615
11747
  n.createdAt,
11616
- n.updatedAt
11748
+ n.updatedAt,
11749
+ n.continuationBudget ? JSON.stringify(n.continuationBudget) : null
11617
11750
  ]
11618
11751
  );
11619
11752
  break;
11620
11753
  }
11621
11754
  case "node.update":
11622
- 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 });
11623
11756
  break;
11624
11757
  case "edge.insert": {
11625
11758
  const e = m2.row;
@@ -12043,7 +12176,7 @@ function newNode(id, workorderId, at, version2) {
12043
12176
  updatedAt: at
12044
12177
  };
12045
12178
  }
12046
- function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
12179
+ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false, resetCheckpointBudget = false) {
12047
12180
  const node2 = state.node(nodeId);
12048
12181
  if (!node2) return;
12049
12182
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
@@ -12052,6 +12185,9 @@ function markNodeForRetry(state, nodeId, at, resetRejectedBudget = false) {
12052
12185
  }
12053
12186
  if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
12054
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
+ }
12055
12191
  for (const req of state.requirementsOf(node2.id)) {
12056
12192
  if (!req.latestReviewId) continue;
12057
12193
  const r = state.review(req.latestReviewId);
@@ -12183,7 +12319,7 @@ var init_plan = __esm({
12183
12319
  const changed = node2.assigneeActorId !== e.assigneeActorId;
12184
12320
  const lw = node2.latestWorkId ? state.work(node2.latestWorkId) : null;
12185
12321
  const settled = lw !== null && lw !== void 0 && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success";
12186
- 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)) {
12187
12323
  state.updateWork(lw.id, { retryAt: ctx.at });
12188
12324
  }
12189
12325
  state.updateNode(e.nodeId, {
@@ -12258,7 +12394,7 @@ var init_plan = __esm({
12258
12394
  name: "plan/node-retry",
12259
12395
  kind: "plan.node_retry",
12260
12396
  apply(e, state, ctx) {
12261
- markNodeForRetry(state, e.nodeId, ctx.at, e.resetRejectedBudget === true);
12397
+ markNodeForRetry(state, e.nodeId, ctx.at, e.resetRejectedBudget === true, e.resetCheckpointBudget === true);
12262
12398
  }
12263
12399
  };
12264
12400
  planUpdateFields = {
@@ -12396,7 +12532,8 @@ var init_workorder = __esm({
12396
12532
  workorderId: e.workorderId,
12397
12533
  nodeId: node2.id,
12398
12534
  by: e.by,
12399
- resetRejectedBudget: e.resetRejectedBudget
12535
+ resetRejectedBudget: e.resetRejectedBudget,
12536
+ resetCheckpointBudget: e.resetCheckpointBudget
12400
12537
  }, state, ctx);
12401
12538
  }
12402
12539
  }
@@ -12419,6 +12556,7 @@ var workCreate, workRedispatch, workSnapshotRecorded, workKill, workResponse, wo
12419
12556
  var init_work = __esm({
12420
12557
  "../engine/src/handlers/work.ts"() {
12421
12558
  "use strict";
12559
+ init_continuation();
12422
12560
  init_views2();
12423
12561
  init_kill();
12424
12562
  init_letters();
@@ -12467,6 +12605,7 @@ var init_work = __esm({
12467
12605
  });
12468
12606
  return;
12469
12607
  }
12608
+ if (e.continuationGateVersion === 1 && continuationExhausted(state, node2)) return;
12470
12609
  supersedeLatestWork(state, e.nodeId, ctx.at);
12471
12610
  killNodeReviews(state, e.nodeId, ctx.at);
12472
12611
  for (const w2 of state.replyWorksOf(e.nodeId)) {
@@ -12947,7 +13086,7 @@ var init_issue = __esm({
12947
13086
  }
12948
13087
  if (issue2.kind === "gap" || issue2.kind === "escalation") {
12949
13088
  const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
12950
- if (nodeId) markNodeForRetry(state, nodeId, ctx.at, e.resetRejectedBudget === true);
13089
+ if (nodeId) markNodeForRetry(state, nodeId, ctx.at, e.resetRejectedBudget === true, e.resetCheckpointBudget === true);
12951
13090
  }
12952
13091
  }
12953
13092
  };
@@ -13148,6 +13287,10 @@ var init_review = __esm({
13148
13287
  }
13149
13288
  return;
13150
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
+ }
13151
13294
  state.updateNode(w2.nodeId, { latestAcceptId: e.workId, updatedAt: ctx.at });
13152
13295
  state.updateWork(e.workId, {
13153
13296
  acceptanceState: "accepted",
@@ -13267,6 +13410,67 @@ var init_review = __esm({
13267
13410
  }
13268
13411
  });
13269
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
+
13270
13474
  // ../engine/src/handlers/index.ts
13271
13475
  var HANDLERS;
13272
13476
  var init_handlers = __esm({
@@ -13278,6 +13482,7 @@ var init_handlers = __esm({
13278
13482
  init_work();
13279
13483
  init_review();
13280
13484
  init_issue();
13485
+ init_continuation2();
13281
13486
  init_types();
13282
13487
  HANDLERS = [
13283
13488
  /* ── 工作区级(5)── */
@@ -13298,6 +13503,9 @@ var init_handlers = __esm({
13298
13503
  erase(planUpdateReviewRequirements),
13299
13504
  erase(planNodeRetry),
13300
13505
  erase(planUpdateFields),
13506
+ erase(continuationInitialize),
13507
+ erase(continuationRequest),
13508
+ erase(continuationDecide),
13301
13509
  /* ── 节点级 work(7)── */
13302
13510
  erase(workCreate),
13303
13511
  erase(workRedispatch),
@@ -13332,7 +13540,7 @@ var init_handlers = __esm({
13332
13540
  // ../engine/src/bus.ts
13333
13541
  function versionCommandEvent(event) {
13334
13542
  if (event.kind === "work.create" || event.kind === "review.create") {
13335
- return { ...event, pauseGateVersion: 1 };
13543
+ return { ...event, pauseGateVersion: 1, ...event.kind === "work.create" ? { continuationGateVersion: 1 } : {} };
13336
13544
  }
13337
13545
  if (event.kind === "issue.reply") return { ...event, handlingVersion: 1 };
13338
13546
  if (event.kind === "work.response" && event.semanticsVersion === 2) {
@@ -13346,11 +13554,11 @@ function versionCommandEvent(event) {
13346
13554
  case "work.handoff_recorded":
13347
13555
  return { ...event, semanticsVersion: 2 };
13348
13556
  case "issue.resolve":
13349
- return { ...event, semanticsVersion: 2, resetRejectedBudget: true };
13557
+ return { ...event, semanticsVersion: 2, resetRejectedBudget: true, resetCheckpointBudget: true };
13350
13558
  case "plan.node_retry":
13351
13559
  case "workorder.resumed":
13352
13560
  case "plan.assign_actor":
13353
- return { ...event, resetRejectedBudget: true };
13561
+ return { ...event, resetRejectedBudget: true, resetCheckpointBudget: true };
13354
13562
  default:
13355
13563
  return event;
13356
13564
  }
@@ -13396,6 +13604,7 @@ var init_bus = __esm({
13396
13604
  init_model();
13397
13605
  init_handlers();
13398
13606
  init_state();
13607
+ init_continuation();
13399
13608
  SYSTEM_ACTOR = "actor:system:engine";
13400
13609
  NOOP_IO = {
13401
13610
  async dispatchWork() {
@@ -13882,6 +14091,21 @@ var init_bus = __esm({
13882
14091
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(workorderId));
13883
14092
  if (!snap || snap.workorder.dispatchPaused) return false;
13884
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
+ }
13885
14109
  if (opts?.neverStarted && work) {
13886
14110
  const node2 = snap.nodes.find((row) => row.id === work.nodeId);
13887
14111
  if (work.startedAt || work.sessionRef || !node2 || node2.cancelledAt || node2.latestWorkId !== workId) return false;
@@ -13991,7 +14215,21 @@ var init_bus = __esm({
13991
14215
  if (!snapshot) return { emitted: 0, terminated: false };
13992
14216
  if (snapshot.workorder.lifecycle !== "active") return { emitted: 0, terminated: false };
13993
14217
  const state = new WorkorderState(snapshot);
13994
- 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() };
13995
14233
  const { events, terminated } = scan(state, scanCtx);
13996
14234
  await tx.applyMutations(workorderId, state.mutations);
13997
14235
  const allEvents = [...state.emitted, ...events];
@@ -14126,6 +14364,7 @@ var init_src4 = __esm({
14126
14364
  "../engine/src/index.ts"() {
14127
14365
  "use strict";
14128
14366
  init_model();
14367
+ init_continuation();
14129
14368
  init_events();
14130
14369
  init_views2();
14131
14370
  init_state();
@@ -14320,7 +14559,8 @@ function memoryIndexLines(entries) {
14320
14559
  const rows = entries.slice(0, MEMORY_INJECT_MAX).map((e) => {
14321
14560
  const tag = e.projectId === null ? "[actor]" : "[project]";
14322
14561
  const kw = e.keywords.length > 0 ? ` {${e.keywords.join(",")}}` : "";
14323
- return `- \`${e.memId}\` ${tag}${kw} ${e.digest}`;
14562
+ const preview2 = e.digest.length > MEMORY_INJECT_DIGEST_CHARS ? `${e.digest.slice(0, MEMORY_INJECT_DIGEST_CHARS)}\u2026` : e.digest;
14563
+ return `- \`${e.memId}\` ${tag}${kw} ${preview2}`;
14324
14564
  });
14325
14565
  return [
14326
14566
  ``,
@@ -15262,6 +15502,7 @@ ${manifestDiff(baseC, newC)}
15262
15502
  let skillsLines = [];
15263
15503
  let identityNote = null;
15264
15504
  let batchedContinuation = false;
15505
+ let continuationMaxRounds = 3;
15265
15506
  let memoryEnabled = true;
15266
15507
  let executorRole = null;
15267
15508
  const executionActor = args.actorId ?? artifact.owner;
@@ -15269,6 +15510,7 @@ ${manifestDiff(baseC, newC)}
15269
15510
  const actorCtx = await args.resolveActorContext(executionActor).catch(() => null);
15270
15511
  if (actorCtx) {
15271
15512
  batchedContinuation = actorCtx.config?.batchedContinuation === true;
15513
+ continuationMaxRounds = actorCtx.config?.continuationMaxRounds ?? 3;
15272
15514
  memoryEnabled = actorCtx.memoryEnabled !== false;
15273
15515
  executorRole = actorCtx.roles?.[0] ?? null;
15274
15516
  if (actorCtx.config?.prompt) {
@@ -15640,7 +15882,7 @@ ${m2.spec.split("\n").map((l) => ` > ${l}`).join("\n")}`);
15640
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`,
15641
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`,
15642
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`] : [],
15643
- ...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`] : [],
15644
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`,
15645
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`,
15646
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`] : []
@@ -18759,6 +19001,36 @@ function bridgeLoadReadModel(input) {
18759
19001
  pinMeta.set(edge.toNodeId, record8);
18760
19002
  }
18761
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
+ }
18762
19034
  const pointedReviewIds = new Set(
18763
19035
  allRequirements.map((r) => r.latestReviewId).filter((x2) => x2 != null)
18764
19036
  );
@@ -19358,6 +19630,7 @@ var import_node_crypto3, PENDING_ROLE_PREFIX2, PENDING_UNASSIGNED2, EngineKernel
19358
19630
  var init_kernel_bridge = __esm({
19359
19631
  "../core/src/kernel-bridge.ts"() {
19360
19632
  "use strict";
19633
+ init_src4();
19361
19634
  import_node_crypto3 = require("node:crypto");
19362
19635
  init_read_model();
19363
19636
  init_closure_gate2();
@@ -21149,7 +21422,35 @@ var init_kernel_bridge = __esm({
21149
21422
  await this.refreshModel();
21150
21423
  return { draftId };
21151
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
+ }
21152
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
+ }
21153
21454
  const card2 = this.draftCards.get(args.draftId);
21154
21455
  if (!card2 || card2.kind !== "command" || !card2.command) {
21155
21456
  throw new Error(`\u4E0D\u662F\u547D\u4EE4\u6388\u6743\u8BF7\u6C42\u6216\u5DF2\u5904\u7406\uFF1A${args.draftId}`);
@@ -21179,6 +21480,10 @@ var init_kernel_bridge = __esm({
21179
21480
  return { command: card2.command, artifactId };
21180
21481
  }
21181
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
+ }
21182
21487
  this.draftCards.delete(args.draftId);
21183
21488
  await this.forgetDraftCard(args.draftId);
21184
21489
  await this.refreshModel();
@@ -24897,6 +25202,8 @@ async function runProtocolSession(job, cfg) {
24897
25202
  const transcript = openTranscript(transcriptRef);
24898
25203
  const outputCbs = [];
24899
25204
  const telemetryCbs = [];
25205
+ const liveCbs = [];
25206
+ const appendByKey = /* @__PURE__ */ new Map();
24900
25207
  const exitCbs = [];
24901
25208
  const state = cfg.createState();
24902
25209
  const pending = /* @__PURE__ */ new Map();
@@ -24932,7 +25239,7 @@ async function runProtocolSession(job, cfg) {
24932
25239
  pending.clear();
24933
25240
  };
24934
25241
  const send = (command) => {
24935
- if (closing || child.stdin.destroyed || !child.stdin.writable) return Promise.reject(new Error("session stdin is not writable"));
25242
+ if (closing || child.stdin.destroyed || !child.stdin.writable) return Promise.reject(new ProtocolSessionClosing("session stdin is not writable"));
24936
25243
  const idValue = command.id ?? `oasis_${++nextRequestId}`;
24937
25244
  const message = { ...command, id: idValue };
24938
25245
  const key = commandId(idValue);
@@ -24952,11 +25259,12 @@ async function runProtocolSession(job, cfg) {
24952
25259
  if (isResponse(message) && responseId !== void 0 && pending.has(responseId)) {
24953
25260
  const waiter = pending.get(responseId);
24954
25261
  pending.delete(responseId);
24955
- if (message.type === "response" && message.success === false) waiter.reject(new Error(String(message.error ?? "runtime rejected command")));
24956
- else if (Object.prototype.hasOwnProperty.call(message, "error")) waiter.reject(new Error(String(message.error?.message ?? message.error)));
25262
+ if (message.type === "response" && message.success === false) waiter.reject(new ProtocolCommandRejected(String(message.error ?? "runtime rejected command")));
25263
+ else if (Object.prototype.hasOwnProperty.call(message, "error")) waiter.reject(new ProtocolCommandRejected(String(message.error?.message ?? message.error)));
24957
25264
  else waiter.resolve(message.result ?? message);
24958
25265
  return;
24959
25266
  }
25267
+ for (const event of cfg.liveEvents?.(message, state) ?? []) for (const cb of liveCbs) cb(event);
24960
25268
  for (const line of cfg.normalize(message, state)) {
24961
25269
  if (line.outputText) for (const cb of outputCbs) cb(line.outputText);
24962
25270
  const event = { kind: line.kind, payload: line.payload, seq: ++eventSeq, ts: (/* @__PURE__ */ new Date()).toISOString() };
@@ -25048,26 +25356,34 @@ async function runProtocolSession(job, cfg) {
25048
25356
  onTelemetry(cb) {
25049
25357
  telemetryCbs.push(cb);
25050
25358
  },
25359
+ onLiveEvent(cb) {
25360
+ liveCbs.push(cb);
25361
+ },
25051
25362
  get canAppendInput() {
25052
25363
  return !closing && started && !exited;
25053
25364
  },
25054
25365
  appendInput(input) {
25055
- if (input.requireConsumptionReceipt) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
25366
+ const key = input.clientMessageKey;
25367
+ if (key && appendByKey.has(key)) return appendByKey.get(key);
25368
+ if (input.requireConsumptionReceipt && !cfg.supportsInputConsumptionReceipts) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
25056
25369
  appendChain = appendChain.then(async () => {
25057
25370
  if (exited || closing) return { accepted: false, reason: "session-closing" };
25058
25371
  const files = materializeFiles(dir, input.files, input.binaryFiles);
25059
25372
  try {
25060
- const result = await cfg.append(input, state, send);
25061
- return { ...result, ...files.written.length || files.failed.length ? { files } : {} };
25062
- } catch {
25063
- return { accepted: false, reason: "rejected", ...files.written.length || files.failed.length ? { files } : {} };
25373
+ const result2 = await cfg.append(input, state, send);
25374
+ return { ...result2, ...files.written.length || files.failed.length ? { files } : {} };
25375
+ } catch (error2) {
25376
+ const reason = error2 instanceof ProtocolSessionClosing ? "session-closing" : error2 instanceof ProtocolCommandRejected ? "rejected" : "unconfirmed";
25377
+ return { accepted: false, reason, ...files.written.length || files.failed.length ? { files } : {} };
25064
25378
  }
25065
25379
  });
25066
- return appendChain;
25380
+ const result = appendChain;
25381
+ if (key) appendByKey.set(key, result);
25382
+ return result;
25067
25383
  }
25068
25384
  };
25069
25385
  }
25070
- var import_node_child_process6, import_node_crypto10, path9;
25386
+ var import_node_child_process6, import_node_crypto10, path9, ProtocolCommandRejected, ProtocolSessionClosing;
25071
25387
  var init_protocol_session = __esm({
25072
25388
  "../adapters/src/_core/protocol-session.ts"() {
25073
25389
  "use strict";
@@ -25083,6 +25399,10 @@ var init_protocol_session = __esm({
25083
25399
  init_transcript();
25084
25400
  init_filter_extra_args();
25085
25401
  init_root_bypass();
25402
+ ProtocolCommandRejected = class extends Error {
25403
+ };
25404
+ ProtocolSessionClosing = class extends Error {
25405
+ };
25086
25406
  }
25087
25407
  });
25088
25408
 
@@ -25595,11 +25915,13 @@ var init_codex = __esm({
25595
25915
  workRoot: this.opts.workRoot,
25596
25916
  env: this.opts.env,
25597
25917
  runtimeKind: "codex",
25918
+ supportsInputConsumptionReceipts: true,
25598
25919
  syncSkills: this.opts.syncSkills,
25599
25920
  createState: () => ({
25600
25921
  threadId: void 0,
25601
25922
  turnId: void 0,
25602
25923
  active: false,
25924
+ pendingInputs: /* @__PURE__ */ new Set(),
25603
25925
  codex: createCodexNormalizeState(),
25604
25926
  // 本次 run 的用量:逐轮累加 `thread/tokenUsage/updated` 的 `last`(见该解析函数注释)。
25605
25927
  usage: { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 },
@@ -25643,6 +25965,34 @@ ${task}` : task;
25643
25965
  if (typeof turnId === "string" && turnId) state.turnId = turnId;
25644
25966
  if (!state.turnId) throw new Error("codex app-server returned no turn id");
25645
25967
  },
25968
+ liveEvents(message, state) {
25969
+ const params = message.params;
25970
+ const method = message.method;
25971
+ const turnId = typeof params?.turnId === "string" ? params.turnId : state.turnId ?? "codex-pending";
25972
+ const base = { protocolVersion: 2, streamId: `codex:${state.threadId}`, turnId };
25973
+ if (method === "item/agentMessage/delta" && typeof params?.itemId === "string" && typeof params.delta === "string") {
25974
+ return [{ ...base, itemId: params.itemId, itemType: "message", operation: "append_text", payload: { text: params.delta } }];
25975
+ }
25976
+ if (method === "item/started" || method === "item/completed") {
25977
+ const item = params?.item;
25978
+ if (item?.type === "userMessage" && typeof item.clientId === "string" && state.pendingInputs.delete(item.clientId)) {
25979
+ return [{
25980
+ ...base,
25981
+ itemId: `input:${item.clientId}`,
25982
+ itemType: "control",
25983
+ operation: "completed",
25984
+ payload: { code: "input_consumed", clientMessageKey: item.clientId }
25985
+ }];
25986
+ }
25987
+ if (method === "item/completed" && item?.type === "agentMessage" && typeof item.id === "string") {
25988
+ return [{ ...base, itemId: item.id, itemType: "message", operation: "completed", payload: { text: typeof item.text === "string" ? item.text : "" } }];
25989
+ }
25990
+ }
25991
+ if (method === "turn/completed") {
25992
+ return [{ ...base, itemId: "turn-end", itemType: "control", operation: "completed", payload: { code: "output_block_completed" } }];
25993
+ }
25994
+ return [];
25995
+ },
25646
25996
  normalize(message, state) {
25647
25997
  const params = message.params;
25648
25998
  const method = typeof message.method === "string" ? message.method : "";
@@ -25701,7 +26051,7 @@ ${task}` : task;
25701
26051
  };
25702
26052
  },
25703
26053
  async append(input, state, send) {
25704
- if (!state.threadId || !state.turnId) return { accepted: false, reason: "rejected" };
26054
+ if (!state.threadId || !state.turnId || !state.active) return { accepted: false, reason: "session-closing" };
25705
26055
  if (input.interrupt) {
25706
26056
  await send({ jsonrpc: "2.0", method: "turn/interrupt", params: { threadId: state.threadId, turnId: state.turnId } });
25707
26057
  const next = await send({ jsonrpc: "2.0", method: "turn/start", params: { threadId: state.threadId, input: [{ type: "text", text: input.text }] } });
@@ -25710,13 +26060,22 @@ ${task}` : task;
25710
26060
  state.active = true;
25711
26061
  return { accepted: true, interrupt: "unknown" };
25712
26062
  }
25713
- await send({ jsonrpc: "2.0", method: "turn/steer", params: {
25714
- threadId: state.threadId,
25715
- expectedTurnId: state.turnId,
25716
- ...input.clientMessageKey ? { clientUserMessageId: input.clientMessageKey } : {},
25717
- input: [{ type: "text", text: input.text }]
25718
- } });
25719
- return { accepted: true };
26063
+ if (input.clientMessageKey) state.pendingInputs.add(input.clientMessageKey);
26064
+ try {
26065
+ await send({ jsonrpc: "2.0", method: "turn/steer", params: {
26066
+ threadId: state.threadId,
26067
+ expectedTurnId: state.turnId,
26068
+ ...input.clientMessageKey ? { clientUserMessageId: input.clientMessageKey } : {},
26069
+ input: [{ type: "text", text: input.text }]
26070
+ } });
26071
+ } catch (error2) {
26072
+ if (input.clientMessageKey) state.pendingInputs.delete(input.clientMessageKey);
26073
+ if (error2 instanceof ProtocolCommandRejected && /no active turn|not active|expected.*turn|turn.*mismatch/i.test(error2.message)) {
26074
+ return { accepted: false, reason: "session-closing" };
26075
+ }
26076
+ throw error2;
26077
+ }
26078
+ return { accepted: true, consumption: "pending" };
25720
26079
  }
25721
26080
  });
25722
26081
  }
@@ -26592,6 +26951,7 @@ async function runACPSession(job, cfg) {
26592
26951
  const transcriptOut = openTranscript(transcriptRef);
26593
26952
  const outputCbs = [];
26594
26953
  const telemetryCbs = [];
26954
+ const liveCbs = [];
26595
26955
  const exitCbs = [];
26596
26956
  let exited;
26597
26957
  let killReason = null;
@@ -26670,6 +27030,34 @@ ${note}` : note };
26670
27030
  });
26671
27031
  let lastRpcError;
26672
27032
  let streamingCurrentTurn = false;
27033
+ const emitBoundary = () => {
27034
+ for (const cb of liveCbs) cb({
27035
+ protocolVersion: 2,
27036
+ streamId: id,
27037
+ turnId: id,
27038
+ itemId: "prompt-boundary",
27039
+ itemType: "control",
27040
+ operation: "completed",
27041
+ payload: { code: "output_block_completed" }
27042
+ });
27043
+ };
27044
+ const confirmedInputs = /* @__PURE__ */ new Set();
27045
+ const confirmAppendBatch = () => {
27046
+ for (const input of inflightAppendBatch) {
27047
+ const key = input.clientMessageKey;
27048
+ if (!key || confirmedInputs.has(key)) continue;
27049
+ confirmedInputs.add(key);
27050
+ for (const cb of liveCbs) cb({
27051
+ protocolVersion: 2,
27052
+ streamId: id,
27053
+ turnId: id,
27054
+ itemId: `input:${key}`,
27055
+ itemType: "control",
27056
+ operation: "completed",
27057
+ payload: { code: "input_consumed", clientMessageKey: key }
27058
+ });
27059
+ }
27060
+ };
26673
27061
  const client = new ACPClient(
26674
27062
  (line) => {
26675
27063
  try {
@@ -26678,9 +27066,11 @@ ${note}` : note };
26678
27066
  }
26679
27067
  },
26680
27068
  (text5) => {
27069
+ confirmAppendBatch();
26681
27070
  for (const cb of outputCbs) cb(text5);
26682
27071
  },
26683
27072
  (e) => {
27073
+ if (["message", "thought", "tool_use", "tool_result"].includes(e.kind)) confirmAppendBatch();
26684
27074
  for (const cb of telemetryCbs) cb(e);
26685
27075
  },
26686
27076
  // runtime 自己用 stopReason=cancelled 收尾时也保留取消语义;此前误记成 error,
@@ -26814,9 +27204,11 @@ ${task}` : task;
26814
27204
  promptResult = await sendPrompt(userText);
26815
27205
  }
26816
27206
  while (!killReason && appendQueue.length > 0) {
27207
+ emitBoundary();
26817
27208
  inflightAppendBatch = appendQueue.splice(0);
26818
27209
  const merged = inflightAppendBatch.map((i) => i.text).filter(Boolean).join("\n\n");
26819
27210
  promptResult = await sendPrompt(merged);
27211
+ confirmAppendBatch();
26820
27212
  inflightAppendBatch = [];
26821
27213
  }
26822
27214
  closingForExit = true;
@@ -26855,6 +27247,9 @@ ${task}` : task;
26855
27247
  onTelemetry(cb) {
26856
27248
  telemetryCbs.push(cb);
26857
27249
  },
27250
+ onLiveEvent(cb) {
27251
+ liveCbs.push(cb);
27252
+ },
26858
27253
  // ADR-0093 对话追加:入队一条 user 输入,drain 循环会把它发成同 session 的下一回合。
26859
27254
  // 已收尾/已退出则拒(session-closing),上层据此降级 kill+resume。
26860
27255
  get canAppendInput() {
@@ -26878,12 +27273,15 @@ ${task}` : task;
26878
27273
  }
26879
27274
  };
26880
27275
  function doAppend(input) {
26881
- if (input.requireConsumptionReceipt) return Promise.resolve({ accepted: false, reason: "consumption-unsupported" });
26882
27276
  if (exited || closingForExit) return Promise.resolve({ accepted: false, reason: "session-closing" });
26883
27277
  const report = materializeFiles(dir, input.files, input.binaryFiles);
26884
27278
  const queued = report.failed.length ? { ...input, text: appendMaterializeWarning(input.text, report) } : input;
26885
27279
  appendQueue.push(queued);
26886
- const withFiles = (r) => report.written.length || report.failed.length ? { ...r, files: report } : r;
27280
+ const withFiles = (r) => ({
27281
+ ...r,
27282
+ ...input.requireConsumptionReceipt && r.accepted ? { consumption: "pending" } : {},
27283
+ ...report.written.length || report.failed.length ? { files: report } : {}
27284
+ });
26887
27285
  if (!input.interrupt) return Promise.resolve(withFiles({ accepted: true, interrupt: "queued_at_boundary" }));
26888
27286
  const run = appendChain.then(async () => {
26889
27287
  const pending = inflightStopReason;
@@ -27637,12 +28035,13 @@ var init_pi = __esm({
27637
28035
  return ["--mode", "rpc", "--session", sessionFile, ...model ? ["--model", model] : []];
27638
28036
  },
27639
28037
  runtimeKind: "pi",
28038
+ supportsInputConsumptionReceipts: true,
27640
28039
  workRoot: opts.workRoot,
27641
28040
  cleanupWorkdir: opts.cleanupWorkdir,
27642
28041
  env: opts.env,
27643
28042
  extraArgs: opts.extraArgs,
27644
28043
  blockedFlags: PI_BLOCKED_FLAGS,
27645
- createState: () => ({}),
28044
+ createState: () => ({ pendingInputs: [] }),
27646
28045
  initialCommand(job2, _dir, task) {
27647
28046
  const prompt2 = job2.systemPrompt ? `${job2.systemPrompt}
27648
28047
 
@@ -27659,6 +28058,28 @@ ${task}` : task;
27659
28058
  } catch {
27660
28059
  }
27661
28060
  },
28061
+ liveEvents(message, state) {
28062
+ const base = { protocolVersion: 2, streamId: "pi-rpc", turnId: "pi-rpc" };
28063
+ const raw = message.message;
28064
+ if (message.type === "message_end" && raw?.role === "assistant") {
28065
+ return [{ ...base, itemId: "message-end", itemType: "control", operation: "completed", payload: { code: "output_block_completed" } }];
28066
+ }
28067
+ if (message.type === "message_start" && raw?.role === "user") {
28068
+ const text5 = typeof raw.content === "string" ? raw.content : Array.isArray(raw.content) ? raw.content.filter((p2) => p2.type === "text").map((p2) => p2.text).join("") : void 0;
28069
+ const pending = state.pendingInputs[0];
28070
+ if (pending && text5 === pending.text) {
28071
+ state.pendingInputs.shift();
28072
+ return [{
28073
+ ...base,
28074
+ itemId: `input:${pending.key}`,
28075
+ itemType: "control",
28076
+ operation: "completed",
28077
+ payload: { code: "input_consumed", clientMessageKey: pending.key }
28078
+ }];
28079
+ }
28080
+ }
28081
+ return [];
28082
+ },
27662
28083
  normalize(message) {
27663
28084
  return normalizePi(JSON.stringify(message));
27664
28085
  },
@@ -27668,14 +28089,21 @@ ${task}` : task;
27668
28089
  runtimeSessionId(state) {
27669
28090
  return state.sessionId;
27670
28091
  },
27671
- async append(input, _state, send) {
27672
- if (input.interrupt) {
27673
- await send({ type: "abort" });
28092
+ async append(input, state, send) {
28093
+ const pending = input.clientMessageKey ? { key: input.clientMessageKey, text: input.text } : void 0;
28094
+ if (pending) state.pendingInputs.push(pending);
28095
+ try {
28096
+ if (input.interrupt) {
28097
+ await send({ type: "abort" });
28098
+ await send({ type: "steer", message: input.text });
28099
+ return { accepted: true, interrupt: "unknown" };
28100
+ }
27674
28101
  await send({ type: "steer", message: input.text });
27675
- return { accepted: true, interrupt: "unknown" };
28102
+ return { accepted: true, consumption: "pending" };
28103
+ } catch (error2) {
28104
+ if (pending) state.pendingInputs = state.pendingInputs.filter((p2) => p2 !== pending);
28105
+ throw error2;
27676
28106
  }
27677
- await send({ type: "steer", message: input.text });
27678
- return { accepted: true };
27679
28107
  }
27680
28108
  });
27681
28109
  }
@@ -28485,10 +28913,13 @@ var init_normalizer = __esm({
28485
28913
  proseClaim = /* @__PURE__ */ new Map();
28486
28914
  /** 本 turn 内已经见过的 item 键——认领闸只拦「新起一段」,不拦已知 item 的后续 op。 */
28487
28915
  claimedKeys = /* @__PURE__ */ new Set();
28916
+ closedKeys = /* @__PURE__ */ new Set();
28488
28917
  /** 见过 stdout 文本没有——见过就压掉 telemetry `message`(与 server.ts 原口径同款去重)。 */
28489
28918
  sawTextOutput = false;
28490
28919
  /** 本 turn 产出过 item op 没有。收尾时用来决定要不要发 turn 信号(见 turnFinished)。 */
28491
28920
  producedAny = false;
28921
+ /** Receipts wait for a whole output block; they never split its bytes. */
28922
+ pendingInputs = /* @__PURE__ */ new Map();
28492
28923
  constructor(opts) {
28493
28924
  this.providerName = opts.providerName;
28494
28925
  this.hasLiveProtocol = opts.hasLiveProtocol === true;
@@ -28532,20 +28963,17 @@ var init_normalizer = __esm({
28532
28963
  }
28533
28964
  /** 结构化实时事件(原生 Live Protocol v2)。 */
28534
28965
  onLiveEvent(event) {
28535
- this.turnId = event.turnId || this.turnId;
28536
28966
  const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
28537
28967
  if (event.itemType === "control" && payload.code === "input_consumed" && typeof payload.clientMessageKey === "string") {
28968
+ this.pendingInputs.set(payload.clientMessageKey, event.itemId);
28969
+ if (!this.open) this.flushInputs();
28970
+ return;
28971
+ }
28972
+ if (event.itemType === "control" && payload.code === "output_block_completed") {
28538
28973
  this.closeOpen("completed");
28539
- this.emitItem({
28540
- key: providerItemKey(this.providerName, event.itemId),
28541
- kind: "control",
28542
- role: "system",
28543
- origin: "live",
28544
- opShape: { op: "set_status", status: "completed" },
28545
- attrs: { code: "input_consumed", clientMessageKey: payload.clientMessageKey }
28546
- });
28547
28974
  return;
28548
28975
  }
28976
+ this.turnId = event.turnId || this.turnId;
28549
28977
  if (event.operation === "turn_completed" || event.operation === "turn_failed") {
28550
28978
  this.turnFinished(event.operation === "turn_failed" ? "failed" : "completed");
28551
28979
  return;
@@ -28561,13 +28989,12 @@ var init_normalizer = __esm({
28561
28989
  }
28562
28990
  return;
28563
28991
  }
28564
- if (event.operation === "completed" && (event.itemType === "message" || event.itemType === "thinking") && typeof payload.text === "string") {
28565
- this.snapshotProse(
28566
- event.itemType === "message" ? "text" : "thinking",
28567
- payload.text,
28568
- "live",
28569
- providerItemKey(this.providerName, event.itemId)
28570
- );
28992
+ if (event.operation === "completed" && (event.itemType === "message" || event.itemType === "thinking")) {
28993
+ const key = providerItemKey(this.providerName, event.itemId);
28994
+ if (typeof payload.text === "string") {
28995
+ this.snapshotProse(event.itemType === "message" ? "text" : "thinking", payload.text, "live", key);
28996
+ }
28997
+ if (this.open?.key === key) this.closeOpen("completed");
28571
28998
  return;
28572
28999
  }
28573
29000
  if (event.operation === "started" && event.itemType === "tool") {
@@ -28592,6 +29019,7 @@ var init_normalizer = __esm({
28592
29019
  this.fallbackIndex.clear();
28593
29020
  this.proseClaim.clear();
28594
29021
  this.claimedKeys.clear();
29022
+ this.closedKeys.clear();
28595
29023
  this.sawTextOutput = false;
28596
29024
  this.producedAny = false;
28597
29025
  if (!produced) return;
@@ -28615,6 +29043,10 @@ var init_normalizer = __esm({
28615
29043
  if (!known && !this.claim(kind, origin)) return;
28616
29044
  const key = explicitKey ?? (this.open && this.open.kind === kind ? this.open.key : this.nextFallbackKey(kind));
28617
29045
  this.claimedKeys.add(key);
29046
+ if (known && this.closedKeys.has(key)) {
29047
+ this.emitItem({ key, kind, role: "assistant", origin, opShape: { op: "append_text", text: text5 } });
29048
+ return;
29049
+ }
28618
29050
  if (!this.open || this.open.key !== key) {
28619
29051
  this.closeOpen("completed");
28620
29052
  this.open = { key, kind, origin };
@@ -28631,6 +29063,10 @@ var init_normalizer = __esm({
28631
29063
  if (!known && !this.claim(kind, origin)) return;
28632
29064
  const key = explicitKey ?? (this.open && this.open.kind === kind ? this.open.key : this.nextFallbackKey(kind));
28633
29065
  this.claimedKeys.add(key);
29066
+ if (known && this.closedKeys.has(key)) {
29067
+ this.emitItem({ key, kind, role: "assistant", origin, opShape: { op: "set_text", text: text5 } });
29068
+ return;
29069
+ }
28634
29070
  if (!this.open || this.open.key !== key) {
28635
29071
  this.closeOpen("completed");
28636
29072
  this.open = { key, kind, origin };
@@ -28649,8 +29085,23 @@ var init_normalizer = __esm({
28649
29085
  closeOpen(status) {
28650
29086
  const open3 = this.open;
28651
29087
  this.open = null;
28652
- if (!open3) return;
28653
- this.emitItem({ key: open3.key, kind: open3.kind, role: "assistant", origin: open3.origin, opShape: { op: "set_status", status } });
29088
+ if (open3) this.closedKeys.add(open3.key);
29089
+ if (open3) this.emitItem({ key: open3.key, kind: open3.kind, role: "assistant", origin: open3.origin, opShape: { op: "set_status", status } });
29090
+ this.flushInputs();
29091
+ }
29092
+ flushInputs() {
29093
+ const inputs = [...this.pendingInputs];
29094
+ this.pendingInputs.clear();
29095
+ for (const [clientMessageKey, id] of inputs) {
29096
+ this.emitItem({
29097
+ key: providerItemKey(this.providerName, id),
29098
+ kind: "control",
29099
+ role: "system",
29100
+ origin: "live",
29101
+ opShape: { op: "set_status", status: "completed" },
29102
+ attrs: { code: "input_consumed", clientMessageKey }
29103
+ });
29104
+ }
28654
29105
  }
28655
29106
  toolCall(origin, id, name, input) {
28656
29107
  this.closeOpen("completed");
@@ -28870,7 +29321,7 @@ var init_chat_item_ledger = __esm({
28870
29321
  telemetryTextInContent;
28871
29322
  /** 本段的「无输出」注解已经提前落过(`closeSegmentBeforeUserInput`),`beginSegment` 别再落一条。 */
28872
29323
  segmentEndEmitted = false;
28873
- /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。切段/收尾清空。 */
29324
+ /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。仅 runtime turn 结束/收尾清空,用户插入保留。 */
28874
29325
  itemKeyMap = /* @__PURE__ */ new Map();
28875
29326
  /** 内存账本:itemId → 行。插入顺序即 seq 顺序。 */
28876
29327
  rows = /* @__PURE__ */ new Map();
@@ -29049,7 +29500,7 @@ var init_chat_item_ledger = __esm({
29049
29500
  }
29050
29501
  });
29051
29502
  const row = this.rows.get(id);
29052
- return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
29503
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord, messageId: row.messageId };
29053
29504
  }
29054
29505
  /**
29055
29506
  * 回灌重启前已经落库的那段正文(`chat-recovery` 重挂 / 迟到对账用)。
@@ -29084,7 +29535,7 @@ var init_chat_item_ledger = __esm({
29084
29535
  // ── 段与归属 ──────────────────────────────────────────────────────────────
29085
29536
  /**
29086
29537
  * 切段(人中途插入):此刻起建的 item 属于**下一条** assistant 行。
29087
- * 映射一并清空——新段是新行,复用旧 itemId 会把新文本写进上一条消息。
29538
+ * 已有 provider item 保留身份与位置;新 item 才归下一段,迟到快照不能复制旧块。
29088
29539
  */
29089
29540
  beginSegment() {
29090
29541
  if (this.segmentEndEmitted) {
@@ -29093,8 +29544,6 @@ var init_chat_item_ledger = __esm({
29093
29544
  this.closeStreamingRows("completed");
29094
29545
  if (this.segmentItemCount === 0 && this.messageId) this.insertSegmentEnd();
29095
29546
  }
29096
- this.itemKeyMap.clear();
29097
- this.lastTurnId = null;
29098
29547
  this.messageId = null;
29099
29548
  this.segment += 1;
29100
29549
  this.segmentItemCount = 0;
@@ -29118,7 +29567,7 @@ var init_chat_item_ledger = __esm({
29118
29567
  const id = this.insertSegmentEnd();
29119
29568
  this.segmentEndEmitted = true;
29120
29569
  const row = this.rows.get(id);
29121
- return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
29570
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord, messageId: row.messageId };
29122
29571
  }
29123
29572
  /** 零输出段兜底(S1 评审 B1):那条 assistant 行库里已经在了,一条 item 都没有会被对账抓成假红。 */
29124
29573
  insertSegmentEnd() {
@@ -29137,6 +29586,9 @@ var init_chat_item_ledger = __esm({
29137
29586
  /** 行落库了,把本段还挂着 NULL 的 assistant item 认领回去,并让后续 item 直接带上它。 */
29138
29587
  bindMessage(messageId) {
29139
29588
  this.messageId = messageId;
29589
+ for (const row of this.rows.values()) {
29590
+ if (row.role === "assistant" && row.messageId === null) row.messageId = messageId;
29591
+ }
29140
29592
  if (!this.persistEnabled) return;
29141
29593
  this.enqueue(async () => {
29142
29594
  await this.deps.items.bindMessage(this.deps.sessionId, this.deps.turnId, messageId);
@@ -29196,7 +29648,8 @@ var init_chat_item_ledger = __esm({
29196
29648
  itemId: id,
29197
29649
  version: row.wireVersion,
29198
29650
  startedVersion: row.startedVersion,
29199
- ord: row.ord
29651
+ ord: row.ord,
29652
+ messageId: row.messageId
29200
29653
  };
29201
29654
  if (event.opShape.op === "set_status") {
29202
29655
  const followUp = this.applyOpTo(row, event);
@@ -29232,7 +29685,7 @@ var init_chat_item_ledger = __esm({
29232
29685
  }
29233
29686
  /** 这一次写的回执。`persist` 已经在里面走过钟了,这里只是把读数带出去给 wire。 */
29234
29687
  outcomeOf(row) {
29235
- return { itemId: row.id, version: row.wireVersion, ord: row.ord };
29688
+ return { itemId: row.id, version: row.wireVersion, ord: row.ord, messageId: row.messageId };
29236
29689
  }
29237
29690
  /** 关掉所有还 streaming 的行(收尾/切段)。 */
29238
29691
  closeStreamingRows(status) {
@@ -29284,13 +29737,16 @@ var init_chat_item_ledger = __esm({
29284
29737
  origin: input.origin,
29285
29738
  inContent: input.inContent,
29286
29739
  persist: input.persist,
29287
- segment: this.segment
29740
+ segment: this.segment,
29741
+ /* 归属在**建行那一刻**定死:链是异步的,等它执行时 this.messageId 可能已被切段改了。
29742
+ 记在行上而不是只记在入库那一步——wire 帧也要带它(2026-09-11),两边必须同一个值。 */
29743
+ messageId: input.forceNullMessage ? null : this.messageId
29288
29744
  };
29289
29745
  this.rows.set(id, row);
29290
29746
  this.createdCount += 1;
29291
29747
  if (!input.forceNullMessage) this.segmentItemCount += 1;
29292
29748
  if (!input.persist || !this.persistEnabled) return id;
29293
- const messageId = input.forceNullMessage ? null : this.messageId;
29749
+ const messageId = row.messageId;
29294
29750
  const createdAt = this.now();
29295
29751
  const payload = input.payload ?? this.payloadOf(row);
29296
29752
  const metadata = input.metadata ?? { [CHAT_ITEM_META_LEGACY_CONTENT]: input.inContent };
@@ -29473,6 +29929,7 @@ async function openAssistantRunningRow(deps) {
29473
29929
  async function finalizeAssistantRow(deps) {
29474
29930
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
29475
29931
  const finishedAt = now();
29932
+ const parts = await appendOutputsPart(deps.parts, deps.outputsPart);
29476
29933
  if (deps.assistantMsgId) {
29477
29934
  if (typeof deps.chatStore.updateMessage !== "function") return;
29478
29935
  try {
@@ -29480,7 +29937,7 @@ async function finalizeAssistantRow(deps) {
29480
29937
  content: deps.content,
29481
29938
  status: deps.status,
29482
29939
  completedAt: finishedAt,
29483
- ...deps.parts.length ? { parts: deps.parts } : {},
29940
+ ...parts.length ? { parts } : {},
29484
29941
  ...deps.runId ? { runId: deps.runId } : {},
29485
29942
  ...deps.turnId ? { turnId: deps.turnId } : {}
29486
29943
  });
@@ -29488,7 +29945,7 @@ async function finalizeAssistantRow(deps) {
29488
29945
  }
29489
29946
  return;
29490
29947
  }
29491
- if (!deps.content && !deps.runId && deps.parts.length === 0) return;
29948
+ if (!deps.content && !deps.runId && parts.length === 0) return;
29492
29949
  if (typeof deps.chatStore.appendMessage !== "function") return;
29493
29950
  try {
29494
29951
  await deps.chatStore.appendMessage({
@@ -29501,11 +29958,18 @@ async function finalizeAssistantRow(deps) {
29501
29958
  status: deps.status,
29502
29959
  ...deps.runId ? { runId: deps.runId } : {},
29503
29960
  ...deps.turnId ? { turnId: deps.turnId } : {},
29504
- ...deps.parts.length ? { parts: deps.parts } : {}
29961
+ ...parts.length ? { parts } : {}
29505
29962
  });
29506
29963
  } catch {
29507
29964
  }
29508
29965
  }
29966
+ async function appendOutputsPart(parts, provider) {
29967
+ if (!provider) return parts;
29968
+ const part = await provider().catch(() => null);
29969
+ if (!part || !part.outputs?.length) return parts;
29970
+ const maxSeq = parts.reduce((m2, p2) => typeof p2.seq === "number" && p2.seq > m2 ? p2.seq : m2, 0);
29971
+ return [...parts, { ...part, seq: maxSeq + 1 }];
29972
+ }
29509
29973
  function runBackgroundAssistantChatTurn(deps) {
29510
29974
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
29511
29975
  const log3 = deps.log ?? ((m2) => console.warn(m2));
@@ -29674,6 +30138,7 @@ function runBackgroundAssistantChatTurn(deps) {
29674
30138
  parts,
29675
30139
  status,
29676
30140
  ...runId ? { runId } : {},
30141
+ ...deps.outputsPart ? { outputsPart: deps.outputsPart } : {},
29677
30142
  now
29678
30143
  });
29679
30144
  if (typeof deps.chatStore.updateSession === "function") {
@@ -51546,7 +52011,7 @@ var require_websocket = __commonJS({
51546
52011
  var http2 = require("http");
51547
52012
  var net = require("net");
51548
52013
  var tls = require("tls");
51549
- var { randomBytes: randomBytes13, createHash: createHash38 } = require("crypto");
52014
+ var { randomBytes: randomBytes13, createHash: createHash39 } = require("crypto");
51550
52015
  var { Duplex, Readable } = require("stream");
51551
52016
  var { URL: URL2 } = require("url");
51552
52017
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -52214,7 +52679,7 @@ var require_websocket = __commonJS({
52214
52679
  abortHandshake(websocket, socket, "Invalid Upgrade header");
52215
52680
  return;
52216
52681
  }
52217
- const digest = createHash38("sha1").update(key + GUID).digest("base64");
52682
+ const digest = createHash39("sha1").update(key + GUID).digest("base64");
52218
52683
  if (res.headers["sec-websocket-accept"] !== digest) {
52219
52684
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
52220
52685
  return;
@@ -52583,7 +53048,7 @@ var require_websocket_server = __commonJS({
52583
53048
  var EventEmitter = require("events");
52584
53049
  var http2 = require("http");
52585
53050
  var { Duplex } = require("stream");
52586
- var { createHash: createHash38 } = require("crypto");
53051
+ var { createHash: createHash39 } = require("crypto");
52587
53052
  var extension3 = require_extension();
52588
53053
  var PerMessageDeflate2 = require_permessage_deflate();
52589
53054
  var subprotocol2 = require_subprotocol();
@@ -52890,7 +53355,7 @@ var require_websocket_server = __commonJS({
52890
53355
  );
52891
53356
  }
52892
53357
  if (this._state > RUNNING) return abortHandshake(socket, 503);
52893
- const digest = createHash38("sha1").update(key + GUID).digest("base64");
53358
+ const digest = createHash39("sha1").update(key + GUID).digest("base64");
52894
53359
  const headers = [
52895
53360
  "HTTP/1.1 101 Switching Protocols",
52896
53361
  "Upgrade: websocket",
@@ -142723,6 +143188,7 @@ function shortId2(id) {
142723
143188
  function stallOf(model, id, runtime, dispatcherSignals) {
142724
143189
  const a = model.artifacts.get(id);
142725
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 };
142726
143192
  const ownerIsAgent = a.owner.startsWith("actor:agent:");
142727
143193
  const escs = unresolvedEscalationsOf(model, id);
142728
143194
  if (escs.length > 0) {
@@ -142878,7 +143344,7 @@ function nodeInfo(model, a) {
142878
143344
  const hasUnproducedParts = (a.parts ?? []).some((p2) => !producedParts.has(p2.name));
142879
143345
  const blockedByIssue = unresolvedGapsOf(model, a.id).length > 0 || unresolvedEscalationsOf(model, a.id).length > 0;
142880
143346
  let mark;
142881
- if (!sealed && blockedByIssue) {
143347
+ if (!sealed && (blockedByIssue || a.continuationPause)) {
142882
143348
  mark = "blocked";
142883
143349
  } else if (a.latestWorkState !== void 0) {
142884
143350
  const st = a.latestWorkState;
@@ -142935,6 +143401,7 @@ function deriveStage(members, opts) {
142935
143401
  }
142936
143402
  function nodeRunState(model, artifact, info, runtime, opts) {
142937
143403
  if (info.sealed) return void 0;
143404
+ if (artifact.continuationPause) return artifact.continuationPause.decision === "pending" ? "pending" : "blocked";
142938
143405
  const id = artifact.id;
142939
143406
  const now = opts?.now ?? Date.now();
142940
143407
  const rt = runtime ?? {};
@@ -160267,6 +160734,7 @@ var init_live_chat = __esm({
160267
160734
  operation: "started",
160268
160735
  itemVersion: outcome.startedVersion,
160269
160736
  ...ord !== void 0 ? { ord } : {},
160737
+ ...outcome.messageId !== null ? { messageId: outcome.messageId } : {},
160270
160738
  offset: 0,
160271
160739
  payload: v3StartedPayload(event.role, event.attrs, itemType, event.kind)
160272
160740
  };
@@ -160300,6 +160768,11 @@ var init_live_chat = __esm({
160300
160768
  // 账本这一次写用掉的那一格钟——**库里那一行的 version 就是它**,快照与增量流同一套版本空间。
160301
160769
  itemVersion: outcome.version,
160302
160770
  ...ord !== void 0 ? { ord } : {},
160771
+ /* 归属:账本写进 `chat_items.message_id` 的那个值,原样带出去(2026-09-11)。
160772
+ 此前只有快照带它,于是「面板挂载之后才生出来的 item」在客户端永远没有归属——
160773
+ 右侧子会话面板靠它把 DB 详情与实时桶合成一条,对不上号就把同一轮画两遍。
160774
+ `null` 不上 wire(省字节):客户端合并层本就按「帧不带 ⇒ 保留已有」处理。 */
160775
+ ...outcome.messageId !== null ? { messageId: outcome.messageId } : {},
160303
160776
  ...offset !== void 0 ? { offset } : {},
160304
160777
  ...payload !== void 0 ? { payload } : {}
160305
160778
  };
@@ -161065,15 +161538,29 @@ var init_chat_attachment_files = __esm({
161065
161538
  async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, attachments, retainLocallyOnFailure = false) {
161066
161539
  const newId = deps.newId ?? import_node_crypto20.randomUUID;
161067
161540
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
161068
- const submitted = await deps.store.enqueuePendingMessage({
161069
- id: newId(),
161541
+ if (!deps.store.claimPendingInsert) {
161542
+ return { accepted: false, reason: "queue-unavailable", queued: false, delivered: "", batch: [] };
161543
+ }
161544
+ const { submitted, batch } = await deps.store.claimPendingInsert({
161545
+ id: clientKey ? `append-request:${(0, import_node_crypto20.createHash)("sha256").update(JSON.stringify([chatSessionId, clientKey])).digest("hex")}` : newId(),
161070
161546
  chatSessionId,
161071
161547
  text: text5,
161072
161548
  ...attachments?.length ? { attachments } : {},
161073
161549
  ...clientKey ? { clientKey } : {},
161074
161550
  createdAt: now()
161075
161551
  });
161076
- const batch = await deps.store.listPendingMessages(chatSessionId);
161552
+ if (!batch.length) {
161553
+ const state = submitted.state ?? "queued";
161554
+ const returned = state === "cancelled" && submitted.stateReason?.startsWith("client-retained:");
161555
+ const reason = returned ? submitted.stateReason.slice("client-retained:".length) : "unconfirmed";
161556
+ return {
161557
+ accepted: state === "delivered",
161558
+ ...state !== "delivered" ? { reason } : {},
161559
+ queued: !returned,
161560
+ delivered: "",
161561
+ batch: [submitted]
161562
+ };
161563
+ }
161077
161564
  const prepared = await buildChatAttachmentFiles(
161078
161565
  batch.flatMap((m2) => m2.attachments ?? []),
161079
161566
  deps.readBlob ?? (async (ref2) => {
@@ -161089,7 +161576,6 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
161089
161576
  })));
161090
161577
  const batchKey = `append:${chatSessionId}:${batch.map((m2) => m2.id).join(",")}`;
161091
161578
  const ids2 = batch.map((m2) => m2.id);
161092
- await deps.store.markPendingMessages?.(chatSessionId, ids2, "delivering").catch(() => void 0);
161093
161579
  const clientKeys = batch.map((m2) => m2.clientKey).filter((k2) => !!k2);
161094
161580
  const res = await deps.deliver(merged, batchKey, {
161095
161581
  ...clientKeys.length ? { clientKeys } : {},
@@ -161102,7 +161588,7 @@ async function appendNowThroughQueue(chatSessionId, text5, deps, clientKey, atta
161102
161588
  const nextState = res.accepted ? "delivered" : DEFINITELY_NOT_DELIVERED.has(res.reason ?? "") ? "queued" : "unconfirmed";
161103
161589
  const returnToClient = retainLocallyOnFailure && !!clientKey && nextState === "queued";
161104
161590
  if (returnToClient && deps.store.markPendingMessages) {
161105
- await deps.store.markPendingMessages(chatSessionId, [submitted.id], "cancelled", "client-retained");
161591
+ await deps.store.markPendingMessages(chatSessionId, [submitted.id], "cancelled", `client-retained:${res.reason}`);
161106
161592
  await deps.store.markPendingMessages(chatSessionId, ids2.filter((id) => id !== submitted.id), nextState, res.reason);
161107
161593
  } else {
161108
161594
  const transition = deps.store.markPendingMessages?.(chatSessionId, ids2, nextState, res.reason);
@@ -161323,6 +161809,17 @@ var init_workorder_drafts = __esm({
161323
161809
  async forWorkspace(workspace) {
161324
161810
  return [...this.drafts.values()].reverse().find((d) => d.workspace === workspace);
161325
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
+ }
161326
161823
  /** 按 chat 会话取最新一份:agent 不自带 ws(后端生成)时,friday 据本会话 id 取回它刚建的草案。 */
161327
161824
  async forSession(chatSessionId) {
161328
161825
  return [...this.drafts.values()].reverse().find((d) => d.chatSessionId === chatSessionId);
@@ -162369,8 +162866,8 @@ async function buildDynamicWorkorderPlan(input) {
162369
162866
  const explicit = node2.owner ? await validateExplicitOwner(input.registry, node2.owner, node2.type) : null;
162370
162867
  if (explicit?.issue) issues.push(explicit.issue);
162371
162868
  const owner = explicit?.actor ? { role: def.ownerRole ?? "", actor: explicit.actor } : def.ownerRole ? await resolveRole(def.ownerRole, node2.type) : null;
162372
- const staffed = owner?.actor && staffableTypes.has(node2.type);
162373
- if (enforceStaffing && def.ownerRole && !staffed && !input.retainUnstaffedNodes) continue;
162869
+ const staffable = staffableTypes.has(node2.type);
162870
+ if (enforceStaffing && def.ownerRole && !staffable && !input.retainUnstaffedNodes) continue;
162374
162871
  const ownerActor = owner?.actor ?? (def.ownerRole ? `pending:role:${def.ownerRole}` : PENDING_UNASSIGNED);
162375
162872
  availableNodes.push({
162376
162873
  type: node2.type,
@@ -162569,7 +163066,18 @@ async function validateExplicitOwner(registry2, owner, artifactType) {
162569
163066
  return { actor: owner };
162570
163067
  }
162571
163068
  function pickPreferred(actors) {
162572
- 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;
162573
163081
  }
162574
163082
  async function resolveActiveRole(registry2, role, artifactType, roles) {
162575
163083
  if (!registry2) {
@@ -162605,7 +163113,10 @@ async function resolveActiveRole(registry2, role, artifactType, roles) {
162605
163113
  };
162606
163114
  }
162607
163115
  const preferred = pickPreferred(actors);
163116
+ const nodeWord = artifactType ? `${artifactType} \u8282\u70B9` : "\u5BF9\u5E94\u8282\u70B9";
162608
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`;
162609
163120
  return {
162610
163121
  role,
162611
163122
  actor: null,
@@ -162614,27 +163125,25 @@ async function resolveActiveRole(registry2, role, artifactType, roles) {
162614
163125
  severity: "blocking",
162615
163126
  role,
162616
163127
  ...artifactType ? { artifactType } : {},
162617
- 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
162618
163129
  }
162619
163130
  };
162620
163131
  }
162621
- if (preferred.kind === "agent") {
162622
- const binding = await registry2.getBinding(preferred.id);
162623
- if (!binding || binding.status !== "active") {
162624
- 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",
162625
163140
  role,
162626
- actor: null,
162627
- issue: {
162628
- code: "missing_binding",
162629
- severity: "blocking",
162630
- role,
162631
- ...artifactType ? { artifactType } : {},
162632
- // 措辞对两条路都成立:规则路会丢节点、agent/剧本路保留为待岗——**都是「暂不自动派发」**,
162633
- // 而非「暂不创建」(后者在保留待岗后就是假话)。
162634
- 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`
162635
- }
162636
- };
162637
- }
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
+ };
162638
163147
  }
162639
163148
  return { role, actor: preferred.id };
162640
163149
  }
@@ -163061,7 +163570,12 @@ function planningStatusOf(_model, arts, resolveRoleExecutor) {
163061
163570
  const role = a.owner.slice(PENDING_ROLE_PREFIX.length);
163062
163571
  if (seenRoles.has(role)) continue;
163063
163572
  seenRoles.add(role);
163064
- 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
+ });
163065
163579
  }
163066
163580
  if (resolveRoleExecutor) {
163067
163581
  for (const a of arts) {
@@ -163109,7 +163623,12 @@ function draftPlanningStatusOf(spawns, isRoleStaffed, extraBlockingIssues = [])
163109
163623
  if (isRoleStaffed(role)) continue;
163110
163624
  if (seenRoles.has(role)) continue;
163111
163625
  seenRoles.add(role);
163112
- 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
+ });
163113
163632
  }
163114
163633
  }
163115
163634
  for (const extra of extraBlockingIssues) {
@@ -164181,7 +164700,7 @@ async function planChatContinuation(input) {
164181
164700
  };
164182
164701
  }
164183
164702
  var UNFINISHED_OPEN, UNFINISHED_EMPTY;
164184
- var init_continuation = __esm({
164703
+ var init_continuation3 = __esm({
164185
164704
  "../server/src/domains/chat-sessions/continuation.ts"() {
164186
164705
  "use strict";
164187
164706
  init_injected_context();
@@ -194775,6 +195294,7 @@ var init_curator_skill = __esm({
194775
195294
  skills,
194776
195295
  connectorIds,
194777
195296
  maxConcurrent: 4,
195297
+ ...currentConfig?.continuationMaxRounds !== void 0 ? { continuationMaxRounds: currentConfig.continuationMaxRounds } : {},
194778
195298
  ...currentConfig?.batchedContinuation !== void 0 ? { batchedContinuation: currentConfig.batchedContinuation } : {}
194779
195299
  }, by);
194780
195300
  }
@@ -195758,7 +196278,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195758
196278
  out.listDirCode = located.code;
195759
196279
  if (located.location) out.location = located.location;
195760
196280
  out.skipped.push({
195761
- source: `${DELEGATION_OUTPUTS_DIR}/`,
196281
+ source: `${SESSION_OUTPUTS_DIR}/`,
195762
196282
  code: located.code,
195763
196283
  message: located.message ?? locationFailureMessage(located.code)
195764
196284
  });
@@ -195766,7 +196286,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195766
196286
  }
195767
196287
  out.location = located.location;
195768
196288
  }
195769
- const listing = await port.listDir(child, DELEGATION_OUTPUTS_DIR).catch((err) => ({
196289
+ const listing = await port.listDir(child, SESSION_OUTPUTS_DIR).catch((err) => ({
195770
196290
  ok: false,
195771
196291
  code: "INTERNAL",
195772
196292
  message: err instanceof Error ? err.message : String(err)
@@ -195776,11 +196296,11 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195776
196296
  if (listing.code === "NOT_FOUND") return out;
195777
196297
  const code2 = listing.code;
195778
196298
  const message = isLocationFailureCode(code2) ? listing.message ?? locationFailureMessage(code2) : listing.message ?? code2;
195779
- out.skipped.push({ source: `${DELEGATION_OUTPUTS_DIR}/`, code: code2, message });
196299
+ out.skipped.push({ source: `${SESSION_OUTPUTS_DIR}/`, code: code2, message });
195780
196300
  return out;
195781
196301
  }
195782
196302
  out.listDirCode = "OK";
195783
- const queue = [DELEGATION_OUTPUTS_DIR];
196303
+ const queue = [SESSION_OUTPUTS_DIR];
195784
196304
  const files = [];
195785
196305
  let guard = 0;
195786
196306
  let hitFileCap = false;
@@ -195791,7 +196311,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195791
196311
  }
195792
196312
  guard += 1;
195793
196313
  const dir = queue.shift();
195794
- const level = dir === DELEGATION_OUTPUTS_DIR ? listing : await port.listDir(child, dir).catch(() => ({ ok: false, code: "INTERNAL" }));
196314
+ const level = dir === SESSION_OUTPUTS_DIR ? listing : await port.listDir(child, dir).catch(() => ({ ok: false, code: "INTERNAL" }));
195795
196315
  if (!level.ok) {
195796
196316
  out.skipped.push({ source: `${dir}/`, code: level.code, message: level.code });
195797
196317
  continue;
@@ -195817,7 +196337,7 @@ async function prepareOutboundFiles(port, child, dirName, limits = DELEGATION_FI
195817
196337
  });
195818
196338
  }
195819
196339
  for (const file of files) {
195820
- const relative5 = normalizeRelPath(file.rel.slice(DELEGATION_OUTPUTS_DIR.length + 1));
196340
+ const relative5 = normalizeRelPath(file.rel.slice(SESSION_OUTPUTS_DIR.length + 1));
195821
196341
  if (!relative5) {
195822
196342
  out.skipped.push({ source: file.rel, code: "PATH_REJECTED", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF0C\u6CA1\u642C" });
195823
196343
  continue;
@@ -202567,33 +203087,13 @@ async function draftActorResolver(registry2) {
202567
203087
  }
202568
203088
  async function draftRoleStaffingResolver(registry2) {
202569
203089
  if (!registry2) return () => true;
202570
- const actors = await registry2.listActors();
202571
- const staffedRoles = /* @__PURE__ */ new Set();
202572
- for (const a of actors) if (a.status === "active") for (const r of a.roles) staffedRoles.add(r);
202573
- return (role) => staffedRoles.has(role);
203090
+ const assignable = await autoAssignableRoles(registry2);
203091
+ return (role) => assignable.has(role);
202574
203092
  }
202575
203093
  async function draftRolePlannableResolver(registry2) {
202576
203094
  if (!registry2) return async () => true;
202577
- const actors = await registry2.listActors();
202578
- const activeActorsByRole = /* @__PURE__ */ new Map();
202579
- for (const a of actors) {
202580
- if (a.status !== "active") continue;
202581
- for (const r of a.roles) {
202582
- const list2 = activeActorsByRole.get(r) ?? [];
202583
- list2.push(a);
202584
- activeActorsByRole.set(r, list2);
202585
- }
202586
- }
202587
- return async (role) => {
202588
- const list2 = activeActorsByRole.get(role);
202589
- if (!list2 || list2.length === 0) return false;
202590
- for (const a of list2) {
202591
- if (a.kind !== "agent") return true;
202592
- const binding = await registry2.getBinding(a.id);
202593
- if (binding && binding.status === "active") return true;
202594
- }
202595
- return false;
202596
- };
203095
+ const assignable = await autoAssignableRoles(registry2);
203096
+ return async (role) => assignable.has(role);
202597
203097
  }
202598
203098
  async function draftTypeReviewerResolver(schema, registry2) {
202599
203099
  if (!schema || !registry2) return void 0;
@@ -202905,6 +203405,8 @@ async function resolveProposeContent(kernel, blobs, args, artifactId, ctx) {
202905
203405
  }
202906
203406
  function authorizedCommandReceipt(command, args, actor) {
202907
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` };
202908
203410
  case "forceConclude":
202909
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` };
202910
203412
  case "promote":
@@ -204058,7 +204560,7 @@ async function startOasisServer(opts) {
204058
204560
  const leftWaiterIds = /* @__PURE__ */ new Map();
204059
204561
  const LEFT_WAITER_IDS_MAX_DRAFTS = 512;
204060
204562
  const terminalReviews = /* @__PURE__ */ new Map();
204061
- const notifyReviewResolved = (draftId, r, resume, storeFor) => {
204563
+ const notifyReviewResolved = (draftId, r, resume, storeFor, companyId) => {
204062
204564
  if (terminalReviews.has(draftId)) return;
204063
204565
  terminalReviews.set(draftId, r);
204064
204566
  if (terminalReviews.size > 512) {
@@ -204078,7 +204580,7 @@ async function startOasisServer(opts) {
204078
204580
  leftWaiterIds.delete(draftId);
204079
204581
  if (resume?.origin) {
204080
204582
  const needsAgentDelivery = resume.kind === "command" && !hadWaiters;
204081
- void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor);
204583
+ void settleAuthorizationStatusLine(resume.origin, r, resume, draftId, needsAgentDelivery, storeFor, companyId);
204082
204584
  }
204083
204585
  };
204084
204586
  const resolveEngine = opts.resolveEngine ?? (async () => ({ kernel: opts.kernel, oplog: opts.oplog, blobs: opts.blobs, ...opts.registry ? { registry: opts.registry } : {} }));
@@ -204156,6 +204658,14 @@ async function startOasisServer(opts) {
204156
204658
  }
204157
204659
  return null;
204158
204660
  };
204661
+ const outputsPartFor = (chatSessionId, companyId) => {
204662
+ const scan2 = opts.sessionOutputsScan;
204663
+ if (!scan2) return void 0;
204664
+ return async () => {
204665
+ const files = await scan2({ chatSessionId, ...companyId !== void 0 ? { companyId } : {} });
204666
+ return files && files.length > 0 ? { type: "outputs", seq: 0, outputs: files } : null;
204667
+ };
204668
+ };
204159
204669
  async function anchorMessageIdFor(origin, storeFor) {
204160
204670
  if (!origin) return void 0;
204161
204671
  const live = liveChat.messageFor(origin);
@@ -204220,7 +204730,7 @@ async function startOasisServer(opts) {
204220
204730
  const label = ctx.effectLabel || ctx.command || "";
204221
204731
  return approved ? `\u547D\u4EE4\u5DF2\u7531 ${by} \u6279\u51C6\u5E76\u4EE5\u5176\u540D\u4E49\u6267\u884C${label ? `\uFF1A${label}` : ""}\u3002` : `\u547D\u4EE4\u5DF2\u88AB ${by} \u9A73\u56DE${reason ? `\uFF1A${reason}` : ""}\u3002`;
204222
204732
  };
204223
- const settleAuthorizationStatusLine = async (origin, resolution, ctx, draftId, needsAgentDelivery, storeFor) => {
204733
+ const settleAuthorizationStatusLine = async (origin, resolution, ctx, draftId, needsAgentDelivery, storeFor, companyId) => {
204224
204734
  const store = storeFor ? await storeFor().catch(() => null) : opts.chatSession;
204225
204735
  if (!store) return;
204226
204736
  const session = await store.getSession(origin).catch(() => null);
@@ -204293,10 +204803,12 @@ async function startOasisServer(opts) {
204293
204803
  ...dispatched.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: dispatched.supportsLiveProtocol } : {},
204294
204804
  done: dispatched.done
204295
204805
  };
204806
+ const resumeOutputsPart = outputsPartFor(origin, companyId);
204296
204807
  runBackgroundAssistantChatTurn({
204297
204808
  chatStore: store,
204298
204809
  chatSessionId: origin,
204299
- session: bgSession
204810
+ session: bgSession,
204811
+ ...resumeOutputsPart ? { outputsPart: resumeOutputsPart } : {}
204300
204812
  });
204301
204813
  await markDelivered();
204302
204814
  };
@@ -204552,10 +205064,12 @@ async function startOasisServer(opts) {
204552
205064
  ...session.supportsLiveProtocol !== void 0 ? { supportsLiveProtocol: session.supportsLiveProtocol } : {},
204553
205065
  ...session.done ? { done: session.done } : {}
204554
205066
  };
205067
+ const broadcastOutputsPart = outputsPartFor(sessionId, companyId);
204555
205068
  runBackgroundAssistantChatTurn({
204556
205069
  chatStore: store,
204557
205070
  chatSessionId: sessionId,
204558
205071
  session: bgSession,
205072
+ ...broadcastOutputsPart ? { outputsPart: broadcastOutputsPart } : {},
204559
205073
  liveChat,
204560
205074
  liveHandle: {
204561
205075
  runtimeSessionId: session.id,
@@ -205383,11 +205897,7 @@ async function startOasisServer(opts) {
205383
205897
  return;
205384
205898
  }
205385
205899
  }
205386
- const staffedRoles = /* @__PURE__ */ new Set();
205387
- if (principalRegistry) {
205388
- const actorsForStaffing = await principalRegistry.listActors();
205389
- for (const a of actorsForStaffing) if (a.status === "active") for (const r of a.roles) staffedRoles.add(r);
205390
- }
205900
+ const staffedRoles = await autoAssignableRoles(principalRegistry);
205391
205901
  const spawnedForPlanning = body2.plan.ops.filter((op) => op.action === "spawn").filter((op) => op.type !== DELIVERY_SUMMARY_ARTIFACT_TYPE).map((op) => {
205392
205902
  const title = op.title ?? op.description;
205393
205903
  return {
@@ -205533,7 +206043,8 @@ async function startOasisServer(opts) {
205533
206043
  draftId,
205534
206044
  { status: "approved", command, effectLabel: review.command.effectLabel, authorizedBy: actor, receipt },
205535
206045
  { origin: review.origin, kind: "command", authorizedBy: actor, effectLabel: review.command.effectLabel, command },
205536
- chatStoreNowOrNull
206046
+ chatStoreNowOrNull,
206047
+ currentCompanyId
205537
206048
  );
205538
206049
  res.writeHead(200, { "content-type": "application/json" }).end(
205539
206050
  JSON.stringify({ applied: 1, command, artifactId, draftId, draftedBy: review.proposedBy, authorizedBy: actor })
@@ -205795,7 +206306,7 @@ async function startOasisServer(opts) {
205795
206306
  }
205796
206307
  if (url.pathname === "/api/workorder/draft/plan" && req.method === "POST" && wodrafts) {
205797
206308
  try {
205798
- const { workspace, plan, holder } = JSON.parse(rawBody);
206309
+ const { workspace, plan, holder, version: version2 } = JSON.parse(rawBody);
205799
206310
  if (!workspace || !plan || !Array.isArray(plan.ops)) throw new Error("\u9700\u8981 { workspace, plan:{ops:[]} }");
205800
206311
  const planLockedBy = wodrafts.editLockHolder(workspace);
205801
206312
  if (planLockedBy && planLockedBy !== holder) {
@@ -205804,9 +206315,10 @@ async function startOasisServer(opts) {
205804
206315
  );
205805
206316
  return;
205806
206317
  }
205807
- const draft = await wodrafts.forWorkspace(workspace);
206318
+ const draft = version2 === void 0 ? await wodrafts.forWorkspace(workspace) : await wodrafts.forWorkspaceVersion(workspace, version2);
205808
206319
  if (!draft) {
205809
- 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}` }));
205810
206322
  return;
205811
206323
  }
205812
206324
  const pv = await engine.kernel.previewIntervention({ ...plan, reason: plan.reason || "(draft edit)" });
@@ -206076,7 +206588,7 @@ async function startOasisServer(opts) {
206076
206588
  if (viewName === "intervention-drafts") {
206077
206589
  const origin = url.searchParams.get("origin");
206078
206590
  const workspace = url.searchParams.get("workspace") ?? void 0;
206079
- 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);
206080
206592
  const data2 = reviews.map((r) => ({
206081
206593
  id: r.draftId,
206082
206594
  plan: r.plan,
@@ -206094,7 +206606,8 @@ async function startOasisServer(opts) {
206094
206606
  anchor: r.anchor,
206095
206607
  // 提案 coordinator-command-authorization:命令授权卡的种类 + 命令本体(前端据此渲成命令卡)。
206096
206608
  ...r.kind !== void 0 ? { kind: r.kind } : {},
206097
- ...r.command !== void 0 ? { command: r.command } : {}
206609
+ ...r.command !== void 0 ? { command: r.command } : {},
206610
+ ...r.continuation ? { continuation: r.continuation } : {}
206098
206611
  }));
206099
206612
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ data: data2 }));
206100
206613
  return;
@@ -206721,6 +207234,7 @@ ${composed}`;
206721
207234
  await checkpointChain.catch(() => void 0);
206722
207235
  await itemLedger.drain();
206723
207236
  const projectedContent = itemLedger.projectContent();
207237
+ const chatOutputsPart = outputsPartFor(persistTarget.id, currentCompanyId);
206724
207238
  await finalizeAssistantRow({
206725
207239
  chatStore,
206726
207240
  chatSessionId: persistTarget.id,
@@ -206730,6 +207244,7 @@ ${composed}`;
206730
207244
  status,
206731
207245
  ...session.runId ? { runId: session.runId } : {},
206732
207246
  ...heldTurn ? { turnId: heldTurn.id } : {},
207247
+ ...chatOutputsPart ? { outputsPart: chatOutputsPart } : {},
206733
207248
  now: () => finishedAt
206734
207249
  });
206735
207250
  await heldTurn?.settle({
@@ -208228,7 +208743,7 @@ var init_server3 = __esm({
208228
208743
  init_planner();
208229
208744
  init_chat_parts();
208230
208745
  init_injected_context();
208231
- init_continuation();
208746
+ init_continuation3();
208232
208747
  init_chat_turn_gate();
208233
208748
  init_chat_workorder_files();
208234
208749
  init_chat_item_ledger();
@@ -208241,7 +208756,7 @@ var init_server3 = __esm({
208241
208756
  init_assembly();
208242
208757
  init_child_live_turn();
208243
208758
  init_src8();
208244
- init_continuation();
208759
+ init_continuation3();
208245
208760
  githubAppPending = new PendingAppCreations();
208246
208761
  connectorAuthSnapshots = /* @__PURE__ */ new Map();
208247
208762
  CONNECTOR_AUTH_SNAPSHOT_TTL_MS = 30 * 60 * 1e3;
@@ -213729,6 +214244,17 @@ var init_dev_store = __esm({
213729
214244
  /** ADR-0166 D6 待发队列(每会话一条,按 seq 升序 = 入队序)。 */
213730
214245
  pending = /* @__PURE__ */ new Map();
213731
214246
  pendingSeq = 0;
214247
+ async claimPendingInsert(m2) {
214248
+ const list2 = this.pending.get(m2.chatSessionId) ?? [];
214249
+ const prior = list2.find((x2) => x2.id === m2.id) ?? list2.find((x2) => m2.clientKey && x2.clientKey === m2.clientKey && (x2.state ?? "queued") === "queued");
214250
+ if (prior) return { submitted: prior, batch: [] };
214251
+ const submitted = { ...m2, state: "queued", stateAt: m2.createdAt, seq: ++this.pendingSeq };
214252
+ const all2 = [...list2, submitted];
214253
+ const batch = all2.filter((x2) => (x2.state ?? "queued") === "queued");
214254
+ this.pending.set(m2.chatSessionId, all2);
214255
+ this.setState(m2.chatSessionId, new Set(batch.map((x2) => x2.id)), "delivering", "append-now");
214256
+ return { submitted, batch };
214257
+ }
213732
214258
  async enqueuePendingMessage(m2) {
213733
214259
  const list2 = this.pending.get(m2.chatSessionId) ?? [];
213734
214260
  if (m2.clientKey) {
@@ -214096,6 +214622,11 @@ var init_dev_store = __esm({
214096
214622
  return r;
214097
214623
  }
214098
214624
  // 待发队列也要落盘——它存在的全部理由就是"别只活在一处内存里"(ADR-0166 D6)。
214625
+ async claimPendingInsert(m2) {
214626
+ const result = await super.claimPendingInsert(m2);
214627
+ this.save();
214628
+ return result;
214629
+ }
214099
214630
  async enqueuePendingMessage(m2) {
214100
214631
  const record8 = await super.enqueuePendingMessage(m2);
214101
214632
  this.save();
@@ -214943,6 +215474,74 @@ var init_company_scoped_registry = __esm({
214943
215474
  }
214944
215475
  });
214945
215476
 
215477
+ // ../server/src/domains/chat-sessions/outputs-scan.ts
215478
+ function joinRel(dir, name) {
215479
+ return dir ? `${dir}/${name}` : name;
215480
+ }
215481
+ async function scanSessionOutputs(bridge, loc, now = Date.now) {
215482
+ const deadline = now() + BUDGET_MS;
215483
+ const out = [];
215484
+ let listCalls = 0;
215485
+ const listDir = (path41) => {
215486
+ listCalls += 1;
215487
+ return bridge.list(loc.nodeId, {
215488
+ workdirKey: loc.workdirKey,
215489
+ runtimeKind: loc.runtimeKind,
215490
+ rtId: loc.rtId,
215491
+ path: path41
215492
+ });
215493
+ };
215494
+ const root = await listDir(SESSION_OUTPUTS_DIR).catch(() => null);
215495
+ if (!root) return null;
215496
+ if (!root.ok) {
215497
+ return root.code === "NOT_FOUND" ? [] : null;
215498
+ }
215499
+ let level = [{ path: SESSION_OUTPUTS_DIR, outcome: root }];
215500
+ for (let depth = 0; depth <= MAX_DEPTH; depth += 1) {
215501
+ const nextDirs = [];
215502
+ for (const { path: dir, outcome } of level) {
215503
+ if (!outcome.ok) continue;
215504
+ for (const entry of outcome.entries) {
215505
+ if (entry.type === "dir") {
215506
+ nextDirs.push(joinRel(dir, entry.name));
215507
+ continue;
215508
+ }
215509
+ if (out.length >= MAX_ENTRIES) continue;
215510
+ out.push({ path: joinRel(dir, entry.name), size: entry.size, mtime: entry.mtime });
215511
+ }
215512
+ }
215513
+ if (nextDirs.length === 0 || depth === MAX_DEPTH) break;
215514
+ if (out.length >= MAX_ENTRIES || listCalls >= MAX_LIST_CALLS || now() >= deadline) break;
215515
+ const batch = [];
215516
+ for (const dir of nextDirs) {
215517
+ if (listCalls >= MAX_LIST_CALLS || now() >= deadline) break;
215518
+ const outcome = await listDir(dir).catch(() => null);
215519
+ if (outcome) batch.push({ path: dir, outcome });
215520
+ }
215521
+ level = batch;
215522
+ }
215523
+ return out;
215524
+ }
215525
+ async function settleSessionOutputs(deps) {
215526
+ const scanned = await scanSessionOutputs(deps.bridge, deps.loc, deps.now);
215527
+ if (scanned === null) return null;
215528
+ const previous3 = await deps.readFingerprints(deps.sessionId).catch(() => null);
215529
+ const diff = diffSessionOutputs(scanned, previous3 ?? void 0);
215530
+ await deps.writeFingerprints(deps.sessionId, diff.fingerprints).catch(() => void 0);
215531
+ return diff;
215532
+ }
215533
+ var MAX_DEPTH, MAX_ENTRIES, MAX_LIST_CALLS, BUDGET_MS;
215534
+ var init_outputs_scan = __esm({
215535
+ "../server/src/domains/chat-sessions/outputs-scan.ts"() {
215536
+ "use strict";
215537
+ init_src();
215538
+ MAX_DEPTH = 3;
215539
+ MAX_ENTRIES = 100;
215540
+ MAX_LIST_CALLS = 12;
215541
+ BUDGET_MS = 5e3;
215542
+ }
215543
+ });
215544
+
214946
215545
  // ../server/src/org-model-backfill.ts
214947
215546
  function resolveDisplayName(existing, humanActor, accountName) {
214948
215547
  const pick3 = existing?.trim() || humanActor?.name?.trim() || accountName?.trim();
@@ -219694,6 +220293,7 @@ var init_service5 = __esm({
219694
220293
  connectorIds: patch.connectorIds ?? latest?.connectorIds ?? [],
219695
220294
  maxConcurrent: patch.maxConcurrent ?? latest?.maxConcurrent ?? 4,
219696
220295
  batchedContinuation: patch.batchedContinuation ?? latest?.batchedContinuation ?? false,
220296
+ continuationMaxRounds: patch.continuationMaxRounds ?? latest?.continuationMaxRounds ?? 3,
219697
220297
  extraArgs: patch.extraArgs ?? latest?.extraArgs ?? [],
219698
220298
  createdBy: by,
219699
220299
  createdAt: this.now(),
@@ -219721,6 +220321,7 @@ var init_service5 = __esm({
219721
220321
  connectorIds: [...target.connectorIds],
219722
220322
  maxConcurrent: target.maxConcurrent,
219723
220323
  batchedContinuation: target.batchedContinuation ?? false,
220324
+ continuationMaxRounds: target.continuationMaxRounds ?? 3,
219724
220325
  extraArgs: [...target.extraArgs ?? []],
219725
220326
  createdBy: by,
219726
220327
  createdAt: this.now(),
@@ -220609,12 +221210,13 @@ ${input.description}
220609
221210
  };
220610
221211
  mask = (plain) => plain.length <= 4 ? "\u2022\u2022\u2022\u2022" : `${"\u2022".repeat(4)}${plain.slice(-4)}`;
220611
221212
  changedKeys = (prev, next) => {
220612
- if (!prev) return ["prompt", "model", "skills", "connectorIds", "maxConcurrent", "batchedContinuation", "extraArgs"];
221213
+ if (!prev) return ["prompt", "model", "skills", "connectorIds", "maxConcurrent", "batchedContinuation", "continuationMaxRounds", "extraArgs"];
220613
221214
  const keys = [];
220614
221215
  if (prev.prompt !== next.prompt) keys.push("prompt");
220615
221216
  if (prev.model !== next.model) keys.push("model");
220616
221217
  if (JSON.stringify(prev.skills) !== JSON.stringify(next.skills)) keys.push("skills");
220617
221218
  if (JSON.stringify(prev.connectorIds) !== JSON.stringify(next.connectorIds)) keys.push("connectorIds");
221219
+ if ((prev.continuationMaxRounds ?? 3) !== (next.continuationMaxRounds ?? 3)) keys.push("continuationMaxRounds");
220618
221220
  if ((prev.batchedContinuation ?? false) !== (next.batchedContinuation ?? false)) keys.push("batchedContinuation");
220619
221221
  if (prev.maxConcurrent !== next.maxConcurrent) keys.push("maxConcurrent");
220620
221222
  if (JSON.stringify(prev.extraArgs ?? []) !== JSON.stringify(next.extraArgs ?? [])) keys.push("extraArgs");
@@ -221367,11 +221969,11 @@ function aggregateUsageByActor(runs) {
221367
221969
  function weekWindow(now = /* @__PURE__ */ new Date()) {
221368
221970
  return { from: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3).toISOString(), to: now.toISOString() };
221369
221971
  }
221370
- async function listWeeklyUsageByActor(trace) {
221972
+ async function listWeeklyUsageByActor(trace, artifactIds) {
221371
221973
  if (!trace?.listUsageRuns) return null;
221372
221974
  const { from, to } = weekWindow();
221373
221975
  try {
221374
- return aggregateUsageByActor(await trace.listUsageRuns({ from, to }));
221976
+ return aggregateUsageByActor(await trace.listUsageRuns({ from, to, ...artifactIds ? { artifactIds: [...artifactIds] } : {} }));
221375
221977
  } catch {
221376
221978
  return null;
221377
221979
  }
@@ -221973,9 +222575,15 @@ function actorsDomain(opts) {
221973
222575
  const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
221974
222576
  const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
221975
222577
  const consoleRevealLimiter = new RevealRateLimiter(consoleRevealLimits.perSession, consoleRevealLimits.perSessionPerKey);
222578
+ async function usageFor(companyId, ctx) {
222579
+ const source = opts.resolveTrace ? await opts.resolveTrace(companyId) : { store: opts.trace?.(), companyScoped: false };
222580
+ const artifactIds = companyId && !source?.companyScoped ? new Set(ctx.getModel?.().artifacts.keys() ?? []) : void 0;
222581
+ return { usage: source?.store, ...artifactIds ? { artifactIds } : {} };
222582
+ }
221976
222583
  return (router) => {
221977
222584
  router.get("/api/actors", async (req) => {
221978
- const { service } = await resolveCtx(req.auth.companyId);
222585
+ const ctx = await resolveCtx(req.auth.companyId);
222586
+ const { service } = ctx;
221979
222587
  const filter = {};
221980
222588
  const kind = req.query.get("kind");
221981
222589
  const teamId = req.query.get("team_id");
@@ -221987,9 +222595,10 @@ function actorsDomain(opts) {
221987
222595
  const includeDeleted = req.query.get("include_deleted") === "1";
221988
222596
  const items = status || includeDeleted ? all2 : all2.filter((a) => a.kind !== "agent" || !isDeletedActor(a));
221989
222597
  const agentIds = items.filter((a) => a.kind === "agent").map((a) => a.id);
222598
+ const usageSource = await usageFor(req.auth.companyId, ctx).catch(() => ({ usage: void 0, artifactIds: void 0 }));
221990
222599
  const [configs, usageByActor] = await Promise.all([
221991
222600
  service.latestConfigs(agentIds),
221992
- listWeeklyUsageByActor(opts.trace?.())
222601
+ listWeeklyUsageByActor(usageSource.usage, usageSource.artifactIds)
221993
222602
  ]);
221994
222603
  return {
221995
222604
  status: 200,
@@ -222131,6 +222740,9 @@ function actorsDomain(opts) {
222131
222740
  checkStringArray("skills");
222132
222741
  checkStringArray("connectorIds");
222133
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
+ }
222134
222746
  const rawExtra = b2.extraArgs;
222135
222747
  if (rawExtra !== void 0 && typeof rawExtra !== "string" && !(Array.isArray(rawExtra) && rawExtra.every((x2) => typeof x2 === "string"))) {
222136
222748
  throw new ApiError(400, "BAD_REQUEST", "extraArgs \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u5B57\u7B26\u4E32\u6570\u7EC4");
@@ -222152,6 +222764,7 @@ function actorsDomain(opts) {
222152
222764
  ...b2.skills !== void 0 ? { skills: b2.skills } : {},
222153
222765
  ...b2.connectorIds !== void 0 ? { connectorIds: b2.connectorIds } : {},
222154
222766
  ...typeof b2.maxConcurrent === "number" ? { maxConcurrent: b2.maxConcurrent } : {},
222767
+ ...b2.continuationMaxRounds !== void 0 ? { continuationMaxRounds: b2.continuationMaxRounds } : {},
222155
222768
  ...b2.batchedContinuation !== void 0 ? { batchedContinuation: b2.batchedContinuation } : {},
222156
222769
  ...extraArgs !== void 0 ? { extraArgs } : {}
222157
222770
  },
@@ -222665,9 +223278,8 @@ function actorsDomain(opts) {
222665
223278
  return {
222666
223279
  status: 200,
222667
223280
  body: await actorStats(req.params.id, {
222668
- usage: req.auth.companyId && !model ? void 0 : opts.trace?.(),
222669
- model,
222670
- ...req.auth.companyId ? { artifactIds: new Set(model?.artifacts.keys() ?? []) } : {}
223281
+ ...await usageFor(req.auth.companyId, ctx),
223282
+ model
222671
223283
  })
222672
223284
  };
222673
223285
  });
@@ -223251,7 +223863,7 @@ function createActorsDomain(opts) {
223251
223863
  return {
223252
223864
  service: defaultCtx.service,
223253
223865
  resolveCtx,
223254
- register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.isCompanyManager ? { isCompanyManager: opts.isCompanyManager } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
223866
+ register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.isCompanyManager ? { isCompanyManager: opts.isCompanyManager } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
223255
223867
  };
223256
223868
  }
223257
223869
  var import_node_crypto49;
@@ -225716,6 +226328,7 @@ var init_activity_trace = __esm({
225716
226328
 
225717
226329
  // ../server/src/domains/collab/inbox.ts
225718
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}`;
225719
226332
  const why = r.plan.reason.slice(0, 100);
225720
226333
  if (r.kind === "command" && r.command) {
225721
226334
  return `\u7BA1\u7406\u8005\u8BF7\u6C42\u6267\u884C\u4E00\u4E2A\u9AD8\u5371\u64CD\u4F5C\uFF0C\u7B49\u4F60\u6388\u6743\uFF1A${r.command.effectLabel}`;
@@ -225876,7 +226489,7 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
225876
226489
  const anchorOwner = model.artifacts.get(r.anchor)?.owner;
225877
226490
  const isOwnerRecipient = creatorOf.get(r.workspace) === me;
225878
226491
  const isLeadRecipient = !!anchorOwner && !!leadOf && leadOf(anchorOwner) === me;
225879
- if (!isOwnerRecipient && !isLeadRecipient) continue;
226492
+ if (r.continuation ? r.continuation.approverActorId !== me : !isOwnerRecipient && !isLeadRecipient) continue;
225880
226493
  const anchorArt = model.artifacts.get(r.anchor);
225881
226494
  const opCount = r.plan.ops?.length ?? 0;
225882
226495
  out.push({
@@ -228494,7 +229107,7 @@ function reasonFromWorkdirCode(code2) {
228494
229107
  return "ERROR";
228495
229108
  }
228496
229109
  }
228497
- function joinRel(dir, name) {
229110
+ function joinRel2(dir, name) {
228498
229111
  return dir ? `${dir}/${name}` : name;
228499
229112
  }
228500
229113
  async function collectWorkdirFiles(bridge, loc, now = Date.now) {
@@ -228527,14 +229140,14 @@ async function collectWorkdirFiles(bridge, loc, now = Date.now) {
228527
229140
  if (outcome.truncated) truncated = true;
228528
229141
  for (const entry of outcome.entries) {
228529
229142
  if (entry.type === "dir") {
228530
- nextDirs.push(joinRel(dir, entry.name));
229143
+ nextDirs.push(joinRel2(dir, entry.name));
228531
229144
  continue;
228532
229145
  }
228533
229146
  if (items.length >= WORKDIR_WALK_MAX_ENTRIES) {
228534
229147
  truncated = true;
228535
229148
  continue;
228536
229149
  }
228537
- const rel = joinRel(dir, entry.name);
229150
+ const rel = joinRel2(dir, entry.name);
228538
229151
  items.push({
228539
229152
  id: `workdir:${rel}`,
228540
229153
  name: entry.name,
@@ -231227,6 +231840,9 @@ function nodesDomain(deps) {
231227
231840
  const rts = runtimesByNode.get(n.id) ?? [];
231228
231841
  const daemonVersion = daemonVersionById.get(n.id);
231229
231842
  const connection = connectionById.get(n.id);
231843
+ const nodeOffline = !onlineIds.has(n.id);
231844
+ const lastDisconnectedAt = connection?.lastDisconnectedAt !== void 0 ? new Date(connection.lastDisconnectedAt).toISOString() : nodeOffline ? n.lastSeenAt : void 0;
231845
+ const lastReconnectedAt = connection?.lastReconnectedAt !== void 0 ? new Date(connection.lastReconnectedAt).toISOString() : void 0;
231230
231846
  const activeRunCount = deps.activeRunCountOf?.(n.id) ?? 0;
231231
231847
  const updateStatus = reconcileNodeUpdate(n.id, daemonVersion, activeRunCount, healthNow);
231232
231848
  return {
@@ -231240,10 +231856,10 @@ function nodesDomain(deps) {
231240
231856
  ...sourceIpById.get(n.id) ? { sourceIp: sourceIpById.get(n.id) } : {},
231241
231857
  ...n.name ? { name: n.name } : {},
231242
231858
  ...updateStatus ? { updateStatus } : {},
231243
- ...connection ? {
231859
+ ...lastDisconnectedAt || lastReconnectedAt ? {
231244
231860
  connection: {
231245
- ...connection.lastDisconnectedAt ? { lastDisconnectedAt: new Date(connection.lastDisconnectedAt).toISOString() } : {},
231246
- ...connection.lastReconnectedAt ? { lastReconnectedAt: new Date(connection.lastReconnectedAt).toISOString() } : {}
231861
+ ...lastDisconnectedAt ? { lastDisconnectedAt } : {},
231862
+ ...lastReconnectedAt ? { lastReconnectedAt } : {}
231247
231863
  }
231248
231864
  } : {},
231249
231865
  ...deps.nodeHealth ? {
@@ -241578,6 +242194,7 @@ var init_postgres_registry = __esm({
241578
242194
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS max_concurrent integer NOT NULL DEFAULT 4`);
241579
242195
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS reviewer_ids jsonb NOT NULL DEFAULT '[]'::jsonb`);
241580
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)`);
241581
242198
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS extra_args jsonb NOT NULL DEFAULT '[]'::jsonb`);
241582
242199
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS memory_enabled boolean NOT NULL DEFAULT true`);
241583
242200
  await pool.query(`
@@ -241829,8 +242446,8 @@ var init_postgres_registry = __esm({
241829
242446
  const r = await this.pool.query(
241830
242447
  // reviewer_ids 列保留(历史数据)但不再写入——决策 0029 删了 actor 级 reviewer override;列有 DEFAULT '[]' 自动填。
241831
242448
  `INSERT INTO ${this.s}.actor_configs
241832
- (actor_id, version, prompt, model, skills, connector_ids, max_concurrent, batched_continuation, extra_args, created_by, created_at, source, rolled_back_from)
241833
- 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
241834
242451
  WHERE (SELECT COALESCE(MAX(version), 0) + 1 FROM ${this.s}.actor_configs WHERE actor_id = $1) = $2::int`,
241835
242452
  [
241836
242453
  c.actorId,
@@ -241845,7 +242462,8 @@ var init_postgres_registry = __esm({
241845
242462
  c.createdBy,
241846
242463
  c.createdAt,
241847
242464
  c.source,
241848
- c.rolledBackFrom ?? null
242465
+ c.rolledBackFrom ?? null,
242466
+ c.continuationMaxRounds ?? 3
241849
242467
  ]
241850
242468
  );
241851
242469
  if (r.rowCount !== 1) throw new Error(`config version conflict for ${c.actorId}: got ${c.version}`);
@@ -242297,6 +242915,7 @@ var init_postgres_registry = __esm({
242297
242915
  connectorIds: row.connector_ids,
242298
242916
  maxConcurrent: row.max_concurrent ?? 4,
242299
242917
  batchedContinuation: row.batched_continuation ?? false,
242918
+ continuationMaxRounds: row.continuation_max_rounds ?? 3,
242300
242919
  extraArgs: row.extra_args ?? [],
242301
242920
  // 注意:memory_enabled 是死列,不映射进 ActorConfig——记忆开关在 agent_prefs。
242302
242921
  createdBy: row.created_by,
@@ -247647,6 +248266,7 @@ var init_postgres_chat_sessions = __esm({
247647
248266
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS escalation_id text`);
247648
248267
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS model text`);
247649
248268
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS visibility text`);
248269
+ await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS outputs_fingerprints jsonb`);
247650
248270
  await pool.query(`
247651
248271
  WITH ranked AS (
247652
248272
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -247877,6 +248497,23 @@ var init_postgres_chat_sessions = __esm({
247877
248497
  args
247878
248498
  );
247879
248499
  }
248500
+ /**
248501
+ * `outputs/` 跨轮指纹(2026-09-11 产出卡)。整表覆盖,不做增量合并——判据的真源永远是
248502
+ * 最近一次扫描看到的现场;合并会让「文件被删掉」这件事永远留在表里。
248503
+ */
248504
+ async readOutputFingerprints(sessionId) {
248505
+ const r = await this.pool.query(
248506
+ `SELECT outputs_fingerprints FROM ${this.s}.chat_sessions WHERE id = $1 AND company_id = $2`,
248507
+ [sessionId, this.companyId]
248508
+ );
248509
+ return r.rows[0]?.outputs_fingerprints ?? null;
248510
+ }
248511
+ async writeOutputFingerprints(sessionId, fingerprints) {
248512
+ await this.pool.query(
248513
+ `UPDATE ${this.s}.chat_sessions SET outputs_fingerprints = $1::jsonb WHERE id = $2 AND company_id = $3`,
248514
+ [JSON.stringify(fingerprints), sessionId, this.companyId]
248515
+ );
248516
+ }
247880
248517
  /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
247881
248518
  async clearProjectBinding(projectId2) {
247882
248519
  if (!projectId2) return 0;
@@ -248279,6 +248916,42 @@ var init_postgres_chat_sessions = __esm({
248279
248916
  return r.rows.map(rowToMessage);
248280
248917
  }
248281
248918
  // ── ADR-0166 D6 待发队列 ─────────────────────────────────────────────────
248919
+ async claimPendingInsert(m2) {
248920
+ const client = await this.pool.connect();
248921
+ try {
248922
+ await client.query("BEGIN");
248923
+ const scope = await client.query(`SELECT id FROM ${this.s}.chat_sessions WHERE id=$1 AND company_id=$2 FOR UPDATE`, [m2.chatSessionId, this.companyId]);
248924
+ if (!scope.rows.length) throw new ChatCompanyScopeError(this.companyId, m2.chatSessionId);
248925
+ const ins = await client.query(
248926
+ `INSERT INTO ${this.s}.chat_pending_messages (id, chat_session_id, text, attachments, client_key, created_at, flavour)
248927
+ VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT DO NOTHING RETURNING *`,
248928
+ [m2.id, m2.chatSessionId, m2.text, m2.attachments ? JSON.stringify(m2.attachments) : null, m2.clientKey ?? null, m2.createdAt, m2.flavour ?? "user"]
248929
+ );
248930
+ if (!ins.rows.length) {
248931
+ const prior = await client.query(
248932
+ `SELECT * FROM ${this.s}.chat_pending_messages WHERE chat_session_id=$1
248933
+ AND (id=$2 OR (client_key=$3 AND state='queued')) ORDER BY (id=$2) DESC LIMIT 1`,
248934
+ [m2.chatSessionId, m2.id, m2.clientKey ?? null]
248935
+ );
248936
+ if (!prior.rows[0]) throw new Error("claimPendingInsert: conflicting submission unavailable");
248937
+ await client.query("COMMIT");
248938
+ return { submitted: rowToPending(prior.rows[0]), batch: [] };
248939
+ }
248940
+ const claimed = await client.query(
248941
+ `UPDATE ${this.s}.chat_pending_messages SET state='delivering', state_at=now(), state_reason='append-now'
248942
+ WHERE chat_session_id=$1 AND state='queued' RETURNING *`,
248943
+ [m2.chatSessionId]
248944
+ );
248945
+ await client.query("COMMIT");
248946
+ return { submitted: rowToPending(ins.rows[0]), batch: claimed.rows.map(rowToPending).sort((a, b2) => a.seq - b2.seq) };
248947
+ } catch (error2) {
248948
+ await client.query("ROLLBACK").catch(() => {
248949
+ });
248950
+ throw error2;
248951
+ } finally {
248952
+ client.release();
248953
+ }
248954
+ }
248282
248955
  async enqueuePendingMessage(m2) {
248283
248956
  await this.assertSessionInScope(m2.chatSessionId);
248284
248957
  const initialState = m2.state;
@@ -252201,6 +252874,7 @@ async function provisionCompanyDataPlaneSchema(pool, schema) {
252201
252874
  reviewer_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
252202
252875
  max_concurrent integer NOT NULL DEFAULT 4,
252203
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),
252204
252878
  extra_args jsonb NOT NULL DEFAULT '[]'::jsonb,
252205
252879
  memory_enabled boolean NOT NULL DEFAULT true,
252206
252880
  created_by text NOT NULL REFERENCES ${s2}.actors(id) ON DELETE RESTRICT,
@@ -252209,6 +252883,7 @@ async function provisionCompanyDataPlaneSchema(pool, schema) {
252209
252883
  rolled_back_from integer,
252210
252884
  PRIMARY KEY (actor_id, version)
252211
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)`);
252212
252887
  await pool.query(`
252213
252888
  CREATE TABLE IF NOT EXISTS ${s2}.agent_prefs (
252214
252889
  actor_id text PRIMARY KEY REFERENCES ${s2}.actors(id) ON DELETE CASCADE,
@@ -252332,6 +253007,7 @@ function rowToConfig2(row) {
252332
253007
  connectorIds: row.connector_ids,
252333
253008
  maxConcurrent: row.max_concurrent,
252334
253009
  batchedContinuation: row.batched_continuation,
253010
+ continuationMaxRounds: row.continuation_max_rounds ?? 3,
252335
253011
  extraArgs: row.extra_args ?? [],
252336
253012
  createdBy: row.created_by,
252337
253013
  createdAt: new Date(row.created_at).toISOString(),
@@ -252614,8 +253290,8 @@ var init_company_owned_registry_store = __esm({
252614
253290
  async appendConfig(c) {
252615
253291
  const r = await this.pool.query(
252616
253292
  `INSERT INTO ${this.s}.actor_configs
252617
- (actor_id, version, prompt, model, skills, connector_ids, max_concurrent, batched_continuation, extra_args, created_by, created_at, source, rolled_back_from)
252618
- 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
252619
253295
  WHERE (SELECT COALESCE(MAX(version), 0) + 1 FROM ${this.s}.actor_configs WHERE actor_id = $1) = $2::int`,
252620
253296
  [
252621
253297
  c.actorId,
@@ -252630,7 +253306,8 @@ var init_company_owned_registry_store = __esm({
252630
253306
  c.createdBy,
252631
253307
  c.createdAt,
252632
253308
  c.source,
252633
- c.rolledBackFrom ?? null
253309
+ c.rolledBackFrom ?? null,
253310
+ c.continuationMaxRounds ?? 3
252634
253311
  ]
252635
253312
  );
252636
253313
  if (r.rowCount !== 1) throw new Error(`config version conflict for ${c.actorId}: got ${c.version}`);
@@ -253695,9 +254372,19 @@ var init_cutover_aware_trace = __esm({
253695
254372
  constructor(o) {
253696
254373
  this.o = o;
253697
254374
  }
253698
- async pick() {
254375
+ /** Resolve store and isolation together, from one cutover snapshot. Read-only consumers
254376
+ * must not infer company isolation merely from having a companyId. */
254377
+ async readTarget() {
253699
254378
  const row = await this.o.cutover.get(this.o.companyId, "trace");
253700
- return this.o.cutover.readsCompanySchema(row) ? this.o.owned : this.o.legacy;
254379
+ const companyScoped = this.o.cutover.readsCompanySchema(row);
254380
+ return { store: companyScoped ? this.o.owned : this.o.legacy, companyScoped };
254381
+ }
254382
+ /** Expose only usage reads: callers cannot bypass the frozen-write fence. */
254383
+ async resolveUsageReadTarget() {
254384
+ return this.readTarget();
254385
+ }
254386
+ async pick() {
254387
+ return (await this.readTarget()).store;
253701
254388
  }
253702
254389
  async pickWritable() {
253703
254390
  const row = await this.o.cutover.get(this.o.companyId, "trace");
@@ -254676,8 +255363,8 @@ async function runIdentityMigration(input) {
254676
255363
  const ins = await client.query(
254677
255364
  `INSERT INTO ${cs}.actor_configs
254678
255365
  (actor_id,version,prompt,model,skills,connector_ids,reviewer_ids,max_concurrent,
254679
- batched_continuation,extra_args,memory_enabled,created_by,created_at,source,rolled_back_from)
254680
- 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)
254681
255368
  ON CONFLICT (actor_id, version) DO NOTHING`,
254682
255369
  [
254683
255370
  r.actor_id,
@@ -254694,7 +255381,8 @@ async function runIdentityMigration(input) {
254694
255381
  r.created_by,
254695
255382
  r.created_at,
254696
255383
  r.source,
254697
- r.rolled_back_from ?? null
255384
+ r.rolled_back_from ?? null,
255385
+ r.continuation_max_rounds ?? 3
254698
255386
  ]
254699
255387
  );
254700
255388
  plan.configRows += ins.rowCount ?? 0;
@@ -257034,6 +257722,9 @@ var init_cutover_aware_workorder_drafts = __esm({
257034
257722
  async forWorkspace(w2) {
257035
257723
  return (await this.pick()).forWorkspace(w2);
257036
257724
  }
257725
+ async forWorkspaceVersion(w2, version2) {
257726
+ return (await this.pick()).forWorkspaceVersion(w2, version2);
257727
+ }
257037
257728
  async forSession(s2) {
257038
257729
  return (await this.pick()).forSession(s2);
257039
257730
  }
@@ -257981,6 +258672,7 @@ var init_src11 = __esm({
257981
258672
  init_tenancy_move_verify();
257982
258673
  init_company_scoped_registry();
257983
258674
  init_session_workdir();
258675
+ init_outputs_scan();
257984
258676
  init_org_model_backfill();
257985
258677
  init_types_admin();
257986
258678
  init_roles_admin();
@@ -258045,7 +258737,7 @@ var init_src11 = __esm({
258045
258737
  init_execution_continuity2();
258046
258738
  init_chat_sessions();
258047
258739
  init_dev_store();
258048
- init_continuation();
258740
+ init_continuation3();
258049
258741
  init_knowledge2();
258050
258742
  init_delegations();
258051
258743
  init_automations();
@@ -258390,6 +259082,13 @@ init_src11();
258390
259082
  init_src();
258391
259083
  var BRAINSTORM_LEGACY_LINE = "\u8BF7\u76F4\u63A5\u56DE\u7B54\u7528\u6237\u95EE\u9898\u3002\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5199\u5165\u7CFB\u7EDF\uFF0C\u5426\u5219\u4E0D\u8981\u521B\u5EFA\u6216\u4FEE\u6539 artifact\u3002";
258392
259084
  var BRAINSTORM_ARTIFACT_GUARD = "\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5199\u5165\u7CFB\u7EDF\uFF0C\u5426\u5219\u4E0D\u8981\u521B\u5EFA\u6216\u4FEE\u6539 artifact\u3002";
259085
+ var OUTPUTS_CONVENTION = [
259086
+ "## \u4EA7\u51FA\u653E\u54EA",
259087
+ "",
259088
+ "\u8981\u7ED9\u7528\u6237\u770B\u7684\u6210\u54C1\u6587\u4EF6\uFF0C**\u653E\u8FDB\u5DE5\u4F5C\u76EE\u5F55\u4E0B\u7684 `outputs/`**\uFF08\u53EF\u4EE5\u6709\u5B50\u76EE\u5F55\uFF0C\u7ED3\u6784\u81EA\u5DF1\u5B9A\uFF09\u3002",
259089
+ "\u53EA\u6709\u8FD9\u4E2A\u76EE\u5F55\u91CC\u7684\u6587\u4EF6\u4F1A\u5728\u5BF9\u8BDD\u91CC\u663E\u793A\u6210\u53EF\u70B9\u5F00\u7684\u6587\u4EF6\u5361\u7247\uFF1B\u5199\u5728\u522B\u5904\u7684\u7528\u6237\u770B\u4E0D\u89C1\u3002",
259090
+ "\u4E2D\u95F4\u4EA7\u7269\u3001\u4E34\u65F6\u811A\u672C\u3001\u8349\u7A3F\u4E0D\u8981\u653E\u8FD9\u91CC\u2014\u2014\u653E\u8FDB\u53BB\u5C31\u4F1A\u6446\u5230\u7528\u6237\u9762\u524D\u3002"
259091
+ ].join("\n");
258393
259092
  function buildChatTaskHeader(input) {
258394
259093
  if (input.kind === "isolated-knowledge") {
258395
259094
  return {
@@ -258410,7 +259109,9 @@ function buildChatTaskHeader(input) {
258410
259109
  `your_actor_id: ${input.actorId}`,
258411
259110
  `workspace: ${input.workspace}`,
258412
259111
  "",
258413
- CONVERSATIONAL_MANAGER_BODY
259112
+ CONVERSATIONAL_MANAGER_BODY,
259113
+ "",
259114
+ OUTPUTS_CONVENTION
258414
259115
  ]
258415
259116
  };
258416
259117
  }
@@ -258422,7 +259123,9 @@ function buildChatTaskHeader(input) {
258422
259123
  input.sessionLine,
258423
259124
  `your_actor_id: ${input.actorId}`,
258424
259125
  "",
258425
- routed ? BRAINSTORM_ARTIFACT_GUARD : BRAINSTORM_LEGACY_LINE
259126
+ routed ? BRAINSTORM_ARTIFACT_GUARD : BRAINSTORM_LEGACY_LINE,
259127
+ // 子会话那一段由 EXPERT_DELEGATION_SYSTEM_PROMPT 讲,这里不重复(见 OUTPUTS_CONVENTION 注释)。
259128
+ ...input.delegatedChildTurn ? [] : ["", OUTPUTS_CONVENTION]
258426
259129
  ],
258427
259130
  ...routed ? { routingSystemPrompt: EXECUTION_ROUTING_BODY } : {}
258428
259131
  };
@@ -258571,9 +259274,9 @@ function render(reason, facts) {
258571
259274
  switch (reason.kind) {
258572
259275
  case "checkpoints-exhausted":
258573
259276
  return {
258574
- 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`,
258575
259278
  detail: facts,
258576
- 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"
258577
259280
  };
258578
259281
  case "gate-rejections-exhausted":
258579
259282
  return {
@@ -259597,6 +260300,12 @@ async function startServe(opts) {
259597
260300
  kernel: defaultCompanyKernel,
259598
260301
  blobs: assets,
259599
260302
  trace: () => traceStoreForActors,
260303
+ // Same current-company ledger as chat/dispatch writes (PR #1223). Resolve the
260304
+ // physical read target and its isolation together; never treat legacy as owned.
260305
+ resolveTrace: async (cid) => {
260306
+ const store = await traceStoreFor(cid);
260307
+ return store instanceof CutoverAwareTraceStore ? store.resolveUsageReadTarget() : { store, companyScoped: !pgPool };
260308
+ },
259600
260309
  audit: registryAudit,
259601
260310
  // ADR 凭据保险库 §4.5/§4.7 / ADR 0125:reveal 判权的项目解析 + 凭据读取审计读写。
259602
260311
  resolveDispatchScope: resolveDispatchScope2,
@@ -261615,6 +262324,39 @@ async function startServe(opts) {
261615
262324
  // 三个方法的组合规则住在 `createDelegationWorkdirPort`(`@oasis/server`)——评审
261616
262325
  // wo:acfee725 blocker ② 回归防线:**serve.ts 与单测走同一处**,那样「装配层退回全局 binding」
261617
262326
  // 这类事故在单测里就能被复现。绝不许在这里再写一份。
262327
+ /**
262328
+ * 会话产出扫描(2026-09-11 产出卡):轮次收尾扫这条会话的 `outputs/`,交出「这一轮变了哪些」。
262329
+ *
262330
+ * 复用 `delegationWorkdirLocator`(与派发、回流、chat 域**同一个 binding resolver 实例**)——
262331
+ * 委派回程读的就是子会话的 `outputs/`,主会话这条只是把同一套读口用到了自己身上。
262332
+ *
262333
+ * 三处 fail-closed,都返回 `null`(= 这一轮没有产出卡),一处都不猜:
262334
+ * · 桥还没建好 / 会话没绑定 / 节点离线 → 拿不到工作区;
262335
+ * · store 没有指纹方法(老 store / 文件态装配)→ 没地方记「上一轮长什么样」,
262336
+ * 硬扫会把整个 `outputs/` 每轮都当成新增,一口气刷几十张卡;
262337
+ * · 解析不出公司 → **绝不回落默认公司**(否则会去别家公司的库里读写指纹)。
262338
+ */
262339
+ sessionOutputsScan: async ({ chatSessionId, companyId }) => {
262340
+ const bridge = workdirBridge;
262341
+ if (!bridge) return null;
262342
+ const store = chatStoreRouter ? await chatStoreFor(companyId).catch(() => null) : chatSessionStore;
262343
+ const read = store?.readOutputFingerprints?.bind(store);
262344
+ const write = store?.writeOutputFingerprints?.bind(store);
262345
+ if (!read || !write) return null;
262346
+ const located = await locateSessionWorkdir(delegationWorkdirLocator, {
262347
+ chatSessionId,
262348
+ ...companyId !== void 0 ? { companyId } : {}
262349
+ });
262350
+ if (!located.ok) return null;
262351
+ const diff = await settleSessionOutputs({
262352
+ bridge,
262353
+ loc: located.loc,
262354
+ sessionId: chatSessionId,
262355
+ readFingerprints: read,
262356
+ writeFingerprints: write
262357
+ }).catch(() => null);
262358
+ return diff?.changed ?? null;
262359
+ },
261618
262360
  delegationWorkdir: createDelegationWorkdirPort({
261619
262361
  locator: delegationWorkdirLocator,
261620
262362
  // 桥在 hub 建好后(本函数尾部)才赋值 → getter 每次调用时读最新值;早于此处的构造时 `workdirBridge`
@@ -263235,6 +263977,10 @@ async function startServe(opts) {
263235
263977
  return false;
263236
263978
  });
263237
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
+ },
263238
263984
  async dispatchWork(workId, opts2) {
263239
263985
  if (engineDispatchBridges.size === 0) {
263240
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`);
@@ -265300,6 +266046,7 @@ async function runSession(dispatchId, job, deps) {
265300
266046
  const cancelledBeforeStart = deps.wasCancelled?.() ?? false;
265301
266047
  const normalizer = new ProviderStreamNormalizer({
265302
266048
  providerName: job.binding?.runtimeKind ?? "runtime",
266049
+ hasLiveProtocol: handle.supportsLiveProtocol === true,
265303
266050
  fallbackTurnId: `oasis-turn:${handle.id}`,
265304
266051
  emit: (event) => deps.sink.stream({ type: "session_normalized_event", dispatchId, event })
265305
266052
  });
@@ -272392,7 +273139,7 @@ function shimScript() {
272392
273139
  }
272393
273140
 
272394
273141
  // src/index.ts
272395
- var PKG_VERSION = true ? "2.2.19" : "dev";
273142
+ var PKG_VERSION = true ? "2.2.21" : "dev";
272396
273143
  var LOCAL_BIN = localBin();
272397
273144
  var NPM_PREFIX = npmPrefix();
272398
273145
  var INSTANCE = DEFAULT_INSTANCE;