oasis_test 0.1.137 → 0.1.139

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 +599 -135
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -719,11 +719,16 @@ var init_link_windows = __esm({
719
719
  browserStaleAfterMs: 45e3,
720
720
  /**
721
721
  * 会话进程发完 `session_exited` 后等 ack 的时限(`session-process.ts`)。
722
- * ⚠ ADR 0300 D7 已判定这个值偏小:实测一次 serve 重启停服 27.9 s,停服窗口内跑完的会话
723
- * 其退出帧**必然**丢失。0300 止血 PR 0 会把它提到 ≥ 90 s 并让未回执的退出帧先落盘,
724
- * 改的就是这一行——本次只收敛位置,不动值。
725
- */
726
- exitAckWaitMs: 1e4,
722
+ *
723
+ * ADR 0300 D7:取 90 s 覆盖一次 serve 重启(实测停服 27.6~27.9 s,数据面会话最慢 38.6 s 连回,
724
+ * 证据 C/D)。会话进程在这段时间里**不退出**、保持数据面重连;serve 回来后 `ws.on("open")` 会
725
+ * 重发 outbox 里未回执的 `session_exited`(`session-process.ts` 的 `for (const m of this.outbox)`),
726
+ * 服务端 `reconcileChatExitFrame` 按 `dispatchId` 幂等收口——退出帧不再随停服窗口蒸发、账本不再
727
+ * 停在 running 等时钟扫描判孤儿。仍小于会话自尽窗 `SESSION_LIMITS.suicideAfterUnreachableMs`(5 min),
728
+ * 等 ack 期间不会先自尽。**更狠的一层**(进程被 TTL/自尽强杀于窗口内时把未回执帧落 `exit.json`、
729
+ * 由 daemon 信使补发)属 D7 后续,另计。
730
+ */
731
+ exitAckWaitMs: 9e4,
727
732
  /** hub 对「暂无人认领」的会话在 serve 冷启动后挂起重判的宽限(`daemon-hub.ts`)。 */
728
733
  hubColdStartGraceMs: 9e4,
729
734
  /** orphan-sweep 判「账上在跑、四张认领表都不认」为孤儿的宽限(`serve.ts`)。 */
@@ -19191,7 +19196,9 @@ var init_kernel_bridge = __esm({
19191
19196
  handActorId: args.hand ?? null,
19192
19197
  ...args.intent !== void 0 ? { intent: args.intent } : {},
19193
19198
  ...args.source !== void 0 ? { source: args.source } : {},
19194
- recipients: [this.model.artifacts.get(args.artifactId)?.owner ?? ""],
19199
+ // Escalation hands responsibility to the workorder owner; never address only the stalled executor.
19200
+ // Human/team lead delivery additionally uses the same live recipient projection as the Inbox.
19201
+ recipients: [...new Set([brief?.owner ?? art?.owner].filter((id) => !!id))],
19195
19202
  attachments: []
19196
19203
  }
19197
19204
  });
@@ -33366,14 +33373,14 @@ var require_axios = __commonJS({
33366
33373
  navigator: _navigator,
33367
33374
  origin
33368
33375
  });
33369
- var platform = {
33376
+ var platform2 = {
33370
33377
  ...utils,
33371
33378
  ...platform$1
33372
33379
  };
33373
33380
  function toURLEncodedForm(data, options) {
33374
- return toFormData(data, new platform.classes.URLSearchParams(), {
33381
+ return toFormData(data, new platform2.classes.URLSearchParams(), {
33375
33382
  visitor: function(value2, key, path40, helpers) {
33376
- if (platform.isNode && utils$1.isBuffer(value2)) {
33383
+ if (platform2.isNode && utils$1.isBuffer(value2)) {
33377
33384
  this.append(key, value2.toString("base64"));
33378
33385
  return false;
33379
33386
  }
@@ -33526,8 +33533,8 @@ var require_axios = __commonJS({
33526
33533
  maxContentLength: -1,
33527
33534
  maxBodyLength: -1,
33528
33535
  env: {
33529
- FormData: platform.classes.FormData,
33530
- Blob: platform.classes.Blob
33536
+ FormData: platform2.classes.FormData,
33537
+ Blob: platform2.classes.Blob
33531
33538
  },
33532
33539
  validateStatus: function validateStatus(status) {
33533
33540
  return status >= 200 && status < 300;
@@ -33890,7 +33897,7 @@ var require_axios = __commonJS({
33890
33897
  }
33891
33898
  var DATA_URL_PATTERN = /^(?:([^;]+);)?(?:[^;]+;)?(base64|),([\s\S]*)$/;
33892
33899
  function fromDataURI(uri, asBlob, options) {
33893
- const _Blob = options && options.Blob || platform.classes.Blob;
33900
+ const _Blob = options && options.Blob || platform2.classes.Blob;
33894
33901
  const protocol = parseProtocol(uri);
33895
33902
  if (asBlob === void 0 && _Blob) {
33896
33903
  asBlob = true;
@@ -34048,7 +34055,7 @@ var require_axios = __commonJS({
34048
34055
  }
34049
34056
  };
34050
34057
  var readBlob$1 = readBlob;
34051
- var BOUNDARY_ALPHABET = platform.ALPHABET.ALPHA_DIGIT + "-_";
34058
+ var BOUNDARY_ALPHABET = platform2.ALPHABET.ALPHA_DIGIT + "-_";
34052
34059
  var textEncoder = typeof TextEncoder === "function" ? new TextEncoder() : new util__default["default"].TextEncoder();
34053
34060
  var CRLF = "\r\n";
34054
34061
  var CRLF_BYTES = textEncoder.encode(CRLF);
@@ -34094,7 +34101,7 @@ var require_axios = __commonJS({
34094
34101
  const {
34095
34102
  tag = "form-data-boundary",
34096
34103
  size = 25,
34097
- boundary = tag + "-" + platform.generateString(size, BOUNDARY_ALPHABET)
34104
+ boundary = tag + "-" + platform2.generateString(size, BOUNDARY_ALPHABET)
34098
34105
  } = options || {};
34099
34106
  if (!utils$1.isFormData(form)) {
34100
34107
  throw TypeError("FormData instance required");
@@ -34323,7 +34330,7 @@ var require_axios = __commonJS({
34323
34330
  var isBrotliSupported = utils$1.isFunction(zlib__default["default"].createBrotliDecompress);
34324
34331
  var { http: httpFollow, https: httpsFollow } = followRedirects__default["default"];
34325
34332
  var isHttps = /https:?/;
34326
- var supportedProtocols = platform.protocols.map((protocol) => {
34333
+ var supportedProtocols = platform2.protocols.map((protocol) => {
34327
34334
  return protocol + ":";
34328
34335
  });
34329
34336
  var flushOnFinish = (stream2, [throttled, flush]) => {
@@ -34575,7 +34582,7 @@ var require_axios = __commonJS({
34575
34582
  }
34576
34583
  });
34577
34584
  const fullPath = buildFullPath(config2.baseURL, config2.url, config2.allowAbsoluteUrls);
34578
- const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : void 0);
34585
+ const parsed = new URL(fullPath, platform2.hasBrowserEnv ? platform2.origin : void 0);
34579
34586
  const protocol = parsed.protocol || supportedProtocols[0];
34580
34587
  if (protocol === "data:") {
34581
34588
  if (config2.maxContentLength > -1) {
@@ -34983,14 +34990,14 @@ var require_axios = __commonJS({
34983
34990
  }
34984
34991
  });
34985
34992
  };
34986
- var isURLSameOrigin = platform.hasStandardBrowserEnv ? /* @__PURE__ */ ((origin2, isMSIE) => (url2) => {
34987
- url2 = new URL(url2, platform.origin);
34993
+ var isURLSameOrigin = platform2.hasStandardBrowserEnv ? /* @__PURE__ */ ((origin2, isMSIE) => (url2) => {
34994
+ url2 = new URL(url2, platform2.origin);
34988
34995
  return origin2.protocol === url2.protocol && origin2.host === url2.host && (isMSIE || origin2.port === url2.port);
34989
34996
  })(
34990
- new URL(platform.origin),
34991
- platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)
34997
+ new URL(platform2.origin),
34998
+ platform2.navigator && /(msie|trident)/i.test(platform2.navigator.userAgent)
34992
34999
  ) : () => true;
34993
- var cookies = platform.hasStandardBrowserEnv ? (
35000
+ var cookies = platform2.hasStandardBrowserEnv ? (
34994
35001
  // Standard browser envs support document.cookie
34995
35002
  {
34996
35003
  write(name, value2, expires, path40, domain2, secure, sameSite) {
@@ -35131,7 +35138,7 @@ var require_axios = __commonJS({
35131
35138
  );
35132
35139
  }
35133
35140
  if (utils$1.isFormData(data)) {
35134
- if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
35141
+ if (platform2.hasStandardBrowserEnv || platform2.hasStandardBrowserWebWorkerEnv) {
35135
35142
  headers.setContentType(void 0);
35136
35143
  } else if (utils$1.isFunction(data.getHeaders)) {
35137
35144
  const formHeaders = data.getHeaders();
@@ -35143,7 +35150,7 @@ var require_axios = __commonJS({
35143
35150
  });
35144
35151
  }
35145
35152
  }
35146
- if (platform.hasStandardBrowserEnv) {
35153
+ if (platform2.hasStandardBrowserEnv) {
35147
35154
  withXSRFToken && utils$1.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(newConfig));
35148
35155
  if (withXSRFToken || withXSRFToken !== false && isURLSameOrigin(newConfig.url)) {
35149
35156
  const xsrfValue = xsrfHeaderName && xsrfCookieName && cookies.read(xsrfCookieName);
@@ -35281,7 +35288,7 @@ var require_axios = __commonJS({
35281
35288
  }
35282
35289
  }
35283
35290
  const protocol = parseProtocol(_config.url);
35284
- if (protocol && platform.protocols.indexOf(protocol) === -1) {
35291
+ if (protocol && platform2.protocols.indexOf(protocol) === -1) {
35285
35292
  reject(
35286
35293
  new AxiosError$1(
35287
35294
  "Unsupported protocol " + protocol + ":",
@@ -35441,7 +35448,7 @@ var require_axios = __commonJS({
35441
35448
  const encodeText = isFetchSupported && (typeof TextEncoder$1 === "function" ? /* @__PURE__ */ ((encoder) => (str2) => encoder.encode(str2))(new TextEncoder$1()) : async (str2) => new Uint8Array(await new Request(str2).arrayBuffer()));
35442
35449
  const supportsRequestStream = isRequestSupported && isReadableStreamSupported && test(() => {
35443
35450
  let duplexAccessed = false;
35444
- const hasContentType = new Request(platform.origin, {
35451
+ const hasContentType = new Request(platform2.origin, {
35445
35452
  body: new ReadableStream$1(),
35446
35453
  method: "POST",
35447
35454
  get duplex() {
@@ -35478,7 +35485,7 @@ var require_axios = __commonJS({
35478
35485
  return body2.size;
35479
35486
  }
35480
35487
  if (utils$1.isSpecCompliantForm(body2)) {
35481
- const _request = new Request(platform.origin, {
35488
+ const _request = new Request(platform2.origin, {
35482
35489
  method: "POST",
35483
35490
  body: body2
35484
35491
  });
@@ -150194,6 +150201,40 @@ var init_live_chat = __esm({
150194
150201
  }
150195
150202
  };
150196
150203
  }
150204
+ /**
150205
+ * serve 重启后**重挂**一个占位轮(ADR 0300 D4)。该会话在本进程注册表里已有轮(真轮或另一个占位轮)
150206
+ * 则不动、返回 null——保证幂等且绝不覆盖真轮。占位轮 status=running、缓冲为空、句柄不带 appendInput
150207
+ * (`canAppend` 因此为 false),`kill()` 走注入的 `onStop`(账本收口)并把本轮标 error 让在挂的观众
150208
+ * 就地收到终止。节点重连时恢复路的 {@link start} 会替掉它。
150209
+ */
150210
+ reopenForRecovery(chatSessionId, opts) {
150211
+ if (this.turns.has(chatSessionId)) return null;
150212
+ let ctrl;
150213
+ const handle = {
150214
+ runtimeSessionId: opts.runtimeSessionId,
150215
+ ...opts.runId ? { runId: opts.runId } : {},
150216
+ canAppendInput: false,
150217
+ kill: () => {
150218
+ try {
150219
+ opts.onStop?.();
150220
+ } finally {
150221
+ ctrl?.finish("error");
150222
+ }
150223
+ }
150224
+ };
150225
+ ctrl = this.start(chatSessionId, handle);
150226
+ const turn = this.turns.get(chatSessionId);
150227
+ if (turn) turn.recovering = true;
150228
+ return ctrl;
150229
+ }
150230
+ /** 占位轮的最后一个观众断开即回收:没真 runtime 喂它,留着只会占内存、也会让 `isRunning` 恒真。 */
150231
+ evictIfEmptyRecovering(chatSessionId, turn) {
150232
+ if (!turn.recovering || turn.status !== "running") return;
150233
+ if (turn.subscribers.size > 0 || turn.liveSubscribers.size > 0) return;
150234
+ if (this.turns.get(chatSessionId) !== turn) return;
150235
+ if (turn.evictTimer) clearTimeout(turn.evictTimer);
150236
+ this.turns.delete(chatSessionId);
150237
+ }
150197
150238
  emitLive(turn, event) {
150198
150239
  turn.liveTurnId = event.turnId;
150199
150240
  const mergeField = this.mergeableLiveField(event);
@@ -150267,6 +150308,7 @@ var init_live_chat = __esm({
150267
150308
  gap,
150268
150309
  detach: () => {
150269
150310
  turn.subscribers.delete(sub);
150311
+ this.evictIfEmptyRecovering(chatSessionId, turn);
150270
150312
  }
150271
150313
  };
150272
150314
  }
@@ -150286,6 +150328,7 @@ var init_live_chat = __esm({
150286
150328
  epoch: turn.liveEpoch,
150287
150329
  detach: () => {
150288
150330
  turn.liveSubscribers.delete(sub);
150331
+ this.evictIfEmptyRecovering(chatSessionId, turn);
150289
150332
  }
150290
150333
  };
150291
150334
  }
@@ -150409,7 +150452,9 @@ var init_live_chat = __esm({
150409
150452
  * 建单草案 submit 发生在这一轮内:用它给草案盖上「产出轮次」标记,供 friday 重进对话页把每版
150410
150453
  * 草案归位到产出它的那条助手消息(message.runId 对齐)。 */
150411
150454
  runFor(chatSessionId) {
150412
- return this.turns.get(chatSessionId)?.runId;
150455
+ const turn = this.turns.get(chatSessionId);
150456
+ if (!turn || turn.recovering) return void 0;
150457
+ return turn.runId;
150413
150458
  }
150414
150459
  /**
150415
150460
  * 反查该会话当前驻留那一轮**这一段**的 assistant 行 id(无进行中/grace 轮、或还没落库时
@@ -150418,7 +150463,9 @@ var init_live_chat = __esm({
150418
150463
  * 两段共用同一个 runId,钉不出前后段的区别,而那正是本字段存在的理由。
150419
150464
  */
150420
150465
  messageFor(chatSessionId) {
150421
- return this.turns.get(chatSessionId)?.assistantMessageId;
150466
+ const turn = this.turns.get(chatSessionId);
150467
+ if (!turn || turn.recovering) return void 0;
150468
+ return turn.assistantMessageId;
150422
150469
  }
150423
150470
  /** 测试/排障用:当前驻留的会话数。 */
150424
150471
  size() {
@@ -184602,16 +184649,16 @@ var init_tool_home = __esm({
184602
184649
  });
184603
184650
 
184604
184651
  // ../connectors/src/_base/install.ts
184605
- function installPlan(cmd, platform, toolsPrefix) {
184652
+ function installPlan(cmd, platform2, toolsPrefix) {
184606
184653
  if (cmd === "lark-cli") {
184607
- const npm = platform === "win32" ? "npm.cmd" : "npm";
184654
+ const npm = platform2 === "win32" ? "npm.cmd" : "npm";
184608
184655
  return [{ file: npm, args: ["install", "-g", "--prefix", toolsPrefix, "@larksuite/cli"], ensureDir: toolsPrefix }];
184609
184656
  }
184610
184657
  const pkg = cmd === "gh" ? { brew: "gh", apt: "gh", dnf: "gh", pacman: "github-cli", apk: "github-cli", winget: "GitHub.cli", choco: "gh", scoop: "gh" } : { brew: "git", apt: "git", dnf: "git", pacman: "git", apk: "git", winget: "Git.Git", choco: "git", scoop: "git" };
184611
- if (platform === "darwin") {
184658
+ if (platform2 === "darwin") {
184612
184659
  return [{ file: "brew", args: ["install", pkg.brew], requires: "brew" }];
184613
184660
  }
184614
- if (platform === "win32") {
184661
+ if (platform2 === "win32") {
184615
184662
  return [
184616
184663
  { file: "winget", args: ["install", "--silent", "--accept-package-agreements", "--accept-source-agreements", "-e", "--id", pkg.winget], requires: "winget" },
184617
184664
  { file: "choco", args: ["install", "-y", pkg.choco], requires: "choco" },
@@ -194314,6 +194361,35 @@ ${task}` : task;
194314
194361
  }
194315
194362
  });
194316
194363
 
194364
+ // ../adapters/src/_core/pdeathsig.ts
194365
+ function pdeathsigAvailable(probe = defaultProbe) {
194366
+ if (cachedAvailable === void 0) cachedAvailable = probe();
194367
+ return cachedAvailable;
194368
+ }
194369
+ function defaultProbe() {
194370
+ if (os6.platform() !== "linux") return false;
194371
+ try {
194372
+ const help = (0, import_node_child_process13.execFileSync)("setpriv", ["--help"], { stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).toString();
194373
+ return help.includes("--pdeathsig");
194374
+ } catch {
194375
+ return false;
194376
+ }
194377
+ }
194378
+ function pdeathsigWrap(bin, args) {
194379
+ return { bin: "setpriv", args: ["--pdeathsig", "SIGKILL", "--", bin, ...args] };
194380
+ }
194381
+ function shouldBindRuntimePdeathsig() {
194382
+ return process.env["OASIS_BIND_RUNTIME_PDEATHSIG"] === "1" && pdeathsigAvailable();
194383
+ }
194384
+ var import_node_child_process13, os6, cachedAvailable;
194385
+ var init_pdeathsig = __esm({
194386
+ "../adapters/src/_core/pdeathsig.ts"() {
194387
+ "use strict";
194388
+ import_node_child_process13 = require("node:child_process");
194389
+ os6 = __toESM(require("node:os"), 1);
194390
+ }
194391
+ });
194392
+
194317
194393
  // ../adapters/src/_core/subprocess.ts
194318
194394
  async function materialize(job, cfg) {
194319
194395
  const slug6 = job.artifactId.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32);
@@ -194416,11 +194492,12 @@ async function runStreamJson(job, cfg) {
194416
194492
  ...cfg.workRoot !== void 0 ? { workRoot: cfg.workRoot } : {},
194417
194493
  ...cfg.env !== void 0 ? { extraEnvEarly: cfg.env } : {}
194418
194494
  });
194419
- const child = containerized ? (0, import_node_child_process13.spawn)(containerized.bin, containerized.args, {
194495
+ const direct = shouldBindRuntimePdeathsig() ? pdeathsigWrap(cfg.bin, args) : { bin: cfg.bin, args };
194496
+ const child = containerized ? (0, import_node_child_process14.spawn)(containerized.bin, containerized.args, {
194420
194497
  cwd: dir,
194421
194498
  env: scrubLeakyEnv(process.env),
194422
194499
  stdio: ["ignore", "pipe", "pipe"]
194423
- }) : (0, import_node_child_process13.spawn)(cfg.bin, args, {
194500
+ }) : (0, import_node_child_process14.spawn)(direct.bin, direct.args, {
194424
194501
  cwd: dir,
194425
194502
  env: buildEnv(job, cfg),
194426
194503
  stdio: ["ignore", "pipe", "pipe"]
@@ -194525,11 +194602,12 @@ async function runOneShotText(job, cfg) {
194525
194602
  ...cfg.workRoot !== void 0 ? { workRoot: cfg.workRoot } : {},
194526
194603
  ...cfg.env !== void 0 ? { extraEnvEarly: cfg.env } : {}
194527
194604
  });
194528
- const child = containerized ? (0, import_node_child_process13.spawn)(containerized.bin, containerized.args, {
194605
+ const direct = shouldBindRuntimePdeathsig() ? pdeathsigWrap(cfg.bin, args) : { bin: cfg.bin, args };
194606
+ const child = containerized ? (0, import_node_child_process14.spawn)(containerized.bin, containerized.args, {
194529
194607
  cwd: dir,
194530
194608
  env: scrubLeakyEnv(process.env),
194531
194609
  stdio: ["ignore", "pipe", "pipe"]
194532
- }) : (0, import_node_child_process13.spawn)(cfg.bin, args, {
194610
+ }) : (0, import_node_child_process14.spawn)(direct.bin, direct.args, {
194533
194611
  cwd: dir,
194534
194612
  env: buildEnv(job, cfg),
194535
194613
  stdio: ["ignore", "pipe", "pipe"]
@@ -194608,11 +194686,11 @@ async function runOneShotText(job, cfg) {
194608
194686
  }
194609
194687
  });
194610
194688
  }
194611
- var import_node_child_process13, import_node_crypto29, fs14, path16, readline4;
194689
+ var import_node_child_process14, import_node_crypto29, fs14, path16, readline4;
194612
194690
  var init_subprocess = __esm({
194613
194691
  "../adapters/src/_core/subprocess.ts"() {
194614
194692
  "use strict";
194615
- import_node_child_process13 = require("node:child_process");
194693
+ import_node_child_process14 = require("node:child_process");
194616
194694
  import_node_crypto29 = require("node:crypto");
194617
194695
  fs14 = __toESM(require("node:fs"), 1);
194618
194696
  path16 = __toESM(require("node:path"), 1);
@@ -194625,6 +194703,7 @@ var init_subprocess = __esm({
194625
194703
  init_transcript();
194626
194704
  init_filter_extra_args();
194627
194705
  init_root_bypass();
194706
+ init_pdeathsig();
194628
194707
  }
194629
194708
  });
194630
194709
 
@@ -198164,6 +198243,22 @@ async function startOasisServer(opts) {
198164
198243
  return;
198165
198244
  }
198166
198245
  const fromSeq = Math.max(0, Number(url.searchParams.get("fromSeq") ?? "0") || 0);
198246
+ const reopenRecoveringTurn = async () => {
198247
+ if (!opts.chatTurns) return;
198248
+ const active = await opts.chatTurns.activeTurn(sid).catch(() => null);
198249
+ if (!active || active.status !== "running") return;
198250
+ liveChat.reopenForRecovery(sid, {
198251
+ runtimeSessionId: cs.runtimeSessionId ?? active.dispatchId ?? "",
198252
+ ...active.assistantRunId ? { runId: active.assistantRunId } : {},
198253
+ onStop: () => {
198254
+ void opts.chatTurns?.settleTurn(active.id, {
198255
+ status: "cancelled",
198256
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
198257
+ lastError: `\u7528\u6237\u5728 serve \u91CD\u542F\u6062\u590D\u7A97\u53E3\u5185\u505C\u6B62\uFF08\u8F6E ${active.id}\uFF09`
198258
+ }).catch(() => void 0);
198259
+ }
198260
+ });
198261
+ };
198167
198262
  const liveProtocol = url.searchParams.get("protocol") === "v2" || wantsLiveProtocol(req);
198168
198263
  if (liveProtocol) {
198169
198264
  const sub2 = (frame) => {
@@ -198172,7 +198267,11 @@ async function startOasisServer(opts) {
198172
198267
  `);
198173
198268
  if (frame.operation === "turn_completed" || frame.operation === "turn_failed") res.end();
198174
198269
  };
198175
- const att2 = liveChat.attachLive(sid, fromSeq, sub2);
198270
+ let att2 = liveChat.attachLive(sid, fromSeq, sub2);
198271
+ if (!att2) {
198272
+ await reopenRecoveringTurn();
198273
+ att2 = liveChat.attachLive(sid, fromSeq, sub2);
198274
+ }
198176
198275
  if (!att2) {
198177
198276
  res.writeHead(204).end();
198178
198277
  return;
@@ -198199,9 +198298,10 @@ async function startOasisServer(opts) {
198199
198298
  res.end();
198200
198299
  return;
198201
198300
  }
198301
+ const attached2 = att2;
198202
198302
  res.on("close", () => {
198203
198303
  stopKa2();
198204
- att2.detach();
198304
+ attached2.detach();
198205
198305
  });
198206
198306
  return;
198207
198307
  }
@@ -198211,7 +198311,11 @@ async function startOasisServer(opts) {
198211
198311
  `);
198212
198312
  if (part.type === "done") res.end();
198213
198313
  };
198214
- const att = liveChat.attach(sid, fromSeq, sub);
198314
+ let att = liveChat.attach(sid, fromSeq, sub);
198315
+ if (!att) {
198316
+ await reopenRecoveringTurn();
198317
+ att = liveChat.attach(sid, fromSeq, sub);
198318
+ }
198215
198319
  if (!att) {
198216
198320
  res.writeHead(204).end();
198217
198321
  return;
@@ -198237,9 +198341,10 @@ async function startOasisServer(opts) {
198237
198341
  res.end();
198238
198342
  return;
198239
198343
  }
198344
+ const attached = att;
198240
198345
  res.on("close", () => {
198241
198346
  stopKa();
198242
- att.detach();
198347
+ attached.detach();
198243
198348
  });
198244
198349
  return;
198245
198350
  }
@@ -205206,6 +205311,7 @@ async function sweepOrphanRuns(deps) {
205206
205311
  const inFlight = await deps.inFlightRunIds();
205207
205312
  const aliveArtifacts = await deps.aliveArtifactIds?.() ?? /* @__PURE__ */ new Set();
205208
205313
  const apiActive = await deps.apiActiveDispatchIds?.() ?? /* @__PURE__ */ new Set();
205314
+ const liveSessions = await deps.liveSessionRunIds?.() ?? /* @__PURE__ */ new Set();
205209
205315
  const orphaned = [];
205210
205316
  const cappedOut = [];
205211
205317
  const rescued = [];
@@ -205231,7 +205337,7 @@ async function sweepOrphanRuns(deps) {
205231
205337
  }
205232
205338
  continue;
205233
205339
  }
205234
- const claimed = inFlight.has(run.id) || run.artifactId !== null && aliveArtifacts.has(run.artifactId) || apiActive.has(run.id);
205340
+ const claimed = inFlight.has(run.id) || run.artifactId !== null && aliveArtifacts.has(run.artifactId) || apiActive.has(run.id) || liveSessions.has(run.id);
205235
205341
  if (!inFlight.has(run.id) && !(run.artifactId !== null && aliveArtifacts.has(run.artifactId)) && apiActive.has(run.id) && ageMs > deps.orphanGraceMs) {
205236
205342
  rescued.push(run.id);
205237
205343
  deps.log?.(
@@ -205268,7 +205374,7 @@ async function sweepDroppedDispatches(deps) {
205268
205374
  const result = {
205269
205375
  scanned: 0,
205270
205376
  redispatched: [],
205271
- skipped: { inFlight: 0, ledgerOpen: 0, backoff: 0, notAgent: 0, failed: 0 }
205377
+ skipped: { inFlight: 0, ledgerOpen: 0, backoff: 0, notAgent: 0, blocked: 0, failed: 0 }
205272
205378
  };
205273
205379
  const createdBefore = new Date(deps.now - deps.graceMs).toISOString();
205274
205380
  const candidates = await deps.listNeverStarted(createdBefore, CANDIDATE_LIMIT);
@@ -205301,6 +205407,10 @@ async function sweepDroppedDispatches(deps) {
205301
205407
  continue;
205302
205408
  }
205303
205409
  try {
205410
+ if (deps.beforeRedispatch && !await deps.beforeRedispatch(c)) {
205411
+ result.skipped.blocked++;
205412
+ continue;
205413
+ }
205304
205414
  await deps.redispatch(c.workId, c.workorderId);
205305
205415
  result.redispatched.push(c.workId);
205306
205416
  } catch {
@@ -205354,6 +205464,35 @@ var init_dispatch_mapping_rebuild = __esm({
205354
205464
  }
205355
205465
  });
205356
205466
 
205467
+ // ../server/src/dispatch-binding-diagnosis.ts
205468
+ async function prepareDroppedDispatch(work, deps) {
205469
+ const { kernel } = deps;
205470
+ await kernel.refreshModel?.();
205471
+ const node2 = kernel.model.artifacts.get(work.nodeId);
205472
+ const snap = await kernel.getStore().transaction((tx) => tx.loadWorkorder(work.workorderId));
205473
+ const current = snap?.works.find((w2) => w2.id === work.workId);
205474
+ if (!snap || !current || current.status !== "running" || current.startedAt || current.sessionRef || current.endedAt || !snap.nodes.some((n) => n.id === work.nodeId && n.latestWorkId === current.id && !n.cancelledAt) || !node2 || node2.workspace !== work.workorderId || lifecycleOf(kernel.model, node2.id) !== "active" || kernel.model.pausedWorkorders.has(work.workorderId) || isHeld(kernel.model, node2.id)) return false;
205475
+ if (snap.issues.some((i) => i.kind === "escalation" && i.aboutNodeId === node2.id && !i.resolvedAt && i.intent !== "advisory")) return false;
205476
+ if (!work.assigneeActorId) return false;
205477
+ const binding = await deps.bindingFor(work.assigneeActorId);
205478
+ if (!binding?.nodeId) return true;
205479
+ const runtimeNode = await deps.nodeFor(binding.nodeId);
205480
+ if (runtimeNode && runtimeNode.status !== "deleted") return true;
205481
+ await kernel.escalate({
205482
+ artifactId: node2.id,
205483
+ actor: SYSTEM_ACTOR,
205484
+ source: "dispatch:missing-node",
205485
+ reason: `\u8FD9\u4E00\u6B65\u8FD8\u6CA1\u6709\u542F\u52A8\uFF1A\u6267\u884C\u4EBA\u7684\u8FD0\u884C\u65F6\u7ED1\u5B9A\u6307\u5411${runtimeNode ? "\u5DF2\u5220\u9664" : "\u672A\u6CE8\u518C"}\u7684\u8282\u70B9 ${binding.nodeId}\u3002\u8BF7\u6062\u590D\u8BE5\u8282\u70B9\uFF0C\u6216\u4E3A\u6267\u884C\u4EBA ${work.assigneeActorId} \u9009\u62E9\u5DF2\u6709\u4E14\u914D\u7F6E\u6B63\u786E\u7684\u8282\u70B9\uFF1B\u4FEE\u590D\u540E\u5173\u95ED\u8FD9\u6761\u4E0A\u62A5\uFF0C\u6D41\u7A0B\u624D\u4F1A\u7EE7\u7EED\u3002\u672C\u6B21\u68C0\u67E5\u6CA1\u6709\u7EC8\u6B62\u4F1A\u8BDD\uFF0C\u4E5F\u6CA1\u6709\u6D88\u8017 work \u91CD\u8BD5\u6B21\u6570\u3002`
205486
+ });
205487
+ return false;
205488
+ }
205489
+ var init_dispatch_binding_diagnosis = __esm({
205490
+ "../server/src/dispatch-binding-diagnosis.ts"() {
205491
+ "use strict";
205492
+ init_src5();
205493
+ }
205494
+ });
205495
+
205357
205496
  // ../server/src/zero-output-breaker.ts
205358
205497
  function absorb(b2, r, startedMs) {
205359
205498
  b2.count += 1;
@@ -205878,14 +206017,14 @@ function classifyDeployRun(facts) {
205878
206017
  if (facts.anyContainingInProgress) return { kind: "running", note: "\u627F\u8F7D\u672C\u6B21\u4EA4\u4ED8\u7684\u90E8\u7F72 run \u4ECD\u5728\u8FDB\u884C" };
205879
206018
  return { kind: "pending", note: "\u8FD8\u6CA1\u770B\u5230\u627F\u8F7D\u672C\u6B21\u4EA4\u4ED8\u7684\u90E8\u7F72 run" };
205880
206019
  }
205881
- var import_node_child_process14, import_node_util5, execFile5, GhDeployProvider;
206020
+ var import_node_child_process15, import_node_util5, execFile5, GhDeployProvider;
205882
206021
  var init_deploy_provider = __esm({
205883
206022
  "../server/src/git/deploy-provider.ts"() {
205884
206023
  "use strict";
205885
- import_node_child_process14 = require("node:child_process");
206024
+ import_node_child_process15 = require("node:child_process");
205886
206025
  import_node_util5 = require("node:util");
205887
206026
  init_remote_util();
205888
- execFile5 = (0, import_node_util5.promisify)(import_node_child_process14.execFile);
206027
+ execFile5 = (0, import_node_util5.promisify)(import_node_child_process15.execFile);
205889
206028
  GhDeployProvider = class {
205890
206029
  constructor(opts = {}) {
205891
206030
  this.opts = opts;
@@ -206121,14 +206260,14 @@ function makeDefaultBranchResolver(opts) {
206121
206260
  }
206122
206261
  };
206123
206262
  }
206124
- var import_node_child_process15, import_node_util6, execFile6;
206263
+ var import_node_child_process16, import_node_util6, execFile6;
206125
206264
  var init_default_branch = __esm({
206126
206265
  "../server/src/git/default-branch.ts"() {
206127
206266
  "use strict";
206128
- import_node_child_process15 = require("node:child_process");
206267
+ import_node_child_process16 = require("node:child_process");
206129
206268
  import_node_util6 = require("node:util");
206130
206269
  init_remote_util();
206131
- execFile6 = (0, import_node_util6.promisify)(import_node_child_process15.execFile);
206270
+ execFile6 = (0, import_node_util6.promisify)(import_node_child_process16.execFile);
206132
206271
  }
206133
206272
  });
206134
206273
 
@@ -207463,6 +207602,14 @@ var init_daemon_hub = __esm({
207463
207602
  sessionMeta = /* @__PURE__ */ new Map();
207464
207603
  sessionConnectListeners = [];
207465
207604
  sessionDisconnectListeners = [];
207605
+ // ── ADR 0300 D2「日志复制」:帧号 + 累积 ackSeq + 幂等摄入 ────────────────────────────
207606
+ // 只对自报 supportsFrameSeq 的会话生效;老会话这三张表始终为空、逐字走原 outbox 语义。
207607
+ /** 自报支持帧号的 dispatchId。 */
207608
+ frameSeqSupported = /* @__PURE__ */ new Set();
207609
+ /** 每 dispatch 已按序摄入的最高节点帧号(累积水位)。跨重连保留、`session_exited` 后延迟清。 */
207610
+ frameWatermark = /* @__PURE__ */ new Map();
207611
+ /** 每 dispatch 上次回 ackSeq 的时刻——把流式期间的回执节流到 ~250ms 一次。 */
207612
+ lastAckSeqAt = /* @__PURE__ */ new Map();
207466
207613
  /**
207467
207614
  * 「这次派发归谁管、现在什么状态」的认领口。**必须由 hub 汇总,不能各 adapter 自己判**——
207468
207615
  * serve 里同时有两个 DaemonHubAdapter(chat 一个、dispatch 一个),一次派发只属于其中一个。
@@ -207697,12 +207844,33 @@ var init_daemon_hub = __esm({
207697
207844
  for (const l of this.sessionConnectListeners) l({ dispatchId, sessionId: msg.sessionId, nodeId });
207698
207845
  void this.judgeSession(dispatchId, ws);
207699
207846
  }
207847
+ if (msg.supportsFrameSeq) {
207848
+ this.frameSeqSupported.add(dispatchId);
207849
+ this.sendAckSeq(ws, dispatchId, true);
207850
+ }
207700
207851
  return;
207701
207852
  }
207702
207853
  if (!registered) {
207703
207854
  ws.close(1008, "session_hello required first");
207704
207855
  return;
207705
207856
  }
207857
+ if (this.frameSeqSupported.has(dispatchId)) {
207858
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
207859
+ if (seq !== void 0) {
207860
+ const wm = this.frameWatermark.get(dispatchId) ?? 0;
207861
+ if (seq <= wm) {
207862
+ this.sendAckSeq(ws, dispatchId, false);
207863
+ return;
207864
+ }
207865
+ this.frameWatermark.set(dispatchId, seq);
207866
+ }
207867
+ for (const listener of this.messageListeners) listener(nodeId, msg);
207868
+ this.onMessage?.(nodeId, msg);
207869
+ const isExit = msg.type === "session_exited";
207870
+ this.sendAckSeq(ws, dispatchId, isExit);
207871
+ if (isExit) this.scheduleFrameStateCleanup(dispatchId);
207872
+ return;
207873
+ }
207706
207874
  for (const listener of this.messageListeners) listener(nodeId, msg);
207707
207875
  this.onMessage?.(nodeId, msg);
207708
207876
  if (msg.type === "session_exited" && ws.readyState === import_websocket.default.OPEN) {
@@ -207718,6 +207886,23 @@ var init_daemon_hub = __esm({
207718
207886
  });
207719
207887
  ws.on("error", (err) => console.error(`[hub] session ${dispatchId} error:`, err.message));
207720
207888
  }
207889
+ /** ADR 0300 D2:回带当前摄入水位的累积回执。流式期间节流到 ~250ms 一次,hello / 收尾帧立即回。 */
207890
+ sendAckSeq(ws, dispatchId, force) {
207891
+ if (ws.readyState !== import_websocket.default.OPEN) return;
207892
+ const now = Date.now();
207893
+ if (!force && now - (this.lastAckSeqAt.get(dispatchId) ?? 0) < 250) return;
207894
+ this.lastAckSeqAt.set(dispatchId, now);
207895
+ ws.send(JSON.stringify({ type: "ack", dispatchId, ackSeq: this.frameWatermark.get(dispatchId) ?? 0 }));
207896
+ }
207897
+ /** `session_exited` 后延迟清帧号状态:给重连重放的收尾帧留一个去重窗口,之后回收避免无界增长。 */
207898
+ scheduleFrameStateCleanup(dispatchId) {
207899
+ const t = setTimeout(() => {
207900
+ this.frameSeqSupported.delete(dispatchId);
207901
+ this.frameWatermark.delete(dispatchId);
207902
+ this.lastAckSeqAt.delete(dispatchId);
207903
+ }, 6e4);
207904
+ t.unref?.();
207905
+ }
207721
207906
  /**
207722
207907
  * 注册一个认领判断。返回:
207723
207908
  * live —— 这次派发在我这儿还活着(别动它)
@@ -207830,6 +208015,22 @@ var init_daemon_hub = __esm({
207830
208015
  hasSession(dispatchId) {
207831
208016
  return this.sessionSockets.get(dispatchId)?.readyState === import_websocket.default.OPEN;
207832
208017
  }
208018
+ /**
208019
+ * 当前持有**活着数据面连接**的全部 dispatchId(ADR-0300 D8:hub 有活连接即认领)。
208020
+ *
208021
+ * orphan-sweep 的认领来源此前只有 dispatcher 在途表、chat 在途表、节点 hello、HTTP 活性——
208022
+ * 独立会话进程(协调者、D2′ 后搬出 daemon 的会话)**不在这四者里**,但它们自己握着一根到 hub 的
208023
+ * 数据面 WS。这些会话 `run.id === dispatchId`(协调者:`job.dispatchId = coordinate:*`),
208024
+ * 于是「hub 此刻还连着它」就是最直接的判活。不接这条线,7 天里 183 条协调者会话被误判孤儿闭合
208025
+ * (ADR-0300 证据 E)。只增不减:命中只能**阻止**误闭合,永不会导致误闭合。
208026
+ */
208027
+ liveSessionDispatchIds() {
208028
+ const ids2 = /* @__PURE__ */ new Set();
208029
+ for (const [dispatchId, ws] of this.sessionSockets) {
208030
+ if (ws.readyState === import_websocket.default.OPEN) ids2.add(dispatchId);
208031
+ }
208032
+ return ids2;
208033
+ }
207833
208034
  /** 往会话进程直投一帧(kill / append_input / ping)。返回是否写进了 OPEN 的 socket。 */
207834
208035
  sendToSession(dispatchId, msg) {
207835
208036
  const ws = this.sessionSockets.get(dispatchId);
@@ -214668,6 +214869,7 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
214668
214869
  if (!isOwnerRecipient && !isLeadRecipient) continue;
214669
214870
  const gaps = unresolvedGapsOf(model, a.id);
214670
214871
  const escalations = unresolvedEscalationsOf(model, a.id);
214872
+ const representedEscalations = /* @__PURE__ */ new Set();
214671
214873
  const who = isOwnerRecipient ? "\uFF08\u4F60\u662F\u5DE5\u5355\u5F52\u5C5E\u4EBA\uFF09" : "\uFF08\u4F60\u662F\u5361\u4F4F\u8282\u70B9 owner \u7684\u4E0A\u7EA7\uFF09";
214672
214874
  for (const gap of gaps) {
214673
214875
  const byGapId = escalations.filter((entry) => entry.gapId === gap.gapId);
@@ -214681,6 +214883,7 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
214681
214883
  const exactEscalation = byGapId.at(-1) ?? (samePartGaps.length === 1 ? samePartEscalations.at(-1) : void 0) ?? (gaps.length === 1 ? partlessEscalations.at(-1) : void 0);
214682
214884
  const gapSince = exactEscalation?.at ?? gap.lastReportedAt ?? model.lastOpAt.get(a.id) ?? "";
214683
214885
  const gapReportCount = gap.reportCount ?? 1;
214886
+ if (exactEscalation) representedEscalations.add(exactEscalation.escalationId);
214684
214887
  gapItems.push({
214685
214888
  kind: "gap-escalation",
214686
214889
  artifactId: a.id,
@@ -214709,8 +214912,8 @@ function buildInbox(model, me, leadOf, resolveActor, markers = [], slaMs, nowIso
214709
214912
  }
214710
214913
  });
214711
214914
  }
214712
- if (gaps.length === 0 && escalations.length > 0) {
214713
- for (const e of escalations) {
214915
+ if (escalations.length > 0) {
214916
+ for (const e of escalations.filter((e2) => !representedEscalations.has(e2.escalationId))) {
214714
214917
  gapItems.push({
214715
214918
  kind: "gap-escalation",
214716
214919
  artifactId: a.id,
@@ -214961,6 +215164,152 @@ var init_inbox = __esm({
214961
215164
  }
214962
215165
  });
214963
215166
 
215167
+ // ../server/src/domains/collab/escalation-attention.ts
215168
+ function escalationDeliveryConfig(env) {
215169
+ const positive = (key, fallback) => {
215170
+ if (!env[key]) return fallback;
215171
+ const n = Number(env[key]);
215172
+ if (!Number.isFinite(n) || n <= 0) throw new Error(`${key} must be a positive number`);
215173
+ return n;
215174
+ };
215175
+ const policy = {
215176
+ reminderMs: positive("OASIS_ESCALATION_REMINDER_MS", DEFAULT_ESCALATION_REMINDERS.reminderMs),
215177
+ acknowledgedReminderMs: positive("OASIS_ESCALATION_ACK_REMINDER_MS", DEFAULT_ESCALATION_REMINDERS.acknowledgedReminderMs)
215178
+ };
215179
+ const webhooks = /* @__PURE__ */ new Map();
215180
+ if (env.OASIS_ESCALATION_WEBHOOKS) {
215181
+ try {
215182
+ const entries = JSON.parse(env.OASIS_ESCALATION_WEBHOOKS);
215183
+ if (!entries || Array.isArray(entries) || typeof entries !== "object") throw new Error();
215184
+ for (const [companyId, endpoint] of Object.entries(entries)) {
215185
+ const url = new URL(endpoint.url);
215186
+ if (!companyId || !["https:", "http:"].includes(url.protocol) || url.username || url.password || endpoint.token !== void 0 && typeof endpoint.token !== "string") throw new Error();
215187
+ webhooks.set(companyId, endpoint);
215188
+ }
215189
+ } catch {
215190
+ throw new Error("OASIS_ESCALATION_WEBHOOKS must map company IDs to { url, token? } endpoints");
215191
+ }
215192
+ }
215193
+ return { policy, webhooks };
215194
+ }
215195
+ function withEscalationAttention(items, companyId, markers, now, policy = DEFAULT_ESCALATION_REMINDERS) {
215196
+ const byScope = new Map(markers.map((m2) => [m2.scope, m2.readAt]));
215197
+ return items.map((item) => {
215198
+ if (item.kind !== "gap-escalation" || !item.escalationId) return item;
215199
+ const start = Date.parse(item.since);
215200
+ if (!Number.isFinite(start)) return item;
215201
+ const ackAt = byScope.get(escalationAttentionScope(companyId, item.escalationId, "ack"));
215202
+ const ack = ackAt ? Date.parse(ackAt) : NaN;
215203
+ const acknowledged = Number.isFinite(ack) && ack >= start;
215204
+ const firstDue = acknowledged ? ack + policy.acknowledgedReminderMs : start;
215205
+ const due = now >= firstDue;
215206
+ const step = Math.max(0, Math.floor((now - firstDue) / policy.reminderMs));
215207
+ const dueAt = new Date(firstDue + step * policy.reminderMs).toISOString();
215208
+ const deliveredAt = byScope.get(escalationAttentionScope(companyId, item.escalationId, "webhook"));
215209
+ return { ...item, attention: {
215210
+ reminderKey: `${item.escalationId}:${dueAt}`,
215211
+ dueAt,
215212
+ needsAcknowledgement: due,
215213
+ ...acknowledged ? { acknowledgedAt: ackAt } : {},
215214
+ ...deliveredAt ? { deliveredAt } : {}
215215
+ } };
215216
+ });
215217
+ }
215218
+ async function sendEscalationWebhook(url, token, batch, item, deliveryId) {
215219
+ const response = await fetch(url, {
215220
+ method: "POST",
215221
+ redirect: "error",
215222
+ signal: AbortSignal.timeout(1e4),
215223
+ headers: { "content-type": "application/json", ...token ? { authorization: `Bearer ${token}` } : {} },
215224
+ body: JSON.stringify({ deliveryId, companyId: batch.companyId, recipientActorId: batch.actorId, item })
215225
+ });
215226
+ await response.body?.cancel();
215227
+ if (!response.ok) throw new Error(`Delivery HTTP ${response.status}`);
215228
+ }
215229
+ var escalationAttentionScope, DEFAULT_ESCALATION_REMINDERS, EscalationDeliveryWorker;
215230
+ var init_escalation_attention = __esm({
215231
+ "../server/src/domains/collab/escalation-attention.ts"() {
215232
+ "use strict";
215233
+ escalationAttentionScope = (companyId, issueId, kind) => `escalation-${kind}:${JSON.stringify([companyId, issueId])}`;
215234
+ DEFAULT_ESCALATION_REMINDERS = {
215235
+ reminderMs: 15 * 6e4,
215236
+ acknowledgedReminderMs: 4 * 60 * 6e4
215237
+ };
215238
+ EscalationDeliveryWorker = class {
215239
+ constructor(deps) {
215240
+ this.deps = deps;
215241
+ }
215242
+ running = false;
215243
+ stopped = false;
215244
+ cursor = "";
215245
+ /** Already-started transport is bounded by its timeout; do not start new deliveries during shutdown. */
215246
+ stop() {
215247
+ this.stopped = true;
215248
+ }
215249
+ async tick() {
215250
+ if (this.running || this.stopped) return;
215251
+ this.running = true;
215252
+ try {
215253
+ const pending = [];
215254
+ let attempts = 0;
215255
+ for (const batch of await this.deps.batches()) {
215256
+ if (this.stopped) return;
215257
+ const markers = await this.deps.markers.listMarkers(batch.actorId);
215258
+ const items = withEscalationAttention(
215259
+ batch.items,
215260
+ batch.companyId,
215261
+ markers,
215262
+ (this.deps.now ?? Date.now)(),
215263
+ this.deps.policy
215264
+ );
215265
+ for (const item of items) {
215266
+ const attention2 = item.attention;
215267
+ if (!item.escalationId || !attention2?.needsAcknowledgement) continue;
215268
+ if (attention2.deliveredAt && Date.parse(attention2.deliveredAt) >= Date.parse(attention2.dueAt)) continue;
215269
+ pending.push({ batch, item, key: JSON.stringify([batch.companyId, batch.actorId, item.escalationId]) });
215270
+ }
215271
+ }
215272
+ pending.sort((a, b2) => a.key < b2.key ? -1 : a.key > b2.key ? 1 : 0);
215273
+ const afterCursor = pending.findIndex((p2) => p2.key > this.cursor);
215274
+ const start = afterCursor < 0 ? 0 : afterCursor;
215275
+ for (let offset = 0; offset < pending.length; offset++) {
215276
+ if (this.stopped) break;
215277
+ if (attempts++ >= (this.deps.maxPerTick ?? 20)) break;
215278
+ const { batch, item: queued, key } = pending[(start + offset) % pending.length];
215279
+ this.cursor = key;
215280
+ try {
215281
+ const current = this.deps.currentItem ? await this.deps.currentItem(batch, queued.escalationId) : queued;
215282
+ if (!current) continue;
215283
+ const item = withEscalationAttention(
215284
+ [current],
215285
+ batch.companyId,
215286
+ await this.deps.markers.listMarkers(batch.actorId),
215287
+ (this.deps.now ?? Date.now)(),
215288
+ this.deps.policy
215289
+ )[0];
215290
+ const attention2 = item.attention;
215291
+ if (this.stopped || !attention2.needsAcknowledgement || attention2.deliveredAt && Date.parse(attention2.deliveredAt) >= Date.parse(attention2.dueAt)) continue;
215292
+ const id = JSON.stringify([batch.companyId, batch.actorId, attention2.reminderKey]);
215293
+ await this.deps.send(batch, item, id);
215294
+ await this.deps.markers.markRead(
215295
+ batch.actorId,
215296
+ escalationAttentionScope(batch.companyId, item.escalationId, "webhook"),
215297
+ attention2.dueAt
215298
+ );
215299
+ } catch {
215300
+ this.deps.log(`[escalation-delivery] delivery failed; retained for retry (${key})`);
215301
+ }
215302
+ }
215303
+ } catch {
215304
+ this.deps.log("[escalation-delivery] scan failed; will retry");
215305
+ } finally {
215306
+ this.running = false;
215307
+ }
215308
+ }
215309
+ };
215310
+ }
215311
+ });
215312
+
214964
215313
  // ../server/src/domains/collab/inbox-chat.ts
214965
215314
  function mergeInbox(kernelItems, chatItems) {
214966
215315
  if (chatItems.length === 0) return kernelItems;
@@ -216408,7 +216757,23 @@ function collabDomain(opts) {
216408
216757
  const { reviews, works } = collectWaitingRows(snaps, me, policy);
216409
216758
  waitingItems = buildWaitingInbox(reviews, works, policy, (/* @__PURE__ */ new Date()).toISOString(), me);
216410
216759
  }
216411
- return { status: 200, body: { items: mergeInbox(kernelItems, [...chatItems, ...waitingItems]) } };
216760
+ const items = mergeInbox(kernelItems, [...chatItems, ...waitingItems]);
216761
+ return { status: 200, body: { items: opts.readMarkers ? withEscalationAttention(items, req.auth.companyId ?? opts.defaultCompanyId ?? "", markers, Date.now(), opts.escalationReminders) : items } };
216762
+ });
216763
+ router.post("/api/inbox/escalations/:id/acknowledge", async (req) => {
216764
+ if (!opts.readMarkers) return { status: 503, body: { error: { code: "not_configured", message: "\u63A5\u624B\u8BB0\u5F55\u672A\u88C5\u914D" } } };
216765
+ const { kernel, registry: registry2 } = await resolveCtx(req.auth.companyId);
216766
+ await kernel.refreshModel?.();
216767
+ const leadOf = await buildLeadResolver(registry2);
216768
+ const item = buildInbox(kernel.model, req.auth.actor, leadOf).find((i) => i.escalationId === req.params.id);
216769
+ if (!item?.escalationId) return { status: 404, body: { error: { code: "not_found", message: "\u5F85\u529E\u4E0D\u5B58\u5728\u6216\u4E0D\u5C5E\u4E8E\u4F60" } } };
216770
+ const acknowledgedAt = (/* @__PURE__ */ new Date()).toISOString();
216771
+ await opts.readMarkers.markRead(
216772
+ req.auth.actor,
216773
+ escalationAttentionScope(req.auth.companyId ?? opts.defaultCompanyId ?? "", item.escalationId, "ack"),
216774
+ acknowledgedAt
216775
+ );
216776
+ return { status: 200, body: { acknowledgedAt } };
216412
216777
  });
216413
216778
  const companyOf = (req) => req.auth.companyId ?? "company:default";
216414
216779
  const noTagStore = { status: 501, body: { error: { code: "not_implemented", message: "\u672C\u90E8\u7F72\u672A\u542F\u7528\u5DE5\u5355\u6807\u7B7E" } } };
@@ -216647,6 +217012,7 @@ var init_collab = __esm({
216647
217012
  init_workorders();
216648
217013
  init_workorder_detail();
216649
217014
  init_inbox();
217015
+ init_escalation_attention();
216650
217016
  init_inbox_chat();
216651
217017
  init_waiting_inbox();
216652
217018
  init_src3();
@@ -216660,6 +217026,7 @@ var init_collab = __esm({
216660
217026
  init_workorder_detail();
216661
217027
  init_workorder_metrics();
216662
217028
  init_inbox();
217029
+ init_escalation_attention();
216663
217030
  init_letter_read_backfill();
216664
217031
  init_audit();
216665
217032
  init_dashboard();
@@ -216686,7 +217053,7 @@ function prepareReadonlyGitBundles(input) {
216686
217053
  if (repos.length === 0) return null;
216687
217054
  const targets = collectTargets(repos, input.pinnedRevisions);
216688
217055
  if (targets.length === 0) return null;
216689
- const bundleRoot = input.bundleRoot ?? os6.tmpdir();
217056
+ const bundleRoot = input.bundleRoot ?? os7.tmpdir();
216690
217057
  fs23.mkdirSync(bundleRoot, { recursive: true });
216691
217058
  const root = fs23.mkdtempSync(path24.join(bundleRoot, "oasis-git-bundles-"));
216692
217059
  const files = {};
@@ -216833,7 +217200,7 @@ function createBundle(root, target, input) {
216833
217200
  function verifyBundleReadable(dir, bundlePath, _bundleRef, commit, expectedTree, env) {
216834
217201
  fs23.rmSync(dir, { recursive: true, force: true });
216835
217202
  try {
216836
- (0, import_node_child_process16.execFileSync)("git", ["clone", bundlePath, dir], { stdio: "ignore", ...env ? { env } : {} });
217203
+ (0, import_node_child_process17.execFileSync)("git", ["clone", bundlePath, dir], { stdio: "ignore", ...env ? { env } : {} });
216837
217204
  git2(dir, ["cat-file", "-e", `${commit}^{commit}`], env, "COMMIT_NOT_FOUND");
216838
217205
  const head = git2(dir, ["rev-parse", "HEAD"], env, "COMMIT_NOT_FOUND").trim();
216839
217206
  if (head.toLowerCase() !== commit.toLowerCase()) {
@@ -216875,11 +217242,11 @@ function ensureSourceRepo(work, repo, commit, env) {
216875
217242
  function gitInitBare(dir, env) {
216876
217243
  if (fs23.existsSync(path24.join(dir, "objects"))) return;
216877
217244
  fs23.mkdirSync(dir, { recursive: true });
216878
- (0, import_node_child_process16.execFileSync)("git", ["init", "--bare", dir], { stdio: "ignore", ...env ? { env } : {} });
217245
+ (0, import_node_child_process17.execFileSync)("git", ["init", "--bare", dir], { stdio: "ignore", ...env ? { env } : {} });
216879
217246
  }
216880
217247
  function git2(cwd, args, env, fallback, timeout) {
216881
217248
  try {
216882
- return (0, import_node_child_process16.execFileSync)("git", ["-C", cwd, ...args], {
217249
+ return (0, import_node_child_process17.execFileSync)("git", ["-C", cwd, ...args], {
216883
217250
  encoding: "utf8",
216884
217251
  stdio: ["ignore", "pipe", "pipe"],
216885
217252
  ...timeout ? { timeout } : {},
@@ -216903,14 +217270,14 @@ function classifyGitError(fallback, err, where) {
216903
217270
  function oneLine(msg) {
216904
217271
  return msg.split("\n").map((s2) => s2.trim()).filter(Boolean).slice(0, 3).join(" ");
216905
217272
  }
216906
- var import_node_child_process16, import_node_crypto45, fs23, os6, path24, GitReadonlyBundleError, HEX40, FETCH_TIMEOUT_MS2;
217273
+ var import_node_child_process17, import_node_crypto45, fs23, os7, path24, GitReadonlyBundleError, HEX40, FETCH_TIMEOUT_MS2;
216907
217274
  var init_readonly_bundle = __esm({
216908
217275
  "../server/src/git/readonly-bundle.ts"() {
216909
217276
  "use strict";
216910
- import_node_child_process16 = require("node:child_process");
217277
+ import_node_child_process17 = require("node:child_process");
216911
217278
  import_node_crypto45 = require("node:crypto");
216912
217279
  fs23 = __toESM(require("node:fs"), 1);
216913
- os6 = __toESM(require("node:os"), 1);
217280
+ os7 = __toESM(require("node:os"), 1);
216914
217281
  path24 = __toESM(require("node:path"), 1);
216915
217282
  init_remote_util();
216916
217283
  GitReadonlyBundleError = class extends Error {
@@ -219417,7 +219784,9 @@ function createChatSessionsDomain(opts) {
219417
219784
  router.get("/api/chat-sessions/:id/turns/active", async (req) => {
219418
219785
  if (!opts.chatTurns) throw new ApiError(404, "NOT_FOUND", "\u672C\u5B9E\u4F8B\u672A\u63A5\u4F1A\u8BDD\u8F6E\u6B21\u8D26\u672C");
219419
219786
  const session = await store.getSession(req.params.id);
219420
- if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
219787
+ if (!session || session.humanActorId !== req.auth.actor && session.aiActorId !== req.auth.actor) {
219788
+ throw new ApiError(404, "NOT_FOUND", "session not found");
219789
+ }
219421
219790
  const turn = await opts.chatTurns.activeTurn(req.params.id);
219422
219791
  return { status: 200, body: { turn } };
219423
219792
  });
@@ -224127,11 +224496,11 @@ var init_engine = __esm({
224127
224496
  constructor(table = /* @__PURE__ */ new Map()) {
224128
224497
  this.table = table;
224129
224498
  }
224130
- set(platform, platformUserId, actor) {
224131
- this.table.set(`${platform}:${platformUserId}`, actor);
224499
+ set(platform2, platformUserId, actor) {
224500
+ this.table.set(`${platform2}:${platformUserId}`, actor);
224132
224501
  }
224133
- resolve(platform, platformUserId) {
224134
- return this.table.get(`${platform}:${platformUserId}`) ?? null;
224502
+ resolve(platform2, platformUserId) {
224503
+ return this.table.get(`${platform2}:${platformUserId}`) ?? null;
224135
224504
  }
224136
224505
  };
224137
224506
  }
@@ -224670,7 +225039,7 @@ ${HIGH_RISK_COMMANDS}`;
224670
225039
  const downstreams = consumersOf(m2, p2.id).map((a) => a.id);
224671
225040
  const task = [
224672
225041
  `## \u672C\u6B21\u4E3A\u4EC0\u4E48\u5524\u8D77\u4F60`,
224673
- `\u8282\u70B9 **${p2.id}**${brief ? `\uFF08${brief}\uFF09` : ""} \u5DF2\u963B\u585E\u7EA6 **${mins} \u5206\u949F**\u6CA1\u8FDB\u5C55\u3002\u7CFB\u7EDF\u5DF2\u673A\u68B0\u8BCA\u65AD\u3001\u786E\u8BA4**\u8FD9\u662F\u56FE\u75C5\u6216\u673A\u68B0\u5B9A\u4F4D\u4E0D\u5230\u7684\u6B8B\u5DEE**\uFF08\u8D44\u6E90 / \u4EBA / \u8FD0\u884C\u65F6\u7C7B\u75C5\u56E0\u5DF2\u6392\u9664\uFF09\u3002`,
225042
+ `\u8282\u70B9 **${p2.id}**${brief ? `\uFF08${brief}\uFF09` : ""} \u5DF2\u963B\u585E\u7EA6 **${mins} \u5206\u949F**\u6CA1\u8FDB\u5C55\u3002\u673A\u68B0\u8BCA\u65AD\u5C06\u5176\u5F52\u5165**\u56FE\u75C5\u6216\u5C1A\u672A\u5B9A\u4F4D\u7684\u6B8B\u5DEE**\u3002\u672A\u53D1\u73B0\u5DF2\u77E5\u8FD0\u884C\u65F6\u4FE1\u53F7\u4E0D\u7B49\u4E8E\u6392\u9664\u4E86\u8FD0\u884C\u65F6\u6545\u969C\uFF1B\u6CA1\u6709\u5728\u8DD1\u4F1A\u8BDD\u65F6\uFF0C\u8BF7\u5148\u6838\u5BF9\u7ED1\u5B9A\u3001\u8282\u70B9\u6CE8\u518C\u72B6\u6001\u548C\u6D3E\u53D1\u9001\u8FBE\u60C5\u51B5\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u6539\u56FE\u3002`,
224674
225043
  ``,
224675
225044
  `## \u75C5\u5386\uFF08\u673A\u68B0\u8BCA\u65AD\uFF1B\u5148\u8BFB\u8FD9\u4E2A\uFF09`,
224676
225045
  caseFile,
@@ -225801,6 +226170,7 @@ var init_src10 = __esm({
225801
226170
  init_orphan_sweep();
225802
226171
  init_dispatch_drop_sweep();
225803
226172
  init_dispatch_mapping_rebuild();
226173
+ init_dispatch_binding_diagnosis();
225804
226174
  init_zero_output_breaker();
225805
226175
  init_dispatch_exit_reconcile();
225806
226176
  init_session_exit_outcome();
@@ -225839,6 +226209,7 @@ var init_src10 = __esm({
225839
226209
  init_node_store();
225840
226210
  init_tokens();
225841
226211
  init_collab();
226212
+ init_collab();
225842
226213
  init_closure_report();
225843
226214
  init_workorders();
225844
226215
  init_workorder_detail();
@@ -233741,22 +234112,22 @@ var init_trajectory = __esm({
233741
234112
 
233742
234113
  // src/index.ts
233743
234114
  var fs42 = __toESM(require("node:fs"));
233744
- var os13 = __toESM(require("node:os"));
234115
+ var os14 = __toESM(require("node:os"));
233745
234116
  var path39 = __toESM(require("node:path"));
233746
- var import_node_child_process22 = require("node:child_process");
234117
+ var import_node_child_process23 = require("node:child_process");
233747
234118
  var import_node_crypto66 = require("node:crypto");
233748
234119
 
233749
234120
  // ../cli/src/cli.ts
233750
234121
  var fs37 = __toESM(require("node:fs"), 1);
233751
- var os11 = __toESM(require("node:os"), 1);
234122
+ var os12 = __toESM(require("node:os"), 1);
233752
234123
  var path34 = __toESM(require("node:path"), 1);
233753
234124
  var import_node_crypto64 = require("node:crypto");
233754
234125
 
233755
234126
  // ../cli/src/serve.ts
233756
234127
  var fs31 = __toESM(require("node:fs"), 1);
233757
- var os7 = __toESM(require("node:os"), 1);
234128
+ var os8 = __toESM(require("node:os"), 1);
233758
234129
  var path28 = __toESM(require("node:path"), 1);
233759
- var import_node_child_process17 = require("node:child_process");
234130
+ var import_node_child_process18 = require("node:child_process");
233760
234131
  var import_node_crypto60 = require("node:crypto");
233761
234132
  var import_node_url7 = require("node:url");
233762
234133
 
@@ -234333,8 +234704,10 @@ var SESSION_TOKEN_GRACE_MS = 15 * 6e4;
234333
234704
  function makeResumeRetryProxy(first, spawnFallback) {
234334
234705
  const outCbs = [];
234335
234706
  const telCbs = [];
234707
+ const liveCbs = [];
234336
234708
  const outBuf = [];
234337
234709
  const telBuf = [];
234710
+ const liveBuf = [];
234338
234711
  let sawOutput = false;
234339
234712
  let active = first;
234340
234713
  const fanOut = (chunk) => {
@@ -234345,12 +234718,17 @@ function makeResumeRetryProxy(first, spawnFallback) {
234345
234718
  if (telCbs.length) for (const cb of telCbs) cb(e);
234346
234719
  else telBuf.push(e);
234347
234720
  };
234721
+ const fanLive = (e) => {
234722
+ if (liveCbs.length) for (const cb of liveCbs) cb(e);
234723
+ else liveBuf.push(e);
234724
+ };
234348
234725
  const wire = (s2) => {
234349
234726
  s2.onOutput((c) => {
234350
234727
  sawOutput = true;
234351
234728
  fanOut(c);
234352
234729
  });
234353
234730
  s2.onTelemetry?.((e) => fanTel(e));
234731
+ s2.onLiveEvent?.((e) => fanLive(e));
234354
234732
  };
234355
234733
  wire(first);
234356
234734
  const isSessionNotFound2 = (err) => {
@@ -234382,6 +234760,10 @@ function makeResumeRetryProxy(first, spawnFallback) {
234382
234760
  for (const e of telBuf.splice(0)) cb(e);
234383
234761
  telCbs.push(cb);
234384
234762
  },
234763
+ onLiveEvent(cb) {
234764
+ for (const e of liveBuf.splice(0)) cb(e);
234765
+ liveCbs.push(cb);
234766
+ },
234385
234767
  done,
234386
234768
  kill: () => active.kill?.() ?? Promise.resolve(),
234387
234769
  // ADR-0093 chat 道:插入能力必须穿过这层代理,否则「带 sessionId 的 resume」这条**常路径**
@@ -234392,6 +234774,11 @@ function makeResumeRetryProxy(first, spawnFallback) {
234392
234774
  get canAppendInput() {
234393
234775
  return active.canAppendInput !== false && typeof active.appendInput === "function";
234394
234776
  },
234777
+ // supportsLiveProtocol 恒读 active(兜底重派后 active 换成 second):不能按 first 快照,否则回退会话的
234778
+ // 能力位会指向已死的首次尝试。
234779
+ get supportsLiveProtocol() {
234780
+ return active.supportsLiveProtocol === true;
234781
+ },
234395
234782
  get nativeSessionId() {
234396
234783
  return active.nativeSessionId;
234397
234784
  }
@@ -234452,15 +234839,15 @@ function oasisWrapperScript() {
234452
234839
  const main = path28.join(repoRoot, "packages/cli/src/main.ts");
234453
234840
  const key = (0, import_node_crypto60.createHash)("sha256").update(repoRoot).digest("hex").slice(0, 12);
234454
234841
  const uid = typeof process.getuid === "function" ? process.getuid() : "na";
234455
- const stable = path28.join(os7.tmpdir(), `oasis-cli-wrapper-${uid}-${key}`);
234842
+ const stable = path28.join(os8.tmpdir(), `oasis-cli-wrapper-${uid}-${key}`);
234456
234843
  let dir;
234457
234844
  try {
234458
234845
  fs31.mkdirSync(stable, { recursive: true, mode: 448 });
234459
234846
  const st = fs31.lstatSync(stable);
234460
234847
  const mine = st.isDirectory() && !st.isSymbolicLink() && (typeof process.getuid !== "function" || st.uid === process.getuid());
234461
- dir = mine ? stable : fs31.mkdtempSync(path28.join(os7.tmpdir(), "oasis-cli-"));
234848
+ dir = mine ? stable : fs31.mkdtempSync(path28.join(os8.tmpdir(), "oasis-cli-"));
234462
234849
  } catch {
234463
- dir = fs31.mkdtempSync(path28.join(os7.tmpdir(), "oasis-cli-"));
234850
+ dir = fs31.mkdtempSync(path28.join(os8.tmpdir(), "oasis-cli-"));
234464
234851
  }
234465
234852
  const wrapper = path28.join(dir, "oasis");
234466
234853
  fs31.writeFileSync(wrapper, `#!/usr/bin/env bash
@@ -234538,7 +234925,7 @@ async function runDeterministicKnowledgeLint(service, config2) {
234538
234925
  if (!wrapper) throw new Error(`\u8FDE\u63A5\u5668 ${config2.connectorSlug} \u5C1A\u672A\u914D\u7F6E\u6216 wrapper \u4E0D\u53EF\u7528`);
234539
234926
  const args = [...connector.knowledgeLintArgs({ libraryRef: config2.libraryRef, rootRef: config2.rootRef })];
234540
234927
  const result = await new Promise((resolveResult) => {
234541
- (0, import_node_child_process17.execFile)(wrapper, args, {
234928
+ (0, import_node_child_process18.execFile)(wrapper, args, {
234542
234929
  env: { ...process.env, ...provision.env },
234543
234930
  timeout: 12e4,
234544
234931
  maxBuffer: 10 * 1024 * 1024
@@ -234808,7 +235195,7 @@ async function startServe(opts) {
234808
235195
  );
234809
235196
  const issuer = createTokenIssuer({ secretFile: path28.join(opts.dir, "session-token-secret") });
234810
235197
  const apiActivity = createApiActivityLedger();
234811
- const operatorActor = `actor:human:${os7.userInfo().username}`;
235198
+ const operatorActor = `actor:human:${os8.userInfo().username}`;
234812
235199
  const operatorToken = issuer.issue(operatorActor);
234813
235200
  fs31.writeFileSync(path28.join(opts.dir, "operator-token"), operatorToken, { mode: 384 });
234814
235201
  let nodeTokens = createNodeTokenStore(path28.join(opts.dir, "node-tokens.json"));
@@ -234929,6 +235316,7 @@ async function startServe(opts) {
234929
235316
  let chatSessionStore;
234930
235317
  let chatTurnStore;
234931
235318
  let readMarkerStore;
235319
+ const escalationDelivery = escalationDeliveryConfig(process.env);
234932
235320
  if (pgDsn && pgPool) {
234933
235321
  projectStateStore = await PostgresProjectStateStore.open(pgPool, pgSchema);
234934
235322
  artifactStateStore = await PostgresArtifactStateStore.open(pgPool, pgSchema);
@@ -235963,6 +236351,8 @@ async function startServe(opts) {
235963
236351
  chatSession: chatSessionStore,
235964
236352
  // ADR-0086:chat 知会条目(与 readMarkers 成对)
235965
236353
  readMarkers: readMarkerStore,
236354
+ escalationReminders: escalationDelivery.policy,
236355
+ defaultCompanyId,
235966
236356
  // 工单标签词表(组织级受控、旁账,与 playbook-overrides 同款侧存)。
235967
236357
  tags: workorderTagStore,
235968
236358
  ...opts.sla ? { sla: opts.sla } : {},
@@ -237504,6 +237894,8 @@ async function startServe(opts) {
237504
237894
  ledgerWarn("cancel(map-miss)")(err);
237505
237895
  }
237506
237896
  },
237897
+ // Inbox is the notification contract (ADR 0090). Escalation delivery reconciles persisted Inbox
237898
+ // items below; it must not depend on this one-shot effect or its legacy recipient list.
237507
237899
  async notify(_actorIds, _payload) {
237508
237900
  }
237509
237901
  };
@@ -237524,6 +237916,14 @@ async function startServe(opts) {
237524
237916
  return live;
237525
237917
  },
237526
237918
  isAgent: async (actorId) => await executorKindOf(actorId) === "agent",
237919
+ beforeRedispatch: async (work) => {
237920
+ return prepareDroppedDispatch(work, {
237921
+ kernel: newKernel,
237922
+ // This sweep belongs to the default engine; legacy workorder.companyId is often empty.
237923
+ bindingFor: async (actorId) => (await actors.resolveCtx(defaultCompanyId)).service.resolveBinding()(actorId),
237924
+ nodeFor: (nodeId) => nodeStore.getNode(nodeId)
237925
+ });
237926
+ },
237527
237927
  // resumeEligible=false:补派一律起**全新会话**。引擎那次派发的取值是节点状态的函数
237528
237928
  //(activate.ts:`latestAcceptId == null && !lastProposedRejected`),本模块拿不到,
237529
237929
  // 而两个方向的代价不对称——该续没续只是少一点上下文,不该续却续了就把返工要隔离的
@@ -238371,7 +238771,45 @@ async function startServe(opts) {
238371
238771
  return { fused, starving, stuck, recovered };
238372
238772
  }
238373
238773
  const slaEscalating = /* @__PURE__ */ new Set();
238774
+ const escalationDeliveryWorker = new EscalationDeliveryWorker({
238775
+ markers: readMarkerStore,
238776
+ policy: escalationDelivery.policy,
238777
+ batches: async () => {
238778
+ const batches = [];
238779
+ for (const companyId of escalationDelivery.webhooks.keys()) {
238780
+ const companyEngine = await getEngine(companyId);
238781
+ await companyEngine.kernel.refreshModel();
238782
+ const leadOf2 = await buildLeadResolver(companyEngine.registry);
238783
+ const recipients = /* @__PURE__ */ new Set();
238784
+ for (const node2 of companyEngine.kernel.model.artifacts.values()) {
238785
+ recipients.add(node2.owner);
238786
+ const lead = leadOf2?.(node2.owner);
238787
+ if (lead) recipients.add(lead);
238788
+ }
238789
+ for (const actorId of recipients) {
238790
+ const items = buildInbox(companyEngine.kernel.model, actorId, leadOf2).filter((item) => item.escalationId && item.artifactId && lifecycleOf(companyEngine.kernel.model, item.artifactId) === "active");
238791
+ if (items.length) batches.push({ companyId, actorId, items });
238792
+ }
238793
+ }
238794
+ return batches;
238795
+ },
238796
+ currentItem: async (batch, issueId) => {
238797
+ const companyEngine = await getEngine(batch.companyId);
238798
+ await companyEngine.kernel.refreshModel();
238799
+ const leadOf2 = await buildLeadResolver(companyEngine.registry);
238800
+ return buildInbox(companyEngine.kernel.model, batch.actorId, leadOf2).find((item) => item.escalationId === issueId && item.artifactId && lifecycleOf(companyEngine.kernel.model, item.artifactId) === "active");
238801
+ },
238802
+ send: async (batch, item, deliveryId) => {
238803
+ const endpoint = escalationDelivery.webhooks.get(batch.companyId);
238804
+ await sendEscalationWebhook(endpoint.url, endpoint.token, batch, item, deliveryId);
238805
+ },
238806
+ log: (message) => console.warn(message)
238807
+ });
238808
+ if (!escalationDelivery.webhooks.size) {
238809
+ console.warn("[escalation-delivery] \u7AD9\u5916\u6E20\u9053\u672A\u914D\u7F6E\uFF1A\u5347\u7EA7\u5C06\u901A\u8FC7 Inbox \u5F39\u7A97\u548C\u50AC\u529E\u63D0\u9192\uFF1B\u6D4F\u89C8\u5668\u5173\u95ED\u65F6\u65E0\u6CD5\u7AD9\u5916\u9001\u8FBE\u3002\u914D\u7F6E OASIS_ESCALATION_WEBHOOKS \u53EF\u63A5\u90AE\u4EF6/IM \u7F51\u5173\u3002");
238810
+ }
238374
238811
  const slaTimer = setInterval(() => {
238812
+ void escalationDeliveryWorker.tick();
238375
238813
  void (async () => {
238376
238814
  const now = Date.now();
238377
238815
  for (const a of kernel.model.artifacts.values()) {
@@ -238683,6 +239121,7 @@ ${nodeFault}` : "");
238683
239121
  for (const c of chatLiveSessions.values()) ids2.add(c.sessionId);
238684
239122
  return ids2;
238685
239123
  };
239124
+ const liveSessionRunIds = () => hub?.liveSessionDispatchIds() ?? /* @__PURE__ */ new Set();
238686
239125
  const aliveArtifactIds = () => {
238687
239126
  const ids2 = /* @__PURE__ */ new Set();
238688
239127
  for (const d of hub?.connectedDaemons() ?? []) {
@@ -238713,6 +239152,8 @@ ${nodeFault}` : "");
238713
239152
  aliveArtifactIds,
238714
239153
  apiActiveDispatchIds,
238715
239154
  // D1(ADR-0078):判活第二条线
239155
+ liveSessionRunIds,
239156
+ // ADR-0300 D8:hub 活数据面连接(协调者/独立会话进程)
238716
239157
  closeRun,
238717
239158
  now: Date.now(),
238718
239159
  orphanGraceMs,
@@ -238892,6 +239333,7 @@ ${nodeFault}` : "");
238892
239333
  close: async () => {
238893
239334
  for (const timer of dispatchTimers) clearInterval(timer);
238894
239335
  clearInterval(slaTimer);
239336
+ escalationDeliveryWorker.stop();
238895
239337
  if (mirrorTimer) clearInterval(mirrorTimer);
238896
239338
  if (gitTimer) clearInterval(gitTimer);
238897
239339
  if (coordTimer) clearInterval(coordTimer);
@@ -238991,7 +239433,7 @@ function log2(tag, msg, extra) {
238991
239433
  }
238992
239434
 
238993
239435
  // ../cli/src/daemon/session-lock.ts
238994
- var import_node_child_process18 = require("node:child_process");
239436
+ var import_node_child_process19 = require("node:child_process");
238995
239437
  var fs32 = __toESM(require("node:fs"), 1);
238996
239438
  var path29 = __toESM(require("node:path"), 1);
238997
239439
  var flockAvailable = null;
@@ -238999,7 +239441,7 @@ function hasFlock() {
238999
239441
  if (flockAvailable !== null) return flockAvailable;
239000
239442
  if (process.platform !== "linux") return flockAvailable = false;
239001
239443
  try {
239002
- const r = (0, import_node_child_process18.spawnSync)("flock", ["--version"], { stdio: "ignore", timeout: 3e3 });
239444
+ const r = (0, import_node_child_process19.spawnSync)("flock", ["--version"], { stdio: "ignore", timeout: 3e3 });
239003
239445
  flockAvailable = r.status === 0;
239004
239446
  } catch {
239005
239447
  flockAvailable = false;
@@ -239014,7 +239456,7 @@ function acquireSessionLock(lockPath) {
239014
239456
  fs32.unlinkSync(readyFile);
239015
239457
  } catch {
239016
239458
  }
239017
- const child = (0, import_node_child_process18.spawn)("flock", ["-n", lockPath, "-c", `printf ok > '${readyFile}'; exec cat`], {
239459
+ const child = (0, import_node_child_process19.spawn)("flock", ["-n", lockPath, "-c", `printf ok > '${readyFile}'; exec cat`], {
239018
239460
  stdio: ["pipe", "ignore", "ignore"],
239019
239461
  detached: true
239020
239462
  });
@@ -239025,7 +239467,7 @@ function acquireSessionLock(lockPath) {
239025
239467
  ready = true;
239026
239468
  break;
239027
239469
  }
239028
- (0, import_node_child_process18.spawnSync)("sleep", ["0.02"], { stdio: "ignore" });
239470
+ (0, import_node_child_process19.spawnSync)("sleep", ["0.02"], { stdio: "ignore" });
239029
239471
  }
239030
239472
  try {
239031
239473
  fs32.unlinkSync(readyFile);
@@ -239056,7 +239498,7 @@ function isSessionLocked(lockPath) {
239056
239498
  if (!hasFlock()) return false;
239057
239499
  if (!fs32.existsSync(lockPath)) return false;
239058
239500
  try {
239059
- const r = (0, import_node_child_process18.spawnSync)("flock", ["-n", lockPath, "-c", "true"], { stdio: "ignore", timeout: 3e3 });
239501
+ const r = (0, import_node_child_process19.spawnSync)("flock", ["-n", lockPath, "-c", "true"], { stdio: "ignore", timeout: 3e3 });
239060
239502
  return r.status !== 0;
239061
239503
  } catch {
239062
239504
  return true;
@@ -239131,7 +239573,10 @@ async function runSession(dispatchId, job, deps) {
239131
239573
  var RECONNECT_MIN_MS = 1e3;
239132
239574
  var RECONNECT_MAX_MS = 3e4;
239133
239575
  var EXIT_ACK_WAIT_MS = LINK_WINDOWS.exitAckWaitMs;
239576
+ var OUTBOX_MAX_FRAMES = 8e3;
239577
+ var isKeyFrame = (m2) => m2.type === "session_started" || m2.type === "session_exited";
239134
239578
  async function runSessionProcess(specFile, adapter) {
239579
+ process.env["OASIS_BIND_RUNTIME_PDEATHSIG"] = "1";
239135
239580
  const spec = readSpec(specFile);
239136
239581
  const { dispatchId, job, auth, sessionDir } = spec;
239137
239582
  const lock = acquireSessionLock(path30.join(sessionDir, "lock"));
@@ -239185,6 +239630,10 @@ var SessionProcessState = class {
239185
239630
  * 而会话其实正在跑。
239186
239631
  */
239187
239632
  outbox = [];
239633
+ /** ADR 0300 D2:每 dispatch 单调帧号,任何副作用之前分配。 */
239634
+ frameSeq = 0;
239635
+ /** 收到过带 `ackSeq` 的 ack = 本服务端会按帧号去重;只有它为真,重连才敢重放**全部**帧(含输出)。 */
239636
+ serverSupportsDedup = false;
239188
239637
  /** append_input 串行链——保证回帧次序 = 到达次序(server 侧按 FIFO 认领等待者)。 */
239189
239638
  appendChain = Promise.resolve();
239190
239639
  async run() {
@@ -239250,9 +239699,12 @@ var SessionProcessState = class {
239250
239699
  daemonVersion: this.spec.daemonVersion,
239251
239700
  // 自报身份要够服务端对上账:chat 道的对账键是 artifactId(run.id=chat-run:* ≠ dispatchId),
239252
239701
  // 只报 dispatchId 的话 serve 重启后它认不出这是条活会话,会当成野进程 evict 掉。
239253
- ...this.spec.job.artifactId ? { jobArtifactId: this.spec.job.artifactId } : {}
239702
+ ...this.spec.job.artifactId ? { jobArtifactId: this.spec.job.artifactId } : {},
239703
+ // ADR 0300 D2:自报支持帧号;新服务端据此回带 ackSeq 的 hello-ack,届时才把 serverSupportsDedup 置真。
239704
+ supportsFrameSeq: true
239254
239705
  });
239255
- for (const m2 of this.outbox) this.rawSend(m2);
239706
+ const toResend = this.serverSupportsDedup ? this.outbox : this.outbox.filter(isKeyFrame);
239707
+ for (const m2 of toResend) this.rawSend(m2);
239256
239708
  });
239257
239709
  ws.on("message", (raw) => {
239258
239710
  let msg;
@@ -239300,8 +239752,14 @@ var SessionProcessState = class {
239300
239752
  }
239301
239753
  case "ack":
239302
239754
  if (msg.dispatchId !== this.spec.dispatchId) return;
239303
- this.outbox.length = 0;
239304
- this.exitAckResolve?.();
239755
+ if (typeof msg.ackSeq === "number") {
239756
+ this.serverSupportsDedup = true;
239757
+ const wm = msg.ackSeq;
239758
+ this.outbox = this.outbox.filter((f2) => (f2.seq ?? 0) > wm);
239759
+ } else {
239760
+ this.outbox.length = 0;
239761
+ }
239762
+ if (this.outbox.length === 0) this.exitAckResolve?.();
239305
239763
  return;
239306
239764
  case "ping":
239307
239765
  this.rawSend({ type: "pong" });
@@ -239316,14 +239774,20 @@ var SessionProcessState = class {
239316
239774
  this.ws.send(JSON.stringify(msg));
239317
239775
  return true;
239318
239776
  }
239319
- /** 必达:先进 outbox 再尽力发;ack 到达才清。 */
239777
+ /** ADR 0300 D2:给帧派单调 seq、进 outbox(超上限丢最老)、再尽力发。返回带 seq 的帧。 */
239778
+ stampAndBuffer(msg) {
239779
+ const framed = { ...msg, seq: ++this.frameSeq };
239780
+ this.outbox.push(framed);
239781
+ if (this.outbox.length > OUTBOX_MAX_FRAMES) this.outbox.splice(0, this.outbox.length - OUTBOX_MAX_FRAMES);
239782
+ return framed;
239783
+ }
239784
+ /** 关键帧(started/exited):必达,进 outbox,ack 到达(按 ackSeq 或整清)才删。 */
239320
239785
  sendKeyFrame(msg) {
239321
- this.outbox.push(msg);
239322
- this.rawSend(msg);
239786
+ this.rawSend(this.stampAndBuffer(msg));
239323
239787
  }
239324
- /** 可丢:连着就发,断着就算了。输出流不值得为它攒内存——真需要补的是必达帧。 */
239788
+ /** 输出/事件帧:ADR 0300 D2 起也进 outbox 带 seq——断线期不再丢;服务端按 ackSeq 累积回执后即从 outbox 排掉。 */
239325
239789
  sendStream(msg) {
239326
- this.rawSend(msg);
239790
+ this.rawSend(this.stampAndBuffer(msg));
239327
239791
  }
239328
239792
  // ── 看门狗(D2′.5 防线 3 + 防线 1 后半)──────────────────────────────────
239329
239793
  startWatchdog() {
@@ -239348,7 +239812,7 @@ var SessionProcessState = class {
239348
239812
  await this.handle?.kill();
239349
239813
  } catch {
239350
239814
  }
239351
- this.rawSend({
239815
+ this.sendKeyFrame({
239352
239816
  type: "session_exited",
239353
239817
  dispatchId: this.spec.dispatchId,
239354
239818
  sessionId: this.handle?.id ?? this.spec.dispatchId,
@@ -239395,7 +239859,7 @@ init_src8();
239395
239859
  init_src7();
239396
239860
 
239397
239861
  // ../cli/src/daemon/detect-adapters.ts
239398
- var import_node_child_process19 = require("node:child_process");
239862
+ var import_node_child_process20 = require("node:child_process");
239399
239863
  init_src7();
239400
239864
  var KNOWN2 = [
239401
239865
  { adapter: "claude-code", binary: "claude" },
@@ -239419,7 +239883,7 @@ var LIVE_PROTOCOL_RUNTIMES = /* @__PURE__ */ new Set(["claude-code"]);
239419
239883
  function resolveOnPath2(binary) {
239420
239884
  try {
239421
239885
  const cmd = process.platform === "win32" ? "where" : "which";
239422
- const out = (0, import_node_child_process19.execFileSync)(cmd, [binary], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
239886
+ const out = (0, import_node_child_process20.execFileSync)(cmd, [binary], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
239423
239887
  const first = out.split("\n")[0]?.trim();
239424
239888
  return first || binary;
239425
239889
  } catch {
@@ -239428,7 +239892,7 @@ function resolveOnPath2(binary) {
239428
239892
  }
239429
239893
  function versionOf(binary) {
239430
239894
  try {
239431
- return (0, import_node_child_process19.execFileSync)(binary, ["--version"], {
239895
+ return (0, import_node_child_process20.execFileSync)(binary, ["--version"], {
239432
239896
  encoding: "utf8",
239433
239897
  stdio: ["ignore", "pipe", "ignore"],
239434
239898
  timeout: 1500
@@ -240384,7 +240848,7 @@ async function startNode(opts) {
240384
240848
  }
240385
240849
 
240386
240850
  // ../cli/src/exec.ts
240387
- var os10 = __toESM(require("node:os"), 1);
240851
+ var os11 = __toESM(require("node:os"), 1);
240388
240852
  var UnsafeExecArgvError = class extends Error {
240389
240853
  code = "ERR_UNSAFE_EXEC_ARGV";
240390
240854
  constructor(message) {
@@ -240421,7 +240885,7 @@ function planExec(opts) {
240421
240885
  function computeExitCode(code2, signal) {
240422
240886
  if (code2 !== null) return code2;
240423
240887
  if (signal) {
240424
- const num2 = os10.constants.signals[signal];
240888
+ const num2 = os11.constants.signals[signal];
240425
240889
  return 128 + (typeof num2 === "number" ? num2 : 1);
240426
240890
  }
240427
240891
  return 1;
@@ -240445,7 +240909,7 @@ function forwardSignal(child, sig, killGroup = (pgid, signal) => {
240445
240909
  }
240446
240910
 
240447
240911
  // ../cli/src/daemon/machine-id.ts
240448
- var import_node_child_process20 = require("node:child_process");
240912
+ var import_node_child_process21 = require("node:child_process");
240449
240913
  var import_node_fs20 = require("node:fs");
240450
240914
  var import_node_crypto62 = require("node:crypto");
240451
240915
  var import_node_os12 = require("node:os");
@@ -240461,7 +240925,7 @@ function linuxMachineId() {
240461
240925
  }
240462
240926
  function macMachineId() {
240463
240927
  try {
240464
- const out = (0, import_node_child_process20.execFileSync)("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
240928
+ const out = (0, import_node_child_process21.execFileSync)("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
240465
240929
  encoding: "utf8",
240466
240930
  timeout: 2e3,
240467
240931
  stdio: ["ignore", "pipe", "ignore"]
@@ -240474,7 +240938,7 @@ function macMachineId() {
240474
240938
  }
240475
240939
  function windowsMachineId() {
240476
240940
  try {
240477
- const out = (0, import_node_child_process20.execFileSync)("reg", [
240941
+ const out = (0, import_node_child_process21.execFileSync)("reg", [
240478
240942
  "query",
240479
240943
  "HKLM\\SOFTWARE\\Microsoft\\Cryptography",
240480
240944
  "/v",
@@ -242184,7 +242648,7 @@ function ownFlagsFromDecl(command) {
242184
242648
 
242185
242649
  // ../cli/src/connector-effect-runner.ts
242186
242650
  var import_node_crypto63 = require("node:crypto");
242187
- var import_node_child_process21 = require("node:child_process");
242651
+ var import_node_child_process22 = require("node:child_process");
242188
242652
  init_src8();
242189
242653
  function stableKey(effect) {
242190
242654
  const keyFields = Object.fromEntries(Object.entries(effect.keyFields).sort(([a], [b2]) => a.localeCompare(b2)));
@@ -242243,7 +242707,7 @@ function productionApi() {
242243
242707
  }
242244
242708
  function execute(bin, args) {
242245
242709
  return new Promise((resolve10, reject) => {
242246
- const child = (0, import_node_child_process21.spawn)(bin, args, { stdio: "inherit", env: process.env });
242710
+ const child = (0, import_node_child_process22.spawn)(bin, args, { stdio: "inherit", env: process.env });
242247
242711
  child.once("error", reject);
242248
242712
  child.once("exit", (code2, signal) => resolve10(code2 ?? (signal ? 128 : 1)));
242249
242713
  });
@@ -242673,7 +243137,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
242673
243137
  const res = await fetch(`${httpBase2}/api/nodes/exchange`, {
242674
243138
  method: "POST",
242675
243139
  headers: { "content-type": "application/json" },
242676
- body: JSON.stringify({ enrollToken, nodeId, hostname: os11.hostname() })
243140
+ body: JSON.stringify({ enrollToken, nodeId, hostname: os12.hostname() })
242677
243141
  });
242678
243142
  if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
242679
243143
  const body2 = await res.json();
@@ -242706,7 +243170,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
242706
243170
  const binPath = path34.join(prefix, "bin", "oasis_test");
242707
243171
  const svcArgs = `node --server-ws ${serverUrl} --dir ${dir}${nodeName ? ` --name ${nodeName}` : ""}`;
242708
243172
  if (process.platform === "linux") {
242709
- const svcFile = path34.join(os11.homedir(), ".config/systemd/user/oasis-node.service");
243173
+ const svcFile = path34.join(os12.homedir(), ".config/systemd/user/oasis-node.service");
242710
243174
  fs37.mkdirSync(path34.dirname(svcFile), { recursive: true });
242711
243175
  if (spawnSync3("systemctl", ["--user", "is-active", "--quiet", "oasis-node"], {}).status === 0)
242712
243176
  spawnSync3("systemctl", ["--user", "stop", "oasis-node"], { stdio: "inherit" });
@@ -242719,7 +243183,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
242719
243183
  `ExecStart=${binPath} ${svcArgs}`,
242720
243184
  // Carry the install-time PATH so the detached daemon detects CLIs in user dirs
242721
243185
  // (~/.npm-global/bin, ~/.local/bin) — systemd --user otherwise gives a minimal PATH.
242722
- `Environment=PATH=${process.env["PATH"] ?? ""}:${path34.join(os11.homedir(), ".npm-global/bin")}:${path34.join(os11.homedir(), ".local/bin")}:/usr/local/bin`,
243186
+ `Environment=PATH=${process.env["PATH"] ?? ""}:${path34.join(os12.homedir(), ".npm-global/bin")}:${path34.join(os12.homedir(), ".local/bin")}:/usr/local/bin`,
242723
243187
  "Restart=on-failure",
242724
243188
  "RestartSec=5",
242725
243189
  "StandardOutput=journal",
@@ -242731,23 +243195,23 @@ async function runCli(argv, println = console.log, progressln = console.error) {
242731
243195
  ].join("\n"));
242732
243196
  spawnSync3("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
242733
243197
  spawnSync3("systemctl", ["--user", "enable", "--now", "oasis-node"], { stdio: "inherit" });
242734
- spawnSync3("loginctl", ["enable-linger", os11.userInfo().username], { stdio: "ignore" });
243198
+ spawnSync3("loginctl", ["enable-linger", os12.userInfo().username], { stdio: "ignore" });
242735
243199
  println("\u2713 Oasis node service started (systemd --user oasis-node)");
242736
243200
  println(" Status : systemctl --user status oasis-node");
242737
243201
  println(" Logs : journalctl --user -u oasis-node -f");
242738
243202
  println(" Stop : systemctl --user stop oasis-node");
242739
243203
  } else if (process.platform === "darwin") {
242740
- const plist = path34.join(os11.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
243204
+ const plist = path34.join(os12.homedir(), "Library/LaunchAgents/com.oasis.node.plist");
242741
243205
  fs37.mkdirSync(path34.dirname(plist), { recursive: true });
242742
243206
  spawnSync3("launchctl", ["unload", plist], { stdio: "ignore" });
242743
243207
  const argXml = [binPath, "node", "--server-ws", serverUrl, "--dir", dir, ...nodeName ? ["--name", nodeName] : []].map((a) => `<string>${a}</string>`).join("");
242744
- fs37.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.oasis.node</string><key>ProgramArguments</key><array>${argXml}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${os11.homedir()}/.oasis-node.log</string><key>StandardErrorPath</key><string>${os11.homedir()}/.oasis-node.err</string></dict></plist>`);
243208
+ fs37.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.oasis.node</string><key>ProgramArguments</key><array>${argXml}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>${os12.homedir()}/.oasis-node.log</string><key>StandardErrorPath</key><string>${os12.homedir()}/.oasis-node.err</string></dict></plist>`);
242745
243209
  spawnSync3("launchctl", ["load", plist], { stdio: "inherit" });
242746
243210
  println("\u2713 Oasis node service started (macOS LaunchAgent com.oasis.node)");
242747
- println(` Logs : tail -f ${os11.homedir()}/.oasis-node.log`);
243211
+ println(` Logs : tail -f ${os12.homedir()}/.oasis-node.log`);
242748
243212
  println(` Stop : launchctl unload ${plist}`);
242749
243213
  } else {
242750
- spawnSync3("sh", ["-c", `nohup ${binPath} ${svcArgs} > ${path34.join(os11.homedir(), ".oasis-node.log")} 2>&1 &`], { stdio: "inherit" });
243214
+ spawnSync3("sh", ["-c", `nohup ${binPath} ${svcArgs} > ${path34.join(os12.homedir(), ".oasis-node.log")} 2>&1 &`], { stdio: "inherit" });
242751
243215
  println(`\u2713 Oasis node started in background (nohup). Logs: ~/.oasis-node.log`);
242752
243216
  println(" Note: no automatic restart on reboot in this environment.");
242753
243217
  }
@@ -242846,8 +243310,8 @@ async function runCli(argv, println = console.log, progressln = console.error) {
242846
243310
  if (command === "serve" && positional[0] === "install") {
242847
243311
  const absDir = path34.resolve(dir);
242848
243312
  const name = `oasis-serve-${path34.basename(absDir)}`;
242849
- const unitDir = flags.get("unit-dir") ?? path34.join(os11.homedir(), ".config", "systemd", "user");
242850
- const bin = flags.get("bin") ?? path34.join(os11.homedir(), ".local", "bin", "oasis");
243313
+ const unitDir = flags.get("unit-dir") ?? path34.join(os12.homedir(), ".config", "systemd", "user");
243314
+ const bin = flags.get("bin") ?? path34.join(os12.homedir(), ".local", "bin", "oasis");
242851
243315
  const port = flags.get("port") ?? "7320";
242852
243316
  const extra = (flags.get("dispatch") === "true" ? ` --dispatch true` : "") + (flags.has("model") ? ` --model ${flags.get("model")}` : "");
242853
243317
  const unit = [
@@ -243191,7 +243655,7 @@ Trace: ${run.traceRunId}`);
243191
243655
  if (plan.inject.mode === "env") {
243192
243656
  childEnv[plan.inject.envName] = value2;
243193
243657
  } else {
243194
- tmpDir = await fs37.promises.mkdtemp(path34.join(os11.tmpdir(), "oasis-var-exec-"));
243658
+ tmpDir = await fs37.promises.mkdtemp(path34.join(os12.tmpdir(), "oasis-var-exec-"));
243195
243659
  const keyfile = path34.join(tmpDir, "value");
243196
243660
  await fs37.promises.writeFile(keyfile, value2, { mode: 384 });
243197
243661
  childEnv[plan.inject.fileEnv] = keyfile;
@@ -243965,7 +244429,7 @@ ${res.warning}`);
243965
244429
  for (const a of st.artifacts) println(` - ${a.path}\uFF08${a.kind ?? "?"}\uFF09status=${a.status ?? "?"}${a.mtime ? ` \xB7 ${new Date(a.mtime).toISOString()}` : ""}`);
243966
244430
  if (flags.get("pull") === "true") {
243967
244431
  const odPublic2 = (flags.get("od-public-url") ?? process.env["OASIS_OPEN_DESIGN_PUBLIC_URL"] ?? "https://design.open-friday.com").replace(/\/$/, "");
243968
- const { dir: dir3, entry: entry2 } = await harvestBoard(odBase2, fetch, odPublic2, os11.tmpdir(), st.boardKey, st.runs[0]?.runId ?? "status-pull");
244432
+ const { dir: dir3, entry: entry2 } = await harvestBoard(odBase2, fetch, odPublic2, os12.tmpdir(), st.boardKey, st.runs[0]?.runId ?? "status-pull");
243969
244433
  println(`[design] \u5DF2\u62C9\u5230\u672C\u5730\uFF1A${dir3}\uFF08\u5165\u53E3 ${entry2}\uFF09`);
243970
244434
  println(`[design] \u8BFB\u5B8C\u6EE1\u610F\u5373\u4EA4\u7A3F\uFF1Aoasis propose ${artifactId2} --from-dir ${dir3} --reason "<\u8FD9\u7248\u505A\u4E86\u4EC0\u4E48>"`);
243971
244435
  } else {
@@ -245335,10 +245799,10 @@ function runtimeAssetSourceRoots(input) {
245335
245799
 
245336
245800
  // src/paths.ts
245337
245801
  var fs40 = __toESM(require("node:fs"));
245338
- var os12 = __toESM(require("node:os"));
245802
+ var os13 = __toESM(require("node:os"));
245339
245803
  var path37 = __toESM(require("node:path"));
245340
245804
  var DEFAULT_INSTANCE = "default";
245341
- var home = () => os12.homedir();
245805
+ var home = () => os13.homedir();
245342
245806
  var oasisDir = () => path37.join(home(), ".oasis");
245343
245807
  var versionsDir = () => path37.join(oasisDir(), "versions");
245344
245808
  var versionDir = (label) => path37.join(versionsDir(), label);
@@ -245537,7 +246001,7 @@ function shimScript() {
245537
246001
  }
245538
246002
 
245539
246003
  // src/index.ts
245540
- var PKG_VERSION = true ? "0.1.137" : "dev";
246004
+ var PKG_VERSION = true ? "0.1.139" : "dev";
245541
246005
  var LOCAL_BIN = localBin();
245542
246006
  var NPM_PREFIX = npmPrefix();
245543
246007
  var INSTANCE = DEFAULT_INSTANCE;
@@ -245637,7 +246101,7 @@ function installSoftware(label) {
245637
246101
  function setupAutostart(name) {
245638
246102
  const inst = instancePaths(name);
245639
246103
  const cmd = `${process.execPath} "${inst.binFile}" --_daemon --instance ${name}`;
245640
- const home2 = os13.homedir();
246104
+ const home2 = os14.homedir();
245641
246105
  const pnpmHome = process.env["PNPM_HOME"] || path39.join(home2, ".local", "share", "pnpm");
245642
246106
  const extraBins = [
245643
246107
  path39.join(home2, ".npm-global", "bin"),
@@ -245647,7 +246111,7 @@ function setupAutostart(name) {
245647
246111
  ];
245648
246112
  const daemonPath = sanitizeDaemonPath([process.env["PATH"] ?? "", ...extraBins].filter(Boolean).join(":"));
245649
246113
  if (process.platform === "linux") {
245650
- const unitDir = path39.join(os13.homedir(), ".config", "systemd", "user");
246114
+ const unitDir = path39.join(os14.homedir(), ".config", "systemd", "user");
245651
246115
  fs42.mkdirSync(unitDir, { recursive: true });
245652
246116
  const unit = systemdUnitName(name);
245653
246117
  fs42.mkdirSync(path39.dirname(inst.logFile), { recursive: true });
@@ -245660,19 +246124,19 @@ function setupAutostart(name) {
245660
246124
  }));
245661
246125
  if (name === DEFAULT_INSTANCE) {
245662
246126
  try {
245663
- (0, import_node_child_process22.execSync)(`systemctl --user disable --now ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
246127
+ (0, import_node_child_process23.execSync)(`systemctl --user disable --now ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
245664
246128
  } catch {
245665
246129
  }
245666
246130
  }
245667
246131
  try {
245668
- (0, import_node_child_process22.execSync)(`systemctl --user daemon-reload && systemctl --user enable --now ${unit}`, { stdio: "ignore" });
246132
+ (0, import_node_child_process23.execSync)(`systemctl --user daemon-reload && systemctl --user enable --now ${unit}`, { stdio: "ignore" });
245669
246133
  return true;
245670
246134
  } catch {
245671
246135
  }
245672
246136
  return false;
245673
246137
  } else if (process.platform === "darwin") {
245674
246138
  const label = launchdLabel(name);
245675
- const plist = path39.join(os13.homedir(), "Library", "LaunchAgents", `${label}.plist`);
246139
+ const plist = path39.join(os14.homedir(), "Library", "LaunchAgents", `${label}.plist`);
245676
246140
  fs42.mkdirSync(path39.dirname(plist), { recursive: true });
245677
246141
  fs42.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
245678
246142
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -245685,7 +246149,7 @@ function setupAutostart(name) {
245685
246149
  <key>StandardErrorPath</key><string>${inst.logFile}</string>
245686
246150
  </dict></plist>`);
245687
246151
  try {
245688
- (0, import_node_child_process22.execSync)(`launchctl load "${plist}"`, { stdio: "ignore" });
246152
+ (0, import_node_child_process23.execSync)(`launchctl load "${plist}"`, { stdio: "ignore" });
245689
246153
  return true;
245690
246154
  } catch {
245691
246155
  }
@@ -245696,7 +246160,7 @@ function setupAutostart(name) {
245696
246160
  function spawnDaemon() {
245697
246161
  fs42.mkdirSync(path39.dirname(LOG_FILE), { recursive: true });
245698
246162
  const log3 = fs42.openSync(LOG_FILE, "a");
245699
- const child = (0, import_node_child_process22.spawn)(process.execPath, [INST.binFile, "--_daemon", "--instance", INSTANCE], {
246163
+ const child = (0, import_node_child_process23.spawn)(process.execPath, [INST.binFile, "--_daemon", "--instance", INSTANCE], {
245700
246164
  detached: true,
245701
246165
  stdio: ["ignore", log3, log3],
245702
246166
  // 与 systemd unit 里那条 Environment 等价——detached 兜底路径不能少,否则同一实例在两条
@@ -245721,24 +246185,24 @@ function stopDaemon(name) {
245721
246185
  }
245722
246186
  if (process.platform === "linux") {
245723
246187
  try {
245724
- (0, import_node_child_process22.execSync)(`systemctl --user stop ${systemdUnitName(name)} 2>/dev/null`, { stdio: "ignore" });
246188
+ (0, import_node_child_process23.execSync)(`systemctl --user stop ${systemdUnitName(name)} 2>/dev/null`, { stdio: "ignore" });
245725
246189
  } catch {
245726
246190
  }
245727
246191
  if (name === DEFAULT_INSTANCE) {
245728
246192
  try {
245729
- (0, import_node_child_process22.execSync)(`systemctl --user stop ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
246193
+ (0, import_node_child_process23.execSync)(`systemctl --user stop ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
245730
246194
  } catch {
245731
246195
  }
245732
246196
  }
245733
246197
  } else if (process.platform === "darwin") {
245734
246198
  const label = launchdLabel(name);
245735
246199
  try {
245736
- (0, import_node_child_process22.execSync)(`launchctl unload "$HOME/Library/LaunchAgents/${label}.plist" 2>/dev/null`, { stdio: "ignore" });
246200
+ (0, import_node_child_process23.execSync)(`launchctl unload "$HOME/Library/LaunchAgents/${label}.plist" 2>/dev/null`, { stdio: "ignore" });
245737
246201
  } catch {
245738
246202
  }
245739
246203
  if (name === DEFAULT_INSTANCE && label !== LEGACY_LAUNCHD_LABEL) {
245740
246204
  try {
245741
- (0, import_node_child_process22.execSync)(`launchctl unload "$HOME/Library/LaunchAgents/${LEGACY_LAUNCHD_LABEL}.plist" 2>/dev/null`, { stdio: "ignore" });
246205
+ (0, import_node_child_process23.execSync)(`launchctl unload "$HOME/Library/LaunchAgents/${LEGACY_LAUNCHD_LABEL}.plist" 2>/dev/null`, { stdio: "ignore" });
245742
246206
  } catch {
245743
246207
  }
245744
246208
  }
@@ -245757,7 +246221,7 @@ function selfUpdate() {
245757
246221
  const inner = `{ echo "[oasis] self-update \u2192 ${PKG_NAME}@latest (registry ${OFFICIAL}, \u5931\u8D25\u56DE\u843D\u9ED8\u8BA4\u6E90)"; sleep 2; { ${installOfficial} || ${installFallback}; } && ${printVersion} && ${startCmd}; } >> '${LOG_FILE}' 2>&1`;
245758
246222
  const hasSystemd = process.platform === "linux" && (() => {
245759
246223
  try {
245760
- (0, import_node_child_process22.execSync)("systemd-run --user --scope --quiet -- true", { stdio: "ignore" });
246224
+ (0, import_node_child_process23.execSync)("systemd-run --user --scope --quiet -- true", { stdio: "ignore" });
245761
246225
  return true;
245762
246226
  } catch {
245763
246227
  return false;
@@ -245765,13 +246229,13 @@ function selfUpdate() {
245765
246229
  })();
245766
246230
  const log3 = fs42.openSync(LOG_FILE, "a");
245767
246231
  if (hasSystemd) {
245768
- const child = (0, import_node_child_process22.spawn)("systemd-run", ["--user", "--scope", "--quiet", "sh", "-c", inner], {
246232
+ const child = (0, import_node_child_process23.spawn)("systemd-run", ["--user", "--scope", "--quiet", "sh", "-c", inner], {
245769
246233
  detached: true,
245770
246234
  stdio: ["ignore", log3, log3]
245771
246235
  });
245772
246236
  child.unref();
245773
246237
  } else {
245774
- const child = (0, import_node_child_process22.spawn)("sh", ["-c", inner], { detached: true, stdio: ["ignore", log3, log3] });
246238
+ const child = (0, import_node_child_process23.spawn)("sh", ["-c", inner], { detached: true, stdio: ["ignore", log3, log3] });
245775
246239
  child.unref();
245776
246240
  }
245777
246241
  }
@@ -245833,7 +246297,7 @@ async function startOneInstance(o) {
245833
246297
  const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
245834
246298
  method: "POST",
245835
246299
  headers: { "content-type": "application/json" },
245836
- body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
246300
+ body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os14.hostname() })
245837
246301
  });
245838
246302
  if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
245839
246303
  const body2 = await res.json();
@@ -245884,7 +246348,7 @@ function derivedNodeIdFor(name) {
245884
246348
  if (name === DEFAULT_INSTANCE) return resolveNodeId();
245885
246349
  const user = (() => {
245886
246350
  try {
245887
- return os13.userInfo().username;
246351
+ return os14.userInfo().username;
245888
246352
  } catch {
245889
246353
  return process.env["USER"] ?? process.env["USERNAME"] ?? "unknown";
245890
246354
  }
@@ -245894,7 +246358,7 @@ function derivedNodeIdFor(name) {
245894
246358
  function userScopeAvailable() {
245895
246359
  if (process.platform !== "linux") return false;
245896
246360
  try {
245897
- (0, import_node_child_process22.execSync)("systemd-run --user --scope --quiet -- true", { stdio: "ignore" });
246361
+ (0, import_node_child_process23.execSync)("systemd-run --user --scope --quiet -- true", { stdio: "ignore" });
245898
246362
  return true;
245899
246363
  } catch {
245900
246364
  return false;
@@ -245925,11 +246389,11 @@ function spawnSessionProcess(spec) {
245925
246389
  const logFd = fs42.openSync(path39.join(dir, "session.log"), "a");
245926
246390
  const argv = [INST.binFile, "--_session", specFile];
245927
246391
  const env = { ...process.env, OASIS_CLI_HOME: pinnedDir };
245928
- const child = userScopeAvailable() ? (0, import_node_child_process22.spawn)(
246392
+ const child = userScopeAvailable() ? (0, import_node_child_process23.spawn)(
245929
246393
  "systemd-run",
245930
246394
  ["--user", "--scope", "--quiet", `--setenv=OASIS_CLI_HOME=${pinnedDir}`, process.execPath, ...argv],
245931
246395
  { detached: true, stdio: ["ignore", logFd, logFd], env }
245932
- ) : (0, import_node_child_process22.spawn)(process.execPath, argv, { detached: true, stdio: ["ignore", logFd, logFd], env });
246396
+ ) : (0, import_node_child_process23.spawn)(process.execPath, argv, { detached: true, stdio: ["ignore", logFd, logFd], env });
245933
246397
  child.unref();
245934
246398
  return true;
245935
246399
  } catch (e) {