oasis_test 0.1.102 → 0.1.104

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 +710 -123
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -387,7 +387,9 @@ function emptyReadModel() {
387
387
  gaps: /* @__PURE__ */ new Map(),
388
388
  escalations: /* @__PURE__ */ new Map(),
389
389
  pendingReviews: /* @__PURE__ */ new Map(),
390
- pausedWorkorders: /* @__PURE__ */ new Set()
390
+ pausedWorkorders: /* @__PURE__ */ new Set(),
391
+ acceptBlockers: /* @__PURE__ */ new Map()
392
+ // 默认空 = 无挡板 = 历史行为;只有 bridge 从引擎快照刷新时才填
391
393
  };
392
394
  }
393
395
  function indexAdd(index, key, value) {
@@ -1513,6 +1515,15 @@ function stuckDiagnosis(model, id, opts) {
1513
1515
  handler: "none"
1514
1516
  });
1515
1517
  }
1518
+ const blocker = model.acceptBlockers.get(id);
1519
+ if (blocker) {
1520
+ causes.push({
1521
+ category: "accept-barrier",
1522
+ source: "oplog",
1523
+ detail: `\u4EA7\u51FA\u5DF2\u5C31\u7EEA\u4F46\u6536\u53E3\u88AB\u62E6\uFF08${blocker.code}\uFF09\uFF1A${blocker.detail}${blocker.overridable ? "\u2014\u2014\u4EBA\u53EF force-conclude \u7B7E\u5B57\u7A7F\u900F\uFF08\u7559 override \u75D5\u8FF9\uFF09" : "\u2014\u2014\u4E0D\u53EF\u7A7F\u900F\uFF0C\u6309\u4E0A\u9762\u7684\u539F\u56E0\u5148\u4FEE\u6B63\u76EE\u6807"}`,
1524
+ handler: "human"
1525
+ });
1526
+ }
1516
1527
  const rt = opts.runtime;
1517
1528
  if (rt.queued) {
1518
1529
  causes.push({
@@ -4335,6 +4346,93 @@ var init_exit_classify = __esm({
4335
4346
  }
4336
4347
  });
4337
4348
 
4349
+ // ../core/src/produce-resume-fallback.ts
4350
+ function isSessionNotFound(info) {
4351
+ return info.reason === "session-not-found";
4352
+ }
4353
+ function makeProduceResumeFallback(first, spawnFallback) {
4354
+ const stableId2 = first.id;
4355
+ const exitCbs = [];
4356
+ const outCbs = [];
4357
+ const telCbs = [];
4358
+ const outBuf = [];
4359
+ const telBuf = [];
4360
+ let sawOutput = false;
4361
+ let active = first;
4362
+ let fellBack = false;
4363
+ let terminal;
4364
+ const fanOut = (chunk) => {
4365
+ if (outCbs.length) for (const cb of outCbs) cb(chunk);
4366
+ else outBuf.push(chunk);
4367
+ };
4368
+ const fanTel = (event) => {
4369
+ if (telCbs.length) for (const cb of telCbs) cb(event);
4370
+ else telBuf.push(event);
4371
+ };
4372
+ const wire = (s2) => {
4373
+ s2.onOutput?.((c) => {
4374
+ sawOutput = true;
4375
+ fanOut(c);
4376
+ });
4377
+ s2.onTelemetry?.((e) => {
4378
+ sawOutput = true;
4379
+ fanTel(e);
4380
+ });
4381
+ };
4382
+ const fireTerminal = (info) => {
4383
+ if (terminal) return;
4384
+ terminal = info;
4385
+ for (const cb of exitCbs.splice(0)) cb(info);
4386
+ };
4387
+ wire(first);
4388
+ first.onExit((info) => {
4389
+ if (fellBack || sawOutput || !isSessionNotFound(info)) {
4390
+ fireTerminal(info);
4391
+ return;
4392
+ }
4393
+ fellBack = true;
4394
+ void spawnFallback().then(
4395
+ (second) => {
4396
+ active = second;
4397
+ wire(second);
4398
+ second.onExit((info2) => fireTerminal(info2));
4399
+ },
4400
+ // 回落连 spawn 都起不来(节点掉线等)→ 把原始 session-not-found 如实上报,交常规重试兜底。
4401
+ () => fireTerminal(info)
4402
+ );
4403
+ });
4404
+ return {
4405
+ get id() {
4406
+ return stableId2;
4407
+ },
4408
+ kill: () => active.kill(),
4409
+ onExit(cb) {
4410
+ if (terminal) {
4411
+ cb(terminal);
4412
+ return;
4413
+ }
4414
+ exitCbs.push(cb);
4415
+ },
4416
+ onOutput(cb) {
4417
+ for (const c of outBuf.splice(0)) cb(c);
4418
+ outCbs.push(cb);
4419
+ },
4420
+ onTelemetry(cb) {
4421
+ for (const e of telBuf.splice(0)) cb(e);
4422
+ telCbs.push(cb);
4423
+ },
4424
+ appendInput: (input) => active.appendInput?.(input) ?? Promise.resolve({ accepted: false, reason: "unsupported" }),
4425
+ get canAppendInput() {
4426
+ return active.canAppendInput !== false && typeof active.appendInput === "function";
4427
+ }
4428
+ };
4429
+ }
4430
+ var init_produce_resume_fallback = __esm({
4431
+ "../core/src/produce-resume-fallback.ts"() {
4432
+ "use strict";
4433
+ }
4434
+ });
4435
+
4338
4436
  // ../timeout-policy/src/policy.ts
4339
4437
  function classify(actorId) {
4340
4438
  if (!actorId) return "unknown";
@@ -4801,6 +4899,7 @@ var init_dispatcher = __esm({
4801
4899
  "../core/src/dispatcher.ts"() {
4802
4900
  "use strict";
4803
4901
  import_node_crypto2 = require("node:crypto");
4902
+ init_produce_resume_fallback();
4804
4903
  init_read_model();
4805
4904
  init_assembler();
4806
4905
  init_content_type_handler();
@@ -5574,7 +5673,10 @@ var init_dispatcher = __esm({
5574
5673
  action: "produce",
5575
5674
  ...args.part !== void 0 ? { part: args.part } : {},
5576
5675
  ...args.engineWorkId !== void 0 ? { engineWorkId: args.engineWorkId } : {},
5577
- ...args.engineCompanyId !== void 0 ? { engineCompanyId: args.engineCompanyId } : {}
5676
+ ...args.engineCompanyId !== void 0 ? { engineCompanyId: args.engineCompanyId } : {},
5677
+ // 工单 e4a43a42:只有短时重试(同一未收口任务)才接续旧会话;变更请求/gap/级联触发的返工
5678
+ // 一律新会话(`resumeEligible: false`)——否则接续会把过时上下文和旧任务书一起拖进来。缺省=可续。
5679
+ ...args.resumeEligible !== void 0 ? { resumeEligible: args.resumeEligible } : {}
5578
5680
  }, args.bypassScheduling === true, args.dispatchId);
5579
5681
  }
5580
5682
  /** 新引擎回信 work(设计-最终 §回信轮)的 effect 调到此——派发收容回信会话(唯一写动作 `oasis reply`)。
@@ -5820,7 +5922,8 @@ var init_dispatcher = __esm({
5820
5922
  const priorConsistent = prior !== void 0 && prior.nodeId === binding?.nodeId && prior.runtimeKind === binding?.runtimeKind;
5821
5923
  const retainedPartSession = spec.action === "produce" && spec.part !== void 0 && priorConsistent && !prior.sessionMissing ? prior.rtId : void 0;
5822
5924
  const retainedWholeSession = spec.action === "produce" && spec.part === void 0 && priorConsistent && !prior.sessionMissing ? this.resumableWholeSession(spec.artifactId, seqNow, prior.rtId) : void 0;
5823
- const retainedSession = retainedPartSession ?? retainedWholeSession;
5925
+ const resumeEligible = spec.resumeEligible !== false;
5926
+ const retainedSession = resumeEligible ? retainedPartSession ?? retainedWholeSession : void 0;
5824
5927
  const runtimeSessionId = retainedSession ?? (isProduceLike ? (0, import_node_crypto2.randomUUID)() : void 0);
5825
5928
  const resumeSession = retainedSession !== void 0;
5826
5929
  const retry = this.strikes.get(jobKey);
@@ -5938,7 +6041,8 @@ var init_dispatcher = __esm({
5938
6041
  // `handle.id` = `agent_runs.id` = token 的 dispatchId claim,三处同值。
5939
6042
  dispatchId: attempt
5940
6043
  };
5941
- const session = await this.opts.adapter.spawn(job);
6044
+ const first = await this.opts.adapter.spawn(job);
6045
+ const session = resumeSession ? makeProduceResumeFallback(first, () => this.opts.adapter.spawn({ ...job, resumeRuntimeSession: false })) : first;
5942
6046
  if (!this.isSpawnAttemptCurrent(jobKey, attempt)) {
5943
6047
  try {
5944
6048
  await session.kill();
@@ -6995,7 +7099,7 @@ function scan(state, ctx) {
6995
7099
  if (wstate === "success" && node.latestAcceptId !== lw.id) {
6996
7100
  const reqs = state.requirementsOf(node.id);
6997
7101
  if (reqs.length === 0) {
6998
- if (!businessHandoffBarrierSatisfied(state, node.id, lw.id)) continue;
7102
+ if (acceptBlock(state, lw.id) !== null) continue;
6999
7103
  events.push({
7000
7104
  kind: "work.accept",
7001
7105
  workorderId: state.workorder.id,
@@ -7036,7 +7140,11 @@ function tryToRun(state, nodeId, ctx, events) {
7036
7140
  nodeId,
7037
7141
  workId,
7038
7142
  reasons: [],
7039
- continuesWorkId: null
7143
+ continuesWorkId: null,
7144
+ // 工单 e4a43a42(接续只对短时重试成立):**已收口过的节点被重新激活 = 返工**(变更请求/驳回/上游
7145
+ // 变更把已 accept 的节点重开)→ 起全新会话,别接续那场过时的旧对话;**从未收口的节点** = 首跑或
7146
+ // 失败后重试,接续 WIP 是对的。判据 = 有没有 latest_accept。派发层据此决定 resumeSession。
7147
+ resumeEligible: node.latestAcceptId == null
7040
7148
  });
7041
7149
  }
7042
7150
  function inputsMatch(state, node, latestWork2) {
@@ -7164,13 +7272,15 @@ function processCommentReplies(state, nodeId, ctx, events) {
7164
7272
  if (latest) {
7165
7273
  const s2 = workState(latest, hasOutput(latest, 0));
7166
7274
  if (s2 === "success" && latest.acceptanceState !== "accepted") {
7167
- events.push({
7168
- kind: "work.accept",
7169
- workorderId: state.workorder.id,
7170
- workId: latest.id,
7171
- by: "actor:system:engine",
7172
- override: null
7173
- });
7275
+ if (acceptBlock(state, latest.id) === null) {
7276
+ events.push({
7277
+ kind: "work.accept",
7278
+ workorderId: state.workorder.id,
7279
+ workId: latest.id,
7280
+ by: "actor:system:engine",
7281
+ override: null
7282
+ });
7283
+ }
7174
7284
  return;
7175
7285
  }
7176
7286
  }
@@ -7254,9 +7364,9 @@ function checkTerminated(state) {
7254
7364
  if (!isBusinessDependencyEdge(state, edge)) continue;
7255
7365
  if (isEdgeInputUnobtainable(state, edge)) continue;
7256
7366
  const upstream = state.node(edge.fromNodeId);
7257
- if (!upstream?.latestWorkId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestWorkId)) return false;
7367
+ if (!upstream?.latestAcceptId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestAcceptId)) return false;
7258
7368
  }
7259
- return live.every((n) => n.latestWorkId !== null && n.latestAcceptId === n.latestWorkId);
7369
+ return live.every((n) => n.latestAcceptId !== null && (n.latestAcceptId === n.latestWorkId || n.latestAcceptId === n.latestSuccessWorkId));
7260
7370
  }
7261
7371
  function isEdgeInputUnobtainable(state, edge) {
7262
7372
  const upstream = state.node(edge.fromNodeId);
@@ -7267,12 +7377,61 @@ function isBusinessDependencyEdge(state, edge) {
7267
7377
  return edge.kind === "data" && edge.required && downstream?.fields?.[BUSINESS_HANDOFF_POLICY_FIELD] !== BUSINESS_HANDOFF_POLICY_EXEMPT;
7268
7378
  }
7269
7379
  function businessHandoffBarrierSatisfied(state, nodeId, workId) {
7380
+ return handoffBlock(state, nodeId, workId) === null;
7381
+ }
7382
+ function acceptBlock(state, workId) {
7383
+ const w2 = state.work(workId);
7384
+ if (!w2) return { code: "work-missing", detail: `work \u4E0D\u5728\u518C\uFF1A${workId}`, overridable: false };
7385
+ if (w2.deadAt) {
7386
+ return {
7387
+ code: "work-dead",
7388
+ detail: `\u8FD9\u4E00\u7248\u5DF2\u4F5C\u5E9F\uFF08${w2.deadAt}${w2.rejectedReason ? `\uFF1A${w2.rejectedReason}` : ""}\uFF09\u2014\u2014\u4F5C\u5E9F\u7248\u4E0D\u80FD\u6536\u53E3\uFF0C\u6539\u5BF9\u6700\u65B0\u7248\u64CD\u4F5C\u6216\u5148\u91CD\u8DD1\u672C\u8282\u70B9`,
7389
+ overridable: false
7390
+ };
7391
+ }
7392
+ if (w2.acceptanceState === "accepted") {
7393
+ return { code: "already-accepted", detail: `\u5DF2\u662F\u9A8C\u6536\u6001\uFF08${w2.acceptedAt ?? "?"}\uFF09\u2014\u2014\u65E0\u9700\u91CD\u590D\u6536\u53E3`, overridable: false };
7394
+ }
7395
+ const node = state.node(w2.nodeId);
7396
+ if (!node) return { code: "node-missing", detail: `\u8282\u70B9\u4E0D\u5728\u518C\uFF1A${w2.nodeId}`, overridable: false };
7397
+ if (!hasOutput(w2, state.artifactsOf(w2.id).length)) {
7398
+ if (!w2.endedAt) {
7399
+ return {
7400
+ code: "work-in-flight",
7401
+ detail: `\u8FD9\u4E00\u7248\u8FD8\u5728\u8DD1\u3001\u4E14\u8FD8\u6CA1\u6709\u4EFB\u4F55\u4EA7\u51FA\u2014\u2014\u7B49\u5B83\u4EA4\u7A3F\u540E\u518D\u6536\u53E3\uFF0C\u6216\u5148\u505C\u6389\uFF08hold\uFF09/ \u91CD\u8DD1`,
7402
+ overridable: false
7403
+ };
7404
+ }
7405
+ return {
7406
+ code: "no-output",
7407
+ detail: `\u8FD9\u4E00\u7248\u5DF2\u7ED3\u675F\u4F46\u6CA1\u6709\u4EFB\u4F55\u4EA7\u51FA\uFF08\u7A7A\u58F3\uFF09\u2014\u2014\u4E0D\u80FD\u628A\u6CA1\u6709\u5185\u5BB9\u7684\u7248\u672C\u6536\u53E3\u6210\u4EA4\u4ED8\u7248\u672C\uFF1B\u6539\u5BF9\u6700\u8FD1\u4E00\u7248\u6709\u4EA7\u51FA\u7684\u7248\u672C\u64CD\u4F5C\uFF0C\u6216\u91CD\u8DD1\u672C\u8282\u70B9`,
7408
+ overridable: false
7409
+ };
7410
+ }
7411
+ return handoffBlock(state, w2.nodeId, workId);
7412
+ }
7413
+ function handoffBlock(state, nodeId, workId) {
7270
7414
  const work = state.work(workId);
7271
- if (!requiresBusinessHandoff(state, nodeId, work)) return true;
7415
+ if (!requiresBusinessHandoff(state, nodeId, work)) return null;
7416
+ if (work?.acceptanceState === "accepted" && work.overrideBy) return null;
7272
7417
  const node = state.node(nodeId);
7273
- return node?.latestWorkId === workId && (work?.businessHandoffStatus === "delivered" || work?.status === "success" && work.agentHandoffAttemptId === null && work.businessHandoffStatus === null);
7418
+ if (workId !== node?.latestWorkId && workId !== node?.latestSuccessWorkId) {
7419
+ return {
7420
+ code: "not-latest-work",
7421
+ detail: `\u4E0D\u662F\u672C\u8282\u70B9\u6700\u65B0\u7248\uFF08\u6700\u65B0\u5C1D\u8BD5\uFF1A${node?.latestWorkId ?? "\u65E0"}\uFF1B\u6700\u65B0\u6709\u6548\u4EA7\u51FA\uFF1A${node?.latestSuccessWorkId ?? "\u65E0"}\uFF09\u2014\u2014\u6536\u53E3\u53EA\u8BA4\u6700\u65B0\u4E00\u7248\u6216\u6700\u65B0\u6709\u6548\u4EA7\u51FA`,
7422
+ overridable: false
7423
+ };
7424
+ }
7425
+ const delivered = work?.businessHandoffStatus === "delivered" || work?.status === "success" && work.agentHandoffAttemptId === null && work.businessHandoffStatus === null;
7426
+ if (delivered) return null;
7427
+ return {
7428
+ code: "handoff-undeclared",
7429
+ detail: `\u4E1A\u52A1\u4EA4\u63A5\u672A\u5B8C\u6210\uFF1Awork.status=${work?.status ?? "?"}\u3001handoff=${work?.businessHandoffStatus ?? "\u672A\u58F0\u660E"}${work?.agentHandoffAttemptId ? `\uFF08attempt ${work.agentHandoffAttemptId}\uFF09` : ""}\u2014\u2014\u7B49\u6267\u884C\u4EBA\u8865\u4EA4\u63A5\uFF0C\u6216\u7531\u4EBA force-conclude \u7B7E\u5B57\u7A7F\u900F`,
7430
+ overridable: true
7431
+ };
7274
7432
  }
7275
7433
  function requiresBusinessHandoff(state, nodeId, work) {
7434
+ if (work?.replyToIssueId) return false;
7276
7435
  if (!state.edgesFrom(nodeId).some((edge) => isBusinessDependencyEdge(state, edge))) return false;
7277
7436
  if (typeof work?.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) return false;
7278
7437
  return true;
@@ -7352,6 +7511,7 @@ CREATE TABLE IF NOT EXISTS ${schema}.workflow_nodes (
7352
7511
  assignee_actor_id text,
7353
7512
  assignee_role text,
7354
7513
  latest_work_id text,
7514
+ latest_success_work_id text,
7355
7515
  latest_accept_id text,
7356
7516
  is_final_output boolean NOT NULL DEFAULT false,
7357
7517
  fields jsonb,
@@ -7465,6 +7625,9 @@ CREATE TABLE IF NOT EXISTS ${schema}.issues (
7465
7625
  author_actor_id text NOT NULL,
7466
7626
  hand_actor_id text,
7467
7627
  raised_by_node_id text,
7628
+ -- \u2605 escalation \u66FF\u54EA\u6761 gap \u53D1\u8D77\uFF08\u88AB\u5F15\u7528 gap \u7684 issue id\uFF09\uFF1B\u4F9B issue.resolve \u7CBE\u786E\u8FDE\u5E26\u5173\u95ED
7629
+ -- \uFF08ADR-0030 D1\uFF1Bwo-05dfbd6e \u8865\u9F50\u6865\u63A5\u8FC1\u79FB\u9057\u6F0F\uFF09\u3002\u975E escalation / \u672A\u5E26 --gap \u65F6\u4E3A NULL\u3002
7630
+ gap_id text,
7468
7631
  -- \u2605 ADR 0144\uFF1Ablocking / state \u5217\u5DF2\u5220\u9664\u2014\u2014\u5B58\u6D3B\u5224\u636E\u552F\u4E00\u5316\u4E3A resolved_at \u5224\u7A7A\uFF1B
7469
7632
  -- resolution \u627F\u63A5\u65E7\u4E09\u503C state \u7684 closed\uFF08=wontfix\uFF09\u8BED\u4E49\u3002\u7EA6\u675F\u7531\u8FC1\u79FB\u6BB5\u7EDF\u4E00\u52A0\uFF08\u540D\u5B57\u5316\u3001\u5E42\u7B49\uFF09\u3002
7470
7633
  proposed_resolution text,
@@ -7589,6 +7752,8 @@ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
7589
7752
  -- \u968F\u540E state / blocking \u5217\u5220\u9664\uFF08\u4F9D\u8D56 state \u7684 CHECK \u4E0E\u4E09\u4E2A\u65E7\u90E8\u5206\u7D22\u5F15\u968F\u5217\u81EA\u52A8\u5220\u9664\uFF09\u3002
7590
7753
  -- \u56DE\u586B\u5FC5\u987B\u5728\u5220\u5217\u524D\u3001\u4E14\u53EA\u5728 state \u5217\u8FD8\u5B58\u5728\u65F6\u6267\u884C\uFF08DO \u5757\u5224 information_schema\uFF0C\u91CD\u590D\u542F\u52A8\u5E42\u7B49\uFF09\u3002
7591
7754
  ALTER TABLE ${schema}.issues ADD COLUMN IF NOT EXISTS resolution text;
7755
+ -- escalation\u2192gap \u7CBE\u786E\u5173\u8054\uFF08wo-05dfbd6e\uFF09\uFF1A\u4F9B issue.resolve \u8FDE\u5E26\u5173\u95ED\u5339\u914D escalation\uFF08ADR-0030 D1\uFF09\u3002
7756
+ ALTER TABLE ${schema}.issues ADD COLUMN IF NOT EXISTS gap_id text;
7592
7757
  DO $do$ BEGIN
7593
7758
  IF EXISTS (SELECT 1 FROM information_schema.columns
7594
7759
  WHERE table_schema = '${schema}' AND table_name = 'issues' AND column_name = 'state') THEN
@@ -7608,6 +7773,9 @@ CREATE INDEX IF NOT EXISTS issues_workorder_alive_idx ON ${schema}.issues (worko
7608
7773
  CREATE INDEX IF NOT EXISTS issues_about_alive_idx ON ${schema}.issues (about_node_id) WHERE resolved_at IS NULL;
7609
7774
  CREATE INDEX IF NOT EXISTS issues_raised_alive_idx ON ${schema}.issues (raised_by_node_id) WHERE resolved_at IS NULL;
7610
7775
  ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS fields jsonb;
7776
+ -- \u2605 \u6709\u6548\u4EA7\u51FA\u6307\u9488\uFF08ADR 0148\uFF09\uFF1Anode \u4E0A\u300C\u6700\u8FD1\u4E00\u6B21\u5C1D\u8BD5\u300D\uFF08latest_work_id\uFF09\u4E4B\u5916\u518D\u5B58\u300C\u6700\u8FD1\u4E00\u6B21\u6709\u6548\u4EA7\u51FA\u300D\uFF0C
7777
+ -- \u5931\u8D25\u7A7A\u6D3E\u53D1\u4E0D\u518D\u9876\u6389\u6210\u54C1\u3002\u56DE\u586B\u5728\u4E0B\u65B9 works.status \u56DE\u586B\u4E4B\u540E\u6267\u884C\uFF08\u4F9D\u8D56 status \u5DF2\u843D\u5E93\uFF09\u3002
7778
+ ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS latest_success_work_id text;
7611
7779
  -- \u2605 \u5148\u5220\u89C6\u56FE\u518D\u5220\u5217\uFF1Awork_view/node_view/workorder_view \u7528 SELECT *\uFF0C\u5BF9\u57FA\u8868\u5217\u6709\u4F9D\u8D56\u2014\u2014\u5B58\u91CF\u5E93\u4E0A
7612
7780
  -- DROP COLUMN \u4F1A\u62A5 "cannot drop column ... because other objects depend on it"\uFF0C\u4E14\u672C\u6BB5\u662F\u5355\u6761
7613
7781
  -- \u591A\u8BED\u53E5 Query\uFF08\u4E00\u4E2A\u9690\u5F0F\u4E8B\u52A1\uFF09\uFF0C\u4E00\u5904\u5931\u8D25\u5168\u6BB5\u56DE\u6EDA\u3001migrate \u629B\u9519 \u2192 serve \u8D77\u4E0D\u6765\u3002\u89C6\u56FE\u968F\u540E\u7531
@@ -7663,6 +7831,16 @@ UPDATE ${schema}.reviews SET status = CASE
7663
7831
  WHEN ended_at IS NOT NULL THEN 'failed'
7664
7832
  ELSE 'running' END
7665
7833
  WHERE status IS NULL;
7834
+ -- \u2605 \u6709\u6548\u4EA7\u51FA\u6307\u9488\u56DE\u586B\uFF08ADR 0148\uFF09\uFF1Alatest_success_work_id = \u8BE5\u8282\u70B9\u6700\u8FD1\u4E00\u4E2A status=success \u7684**\u4E3B\u94FE** work
7835
+ -- \uFF08\u6392\u9664\u56DE\u4FE1 work reply_to_issue_id\uFF09\u3002\u5FC5\u987B\u5728\u4E0A\u9762 works.status \u56DE\u586B\u4E4B\u540E\uFF08\u4F9D\u8D56 status \u5DF2\u843D\u5E93\uFF09\u3002
7836
+ -- \u53EA\u586B\u5F53\u524D\u4E3A NULL \u7684\u884C \u2192 \u5E42\u7B49\uFF1A\u91CD\u542F\u65F6\u4E0D\u8986\u76D6 handler \u5DF2\u7EF4\u62A4\u7684\u503C\uFF1B\u4ECE\u672A\u6210\u529F\u8FC7\u7684\u8282\u70B9\u4FDD\u6301 NULL\u3002
7837
+ UPDATE ${schema}.workflow_nodes n SET latest_success_work_id = (
7838
+ SELECT w.id FROM ${schema}.works w
7839
+ WHERE w.node_id = n.id AND w.status = 'success' AND w.reply_to_issue_id IS NULL
7840
+ ORDER BY w.created_at DESC, w.id DESC
7841
+ LIMIT 1
7842
+ )
7843
+ WHERE n.latest_success_work_id IS NULL;
7666
7844
  `;
7667
7845
  }
7668
7846
  function ENGINE_VIEWS(schema) {
@@ -7678,20 +7856,25 @@ SELECT r.*, r.status AS state
7678
7856
  FROM ${schema}.reviews r;
7679
7857
 
7680
7858
  DROP VIEW IF EXISTS ${schema}.node_view;
7859
+ -- \u2605 ADR 0148\uFF1A\u4E3B\u6001\u6309 latest_success\uFF08\u6700\u8FD1\u4E00\u4E2A\u6709\u6548\u4EA7\u51FA\uFF09\u8D70\u2014\u2014\u300C\u5DF2\u4EA4\u4ED8 v3\u3001\u672B\u6B21\u5C1D\u8BD5 v4 \u5D29\u4E86\u300D\u663E\u793A\u5DF2\u5B8C\u6210\uFF0C
7860
+ -- \u672B\u6B21\u5931\u8D25\u53EA\u4F5C\u526F\u6807\u8BB0\uFF08\u7248\u672C\u5217\u8868\u91CC\u90A3\u6761 failed \u4ECD\u5728\uFF09\uFF0C\u4E0D\u518D\u51FA\u73B0\u300C\u56FE\u4E0A\u7EA2\u3001\u4E0B\u6E38\u5728\u8DD1\u300D\u7684\u5272\u88C2\u3002
7861
+ -- \u4E0E views.ts nodeState \u540C\u6E90\uFF1A\u5728\u8DD1\uFF08latest_work running\uFF09\u4F18\u5148\uFF1B\u5426\u5219\u6709 latest_success \u5C31\u6309\u5176\u9A8C\u6536\u4E0E\u5426\u5206
7862
+ -- \u5DF2\u5B8C\u6210/\u8BC4\u5BA1\u4E2D\uFF1B\u518D\u5426\u5219\u672B\u6B21\u5C1D\u8BD5\u5931\u8D25/dead \u2192 \u5361\u4F4F\u3002
7681
7863
  CREATE VIEW ${schema}.node_view AS
7682
7864
  SELECT n.*,
7683
7865
  CASE
7684
7866
  WHEN n.cancelled_at IS NOT NULL THEN 'dead' -- \u5DF2\u4F5C\u5E9F\uFF08\u79FB\u51FA\u8BA1\u5212\uFF09
7685
7867
  WHEN lw.id IS NULL THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08latest_work \u4E3A\u7A7A\uFF09
7686
7868
  WHEN lw.status = 'retry' THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08\u7528\u6237\u4ECB\u5165\u91CD\u8BD5\uFF0C\u7B49 try_to_run\uFF09
7687
- WHEN lw.status = 'running' THEN 'running' -- \u5728\u8FDB\u884C
7688
- WHEN lw.status = 'success' AND n.latest_accept_id IS DISTINCT FROM lw.id THEN 'reviewing' -- \u8BC4\u5BA1\u4E2D
7689
- WHEN lw.status = 'success' AND n.latest_accept_id = lw.id THEN 'done' -- \u5DF2\u5B8C\u6210
7690
- WHEN lw.status IN ('failed','dead') THEN 'dead' -- \u5361\u4F4F\u4E86
7869
+ WHEN lw.status = 'running' THEN 'running' -- \u5728\u8FDB\u884C\uFF08\u91CD\u8DD1\u8FDB\u884C\u4E2D\u4E5F\u4F18\u5148\u663E\u793A\u5728\u8FDB\u884C\uFF09
7870
+ WHEN ls.id IS NOT NULL AND n.latest_accept_id = ls.id THEN 'done' -- \u5DF2\u5B8C\u6210\uFF08\u6709\u6548\u4EA7\u51FA\u5DF2\u9A8C\u6536\uFF09
7871
+ WHEN ls.id IS NOT NULL AND n.latest_accept_id IS DISTINCT FROM ls.id THEN 'reviewing' -- \u8BC4\u5BA1\u4E2D\uFF08\u6709\u6548\u4EA7\u51FA\u672A\u9A8C\u6536\uFF09
7872
+ WHEN lw.status IN ('failed','dead') THEN 'dead' -- \u4ECE\u672A\u4EA7\u51FA\u4E14\u672B\u6B21\u5C1D\u8BD5\u5931\u8D25 \u2192 \u5361\u4F4F\u4E86
7691
7873
  ELSE 'waiting'
7692
7874
  END AS state
7693
7875
  FROM ${schema}.workflow_nodes n
7694
- LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id;
7876
+ LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id
7877
+ LEFT JOIN ${schema}.works ls ON ls.id = n.latest_success_work_id;
7695
7878
 
7696
7879
  DROP VIEW IF EXISTS ${schema}.workorder_view;
7697
7880
  CREATE VIEW ${schema}.workorder_view AS
@@ -12971,6 +13154,7 @@ var init_store_postgres = __esm({
12971
13154
  assigneeActorId: n.assignee_actor_id,
12972
13155
  assigneeRole: n.assignee_role,
12973
13156
  latestWorkId: n.latest_work_id,
13157
+ latestSuccessWorkId: n.latest_success_work_id ?? null,
12974
13158
  latestAcceptId: n.latest_accept_id,
12975
13159
  isFinalOutput: n.is_final_output,
12976
13160
  cancelledAt: iso(n.cancelled_at),
@@ -13054,6 +13238,7 @@ var init_store_postgres = __esm({
13054
13238
  authorActorId: i.author_actor_id,
13055
13239
  handActorId: i.hand_actor_id,
13056
13240
  raisedByNodeId: i.raised_by_node_id,
13241
+ gapId: i.gap_id ?? null,
13057
13242
  proposedResolution: i.proposed_resolution,
13058
13243
  proposedBy: i.proposed_by,
13059
13244
  proposedAt: iso(i.proposed_at),
@@ -13146,9 +13331,9 @@ var init_store_postgres = __esm({
13146
13331
  await this.c.query(
13147
13332
  `INSERT INTO ${this.s}.workflow_nodes
13148
13333
  (id,workorder_id,type,title,spec,version,assignee_actor_id,assignee_role,
13149
- latest_work_id,latest_accept_id,is_final_output,
13334
+ latest_work_id,latest_success_work_id,latest_accept_id,is_final_output,
13150
13335
  fields,cancelled_at,row_version,created_at,updated_at)
13151
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
13336
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
13152
13337
  ON CONFLICT (id) DO NOTHING`,
13153
13338
  [
13154
13339
  n.id,
@@ -13160,6 +13345,7 @@ var init_store_postgres = __esm({
13160
13345
  n.assigneeActorId,
13161
13346
  n.assigneeRole,
13162
13347
  n.latestWorkId,
13348
+ n.latestSuccessWorkId,
13163
13349
  n.latestAcceptId,
13164
13350
  n.isFinalOutput,
13165
13351
  n.fields ? JSON.stringify(n.fields) : null,
@@ -13288,9 +13474,9 @@ var init_store_postgres = __esm({
13288
13474
  await this.c.query(
13289
13475
  `INSERT INTO ${this.s}.issues
13290
13476
  (id,workorder_id,kind,body,about_node_id,about_work_id,author_actor_id,hand_actor_id,
13291
- raised_by_node_id,proposed_resolution,proposed_by,proposed_at,
13477
+ raised_by_node_id,gap_id,proposed_resolution,proposed_by,proposed_at,
13292
13478
  resolved_at,resolution,resolved_by,resolved_note,resolved_via_work_id,created_at)
13293
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
13479
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
13294
13480
  ON CONFLICT (id) DO NOTHING`,
13295
13481
  [
13296
13482
  i.id,
@@ -13302,6 +13488,7 @@ var init_store_postgres = __esm({
13302
13488
  i.authorActorId,
13303
13489
  i.handActorId,
13304
13490
  i.raisedByNodeId,
13491
+ i.gapId,
13305
13492
  i.proposedResolution,
13306
13493
  i.proposedBy,
13307
13494
  i.proposedAt,
@@ -13427,11 +13614,14 @@ var init_store_postgres = __esm({
13427
13614
  * `NOT EXISTS` 那一段是"我前面还有没跑完的兄弟就别动我";`SKIP LOCKED` 是
13428
13615
  * "别的 worker 正在处理这张工单就跳过整张"。两者合起来 = 按工单分区的串行队列。
13429
13616
  */
13430
- async claimNextEvent(now) {
13617
+ async claimNextEvent(now, recoverAppliedBefore) {
13431
13618
  const res = await this.c.query(
13432
13619
  `SELECT e.* FROM ${this.s}.events e
13433
- WHERE e.status = 'pending'
13434
- AND (e.available_at IS NULL OR e.available_at <= $1)
13620
+ WHERE (
13621
+ (e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1))
13622
+ -- \u5D29\u6E83\u81EA\u6108\uFF1A\u5361\u5728 applied \u592A\u4E45\u7684\uFF08\u8FDB\u7A0B\u6B7B\u5728\u300C\u843D\u72B6\u6001\u300D\u4E0E\u300C\u6536\u5C3E\u300D\u4E4B\u95F4\uFF09\u4E5F\u56DE\u6536\u91CD\u8DD1
13623
+ OR ($2::timestamptz IS NOT NULL AND e.status = 'applied' AND e.applied_at < $2::timestamptz)
13624
+ )
13435
13625
  AND NOT EXISTS (
13436
13626
  SELECT 1 FROM ${this.s}.events p
13437
13627
  WHERE p.workorder_id = e.workorder_id
@@ -13440,7 +13630,7 @@ var init_store_postgres = __esm({
13440
13630
  ORDER BY e.seq
13441
13631
  FOR UPDATE OF e SKIP LOCKED
13442
13632
  LIMIT 1`,
13443
- [now]
13633
+ [now, recoverAppliedBefore ?? null]
13444
13634
  );
13445
13635
  return res.rows[0] ? toRecord(res.rows[0]) : null;
13446
13636
  }
@@ -13481,10 +13671,14 @@ var init_store_postgres = __esm({
13481
13671
  );
13482
13672
  return (res.rowCount ?? 0) > 0;
13483
13673
  }
13484
- async claimAllPending(now) {
13674
+ async claimAllPending(now, recoverAppliedBefore) {
13485
13675
  const res = await this.c.query(
13486
13676
  `SELECT e.* FROM ${this.s}.events e
13487
- WHERE e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1)
13677
+ WHERE (
13678
+ (e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1))
13679
+ -- \u5D29\u6E83\u81EA\u6108\uFF1A\u89C1 claimNextEvent \u7684\u6CE8\u91CA
13680
+ OR ($2::timestamptz IS NOT NULL AND e.status = 'applied' AND e.applied_at < $2::timestamptz)
13681
+ )
13488
13682
  AND NOT EXISTS (SELECT 1 FROM ${this.s}.events p WHERE p.workorder_id = e.workorder_id
13489
13683
  AND p.status IN ('pending','applied') AND p.seq < e.seq)
13490
13684
  -- \u2605 scan \u4E8B\u4EF6\u6392\u524D\u9762\uFF1Aorigin='apply' \u4E14\u65E0\u4E0A\u6E38\u4E8B\u4EF6\uFF08\u4E0D\u662F\u88AB\u5176\u4ED6\u4E8B\u4EF6\u89E6\u53D1\u7684\uFF09
@@ -13494,7 +13688,7 @@ var init_store_postgres = __esm({
13494
13688
  CASE WHEN e.origin = 'apply' AND e.caused_by_seq IS NULL THEN 0 ELSE 1 END,
13495
13689
  e.seq
13496
13690
  FOR UPDATE OF e SKIP LOCKED`,
13497
- [now]
13691
+ [now, recoverAppliedBefore ?? null]
13498
13692
  );
13499
13693
  return res.rows.map(toRecord);
13500
13694
  }
@@ -13513,7 +13707,25 @@ var init_store_postgres = __esm({
13513
13707
  async lockWorkorder(workorderId) {
13514
13708
  await this.c.query(`SELECT 1 FROM ${this.s}.workorders WHERE id = $1 FOR UPDATE`, [workorderId]);
13515
13709
  }
13516
- async listEvents(workorderId, fromSeq = 0) {
13710
+ async listEvents(workorderId, fromSeq = 0, limit, kinds) {
13711
+ if (kinds && kinds.length > 0) {
13712
+ const res2 = workorderId ? await this.c.query(
13713
+ `SELECT * FROM ${this.s}.events WHERE workorder_id = $1 AND seq > $2 AND kind = ANY($3) ORDER BY seq${limit !== void 0 ? ` DESC LIMIT ${Number(limit)}` : ""}`,
13714
+ [workorderId, fromSeq, [...kinds]]
13715
+ ) : await this.c.query(
13716
+ `SELECT * FROM ${this.s}.events WHERE seq > $1 AND kind = ANY($2) ORDER BY seq${limit !== void 0 ? ` DESC LIMIT ${Number(limit)}` : ""}`,
13717
+ [fromSeq, [...kinds]]
13718
+ );
13719
+ const rows = res2.rows.map(toRecord);
13720
+ return limit !== void 0 ? rows.reverse() : rows;
13721
+ }
13722
+ if (limit !== void 0) {
13723
+ const res2 = workorderId ? await this.c.query(
13724
+ `SELECT * FROM ${this.s}.events WHERE workorder_id = $1 AND seq > $2 ORDER BY seq DESC LIMIT $3`,
13725
+ [workorderId, fromSeq, limit]
13726
+ ) : await this.c.query(`SELECT * FROM ${this.s}.events WHERE seq > $1 ORDER BY seq DESC LIMIT $2`, [fromSeq, limit]);
13727
+ return res2.rows.map(toRecord).reverse();
13728
+ }
13517
13729
  const res = workorderId ? await this.c.query(
13518
13730
  `SELECT * FROM ${this.s}.events WHERE workorder_id = $1 AND seq > $2 ORDER BY seq`,
13519
13731
  [workorderId, fromSeq]
@@ -13633,6 +13845,7 @@ function newNode(id, workorderId, at, version2) {
13633
13845
  assigneeActorId: null,
13634
13846
  assigneeRole: null,
13635
13847
  latestWorkId: null,
13848
+ latestSuccessWorkId: null,
13636
13849
  latestAcceptId: null,
13637
13850
  isFinalOutput: false,
13638
13851
  cancelledAt: null,
@@ -13961,6 +14174,10 @@ var init_workorder = __esm({
13961
14174
  });
13962
14175
 
13963
14176
  // ../engine/src/handlers/work.ts
14177
+ function latestSuccessOf(state, nodeId) {
14178
+ const successes = state.worksOf(nodeId).filter((w2) => !w2.replyToIssueId && w2.status === "success").sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14179
+ return successes.length > 0 ? successes[successes.length - 1].id : null;
14180
+ }
13964
14181
  var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workTimeout;
13965
14182
  var init_work = __esm({
13966
14183
  "../engine/src/handlers/work.ts"() {
@@ -14064,7 +14281,7 @@ var init_work = __esm({
14064
14281
  // 回信 work 不杀前会话(apply 不 kill 主链,节点已完结、无在跑会话),只派发。
14065
14282
  async effect(e, ctx) {
14066
14283
  if (!e.replyToIssueId) await ctx.cancelPreviousWork(e.nodeId, e.workId);
14067
- await ctx.dispatchWork(e.workId);
14284
+ await ctx.dispatchWork(e.workId, e.resumeEligible !== void 0 ? { resumeEligible: e.resumeEligible } : void 0);
14068
14285
  }
14069
14286
  };
14070
14287
  workKill = {
@@ -14090,6 +14307,12 @@ var init_work = __esm({
14090
14307
  ...w2.status !== "dead" ? { status: "dead" } : {}
14091
14308
  });
14092
14309
  }
14310
+ if (node.latestSuccessWorkId) {
14311
+ const ls = state.work(node.latestSuccessWorkId);
14312
+ if (!ls || ls.status !== "success") {
14313
+ state.updateNode(e.nodeId, { latestSuccessWorkId: latestSuccessOf(state, e.nodeId) });
14314
+ }
14315
+ }
14093
14316
  killNodeReviews(state, e.nodeId, ctx.at);
14094
14317
  },
14095
14318
  async effect(e, ctx) {
@@ -14123,6 +14346,9 @@ var init_work = __esm({
14123
14346
  outcomeDetail: businessFailed ? { ...e.detail ?? {}, reason: "agent business handoff not delivered" } : e.detail,
14124
14347
  status
14125
14348
  });
14349
+ if (status === "success") {
14350
+ state.updateNode(w2.nodeId, { latestSuccessWorkId: e.workId });
14351
+ }
14126
14352
  }
14127
14353
  };
14128
14354
  workHandoffRecorded = {
@@ -14132,17 +14358,20 @@ var init_work = __esm({
14132
14358
  const w2 = state.work(e.workId);
14133
14359
  if (!w2 || e.producedBy !== "agent") return;
14134
14360
  if (w2.agentHandoffAttemptId && w2.agentHandoffAttemptId !== e.attemptId) return;
14361
+ const flipsToFailed = e.status === "not_delivered" && w2.endedAt && !w2.deadAt;
14135
14362
  state.updateWork(e.workId, {
14136
14363
  agentHandoffAttemptId: e.attemptId,
14137
14364
  businessHandoffStatus: e.status,
14138
14365
  businessHandoffRecordedAt: ctx.at,
14139
- // handoff 可以在 work.response 后到达。明确未交付必须回到现有失败/重试路径。
14140
- ...e.status === "not_delivered" && w2.endedAt && !w2.deadAt ? {
14366
+ ...flipsToFailed ? {
14141
14367
  status: "failed",
14142
14368
  outcome: "failed",
14143
14369
  outcomeDetail: { ...w2.outcomeDetail ?? {}, reason: "agent business handoff not delivered" }
14144
14370
  } : {}
14145
14371
  });
14372
+ if (flipsToFailed && state.node(w2.nodeId)?.latestSuccessWorkId === e.workId) {
14373
+ state.updateNode(w2.nodeId, { latestSuccessWorkId: latestSuccessOf(state, w2.nodeId) });
14374
+ }
14146
14375
  }
14147
14376
  };
14148
14377
  workStart = {
@@ -14226,6 +14455,7 @@ var init_issue = __esm({
14226
14455
  authorActorId: e.authorActorId,
14227
14456
  handActorId: e.handActorId,
14228
14457
  raisedByNodeId: e.raisedByNodeId,
14458
+ gapId: e.gapId ?? null,
14229
14459
  proposedResolution: null,
14230
14460
  proposedBy: null,
14231
14461
  proposedAt: null,
@@ -14291,6 +14521,22 @@ var init_issue = __esm({
14291
14521
  resolvedNote: e.note,
14292
14522
  resolvedViaWorkId: e.viaWorkId
14293
14523
  });
14524
+ if (issue2.kind === "gap") {
14525
+ for (const other of state.allIssues()) {
14526
+ if (other.kind === "escalation" && other.resolvedAt === null && other.workorderId === issue2.workorderId && other.gapId === issue2.id) {
14527
+ state.updateIssue(other.id, {
14528
+ proposedResolution: null,
14529
+ proposedBy: null,
14530
+ proposedAt: null,
14531
+ resolvedAt: ctx.at,
14532
+ resolution: "resolved",
14533
+ resolvedBy: "actor:system:engine",
14534
+ resolvedNote: e.note,
14535
+ resolvedViaWorkId: null
14536
+ });
14537
+ }
14538
+ }
14539
+ }
14294
14540
  if (issue2.kind === "gap" || issue2.kind === "escalation") {
14295
14541
  const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
14296
14542
  if (nodeId) markNodeForRetry(state, nodeId, ctx.at);
@@ -14461,11 +14707,9 @@ var init_review = __esm({
14461
14707
  name: "work/accept",
14462
14708
  kind: "work.accept",
14463
14709
  apply(e, state, ctx) {
14710
+ const block = acceptBlock(state, e.workId);
14711
+ if (block && !(block.overridable && e.override)) return;
14464
14712
  const w2 = state.work(e.workId);
14465
- if (!w2 || w2.deadAt || w2.acceptanceState === "accepted") return;
14466
- const node = state.node(w2.nodeId);
14467
- if (!node) return;
14468
- if (!businessHandoffBarrierSatisfied(state, w2.nodeId, w2.id)) return;
14469
14713
  if (w2.replyToIssueId) {
14470
14714
  state.updateWork(e.workId, {
14471
14715
  acceptanceState: "accepted",
@@ -14673,7 +14917,7 @@ function emptySnapshotPlaceholder(record8) {
14673
14917
  reviews: []
14674
14918
  };
14675
14919
  }
14676
- var SYSTEM_ACTOR2, NOOP_IO, SYSTEM_CLOCK, HandlerRegistry, EngineBus, ApplyFailure, sleep;
14920
+ var SYSTEM_ACTOR2, NOOP_IO, SYSTEM_CLOCK, HandlerRegistry, DEFAULT_STUCK_APPLIED_RECOVERY_MS, EngineBus, ApplyFailure, sleep;
14677
14921
  var init_bus = __esm({
14678
14922
  "../engine/src/bus.ts"() {
14679
14923
  "use strict";
@@ -14711,6 +14955,7 @@ var init_bus = __esm({
14711
14955
  return [...this.byKind.values()].flat();
14712
14956
  }
14713
14957
  };
14958
+ DEFAULT_STUCK_APPLIED_RECOVERY_MS = 5 * 6e4;
14714
14959
  EngineBus = class {
14715
14960
  store;
14716
14961
  io;
@@ -14719,6 +14964,7 @@ var init_bus = __esm({
14719
14964
  policy;
14720
14965
  registry;
14721
14966
  maxEffectAttempts;
14967
+ stuckAppliedRecoveryMs;
14722
14968
  running = false;
14723
14969
  /** 事件监听器(内存 pubsub):每批事件处理完调用,供 /api/events SSE 推送(替代旧 oplog.subscribe)。 */
14724
14970
  eventListener = null;
@@ -14730,10 +14976,19 @@ var init_bus = __esm({
14730
14976
  this.policy = opts.policy ?? CODE_DEFAULT_POLICY;
14731
14977
  this.registry = new HandlerRegistry(opts.handlers);
14732
14978
  this.maxEffectAttempts = opts.maxEffectAttempts ?? 3;
14979
+ this.stuckAppliedRecoveryMs = opts.stuckAppliedRecoveryMs ?? DEFAULT_STUCK_APPLIED_RECOVERY_MS;
14733
14980
  }
14734
14981
  setIO(io) {
14735
14982
  this.io = io;
14736
14983
  }
14984
+ /**
14985
+ * 崩溃自愈水位线:早于此刻仍停在 `applied` 的事件,认为是「进程死在落状态与收尾之间」的残留,
14986
+ * 回收重跑。`stuckAppliedRecoveryMs <= 0` 时返回 undefined(= 关闭自愈,维持旧行为)。
14987
+ */
14988
+ recoverAppliedBefore() {
14989
+ if (this.stuckAppliedRecoveryMs <= 0) return void 0;
14990
+ return new Date(Date.parse(this.clock.now()) - this.stuckAppliedRecoveryMs).toISOString();
14991
+ }
14737
14992
  /** 注册事件监听器——新事件处理完即回调(serve 桥接 SSE 实时推送,替代 oplog.subscribe)。返回退订。 */
14738
14993
  onEvent(fn) {
14739
14994
  this.eventListener = fn;
@@ -14742,8 +14997,10 @@ var init_bus = __esm({
14742
14997
  };
14743
14998
  }
14744
14999
  /** 读最近事件(/api/events 首次连接回放 / 调试视图),替代 oplog.read。 */
15000
+ /** 最近 N 条事件(时间线/详情页用)。★ 2026-08-16:limit 以前**声明了但没传下去**——
15001
+ * 百万级事件的病态工单一次性全量拉回内存,直接把 serve 打崩(ws:wo-18c2bc4b 实证)。 */
14745
15002
  recentEvents(workorderId, fromSeq, limit = 200) {
14746
- return this.store.transaction((tx) => tx.listEvents(workorderId, fromSeq));
15003
+ return this.store.transaction((tx) => tx.listEvents(workorderId, fromSeq, limit));
14747
15004
  }
14748
15005
  /** 取存储引用——供 serve 层创建 IO 适配器时使用。 */
14749
15006
  getStore() {
@@ -14820,7 +15077,7 @@ var init_bus = __esm({
14820
15077
  let lastFullScan = 0;
14821
15078
  while (true) {
14822
15079
  try {
14823
- const batch = await this.store.transaction((tx) => tx.claimAllPending(this.clock.now()));
15080
+ const batch = await this.store.transaction((tx) => tx.claimAllPending(this.clock.now(), this.recoverAppliedBefore()));
14824
15081
  const touched = /* @__PURE__ */ new Set();
14825
15082
  if (batch.length > 0) {
14826
15083
  for (const record8 of batch) {
@@ -15021,7 +15278,7 @@ var init_bus = __esm({
15021
15278
  eventSeq: record8.seq,
15022
15279
  actorId: record8.actorId,
15023
15280
  emit: (event, opts) => void buffered.push({ event, ...opts }),
15024
- dispatchWork: (id) => this.io.dispatchWork(id),
15281
+ dispatchWork: (id, opts) => this.io.dispatchWork(id, opts),
15025
15282
  dispatchReview: (id, nodeId, reviewerActorId) => this.io.dispatchReview(id, nodeId, reviewerActorId),
15026
15283
  cancelSession: (ref2) => this.io.cancelSession(ref2),
15027
15284
  // ★ work.create 的「kill 旧会话」:apply 已把该节点上一个 latest_work 置 deadAt(会话可能还挂着),
@@ -15053,6 +15310,10 @@ var init_bus = __esm({
15053
15310
  };
15054
15311
  let ok = true;
15055
15312
  for (const h of handlers) {
15313
+ const effectKey = `${h.name}#effect`;
15314
+ if (record8.appliedAt && await this.store.transaction((tx) => tx.isHandlerDone(record8.seq, effectKey))) {
15315
+ continue;
15316
+ }
15056
15317
  let lastErr = "";
15057
15318
  for (let attempt = 1; attempt <= this.maxEffectAttempts; attempt++) {
15058
15319
  try {
@@ -15065,6 +15326,8 @@ var init_bus = __esm({
15065
15326
  }
15066
15327
  if (lastErr) {
15067
15328
  ok = false;
15329
+ } else {
15330
+ await this.store.transaction((tx) => tx.markHandlerDone(record8.seq, effectKey));
15068
15331
  }
15069
15332
  }
15070
15333
  await this.store.transaction(async (tx) => {
@@ -15123,7 +15386,7 @@ var init_bus = __esm({
15123
15386
  /* ═══════════════════════ 内部:旧 applyPhase(claim + apply 一体) ═══════════════════════ */
15124
15387
  async applyPhase() {
15125
15388
  return this.store.transaction(async (tx) => {
15126
- const record8 = await tx.claimNextEvent(this.clock.now());
15389
+ const record8 = await tx.claimNextEvent(this.clock.now(), this.recoverAppliedBefore());
15127
15390
  if (!record8) return null;
15128
15391
  const snapshot = await tx.loadWorkorder(record8.workorderId);
15129
15392
  if (snapshot?.workorder.doneAt) {
@@ -15248,6 +15511,10 @@ function mapWorkStateToRevisionState(w2) {
15248
15511
  if (w2.acceptanceState === "rejected") return "rejected";
15249
15512
  return "proposed";
15250
15513
  // success but not yet judged
15514
+ // ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
15515
+ // superseded,改枚举会牵动契约与前端,见红线 4)——仍投影成 superseded 保持生命周期语义,但**内容
15516
+ // 如实为 empty**(workToRevision 对无产出 work 给 contentKind="empty")。读面(CLI 版本列表)据此把
15517
+ // 「superseded + empty」如实标成「未产出」,不再让「这次失败了」被读成「被更好的版本取代」。
15251
15518
  case "failed":
15252
15519
  return "superseded";
15253
15520
  case "retry":
@@ -15491,6 +15758,18 @@ function bridgeLoadReadModel(input) {
15491
15758
  name: a.name
15492
15759
  })));
15493
15760
  }
15761
+ const acceptBlockers = /* @__PURE__ */ new Map();
15762
+ for (const snap of snapshots2) {
15763
+ let state = null;
15764
+ for (const node of snap.nodes) {
15765
+ if (!node.latestWorkId || node.latestAcceptId === node.latestWorkId) continue;
15766
+ const lw = snap.works.find((w2) => w2.id === node.latestWorkId);
15767
+ if (!lw || !lw.endedAt || lw.deadAt) continue;
15768
+ state ??= new WorkorderState(snap);
15769
+ const block = acceptBlock(state, lw.id);
15770
+ if (block) acceptBlockers.set(node.id, block);
15771
+ }
15772
+ }
15494
15773
  const artifactFiles = /* @__PURE__ */ new Map();
15495
15774
  for (const a of allArtifacts) {
15496
15775
  const list = artifactFiles.get(a.workId) ?? [];
@@ -15505,8 +15784,16 @@ function bridgeLoadReadModel(input) {
15505
15784
  const lw = node.latestWorkId ? workById.get(node.latestWorkId) : void 0;
15506
15785
  if (lw) {
15507
15786
  const st = lw.status ?? (lw.retryAt ? "retry" : lw.deadAt ? "dead" : lw.endedAt ? lw.outcome === "failed" ? "failed" : "success" : "running");
15508
- art.latestWorkState = st === "retry" ? null : st;
15509
- art.latestAccepted = node.latestAcceptId === node.latestWorkId;
15787
+ let uiSt;
15788
+ if (st === "retry") uiSt = null;
15789
+ else if (st === "running") uiSt = "running";
15790
+ else if (node.latestSuccessWorkId) uiSt = "success";
15791
+ else uiSt = st;
15792
+ art.latestWorkState = uiSt;
15793
+ art.latestAccepted = node.latestSuccessWorkId !== null && node.latestAcceptId === node.latestSuccessWorkId;
15794
+ if ((st === "failed" || st === "dead") && node.latestSuccessWorkId) {
15795
+ art.currentRev = node.latestSuccessWorkId;
15796
+ }
15510
15797
  } else {
15511
15798
  art.latestWorkState = null;
15512
15799
  art.latestAccepted = false;
@@ -15685,7 +15972,8 @@ function bridgeLoadReadModel(input) {
15685
15972
  pinMeta,
15686
15973
  gaps,
15687
15974
  escalations,
15688
- pendingReviews
15975
+ pendingReviews,
15976
+ acceptBlockers
15689
15977
  };
15690
15978
  }
15691
15979
  var init_bridge_readmodel = __esm({
@@ -15695,6 +15983,26 @@ var init_bridge_readmodel = __esm({
15695
15983
  }
15696
15984
  });
15697
15985
 
15986
+ // ../core/src/propose-guard.ts
15987
+ function proposeBlock(head, headArtifactCount) {
15988
+ if (!head) return null;
15989
+ const state = workState(head, hasOutput(head, headArtifactCount));
15990
+ if (state === "dead") {
15991
+ const why = head.rejectedReason ? `\uFF1A${head.rejectedReason}` : "";
15992
+ return {
15993
+ code: "head-dead",
15994
+ detail: `\u8FD9\u4E00\u7248\uFF08${head.id}\uFF09\u5DF2\u88AB\u505C\u6389\uFF08${head.deadAt ?? "?"}${why}\uFF09\u2014\u2014\u4F5C\u5E9F\u7248\u4E4B\u4E0A\u4E0D\u80FD\u518D\u4EA4\u7A3F\u3002\u8981\u7EE7\u7EED\uFF0C\u5148\u91CD\u8DD1\u672C\u8282\u70B9\uFF08\u5728\u63A7\u5236\u53F0\u70B9\u300C\u91CD\u65B0\u8FD0\u884C\u300D\uFF0C\u6216\u8BA9\u7BA1\u7406\u8005\u91CD\u6D3E\uFF09\u62FF\u5230\u4E00\u4E2A\u53EF\u5199\u7684\u65B0\u7248\u672C\uFF0C\u518D propose\u3002`
15995
+ };
15996
+ }
15997
+ return null;
15998
+ }
15999
+ var init_propose_guard = __esm({
16000
+ "../core/src/propose-guard.ts"() {
16001
+ "use strict";
16002
+ init_src3();
16003
+ }
16004
+ });
16005
+
15698
16006
  // ../core/src/preview-intervention.ts
15699
16007
  function resolveOwner(type, typeDef, roleIndex, schemaSize) {
15700
16008
  if (!typeDef) {
@@ -16160,6 +16468,7 @@ var init_kernel_bridge = __esm({
16160
16468
  init_closure_gate();
16161
16469
  init_kernel();
16162
16470
  init_bridge_readmodel();
16471
+ init_propose_guard();
16163
16472
  init_src3();
16164
16473
  init_src3();
16165
16474
  init_preview_intervention();
@@ -16490,8 +16799,14 @@ var init_kernel_bridge = __esm({
16490
16799
  }
16491
16800
  }
16492
16801
  if (!workId) {
16493
- const deadRev = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId && r.state === "superseded").sort((a, b2) => b2.createdAt.localeCompare(a.createdAt))[0];
16494
- if (deadRev) return deadRev;
16802
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16803
+ const headId = snap?.nodes.find((n) => n.id === args.artifactId)?.latestWorkId ?? null;
16804
+ if (headId) {
16805
+ const headWork = snap?.works.find((w2) => w2.id === headId);
16806
+ const headOutputs = snap?.artifacts.filter((a) => a.workId === headId).length ?? 0;
16807
+ const block = proposeBlock(headWork, headOutputs);
16808
+ if (block) throw new KernelError(block.detail, block.code);
16809
+ }
16495
16810
  const worksCount = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId).length;
16496
16811
  const revisionId = `wk:${args.artifactId}:${worksCount + 1}`;
16497
16812
  await this.commit({
@@ -16677,8 +16992,8 @@ var init_kernel_bridge = __esm({
16677
16992
  const pendingHead = pendingConcludeAttempt(this.model, args.artifactId)?.head;
16678
16993
  const targetWorkId = args.revisionId ?? (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? pendingHead ?? concludedHead(this.model, args.artifactId);
16679
16994
  if (!targetWorkId) throw new Error("no concluded head to review");
16680
- let reviewId = null;
16681
- let reviewGroup = "default";
16995
+ const verdict = args.verdict === "approve" ? "approve" : "request_changes";
16996
+ let targets = [];
16682
16997
  if (args.reviewId !== void 0) {
16683
16998
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16684
16999
  const row = snap?.reviews.find((r) => r.id === args.reviewId) ?? null;
@@ -16698,32 +17013,67 @@ var init_kernel_bridge = __esm({
16698
17013
  if (row.verdict === null && (row.cancelledAt || row.endedAt)) {
16699
17014
  throw new Error(`review ${args.reviewId} is no longer open\uFF08\u8BE5\u8BC4\u5BA1\u884C\u5DF2\u8D85\u65F6\u5931\u8D25/\u88AB\u53D6\u6D88\uFF0C\u8FDF\u5230\u7968\u88AB\u62D2\uFF09`);
16700
17015
  }
16701
- reviewId = args.reviewId;
16702
- reviewGroup = row.reviewGroup;
16703
- } else if (!args.revisionId) {
17016
+ targets = [{ id: row.id, group: row.reviewGroup }];
17017
+ } else {
16704
17018
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16705
- const open2 = snap?.reviews.find((r) => r.targetWorkId === targetWorkId && r.reviewerActorId === args.actor && isReviewOpen(r));
16706
- reviewId = open2?.id ?? null;
16707
- if (open2) reviewGroup = open2.reviewGroup;
17019
+ const rows = snap?.reviews.filter(
17020
+ (r) => r.targetWorkId === targetWorkId && r.reviewerActorId === args.actor
17021
+ ) ?? [];
17022
+ const open2 = rows.filter((r) => isReviewOpen(r));
17023
+ if (open2.length === 0) {
17024
+ const decided = rows.filter((r) => r.verdict !== null);
17025
+ if (decided.length > 0) {
17026
+ if (decided.every((r) => r.verdict === verdict)) {
17027
+ return { status: verdict === "approve" ? "satisfied" : "failed" };
17028
+ }
17029
+ throw new KernelError(
17030
+ `\u4F60\u5DF2\u5BF9\u8FD9\u4E00\u7248\u6295\u8FC7\u300C${decided[0].verdict}\u300D\uFF08${args.artifactId} @ ${targetWorkId}\uFF09\u2014\u2014\u540C\u4E00\u7248\u4E0D\u63A5\u53D7\u6539\u5224\uFF0C\u8981\u91CD\u6295\u8BF7\u5148\u91CD\u65B0\u53D1\u8D77\u8BC4\u5BA1\uFF08oasis rereview / \u63A7\u5236\u53F0\u300C\u91CD\u65B0\u8BC4\u5BA1\u300D\uFF09\u3002`,
17031
+ "review-already-decided"
17032
+ );
17033
+ }
17034
+ const reqs = snap?.requirements.filter((r) => r.nodeId === args.artifactId) ?? [];
17035
+ const mine = reqs.filter((r) => r.reviewerActorId === args.actor);
17036
+ if (mine.length > 0) {
17037
+ targets = [...new Set(mine.map((r) => r.reviewGroup))].map((group) => ({ id: null, group }));
17038
+ } else {
17039
+ const stale = (snap?.reviews ?? []).filter(
17040
+ (r) => r.nodeId === args.artifactId && r.reviewerActorId === args.actor && r.targetWorkId !== targetWorkId
17041
+ );
17042
+ if (reqs.length === 0) {
17043
+ throw new KernelError(
17044
+ `${args.artifactId} \u6CA1\u6709\u8BC4\u5BA1\u8981\u6C42\u2014\u2014\u8FD9\u4E2A\u8282\u70B9\u7684\u6536\u53E3\u4E0D\u9700\u8981\u6295\u7968\uFF0C\u4F60\u7684\u7968\u6CA1\u6709\u53EF\u843D\u7684\u884C\uFF08\u8981\u8D8A\u8FC7\u6D41\u7A0B\u76F4\u63A5\u6536\u53E3\u7528 force-conclude\uFF09\u3002`,
17045
+ "no-review-requirement"
17046
+ );
17047
+ }
17048
+ throw new KernelError(
17049
+ `\u8FD9\u4E00\u7248\u4E0A\u6CA1\u6709\u6D3E\u7ED9\u4F60\u7684\u8BC4\u5BA1\u884C\uFF08${args.artifactId} @ ${targetWorkId}\uFF09\uFF1A\u672C\u7248\u8BC4\u5BA1\u4EBA = [${[...new Set(reqs.map((r) => r.reviewerActorId))].join("\u3001")}]${stale.length > 0 ? `\uFF1B\u4F60\u5728\u65E7\u7248 ${stale[0].targetWorkId} \u4E0A\u6709\u4E00\u884C\uFF0C\u90A3\u4E00\u7248\u5DF2\u7ECF\u4E0D\u662F\u5F53\u524D\u5F85\u5BA1\u7248\u672C` : ""}\u2014\u2014\u7968\u6CA1\u6709\u843D\u8D26\uFF08\u4EE5\u524D\u8FD9\u91CC\u4F1A\u6084\u6084\u65B0\u5EFA\u4E00\u884C\u628A\u7968\u541E\u6389\uFF09\u3002`,
17050
+ "review-row-not-found"
17051
+ );
17052
+ }
17053
+ } else {
17054
+ targets = open2.map((r) => ({ id: r.id, group: r.reviewGroup }));
17055
+ }
16708
17056
  }
16709
- await this.commit({
16710
- companyId: "",
16711
- workorderId: wid,
16712
- actorId: args.actor,
16713
- handActorId: args.hand ?? null,
16714
- event: {
16715
- kind: "review.response",
17057
+ for (const target of targets) {
17058
+ await this.commit({
17059
+ companyId: "",
16716
17060
  workorderId: wid,
16717
- reviewId,
16718
- // open 行投它(保留 group);无则让 handler 新建一行
16719
- targetWorkId,
16720
- reviewerActorId: args.actor,
16721
- reviewGroup,
16722
- verdict: args.verdict === "approve" ? "approve" : "request_changes",
16723
- note: args.note ?? null,
16724
- handActorId: args.hand ?? null
16725
- }
16726
- });
17061
+ actorId: args.actor,
17062
+ handActorId: args.hand ?? null,
17063
+ event: {
17064
+ kind: "review.response",
17065
+ workorderId: wid,
17066
+ reviewId: target.id,
17067
+ // 有行投行;无行时带真实桶名建本人的行(不再是没人指的 default 幽灵行)
17068
+ targetWorkId,
17069
+ reviewerActorId: args.actor,
17070
+ reviewGroup: target.group,
17071
+ verdict,
17072
+ note: args.note ?? null,
17073
+ handActorId: args.hand ?? null
17074
+ }
17075
+ });
17076
+ }
16727
17077
  const attempt = this.model.concludeAttempts.get(args.artifactId)?.at(-1);
16728
17078
  return attempt ? { status: args.verdict === "approve" ? "satisfied" : "failed" } : { status: "pending" };
16729
17079
  }
@@ -16731,8 +17081,18 @@ var init_kernel_bridge = __esm({
16731
17081
  const wid = this.wo(args.artifactId);
16732
17082
  const artifact = this.model.artifacts.get(args.artifactId);
16733
17083
  const currentRev = artifact?.currentRev ? this.model.revisions.get(artifact.currentRev) : void 0;
16734
- const head = (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? concludedHead(this.model, args.artifactId);
16735
- if (!head) throw new Error("no head to force-conclude");
17084
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
17085
+ const targetNode = snap?.nodes.find((n) => n.id === args.artifactId);
17086
+ const head = (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? concludedHead(this.model, args.artifactId) ?? (targetNode?.latestSuccessWorkId ?? targetNode?.latestWorkId ?? null);
17087
+ if (!head) throw new KernelError(`force-conclude \u65E0\u4ECE\u4E0B\u624B\uFF08${args.artifactId}\uFF09\uFF1A\u672C\u8282\u70B9\u8FD8\u6CA1\u6709\u4EFB\u4F55\u4E00\u7248\u4EA7\u51FA`, "no-head");
17088
+ const block = snap ? acceptBlock(new WorkorderState(snap), head) : null;
17089
+ if (block?.code === "already-accepted") return;
17090
+ if (block && !block.overridable) {
17091
+ throw new KernelError(
17092
+ `force-conclude \u672A\u6267\u884C\uFF08${args.artifactId} @ ${head}\uFF09\uFF1A${block.detail}`,
17093
+ `accept-blocked:${block.code}`
17094
+ );
17095
+ }
16736
17096
  await this.commit({
16737
17097
  companyId: "",
16738
17098
  workorderId: wid,
@@ -16745,6 +17105,14 @@ var init_kernel_bridge = __esm({
16745
17105
  override: { by: args.actor, reason: args.reason ?? args.note ?? "force concluded" }
16746
17106
  }
16747
17107
  });
17108
+ const after = await this.store.transaction((tx) => tx.loadWorkorder(wid));
17109
+ const accepted = after?.works.find((w2) => w2.id === head)?.acceptanceState === "accepted";
17110
+ if (!accepted) {
17111
+ throw new KernelError(
17112
+ `force-conclude \u672A\u751F\u6548\uFF08${args.artifactId} @ ${head}\uFF09\uFF1Awork.accept \u4E8B\u4EF6\u5DF2\u5165 oplog\uFF0C\u4F46 apply \u672A\u6539\u53D8\u9A8C\u6536\u6001${block ? `\u2014\u2014\u4E8B\u524D\u5224\u636E\uFF1A${block.detail}` : "\u2014\u2014\u539F\u56E0\u672A\u77E5\uFF0C\u8BF7\u67E5 work/node \u73B0\u72B6"}`,
17113
+ "accept-no-effect"
17114
+ );
17115
+ }
16748
17116
  }
16749
17117
  async groupCommit(args) {
16750
17118
  await this.merge({ artifactId: args.artifactId, actor: args.actor });
@@ -16953,6 +17321,7 @@ var init_kernel_bridge = __esm({
16953
17321
  issueId,
16954
17322
  issueKind: "escalation",
16955
17323
  body,
17324
+ gapId: args.gapId,
16956
17325
  aboutNodeId: args.artifactId,
16957
17326
  aboutWorkId: null,
16958
17327
  // ★ 修复(实证 ws:wo-a558f8cc):raisedByNodeId 只在 **agent/人自提**(/api/cmd escalate)时填节点——
@@ -17234,7 +17603,10 @@ var init_kernel_bridge = __esm({
17234
17603
  });
17235
17604
  }
17236
17605
  async bumpPin(args) {
17237
- console.warn(`[kernel] bumpPin \u5FFD\u7565\uFF08${args.artifactId} \u2192 ${args.to}@${args.pinned}\uFF09\uFF1A\u65B0\u5F15\u64CE pin \u7531 work.accept \u81EA\u52A8\u63A8\u8FDB`);
17606
+ throw new KernelError(
17607
+ `pin \u5DF2\u5E9F\u5F03\uFF08${args.artifactId} \u2192 ${args.to}@${args.pinned} \u672A\u6267\u884C\uFF09\uFF1A\u65B0\u5F15\u64CE\u7684\u4E0B\u6E38\u7248\u672C\u56FA\u5B9A\u7531 work.accept \u81EA\u52A8\u63A8\u8FDB\u2014\u2014\u8981\u8BA9\u4E0B\u6E38\u6362\u7248\uFF0C\u53BB\u9A8C\u6536\u4E0A\u6E38\u5BF9\u5E94\u7684\u90A3\u4E00\u7248\uFF08conclude / force-conclude\uFF09\u3002`,
17608
+ "deprecated-op"
17609
+ );
17238
17610
  }
17239
17611
  async recordToolCall(_args) {
17240
17612
  }
@@ -17331,6 +17703,7 @@ var init_kernel_bridge = __esm({
17331
17703
  }
17332
17704
  const wid = plan.workspace ?? plan.ops.find((o) => o.action === "spawn" && o.workspace)?.workspace ?? "ws:default";
17333
17705
  let applied = 0;
17706
+ const unsupported = [];
17334
17707
  await this.ensureWorkorder(wid, "", _actor);
17335
17708
  for (const op of plan.ops) {
17336
17709
  switch (op.action) {
@@ -17469,6 +17842,7 @@ var init_kernel_bridge = __esm({
17469
17842
  break;
17470
17843
  }
17471
17844
  default:
17845
+ unsupported.push(`${op.action}\uFF08${op.artifactId ?? "?"}\uFF09`);
17472
17846
  break;
17473
17847
  }
17474
17848
  }
@@ -17492,6 +17866,12 @@ var init_kernel_bridge = __esm({
17492
17866
  this.draftCards.delete(plan.resolvesProposal);
17493
17867
  await this.refreshModel();
17494
17868
  }
17869
+ if (unsupported.length > 0) {
17870
+ throw new KernelError(
17871
+ `\u5DF2\u843D\u5730 ${applied} \u6761 op\uFF1B\u4EE5\u4E0B op \u672C\u5F15\u64CE\u4E0D\u652F\u6301\u3001\u672A\u6267\u884C\uFF1A${unsupported.join("\u3001")}\uFF08pin \u5DF2\u5E9F\u5F03\u2014\u2014\u4E0B\u6E38\u7248\u672C\u7531\u9A8C\u6536\u4E0A\u6E38\u81EA\u52A8\u63A8\u8FDB\uFF09`,
17872
+ "unsupported-op"
17873
+ );
17874
+ }
17495
17875
  return { interventionId: `intervention:${(0, import_node_crypto3.randomUUID)()}`, applied };
17496
17876
  }
17497
17877
  };
@@ -17516,6 +17896,7 @@ var init_src4 = __esm({
17516
17896
  init_workorder_ledger();
17517
17897
  init_closure_gate();
17518
17898
  init_exit_classify();
17899
+ init_produce_resume_fallback();
17519
17900
  init_dispatcher();
17520
17901
  init_session_limits();
17521
17902
  init_kernel_bridge();
@@ -160686,6 +161067,9 @@ ${task}` : task;
160686
161067
  });
160687
161068
 
160688
161069
  // ../adapters/src/_core/workdir-gc.ts
161070
+ function reclaimableBytes(c) {
161071
+ return c.sizeBytes + (c.runtimeStateBytes ?? 0);
161072
+ }
160689
161073
  function idleMs(c, now) {
160690
161074
  const last = c.meta?.lastUsedAt ? Date.parse(c.meta.lastUsedAt) : NaN;
160691
161075
  return now - (Number.isFinite(last) ? last : c.mtimeMs);
@@ -160706,19 +161090,58 @@ function decideOne(c, policy, now) {
160706
161090
  }
160707
161091
  if (c.artifactState === void 0) return mk("keep", "artifact-unknown");
160708
161092
  if (c.artifactState === "active") {
160709
- if (c.meta.lastExit?.reason === "clean") return mk("delete", "settled-clean");
160710
- return c.meta.lastExit && idle > policy.abnormalGraceMs ? mk("delete", "one-shot-ttl") : mk("keep", "artifact-active");
161093
+ return mk("keep", "artifact-active");
160711
161094
  }
160712
- if (c.meta.lastExit?.reason === "clean") return mk("delete", "settled-clean");
160713
161095
  return idle > policy.ttlMs ? mk("delete", "settled-ttl") : mk("keep", "within-ttl");
160714
161096
  }
160715
161097
  function planGc(candidates, policy, now) {
160716
161098
  const decisions = candidates.map((c) => decideOne(c, policy, now));
160717
- const { highWaterBytes, lowWaterBytes } = policy;
160718
- if (highWaterBytes === void 0) return decisions;
160719
161099
  const byDir = new Map(candidates.map((c) => [c.dir, c]));
161100
+ applyRetentionBudget(decisions, byDir, policy, now);
161101
+ applyHighWaterLru(decisions, byDir, policy, now);
161102
+ return decisions;
161103
+ }
161104
+ function applyRetentionBudget(decisions, byDir, policy, now) {
161105
+ const maxRounds = policy.maxRoundsPerArtifact;
161106
+ const capBytes = policy.sessionStoreCapBytes;
161107
+ const evictable = () => decisions.filter((d) => d.action === "keep").map((d) => ({ d, c: byDir.get(d.dir) })).filter(({ c }) => !c.live && (c.artifactState === "active" || c.artifactState === "settled"));
161108
+ if (maxRounds !== void 0 && maxRounds >= 0) {
161109
+ const byArtifact = /* @__PURE__ */ new Map();
161110
+ for (const item of evictable()) {
161111
+ const id = item.c.meta?.artifactId;
161112
+ if (!id) continue;
161113
+ (byArtifact.get(id) ?? byArtifact.set(id, []).get(id)).push(item);
161114
+ }
161115
+ for (const group of byArtifact.values()) {
161116
+ if (group.length <= maxRounds) continue;
161117
+ group.sort((a, b2) => idleMs(a.c, now) - idleMs(b2.c, now));
161118
+ for (const { d } of group.slice(maxRounds)) {
161119
+ d.action = "delete";
161120
+ d.reason = "budget-rounds";
161121
+ }
161122
+ }
161123
+ }
161124
+ if (capBytes !== void 0) {
161125
+ let remaining = decisions.filter((d) => d.action === "keep").reduce((sum, d) => sum + reclaimableBytes(byDir.get(d.dir)), 0);
161126
+ if (remaining > capBytes) {
161127
+ const tier = (c) => c.artifactState === "settled" ? 0 : 1;
161128
+ const order = evictable().sort(
161129
+ (a, b2) => tier(a.c) - tier(b2.c) || idleMs(b2.c, now) - idleMs(a.c, now)
161130
+ );
161131
+ for (const { d, c } of order) {
161132
+ if (remaining <= capBytes) break;
161133
+ d.action = "delete";
161134
+ d.reason = "budget-cap";
161135
+ remaining -= reclaimableBytes(c);
161136
+ }
161137
+ }
161138
+ }
161139
+ }
161140
+ function applyHighWaterLru(decisions, byDir, policy, now) {
161141
+ const { highWaterBytes, lowWaterBytes } = policy;
161142
+ if (highWaterBytes === void 0) return;
160720
161143
  let remaining = decisions.filter((d) => d.action === "keep").reduce((sum, d) => sum + d.sizeBytes, 0);
160721
- if (remaining <= highWaterBytes) return decisions;
161144
+ if (remaining <= highWaterBytes) return;
160722
161145
  const target = lowWaterBytes ?? highWaterBytes;
160723
161146
  const lru = decisions.filter((d) => d.action === "keep").map((d) => ({ d, c: byDir.get(d.dir) })).filter(({ c }) => !c.live && c.artifactState === "settled").sort((a, b2) => idleMs(b2.c, now) - idleMs(a.c, now));
160724
161147
  for (const { d } of lru) {
@@ -160727,17 +161150,23 @@ function planGc(candidates, policy, now) {
160727
161150
  d.reason = "high-water-lru";
160728
161151
  remaining -= d.sizeBytes;
160729
161152
  }
160730
- return decisions;
160731
161153
  }
160732
- function gcAccounting(decisions, policy) {
160733
- const totalBytes = decisions.reduce((s2, d) => s2 + d.sizeBytes, 0);
160734
- const freedBytes = decisions.filter((d) => d.action === "delete").reduce((s2, d) => s2 + d.sizeBytes, 0);
161154
+ function gcAccounting(decisions, policy, candidates) {
161155
+ const byDir = candidates ? new Map(candidates.map((c) => [c.dir, c])) : void 0;
161156
+ const sizeOf = (d) => {
161157
+ const c = byDir?.get(d.dir);
161158
+ return c ? reclaimableBytes(c) : d.sizeBytes;
161159
+ };
161160
+ const totalBytes = decisions.reduce((s2, d) => s2 + sizeOf(d), 0);
161161
+ const freedBytes = decisions.filter((d) => d.action === "delete").reduce((s2, d) => s2 + sizeOf(d), 0);
160735
161162
  const keptBytes = totalBytes - freedBytes;
161163
+ const cap = policy.sessionStoreCapBytes;
160736
161164
  return {
160737
161165
  totalBytes,
160738
161166
  keptBytes,
160739
161167
  freedBytes,
160740
- overHighWater: policy.highWaterBytes !== void 0 && keptBytes > policy.highWaterBytes
161168
+ // 高水位报警:旧 highWaterBytes 或新总量上限任一被突破都报(新上限缺省即开)。
161169
+ overHighWater: policy.highWaterBytes !== void 0 && keptBytes > policy.highWaterBytes || cap !== void 0 && keptBytes > cap
160741
161170
  };
160742
161171
  }
160743
161172
  var DEFAULT_GC_POLICY;
@@ -160748,7 +161177,9 @@ var init_workdir_gc = __esm({
160748
161177
  orphanTtlMs: 24 * 36e5,
160749
161178
  ttlMs: 7 * 24 * 36e5,
160750
161179
  chatTtlMs: 14 * 24 * 36e5,
160751
- abnormalGraceMs: 24 * 36e5
161180
+ abnormalGraceMs: 24 * 36e5,
161181
+ maxRoundsPerArtifact: 3,
161182
+ sessionStoreCapBytes: 2 * 1024 ** 3
160752
161183
  };
160753
161184
  }
160754
161185
  });
@@ -160819,14 +161250,20 @@ function kindFromPath(workRoot, dir) {
160819
161250
  if (parts[0] === "tmp" && parts[1]) return parts[1].replace(/-[^-]*$/, "");
160820
161251
  return void 0;
160821
161252
  }
160822
- function observe(dir) {
161253
+ function observe(dir, workRoot, measureRuntimeState) {
160823
161254
  const meta = readMeta(dir);
160824
161255
  let mtimeMs = 0;
160825
161256
  try {
160826
161257
  mtimeMs = import_node_fs11.default.statSync(dir).mtimeMs;
160827
161258
  } catch {
160828
161259
  }
160829
- return { dir, meta, sizeBytes: dirSizeBytes(dir), mtimeMs, live: isWorkdirLive(dir) };
161260
+ const runtimeKind = meta?.runtimeKind ?? kindFromPath(workRoot, dir);
161261
+ let runtimeStateBytes = 0;
161262
+ try {
161263
+ runtimeStateBytes = measureRuntimeState?.(dir, runtimeKind) ?? 0;
161264
+ } catch {
161265
+ }
161266
+ return { dir, meta, sizeBytes: dirSizeBytes(dir), runtimeStateBytes, mtimeMs, live: isWorkdirLive(dir) };
160830
161267
  }
160831
161268
  async function runGcSweep(deps) {
160832
161269
  const policy = { ...DEFAULT_GC_POLICY, ...deps.policy };
@@ -160834,7 +161271,7 @@ async function runGcSweep(deps) {
160834
161271
  const log3 = deps.log ?? (() => {
160835
161272
  });
160836
161273
  const dirs = listWorkdirs(deps.workRoot, deps.legacyRoot);
160837
- const candidates = dirs.map((d) => observe(d));
161274
+ const candidates = dirs.map((d) => observe(d, deps.workRoot, deps.measureRuntimeState));
160838
161275
  const needStates = candidates.filter(
160839
161276
  (c) => c.meta?.artifactId && c.meta.kind !== "chat" && c.meta.kind !== "one-shot"
160840
161277
  );
@@ -160852,7 +161289,7 @@ async function runGcSweep(deps) {
160852
161289
  if (st) c.artifactState = st;
160853
161290
  }
160854
161291
  const decisions = planGc(candidates, policy, now);
160855
- const acct = gcAccounting(decisions, policy);
161292
+ const acct = gcAccounting(decisions, policy, candidates);
160856
161293
  let deleted = 0;
160857
161294
  for (const d of decisions) {
160858
161295
  if (d.action !== "delete") continue;
@@ -164661,7 +165098,13 @@ async function startOasisServer(opts) {
164661
165098
  try {
164662
165099
  let companyId;
164663
165100
  if (companyCtx) {
164664
- if (companyCtx.kind === "not-found") throw new ApiError(404, "COMPANY_NOT_FOUND", "\u8BF7\u6C42\u6307\u5B9A\u7684\u516C\u53F8\u4E0D\u5B58\u5728");
165101
+ if (companyCtx.kind === "not-found") {
165102
+ throw companyCtx.selector ? new ApiError(404, "COMPANY_NOT_FOUND", `\u8BF7\u6C42\u6307\u5B9A\u7684\u516C\u53F8\u4E0D\u5B58\u5728\uFF1A${companyCtx.selector}`) : new ApiError(
165103
+ 404,
165104
+ "COMPANY_UNRESOLVED",
165105
+ `\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u516C\u53F8\uFF1A${actor} \u672A\u6307\u5B9A\u516C\u53F8\uFF08x-oasis-company\uFF09\uFF0C\u81EA\u8EAB\u4E5F\u4E0D\u5C5E\u4E8E\u4EFB\u4F55\u516C\u53F8\u3002agent \u4F1A\u8BDD\u5C5E\u4E8E\u5176\u88AB\u6D3E\u53D1\u7684\u90A3\u5BB6\u516C\u53F8\uFF0C\u5E94\u7531\u6D3E\u53D1\u65B9\u5728\u4F1A\u8BDD token \u91CC\u7B7E companyId\u2014\u2014\u6536\u5230\u672C\u9519\u8BEF\u901A\u5E38\u662F\u7B7E\u53D1\u4FA7\u6F0F\u7B7E\uFF0C\u800C\u975E\u516C\u53F8\u8BB0\u5F55\u6709\u95EE\u9898\u3002`
165106
+ );
165107
+ }
164665
165108
  if (companyCtx.kind === "forbidden") throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
164666
165109
  if (companyCtx.kind === "ok") companyId = companyCtx.companyId;
164667
165110
  }
@@ -168146,6 +168589,7 @@ var init_service3 = __esm({
168146
168589
  * 队首若 stale(base ≠ 当前 head,文档层乱序 approve 会撞上)→ 内核拒绝并指 rebase/group-commit。
168147
168590
  */
168148
168591
  async mergeUpTo(artifactId, revisionId, actor) {
168592
+ let voted = false;
168149
168593
  for (let guard = 0; guard < 1e3; guard++) {
168150
168594
  const target = this.kernel.model.revisions.get(revisionId);
168151
168595
  if (!target) throw new Error(`revision not found in kernel: ${revisionId}`);
@@ -168153,7 +168597,14 @@ var init_service3 = __esm({
168153
168597
  if (target.state === "working") {
168154
168598
  await this.kernel.merge({ artifactId, actor });
168155
168599
  } else if (target.state === "proposed") {
168600
+ if (voted) {
168601
+ const gate = pendingConcludeAttempt(this.kernel.model, artifactId)?.gate;
168602
+ throw new Error(
168603
+ `\u5DF2\u8BB0\u5F55\u4F60\u7684\u901A\u8FC7\u7968\uFF0C\u4F46\u8FD9\u4E00\u7248\u8FD8\u6CA1\u6536\u9F50\u8BC4\u5BA1${gate ? `\uFF08\u9700 ${gate.quorum} \u7968\uFF0C\u8BC4\u5BA1\u4EBA [${gate.eligible.join("\u3001")}]\uFF09` : ""}\u2014\u2014\u7B49\u5176\u4F59\u8BC4\u5BA1\u4EBA\u8868\u6001\u540E\u81EA\u52A8\u843D\u5730\u3002`
168604
+ );
168605
+ }
168156
168606
  await this.kernel.submitReview({ artifactId, actor, verdict: "approve", revisionId });
168607
+ voted = true;
168157
168608
  } else {
168158
168609
  throw new Error(`revision not approvable (state=${target.state}): ${revisionId}`);
168159
168610
  }
@@ -180387,7 +180838,7 @@ function collabDomain(opts) {
180387
180838
  const resolveProject = await buildProjectResolver(artifacts);
180388
180839
  const dispatchPausedOf = await buildDispatchPausedResolver(kernel.model, artifacts);
180389
180840
  const wsArts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === req.params.id);
180390
- const coordinatorEvents = engineStore ? await engineStore.transaction((tx) => tx.listEvents(req.params.id)) : void 0;
180841
+ const coordinatorEvents = engineStore ? await engineStore.transaction((tx) => tx.listEvents(req.params.id, 0, void 0, COORDINATOR_ACTIVITY_EVENT_KINDS)) : void 0;
180391
180842
  const signalsForThisCompany = opts.dispatcherSignalsOf ? (id) => opts.dispatcherSignalsOf(req.auth.companyId, id) : void 0;
180392
180843
  const roleDefsByName = opts.roles ? new Map((await opts.roles.list()).map((r) => [r.name, { label: r.label, defaultOwner: r.defaultOwner }])) : void 0;
180393
180844
  const roleDefaultOwnerOf = roleDefsByName ? (role) => {
@@ -180739,6 +181190,7 @@ function collabDomain(opts) {
180739
181190
  });
180740
181191
  };
180741
181192
  }
181193
+ var COORDINATOR_ACTIVITY_EVENT_KINDS;
180742
181194
  var init_collab = __esm({
180743
181195
  "../server/src/domains/collab/index.ts"() {
180744
181196
  "use strict";
@@ -180764,6 +181216,7 @@ var init_collab = __esm({
180764
181216
  init_organization_usage();
180765
181217
  init_planner();
180766
181218
  init_closure_report();
181219
+ COORDINATOR_ACTIVITY_EVENT_KINDS = ["plan.changed", "issue.create", "issue.resolve"];
180767
181220
  }
180768
181221
  });
180769
181222
 
@@ -182041,6 +182494,7 @@ var init_service7 = __esm({
182041
182494
  onCheckpointState;
182042
182495
  readBundleFile;
182043
182496
  onAgentHandoffPersisted;
182497
+ resolveNodeConclusions;
182044
182498
  constructor(opts) {
182045
182499
  this.store = opts.store;
182046
182500
  this.mode = opts.mode;
@@ -182051,6 +182505,7 @@ var init_service7 = __esm({
182051
182505
  this.onCheckpointState = opts.onCheckpointState;
182052
182506
  this.readBundleFile = opts.readBundleFile;
182053
182507
  this.onAgentHandoffPersisted = opts.onAgentHandoffPersisted;
182508
+ this.resolveNodeConclusions = opts.resolveNodeConclusions;
182054
182509
  }
182055
182510
  assertWritable() {
182056
182511
  if (this.mode === "off") throw new Error("execution continuity is disabled");
@@ -182246,6 +182701,12 @@ var init_service7 = __esm({
182246
182701
  async coordinatorView(workOrderId, companyId) {
182247
182702
  const graph = await this.resolveGraph(workOrderId, companyId);
182248
182703
  if (graph.length === 0) return null;
182704
+ let conclusions = {};
182705
+ try {
182706
+ conclusions = await this.resolveNodeConclusions?.(workOrderId, companyId) ?? {};
182707
+ } catch (error2) {
182708
+ console.warn(`[execution-continuity] conclusion \u8BFB\u53D6\u5931\u8D25\uFF08\u534F\u8C03\u8005\u89C6\u56FE\u7167\u5E38\u8FD4\u56DE\uFF09: ${String(error2)}`);
182709
+ }
182249
182710
  const attempts = await this.store.listAttempts(workOrderId);
182250
182711
  const latest = latestAttemptByNode(attempts);
182251
182712
  const handoffs = /* @__PURE__ */ new Map();
@@ -182309,7 +182770,10 @@ var init_service7 = __esm({
182309
182770
  recoverability: checkpoint.recoverability,
182310
182771
  lastCheckpointAt: checkpoint.createdAt
182311
182772
  } } : {},
182312
- blockedBy: nodeState === "waiting" ? node.dependsOn.filter((id) => !DONE_STATES.has(stateByNode.get(id) ?? "waiting")) : []
182773
+ blockedBy: nodeState === "waiting" ? node.dependsOn.filter((id) => !DONE_STATES.has(stateByNode.get(id) ?? "waiting")) : [],
182774
+ // 纯透传(展示层兜底料源,见 CoordinatorNodeView.conclusion)。不参与本方法里任何
182775
+ // nodeState / todo / dependencyBlocks / collaborationStatus 的派生。
182776
+ ...conclusions[node.nodeId]?.trim() ? { conclusion: conclusions[node.nodeId].trim() } : {}
182313
182777
  };
182314
182778
  });
182315
182779
  const reverse = reverseEdges(graph);
@@ -182457,6 +182921,23 @@ var init_upstream_handoff_input = __esm({
182457
182921
  }
182458
182922
  });
182459
182923
 
182924
+ // ../server/src/domains/execution-continuity/node-conclusions.ts
182925
+ function nodeConclusionsFromSnapshot(snapshot) {
182926
+ const workById = new Map(snapshot.works.map((work) => [work.id, work]));
182927
+ const out = {};
182928
+ for (const node of snapshot.nodes) {
182929
+ const accepted = node.latestAcceptId ? workById.get(node.latestAcceptId) : void 0;
182930
+ const conclusion = accepted?.conclusion?.trim() || snapshot.works.filter((work) => work.nodeId === node.id && work.status === "success" && work.conclusion?.trim()).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt)).at(-1)?.conclusion?.trim();
182931
+ if (conclusion) out[node.id] = conclusion;
182932
+ }
182933
+ return out;
182934
+ }
182935
+ var init_node_conclusions = __esm({
182936
+ "../server/src/domains/execution-continuity/node-conclusions.ts"() {
182937
+ "use strict";
182938
+ }
182939
+ });
182940
+
182460
182941
  // ../server/src/domains/execution-continuity/index.ts
182461
182942
  function createExecutionContinuityDomain(opts) {
182462
182943
  const service = new ExecutionContinuityService(opts);
@@ -182471,6 +182952,7 @@ var init_execution_continuity2 = __esm({
182471
182952
  init_routes8();
182472
182953
  init_handoff_work_bridge();
182473
182954
  init_upstream_handoff_input();
182955
+ init_node_conclusions();
182474
182956
  }
182475
182957
  });
182476
182958
 
@@ -186141,15 +186623,16 @@ var init_builtin_skills = __esm({
186141
186623
 
186142
186624
  // ../server/src/design/board-watch.ts
186143
186625
  function decideBoardLetter(c, nowMs, quietMs) {
186626
+ if (c.hasOpenWatchLetter) return false;
186627
+ if (c.freshTerminalRunId !== null) return true;
186144
186628
  if (c.boardMtime === null) return false;
186145
186629
  if (!c.hasRevision) return false;
186146
186630
  if (c.lastOpAtMs === null || c.boardMtime <= c.lastOpAtMs) return false;
186147
186631
  if (nowMs - c.boardMtime < quietMs) return false;
186148
- if (c.hasOpenWatchLetter) return false;
186149
186632
  if (c.lastWatchMtime !== null && c.boardMtime <= c.lastWatchMtime) return false;
186150
186633
  return true;
186151
186634
  }
186152
- var MARKER_PREFIX, boardWatchMarker, parseMarker, BoardWatcher;
186635
+ var MARKER_PREFIX, boardWatchMarker, parseMarker, RUN_MARKER_PREFIX, boardWatchRunMarker, parseRunMarker, TERMINAL_RUN_STATUS, BoardWatcher;
186153
186636
  var init_board_watch = __esm({
186154
186637
  "../server/src/design/board-watch.ts"() {
186155
186638
  "use strict";
@@ -186162,6 +186645,17 @@ var init_board_watch = __esm({
186162
186645
  const n = Number.parseInt(body.slice(i + MARKER_PREFIX.length), 10);
186163
186646
  return Number.isFinite(n) ? n : null;
186164
186647
  };
186648
+ RUN_MARKER_PREFIX = "[od-board-watch run:";
186649
+ boardWatchRunMarker = (runId) => `${RUN_MARKER_PREFIX}${runId}]`;
186650
+ parseRunMarker = (body) => {
186651
+ const i = body.indexOf(RUN_MARKER_PREFIX);
186652
+ if (i < 0) return null;
186653
+ const j = body.indexOf("]", i + RUN_MARKER_PREFIX.length);
186654
+ if (j < 0) return null;
186655
+ const id = body.slice(i + RUN_MARKER_PREFIX.length, j);
186656
+ return id.length > 0 ? id : null;
186657
+ };
186658
+ TERMINAL_RUN_STATUS = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled", "cancelled"]);
186165
186659
  BoardWatcher = class {
186166
186660
  constructor(deps) {
186167
186661
  this.deps = deps;
@@ -186178,6 +186672,22 @@ var init_board_watch = __esm({
186178
186672
  return null;
186179
186673
  }
186180
186674
  }
186675
+ /**
186676
+ * 查一块板的 run 列表(`GET /api/runs?projectId=`,与 CLI `design status` 同源)。
186677
+ * run 注册表是 od 的**内存态**——重启即空,查不到不代表没跑过;所以这里失败一律降级成空数组,
186678
+ * 由板文件 mtime 那条慢路径兜底,绝不因为查不到 run 就压掉写信。
186679
+ */
186680
+ async boardRuns(boardKey) {
186681
+ const fetchImpl = this.deps.fetchImpl ?? fetch;
186682
+ try {
186683
+ const res = await fetchImpl(`${this.deps.odBase}/api/runs?projectId=${encodeURIComponent(boardKey)}`);
186684
+ if (!res.ok) return [];
186685
+ const body = await res.json();
186686
+ return (body.runs ?? []).map((r) => ({ runId: String(r["runId"] ?? r["id"] ?? ""), status: String(r["status"] ?? "unknown") })).filter((r) => r.runId.length > 0);
186687
+ } catch {
186688
+ return [];
186689
+ }
186690
+ }
186181
186691
  async tick(now = Date.now()) {
186182
186692
  const { kernel, boardKeyOf, log: log3 } = this.deps;
186183
186693
  const quietMs = this.deps.quietMs ?? 10 * 6e4;
@@ -186194,9 +186704,17 @@ var init_board_watch = __esm({
186194
186704
  }
186195
186705
  const boardKey = boardKeyOf(a.id);
186196
186706
  const files = await this.boardFiles(boardKey);
186197
- if (!files || files.length === 0) continue;
186707
+ if (files === null) continue;
186708
+ const runs = await this.boardRuns(boardKey);
186709
+ if (files.length === 0 && runs.length === 0) continue;
186198
186710
  const boardMtime = files.reduce((mx, f2) => f2.mtime !== null && (mx === null || f2.mtime > mx) ? f2.mtime : mx, null);
186199
- const watchLetters = annotationsOf(model, a.id).filter((x2) => x2.author === actor && parseMarker(x2.body) !== null);
186711
+ const watchLetters = annotationsOf(model, a.id).filter(
186712
+ (x2) => x2.author === actor && (parseMarker(x2.body) !== null || parseRunMarker(x2.body) !== null)
186713
+ );
186714
+ const letteredRuns = new Set(
186715
+ watchLetters.map((x2) => parseRunMarker(x2.body)).filter((s2) => s2 !== null)
186716
+ );
186717
+ const newestTerminalRun = runs.find((r) => TERMINAL_RUN_STATUS.has(r.status)) ?? null;
186200
186718
  const lastOpAt = model.lastOpAt.get(a.id);
186201
186719
  const candidate = {
186202
186720
  boardMtime,
@@ -186206,21 +186724,41 @@ var init_board_watch = __esm({
186206
186724
  lastWatchMtime: watchLetters.reduce((mx, x2) => {
186207
186725
  const m2 = parseMarker(x2.body);
186208
186726
  return m2 !== null && (mx === null || m2 > mx) ? m2 : mx;
186209
- }, null)
186727
+ }, null),
186728
+ freshTerminalRunId: newestTerminalRun !== null && !letteredRuns.has(newestTerminalRun.runId) ? newestTerminalRun.runId : null
186210
186729
  };
186211
186730
  if (!decideBoardLetter(candidate, now, quietMs)) continue;
186212
- const newest = files.filter((f2) => f2.mtime !== null && candidate.lastOpAtMs !== null && f2.mtime > candidate.lastOpAtMs).sort((x2, y) => (y.mtime ?? 0) - (x2.mtime ?? 0));
186213
- const listed = (newest.length > 0 ? newest : files).slice(0, 5).map((f2) => `- ${f2.path}\uFF08${f2.kind ?? "?"}\uFF09\xB7 ${f2.mtime ? new Date(f2.mtime).toISOString() : "?"}`).join("\n");
186214
- const body = [
186215
- boardWatchMarker(boardMtime),
186216
- `\u753B\u677F ${boardKey} \u4E0A\u6709 Oasis \u4E4B\u5916\u7684\u65B0\u52A8\u9759\uFF08\u4EBA\u5728\u677F\u4E0A\u5BF9\u8BDD/\u6539\u7A3F\uFF0C\u6216\u5F15\u64CE\u4EA7\u4E86\u6CA1\u4EBA\u6536\u7684\u65B0\u7248\uFF09\uFF0C\u6BD4\u672C\u4EA7\u7269\u6700\u8FD1\u4E00\u6B21\u64CD\u4F5C\uFF08${lastOpAt ?? "?"}\uFF09\u65B0\u3001\u4E14\u5DF2\u9759\u9ED8 ${Math.round(quietMs / 6e4)} \u5206\u949F\uFF1A`,
186217
- listed,
186218
- ``,
186219
- `\u8BF7\u53D6\u8D27\u6838\u5BF9\uFF1A\`oasis design status ${a.id} --pull\` \u2192 \u5185\u5BB9\u5408\u610F\u5C31 \`oasis propose ${a.id} --from-dir <\u76EE\u5F55> --reason "<\u6536\u5F55\u677F\u4E0A\u4EBA\u5DE5\u8FED\u4EE3>"\` \u6536\u5F55\u4E3A\u65B0\u7248\u672C\uFF1B\u4E0D\u5408\u610F/\u4E0D\u5FC5\u6536\u5F55\uFF0C\u56DE\u5E16\u8BF4\u660E\u540E\u6309 wontfix \u5904\u7406\u5373\u53EF\uFF08\u540C\u4E00\u7248\u4E0D\u4F1A\u518D\u6765\u4FE1\uFF09\u3002`
186220
- ].join("\n");
186731
+ const fmt = (fs34) => fs34.slice(0, 5).map((f2) => `- ${f2.path}\uFF08${f2.kind ?? "?"}\uFF09\xB7 ${f2.mtime ? new Date(f2.mtime).toISOString() : "?"}`).join("\n");
186732
+ let body;
186733
+ let logLine;
186734
+ if (candidate.freshTerminalRunId !== null) {
186735
+ const status = newestTerminalRun.status;
186736
+ const ok = status === "succeeded";
186737
+ const listed = files.length > 0 ? fmt([...files].sort((x2, y) => (y.mtime ?? 0) - (x2.mtime ?? 0))) : "\uFF08\u677F\u4E0A\u6682\u65E0\u4EA4\u4ED8\u7269\uFF09";
186738
+ body = [
186739
+ // 两个 marker 都带:run marker 管快路径去重,mtime marker 顺手把慢路径也盖住——
186740
+ // 否则这封信被 resolve 后,慢路径会就**同一批板内容**再写一封(两路去重各管各的)。
186741
+ boardMtime !== null ? `${boardWatchRunMarker(candidate.freshTerminalRunId)} ${boardWatchMarker(boardMtime)}` : boardWatchRunMarker(candidate.freshTerminalRunId),
186742
+ ok ? `\u753B\u677F ${boardKey} \u7684\u5F15\u64CE\u8DD1\u5B8C\u4E86\uFF08run ${candidate.freshTerminalRunId.slice(0, 8)} \u7EC8\u6001=${status}\uFF09\uFF0C\u8D27\u5728\u677F\u4E0A\u7B49\u4F60\u6536\uFF1A` : `\u753B\u677F ${boardKey} \u7684\u5F15\u64CE\u8FD9\u4E00\u8F6E**\u6CA1\u8DD1\u6210**\uFF08run ${candidate.freshTerminalRunId.slice(0, 8)} \u7EC8\u6001=${status}\uFF09\u3002\u677F\u4E0A\u5F53\u524D\u5185\u5BB9\uFF1A`,
186743
+ listed,
186744
+ ``,
186745
+ ok ? `\u53D6\u8D27\u6838\u5BF9\uFF1A\`oasis design status ${a.id} --pull\` \u2192 \u5BF9\u7740 DESIGN_BRIEF.md \u4E0E inputs/ \u9010\u5B57\u6BB5\u6838\uFF08\u6570\u636E\u5BF9\u4E0D\u5BF9\u3001\u72B6\u6001\u5168\u4E0D\u5168\u3001\u6709\u6CA1\u6709\u7F16\u9020\uFF09\u3002\u591F\u597D\u5C31 \`oasis propose ${a.id} --from-dir <\u76EE\u5F55> --reason "<\u8FD9\u7248\u505A\u4E86\u4EC0\u4E48>"\`\uFF1B\u8981\u6539\u5C31\u56DE\u677F\u4E0A\u518D\u6765\u4E00\u8F6E \`oasis design chat ${a.id} --turn continue --message "<\u8981\u6539\u4EC0\u4E48>" --async\`\uFF08\u26D4 \u522B\u628A --pull \u4E0B\u6765\u7684\u6587\u4EF6\u5728\u672C\u5730\u624B\u6539\u540E propose\uFF09\u3002` : `\u5148 \`oasis design status ${a.id}\` \u770B\u677F\u4E0A\u6709\u6CA1\u6709\u53EF\u7528\u7684\u4E0A\u4E00\u7248\uFF1A\u6709\u5C31\u7167\u5E38\u53D6\u8D27\u6838\u5BF9\uFF1B\u6CA1\u6709\u5C31\u628A\u9700\u6C42\u4E0E\u771F\u5B9E\u6570\u636E\u9489\u5F97\u66F4\u6B7B\u540E\u91CD\u8D77\u4E00\u8F6E \`oasis design chat ${a.id} --message "<\u2026>" --async\`\uFF0C\u4ECD\u4E0D\u884C\u7528 \`oasis gap ${a.id} --desc "<\u5361\u5728\u54EA>"\` \u5192\u6CE1\u7ED9\u4EBA\u3002`
186746
+ ].join("\n");
186747
+ logLine = `[board-watch] ${a.id} \u2190 \u677F ${boardKey} \u5F15\u64CE\u6536\u5C3E\uFF08run ${candidate.freshTerminalRunId.slice(0, 8)} ${status}\uFF09\u2192 \u5DF2\u5199\u4FE1\u5524 owner`;
186748
+ } else {
186749
+ const newest = files.filter((f2) => f2.mtime !== null && candidate.lastOpAtMs !== null && f2.mtime > candidate.lastOpAtMs).sort((x2, y) => (y.mtime ?? 0) - (x2.mtime ?? 0));
186750
+ body = [
186751
+ boardWatchMarker(boardMtime),
186752
+ `\u753B\u677F ${boardKey} \u4E0A\u6709 Oasis \u4E4B\u5916\u7684\u65B0\u52A8\u9759\uFF08\u4EBA\u5728\u677F\u4E0A\u5BF9\u8BDD/\u6539\u7A3F\uFF0C\u6216\u5F15\u64CE\u4EA7\u4E86\u6CA1\u4EBA\u6536\u7684\u65B0\u7248\uFF09\uFF0C\u6BD4\u672C\u4EA7\u7269\u6700\u8FD1\u4E00\u6B21\u64CD\u4F5C\uFF08${lastOpAt ?? "?"}\uFF09\u65B0\u3001\u4E14\u5DF2\u9759\u9ED8 ${Math.round(quietMs / 6e4)} \u5206\u949F\uFF1A`,
186753
+ fmt(newest.length > 0 ? newest : files),
186754
+ ``,
186755
+ `\u8BF7\u53D6\u8D27\u6838\u5BF9\uFF1A\`oasis design status ${a.id} --pull\` \u2192 \u5185\u5BB9\u5408\u610F\u5C31 \`oasis propose ${a.id} --from-dir <\u76EE\u5F55> --reason "<\u6536\u5F55\u677F\u4E0A\u4EBA\u5DE5\u8FED\u4EE3>"\` \u6536\u5F55\u4E3A\u65B0\u7248\u672C\uFF1B\u4E0D\u5408\u610F/\u4E0D\u5FC5\u6536\u5F55\uFF0C\u56DE\u5E16\u8BF4\u660E\u540E\u6309 wontfix \u5904\u7406\u5373\u53EF\uFF08\u540C\u4E00\u7248\u4E0D\u4F1A\u518D\u6765\u4FE1\uFF09\u3002`
186756
+ ].join("\n");
186757
+ logLine = `[board-watch] ${a.id} \u2190 \u677F ${boardKey} \u6709\u65B0\u5185\u5BB9\uFF08mtime=${new Date(boardMtime).toISOString()}\uFF09\u2192 \u5DF2\u5199\u4FE1\u5524 owner`;
186758
+ }
186221
186759
  await kernel.annotate({ artifactId: a.id, actor, body, kind: "comment" });
186222
186760
  written++;
186223
- log3?.(`[board-watch] ${a.id} \u2190 \u677F ${boardKey} \u6709\u65B0\u5185\u5BB9\uFF08mtime=${new Date(boardMtime).toISOString()}\uFF09\u2192 \u5DF2\u5199\u4FE1\u5524 owner`);
186761
+ log3?.(logLine);
186224
186762
  }
186225
186763
  }
186226
186764
  };
@@ -192761,12 +193299,12 @@ function makeResumeRetryProxy(first, spawnFallback) {
192761
193299
  s2.onTelemetry?.((e) => fanTel(e));
192762
193300
  };
192763
193301
  wire(first);
192764
- const isSessionNotFound = (err) => {
193302
+ const isSessionNotFound2 = (err) => {
192765
193303
  const chatExit = err instanceof Error ? err.chatExit : void 0;
192766
193304
  return chatExit?.reason === "session-not-found";
192767
193305
  };
192768
193306
  const done = first.done.catch(async (err) => {
192769
- if (sawOutput || !isSessionNotFound(err)) throw err;
193307
+ if (sawOutput || !isSessionNotFound2(err)) throw err;
192770
193308
  const second = await spawnFallback();
192771
193309
  active = second;
192772
193310
  wire(second);
@@ -193440,6 +193978,16 @@ async function startServe(opts) {
193440
193978
  ...liveSchemaMap.get(artifact.type)?.ownerRole ? { assignedRole: liveSchemaMap.get(artifact.type).ownerRole } : {},
193441
193979
  dependsOn: artifact.inputs.map((edge) => edge.to)
193442
193980
  }));
193981
+ },
193982
+ // 业务交接的展示料源(见 CoordinatorNodeView.conclusion):每个节点最近一版**已收口** Work 的
193983
+ // conclusion。纯读、纯可选——读不到(无引擎 store / 工单不在库 / 任何异常)就返回空,
193984
+ // 协调者视图照常工作。
193985
+ resolveNodeConclusions: async (workOrderId, companyId) => {
193986
+ const companyEngine = await getEngine(companyId ?? defaultCompanyId);
193987
+ const store = companyEngine.kernel.getStore?.();
193988
+ if (!store) return {};
193989
+ const snap = await store.transaction((tx) => tx.loadWorkorder(workOrderId));
193990
+ return snap ? nodeConclusionsFromSnapshot(snap) : {};
193443
193991
  }
193444
193992
  });
193445
193993
  console.log(`[serve] execution-continuity mode=${continuityMode}\uFF08record=\u5BF9\u7167\u7EC4\uFF0Cresume=\u6062\u590D\u5305\u5B9E\u9A8C\u7EC4\uFF09`);
@@ -193585,7 +194133,7 @@ async function startServe(opts) {
193585
194133
  const selector = typeof raw === "string" && raw ? raw : void 0;
193586
194134
  const me = await companies.currentUser({ actor }, headers);
193587
194135
  const ctx = await companies.service.resolveCurrentCompany(me.accountId, selector);
193588
- if (!ctx) return { kind: "not-found" };
194136
+ if (!ctx) return { kind: "not-found", ...selector !== void 0 ? { selector } : {} };
193589
194137
  return ctx.isMember ? { kind: "ok", companyId: ctx.company.id } : { kind: "forbidden" };
193590
194138
  };
193591
194139
  const resolveActor = (token) => issuer.resolve(token) ?? (opts.allowSimpleTokens && token.startsWith("token:") ? token.slice("token:".length) : null);
@@ -193913,7 +194461,8 @@ async function startServe(opts) {
193913
194461
  const artifactId = `artifact:planner:${(0, import_node_crypto41.randomUUID)()}`;
193914
194462
  const handle = await chatRemoteAdapter.spawn({
193915
194463
  actor: planner.id,
193916
- actorToken: issueSessionToken(planner.id, { artifactId, action: "plan-workorder", limits, binding }),
194464
+ // companyId 同协调者:planner 用的就是默认公司的 actors 服务,漏签会让它一调公司域端点就 404。
194465
+ actorToken: issueSessionToken(planner.id, { artifactId, action: "plan-workorder", limits, binding, companyId: defaultCompanyId }),
193917
194466
  artifactId,
193918
194467
  bundle: { files },
193919
194468
  ...actorCtx?.config?.prompt ? { systemPrompt: actorCtx.config.prompt } : {},
@@ -194926,7 +195475,7 @@ async function startServe(opts) {
194926
195475
  if (engStore) void rebuildDispatchMappings(engStore);
194927
195476
  }
194928
195477
  newKernel.setEngineIO({
194929
- async dispatchWork(workId) {
195478
+ async dispatchWork(workId, opts2) {
194930
195479
  if (!newEngineProduce) return;
194931
195480
  if (dispatchedWorks.has(workId)) return;
194932
195481
  const kernel2 = newKernel;
@@ -194955,7 +195504,7 @@ async function startServe(opts) {
194955
195504
  }
194956
195505
  let result;
194957
195506
  try {
194958
- result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, workId, ledgerId);
195507
+ result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, workId, ledgerId, opts2?.resumeEligible);
194959
195508
  } catch (err) {
194960
195509
  dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` }).catch(ledgerWarn("close(error)"));
194961
195510
  console.log(`[new-engine] dispatchWork ${workId} \u6D3E\u53D1\u5F02\u5E38\uFF1A${String(err)} \u2192 \u7F6E work failed`);
@@ -195375,14 +195924,16 @@ async function startServe(opts) {
195375
195924
  });
195376
195925
  const slot = { dispatcher: d, logged: 0, ticking: false };
195377
195926
  dispatchers.set(companyId, slot);
195378
- newEngineProduce = async (artifactId, actorId, workId, dispatchId) => {
195927
+ newEngineProduce = async (artifactId, actorId, workId, dispatchId, resumeEligible) => {
195379
195928
  const result = await d.requestProduce({
195380
195929
  artifactId,
195381
195930
  actor: actorId,
195382
195931
  bypassScheduling: true,
195383
195932
  engineWorkId: workId,
195384
195933
  engineCompanyId: companyId,
195385
- ...dispatchId !== void 0 ? { dispatchId } : {}
195934
+ ...dispatchId !== void 0 ? { dispatchId } : {},
195935
+ // 工单 e4a43a42:返工(已收口节点被重激活)→ resumeEligible=false → 派发层起全新会话。
195936
+ ...resumeEligible !== void 0 ? { resumeEligible } : {}
195386
195937
  });
195387
195938
  return { kind: result.kind, dispatchId: result.kind === "dispatched" ? result.dispatchId : void 0 };
195388
195939
  };
@@ -195876,6 +196427,12 @@ ${st.detail}
195876
196427
  artifactId: ctx?.artifactId ?? "artifact:coordinator",
195877
196428
  action: ctx?.action ?? "coordinate",
195878
196429
  limits: ctx?.limits ?? { wallClockMs: wallClockForKind("coordinate") },
196430
+ // CO-101 门禁要这个 claim 才认得出「这次请求属于哪家公司」。漏签的后果不是报错这么简单:
196431
+ // 管理者的 roles / actors / workorders / inbox / session-log 会**全部** 404,而 status / ls /
196432
+ // graph / content 照常(热路径回退默认公司),于是它只瞎一半——够它误诊成「平台名册服务挂了」,
196433
+ // 再上报给人;未关的上报又会关掉自动重唤,把本可自愈的节点永久悬挂。2026-08-14 至少 3 张单
196434
+ // 栽在这上面。协调者与 apply 同用默认公司 kernel(见下方 structuralBaselines 注释),故取默认公司。
196435
+ companyId: defaultCompanyId,
195879
196436
  ...ctx?.binding !== void 0 ? { binding: ctx.binding } : {}
195880
196437
  });
195881
196438
  const ws = ctx?.artifactId ? kernel.model.artifacts.get(ctx.artifactId)?.workspace : void 0;
@@ -196846,6 +197403,33 @@ function reapClaudeProjects(workdir, runtimeKind) {
196846
197403
  } catch {
196847
197404
  }
196848
197405
  }
197406
+ function measureClaudeProjects(workdir, runtimeKind) {
197407
+ if (runtimeKind !== "claude" && runtimeKind !== "claude-code") return 0;
197408
+ const dir = import_node_path24.default.join(claudeProjectsRoot(), claudeProjectSlug(workdir));
197409
+ return dirSizeBytes2(dir);
197410
+ }
197411
+ function dirSizeBytes2(dir) {
197412
+ let total = 0;
197413
+ let entries;
197414
+ try {
197415
+ entries = import_node_fs20.default.readdirSync(dir, { withFileTypes: true });
197416
+ } catch {
197417
+ return 0;
197418
+ }
197419
+ for (const e of entries) {
197420
+ const p2 = import_node_path24.default.join(dir, e.name);
197421
+ if (e.isSymbolicLink()) continue;
197422
+ if (e.isDirectory()) {
197423
+ total += dirSizeBytes2(p2);
197424
+ continue;
197425
+ }
197426
+ try {
197427
+ total += import_node_fs20.default.statSync(p2).size;
197428
+ } catch {
197429
+ }
197430
+ }
197431
+ return total;
197432
+ }
196849
197433
 
196850
197434
  // ../cli/src/node.ts
196851
197435
  init_src6();
@@ -196928,6 +197512,8 @@ async function startNode(opts) {
196928
197512
  ...process.env["OASIS_GC_INTERVAL_MS"] ? { intervalMs: Number(process.env["OASIS_GC_INTERVAL_MS"]) } : {},
196929
197513
  queryArtifactStates: (ids2) => client.queryArtifactStates(ids2),
196930
197514
  reapRuntimeState: reapClaudeProjects,
197515
+ // 工单 e4a43a42:把连带的 claude 会话记录占用一并量进保留预算(比工作目录还大的那一坨)。
197516
+ measureRuntimeState: measureClaudeProjects,
196931
197517
  log: (m2) => console.log(`[oasis node] ${m2}`)
196932
197518
  });
196933
197519
  if (opts.gcEnabled !== false) console.log(`[oasis node] \u5DE5\u4F5C\u533A\u56DE\u6536\u5668\uFF1A\u5DF2\u542F\u7528\uFF08\u6839 ${workRoot}\uFF09`);
@@ -201013,7 +201599,8 @@ ${res.warning}`);
201013
201599
  r.id === v2.artifact.currentRev ? "\u2190 current head" : "",
201014
201600
  r.id === v2.concludedHead ? "\u2190 \u5DF2\u4EA4\u4ED8" : ""
201015
201601
  ].filter(Boolean).join(" ");
201016
- println(` ${r.id} ${r.state} by ${r.author} "${r.reason}"${marks ? ` ${marks}` : ""}`);
201602
+ const stateLabel = r.state === "superseded" && r.contentKind === "empty" ? "\u672A\u4EA7\u51FA" : r.state;
201603
+ println(` ${r.id} ${stateLabel} by ${r.author} "${r.reason}"${marks ? ` ${marks}` : ""}`);
201017
201604
  }
201018
201605
  const NOTE_ABSTRACT = 150;
201019
201606
  const voteRows = [];
@@ -201302,7 +201889,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
201302
201889
  }
201303
201890
 
201304
201891
  // src/index.ts
201305
- var PKG_VERSION = true ? "0.1.102" : "dev";
201892
+ var PKG_VERSION = true ? "0.1.104" : "dev";
201306
201893
  var OASIS_DIR = path26.join(os10.homedir(), ".oasis");
201307
201894
  var CONFIG_FILE = path26.join(OASIS_DIR, "node-config.json");
201308
201895
  var PID_FILE = path26.join(OASIS_DIR, "node.pid");