oasis_test 0.1.53 → 0.1.56

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 +747 -78
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ function emptyReadModel() {
46
46
  annotations: /* @__PURE__ */ new Map(),
47
47
  annotationSeq: /* @__PURE__ */ new Map(),
48
48
  seals: /* @__PURE__ */ new Map(),
49
+ holds: /* @__PURE__ */ new Map(),
49
50
  lastOpAt: /* @__PURE__ */ new Map(),
50
51
  pinMeta: /* @__PURE__ */ new Map(),
51
52
  gaps: /* @__PURE__ */ new Map(),
@@ -316,13 +317,28 @@ function fold(model, op) {
316
317
  gap.resolved = true;
317
318
  gap.resolvedAtSeq = op.seq;
318
319
  if (p2.note !== void 0) gap.note = p2.note;
319
- for (const e of model.escalations.get(op.artifactId) ?? []) e.resolved = true;
320
+ for (const e of model.escalations.get(op.artifactId) ?? []) {
321
+ if (e.resolved) continue;
322
+ if (e.gapId !== void 0) {
323
+ if (e.gapId === p2.gapId) e.resolved = true;
324
+ } else if (e.part === gap.part) {
325
+ e.resolved = true;
326
+ }
327
+ }
320
328
  break;
321
329
  }
322
330
  case "escalate": {
323
331
  const p2 = op.payload;
324
332
  const list = model.escalations.get(op.artifactId) ?? [];
325
- list.push({ part: p2.part ?? null, reason: p2.reason, by: op.actor, ...op.hand !== void 0 ? { hand: op.hand } : {}, at: op.timestamp, resolved: false });
333
+ list.push({
334
+ part: p2.part ?? null,
335
+ reason: p2.reason,
336
+ by: op.actor,
337
+ ...op.hand !== void 0 ? { hand: op.hand } : {},
338
+ at: op.timestamp,
339
+ resolved: false,
340
+ ...p2.gapId !== void 0 ? { gapId: p2.gapId } : {}
341
+ });
326
342
  model.escalations.set(op.artifactId, list);
327
343
  break;
328
344
  }
@@ -418,6 +434,21 @@ function fold(model, op) {
418
434
  model.seals.delete(op.artifactId);
419
435
  break;
420
436
  }
437
+ case "hold": {
438
+ const p2 = op.payload;
439
+ if (!model.artifacts.has(op.artifactId)) throw new CorruptLogError(op, "hold on missing artifact");
440
+ model.holds.set(op.artifactId, {
441
+ source: p2.source,
442
+ by: op.actor,
443
+ at: op.timestamp,
444
+ ...p2.reason !== void 0 ? { reason: p2.reason } : {}
445
+ });
446
+ break;
447
+ }
448
+ case "release": {
449
+ model.holds.delete(op.artifactId);
450
+ break;
451
+ }
421
452
  case "unlink_input": {
422
453
  const p2 = op.payload;
423
454
  const artifact = model.artifacts.get(op.artifactId);
@@ -499,6 +530,12 @@ function lifecycleOf(model, id) {
499
530
  const seal = model.seals.get(id);
500
531
  return seal ? `sealed:${seal.reason}` : "active";
501
532
  }
533
+ function isHeld(model, id) {
534
+ return model.holds.has(id);
535
+ }
536
+ function holdOf(model, id) {
537
+ return model.holds.get(id) ?? null;
538
+ }
502
539
  function annotationsOf(model, id) {
503
540
  const revisionIds = new Set(revisionsOf(model, id).map((r) => r.id));
504
541
  return [...model.annotations.values()].filter(
@@ -565,7 +602,14 @@ function findGap(model, gapId) {
565
602
  return null;
566
603
  }
567
604
  function unresolvedEscalationsOf(model, id) {
568
- return (model.escalations.get(id) ?? []).filter((e) => !e.resolved).map((e) => ({ part: e.part, reason: e.reason, by: e.by, ...e.hand !== void 0 ? { hand: e.hand } : {}, at: e.at }));
605
+ return (model.escalations.get(id) ?? []).filter((e) => !e.resolved).map((e) => ({
606
+ part: e.part,
607
+ reason: e.reason,
608
+ by: e.by,
609
+ ...e.hand !== void 0 ? { hand: e.hand } : {},
610
+ at: e.at,
611
+ ...e.gapId !== void 0 ? { gapId: e.gapId } : {}
612
+ }));
569
613
  }
570
614
  function listPendingReviews(model, workspace) {
571
615
  const all = [...model.pendingReviews.values()];
@@ -606,6 +650,16 @@ function stuckDiagnosis(model, id, opts) {
606
650
  if (!artifact || lifecycle !== "active") {
607
651
  return { id, lifecycle, staleMs, causes, needsCoordinator: false };
608
652
  }
653
+ if (isHeld(model, id)) {
654
+ const h = holdOf(model, id);
655
+ causes.push({
656
+ category: "held",
657
+ source: "oplog",
658
+ detail: `\u8282\u70B9\u5DF2\u6682\u505C\u6D3E\u53D1\uFF08${h?.source ?? "?"}${h?.reason ? `\uFF1A${h.reason}` : ""}\uFF09\u2014\u2014\u5F85\u89E3\u963B`,
659
+ handler: "human"
660
+ });
661
+ return { id, lifecycle, staleMs, causes, needsCoordinator: false };
662
+ }
609
663
  if (artifact.owner.startsWith("pending:role:")) {
610
664
  causes.push({
611
665
  category: "owner-pending",
@@ -2454,19 +2508,28 @@ var init_kernel = __esm({
2454
2508
  */
2455
2509
  async escalate(args) {
2456
2510
  this.artifactOrThrow(args.artifactId);
2457
- if (!isHumanActor(args.actor)) {
2511
+ if (!isHumanActor(args.actor) && args.actor !== SYSTEM_ACTOR) {
2458
2512
  const ws = this.model.artifacts.get(args.artifactId)?.workspace;
2459
2513
  const brief = [...this.model.artifacts.values()].find((a) => a.type === "brief" && a.workspace === ws);
2460
2514
  const manager = brief?.fields?.["manager"];
2461
2515
  if (manager !== args.actor) {
2462
2516
  throw new KernelError(
2463
- `escalate \u4EC5\u9650\u672C\u5DE5\u5355\u7BA1\u7406\u8005\uFF08brief.fields.manager\uFF09\u6216\u4EBA\u7C7B\u8C03\u7528\u2014\u2014${args.actor} \u4E0D\u662F\u3002\u4F60\u82E5\u662F producer/owner \u5361\u4F4F\u4E86\uFF1A\u7528 \`oasis gap\` \u4E0A\u62A5\u9700\u8981\uFF0C\u7531\u7BA1\u7406\u8005\u5206\u8BCA\uFF08\u51B3\u7B56 0030 D4\uFF09\u3002`
2517
+ `escalate \u4EC5\u9650\u672C\u5DE5\u5355\u7BA1\u7406\u8005\uFF08brief.fields.manager\uFF09\u3001\u4EBA\u7C7B\u6216\u7CFB\u7EDF\u8C03\u7528\u2014\u2014${args.actor} \u4E0D\u662F\u3002\u4F60\u82E5\u662F producer/owner \u5361\u4F4F\u4E86\uFF1A\u7528 \`oasis gap\` \u4E0A\u62A5\u9700\u8981\uFF0C\u7531\u7BA1\u7406\u8005\u5206\u8BCA\uFF08\u51B3\u7B56 0030 D4\uFF09\u3002`
2464
2518
  );
2465
2519
  }
2466
2520
  }
2521
+ let part = args.part;
2522
+ let gapId = args.gapId;
2523
+ if (gapId) {
2524
+ const found = findGap(this.model, gapId);
2525
+ if (found && found.artifactId === args.artifactId) {
2526
+ if (part === void 0 && found.gap.part) part = found.gap.part;
2527
+ }
2528
+ }
2467
2529
  await this.commit(args.artifactId, args.actor, "escalate", args.artifactId, {
2468
2530
  reason: args.reason,
2469
- ...args.part !== void 0 ? { part: args.part } : {}
2531
+ ...part !== void 0 ? { part } : {},
2532
+ ...gapId !== void 0 ? { gapId } : {}
2470
2533
  }, void 0, args.hand);
2471
2534
  }
2472
2535
  /** 决策 0027 D1:人显式关闭本 artifact 上的 escalate(off-graph 解决兜底)。 */
@@ -2619,6 +2682,43 @@ var init_kernel = __esm({
2619
2682
  const payload = { persistence: "persistent" };
2620
2683
  await this.commit(args.artifactId, args.actor, "promote", args.artifactId, payload);
2621
2684
  }
2685
+ /**
2686
+ * 节点级派发暂停(node-hold,本批):落一条 hold op——**只挡 agent 派发、不锁内容**(≠ seal/freeze,
2687
+ * 见 docs/notes/artifact-seal-lifecycle.md)。用于自动升级冻结(source:"sla")与人/管理者显式暂停。
2688
+ * 不校验 lifecycle:sealed 节点本就不派、held 于它无意义但无害;主用途是冻 active 节点。幂等——重复 hold 覆盖来源。
2689
+ */
2690
+ async hold(args) {
2691
+ this.artifactOrThrow(args.artifactId);
2692
+ this.assertHoldAdmission(args.artifactId, args.actor);
2693
+ const payload = {
2694
+ source: args.source,
2695
+ ...args.reason !== void 0 ? { reason: args.reason } : {}
2696
+ };
2697
+ await this.commit(args.artifactId, args.actor, "hold", args.artifactId, payload);
2698
+ }
2699
+ /** 解阻(node-hold):撤销 hold,节点恢复可派(投影下一拍重推当下欠的活)。空解(未 held)拦下,避免日志噪声。 */
2700
+ async release(args) {
2701
+ this.artifactOrThrow(args.artifactId);
2702
+ this.assertHoldAdmission(args.artifactId, args.actor);
2703
+ if (!isHeld(this.model, args.artifactId)) throw new KernelError(`not held: ${args.artifactId}`);
2704
+ const payload = { ...args.reason !== void 0 ? { reason: args.reason } : {} };
2705
+ await this.commit(args.artifactId, args.actor, "release", args.artifactId, payload);
2706
+ }
2707
+ /**
2708
+ * hold/release 准入闸(红线 6:管控在服务端收紧):人类 / 本工单管理者 / `actor:system`(自动升级)可调;
2709
+ * 普通 producer/owner agent 不可——防 agent 把自己节点暂停了逃避派发(同 escalate 的机制闸精神,kernel.escalate)。
2710
+ */
2711
+ assertHoldAdmission(artifactId, actor) {
2712
+ if (isHumanActor(actor)) return;
2713
+ if (actor === SYSTEM_ACTOR) return;
2714
+ const ws = this.model.artifacts.get(artifactId)?.workspace;
2715
+ const brief = [...this.model.artifacts.values()].find((a) => a.type === "brief" && a.workspace === ws);
2716
+ const manager = brief?.fields?.["manager"];
2717
+ if (manager === actor) return;
2718
+ throw new KernelError(
2719
+ `hold/release \u4EC5\u9650\u4EBA\u7C7B\u3001\u672C\u5DE5\u5355\u7BA1\u7406\u8005\uFF08brief.fields.manager\uFF09\u6216\u7CFB\u7EDF\u8C03\u7528\u2014\u2014${actor} \u4E0D\u662F\u3002`
2720
+ );
2721
+ }
2622
2722
  // ── 治理 op(§11.3):force/override 单点豁免可直发;结构 op 以 Intervention 为载体 ──────
2623
2723
  /** 治理解封:sealed → active(lifecycle = 最后一条 seal/reopen,§4.4)。frozen 解冻、cancelled 复活都走它 */
2624
2724
  async reopen(args) {
@@ -3914,7 +4014,10 @@ function computeReviewJobs(model) {
3914
4014
  }
3915
4015
  return out;
3916
4016
  }
3917
- var import_node_crypto2, DEFAULT_PREDISPATCH_TIMEOUT_MS, SpawnAttemptStale, SEED_ROOT_SUCCESSFUL_PRODUCE_LIMIT, CANVAS_RUNTIME_KIND, NON_PRODUCE_FALLBACK_RUNTIME_KIND, Dispatcher;
4017
+ function isSelfHealingExit(reason) {
4018
+ return reason !== void 0 && SELF_HEALING_EXITS.has(reason);
4019
+ }
4020
+ var import_node_crypto2, DEFAULT_PREDISPATCH_TIMEOUT_MS, SpawnAttemptStale, SEED_ROOT_SUCCESSFUL_PRODUCE_LIMIT, SELF_HEALING_EXITS, CANVAS_RUNTIME_KIND, NON_PRODUCE_FALLBACK_RUNTIME_KIND, Dispatcher;
3918
4021
  var init_dispatcher = __esm({
3919
4022
  "../core/src/dispatcher.ts"() {
3920
4023
  "use strict";
@@ -3932,6 +4035,7 @@ var init_dispatcher = __esm({
3932
4035
  }
3933
4036
  };
3934
4037
  SEED_ROOT_SUCCESSFUL_PRODUCE_LIMIT = 5;
4038
+ SELF_HEALING_EXITS = /* @__PURE__ */ new Set(["rate-limit"]);
3935
4039
  CANVAS_RUNTIME_KIND = "open-design";
3936
4040
  NON_PRODUCE_FALLBACK_RUNTIME_KIND = "claude-code";
3937
4041
  Dispatcher = class {
@@ -3976,6 +4080,20 @@ var init_dispatcher = __esm({
3976
4080
  fusedAt: st.fusedAt
3977
4081
  }));
3978
4082
  }
4083
+ /**
4084
+ * 长期饥饿的自愈类 job(rate-limit 持续退避超 starvationMs 仍未恢复):自愈类从不熔断、进不了 fusedJobs,
4085
+ * 若配额**长期**不足就成了静默卡死——本访问器供 SLA 扫描把它升级为**算力缺口**(提案"长期不足才=算力缺口")。
4086
+ */
4087
+ starvingJobs(starvationMs, now) {
4088
+ const out = [];
4089
+ for (const [jobKey, st] of this.strikes) {
4090
+ if (st.fused || !isSelfHealingExit(st.lastReason) || !st.firstFailedAt) continue;
4091
+ const sinceMs = now - Date.parse(st.firstFailedAt);
4092
+ if (sinceMs <= starvationMs) continue;
4093
+ out.push({ jobKey, artifactId: st.artifactId, actor: st.actor, action: st.action, consecutiveFailures: st.count, lastReason: st.lastReason, sinceMs });
4094
+ }
4095
+ return out;
4096
+ }
3979
4097
  /** v1 调度准入:当前被并发上限挡下、等待重评的 produce 会话数(/api/view dispatches 表面化)。 */
3980
4098
  queuedCount() {
3981
4099
  return this.queued.size;
@@ -4215,19 +4333,22 @@ var init_dispatcher = __esm({
4215
4333
  }
4216
4334
  recordSpawnFailure(jobKey, spec, reason) {
4217
4335
  const seqAtVerdict = this.opts.kernel.model.lastSeq.get(spec.artifactId) ?? 0;
4218
- const consecutive = (this.strikes.get(jobKey)?.count ?? 0) + 1;
4336
+ const prev = this.strikes.get(jobKey);
4337
+ const consecutive = (prev?.count ?? 0) + 1;
4219
4338
  const max = this.opts.backoff?.maxConsecutiveFailures ?? 3;
4220
4339
  const base = this.opts.backoff?.baseMs ?? 3e4;
4221
- const fused = consecutive >= max;
4340
+ const cap = this.opts.backoff?.maxBackoffMs ?? 30 * 6e4;
4341
+ const fused = consecutive >= max && !isSelfHealingExit(reason);
4222
4342
  this.strikes.set(jobKey, {
4223
4343
  artifactId: spec.artifactId,
4224
4344
  actor: spec.actor,
4225
4345
  action: spec.action,
4226
4346
  count: consecutive,
4227
- nextEligibleAt: Date.now() + base * 2 ** (consecutive - 1),
4347
+ nextEligibleAt: Date.now() + Math.min(base * 2 ** (consecutive - 1), cap),
4228
4348
  fused,
4229
4349
  ...fused ? { fusedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
4230
4350
  seqAtVerdict,
4351
+ firstFailedAt: prev?.firstFailedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4231
4352
  lastReason: reason
4232
4353
  });
4233
4354
  if (fused) {
@@ -4269,6 +4390,7 @@ var init_dispatcher = __esm({
4269
4390
  const { kernel } = this.opts;
4270
4391
  const jobWorkspace = kernel.model.artifacts.get(spec.artifactId)?.workspace;
4271
4392
  if (jobWorkspace && await this.opts.resolveDispatchHold?.(jobWorkspace)) return 0;
4393
+ if (isHeld(kernel.model, spec.artifactId)) return 0;
4272
4394
  this.assertSpawnAttemptCurrent(jobKey, attempt);
4273
4395
  const seqNow = kernel.model.lastSeq.get(spec.artifactId) ?? 0;
4274
4396
  const strike = this.strikes.get(jobKey);
@@ -4300,7 +4422,7 @@ var init_dispatcher = __esm({
4300
4422
  }
4301
4423
  }
4302
4424
  const retry = this.strikes.get(jobKey);
4303
- const lastFailure = retry && retry.count > 0 && retry.lastReason ? { reason: retry.lastReason } : void 0;
4425
+ const lastFailure = retry && retry.count > 0 && retry.lastReason && !isSelfHealingExit(retry.lastReason) ? { reason: retry.lastReason } : void 0;
4304
4426
  const ws = kernel.model.artifacts.get(spec.artifactId)?.workspace;
4305
4427
  const codeRepo = ws ? await this.opts.resolveCodeRepo?.(ws) ?? null : null;
4306
4428
  this.assertSpawnAttemptCurrent(jobKey, attempt);
@@ -4487,19 +4609,23 @@ var init_dispatcher = __esm({
4487
4609
  if (progressed) {
4488
4610
  this.strikes.delete(jobKey);
4489
4611
  } else {
4490
- consecutive = (this.strikes.get(jobKey)?.count ?? 0) + 1;
4612
+ const prev = this.strikes.get(jobKey);
4613
+ consecutive = (prev?.count ?? 0) + 1;
4491
4614
  const max = this.opts.backoff?.maxConsecutiveFailures ?? 3;
4492
4615
  const base = this.opts.backoff?.baseMs ?? 3e4;
4493
- const fused = consecutive >= max;
4616
+ const cap = this.opts.backoff?.maxBackoffMs ?? 30 * 6e4;
4617
+ const fused = consecutive >= max && !isSelfHealingExit(info.reason);
4494
4618
  this.strikes.set(jobKey, {
4495
4619
  artifactId: spec.artifactId,
4496
4620
  actor: spec.actor,
4497
4621
  action: spec.action,
4498
4622
  count: consecutive,
4499
- nextEligibleAt: Date.now() + base * 2 ** (consecutive - 1),
4623
+ // 退避封顶(maxBackoffMs,默认 30min):自愈类不熔断→consecutive 可无界增长,不封顶则间隔爆炸。
4624
+ nextEligibleAt: Date.now() + Math.min(base * 2 ** (consecutive - 1), cap),
4500
4625
  fused,
4501
4626
  ...fused ? { fusedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
4502
4627
  seqAtVerdict: seqAtExit,
4628
+ firstFailedAt: prev?.firstFailedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4503
4629
  ...info.reason ? { lastReason: info.reason } : {}
4504
4630
  });
4505
4631
  if (fused) {
@@ -22353,6 +22479,82 @@ var init_node_state = __esm({
22353
22479
  }
22354
22480
  });
22355
22481
 
22482
+ // ../server/src/domains/collab/stall.ts
22483
+ function shortId(id) {
22484
+ return id.length > 22 ? `${id.slice(0, 14)}\u2026` : id;
22485
+ }
22486
+ function stallOf(model, id, runtime) {
22487
+ const a = model.artifacts.get(id);
22488
+ if (!a) return null;
22489
+ const ownerIsAgent = a.owner.startsWith("actor:agent:");
22490
+ if (runtime?.status === "running") {
22491
+ return { kind: "running", detail: `${runtime.action ?? "produce"} \u5728\u8DD1`, ...runtime.startedAt ? { since: runtime.startedAt } : {} };
22492
+ }
22493
+ if (isHeld(model, id)) {
22494
+ const h = holdOf(model, id);
22495
+ return { kind: "held", action: "resume", detail: `\u5DF2\u6682\u505C\u6D3E\u53D1${h?.reason ? `\uFF1A${h.reason}` : ""}\u2014\u2014\u5F85\u6062\u590D`, ...h?.at ? { since: h.at } : {} };
22496
+ }
22497
+ if (runtime && (runtime.status === "failed" || runtime.status === "fused" || runtime.status === "escalated")) {
22498
+ const label = runtime.status === "escalated" ? "\u5DF2\u5347\u7EA7\u3001\u5F85\u4EBA\u4ECB\u5165" : runtime.status === "fused" ? "\u8FDE\u8D25\u7194\u65AD\u3001\u505C\u6D3E\u5F85\u5904\u7406" : "\u8FD0\u884C\u51FA\u9519";
22499
+ return {
22500
+ kind: "failed",
22501
+ detail: `${label}${runtime.reason ? `\uFF08${runtime.reason}\uFF09` : ""}`,
22502
+ ...runtime.reason ? { reason: runtime.reason } : {},
22503
+ ...runtime.finishedAt ? { since: runtime.finishedAt } : {}
22504
+ };
22505
+ }
22506
+ const head = queueOf(model, id)[0];
22507
+ if (head) {
22508
+ const authors = declaredHumanAuthorsOf(model, head);
22509
+ if (authors.size > 0) {
22510
+ const approved = new Set((model.reviews.get(head.id) ?? []).filter((v2) => v2.verdict === "approve").map((v2) => v2.author));
22511
+ const missing = [...authors].filter((x2) => !approved.has(x2));
22512
+ if (missing.length > 0) {
22513
+ return {
22514
+ kind: "waiting_human",
22515
+ action: "accept",
22516
+ actorId: missing[0],
22517
+ ref: head.id,
22518
+ detail: `\u7B49 ${shortId(missing[0])} \u9A8C\u6536 ${shortId(head.id)} \u7248${missing.length > 1 ? `\uFF08\u5171 ${missing.length} \u4EBA\uFF09` : ""}`,
22519
+ since: head.createdAt
22520
+ };
22521
+ }
22522
+ }
22523
+ }
22524
+ const gate = pendingConcludeAttempt(model, id);
22525
+ if (gate?.gate) {
22526
+ const votedBy = new Set((model.reviews.get(gate.head) ?? []).map((v2) => v2.author));
22527
+ const humanEligible = gate.gate.eligible.filter((r) => r.startsWith("actor:human:") && !votedBy.has(r));
22528
+ if (humanEligible.length > 0) {
22529
+ return { kind: "waiting_human", action: "review", actorId: humanEligible[0], detail: `\u7B49 ${shortId(humanEligible[0])} \u5BA1\u6279\uFF08quorum=${gate.gate.quorum}\uFF09`, since: gate.at };
22530
+ }
22531
+ }
22532
+ if (hasOpenHumanChangeRequest(model, id) || headRejected(model, id)) {
22533
+ return ownerIsAgent ? { kind: "waiting_agent", action: "rework", actorId: a.owner, detail: "\u6709\u672A\u5904\u7406\u7684\u53D8\u66F4\u8BF7\u6C42\uFF0C\u5F85\u8FD4\u5DE5" } : { kind: "waiting_human", action: "rework", actorId: a.owner, detail: "\u6709\u672A\u5904\u7406\u7684\u53D8\u66F4\u8BF7\u6C42\uFF0C\u5F85\u4F60\u8FD4\u5DE5" };
22534
+ }
22535
+ const escs = unresolvedEscalationsOf(model, id);
22536
+ if (escs.length > 0) {
22537
+ return { kind: "waiting_human", action: "discuss", ref: id, detail: `\u5DF2\u4E0A\u62A5\u5F85\u4EBA\u4ECB\u5165\uFF1A${escs.at(-1).reason.slice(0, 60)}`, since: escs.at(-1).at };
22538
+ }
22539
+ const gaps = unresolvedGapsOf(model, id);
22540
+ if (gaps.length > 0) {
22541
+ return { kind: "waiting_human", action: "resolve_gap", detail: `\u5361\u5728\u7F3A\u53E3\u4E0A\u5F85\u4EBA\u8865\uFF1A${gaps[0].description.slice(0, 60)}` };
22542
+ }
22543
+ const b2 = blockedOf(model, id);
22544
+ if (b2.blocked) {
22545
+ const dep = a.inputs.find((e) => e.required && !isConcluded(model, e.to));
22546
+ if (dep) return { kind: "waiting_system", ref: dep.to, detail: `\u7B49\u4E0A\u6E38\u6536\u53E3\uFF1A${shortId(dep.to)}` };
22547
+ return { kind: "waiting_system", detail: `\u963B\u585E\u4E2D\uFF1A${b2.reasons[0] ?? "\u5F85\u524D\u7F6E\u5C31\u7EEA"}` };
22548
+ }
22549
+ return null;
22550
+ }
22551
+ var init_stall = __esm({
22552
+ "../server/src/domains/collab/stall.ts"() {
22553
+ "use strict";
22554
+ init_src2();
22555
+ }
22556
+ });
22557
+
22356
22558
  // ../server/src/coordinator/identity.ts
22357
22559
  function isCoordinatorAgentName(name) {
22358
22560
  return COORDINATOR_NAME_RE.test(name);
@@ -23004,6 +23206,11 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
23004
23206
  ...e.upstreamCancelled ? { upstreamCancelled: true } : {}
23005
23207
  })) : [];
23006
23208
  const pv = reviewerPreview?.(a.id);
23209
+ const stallRaw = stallOf(model, a.id, runtime);
23210
+ const stall = stallRaw ? (() => {
23211
+ const { actorId, ...rest } = stallRaw;
23212
+ return { ...rest, ...actorId ? { actor: ref(actorId) } : {} };
23213
+ })() : void 0;
23007
23214
  return {
23008
23215
  id: a.id,
23009
23216
  type: a.type,
@@ -23018,12 +23225,35 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
23018
23225
  ...runtime ? { runtime } : {},
23019
23226
  ...pv ? { reviewers: pv.reviewers.map((r) => ({ actor: ref(r.actor), source: r.source })), quorum: pv.quorum, typeName: pv.typeName } : {},
23020
23227
  // 过期事实(提案 rework-cascade-rollout):无条件透出,前端 DAG 挂「上游已更新」徽标。
23021
- ...outdated.length > 0 ? { outdated } : {}
23228
+ ...outdated.length > 0 ? { outdated } : {},
23229
+ // 停滞四态(看得清):为什么不动 + 谁欠什么 + 去处理锚;null 时不带。
23230
+ ...stall ? { stall } : {}
23022
23231
  };
23023
23232
  });
23024
23233
  const edges = arts.flatMap(
23025
23234
  (a) => a.inputs.map((e) => ({ from: a.id, to: e.to, pinned: e.pinned, required: e.required }))
23026
23235
  );
23236
+ const stallSummary = (() => {
23237
+ const counts = { running: 0, held: 0, failed: 0, waitingHuman: 0, waitingAgent: 0, waitingSystem: 0 };
23238
+ const sev = { failed: 5, held: 4, waiting_human: 3, waiting_agent: 2, waiting_system: 1, running: 0 };
23239
+ const stuck = [];
23240
+ for (const n of nodes) {
23241
+ const st = n.stall;
23242
+ if (!st) continue;
23243
+ if (st.kind === "running") counts.running++;
23244
+ else if (st.kind === "held") counts.held++;
23245
+ else if (st.kind === "failed") counts.failed++;
23246
+ else if (st.kind === "waiting_human") counts.waitingHuman++;
23247
+ else if (st.kind === "waiting_agent") counts.waitingAgent++;
23248
+ else counts.waitingSystem++;
23249
+ if (st.kind !== "running") {
23250
+ stuck.push({ id: n.id, label: n.label, kind: st.kind, ...st.action ? { action: st.action } : {}, ...st.ref ? { ref: st.ref } : {}, detail: st.detail, ...st.actor ? { actor: st.actor } : {} });
23251
+ }
23252
+ }
23253
+ stuck.sort((a, b2) => sev[b2.kind] - sev[a.kind]);
23254
+ const total = counts.running + counts.held + counts.failed + counts.waitingHuman + counts.waitingAgent + counts.waitingSystem;
23255
+ return total > 0 ? { counts, stuck } : void 0;
23256
+ })();
23027
23257
  const activity = buildActivity(model, arts, ref);
23028
23258
  const coordinatorActivity = buildCoordinatorActivity(arts, ops, ref);
23029
23259
  const acceptance = buildAcceptance(model, arts);
@@ -23039,6 +23269,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
23039
23269
  acceptance,
23040
23270
  ...managerActorId ? { managerActorId } : {},
23041
23271
  ...spec?.acceptanceCriteria ? { acceptanceCriteria: spec.acceptanceCriteria } : {},
23272
+ ...stallSummary ? { stallSummary } : {},
23042
23273
  planning
23043
23274
  };
23044
23275
  }
@@ -23334,6 +23565,7 @@ var init_workorder_detail = __esm({
23334
23565
  "use strict";
23335
23566
  init_src2();
23336
23567
  init_node_state();
23568
+ init_stall();
23337
23569
  init_workorders();
23338
23570
  ACTIVITY_NOTE_MAX_CHARS = 500;
23339
23571
  COORDINATOR_EDIT_KINDS = /* @__PURE__ */ new Set([
@@ -24554,7 +24786,8 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
24554
24786
  artifactId: str(args, "artifactId"),
24555
24787
  actor,
24556
24788
  reason: str(args, "reason"),
24557
- ...optStr(args, "part") !== void 0 ? { part: optStr(args, "part") } : {}
24789
+ ...optStr(args, "part") !== void 0 ? { part: optStr(args, "part") } : {},
24790
+ ...optStr(args, "gapId") !== void 0 ? { gapId: optStr(args, "gapId") } : {}
24558
24791
  });
24559
24792
  return { message: '\u5DF2\u4E0A\u62A5\uFF08escalate\uFF09\uFF1A\u7CFB\u7EDF\u5C06\u5347\u7EA7\u5230\u8D1F\u8D23\u4EBA\u6536\u4EF6\u7BB1\u3002\u4F60\u53EF\u4EE5\u6536\u5DE5\u4E86\uFF1B\u4E4B\u540E\u53EF\u80FD\u5728\u5BF9\u8BDD\u91CC\u88AB ta \u95EE\u5230\u3002\u26A0\uFE0F \u82E5\u8FD9\u6B21\u4E0A\u62A5\u5173\u8054\u67D0\u6761 gap\uFF0C**\u4E0D\u8981\u5BF9\u5B83\u8C03 resolve-gap**\u2014\u2014\u90A3\u8868\u793A"\u9700\u6C42\u5DF2\u6EE1\u8DB3\u3001\u8BA9 owner \u590D\u5DE5"\uFF0C\u73B0\u5728\u8FD8\u6CA1\u6EE1\u8DB3\uFF1Bgap \u4FDD\u6301 open \u8282\u70B9\u624D\u4F1A\u7EE7\u7EED\u7B49\u5F85\u3002' };
24560
24793
  }
@@ -24610,6 +24843,23 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
24610
24843
  await kernel.reopen({ artifactId: str(args, "artifactId"), actor, note: optStr(args, "note") });
24611
24844
  return { message: `reopened ${str(args, "artifactId")}\uFF08lifecycle \u2192 active\uFF09` };
24612
24845
  }
24846
+ case "hold": {
24847
+ await kernel.hold({
24848
+ artifactId: str(args, "artifactId"),
24849
+ actor,
24850
+ source: optStr(args, "source") ?? "human",
24851
+ ...optStr(args, "reason") !== void 0 ? { reason: optStr(args, "reason") } : {}
24852
+ });
24853
+ return { message: `\u8282\u70B9\u5DF2\u6682\u505C\u6D3E\u53D1\uFF08hold\uFF09\uFF1A${str(args, "artifactId")}\u3002\u6062\u590D\u7528 release\u3002` };
24854
+ }
24855
+ case "release": {
24856
+ await kernel.release({
24857
+ artifactId: str(args, "artifactId"),
24858
+ actor,
24859
+ ...optStr(args, "reason") !== void 0 ? { reason: optStr(args, "reason") } : {}
24860
+ });
24861
+ return { message: `\u8282\u70B9\u5DF2\u89E3\u963B\uFF08release\uFF09\uFF1A${str(args, "artifactId")}\u2014\u2014\u6295\u5F71\u4E0B\u4E00\u62CD\u4F1A\u91CD\u63A8\u8BE5\u8282\u70B9\u5F53\u4E0B\u6B20\u7684\u6D3B\u3002` };
24862
+ }
24613
24863
  case "forceConclude": {
24614
24864
  await kernel.forceConclude({
24615
24865
  artifactId: str(args, "artifactId"),
@@ -26675,6 +26925,15 @@ var init_memory_trace_store = __esm({
26675
26925
  const nextCursor = items.length > limit ? page[page.length - 1]?.id ?? null : null;
26676
26926
  return { items: page.map((r) => this.decorate(structuredClone(r))), nextCursor };
26677
26927
  }
26928
+ async listUsageRuns(filter) {
26929
+ return [...this.runs.values()].filter((run) => run.usage != null && run.startedAt >= filter.from && run.startedAt <= filter.to).map((run) => ({
26930
+ actorId: run.actorId,
26931
+ artifactId: run.artifactId,
26932
+ startedAt: run.startedAt,
26933
+ effectiveModel: run.effectiveModel,
26934
+ usage: structuredClone(run.usage)
26935
+ }));
26936
+ }
26678
26937
  async appendEvent(runId, event) {
26679
26938
  return (await this.appendEvents(runId, [event]))[0];
26680
26939
  }
@@ -33711,6 +33970,10 @@ var init_daemon_hub = __esm({
33711
33970
  connectedDaemons() {
33712
33971
  return [...this.daemons.values()];
33713
33972
  }
33973
+ /** 最近一帧(含 pong 心跳)的时刻(ms)——节点心跳时钟,供节点健康派生判"心跳新鲜"。缺 = 未连接/已清。 */
33974
+ lastSeenAt(daemonId) {
33975
+ return this.lastSeen.get(daemonId);
33976
+ }
33714
33977
  /** Nodes that disconnected within the last GHOST_TTL_MS ms. */
33715
33978
  recentlyDisconnected() {
33716
33979
  const cutoff = Date.now() - _DaemonHub.GHOST_TTL_MS;
@@ -35400,6 +35663,7 @@ function actorsDomain(opts) {
35400
35663
  status: "active",
35401
35664
  ...b2.email !== void 0 ? { email: b2.email } : {},
35402
35665
  ...b2.teamId !== void 0 ? { teamId: b2.teamId } : {},
35666
+ ...b2.reportsTo ? { reportsTo: b2.reportsTo } : {},
35403
35667
  ...avatar !== void 0 ? { avatar } : {},
35404
35668
  ...positions !== void 0 ? { roles: positions } : {}
35405
35669
  },
@@ -35414,10 +35678,12 @@ function actorsDomain(opts) {
35414
35678
  const b2 = req.body ?? {};
35415
35679
  const positions = positionsFromPayload(b2);
35416
35680
  const avatar = avatarRefFromPayload(b2);
35417
- const { positions: _positions, id: _id, kind: _kind, avatar: _avatar, ...patch } = b2;
35681
+ const { positions: _positions, id: _id, kind: _kind, avatar: _avatar, reportsTo: _reportsTo, ...patch } = b2;
35418
35682
  const actor = await service.upsertActor({
35419
35683
  ...existing,
35420
35684
  ...patch,
35685
+ // 汇报人:显式给(含清空)——传空串/null 归一为 undefined(写库落 null);未传则保留原值
35686
+ ...b2.reportsTo !== void 0 ? { reportsTo: b2.reportsTo || void 0 } : {},
35421
35687
  ...avatar !== void 0 ? { avatar } : {},
35422
35688
  ...positions !== void 0 ? { roles: positions } : {},
35423
35689
  id: existing.id,
@@ -36252,12 +36518,12 @@ function parseAcceptanceCriteria(body) {
36252
36518
  }
36253
36519
  function parseReviewActor(req) {
36254
36520
  const actor = String(req.auth.actor);
36255
- const shortId = actor.split(":").slice(2).join(":") || actor;
36521
+ const shortId2 = actor.split(":").slice(2).join(":") || actor;
36256
36522
  const b2 = req.body ?? {};
36257
36523
  return {
36258
36524
  actorKind: actor.includes(":agent:") ? "agent" : "human",
36259
36525
  actorId: actor,
36260
- ...b2.actor_name !== void 0 || b2.actorName !== void 0 ? { actorName: b2.actor_name ?? b2.actorName } : { actorName: shortId }
36526
+ ...b2.actor_name !== void 0 || b2.actorName !== void 0 ? { actorName: b2.actor_name ?? b2.actorName } : { actorName: shortId2 }
36261
36527
  };
36262
36528
  }
36263
36529
  function parseAnnotationActor(req) {
@@ -39704,6 +39970,8 @@ async function runACPSession(job, cfg) {
39704
39970
  ...job.env,
39705
39971
  ...cfg.yoloEnv ?? {},
39706
39972
  ...job.wrapperPaths?.length ? { PATH: `${job.wrapperPaths.map((p2) => path11.dirname(p2)).join(path11.delimiter)}${path11.delimiter}${job.env?.["PATH"] ?? process.env["PATH"] ?? ""}` } : {},
39973
+ // 每会话技能目录绝对路径 → env(供不扫 cwd 的 runtime 经 config external_dirs 发现,见 injectSkillsDirEnv)。
39974
+ ...cfg.injectSkillsDirEnv ? { [cfg.injectSkillsDirEnv]: path11.join(dir, sessionSkillsSubdir(cfg.runtimeKind ?? "")) } : {},
39707
39975
  OASIS_SERVER: job.server.url,
39708
39976
  OASIS_TOKEN: job.actorToken
39709
39977
  },
@@ -39946,6 +40214,7 @@ var init_acp = __esm({
39946
40214
  path11 = __toESM(require("node:path"), 1);
39947
40215
  readline3 = __toESM(require("node:readline"), 1);
39948
40216
  init_sync_skills();
40217
+ init_skill_cache();
39949
40218
  init_claude_code();
39950
40219
  init_usage();
39951
40220
  init_pricing();
@@ -40218,6 +40487,11 @@ var init_hermes = __esm({
40218
40487
  },
40219
40488
  extraArgs: this.opts.extraArgs,
40220
40489
  runtimeKind: "hermes",
40490
+ // hermes 不扫 cwd 技能目录(只认 $HERMES_HOME/skills + config external_dirs)。把本会话
40491
+ // 技能目录绝对路径喂到 OASIS_HERMES_SKILLS_DIR;node enroll 已把 config.yaml 的
40492
+ // skills.external_dirs 配成该 env → hermes 每 session 发现各自技能。见
40493
+ // packages/adapters/docs/modules/07-skills-delivery.md 与 connect-script.ts。
40494
+ injectSkillsDirEnv: "OASIS_HERMES_SKILLS_DIR",
40221
40495
  ...this.opts.syncSkills ? { syncSkills: this.opts.syncSkills } : {}
40222
40496
  });
40223
40497
  }
@@ -41036,6 +41310,45 @@ if command -v hermes &>/dev/null; then
41036
41310
  "$HERMES_PIP" install 'agent-client-protocol>=0.9.0,<1.0'
41037
41311
  fi
41038
41312
  fi
41313
+ # Oasis \u6BCF\u4F1A\u8BDD\u6280\u80FD\uFF1Ahermes \u4E0D\u626B cwd\uFF0C\u53EA\u8BA4 $HERMES_HOME/skills + config skills.external_dirs\u3002
41314
+ # \u5E42\u7B49\u5730\u628A config.yaml \u7684 external_dirs \u914D\u6210\u5B57\u9762\u91CF $OASIS_HERMES_SKILLS_DIR\uFF08\u7531 hermes \u8FD0\u884C\u65F6
41315
+ # \u6309 env \u5C55\u5F00\uFF1Badapter \u6BCF\u4F1A\u8BDD\u628A\u8BE5 env \u6307\u5411\u672C workdir \u7684\u6280\u80FD\u76EE\u5F55 \u2192 \u6BCF session \u5404\u81EA\u53D1\u73B0\uFF0C\u8BFB\u4FA7\u9694\u79BB\uFF09\u3002
41316
+ HERMES_HOME_DIR="$HERMES_HOME"
41317
+ if [ -z "$HERMES_HOME_DIR" ]; then HERMES_HOME_DIR="$HOME/.hermes"; fi
41318
+ "$HERMES_PY" - "$HERMES_HOME_DIR/config.yaml" <<'PYEOF' || true
41319
+ import sys, os
41320
+ try:
41321
+ import yaml
41322
+ except Exception:
41323
+ sys.exit(0)
41324
+ p = sys.argv[1]
41325
+ data = {}
41326
+ if os.path.exists(p):
41327
+ try:
41328
+ with open(p) as f:
41329
+ data = yaml.safe_load(f) or {}
41330
+ except Exception:
41331
+ sys.exit(0)
41332
+ if not isinstance(data, dict):
41333
+ sys.exit(0)
41334
+ skills = data.get("skills")
41335
+ if not isinstance(skills, dict):
41336
+ skills = {}
41337
+ data["skills"] = skills
41338
+ ext = skills.get("external_dirs")
41339
+ if not isinstance(ext, list):
41340
+ ext = [] if not ext else [ext]
41341
+ skills["external_dirs"] = ext
41342
+ token = "$OASIS_HERMES_SKILLS_DIR"
41343
+ if token not in ext:
41344
+ ext.append(token)
41345
+ os.makedirs(os.path.dirname(p), exist_ok=True)
41346
+ tmp = p + ".oasis.tmp"
41347
+ with open(tmp, "w") as f:
41348
+ yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
41349
+ os.replace(tmp, p)
41350
+ print("-> hermes config.yaml: skills.external_dirs += $OASIS_HERMES_SKILLS_DIR")
41351
+ PYEOF
41039
41352
  fi
41040
41353
 
41041
41354
  OASIS_DIR="$(pwd)"
@@ -41228,6 +41541,63 @@ var init_connect_script = __esm({
41228
41541
  }
41229
41542
  });
41230
41543
 
41544
+ // ../server/src/node-health.ts
41545
+ function deriveNodeHealth(input) {
41546
+ const staleMs = input.staleMs ?? 9e4;
41547
+ const maxFail = input.maxConsecutiveFailures ?? 3;
41548
+ const cf = input.snapshot?.consecutiveFailures ?? 0;
41549
+ const heartbeatFresh = input.lastHeartbeatMs !== void 0 && input.now - input.lastHeartbeatMs < staleMs;
41550
+ const healthy = input.online && heartbeatFresh && cf < maxFail;
41551
+ return {
41552
+ healthy,
41553
+ online: input.online,
41554
+ ...input.lastHeartbeatMs !== void 0 ? { lastHeartbeatAt: new Date(input.lastHeartbeatMs).toISOString() } : {},
41555
+ activeRunCount: input.activeRunCount,
41556
+ consecutiveFailures: cf,
41557
+ ...input.snapshot?.lastSuccessfulDispatchAt ? { lastSuccessfulDispatchAt: input.snapshot.lastSuccessfulDispatchAt } : {},
41558
+ ...input.snapshot?.lastFailureAt ? { lastFailureAt: input.snapshot.lastFailureAt } : {},
41559
+ ...input.snapshot?.lastFailureReason ? { lastFailureReason: input.snapshot.lastFailureReason } : {}
41560
+ };
41561
+ }
41562
+ var NodeHealthTracker;
41563
+ var init_node_health = __esm({
41564
+ "../server/src/node-health.ts"() {
41565
+ "use strict";
41566
+ NodeHealthTracker = class {
41567
+ byNode = /* @__PURE__ */ new Map();
41568
+ ensure(nodeId) {
41569
+ let s2 = this.byNode.get(nodeId);
41570
+ if (!s2) {
41571
+ s2 = { consecutiveFailures: 0 };
41572
+ this.byNode.set(nodeId, s2);
41573
+ }
41574
+ return s2;
41575
+ }
41576
+ /** 节点交付了一次会话(可达):清零连败、记成功时刻。 */
41577
+ recordReachable(nodeId, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
41578
+ const s2 = this.ensure(nodeId);
41579
+ s2.consecutiveFailures = 0;
41580
+ s2.lastSuccessfulDispatchAt = nowIso;
41581
+ }
41582
+ /** 节点级失败(server-unreachable/失联/派发丢失):连败 +1、记失败时刻与归因。 */
41583
+ recordUnreachable(nodeId, reason, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
41584
+ const s2 = this.ensure(nodeId);
41585
+ s2.consecutiveFailures += 1;
41586
+ s2.lastFailureAt = nowIso;
41587
+ if (reason !== void 0) s2.lastFailureReason = reason;
41588
+ }
41589
+ snapshot(nodeId) {
41590
+ const s2 = this.byNode.get(nodeId);
41591
+ return s2 ? { ...s2 } : void 0;
41592
+ }
41593
+ /** 节点被删除/换机时清账(可选调用)。 */
41594
+ forget(nodeId) {
41595
+ this.byNode.delete(nodeId);
41596
+ }
41597
+ };
41598
+ }
41599
+ });
41600
+
41231
41601
  // ../server/src/domains/nodes/routes.ts
41232
41602
  function nodesDomain(deps) {
41233
41603
  let hubWired = false;
@@ -41311,6 +41681,8 @@ function nodesDomain(deps) {
41311
41681
  }
41312
41682
  const daemonVersionById = new Map((hub?.connectedDaemons() ?? []).map((d) => [d.daemonId, d.meta.daemonVersion]));
41313
41683
  const connectionById = new Map((hub?.recentConnectionChanges() ?? []).map((d) => [d.daemonId, d]));
41684
+ const onlineIds = new Set((hub?.connectedDaemons() ?? []).map((d) => d.daemonId));
41685
+ const healthNow = Date.now();
41314
41686
  const nodes = await deps.nodeStore.listNodes();
41315
41687
  const items = await Promise.all(nodes.map(async (n) => {
41316
41688
  const rts = await deps.nodeStore.listRuntimes(n.id);
@@ -41339,6 +41711,15 @@ function nodesDomain(deps) {
41339
41711
  ...connection.lastDisconnectedAt ? { lastDisconnectedAt: new Date(connection.lastDisconnectedAt).toISOString() } : {},
41340
41712
  ...connection.lastReconnectedAt ? { lastReconnectedAt: new Date(connection.lastReconnectedAt).toISOString() } : {}
41341
41713
  }
41714
+ } : {},
41715
+ ...deps.nodeHealth ? {
41716
+ health: deriveNodeHealth({
41717
+ online: onlineIds.has(n.id),
41718
+ ...hub?.lastSeenAt(n.id) !== void 0 ? { lastHeartbeatMs: hub.lastSeenAt(n.id) } : {},
41719
+ activeRunCount: deps.activeRunCountOf?.(n.id) ?? 0,
41720
+ ...deps.nodeHealth.snapshot(n.id) ? { snapshot: deps.nodeHealth.snapshot(n.id) } : {},
41721
+ now: healthNow
41722
+ })
41342
41723
  } : {}
41343
41724
  };
41344
41725
  }));
@@ -41537,6 +41918,7 @@ var init_routes4 = __esm({
41537
41918
  import_node_path2 = require("node:path");
41538
41919
  init_src4();
41539
41920
  init_connect_script();
41921
+ init_node_health();
41540
41922
  }
41541
41923
  });
41542
41924
 
@@ -41743,8 +42125,18 @@ var init_node_store = __esm({
41743
42125
  });
41744
42126
 
41745
42127
  // ../server/src/domains/collab/inbox.ts
41746
- function buildInbox(model, me, leadOf) {
42128
+ function buildInbox(model, me, leadOf, resolveActor) {
41747
42129
  const out = [];
42130
+ const gapItems = [];
42131
+ const actorRef = (id) => {
42132
+ const extra = resolveActor?.(id);
42133
+ return {
42134
+ id,
42135
+ ...extra?.name ? { name: extra.name } : {},
42136
+ ...extra?.role ? { role: extra.role } : {},
42137
+ ...extra?.avatar ? { avatar: extra.avatar } : {}
42138
+ };
42139
+ };
41748
42140
  const creatorOf = /* @__PURE__ */ new Map();
41749
42141
  for (const a of model.artifacts.values()) {
41750
42142
  if (a.type === "brief") creatorOf.set(a.workspace, a.owner);
@@ -41765,44 +42157,53 @@ function buildInbox(model, me, leadOf) {
41765
42157
  summary: `\u5DE5\u5355\u7F3A\u4EBA\u624B\uFF1A${pendingRole[1]} \u89D2\u8272\u65E0\u5458\u5DE5\uFF0C\u300C${nodeLabel(a)}\u300D\u5F85\u6D3E\u2014\u2014\u8BF7\u8865\u4EBA\u6216 assign`
41766
42158
  });
41767
42159
  }
41768
- const gapEscalationSeen = /* @__PURE__ */ new Set();
41769
42160
  for (const a of model.artifacts.values()) {
41770
- const escs = unresolvedEscalationsOf(model, a.id);
41771
- if (escs.length === 0) continue;
41772
42161
  const isOwnerRecipient = creatorOf.get(a.workspace) === me;
41773
42162
  const isLeadRecipient = !!leadOf && leadOf(a.owner) === me;
41774
42163
  if (!isOwnerRecipient && !isLeadRecipient) continue;
41775
- const key = `${a.workspace}:${a.id}`;
41776
- if (gapEscalationSeen.has(key)) continue;
41777
- gapEscalationSeen.add(key);
41778
- const who = isOwnerRecipient ? "\uFF08\u4F60\u662F\u5DE5\u5355\u5F52\u5C5E\u4EBA\uFF09" : "\uFF08\u4F60\u662F\u5361\u4F4F\u8282\u70B9 owner \u7684\u4E0A\u7EA7\uFF09";
41779
- out.push({
41780
- kind: "gap-escalation",
41781
- artifactId: a.id,
41782
- workspace: a.workspace,
41783
- since: escs.at(-1).at,
41784
- actionRequired: true,
41785
- summary: `\u300C${nodeLabel(a)}\u300D\u88AB\u4E0A\u62A5\u9700\u4F60\u4ECB\u5165${who}\uFF1A${escs.at(-1).reason.slice(0, 100)}${escs.length > 1 ? `\uFF08\u5171 ${escs.length} \u6761\uFF09` : ""}`
41786
- });
41787
- }
41788
- for (const a of model.artifacts.values()) {
41789
42164
  const gaps = unresolvedGapsOf(model, a.id);
41790
- if (gaps.length === 0) continue;
41791
- const isOwnerRecipient = creatorOf.get(a.workspace) === me;
41792
- const isLeadRecipient = !!leadOf && leadOf(a.owner) === me;
41793
- if (!isOwnerRecipient && !isLeadRecipient) continue;
41794
- const key = `${a.workspace}:${a.id}`;
41795
- if (gapEscalationSeen.has(key)) continue;
41796
- gapEscalationSeen.add(key);
42165
+ const escalations = unresolvedEscalationsOf(model, a.id);
41797
42166
  const who = isOwnerRecipient ? "\uFF08\u4F60\u662F\u5DE5\u5355\u5F52\u5C5E\u4EBA\uFF09" : "\uFF08\u4F60\u662F\u5361\u4F4F\u8282\u70B9 owner \u7684\u4E0A\u7EA7\uFF09";
41798
- out.push({
41799
- kind: "gap-escalation",
41800
- artifactId: a.id,
41801
- workspace: a.workspace,
41802
- since: model.lastOpAt.get(a.id) ?? "",
41803
- actionRequired: true,
41804
- summary: `\u300C${nodeLabel(a)}\u300D\u5361\u5728\u672A\u89E3\u51B3\u7684\u7F3A\u53E3\u4E0A\u3001\u9700\u4F60\u4ECB\u5165${who}\uFF1A${gaps[0].description.slice(0, 80)}${gaps.length > 1 ? `\uFF08\u5171 ${gaps.length} \u6761\uFF09` : ""}`
41805
- });
42167
+ for (const gap of gaps) {
42168
+ const byGapId = escalations.filter((entry) => entry.gapId === gap.gapId);
42169
+ const samePartGaps = gaps.filter((candidate) => candidate.part === gap.part);
42170
+ const samePartEscalations = escalations.filter(
42171
+ (entry) => entry.gapId === void 0 && entry.part === gap.part
42172
+ );
42173
+ const partlessEscalations = escalations.filter(
42174
+ (entry) => entry.gapId === void 0 && entry.part === null
42175
+ );
42176
+ const exactEscalation = byGapId.at(-1) ?? (samePartGaps.length === 1 ? samePartEscalations.at(-1) : void 0) ?? (gaps.length === 1 ? partlessEscalations.at(-1) : void 0);
42177
+ gapItems.push({
42178
+ kind: "gap-escalation",
42179
+ artifactId: a.id,
42180
+ workspace: a.workspace,
42181
+ since: exactEscalation?.at ?? gap.lastReportedAt ?? model.lastOpAt.get(a.id) ?? "",
42182
+ actionRequired: true,
42183
+ summary: exactEscalation?.reason ?? gap.description,
42184
+ gap: {
42185
+ gapId: gap.gapId,
42186
+ // 契约:reporter 永远是 GapView.by(原始上报人);诊断者另见 diagnosis 文案,不覆盖署名。
42187
+ reporter: actorRef(gap.by),
42188
+ artifactLabel: nodeLabel(a),
42189
+ problem: gap.description,
42190
+ ...exactEscalation ? { diagnosis: exactEscalation.reason } : {},
42191
+ priority: exactEscalation ? "decision_required" : "blocked",
42192
+ reportCount: gap.reportCount ?? 1
42193
+ }
42194
+ });
42195
+ }
42196
+ if (gaps.length === 0 && escalations.length > 0) {
42197
+ const last = escalations.at(-1);
42198
+ gapItems.push({
42199
+ kind: "gap-escalation",
42200
+ artifactId: a.id,
42201
+ workspace: a.workspace,
42202
+ since: last.at,
42203
+ actionRequired: true,
42204
+ summary: `\u300C${nodeLabel(a)}\u300D\u88AB\u4E0A\u62A5\u9700\u4F60\u4ECB\u5165${who}\uFF1A${last.reason.slice(0, 100)}${escalations.length > 1 ? `\uFF08\u5171 ${escalations.length} \u6761\uFF09` : ""}`
42205
+ });
42206
+ }
41806
42207
  }
41807
42208
  for (const r of listPendingReviews(model)) {
41808
42209
  if (r.origin) continue;
@@ -41910,7 +42311,10 @@ function buildInbox(model, me, leadOf) {
41910
42311
  if (gate) continue;
41911
42312
  out.push({ kind: "assigned", artifactId: a.id, workspace: ws, since: model.lastOpAt.get(a.id) ?? "", actionRequired: true, summary: `\u5728\u9014\u5F85\u529E\uFF1A${label}` });
41912
42313
  }
41913
- return out.sort((x2, y) => x2.since < y.since ? 1 : x2.since > y.since ? -1 : 0);
42314
+ const rank = (item) => item.gap?.priority === "decision_required" ? 0 : 1;
42315
+ gapItems.sort((a, b2) => rank(a) - rank(b2) || a.since.localeCompare(b2.since));
42316
+ out.sort((x2, y) => x2.since < y.since ? 1 : x2.since > y.since ? -1 : 0);
42317
+ return [...gapItems, ...out];
41914
42318
  }
41915
42319
  var init_inbox = __esm({
41916
42320
  "../server/src/domains/collab/inbox.ts"() {
@@ -41970,7 +42374,9 @@ var init_audit = __esm({
41970
42374
  reopen: "\u91CD\u5F00",
41971
42375
  seal: "\u5C01\u5B58",
41972
42376
  link_input: "\u63A5\u4F9D\u8D56",
41973
- report_gap: "\u7559\u7F3A\u53E3"
42377
+ report_gap: "\u7559\u7F3A\u53E3",
42378
+ hold: "\u6682\u505C\u6D3E\u53D1",
42379
+ release: "\u89E3\u963B"
41974
42380
  };
41975
42381
  }
41976
42382
  });
@@ -42021,6 +42427,139 @@ var init_dashboard = __esm({
42021
42427
  }
42022
42428
  });
42023
42429
 
42430
+ // ../server/src/domains/collab/organization-usage.ts
42431
+ function emptyUsage() {
42432
+ return {
42433
+ input: 0,
42434
+ cache: 0,
42435
+ output: 0,
42436
+ tokens: 0,
42437
+ costUsdMicros: 0,
42438
+ costParts: { input: 0, cache: 0, output: 0 }
42439
+ };
42440
+ }
42441
+ function usageOf(run) {
42442
+ if (!run.effectiveModel?.trim()) return null;
42443
+ const input = Math.max(run.usage.inputTokens ?? 0, 0);
42444
+ const cache = Math.max(run.usage.cacheReadTokens ?? 0, 0) + Math.max(run.usage.cacheCreationTokens ?? 0, 0);
42445
+ const output = Math.max(run.usage.outputTokens ?? 0, 0);
42446
+ const tokens = input + cache + output;
42447
+ const costUsdMicros = Math.max(run.usage.costUsdMicros ?? 0, 0);
42448
+ const denominator = tokens || 1;
42449
+ const inputCost = Math.round(costUsdMicros * input / denominator);
42450
+ const cacheCost = Math.round(costUsdMicros * cache / denominator);
42451
+ return {
42452
+ input,
42453
+ cache,
42454
+ output,
42455
+ tokens,
42456
+ costUsdMicros,
42457
+ costParts: {
42458
+ input: inputCost,
42459
+ cache: cacheCost,
42460
+ output: costUsdMicros - inputCost - cacheCost
42461
+ }
42462
+ };
42463
+ }
42464
+ function addUsage(target, addition) {
42465
+ target.input += addition.input;
42466
+ target.cache += addition.cache;
42467
+ target.output += addition.output;
42468
+ target.tokens += addition.tokens;
42469
+ target.costUsdMicros += addition.costUsdMicros;
42470
+ target.costParts.input += addition.costParts.input;
42471
+ target.costParts.cache += addition.costParts.cache;
42472
+ target.costParts.output += addition.costParts.output;
42473
+ }
42474
+ function bucketFormatter(timeZone, kind) {
42475
+ return new Intl.DateTimeFormat("en-CA", {
42476
+ timeZone,
42477
+ year: "numeric",
42478
+ month: "2-digit",
42479
+ day: "2-digit",
42480
+ ...kind === "hour" ? { hour: "2-digit", hourCycle: "h23" } : {}
42481
+ });
42482
+ }
42483
+ function bucketKey(formatter, value, kind) {
42484
+ const date3 = new Date(value);
42485
+ if (!Number.isFinite(date3.getTime())) return null;
42486
+ const parts = new Map(formatter.formatToParts(date3).map((part) => [part.type, part.value]));
42487
+ const day = `${parts.get("year")}-${parts.get("month")}-${parts.get("day")}`;
42488
+ return kind === "hour" ? `${day}T${parts.get("hour")}` : day;
42489
+ }
42490
+ function buildOrganizationUsageSummary(input) {
42491
+ const formatter = bucketFormatter(input.timeZone, input.bucketKind);
42492
+ const workorders = buildWorkorderSummaries(input.model);
42493
+ const workorderById = new Map(workorders.map((workorder) => [workorder.id, workorder]));
42494
+ const workspaceByArtifact = new Map([...input.model.artifacts.values()].map((artifact) => [artifact.id, artifact.workspace]));
42495
+ const buckets = /* @__PURE__ */ new Map();
42496
+ const rows = /* @__PURE__ */ new Map();
42497
+ const total = emptyUsage();
42498
+ let unpricedRunCount = 0;
42499
+ for (const run of input.runs) {
42500
+ const usage = usageOf(run);
42501
+ if (!usage) {
42502
+ unpricedRunCount += 1;
42503
+ continue;
42504
+ }
42505
+ addUsage(total, usage);
42506
+ const key = bucketKey(formatter, run.startedAt, input.bucketKind);
42507
+ if (key) {
42508
+ const bucket = buckets.get(key) ?? { key, ...emptyUsage() };
42509
+ addUsage(bucket, usage);
42510
+ buckets.set(key, bucket);
42511
+ }
42512
+ const workspace = run.artifactId ? workspaceByArtifact.get(run.artifactId) : void 0;
42513
+ const workorder = workspace ? workorderById.get(workspace) : void 0;
42514
+ const rowId = workorder?.id ?? UNASSIGNED_ID;
42515
+ let row = rows.get(rowId);
42516
+ if (!row) {
42517
+ row = {
42518
+ id: rowId,
42519
+ title: workorder?.title ?? "\u672A\u5173\u8054\u5DE5\u5355",
42520
+ stage: workorder?.stage ?? "unassigned",
42521
+ usage: emptyUsage(),
42522
+ employees: [],
42523
+ share: 0,
42524
+ ...!workorder ? { unassigned: true } : {}
42525
+ };
42526
+ rows.set(rowId, row);
42527
+ }
42528
+ addUsage(row.usage, usage);
42529
+ let employee = row.employees.find((item) => item.actorId === run.actorId);
42530
+ if (!employee) {
42531
+ employee = { actorId: run.actorId, usage: emptyUsage(), share: 0 };
42532
+ row.employees.push(employee);
42533
+ }
42534
+ addUsage(employee.usage, usage);
42535
+ }
42536
+ const workorderRows = [...rows.values()].map((row) => {
42537
+ row.share = total.tokens > 0 ? row.usage.tokens / total.tokens : 0;
42538
+ row.employees = row.employees.map((employee) => ({
42539
+ ...employee,
42540
+ share: row.usage.tokens > 0 ? employee.usage.tokens / row.usage.tokens : 0
42541
+ })).sort((a, b2) => b2.usage.tokens - a.usage.tokens);
42542
+ return row;
42543
+ }).sort((a, b2) => {
42544
+ if (a.unassigned !== b2.unassigned) return a.unassigned ? 1 : -1;
42545
+ return b2.usage.tokens - a.usage.tokens;
42546
+ });
42547
+ return {
42548
+ total,
42549
+ buckets: [...buckets.values()].sort((a, b2) => a.key.localeCompare(b2.key)),
42550
+ workorders: workorderRows,
42551
+ unpricedRunCount
42552
+ };
42553
+ }
42554
+ var UNASSIGNED_ID;
42555
+ var init_organization_usage = __esm({
42556
+ "../server/src/domains/collab/organization-usage.ts"() {
42557
+ "use strict";
42558
+ init_workorders();
42559
+ UNASSIGNED_ID = "__unassigned__";
42560
+ }
42561
+ });
42562
+
42024
42563
  // ../server/src/domains/collab/index.ts
42025
42564
  async function resolveLead(registry2, me) {
42026
42565
  const rec = await registry2.getActor(me);
@@ -42098,6 +42637,34 @@ function collabDomain(opts) {
42098
42637
  return ctx;
42099
42638
  }
42100
42639
  return (router) => {
42640
+ router.get("/api/organization/usage-summary", async (req) => {
42641
+ if (!opts.trace) {
42642
+ return { status: 503, body: { error: { code: "no_trace_store", message: "\u8FD0\u884C\u7528\u91CF\u6570\u636E\u6E90\u672A\u63A5\u5165" } } };
42643
+ }
42644
+ const from = req.query.get("from");
42645
+ const to = req.query.get("to");
42646
+ const bucket = req.query.get("bucket");
42647
+ const timeZone = req.query.get("timeZone") ?? "UTC";
42648
+ if (!from || !to || bucket !== "hour" && bucket !== "day") {
42649
+ return { status: 400, body: { error: { code: "bad_request", message: "from / to / bucket(hour|day) \u5FC5\u586B" } } };
42650
+ }
42651
+ const fromMs = Date.parse(from);
42652
+ const toMs = Date.parse(to);
42653
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs > toMs) {
42654
+ return { status: 400, body: { error: { code: "bad_request", message: "from / to \u65F6\u95F4\u8303\u56F4\u65E0\u6548" } } };
42655
+ }
42656
+ try {
42657
+ new Intl.DateTimeFormat("en", { timeZone }).format();
42658
+ } catch {
42659
+ return { status: 400, body: { error: { code: "bad_request", message: "timeZone \u65E0\u6548" } } };
42660
+ }
42661
+ const { kernel } = await resolveCtx(req.auth.companyId);
42662
+ const runs = await opts.trace.listUsageRuns({ from, to });
42663
+ return {
42664
+ status: 200,
42665
+ body: buildOrganizationUsageSummary({ model: kernel.model, runs, bucketKind: bucket, timeZone })
42666
+ };
42667
+ });
42101
42668
  router.get("/api/workorders", async (req) => {
42102
42669
  const { kernel, registry: registry2, artifacts } = await resolveCtx(req.auth.companyId);
42103
42670
  const resolve5 = await buildResolver(registry2);
@@ -42165,8 +42732,13 @@ function collabDomain(opts) {
42165
42732
  });
42166
42733
  router.get("/api/inbox", async (req) => {
42167
42734
  const { kernel, registry: registry2 } = await resolveCtx(req.auth.companyId);
42168
- const leadOf = await buildLeadResolver(registry2);
42169
- return { status: 200, body: { items: buildInbox(kernel.model, req.auth.actor, leadOf) } };
42735
+ const [leadOf, resolveActor] = await Promise.all([
42736
+ buildLeadResolver(registry2),
42737
+ // 决策 0026 (c):gap 升级也发卡住节点 owner 的人类上级
42738
+ buildResolver(registry2)
42739
+ // 决策 0042:gap reporter 名册 join
42740
+ ]);
42741
+ return { status: 200, body: { items: buildInbox(kernel.model, req.auth.actor, leadOf, resolveActor) } };
42170
42742
  });
42171
42743
  router.get("/api/audit", async (req) => {
42172
42744
  const { oplog } = await resolveCtx(req.auth.companyId);
@@ -42227,12 +42799,14 @@ var init_collab = __esm({
42227
42799
  init_inbox();
42228
42800
  init_audit();
42229
42801
  init_dashboard();
42802
+ init_organization_usage();
42230
42803
  init_node_state();
42231
42804
  init_workorders();
42232
42805
  init_workorder_detail();
42233
42806
  init_inbox();
42234
42807
  init_audit();
42235
42808
  init_dashboard();
42809
+ init_organization_usage();
42236
42810
  init_planner();
42237
42811
  }
42238
42812
  });
@@ -42753,6 +43327,17 @@ var init_service4 = __esm({
42753
43327
  const page = await this.store.listRuns(filter);
42754
43328
  return { ...page, items: page.items.map((run) => this.withDisplayCost(run)) };
42755
43329
  }
43330
+ /** 组织汇总专用瘦读:只取归属、时间和 usage,并统一补齐展示成本。 */
43331
+ async listUsageRuns(filter) {
43332
+ const rows = await this.store.listUsageRuns(filter);
43333
+ return rows.filter((row) => row.usage != null).map((row) => ({
43334
+ ...row,
43335
+ usage: {
43336
+ ...row.usage,
43337
+ costUsdMicros: displayCostUsdMicros(row.effectiveModel, row.usage)
43338
+ }
43339
+ }));
43340
+ }
42756
43341
  /** 按维度查聚合用量(per-session/per-agent plan §4)。store 未实现聚合时返回空。
42757
43342
  * 读侧补全展示成本:adapter 未上报 cost 时按价格快照估算(与员工周统计同口径)。 */
42758
43343
  async queryUsageStats(filter) {
@@ -44827,6 +45412,7 @@ var init_daemon_adapter = __esm({
44827
45412
  this.nodeLostGraceMs = opts.nodeLostGraceMs ?? graceFromEnv(12e4);
44828
45413
  this.ackTimeoutMs = opts.ackTimeoutMs ?? ackMsFromEnv(15e3);
44829
45414
  this.log = opts.log ?? ((m2) => console.warn(m2));
45415
+ this.health = opts.health;
44830
45416
  hub.addMessageListener((_daemonId, msg) => this.onDaemonMessage(msg));
44831
45417
  hub.addDisconnectListener((daemonId) => this.onNodeDown(daemonId));
44832
45418
  hub.addConnectListener((daemon) => this.onNodeUp(daemon.daemonId));
@@ -44835,6 +45421,7 @@ var init_daemon_adapter = __esm({
44835
45421
  nodeLostGraceMs;
44836
45422
  ackTimeoutMs;
44837
45423
  log;
45424
+ health;
44838
45425
  /** 断联节点 → 收割定时器。重连取消;到期收割。 */
44839
45426
  reapTimers = /* @__PURE__ */ new Map();
44840
45427
  /** 统一结算一次会话退出:清 ack 定时器、置 exited、触发回调、从 pending 摘除。幂等(已退出即跳过)。 */
@@ -44846,9 +45433,17 @@ var init_daemon_adapter = __esm({
44846
45433
  entry.ackTimer = void 0;
44847
45434
  }
44848
45435
  entry.exited = info;
45436
+ if (info.reason === "server-unreachable") this.health?.recordUnreachable(entry.nodeId, info.reason);
45437
+ else this.health?.recordReachable(entry.nodeId);
44849
45438
  for (const cb of entry.exitCbs.splice(0)) cb(info);
44850
45439
  this.pending.delete(dispatchId);
44851
45440
  }
45441
+ /** 该节点当前在途(未退出)会话数——节点健康的 activeRunCount。 */
45442
+ activeRunCount(nodeId) {
45443
+ let n = 0;
45444
+ for (const e of this.pending.values()) if (e.nodeId === nodeId && !e.exited) n++;
45445
+ return n;
45446
+ }
44852
45447
  /** 收到任何回帧(started/event/output/exited)即视为派发已达,取消 ack 超时。 */
44853
45448
  cancelAck(dispatchId) {
44854
45449
  const entry = this.pending.get(dispatchId);
@@ -44891,6 +45486,8 @@ var init_daemon_adapter = __esm({
44891
45486
  onDaemonMessage(msg) {
44892
45487
  if (msg.type === "session_started") {
44893
45488
  this.cancelAck(msg.dispatchId);
45489
+ const entry = this.pending.get(msg.dispatchId);
45490
+ if (entry) this.health?.recordReachable(entry.nodeId);
44894
45491
  } else if (msg.type === "session_exited") {
44895
45492
  const info = msg.info.reason || msg.info.code !== null ? msg.info : { ...msg.info, reason: "error" };
44896
45493
  this.settle(msg.dispatchId, info);
@@ -45391,6 +45988,12 @@ var init_ci_gate = __esm({
45391
45988
  const statuses = [];
45392
45989
  const repoViews = [];
45393
45990
  for (const unit of units) {
45991
+ if (await ci.isMerged({ repo: unit.repo, sha: unit.sha })) {
45992
+ const status2 = { state: "success", summary: `${unit.repo.id}@${unit.sha.slice(0, 8)}: \u5185\u5BB9\u5DF2\u5728 ${unit.repo.develop}\uFF08\u4E0A\u8F6E\u5DF2\u5408\uFF09\uFF0CCI \u514D\u68C0` };
45993
+ statuses.push({ unit, status: status2 });
45994
+ repoViews.push({ repoId: unit.repo.id, sha: unit.sha.slice(0, 8), state: "success", summary: status2.summary });
45995
+ continue;
45996
+ }
45394
45997
  const pr = await ci.ensurePr(
45395
45998
  { repo: unit.repo, branch: unit.branch, sha: unit.sha },
45396
45999
  { title: `[oasis-ci] ${artifactId} \u2192 ${unit.repo.develop}`, body: `CI gate for artifact ${artifactId} @ ${unit.sha}` }
@@ -45586,7 +46189,7 @@ var init_worker = __esm({
45586
46189
  \u3010\u600E\u4E48\u5E72\u3011\u963B\u585E\u662F**\u75C7\u72B6**\u4E0D\u662F\u75C5\u3002\u7CFB\u7EDF\u5DF2\u5148\u673A\u68B0\u5206\u8BCA\u3001\u628A**\u80FD\u5B9A\u4F4D\u7684**\uFF08\u8D44\u6E90\u6392\u961F / \u8FD0\u884C\u65F6\u7194\u65AD / \u4EBA\u6CA1\u63A5 / \u7B49\u4E00\u4E2A\u5065\u5EB7\u5728\u4EA7\u7684\u4E0A\u6E38\uFF09\u8DEF\u7531\u7ED9\u4E86\u5BF9\u53E3\u5904\u7F6E\u8005\uFF1B\u8D70\u5230\u4F60\u8FD9\u513F\u7684\u662F\u9700\u8981\u4F60\u5224\u65AD\u7684\u90A3\u7C7B\u3002
45587
46190
  \u2460 \u5148\u8BFB\u4E0B\u65B9"\u75C5\u5386"\u6BB5 + \u5FC5\u8981\u65F6 \`oasis status/content <id>\` \u6CBF\u4F9D\u8D56\u94FE\u6478\u6E05\u8BED\u4E49\u6839\uFF1B
45588
46191
  \u2461 \u51FA**\u6700\u7701\u4E8B\u3001\u6700\u6CBB\u672C**\u7684\u4E00\u624B\uFF1A\u80FD\u8865\u4E0A\u4E0B\u6587\u5C31\u522B\u6539\u56FE\u3001\u80FD\u8FDE\u65E2\u6709\u5C31\u522B\u65B0\u5EFA\u3001\u80FD\u81EA\u5DF1\u89E3\u51B3\u5C31\u522B\u60CA\u52A8\u4EBA\uFF1B\u9500\u6BC1\u6027\u6539\u56FE\uFF08\u65AD\u8FB9/\u5E9F\u8282\u70B9/\u6539\u6D3E\uFF09apply \u65F6\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u8F6C\u6210\u5F85\u4EBA\u786E\u8BA4\u7684\u63D0\u6848\uFF0C\u4F60 stage \u597D\u8BB2\u6E05\u5F71\u54CD\u5373\u53EF\u3002
45589
- \u2462 **\u6536\u5C3E\u5FC5\u987B\u663E\u5F0F**\u2014\u2014\u628A\u8FD9\u6761\u7EBF\u63A8\u52A8\u4E86\u3001\u6216\u5224\u65AD\u8BE5\u7531 owner \u7EE7\u7EED \u2192 \`oasis resolve-gap --gap <id> --note "<\u4F60\u505A\u4E86\u4EC0\u4E48 / owner \u8BE5\u600E\u4E48\u63A5>"\`\uFF08**\u4E0D\u53D1\u8FD9\u53E5 gap \u4E0D\u89E3\u3001owner \u4E0D\u9192**\uFF09\uFF1B\u591F\u4E0D\u7740 / \u8981\u4EBA\u5B9A \u2192 \`oasis escalate <\u8282\u70B9> --reason "<\u6839\u56E0 + \u8981\u4EBA\u505A\u4EC0\u4E48>"\` \u4E0A\u62A5\u3002**\u7EDD\u4E0D\u7A7A\u624B\u9000\u51FA**\uFF1A\u4EC0\u4E48\u90FD\u4E0D\u505A\u5730\u6536\u5DE5 = \u628A\u8282\u70B9\u6C38\u4E45\u667E\u6B7B\uFF0C\u6700\u7CDF\u3002`;
46192
+ \u2462 **\u6536\u5C3E\u5FC5\u987B\u663E\u5F0F**\u2014\u2014\u628A\u8FD9\u6761\u7EBF\u63A8\u52A8\u4E86\u3001\u6216\u5224\u65AD\u8BE5\u7531 owner \u7EE7\u7EED \u2192 \`oasis resolve-gap --gap <id> --note "<\u4F60\u505A\u4E86\u4EC0\u4E48 / owner \u8BE5\u600E\u4E48\u63A5>"\`\uFF08**\u4E0D\u53D1\u8FD9\u53E5 gap \u4E0D\u89E3\u3001owner \u4E0D\u9192**\uFF09\uFF1B\u591F\u4E0D\u7740 / \u8981\u4EBA\u5B9A \u2192 \`oasis escalate <\u8282\u70B9> --reason "<\u6839\u56E0 + \u8981\u4EBA\u505A\u4EC0\u4E48>" --gap <gapId> [--part <part>]\` \u4E0A\u62A5\uFF08**\u5FC5\u987B\u5E26 --gap**\uFF0C\u6709 part \u65F6\u4E00\u5E76\u5E26\u4E0A\uFF09\u3002**\u7EDD\u4E0D\u7A7A\u624B\u9000\u51FA**\uFF1A\u4EC0\u4E48\u90FD\u4E0D\u505A\u5730\u6536\u5DE5 = \u628A\u8282\u70B9\u6C38\u4E45\u667E\u6B7B\uFF0C\u6700\u7CDF\u3002`;
45590
46193
  CONVERSATIONAL_ETHOS = `\u4F60\u662F\u8FD9\u4E2A\u5DE5\u5355\u7684**\u7BA1\u7406\u8005**\uFF0C\u6B63\u5728\u548C\u5DE5\u5355\u53D1\u8D77\u4EBA\u5BF9\u8BDD\u3001\u4E00\u8D77\u628A\u5B83\u5F80\u524D\u63A8\u3002\u4F60\u7684\u624B\u6BB5\u5F88\u5BBD\uFF1A\u5E2E\u5404\u8282\u70B9 owner \u626B\u6E05\u969C\u788D\u3001\u7B54\u7591\u3001\u8865\u4E0A\u4E0B\u6587\u3001**\u81EA\u5DF1\u8DD1\u5206\u6790 / \u62C9\u6570\u636E / \u5199\u8BF4\u660E**\u3001\u534F\u8C03\u8DE8\u8282\u70B9\u3001\u6309\u9700\u6539\u56FE\uFF08\u8FDE\u8FB9 / \u65B0\u5EFA / \u53BB\u91CD / \u6539\u6D3E / \u6539\u6807\u9898\u4E0E brief\uFF09\u3001\u4E3A\u53D1\u8D77\u4EBA\u89E3\u7B54\u6216\u4EE3\u529E\u5DE5\u5355\u8303\u56F4\u5185\u7684\u4E8B\u52A1\u3002**\u4F60\u6709\u4F1A\u8BDD\u5185\u7684\u5168\u90E8\u80FD\u529B\uFF08Bash / \u6587\u4EF6 / \u4EFB\u610F CLI\uFF09\uFF0C\u653E\u5F00\u7528\u3002\u552F\u4E00\u786C\u8FB9\u754C\uFF1A\u4E0D\u66FF owner \u5199\u8282\u70B9\u7684\u4EA7\u7269\u6B63\u6587**\uFF08\u8865\u4E0A\u4E0B\u6587\u3001\u7ED9\u53C2\u8003\u90FD\u884C\uFF0C\u522B\u4EE3\u7B14\uFF09\u3002
45591
46194
  - **\u5148\u8BCA\u65AD\u3001\u7ED9\u65B9\u6848\u3001\u95EE ta**\uFF1A\u770B\u6E05\u5361\u5728\u54EA\uFF0C\u7ED9 1\u20132 \u4E2A\u53EF\u884C\u505A\u6CD5\u3001\u8BB2\u6E05\u5404\u81EA\u5F71\u54CD\uFF0C\u95EE ta \u91C7\u7528\u54EA\u4E2A\u2014\u2014\u522B\u81EA\u4F5C\u4E3B\u5F20\u95F7\u5934\u6539\u3002\u660E\u663E\u4E14\u4F4E\u98CE\u9669\u7684\u53EF\u4EE5\u5148\u505A\u4E00\u7248\u518D\u8BF4\u3002
45592
46195
  - **\u6539\u52A8\u5206\u4E24\u7C7B**\uFF08\u6388\u6743\u5355\u4F4D\u662F diff\uFF09\uFF1A\u52A0\u8FB9 / \u65B0\u589E\u8282\u70B9 / \u6539\u6807\u9898 brief \u8FD9\u7C7B**\u53EF\u9006\u7684**\uFF08B\uFF09\uFF0C\`stage\`\u2192\`apply\` \u76F4\u63A5\u843D\u3001\u505A\u5B8C\u628A\u52A8\u4E86\u4EC0\u4E48\u544A\u8BC9 ta\uFF1B\u65AD\u8FB9 / \u5E9F\u8282\u70B9 / \u6539\u6D3E / \u64A4\u90E8\u4EF6\u8FD9\u7C7B**\u9500\u6BC1\u6027\u7684**\uFF08C\uFF09\uFF0C\u4F60 \`apply\` \u65F6\u7CFB\u7EDF\u4F1A**\u81EA\u52A8\u8F6C\u6210\u4E00\u4EFD\u5F85\u786E\u8BA4\u63D0\u6848**\u653E\u5230 ta \u9762\u524D\u7684\u300C\u5F85\u786E\u8BA4\u53D8\u66F4\u300D\u5361\u2014\u2014\u4F60 **stage \u597D\u3001\u628A\u5F71\u54CD\u8BB2\u6E05\u695A\u5C31\u884C\uFF0C\u522B\u81EA\u5DF1\u786C\u843D**\uFF0C\u7531 ta \u5728\u5361\u4E0A\u786E\u8BA4\u6216\u9A73\u56DE\u3002
@@ -45613,7 +46216,7 @@ var init_worker = __esm({
45613
46216
 
45614
46217
  \u3010\u6536\u5C3E gap / \u4E0A\u62A5\u5361\u70B9\u3011\uFF08\u4E0D\u8D70\u6682\u5B58\u7F13\u51B2\uFF0C\u76F4\u63A5\u751F\u6548\uFF09
45615
46218
  - \`oasis resolve-gap --gap <gapId> --note "<\u4F60\u505A\u4E86\u4EC0\u4E48 / owner \u8BE5\u600E\u4E48\u7EE7\u7EED>"\` \u2014\u2014 **\u4FEE\u5B8C\u4E00\u6761 gap \u5FC5\u987B\u663E\u5F0F\u6536\u5C3E**\uFF1A\u8FDE\u8FB9 / \u5EFA\u8282\u70B9**\u4E0D\u4F1A\u81EA\u52A8\u89E3 gap**\uFF0C\u4E0D\u53D1\u8FD9\u53E5\u62A5 gap \u7684 owner \u9192\u4E0D\u8FC7\u6765\u3002\`--gap\` = \u90A3\u6761 gap \u7684 id\uFF08reconcile \u4EFB\u52A1\u91CC\u4F1A\u7ED9\u4F60\uFF09\uFF1B\`--note\` \u539F\u6837\u7ED9 owner \u770B\uFF0C\u5199\u4EBA\u8BDD\u3002
45616
- - \`oasis escalate <id> --reason "<\u7F3A\u4EC0\u4E48\u80FD\u529B / \u8981\u4EBA\u66FF\u4F60\u5B9A\u6216\u505A\u4EC0\u4E48>"\` \u2014\u2014 **\u641E\u4E0D\u5B9A / \u62FF\u4E0D\u51C6 / \u6839\u56E0\u4E0D\u5728\u56FE\u4E0A\u5C31\u4E0A\u62A5\u53D1\u8D77\u4EBA**\uFF08\u522B\u6C89\u9ED8\u9000\u51FA\uFF09\u3002\`<id>\` = \u5361\u4F4F\u7684\u8282\u70B9\uFF1B\`--reason\` \u8BF4\u6E05\u4F60\u5224\u65AD\u7684\u6839\u56E0\u548C\u9700\u8981\u4EBA\u505A\u4EC0\u4E48\u3002\u7CFB\u7EDF\u4F1A\u767B\u8BB0\u672C\u6B21\u4F1A\u8BDD\uFF0C\u4EBA\u56DE\u8BDD\u65F6\u4F60\u88AB**\u63A5\u7740\u5524\u8D77\u3001\u5E26\u4E0A\u4E0B\u6587\u7EE7\u7EED\u89E3**\u3002
46219
+ - \`oasis escalate <id> --reason "<\u7F3A\u4EC0\u4E48\u80FD\u529B / \u8981\u4EBA\u66FF\u4F60\u5B9A\u6216\u505A\u4EC0\u4E48>" --gap <gapId> [--part <part>]\` \u2014\u2014 **\u641E\u4E0D\u5B9A / \u62FF\u4E0D\u51C6 / \u6839\u56E0\u4E0D\u5728\u56FE\u4E0A\u5C31\u4E0A\u62A5\u53D1\u8D77\u4EBA**\uFF08\u522B\u6C89\u9ED8\u9000\u51FA\uFF09\u3002\`<id>\` = \u5361\u4F4F\u7684\u8282\u70B9\uFF1B\`--reason\` \u8BF4\u6E05\u4F60\u5224\u65AD\u7684\u6839\u56E0\u548C\u9700\u8981\u4EBA\u505A\u4EC0\u4E48\u3002\u7CFB\u7EDF\u4F1A\u767B\u8BB0\u672C\u6B21\u4F1A\u8BDD\uFF0C\u4EBA\u56DE\u8BDD\u65F6\u4F60\u88AB**\u63A5\u7740\u5524\u8D77\u3001\u5E26\u4E0A\u4E0B\u6587\u7EE7\u7EED\u89E3**\u3002
45617
46220
 
45618
46221
  \u3010\u8BFB\u56FE\u3011\uFF08\u90FD\u8BFB**\u771F\u56FE**\uFF0C\u4E0E\u522B\u7684 agent \u540C\u8BED\u4E49\uFF0C\u4E0D\u53D7\u4F60\u6682\u5B58\u5F71\u54CD\uFF09
45619
46222
  - \`oasis graph\` \u2014\u2014 \u5168\u56FE\u4F9D\u8D56\u8FB9\uFF08\u8C01\u8FDE\u8C01\uFF09\uFF1B\`oasis ls\` \u2014\u2014 \u5168\u4EA7\u7269\u5217\u8868\uFF08id / type / owner / head / \u961F\u5217 / \u72B6\u6001\u6807\uFF09\u3002\u5148\u7528\u5B83\u4EEC\u5EFA\u7ACB\u5168\u5C40\u56FE\u666F\u3002
@@ -45779,7 +46382,7 @@ ${SHARED_GRAPH_BODY}`;
45779
46382
  const out = [];
45780
46383
  for (const artifact of this.deps.kernel.model.artifacts.values()) {
45781
46384
  for (const g2 of unresolvedGapsOf(this.deps.kernel.model, artifact.id)) {
45782
- out.push({ artifactId: artifact.id, gapId: g2.gapId, description: g2.description });
46385
+ out.push({ artifactId: artifact.id, gapId: g2.gapId, description: g2.description, part: g2.part });
45783
46386
  }
45784
46387
  }
45785
46388
  return out;
@@ -45814,6 +46417,10 @@ ${SHARED_GRAPH_BODY}`;
45814
46417
  }
45815
46418
  return lines.length > 0 ? lines.join("\n") : "\uFF08\u56FE\u91CC\u6682\u65E0\u5176\u5B83\u4EA7\u7269\uFF09";
45816
46419
  }
46420
+ escalateCmd(gap, reasonPlaceholder) {
46421
+ const partFlag = gap.part ? ` --part ${gap.part}` : "";
46422
+ return `oasis escalate ${gap.artifactId} --reason "${reasonPlaceholder}" --gap ${gap.gapId}${partFlag}`;
46423
+ }
45817
46424
  async reconcile(gap) {
45818
46425
  const gapper = this.deps.kernel.model.artifacts.get(gap.artifactId);
45819
46426
  const brief = gapper?.title ?? gapper?.description ?? "";
@@ -45826,7 +46433,7 @@ ${SHARED_GRAPH_BODY}`;
45826
46433
  `## \u8981\u4F60\u505A\u4EC0\u4E48\uFF08\u5148\u5206\u8BCA\uFF09`,
45827
46434
  `\u8FD9\u4E2A\u9700\u6C42**\u53EF\u80FD\u662F\u4EFB\u4F55\u963B\u585E**\u2014\u2014\u7F3A\u4E0A\u6E38\u8F93\u5165 / \u7F3A\u4FE1\u606F / \u8981\u4EBA\u51B3\u7B56 / \u7F3A\u80FD\u529B\uFF08\u5982\u67D0\u4E2A\u5DE5\u5177\u88C5\u4E0D\u4E0A\uFF09\u3002\u4F60\u5206\u8BCA\uFF1A`,
45828
46435
  `- **\u56FE\u4E0A\u80FD\u4FEE\u7684**\uFF08\u7F3A\u4E0A\u6E38/\u8BE5\u8FDE\u6CA1\u8FDE\uFF09\u2192 \u5224\u65AD\u8FDE\u65E2\u6709\uFF08\u53BB\u91CD\uFF0C\u4F18\u5148\uFF09\u8FD8\u662F\u65B0\u5EFA\u2014\u2014\u6BD4\u5BF9\u4E0B\u9762\u73B0\u6709\u4EA7\u7269\u7684 brief\uFF1B\u62FF\u4E0D\u51C6\u5C31 \`oasis content <\u5019\u9009id>\` \u8BFB\u6B63\u6587\u3002`,
45829
- `- **\u56FE\u4E0A\u4FEE\u4E0D\u4E86\u7684**\uFF08\u8981\u4EBA\u62CD\u677F / \u7F3A\u73AF\u5883\u80FD\u529B\uFF09\u2192 \`oasis escalate ${gap.artifactId} --reason "<\u8981\u4EBA\u5B9A/\u505A\u4EC0\u4E48>"\`\uFF0C\u7CFB\u7EDF\u4F1A\u5347\u7EA7\u7ED9\u53D1\u8D77\u4EBA\u3001\u4F60\u4E4B\u540E\u53EF\u80FD\u5728\u5BF9\u8BDD\u91CC\u88AB ta \u95EE\u5230\uFF0C\u7136\u540E\u6536\u5DE5\u3002`,
46436
+ `- **\u56FE\u4E0A\u4FEE\u4E0D\u4E86\u7684**\uFF08\u8981\u4EBA\u62CD\u677F / \u7F3A\u73AF\u5883\u80FD\u529B\uFF09\u2192 \`${this.escalateCmd(gap, "<\u8981\u4EBA\u5B9A/\u505A\u4EC0\u4E48>")}\`\uFF0C\u7CFB\u7EDF\u4F1A\u5347\u7EA7\u7ED9\u53D1\u8D77\u4EBA\u3001\u4F60\u4E4B\u540E\u53EF\u80FD\u5728\u5BF9\u8BDD\u91CC\u88AB ta \u95EE\u5230\uFF0C\u7136\u540E\u6536\u5DE5\u3002`,
45830
46437
  ``,
45831
46438
  `## \u73B0\u6709\u4EA7\u7269\uFF08id / type / \u662F\u5426\u5DF2\u4EA4\u4ED8 / brief / \u4F9D\u8D56\uFF09`,
45832
46439
  this.structDigest(gap.artifactId),
@@ -45840,11 +46447,11 @@ ${SHARED_GRAPH_BODY}`;
45840
46447
  `- \u786E\u65E0\u5339\u914D\u3001\u65B0\u9700\u6C42 \u2192 \`oasis spawn --type <\u7C7B\u578B> --title "<\u77ED\u540D>" --brief "${gap.description}"\`\uFF08\u4E0D\u7528\u7ED9 id\uFF09+ \`oasis link ${gap.artifactId} --to <\u7C7B\u578B>:<\u77ED\u540D>\`\uFF08\u6309 type:title \u5F15\u521A\u5EFA\u7684\uFF09\u2192 apply\u3002`,
45841
46448
  `- **\u4FEE\u5B8C\u624D\u6536\u5C3E**\uFF1A\`oasis resolve-gap --gap ${gap.gapId} --note "<\u4F60\u505A\u4E86\u4EC0\u4E48 / owner \u8BE5\u600E\u4E48\u7EE7\u7EED>"\`\u2014\u2014**\u8FDE\u8FB9\u4E0D\u4F1A\u81EA\u52A8\u89E3 gap**\uFF0C\u4E0D\u53D1\u8FD9\u53E5 owner \u9192\u4E0D\u8FC7\u6765\uFF1Bnote \u4F1A\u539F\u6837\u7ED9 owner \u770B\uFF0C\u5199\u4EBA\u8BDD\u3002`,
45842
46449
  `**B. \u8981\u4EBA**\uFF08\u51B3\u7B56/\u80FD\u529B\u95EE\u9898\uFF0C\u56FE\u4E0A\u4FEE\u4E0D\u4E86\uFF09\uFF1A`,
45843
- `- \`oasis escalate ${gap.artifactId} --reason "<\u8981\u4EBA\u5B9A/\u505A\u4EC0\u4E48>"\` \u540E**\u76F4\u63A5\u6536\u5DE5**\u3002`,
46450
+ `- \`${this.escalateCmd(gap, "<\u8981\u4EBA\u5B9A/\u505A\u4EC0\u4E48>")}\` \u540E**\u76F4\u63A5\u6536\u5DE5**\u3002`,
45844
46451
  `- \u26A0\uFE0F **\u7EDD\u4E0D\u8981\u5BF9\u8FD9\u6761 gap \u8C03 resolve-gap**\u2014\u2014resolve-gap \u7684\u542B\u4E49\u662F"**\u9700\u6C42\u5DF2\u6EE1\u8DB3\u3001\u8BA9 owner \u590D\u5DE5**"\uFF0C\u4E0D\u662F"\u6211\u5206\u8BCA\u5B8C\u4E86"\u7684\u767B\u8BB0\u3002\u73B0\u5728\u9700\u6C42\u8FD8\u6CA1\u6EE1\u8DB3\uFF1Agap \u4FDD\u6301 open \u624D\u662F"\u8282\u70B9\u7EE7\u7EED\u7B49\u5F85"\u7684\u6B63\u786E\u72B6\u6001\uFF1B\u63D0\u524D resolve \u4F1A\u8BA9 owner \u5728\u5565\u90FD\u6CA1\u53D8\u7684\u60C5\u51B5\u4E0B\u88AB\u53EB\u9192\u767D\u8DD1\u3001\u8FD8\u4F1A\u628A\u4F60\u7684\u4E0A\u62A5\u4ECE\u4EBA\u7684\u6536\u4EF6\u7BB1\u91CC\u62B9\u6389\u3002\u4EBA\u7B54\u590D\u540E\uFF08\u591A\u534A\u5728\u5BF9\u8BDD\u91CC\u627E\u4F60\uFF09\u4F60\u518D\u4FEE\u56FE\u3001\u90A3\u65F6\u624D resolve-gap\u3002`,
45845
46452
  `**\u5176\u5B83**\uFF1A`,
45846
46453
  `- \u987A\u624B\u53D1\u73B0\u4E24\u4E2A\u4EA7\u7269\u91CD\u590D \u2192 **\u5148\u628A\u89E3 gap \u90A3\u7B14\uFF08\u542B resolve-gap\uFF09\u5355\u72EC\u6536\u6389**\uFF0C\u518D\u53E6\u8D77\u4E00\u7B14\uFF1A\`oasis edit\` \u6269\u4FDD\u7559\u4EF6 brief + \`oasis link\` \u6539\u8FDE\u6D88\u8D39\u8005 + \`oasis cancel\` \u5E9F\u5F03\u4EF6\uFF0C\u4E00\u8D77\u6682\u5B58\u518D apply\u3002`,
45847
- `- **\u62FF\u4E0D\u51C6\u4FEE\u6CD5 \u2192 \u4E00\u5F8B\u627E\u4EBA\uFF0C\u7EDD\u4E0D\u6C89\u9ED8\u9000\u51FA**\uFF1A\`oasis escalate ${gap.artifactId} --reason "<\u4F60\u5361\u5728\u54EA\u3001\u9700\u8981\u4EBA\u66FF\u4F60\u5B9A/\u505A\u4EC0\u4E48>"\` \u628A\u7591\u8651\u4E0A\u62A5\u7ED9\u53D1\u8D77\u4EBA\u2014\u2014\u7CFB\u7EDF\u4F1A\u767B\u8BB0\u672C\u6B21\u4F1A\u8BDD\uFF0C\u4EBA\u56DE\u8BDD\u65F6\u4F60\u88AB\u63A5\u7740\u5524\u8D77\u3001\u5E26\u7740\u4E0A\u4E0B\u6587\u7EE7\u7EED\u89E3\u3002**\u6CA1\u6709"\u8FD8\u4E0D\u5230\u8981\u4EBA\u7684\u7A0B\u5EA6"\u8FD9\u4E00\u6863\uFF1A\u6539\u4E0D\u52A8\u53C8\u62FF\u4E0D\u51C6\uFF0C\u5C31\u662F\u8981\u4EBA\u3002\u4EC0\u4E48\u90FD\u4E0D\u505A\u5730\u6536\u5DE5 = \u628A\u8282\u70B9\u6C38\u4E45\u667E\u6B7B\uFF0C\u6700\u7CDF\u3002**`
46454
+ `- **\u62FF\u4E0D\u51C6\u4FEE\u6CD5 \u2192 \u4E00\u5F8B\u627E\u4EBA\uFF0C\u7EDD\u4E0D\u6C89\u9ED8\u9000\u51FA**\uFF1A\`${this.escalateCmd(gap, "<\u4F60\u5361\u5728\u54EA\u3001\u9700\u8981\u4EBA\u66FF\u4F60\u5B9A/\u505A\u4EC0\u4E48>")}\` \u628A\u7591\u8651\u4E0A\u62A5\u7ED9\u53D1\u8D77\u4EBA\u2014\u2014\u7CFB\u7EDF\u4F1A\u767B\u8BB0\u672C\u6B21\u4F1A\u8BDD\uFF0C\u4EBA\u56DE\u8BDD\u65F6\u4F60\u88AB\u63A5\u7740\u5524\u8D77\u3001\u5E26\u7740\u4E0A\u4E0B\u6587\u7EE7\u7EED\u89E3\u3002**\u6CA1\u6709"\u8FD8\u4E0D\u5230\u8981\u4EBA\u7684\u7A0B\u5EA6"\u8FD9\u4E00\u6863\uFF1A\u6539\u4E0D\u52A8\u53C8\u62FF\u4E0D\u51C6\uFF0C\u5C31\u662F\u8981\u4EBA\u3002\u4EC0\u4E48\u90FD\u4E0D\u505A\u5730\u6536\u5DE5 = \u628A\u8282\u70B9\u6C38\u4E45\u667E\u6B7B\uFF0C\u6700\u7CDF\u3002**`
45848
46455
  ].join("\n");
45849
46456
  const subjectKey = `gap:${gap.gapId}`;
45850
46457
  const emptyCount = this.emptyHandedGap.get(subjectKey) ?? 0;
@@ -45854,7 +46461,7 @@ ${SHARED_GRAPH_BODY}`;
45854
46461
  `## \u26A0\uFE0F \u63A5\u4E0A\u4E00\u8F6E\u2014\u2014\u4F60\u4E0A\u6B21\u88AB\u5524\u8D77\u5374**\u7A7A\u624B\u9000\u51FA\u4E86**\uFF08\u65E2\u6CA1 resolve-gap \u4E5F\u6CA1 escalate\uFF09`,
45855
46462
  `\u8FD9\u6761\u7EBF\u8FD8\u5361\u7740\u3002\u8FD9\u4E00\u8F6E\u4F60**\u5FC5\u987B**\u4E8C\u9009\u4E00\u6536\u53E3\uFF0C\u4E0D\u8BB8\u518D\u7A7A\u624B\u9000\u51FA\uFF1A`,
45856
46463
  `- \u5DF2\u628A\u8FD9\u6761\u7EBF\u63A8\u52A8 / \u5224\u65AD\u8BE5 owner \u63A5\u624B \u2192 \`oasis resolve-gap --gap ${gap.gapId} --note "<\u4F60\u505A\u4E86\u4EC0\u4E48 / owner \u600E\u4E48\u63A5>"\``,
45857
- `- \u591F\u4E0D\u7740 / \u8981\u4EBA\u62CD\u677F \u2192 \`oasis escalate ${gap.artifactId} --reason "<\u6839\u56E0 + \u8981\u4EBA\u505A\u4EC0\u4E48>"\``,
46464
+ `- \u591F\u4E0D\u7740 / \u8981\u4EBA\u62CD\u677F \u2192 \`${this.escalateCmd(gap, "<\u6839\u56E0 + \u8981\u4EBA\u505A\u4EC0\u4E48>")}\``,
45858
46465
  `\u4E0D\u53D1\u8FD9\u4E24\u53E5\u4E4B\u4E00\uFF0Cgap \u6C38\u8FDC\u89E3\u4E0D\u5F00\u3001owner \u6C38\u8FDC\u9192\u4E0D\u8FC7\u6765\u3002**\u8FD9\u662F\u6700\u540E\u4E00\u6B21\u673A\u4F1A\u2014\u2014\u518D\u7A7A\u624B\uFF0C\u7CFB\u7EDF\u4F1A\u66FF\u4F60\u4E0A\u62A5\u7ED9\u4EBA\u3002**`
45859
46466
  ].join("\n");
45860
46467
  const resumeId = emptyCount > 0 ? this.emptyHandedSession.get(subjectKey) : void 0;
@@ -45877,7 +46484,9 @@ ${SHARED_GRAPH_BODY}`;
45877
46484
  await this.deps.kernel.escalate({
45878
46485
  artifactId: gap.artifactId,
45879
46486
  actor: run.managerActorId,
45880
- reason: `\u7BA1\u7406\u8005\u88AB\u7CFB\u7EDF\u5524\u8D77 ${n} \u8F6E\u4ECD\u672A\u6536\u53E3\uFF08\u65E2\u672A resolve-gap \u4E5F\u672A escalate\uFF09\u2014\u2014\u7CFB\u7EDF\u4EE3\u4E3A\u4E0A\u62A5\uFF0C\u8BF7\u4F60\u5904\u7406\u8FD9\u6761\u5361\u70B9\uFF1A${gap.description}`
46487
+ reason: `\u7BA1\u7406\u8005\u88AB\u7CFB\u7EDF\u5524\u8D77 ${n} \u8F6E\u4ECD\u672A\u6536\u53E3\uFF08\u65E2\u672A resolve-gap \u4E5F\u672A escalate\uFF09\u2014\u2014\u7CFB\u7EDF\u4EE3\u4E3A\u4E0A\u62A5\uFF0C\u8BF7\u4F60\u5904\u7406\u8FD9\u6761\u5361\u70B9\uFF1A${gap.description}`,
46488
+ gapId: gap.gapId,
46489
+ ...gap.part ? { part: gap.part } : {}
45881
46490
  }).catch((e) => this.deps.log?.(`[coordinator] ${gap.artifactId} \u81EA\u52A8 escalate \u5931\u8D25\uFF1A${String(e)}`));
45882
46491
  this.deps.log?.(`[coordinator] ${gap.artifactId} \u7BA1\u7406\u8005\u8FDE\u7EED ${n} \u8F6E\u7A7A\u624B \u2192 \u7CFB\u7EDF\u81EA\u52A8 escalate \u4EA4\u53D1\u8D77\u4EBA`);
45883
46492
  this.emptyHandedGap.delete(subjectKey);
@@ -46174,6 +46783,7 @@ var init_src5 = __esm({
46174
46783
  init_playbooks2();
46175
46784
  init_scheduler();
46176
46785
  init_daemon_adapter();
46786
+ init_node_health();
46177
46787
  init_side_map();
46178
46788
  init_engine();
46179
46789
  init_fs_state();
@@ -52524,6 +53134,7 @@ var PostgresRegistryStore = class _PostgresRegistryStore {
52524
53134
  name text NOT NULL,
52525
53135
  email text,
52526
53136
  team_id text,
53137
+ reports_to text, -- \u6C47\u62A5\u4EBA\uFF08\u8D1F\u8D23\u4EBA\uFF09\uFF1A\u53E6\u4E00\u4E2A actor id\uFF0C\u53EF\u7A7A
52527
53138
  status text NOT NULL, -- active | disabled\uFF08\u5220\u9664\u5373\u505C\u7528\uFF09
52528
53139
  roles jsonb NOT NULL DEFAULT '[]'::jsonb, -- spec \xA76.2 \u540D\u518C\u89D2\u8272\uFF08\u4EFB\u52A1 2.0\uFF09
52529
53140
  avatar text, -- "blob:<hash>"\uFF0C\u7A7A=\u9ED8\u8BA4\u751F\u6210
@@ -52549,6 +53160,7 @@ var PostgresRegistryStore = class _PostgresRegistryStore {
52549
53160
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS max_concurrent integer NOT NULL DEFAULT 4`);
52550
53161
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS reviewer_ids jsonb NOT NULL DEFAULT '[]'::jsonb`);
52551
53162
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS batched_continuation boolean NOT NULL DEFAULT false`);
53163
+ await pool.query(`ALTER TABLE "${s2}".actors ADD COLUMN IF NOT EXISTS reports_to text`);
52552
53164
  await pool.query(`
52553
53165
  CREATE TABLE IF NOT EXISTS "${s2}".teams (
52554
53166
  id text PRIMARY KEY,
@@ -52652,10 +53264,10 @@ var PostgresRegistryStore = class _PostgresRegistryStore {
52652
53264
  /* ---------- 员工 ---------- */
52653
53265
  async upsertActor(a) {
52654
53266
  await this.pool.query(
52655
- `INSERT INTO ${this.s}.actors (id, kind, name, email, team_id, status, roles, avatar, created_at)
52656
- VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9)
52657
- ON CONFLICT (id) DO UPDATE SET kind=$2, name=$3, email=$4, team_id=$5, status=$6, roles=$7::jsonb, avatar=$8`,
52658
- [a.id, a.kind, a.name, a.email ?? null, a.teamId ?? null, a.status, JSON.stringify(a.roles), a.avatar ?? null, a.createdAt]
53267
+ `INSERT INTO ${this.s}.actors (id, kind, name, email, team_id, status, roles, avatar, created_at, reports_to)
53268
+ VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9,$10)
53269
+ ON CONFLICT (id) DO UPDATE SET kind=$2, name=$3, email=$4, team_id=$5, status=$6, roles=$7::jsonb, avatar=$8, reports_to=$10`,
53270
+ [a.id, a.kind, a.name, a.email ?? null, a.teamId ?? null, a.status, JSON.stringify(a.roles), a.avatar ?? null, a.createdAt, a.reportsTo ?? null]
52659
53271
  );
52660
53272
  }
52661
53273
  async getActor(id) {
@@ -53014,6 +53626,7 @@ var rowToActor = (row) => ({
53014
53626
  name: row.name,
53015
53627
  ...row.email !== null ? { email: row.email } : {},
53016
53628
  ...row.team_id !== null ? { teamId: row.team_id } : {},
53629
+ ...row.reports_to !== null && row.reports_to !== void 0 ? { reportsTo: row.reports_to } : {},
53017
53630
  status: row.status,
53018
53631
  roles: row.roles ?? [],
53019
53632
  ...row.avatar !== null && row.avatar !== void 0 ? { avatar: row.avatar } : {},
@@ -54496,6 +55109,22 @@ var PostgresTraceStore = class _PostgresTraceStore {
54496
55109
  const nextCursor = hasMore ? page[page.length - 1]?.id ?? null : null;
54497
55110
  return { items: page, nextCursor };
54498
55111
  }
55112
+ async listUsageRuns(filter) {
55113
+ const r = await this.pool.query(
55114
+ `SELECT actor_id, artifact_id, started_at, effective_model, usage
55115
+ FROM ${this.s}.agent_runs
55116
+ WHERE jsonb_typeof(usage) = 'object' AND started_at >= $1 AND started_at <= $2
55117
+ ORDER BY started_at`,
55118
+ [filter.from, filter.to]
55119
+ );
55120
+ return r.rows.map((row) => ({
55121
+ actorId: row.actor_id,
55122
+ artifactId: row.artifact_id ?? null,
55123
+ startedAt: row.started_at,
55124
+ effectiveModel: row.effective_model ?? null,
55125
+ usage: row.usage
55126
+ }));
55127
+ }
54499
55128
  /* ---------- 事件流(单写者 seq + 幂等吸收)---------- */
54500
55129
  async appendEvent(runId, event) {
54501
55130
  return (await this.appendEvents(runId, [event]))[0];
@@ -56268,6 +56897,8 @@ async function startServe(opts) {
56268
56897
  let hub = null;
56269
56898
  let chatRemoteAdapter = null;
56270
56899
  let dispatchRemoteAdapter = null;
56900
+ const nodeHealth = new NodeHealthTracker();
56901
+ const activeRunCountOf = (nodeId) => (chatRemoteAdapter?.activeRunCount(nodeId) ?? 0) + (dispatchRemoteAdapter?.activeRunCount(nodeId) ?? 0);
56271
56902
  let localUrl = "";
56272
56903
  const chatLiveSessions = /* @__PURE__ */ new Map();
56273
56904
  const remoteApiUrl = opts.publicUrl ? normalizeHttpBaseUrl(opts.publicUrl) : apiUrlFromGatewayUrl(opts.gatewayUrl);
@@ -56409,11 +57040,14 @@ async function startServe(opts) {
56409
57040
  nodeStore,
56410
57041
  registry: registryStore,
56411
57042
  gatewayUrl: opts.gatewayUrl ?? `ws://127.0.0.1:${opts.port ?? 7320}/node-gateway`,
56412
- repoRemote: opts.nodeRepoRemote ?? "git@github.com:open-friday/oasis-core.git"
57043
+ repoRemote: opts.nodeRepoRemote ?? "git@github.com:open-friday/oasis-core.git",
57044
+ nodeHealth,
57045
+ activeRunCountOf
56413
57046
  }),
56414
57047
  collabDomain({
56415
57048
  kernel,
56416
57049
  oplog,
57050
+ trace: trace.service,
56417
57051
  registry: registryStore,
56418
57052
  artifacts: artifactStateStore,
56419
57053
  dispatchJournal: () => journalRing.slice(-200),
@@ -56777,7 +57411,7 @@ ${detail}` : genericLine));
56777
57411
  }
56778
57412
  });
56779
57413
  hub = new DaemonHub(server.httpServer, "/node-gateway", (t) => nodeTokens.resolve(t));
56780
- const hubAdapterOpts = { log: (m2) => console.warn(m2) };
57414
+ const hubAdapterOpts = { log: (m2) => console.warn(m2), health: nodeHealth };
56781
57415
  chatRemoteAdapter = new DaemonHubAdapter(hub, hubAdapterOpts);
56782
57416
  dispatchRemoteAdapter = new DaemonHubAdapter(hub, hubAdapterOpts);
56783
57417
  const port = new URL(server.baseUrl).port;
@@ -57012,6 +57646,7 @@ ${detail}` : genericLine));
57012
57646
  }
57013
57647
  const humanMs = opts.sla?.humanMs ?? 24 * 36e5;
57014
57648
  const agentMs = opts.sla?.agentMs ?? 10 * 6e4;
57649
+ const quotaStarvationMs = opts.sla?.quotaStarvationMs ?? 6 * 36e5;
57015
57650
  const initiatorCache = /* @__PURE__ */ new Map();
57016
57651
  const initiatorOf = async (workspace) => {
57017
57652
  if (initiatorCache.has(workspace)) return initiatorCache.get(workspace);
@@ -57084,13 +57719,29 @@ ${detail}` : genericLine));
57084
57719
  await escalate(a.id, a.owner, `human \u6301\u6709 ${Math.round(humanMs / 6e4)} \u5206\u949F\u672A\u52A8\u5DE5`);
57085
57720
  }
57086
57721
  }
57722
+ const escalateHold = async (artifactId, reason, kind) => {
57723
+ if (unresolvedEscalationsOf(kernel.model, artifactId).length > 0) return;
57724
+ if (isHeld(kernel.model, artifactId)) return;
57725
+ const owner = kernel.model.artifacts.get(artifactId)?.owner ?? "";
57726
+ try {
57727
+ await kernel.escalate({ artifactId, actor: SYSTEM_ACTOR, reason });
57728
+ await kernel.hold({ artifactId, actor: SYSTEM_ACTOR, source: "sla", reason });
57729
+ journal({ kind: "escalated", at: (/* @__PURE__ */ new Date()).toISOString(), artifactId, from: owner, to: owner, reason });
57730
+ console.log(`[sla] ${artifactId} ${kind}\uFF1A\u843D escalate + hold\uFF08owner \u4FDD\u6301 ${owner}\uFF0C\u4E0D\u593A\uFF09`);
57731
+ } catch (err) {
57732
+ console.error(`[sla] ${kind}\u5931\u8D25 ${artifactId}: ${String(err)}`);
57733
+ }
57734
+ };
57087
57735
  for (const f2 of allDispatchers().flatMap((d) => d.fusedJobs())) {
57088
57736
  if (f2.action !== "produce") continue;
57089
- const cur = kernel.model.artifacts.get(f2.artifactId);
57090
- if (!cur) continue;
57091
- if (now - Date.parse(f2.fusedAt) > agentMs) {
57092
- await escalate(f2.artifactId, cur.owner, `agent \u8FDE\u8D25\u7194\u65AD\u540E ${Math.round(agentMs / 6e4)} \u5206\u949F\u65E0\u4EBA\u5904\u7406`);
57093
- }
57737
+ if (!kernel.model.artifacts.get(f2.artifactId)) continue;
57738
+ if (now - Date.parse(f2.fusedAt) <= agentMs) continue;
57739
+ await escalateHold(f2.artifactId, `agent \u8FDE\u8D25\u7194\u65AD\u540E ${Math.round(agentMs / 6e4)} \u5206\u949F\u65E0\u4EBA\u5904\u7406\uFF08\u8FDE\u8D25 ${f2.consecutiveFailures} \u6B21\uFF09`, "\u7194\u65AD\u5347\u7EA7");
57740
+ }
57741
+ for (const s2 of allDispatchers().flatMap((d) => d.starvingJobs(quotaStarvationMs, now))) {
57742
+ if (s2.action !== "produce") continue;
57743
+ if (!kernel.model.artifacts.get(s2.artifactId)) continue;
57744
+ await escalateHold(s2.artifactId, `\u914D\u989D\u957F\u671F\u4E0D\u8DB3\uFF1A\u7ED1\u5B9A\u8D26\u53F7\u5DF2\u9650\u6D41 ${Math.round(s2.sinceMs / 36e5)} \u5C0F\u65F6\u4ECD\u672A\u6062\u590D\uFF08\u7591\u4F3C\u7B97\u529B\u7F3A\u53E3\uFF0C\u9700\u52A0\u989D\u5EA6/\u6362\u7ED1\u5B9A/\u51CF\u5E76\u53D1\uFF09`, "\u914D\u989D\u9965\u997F\u5347\u7EA7");
57094
57745
  }
57095
57746
  })().catch((err) => console.error(`[sla] \u626B\u63CF\u5931\u8D25: ${String(err)}`));
57096
57747
  }, opts.sla?.scanMs ?? 6e4);
@@ -58861,9 +59512,10 @@ ${res.warning}`);
58861
59512
  }
58862
59513
  case "escalate": {
58863
59514
  const { message } = await api.cmd("escalate", {
58864
- artifactId: needPos(positional, 0, "oasis escalate <artifactId> --reason t [--part p]"),
59515
+ artifactId: needPos(positional, 0, "oasis escalate <artifactId> --reason t [--gap id] [--part p]"),
58865
59516
  reason: need(flags, "reason"),
58866
- ...flags.get("part") !== void 0 ? { part: flags.get("part") } : {}
59517
+ ...flags.get("part") !== void 0 ? { part: flags.get("part") } : {},
59518
+ ...flags.get("gap") !== void 0 ? { gapId: flags.get("gap") } : {}
58867
59519
  });
58868
59520
  println(message);
58869
59521
  break;
@@ -58876,6 +59528,23 @@ ${res.warning}`);
58876
59528
  println(message);
58877
59529
  break;
58878
59530
  }
59531
+ case "hold": {
59532
+ const { message } = await api.cmd("hold", {
59533
+ artifactId: needPos(positional, 0, "oasis hold <artifactId> [--reason t]"),
59534
+ ...flags.get("reason") !== void 0 ? { reason: flags.get("reason") } : {},
59535
+ ...flags.get("source") !== void 0 ? { source: flags.get("source") } : {}
59536
+ });
59537
+ println(message);
59538
+ break;
59539
+ }
59540
+ case "release": {
59541
+ const { message } = await api.cmd("release", {
59542
+ artifactId: needPos(positional, 0, "oasis release <artifactId> [--reason t]"),
59543
+ ...flags.get("reason") !== void 0 ? { reason: flags.get("reason") } : {}
59544
+ });
59545
+ println(message);
59546
+ break;
59547
+ }
58879
59548
  case "add-revision": {
58880
59549
  const { message } = await api.cmd("addRevision", {
58881
59550
  artifactId: needPos(positional, 0, "oasis add-revision <artifactId> --desc <\u8981\u5B83\u5B8C\u6210\u4EC0\u4E48> [--role <\u8C01\u6765\u505A>]"),
@@ -59328,7 +59997,7 @@ ${res.warning}`);
59328
59997
  }
59329
59998
 
59330
59999
  // src/index.ts
59331
- var PKG_VERSION = true ? "0.1.53" : "dev";
60000
+ var PKG_VERSION = true ? "0.1.56" : "dev";
59332
60001
  var OASIS_DIR = path18.join(os9.homedir(), ".oasis");
59333
60002
  var CONFIG_FILE = path18.join(OASIS_DIR, "node-config.json");
59334
60003
  var PID_FILE = path18.join(OASIS_DIR, "node.pid");