oasis_test 0.1.54 → 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 +276 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4014,7 +4014,10 @@ function computeReviewJobs(model) {
4014
4014
  }
4015
4015
  return out;
4016
4016
  }
4017
- 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;
4018
4021
  var init_dispatcher = __esm({
4019
4022
  "../core/src/dispatcher.ts"() {
4020
4023
  "use strict";
@@ -4032,6 +4035,7 @@ var init_dispatcher = __esm({
4032
4035
  }
4033
4036
  };
4034
4037
  SEED_ROOT_SUCCESSFUL_PRODUCE_LIMIT = 5;
4038
+ SELF_HEALING_EXITS = /* @__PURE__ */ new Set(["rate-limit"]);
4035
4039
  CANVAS_RUNTIME_KIND = "open-design";
4036
4040
  NON_PRODUCE_FALLBACK_RUNTIME_KIND = "claude-code";
4037
4041
  Dispatcher = class {
@@ -4076,6 +4080,20 @@ var init_dispatcher = __esm({
4076
4080
  fusedAt: st.fusedAt
4077
4081
  }));
4078
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
+ }
4079
4097
  /** v1 调度准入:当前被并发上限挡下、等待重评的 produce 会话数(/api/view dispatches 表面化)。 */
4080
4098
  queuedCount() {
4081
4099
  return this.queued.size;
@@ -4315,19 +4333,22 @@ var init_dispatcher = __esm({
4315
4333
  }
4316
4334
  recordSpawnFailure(jobKey, spec, reason) {
4317
4335
  const seqAtVerdict = this.opts.kernel.model.lastSeq.get(spec.artifactId) ?? 0;
4318
- const consecutive = (this.strikes.get(jobKey)?.count ?? 0) + 1;
4336
+ const prev = this.strikes.get(jobKey);
4337
+ const consecutive = (prev?.count ?? 0) + 1;
4319
4338
  const max = this.opts.backoff?.maxConsecutiveFailures ?? 3;
4320
4339
  const base = this.opts.backoff?.baseMs ?? 3e4;
4321
- const fused = consecutive >= max;
4340
+ const cap = this.opts.backoff?.maxBackoffMs ?? 30 * 6e4;
4341
+ const fused = consecutive >= max && !isSelfHealingExit(reason);
4322
4342
  this.strikes.set(jobKey, {
4323
4343
  artifactId: spec.artifactId,
4324
4344
  actor: spec.actor,
4325
4345
  action: spec.action,
4326
4346
  count: consecutive,
4327
- nextEligibleAt: Date.now() + base * 2 ** (consecutive - 1),
4347
+ nextEligibleAt: Date.now() + Math.min(base * 2 ** (consecutive - 1), cap),
4328
4348
  fused,
4329
4349
  ...fused ? { fusedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
4330
4350
  seqAtVerdict,
4351
+ firstFailedAt: prev?.firstFailedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4331
4352
  lastReason: reason
4332
4353
  });
4333
4354
  if (fused) {
@@ -4401,7 +4422,7 @@ var init_dispatcher = __esm({
4401
4422
  }
4402
4423
  }
4403
4424
  const retry = this.strikes.get(jobKey);
4404
- 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;
4405
4426
  const ws = kernel.model.artifacts.get(spec.artifactId)?.workspace;
4406
4427
  const codeRepo = ws ? await this.opts.resolveCodeRepo?.(ws) ?? null : null;
4407
4428
  this.assertSpawnAttemptCurrent(jobKey, attempt);
@@ -4588,19 +4609,23 @@ var init_dispatcher = __esm({
4588
4609
  if (progressed) {
4589
4610
  this.strikes.delete(jobKey);
4590
4611
  } else {
4591
- consecutive = (this.strikes.get(jobKey)?.count ?? 0) + 1;
4612
+ const prev = this.strikes.get(jobKey);
4613
+ consecutive = (prev?.count ?? 0) + 1;
4592
4614
  const max = this.opts.backoff?.maxConsecutiveFailures ?? 3;
4593
4615
  const base = this.opts.backoff?.baseMs ?? 3e4;
4594
- const fused = consecutive >= max;
4616
+ const cap = this.opts.backoff?.maxBackoffMs ?? 30 * 6e4;
4617
+ const fused = consecutive >= max && !isSelfHealingExit(info.reason);
4595
4618
  this.strikes.set(jobKey, {
4596
4619
  artifactId: spec.artifactId,
4597
4620
  actor: spec.actor,
4598
4621
  action: spec.action,
4599
4622
  count: consecutive,
4600
- nextEligibleAt: Date.now() + base * 2 ** (consecutive - 1),
4623
+ // 退避封顶(maxBackoffMs,默认 30min):自愈类不熔断→consecutive 可无界增长,不封顶则间隔爆炸。
4624
+ nextEligibleAt: Date.now() + Math.min(base * 2 ** (consecutive - 1), cap),
4601
4625
  fused,
4602
4626
  ...fused ? { fusedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
4603
4627
  seqAtVerdict: seqAtExit,
4628
+ firstFailedAt: prev?.firstFailedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4604
4629
  ...info.reason ? { lastReason: info.reason } : {}
4605
4630
  });
4606
4631
  if (fused) {
@@ -22454,6 +22479,82 @@ var init_node_state = __esm({
22454
22479
  }
22455
22480
  });
22456
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
+
22457
22558
  // ../server/src/coordinator/identity.ts
22458
22559
  function isCoordinatorAgentName(name) {
22459
22560
  return COORDINATOR_NAME_RE.test(name);
@@ -23105,6 +23206,11 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
23105
23206
  ...e.upstreamCancelled ? { upstreamCancelled: true } : {}
23106
23207
  })) : [];
23107
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;
23108
23214
  return {
23109
23215
  id: a.id,
23110
23216
  type: a.type,
@@ -23119,12 +23225,35 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
23119
23225
  ...runtime ? { runtime } : {},
23120
23226
  ...pv ? { reviewers: pv.reviewers.map((r) => ({ actor: ref(r.actor), source: r.source })), quorum: pv.quorum, typeName: pv.typeName } : {},
23121
23227
  // 过期事实(提案 rework-cascade-rollout):无条件透出,前端 DAG 挂「上游已更新」徽标。
23122
- ...outdated.length > 0 ? { outdated } : {}
23228
+ ...outdated.length > 0 ? { outdated } : {},
23229
+ // 停滞四态(看得清):为什么不动 + 谁欠什么 + 去处理锚;null 时不带。
23230
+ ...stall ? { stall } : {}
23123
23231
  };
23124
23232
  });
23125
23233
  const edges = arts.flatMap(
23126
23234
  (a) => a.inputs.map((e) => ({ from: a.id, to: e.to, pinned: e.pinned, required: e.required }))
23127
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
+ })();
23128
23257
  const activity = buildActivity(model, arts, ref);
23129
23258
  const coordinatorActivity = buildCoordinatorActivity(arts, ops, ref);
23130
23259
  const acceptance = buildAcceptance(model, arts);
@@ -23140,6 +23269,7 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
23140
23269
  acceptance,
23141
23270
  ...managerActorId ? { managerActorId } : {},
23142
23271
  ...spec?.acceptanceCriteria ? { acceptanceCriteria: spec.acceptanceCriteria } : {},
23272
+ ...stallSummary ? { stallSummary } : {},
23143
23273
  planning
23144
23274
  };
23145
23275
  }
@@ -23435,6 +23565,7 @@ var init_workorder_detail = __esm({
23435
23565
  "use strict";
23436
23566
  init_src2();
23437
23567
  init_node_state();
23568
+ init_stall();
23438
23569
  init_workorders();
23439
23570
  ACTIVITY_NOTE_MAX_CHARS = 500;
23440
23571
  COORDINATOR_EDIT_KINDS = /* @__PURE__ */ new Set([
@@ -26795,7 +26926,7 @@ var init_memory_trace_store = __esm({
26795
26926
  return { items: page.map((r) => this.decorate(structuredClone(r))), nextCursor };
26796
26927
  }
26797
26928
  async listUsageRuns(filter) {
26798
- return [...this.runs.values()].filter((run) => run.usage !== null && run.startedAt >= filter.from && run.startedAt <= filter.to).map((run) => ({
26929
+ return [...this.runs.values()].filter((run) => run.usage != null && run.startedAt >= filter.from && run.startedAt <= filter.to).map((run) => ({
26799
26930
  actorId: run.actorId,
26800
26931
  artifactId: run.artifactId,
26801
26932
  startedAt: run.startedAt,
@@ -33839,6 +33970,10 @@ var init_daemon_hub = __esm({
33839
33970
  connectedDaemons() {
33840
33971
  return [...this.daemons.values()];
33841
33972
  }
33973
+ /** 最近一帧(含 pong 心跳)的时刻(ms)——节点心跳时钟,供节点健康派生判"心跳新鲜"。缺 = 未连接/已清。 */
33974
+ lastSeenAt(daemonId) {
33975
+ return this.lastSeen.get(daemonId);
33976
+ }
33842
33977
  /** Nodes that disconnected within the last GHOST_TTL_MS ms. */
33843
33978
  recentlyDisconnected() {
33844
33979
  const cutoff = Date.now() - _DaemonHub.GHOST_TTL_MS;
@@ -35528,6 +35663,7 @@ function actorsDomain(opts) {
35528
35663
  status: "active",
35529
35664
  ...b2.email !== void 0 ? { email: b2.email } : {},
35530
35665
  ...b2.teamId !== void 0 ? { teamId: b2.teamId } : {},
35666
+ ...b2.reportsTo ? { reportsTo: b2.reportsTo } : {},
35531
35667
  ...avatar !== void 0 ? { avatar } : {},
35532
35668
  ...positions !== void 0 ? { roles: positions } : {}
35533
35669
  },
@@ -35542,10 +35678,12 @@ function actorsDomain(opts) {
35542
35678
  const b2 = req.body ?? {};
35543
35679
  const positions = positionsFromPayload(b2);
35544
35680
  const avatar = avatarRefFromPayload(b2);
35545
- 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;
35546
35682
  const actor = await service.upsertActor({
35547
35683
  ...existing,
35548
35684
  ...patch,
35685
+ // 汇报人:显式给(含清空)——传空串/null 归一为 undefined(写库落 null);未传则保留原值
35686
+ ...b2.reportsTo !== void 0 ? { reportsTo: b2.reportsTo || void 0 } : {},
35549
35687
  ...avatar !== void 0 ? { avatar } : {},
35550
35688
  ...positions !== void 0 ? { roles: positions } : {},
35551
35689
  id: existing.id,
@@ -36380,12 +36518,12 @@ function parseAcceptanceCriteria(body) {
36380
36518
  }
36381
36519
  function parseReviewActor(req) {
36382
36520
  const actor = String(req.auth.actor);
36383
- const shortId = actor.split(":").slice(2).join(":") || actor;
36521
+ const shortId2 = actor.split(":").slice(2).join(":") || actor;
36384
36522
  const b2 = req.body ?? {};
36385
36523
  return {
36386
36524
  actorKind: actor.includes(":agent:") ? "agent" : "human",
36387
36525
  actorId: actor,
36388
- ...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 }
36389
36527
  };
36390
36528
  }
36391
36529
  function parseAnnotationActor(req) {
@@ -41403,6 +41541,63 @@ var init_connect_script = __esm({
41403
41541
  }
41404
41542
  });
41405
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
+
41406
41601
  // ../server/src/domains/nodes/routes.ts
41407
41602
  function nodesDomain(deps) {
41408
41603
  let hubWired = false;
@@ -41486,6 +41681,8 @@ function nodesDomain(deps) {
41486
41681
  }
41487
41682
  const daemonVersionById = new Map((hub?.connectedDaemons() ?? []).map((d) => [d.daemonId, d.meta.daemonVersion]));
41488
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();
41489
41686
  const nodes = await deps.nodeStore.listNodes();
41490
41687
  const items = await Promise.all(nodes.map(async (n) => {
41491
41688
  const rts = await deps.nodeStore.listRuntimes(n.id);
@@ -41514,6 +41711,15 @@ function nodesDomain(deps) {
41514
41711
  ...connection.lastDisconnectedAt ? { lastDisconnectedAt: new Date(connection.lastDisconnectedAt).toISOString() } : {},
41515
41712
  ...connection.lastReconnectedAt ? { lastReconnectedAt: new Date(connection.lastReconnectedAt).toISOString() } : {}
41516
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
+ })
41517
41723
  } : {}
41518
41724
  };
41519
41725
  }));
@@ -41712,6 +41918,7 @@ var init_routes4 = __esm({
41712
41918
  import_node_path2 = require("node:path");
41713
41919
  init_src4();
41714
41920
  init_connect_script();
41921
+ init_node_health();
41715
41922
  }
41716
41923
  });
41717
41924
 
@@ -43123,7 +43330,7 @@ var init_service4 = __esm({
43123
43330
  /** 组织汇总专用瘦读:只取归属、时间和 usage,并统一补齐展示成本。 */
43124
43331
  async listUsageRuns(filter) {
43125
43332
  const rows = await this.store.listUsageRuns(filter);
43126
- return rows.map((row) => ({
43333
+ return rows.filter((row) => row.usage != null).map((row) => ({
43127
43334
  ...row,
43128
43335
  usage: {
43129
43336
  ...row.usage,
@@ -45205,6 +45412,7 @@ var init_daemon_adapter = __esm({
45205
45412
  this.nodeLostGraceMs = opts.nodeLostGraceMs ?? graceFromEnv(12e4);
45206
45413
  this.ackTimeoutMs = opts.ackTimeoutMs ?? ackMsFromEnv(15e3);
45207
45414
  this.log = opts.log ?? ((m2) => console.warn(m2));
45415
+ this.health = opts.health;
45208
45416
  hub.addMessageListener((_daemonId, msg) => this.onDaemonMessage(msg));
45209
45417
  hub.addDisconnectListener((daemonId) => this.onNodeDown(daemonId));
45210
45418
  hub.addConnectListener((daemon) => this.onNodeUp(daemon.daemonId));
@@ -45213,6 +45421,7 @@ var init_daemon_adapter = __esm({
45213
45421
  nodeLostGraceMs;
45214
45422
  ackTimeoutMs;
45215
45423
  log;
45424
+ health;
45216
45425
  /** 断联节点 → 收割定时器。重连取消;到期收割。 */
45217
45426
  reapTimers = /* @__PURE__ */ new Map();
45218
45427
  /** 统一结算一次会话退出:清 ack 定时器、置 exited、触发回调、从 pending 摘除。幂等(已退出即跳过)。 */
@@ -45224,9 +45433,17 @@ var init_daemon_adapter = __esm({
45224
45433
  entry.ackTimer = void 0;
45225
45434
  }
45226
45435
  entry.exited = info;
45436
+ if (info.reason === "server-unreachable") this.health?.recordUnreachable(entry.nodeId, info.reason);
45437
+ else this.health?.recordReachable(entry.nodeId);
45227
45438
  for (const cb of entry.exitCbs.splice(0)) cb(info);
45228
45439
  this.pending.delete(dispatchId);
45229
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
+ }
45230
45447
  /** 收到任何回帧(started/event/output/exited)即视为派发已达,取消 ack 超时。 */
45231
45448
  cancelAck(dispatchId) {
45232
45449
  const entry = this.pending.get(dispatchId);
@@ -45269,6 +45486,8 @@ var init_daemon_adapter = __esm({
45269
45486
  onDaemonMessage(msg) {
45270
45487
  if (msg.type === "session_started") {
45271
45488
  this.cancelAck(msg.dispatchId);
45489
+ const entry = this.pending.get(msg.dispatchId);
45490
+ if (entry) this.health?.recordReachable(entry.nodeId);
45272
45491
  } else if (msg.type === "session_exited") {
45273
45492
  const info = msg.info.reason || msg.info.code !== null ? msg.info : { ...msg.info, reason: "error" };
45274
45493
  this.settle(msg.dispatchId, info);
@@ -45769,6 +45988,12 @@ var init_ci_gate = __esm({
45769
45988
  const statuses = [];
45770
45989
  const repoViews = [];
45771
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
+ }
45772
45997
  const pr = await ci.ensurePr(
45773
45998
  { repo: unit.repo, branch: unit.branch, sha: unit.sha },
45774
45999
  { title: `[oasis-ci] ${artifactId} \u2192 ${unit.repo.develop}`, body: `CI gate for artifact ${artifactId} @ ${unit.sha}` }
@@ -46558,6 +46783,7 @@ var init_src5 = __esm({
46558
46783
  init_playbooks2();
46559
46784
  init_scheduler();
46560
46785
  init_daemon_adapter();
46786
+ init_node_health();
46561
46787
  init_side_map();
46562
46788
  init_engine();
46563
46789
  init_fs_state();
@@ -52908,6 +53134,7 @@ var PostgresRegistryStore = class _PostgresRegistryStore {
52908
53134
  name text NOT NULL,
52909
53135
  email text,
52910
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
52911
53138
  status text NOT NULL, -- active | disabled\uFF08\u5220\u9664\u5373\u505C\u7528\uFF09
52912
53139
  roles jsonb NOT NULL DEFAULT '[]'::jsonb, -- spec \xA76.2 \u540D\u518C\u89D2\u8272\uFF08\u4EFB\u52A1 2.0\uFF09
52913
53140
  avatar text, -- "blob:<hash>"\uFF0C\u7A7A=\u9ED8\u8BA4\u751F\u6210
@@ -52933,6 +53160,7 @@ var PostgresRegistryStore = class _PostgresRegistryStore {
52933
53160
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS max_concurrent integer NOT NULL DEFAULT 4`);
52934
53161
  await pool.query(`ALTER TABLE "${s2}".actor_configs ADD COLUMN IF NOT EXISTS reviewer_ids jsonb NOT NULL DEFAULT '[]'::jsonb`);
52935
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`);
52936
53164
  await pool.query(`
52937
53165
  CREATE TABLE IF NOT EXISTS "${s2}".teams (
52938
53166
  id text PRIMARY KEY,
@@ -53036,10 +53264,10 @@ var PostgresRegistryStore = class _PostgresRegistryStore {
53036
53264
  /* ---------- 员工 ---------- */
53037
53265
  async upsertActor(a) {
53038
53266
  await this.pool.query(
53039
- `INSERT INTO ${this.s}.actors (id, kind, name, email, team_id, status, roles, avatar, created_at)
53040
- VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9)
53041
- ON CONFLICT (id) DO UPDATE SET kind=$2, name=$3, email=$4, team_id=$5, status=$6, roles=$7::jsonb, avatar=$8`,
53042
- [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]
53043
53271
  );
53044
53272
  }
53045
53273
  async getActor(id) {
@@ -53398,6 +53626,7 @@ var rowToActor = (row) => ({
53398
53626
  name: row.name,
53399
53627
  ...row.email !== null ? { email: row.email } : {},
53400
53628
  ...row.team_id !== null ? { teamId: row.team_id } : {},
53629
+ ...row.reports_to !== null && row.reports_to !== void 0 ? { reportsTo: row.reports_to } : {},
53401
53630
  status: row.status,
53402
53631
  roles: row.roles ?? [],
53403
53632
  ...row.avatar !== null && row.avatar !== void 0 ? { avatar: row.avatar } : {},
@@ -54884,7 +55113,7 @@ var PostgresTraceStore = class _PostgresTraceStore {
54884
55113
  const r = await this.pool.query(
54885
55114
  `SELECT actor_id, artifact_id, started_at, effective_model, usage
54886
55115
  FROM ${this.s}.agent_runs
54887
- WHERE usage IS NOT NULL AND started_at >= $1 AND started_at <= $2
55116
+ WHERE jsonb_typeof(usage) = 'object' AND started_at >= $1 AND started_at <= $2
54888
55117
  ORDER BY started_at`,
54889
55118
  [filter.from, filter.to]
54890
55119
  );
@@ -56668,6 +56897,8 @@ async function startServe(opts) {
56668
56897
  let hub = null;
56669
56898
  let chatRemoteAdapter = null;
56670
56899
  let dispatchRemoteAdapter = null;
56900
+ const nodeHealth = new NodeHealthTracker();
56901
+ const activeRunCountOf = (nodeId) => (chatRemoteAdapter?.activeRunCount(nodeId) ?? 0) + (dispatchRemoteAdapter?.activeRunCount(nodeId) ?? 0);
56671
56902
  let localUrl = "";
56672
56903
  const chatLiveSessions = /* @__PURE__ */ new Map();
56673
56904
  const remoteApiUrl = opts.publicUrl ? normalizeHttpBaseUrl(opts.publicUrl) : apiUrlFromGatewayUrl(opts.gatewayUrl);
@@ -56809,7 +57040,9 @@ async function startServe(opts) {
56809
57040
  nodeStore,
56810
57041
  registry: registryStore,
56811
57042
  gatewayUrl: opts.gatewayUrl ?? `ws://127.0.0.1:${opts.port ?? 7320}/node-gateway`,
56812
- 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
56813
57046
  }),
56814
57047
  collabDomain({
56815
57048
  kernel,
@@ -57178,7 +57411,7 @@ ${detail}` : genericLine));
57178
57411
  }
57179
57412
  });
57180
57413
  hub = new DaemonHub(server.httpServer, "/node-gateway", (t) => nodeTokens.resolve(t));
57181
- const hubAdapterOpts = { log: (m2) => console.warn(m2) };
57414
+ const hubAdapterOpts = { log: (m2) => console.warn(m2), health: nodeHealth };
57182
57415
  chatRemoteAdapter = new DaemonHubAdapter(hub, hubAdapterOpts);
57183
57416
  dispatchRemoteAdapter = new DaemonHubAdapter(hub, hubAdapterOpts);
57184
57417
  const port = new URL(server.baseUrl).port;
@@ -57413,6 +57646,7 @@ ${detail}` : genericLine));
57413
57646
  }
57414
57647
  const humanMs = opts.sla?.humanMs ?? 24 * 36e5;
57415
57648
  const agentMs = opts.sla?.agentMs ?? 10 * 6e4;
57649
+ const quotaStarvationMs = opts.sla?.quotaStarvationMs ?? 6 * 36e5;
57416
57650
  const initiatorCache = /* @__PURE__ */ new Map();
57417
57651
  const initiatorOf = async (workspace) => {
57418
57652
  if (initiatorCache.has(workspace)) return initiatorCache.get(workspace);
@@ -57485,22 +57719,29 @@ ${detail}` : genericLine));
57485
57719
  await escalate(a.id, a.owner, `human \u6301\u6709 ${Math.round(humanMs / 6e4)} \u5206\u949F\u672A\u52A8\u5DE5`);
57486
57720
  }
57487
57721
  }
57488
- for (const f2 of allDispatchers().flatMap((d) => d.fusedJobs())) {
57489
- if (f2.action !== "produce") continue;
57490
- const cur = kernel.model.artifacts.get(f2.artifactId);
57491
- if (!cur) continue;
57492
- if (now - Date.parse(f2.fusedAt) <= agentMs) continue;
57493
- if (unresolvedEscalationsOf(kernel.model, f2.artifactId).length > 0) continue;
57494
- if (isHeld(kernel.model, f2.artifactId)) continue;
57495
- const reason = `agent \u8FDE\u8D25\u7194\u65AD\u540E ${Math.round(agentMs / 6e4)} \u5206\u949F\u65E0\u4EBA\u5904\u7406\uFF08\u8FDE\u8D25 ${f2.consecutiveFailures} \u6B21\uFF09`;
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 ?? "";
57496
57726
  try {
57497
- await kernel.escalate({ artifactId: f2.artifactId, actor: SYSTEM_ACTOR, reason });
57498
- await kernel.hold({ artifactId: f2.artifactId, actor: SYSTEM_ACTOR, source: "sla", reason });
57499
- journal({ kind: "escalated", at: (/* @__PURE__ */ new Date()).toISOString(), artifactId: f2.artifactId, from: cur.owner, to: cur.owner, reason });
57500
- console.log(`[sla] ${f2.artifactId} \u7194\u65AD\u5347\u7EA7\uFF1A\u843D escalate + hold\uFF08owner \u4FDD\u6301 ${cur.owner}\uFF0C\u4E0D\u593A\uFF09`);
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`);
57501
57731
  } catch (err) {
57502
- console.error(`[sla] \u7194\u65AD\u5347\u7EA7\u5931\u8D25 ${f2.artifactId}: ${String(err)}`);
57732
+ console.error(`[sla] ${kind}\u5931\u8D25 ${artifactId}: ${String(err)}`);
57503
57733
  }
57734
+ };
57735
+ for (const f2 of allDispatchers().flatMap((d) => d.fusedJobs())) {
57736
+ if (f2.action !== "produce") continue;
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");
57504
57745
  }
57505
57746
  })().catch((err) => console.error(`[sla] \u626B\u63CF\u5931\u8D25: ${String(err)}`));
57506
57747
  }, opts.sla?.scanMs ?? 6e4);
@@ -59756,7 +59997,7 @@ ${res.warning}`);
59756
59997
  }
59757
59998
 
59758
59999
  // src/index.ts
59759
- var PKG_VERSION = true ? "0.1.54" : "dev";
60000
+ var PKG_VERSION = true ? "0.1.56" : "dev";
59760
60001
  var OASIS_DIR = path18.join(os9.homedir(), ".oasis");
59761
60002
  var CONFIG_FILE = path18.join(OASIS_DIR, "node-config.json");
59762
60003
  var PID_FILE = path18.join(OASIS_DIR, "node.pid");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"