@feng0531/dsh-execution-bus 0.3.0 → 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 +9 -3
- 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/docs/OPTIMIZATION.md +47 -0
- package/docs/protocol.md +10 -4
- package/docs/releases/0.4.0.md +17 -0
- package/docs/validation.md +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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
|
|
|
@@ -115,7 +115,7 @@ flowchart TD
|
|
|
115
115
|
2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
|
|
116
116
|
|
|
117
117
|
```powershell
|
|
118
|
-
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.
|
|
118
|
+
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.4.0
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
|
|
@@ -210,6 +210,12 @@ stateDiagram-v2
|
|
|
210
210
|
|
|
211
211
|
执行终态不会再改成另一个终态。取消与成功竞争时,若处理器最终正常返回,仍可能是成功。`delivery` 是另一个字段:`pending` 表示尚未走到返回标记,`returned` 只表示插件已走到工具返回路径,**不证明会话结果已经写入磁盘或模型已经消费**。
|
|
212
212
|
|
|
213
|
+
## 调度与诊断
|
|
214
|
+
|
|
215
|
+
0.4.0 起对已受理的排队请求按会话轮转,同一会话保持 FIFO;不抢占运行任务,也不保证队列满时为其他会话预留名额。执行结果仍由 core 唯一提交,宿主 Job 只负责任务展示、控制与生命周期关联。
|
|
216
|
+
|
|
217
|
+
core 开发者可调用实例 `diagnostics()` 取得队列、运行数、保留结果字节及最近 128 条数值阶段轨迹。不含请求/会话 ID、输入、输出或错误文本,不写日志;诊断不增加模型调用工具,也不是跨重启审计记录。详见[优化范围与验证](<docs/OPTIMIZATION.md>)。
|
|
218
|
+
|
|
213
219
|
## 容量与重要限制
|
|
214
220
|
|
|
215
221
|
默认并发 **2**,排队上限 **32**,最多保存 **256** 条任务记录,单结果最多 **64 KiB**,完成结果保留期 **1 小时**,在新请求和查询时惰性清理;不是一小时一到即定时删除。队列或记录满时拒绝新任务;宿主 Job 自身也有容量限制。
|
|
@@ -244,7 +250,7 @@ npm run test:install # 临时目录安装,不操作日常 DSH
|
|
|
244
250
|
|
|
245
251
|
- [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
|
|
246
252
|
- [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
|
|
247
|
-
- [验证记录](<docs/validation.md>)、[0.
|
|
253
|
+
- [验证记录](<docs/validation.md>)、[0.4.0 发布说明](<docs/releases/0.4.0.md>):已验证与未验证范围。
|
|
248
254
|
- [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
|
|
249
255
|
|
|
250
256
|
## 许可
|
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
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { ExecutionOwner } from "./execution.js";
|
|
3
3
|
import { ExecutionBus, parse } from "./bus.js";
|
|
4
4
|
import { testHandler, readHandler } from "./handlers.js";
|
|
5
5
|
export const name = "execution-bus";
|
|
@@ -8,12 +8,8 @@ export function apply(ctx) {
|
|
|
8
8
|
const bus = new ExecutionBus();
|
|
9
9
|
bus.register("test", testHandler);
|
|
10
10
|
bus.register("host.read", readHandler);
|
|
11
|
-
const
|
|
12
|
-
ctx.effect(() =>
|
|
13
|
-
await bus.close();
|
|
14
|
-
await Promise.allSettled(sharedJobs.values());
|
|
15
|
-
sharedJobs.clear();
|
|
16
|
-
});
|
|
11
|
+
const executions = new ExecutionOwner();
|
|
12
|
+
ctx.effect(() => () => executions.close(() => bus.close()));
|
|
17
13
|
ctx.tools.register({
|
|
18
14
|
name: "execution_bus",
|
|
19
15
|
description: "萧何 / Xiao He. Submit a version 1 request and WAIT for its terminal result in this tool call. Reuse requestId only for the same logical operation. Handlers: test {delayMs:0..30000,fail:boolean}, host.read {file_path,offset?,limit?<=100}. Control types status/result/cancel use empty payload. Acceptance is volatile, not execution success. No polling needed; no restart resume.",
|
|
@@ -42,147 +38,79 @@ export function apply(ctx) {
|
|
|
42
38
|
schema: {},
|
|
43
39
|
render: (_a, v) => [{ type: "text", text: JSON.stringify(v) }],
|
|
44
40
|
},
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
execute(raw, exec) {
|
|
42
|
+
if (executions.phase !== "open")
|
|
43
|
+
return Promise.reject(Error("CLOSED"));
|
|
44
|
+
return executions.track(dispatch(raw, exec));
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
async function dispatch(raw, exec) {
|
|
48
|
+
if (!exec.agent)
|
|
49
|
+
throw Error("OWNING_SESSION_REQUIRED");
|
|
50
|
+
const owner = {
|
|
51
|
+
sessionId: String(exec.agent.id),
|
|
52
|
+
callId: String(exec.callId),
|
|
53
|
+
}, m = parse(raw);
|
|
54
|
+
if (m.type !== "request")
|
|
55
|
+
return bus.command(owner, m);
|
|
56
|
+
let projection;
|
|
57
|
+
const attached = bus.submit(owner, m, (event) => projection?.progress(event), async (input, signal) => {
|
|
58
|
+
const result = await ctx.tools.execute({
|
|
59
|
+
name: "read",
|
|
60
|
+
callId: (String(exec.callId) +
|
|
61
|
+
":bus:" +
|
|
62
|
+
randomUUID()),
|
|
63
|
+
rootCallId: exec.rootCallId,
|
|
64
|
+
parent: exec.token,
|
|
65
|
+
agent: exec.agent,
|
|
66
|
+
arguments: input,
|
|
67
|
+
signal,
|
|
72
68
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
attached.detach();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const abort = () => attached.cancel();
|
|
86
|
-
const jobKey = JSON.stringify([owner.sessionId, m.requestId]);
|
|
87
|
-
let settled = sharedJobs.get(jobKey);
|
|
88
|
-
if (!settled) {
|
|
89
|
-
try {
|
|
90
|
-
const outcome = attached.done.then((v) => ({
|
|
91
|
-
status: v.state === "succeeded"
|
|
92
|
-
? "completed"
|
|
93
|
-
: v.state === "cancelled"
|
|
94
|
-
? "killed"
|
|
95
|
-
: "failed",
|
|
96
|
-
detail: v.state,
|
|
97
|
-
}));
|
|
98
|
-
const job = ctx.jobs.start({
|
|
99
|
-
kind: "execution-bus",
|
|
100
|
-
label: "萧何 / Xiao He",
|
|
101
|
-
owner: exec.agent.id,
|
|
102
|
-
run(h) {
|
|
103
|
-
handle = h;
|
|
104
|
-
return {
|
|
105
|
-
done: outcome,
|
|
106
|
-
cancel: abort,
|
|
107
|
-
};
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
const ownerId = exec.agent.id;
|
|
111
|
-
// One registered host waiter suppresses extra completion messages for every attachment.
|
|
112
|
-
settled = (async () => {
|
|
113
|
-
try {
|
|
114
|
-
for (;;) {
|
|
115
|
-
const view = await ctx.jobs.wait(job, 60000, ownerId);
|
|
116
|
-
if (view.status !== "running" && view.status !== "stopping")
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
ctx.jobs.remove(job, ownerId);
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
// rc.2 has no typed not-found code. Match only this minted Job and
|
|
123
|
-
// confirm absence in the original owner scope; never swallow access/service errors.
|
|
124
|
-
let alreadyRemoved = false;
|
|
125
|
-
try {
|
|
126
|
-
alreadyRemoved =
|
|
127
|
-
error instanceof Error &&
|
|
128
|
-
error.message === `unknown job ${job}` &&
|
|
129
|
-
!ctx.jobs.list(ownerId).some((view) => view.id === job);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
/* Preserve the original wait/cleanup error. */
|
|
133
|
-
}
|
|
134
|
-
// A broken wait channel does not terminate execution. Keep caller cancellation
|
|
135
|
-
// attached, drain both producer and host outcome, then attempt cleanup once.
|
|
136
|
-
await attached.done;
|
|
137
|
-
if (!alreadyRemoved) {
|
|
138
|
-
// No scope-owned observer: teardown can remove it before settlement.
|
|
139
|
-
// After producer drain, allow a bounded host commit window. This is
|
|
140
|
-
// program-side cleanup only, never execution retry or model polling.
|
|
141
|
-
for (let attempt = 0; attempt < 20; attempt++) {
|
|
142
|
-
try {
|
|
143
|
-
const view = ctx.jobs
|
|
144
|
-
.list(ownerId)
|
|
145
|
-
.find((view) => view.id === job && view.owner === ownerId);
|
|
146
|
-
if (!view)
|
|
147
|
-
break;
|
|
148
|
-
if (view.status !== "running" &&
|
|
149
|
-
view.status !== "stopping") {
|
|
150
|
-
ctx.jobs.remove(job, ownerId);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
await pause(10);
|
|
158
|
-
}
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
})();
|
|
163
|
-
sharedJobs.set(jobKey, settled);
|
|
164
|
-
void settled.then(() => sharedJobs.delete(jobKey), () => sharedJobs.delete(jobKey));
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
abort();
|
|
168
|
-
await attached.done;
|
|
169
|
-
attached.detach();
|
|
170
|
-
throw Error("HOST_JOB_UNAVAILABLE");
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
exec.signal.addEventListener("abort", abort, { once: true });
|
|
174
|
-
if (exec.signal.aborted)
|
|
175
|
-
abort();
|
|
69
|
+
if (result.isError)
|
|
70
|
+
throw Error("HOST_READ_FAILED");
|
|
71
|
+
return result.value;
|
|
72
|
+
});
|
|
73
|
+
// Terminal reuse is a read of an existing result, not admission of new work.
|
|
74
|
+
if (attached.accepted.payload.state !== "queued" &&
|
|
75
|
+
attached.accepted.payload.state !== "running") {
|
|
176
76
|
try {
|
|
177
|
-
await settled;
|
|
178
77
|
const value = await attached.done;
|
|
179
78
|
attached.returned();
|
|
180
79
|
return value;
|
|
181
80
|
}
|
|
182
81
|
finally {
|
|
183
|
-
exec.signal.removeEventListener("abort", abort);
|
|
184
82
|
attached.detach();
|
|
185
83
|
}
|
|
186
|
-
}
|
|
187
|
-
|
|
84
|
+
}
|
|
85
|
+
const abort = () => attached.cancel();
|
|
86
|
+
const jobKey = JSON.stringify([owner.sessionId, m.requestId]);
|
|
87
|
+
let execution = executions.get(jobKey);
|
|
88
|
+
if (execution)
|
|
89
|
+
attached.detach(); // only the first attachment projects progress into the shared Job
|
|
90
|
+
if (!execution) {
|
|
91
|
+
try {
|
|
92
|
+
execution = executions.create(jobKey, ctx.jobs, exec.agent.id, attached);
|
|
93
|
+
projection = execution;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
abort();
|
|
97
|
+
await attached.done;
|
|
98
|
+
attached.detach();
|
|
99
|
+
throw Error("HOST_JOB_UNAVAILABLE");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exec.signal.addEventListener("abort", abort, { once: true });
|
|
103
|
+
if (exec.signal.aborted)
|
|
104
|
+
abort();
|
|
105
|
+
try {
|
|
106
|
+
await execution.settled;
|
|
107
|
+
const value = await attached.done;
|
|
108
|
+
attached.returned();
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
exec.signal.removeEventListener("abort", abort);
|
|
113
|
+
attached.detach();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
188
116
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 引擎经验优化整合与验收
|
|
2
|
+
|
|
3
|
+
本轮统一在 0.4.0 交付,不将普通 BUG 修复冒充整体架构优化,不声称模型性能已提高。运行逻辑仅作用于萧何总线及其 Job 关联;保留 0.3.0 的 PTC 派生方式、权限和原工具能力。
|
|
4
|
+
|
|
5
|
+
## 优先级与交付
|
|
6
|
+
|
|
7
|
+
| 优先级 | 交付 | 范围与限制 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| P1 | 执行所有权集中 | 内部 ExecutionHandle 关联 core completion、请求级取消、宿主 Job 和进度;ExecutionOwner 集中跟踪共享投影及工具交付。不是第二个执行状态机 |
|
|
10
|
+
| P1 | 阶段化关闭 | 停止受理 → 请求取消及排空 core → 等待活跃工具交付 → 等待并释放 Job 关联。重复关闭返回同一 Promise,先发布 closing 再触发取消,允许回调重入 |
|
|
11
|
+
| P1 | 通知批次边界 | 每次固定名单;新增监听从后续事件接收;解除的监听跳过;嵌套事件按请求内序号排在当前批之后。同步与异步错误继续隔离 |
|
|
12
|
+
| P2 | 有界脱敏诊断 | core diagnostics 快照及内部 Job 聚合轨迹。每个环最近 128 条;只存数值编号、阶段、耗时、计数,不存业务内容 |
|
|
13
|
+
| P2 | 会话轮转 | 两会话积压测试确认 FIFO 的队头延迟后,实现受理后轮转;保持会话内 FIFO 和并发上限,无入队公平保证或时间 SLA |
|
|
14
|
+
| 暂缓 | 按资源串行 | 没有真实写 Handler,不引入资源锁;PTC 原工具不被全局接管 |
|
|
15
|
+
| 不做 | 工作流/线程系统 | 不加入 DAG、线程池、无锁队列、对象池或按引用数自动取消 |
|
|
16
|
+
|
|
17
|
+
## 实测证据及不能推导的结论
|
|
18
|
+
|
|
19
|
+
确定性压力用例中,A0 已运行,A1–A12 排队后 B0 入队:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
原 FIFO: A0,A1,A2,A3,A4,A5,A6,A7,A8,A9,A10,A11,A12,B0
|
|
23
|
+
会话轮转:A0,A1,B0,A2,A3,A4,A5,A6,A7,A8,A9,A10,A11,A12
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
这是已受理任务的开始顺序对比,不是生产延迟、吞吐或模型轮次提升比例。运行任务不可抢占,新会话可能仍因队列满被拒绝。同一会话多个长任务也可能占用并发槽,轮转不等于资源配额。
|
|
27
|
+
|
|
28
|
+
另外验证三会话、并发为二、取消一个排队任务:只执行其余 23 个不同请求,未超过并发上限,同会话开始顺序保持 FIFO。已有真实宿主等待/清理故障与卸载回归继续执行,不以纯 core 测试替代。
|
|
29
|
+
|
|
30
|
+
## 如何查看诊断
|
|
31
|
+
|
|
32
|
+
从包的 ./core 使用 ExecutionBus,调用 `bus.diagnostics()`。这不是新的 Agent 工具,也不提供跨会话读取入口;生产插件内部所有者的诊断尚未公开为服务或 UI。
|
|
33
|
+
|
|
34
|
+
阶段 elapsedMs 相对于本次受理的单调时钟时间:started 减 accepted 为排队时间、terminal 减 started 为执行时间、returned 减 terminal 为首次返回标记延迟。cancel_requested 与 terminal 间隔只能解释为取消请求到执行终态,未必为成功取消。
|
|
35
|
+
|
|
36
|
+
查询快照会遍历最多 recordLimit 条记录计算保留结果字节数;只在开发者调用时发生。环形轨迹不会记录结果内容,但仍是内部运行信息,不作为可信审计/授权依据。进程关闭后不恢复。
|
|
37
|
+
|
|
38
|
+
## 引擎经验来源
|
|
39
|
+
|
|
40
|
+
- [Bevy Task v0.16.1](https://github.com/bevyengine/bevy/blob/v0.16.1/crates/bevy_tasks/src/task.rs):区分任务完成、取消和 detach;没有照搬 Rust Drop 语义。
|
|
41
|
+
- [Bevy CommandQueue v0.16.1](https://github.com/bevyengine/bevy/blob/v0.16.1/crates/bevy_ecs/src/world/command_queue.rs):先确定消费边界再调用外部代码;未移植 unsafe/字节布局。
|
|
42
|
+
- [Godot 4.4 MessageQueue](https://github.com/godotengine/godot/blob/4.4/core/object/message_queue.cpp):派发重入与消费位置的明确约束。
|
|
43
|
+
- [Godot 4.4 WorkerThreadPool](https://github.com/godotengine/godot/blob/4.4/core/object/worker_thread_pool.h):分阶段关闭;没有增加线程池。
|
|
44
|
+
- [Unity Addressables](https://docs.unity3d.com/Packages/com.unity.addressables@1.21/manual/AddressableAssetsAsyncOperationHandle.html):区分异步操作所有权、释放和取消,不增加模拟指针引用计数。
|
|
45
|
+
- [Stride Scheduler](https://github.com/stride3d/stride/blob/master/sources/core/Stride.Core.MicroThreading/Scheduler.cs)、[O3DE JobCancelGroup](https://github.com/o3de/o3de/blob/development/Code/Framework/AzCore/AzCore/Jobs/JobCancelGroup.h):作为调研参考,分支链接可变;不宣称固定 SHA 或已构建这些引擎。
|
|
46
|
+
|
|
47
|
+
本项目是依据原则自行实现,没有复制这些引擎的源码或引入其运行依赖。
|
package/docs/protocol.md
CHANGED
|
@@ -44,7 +44,7 @@ core 的 `submit()` 返回 `accepted`、`done` Promise 和取消/解除通知/
|
|
|
44
44
|
|
|
45
45
|
通知包含版本、消息 ID、请求 ID、关联 ID(correlationId)、请求内序号、类型与 payload。accepted 的 correlationId 对应本次提交消息,序号是当前快照;后续事件关联首次请求 messageId,新增序号。类型包括 started、progress、cancel_requested 和 succeeded、failed、cancelled、unknown 四种终态事件。
|
|
46
46
|
|
|
47
|
-
Handler 报告的进度序号必须递增;重复、乱序及终态后进度忽略,百分比须在 0–100 内。事件及 payload 在通知前冻结。每个附着独立解除通知,detach
|
|
47
|
+
Handler 报告的进度序号必须递增;重复、乱序及终态后进度忽略,百分比须在 0–100 内。事件及 payload 在通知前冻结。每个附着独立解除通知,detach 不取消执行;取消是另一个操作。派发开始时固定本批名单,派发期间新增监听从下个事件开始接收;已解除且尚未轮到的回调跳过。回调中触发的嵌套事件按序排在当前批之后,不跨越当前通知。观察者抛错或异步拒绝被隔离,不记录异常原文,也不等待观察者资源完成。
|
|
48
48
|
|
|
49
49
|
## 3. 执行状态和交付标记
|
|
50
50
|
|
|
@@ -80,13 +80,13 @@ queued → running → succeeded / failed / cancelled / unknown
|
|
|
80
80
|
| 单结果 JSON | 64 KiB |
|
|
81
81
|
| 终态保留期 | 自完成起 1 小时 |
|
|
82
82
|
|
|
83
|
-
新执行请求受队列和记录容量限制;运行中重复附着受消息/监听名额限制;未过期终态复用不申请新 Job。宿主新 Job
|
|
83
|
+
新执行请求受队列和记录容量限制;运行中重复附着受消息/监听名额限制;未过期终态复用不申请新 Job。宿主新 Job 准入还有独立容量要求。已受理的排队任务按会话轮转,各会话内部 FIFO;不抢占运行任务,没有完成时间 SLA,单会话仍可占满受理容量,不保证其他会话一定获得入队名额。
|
|
84
84
|
|
|
85
85
|
core 不传配置时用默认值;显式配置必须完整包含 concurrency、queueLimit、recordLimit、resultBytes、retentionMs 五项正安全整数。拒绝缺项、额外字段与访问器;构造时复制冻结,无动态更新接口。
|
|
86
86
|
|
|
87
87
|
保留期通过新请求或控制消息触发惰性清理,也可调用实例 `sweep()`;不是精确的一小时定时删除,闲置时记录可能继续占内存,但后续正常查询会先清理过期记录。无磁盘任务库。结果大小检查在 Handler 返回后进行,Handler 自身也必须限制内存分配。敏感返回内容仍可能进入宿主会话日志,本插件不是秘密识别器。
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
关闭幂等且可重入,多次 close 返回同一 Promise。core 先停止受理,取消队列和运行任务,排空后清理内存;宿主执行所有者继续等原工具交付路径及 Job 关联结束,再释放。等待某个 Handler 的通知回调里不应反向 await 整个关闭完成。正常路径复用 Job 窗口等待;服务异常后仍先排空执行,再有界检查记录并保留原服务错误。持续故障可能留记录到宿主生命周期清理,不合作 Handler 可能阻碍关闭。
|
|
90
90
|
|
|
91
91
|
## 5. 重试与恢复语义
|
|
92
92
|
|
|
@@ -94,7 +94,13 @@ core 不传配置时用默认值;显式配置必须完整包含 concurrency、
|
|
|
94
94
|
|
|
95
95
|
同存活实例、保存期内的请求复用不是端到端 exactly-once;每个实际工具调用仍有自己的宿主结果记录。没有外部终态注入接口,也没有跨重启恢复:任一阶段进程崩溃都会丢失插件内存状态。宿主日志不能复活旧 Promise,不追加聊天消息冒充原调用恢复。
|
|
96
96
|
|
|
97
|
-
## 6.
|
|
97
|
+
## 6. 诊断
|
|
98
|
+
|
|
99
|
+
core 实例的 `diagnostics()` 返回只读用途的独立快照:阶段计数、最近 128 条 `{operation,stage,elapsedMs}` 轨迹、关闭阶段、队列/运行/记录数及保留结果字节数。operation 是实例内递增数值,不是消息或会话 ID;elapsedMs 以单调时钟计算,阶段时间均相对受理时间,可相减得到排队/执行/返回延迟。复用计数仅在附着受理成功时增加;返回阶段按逻辑结果首次标记,不代表每个工具调用都持久化。
|
|
100
|
+
|
|
101
|
+
宿主执行所有者另有内部 Job 创建/释放/异常聚合轨迹,不作为模型工具或公开服务导出;其 operation 是独立编号,不能与 core 轨迹按编号关联。数据只在内存,不保存参数、结果、Handler 名称、请求/会话 ID、异常文本或聊天记录。总计数会持续累积,环形轨迹有界;异常计数不等于每次清理最终失败。
|
|
102
|
+
|
|
103
|
+
## 7. 扩展 Handler
|
|
98
104
|
|
|
99
105
|
包的 `./core` 导出 ExecutionBus、Handler、Capability 类型。Handler 只接收输入、requestId、signal、progress 和可选最小 capability,不接收聊天历史。register 返回绑定唯一注册令牌的幂等注销函数,不会误删后来同名注册;已受理请求继续持有原 Handler。
|
|
100
106
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 萧何 / Xiao He 0.4.0
|
|
2
|
+
|
|
3
|
+
## 本版变化
|
|
4
|
+
|
|
5
|
+
统一交付引擎调研中优先级最高的生命周期优化,不改变请求级取消、内存保存或宿主权限规则。
|
|
6
|
+
|
|
7
|
+
- 内部执行句柄集中持有完成结果、请求取消及宿主 Job 投影,core 仍是执行状态唯一事实源。共享执行只由首个附着投影进度,重复调用不重复创建 Job。
|
|
8
|
+
- 关闭分阶段、可重入并幂等,多次 close 返回同一完成 Promise,等待已启动执行和活跃工具交付后释放关联。
|
|
9
|
+
- 固定事件通知批次:回调中新增监听不接收当前事件,解除监听跳过尚未调用部分;嵌套取消/终态按序派发。事件仍不可变,观察者异常继续隔离。
|
|
10
|
+
- core 增加 diagnostics():最近 128 条数值阶段轨迹及容量快照,内部执行所有者另计 Job 事件。无业务参数、结果、请求/会话 ID 或错误文本,无新增模型工具。
|
|
11
|
+
- 压力验证后将已受理调度改为按会话轮转,保持会话内 FIFO;不抢占、不保证入队公平或完成时间。
|
|
12
|
+
|
|
13
|
+
## 验证和范围
|
|
14
|
+
|
|
15
|
+
新增通知重入/关闭、诊断脱敏及多会话调度回归。确定性用例 B0 从 A 的 12 个积压任务之后提前至 A1 后执行,不代表真实模型性能提升;具体顺序与边界见优化说明。
|
|
16
|
+
|
|
17
|
+
沿用 PTC 声明派生,原工具不强制经过总线;Handler 仍只含 test/host.read。资源锁、DAG、线程池、对象池、跨重启恢复不在本版。真实模型、桌面 UI 和实际沙箱仍未验收。兼容 DSH 0.2.0-rc.2,非商业源码公开许可;不安装或重启日常 DSH。
|
package/docs/validation.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 验证范围与使用限制
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本文说明当前版本的证据边界,不是生产环境认证。当前优化基线为 **v0.4.0**,新增执行所有权/关闭、通知重入边界、数值诊断及会话轮转;预设沿用 v0.3.0 的宿主 PTC 声明派生方式。验证环境为 Windows、Node.js 24.21.0,宿主组件锁定 DSH 0.2.0-rc.2、Cordis 4.0.4。
|
|
4
4
|
|
|
5
5
|
## 已验证什么?
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| 故障注入 | 克隆失败、观察者拒绝、Job 等待/清理异常、先行移除与卸载交错 | 指定故障下的回归约束;不证明所有故障组合都已覆盖 |
|
|
14
14
|
| 候选包 | 严格文件白名单、临时目录生产依赖安装、入口注册 | 包可安装和加载;安装验证使用简化宿主夹具,不是桌面插件管理器验收 |
|
|
15
15
|
|
|
16
|
-
v0.
|
|
16
|
+
此前 v0.3.0 本地回归为 **51/51 通过**;本轮新增生命周期与多会话压力回归,最新数量以命令输出为准。测试项数量是用例数量,不是全部产品验收条数。当前结果请重新运行下述命令;文档测试数不应代替实际输出。
|
|
17
17
|
|
|
18
18
|
```powershell
|
|
19
19
|
npm ci --include=dev --ignore-scripts
|
|
@@ -49,7 +49,7 @@ eb95b4e51675f5f1821275e0812ef4dd4984b1aba45a52fbc6bab3518bbf6f38
|
|
|
49
49
|
## 需要知道的运行约束
|
|
50
50
|
|
|
51
51
|
1. 受理、队列、结果及交付标记均在内存。进程关闭会丢失,宿主会话日志不等于插件恢复执行权。
|
|
52
|
-
2.
|
|
52
|
+
2. 同一预设的会话共享总线容量,已受理任务会话轮转、会话内 FIFO;结果访问按会话隔离,不是独立进程或资源安全隔离,也不保证入队名额公平。
|
|
53
53
|
3. 取消任一正在等待的重复调用会请求取消整个逻辑执行,不是仅停止该等待者。
|
|
54
54
|
4. 已终态且未过期的请求可直接复用结果,不申请新 Job;仍须通过身份、冲突及消息额度检查。
|
|
55
55
|
5. 等待服务异常后先排空原执行,再有界检查宿主记录;服务持续不可用时记录可能留到所属生命周期清理。不合作 Handler 仍可能阻碍关闭。
|