@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 +1 -1
- package/bin.mjs +109 -7
- package/bin.mjs.map +1 -1
- package/package.json +1 -1
package/RUNTIME.json
CHANGED
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");
|