@aidotnet/nextcowork-device-service 2.8.6 → 2.8.7

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.
package/RUNTIME.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "entry": "bin.mjs",
3
3
  "node": ">=24",
4
- "version": "2.8.6",
4
+ "version": "2.8.7",
5
5
  "note": "Dependencies are external; install them with the repository package.json before running.",
6
6
  "files": [
7
7
  "BUNDLE_IMPORTS.json",
package/bin.mjs CHANGED
@@ -6008,6 +6008,14 @@ class RunHandle {
6008
6008
  depth;
6009
6009
  /** Metadata of a detached Task run, kept with its authoritative run handle. */
6010
6010
  backgroundTask;
6011
+ /**
6012
+ * 用户按了停止,驱动却在宽限期内一直没收尾,被 `launch` 强行收口了(#187)。
6013
+ *
6014
+ * ★ 置位之后这个 run 在注册表里已经是 `aborted`、会话互斥也已放开 —— 下一轮可能
6015
+ * 已经开跑。驱动哪天自己醒过来,它手里那份转录是**过期的**:不能再逐条落盘,
6016
+ * 也不能在收尾时整份覆盖库里的历史(那会抹掉下一轮的消息)。
6017
+ */
6018
+ abandoned = false;
6011
6019
  status = "running";
6012
6020
  startedAt = Date.now();
6013
6021
  endedAt;
@@ -6290,6 +6298,63 @@ class RunRegistry {
6290
6298
  }
6291
6299
  return void 0;
6292
6300
  }
6301
+ /**
6302
+ * 这几条会话此刻「到底谁占着」—— 删除会话前的**活体检查**,按真实 run 状态分类,
6303
+ * 而不是只看 `isSessionBusy` 那一个布尔值。
6304
+ *
6305
+ * 需求:删除被「会话忙」挡住时,要分得清是真的有 Agent 在跑(该问用户要不要停),
6306
+ * 还是注册表里残留的条目(该清掉、照删)。只有一个布尔值的时候,一条中断了却没
6307
+ * 收尾的 run、或一把收尾时没放掉的互斥,会把删除永久挡住 —— 表现为「删不掉,
6308
+ * 重启后就能删」(重启清空的正是这张进程内的表)。
6309
+ *
6310
+ * - `live`:status 是 running,且没被中断 —— 真在跑;
6311
+ * - `stopping`:已经按过停止(signal 已 abort),status 却还停在 running ——
6312
+ * 驱动要么正在退,要么卡死了(`abort()` 不改 status,只有 `finish()` 改);
6313
+ * - `finalizing`:互斥被一个**已经结束**(或已不在表里)的 run 占着 —— 驱动收尾落盘中,
6314
+ * 正常是毫秒级,长时间不放就是漏了;
6315
+ * - `operations`:没有 run 的历史操作(手动压缩)占着会话。
6316
+ */
6317
+ sessionActivity(sessionIds) {
6318
+ const ids = new Set(sessionIds);
6319
+ const activity = { live: [], stopping: [], finalizing: [], operations: [] };
6320
+ for (const handle2 of this.runs.values()) {
6321
+ if (!ids.has(handle2.sessionId) || handle2.status !== "running") continue;
6322
+ if (handle2.signal.aborted) activity.stopping.push(handle2);
6323
+ else activity.live.push(handle2);
6324
+ }
6325
+ for (const [sessionId, reservation] of this.sessionOperations) {
6326
+ if (!ids.has(sessionId)) continue;
6327
+ if (reservation.runId === void 0) {
6328
+ activity.operations.push(sessionId);
6329
+ continue;
6330
+ }
6331
+ const owner = this.runs.get(reservation.runId);
6332
+ if (owner === void 0 || owner.status !== "running") activity.finalizing.push(sessionId);
6333
+ }
6334
+ return activity;
6335
+ }
6336
+ /**
6337
+ * 删除会话的最后一步:把这些会话名下还挂着的 run 强制判成 `aborted`、互斥强制放掉。
6338
+ *
6339
+ * ★ 只在「已经中断过、等过一段宽限期仍没收尾」之后调用(见 `ipc/sessions.ts` 的
6340
+ * `deleteSession`)。被强制收掉的驱动如果之后还醒过来:`emit` 会丢掉迟到事件,
6341
+ * 收尾写库撞上已删除的会话会抛并在它自己的 catch 里记日志,`release()` 的 token
6342
+ * 已经对不上、是空操作 —— 都不会把会话复活。
6343
+ */
6344
+ forceSettleSessions(sessionIds) {
6345
+ const ids = new Set(sessionIds);
6346
+ let n = 0;
6347
+ for (const handle2 of [...this.runs.values()]) {
6348
+ if (!ids.has(handle2.sessionId) || handle2.status !== "running") continue;
6349
+ handle2.abort({ by: "user" });
6350
+ handle2.finish("aborted");
6351
+ n++;
6352
+ }
6353
+ for (const sessionId of ids) {
6354
+ if (this.sessionOperations.delete(sessionId)) n++;
6355
+ }
6356
+ return n;
6357
+ }
6293
6358
  hasSessionOperations() {
6294
6359
  return this.sessionOperations.size > 0;
6295
6360
  }
@@ -7453,6 +7518,21 @@ const PROVIDER_PRESETS = [
7453
7518
  notes: "模型 ID 带厂商前缀(如 deepseek-ai/deepseek-v4-flash),请使用模型列表返回的完整 ID。模型列表免鉴权就能拉,没填 key 也能先看有哪些模型(因此列表能拉到不代表 key 有效)。同一个 key 也可走 OpenAI Responses 与 Anthropic 协议。",
7454
7519
  verification: "documented"
7455
7520
  },
7521
+ {
7522
+ id: "cheaperinference",
7523
+ name: "Cheaper Inference",
7524
+ category: "aggregator",
7525
+ endpoints: [
7526
+ oa("https://api.cheaperinference.com/v1", true),
7527
+ resp("https://api.cheaperinference.com/v1", true),
7528
+ anth$1("https://api.cheaperinference.com")
7529
+ ],
7530
+ docsUrl: "https://cheaperinference.com/docs",
7531
+ apiKeyUrl: "https://cheaperinference.com/signup",
7532
+ suggestedModels: ["gpt-5.4-mini", "claude-sonnet-5", "deepseek-v4-flash"],
7533
+ notes: "模型 ID 用不带厂商前缀的短名(如 gpt-5.4-mini、claude-sonnet-5),用模型列表返回的完整 ID 即可。模型列表要先填 key 才能拉。同一个 key 也可走 OpenAI Responses 与 Anthropic 协议。",
7534
+ verification: "probed"
7535
+ },
7456
7536
  {
7457
7537
  id: "siliconflow",
7458
7538
  name: "硅基流动(国内)",
@@ -35067,6 +35147,7 @@ ${injected}
35067
35147
  acceptsGoalInput: (goalId) => primary && getActiveGoal(req.sessionId)?.id === goalId,
35068
35148
  prepareMessage: (message2) => primary ? prepareGoalMessage(req.sessionId, message2) : message2,
35069
35149
  onMessageCommit: (message2) => {
35150
+ if (handle2.abandoned) return;
35070
35151
  store.commitMessage(req.sessionId, message2, req.runId);
35071
35152
  if (req.parentSessionId === void 0) sessionOnChange?.({ kind: "messages", sessionIds: [req.sessionId], workspaceId: req.workspaceId });
35072
35153
  },
@@ -35215,6 +35296,7 @@ ${injected}
35215
35296
  }
35216
35297
  let writtenSkills = [];
35217
35298
  try {
35299
+ if (handle2.abandoned) return;
35218
35300
  const latest = store.getHistory(req.sessionId);
35219
35301
  store.setHistory(req.sessionId, mergeGoalStatusHistory(mergeLatestSubagentReceipts(agentSession.history, latest), latest));
35220
35302
  store.setRunRecord(req.runId, req.sessionId, handle2.status, startedAt, getHost().clock.now());
@@ -35681,7 +35763,7 @@ function statIfPresent(path) {
35681
35763
  throw error;
35682
35764
  }
35683
35765
  }
35684
- function checkedPath(root, path, allowRoot = false) {
35766
+ function checkedPath(root, path, allowRoot = false, links = "reject") {
35685
35767
  if (typeof path !== "string" || path.includes("\0")) fail$2("invalid-path");
35686
35768
  if (path === "" && allowRoot) return root;
35687
35769
  if (isAbsolute(path)) {
@@ -35699,7 +35781,7 @@ function checkedPath(root, path, allowRoot = false) {
35699
35781
  for (let index = 1; index <= segments.length; index++) {
35700
35782
  const component = resolve(root, ...segments.slice(0, index));
35701
35783
  const stat2 = statIfPresent(component);
35702
- if (stat2?.isSymbolicLink()) fail$2("symlink");
35784
+ if (stat2?.isSymbolicLink() && (links === "reject" || links === "follow-dirs" && index === segments.length)) fail$2("symlink");
35703
35785
  if (!stat2) break;
35704
35786
  }
35705
35787
  return target;
@@ -35728,7 +35810,7 @@ function readBounded(path, limit) {
35728
35810
  }
35729
35811
  }
35730
35812
  function readFileAt(root, path) {
35731
- const target = checkedPath(root, path);
35813
+ const target = checkedPath(root, path, false, "follow-dirs");
35732
35814
  const extension = extname(path).toLowerCase();
35733
35815
  const { bytes, stat: stat2 } = readBounded(target, workspaceReadLimit(extension));
35734
35816
  return classifyWorkspaceFile(path, extension, bytes, stat2);
@@ -35781,7 +35863,7 @@ function readWorkspaceFile(req) {
35781
35863
  function writeWorkspaceFile(req) {
35782
35864
  try {
35783
35865
  const root = workspaceRoot(req.workspaceId);
35784
- const target = checkedPath(root, req.path);
35866
+ const target = checkedPath(root, req.path, false, "follow-dirs");
35785
35867
  if (typeof req.content !== "string" || typeof req.revision !== "string") fail$2("invalid-encoding");
35786
35868
  if (req.encoding === "base64") return writeBase64File(root, req, target);
35787
35869
  if (req.content.length > WORKSPACE_TEXT_LIMIT) fail$2("too-large");
@@ -35803,7 +35885,7 @@ function writeWorkspaceFile(req) {
35803
35885
  } finally {
35804
35886
  closeSync(fd);
35805
35887
  }
35806
- checkedPath(root, req.path);
35888
+ checkedPath(root, req.path, false, "follow-dirs");
35807
35889
  if (readFileAt(root, req.path).revision !== req.revision) fail$2("conflict");
35808
35890
  renameSync(temporary, target);
35809
35891
  created = false;
@@ -35837,7 +35919,7 @@ function writeBase64File(root, req, target) {
35837
35919
  } finally {
35838
35920
  closeSync(fd);
35839
35921
  }
35840
- checkedPath(root, req.path);
35922
+ checkedPath(root, req.path, false, "follow-dirs");
35841
35923
  if (readFileAt(root, req.path).revision !== req.revision) fail$2("conflict");
35842
35924
  renameSync(temporary, target);
35843
35925
  created = false;
@@ -36925,7 +37007,13 @@ function launch(req, driver) {
36925
37007
  const release2 = runs.retainSessionForRun(handle2);
36926
37008
  pumps.set(req.runId, new RunPump(handle2));
36927
37009
  sessionRuntime.runStarted(handle2, req);
37010
+ let settled = false;
37011
+ let abandonTimer;
36928
37012
  const settle = () => {
37013
+ if (abandonTimer !== void 0) clearTimeout(abandonTimer);
37014
+ handle2.signal.removeEventListener("abort", watchAbort);
37015
+ if (settled) return;
37016
+ settled = true;
36929
37017
  release2();
36930
37018
  try {
36931
37019
  sessionRuntime.runSettled(handle2);
@@ -36938,6 +37026,19 @@ function launch(req, driver) {
36938
37026
  if (handle2.signal.aborted) handle2.finish("aborted");
36939
37027
  else handle2.finish("error", toAgentError(error));
36940
37028
  };
37029
+ function watchAbort() {
37030
+ abandonTimer = setTimeout(() => {
37031
+ abandonTimer = void 0;
37032
+ if (settled || handle2.status !== "running") return;
37033
+ console.warn(`[run] ${handle2.runId} 停止后 ${String(ABORT_SETTLE_GRACE_MS / 1e3)} 秒仍未收尾,强制结束`);
37034
+ handle2.abandoned = true;
37035
+ handle2.finish("aborted");
37036
+ settle();
37037
+ }, ABORT_SETTLE_GRACE_MS);
37038
+ abandonTimer.unref?.();
37039
+ }
37040
+ if (handle2.signal.aborted) watchAbort();
37041
+ else handle2.signal.addEventListener("abort", watchAbort, { once: true });
36941
37042
  ensureReapTimer();
36942
37043
  try {
36943
37044
  void Promise.resolve(driver(handle2, req)).catch(failed).finally(settle);
@@ -36950,6 +37051,7 @@ function launch(req, driver) {
36950
37051
  }
36951
37052
  return handle2;
36952
37053
  }
37054
+ const ABORT_SETTLE_GRACE_MS = 5e3;
36953
37055
  const REAP_INTERVAL_MS = 3e4;
36954
37056
  let reapTimer = null;
36955
37057
  function reapFinishedRuns(now2 = Date.now()) {
@@ -38796,7 +38898,7 @@ async function listWorkspaceDir(req) {
38796
38898
  function readWorkspaceRange(req) {
38797
38899
  let fd = null;
38798
38900
  try {
38799
- const target = checkedPath(workspaceRoot(req.workspaceId), req.path);
38901
+ const target = checkedPath(workspaceRoot(req.workspaceId), req.path, false, "follow-dirs");
38800
38902
  fd = openSync(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
38801
38903
  const stat2 = fstatSync(fd);
38802
38904
  if (!stat2.isFile()) fail$2("not-file");