@tt-a1i/hive 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +8 -0
- package/assets/qq-group.jpg +0 -0
- package/dist/bin/team.cmd +1 -0
- package/dist/src/cli/hive-update.d.ts +45 -17
- package/dist/src/cli/hive-update.js +63 -25
- package/dist/src/cli/hive.d.ts +25 -0
- package/dist/src/cli/hive.js +41 -3
- package/dist/src/cli/team.d.ts +1 -0
- package/dist/src/cli/team.js +199 -3
- package/dist/src/server/agent-command-resolver.js +3 -19
- package/dist/src/server/agent-manager-support.d.ts +2 -2
- package/dist/src/server/agent-manager-support.js +98 -24
- package/dist/src/server/agent-run-starter.d.ts +7 -1
- package/dist/src/server/agent-run-starter.js +9 -2
- package/dist/src/server/agent-run-store.d.ts +1 -1
- package/dist/src/server/agent-runtime-close.d.ts +1 -0
- package/dist/src/server/agent-runtime-close.js +25 -1
- package/dist/src/server/agent-runtime-contract.d.ts +2 -1
- package/dist/src/server/agent-runtime.d.ts +1 -1
- package/dist/src/server/agent-runtime.js +8 -2
- package/dist/src/server/agent-startup-instructions.d.ts +8 -1
- package/dist/src/server/agent-startup-instructions.js +15 -9
- package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
- package/dist/src/server/agent-stdin-dispatcher.js +129 -40
- package/dist/src/server/cron-util.d.ts +7 -0
- package/dist/src/server/cron-util.js +19 -0
- package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
- package/dist/src/server/dispatch-ledger-store.js +51 -3
- package/dist/src/server/env-sync-message.js +9 -9
- package/dist/src/server/fs-pick-folder.js +4 -0
- package/dist/src/server/fs-sandbox.js +36 -7
- package/dist/src/server/hive-team-guidance.d.ts +11 -6
- package/dist/src/server/hive-team-guidance.js +252 -71
- package/dist/src/server/live-run-registry.d.ts +1 -0
- package/dist/src/server/live-run-registry.js +1 -1
- package/dist/src/server/open-target-commands.js +5 -6
- package/dist/src/server/orchestrator-autostart.d.ts +12 -0
- package/dist/src/server/orchestrator-autostart.js +15 -13
- package/dist/src/server/path-canonicalization.d.ts +3 -0
- package/dist/src/server/path-canonicalization.js +29 -0
- package/dist/src/server/platform-path.d.ts +3 -0
- package/dist/src/server/platform-path.js +13 -0
- package/dist/src/server/post-start-input-writer.d.ts +1 -1
- package/dist/src/server/post-start-input-writer.js +110 -13
- package/dist/src/server/preset-launch-support.d.ts +1 -1
- package/dist/src/server/preset-launch-support.js +33 -2
- package/dist/src/server/recovery-summary.d.ts +6 -1
- package/dist/src/server/recovery-summary.js +17 -17
- package/dist/src/server/restart-policy-support.d.ts +6 -1
- package/dist/src/server/restart-policy-support.js +9 -1
- package/dist/src/server/restart-policy.d.ts +2 -2
- package/dist/src/server/restart-policy.js +3 -1
- package/dist/src/server/role-template-store.d.ts +1 -0
- package/dist/src/server/role-template-store.js +11 -1
- package/dist/src/server/route-types.d.ts +43 -0
- package/dist/src/server/routes-runtime.js +2 -1
- package/dist/src/server/routes-settings.js +76 -0
- package/dist/src/server/routes-team.js +211 -1
- package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
- package/dist/src/server/routes-workflow-schedules.js +58 -0
- package/dist/src/server/routes-workflows.d.ts +2 -0
- package/dist/src/server/routes-workflows.js +83 -0
- package/dist/src/server/routes.js +4 -0
- package/dist/src/server/runtime-restart-policy.d.ts +3 -1
- package/dist/src/server/runtime-restart-policy.js +3 -1
- package/dist/src/server/runtime-store-contract.d.ts +122 -0
- package/dist/src/server/runtime-store-contract.js +1 -0
- package/dist/src/server/runtime-store-helpers.d.ts +9 -0
- package/dist/src/server/runtime-store-helpers.js +101 -2
- package/dist/src/server/runtime-store-workflows.d.ts +6 -0
- package/dist/src/server/runtime-store-workflows.js +100 -0
- package/dist/src/server/runtime-store.d.ts +3 -72
- package/dist/src/server/runtime-store.js +70 -4
- package/dist/src/server/session-capture-codex.d.ts +3 -3
- package/dist/src/server/session-capture-codex.js +9 -7
- package/dist/src/server/session-capture-gemini.d.ts +1 -1
- package/dist/src/server/session-capture-gemini.js +6 -3
- package/dist/src/server/settings-store.d.ts +3 -0
- package/dist/src/server/settings-store.js +1 -0
- package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v19.js +17 -0
- package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v20.js +20 -0
- package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v21.js +20 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +97 -1
- package/dist/src/server/system-message.d.ts +7 -0
- package/dist/src/server/system-message.js +8 -1
- package/dist/src/server/tasks-file-watcher.d.ts +13 -1
- package/dist/src/server/tasks-file-watcher.js +127 -23
- package/dist/src/server/tasks-file.d.ts +2 -1
- package/dist/src/server/tasks-file.js +32 -9
- package/dist/src/server/tasks-websocket-server.js +13 -14
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +9 -1
- package/dist/src/server/team-autostaff.d.ts +16 -0
- package/dist/src/server/team-autostaff.js +16 -0
- package/dist/src/server/team-list-serializer.d.ts +1 -1
- package/dist/src/server/team-list-serializer.js +3 -1
- package/dist/src/server/team-operations.d.ts +15 -1
- package/dist/src/server/team-operations.js +116 -11
- package/dist/src/server/terminal-protocol.js +9 -3
- package/dist/src/server/terminal-stream-hub.js +16 -10
- package/dist/src/server/terminal-ws-server.js +10 -8
- package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
- package/dist/src/server/websocket-upgrade-safety.js +35 -0
- package/dist/src/server/windows-command-line.d.ts +3 -0
- package/dist/src/server/windows-command-line.js +9 -0
- package/dist/src/server/windows-filename.d.ts +2 -0
- package/dist/src/server/windows-filename.js +33 -0
- package/dist/src/server/workflow-cli-policy.d.ts +60 -0
- package/dist/src/server/workflow-cli-policy.js +110 -0
- package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
- package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
- package/dist/src/server/workflow-feature.d.ts +15 -0
- package/dist/src/server/workflow-feature.js +15 -0
- package/dist/src/server/workflow-http-serializers.d.ts +64 -0
- package/dist/src/server/workflow-http-serializers.js +58 -0
- package/dist/src/server/workflow-run-log-store.d.ts +19 -0
- package/dist/src/server/workflow-run-log-store.js +45 -0
- package/dist/src/server/workflow-run-store.d.ts +50 -0
- package/dist/src/server/workflow-run-store.js +103 -0
- package/dist/src/server/workflow-runner.d.ts +147 -0
- package/dist/src/server/workflow-runner.js +401 -0
- package/dist/src/server/workflow-schedule-create.d.ts +14 -0
- package/dist/src/server/workflow-schedule-create.js +41 -0
- package/dist/src/server/workflow-schedule-store.d.ts +43 -0
- package/dist/src/server/workflow-schedule-store.js +112 -0
- package/dist/src/server/workflow-scheduler.d.ts +36 -0
- package/dist/src/server/workflow-scheduler.js +97 -0
- package/dist/src/server/workflow-script-loader.d.ts +34 -0
- package/dist/src/server/workflow-script-loader.js +106 -0
- package/dist/src/server/workspace-path-validation.js +16 -4
- package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
- package/dist/src/server/workspace-shell-runtime.js +24 -2
- package/dist/src/server/workspace-store-contract.d.ts +4 -1
- package/dist/src/server/workspace-store-hydration.js +23 -7
- package/dist/src/server/workspace-store-mutations.js +2 -5
- package/dist/src/server/workspace-store-support.d.ts +4 -0
- package/dist/src/server/workspace-store-support.js +13 -1
- package/dist/src/server/workspace-store.js +38 -4
- package/dist/src/shared/types.d.ts +16 -1
- package/package.json +4 -2
- package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
- package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
- package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-BYX_ocQn.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
- package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
- package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
- package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
- package/web/dist/assets/{WorkspaceTerminalPanels-CvibsPSd.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
- package/web/dist/assets/index-BiOvKIVw.css +1 -0
- package/web/dist/assets/index-DMRUklT3.js +73 -0
- package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
- package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
- package/web/dist/assets/index-BEsTmfrO.css +0 -1
- package/web/dist/assets/index-Ddb7bDN5.js +0 -75
- package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { WorkerRole } from '../shared/types.js';
|
|
2
|
+
import type { AgentLaunchConfigInput } from './agent-run-store.js';
|
|
3
|
+
import { type WorkflowCliPolicy } from './workflow-cli-policy.js';
|
|
4
|
+
import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
|
|
5
|
+
import type { WorkflowRunRecord, WorkflowRunStatus } from './workflow-run-store.js';
|
|
6
|
+
export interface RunWorkflowInput {
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
scriptPath: string;
|
|
9
|
+
hivePort: string;
|
|
10
|
+
args?: unknown;
|
|
11
|
+
/** Agent (usually the orchestrator) that fired this workflow; the runner
|
|
12
|
+
* notifies its PTY when the run finishes. Optional — workflows fired by
|
|
13
|
+
* cron / UI have no triggering agent. */
|
|
14
|
+
triggeredByAgentId?: string;
|
|
15
|
+
/** TIER 2 #5 — set internally when the runner is spawning a nested
|
|
16
|
+
* workflow() from inside another run. External callers leave it
|
|
17
|
+
* undefined; the row goes in as a top-level run. */
|
|
18
|
+
parentRunId?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface RunInlineWorkflowInput {
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
source: string;
|
|
23
|
+
/** Synthetic path stamped into workflow_runs.script_path for display only;
|
|
24
|
+
* no file is read or written. Defaults to `<inline>` if omitted. */
|
|
25
|
+
scriptPath?: string;
|
|
26
|
+
hivePort: string;
|
|
27
|
+
args?: unknown;
|
|
28
|
+
triggeredByAgentId?: string;
|
|
29
|
+
}
|
|
30
|
+
/** TIER 2 #4 — narrow port the runner needs to clone a custom role
|
|
31
|
+
* into an ephemeral worker. Returns `undefined` if the name doesn't
|
|
32
|
+
* match any template (built-in or custom); the runner falls back to
|
|
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<unknown>;
|
|
55
|
+
dispatchTaskByWorkerName: (workspaceId: string, workerName: string, text: string, input: {
|
|
56
|
+
fromAgentId: string;
|
|
57
|
+
hivePort: string;
|
|
58
|
+
workflowRunId: string;
|
|
59
|
+
stepIndex: number;
|
|
60
|
+
phase?: string;
|
|
61
|
+
label?: string;
|
|
62
|
+
}) => Promise<{
|
|
63
|
+
id: string;
|
|
64
|
+
}>;
|
|
65
|
+
deleteWorker: (workspaceId: string, workerId: string) => void;
|
|
66
|
+
}
|
|
67
|
+
/** TIER 2 #3 — narrator lane sink. The runner calls append(runId, message)
|
|
68
|
+
* every time the script invokes log(). Implementation lives in
|
|
69
|
+
* workflow-run-log-store; the port keeps the runner free of DB types. */
|
|
70
|
+
export interface WorkflowRunLogPort {
|
|
71
|
+
append(runId: string, message: string, ts?: number): void;
|
|
72
|
+
}
|
|
73
|
+
interface WorkflowRunStorePort {
|
|
74
|
+
createRun: (input: {
|
|
75
|
+
workspaceId: string;
|
|
76
|
+
scriptPath: string;
|
|
77
|
+
name: string;
|
|
78
|
+
scriptHash?: string;
|
|
79
|
+
args?: unknown;
|
|
80
|
+
parentRunId?: string | null;
|
|
81
|
+
}) => WorkflowRunRecord;
|
|
82
|
+
updateRun: (id: string, input: {
|
|
83
|
+
status?: WorkflowRunStatus;
|
|
84
|
+
phase?: string;
|
|
85
|
+
finishedAt?: number;
|
|
86
|
+
error?: string;
|
|
87
|
+
result?: unknown;
|
|
88
|
+
}) => void;
|
|
89
|
+
getRun: (id: string) => WorkflowRunRecord | undefined;
|
|
90
|
+
}
|
|
91
|
+
export interface WorkflowRunner {
|
|
92
|
+
/** Runs the script to completion; returns the FINAL record. Used by tests
|
|
93
|
+
* and any caller that wants to wait synchronously. */
|
|
94
|
+
runWorkflow: (input: RunWorkflowInput) => Promise<WorkflowRunRecord>;
|
|
95
|
+
/** Creates the run row + kicks off execution in the background; returns the
|
|
96
|
+
* INITIAL ('running') record. Used by the HTTP route so the response is
|
|
97
|
+
* fast — clients poll `getWorkflowRun(id)` for progress. */
|
|
98
|
+
startWorkflow: (input: RunWorkflowInput) => Promise<WorkflowRunRecord>;
|
|
99
|
+
/** Like startWorkflow but takes raw source (no file). Used by `team workflow
|
|
100
|
+
* run --stdin/--inline` so the orchestrator can fire workflows from a PTY
|
|
101
|
+
* without writing a file first — matches Claude Code's Workflow tool
|
|
102
|
+
* invocation model. */
|
|
103
|
+
startWorkflowInline: (input: RunInlineWorkflowInput) => Promise<WorkflowRunRecord>;
|
|
104
|
+
/** Cancel any in-flight `agent()` calls for `runId` (rejects their awaiters)
|
|
105
|
+
* and mark the next executeWorkflow catch as 'stopped' instead of 'failed'.
|
|
106
|
+
* Returns true if the run was running and got stopped, false otherwise. */
|
|
107
|
+
stopRun: (runId: string) => boolean;
|
|
108
|
+
}
|
|
109
|
+
interface ListDispatchesForStopPort {
|
|
110
|
+
listOpenDispatchIdsForRun: (runId: string) => string[];
|
|
111
|
+
}
|
|
112
|
+
export declare const createWorkflowRunner: (deps: {
|
|
113
|
+
store: RunnerStorePort;
|
|
114
|
+
workflowRunStore: WorkflowRunStorePort;
|
|
115
|
+
awaiter: WorkflowDispatchAwaiter;
|
|
116
|
+
dispatchPort: ListDispatchesForStopPort;
|
|
117
|
+
/** Resolves the workspace's on-disk path. The nested `workflow(name)` DSL
|
|
118
|
+
* call needs it to locate sibling scripts when the parent run was fired
|
|
119
|
+
* inline via `team workflow run --stdin` (TIER 1 #7) — its scriptPath is
|
|
120
|
+
* a synthetic `<inline>` token that dirname() can't traverse. */
|
|
121
|
+
resolveWorkspacePath: (workspaceId: string) => string;
|
|
122
|
+
/** TIER 2 #4 — used when agent() opts.agentType isn't a built-in role.
|
|
123
|
+
* The runner asks the resolver for a matching custom template; on hit
|
|
124
|
+
* the template's command + args replace the defaults, on miss the
|
|
125
|
+
* runner throws a clear error rather than silently spawning claude. */
|
|
126
|
+
roleTemplateResolver: RoleTemplateResolver;
|
|
127
|
+
/** TIER 2 #3 — sink for the script's `log()` calls. The runner used
|
|
128
|
+
* to drop them on server stdout; routing through this port lets the
|
|
129
|
+
* Drawer render them as a narrator lane and the completion
|
|
130
|
+
* reminder splice the tail into the orchestrator's notification. */
|
|
131
|
+
logStore: WorkflowRunLogPort;
|
|
132
|
+
/** Global workflow CLI policy (default + allowlist). Replaces the old
|
|
133
|
+
* hard-coded `claude` default: an `agent()` that omits `cli` now uses
|
|
134
|
+
* the user's configured default, and an explicit `cli` outside the
|
|
135
|
+
* allowlist fails the call with a clear, fixable error. */
|
|
136
|
+
getWorkflowCliPolicy: () => WorkflowCliPolicy;
|
|
137
|
+
/** Called when a run reaches a terminal state (completed/failed/stopped).
|
|
138
|
+
* The runtime uses this to inject a `<hive-system-reminder>` into the
|
|
139
|
+
* triggering agent's PTY so the orchestrator picks the result back up,
|
|
140
|
+
* mirroring Claude Code's `<task-notification>` flow. */
|
|
141
|
+
onRunFinished?: (input: {
|
|
142
|
+
runId: string;
|
|
143
|
+
triggeredByAgentId: string;
|
|
144
|
+
finalRecord: WorkflowRunRecord;
|
|
145
|
+
}) => void;
|
|
146
|
+
}) => WorkflowRunner;
|
|
147
|
+
export {};
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { cpus } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { assertWindowsSafeFilename } from './windows-filename.js';
|
|
5
|
+
import { resolveWorkflowCli } from './workflow-cli-policy.js';
|
|
6
|
+
import { loadWorkflowScriptFile, loadWorkflowScriptSource } from './workflow-script-loader.js';
|
|
7
|
+
import { getWorkflowAgentId } from './workspace-store-support.js';
|
|
8
|
+
// TIER 2 #2 + #11 — runtime caps. Match Claude Code's documented caps
|
|
9
|
+
// (1000 lifetime calls, min(16, cores-2) concurrent) so scripts copied
|
|
10
|
+
// from CC docs behave the same way. The duration default is Hive's own
|
|
11
|
+
// choice; CC docs don't specify one but workflows that run >60min are
|
|
12
|
+
// almost always a runaway loop in practice.
|
|
13
|
+
const DEFAULT_MAX_AGENTS_PER_RUN = 1000;
|
|
14
|
+
const DEFAULT_MAX_DURATION_MS = 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_MAX_CONCURRENT_AGENTS = Math.min(16, Math.max(2, cpus().length - 2));
|
|
16
|
+
const BUILT_IN_WORKER_ROLES = new Set(['coder', 'reviewer', 'tester', 'custom']);
|
|
17
|
+
const isBuiltInWorkerRole = (value) => BUILT_IN_WORKER_ROLES.has(value);
|
|
18
|
+
const buildModelArgs = (_cli, model) => {
|
|
19
|
+
if (!model?.trim())
|
|
20
|
+
return [];
|
|
21
|
+
/* All four supported CLIs (claude / codex / opencode / gemini) accept
|
|
22
|
+
`--model <id>` as a positional flag. Keeping the mapping centralised
|
|
23
|
+
here so future CLI quirks (e.g. opencode wanting `-m` instead) can
|
|
24
|
+
be patched without touching the runner body. */
|
|
25
|
+
return ['--model', model];
|
|
26
|
+
};
|
|
27
|
+
const toNestedWorkflowFilename = (scriptName) => {
|
|
28
|
+
const trimmed = scriptName.trim();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
throw new Error('workflow(scriptName): scriptName must be a non-empty string');
|
|
31
|
+
const filename = trimmed.toLowerCase().endsWith('.ts') ? trimmed : `${trimmed}.ts`;
|
|
32
|
+
assertWindowsSafeFilename(filename);
|
|
33
|
+
return filename;
|
|
34
|
+
};
|
|
35
|
+
export const createWorkflowRunner = (deps) => {
|
|
36
|
+
const { store, workflowRunStore, awaiter, dispatchPort, resolveWorkspacePath, roleTemplateResolver, logStore, getWorkflowCliPolicy, } = deps;
|
|
37
|
+
const stoppedRuns = new Set();
|
|
38
|
+
// In-memory map: runId → triggering agent. Lost on restart; the spec already
|
|
39
|
+
// doesn't auto-resume interrupted runs, so this is consistent.
|
|
40
|
+
const triggeringAgentByRun = new Map();
|
|
41
|
+
const executeWorkflow = async (run, loaded, args, hivePort) => {
|
|
42
|
+
const workspaceId = run.workspaceId;
|
|
43
|
+
const workflowAgentId = getWorkflowAgentId(workspaceId);
|
|
44
|
+
let stepCounter = 0;
|
|
45
|
+
const spawnedWorkers = [];
|
|
46
|
+
// Read the CLI policy once per run so a 1000-way fan-out doesn't hit the
|
|
47
|
+
// app_state table per agent() call.
|
|
48
|
+
const cliPolicy = getWorkflowCliPolicy();
|
|
49
|
+
// TIER 2 #2 + #11 — runtime caps. Per-run agent ceiling, per-call
|
|
50
|
+
// concurrency throttle, and a hard wall-clock budget. meta can
|
|
51
|
+
// override the defaults per script. The budget timer arms here and
|
|
52
|
+
// gets cleared in the finally branch so a fast-completing run
|
|
53
|
+
// doesn't leave a dangling timer in the event loop.
|
|
54
|
+
const maxAgents = typeof loaded.meta.maxAgentCalls === 'number' && loaded.meta.maxAgentCalls > 0
|
|
55
|
+
? loaded.meta.maxAgentCalls
|
|
56
|
+
: DEFAULT_MAX_AGENTS_PER_RUN;
|
|
57
|
+
const maxDurationMs = typeof loaded.meta.maxDurationMs === 'number' && loaded.meta.maxDurationMs > 0
|
|
58
|
+
? loaded.meta.maxDurationMs
|
|
59
|
+
: DEFAULT_MAX_DURATION_MS;
|
|
60
|
+
// Simple semaphore — a FIFO queue of resolvers. Each acquire() returns
|
|
61
|
+
// a release() callback that pumps the next waiter. Default
|
|
62
|
+
// concurrency is min(16, cores-2), matching CC's documented cap;
|
|
63
|
+
// intentionally shared across all parallel/pipeline calls in this
|
|
64
|
+
// run so nested fan-outs don't blow through the cap.
|
|
65
|
+
const concurrencyLimit = DEFAULT_MAX_CONCURRENT_AGENTS;
|
|
66
|
+
let inFlight = 0;
|
|
67
|
+
const waitQueue = [];
|
|
68
|
+
const acquireSlot = async () => {
|
|
69
|
+
if (inFlight < concurrencyLimit) {
|
|
70
|
+
inFlight++;
|
|
71
|
+
return () => {
|
|
72
|
+
inFlight--;
|
|
73
|
+
waitQueue.shift()?.();
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
await new Promise((resolve) => waitQueue.push(resolve));
|
|
77
|
+
inFlight++;
|
|
78
|
+
return () => {
|
|
79
|
+
inFlight--;
|
|
80
|
+
waitQueue.shift()?.();
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
// R1/R2 auto-rounding: a phase('Find') called twice in the same run gets
|
|
84
|
+
// rendered as "Find" then "Find R2" etc. Mirrors Claude Code's /workflows
|
|
85
|
+
// view — keeps the UI readable when a workflow loops phases.
|
|
86
|
+
const phaseEntryCount = new Map();
|
|
87
|
+
let currentPhaseTitle = null;
|
|
88
|
+
const phase = (title) => {
|
|
89
|
+
const trimmed = (title ?? '').toString().trim() || 'default';
|
|
90
|
+
const next = (phaseEntryCount.get(trimmed) ?? 0) + 1;
|
|
91
|
+
phaseEntryCount.set(trimmed, next);
|
|
92
|
+
currentPhaseTitle = next > 1 ? `${trimmed} R${next}` : trimmed;
|
|
93
|
+
workflowRunStore.updateRun(run.id, { phase: currentPhaseTitle });
|
|
94
|
+
};
|
|
95
|
+
const agent = async (prompt, opts = {}) => {
|
|
96
|
+
// TIER 2 #11 — hard ceiling so a runaway `while (true) await agent()`
|
|
97
|
+
// can't spawn unbounded PTY subprocesses. The check is BEFORE the
|
|
98
|
+
// step counter increments so the error message names the cap, not
|
|
99
|
+
// the over-cap step.
|
|
100
|
+
if (stepCounter >= maxAgents) {
|
|
101
|
+
throw new Error(`Workflow agent cap exceeded: ${maxAgents} calls (set meta.maxAgentCalls to raise)`);
|
|
102
|
+
}
|
|
103
|
+
const myStep = ++stepCounter;
|
|
104
|
+
// TIER 2 #4 — resolve agentType. If it's a built-in WorkerRole
|
|
105
|
+
// (coder/reviewer/tester/custom) we honour it directly with the
|
|
106
|
+
// CLI default. Otherwise look up a workspace custom role template
|
|
107
|
+
// by name: on hit we clone its command + args; on miss we throw a
|
|
108
|
+
// clear error (silent fallback to 'coder' would mask typos and
|
|
109
|
+
// make the Hive-distinctive custom-role library invisible).
|
|
110
|
+
const requestedType = opts.agentType ?? 'coder';
|
|
111
|
+
let role;
|
|
112
|
+
let command;
|
|
113
|
+
let templateArgs = [];
|
|
114
|
+
if (typeof requestedType === 'string' && !isBuiltInWorkerRole(requestedType)) {
|
|
115
|
+
const template = roleTemplateResolver.findByName(requestedType);
|
|
116
|
+
if (!template) {
|
|
117
|
+
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. ` +
|
|
118
|
+
`Create one via Add Worker → custom role, or use a built-in role.`);
|
|
119
|
+
}
|
|
120
|
+
/* Templates can carry roleType='orchestrator' for the system-level
|
|
121
|
+
Orchestrator template; that's not a valid workflow worker role,
|
|
122
|
+
so we collapse it to 'custom'. Anything else maps through as-is. */
|
|
123
|
+
role = template.roleType === 'orchestrator' ? 'custom' : template.roleType;
|
|
124
|
+
command = resolveWorkflowCli({
|
|
125
|
+
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
126
|
+
isCustomTemplate: true,
|
|
127
|
+
templateDefaultCommand: template.defaultCommand,
|
|
128
|
+
policy: cliPolicy,
|
|
129
|
+
});
|
|
130
|
+
templateArgs = template.defaultArgs;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
role = requestedType;
|
|
134
|
+
command = resolveWorkflowCli({
|
|
135
|
+
...(opts.cli !== undefined ? { requestedCli: opts.cli } : {}),
|
|
136
|
+
isCustomTemplate: false,
|
|
137
|
+
policy: cliPolicy,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const name = opts.label ?? `${requestedType}-${myStep}-${randomUUID()}`;
|
|
141
|
+
/* Model flag goes AFTER the template's own args so an explicit
|
|
142
|
+
opts.model overrides any --model the template baked in. */
|
|
143
|
+
const launchArgs = [...templateArgs, ...buildModelArgs(command, opts.model)];
|
|
144
|
+
// TIER 2 #2 — semaphore. Holding the slot across the full
|
|
145
|
+
// dispatch+await means a parallel(100) fan-out gets paced at
|
|
146
|
+
// min(16, cores-2) concurrent PTYs instead of 100 simultaneous
|
|
147
|
+
// process spawns.
|
|
148
|
+
const releaseSlot = await acquireSlot();
|
|
149
|
+
const worker = store.addWorkerWithLaunch(workspaceId, { name, role, ephemeral: true, spawnedBy: 'workflow' }, { command, args: launchArgs });
|
|
150
|
+
spawnedWorkers.push(worker.id);
|
|
151
|
+
try {
|
|
152
|
+
await store.startAgent(workspaceId, worker.id, { hivePort });
|
|
153
|
+
const dispatch = await store.dispatchTaskByWorkerName(workspaceId, name, prompt, {
|
|
154
|
+
fromAgentId: workflowAgentId,
|
|
155
|
+
hivePort,
|
|
156
|
+
workflowRunId: run.id,
|
|
157
|
+
stepIndex: myStep,
|
|
158
|
+
...(currentPhaseTitle ? { phase: currentPhaseTitle } : {}),
|
|
159
|
+
label: opts.label ?? name,
|
|
160
|
+
});
|
|
161
|
+
const report = await awaiter.awaitReport(dispatch.id, opts.timeoutMs);
|
|
162
|
+
return report.text;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
try {
|
|
166
|
+
store.deleteWorker(workspaceId, worker.id);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
/* idempotent — worker may already be gone via cascade or boot cleanup */
|
|
170
|
+
}
|
|
171
|
+
const idx = spawnedWorkers.indexOf(worker.id);
|
|
172
|
+
if (idx !== -1)
|
|
173
|
+
spawnedWorkers.splice(idx, 1);
|
|
174
|
+
releaseSlot();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
// TIER 1 #3 — per-item rejections become null (preserves the
|
|
178
|
+
// documented contract: callers use `.filter(Boolean)` patterns), BUT
|
|
179
|
+
// if the run is being stopped we re-throw so the outer catch handles
|
|
180
|
+
// it as `status='stopped'` instead of silently completing with a
|
|
181
|
+
// result array full of nulls. Per-item failures still get logged so
|
|
182
|
+
// they're not entirely invisible (TIER 2 #3 will pipe these into the
|
|
183
|
+
// run timeline via log()).
|
|
184
|
+
const catchPerItem = (value) => {
|
|
185
|
+
if (stoppedRuns.has(run.id))
|
|
186
|
+
throw value;
|
|
187
|
+
console.warn(`[workflow ${loaded.meta.name}] item failed:`, value);
|
|
188
|
+
return null;
|
|
189
|
+
};
|
|
190
|
+
const parallel = async (thunks) => {
|
|
191
|
+
return Promise.all(thunks.map((thunk) => thunk().catch((err) => catchPerItem(err))));
|
|
192
|
+
};
|
|
193
|
+
const pipeline = async (items, ...stages) => {
|
|
194
|
+
return Promise.all(items.map((item, index) => {
|
|
195
|
+
let chain = Promise.resolve(item);
|
|
196
|
+
for (const stage of stages) {
|
|
197
|
+
chain = chain.then((prev) => stage(prev, item, index));
|
|
198
|
+
}
|
|
199
|
+
return chain.catch((err) => catchPerItem(err));
|
|
200
|
+
}));
|
|
201
|
+
};
|
|
202
|
+
const log = (message) => {
|
|
203
|
+
// TIER 2 #3 — persist + still echo to stdout for server-log
|
|
204
|
+
// visibility. Authors expect `log()` to surface in the Drawer's
|
|
205
|
+
// narrator lane and in the orchestrator's completion reminder.
|
|
206
|
+
// The console.log retains the server-side breadcrumb for ops.
|
|
207
|
+
const text = typeof message === 'string' ? message : String(message);
|
|
208
|
+
try {
|
|
209
|
+
logStore.append(run.id, text);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error('[hive] swallowed:workflow.log.append', error);
|
|
213
|
+
}
|
|
214
|
+
console.log(`[workflow ${loaded.meta.name}] ${text}`);
|
|
215
|
+
};
|
|
216
|
+
// Nested workflow: look up a sibling script in the same .hive/workflows/
|
|
217
|
+
// directory and run it through THIS runner instance. The child run gets
|
|
218
|
+
// its own row in workflow_runs and shares the dispatch-await machinery
|
|
219
|
+
// (so its `agent()` calls work the same way the parent's do).
|
|
220
|
+
//
|
|
221
|
+
// TIER 1 #7 — when the parent was fired inline (`team workflow run
|
|
222
|
+
// --stdin`), its scriptPath is the synthetic token `<inline>` /
|
|
223
|
+
// `<inline:name>`. `dirname('<inline>')` collapses to `.` and the
|
|
224
|
+
// child would resolve against the runtime's CWD — wrong directory,
|
|
225
|
+
// reliably broken. Detect the synthetic prefix and fall back to the
|
|
226
|
+
// workspace's `.hive/workflows/` directory, which is the canonical
|
|
227
|
+
// sibling-script location.
|
|
228
|
+
const isSyntheticParentPath = run.scriptPath.startsWith('<inline');
|
|
229
|
+
const workflow = async (scriptName, childArgs) => {
|
|
230
|
+
if (typeof scriptName !== 'string' || !scriptName.trim()) {
|
|
231
|
+
throw new Error('workflow(scriptName): scriptName must be a non-empty string');
|
|
232
|
+
}
|
|
233
|
+
const filename = toNestedWorkflowFilename(scriptName);
|
|
234
|
+
const childPath = isSyntheticParentPath
|
|
235
|
+
? join(resolveWorkspacePath(workspaceId), '.hive', 'workflows', filename)
|
|
236
|
+
: join(dirname(run.scriptPath), filename);
|
|
237
|
+
return runWorkflow({
|
|
238
|
+
workspaceId,
|
|
239
|
+
scriptPath: childPath,
|
|
240
|
+
hivePort,
|
|
241
|
+
// TIER 2 #5 — stamp the parent run id so the UI can render the
|
|
242
|
+
// nested workflow tree.
|
|
243
|
+
parentRunId: run.id,
|
|
244
|
+
...(childArgs !== undefined ? { args: childArgs } : {}),
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
// TIER 2 #11 — wall-clock budget timer. Triggers stopRun on
|
|
248
|
+
// expiry, which routes through the same path as a user-initiated
|
|
249
|
+
// stop (in-flight awaiters reject; outer catch records 'stopped').
|
|
250
|
+
// The setTimeout is unref'd so a forgotten timer can't keep the
|
|
251
|
+
// Node process alive after shutdown. stopRun is declared further
|
|
252
|
+
// down the file but exists by the time this timer fires, so the
|
|
253
|
+
// closure reference is safe.
|
|
254
|
+
const budgetTimer = setTimeout(() => {
|
|
255
|
+
log(`[hive] maxDurationMs (${maxDurationMs}ms) exceeded — stopping run`);
|
|
256
|
+
stopRun(run.id);
|
|
257
|
+
}, maxDurationMs);
|
|
258
|
+
budgetTimer.unref?.();
|
|
259
|
+
try {
|
|
260
|
+
const factory = new Function(`${loaded.compiledFunctionSource}; return __wf`);
|
|
261
|
+
const fn = factory();
|
|
262
|
+
const returnValue = await fn(agent, parallel, pipeline, phase, log, workflow, args);
|
|
263
|
+
// TIER 1 #2 — if stop was called DURING the run, parallel/pipeline may
|
|
264
|
+
// have caught the cancel rejections (one per in-flight thunk) before
|
|
265
|
+
// the per-item catch could re-throw, e.g. when the user stops AFTER
|
|
266
|
+
// the inner Promise.all has already started but BEFORE any thunk
|
|
267
|
+
// rejects. In that race we'd otherwise write 'completed' with a
|
|
268
|
+
// degraded result (often a list of nulls), which both lies to the UI
|
|
269
|
+
// and lies to the orchestrator's completion notification. Check the
|
|
270
|
+
// marker after fn returns and record the truth instead.
|
|
271
|
+
if (stoppedRuns.has(run.id)) {
|
|
272
|
+
stoppedRuns.delete(run.id);
|
|
273
|
+
workflowRunStore.updateRun(run.id, {
|
|
274
|
+
status: 'stopped',
|
|
275
|
+
finishedAt: Date.now(),
|
|
276
|
+
error: 'Stopped by user',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
stoppedRuns.delete(run.id);
|
|
281
|
+
// M10: capture the script's return value so the UI can render a single
|
|
282
|
+
// canonical "Result" panel and the orchestrator notification can quote
|
|
283
|
+
// it. `undefined` (no explicit return) stays null on the row.
|
|
284
|
+
workflowRunStore.updateRun(run.id, {
|
|
285
|
+
status: 'completed',
|
|
286
|
+
finishedAt: Date.now(),
|
|
287
|
+
...(returnValue !== undefined ? { result: returnValue } : {}),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
const wasStopped = stoppedRuns.has(run.id);
|
|
293
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
294
|
+
workflowRunStore.updateRun(run.id, {
|
|
295
|
+
status: wasStopped ? 'stopped' : 'failed',
|
|
296
|
+
finishedAt: Date.now(),
|
|
297
|
+
error: wasStopped ? 'Stopped by user' : message,
|
|
298
|
+
});
|
|
299
|
+
stoppedRuns.delete(run.id);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
clearTimeout(budgetTimer);
|
|
303
|
+
// Belt-and-suspenders: dismiss any ephemeral worker still alive. The
|
|
304
|
+
// per-call try/finally should already have cleaned each one up; this is
|
|
305
|
+
// an idempotent safety net for unexpected paths.
|
|
306
|
+
for (const workerId of spawnedWorkers.splice(0)) {
|
|
307
|
+
try {
|
|
308
|
+
store.deleteWorker(workspaceId, workerId);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
/* swallow */
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Notify the triggering agent (orchestrator) that the run reached a
|
|
315
|
+
// terminal state. Mirrors Claude Code's <task-notification> envelope.
|
|
316
|
+
const triggeredByAgentId = triggeringAgentByRun.get(run.id);
|
|
317
|
+
triggeringAgentByRun.delete(run.id);
|
|
318
|
+
if (triggeredByAgentId && deps.onRunFinished) {
|
|
319
|
+
const finalRecord = workflowRunStore.getRun(run.id);
|
|
320
|
+
if (finalRecord) {
|
|
321
|
+
try {
|
|
322
|
+
deps.onRunFinished({ runId: run.id, triggeredByAgentId, finalRecord });
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error('[hive] swallowed:workflow.onRunFinished', error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const buildCreateInput = (input, loaded) => {
|
|
332
|
+
const createInput = {
|
|
333
|
+
workspaceId: input.workspaceId,
|
|
334
|
+
scriptPath: input.scriptPath,
|
|
335
|
+
name: loaded.meta.name,
|
|
336
|
+
scriptHash: loaded.scriptHash,
|
|
337
|
+
};
|
|
338
|
+
if (input.args !== undefined)
|
|
339
|
+
createInput.args = input.args;
|
|
340
|
+
if (input.parentRunId !== undefined)
|
|
341
|
+
createInput.parentRunId = input.parentRunId;
|
|
342
|
+
return createInput;
|
|
343
|
+
};
|
|
344
|
+
const rememberTrigger = (runId, triggeredByAgentId) => {
|
|
345
|
+
if (triggeredByAgentId)
|
|
346
|
+
triggeringAgentByRun.set(runId, triggeredByAgentId);
|
|
347
|
+
};
|
|
348
|
+
const runWorkflow = async (input) => {
|
|
349
|
+
const loaded = await loadWorkflowScriptFile(input.scriptPath);
|
|
350
|
+
const run = workflowRunStore.createRun(buildCreateInput(input, loaded));
|
|
351
|
+
rememberTrigger(run.id, input.triggeredByAgentId);
|
|
352
|
+
await executeWorkflow(run, loaded, input.args, input.hivePort);
|
|
353
|
+
const finalized = workflowRunStore.getRun(run.id);
|
|
354
|
+
if (!finalized)
|
|
355
|
+
throw new Error(`workflow run vanished mid-flight: ${run.id}`);
|
|
356
|
+
return finalized;
|
|
357
|
+
};
|
|
358
|
+
const startWorkflow = async (input) => {
|
|
359
|
+
const loaded = await loadWorkflowScriptFile(input.scriptPath);
|
|
360
|
+
const run = workflowRunStore.createRun(buildCreateInput(input, loaded));
|
|
361
|
+
rememberTrigger(run.id, input.triggeredByAgentId);
|
|
362
|
+
queueMicrotask(() => {
|
|
363
|
+
executeWorkflow(run, loaded, input.args, input.hivePort).catch((error) => {
|
|
364
|
+
console.error('[hive] swallowed:workflow.background', error);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
return run;
|
|
368
|
+
};
|
|
369
|
+
const startWorkflowInline = async (input) => {
|
|
370
|
+
const scriptPath = input.scriptPath ?? '<inline>';
|
|
371
|
+
const loaded = await loadWorkflowScriptSource(input.source, scriptPath);
|
|
372
|
+
const run = workflowRunStore.createRun(buildCreateInput({ ...input, scriptPath }, loaded));
|
|
373
|
+
rememberTrigger(run.id, input.triggeredByAgentId);
|
|
374
|
+
queueMicrotask(() => {
|
|
375
|
+
executeWorkflow(run, loaded, input.args, input.hivePort).catch((error) => {
|
|
376
|
+
console.error('[hive] swallowed:workflow.background', error);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
return run;
|
|
380
|
+
};
|
|
381
|
+
const stopRun = (runId) => {
|
|
382
|
+
const current = workflowRunStore.getRun(runId);
|
|
383
|
+
if (!current || current.status !== 'running')
|
|
384
|
+
return false;
|
|
385
|
+
stoppedRuns.add(runId);
|
|
386
|
+
// Cancel every open workflow dispatch tied to this run; this rejects the
|
|
387
|
+
// runner's pending `awaitReport` promises, which propagates up the
|
|
388
|
+
// executeWorkflow try → its catch sets status='stopped'.
|
|
389
|
+
for (const dispatchId of dispatchPort.listOpenDispatchIdsForRun(runId)) {
|
|
390
|
+
awaiter.notifyCancel(dispatchId, 'Stopped by user');
|
|
391
|
+
}
|
|
392
|
+
// If the script had no in-flight agent() call when stop was requested,
|
|
393
|
+
// the workflow may simply continue to its natural end. We still record
|
|
394
|
+
// the intent so a subsequent agent() call rejects immediately. To make
|
|
395
|
+
// single-step or no-agent() scripts also reflect 'stopped', mark the row
|
|
396
|
+
// synchronously as a hint to UI — the runner will overwrite to 'stopped'
|
|
397
|
+
// (or 'completed' if it really did finish) when executeWorkflow exits.
|
|
398
|
+
return true;
|
|
399
|
+
};
|
|
400
|
+
return { runWorkflow, startWorkflow, startWorkflowInline, stopRun };
|
|
401
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { createWorkflowScheduleStore } from './workflow-schedule-store.js';
|
|
2
|
+
type ScheduleStore = ReturnType<typeof createWorkflowScheduleStore>;
|
|
3
|
+
export interface PersistWorkflowScheduleInput {
|
|
4
|
+
workspacePath: string;
|
|
5
|
+
scheduleStore: ScheduleStore;
|
|
6
|
+
workspaceId: string;
|
|
7
|
+
source: string;
|
|
8
|
+
name: string;
|
|
9
|
+
cron: string;
|
|
10
|
+
nextRunAt: number;
|
|
11
|
+
args?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare const persistWorkflowSchedule: (input: PersistWorkflowScheduleInput) => Promise<import("./workflow-schedule-store.js").WorkflowScheduleRecord>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { BadRequestError } from './http-errors.js';
|
|
5
|
+
import { getWindowsFilenameError } from './windows-filename.js';
|
|
6
|
+
// Agent-scheduled workflows persist their inline source to a file under
|
|
7
|
+
// <workspace>/.hive/workflows/ so the existing file-based scheduler can load
|
|
8
|
+
// it at fire time — no orchestrator is in the loop when cron fires, so the
|
|
9
|
+
// source must outlive the request. The file is agent-managed and is
|
|
10
|
+
// intentionally NOT surfaced in the UI (workflows are agent-authored, not a
|
|
11
|
+
// human script library).
|
|
12
|
+
const toScriptFilename = (nameRaw) => {
|
|
13
|
+
const slug = nameRaw
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/\.ts$/, '')
|
|
17
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '');
|
|
19
|
+
if (!slug) {
|
|
20
|
+
throw new BadRequestError('schedule name must contain at least one alphanumeric character');
|
|
21
|
+
}
|
|
22
|
+
const windowsNameProbe = `${slug}.ts`;
|
|
23
|
+
const filenameError = getWindowsFilenameError(windowsNameProbe);
|
|
24
|
+
if (filenameError)
|
|
25
|
+
throw new BadRequestError(`Invalid workflow schedule name: ${filenameError}`);
|
|
26
|
+
return `${slug}-${randomUUID()}.ts`;
|
|
27
|
+
};
|
|
28
|
+
export const persistWorkflowSchedule = async (input) => {
|
|
29
|
+
const filename = toScriptFilename(input.name);
|
|
30
|
+
const dir = join(input.workspacePath, '.hive', 'workflows');
|
|
31
|
+
await mkdir(dir, { recursive: true });
|
|
32
|
+
const scriptPath = join(dir, filename);
|
|
33
|
+
await writeFile(scriptPath, input.source, 'utf8');
|
|
34
|
+
return input.scheduleStore.create({
|
|
35
|
+
workspaceId: input.workspaceId,
|
|
36
|
+
scriptPath,
|
|
37
|
+
cron: input.cron,
|
|
38
|
+
nextRunAt: input.nextRunAt,
|
|
39
|
+
...(input.args !== undefined ? { args: input.args } : {}),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
export interface WorkflowScheduleRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
scriptPath: string;
|
|
6
|
+
cron: string;
|
|
7
|
+
args: unknown;
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
lastRunAt: number | null;
|
|
10
|
+
nextRunAt: number;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
}
|
|
14
|
+
interface CreateInput {
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
scriptPath: string;
|
|
17
|
+
cron: string;
|
|
18
|
+
nextRunAt: number;
|
|
19
|
+
args?: unknown;
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface UpdateInput {
|
|
23
|
+
cron?: string;
|
|
24
|
+
args?: unknown;
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
lastRunAt?: number;
|
|
27
|
+
nextRunAt?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const createWorkflowScheduleStore: (db: Database) => {
|
|
30
|
+
create: (input: CreateInput) => WorkflowScheduleRecord;
|
|
31
|
+
update: (id: string, input: UpdateInput) => void;
|
|
32
|
+
get: (id: string) => WorkflowScheduleRecord | undefined;
|
|
33
|
+
listForWorkspace: (workspaceId: string) => WorkflowScheduleRecord[];
|
|
34
|
+
listDueSchedules: (now: number) => WorkflowScheduleRecord[];
|
|
35
|
+
deleteSchedule: (id: string) => void;
|
|
36
|
+
claimDueSchedule: (input: {
|
|
37
|
+
id: string;
|
|
38
|
+
expectedNextRunAt: number;
|
|
39
|
+
newNextRunAt: number;
|
|
40
|
+
lastRunAt: number;
|
|
41
|
+
}) => boolean;
|
|
42
|
+
};
|
|
43
|
+
export {};
|