@hellcoder/companion 0.96.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/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { Cron } from "croner";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import type { AgentConfig, AgentExecution } from "./agent-types.js";
|
|
6
|
+
import type { CliLauncher, SdkSessionInfo } from "./cli-launcher.js";
|
|
7
|
+
import type { WsBridge } from "./ws-bridge.js";
|
|
8
|
+
import * as agentStore from "./agent-store.js";
|
|
9
|
+
import * as envManager from "./env-manager.js";
|
|
10
|
+
import * as sessionNames from "./session-names.js";
|
|
11
|
+
import { ExecutionStore } from "./execution-store.js";
|
|
12
|
+
|
|
13
|
+
/** Max consecutive failures before auto-disabling an agent */
|
|
14
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
15
|
+
/** Max time to wait for CLI to connect (ms) */
|
|
16
|
+
const CLI_CONNECT_TIMEOUT_MS = 30_000;
|
|
17
|
+
/** Poll interval when waiting for CLI connection */
|
|
18
|
+
const CLI_CONNECT_POLL_MS = 500;
|
|
19
|
+
|
|
20
|
+
export interface ExecuteAgentOptions {
|
|
21
|
+
force?: boolean;
|
|
22
|
+
triggerType?: "manual" | "webhook" | "schedule" | "linear";
|
|
23
|
+
additionalEnv?: Record<string, string>;
|
|
24
|
+
systemPrompt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class AgentExecutor {
|
|
28
|
+
private timers = new Map<string, Cron>();
|
|
29
|
+
private launcher: CliLauncher;
|
|
30
|
+
private wsBridge: WsBridge;
|
|
31
|
+
/** In-memory execution history (last N per agent) */
|
|
32
|
+
private executions = new Map<string, AgentExecution[]>();
|
|
33
|
+
private static readonly MAX_EXECUTIONS_PER_AGENT = 50;
|
|
34
|
+
/** Persistent execution store (JSONL on disk) */
|
|
35
|
+
private executionStore = new ExecutionStore();
|
|
36
|
+
|
|
37
|
+
constructor(launcher: CliLauncher, wsBridge: WsBridge) {
|
|
38
|
+
this.launcher = launcher;
|
|
39
|
+
this.wsBridge = wsBridge;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Start all enabled agents with schedule triggers from disk. Called once at server startup. */
|
|
43
|
+
startAll(): void {
|
|
44
|
+
const agents = agentStore.listAgents();
|
|
45
|
+
let started = 0;
|
|
46
|
+
for (const agent of agents) {
|
|
47
|
+
if (agent.enabled && agent.triggers?.schedule?.enabled) {
|
|
48
|
+
this.scheduleAgent(agent);
|
|
49
|
+
started++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (started > 0) {
|
|
53
|
+
console.log(`[agent-executor] Started ${started} scheduled agent(s)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Schedule (or reschedule) an agent's cron trigger. */
|
|
58
|
+
scheduleAgent(agent: AgentConfig): void {
|
|
59
|
+
this.stopAgent(agent.id);
|
|
60
|
+
|
|
61
|
+
const schedule = agent.triggers?.schedule;
|
|
62
|
+
if (!agent.enabled || !schedule?.enabled || !schedule.expression) return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (schedule.recurring) {
|
|
66
|
+
const cronTask = new Cron(schedule.expression, {}, () => {
|
|
67
|
+
this.executeAgent(agent.id, undefined, { triggerType: "schedule" }).catch((err) => {
|
|
68
|
+
console.error(`[agent-executor] Unhandled error in agent "${agent.name}":`, err);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
this.timers.set(agent.id, cronTask);
|
|
72
|
+
console.log(`[agent-executor] Scheduled "${agent.name}" with cron "${schedule.expression}"`);
|
|
73
|
+
} else {
|
|
74
|
+
// One-shot: schedule for the specified datetime
|
|
75
|
+
const targetTime = new Date(schedule.expression);
|
|
76
|
+
if (targetTime.getTime() > Date.now()) {
|
|
77
|
+
const cronTask = new Cron(targetTime, () => {
|
|
78
|
+
this.executeAgent(agent.id, undefined, { triggerType: "schedule" })
|
|
79
|
+
.then(() => {
|
|
80
|
+
// Auto-disable schedule after one-shot execution
|
|
81
|
+
const current = agentStore.getAgent(agent.id);
|
|
82
|
+
if (current?.triggers?.schedule) {
|
|
83
|
+
agentStore.updateAgent(agent.id, {
|
|
84
|
+
triggers: {
|
|
85
|
+
...current.triggers,
|
|
86
|
+
schedule: { ...current.triggers.schedule, enabled: false },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
this.timers.delete(agent.id);
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
console.error(`[agent-executor] Unhandled error in one-shot agent "${agent.name}":`, err);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
this.timers.set(agent.id, cronTask);
|
|
97
|
+
console.log(`[agent-executor] Scheduled one-shot "${agent.name}" at ${targetTime.toISOString()}`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(`[agent-executor] Skipping one-shot "${agent.name}" — target time is in the past`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`[agent-executor] Failed to schedule "${agent.name}":`, err);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Stop an agent's cron timer. */
|
|
108
|
+
stopAgent(agentId: string): void {
|
|
109
|
+
const timer = this.timers.get(agentId);
|
|
110
|
+
if (timer) {
|
|
111
|
+
timer.stop();
|
|
112
|
+
this.timers.delete(agentId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Execute an agent: create a session, configure MCP, send the prompt, track the result. */
|
|
117
|
+
async executeAgent(
|
|
118
|
+
agentId: string,
|
|
119
|
+
input?: string,
|
|
120
|
+
opts?: ExecuteAgentOptions,
|
|
121
|
+
): Promise<SdkSessionInfo | undefined> {
|
|
122
|
+
const agent = agentStore.getAgent(agentId);
|
|
123
|
+
if (!agent) return;
|
|
124
|
+
if (!agent.enabled && !opts?.force) return;
|
|
125
|
+
|
|
126
|
+
// Overlap prevention: skip if previous execution is still running (unless forced)
|
|
127
|
+
if (!opts?.force && agent.lastSessionId && this.launcher.isAlive(agent.lastSessionId)) {
|
|
128
|
+
console.log(`[agent-executor] Skipping "${agent.name}" — previous execution still running (${agent.lastSessionId})`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const triggerType = opts?.triggerType || "manual";
|
|
133
|
+
console.log(`[agent-executor] Executing agent "${agent.name}" (${agentId}) via ${triggerType}`);
|
|
134
|
+
|
|
135
|
+
const execution: AgentExecution = {
|
|
136
|
+
sessionId: "",
|
|
137
|
+
agentId,
|
|
138
|
+
triggerType,
|
|
139
|
+
startedAt: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Resolve environment variables
|
|
144
|
+
let envVars: Record<string, string> | undefined;
|
|
145
|
+
if (agent.envSlug) {
|
|
146
|
+
const env = envManager.getEnv(agent.envSlug);
|
|
147
|
+
if (env) envVars = { ...env.variables };
|
|
148
|
+
}
|
|
149
|
+
if (agent.env) {
|
|
150
|
+
envVars = { ...envVars, ...agent.env };
|
|
151
|
+
}
|
|
152
|
+
if (opts?.additionalEnv) {
|
|
153
|
+
envVars = { ...envVars, ...opts.additionalEnv };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolve working directory
|
|
157
|
+
let cwd = agent.cwd;
|
|
158
|
+
if (cwd === "temp" || !cwd) {
|
|
159
|
+
cwd = mkdtempSync(join(tmpdir(), `companion-agent-${agent.id}-`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Launch the session via CliLauncher.
|
|
163
|
+
// Agents always run with full permissions — no interactive prompts.
|
|
164
|
+
// For Claude Code this sets --permission-mode bypassPermissions;
|
|
165
|
+
// for Codex, approvalPolicy is already hardcoded to "never".
|
|
166
|
+
if (agent.permissionMode && agent.permissionMode !== "bypassPermissions") {
|
|
167
|
+
console.warn(
|
|
168
|
+
`[agent-executor] Agent "${agent.name}" has permissionMode="${agent.permissionMode}" ` +
|
|
169
|
+
`but agent sessions always run with bypassPermissions`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const sessionInfo = this.launcher.launch({
|
|
173
|
+
model: agent.model,
|
|
174
|
+
permissionMode: "bypassPermissions",
|
|
175
|
+
cwd,
|
|
176
|
+
env: envVars,
|
|
177
|
+
allowedTools: agent.allowedTools,
|
|
178
|
+
backendType: agent.backendType,
|
|
179
|
+
codexInternetAccess: agent.backendType === "codex" ? (agent.codexInternetAccess ?? true) : undefined,
|
|
180
|
+
codexSandbox: agent.backendType === "codex"
|
|
181
|
+
? (agent.permissionMode === "bypassPermissions" ? "danger-full-access" : "workspace-write")
|
|
182
|
+
: undefined,
|
|
183
|
+
systemPrompt: agent.backendType === "codex" ? opts?.systemPrompt : undefined,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
execution.sessionId = sessionInfo.sessionId;
|
|
187
|
+
|
|
188
|
+
// Tag the session as agent-originated
|
|
189
|
+
sessionInfo.agentId = agentId;
|
|
190
|
+
sessionInfo.agentName = agent.name;
|
|
191
|
+
|
|
192
|
+
// Set the session name
|
|
193
|
+
const runLabel = `🤖 ${agent.name}`;
|
|
194
|
+
sessionNames.setName(sessionInfo.sessionId, runLabel);
|
|
195
|
+
|
|
196
|
+
// Wait for CLI to connect
|
|
197
|
+
await this.waitForCLIConnection(sessionInfo.sessionId);
|
|
198
|
+
|
|
199
|
+
// Configure MCP servers if specified
|
|
200
|
+
if (agent.mcpServers && Object.keys(agent.mcpServers).length > 0) {
|
|
201
|
+
this.wsBridge.injectMcpSetServers(sessionInfo.sessionId, agent.mcpServers);
|
|
202
|
+
// MCP servers need time to initialize before the CLI processes the prompt.
|
|
203
|
+
// The CLI handles MCP setup asynchronously; this delay ensures servers are
|
|
204
|
+
// ready. A proper health-check mechanism would be better long-term, but the
|
|
205
|
+
// CLI doesn't expose an MCP-ready signal yet.
|
|
206
|
+
const MCP_INIT_DELAY_MS = 2000;
|
|
207
|
+
await new Promise((r) => setTimeout(r, MCP_INIT_DELAY_MS));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (opts?.systemPrompt && agent.backendType === "claude") {
|
|
211
|
+
this.wsBridge.injectSystemPrompt(sessionInfo.sessionId, opts.systemPrompt);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Resolve prompt: replace {{input}} placeholder with trigger input
|
|
215
|
+
let resolvedPrompt = agent.prompt;
|
|
216
|
+
if (input !== undefined) {
|
|
217
|
+
resolvedPrompt = resolvedPrompt.replace(/\{\{input\}\}/g, input);
|
|
218
|
+
} else {
|
|
219
|
+
resolvedPrompt = resolvedPrompt.replace(/\{\{input\}\}/g, "");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Send the prompt with agent prefix for traceability
|
|
223
|
+
const fullPrompt = `[agent:${agent.id} ${agent.name}]\n\n${resolvedPrompt}`;
|
|
224
|
+
this.wsBridge.injectUserMessage(sessionInfo.sessionId, fullPrompt);
|
|
225
|
+
|
|
226
|
+
// Update agent tracking
|
|
227
|
+
agentStore.updateAgent(agentId, {
|
|
228
|
+
lastRunAt: Date.now(),
|
|
229
|
+
lastSessionId: sessionInfo.sessionId,
|
|
230
|
+
totalRuns: agent.totalRuns + 1,
|
|
231
|
+
consecutiveFailures: 0,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Execution is now "running" — completedAt/success will be set
|
|
235
|
+
// when the CLI process exits via handleSessionExited().
|
|
236
|
+
this.addExecution(agentId, execution);
|
|
237
|
+
|
|
238
|
+
return sessionInfo;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(`[agent-executor] Agent "${agent.name}" failed:`, err);
|
|
241
|
+
execution.error = err instanceof Error ? err.message : String(err);
|
|
242
|
+
execution.completedAt = Date.now();
|
|
243
|
+
this.addExecution(agentId, execution);
|
|
244
|
+
|
|
245
|
+
const failures = agent.consecutiveFailures + 1;
|
|
246
|
+
const updates: Partial<AgentConfig> = {
|
|
247
|
+
consecutiveFailures: failures,
|
|
248
|
+
lastRunAt: Date.now(),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Auto-disable after too many failures
|
|
252
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES) {
|
|
253
|
+
updates.enabled = false;
|
|
254
|
+
this.stopAgent(agentId);
|
|
255
|
+
console.warn(`[agent-executor] Agent "${agent.name}" disabled after ${failures} consecutive failures`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
agentStore.updateAgent(agentId, updates);
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Manual trigger (run now regardless of schedule, bypasses enabled check). */
|
|
264
|
+
executeAgentManually(agentId: string, input?: string): void {
|
|
265
|
+
this.executeAgent(agentId, input, { force: true, triggerType: "manual" }).catch((err) => {
|
|
266
|
+
console.error(`[agent-executor] Manual execution of agent "${agentId}" failed:`, err);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Wait for CLI to be connected (poll up to timeout). */
|
|
271
|
+
private async waitForCLIConnection(sessionId: string): Promise<void> {
|
|
272
|
+
const start = Date.now();
|
|
273
|
+
|
|
274
|
+
while (Date.now() - start < CLI_CONNECT_TIMEOUT_MS) {
|
|
275
|
+
const info = this.launcher.getSession(sessionId);
|
|
276
|
+
if (info && (info.state === "connected" || info.state === "running")) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (info?.state === "exited") {
|
|
280
|
+
throw new Error(`CLI process exited before connecting (exit code: ${info.exitCode})`);
|
|
281
|
+
}
|
|
282
|
+
await new Promise((r) => setTimeout(r, CLI_CONNECT_POLL_MS));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw new Error(`CLI process did not connect within ${CLI_CONNECT_TIMEOUT_MS / 1000}s`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Get next run time for an agent. */
|
|
289
|
+
getNextRunTime(agentId: string): Date | null {
|
|
290
|
+
const timer = this.timers.get(agentId);
|
|
291
|
+
if (!timer) return null;
|
|
292
|
+
return timer.nextRun() || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Get recent executions for an agent. */
|
|
296
|
+
getExecutions(agentId: string): AgentExecution[] {
|
|
297
|
+
return this.executions.get(agentId) || [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private addExecution(agentId: string, execution: AgentExecution): void {
|
|
301
|
+
if (!this.executions.has(agentId)) {
|
|
302
|
+
this.executions.set(agentId, []);
|
|
303
|
+
}
|
|
304
|
+
const list = this.executions.get(agentId)!;
|
|
305
|
+
list.push(execution);
|
|
306
|
+
if (list.length > AgentExecutor.MAX_EXECUTIONS_PER_AGENT) {
|
|
307
|
+
list.splice(0, list.length - AgentExecutor.MAX_EXECUTIONS_PER_AGENT);
|
|
308
|
+
}
|
|
309
|
+
// Persist to disk
|
|
310
|
+
this.executionStore.append(execution);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Query executions across all agents (for Runs view). */
|
|
314
|
+
listAllExecutions(opts?: { agentId?: string; triggerType?: string; status?: "running" | "success" | "error"; limit?: number; offset?: number }) {
|
|
315
|
+
return this.executionStore.list(opts);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Handle session exit: mark the corresponding execution as completed. */
|
|
319
|
+
handleSessionExited(sessionId: string, exitCode: number | null): void {
|
|
320
|
+
for (const [, execs] of this.executions) {
|
|
321
|
+
const exec = execs.find((e) => e.sessionId === sessionId && !e.completedAt);
|
|
322
|
+
if (exec) {
|
|
323
|
+
exec.completedAt = Date.now();
|
|
324
|
+
exec.success = exitCode === 0 || exitCode === null;
|
|
325
|
+
if (exitCode && exitCode !== 0) {
|
|
326
|
+
exec.error = exec.error || `Process exited with code ${exitCode}`;
|
|
327
|
+
}
|
|
328
|
+
this.executionStore.update(sessionId, {
|
|
329
|
+
completedAt: exec.completedAt,
|
|
330
|
+
success: exec.success,
|
|
331
|
+
error: exec.error,
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Stop all timers (for graceful shutdown). */
|
|
339
|
+
destroy(): void {
|
|
340
|
+
for (const timer of this.timers.values()) {
|
|
341
|
+
timer.stop();
|
|
342
|
+
}
|
|
343
|
+
this.timers.clear();
|
|
344
|
+
this.executions.clear();
|
|
345
|
+
}
|
|
346
|
+
}
|