@feng0531/dsh-execution-bus 0.2.12 → 0.4.0
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/README.md +18 -6
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/cordis.patch.yml +4 -23
- package/dist/bus.d.ts +27 -0
- package/dist/bus.js +92 -18
- package/dist/diagnostics.d.ts +38 -0
- package/dist/diagnostics.js +26 -0
- package/dist/execution.d.ts +61 -0
- package/dist/execution.js +174 -0
- package/dist/plugin.js +69 -141
- package/dist/preset.d.ts +12 -0
- package/dist/preset.js +73 -0
- package/dist/test-mode.d.ts +1 -1
- package/dist/test-mode.js +8 -17
- package/docs/FLOW-TEST.md +2 -1
- package/docs/OPTIMIZATION.md +47 -0
- package/docs/host-compatibility.md +5 -3
- package/docs/protocol.md +10 -4
- package/docs/releases/0.3.0.md +21 -0
- package/docs/releases/0.4.0.md +17 -0
- package/docs/validation.md +5 -5
- package/package.json +19 -4
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
“萧何”是产品与模式显示名,执行总线是其技术功能;不新增人物 Agent,不进行角色扮演。仓库 dsh-execution-bus、npm 包名、execution_bus 工具名及 execution-bus-test 预设 ID 保持不变。历史版本发布说明保留当时名称。
|
|
6
6
|
|
|
7
|
-
DSH 独立插件:`@feng0531/dsh-execution-bus` · 当前版本 **0.
|
|
7
|
+
DSH 独立插件:`@feng0531/dsh-execution-bus` · 当前版本 **0.4.0**
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> 萧何模式从宿主已声明的 PTC 组合派生,保留日常工具,再叠加可选执行总线。已验证隔离的真实 Node PTC → 原 read + 总线调用,以及脚本模型内存会话;真实模型、桌面界面与实际沙箱仍未验收。**不支持重启后恢复任务。**
|
|
10
10
|
|
|
11
11
|
## 它解决什么问题?
|
|
12
12
|
|
|
@@ -92,7 +92,13 @@ flowchart TD
|
|
|
92
92
|
|
|
93
93
|
包内提供标准 DSH bundle patch(预设组合补丁)。宿主正常加载后会收到“萧何 / Xiao He”预设定义,由既有设置 → Agent 预设 → **自定义**列表展示。建议选择它新建专门会话,不改默认选择或正在使用的普通会话。已有会话的切换受宿主空白会话检查约束。**实际桌面卡片展示尚未验收**,不能把注册表测试当作界面已通过。
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
**0.3.0 起不再是只读验证模式。** 加载时读取宿主 `ptc` 预设的组合声明快照,保留原工具、平台表达式、禁用项和配置,只追加萧何总线与说明。不再强制 native 呈现或添加只读白名单;终端、写入、搜索、技能等能力是否可用,取决于原 PTC 配置和宿主服务。其他预设和默认模式不被修改,原 PTC 会话看不到新增总线。
|
|
96
|
+
|
|
97
|
+
普通任务仍通过 `run_code` 调用原工具;**总线目前只支持 test 和 host.read,不接管任意命令、安装或全部工具**。例如更新插件应使用原终端能力并遵守宿主授权;专用插件管理工具若在源 PTC 中禁用,萧何不会擅自启用。命令受理、返回 Job ID 不等于执行完成。
|
|
98
|
+
|
|
99
|
+
升级提示:预设 ID 保持兼容,但从旧版只读模式升级后能力范围扩大,使用者须审阅后加载;本次不自动升级日常配置。它不是进程沙箱,同一预设会话共享总线容量,结果按会话隔离。派生是加载时快照,不自动跟随源预设热编辑;源 PTC 更新后应按宿主正常流程重新加载派生行,旧会话遵循宿主版本保留规则。
|
|
100
|
+
|
|
101
|
+
源组合中的相对模块名、include 子文件缺少公开基准地址信息,当前明确拒绝派生,不能称为兼容所有自定义 PTC。遇到该诊断应先将源组合整理成可解析的显式模块行,不能绕过或回退成只读工具集。缺失 ptc 声明时注册器最多等候约 5 秒后明确失败。
|
|
96
102
|
|
|
97
103
|
**不要同时把包根入口作为全局工具插件加载**:那是手工集成入口,会使 execution_bus 全局可见,失去此预设的可见性隔离。仅安装 npm 包也不保证卡片立即出现,需要加载包声明的 bundle patch。项目发布不会自动安装到任何读者的宿主,卡片是否可用需要在目标实例实际检查。
|
|
98
104
|
|
|
@@ -109,7 +115,7 @@ flowchart TD
|
|
|
109
115
|
2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
|
|
110
116
|
|
|
111
117
|
```powershell
|
|
112
|
-
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.
|
|
118
|
+
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.4.0
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
|
|
@@ -178,7 +184,7 @@ flowchart TD
|
|
|
178
184
|
|
|
179
185
|
### 如果宿主使用 PTC 模式
|
|
180
186
|
|
|
181
|
-
PTC 是 DSH 的程序化工具调用入口。在 `run_code` 内使用 `await tools.execution_bus(请求对象)`,而不是直接调用折叠后的工具名。执行总线不会解除 `run_code`
|
|
187
|
+
PTC 是 DSH 的程序化工具调用入口。在 `run_code` 内使用 `await tools.execution_bus(请求对象)`,而不是直接调用折叠后的工具名。执行总线不会解除 `run_code` 自身的时间预算。已有真实 Node PTC 隔离进程读文件和调用总线的验证,但尚未进行实际模型/桌面沙箱全链路验收。
|
|
182
188
|
|
|
183
189
|
## 如何理解任务状态?
|
|
184
190
|
|
|
@@ -204,6 +210,12 @@ stateDiagram-v2
|
|
|
204
210
|
|
|
205
211
|
执行终态不会再改成另一个终态。取消与成功竞争时,若处理器最终正常返回,仍可能是成功。`delivery` 是另一个字段:`pending` 表示尚未走到返回标记,`returned` 只表示插件已走到工具返回路径,**不证明会话结果已经写入磁盘或模型已经消费**。
|
|
206
212
|
|
|
213
|
+
## 调度与诊断
|
|
214
|
+
|
|
215
|
+
0.4.0 起对已受理的排队请求按会话轮转,同一会话保持 FIFO;不抢占运行任务,也不保证队列满时为其他会话预留名额。执行结果仍由 core 唯一提交,宿主 Job 只负责任务展示、控制与生命周期关联。
|
|
216
|
+
|
|
217
|
+
core 开发者可调用实例 `diagnostics()` 取得队列、运行数、保留结果字节及最近 128 条数值阶段轨迹。不含请求/会话 ID、输入、输出或错误文本,不写日志;诊断不增加模型调用工具,也不是跨重启审计记录。详见[优化范围与验证](<docs/OPTIMIZATION.md>)。
|
|
218
|
+
|
|
207
219
|
## 容量与重要限制
|
|
208
220
|
|
|
209
221
|
默认并发 **2**,排队上限 **32**,最多保存 **256** 条任务记录,单结果最多 **64 KiB**,完成结果保留期 **1 小时**,在新请求和查询时惰性清理;不是一小时一到即定时删除。队列或记录满时拒绝新任务;宿主 Job 自身也有容量限制。
|
|
@@ -238,7 +250,7 @@ npm run test:install # 临时目录安装,不操作日常 DSH
|
|
|
238
250
|
|
|
239
251
|
- [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
|
|
240
252
|
- [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
|
|
241
|
-
- [验证记录](<docs/validation.md>)、[0.
|
|
253
|
+
- [验证记录](<docs/validation.md>)、[0.4.0 发布说明](<docs/releases/0.4.0.md>):已验证与未验证范围。
|
|
242
254
|
- [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
|
|
243
255
|
|
|
244
256
|
## 许可
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# 第三方说明
|
|
2
2
|
|
|
3
|
-
本包没有捆绑 DSH、Cordis、TypeScript、测试依赖或宿主源码快照。DSH 与 Cordis 作为宿主 peer 使用,其独立包分别受原许可证约束,本项目非商业许可证不限制第三方已授予的权利。开发依赖不进入本项目 tgz。
|
|
3
|
+
本包没有捆绑 DSH、Cordis、TypeScript、测试依赖或宿主源码快照。DSH 与 Cordis 作为宿主 peer 使用,其独立包分别受原许可证约束,本项目非商业许可证不限制第三方已授予的权利。开发依赖不进入本项目 tgz。js-yaml 为 MIT 许可的独立运行依赖,由包管理器按声明安装,不捆绑进 tgz;其源码和许可见 https://github.com/nodeca/js-yaml ,本项目许可不限制其原有权利。
|
|
4
4
|
|
|
5
5
|
CI 与发布组织方式参考 dsh-vcs,并按本项目执行能力重新实现;本包不含 Monaco、React 或 SVN 实现。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Add a separate selectable preset, deriving the host's configured PTC declaration.
|
|
2
|
+
# Does not patch preset-ptc, selectedDefault or global tools.
|
|
2
3
|
- insert:
|
|
3
4
|
- id: preset-execution-bus-test
|
|
4
|
-
name: '@
|
|
5
|
-
config:
|
|
6
|
-
id: execution-bus-test
|
|
7
|
-
name: 萧何 / Xiao He
|
|
8
|
-
description: 独立执行模式:请求排队、异步等待、进度和取消;当前支持受控验证与只读文件。不修改默认模式,禁止命令和写入执行;不支持重启恢复。
|
|
9
|
-
order: 20
|
|
10
|
-
plugins:
|
|
11
|
-
- id: execution-bus-persona
|
|
12
|
-
name: '@deepseek-ai/dsh-persona'
|
|
13
|
-
config:
|
|
14
|
-
prefix: >-
|
|
15
|
-
你正在萧何模式中;这是执行请求协调模式,不是人物角色扮演。只使用 execution_bus、只读文件和任务控制工具。
|
|
16
|
-
请求只提交一次并等待结果,不循环查询进度;同一逻辑操作重发沿用 requestId。
|
|
17
|
-
默认先用 test Handler 做延迟成功、失败和取消测试,读取真实文件须有用户要求。
|
|
18
|
-
cancelRequested 不代表已取消,unknown 不代表执行失败。禁止声称跨重启恢复或完整模型性能提升。
|
|
19
|
-
suffix: 'Your working directory is {{cwd}}.'
|
|
20
|
-
- id: execution-bus-file-tools
|
|
21
|
-
name: '@deepseek-ai/dsh-tool-fs'
|
|
22
|
-
- id: execution-bus-job-tools
|
|
23
|
-
name: '@deepseek-ai/dsh-tool-jobs'
|
|
24
|
-
- id: execution-bus-scoped-runtime
|
|
25
|
-
name: '@feng0531/dsh-execution-bus/test-mode'
|
|
5
|
+
name: '@feng0531/dsh-execution-bus/preset'
|
|
6
|
+
config: {}
|
package/dist/bus.d.ts
CHANGED
|
@@ -53,8 +53,35 @@ export declare class ExecutionBus {
|
|
|
53
53
|
private entries;
|
|
54
54
|
private handlers;
|
|
55
55
|
private queue;
|
|
56
|
+
private readyOwners;
|
|
56
57
|
private running;
|
|
57
58
|
private closed;
|
|
59
|
+
private telemetry;
|
|
60
|
+
diagnostics(): {
|
|
61
|
+
phase: "open" | "cancelling" | "draining" | "closed";
|
|
62
|
+
queued: number;
|
|
63
|
+
running: number;
|
|
64
|
+
records: number;
|
|
65
|
+
retainedResultBytes: number;
|
|
66
|
+
counts: {
|
|
67
|
+
accepted?: number | undefined;
|
|
68
|
+
started?: number | undefined;
|
|
69
|
+
cancel_requested?: number | undefined;
|
|
70
|
+
terminal?: number | undefined;
|
|
71
|
+
returned?: number | undefined;
|
|
72
|
+
job_created?: number | undefined;
|
|
73
|
+
job_released?: number | undefined;
|
|
74
|
+
job_error?: number | undefined;
|
|
75
|
+
reused?: number | undefined;
|
|
76
|
+
};
|
|
77
|
+
traces: {
|
|
78
|
+
operation: number;
|
|
79
|
+
stage: import("./diagnostics.js").TraceStage;
|
|
80
|
+
elapsedMs: number;
|
|
81
|
+
}[];
|
|
82
|
+
};
|
|
83
|
+
private closing?;
|
|
84
|
+
private shutdownPhase;
|
|
58
85
|
constructor(options?: {
|
|
59
86
|
concurrency: number;
|
|
60
87
|
queueLimit: number;
|
package/dist/bus.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "node:crypto";
|
|
2
2
|
import { assertJson } from "./json.js";
|
|
3
|
+
import { Diagnostics } from "./diagnostics.js";
|
|
3
4
|
export function object(v) {
|
|
4
5
|
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
5
6
|
throw Error("INVALID_OBJECT");
|
|
@@ -58,8 +59,25 @@ export class ExecutionBus {
|
|
|
58
59
|
entries = new Map();
|
|
59
60
|
handlers = new Map();
|
|
60
61
|
queue = [];
|
|
62
|
+
readyOwners = [];
|
|
61
63
|
running = 0;
|
|
62
64
|
closed = false;
|
|
65
|
+
telemetry = new Diagnostics();
|
|
66
|
+
diagnostics() {
|
|
67
|
+
return {
|
|
68
|
+
...this.telemetry.snapshot(),
|
|
69
|
+
phase: this.shutdownPhase,
|
|
70
|
+
queued: this.queue.length,
|
|
71
|
+
running: this.running,
|
|
72
|
+
records: this.entries.size,
|
|
73
|
+
retainedResultBytes: [...this.entries.values()].reduce((sum, e) => sum +
|
|
74
|
+
(e.view.result === undefined
|
|
75
|
+
? 0
|
|
76
|
+
: Buffer.byteLength(JSON.stringify(e.view.result))), 0),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
closing;
|
|
80
|
+
shutdownPhase = "open";
|
|
63
81
|
constructor(options = {
|
|
64
82
|
concurrency: 2,
|
|
65
83
|
queueLimit: 32,
|
|
@@ -119,16 +137,30 @@ export class ExecutionBus {
|
|
|
119
137
|
type,
|
|
120
138
|
payload,
|
|
121
139
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
// Snapshot and enqueue before invoking user code: nested cancellation cannot
|
|
141
|
+
// overtake the current event for later observers. Removed listeners are skipped.
|
|
142
|
+
e.notifications.push({ event, listeners: [...e.listeners] });
|
|
143
|
+
if (e.dispatching)
|
|
144
|
+
return;
|
|
145
|
+
e.dispatching = true;
|
|
146
|
+
try {
|
|
147
|
+
while (e.notifications.length) {
|
|
148
|
+
const batch = e.notifications.shift();
|
|
149
|
+
for (const listener of batch.listeners) {
|
|
150
|
+
if (!e.listeners.has(listener))
|
|
151
|
+
continue;
|
|
152
|
+
try {
|
|
153
|
+
void Promise.resolve(listener(batch.event)).catch(() => { });
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
}
|
|
131
157
|
}
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
e.dispatching = false;
|
|
161
|
+
if (terminal(e.view.state))
|
|
162
|
+
e.listeners.clear();
|
|
163
|
+
}
|
|
132
164
|
}
|
|
133
165
|
view(e) {
|
|
134
166
|
return structuredClone(e.view);
|
|
@@ -170,6 +202,7 @@ export class ExecutionBus {
|
|
|
170
202
|
const attached = this.attachment(e, onEvent, m.messageId);
|
|
171
203
|
if (!previous)
|
|
172
204
|
e.messages.set(m.messageId, canonical(m));
|
|
205
|
+
this.telemetry.record(e.trace, "reused");
|
|
173
206
|
return attached;
|
|
174
207
|
}
|
|
175
208
|
const handler = this.handlers.get(p.handler)?.handler;
|
|
@@ -182,6 +215,7 @@ export class ExecutionBus {
|
|
|
182
215
|
let resolve;
|
|
183
216
|
const done = new Promise((r) => (resolve = r));
|
|
184
217
|
e = {
|
|
218
|
+
trace: this.telemetry.begin(),
|
|
185
219
|
invoke,
|
|
186
220
|
correlationId: m.messageId,
|
|
187
221
|
owner: owner.sessionId,
|
|
@@ -199,13 +233,18 @@ export class ExecutionBus {
|
|
|
199
233
|
done,
|
|
200
234
|
resolve,
|
|
201
235
|
listeners: new Set(),
|
|
236
|
+
notifications: [],
|
|
237
|
+
dispatching: false,
|
|
202
238
|
sourceSequence: 0,
|
|
203
239
|
created: Date.now(),
|
|
204
240
|
messages: new Map([[m.messageId, canonical(m)]]),
|
|
205
241
|
};
|
|
206
242
|
this.entries.set(key, e);
|
|
243
|
+
this.telemetry.record(e.trace, "accepted");
|
|
207
244
|
const attached = this.attachment(e, onEvent, m.messageId);
|
|
208
245
|
this.queue.push(e);
|
|
246
|
+
if (!this.readyOwners.includes(e.owner))
|
|
247
|
+
this.readyOwners.push(e.owner);
|
|
209
248
|
queueMicrotask(() => this.pump());
|
|
210
249
|
return attached;
|
|
211
250
|
}
|
|
@@ -235,6 +274,8 @@ export class ExecutionBus {
|
|
|
235
274
|
e.listeners.delete(listener);
|
|
236
275
|
},
|
|
237
276
|
returned: () => {
|
|
277
|
+
if (e.view.delivery !== "returned")
|
|
278
|
+
this.telemetry.record(e.trace, "returned");
|
|
238
279
|
e.view.delivery = "returned";
|
|
239
280
|
},
|
|
240
281
|
};
|
|
@@ -268,10 +309,13 @@ export class ExecutionBus {
|
|
|
268
309
|
if (terminal(e.view.state) || e.view.cancelRequested)
|
|
269
310
|
return;
|
|
270
311
|
e.view.cancelRequested = true;
|
|
312
|
+
this.telemetry.record(e.trace, "cancel_requested");
|
|
271
313
|
e.controller.abort();
|
|
272
314
|
this.emit(e, "cancel_requested", {});
|
|
273
315
|
if (e.view.state === "queued") {
|
|
274
316
|
this.queue = this.queue.filter((x) => x !== e);
|
|
317
|
+
if (!this.queue.some((entry) => entry.owner === e.owner))
|
|
318
|
+
this.readyOwners = this.readyOwners.filter((owner) => owner !== e.owner);
|
|
275
319
|
this.finish(e, "cancelled");
|
|
276
320
|
}
|
|
277
321
|
}
|
|
@@ -302,19 +346,33 @@ export class ExecutionBus {
|
|
|
302
346
|
}
|
|
303
347
|
e.created = Date.now();
|
|
304
348
|
e.view = { ...next, sequence: next.sequence - 1 };
|
|
305
|
-
this.
|
|
349
|
+
this.telemetry.record(e.trace, "terminal");
|
|
306
350
|
e.resolve(snapshot);
|
|
307
|
-
|
|
351
|
+
this.emit(e, state, { state });
|
|
352
|
+
// A reentrant terminal notification may still be queued behind this batch.
|
|
353
|
+
if (!e.dispatching)
|
|
354
|
+
e.listeners.clear();
|
|
308
355
|
}
|
|
309
356
|
pump() {
|
|
310
357
|
while (!this.closed &&
|
|
311
358
|
this.running < this.options.concurrency &&
|
|
312
359
|
this.queue.length) {
|
|
313
|
-
|
|
360
|
+
let index = -1;
|
|
361
|
+
while (this.readyOwners.length && index < 0) {
|
|
362
|
+
const owner = this.readyOwners.shift();
|
|
363
|
+
index = this.queue.findIndex((entry) => entry.owner === owner);
|
|
364
|
+
if (index >= 0 &&
|
|
365
|
+
this.queue.some((entry, i) => i !== index && entry.owner === owner))
|
|
366
|
+
this.readyOwners.push(owner);
|
|
367
|
+
}
|
|
368
|
+
if (index < 0)
|
|
369
|
+
index = 0;
|
|
370
|
+
const e = this.queue.splice(index, 1)[0];
|
|
314
371
|
if (e.view.state !== "queued")
|
|
315
372
|
continue;
|
|
316
373
|
this.running++;
|
|
317
374
|
e.view.state = "running";
|
|
375
|
+
this.telemetry.record(e.trace, "started");
|
|
318
376
|
this.emit(e, "started", {});
|
|
319
377
|
void this.run(e).finally(() => {
|
|
320
378
|
this.running--;
|
|
@@ -372,12 +430,28 @@ export class ExecutionBus {
|
|
|
372
430
|
: "HANDLER_FAILED");
|
|
373
431
|
}
|
|
374
432
|
}
|
|
375
|
-
|
|
433
|
+
close() {
|
|
434
|
+
if (this.closing)
|
|
435
|
+
return this.closing;
|
|
376
436
|
this.closed = true;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
437
|
+
this.shutdownPhase = "cancelling";
|
|
438
|
+
let resolve, reject;
|
|
439
|
+
this.closing = new Promise((r, j) => {
|
|
440
|
+
resolve = r;
|
|
441
|
+
reject = j;
|
|
442
|
+
});
|
|
443
|
+
void (async () => {
|
|
444
|
+
const entries = [...this.entries.values()];
|
|
445
|
+
for (const e of entries)
|
|
446
|
+
this.cancel(e);
|
|
447
|
+
this.shutdownPhase = "draining";
|
|
448
|
+
await Promise.all(entries.map((e) => e.done));
|
|
449
|
+
this.handlers.clear();
|
|
450
|
+
this.entries.clear();
|
|
451
|
+
this.queue = [];
|
|
452
|
+
this.readyOwners = [];
|
|
453
|
+
this.shutdownPhase = "closed";
|
|
454
|
+
})().then(resolve, reject);
|
|
455
|
+
return this.closing;
|
|
382
456
|
}
|
|
383
457
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type TraceStage = "accepted" | "started" | "cancel_requested" | "terminal" | "returned" | "job_created" | "job_released" | "job_error" | "reused";
|
|
2
|
+
export interface Trace {
|
|
3
|
+
operation: number;
|
|
4
|
+
stage: TraceStage;
|
|
5
|
+
elapsedMs: number;
|
|
6
|
+
}
|
|
7
|
+
/** In-memory numeric metadata only: never accepts request/session strings, input, output or error text. */
|
|
8
|
+
export declare class Diagnostics {
|
|
9
|
+
private next;
|
|
10
|
+
private events;
|
|
11
|
+
private counts;
|
|
12
|
+
begin(): {
|
|
13
|
+
operation: number;
|
|
14
|
+
at: number;
|
|
15
|
+
};
|
|
16
|
+
record(context: {
|
|
17
|
+
operation: number;
|
|
18
|
+
at: number;
|
|
19
|
+
}, stage: TraceStage): void;
|
|
20
|
+
snapshot(): {
|
|
21
|
+
counts: {
|
|
22
|
+
accepted?: number | undefined;
|
|
23
|
+
started?: number | undefined;
|
|
24
|
+
cancel_requested?: number | undefined;
|
|
25
|
+
terminal?: number | undefined;
|
|
26
|
+
returned?: number | undefined;
|
|
27
|
+
job_created?: number | undefined;
|
|
28
|
+
job_released?: number | undefined;
|
|
29
|
+
job_error?: number | undefined;
|
|
30
|
+
reused?: number | undefined;
|
|
31
|
+
};
|
|
32
|
+
traces: {
|
|
33
|
+
operation: number;
|
|
34
|
+
stage: TraceStage;
|
|
35
|
+
elapsedMs: number;
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
/** In-memory numeric metadata only: never accepts request/session strings, input, output or error text. */
|
|
3
|
+
export class Diagnostics {
|
|
4
|
+
next = 0;
|
|
5
|
+
events = [];
|
|
6
|
+
counts = {};
|
|
7
|
+
begin() {
|
|
8
|
+
return { operation: ++this.next, at: performance.now() };
|
|
9
|
+
}
|
|
10
|
+
record(context, stage) {
|
|
11
|
+
this.counts[stage] = (this.counts[stage] ?? 0) + 1;
|
|
12
|
+
this.events.push({
|
|
13
|
+
operation: context.operation,
|
|
14
|
+
stage,
|
|
15
|
+
elapsedMs: Math.max(0, performance.now() - context.at),
|
|
16
|
+
});
|
|
17
|
+
if (this.events.length > 128)
|
|
18
|
+
this.events.shift();
|
|
19
|
+
}
|
|
20
|
+
snapshot() {
|
|
21
|
+
return {
|
|
22
|
+
counts: { ...this.counts },
|
|
23
|
+
traces: this.events.map((e) => ({ ...e })),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { JobRegistry, JobId } from "@deepseek-ai/dsh-jobs";
|
|
2
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import type { RecordView, Event } from "./bus.js";
|
|
4
|
+
import { Diagnostics } from "./diagnostics.js";
|
|
5
|
+
export interface ExecutionAttachment {
|
|
6
|
+
done: Promise<RecordView>;
|
|
7
|
+
cancel(): void;
|
|
8
|
+
}
|
|
9
|
+
/** One host projection of a logical execution. The core completion remains authoritative. */
|
|
10
|
+
export declare class ExecutionHandle {
|
|
11
|
+
private jobs;
|
|
12
|
+
private owner;
|
|
13
|
+
private execution;
|
|
14
|
+
private telemetry;
|
|
15
|
+
readonly completion: Promise<RecordView>;
|
|
16
|
+
readonly settled: Promise<void>;
|
|
17
|
+
readonly job: JobId;
|
|
18
|
+
private progressHandle?;
|
|
19
|
+
private trace;
|
|
20
|
+
constructor(jobs: JobRegistry, owner: SessionId, execution: ExecutionAttachment, telemetry?: Diagnostics);
|
|
21
|
+
requestCancel(): void;
|
|
22
|
+
progress(event: Event): void;
|
|
23
|
+
private absent;
|
|
24
|
+
private waitAndClean;
|
|
25
|
+
}
|
|
26
|
+
export type ShutdownPhase = "open" | "stopping" | "draining" | "releasing" | "closed";
|
|
27
|
+
/** Owns host projections and active deliveries, not a second execution state machine. */
|
|
28
|
+
export declare class ExecutionOwner {
|
|
29
|
+
private handles;
|
|
30
|
+
private deliveries;
|
|
31
|
+
private closing?;
|
|
32
|
+
private telemetry;
|
|
33
|
+
create(key: string, jobs: JobRegistry, owner: SessionId, execution: ExecutionAttachment): ExecutionHandle;
|
|
34
|
+
diagnostics(): {
|
|
35
|
+
phase: ShutdownPhase;
|
|
36
|
+
activeProjections: number;
|
|
37
|
+
deliveries: number;
|
|
38
|
+
counts: {
|
|
39
|
+
accepted?: number | undefined;
|
|
40
|
+
started?: number | undefined;
|
|
41
|
+
cancel_requested?: number | undefined;
|
|
42
|
+
terminal?: number | undefined;
|
|
43
|
+
returned?: number | undefined;
|
|
44
|
+
job_created?: number | undefined;
|
|
45
|
+
job_released?: number | undefined;
|
|
46
|
+
job_error?: number | undefined;
|
|
47
|
+
reused?: number | undefined;
|
|
48
|
+
};
|
|
49
|
+
traces: {
|
|
50
|
+
operation: number;
|
|
51
|
+
stage: import("./diagnostics.js").TraceStage;
|
|
52
|
+
elapsedMs: number;
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
55
|
+
private currentPhase;
|
|
56
|
+
get phase(): ShutdownPhase;
|
|
57
|
+
get(key: string): ExecutionHandle | undefined;
|
|
58
|
+
add(key: string, handle: ExecutionHandle): void;
|
|
59
|
+
track<T>(delivery: Promise<T>): Promise<T>;
|
|
60
|
+
close(drainCore: () => Promise<void>): Promise<void>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { setTimeout as pause } from "node:timers/promises";
|
|
2
|
+
import { Diagnostics } from "./diagnostics.js";
|
|
3
|
+
/** One host projection of a logical execution. The core completion remains authoritative. */
|
|
4
|
+
export class ExecutionHandle {
|
|
5
|
+
jobs;
|
|
6
|
+
owner;
|
|
7
|
+
execution;
|
|
8
|
+
telemetry;
|
|
9
|
+
completion;
|
|
10
|
+
settled;
|
|
11
|
+
job;
|
|
12
|
+
progressHandle;
|
|
13
|
+
trace;
|
|
14
|
+
constructor(jobs, owner, execution, telemetry = new Diagnostics()) {
|
|
15
|
+
this.jobs = jobs;
|
|
16
|
+
this.owner = owner;
|
|
17
|
+
this.execution = execution;
|
|
18
|
+
this.telemetry = telemetry;
|
|
19
|
+
this.trace = telemetry.begin();
|
|
20
|
+
this.completion = execution.done;
|
|
21
|
+
const outcome = this.completion.then((v) => ({
|
|
22
|
+
status: v.state === "succeeded"
|
|
23
|
+
? "completed"
|
|
24
|
+
: v.state === "cancelled"
|
|
25
|
+
? "killed"
|
|
26
|
+
: "failed",
|
|
27
|
+
detail: v.state,
|
|
28
|
+
}));
|
|
29
|
+
this.job = jobs.start({
|
|
30
|
+
kind: "execution-bus",
|
|
31
|
+
label: "萧何 / Xiao He",
|
|
32
|
+
owner,
|
|
33
|
+
run: (handle) => {
|
|
34
|
+
this.progressHandle = handle;
|
|
35
|
+
return { done: outcome, cancel: () => this.requestCancel() };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
telemetry.record(this.trace, "job_created");
|
|
39
|
+
this.settled = this.waitAndClean();
|
|
40
|
+
}
|
|
41
|
+
requestCancel() {
|
|
42
|
+
this.execution.cancel();
|
|
43
|
+
}
|
|
44
|
+
progress(event) {
|
|
45
|
+
this.progressHandle?.updateProgress(event.type === "progress"
|
|
46
|
+
? String(event.payload.percent) + "%"
|
|
47
|
+
: event.type);
|
|
48
|
+
}
|
|
49
|
+
absent(error) {
|
|
50
|
+
try {
|
|
51
|
+
return (error instanceof Error &&
|
|
52
|
+
error.message === "unknown job " + this.job &&
|
|
53
|
+
!this.jobs.list(this.owner).some((v) => v.id === this.job));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async waitAndClean() {
|
|
60
|
+
try {
|
|
61
|
+
for (;;) {
|
|
62
|
+
const view = await this.jobs.wait(this.job, 60000, this.owner);
|
|
63
|
+
if (view.status !== "running" && view.status !== "stopping")
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
this.jobs.remove(this.job, this.owner);
|
|
67
|
+
this.telemetry.record(this.trace, "job_released");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.telemetry.record(this.trace, "job_error");
|
|
71
|
+
const removed = this.absent(error);
|
|
72
|
+
// Retain the original execution, including while its caller/owner is being disposed.
|
|
73
|
+
await this.completion;
|
|
74
|
+
if (removed)
|
|
75
|
+
this.telemetry.record(this.trace, "job_released");
|
|
76
|
+
if (!removed) {
|
|
77
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
const view = this.jobs
|
|
80
|
+
.list(this.owner)
|
|
81
|
+
.find((v) => v.id === this.job && v.owner === this.owner);
|
|
82
|
+
if (!view) {
|
|
83
|
+
this.telemetry.record(this.trace, "job_released");
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (view.status !== "running" && view.status !== "stopping") {
|
|
87
|
+
this.jobs.remove(this.job, this.owner);
|
|
88
|
+
this.telemetry.record(this.trace, "job_released");
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
await pause(10);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
this.progressHandle = undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Owns host projections and active deliveries, not a second execution state machine. */
|
|
106
|
+
export class ExecutionOwner {
|
|
107
|
+
handles = new Map();
|
|
108
|
+
deliveries = new Set();
|
|
109
|
+
closing;
|
|
110
|
+
telemetry = new Diagnostics();
|
|
111
|
+
create(key, jobs, owner, execution) {
|
|
112
|
+
if (this.phase !== "open")
|
|
113
|
+
throw Error("CLOSED");
|
|
114
|
+
if (this.handles.has(key))
|
|
115
|
+
throw Error("DUPLICATE_EXECUTION_HANDLE");
|
|
116
|
+
const handle = new ExecutionHandle(jobs, owner, execution, this.telemetry);
|
|
117
|
+
this.add(key, handle);
|
|
118
|
+
return handle;
|
|
119
|
+
}
|
|
120
|
+
diagnostics() {
|
|
121
|
+
return {
|
|
122
|
+
...this.telemetry.snapshot(),
|
|
123
|
+
phase: this.phase,
|
|
124
|
+
activeProjections: this.handles.size,
|
|
125
|
+
deliveries: this.deliveries.size,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
currentPhase = "open";
|
|
129
|
+
get phase() {
|
|
130
|
+
return this.currentPhase;
|
|
131
|
+
}
|
|
132
|
+
get(key) {
|
|
133
|
+
return this.handles.get(key);
|
|
134
|
+
}
|
|
135
|
+
add(key, handle) {
|
|
136
|
+
if (this.phase !== "open")
|
|
137
|
+
throw Error("CLOSED");
|
|
138
|
+
if (this.handles.has(key))
|
|
139
|
+
throw Error("DUPLICATE_EXECUTION_HANDLE");
|
|
140
|
+
this.handles.set(key, handle);
|
|
141
|
+
const release = () => {
|
|
142
|
+
if (this.handles.get(key) === handle)
|
|
143
|
+
this.handles.delete(key);
|
|
144
|
+
};
|
|
145
|
+
void handle.settled.then(release, release);
|
|
146
|
+
}
|
|
147
|
+
track(delivery) {
|
|
148
|
+
this.deliveries.add(delivery);
|
|
149
|
+
const release = () => this.deliveries.delete(delivery);
|
|
150
|
+
void delivery.then(release, release);
|
|
151
|
+
return delivery;
|
|
152
|
+
}
|
|
153
|
+
close(drainCore) {
|
|
154
|
+
if (this.closing)
|
|
155
|
+
return this.closing;
|
|
156
|
+
this.currentPhase = "stopping";
|
|
157
|
+
let resolve, reject;
|
|
158
|
+
this.closing = new Promise((r, j) => {
|
|
159
|
+
resolve = r;
|
|
160
|
+
reject = j;
|
|
161
|
+
});
|
|
162
|
+
void (async () => {
|
|
163
|
+
const core = drainCore();
|
|
164
|
+
this.currentPhase = "draining";
|
|
165
|
+
await core;
|
|
166
|
+
await Promise.allSettled([...this.deliveries]);
|
|
167
|
+
this.currentPhase = "releasing";
|
|
168
|
+
await Promise.allSettled([...this.handles.values()].map((h) => h.settled));
|
|
169
|
+
this.handles.clear();
|
|
170
|
+
this.currentPhase = "closed";
|
|
171
|
+
})().then(resolve, reject);
|
|
172
|
+
return this.closing;
|
|
173
|
+
}
|
|
174
|
+
}
|