@paperclipai/server 0.3.0 → 0.3.1-canary.1
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/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +12 -0
- package/dist/adapters/registry.js.map +1 -1
- package/dist/app.d.ts +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +9 -2
- package/dist/app.js.map +1 -1
- package/dist/attachment-types.d.ts +33 -0
- package/dist/attachment-types.d.ts.map +1 -0
- package/dist/attachment-types.js +61 -0
- package/dist/attachment-types.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/log-redaction.d.ts +10 -0
- package/dist/log-redaction.d.ts.map +1 -0
- package/dist/log-redaction.js +110 -0
- package/dist/log-redaction.js.map +1 -0
- package/dist/middleware/logger.d.ts.map +1 -1
- package/dist/middleware/logger.js +1 -0
- package/dist/middleware/logger.js.map +1 -1
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +12 -21
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +102 -3
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +87 -83
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +5 -12
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +16 -11
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/sidebar-badges.d.ts.map +1 -1
- package/dist/routes/sidebar-badges.js +1 -4
- package/dist/routes/sidebar-badges.js.map +1 -1
- package/dist/services/activity-log.d.ts.map +1 -1
- package/dist/services/activity-log.js +4 -2
- package/dist/services/activity-log.js.map +1 -1
- package/dist/services/approvals.d.ts +30 -24
- package/dist/services/approvals.d.ts.map +1 -1
- package/dist/services/approvals.js +51 -42
- package/dist/services/approvals.js.map +1 -1
- package/dist/services/company-portability.d.ts.map +1 -1
- package/dist/services/company-portability.js +5 -1
- package/dist/services/company-portability.js.map +1 -1
- package/dist/services/dashboard.d.ts +0 -1
- package/dist/services/dashboard.d.ts.map +1 -1
- package/dist/services/dashboard.js +0 -7
- package/dist/services/dashboard.js.map +1 -1
- package/dist/services/execution-workspace-policy.d.ts +19 -0
- package/dist/services/execution-workspace-policy.d.ts.map +1 -0
- package/dist/services/execution-workspace-policy.js +117 -0
- package/dist/services/execution-workspace-policy.js.map +1 -0
- package/dist/services/goals.d.ts +26 -0
- package/dist/services/goals.d.ts.map +1 -1
- package/dist/services/goals.js +26 -1
- package/dist/services/goals.js.map +1 -1
- package/dist/services/heartbeat-run-summary.d.ts +2 -0
- package/dist/services/heartbeat-run-summary.d.ts.map +1 -0
- package/dist/services/heartbeat-run-summary.js +30 -0
- package/dist/services/heartbeat-run-summary.js.map +1 -0
- package/dist/services/heartbeat.d.ts +30 -996
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +236 -47
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/issue-goal-fallback.d.ts +15 -0
- package/dist/services/issue-goal-fallback.d.ts.map +1 -0
- package/dist/services/issue-goal-fallback.js +15 -0
- package/dist/services/issue-goal-fallback.js.map +1 -0
- package/dist/services/issues.d.ts +5 -278
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +52 -14
- package/dist/services/issues.js.map +1 -1
- package/dist/services/projects.d.ts +4 -2
- package/dist/services/projects.d.ts.map +1 -1
- package/dist/services/projects.js +49 -6
- package/dist/services/projects.js.map +1 -1
- package/dist/services/workspace-runtime.d.ts +126 -0
- package/dist/services/workspace-runtime.d.ts.map +1 -0
- package/dist/services/workspace-runtime.js +852 -0
- package/dist/services/workspace-runtime.js.map +1 -0
- package/dist/ui-branding.d.ts +4 -0
- package/dist/ui-branding.d.ts.map +1 -0
- package/dist/ui-branding.js +37 -0
- package/dist/ui-branding.js.map +1 -0
- package/package.json +11 -10
- package/skills/paperclip/SKILL.md +23 -19
- package/ui-dist/assets/{_basePickBy-BYrQlacK.js → _basePickBy-B0xbZITw.js} +1 -1
- package/ui-dist/assets/{_baseUniq-DOSawgF3.js → _baseUniq-Cfd5u3qc.js} +1 -1
- package/ui-dist/assets/{arc-CG7T0hfG.js → arc-7Xbu8tBF.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-VXUJARFQ-Bcn7ytDO.js → architectureDiagram-VXUJARFQ-C8F7ZRYc.js} +1 -1
- package/ui-dist/assets/{blockDiagram-VD42YOAC-BQGrx2lv.js → blockDiagram-VD42YOAC-Dp08a65A.js} +1 -1
- package/ui-dist/assets/{c4Diagram-YG6GDRKO-owH9Kb3t.js → c4Diagram-YG6GDRKO-BJfslTgZ.js} +1 -1
- package/ui-dist/assets/channel-BViQDbSq.js +1 -0
- package/ui-dist/assets/{chunk-4BX2VUAB-DY1UIe4g.js → chunk-4BX2VUAB-BhaYWH7e.js} +1 -1
- package/ui-dist/assets/{chunk-55IACEB6-CnWFPfPQ.js → chunk-55IACEB6-BOzJUsYW.js} +1 -1
- package/ui-dist/assets/{chunk-B4BG7PRW-DhlLW80l.js → chunk-B4BG7PRW-CbVcziyE.js} +1 -1
- package/ui-dist/assets/{chunk-DI55MBZ5-DPt7dj6c.js → chunk-DI55MBZ5-DghPtP3y.js} +1 -1
- package/ui-dist/assets/{chunk-FMBD7UC4-GQwzgYa4.js → chunk-FMBD7UC4-CB095Kfn.js} +1 -1
- package/ui-dist/assets/{chunk-QN33PNHL-BFHLVk5s.js → chunk-QN33PNHL-DfpzfDqJ.js} +1 -1
- package/ui-dist/assets/{chunk-QZHKN3VN-CLe3KEAf.js → chunk-QZHKN3VN-Df3d5z1y.js} +1 -1
- package/ui-dist/assets/{chunk-TZMSLE5B-BhccYB4e.js → chunk-TZMSLE5B-B6tX6bZI.js} +1 -1
- package/ui-dist/assets/classDiagram-2ON5EDUG-CjMCmxMT.js +1 -0
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-CjMCmxMT.js +1 -0
- package/ui-dist/assets/clone-Dbn9wtE1.js +1 -0
- package/ui-dist/assets/{cose-bilkent-S5V4N54A-DqECYL1w.js → cose-bilkent-S5V4N54A-38wg_s9V.js} +1 -1
- package/ui-dist/assets/{dagre-6UL2VRFP-DXeQqIJ2.js → dagre-6UL2VRFP-DPVj3XLS.js} +1 -1
- package/ui-dist/assets/{diagram-PSM6KHXK-DNu3Ctuy.js → diagram-PSM6KHXK-BJh89zUA.js} +1 -1
- package/ui-dist/assets/{diagram-QEK2KX5R-1wUR_z9S.js → diagram-QEK2KX5R-Bw0m_j10.js} +1 -1
- package/ui-dist/assets/{diagram-S2PKOQOG-D3IK8rZb.js → diagram-S2PKOQOG-D7_M2F3u.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q2GNP2WA-DU3L0RbU.js → erDiagram-Q2GNP2WA-CLsjqTWP.js} +1 -1
- package/ui-dist/assets/{flowDiagram-NV44I4VS-CN46A5Ez.js → flowDiagram-NV44I4VS-BZZ7ezVB.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-JELNMOA3-DGbOi1Wz.js → ganttDiagram-JELNMOA3-KzsFuwBt.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-D98N7SOj.js → gitGraphDiagram-V2S2FVAM-DSHtY7Vu.js} +1 -1
- package/ui-dist/assets/{graph-Cf7LCNJy.js → graph-luIG1UAS.js} +1 -1
- package/ui-dist/assets/{index-CP1BgxcV.js → index-00kuG4sI.js} +1 -1
- package/ui-dist/assets/{index-DiXE2gv-.js → index-1gX09-Fl.js} +1 -1
- package/ui-dist/assets/{index-CqG5WZHq.js → index-B1ZMzzs0.js} +1 -1
- package/ui-dist/assets/index-BHP9dico.js +1006 -0
- package/ui-dist/assets/{index-cx0y6-1h.js → index-B_3g3Rie.js} +1 -1
- package/ui-dist/assets/{index-CI56poQD.js → index-BeeKMqNU.js} +1 -1
- package/ui-dist/assets/{index-BfG2u5u0.js → index-BfB4lKJN.js} +1 -1
- package/ui-dist/assets/index-BfNaDZnn.css +1 -0
- package/ui-dist/assets/{index-DZdNKByU.js → index-Bv4xCjxl.js} +1 -1
- package/ui-dist/assets/{index--K1VLoF-.js → index-C2SZYIDA.js} +1 -1
- package/ui-dist/assets/{index-DB5nKqAA.js → index-C8XyGAr9.js} +1 -1
- package/ui-dist/assets/{index-C2-SE7P0.js → index-CaR9XM4h.js} +1 -1
- package/ui-dist/assets/{index-DujThSls.js → index-CbGtsjW7.js} +1 -1
- package/ui-dist/assets/{index-D0EsfNYg.js → index-CeDCs_2i.js} +1 -1
- package/ui-dist/assets/{index-OkxoZoQy.js → index-DBy0vJy3.js} +1 -1
- package/ui-dist/assets/{index-BoAYxRAO.js → index-DJf8diAA.js} +1 -1
- package/ui-dist/assets/{index-N1SX_i0z.js → index-DcfLFstG.js} +1 -1
- package/ui-dist/assets/{index-DXgtGequ.js → index-DnIPDZLp.js} +1 -1
- package/ui-dist/assets/{index-Cick_QSL.js → index-DoTq-BeR.js} +1 -1
- package/ui-dist/assets/{index-0BSerEC2.js → index-Ds7vLTSK.js} +1 -1
- package/ui-dist/assets/{index-Beb2ZlSv.js → index-RUBFVv6t.js} +1 -1
- package/ui-dist/assets/{index-TFF7cXd7.js → index-itc7BfMy.js} +1 -1
- package/ui-dist/assets/{index-BD6My-aI.js → index-nFjLambq.js} +1 -1
- package/ui-dist/assets/{index-B-xuGUs-.js → index-uR3zjYaD.js} +1 -1
- package/ui-dist/assets/{infoDiagram-HS3SLOUP-D_b1CK0Y.js → infoDiagram-HS3SLOUP-CDEfWpme.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-4oCVXUve.js → journeyDiagram-XKPGCS4Q-i7nsbg_Y.js} +1 -1
- package/ui-dist/assets/{kanban-definition-3W4ZIXB7-0VcjP_qf.js → kanban-definition-3W4ZIXB7-DDlx1qVE.js} +1 -1
- package/ui-dist/assets/{layout-BQcYXlNv.js → layout-C7AtmJzX.js} +1 -1
- package/ui-dist/assets/{linear-nz0Lfiys.js → linear-C5jHT-WP.js} +1 -1
- package/ui-dist/assets/{mermaid.core-BaxvgwjG.js → mermaid.core-C8YQ4fcY.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-VGOIOE7T-BnW6nEhl.js → mindmap-definition-VGOIOE7T-B9m9PuUg.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ADFJNKIX-O1tvU_18.js → pieDiagram-ADFJNKIX-SvKywCSE.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-BfM2aQbf.js → quadrantDiagram-AYHSOK5B-QfBPm7Y1.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-rXVZupag.js → requirementDiagram-UZGBJVZJ-DnfQQuwi.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-BNgaPVo6.js → sankeyDiagram-TZEHDZUN-DvLkjzIW.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-WL72ISMW--KnZ0qRV.js → sequenceDiagram-WL72ISMW-5Lq2rWBc.js} +1 -1
- package/ui-dist/assets/{stateDiagram-FKZM4ZOC-DlGdC88b.js → stateDiagram-FKZM4ZOC-DcYPHDyi.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-nv50YYrl.js +1 -0
- package/ui-dist/assets/{timeline-definition-IT6M3QCI-CCwriy0-.js → timeline-definition-IT6M3QCI-lrN4JmWa.js} +1 -1
- package/ui-dist/assets/{treemap-GDKQZRPO-C-79yojr.js → treemap-GDKQZRPO-DFJKIBTQ.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-PRI3JC2R-Dj0jcMBZ.js → xychartDiagram-PRI3JC2R-CT47vtdm.js} +1 -1
- package/ui-dist/index.html +4 -2
- package/ui-dist/worktree-favicon-16x16.png +0 -0
- package/ui-dist/worktree-favicon-32x32.png +0 -0
- package/ui-dist/worktree-favicon.ico +0 -0
- package/ui-dist/worktree-favicon.svg +9 -0
- package/skills/release/SKILL.md +0 -261
- package/skills/release-changelog/SKILL.md +0 -178
- package/ui-dist/assets/channel-DdXqC9Qy.js +0 -1
- package/ui-dist/assets/classDiagram-2ON5EDUG-ZV36NLFv.js +0 -1
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-ZV36NLFv.js +0 -1
- package/ui-dist/assets/clone-CvxIjPQa.js +0 -1
- package/ui-dist/assets/index-BYw6Loly.js +0 -900
- package/ui-dist/assets/index-nfAtmpEH.css +0 -1
- package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-CuVnpOfP.js +0 -1
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import { workspaceRuntimeServices } from "@paperclipai/db";
|
|
8
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
9
|
+
import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js";
|
|
10
|
+
import { resolveHomeAwarePath } from "../home-paths.js";
|
|
11
|
+
const runtimeServicesById = new Map();
|
|
12
|
+
const runtimeServicesByReuseKey = new Map();
|
|
13
|
+
const runtimeServiceLeasesByRun = new Map();
|
|
14
|
+
function stableStringify(value) {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
17
|
+
}
|
|
18
|
+
if (value && typeof value === "object") {
|
|
19
|
+
const rec = value;
|
|
20
|
+
return `{${Object.keys(rec).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(rec[key])}`).join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
function stableRuntimeServiceId(input) {
|
|
25
|
+
if (input.reportId)
|
|
26
|
+
return input.reportId;
|
|
27
|
+
const digest = createHash("sha256")
|
|
28
|
+
.update(stableStringify({
|
|
29
|
+
adapterType: input.adapterType,
|
|
30
|
+
runId: input.runId,
|
|
31
|
+
scopeType: input.scopeType,
|
|
32
|
+
scopeId: input.scopeId,
|
|
33
|
+
serviceName: input.serviceName,
|
|
34
|
+
providerRef: input.providerRef,
|
|
35
|
+
reuseKey: input.reuseKey,
|
|
36
|
+
}))
|
|
37
|
+
.digest("hex")
|
|
38
|
+
.slice(0, 32);
|
|
39
|
+
return `${input.adapterType}-${digest}`;
|
|
40
|
+
}
|
|
41
|
+
function toRuntimeServiceRef(record, overrides) {
|
|
42
|
+
return {
|
|
43
|
+
id: record.id,
|
|
44
|
+
companyId: record.companyId,
|
|
45
|
+
projectId: record.projectId,
|
|
46
|
+
projectWorkspaceId: record.projectWorkspaceId,
|
|
47
|
+
issueId: record.issueId,
|
|
48
|
+
serviceName: record.serviceName,
|
|
49
|
+
status: record.status,
|
|
50
|
+
lifecycle: record.lifecycle,
|
|
51
|
+
scopeType: record.scopeType,
|
|
52
|
+
scopeId: record.scopeId,
|
|
53
|
+
reuseKey: record.reuseKey,
|
|
54
|
+
command: record.command,
|
|
55
|
+
cwd: record.cwd,
|
|
56
|
+
port: record.port,
|
|
57
|
+
url: record.url,
|
|
58
|
+
provider: record.provider,
|
|
59
|
+
providerRef: record.providerRef,
|
|
60
|
+
ownerAgentId: record.ownerAgentId,
|
|
61
|
+
startedByRunId: record.startedByRunId,
|
|
62
|
+
lastUsedAt: record.lastUsedAt,
|
|
63
|
+
startedAt: record.startedAt,
|
|
64
|
+
stoppedAt: record.stoppedAt,
|
|
65
|
+
stopPolicy: record.stopPolicy,
|
|
66
|
+
healthStatus: record.healthStatus,
|
|
67
|
+
reused: record.reused,
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function sanitizeSlugPart(value, fallback) {
|
|
72
|
+
const raw = (value ?? "").trim().toLowerCase();
|
|
73
|
+
const normalized = raw
|
|
74
|
+
.replace(/[^a-z0-9/_-]+/g, "-")
|
|
75
|
+
.replace(/-+/g, "-")
|
|
76
|
+
.replace(/^[-/]+|[-/]+$/g, "");
|
|
77
|
+
return normalized.length > 0 ? normalized : fallback;
|
|
78
|
+
}
|
|
79
|
+
function renderWorkspaceTemplate(template, input) {
|
|
80
|
+
const issueIdentifier = input.issue?.identifier ?? input.issue?.id ?? "issue";
|
|
81
|
+
const slug = sanitizeSlugPart(input.issue?.title, sanitizeSlugPart(issueIdentifier, "issue"));
|
|
82
|
+
return renderTemplate(template, {
|
|
83
|
+
issue: {
|
|
84
|
+
id: input.issue?.id ?? "",
|
|
85
|
+
identifier: input.issue?.identifier ?? "",
|
|
86
|
+
title: input.issue?.title ?? "",
|
|
87
|
+
},
|
|
88
|
+
agent: {
|
|
89
|
+
id: input.agent.id,
|
|
90
|
+
name: input.agent.name,
|
|
91
|
+
},
|
|
92
|
+
project: {
|
|
93
|
+
id: input.projectId ?? "",
|
|
94
|
+
},
|
|
95
|
+
workspace: {
|
|
96
|
+
repoRef: input.repoRef ?? "",
|
|
97
|
+
},
|
|
98
|
+
slug,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function sanitizeBranchName(value) {
|
|
102
|
+
return value
|
|
103
|
+
.trim()
|
|
104
|
+
.replace(/[^A-Za-z0-9._/-]+/g, "-")
|
|
105
|
+
.replace(/-+/g, "-")
|
|
106
|
+
.replace(/^[-/.]+|[-/.]+$/g, "")
|
|
107
|
+
.slice(0, 120) || "paperclip-work";
|
|
108
|
+
}
|
|
109
|
+
function isAbsolutePath(value) {
|
|
110
|
+
return path.isAbsolute(value) || value.startsWith("~");
|
|
111
|
+
}
|
|
112
|
+
function resolveConfiguredPath(value, baseDir) {
|
|
113
|
+
if (isAbsolutePath(value)) {
|
|
114
|
+
return resolveHomeAwarePath(value);
|
|
115
|
+
}
|
|
116
|
+
return path.resolve(baseDir, value);
|
|
117
|
+
}
|
|
118
|
+
async function runGit(args, cwd) {
|
|
119
|
+
const proc = await new Promise((resolve, reject) => {
|
|
120
|
+
const child = spawn("git", args, {
|
|
121
|
+
cwd,
|
|
122
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
123
|
+
env: process.env,
|
|
124
|
+
});
|
|
125
|
+
let stdout = "";
|
|
126
|
+
let stderr = "";
|
|
127
|
+
child.stdout?.on("data", (chunk) => {
|
|
128
|
+
stdout += String(chunk);
|
|
129
|
+
});
|
|
130
|
+
child.stderr?.on("data", (chunk) => {
|
|
131
|
+
stderr += String(chunk);
|
|
132
|
+
});
|
|
133
|
+
child.on("error", reject);
|
|
134
|
+
child.on("close", (code) => resolve({ stdout, stderr, code }));
|
|
135
|
+
});
|
|
136
|
+
if (proc.code !== 0) {
|
|
137
|
+
throw new Error(proc.stderr.trim() || proc.stdout.trim() || `git ${args.join(" ")} failed`);
|
|
138
|
+
}
|
|
139
|
+
return proc.stdout.trim();
|
|
140
|
+
}
|
|
141
|
+
async function directoryExists(value) {
|
|
142
|
+
return fs.stat(value).then((stats) => stats.isDirectory()).catch(() => false);
|
|
143
|
+
}
|
|
144
|
+
function buildWorkspaceCommandEnv(input) {
|
|
145
|
+
const env = { ...process.env };
|
|
146
|
+
env.PAPERCLIP_WORKSPACE_CWD = input.worktreePath;
|
|
147
|
+
env.PAPERCLIP_WORKSPACE_PATH = input.worktreePath;
|
|
148
|
+
env.PAPERCLIP_WORKSPACE_WORKTREE_PATH = input.worktreePath;
|
|
149
|
+
env.PAPERCLIP_WORKSPACE_BRANCH = input.branchName;
|
|
150
|
+
env.PAPERCLIP_WORKSPACE_BASE_CWD = input.base.baseCwd;
|
|
151
|
+
env.PAPERCLIP_WORKSPACE_REPO_ROOT = input.repoRoot;
|
|
152
|
+
env.PAPERCLIP_WORKSPACE_SOURCE = input.base.source;
|
|
153
|
+
env.PAPERCLIP_WORKSPACE_REPO_REF = input.base.repoRef ?? "";
|
|
154
|
+
env.PAPERCLIP_WORKSPACE_REPO_URL = input.base.repoUrl ?? "";
|
|
155
|
+
env.PAPERCLIP_WORKSPACE_CREATED = input.created ? "true" : "false";
|
|
156
|
+
env.PAPERCLIP_PROJECT_ID = input.base.projectId ?? "";
|
|
157
|
+
env.PAPERCLIP_PROJECT_WORKSPACE_ID = input.base.workspaceId ?? "";
|
|
158
|
+
env.PAPERCLIP_AGENT_ID = input.agent.id;
|
|
159
|
+
env.PAPERCLIP_AGENT_NAME = input.agent.name;
|
|
160
|
+
env.PAPERCLIP_COMPANY_ID = input.agent.companyId;
|
|
161
|
+
env.PAPERCLIP_ISSUE_ID = input.issue?.id ?? "";
|
|
162
|
+
env.PAPERCLIP_ISSUE_IDENTIFIER = input.issue?.identifier ?? "";
|
|
163
|
+
env.PAPERCLIP_ISSUE_TITLE = input.issue?.title ?? "";
|
|
164
|
+
return env;
|
|
165
|
+
}
|
|
166
|
+
async function runWorkspaceCommand(input) {
|
|
167
|
+
const shell = process.env.SHELL?.trim() || "/bin/sh";
|
|
168
|
+
const proc = await new Promise((resolve, reject) => {
|
|
169
|
+
const child = spawn(shell, ["-c", input.command], {
|
|
170
|
+
cwd: input.cwd,
|
|
171
|
+
env: input.env,
|
|
172
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
173
|
+
});
|
|
174
|
+
let stdout = "";
|
|
175
|
+
let stderr = "";
|
|
176
|
+
child.stdout?.on("data", (chunk) => {
|
|
177
|
+
stdout += String(chunk);
|
|
178
|
+
});
|
|
179
|
+
child.stderr?.on("data", (chunk) => {
|
|
180
|
+
stderr += String(chunk);
|
|
181
|
+
});
|
|
182
|
+
child.on("error", reject);
|
|
183
|
+
child.on("close", (code) => resolve({ stdout, stderr, code }));
|
|
184
|
+
});
|
|
185
|
+
if (proc.code === 0)
|
|
186
|
+
return;
|
|
187
|
+
const details = [proc.stderr.trim(), proc.stdout.trim()].filter(Boolean).join("\n");
|
|
188
|
+
throw new Error(details.length > 0
|
|
189
|
+
? `${input.label} failed: ${details}`
|
|
190
|
+
: `${input.label} failed with exit code ${proc.code ?? -1}`);
|
|
191
|
+
}
|
|
192
|
+
async function provisionExecutionWorktree(input) {
|
|
193
|
+
const provisionCommand = asString(input.strategy.provisionCommand, "").trim();
|
|
194
|
+
if (!provisionCommand)
|
|
195
|
+
return;
|
|
196
|
+
await runWorkspaceCommand({
|
|
197
|
+
command: provisionCommand,
|
|
198
|
+
cwd: input.worktreePath,
|
|
199
|
+
env: buildWorkspaceCommandEnv({
|
|
200
|
+
base: input.base,
|
|
201
|
+
repoRoot: input.repoRoot,
|
|
202
|
+
worktreePath: input.worktreePath,
|
|
203
|
+
branchName: input.branchName,
|
|
204
|
+
issue: input.issue,
|
|
205
|
+
agent: input.agent,
|
|
206
|
+
created: input.created,
|
|
207
|
+
}),
|
|
208
|
+
label: `Execution workspace provision command "${provisionCommand}"`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export async function realizeExecutionWorkspace(input) {
|
|
212
|
+
const rawStrategy = parseObject(input.config.workspaceStrategy);
|
|
213
|
+
const strategyType = asString(rawStrategy.type, "project_primary");
|
|
214
|
+
if (strategyType !== "git_worktree") {
|
|
215
|
+
return {
|
|
216
|
+
...input.base,
|
|
217
|
+
strategy: "project_primary",
|
|
218
|
+
cwd: input.base.baseCwd,
|
|
219
|
+
branchName: null,
|
|
220
|
+
worktreePath: null,
|
|
221
|
+
warnings: [],
|
|
222
|
+
created: false,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const repoRoot = await runGit(["rev-parse", "--show-toplevel"], input.base.baseCwd);
|
|
226
|
+
const branchTemplate = asString(rawStrategy.branchTemplate, "{{issue.identifier}}-{{slug}}");
|
|
227
|
+
const renderedBranch = renderWorkspaceTemplate(branchTemplate, {
|
|
228
|
+
issue: input.issue,
|
|
229
|
+
agent: input.agent,
|
|
230
|
+
projectId: input.base.projectId,
|
|
231
|
+
repoRef: input.base.repoRef,
|
|
232
|
+
});
|
|
233
|
+
const branchName = sanitizeBranchName(renderedBranch);
|
|
234
|
+
const configuredParentDir = asString(rawStrategy.worktreeParentDir, "");
|
|
235
|
+
const worktreeParentDir = configuredParentDir
|
|
236
|
+
? resolveConfiguredPath(configuredParentDir, repoRoot)
|
|
237
|
+
: path.join(repoRoot, ".paperclip", "worktrees");
|
|
238
|
+
const worktreePath = path.join(worktreeParentDir, branchName);
|
|
239
|
+
const baseRef = asString(rawStrategy.baseRef, input.base.repoRef ?? "HEAD");
|
|
240
|
+
await fs.mkdir(worktreeParentDir, { recursive: true });
|
|
241
|
+
const existingWorktree = await directoryExists(worktreePath);
|
|
242
|
+
if (existingWorktree) {
|
|
243
|
+
const existingGitDir = await runGit(["rev-parse", "--git-dir"], worktreePath).catch(() => null);
|
|
244
|
+
if (existingGitDir) {
|
|
245
|
+
await provisionExecutionWorktree({
|
|
246
|
+
strategy: rawStrategy,
|
|
247
|
+
base: input.base,
|
|
248
|
+
repoRoot,
|
|
249
|
+
worktreePath,
|
|
250
|
+
branchName,
|
|
251
|
+
issue: input.issue,
|
|
252
|
+
agent: input.agent,
|
|
253
|
+
created: false,
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
...input.base,
|
|
257
|
+
strategy: "git_worktree",
|
|
258
|
+
cwd: worktreePath,
|
|
259
|
+
branchName,
|
|
260
|
+
worktreePath,
|
|
261
|
+
warnings: [],
|
|
262
|
+
created: false,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Configured worktree path "${worktreePath}" already exists and is not a git worktree.`);
|
|
266
|
+
}
|
|
267
|
+
await runGit(["worktree", "add", "-B", branchName, worktreePath, baseRef], repoRoot);
|
|
268
|
+
await provisionExecutionWorktree({
|
|
269
|
+
strategy: rawStrategy,
|
|
270
|
+
base: input.base,
|
|
271
|
+
repoRoot,
|
|
272
|
+
worktreePath,
|
|
273
|
+
branchName,
|
|
274
|
+
issue: input.issue,
|
|
275
|
+
agent: input.agent,
|
|
276
|
+
created: true,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
...input.base,
|
|
280
|
+
strategy: "git_worktree",
|
|
281
|
+
cwd: worktreePath,
|
|
282
|
+
branchName,
|
|
283
|
+
worktreePath,
|
|
284
|
+
warnings: [],
|
|
285
|
+
created: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async function allocatePort() {
|
|
289
|
+
return await new Promise((resolve, reject) => {
|
|
290
|
+
const server = net.createServer();
|
|
291
|
+
server.listen(0, "127.0.0.1", () => {
|
|
292
|
+
const address = server.address();
|
|
293
|
+
server.close((err) => {
|
|
294
|
+
if (err) {
|
|
295
|
+
reject(err);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!address || typeof address === "string") {
|
|
299
|
+
reject(new Error("Failed to allocate port"));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
resolve(address.port);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
server.on("error", reject);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function buildTemplateData(input) {
|
|
309
|
+
return {
|
|
310
|
+
workspace: {
|
|
311
|
+
cwd: input.workspace.cwd,
|
|
312
|
+
branchName: input.workspace.branchName ?? "",
|
|
313
|
+
worktreePath: input.workspace.worktreePath ?? "",
|
|
314
|
+
repoUrl: input.workspace.repoUrl ?? "",
|
|
315
|
+
repoRef: input.workspace.repoRef ?? "",
|
|
316
|
+
env: input.adapterEnv,
|
|
317
|
+
},
|
|
318
|
+
issue: {
|
|
319
|
+
id: input.issue?.id ?? "",
|
|
320
|
+
identifier: input.issue?.identifier ?? "",
|
|
321
|
+
title: input.issue?.title ?? "",
|
|
322
|
+
},
|
|
323
|
+
agent: {
|
|
324
|
+
id: input.agent.id,
|
|
325
|
+
name: input.agent.name,
|
|
326
|
+
},
|
|
327
|
+
port: input.port ?? "",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function resolveServiceScopeId(input) {
|
|
331
|
+
const scopeTypeRaw = asString(input.service.reuseScope, input.service.lifecycle === "shared" ? "project_workspace" : "run");
|
|
332
|
+
const scopeType = scopeTypeRaw === "project_workspace" ||
|
|
333
|
+
scopeTypeRaw === "execution_workspace" ||
|
|
334
|
+
scopeTypeRaw === "agent"
|
|
335
|
+
? scopeTypeRaw
|
|
336
|
+
: "run";
|
|
337
|
+
if (scopeType === "project_workspace")
|
|
338
|
+
return { scopeType, scopeId: input.workspace.workspaceId ?? input.workspace.projectId };
|
|
339
|
+
if (scopeType === "execution_workspace")
|
|
340
|
+
return { scopeType, scopeId: input.workspace.cwd };
|
|
341
|
+
if (scopeType === "agent")
|
|
342
|
+
return { scopeType, scopeId: input.agent.id };
|
|
343
|
+
return { scopeType: "run", scopeId: input.runId };
|
|
344
|
+
}
|
|
345
|
+
async function waitForReadiness(input) {
|
|
346
|
+
const readiness = parseObject(input.service.readiness);
|
|
347
|
+
const readinessType = asString(readiness.type, "");
|
|
348
|
+
if (readinessType !== "http" || !input.url)
|
|
349
|
+
return;
|
|
350
|
+
const timeoutSec = Math.max(1, asNumber(readiness.timeoutSec, 30));
|
|
351
|
+
const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500));
|
|
352
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
353
|
+
let lastError = "service did not become ready";
|
|
354
|
+
while (Date.now() < deadline) {
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch(input.url);
|
|
357
|
+
if (response.ok)
|
|
358
|
+
return;
|
|
359
|
+
lastError = `received HTTP ${response.status}`;
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
363
|
+
}
|
|
364
|
+
await delay(intervalMs);
|
|
365
|
+
}
|
|
366
|
+
throw new Error(`Readiness check failed for ${input.url}: ${lastError}`);
|
|
367
|
+
}
|
|
368
|
+
function toPersistedWorkspaceRuntimeService(record) {
|
|
369
|
+
return {
|
|
370
|
+
id: record.id,
|
|
371
|
+
companyId: record.companyId,
|
|
372
|
+
projectId: record.projectId,
|
|
373
|
+
projectWorkspaceId: record.projectWorkspaceId,
|
|
374
|
+
issueId: record.issueId,
|
|
375
|
+
scopeType: record.scopeType,
|
|
376
|
+
scopeId: record.scopeId,
|
|
377
|
+
serviceName: record.serviceName,
|
|
378
|
+
status: record.status,
|
|
379
|
+
lifecycle: record.lifecycle,
|
|
380
|
+
reuseKey: record.reuseKey,
|
|
381
|
+
command: record.command,
|
|
382
|
+
cwd: record.cwd,
|
|
383
|
+
port: record.port,
|
|
384
|
+
url: record.url,
|
|
385
|
+
provider: record.provider,
|
|
386
|
+
providerRef: record.providerRef,
|
|
387
|
+
ownerAgentId: record.ownerAgentId,
|
|
388
|
+
startedByRunId: record.startedByRunId,
|
|
389
|
+
lastUsedAt: new Date(record.lastUsedAt),
|
|
390
|
+
startedAt: new Date(record.startedAt),
|
|
391
|
+
stoppedAt: record.stoppedAt ? new Date(record.stoppedAt) : null,
|
|
392
|
+
stopPolicy: record.stopPolicy,
|
|
393
|
+
healthStatus: record.healthStatus,
|
|
394
|
+
updatedAt: new Date(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async function persistRuntimeServiceRecord(db, record) {
|
|
398
|
+
if (!db)
|
|
399
|
+
return;
|
|
400
|
+
const values = toPersistedWorkspaceRuntimeService(record);
|
|
401
|
+
await db
|
|
402
|
+
.insert(workspaceRuntimeServices)
|
|
403
|
+
.values(values)
|
|
404
|
+
.onConflictDoUpdate({
|
|
405
|
+
target: workspaceRuntimeServices.id,
|
|
406
|
+
set: {
|
|
407
|
+
projectId: values.projectId,
|
|
408
|
+
projectWorkspaceId: values.projectWorkspaceId,
|
|
409
|
+
issueId: values.issueId,
|
|
410
|
+
scopeType: values.scopeType,
|
|
411
|
+
scopeId: values.scopeId,
|
|
412
|
+
serviceName: values.serviceName,
|
|
413
|
+
status: values.status,
|
|
414
|
+
lifecycle: values.lifecycle,
|
|
415
|
+
reuseKey: values.reuseKey,
|
|
416
|
+
command: values.command,
|
|
417
|
+
cwd: values.cwd,
|
|
418
|
+
port: values.port,
|
|
419
|
+
url: values.url,
|
|
420
|
+
provider: values.provider,
|
|
421
|
+
providerRef: values.providerRef,
|
|
422
|
+
ownerAgentId: values.ownerAgentId,
|
|
423
|
+
startedByRunId: values.startedByRunId,
|
|
424
|
+
lastUsedAt: values.lastUsedAt,
|
|
425
|
+
startedAt: values.startedAt,
|
|
426
|
+
stoppedAt: values.stoppedAt,
|
|
427
|
+
stopPolicy: values.stopPolicy,
|
|
428
|
+
healthStatus: values.healthStatus,
|
|
429
|
+
updatedAt: values.updatedAt,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function clearIdleTimer(record) {
|
|
434
|
+
if (!record.idleTimer)
|
|
435
|
+
return;
|
|
436
|
+
clearTimeout(record.idleTimer);
|
|
437
|
+
record.idleTimer = null;
|
|
438
|
+
}
|
|
439
|
+
export function normalizeAdapterManagedRuntimeServices(input) {
|
|
440
|
+
const nowIso = (input.now ?? new Date()).toISOString();
|
|
441
|
+
return input.reports.map((report) => {
|
|
442
|
+
const scopeType = report.scopeType ?? "run";
|
|
443
|
+
const scopeId = report.scopeId ??
|
|
444
|
+
(scopeType === "project_workspace"
|
|
445
|
+
? input.workspace.workspaceId
|
|
446
|
+
: scopeType === "execution_workspace"
|
|
447
|
+
? input.workspace.cwd
|
|
448
|
+
: scopeType === "agent"
|
|
449
|
+
? input.agent.id
|
|
450
|
+
: input.runId) ??
|
|
451
|
+
null;
|
|
452
|
+
const serviceName = asString(report.serviceName, "").trim() || "service";
|
|
453
|
+
const status = report.status ?? "running";
|
|
454
|
+
const lifecycle = report.lifecycle ?? "ephemeral";
|
|
455
|
+
const healthStatus = report.healthStatus ??
|
|
456
|
+
(status === "running" ? "healthy" : status === "failed" ? "unhealthy" : "unknown");
|
|
457
|
+
return {
|
|
458
|
+
id: stableRuntimeServiceId({
|
|
459
|
+
adapterType: input.adapterType,
|
|
460
|
+
runId: input.runId,
|
|
461
|
+
scopeType,
|
|
462
|
+
scopeId,
|
|
463
|
+
serviceName,
|
|
464
|
+
reportId: report.id ?? null,
|
|
465
|
+
providerRef: report.providerRef ?? null,
|
|
466
|
+
reuseKey: report.reuseKey ?? null,
|
|
467
|
+
}),
|
|
468
|
+
companyId: input.agent.companyId,
|
|
469
|
+
projectId: report.projectId ?? input.workspace.projectId,
|
|
470
|
+
projectWorkspaceId: report.projectWorkspaceId ?? input.workspace.workspaceId,
|
|
471
|
+
issueId: report.issueId ?? input.issue?.id ?? null,
|
|
472
|
+
serviceName,
|
|
473
|
+
status,
|
|
474
|
+
lifecycle,
|
|
475
|
+
scopeType,
|
|
476
|
+
scopeId,
|
|
477
|
+
reuseKey: report.reuseKey ?? null,
|
|
478
|
+
command: report.command ?? null,
|
|
479
|
+
cwd: report.cwd ?? null,
|
|
480
|
+
port: report.port ?? null,
|
|
481
|
+
url: report.url ?? null,
|
|
482
|
+
provider: "adapter_managed",
|
|
483
|
+
providerRef: report.providerRef ?? null,
|
|
484
|
+
ownerAgentId: report.ownerAgentId ?? input.agent.id,
|
|
485
|
+
startedByRunId: input.runId,
|
|
486
|
+
lastUsedAt: nowIso,
|
|
487
|
+
startedAt: nowIso,
|
|
488
|
+
stoppedAt: status === "running" || status === "starting" ? null : nowIso,
|
|
489
|
+
stopPolicy: report.stopPolicy ?? null,
|
|
490
|
+
healthStatus,
|
|
491
|
+
reused: false,
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async function startLocalRuntimeService(input) {
|
|
496
|
+
const serviceName = asString(input.service.name, "service");
|
|
497
|
+
const lifecycle = asString(input.service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
|
|
498
|
+
const command = asString(input.service.command, "");
|
|
499
|
+
if (!command)
|
|
500
|
+
throw new Error(`Runtime service "${serviceName}" is missing command`);
|
|
501
|
+
const serviceCwdTemplate = asString(input.service.cwd, ".");
|
|
502
|
+
const portConfig = parseObject(input.service.port);
|
|
503
|
+
const port = asString(portConfig.type, "") === "auto" ? await allocatePort() : null;
|
|
504
|
+
const envConfig = parseObject(input.service.env);
|
|
505
|
+
const templateData = buildTemplateData({
|
|
506
|
+
workspace: input.workspace,
|
|
507
|
+
agent: input.agent,
|
|
508
|
+
issue: input.issue,
|
|
509
|
+
adapterEnv: input.adapterEnv,
|
|
510
|
+
port,
|
|
511
|
+
});
|
|
512
|
+
const serviceCwd = resolveConfiguredPath(renderTemplate(serviceCwdTemplate, templateData), input.workspace.cwd);
|
|
513
|
+
const env = { ...process.env, ...input.adapterEnv };
|
|
514
|
+
for (const [key, value] of Object.entries(envConfig)) {
|
|
515
|
+
if (typeof value === "string") {
|
|
516
|
+
env[key] = renderTemplate(value, templateData);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (port) {
|
|
520
|
+
const portEnvKey = asString(portConfig.envKey, "PORT");
|
|
521
|
+
env[portEnvKey] = String(port);
|
|
522
|
+
}
|
|
523
|
+
const shell = process.env.SHELL?.trim() || "/bin/sh";
|
|
524
|
+
const child = spawn(shell, ["-lc", command], {
|
|
525
|
+
cwd: serviceCwd,
|
|
526
|
+
env,
|
|
527
|
+
detached: false,
|
|
528
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
529
|
+
});
|
|
530
|
+
let stderrExcerpt = "";
|
|
531
|
+
let stdoutExcerpt = "";
|
|
532
|
+
child.stdout?.on("data", async (chunk) => {
|
|
533
|
+
const text = String(chunk);
|
|
534
|
+
stdoutExcerpt = (stdoutExcerpt + text).slice(-4096);
|
|
535
|
+
if (input.onLog)
|
|
536
|
+
await input.onLog("stdout", `[service:${serviceName}] ${text}`);
|
|
537
|
+
});
|
|
538
|
+
child.stderr?.on("data", async (chunk) => {
|
|
539
|
+
const text = String(chunk);
|
|
540
|
+
stderrExcerpt = (stderrExcerpt + text).slice(-4096);
|
|
541
|
+
if (input.onLog)
|
|
542
|
+
await input.onLog("stderr", `[service:${serviceName}] ${text}`);
|
|
543
|
+
});
|
|
544
|
+
const expose = parseObject(input.service.expose);
|
|
545
|
+
const readiness = parseObject(input.service.readiness);
|
|
546
|
+
const urlTemplate = asString(expose.urlTemplate, "") ||
|
|
547
|
+
asString(readiness.urlTemplate, "");
|
|
548
|
+
const url = urlTemplate ? renderTemplate(urlTemplate, templateData) : null;
|
|
549
|
+
try {
|
|
550
|
+
await waitForReadiness({ service: input.service, url });
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
child.kill("SIGTERM");
|
|
554
|
+
throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
|
|
555
|
+
}
|
|
556
|
+
const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
|
|
557
|
+
return {
|
|
558
|
+
id: randomUUID(),
|
|
559
|
+
companyId: input.agent.companyId,
|
|
560
|
+
projectId: input.workspace.projectId,
|
|
561
|
+
projectWorkspaceId: input.workspace.workspaceId,
|
|
562
|
+
issueId: input.issue?.id ?? null,
|
|
563
|
+
serviceName,
|
|
564
|
+
status: "running",
|
|
565
|
+
lifecycle,
|
|
566
|
+
scopeType: input.scopeType,
|
|
567
|
+
scopeId: input.scopeId,
|
|
568
|
+
reuseKey: input.reuseKey,
|
|
569
|
+
command,
|
|
570
|
+
cwd: serviceCwd,
|
|
571
|
+
port,
|
|
572
|
+
url,
|
|
573
|
+
provider: "local_process",
|
|
574
|
+
providerRef: child.pid ? String(child.pid) : null,
|
|
575
|
+
ownerAgentId: input.agent.id,
|
|
576
|
+
startedByRunId: input.runId,
|
|
577
|
+
lastUsedAt: new Date().toISOString(),
|
|
578
|
+
startedAt: new Date().toISOString(),
|
|
579
|
+
stoppedAt: null,
|
|
580
|
+
stopPolicy: parseObject(input.service.stopPolicy),
|
|
581
|
+
healthStatus: "healthy",
|
|
582
|
+
reused: false,
|
|
583
|
+
db: input.db,
|
|
584
|
+
child,
|
|
585
|
+
leaseRunIds: new Set([input.runId]),
|
|
586
|
+
idleTimer: null,
|
|
587
|
+
envFingerprint,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function scheduleIdleStop(record) {
|
|
591
|
+
clearIdleTimer(record);
|
|
592
|
+
const stopType = asString(record.stopPolicy?.type, "manual");
|
|
593
|
+
if (stopType !== "idle_timeout")
|
|
594
|
+
return;
|
|
595
|
+
const idleSeconds = Math.max(1, asNumber(record.stopPolicy?.idleSeconds, 1800));
|
|
596
|
+
record.idleTimer = setTimeout(() => {
|
|
597
|
+
stopRuntimeService(record.id).catch(() => undefined);
|
|
598
|
+
}, idleSeconds * 1000);
|
|
599
|
+
}
|
|
600
|
+
async function stopRuntimeService(serviceId) {
|
|
601
|
+
const record = runtimeServicesById.get(serviceId);
|
|
602
|
+
if (!record)
|
|
603
|
+
return;
|
|
604
|
+
clearIdleTimer(record);
|
|
605
|
+
record.status = "stopped";
|
|
606
|
+
record.lastUsedAt = new Date().toISOString();
|
|
607
|
+
record.stoppedAt = new Date().toISOString();
|
|
608
|
+
if (record.child && !record.child.killed) {
|
|
609
|
+
record.child.kill("SIGTERM");
|
|
610
|
+
}
|
|
611
|
+
runtimeServicesById.delete(serviceId);
|
|
612
|
+
if (record.reuseKey) {
|
|
613
|
+
runtimeServicesByReuseKey.delete(record.reuseKey);
|
|
614
|
+
}
|
|
615
|
+
await persistRuntimeServiceRecord(record.db, record);
|
|
616
|
+
}
|
|
617
|
+
function registerRuntimeService(db, record) {
|
|
618
|
+
record.db = db;
|
|
619
|
+
runtimeServicesById.set(record.id, record);
|
|
620
|
+
if (record.reuseKey) {
|
|
621
|
+
runtimeServicesByReuseKey.set(record.reuseKey, record.id);
|
|
622
|
+
}
|
|
623
|
+
record.child?.on("exit", (code, signal) => {
|
|
624
|
+
const current = runtimeServicesById.get(record.id);
|
|
625
|
+
if (!current)
|
|
626
|
+
return;
|
|
627
|
+
clearIdleTimer(current);
|
|
628
|
+
current.status = code === 0 || signal === "SIGTERM" ? "stopped" : "failed";
|
|
629
|
+
current.healthStatus = current.status === "failed" ? "unhealthy" : "unknown";
|
|
630
|
+
current.lastUsedAt = new Date().toISOString();
|
|
631
|
+
current.stoppedAt = new Date().toISOString();
|
|
632
|
+
runtimeServicesById.delete(current.id);
|
|
633
|
+
if (current.reuseKey && runtimeServicesByReuseKey.get(current.reuseKey) === current.id) {
|
|
634
|
+
runtimeServicesByReuseKey.delete(current.reuseKey);
|
|
635
|
+
}
|
|
636
|
+
void persistRuntimeServiceRecord(db, current);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
export async function ensureRuntimeServicesForRun(input) {
|
|
640
|
+
const runtime = parseObject(input.config.workspaceRuntime);
|
|
641
|
+
const rawServices = Array.isArray(runtime.services)
|
|
642
|
+
? runtime.services.filter((entry) => typeof entry === "object" && entry !== null)
|
|
643
|
+
: [];
|
|
644
|
+
const acquiredServiceIds = [];
|
|
645
|
+
const refs = [];
|
|
646
|
+
runtimeServiceLeasesByRun.set(input.runId, acquiredServiceIds);
|
|
647
|
+
try {
|
|
648
|
+
for (const service of rawServices) {
|
|
649
|
+
const lifecycle = asString(service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
|
|
650
|
+
const { scopeType, scopeId } = resolveServiceScopeId({
|
|
651
|
+
service,
|
|
652
|
+
workspace: input.workspace,
|
|
653
|
+
issue: input.issue,
|
|
654
|
+
runId: input.runId,
|
|
655
|
+
agent: input.agent,
|
|
656
|
+
});
|
|
657
|
+
const envConfig = parseObject(service.env);
|
|
658
|
+
const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
|
|
659
|
+
const serviceName = asString(service.name, "service");
|
|
660
|
+
const reuseKey = lifecycle === "shared"
|
|
661
|
+
? [scopeType, scopeId ?? "", serviceName, envFingerprint].join(":")
|
|
662
|
+
: null;
|
|
663
|
+
if (reuseKey) {
|
|
664
|
+
const existingId = runtimeServicesByReuseKey.get(reuseKey);
|
|
665
|
+
const existing = existingId ? runtimeServicesById.get(existingId) : null;
|
|
666
|
+
if (existing && existing.status === "running") {
|
|
667
|
+
existing.leaseRunIds.add(input.runId);
|
|
668
|
+
existing.lastUsedAt = new Date().toISOString();
|
|
669
|
+
existing.stoppedAt = null;
|
|
670
|
+
clearIdleTimer(existing);
|
|
671
|
+
await persistRuntimeServiceRecord(input.db, existing);
|
|
672
|
+
acquiredServiceIds.push(existing.id);
|
|
673
|
+
refs.push(toRuntimeServiceRef(existing, { reused: true }));
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const record = await startLocalRuntimeService({
|
|
678
|
+
db: input.db,
|
|
679
|
+
runId: input.runId,
|
|
680
|
+
agent: input.agent,
|
|
681
|
+
issue: input.issue,
|
|
682
|
+
workspace: input.workspace,
|
|
683
|
+
adapterEnv: input.adapterEnv,
|
|
684
|
+
service,
|
|
685
|
+
onLog: input.onLog,
|
|
686
|
+
reuseKey,
|
|
687
|
+
scopeType,
|
|
688
|
+
scopeId,
|
|
689
|
+
});
|
|
690
|
+
registerRuntimeService(input.db, record);
|
|
691
|
+
await persistRuntimeServiceRecord(input.db, record);
|
|
692
|
+
acquiredServiceIds.push(record.id);
|
|
693
|
+
refs.push(toRuntimeServiceRef(record));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
await releaseRuntimeServicesForRun(input.runId);
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
return refs;
|
|
701
|
+
}
|
|
702
|
+
export async function releaseRuntimeServicesForRun(runId) {
|
|
703
|
+
const acquired = runtimeServiceLeasesByRun.get(runId) ?? [];
|
|
704
|
+
runtimeServiceLeasesByRun.delete(runId);
|
|
705
|
+
for (const serviceId of acquired) {
|
|
706
|
+
const record = runtimeServicesById.get(serviceId);
|
|
707
|
+
if (!record)
|
|
708
|
+
continue;
|
|
709
|
+
record.leaseRunIds.delete(runId);
|
|
710
|
+
record.lastUsedAt = new Date().toISOString();
|
|
711
|
+
const stopType = asString(record.stopPolicy?.type, record.lifecycle === "ephemeral" ? "on_run_finish" : "manual");
|
|
712
|
+
await persistRuntimeServiceRecord(record.db, record);
|
|
713
|
+
if (record.leaseRunIds.size === 0) {
|
|
714
|
+
if (record.lifecycle === "ephemeral" || stopType === "on_run_finish") {
|
|
715
|
+
await stopRuntimeService(serviceId);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
scheduleIdleStop(record);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
export async function listWorkspaceRuntimeServicesForProjectWorkspaces(db, companyId, projectWorkspaceIds) {
|
|
723
|
+
if (projectWorkspaceIds.length === 0)
|
|
724
|
+
return new Map();
|
|
725
|
+
const rows = await db
|
|
726
|
+
.select()
|
|
727
|
+
.from(workspaceRuntimeServices)
|
|
728
|
+
.where(and(eq(workspaceRuntimeServices.companyId, companyId), inArray(workspaceRuntimeServices.projectWorkspaceId, projectWorkspaceIds)))
|
|
729
|
+
.orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt));
|
|
730
|
+
const grouped = new Map();
|
|
731
|
+
for (const row of rows) {
|
|
732
|
+
if (!row.projectWorkspaceId)
|
|
733
|
+
continue;
|
|
734
|
+
const existing = grouped.get(row.projectWorkspaceId);
|
|
735
|
+
if (existing)
|
|
736
|
+
existing.push(row);
|
|
737
|
+
else
|
|
738
|
+
grouped.set(row.projectWorkspaceId, [row]);
|
|
739
|
+
}
|
|
740
|
+
return grouped;
|
|
741
|
+
}
|
|
742
|
+
export async function reconcilePersistedRuntimeServicesOnStartup(db) {
|
|
743
|
+
const staleRows = await db
|
|
744
|
+
.select({ id: workspaceRuntimeServices.id })
|
|
745
|
+
.from(workspaceRuntimeServices)
|
|
746
|
+
.where(and(eq(workspaceRuntimeServices.provider, "local_process"), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
|
|
747
|
+
if (staleRows.length === 0)
|
|
748
|
+
return { reconciled: 0 };
|
|
749
|
+
const now = new Date();
|
|
750
|
+
await db
|
|
751
|
+
.update(workspaceRuntimeServices)
|
|
752
|
+
.set({
|
|
753
|
+
status: "stopped",
|
|
754
|
+
healthStatus: "unknown",
|
|
755
|
+
stoppedAt: now,
|
|
756
|
+
lastUsedAt: now,
|
|
757
|
+
updatedAt: now,
|
|
758
|
+
})
|
|
759
|
+
.where(and(eq(workspaceRuntimeServices.provider, "local_process"), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
|
|
760
|
+
return { reconciled: staleRows.length };
|
|
761
|
+
}
|
|
762
|
+
export async function persistAdapterManagedRuntimeServices(input) {
|
|
763
|
+
const refs = normalizeAdapterManagedRuntimeServices(input);
|
|
764
|
+
if (refs.length === 0)
|
|
765
|
+
return refs;
|
|
766
|
+
const existingRows = await input.db
|
|
767
|
+
.select()
|
|
768
|
+
.from(workspaceRuntimeServices)
|
|
769
|
+
.where(inArray(workspaceRuntimeServices.id, refs.map((ref) => ref.id)));
|
|
770
|
+
const existingById = new Map(existingRows.map((row) => [row.id, row]));
|
|
771
|
+
for (const ref of refs) {
|
|
772
|
+
const existing = existingById.get(ref.id);
|
|
773
|
+
const startedAt = existing?.startedAt ?? new Date(ref.startedAt);
|
|
774
|
+
const createdAt = existing?.createdAt ?? new Date();
|
|
775
|
+
await input.db
|
|
776
|
+
.insert(workspaceRuntimeServices)
|
|
777
|
+
.values({
|
|
778
|
+
id: ref.id,
|
|
779
|
+
companyId: ref.companyId,
|
|
780
|
+
projectId: ref.projectId,
|
|
781
|
+
projectWorkspaceId: ref.projectWorkspaceId,
|
|
782
|
+
issueId: ref.issueId,
|
|
783
|
+
scopeType: ref.scopeType,
|
|
784
|
+
scopeId: ref.scopeId,
|
|
785
|
+
serviceName: ref.serviceName,
|
|
786
|
+
status: ref.status,
|
|
787
|
+
lifecycle: ref.lifecycle,
|
|
788
|
+
reuseKey: ref.reuseKey,
|
|
789
|
+
command: ref.command,
|
|
790
|
+
cwd: ref.cwd,
|
|
791
|
+
port: ref.port,
|
|
792
|
+
url: ref.url,
|
|
793
|
+
provider: ref.provider,
|
|
794
|
+
providerRef: ref.providerRef,
|
|
795
|
+
ownerAgentId: ref.ownerAgentId,
|
|
796
|
+
startedByRunId: ref.startedByRunId,
|
|
797
|
+
lastUsedAt: new Date(ref.lastUsedAt),
|
|
798
|
+
startedAt,
|
|
799
|
+
stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
|
|
800
|
+
stopPolicy: ref.stopPolicy,
|
|
801
|
+
healthStatus: ref.healthStatus,
|
|
802
|
+
createdAt,
|
|
803
|
+
updatedAt: new Date(),
|
|
804
|
+
})
|
|
805
|
+
.onConflictDoUpdate({
|
|
806
|
+
target: workspaceRuntimeServices.id,
|
|
807
|
+
set: {
|
|
808
|
+
projectId: ref.projectId,
|
|
809
|
+
projectWorkspaceId: ref.projectWorkspaceId,
|
|
810
|
+
issueId: ref.issueId,
|
|
811
|
+
scopeType: ref.scopeType,
|
|
812
|
+
scopeId: ref.scopeId,
|
|
813
|
+
serviceName: ref.serviceName,
|
|
814
|
+
status: ref.status,
|
|
815
|
+
lifecycle: ref.lifecycle,
|
|
816
|
+
reuseKey: ref.reuseKey,
|
|
817
|
+
command: ref.command,
|
|
818
|
+
cwd: ref.cwd,
|
|
819
|
+
port: ref.port,
|
|
820
|
+
url: ref.url,
|
|
821
|
+
provider: ref.provider,
|
|
822
|
+
providerRef: ref.providerRef,
|
|
823
|
+
ownerAgentId: ref.ownerAgentId,
|
|
824
|
+
startedByRunId: ref.startedByRunId,
|
|
825
|
+
lastUsedAt: new Date(ref.lastUsedAt),
|
|
826
|
+
startedAt,
|
|
827
|
+
stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
|
|
828
|
+
stopPolicy: ref.stopPolicy,
|
|
829
|
+
healthStatus: ref.healthStatus,
|
|
830
|
+
updatedAt: new Date(),
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
return refs;
|
|
835
|
+
}
|
|
836
|
+
export function buildWorkspaceReadyComment(input) {
|
|
837
|
+
const lines = ["## Workspace Ready", ""];
|
|
838
|
+
lines.push(`- Strategy: \`${input.workspace.strategy}\``);
|
|
839
|
+
if (input.workspace.branchName)
|
|
840
|
+
lines.push(`- Branch: \`${input.workspace.branchName}\``);
|
|
841
|
+
lines.push(`- CWD: \`${input.workspace.cwd}\``);
|
|
842
|
+
if (input.workspace.worktreePath && input.workspace.worktreePath !== input.workspace.cwd) {
|
|
843
|
+
lines.push(`- Worktree: \`${input.workspace.worktreePath}\``);
|
|
844
|
+
}
|
|
845
|
+
for (const service of input.runtimeServices) {
|
|
846
|
+
const detail = service.url ? `${service.serviceName}: ${service.url}` : `${service.serviceName}: running`;
|
|
847
|
+
const suffix = service.reused ? " (reused)" : "";
|
|
848
|
+
lines.push(`- Service: ${detail}${suffix}`);
|
|
849
|
+
}
|
|
850
|
+
return lines.join("\n");
|
|
851
|
+
}
|
|
852
|
+
//# sourceMappingURL=workspace-runtime.js.map
|