@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,112 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
const parseArgs = (value) => {
|
|
3
|
+
if (!value)
|
|
4
|
+
return null;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(value);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const toRecord = (row) => ({
|
|
13
|
+
id: row.id,
|
|
14
|
+
workspaceId: row.workspace_id,
|
|
15
|
+
scriptPath: row.script_path,
|
|
16
|
+
cron: row.cron,
|
|
17
|
+
args: parseArgs(row.args),
|
|
18
|
+
enabled: row.enabled === 1,
|
|
19
|
+
lastRunAt: row.last_run_at,
|
|
20
|
+
nextRunAt: row.next_run_at,
|
|
21
|
+
createdAt: row.created_at,
|
|
22
|
+
updatedAt: row.updated_at,
|
|
23
|
+
});
|
|
24
|
+
export const createWorkflowScheduleStore = (db) => {
|
|
25
|
+
const create = (input) => {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const record = {
|
|
28
|
+
id: randomUUID(),
|
|
29
|
+
workspaceId: input.workspaceId,
|
|
30
|
+
scriptPath: input.scriptPath,
|
|
31
|
+
cron: input.cron,
|
|
32
|
+
args: input.args ?? null,
|
|
33
|
+
enabled: input.enabled ?? true,
|
|
34
|
+
lastRunAt: null,
|
|
35
|
+
nextRunAt: input.nextRunAt,
|
|
36
|
+
createdAt: now,
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
};
|
|
39
|
+
db.prepare(`INSERT INTO workflow_schedules (
|
|
40
|
+
id, workspace_id, script_path, cron, args, enabled,
|
|
41
|
+
last_run_at, next_run_at, created_at, updated_at
|
|
42
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(record.id, record.workspaceId, record.scriptPath, record.cron, record.args === null ? null : JSON.stringify(record.args), record.enabled ? 1 : 0, record.lastRunAt, record.nextRunAt, record.createdAt, record.updatedAt);
|
|
43
|
+
return record;
|
|
44
|
+
};
|
|
45
|
+
const update = (id, input) => {
|
|
46
|
+
const sets = [];
|
|
47
|
+
const values = [];
|
|
48
|
+
if (input.cron !== undefined) {
|
|
49
|
+
sets.push('cron = ?');
|
|
50
|
+
values.push(input.cron);
|
|
51
|
+
}
|
|
52
|
+
if (input.args !== undefined) {
|
|
53
|
+
sets.push('args = ?');
|
|
54
|
+
values.push(input.args === null ? null : JSON.stringify(input.args));
|
|
55
|
+
}
|
|
56
|
+
if (input.enabled !== undefined) {
|
|
57
|
+
sets.push('enabled = ?');
|
|
58
|
+
values.push(input.enabled ? 1 : 0);
|
|
59
|
+
}
|
|
60
|
+
if (input.lastRunAt !== undefined) {
|
|
61
|
+
sets.push('last_run_at = ?');
|
|
62
|
+
values.push(input.lastRunAt);
|
|
63
|
+
}
|
|
64
|
+
if (input.nextRunAt !== undefined) {
|
|
65
|
+
sets.push('next_run_at = ?');
|
|
66
|
+
values.push(input.nextRunAt);
|
|
67
|
+
}
|
|
68
|
+
if (sets.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
sets.push('updated_at = ?');
|
|
71
|
+
values.push(Date.now());
|
|
72
|
+
values.push(id);
|
|
73
|
+
db.prepare(`UPDATE workflow_schedules SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
74
|
+
};
|
|
75
|
+
const get = (id) => {
|
|
76
|
+
const row = db.prepare('SELECT * FROM workflow_schedules WHERE id = ?').get(id);
|
|
77
|
+
return row ? toRecord(row) : undefined;
|
|
78
|
+
};
|
|
79
|
+
const listForWorkspace = (workspaceId) => db
|
|
80
|
+
.prepare('SELECT * FROM workflow_schedules WHERE workspace_id = ? ORDER BY created_at DESC, id DESC')
|
|
81
|
+
.all(workspaceId).map(toRecord);
|
|
82
|
+
// Enabled schedules whose next_run_at has arrived — the scheduler queries
|
|
83
|
+
// this every tick and fires each.
|
|
84
|
+
const listDueSchedules = (now) => db
|
|
85
|
+
.prepare('SELECT * FROM workflow_schedules WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at')
|
|
86
|
+
.all(now).map(toRecord);
|
|
87
|
+
const deleteSchedule = (id) => {
|
|
88
|
+
db.prepare('DELETE FROM workflow_schedules WHERE id = ?').run(id);
|
|
89
|
+
};
|
|
90
|
+
// TIER 1 #5 — compare-and-swap claim. If two scheduler ticks see the same
|
|
91
|
+
// due schedule (overlapping setInterval invocations, slow esbuild
|
|
92
|
+
// transpile in startWorkflow, or future multi-process), only the one
|
|
93
|
+
// whose UPDATE matches the still-original next_run_at wins. Returns
|
|
94
|
+
// true if the caller may proceed to fire the workflow.
|
|
95
|
+
const claimDueSchedule = (input) => {
|
|
96
|
+
const result = db
|
|
97
|
+
.prepare(`UPDATE workflow_schedules
|
|
98
|
+
SET next_run_at = ?, last_run_at = ?, updated_at = ?
|
|
99
|
+
WHERE id = ? AND next_run_at = ?`)
|
|
100
|
+
.run(input.newNextRunAt, input.lastRunAt, Date.now(), input.id, input.expectedNextRunAt);
|
|
101
|
+
return result.changes === 1;
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
create,
|
|
105
|
+
update,
|
|
106
|
+
get,
|
|
107
|
+
listForWorkspace,
|
|
108
|
+
listDueSchedules,
|
|
109
|
+
deleteSchedule,
|
|
110
|
+
claimDueSchedule,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WorkflowRunRecord } from './workflow-run-store.js';
|
|
2
|
+
import type { RunWorkflowInput } from './workflow-runner.js';
|
|
3
|
+
import type { createWorkflowScheduleStore } from './workflow-schedule-store.js';
|
|
4
|
+
type ScheduleStore = ReturnType<typeof createWorkflowScheduleStore>;
|
|
5
|
+
export interface WorkflowSchedulerDeps {
|
|
6
|
+
schedules: ScheduleStore;
|
|
7
|
+
startWorkflow: (input: RunWorkflowInput) => Promise<WorkflowRunRecord>;
|
|
8
|
+
/** Defensive guard against orphan schedules (TIER 1 #4). The workspace
|
|
9
|
+
* delete cascade clears workflow_schedules in the same transaction so
|
|
10
|
+
* fresh orphans cannot appear; this port catches any pre-existing or
|
|
11
|
+
* externally-introduced orphan and self-heals by deleting the schedule
|
|
12
|
+
* before it can fire and crash startWorkflow → re-fire next tick. */
|
|
13
|
+
workspaceExists?: (workspaceId: string) => boolean;
|
|
14
|
+
/** HivePort to pass into every fired workflow. Resolved lazily so the
|
|
15
|
+
* scheduler doesn't need to be reconstructed when the runtime listens
|
|
16
|
+
* on a different port (e.g. tests using port 0). */
|
|
17
|
+
getHivePort?: () => string;
|
|
18
|
+
/** Test seam: defaults to cron-parser. Returns next fire time in ms-epoch. */
|
|
19
|
+
computeNextRunAt?: (cron: string, after: Date) => number;
|
|
20
|
+
/** Experimental workflow gate. When provided and returns false, ticks fire
|
|
21
|
+
* nothing (scheduled runs are held, not dropped — they fire once re-enabled).
|
|
22
|
+
* Omitted → no gate (the scheduler unit tests rely on this default). */
|
|
23
|
+
isWorkflowEnabled?: () => boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkflowScheduler {
|
|
26
|
+
/** Run one tick using the given `now` (ms-epoch). For both production and tests. */
|
|
27
|
+
tick: (now?: number) => Promise<void>;
|
|
28
|
+
/** Arm a setInterval that calls tick() every `tickIntervalMs`. Returns void. */
|
|
29
|
+
start: (input?: {
|
|
30
|
+
tickIntervalMs?: number;
|
|
31
|
+
}) => void;
|
|
32
|
+
/** Clear the timer. Idempotent. */
|
|
33
|
+
close: () => void;
|
|
34
|
+
}
|
|
35
|
+
export declare const createWorkflowScheduler: (deps: WorkflowSchedulerDeps) => WorkflowScheduler;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
2
|
+
const defaultComputeNextRunAt = (cron, after) => {
|
|
3
|
+
// cron-parser 5.x: `currentDate` is the reference point; `next()` returns
|
|
4
|
+
// the first strictly-greater fire. UTC by default.
|
|
5
|
+
const expr = CronExpressionParser.parse(cron, { currentDate: after, tz: 'UTC' });
|
|
6
|
+
return expr.next().toDate().getTime();
|
|
7
|
+
};
|
|
8
|
+
const DEFAULT_TICK_INTERVAL_MS = 30_000;
|
|
9
|
+
export const createWorkflowScheduler = (deps) => {
|
|
10
|
+
const computeNext = deps.computeNextRunAt ?? defaultComputeNextRunAt;
|
|
11
|
+
let timer = null;
|
|
12
|
+
const scheduler = {
|
|
13
|
+
async tick(now = Date.now()) {
|
|
14
|
+
// Experimental gate: while workflows are disabled, hold all scheduled
|
|
15
|
+
// runs (don't fire, don't advance nextRunAt) so they resume cleanly
|
|
16
|
+
// once re-enabled.
|
|
17
|
+
if (deps.isWorkflowEnabled && !deps.isWorkflowEnabled())
|
|
18
|
+
return;
|
|
19
|
+
const due = deps.schedules.listDueSchedules(now);
|
|
20
|
+
for (const schedule of due) {
|
|
21
|
+
// TIER 1 #4 — self-heal orphan schedules: if a schedule survived a
|
|
22
|
+
// workspace-delete operation (shouldn't happen after the cascade
|
|
23
|
+
// fix, but defending against historical orphans), drop it now so
|
|
24
|
+
// it doesn't error-spam every tick from here forward.
|
|
25
|
+
if (deps.workspaceExists && !deps.workspaceExists(schedule.workspaceId)) {
|
|
26
|
+
console.warn('[hive] workflow-scheduler: deleting orphan schedule', {
|
|
27
|
+
scheduleId: schedule.id,
|
|
28
|
+
workspaceId: schedule.workspaceId,
|
|
29
|
+
});
|
|
30
|
+
deps.schedules.deleteSchedule(schedule.id);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Compute the next fire BEFORE firing this one so a startWorkflow
|
|
34
|
+
// exception cannot keep us re-firing the same row each tick.
|
|
35
|
+
let nextRunAt;
|
|
36
|
+
try {
|
|
37
|
+
nextRunAt = computeNext(schedule.cron, new Date(now));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('[hive] workflow-scheduler: invalid cron, disabling schedule', {
|
|
41
|
+
scheduleId: schedule.id,
|
|
42
|
+
cron: schedule.cron,
|
|
43
|
+
error: error instanceof Error ? error.message : String(error),
|
|
44
|
+
});
|
|
45
|
+
deps.schedules.update(schedule.id, { enabled: false });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// TIER 1 #5 — compare-and-swap claim. If a previous tick is still
|
|
49
|
+
// running (slow esbuild / slow startWorkflow / many due schedules
|
|
50
|
+
// in one tick) and setInterval fires again, both ticks would
|
|
51
|
+
// otherwise see the same due schedule and fire it twice. CAS on
|
|
52
|
+
// the original next_run_at means only the first tick wins;
|
|
53
|
+
// losers see changes=0 and quietly skip.
|
|
54
|
+
const claimed = deps.schedules.claimDueSchedule({
|
|
55
|
+
id: schedule.id,
|
|
56
|
+
expectedNextRunAt: schedule.nextRunAt ?? 0,
|
|
57
|
+
newNextRunAt: nextRunAt,
|
|
58
|
+
lastRunAt: now,
|
|
59
|
+
});
|
|
60
|
+
if (!claimed)
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
await deps.startWorkflow({
|
|
64
|
+
workspaceId: schedule.workspaceId,
|
|
65
|
+
scriptPath: schedule.scriptPath,
|
|
66
|
+
hivePort: deps.getHivePort?.() ?? '',
|
|
67
|
+
...(schedule.args !== undefined && schedule.args !== null
|
|
68
|
+
? { args: schedule.args }
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error('[hive] workflow-scheduler: startWorkflow failed', {
|
|
74
|
+
scheduleId: schedule.id,
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
start({ tickIntervalMs = DEFAULT_TICK_INTERVAL_MS } = {}) {
|
|
81
|
+
if (timer)
|
|
82
|
+
clearInterval(timer);
|
|
83
|
+
timer = setInterval(() => {
|
|
84
|
+
scheduler.tick().catch((error) => {
|
|
85
|
+
console.error('[hive] swallowed:workflow-scheduler.tick', error);
|
|
86
|
+
});
|
|
87
|
+
}, tickIntervalMs);
|
|
88
|
+
},
|
|
89
|
+
close() {
|
|
90
|
+
if (timer) {
|
|
91
|
+
clearInterval(timer);
|
|
92
|
+
timer = null;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return scheduler;
|
|
97
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface WorkflowMeta {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
cron?: string;
|
|
5
|
+
phases?: Array<{
|
|
6
|
+
title: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** TIER 2 #11 — per-script budget overrides. Hard cap on total
|
|
11
|
+
* `agent()` calls (default 1000 — matches CC's lifetime cap) and on
|
|
12
|
+
* wall-clock duration in ms (default 60 min). Both apply to the
|
|
13
|
+
* ENTIRE run including nested workflow() calls; exceeding either
|
|
14
|
+
* rejects the offending agent() call and the run transitions to
|
|
15
|
+
* 'failed' (or 'stopped' if duration was the trigger). */
|
|
16
|
+
maxAgentCalls?: number;
|
|
17
|
+
maxDurationMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LoadedWorkflow {
|
|
20
|
+
meta: WorkflowMeta;
|
|
21
|
+
scriptPath: string;
|
|
22
|
+
scriptHash: string;
|
|
23
|
+
/** Transpiled `async function __wf(...) {…}` — the runner evals it via
|
|
24
|
+
* `new Function(source + '; return __wf')()` and calls it with the DSL. */
|
|
25
|
+
compiledFunctionSource: string;
|
|
26
|
+
}
|
|
27
|
+
interface ExtractedMeta {
|
|
28
|
+
meta: WorkflowMeta;
|
|
29
|
+
body: string;
|
|
30
|
+
}
|
|
31
|
+
export declare const extractMeta: (source: string) => ExtractedMeta;
|
|
32
|
+
export declare const loadWorkflowScriptSource: (source: string, scriptPath: string) => Promise<LoadedWorkflow>;
|
|
33
|
+
export declare const loadWorkflowScriptFile: (absPath: string) => Promise<LoadedWorkflow>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
const DSL_PARAMS = 'agent, parallel, pipeline, phase, log, workflow, args';
|
|
4
|
+
// Find the matching close brace for the object literal whose '{' is at
|
|
5
|
+
// openIndex, ignoring braces inside '...' "..." `...` strings and // /* */
|
|
6
|
+
// comments so they don't miscount the depth.
|
|
7
|
+
const matchBrace = (source, openIndex) => {
|
|
8
|
+
let depth = 0;
|
|
9
|
+
let i = openIndex;
|
|
10
|
+
let str = null;
|
|
11
|
+
while (i < source.length) {
|
|
12
|
+
const ch = source[i];
|
|
13
|
+
const next = source[i + 1];
|
|
14
|
+
if (str) {
|
|
15
|
+
if (ch === '\\') {
|
|
16
|
+
i += 2;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (ch === str)
|
|
20
|
+
str = null;
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === '/' && next === '/') {
|
|
25
|
+
const nl = source.indexOf('\n', i);
|
|
26
|
+
i = nl === -1 ? source.length : nl;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (ch === '/' && next === '*') {
|
|
30
|
+
const end = source.indexOf('*/', i + 2);
|
|
31
|
+
i = end === -1 ? source.length : end + 2;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
35
|
+
str = ch;
|
|
36
|
+
i += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === '{')
|
|
40
|
+
depth += 1;
|
|
41
|
+
else if (ch === '}') {
|
|
42
|
+
depth -= 1;
|
|
43
|
+
if (depth === 0)
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
i += 1;
|
|
47
|
+
}
|
|
48
|
+
throw new Error('workflow meta: unbalanced braces in `export const meta`');
|
|
49
|
+
};
|
|
50
|
+
export const extractMeta = (source) => {
|
|
51
|
+
const re = /export\s+const\s+meta\s*(?::[^=]+)?=\s*\{/;
|
|
52
|
+
const m = re.exec(source);
|
|
53
|
+
if (!m) {
|
|
54
|
+
throw new Error('workflow script must `export const meta = { name, description }`');
|
|
55
|
+
}
|
|
56
|
+
const braceStart = source.indexOf('{', m.index + m[0].length - 1);
|
|
57
|
+
const braceEnd = matchBrace(source, braceStart);
|
|
58
|
+
const literal = source.slice(braceStart, braceEnd + 1);
|
|
59
|
+
let meta;
|
|
60
|
+
try {
|
|
61
|
+
// meta MUST be a pure literal (no calls/vars); eval it in isolation.
|
|
62
|
+
meta = new Function(`return (${literal})`)();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`workflow meta is not a plain literal: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
if (!meta || typeof meta.name !== 'string' || !meta.name.trim()) {
|
|
68
|
+
throw new Error('workflow meta requires a non-empty `name`');
|
|
69
|
+
}
|
|
70
|
+
if (typeof meta.description !== 'string') {
|
|
71
|
+
throw new Error('workflow meta requires a `description`');
|
|
72
|
+
}
|
|
73
|
+
let after = braceEnd + 1;
|
|
74
|
+
const tailMatch = /^(\s*as\s+const)?\s*;?/.exec(source.slice(after));
|
|
75
|
+
if (tailMatch)
|
|
76
|
+
after += tailMatch[0].length;
|
|
77
|
+
const body = source.slice(0, m.index) + source.slice(after);
|
|
78
|
+
return { meta, body };
|
|
79
|
+
};
|
|
80
|
+
// Module-level cache for esbuild's dynamic import. esbuild's top-level
|
|
81
|
+
// invariant check requires `new TextEncoder().encode('') instanceof
|
|
82
|
+
// Uint8Array` — true in Node, FALSE in jsdom's realm. Workflow runs are not
|
|
83
|
+
// designed to be exercised end-to-end inside a jsdom test environment;
|
|
84
|
+
// jsdom-hosted tests stub the run step at the network layer.
|
|
85
|
+
let esbuildModulePromise = null;
|
|
86
|
+
const loadEsbuild = async () => {
|
|
87
|
+
if (!esbuildModulePromise)
|
|
88
|
+
esbuildModulePromise = import('esbuild');
|
|
89
|
+
return esbuildModulePromise;
|
|
90
|
+
};
|
|
91
|
+
export const loadWorkflowScriptSource = async (source, scriptPath) => {
|
|
92
|
+
if (/^\s*import\s/m.test(source)) {
|
|
93
|
+
throw new Error('workflow scripts may not use `import`; use the ambient DSL + inline schemas');
|
|
94
|
+
}
|
|
95
|
+
const { meta, body } = extractMeta(source);
|
|
96
|
+
const wrapped = `async function __wf(${DSL_PARAMS}) {\n${body}\n}`;
|
|
97
|
+
// Lazy-load esbuild: its native binary breaks under jsdom/worker contexts
|
|
98
|
+
// used by the web test suite, so importing it at module-load time would
|
|
99
|
+
// crash any test file that transitively pulls in the runtime store. The
|
|
100
|
+
// transpile path is only reached when a workflow actually runs.
|
|
101
|
+
const { transform } = await loadEsbuild();
|
|
102
|
+
const { code } = await transform(wrapped, { loader: 'ts', target: 'es2022' });
|
|
103
|
+
const scriptHash = createHash('sha256').update(code).digest('hex');
|
|
104
|
+
return { meta, scriptPath, scriptHash, compiledFunctionSource: code };
|
|
105
|
+
};
|
|
106
|
+
export const loadWorkflowScriptFile = async (absPath) => loadWorkflowScriptSource(await readFile(absPath, 'utf8'), absPath);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import { BadRequestError } from './http-errors.js';
|
|
3
|
+
import { realpathNative } from './path-canonicalization.js';
|
|
3
4
|
export const validateWorkspacePath = (path) => {
|
|
4
5
|
if (typeof path !== 'string' || path.trim().length === 0) {
|
|
5
6
|
throw new BadRequestError('Workspace path is required');
|
|
@@ -7,16 +8,27 @@ export const validateWorkspacePath = (path) => {
|
|
|
7
8
|
const candidate = path.trim();
|
|
8
9
|
let resolved;
|
|
9
10
|
try {
|
|
10
|
-
resolved =
|
|
11
|
+
resolved = realpathNative(candidate);
|
|
11
12
|
}
|
|
12
|
-
catch {
|
|
13
|
+
catch (error) {
|
|
14
|
+
const code = error?.code;
|
|
15
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
16
|
+
throw new BadRequestError(`Workspace path is not accessible: ${candidate}`);
|
|
17
|
+
}
|
|
18
|
+
if (code === 'ENAMETOOLONG') {
|
|
19
|
+
throw new BadRequestError(`Workspace path is too long: ${candidate}`);
|
|
20
|
+
}
|
|
13
21
|
throw new BadRequestError(`Workspace path does not exist: ${candidate}`);
|
|
14
22
|
}
|
|
15
23
|
let stat;
|
|
16
24
|
try {
|
|
17
25
|
stat = statSync(resolved);
|
|
18
26
|
}
|
|
19
|
-
catch {
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error?.code;
|
|
29
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
30
|
+
throw new BadRequestError(`Workspace path is not accessible: ${candidate}`);
|
|
31
|
+
}
|
|
20
32
|
throw new BadRequestError(`Workspace path does not exist: ${candidate}`);
|
|
21
33
|
}
|
|
22
34
|
if (!stat.isDirectory()) {
|
|
@@ -7,6 +7,11 @@ export declare const resolveWorkspaceShellLaunch: (env?: NodeJS.ProcessEnv, plat
|
|
|
7
7
|
args: string[];
|
|
8
8
|
command: string;
|
|
9
9
|
};
|
|
10
|
+
export declare const resolveWorkspaceShellStart: (workspacePath: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => {
|
|
11
|
+
args: string[];
|
|
12
|
+
command: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
};
|
|
10
15
|
export declare const createWorkspaceShellRuntime: (agentManager: AgentManager | undefined) => {
|
|
11
16
|
close(): void;
|
|
12
17
|
closeRun(workspaceId: string, runId: string): boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
+
import { escapeCmdToken } from './windows-command-line.js';
|
|
2
3
|
const WORKSPACE_SHELL_SUFFIX = ':shell';
|
|
3
4
|
const WORKSPACE_SHELL_LABEL = 'Shell';
|
|
4
5
|
const EXITED_SHELL_RETENTION_MS = 5000;
|
|
@@ -20,6 +21,27 @@ export const resolveWorkspaceShellLaunch = (env = process.env, platform = proces
|
|
|
20
21
|
const command = env.SHELL || '/bin/sh';
|
|
21
22
|
return { command, args: shouldUseLoginShell(command) ? ['-l'] : [] };
|
|
22
23
|
};
|
|
24
|
+
const isWindowsUncPath = (path, platform = process.platform) => platform === 'win32' && /^[\\/]{2}[^\\/]+[\\/]+[^\\/]+/u.test(path);
|
|
25
|
+
const getWindowsSafeShellCwd = (env = process.env) => {
|
|
26
|
+
const systemRoot = getEnvValue(env, 'SystemRoot', 'win32');
|
|
27
|
+
if (systemRoot)
|
|
28
|
+
return systemRoot;
|
|
29
|
+
const systemDrive = getEnvValue(env, 'SystemDrive', 'win32');
|
|
30
|
+
if (systemDrive)
|
|
31
|
+
return `${systemDrive}\\`;
|
|
32
|
+
return process.cwd();
|
|
33
|
+
};
|
|
34
|
+
export const resolveWorkspaceShellStart = (workspacePath, env = process.env, platform = process.platform) => {
|
|
35
|
+
const launch = resolveWorkspaceShellLaunch(env, platform);
|
|
36
|
+
if (isWindowsUncPath(workspacePath, platform)) {
|
|
37
|
+
return {
|
|
38
|
+
args: ['/d', '/s', '/k', `pushd ${escapeCmdToken(workspacePath)}`],
|
|
39
|
+
command: launch.command,
|
|
40
|
+
cwd: getWindowsSafeShellCwd(env),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { ...launch, cwd: workspacePath };
|
|
44
|
+
};
|
|
23
45
|
export const createWorkspaceShellRuntime = (agentManager) => {
|
|
24
46
|
const labelsByRunId = new Map();
|
|
25
47
|
const workspaceIdsByRunId = new Map();
|
|
@@ -176,12 +198,12 @@ export const createWorkspaceShellRuntime = (agentManager) => {
|
|
|
176
198
|
},
|
|
177
199
|
async start(workspace) {
|
|
178
200
|
const startedAt = Date.now();
|
|
179
|
-
const launch =
|
|
201
|
+
const launch = resolveWorkspaceShellStart(workspace.path);
|
|
180
202
|
const run = await requireManager().startAgent({
|
|
181
203
|
agentId: getWorkspaceShellAgentId(workspace.id),
|
|
182
204
|
args: launch.args,
|
|
183
205
|
command: launch.command,
|
|
184
|
-
cwd:
|
|
206
|
+
cwd: launch.cwd,
|
|
185
207
|
env: {
|
|
186
208
|
COLORTERM: 'truecolor',
|
|
187
209
|
FORCE_COLOR: '1',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSummary, TeamListItem, WorkerRole, WorkspaceSummary } from '../shared/types.js';
|
|
1
|
+
import type { AgentSummary, TeamListItem, WorkerRole, WorkerSpawnSource, WorkspaceSummary } from '../shared/types.js';
|
|
2
2
|
export interface WorkspaceRecord {
|
|
3
3
|
summary: WorkspaceSummary;
|
|
4
4
|
agents: AgentSummary[];
|
|
@@ -7,6 +7,8 @@ export interface WorkerInput {
|
|
|
7
7
|
description?: string;
|
|
8
8
|
name: string;
|
|
9
9
|
role: WorkerRole;
|
|
10
|
+
ephemeral?: boolean;
|
|
11
|
+
spawnedBy?: WorkerSpawnSource;
|
|
10
12
|
}
|
|
11
13
|
export interface WorkspaceStore {
|
|
12
14
|
addWorker: (workspaceId: string, input: WorkerInput) => AgentSummary;
|
|
@@ -19,6 +21,7 @@ export interface WorkspaceStore {
|
|
|
19
21
|
getWorkerByName: (workspaceId: string, workerName: string) => AgentSummary;
|
|
20
22
|
getWorkspaceSnapshot: (workspaceId: string) => WorkspaceRecord;
|
|
21
23
|
hasAgent: (workspaceId: string, agentId: string) => boolean;
|
|
24
|
+
hasWorkspace: (workspaceId: string) => boolean;
|
|
22
25
|
listWorkers: (workspaceId: string) => TeamListItem[];
|
|
23
26
|
listWorkspaces: () => WorkspaceSummary[];
|
|
24
27
|
markAgentStarted: (workspaceId: string, agentId: string) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDefaultRoleDescription } from './role-templates.js';
|
|
2
|
-
import { applyPendingTaskCount, createOrchestrator, isWorkerAgent, } from './workspace-store-support.js';
|
|
2
|
+
import { applyPendingTaskCount, createOrchestrator, createWorkflowAgent, isWorkerAgent, } from './workspace-store-support.js';
|
|
3
3
|
const createWorkerSummary = (workspaceId, row) => ({
|
|
4
4
|
id: row.id,
|
|
5
5
|
workspaceId,
|
|
@@ -8,7 +8,25 @@ const createWorkerSummary = (workspaceId, row) => ({
|
|
|
8
8
|
role: row.role,
|
|
9
9
|
status: 'stopped',
|
|
10
10
|
pendingTaskCount: 0,
|
|
11
|
+
ephemeral: row.ephemeral === 1,
|
|
12
|
+
spawnedBy: row.spawned_by ?? null,
|
|
11
13
|
});
|
|
14
|
+
/**
|
|
15
|
+
* Build the worker SELECT defensively. The global runtime data dir
|
|
16
|
+
* (`~/.config/hive`) is shared across every Hive install on the machine, so a
|
|
17
|
+
* DB may have been migrated forward by a NEWER Hive whose `schema_version`
|
|
18
|
+
* already lists 19+. In that case our own v19 ALTER is skipped and the
|
|
19
|
+
* `workers` table can lack `ephemeral`/`spawned_by`. Selecting a non-existent
|
|
20
|
+
* column throws and crashes startup, so we only request the optional columns
|
|
21
|
+
* when they actually exist; `createWorkerSummary` defaults them otherwise.
|
|
22
|
+
*/
|
|
23
|
+
const buildWorkerSelect = (db, whereClause) => {
|
|
24
|
+
const present = new Set(db.prepare('PRAGMA table_info(workers)').all().map((c) => c.name));
|
|
25
|
+
const optional = ['ephemeral', 'spawned_by'].filter((column) => present.has(column));
|
|
26
|
+
const columns = ['id', 'workspace_id', 'name', 'description', 'role', ...optional].join(', ');
|
|
27
|
+
const where = whereClause ? `${whereClause} ` : '';
|
|
28
|
+
return `SELECT ${columns} FROM workers ${where}ORDER BY created_at ASC`;
|
|
29
|
+
};
|
|
12
30
|
const applyMessageKinds = (workspaces, messageKinds, workspaceId) => {
|
|
13
31
|
for (const row of messageKinds) {
|
|
14
32
|
if (workspaceId && row.workspace_id !== workspaceId) {
|
|
@@ -33,10 +51,10 @@ export const hydrateWorkspaceFromDb = (db, workspaces, messageKinds, workspaceId
|
|
|
33
51
|
}
|
|
34
52
|
workspaces.set(row.id, {
|
|
35
53
|
summary: { id: row.id, name: row.name, path: row.path },
|
|
36
|
-
agents: [createOrchestrator(row.id)],
|
|
54
|
+
agents: [createOrchestrator(row.id), createWorkflowAgent(row.id)],
|
|
37
55
|
});
|
|
38
56
|
for (const workerRow of db
|
|
39
|
-
.prepare(
|
|
57
|
+
.prepare(buildWorkerSelect(db, 'WHERE workspace_id = ?'))
|
|
40
58
|
.all(workspaceId)) {
|
|
41
59
|
workspaces.get(workspaceId)?.agents.push(createWorkerSummary(workerRow.workspace_id, workerRow));
|
|
42
60
|
}
|
|
@@ -48,12 +66,10 @@ export const seedWorkspacesFromDb = (db, workspaces, messageKinds) => {
|
|
|
48
66
|
.all()) {
|
|
49
67
|
workspaces.set(row.id, {
|
|
50
68
|
summary: { id: row.id, name: row.name, path: row.path },
|
|
51
|
-
agents: [createOrchestrator(row.id)],
|
|
69
|
+
agents: [createOrchestrator(row.id), createWorkflowAgent(row.id)],
|
|
52
70
|
});
|
|
53
71
|
}
|
|
54
|
-
for (const row of db
|
|
55
|
-
.prepare('SELECT id, workspace_id, name, description, role FROM workers ORDER BY created_at ASC')
|
|
56
|
-
.all()) {
|
|
72
|
+
for (const row of db.prepare(buildWorkerSelect(db, '')).all()) {
|
|
57
73
|
workspaces.get(row.workspace_id)?.agents.push(createWorkerSummary(row.workspace_id, row));
|
|
58
74
|
}
|
|
59
75
|
applyMessageKinds(workspaces, messageKinds);
|
|
@@ -24,11 +24,8 @@ export const getWorkerByNameRecord = (workspaces, workspaceId, workerName) => {
|
|
|
24
24
|
return worker;
|
|
25
25
|
};
|
|
26
26
|
export const markAgentStarted = (workspaces, workspaceId, agentId) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// dispatch ledger replayed pendingTaskCount > 0 during hydration. The next
|
|
30
|
-
// team send will flip status to 'working' via markTaskDispatched.
|
|
31
|
-
getAgentRecord(workspaces, workspaceId, agentId).status = 'idle';
|
|
27
|
+
const agent = getAgentRecord(workspaces, workspaceId, agentId);
|
|
28
|
+
agent.status = isWorkerAgent(agent) ? getStatusFromPendingCount(agent.pendingTaskCount) : 'idle';
|
|
32
29
|
};
|
|
33
30
|
export const markAgentStopped = (workspaces, workspaceId, agentId) => {
|
|
34
31
|
getAgentRecord(workspaces, workspaceId, agentId).status = 'stopped';
|
|
@@ -15,11 +15,15 @@ export interface WorkerRow {
|
|
|
15
15
|
name: string;
|
|
16
16
|
description: string | null;
|
|
17
17
|
role: WorkerRole;
|
|
18
|
+
ephemeral: number;
|
|
19
|
+
spawned_by: string | null;
|
|
18
20
|
}
|
|
19
21
|
export interface WorkspaceSummaryRow extends WorkspaceRow {
|
|
20
22
|
}
|
|
21
23
|
export declare const getOrchestratorId: (workspaceId: string) => string;
|
|
22
24
|
export declare const createOrchestrator: (workspaceId: string) => AgentSummary;
|
|
25
|
+
export declare const getWorkflowAgentId: (workspaceId: string) => string;
|
|
26
|
+
export declare const createWorkflowAgent: (workspaceId: string) => AgentSummary;
|
|
23
27
|
export declare const isWorkerAgent: (agent: AgentSummary) => agent is AgentSummary & {
|
|
24
28
|
role: WorkerRole;
|
|
25
29
|
};
|
|
@@ -9,8 +9,20 @@ export const createOrchestrator = (workspaceId) => ({
|
|
|
9
9
|
status: 'stopped',
|
|
10
10
|
pendingTaskCount: 0,
|
|
11
11
|
});
|
|
12
|
+
export const getWorkflowAgentId = (workspaceId) => `${workspaceId}:__workflow__`;
|
|
13
|
+
// In-memory pseudo-agent (no DB row, no PTY) that gives the deterministic
|
|
14
|
+
// workflow runner a dispatch identity — mirrors the orchestrator pseudo-agent.
|
|
15
|
+
export const createWorkflowAgent = (workspaceId) => ({
|
|
16
|
+
id: getWorkflowAgentId(workspaceId),
|
|
17
|
+
workspaceId,
|
|
18
|
+
name: 'Workflow',
|
|
19
|
+
description: 'Hive workflow runner — deterministic multi-agent orchestration driver.',
|
|
20
|
+
role: 'workflow',
|
|
21
|
+
status: 'stopped',
|
|
22
|
+
pendingTaskCount: 0,
|
|
23
|
+
});
|
|
12
24
|
export const isWorkerAgent = (agent) => {
|
|
13
|
-
return agent.role !== 'orchestrator';
|
|
25
|
+
return agent.role !== 'orchestrator' && agent.role !== 'workflow';
|
|
14
26
|
};
|
|
15
27
|
export const getStatusFromPendingCount = (pendingTaskCount) => {
|
|
16
28
|
return pendingTaskCount > 0 ? 'working' : 'idle';
|