@feng0531/dsh-execution-bus 0.5.0 → 0.6.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 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.5.0**
7
+ DSH 独立插件:`@feng0531/dsh-execution-bus` · 当前版本 **0.6.0**
8
8
 
9
9
  > 萧何模式从宿主已声明的 PTC 组合派生,保留日常工具,再叠加可选执行总线。已验证隔离的真实 Node PTC → 原 read + 总线调用,以及脚本模型内存会话;真实模型、桌面界面与实际沙箱仍未验收。**不支持重启后恢复任务。**
10
10
 
@@ -83,13 +83,22 @@ flowchart TD
83
83
 
84
84
  | Handler 名称 | 用途 | 输入 |
85
85
  |---|---|---|
86
- | `test` | 检查延迟、进度、成功、失败和取消 | `delayMs`:0–30000;`fail`:布尔值 |
86
+ | `test`(诊断,非业务) | 仅在明确诊断时检查延迟、进度、成功、失败和取消 | `delayMs`:0–30000;`fail`:布尔值 |
87
87
  | `host.read` | 经由 DSH 现有 `read` 工具读取文本 | `file_path`;可选 `offset` 和 `limit`,最多 100 行 |
88
88
 
89
89
  `host.read` 在出队时重新进入宿主工具管线,沿用宿主当前权限、会话工作目录及输出限制。插件不自行启动命令、不提供任意远程执行服务。它也不会强制将现有工具改成总线调用。
90
90
 
91
91
  ## 选择“萧何”预设
92
92
 
93
+ 正式模式入口为 `/mode`。新生成预设在原 PTC 组合末尾追加以下行,它不是整份预设:
94
+
95
+ ```yaml
96
+ - id: xiao-he-execution-bus
97
+ name: '@feng0531/dsh-execution-bus/mode'
98
+ ```
99
+
100
+ 旧 /test-mode 保留为兼容别名,两个入口指向同一正式模块,不表示测试专用模式,也不要同时配置新旧两行。必须先安装 0.5.1 或以上才能使用 /mode;旧包没有此导出。旧预设不会自动重写,旧入口继续有效。历史预设 ID execution-bus-test 和包级配置行 ID 不改,避免已有会话失联。
101
+
93
102
  包内提供标准 DSH bundle patch(预设组合补丁)。宿主正常加载后会收到“萧何 / Xiao He”预设定义,由既有设置 → Agent 预设 → **自定义**列表展示。建议选择它新建专门会话,不改默认选择或正在使用的普通会话。已有会话的切换受宿主空白会话检查约束。**实际桌面卡片展示尚未验收**,不能把注册表测试当作界面已通过。
94
103
 
95
104
  **0.3.0 起不再是只读验证模式。** 加载时读取宿主 `ptc` 预设的组合声明快照,保留原工具、平台表达式、禁用项和配置,只追加萧何总线与说明。不再强制 native 呈现或添加只读白名单;终端、写入、搜索、技能等能力是否可用,取决于原 PTC 配置和宿主服务。其他预设和默认模式不被修改,原 PTC 会话看不到新增总线。
@@ -115,7 +124,7 @@ flowchart TD
115
124
  2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
116
125
 
117
126
  ```powershell
118
- npm install --ignore-scripts @feng0531/dsh-execution-bus@0.5.0
127
+ npm install --ignore-scripts @feng0531/dsh-execution-bus@0.6.0
119
128
  ```
120
129
 
121
130
  这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
@@ -254,9 +263,11 @@ npm run test:install # 临时目录安装,不操作日常 DSH
254
263
 
255
264
  ## 进一步阅读
256
265
 
266
+ - [产品结构与兼容契约](<docs/PRODUCT.md>):正式入口、模块职责及维护验收边界。
267
+
257
268
  - [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
258
269
  - [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
259
- - [验证记录](<docs/validation.md>)、[0.5.0 发布说明](<docs/releases/0.5.0.md>):已验证与未验证范围。
270
+ - [验证记录](<docs/validation.md>)、[0.6.0 发布说明](<docs/releases/0.6.0.md>):已验证与未验证范围。
260
271
  - [同类项目源码整合](<docs/INTEGRATION.md>):固定来源、采用范围、许可与验证边界。
261
272
  - [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
262
273
 
package/dist/bus.d.ts CHANGED
@@ -1,50 +1,11 @@
1
+ import { object, parse } from "./protocol.js";
2
+ import type { Capability, Event, Handler, Message, Owner, RecordView, State } from "./protocol.js";
3
+ export { object, parse };
4
+ export type { Capability, Event, Handler, Message, Owner, RecordView, State };
1
5
  export interface AdmissionPolicy {
2
6
  sessionQueueLimit?: number;
3
7
  maxQueueWaitMs?: number;
4
8
  }
5
- export type State = "queued" | "running" | "succeeded" | "failed" | "cancelled" | "unknown";
6
- export type Owner = {
7
- sessionId: string;
8
- callId: string;
9
- };
10
- export type Message = {
11
- protocolVersion: 1;
12
- messageId: string;
13
- requestId: string;
14
- type: "request" | "status" | "result" | "cancel";
15
- payload: Record<string, unknown>;
16
- };
17
- export type Event = {
18
- protocolVersion: 1;
19
- messageId: string;
20
- requestId: string;
21
- correlationId: string;
22
- sequence: number;
23
- type: string;
24
- payload: Record<string, unknown>;
25
- };
26
- export type Capability = (input: Record<string, unknown>, signal: AbortSignal) => Promise<unknown>;
27
- export interface Handler {
28
- validate(input: Record<string, unknown>): void;
29
- run(input: Record<string, unknown>, ctx: {
30
- signal: AbortSignal;
31
- progress: (sequence: number, percent: number) => void;
32
- requestId: string;
33
- invoke?: Capability;
34
- }): Promise<unknown>;
35
- }
36
- export interface RecordView {
37
- requestId: string;
38
- state: State;
39
- sequence: number;
40
- cancelRequested: boolean;
41
- delivery: "pending" | "returned";
42
- result?: unknown;
43
- error?: string;
44
- progress?: number;
45
- }
46
- export declare function object(v: unknown): Record<string, unknown>;
47
- export declare function parse(raw: unknown): Message;
48
9
  /** Volatile single-process bus; no acceptance durability or automatic execution retry. */
49
10
  export declare class ExecutionBus {
50
11
  readonly options: {
@@ -108,6 +69,14 @@ export declare class ExecutionBus {
108
69
  detach: () => void;
109
70
  returned: () => void;
110
71
  };
72
+ /** 校验并拆解 request 的 payload,返回 handler 名、入参与原文。 */
73
+ private requestParts;
74
+ /** 同一 requestId 的重复提交:校验一致性后复用执行或结果。 */
75
+ private reuseInvocation;
76
+ /** 受理全新请求:校验 Handler、检查容量、构建记录并入队。 */
77
+ private admitInvocation;
78
+ /** 若启用了排队期限,登记到期结算为 QUEUE_WAIT_TIMEOUT 的计时器。 */
79
+ private scheduleQueueDeadline;
111
80
  private attachment;
112
81
  command(owner: Owner, raw: unknown): RecordView;
113
82
  private removeQueued;
@@ -140,6 +109,8 @@ export declare class ExecutionBus {
140
109
  };
141
110
  private cancel;
142
111
  private finish;
112
+ /** 按会话轮转挑选下一个出队项:各会话内部 FIFO,跨会话公平。 */
113
+ private nextQueuedIndex;
143
114
  private pump;
144
115
  private run;
145
116
  close(): Promise<void>;
package/dist/bus.js CHANGED
@@ -1,59 +1,17 @@
1
- import { randomUUID, createHash } from "node:crypto";
2
- import { assertJson } from "./json.js";
3
- import { Diagnostics } from "./diagnostics.js";
1
+ import { createHash, randomUUID } from "node:crypto";
4
2
  import { performance } from "node:perf_hooks";
5
- export function object(v) {
6
- if (!v || typeof v !== "object" || Array.isArray(v))
7
- throw Error("INVALID_OBJECT");
8
- return v;
9
- }
10
- function canonical(v) {
11
- if (v === null || typeof v !== "object")
12
- return JSON.stringify(v);
13
- if (Array.isArray(v))
14
- return "[" + v.map(canonical).join(",") + "]";
15
- return ("{" +
16
- Object.keys(v)
17
- .sort()
18
- .map((k) => JSON.stringify(k) +
19
- ":" +
20
- canonical(v[k]))
21
- .join(",") +
22
- "}");
23
- }
24
- export function parse(raw) {
25
- assertJson(raw);
26
- const m = object(raw);
27
- if (Object.keys(m).some((k) => ![
28
- "protocolVersion",
29
- "messageId",
30
- "requestId",
31
- "type",
32
- "payload",
33
- ].includes(k)))
34
- throw Error("UNKNOWN_FIELD");
35
- if (m.protocolVersion !== 1)
36
- throw Error("UNSUPPORTED_VERSION");
37
- for (const k of ["messageId", "requestId"])
38
- if (typeof m[k] !== "string" ||
39
- !/^[a-zA-Z0-9_.:-]{1,128}$/.test(m[k]))
40
- throw Error("INVALID_ID");
41
- if (!["request", "status", "result", "cancel"].includes(m.type))
42
- throw Error("UNSUPPORTED_TYPE");
43
- object(m.payload);
44
- if (Buffer.byteLength(JSON.stringify(m)) > 16384)
45
- throw Error("MESSAGE_TOO_LARGE");
46
- return structuredClone(m);
47
- }
48
- function freezeJson(value) {
49
- if (value && typeof value === "object") {
50
- for (const child of Object.values(value))
51
- freezeJson(child);
52
- Object.freeze(value);
53
- }
54
- return value;
3
+ import { Diagnostics } from "./diagnostics.js";
4
+ import { ERRORS } from "./error-codes.js";
5
+ import { assertJson } from "./json.js";
6
+ import { LIMITS } from "./limits.js";
7
+ import { CORE_DEFAULTS } from "./product.js";
8
+ import { canonical, freezeJson, object, parse } from "./protocol.js";
9
+ export { object, parse };
10
+ const isTerminal = (s) => !["queued", "running"].includes(s);
11
+ /** 对 payload 做规范排序后的 SHA-256 摘要,仅用于同 requestId 的冲突比较。 */
12
+ function hashPayload(payload) {
13
+ return createHash("sha256").update(canonical(payload)).digest("hex");
55
14
  }
56
- const terminal = (s) => !["queued", "running"].includes(s);
57
15
  /** Volatile single-process bus; no acceptance durability or automatic execution retry. */
58
16
  export class ExecutionBus {
59
17
  options;
@@ -78,13 +36,7 @@ export class ExecutionBus {
78
36
  }
79
37
  closing;
80
38
  shutdownPhase = "open";
81
- constructor(options = {
82
- concurrency: 2,
83
- queueLimit: 32,
84
- recordLimit: 256,
85
- resultBytes: 65536,
86
- retentionMs: 3600000,
87
- }, admission = {}) {
39
+ constructor(options = CORE_DEFAULTS, admission = {}) {
88
40
  this.options = options;
89
41
  this.admission = admission;
90
42
  const allowed = ["sessionQueueLimit", "maxQueueWaitMs"];
@@ -92,7 +44,7 @@ export class ExecutionBus {
92
44
  typeof admission !== "object" ||
93
45
  Array.isArray(admission) ||
94
46
  Reflect.ownKeys(admission).some((key) => typeof key !== "string" || !allowed.includes(key)))
95
- throw Error("INVALID_ADMISSION_POLICY");
47
+ throw Error(ERRORS.INVALID_ADMISSION_POLICY);
96
48
  for (const key of Reflect.ownKeys(admission)) {
97
49
  const descriptor = Object.getOwnPropertyDescriptor(admission, key);
98
50
  if (!("value" in descriptor) ||
@@ -100,7 +52,7 @@ export class ExecutionBus {
100
52
  !Number.isSafeInteger(descriptor.value) ||
101
53
  descriptor.value < 1 ||
102
54
  descriptor.value > 2147483647)
103
- throw Error("INVALID_ADMISSION_POLICY");
55
+ throw Error(ERRORS.INVALID_ADMISSION_POLICY);
104
56
  }
105
57
  this.admission = Object.freeze({ ...admission });
106
58
  const keys = [
@@ -118,15 +70,15 @@ export class ExecutionBus {
118
70
  const descriptor = Object.getOwnPropertyDescriptor(options, key);
119
71
  return (!descriptor || !("value" in descriptor) || !descriptor.enumerable);
120
72
  }))
121
- throw Error("INVALID_OPTIONS");
73
+ throw Error(ERRORS.INVALID_OPTIONS);
122
74
  this.options = Object.freeze({ ...options });
123
75
  for (const v of Object.values(this.options))
124
76
  if (!Number.isSafeInteger(v) || v < 1)
125
- throw Error("INVALID_OPTIONS");
77
+ throw Error(ERRORS.INVALID_OPTIONS);
126
78
  }
127
79
  register(name, handler) {
128
80
  if (this.handlers.has(name))
129
- throw Error("DUPLICATE_HANDLER");
81
+ throw Error(ERRORS.DUPLICATE_HANDLER);
130
82
  const registration = { handler, token: Symbol(name) };
131
83
  this.handlers.set(name, registration);
132
84
  let disposed = false;
@@ -140,7 +92,7 @@ export class ExecutionBus {
140
92
  }
141
93
  key(o, id) {
142
94
  if (!o.sessionId || !o.callId)
143
- throw Error("UNTRUSTED_CONTEXT");
95
+ throw Error(ERRORS.UNTRUSTED_CONTEXT);
144
96
  return JSON.stringify([o.sessionId, id]);
145
97
  }
146
98
  emit(e, type, payload, correlationId = "") {
@@ -154,8 +106,7 @@ export class ExecutionBus {
154
106
  type,
155
107
  payload,
156
108
  });
157
- // Snapshot and enqueue before invoking user code: nested cancellation cannot
158
- // overtake the current event for later observers. Removed listeners are skipped.
109
+ // 在调用用户代码前快照并排队:嵌套取消无法抢到当前事件。
159
110
  e.notifications.push({ event, listeners: [...e.listeners] });
160
111
  if (e.dispatching)
161
112
  return;
@@ -175,7 +126,7 @@ export class ExecutionBus {
175
126
  }
176
127
  finally {
177
128
  e.dispatching = false;
178
- if (terminal(e.view.state))
129
+ if (isTerminal(e.view.state))
179
130
  e.listeners.clear();
180
131
  }
181
132
  }
@@ -185,68 +136,87 @@ export class ExecutionBus {
185
136
  get(o, id) {
186
137
  const e = this.entries.get(this.key(o, id));
187
138
  if (!e)
188
- throw Error("NOT_FOUND");
139
+ throw Error(ERRORS.NOT_FOUND);
189
140
  return e;
190
141
  }
191
142
  sweep(now = Date.now()) {
192
143
  for (const [k, e] of this.entries)
193
- if (terminal(e.view.state) &&
194
- now - e.created > this.options.retentionMs) {
144
+ if (isTerminal(e.view.state) && now - e.created > this.options.retentionMs) {
195
145
  this.retainedResultBytes -= e.resultBytes;
196
146
  this.entries.delete(k);
197
147
  }
198
148
  }
199
149
  submit(owner, raw, onEvent, invoke) {
200
150
  if (this.closed)
201
- throw Error("CLOSED");
151
+ throw Error(ERRORS.CLOSED);
202
152
  const m = parse(raw);
203
153
  if (m.type !== "request")
204
- throw Error("EXPECTED_REQUEST");
154
+ throw Error(ERRORS.EXPECTED_REQUEST);
155
+ const parts = this.requestParts(m);
156
+ const key = this.key(owner, m.requestId);
157
+ const fingerprint = hashPayload(parts.payload);
158
+ this.sweep();
159
+ const existing = this.entries.get(key);
160
+ return existing
161
+ ? this.reuseInvocation(existing, m, fingerprint, onEvent)
162
+ : this.admitInvocation(owner, m, parts, key, fingerprint, invoke, onEvent);
163
+ }
164
+ /** 校验并拆解 request 的 payload,返回 handler 名、入参与原文。 */
165
+ requestParts(m) {
205
166
  const p = m.payload;
206
167
  if (Object.keys(p).some((k) => !["handler", "input"].includes(k)) ||
207
168
  typeof p.handler !== "string")
208
- throw Error("INVALID_REQUEST");
209
- const input = object(p.input);
210
- const key = this.key(owner, m.requestId), fp = createHash("sha256").update(canonical(p)).digest("hex");
211
- this.sweep();
212
- let e = this.entries.get(key);
213
- if (e) {
214
- if (e.fingerprint !== fp)
215
- throw Error("REQUEST_CONFLICT");
216
- const previous = e.messages.get(m.messageId);
217
- if (previous && previous !== canonical(m))
218
- throw Error("MESSAGE_CONFLICT");
219
- if (!previous && e.messages.size >= 128)
220
- throw Error("MESSAGE_LIMIT");
221
- // Acquire the attachment before committing its message identity. Rejection is side-effect free.
222
- const attached = this.attachment(e, onEvent, m.messageId);
223
- if (!previous)
224
- e.messages.set(m.messageId, canonical(m));
225
- this.telemetry.record(e.trace, "reused");
226
- return attached;
227
- }
228
- const handler = this.handlers.get(p.handler)?.handler;
169
+ throw Error(ERRORS.INVALID_REQUEST);
170
+ return {
171
+ handler: p.handler,
172
+ input: object(p.input),
173
+ payload: p,
174
+ };
175
+ }
176
+ /** 同一 requestId 的重复提交:校验一致性后复用执行或结果。 */
177
+ reuseInvocation(e, m, fingerprint, onEvent) {
178
+ if (e.fingerprint !== fingerprint)
179
+ throw Error(ERRORS.REQUEST_CONFLICT);
180
+ // 仅显式声明非幂等(idempotent:false)的 handler,终态结果才不可复用:
181
+ // 返回"已执行"会掩盖副作用状态。运行中的重复 attach 仍放行,不会二次执行。
182
+ if (e.handler.idempotent === false && isTerminal(e.view.state))
183
+ throw Error(ERRORS.MUTATING_REUSE_DENIED);
184
+ const previous = e.messages.get(m.messageId);
185
+ if (previous && previous !== canonical(m))
186
+ throw Error(ERRORS.MESSAGE_CONFLICT);
187
+ if (!previous && e.messages.size >= LIMITS.messageAccountLimit)
188
+ throw Error(ERRORS.MESSAGE_LIMIT);
189
+ // 先取得附着,再提交消息身份:附着被拒(WAITER_LIMIT)时无副作用。
190
+ const attached = this.attachment(e, onEvent, m.messageId);
191
+ if (!previous)
192
+ e.messages.set(m.messageId, canonical(m));
193
+ this.telemetry.record(e.trace, "reused");
194
+ return attached;
195
+ }
196
+ /** 受理全新请求:校验 Handler、检查容量、构建记录并入队。 */
197
+ admitInvocation(owner, m, parts, key, fingerprint, invoke, onEvent) {
198
+ const handler = this.handlers.get(parts.handler)?.handler;
229
199
  if (!handler)
230
- throw Error("UNKNOWN_HANDLER");
231
- handler.validate(input);
200
+ throw Error(ERRORS.UNKNOWN_HANDLER);
201
+ handler.validate(parts.input);
232
202
  if (this.entries.size >= this.options.recordLimit ||
233
203
  this.queue.length >= this.options.queueLimit)
234
- throw Error("BACKPRESSURE");
204
+ throw Error(ERRORS.BACKPRESSURE);
235
205
  if (this.admission.sessionQueueLimit !== undefined &&
236
206
  this.queue.filter((entry) => entry.owner === owner.sessionId).length >=
237
207
  this.admission.sessionQueueLimit)
238
- throw Error("SESSION_BACKPRESSURE");
208
+ throw Error(ERRORS.SESSION_BACKPRESSURE);
239
209
  let resolve;
240
210
  const done = new Promise((r) => (resolve = r));
241
- e = {
211
+ const e = {
242
212
  trace: this.telemetry.begin(),
243
213
  resultBytes: 0,
244
214
  invoke,
245
215
  correlationId: m.messageId,
246
216
  owner: owner.sessionId,
247
- fingerprint: fp,
217
+ fingerprint,
248
218
  handler,
249
- input: structuredClone(input),
219
+ input: structuredClone(parts.input),
250
220
  view: {
251
221
  requestId: m.requestId,
252
222
  state: "queued",
@@ -268,33 +238,36 @@ export class ExecutionBus {
268
238
  this.telemetry.record(e.trace, "accepted");
269
239
  const attached = this.attachment(e, onEvent, m.messageId);
270
240
  this.queue.push(e);
271
- if (this.admission.maxQueueWaitMs !== undefined) {
272
- e.deadline = performance.now() + this.admission.maxQueueWaitMs;
273
- const entry = e;
274
- const expire = () => {
275
- if (entry.view.state !== "queued")
276
- return;
277
- const remaining = entry.deadline - performance.now();
278
- if (remaining > 0) {
279
- entry.queueTimer = setTimeout(expire, Math.ceil(remaining));
280
- return;
281
- }
282
- this.removeQueued(entry);
283
- this.finish(entry, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
284
- };
285
- e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
286
- }
241
+ this.scheduleQueueDeadline(e);
287
242
  if (!this.readyOwners.includes(e.owner))
288
243
  this.readyOwners.push(e.owner);
289
244
  queueMicrotask(() => this.pump());
290
245
  return attached;
291
246
  }
247
+ /** 若启用了排队期限,登记到期结算为 QUEUE_WAIT_TIMEOUT 的计时器。 */
248
+ scheduleQueueDeadline(e) {
249
+ if (this.admission.maxQueueWaitMs === undefined)
250
+ return;
251
+ e.deadline = performance.now() + this.admission.maxQueueWaitMs;
252
+ const expire = () => {
253
+ if (e.view.state !== "queued")
254
+ return;
255
+ const remaining = e.deadline - performance.now();
256
+ if (remaining > 0) {
257
+ e.queueTimer = setTimeout(expire, Math.ceil(remaining));
258
+ return;
259
+ }
260
+ this.removeQueued(e);
261
+ this.finish(e, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
262
+ };
263
+ e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
264
+ }
292
265
  attachment(e, onEvent, id) {
293
- // Each attachment owns its registration, even when callers reuse one function.
266
+ // 每次附着独立占配额,即使调用方复用同一个回调。
294
267
  const listener = onEvent ? (event) => onEvent(event) : undefined;
295
- if (listener && !terminal(e.view.state)) {
296
- if (e.listeners.size >= 32)
297
- throw Error("WAITER_LIMIT");
268
+ if (listener && !isTerminal(e.view.state)) {
269
+ if (e.listeners.size >= LIMITS.waiterLimit)
270
+ throw Error(ERRORS.WAITER_LIMIT);
298
271
  e.listeners.add(listener);
299
272
  }
300
273
  const accepted = {
@@ -324,20 +297,20 @@ export class ExecutionBus {
324
297
  command(owner, raw) {
325
298
  const m = parse(raw);
326
299
  if (m.type === "request")
327
- throw Error("EXPECTED_CONTROL");
300
+ throw Error(ERRORS.EXPECTED_CONTROL);
328
301
  if (Object.keys(m.payload).length)
329
- throw Error("INVALID_CONTROL");
302
+ throw Error(ERRORS.INVALID_CONTROL);
330
303
  this.sweep();
331
304
  const e = this.get(owner, m.requestId);
332
305
  const previous = e.messages.get(m.messageId), fingerprint = canonical(m);
333
306
  if (previous && previous !== fingerprint)
334
- throw Error("MESSAGE_CONFLICT");
307
+ throw Error(ERRORS.MESSAGE_CONFLICT);
335
308
  if (!previous) {
336
- // Keep one emergency control slot: read traffic must never prevent cancellation.
337
- if (e.messages.size >= 128) {
309
+ // 保留一个应急控制槽:读流量绝不能阻塞取消。
310
+ if (e.messages.size >= LIMITS.messageAccountLimit) {
338
311
  if (m.type !== "cancel")
339
- throw Error("MESSAGE_LIMIT");
340
- if (e.view.cancelRequested || terminal(e.view.state))
312
+ throw Error(ERRORS.MESSAGE_LIMIT);
313
+ if (e.view.cancelRequested || isTerminal(e.view.state))
341
314
  return this.view(e);
342
315
  }
343
316
  e.messages.set(m.messageId, fingerprint);
@@ -354,14 +327,14 @@ export class ExecutionBus {
354
327
  /** Trusted host owner required. Never exposes other sessions or business payloads. */
355
328
  inspect(owner) {
356
329
  if (!owner || typeof owner.sessionId !== "string" || !owner.sessionId)
357
- throw Error("OWNING_SESSION_REQUIRED");
330
+ throw Error(ERRORS.OWNING_SESSION_REQUIRED);
358
331
  this.sweep();
359
332
  const now = performance.now();
360
333
  const records = [...this.entries.values()].filter((e) => e.owner === owner.sessionId);
361
- // Active work must not disappear behind newer terminal history.
362
- const active = records.filter((e) => !terminal(e.view.state));
363
- const history = records.filter((e) => terminal(e.view.state)).reverse();
364
- const selected = [...active, ...history].slice(0, 50);
334
+ // 活跃任务不得被更新的终态历史挤到列表之外。
335
+ const active = records.filter((e) => !isTerminal(e.view.state));
336
+ const history = records.filter((e) => isTerminal(e.view.state)).reverse();
337
+ const selected = [...active, ...history].slice(0, LIMITS.inspectTaskLimit);
365
338
  const tasks = selected.map((e) => ({
366
339
  requestId: e.view.requestId,
367
340
  state: e.view.state,
@@ -374,7 +347,7 @@ export class ExecutionBus {
374
347
  ? "concurrency"
375
348
  : "dispatch"
376
349
  : null,
377
- phase: terminal(e.view.state)
350
+ phase: isTerminal(e.view.state)
378
351
  ? e.view.delivery === "returned"
379
352
  ? "returned"
380
353
  : "terminal"
@@ -395,14 +368,14 @@ export class ExecutionBus {
395
368
  counts: {
396
369
  queued: records.filter((e) => e.view.state === "queued").length,
397
370
  running: records.filter((e) => e.view.state === "running").length,
398
- terminal: records.filter((e) => terminal(e.view.state)).length,
371
+ terminal: records.filter((e) => isTerminal(e.view.state)).length,
399
372
  },
400
373
  tasks,
401
- truncated: records.length > 50,
374
+ truncated: records.length > LIMITS.inspectTaskLimit,
402
375
  });
403
376
  }
404
377
  cancel(e) {
405
- if (terminal(e.view.state) || e.view.cancelRequested)
378
+ if (isTerminal(e.view.state) || e.view.cancelRequested)
406
379
  return;
407
380
  e.view.cancelRequested = true;
408
381
  this.telemetry.record(e.trace, "cancel_requested");
@@ -414,15 +387,16 @@ export class ExecutionBus {
414
387
  }
415
388
  }
416
389
  finish(e, state, result, error, resultBytes = 0) {
417
- if (terminal(e.view.state))
390
+ if (isTerminal(e.view.state))
418
391
  return;
419
392
  if (e.queueTimer !== undefined) {
420
393
  clearTimeout(e.queueTimer);
421
394
  e.queueTimer = undefined;
422
395
  }
423
396
  e.finishedAt = performance.now();
424
- // Prepare the delivery snapshot BEFORE committing an irreversible terminal state.
425
- let next = { ...e.view, state, sequence: e.view.sequence + 1 };
397
+ // 终态事件是下一个序号;先用它构建快照,避免把不可逆终态提交到视图之外。
398
+ const terminalSequence = e.view.sequence + 1;
399
+ let next = { ...e.view, state, sequence: terminalSequence };
426
400
  if (result !== undefined)
427
401
  next.result = result;
428
402
  if (error)
@@ -437,7 +411,7 @@ export class ExecutionBus {
437
411
  next = {
438
412
  requestId: e.view.requestId,
439
413
  state,
440
- sequence: e.view.sequence + 1,
414
+ sequence: terminalSequence,
441
415
  cancelRequested: e.view.cancelRequested,
442
416
  delivery: e.view.delivery,
443
417
  error: "RESULT_NOT_RETAINED",
@@ -447,28 +421,36 @@ export class ExecutionBus {
447
421
  e.resultBytes = resultBytes;
448
422
  this.retainedResultBytes += resultBytes;
449
423
  e.created = Date.now();
450
- e.view = { ...next, sequence: next.sequence - 1 };
424
+ // emit() 会再自增序号;先整体回退一档,使终态事件恰好落在 terminalSequence,
425
+ // 与上面交付给等待者的快照序号完全一致。
426
+ e.view = { ...next, sequence: terminalSequence - 1 };
451
427
  this.telemetry.record(e.trace, "terminal");
452
428
  e.resolve(snapshot);
453
429
  this.emit(e, state, { state });
454
- // A reentrant terminal notification may still be queued behind this batch.
430
+ // 重入的终态通知可能仍排在本批之后。
455
431
  if (!e.dispatching)
456
432
  e.listeners.clear();
457
433
  }
434
+ /** 按会话轮转挑选下一个出队项:各会话内部 FIFO,跨会话公平。 */
435
+ nextQueuedIndex() {
436
+ let index = -1;
437
+ while (this.readyOwners.length && index < 0) {
438
+ const owner = this.readyOwners.shift();
439
+ const i = this.queue.findIndex((entry) => entry.owner === owner);
440
+ if (i >= 0) {
441
+ index = i;
442
+ // 找到后,若该会话仍有其它排队任务,保留其轮次避免单个会话饥饿。
443
+ if (this.queue.some((entry, j) => j !== i && entry.owner === owner))
444
+ this.readyOwners.push(owner);
445
+ }
446
+ }
447
+ return index < 0 ? 0 : index;
448
+ }
458
449
  pump() {
459
450
  while (!this.closed &&
460
451
  this.running < this.options.concurrency &&
461
452
  this.queue.length) {
462
- let index = -1;
463
- while (this.readyOwners.length && index < 0) {
464
- const owner = this.readyOwners.shift();
465
- index = this.queue.findIndex((entry) => entry.owner === owner);
466
- if (index >= 0 &&
467
- this.queue.some((entry, i) => i !== index && entry.owner === owner))
468
- this.readyOwners.push(owner);
469
- }
470
- if (index < 0)
471
- index = 0;
453
+ const index = this.nextQueuedIndex();
472
454
  const e = this.queue.splice(index, 1)[0];
473
455
  if (e.view.state !== "queued")
474
456
  continue;
@@ -1,8 +1,9 @@
1
1
  import { performance } from "node:perf_hooks";
2
+ import { LIMITS } from "./limits.js";
2
3
  /** In-memory numeric metadata only: never accepts request/session strings, input, output or error text. */
3
4
  export class Diagnostics {
4
5
  next = 0;
5
- events = new Array(128);
6
+ events = new Array(LIMITS.traceRingSize);
6
7
  cursor = 0;
7
8
  size = 0;
8
9
  counts = {};
@@ -16,14 +17,15 @@ export class Diagnostics {
16
17
  stage,
17
18
  elapsedMs: Math.max(0, performance.now() - context.at),
18
19
  };
19
- this.cursor = (this.cursor + 1) % 128;
20
- this.size = Math.min(this.size + 1, 128);
20
+ this.cursor = (this.cursor + 1) % LIMITS.traceRingSize;
21
+ this.size = Math.min(this.size + 1, LIMITS.traceRingSize);
21
22
  }
22
23
  snapshot() {
23
24
  return {
24
25
  counts: { ...this.counts },
25
26
  traces: Array.from({ length: this.size }, (_, index) => ({
26
- ...this.events[(this.cursor - this.size + 128 + index) % 128],
27
+ ...this.events[(this.cursor - this.size + LIMITS.traceRingSize + index) %
28
+ LIMITS.traceRingSize],
27
29
  })),
28
30
  };
29
31
  }