@feng0531/dsh-execution-bus 0.5.1 → 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.
@@ -0,0 +1,60 @@
1
+ /** 集中全部固定错误码。值即协议 v1 的稳定错误词汇,不得重命名或改动。
2
+ * 新增错误码前先更新 docs/protocol.md 的错误词汇表。 */
3
+ export const ERRORS = Object.freeze({
4
+ // —— 协议解析(protocol) ——
5
+ INVALID_OBJECT: "INVALID_OBJECT",
6
+ UNKNOWN_FIELD: "UNKNOWN_FIELD",
7
+ UNSUPPORTED_VERSION: "UNSUPPORTED_VERSION",
8
+ INVALID_ID: "INVALID_ID",
9
+ UNSUPPORTED_TYPE: "UNSUPPORTED_TYPE",
10
+ MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE",
11
+ // —— 严格 JSON(json) ——
12
+ JSON_LIMIT: "JSON_LIMIT",
13
+ INVALID_JSON: "INVALID_JSON",
14
+ CYCLIC_JSON: "CYCLIC_JSON",
15
+ INVALID_JSON_ARRAY: "INVALID_JSON_ARRAY",
16
+ INVALID_JSON_OBJECT: "INVALID_JSON_OBJECT",
17
+ INVALID_JSON_KEY: "INVALID_JSON_KEY",
18
+ INVALID_JSON_PROPERTY: "INVALID_JSON_PROPERTY",
19
+ // —— 总线调度与准入(bus) ——
20
+ CLOSED: "CLOSED",
21
+ EXPECTED_REQUEST: "EXPECTED_REQUEST",
22
+ INVALID_REQUEST: "INVALID_REQUEST",
23
+ REQUEST_CONFLICT: "REQUEST_CONFLICT",
24
+ MESSAGE_CONFLICT: "MESSAGE_CONFLICT",
25
+ MESSAGE_LIMIT: "MESSAGE_LIMIT",
26
+ UNKNOWN_HANDLER: "UNKNOWN_HANDLER",
27
+ BACKPRESSURE: "BACKPRESSURE",
28
+ SESSION_BACKPRESSURE: "SESSION_BACKPRESSURE",
29
+ WAITER_LIMIT: "WAITER_LIMIT",
30
+ EXPECTED_CONTROL: "EXPECTED_CONTROL",
31
+ INVALID_CONTROL: "INVALID_CONTROL",
32
+ NOT_FOUND: "NOT_FOUND",
33
+ UNTRUSTED_CONTEXT: "UNTRUSTED_CONTEXT",
34
+ OWNING_SESSION_REQUIRED: "OWNING_SESSION_REQUIRED",
35
+ QUEUE_WAIT_TIMEOUT: "QUEUE_WAIT_TIMEOUT",
36
+ RESULT_NOT_RETAINED: "RESULT_NOT_RETAINED",
37
+ CANCELLED: "CANCELLED",
38
+ CANCELLATION_UNCONFIRMED: "CANCELLATION_UNCONFIRMED",
39
+ HANDLER_FAILED: "HANDLER_FAILED",
40
+ INVALID_ADMISSION_POLICY: "INVALID_ADMISSION_POLICY",
41
+ INVALID_OPTIONS: "INVALID_OPTIONS",
42
+ DUPLICATE_HANDLER: "DUPLICATE_HANDLER",
43
+ MUTATING_REUSE_DENIED: "MUTATING_REUSE_DENIED",
44
+ // —— 宿主投影(execution / plugin) ——
45
+ DUPLICATE_EXECUTION_HANDLE: "DUPLICATE_EXECUTION_HANDLE",
46
+ INVALID_STATE_QUERY: "INVALID_STATE_QUERY",
47
+ HOST_READ_FAILED: "HOST_READ_FAILED",
48
+ HOST_JOB_UNAVAILABLE: "HOST_JOB_UNAVAILABLE",
49
+ // —— 预设派生(preset) ——
50
+ PTC_PRESET_TOO_LARGE: "PTC_PRESET_TOO_LARGE",
51
+ PTC_PRESET_ALREADY_CONTAINS_BUS: "PTC_PRESET_ALREADY_CONTAINS_BUS",
52
+ PTC_PRESET_RELATIVE_BASE_UNSUPPORTED: "PTC_PRESET_RELATIVE_BASE_UNSUPPORTED",
53
+ PTC_PRESET_UNAVAILABLE: "PTC_PRESET_UNAVAILABLE",
54
+ // —— 内置 Handler(handlers) ——
55
+ INVALID_TEST_INPUT: "INVALID_TEST_INPUT",
56
+ INVALID_READ_INPUT: "INVALID_READ_INPUT",
57
+ READ_LIMIT: "READ_LIMIT",
58
+ CONTROLLED_FAILURE: "CONTROLLED_FAILURE",
59
+ HOST_CAPABILITY_REQUIRED: "HOST_CAPABILITY_REQUIRED",
60
+ });
@@ -22,6 +22,10 @@ export declare class ExecutionHandle {
22
22
  progress(event: Event): void;
23
23
  private absent;
24
24
  private waitAndClean;
25
+ /** 等待宿主 Job 到达终态(离开 running/stopping)。 */
26
+ private awaitTerminal;
27
+ /** wait 失败后的有界恢复:清理已终态的 Job,但仍向上保留原始错误可见性。 */
28
+ private recoverOnWaitError;
25
29
  }
26
30
  export type ShutdownPhase = "open" | "stopping" | "draining" | "releasing" | "closed";
27
31
  /** Owns host projections and active deliveries, not a second execution state machine. */
@@ -56,6 +60,10 @@ export declare class ExecutionOwner {
56
60
  get phase(): ShutdownPhase;
57
61
  get(key: string): ExecutionHandle | undefined;
58
62
  add(key: string, handle: ExecutionHandle): void;
63
+ /** 拒绝关闭后或重复 key 的准入;create/add 共用这一道守卫。 */
64
+ private assertAdmittable;
65
+ /** 登记 handle,并在其结算后从表中移除该登记。 */
66
+ private attach;
59
67
  track<T>(delivery: Promise<T>): Promise<T>;
60
68
  close(drainCore: () => Promise<void>): Promise<void>;
61
69
  }
package/dist/execution.js CHANGED
@@ -1,5 +1,17 @@
1
1
  import { setTimeout as pause } from "node:timers/promises";
2
2
  import { Diagnostics } from "./diagnostics.js";
3
+ import { ERRORS } from "./error-codes.js";
4
+ /** 把执行终态映射为宿主 Job 的结果状态。 */
5
+ function jobOutcome(state) {
6
+ return {
7
+ status: state === "succeeded"
8
+ ? "completed"
9
+ : state === "cancelled"
10
+ ? "killed"
11
+ : "failed",
12
+ detail: state,
13
+ };
14
+ }
3
15
  /** One host projection of a logical execution. The core completion remains authoritative. */
4
16
  export class ExecutionHandle {
5
17
  jobs;
@@ -18,14 +30,7 @@ export class ExecutionHandle {
18
30
  this.telemetry = telemetry;
19
31
  this.trace = telemetry.begin();
20
32
  this.completion = execution.done;
21
- const outcome = this.completion.then((v) => ({
22
- status: v.state === "succeeded"
23
- ? "completed"
24
- : v.state === "cancelled"
25
- ? "killed"
26
- : "failed",
27
- detail: v.state,
28
- }));
33
+ const outcome = this.completion.then((v) => jobOutcome(v.state));
29
34
  this.job = jobs.start({
30
35
  kind: "execution-bus",
31
36
  label: "萧何 / Xiao He",
@@ -58,49 +63,58 @@ export class ExecutionHandle {
58
63
  }
59
64
  async waitAndClean() {
60
65
  try {
61
- for (;;) {
62
- const view = await this.jobs.wait(this.job, 60000, this.owner);
63
- if (view.status !== "running" && view.status !== "stopping")
64
- break;
65
- }
66
+ await this.awaitTerminal();
66
67
  this.jobs.remove(this.job, this.owner);
67
68
  this.telemetry.record(this.trace, "job_released");
68
69
  }
69
70
  catch (error) {
70
71
  this.telemetry.record(this.trace, "job_error");
71
- const removed = this.absent(error);
72
- // Retain the original execution, including while its caller/owner is being disposed.
73
- await this.completion;
74
- if (removed)
75
- this.telemetry.record(this.trace, "job_released");
76
- if (!removed) {
77
- for (let attempt = 0; attempt < 20; attempt++) {
78
- try {
79
- const view = this.jobs
80
- .list(this.owner)
81
- .find((v) => v.id === this.job && v.owner === this.owner);
82
- if (!view) {
83
- this.telemetry.record(this.trace, "job_released");
84
- break;
85
- }
86
- if (view.status !== "running" && view.status !== "stopping") {
87
- this.jobs.remove(this.job, this.owner);
88
- this.telemetry.record(this.trace, "job_released");
89
- break;
90
- }
91
- }
92
- catch {
93
- break;
94
- }
95
- await pause(10);
96
- }
97
- throw error;
98
- }
72
+ await this.recoverOnWaitError(error);
99
73
  }
100
74
  finally {
101
75
  this.progressHandle = undefined;
102
76
  }
103
77
  }
78
+ /** 等待宿主 Job 到达终态(离开 running/stopping)。 */
79
+ async awaitTerminal() {
80
+ for (;;) {
81
+ const view = await this.jobs.wait(this.job, 60000, this.owner);
82
+ if (view.status !== "running" && view.status !== "stopping")
83
+ return;
84
+ }
85
+ }
86
+ /** wait 失败后的有界恢复:清理已终态的 Job,但仍向上保留原始错误可见性。 */
87
+ async recoverOnWaitError(error) {
88
+ const alreadyRemoved = this.absent(error);
89
+ // 保留原执行结果,即便其调用者/owner 正在被释放。
90
+ await this.completion;
91
+ // Job 已不在列表时仅记录释放,不把 wait 错误视为未处理的清理失败。
92
+ if (alreadyRemoved) {
93
+ this.telemetry.record(this.trace, "job_released");
94
+ return;
95
+ }
96
+ for (let attempt = 0; attempt < 20; attempt++) {
97
+ try {
98
+ const view = this.jobs
99
+ .list(this.owner)
100
+ .find((v) => v.id === this.job && v.owner === this.owner);
101
+ if (!view) {
102
+ this.telemetry.record(this.trace, "job_released");
103
+ break;
104
+ }
105
+ if (view.status !== "running" && view.status !== "stopping") {
106
+ this.jobs.remove(this.job, this.owner);
107
+ this.telemetry.record(this.trace, "job_released");
108
+ break;
109
+ }
110
+ }
111
+ catch {
112
+ break;
113
+ }
114
+ await pause(10);
115
+ }
116
+ throw error;
117
+ }
104
118
  }
105
119
  /** Owns host projections and active deliveries, not a second execution state machine. */
106
120
  export class ExecutionOwner {
@@ -109,12 +123,9 @@ export class ExecutionOwner {
109
123
  closing;
110
124
  telemetry = new Diagnostics();
111
125
  create(key, jobs, owner, execution) {
112
- if (this.phase !== "open")
113
- throw Error("CLOSED");
114
- if (this.handles.has(key))
115
- throw Error("DUPLICATE_EXECUTION_HANDLE");
126
+ this.assertAdmittable(key);
116
127
  const handle = new ExecutionHandle(jobs, owner, execution, this.telemetry);
117
- this.add(key, handle);
128
+ this.attach(key, handle);
118
129
  return handle;
119
130
  }
120
131
  diagnostics() {
@@ -133,10 +144,18 @@ export class ExecutionOwner {
133
144
  return this.handles.get(key);
134
145
  }
135
146
  add(key, handle) {
147
+ this.assertAdmittable(key);
148
+ this.attach(key, handle);
149
+ }
150
+ /** 拒绝关闭后或重复 key 的准入;create/add 共用这一道守卫。 */
151
+ assertAdmittable(key) {
136
152
  if (this.phase !== "open")
137
- throw Error("CLOSED");
153
+ throw Error(ERRORS.CLOSED);
138
154
  if (this.handles.has(key))
139
- throw Error("DUPLICATE_EXECUTION_HANDLE");
155
+ throw Error(ERRORS.DUPLICATE_EXECUTION_HANDLE);
156
+ }
157
+ /** 登记 handle,并在其结算后从表中移除该登记。 */
158
+ attach(key, handle) {
140
159
  this.handles.set(key, handle);
141
160
  const release = () => {
142
161
  if (this.handles.get(key) === handle)
package/dist/handlers.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { setTimeout } from "node:timers/promises";
2
- import { LIMITS } from "./product.js";
2
+ import { ERRORS } from "./error-codes.js";
3
+ import { HANDLER_LIMITS } from "./product.js";
3
4
  export const testHandler = {
5
+ idempotent: true,
4
6
  validate(input) {
5
7
  if (Object.keys(input).some((k) => !["delayMs", "fail"].includes(k)) ||
6
8
  !Number.isSafeInteger(input.delayMs) ||
7
9
  Number(input.delayMs) < 0 ||
8
- Number(input.delayMs) > LIMITS.diagnosticDelayMs ||
10
+ Number(input.delayMs) > HANDLER_LIMITS.diagnosticDelayMs ||
9
11
  typeof input.fail !== "boolean")
10
- throw Error("INVALID_TEST_INPUT");
12
+ throw Error(ERRORS.INVALID_TEST_INPUT);
11
13
  },
12
14
  async run(input, ctx) {
13
15
  for (let i = 1; i <= 5; i++) {
@@ -17,27 +19,28 @@ export const testHandler = {
17
19
  ctx.progress(i, i * 20);
18
20
  }
19
21
  if (input.fail)
20
- throw Error("CONTROLLED_FAILURE");
22
+ throw Error(ERRORS.CONTROLLED_FAILURE);
21
23
  return { ok: true };
22
24
  },
23
25
  };
24
26
  export const readHandler = {
27
+ idempotent: true,
25
28
  validate(input) {
26
29
  if (Object.keys(input).some((k) => !["file_path", "offset", "limit"].includes(k)) ||
27
30
  typeof input.file_path !== "string" ||
28
31
  !input.file_path ||
29
- input.file_path.length > LIMITS.readPathChars)
30
- throw Error("INVALID_READ_INPUT");
32
+ input.file_path.length > HANDLER_LIMITS.readPathChars)
33
+ throw Error(ERRORS.INVALID_READ_INPUT);
31
34
  for (const k of ["offset", "limit"])
32
35
  if (input[k] !== undefined &&
33
36
  (!Number.isSafeInteger(input[k]) || Number(input[k]) < 1))
34
- throw Error("INVALID_READ_INPUT");
35
- if (Number(input.limit ?? LIMITS.readLines) > LIMITS.readLines)
36
- throw Error("READ_LIMIT");
37
+ throw Error(ERRORS.INVALID_READ_INPUT);
38
+ if (Number(input.limit ?? HANDLER_LIMITS.readLines) > HANDLER_LIMITS.readLines)
39
+ throw Error(ERRORS.READ_LIMIT);
37
40
  },
38
41
  async run(input, ctx) {
39
42
  if (!ctx.invoke)
40
- throw Error("HOST_CAPABILITY_REQUIRED");
41
- return ctx.invoke({ ...input, limit: input.limit ?? LIMITS.readLines }, ctx.signal);
43
+ throw Error(ERRORS.HOST_CAPABILITY_REQUIRED);
44
+ return ctx.invoke({ ...input, limit: input.limit ?? HANDLER_LIMITS.readLines }, ctx.signal);
42
45
  },
43
46
  };
package/dist/json.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Strict JSON tree validation before serialization, cloning or recursive freezing. */
2
2
  export declare function assertJson(value: unknown, depth?: number, active?: Set<object>, budget?: {
3
- left: number;
3
+ left: 65536;
4
4
  }): void;
package/dist/json.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { ERRORS } from "./error-codes.js";
2
+ import { LIMITS } from "./limits.js";
1
3
  /** Strict JSON tree validation before serialization, cloning or recursive freezing. */
2
- export function assertJson(value, depth = 0, active = new Set(), budget = { left: 65536 }) {
3
- if (--budget.left < 0 || depth > 64)
4
- throw Error("JSON_LIMIT");
4
+ export function assertJson(value, depth = 0, active = new Set(), budget = { left: LIMITS.jsonNodeBudget }) {
5
+ if (--budget.left < 0 || depth > LIMITS.jsonMaxDepth)
6
+ throw Error(ERRORS.JSON_LIMIT);
5
7
  if (value === null || typeof value === "string" || typeof value === "boolean")
6
8
  return;
7
9
  if (typeof value === "number" &&
@@ -9,32 +11,32 @@ export function assertJson(value, depth = 0, active = new Set(), budget = { left
9
11
  !Object.is(value, -0))
10
12
  return;
11
13
  if (typeof value !== "object" || value === null)
12
- throw Error("INVALID_JSON");
14
+ throw Error(ERRORS.INVALID_JSON);
13
15
  if (active.has(value))
14
- throw Error("CYCLIC_JSON");
16
+ throw Error(ERRORS.CYCLIC_JSON);
15
17
  const array = Array.isArray(value);
16
18
  if (array && Object.getPrototypeOf(value) !== Array.prototype)
17
- throw Error("INVALID_JSON_ARRAY");
19
+ throw Error(ERRORS.INVALID_JSON_ARRAY);
18
20
  if (!array &&
19
21
  Object.getPrototypeOf(value) !== Object.prototype &&
20
22
  Object.getPrototypeOf(value) !== null)
21
- throw Error("INVALID_JSON_OBJECT");
23
+ throw Error(ERRORS.INVALID_JSON_OBJECT);
22
24
  active.add(value);
23
25
  try {
24
26
  const keys = Reflect.ownKeys(value);
25
27
  if (array && keys.length !== value.length + 1)
26
- throw Error("INVALID_JSON_ARRAY");
28
+ throw Error(ERRORS.INVALID_JSON_ARRAY);
27
29
  for (const key of keys) {
28
30
  if (array && key === "length")
29
31
  continue;
30
32
  if (typeof key !== "string")
31
- throw Error("INVALID_JSON_KEY");
33
+ throw Error(ERRORS.INVALID_JSON_KEY);
32
34
  if (array &&
33
35
  (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= value.length))
34
- throw Error("INVALID_JSON_ARRAY");
36
+ throw Error(ERRORS.INVALID_JSON_ARRAY);
35
37
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
36
38
  if (!descriptor.enumerable || !("value" in descriptor))
37
- throw Error("INVALID_JSON_PROPERTY");
39
+ throw Error(ERRORS.INVALID_JSON_PROPERTY);
38
40
  assertJson(descriptor.value, depth + 1, active, budget);
39
41
  }
40
42
  }
@@ -0,0 +1,20 @@
1
+ export declare const LIMITS: Readonly<{
2
+ /** 单条协议消息 JSON 的最大字节数(parse 拒绝更大消息)。 */
3
+ messageMaxBytes: 16384;
4
+ /** messageId / requestId 的最大字符数。 */
5
+ idMaxLength: 128;
6
+ /** messageId / requestId 的合法字符集。 */
7
+ idPattern: RegExp;
8
+ /** 单个逻辑执行所记账的消息条数上限(含首次、重复请求与查询)。 */
9
+ messageAccountLimit: 128;
10
+ /** 单个执行可同时附着的观察者上限。 */
11
+ waiterLimit: 32;
12
+ /** inspect() 单次返回的任务元数据条数上限。 */
13
+ inspectTaskLimit: 50;
14
+ /** 严格 JSON 树的最大嵌套深度。 */
15
+ jsonMaxDepth: 64;
16
+ /** 严格 JSON 树可访问的节点总数预算。 */
17
+ jsonNodeBudget: 65536;
18
+ /** 诊断环形轨迹的槽位数量。 */
19
+ traceRingSize: 128;
20
+ }>;
package/dist/limits.js ADDED
@@ -0,0 +1,23 @@
1
+ /** 集中定义跨模块共享的固定数值上限。内部实现约束,非用户可配置项。
2
+ * 数值相同时语义依然独立,勿为了“省一个常量”而合并。 */
3
+ const ID_MAX_LENGTH = 128;
4
+ export const LIMITS = Object.freeze({
5
+ /** 单条协议消息 JSON 的最大字节数(parse 拒绝更大消息)。 */
6
+ messageMaxBytes: 16384,
7
+ /** messageId / requestId 的最大字符数。 */
8
+ idMaxLength: ID_MAX_LENGTH,
9
+ /** messageId / requestId 的合法字符集。 */
10
+ idPattern: new RegExp(`^[a-zA-Z0-9_.:-]{1,${ID_MAX_LENGTH}}$`),
11
+ /** 单个逻辑执行所记账的消息条数上限(含首次、重复请求与查询)。 */
12
+ messageAccountLimit: 128,
13
+ /** 单个执行可同时附着的观察者上限。 */
14
+ waiterLimit: 32,
15
+ /** inspect() 单次返回的任务元数据条数上限。 */
16
+ inspectTaskLimit: 50,
17
+ /** 严格 JSON 树的最大嵌套深度。 */
18
+ jsonMaxDepth: 64,
19
+ /** 严格 JSON 树可访问的节点总数预算。 */
20
+ jsonNodeBudget: 65536,
21
+ /** 诊断环形轨迹的槽位数量。 */
22
+ traceRingSize: 128,
23
+ });
package/dist/plugin.js CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { ADMISSION_DEFAULTS, REQUEST_DESCRIPTION, REQUEST_PARAMETERS, STATE_PARAMETERS, } from "./product.js";
3
3
  import { ExecutionOwner } from "./execution.js";
4
4
  import { ExecutionBus, parse } from "./bus.js";
5
+ import { ERRORS } from "./error-codes.js";
5
6
  import { testHandler, readHandler } from "./handlers.js";
6
7
  export const name = "execution-bus";
7
8
  export const inject = ["tools", "jobs"];
@@ -21,14 +22,14 @@ export function apply(ctx) {
21
22
  },
22
23
  async execute(raw, exec) {
23
24
  if (executions.phase !== "open")
24
- throw Error("CLOSED");
25
+ throw Error(ERRORS.CLOSED);
25
26
  if (!exec.agent)
26
- throw Error("OWNING_SESSION_REQUIRED");
27
+ throw Error(ERRORS.OWNING_SESSION_REQUIRED);
27
28
  if (!raw ||
28
29
  typeof raw !== "object" ||
29
30
  Array.isArray(raw) ||
30
31
  Reflect.ownKeys(raw).length)
31
- throw Error("INVALID_STATE_QUERY");
32
+ throw Error(ERRORS.INVALID_STATE_QUERY);
32
33
  return bus.inspect({
33
34
  sessionId: String(exec.agent.id),
34
35
  callId: String(exec.callId),
@@ -45,21 +46,25 @@ export function apply(ctx) {
45
46
  },
46
47
  execute(raw, exec) {
47
48
  if (executions.phase !== "open")
48
- return Promise.reject(Error("CLOSED"));
49
+ return Promise.reject(Error(ERRORS.CLOSED));
49
50
  return executions.track(dispatch(raw, exec));
50
51
  },
51
52
  });
52
53
  async function dispatch(raw, exec) {
53
54
  if (!exec.agent)
54
- throw Error("OWNING_SESSION_REQUIRED");
55
+ throw Error(ERRORS.OWNING_SESSION_REQUIRED);
56
+ const agent = exec.agent;
55
57
  const owner = {
56
- sessionId: String(exec.agent.id),
58
+ sessionId: String(agent.id),
57
59
  callId: String(exec.callId),
58
- }, m = parse(raw);
60
+ };
61
+ const m = parse(raw);
59
62
  if (m.type !== "request")
60
63
  return bus.command(owner, m);
61
64
  let projection;
62
- const attached = bus.submit(owner, m, (event) => projection?.progress(event), async (input, signal) => {
65
+ const attached = bus.submit(owner, m, (event) => projection?.progress(event),
66
+ // 宿主 read 能力:出队时重新进入宿主工具管线,沿用宿主权限与工作目录。
67
+ async (input, signal) => {
63
68
  const result = await ctx.tools.execute({
64
69
  name: "read",
65
70
  callId: (String(exec.callId) +
@@ -67,43 +72,62 @@ export function apply(ctx) {
67
72
  randomUUID()),
68
73
  rootCallId: exec.rootCallId,
69
74
  parent: exec.token,
70
- agent: exec.agent,
75
+ agent,
71
76
  arguments: input,
72
77
  signal,
73
78
  });
74
79
  if (result.isError)
75
- throw Error("HOST_READ_FAILED");
80
+ throw Error(ERRORS.HOST_READ_FAILED);
76
81
  return result.value;
77
82
  });
78
- // Terminal reuse is a read of an existing result, not admission of new work.
79
- if (attached.accepted.payload.state !== "queued" &&
80
- attached.accepted.payload.state !== "running") {
81
- try {
82
- const value = await attached.done;
83
- attached.returned();
84
- return value;
85
- }
86
- finally {
87
- attached.detach();
88
- }
83
+ // 1) 终态复用:已有执行直接读结果,不是新工作受理、不再申请 Job。
84
+ const terminal = await readTerminal(attached);
85
+ if (terminal)
86
+ return terminal;
87
+ // 2) 新执行:取得或创建宿主 Job 投影。
88
+ const execution = await projectExecution(owner, m, attached, agent.id, (handle) => (projection = handle));
89
+ // 3) 挂接取消生命周期,等待投影结算后返回执行快照。
90
+ return await awaitProjection(exec, attached, execution);
91
+ }
92
+ // —— 三个职责单一的拆分函数(闭包内共享 ctx/bus/executions) ——
93
+ /** accepted 已是终态时直接返回现有结果;否则返回 undefined 走新执行路径。 */
94
+ async function readTerminal(attached) {
95
+ const state = attached.accepted.payload.state;
96
+ if (state === "queued" || state === "running")
97
+ return undefined;
98
+ try {
99
+ const value = await attached.done;
100
+ attached.returned();
101
+ return value;
89
102
  }
90
- const abort = () => attached.cancel();
103
+ finally {
104
+ attached.detach();
105
+ }
106
+ }
107
+ /** 命中已存在的共享投影则复用,否则创建投影;创建失败按 HOST_JOB_UNAVAILABLE 结算。 */
108
+ async function projectExecution(owner, m, attached, ownerId, onProjected) {
91
109
  const jobKey = JSON.stringify([owner.sessionId, m.requestId]);
92
- let execution = executions.get(jobKey);
93
- if (execution)
94
- attached.detach(); // only the first attachment projects progress into the shared Job
95
- if (!execution) {
96
- try {
97
- execution = executions.create(jobKey, ctx.jobs, exec.agent.id, attached);
98
- projection = execution;
99
- }
100
- catch {
101
- abort();
102
- await attached.done;
103
- attached.detach();
104
- throw Error("HOST_JOB_UNAVAILABLE");
105
- }
110
+ const existing = executions.get(jobKey);
111
+ if (existing) {
112
+ // 只让第一个附着把进度投影到共享 Job;后续附着立即解除。
113
+ attached.detach();
114
+ return existing;
115
+ }
116
+ try {
117
+ const created = executions.create(jobKey, ctx.jobs, ownerId, attached);
118
+ onProjected(created);
119
+ return created;
106
120
  }
121
+ catch {
122
+ attached.cancel();
123
+ await attached.done;
124
+ attached.detach();
125
+ throw Error(ERRORS.HOST_JOB_UNAVAILABLE);
126
+ }
127
+ }
128
+ /** 把宿主工具调用的取消映射到执行,并等待投影结算后返回快照。 */
129
+ async function awaitProjection(exec, attached, execution) {
130
+ const abort = () => attached.cancel();
107
131
  exec.signal.addEventListener("abort", abort, { once: true });
108
132
  if (exec.signal.aborted)
109
133
  abort();
package/dist/preset.js CHANGED
@@ -4,31 +4,33 @@ import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
4
4
  import { load } from "js-yaml";
5
5
  import { setTimeout as pause } from "node:timers/promises";
6
6
  import { PRODUCT } from "./product.js";
7
+ import { ERRORS } from "./error-codes.js";
7
8
  export const PRESET_ID = PRODUCT.presetId;
9
+ /** 校验派生源组合:拒绝已含总线,以及缺少公开基准地址的相对模块/include 子文件。 */
10
+ function validateDerivation(rows) {
11
+ for (const row of rows) {
12
+ if (row.id === "xiao-he-execution-bus" ||
13
+ row.name?.startsWith("@feng0531/dsh-execution-bus"))
14
+ throw Error(ERRORS.PTC_PRESET_ALREADY_CONTAINS_BUS);
15
+ // readDocument 不发布声明方的 baseUrl,不能把相对模块/include
16
+ // 静默重定位到 registrar 的不同上下文。
17
+ if (row.name?.startsWith(".") ||
18
+ row.name === "@deepseek-ai/cordis-plugin-include")
19
+ throw Error(ERRORS.PTC_PRESET_RELATIVE_BASE_UNSUPPORTED);
20
+ if (row.group && Array.isArray(row.config))
21
+ validateDerivation(row.config);
22
+ }
23
+ }
8
24
  /** Capture the host-declared composition without evaluating !!js or changing disabled rows. */
9
25
  export function derivePreset(content) {
10
26
  if (Buffer.byteLength(content) > 1048576)
11
- throw Error("PTC_PRESET_TOO_LARGE");
27
+ throw Error(ERRORS.PTC_PRESET_TOO_LARGE);
12
28
  const plugins = load(content, { schema: entryListSchema });
13
29
  const problem = entryListProblem(plugins);
14
30
  if (problem)
15
31
  throw Error("Invalid PTC preset: " + problem);
16
32
  const rows = plugins;
17
- const inspect = (entries) => {
18
- for (const row of entries) {
19
- if (row.id === "xiao-he-execution-bus" ||
20
- row.name?.startsWith("@feng0531/dsh-execution-bus"))
21
- throw Error("PTC_PRESET_ALREADY_CONTAINS_BUS");
22
- // readDocument does not publish the declaring baseUrl. Do not silently
23
- // rebase relative modules/includes into the registrar's different context.
24
- if (row.name?.startsWith(".") ||
25
- row.name === "@deepseek-ai/cordis-plugin-include")
26
- throw Error("PTC_PRESET_RELATIVE_BASE_UNSUPPORTED");
27
- if (row.group && Array.isArray(row.config))
28
- inspect(row.config);
29
- }
30
- };
31
- inspect(rows);
33
+ validateDerivation(rows);
32
34
  return {
33
35
  id: PRESET_ID,
34
36
  name: PRODUCT.name,
@@ -68,7 +70,7 @@ export default class XiaoHePreset {
68
70
  }
69
71
  }
70
72
  if (!document)
71
- throw Error("PTC_PRESET_UNAVAILABLE");
73
+ throw Error(ERRORS.PTC_PRESET_UNAVAILABLE);
72
74
  yield await this.ctx.agentPresets.register(derivePreset(document.content));
73
75
  }
74
76
  }
package/dist/product.d.ts CHANGED
@@ -18,7 +18,7 @@ export declare const ADMISSION_DEFAULTS: Readonly<{
18
18
  sessionQueueLimit: 16;
19
19
  maxQueueWaitMs: 120000;
20
20
  }>;
21
- export declare const LIMITS: Readonly<{
21
+ export declare const HANDLER_LIMITS: Readonly<{
22
22
  readLines: 100;
23
23
  readPathChars: 2048;
24
24
  diagnosticDelayMs: 30000;
package/dist/product.js CHANGED
@@ -18,7 +18,7 @@ export const ADMISSION_DEFAULTS = Object.freeze({
18
18
  sessionQueueLimit: 16,
19
19
  maxQueueWaitMs: 120000,
20
20
  });
21
- export const LIMITS = Object.freeze({
21
+ export const HANDLER_LIMITS = Object.freeze({
22
22
  readLines: 100,
23
23
  readPathChars: 2048,
24
24
  diagnosticDelayMs: 30000,