@zaalipro/dsh-workflows 0.1.0-rc.3

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.
Files changed (229) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +224 -0
  3. package/README.i18n.yaml +4 -0
  4. package/README.md +109 -0
  5. package/README.zh.md +109 -0
  6. package/cordis.patch.yml +10 -0
  7. package/docs/architecture.i18n.yaml +4 -0
  8. package/docs/architecture.md +177 -0
  9. package/docs/architecture.zh.md +177 -0
  10. package/docs/testing.i18n.yaml +4 -0
  11. package/docs/testing.md +125 -0
  12. package/docs/testing.zh.md +125 -0
  13. package/docs/user-guide.i18n.yaml +4 -0
  14. package/docs/user-guide.md +351 -0
  15. package/docs/user-guide.zh.md +351 -0
  16. package/lib/client-types/WorkflowMemberInspector.d.ts +28 -0
  17. package/lib/client-types/WorkflowMemberInspector.d.ts.map +1 -0
  18. package/lib/client-types/WorkflowMemberInspector.js +181 -0
  19. package/lib/client-types/WorkflowMemberInspector.js.map +1 -0
  20. package/lib/client-types/WorkflowRunPanel.d.ts +43 -0
  21. package/lib/client-types/WorkflowRunPanel.d.ts.map +1 -0
  22. package/lib/client-types/WorkflowRunPanel.js +130 -0
  23. package/lib/client-types/WorkflowRunPanel.js.map +1 -0
  24. package/lib/client-types/WorkflowsDashboard.d.ts +49 -0
  25. package/lib/client-types/WorkflowsDashboard.d.ts.map +1 -0
  26. package/lib/client-types/WorkflowsDashboard.js +942 -0
  27. package/lib/client-types/WorkflowsDashboard.js.map +1 -0
  28. package/lib/client-types/adapter.d.ts +60 -0
  29. package/lib/client-types/adapter.d.ts.map +1 -0
  30. package/lib/client-types/adapter.js +117 -0
  31. package/lib/client-types/adapter.js.map +1 -0
  32. package/lib/client-types/chat-renderer.d.ts +10 -0
  33. package/lib/client-types/chat-renderer.d.ts.map +1 -0
  34. package/lib/client-types/chat-renderer.js +11 -0
  35. package/lib/client-types/chat-renderer.js.map +1 -0
  36. package/lib/client-types/contract.d.ts +210 -0
  37. package/lib/client-types/contract.d.ts.map +1 -0
  38. package/lib/client-types/contract.js +37 -0
  39. package/lib/client-types/contract.js.map +1 -0
  40. package/lib/client-types/controller.d.ts +83 -0
  41. package/lib/client-types/controller.d.ts.map +1 -0
  42. package/lib/client-types/controller.js +705 -0
  43. package/lib/client-types/controller.js.map +1 -0
  44. package/lib/client-types/index.d.ts +19 -0
  45. package/lib/client-types/index.d.ts.map +1 -0
  46. package/lib/client-types/index.js +730 -0
  47. package/lib/client-types/index.js.map +1 -0
  48. package/lib/client-types/locales.d.ts +257 -0
  49. package/lib/client-types/locales.d.ts.map +1 -0
  50. package/lib/client-types/locales.js +270 -0
  51. package/lib/client-types/locales.js.map +1 -0
  52. package/lib/client-types/slot-components.d.ts +38 -0
  53. package/lib/client-types/slot-components.d.ts.map +1 -0
  54. package/lib/client-types/slot-components.js +22 -0
  55. package/lib/client-types/slot-components.js.map +1 -0
  56. package/lib/client-types/store.d.ts +69 -0
  57. package/lib/client-types/store.d.ts.map +1 -0
  58. package/lib/client-types/store.js +135 -0
  59. package/lib/client-types/store.js.map +1 -0
  60. package/lib/client-types/workflow-definition.d.ts +52 -0
  61. package/lib/client-types/workflow-definition.d.ts.map +1 -0
  62. package/lib/client-types/workflow-definition.js +135 -0
  63. package/lib/client-types/workflow-definition.js.map +1 -0
  64. package/lib/client.js +11064 -0
  65. package/lib/client.js.map +1 -0
  66. package/lib/compat-engine/index.js +1926 -0
  67. package/lib/compat-engine/index.js.map +1 -0
  68. package/lib/compat-engine/worker.cjs +1500 -0
  69. package/lib/compat-engine/worker.cjs.map +1 -0
  70. package/lib/typert.host.d.ts +3 -0
  71. package/lib/typert.host.js +1709 -0
  72. package/lib/typert.remote-client.d.ts +57 -0
  73. package/lib/typert.remote-client.js +1662 -0
  74. package/lib/types/commands/aliases.d.ts +4 -0
  75. package/lib/types/commands/aliases.d.ts.map +1 -0
  76. package/lib/types/commands/aliases.js +25 -0
  77. package/lib/types/commands/aliases.js.map +1 -0
  78. package/lib/types/commands/index.d.ts +66 -0
  79. package/lib/types/commands/index.d.ts.map +1 -0
  80. package/lib/types/commands/index.js +550 -0
  81. package/lib/types/commands/index.js.map +1 -0
  82. package/lib/types/commands/parser.d.ts +17 -0
  83. package/lib/types/commands/parser.d.ts.map +1 -0
  84. package/lib/types/commands/parser.js +45 -0
  85. package/lib/types/commands/parser.js.map +1 -0
  86. package/lib/types/config.d.ts +71 -0
  87. package/lib/types/config.d.ts.map +1 -0
  88. package/lib/types/config.js +109 -0
  89. package/lib/types/config.js.map +1 -0
  90. package/lib/types/index.d.ts +41 -0
  91. package/lib/types/index.d.ts.map +1 -0
  92. package/lib/types/index.js +483 -0
  93. package/lib/types/index.js.map +1 -0
  94. package/lib/types/invariant.d.ts +21 -0
  95. package/lib/types/invariant.d.ts.map +1 -0
  96. package/lib/types/invariant.js +85 -0
  97. package/lib/types/invariant.js.map +1 -0
  98. package/lib/types/registry/definition.d.ts +24 -0
  99. package/lib/types/registry/definition.d.ts.map +1 -0
  100. package/lib/types/registry/definition.js +189 -0
  101. package/lib/types/registry/definition.js.map +1 -0
  102. package/lib/types/registry/index.d.ts +100 -0
  103. package/lib/types/registry/index.d.ts.map +1 -0
  104. package/lib/types/registry/index.js +899 -0
  105. package/lib/types/registry/index.js.map +1 -0
  106. package/lib/types/registry/names.d.ts +19 -0
  107. package/lib/types/registry/names.d.ts.map +1 -0
  108. package/lib/types/registry/names.js +69 -0
  109. package/lib/types/registry/names.js.map +1 -0
  110. package/lib/types/registry/remote.d.ts +15 -0
  111. package/lib/types/registry/remote.d.ts.map +1 -0
  112. package/lib/types/registry/remote.js +128 -0
  113. package/lib/types/registry/remote.js.map +1 -0
  114. package/lib/types/registry/roots.d.ts +44 -0
  115. package/lib/types/registry/roots.d.ts.map +1 -0
  116. package/lib/types/registry/roots.js +118 -0
  117. package/lib/types/registry/roots.js.map +1 -0
  118. package/lib/types/registry/types.d.ts +62 -0
  119. package/lib/types/registry/types.d.ts.map +1 -0
  120. package/lib/types/registry/types.js +2 -0
  121. package/lib/types/registry/types.js.map +1 -0
  122. package/lib/types/registry/watchers.d.ts +70 -0
  123. package/lib/types/registry/watchers.d.ts.map +1 -0
  124. package/lib/types/registry/watchers.js +152 -0
  125. package/lib/types/registry/watchers.js.map +1 -0
  126. package/lib/types/remote-events.d.ts +42 -0
  127. package/lib/types/remote-events.d.ts.map +1 -0
  128. package/lib/types/remote-events.js +21 -0
  129. package/lib/types/remote-events.js.map +1 -0
  130. package/lib/types/run-recorder.d.ts +44 -0
  131. package/lib/types/run-recorder.d.ts.map +1 -0
  132. package/lib/types/run-recorder.js +356 -0
  133. package/lib/types/run-recorder.js.map +1 -0
  134. package/lib/types/supervisor/canned-validate.d.ts +25 -0
  135. package/lib/types/supervisor/canned-validate.d.ts.map +1 -0
  136. package/lib/types/supervisor/canned-validate.js +122 -0
  137. package/lib/types/supervisor/canned-validate.js.map +1 -0
  138. package/lib/types/supervisor/completion-notice.d.ts +49 -0
  139. package/lib/types/supervisor/completion-notice.d.ts.map +1 -0
  140. package/lib/types/supervisor/completion-notice.js +281 -0
  141. package/lib/types/supervisor/completion-notice.js.map +1 -0
  142. package/lib/types/supervisor/cursors.d.ts +68 -0
  143. package/lib/types/supervisor/cursors.d.ts.map +1 -0
  144. package/lib/types/supervisor/cursors.js +158 -0
  145. package/lib/types/supervisor/cursors.js.map +1 -0
  146. package/lib/types/supervisor/engine-compat.d.ts +48 -0
  147. package/lib/types/supervisor/engine-compat.d.ts.map +1 -0
  148. package/lib/types/supervisor/engine-compat.js +116 -0
  149. package/lib/types/supervisor/engine-compat.js.map +1 -0
  150. package/lib/types/supervisor/index.d.ts +183 -0
  151. package/lib/types/supervisor/index.d.ts.map +1 -0
  152. package/lib/types/supervisor/index.js +2040 -0
  153. package/lib/types/supervisor/index.js.map +1 -0
  154. package/lib/types/supervisor/parallel-compat.d.ts +36 -0
  155. package/lib/types/supervisor/parallel-compat.d.ts.map +1 -0
  156. package/lib/types/supervisor/parallel-compat.js +180 -0
  157. package/lib/types/supervisor/parallel-compat.js.map +1 -0
  158. package/lib/types/supervisor/remote.d.ts +25 -0
  159. package/lib/types/supervisor/remote.d.ts.map +1 -0
  160. package/lib/types/supervisor/remote.js +478 -0
  161. package/lib/types/supervisor/remote.js.map +1 -0
  162. package/lib/types/supervisor/storage/bounded-file.d.ts +41 -0
  163. package/lib/types/supervisor/storage/bounded-file.d.ts.map +1 -0
  164. package/lib/types/supervisor/storage/bounded-file.js +186 -0
  165. package/lib/types/supervisor/storage/bounded-file.js.map +1 -0
  166. package/lib/types/supervisor/storage/details-codec.d.ts +14 -0
  167. package/lib/types/supervisor/storage/details-codec.d.ts.map +1 -0
  168. package/lib/types/supervisor/storage/details-codec.js +359 -0
  169. package/lib/types/supervisor/storage/details-codec.js.map +1 -0
  170. package/lib/types/supervisor/storage/index.d.ts +26 -0
  171. package/lib/types/supervisor/storage/index.d.ts.map +1 -0
  172. package/lib/types/supervisor/storage/index.js +100 -0
  173. package/lib/types/supervisor/storage/index.js.map +1 -0
  174. package/lib/types/supervisor/storage/lease.d.ts +34 -0
  175. package/lib/types/supervisor/storage/lease.d.ts.map +1 -0
  176. package/lib/types/supervisor/storage/lease.js +289 -0
  177. package/lib/types/supervisor/storage/lease.js.map +1 -0
  178. package/lib/types/supervisor/storage/manifest-codec.d.ts +10 -0
  179. package/lib/types/supervisor/storage/manifest-codec.d.ts.map +1 -0
  180. package/lib/types/supervisor/storage/manifest-codec.js +335 -0
  181. package/lib/types/supervisor/storage/manifest-codec.js.map +1 -0
  182. package/lib/types/supervisor/storage/manifest-store.d.ts +99 -0
  183. package/lib/types/supervisor/storage/manifest-store.d.ts.map +1 -0
  184. package/lib/types/supervisor/storage/manifest-store.js +1606 -0
  185. package/lib/types/supervisor/storage/manifest-store.js.map +1 -0
  186. package/lib/types/supervisor/storage/manifest-types.d.ts +269 -0
  187. package/lib/types/supervisor/storage/manifest-types.d.ts.map +1 -0
  188. package/lib/types/supervisor/storage/manifest-types.js +2 -0
  189. package/lib/types/supervisor/storage/manifest-types.js.map +1 -0
  190. package/lib/types/supervisor/storage/private-root.d.ts +143 -0
  191. package/lib/types/supervisor/storage/private-root.d.ts.map +1 -0
  192. package/lib/types/supervisor/storage/private-root.js +860 -0
  193. package/lib/types/supervisor/storage/private-root.js.map +1 -0
  194. package/lib/types/supervisor/storage/recovery.d.ts +14 -0
  195. package/lib/types/supervisor/storage/recovery.d.ts.map +1 -0
  196. package/lib/types/supervisor/storage/recovery.js +126 -0
  197. package/lib/types/supervisor/storage/recovery.js.map +1 -0
  198. package/lib/types/supervisor/storage/run-files.d.ts +67 -0
  199. package/lib/types/supervisor/storage/run-files.d.ts.map +1 -0
  200. package/lib/types/supervisor/storage/run-files.js +346 -0
  201. package/lib/types/supervisor/storage/run-files.js.map +1 -0
  202. package/lib/types/supervisor/types.d.ts +295 -0
  203. package/lib/types/supervisor/types.d.ts.map +1 -0
  204. package/lib/types/supervisor/types.js +3 -0
  205. package/lib/types/supervisor/types.js.map +1 -0
  206. package/lib/types/supervisor/value-view.d.ts +19 -0
  207. package/lib/types/supervisor/value-view.d.ts.map +1 -0
  208. package/lib/types/supervisor/value-view.js +183 -0
  209. package/lib/types/supervisor/value-view.js.map +1 -0
  210. package/lib/types/tool/index.d.ts +86 -0
  211. package/lib/types/tool/index.d.ts.map +1 -0
  212. package/lib/types/tool/index.js +810 -0
  213. package/lib/types/tool/index.js.map +1 -0
  214. package/lib/types/tool/schema.d.ts +158 -0
  215. package/lib/types/tool/schema.d.ts.map +1 -0
  216. package/lib/types/tool/schema.js +137 -0
  217. package/lib/types/tool/schema.js.map +1 -0
  218. package/lib/types/types.d.ts +9 -0
  219. package/lib/types/types.d.ts.map +1 -0
  220. package/lib/types/types.js +3 -0
  221. package/lib/types/types.js.map +1 -0
  222. package/lib/types/user-questions.d.ts +9 -0
  223. package/lib/types/user-questions.d.ts.map +1 -0
  224. package/lib/types/user-questions.js +91 -0
  225. package/lib/types/user-questions.js.map +1 -0
  226. package/package.json +273 -0
  227. package/skills/create-workflow/SKILL.md +166 -0
  228. package/vendor/workflow-engine/LICENSE +21 -0
  229. package/vendor/workflow-engine/README.md +1 -0
@@ -0,0 +1,1926 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { availableParallelism, tmpdir } from "node:os";
3
+ import * as vm from "node:vm";
4
+ import z from "@deepseek-ai/schemastery";
5
+ import { WorkflowError, WorkflowRunId } from "@deepseek-ai/dsh-workflow";
6
+ import { snapshotJsonValue } from "@deepseek-ai/dsh-session";
7
+ import { Buffer as Buffer$1 } from "node:buffer";
8
+ import { constants } from "node:fs";
9
+ import { chmod, link, lstat, mkdir, open, readdir, rename, rm } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { Worker } from "node:worker_threads";
12
+ import { fileURLToPath } from "node:url";
13
+ import { assertObjectJsonSchema } from "@deepseek-ai/dsh-tools";
14
+ //#region vendor/workflow-engine/compat-seam.ts
15
+ /** Local extensions deliberately absent from the official 0.1.1-rc.2 seam. */
16
+ const WorkflowError$1 = WorkflowError;
17
+ //#endregion
18
+ //#region vendor/workflow-engine/realm.ts
19
+ /**
20
+ * Materializes values leaving the script vm into plain JSON before they cross the worker
21
+ * boundary, and renders thrown script values without rejecting the run. The walk rejects
22
+ * values that JSON cannot preserve but trusts model-written workflow scripts: getters and proxy traps may
23
+ * run, and the vm is not a security boundary. The worker provides host-loop isolation and
24
+ * forced termination, not hostile-value containment. See
25
+ * .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md for the isolation rationale.
26
+ * @module @deepseek-ai/dsh-workflow-worker-thread/realm
27
+ */
28
+ /** Thrown by {@link materializeFromRealm}; the caller wraps it into the right `WorkflowError` code. */
29
+ var MaterializeError = class extends Error {
30
+ constructor(path, reason) {
31
+ super(`${path}: ${reason}`);
32
+ this.path = path;
33
+ this.reason = reason;
34
+ this.name = "MaterializeError";
35
+ }
36
+ };
37
+ /**
38
+ * Render a thrown value to failure text without ever throwing: prefer the
39
+ * `stack` (host or realm — a realm error's `stack` is a plain string read),
40
+ * fall back to `message`, then `String()`. Reading those properties MAY run
41
+ * script code (a getter, `toString`) — accepted under the module's trust
42
+ * premise; if that code itself throws, a fixed label is returned instead.
43
+ * @param error - any value thrown in the host or worker realm.
44
+ * @returns human-readable text for the failure report; prefers the stack.
45
+ */
46
+ function renderThrown(error) {
47
+ try {
48
+ const stack = error?.stack;
49
+ if (typeof stack === "string" && stack.length > 0) return stack;
50
+ const message = error?.message;
51
+ if (typeof message === "string" && message.length > 0) return message;
52
+ return String(error);
53
+ } catch {
54
+ return "[unrenderable thrown value]";
55
+ }
56
+ }
57
+ /**
58
+ * Whether an object's prototype chain represents a plain data object: `null`, or a prototype
59
+ * whose own prototype is `null` (the realm's `Object.prototype` — which we
60
+ * cannot compare by identity across realms). A `Date`/`Map`/class instance
61
+ * has a longer chain and is rejected.
62
+ */
63
+ function hasPlainPrototype(value) {
64
+ const proto = Object.getPrototypeOf(value);
65
+ if (proto === null) return true;
66
+ return Object.getPrototypeOf(proto) === null;
67
+ }
68
+ /**
69
+ * Copy `value` (typically from the vm realm) into plain host JSON data. Root `undefined` is
70
+ * returned unchanged; nested `undefined` and values JSON cannot represent losslessly fail
71
+ * with the offending path. Property accessors run normally, and a throwing read is wrapped
72
+ * with its rendered failure.
73
+ *
74
+ * @param value - the realm value to materialize.
75
+ * @param root - the path label for the root value (error messages).
76
+ * @returns the host-realm copy (plain objects/arrays/scalars only).
77
+ * @throws {@link MaterializeError} for unsupported values, cycles, sparse arrays, exotic
78
+ * prototypes, or property reads that throw.
79
+ */
80
+ function materializeFromRealm(value, root = "value") {
81
+ if (value === void 0) return void 0;
82
+ try {
83
+ return materialize(value, root, /* @__PURE__ */ new Set());
84
+ } catch (error) {
85
+ if (error instanceof MaterializeError) throw error;
86
+ throw new MaterializeError(root, `reading the value threw: ${renderThrown(error)}`);
87
+ }
88
+ }
89
+ function materialize(value, path, seen) {
90
+ switch (typeof value) {
91
+ case "boolean":
92
+ case "string": return value;
93
+ case "number":
94
+ if (!Number.isFinite(value)) throw new MaterializeError(path, "non-finite numbers are not JSON data");
95
+ return value;
96
+ case "bigint": throw new MaterializeError(path, "bigints are not JSON data");
97
+ case "function": throw new MaterializeError(path, "functions are not plain JSON data");
98
+ case "symbol": throw new MaterializeError(path, "symbols are not plain JSON data");
99
+ case "undefined": throw new MaterializeError(path, "undefined is not JSON data");
100
+ }
101
+ if (value === null) return null;
102
+ const objectValue = value;
103
+ if (seen.has(objectValue)) throw new MaterializeError(path, "circular references are not JSON data");
104
+ seen.add(objectValue);
105
+ try {
106
+ if (Array.isArray(objectValue)) return materializeArray(objectValue, path, seen);
107
+ return materializeObject(objectValue, path, seen);
108
+ } finally {
109
+ seen.delete(objectValue);
110
+ }
111
+ }
112
+ function materializeArray(value, path, seen) {
113
+ const out = [];
114
+ for (let index = 0; index < value.length; index++) {
115
+ if (!(index in value)) throw new MaterializeError(`${path}[${index}]`, "sparse arrays are not JSON data");
116
+ out.push(materialize(value[index], `${path}[${index}]`, seen));
117
+ }
118
+ for (const key of Object.keys(value)) {
119
+ const index = Number(key);
120
+ if (!Number.isInteger(index) || index < 0 || index >= value.length) throw new MaterializeError(`${path}.${key}`, "arrays with non-index properties are not JSON data");
121
+ }
122
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new MaterializeError(path, "symbol-keyed properties are not plain JSON data");
123
+ return out;
124
+ }
125
+ function materializeObject(value, path, seen) {
126
+ if (!hasPlainPrototype(value)) throw new MaterializeError(path, "only plain objects and arrays are JSON data (exotic prototype)");
127
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new MaterializeError(path, "symbol-keyed properties are not plain JSON data");
128
+ const out = {};
129
+ for (const key of Object.keys(value)) Object.defineProperty(out, key, {
130
+ value: materialize(value[key], `${path}.${key}`, seen),
131
+ enumerable: true,
132
+ writable: true,
133
+ configurable: true
134
+ });
135
+ return out;
136
+ }
137
+ //#endregion
138
+ //#region vendor/workflow-engine/protocol.ts
139
+ /**
140
+ * The host⇄worker wire protocol: one string-valued enum of message tags per direction, a
141
+ * payload map giving each tag its parameters (the single source of truth), and the message
142
+ * unions derived from them. Payloads are plain JSON by construction for structured clone. Both
143
+ * directions are closed engine protocols. The host runtime-decodes the worker direction before
144
+ * dispatch; generic typed senders make tag/payload mismatches compile-time errors rather than
145
+ * silently skipped messages.
146
+ * @module @deepseek-ai/dsh-workflow-worker-thread/protocol
147
+ */
148
+ /** A worker message failed the runtime protocol checks at the untrusted thread boundary. */
149
+ var WorkflowProtocolError = class extends Error {
150
+ constructor(detail) {
151
+ super(`invalid workflow worker message: ${detail}`);
152
+ this.name = "WorkflowProtocolError";
153
+ }
154
+ };
155
+ /** Convert one JSON snapshot to a record or reject it with a path-qualified diagnostic. */
156
+ function record(value, path) {
157
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new WorkflowProtocolError(`${path} must be an object`);
158
+ return value;
159
+ }
160
+ /** Reject missing, extra, or inherited wire fields. */
161
+ function fields(value, path, required, optional = []) {
162
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
163
+ for (const key of required) if (!Object.hasOwn(value, key)) throw new WorkflowProtocolError(`${path}.${key} is required`);
164
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new WorkflowProtocolError(`${path}.${key} is not recognized`);
165
+ }
166
+ function string(value, path, empty = true) {
167
+ if (typeof value !== "string" || !empty && value.length === 0) throw new WorkflowProtocolError(`${path} must be ${empty ? "a string" : "a non-empty string"}`);
168
+ return value;
169
+ }
170
+ function integer(value, path, minimum) {
171
+ if (!Number.isSafeInteger(value) || value < minimum) throw new WorkflowProtocolError(`${path} must be a safe integer no less than ${minimum}`);
172
+ return value;
173
+ }
174
+ function optionalString(value, key, path) {
175
+ return value[key] === void 0 ? void 0 : string(value[key], `${path}.${key}`);
176
+ }
177
+ function agentInfo(value, path) {
178
+ const info = record(value, path);
179
+ fields(info, path, [
180
+ "seq",
181
+ "label",
182
+ "childId"
183
+ ], ["phase"]);
184
+ const phase = optionalString(info, "phase", path);
185
+ return {
186
+ seq: integer(info.seq, `${path}.seq`, 1),
187
+ label: string(info.label, `${path}.label`),
188
+ ...phase === void 0 ? {} : { phase },
189
+ childId: string(info.childId, `${path}.childId`, false)
190
+ };
191
+ }
192
+ function agentEndInfo(value) {
193
+ const info = record(value, "message.info");
194
+ fields(info, "message.info", [
195
+ "seq",
196
+ "label",
197
+ "childId",
198
+ "outcome"
199
+ ], ["phase"]);
200
+ if (info.outcome !== "completed" && info.outcome !== "failed" && info.outcome !== "cancelled") throw new WorkflowProtocolError("message.info.outcome is not recognized");
201
+ const phase = optionalString(info, "phase", "message.info");
202
+ return {
203
+ seq: integer(info.seq, "message.info.seq", 1),
204
+ label: string(info.label, "message.info.label"),
205
+ ...phase === void 0 ? {} : { phase },
206
+ childId: string(info.childId, "message.info.childId", false),
207
+ outcome: info.outcome
208
+ };
209
+ }
210
+ function gateInfo(value) {
211
+ const gate = record(value, "message.gate");
212
+ fields(gate, "message.gate", [
213
+ "kind",
214
+ "message",
215
+ "resumable"
216
+ ]);
217
+ if (gate.kind !== "user" && gate.kind !== "back_off" && gate.kind !== "no_progress" && gate.kind !== "verification" && gate.kind !== "infra") throw new WorkflowProtocolError("message.gate.kind is not recognized");
218
+ if (typeof gate.resumable !== "boolean") throw new WorkflowProtocolError("message.gate.resumable must be a boolean");
219
+ return {
220
+ kind: gate.kind,
221
+ message: string(gate.message, "message.gate.message"),
222
+ resumable: gate.resumable
223
+ };
224
+ }
225
+ function childStartRequest(value) {
226
+ const request = record(value, "message.request");
227
+ fields(request, "message.request", ["prompt"], [
228
+ "schema",
229
+ "provider",
230
+ "model"
231
+ ]);
232
+ const schema = request.schema;
233
+ if (schema !== void 0) try {
234
+ assertObjectJsonSchema(schema);
235
+ } catch (error) {
236
+ throw new WorkflowProtocolError(`message.request.schema is unsupported: ${String(error)}`);
237
+ }
238
+ const provider = optionalString(request, "provider", "message.request");
239
+ const model = optionalString(request, "model", "message.request");
240
+ return {
241
+ prompt: string(request.prompt, "message.request.prompt", false),
242
+ ...schema === void 0 ? {} : { schema },
243
+ ...provider === void 0 ? {} : { provider },
244
+ ...model === void 0 ? {} : { model }
245
+ };
246
+ }
247
+ function workflowResult(value) {
248
+ const result = record(value, "message.result");
249
+ fields(result, "message.result", [
250
+ "value",
251
+ "stopReason",
252
+ "agentsStarted"
253
+ ], ["error", "errorCode"]);
254
+ if (result.stopReason !== "completed" && result.stopReason !== "cancelled" && result.stopReason !== "error") throw new WorkflowProtocolError("message.result.stopReason is not recognized");
255
+ const error = optionalString(result, "error", "message.result");
256
+ const errorCode = result.errorCode === void 0 ? void 0 : workflowErrorCode(result.errorCode, "message.result.errorCode");
257
+ if (result.stopReason === "completed" && (error !== void 0 || errorCode !== void 0)) throw new WorkflowProtocolError("message.result.error and errorCode are forbidden for a completed result");
258
+ if (result.stopReason !== "completed" && error === void 0) throw new WorkflowProtocolError("message.result.error is required for a non-completed result");
259
+ return {
260
+ value: result.value,
261
+ stopReason: result.stopReason,
262
+ ...error === void 0 ? {} : { error },
263
+ ...errorCode === void 0 ? {} : { errorCode },
264
+ agentsStarted: integer(result.agentsStarted, "message.result.agentsStarted", 0)
265
+ };
266
+ }
267
+ /** Compile-time-exhaustive runtime table for the seam's closed failure taxonomy. */
268
+ const WORKFLOW_ERROR_CODES = {
269
+ SCRIPT_PARSE: "SCRIPT_PARSE",
270
+ META_INVALID: "META_INVALID",
271
+ INVALID_ARGUMENT: "INVALID_ARGUMENT",
272
+ UNSUPPORTED_OPTION: "UNSUPPORTED_OPTION",
273
+ UNSUPPORTED_SCHEMA: "UNSUPPORTED_SCHEMA",
274
+ AGENT_CAP: "AGENT_CAP",
275
+ ITEM_CAP: "ITEM_CAP",
276
+ AGENT_START: "AGENT_START",
277
+ AGENT_RESULT: "AGENT_RESULT",
278
+ JOURNAL_DIVERGENCE: "JOURNAL_DIVERGENCE",
279
+ RESULT_UNSERIALIZABLE: "RESULT_UNSERIALIZABLE",
280
+ CANCELLED: "CANCELLED"
281
+ };
282
+ /** Decode one machine-routable workflow failure code. */
283
+ function workflowErrorCode(value, path) {
284
+ if (typeof value !== "string" || !Object.hasOwn(WORKFLOW_ERROR_CODES, value)) throw new WorkflowProtocolError(`${path} is not recognized`);
285
+ return WORKFLOW_ERROR_CODES[value];
286
+ }
287
+ function journalEntry(value) {
288
+ const entry = record(value, "message.entry");
289
+ const kind = string(entry.kind, "message.entry.kind", false);
290
+ const fingerprint = string(entry.fingerprint, "message.entry.fingerprint", false);
291
+ if (!/^[a-f0-9]{64}$/u.test(fingerprint)) throw new WorkflowProtocolError("message.entry.fingerprint must be a lowercase SHA-256 digest");
292
+ const base = {
293
+ ordinal: integer(entry.ordinal, "message.entry.ordinal", 1),
294
+ callId: journalCallId(entry.callId),
295
+ fingerprint
296
+ };
297
+ switch (kind) {
298
+ case "agent":
299
+ fields(entry, "message.entry", [
300
+ "kind",
301
+ "ordinal",
302
+ "callId",
303
+ "fingerprint",
304
+ "seq",
305
+ "result"
306
+ ]);
307
+ return {
308
+ ...base,
309
+ kind,
310
+ seq: integer(entry.seq, "message.entry.seq", 1),
311
+ result: entry.result
312
+ };
313
+ case "phase":
314
+ fields(entry, "message.entry", [
315
+ "kind",
316
+ "ordinal",
317
+ "callId",
318
+ "fingerprint",
319
+ "title"
320
+ ]);
321
+ return {
322
+ ...base,
323
+ kind,
324
+ title: string(entry.title, "message.entry.title", false)
325
+ };
326
+ case "log":
327
+ fields(entry, "message.entry", [
328
+ "kind",
329
+ "ordinal",
330
+ "callId",
331
+ "fingerprint",
332
+ "message"
333
+ ]);
334
+ return {
335
+ ...base,
336
+ kind,
337
+ message: string(entry.message, "message.entry.message")
338
+ };
339
+ case "scratch-write":
340
+ case "await-user":
341
+ fields(entry, "message.entry", [
342
+ "kind",
343
+ "ordinal",
344
+ "callId",
345
+ "fingerprint"
346
+ ]);
347
+ return {
348
+ ...base,
349
+ kind
350
+ };
351
+ case "scratch-read": {
352
+ fields(entry, "message.entry", [
353
+ "kind",
354
+ "ordinal",
355
+ "callId",
356
+ "fingerprint"
357
+ ], ["content"]);
358
+ const content = optionalString(entry, "content", "message.entry");
359
+ return {
360
+ ...base,
361
+ kind,
362
+ ...content === void 0 ? {} : { content }
363
+ };
364
+ }
365
+ default: throw new WorkflowProtocolError("message.entry.kind is not recognized");
366
+ }
367
+ }
368
+ /** Decode one non-empty deterministic positive-integer call path. */
369
+ function journalCallId(value) {
370
+ if (!Array.isArray(value) || value.length === 0) throw new WorkflowProtocolError("message.entry.callId must be a non-empty integer array");
371
+ return value.map((part, index) => integer(part, `message.entry.callId[${index}]`, 1));
372
+ }
373
+ /**
374
+ * Decode and detach one worker→host message before any observer, filesystem,
375
+ * or subagent side effect. The worker is model-script controlled after a VM
376
+ * escape, so TypeScript's protocol union is not evidence at this boundary.
377
+ * @param value - raw structured-clone payload received from the Worker.
378
+ * @param maxBytes - maximum UTF-8 JSON size accepted for one frame.
379
+ * @returns a detached, strictly validated protocol message.
380
+ */
381
+ function decodeWorkerToHostMessage(value, maxBytes = Number.MAX_SAFE_INTEGER) {
382
+ let detached;
383
+ try {
384
+ detached = snapshotJsonValue(value);
385
+ } catch (error) {
386
+ throw new WorkflowProtocolError(`message could not be read as JSON data: ${String(error)}`);
387
+ }
388
+ if (detached === void 0) throw new WorkflowProtocolError("message must be lossless JSON data");
389
+ const encoded = JSON.stringify(detached);
390
+ if (Buffer$1.byteLength(encoded, "utf8") > maxBytes) throw new WorkflowProtocolError(`message exceeds the ${maxBytes}-byte protocol limit`);
391
+ const message = record(detached, "message");
392
+ const type = string(message.type, "message.type", false);
393
+ switch (type) {
394
+ case "ready":
395
+ fields(message, "message", ["type"]);
396
+ return { type };
397
+ case "phase":
398
+ fields(message, "message", ["type", "title"]);
399
+ return {
400
+ type,
401
+ title: string(message.title, "message.title", false)
402
+ };
403
+ case "log":
404
+ fields(message, "message", ["type", "message"]);
405
+ return {
406
+ type,
407
+ message: string(message.message, "message.message")
408
+ };
409
+ case "agent-start":
410
+ fields(message, "message", ["type", "info"]);
411
+ return {
412
+ type,
413
+ info: agentInfo(message.info, "message.info")
414
+ };
415
+ case "agent-end":
416
+ fields(message, "message", ["type", "info"]);
417
+ return {
418
+ type,
419
+ info: agentEndInfo(message.info)
420
+ };
421
+ case "gate":
422
+ fields(message, "message", ["type", "gate"]);
423
+ return {
424
+ type,
425
+ gate: gateInfo(message.gate)
426
+ };
427
+ case "journal-commit":
428
+ fields(message, "message", ["type", "entry"]);
429
+ return {
430
+ type,
431
+ entry: journalEntry(message.entry)
432
+ };
433
+ case "child-start":
434
+ fields(message, "message", [
435
+ "type",
436
+ "callId",
437
+ "request"
438
+ ]);
439
+ return {
440
+ type,
441
+ callId: integer(message.callId, "message.callId", 1),
442
+ request: childStartRequest(message.request)
443
+ };
444
+ case "child-dispose":
445
+ fields(message, "message", ["type", "callId"]);
446
+ return {
447
+ type,
448
+ callId: integer(message.callId, "message.callId", 1)
449
+ };
450
+ case "scratch-write":
451
+ fields(message, "message", [
452
+ "type",
453
+ "callId",
454
+ "name",
455
+ "content"
456
+ ]);
457
+ return {
458
+ type,
459
+ callId: integer(message.callId, "message.callId", 1),
460
+ name: string(message.name, "message.name", false),
461
+ content: string(message.content, "message.content")
462
+ };
463
+ case "scratch-read":
464
+ fields(message, "message", [
465
+ "type",
466
+ "callId",
467
+ "name"
468
+ ]);
469
+ return {
470
+ type,
471
+ callId: integer(message.callId, "message.callId", 1),
472
+ name: string(message.name, "message.name", false)
473
+ };
474
+ case "result":
475
+ fields(message, "message", ["type", "result"]);
476
+ return {
477
+ type,
478
+ result: workflowResult(message.result)
479
+ };
480
+ default: throw new WorkflowProtocolError(`message.type ${JSON.stringify(type)} is not recognized`);
481
+ }
482
+ }
483
+ //#endregion
484
+ //#region vendor/workflow-engine/host.ts
485
+ /**
486
+ * Host side of one workflow run. The first worker result, unexpected death, or
487
+ * cancellation-grace expiry owns settlement and closes message admission.
488
+ * Pending starts share one abort signal; published children share idempotent
489
+ * cleanup, and quiescence waits for both while synthesizing any missing end events.
490
+ * @module @deepseek-ai/dsh-workflow-worker-thread/host
491
+ */
492
+ /** Single-component scratch file name grammar (mirrors the worker-side validation). */
493
+ const SCRATCH_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
494
+ /**
495
+ * Build the credential-free environment inherited by a workflow worker.
496
+ * Windows receives only its absolute temp directory; source workers may also
497
+ * receive the tsx paths-map pin used by the source launcher.
498
+ * @param platform - host platform, overridable for tests.
499
+ * @param tsconfigPath - optional source-launcher tsconfig pin.
500
+ * @returns the environment passed to the Worker constructor.
501
+ */
502
+ function workerSpawnEnv(platform = process.platform, tsconfigPath) {
503
+ const env = {};
504
+ if (platform === "win32") {
505
+ const tmp = tmpdir();
506
+ env.TMP = tmp;
507
+ env.TEMP = tmp;
508
+ }
509
+ if (tsconfigPath !== void 0) env.TSX_TSCONFIG_PATH = tsconfigPath;
510
+ return env;
511
+ }
512
+ /** Resolve the built worker or source-mode tsx bootstrap for one isolated run. */
513
+ function resolveWorkerSpawn(init) {
514
+ /* v8 ignore next 3 -- built-output arm is exercised by the built-worker e2e */
515
+ if (!import.meta.url.endsWith(".ts")) return {
516
+ entry: fileURLToPath(new URL("./worker.cjs", import.meta.url)),
517
+ options: {
518
+ workerData: init,
519
+ env: workerSpawnEnv(),
520
+ execArgv: []
521
+ }
522
+ };
523
+ const workerEntry = new URL("./worker.ts", import.meta.url);
524
+ const tsxEsmApiEntry = import.meta.resolve("tsx/esm/api");
525
+ const tsxCjsApiEntry = import.meta.resolve("tsx/cjs/api");
526
+ const bootstrap = [
527
+ `import { register as registerEsm } from ${JSON.stringify(tsxEsmApiEntry)}`,
528
+ `import { register as registerCjs } from ${JSON.stringify(tsxCjsApiEntry)}`,
529
+ "registerCjs()",
530
+ "registerEsm()",
531
+ `await import(${JSON.stringify(workerEntry.href)})`
532
+ ].join("\n");
533
+ return {
534
+ entry: new URL(`data:text/javascript,${encodeURIComponent(bootstrap)}`),
535
+ options: {
536
+ workerData: init,
537
+ env: workerSpawnEnv(void 0, process.env.TSX_TSCONFIG_PATH),
538
+ execArgv: []
539
+ }
540
+ };
541
+ }
542
+ /**
543
+ * One live worker-engine run — the seam's {@link WorkflowRun}, returned by
544
+ * `start()` directly. Owns the Worker, the child registry, and the result
545
+ * settlement; `result` never rejects. `meta` is trusted same-process data
546
+ * borrowed as immutable by the handle and lifecycle events. The holder-bound
547
+ * SubagentRuntime handle is captured before the
548
+ * engine returns this run, so unloading the engine removes only the ability to
549
+ * start another workflow; this run can still start and clean up its children.
550
+ */
551
+ var WorkerRun = class {
552
+ constructor(ctx, subagents, id, meta, parent, init, provider, disposeGraceMs, observer, signal, scratchDir, hostLimits, deferStart = false, scratchPort = void 0) {
553
+ this.ctx = ctx;
554
+ this.subagents = subagents;
555
+ this.id = id;
556
+ this.meta = meta;
557
+ this.parent = parent;
558
+ this.init = init;
559
+ this.provider = provider;
560
+ this.disposeGraceMs = disposeGraceMs;
561
+ this.observer = observer;
562
+ this.scratchDir = scratchDir;
563
+ this.hostLimits = hostLimits;
564
+ this.deferStart = deferStart;
565
+ this.scratchPort = scratchPort;
566
+ this.settled = false;
567
+ this.finalAgentSpend = 0;
568
+ this.terminalClaimed = false;
569
+ this.workerDeathObserved = false;
570
+ this.workerGone = false;
571
+ this.workerReady = false;
572
+ this.released = false;
573
+ this.hostStarted = 0;
574
+ this.children = /* @__PURE__ */ new Map();
575
+ this.pendingStarts = /* @__PURE__ */ new Set();
576
+ this.activeChildCallIds = /* @__PURE__ */ new Set();
577
+ this.claimedCallIds = /* @__PURE__ */ new Set();
578
+ this.childCallIds = /* @__PURE__ */ new Set();
579
+ this.disposedChildCallIds = /* @__PURE__ */ new Set();
580
+ this.reapedChildCallIds = /* @__PURE__ */ new Set();
581
+ this.announcedChildren = /* @__PURE__ */ new Set();
582
+ this.agentChildren = /* @__PURE__ */ new Map();
583
+ this.announcedAgentSeqs = /* @__PURE__ */ new Set();
584
+ this.committedAgentSeqs = /* @__PURE__ */ new Set();
585
+ this.committedJournalCallIds = /* @__PURE__ */ new Set();
586
+ this.lastJournalOrdinal = 0;
587
+ this.liveAgents = /* @__PURE__ */ new Map();
588
+ this.pendingScratch = /* @__PURE__ */ new Set();
589
+ this.scratchOperations = 0;
590
+ this.scratchTail = Promise.resolve();
591
+ this.scratchController = new AbortController();
592
+ this.drainingWorkerResult = false;
593
+ this.quiescenceWaiters = [];
594
+ this.controller = new AbortController();
595
+ this.result = new Promise((resolve) => {
596
+ this.settleResolve = resolve;
597
+ });
598
+ const initialJournal = JSON.stringify(init.journal ?? []);
599
+ this.journalBytes = Buffer$1.byteLength(initialJournal, "utf8");
600
+ this.journalEntries = init.journal?.length ?? 0;
601
+ this.journal = [...init.journal ?? []];
602
+ for (const entry of init.journal ?? []) {
603
+ this.committedJournalCallIds.add(entry.callId.join("."));
604
+ this.lastJournalOrdinal = entry.ordinal;
605
+ }
606
+ if (this.journalBytes > hostLimits.maxJournalBytes) throw new WorkflowError$1(`workflow journal exceeds the ${hostLimits.maxJournalBytes}-byte limit before this attempt starts`, "INVALID_ARGUMENT");
607
+ const { entry, options } = resolveWorkerSpawn(init);
608
+ this.worker = new Worker(entry, options);
609
+ this.worker.on("message", (message) => {
610
+ this.onRawMessage(message);
611
+ });
612
+ this.worker.on("error", (error) => {
613
+ this.onWorkerDeath(`workflow worker failed: ${renderThrown(error)}`, false);
614
+ });
615
+ /* v8 ignore next -- messageerror: not constructible from the engine's own protocol (every payload is JSON data) */
616
+ this.worker.on("messageerror", (error) => {
617
+ this.onWorkerDeath(`workflow worker message failed to deserialize: ${renderThrown(error)}`, false);
618
+ });
619
+ this.worker.on("exit", (code) => {
620
+ this.workerGone = true;
621
+ this.onWorkerDeath(`workflow worker exited before the run settled (exit code ${code})`, true);
622
+ });
623
+ if (signal?.aborted) this.cancel("workflow start signal already aborted");
624
+ else if (signal !== void 0) {
625
+ const onAbort = () => {
626
+ this.detachInputSignal();
627
+ this.cancel("workflow signal aborted");
628
+ };
629
+ this.inputSignal = signal;
630
+ this.inputSignalAbort = onAbort;
631
+ signal.addEventListener("abort", onAbort, { once: true });
632
+ }
633
+ }
634
+ /**
635
+ * Cancel the run: the worker is told (its hooks start throwing and the
636
+ * script dies at its next await), the required signal shared by every child
637
+ * start is aborted, and the grace timer
638
+ * arms: a run still unsettled `disposeGraceMs` later force-settles
639
+ * `cancelled` and its worker is TERMINATED. Idempotent; the first reason
640
+ * wins.
641
+ * @param reason - human-readable cause (default `'workflow cancelled'`).
642
+ */
643
+ cancel(reason) {
644
+ if (this.settled || this.terminalClaimed && !this.drainingWorkerResult || this.cancelReason !== void 0) return;
645
+ this.cancelReason = reason ?? "workflow cancelled";
646
+ this.post("cancel", { reason: this.cancelReason });
647
+ this.abortChildren(this.cancelReason);
648
+ if (!this.scratchController.signal.aborted) this.scratchController.abort(this.cancelReason);
649
+ this.graceTimer = setTimeout(() => {
650
+ this.terminalClaimed = true;
651
+ this.endStrandedAgents();
652
+ this.settleResult(this.cancelledResult(this.observedAgentSpend()));
653
+ this.worker.terminate();
654
+ }, this.disposeGraceMs);
655
+ this.graceTimer.unref();
656
+ }
657
+ /**
658
+ * Release a parked script gate. A no-op once the run settled or a cancel is
659
+ * already in flight (the cancel path owns the terminal state then).
660
+ */
661
+ resume() {
662
+ if (this.settled || this.terminalClaimed || this.cancelReason !== void 0) return;
663
+ this.post("resume", {});
664
+ }
665
+ /** Release a deferred fresh attempt exactly once. */
666
+ release() {
667
+ if (this.released || this.settled || this.cancelReason !== void 0) return;
668
+ this.released = true;
669
+ if (this.workerReady) this.post("go", {});
670
+ }
671
+ /** Return replay authority only after terminal settlement. */
672
+ checkpoint() {
673
+ if (!this.settled) return void 0;
674
+ const initialSpend = this.init.initialAgentSpend ?? 0;
675
+ const cumulativeSpend = Math.max(this.observedAgentSpend(), this.finalAgentSpend);
676
+ return {
677
+ journal: this.journal.map((entry) => ({
678
+ ...entry,
679
+ callId: [...entry.callId]
680
+ })),
681
+ agentSpend: cumulativeSpend,
682
+ agentSeq: Math.max(cumulativeSpend, this.init.initialAgentSeq ?? 0, (this.init.initialAgentSeq ?? 0) + Math.max(0, cumulativeSpend - initialSpend), ...this.announcedAgentSeqs, ...this.journal.flatMap((entry) => entry.kind === "agent" ? [entry.seq] : []))
683
+ };
684
+ }
685
+ /**
686
+ * Cancel + bounded settle + termination. Host-drives every registered
687
+ * child's disposal IMMEDIATELY — a wedged worker can relay no dispose RPC,
688
+ * and deferring child teardown to the post-terminate reap would spend the
689
+ * whole grace waiting for a quiescence that cannot start, then return with
690
+ * the disposals still in flight — so child disposal overlaps the same
691
+ * grace the worker gets to settle (the worker's own dispose RPCs join the
692
+ * shared per-child disposal). Waits (at most the grace) for the result and
693
+ * child quiescence, then terminates the worker unconditionally — the
694
+ * thread never outlives its run — and reaps whatever children remain
695
+ * (their disposal is contained, not awaited past the grace, the same
696
+ * abandonment the seam documents for a slow-disposing child). Idempotent;
697
+ * safe on every path.
698
+ * @returns resolves when the run's resources are released or abandoned.
699
+ */
700
+ dispose() {
701
+ if (this.disposed !== void 0) return this.disposed;
702
+ const claimed = Promise.withResolvers();
703
+ this.disposed = claimed.promise;
704
+ (async () => {
705
+ this.detachInputSignal();
706
+ this.cancel("workflow disposed");
707
+ this.reapChildren("workflow disposed");
708
+ await Promise.race([(async () => {
709
+ await this.result;
710
+ await this.runQuiescence();
711
+ })(), sleep(this.disposeGraceMs)]);
712
+ await this.worker.terminate();
713
+ this.reapChildren("workflow disposed");
714
+ })().then(
715
+ () => {
716
+ claimed.resolve(void 0);
717
+ },
718
+ /* v8 ignore next -- result/quiescence never reject and Worker.terminate is the only external promise */
719
+ (error) => {
720
+ claimed.reject(error);
721
+ }
722
+ );
723
+ return this.disposed;
724
+ }
725
+ /** Post one message to the worker (payload looked up from the tag's map entry), tolerating a thread that is already gone. */
726
+ post(type, payload) {
727
+ if (this.workerGone || this.workerDeathObserved) return;
728
+ try {
729
+ this.worker.postMessage({
730
+ type,
731
+ ...payload
732
+ });
733
+ } catch (error) {
734
+ /* v8 ignore next -- postMessage teardown race (a throw between exit and its event): not constructible in-process */
735
+ this.ctx.logger.warn(`workflow-worker-thread: postMessage failed: ${renderThrown(error)}`);
736
+ }
737
+ }
738
+ /** Decode one untrusted worker frame and contain protocol failures to this run. */
739
+ onRawMessage(raw) {
740
+ if (this.workerDeathObserved || this.terminalClaimed) return;
741
+ try {
742
+ this.onMessage(decodeWorkerToHostMessage(raw, this.hostLimits.maxProtocolMessageBytes));
743
+ } catch (error) {
744
+ const detail = error instanceof WorkflowProtocolError ? error.message : renderThrown(error);
745
+ this.onWorkerDeath(`workflow worker protocol violation: ${detail}`, false);
746
+ this.worker.terminate();
747
+ }
748
+ }
749
+ onMessage(message) {
750
+ if (!this.workerReady && message.type !== "ready") throw new WorkflowProtocolError(`${message.type} arrived before ready`);
751
+ switch (message.type) {
752
+ case "ready":
753
+ if (this.workerReady) throw new WorkflowProtocolError("ready arrived more than once");
754
+ this.workerReady = true;
755
+ if (!this.deferStart || this.released) this.post("go", {});
756
+ break;
757
+ case "phase":
758
+ this.assertEventText(message.title, "phase title");
759
+ if (this.cancelReason === void 0 && !this.terminalClaimed) this.observer.phase(message.title);
760
+ break;
761
+ case "log":
762
+ this.assertEventText(message.message, "log message");
763
+ if (this.cancelReason === void 0 && !this.terminalClaimed) this.observer.log(message.message);
764
+ break;
765
+ case "agent-start":
766
+ this.onAgentStart(message.info);
767
+ break;
768
+ case "agent-end":
769
+ this.onAgentEnd(message.info);
770
+ break;
771
+ case "gate":
772
+ this.assertEventText(message.gate.message, "gate message");
773
+ if (this.cancelReason === void 0 && !this.terminalClaimed) this.observer.gate(message.gate);
774
+ break;
775
+ case "journal-commit":
776
+ this.onJournalCommit(message.entry);
777
+ break;
778
+ case "scratch-write":
779
+ this.onScratchWrite(message.callId, message.name, message.content);
780
+ break;
781
+ case "scratch-read":
782
+ this.onScratchRead(message.callId, message.name);
783
+ break;
784
+ case "child-start":
785
+ this.onChildStart(message.callId, message.request);
786
+ break;
787
+ case "child-dispose":
788
+ this.onChildDispose(message.callId);
789
+ break;
790
+ case "result": this.onResult(message.result);
791
+ }
792
+ }
793
+ /** Why a ready provider result may no longer be admitted to the worker. */
794
+ childAdmissionFailure() {
795
+ if (this.cancelReason !== void 0) return {
796
+ reason: this.cancelReason,
797
+ rendered: `workflow run cancelled: ${this.cancelReason}`
798
+ };
799
+ if (this.workerDeathObserved) return {
800
+ reason: "workflow worker gone",
801
+ rendered: "workflow worker is no longer available"
802
+ };
803
+ if (this.terminalClaimed) return {
804
+ reason: "workflow settled",
805
+ rendered: "workflow run already settled"
806
+ };
807
+ }
808
+ /** Reserve a worker RPC id exactly once across every side-effecting family. */
809
+ claimCallId(callId, operation) {
810
+ if (this.claimedCallIds.has(callId)) throw new WorkflowProtocolError(`${operation} reused callId ${callId}`);
811
+ this.claimedCallIds.add(callId);
812
+ }
813
+ /** Bound observer text before retaining or dispatching worker-controlled content. */
814
+ assertEventText(value, label) {
815
+ if (Buffer$1.byteLength(value, "utf8") > this.hostLimits.maxEventTextBytes) throw new WorkflowProtocolError(`${label} exceeds the ${this.hostLimits.maxEventTextBytes}-byte limit`);
816
+ }
817
+ /** Admit one lifecycle start only for a child the host actually published. */
818
+ onAgentStart(info) {
819
+ if (this.cancelReason !== void 0 || this.terminalClaimed) return;
820
+ this.assertEventText(info.label, "agent label");
821
+ if (info.phase !== void 0) this.assertEventText(info.phase, "agent phase");
822
+ const priorSequence = this.init.initialAgentSeq ?? 0;
823
+ if (info.seq <= priorSequence) throw new WorkflowProtocolError(`agent-start seq ${info.seq} does not advance prior seq ${priorSequence}`);
824
+ if (info.seq > priorSequence + this.hostStarted) throw new WorkflowProtocolError(`agent-start seq ${info.seq} exceeds the host-observed sequence range`);
825
+ if (this.announcedAgentSeqs.has(info.seq)) throw new WorkflowProtocolError(`agent-start reused seq ${info.seq}`);
826
+ if (this.announcedChildren.has(info.childId)) throw new WorkflowProtocolError(`agent-start reused child id ${JSON.stringify(info.childId)}`);
827
+ const published = [...this.children.values()].find((record) => record.run.id === info.childId);
828
+ if (!published) throw new WorkflowProtocolError(`agent-start references unpublished child ${JSON.stringify(info.childId)}`);
829
+ this.announcedChildren.add(info.childId);
830
+ this.announcedAgentSeqs.add(info.seq);
831
+ this.agentChildren.set(info.seq, published);
832
+ this.liveAgents.set(info.seq, info);
833
+ this.observer.agentStart(info);
834
+ }
835
+ /** Require one end to match the exact start snapshot before forwarding it. */
836
+ onAgentEnd(info) {
837
+ const start = this.liveAgents.get(info.seq);
838
+ if (start === void 0) {
839
+ if (this.cancelReason !== void 0 || this.terminalClaimed) return;
840
+ throw new WorkflowProtocolError(`agent-end references unknown seq ${info.seq}`);
841
+ }
842
+ if (start.label !== info.label || start.phase !== info.phase || start.childId !== info.childId) throw new WorkflowProtocolError(`agent-end metadata does not match agent-start seq ${info.seq}`);
843
+ if (this.cancelReason !== void 0) {
844
+ this.endAgent({
845
+ ...start,
846
+ outcome: "cancelled"
847
+ });
848
+ return;
849
+ }
850
+ const child = this.agentChildren.get(info.seq);
851
+ if (child === void 0) throw new WorkflowProtocolError(`agent-end seq ${info.seq} lost its host child correlation`);
852
+ const committed = this.committedAgentSeqs.has(info.seq);
853
+ if (info.outcome === "cancelled") throw new WorkflowProtocolError(`agent-end seq ${info.seq} reported cancellation before the run was cancelled`);
854
+ if (info.outcome === "completed" && (!committed || child.resultState !== "settled")) throw new WorkflowProtocolError(`agent-end seq ${info.seq} settled without a committed result`);
855
+ if (info.outcome === "failed" && !(committed && child.resultState === "settled" || !committed && child.resultState === "failed")) throw new WorkflowProtocolError(`agent-end seq ${info.seq} does not match the host-observed child result`);
856
+ this.endAgent(info);
857
+ }
858
+ /** Commit one completed host call in monotonic order and at most once. */
859
+ onJournalCommit(entry) {
860
+ if (this.cancelReason !== void 0 || this.terminalClaimed) return;
861
+ if (entry.ordinal !== this.lastJournalOrdinal + 1) throw new WorkflowProtocolError(`journal-commit ordinal ${entry.ordinal} does not follow ${this.lastJournalOrdinal}`);
862
+ const callKey = entry.callId.join(".");
863
+ if (this.committedJournalCallIds.has(callKey)) throw new WorkflowProtocolError(`journal-commit reused call identity ${JSON.stringify(entry.callId)}`);
864
+ if (entry.kind === "agent") {
865
+ if (!this.liveAgents.has(entry.seq)) throw new WorkflowProtocolError(`agent journal commit references unknown live seq ${entry.seq}`);
866
+ if (this.agentChildren.get(entry.seq)?.resultState !== "settled") throw new WorkflowProtocolError(`agent journal commit seq ${entry.seq} arrived before a host-observed child result`);
867
+ if (this.committedAgentSeqs.has(entry.seq)) throw new WorkflowProtocolError(`agent journal commit reused seq ${entry.seq}`);
868
+ }
869
+ const encodedEntry = JSON.stringify(entry);
870
+ const addedBytes = Buffer$1.byteLength(encodedEntry, "utf8") + (this.journalEntries === 0 ? 0 : 1);
871
+ if (addedBytes > this.hostLimits.maxJournalBytes - this.journalBytes) throw new WorkflowProtocolError(`journal-commit exceeds the ${this.hostLimits.maxJournalBytes}-byte journal limit`);
872
+ if (entry.kind === "agent") this.committedAgentSeqs.add(entry.seq);
873
+ this.committedJournalCallIds.add(callKey);
874
+ this.journal.push(entry);
875
+ this.lastJournalOrdinal = entry.ordinal;
876
+ this.journalBytes += addedBytes;
877
+ this.journalEntries += 1;
878
+ this.observer.journalCommit(entry);
879
+ }
880
+ onChildStart(callId, request) {
881
+ this.claimCallId(callId, "child-start");
882
+ this.childCallIds.add(callId);
883
+ if (Buffer$1.byteLength(request.prompt, "utf8") > this.hostLimits.maxChildPromptBytes) throw new WorkflowProtocolError(`child-start prompt exceeds the ${this.hostLimits.maxChildPromptBytes}-byte limit`);
884
+ if ((this.init.initialAgentSpend ?? 0) + this.hostStarted >= this.init.limits.maxTotalAgents) throw new WorkflowProtocolError("child-start exceeds the host-enforced total agent cap");
885
+ if (this.activeChildCallIds.size >= this.init.limits.maxConcurrentAgents) throw new WorkflowProtocolError("child-start exceeds the host-enforced concurrent agent cap");
886
+ const initialFailure = this.childAdmissionFailure();
887
+ if (initialFailure !== void 0) {
888
+ this.post("child-start-error", {
889
+ callId,
890
+ rendered: initialFailure.rendered
891
+ });
892
+ return;
893
+ }
894
+ this.hostStarted += 1;
895
+ this.activeChildCallIds.add(callId);
896
+ const task = this.startChild(callId, request);
897
+ this.pendingStarts.add(task);
898
+ task.then(
899
+ () => {
900
+ this.finishPendingStart(task, callId);
901
+ },
902
+ /* v8 ignore next -- startChild contains provider and cleanup failures */
903
+ () => {
904
+ this.finishPendingStart(task, callId);
905
+ }
906
+ );
907
+ }
908
+ /** Await one provider-owned startup transaction and publish only while admitted. */
909
+ async startChild(callId, request) {
910
+ let run;
911
+ try {
912
+ run = await this.subagents.start(this.provider, {
913
+ prompt: [{
914
+ type: "text",
915
+ text: request.prompt
916
+ }],
917
+ parent: this.parent,
918
+ signal: this.controller.signal,
919
+ ...request.schema !== void 0 ? { outputSchema: request.schema } : {},
920
+ ...request.provider !== void 0 || request.model !== void 0 ? { agentOptions: {
921
+ ...request.provider !== void 0 ? { provider: request.provider } : {},
922
+ ...request.model !== void 0 ? { model: request.model } : {}
923
+ } } : {}
924
+ });
925
+ } catch (error) {
926
+ const failure = this.childAdmissionFailure();
927
+ this.post("child-start-error", {
928
+ callId,
929
+ rendered: failure?.rendered ?? renderThrown(error)
930
+ });
931
+ return;
932
+ }
933
+ const failure = this.childAdmissionFailure();
934
+ if (failure !== void 0) {
935
+ this.post("child-start-error", {
936
+ callId,
937
+ rendered: failure.rendered
938
+ });
939
+ try {
940
+ await run.dispose();
941
+ } catch (error) {
942
+ this.ctx.logger.warn(`workflow-worker-thread: refused child dispose failed: ${renderThrown(error)}`);
943
+ }
944
+ return;
945
+ }
946
+ const record = {
947
+ run,
948
+ resultState: "pending"
949
+ };
950
+ this.children.set(callId, record);
951
+ const forwardResult = run.result.then((result) => {
952
+ try {
953
+ const snapshot = snapshotJsonValue({
954
+ output: result.output,
955
+ ...result.structured !== void 0 ? { structured: result.structured } : {},
956
+ stopReason: result.stopReason
957
+ });
958
+ if (snapshot === void 0) throw new TypeError("child result is not losslessly JSON-serializable");
959
+ record.resultState = "settled";
960
+ return () => {
961
+ this.post("child-settled", {
962
+ callId,
963
+ result: snapshot
964
+ });
965
+ };
966
+ } catch (error) {
967
+ record.resultState = "failed";
968
+ const rendered = `workflow child result could not cross the worker boundary: ${renderThrown(error)}`;
969
+ return () => {
970
+ this.post("child-failed", {
971
+ callId,
972
+ rendered
973
+ });
974
+ };
975
+ }
976
+ }, (error) => {
977
+ record.resultState = "failed";
978
+ const rendered = renderThrown(error);
979
+ return () => {
980
+ this.post("child-failed", {
981
+ callId,
982
+ rendered
983
+ });
984
+ };
985
+ });
986
+ this.post("child-started", {
987
+ callId,
988
+ childId: run.id
989
+ });
990
+ forwardResult.then((forward) => {
991
+ forward();
992
+ });
993
+ }
994
+ onChildDispose(callId) {
995
+ if (!this.childCallIds.has(callId)) throw new WorkflowProtocolError(`child-dispose references unknown callId ${callId}`);
996
+ if (this.disposedChildCallIds.has(callId)) throw new WorkflowProtocolError(`child-dispose repeated callId ${callId}`);
997
+ this.disposedChildCallIds.add(callId);
998
+ const record = this.children.get(callId);
999
+ if (record === void 0) {
1000
+ if (!this.reapedChildCallIds.has(callId)) throw new WorkflowProtocolError(`child-dispose references child ${callId} before host-side disposal`);
1001
+ this.post("child-disposed", { callId });
1002
+ return;
1003
+ }
1004
+ this.disposeChild(callId, record).then(() => {
1005
+ this.post("child-disposed", { callId });
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Start (or join) one registered child's disposal; the registry entry
1010
+ * leaves when it settles. Memoized per callId: the worker's dispose RPC,
1011
+ * the dispose() host drive, and the reap can all land on the same child —
1012
+ * the child's `dispose()` runs once and every caller awaits that one
1013
+ * settlement. A rejection is contained (the subagent seam's dispose() is
1014
+ * not supposed to reject, but a backend that does anyway must not break
1015
+ * quiescence): logged, and the child still leaves the registry.
1016
+ * @param callId - the child's registry key.
1017
+ * @param record - the registered child (the caller looked it up).
1018
+ * @returns resolves when the disposal settled either way; never rejects.
1019
+ */
1020
+ disposeChild(callId, record) {
1021
+ if (record.disposal !== void 0) return record.disposal;
1022
+ record.disposal = Promise.resolve().then(() => record.run.dispose()).catch((error) => {
1023
+ this.ctx.logger.warn(`workflow-worker-thread: child dispose failed: ${renderThrown(error)}`);
1024
+ }).then(() => {
1025
+ this.finishChild(callId);
1026
+ });
1027
+ return record.disposal;
1028
+ }
1029
+ /** Drop a child record and release quiescence waiters when all work ends. */
1030
+ finishChild(callId) {
1031
+ this.children.delete(callId);
1032
+ this.reapedChildCallIds.add(callId);
1033
+ this.activeChildCallIds.delete(callId);
1034
+ this.notifyRunQuiescence();
1035
+ }
1036
+ /** Retire one provider startup transaction. */
1037
+ finishPendingStart(task, callId) {
1038
+ this.pendingStarts.delete(task);
1039
+ if (!this.children.has(callId)) this.activeChildCallIds.delete(callId);
1040
+ this.notifyRunQuiescence();
1041
+ }
1042
+ /** Release waiters only after provider, child, and scratch work ends. */
1043
+ notifyRunQuiescence() {
1044
+ if (this.children.size !== 0 || this.pendingStarts.size !== 0 || this.pendingScratch.size !== 0) return;
1045
+ for (const waiter of this.quiescenceWaiters.splice(0)) waiter();
1046
+ }
1047
+ /** Resolves once every pending start, child, and admitted scratch operation is quiescent. */
1048
+ runQuiescence() {
1049
+ if (this.children.size === 0 && this.pendingStarts.size === 0 && this.pendingScratch.size === 0) return Promise.resolve();
1050
+ return new Promise((resolve) => {
1051
+ this.quiescenceWaiters.push(resolve);
1052
+ });
1053
+ }
1054
+ /** Abort + dispose every registered child (worker death / final teardown); disposal is contained, not awaited. */
1055
+ reapChildren(reason) {
1056
+ this.abortChildren(this.cancelReason ?? reason);
1057
+ for (const [callId, record] of [...this.children]) this.disposeChild(callId, record);
1058
+ }
1059
+ /** Abort the one canonical signal shared by pending and published children. */
1060
+ abortChildren(reason) {
1061
+ if (!this.controller.signal.aborted) this.controller.abort(reason);
1062
+ }
1063
+ onResult(result) {
1064
+ const cancellationWasRequested = this.cancelReason !== void 0;
1065
+ const observedSpend = this.observedAgentSpend();
1066
+ if (result.agentsStarted < observedSpend) throw new WorkflowProtocolError(`result agentsStarted ${result.agentsStarted} is below the host-observed spend ${observedSpend}`);
1067
+ if (result.agentsStarted > this.init.limits.maxTotalAgents) throw new WorkflowProtocolError(`result agentsStarted ${result.agentsStarted} exceeds the ${this.init.limits.maxTotalAgents}-agent cap`);
1068
+ this.terminalClaimed = true;
1069
+ this.reapChildren("workflow settled");
1070
+ this.endStrandedAgents();
1071
+ this.drainingWorkerResult = true;
1072
+ this.settleAfterScratch(result, cancellationWasRequested);
1073
+ }
1074
+ /** Drain admitted scratch effects before publishing the worker-selected outcome. */
1075
+ async settleAfterScratch(result, cancellationWasRequested) {
1076
+ await this.scratchQuiescence();
1077
+ /* v8 ignore next -- scratch quiescence resolves before any competing settle can pass the terminal claim */
1078
+ if (this.settled) return;
1079
+ if (cancellationWasRequested || this.cancelReason !== void 0) {
1080
+ this.settleResult(result.stopReason === "cancelled" ? result : this.cancelledResult(result.agentsStarted));
1081
+ return;
1082
+ }
1083
+ /* v8 ignore start -- deterministic tests cannot order Result ahead of
1084
+ * scratch-failure termination; onScratchFailure covers I/O failure. */
1085
+ if (this.scratchFailure !== void 0) {
1086
+ this.settleResult({
1087
+ value: null,
1088
+ stopReason: "error",
1089
+ error: this.scratchFailure,
1090
+ agentsStarted: result.agentsStarted
1091
+ });
1092
+ return;
1093
+ }
1094
+ /* v8 ignore stop */
1095
+ this.settleResult(result);
1096
+ }
1097
+ /** Serve one quota-checked, atomic scratch write. */
1098
+ onScratchWrite(callId, name, content) {
1099
+ this.claimScratchCall(callId, name, "scratch-write");
1100
+ return this.trackScratch(async () => {
1101
+ this.scratchController.signal.throwIfAborted();
1102
+ if (this.scratchPort !== void 0) {
1103
+ await this.scratchPort.write(name, content, this.scratchController.signal);
1104
+ this.post("scratch-written", { callId });
1105
+ return;
1106
+ }
1107
+ const state = await this.getScratchState();
1108
+ if (state === void 0) {
1109
+ this.post("scratch-written", { callId });
1110
+ return;
1111
+ }
1112
+ await this.writeScratch(state, name, content);
1113
+ this.post("scratch-written", { callId });
1114
+ });
1115
+ }
1116
+ /** Serve one no-follow, bounded scratch read. */
1117
+ onScratchRead(callId, name) {
1118
+ this.claimScratchCall(callId, name, "scratch-read");
1119
+ return this.trackScratch(async () => {
1120
+ this.scratchController.signal.throwIfAborted();
1121
+ if (this.scratchPort !== void 0) {
1122
+ const content = await this.scratchPort.read(name, this.scratchController.signal);
1123
+ this.post("scratch-read-result", {
1124
+ callId,
1125
+ ...content === void 0 ? {} : { content }
1126
+ });
1127
+ return;
1128
+ }
1129
+ const state = await this.getScratchState();
1130
+ if (state === void 0) {
1131
+ this.post("scratch-read-result", { callId });
1132
+ return;
1133
+ }
1134
+ const content = await this.readScratch(state, name);
1135
+ this.post("scratch-read-result", {
1136
+ callId,
1137
+ ...content === void 0 ? {} : { content }
1138
+ });
1139
+ });
1140
+ }
1141
+ /** Validate one scratch RPC before queueing any filesystem work. */
1142
+ claimScratchCall(callId, name, operation) {
1143
+ if (!SCRATCH_NAME.test(name)) throw new WorkflowProtocolError(`${operation} name must be one safe path component`);
1144
+ if (this.cancelReason !== void 0 || this.workerDeathObserved || this.terminalClaimed) throw new WorkflowProtocolError(`${operation} arrived after the run stopped admitting effects`);
1145
+ if (this.scratchOperations >= this.hostLimits.scratch.maxOperations) throw new WorkflowProtocolError(`${operation} exceeds the ${this.hostLimits.scratch.maxOperations}-operation scratch limit`);
1146
+ if (this.pendingScratch.size >= this.hostLimits.scratch.maxPendingOperations) throw new WorkflowProtocolError(`${operation} exceeds the ${this.hostLimits.scratch.maxPendingOperations}-operation pending scratch limit`);
1147
+ this.claimCallId(callId, operation);
1148
+ this.scratchOperations += 1;
1149
+ }
1150
+ /** Serialize, retain, and contain one admitted scratch operation. */
1151
+ trackScratch(operation) {
1152
+ const execution = this.scratchTail.then(operation);
1153
+ this.scratchTail = execution.catch(() => {});
1154
+ const tracked = execution.then(() => {}, (error) => {
1155
+ this.onScratchFailure(error);
1156
+ });
1157
+ this.pendingScratch.add(tracked);
1158
+ tracked.then(() => {
1159
+ this.pendingScratch.delete(tracked);
1160
+ this.notifyRunQuiescence();
1161
+ });
1162
+ return tracked;
1163
+ }
1164
+ /** Resolve once every scratch operation admitted before the terminal frame is quiescent. */
1165
+ scratchQuiescence() {
1166
+ if (this.pendingScratch.size === 0) return Promise.resolve();
1167
+ return Promise.all([...this.pendingScratch]).then(() => {});
1168
+ }
1169
+ /** Turn a scratch quota, integrity, or I/O failure into a run-local terminal error. */
1170
+ onScratchFailure(error) {
1171
+ if (this.cancelReason !== void 0) return;
1172
+ if (this.scratchFailure !== void 0) return;
1173
+ this.scratchFailure = `workflow scratch operation failed: ${renderThrown(error)}`;
1174
+ if (!this.scratchController.signal.aborted) this.scratchController.abort(this.scratchFailure);
1175
+ this.onWorkerDeath(this.scratchFailure, false);
1176
+ this.worker.terminate();
1177
+ }
1178
+ /** Initialize scratch storage at first use, never merely because a run started. */
1179
+ getScratchState() {
1180
+ const existing = this.scratchState;
1181
+ if (existing !== void 0) return existing;
1182
+ const created = this.initializeScratch();
1183
+ this.scratchState = created;
1184
+ return created;
1185
+ }
1186
+ /** Verify/create the owner-private scratch directory and account retained files on resume. */
1187
+ async initializeScratch() {
1188
+ if (this.scratchDir === void 0) return void 0;
1189
+ const dir = join(this.scratchDir, "scratch");
1190
+ await mkdir(dir, {
1191
+ recursive: true,
1192
+ mode: 448
1193
+ });
1194
+ const directory = await lstat(dir);
1195
+ if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error("scratch path is not a real directory");
1196
+ await chmod(dir, 448);
1197
+ const files = /* @__PURE__ */ new Map();
1198
+ let totalBytes = 0;
1199
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
1200
+ if (!SCRATCH_NAME.test(entry.name) || !entry.isFile() || entry.isSymbolicLink()) throw new Error(`scratch directory contains an unsupported entry ${JSON.stringify(entry.name)}`);
1201
+ const file = await this.scratchFileState(join(dir, entry.name));
1202
+ if (file.size > this.hostLimits.scratch.maxFileBytes) throw new Error(`scratch file ${JSON.stringify(entry.name)} exceeds the per-file quota`);
1203
+ files.set(entry.name, file);
1204
+ totalBytes += file.size;
1205
+ }
1206
+ if (files.size > this.hostLimits.scratch.maxFiles) throw new Error(`scratch directory exceeds the ${this.hostLimits.scratch.maxFiles}-file quota`);
1207
+ if (totalBytes > this.hostLimits.scratch.maxTotalBytes) throw new Error(`scratch directory exceeds the ${this.hostLimits.scratch.maxTotalBytes}-byte quota`);
1208
+ return {
1209
+ dir,
1210
+ device: directory.dev,
1211
+ inode: directory.ino,
1212
+ files,
1213
+ totalBytes
1214
+ };
1215
+ }
1216
+ /** Inspect one singly linked scratch path without following a final symlink. */
1217
+ async scratchFileState(path) {
1218
+ const before = await lstat(path);
1219
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error(`scratch path ${JSON.stringify(path)} is not a regular file`);
1220
+ if (before.nlink !== 1) throw new Error(`scratch path ${JSON.stringify(path)} has multiple hard links`);
1221
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1222
+ try {
1223
+ const info = await handle.stat();
1224
+ /* v8 ignore next -- a non-file descriptor requires a path replacement between lstat() and open(). */
1225
+ if (!info.isFile()) throw new Error(`scratch path ${JSON.stringify(path)} is not a regular file`);
1226
+ /* v8 ignore next -- inode mismatch requires a path replacement between lstat() and open(). */
1227
+ if (info.dev !== before.dev || info.ino !== before.ino)
1228
+ /* v8 ignore next */
1229
+ throw new Error(`scratch path ${JSON.stringify(path)} changed while opening`);
1230
+ /* v8 ignore next -- link-count change requires an external hard-link race after lstat(). */
1231
+ if (info.nlink !== 1)
1232
+ /* v8 ignore next */
1233
+ throw new Error(`scratch path ${JSON.stringify(path)} gained a hard link while opening`);
1234
+ /* v8 ignore next -- Node fs.Stat.size is always a non-negative safe integer for an opened local file. */
1235
+ if (!Number.isSafeInteger(info.size) || info.size < 0) throw new Error("scratch file size is invalid");
1236
+ await handle.chmod(384);
1237
+ return {
1238
+ device: info.dev,
1239
+ inode: info.ino,
1240
+ size: info.size
1241
+ };
1242
+ } finally {
1243
+ await handle.close();
1244
+ }
1245
+ }
1246
+ /** Atomically publish one owner-only scratch file after quota admission. */
1247
+ async writeScratch(state, name, content) {
1248
+ this.scratchController.signal.throwIfAborted();
1249
+ await this.assertScratchDirectory(state);
1250
+ const bytes = Buffer$1.from(content, "utf8");
1251
+ const limits = this.hostLimits.scratch;
1252
+ if (bytes.length > limits.maxFileBytes) throw new Error(`scratch file ${JSON.stringify(name)} exceeds the ${limits.maxFileBytes}-byte per-file quota`);
1253
+ const previous = state.files.get(name);
1254
+ if (previous === void 0 && state.files.size >= limits.maxFiles) throw new Error(`scratch write exceeds the ${limits.maxFiles}-file quota`);
1255
+ const nextTotal = state.totalBytes - (previous?.size ?? 0) + bytes.length;
1256
+ if (nextTotal > limits.maxTotalBytes) throw new Error(`scratch write exceeds the ${limits.maxTotalBytes}-byte total quota`);
1257
+ const target = join(state.dir, name);
1258
+ const temporary = join(state.dir, `.${randomBytes(12).toString("hex")}.tmp`);
1259
+ const backup = join(state.dir, `.${randomBytes(12).toString("hex")}.bak`);
1260
+ let handle;
1261
+ let staged;
1262
+ let previousMoved = false;
1263
+ let published = false;
1264
+ try {
1265
+ handle = await open(temporary, "wx", 384);
1266
+ await handle.chmod(384);
1267
+ await handle.writeFile(bytes, { signal: this.scratchController.signal });
1268
+ await handle.sync();
1269
+ const stagedInfo = await handle.stat();
1270
+ /* v8 ignore next -- the owner-held wx descriptor cannot change file type; link-count mutation needs an external race. */
1271
+ if (!stagedInfo.isFile() || stagedInfo.nlink !== 1)
1272
+ /* v8 ignore next */
1273
+ throw new Error("scratch temporary path changed while writing");
1274
+ staged = {
1275
+ device: stagedInfo.dev,
1276
+ inode: stagedInfo.ino,
1277
+ size: bytes.length
1278
+ };
1279
+ await handle.close();
1280
+ handle = void 0;
1281
+ this.scratchController.signal.throwIfAborted();
1282
+ await this.assertScratchDirectory(state);
1283
+ if (previous !== void 0) {
1284
+ await this.assertScratchFileIdentity(target, previous);
1285
+ await rename(target, backup);
1286
+ previousMoved = true;
1287
+ await this.assertScratchFileIdentity(backup, previous);
1288
+ this.scratchController.signal.throwIfAborted();
1289
+ }
1290
+ await link(temporary, target);
1291
+ published = true;
1292
+ await this.assertScratchFileIdentity(target, staged, true);
1293
+ await rm(temporary);
1294
+ await this.assertScratchFileIdentity(target, staged);
1295
+ state.files.set(name, staged);
1296
+ state.totalBytes = nextTotal;
1297
+ if (previousMoved && previous !== void 0) {
1298
+ await this.assertScratchFileIdentity(backup, previous);
1299
+ await rm(backup);
1300
+ previousMoved = false;
1301
+ }
1302
+ } catch (error) {
1303
+ if (previousMoved && !published) await link(backup, target).catch(() => {});
1304
+ throw error;
1305
+ } finally {
1306
+ /* v8 ignore next -- reaching cleanup with an open handle requires an injected fs write/sync failure. */
1307
+ if (handle !== void 0) await handle.close().catch(() => {});
1308
+ /* v8 ignore next -- cleanup failures are deliberately swallowed after the primary transaction failure. */
1309
+ if (!published) await rm(temporary, { force: true }).catch(() => {});
1310
+ }
1311
+ }
1312
+ /** Read one expected regular file through an owner-held no-follow descriptor. */
1313
+ async readScratch(state, name) {
1314
+ const expected = state.files.get(name);
1315
+ if (expected === void 0) return void 0;
1316
+ this.scratchController.signal.throwIfAborted();
1317
+ await this.assertScratchDirectory(state);
1318
+ const path = join(state.dir, name);
1319
+ let before;
1320
+ try {
1321
+ before = await lstat(path);
1322
+ } catch (error) {
1323
+ /* v8 ignore next -- safe component + verified directory leaves ENOENT as the only ordinary lstat failure. */
1324
+ if (error.code !== "ENOENT") throw error;
1325
+ state.files.delete(name);
1326
+ state.totalBytes -= expected.size;
1327
+ return;
1328
+ }
1329
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error(`scratch path ${JSON.stringify(name)} is not a regular file`);
1330
+ if (before.nlink !== 1) throw new Error(`scratch path ${JSON.stringify(name)} has multiple hard links`);
1331
+ if (before.dev !== expected.device || before.ino !== expected.inode) throw new Error(`scratch path ${JSON.stringify(name)} changed after initialization`);
1332
+ let handle;
1333
+ try {
1334
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1335
+ } catch (error) {
1336
+ /* v8 ignore next -- after successful lstat, a non-ENOENT open failure requires an external permission/type race. */
1337
+ if (error.code !== "ENOENT") throw error;
1338
+ /* v8 ignore start -- disappearance between lstat() and open() is a real filesystem race that deterministic tests cannot order. */
1339
+ state.files.delete(name);
1340
+ state.totalBytes -= expected.size;
1341
+ return;
1342
+ }
1343
+ try {
1344
+ const info = await handle.stat();
1345
+ /* v8 ignore next -- a non-file descriptor requires a path replacement between lstat() and open(). */
1346
+ if (!info.isFile()) throw new Error(`scratch path ${JSON.stringify(name)} is not a regular file`);
1347
+ /* v8 ignore next -- inode mismatch requires a path replacement between lstat() and open(). */
1348
+ if (info.dev !== before.dev || info.ino !== before.ino)
1349
+ /* v8 ignore next */
1350
+ throw new Error(`scratch path ${JSON.stringify(name)} changed while opening`);
1351
+ /* v8 ignore next -- link-count change requires an external hard-link race after open(). */
1352
+ if (info.nlink !== 1)
1353
+ /* v8 ignore next */
1354
+ throw new Error(`scratch path ${JSON.stringify(name)} gained a hard link while opening`);
1355
+ if (info.size > this.hostLimits.scratch.maxFileBytes) throw new Error(`scratch file ${JSON.stringify(name)} exceeds the per-file quota`);
1356
+ const bytes = await handle.readFile({ signal: this.scratchController.signal });
1357
+ /* v8 ignore next -- growth after descriptor stat and before read completion is an external filesystem race. */
1358
+ if (bytes.length > this.hostLimits.scratch.maxFileBytes)
1359
+ /* v8 ignore next */
1360
+ throw new Error(`scratch file ${JSON.stringify(name)} grew beyond the per-file quota while reading`);
1361
+ const nextTotal = state.totalBytes - expected.size + bytes.length;
1362
+ if (nextTotal > this.hostLimits.scratch.maxTotalBytes) throw new Error("scratch directory grew beyond the total quota while reading");
1363
+ expected.size = bytes.length;
1364
+ state.totalBytes = nextTotal;
1365
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1366
+ } finally {
1367
+ await handle.close();
1368
+ }
1369
+ }
1370
+ /** Reject a scratch path whose current inode is not the transaction-owned file. */
1371
+ async assertScratchFileIdentity(path, expected, allowStagingLink = false) {
1372
+ const current = await lstat(path);
1373
+ if (!current.isFile() || current.isSymbolicLink() || current.dev !== expected.device || current.ino !== expected.inode || current.nlink !== (allowStagingLink ? 2 : 1)) throw new Error(`scratch file ${JSON.stringify(path)} changed during publication`);
1374
+ }
1375
+ /** Reject a scratch directory that was replaced after lazy initialization. */
1376
+ async assertScratchDirectory(state) {
1377
+ const current = await lstat(state.dir);
1378
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== state.device || current.ino !== state.inode) throw new Error("scratch directory changed after initialization");
1379
+ }
1380
+ /** Process an error/messageerror/exit signal; `exit` also performs the final disposal sweep. */
1381
+ onWorkerDeath(message, isExit) {
1382
+ if (!this.workerDeathObserved) {
1383
+ this.workerDeathObserved = true;
1384
+ const outcomeWasClaimed = this.terminalClaimed;
1385
+ const cancellationWasRequested = this.cancelReason !== void 0;
1386
+ if (!outcomeWasClaimed && !this.scratchController.signal.aborted) this.scratchController.abort("workflow worker gone");
1387
+ if (!outcomeWasClaimed) this.terminalClaimed = true;
1388
+ if (this.children.size > 0 || this.pendingStarts.size > 0) this.reapChildren("workflow worker gone");
1389
+ this.endStrandedAgents();
1390
+ if (!outcomeWasClaimed) {
1391
+ if (cancellationWasRequested) this.settleResult(this.cancelledResult(this.observedAgentSpend()));
1392
+ else this.settleResult({
1393
+ value: null,
1394
+ stopReason: "error",
1395
+ error: message,
1396
+ agentsStarted: this.observedAgentSpend()
1397
+ });
1398
+ }
1399
+ }
1400
+ if (!isExit) return;
1401
+ for (const [callId, record] of [...this.children]) this.disposeChild(callId, record);
1402
+ this.endStrandedAgents();
1403
+ }
1404
+ /**
1405
+ * The single agent-end emission gate: forwards `end` iff its start is still
1406
+ * unpaired in the ledger, so every forwarded `workflow/agent-start` gets
1407
+ * EXACTLY one `workflow/agent-end` — the worker's own report where it can
1408
+ * speak, a host-synthesized one where it cannot ({@link endStrandedAgents}).
1409
+ * @param end - the settlement to emit (worker-reported or synthesized).
1410
+ */
1411
+ endAgent(end) {
1412
+ /* v8 ignore next -- a real end still in flight across the grace force-settle: not orderable in-process */
1413
+ if (!this.liveAgents.delete(end.seq)) return;
1414
+ this.observer.agentEnd(end);
1415
+ }
1416
+ /**
1417
+ * Synthesize the missing `agent-end` for every started-but-unpaired agent,
1418
+ * outcome `'cancelled'`: the reap cancels every child, and a real
1419
+ * settlement racing the force-settle loses to that already-started external
1420
+ * cancellation. The atomic terminal boundaries in {@link onResult} and
1421
+ * {@link onWorkerDeath} deliberately exclude teardown callbacks as contenders.
1422
+ * Called where the worker can no longer speak (the grace force-settle,
1423
+ * worker death, physical exit). When grace/death is the terminal source it
1424
+ * runs before settleResult, so already-known pairs precede `workflow/end`;
1425
+ * after an earlier Result, exit cleanup may close a survivor afterward.
1426
+ * The ledger preserves exactly-once pairing in both orders.
1427
+ */
1428
+ endStrandedAgents() {
1429
+ for (const info of [...this.liveAgents.values()]) this.endAgent({
1430
+ ...info,
1431
+ outcome: "cancelled"
1432
+ });
1433
+ }
1434
+ cancelledResult(agentsStarted) {
1435
+ return {
1436
+ value: null,
1437
+ stopReason: "cancelled",
1438
+ error: `workflow run cancelled: ${this.cancelReason ?? "workflow cancelled"}`,
1439
+ errorCode: "CANCELLED",
1440
+ agentsStarted
1441
+ };
1442
+ }
1443
+ /** Cumulative logical-agent spend the host can prove across resume attempts. */
1444
+ observedAgentSpend() {
1445
+ return (this.init.initialAgentSpend ?? 0) + this.hostStarted;
1446
+ }
1447
+ /** Remove the exact abort callback installed on the caller's start signal. */
1448
+ detachInputSignal() {
1449
+ const signal = this.inputSignal;
1450
+ const onAbort = this.inputSignalAbort;
1451
+ if (signal === void 0 || onAbort === void 0) return;
1452
+ this.inputSignal = void 0;
1453
+ this.inputSignalAbort = void 0;
1454
+ signal.removeEventListener("abort", onAbort);
1455
+ }
1456
+ /** First settle wins; disarms the grace timer and releases the caller signal. */
1457
+ settleResult(result) {
1458
+ /* v8 ignore next -- defensive fallback outside the claimed state machine */
1459
+ if (this.settled) return;
1460
+ this.terminalClaimed = true;
1461
+ this.settled = true;
1462
+ this.finalAgentSpend = Math.max(this.finalAgentSpend, result.agentsStarted);
1463
+ this.detachInputSignal();
1464
+ clearTimeout(this.graceTimer);
1465
+ this.settleResolve(result);
1466
+ }
1467
+ };
1468
+ /** A plain timer sleep (the dispose grace); unref'd so it never holds the process open. */
1469
+ function sleep(ms) {
1470
+ return new Promise((resolve) => {
1471
+ setTimeout(resolve, ms).unref();
1472
+ });
1473
+ }
1474
+ //#endregion
1475
+ //#region vendor/workflow-engine/meta.ts
1476
+ /**
1477
+ * Meta validation checks caller-provided DATA against the {@link WorkflowMeta}
1478
+ * contract and rejects every violation by name. Meta arrives as schema-checked
1479
+ * JSON data, never evaluated script text; evaluating it on the host could run getters outside the
1480
+ * worker timeout that exists to isolate model-written code.
1481
+ * @module @deepseek-ai/dsh-workflow-worker-thread/meta
1482
+ */
1483
+ /** Collect shape violations for a meta value (plain JSON data by the seam contract). */
1484
+ function validateMetaShape(meta) {
1485
+ const violations = [];
1486
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return { violations: ["meta must be an object"] };
1487
+ const record = meta;
1488
+ const known = /* @__PURE__ */ new Set([
1489
+ "name",
1490
+ "description",
1491
+ "whenToUse",
1492
+ "phases"
1493
+ ]);
1494
+ for (const key of Object.keys(record)) if (!known.has(key)) violations.push(`meta.${key} is not a recognized field (name/description/whenToUse/phases)`);
1495
+ if (typeof record.name !== "string" || record.name.length === 0) violations.push("meta.name must be a non-empty string");
1496
+ if (typeof record.description !== "string" || record.description.length === 0) violations.push("meta.description must be a non-empty string");
1497
+ if (record.whenToUse !== void 0 && typeof record.whenToUse !== "string") violations.push("meta.whenToUse must be a string");
1498
+ const phases = [];
1499
+ if (record.phases !== void 0) {
1500
+ if (!Array.isArray(record.phases)) violations.push("meta.phases must be an array");
1501
+ else record.phases.forEach((phase, index) => {
1502
+ if (typeof phase !== "object" || phase === null || Array.isArray(phase)) {
1503
+ violations.push(`meta.phases[${index}] must be an object`);
1504
+ return;
1505
+ }
1506
+ const entry = phase;
1507
+ for (const key of Object.keys(entry)) if (![
1508
+ "title",
1509
+ "detail",
1510
+ "provider",
1511
+ "model"
1512
+ ].includes(key)) violations.push(`meta.phases[${index}].${key} is not a recognized field`);
1513
+ if (typeof entry.title !== "string" || entry.title.length === 0) violations.push(`meta.phases[${index}].title must be a non-empty string`);
1514
+ if (entry.detail !== void 0 && typeof entry.detail !== "string") violations.push(`meta.phases[${index}].detail must be a string`);
1515
+ if (entry.provider !== void 0 && typeof entry.provider !== "string") violations.push(`meta.phases[${index}].provider must be a string`);
1516
+ if (entry.model !== void 0 && typeof entry.model !== "string") violations.push(`meta.phases[${index}].model must be a string`);
1517
+ if (violations.length === 0) phases.push({
1518
+ title: entry.title,
1519
+ ...entry.detail !== void 0 ? { detail: entry.detail } : {},
1520
+ ...entry.provider !== void 0 ? { provider: entry.provider } : {},
1521
+ ...entry.model !== void 0 ? { model: entry.model } : {}
1522
+ });
1523
+ });
1524
+ }
1525
+ if (violations.length > 0) return { violations };
1526
+ return {
1527
+ violations,
1528
+ meta: {
1529
+ name: record.name,
1530
+ description: record.description,
1531
+ ...record.whenToUse !== void 0 ? { whenToUse: record.whenToUse } : {},
1532
+ ...record.phases !== void 0 ? { phases } : {}
1533
+ }
1534
+ };
1535
+ }
1536
+ /**
1537
+ * Validate a caller-provided meta value against the {@link WorkflowMeta}
1538
+ * contract. Throws `META_INVALID` naming every violation (unknown fields,
1539
+ * missing/mistyped `name`/`description`, malformed `phases`); the returned
1540
+ * meta is a NORMALIZED copy built from the validated fields, so the engine
1541
+ * never aliases the caller's object.
1542
+ * @param value - the meta data from the start request (plain JSON by the seam contract).
1543
+ * @returns the validated, normalized meta block.
1544
+ */
1545
+ function validateMeta(value) {
1546
+ const { meta, violations } = validateMetaShape(value);
1547
+ if (meta === void 0) throw new WorkflowError(`invalid meta: ${violations.join("; ")}`, "META_INVALID");
1548
+ return meta;
1549
+ }
1550
+ //#endregion
1551
+ //#region vendor/workflow-engine/index.ts
1552
+ /**
1553
+ * Worker-thread workflow engine. Each run executes its model-written script in
1554
+ * an escapable vm context on a fresh worker and bridges `agent()` calls to host
1555
+ * subagents. The thread prevents synchronous script work from blocking the host
1556
+ * and permits forced termination, but it is containment rather than a security boundary.
1557
+ * @module @deepseek-ai/dsh-workflow-worker-thread
1558
+ */
1559
+ /** A body that still carries the Claude Code-style meta header (meta rides the seam as data here). */
1560
+ const META_STATEMENT = /^\s*export\s+const\s+meta\b/;
1561
+ /**
1562
+ * Parse-check the body with the SAME wrapper the worker-side runtime
1563
+ * compiles, so `start()` keeps the seam's synchronous `SCRIPT_PARSE` throw
1564
+ * (the worker's own compile happens a thread away, after `start()` returned).
1565
+ * One redundant parse per run, bought deliberately for the contract. A body
1566
+ * opening with `export const meta` gets a pointed message instead of the
1567
+ * wrapper's bare SyntaxError — the model's likeliest authoring slip.
1568
+ */
1569
+ function assertBodyParses(body, name) {
1570
+ if (META_STATEMENT.test(body)) throw new WorkflowError$1("workflow meta rides the `meta` request field, not the script: remove the `export const meta = {...}` statement from the body", "SCRIPT_PARSE");
1571
+ try {
1572
+ new vm.Script(`(async () => {\n${body}\n})()`, {
1573
+ filename: `workflow:${name}`,
1574
+ lineOffset: -1
1575
+ });
1576
+ } catch (error) {
1577
+ throw new WorkflowError$1(`workflow script does not parse: ${String(error)}`, "SCRIPT_PARSE", { cause: error });
1578
+ }
1579
+ }
1580
+ /** Resolve one run's provider route before publishing work. */
1581
+ function resolveSubagentProvider(ctx, configured, override) {
1582
+ const provider = override ?? configured;
1583
+ if (provider.length === 0 || provider !== provider.trim()) throw new WorkflowError$1("workflow subagentProvider must be a non-empty normalized string", "INVALID_ARGUMENT");
1584
+ if (ctx.subagents.getProvider(provider) === void 0) throw new WorkflowError$1(`no subagent provider registered for "${provider}"`, "AGENT_START");
1585
+ return provider;
1586
+ }
1587
+ /** Resolve one run's total-child cap against the engine deployment ceiling. */
1588
+ function resolveMaxTotalAgents(requested, ceiling) {
1589
+ if (requested === void 0) return ceiling;
1590
+ if (!Number.isSafeInteger(requested) || requested < 1) throw new WorkflowError$1("workflow maxTotalAgents must be a positive safe integer", "INVALID_ARGUMENT");
1591
+ if (requested > ceiling) throw new WorkflowError$1(`workflow maxTotalAgents ${requested} exceeds the engine ceiling ${ceiling}`, "INVALID_ARGUMENT");
1592
+ return requested;
1593
+ }
1594
+ /** Validate cumulative spend supplied by a logical-run supervisor. */
1595
+ function resolveInitialAgentSpend(requested, total, journal) {
1596
+ const committed = journal?.filter((entry) => entry.kind === "agent").length ?? 0;
1597
+ const resolved = requested ?? committed;
1598
+ if (!Number.isSafeInteger(resolved) || resolved < committed || resolved > total) throw new WorkflowError$1(`workflow initialAgentSpend must be a safe integer between the committed journal count (${committed}) and maxTotalAgents (${total})`, "INVALID_ARGUMENT");
1599
+ return resolved;
1600
+ }
1601
+ /** Validate the monotonic member sequence seed supplied by a logical-run supervisor. */
1602
+ function resolveInitialAgentSeq(requested, spend, total, journal) {
1603
+ let journalMaximum = 0;
1604
+ for (const entry of journal ?? []) {
1605
+ if (entry.kind !== "agent") continue;
1606
+ journalMaximum = Math.max(journalMaximum, entry.seq);
1607
+ }
1608
+ const minimum = Math.max(spend, journalMaximum);
1609
+ const resolved = requested ?? minimum;
1610
+ if (!Number.isSafeInteger(resolved) || resolved < minimum || resolved > Number.MAX_SAFE_INTEGER - (total - spend)) throw new WorkflowError$1(`workflow initialAgentSeq must be a safe integer no less than prior spend or journal sequence (${minimum}) with room for the remaining logical-agent budget`, "INVALID_ARGUMENT");
1611
+ return resolved;
1612
+ }
1613
+ /** Snapshot and validate replay data before it crosses into workerData. */
1614
+ function resolveJournal(journal) {
1615
+ if (journal === void 0) return void 0;
1616
+ let snapshot;
1617
+ try {
1618
+ snapshot = snapshotJsonValue(journal);
1619
+ } catch (error) {
1620
+ throw new WorkflowError$1("workflow journal must be lossless JSON data", "JOURNAL_DIVERGENCE", { cause: error });
1621
+ }
1622
+ if (snapshot === void 0 || !Array.isArray(snapshot)) throw new WorkflowError$1("workflow journal must be lossless JSON data", "JOURNAL_DIVERGENCE");
1623
+ const entries = snapshot;
1624
+ const callIds = /* @__PURE__ */ new Set();
1625
+ const agentSeqs = /* @__PURE__ */ new Set();
1626
+ let priorOrdinal = 0;
1627
+ for (const candidate of entries) {
1628
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) throw new WorkflowError$1("workflow journal entries must be objects", "JOURNAL_DIVERGENCE");
1629
+ const entry = candidate;
1630
+ const ordinal = entry.ordinal;
1631
+ if (typeof ordinal !== "number" || !Number.isSafeInteger(ordinal) || ordinal !== priorOrdinal + 1) throw new WorkflowError$1("workflow journal entry ordinal must be the next positive safe integer", "JOURNAL_DIVERGENCE");
1632
+ priorOrdinal = ordinal;
1633
+ const callId = entry.callId;
1634
+ const callKey = Array.isArray(callId) ? callId.join(".") : "";
1635
+ if (!Array.isArray(callId) || callId.length === 0 || callId.some((part) => !Number.isSafeInteger(part) || part <= 0) || callIds.has(callKey)) throw new WorkflowError$1("workflow journal call identities must be non-empty and unique", "JOURNAL_DIVERGENCE");
1636
+ callIds.add(callKey);
1637
+ const fingerprint = entry.fingerprint;
1638
+ if (typeof fingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(fingerprint)) throw new WorkflowError$1("workflow journal fingerprint must be a lowercase SHA-256 digest", "JOURNAL_DIVERGENCE");
1639
+ switch (entry.kind) {
1640
+ case "agent":
1641
+ if (Object.keys(entry).some((key) => ![
1642
+ "kind",
1643
+ "ordinal",
1644
+ "callId",
1645
+ "fingerprint",
1646
+ "seq",
1647
+ "result"
1648
+ ].includes(key)) || !Object.hasOwn(entry, "result")) throw new WorkflowError$1("workflow agent journal fields are not recognized", "JOURNAL_DIVERGENCE");
1649
+ if (typeof entry.seq !== "number" || !Number.isSafeInteger(entry.seq) || entry.seq < 1) throw new WorkflowError$1("workflow journal agent seq must be a positive safe integer", "JOURNAL_DIVERGENCE");
1650
+ if (agentSeqs.has(entry.seq)) throw new WorkflowError$1(`workflow journal repeats agent sequence ${entry.seq}`, "JOURNAL_DIVERGENCE");
1651
+ agentSeqs.add(entry.seq);
1652
+ break;
1653
+ case "phase":
1654
+ if (Object.keys(entry).some((key) => ![
1655
+ "kind",
1656
+ "ordinal",
1657
+ "callId",
1658
+ "fingerprint",
1659
+ "title"
1660
+ ].includes(key))) throw new WorkflowError$1("workflow phase journal fields are not recognized", "JOURNAL_DIVERGENCE");
1661
+ if (typeof entry.title !== "string" || entry.title.length === 0) throw new WorkflowError$1("workflow phase journal title must be a non-empty string", "JOURNAL_DIVERGENCE");
1662
+ break;
1663
+ case "log":
1664
+ if (Object.keys(entry).some((key) => ![
1665
+ "kind",
1666
+ "ordinal",
1667
+ "callId",
1668
+ "fingerprint",
1669
+ "message"
1670
+ ].includes(key))) throw new WorkflowError$1("workflow log journal fields are not recognized", "JOURNAL_DIVERGENCE");
1671
+ if (typeof entry.message !== "string") throw new WorkflowError$1("workflow log journal message must be a string", "JOURNAL_DIVERGENCE");
1672
+ break;
1673
+ case "scratch-read":
1674
+ if (Object.keys(entry).some((key) => ![
1675
+ "kind",
1676
+ "ordinal",
1677
+ "callId",
1678
+ "fingerprint",
1679
+ "content"
1680
+ ].includes(key))) throw new WorkflowError$1("workflow scratch-read journal fields are not recognized", "JOURNAL_DIVERGENCE");
1681
+ if (entry.content !== void 0 && typeof entry.content !== "string") throw new WorkflowError$1("workflow scratch-read journal content must be a string", "JOURNAL_DIVERGENCE");
1682
+ break;
1683
+ case "scratch-write":
1684
+ case "await-user":
1685
+ if (Object.keys(entry).some((key) => ![
1686
+ "kind",
1687
+ "ordinal",
1688
+ "callId",
1689
+ "fingerprint"
1690
+ ].includes(key))) throw new WorkflowError$1(`workflow ${entry.kind} journal fields are not recognized`, "JOURNAL_DIVERGENCE");
1691
+ break;
1692
+ default: throw new WorkflowError$1("workflow journal entry kind is not recognized", "JOURNAL_DIVERGENCE");
1693
+ }
1694
+ }
1695
+ return entries;
1696
+ }
1697
+ /**
1698
+ * The worker-thread engine service. `start()` validates the script up front
1699
+ * (meta + a host-side body parse) and returns a {@link WorkflowRun} whose
1700
+ * `result` never rejects; the `workflow/*` events fire around the run per
1701
+ * the seam contract.
1702
+ */
1703
+ var WorkerThreadWorkflowEngine = class {
1704
+ static {
1705
+ this.inject = ["subagents"];
1706
+ }
1707
+ static {
1708
+ this.Config = z.object({
1709
+ provider: z.string().default("spawn"),
1710
+ maxConcurrentAgents: z.natural().default(0),
1711
+ maxTotalAgents: z.natural().min(1).default(1024),
1712
+ maxItemsPerCall: z.natural().min(1).default(4096),
1713
+ syncTimeoutMs: z.natural().min(1).default(5e3),
1714
+ disposeGraceMs: z.natural().default(5e3),
1715
+ maxProtocolMessageBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(8388608),
1716
+ maxJournalBytes: z.number().step(1).min(2).max(Number.MAX_SAFE_INTEGER).default(67108864),
1717
+ maxChildPromptBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(1048576),
1718
+ maxEventTextBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(65536),
1719
+ scratchMaxOperations: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(4096),
1720
+ scratchMaxPendingOperations: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(64),
1721
+ scratchMaxFiles: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(64),
1722
+ scratchMaxFileBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(1048576),
1723
+ scratchMaxTotalBytes: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(8388608)
1724
+ });
1725
+ }
1726
+ constructor(ctx, config = {}) {
1727
+ this.ctx = ctx;
1728
+ this.dshWorkflowsNative = true;
1729
+ this.config = {
1730
+ provider: config.provider ?? "spawn",
1731
+ maxConcurrentAgents: config.maxConcurrentAgents ?? 0,
1732
+ maxTotalAgents: config.maxTotalAgents ?? 1024,
1733
+ maxItemsPerCall: config.maxItemsPerCall ?? 4096,
1734
+ syncTimeoutMs: config.syncTimeoutMs ?? 5e3,
1735
+ disposeGraceMs: config.disposeGraceMs ?? 5e3,
1736
+ maxProtocolMessageBytes: config.maxProtocolMessageBytes ?? 8388608,
1737
+ maxJournalBytes: config.maxJournalBytes ?? 67108864,
1738
+ maxChildPromptBytes: config.maxChildPromptBytes ?? 1048576,
1739
+ maxEventTextBytes: config.maxEventTextBytes ?? 65536,
1740
+ scratchMaxOperations: config.scratchMaxOperations ?? 4096,
1741
+ scratchMaxPendingOperations: config.scratchMaxPendingOperations ?? 64,
1742
+ scratchMaxFiles: config.scratchMaxFiles ?? 64,
1743
+ scratchMaxFileBytes: config.scratchMaxFileBytes ?? 1048576,
1744
+ scratchMaxTotalBytes: config.scratchMaxTotalBytes ?? 8388608,
1745
+ validationTimeoutMs: config.validationTimeoutMs ?? 1e4
1746
+ };
1747
+ if (this.config.scratchMaxFileBytes > this.config.scratchMaxTotalBytes) throw new WorkflowError$1("workflow scratchMaxFileBytes cannot exceed scratchMaxTotalBytes", "INVALID_ARGUMENT");
1748
+ if (this.config.scratchMaxPendingOperations > this.config.scratchMaxOperations) throw new WorkflowError$1("workflow scratchMaxPendingOperations cannot exceed scratchMaxOperations", "INVALID_ARGUMENT");
1749
+ }
1750
+ /** Dispatch through the host event registry without registering a global service. */
1751
+ emitWorkflowEvent(name, ...args) {
1752
+ const dispatch = this.ctx?.events?.dispatch;
1753
+ if (typeof dispatch !== "function") return;
1754
+ for (const callback of dispatch.call(this.ctx.events, "emit", [name, ...args]) ?? []) try {
1755
+ Promise.resolve(callback(...args)).catch((error) => this.ctx?.logger?.warn?.(`workflow: ${name} listener rejected`, error));
1756
+ } catch (error) {
1757
+ this.ctx?.logger?.warn?.(`workflow: ${name} listener threw`, error);
1758
+ }
1759
+ }
1760
+ /** Side-effect-free smoke validation through the same worker runtime. */
1761
+ async validate(request) {
1762
+ const timeout = new AbortController();
1763
+ const timer = setTimeout(() => timeout.abort(/* @__PURE__ */ new Error("workflow validation timed out")), this.config.validationTimeoutMs);
1764
+ timer.unref();
1765
+ const callerSignal = request.signal;
1766
+ const signal = callerSignal === void 0 ? timeout.signal : AbortSignal.any([callerSignal, timeout.signal]);
1767
+ const files = /* @__PURE__ */ new Map();
1768
+ let operations = 0;
1769
+ let pending = 0;
1770
+ const bytes = (value) => Buffer.byteLength(value, "utf8");
1771
+ const begin = () => {
1772
+ operations += 1;
1773
+ pending += 1;
1774
+ if (operations > this.config.scratchMaxOperations) throw new RangeError("workflow scratch operation limit exceeded");
1775
+ if (pending > this.config.scratchMaxPendingOperations) throw new RangeError("workflow scratch pending-operation limit exceeded");
1776
+ };
1777
+ const scratch = {
1778
+ read: async (name) => {
1779
+ begin();
1780
+ try {
1781
+ return files.get(name);
1782
+ } finally {
1783
+ pending -= 1;
1784
+ }
1785
+ },
1786
+ write: async (name, content) => {
1787
+ begin();
1788
+ try {
1789
+ const size = bytes(content);
1790
+ if (size > this.config.scratchMaxFileBytes) throw new RangeError("workflow scratch file limit exceeded");
1791
+ if (!files.has(name) && files.size >= this.config.scratchMaxFiles) throw new RangeError("workflow scratch file-count limit exceeded");
1792
+ let total = size;
1793
+ for (const [other, value] of files) if (other !== name) total += bytes(value);
1794
+ if (total > this.config.scratchMaxTotalBytes) throw new RangeError("workflow scratch total-byte limit exceeded");
1795
+ files.set(name, content);
1796
+ } finally {
1797
+ pending -= 1;
1798
+ }
1799
+ }
1800
+ };
1801
+ const run = this.start({
1802
+ ...request,
1803
+ signal,
1804
+ scratch,
1805
+ validateOnly: true,
1806
+ deferStart: true
1807
+ });
1808
+ let result;
1809
+ try {
1810
+ run.release();
1811
+ result = await run.result;
1812
+ if (callerSignal?.aborted) throw callerSignal.reason ?? new DOMException("This operation was aborted", "AbortError");
1813
+ } finally {
1814
+ clearTimeout(timer);
1815
+ run.cancel("workflow validation finished");
1816
+ await run.dispose();
1817
+ }
1818
+ if (result.stopReason !== "completed") return {
1819
+ ok: false,
1820
+ status: "error",
1821
+ error: result.error ?? "workflow validation failed",
1822
+ errorCode: result.errorCode
1823
+ };
1824
+ if (typeof result.value === "string" && /^would (?:pause|await_user)\b/u.test(result.value)) return {
1825
+ ok: true,
1826
+ status: "would-pause",
1827
+ value: result.value
1828
+ };
1829
+ return {
1830
+ ok: true,
1831
+ status: "completed",
1832
+ value: result.value
1833
+ };
1834
+ }
1835
+ /**
1836
+ * Validate and execute a workflow script in a fresh worker thread. Throws
1837
+ * {@link WorkflowError} synchronously (`META_INVALID` for a malformed meta
1838
+ * block, `SCRIPT_PARSE` for a body that does not compile) for a request
1839
+ * that cannot begin; once a run is returned, every failure resolves through
1840
+ * `result.stopReason` instead.
1841
+ * @param request - the script body, its meta data and `args`, the parent
1842
+ * agent, and an optional cancel signal.
1843
+ * @returns the live run (its `result` resolves when the script settles).
1844
+ */
1845
+ start(request) {
1846
+ const meta = validateMeta(request.meta);
1847
+ assertBodyParses(request.script, meta.name);
1848
+ const args = request.args === void 0 ? void 0 : snapshotJsonValue(request.args);
1849
+ if (request.args !== void 0 && args === void 0) throw new WorkflowError$1("workflow args must be losslessly JSON-serializable data", "INVALID_ARGUMENT");
1850
+ const checkpoint = request.replay?.checkpoint;
1851
+ const journal = resolveJournal(request.journal ?? checkpoint?.journal);
1852
+ const subagentProvider = resolveSubagentProvider(this.ctx, this.config.provider, request.subagentProvider);
1853
+ const maxTotalAgents = resolveMaxTotalAgents(request.maxTotalAgents, this.config.maxTotalAgents);
1854
+ const initialAgentSpend = resolveInitialAgentSpend(request.initialAgentSpend ?? checkpoint?.agentSpend, maxTotalAgents, journal);
1855
+ const initialAgentSeq = resolveInitialAgentSeq(request.initialAgentSeq ?? checkpoint?.agentSeq, initialAgentSpend, maxTotalAgents, journal);
1856
+ const id = WorkflowRunId(typeof request.runId === "string" ? request.runId : randomUUID());
1857
+ const info = {
1858
+ id,
1859
+ meta
1860
+ };
1861
+ const limits = {
1862
+ maxConcurrentAgents: this.config.maxConcurrentAgents === 0 ? Math.min(16, Math.max(1, availableParallelism() - 2)) : this.config.maxConcurrentAgents,
1863
+ maxTotalAgents,
1864
+ maxItemsPerCall: this.config.maxItemsPerCall,
1865
+ syncTimeoutMs: this.config.syncTimeoutMs
1866
+ };
1867
+ const init = {
1868
+ meta,
1869
+ body: request.script,
1870
+ ...args !== void 0 ? { args } : {},
1871
+ ...journal !== void 0 ? { journal } : {},
1872
+ initialAgentSpend,
1873
+ initialAgentSeq,
1874
+ ...request.validateOnly !== void 0 ? { validateOnly: request.validateOnly } : {},
1875
+ limits
1876
+ };
1877
+ const runCtx = this.ctx;
1878
+ const subagents = runCtx.subagents;
1879
+ const workerRun = new WorkerRun(runCtx, subagents, id, meta, request.parent, init, subagentProvider, this.config.disposeGraceMs, {
1880
+ phase: (title) => {
1881
+ this.emitWorkflowEvent("workflow/phase", info, title);
1882
+ },
1883
+ log: (message) => {
1884
+ this.emitWorkflowEvent("workflow/log", info, message);
1885
+ },
1886
+ agentStart: (agent) => {
1887
+ this.emitWorkflowEvent("workflow/agent-start", info, agent);
1888
+ },
1889
+ agentEnd: (agent) => {
1890
+ this.emitWorkflowEvent("workflow/agent-end", info, agent);
1891
+ },
1892
+ gate: (gate) => {
1893
+ this.emitWorkflowEvent("workflow/gate", info, gate);
1894
+ },
1895
+ journalCommit: (entry) => {
1896
+ this.emitWorkflowEvent("workflow/journal-commit", info, entry);
1897
+ }
1898
+ }, request.signal, request.scratchDir, {
1899
+ maxProtocolMessageBytes: this.config.maxProtocolMessageBytes,
1900
+ maxJournalBytes: this.config.maxJournalBytes,
1901
+ maxChildPromptBytes: this.config.maxChildPromptBytes,
1902
+ maxEventTextBytes: this.config.maxEventTextBytes,
1903
+ scratch: {
1904
+ maxOperations: this.config.scratchMaxOperations,
1905
+ maxPendingOperations: this.config.scratchMaxPendingOperations,
1906
+ maxFiles: this.config.scratchMaxFiles,
1907
+ maxFileBytes: this.config.scratchMaxFileBytes,
1908
+ maxTotalBytes: this.config.scratchMaxTotalBytes
1909
+ }
1910
+ }, request.deferStart === true, request.scratch);
1911
+ this.emitWorkflowEvent("workflow/start", info);
1912
+ workerRun.result.then((settled) => {
1913
+ this.emitWorkflowEvent("workflow/end", info, {
1914
+ stopReason: settled.stopReason,
1915
+ ...settled.error !== void 0 ? { error: settled.error } : {},
1916
+ ...settled.errorCode !== void 0 ? { errorCode: settled.errorCode } : {},
1917
+ agentsStarted: settled.agentsStarted
1918
+ });
1919
+ });
1920
+ return workerRun;
1921
+ }
1922
+ };
1923
+ //#endregion
1924
+ export { MaterializeError, WorkerThreadWorkflowEngine as default, materializeFromRealm, validateMeta };
1925
+
1926
+ //# sourceMappingURL=index.js.map