oasis_test 0.1.118 → 0.1.119

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 +178 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4801,6 +4801,11 @@ function createPolicy(cfg) {
4801
4801
  }
4802
4802
  };
4803
4803
  }
4804
+ function policyGetter(source, fallback = CODE_DEFAULT_POLICY) {
4805
+ if (typeof source === "function") return source;
4806
+ if (source) return () => source;
4807
+ return () => fallback;
4808
+ }
4804
4809
  var MINUTE, HOUR, DAY, AGENT_WORK_DEADLINE_MS, AGENT_REVIEW_DEADLINE_MS, AGENT_REVIEW_MAX_RETRIES, AGENT_WORK_MAX_RETRIES, RETRY_BASE_MS, RUNTIME_MAX_RETRIES, DEFAULT_IDLE_BY_RUNTIME, DEFAULT_IDLE_MS, DEFAULT_WALL_CLOCK_BY_RUNTIME, DEFAULT_WALL_CLOCK_MS, DEFAULT_HUMAN_CADENCE, CODE_DEFAULT_POLICY;
4805
4810
  var init_policy = __esm({
4806
4811
  "../timeout-policy/src/policy.ts"() {
@@ -7571,6 +7576,10 @@ function inputsMatch(state, node, latestWork) {
7571
7576
  const latestInputs = new Set(state.workInputsOf(latestWork.id).map((i) => i.upstreamWorkId));
7572
7577
  return currInputs.size === latestInputs.size && [...currInputs].every((x2) => latestInputs.has(x2));
7573
7578
  }
7579
+ function reviewMaxRetriesOf(policy) {
7580
+ const act = policy.resolve({ executor: "agent", scenario: "review" }).timeoutAction;
7581
+ return act.kind === "kill-rebuild" ? act.maxRetries : AGENT_REVIEW_MAX_RETRIES;
7582
+ }
7574
7583
  function processReviews(state, nodeId, workId, ctx, events) {
7575
7584
  const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
7576
7585
  const reqs = state.requirementsOf(nodeId);
@@ -7620,7 +7629,7 @@ function processReviews(state, nodeId, workId, ctx, events) {
7620
7629
  }
7621
7630
  } else if (info.status === "failed") {
7622
7631
  const req = reqs.find((r) => r.reviewGroup === g2);
7623
- const overLimit = countFailedForReviewGroup(g2, reviews) > AGENT_REVIEW_MAX_RETRIES;
7632
+ const overLimit = countFailedForReviewGroup(g2, reviews) > reviewMaxRetriesOf(policy);
7624
7633
  if (overLimit && classify(req.reviewerActorId) === "agent") {
7625
7634
  const lrId = req.latestReviewId;
7626
7635
  if (lrId) events.push({
@@ -15427,7 +15436,8 @@ var init_bus = __esm({
15427
15436
  io;
15428
15437
  clock;
15429
15438
  config;
15430
- policy;
15439
+ /** 取当前策略(不是策略本身)——见 {@link EngineBusOptions.policy} 的热载不变量。 */
15440
+ policyOf;
15431
15441
  registry;
15432
15442
  maxEffectAttempts;
15433
15443
  stuckAppliedRecoveryMs;
@@ -15439,7 +15449,7 @@ var init_bus = __esm({
15439
15449
  this.io = opts.io ?? NOOP_IO;
15440
15450
  this.clock = opts.clock ?? SYSTEM_CLOCK;
15441
15451
  this.config = opts.config ?? DEFAULT_CONFIG;
15442
- this.policy = opts.policy ?? CODE_DEFAULT_POLICY;
15452
+ this.policyOf = policyGetter(opts.policy);
15443
15453
  this.registry = new HandlerRegistry(opts.handlers);
15444
15454
  this.maxEffectAttempts = opts.maxEffectAttempts ?? 3;
15445
15455
  this.stuckAppliedRecoveryMs = opts.stuckAppliedRecoveryMs ?? DEFAULT_STUCK_APPLIED_RECOVERY_MS;
@@ -15828,7 +15838,7 @@ var init_bus = __esm({
15828
15838
  if (!snapshot) return { emitted: 0, terminated: false };
15829
15839
  if (snapshot.workorder.lifecycle !== "active") return { emitted: 0, terminated: false };
15830
15840
  const state = new WorkorderState(snapshot);
15831
- const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR2, config: this.config, policy: this.policy };
15841
+ const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR2, config: this.config, policy: this.policyOf() };
15832
15842
  const { events, terminated } = scan(state, scanCtx);
15833
15843
  await tx.applyMutations(workorderId, state.mutations);
15834
15844
  const allEvents = [...state.emitted, ...events];
@@ -128738,13 +128748,13 @@ ${block}
128738
128748
  throw err;
128739
128749
  }
128740
128750
  const delay = base * Math.pow(3, attempt - 1);
128741
- yield sleep4(delay);
128751
+ yield sleep5(delay);
128742
128752
  }
128743
128753
  }
128744
128754
  throw lastErr;
128745
128755
  });
128746
128756
  }
128747
- function sleep4(ms) {
128757
+ function sleep5(ms) {
128748
128758
  return new Promise((r) => setTimeout(r, ms));
128749
128759
  }
128750
128760
  var DEFAULT_CHUNK_LIMIT = 3500;
@@ -173922,7 +173932,9 @@ async function openNewEngine(opts) {
173922
173932
  const bus = new EngineBus({
173923
173933
  store,
173924
173934
  io: NOOP_IO,
173925
- clock: SYSTEM_CLOCK
173935
+ clock: SYSTEM_CLOCK,
173936
+ // 原样交给 bus(bus 内部归一成取值函数)——这里绝不能 call 一次存下来,那就把热载掐死了。
173937
+ policy: opts.policy
173926
173938
  });
173927
173939
  const kernel = await EngineKernel.open(store, bus, {
173928
173940
  schema: opts.schemaDefs ?? [],
@@ -198389,7 +198401,10 @@ async function startServe(opts) {
198389
198401
  schemaDefs: schema,
198390
198402
  registry: kernelRegistry,
198391
198403
  reworkWavefront,
198392
- convergenceUnified
198404
+ convergenceUnified,
198405
+ // ★ ADR 0147:引擎 scan 的 work/review 超时与重试上限走策略中心(与 dispatcher 墙钟、
198406
+ // waiting-inbox 催办同源)。thunk 而非实例——随 SIGHUP / POST /api/policy/reload 热载而变。
198407
+ policy: () => activeTimeoutPolicy
198393
198408
  });
198394
198409
  const kernel = newEngine.kernel;
198395
198410
  const dispatchLedger = await PostgresDispatchStore.open(pgPool, pgSchema);
@@ -198998,7 +199013,9 @@ async function startServe(opts) {
198998
199013
  schemaDefs: schema,
198999
199014
  registry: kernelRegistry,
199000
199015
  reworkWavefront,
199001
- convergenceUnified
199016
+ convergenceUnified,
199017
+ // ★ 同默认公司:漏了这里,非默认公司的引擎照旧按代码默认 30min 判超时(策略文件形同虚设)。
199018
+ policy: () => activeTimeoutPolicy
199002
199019
  });
199003
199020
  await ensureCompanyActors(controlPlaneStore, companyId, registryStore);
199004
199021
  await syncRegistryRolesToKernel(registryStore, engine2.kernel);
@@ -201950,6 +201967,9 @@ var PENDING_STREAM_MAX = 2e3;
201950
201967
  var AUTH_REJECT_BACKOFF_MAX_MS = 15 * 6e4;
201951
201968
  var AUTH_REJECT_GIVE_UP_AFTER = 3;
201952
201969
  var AUTH_REJECT_RE = /Unexpected server response:\s*(401|403)/;
201970
+ var DRAIN_TIMEOUT_MS = 15e3;
201971
+ var DRAIN_POLL_MS = 50;
201972
+ var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
201953
201973
  async function preflightOasisAccess(job) {
201954
201974
  const base = job.server.url.replace(/\/+$/, "");
201955
201975
  try {
@@ -201994,6 +202014,8 @@ var DaemonWsClient = class {
201994
202014
  stopped = false;
201995
202015
  /** 收到 update 但有活跃会话时置真:延后到所有会话结束再自更新,避免打断执行中的 agent。 */
201996
202016
  pendingUpdate = false;
202017
+ /** 停机排空中(见 {@link drain}):不再接新派发、不再触发延后的自更新。 */
202018
+ draining = false;
201997
202019
  /** 已回 dispatch_received、尚未完成 runtime spawn 的派发;也随 hello 上报供重连对账。 */
201998
202020
  pendingDispatchIds = /* @__PURE__ */ new Set();
201999
202021
  /** setup 期间收到 kill 的派发。spawn 返回后先杀句柄,不得再宣告 session_started。 */
@@ -202050,6 +202072,73 @@ var DaemonWsClient = class {
202050
202072
  if (this.pingTimer) clearInterval(this.pingTimer);
202051
202073
  this.ws?.close();
202052
202074
  }
202075
+ /** 排空中?(观测/测试用) */
202076
+ isDraining() {
202077
+ return this.draining;
202078
+ }
202079
+ /**
202080
+ * 停机排空——在进程退出前,把「在途会话已经死了」这件事告诉服务端。
202081
+ *
202082
+ * ## 为什么必须有这一步(2026-08-25 生产实证)
202083
+ *
202084
+ * 引擎里一条 work 从 `running` 变成别的状态,全后端只有三个入口:收到 `session_exited` 帧走主路
202085
+ * (`serve.ts` 的 work.response 桥)、同一帧走 journal 反查的兜底、以及 scan 里 **30 分钟**的
202086
+ * `work.timeout`。前两个都要求**这一帧真的被发出来**。
202087
+ *
202088
+ * 而这一帧的生成者,是本进程 dispatch 分支里注册的那个 `handle.onExit` 回调——**它住在 daemon
202089
+ * 进程内部**。子进程被杀不会自动产生任何帧;进程一死,回调就再没机会跑。
202090
+ *
202091
+ * 旧行为:SIGTERM handler 只删 pid 文件就 `process.exit(0)`;systemd unit 又没设 `KillMode`,
202092
+ * 默认 `control-group` 把 daemon 和它拉起的所有会话**同时** SIGTERM——父子同死,一帧都发不出去。
202093
+ * 实测 node-6745008c1fc7:5389 次会话起跑里 48 个没有配对退出帧,其中 43 个(90%)是重启带走的;
202094
+ * 每一个都让对应的 work 空转到 30 分钟超时才被判死,控制台全程显示"运行中"。
202095
+ *
202096
+ * ## 做法:让真实退出帧走正路,只有两类才合成
202097
+ *
202098
+ * 不图省事直接合成假帧——`kill()` 掉在途会话,adapter 的 `killReason` 归因成 `cancelled`,既有
202099
+ * `onExit` 正路会带着 `transcriptRef` 把**真实**帧发出去。只有两类兜底才合成:还没 spawn 的派发
202100
+ * (没有子进程可 kill,不可能有真实帧),以及超时后仍未退出的会话(宁可给服务端一帧不精确的死讯,
202101
+ * 也好过让它等满 30 分钟)。
202102
+ *
202103
+ * 排空期间 `draining` 置真:不再接新派发(见 dispatch 分支),也不再触发延后的自更新。
202104
+ */
202105
+ async drain(reason, timeoutMs = DRAIN_TIMEOUT_MS) {
202106
+ if (this.draining) return;
202107
+ this.draining = true;
202108
+ const active = this.sessions.activeSessions();
202109
+ const pending = [...this.pendingDispatchIds];
202110
+ if (active.length === 0 && pending.length === 0) {
202111
+ this.stop();
202112
+ return;
202113
+ }
202114
+ log2("[node-cli]", `\u505C\u673A\u6392\u7A7A\uFF08${reason}\uFF09\uFF1A\u5728\u9014\u4F1A\u8BDD ${active.length} \u4E2A\u3001\u5F85\u8D77\u6D3E\u53D1 ${pending.length} \u4E2A\u2014\u2014\u53D1\u5B8C\u9000\u51FA\u5E27\u518D\u9000\u51FA`);
202115
+ for (const dispatchId of pending) {
202116
+ this.pendingDispatchIds.delete(dispatchId);
202117
+ this.sendKeyFrame(dispatchId, {
202118
+ type: "session_exited",
202119
+ dispatchId,
202120
+ sessionId: "node-shutdown",
202121
+ info: { code: null, reason: "cancelled", errorMessage: `\u8282\u70B9 daemon \u505C\u673A\uFF08${reason}\uFF09\uFF1A\u6D3E\u53D1\u5C1A\u672A\u8D77\u4F1A\u8BDD` }
202122
+ });
202123
+ }
202124
+ await Promise.all(active.map((s2) => this.sessions.kill(s2.dispatchId).catch(() => void 0)));
202125
+ const deadline = Date.now() + timeoutMs;
202126
+ while (Date.now() < deadline && (this.sessions.activeCount() > 0 || this.outbox.size > 0)) await sleep4(DRAIN_POLL_MS);
202127
+ const stuck = this.sessions.activeSessions();
202128
+ for (const s2 of stuck) {
202129
+ log2("[node-cli]", ` \u26A0 \u4F1A\u8BDD ${s2.sessionId} \u5728 ${timeoutMs}ms \u5185\u6CA1\u9000\u51FA\uFF0C\u6309\u505C\u673A\u5408\u6210\u9000\u51FA\u5E27`);
202130
+ this.sendKeyFrame(s2.dispatchId, {
202131
+ type: "session_exited",
202132
+ dispatchId: s2.dispatchId,
202133
+ sessionId: s2.sessionId,
202134
+ info: { code: null, reason: "cancelled", errorMessage: `\u8282\u70B9 daemon \u505C\u673A\uFF08${reason}\uFF09\uFF1A\u4F1A\u8BDD\u672A\u5728\u6392\u7A7A\u7A97\u53E3\u5185\u9000\u51FA` }
202135
+ });
202136
+ }
202137
+ if (stuck.length > 0) await sleep4(DRAIN_POLL_MS * 4);
202138
+ if (this.outbox.size > 0) log2("[node-cli]", `\u26A0 \u505C\u673A\u6392\u7A7A\u7ED3\u675F\uFF1A\u4ECD\u6709 ${this.outbox.size} \u4E2A\u9000\u51FA\u5E27\u672A\u786E\u8BA4\u9001\u8FBE\uFF08\u8FD9\u4E9B work \u4F1A\u9000\u56DE 30 \u5206\u949F\u8D85\u65F6\u515C\u5E95\uFF09`);
202139
+ else log2("[node-cli]", "\u505C\u673A\u6392\u7A7A\u5B8C\u6210\uFF1A\u5728\u9014\u4F1A\u8BDD\u7684\u9000\u51FA\u5E27\u5747\u5DF2\u786E\u8BA4\u9001\u8FBE");
202140
+ this.stop();
202141
+ }
202053
202142
  connect() {
202054
202143
  if (this.stopped) return;
202055
202144
  const ws = new import_websocket.default(this.serverUrl, {
@@ -202126,6 +202215,16 @@ var DaemonWsClient = class {
202126
202215
  const { dispatchId, job } = msg;
202127
202216
  const t0 = Date.now();
202128
202217
  log2("[node-cli]", `\u2190 dispatch ${dispatchId} artifact=${job.artifactId}`);
202218
+ if (this.draining) {
202219
+ log2("[node-cli]", ` \u62D2\u6536\uFF1A\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A \u2192 \u56DE\u9000\u51FA\u5E27\uFF0C\u670D\u52A1\u7AEF\u53EF\u7ACB\u5373\u6539\u6D3E`);
202220
+ this.sendKeyFrame(dispatchId, {
202221
+ type: "session_exited",
202222
+ dispatchId,
202223
+ sessionId: "node-draining",
202224
+ info: { code: null, reason: "cancelled", errorMessage: "\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A\uFF0C\u672C\u6B21\u6D3E\u53D1\u672A\u8D77\u4F1A\u8BDD" }
202225
+ });
202226
+ break;
202227
+ }
202129
202228
  if (this.pendingDispatchIds.has(dispatchId) || this.sessions.get(dispatchId)) {
202130
202229
  this.send({ type: "dispatch_received", dispatchId });
202131
202230
  break;
@@ -202243,6 +202342,10 @@ var DaemonWsClient = class {
202243
202342
  log2("[node-cli]", "\u2190 update \u5FFD\u7565\uFF1A\u672C\u8FDB\u7A0B\u672A\u6CE8\u5165\u81EA\u66F4\u65B0\u903B\u8F91\uFF08\u975E node-daemon \u6258\u7BA1\uFF1F\uFF09");
202244
202343
  break;
202245
202344
  }
202345
+ if (this.draining) {
202346
+ log2("[node-cli]", "\u2190 update \u5FFD\u7565\uFF1A\u672C\u8FDB\u7A0B\u6B63\u5728\u505C\u673A\u6392\u7A7A");
202347
+ break;
202348
+ }
202246
202349
  const active = this.sessions.activeCount();
202247
202350
  if (active > 0) {
202248
202351
  this.pendingUpdate = true;
@@ -202330,6 +202433,7 @@ var DaemonWsClient = class {
202330
202433
  }
202331
202434
  /** 会话结束后调用:若之前收到过 update 但因忙延后了,且现已空闲,则触发自更新。 */
202332
202435
  maybeRunPendingUpdate() {
202436
+ if (this.draining) return;
202333
202437
  if (this.pendingUpdate && this.sessions.activeCount() === 0) {
202334
202438
  this.pendingUpdate = false;
202335
202439
  log2("[node-cli]", "\u6240\u6709\u4F1A\u8BDD\u5DF2\u7ED3\u675F,\u6267\u884C\u5EF6\u540E\u7684\u66F4\u65B0\uFF1A\u62C9\u53D6\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F");
@@ -202493,6 +202597,7 @@ async function startNode(opts) {
202493
202597
  });
202494
202598
  console.log(`[oasis node] ${opts.nodeId} \u2192 ${opts.serverUrl}`);
202495
202599
  client.start();
202600
+ opts.onReady?.({ drain: (reason) => client.drain(reason) });
202496
202601
  const { workRoot, legacyRoot } = resolveWorkRoots(opts.workRoot);
202497
202602
  const workRootIsEphemeral = workRoot.startsWith(import_node_os11.default.tmpdir());
202498
202603
  if (opts.gcEnabled === false && !workRootIsEphemeral) {
@@ -207460,6 +207565,37 @@ function gcVersions(versionsDirPath, opts) {
207460
207565
  }
207461
207566
  return deleted;
207462
207567
  }
207568
+ function systemdUnitText(o) {
207569
+ return [
207570
+ "[Unit]",
207571
+ `Description=Oasis Node Daemon (${o.name})`,
207572
+ "After=network.target",
207573
+ "",
207574
+ // OASIS_CLI_HOME 由 daemon 继承给它 spawn 的 agent 会话——非 default 实例的会话因此
207575
+ // 用的是**本实例**的 CLI 版本,而不是 PATH 上那个默认指向 default 的 shim。
207576
+ // (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
207577
+ "[Service]",
207578
+ `ExecStart=${o.cmd}`,
207579
+ `Environment=PATH=${o.daemonPath}`,
207580
+ `Environment=OASIS_CLI_HOME=${o.cliHome}`,
207581
+ `StandardOutput=append:${o.logFile}`,
207582
+ `StandardError=append:${o.logFile}`,
207583
+ "Restart=on-failure",
207584
+ "RestartSec=5",
207585
+ // 🚨 停机时只 SIGTERM 主进程,子会话留给 daemon 自己收——它要先给每个在途会话发 session_exited,
207586
+ // 那是服务端唯一的死亡通知渠道(生成者是 daemon 进程内的 onExit 回调)。
207587
+ // systemd 默认的 KillMode=control-group 会把 daemon 和它拉起的所有 claude 会话**同时**杀掉:
207588
+ // 父子同死,一帧都发不出去,对应的 work 只能空转到 scan 的 30 分钟超时才被判死。
207589
+ // 实测占本机孤儿会话的 90%(43/48)。详见 change-record 20260825-daemon-restart-swallows-exit-frames。
207590
+ // TimeoutStopSec 必须明显大于排空预算 DRAIN_TIMEOUT_MS(15s),否则排空没做完就被 SIGKILL。
207591
+ "KillMode=mixed",
207592
+ "TimeoutStopSec=30",
207593
+ "",
207594
+ "[Install]",
207595
+ "WantedBy=default.target",
207596
+ ""
207597
+ ].join("\n");
207598
+ }
207463
207599
  function shimScript() {
207464
207600
  return [
207465
207601
  "#!/bin/sh",
@@ -207472,7 +207608,7 @@ function shimScript() {
207472
207608
  }
207473
207609
 
207474
207610
  // src/index.ts
207475
- var PKG_VERSION = true ? "0.1.118" : "dev";
207611
+ var PKG_VERSION = true ? "0.1.119" : "dev";
207476
207612
  var LOCAL_BIN = localBin();
207477
207613
  var NPM_PREFIX = npmPrefix();
207478
207614
  var INSTANCE = DEFAULT_INSTANCE;
@@ -207586,27 +207722,13 @@ function setupAutostart(name) {
207586
207722
  fs38.mkdirSync(unitDir, { recursive: true });
207587
207723
  const unit = systemdUnitName(name);
207588
207724
  fs38.mkdirSync(path32.dirname(inst.logFile), { recursive: true });
207589
- fs38.writeFileSync(path32.join(unitDir, unit), [
207590
- "[Unit]",
207591
- `Description=Oasis Node Daemon (${name})`,
207592
- "After=network.target",
207593
- "",
207594
- // OASIS_CLI_HOME 由 daemon 继承给它 spawn 的 agent 会话——非 default 实例的会话因此
207595
- // 用的是**本实例**的 CLI 版本,而不是 PATH 上那个默认指向 default 的 shim。
207596
- // (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
207597
- "[Service]",
207598
- `ExecStart=${cmd}`,
207599
- `Environment=PATH=${daemonPath}`,
207600
- `Environment=OASIS_CLI_HOME=${inst.currentLink}`,
207601
- `StandardOutput=append:${inst.logFile}`,
207602
- `StandardError=append:${inst.logFile}`,
207603
- "Restart=on-failure",
207604
- "RestartSec=5",
207605
- "",
207606
- "[Install]",
207607
- "WantedBy=default.target",
207608
- ""
207609
- ].join("\n"));
207725
+ fs38.writeFileSync(path32.join(unitDir, unit), systemdUnitText({
207726
+ name,
207727
+ cmd,
207728
+ daemonPath,
207729
+ cliHome: inst.currentLink,
207730
+ logFile: inst.logFile
207731
+ }));
207610
207732
  if (name === DEFAULT_INSTANCE) {
207611
207733
  try {
207612
207734
  (0, import_node_child_process19.execSync)(`systemctl --user disable --now ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
@@ -207876,19 +207998,32 @@ void (async () => {
207876
207998
  }
207877
207999
  const cleanup = () => {
207878
208000
  try {
207879
- fs38.unlinkSync(PID_FILE);
208001
+ if (fs38.readFileSync(PID_FILE, "utf8").trim() === String(process.pid)) fs38.unlinkSync(PID_FILE);
207880
208002
  } catch {
207881
208003
  }
207882
208004
  };
207883
208005
  process.on("exit", cleanup);
207884
- process.on("SIGTERM", () => {
207885
- cleanup();
207886
- process.exit(0);
207887
- });
207888
- process.on("SIGINT", () => {
207889
- cleanup();
207890
- process.exit(0);
207891
- });
208006
+ let drainNode = null;
208007
+ let shuttingDown = false;
208008
+ const shutdown = (signal) => {
208009
+ if (shuttingDown) {
208010
+ console.error(`[oasis] \u6536\u5230\u7B2C\u4E8C\u6B21 ${signal}\uFF1A\u653E\u5F03\u6392\u7A7A\uFF0C\u7ACB\u5373\u9000\u51FA`);
208011
+ cleanup();
208012
+ process.exit(0);
208013
+ }
208014
+ shuttingDown = true;
208015
+ void (async () => {
208016
+ try {
208017
+ await drainNode?.(signal);
208018
+ } catch (e) {
208019
+ console.error("[oasis] \u505C\u673A\u6392\u7A7A\u5931\u8D25\uFF08\u5728\u9014\u4F1A\u8BDD\u7684\u9000\u51FA\u5E27\u53EF\u80FD\u6CA1\u53D1\u51FA\u53BB\uFF09:", String(e));
208020
+ }
208021
+ cleanup();
208022
+ process.exit(0);
208023
+ })();
208024
+ };
208025
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
208026
+ process.on("SIGINT", () => shutdown("SIGINT"));
207892
208027
  const cleanPath = sanitizeDaemonPath(process.env["PATH"]);
207893
208028
  if (cleanPath !== process.env["PATH"]) {
207894
208029
  console.error(`[oasis] daemon PATH \u5DF2\u6E05\u6D17\uFF1A\u5254\u9664 agent \u4F1A\u8BDD\u4E34\u65F6 wrapper \u76EE\u5F55 ${(process.env["PATH"] ?? "").split(":").length - cleanPath.split(":").length} \u6761`);
@@ -207928,6 +208063,9 @@ void (async () => {
207928
208063
  } catch (e) {
207929
208064
  console.error("[oasis] self-update failed:", String(e));
207930
208065
  }
208066
+ },
208067
+ onReady: (ctl) => {
208068
+ drainNode = ctl.drain;
207931
208069
  }
207932
208070
  });
207933
208071
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.118",
3
+ "version": "0.1.119",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"