@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.
Files changed (242) hide show
  1. package/bin/cli.ts +168 -0
  2. package/bin/ctl.ts +528 -0
  3. package/bin/generate-token.ts +28 -0
  4. package/dist/apple-touch-icon.png +0 -0
  5. package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
  6. package/dist/assets/CronManager-EGwLJONv.js +1 -0
  7. package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
  8. package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
  9. package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
  10. package/dist/assets/Playground-BV3k0RbV.js +109 -0
  11. package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
  12. package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
  13. package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
  14. package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
  15. package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
  16. package/dist/assets/index-BhUa1e6X.css +1 -0
  17. package/dist/assets/index-DkqeP-R9.js +134 -0
  18. package/dist/assets/sw-register-BibwRdvC.js +1 -0
  19. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  20. package/dist/favicon.svg +8 -0
  21. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  22. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  23. package/dist/icon-192.png +0 -0
  24. package/dist/icon-512.png +0 -0
  25. package/dist/index.html +20 -0
  26. package/dist/logo-codex.svg +14 -0
  27. package/dist/logo-docker.svg +4 -0
  28. package/dist/logo.svg +14 -0
  29. package/dist/manifest.json +24 -0
  30. package/dist/sw.js +2 -0
  31. package/package.json +104 -0
  32. package/server/agent-cron-migrator.test.ts +610 -0
  33. package/server/agent-cron-migrator.ts +85 -0
  34. package/server/agent-executor.test.ts +1108 -0
  35. package/server/agent-executor.ts +346 -0
  36. package/server/agent-store.test.ts +588 -0
  37. package/server/agent-store.ts +185 -0
  38. package/server/agent-types.ts +138 -0
  39. package/server/ai-validation-settings.test.ts +128 -0
  40. package/server/ai-validation-settings.ts +35 -0
  41. package/server/ai-validator.test.ts +387 -0
  42. package/server/ai-validator.ts +271 -0
  43. package/server/auth-manager.test.ts +83 -0
  44. package/server/auth-manager.ts +150 -0
  45. package/server/auto-namer.test.ts +252 -0
  46. package/server/auto-namer.ts +78 -0
  47. package/server/backend-adapter.test.ts +38 -0
  48. package/server/backend-adapter.ts +54 -0
  49. package/server/cache-headers.test.ts +98 -0
  50. package/server/cache-headers.ts +61 -0
  51. package/server/claude-adapter.test.ts +1363 -0
  52. package/server/claude-adapter.ts +889 -0
  53. package/server/claude-container-auth.test.ts +44 -0
  54. package/server/claude-container-auth.ts +30 -0
  55. package/server/claude-protocol-contract.test.ts +71 -0
  56. package/server/claude-protocol-drift.test.ts +78 -0
  57. package/server/claude-session-discovery.test.ts +132 -0
  58. package/server/claude-session-discovery.ts +157 -0
  59. package/server/claude-session-history.test.ts +158 -0
  60. package/server/claude-session-history.ts +410 -0
  61. package/server/cli-launcher.test.ts +1343 -0
  62. package/server/cli-launcher.ts +1298 -0
  63. package/server/cli.test.ts +16 -0
  64. package/server/codex-adapter.test.ts +5545 -0
  65. package/server/codex-adapter.ts +3062 -0
  66. package/server/codex-container-auth.test.ts +50 -0
  67. package/server/codex-container-auth.ts +24 -0
  68. package/server/codex-home.test.ts +61 -0
  69. package/server/codex-home.ts +26 -0
  70. package/server/codex-protocol-contract.test.ts +96 -0
  71. package/server/codex-protocol-drift.test.ts +123 -0
  72. package/server/codex-ws-proxy.cjs +226 -0
  73. package/server/commands-discovery.test.ts +179 -0
  74. package/server/commands-discovery.ts +81 -0
  75. package/server/constants.ts +7 -0
  76. package/server/container-manager.test.ts +1211 -0
  77. package/server/container-manager.ts +1053 -0
  78. package/server/cron-scheduler.test.ts +957 -0
  79. package/server/cron-scheduler.ts +243 -0
  80. package/server/cron-store.test.ts +422 -0
  81. package/server/cron-store.ts +148 -0
  82. package/server/cron-types.ts +63 -0
  83. package/server/env-manager.test.ts +268 -0
  84. package/server/env-manager.ts +161 -0
  85. package/server/event-bus-types.ts +64 -0
  86. package/server/event-bus.test.ts +244 -0
  87. package/server/event-bus.ts +124 -0
  88. package/server/execution-store.test.ts +307 -0
  89. package/server/execution-store.ts +170 -0
  90. package/server/fs-utils.ts +15 -0
  91. package/server/git-utils.test.ts +938 -0
  92. package/server/git-utils.ts +421 -0
  93. package/server/github-pr.test.ts +498 -0
  94. package/server/github-pr.ts +379 -0
  95. package/server/image-pull-manager.test.ts +303 -0
  96. package/server/image-pull-manager.ts +279 -0
  97. package/server/index.ts +396 -0
  98. package/server/linear-agent-bridge.test.ts +1157 -0
  99. package/server/linear-agent-bridge.ts +629 -0
  100. package/server/linear-agent.test.ts +473 -0
  101. package/server/linear-agent.ts +479 -0
  102. package/server/linear-cache.test.ts +136 -0
  103. package/server/linear-cache.ts +113 -0
  104. package/server/linear-connections.test.ts +350 -0
  105. package/server/linear-connections.ts +231 -0
  106. package/server/linear-credential-migration.test.ts +337 -0
  107. package/server/linear-credential-migration.ts +63 -0
  108. package/server/linear-oauth-connections-migration.test.ts +268 -0
  109. package/server/linear-oauth-connections.test.ts +365 -0
  110. package/server/linear-oauth-connections.ts +294 -0
  111. package/server/linear-project-manager.test.ts +162 -0
  112. package/server/linear-project-manager.ts +111 -0
  113. package/server/linear-prompt-builder.test.ts +74 -0
  114. package/server/linear-prompt-builder.ts +61 -0
  115. package/server/linear-staging.test.ts +276 -0
  116. package/server/linear-staging.ts +142 -0
  117. package/server/logger.test.ts +393 -0
  118. package/server/logger.ts +259 -0
  119. package/server/metrics-collector.test.ts +413 -0
  120. package/server/metrics-collector.ts +350 -0
  121. package/server/metrics-types.ts +108 -0
  122. package/server/middleware/managed-auth.test.ts +264 -0
  123. package/server/middleware/managed-auth.ts +195 -0
  124. package/server/novnc-proxy.test.ts +333 -0
  125. package/server/novnc-proxy.ts +99 -0
  126. package/server/path-resolver.test.ts +552 -0
  127. package/server/path-resolver.ts +186 -0
  128. package/server/paths.test.ts +31 -0
  129. package/server/paths.ts +11 -0
  130. package/server/pr-poller.test.ts +191 -0
  131. package/server/pr-poller.ts +162 -0
  132. package/server/prompt-manager.test.ts +211 -0
  133. package/server/prompt-manager.ts +211 -0
  134. package/server/protocol/claude-upstream/README.md +19 -0
  135. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  136. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  137. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  138. package/server/protocol/codex-upstream/README.md +18 -0
  139. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  140. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  141. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  142. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  143. package/server/protocol-monitor.ts +50 -0
  144. package/server/recorder.test.ts +454 -0
  145. package/server/recorder.ts +374 -0
  146. package/server/recording-hub/compat-validator.test.ts +150 -0
  147. package/server/recording-hub/compat-validator.ts +284 -0
  148. package/server/recording-hub/diagnostics.test.ts +140 -0
  149. package/server/recording-hub/diagnostics.ts +299 -0
  150. package/server/recording-hub/hub-config.test.ts +44 -0
  151. package/server/recording-hub/hub-config.ts +19 -0
  152. package/server/recording-hub/hub-routes.test.ts +417 -0
  153. package/server/recording-hub/hub-routes.ts +236 -0
  154. package/server/recording-hub/hub-store.test.ts +262 -0
  155. package/server/recording-hub/hub-store.ts +265 -0
  156. package/server/recording-hub/replay-adapter.test.ts +294 -0
  157. package/server/recording-hub/replay-adapter.ts +207 -0
  158. package/server/relay-client.test.ts +337 -0
  159. package/server/relay-client.ts +320 -0
  160. package/server/replay.test.ts +200 -0
  161. package/server/replay.ts +78 -0
  162. package/server/routes/agent-routes.test.ts +1400 -0
  163. package/server/routes/agent-routes.ts +409 -0
  164. package/server/routes/cron-routes.test.ts +881 -0
  165. package/server/routes/cron-routes.ts +103 -0
  166. package/server/routes/env-routes.test.ts +383 -0
  167. package/server/routes/env-routes.ts +95 -0
  168. package/server/routes/fs-routes.test.ts +1198 -0
  169. package/server/routes/fs-routes.ts +605 -0
  170. package/server/routes/git-routes.test.ts +813 -0
  171. package/server/routes/git-routes.ts +97 -0
  172. package/server/routes/linear-agent-routes.test.ts +721 -0
  173. package/server/routes/linear-agent-routes.ts +304 -0
  174. package/server/routes/linear-connection-routes.test.ts +927 -0
  175. package/server/routes/linear-connection-routes.ts +244 -0
  176. package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
  177. package/server/routes/linear-oauth-connection-routes.ts +129 -0
  178. package/server/routes/linear-routes.test.ts +1510 -0
  179. package/server/routes/linear-routes.ts +953 -0
  180. package/server/routes/metrics-routes.test.ts +103 -0
  181. package/server/routes/metrics-routes.ts +13 -0
  182. package/server/routes/prompt-routes.ts +67 -0
  183. package/server/routes/sandbox-routes.test.ts +513 -0
  184. package/server/routes/sandbox-routes.ts +127 -0
  185. package/server/routes/settings-routes.ts +270 -0
  186. package/server/routes/skills-routes.test.ts +690 -0
  187. package/server/routes/skills-routes.ts +100 -0
  188. package/server/routes/system-routes.test.ts +637 -0
  189. package/server/routes/system-routes.ts +228 -0
  190. package/server/routes/tailscale-routes.test.ts +176 -0
  191. package/server/routes/tailscale-routes.ts +22 -0
  192. package/server/routes.test.ts +4655 -0
  193. package/server/routes.ts +1277 -0
  194. package/server/sandbox-manager.test.ts +378 -0
  195. package/server/sandbox-manager.ts +168 -0
  196. package/server/service.test.ts +1419 -0
  197. package/server/service.ts +718 -0
  198. package/server/session-creation-service.test.ts +661 -0
  199. package/server/session-creation-service.ts +473 -0
  200. package/server/session-git-info.ts +104 -0
  201. package/server/session-linear-issues.test.ts +118 -0
  202. package/server/session-linear-issues.ts +88 -0
  203. package/server/session-names.test.ts +94 -0
  204. package/server/session-names.ts +67 -0
  205. package/server/session-orchestrator.test.ts +1784 -0
  206. package/server/session-orchestrator.ts +973 -0
  207. package/server/session-state-machine.test.ts +606 -0
  208. package/server/session-state-machine.ts +207 -0
  209. package/server/session-store.test.ts +290 -0
  210. package/server/session-store.ts +146 -0
  211. package/server/session-types.ts +509 -0
  212. package/server/settings-manager.test.ts +275 -0
  213. package/server/settings-manager.ts +173 -0
  214. package/server/tailscale-manager.test.ts +553 -0
  215. package/server/tailscale-manager.ts +451 -0
  216. package/server/terminal-manager.ts +240 -0
  217. package/server/update-checker.test.ts +306 -0
  218. package/server/update-checker.ts +197 -0
  219. package/server/usage-limits.test.ts +536 -0
  220. package/server/usage-limits.ts +225 -0
  221. package/server/worktree-tracker.test.ts +243 -0
  222. package/server/worktree-tracker.ts +84 -0
  223. package/server/ws-auth.test.ts +59 -0
  224. package/server/ws-auth.ts +41 -0
  225. package/server/ws-bridge-browser-ingest.test.ts +272 -0
  226. package/server/ws-bridge-browser-ingest.ts +72 -0
  227. package/server/ws-bridge-browser.ts +112 -0
  228. package/server/ws-bridge-cli-ingest.test.ts +302 -0
  229. package/server/ws-bridge-cli-ingest.ts +81 -0
  230. package/server/ws-bridge-codex.test.ts +1837 -0
  231. package/server/ws-bridge-codex.ts +266 -0
  232. package/server/ws-bridge-controls.test.ts +124 -0
  233. package/server/ws-bridge-controls.ts +20 -0
  234. package/server/ws-bridge-persist.test.ts +296 -0
  235. package/server/ws-bridge-persist.ts +66 -0
  236. package/server/ws-bridge-publish.test.ts +234 -0
  237. package/server/ws-bridge-publish.ts +79 -0
  238. package/server/ws-bridge-replay.test.ts +44 -0
  239. package/server/ws-bridge-replay.ts +61 -0
  240. package/server/ws-bridge-types.ts +106 -0
  241. package/server/ws-bridge.test.ts +4777 -0
  242. 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
+ }