@tt-a1i/hive 2.1.0 → 2.1.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 (93) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.en.md +5 -460
  3. package/README.md +338 -187
  4. package/README.zh.md +389 -0
  5. package/dist/src/cli/hive-update.d.ts +3 -2
  6. package/dist/src/cli/hive-update.js +20 -60
  7. package/dist/src/cli/hive.js +5 -1
  8. package/dist/src/server/agent-manager-support.js +16 -9
  9. package/dist/src/server/agent-runtime-contract.d.ts +4 -0
  10. package/dist/src/server/agent-runtime.js +27 -0
  11. package/dist/src/server/agent-stdin-dispatcher.d.ts +3 -0
  12. package/dist/src/server/agent-stdin-dispatcher.js +3 -0
  13. package/dist/src/server/diagnostics-support-bundle.d.ts +3 -0
  14. package/dist/src/server/diagnostics-support-bundle.js +3 -0
  15. package/dist/src/server/hive-team-guidance.js +34 -4
  16. package/dist/src/server/message-log-store.d.ts +1 -0
  17. package/dist/src/server/message-log-store.js +13 -0
  18. package/dist/src/server/orchestrator-autostart.d.ts +2 -0
  19. package/dist/src/server/orchestrator-autostart.js +30 -11
  20. package/dist/src/server/post-start-input-writer.d.ts +1 -0
  21. package/dist/src/server/post-start-input-writer.js +80 -12
  22. package/dist/src/server/role-templates.d.ts +8 -1
  23. package/dist/src/server/role-templates.js +102 -48
  24. package/dist/src/server/route-types.d.ts +6 -0
  25. package/dist/src/server/routes-scenarios.d.ts +2 -1
  26. package/dist/src/server/routes-scenarios.js +11 -6
  27. package/dist/src/server/routes-team.js +21 -15
  28. package/dist/src/server/routes-workspaces.js +22 -2
  29. package/dist/src/server/runtime-store-contract.d.ts +2 -0
  30. package/dist/src/server/runtime-store-helpers.d.ts +11 -2
  31. package/dist/src/server/runtime-store-helpers.js +29 -5
  32. package/dist/src/server/runtime-store-workflows.js +20 -0
  33. package/dist/src/server/runtime-store.js +2 -0
  34. package/dist/src/server/scenario-presets.d.ts +3 -1
  35. package/dist/src/server/scenario-presets.js +12 -1
  36. package/dist/src/server/spawn-worker-defaults.d.ts +2 -0
  37. package/dist/src/server/spawn-worker-defaults.js +10 -5
  38. package/dist/src/server/team-operations.d.ts +3 -0
  39. package/dist/src/server/team-operations.js +97 -29
  40. package/dist/src/server/terminal-input-profile.d.ts +2 -0
  41. package/dist/src/server/update-install-plan.d.ts +20 -0
  42. package/dist/src/server/update-install-plan.js +151 -0
  43. package/dist/src/server/version-service.d.ts +5 -0
  44. package/dist/src/server/version-service.js +17 -13
  45. package/dist/src/server/workflow-agent-call-executor.d.ts +81 -0
  46. package/dist/src/server/workflow-agent-call-executor.js +259 -0
  47. package/dist/src/server/workflow-dag-layer-tracker.d.ts +14 -0
  48. package/dist/src/server/workflow-dag-layer-tracker.js +37 -0
  49. package/dist/src/server/workflow-dag.d.ts +8 -0
  50. package/dist/src/server/workflow-dag.js +65 -0
  51. package/dist/src/server/workflow-dispatch-awaiter.d.ts +7 -1
  52. package/dist/src/server/workflow-dispatch-awaiter.js +60 -9
  53. package/dist/src/server/workflow-runner.d.ts +5 -38
  54. package/dist/src/server/workflow-runner.js +113 -445
  55. package/dist/src/server/workflow-script-host-dispatcher.d.ts +14 -0
  56. package/dist/src/server/workflow-script-host-dispatcher.js +42 -0
  57. package/dist/src/server/workflow-script-loader.d.ts +2 -2
  58. package/dist/src/server/workflow-script-loader.js +2 -2
  59. package/dist/src/server/workflow-script-worker.d.ts +14 -0
  60. package/dist/src/server/workflow-script-worker.js +81 -0
  61. package/dist/src/server/workflow-vm-worker-source.d.ts +1 -0
  62. package/dist/src/server/workflow-vm-worker-source.js +1 -0
  63. package/dist/src/server/workflow-vm-worker.cjs +238 -0
  64. package/dist/src/server/workspace-ui-language.d.ts +7 -0
  65. package/dist/src/server/workspace-ui-language.js +9 -0
  66. package/dist/src/shared/scenario-presets.d.ts +6 -1
  67. package/dist/src/shared/scenario-presets.js +80 -28
  68. package/dist/src/shared/types.d.ts +11 -7
  69. package/dist/src/shared/ui-language.d.ts +4 -0
  70. package/dist/src/shared/ui-language.js +3 -0
  71. package/package.json +4 -2
  72. package/web/dist/assets/{AddWorkerDialog-DBLhwb91.js → AddWorkerDialog-DHwOZ75i.js} +2 -2
  73. package/web/dist/assets/AddWorkspaceFlow-qOI1CmqC.js +1 -0
  74. package/web/dist/assets/{FirstRunWizard-DlEPnWWw.js → FirstRunWizard-D5kGMD_V.js} +1 -1
  75. package/web/dist/assets/{MarketplaceDrawer-CfSiRi8e.js → MarketplaceDrawer-BuCbtqFk.js} +1 -1
  76. package/web/dist/assets/TaskGraphDrawer-e_bg3f5u.js +1 -0
  77. package/web/dist/assets/{WhatsNewDialog-vP7buLos.js → WhatsNewDialog-BB9ieq3T.js} +1 -1
  78. package/web/dist/assets/{WorkerModal-CSorwcdP.js → WorkerModal-CGeWO3Dg.js} +1 -1
  79. package/web/dist/assets/{WorkflowsDrawer-BXS3w9Uq.js → WorkflowsDrawer-CVvGxxdR.js} +1 -1
  80. package/web/dist/assets/WorkspaceMemoryDrawer-BamHuJEp.js +1 -0
  81. package/web/dist/assets/WorkspaceTaskDrawer-CqntbH9-.js +1 -0
  82. package/web/dist/assets/index-BOpmn5Ij.js +84 -0
  83. package/web/dist/assets/index-rPYd0MUk.css +1 -0
  84. package/web/dist/assets/{search-BZw4T67h.js → search-DKgM9IgX.js} +1 -1
  85. package/web/dist/assets/{square-terminal-B7E57In1.js → square-terminal-DMWazpEv.js} +1 -1
  86. package/web/dist/index.html +2 -2
  87. package/web/dist/sw.js +1 -1
  88. package/web/dist/assets/AddWorkspaceFlow-cxvhVAsT.js +0 -1
  89. package/web/dist/assets/TaskGraphDrawer-C2JufcPs.js +0 -1
  90. package/web/dist/assets/WorkspaceMemoryDrawer-D71ivohr.js +0 -1
  91. package/web/dist/assets/WorkspaceTaskDrawer-CGCTSHKa.js +0 -1
  92. package/web/dist/assets/index-BcwN8cCw.js +0 -79
  93. package/web/dist/assets/index-StXTPHls.css +0 -1
@@ -0,0 +1,259 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { cpus } from 'node:os';
3
+ import { resolveWorkflowCli } from './workflow-cli-policy.js';
4
+ import { buildSchemaInstruction, extractJsonBlock } from './workflow-output-schema.js';
5
+ const DEFAULT_MAX_AGENTS_PER_RUN = 1000;
6
+ const DEFAULT_MAX_CONCURRENT_AGENTS = Math.min(16, Math.max(2, cpus().length - 2));
7
+ const BUILT_IN_WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
8
+ const isBuiltInWorkerRole = (value) => BUILT_IN_WORKER_ROLES.has(value);
9
+ const buildModelArgs = (_cli, model) => {
10
+ if (!model?.trim())
11
+ return [];
12
+ /* All supported CLIs accept `--model <id>` as a positional flag. Keeping
13
+ the mapping centralised here so future CLI quirks can be patched without
14
+ touching workflow run lifecycle code. */
15
+ return ['--model', model];
16
+ };
17
+ export const createWorkflowAgentCallExecutor = ({ assertRunActive, awaiter, cancelOpenDispatch, cliPolicy, getCurrentPhaseTitle, hivePort, isRunStopped, maxAgentCalls, maxConcurrentAgents: configuredMaxConcurrentAgents, registerDagDispatch, resolveCliLaunchConfig, roleTemplateResolver, runId, store, workflowAgentId, workflowName, workspaceId, }) => {
18
+ let stepCounter = 0;
19
+ const spawnedWorkers = [];
20
+ const activeAgentCalls = new Set();
21
+ const activeDispatchIds = new Set();
22
+ const maxAgents = typeof maxAgentCalls === 'number' && maxAgentCalls > 0
23
+ ? maxAgentCalls
24
+ : DEFAULT_MAX_AGENTS_PER_RUN;
25
+ const maxConcurrentAgents = typeof configuredMaxConcurrentAgents === 'number' && configuredMaxConcurrentAgents > 0
26
+ ? configuredMaxConcurrentAgents
27
+ : DEFAULT_MAX_CONCURRENT_AGENTS;
28
+ let inFlight = 0;
29
+ const waitQueue = [];
30
+ const releaseSlot = () => {
31
+ let released = false;
32
+ return () => {
33
+ if (released)
34
+ return;
35
+ released = true;
36
+ inFlight--;
37
+ drainSlotQueue();
38
+ };
39
+ };
40
+ const takeSlot = () => {
41
+ inFlight++;
42
+ return releaseSlot();
43
+ };
44
+ function drainSlotQueue() {
45
+ while (inFlight < maxConcurrentAgents) {
46
+ const next = waitQueue.shift();
47
+ if (!next)
48
+ return;
49
+ next.resolve(takeSlot());
50
+ }
51
+ }
52
+ const acquireSlot = async (dagLayerId) => {
53
+ if (inFlight < maxConcurrentAgents) {
54
+ return takeSlot();
55
+ }
56
+ return new Promise((resolve, reject) => {
57
+ waitQueue.push({ dagLayerId, reject, resolve });
58
+ });
59
+ };
60
+ const cancelQueuedSlotWaiters = (reason, predicate) => {
61
+ const error = new Error(reason);
62
+ const remaining = [];
63
+ for (const waiter of waitQueue) {
64
+ if (predicate(waiter))
65
+ waiter.reject(error);
66
+ else
67
+ remaining.push(waiter);
68
+ }
69
+ waitQueue.length = 0;
70
+ waitQueue.push(...remaining);
71
+ };
72
+ const resolveWorkerLaunch = (opts) => {
73
+ const requestedType = opts.agentType ?? 'coder';
74
+ let role;
75
+ let command;
76
+ let templateArgs = [];
77
+ if (typeof requestedType === 'string' && !isBuiltInWorkerRole(requestedType)) {
78
+ const template = roleTemplateResolver.findByName(requestedType);
79
+ if (!template) {
80
+ throw new Error(`Workflow agentType '${requestedType}' is not a built-in role (coder/reviewer/tester/custom) and no matching role template exists in this workspace. ` +
81
+ `Use a built-in role or an existing dispatchable role template.`);
82
+ }
83
+ if (template.roleType === 'sentinel') {
84
+ throw new Error(`Workflow agentType '${requestedType}' resolves to a sentinel role, but sentinels are read-only observers and cannot receive workflow dispatches. Use coder, reviewer, tester, custom, or a dispatchable role template.`);
85
+ }
86
+ role = template.roleType === 'orchestrator' ? 'custom' : template.roleType;
87
+ command = resolveWorkflowCli({
88
+ ...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
89
+ isCustomTemplate: true,
90
+ templateDefaultCommand: template.defaultCommand,
91
+ policy: cliPolicy,
92
+ });
93
+ templateArgs = template.defaultArgs;
94
+ }
95
+ else {
96
+ role = requestedType;
97
+ command = resolveWorkflowCli({
98
+ ...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
99
+ isCustomTemplate: false,
100
+ policy: cliPolicy,
101
+ });
102
+ }
103
+ const baseLaunchConfig = resolveCliLaunchConfig(command) ?? { command, args: [] };
104
+ const launchArgs = [
105
+ ...(baseLaunchConfig.args ?? []),
106
+ ...templateArgs,
107
+ ...buildModelArgs(baseLaunchConfig.command, opts.model),
108
+ ];
109
+ return {
110
+ requestedType,
111
+ role,
112
+ launchConfig: { ...baseLaunchConfig, args: launchArgs },
113
+ };
114
+ };
115
+ const agent = async (prompt, opts = {}) => {
116
+ let markAgentCallDone;
117
+ const agentCallDone = new Promise((resolve) => {
118
+ markAgentCallDone = resolve;
119
+ });
120
+ activeAgentCalls.add(agentCallDone);
121
+ try {
122
+ assertRunActive();
123
+ if (stepCounter >= maxAgents) {
124
+ throw new Error(`Workflow agent cap exceeded: ${maxAgents} calls (set meta.maxAgentCalls to raise)`);
125
+ }
126
+ const myStep = ++stepCounter;
127
+ const dagLayerId = typeof opts.__hiveDagLayerId === 'string' && opts.__hiveDagLayerId.trim()
128
+ ? opts.__hiveDagLayerId.trim()
129
+ : null;
130
+ const { launchConfig, requestedType, role } = resolveWorkerLaunch(opts);
131
+ const name = opts.label ?? `${requestedType}-${myStep}-${randomUUID()}`;
132
+ const releaseSlot = await acquireSlot(dagLayerId);
133
+ let worker;
134
+ let dispatchId;
135
+ try {
136
+ assertRunActive();
137
+ worker = store.addWorkerWithLaunch(workspaceId, { name, role, ephemeral: true, spawnedBy: 'workflow' }, launchConfig);
138
+ spawnedWorkers.push(worker.id);
139
+ assertRunActive();
140
+ const liveRun = await store.startAgent(workspaceId, worker.id, { hivePort });
141
+ await liveRun.postStartInputReady;
142
+ assertRunActive();
143
+ const dispatchPrompt = opts.outputSchema
144
+ ? prompt + buildSchemaInstruction(opts.outputSchema)
145
+ : prompt;
146
+ const currentPhaseTitle = getCurrentPhaseTitle();
147
+ const dispatch = await store.dispatchTaskByWorkerName(workspaceId, name, dispatchPrompt, {
148
+ fromAgentId: workflowAgentId,
149
+ hivePort,
150
+ workflowRunId: runId,
151
+ stepIndex: myStep,
152
+ ...(currentPhaseTitle ? { phase: currentPhaseTitle } : {}),
153
+ label: opts.label ?? name,
154
+ });
155
+ dispatchId = dispatch.id;
156
+ activeDispatchIds.add(dispatch.id);
157
+ registerDagDispatch(dagLayerId, dispatch.id);
158
+ assertRunActive();
159
+ const report = await awaiter.awaitReport(dispatch.id, opts.timeoutMs);
160
+ assertRunActive();
161
+ if (opts.outputSchema) {
162
+ return extractJsonBlock(report.text) ?? { text: report.text };
163
+ }
164
+ return report.text;
165
+ }
166
+ catch (error) {
167
+ if (dispatchId) {
168
+ try {
169
+ cancelOpenDispatch(dispatchId, error instanceof Error ? error.message : String(error));
170
+ }
171
+ catch (cancelError) {
172
+ console.error('[hive] swallowed:workflow.agent.cancelDispatch', cancelError);
173
+ }
174
+ }
175
+ throw error;
176
+ }
177
+ finally {
178
+ if (dispatchId)
179
+ activeDispatchIds.delete(dispatchId);
180
+ if (worker) {
181
+ try {
182
+ store.deleteWorker(workspaceId, worker.id);
183
+ }
184
+ catch {
185
+ /* idempotent — worker may already be gone via cascade or boot cleanup */
186
+ }
187
+ const idx = spawnedWorkers.indexOf(worker.id);
188
+ if (idx !== -1)
189
+ spawnedWorkers.splice(idx, 1);
190
+ }
191
+ releaseSlot();
192
+ }
193
+ }
194
+ finally {
195
+ markAgentCallDone();
196
+ activeAgentCalls.delete(agentCallDone);
197
+ }
198
+ };
199
+ const catchPerItem = (value) => {
200
+ if (isRunStopped())
201
+ throw value;
202
+ console.warn(`[workflow ${workflowName}] item failed:`, value);
203
+ return null;
204
+ };
205
+ const waitForActiveCalls = async (timeoutMs) => {
206
+ const calls = [...activeAgentCalls];
207
+ if (calls.length === 0) {
208
+ return { settled: true, activeCount: 0 };
209
+ }
210
+ if (timeoutMs === undefined) {
211
+ await Promise.allSettled(calls);
212
+ return { settled: true, activeCount: 0 };
213
+ }
214
+ let timeoutHandle;
215
+ const timeout = new Promise((resolve) => {
216
+ timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs);
217
+ });
218
+ const result = await Promise.race([
219
+ Promise.allSettled(calls).then(() => 'settled'),
220
+ timeout,
221
+ ]);
222
+ if (timeoutHandle)
223
+ clearTimeout(timeoutHandle);
224
+ return {
225
+ settled: result === 'settled',
226
+ activeCount: activeAgentCalls.size,
227
+ };
228
+ };
229
+ const forceCancelActiveDispatchWaiters = (reason) => {
230
+ for (const dispatchId of activeDispatchIds) {
231
+ awaiter.forceCancel(dispatchId, reason);
232
+ }
233
+ cancelQueuedSlotWaiters(reason, () => true);
234
+ };
235
+ const cancelQueuedAgentCallsForDagLayer = (layerId, reason) => {
236
+ const normalized = layerId.trim();
237
+ if (!normalized)
238
+ return;
239
+ cancelQueuedSlotWaiters(reason, (waiter) => waiter.dagLayerId === normalized);
240
+ };
241
+ const deleteSpawnedWorkers = () => {
242
+ for (const workerId of spawnedWorkers.splice(0)) {
243
+ try {
244
+ store.deleteWorker(workspaceId, workerId);
245
+ }
246
+ catch {
247
+ /* swallow */
248
+ }
249
+ }
250
+ };
251
+ return {
252
+ agent,
253
+ cancelQueuedAgentCallsForDagLayer,
254
+ catchPerItem,
255
+ deleteSpawnedWorkers,
256
+ forceCancelActiveDispatchWaiters,
257
+ waitForActiveCalls,
258
+ };
259
+ };
@@ -0,0 +1,14 @@
1
+ import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
2
+ interface WorkflowDagDispatchCancelPort {
3
+ cancelOpenDispatchForRun: (workspaceId: string, dispatchId: string, reason: string) => boolean;
4
+ }
5
+ export declare const createWorkflowDagLayerTracker: (input: {
6
+ awaiter: Pick<WorkflowDispatchAwaiter, "notifyCancel">;
7
+ cancelQueuedAgentCallsForDagLayer: (layerId: string, reason: string) => void;
8
+ dispatchPort: WorkflowDagDispatchCancelPort;
9
+ workspaceId: string;
10
+ }) => {
11
+ cancelLayerAgents: (layerId: unknown, reason: unknown) => void;
12
+ registerDispatch: (layerId: string | null, dispatchId: string) => void;
13
+ };
14
+ export {};
@@ -0,0 +1,37 @@
1
+ const errorToMessage = (error) => error instanceof Error ? error.message : String(error);
2
+ export const createWorkflowDagLayerTracker = (input) => {
3
+ const dagLayerDispatches = new Map();
4
+ const registerDispatch = (layerId, dispatchId) => {
5
+ if (!layerId)
6
+ return;
7
+ let dispatches = dagLayerDispatches.get(layerId);
8
+ if (!dispatches) {
9
+ dispatches = new Set();
10
+ dagLayerDispatches.set(layerId, dispatches);
11
+ }
12
+ dispatches.add(dispatchId);
13
+ };
14
+ const cancelLayerAgents = (layerId, reason) => {
15
+ const message = typeof reason === 'string' && reason.trim() ? reason : 'Workflow DAG node failed';
16
+ const normalizedLayerId = typeof layerId === 'string' ? layerId.trim() : '';
17
+ if (!normalizedLayerId)
18
+ throw new Error('Missing DAG layer id');
19
+ input.cancelQueuedAgentCallsForDagLayer(normalizedLayerId, message);
20
+ const dispatchIds = dagLayerDispatches.get(normalizedLayerId) ?? new Set();
21
+ const errors = [];
22
+ for (const dispatchId of dispatchIds) {
23
+ try {
24
+ const cancelled = input.dispatchPort.cancelOpenDispatchForRun(input.workspaceId, dispatchId, message);
25
+ if (cancelled)
26
+ input.awaiter.notifyCancel(dispatchId, message);
27
+ }
28
+ catch (error) {
29
+ errors.push(errorToMessage(error));
30
+ }
31
+ }
32
+ if (errors.length > 0) {
33
+ throw new Error(errors.join('; '));
34
+ }
35
+ };
36
+ return { cancelLayerAgents, registerDispatch };
37
+ };
@@ -0,0 +1,8 @@
1
+ export interface WorkflowDagNodeInput {
2
+ deps: string[];
3
+ id: string;
4
+ }
5
+ export interface WorkflowDagPlan {
6
+ layers: string[][];
7
+ }
8
+ export declare const planWorkflowDag: (input: unknown) => WorkflowDagPlan;
@@ -0,0 +1,65 @@
1
+ const assertDagNodeInput = (node, index) => {
2
+ if (!node || typeof node !== 'object') {
3
+ throw new Error(`dag(): node #${index + 1} must be an object`);
4
+ }
5
+ const record = node;
6
+ const id = typeof record.id === 'string' ? record.id.trim() : '';
7
+ if (!id)
8
+ throw new Error(`dag(): node #${index + 1} requires a non-empty string id`);
9
+ if (!Array.isArray(record.deps)) {
10
+ throw new Error(`dag(${id}): deps must be an array`);
11
+ }
12
+ const deps = record.deps.map((dep) => String(dep).trim());
13
+ for (const dep of deps) {
14
+ if (!dep)
15
+ throw new Error(`dag(${id}): dependency ids must be non-empty strings`);
16
+ }
17
+ return { deps, id };
18
+ };
19
+ export const planWorkflowDag = (input) => {
20
+ if (!Array.isArray(input)) {
21
+ throw new Error('dag(): expected planner input to be an array of nodes');
22
+ }
23
+ const nodes = input.map(assertDagNodeInput);
24
+ const remainingDeps = new Map();
25
+ const dependents = new Map();
26
+ for (const node of nodes) {
27
+ if (remainingDeps.has(node.id))
28
+ throw new Error(`dag(): duplicate node id "${node.id}"`);
29
+ remainingDeps.set(node.id, node.deps.length);
30
+ dependents.set(node.id, []);
31
+ }
32
+ for (const node of nodes) {
33
+ for (const dep of node.deps) {
34
+ const children = dependents.get(dep);
35
+ if (!children)
36
+ throw new Error(`dag(${node.id}): unknown dependency "${dep}"`);
37
+ children.push(node.id);
38
+ }
39
+ }
40
+ const queue = nodes.filter((node) => node.deps.length === 0).map((node) => node.id);
41
+ const layers = [];
42
+ let cursor = 0;
43
+ let visited = 0;
44
+ while (cursor < queue.length) {
45
+ const layer = queue.slice(cursor);
46
+ cursor = queue.length;
47
+ layers.push(layer);
48
+ for (const id of layer) {
49
+ visited += 1;
50
+ for (const childId of dependents.get(id) ?? []) {
51
+ const next = (remainingDeps.get(childId) ?? 0) - 1;
52
+ remainingDeps.set(childId, next);
53
+ if (next === 0)
54
+ queue.push(childId);
55
+ }
56
+ }
57
+ }
58
+ if (visited !== nodes.length) {
59
+ const cyclic = nodes
60
+ .filter((node) => (remainingDeps.get(node.id) ?? 0) > 0)
61
+ .map((node) => node.id);
62
+ throw new Error(`dag(): cycle or unsatisfied dependencies among: ${cyclic.join(', ')}`);
63
+ }
64
+ return { layers };
65
+ };
@@ -7,6 +7,12 @@ export interface WorkflowDispatchAwaiter {
7
7
  awaitReport(dispatchId: string, timeoutMs?: number): Promise<ReportPayload>;
8
8
  notifyReport(dispatchId: string, payload: ReportPayload): void;
9
9
  notifyCancel(dispatchId: string, reason: string): void;
10
+ forceCancel(dispatchId: string, reason: string): void;
10
11
  cancelAll(reason: string): void;
11
12
  }
12
- export declare const createWorkflowDispatchAwaiter: () => WorkflowDispatchAwaiter;
13
+ export interface WorkflowDispatchAwaiterOptions {
14
+ deferredEventTtlMs?: number;
15
+ maxDeferredEvents?: number;
16
+ now?: () => number;
17
+ }
18
+ export declare const createWorkflowDispatchAwaiter: (options?: WorkflowDispatchAwaiterOptions) => WorkflowDispatchAwaiter;
@@ -1,6 +1,8 @@
1
1
  // Default 10 minutes — workflows often run long. Callers may shorten via opts.
2
2
  const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
3
- export const createWorkflowDispatchAwaiter = () => {
3
+ const DEFAULT_DEFERRED_EVENT_TTL_MS = DEFAULT_TIMEOUT_MS;
4
+ const DEFAULT_MAX_DEFERRED_EVENTS = 4096;
5
+ export const createWorkflowDispatchAwaiter = (options = {}) => {
4
6
  const pending = new Map();
5
7
  // TIER 2 #10 — deferred-cancel state. The runner creates a dispatch
6
8
  // and THEN calls awaitReport, with at least one microtask gap between
@@ -13,6 +15,13 @@ export const createWorkflowDispatchAwaiter = () => {
13
15
  // reason — defensive against a future ordering change.
14
16
  const cancelledIds = new Map();
15
17
  const reportedIds = new Map();
18
+ const now = options.now ?? (() => Date.now());
19
+ const deferredEventTtlMs = typeof options.deferredEventTtlMs === 'number' && options.deferredEventTtlMs > 0
20
+ ? options.deferredEventTtlMs
21
+ : DEFAULT_DEFERRED_EVENT_TTL_MS;
22
+ const maxDeferredEvents = typeof options.maxDeferredEvents === 'number' && options.maxDeferredEvents > 0
23
+ ? options.maxDeferredEvents
24
+ : DEFAULT_MAX_DEFERRED_EVENTS;
16
25
  const take = (dispatchId) => {
17
26
  const entry = pending.get(dispatchId);
18
27
  if (entry) {
@@ -21,19 +30,53 @@ export const createWorkflowDispatchAwaiter = () => {
21
30
  }
22
31
  return entry;
23
32
  };
33
+ const pruneDeferredEvents = () => {
34
+ const time = now();
35
+ for (const [id, entry] of cancelledIds) {
36
+ if (entry.expiresAt <= time)
37
+ cancelledIds.delete(id);
38
+ }
39
+ for (const [id, entry] of reportedIds) {
40
+ if (entry.expiresAt <= time)
41
+ reportedIds.delete(id);
42
+ }
43
+ while (cancelledIds.size + reportedIds.size > maxDeferredEvents) {
44
+ const reportedKey = reportedIds.keys().next().value;
45
+ if (typeof reportedKey === 'string') {
46
+ reportedIds.delete(reportedKey);
47
+ continue;
48
+ }
49
+ const cancelledKey = cancelledIds.keys().next().value;
50
+ if (typeof cancelledKey === 'string') {
51
+ cancelledIds.delete(cancelledKey);
52
+ continue;
53
+ }
54
+ break;
55
+ }
56
+ };
57
+ const putDeferred = (map, dispatchId, value) => {
58
+ map.set(dispatchId, { value, expiresAt: now() + deferredEventTtlMs });
59
+ pruneDeferredEvents();
60
+ };
61
+ const takeDeferred = (map, dispatchId) => {
62
+ pruneDeferredEvents();
63
+ const entry = map.get(dispatchId);
64
+ if (!entry)
65
+ return undefined;
66
+ map.delete(dispatchId);
67
+ return entry.value;
68
+ };
24
69
  return {
25
70
  awaitReport(dispatchId, timeoutMs = DEFAULT_TIMEOUT_MS) {
26
71
  return new Promise((resolve, reject) => {
27
72
  // Pre-arrived events: drain immediately, no timer / no entry.
28
- const earlyReport = reportedIds.get(dispatchId);
73
+ const earlyReport = takeDeferred(reportedIds, dispatchId);
29
74
  if (earlyReport) {
30
- reportedIds.delete(dispatchId);
31
75
  resolve(earlyReport);
32
76
  return;
33
77
  }
34
- const earlyCancel = cancelledIds.get(dispatchId);
35
- if (earlyCancel) {
36
- cancelledIds.delete(dispatchId);
78
+ const earlyCancel = takeDeferred(cancelledIds, dispatchId);
79
+ if (earlyCancel !== undefined) {
37
80
  reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${earlyCancel}`));
38
81
  return;
39
82
  }
@@ -51,8 +94,8 @@ export const createWorkflowDispatchAwaiter = () => {
51
94
  }
52
95
  else {
53
96
  // Pre-arrival: stash for the eventual awaitReport call. Cap
54
- // stash size implicitly by overwriting on duplicate id.
55
- reportedIds.set(dispatchId, payload);
97
+ // stash size and TTL so unknown ids cannot grow forever.
98
+ putDeferred(reportedIds, dispatchId, payload);
56
99
  }
57
100
  },
58
101
  notifyCancel(dispatchId, reason) {
@@ -61,7 +104,15 @@ export const createWorkflowDispatchAwaiter = () => {
61
104
  entry.reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${reason}`));
62
105
  }
63
106
  else {
64
- cancelledIds.set(dispatchId, reason);
107
+ putDeferred(cancelledIds, dispatchId, reason);
108
+ }
109
+ },
110
+ forceCancel(dispatchId, reason) {
111
+ cancelledIds.delete(dispatchId);
112
+ reportedIds.delete(dispatchId);
113
+ const entry = take(dispatchId);
114
+ if (entry) {
115
+ entry.reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${reason}`));
65
116
  }
66
117
  },
67
118
  cancelAll(reason) {
@@ -1,6 +1,6 @@
1
- import type { WorkerRole } from '../shared/types.js';
2
1
  import type { AgentLaunchConfigInput } from './agent-run-store.js';
3
- import { type WorkflowCliPolicy } from './workflow-cli-policy.js';
2
+ import { type WorkflowAgentExecutorStorePort, type WorkflowRoleTemplateResolver } from './workflow-agent-call-executor.js';
3
+ import type { WorkflowCliPolicy } from './workflow-cli-policy.js';
4
4
  import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
5
5
  import type { WorkflowRunRecord, WorkflowRunStatus } from './workflow-run-store.js';
6
6
  export interface RunWorkflowInput {
@@ -31,41 +31,7 @@ export interface RunInlineWorkflowInput {
31
31
  * into an ephemeral worker. Returns `undefined` if the name doesn't
32
32
  * match any template (built-in or custom); the runner falls back to
33
33
  * built-in-role semantics in that case. */
34
- export interface RoleTemplateResolver {
35
- findByName(name: string): {
36
- name: string;
37
- roleType: WorkerRole | 'orchestrator';
38
- defaultCommand: string;
39
- defaultArgs: string[];
40
- } | undefined;
41
- }
42
- interface RunnerStorePort {
43
- addWorkerWithLaunch: (workspaceId: string, input: {
44
- name: string;
45
- role: WorkerRole;
46
- ephemeral: true;
47
- spawnedBy: 'workflow';
48
- }, launchConfig: AgentLaunchConfigInput) => {
49
- id: string;
50
- name: string;
51
- };
52
- startAgent: (workspaceId: string, agentId: string, input: {
53
- hivePort: string;
54
- }) => Promise<{
55
- postStartInputReady?: Promise<void>;
56
- }>;
57
- dispatchTaskByWorkerName: (workspaceId: string, workerName: string, text: string, input: {
58
- fromAgentId: string;
59
- hivePort: string;
60
- workflowRunId: string;
61
- stepIndex: number;
62
- phase?: string;
63
- label?: string;
64
- }) => Promise<{
65
- id: string;
66
- }>;
67
- deleteWorker: (workspaceId: string, workerId: string) => void;
68
- }
34
+ export type RoleTemplateResolver = WorkflowRoleTemplateResolver;
69
35
  /** TIER 2 #3 — narrator lane sink. The runner calls append(runId, message)
70
36
  * every time the script invokes log(). Implementation lives in
71
37
  * workflow-run-log-store; the port keeps the runner free of DB types. */
@@ -111,9 +77,10 @@ export interface WorkflowRunner {
111
77
  }
112
78
  interface ListDispatchesForStopPort {
113
79
  listOpenDispatchIdsForRun: (runId: string) => string[];
80
+ cancelOpenDispatchForRun: (workspaceId: string, dispatchId: string, reason: string) => boolean;
114
81
  }
115
82
  export declare const createWorkflowRunner: (deps: {
116
- store: RunnerStorePort;
83
+ store: WorkflowAgentExecutorStorePort;
117
84
  workflowRunStore: WorkflowRunStorePort;
118
85
  awaiter: WorkflowDispatchAwaiter;
119
86
  dispatchPort: ListDispatchesForStopPort;