bopodev-api 0.1.12 → 0.1.13
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/package.json +6 -4
- package/src/app.ts +2 -0
- package/src/lib/instance-paths.ts +12 -0
- package/src/lib/opencode-model.ts +35 -0
- package/src/lib/workspace-policy.ts +5 -0
- package/src/realtime/heartbeat-runs.ts +78 -0
- package/src/realtime/hub.ts +37 -1
- package/src/realtime/office-space.ts +10 -1
- package/src/routes/agents.ts +89 -2
- package/src/routes/companies.ts +2 -0
- package/src/routes/governance.ts +9 -2
- package/src/routes/heartbeats.ts +2 -1
- package/src/routes/issues.ts +321 -0
- package/src/routes/observability.ts +546 -18
- package/src/routes/plugins.ts +257 -0
- package/src/scripts/onboard-seed.ts +57 -12
- package/src/server.ts +62 -3
- package/src/services/governance-service.ts +97 -23
- package/src/services/heartbeat-service.ts +633 -31
- package/src/services/memory-file-service.ts +249 -0
- package/src/services/plugin-manifest-loader.ts +65 -0
- package/src/services/plugin-runtime.ts +580 -0
- package/src/services/plugin-webhook-executor.ts +94 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
2
3
|
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
3
4
|
import { nanoid } from "nanoid";
|
|
4
5
|
import { resolveAdapter } from "bopodev-agent-sdk";
|
|
@@ -11,16 +12,39 @@ import {
|
|
|
11
12
|
type ExecutionOutcome
|
|
12
13
|
} from "bopodev-contracts";
|
|
13
14
|
import type { BopoDb } from "bopodev-db";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
agents,
|
|
17
|
+
appendActivity,
|
|
18
|
+
appendHeartbeatRunMessages,
|
|
19
|
+
companies,
|
|
20
|
+
goals,
|
|
21
|
+
heartbeatRuns,
|
|
22
|
+
issueAttachments,
|
|
23
|
+
issues,
|
|
24
|
+
projects
|
|
25
|
+
} from "bopodev-db";
|
|
15
26
|
import { appendAuditEvent, appendCost } from "bopodev-db";
|
|
16
27
|
import { parseRuntimeConfigFromAgentRow } from "../lib/agent-config";
|
|
28
|
+
import { resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
17
29
|
import { getProjectWorkspaceMap, hasText, resolveAgentFallbackWorkspace } from "../lib/workspace-policy";
|
|
18
30
|
import type { RealtimeHub } from "../realtime/hub";
|
|
31
|
+
import { createHeartbeatRunsRealtimeEvent } from "../realtime/heartbeat-runs";
|
|
19
32
|
import { publishOfficeOccupantForAgent } from "../realtime/office-space";
|
|
20
33
|
import { checkAgentBudget } from "./budget-service";
|
|
34
|
+
import { appendDurableFact, loadAgentMemoryContext, persistHeartbeatMemory } from "./memory-file-service";
|
|
35
|
+
import { runPluginHook } from "./plugin-runtime";
|
|
21
36
|
|
|
22
37
|
type HeartbeatRunTrigger = "manual" | "scheduler";
|
|
23
38
|
type HeartbeatRunMode = "default" | "resume" | "redo";
|
|
39
|
+
type HeartbeatProviderType =
|
|
40
|
+
| "claude_code"
|
|
41
|
+
| "codex"
|
|
42
|
+
| "cursor"
|
|
43
|
+
| "opencode"
|
|
44
|
+
| "openai_api"
|
|
45
|
+
| "anthropic_api"
|
|
46
|
+
| "http"
|
|
47
|
+
| "shell";
|
|
24
48
|
|
|
25
49
|
type ActiveHeartbeatRun = {
|
|
26
50
|
companyId: string;
|
|
@@ -87,7 +111,7 @@ export async function stopHeartbeatRun(
|
|
|
87
111
|
db: BopoDb,
|
|
88
112
|
companyId: string,
|
|
89
113
|
runId: string,
|
|
90
|
-
options?: { requestId?: string; actorId?: string; trigger?: HeartbeatRunTrigger }
|
|
114
|
+
options?: { requestId?: string; actorId?: string; trigger?: HeartbeatRunTrigger; realtimeHub?: RealtimeHub }
|
|
91
115
|
) {
|
|
92
116
|
const runTrigger = options?.trigger ?? "manual";
|
|
93
117
|
const [run] = await db
|
|
@@ -114,14 +138,22 @@ export async function stopHeartbeatRun(
|
|
|
114
138
|
active.cancelRequestedBy = options?.actorId ?? null;
|
|
115
139
|
active.abortController.abort(cancelReason);
|
|
116
140
|
} else {
|
|
141
|
+
const finishedAt = new Date();
|
|
117
142
|
await db
|
|
118
143
|
.update(heartbeatRuns)
|
|
119
144
|
.set({
|
|
120
145
|
status: "failed",
|
|
121
|
-
finishedAt
|
|
146
|
+
finishedAt,
|
|
122
147
|
message: "Heartbeat cancelled by stop request."
|
|
123
148
|
})
|
|
124
149
|
.where(and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.id, runId)));
|
|
150
|
+
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
151
|
+
companyId,
|
|
152
|
+
runId,
|
|
153
|
+
status: "failed",
|
|
154
|
+
message: "Heartbeat cancelled by stop request.",
|
|
155
|
+
finishedAt
|
|
156
|
+
});
|
|
125
157
|
}
|
|
126
158
|
await appendAuditEvent(db, {
|
|
127
159
|
companyId,
|
|
@@ -224,14 +256,22 @@ export async function runHeartbeatForAgent(
|
|
|
224
256
|
});
|
|
225
257
|
if (!claimed) {
|
|
226
258
|
const skippedRunId = nanoid(14);
|
|
259
|
+
const skippedAt = new Date();
|
|
227
260
|
await db.insert(heartbeatRuns).values({
|
|
228
261
|
id: skippedRunId,
|
|
229
262
|
companyId,
|
|
230
263
|
agentId,
|
|
231
264
|
status: "skipped",
|
|
232
|
-
finishedAt:
|
|
265
|
+
finishedAt: skippedAt,
|
|
233
266
|
message: "Heartbeat skipped: another run is already in progress for this agent."
|
|
234
267
|
});
|
|
268
|
+
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
269
|
+
companyId,
|
|
270
|
+
runId: skippedRunId,
|
|
271
|
+
status: "skipped",
|
|
272
|
+
message: "Heartbeat skipped: another run is already in progress for this agent.",
|
|
273
|
+
finishedAt: skippedAt
|
|
274
|
+
});
|
|
235
275
|
await appendAuditEvent(db, {
|
|
236
276
|
companyId,
|
|
237
277
|
actorType: "system",
|
|
@@ -251,6 +291,12 @@ export async function runHeartbeatForAgent(
|
|
|
251
291
|
status: "skipped",
|
|
252
292
|
message: "Heartbeat skipped due to budget hard-stop."
|
|
253
293
|
});
|
|
294
|
+
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
295
|
+
companyId,
|
|
296
|
+
runId,
|
|
297
|
+
status: "skipped",
|
|
298
|
+
message: "Heartbeat skipped due to budget hard-stop."
|
|
299
|
+
});
|
|
254
300
|
}
|
|
255
301
|
|
|
256
302
|
if (budgetCheck.allowed) {
|
|
@@ -269,6 +315,12 @@ export async function runHeartbeatForAgent(
|
|
|
269
315
|
sourceRunId: options?.sourceRunId ?? null
|
|
270
316
|
}
|
|
271
317
|
});
|
|
318
|
+
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
319
|
+
companyId,
|
|
320
|
+
runId,
|
|
321
|
+
status: "started",
|
|
322
|
+
message: "Heartbeat started."
|
|
323
|
+
});
|
|
272
324
|
}
|
|
273
325
|
|
|
274
326
|
if (!budgetCheck.allowed) {
|
|
@@ -318,14 +370,149 @@ export async function runHeartbeatForAgent(
|
|
|
318
370
|
let executionSummary = "";
|
|
319
371
|
let executionTrace: unknown = null;
|
|
320
372
|
let executionOutcome: ExecutionOutcome | null = null;
|
|
373
|
+
let memoryContext: HeartbeatContext["memoryContext"] | undefined;
|
|
321
374
|
let stateParseError: string | null = null;
|
|
322
375
|
let runtimeLaunchSummary: ReturnType<typeof summarizeRuntimeLaunch> | null = null;
|
|
376
|
+
let transcriptSequence = 0;
|
|
377
|
+
let transcriptWriteQueue = Promise.resolve();
|
|
378
|
+
let transcriptLiveCount = 0;
|
|
379
|
+
let transcriptLiveUsefulCount = 0;
|
|
380
|
+
let transcriptLiveHighSignalCount = 0;
|
|
381
|
+
let transcriptPersistFailureReported = false;
|
|
382
|
+
let pluginFailureSummary: string[] = [];
|
|
383
|
+
|
|
384
|
+
const enqueueTranscriptEvent = (event: {
|
|
385
|
+
kind: string;
|
|
386
|
+
label?: string;
|
|
387
|
+
text?: string;
|
|
388
|
+
payload?: string;
|
|
389
|
+
signalLevel?: "high" | "medium" | "low" | "noise";
|
|
390
|
+
groupKey?: string;
|
|
391
|
+
source?: "stdout" | "stderr" | "trace_fallback";
|
|
392
|
+
}) => {
|
|
393
|
+
const sequence = transcriptSequence++;
|
|
394
|
+
const createdAt = new Date();
|
|
395
|
+
const messageId = nanoid(14);
|
|
396
|
+
const signalLevel = normalizeTranscriptSignalLevel(event.signalLevel, event.kind);
|
|
397
|
+
const groupKey = event.groupKey ?? defaultTranscriptGroupKey(event.kind, event.label);
|
|
398
|
+
const source = event.source ?? "stdout";
|
|
399
|
+
transcriptLiveCount += 1;
|
|
400
|
+
if (isUsefulTranscriptSignal(signalLevel)) {
|
|
401
|
+
transcriptLiveUsefulCount += 1;
|
|
402
|
+
}
|
|
403
|
+
if (signalLevel === "high") {
|
|
404
|
+
transcriptLiveHighSignalCount += 1;
|
|
405
|
+
}
|
|
406
|
+
transcriptWriteQueue = transcriptWriteQueue
|
|
407
|
+
.then(async () => {
|
|
408
|
+
await appendHeartbeatRunMessages(db, {
|
|
409
|
+
companyId,
|
|
410
|
+
runId,
|
|
411
|
+
messages: [
|
|
412
|
+
{
|
|
413
|
+
id: messageId,
|
|
414
|
+
sequence,
|
|
415
|
+
kind: event.kind,
|
|
416
|
+
label: event.label ?? null,
|
|
417
|
+
text: event.text ?? null,
|
|
418
|
+
payloadJson: event.payload ?? null,
|
|
419
|
+
signalLevel,
|
|
420
|
+
groupKey,
|
|
421
|
+
source,
|
|
422
|
+
createdAt
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
});
|
|
426
|
+
options?.realtimeHub?.publish(
|
|
427
|
+
createHeartbeatRunsRealtimeEvent(companyId, {
|
|
428
|
+
type: "run.transcript.append",
|
|
429
|
+
runId,
|
|
430
|
+
messages: [
|
|
431
|
+
{
|
|
432
|
+
id: messageId,
|
|
433
|
+
runId,
|
|
434
|
+
sequence,
|
|
435
|
+
kind: normalizeTranscriptKind(event.kind),
|
|
436
|
+
label: event.label ?? null,
|
|
437
|
+
text: event.text ?? null,
|
|
438
|
+
payload: event.payload ?? null,
|
|
439
|
+
signalLevel,
|
|
440
|
+
groupKey,
|
|
441
|
+
source,
|
|
442
|
+
createdAt: createdAt.toISOString()
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
})
|
|
448
|
+
.catch(async (error) => {
|
|
449
|
+
if (transcriptPersistFailureReported) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
transcriptPersistFailureReported = true;
|
|
453
|
+
try {
|
|
454
|
+
await appendAuditEvent(db, {
|
|
455
|
+
companyId,
|
|
456
|
+
actorType: "system",
|
|
457
|
+
eventType: "heartbeat.transcript_persist_failed",
|
|
458
|
+
entityType: "heartbeat_run",
|
|
459
|
+
entityId: runId,
|
|
460
|
+
correlationId: options?.requestId ?? runId,
|
|
461
|
+
payload: {
|
|
462
|
+
agentId,
|
|
463
|
+
sequence,
|
|
464
|
+
messageId,
|
|
465
|
+
error: String(error)
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
} catch {
|
|
469
|
+
// Best effort: keep run execution resilient even when observability insert fails.
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
const emitCanonicalResultEvent = (text: string, label: "completed" | "failed") => {
|
|
474
|
+
const trimmed = text.trim();
|
|
475
|
+
if (!trimmed) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
enqueueTranscriptEvent({
|
|
479
|
+
kind: "result",
|
|
480
|
+
label,
|
|
481
|
+
text: trimmed,
|
|
482
|
+
signalLevel: "high",
|
|
483
|
+
groupKey: "result",
|
|
484
|
+
source: "trace_fallback"
|
|
485
|
+
});
|
|
486
|
+
};
|
|
323
487
|
|
|
324
488
|
try {
|
|
489
|
+
await runPluginHook(db, {
|
|
490
|
+
hook: "beforeClaim",
|
|
491
|
+
context: {
|
|
492
|
+
companyId,
|
|
493
|
+
agentId,
|
|
494
|
+
runId,
|
|
495
|
+
requestId: options?.requestId,
|
|
496
|
+
providerType: agent.providerType
|
|
497
|
+
},
|
|
498
|
+
failClosed: false
|
|
499
|
+
});
|
|
325
500
|
const workItems = await claimIssuesForAgent(db, companyId, agentId, runId);
|
|
326
501
|
issueIds = workItems.map((item) => item.id);
|
|
502
|
+
await runPluginHook(db, {
|
|
503
|
+
hook: "afterClaim",
|
|
504
|
+
context: {
|
|
505
|
+
companyId,
|
|
506
|
+
agentId,
|
|
507
|
+
runId,
|
|
508
|
+
requestId: options?.requestId,
|
|
509
|
+
providerType: agent.providerType,
|
|
510
|
+
workItemCount: workItems.length
|
|
511
|
+
},
|
|
512
|
+
failClosed: false
|
|
513
|
+
});
|
|
327
514
|
await publishOfficeOccupantForAgent(db, options?.realtimeHub, companyId, agentId);
|
|
328
|
-
const adapter = resolveAdapter(agent.providerType as
|
|
515
|
+
const adapter = resolveAdapter(agent.providerType as HeartbeatProviderType);
|
|
329
516
|
const parsedState = parseAgentState(agent.stateBlob);
|
|
330
517
|
state = parsedState.state;
|
|
331
518
|
stateParseError = parsedState.parseError;
|
|
@@ -347,6 +534,25 @@ export async function runHeartbeatForAgent(
|
|
|
347
534
|
...persistedRuntime.runtimeEnv,
|
|
348
535
|
...heartbeatRuntimeEnv
|
|
349
536
|
},
|
|
537
|
+
onTranscriptEvent: (event: {
|
|
538
|
+
kind: string;
|
|
539
|
+
label?: string;
|
|
540
|
+
text?: string;
|
|
541
|
+
payload?: string;
|
|
542
|
+
signalLevel?: "high" | "medium" | "low" | "noise";
|
|
543
|
+
groupKey?: string;
|
|
544
|
+
source?: "stdout" | "stderr" | "trace_fallback";
|
|
545
|
+
}) => {
|
|
546
|
+
enqueueTranscriptEvent({
|
|
547
|
+
kind: event.kind,
|
|
548
|
+
label: event.label,
|
|
549
|
+
text: event.text,
|
|
550
|
+
payload: event.payload,
|
|
551
|
+
signalLevel: event.signalLevel,
|
|
552
|
+
groupKey: event.groupKey,
|
|
553
|
+
source: event.source
|
|
554
|
+
});
|
|
555
|
+
},
|
|
350
556
|
model: persistedRuntime.runtimeModel,
|
|
351
557
|
thinkingEffort: persistedRuntime.runtimeThinkingEffort,
|
|
352
558
|
bootstrapPrompt: persistedRuntime.bootstrapPrompt,
|
|
@@ -365,15 +571,20 @@ export async function runHeartbeatForAgent(
|
|
|
365
571
|
...state,
|
|
366
572
|
runtime: workspaceResolution.runtime
|
|
367
573
|
};
|
|
574
|
+
memoryContext = await loadAgentMemoryContext({
|
|
575
|
+
companyId,
|
|
576
|
+
agentId
|
|
577
|
+
});
|
|
368
578
|
|
|
369
|
-
|
|
579
|
+
let context = await buildHeartbeatContext(db, companyId, {
|
|
370
580
|
agentId,
|
|
371
581
|
agentName: agent.name,
|
|
372
582
|
agentRole: agent.role,
|
|
373
583
|
managerAgentId: agent.managerAgentId,
|
|
374
|
-
providerType: agent.providerType as
|
|
584
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
375
585
|
heartbeatRunId: runId,
|
|
376
586
|
state,
|
|
587
|
+
memoryContext,
|
|
377
588
|
runtime: workspaceResolution.runtime,
|
|
378
589
|
workItems
|
|
379
590
|
});
|
|
@@ -435,7 +646,7 @@ export async function runHeartbeatForAgent(
|
|
|
435
646
|
if (
|
|
436
647
|
resolveControlPlanePreflightEnabled() &&
|
|
437
648
|
shouldRequireControlPlanePreflight(
|
|
438
|
-
agent.providerType as
|
|
649
|
+
agent.providerType as HeartbeatProviderType,
|
|
439
650
|
workItems.length
|
|
440
651
|
)
|
|
441
652
|
) {
|
|
@@ -463,7 +674,7 @@ export async function runHeartbeatForAgent(
|
|
|
463
674
|
}
|
|
464
675
|
|
|
465
676
|
runtimeLaunchSummary = summarizeRuntimeLaunch(
|
|
466
|
-
agent.providerType as
|
|
677
|
+
agent.providerType as HeartbeatProviderType,
|
|
467
678
|
workspaceResolution.runtime
|
|
468
679
|
);
|
|
469
680
|
await appendAuditEvent(db, {
|
|
@@ -508,6 +719,40 @@ export async function runHeartbeatForAgent(
|
|
|
508
719
|
abortController: activeRunAbort
|
|
509
720
|
});
|
|
510
721
|
|
|
722
|
+
const beforeAdapterHook = await runPluginHook(db, {
|
|
723
|
+
hook: "beforeAdapterExecute",
|
|
724
|
+
context: {
|
|
725
|
+
companyId,
|
|
726
|
+
agentId,
|
|
727
|
+
runId,
|
|
728
|
+
requestId: options?.requestId,
|
|
729
|
+
providerType: agent.providerType,
|
|
730
|
+
workItemCount: workItems.length,
|
|
731
|
+
runtime: {
|
|
732
|
+
command: workspaceResolution.runtime.command,
|
|
733
|
+
cwd: workspaceResolution.runtime.cwd
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
failClosed: true
|
|
737
|
+
});
|
|
738
|
+
if (beforeAdapterHook.blocked) {
|
|
739
|
+
pluginFailureSummary = beforeAdapterHook.failures;
|
|
740
|
+
throw new Error(`Plugin policy blocked adapter execution: ${beforeAdapterHook.failures.join(" | ")}`);
|
|
741
|
+
}
|
|
742
|
+
if (beforeAdapterHook.promptAppend && beforeAdapterHook.promptAppend.trim().length > 0) {
|
|
743
|
+
const existingPrompt = context.runtime?.bootstrapPrompt ?? "";
|
|
744
|
+
const nextPrompt = existingPrompt.trim().length > 0
|
|
745
|
+
? `${existingPrompt}\n\n${beforeAdapterHook.promptAppend}`
|
|
746
|
+
: beforeAdapterHook.promptAppend;
|
|
747
|
+
context = {
|
|
748
|
+
...context,
|
|
749
|
+
runtime: {
|
|
750
|
+
...(context.runtime ?? {}),
|
|
751
|
+
bootstrapPrompt: nextPrompt
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
511
756
|
const execution = await executeAdapterWithWatchdog({
|
|
512
757
|
execute: (abortSignal) =>
|
|
513
758
|
adapter.execute({
|
|
@@ -517,14 +762,78 @@ export async function runHeartbeatForAgent(
|
|
|
517
762
|
abortSignal
|
|
518
763
|
}
|
|
519
764
|
}),
|
|
520
|
-
providerType: agent.providerType as
|
|
765
|
+
providerType: agent.providerType as HeartbeatProviderType,
|
|
521
766
|
runtime: workspaceResolution.runtime,
|
|
522
767
|
externalAbortSignal: activeRunAbort.signal
|
|
523
768
|
});
|
|
524
769
|
executionSummary = execution.summary;
|
|
770
|
+
const afterAdapterHook = await runPluginHook(db, {
|
|
771
|
+
hook: "afterAdapterExecute",
|
|
772
|
+
context: {
|
|
773
|
+
companyId,
|
|
774
|
+
agentId,
|
|
775
|
+
runId,
|
|
776
|
+
requestId: options?.requestId,
|
|
777
|
+
providerType: agent.providerType,
|
|
778
|
+
status: execution.status,
|
|
779
|
+
summary: execution.summary,
|
|
780
|
+
trace: execution.trace ?? null,
|
|
781
|
+
outcome: execution.outcome ?? null
|
|
782
|
+
},
|
|
783
|
+
failClosed: false
|
|
784
|
+
});
|
|
785
|
+
if (afterAdapterHook.failures.length > 0) {
|
|
786
|
+
pluginFailureSummary = [...pluginFailureSummary, ...afterAdapterHook.failures];
|
|
787
|
+
}
|
|
788
|
+
emitCanonicalResultEvent(executionSummary, "completed");
|
|
525
789
|
executionTrace = execution.trace ?? null;
|
|
526
790
|
const parsedOutcome = ExecutionOutcomeSchema.safeParse(execution.outcome);
|
|
527
791
|
executionOutcome = parsedOutcome.success ? parsedOutcome.data : null;
|
|
792
|
+
const persistedMemory = await persistHeartbeatMemory({
|
|
793
|
+
companyId,
|
|
794
|
+
agentId,
|
|
795
|
+
runId,
|
|
796
|
+
status: execution.status,
|
|
797
|
+
summary: execution.summary,
|
|
798
|
+
outcomeKind: executionOutcome?.kind ?? null
|
|
799
|
+
});
|
|
800
|
+
await appendAuditEvent(db, {
|
|
801
|
+
companyId,
|
|
802
|
+
actorType: "system",
|
|
803
|
+
eventType: "heartbeat.memory_updated",
|
|
804
|
+
entityType: "heartbeat_run",
|
|
805
|
+
entityId: runId,
|
|
806
|
+
correlationId: options?.requestId ?? runId,
|
|
807
|
+
payload: {
|
|
808
|
+
agentId,
|
|
809
|
+
memoryRoot: persistedMemory.memoryRoot,
|
|
810
|
+
dailyNotePath: persistedMemory.dailyNotePath,
|
|
811
|
+
candidateFacts: persistedMemory.candidateFacts
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
if (execution.status === "ok") {
|
|
815
|
+
for (const fact of persistedMemory.candidateFacts) {
|
|
816
|
+
const targetFile = await appendDurableFact({
|
|
817
|
+
companyId,
|
|
818
|
+
agentId,
|
|
819
|
+
fact,
|
|
820
|
+
sourceRunId: runId
|
|
821
|
+
});
|
|
822
|
+
await appendAuditEvent(db, {
|
|
823
|
+
companyId,
|
|
824
|
+
actorType: "system",
|
|
825
|
+
eventType: "heartbeat.memory_fact_promoted",
|
|
826
|
+
entityType: "heartbeat_run",
|
|
827
|
+
entityId: runId,
|
|
828
|
+
correlationId: options?.requestId ?? runId,
|
|
829
|
+
payload: {
|
|
830
|
+
agentId,
|
|
831
|
+
fact,
|
|
832
|
+
targetFile
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
528
837
|
|
|
529
838
|
if (execution.tokenInput > 0 || execution.tokenOutput > 0 || execution.usdCost > 0) {
|
|
530
839
|
await appendCost(db, {
|
|
@@ -613,6 +922,23 @@ export async function runHeartbeatForAgent(
|
|
|
613
922
|
});
|
|
614
923
|
}
|
|
615
924
|
|
|
925
|
+
const beforePersistHook = await runPluginHook(db, {
|
|
926
|
+
hook: "beforePersist",
|
|
927
|
+
context: {
|
|
928
|
+
companyId,
|
|
929
|
+
agentId,
|
|
930
|
+
runId,
|
|
931
|
+
requestId: options?.requestId,
|
|
932
|
+
providerType: agent.providerType,
|
|
933
|
+
status: execution.status,
|
|
934
|
+
summary: execution.summary
|
|
935
|
+
},
|
|
936
|
+
failClosed: false
|
|
937
|
+
});
|
|
938
|
+
if (beforePersistHook.failures.length > 0) {
|
|
939
|
+
pluginFailureSummary = [...pluginFailureSummary, ...beforePersistHook.failures];
|
|
940
|
+
}
|
|
941
|
+
|
|
616
942
|
await db
|
|
617
943
|
.update(heartbeatRuns)
|
|
618
944
|
.set({
|
|
@@ -621,6 +947,91 @@ export async function runHeartbeatForAgent(
|
|
|
621
947
|
message: execution.summary
|
|
622
948
|
})
|
|
623
949
|
.where(eq(heartbeatRuns.id, runId));
|
|
950
|
+
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
951
|
+
companyId,
|
|
952
|
+
runId,
|
|
953
|
+
status: execution.status === "failed" ? "failed" : "completed",
|
|
954
|
+
message: execution.summary,
|
|
955
|
+
finishedAt: new Date()
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
const fallbackMessages = normalizeTraceTranscript(executionTrace);
|
|
959
|
+
const fallbackHighSignalCount = fallbackMessages.filter((message) => message.signalLevel === "high").length;
|
|
960
|
+
const shouldAppendFallback =
|
|
961
|
+
fallbackMessages.length > 0 &&
|
|
962
|
+
(transcriptLiveCount === 0 ||
|
|
963
|
+
transcriptLiveUsefulCount < 2 ||
|
|
964
|
+
transcriptLiveHighSignalCount < 1 ||
|
|
965
|
+
(transcriptLiveHighSignalCount < 2 && fallbackHighSignalCount > transcriptLiveHighSignalCount));
|
|
966
|
+
if (shouldAppendFallback) {
|
|
967
|
+
const createdAt = new Date();
|
|
968
|
+
const rows: Array<{
|
|
969
|
+
id: string;
|
|
970
|
+
sequence: number;
|
|
971
|
+
kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
|
|
972
|
+
label: string | null;
|
|
973
|
+
text: string | null;
|
|
974
|
+
payloadJson: string | null;
|
|
975
|
+
signalLevel: "high" | "medium" | "low" | "noise";
|
|
976
|
+
groupKey: string | null;
|
|
977
|
+
source: "trace_fallback";
|
|
978
|
+
createdAt: Date;
|
|
979
|
+
}> = fallbackMessages.map((message) => ({
|
|
980
|
+
id: nanoid(14),
|
|
981
|
+
sequence: transcriptSequence++,
|
|
982
|
+
kind: message.kind,
|
|
983
|
+
label: message.label ?? null,
|
|
984
|
+
text: message.text ?? null,
|
|
985
|
+
payloadJson: message.payload ?? null,
|
|
986
|
+
signalLevel: message.signalLevel,
|
|
987
|
+
groupKey: message.groupKey ?? null,
|
|
988
|
+
source: "trace_fallback",
|
|
989
|
+
createdAt
|
|
990
|
+
}));
|
|
991
|
+
await appendHeartbeatRunMessages(db, {
|
|
992
|
+
companyId,
|
|
993
|
+
runId,
|
|
994
|
+
messages: rows
|
|
995
|
+
});
|
|
996
|
+
options?.realtimeHub?.publish(
|
|
997
|
+
createHeartbeatRunsRealtimeEvent(companyId, {
|
|
998
|
+
type: "run.transcript.append",
|
|
999
|
+
runId,
|
|
1000
|
+
messages: rows.map((row) => ({
|
|
1001
|
+
id: row.id,
|
|
1002
|
+
runId,
|
|
1003
|
+
sequence: row.sequence,
|
|
1004
|
+
kind: normalizeTranscriptKind(row.kind),
|
|
1005
|
+
label: row.label,
|
|
1006
|
+
text: row.text,
|
|
1007
|
+
payload: row.payloadJson,
|
|
1008
|
+
signalLevel: row.signalLevel ?? undefined,
|
|
1009
|
+
groupKey: row.groupKey ?? undefined,
|
|
1010
|
+
source: row.source ?? undefined,
|
|
1011
|
+
createdAt: row.createdAt.toISOString()
|
|
1012
|
+
}))
|
|
1013
|
+
})
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const afterPersistHook = await runPluginHook(db, {
|
|
1018
|
+
hook: "afterPersist",
|
|
1019
|
+
context: {
|
|
1020
|
+
companyId,
|
|
1021
|
+
agentId,
|
|
1022
|
+
runId,
|
|
1023
|
+
requestId: options?.requestId,
|
|
1024
|
+
providerType: agent.providerType,
|
|
1025
|
+
status: execution.status,
|
|
1026
|
+
summary: execution.summary,
|
|
1027
|
+
trace: executionTrace,
|
|
1028
|
+
outcome: executionOutcome
|
|
1029
|
+
},
|
|
1030
|
+
failClosed: false
|
|
1031
|
+
});
|
|
1032
|
+
if (afterPersistHook.failures.length > 0) {
|
|
1033
|
+
pluginFailureSummary = [...pluginFailureSummary, ...afterPersistHook.failures];
|
|
1034
|
+
}
|
|
624
1035
|
|
|
625
1036
|
await appendAuditEvent(db, {
|
|
626
1037
|
companyId,
|
|
@@ -645,7 +1056,8 @@ export async function runHeartbeatForAgent(
|
|
|
645
1056
|
diagnostics: {
|
|
646
1057
|
stateParseError,
|
|
647
1058
|
requestId: options?.requestId,
|
|
648
|
-
trigger: runTrigger
|
|
1059
|
+
trigger: runTrigger,
|
|
1060
|
+
pluginFailures: pluginFailureSummary
|
|
649
1061
|
}
|
|
650
1062
|
}
|
|
651
1063
|
});
|
|
@@ -655,6 +1067,23 @@ export async function runHeartbeatForAgent(
|
|
|
655
1067
|
classified.type === "cancelled"
|
|
656
1068
|
? "Heartbeat cancelled by stop request."
|
|
657
1069
|
: `Heartbeat failed (${classified.type}): ${classified.message}`;
|
|
1070
|
+
emitCanonicalResultEvent(executionSummary, "failed");
|
|
1071
|
+
const pluginErrorHook = await runPluginHook(db, {
|
|
1072
|
+
hook: "onError",
|
|
1073
|
+
context: {
|
|
1074
|
+
companyId,
|
|
1075
|
+
agentId,
|
|
1076
|
+
runId,
|
|
1077
|
+
requestId: options?.requestId,
|
|
1078
|
+
providerType: agent.providerType,
|
|
1079
|
+
error: String(error),
|
|
1080
|
+
summary: executionSummary
|
|
1081
|
+
},
|
|
1082
|
+
failClosed: false
|
|
1083
|
+
});
|
|
1084
|
+
if (pluginErrorHook.failures.length > 0) {
|
|
1085
|
+
pluginFailureSummary = [...pluginFailureSummary, ...pluginErrorHook.failures];
|
|
1086
|
+
}
|
|
658
1087
|
if (!executionTrace && classified.type === "cancelled") {
|
|
659
1088
|
executionTrace = {
|
|
660
1089
|
command: runtimeLaunchSummary?.command ?? null,
|
|
@@ -688,6 +1117,13 @@ export async function runHeartbeatForAgent(
|
|
|
688
1117
|
message: executionSummary
|
|
689
1118
|
})
|
|
690
1119
|
.where(eq(heartbeatRuns.id, runId));
|
|
1120
|
+
publishHeartbeatRunStatus(options?.realtimeHub, {
|
|
1121
|
+
companyId,
|
|
1122
|
+
runId,
|
|
1123
|
+
status: "failed",
|
|
1124
|
+
message: executionSummary,
|
|
1125
|
+
finishedAt: new Date()
|
|
1126
|
+
});
|
|
691
1127
|
await appendAuditEvent(db, {
|
|
692
1128
|
companyId,
|
|
693
1129
|
actorType: "system",
|
|
@@ -710,7 +1146,8 @@ export async function runHeartbeatForAgent(
|
|
|
710
1146
|
diagnostics: {
|
|
711
1147
|
stateParseError,
|
|
712
1148
|
requestId: options?.requestId,
|
|
713
|
-
trigger: runTrigger
|
|
1149
|
+
trigger: runTrigger,
|
|
1150
|
+
pluginFailures: pluginFailureSummary
|
|
714
1151
|
}
|
|
715
1152
|
}
|
|
716
1153
|
});
|
|
@@ -731,6 +1168,7 @@ export async function runHeartbeatForAgent(
|
|
|
731
1168
|
});
|
|
732
1169
|
}
|
|
733
1170
|
} finally {
|
|
1171
|
+
await transcriptWriteQueue;
|
|
734
1172
|
unregisterActiveHeartbeatRun(runId);
|
|
735
1173
|
try {
|
|
736
1174
|
await releaseClaimedIssues(db, companyId, issueIds);
|
|
@@ -899,9 +1337,10 @@ async function buildHeartbeatContext(
|
|
|
899
1337
|
agentName: string;
|
|
900
1338
|
agentRole: string;
|
|
901
1339
|
managerAgentId: string | null;
|
|
902
|
-
providerType:
|
|
1340
|
+
providerType: HeartbeatProviderType;
|
|
903
1341
|
heartbeatRunId: string;
|
|
904
1342
|
state: AgentState;
|
|
1343
|
+
memoryContext?: HeartbeatContext["memoryContext"];
|
|
905
1344
|
runtime?: { command?: string; args?: string[]; cwd?: string; timeoutMs?: number };
|
|
906
1345
|
workItems: Array<{
|
|
907
1346
|
id: string;
|
|
@@ -929,6 +1368,48 @@ async function buildHeartbeatContext(
|
|
|
929
1368
|
.where(and(eq(projects.companyId, companyId), inArray(projects.id, projectIds)))
|
|
930
1369
|
: [];
|
|
931
1370
|
const projectNameById = new Map(projectRows.map((row) => [row.id, row.name]));
|
|
1371
|
+
const projectWorkspaceMap = await getProjectWorkspaceMap(db, companyId, projectIds);
|
|
1372
|
+
const issueIds = input.workItems.map((item) => item.id);
|
|
1373
|
+
const attachmentRows =
|
|
1374
|
+
issueIds.length > 0
|
|
1375
|
+
? await db
|
|
1376
|
+
.select({
|
|
1377
|
+
id: issueAttachments.id,
|
|
1378
|
+
issueId: issueAttachments.issueId,
|
|
1379
|
+
projectId: issueAttachments.projectId,
|
|
1380
|
+
fileName: issueAttachments.fileName,
|
|
1381
|
+
mimeType: issueAttachments.mimeType,
|
|
1382
|
+
fileSizeBytes: issueAttachments.fileSizeBytes,
|
|
1383
|
+
relativePath: issueAttachments.relativePath
|
|
1384
|
+
})
|
|
1385
|
+
.from(issueAttachments)
|
|
1386
|
+
.where(and(eq(issueAttachments.companyId, companyId), inArray(issueAttachments.issueId, issueIds)))
|
|
1387
|
+
: [];
|
|
1388
|
+
const attachmentsByIssue = new Map<
|
|
1389
|
+
string,
|
|
1390
|
+
Array<{
|
|
1391
|
+
id: string;
|
|
1392
|
+
fileName: string;
|
|
1393
|
+
mimeType: string | null;
|
|
1394
|
+
fileSizeBytes: number;
|
|
1395
|
+
relativePath: string;
|
|
1396
|
+
absolutePath: string;
|
|
1397
|
+
}>
|
|
1398
|
+
>();
|
|
1399
|
+
for (const row of attachmentRows) {
|
|
1400
|
+
const projectWorkspace = projectWorkspaceMap.get(row.projectId) ?? resolveProjectWorkspacePath(companyId, row.projectId);
|
|
1401
|
+
const absolutePath = resolve(projectWorkspace, row.relativePath);
|
|
1402
|
+
const existing = attachmentsByIssue.get(row.issueId) ?? [];
|
|
1403
|
+
existing.push({
|
|
1404
|
+
id: row.id,
|
|
1405
|
+
fileName: row.fileName,
|
|
1406
|
+
mimeType: row.mimeType,
|
|
1407
|
+
fileSizeBytes: row.fileSizeBytes,
|
|
1408
|
+
relativePath: row.relativePath,
|
|
1409
|
+
absolutePath
|
|
1410
|
+
});
|
|
1411
|
+
attachmentsByIssue.set(row.issueId, existing);
|
|
1412
|
+
}
|
|
932
1413
|
const goalRows = await db
|
|
933
1414
|
.select({
|
|
934
1415
|
id: goals.id,
|
|
@@ -968,6 +1449,7 @@ async function buildHeartbeatContext(
|
|
|
968
1449
|
managerAgentId: input.managerAgentId
|
|
969
1450
|
},
|
|
970
1451
|
state: input.state,
|
|
1452
|
+
memoryContext: input.memoryContext,
|
|
971
1453
|
runtime: input.runtime,
|
|
972
1454
|
goalContext: {
|
|
973
1455
|
companyGoals: activeCompanyGoals,
|
|
@@ -983,7 +1465,8 @@ async function buildHeartbeatContext(
|
|
|
983
1465
|
status: item.status,
|
|
984
1466
|
priority: item.priority,
|
|
985
1467
|
labels: parseStringArray(item.labels_json),
|
|
986
|
-
tags: parseStringArray(item.tags_json)
|
|
1468
|
+
tags: parseStringArray(item.tags_json),
|
|
1469
|
+
attachments: attachmentsByIssue.get(item.id) ?? []
|
|
987
1470
|
}))
|
|
988
1471
|
};
|
|
989
1472
|
}
|
|
@@ -1116,6 +1599,131 @@ function readTraceString(trace: unknown, key: string) {
|
|
|
1116
1599
|
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
1117
1600
|
}
|
|
1118
1601
|
|
|
1602
|
+
function normalizeTraceTranscript(trace: unknown) {
|
|
1603
|
+
type NormalizedTranscriptMessage = {
|
|
1604
|
+
kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
|
|
1605
|
+
label: string | undefined;
|
|
1606
|
+
text: string | undefined;
|
|
1607
|
+
payload: string | undefined;
|
|
1608
|
+
signalLevel: "high" | "medium" | "low" | "noise";
|
|
1609
|
+
groupKey: string | undefined;
|
|
1610
|
+
};
|
|
1611
|
+
if (!trace || typeof trace !== "object") {
|
|
1612
|
+
return [] as NormalizedTranscriptMessage[];
|
|
1613
|
+
}
|
|
1614
|
+
const transcript = (trace as Record<string, unknown>).transcript;
|
|
1615
|
+
if (!Array.isArray(transcript)) {
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
const normalized: NormalizedTranscriptMessage[] = [];
|
|
1619
|
+
for (const entry of transcript) {
|
|
1620
|
+
if (!entry || typeof entry !== "object") {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
const record = entry as Record<string, unknown>;
|
|
1624
|
+
const kind = normalizeTranscriptKind(String(record.kind ?? "system"));
|
|
1625
|
+
const label = typeof record.label === "string" ? record.label : undefined;
|
|
1626
|
+
normalized.push({
|
|
1627
|
+
kind,
|
|
1628
|
+
label: typeof record.label === "string" ? record.label : undefined,
|
|
1629
|
+
text: typeof record.text === "string" ? record.text : undefined,
|
|
1630
|
+
payload: typeof record.payload === "string" ? record.payload : undefined,
|
|
1631
|
+
signalLevel: normalizeTranscriptSignalLevel(
|
|
1632
|
+
typeof record.signalLevel === "string" ? (record.signalLevel as "high" | "medium" | "low" | "noise") : undefined,
|
|
1633
|
+
kind
|
|
1634
|
+
),
|
|
1635
|
+
groupKey:
|
|
1636
|
+
typeof record.groupKey === "string" && record.groupKey.trim().length > 0
|
|
1637
|
+
? record.groupKey
|
|
1638
|
+
: defaultTranscriptGroupKey(kind, label)
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
return normalized;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function normalizeTranscriptKind(
|
|
1645
|
+
value: string
|
|
1646
|
+
): "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr" {
|
|
1647
|
+
const normalized = value.trim().toLowerCase();
|
|
1648
|
+
if (
|
|
1649
|
+
normalized === "system" ||
|
|
1650
|
+
normalized === "assistant" ||
|
|
1651
|
+
normalized === "thinking" ||
|
|
1652
|
+
normalized === "tool_call" ||
|
|
1653
|
+
normalized === "tool_result" ||
|
|
1654
|
+
normalized === "result" ||
|
|
1655
|
+
normalized === "stderr"
|
|
1656
|
+
) {
|
|
1657
|
+
return normalized;
|
|
1658
|
+
}
|
|
1659
|
+
return "system";
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function defaultTranscriptGroupKey(kind: string, label?: string) {
|
|
1663
|
+
if (kind === "tool_call" || kind === "tool_result") {
|
|
1664
|
+
return `tool:${(label ?? "unknown").trim().toLowerCase()}`;
|
|
1665
|
+
}
|
|
1666
|
+
if (kind === "result") {
|
|
1667
|
+
return "result";
|
|
1668
|
+
}
|
|
1669
|
+
if (kind === "assistant") {
|
|
1670
|
+
return "assistant";
|
|
1671
|
+
}
|
|
1672
|
+
if (kind === "stderr") {
|
|
1673
|
+
return "stderr";
|
|
1674
|
+
}
|
|
1675
|
+
return "system";
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function normalizeTranscriptSignalLevel(
|
|
1679
|
+
value: "high" | "medium" | "low" | "noise" | undefined,
|
|
1680
|
+
kind: string
|
|
1681
|
+
): "high" | "medium" | "low" | "noise" {
|
|
1682
|
+
if (value === "high" || value === "medium" || value === "low" || value === "noise") {
|
|
1683
|
+
return value;
|
|
1684
|
+
}
|
|
1685
|
+
if (kind === "tool_call" || kind === "tool_result" || kind === "result") {
|
|
1686
|
+
return "high";
|
|
1687
|
+
}
|
|
1688
|
+
if (kind === "assistant") {
|
|
1689
|
+
return "medium";
|
|
1690
|
+
}
|
|
1691
|
+
if (kind === "stderr") {
|
|
1692
|
+
return "low";
|
|
1693
|
+
}
|
|
1694
|
+
return "noise";
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function isUsefulTranscriptSignal(level: "high" | "medium" | "low" | "noise") {
|
|
1698
|
+
return level === "high" || level === "medium";
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function publishHeartbeatRunStatus(
|
|
1702
|
+
realtimeHub: RealtimeHub | undefined,
|
|
1703
|
+
input: {
|
|
1704
|
+
companyId: string;
|
|
1705
|
+
runId: string;
|
|
1706
|
+
status: "started" | "completed" | "failed" | "skipped";
|
|
1707
|
+
message?: string | null;
|
|
1708
|
+
startedAt?: Date;
|
|
1709
|
+
finishedAt?: Date;
|
|
1710
|
+
}
|
|
1711
|
+
) {
|
|
1712
|
+
if (!realtimeHub) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
realtimeHub.publish(
|
|
1716
|
+
createHeartbeatRunsRealtimeEvent(input.companyId, {
|
|
1717
|
+
type: "run.status.updated",
|
|
1718
|
+
runId: input.runId,
|
|
1719
|
+
status: input.status,
|
|
1720
|
+
message: input.message ?? null,
|
|
1721
|
+
startedAt: input.startedAt?.toISOString(),
|
|
1722
|
+
finishedAt: input.finishedAt?.toISOString() ?? null
|
|
1723
|
+
})
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1119
1727
|
async function resolveRuntimeWorkspaceForWorkItems(
|
|
1120
1728
|
db: BopoDb,
|
|
1121
1729
|
companyId: string,
|
|
@@ -1226,7 +1834,7 @@ function resolveEffectiveStaleRunThresholdMs(input: {
|
|
|
1226
1834
|
|
|
1227
1835
|
async function executeAdapterWithWatchdog<T>(input: {
|
|
1228
1836
|
execute: (abortSignal: AbortSignal) => Promise<T>;
|
|
1229
|
-
providerType:
|
|
1837
|
+
providerType: HeartbeatProviderType;
|
|
1230
1838
|
externalAbortSignal?: AbortSignal;
|
|
1231
1839
|
runtime:
|
|
1232
1840
|
| {
|
|
@@ -1314,7 +1922,7 @@ class AdapterExecutionCancelledError extends Error {
|
|
|
1314
1922
|
}
|
|
1315
1923
|
|
|
1316
1924
|
function resolveAdapterWatchdogTimeoutMs(
|
|
1317
|
-
providerType:
|
|
1925
|
+
providerType: HeartbeatProviderType,
|
|
1318
1926
|
runtime:
|
|
1319
1927
|
| {
|
|
1320
1928
|
timeoutMs?: number;
|
|
@@ -1331,7 +1939,7 @@ function resolveAdapterWatchdogTimeoutMs(
|
|
|
1331
1939
|
}
|
|
1332
1940
|
|
|
1333
1941
|
function estimateProviderExecutionBudgetMs(
|
|
1334
|
-
providerType:
|
|
1942
|
+
providerType: HeartbeatProviderType,
|
|
1335
1943
|
runtime:
|
|
1336
1944
|
| {
|
|
1337
1945
|
timeoutMs?: number;
|
|
@@ -1351,32 +1959,26 @@ function estimateProviderExecutionBudgetMs(
|
|
|
1351
1959
|
}
|
|
1352
1960
|
|
|
1353
1961
|
function resolveRuntimeAttemptTimeoutMs(
|
|
1354
|
-
providerType:
|
|
1962
|
+
providerType: HeartbeatProviderType,
|
|
1355
1963
|
configuredTimeoutMs: number | undefined
|
|
1356
1964
|
) {
|
|
1357
1965
|
if (Number.isFinite(configuredTimeoutMs) && (configuredTimeoutMs ?? 0) > 0) {
|
|
1358
1966
|
return Math.floor(configuredTimeoutMs ?? 0);
|
|
1359
1967
|
}
|
|
1360
|
-
if (providerType === "claude_code") {
|
|
1361
|
-
return
|
|
1362
|
-
}
|
|
1363
|
-
if (providerType === "codex") {
|
|
1364
|
-
return 5 * 60 * 1000;
|
|
1365
|
-
}
|
|
1366
|
-
if (providerType === "cursor") {
|
|
1367
|
-
return 30_000;
|
|
1968
|
+
if (providerType === "claude_code" || providerType === "codex" || providerType === "opencode" || providerType === "cursor") {
|
|
1969
|
+
return 15 * 60 * 1000;
|
|
1368
1970
|
}
|
|
1369
|
-
return
|
|
1971
|
+
return 15 * 60 * 1000;
|
|
1370
1972
|
}
|
|
1371
1973
|
|
|
1372
1974
|
function resolveRuntimeRetryCount(
|
|
1373
|
-
providerType:
|
|
1975
|
+
providerType: HeartbeatProviderType,
|
|
1374
1976
|
configuredRetryCount: number | undefined
|
|
1375
1977
|
) {
|
|
1376
1978
|
if (Number.isFinite(configuredRetryCount)) {
|
|
1377
1979
|
return Math.max(0, Math.min(2, Math.floor(configuredRetryCount ?? 0)));
|
|
1378
1980
|
}
|
|
1379
|
-
return providerType === "codex" ? 1 : 0;
|
|
1981
|
+
return providerType === "codex" || providerType === "opencode" ? 1 : 0;
|
|
1380
1982
|
}
|
|
1381
1983
|
|
|
1382
1984
|
function mergeRuntimeForExecution(
|
|
@@ -1550,7 +2152,7 @@ function resolveClaudeApiKey() {
|
|
|
1550
2152
|
}
|
|
1551
2153
|
|
|
1552
2154
|
function summarizeRuntimeLaunch(
|
|
1553
|
-
providerType:
|
|
2155
|
+
providerType: HeartbeatProviderType,
|
|
1554
2156
|
runtime:
|
|
1555
2157
|
| {
|
|
1556
2158
|
command?: string;
|
|
@@ -1636,7 +2238,7 @@ function validateControlPlaneRuntimeEnv(runtimeEnv: Record<string, string>, runI
|
|
|
1636
2238
|
}
|
|
1637
2239
|
|
|
1638
2240
|
function shouldRequireControlPlanePreflight(
|
|
1639
|
-
providerType:
|
|
2241
|
+
providerType: HeartbeatProviderType,
|
|
1640
2242
|
workItemCount: number
|
|
1641
2243
|
) {
|
|
1642
2244
|
if (workItemCount < 1) {
|