@paperclipai/server 0.2.2
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/dist/adapters/codex-models.d.ts +4 -0
- package/dist/adapters/codex-models.d.ts.map +1 -0
- package/dist/adapters/codex-models.js +98 -0
- package/dist/adapters/codex-models.js.map +1 -0
- package/dist/adapters/http/execute.d.ts +3 -0
- package/dist/adapters/http/execute.d.ts.map +1 -0
- package/dist/adapters/http/execute.js +39 -0
- package/dist/adapters/http/execute.js.map +1 -0
- package/dist/adapters/http/index.d.ts +3 -0
- package/dist/adapters/http/index.d.ts.map +1 -0
- package/dist/adapters/http/index.js +20 -0
- package/dist/adapters/http/index.js.map +1 -0
- package/dist/adapters/http/test.d.ts +3 -0
- package/dist/adapters/http/test.d.ts.map +1 -0
- package/dist/adapters/http/test.js +106 -0
- package/dist/adapters/http/test.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/process/execute.d.ts +3 -0
- package/dist/adapters/process/execute.d.ts.map +1 -0
- package/dist/adapters/process/execute.js +63 -0
- package/dist/adapters/process/execute.js.map +1 -0
- package/dist/adapters/process/index.d.ts +3 -0
- package/dist/adapters/process/index.d.ts.map +1 -0
- package/dist/adapters/process/index.js +23 -0
- package/dist/adapters/process/index.js.map +1 -0
- package/dist/adapters/process/test.d.ts +3 -0
- package/dist/adapters/process/test.d.ts.map +1 -0
- package/dist/adapters/process/test.js +77 -0
- package/dist/adapters/process/test.js.map +1 -0
- package/dist/adapters/registry.d.ts +9 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +63 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/utils.d.ts +10 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +14 -0
- package/dist/adapters/utils.js.map +1 -0
- package/dist/agent-auth-jwt.d.ts +14 -0
- package/dist/agent-auth-jwt.d.ts.map +1 -0
- package/dist/agent-auth-jwt.js +117 -0
- package/dist/agent-auth-jwt.js.map +1 -0
- package/dist/app.d.ts +20 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +127 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/better-auth.d.ts +23 -0
- package/dist/auth/better-auth.d.ts.map +1 -0
- package/dist/auth/better-auth.js +80 -0
- package/dist/auth/better-auth.js.map +1 -0
- package/dist/board-claim.d.ts +23 -0
- package/dist/board-claim.d.ts.map +1 -0
- package/dist/board-claim.js +115 -0
- package/dist/board-claim.js.map +1 -0
- package/dist/config-file.d.ts +3 -0
- package/dist/config-file.d.ts.map +1 -0
- package/dist/config-file.js +16 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +114 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/home-paths.d.ts +11 -0
- package/dist/home-paths.d.ts.map +1 -0
- package/dist/home-paths.js +54 -0
- package/dist/home-paths.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +439 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +12 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +124 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/board-mutation-guard.d.ts +3 -0
- package/dist/middleware/board-mutation-guard.d.ts.map +1 -0
- package/dist/middleware/board-mutation-guard.js +60 -0
- package/dist/middleware/board-mutation-guard.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +3 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +22 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +4 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +37 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/private-hostname-guard.d.ts +11 -0
- package/dist/middleware/private-hostname-guard.d.ts.map +1 -0
- package/dist/middleware/private-hostname-guard.js +78 -0
- package/dist/middleware/private-hostname-guard.js.map +1 -0
- package/dist/middleware/validate.d.ts +4 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +7 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -0
- package/dist/realtime/live-events-ws.d.ts +10 -0
- package/dist/realtime/live-events-ws.d.ts.map +1 -0
- package/dist/realtime/live-events-ws.js +185 -0
- package/dist/realtime/live-events-ws.js.map +1 -0
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +63 -0
- package/dist/redaction.js.map +1 -0
- package/dist/routes/access.d.ts +9 -0
- package/dist/routes/access.d.ts.map +1 -0
- package/dist/routes/access.js +887 -0
- package/dist/routes/access.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +87 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/agents.d.ts +3 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +1132 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/approvals.d.ts +3 -0
- package/dist/routes/approvals.d.ts.map +1 -0
- package/dist/routes/approvals.js +271 -0
- package/dist/routes/approvals.js.map +1 -0
- package/dist/routes/assets.d.ts +4 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +138 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/authz.d.ts +15 -0
- package/dist/routes/authz.d.ts.map +1 -0
- package/dist/routes/authz.js +40 -0
- package/dist/routes/authz.js.map +1 -0
- package/dist/routes/companies.d.ts +3 -0
- package/dist/routes/companies.d.ts.map +1 -0
- package/dist/routes/companies.js +159 -0
- package/dist/routes/companies.js.map +1 -0
- package/dist/routes/costs.d.ts +3 -0
- package/dist/routes/costs.d.ts.map +1 -0
- package/dist/routes/costs.js +113 -0
- package/dist/routes/costs.js.map +1 -0
- package/dist/routes/dashboard.d.ts +3 -0
- package/dist/routes/dashboard.d.ts.map +1 -0
- package/dist/routes/dashboard.js +15 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/goals.d.ts +3 -0
- package/dist/routes/goals.d.ts.map +1 -0
- package/dist/routes/goals.js +95 -0
- package/dist/routes/goals.js.map +1 -0
- package/dist/routes/health.d.ts +9 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +38 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +15 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +15 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/issues.d.ts +4 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +973 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/llms.d.ts +3 -0
- package/dist/routes/llms.d.ts.map +1 -0
- package/dist/routes/llms.js +78 -0
- package/dist/routes/llms.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +253 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/secrets.d.ts +3 -0
- package/dist/routes/secrets.d.ts.map +1 -0
- package/dist/routes/secrets.js +128 -0
- package/dist/routes/secrets.js.map +1 -0
- package/dist/routes/sidebar-badges.d.ts +3 -0
- package/dist/routes/sidebar-badges.d.ts.map +1 -0
- package/dist/routes/sidebar-badges.js +47 -0
- package/dist/routes/sidebar-badges.js.map +1 -0
- package/dist/secrets/external-stub-providers.d.ts +5 -0
- package/dist/secrets/external-stub-providers.d.ts.map +1 -0
- package/dist/secrets/external-stub-providers.js +21 -0
- package/dist/secrets/external-stub-providers.js.map +1 -0
- package/dist/secrets/local-encrypted-provider.d.ts +3 -0
- package/dist/secrets/local-encrypted-provider.d.ts.map +1 -0
- package/dist/secrets/local-encrypted-provider.js +116 -0
- package/dist/secrets/local-encrypted-provider.js.map +1 -0
- package/dist/secrets/provider-registry.d.ts +5 -0
- package/dist/secrets/provider-registry.d.ts.map +1 -0
- package/dist/secrets/provider-registry.js +20 -0
- package/dist/secrets/provider-registry.js.map +1 -0
- package/dist/secrets/types.d.ts +21 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +2 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/services/access.d.ts +81 -0
- package/dist/services/access.d.ts.map +1 -0
- package/dist/services/access.js +187 -0
- package/dist/services/access.js.map +1 -0
- package/dist/services/activity-log.d.ts +14 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +32 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/activity.d.ts +764 -0
- package/dist/services/activity.d.ts.map +1 -0
- package/dist/services/activity.js +105 -0
- package/dist/services/activity.js.map +1 -0
- package/dist/services/agent-permissions.d.ts +6 -0
- package/dist/services/agent-permissions.d.ts.map +1 -0
- package/dist/services/agent-permissions.js +18 -0
- package/dist/services/agent-permissions.js.map +1 -0
- package/dist/services/agents.d.ts +1494 -0
- package/dist/services/agents.d.ts.map +1 -0
- package/dist/services/agents.js +454 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/approvals.d.ts +540 -0
- package/dist/services/approvals.d.ts.map +1 -0
- package/dist/services/approvals.js +173 -0
- package/dist/services/approvals.js.map +1 -0
- package/dist/services/assets.d.ts +33 -0
- package/dist/services/assets.d.ts.map +1 -0
- package/dist/services/assets.js +17 -0
- package/dist/services/assets.js.map +1 -0
- package/dist/services/companies.d.ts +503 -0
- package/dist/services/companies.d.ts.map +1 -0
- package/dist/services/companies.js +120 -0
- package/dist/services/companies.js.map +1 -0
- package/dist/services/company-portability.d.ts +8 -0
- package/dist/services/company-portability.d.ts.map +1 -0
- package/dist/services/company-portability.js +851 -0
- package/dist/services/company-portability.js.map +1 -0
- package/dist/services/costs.d.ts +50 -0
- package/dist/services/costs.d.ts.map +1 -0
- package/dist/services/costs.js +166 -0
- package/dist/services/costs.js.map +1 -0
- package/dist/services/dashboard.d.ts +21 -0
- package/dist/services/dashboard.d.ts.map +1 -0
- package/dist/services/dashboard.js +96 -0
- package/dist/services/dashboard.js.map +1 -0
- package/dist/services/goals.d.ts +407 -0
- package/dist/services/goals.d.ts.map +1 -0
- package/dist/services/goals.js +29 -0
- package/dist/services/goals.js.map +1 -0
- package/dist/services/heartbeat.d.ts +1666 -0
- package/dist/services/heartbeat.d.ts.map +1 -0
- package/dist/services/heartbeat.js +1752 -0
- package/dist/services/heartbeat.js.map +1 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +20 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/issue-approvals.d.ts +56 -0
- package/dist/services/issue-approvals.d.ts.map +1 -0
- package/dist/services/issue-approvals.js +153 -0
- package/dist/services/issue-approvals.js.map +1 -0
- package/dist/services/issues.d.ts +756 -0
- package/dist/services/issues.d.ts.map +1 -0
- package/dist/services/issues.js +917 -0
- package/dist/services/issues.js.map +1 -0
- package/dist/services/live-events.d.ts +12 -0
- package/dist/services/live-events.d.ts.map +1 -0
- package/dist/services/live-events.js +24 -0
- package/dist/services/live-events.js.map +1 -0
- package/dist/services/projects.d.ts +66 -0
- package/dist/services/projects.d.ts.map +1 -0
- package/dist/services/projects.js +472 -0
- package/dist/services/projects.js.map +1 -0
- package/dist/services/run-log-store.d.ts +34 -0
- package/dist/services/run-log-store.d.ts.map +1 -0
- package/dist/services/run-log-store.js +112 -0
- package/dist/services/run-log-store.js.map +1 -0
- package/dist/services/secrets.d.ts +506 -0
- package/dist/services/secrets.d.ts.map +1 -0
- package/dist/services/secrets.js +284 -0
- package/dist/services/secrets.js.map +1 -0
- package/dist/services/sidebar-badges.d.ts +9 -0
- package/dist/services/sidebar-badges.d.ts.map +1 -0
- package/dist/services/sidebar-badges.js +33 -0
- package/dist/services/sidebar-badges.js.map +1 -0
- package/dist/startup-banner.d.ts +27 -0
- package/dist/startup-banner.d.ts.map +1 -0
- package/dist/startup-banner.js +112 -0
- package/dist/startup-banner.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +29 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-disk-provider.d.ts +3 -0
- package/dist/storage/local-disk-provider.d.ts.map +1 -0
- package/dist/storage/local-disk-provider.js +79 -0
- package/dist/storage/local-disk-provider.js.map +1 -0
- package/dist/storage/provider-registry.d.ts +4 -0
- package/dist/storage/provider-registry.d.ts.map +1 -0
- package/dist/storage/provider-registry.js +15 -0
- package/dist/storage/provider-registry.js.map +1 -0
- package/dist/storage/s3-provider.d.ts +11 -0
- package/dist/storage/s3-provider.d.ts.map +1 -0
- package/dist/storage/s3-provider.js +123 -0
- package/dist/storage/s3-provider.js.map +1 -0
- package/dist/storage/service.d.ts +3 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +120 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,1752 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
|
3
|
+
import { agents, agentRuntimeState, agentTaskSessions, agentWakeupRequests, heartbeatRunEvents, heartbeatRuns, costEvents, issues, projectWorkspaces, } from "@paperclipai/db";
|
|
4
|
+
import { conflict, notFound } from "../errors.js";
|
|
5
|
+
import { logger } from "../middleware/logger.js";
|
|
6
|
+
import { publishLiveEvent } from "./live-events.js";
|
|
7
|
+
import { getRunLogStore } from "./run-log-store.js";
|
|
8
|
+
import { getServerAdapter, runningProcesses } from "../adapters/index.js";
|
|
9
|
+
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
|
|
10
|
+
import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js";
|
|
11
|
+
import { secretService } from "./secrets.js";
|
|
12
|
+
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
|
|
13
|
+
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
|
14
|
+
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
|
15
|
+
const HEARTBEAT_MAX_CONCURRENT_RUNS_MAX = 10;
|
|
16
|
+
const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
|
|
17
|
+
const startLocksByAgent = new Map();
|
|
18
|
+
const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
|
|
19
|
+
function appendExcerpt(prev, chunk) {
|
|
20
|
+
return appendWithCap(prev, chunk, MAX_EXCERPT_BYTES);
|
|
21
|
+
}
|
|
22
|
+
function normalizeMaxConcurrentRuns(value) {
|
|
23
|
+
const parsed = Math.floor(asNumber(value, HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT));
|
|
24
|
+
if (!Number.isFinite(parsed))
|
|
25
|
+
return HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT;
|
|
26
|
+
return Math.max(HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT, Math.min(HEARTBEAT_MAX_CONCURRENT_RUNS_MAX, parsed));
|
|
27
|
+
}
|
|
28
|
+
async function withAgentStartLock(agentId, fn) {
|
|
29
|
+
const previous = startLocksByAgent.get(agentId) ?? Promise.resolve();
|
|
30
|
+
const run = previous.then(fn);
|
|
31
|
+
const marker = run.then(() => undefined, () => undefined);
|
|
32
|
+
startLocksByAgent.set(agentId, marker);
|
|
33
|
+
try {
|
|
34
|
+
return await run;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (startLocksByAgent.get(agentId) === marker) {
|
|
38
|
+
startLocksByAgent.delete(agentId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readNonEmptyString(value) {
|
|
43
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
44
|
+
}
|
|
45
|
+
function parseIssueAssigneeAdapterOverrides(raw) {
|
|
46
|
+
const parsed = parseObject(raw);
|
|
47
|
+
const parsedAdapterConfig = parseObject(parsed.adapterConfig);
|
|
48
|
+
const adapterConfig = Object.keys(parsedAdapterConfig).length > 0 ? parsedAdapterConfig : null;
|
|
49
|
+
const useProjectWorkspace = typeof parsed.useProjectWorkspace === "boolean"
|
|
50
|
+
? parsed.useProjectWorkspace
|
|
51
|
+
: null;
|
|
52
|
+
if (!adapterConfig && useProjectWorkspace === null)
|
|
53
|
+
return null;
|
|
54
|
+
return {
|
|
55
|
+
adapterConfig,
|
|
56
|
+
useProjectWorkspace,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function deriveTaskKey(contextSnapshot, payload) {
|
|
60
|
+
return (readNonEmptyString(contextSnapshot?.taskKey) ??
|
|
61
|
+
readNonEmptyString(contextSnapshot?.taskId) ??
|
|
62
|
+
readNonEmptyString(contextSnapshot?.issueId) ??
|
|
63
|
+
readNonEmptyString(payload?.taskKey) ??
|
|
64
|
+
readNonEmptyString(payload?.taskId) ??
|
|
65
|
+
readNonEmptyString(payload?.issueId) ??
|
|
66
|
+
null);
|
|
67
|
+
}
|
|
68
|
+
function deriveCommentId(contextSnapshot, payload) {
|
|
69
|
+
return (readNonEmptyString(contextSnapshot?.wakeCommentId) ??
|
|
70
|
+
readNonEmptyString(contextSnapshot?.commentId) ??
|
|
71
|
+
readNonEmptyString(payload?.commentId) ??
|
|
72
|
+
null);
|
|
73
|
+
}
|
|
74
|
+
function enrichWakeContextSnapshot(input) {
|
|
75
|
+
const { contextSnapshot, reason, source, triggerDetail, payload } = input;
|
|
76
|
+
const issueIdFromPayload = readNonEmptyString(payload?.["issueId"]);
|
|
77
|
+
const commentIdFromPayload = readNonEmptyString(payload?.["commentId"]);
|
|
78
|
+
const taskKey = deriveTaskKey(contextSnapshot, payload);
|
|
79
|
+
const wakeCommentId = deriveCommentId(contextSnapshot, payload);
|
|
80
|
+
if (!readNonEmptyString(contextSnapshot["wakeReason"]) && reason) {
|
|
81
|
+
contextSnapshot.wakeReason = reason;
|
|
82
|
+
}
|
|
83
|
+
if (!readNonEmptyString(contextSnapshot["issueId"]) && issueIdFromPayload) {
|
|
84
|
+
contextSnapshot.issueId = issueIdFromPayload;
|
|
85
|
+
}
|
|
86
|
+
if (!readNonEmptyString(contextSnapshot["taskId"]) && issueIdFromPayload) {
|
|
87
|
+
contextSnapshot.taskId = issueIdFromPayload;
|
|
88
|
+
}
|
|
89
|
+
if (!readNonEmptyString(contextSnapshot["taskKey"]) && taskKey) {
|
|
90
|
+
contextSnapshot.taskKey = taskKey;
|
|
91
|
+
}
|
|
92
|
+
if (!readNonEmptyString(contextSnapshot["commentId"]) && commentIdFromPayload) {
|
|
93
|
+
contextSnapshot.commentId = commentIdFromPayload;
|
|
94
|
+
}
|
|
95
|
+
if (!readNonEmptyString(contextSnapshot["wakeCommentId"]) && wakeCommentId) {
|
|
96
|
+
contextSnapshot.wakeCommentId = wakeCommentId;
|
|
97
|
+
}
|
|
98
|
+
if (!readNonEmptyString(contextSnapshot["wakeSource"]) && source) {
|
|
99
|
+
contextSnapshot.wakeSource = source;
|
|
100
|
+
}
|
|
101
|
+
if (!readNonEmptyString(contextSnapshot["wakeTriggerDetail"]) && triggerDetail) {
|
|
102
|
+
contextSnapshot.wakeTriggerDetail = triggerDetail;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
contextSnapshot,
|
|
106
|
+
issueIdFromPayload,
|
|
107
|
+
commentIdFromPayload,
|
|
108
|
+
taskKey,
|
|
109
|
+
wakeCommentId,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function mergeCoalescedContextSnapshot(existingRaw, incoming) {
|
|
113
|
+
const existing = parseObject(existingRaw);
|
|
114
|
+
const merged = {
|
|
115
|
+
...existing,
|
|
116
|
+
...incoming,
|
|
117
|
+
};
|
|
118
|
+
const commentId = deriveCommentId(incoming, null);
|
|
119
|
+
if (commentId) {
|
|
120
|
+
merged.commentId = commentId;
|
|
121
|
+
merged.wakeCommentId = commentId;
|
|
122
|
+
}
|
|
123
|
+
return merged;
|
|
124
|
+
}
|
|
125
|
+
function runTaskKey(run) {
|
|
126
|
+
return deriveTaskKey(run.contextSnapshot, null);
|
|
127
|
+
}
|
|
128
|
+
function isSameTaskScope(left, right) {
|
|
129
|
+
return (left ?? null) === (right ?? null);
|
|
130
|
+
}
|
|
131
|
+
function truncateDisplayId(value, max = 128) {
|
|
132
|
+
if (!value)
|
|
133
|
+
return null;
|
|
134
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
135
|
+
}
|
|
136
|
+
function normalizeAgentNameKey(value) {
|
|
137
|
+
if (typeof value !== "string")
|
|
138
|
+
return null;
|
|
139
|
+
const normalized = value.trim().toLowerCase();
|
|
140
|
+
return normalized.length > 0 ? normalized : null;
|
|
141
|
+
}
|
|
142
|
+
const defaultSessionCodec = {
|
|
143
|
+
deserialize(raw) {
|
|
144
|
+
const asObj = parseObject(raw);
|
|
145
|
+
if (Object.keys(asObj).length > 0)
|
|
146
|
+
return asObj;
|
|
147
|
+
const sessionId = readNonEmptyString(raw?.sessionId);
|
|
148
|
+
if (sessionId)
|
|
149
|
+
return { sessionId };
|
|
150
|
+
return null;
|
|
151
|
+
},
|
|
152
|
+
serialize(params) {
|
|
153
|
+
if (!params || Object.keys(params).length === 0)
|
|
154
|
+
return null;
|
|
155
|
+
return params;
|
|
156
|
+
},
|
|
157
|
+
getDisplayId(params) {
|
|
158
|
+
return readNonEmptyString(params?.sessionId);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
function getAdapterSessionCodec(adapterType) {
|
|
162
|
+
const adapter = getServerAdapter(adapterType);
|
|
163
|
+
return adapter.sessionCodec ?? defaultSessionCodec;
|
|
164
|
+
}
|
|
165
|
+
function normalizeSessionParams(params) {
|
|
166
|
+
if (!params)
|
|
167
|
+
return null;
|
|
168
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
169
|
+
}
|
|
170
|
+
function resolveNextSessionState(input) {
|
|
171
|
+
const { codec, adapterResult, previousParams, previousDisplayId, previousLegacySessionId } = input;
|
|
172
|
+
if (adapterResult.clearSession) {
|
|
173
|
+
return {
|
|
174
|
+
params: null,
|
|
175
|
+
displayId: null,
|
|
176
|
+
legacySessionId: null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const explicitParams = adapterResult.sessionParams;
|
|
180
|
+
const hasExplicitParams = adapterResult.sessionParams !== undefined;
|
|
181
|
+
const hasExplicitSessionId = adapterResult.sessionId !== undefined;
|
|
182
|
+
const explicitSessionId = readNonEmptyString(adapterResult.sessionId);
|
|
183
|
+
const hasExplicitDisplay = adapterResult.sessionDisplayId !== undefined;
|
|
184
|
+
const explicitDisplayId = readNonEmptyString(adapterResult.sessionDisplayId);
|
|
185
|
+
const shouldUsePrevious = !hasExplicitParams && !hasExplicitSessionId && !hasExplicitDisplay;
|
|
186
|
+
const candidateParams = hasExplicitParams
|
|
187
|
+
? explicitParams
|
|
188
|
+
: hasExplicitSessionId
|
|
189
|
+
? (explicitSessionId ? { sessionId: explicitSessionId } : null)
|
|
190
|
+
: previousParams;
|
|
191
|
+
const serialized = normalizeSessionParams(codec.serialize(normalizeSessionParams(candidateParams) ?? null));
|
|
192
|
+
const deserialized = normalizeSessionParams(codec.deserialize(serialized));
|
|
193
|
+
const displayId = truncateDisplayId(explicitDisplayId ??
|
|
194
|
+
(codec.getDisplayId ? codec.getDisplayId(deserialized) : null) ??
|
|
195
|
+
readNonEmptyString(deserialized?.sessionId) ??
|
|
196
|
+
(shouldUsePrevious ? previousDisplayId : null) ??
|
|
197
|
+
explicitSessionId ??
|
|
198
|
+
(shouldUsePrevious ? previousLegacySessionId : null));
|
|
199
|
+
const legacySessionId = explicitSessionId ??
|
|
200
|
+
readNonEmptyString(deserialized?.sessionId) ??
|
|
201
|
+
displayId ??
|
|
202
|
+
(shouldUsePrevious ? previousLegacySessionId : null);
|
|
203
|
+
return {
|
|
204
|
+
params: serialized,
|
|
205
|
+
displayId,
|
|
206
|
+
legacySessionId,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export function heartbeatService(db) {
|
|
210
|
+
const runLogStore = getRunLogStore();
|
|
211
|
+
const secretsSvc = secretService(db);
|
|
212
|
+
async function getAgent(agentId) {
|
|
213
|
+
return db
|
|
214
|
+
.select()
|
|
215
|
+
.from(agents)
|
|
216
|
+
.where(eq(agents.id, agentId))
|
|
217
|
+
.then((rows) => rows[0] ?? null);
|
|
218
|
+
}
|
|
219
|
+
async function getRun(runId) {
|
|
220
|
+
return db
|
|
221
|
+
.select()
|
|
222
|
+
.from(heartbeatRuns)
|
|
223
|
+
.where(eq(heartbeatRuns.id, runId))
|
|
224
|
+
.then((rows) => rows[0] ?? null);
|
|
225
|
+
}
|
|
226
|
+
async function getRuntimeState(agentId) {
|
|
227
|
+
return db
|
|
228
|
+
.select()
|
|
229
|
+
.from(agentRuntimeState)
|
|
230
|
+
.where(eq(agentRuntimeState.agentId, agentId))
|
|
231
|
+
.then((rows) => rows[0] ?? null);
|
|
232
|
+
}
|
|
233
|
+
async function getTaskSession(companyId, agentId, adapterType, taskKey) {
|
|
234
|
+
return db
|
|
235
|
+
.select()
|
|
236
|
+
.from(agentTaskSessions)
|
|
237
|
+
.where(and(eq(agentTaskSessions.companyId, companyId), eq(agentTaskSessions.agentId, agentId), eq(agentTaskSessions.adapterType, adapterType), eq(agentTaskSessions.taskKey, taskKey)))
|
|
238
|
+
.then((rows) => rows[0] ?? null);
|
|
239
|
+
}
|
|
240
|
+
async function resolveSessionBeforeForWakeup(agent, taskKey) {
|
|
241
|
+
if (taskKey) {
|
|
242
|
+
const codec = getAdapterSessionCodec(agent.adapterType);
|
|
243
|
+
const existingTaskSession = await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey);
|
|
244
|
+
const parsedParams = normalizeSessionParams(codec.deserialize(existingTaskSession?.sessionParamsJson ?? null));
|
|
245
|
+
return truncateDisplayId(existingTaskSession?.sessionDisplayId ??
|
|
246
|
+
(codec.getDisplayId ? codec.getDisplayId(parsedParams) : null) ??
|
|
247
|
+
readNonEmptyString(parsedParams?.sessionId));
|
|
248
|
+
}
|
|
249
|
+
const runtimeForRun = await getRuntimeState(agent.id);
|
|
250
|
+
return runtimeForRun?.sessionId ?? null;
|
|
251
|
+
}
|
|
252
|
+
async function resolveWorkspaceForRun(agent, context, previousSessionParams, opts) {
|
|
253
|
+
const issueId = readNonEmptyString(context.issueId);
|
|
254
|
+
const contextProjectId = readNonEmptyString(context.projectId);
|
|
255
|
+
const issueProjectId = issueId
|
|
256
|
+
? await db
|
|
257
|
+
.select({ projectId: issues.projectId })
|
|
258
|
+
.from(issues)
|
|
259
|
+
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
|
|
260
|
+
.then((rows) => rows[0]?.projectId ?? null)
|
|
261
|
+
: null;
|
|
262
|
+
const resolvedProjectId = issueProjectId ?? contextProjectId;
|
|
263
|
+
const useProjectWorkspace = opts?.useProjectWorkspace !== false;
|
|
264
|
+
const workspaceProjectId = useProjectWorkspace ? resolvedProjectId : null;
|
|
265
|
+
const projectWorkspaceRows = workspaceProjectId
|
|
266
|
+
? await db
|
|
267
|
+
.select()
|
|
268
|
+
.from(projectWorkspaces)
|
|
269
|
+
.where(and(eq(projectWorkspaces.companyId, agent.companyId), eq(projectWorkspaces.projectId, workspaceProjectId)))
|
|
270
|
+
.orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
|
|
271
|
+
: [];
|
|
272
|
+
const workspaceHints = projectWorkspaceRows.map((workspace) => ({
|
|
273
|
+
workspaceId: workspace.id,
|
|
274
|
+
cwd: readNonEmptyString(workspace.cwd),
|
|
275
|
+
repoUrl: readNonEmptyString(workspace.repoUrl),
|
|
276
|
+
repoRef: readNonEmptyString(workspace.repoRef),
|
|
277
|
+
}));
|
|
278
|
+
if (projectWorkspaceRows.length > 0) {
|
|
279
|
+
for (const workspace of projectWorkspaceRows) {
|
|
280
|
+
const projectCwd = readNonEmptyString(workspace.cwd);
|
|
281
|
+
if (!projectCwd || projectCwd === REPO_ONLY_CWD_SENTINEL) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const projectCwdExists = await fs
|
|
285
|
+
.stat(projectCwd)
|
|
286
|
+
.then((stats) => stats.isDirectory())
|
|
287
|
+
.catch(() => false);
|
|
288
|
+
if (projectCwdExists) {
|
|
289
|
+
return {
|
|
290
|
+
cwd: projectCwd,
|
|
291
|
+
source: "project_primary",
|
|
292
|
+
projectId: resolvedProjectId,
|
|
293
|
+
workspaceId: workspace.id,
|
|
294
|
+
repoUrl: workspace.repoUrl,
|
|
295
|
+
repoRef: workspace.repoRef,
|
|
296
|
+
workspaceHints,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const fallbackCwd = resolveDefaultAgentWorkspaceDir(agent.id);
|
|
301
|
+
await fs.mkdir(fallbackCwd, { recursive: true });
|
|
302
|
+
return {
|
|
303
|
+
cwd: fallbackCwd,
|
|
304
|
+
source: "project_primary",
|
|
305
|
+
projectId: resolvedProjectId,
|
|
306
|
+
workspaceId: projectWorkspaceRows[0]?.id ?? null,
|
|
307
|
+
repoUrl: projectWorkspaceRows[0]?.repoUrl ?? null,
|
|
308
|
+
repoRef: projectWorkspaceRows[0]?.repoRef ?? null,
|
|
309
|
+
workspaceHints,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const sessionCwd = readNonEmptyString(previousSessionParams?.cwd);
|
|
313
|
+
if (sessionCwd) {
|
|
314
|
+
const sessionCwdExists = await fs
|
|
315
|
+
.stat(sessionCwd)
|
|
316
|
+
.then((stats) => stats.isDirectory())
|
|
317
|
+
.catch(() => false);
|
|
318
|
+
if (sessionCwdExists) {
|
|
319
|
+
return {
|
|
320
|
+
cwd: sessionCwd,
|
|
321
|
+
source: "task_session",
|
|
322
|
+
projectId: resolvedProjectId,
|
|
323
|
+
workspaceId: readNonEmptyString(previousSessionParams?.workspaceId),
|
|
324
|
+
repoUrl: readNonEmptyString(previousSessionParams?.repoUrl),
|
|
325
|
+
repoRef: readNonEmptyString(previousSessionParams?.repoRef),
|
|
326
|
+
workspaceHints,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const cwd = resolveDefaultAgentWorkspaceDir(agent.id);
|
|
331
|
+
await fs.mkdir(cwd, { recursive: true });
|
|
332
|
+
return {
|
|
333
|
+
cwd,
|
|
334
|
+
source: "agent_home",
|
|
335
|
+
projectId: resolvedProjectId,
|
|
336
|
+
workspaceId: null,
|
|
337
|
+
repoUrl: null,
|
|
338
|
+
repoRef: null,
|
|
339
|
+
workspaceHints,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async function upsertTaskSession(input) {
|
|
343
|
+
const existing = await getTaskSession(input.companyId, input.agentId, input.adapterType, input.taskKey);
|
|
344
|
+
if (existing) {
|
|
345
|
+
return db
|
|
346
|
+
.update(agentTaskSessions)
|
|
347
|
+
.set({
|
|
348
|
+
sessionParamsJson: input.sessionParamsJson,
|
|
349
|
+
sessionDisplayId: input.sessionDisplayId,
|
|
350
|
+
lastRunId: input.lastRunId,
|
|
351
|
+
lastError: input.lastError,
|
|
352
|
+
updatedAt: new Date(),
|
|
353
|
+
})
|
|
354
|
+
.where(eq(agentTaskSessions.id, existing.id))
|
|
355
|
+
.returning()
|
|
356
|
+
.then((rows) => rows[0] ?? null);
|
|
357
|
+
}
|
|
358
|
+
return db
|
|
359
|
+
.insert(agentTaskSessions)
|
|
360
|
+
.values({
|
|
361
|
+
companyId: input.companyId,
|
|
362
|
+
agentId: input.agentId,
|
|
363
|
+
adapterType: input.adapterType,
|
|
364
|
+
taskKey: input.taskKey,
|
|
365
|
+
sessionParamsJson: input.sessionParamsJson,
|
|
366
|
+
sessionDisplayId: input.sessionDisplayId,
|
|
367
|
+
lastRunId: input.lastRunId,
|
|
368
|
+
lastError: input.lastError,
|
|
369
|
+
})
|
|
370
|
+
.returning()
|
|
371
|
+
.then((rows) => rows[0] ?? null);
|
|
372
|
+
}
|
|
373
|
+
async function clearTaskSessions(companyId, agentId, opts) {
|
|
374
|
+
const conditions = [
|
|
375
|
+
eq(agentTaskSessions.companyId, companyId),
|
|
376
|
+
eq(agentTaskSessions.agentId, agentId),
|
|
377
|
+
];
|
|
378
|
+
if (opts?.taskKey) {
|
|
379
|
+
conditions.push(eq(agentTaskSessions.taskKey, opts.taskKey));
|
|
380
|
+
}
|
|
381
|
+
if (opts?.adapterType) {
|
|
382
|
+
conditions.push(eq(agentTaskSessions.adapterType, opts.adapterType));
|
|
383
|
+
}
|
|
384
|
+
return db
|
|
385
|
+
.delete(agentTaskSessions)
|
|
386
|
+
.where(and(...conditions))
|
|
387
|
+
.returning()
|
|
388
|
+
.then((rows) => rows.length);
|
|
389
|
+
}
|
|
390
|
+
async function ensureRuntimeState(agent) {
|
|
391
|
+
const existing = await getRuntimeState(agent.id);
|
|
392
|
+
if (existing)
|
|
393
|
+
return existing;
|
|
394
|
+
return db
|
|
395
|
+
.insert(agentRuntimeState)
|
|
396
|
+
.values({
|
|
397
|
+
agentId: agent.id,
|
|
398
|
+
companyId: agent.companyId,
|
|
399
|
+
adapterType: agent.adapterType,
|
|
400
|
+
stateJson: {},
|
|
401
|
+
})
|
|
402
|
+
.returning()
|
|
403
|
+
.then((rows) => rows[0]);
|
|
404
|
+
}
|
|
405
|
+
async function setRunStatus(runId, status, patch) {
|
|
406
|
+
const updated = await db
|
|
407
|
+
.update(heartbeatRuns)
|
|
408
|
+
.set({ status, ...patch, updatedAt: new Date() })
|
|
409
|
+
.where(eq(heartbeatRuns.id, runId))
|
|
410
|
+
.returning()
|
|
411
|
+
.then((rows) => rows[0] ?? null);
|
|
412
|
+
if (updated) {
|
|
413
|
+
publishLiveEvent({
|
|
414
|
+
companyId: updated.companyId,
|
|
415
|
+
type: "heartbeat.run.status",
|
|
416
|
+
payload: {
|
|
417
|
+
runId: updated.id,
|
|
418
|
+
agentId: updated.agentId,
|
|
419
|
+
status: updated.status,
|
|
420
|
+
invocationSource: updated.invocationSource,
|
|
421
|
+
triggerDetail: updated.triggerDetail,
|
|
422
|
+
error: updated.error ?? null,
|
|
423
|
+
errorCode: updated.errorCode ?? null,
|
|
424
|
+
startedAt: updated.startedAt ? new Date(updated.startedAt).toISOString() : null,
|
|
425
|
+
finishedAt: updated.finishedAt ? new Date(updated.finishedAt).toISOString() : null,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return updated;
|
|
430
|
+
}
|
|
431
|
+
async function setWakeupStatus(wakeupRequestId, status, patch) {
|
|
432
|
+
if (!wakeupRequestId)
|
|
433
|
+
return;
|
|
434
|
+
await db
|
|
435
|
+
.update(agentWakeupRequests)
|
|
436
|
+
.set({ status, ...patch, updatedAt: new Date() })
|
|
437
|
+
.where(eq(agentWakeupRequests.id, wakeupRequestId));
|
|
438
|
+
}
|
|
439
|
+
async function appendRunEvent(run, seq, event) {
|
|
440
|
+
await db.insert(heartbeatRunEvents).values({
|
|
441
|
+
companyId: run.companyId,
|
|
442
|
+
runId: run.id,
|
|
443
|
+
agentId: run.agentId,
|
|
444
|
+
seq,
|
|
445
|
+
eventType: event.eventType,
|
|
446
|
+
stream: event.stream,
|
|
447
|
+
level: event.level,
|
|
448
|
+
color: event.color,
|
|
449
|
+
message: event.message,
|
|
450
|
+
payload: event.payload,
|
|
451
|
+
});
|
|
452
|
+
publishLiveEvent({
|
|
453
|
+
companyId: run.companyId,
|
|
454
|
+
type: "heartbeat.run.event",
|
|
455
|
+
payload: {
|
|
456
|
+
runId: run.id,
|
|
457
|
+
agentId: run.agentId,
|
|
458
|
+
seq,
|
|
459
|
+
eventType: event.eventType,
|
|
460
|
+
stream: event.stream ?? null,
|
|
461
|
+
level: event.level ?? null,
|
|
462
|
+
color: event.color ?? null,
|
|
463
|
+
message: event.message ?? null,
|
|
464
|
+
payload: event.payload ?? null,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function parseHeartbeatPolicy(agent) {
|
|
469
|
+
const runtimeConfig = parseObject(agent.runtimeConfig);
|
|
470
|
+
const heartbeat = parseObject(runtimeConfig.heartbeat);
|
|
471
|
+
return {
|
|
472
|
+
enabled: asBoolean(heartbeat.enabled, true),
|
|
473
|
+
intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
|
|
474
|
+
wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
|
|
475
|
+
maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async function countRunningRunsForAgent(agentId) {
|
|
479
|
+
const [{ count }] = await db
|
|
480
|
+
.select({ count: sql `count(*)` })
|
|
481
|
+
.from(heartbeatRuns)
|
|
482
|
+
.where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running")));
|
|
483
|
+
return Number(count ?? 0);
|
|
484
|
+
}
|
|
485
|
+
async function claimQueuedRun(run) {
|
|
486
|
+
if (run.status !== "queued")
|
|
487
|
+
return run;
|
|
488
|
+
const claimedAt = new Date();
|
|
489
|
+
const claimed = await db
|
|
490
|
+
.update(heartbeatRuns)
|
|
491
|
+
.set({
|
|
492
|
+
status: "running",
|
|
493
|
+
startedAt: run.startedAt ?? claimedAt,
|
|
494
|
+
updatedAt: claimedAt,
|
|
495
|
+
})
|
|
496
|
+
.where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "queued")))
|
|
497
|
+
.returning()
|
|
498
|
+
.then((rows) => rows[0] ?? null);
|
|
499
|
+
if (!claimed)
|
|
500
|
+
return null;
|
|
501
|
+
publishLiveEvent({
|
|
502
|
+
companyId: claimed.companyId,
|
|
503
|
+
type: "heartbeat.run.status",
|
|
504
|
+
payload: {
|
|
505
|
+
runId: claimed.id,
|
|
506
|
+
agentId: claimed.agentId,
|
|
507
|
+
status: claimed.status,
|
|
508
|
+
invocationSource: claimed.invocationSource,
|
|
509
|
+
triggerDetail: claimed.triggerDetail,
|
|
510
|
+
error: claimed.error ?? null,
|
|
511
|
+
errorCode: claimed.errorCode ?? null,
|
|
512
|
+
startedAt: claimed.startedAt ? new Date(claimed.startedAt).toISOString() : null,
|
|
513
|
+
finishedAt: claimed.finishedAt ? new Date(claimed.finishedAt).toISOString() : null,
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
await setWakeupStatus(claimed.wakeupRequestId, "claimed", { claimedAt });
|
|
517
|
+
return claimed;
|
|
518
|
+
}
|
|
519
|
+
async function finalizeAgentStatus(agentId, outcome) {
|
|
520
|
+
const existing = await getAgent(agentId);
|
|
521
|
+
if (!existing)
|
|
522
|
+
return;
|
|
523
|
+
if (existing.status === "paused" || existing.status === "terminated") {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const runningCount = await countRunningRunsForAgent(agentId);
|
|
527
|
+
const nextStatus = runningCount > 0
|
|
528
|
+
? "running"
|
|
529
|
+
: outcome === "succeeded" || outcome === "cancelled"
|
|
530
|
+
? "idle"
|
|
531
|
+
: "error";
|
|
532
|
+
const updated = await db
|
|
533
|
+
.update(agents)
|
|
534
|
+
.set({
|
|
535
|
+
status: nextStatus,
|
|
536
|
+
lastHeartbeatAt: new Date(),
|
|
537
|
+
updatedAt: new Date(),
|
|
538
|
+
})
|
|
539
|
+
.where(eq(agents.id, agentId))
|
|
540
|
+
.returning()
|
|
541
|
+
.then((rows) => rows[0] ?? null);
|
|
542
|
+
if (updated) {
|
|
543
|
+
publishLiveEvent({
|
|
544
|
+
companyId: updated.companyId,
|
|
545
|
+
type: "agent.status",
|
|
546
|
+
payload: {
|
|
547
|
+
agentId: updated.id,
|
|
548
|
+
status: updated.status,
|
|
549
|
+
lastHeartbeatAt: updated.lastHeartbeatAt
|
|
550
|
+
? new Date(updated.lastHeartbeatAt).toISOString()
|
|
551
|
+
: null,
|
|
552
|
+
outcome,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function reapOrphanedRuns(opts) {
|
|
558
|
+
const staleThresholdMs = opts?.staleThresholdMs ?? 0;
|
|
559
|
+
const now = new Date();
|
|
560
|
+
// Find all runs in "queued" or "running" state
|
|
561
|
+
const activeRuns = await db
|
|
562
|
+
.select()
|
|
563
|
+
.from(heartbeatRuns)
|
|
564
|
+
.where(inArray(heartbeatRuns.status, ["queued", "running"]));
|
|
565
|
+
const reaped = [];
|
|
566
|
+
for (const run of activeRuns) {
|
|
567
|
+
if (runningProcesses.has(run.id))
|
|
568
|
+
continue;
|
|
569
|
+
// Apply staleness threshold to avoid false positives
|
|
570
|
+
if (staleThresholdMs > 0) {
|
|
571
|
+
const refTime = run.updatedAt ? new Date(run.updatedAt).getTime() : 0;
|
|
572
|
+
if (now.getTime() - refTime < staleThresholdMs)
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
await setRunStatus(run.id, "failed", {
|
|
576
|
+
error: "Process lost -- server may have restarted",
|
|
577
|
+
errorCode: "process_lost",
|
|
578
|
+
finishedAt: now,
|
|
579
|
+
});
|
|
580
|
+
await setWakeupStatus(run.wakeupRequestId, "failed", {
|
|
581
|
+
finishedAt: now,
|
|
582
|
+
error: "Process lost -- server may have restarted",
|
|
583
|
+
});
|
|
584
|
+
const updatedRun = await getRun(run.id);
|
|
585
|
+
if (updatedRun) {
|
|
586
|
+
await appendRunEvent(updatedRun, 1, {
|
|
587
|
+
eventType: "lifecycle",
|
|
588
|
+
stream: "system",
|
|
589
|
+
level: "error",
|
|
590
|
+
message: "Process lost -- server may have restarted",
|
|
591
|
+
});
|
|
592
|
+
await releaseIssueExecutionAndPromote(updatedRun);
|
|
593
|
+
}
|
|
594
|
+
await finalizeAgentStatus(run.agentId, "failed");
|
|
595
|
+
await startNextQueuedRunForAgent(run.agentId);
|
|
596
|
+
runningProcesses.delete(run.id);
|
|
597
|
+
reaped.push(run.id);
|
|
598
|
+
}
|
|
599
|
+
if (reaped.length > 0) {
|
|
600
|
+
logger.warn({ reapedCount: reaped.length, runIds: reaped }, "reaped orphaned heartbeat runs");
|
|
601
|
+
}
|
|
602
|
+
return { reaped: reaped.length, runIds: reaped };
|
|
603
|
+
}
|
|
604
|
+
async function updateRuntimeState(agent, run, result, session) {
|
|
605
|
+
await ensureRuntimeState(agent);
|
|
606
|
+
const usage = result.usage;
|
|
607
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
608
|
+
const outputTokens = usage?.outputTokens ?? 0;
|
|
609
|
+
const cachedInputTokens = usage?.cachedInputTokens ?? 0;
|
|
610
|
+
const additionalCostCents = Math.max(0, Math.round((result.costUsd ?? 0) * 100));
|
|
611
|
+
const hasTokenUsage = inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0;
|
|
612
|
+
await db
|
|
613
|
+
.update(agentRuntimeState)
|
|
614
|
+
.set({
|
|
615
|
+
adapterType: agent.adapterType,
|
|
616
|
+
sessionId: session.legacySessionId,
|
|
617
|
+
lastRunId: run.id,
|
|
618
|
+
lastRunStatus: run.status,
|
|
619
|
+
lastError: result.errorMessage ?? null,
|
|
620
|
+
totalInputTokens: sql `${agentRuntimeState.totalInputTokens} + ${inputTokens}`,
|
|
621
|
+
totalOutputTokens: sql `${agentRuntimeState.totalOutputTokens} + ${outputTokens}`,
|
|
622
|
+
totalCachedInputTokens: sql `${agentRuntimeState.totalCachedInputTokens} + ${cachedInputTokens}`,
|
|
623
|
+
totalCostCents: sql `${agentRuntimeState.totalCostCents} + ${additionalCostCents}`,
|
|
624
|
+
updatedAt: new Date(),
|
|
625
|
+
})
|
|
626
|
+
.where(eq(agentRuntimeState.agentId, agent.id));
|
|
627
|
+
if (additionalCostCents > 0 || hasTokenUsage) {
|
|
628
|
+
await db.insert(costEvents).values({
|
|
629
|
+
companyId: agent.companyId,
|
|
630
|
+
agentId: agent.id,
|
|
631
|
+
provider: result.provider ?? "unknown",
|
|
632
|
+
model: result.model ?? "unknown",
|
|
633
|
+
inputTokens,
|
|
634
|
+
outputTokens,
|
|
635
|
+
costCents: additionalCostCents,
|
|
636
|
+
occurredAt: new Date(),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (additionalCostCents > 0) {
|
|
640
|
+
await db
|
|
641
|
+
.update(agents)
|
|
642
|
+
.set({
|
|
643
|
+
spentMonthlyCents: sql `${agents.spentMonthlyCents} + ${additionalCostCents}`,
|
|
644
|
+
updatedAt: new Date(),
|
|
645
|
+
})
|
|
646
|
+
.where(eq(agents.id, agent.id));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function startNextQueuedRunForAgent(agentId) {
|
|
650
|
+
return withAgentStartLock(agentId, async () => {
|
|
651
|
+
const agent = await getAgent(agentId);
|
|
652
|
+
if (!agent)
|
|
653
|
+
return [];
|
|
654
|
+
const policy = parseHeartbeatPolicy(agent);
|
|
655
|
+
const runningCount = await countRunningRunsForAgent(agentId);
|
|
656
|
+
const availableSlots = Math.max(0, policy.maxConcurrentRuns - runningCount);
|
|
657
|
+
if (availableSlots <= 0)
|
|
658
|
+
return [];
|
|
659
|
+
const queuedRuns = await db
|
|
660
|
+
.select()
|
|
661
|
+
.from(heartbeatRuns)
|
|
662
|
+
.where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "queued")))
|
|
663
|
+
.orderBy(asc(heartbeatRuns.createdAt))
|
|
664
|
+
.limit(availableSlots);
|
|
665
|
+
if (queuedRuns.length === 0)
|
|
666
|
+
return [];
|
|
667
|
+
const claimedRuns = [];
|
|
668
|
+
for (const queuedRun of queuedRuns) {
|
|
669
|
+
const claimed = await claimQueuedRun(queuedRun);
|
|
670
|
+
if (claimed)
|
|
671
|
+
claimedRuns.push(claimed);
|
|
672
|
+
}
|
|
673
|
+
if (claimedRuns.length === 0)
|
|
674
|
+
return [];
|
|
675
|
+
for (const claimedRun of claimedRuns) {
|
|
676
|
+
void executeRun(claimedRun.id).catch((err) => {
|
|
677
|
+
logger.error({ err, runId: claimedRun.id }, "queued heartbeat execution failed");
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return claimedRuns;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
async function executeRun(runId) {
|
|
684
|
+
let run = await getRun(runId);
|
|
685
|
+
if (!run)
|
|
686
|
+
return;
|
|
687
|
+
if (run.status !== "queued" && run.status !== "running")
|
|
688
|
+
return;
|
|
689
|
+
if (run.status === "queued") {
|
|
690
|
+
const claimed = await claimQueuedRun(run);
|
|
691
|
+
if (!claimed) {
|
|
692
|
+
// Another worker has already claimed or finalized this run.
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
run = claimed;
|
|
696
|
+
}
|
|
697
|
+
const agent = await getAgent(run.agentId);
|
|
698
|
+
if (!agent) {
|
|
699
|
+
await setRunStatus(runId, "failed", {
|
|
700
|
+
error: "Agent not found",
|
|
701
|
+
errorCode: "agent_not_found",
|
|
702
|
+
finishedAt: new Date(),
|
|
703
|
+
});
|
|
704
|
+
await setWakeupStatus(run.wakeupRequestId, "failed", {
|
|
705
|
+
finishedAt: new Date(),
|
|
706
|
+
error: "Agent not found",
|
|
707
|
+
});
|
|
708
|
+
const failedRun = await getRun(runId);
|
|
709
|
+
if (failedRun)
|
|
710
|
+
await releaseIssueExecutionAndPromote(failedRun);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const runtime = await ensureRuntimeState(agent);
|
|
714
|
+
const context = parseObject(run.contextSnapshot);
|
|
715
|
+
const taskKey = deriveTaskKey(context, null);
|
|
716
|
+
const sessionCodec = getAdapterSessionCodec(agent.adapterType);
|
|
717
|
+
const issueId = readNonEmptyString(context.issueId);
|
|
718
|
+
const issueAssigneeConfig = issueId
|
|
719
|
+
? await db
|
|
720
|
+
.select({
|
|
721
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
722
|
+
assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
|
|
723
|
+
})
|
|
724
|
+
.from(issues)
|
|
725
|
+
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
|
|
726
|
+
.then((rows) => rows[0] ?? null)
|
|
727
|
+
: null;
|
|
728
|
+
const issueAssigneeOverrides = issueAssigneeConfig && issueAssigneeConfig.assigneeAgentId === agent.id
|
|
729
|
+
? parseIssueAssigneeAdapterOverrides(issueAssigneeConfig.assigneeAdapterOverrides)
|
|
730
|
+
: null;
|
|
731
|
+
const taskSession = taskKey
|
|
732
|
+
? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
|
|
733
|
+
: null;
|
|
734
|
+
const previousSessionParams = normalizeSessionParams(sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null));
|
|
735
|
+
const resolvedWorkspace = await resolveWorkspaceForRun(agent, context, previousSessionParams, { useProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null });
|
|
736
|
+
context.paperclipWorkspace = {
|
|
737
|
+
cwd: resolvedWorkspace.cwd,
|
|
738
|
+
source: resolvedWorkspace.source,
|
|
739
|
+
projectId: resolvedWorkspace.projectId,
|
|
740
|
+
workspaceId: resolvedWorkspace.workspaceId,
|
|
741
|
+
repoUrl: resolvedWorkspace.repoUrl,
|
|
742
|
+
repoRef: resolvedWorkspace.repoRef,
|
|
743
|
+
};
|
|
744
|
+
context.paperclipWorkspaces = resolvedWorkspace.workspaceHints;
|
|
745
|
+
if (resolvedWorkspace.projectId && !readNonEmptyString(context.projectId)) {
|
|
746
|
+
context.projectId = resolvedWorkspace.projectId;
|
|
747
|
+
}
|
|
748
|
+
const runtimeSessionFallback = taskKey ? null : runtime.sessionId;
|
|
749
|
+
const previousSessionDisplayId = truncateDisplayId(taskSession?.sessionDisplayId ??
|
|
750
|
+
(sessionCodec.getDisplayId ? sessionCodec.getDisplayId(previousSessionParams) : null) ??
|
|
751
|
+
readNonEmptyString(previousSessionParams?.sessionId) ??
|
|
752
|
+
runtimeSessionFallback);
|
|
753
|
+
const runtimeForAdapter = {
|
|
754
|
+
sessionId: readNonEmptyString(previousSessionParams?.sessionId) ?? runtimeSessionFallback,
|
|
755
|
+
sessionParams: previousSessionParams,
|
|
756
|
+
sessionDisplayId: previousSessionDisplayId,
|
|
757
|
+
taskKey,
|
|
758
|
+
};
|
|
759
|
+
let seq = 1;
|
|
760
|
+
let handle = null;
|
|
761
|
+
let stdoutExcerpt = "";
|
|
762
|
+
let stderrExcerpt = "";
|
|
763
|
+
try {
|
|
764
|
+
const startedAt = run.startedAt ?? new Date();
|
|
765
|
+
const runningWithSession = await db
|
|
766
|
+
.update(heartbeatRuns)
|
|
767
|
+
.set({
|
|
768
|
+
startedAt,
|
|
769
|
+
sessionIdBefore: runtimeForAdapter.sessionDisplayId ?? runtimeForAdapter.sessionId,
|
|
770
|
+
updatedAt: new Date(),
|
|
771
|
+
})
|
|
772
|
+
.where(eq(heartbeatRuns.id, run.id))
|
|
773
|
+
.returning()
|
|
774
|
+
.then((rows) => rows[0] ?? null);
|
|
775
|
+
if (runningWithSession)
|
|
776
|
+
run = runningWithSession;
|
|
777
|
+
const runningAgent = await db
|
|
778
|
+
.update(agents)
|
|
779
|
+
.set({ status: "running", updatedAt: new Date() })
|
|
780
|
+
.where(eq(agents.id, agent.id))
|
|
781
|
+
.returning()
|
|
782
|
+
.then((rows) => rows[0] ?? null);
|
|
783
|
+
if (runningAgent) {
|
|
784
|
+
publishLiveEvent({
|
|
785
|
+
companyId: runningAgent.companyId,
|
|
786
|
+
type: "agent.status",
|
|
787
|
+
payload: {
|
|
788
|
+
agentId: runningAgent.id,
|
|
789
|
+
status: runningAgent.status,
|
|
790
|
+
outcome: "running",
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
const currentRun = run;
|
|
795
|
+
await appendRunEvent(currentRun, seq++, {
|
|
796
|
+
eventType: "lifecycle",
|
|
797
|
+
stream: "system",
|
|
798
|
+
level: "info",
|
|
799
|
+
message: "run started",
|
|
800
|
+
});
|
|
801
|
+
handle = await runLogStore.begin({
|
|
802
|
+
companyId: run.companyId,
|
|
803
|
+
agentId: run.agentId,
|
|
804
|
+
runId,
|
|
805
|
+
});
|
|
806
|
+
await db
|
|
807
|
+
.update(heartbeatRuns)
|
|
808
|
+
.set({
|
|
809
|
+
logStore: handle.store,
|
|
810
|
+
logRef: handle.logRef,
|
|
811
|
+
updatedAt: new Date(),
|
|
812
|
+
})
|
|
813
|
+
.where(eq(heartbeatRuns.id, runId));
|
|
814
|
+
const onLog = async (stream, chunk) => {
|
|
815
|
+
if (stream === "stdout")
|
|
816
|
+
stdoutExcerpt = appendExcerpt(stdoutExcerpt, chunk);
|
|
817
|
+
if (stream === "stderr")
|
|
818
|
+
stderrExcerpt = appendExcerpt(stderrExcerpt, chunk);
|
|
819
|
+
if (handle) {
|
|
820
|
+
await runLogStore.append(handle, {
|
|
821
|
+
stream,
|
|
822
|
+
chunk,
|
|
823
|
+
ts: new Date().toISOString(),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
const payloadChunk = chunk.length > MAX_LIVE_LOG_CHUNK_BYTES
|
|
827
|
+
? chunk.slice(chunk.length - MAX_LIVE_LOG_CHUNK_BYTES)
|
|
828
|
+
: chunk;
|
|
829
|
+
publishLiveEvent({
|
|
830
|
+
companyId: run.companyId,
|
|
831
|
+
type: "heartbeat.run.log",
|
|
832
|
+
payload: {
|
|
833
|
+
runId: run.id,
|
|
834
|
+
agentId: run.agentId,
|
|
835
|
+
stream,
|
|
836
|
+
chunk: payloadChunk,
|
|
837
|
+
truncated: payloadChunk.length !== chunk.length,
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
const config = parseObject(agent.adapterConfig);
|
|
842
|
+
const mergedConfig = issueAssigneeOverrides?.adapterConfig
|
|
843
|
+
? { ...config, ...issueAssigneeOverrides.adapterConfig }
|
|
844
|
+
: config;
|
|
845
|
+
const resolvedConfig = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, mergedConfig);
|
|
846
|
+
const onAdapterMeta = async (meta) => {
|
|
847
|
+
await appendRunEvent(currentRun, seq++, {
|
|
848
|
+
eventType: "adapter.invoke",
|
|
849
|
+
stream: "system",
|
|
850
|
+
level: "info",
|
|
851
|
+
message: "adapter invocation",
|
|
852
|
+
payload: meta,
|
|
853
|
+
});
|
|
854
|
+
};
|
|
855
|
+
const adapter = getServerAdapter(agent.adapterType);
|
|
856
|
+
const authToken = adapter.supportsLocalAgentJwt
|
|
857
|
+
? createLocalAgentJwt(agent.id, agent.companyId, agent.adapterType, run.id)
|
|
858
|
+
: null;
|
|
859
|
+
if (adapter.supportsLocalAgentJwt && !authToken) {
|
|
860
|
+
logger.warn({
|
|
861
|
+
companyId: agent.companyId,
|
|
862
|
+
agentId: agent.id,
|
|
863
|
+
runId: run.id,
|
|
864
|
+
adapterType: agent.adapterType,
|
|
865
|
+
}, "local agent jwt secret missing or invalid; running without injected PAPERCLIP_API_KEY");
|
|
866
|
+
}
|
|
867
|
+
const adapterResult = await adapter.execute({
|
|
868
|
+
runId: run.id,
|
|
869
|
+
agent,
|
|
870
|
+
runtime: runtimeForAdapter,
|
|
871
|
+
config: resolvedConfig,
|
|
872
|
+
context,
|
|
873
|
+
onLog,
|
|
874
|
+
onMeta: onAdapterMeta,
|
|
875
|
+
authToken: authToken ?? undefined,
|
|
876
|
+
});
|
|
877
|
+
const nextSessionState = resolveNextSessionState({
|
|
878
|
+
codec: sessionCodec,
|
|
879
|
+
adapterResult,
|
|
880
|
+
previousParams: previousSessionParams,
|
|
881
|
+
previousDisplayId: runtimeForAdapter.sessionDisplayId,
|
|
882
|
+
previousLegacySessionId: runtimeForAdapter.sessionId,
|
|
883
|
+
});
|
|
884
|
+
let outcome;
|
|
885
|
+
const latestRun = await getRun(run.id);
|
|
886
|
+
if (latestRun?.status === "cancelled") {
|
|
887
|
+
outcome = "cancelled";
|
|
888
|
+
}
|
|
889
|
+
else if (adapterResult.timedOut) {
|
|
890
|
+
outcome = "timed_out";
|
|
891
|
+
}
|
|
892
|
+
else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) {
|
|
893
|
+
outcome = "succeeded";
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
outcome = "failed";
|
|
897
|
+
}
|
|
898
|
+
let logSummary = null;
|
|
899
|
+
if (handle) {
|
|
900
|
+
logSummary = await runLogStore.finalize(handle);
|
|
901
|
+
}
|
|
902
|
+
const status = outcome === "succeeded"
|
|
903
|
+
? "succeeded"
|
|
904
|
+
: outcome === "cancelled"
|
|
905
|
+
? "cancelled"
|
|
906
|
+
: outcome === "timed_out"
|
|
907
|
+
? "timed_out"
|
|
908
|
+
: "failed";
|
|
909
|
+
const usageJson = adapterResult.usage || adapterResult.costUsd != null
|
|
910
|
+
? {
|
|
911
|
+
...(adapterResult.usage ?? {}),
|
|
912
|
+
...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}),
|
|
913
|
+
...(adapterResult.billingType ? { billingType: adapterResult.billingType } : {}),
|
|
914
|
+
}
|
|
915
|
+
: null;
|
|
916
|
+
await setRunStatus(run.id, status, {
|
|
917
|
+
finishedAt: new Date(),
|
|
918
|
+
error: outcome === "succeeded"
|
|
919
|
+
? null
|
|
920
|
+
: adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed"),
|
|
921
|
+
errorCode: outcome === "timed_out"
|
|
922
|
+
? "timeout"
|
|
923
|
+
: outcome === "cancelled"
|
|
924
|
+
? "cancelled"
|
|
925
|
+
: outcome === "failed"
|
|
926
|
+
? (adapterResult.errorCode ?? "adapter_failed")
|
|
927
|
+
: null,
|
|
928
|
+
exitCode: adapterResult.exitCode,
|
|
929
|
+
signal: adapterResult.signal,
|
|
930
|
+
usageJson,
|
|
931
|
+
resultJson: adapterResult.resultJson ?? null,
|
|
932
|
+
sessionIdAfter: nextSessionState.displayId ?? nextSessionState.legacySessionId,
|
|
933
|
+
stdoutExcerpt,
|
|
934
|
+
stderrExcerpt,
|
|
935
|
+
logBytes: logSummary?.bytes,
|
|
936
|
+
logSha256: logSummary?.sha256,
|
|
937
|
+
logCompressed: logSummary?.compressed ?? false,
|
|
938
|
+
});
|
|
939
|
+
await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded" ? "completed" : status, {
|
|
940
|
+
finishedAt: new Date(),
|
|
941
|
+
error: adapterResult.errorMessage ?? null,
|
|
942
|
+
});
|
|
943
|
+
const finalizedRun = await getRun(run.id);
|
|
944
|
+
if (finalizedRun) {
|
|
945
|
+
await appendRunEvent(finalizedRun, seq++, {
|
|
946
|
+
eventType: "lifecycle",
|
|
947
|
+
stream: "system",
|
|
948
|
+
level: outcome === "succeeded" ? "info" : "error",
|
|
949
|
+
message: `run ${outcome}`,
|
|
950
|
+
payload: {
|
|
951
|
+
status,
|
|
952
|
+
exitCode: adapterResult.exitCode,
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
await releaseIssueExecutionAndPromote(finalizedRun);
|
|
956
|
+
}
|
|
957
|
+
if (finalizedRun) {
|
|
958
|
+
await updateRuntimeState(agent, finalizedRun, adapterResult, {
|
|
959
|
+
legacySessionId: nextSessionState.legacySessionId,
|
|
960
|
+
});
|
|
961
|
+
if (taskKey) {
|
|
962
|
+
if (adapterResult.clearSession || (!nextSessionState.params && !nextSessionState.displayId)) {
|
|
963
|
+
await clearTaskSessions(agent.companyId, agent.id, {
|
|
964
|
+
taskKey,
|
|
965
|
+
adapterType: agent.adapterType,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
await upsertTaskSession({
|
|
970
|
+
companyId: agent.companyId,
|
|
971
|
+
agentId: agent.id,
|
|
972
|
+
adapterType: agent.adapterType,
|
|
973
|
+
taskKey,
|
|
974
|
+
sessionParamsJson: nextSessionState.params,
|
|
975
|
+
sessionDisplayId: nextSessionState.displayId,
|
|
976
|
+
lastRunId: finalizedRun.id,
|
|
977
|
+
lastError: outcome === "succeeded" ? null : (adapterResult.errorMessage ?? "run_failed"),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
await finalizeAgentStatus(agent.id, outcome);
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
const message = err instanceof Error ? err.message : "Unknown adapter failure";
|
|
986
|
+
logger.error({ err, runId }, "heartbeat execution failed");
|
|
987
|
+
let logSummary = null;
|
|
988
|
+
if (handle) {
|
|
989
|
+
try {
|
|
990
|
+
logSummary = await runLogStore.finalize(handle);
|
|
991
|
+
}
|
|
992
|
+
catch (finalizeErr) {
|
|
993
|
+
logger.warn({ err: finalizeErr, runId }, "failed to finalize run log after error");
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const failedRun = await setRunStatus(run.id, "failed", {
|
|
997
|
+
error: message,
|
|
998
|
+
errorCode: "adapter_failed",
|
|
999
|
+
finishedAt: new Date(),
|
|
1000
|
+
stdoutExcerpt,
|
|
1001
|
+
stderrExcerpt,
|
|
1002
|
+
logBytes: logSummary?.bytes,
|
|
1003
|
+
logSha256: logSummary?.sha256,
|
|
1004
|
+
logCompressed: logSummary?.compressed ?? false,
|
|
1005
|
+
});
|
|
1006
|
+
await setWakeupStatus(run.wakeupRequestId, "failed", {
|
|
1007
|
+
finishedAt: new Date(),
|
|
1008
|
+
error: message,
|
|
1009
|
+
});
|
|
1010
|
+
if (failedRun) {
|
|
1011
|
+
await appendRunEvent(failedRun, seq++, {
|
|
1012
|
+
eventType: "error",
|
|
1013
|
+
stream: "system",
|
|
1014
|
+
level: "error",
|
|
1015
|
+
message,
|
|
1016
|
+
});
|
|
1017
|
+
await releaseIssueExecutionAndPromote(failedRun);
|
|
1018
|
+
await updateRuntimeState(agent, failedRun, {
|
|
1019
|
+
exitCode: null,
|
|
1020
|
+
signal: null,
|
|
1021
|
+
timedOut: false,
|
|
1022
|
+
errorMessage: message,
|
|
1023
|
+
}, {
|
|
1024
|
+
legacySessionId: runtimeForAdapter.sessionId,
|
|
1025
|
+
});
|
|
1026
|
+
if (taskKey && (previousSessionParams || previousSessionDisplayId || taskSession)) {
|
|
1027
|
+
await upsertTaskSession({
|
|
1028
|
+
companyId: agent.companyId,
|
|
1029
|
+
agentId: agent.id,
|
|
1030
|
+
adapterType: agent.adapterType,
|
|
1031
|
+
taskKey,
|
|
1032
|
+
sessionParamsJson: previousSessionParams,
|
|
1033
|
+
sessionDisplayId: previousSessionDisplayId,
|
|
1034
|
+
lastRunId: failedRun.id,
|
|
1035
|
+
lastError: message,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
await finalizeAgentStatus(agent.id, "failed");
|
|
1040
|
+
}
|
|
1041
|
+
finally {
|
|
1042
|
+
await startNextQueuedRunForAgent(agent.id);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function releaseIssueExecutionAndPromote(run) {
|
|
1046
|
+
const promotedRun = await db.transaction(async (tx) => {
|
|
1047
|
+
await tx.execute(sql `select id from issues where company_id = ${run.companyId} and execution_run_id = ${run.id} for update`);
|
|
1048
|
+
const issue = await tx
|
|
1049
|
+
.select({
|
|
1050
|
+
id: issues.id,
|
|
1051
|
+
companyId: issues.companyId,
|
|
1052
|
+
})
|
|
1053
|
+
.from(issues)
|
|
1054
|
+
.where(and(eq(issues.companyId, run.companyId), eq(issues.executionRunId, run.id)))
|
|
1055
|
+
.then((rows) => rows[0] ?? null);
|
|
1056
|
+
if (!issue)
|
|
1057
|
+
return;
|
|
1058
|
+
await tx
|
|
1059
|
+
.update(issues)
|
|
1060
|
+
.set({
|
|
1061
|
+
executionRunId: null,
|
|
1062
|
+
executionAgentNameKey: null,
|
|
1063
|
+
executionLockedAt: null,
|
|
1064
|
+
updatedAt: new Date(),
|
|
1065
|
+
})
|
|
1066
|
+
.where(eq(issues.id, issue.id));
|
|
1067
|
+
while (true) {
|
|
1068
|
+
const deferred = await tx
|
|
1069
|
+
.select()
|
|
1070
|
+
.from(agentWakeupRequests)
|
|
1071
|
+
.where(and(eq(agentWakeupRequests.companyId, issue.companyId), eq(agentWakeupRequests.status, "deferred_issue_execution"), sql `${agentWakeupRequests.payload} ->> 'issueId' = ${issue.id}`))
|
|
1072
|
+
.orderBy(asc(agentWakeupRequests.requestedAt))
|
|
1073
|
+
.limit(1)
|
|
1074
|
+
.then((rows) => rows[0] ?? null);
|
|
1075
|
+
if (!deferred)
|
|
1076
|
+
return null;
|
|
1077
|
+
const deferredAgent = await tx
|
|
1078
|
+
.select()
|
|
1079
|
+
.from(agents)
|
|
1080
|
+
.where(eq(agents.id, deferred.agentId))
|
|
1081
|
+
.then((rows) => rows[0] ?? null);
|
|
1082
|
+
if (!deferredAgent ||
|
|
1083
|
+
deferredAgent.companyId !== issue.companyId ||
|
|
1084
|
+
deferredAgent.status === "paused" ||
|
|
1085
|
+
deferredAgent.status === "terminated" ||
|
|
1086
|
+
deferredAgent.status === "pending_approval") {
|
|
1087
|
+
await tx
|
|
1088
|
+
.update(agentWakeupRequests)
|
|
1089
|
+
.set({
|
|
1090
|
+
status: "failed",
|
|
1091
|
+
finishedAt: new Date(),
|
|
1092
|
+
error: "Deferred wake could not be promoted: agent is not invokable",
|
|
1093
|
+
updatedAt: new Date(),
|
|
1094
|
+
})
|
|
1095
|
+
.where(eq(agentWakeupRequests.id, deferred.id));
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const deferredPayload = parseObject(deferred.payload);
|
|
1099
|
+
const deferredContextSeed = parseObject(deferredPayload[DEFERRED_WAKE_CONTEXT_KEY]);
|
|
1100
|
+
const promotedContextSeed = { ...deferredContextSeed };
|
|
1101
|
+
const promotedReason = readNonEmptyString(deferred.reason) ?? "issue_execution_promoted";
|
|
1102
|
+
const promotedSource = readNonEmptyString(deferred.source) ?? "automation";
|
|
1103
|
+
const promotedTriggerDetail = readNonEmptyString(deferred.triggerDetail) ?? null;
|
|
1104
|
+
const promotedPayload = deferredPayload;
|
|
1105
|
+
delete promotedPayload[DEFERRED_WAKE_CONTEXT_KEY];
|
|
1106
|
+
const { contextSnapshot: promotedContextSnapshot, taskKey: promotedTaskKey, } = enrichWakeContextSnapshot({
|
|
1107
|
+
contextSnapshot: promotedContextSeed,
|
|
1108
|
+
reason: promotedReason,
|
|
1109
|
+
source: promotedSource,
|
|
1110
|
+
triggerDetail: promotedTriggerDetail,
|
|
1111
|
+
payload: promotedPayload,
|
|
1112
|
+
});
|
|
1113
|
+
const sessionBefore = await resolveSessionBeforeForWakeup(deferredAgent, promotedTaskKey);
|
|
1114
|
+
const now = new Date();
|
|
1115
|
+
const newRun = await tx
|
|
1116
|
+
.insert(heartbeatRuns)
|
|
1117
|
+
.values({
|
|
1118
|
+
companyId: deferredAgent.companyId,
|
|
1119
|
+
agentId: deferredAgent.id,
|
|
1120
|
+
invocationSource: promotedSource,
|
|
1121
|
+
triggerDetail: promotedTriggerDetail,
|
|
1122
|
+
status: "queued",
|
|
1123
|
+
wakeupRequestId: deferred.id,
|
|
1124
|
+
contextSnapshot: promotedContextSnapshot,
|
|
1125
|
+
sessionIdBefore: sessionBefore,
|
|
1126
|
+
})
|
|
1127
|
+
.returning()
|
|
1128
|
+
.then((rows) => rows[0]);
|
|
1129
|
+
await tx
|
|
1130
|
+
.update(agentWakeupRequests)
|
|
1131
|
+
.set({
|
|
1132
|
+
status: "queued",
|
|
1133
|
+
reason: "issue_execution_promoted",
|
|
1134
|
+
runId: newRun.id,
|
|
1135
|
+
claimedAt: null,
|
|
1136
|
+
finishedAt: null,
|
|
1137
|
+
error: null,
|
|
1138
|
+
updatedAt: now,
|
|
1139
|
+
})
|
|
1140
|
+
.where(eq(agentWakeupRequests.id, deferred.id));
|
|
1141
|
+
await tx
|
|
1142
|
+
.update(issues)
|
|
1143
|
+
.set({
|
|
1144
|
+
executionRunId: newRun.id,
|
|
1145
|
+
executionAgentNameKey: normalizeAgentNameKey(deferredAgent.name),
|
|
1146
|
+
executionLockedAt: now,
|
|
1147
|
+
updatedAt: now,
|
|
1148
|
+
})
|
|
1149
|
+
.where(eq(issues.id, issue.id));
|
|
1150
|
+
return newRun;
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
if (!promotedRun)
|
|
1154
|
+
return;
|
|
1155
|
+
publishLiveEvent({
|
|
1156
|
+
companyId: promotedRun.companyId,
|
|
1157
|
+
type: "heartbeat.run.queued",
|
|
1158
|
+
payload: {
|
|
1159
|
+
runId: promotedRun.id,
|
|
1160
|
+
agentId: promotedRun.agentId,
|
|
1161
|
+
invocationSource: promotedRun.invocationSource,
|
|
1162
|
+
triggerDetail: promotedRun.triggerDetail,
|
|
1163
|
+
wakeupRequestId: promotedRun.wakeupRequestId,
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
await startNextQueuedRunForAgent(promotedRun.agentId);
|
|
1167
|
+
}
|
|
1168
|
+
async function enqueueWakeup(agentId, opts = {}) {
|
|
1169
|
+
const source = opts.source ?? "on_demand";
|
|
1170
|
+
const triggerDetail = opts.triggerDetail ?? null;
|
|
1171
|
+
const contextSnapshot = { ...(opts.contextSnapshot ?? {}) };
|
|
1172
|
+
const reason = opts.reason ?? null;
|
|
1173
|
+
const payload = opts.payload ?? null;
|
|
1174
|
+
const { contextSnapshot: enrichedContextSnapshot, issueIdFromPayload, taskKey, wakeCommentId, } = enrichWakeContextSnapshot({
|
|
1175
|
+
contextSnapshot,
|
|
1176
|
+
reason,
|
|
1177
|
+
source,
|
|
1178
|
+
triggerDetail,
|
|
1179
|
+
payload,
|
|
1180
|
+
});
|
|
1181
|
+
const issueId = readNonEmptyString(enrichedContextSnapshot.issueId) ?? issueIdFromPayload;
|
|
1182
|
+
const agent = await getAgent(agentId);
|
|
1183
|
+
if (!agent)
|
|
1184
|
+
throw notFound("Agent not found");
|
|
1185
|
+
if (agent.status === "paused" ||
|
|
1186
|
+
agent.status === "terminated" ||
|
|
1187
|
+
agent.status === "pending_approval") {
|
|
1188
|
+
throw conflict("Agent is not invokable in its current state", { status: agent.status });
|
|
1189
|
+
}
|
|
1190
|
+
const policy = parseHeartbeatPolicy(agent);
|
|
1191
|
+
const writeSkippedRequest = async (reason) => {
|
|
1192
|
+
await db.insert(agentWakeupRequests).values({
|
|
1193
|
+
companyId: agent.companyId,
|
|
1194
|
+
agentId,
|
|
1195
|
+
source,
|
|
1196
|
+
triggerDetail,
|
|
1197
|
+
reason,
|
|
1198
|
+
payload,
|
|
1199
|
+
status: "skipped",
|
|
1200
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1201
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1202
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1203
|
+
finishedAt: new Date(),
|
|
1204
|
+
});
|
|
1205
|
+
};
|
|
1206
|
+
if (source === "timer" && !policy.enabled) {
|
|
1207
|
+
await writeSkippedRequest("heartbeat.disabled");
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
if (source !== "timer" && !policy.wakeOnDemand) {
|
|
1211
|
+
await writeSkippedRequest("heartbeat.wakeOnDemand.disabled");
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const bypassIssueExecutionLock = reason === "issue_comment_mentioned" ||
|
|
1215
|
+
readNonEmptyString(enrichedContextSnapshot.wakeReason) === "issue_comment_mentioned";
|
|
1216
|
+
if (issueId && !bypassIssueExecutionLock) {
|
|
1217
|
+
const agentNameKey = normalizeAgentNameKey(agent.name);
|
|
1218
|
+
const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
|
|
1219
|
+
const outcome = await db.transaction(async (tx) => {
|
|
1220
|
+
await tx.execute(sql `select id from issues where id = ${issueId} and company_id = ${agent.companyId} for update`);
|
|
1221
|
+
const issue = await tx
|
|
1222
|
+
.select({
|
|
1223
|
+
id: issues.id,
|
|
1224
|
+
companyId: issues.companyId,
|
|
1225
|
+
executionRunId: issues.executionRunId,
|
|
1226
|
+
executionAgentNameKey: issues.executionAgentNameKey,
|
|
1227
|
+
})
|
|
1228
|
+
.from(issues)
|
|
1229
|
+
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
|
|
1230
|
+
.then((rows) => rows[0] ?? null);
|
|
1231
|
+
if (!issue) {
|
|
1232
|
+
await tx.insert(agentWakeupRequests).values({
|
|
1233
|
+
companyId: agent.companyId,
|
|
1234
|
+
agentId,
|
|
1235
|
+
source,
|
|
1236
|
+
triggerDetail,
|
|
1237
|
+
reason: "issue_execution_issue_not_found",
|
|
1238
|
+
payload,
|
|
1239
|
+
status: "skipped",
|
|
1240
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1241
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1242
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1243
|
+
finishedAt: new Date(),
|
|
1244
|
+
});
|
|
1245
|
+
return { kind: "skipped" };
|
|
1246
|
+
}
|
|
1247
|
+
let activeExecutionRun = issue.executionRunId
|
|
1248
|
+
? await tx
|
|
1249
|
+
.select()
|
|
1250
|
+
.from(heartbeatRuns)
|
|
1251
|
+
.where(eq(heartbeatRuns.id, issue.executionRunId))
|
|
1252
|
+
.then((rows) => rows[0] ?? null)
|
|
1253
|
+
: null;
|
|
1254
|
+
if (activeExecutionRun && activeExecutionRun.status !== "queued" && activeExecutionRun.status !== "running") {
|
|
1255
|
+
activeExecutionRun = null;
|
|
1256
|
+
}
|
|
1257
|
+
if (!activeExecutionRun && issue.executionRunId) {
|
|
1258
|
+
await tx
|
|
1259
|
+
.update(issues)
|
|
1260
|
+
.set({
|
|
1261
|
+
executionRunId: null,
|
|
1262
|
+
executionAgentNameKey: null,
|
|
1263
|
+
executionLockedAt: null,
|
|
1264
|
+
updatedAt: new Date(),
|
|
1265
|
+
})
|
|
1266
|
+
.where(eq(issues.id, issue.id));
|
|
1267
|
+
}
|
|
1268
|
+
if (!activeExecutionRun) {
|
|
1269
|
+
const legacyRun = await tx
|
|
1270
|
+
.select()
|
|
1271
|
+
.from(heartbeatRuns)
|
|
1272
|
+
.where(and(eq(heartbeatRuns.companyId, issue.companyId), inArray(heartbeatRuns.status, ["queued", "running"]), sql `${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`))
|
|
1273
|
+
.orderBy(sql `case when ${heartbeatRuns.status} = 'running' then 0 else 1 end`, asc(heartbeatRuns.createdAt))
|
|
1274
|
+
.limit(1)
|
|
1275
|
+
.then((rows) => rows[0] ?? null);
|
|
1276
|
+
if (legacyRun) {
|
|
1277
|
+
activeExecutionRun = legacyRun;
|
|
1278
|
+
const legacyAgent = await tx
|
|
1279
|
+
.select({ name: agents.name })
|
|
1280
|
+
.from(agents)
|
|
1281
|
+
.where(eq(agents.id, legacyRun.agentId))
|
|
1282
|
+
.then((rows) => rows[0] ?? null);
|
|
1283
|
+
await tx
|
|
1284
|
+
.update(issues)
|
|
1285
|
+
.set({
|
|
1286
|
+
executionRunId: legacyRun.id,
|
|
1287
|
+
executionAgentNameKey: normalizeAgentNameKey(legacyAgent?.name),
|
|
1288
|
+
executionLockedAt: new Date(),
|
|
1289
|
+
updatedAt: new Date(),
|
|
1290
|
+
})
|
|
1291
|
+
.where(eq(issues.id, issue.id));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (activeExecutionRun) {
|
|
1295
|
+
const executionAgent = await tx
|
|
1296
|
+
.select({ name: agents.name })
|
|
1297
|
+
.from(agents)
|
|
1298
|
+
.where(eq(agents.id, activeExecutionRun.agentId))
|
|
1299
|
+
.then((rows) => rows[0] ?? null);
|
|
1300
|
+
const executionAgentNameKey = normalizeAgentNameKey(issue.executionAgentNameKey) ??
|
|
1301
|
+
normalizeAgentNameKey(executionAgent?.name);
|
|
1302
|
+
const isSameExecutionAgent = Boolean(executionAgentNameKey) && executionAgentNameKey === agentNameKey;
|
|
1303
|
+
const shouldQueueFollowupForCommentWake = Boolean(wakeCommentId) &&
|
|
1304
|
+
activeExecutionRun.status === "running" &&
|
|
1305
|
+
isSameExecutionAgent;
|
|
1306
|
+
if (isSameExecutionAgent && !shouldQueueFollowupForCommentWake) {
|
|
1307
|
+
const mergedContextSnapshot = mergeCoalescedContextSnapshot(activeExecutionRun.contextSnapshot, enrichedContextSnapshot);
|
|
1308
|
+
const mergedRun = await tx
|
|
1309
|
+
.update(heartbeatRuns)
|
|
1310
|
+
.set({
|
|
1311
|
+
contextSnapshot: mergedContextSnapshot,
|
|
1312
|
+
updatedAt: new Date(),
|
|
1313
|
+
})
|
|
1314
|
+
.where(eq(heartbeatRuns.id, activeExecutionRun.id))
|
|
1315
|
+
.returning()
|
|
1316
|
+
.then((rows) => rows[0] ?? activeExecutionRun);
|
|
1317
|
+
await tx.insert(agentWakeupRequests).values({
|
|
1318
|
+
companyId: agent.companyId,
|
|
1319
|
+
agentId,
|
|
1320
|
+
source,
|
|
1321
|
+
triggerDetail,
|
|
1322
|
+
reason: "issue_execution_same_name",
|
|
1323
|
+
payload,
|
|
1324
|
+
status: "coalesced",
|
|
1325
|
+
coalescedCount: 1,
|
|
1326
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1327
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1328
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1329
|
+
runId: mergedRun.id,
|
|
1330
|
+
finishedAt: new Date(),
|
|
1331
|
+
});
|
|
1332
|
+
return { kind: "coalesced", run: mergedRun };
|
|
1333
|
+
}
|
|
1334
|
+
const deferredPayload = {
|
|
1335
|
+
...(payload ?? {}),
|
|
1336
|
+
issueId,
|
|
1337
|
+
[DEFERRED_WAKE_CONTEXT_KEY]: enrichedContextSnapshot,
|
|
1338
|
+
};
|
|
1339
|
+
const existingDeferred = await tx
|
|
1340
|
+
.select()
|
|
1341
|
+
.from(agentWakeupRequests)
|
|
1342
|
+
.where(and(eq(agentWakeupRequests.companyId, agent.companyId), eq(agentWakeupRequests.agentId, agentId), eq(agentWakeupRequests.status, "deferred_issue_execution"), sql `${agentWakeupRequests.payload} ->> 'issueId' = ${issue.id}`))
|
|
1343
|
+
.orderBy(asc(agentWakeupRequests.requestedAt))
|
|
1344
|
+
.limit(1)
|
|
1345
|
+
.then((rows) => rows[0] ?? null);
|
|
1346
|
+
if (existingDeferred) {
|
|
1347
|
+
const existingDeferredPayload = parseObject(existingDeferred.payload);
|
|
1348
|
+
const existingDeferredContext = parseObject(existingDeferredPayload[DEFERRED_WAKE_CONTEXT_KEY]);
|
|
1349
|
+
const mergedDeferredContext = mergeCoalescedContextSnapshot(existingDeferredContext, enrichedContextSnapshot);
|
|
1350
|
+
const mergedDeferredPayload = {
|
|
1351
|
+
...existingDeferredPayload,
|
|
1352
|
+
...(payload ?? {}),
|
|
1353
|
+
issueId,
|
|
1354
|
+
[DEFERRED_WAKE_CONTEXT_KEY]: mergedDeferredContext,
|
|
1355
|
+
};
|
|
1356
|
+
await tx
|
|
1357
|
+
.update(agentWakeupRequests)
|
|
1358
|
+
.set({
|
|
1359
|
+
payload: mergedDeferredPayload,
|
|
1360
|
+
coalescedCount: (existingDeferred.coalescedCount ?? 0) + 1,
|
|
1361
|
+
updatedAt: new Date(),
|
|
1362
|
+
})
|
|
1363
|
+
.where(eq(agentWakeupRequests.id, existingDeferred.id));
|
|
1364
|
+
return { kind: "deferred" };
|
|
1365
|
+
}
|
|
1366
|
+
await tx.insert(agentWakeupRequests).values({
|
|
1367
|
+
companyId: agent.companyId,
|
|
1368
|
+
agentId,
|
|
1369
|
+
source,
|
|
1370
|
+
triggerDetail,
|
|
1371
|
+
reason: "issue_execution_deferred",
|
|
1372
|
+
payload: deferredPayload,
|
|
1373
|
+
status: "deferred_issue_execution",
|
|
1374
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1375
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1376
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1377
|
+
});
|
|
1378
|
+
return { kind: "deferred" };
|
|
1379
|
+
}
|
|
1380
|
+
const wakeupRequest = await tx
|
|
1381
|
+
.insert(agentWakeupRequests)
|
|
1382
|
+
.values({
|
|
1383
|
+
companyId: agent.companyId,
|
|
1384
|
+
agentId,
|
|
1385
|
+
source,
|
|
1386
|
+
triggerDetail,
|
|
1387
|
+
reason,
|
|
1388
|
+
payload,
|
|
1389
|
+
status: "queued",
|
|
1390
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1391
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1392
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1393
|
+
})
|
|
1394
|
+
.returning()
|
|
1395
|
+
.then((rows) => rows[0]);
|
|
1396
|
+
const newRun = await tx
|
|
1397
|
+
.insert(heartbeatRuns)
|
|
1398
|
+
.values({
|
|
1399
|
+
companyId: agent.companyId,
|
|
1400
|
+
agentId,
|
|
1401
|
+
invocationSource: source,
|
|
1402
|
+
triggerDetail,
|
|
1403
|
+
status: "queued",
|
|
1404
|
+
wakeupRequestId: wakeupRequest.id,
|
|
1405
|
+
contextSnapshot: enrichedContextSnapshot,
|
|
1406
|
+
sessionIdBefore: sessionBefore,
|
|
1407
|
+
})
|
|
1408
|
+
.returning()
|
|
1409
|
+
.then((rows) => rows[0]);
|
|
1410
|
+
await tx
|
|
1411
|
+
.update(agentWakeupRequests)
|
|
1412
|
+
.set({
|
|
1413
|
+
runId: newRun.id,
|
|
1414
|
+
updatedAt: new Date(),
|
|
1415
|
+
})
|
|
1416
|
+
.where(eq(agentWakeupRequests.id, wakeupRequest.id));
|
|
1417
|
+
await tx
|
|
1418
|
+
.update(issues)
|
|
1419
|
+
.set({
|
|
1420
|
+
executionRunId: newRun.id,
|
|
1421
|
+
executionAgentNameKey: agentNameKey,
|
|
1422
|
+
executionLockedAt: new Date(),
|
|
1423
|
+
updatedAt: new Date(),
|
|
1424
|
+
})
|
|
1425
|
+
.where(eq(issues.id, issue.id));
|
|
1426
|
+
return { kind: "queued", run: newRun };
|
|
1427
|
+
});
|
|
1428
|
+
if (outcome.kind === "deferred" || outcome.kind === "skipped")
|
|
1429
|
+
return null;
|
|
1430
|
+
if (outcome.kind === "coalesced")
|
|
1431
|
+
return outcome.run;
|
|
1432
|
+
const newRun = outcome.run;
|
|
1433
|
+
publishLiveEvent({
|
|
1434
|
+
companyId: newRun.companyId,
|
|
1435
|
+
type: "heartbeat.run.queued",
|
|
1436
|
+
payload: {
|
|
1437
|
+
runId: newRun.id,
|
|
1438
|
+
agentId: newRun.agentId,
|
|
1439
|
+
invocationSource: newRun.invocationSource,
|
|
1440
|
+
triggerDetail: newRun.triggerDetail,
|
|
1441
|
+
wakeupRequestId: newRun.wakeupRequestId,
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
await startNextQueuedRunForAgent(agent.id);
|
|
1445
|
+
return newRun;
|
|
1446
|
+
}
|
|
1447
|
+
const activeRuns = await db
|
|
1448
|
+
.select()
|
|
1449
|
+
.from(heartbeatRuns)
|
|
1450
|
+
.where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"])))
|
|
1451
|
+
.orderBy(desc(heartbeatRuns.createdAt));
|
|
1452
|
+
const sameScopeQueuedRun = activeRuns.find((candidate) => candidate.status === "queued" && isSameTaskScope(runTaskKey(candidate), taskKey));
|
|
1453
|
+
const sameScopeRunningRun = activeRuns.find((candidate) => candidate.status === "running" && isSameTaskScope(runTaskKey(candidate), taskKey));
|
|
1454
|
+
const shouldQueueFollowupForCommentWake = Boolean(wakeCommentId) && Boolean(sameScopeRunningRun) && !sameScopeQueuedRun;
|
|
1455
|
+
const coalescedTargetRun = sameScopeQueuedRun ??
|
|
1456
|
+
(shouldQueueFollowupForCommentWake ? null : sameScopeRunningRun ?? null);
|
|
1457
|
+
if (coalescedTargetRun) {
|
|
1458
|
+
const mergedContextSnapshot = mergeCoalescedContextSnapshot(coalescedTargetRun.contextSnapshot, contextSnapshot);
|
|
1459
|
+
const mergedRun = await db
|
|
1460
|
+
.update(heartbeatRuns)
|
|
1461
|
+
.set({
|
|
1462
|
+
contextSnapshot: mergedContextSnapshot,
|
|
1463
|
+
updatedAt: new Date(),
|
|
1464
|
+
})
|
|
1465
|
+
.where(eq(heartbeatRuns.id, coalescedTargetRun.id))
|
|
1466
|
+
.returning()
|
|
1467
|
+
.then((rows) => rows[0] ?? coalescedTargetRun);
|
|
1468
|
+
await db.insert(agentWakeupRequests).values({
|
|
1469
|
+
companyId: agent.companyId,
|
|
1470
|
+
agentId,
|
|
1471
|
+
source,
|
|
1472
|
+
triggerDetail,
|
|
1473
|
+
reason,
|
|
1474
|
+
payload,
|
|
1475
|
+
status: "coalesced",
|
|
1476
|
+
coalescedCount: 1,
|
|
1477
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1478
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1479
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1480
|
+
runId: mergedRun.id,
|
|
1481
|
+
finishedAt: new Date(),
|
|
1482
|
+
});
|
|
1483
|
+
return mergedRun;
|
|
1484
|
+
}
|
|
1485
|
+
const wakeupRequest = await db
|
|
1486
|
+
.insert(agentWakeupRequests)
|
|
1487
|
+
.values({
|
|
1488
|
+
companyId: agent.companyId,
|
|
1489
|
+
agentId,
|
|
1490
|
+
source,
|
|
1491
|
+
triggerDetail,
|
|
1492
|
+
reason,
|
|
1493
|
+
payload,
|
|
1494
|
+
status: "queued",
|
|
1495
|
+
requestedByActorType: opts.requestedByActorType ?? null,
|
|
1496
|
+
requestedByActorId: opts.requestedByActorId ?? null,
|
|
1497
|
+
idempotencyKey: opts.idempotencyKey ?? null,
|
|
1498
|
+
})
|
|
1499
|
+
.returning()
|
|
1500
|
+
.then((rows) => rows[0]);
|
|
1501
|
+
const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
|
|
1502
|
+
const newRun = await db
|
|
1503
|
+
.insert(heartbeatRuns)
|
|
1504
|
+
.values({
|
|
1505
|
+
companyId: agent.companyId,
|
|
1506
|
+
agentId,
|
|
1507
|
+
invocationSource: source,
|
|
1508
|
+
triggerDetail,
|
|
1509
|
+
status: "queued",
|
|
1510
|
+
wakeupRequestId: wakeupRequest.id,
|
|
1511
|
+
contextSnapshot: enrichedContextSnapshot,
|
|
1512
|
+
sessionIdBefore: sessionBefore,
|
|
1513
|
+
})
|
|
1514
|
+
.returning()
|
|
1515
|
+
.then((rows) => rows[0]);
|
|
1516
|
+
await db
|
|
1517
|
+
.update(agentWakeupRequests)
|
|
1518
|
+
.set({
|
|
1519
|
+
runId: newRun.id,
|
|
1520
|
+
updatedAt: new Date(),
|
|
1521
|
+
})
|
|
1522
|
+
.where(eq(agentWakeupRequests.id, wakeupRequest.id));
|
|
1523
|
+
publishLiveEvent({
|
|
1524
|
+
companyId: newRun.companyId,
|
|
1525
|
+
type: "heartbeat.run.queued",
|
|
1526
|
+
payload: {
|
|
1527
|
+
runId: newRun.id,
|
|
1528
|
+
agentId: newRun.agentId,
|
|
1529
|
+
invocationSource: newRun.invocationSource,
|
|
1530
|
+
triggerDetail: newRun.triggerDetail,
|
|
1531
|
+
wakeupRequestId: newRun.wakeupRequestId,
|
|
1532
|
+
},
|
|
1533
|
+
});
|
|
1534
|
+
await startNextQueuedRunForAgent(agent.id);
|
|
1535
|
+
return newRun;
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
list: (companyId, agentId, limit) => {
|
|
1539
|
+
const query = db
|
|
1540
|
+
.select()
|
|
1541
|
+
.from(heartbeatRuns)
|
|
1542
|
+
.where(agentId
|
|
1543
|
+
? and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.agentId, agentId))
|
|
1544
|
+
: eq(heartbeatRuns.companyId, companyId))
|
|
1545
|
+
.orderBy(desc(heartbeatRuns.createdAt));
|
|
1546
|
+
if (limit) {
|
|
1547
|
+
return query.limit(limit);
|
|
1548
|
+
}
|
|
1549
|
+
return query;
|
|
1550
|
+
},
|
|
1551
|
+
getRun,
|
|
1552
|
+
getRuntimeState: async (agentId) => {
|
|
1553
|
+
const state = await getRuntimeState(agentId);
|
|
1554
|
+
const agent = await getAgent(agentId);
|
|
1555
|
+
if (!agent)
|
|
1556
|
+
return null;
|
|
1557
|
+
const ensured = state ?? (await ensureRuntimeState(agent));
|
|
1558
|
+
const latestTaskSession = await db
|
|
1559
|
+
.select()
|
|
1560
|
+
.from(agentTaskSessions)
|
|
1561
|
+
.where(and(eq(agentTaskSessions.companyId, agent.companyId), eq(agentTaskSessions.agentId, agent.id)))
|
|
1562
|
+
.orderBy(desc(agentTaskSessions.updatedAt))
|
|
1563
|
+
.limit(1)
|
|
1564
|
+
.then((rows) => rows[0] ?? null);
|
|
1565
|
+
return {
|
|
1566
|
+
...ensured,
|
|
1567
|
+
sessionDisplayId: latestTaskSession?.sessionDisplayId ?? ensured.sessionId,
|
|
1568
|
+
sessionParamsJson: latestTaskSession?.sessionParamsJson ?? null,
|
|
1569
|
+
};
|
|
1570
|
+
},
|
|
1571
|
+
listTaskSessions: async (agentId) => {
|
|
1572
|
+
const agent = await getAgent(agentId);
|
|
1573
|
+
if (!agent)
|
|
1574
|
+
throw notFound("Agent not found");
|
|
1575
|
+
return db
|
|
1576
|
+
.select()
|
|
1577
|
+
.from(agentTaskSessions)
|
|
1578
|
+
.where(and(eq(agentTaskSessions.companyId, agent.companyId), eq(agentTaskSessions.agentId, agentId)))
|
|
1579
|
+
.orderBy(desc(agentTaskSessions.updatedAt), desc(agentTaskSessions.createdAt));
|
|
1580
|
+
},
|
|
1581
|
+
resetRuntimeSession: async (agentId, opts) => {
|
|
1582
|
+
const agent = await getAgent(agentId);
|
|
1583
|
+
if (!agent)
|
|
1584
|
+
throw notFound("Agent not found");
|
|
1585
|
+
await ensureRuntimeState(agent);
|
|
1586
|
+
const taskKey = readNonEmptyString(opts?.taskKey);
|
|
1587
|
+
const clearedTaskSessions = await clearTaskSessions(agent.companyId, agent.id, taskKey ? { taskKey, adapterType: agent.adapterType } : undefined);
|
|
1588
|
+
const runtimePatch = {
|
|
1589
|
+
sessionId: null,
|
|
1590
|
+
lastError: null,
|
|
1591
|
+
updatedAt: new Date(),
|
|
1592
|
+
};
|
|
1593
|
+
if (!taskKey) {
|
|
1594
|
+
runtimePatch.stateJson = {};
|
|
1595
|
+
}
|
|
1596
|
+
const updated = await db
|
|
1597
|
+
.update(agentRuntimeState)
|
|
1598
|
+
.set(runtimePatch)
|
|
1599
|
+
.where(eq(agentRuntimeState.agentId, agentId))
|
|
1600
|
+
.returning()
|
|
1601
|
+
.then((rows) => rows[0] ?? null);
|
|
1602
|
+
if (!updated)
|
|
1603
|
+
return null;
|
|
1604
|
+
return {
|
|
1605
|
+
...updated,
|
|
1606
|
+
sessionDisplayId: null,
|
|
1607
|
+
sessionParamsJson: null,
|
|
1608
|
+
clearedTaskSessions,
|
|
1609
|
+
};
|
|
1610
|
+
},
|
|
1611
|
+
listEvents: (runId, afterSeq = 0, limit = 200) => db
|
|
1612
|
+
.select()
|
|
1613
|
+
.from(heartbeatRunEvents)
|
|
1614
|
+
.where(and(eq(heartbeatRunEvents.runId, runId), gt(heartbeatRunEvents.seq, afterSeq)))
|
|
1615
|
+
.orderBy(asc(heartbeatRunEvents.seq))
|
|
1616
|
+
.limit(Math.max(1, Math.min(limit, 1000))),
|
|
1617
|
+
readLog: async (runId, opts) => {
|
|
1618
|
+
const run = await getRun(runId);
|
|
1619
|
+
if (!run)
|
|
1620
|
+
throw notFound("Heartbeat run not found");
|
|
1621
|
+
if (!run.logStore || !run.logRef)
|
|
1622
|
+
throw notFound("Run log not found");
|
|
1623
|
+
const result = await runLogStore.read({
|
|
1624
|
+
store: run.logStore,
|
|
1625
|
+
logRef: run.logRef,
|
|
1626
|
+
}, opts);
|
|
1627
|
+
return {
|
|
1628
|
+
runId,
|
|
1629
|
+
store: run.logStore,
|
|
1630
|
+
logRef: run.logRef,
|
|
1631
|
+
...result,
|
|
1632
|
+
};
|
|
1633
|
+
},
|
|
1634
|
+
invoke: async (agentId, source = "on_demand", contextSnapshot = {}, triggerDetail = "manual", actor) => enqueueWakeup(agentId, {
|
|
1635
|
+
source,
|
|
1636
|
+
triggerDetail,
|
|
1637
|
+
contextSnapshot,
|
|
1638
|
+
requestedByActorType: actor?.actorType,
|
|
1639
|
+
requestedByActorId: actor?.actorId ?? null,
|
|
1640
|
+
}),
|
|
1641
|
+
wakeup: enqueueWakeup,
|
|
1642
|
+
reapOrphanedRuns,
|
|
1643
|
+
tickTimers: async (now = new Date()) => {
|
|
1644
|
+
const allAgents = await db.select().from(agents);
|
|
1645
|
+
let checked = 0;
|
|
1646
|
+
let enqueued = 0;
|
|
1647
|
+
let skipped = 0;
|
|
1648
|
+
for (const agent of allAgents) {
|
|
1649
|
+
if (agent.status === "paused" || agent.status === "terminated")
|
|
1650
|
+
continue;
|
|
1651
|
+
const policy = parseHeartbeatPolicy(agent);
|
|
1652
|
+
if (!policy.enabled || policy.intervalSec <= 0)
|
|
1653
|
+
continue;
|
|
1654
|
+
checked += 1;
|
|
1655
|
+
const baseline = new Date(agent.lastHeartbeatAt ?? agent.createdAt).getTime();
|
|
1656
|
+
const elapsedMs = now.getTime() - baseline;
|
|
1657
|
+
if (elapsedMs < policy.intervalSec * 1000)
|
|
1658
|
+
continue;
|
|
1659
|
+
const run = await enqueueWakeup(agent.id, {
|
|
1660
|
+
source: "timer",
|
|
1661
|
+
triggerDetail: "system",
|
|
1662
|
+
reason: "heartbeat_timer",
|
|
1663
|
+
requestedByActorType: "system",
|
|
1664
|
+
requestedByActorId: "heartbeat_scheduler",
|
|
1665
|
+
contextSnapshot: {
|
|
1666
|
+
source: "scheduler",
|
|
1667
|
+
reason: "interval_elapsed",
|
|
1668
|
+
now: now.toISOString(),
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
if (run)
|
|
1672
|
+
enqueued += 1;
|
|
1673
|
+
else
|
|
1674
|
+
skipped += 1;
|
|
1675
|
+
}
|
|
1676
|
+
return { checked, enqueued, skipped };
|
|
1677
|
+
},
|
|
1678
|
+
cancelRun: async (runId) => {
|
|
1679
|
+
const run = await getRun(runId);
|
|
1680
|
+
if (!run)
|
|
1681
|
+
throw notFound("Heartbeat run not found");
|
|
1682
|
+
if (run.status !== "running" && run.status !== "queued")
|
|
1683
|
+
return run;
|
|
1684
|
+
const running = runningProcesses.get(run.id);
|
|
1685
|
+
if (running) {
|
|
1686
|
+
running.child.kill("SIGTERM");
|
|
1687
|
+
const graceMs = Math.max(1, running.graceSec) * 1000;
|
|
1688
|
+
setTimeout(() => {
|
|
1689
|
+
if (!running.child.killed) {
|
|
1690
|
+
running.child.kill("SIGKILL");
|
|
1691
|
+
}
|
|
1692
|
+
}, graceMs);
|
|
1693
|
+
}
|
|
1694
|
+
const cancelled = await setRunStatus(run.id, "cancelled", {
|
|
1695
|
+
finishedAt: new Date(),
|
|
1696
|
+
error: "Cancelled by control plane",
|
|
1697
|
+
errorCode: "cancelled",
|
|
1698
|
+
});
|
|
1699
|
+
await setWakeupStatus(run.wakeupRequestId, "cancelled", {
|
|
1700
|
+
finishedAt: new Date(),
|
|
1701
|
+
error: "Cancelled by control plane",
|
|
1702
|
+
});
|
|
1703
|
+
if (cancelled) {
|
|
1704
|
+
await appendRunEvent(cancelled, 1, {
|
|
1705
|
+
eventType: "lifecycle",
|
|
1706
|
+
stream: "system",
|
|
1707
|
+
level: "warn",
|
|
1708
|
+
message: "run cancelled",
|
|
1709
|
+
});
|
|
1710
|
+
await releaseIssueExecutionAndPromote(cancelled);
|
|
1711
|
+
}
|
|
1712
|
+
runningProcesses.delete(run.id);
|
|
1713
|
+
await finalizeAgentStatus(run.agentId, "cancelled");
|
|
1714
|
+
await startNextQueuedRunForAgent(run.agentId);
|
|
1715
|
+
return cancelled;
|
|
1716
|
+
},
|
|
1717
|
+
cancelActiveForAgent: async (agentId) => {
|
|
1718
|
+
const runs = await db
|
|
1719
|
+
.select()
|
|
1720
|
+
.from(heartbeatRuns)
|
|
1721
|
+
.where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"])));
|
|
1722
|
+
for (const run of runs) {
|
|
1723
|
+
await setRunStatus(run.id, "cancelled", {
|
|
1724
|
+
finishedAt: new Date(),
|
|
1725
|
+
error: "Cancelled due to agent pause",
|
|
1726
|
+
errorCode: "cancelled",
|
|
1727
|
+
});
|
|
1728
|
+
await setWakeupStatus(run.wakeupRequestId, "cancelled", {
|
|
1729
|
+
finishedAt: new Date(),
|
|
1730
|
+
error: "Cancelled due to agent pause",
|
|
1731
|
+
});
|
|
1732
|
+
const running = runningProcesses.get(run.id);
|
|
1733
|
+
if (running) {
|
|
1734
|
+
running.child.kill("SIGTERM");
|
|
1735
|
+
runningProcesses.delete(run.id);
|
|
1736
|
+
}
|
|
1737
|
+
await releaseIssueExecutionAndPromote(run);
|
|
1738
|
+
}
|
|
1739
|
+
return runs.length;
|
|
1740
|
+
},
|
|
1741
|
+
getActiveRunForAgent: async (agentId) => {
|
|
1742
|
+
const [run] = await db
|
|
1743
|
+
.select()
|
|
1744
|
+
.from(heartbeatRuns)
|
|
1745
|
+
.where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running")))
|
|
1746
|
+
.orderBy(desc(heartbeatRuns.startedAt))
|
|
1747
|
+
.limit(1);
|
|
1748
|
+
return run ?? null;
|
|
1749
|
+
},
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
//# sourceMappingURL=heartbeat.js.map
|