agent-relay-server 0.120.1 → 0.120.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/MessageBubble-DazQ9Rn-.js +2 -0
- package/public/assets/{MessageBubble-jpshgh3K.js.map → MessageBubble-DazQ9Rn-.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CjVG-Jsq.js → PlanGraphPanel-DpTiGCkz.js} +2 -2
- package/public/assets/{PlanGraphPanel-CjVG-Jsq.js.map → PlanGraphPanel-DpTiGCkz.js.map} +1 -1
- package/public/assets/{activity-DLTFF8Dz.js → activity-DReCE2mK.js} +2 -2
- package/public/assets/{activity-DLTFF8Dz.js.map → activity-DReCE2mK.js.map} +1 -1
- package/public/assets/{agent-profiles-BcCLIrON.js → agent-profiles-D3ba8Xpe.js} +2 -2
- package/public/assets/{agent-profiles-BcCLIrON.js.map → agent-profiles-D3ba8Xpe.js.map} +1 -1
- package/public/assets/agents-BcsIxadm.js +2 -0
- package/public/assets/{agents-QemvuxKO.js.map → agents-BcsIxadm.js.map} +1 -1
- package/public/assets/{analytics-DMLoMwta.js → analytics-0lOlkaib.js} +2 -2
- package/public/assets/{analytics-DMLoMwta.js.map → analytics-0lOlkaib.js.map} +1 -1
- package/public/assets/{automation-DACSOffp.js → automation-D66mthBn.js} +2 -2
- package/public/assets/{automation-DACSOffp.js.map → automation-D66mthBn.js.map} +1 -1
- package/public/assets/{branch-state-badge-BYE2U_y4.js → branch-state-badge-NH-l2Eh3.js} +2 -2
- package/public/assets/{branch-state-badge-BYE2U_y4.js.map → branch-state-badge-NH-l2Eh3.js.map} +1 -1
- package/public/assets/{channels-3SlDQL3j.js → channels-BlHLMQVk.js} +2 -2
- package/public/assets/{channels-3SlDQL3j.js.map → channels-BlHLMQVk.js.map} +1 -1
- package/public/assets/chat-rwffRsPX.js +2 -0
- package/public/assets/chat-rwffRsPX.js.map +1 -0
- package/public/assets/{connectors-CrEW591Q.js → connectors-CdsiaLD3.js} +2 -2
- package/public/assets/{connectors-CrEW591Q.js.map → connectors-CdsiaLD3.js.map} +1 -1
- package/public/assets/{formatted-body-Dg9R3RD7.js → formatted-body-C1AprxyU.js} +3 -3
- package/public/assets/{formatted-body-Dg9R3RD7.js.map → formatted-body-C1AprxyU.js.map} +1 -1
- package/public/assets/formatted-body-impl-D_ZJQVNk.js +3 -0
- package/public/assets/formatted-body-impl-D_ZJQVNk.js.map +1 -0
- package/public/assets/{index-D_fNJnsI.js → index-DfPouNiV.js} +10 -10
- package/public/assets/index-DfPouNiV.js.map +1 -0
- package/public/assets/{insights-D48n2ojF.js → insights-BJf1vwrf.js} +2 -2
- package/public/assets/{insights-D48n2ojF.js.map → insights-BJf1vwrf.js.map} +1 -1
- package/public/assets/{integrations-B-bdliwU.js → integrations-BZSHasnY.js} +2 -2
- package/public/assets/{integrations-B-bdliwU.js.map → integrations-BZSHasnY.js.map} +1 -1
- package/public/assets/{maintenance-LQ_U3Nqy.js → maintenance-uEI1lArh.js} +2 -2
- package/public/assets/{maintenance-LQ_U3Nqy.js.map → maintenance-uEI1lArh.js.map} +1 -1
- package/public/assets/{managed-agents-Cfh8FvY8.js → managed-agents-mX3hHpOP.js} +4 -4
- package/public/assets/{managed-agents-Cfh8FvY8.js.map → managed-agents-mX3hHpOP.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BSa7zYx1.js → markdown-preview-impl-ClNwMC48.js} +2 -2
- package/public/assets/{markdown-preview-impl-BSa7zYx1.js.map → markdown-preview-impl-ClNwMC48.js.map} +1 -1
- package/public/assets/{memory-Dr1iYKkC.js → memory-OfusEzGh.js} +2 -2
- package/public/assets/{memory-Dr1iYKkC.js.map → memory-OfusEzGh.js.map} +1 -1
- package/public/assets/messages-DsQGj0gU.js +2 -0
- package/public/assets/{messages-DRKulS-2.js.map → messages-DsQGj0gU.js.map} +1 -1
- package/public/assets/{orchestrators-BguyeViG.js → orchestrators-BwWyl4mB.js} +2 -2
- package/public/assets/{orchestrators-BguyeViG.js.map → orchestrators-BwWyl4mB.js.map} +1 -1
- package/public/assets/overview-CED1SeTY.js +2 -0
- package/public/assets/{overview-Dbq2lr7Y.js.map → overview-CED1SeTY.js.map} +1 -1
- package/public/assets/{pairs-DD7i2scF.js → pairs-BNu6ZWg4.js} +2 -2
- package/public/assets/{pairs-DD7i2scF.js.map → pairs-BNu6ZWg4.js.map} +1 -1
- package/public/assets/{projects-DEn85LQb.js → projects-BmopRiUD.js} +2 -2
- package/public/assets/{projects-DEn85LQb.js.map → projects-BmopRiUD.js.map} +1 -1
- package/public/assets/{prompt-settings-DwgrPKY3.js → prompt-settings-Ci2n1cFt.js} +2 -2
- package/public/assets/{prompt-settings-DwgrPKY3.js.map → prompt-settings-Ci2n1cFt.js.map} +1 -1
- package/public/assets/{registry-DCR3xFpe.js → registry-DwXnDIql.js} +2 -2
- package/public/assets/{registry-DCR3xFpe.js.map → registry-DwXnDIql.js.map} +1 -1
- package/public/assets/security-AW5EbdOm.js +3 -0
- package/public/assets/security-AW5EbdOm.js.map +1 -0
- package/public/assets/{select-CpyCRBK3.js → select-CEeWX4hp.js} +2 -2
- package/public/assets/{select-CpyCRBK3.js.map → select-CEeWX4hp.js.map} +1 -1
- package/public/assets/settings-DYUdGz4G.js +6 -0
- package/public/assets/{settings-DYNFGC2-.js.map → settings-DYUdGz4G.js.map} +1 -1
- package/public/assets/store-BaslPLIP.js +9 -0
- package/public/assets/store-BaslPLIP.js.map +1 -0
- package/public/assets/{tasks-rdy9Nsbf.js → tasks-DxCsrY4g.js} +2 -2
- package/public/assets/{tasks-rdy9Nsbf.js.map → tasks-DxCsrY4g.js.map} +1 -1
- package/public/assets/{teams-DHloBagJ.js → teams-CF7vi-oM.js} +2 -2
- package/public/assets/{teams-DHloBagJ.js.map → teams-CF7vi-oM.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DWTuNejR.js → terminal-viewer-impl-Dy3vqOnW.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DWTuNejR.js.map → terminal-viewer-impl-Dy3vqOnW.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-CShT7nTC.js → work-queue-BPaGXrCE.js} +2 -2
- package/public/assets/{work-queue-CShT7nTC.js.map → work-queue-BPaGXrCE.js.map} +1 -1
- package/public/assets/{workspaces-DjQ6EdrZ.js → workspaces-BLEXgnlj.js} +2 -2
- package/public/assets/{workspaces-DjQ6EdrZ.js.map → workspaces-BLEXgnlj.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/automations/reconcile.ts +69 -4
- package/src/branch-landed.ts +18 -7
- package/src/bus.ts +23 -16
- package/src/db/connection.ts +29 -1
- package/src/db/maintenance-worker.ts +25 -0
- package/src/db/message-reads.ts +69 -44
- package/src/db/migrations.ts +1 -0
- package/src/db/provider-quotas.ts +6 -7
- package/src/db/schema.ts +2 -1
- package/src/maintenance/jobs.ts +3 -3
- package/src/mcp/index.ts +5 -5
- package/src/mcp/tools-workspace.ts +8 -3
- package/src/routes/orchestrator-proxy.ts +7 -6
- package/src/routes/tasks.ts +29 -0
- package/src/routes/workspaces.ts +9 -2
- package/src/runtime-tokens.ts +4 -0
- package/src/security.ts +31 -7
- package/src/server.ts +40 -18
- package/src/services/git-remote.ts +29 -0
- package/src/services/issue-lifecycle.ts +6 -6
- package/src/services/project.ts +47 -1
- package/src/services/send-message.ts +2 -4
- package/src/services/spawn-agent.ts +2 -2
- package/src/services/task-lifecycle.ts +6 -5
- package/src/settings/registry.ts +6 -4
- package/src/spawn-targets.ts +2 -2
- package/src/sse.ts +37 -18
- package/src/steward.ts +10 -6
- package/src/terminal-authz.ts +50 -0
- package/src/token-db.ts +3 -3
- package/src/validation.ts +1 -1
- package/src/workspace-actions.ts +46 -4
- package/src/workspace-auto-merge.ts +120 -13
- package/src/workspace-orphans.ts +1 -1
- package/src/workspace-probe.ts +19 -10
- package/public/assets/MessageBubble-jpshgh3K.js +0 -2
- package/public/assets/agents-QemvuxKO.js +0 -2
- package/public/assets/chat-xKXCafN9.js +0 -2
- package/public/assets/chat-xKXCafN9.js.map +0 -1
- package/public/assets/formatted-body-impl-DlasTD8X.js +0 -3
- package/public/assets/formatted-body-impl-DlasTD8X.js.map +0 -1
- package/public/assets/index-D_fNJnsI.js.map +0 -1
- package/public/assets/messages-DRKulS-2.js +0 -2
- package/public/assets/overview-Dbq2lr7Y.js +0 -2
- package/public/assets/security-BH8kTdA-.js +0 -3
- package/public/assets/security-BH8kTdA-.js.map +0 -1
- package/public/assets/settings-DYNFGC2-.js +0 -6
- package/public/assets/store-BuuJUn70.js +0 -9
- package/public/assets/store-BuuJUn70.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCommand } from "../commands-db";
|
|
2
|
-
import { getAgent, getTask, listOrchestrators, sendMessage, updateTaskStatus } from "../db";
|
|
2
|
+
import { getAgent, getTask, listAgents, listOrchestrators, sendMessage, updateTaskStatus } from "../db";
|
|
3
3
|
import { renderTemplate } from "../prompt-resolver";
|
|
4
4
|
import { CLOSED_TASK_STATUS } from "./constants";
|
|
5
5
|
import {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
db,
|
|
12
12
|
} from "./storage";
|
|
13
13
|
import type { AutomationReconcileResult } from "./types";
|
|
14
|
-
import type { AutomationRun, AutomationRunStatus, AutomationRuntimeBudget, Command, Message, Task } from "../types";
|
|
14
|
+
import type { AgentCard, AutomationRun, AutomationRunStatus, AutomationRuntimeBudget, Command, Message, Task } from "../types";
|
|
15
15
|
|
|
16
16
|
export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileResult[] {
|
|
17
17
|
ensureAutomationTables();
|
|
@@ -41,6 +41,11 @@ export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileRe
|
|
|
41
41
|
if (!CLOSED_TASK_STATUS.has(task.status)) {
|
|
42
42
|
const updated = getAutomationRun(run.id);
|
|
43
43
|
if (!updated) continue;
|
|
44
|
+
const neverReadyOffline = maybeFailNeverReadyOfflineTarget(updated, task, now);
|
|
45
|
+
if (neverReadyOffline) {
|
|
46
|
+
results.push(neverReadyOffline);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
44
49
|
const timedOut = maybeTimeoutRuntimeBudget(updated, task, now);
|
|
45
50
|
if (timedOut) {
|
|
46
51
|
results.push(timedOut);
|
|
@@ -68,9 +73,8 @@ export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileRe
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
function automationRunRuntimeMeta(run: AutomationRun, task: Task): Record<string, unknown> {
|
|
71
|
-
const agentId
|
|
76
|
+
const { agentId, agent } = resolveAutomationRunAgent(run, task);
|
|
72
77
|
if (!agentId) return {};
|
|
73
|
-
const agent = getAgent(agentId);
|
|
74
78
|
const managed = listOrchestrators()
|
|
75
79
|
.flatMap((orchestrator) => orchestrator.managedAgents.map((managedAgent) => ({ orchestrator, managedAgent })))
|
|
76
80
|
.find(({ managedAgent }) => managedAgent.agentId === agentId);
|
|
@@ -103,6 +107,23 @@ function hasMetaChanges(current: Record<string, unknown>, patch: Record<string,
|
|
|
103
107
|
return Object.entries(patch).some(([key, value]) => current[key] !== value);
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
function resolveAutomationRunAgent(run: AutomationRun, task: Task): { agentId?: string; agent?: AgentCard } {
|
|
111
|
+
const explicitAgentId = task.claimedBy ?? run.targetAgentId ?? run.spawnedAgentId ?? stringMeta(run.meta, "runtimeAgentId");
|
|
112
|
+
if (explicitAgentId) return { agentId: explicitAgentId, agent: getAgent(explicitAgentId) ?? undefined };
|
|
113
|
+
|
|
114
|
+
const onDemandLabel = stringMeta(run.meta, "onDemandLabel");
|
|
115
|
+
const agent = listAgents().find((candidate) =>
|
|
116
|
+
(onDemandLabel !== undefined && candidate.label === onDemandLabel) ||
|
|
117
|
+
stringMeta(candidate.meta, "automationRunId") === run.id
|
|
118
|
+
);
|
|
119
|
+
return agent ? { agentId: agent.id, agent } : {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isOnDemandAutomationRun(run: AutomationRun): boolean {
|
|
123
|
+
if (run.meta.targetMode === "on_demand_agent") return true;
|
|
124
|
+
return getAutomation(run.automationId)?.targetPolicy.mode === "on_demand_agent";
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
function runtimeBudgetForRun(run: AutomationRun): AutomationRuntimeBudget | undefined {
|
|
107
128
|
const automation = getAutomation(run.automationId);
|
|
108
129
|
if (!automation || automation.targetPolicy.mode !== "on_demand_agent") return undefined;
|
|
@@ -163,6 +184,50 @@ function maybeWarnRuntimeBudget(run: AutomationRun, task: Task, now: number): Me
|
|
|
163
184
|
return message;
|
|
164
185
|
}
|
|
165
186
|
|
|
187
|
+
function maybeFailNeverReadyOfflineTarget(run: AutomationRun, task: Task, now: number): AutomationReconcileResult | undefined {
|
|
188
|
+
if (!isOnDemandAutomationRun(run) || task.claimedBy) return undefined;
|
|
189
|
+
const { agentId, agent } = resolveAutomationRunAgent(run, task);
|
|
190
|
+
if (!agentId || !agent || agent.status !== "offline" || agent.ready) return undefined;
|
|
191
|
+
|
|
192
|
+
const reason = "Automation target went offline before becoming ready";
|
|
193
|
+
const taskResult = updateTaskStatus(task.id, {
|
|
194
|
+
status: "canceled",
|
|
195
|
+
agentId,
|
|
196
|
+
result: reason,
|
|
197
|
+
body: reason,
|
|
198
|
+
metadata: {
|
|
199
|
+
automationId: run.automationId,
|
|
200
|
+
automationRunId: run.id,
|
|
201
|
+
automationTargetOffline: true,
|
|
202
|
+
automationTargetNeverReady: true,
|
|
203
|
+
agentId,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const updatedTask = taskResult.task ?? getTask(task.id) ?? task;
|
|
207
|
+
updateRun(run.id, {
|
|
208
|
+
status: "failed",
|
|
209
|
+
finishedAt: now,
|
|
210
|
+
error: reason,
|
|
211
|
+
result: {
|
|
212
|
+
taskStatus: updatedTask.status,
|
|
213
|
+
automationTargetOffline: true,
|
|
214
|
+
automationTargetNeverReady: true,
|
|
215
|
+
agentId,
|
|
216
|
+
},
|
|
217
|
+
targetAgentId: agentId,
|
|
218
|
+
spawnedAgentId: run.spawnedAgentId ?? agentId,
|
|
219
|
+
meta: {
|
|
220
|
+
...run.meta,
|
|
221
|
+
runtimeAgentId: agentId,
|
|
222
|
+
runtimeAgentStatus: agent.status,
|
|
223
|
+
runtimeAgentReady: agent.ready,
|
|
224
|
+
runtimeTargetOfflineAt: now,
|
|
225
|
+
runtimeTargetNeverReady: true,
|
|
226
|
+
},
|
|
227
|
+
}, now);
|
|
228
|
+
return { run: getAutomationRun(run.id)!, task: getTask(task.id) ?? updatedTask };
|
|
229
|
+
}
|
|
230
|
+
|
|
166
231
|
function maybeTimeoutRuntimeBudget(run: AutomationRun, task: Task, now: number): AutomationReconcileResult | undefined {
|
|
167
232
|
const budget = runtimeBudgetForRun(run);
|
|
168
233
|
if (!budget) return undefined;
|
package/src/branch-landed.ts
CHANGED
|
@@ -46,7 +46,7 @@ export interface BranchLandedInput {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
type BranchLandedLogFn = (message: string) => void;
|
|
49
|
-
type LandedCommitSubjectResolver = (input: BranchLandedInput, log: BranchLandedLogFn) => string[] | undefined
|
|
49
|
+
type LandedCommitSubjectResolver = (input: BranchLandedInput, log: BranchLandedLogFn) => string[] | undefined | Promise<string[] | undefined>;
|
|
50
50
|
|
|
51
51
|
let landedCommitSubjectResolverForTests: LandedCommitSubjectResolver | undefined;
|
|
52
52
|
|
|
@@ -59,7 +59,7 @@ function cleanRef(value: string | undefined): string | undefined {
|
|
|
59
59
|
return cleaned || undefined;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn): string[] | undefined {
|
|
62
|
+
async function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn): Promise<string[] | undefined> {
|
|
63
63
|
if (landedCommitSubjectResolverForTests) return landedCommitSubjectResolverForTests(input, log);
|
|
64
64
|
if (input.prLand) return undefined;
|
|
65
65
|
const repoRoot = cleanRef(input.workspace.repoRoot);
|
|
@@ -67,17 +67,18 @@ function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn):
|
|
|
67
67
|
const tipSha = cleanRef(input.mergedSha);
|
|
68
68
|
if (!repoRoot || !baseSha || !tipSha || baseSha === tipSha) return undefined;
|
|
69
69
|
try {
|
|
70
|
-
const proc = Bun.
|
|
70
|
+
const proc = Bun.spawn(["git", "-C", repoRoot, "log", "--format=%B%x00", `${baseSha}..${tipSha}`], {
|
|
71
71
|
stdin: "ignore",
|
|
72
72
|
stdout: "pipe",
|
|
73
73
|
stderr: "pipe",
|
|
74
74
|
});
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
|
76
|
+
if (exitCode !== 0) {
|
|
77
|
+
const error = stderr.trim() || `exit ${exitCode}`;
|
|
77
78
|
log(`issue lifecycle commit range scan failed for ${input.workspace.id} (${baseSha.slice(0, 12)}..${tipSha.slice(0, 12)}): ${error}; falling back to tip subject`);
|
|
78
79
|
return undefined;
|
|
79
80
|
}
|
|
80
|
-
const subjects =
|
|
81
|
+
const subjects = stdout.split("\x00").map((msg) => msg.trim()).filter(Boolean);
|
|
81
82
|
return subjects.length > 0 ? subjects : undefined;
|
|
82
83
|
} catch (error) {
|
|
83
84
|
log(`issue lifecycle commit range scan failed for ${input.workspace.id} (${baseSha.slice(0, 12)}..${tipSha.slice(0, 12)}): ${errMessage(error)}; falling back to tip subject`);
|
|
@@ -94,6 +95,13 @@ function resolveLandedTask(workspace: Pick<WorkspaceRecord, "branch" | "ownerAge
|
|
|
94
95
|
return task ? { taskId: task.id, taskLabel: task.title } : undefined;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
function landedNotificationIdempotencyKey(input: BranchLandedInput, landedTask: { taskId: number } | undefined, base: string): string | undefined {
|
|
99
|
+
const sha = cleanRef(input.mergedSha);
|
|
100
|
+
if (sha) return `branch-landed:${sha}`;
|
|
101
|
+
if (landedTask) return `branch-landed:task:${landedTask.taskId}:${base}`;
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
/**
|
|
98
106
|
* #239 — turn an authoritative land completion into a relay-driven push so the author
|
|
99
107
|
* stops polling to learn it merged. Always emits the durable `branch.landed` event (the
|
|
@@ -108,8 +116,8 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
108
116
|
const landedBranch = workspace.branch;
|
|
109
117
|
const shortSha = input.mergedSha ? input.mergedSha.slice(0, 12) : undefined;
|
|
110
118
|
const lifecycleLog = (message: string) => console.warn(message);
|
|
111
|
-
const commitSubjects = landedCommitSubjects(input, lifecycleLog);
|
|
112
119
|
const landedTask = resolveLandedTask(workspace);
|
|
120
|
+
const idempotencyKey = landedNotificationIdempotencyKey(input, landedTask, base);
|
|
113
121
|
|
|
114
122
|
emitRelayEvent({
|
|
115
123
|
type: "branch.landed",
|
|
@@ -151,6 +159,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
151
159
|
// receipt accurate. Both are best-effort + isolated — a throw in either can never fail the merge
|
|
152
160
|
// (it already landed before notifyBranchLanded runs). A non-task branch makes the Task drive a no-op.
|
|
153
161
|
void (async () => {
|
|
162
|
+
const commitSubjects = await landedCommitSubjects(input, lifecycleLog);
|
|
154
163
|
try {
|
|
155
164
|
await handleLandedIssueLifecycle({ ...input, commitSubjects }, { log: lifecycleLog });
|
|
156
165
|
} catch (error) {
|
|
@@ -208,6 +217,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
208
217
|
continueLabel,
|
|
209
218
|
}),
|
|
210
219
|
payload: { ...payload, notification: "landed-success" },
|
|
220
|
+
idempotencyKey,
|
|
211
221
|
replyExpected: false,
|
|
212
222
|
},
|
|
213
223
|
});
|
|
@@ -231,6 +241,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
231
241
|
subject: `Merged to ${base}`,
|
|
232
242
|
body: `🔀 ${branchLabel}${authorLabel} merged to \`${base}\`${shaLabel}${subjectLabel}.${publishLabel}`,
|
|
233
243
|
payload: { ...payload, notification: "landed-success" },
|
|
244
|
+
idempotencyKey,
|
|
234
245
|
replyExpected: false,
|
|
235
246
|
},
|
|
236
247
|
});
|
package/src/bus.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "agent-relay-sdk/protocol";
|
|
32
32
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
33
33
|
import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
|
|
34
|
-
import type { Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
34
|
+
import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
35
35
|
|
|
36
36
|
interface BusSocketData {
|
|
37
37
|
kind: "bus";
|
|
@@ -503,30 +503,37 @@ function withRegistered(
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
function broadcastRelayEvent(event: RelayEvent): void {
|
|
506
|
+
const frame = JSON.stringify({
|
|
507
|
+
type: "event",
|
|
508
|
+
payload: {
|
|
509
|
+
seq: event.seq,
|
|
510
|
+
eventType: event.type,
|
|
511
|
+
source: event.source,
|
|
512
|
+
subject: event.subject,
|
|
513
|
+
data: event.data,
|
|
514
|
+
timestamp: event.timestamp,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
const agentCache = new Map<string, AgentCard | null>();
|
|
506
518
|
for (const conn of busConnections.values()) {
|
|
507
|
-
if (!connectionWantsEvent(conn, event)) continue;
|
|
508
|
-
|
|
509
|
-
type: "event",
|
|
510
|
-
payload: {
|
|
511
|
-
seq: event.seq,
|
|
512
|
-
eventType: event.type,
|
|
513
|
-
source: event.source,
|
|
514
|
-
subject: event.subject,
|
|
515
|
-
data: event.data,
|
|
516
|
-
timestamp: event.timestamp,
|
|
517
|
-
},
|
|
518
|
-
});
|
|
519
|
+
if (!connectionWantsEvent(conn, event, agentCache)) continue;
|
|
520
|
+
conn.ws.send(frame);
|
|
519
521
|
}
|
|
520
522
|
}
|
|
521
523
|
|
|
522
|
-
function
|
|
524
|
+
function fanoutAgent(agentId: string, cache: Map<string, AgentCard | null>): AgentCard | null {
|
|
525
|
+
if (!cache.has(agentId)) cache.set(agentId, getAgent(agentId));
|
|
526
|
+
return cache.get(agentId) ?? null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function connectionWantsEvent(conn: BusConnection, event: RelayEvent, agentCache: Map<string, AgentCard | null>): boolean {
|
|
523
530
|
if (!matchesSubscription(conn.subscriptions, event.type)) return false;
|
|
524
531
|
if (event.type === "message.new" && conn.agentId) {
|
|
525
|
-
return messageMatchesAgent(event.data as unknown as Message, conn.agentId,
|
|
532
|
+
return messageMatchesAgent(event.data as unknown as Message, conn.agentId, fanoutAgent(conn.agentId, agentCache));
|
|
526
533
|
}
|
|
527
534
|
if (event.type.startsWith("task.") && conn.agentId) {
|
|
528
535
|
const target = typeof event.data.target === "string" ? event.data.target : "";
|
|
529
|
-
return targetMatchesAgent(target, conn.agentId,
|
|
536
|
+
return targetMatchesAgent(target, conn.agentId, fanoutAgent(conn.agentId, agentCache));
|
|
530
537
|
}
|
|
531
538
|
if (event.type.startsWith("command.")) {
|
|
532
539
|
const command = isRecord(event.data.command) ? event.data.command : undefined;
|
package/src/db/connection.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { DB_SLOW_QUERY_MS } from "../config";
|
|
3
4
|
|
|
4
5
|
// The shared SQLite handle. Set once by initDb() (schema.ts) and read everywhere
|
|
5
6
|
// else through getDb(). Kept module-private so the only mutation path is setDb().
|
|
6
7
|
let db: Database;
|
|
8
|
+
let dbPath: string | undefined;
|
|
7
9
|
|
|
8
|
-
export function setDb(conn: Database): void {
|
|
10
|
+
export function setDb(conn: Database, path?: string): void {
|
|
9
11
|
db = conn;
|
|
12
|
+
dbPath = path;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export function getDb(): Database {
|
|
@@ -14,6 +17,10 @@ export function getDb(): Database {
|
|
|
14
17
|
return db;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
export function getDbPath(): string | undefined {
|
|
21
|
+
return dbPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
// Connection-scoped pragmas. journal_mode and auto_vacuum persist in the file;
|
|
18
25
|
// the rest reset per connection, so they must be re-applied on every open.
|
|
19
26
|
export function applyConnectionPragmas(conn: Database): void {
|
|
@@ -67,6 +74,27 @@ export function runDbMaintenance(options: { vacuum?: boolean } = {}): {
|
|
|
67
74
|
return { analyzed: true, checkpointed: true, vacuumed };
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
export async function runDbMaintenanceAsync(options: { vacuum?: boolean } = {}): Promise<{
|
|
78
|
+
analyzed: boolean;
|
|
79
|
+
checkpointed: boolean;
|
|
80
|
+
vacuumed: boolean;
|
|
81
|
+
offThread: boolean;
|
|
82
|
+
}> {
|
|
83
|
+
if (!dbPath || dbPath === ":memory:") return { ...runDbMaintenance(options), offThread: false };
|
|
84
|
+
const proc = Bun.spawn([process.execPath, join(import.meta.dir, "maintenance-worker.ts"), dbPath, options.vacuum ? "vacuum" : "no-vacuum"], {
|
|
85
|
+
stdin: "ignore",
|
|
86
|
+
stdout: "pipe",
|
|
87
|
+
stderr: "pipe",
|
|
88
|
+
});
|
|
89
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
|
90
|
+
if (exitCode !== 0) {
|
|
91
|
+
const detail = stderr.trim() || stdout.trim() || `exit ${exitCode}`;
|
|
92
|
+
throw new Error(`db maintenance worker failed: ${detail}`);
|
|
93
|
+
}
|
|
94
|
+
const result = JSON.parse(stdout) as { analyzed: boolean; checkpointed: boolean; vacuumed: boolean };
|
|
95
|
+
return { ...result, offThread: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
// Shared error types thrown across query domains and unwrapped at call sites.
|
|
71
99
|
// `ValidationError` now lives in the SDK (agent-relay-sdk) as the single canonical
|
|
72
100
|
// home shared with the orchestrator package; re-exported here so the ~120 server
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { applyConnectionPragmas } from "./connection";
|
|
3
|
+
|
|
4
|
+
const [, , dbPath, mode] = Bun.argv;
|
|
5
|
+
|
|
6
|
+
if (!dbPath) {
|
|
7
|
+
console.error("db path is required");
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const db = new Database(dbPath, { create: true });
|
|
12
|
+
try {
|
|
13
|
+
applyConnectionPragmas(db);
|
|
14
|
+
db.run("ANALYZE");
|
|
15
|
+
db.run("PRAGMA optimize");
|
|
16
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
17
|
+
let vacuumed = false;
|
|
18
|
+
if (mode === "vacuum") {
|
|
19
|
+
db.run("VACUUM");
|
|
20
|
+
vacuumed = true;
|
|
21
|
+
}
|
|
22
|
+
console.log(JSON.stringify({ analyzed: true, checkpointed: true, vacuumed }));
|
|
23
|
+
} finally {
|
|
24
|
+
db.close();
|
|
25
|
+
}
|
package/src/db/message-reads.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { getAgent } from "./agents.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { sendMessageWithResult } from "./messages.ts";
|
|
19
19
|
import { deleteArtifactLinksForEntity } from "./artifacts.ts";
|
|
20
20
|
import { getDb, timedQuery } from "./connection.ts";
|
|
21
21
|
import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
|
|
@@ -393,25 +393,58 @@ function relayConversationId(message: Message): string | undefined {
|
|
|
393
393
|
return stringField(replyContext?.conversationId) ?? stringField(conversation?.id);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
function
|
|
396
|
+
function messagesCoveredByLaterAgentResponses(messages: Message[], agentId: string): Set<number> {
|
|
397
|
+
if (messages.length === 0) return new Set();
|
|
397
398
|
// Order by id, not created_at: ids are monotonic insertion order, so this is
|
|
398
399
|
// robust when a reply lands in the same millisecond as the message it covers
|
|
399
400
|
// (created_at > … strictly would miss it, leaving the message wrongly pending).
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
401
|
+
const minMessageId = Math.min(...messages.map((message) => message.id));
|
|
402
|
+
const replies = getDb().query(`
|
|
403
|
+
SELECT id, thread_id, channel, reply_to, payload
|
|
404
|
+
FROM messages INDEXED BY idx_msg_from_id
|
|
405
|
+
WHERE from_agent = ?
|
|
406
|
+
AND id > ?
|
|
407
|
+
ORDER BY id ASC
|
|
408
|
+
`).all(agentId, minMessageId) as Array<{ id: number; thread_id: number | null; channel: string | null; reply_to: number | null; payload: string }>;
|
|
409
|
+
if (replies.length === 0) return new Set();
|
|
410
|
+
|
|
411
|
+
const covered = new Set<number>();
|
|
412
|
+
const conversationsById = new Map<number, string | undefined>();
|
|
413
|
+
for (const message of messages) conversationsById.set(message.id, relayConversationId(message));
|
|
414
|
+
|
|
415
|
+
for (const message of messages) {
|
|
416
|
+
const conversationId = conversationsById.get(message.id);
|
|
417
|
+
for (const reply of replies) {
|
|
418
|
+
if (reply.id <= message.id) continue;
|
|
419
|
+
if (reply.reply_to === message.id) {
|
|
420
|
+
covered.add(message.id);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
if (message.threadId !== undefined && reply.thread_id === message.threadId) {
|
|
424
|
+
covered.add(message.id);
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (!message.channel || reply.channel !== message.channel || !conversationId) continue;
|
|
428
|
+
if (relayConversationId({
|
|
429
|
+
id: reply.id,
|
|
430
|
+
from: agentId,
|
|
431
|
+
to: "",
|
|
432
|
+
body: "",
|
|
433
|
+
kind: "chat",
|
|
434
|
+
channel: reply.channel ?? undefined,
|
|
435
|
+
replyTo: reply.reply_to ?? undefined,
|
|
436
|
+
threadId: reply.thread_id ?? undefined,
|
|
437
|
+
payload: parseJson(reply.payload ?? "{}", {}),
|
|
438
|
+
meta: {},
|
|
439
|
+
readBy: [],
|
|
440
|
+
createdAt: 0,
|
|
441
|
+
}) === conversationId) {
|
|
442
|
+
covered.add(message.id);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return covered;
|
|
415
448
|
}
|
|
416
449
|
|
|
417
450
|
function replyObligationFromMessage(message: Message, agentId: string): ReplyObligation {
|
|
@@ -433,11 +466,20 @@ function replyObligationFromMessage(message: Message, agentId: string): ReplyObl
|
|
|
433
466
|
export function listPendingReplyObligations(agentId: string, limit = 20): ReplyObligation[] {
|
|
434
467
|
const scanLimit = Math.max(limit * 5, 50);
|
|
435
468
|
const rows = timedQuery("listPendingReplyObligations", () => getDb().query(`
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
469
|
+
SELECT m.*, (
|
|
470
|
+
SELECT json_group_array(agent_id) FROM message_reads WHERE message_id = m.id
|
|
471
|
+
) AS read_by_agents, (
|
|
472
|
+
SELECT json_group_array(json_object(
|
|
473
|
+
'messageId', message_id,
|
|
474
|
+
'actorId', actor_id,
|
|
475
|
+
'emoji', emoji,
|
|
476
|
+
'createdAt', created_at,
|
|
477
|
+
'updatedAt', updated_at
|
|
478
|
+
)) FROM message_reactions WHERE message_id = m.id
|
|
479
|
+
) AS reactions_json
|
|
480
|
+
FROM message_reads mr INDEXED BY idx_mr_agent
|
|
481
|
+
JOIN messages m ON m.id = mr.message_id
|
|
482
|
+
WHERE mr.agent_id = ?
|
|
441
483
|
AND m.from_agent != ?
|
|
442
484
|
AND NOT EXISTS (
|
|
443
485
|
SELECT 1 FROM messages reply
|
|
@@ -447,10 +489,12 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
|
|
|
447
489
|
ORDER BY m.created_at ASC
|
|
448
490
|
LIMIT ?
|
|
449
491
|
`).all(agentId, agentId, agentId, scanLimit) as any[]);
|
|
450
|
-
|
|
492
|
+
const candidates = rows
|
|
451
493
|
.map(rowToMessage)
|
|
452
|
-
.filter(messageRequiresReply)
|
|
453
|
-
|
|
494
|
+
.filter(messageRequiresReply);
|
|
495
|
+
const covered = messagesCoveredByLaterAgentResponses(candidates, agentId);
|
|
496
|
+
return candidates
|
|
497
|
+
.filter((message) => !covered.has(message.id))
|
|
454
498
|
.slice(0, limit)
|
|
455
499
|
.map((message) => replyObligationFromMessage(message, agentId));
|
|
456
500
|
}
|
|
@@ -484,25 +528,6 @@ export function channelObligationAnsweredBy(
|
|
|
484
528
|
return open.length === 1 ? open[0]!.messageId : null;
|
|
485
529
|
}
|
|
486
530
|
|
|
487
|
-
/**
|
|
488
|
-
* Spawn report-back linking (#492): a spawned worker's actual task is delivered as the provider
|
|
489
|
-
* prompt, while `seedSpawnReplyObligation` creates a short Stop-hook contract message so the worker
|
|
490
|
-
* cannot go idle without reporting back. Workers commonly send their natural completion report
|
|
491
|
-
* directly to the spawning coordinator; when there is exactly one open spawn obligation from that
|
|
492
|
-
* coordinator, that send is the answer. Link it to the obligation at send time so the Stop hook does
|
|
493
|
-
* not nag the worker into re-sending the same verdict through `/reply`.
|
|
494
|
-
*/
|
|
495
|
-
export function spawnObligationAnsweredBy(
|
|
496
|
-
agentId: string,
|
|
497
|
-
spawnerAgentId: string,
|
|
498
|
-
): number | null {
|
|
499
|
-
const open = listPendingReplyObligations(agentId, 50).filter((obligation) => {
|
|
500
|
-
if (obligation.from !== spawnerAgentId) return false;
|
|
501
|
-
return getMessage(obligation.messageId)?.payload?.source === "spawn-obligation";
|
|
502
|
-
});
|
|
503
|
-
return open.length === 1 ? open[0]!.messageId : null;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
531
|
/**
|
|
507
532
|
* #573 — spawned children no longer get a report-back reply obligation. Their turn-final
|
|
508
533
|
* response is promoted server-side to the spawn-parent by lineage, so creating a separate
|
package/src/db/migrations.ts
CHANGED
|
@@ -411,6 +411,7 @@ export function applyMigrations(): void {
|
|
|
411
411
|
getDb().run("ALTER TABLE messages ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'");
|
|
412
412
|
}
|
|
413
413
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_kind ON messages(kind)");
|
|
414
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_id ON messages(from_agent, id)");
|
|
414
415
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_kind_id ON messages(from_agent, kind, id)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_analytics_created ON messages(created_at, kind, from_agent, claimable, reply_to)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_session_analytics ON messages(kind, created_at, json_extract(payload, '$.session.type'), json_extract(payload, '$.session.label'), json_extract(payload, '$.session.turnId'))");
|
|
415
416
|
// (from_agent, idempotency_key) powers findMessageByIdempotencyKey — the retry-dedup lookup
|
|
416
417
|
// AND the session-step in-place upsert (Codex tool running→completed / streamed reasoning),
|
|
@@ -125,10 +125,15 @@ function supersededAccountKeys(provider: string, accountKey: string, rawAccountK
|
|
|
125
125
|
return [...candidates];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Source priority for quota precedence: "ingest" (WHAM/API) beats "probe" (agent self-report).
|
|
129
|
+
const QUOTA_SOURCE_PRIORITY: Record<string, number> = { ingest: 3, header: 2, probe: 1, statusline: 0, emulated: -1 };
|
|
130
|
+
function quotaSourcePriority(source: string): number { return QUOTA_SOURCE_PRIORITY[source] ?? 1; }
|
|
131
|
+
|
|
128
132
|
function newestQuotaState(rows: ProviderQuotaRow[]): string | null {
|
|
133
|
+
const priority = (candidate: ProviderQuotaRow) => { const source = parseJson<Partial<QuotaState>>(candidate.quota_state, { source: "probe" }).source; return quotaSourcePriority(typeof source === "string" ? source : "probe"); };
|
|
129
134
|
const row = rows
|
|
130
135
|
.filter((candidate) => candidate.quota_state)
|
|
131
|
-
.sort((a, b) => (b.last_updated_at ?? 0) - (a.last_updated_at ?? 0))[0];
|
|
136
|
+
.sort((a, b) => priority(b) - priority(a) || (b.last_updated_at ?? 0) - (a.last_updated_at ?? 0))[0];
|
|
132
137
|
return row?.quota_state ?? null;
|
|
133
138
|
}
|
|
134
139
|
|
|
@@ -285,12 +290,6 @@ function storedQuotaWindowSampleTimes(quota: StoredQuotaState): Map<string, numb
|
|
|
285
290
|
return times;
|
|
286
291
|
}
|
|
287
292
|
|
|
288
|
-
// Source priority for per-window precedence: "ingest" (WHAM/API) beats "probe" (agent self-report).
|
|
289
|
-
const QUOTA_SOURCE_PRIORITY: Record<string, number> = { ingest: 3, header: 2, probe: 1, statusline: 0, emulated: -1 };
|
|
290
|
-
function quotaSourcePriority(source: string): number {
|
|
291
|
-
return QUOTA_SOURCE_PRIORITY[source] ?? 1;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
293
|
function storedQuotaWindowSources(quota: StoredQuotaState): Map<string, string> {
|
|
295
294
|
const sources = new Map<string, string>();
|
|
296
295
|
const rawSources = quota._windowSource && typeof quota._windowSource === "object" ? quota._windowSource : {};
|
package/src/db/schema.ts
CHANGED
|
@@ -91,7 +91,7 @@ import type {
|
|
|
91
91
|
export function initDb(path: string = "agent-relay.db"): Database {
|
|
92
92
|
const conn = new Database(path, { create: true });
|
|
93
93
|
applyConnectionPragmas(conn);
|
|
94
|
-
setDb(conn);
|
|
94
|
+
setDb(conn, path);
|
|
95
95
|
|
|
96
96
|
getDb().run(`
|
|
97
97
|
CREATE TABLE IF NOT EXISTS agents (
|
|
@@ -165,6 +165,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
165
165
|
-- resolved_to_agent ALTER — never inline here (same hazard as #483/#559).
|
|
166
166
|
-- idx_msg_reply_to is also migration-owned because reply_to was added by ALTER.
|
|
167
167
|
CREATE INDEX IF NOT EXISTS idx_msg_to_id ON messages(to_target, id);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_msg_from_id ON messages(from_agent, id);
|
|
168
169
|
CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(created_at);
|
|
169
170
|
CREATE INDEX IF NOT EXISTS idx_msg_channel ON messages(channel);
|
|
170
171
|
CREATE TABLE IF NOT EXISTS scheduled_timers (
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
releaseExpiredClaims,
|
|
21
21
|
releaseExpiredDriveLeases,
|
|
22
22
|
releaseOrphanedTasks,
|
|
23
|
-
|
|
23
|
+
runDbMaintenanceAsync,
|
|
24
24
|
sweepArtifacts,
|
|
25
25
|
} from "../db";
|
|
26
26
|
import { isAgentUnavailable } from "../db/agent-predicates";
|
|
@@ -385,10 +385,10 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
385
385
|
intervalMs: DB_MAINTENANCE_INTERVAL_MS,
|
|
386
386
|
runOnStart: false,
|
|
387
387
|
timeoutMs: 5 * 60 * 1000,
|
|
388
|
-
handler() {
|
|
388
|
+
async handler() {
|
|
389
389
|
dbMaintenanceRuns += 1;
|
|
390
390
|
const vacuum = DB_VACUUM_EVERY > 0 && dbMaintenanceRuns % DB_VACUUM_EVERY === 0;
|
|
391
|
-
return
|
|
391
|
+
return runDbMaintenanceAsync({ vacuum });
|
|
392
392
|
},
|
|
393
393
|
},
|
|
394
394
|
{
|
package/src/mcp/index.ts
CHANGED
|
@@ -167,11 +167,11 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
167
167
|
else if (name === "relay_agent_action") result = relayAgentAction(auth, args);
|
|
168
168
|
else if (name === "relay_workspace_status") result = await relayWorkspaceStatus(auth, args);
|
|
169
169
|
else if (name === "relay_workspace_list") result = relayWorkspaceList(auth, args);
|
|
170
|
-
else if (name === "relay_workspace_ready") result = relayWorkspaceMutation(auth, "ready", args);
|
|
171
|
-
else if (name === "relay_workspace_deps") result = relayWorkspaceMutation(auth, "deps-refresh", args);
|
|
172
|
-
else if (name === "relay_workspace_claim") result = relayWorkspaceMutation(auth, "claim", args);
|
|
173
|
-
else if (name === "relay_workspace_release") result = relayWorkspaceMutation(auth, "release-claim", args);
|
|
174
|
-
else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
|
|
170
|
+
else if (name === "relay_workspace_ready") result = await relayWorkspaceMutation(auth, "ready", args);
|
|
171
|
+
else if (name === "relay_workspace_deps") result = await relayWorkspaceMutation(auth, "deps-refresh", args);
|
|
172
|
+
else if (name === "relay_workspace_claim") result = await relayWorkspaceMutation(auth, "claim", args);
|
|
173
|
+
else if (name === "relay_workspace_release") result = await relayWorkspaceMutation(auth, "release-claim", args);
|
|
174
|
+
else if (name === "relay_workspace_land") result = await relayWorkspaceMutation(auth, "merge", args);
|
|
175
175
|
else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
|
|
176
176
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
177
177
|
else if (name.startsWith("relay_task_memory_")) result = relayTaskMemoryTool(auth, name, args);
|
|
@@ -4,6 +4,7 @@ import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
|
4
4
|
import { AUTO_MERGE_POLICIES } from "../workspace-merge";
|
|
5
5
|
import { describeWorkspacePhase, landReceipt, readyContract } from "../workspace-phase";
|
|
6
6
|
import { LAND_STRATEGIES, applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "../workspace-actions";
|
|
7
|
+
import { refreshWorkspaceGitState } from "../workspace-probe";
|
|
7
8
|
import { optionalEnum } from "../validation";
|
|
8
9
|
import type { Command, WorkspaceAutoMergePolicy, WorkspaceMergeStrategy, WorkspaceRecord } from "../types";
|
|
9
10
|
import type { McpAuthContext, ToolDefinition } from "./types";
|
|
@@ -185,9 +186,13 @@ export function relayWorkspaceList(auth: McpAuthContext, args: Record<string, un
|
|
|
185
186
|
return { workspaces, count: workspaces.length };
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
export function relayWorkspaceMutation(auth: McpAuthContext, action: WorkspaceAction, args: Record<string, unknown>): Record<string, unknown
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
export async function relayWorkspaceMutation(auth: McpAuthContext, action: WorkspaceAction, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
190
|
+
let ws = resolveWorkspaceForCaller(auth, args);
|
|
191
|
+
if (action === "ready" || action === "request-review") {
|
|
192
|
+
await refreshWorkspaceGitState(ws);
|
|
193
|
+
ws = getWorkspace(ws.id) ?? ws;
|
|
194
|
+
}
|
|
195
|
+
const result = await applyWorkspaceAction(ws, {
|
|
191
196
|
action,
|
|
192
197
|
agentId: callerAgentId(auth) ?? auth.actor,
|
|
193
198
|
detail: optionalString(args.detail, "detail", 4000),
|
|
@@ -6,6 +6,7 @@ import { cleanStringArray } from "../validation";
|
|
|
6
6
|
import { emitOrchestratorStatus } from "../sse";
|
|
7
7
|
import { type OrchestratorRuntimeInput, type SpawnProvider } from "../types";
|
|
8
8
|
import { relayToken } from "../config";
|
|
9
|
+
import { isTerminalSessionRequestAuthorized } from "../terminal-authz";
|
|
9
10
|
|
|
10
11
|
export const getOrchestratorDirectories: Handler = async (req, params) => {
|
|
11
12
|
const orch = getOrchestrator(params.id!);
|
|
@@ -146,16 +147,16 @@ export const getOrchestratorLogs: Handler = (req, params) => {
|
|
|
146
147
|
};
|
|
147
148
|
|
|
148
149
|
export const getOrchestratorTerminal: Handler = (req, params) => {
|
|
149
|
-
|
|
150
|
-
return
|
|
150
|
+
if (!isTerminalSessionRequestAuthorized(req, params.id!, params.session!, "read")) return error("forbidden", 403);
|
|
151
|
+
return proxyOrchestratorGet(req, params.id!, `/api/terminal/${encodeURIComponent(params.session!)}`);
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
export const postOrchestratorTerminalInput: Handler = (req, params) => {
|
|
154
|
-
|
|
155
|
-
return
|
|
155
|
+
if (!isTerminalSessionRequestAuthorized(req, params.id!, params.session!, "write")) return error("forbidden", 403);
|
|
156
|
+
return proxyOrchestratorPost(req, params.id!, `/api/terminal/${encodeURIComponent(params.session!)}/input`);
|
|
156
157
|
};
|
|
157
158
|
|
|
158
159
|
export const postOrchestratorTerminalResize: Handler = (req, params) => {
|
|
159
|
-
|
|
160
|
-
return
|
|
160
|
+
if (!isTerminalSessionRequestAuthorized(req, params.id!, params.session!, "read")) return error("forbidden", 403);
|
|
161
|
+
return proxyOrchestratorPost(req, params.id!, `/api/terminal/${encodeURIComponent(params.session!)}/resize`);
|
|
161
162
|
};
|