agent-relay-server 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +516 -1
- package/package.json +3 -2
- package/public/assets/MessageBubble-CX63ZV4X.js +2 -0
- package/public/assets/MessageBubble-CX63ZV4X.js.map +1 -0
- package/public/assets/{PlanGraphPanel-CXix1C-H.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
- package/public/assets/{PlanGraphPanel-CXix1C-H.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
- package/public/assets/{activity-CRafQ1Je.js → activity-2xPOScu_.js} +2 -2
- package/public/assets/{activity-CRafQ1Je.js.map → activity-2xPOScu_.js.map} +1 -1
- package/public/assets/{agent-profiles-DW4o5-ND.js → agent-profiles-BfGTTbeY.js} +2 -2
- package/public/assets/{agent-profiles-DW4o5-ND.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
- package/public/assets/agents-Bg3WOBRV.js +2 -0
- package/public/assets/{agents-Cbegv0Hm.js.map → agents-Bg3WOBRV.js.map} +1 -1
- package/public/assets/{analytics-DX6T7JX3.js → analytics-De0cEZGx.js} +3 -3
- package/public/assets/{analytics-DX6T7JX3.js.map → analytics-De0cEZGx.js.map} +1 -1
- package/public/assets/{automation-CzSBv4gg.js → automation-CHxhzq2t.js} +2 -2
- package/public/assets/{automation-CzSBv4gg.js.map → automation-CHxhzq2t.js.map} +1 -1
- package/public/assets/{badge-Cy6qO_Jz.js → badge-DhLyCPag.js} +2 -2
- package/public/assets/{badge-Cy6qO_Jz.js.map → badge-DhLyCPag.js.map} +1 -1
- package/public/assets/branch-state-badge-DSyWKluD.js +2 -0
- package/public/assets/{branch-state-badge-CRA50r81.js.map → branch-state-badge-DSyWKluD.js.map} +1 -1
- package/public/assets/{button-73wpRw65.js → button-K20wAZG1.js} +2 -2
- package/public/assets/{button-73wpRw65.js.map → button-K20wAZG1.js.map} +1 -1
- package/public/assets/card-CcZ9hLI7.js +2 -0
- package/public/assets/{card-BU6PvVHv.js.map → card-CcZ9hLI7.js.map} +1 -1
- package/public/assets/{channels-B1MQCPCT.js → channels-BG0Nib7y.js} +2 -2
- package/public/assets/{channels-B1MQCPCT.js.map → channels-BG0Nib7y.js.map} +1 -1
- package/public/assets/chat-CjP_crSl.js +2 -0
- package/public/assets/{chat-BqIaFYGJ.js.map → chat-CjP_crSl.js.map} +1 -1
- package/public/assets/{connectors-C9aFJIrh.js → connectors-DWh7MwV_.js} +2 -2
- package/public/assets/{connectors-C9aFJIrh.js.map → connectors-DWh7MwV_.js.map} +1 -1
- package/public/assets/{copy-button-HRopUwui.js → copy-button-eF3cKp4Q.js} +2 -2
- package/public/assets/{copy-button-HRopUwui.js.map → copy-button-eF3cKp4Q.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js +3 -0
- package/public/assets/{display-CketawEF.js.map → display-DzNvOu1j.js.map} +1 -1
- package/public/assets/{dist-Kkpnjx_M.js → dist-1dhfdhm9.js} +2 -2
- package/public/assets/{dist-Kkpnjx_M.js.map → dist-1dhfdhm9.js.map} +1 -1
- package/public/assets/dist-BLBF7qqr.js +2 -0
- package/public/assets/{dist-De2oguBm.js.map → dist-BLBF7qqr.js.map} +1 -1
- package/public/assets/{dist-T7u7IZGJ.js → dist-BrQsqBbc.js} +2 -2
- package/public/assets/{dist-T7u7IZGJ.js.map → dist-BrQsqBbc.js.map} +1 -1
- package/public/assets/{dist-Cko2KPW8.js → dist-COQuunEv.js} +2 -2
- package/public/assets/{dist-Cko2KPW8.js.map → dist-COQuunEv.js.map} +1 -1
- package/public/assets/{dist-CLHMeXYi.js → dist-Ci5Y37nj.js} +2 -2
- package/public/assets/{dist-CLHMeXYi.js.map → dist-Ci5Y37nj.js.map} +1 -1
- package/public/assets/{dist-Ci1MM51P.js → dist-CoROafNg.js} +2 -2
- package/public/assets/{dist-Ci1MM51P.js.map → dist-CoROafNg.js.map} +1 -1
- package/public/assets/{dist-CC5f7oYw.js → dist-I-MFUv51.js} +2 -2
- package/public/assets/{dist-CC5f7oYw.js.map → dist-I-MFUv51.js.map} +1 -1
- package/public/assets/{dist-DFLciw0h.js → dist-UQGKwqZH.js} +2 -2
- package/public/assets/{dist-DFLciw0h.js.map → dist-UQGKwqZH.js.map} +1 -1
- package/public/assets/dist-YpHfnnIA.js +2 -0
- package/public/assets/{dist-DUoZnlB2.js.map → dist-YpHfnnIA.js.map} +1 -1
- package/public/assets/dist-qd198ib9.js +2 -0
- package/public/assets/{dist-D41tzJYg.js.map → dist-qd198ib9.js.map} +1 -1
- package/public/assets/{dist-DmN94rc8.js → dist-yGOq6sB3.js} +2 -2
- package/public/assets/{dist-DmN94rc8.js.map → dist-yGOq6sB3.js.map} +1 -1
- package/public/assets/{es2015-Df08Opjx.js → es2015-CRxz9lOj.js} +2 -2
- package/public/assets/{es2015-Df08Opjx.js.map → es2015-CRxz9lOj.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DOXH7oDF.js → formatted-body-impl-u0W_hWYA.js} +2 -2
- package/public/assets/{formatted-body-impl-DOXH7oDF.js.map → formatted-body-impl-u0W_hWYA.js.map} +1 -1
- package/public/assets/index-BwfVOzUG.js +21 -0
- package/public/assets/{index-br_Y6Cvt.js.map → index-BwfVOzUG.js.map} +1 -1
- package/public/assets/index-Dn_GDXbi.css +2 -0
- package/public/assets/{input-B2EDz-BE.js → input-BHYNpXAG.js} +2 -2
- package/public/assets/{input-B2EDz-BE.js.map → input-BHYNpXAG.js.map} +1 -1
- package/public/assets/{insights-B1edq5lk.js → insights-CAF9Hi5D.js} +2 -2
- package/public/assets/{insights-B1edq5lk.js.map → insights-CAF9Hi5D.js.map} +1 -1
- package/public/assets/{integrations-Cm4fjp7_.js → integrations-CG9qyHUf.js} +2 -2
- package/public/assets/{integrations-Cm4fjp7_.js.map → integrations-CG9qyHUf.js.map} +1 -1
- package/public/assets/{lib-BZrjIp-o.js → lib-Df4aph5u.js} +2 -2
- package/public/assets/{lib-BZrjIp-o.js.map → lib-Df4aph5u.js.map} +1 -1
- package/public/assets/lucide-react-L2UxFDic.js +9 -0
- package/public/assets/lucide-react-L2UxFDic.js.map +1 -0
- package/public/assets/{maintenance-D6zvw0fU.js → maintenance-DgHA35ve.js} +2 -2
- package/public/assets/{maintenance-D6zvw0fU.js.map → maintenance-DgHA35ve.js.map} +1 -1
- package/public/assets/{managed-agents-CzfEznM1.js → managed-agents-Rk6ghKR9.js} +2 -2
- package/public/assets/{managed-agents-CzfEznM1.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DZlCaTHQ.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
- package/public/assets/{markdown-preview-impl-DZlCaTHQ.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
- package/public/assets/memory-DamwWa9A.js +2 -0
- package/public/assets/{memory-CUUAzJDc.js.map → memory-DamwWa9A.js.map} +1 -1
- package/public/assets/{messages-6QZH46LG.js → messages--NU-YIL6.js} +2 -2
- package/public/assets/{messages-6QZH46LG.js.map → messages--NU-YIL6.js.map} +1 -1
- package/public/assets/{orchestrators-CrcqkPuc.js → orchestrators-DQnIn6qx.js} +2 -2
- package/public/assets/{orchestrators-CrcqkPuc.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
- package/public/assets/{overview-418xXZO_.js → overview-DCmNgXDf.js} +2 -2
- package/public/assets/{overview-418xXZO_.js.map → overview-DCmNgXDf.js.map} +1 -1
- package/public/assets/{pairs-CnedATgT.js → pairs-DNELeC9P.js} +2 -2
- package/public/assets/{pairs-CnedATgT.js.map → pairs-DNELeC9P.js.map} +1 -1
- package/public/assets/{react-dom-WI6TjVe7.js → react-dom-CrP_AtcL.js} +2 -2
- package/public/assets/{react-dom-WI6TjVe7.js.map → react-dom-CrP_AtcL.js.map} +1 -1
- package/public/assets/{security-DS13pbag.js → security-Dd3Kh0oC.js} +2 -2
- package/public/assets/{security-DS13pbag.js.map → security-Dd3Kh0oC.js.map} +1 -1
- package/public/assets/{select-BOprGZDS.js → select-DQbeJZX0.js} +2 -2
- package/public/assets/{select-BOprGZDS.js.map → select-DQbeJZX0.js.map} +1 -1
- package/public/assets/settings-BMAJrBIH.js +4 -0
- package/public/assets/settings-BMAJrBIH.js.map +1 -0
- package/public/assets/store-B8xP6z6T.js +9 -0
- package/public/assets/store-B8xP6z6T.js.map +1 -0
- package/public/assets/tasks-BYR4L9GP.js +2 -0
- package/public/assets/{tasks-B-4oCPuv.js.map → tasks-BYR4L9GP.js.map} +1 -1
- package/public/assets/teams-CoTifrq_.js +2 -0
- package/public/assets/{teams-sCQAs_QA.js.map → teams-CoTifrq_.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BhqS2XpS.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BhqS2XpS.js.map → terminal-viewer-impl-B1YzfpE6.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-fOL_3RHB.js → use-keyboard-viewport-DrRH_QzG.js} +2 -2
- package/public/assets/{use-keyboard-viewport-fOL_3RHB.js.map → use-keyboard-viewport-DrRH_QzG.js.map} +1 -1
- package/public/assets/{work-queue-BDhjlwUH.js → work-queue-x57vlgyZ.js} +2 -2
- package/public/assets/{work-queue-BDhjlwUH.js.map → work-queue-x57vlgyZ.js.map} +1 -1
- package/public/assets/{workspaces-6I-C69nf.js → workspaces-CYm5bLg9.js} +3 -3
- package/public/assets/{workspaces-6I-C69nf.js.map → workspaces-CYm5bLg9.js.map} +1 -1
- package/public/index.html +22 -22
- package/runner/src/config.ts +66 -2
- package/src/automations.ts +3 -4
- package/src/cli/index.ts +11 -0
- package/src/cli/merges.ts +89 -0
- package/src/config-store.ts +5 -6
- package/src/config.ts +4 -0
- package/src/connectors.ts +5 -0
- package/src/db/merge-lease.ts +95 -1
- package/src/db/migrations.ts +21 -0
- package/src/db/schema.ts +1 -1
- package/src/index.ts +5 -2
- package/src/maintenance.ts +8 -8
- package/src/mcp-merge-tools.ts +66 -0
- package/src/mcp-provider-catalog.ts +9 -0
- package/src/mcp.ts +7 -8
- package/src/merge-pause-maintenance.ts +13 -0
- package/src/provider-catalog-store.ts +33 -4
- package/src/provider-config-store.ts +216 -0
- package/src/recipe-runner.ts +2 -1
- package/src/recipe-validator.ts +4 -3
- package/src/routes/agent-sessions.ts +4 -2
- package/src/routes/agents-spawn.ts +2 -2
- package/src/routes/commands.ts +20 -0
- package/src/routes/index.ts +19 -2
- package/src/routes/insights.ts +4 -3
- package/src/routes/merges.ts +95 -0
- package/src/routes/provider-config.ts +89 -41
- package/src/routes/settings.ts +127 -0
- package/src/routes/steward.ts +5 -5
- package/src/routes/tokens.ts +32 -1
- package/src/security.ts +36 -37
- package/src/services/merge-pause.ts +37 -0
- package/src/services/spawn-agent.ts +10 -8
- package/src/settings/modules.ts +149 -0
- package/src/settings/registry.ts +397 -0
- package/src/spawn-command.ts +5 -1
- package/src/team-template-config.ts +2 -2
- package/src/token-db.ts +20 -0
- package/src/workspace-pr-completion.ts +70 -0
- package/public/assets/MessageBubble-BrEKymoV.js +0 -2
- package/public/assets/MessageBubble-BrEKymoV.js.map +0 -1
- package/public/assets/agents-Cbegv0Hm.js +0 -2
- package/public/assets/branch-state-badge-CRA50r81.js +0 -2
- package/public/assets/card-BU6PvVHv.js +0 -2
- package/public/assets/chat-BqIaFYGJ.js +0 -2
- package/public/assets/display-CketawEF.js +0 -3
- package/public/assets/dist-D41tzJYg.js +0 -2
- package/public/assets/dist-DUoZnlB2.js +0 -2
- package/public/assets/dist-De2oguBm.js +0 -2
- package/public/assets/index-DaNOQJci.css +0 -2
- package/public/assets/index-br_Y6Cvt.js +0 -21
- package/public/assets/lucide-react-H_-8G79A.js +0 -9
- package/public/assets/lucide-react-H_-8G79A.js.map +0 -1
- package/public/assets/memory-CUUAzJDc.js +0 -2
- package/public/assets/settings-BgInn_f3.js +0 -10
- package/public/assets/settings-BgInn_f3.js.map +0 -1
- package/public/assets/store-DHO_a8BD.js +0 -9
- package/public/assets/store-DHO_a8BD.js.map +0 -1
- package/public/assets/tasks-B-4oCPuv.js +0 -2
- package/public/assets/teams-sCQAs_QA.js +0 -2
package/runner/src/config.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import { DEFAULT_RELAY_URL, stringValue } from "agent-relay-sdk";
|
|
6
|
-
import type { WorkspaceMetadata } from "agent-relay-sdk";
|
|
5
|
+
import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "agent-relay-sdk";
|
|
6
|
+
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
8
|
import type { ProviderConfig } from "./adapter";
|
|
9
9
|
|
|
@@ -36,6 +36,7 @@ export function defaultProviderConfig(provider: string): ProviderConfig {
|
|
|
36
36
|
defaultApprovalMode: "guarded",
|
|
37
37
|
defaultTags: [],
|
|
38
38
|
chatCaptureMode: "final",
|
|
39
|
+
reasoningCapture: true,
|
|
39
40
|
headless: {
|
|
40
41
|
tmuxPrefix: `${provider}-relay`,
|
|
41
42
|
shutdownTimeoutMs: 10_000,
|
|
@@ -67,6 +68,7 @@ export function loadProviderConfig(provider: string, home = agentRelayHome()): L
|
|
|
67
68
|
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
68
69
|
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
69
70
|
chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
|
|
71
|
+
reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
|
|
70
72
|
headless: {
|
|
71
73
|
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
72
74
|
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
@@ -74,6 +76,24 @@ export function loadProviderConfig(provider: string, home = agentRelayHome()): L
|
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
export async function loadProviderConfigFromRelay(
|
|
80
|
+
provider: string,
|
|
81
|
+
opts: { relayUrl: string; token?: string; home?: string; host?: string },
|
|
82
|
+
): Promise<LoadedProviderConfig> {
|
|
83
|
+
const host = opts.host ?? hostname();
|
|
84
|
+
const home = opts.home ?? agentRelayHome();
|
|
85
|
+
const local = loadProviderConfig(provider, home);
|
|
86
|
+
const key = `${host}/${provider}`;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const fetched = await fetchProviderConfig(provider, opts.relayUrl, opts.token, key);
|
|
90
|
+
if (fetched) return fetched;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`[agent-relay] provider config relay read failed (${errMessage(error)}); using local fallback ${local.path}.`);
|
|
93
|
+
}
|
|
94
|
+
return local;
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
export function writeProviderConfig(provider: string, config: ProviderConfig, home = agentRelayHome()): LoadedProviderConfig {
|
|
78
98
|
const dir = providersDir(home);
|
|
79
99
|
mkdirSync(dir, { recursive: true });
|
|
@@ -93,6 +113,7 @@ export function providerConfigPublic(config: LoadedProviderConfig): Record<strin
|
|
|
93
113
|
defaultApprovalMode: config.defaultApprovalMode,
|
|
94
114
|
defaultTags: config.defaultTags,
|
|
95
115
|
chatCaptureMode: config.chatCaptureMode,
|
|
116
|
+
reasoningCapture: config.reasoningCapture,
|
|
96
117
|
headless: config.headless,
|
|
97
118
|
};
|
|
98
119
|
}
|
|
@@ -156,6 +177,10 @@ function enumValue<T extends string>(value: unknown, allowed: T[]): T | undefine
|
|
|
156
177
|
return typeof value === "string" && allowed.includes(value as T) ? (value as T) : undefined;
|
|
157
178
|
}
|
|
158
179
|
|
|
180
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
181
|
+
return typeof value === "boolean" ? value : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
function stringArray(value: unknown): string[] | undefined {
|
|
160
185
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
|
|
161
186
|
}
|
|
@@ -178,3 +203,42 @@ function maskEnv(env: Record<string, string>): Record<string, string> {
|
|
|
178
203
|
}
|
|
179
204
|
return result;
|
|
180
205
|
}
|
|
206
|
+
|
|
207
|
+
async function fetchProviderConfig(
|
|
208
|
+
provider: string,
|
|
209
|
+
relayUrl: string,
|
|
210
|
+
token: string | undefined,
|
|
211
|
+
key: string,
|
|
212
|
+
): Promise<LoadedProviderConfig | null> {
|
|
213
|
+
const url = new URL(`/api/settings/provider-config/${encodeURIComponent(key)}`, relayUrl);
|
|
214
|
+
const res = await fetch(url, {
|
|
215
|
+
headers: token ? { [RELAY_TOKEN_HEADER]: token } : undefined,
|
|
216
|
+
signal: AbortSignal.timeout(5_000),
|
|
217
|
+
});
|
|
218
|
+
if (res.status === 404) return null;
|
|
219
|
+
const body = await res.json().catch(() => null) as (Partial<SettingEntry> & { error?: string }) | null;
|
|
220
|
+
if (!res.ok) throw new Error(body?.error ? `${res.status}: ${body.error}` : `HTTP ${res.status}`);
|
|
221
|
+
if (body?.version === 0) return null;
|
|
222
|
+
return providerConfigFromValue(provider, body?.value, `config:provider-config/${key}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function providerConfigFromValue(provider: string, value: unknown, path: string): LoadedProviderConfig {
|
|
226
|
+
const raw = recordValue(value);
|
|
227
|
+
const defaults = defaultProviderConfig(provider);
|
|
228
|
+
return {
|
|
229
|
+
path,
|
|
230
|
+
command: stringValue(raw.command) ?? defaults.command,
|
|
231
|
+
defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
|
|
232
|
+
env: stringRecord(raw.env) ?? defaults.env,
|
|
233
|
+
pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
|
|
234
|
+
defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
|
|
235
|
+
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
236
|
+
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
237
|
+
chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
|
|
238
|
+
reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
|
|
239
|
+
headless: {
|
|
240
|
+
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
241
|
+
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
package/src/automations.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { createCommand } from "./commands-db";
|
|
|
16
16
|
import { cleanEnum, cleanString, cleanStringArray, optionalEnum } from "./validation";
|
|
17
17
|
import { getAgentProfile, getSpawnPolicy } from "./config-store";
|
|
18
18
|
import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
|
|
19
|
-
import { resolveProviderSelection, type ProviderEffort
|
|
19
|
+
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
20
20
|
import { errMessage, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
21
21
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
22
22
|
import type {
|
|
@@ -38,7 +38,6 @@ import type {
|
|
|
38
38
|
TaskSeverity,
|
|
39
39
|
UpdateAutomationInput,
|
|
40
40
|
} from "./types";
|
|
41
|
-
|
|
42
41
|
const AUTOMATION_KIND = "scheduled_task";
|
|
43
42
|
const DEFAULT_TIMEZONE = "UTC";
|
|
44
43
|
const DEFAULT_CATCH_UP: AutomationCatchUpPolicy = "skip";
|
|
@@ -200,11 +199,11 @@ function normalizeTargetPolicy(value: unknown): AutomationTargetPolicy {
|
|
|
200
199
|
}
|
|
201
200
|
const provider = optionalEnum(input.provider, "targetPolicy.provider", ["claude", "codex"] as const, "codex");
|
|
202
201
|
const model = cleanString(input.model, "targetPolicy.model", { max: 120 });
|
|
203
|
-
const effort =
|
|
202
|
+
const effort = cleanString(input.effort, "targetPolicy.effort", { max: 120 }) as ProviderEffort | undefined;
|
|
204
203
|
const profile = cleanString(input.profile, "targetPolicy.profile", { max: 120 });
|
|
205
204
|
if (profile && !getAgentProfile(profile)) throw new ValidationError("agent profile not found");
|
|
206
205
|
try {
|
|
207
|
-
resolveProviderSelection({ provider, model, effort });
|
|
206
|
+
resolveProviderSelection({ provider, model, effort }, effectiveProviderCatalogList());
|
|
208
207
|
} catch (error) {
|
|
209
208
|
throw new ValidationError(errMessage(error));
|
|
210
209
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { handleDevCommand } from "./dev";
|
|
|
14
14
|
import { handleMemoryCommand } from "./memory";
|
|
15
15
|
import { handleSlashOrPairCommand } from "./pair";
|
|
16
16
|
import { handleWorkspaceCommand } from "./workspace";
|
|
17
|
+
import { handleMergesCommand } from "./merges";
|
|
17
18
|
import { handleStewardCommand } from "./steward";
|
|
18
19
|
import { handleMessageCommand, handleGetMessageCommand, handleReactCommand } from "./message";
|
|
19
20
|
import { handleReplyCommand } from "./reply";
|
|
@@ -44,6 +45,7 @@ Usage:
|
|
|
44
45
|
agent-relay context-probe [print-status-line] [--wrap COMMAND] [--agent-id ID] [--state-dir DIR] [--standalone]
|
|
45
46
|
agent-relay token <create|list|revoke|verify> [options]
|
|
46
47
|
agent-relay pair <target|create|status|accept|reject|hangup|send> [options]
|
|
48
|
+
agent-relay merges <pause|resume|renew|list> [--repo PATH] [--base BRANCH] [--ttl 3m|--ttl-ms N] [--reason TEXT] [--holder TEXT] [--id ID] [--json]
|
|
47
49
|
agent-relay workspace <status|diagnostics|ready|land|claim|release|list|cleanup-stale|deps> [--id ID] [--strategy ...] [--auto-merge on-green|on-approval|manual] [--reviewer HANDLE] [--purpose TEXT] [--repo PATH] [--wait] [--timeout SECONDS] [--check] [--execute] [--json]
|
|
48
50
|
agent-relay steward <queue|inspect|checks> [WORKSPACE_ID] [--repo PATH] [--json]
|
|
49
51
|
agent-relay message <target> <body> [options]
|
|
@@ -141,6 +143,11 @@ Token examples:
|
|
|
141
143
|
agent-relay token list
|
|
142
144
|
agent-relay token revoke JTI
|
|
143
145
|
|
|
146
|
+
Merge pause examples:
|
|
147
|
+
agent-relay merges pause --repo /repo --reason release --ttl 5m
|
|
148
|
+
agent-relay merges renew --id PAUSE_ID --ttl 5m
|
|
149
|
+
agent-relay merges resume --id PAUSE_ID
|
|
150
|
+
|
|
144
151
|
Context probe examples:
|
|
145
152
|
agent-relay context-probe print-status-line --wrap 'ccstatusline'
|
|
146
153
|
agent-relay context-probe --wrap 'ccstatusline'
|
|
@@ -345,6 +352,10 @@ export async function handleCli(args: string[]): Promise<"start" | "handled"> {
|
|
|
345
352
|
await handleTagsCommand(args.slice(1));
|
|
346
353
|
return "handled";
|
|
347
354
|
}
|
|
355
|
+
if (command === "merges" || command === "merge") {
|
|
356
|
+
await handleMergesCommand(args.slice(1));
|
|
357
|
+
return "handled";
|
|
358
|
+
}
|
|
348
359
|
if (command === "workspace" || command === "workspaces") {
|
|
349
360
|
await handleWorkspaceCommand(args.slice(1));
|
|
350
361
|
return "handled";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Merge coordination CLI. Generic pause/resume lease for relay-owned auto-merge
|
|
2
|
+
// sweeps; useful for release/build critical sections (#448).
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { apiRequest } from "./_shared";
|
|
5
|
+
|
|
6
|
+
const MERGES_USAGE = "Usage: agent-relay merges <pause|resume|renew|list> [--repo PATH] [--base BRANCH] [--ttl 3m|--ttl-ms N] [--reason TEXT] [--holder TEXT] [--id ID] [--json]";
|
|
7
|
+
|
|
8
|
+
function gitRoot(): string | undefined {
|
|
9
|
+
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: "pipe" });
|
|
10
|
+
return r.status === 0 ? r.stdout.trim() : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseDurationMs(raw: string): number {
|
|
14
|
+
const match = raw.trim().match(/^(\d+)(ms|s|m|h)?$/);
|
|
15
|
+
if (!match) throw new Error("--ttl must be a positive duration like 180s, 3m, 1h, or use --ttl-ms");
|
|
16
|
+
const value = Number(match[1]);
|
|
17
|
+
const unit = match[2] ?? "ms";
|
|
18
|
+
const multiplier = unit === "h" ? 60 * 60_000 : unit === "m" ? 60_000 : unit === "s" ? 1000 : 1;
|
|
19
|
+
const ms = value * multiplier;
|
|
20
|
+
if (!Number.isSafeInteger(ms) || ms <= 0) throw new Error("--ttl is out of range");
|
|
21
|
+
return ms;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatPause(pause: any): string {
|
|
25
|
+
const until = new Date(pause.expiresAt).toISOString();
|
|
26
|
+
return `Merge pause ${pause.id}: ${pause.repoRoot}#${pause.baseRef} held by ${pause.holder} until ${until}${pause.reason ? ` (${pause.reason})` : ""}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleMergesCommand(args: string[]): Promise<void> {
|
|
30
|
+
const action = args[0];
|
|
31
|
+
if (action === "--help" || action === "-h" || action === "help") {
|
|
32
|
+
console.log(MERGES_USAGE);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!action || !["pause", "resume", "renew", "list"].includes(action)) throw new Error(MERGES_USAGE);
|
|
36
|
+
|
|
37
|
+
let repoRoot: string | undefined;
|
|
38
|
+
let baseRef = "main";
|
|
39
|
+
let ttlMs: number | undefined;
|
|
40
|
+
let reason: string | undefined;
|
|
41
|
+
let holder: string | undefined;
|
|
42
|
+
let id: string | undefined;
|
|
43
|
+
let json = false;
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i < args.length; i++) {
|
|
46
|
+
const arg = args[i];
|
|
47
|
+
if (arg === "--repo" && i + 1 < args.length) repoRoot = args[++i];
|
|
48
|
+
else if (arg === "--base" && i + 1 < args.length) baseRef = args[++i]!;
|
|
49
|
+
else if (arg === "--ttl" && i + 1 < args.length) ttlMs = parseDurationMs(args[++i]!);
|
|
50
|
+
else if (arg === "--ttl-ms" && i + 1 < args.length) {
|
|
51
|
+
ttlMs = Number.parseInt(args[++i]!, 10);
|
|
52
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new Error("--ttl-ms must be a positive integer");
|
|
53
|
+
}
|
|
54
|
+
else if (arg === "--reason" && i + 1 < args.length) reason = args[++i];
|
|
55
|
+
else if (arg === "--holder" && i + 1 < args.length) holder = args[++i];
|
|
56
|
+
else if (arg === "--id" && i + 1 < args.length) id = args[++i];
|
|
57
|
+
else if (arg === "--json") json = true;
|
|
58
|
+
else throw new Error(`Unknown merges option "${arg}".`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (action === "list") {
|
|
62
|
+
const result = await apiRequest("GET", "/api/merges/pauses");
|
|
63
|
+
console.log(JSON.stringify(result, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (action === "renew") {
|
|
68
|
+
if (!id) throw new Error("merges renew requires --id");
|
|
69
|
+
const result = await apiRequest("POST", "/api/merges/renew", { id, ttlMs });
|
|
70
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
else console.log(formatPause((result as any).pause));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
repoRoot = repoRoot ?? gitRoot();
|
|
76
|
+
if (action === "pause" && !repoRoot) throw new Error("No git repo detected. Pass --repo PATH.");
|
|
77
|
+
if (action === "resume" && !id && !repoRoot) throw new Error("No git repo detected. Pass --id ID or --repo PATH.");
|
|
78
|
+
|
|
79
|
+
if (action === "pause") {
|
|
80
|
+
const result = await apiRequest("POST", "/api/merges/pause", { repoRoot, baseRef, ttlMs, reason, holder });
|
|
81
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
82
|
+
else console.log(formatPause((result as any).pause));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await apiRequest("POST", "/api/merges/resume", id ? { id } : { repoRoot, baseRef });
|
|
87
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
88
|
+
else console.log((result as any).released ? "Merge pause released." : "No matching merge pause was active.");
|
|
89
|
+
}
|
package/src/config-store.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { DEFAULT_CONTEXT_THRESHOLD_CONFIG, validateContextThresholdConfig } from
|
|
|
4
4
|
import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-resume-config";
|
|
5
5
|
import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
|
|
6
6
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
7
|
-
import { errMessage, isRecord, SPAWN_PROVIDERS,
|
|
7
|
+
import { errMessage, isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES } from "agent-relay-sdk"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
8
8
|
import type {
|
|
9
9
|
AgentProfile,
|
|
10
10
|
AgentProfileBase,
|
|
@@ -23,7 +23,6 @@ import type {
|
|
|
23
23
|
WorkspaceConfig,
|
|
24
24
|
WorkspaceLandingPolicy,
|
|
25
25
|
} from "./types";
|
|
26
|
-
|
|
27
26
|
const CONFIG_HISTORY_LIMIT = 50;
|
|
28
27
|
const SPAWN_POLICY_NAMESPACE = "spawn-policy";
|
|
29
28
|
const AGENT_PROFILE_NAMESPACE = "agent-profile";
|
|
@@ -342,7 +341,7 @@ function validateSpawnPolicy(key: string, value: unknown): SpawnPolicy {
|
|
|
342
341
|
workspaceMode: value.workspaceMode === undefined || value.workspaceMode === null ? "inherit" : cleanEnum(value.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES),
|
|
343
342
|
rig: cleanString(value.rig, "rig", { max: 120 }),
|
|
344
343
|
model: cleanString(value.model, "model", { max: 120 }),
|
|
345
|
-
effort:
|
|
344
|
+
effort: cleanString(value.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
|
|
346
345
|
profile: cleanString(value.profile, "profile", { max: 120 }),
|
|
347
346
|
providerArgs: cleanStringArray(value.providerArgs, "providerArgs", { itemMax: 120, maxItems: 100 }) ?? [],
|
|
348
347
|
prompt: cleanString(value.prompt, "prompt", { max: 16_000 }),
|
|
@@ -356,7 +355,7 @@ function validateSpawnPolicy(key: string, value: unknown): SpawnPolicy {
|
|
|
356
355
|
backoff: cleanBackoff(value.backoff),
|
|
357
356
|
};
|
|
358
357
|
try {
|
|
359
|
-
resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort });
|
|
358
|
+
resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort }, effectiveProviderCatalogList());
|
|
360
359
|
} catch (error) {
|
|
361
360
|
throw new ValidationError(errMessage(error));
|
|
362
361
|
}
|
|
@@ -408,7 +407,7 @@ function validateStewardConfig(value: unknown): StewardConfig {
|
|
|
408
407
|
enabled: value.enabled === undefined ? false : cleanBoolean(value.enabled, "enabled"),
|
|
409
408
|
provider: cleanEnum(value.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider,
|
|
410
409
|
model: cleanString(value.model, "model", { max: 120 }),
|
|
411
|
-
effort:
|
|
410
|
+
effort: cleanString(value.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
|
|
412
411
|
permissionMode: (value.permissionMode === undefined || value.permissionMode === null
|
|
413
412
|
? "open"
|
|
414
413
|
: cleanEnum(value.permissionMode, "permissionMode", VALID_PERMISSION_MODES)) as SpawnApprovalMode,
|
|
@@ -419,7 +418,7 @@ function validateStewardConfig(value: unknown): StewardConfig {
|
|
|
419
418
|
// Reject a provider/model/effort combo the catalog can't resolve before it ever
|
|
420
419
|
// reaches a spawn (same guard as spawn policies).
|
|
421
420
|
try {
|
|
422
|
-
resolveProviderSelection({ provider: config.provider, model: config.model, effort: config.effort });
|
|
421
|
+
resolveProviderSelection({ provider: config.provider, model: config.model, effort: config.effort }, effectiveProviderCatalogList());
|
|
423
422
|
} catch (error) {
|
|
424
423
|
throw new ValidationError(errMessage(error));
|
|
425
424
|
}
|
package/src/config.ts
CHANGED
|
@@ -33,6 +33,10 @@ export const POOL_CLAIM_LEASE_MS = envPositiveInt("AGENT_RELAY_POOL_CLAIM_LEASE_
|
|
|
33
33
|
// repo. Held from when a workspace.merge command is dispatched until it settles
|
|
34
34
|
// (or this TTL expires, in case the orchestrator never reports back).
|
|
35
35
|
export const WORKSPACE_MERGE_LEASE_MS = envPositiveInt("AGENT_RELAY_WORKSPACE_MERGE_LEASE_MS", 900_000); // 15min
|
|
36
|
+
// Opt-in release/build critical-section pause for relay-owned auto-merge sweeps.
|
|
37
|
+
// This narrows the race window while the release script creates and pushes a tag;
|
|
38
|
+
// expiry is the safety net so a killed holder cannot wedge the repo.
|
|
39
|
+
export const MAIN_MERGE_PAUSE_TTL_MS = envPositiveInt("AGENT_RELAY_MAIN_MERGE_PAUSE_TTL_MS", 5 * 60_000); // 5min
|
|
36
40
|
|
|
37
41
|
// Max body size for any POST/PATCH request (64 KiB).
|
|
38
42
|
export const MAX_BODY_BYTES = 64 * 1024;
|
package/src/connectors.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import type { ConnectorAction, ConnectorActionResult, ConnectorManifest, ConnectorSummary } from "./types";
|
|
5
5
|
import { ValidationError } from "./db";
|
|
6
6
|
import { isRecord } from "agent-relay-sdk";
|
|
7
|
+
import { registerConnectorConfigSetting, validateConnectorConfig } from "./settings/modules";
|
|
7
8
|
|
|
8
9
|
const CONNECTOR_SCHEMA = "agent-relay.connector.v1";
|
|
9
10
|
const VALID_KINDS = new Set(["channel", "event", "provider", "orchestrator"]);
|
|
@@ -99,6 +100,7 @@ function stringArray(value: unknown, field: string): string[] {
|
|
|
99
100
|
|
|
100
101
|
export function registerConnectorManifest(manifest: ConnectorManifest, opts: { config?: Record<string, unknown>; state?: Record<string, unknown> } = {}): ConnectorSummary {
|
|
101
102
|
const normalized = normalizeConnectorManifest(manifest);
|
|
103
|
+
registerConnectorConfigSetting(normalized, { defaults: opts.config });
|
|
102
104
|
const dir = connectorDir(normalized.id);
|
|
103
105
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
104
106
|
writeFileSync(join(dir, "manifest.json"), JSON.stringify(normalized, null, 2) + "\n", { mode: 0o600 });
|
|
@@ -165,6 +167,9 @@ export function readConnectorConfig(id: string): Record<string, unknown> {
|
|
|
165
167
|
|
|
166
168
|
export function writeConnectorConfig(id: string, config: Record<string, unknown>): Record<string, unknown> {
|
|
167
169
|
if (!isRecord(config)) throw new ValidationError("connector config must be an object");
|
|
170
|
+
const connector = getConnector(id);
|
|
171
|
+
if (!connector) throw new ValidationError(`connector ${id} not found`);
|
|
172
|
+
validateConnectorConfig(connector.manifest, config);
|
|
168
173
|
const path = join(connectorDir(id), "config.json");
|
|
169
174
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
170
175
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
package/src/db/merge-lease.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
parseRuntimePackage,
|
|
13
13
|
type RuntimeContracts,
|
|
14
14
|
} from "../contracts";
|
|
15
|
-
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
15
|
+
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS, MAIN_MERGE_PAUSE_TTL_MS } from "../config";
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { getDb } from "./connection.ts";
|
|
18
18
|
import type {
|
|
@@ -158,3 +158,97 @@ export function releaseMergeLease(opts: { repoRoot?: string; commandId?: string;
|
|
|
158
158
|
return getDb().query(`DELETE FROM workspace_merge_leases WHERE ${where.join(" AND ")}`).run(...params).changes > 0;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
// --- Per-target merge pause lease (issue #448) -----------------------------
|
|
162
|
+
|
|
163
|
+
export interface MergePauseRecord {
|
|
164
|
+
id: string;
|
|
165
|
+
repoRoot: string;
|
|
166
|
+
baseRef: string;
|
|
167
|
+
holder: string;
|
|
168
|
+
reason?: string;
|
|
169
|
+
createdAt: number;
|
|
170
|
+
updatedAt: number;
|
|
171
|
+
expiresAt: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function rowToMergePause(row: any): MergePauseRecord {
|
|
175
|
+
return {
|
|
176
|
+
id: row.id,
|
|
177
|
+
repoRoot: row.repo_root,
|
|
178
|
+
baseRef: row.base_ref,
|
|
179
|
+
holder: row.holder,
|
|
180
|
+
reason: row.reason ?? undefined,
|
|
181
|
+
createdAt: row.created_at,
|
|
182
|
+
updatedAt: row.updated_at,
|
|
183
|
+
expiresAt: row.expires_at,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getMergePause(id: string): MergePauseRecord | null {
|
|
188
|
+
const row = getDb().query("SELECT * FROM workspace_merge_pauses WHERE id = ?").get(id) as any;
|
|
189
|
+
return row ? rowToMergePause(row) : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getActiveMergePause(repoRoot: string, baseRef = "main", now: number = Date.now()): MergePauseRecord | null {
|
|
193
|
+
const row = getDb().query("SELECT * FROM workspace_merge_pauses WHERE repo_root = ? AND base_ref = ? AND expires_at > ?")
|
|
194
|
+
.get(repoRoot, baseRef, now) as any;
|
|
195
|
+
return row ? rowToMergePause(row) : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function listMergePauses(): MergePauseRecord[] {
|
|
199
|
+
return (getDb().query("SELECT * FROM workspace_merge_pauses ORDER BY updated_at DESC").all() as any[]).map(rowToMergePause);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function acquireMergePause(input: {
|
|
203
|
+
repoRoot: string;
|
|
204
|
+
baseRef?: string;
|
|
205
|
+
holder: string;
|
|
206
|
+
reason?: string;
|
|
207
|
+
ttlMs?: number;
|
|
208
|
+
now?: number;
|
|
209
|
+
}): { ok: true; pause: MergePauseRecord } | { ok: false; pause: MergePauseRecord } {
|
|
210
|
+
return getDb().transaction(() => {
|
|
211
|
+
const now = input.now ?? Date.now();
|
|
212
|
+
const baseRef = input.baseRef || "main";
|
|
213
|
+
const existing = getActiveMergePause(input.repoRoot, baseRef, now);
|
|
214
|
+
if (existing) return { ok: false as const, pause: existing };
|
|
215
|
+
const id = randomUUID();
|
|
216
|
+
const expiresAt = now + (input.ttlMs ?? MAIN_MERGE_PAUSE_TTL_MS);
|
|
217
|
+
getDb().query(`
|
|
218
|
+
INSERT INTO workspace_merge_pauses (id, repo_root, base_ref, holder, reason, created_at, updated_at, expires_at)
|
|
219
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
220
|
+
ON CONFLICT(repo_root, base_ref) DO UPDATE SET
|
|
221
|
+
id = excluded.id,
|
|
222
|
+
holder = excluded.holder,
|
|
223
|
+
reason = excluded.reason,
|
|
224
|
+
created_at = excluded.created_at,
|
|
225
|
+
updated_at = excluded.updated_at,
|
|
226
|
+
expires_at = excluded.expires_at
|
|
227
|
+
`).run(id, input.repoRoot, baseRef, input.holder, input.reason ?? null, now, now, expiresAt);
|
|
228
|
+
return { ok: true as const, pause: getMergePause(id)! };
|
|
229
|
+
})();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function renewMergePause(id: string, ttlMs: number = MAIN_MERGE_PAUSE_TTL_MS, now: number = Date.now()): MergePauseRecord | null {
|
|
233
|
+
const current = getMergePause(id);
|
|
234
|
+
if (!current || current.expiresAt <= now) return null;
|
|
235
|
+
getDb().query("UPDATE workspace_merge_pauses SET updated_at = ?, expires_at = ? WHERE id = ?").run(now, now + ttlMs, id);
|
|
236
|
+
return getMergePause(id);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function releaseMergePause(opts: { id?: string; repoRoot?: string; baseRef?: string }): boolean {
|
|
240
|
+
const where: string[] = [];
|
|
241
|
+
const params: string[] = [];
|
|
242
|
+
if (opts.id) { where.push("id = ?"); params.push(opts.id); }
|
|
243
|
+
if (opts.repoRoot) { where.push("repo_root = ?"); params.push(opts.repoRoot); }
|
|
244
|
+
if (opts.baseRef) { where.push("base_ref = ?"); params.push(opts.baseRef); }
|
|
245
|
+
if (!where.length) return false;
|
|
246
|
+
return getDb().query(`DELETE FROM workspace_merge_pauses WHERE ${where.join(" AND ")}`).run(...params).changes > 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function releaseExpiredMergePauses(now: number = Date.now()): string[] {
|
|
250
|
+
const expired = getDb().query("SELECT id FROM workspace_merge_pauses WHERE expires_at <= ?").all(now) as Array<{ id: string }>;
|
|
251
|
+
if (!expired.length) return [];
|
|
252
|
+
getDb().query("DELETE FROM workspace_merge_pauses WHERE expires_at <= ?").run(now);
|
|
253
|
+
return expired.map((r) => r.id);
|
|
254
|
+
}
|
package/src/db/migrations.ts
CHANGED
|
@@ -117,6 +117,21 @@ export function applyMigrations(): void {
|
|
|
117
117
|
normalizeExistingMessageReactions();
|
|
118
118
|
|
|
119
119
|
// Migrations
|
|
120
|
+
getDb().run(`
|
|
121
|
+
CREATE TABLE IF NOT EXISTS workspace_merge_pauses (
|
|
122
|
+
id TEXT PRIMARY KEY,
|
|
123
|
+
repo_root TEXT NOT NULL,
|
|
124
|
+
base_ref TEXT NOT NULL,
|
|
125
|
+
holder TEXT NOT NULL,
|
|
126
|
+
reason TEXT,
|
|
127
|
+
created_at INTEGER NOT NULL,
|
|
128
|
+
updated_at INTEGER NOT NULL,
|
|
129
|
+
expires_at INTEGER NOT NULL,
|
|
130
|
+
UNIQUE(repo_root, base_ref)
|
|
131
|
+
)
|
|
132
|
+
`);
|
|
133
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_workspace_merge_pauses_expires ON workspace_merge_pauses(expires_at)");
|
|
134
|
+
|
|
120
135
|
const cols = getDb().query("PRAGMA table_info(messages)").all() as any[];
|
|
121
136
|
const colNames = cols.map((c: any) => c.name);
|
|
122
137
|
if (!colNames.includes("thread_id")) {
|
|
@@ -379,6 +394,12 @@ export function applyMigrations(): void {
|
|
|
379
394
|
"UPDATE tasks SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
|
|
380
395
|
).run(Date.now(), CLAIM_LEASE_MS);
|
|
381
396
|
|
|
397
|
+
const providerModelOverrideCols = getDb().query("PRAGMA table_info(provider_model_overrides)").all() as any[];
|
|
398
|
+
const providerModelOverrideColNames = providerModelOverrideCols.map((c: any) => c.name);
|
|
399
|
+
if (!providerModelOverrideColNames.includes("disabled")) {
|
|
400
|
+
getDb().run("ALTER TABLE provider_model_overrides ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0");
|
|
401
|
+
}
|
|
402
|
+
|
|
382
403
|
// Migration: orchestrators.api_url
|
|
383
404
|
const orchCols = getDb().query("PRAGMA table_info(orchestrators)").all() as any[];
|
|
384
405
|
const orchColNames = orchCols.map((c: any) => c.name);
|
package/src/db/schema.ts
CHANGED
|
@@ -829,7 +829,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
829
829
|
provider TEXT NOT NULL,
|
|
830
830
|
alias TEXT NOT NULL,
|
|
831
831
|
entry TEXT NOT NULL DEFAULT '{}',
|
|
832
|
-
deleted INTEGER NOT NULL DEFAULT 0,
|
|
832
|
+
deleted INTEGER NOT NULL DEFAULT 0, disabled INTEGER NOT NULL DEFAULT 0,
|
|
833
833
|
updated_at INTEGER NOT NULL,
|
|
834
834
|
updated_by TEXT,
|
|
835
835
|
PRIMARY KEY (provider, alias)
|
package/src/index.ts
CHANGED
|
@@ -268,13 +268,16 @@ export function createFetchHandler(
|
|
|
268
268
|
if (!publicPaths.includes(url.pathname)) {
|
|
269
269
|
const integrationAuth = getIntegrationAuth(req);
|
|
270
270
|
const componentAuth = getComponentAuth(req);
|
|
271
|
+
const handlerOwnsScopedAuth = url.pathname === "/api/tokens/me" ||
|
|
272
|
+
url.pathname === "/api/settings" ||
|
|
273
|
+
url.pathname.startsWith("/api/settings/");
|
|
271
274
|
if (!isAuthorized(req)) {
|
|
272
275
|
if (!integrationAuth) {
|
|
273
276
|
return unauthorized(req);
|
|
274
277
|
}
|
|
275
|
-
if (!isScopedRequestAuthorized(req)) return forbidden(req);
|
|
278
|
+
if (!handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return forbidden(req);
|
|
276
279
|
}
|
|
277
|
-
if (componentAuth && !isScopedRequestAuthorized(req)) return forbidden(req);
|
|
280
|
+
if (componentAuth && !handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return forbidden(req);
|
|
278
281
|
}
|
|
279
282
|
const response = await matched.handler(req, matched.params);
|
|
280
283
|
applyCors(req, response);
|
package/src/maintenance.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
expireQueuedMessages,
|
|
14
14
|
getAgent,
|
|
15
15
|
getDb,
|
|
16
|
+
getActiveMergePause,
|
|
16
17
|
getRepoSteward,
|
|
17
18
|
getWorkspace,
|
|
18
19
|
listActiveTeams,
|
|
@@ -64,6 +65,7 @@ import { pruneExpiredTokenRecords } from "./token-db";
|
|
|
64
65
|
import { dissolveTeamById } from "./services/team";
|
|
65
66
|
import { reapTransientAgents, resetTransientTrackerForTests } from "./services/transient-agent-reaper";
|
|
66
67
|
import { resumeRateLimitedAgents, resetRateLimitResumeTrackerForTests } from "./services/rate-limit-resume";
|
|
68
|
+
import { MERGE_PAUSE_MAINTENANCE_JOB } from "./merge-pause-maintenance";
|
|
67
69
|
import type { Command, MaintenanceJob, MaintenanceJobRun } from "./types";
|
|
68
70
|
|
|
69
71
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -197,6 +199,7 @@ const definitions: MaintenanceJobDefinition[] = [
|
|
|
197
199
|
};
|
|
198
200
|
},
|
|
199
201
|
},
|
|
202
|
+
MERGE_PAUSE_MAINTENANCE_JOB,
|
|
200
203
|
{
|
|
201
204
|
id: "task-orphan-release",
|
|
202
205
|
title: "Orphaned task release",
|
|
@@ -659,9 +662,8 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
659
662
|
const candidates = listWorkspaces().filter(
|
|
660
663
|
(ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && CONFLICT_SCAN_STATUSES.has(ws.status),
|
|
661
664
|
);
|
|
662
|
-
const flagged: string[] = [], cleared: string[] = [], merged: string[] = [], notifiedStewards: string[] = [];
|
|
665
|
+
const flagged: string[] = [], cleared: string[] = [], merged: string[] = [], notifiedStewards: string[] = [], prRecycled: string[] = [];
|
|
663
666
|
const reviewerArmed: string[] = [], reviewerChangesRequested: string[] = [], relayPrMerged: string[] = [], relayPrWaiting: string[] = [];
|
|
664
|
-
const prRecycled: string[] = [];
|
|
665
667
|
|
|
666
668
|
for (const ws of candidates) {
|
|
667
669
|
const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
|
|
@@ -708,6 +710,7 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
708
710
|
const prAction = preparePrCompletionScan(ws, p, orch.agentId);
|
|
709
711
|
if (prAction.armCommand) { emitCommand(prAction.armCommand); reviewerArmed.push(ws.id); continue; }
|
|
710
712
|
if (prAction.notifiedOwner) { reviewerChangesRequested.push(ws.id); continue; }
|
|
713
|
+
if (prAction.refreshCommand) { emitCommand(prAction.refreshCommand); continue; }
|
|
711
714
|
if (prAction.mergeCommand) { emitCommand(prAction.mergeCommand); relayPrMerged.push(ws.id); continue; }
|
|
712
715
|
if (prAction.waitReason) relayPrWaiting.push(`${ws.id}:${prAction.waitReason}`);
|
|
713
716
|
if (prAction.stop) continue;
|
|
@@ -786,16 +789,13 @@ async function autoMergeCleanFastForwards(): Promise<Record<string, unknown>> {
|
|
|
786
789
|
);
|
|
787
790
|
const stewardEnabled = getStewardConfig().enabled;
|
|
788
791
|
const merged: string[] = [];
|
|
789
|
-
const heldByLease: string[] = [];
|
|
790
|
-
const heldByClaim: string[] = [];
|
|
791
|
-
const heldAwaitingApproval: string[] = [];
|
|
792
|
-
const leftForSteward: string[] = [];
|
|
793
|
-
const wokeStewards: string[] = [];
|
|
792
|
+
const heldByLease: string[] = [], heldByClaim: string[] = [], heldByPause: string[] = [], heldAwaitingApproval: string[] = [], leftForSteward: string[] = [], wokeStewards: string[] = [];
|
|
794
793
|
|
|
795
794
|
for (const ws of candidates) {
|
|
796
795
|
if (isAwaitingReviewerApproval(ws)) { heldAwaitingApproval.push(ws.id); continue; }
|
|
797
796
|
// A claimed workspace is being validated by a steward — don't race it (#208).
|
|
798
797
|
if (workspaceActiveClaim(ws)) { heldByClaim.push(ws.id); continue; }
|
|
798
|
+
if (getActiveMergePause(ws.repoRoot, ws.baseRef ?? "main")) { heldByPause.push(ws.id); continue; }
|
|
799
799
|
const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
|
|
800
800
|
if (!orch?.apiUrl) continue;
|
|
801
801
|
const preview = await fetchHostMergePreview(orch.apiUrl, ws);
|
|
@@ -857,7 +857,7 @@ async function autoMergeCleanFastForwards(): Promise<Record<string, unknown>> {
|
|
|
857
857
|
});
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
-
return { scanned: candidates.length, merged, heldByLease, heldByClaim, heldAwaitingApproval, leftForSteward, wokeStewards };
|
|
860
|
+
return { scanned: candidates.length, merged, heldByLease, heldByClaim, heldByPause, heldAwaitingApproval, leftForSteward, wokeStewards };
|
|
861
861
|
}
|
|
862
862
|
|
|
863
863
|
// Route a GC notification, swallowing failures (a stale/missing/misconfigured target must never
|