oasis_test 0.1.102 → 0.1.103

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 +574 -97
  2. package/package.json +1 -1
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
  }
@@ -7267,12 +7377,54 @@ 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 (!w2.endedAt && !hasOutput(w2, state.artifactsOf(w2.id).length)) {
7398
+ return {
7399
+ code: "work-in-flight",
7400
+ 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`,
7401
+ overridable: false
7402
+ };
7403
+ }
7404
+ return handoffBlock(state, w2.nodeId, workId);
7405
+ }
7406
+ function handoffBlock(state, nodeId, workId) {
7270
7407
  const work = state.work(workId);
7271
- if (!requiresBusinessHandoff(state, nodeId, work)) return true;
7408
+ if (!requiresBusinessHandoff(state, nodeId, work)) return null;
7409
+ if (work?.acceptanceState === "accepted" && work.overrideBy) return null;
7272
7410
  const node = state.node(nodeId);
7273
- return node?.latestWorkId === workId && (work?.businessHandoffStatus === "delivered" || work?.status === "success" && work.agentHandoffAttemptId === null && work.businessHandoffStatus === null);
7411
+ if (node?.latestWorkId !== workId) {
7412
+ return {
7413
+ code: "not-latest-work",
7414
+ detail: `\u4E0D\u662F\u672C\u8282\u70B9\u6700\u65B0\u4E00\u7248\uFF08\u6700\u65B0\uFF1A${node?.latestWorkId ?? "\u65E0"}\uFF09\u2014\u2014\u6536\u53E3\u53EA\u8BA4\u6700\u65B0\u7248`,
7415
+ overridable: false
7416
+ };
7417
+ }
7418
+ const delivered = work?.businessHandoffStatus === "delivered" || work?.status === "success" && work.agentHandoffAttemptId === null && work.businessHandoffStatus === null;
7419
+ if (delivered) return null;
7420
+ return {
7421
+ code: "handoff-undeclared",
7422
+ 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`,
7423
+ overridable: true
7424
+ };
7274
7425
  }
7275
7426
  function requiresBusinessHandoff(state, nodeId, work) {
7427
+ if (work?.replyToIssueId) return false;
7276
7428
  if (!state.edgesFrom(nodeId).some((edge) => isBusinessDependencyEdge(state, edge))) return false;
7277
7429
  if (typeof work?.assigneeActorId === "string" && work.assigneeActorId.startsWith("actor:human:")) return false;
7278
7430
  return true;
@@ -7465,6 +7617,9 @@ CREATE TABLE IF NOT EXISTS ${schema}.issues (
7465
7617
  author_actor_id text NOT NULL,
7466
7618
  hand_actor_id text,
7467
7619
  raised_by_node_id text,
7620
+ -- \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
7621
+ -- \uFF08ADR-0030 D1\uFF1Bwo-05dfbd6e \u8865\u9F50\u6865\u63A5\u8FC1\u79FB\u9057\u6F0F\uFF09\u3002\u975E escalation / \u672A\u5E26 --gap \u65F6\u4E3A NULL\u3002
7622
+ gap_id text,
7468
7623
  -- \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
7624
  -- 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
7625
  proposed_resolution text,
@@ -7589,6 +7744,8 @@ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS reply_to_issue_id text;
7589
7744
  -- \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
7745
  -- \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
7746
  ALTER TABLE ${schema}.issues ADD COLUMN IF NOT EXISTS resolution text;
7747
+ -- 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
7748
+ ALTER TABLE ${schema}.issues ADD COLUMN IF NOT EXISTS gap_id text;
7592
7749
  DO $do$ BEGIN
7593
7750
  IF EXISTS (SELECT 1 FROM information_schema.columns
7594
7751
  WHERE table_schema = '${schema}' AND table_name = 'issues' AND column_name = 'state') THEN
@@ -13054,6 +13211,7 @@ var init_store_postgres = __esm({
13054
13211
  authorActorId: i.author_actor_id,
13055
13212
  handActorId: i.hand_actor_id,
13056
13213
  raisedByNodeId: i.raised_by_node_id,
13214
+ gapId: i.gap_id ?? null,
13057
13215
  proposedResolution: i.proposed_resolution,
13058
13216
  proposedBy: i.proposed_by,
13059
13217
  proposedAt: iso(i.proposed_at),
@@ -13288,9 +13446,9 @@ var init_store_postgres = __esm({
13288
13446
  await this.c.query(
13289
13447
  `INSERT INTO ${this.s}.issues
13290
13448
  (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,
13449
+ raised_by_node_id,gap_id,proposed_resolution,proposed_by,proposed_at,
13292
13450
  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)
13451
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
13294
13452
  ON CONFLICT (id) DO NOTHING`,
13295
13453
  [
13296
13454
  i.id,
@@ -13302,6 +13460,7 @@ var init_store_postgres = __esm({
13302
13460
  i.authorActorId,
13303
13461
  i.handActorId,
13304
13462
  i.raisedByNodeId,
13463
+ i.gapId,
13305
13464
  i.proposedResolution,
13306
13465
  i.proposedBy,
13307
13466
  i.proposedAt,
@@ -13513,7 +13672,25 @@ var init_store_postgres = __esm({
13513
13672
  async lockWorkorder(workorderId) {
13514
13673
  await this.c.query(`SELECT 1 FROM ${this.s}.workorders WHERE id = $1 FOR UPDATE`, [workorderId]);
13515
13674
  }
13516
- async listEvents(workorderId, fromSeq = 0) {
13675
+ async listEvents(workorderId, fromSeq = 0, limit, kinds) {
13676
+ if (kinds && kinds.length > 0) {
13677
+ const res2 = workorderId ? await this.c.query(
13678
+ `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)}` : ""}`,
13679
+ [workorderId, fromSeq, [...kinds]]
13680
+ ) : await this.c.query(
13681
+ `SELECT * FROM ${this.s}.events WHERE seq > $1 AND kind = ANY($2) ORDER BY seq${limit !== void 0 ? ` DESC LIMIT ${Number(limit)}` : ""}`,
13682
+ [fromSeq, [...kinds]]
13683
+ );
13684
+ const rows = res2.rows.map(toRecord);
13685
+ return limit !== void 0 ? rows.reverse() : rows;
13686
+ }
13687
+ if (limit !== void 0) {
13688
+ const res2 = workorderId ? await this.c.query(
13689
+ `SELECT * FROM ${this.s}.events WHERE workorder_id = $1 AND seq > $2 ORDER BY seq DESC LIMIT $3`,
13690
+ [workorderId, fromSeq, limit]
13691
+ ) : await this.c.query(`SELECT * FROM ${this.s}.events WHERE seq > $1 ORDER BY seq DESC LIMIT $2`, [fromSeq, limit]);
13692
+ return res2.rows.map(toRecord).reverse();
13693
+ }
13517
13694
  const res = workorderId ? await this.c.query(
13518
13695
  `SELECT * FROM ${this.s}.events WHERE workorder_id = $1 AND seq > $2 ORDER BY seq`,
13519
13696
  [workorderId, fromSeq]
@@ -14064,7 +14241,7 @@ var init_work = __esm({
14064
14241
  // 回信 work 不杀前会话(apply 不 kill 主链,节点已完结、无在跑会话),只派发。
14065
14242
  async effect(e, ctx) {
14066
14243
  if (!e.replyToIssueId) await ctx.cancelPreviousWork(e.nodeId, e.workId);
14067
- await ctx.dispatchWork(e.workId);
14244
+ await ctx.dispatchWork(e.workId, e.resumeEligible !== void 0 ? { resumeEligible: e.resumeEligible } : void 0);
14068
14245
  }
14069
14246
  };
14070
14247
  workKill = {
@@ -14226,6 +14403,7 @@ var init_issue = __esm({
14226
14403
  authorActorId: e.authorActorId,
14227
14404
  handActorId: e.handActorId,
14228
14405
  raisedByNodeId: e.raisedByNodeId,
14406
+ gapId: e.gapId ?? null,
14229
14407
  proposedResolution: null,
14230
14408
  proposedBy: null,
14231
14409
  proposedAt: null,
@@ -14291,6 +14469,22 @@ var init_issue = __esm({
14291
14469
  resolvedNote: e.note,
14292
14470
  resolvedViaWorkId: e.viaWorkId
14293
14471
  });
14472
+ if (issue2.kind === "gap") {
14473
+ for (const other of state.allIssues()) {
14474
+ if (other.kind === "escalation" && other.resolvedAt === null && other.workorderId === issue2.workorderId && other.gapId === issue2.id) {
14475
+ state.updateIssue(other.id, {
14476
+ proposedResolution: null,
14477
+ proposedBy: null,
14478
+ proposedAt: null,
14479
+ resolvedAt: ctx.at,
14480
+ resolution: "resolved",
14481
+ resolvedBy: "actor:system:engine",
14482
+ resolvedNote: e.note,
14483
+ resolvedViaWorkId: null
14484
+ });
14485
+ }
14486
+ }
14487
+ }
14294
14488
  if (issue2.kind === "gap" || issue2.kind === "escalation") {
14295
14489
  const nodeId = issue2.raisedByNodeId ?? issue2.aboutNodeId;
14296
14490
  if (nodeId) markNodeForRetry(state, nodeId, ctx.at);
@@ -14461,11 +14655,9 @@ var init_review = __esm({
14461
14655
  name: "work/accept",
14462
14656
  kind: "work.accept",
14463
14657
  apply(e, state, ctx) {
14658
+ const block = acceptBlock(state, e.workId);
14659
+ if (block && !(block.overridable && e.override)) return;
14464
14660
  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
14661
  if (w2.replyToIssueId) {
14470
14662
  state.updateWork(e.workId, {
14471
14663
  acceptanceState: "accepted",
@@ -14742,8 +14934,10 @@ var init_bus = __esm({
14742
14934
  };
14743
14935
  }
14744
14936
  /** 读最近事件(/api/events 首次连接回放 / 调试视图),替代 oplog.read。 */
14937
+ /** 最近 N 条事件(时间线/详情页用)。★ 2026-08-16:limit 以前**声明了但没传下去**——
14938
+ * 百万级事件的病态工单一次性全量拉回内存,直接把 serve 打崩(ws:wo-18c2bc4b 实证)。 */
14745
14939
  recentEvents(workorderId, fromSeq, limit = 200) {
14746
- return this.store.transaction((tx) => tx.listEvents(workorderId, fromSeq));
14940
+ return this.store.transaction((tx) => tx.listEvents(workorderId, fromSeq, limit));
14747
14941
  }
14748
14942
  /** 取存储引用——供 serve 层创建 IO 适配器时使用。 */
14749
14943
  getStore() {
@@ -15021,7 +15215,7 @@ var init_bus = __esm({
15021
15215
  eventSeq: record8.seq,
15022
15216
  actorId: record8.actorId,
15023
15217
  emit: (event, opts) => void buffered.push({ event, ...opts }),
15024
- dispatchWork: (id) => this.io.dispatchWork(id),
15218
+ dispatchWork: (id, opts) => this.io.dispatchWork(id, opts),
15025
15219
  dispatchReview: (id, nodeId, reviewerActorId) => this.io.dispatchReview(id, nodeId, reviewerActorId),
15026
15220
  cancelSession: (ref2) => this.io.cancelSession(ref2),
15027
15221
  // ★ work.create 的「kill 旧会话」:apply 已把该节点上一个 latest_work 置 deadAt(会话可能还挂着),
@@ -15491,6 +15685,18 @@ function bridgeLoadReadModel(input) {
15491
15685
  name: a.name
15492
15686
  })));
15493
15687
  }
15688
+ const acceptBlockers = /* @__PURE__ */ new Map();
15689
+ for (const snap of snapshots2) {
15690
+ let state = null;
15691
+ for (const node of snap.nodes) {
15692
+ if (!node.latestWorkId || node.latestAcceptId === node.latestWorkId) continue;
15693
+ const lw = snap.works.find((w2) => w2.id === node.latestWorkId);
15694
+ if (!lw || !lw.endedAt || lw.deadAt) continue;
15695
+ state ??= new WorkorderState(snap);
15696
+ const block = acceptBlock(state, lw.id);
15697
+ if (block) acceptBlockers.set(node.id, block);
15698
+ }
15699
+ }
15494
15700
  const artifactFiles = /* @__PURE__ */ new Map();
15495
15701
  for (const a of allArtifacts) {
15496
15702
  const list = artifactFiles.get(a.workId) ?? [];
@@ -15685,7 +15891,8 @@ function bridgeLoadReadModel(input) {
15685
15891
  pinMeta,
15686
15892
  gaps,
15687
15893
  escalations,
15688
- pendingReviews
15894
+ pendingReviews,
15895
+ acceptBlockers
15689
15896
  };
15690
15897
  }
15691
15898
  var init_bridge_readmodel = __esm({
@@ -16677,8 +16884,8 @@ var init_kernel_bridge = __esm({
16677
16884
  const pendingHead = pendingConcludeAttempt(this.model, args.artifactId)?.head;
16678
16885
  const targetWorkId = args.revisionId ?? (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? pendingHead ?? concludedHead(this.model, args.artifactId);
16679
16886
  if (!targetWorkId) throw new Error("no concluded head to review");
16680
- let reviewId = null;
16681
- let reviewGroup = "default";
16887
+ const verdict = args.verdict === "approve" ? "approve" : "request_changes";
16888
+ let targets = [];
16682
16889
  if (args.reviewId !== void 0) {
16683
16890
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16684
16891
  const row = snap?.reviews.find((r) => r.id === args.reviewId) ?? null;
@@ -16698,32 +16905,67 @@ var init_kernel_bridge = __esm({
16698
16905
  if (row.verdict === null && (row.cancelledAt || row.endedAt)) {
16699
16906
  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
16907
  }
16701
- reviewId = args.reviewId;
16702
- reviewGroup = row.reviewGroup;
16703
- } else if (!args.revisionId) {
16908
+ targets = [{ id: row.id, group: row.reviewGroup }];
16909
+ } else {
16704
16910
  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;
16911
+ const rows = snap?.reviews.filter(
16912
+ (r) => r.targetWorkId === targetWorkId && r.reviewerActorId === args.actor
16913
+ ) ?? [];
16914
+ const open2 = rows.filter((r) => isReviewOpen(r));
16915
+ if (open2.length === 0) {
16916
+ const decided = rows.filter((r) => r.verdict !== null);
16917
+ if (decided.length > 0) {
16918
+ if (decided.every((r) => r.verdict === verdict)) {
16919
+ return { status: verdict === "approve" ? "satisfied" : "failed" };
16920
+ }
16921
+ throw new KernelError(
16922
+ `\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`,
16923
+ "review-already-decided"
16924
+ );
16925
+ }
16926
+ const reqs = snap?.requirements.filter((r) => r.nodeId === args.artifactId) ?? [];
16927
+ const mine = reqs.filter((r) => r.reviewerActorId === args.actor);
16928
+ if (mine.length > 0) {
16929
+ targets = [...new Set(mine.map((r) => r.reviewGroup))].map((group) => ({ id: null, group }));
16930
+ } else {
16931
+ const stale = (snap?.reviews ?? []).filter(
16932
+ (r) => r.nodeId === args.artifactId && r.reviewerActorId === args.actor && r.targetWorkId !== targetWorkId
16933
+ );
16934
+ if (reqs.length === 0) {
16935
+ throw new KernelError(
16936
+ `${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`,
16937
+ "no-review-requirement"
16938
+ );
16939
+ }
16940
+ throw new KernelError(
16941
+ `\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`,
16942
+ "review-row-not-found"
16943
+ );
16944
+ }
16945
+ } else {
16946
+ targets = open2.map((r) => ({ id: r.id, group: r.reviewGroup }));
16947
+ }
16708
16948
  }
16709
- await this.commit({
16710
- companyId: "",
16711
- workorderId: wid,
16712
- actorId: args.actor,
16713
- handActorId: args.hand ?? null,
16714
- event: {
16715
- kind: "review.response",
16949
+ for (const target of targets) {
16950
+ await this.commit({
16951
+ companyId: "",
16716
16952
  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
- });
16953
+ actorId: args.actor,
16954
+ handActorId: args.hand ?? null,
16955
+ event: {
16956
+ kind: "review.response",
16957
+ workorderId: wid,
16958
+ reviewId: target.id,
16959
+ // 有行投行;无行时带真实桶名建本人的行(不再是没人指的 default 幽灵行)
16960
+ targetWorkId,
16961
+ reviewerActorId: args.actor,
16962
+ reviewGroup: target.group,
16963
+ verdict,
16964
+ note: args.note ?? null,
16965
+ handActorId: args.hand ?? null
16966
+ }
16967
+ });
16968
+ }
16727
16969
  const attempt = this.model.concludeAttempts.get(args.artifactId)?.at(-1);
16728
16970
  return attempt ? { status: args.verdict === "approve" ? "satisfied" : "failed" } : { status: "pending" };
16729
16971
  }
@@ -16731,8 +16973,17 @@ var init_kernel_bridge = __esm({
16731
16973
  const wid = this.wo(args.artifactId);
16732
16974
  const artifact = this.model.artifacts.get(args.artifactId);
16733
16975
  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");
16976
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16977
+ const head = (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? concludedHead(this.model, args.artifactId) ?? (snap?.nodes.find((n) => n.id === args.artifactId)?.latestWorkId ?? null);
16978
+ 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");
16979
+ const block = snap ? acceptBlock(new WorkorderState(snap), head) : null;
16980
+ if (block?.code === "already-accepted") return;
16981
+ if (block && !block.overridable) {
16982
+ throw new KernelError(
16983
+ `force-conclude \u672A\u6267\u884C\uFF08${args.artifactId} @ ${head}\uFF09\uFF1A${block.detail}`,
16984
+ `accept-blocked:${block.code}`
16985
+ );
16986
+ }
16736
16987
  await this.commit({
16737
16988
  companyId: "",
16738
16989
  workorderId: wid,
@@ -16745,6 +16996,14 @@ var init_kernel_bridge = __esm({
16745
16996
  override: { by: args.actor, reason: args.reason ?? args.note ?? "force concluded" }
16746
16997
  }
16747
16998
  });
16999
+ const after = await this.store.transaction((tx) => tx.loadWorkorder(wid));
17000
+ const accepted = after?.works.find((w2) => w2.id === head)?.acceptanceState === "accepted";
17001
+ if (!accepted) {
17002
+ throw new KernelError(
17003
+ `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"}`,
17004
+ "accept-no-effect"
17005
+ );
17006
+ }
16748
17007
  }
16749
17008
  async groupCommit(args) {
16750
17009
  await this.merge({ artifactId: args.artifactId, actor: args.actor });
@@ -16953,6 +17212,7 @@ var init_kernel_bridge = __esm({
16953
17212
  issueId,
16954
17213
  issueKind: "escalation",
16955
17214
  body,
17215
+ gapId: args.gapId,
16956
17216
  aboutNodeId: args.artifactId,
16957
17217
  aboutWorkId: null,
16958
17218
  // ★ 修复(实证 ws:wo-a558f8cc):raisedByNodeId 只在 **agent/人自提**(/api/cmd escalate)时填节点——
@@ -17234,7 +17494,10 @@ var init_kernel_bridge = __esm({
17234
17494
  });
17235
17495
  }
17236
17496
  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`);
17497
+ throw new KernelError(
17498
+ `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`,
17499
+ "deprecated-op"
17500
+ );
17238
17501
  }
17239
17502
  async recordToolCall(_args) {
17240
17503
  }
@@ -17331,6 +17594,7 @@ var init_kernel_bridge = __esm({
17331
17594
  }
17332
17595
  const wid = plan.workspace ?? plan.ops.find((o) => o.action === "spawn" && o.workspace)?.workspace ?? "ws:default";
17333
17596
  let applied = 0;
17597
+ const unsupported = [];
17334
17598
  await this.ensureWorkorder(wid, "", _actor);
17335
17599
  for (const op of plan.ops) {
17336
17600
  switch (op.action) {
@@ -17469,6 +17733,7 @@ var init_kernel_bridge = __esm({
17469
17733
  break;
17470
17734
  }
17471
17735
  default:
17736
+ unsupported.push(`${op.action}\uFF08${op.artifactId ?? "?"}\uFF09`);
17472
17737
  break;
17473
17738
  }
17474
17739
  }
@@ -17492,6 +17757,12 @@ var init_kernel_bridge = __esm({
17492
17757
  this.draftCards.delete(plan.resolvesProposal);
17493
17758
  await this.refreshModel();
17494
17759
  }
17760
+ if (unsupported.length > 0) {
17761
+ throw new KernelError(
17762
+ `\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`,
17763
+ "unsupported-op"
17764
+ );
17765
+ }
17495
17766
  return { interventionId: `intervention:${(0, import_node_crypto3.randomUUID)()}`, applied };
17496
17767
  }
17497
17768
  };
@@ -17516,6 +17787,7 @@ var init_src4 = __esm({
17516
17787
  init_workorder_ledger();
17517
17788
  init_closure_gate();
17518
17789
  init_exit_classify();
17790
+ init_produce_resume_fallback();
17519
17791
  init_dispatcher();
17520
17792
  init_session_limits();
17521
17793
  init_kernel_bridge();
@@ -160686,6 +160958,9 @@ ${task}` : task;
160686
160958
  });
160687
160959
 
160688
160960
  // ../adapters/src/_core/workdir-gc.ts
160961
+ function reclaimableBytes(c) {
160962
+ return c.sizeBytes + (c.runtimeStateBytes ?? 0);
160963
+ }
160689
160964
  function idleMs(c, now) {
160690
160965
  const last = c.meta?.lastUsedAt ? Date.parse(c.meta.lastUsedAt) : NaN;
160691
160966
  return now - (Number.isFinite(last) ? last : c.mtimeMs);
@@ -160706,19 +160981,58 @@ function decideOne(c, policy, now) {
160706
160981
  }
160707
160982
  if (c.artifactState === void 0) return mk("keep", "artifact-unknown");
160708
160983
  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");
160984
+ return mk("keep", "artifact-active");
160711
160985
  }
160712
- if (c.meta.lastExit?.reason === "clean") return mk("delete", "settled-clean");
160713
160986
  return idle > policy.ttlMs ? mk("delete", "settled-ttl") : mk("keep", "within-ttl");
160714
160987
  }
160715
160988
  function planGc(candidates, policy, now) {
160716
160989
  const decisions = candidates.map((c) => decideOne(c, policy, now));
160717
- const { highWaterBytes, lowWaterBytes } = policy;
160718
- if (highWaterBytes === void 0) return decisions;
160719
160990
  const byDir = new Map(candidates.map((c) => [c.dir, c]));
160991
+ applyRetentionBudget(decisions, byDir, policy, now);
160992
+ applyHighWaterLru(decisions, byDir, policy, now);
160993
+ return decisions;
160994
+ }
160995
+ function applyRetentionBudget(decisions, byDir, policy, now) {
160996
+ const maxRounds = policy.maxRoundsPerArtifact;
160997
+ const capBytes = policy.sessionStoreCapBytes;
160998
+ 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"));
160999
+ if (maxRounds !== void 0 && maxRounds >= 0) {
161000
+ const byArtifact = /* @__PURE__ */ new Map();
161001
+ for (const item of evictable()) {
161002
+ const id = item.c.meta?.artifactId;
161003
+ if (!id) continue;
161004
+ (byArtifact.get(id) ?? byArtifact.set(id, []).get(id)).push(item);
161005
+ }
161006
+ for (const group of byArtifact.values()) {
161007
+ if (group.length <= maxRounds) continue;
161008
+ group.sort((a, b2) => idleMs(a.c, now) - idleMs(b2.c, now));
161009
+ for (const { d } of group.slice(maxRounds)) {
161010
+ d.action = "delete";
161011
+ d.reason = "budget-rounds";
161012
+ }
161013
+ }
161014
+ }
161015
+ if (capBytes !== void 0) {
161016
+ let remaining = decisions.filter((d) => d.action === "keep").reduce((sum, d) => sum + reclaimableBytes(byDir.get(d.dir)), 0);
161017
+ if (remaining > capBytes) {
161018
+ const tier = (c) => c.artifactState === "settled" ? 0 : 1;
161019
+ const order = evictable().sort(
161020
+ (a, b2) => tier(a.c) - tier(b2.c) || idleMs(b2.c, now) - idleMs(a.c, now)
161021
+ );
161022
+ for (const { d, c } of order) {
161023
+ if (remaining <= capBytes) break;
161024
+ d.action = "delete";
161025
+ d.reason = "budget-cap";
161026
+ remaining -= reclaimableBytes(c);
161027
+ }
161028
+ }
161029
+ }
161030
+ }
161031
+ function applyHighWaterLru(decisions, byDir, policy, now) {
161032
+ const { highWaterBytes, lowWaterBytes } = policy;
161033
+ if (highWaterBytes === void 0) return;
160720
161034
  let remaining = decisions.filter((d) => d.action === "keep").reduce((sum, d) => sum + d.sizeBytes, 0);
160721
- if (remaining <= highWaterBytes) return decisions;
161035
+ if (remaining <= highWaterBytes) return;
160722
161036
  const target = lowWaterBytes ?? highWaterBytes;
160723
161037
  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
161038
  for (const { d } of lru) {
@@ -160727,17 +161041,23 @@ function planGc(candidates, policy, now) {
160727
161041
  d.reason = "high-water-lru";
160728
161042
  remaining -= d.sizeBytes;
160729
161043
  }
160730
- return decisions;
160731
161044
  }
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);
161045
+ function gcAccounting(decisions, policy, candidates) {
161046
+ const byDir = candidates ? new Map(candidates.map((c) => [c.dir, c])) : void 0;
161047
+ const sizeOf = (d) => {
161048
+ const c = byDir?.get(d.dir);
161049
+ return c ? reclaimableBytes(c) : d.sizeBytes;
161050
+ };
161051
+ const totalBytes = decisions.reduce((s2, d) => s2 + sizeOf(d), 0);
161052
+ const freedBytes = decisions.filter((d) => d.action === "delete").reduce((s2, d) => s2 + sizeOf(d), 0);
160735
161053
  const keptBytes = totalBytes - freedBytes;
161054
+ const cap = policy.sessionStoreCapBytes;
160736
161055
  return {
160737
161056
  totalBytes,
160738
161057
  keptBytes,
160739
161058
  freedBytes,
160740
- overHighWater: policy.highWaterBytes !== void 0 && keptBytes > policy.highWaterBytes
161059
+ // 高水位报警:旧 highWaterBytes 或新总量上限任一被突破都报(新上限缺省即开)。
161060
+ overHighWater: policy.highWaterBytes !== void 0 && keptBytes > policy.highWaterBytes || cap !== void 0 && keptBytes > cap
160741
161061
  };
160742
161062
  }
160743
161063
  var DEFAULT_GC_POLICY;
@@ -160748,7 +161068,9 @@ var init_workdir_gc = __esm({
160748
161068
  orphanTtlMs: 24 * 36e5,
160749
161069
  ttlMs: 7 * 24 * 36e5,
160750
161070
  chatTtlMs: 14 * 24 * 36e5,
160751
- abnormalGraceMs: 24 * 36e5
161071
+ abnormalGraceMs: 24 * 36e5,
161072
+ maxRoundsPerArtifact: 3,
161073
+ sessionStoreCapBytes: 2 * 1024 ** 3
160752
161074
  };
160753
161075
  }
160754
161076
  });
@@ -160819,14 +161141,20 @@ function kindFromPath(workRoot, dir) {
160819
161141
  if (parts[0] === "tmp" && parts[1]) return parts[1].replace(/-[^-]*$/, "");
160820
161142
  return void 0;
160821
161143
  }
160822
- function observe(dir) {
161144
+ function observe(dir, workRoot, measureRuntimeState) {
160823
161145
  const meta = readMeta(dir);
160824
161146
  let mtimeMs = 0;
160825
161147
  try {
160826
161148
  mtimeMs = import_node_fs11.default.statSync(dir).mtimeMs;
160827
161149
  } catch {
160828
161150
  }
160829
- return { dir, meta, sizeBytes: dirSizeBytes(dir), mtimeMs, live: isWorkdirLive(dir) };
161151
+ const runtimeKind = meta?.runtimeKind ?? kindFromPath(workRoot, dir);
161152
+ let runtimeStateBytes = 0;
161153
+ try {
161154
+ runtimeStateBytes = measureRuntimeState?.(dir, runtimeKind) ?? 0;
161155
+ } catch {
161156
+ }
161157
+ return { dir, meta, sizeBytes: dirSizeBytes(dir), runtimeStateBytes, mtimeMs, live: isWorkdirLive(dir) };
160830
161158
  }
160831
161159
  async function runGcSweep(deps) {
160832
161160
  const policy = { ...DEFAULT_GC_POLICY, ...deps.policy };
@@ -160834,7 +161162,7 @@ async function runGcSweep(deps) {
160834
161162
  const log3 = deps.log ?? (() => {
160835
161163
  });
160836
161164
  const dirs = listWorkdirs(deps.workRoot, deps.legacyRoot);
160837
- const candidates = dirs.map((d) => observe(d));
161165
+ const candidates = dirs.map((d) => observe(d, deps.workRoot, deps.measureRuntimeState));
160838
161166
  const needStates = candidates.filter(
160839
161167
  (c) => c.meta?.artifactId && c.meta.kind !== "chat" && c.meta.kind !== "one-shot"
160840
161168
  );
@@ -160852,7 +161180,7 @@ async function runGcSweep(deps) {
160852
161180
  if (st) c.artifactState = st;
160853
161181
  }
160854
161182
  const decisions = planGc(candidates, policy, now);
160855
- const acct = gcAccounting(decisions, policy);
161183
+ const acct = gcAccounting(decisions, policy, candidates);
160856
161184
  let deleted = 0;
160857
161185
  for (const d of decisions) {
160858
161186
  if (d.action !== "delete") continue;
@@ -164661,7 +164989,13 @@ async function startOasisServer(opts) {
164661
164989
  try {
164662
164990
  let companyId;
164663
164991
  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");
164992
+ if (companyCtx.kind === "not-found") {
164993
+ throw companyCtx.selector ? new ApiError(404, "COMPANY_NOT_FOUND", `\u8BF7\u6C42\u6307\u5B9A\u7684\u516C\u53F8\u4E0D\u5B58\u5728\uFF1A${companyCtx.selector}`) : new ApiError(
164994
+ 404,
164995
+ "COMPANY_UNRESOLVED",
164996
+ `\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`
164997
+ );
164998
+ }
164665
164999
  if (companyCtx.kind === "forbidden") throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
164666
165000
  if (companyCtx.kind === "ok") companyId = companyCtx.companyId;
164667
165001
  }
@@ -168146,6 +168480,7 @@ var init_service3 = __esm({
168146
168480
  * 队首若 stale(base ≠ 当前 head,文档层乱序 approve 会撞上)→ 内核拒绝并指 rebase/group-commit。
168147
168481
  */
168148
168482
  async mergeUpTo(artifactId, revisionId, actor) {
168483
+ let voted = false;
168149
168484
  for (let guard = 0; guard < 1e3; guard++) {
168150
168485
  const target = this.kernel.model.revisions.get(revisionId);
168151
168486
  if (!target) throw new Error(`revision not found in kernel: ${revisionId}`);
@@ -168153,7 +168488,14 @@ var init_service3 = __esm({
168153
168488
  if (target.state === "working") {
168154
168489
  await this.kernel.merge({ artifactId, actor });
168155
168490
  } else if (target.state === "proposed") {
168491
+ if (voted) {
168492
+ const gate = pendingConcludeAttempt(this.kernel.model, artifactId)?.gate;
168493
+ throw new Error(
168494
+ `\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`
168495
+ );
168496
+ }
168156
168497
  await this.kernel.submitReview({ artifactId, actor, verdict: "approve", revisionId });
168498
+ voted = true;
168157
168499
  } else {
168158
168500
  throw new Error(`revision not approvable (state=${target.state}): ${revisionId}`);
168159
168501
  }
@@ -180387,7 +180729,7 @@ function collabDomain(opts) {
180387
180729
  const resolveProject = await buildProjectResolver(artifacts);
180388
180730
  const dispatchPausedOf = await buildDispatchPausedResolver(kernel.model, artifacts);
180389
180731
  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;
180732
+ const coordinatorEvents = engineStore ? await engineStore.transaction((tx) => tx.listEvents(req.params.id, 0, void 0, COORDINATOR_ACTIVITY_EVENT_KINDS)) : void 0;
180391
180733
  const signalsForThisCompany = opts.dispatcherSignalsOf ? (id) => opts.dispatcherSignalsOf(req.auth.companyId, id) : void 0;
180392
180734
  const roleDefsByName = opts.roles ? new Map((await opts.roles.list()).map((r) => [r.name, { label: r.label, defaultOwner: r.defaultOwner }])) : void 0;
180393
180735
  const roleDefaultOwnerOf = roleDefsByName ? (role) => {
@@ -180739,6 +181081,7 @@ function collabDomain(opts) {
180739
181081
  });
180740
181082
  };
180741
181083
  }
181084
+ var COORDINATOR_ACTIVITY_EVENT_KINDS;
180742
181085
  var init_collab = __esm({
180743
181086
  "../server/src/domains/collab/index.ts"() {
180744
181087
  "use strict";
@@ -180764,6 +181107,7 @@ var init_collab = __esm({
180764
181107
  init_organization_usage();
180765
181108
  init_planner();
180766
181109
  init_closure_report();
181110
+ COORDINATOR_ACTIVITY_EVENT_KINDS = ["plan.changed", "issue.create", "issue.resolve"];
180767
181111
  }
180768
181112
  });
180769
181113
 
@@ -182041,6 +182385,7 @@ var init_service7 = __esm({
182041
182385
  onCheckpointState;
182042
182386
  readBundleFile;
182043
182387
  onAgentHandoffPersisted;
182388
+ resolveNodeConclusions;
182044
182389
  constructor(opts) {
182045
182390
  this.store = opts.store;
182046
182391
  this.mode = opts.mode;
@@ -182051,6 +182396,7 @@ var init_service7 = __esm({
182051
182396
  this.onCheckpointState = opts.onCheckpointState;
182052
182397
  this.readBundleFile = opts.readBundleFile;
182053
182398
  this.onAgentHandoffPersisted = opts.onAgentHandoffPersisted;
182399
+ this.resolveNodeConclusions = opts.resolveNodeConclusions;
182054
182400
  }
182055
182401
  assertWritable() {
182056
182402
  if (this.mode === "off") throw new Error("execution continuity is disabled");
@@ -182246,6 +182592,12 @@ var init_service7 = __esm({
182246
182592
  async coordinatorView(workOrderId, companyId) {
182247
182593
  const graph = await this.resolveGraph(workOrderId, companyId);
182248
182594
  if (graph.length === 0) return null;
182595
+ let conclusions = {};
182596
+ try {
182597
+ conclusions = await this.resolveNodeConclusions?.(workOrderId, companyId) ?? {};
182598
+ } catch (error2) {
182599
+ console.warn(`[execution-continuity] conclusion \u8BFB\u53D6\u5931\u8D25\uFF08\u534F\u8C03\u8005\u89C6\u56FE\u7167\u5E38\u8FD4\u56DE\uFF09: ${String(error2)}`);
182600
+ }
182249
182601
  const attempts = await this.store.listAttempts(workOrderId);
182250
182602
  const latest = latestAttemptByNode(attempts);
182251
182603
  const handoffs = /* @__PURE__ */ new Map();
@@ -182309,7 +182661,10 @@ var init_service7 = __esm({
182309
182661
  recoverability: checkpoint.recoverability,
182310
182662
  lastCheckpointAt: checkpoint.createdAt
182311
182663
  } } : {},
182312
- blockedBy: nodeState === "waiting" ? node.dependsOn.filter((id) => !DONE_STATES.has(stateByNode.get(id) ?? "waiting")) : []
182664
+ blockedBy: nodeState === "waiting" ? node.dependsOn.filter((id) => !DONE_STATES.has(stateByNode.get(id) ?? "waiting")) : [],
182665
+ // 纯透传(展示层兜底料源,见 CoordinatorNodeView.conclusion)。不参与本方法里任何
182666
+ // nodeState / todo / dependencyBlocks / collaborationStatus 的派生。
182667
+ ...conclusions[node.nodeId]?.trim() ? { conclusion: conclusions[node.nodeId].trim() } : {}
182313
182668
  };
182314
182669
  });
182315
182670
  const reverse = reverseEdges(graph);
@@ -182457,6 +182812,23 @@ var init_upstream_handoff_input = __esm({
182457
182812
  }
182458
182813
  });
182459
182814
 
182815
+ // ../server/src/domains/execution-continuity/node-conclusions.ts
182816
+ function nodeConclusionsFromSnapshot(snapshot) {
182817
+ const workById = new Map(snapshot.works.map((work) => [work.id, work]));
182818
+ const out = {};
182819
+ for (const node of snapshot.nodes) {
182820
+ const accepted = node.latestAcceptId ? workById.get(node.latestAcceptId) : void 0;
182821
+ 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();
182822
+ if (conclusion) out[node.id] = conclusion;
182823
+ }
182824
+ return out;
182825
+ }
182826
+ var init_node_conclusions = __esm({
182827
+ "../server/src/domains/execution-continuity/node-conclusions.ts"() {
182828
+ "use strict";
182829
+ }
182830
+ });
182831
+
182460
182832
  // ../server/src/domains/execution-continuity/index.ts
182461
182833
  function createExecutionContinuityDomain(opts) {
182462
182834
  const service = new ExecutionContinuityService(opts);
@@ -182471,6 +182843,7 @@ var init_execution_continuity2 = __esm({
182471
182843
  init_routes8();
182472
182844
  init_handoff_work_bridge();
182473
182845
  init_upstream_handoff_input();
182846
+ init_node_conclusions();
182474
182847
  }
182475
182848
  });
182476
182849
 
@@ -186141,15 +186514,16 @@ var init_builtin_skills = __esm({
186141
186514
 
186142
186515
  // ../server/src/design/board-watch.ts
186143
186516
  function decideBoardLetter(c, nowMs, quietMs) {
186517
+ if (c.hasOpenWatchLetter) return false;
186518
+ if (c.freshTerminalRunId !== null) return true;
186144
186519
  if (c.boardMtime === null) return false;
186145
186520
  if (!c.hasRevision) return false;
186146
186521
  if (c.lastOpAtMs === null || c.boardMtime <= c.lastOpAtMs) return false;
186147
186522
  if (nowMs - c.boardMtime < quietMs) return false;
186148
- if (c.hasOpenWatchLetter) return false;
186149
186523
  if (c.lastWatchMtime !== null && c.boardMtime <= c.lastWatchMtime) return false;
186150
186524
  return true;
186151
186525
  }
186152
- var MARKER_PREFIX, boardWatchMarker, parseMarker, BoardWatcher;
186526
+ var MARKER_PREFIX, boardWatchMarker, parseMarker, RUN_MARKER_PREFIX, boardWatchRunMarker, parseRunMarker, TERMINAL_RUN_STATUS, BoardWatcher;
186153
186527
  var init_board_watch = __esm({
186154
186528
  "../server/src/design/board-watch.ts"() {
186155
186529
  "use strict";
@@ -186162,6 +186536,17 @@ var init_board_watch = __esm({
186162
186536
  const n = Number.parseInt(body.slice(i + MARKER_PREFIX.length), 10);
186163
186537
  return Number.isFinite(n) ? n : null;
186164
186538
  };
186539
+ RUN_MARKER_PREFIX = "[od-board-watch run:";
186540
+ boardWatchRunMarker = (runId) => `${RUN_MARKER_PREFIX}${runId}]`;
186541
+ parseRunMarker = (body) => {
186542
+ const i = body.indexOf(RUN_MARKER_PREFIX);
186543
+ if (i < 0) return null;
186544
+ const j = body.indexOf("]", i + RUN_MARKER_PREFIX.length);
186545
+ if (j < 0) return null;
186546
+ const id = body.slice(i + RUN_MARKER_PREFIX.length, j);
186547
+ return id.length > 0 ? id : null;
186548
+ };
186549
+ TERMINAL_RUN_STATUS = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled", "cancelled"]);
186165
186550
  BoardWatcher = class {
186166
186551
  constructor(deps) {
186167
186552
  this.deps = deps;
@@ -186178,6 +186563,22 @@ var init_board_watch = __esm({
186178
186563
  return null;
186179
186564
  }
186180
186565
  }
186566
+ /**
186567
+ * 查一块板的 run 列表(`GET /api/runs?projectId=`,与 CLI `design status` 同源)。
186568
+ * run 注册表是 od 的**内存态**——重启即空,查不到不代表没跑过;所以这里失败一律降级成空数组,
186569
+ * 由板文件 mtime 那条慢路径兜底,绝不因为查不到 run 就压掉写信。
186570
+ */
186571
+ async boardRuns(boardKey) {
186572
+ const fetchImpl = this.deps.fetchImpl ?? fetch;
186573
+ try {
186574
+ const res = await fetchImpl(`${this.deps.odBase}/api/runs?projectId=${encodeURIComponent(boardKey)}`);
186575
+ if (!res.ok) return [];
186576
+ const body = await res.json();
186577
+ return (body.runs ?? []).map((r) => ({ runId: String(r["runId"] ?? r["id"] ?? ""), status: String(r["status"] ?? "unknown") })).filter((r) => r.runId.length > 0);
186578
+ } catch {
186579
+ return [];
186580
+ }
186581
+ }
186181
186582
  async tick(now = Date.now()) {
186182
186583
  const { kernel, boardKeyOf, log: log3 } = this.deps;
186183
186584
  const quietMs = this.deps.quietMs ?? 10 * 6e4;
@@ -186194,9 +186595,17 @@ var init_board_watch = __esm({
186194
186595
  }
186195
186596
  const boardKey = boardKeyOf(a.id);
186196
186597
  const files = await this.boardFiles(boardKey);
186197
- if (!files || files.length === 0) continue;
186598
+ if (files === null) continue;
186599
+ const runs = await this.boardRuns(boardKey);
186600
+ if (files.length === 0 && runs.length === 0) continue;
186198
186601
  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);
186602
+ const watchLetters = annotationsOf(model, a.id).filter(
186603
+ (x2) => x2.author === actor && (parseMarker(x2.body) !== null || parseRunMarker(x2.body) !== null)
186604
+ );
186605
+ const letteredRuns = new Set(
186606
+ watchLetters.map((x2) => parseRunMarker(x2.body)).filter((s2) => s2 !== null)
186607
+ );
186608
+ const newestTerminalRun = runs.find((r) => TERMINAL_RUN_STATUS.has(r.status)) ?? null;
186200
186609
  const lastOpAt = model.lastOpAt.get(a.id);
186201
186610
  const candidate = {
186202
186611
  boardMtime,
@@ -186206,21 +186615,41 @@ var init_board_watch = __esm({
186206
186615
  lastWatchMtime: watchLetters.reduce((mx, x2) => {
186207
186616
  const m2 = parseMarker(x2.body);
186208
186617
  return m2 !== null && (mx === null || m2 > mx) ? m2 : mx;
186209
- }, null)
186618
+ }, null),
186619
+ freshTerminalRunId: newestTerminalRun !== null && !letteredRuns.has(newestTerminalRun.runId) ? newestTerminalRun.runId : null
186210
186620
  };
186211
186621
  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");
186622
+ 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");
186623
+ let body;
186624
+ let logLine;
186625
+ if (candidate.freshTerminalRunId !== null) {
186626
+ const status = newestTerminalRun.status;
186627
+ const ok = status === "succeeded";
186628
+ 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";
186629
+ body = [
186630
+ // 两个 marker 都带:run marker 管快路径去重,mtime marker 顺手把慢路径也盖住——
186631
+ // 否则这封信被 resolve 后,慢路径会就**同一批板内容**再写一封(两路去重各管各的)。
186632
+ boardMtime !== null ? `${boardWatchRunMarker(candidate.freshTerminalRunId)} ${boardWatchMarker(boardMtime)}` : boardWatchRunMarker(candidate.freshTerminalRunId),
186633
+ 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`,
186634
+ listed,
186635
+ ``,
186636
+ 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`
186637
+ ].join("\n");
186638
+ 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`;
186639
+ } else {
186640
+ const newest = files.filter((f2) => f2.mtime !== null && candidate.lastOpAtMs !== null && f2.mtime > candidate.lastOpAtMs).sort((x2, y) => (y.mtime ?? 0) - (x2.mtime ?? 0));
186641
+ body = [
186642
+ boardWatchMarker(boardMtime),
186643
+ `\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`,
186644
+ fmt(newest.length > 0 ? newest : files),
186645
+ ``,
186646
+ `\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`
186647
+ ].join("\n");
186648
+ logLine = `[board-watch] ${a.id} \u2190 \u677F ${boardKey} \u6709\u65B0\u5185\u5BB9\uFF08mtime=${new Date(boardMtime).toISOString()}\uFF09\u2192 \u5DF2\u5199\u4FE1\u5524 owner`;
186649
+ }
186221
186650
  await kernel.annotate({ artifactId: a.id, actor, body, kind: "comment" });
186222
186651
  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`);
186652
+ log3?.(logLine);
186224
186653
  }
186225
186654
  }
186226
186655
  };
@@ -192761,12 +193190,12 @@ function makeResumeRetryProxy(first, spawnFallback) {
192761
193190
  s2.onTelemetry?.((e) => fanTel(e));
192762
193191
  };
192763
193192
  wire(first);
192764
- const isSessionNotFound = (err) => {
193193
+ const isSessionNotFound2 = (err) => {
192765
193194
  const chatExit = err instanceof Error ? err.chatExit : void 0;
192766
193195
  return chatExit?.reason === "session-not-found";
192767
193196
  };
192768
193197
  const done = first.done.catch(async (err) => {
192769
- if (sawOutput || !isSessionNotFound(err)) throw err;
193198
+ if (sawOutput || !isSessionNotFound2(err)) throw err;
192770
193199
  const second = await spawnFallback();
192771
193200
  active = second;
192772
193201
  wire(second);
@@ -193440,6 +193869,16 @@ async function startServe(opts) {
193440
193869
  ...liveSchemaMap.get(artifact.type)?.ownerRole ? { assignedRole: liveSchemaMap.get(artifact.type).ownerRole } : {},
193441
193870
  dependsOn: artifact.inputs.map((edge) => edge.to)
193442
193871
  }));
193872
+ },
193873
+ // 业务交接的展示料源(见 CoordinatorNodeView.conclusion):每个节点最近一版**已收口** Work 的
193874
+ // conclusion。纯读、纯可选——读不到(无引擎 store / 工单不在库 / 任何异常)就返回空,
193875
+ // 协调者视图照常工作。
193876
+ resolveNodeConclusions: async (workOrderId, companyId) => {
193877
+ const companyEngine = await getEngine(companyId ?? defaultCompanyId);
193878
+ const store = companyEngine.kernel.getStore?.();
193879
+ if (!store) return {};
193880
+ const snap = await store.transaction((tx) => tx.loadWorkorder(workOrderId));
193881
+ return snap ? nodeConclusionsFromSnapshot(snap) : {};
193443
193882
  }
193444
193883
  });
193445
193884
  console.log(`[serve] execution-continuity mode=${continuityMode}\uFF08record=\u5BF9\u7167\u7EC4\uFF0Cresume=\u6062\u590D\u5305\u5B9E\u9A8C\u7EC4\uFF09`);
@@ -193585,7 +194024,7 @@ async function startServe(opts) {
193585
194024
  const selector = typeof raw === "string" && raw ? raw : void 0;
193586
194025
  const me = await companies.currentUser({ actor }, headers);
193587
194026
  const ctx = await companies.service.resolveCurrentCompany(me.accountId, selector);
193588
- if (!ctx) return { kind: "not-found" };
194027
+ if (!ctx) return { kind: "not-found", ...selector !== void 0 ? { selector } : {} };
193589
194028
  return ctx.isMember ? { kind: "ok", companyId: ctx.company.id } : { kind: "forbidden" };
193590
194029
  };
193591
194030
  const resolveActor = (token) => issuer.resolve(token) ?? (opts.allowSimpleTokens && token.startsWith("token:") ? token.slice("token:".length) : null);
@@ -193913,7 +194352,8 @@ async function startServe(opts) {
193913
194352
  const artifactId = `artifact:planner:${(0, import_node_crypto41.randomUUID)()}`;
193914
194353
  const handle = await chatRemoteAdapter.spawn({
193915
194354
  actor: planner.id,
193916
- actorToken: issueSessionToken(planner.id, { artifactId, action: "plan-workorder", limits, binding }),
194355
+ // companyId 同协调者:planner 用的就是默认公司的 actors 服务,漏签会让它一调公司域端点就 404。
194356
+ actorToken: issueSessionToken(planner.id, { artifactId, action: "plan-workorder", limits, binding, companyId: defaultCompanyId }),
193917
194357
  artifactId,
193918
194358
  bundle: { files },
193919
194359
  ...actorCtx?.config?.prompt ? { systemPrompt: actorCtx.config.prompt } : {},
@@ -194926,7 +195366,7 @@ async function startServe(opts) {
194926
195366
  if (engStore) void rebuildDispatchMappings(engStore);
194927
195367
  }
194928
195368
  newKernel.setEngineIO({
194929
- async dispatchWork(workId) {
195369
+ async dispatchWork(workId, opts2) {
194930
195370
  if (!newEngineProduce) return;
194931
195371
  if (dispatchedWorks.has(workId)) return;
194932
195372
  const kernel2 = newKernel;
@@ -194955,7 +195395,7 @@ async function startServe(opts) {
194955
195395
  }
194956
195396
  let result;
194957
195397
  try {
194958
- result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, workId, ledgerId);
195398
+ result = replyIssueId && newEngineReplyWork ? await newEngineReplyWork(rev.artifactId, rev.author, replyIssueId, ledgerId) : await newEngineProduce(rev.artifactId, rev.author, workId, ledgerId, opts2?.resumeEligible);
194959
195399
  } catch (err) {
194960
195400
  dispatchLedger.close(ledgerId, { outcome: "failed", reason: `dispatch error: ${err instanceof Error ? err.message : String(err)}` }).catch(ledgerWarn("close(error)"));
194961
195401
  console.log(`[new-engine] dispatchWork ${workId} \u6D3E\u53D1\u5F02\u5E38\uFF1A${String(err)} \u2192 \u7F6E work failed`);
@@ -195375,14 +195815,16 @@ async function startServe(opts) {
195375
195815
  });
195376
195816
  const slot = { dispatcher: d, logged: 0, ticking: false };
195377
195817
  dispatchers.set(companyId, slot);
195378
- newEngineProduce = async (artifactId, actorId, workId, dispatchId) => {
195818
+ newEngineProduce = async (artifactId, actorId, workId, dispatchId, resumeEligible) => {
195379
195819
  const result = await d.requestProduce({
195380
195820
  artifactId,
195381
195821
  actor: actorId,
195382
195822
  bypassScheduling: true,
195383
195823
  engineWorkId: workId,
195384
195824
  engineCompanyId: companyId,
195385
- ...dispatchId !== void 0 ? { dispatchId } : {}
195825
+ ...dispatchId !== void 0 ? { dispatchId } : {},
195826
+ // 工单 e4a43a42:返工(已收口节点被重激活)→ resumeEligible=false → 派发层起全新会话。
195827
+ ...resumeEligible !== void 0 ? { resumeEligible } : {}
195386
195828
  });
195387
195829
  return { kind: result.kind, dispatchId: result.kind === "dispatched" ? result.dispatchId : void 0 };
195388
195830
  };
@@ -195876,6 +196318,12 @@ ${st.detail}
195876
196318
  artifactId: ctx?.artifactId ?? "artifact:coordinator",
195877
196319
  action: ctx?.action ?? "coordinate",
195878
196320
  limits: ctx?.limits ?? { wallClockMs: wallClockForKind("coordinate") },
196321
+ // CO-101 门禁要这个 claim 才认得出「这次请求属于哪家公司」。漏签的后果不是报错这么简单:
196322
+ // 管理者的 roles / actors / workorders / inbox / session-log 会**全部** 404,而 status / ls /
196323
+ // graph / content 照常(热路径回退默认公司),于是它只瞎一半——够它误诊成「平台名册服务挂了」,
196324
+ // 再上报给人;未关的上报又会关掉自动重唤,把本可自愈的节点永久悬挂。2026-08-14 至少 3 张单
196325
+ // 栽在这上面。协调者与 apply 同用默认公司 kernel(见下方 structuralBaselines 注释),故取默认公司。
196326
+ companyId: defaultCompanyId,
195879
196327
  ...ctx?.binding !== void 0 ? { binding: ctx.binding } : {}
195880
196328
  });
195881
196329
  const ws = ctx?.artifactId ? kernel.model.artifacts.get(ctx.artifactId)?.workspace : void 0;
@@ -196846,6 +197294,33 @@ function reapClaudeProjects(workdir, runtimeKind) {
196846
197294
  } catch {
196847
197295
  }
196848
197296
  }
197297
+ function measureClaudeProjects(workdir, runtimeKind) {
197298
+ if (runtimeKind !== "claude" && runtimeKind !== "claude-code") return 0;
197299
+ const dir = import_node_path24.default.join(claudeProjectsRoot(), claudeProjectSlug(workdir));
197300
+ return dirSizeBytes2(dir);
197301
+ }
197302
+ function dirSizeBytes2(dir) {
197303
+ let total = 0;
197304
+ let entries;
197305
+ try {
197306
+ entries = import_node_fs20.default.readdirSync(dir, { withFileTypes: true });
197307
+ } catch {
197308
+ return 0;
197309
+ }
197310
+ for (const e of entries) {
197311
+ const p2 = import_node_path24.default.join(dir, e.name);
197312
+ if (e.isSymbolicLink()) continue;
197313
+ if (e.isDirectory()) {
197314
+ total += dirSizeBytes2(p2);
197315
+ continue;
197316
+ }
197317
+ try {
197318
+ total += import_node_fs20.default.statSync(p2).size;
197319
+ } catch {
197320
+ }
197321
+ }
197322
+ return total;
197323
+ }
196849
197324
 
196850
197325
  // ../cli/src/node.ts
196851
197326
  init_src6();
@@ -196928,6 +197403,8 @@ async function startNode(opts) {
196928
197403
  ...process.env["OASIS_GC_INTERVAL_MS"] ? { intervalMs: Number(process.env["OASIS_GC_INTERVAL_MS"]) } : {},
196929
197404
  queryArtifactStates: (ids2) => client.queryArtifactStates(ids2),
196930
197405
  reapRuntimeState: reapClaudeProjects,
197406
+ // 工单 e4a43a42:把连带的 claude 会话记录占用一并量进保留预算(比工作目录还大的那一坨)。
197407
+ measureRuntimeState: measureClaudeProjects,
196931
197408
  log: (m2) => console.log(`[oasis node] ${m2}`)
196932
197409
  });
196933
197410
  if (opts.gcEnabled !== false) console.log(`[oasis node] \u5DE5\u4F5C\u533A\u56DE\u6536\u5668\uFF1A\u5DF2\u542F\u7528\uFF08\u6839 ${workRoot}\uFF09`);
@@ -201302,7 +201779,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
201302
201779
  }
201303
201780
 
201304
201781
  // src/index.ts
201305
- var PKG_VERSION = true ? "0.1.102" : "dev";
201782
+ var PKG_VERSION = true ? "0.1.103" : "dev";
201306
201783
  var OASIS_DIR = path26.join(os10.homedir(), ".oasis");
201307
201784
  var CONFIG_FILE = path26.join(OASIS_DIR, "node-config.json");
201308
201785
  var PID_FILE = path26.join(OASIS_DIR, "node.pid");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"