@feng0531/dsh-execution-bus 0.11.3 → 0.12.1
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 +11 -4
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/dist/bus.d.ts +34 -2
- package/dist/bus.js +304 -26
- package/dist/diagnostics.d.ts +2 -1
- package/dist/error-codes.d.ts +2 -0
- package/dist/error-codes.js +2 -0
- package/dist/execution.d.ts +1 -0
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +21 -1
- package/dist/product.d.ts +6 -0
- package/dist/product.js +11 -0
- package/dist/protocol.d.ts +11 -0
- package/docs/INTEGRATION.md +28 -1
- package/docs/OPTIMIZATION.md +2 -0
- package/docs/PRODUCT.md +4 -0
- package/docs/protocol.md +20 -3
- package/docs/releases/0.11.1.md +2 -0
- package/docs/releases/0.12.0.md +20 -0
- package/docs/releases/0.12.1.md +30 -0
- package/docs/validation.md +7 -3
- package/package.json +2 -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.12.1**
|
|
8
8
|
|
|
9
9
|
> 萧何模式从宿主已声明的 PTC 组合派生,保留日常工具,再叠加可选执行总线。已验证隔离的真实 Node PTC → 原 read + 总线调用,以及脚本模型内存会话;真实模型、桌面界面与实际沙箱仍未验收。**不支持重启后恢复任务。**
|
|
10
10
|
|
|
@@ -130,7 +130,7 @@ flowchart TD
|
|
|
130
130
|
2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
|
|
131
131
|
|
|
132
132
|
```powershell
|
|
133
|
-
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.
|
|
133
|
+
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.12.1
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
|
|
@@ -205,7 +205,14 @@ PTC 是 DSH 的程序化工具调用入口。在 `run_code` 内使用 `await too
|
|
|
205
205
|
|
|
206
206
|
**跨步骤取回的正确姿势(0.11.1 补充):** 同一 `run_code` 程序内,未完成的 request 会阻塞后发工具调用的结果提交(宿主按提交顺序结算);不要在同一个程序里“启动后立刻 status 轮询”。启动步骤只发请求并尽早结束程序,下一步再用同一 `requestId` 的 `status`/`result`(或重发同一 request)附着取回——pilot 实测单次执行、返回 `reused:true`。
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
### 调度与互斥(0.12.0 起,0.12.1 修正)
|
|
209
|
+
|
|
210
|
+
- **队列类别**:内置 Handler 分为 `fs`(read/glob/grep/edit/write)、`command`(pwsh)、`diagnostic`(test)。同会话内按类别优先级 + 等待老化选择:等待越久提升越多、不封顶(最低到 0 后按 FIFO),避免低优先类别饥饿。
|
|
211
|
+
- **舱壁**:Handler 可声明 `maxConcurrency`,同一 Handler 运行数受限;`execution_bus_state` 的 `waitReason` 会显示 `concurrency`/`bulkhead`/`resource`/`dispatch`,运行超过 30 秒的任务标记 `longRunning`。
|
|
212
|
+
- **资源互斥**:`host.edit`/`host.write` 按 `file_path` 资源键在同一总线实例内串行,避免同文件并发写;失败/取消/关闭都会释放。它是插件内互斥,不是 OS 文件锁或跨进程保证;同一文件的相对/绝对、大小写等别名写法可能绕过互斥。
|
|
213
|
+
- **唯一任务**:核心 Handler 接口支持 `dedupe:"payload"`;**0.12.1 起内置 host.* 不启用**——文件、权限或工作目录变化后缓存旧结果会静默返回过期内容。扩展 Handler 可显式声明并自行承担失效责任(失败/取消/unknown 不缓存)。
|
|
214
|
+
|
|
215
|
+
**0.9.0 起,预算到期不再连带取消已受理执行。** `run_code` 因时间预算到期或程序结束时,宿主会中断其中的工具子调用;总线把这种中断视为**只停止等待**:已受理执行与宿主 Job 继续,程序结束或预算到期后,可在后续 `run_code` 中用同一 `requestId` 的 `status`/`result` 读快照,或重发同一 request 等待终态(`reused:true`),或用 `cancel` 取消。用户主动中断、会话取消、沙箱模式变化等其他原因仍会请求取消整个执行。该识别依赖 DSH 0.2.1-rc.2 的中断原因字符串(见[宿主兼容性](<docs/host-compatibility.md>)),不是宿主公开稳定契约;进程退出仍会丢失全部内存状态,保留期一小时。
|
|
209
216
|
|
|
210
217
|
## 如何理解任务状态?
|
|
211
218
|
|
|
@@ -280,7 +287,7 @@ npm run test:install # 临时目录安装,不操作日常 DSH
|
|
|
280
287
|
|
|
281
288
|
- [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
|
|
282
289
|
- [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
|
|
283
|
-
- [验证记录](<docs/validation.md>)、[0.
|
|
290
|
+
- [验证记录](<docs/validation.md>)、[0.12.1 发布说明](<docs/releases/0.12.1.md>):已验证与未验证范围。
|
|
284
291
|
- [同类项目源码整合](<docs/INTEGRATION.md>):固定来源、采用范围、许可与验证边界。
|
|
285
292
|
- [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
|
|
286
293
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
CI 与发布组织方式参考 dsh-vcs,并按本项目执行能力重新实现;本包不含 Monaco、React 或 SVN 实现。
|
|
6
6
|
|
|
7
7
|
0.5.0 定向参考 dsh-llm-rate-limit(MIT,hanruochong)、dsh-nightshift(MIT,contributors)和 dsh-input-traffic(MIT,contributors)的准入、生命周期和会话视图原则,按本项目契约自行实现,没有复制其源码或捆绑依赖。AsyncGate 与 cc-multi-process-mcp 仅用于边界比较,所查快照未发现 LICENSE,不复制代码。固定来源与阅读范围见 [整合说明](<docs/INTEGRATION.md>)。
|
|
8
|
+
|
|
9
|
+
0.12.0 定向阅读 Temporal(文档;SDK MIT)、Restate(文档;BUSL-1.1,未复制代码)、DBOS(文档;Transact MIT)、Oban(Apache-2.0)、BullMQ(MIT)与 Resilience4j(Apache-2.0)的公开文档/README,按本插件边界重新实现调度隔离与资源互斥,没有复制第三方源码或新增运行依赖。fast-check(MIT,dubzzz)仅作为开发依赖用于属性测试,不进入本项目 tgz。
|
package/dist/bus.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ export interface AdmissionPolicy {
|
|
|
6
6
|
sessionQueueLimit?: number;
|
|
7
7
|
maxQueueWaitMs?: number;
|
|
8
8
|
}
|
|
9
|
+
export interface SchedulerPolicy {
|
|
10
|
+
/** Handler.queue → 类别优先级(数值越小越先出队)。 */
|
|
11
|
+
queueClassOrder?: Readonly<Record<string, number>>;
|
|
12
|
+
/** 等待多久提升一级(最多提升两级)。 */
|
|
13
|
+
queueAgingMs?: number;
|
|
14
|
+
/** 运行超过该阈值时在状态快照标记 longRunning。 */
|
|
15
|
+
longRunningMs?: number;
|
|
16
|
+
}
|
|
9
17
|
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
10
18
|
export declare class ExecutionBus {
|
|
11
19
|
readonly options: {
|
|
@@ -16,11 +24,17 @@ export declare class ExecutionBus {
|
|
|
16
24
|
retentionMs: number;
|
|
17
25
|
};
|
|
18
26
|
readonly admission: AdmissionPolicy;
|
|
27
|
+
readonly scheduler: Readonly<Required<SchedulerPolicy>>;
|
|
19
28
|
private entries;
|
|
20
29
|
private handlers;
|
|
21
30
|
private queue;
|
|
22
31
|
private readyOwners;
|
|
23
32
|
private running;
|
|
33
|
+
private runningByHandler;
|
|
34
|
+
/** 资源键 → 占用它的运行中 Entry;有副作用 Handler 据此互斥。 */
|
|
35
|
+
private resourceLocks;
|
|
36
|
+
/** payload 指纹 → 已成功或运行中的 Entry,用于唯一任务复用。 */
|
|
37
|
+
private dedupeIndex;
|
|
24
38
|
private retainedResultBytes;
|
|
25
39
|
private closed;
|
|
26
40
|
private telemetry;
|
|
@@ -40,6 +54,7 @@ export declare class ExecutionBus {
|
|
|
40
54
|
job_released?: number | undefined;
|
|
41
55
|
job_error?: number | undefined;
|
|
42
56
|
reused?: number | undefined;
|
|
57
|
+
unique_reused?: number | undefined;
|
|
43
58
|
};
|
|
44
59
|
traces: {
|
|
45
60
|
operation: number;
|
|
@@ -55,7 +70,7 @@ export declare class ExecutionBus {
|
|
|
55
70
|
recordLimit: number;
|
|
56
71
|
resultBytes: number;
|
|
57
72
|
retentionMs: number;
|
|
58
|
-
}, admission?: AdmissionPolicy);
|
|
73
|
+
}, admission?: AdmissionPolicy, scheduler?: Readonly<Required<SchedulerPolicy>>);
|
|
59
74
|
register(name: string, handler: Handler): () => void;
|
|
60
75
|
private key;
|
|
61
76
|
private emit;
|
|
@@ -81,6 +96,22 @@ export declare class ExecutionBus {
|
|
|
81
96
|
private admitInvocation;
|
|
82
97
|
/** 若启用了排队期限,登记到期结算为 QUEUE_WAIT_TIMEOUT 的计时器。 */
|
|
83
98
|
private scheduleQueueDeadline;
|
|
99
|
+
/** 计算并校验 Handler 的资源键;失败则请求不进入队列。 */
|
|
100
|
+
private resourcesFor;
|
|
101
|
+
/** 唯一任务命中已成功终态时,构造一个不再执行 Handler 的只读结果记录。 */
|
|
102
|
+
private syntheticUnique;
|
|
103
|
+
private releaseResources;
|
|
104
|
+
/** 优先类别表按自有属性查;未知类别回退 default 权重,避免原型链 NaN。 */
|
|
105
|
+
private classRank;
|
|
106
|
+
/** 当前阻塞来源:Handler 舱壁优先于资源锁;undefined 表示可运行。 */
|
|
107
|
+
private blockedBy;
|
|
108
|
+
/** 是否有空闲槽位供此 Entry 开始执行(Handler 舱壁 + 资源互斥)。 */
|
|
109
|
+
private eligible;
|
|
110
|
+
/** 类别优先级 + 等待老化:等待越久提升越多(不封顶),最低到 0;
|
|
111
|
+
* 同分时按队列先后(FIFO)决出,避免低优先类别饥饿。 */
|
|
112
|
+
private effectivePriority;
|
|
113
|
+
/** 对已到期的排队任务结算 QUEUE_WAIT_TIMEOUT,再从队列移除。 */
|
|
114
|
+
private expireQueued;
|
|
84
115
|
private attachment;
|
|
85
116
|
command(owner: Owner, raw: unknown): RecordView;
|
|
86
117
|
private removeQueued;
|
|
@@ -104,6 +135,7 @@ export declare class ExecutionBus {
|
|
|
104
135
|
error: string | null;
|
|
105
136
|
progress: number | null;
|
|
106
137
|
waitReason: string | null;
|
|
138
|
+
longRunning: boolean;
|
|
107
139
|
phase: string;
|
|
108
140
|
queueMs: number;
|
|
109
141
|
runMs: number;
|
|
@@ -113,7 +145,7 @@ export declare class ExecutionBus {
|
|
|
113
145
|
};
|
|
114
146
|
private cancel;
|
|
115
147
|
private finish;
|
|
116
|
-
/**
|
|
148
|
+
/** 按会话轮转选择下一个可执行项:会话内按类别优先级 + 老化,跨会话公平。 */
|
|
117
149
|
private nextQueuedIndex;
|
|
118
150
|
private pump;
|
|
119
151
|
private run;
|
package/dist/bus.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Diagnostics } from "./diagnostics.js";
|
|
|
4
4
|
import { ERRORS } from "./error-codes.js";
|
|
5
5
|
import { assertJson } from "./json.js";
|
|
6
6
|
import { LIMITS } from "./limits.js";
|
|
7
|
-
import { CORE_DEFAULTS } from "./product.js";
|
|
7
|
+
import { CORE_DEFAULTS, SCHEDULER_POLICY } from "./product.js";
|
|
8
8
|
import { canonical, freezeJson, object, parse } from "./protocol.js";
|
|
9
9
|
export { object, parse };
|
|
10
10
|
const isTerminal = (s) => !["queued", "running"].includes(s);
|
|
@@ -12,15 +12,25 @@ const isTerminal = (s) => !["queued", "running"].includes(s);
|
|
|
12
12
|
function hashPayload(payload) {
|
|
13
13
|
return createHash("sha256").update(canonical(payload)).digest("hex");
|
|
14
14
|
}
|
|
15
|
+
/** 消息身份只保存摘要,避免为每个 messageId 保留完整消息文本占用内存。 */
|
|
16
|
+
function hashMessage(m) {
|
|
17
|
+
return createHash("sha256").update(canonical(m)).digest("hex");
|
|
18
|
+
}
|
|
15
19
|
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
16
20
|
export class ExecutionBus {
|
|
17
21
|
options;
|
|
18
22
|
admission;
|
|
23
|
+
scheduler;
|
|
19
24
|
entries = new Map();
|
|
20
25
|
handlers = new Map();
|
|
21
26
|
queue = [];
|
|
22
27
|
readyOwners = [];
|
|
23
28
|
running = 0;
|
|
29
|
+
runningByHandler = new Map();
|
|
30
|
+
/** 资源键 → 占用它的运行中 Entry;有副作用 Handler 据此互斥。 */
|
|
31
|
+
resourceLocks = new Map();
|
|
32
|
+
/** payload 指纹 → 已成功或运行中的 Entry,用于唯一任务复用。 */
|
|
33
|
+
dedupeIndex = new Map();
|
|
24
34
|
retainedResultBytes = 0;
|
|
25
35
|
closed = false;
|
|
26
36
|
telemetry = new Diagnostics();
|
|
@@ -36,9 +46,55 @@ export class ExecutionBus {
|
|
|
36
46
|
}
|
|
37
47
|
closing;
|
|
38
48
|
shutdownPhase = "open";
|
|
39
|
-
constructor(options = CORE_DEFAULTS, admission = {}) {
|
|
49
|
+
constructor(options = CORE_DEFAULTS, admission = {}, scheduler = SCHEDULER_POLICY) {
|
|
40
50
|
this.options = options;
|
|
41
51
|
this.admission = admission;
|
|
52
|
+
this.scheduler = scheduler;
|
|
53
|
+
const schedulerKeys = ["queueClassOrder", "queueAgingMs", "longRunningMs"];
|
|
54
|
+
if (!scheduler ||
|
|
55
|
+
typeof scheduler !== "object" ||
|
|
56
|
+
Array.isArray(scheduler) ||
|
|
57
|
+
Reflect.ownKeys(scheduler).some((key) => typeof key !== "string" || !schedulerKeys.includes(key)))
|
|
58
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
59
|
+
for (const key of Reflect.ownKeys(scheduler)) {
|
|
60
|
+
const descriptor = Object.getOwnPropertyDescriptor(scheduler, key);
|
|
61
|
+
if (!("value" in descriptor) || !descriptor.enumerable)
|
|
62
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
63
|
+
}
|
|
64
|
+
// 只读取一次:拒绝 accessor,避免校验值与落库值不一致。
|
|
65
|
+
const queueAgingMs = scheduler.queueAgingMs;
|
|
66
|
+
const longRunningMs = scheduler.longRunningMs;
|
|
67
|
+
const rawOrder = scheduler.queueClassOrder;
|
|
68
|
+
if (!Number.isSafeInteger(queueAgingMs) ||
|
|
69
|
+
queueAgingMs < 1 ||
|
|
70
|
+
!Number.isSafeInteger(longRunningMs) ||
|
|
71
|
+
longRunningMs < 1 ||
|
|
72
|
+
!rawOrder ||
|
|
73
|
+
typeof rawOrder !== "object" ||
|
|
74
|
+
Array.isArray(rawOrder))
|
|
75
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
76
|
+
const order = Object.create(null);
|
|
77
|
+
for (const className of Reflect.ownKeys(rawOrder)) {
|
|
78
|
+
if (typeof className !== "string")
|
|
79
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
80
|
+
if (["__proto__", "constructor", "prototype"].includes(className))
|
|
81
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
82
|
+
const descriptor = Object.getOwnPropertyDescriptor(rawOrder, className);
|
|
83
|
+
if (!("value" in descriptor) || !descriptor.enumerable)
|
|
84
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
85
|
+
const rank = descriptor.value;
|
|
86
|
+
if (className.length === 0 ||
|
|
87
|
+
className.length > 64 ||
|
|
88
|
+
!Number.isSafeInteger(rank) ||
|
|
89
|
+
rank < 0)
|
|
90
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
91
|
+
order[className] = rank;
|
|
92
|
+
}
|
|
93
|
+
this.scheduler = Object.freeze({
|
|
94
|
+
queueClassOrder: Object.freeze(order),
|
|
95
|
+
queueAgingMs: queueAgingMs,
|
|
96
|
+
longRunningMs: longRunningMs,
|
|
97
|
+
});
|
|
42
98
|
const allowed = ["sessionQueueLimit", "maxQueueWaitMs"];
|
|
43
99
|
if (!admission ||
|
|
44
100
|
typeof admission !== "object" ||
|
|
@@ -79,7 +135,34 @@ export class ExecutionBus {
|
|
|
79
135
|
register(name, handler) {
|
|
80
136
|
if (this.handlers.has(name))
|
|
81
137
|
throw Error(ERRORS.DUPLICATE_HANDLER);
|
|
82
|
-
|
|
138
|
+
if (!handler ||
|
|
139
|
+
typeof handler !== "object" ||
|
|
140
|
+
typeof handler.validate !== "function" ||
|
|
141
|
+
typeof handler.run !== "function" ||
|
|
142
|
+
(handler.idempotent !== undefined && typeof handler.idempotent !== "boolean") ||
|
|
143
|
+
(handler.queue !== undefined &&
|
|
144
|
+
(typeof handler.queue !== "string" ||
|
|
145
|
+
handler.queue.length === 0 ||
|
|
146
|
+
handler.queue.length > 64 ||
|
|
147
|
+
["__proto__", "constructor", "prototype"].includes(handler.queue))) ||
|
|
148
|
+
(handler.maxConcurrency !== undefined &&
|
|
149
|
+
(!Number.isSafeInteger(handler.maxConcurrency) ||
|
|
150
|
+
handler.maxConcurrency < 1)) ||
|
|
151
|
+
(handler.dedupe !== undefined && handler.dedupe !== "payload") ||
|
|
152
|
+
(handler.dedupe === "payload" && handler.idempotent !== true) ||
|
|
153
|
+
(handler.resources !== undefined && typeof handler.resources !== "function"))
|
|
154
|
+
throw Error(ERRORS.INVALID_HANDLER_OPTIONS);
|
|
155
|
+
// 注册时快照并冻结调度/复用选项,运行期修改原对象不再影响已注册行为。
|
|
156
|
+
const fixed = Object.freeze({
|
|
157
|
+
idempotent: handler.idempotent,
|
|
158
|
+
queue: handler.queue,
|
|
159
|
+
maxConcurrency: handler.maxConcurrency,
|
|
160
|
+
dedupe: handler.dedupe,
|
|
161
|
+
resources: handler.resources?.bind(handler),
|
|
162
|
+
validate: handler.validate.bind(handler),
|
|
163
|
+
run: handler.run.bind(handler),
|
|
164
|
+
});
|
|
165
|
+
const registration = { handler: fixed, token: Symbol(name) };
|
|
83
166
|
this.handlers.set(name, registration);
|
|
84
167
|
let disposed = false;
|
|
85
168
|
return () => {
|
|
@@ -145,6 +228,10 @@ export class ExecutionBus {
|
|
|
145
228
|
this.retainedResultBytes -= e.resultBytes;
|
|
146
229
|
this.entries.delete(k);
|
|
147
230
|
}
|
|
231
|
+
const live = new Set(this.entries.values());
|
|
232
|
+
for (const [key, e] of this.dedupeIndex)
|
|
233
|
+
if (!live.has(e))
|
|
234
|
+
this.dedupeIndex.delete(key);
|
|
148
235
|
}
|
|
149
236
|
submit(owner, raw, onEvent, invoke) {
|
|
150
237
|
if (this.closed)
|
|
@@ -182,14 +269,14 @@ export class ExecutionBus {
|
|
|
182
269
|
if (e.handler.idempotent === false && isTerminal(e.view.state))
|
|
183
270
|
throw Error(ERRORS.MUTATING_REUSE_DENIED);
|
|
184
271
|
const previous = e.messages.get(m.messageId);
|
|
185
|
-
if (previous && previous !==
|
|
272
|
+
if (previous && previous !== hashMessage(m))
|
|
186
273
|
throw Error(ERRORS.MESSAGE_CONFLICT);
|
|
187
274
|
if (!previous && e.messages.size >= LIMITS.messageAccountLimit)
|
|
188
275
|
throw Error(ERRORS.MESSAGE_LIMIT);
|
|
189
276
|
// 先取得附着,再提交消息身份:附着被拒(WAITER_LIMIT)时无副作用。
|
|
190
277
|
const attached = this.attachment(e, onEvent, m.messageId, true);
|
|
191
278
|
if (!previous)
|
|
192
|
-
e.messages.set(m.messageId,
|
|
279
|
+
e.messages.set(m.messageId, hashMessage(m));
|
|
193
280
|
this.telemetry.record(e.trace, "reused");
|
|
194
281
|
return attached;
|
|
195
282
|
}
|
|
@@ -199,6 +286,32 @@ export class ExecutionBus {
|
|
|
199
286
|
if (!handler)
|
|
200
287
|
throw Error(ERRORS.UNKNOWN_HANDLER);
|
|
201
288
|
handler.validate(parts.input);
|
|
289
|
+
// validate 是用户代码:可能在同步执行中触发 close()。
|
|
290
|
+
if (this.closed)
|
|
291
|
+
throw Error(ERRORS.CLOSED);
|
|
292
|
+
const dedupeKey = handler.dedupe === "payload"
|
|
293
|
+
? JSON.stringify([owner.sessionId, parts.handler, fingerprint])
|
|
294
|
+
: undefined;
|
|
295
|
+
if (dedupeKey !== undefined) {
|
|
296
|
+
const target = this.dedupeIndex.get(dedupeKey);
|
|
297
|
+
if (target !== undefined &&
|
|
298
|
+
target.view.state === "succeeded" &&
|
|
299
|
+
isTerminal(target.view.state)) {
|
|
300
|
+
try {
|
|
301
|
+
const unique = this.syntheticUnique(owner, m, parts, key, fingerprint, handler, target, dedupeKey);
|
|
302
|
+
return this.attachment(unique, onEvent, m.messageId, true);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error instanceof Error && error.message === ERRORS.BACKPRESSURE)
|
|
306
|
+
throw error;
|
|
307
|
+
// 结果不可克隆时退回正常执行,避免把唯一缓存错误变成请求失败。
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const resources = this.resourcesFor(handler, parts.input);
|
|
312
|
+
// resources 也是用户代码,返回后重新确认总线未在同步段关闭。
|
|
313
|
+
if (this.closed)
|
|
314
|
+
throw Error(ERRORS.CLOSED);
|
|
202
315
|
if (this.entries.size >= this.options.recordLimit ||
|
|
203
316
|
this.queue.length >= this.options.queueLimit)
|
|
204
317
|
throw Error(ERRORS.BACKPRESSURE);
|
|
@@ -216,6 +329,10 @@ export class ExecutionBus {
|
|
|
216
329
|
owner: owner.sessionId,
|
|
217
330
|
fingerprint,
|
|
218
331
|
handler,
|
|
332
|
+
handlerName: parts.handler,
|
|
333
|
+
queueClass: handler.queue ?? "default",
|
|
334
|
+
resources,
|
|
335
|
+
dedupeKey,
|
|
219
336
|
input: structuredClone(parts.input),
|
|
220
337
|
view: {
|
|
221
338
|
requestId: m.requestId,
|
|
@@ -232,9 +349,11 @@ export class ExecutionBus {
|
|
|
232
349
|
dispatching: false,
|
|
233
350
|
sourceSequence: 0,
|
|
234
351
|
created: Date.now(),
|
|
235
|
-
messages: new Map([[m.messageId,
|
|
352
|
+
messages: new Map([[m.messageId, hashMessage(m)]]),
|
|
236
353
|
};
|
|
237
354
|
this.entries.set(key, e);
|
|
355
|
+
if (dedupeKey !== undefined)
|
|
356
|
+
this.dedupeIndex.set(dedupeKey, e);
|
|
238
357
|
this.telemetry.record(e.trace, "accepted");
|
|
239
358
|
const attached = this.attachment(e, onEvent, m.messageId);
|
|
240
359
|
this.queue.push(e);
|
|
@@ -262,6 +381,131 @@ export class ExecutionBus {
|
|
|
262
381
|
};
|
|
263
382
|
e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
|
|
264
383
|
}
|
|
384
|
+
/** 计算并校验 Handler 的资源键;失败则请求不进入队列。 */
|
|
385
|
+
resourcesFor(handler, input) {
|
|
386
|
+
if (handler.resources === undefined)
|
|
387
|
+
return [];
|
|
388
|
+
let raw;
|
|
389
|
+
try {
|
|
390
|
+
raw = handler.resources(input);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
throw Error(ERRORS.INVALID_RESOURCES);
|
|
394
|
+
}
|
|
395
|
+
if (!Array.isArray(raw) || raw.length > 8)
|
|
396
|
+
throw Error(ERRORS.INVALID_RESOURCES);
|
|
397
|
+
const resources = [];
|
|
398
|
+
for (const resource of raw) {
|
|
399
|
+
if (typeof resource !== "string" ||
|
|
400
|
+
resource.length === 0 ||
|
|
401
|
+
resource.length > 256)
|
|
402
|
+
throw Error(ERRORS.INVALID_RESOURCES);
|
|
403
|
+
if (!resources.includes(resource))
|
|
404
|
+
resources.push(resource);
|
|
405
|
+
}
|
|
406
|
+
resources.sort();
|
|
407
|
+
return resources;
|
|
408
|
+
}
|
|
409
|
+
/** 唯一任务命中已成功终态时,构造一个不再执行 Handler 的只读结果记录。 */
|
|
410
|
+
syntheticUnique(owner, m, parts, key, fingerprint, handler, target, dedupeKey) {
|
|
411
|
+
if (this.entries.size >= this.options.recordLimit)
|
|
412
|
+
throw Error(ERRORS.BACKPRESSURE);
|
|
413
|
+
const snapshot = freezeJson(structuredClone({
|
|
414
|
+
requestId: m.requestId,
|
|
415
|
+
state: "succeeded",
|
|
416
|
+
sequence: 1,
|
|
417
|
+
cancelRequested: false,
|
|
418
|
+
delivery: "pending",
|
|
419
|
+
result: target.view.result,
|
|
420
|
+
}));
|
|
421
|
+
let resultBytes = 0;
|
|
422
|
+
try {
|
|
423
|
+
if (snapshot.result !== undefined)
|
|
424
|
+
resultBytes = Buffer.byteLength(JSON.stringify(snapshot.result));
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
resultBytes = target.resultBytes;
|
|
428
|
+
}
|
|
429
|
+
let resolve;
|
|
430
|
+
const done = new Promise((r) => (resolve = r));
|
|
431
|
+
const trace = this.telemetry.begin();
|
|
432
|
+
const e = {
|
|
433
|
+
trace,
|
|
434
|
+
resultBytes,
|
|
435
|
+
startedAt: trace.at,
|
|
436
|
+
finishedAt: trace.at,
|
|
437
|
+
invoke: undefined,
|
|
438
|
+
correlationId: m.messageId,
|
|
439
|
+
owner: owner.sessionId,
|
|
440
|
+
fingerprint,
|
|
441
|
+
handler,
|
|
442
|
+
handlerName: parts.handler,
|
|
443
|
+
queueClass: handler.queue ?? "default",
|
|
444
|
+
resources: [],
|
|
445
|
+
dedupeKey,
|
|
446
|
+
input: structuredClone(parts.input),
|
|
447
|
+
view: { ...snapshot, sequence: 1 },
|
|
448
|
+
controller: new AbortController(),
|
|
449
|
+
done,
|
|
450
|
+
resolve,
|
|
451
|
+
listeners: new Set(),
|
|
452
|
+
notifications: [],
|
|
453
|
+
dispatching: false,
|
|
454
|
+
sourceSequence: 0,
|
|
455
|
+
created: Date.now(),
|
|
456
|
+
messages: new Map([[m.messageId, hashMessage(m)]]),
|
|
457
|
+
};
|
|
458
|
+
this.entries.set(key, e);
|
|
459
|
+
this.retainedResultBytes += resultBytes;
|
|
460
|
+
this.telemetry.record(e.trace, "accepted");
|
|
461
|
+
this.telemetry.record(e.trace, "unique_reused");
|
|
462
|
+
e.resolve(snapshot);
|
|
463
|
+
return e;
|
|
464
|
+
}
|
|
465
|
+
releaseResources(e) {
|
|
466
|
+
for (const resource of e.resources)
|
|
467
|
+
if (this.resourceLocks.get(resource) === e)
|
|
468
|
+
this.resourceLocks.delete(resource);
|
|
469
|
+
}
|
|
470
|
+
/** 优先类别表按自有属性查;未知类别回退 default 权重,避免原型链 NaN。 */
|
|
471
|
+
classRank(queueClass) {
|
|
472
|
+
const order = this.scheduler.queueClassOrder;
|
|
473
|
+
return Object.hasOwn(order, queueClass) ? order[queueClass] : 3;
|
|
474
|
+
}
|
|
475
|
+
/** 当前阻塞来源:Handler 舱壁优先于资源锁;undefined 表示可运行。 */
|
|
476
|
+
blockedBy(e) {
|
|
477
|
+
const limit = e.handler.maxConcurrency;
|
|
478
|
+
if (limit !== undefined &&
|
|
479
|
+
(this.runningByHandler.get(e.handler) ?? 0) >= limit)
|
|
480
|
+
return "bulkhead";
|
|
481
|
+
for (const resource of e.resources)
|
|
482
|
+
if (this.resourceLocks.has(resource))
|
|
483
|
+
return "resource";
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
/** 是否有空闲槽位供此 Entry 开始执行(Handler 舱壁 + 资源互斥)。 */
|
|
487
|
+
eligible(e) {
|
|
488
|
+
return this.blockedBy(e) === undefined;
|
|
489
|
+
}
|
|
490
|
+
/** 类别优先级 + 等待老化:等待越久提升越多(不封顶),最低到 0;
|
|
491
|
+
* 同分时按队列先后(FIFO)决出,避免低优先类别饥饿。 */
|
|
492
|
+
effectivePriority(e, now) {
|
|
493
|
+
const boost = Math.floor(Math.max(0, now - e.trace.at) / this.scheduler.queueAgingMs);
|
|
494
|
+
return Math.max(0, this.classRank(e.queueClass) - boost);
|
|
495
|
+
}
|
|
496
|
+
/** 对已到期的排队任务结算 QUEUE_WAIT_TIMEOUT,再从队列移除。 */
|
|
497
|
+
expireQueued(now) {
|
|
498
|
+
for (const e of [...this.queue]) {
|
|
499
|
+
if (e.view.state !== "queued") {
|
|
500
|
+
this.removeQueued(e);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (e.deadline !== undefined && now >= e.deadline) {
|
|
504
|
+
this.removeQueued(e);
|
|
505
|
+
this.finish(e, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
265
509
|
attachment(e, onEvent, id, reused = false) {
|
|
266
510
|
// 每次附着独立占配额,即使调用方复用同一个回调。
|
|
267
511
|
const listener = onEvent ? (event) => onEvent(event) : undefined;
|
|
@@ -306,7 +550,7 @@ export class ExecutionBus {
|
|
|
306
550
|
throw Error(ERRORS.INVALID_CONTROL);
|
|
307
551
|
this.sweep();
|
|
308
552
|
const e = this.get(owner, m.requestId);
|
|
309
|
-
const previous = e.messages.get(m.messageId), fingerprint =
|
|
553
|
+
const previous = e.messages.get(m.messageId), fingerprint = hashMessage(m);
|
|
310
554
|
if (previous && previous !== fingerprint)
|
|
311
555
|
throw Error(ERRORS.MESSAGE_CONFLICT);
|
|
312
556
|
if (!previous) {
|
|
@@ -349,8 +593,12 @@ export class ExecutionBus {
|
|
|
349
593
|
waitReason: e.view.state === "queued"
|
|
350
594
|
? this.running >= this.options.concurrency
|
|
351
595
|
? "concurrency"
|
|
352
|
-
: "dispatch"
|
|
596
|
+
: (this.blockedBy(e) ?? "dispatch")
|
|
353
597
|
: null,
|
|
598
|
+
longRunning: e.view.state === "running" &&
|
|
599
|
+
e.startedAt !== undefined &&
|
|
600
|
+
(e.finishedAt ?? now) - e.startedAt >=
|
|
601
|
+
this.scheduler.longRunningMs,
|
|
354
602
|
phase: isTerminal(e.view.state)
|
|
355
603
|
? e.view.delivery === "returned"
|
|
356
604
|
? "returned"
|
|
@@ -393,6 +641,11 @@ export class ExecutionBus {
|
|
|
393
641
|
finish(e, state, result, error, resultBytes = 0) {
|
|
394
642
|
if (isTerminal(e.view.state))
|
|
395
643
|
return;
|
|
644
|
+
this.releaseResources(e);
|
|
645
|
+
if (e.dedupeKey !== undefined &&
|
|
646
|
+
this.dedupeIndex.get(e.dedupeKey) === e &&
|
|
647
|
+
state !== "succeeded")
|
|
648
|
+
this.dedupeIndex.delete(e.dedupeKey);
|
|
396
649
|
if (e.queueTimer !== undefined) {
|
|
397
650
|
clearTimeout(e.queueTimer);
|
|
398
651
|
e.queueTimer = undefined;
|
|
@@ -435,44 +688,66 @@ export class ExecutionBus {
|
|
|
435
688
|
if (!e.dispatching)
|
|
436
689
|
e.listeners.clear();
|
|
437
690
|
}
|
|
438
|
-
/**
|
|
439
|
-
nextQueuedIndex() {
|
|
440
|
-
|
|
441
|
-
|
|
691
|
+
/** 按会话轮转选择下一个可执行项:会话内按类别优先级 + 老化,跨会话公平。 */
|
|
692
|
+
nextQueuedIndex(now) {
|
|
693
|
+
const owners = this.readyOwners.length;
|
|
694
|
+
for (let n = 0; n < owners; n++) {
|
|
442
695
|
const owner = this.readyOwners.shift();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (
|
|
696
|
+
let chosen = -1;
|
|
697
|
+
let chosenScore = Infinity;
|
|
698
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
699
|
+
const e = this.queue[i];
|
|
700
|
+
if (e.owner !== owner || !this.eligible(e))
|
|
701
|
+
continue;
|
|
702
|
+
const score = this.effectivePriority(e, now);
|
|
703
|
+
if (score < chosenScore) {
|
|
704
|
+
chosen = i;
|
|
705
|
+
chosenScore = score;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (chosen < 0) {
|
|
709
|
+
// 该会话暂时全部被舱壁/资源阻塞;保留轮次,等释放后重新争取。
|
|
710
|
+
if (this.queue.some((entry) => entry.owner === owner))
|
|
448
711
|
this.readyOwners.push(owner);
|
|
712
|
+
continue;
|
|
449
713
|
}
|
|
714
|
+
if (this.queue.some((entry, i) => i !== chosen && entry.owner === owner))
|
|
715
|
+
this.readyOwners.push(owner);
|
|
716
|
+
return chosen;
|
|
450
717
|
}
|
|
451
|
-
return
|
|
718
|
+
return -1;
|
|
452
719
|
}
|
|
453
720
|
pump() {
|
|
454
|
-
while (!this.closed
|
|
455
|
-
|
|
456
|
-
this.
|
|
457
|
-
|
|
721
|
+
while (!this.closed) {
|
|
722
|
+
const now = performance.now();
|
|
723
|
+
this.expireQueued(now);
|
|
724
|
+
if (this.running >= this.options.concurrency || this.queue.length === 0)
|
|
725
|
+
return;
|
|
726
|
+
const index = this.nextQueuedIndex(now);
|
|
727
|
+
if (index < 0)
|
|
728
|
+
return;
|
|
458
729
|
const e = this.queue.splice(index, 1)[0];
|
|
459
730
|
if (e.view.state !== "queued")
|
|
460
731
|
continue;
|
|
461
|
-
if (e.deadline !== undefined && performance.now() >= e.deadline) {
|
|
462
|
-
this.finish(e, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
732
|
if (e.queueTimer !== undefined) {
|
|
466
733
|
clearTimeout(e.queueTimer);
|
|
467
734
|
e.queueTimer = undefined;
|
|
468
735
|
}
|
|
469
736
|
e.startedAt = performance.now();
|
|
470
737
|
this.running++;
|
|
738
|
+
this.runningByHandler.set(e.handler, (this.runningByHandler.get(e.handler) ?? 0) + 1);
|
|
739
|
+
for (const resource of e.resources)
|
|
740
|
+
this.resourceLocks.set(resource, e);
|
|
471
741
|
e.view.state = "running";
|
|
472
742
|
this.telemetry.record(e.trace, "started");
|
|
473
743
|
this.emit(e, "started", {});
|
|
474
744
|
void this.run(e).finally(() => {
|
|
475
745
|
this.running--;
|
|
746
|
+
const remaining = (this.runningByHandler.get(e.handler) ?? 1) - 1;
|
|
747
|
+
if (remaining > 0)
|
|
748
|
+
this.runningByHandler.set(e.handler, remaining);
|
|
749
|
+
else
|
|
750
|
+
this.runningByHandler.delete(e.handler);
|
|
476
751
|
this.pump();
|
|
477
752
|
});
|
|
478
753
|
}
|
|
@@ -545,6 +820,9 @@ export class ExecutionBus {
|
|
|
545
820
|
await Promise.all(entries.map((e) => e.done));
|
|
546
821
|
this.handlers.clear();
|
|
547
822
|
this.entries.clear();
|
|
823
|
+
this.dedupeIndex.clear();
|
|
824
|
+
this.resourceLocks.clear();
|
|
825
|
+
this.runningByHandler.clear();
|
|
548
826
|
this.retainedResultBytes = 0;
|
|
549
827
|
this.queue = [];
|
|
550
828
|
this.readyOwners = [];
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TraceStage = "accepted" | "started" | "cancel_requested" | "terminal" | "returned" | "job_created" | "job_released" | "job_error" | "reused";
|
|
1
|
+
export type TraceStage = "accepted" | "started" | "cancel_requested" | "terminal" | "returned" | "job_created" | "job_released" | "job_error" | "reused" | "unique_reused";
|
|
2
2
|
export interface Trace {
|
|
3
3
|
operation: number;
|
|
4
4
|
stage: TraceStage;
|
|
@@ -30,6 +30,7 @@ export declare class Diagnostics {
|
|
|
30
30
|
job_released?: number | undefined;
|
|
31
31
|
job_error?: number | undefined;
|
|
32
32
|
reused?: number | undefined;
|
|
33
|
+
unique_reused?: number | undefined;
|
|
33
34
|
};
|
|
34
35
|
traces: {
|
|
35
36
|
operation: number;
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare const ERRORS: Readonly<{
|
|
|
38
38
|
INVALID_OPTIONS: "INVALID_OPTIONS";
|
|
39
39
|
DUPLICATE_HANDLER: "DUPLICATE_HANDLER";
|
|
40
40
|
MUTATING_REUSE_DENIED: "MUTATING_REUSE_DENIED";
|
|
41
|
+
INVALID_HANDLER_OPTIONS: "INVALID_HANDLER_OPTIONS";
|
|
42
|
+
INVALID_RESOURCES: "INVALID_RESOURCES";
|
|
41
43
|
DUPLICATE_EXECUTION_HANDLE: "DUPLICATE_EXECUTION_HANDLE";
|
|
42
44
|
INVALID_STATE_QUERY: "INVALID_STATE_QUERY";
|
|
43
45
|
HOST_READ_FAILED: "HOST_READ_FAILED";
|
package/dist/error-codes.js
CHANGED
|
@@ -41,6 +41,8 @@ export const ERRORS = Object.freeze({
|
|
|
41
41
|
INVALID_OPTIONS: "INVALID_OPTIONS",
|
|
42
42
|
DUPLICATE_HANDLER: "DUPLICATE_HANDLER",
|
|
43
43
|
MUTATING_REUSE_DENIED: "MUTATING_REUSE_DENIED",
|
|
44
|
+
INVALID_HANDLER_OPTIONS: "INVALID_HANDLER_OPTIONS",
|
|
45
|
+
INVALID_RESOURCES: "INVALID_RESOURCES",
|
|
44
46
|
// —— 宿主投影(execution / plugin) ——
|
|
45
47
|
DUPLICATE_EXECUTION_HANDLE: "DUPLICATE_EXECUTION_HANDLE",
|
|
46
48
|
INVALID_STATE_QUERY: "INVALID_STATE_QUERY",
|
package/dist/execution.d.ts
CHANGED
package/dist/handlers.d.ts
CHANGED
|
@@ -5,9 +5,15 @@ export declare function hostToolHandler(toolName: string, opts: {
|
|
|
5
5
|
idempotent: boolean;
|
|
6
6
|
validate(input: Record<string, unknown>): void;
|
|
7
7
|
prepare?(input: Record<string, unknown>): Record<string, unknown>;
|
|
8
|
+
queue?: string;
|
|
9
|
+
maxConcurrency?: number;
|
|
10
|
+
resources?(input: Record<string, unknown>): readonly string[];
|
|
11
|
+
dedupe?: "payload";
|
|
8
12
|
}): Handler;
|
|
9
13
|
export declare const testHandler: Handler;
|
|
10
|
-
/** 读取:无副作用,幂等,终态结果可复用。
|
|
14
|
+
/** 读取:无副作用,幂等,终态结果可复用。0.12.1 起内置 host.* 不再声明
|
|
15
|
+
* payload 去重:文件/权限/工作目录会变化,缓存旧结果会静默返回过期内容。
|
|
16
|
+
* 扩展 Handler 可自行声明 dedupe 并承担失效责任。 */
|
|
11
17
|
export declare const readHandler: Handler;
|
|
12
18
|
/** 文件名搜索:只读,幂等,终态结果可复用;搜索基数由宿主工具限制。 */
|
|
13
19
|
export declare const globHandler: Handler;
|
package/dist/handlers.js
CHANGED
|
@@ -8,6 +8,10 @@ export function hostToolHandler(toolName, opts) {
|
|
|
8
8
|
void toolName;
|
|
9
9
|
return {
|
|
10
10
|
idempotent: opts.idempotent,
|
|
11
|
+
queue: opts.queue,
|
|
12
|
+
maxConcurrency: opts.maxConcurrency,
|
|
13
|
+
resources: opts.resources,
|
|
14
|
+
dedupe: opts.dedupe,
|
|
11
15
|
validate: opts.validate,
|
|
12
16
|
async run(input, ctx) {
|
|
13
17
|
if (!ctx.invoke)
|
|
@@ -18,6 +22,7 @@ export function hostToolHandler(toolName, opts) {
|
|
|
18
22
|
}
|
|
19
23
|
export const testHandler = {
|
|
20
24
|
idempotent: true,
|
|
25
|
+
queue: "diagnostic",
|
|
21
26
|
validate(input) {
|
|
22
27
|
if (Object.keys(input).some((k) => !["delayMs", "fail"].includes(k)) ||
|
|
23
28
|
!Number.isSafeInteger(input.delayMs) ||
|
|
@@ -61,9 +66,17 @@ function validateEdit(input) {
|
|
|
61
66
|
(input.replace_all !== undefined && typeof input.replace_all !== "boolean"))
|
|
62
67
|
throw Error(ERRORS.INVALID_EDIT_INPUT);
|
|
63
68
|
}
|
|
64
|
-
/**
|
|
69
|
+
/** 同一路径的文件写入/编辑互斥;路径按调用原文作为资源键(不做跨平台归一化,
|
|
70
|
+
* 因此别名写法可能绕过互斥;见 docs/validation.md)。 */
|
|
71
|
+
function fileResource(input) {
|
|
72
|
+
return ["file:" + String(input.file_path)];
|
|
73
|
+
}
|
|
74
|
+
/** 读取:无副作用,幂等,终态结果可复用。0.12.1 起内置 host.* 不再声明
|
|
75
|
+
* payload 去重:文件/权限/工作目录会变化,缓存旧结果会静默返回过期内容。
|
|
76
|
+
* 扩展 Handler 可自行声明 dedupe 并承担失效责任。 */
|
|
65
77
|
export const readHandler = hostToolHandler("read", {
|
|
66
78
|
idempotent: true,
|
|
79
|
+
queue: "fs",
|
|
67
80
|
validate: validateRead,
|
|
68
81
|
prepare: (input) => ({
|
|
69
82
|
...input,
|
|
@@ -81,6 +94,7 @@ function validateGlob(input) {
|
|
|
81
94
|
/** 文件名搜索:只读,幂等,终态结果可复用;搜索基数由宿主工具限制。 */
|
|
82
95
|
export const globHandler = hostToolHandler("glob", {
|
|
83
96
|
idempotent: true,
|
|
97
|
+
queue: "fs",
|
|
84
98
|
validate: validateGlob,
|
|
85
99
|
});
|
|
86
100
|
function validateGrep(input) {
|
|
@@ -111,6 +125,7 @@ function validateGrep(input) {
|
|
|
111
125
|
/** 内容搜索:只读,幂等,终态结果可复用;匹配上限由宿主工具限制。 */
|
|
112
126
|
export const grepHandler = hostToolHandler("grep", {
|
|
113
127
|
idempotent: true,
|
|
128
|
+
queue: "fs",
|
|
114
129
|
validate: validateGrep,
|
|
115
130
|
});
|
|
116
131
|
function validateWrite(input) {
|
|
@@ -124,11 +139,15 @@ function validateWrite(input) {
|
|
|
124
139
|
/** 写入:有副作用,非幂等,终态结果不可复用。 */
|
|
125
140
|
export const writeHandler = hostToolHandler("write", {
|
|
126
141
|
idempotent: false,
|
|
142
|
+
queue: "fs",
|
|
143
|
+
resources: fileResource,
|
|
127
144
|
validate: validateWrite,
|
|
128
145
|
});
|
|
129
146
|
/** 编辑:有副作用,非幂等,终态结果不可复用。 */
|
|
130
147
|
export const editHandler = hostToolHandler("edit", {
|
|
131
148
|
idempotent: false,
|
|
149
|
+
queue: "fs",
|
|
150
|
+
resources: fileResource,
|
|
132
151
|
validate: validateEdit,
|
|
133
152
|
});
|
|
134
153
|
/** 与宿主 pwsh 工具一致的提权目标;沙箱组合未提供提权面时由宿主拒绝。 */
|
|
@@ -170,5 +189,6 @@ function validatePwsh(input) {
|
|
|
170
189
|
/** 命令执行:有副作用,非幂等,终态结果不可复用;后台模式返回 Job ID 不等于完成。 */
|
|
171
190
|
export const pwshHandler = hostToolHandler("pwsh", {
|
|
172
191
|
idempotent: false,
|
|
192
|
+
queue: "command",
|
|
173
193
|
validate: validatePwsh,
|
|
174
194
|
});
|
package/dist/product.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare const HANDLER_LIMITS: Readonly<{
|
|
|
23
23
|
readPathChars: 2048;
|
|
24
24
|
diagnosticDelayMs: 30000;
|
|
25
25
|
}>;
|
|
26
|
+
/** 调度策略:类别优先级 + 等待老化 + 长任务诊断阈值。 */
|
|
27
|
+
export declare const SCHEDULER_POLICY: Readonly<{
|
|
28
|
+
longRunningMs: 30000;
|
|
29
|
+
queueAgingMs: 30000;
|
|
30
|
+
queueClassOrder: Readonly<Record<string, number>>;
|
|
31
|
+
}>;
|
|
26
32
|
export declare const REQUEST_PARAMETERS: {
|
|
27
33
|
readonly oneOf: readonly [...{
|
|
28
34
|
type: "object";
|
package/dist/product.js
CHANGED
|
@@ -23,6 +23,17 @@ export const HANDLER_LIMITS = Object.freeze({
|
|
|
23
23
|
readPathChars: 2048,
|
|
24
24
|
diagnosticDelayMs: 30000,
|
|
25
25
|
});
|
|
26
|
+
/** 调度策略:类别优先级 + 等待老化 + 长任务诊断阈值。 */
|
|
27
|
+
export const SCHEDULER_POLICY = Object.freeze({
|
|
28
|
+
longRunningMs: 30000,
|
|
29
|
+
queueAgingMs: 30000,
|
|
30
|
+
queueClassOrder: Object.freeze({
|
|
31
|
+
fs: 0,
|
|
32
|
+
diagnostic: 1,
|
|
33
|
+
command: 2,
|
|
34
|
+
default: 3,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
26
37
|
/** 每个 Handler 的模型可见输入声明。只用于工具 schema/SDK 生成;运行时仍由 Handler.validate 严格校验。 */
|
|
27
38
|
const INPUT_SCHEMAS = {
|
|
28
39
|
test: {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -24,6 +24,17 @@ export interface Handler {
|
|
|
24
24
|
/** 是否可安全复用终态结果。省略默认 true(可复用,向后兼容);
|
|
25
25
|
* 显式 false = 有副作用,终态结果复用将被拒绝(运行中等待仍放行)。 */
|
|
26
26
|
readonly idempotent?: boolean;
|
|
27
|
+
/** 调度队列类别;默认按 "default"。不同类别之间按类别优先级 + 等待老化排序,
|
|
28
|
+
* 避免长任务类独占出队顺序。 */
|
|
29
|
+
readonly queue?: string;
|
|
30
|
+
/** 同一 Handler 同时在跑的上限;省略表示只受全局并发限制(舱壁)。 */
|
|
31
|
+
readonly maxConcurrency?: number;
|
|
32
|
+
/** 可选资源键(如文件路径):同键请求在总线内互斥,避免并发写同一资源。
|
|
33
|
+
* 由 Handler 在执行前从输入推导;返回空数组表示不占用资源。 */
|
|
34
|
+
resources?(input: Record<string, unknown>): readonly string[];
|
|
35
|
+
/** 唯一任务策略:"payload" 表示幂等只读 Handler 可按 handler+input 指纹复用
|
|
36
|
+
* 已成功的终态结果(不同 requestId);省略表示只按 requestId 去重。 */
|
|
37
|
+
readonly dedupe?: "payload";
|
|
27
38
|
validate(input: Record<string, unknown>): void;
|
|
28
39
|
run(input: Record<string, unknown>, ctx: {
|
|
29
40
|
signal: AbortSignal;
|
package/docs/INTEGRATION.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 同类项目源码参考与整合(0.5.0)
|
|
1
|
+
# 同类项目源码参考与整合(0.5.0–0.12.1)
|
|
2
2
|
|
|
3
3
|
## 本轮交付
|
|
4
4
|
|
|
@@ -21,6 +21,33 @@
|
|
|
21
21
|
| [cc-multi-process-mcp / 234823f1](https://github.com/L-x-C/cc-multi-process-mcp/tree/234823f15616d7e9c759072e940396d939f823c2) | main.py 的进程检测、信号回收、同步/异步执行、结果文件和清理路径 | 不引入自建进程池或轮询。源码的 skip_permissions 默认和 /tmp 存储不适合本插件宿主边界;未发现 LICENSE 文件,不复制代码 |
|
|
22
22
|
|
|
23
23
|
这是相关路径的定向阅读,不是五仓全量逐文件安全审核。参考快照在开发目录,明确忽略,不进入源码提交或 npm 包。
|
|
24
|
+
## 0.12.0 追加整合:调度隔离、资源互斥、唯一任务与属性测试
|
|
25
|
+
|
|
26
|
+
本轮以 0.11.3 为基线,读取以下公开文档/README(2026-10-18,文档站点与仓库链接;未复制源码,未新增运行依赖),只采用与插件边界相符的机制:
|
|
27
|
+
|
|
28
|
+
| 来源 | 实际阅读内容 | 采用与不采用 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| [Temporal Workflow Id / Reuse Policy](https://docs.temporal.io/workflow-execution/workflowid-runid) | Workflow Id 唯一性、Run Id 区分、关闭后复用策略 | 参考“稳定逻辑编号 + 复用策略”分层;不引入持久化运行历史 |
|
|
31
|
+
| [Restate 文档](https://docs.restate.dev/)(BUSL-1.1,仅文档) | exactly-once、按 scope 的并发/流量控制、virtual object 串行化 | 参考“按类别/资源隔离并发”;不复制代码,不引入其运行时 |
|
|
32
|
+
| [DBOS 文档](https://docs.dbos.dev/) | durable queues、exactly-once、事务入队 | 仅对照边界;无持久化宿主契约,不实现 |
|
|
33
|
+
| [Oban 文档](https://oban.hexdocs.pm/Oban.html)(Apache-2.0) | 队列独立并发上限、唯一任务、10 级优先级 | 实现 Handler.queue 类别 + 优先级 + 等待老化;不引入数据库/自动重试 |
|
|
34
|
+
| [BullMQ README](https://github.com/taskforcesh/bullmq)(MIT) | priorities、concurrency、rate limiter、stalled jobs | 参考优先级/舱壁;不引入 Redis/停滞重排 |
|
|
35
|
+
| [Resilience4j README](https://github.com/resilience4j/resilience4j)(Apache-2.0) | Bulkhead、TimeLimiter | 实现 Handler.maxConcurrency 舱壁;超时暂只做 longRunning 诊断 |
|
|
36
|
+
| [fast-check](https://github.com/dubzzz/fast-check)(MIT,devDependency) | 属性测试 API | 新增随机序列不变量回归;仅开发依赖,不进入 tgz |
|
|
37
|
+
|
|
38
|
+
本轮实现(0.12.0):
|
|
39
|
+
|
|
40
|
+
1. Handler 调度声明:queue、maxConcurrency、resources(input)、dedupe:"payload";register 时校验。
|
|
41
|
+
2. 调度:同会话内按类别优先级(fs < diagnostic < command < default)+ 等待老化(每 30 秒提升一级,不封顶,最低到 0)选择;跨会话仍按会话轮转;Handler.maxConcurrency 作为舱壁。0.12.1 修正老化封顶导致的 default 类饥饿。
|
|
42
|
+
3. 资源互斥:Handler.resources 返回稳定字符串键(host.edit/host.write 使用 file:<file_path>);同一总线实例内同键串行,失败/取消/关闭释放;排队期限仍然有效。
|
|
43
|
+
4. 唯一任务:幂等 Handler 声明 dedupe:"payload"(host.read/glob/grep);同会话同 handler+payload 已有 succeeded 终态时,新 requestId 直接复用快照并带 reused:true;失败结果不缓存。
|
|
44
|
+
5. 诊断:execution_bus_state 任务增加 longRunning(30 秒阈值);waitReason 增加 resource。
|
|
45
|
+
6. 测试:新增 8 项调度/唯一/资源回归、2 项 fast-check 属性回归与 2 项插件级 Handler 回归。
|
|
46
|
+
|
|
47
|
+
不采用:Oban/BullMQ 的数据库/Redis 持久化、自动重试、停滞重排;Temporal/DBOS 的持久化执行与跨重启恢复(缺宿主契约);Restate 源码(BUSL-1.1)。资源互斥仅在单个总线实例内有效,不是 OS 文件锁或跨进程保证。
|
|
48
|
+
|
|
49
|
+
许可:本轮未复制第三方源码;fast-check 作为 devDependency 使用(MIT),不进入 tgz;Restate 文档仅作机制参考(BUSL-1.1)。
|
|
50
|
+
|
|
24
51
|
|
|
25
52
|
## 许可与实现归属
|
|
26
53
|
|
package/docs/OPTIMIZATION.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 引擎经验优化整合与验收
|
|
2
2
|
|
|
3
|
+
> 历史文档:本页记录 0.4.0 的优化。其中“按资源串行”当时暂缓,已在 0.12.0 以 Handler.resources + 资源锁实现(见 [整合说明](<INTEGRATION.md>) 与 [0.12.1 发布说明](<releases/0.12.1.md>));冲突时以 README/protocol 与当前发布说明为准。
|
|
4
|
+
|
|
3
5
|
本轮统一在 0.4.0 交付,不将普通 BUG 修复冒充整体架构优化,不声称模型性能已提高。运行逻辑仅作用于萧何总线及其 Job 关联;保留 0.3.0 的 PTC 派生方式、权限和原工具能力。
|
|
4
6
|
|
|
5
7
|
## 优先级与交付
|
package/docs/PRODUCT.md
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
0.11.0 根据 pilot 收紧为收益导向触发:重复提交/重试去重、预算结束后继续/跨步骤取回、多等待者同一执行才用总线,时长本身不是理由;新增简化调用(requestId+handler+input,messageId 自动生成)与控制(requestId+type),完整 v1 消息仍兼容;快照在复用时带 reused:true,并新增只读的会话采用率统计脚本。
|
|
14
14
|
|
|
15
|
+
0.12.0 补充调度隔离:Handler 可声明调度类别、舱壁、资源键与 payload 唯一任务;内置写 Handler 按文件路径互斥;状态快照增加 longRunning 与等待原因。参考来源与许可见 [整合说明](<INTEGRATION.md>),未引入第三方运行依赖;fast-check 仅用于开发属性测试。
|
|
16
|
+
|
|
17
|
+
0.12.1 修正 0.12.0 的默认行为风险:内置 host.* 关闭 payload 去重(文件/权限/工作目录变化后旧结果会静默过期),只保留核心 Handler 能力供扩展自担失效责任;等待老化不封顶,避免 default 类别饥饿;唯一复用副本计入保留字节并写入时间戳;waitReason 区分 bulkhead 与 resource;注册时校验并冻结调度选项、拒绝 accessor/原型污染键;validate/resources 内同步 close 后不再插入不可结算记录;消息身份改存摘要,降低同 requestId 多条消息的内存占用。
|
|
18
|
+
|
|
15
19
|
## 入口及配置层次
|
|
16
20
|
|
|
17
21
|
| 入口 | 谁使用 | 责任 |
|
package/docs/protocol.md
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
### 去重与消息额度
|
|
30
30
|
|
|
31
|
-
- 逻辑键是 **当前存活总线实例 + 宿主会话身份 + requestId
|
|
32
|
-
- 对参数规范排序后的摘要仅用于同 requestId 的冲突比较,不用于永久按参数去重。相同参数的新 requestId
|
|
31
|
+
- 逻辑键是 **当前存活总线实例 + 宿主会话身份 + requestId**。不同会话不能查询/取消彼此记录;同一预设修订(generation)的会话共享总线容量。
|
|
32
|
+
- 对参数规范排序后的摘要仅用于同 requestId 的冲突比较,不用于永久按参数去重。相同参数的新 requestId 默认仍是新操作;仅显式声明 `dedupe:"payload"` 的幂等 Handler 可复用同会话已成功的 payload 结果(0.12.1 起内置 host.* 不声明)。
|
|
33
33
|
- 普通消息最多记账 128 条,含首次请求、重复请求及查询。满后拒绝普通新消息;首次尚未确认的有效取消可使用一个额外控制槽,最多 129 条。已记账消息的冲突检查不因控制槽而放松。
|
|
34
34
|
- 活跃观察者附着最多 32 个;每个附着独立占名额,即使共用回调。附着被拒绝时不占用新消息编号/额度。
|
|
35
35
|
- 终态超期清理后旧请求键失效,不应把此机制用于长期避免外部副作用重复。
|
|
@@ -110,7 +110,7 @@ core 不传配置时用默认值;显式配置必须完整包含 concurrency、
|
|
|
110
110
|
|
|
111
111
|
期限使用单调时钟,计时器及出队前均检查,超期只将 queued 结算 failed / QUEUE_WAIT_TIMEOUT,不调用 Handler、不设置 cancelRequested、不重试,沿原 done/Job 返回。开始运行、取消或关闭时清除计时器。事件循环繁忙会令通知晚于期限,但不会让超期任务出队执行。已运行任务继续遵守原执行/取消规则。
|
|
112
112
|
|
|
113
|
-
execution_bus_state 工具仅接受 {},从宿主 Agent 取 sessionId,拒绝匿名或自报身份,关闭后拒绝查询。它读取 core inspect 的不可变快照,不创建 Job、不改变消息额度;查询会做正常终态 TTL 清理。不包含输入/输出,不访问其他会话。最多 50 条、活跃优先,其后按受理次序倒序列出终态;总数未截断,truncated 提示列表截断。元数据含 requestId、state、delivery、cancelRequested、固定 error/progress、waitReason
|
|
113
|
+
execution_bus_state 工具仅接受 {},从宿主 Agent 取 sessionId,拒绝匿名或自报身份,关闭后拒绝查询。它读取 core inspect 的不可变快照,不创建 Job、不改变消息额度;查询会做正常终态 TTL 清理。不包含输入/输出,不访问其他会话。最多 50 条、活跃优先,其后按受理次序倒序列出终态;总数未截断,truncated 提示列表截断。元数据含 requestId、state、delivery、cancelRequested、固定 error/progress、waitReason(concurrency/bulkhead/resource/dispatch)、longRunning(运行超过 30 秒)、queueMs/runMs/remainingQueueMs。phase terminal 只表示执行终态且尚未标记返回,不代表可推断宿主 Job 清理进度。
|
|
114
114
|
|
|
115
115
|
core inspect(trustedOwner) 要求非空 sessionId,但不提供鉴权,可信身份和访问控制属于嵌入宿主。勿把 core 的任意 owner 参数直接暴露给远端。状态不是持久审计/模型消费回执,不应循环调用。
|
|
116
116
|
|
|
@@ -128,4 +128,21 @@ core 实例的 `diagnostics()` 返回只读用途的独立快照:阶段计数
|
|
|
128
128
|
|
|
129
129
|
0.11.0 起,模型可传简化输入:请求 `{requestId, handler, input, messageId?}`、控制 `{requestId, type, messageId?}`;插件在进入总线前补齐完整 v1 消息,完整 v1 消息仍被接受。返回快照在附着已有执行时追加可选 `reused:true`,只表示复用了逻辑执行,不代表外部副作用对账。
|
|
130
130
|
|
|
131
|
+
0.12.0 扩展 Handler 可选声明:`queue`(调度类别,内置 fs/diagnostic/command)、`maxConcurrency`(同一 Handler 舱壁)、`resources(input)`(资源键串行)、`dedupe:"payload"`(幂等 Handler 的唯一任务复用)。调度和资源锁按总线实例生效;插件实例由同一预设修订(generation)的会话共享,跨进程/跨代际不保证。资源键是调用方提供的字符串,不是 OS 文件锁。唯一任务只复用 succeeded 终态,失败/取消/unknown 不缓存;命中时返回快照带 `reused:true`,且不会再次执行 Handler。
|
|
132
|
+
|
|
133
|
+
0.12.1 起内置 host.read/host.glob/host.grep **不再声明** `dedupe:"payload"`:文件、权限或工作目录变化后缓存结果可能过期,扩展 Handler 如声明该能力需自行承担失效责任。同时 `waitReason` 区分 `bulkhead`(Handler 舱壁)与 `resource`(资源锁)。
|
|
134
|
+
|
|
131
135
|
内置 test、host.read、host.glob 与 host.grep 没有副作用;host.edit、host.write 与 host.pwsh 是非幂等写/命令 Handler,通过 `Handler.idempotent=false` 显式声明,其终态结果复用被 `MUTATING_REUSE_DENIED` 拒绝,运行中的重复附着仍只等待同一执行。0.8.0 新增宿主工具输入错误码 INVALID_GLOB_INPUT、INVALID_GREP_INPUT 与 INVALID_PWSH_INPUT;协议既有错误码不改名。host.pwsh 的后台结果只是宿主 Job ID,不是完成回执。当前没有 ctx.executionBus 服务、远端传输、跨插件动态注册或外部副作用对账契约。扩展有副作用 Handler 前必须显式声明幂等并另行设计授权与崩溃对账,不能仅依赖本版请求键。
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## 9. 错误词汇(0.12.1 补录)
|
|
139
|
+
|
|
140
|
+
错误码值即协议 v1 稳定词汇,不得重命名;完整定义见 `src/error-codes.ts`。
|
|
141
|
+
|
|
142
|
+
- 协议解析:`INVALID_OBJECT`、`UNKNOWN_FIELD`、`UNSUPPORTED_VERSION`、`INVALID_ID`、`UNSUPPORTED_TYPE`、`MESSAGE_TOO_LARGE`。
|
|
143
|
+
- 严格 JSON:`JSON_LIMIT`、`INVALID_JSON`、`CYCLIC_JSON`、`INVALID_JSON_ARRAY`、`INVALID_JSON_OBJECT`、`INVALID_JSON_KEY`、`INVALID_JSON_PROPERTY`。
|
|
144
|
+
- 总线调度与准入:`CLOSED`、`EXPECTED_REQUEST`、`INVALID_REQUEST`、`REQUEST_CONFLICT`、`MESSAGE_CONFLICT`、`MESSAGE_LIMIT`、`UNKNOWN_HANDLER`、`BACKPRESSURE`、`SESSION_BACKPRESSURE`、`WAITER_LIMIT`、`EXPECTED_CONTROL`、`INVALID_CONTROL`、`NOT_FOUND`、`UNTRUSTED_CONTEXT`、`OWNING_SESSION_REQUIRED`、`QUEUE_WAIT_TIMEOUT`、`RESULT_NOT_RETAINED`、`CANCELLED`、`CANCELLATION_UNCONFIRMED`、`HANDLER_FAILED`、`INVALID_ADMISSION_POLICY`、`INVALID_OPTIONS`、`DUPLICATE_HANDLER`、`MUTATING_REUSE_DENIED`、`INVALID_HANDLER_OPTIONS`、`INVALID_RESOURCES`。
|
|
145
|
+
- 宿主投影:`DUPLICATE_EXECUTION_HANDLE`、`INVALID_STATE_QUERY`、`HOST_READ_FAILED`、`HOST_TOOL_FAILED`、`HOST_JOB_UNAVAILABLE`。
|
|
146
|
+
- 预设派生:`PTC_PRESET_TOO_LARGE`、`PTC_PRESET_ALREADY_CONTAINS_BUS`、`PTC_PRESET_RELATIVE_BASE_UNSUPPORTED`、`PTC_PRESET_UNAVAILABLE`。
|
|
147
|
+
- 内置 Handler:`INVALID_TEST_INPUT`、`INVALID_READ_INPUT`、`INVALID_EDIT_INPUT`、`INVALID_WRITE_INPUT`、`INVALID_GLOB_INPUT`、`INVALID_GREP_INPUT`、`INVALID_PWSH_INPUT`、`READ_LIMIT`、`CONTROLLED_FAILURE`、`HOST_CAPABILITY_REQUIRED`。
|
|
148
|
+
|
package/docs/releases/0.11.1.md
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 萧何 / Xiao He 0.12.0
|
|
2
|
+
|
|
3
|
+
## 调度隔离、资源互斥与唯一任务(开源机制整合)
|
|
4
|
+
|
|
5
|
+
本版把一组调度/可靠性机制收敛进总线。参考来源、实际阅读范围与许可见 [整合说明](<../INTEGRATION.md>);本轮未复制第三方源码、未新增运行依赖。
|
|
6
|
+
|
|
7
|
+
- **Handler 调度声明**:新增可选 `queue`(类别)、`maxConcurrency`(舱壁)、`resources(input)`(资源键)、`dedupe:"payload"`(唯一任务);注册时校验(`INVALID_HANDLER_OPTIONS`/`INVALID_RESOURCES`)。
|
|
8
|
+
- **调度**:同会话内按类别优先级(fs < diagnostic < command < default)+ 等待老化(每 30 秒削平一级,最多两级)选择;跨会话仍按会话轮转;`maxConcurrency` 限制同一 Handler 并发。排队期限、取消、关闭与结果语义不变。
|
|
9
|
+
- **资源互斥**:`host.edit`/`host.write` 以 `file:<file_path>` 为资源键,在同一总线实例内串行执行;失败/取消/关闭释放资源;排队期限继续生效。**不是 OS 文件锁或跨进程保证。**
|
|
10
|
+
- **唯一任务**:`host.read`/`host.glob`/`host.grep` 声明 payload 去重;同会话同 handler+input 已有 `succeeded` 终态时,新 requestId 直接复用快照并返回 `reused:true`,不重复执行 Handler;失败/取消/unknown 不缓存。
|
|
11
|
+
- **诊断**:`execution_bus_state` 任务增加 `longRunning`(运行超过 30 秒);`waitReason` 增加 `resource`。
|
|
12
|
+
- **开发验证**:新增 8 项调度/唯一/资源回归、2 项 fast-check 属性回归与 2 项插件级 Handler 回归;全量 **122** 项测试通过。fast-check 仅 devDependency(MIT),不进入 tgz。
|
|
13
|
+
|
|
14
|
+
## 升级说明
|
|
15
|
+
|
|
16
|
+
需要 DSH 0.2.1-rc.2。wire 协议与工具调用形状不变;新增能力只在 core/Handler 层,宿主预设、工具名与既有错误码不变。默认调度对内置 Handler 生效(类别优先级与文件互斥),如需完全旧行为应自行评估 effect。不自动安装、重写日常配置或重启 DSH。
|
|
17
|
+
|
|
18
|
+
## 验证与限制
|
|
19
|
+
|
|
20
|
+
本地全量 122 项测试通过;属性测试固定种子,不是穷尽证明。调度与资源锁只在单个总线实例内;跨会话/跨进程/OS 级互斥、外部副作用对账、持久化与自动重试均未实现。真实模型、桌面 UI 与实际沙箱仍未完整验收。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 萧何 / Xiao He 0.12.1
|
|
2
|
+
|
|
3
|
+
## 0.12.0 行为风险热修
|
|
4
|
+
|
|
5
|
+
本版修正 0.12.0 默认 payload 去重导致的数据正确性问题,并处理同轮审查发现的高优先级调度/生命周期/文档缺陷。**wire 协议与工具调用形状不变。**
|
|
6
|
+
|
|
7
|
+
## 修正
|
|
8
|
+
|
|
9
|
+
- **关闭内置 host.* 的 payload 去重**:0.12.0 给 `host.read`/`host.glob`/`host.grep` 默认声明了 `dedupe:"payload"`。文件被编辑、会话切换工作目录或权限变化后,同输入的新 requestId 会静默返回旧内容(现场已复现:read 记 v1 → write 改 v2 → 再 read 仍返回 v1)。0.12.1 起内置 Handler 不再声明该能力,恢复“相同参数的新 requestId 是新操作”;核心 `Handler.dedupe` 能力保留,扩展 Handler 如声明须自行承担失效责任。
|
|
10
|
+
- **等待老化不封顶**:原 `boost=min(2,...)` 削不平 default(3) 与 fs(0) 的差距,持续 fs 负载下 default 类可饥饿/必超时;改为按等待时间持续提升、最低到 0,同分按 FIFO。
|
|
11
|
+
- **唯一复用记录计入保留字节与时间戳**:`syntheticUnique` 深拷贝结果但 `resultBytes=0`,且无 startedAt/finishedAt 导致 `queueMs` 随保留期增长;现按结果字节计入、写入时间戳,sweep/close 正常递减。
|
|
12
|
+
- **waitReason 区分舱壁与资源**:新增 `bulkhead`,不再把 Handler.maxConcurrency 等待误报为 `resource`。
|
|
13
|
+
- **注册时校验并快照 Handler 选项**:校验 run/validate 为函数、idempotent 严格布尔、queue/dedupe 合法;冻结调度字段并拒绝 `__proto__` 等类名,避免运行期篡改、NaN 优先级和永不调度;scheduler 策略拒绝 accessor 与原型污染键,数值只读取一次后校验。
|
|
14
|
+
- **关闭重入**:validate/resources 同步执行中触发 `close()` 后不再向已关闭总线插入永不结算的记录,改为拒绝准入(CLOSED)。
|
|
15
|
+
- **消息身份改存 SHA-256 摘要**:同一 requestId 多条消息不再保存完整 canonical 文本,避免消息额度内无字节上限的内存放大;冲突检测语义等价。
|
|
16
|
+
- **文档修正**:protocol 恢复“新 requestId 新操作”并补错误词汇表;validation 更新 PTC 预算参数(8s/12s)、资源互斥跨会话范围(同一预设修订共享,跨进程/跨代际不保证)、修正 0.12.0 发布说明中的范围表述;0.11.1 标注“未发布”;OPTIMIZATION 标注历史并链接当前实现;INTEGRATION 标题与老化说明同步。
|
|
17
|
+
|
|
18
|
+
## 验证
|
|
19
|
+
|
|
20
|
+
- 新增 6 项回归:default 类老化不饥饿、bulkhead/resource 等待原因、注册负例、scheduler accessor/原型污染、关闭重入、唯一复用字节/时间戳、资源锁失败/取消释放。
|
|
21
|
+
- 本地全量测试通过(122 + 6 = 128 项;以 `npm run check` 实际输出为准)。
|
|
22
|
+
- 未复制第三方源码、未新增运行依赖;fast-check 仍仅为 devDependency。
|
|
23
|
+
|
|
24
|
+
## 升级说明
|
|
25
|
+
|
|
26
|
+
需要 DSH 0.2.1-rc.2。工具名、wire 协议与既有错误码不变;行为变更仅影响内置 host.* 的 payload 去重(默认关闭)。旧版创建的总线实例/记录不迁移;重载或升级会创建新的预设修订,旧 requestId 不可取回。
|
|
27
|
+
|
|
28
|
+
## 验证与限制
|
|
29
|
+
|
|
30
|
+
调度与资源锁按预设修订共享总线实例,跨进程、跨代际、OS 级文件锁均不保证;资源键别名(相对/绝对、大小写)可能绕过互斥。真实模型、桌面 UI 与实际沙箱仍未完整验收。
|
package/docs/validation.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 验证范围与使用限制
|
|
2
2
|
|
|
3
|
-
本文说明当前版本的证据边界,不是生产环境认证。当前能力基线为 **v0.
|
|
3
|
+
本文说明当前版本的证据边界,不是生产环境认证。当前能力基线为 **v0.12.1**,在 0.5.0 的会话准入/排队、0.7.0 的宿主工具转发、0.8.0 的搜索/命令 Handler、0.9.0 的预算分离等待与 0.10.0 的采用面改造上,收紧为收益导向触发并加入简化调用、复用标记与本地采用率统计;0.11.1 补充跨步骤取回姿势,0.11.3 补记指引级 A/B 证据,0.12.0 加入调度类别/舱壁/资源互斥/唯一任务与属性测试,0.12.1 关闭内置 host.* 的 payload 去重并修正老化/字节计数/等待原因/注册校验/关闭重入问题;预设沿用 v0.3.0 的宿主 PTC 声明派生方式。验证环境为 Windows、Node.js 24.21.0,宿主组件锁定 DSH 0.2.1-rc.2、Cordis 4.0.4。
|
|
4
4
|
|
|
5
5
|
## 已验证什么?
|
|
6
6
|
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
| 总线核心 | 参数、队列、并发、重复请求、冲突、取消竞争、结果保存、TTL、监听与注销 | 程序内部约束及已覆盖异常路径可重复检查 |
|
|
10
10
|
| 真实宿主组件 | Cordis、ToolRuntime、LocalJobs、LocalFS、文件工具;host.glob/host.grep/host.pwsh 的转发测试使用宿主工具桩 | 请求经过真实工具管线;权限拒绝、Job 进度、共享任务、参数校验、幂等分流与清理逻辑有效;Agent 身份与工具实现为夹具,桩不证明真实搜索/命令工具实现 |
|
|
11
11
|
| 真实会话循环 | AgentLoop、AgentRegistry、SessionStore;脚本模型适配器 | 内存会话中原工具结果只提交一次,完成后再进入下一次适配器调用 |
|
|
12
|
-
| PTC 预算分离等待 | 真实 Node PTC `run_code` 以
|
|
12
|
+
| PTC 预算分离等待 | 真实 Node PTC `run_code` 以 8 秒预算等待 12 秒总线执行:预算到期后执行继续、宿主 Job 保留、同 requestId 取回结果;外部中断仍取消执行 | 在 DSH 0.2.1-rc.2 实际中断原因下成立;固定字符串不是跨版本契约 |
|
|
13
13
|
| 简化调用与复用标记 | 简化请求/控制、完整 v1 消息兼容、终态与运行中复用返回 `reused:true`、非法形状拒绝 | 组件级测试确认;不改变 wire 语义与副作用边界 |
|
|
14
14
|
| 采用策略 A/B(指引级) | 同一版本 0.11.2、同一模型、5 类任务各 5 个全新子会话:可选触发 vs 总线优先指引,统计总线调用/复用/耗时 | 指出可选触发更优(总线优先把一次性长命令也包装、并有约 19s 排队延迟);非预设级、并发单机小样本,非统计显著 |
|
|
15
|
+
| 调度隔离与资源互斥 | Handler queue/maxConcurrency/resources、类别优先级 + 等待老化(不封顶)、资源等待期限、waitReason(含 bulkhead)/longRunning、唯一任务只复用 succeeded | 组件级回归确认;按预设修订共享总线实例,跨进程/跨代际不保证,不是 OS 锁 |
|
|
16
|
+
| 0.12.1 热修 | 内置 host.* 关闭 payload 去重、老化不封顶、唯一复用字节/时间戳、注册选项快照校验、validate/resources 内 close 重入、消息身份改摘要 | 组件级回归确认;不改变 wire 协议与工具调用形状 |
|
|
17
|
+
| 属性测试 | fast-check 随机提交/重放与容量压力不变量(固定种子) | 开发依赖(MIT);随机覆盖不是穷尽验证,种子固定保证可复现 |
|
|
15
18
|
| 预设隔离 | AgentPresetRegistry 与 Loader | 派生预设可激活;源 ptc 声明不变、普通 Agent 不可见总线;原 write/edit/Job 工具保留,宿主前置策略拒绝仍有效 |
|
|
16
19
|
| 故障注入 | 克隆失败、观察者拒绝、Job 等待/清理异常、先行移除与卸载交错 | 指定故障下的回归约束;不证明所有故障组合都已覆盖 |
|
|
17
20
|
| 候选包 | 严格文件白名单、临时目录生产依赖安装、入口注册 | 包可安装和加载;安装验证使用简化宿主夹具,不是桌面插件管理器验收 |
|
|
@@ -51,6 +54,7 @@ eb95b4e51675f5f1821275e0812ef4dd4984b1aba45a52fbc6bab3518bbf6f38
|
|
|
51
54
|
| 从真实 tgz 经宿主插件管理器加载预设、卸载与回滚 | 未完成完整桌面流程;源码工作区的 Loader 测试不能替代 |
|
|
52
55
|
| 真实 glob/grep/pwsh 工具实现 | 未在测试环境启用:真实搜索工具依赖打包 ripgrep 与 subprocess 服务,真实 pwsh 依赖 shell/sandbox/审批组合;本版只验证按 handler 名转发、输入校验、幂等分流与失败固定码 |
|
|
53
56
|
| 跨版本 PTC 中断原因 | 分离等待只识别 DSH 0.2.1-rc.2 实际使用的固定中断原因字符串;宿主升级后未知原因保守按取消处理,需复核 |
|
|
57
|
+
| 跨会话/跨进程资源互斥 | 同一预设修订(generation)的会话共享总线实例,资源锁/舱壁/容量跨会话生效;不提供 OS 级文件锁,跨进程、跨代际不保证;同一文件的相对/绝对、大小写等别名写法可能绕过文件互斥 |
|
|
54
58
|
| 模式采用率与效果 | 已完成指引级 A/B(可选触发 vs 总线优先),但非预设级、并发单机小样本,不具统计显著性;轮次/耗时/成功率的大样本对照仍未完成,不能由 schema 或单元测试推断 |
|
|
55
59
|
| 磁盘会话结果提交及重启恢复原调用 | 未验收磁盘提交;本插件不支持跨重启恢复 |
|
|
56
60
|
| 减少真实模型轮次、状态查询或总耗时的对照 | 未验证,不给提升比例 |
|
|
@@ -59,7 +63,7 @@ eb95b4e51675f5f1821275e0812ef4dd4984b1aba45a52fbc6bab3518bbf6f38
|
|
|
59
63
|
## 需要知道的运行约束
|
|
60
64
|
|
|
61
65
|
1. 受理、队列、结果及交付标记均在内存。进程关闭会丢失,宿主会话日志不等于插件恢复执行权。
|
|
62
|
-
2.
|
|
66
|
+
2. 同一预设修订(generation)的会话共享总线容量,已受理任务会话轮转、会话内 FIFO;结果访问按会话隔离,不是独立进程或资源安全隔离,也不保证入队名额公平。重载/升级会创建新的预设修订,旧实例状态、容量与资源锁不延续。
|
|
63
67
|
3. 取消任一正在等待的重复调用会请求取消整个逻辑执行,不是仅停止该等待者。
|
|
64
68
|
4. 已终态且未过期的请求可直接复用结果,不申请新 Job;仍须通过身份、冲突及消息额度检查。
|
|
65
69
|
5. 等待服务异常后先排空原执行,再有界检查宿主记录;服务持续不可用时记录可能留到所属生命周期清理。不合作 Handler 仍可能阻碍关闭。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-execution-bus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"@deepseek-ai/dsh-tool-jobs": "0.2.1-rc.2",
|
|
75
75
|
"@types/js-yaml": "4.0.9",
|
|
76
76
|
"@types/node": "^22",
|
|
77
|
+
"fast-check": "^4.10.2",
|
|
77
78
|
"prettier": "3.6.2",
|
|
78
79
|
"typescript": "~5.9.3",
|
|
79
80
|
"yaml": "2.9.1"
|