oasis_test 0.1.124 → 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 +141 -11
  2. package/package.json +1 -1
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
  }
@@ -203509,7 +203636,10 @@ var SessionProcessState = class {
203509
203636
  sessionId: this.handle?.id ?? this.spec.dispatchId,
203510
203637
  dispatchId: this.spec.dispatchId,
203511
203638
  nodeId: this.spec.nodeId,
203512
- 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 } : {}
203513
203643
  });
203514
203644
  for (const m2 of this.outbox) this.rawSend(m2);
203515
203645
  });
@@ -209724,7 +209854,7 @@ function shimScript() {
209724
209854
  }
209725
209855
 
209726
209856
  // src/index.ts
209727
- var PKG_VERSION = true ? "0.1.124" : "dev";
209857
+ var PKG_VERSION = true ? "0.1.125" : "dev";
209728
209858
  var LOCAL_BIN = localBin();
209729
209859
  var NPM_PREFIX = npmPrefix();
209730
209860
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.124",
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"