oasis_test 0.1.123 → 0.1.125

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 +152 -15
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -174511,6 +174511,16 @@ var init_daemon_hub = __esm({
174511
174511
  * (2026-08-25 真机实测:chat 会话刚连上就收到 evict(unknown),用户看到「对话已中断」)。
174512
174512
  */
174513
174513
  sessionClaimCheckers = [];
174514
+ /**
174515
+ * 持久层认领(异步)。只在同步认领全 `unknown` 时才问——内存里没有,不代表系统里没有。
174516
+ * 与同步那圈同样是**多个**:chat 道与 dispatch 道各查各的账(对账键都不一样),汇总规则也同口径。
174517
+ */
174518
+ sessionClaimResolvers = [];
174519
+ /** 本 hub(≈本 serve 进程)起于何时——冷启动宽限的基准。 */
174520
+ startedAt = Date.now();
174521
+ coldStartGraceMs;
174522
+ /** 冷启动宽限期内被挂起、待重判的会话(键=dispatchId)。连接断开或重判跑完即清。 */
174523
+ sessionJudgeTimers = /* @__PURE__ */ new Map();
174514
174524
  resolveDataplane;
174515
174525
  /** 每个 daemon 最近一次收到任何帧的时间戳(含它自发的心跳 pong)——liveness 判定的依据。 */
174516
174526
  lastSeen = /* @__PURE__ */ new Map();
@@ -174558,6 +174568,7 @@ var init_daemon_hub = __esm({
174558
174568
  this.resolveNode = resolveNode;
174559
174569
  this.getNodeStatus = opts.getNodeStatus;
174560
174570
  this.resolveDataplane = opts.resolveDataplane;
174571
+ this.coldStartGraceMs = opts.coldStartGraceMs ?? 9e4;
174561
174572
  this.onHandshakeRejected = opts.onHandshakeRejected;
174562
174573
  this.throttle = new HandshakeThrottle(opts.throttle);
174563
174574
  this.wss = new import_websocket_server.default({
@@ -174718,9 +174729,14 @@ var init_daemon_hub = __esm({
174718
174729
  if (!registered) {
174719
174730
  registered = true;
174720
174731
  this.sessionSockets.set(dispatchId, ws);
174721
- this.sessionMeta.set(dispatchId, { nodeId, sessionId: msg.sessionId, connectedAt: Date.now() });
174732
+ this.sessionMeta.set(dispatchId, {
174733
+ nodeId,
174734
+ sessionId: msg.sessionId,
174735
+ connectedAt: Date.now(),
174736
+ ...msg.jobArtifactId ? { jobArtifactId: msg.jobArtifactId } : {}
174737
+ });
174722
174738
  for (const l of this.sessionConnectListeners) l({ dispatchId, sessionId: msg.sessionId, nodeId });
174723
- this.judgeSession(dispatchId);
174739
+ void this.judgeSession(dispatchId, ws);
174724
174740
  }
174725
174741
  return;
174726
174742
  }
@@ -174738,6 +174754,7 @@ var init_daemon_hub = __esm({
174738
174754
  if (this.sessionSockets.get(dispatchId) !== ws) return;
174739
174755
  this.sessionSockets.delete(dispatchId);
174740
174756
  this.sessionMeta.delete(dispatchId);
174757
+ this.clearJudgeTimer(dispatchId);
174741
174758
  for (const l of this.sessionDisconnectListeners) l({ dispatchId, nodeId });
174742
174759
  });
174743
174760
  ws.on("error", (err) => console.error(`[hub] session ${dispatchId} error:`, err.message));
@@ -174751,18 +174768,96 @@ var init_daemon_hub = __esm({
174751
174768
  registerSessionClaimCheck(fn) {
174752
174769
  this.sessionClaimCheckers.push(fn);
174753
174770
  }
174754
- /** 会话连上来时问一圈:没人说 live、且有人说 settled,才 evict。全 unknown = 没人认领,也 evict。 */
174755
- judgeSession(dispatchId) {
174756
- if (this.sessionClaimCheckers.length === 0) return;
174771
+ /**
174772
+ * 注册**持久层**认领(异步,唯一)。只在同步认领全 `unknown` 时才被问到。
174773
+ *
174774
+ * 存在的理由:同步那圈问的是 adapter 的内存在途表,serve 一重启就全空,于是所有活着的会话
174775
+ * 重连上来都是 unknown。会话自己已经把身份报全了(`session_hello`),缺的是服务端拿这份身份
174776
+ * 去查一次**持久事实**(运行账本 / 在跑的 chat 行)。查到在跑的,就该认领它、重新接上,
174777
+ * 而不是当野进程杀掉。
174778
+ */
174779
+ registerSessionClaimResolver(fn) {
174780
+ this.sessionClaimResolvers.push(fn);
174781
+ }
174782
+ /** 会话自报的身份(`session_hello` 已校验过与 token 一致)。连接已不在则返回 null。 */
174783
+ sessionIdentity(dispatchId) {
174784
+ const meta = this.sessionMeta.get(dispatchId);
174785
+ if (!meta) return null;
174786
+ return {
174787
+ dispatchId,
174788
+ sessionId: meta.sessionId,
174789
+ nodeId: meta.nodeId,
174790
+ ...meta.jobArtifactId ? { jobArtifactId: meta.jobArtifactId } : {}
174791
+ };
174792
+ }
174793
+ clearJudgeTimer(dispatchId) {
174794
+ const t = this.sessionJudgeTimers.get(dispatchId);
174795
+ if (t) {
174796
+ clearTimeout(t);
174797
+ this.sessionJudgeTimers.delete(dispatchId);
174798
+ }
174799
+ }
174800
+ /**
174801
+ * 会话连上来时判它该不该活。三级,从便宜到贵、从「我记得」到「我不知道」:
174802
+ *
174803
+ * ① 同步问各 adapter 的内存在途表:有人说 live → 放行;有人说 settled(我明确收过它)→ evict。
174804
+ * ② 全 unknown:**内存里没有 ≠ 系统里没有**。拿会话自报的身份去查持久层(运行账本 / chat 行),
174805
+ * 查到还在跑的就认领——这一格正是 serve 重启后所有活会话会落进来的地方。
174806
+ * ③ 持久层也说不知道:若还在冷启动宽限内,解读为「我还没恢复完」,挂起、到点重判一次;
174807
+ * 宽限外(或重判后仍无人认领)才 evict。
174808
+ *
174809
+ * 传 `ws` 是因为 ② 要 await:期间同一 dispatch 可能已重连、旧连接已被顶掉,此时这轮判定的
174810
+ * 结论对新连接无效,必须整段作废(与 close 里那道 `sessionSockets.get(...) !== ws` 同源)。
174811
+ */
174812
+ async judgeSession(dispatchId, ws, deferred = false) {
174813
+ if (this.sessionClaimCheckers.length === 0 && this.sessionClaimResolvers.length === 0) return;
174814
+ const identity = this.sessionIdentity(dispatchId);
174815
+ if (!identity) return;
174757
174816
  const verdicts = this.sessionClaimCheckers.map((f2) => {
174758
174817
  try {
174759
- return f2(dispatchId);
174818
+ return f2(identity);
174760
174819
  } catch {
174761
174820
  return "unknown";
174762
174821
  }
174763
174822
  });
174764
174823
  if (verdicts.includes("live")) return;
174765
- this.evictSession(dispatchId, verdicts.includes("settled") ? "reaped" : "unknown");
174824
+ if (verdicts.includes("settled")) {
174825
+ this.evictSession(dispatchId, "reaped");
174826
+ return;
174827
+ }
174828
+ if (this.sessionClaimResolvers.length > 0) {
174829
+ const resolved = await Promise.all(
174830
+ this.sessionClaimResolvers.map(async (f2) => {
174831
+ try {
174832
+ return await f2(identity);
174833
+ } catch (err) {
174834
+ console.warn(`[hub] \u4F1A\u8BDD\u8BA4\u9886\u67E5\u8BE2\u5931\u8D25 dispatch=${dispatchId}: ${String(err)}`);
174835
+ return "unknown";
174836
+ }
174837
+ })
174838
+ );
174839
+ if (this.sessionSockets.get(dispatchId) !== ws) return;
174840
+ if (resolved.includes("live")) return;
174841
+ if (resolved.includes("settled")) {
174842
+ this.evictSession(dispatchId, "reaped");
174843
+ return;
174844
+ }
174845
+ }
174846
+ const graceLeft = deferred ? 0 : this.coldStartGraceMs - (Date.now() - this.startedAt);
174847
+ if (graceLeft > 0) {
174848
+ if (this.sessionJudgeTimers.has(dispatchId)) return;
174849
+ console.warn(
174850
+ `[hub] \u4F1A\u8BDD dispatch=${dispatchId} \u6682\u65E0\u4EBA\u8BA4\u9886\uFF0C\u51B7\u542F\u52A8\u5BBD\u9650\u5185\u6302\u8D77 ${graceLeft}ms \u540E\u91CD\u5224\uFF08serve \u53EF\u80FD\u8FD8\u6CA1\u6062\u590D\u5B8C\uFF09`
174851
+ );
174852
+ const timer = setTimeout(() => {
174853
+ this.sessionJudgeTimers.delete(dispatchId);
174854
+ if (this.sessionSockets.get(dispatchId) === ws) void this.judgeSession(dispatchId, ws, true);
174855
+ }, graceLeft);
174856
+ timer.unref?.();
174857
+ this.sessionJudgeTimers.set(dispatchId, timer);
174858
+ return;
174859
+ }
174860
+ this.evictSession(dispatchId, "unknown");
174766
174861
  }
174767
174862
  /** 会话连接建立(`session_hello` 通过校验)时触发——2b 的收割判据挂在这上面。 */
174768
174863
  addSessionConnectListener(l) {
@@ -174887,6 +174982,8 @@ var init_daemon_hub = __esm({
174887
174982
  }
174888
174983
  for (const ws of this.sockets.values()) ws.close();
174889
174984
  for (const ws of this.sessionSockets.values()) ws.close();
174985
+ for (const t of this.sessionJudgeTimers.values()) clearTimeout(t);
174986
+ this.sessionJudgeTimers.clear();
174890
174987
  this.daemons.clear();
174891
174988
  this.sockets.clear();
174892
174989
  this.sessionSockets.clear();
@@ -190156,7 +190253,7 @@ var init_daemon_adapter = __esm({
190156
190253
  hub.addDisconnectListener((daemonId) => this.onNodeDown(daemonId));
190157
190254
  hub.addConnectListener((daemon) => this.onNodeUp(daemon));
190158
190255
  hub.addSessionConnectListener?.((i) => this.onSessionUp(i.dispatchId));
190159
- hub.registerSessionClaimCheck?.((dispatchId) => {
190256
+ hub.registerSessionClaimCheck?.(({ dispatchId }) => {
190160
190257
  const e = this.pending.get(dispatchId);
190161
190258
  if (e) return e.exited ? "settled" : "live";
190162
190259
  return this.settledDispatchIds.has(dispatchId) ? "settled" : "unknown";
@@ -201589,6 +201686,21 @@ async function startServe(opts) {
201589
201686
  console.log(`[chat-recovery] \u91CD\u6302 chat \u8F6E\uFF1Asession=${plan.chatSessionId} run=${plan.runId} node=${daemonId}`);
201590
201687
  }
201591
201688
  };
201689
+ hub.registerSessionClaimResolver?.(async (id) => {
201690
+ if (!id.jobArtifactId || !chatRemoteAdapter) return "unknown";
201691
+ const jobKey = `chat::${id.jobArtifactId}`;
201692
+ if (chatLiveSessions.has(jobKey)) return "live";
201693
+ await new Promise((resolve10) => {
201694
+ chatRecoveryChain = chatRecoveryChain.then(() => recoverChatTurns(id.nodeId, [
201695
+ { dispatchId: id.dispatchId, sessionId: id.sessionId, jobArtifactId: id.jobArtifactId }
201696
+ ])).catch((err) => {
201697
+ console.error(`[chat-recovery] session_hello \u8BA4\u9886\u5931\u8D25: ${String(err)}`);
201698
+ }).then(() => {
201699
+ resolve10();
201700
+ });
201701
+ });
201702
+ return chatLiveSessions.has(jobKey) ? "live" : "unknown";
201703
+ });
201592
201704
  hub.addMessageListener((daemonId, msg) => {
201593
201705
  if (msg.type !== "gc_query_artifacts") return;
201594
201706
  void (async () => {
@@ -202434,7 +202546,8 @@ async function startServe(opts) {
202434
202546
  }
202435
202547
  }
202436
202548
  const recoverActiveSessions = async (daemonId, activeSessions) => {
202437
- if (!dispatchRemoteAdapter) return;
202549
+ if (!dispatchRemoteAdapter) return 0;
202550
+ let restoredCount = 0;
202438
202551
  const { plans, zombies } = await planSessionRecoveryWithZombies(activeSessions, (id) => traceStore.getRun(id));
202439
202552
  for (const z of zombies) {
202440
202553
  console.warn(
@@ -202493,14 +202606,28 @@ async function startServe(opts) {
202493
202606
  reason: "server-restart"
202494
202607
  });
202495
202608
  console.log(`[dispatch:${companyId}] recovered ${plan.jobKey} from node ${daemonId} session=${plan.sessionId ?? plan.dispatchId}`);
202609
+ restoredCount += 1;
202496
202610
  }
202497
202611
  }
202612
+ return restoredCount;
202498
202613
  };
202499
202614
  hub.addMessageListener((daemonId, msg) => {
202500
202615
  if (msg.type === "hello" && msg.meta.activeSessions?.length) {
202501
202616
  void recoverActiveSessions(daemonId, msg.meta.activeSessions);
202502
202617
  }
202503
202618
  });
202619
+ hub.registerSessionClaimResolver?.(async (id) => {
202620
+ if (!dispatchRemoteAdapter) return "unknown";
202621
+ const run = await traceStore.getRun(id.dispatchId).catch(() => null);
202622
+ if (!run) return "unknown";
202623
+ const restored = await recoverActiveSessions(id.nodeId, [{
202624
+ dispatchId: id.dispatchId,
202625
+ sessionId: id.sessionId,
202626
+ ...id.jobArtifactId ? { jobArtifactId: id.jobArtifactId } : {}
202627
+ }]);
202628
+ if (restored > 0) return "live";
202629
+ return run.status === "running" ? "unknown" : "settled";
202630
+ });
202504
202631
  for (const daemon of hub.connectedDaemons()) {
202505
202632
  if (daemon.meta.activeSessions?.length) void recoverActiveSessions(daemon.daemonId, daemon.meta.activeSessions);
202506
202633
  }
@@ -203422,6 +203549,10 @@ var SessionProcessState = class {
203422
203549
  this.spec = spec;
203423
203550
  this.adapter = adapter;
203424
203551
  this.lock = lock;
203552
+ const o = spec.limits ?? {};
203553
+ this.hardTtlMs = o.hardTtlMs ?? SESSION_LIMITS.hardTtlMs;
203554
+ this.suicideAfterUnreachableMs = o.suicideAfterUnreachableMs ?? SESSION_LIMITS.suicideAfterUnreachableMs;
203555
+ this.watchdogIntervalMs = o.watchdogIntervalMs ?? 3e4;
203425
203556
  }
203426
203557
  ws = null;
203427
203558
  connected = false;
@@ -203431,6 +203562,9 @@ var SessionProcessState = class {
203431
203562
  reconnectMs = RECONNECT_MIN_MS;
203432
203563
  lastConnectedAt = Date.now();
203433
203564
  startedAt = Date.now();
203565
+ hardTtlMs;
203566
+ suicideAfterUnreachableMs;
203567
+ watchdogIntervalMs;
203434
203568
  watchdog = null;
203435
203569
  exitAckResolve = null;
203436
203570
  /** 整个会话进程「可以退了」的信号:会话正常退出、或被 evict / TTL / 失联自尽。 */
@@ -203502,7 +203636,10 @@ var SessionProcessState = class {
203502
203636
  sessionId: this.handle?.id ?? this.spec.dispatchId,
203503
203637
  dispatchId: this.spec.dispatchId,
203504
203638
  nodeId: this.spec.nodeId,
203505
- daemonVersion: this.spec.daemonVersion
203639
+ daemonVersion: this.spec.daemonVersion,
203640
+ // 自报身份要够服务端对上账:chat 道的对账键是 artifactId(run.id=chat-run:* ≠ dispatchId),
203641
+ // 只报 dispatchId 的话 serve 重启后它认不出这是条活会话,会当成野进程 evict 掉。
203642
+ ...this.spec.job.artifactId ? { jobArtifactId: this.spec.job.artifactId } : {}
203506
203643
  });
203507
203644
  for (const m2 of this.outbox) this.rawSend(m2);
203508
203645
  });
@@ -203571,16 +203708,16 @@ var SessionProcessState = class {
203571
203708
  startWatchdog() {
203572
203709
  this.watchdog = setInterval(() => {
203573
203710
  const now = Date.now();
203574
- if (now - this.startedAt >= SESSION_LIMITS.hardTtlMs) {
203711
+ if (now - this.startedAt >= this.hardTtlMs) {
203575
203712
  log2("[session]", `${this.spec.dispatchId}: \u8FBE\u786C\u4E0A\u9650 TTL\uFF0C\u81EA\u9000`);
203576
203713
  void this.selfTerminate("ttl");
203577
203714
  return;
203578
203715
  }
203579
- if (!this.connected && now - this.lastConnectedAt >= SESSION_LIMITS.suicideAfterUnreachableMs) {
203580
- log2("[session]", `${this.spec.dispatchId}: \u5931\u8054\u8D85\u8FC7 ${SESSION_LIMITS.suicideAfterUnreachableMs}ms\uFF0C\u81EA\u5C3D\uFF08\u9632\u53CC\u8DD1\uFF09`);
203716
+ if (!this.connected && now - this.lastConnectedAt >= this.suicideAfterUnreachableMs) {
203717
+ log2("[session]", `${this.spec.dispatchId}: \u5931\u8054\u8D85\u8FC7 ${this.suicideAfterUnreachableMs}ms\uFF0C\u81EA\u5C3D\uFF08\u9632\u53CC\u8DD1\uFF09`);
203581
203718
  void this.selfTerminate("unreachable");
203582
203719
  }
203583
- }, 3e4);
203720
+ }, this.watchdogIntervalMs);
203584
203721
  this.watchdog.unref?.();
203585
203722
  }
203586
203723
  async selfTerminate(reason) {
@@ -209717,7 +209854,7 @@ function shimScript() {
209717
209854
  }
209718
209855
 
209719
209856
  // src/index.ts
209720
- var PKG_VERSION = true ? "0.1.123" : "dev";
209857
+ var PKG_VERSION = true ? "0.1.125" : "dev";
209721
209858
  var LOCAL_BIN = localBin();
209722
209859
  var NPM_PREFIX = npmPrefix();
209723
209860
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.123",
3
+ "version": "0.1.125",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "77b69695eb0d4289690e802db5743a48444488fd"
29
+ "sourceHead": "3c7ad5a874aa215ba7cee828b8cab4ab0a499e87"
30
30
  }
31
31
  }