bopodev-api 0.1.8 → 0.1.9

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.
@@ -1,8 +1,27 @@
1
1
  import { Router } from "express";
2
+ import { mkdir } from "node:fs/promises";
2
3
  import { z } from "zod";
3
- import { appendAuditEvent, createAgent, createApprovalRequest, deleteAgent, getApprovalRequest, listAgents, updateAgent } from "bopodev-db";
4
+ import { executeAgentRuntime } from "bopodev-agent-sdk";
5
+ import { AgentCreateRequestSchema, AgentUpdateRequestSchema } from "bopodev-contracts";
6
+ import {
7
+ appendAuditEvent,
8
+ createAgent,
9
+ createApprovalRequest,
10
+ deleteAgent,
11
+ getApprovalRequest,
12
+ listAgents,
13
+ updateAgent
14
+ } from "bopodev-db";
4
15
  import type { AppContext } from "../context";
5
16
  import { sendError, sendOk } from "../http";
17
+ import {
18
+ normalizeRuntimeConfig,
19
+ parseRuntimeConfigFromAgentRow,
20
+ requiresRuntimeCwd,
21
+ runtimeConfigToDb,
22
+ runtimeConfigToStateBlobPatch
23
+ } from "../lib/agent-config";
24
+ import { hasText, resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
6
25
  import { requireCompanyScope } from "../middleware/company-scope";
7
26
  import { requireBoardRole, requirePermission } from "../middleware/request-actor";
8
27
  import { createGovernanceRealtimeEvent, serializeStoredApproval } from "../realtime/governance";
@@ -12,37 +31,74 @@ import {
12
31
  } from "../realtime/office-space";
13
32
  import { isApprovalRequired } from "../services/governance-service";
14
33
 
15
- const createAgentSchema = z.object({
16
- managerAgentId: z.string().optional(),
17
- role: z.string().min(1),
18
- name: z.string().min(1),
19
- providerType: z.enum(["claude_code", "codex", "http", "shell"]),
20
- heartbeatCron: z.string().default("*/5 * * * *"),
21
- monthlyBudgetUsd: z.number().nonnegative().default(30),
22
- canHireAgents: z.boolean().default(false),
23
- requestApproval: z.boolean().default(true),
34
+ const legacyRuntimeConfigSchema = z.object({
24
35
  runtimeCommand: z.string().optional(),
25
36
  runtimeArgs: z.array(z.string()).optional(),
26
37
  runtimeCwd: z.string().optional(),
27
- runtimeTimeoutMs: z.number().int().positive().max(600000).optional()
38
+ runtimeTimeoutMs: z.number().int().positive().max(600000).optional(),
39
+ runtimeModel: z.string().optional(),
40
+ runtimeThinkingEffort: z.enum(["auto", "low", "medium", "high"]).optional(),
41
+ bootstrapPrompt: z.string().optional(),
42
+ runtimeTimeoutSec: z.number().int().nonnegative().optional(),
43
+ interruptGraceSec: z.number().int().nonnegative().optional(),
44
+ runtimeEnv: z.record(z.string(), z.string()).optional(),
45
+ runPolicy: z
46
+ .object({
47
+ sandboxMode: z.enum(["workspace_write", "full_access"]).optional(),
48
+ allowWebSearch: z.boolean().optional()
49
+ })
50
+ .optional()
51
+ });
52
+
53
+ const createAgentSchema = AgentCreateRequestSchema.extend({
54
+ ...legacyRuntimeConfigSchema.shape
55
+ });
56
+
57
+ const updateAgentSchema = AgentUpdateRequestSchema.extend({
58
+ ...legacyRuntimeConfigSchema.shape
28
59
  });
29
60
 
30
- const updateAgentSchema = z
31
- .object({
32
- managerAgentId: z.string().nullable().optional(),
33
- role: z.string().min(1).optional(),
34
- name: z.string().min(1).optional(),
35
- providerType: z.enum(["claude_code", "codex", "http", "shell"]).optional(),
36
- status: z.enum(["idle", "running", "paused", "terminated"]).optional(),
37
- heartbeatCron: z.string().min(1).optional(),
38
- monthlyBudgetUsd: z.number().nonnegative().optional(),
39
- canHireAgents: z.boolean().optional(),
40
- runtimeCommand: z.string().optional(),
41
- runtimeArgs: z.array(z.string()).optional(),
42
- runtimeCwd: z.string().optional(),
43
- runtimeTimeoutMs: z.number().int().positive().max(600000).optional()
44
- })
45
- .refine((payload) => Object.keys(payload).length > 0, "At least one field must be provided.");
61
+ const runtimePreflightSchema = z.object({
62
+ providerType: z.enum(["claude_code", "codex", "http", "shell"]),
63
+ runtimeConfig: z.record(z.string(), z.unknown()).optional(),
64
+ ...legacyRuntimeConfigSchema.shape
65
+ });
66
+ const CODEX_AUTH_REQUIRED_RE =
67
+ /(not\s+logged\s+in|login\s+required|authentication\s+required|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|api[_\s-]?key.*required|missing bearer|missing bearer or basic authentication)/i;
68
+ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
69
+ "managerAgentId",
70
+ "role",
71
+ "name",
72
+ "providerType",
73
+ "status",
74
+ "heartbeatCron",
75
+ "monthlyBudgetUsd",
76
+ "canHireAgents",
77
+ "runtimeConfig",
78
+ "runtimeCommand",
79
+ "runtimeArgs",
80
+ "runtimeCwd",
81
+ "runtimeTimeoutMs",
82
+ "runtimeModel",
83
+ "runtimeThinkingEffort",
84
+ "bootstrapPrompt",
85
+ "runtimeTimeoutSec",
86
+ "interruptGraceSec",
87
+ "runtimeEnv",
88
+ "runPolicy"
89
+ ]);
90
+ const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
91
+ "runtimeCommand",
92
+ "runtimeArgs",
93
+ "runtimeCwd",
94
+ "runtimeEnv",
95
+ "runtimeModel",
96
+ "runtimeThinkingEffort",
97
+ "bootstrapPrompt",
98
+ "runtimeTimeoutSec",
99
+ "interruptGraceSec",
100
+ "runPolicy"
101
+ ]);
46
102
 
47
103
  function toAgentResponse(agent: Record<string, unknown>) {
48
104
  return {
@@ -65,22 +121,195 @@ export function createAgentsRouter(ctx: AppContext) {
65
121
  );
66
122
  });
67
123
 
124
+ router.get("/runtime-default-cwd", async (req, res) => {
125
+ const runtimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
126
+ await mkdir(runtimeCwd, { recursive: true });
127
+ return sendOk(res, { runtimeCwd });
128
+ });
129
+
130
+ router.post("/runtime-preflight", async (req, res) => {
131
+ const parsed = runtimePreflightSchema.safeParse(req.body);
132
+ if (!parsed.success) {
133
+ return sendError(res, parsed.error.message, 422);
134
+ }
135
+ const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
136
+ const runtimeConfig = normalizeRuntimeConfig({
137
+ runtimeConfig: parsed.data.runtimeConfig,
138
+ legacy: {
139
+ runtimeCommand: parsed.data.runtimeCommand,
140
+ runtimeArgs: parsed.data.runtimeArgs,
141
+ runtimeCwd: parsed.data.runtimeCwd,
142
+ runtimeTimeoutMs: parsed.data.runtimeTimeoutMs,
143
+ runtimeModel: parsed.data.runtimeModel,
144
+ runtimeThinkingEffort: parsed.data.runtimeThinkingEffort,
145
+ bootstrapPrompt: parsed.data.bootstrapPrompt,
146
+ runtimeTimeoutSec: parsed.data.runtimeTimeoutSec,
147
+ interruptGraceSec: parsed.data.interruptGraceSec,
148
+ runtimeEnv: parsed.data.runtimeEnv,
149
+ runPolicy: parsed.data.runPolicy
150
+ },
151
+ defaultRuntimeCwd
152
+ });
153
+
154
+ const checks: Array<{
155
+ code: string;
156
+ level: "info" | "warn" | "error";
157
+ message: string;
158
+ detail?: string;
159
+ hint?: string;
160
+ }> = [];
161
+
162
+ if (parsed.data.providerType !== "codex") {
163
+ return sendOk(res, {
164
+ status: "pass",
165
+ testedAt: new Date().toISOString(),
166
+ checks: [
167
+ {
168
+ code: "preflight_not_required",
169
+ level: "info",
170
+ message: "Preflight probe is currently required only for Codex runtime."
171
+ }
172
+ ]
173
+ });
174
+ }
175
+
176
+ if (!runtimeConfig.runtimeCwd) {
177
+ checks.push({
178
+ code: "codex_cwd_missing",
179
+ level: "error",
180
+ message: "Runtime working directory is required for Codex preflight."
181
+ });
182
+ return sendOk(res, { status: "fail", testedAt: new Date().toISOString(), checks });
183
+ }
184
+
185
+ await mkdir(runtimeConfig.runtimeCwd, { recursive: true });
186
+
187
+ const timeoutMs =
188
+ runtimeConfig.runtimeTimeoutSec > 0 ? Math.min(runtimeConfig.runtimeTimeoutSec * 1000, 45_000) : 45_000;
189
+ const probe = await executeAgentRuntime("codex", "Respond with hello.", {
190
+ command: runtimeConfig.runtimeCommand,
191
+ args: runtimeConfig.runtimeArgs,
192
+ cwd: runtimeConfig.runtimeCwd,
193
+ env: runtimeConfig.runtimeEnv,
194
+ model: runtimeConfig.runtimeModel,
195
+ thinkingEffort: runtimeConfig.runtimeThinkingEffort,
196
+ runPolicy: runtimeConfig.runPolicy,
197
+ timeoutMs,
198
+ retryCount: 0
199
+ });
200
+
201
+ if (probe.ok) {
202
+ const summary = (probe.parsedUsage?.summary ?? "").trim();
203
+ const hasHello = /\bhello\b/i.test(summary || probe.stdout);
204
+ checks.push({
205
+ code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
206
+ level: hasHello ? "info" : "warn",
207
+ message: hasHello ? "Codex preflight probe succeeded." : "Codex probe succeeded but response was unexpected.",
208
+ ...(summary ? { detail: summary.slice(0, 240) } : {})
209
+ });
210
+ } else {
211
+ const detail = `${probe.stderr || ""}\n${probe.stdout || ""}`.trim().slice(0, 500);
212
+ if (probe.failureType === "spawn_error") {
213
+ checks.push({
214
+ code: "codex_command_unresolvable",
215
+ level: "error",
216
+ message: "Codex command is not executable from this runtime configuration.",
217
+ detail,
218
+ hint: "Install Codex CLI or set runtime command to an executable path."
219
+ });
220
+ } else if (probe.timedOut) {
221
+ checks.push({
222
+ code: "codex_hello_probe_timed_out",
223
+ level: "warn",
224
+ message: "Codex preflight timed out.",
225
+ detail,
226
+ hint: "Retry preflight. If this repeats, check runtime command/cwd and local Codex health."
227
+ });
228
+ } else if (CODEX_AUTH_REQUIRED_RE.test(detail)) {
229
+ checks.push({
230
+ code: "codex_auth_required",
231
+ level: "warn",
232
+ message: "Codex authentication is not ready for this runtime.",
233
+ detail,
234
+ hint: "Run `codex login` locally or set a global `BOPO_OPENAI_API_KEY`/`OPENAI_API_KEY`."
235
+ });
236
+ } else {
237
+ checks.push({
238
+ code: "codex_hello_probe_failed",
239
+ level: "error",
240
+ message: "Codex preflight failed.",
241
+ detail,
242
+ hint: "Run `codex exec --json -` manually in the runtime directory with prompt `Respond with hello.`."
243
+ });
244
+ }
245
+ }
246
+
247
+ const status =
248
+ checks.some((check) => check.level === "error")
249
+ ? "fail"
250
+ : checks.some((check) => check.level === "warn")
251
+ ? "warn"
252
+ : "pass";
253
+ return sendOk(res, {
254
+ status,
255
+ testedAt: new Date().toISOString(),
256
+ checks
257
+ });
258
+ });
259
+
68
260
  router.post("/", async (req, res) => {
69
261
  const requireCreate = requirePermission("agents:write");
70
262
  requireCreate(req, res, () => {});
71
263
  if (res.headersSent) {
72
264
  return;
73
265
  }
266
+ if (req.actor?.type === "agent") {
267
+ const companyAgents = await listAgents(ctx.db, req.companyId!);
268
+ const requestingAgent = companyAgents.find((row) => row.id === req.actor?.id);
269
+ if (!requestingAgent) {
270
+ return sendError(res, "Requesting agent not found.", 403);
271
+ }
272
+ if (!requestingAgent.canHireAgents) {
273
+ return sendError(res, "This agent is not allowed to create new agents.", 403);
274
+ }
275
+ }
74
276
  const parsed = createAgentSchema.safeParse(req.body);
75
277
  if (!parsed.success) {
76
278
  return sendError(res, parsed.error.message, 422);
77
279
  }
280
+ const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
281
+ const runtimeConfig = normalizeRuntimeConfig({
282
+ runtimeConfig: parsed.data.runtimeConfig,
283
+ legacy: {
284
+ runtimeCommand: parsed.data.runtimeCommand,
285
+ runtimeArgs: parsed.data.runtimeArgs,
286
+ runtimeCwd: parsed.data.runtimeCwd,
287
+ runtimeTimeoutMs: parsed.data.runtimeTimeoutMs,
288
+ runtimeModel: parsed.data.runtimeModel,
289
+ runtimeThinkingEffort: parsed.data.runtimeThinkingEffort,
290
+ bootstrapPrompt: parsed.data.bootstrapPrompt,
291
+ runtimeTimeoutSec: parsed.data.runtimeTimeoutSec,
292
+ interruptGraceSec: parsed.data.interruptGraceSec,
293
+ runtimeEnv: parsed.data.runtimeEnv,
294
+ runPolicy: parsed.data.runPolicy
295
+ },
296
+ defaultRuntimeCwd
297
+ });
298
+ if (requiresRuntimeCwd(parsed.data.providerType) && !hasText(runtimeConfig.runtimeCwd)) {
299
+ return sendError(res, "Runtime working directory is required for this runtime provider.", 422);
300
+ }
301
+ if (requiresRuntimeCwd(parsed.data.providerType) && hasText(runtimeConfig.runtimeCwd)) {
302
+ await mkdir(runtimeConfig.runtimeCwd!, { recursive: true });
303
+ }
78
304
 
79
305
  if (parsed.data.requestApproval && isApprovalRequired("hire_agent")) {
80
306
  const approvalId = await createApprovalRequest(ctx.db, {
81
307
  companyId: req.companyId!,
82
308
  action: "hire_agent",
83
- payload: parsed.data
309
+ payload: {
310
+ ...parsed.data,
311
+ runtimeConfig
312
+ }
84
313
  });
85
314
  const approval = await getApprovalRequest(ctx.db, req.companyId!, approvalId);
86
315
  if (approval) {
@@ -104,14 +333,8 @@ export function createAgentsRouter(ctx: AppContext) {
104
333
  heartbeatCron: parsed.data.heartbeatCron,
105
334
  monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
106
335
  canHireAgents: parsed.data.canHireAgents,
107
- initialState: {
108
- runtime: {
109
- command: parsed.data.runtimeCommand,
110
- args: parsed.data.runtimeArgs,
111
- cwd: parsed.data.runtimeCwd,
112
- timeoutMs: parsed.data.runtimeTimeoutMs
113
- }
114
- }
336
+ ...runtimeConfigToDb(runtimeConfig),
337
+ initialState: runtimeConfigToStateBlobPatch(runtimeConfig)
115
338
  });
116
339
 
117
340
  await appendAuditEvent(ctx.db, {
@@ -132,6 +355,15 @@ export function createAgentsRouter(ctx: AppContext) {
132
355
  if (res.headersSent) {
133
356
  return;
134
357
  }
358
+ const unsupportedKeys = listUnsupportedAgentUpdateKeys(req.body);
359
+ if (unsupportedKeys.length > 0) {
360
+ return sendError(
361
+ res,
362
+ `Unsupported agent update fields: ${unsupportedKeys.join(", ")}. Supported fields: ${Array.from(UPDATE_AGENT_ALLOWED_KEYS).join(", ")}.`,
363
+ 422
364
+ );
365
+ }
366
+
135
367
  const parsed = updateAgentSchema.safeParse(req.body);
136
368
  if (!parsed.success) {
137
369
  return sendError(res, parsed.error.message, 422);
@@ -141,39 +373,56 @@ export function createAgentsRouter(ctx: AppContext) {
141
373
  if (!existingAgent) {
142
374
  return sendError(res, "Agent not found.", 404);
143
375
  }
144
-
145
- const hasRuntimePatch =
376
+ const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
377
+ const existingRuntime = parseRuntimeConfigFromAgentRow(existingAgent as unknown as Record<string, unknown>);
378
+ const effectiveProviderType = parsed.data.providerType ?? existingAgent.providerType;
379
+ const hasRuntimeInput =
380
+ parsed.data.runtimeConfig !== undefined ||
146
381
  parsed.data.runtimeCommand !== undefined ||
147
382
  parsed.data.runtimeArgs !== undefined ||
148
383
  parsed.data.runtimeCwd !== undefined ||
149
- parsed.data.runtimeTimeoutMs !== undefined;
150
-
151
- let stateBlobPatch: Record<string, unknown> | undefined;
152
- if (hasRuntimePatch) {
153
- const existingState = (() => {
154
- try {
155
- const parsedState = JSON.parse(existingAgent.stateBlob ?? "{}") as Record<string, unknown>;
156
- return typeof parsedState === "object" && parsedState !== null ? parsedState : {};
157
- } catch {
158
- return {};
159
- }
160
- })();
161
- const existingRuntime =
162
- typeof existingState.runtime === "object" && existingState.runtime !== null
163
- ? (existingState.runtime as Record<string, unknown>)
164
- : {};
165
- stateBlobPatch = {
166
- ...existingState,
167
- runtime: {
168
- ...existingRuntime,
169
- ...(parsed.data.runtimeCommand === undefined ? {} : { command: parsed.data.runtimeCommand }),
170
- ...(parsed.data.runtimeArgs === undefined ? {} : { args: parsed.data.runtimeArgs }),
171
- ...(parsed.data.runtimeCwd === undefined ? {} : { cwd: parsed.data.runtimeCwd }),
172
- ...(parsed.data.runtimeTimeoutMs === undefined ? {} : { timeoutMs: parsed.data.runtimeTimeoutMs })
173
- }
174
- };
384
+ parsed.data.runtimeTimeoutMs !== undefined ||
385
+ parsed.data.runtimeModel !== undefined ||
386
+ parsed.data.runtimeThinkingEffort !== undefined ||
387
+ parsed.data.bootstrapPrompt !== undefined ||
388
+ parsed.data.runtimeTimeoutSec !== undefined ||
389
+ parsed.data.interruptGraceSec !== undefined ||
390
+ parsed.data.runtimeEnv !== undefined ||
391
+ parsed.data.runPolicy !== undefined;
392
+ const nextRuntime = {
393
+ ...existingRuntime,
394
+ ...(hasRuntimeInput
395
+ ? normalizeRuntimeConfig({
396
+ runtimeConfig: {
397
+ ...existingRuntime,
398
+ ...(parsed.data.runtimeConfig ?? {})
399
+ },
400
+ legacy: {
401
+ runtimeCommand: parsed.data.runtimeCommand,
402
+ runtimeArgs: parsed.data.runtimeArgs,
403
+ runtimeCwd: parsed.data.runtimeCwd,
404
+ runtimeTimeoutMs: parsed.data.runtimeTimeoutMs,
405
+ runtimeModel: parsed.data.runtimeModel,
406
+ runtimeThinkingEffort: parsed.data.runtimeThinkingEffort,
407
+ bootstrapPrompt: parsed.data.bootstrapPrompt,
408
+ runtimeTimeoutSec: parsed.data.runtimeTimeoutSec ?? existingRuntime.runtimeTimeoutSec,
409
+ interruptGraceSec: parsed.data.interruptGraceSec,
410
+ runtimeEnv: parsed.data.runtimeEnv,
411
+ runPolicy: parsed.data.runPolicy
412
+ }
413
+ })
414
+ : {})
415
+ };
416
+ if (!nextRuntime.runtimeCwd && defaultRuntimeCwd) {
417
+ nextRuntime.runtimeCwd = defaultRuntimeCwd;
418
+ }
419
+ const effectiveRuntimeCwd = nextRuntime.runtimeCwd ?? "";
420
+ if (requiresRuntimeCwd(effectiveProviderType) && !hasText(effectiveRuntimeCwd)) {
421
+ return sendError(res, "Runtime working directory is required for this runtime provider.", 422);
422
+ }
423
+ if (requiresRuntimeCwd(effectiveProviderType) && hasText(effectiveRuntimeCwd)) {
424
+ await mkdir(effectiveRuntimeCwd, { recursive: true });
175
425
  }
176
-
177
426
  const agent = await updateAgent(ctx.db, {
178
427
  companyId: req.companyId!,
179
428
  id: req.params.agentId,
@@ -186,7 +435,8 @@ export function createAgentsRouter(ctx: AppContext) {
186
435
  monthlyBudgetUsd:
187
436
  typeof parsed.data.monthlyBudgetUsd === "number" ? parsed.data.monthlyBudgetUsd.toFixed(4) : undefined,
188
437
  canHireAgents: parsed.data.canHireAgents,
189
- stateBlob: stateBlobPatch
438
+ ...runtimeConfigToDb(nextRuntime),
439
+ stateBlob: runtimeConfigToStateBlobPatch(nextRuntime)
190
440
  });
191
441
  if (!agent) {
192
442
  return sendError(res, "Agent not found.", 404);
@@ -303,3 +553,22 @@ export function createAgentsRouter(ctx: AppContext) {
303
553
 
304
554
  return router;
305
555
  }
556
+
557
+ function listUnsupportedAgentUpdateKeys(payload: unknown) {
558
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
559
+ return [] as string[];
560
+ }
561
+ const body = payload as Record<string, unknown>;
562
+ const unsupported = Object.keys(body)
563
+ .filter((key) => !UPDATE_AGENT_ALLOWED_KEYS.has(key))
564
+ .sort();
565
+ const runtimeConfig = body.runtimeConfig;
566
+ if (runtimeConfig && typeof runtimeConfig === "object" && !Array.isArray(runtimeConfig)) {
567
+ for (const key of Object.keys(runtimeConfig as Record<string, unknown>).sort()) {
568
+ if (!UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS.has(key)) {
569
+ unsupported.push(`runtimeConfig.${key}`);
570
+ }
571
+ }
572
+ }
573
+ return unsupported;
574
+ }
@@ -1,7 +1,7 @@
1
1
  import { Router } from "express";
2
2
  import { z } from "zod";
3
3
  import { and, eq } from "drizzle-orm";
4
- import { agents } from "bopodev-db";
4
+ import { agents, heartbeatRuns } from "bopodev-db";
5
5
  import type { AppContext } from "../context";
6
6
  import { sendError, sendOk } from "../http";
7
7
  import { requireCompanyScope } from "../middleware/company-scope";
@@ -42,7 +42,26 @@ export function createHeartbeatRouter(ctx: AppContext) {
42
42
  trigger: "manual",
43
43
  realtimeHub: ctx.realtimeHub
44
44
  });
45
- return sendOk(res, { runId, requestId: req.requestId });
45
+ if (!runId) {
46
+ return sendError(res, "Heartbeat could not be started for this agent.", 409);
47
+ }
48
+ const [runRow] = await ctx.db
49
+ .select({ id: heartbeatRuns.id, status: heartbeatRuns.status, message: heartbeatRuns.message })
50
+ .from(heartbeatRuns)
51
+ .where(and(eq(heartbeatRuns.companyId, req.companyId!), eq(heartbeatRuns.id, runId)))
52
+ .limit(1);
53
+ const invokeStatus =
54
+ runRow?.status === "skipped" && String(runRow.message ?? "").includes("already in progress")
55
+ ? "skipped_overlap"
56
+ : runRow?.status === "skipped"
57
+ ? "skipped"
58
+ : "started";
59
+ return sendOk(res, {
60
+ runId,
61
+ requestId: req.requestId,
62
+ status: invokeStatus,
63
+ message: runRow?.message ?? null
64
+ });
46
65
  });
47
66
 
48
67
  router.post("/sweep", async (req, res) => {