@feng0531/dsh-execution-bus 0.5.1 → 0.7.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.1**
7
+ DSH 独立插件:`@feng0531/dsh-execution-bus` · 当前版本 **0.7.0**
8
8
 
9
9
  > 萧何模式从宿主已声明的 PTC 组合派生,保留日常工具,再叠加可选执行总线。已验证隔离的真实 Node PTC → 原 read + 总线调用,以及脚本模型内存会话;真实模型、桌面界面与实际沙箱仍未验收。**不支持重启后恢复任务。**
10
10
 
@@ -85,8 +85,10 @@ flowchart TD
85
85
  |---|---|---|
86
86
  | `test`(诊断,非业务) | 仅在明确诊断时检查延迟、进度、成功、失败和取消 | `delayMs`:0–30000;`fail`:布尔值 |
87
87
  | `host.read` | 经由 DSH 现有 `read` 工具读取文本 | `file_path`;可选 `offset` 和 `limit`,最多 100 行 |
88
+ | `host.edit` | 经由 DSH 现有 `edit` 工具替换文本(有副作用) | `file_path`、`old_string`、`new_string`;可选 `replace_all` |
89
+ | `host.write` | 经由 DSH 现有 `write` 工具写入文本(有副作用) | `file_path`、`content` |
88
90
 
89
- `host.read` 在出队时重新进入宿主工具管线,沿用宿主当前权限、会话工作目录及输出限制。插件不自行启动命令、不提供任意远程执行服务。它也不会强制将现有工具改成总线调用。
91
+ `host.read`、`host.edit` 与 `host.write` 在出队时重新进入宿主工具管线,沿用宿主当前权限、会话工作目录及输出限制。`host.edit`/`host.write` 有副作用,显式声明非幂等:同一 requestId 的终态结果不可复用,重复提交返回 `MUTATING_REUSE_DENIED`。插件不自行启动命令、不提供任意远程执行服务,也不会强制将现有工具改成总线调用。
90
92
 
91
93
  ## 选择“萧何”预设
92
94
 
@@ -103,7 +105,7 @@ flowchart TD
103
105
 
104
106
  **0.3.0 起不再是只读验证模式。** 加载时读取宿主 `ptc` 预设的组合声明快照,保留原工具、平台表达式、禁用项和配置,只追加萧何总线与说明。不再强制 native 呈现或添加只读白名单;终端、写入、搜索、技能等能力是否可用,取决于原 PTC 配置和宿主服务。其他预设和默认模式不被修改,原 PTC 会话看不到新增总线。
105
107
 
106
- 普通任务仍通过 `run_code` 调用原工具;**总线目前只支持 test 和 host.read,不接管任意命令、安装或全部工具**。例如更新插件应使用原终端能力并遵守宿主授权;专用插件管理工具若在源 PTC 中禁用,萧何不会擅自启用。命令受理、返回 Job ID 不等于执行完成。
108
+ 普通任务仍通过 `run_code` 调用原工具;**总线目前只支持 test、host.read、host.edit 和 host.write,不接管任意命令、安装或全部工具**。例如更新插件应使用原终端能力并遵守宿主授权;专用插件管理工具若在源 PTC 中禁用,萧何不会擅自启用。命令受理、返回 Job ID 不等于执行完成。
107
109
 
108
110
  升级提示:预设 ID 保持兼容,但从旧版只读模式升级后能力范围扩大,使用者须审阅后加载;本次不自动升级日常配置。它不是进程沙箱,同一预设会话共享总线容量,结果按会话隔离。派生是加载时快照,不自动跟随源预设热编辑;源 PTC 更新后应按宿主正常流程重新加载派生行,旧会话遵循宿主版本保留规则。
109
111
 
@@ -124,7 +126,7 @@ flowchart TD
124
126
  2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
125
127
 
126
128
  ```powershell
127
- npm install --ignore-scripts @feng0531/dsh-execution-bus@0.5.1
129
+ npm install --ignore-scripts @feng0531/dsh-execution-bus@0.7.0
128
130
  ```
129
131
 
130
132
  这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
@@ -267,7 +269,7 @@ npm run test:install # 临时目录安装,不操作日常 DSH
267
269
 
268
270
  - [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
269
271
  - [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
270
- - [验证记录](<docs/validation.md>)、[0.5.1 发布说明](<docs/releases/0.5.1.md>):已验证与未验证范围。
272
+ - [验证记录](<docs/validation.md>)、[0.7.0 发布说明](<docs/releases/0.7.0.md>):已验证与未验证范围。
271
273
  - [同类项目源码整合](<docs/INTEGRATION.md>):固定来源、采用范围、许可与验证边界。
272
274
  - [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
273
275
 
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,60 +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";
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";
5
7
  import { CORE_DEFAULTS } from "./product.js";
6
- export function object(v) {
7
- if (!v || typeof v !== "object" || Array.isArray(v))
8
- throw Error("INVALID_OBJECT");
9
- return v;
10
- }
11
- function canonical(v) {
12
- if (v === null || typeof v !== "object")
13
- return JSON.stringify(v);
14
- if (Array.isArray(v))
15
- return "[" + v.map(canonical).join(",") + "]";
16
- return ("{" +
17
- Object.keys(v)
18
- .sort()
19
- .map((k) => JSON.stringify(k) +
20
- ":" +
21
- canonical(v[k]))
22
- .join(",") +
23
- "}");
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");
24
14
  }
25
- export function parse(raw) {
26
- assertJson(raw);
27
- const m = object(raw);
28
- if (Object.keys(m).some((k) => ![
29
- "protocolVersion",
30
- "messageId",
31
- "requestId",
32
- "type",
33
- "payload",
34
- ].includes(k)))
35
- throw Error("UNKNOWN_FIELD");
36
- if (m.protocolVersion !== 1)
37
- throw Error("UNSUPPORTED_VERSION");
38
- for (const k of ["messageId", "requestId"])
39
- if (typeof m[k] !== "string" ||
40
- !/^[a-zA-Z0-9_.:-]{1,128}$/.test(m[k]))
41
- throw Error("INVALID_ID");
42
- if (!["request", "status", "result", "cancel"].includes(m.type))
43
- throw Error("UNSUPPORTED_TYPE");
44
- object(m.payload);
45
- if (Buffer.byteLength(JSON.stringify(m)) > 16384)
46
- throw Error("MESSAGE_TOO_LARGE");
47
- return structuredClone(m);
48
- }
49
- function freezeJson(value) {
50
- if (value && typeof value === "object") {
51
- for (const child of Object.values(value))
52
- freezeJson(child);
53
- Object.freeze(value);
54
- }
55
- return value;
56
- }
57
- const terminal = (s) => !["queued", "running"].includes(s);
58
15
  /** Volatile single-process bus; no acceptance durability or automatic execution retry. */
59
16
  export class ExecutionBus {
60
17
  options;
@@ -87,7 +44,7 @@ export class ExecutionBus {
87
44
  typeof admission !== "object" ||
88
45
  Array.isArray(admission) ||
89
46
  Reflect.ownKeys(admission).some((key) => typeof key !== "string" || !allowed.includes(key)))
90
- throw Error("INVALID_ADMISSION_POLICY");
47
+ throw Error(ERRORS.INVALID_ADMISSION_POLICY);
91
48
  for (const key of Reflect.ownKeys(admission)) {
92
49
  const descriptor = Object.getOwnPropertyDescriptor(admission, key);
93
50
  if (!("value" in descriptor) ||
@@ -95,7 +52,7 @@ export class ExecutionBus {
95
52
  !Number.isSafeInteger(descriptor.value) ||
96
53
  descriptor.value < 1 ||
97
54
  descriptor.value > 2147483647)
98
- throw Error("INVALID_ADMISSION_POLICY");
55
+ throw Error(ERRORS.INVALID_ADMISSION_POLICY);
99
56
  }
100
57
  this.admission = Object.freeze({ ...admission });
101
58
  const keys = [
@@ -113,15 +70,15 @@ export class ExecutionBus {
113
70
  const descriptor = Object.getOwnPropertyDescriptor(options, key);
114
71
  return (!descriptor || !("value" in descriptor) || !descriptor.enumerable);
115
72
  }))
116
- throw Error("INVALID_OPTIONS");
73
+ throw Error(ERRORS.INVALID_OPTIONS);
117
74
  this.options = Object.freeze({ ...options });
118
75
  for (const v of Object.values(this.options))
119
76
  if (!Number.isSafeInteger(v) || v < 1)
120
- throw Error("INVALID_OPTIONS");
77
+ throw Error(ERRORS.INVALID_OPTIONS);
121
78
  }
122
79
  register(name, handler) {
123
80
  if (this.handlers.has(name))
124
- throw Error("DUPLICATE_HANDLER");
81
+ throw Error(ERRORS.DUPLICATE_HANDLER);
125
82
  const registration = { handler, token: Symbol(name) };
126
83
  this.handlers.set(name, registration);
127
84
  let disposed = false;
@@ -135,7 +92,7 @@ export class ExecutionBus {
135
92
  }
136
93
  key(o, id) {
137
94
  if (!o.sessionId || !o.callId)
138
- throw Error("UNTRUSTED_CONTEXT");
95
+ throw Error(ERRORS.UNTRUSTED_CONTEXT);
139
96
  return JSON.stringify([o.sessionId, id]);
140
97
  }
141
98
  emit(e, type, payload, correlationId = "") {
@@ -149,8 +106,7 @@ export class ExecutionBus {
149
106
  type,
150
107
  payload,
151
108
  });
152
- // Snapshot and enqueue before invoking user code: nested cancellation cannot
153
- // overtake the current event for later observers. Removed listeners are skipped.
109
+ // 在调用用户代码前快照并排队:嵌套取消无法抢到当前事件。
154
110
  e.notifications.push({ event, listeners: [...e.listeners] });
155
111
  if (e.dispatching)
156
112
  return;
@@ -170,7 +126,7 @@ export class ExecutionBus {
170
126
  }
171
127
  finally {
172
128
  e.dispatching = false;
173
- if (terminal(e.view.state))
129
+ if (isTerminal(e.view.state))
174
130
  e.listeners.clear();
175
131
  }
176
132
  }
@@ -180,68 +136,87 @@ export class ExecutionBus {
180
136
  get(o, id) {
181
137
  const e = this.entries.get(this.key(o, id));
182
138
  if (!e)
183
- throw Error("NOT_FOUND");
139
+ throw Error(ERRORS.NOT_FOUND);
184
140
  return e;
185
141
  }
186
142
  sweep(now = Date.now()) {
187
143
  for (const [k, e] of this.entries)
188
- if (terminal(e.view.state) &&
189
- now - e.created > this.options.retentionMs) {
144
+ if (isTerminal(e.view.state) && now - e.created > this.options.retentionMs) {
190
145
  this.retainedResultBytes -= e.resultBytes;
191
146
  this.entries.delete(k);
192
147
  }
193
148
  }
194
149
  submit(owner, raw, onEvent, invoke) {
195
150
  if (this.closed)
196
- throw Error("CLOSED");
151
+ throw Error(ERRORS.CLOSED);
197
152
  const m = parse(raw);
198
153
  if (m.type !== "request")
199
- 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) {
200
166
  const p = m.payload;
201
167
  if (Object.keys(p).some((k) => !["handler", "input"].includes(k)) ||
202
168
  typeof p.handler !== "string")
203
- throw Error("INVALID_REQUEST");
204
- const input = object(p.input);
205
- const key = this.key(owner, m.requestId), fp = createHash("sha256").update(canonical(p)).digest("hex");
206
- this.sweep();
207
- let e = this.entries.get(key);
208
- if (e) {
209
- if (e.fingerprint !== fp)
210
- throw Error("REQUEST_CONFLICT");
211
- const previous = e.messages.get(m.messageId);
212
- if (previous && previous !== canonical(m))
213
- throw Error("MESSAGE_CONFLICT");
214
- if (!previous && e.messages.size >= 128)
215
- throw Error("MESSAGE_LIMIT");
216
- // Acquire the attachment before committing its message identity. Rejection is side-effect free.
217
- const attached = this.attachment(e, onEvent, m.messageId);
218
- if (!previous)
219
- e.messages.set(m.messageId, canonical(m));
220
- this.telemetry.record(e.trace, "reused");
221
- return attached;
222
- }
223
- 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;
224
199
  if (!handler)
225
- throw Error("UNKNOWN_HANDLER");
226
- handler.validate(input);
200
+ throw Error(ERRORS.UNKNOWN_HANDLER);
201
+ handler.validate(parts.input);
227
202
  if (this.entries.size >= this.options.recordLimit ||
228
203
  this.queue.length >= this.options.queueLimit)
229
- throw Error("BACKPRESSURE");
204
+ throw Error(ERRORS.BACKPRESSURE);
230
205
  if (this.admission.sessionQueueLimit !== undefined &&
231
206
  this.queue.filter((entry) => entry.owner === owner.sessionId).length >=
232
207
  this.admission.sessionQueueLimit)
233
- throw Error("SESSION_BACKPRESSURE");
208
+ throw Error(ERRORS.SESSION_BACKPRESSURE);
234
209
  let resolve;
235
210
  const done = new Promise((r) => (resolve = r));
236
- e = {
211
+ const e = {
237
212
  trace: this.telemetry.begin(),
238
213
  resultBytes: 0,
239
214
  invoke,
240
215
  correlationId: m.messageId,
241
216
  owner: owner.sessionId,
242
- fingerprint: fp,
217
+ fingerprint,
243
218
  handler,
244
- input: structuredClone(input),
219
+ input: structuredClone(parts.input),
245
220
  view: {
246
221
  requestId: m.requestId,
247
222
  state: "queued",
@@ -263,33 +238,36 @@ export class ExecutionBus {
263
238
  this.telemetry.record(e.trace, "accepted");
264
239
  const attached = this.attachment(e, onEvent, m.messageId);
265
240
  this.queue.push(e);
266
- if (this.admission.maxQueueWaitMs !== undefined) {
267
- e.deadline = performance.now() + this.admission.maxQueueWaitMs;
268
- const entry = e;
269
- const expire = () => {
270
- if (entry.view.state !== "queued")
271
- return;
272
- const remaining = entry.deadline - performance.now();
273
- if (remaining > 0) {
274
- entry.queueTimer = setTimeout(expire, Math.ceil(remaining));
275
- return;
276
- }
277
- this.removeQueued(entry);
278
- this.finish(entry, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
279
- };
280
- e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
281
- }
241
+ this.scheduleQueueDeadline(e);
282
242
  if (!this.readyOwners.includes(e.owner))
283
243
  this.readyOwners.push(e.owner);
284
244
  queueMicrotask(() => this.pump());
285
245
  return attached;
286
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
+ }
287
265
  attachment(e, onEvent, id) {
288
- // Each attachment owns its registration, even when callers reuse one function.
266
+ // 每次附着独立占配额,即使调用方复用同一个回调。
289
267
  const listener = onEvent ? (event) => onEvent(event) : undefined;
290
- if (listener && !terminal(e.view.state)) {
291
- if (e.listeners.size >= 32)
292
- throw Error("WAITER_LIMIT");
268
+ if (listener && !isTerminal(e.view.state)) {
269
+ if (e.listeners.size >= LIMITS.waiterLimit)
270
+ throw Error(ERRORS.WAITER_LIMIT);
293
271
  e.listeners.add(listener);
294
272
  }
295
273
  const accepted = {
@@ -319,20 +297,20 @@ export class ExecutionBus {
319
297
  command(owner, raw) {
320
298
  const m = parse(raw);
321
299
  if (m.type === "request")
322
- throw Error("EXPECTED_CONTROL");
300
+ throw Error(ERRORS.EXPECTED_CONTROL);
323
301
  if (Object.keys(m.payload).length)
324
- throw Error("INVALID_CONTROL");
302
+ throw Error(ERRORS.INVALID_CONTROL);
325
303
  this.sweep();
326
304
  const e = this.get(owner, m.requestId);
327
305
  const previous = e.messages.get(m.messageId), fingerprint = canonical(m);
328
306
  if (previous && previous !== fingerprint)
329
- throw Error("MESSAGE_CONFLICT");
307
+ throw Error(ERRORS.MESSAGE_CONFLICT);
330
308
  if (!previous) {
331
- // Keep one emergency control slot: read traffic must never prevent cancellation.
332
- if (e.messages.size >= 128) {
309
+ // 保留一个应急控制槽:读流量绝不能阻塞取消。
310
+ if (e.messages.size >= LIMITS.messageAccountLimit) {
333
311
  if (m.type !== "cancel")
334
- throw Error("MESSAGE_LIMIT");
335
- if (e.view.cancelRequested || terminal(e.view.state))
312
+ throw Error(ERRORS.MESSAGE_LIMIT);
313
+ if (e.view.cancelRequested || isTerminal(e.view.state))
336
314
  return this.view(e);
337
315
  }
338
316
  e.messages.set(m.messageId, fingerprint);
@@ -349,14 +327,14 @@ export class ExecutionBus {
349
327
  /** Trusted host owner required. Never exposes other sessions or business payloads. */
350
328
  inspect(owner) {
351
329
  if (!owner || typeof owner.sessionId !== "string" || !owner.sessionId)
352
- throw Error("OWNING_SESSION_REQUIRED");
330
+ throw Error(ERRORS.OWNING_SESSION_REQUIRED);
353
331
  this.sweep();
354
332
  const now = performance.now();
355
333
  const records = [...this.entries.values()].filter((e) => e.owner === owner.sessionId);
356
- // Active work must not disappear behind newer terminal history.
357
- const active = records.filter((e) => !terminal(e.view.state));
358
- const history = records.filter((e) => terminal(e.view.state)).reverse();
359
- 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);
360
338
  const tasks = selected.map((e) => ({
361
339
  requestId: e.view.requestId,
362
340
  state: e.view.state,
@@ -369,7 +347,7 @@ export class ExecutionBus {
369
347
  ? "concurrency"
370
348
  : "dispatch"
371
349
  : null,
372
- phase: terminal(e.view.state)
350
+ phase: isTerminal(e.view.state)
373
351
  ? e.view.delivery === "returned"
374
352
  ? "returned"
375
353
  : "terminal"
@@ -390,14 +368,14 @@ export class ExecutionBus {
390
368
  counts: {
391
369
  queued: records.filter((e) => e.view.state === "queued").length,
392
370
  running: records.filter((e) => e.view.state === "running").length,
393
- terminal: records.filter((e) => terminal(e.view.state)).length,
371
+ terminal: records.filter((e) => isTerminal(e.view.state)).length,
394
372
  },
395
373
  tasks,
396
- truncated: records.length > 50,
374
+ truncated: records.length > LIMITS.inspectTaskLimit,
397
375
  });
398
376
  }
399
377
  cancel(e) {
400
- if (terminal(e.view.state) || e.view.cancelRequested)
378
+ if (isTerminal(e.view.state) || e.view.cancelRequested)
401
379
  return;
402
380
  e.view.cancelRequested = true;
403
381
  this.telemetry.record(e.trace, "cancel_requested");
@@ -409,15 +387,16 @@ export class ExecutionBus {
409
387
  }
410
388
  }
411
389
  finish(e, state, result, error, resultBytes = 0) {
412
- if (terminal(e.view.state))
390
+ if (isTerminal(e.view.state))
413
391
  return;
414
392
  if (e.queueTimer !== undefined) {
415
393
  clearTimeout(e.queueTimer);
416
394
  e.queueTimer = undefined;
417
395
  }
418
396
  e.finishedAt = performance.now();
419
- // Prepare the delivery snapshot BEFORE committing an irreversible terminal state.
420
- 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 };
421
400
  if (result !== undefined)
422
401
  next.result = result;
423
402
  if (error)
@@ -432,7 +411,7 @@ export class ExecutionBus {
432
411
  next = {
433
412
  requestId: e.view.requestId,
434
413
  state,
435
- sequence: e.view.sequence + 1,
414
+ sequence: terminalSequence,
436
415
  cancelRequested: e.view.cancelRequested,
437
416
  delivery: e.view.delivery,
438
417
  error: "RESULT_NOT_RETAINED",
@@ -442,28 +421,36 @@ export class ExecutionBus {
442
421
  e.resultBytes = resultBytes;
443
422
  this.retainedResultBytes += resultBytes;
444
423
  e.created = Date.now();
445
- e.view = { ...next, sequence: next.sequence - 1 };
424
+ // emit() 会再自增序号;先整体回退一档,使终态事件恰好落在 terminalSequence,
425
+ // 与上面交付给等待者的快照序号完全一致。
426
+ e.view = { ...next, sequence: terminalSequence - 1 };
446
427
  this.telemetry.record(e.trace, "terminal");
447
428
  e.resolve(snapshot);
448
429
  this.emit(e, state, { state });
449
- // A reentrant terminal notification may still be queued behind this batch.
430
+ // 重入的终态通知可能仍排在本批之后。
450
431
  if (!e.dispatching)
451
432
  e.listeners.clear();
452
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
+ }
453
449
  pump() {
454
450
  while (!this.closed &&
455
451
  this.running < this.options.concurrency &&
456
452
  this.queue.length) {
457
- let index = -1;
458
- while (this.readyOwners.length && index < 0) {
459
- const owner = this.readyOwners.shift();
460
- index = this.queue.findIndex((entry) => entry.owner === owner);
461
- if (index >= 0 &&
462
- this.queue.some((entry, i) => i !== index && entry.owner === owner))
463
- this.readyOwners.push(owner);
464
- }
465
- if (index < 0)
466
- index = 0;
453
+ const index = this.nextQueuedIndex();
467
454
  const e = this.queue.splice(index, 1)[0];
468
455
  if (e.view.state !== "queued")
469
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
  }