agent-relay-server 0.120.5 → 0.121.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 +1 -1
- package/package.json +3 -3
- package/public/assets/{MessageBubble-BVff6PSi.js → MessageBubble-BaPGFJxq.js} +2 -2
- package/public/assets/{MessageBubble-BVff6PSi.js.map → MessageBubble-BaPGFJxq.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BjrVU5Tg.js → PlanGraphPanel-BXCVfmln.js} +2 -2
- package/public/assets/{PlanGraphPanel-BjrVU5Tg.js.map → PlanGraphPanel-BXCVfmln.js.map} +1 -1
- package/public/assets/{activity-Bk9LEUr4.js → activity-D_shwAZt.js} +2 -2
- package/public/assets/{activity-Bk9LEUr4.js.map → activity-D_shwAZt.js.map} +1 -1
- package/public/assets/{agent-profiles-DSVDoQ_N.js → agent-profiles-BrfVz5IV.js} +2 -2
- package/public/assets/{agent-profiles-DSVDoQ_N.js.map → agent-profiles-BrfVz5IV.js.map} +1 -1
- package/public/assets/agents-B5w-Z9Ic.js +2 -0
- package/public/assets/{agents-8rQuJqii.js.map → agents-B5w-Z9Ic.js.map} +1 -1
- package/public/assets/{analytics-DifGnk7q.js → analytics-Bm3D12UN.js} +2 -2
- package/public/assets/{analytics-DifGnk7q.js.map → analytics-Bm3D12UN.js.map} +1 -1
- package/public/assets/{automation-CMxQAf3l.js → automation-DOTL8BFs.js} +2 -2
- package/public/assets/{automation-CMxQAf3l.js.map → automation-DOTL8BFs.js.map} +1 -1
- package/public/assets/{branch-state-badge-CyC5Qrhc.js → branch-state-badge-BlqeJZ5-.js} +2 -2
- package/public/assets/{branch-state-badge-CyC5Qrhc.js.map → branch-state-badge-BlqeJZ5-.js.map} +1 -1
- package/public/assets/{channels-BLkdt2XR.js → channels-BoiKfK6Q.js} +2 -2
- package/public/assets/{channels-BLkdt2XR.js.map → channels-BoiKfK6Q.js.map} +1 -1
- package/public/assets/chat-PeU3iOaa.js +2 -0
- package/public/assets/{chat-jlXywb9G.js.map → chat-PeU3iOaa.js.map} +1 -1
- package/public/assets/{connectors-C0n12Je8.js → connectors-BvV8Hee2.js} +2 -2
- package/public/assets/{connectors-C0n12Je8.js.map → connectors-BvV8Hee2.js.map} +1 -1
- package/public/assets/{formatted-body-CzehtPtx.js → formatted-body-C1FgzlT1.js} +3 -3
- package/public/assets/{formatted-body-CzehtPtx.js.map → formatted-body-C1FgzlT1.js.map} +1 -1
- package/public/assets/{formatted-body-impl-D3Bnt0Fj.js → formatted-body-impl-BTpU1xH4.js} +2 -2
- package/public/assets/{formatted-body-impl-D3Bnt0Fj.js.map → formatted-body-impl-BTpU1xH4.js.map} +1 -1
- package/public/assets/index-DZEahmyc.js +25 -0
- package/public/assets/index-DZEahmyc.js.map +1 -0
- package/public/assets/{insights-3H6KttBi.js → insights-BqmgSpQm.js} +2 -2
- package/public/assets/{insights-3H6KttBi.js.map → insights-BqmgSpQm.js.map} +1 -1
- package/public/assets/{integrations-v14ehskT.js → integrations-CuT92zQi.js} +2 -2
- package/public/assets/{integrations-v14ehskT.js.map → integrations-CuT92zQi.js.map} +1 -1
- package/public/assets/{maintenance-BANYcBcn.js → maintenance-lM7FeXVC.js} +2 -2
- package/public/assets/{maintenance-BANYcBcn.js.map → maintenance-lM7FeXVC.js.map} +1 -1
- package/public/assets/{managed-agents-DCFMWKqt.js → managed-agents-DaPNv3yY.js} +3 -3
- package/public/assets/{managed-agents-DCFMWKqt.js.map → managed-agents-DaPNv3yY.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CfaR-GRX.js → markdown-preview-impl-Wia3Ho1s.js} +2 -2
- package/public/assets/{markdown-preview-impl-CfaR-GRX.js.map → markdown-preview-impl-Wia3Ho1s.js.map} +1 -1
- package/public/assets/{memory-BmkUEyd5.js → memory-B1v_zD-M.js} +2 -2
- package/public/assets/{memory-BmkUEyd5.js.map → memory-B1v_zD-M.js.map} +1 -1
- package/public/assets/{messages-BQ_fVnSA.js → messages-BNOx3yK6.js} +2 -2
- package/public/assets/{messages-BQ_fVnSA.js.map → messages-BNOx3yK6.js.map} +1 -1
- package/public/assets/{orchestrators-DC7y9vQD.js → orchestrators-DWFy4asl.js} +2 -2
- package/public/assets/{orchestrators-DC7y9vQD.js.map → orchestrators-DWFy4asl.js.map} +1 -1
- package/public/assets/{overview-6lzywFHq.js → overview-C_12cFPM.js} +2 -2
- package/public/assets/{overview-6lzywFHq.js.map → overview-C_12cFPM.js.map} +1 -1
- package/public/assets/{pairs-CRrRIaHI.js → pairs-DIc7gX83.js} +2 -2
- package/public/assets/{pairs-CRrRIaHI.js.map → pairs-DIc7gX83.js.map} +1 -1
- package/public/assets/{projects-CZJW35C7.js → projects-DPjvJUzT.js} +2 -2
- package/public/assets/{projects-CZJW35C7.js.map → projects-DPjvJUzT.js.map} +1 -1
- package/public/assets/{prompt-settings-DvlnMq8H.js → prompt-settings-CtumNgG8.js} +2 -2
- package/public/assets/{prompt-settings-DvlnMq8H.js.map → prompt-settings-CtumNgG8.js.map} +1 -1
- package/public/assets/{registry-3TWg4zbE.js → registry-DzUhgMq9.js} +2 -2
- package/public/assets/{registry-3TWg4zbE.js.map → registry-DzUhgMq9.js.map} +1 -1
- package/public/assets/{security-A2VYKHeI.js → security-BJ3_u0uY.js} +2 -2
- package/public/assets/{security-A2VYKHeI.js.map → security-BJ3_u0uY.js.map} +1 -1
- package/public/assets/{select-_gK5Ojjc.js → select-AWvlctyQ.js} +2 -2
- package/public/assets/{select-_gK5Ojjc.js.map → select-AWvlctyQ.js.map} +1 -1
- package/public/assets/{settings-BZ26ewcu.js → settings-5ofqi-bN.js} +4 -4
- package/public/assets/settings-5ofqi-bN.js.map +1 -0
- package/public/assets/store-D5et8685.js +9 -0
- package/public/assets/store-D5et8685.js.map +1 -0
- package/public/assets/{tasks-DlokSl_P.js → tasks-BEE9bBf7.js} +2 -2
- package/public/assets/{tasks-DlokSl_P.js.map → tasks-BEE9bBf7.js.map} +1 -1
- package/public/assets/{teams-t7M1HMgS.js → teams-C27H_Gwo.js} +2 -2
- package/public/assets/{teams-t7M1HMgS.js.map → teams-C27H_Gwo.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DxgrO-jH.js → terminal-viewer-impl-WD2TboA7.js} +22 -22
- package/public/assets/{terminal-viewer-impl-DxgrO-jH.js.map → terminal-viewer-impl-WD2TboA7.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-V8LR-3Gi.js → work-queue-CtmPK2hK.js} +2 -2
- package/public/assets/{work-queue-V8LR-3Gi.js.map → work-queue-CtmPK2hK.js.map} +1 -1
- package/public/assets/{workspaces-DI317fMk.js → workspaces-B26EKv_H.js} +2 -2
- package/public/assets/{workspaces-DI317fMk.js.map → workspaces-B26EKv_H.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/agent-card-projection.ts +39 -9
- package/src/bus-outbox.ts +30 -0
- package/src/bus.ts +57 -1
- package/src/commands-db.ts +14 -0
- package/src/config-store.ts +2 -0
- package/src/config.ts +4 -0
- package/src/db/connection.ts +3 -1
- package/src/db/delivery.ts +85 -3
- package/src/db/index.ts +1 -0
- package/src/db/maintenance-worker.ts +91 -0
- package/src/db/messages.ts +19 -4
- package/src/db/migrations.ts +7 -0
- package/src/db/retention.ts +130 -0
- package/src/db/schema.ts +5 -0
- package/src/db/stats.ts +32 -8
- package/src/db/task-entity.ts +7 -8
- package/src/event-loop-lag.ts +90 -0
- package/src/lifecycle-manager.ts +11 -12
- package/src/maintenance/jobs.ts +23 -7
- package/src/maintenance/workspaces.ts +28 -47
- package/src/managed-policy-escalation.ts +47 -0
- package/src/mcp-task-tools.ts +4 -5
- package/src/retention-config-store.ts +32 -0
- package/src/retention-config.ts +58 -0
- package/src/routes/commands.ts +8 -245
- package/src/routes/messages.ts +13 -1
- package/src/routes/tasks.ts +3 -5
- package/src/routing-policy-store.ts +11 -1
- package/src/server.ts +2 -0
- package/src/services/issue-lifecycle.ts +23 -8
- package/src/services/send-message.ts +63 -21
- package/src/services/spawn-registration-watchdog.ts +74 -9
- package/src/services/task-entity.ts +54 -2
- package/src/settings/registry.ts +11 -0
- package/src/sse.ts +54 -9
- package/src/workspace-auto-merge.ts +94 -34
- package/src/workspace-command-settle.ts +227 -0
- package/src/workspace-escalation.ts +45 -0
- package/src/workspace-merge-progress.ts +16 -0
- package/src/workspace-orphans.ts +91 -3
- package/public/assets/agents-8rQuJqii.js +0 -2
- package/public/assets/chat-jlXywb9G.js +0 -2
- package/public/assets/index-4g3PbGVN.js +0 -25
- package/public/assets/index-4g3PbGVN.js.map +0 -1
- package/public/assets/settings-BZ26ewcu.js.map +0 -1
- package/public/assets/store-nXGxgz2Q.js +0 -9
- package/public/assets/store-nXGxgz2Q.js.map +0 -1
package/src/mcp-task-tools.ts
CHANGED
|
@@ -88,6 +88,7 @@ export const TASK_TOOLS = [
|
|
|
88
88
|
teamId: { type: "string" },
|
|
89
89
|
projectId: { type: "string" },
|
|
90
90
|
repo: { type: "string", description: "owner/repo for the task's project/repository" },
|
|
91
|
+
branch: { type: "string", description: "optional branch ref to seed on the task" },
|
|
91
92
|
dispatch: DISPATCH_SCHEMA,
|
|
92
93
|
issueRefs: { type: "array", items: { type: "string" }, description: "#n, owner/repo#n, GitHub URL, or issue_<id>" },
|
|
93
94
|
defaultRepo: { type: "string", description: "owner/repo for bare #n issue refs" },
|
|
@@ -140,14 +141,12 @@ export function relayTaskTool(auth: McpAuthLike, name: string, args: Record<stri
|
|
|
140
141
|
teamId: cleanString(args.teamId, "teamId", { max: 200 }),
|
|
141
142
|
projectId: cleanString(args.projectId, "projectId", { max: 120 }),
|
|
142
143
|
repo: cleanString(args.repo, "repo", { max: 500 }),
|
|
144
|
+
branch: cleanString(args.branch, "branch", { max: 500 }),
|
|
143
145
|
dispatch: args.dispatch as TaskDispatch | undefined,
|
|
146
|
+
issueRefs: Array.isArray(args.issueRefs) ? args.issueRefs.filter((ref): ref is string => typeof ref === "string") : undefined,
|
|
147
|
+
defaultRepo: cleanString(args.defaultRepo, "defaultRepo", { max: 500 }),
|
|
144
148
|
createdBy: actor,
|
|
145
149
|
});
|
|
146
|
-
if (Array.isArray(args.issueRefs)) {
|
|
147
|
-
for (const ref of args.issueRefs) {
|
|
148
|
-
if (typeof ref === "string" && ref.trim()) addTaskEntityRef(created.id, { issue: ref, defaultRepo: cleanString(args.defaultRepo, "defaultRepo", { max: 500 }), actor });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
150
|
if (Array.isArray(args.dependsOn)) {
|
|
152
151
|
for (const dep of args.dependsOn) addTaskEntityDependency(created.id, taskIdArg(dep), actor);
|
|
153
152
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getConfig, setConfig } from "./config-store";
|
|
2
|
+
import {
|
|
3
|
+
RETENTION_CONFIG_DEFAULTS,
|
|
4
|
+
RETENTION_KEY,
|
|
5
|
+
RETENTION_NAMESPACE,
|
|
6
|
+
validateRetentionConfig,
|
|
7
|
+
type RetentionConfig,
|
|
8
|
+
} from "./retention-config";
|
|
9
|
+
import type { ConfigEntry } from "./types";
|
|
10
|
+
|
|
11
|
+
/** Instance-wide retention config, merged over defaults (always returns a usable value). */
|
|
12
|
+
export function getRetentionConfig(): RetentionConfig {
|
|
13
|
+
const entry = getConfig<Partial<RetentionConfig>>(RETENTION_NAMESPACE, RETENTION_KEY);
|
|
14
|
+
if (!entry) return { ...RETENTION_CONFIG_DEFAULTS };
|
|
15
|
+
return validateRetentionConfig({ ...RETENTION_CONFIG_DEFAULTS, ...entry.value });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getRetentionConfigEntry(): ConfigEntry<RetentionConfig> {
|
|
19
|
+
const entry = getConfig<RetentionConfig>(RETENTION_NAMESPACE, RETENTION_KEY);
|
|
20
|
+
return entry ?? {
|
|
21
|
+
namespace: RETENTION_NAMESPACE,
|
|
22
|
+
key: RETENTION_KEY,
|
|
23
|
+
value: { ...RETENTION_CONFIG_DEFAULTS },
|
|
24
|
+
version: 0,
|
|
25
|
+
updatedAt: "default",
|
|
26
|
+
updatedBy: "system",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setRetentionConfig(value: unknown, updatedBy?: string): ConfigEntry<RetentionConfig> {
|
|
31
|
+
return setConfig(RETENTION_NAMESPACE, RETENTION_KEY, value as RetentionConfig, updatedBy);
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { ValidationError } from "./db/connection.ts";
|
|
3
|
+
|
|
4
|
+
export const RETENTION_NAMESPACE = "retention";
|
|
5
|
+
export const RETENTION_KEY = "default";
|
|
6
|
+
|
|
7
|
+
export interface RetentionConfig {
|
|
8
|
+
messageRetentionDays: number;
|
|
9
|
+
activityEventRetentionDays: number;
|
|
10
|
+
activityEventMaxRows: number;
|
|
11
|
+
spawnCommandTerminalRetentionDays: number;
|
|
12
|
+
pruneBatchSize: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const RETENTION_CONFIG_DEFAULTS: RetentionConfig = {
|
|
16
|
+
messageRetentionDays: 30,
|
|
17
|
+
activityEventRetentionDays: 14,
|
|
18
|
+
activityEventMaxRows: 100_000,
|
|
19
|
+
spawnCommandTerminalRetentionDays: 14,
|
|
20
|
+
pruneBatchSize: 10_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const RETENTION_CONFIG_SCHEMA = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
messageRetentionDays: { type: "integer", minimum: 1, maximum: 3650 },
|
|
27
|
+
activityEventRetentionDays: { type: "integer", minimum: 1, maximum: 3650 },
|
|
28
|
+
activityEventMaxRows: { type: "integer", minimum: 1, maximum: 10_000_000 },
|
|
29
|
+
spawnCommandTerminalRetentionDays: { type: "integer", minimum: 1, maximum: 3650 },
|
|
30
|
+
pruneBatchSize: { type: "integer", minimum: 1, maximum: 50_000 },
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
function cleanInteger(value: unknown, field: string, fallback: number, min: number, max: number): number {
|
|
36
|
+
if (value === undefined || value === null) return fallback;
|
|
37
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
38
|
+
throw new ValidationError(`${field} must be an integer between ${min} and ${max}`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateRetentionConfig(value: unknown): RetentionConfig {
|
|
44
|
+
if (!isRecord(value)) throw new ValidationError("retention config value must be an object");
|
|
45
|
+
return {
|
|
46
|
+
messageRetentionDays: cleanInteger(value.messageRetentionDays, "messageRetentionDays", RETENTION_CONFIG_DEFAULTS.messageRetentionDays, 1, 3650),
|
|
47
|
+
activityEventRetentionDays: cleanInteger(value.activityEventRetentionDays, "activityEventRetentionDays", RETENTION_CONFIG_DEFAULTS.activityEventRetentionDays, 1, 3650),
|
|
48
|
+
activityEventMaxRows: cleanInteger(value.activityEventMaxRows, "activityEventMaxRows", RETENTION_CONFIG_DEFAULTS.activityEventMaxRows, 1, 10_000_000),
|
|
49
|
+
spawnCommandTerminalRetentionDays: cleanInteger(
|
|
50
|
+
value.spawnCommandTerminalRetentionDays,
|
|
51
|
+
"spawnCommandTerminalRetentionDays",
|
|
52
|
+
RETENTION_CONFIG_DEFAULTS.spawnCommandTerminalRetentionDays,
|
|
53
|
+
1,
|
|
54
|
+
3650,
|
|
55
|
+
),
|
|
56
|
+
pruneBatchSize: cleanInteger(value.pruneBatchSize, "pruneBatchSize", RETENTION_CONFIG_DEFAULTS.pruneBatchSize, 1, 50_000),
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/routes/commands.ts
CHANGED
|
@@ -1,76 +1,28 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: commands.
|
|
2
2
|
import { VALID_AGENT_ACTIONS, agentControlActionIcon, auditEvent, authorizeRoute, emitCommand, error, json, parseBody, parseQueryInt, type AgentControlAction, type Handler } from "./_shared";
|
|
3
3
|
import { VALID_WORKSPACE_STATUSES, cleanParams, cleanString, optionalEnum } from "../validation";
|
|
4
|
-
import { ValidationError, deleteWorkspace, getOrchestrator, getWorkspace, patchWorkspaceMetadata,
|
|
4
|
+
import { ValidationError, deleteWorkspace, getOrchestrator, getWorkspace, patchWorkspaceMetadata, setOrchestratorUpgradeState, updateWorkspaceStatus } from "../db";
|
|
5
5
|
import { applyCommandToRecipe } from "../recipe-runner";
|
|
6
|
-
import { claimMetadataPatch } from "../workspace-claim";
|
|
7
6
|
import { clearActiveMemories, injectAlwaysReloadMemories } from "../memory-service";
|
|
8
7
|
import { getCommand, listCommands, updateCommand } from "../commands-db";
|
|
9
8
|
import { emitOrchestratorStatus } from "../sse";
|
|
10
9
|
import { getCompactionWatch } from "../compaction-watch";
|
|
11
|
-
import { isRecord
|
|
12
|
-
import { storeTextArtifact } from "../artifact-text";
|
|
10
|
+
import { isRecord } from "agent-relay-sdk";
|
|
13
11
|
import { authContextFromRequest } from "../services/auth-context";
|
|
14
12
|
import { commandAuthorizationResource as dispatchCommandAuthorizationResource, dispatchCommand } from "../services/dispatch-command";
|
|
15
13
|
import { prepareScheduledCommandResultForStorage, settleScheduledCommandTimer } from "../services/scheduler-command";
|
|
16
|
-
import { emitWorkspaceTaskSemanticNotifications } from "../services/task-lifecycle";
|
|
17
14
|
import { ServiceAuthError } from "../services/errors";
|
|
18
|
-
import { notifyBaseWorktreeStrand, notifyBranchLandFailed, notifyBranchLanded, notifyLandGateFailed, notifyLandGateWarnings } from "../branch-landed";
|
|
19
15
|
import { applyIdleRefreshResult } from "../maintenance/workspaces";
|
|
20
16
|
import { seedProviderConfigs } from "../provider-config-store";
|
|
21
17
|
import { notifyAgentSpawnFailed } from "../agent-lifecycle-events";
|
|
22
|
-
import {
|
|
23
|
-
import { CLEAR_MERGE_NO_PROGRESS, noteMergeNoProgress, scheduleRepoMerge } from "../workspace-auto-merge";
|
|
18
|
+
import { scheduleRepoMerge } from "../workspace-auto-merge";
|
|
24
19
|
import { enqueueContinuationInjectionAfterSelfResume } from "../self-resume";
|
|
25
20
|
import { type Command, type CommandStatus, type CreateCommandInput, type WorkspaceStatus } from "../types";
|
|
26
21
|
import { commandTargetsWorkspaceOwner } from "../workspace-host";
|
|
22
|
+
import { settleWorkspaceCommand } from "../workspace-command-settle";
|
|
27
23
|
|
|
28
24
|
const VALID_COMMAND_STATUSES = ["pending", "accepted", "running", "succeeded", "failed", "timed_out", "rejected", "canceled"] as const;
|
|
29
25
|
|
|
30
|
-
// #902 — normalize a land-gate run result that round-tripped through the command bus
|
|
31
|
-
// (JSON, loosely typed) back into a LandGateRunResult. Returns undefined when the value
|
|
32
|
-
// isn't a gate result, so the caller falls back to the generic land-failed path.
|
|
33
|
-
function normalizeGateRunResult(value: unknown): LandGateRunResult | undefined {
|
|
34
|
-
if (!isRecord(value)) return undefined;
|
|
35
|
-
const name = typeof value.name === "string" ? value.name : undefined;
|
|
36
|
-
const command = typeof value.command === "string" ? value.command : "";
|
|
37
|
-
if (!name) return undefined;
|
|
38
|
-
return {
|
|
39
|
-
name,
|
|
40
|
-
command,
|
|
41
|
-
optional: value.optional === true,
|
|
42
|
-
passed: value.passed === true,
|
|
43
|
-
exitCode: typeof value.exitCode === "number" ? value.exitCode : null,
|
|
44
|
-
timedOut: value.timedOut === true,
|
|
45
|
-
durationMs: typeof value.durationMs === "number" ? value.durationMs : 0,
|
|
46
|
-
outputTail: typeof value.outputTail === "string" ? value.outputTail : "",
|
|
47
|
-
output: typeof value.output === "string" ? value.output : (typeof value.outputTail === "string" ? value.outputTail : ""),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeGateWarnings(value: unknown): LandGateRunResult[] {
|
|
52
|
-
if (!Array.isArray(value)) return [];
|
|
53
|
-
return value.map(normalizeGateRunResult).filter((g): g is LandGateRunResult => Boolean(g));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Upload a gate's full output to a relay artifact; returns its id, or undefined on any
|
|
57
|
-
// failure (artifact upload is best-effort — the tail still rides in the notification).
|
|
58
|
-
async function uploadGateOutputArtifact(workspaceId: string, gate: LandGateRunResult): Promise<string | undefined> {
|
|
59
|
-
try {
|
|
60
|
-
const safeName = sanitizeFsName(gate.name, { replacement: "-", maxLen: 60, trimEdge: true, fallback: "gate" });
|
|
61
|
-
const stored = await storeTextArtifact({
|
|
62
|
-
text: gate.output || gate.outputTail || "(no output)",
|
|
63
|
-
filename: `land-gate-${safeName}-${workspaceId}.log`,
|
|
64
|
-
createdBy: "server",
|
|
65
|
-
metadata: { kind: "land-gate-output", workspaceId, gateName: gate.name, gateCommand: gate.command, exitCode: gate.exitCode, timedOut: gate.timedOut },
|
|
66
|
-
});
|
|
67
|
-
return stored.id;
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.warn(`land-gate artifact upload failed for ${workspaceId} gate "${gate.name}": ${String(error)}`);
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
26
|
function auditCommandOutcome(command: Command): void {
|
|
75
27
|
if (command.status !== "succeeded" && command.status !== "failed") return;
|
|
76
28
|
if (command.type !== "agent.restart" && command.type !== "agent.shutdown" && command.type !== "agent.reconnect") return;
|
|
@@ -232,204 +184,15 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
232
184
|
if (command.type === "scheduler.command") {
|
|
233
185
|
await settleScheduledCommandTimer(command);
|
|
234
186
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (workspace && commandTargetsWorkspaceOwner(command, workspace)) {
|
|
240
|
-
updateWorkspaceStatus(workspaceId, "cleaned", {
|
|
241
|
-
cleanupResult: command.result,
|
|
242
|
-
cleanupCommandId: command.id,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
187
|
+
const workspaceSettle = settleWorkspaceCommand(command, { onMergeSettled: scheduleRepoMerge });
|
|
188
|
+
if (workspaceSettle.ignoredWrongOwner) {
|
|
189
|
+
auditCommandOutcome(command);
|
|
190
|
+
return json(command);
|
|
246
191
|
}
|
|
247
192
|
if (command.type === "workspace.idle-refresh" && command.status === "succeeded" && isRecord(command.result)) {
|
|
248
193
|
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
249
194
|
if (workspaceId) applyIdleRefreshResult(workspaceId, command.result);
|
|
250
195
|
}
|
|
251
|
-
if (command.type === "workspace.merge") {
|
|
252
|
-
// Merge settled (either way) — free the per-repo merge lease so the next
|
|
253
|
-
// base merge can proceed (issue #157).
|
|
254
|
-
if (command.status === "succeeded" || command.status === "failed") {
|
|
255
|
-
releaseMergeLease({ commandId: command.id });
|
|
256
|
-
// #237: the lease just freed — drain the next ready/review_requested row for
|
|
257
|
-
// this repo right now instead of waiting for the backstop sweep. This is the
|
|
258
|
-
// on-settle link of the sequential per-repo drain chain (no-op if nothing else
|
|
259
|
-
// is queued, or if AUTO_MERGE is disabled).
|
|
260
|
-
const settledRepoRoot = typeof command.params.repoRoot === "string" ? command.params.repoRoot : undefined;
|
|
261
|
-
scheduleRepoMerge(settledRepoRoot);
|
|
262
|
-
}
|
|
263
|
-
if (command.status === "succeeded" && isRecord(command.result)) {
|
|
264
|
-
if (handleReviewerMergeResult(command, command.result)) {
|
|
265
|
-
emitCommand(command);
|
|
266
|
-
auditCommandOutcome(command);
|
|
267
|
-
return json(command);
|
|
268
|
-
}
|
|
269
|
-
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
270
|
-
const resultStatus = optionalEnum(command.result.status, "result.status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
|
|
271
|
-
if (workspaceId && resultStatus) {
|
|
272
|
-
// Snapshot the row BEFORE the recycle repoints `branch` (#206) — the landed
|
|
273
|
-
// branch name + author (#239 branch.landed push) come from this pre-mutation state.
|
|
274
|
-
const landedWorkspace = getWorkspace(workspaceId);
|
|
275
|
-
if (landedWorkspace && !commandTargetsWorkspaceOwner(command, landedWorkspace)) {
|
|
276
|
-
auditCommandOutcome(command);
|
|
277
|
-
return json(command);
|
|
278
|
-
}
|
|
279
|
-
const newBranch = cleanString(command.result.newBranch, "result.newBranch", { max: 240 });
|
|
280
|
-
const normalizedStatus = command.result.merged === true
|
|
281
|
-
&& !newBranch
|
|
282
|
-
&& ["review_requested", "merge_planned", "conflict"].includes(resultStatus)
|
|
283
|
-
? "merged"
|
|
284
|
-
: resultStatus;
|
|
285
|
-
updateWorkspaceStatus(workspaceId, normalizedStatus, {
|
|
286
|
-
mergeResult: command.result,
|
|
287
|
-
mergeCommandId: command.id,
|
|
288
|
-
mergedAt: Date.now(),
|
|
289
|
-
// A merge that settles `succeeded` made genuine progress (land / recycle /
|
|
290
|
-
// noop-resolve / PR-opened) — clear any prior no-progress back-off so the lane
|
|
291
|
-
// is attempted normally next time (#638).
|
|
292
|
-
...CLEAR_MERGE_NO_PROGRESS,
|
|
293
|
-
mergeError: undefined,
|
|
294
|
-
...(normalizedStatus !== resultStatus ? { mergeStatusNormalizedFrom: resultStatus } : {}),
|
|
295
|
-
// The land consumes the steward's claim (#208 steward contract / vent
|
|
296
|
-
// #62): a successful land must release it, or `steward inspect` keeps
|
|
297
|
-
// showing a stale claim on the recycled workspace and blocks the next
|
|
298
|
-
// automation. Clearing a null claim (auto-merge path) is a harmless no-op.
|
|
299
|
-
...claimMetadataPatch(true),
|
|
300
|
-
});
|
|
301
|
-
// Land-and-continue (#206): the worktree was recycled onto a fresh branch.
|
|
302
|
-
// Repoint the row so the next merge targets the live branch, not the deleted one.
|
|
303
|
-
const mergedSha = cleanString(command.result.mergedSha, "result.mergedSha", { max: 64 });
|
|
304
|
-
// base_sha tracks the tip the recycled workspace forks from. On a no-ff land
|
|
305
|
-
// (#287) that's the merge commit (result.baseSha), not the preserved landed
|
|
306
|
-
// commit (mergedSha); fall back to mergedSha for older orchestrators.
|
|
307
|
-
const baseSha = cleanString(command.result.baseSha, "result.baseSha", { max: 64 });
|
|
308
|
-
if (newBranch) {
|
|
309
|
-
setWorkspaceBranch(workspaceId, newBranch, baseSha ?? mergedSha);
|
|
310
|
-
}
|
|
311
|
-
// #239 — push the author a "your branch landed" notice (no polling). Fires on a
|
|
312
|
-
// real land; a no-op resolution (#230) merged nothing, so it earns no notice.
|
|
313
|
-
// #423 — a PR-land RECYCLE is a no-op merge (`merged:false`) but a land DID happen
|
|
314
|
-
// on the remote: the relay carries that ground truth in `params.prLanded`, so the
|
|
315
|
-
// notice fires from it with the recycled `newBranch` — full rebase-ff parity.
|
|
316
|
-
const prLanded = isRecord(command.params.prLanded) ? command.params.prLanded : undefined;
|
|
317
|
-
// #824 — the land advanced a DIRTY base by ref plumbing and the shared checkout was
|
|
318
|
-
// left in a MIXED state (HEAD moved, some landed working files didn't). The land
|
|
319
|
-
// succeeded, but surface the strand LOUDLY so it's reconciled before anything reads/
|
|
320
|
-
// builds/publishes from the stale checkout — never silently as before.
|
|
321
|
-
const baseWorktreeSync = isRecord(command.result.baseWorktreeSync) ? command.result.baseWorktreeSync : undefined;
|
|
322
|
-
if (baseWorktreeSync && baseWorktreeSync.reconciled === false && landedWorkspace) {
|
|
323
|
-
notifyBaseWorktreeStrand({
|
|
324
|
-
workspace: landedWorkspace,
|
|
325
|
-
sync: {
|
|
326
|
-
reconciled: false,
|
|
327
|
-
unsyncedPaths: Array.isArray(baseWorktreeSync.unsyncedPaths) ? baseWorktreeSync.unsyncedPaths.filter((p): p is string => typeof p === "string") : undefined,
|
|
328
|
-
preservedWipPaths: Array.isArray(baseWorktreeSync.preservedWipPaths) ? baseWorktreeSync.preservedWipPaths.filter((p): p is string => typeof p === "string") : undefined,
|
|
329
|
-
message: typeof baseWorktreeSync.message === "string" ? baseWorktreeSync.message : undefined,
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
if ((command.result.merged === true || prLanded) && landedWorkspace) {
|
|
334
|
-
notifyBranchLanded({
|
|
335
|
-
workspace: landedWorkspace,
|
|
336
|
-
mergedSha: prLanded ? cleanString(prLanded.sha, "params.prLanded.sha", { max: 64 }) ?? mergedSha : mergedSha,
|
|
337
|
-
subject: prLanded
|
|
338
|
-
? cleanString(prLanded.subject, "params.prLanded.subject", { max: 200 })
|
|
339
|
-
: cleanString(command.result.subject, "result.subject", { max: 200 }),
|
|
340
|
-
// PR-land subjects carry a trailing `(#PR)` GitHub stamps on the squash — not an
|
|
341
|
-
// issue. Tell the lifecycle hook so it won't close the PR number as an issue (#482).
|
|
342
|
-
prLand: Boolean(prLanded),
|
|
343
|
-
newBranch,
|
|
344
|
-
pushed: prLanded ? true : typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
// #902 — OPTIONAL land gates that failed on a SUCCESSFUL land (merged:true, ref
|
|
348
|
-
// advanced). Warn the worker (tail + combined-output artifact) without blocking.
|
|
349
|
-
// Best-effort + isolated so a warn-notify throw can't undo the completed land.
|
|
350
|
-
const gateWarnings = command.result.merged === true ? normalizeGateWarnings(command.result.gateWarnings) : [];
|
|
351
|
-
if (gateWarnings.length && landedWorkspace) {
|
|
352
|
-
const warnWorkspace = landedWorkspace;
|
|
353
|
-
void (async () => {
|
|
354
|
-
const combined = gateWarnings.map((g) => `### ${g.name} (${g.command}) — ${g.timedOut ? "timed out" : g.exitCode === null ? "could not run" : `exited ${g.exitCode}`}\n${g.output || g.outputTail || "(no output)"}`).join("\n\n");
|
|
355
|
-
let artifactId: string | undefined;
|
|
356
|
-
try {
|
|
357
|
-
artifactId = (await storeTextArtifact({ text: combined, filename: `land-gate-warnings-${warnWorkspace.id}.log`, createdBy: "server", metadata: { kind: "land-gate-warnings", workspaceId: warnWorkspace.id, gateNames: gateWarnings.map((g) => g.name) } })).id;
|
|
358
|
-
} catch (error) {
|
|
359
|
-
console.warn(`land-gate-warnings artifact upload failed for ${warnWorkspace.id}: ${String(error)}`);
|
|
360
|
-
}
|
|
361
|
-
try {
|
|
362
|
-
notifyLandGateWarnings({ workspace: warnWorkspace, warnings: gateWarnings, artifactId });
|
|
363
|
-
} catch (error) {
|
|
364
|
-
console.warn(`land-gate-warnings notify threw for ${warnWorkspace.id}: ${String(error)}`);
|
|
365
|
-
}
|
|
366
|
-
})();
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
} else if (command.status === "failed" && command.correlationId) {
|
|
370
|
-
// Merge couldn't complete — don't leave it stuck in merge_planned. A no-progress
|
|
371
|
-
// merge now settles `failed` (orchestrator #638) carrying the result; honor the
|
|
372
|
-
// orchestrator's intended target status when it gave a valid one so a real
|
|
373
|
-
// `conflict` routes to the steward (a non-landable status the auto-merge job
|
|
374
|
-
// skips, which itself stops the loop) rather than bouncing back to the landable
|
|
375
|
-
// `review_requested`. Surface the error and count the no-progress attempt so the
|
|
376
|
-
// auto-merge job backs off after N instead of busy-looping (#638).
|
|
377
|
-
const current = getWorkspace(command.correlationId);
|
|
378
|
-
if (current && current.status === "merge_planned") {
|
|
379
|
-
if (!commandTargetsWorkspaceOwner(command, current)) {
|
|
380
|
-
auditCommandOutcome(command);
|
|
381
|
-
return json(command);
|
|
382
|
-
}
|
|
383
|
-
const failedResultStatus = isRecord(command.result)
|
|
384
|
-
? optionalEnum(command.result.status, "result.status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined
|
|
385
|
-
: undefined;
|
|
386
|
-
const nextStatus: WorkspaceStatus = failedResultStatus && failedResultStatus !== "merge_planned"
|
|
387
|
-
? failedResultStatus
|
|
388
|
-
: "review_requested";
|
|
389
|
-
updateWorkspaceStatus(command.correlationId, nextStatus, {
|
|
390
|
-
mergeCommandId: command.id,
|
|
391
|
-
mergeError: command.error ?? "merge failed",
|
|
392
|
-
...noteMergeNoProgress(current.metadata),
|
|
393
|
-
});
|
|
394
|
-
if (nextStatus === "conflict") {
|
|
395
|
-
emitWorkspaceTaskSemanticNotifications({
|
|
396
|
-
workspace: current,
|
|
397
|
-
transition: "land-conflict",
|
|
398
|
-
actor: "workspace-merge",
|
|
399
|
-
occurrenceKey: `workspace-merge-conflict:${current.id}:${command.id}`,
|
|
400
|
-
outcome: {
|
|
401
|
-
reason: command.error ?? "merge failed",
|
|
402
|
-
commandId: command.id,
|
|
403
|
-
status: nextStatus,
|
|
404
|
-
},
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
const gateFailure = isRecord(command.result) ? normalizeGateRunResult(command.result.gateFailure) : undefined;
|
|
408
|
-
if (gateFailure) {
|
|
409
|
-
// #902 — a REQUIRED land gate failed: the orchestrator aborted WITHOUT advancing
|
|
410
|
-
// the ref and settled status "review_requested" (not "conflict", so the steward
|
|
411
|
-
// path above is never taken). Send the richer worker+coordinator notice (gate name
|
|
412
|
-
// + output tail + full-output artifact) INSTEAD of the generic land-failed wake.
|
|
413
|
-
// Best-effort + isolated: the land already (didn't) happen; a notify/upload throw
|
|
414
|
-
// must not break command settling.
|
|
415
|
-
void (async () => {
|
|
416
|
-
const artifactId = await uploadGateOutputArtifact(current.id, gateFailure);
|
|
417
|
-
try {
|
|
418
|
-
notifyLandGateFailed({ workspace: current, gate: gateFailure, artifactId });
|
|
419
|
-
} catch (error) {
|
|
420
|
-
console.warn(`land-gate-failed notify threw for ${current.id}: ${String(error)}`);
|
|
421
|
-
}
|
|
422
|
-
})();
|
|
423
|
-
} else {
|
|
424
|
-
notifyBranchLandFailed({
|
|
425
|
-
workspace: current,
|
|
426
|
-
commandId: command.id,
|
|
427
|
-
reason: command.error ?? "merge failed",
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
196
|
if (command.type === "workspace.reconcile" && command.status === "succeeded" && isRecord(command.result)) {
|
|
434
197
|
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
435
198
|
const resultStatus = optionalEnum(command.result.status, "result.status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
|
package/src/routes/messages.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { emitMessageClaimed, emitMessageDeleted, emitMessageDeliveryUpdated, emi
|
|
|
7
7
|
import { errMessage, isMechanicalMessageKind, isRecord } from "agent-relay-sdk";
|
|
8
8
|
import { injectMemoryForMessageDelivery } from "../memory-service";
|
|
9
9
|
import { authContextFromRequest } from "../services/auth-context";
|
|
10
|
-
import { sendMessageService } from "../services/send-message";
|
|
10
|
+
import { sendMessagesBatchService, sendMessageService } from "../services/send-message";
|
|
11
11
|
import { AmbiguousTargetError, ServiceAuthError } from "../services/errors";
|
|
12
12
|
import { type AgentSessionGuard, type Message, type SendMessageInput } from "../types";
|
|
13
13
|
|
|
@@ -73,6 +73,7 @@ function isDirectTarget(to: string): boolean {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const VALID_MSG_KINDS = ["chat", "channel.event", "task", "pair", "control", "system", "session"];
|
|
76
|
+
const MAX_MESSAGE_BATCH = 100;
|
|
76
77
|
|
|
77
78
|
export const getQueuedMessagesRoute: Handler = (req) => {
|
|
78
79
|
const url = new URL(req.url);
|
|
@@ -149,6 +150,17 @@ export const postMessage: Handler = async (req) => {
|
|
|
149
150
|
const parsed = await parseBody<unknown>(req);
|
|
150
151
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
151
152
|
try {
|
|
153
|
+
if (isRecord(parsed.body) && Array.isArray(parsed.body.messages)) {
|
|
154
|
+
if (parsed.body.messages.length === 0 || parsed.body.messages.length > MAX_MESSAGE_BATCH) {
|
|
155
|
+
throw new ValidationError(`messages must contain 1-${MAX_MESSAGE_BATCH} items`);
|
|
156
|
+
}
|
|
157
|
+
const inputs = parsed.body.messages.map((message) => normalizeMessageInput(message));
|
|
158
|
+
if (inputs.some((input) => input.kind !== "session")) {
|
|
159
|
+
throw new ValidationError("batch messages only supports kind=session");
|
|
160
|
+
}
|
|
161
|
+
const results = sendMessagesBatchService(inputs, authContextFromRequest(req));
|
|
162
|
+
return json({ messages: results.map((result) => result.message) }, 201);
|
|
163
|
+
}
|
|
152
164
|
const input = normalizeMessageInput(parsed.body);
|
|
153
165
|
if (!input.idempotencyKey) {
|
|
154
166
|
input.idempotencyKey = cleanString(req.headers.get("Idempotency-Key") ?? undefined, "idempotencyKey", { max: 240 });
|
package/src/routes/tasks.ts
CHANGED
|
@@ -130,14 +130,12 @@ export const postTaskCreate: Handler = async (req) => {
|
|
|
130
130
|
teamId: cleanString(body.teamId, "teamId", { max: 200 }),
|
|
131
131
|
projectId: cleanString(body.projectId, "projectId", { max: 120 }),
|
|
132
132
|
repo: cleanString(body.repo, "repo", { max: 500 }),
|
|
133
|
+
branch: cleanString(body.branch, "branch", { max: 500 }),
|
|
133
134
|
dispatch: body.dispatch as TaskDispatch | undefined,
|
|
135
|
+
issueRefs: Array.isArray(body.issueRefs) ? body.issueRefs.filter((ref): ref is string => typeof ref === "string") : undefined,
|
|
136
|
+
defaultRepo: cleanString(body.defaultRepo, "defaultRepo", { max: 500 }),
|
|
134
137
|
createdBy: actorOf(req, body.createdBy),
|
|
135
138
|
});
|
|
136
|
-
if (Array.isArray(body.issueRefs)) {
|
|
137
|
-
for (const ref of body.issueRefs) {
|
|
138
|
-
if (typeof ref === "string" && ref.trim()) addTaskEntityRef(created.id, { issue: ref, defaultRepo: cleanString(body.defaultRepo, "defaultRepo", { max: 500 }), actor: actorOf(req, body.createdBy) });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
139
|
if (Array.isArray(body.dependsOn)) {
|
|
142
140
|
for (const dep of body.dependsOn) {
|
|
143
141
|
const depId = parseId(String(dep));
|
|
@@ -278,7 +278,7 @@ function normalizeProviderQuotaConfig(raw: unknown, provider: string): ProviderQ
|
|
|
278
278
|
if (!isRecord(raw)) throw new ValidationError(`providerQuota.${provider} must be an object`);
|
|
279
279
|
const cfg: ProviderQuotaRoutingConfig = {};
|
|
280
280
|
if (raw.hardBackstopUtilization !== undefined) {
|
|
281
|
-
cfg.hardBackstopUtilization =
|
|
281
|
+
cfg.hardBackstopUtilization = cleanHardBackstopFraction(raw.hardBackstopUtilization);
|
|
282
282
|
}
|
|
283
283
|
if (raw.burnPenaltyUtilizationFloor !== undefined) {
|
|
284
284
|
cfg.burnPenaltyUtilizationFloor = cleanFraction(raw.burnPenaltyUtilizationFloor, `providerQuota.${provider}.burnPenaltyUtilizationFloor`);
|
|
@@ -293,6 +293,16 @@ function cleanQuotaFraction(value: unknown, field: string): number {
|
|
|
293
293
|
return value;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
function cleanHardBackstopFraction(value: unknown): number {
|
|
297
|
+
// hardBackstopUtilization=0 is an intentional contract: disable the hard backstop
|
|
298
|
+
// for that provider. Other invalid values normalize to the default instead.
|
|
299
|
+
if (value === 0) return 0;
|
|
300
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
301
|
+
return 0.99;
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
296
306
|
function cleanFraction(value: unknown, field: string): number {
|
|
297
307
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
298
308
|
throw new ValidationError(`${field} must be a number between 0 and 1`);
|
package/src/server.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { installNotificationSubscriptionRule } from "./notification-subscription
|
|
|
13
13
|
import { installQuotaAdvisoryHandler } from "./quota-advisory";
|
|
14
14
|
import { startPlanLiveBindings } from "./services/plan-live-bindings";
|
|
15
15
|
import { startRelayScheduler } from "./services/scheduler";
|
|
16
|
+
import { startEventLoopLagSampler } from "./event-loop-lag";
|
|
16
17
|
import { recordWorkspaceIssueActivity } from "./services/spawn-issue-activity";
|
|
17
18
|
import { onWorkspaceCreate } from "./db";
|
|
18
19
|
import { isTerminalSessionRequestAuthorized } from "./terminal-authz";
|
|
@@ -80,6 +81,7 @@ export function startServer(): void {
|
|
|
80
81
|
getCompactionWatch().start();
|
|
81
82
|
reconcileRelayVersion();
|
|
82
83
|
startConnectorStatusPoller();
|
|
84
|
+
startEventLoopLagSampler();
|
|
83
85
|
startMaintenanceScheduler();
|
|
84
86
|
startRelayScheduler();
|
|
85
87
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errMessage } from "agent-relay-sdk";
|
|
2
|
-
import { getAgent, listPlansWithBoundOwner, deliverableTaskIdsByBranch } from "../db";
|
|
2
|
+
import { getAgent, listPlansWithBoundOwner, deliverableTaskIdsByBranch, deliverableTaskIdsByOwnerAgent, deliverableTaskIdsBySpawnRequestId } from "../db";
|
|
3
3
|
import { listIssuesForEntity, planNodeIssueEntityId } from "../db/issue-associations";
|
|
4
4
|
import {
|
|
5
5
|
getSuccessfulIssueLifecycleAction,
|
|
@@ -142,17 +142,28 @@ function associatedIssueRefs(workspace: LifecycleWorkspace): IssueRef[] {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// advances the Task itself on land is #831.
|
|
148
|
-
if (workspace.branch) {
|
|
149
|
-
for (const taskId of deliverableTaskIdsByBranch(workspace.branch)) {
|
|
150
|
-
refs.push(...listIssuesForEntity("task", String(taskId)).map((item) => item.issueRef));
|
|
151
|
-
}
|
|
145
|
+
for (const taskId of associatedTaskIds(workspace)) {
|
|
146
|
+
refs.push(...listIssuesForEntity("task", String(taskId)).map((item) => item.issueRef));
|
|
152
147
|
}
|
|
153
148
|
return issueRefMap(refs);
|
|
154
149
|
}
|
|
155
150
|
|
|
151
|
+
function associatedTaskIds(workspace: LifecycleWorkspace): number[] {
|
|
152
|
+
const ids = new Set<number>();
|
|
153
|
+
const ownerId = workspace.ownerAgentId?.trim();
|
|
154
|
+
if (ownerId) {
|
|
155
|
+
for (const id of deliverableTaskIdsByOwnerAgent(ownerId)) ids.add(id);
|
|
156
|
+
const spawnRequestId = getAgent(ownerId)?.meta?.spawnRequestId;
|
|
157
|
+
if (typeof spawnRequestId === "string" && spawnRequestId.trim()) {
|
|
158
|
+
for (const id of deliverableTaskIdsBySpawnRequestId(spawnRequestId)) ids.add(id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (workspace.branch) {
|
|
162
|
+
for (const id of deliverableTaskIdsByBranch(workspace.branch)) ids.add(id);
|
|
163
|
+
}
|
|
164
|
+
return [...ids].sort((a, b) => a - b);
|
|
165
|
+
}
|
|
166
|
+
|
|
156
167
|
const CLOSING_KEYWORD_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+([^\n\r]+)/gi;
|
|
157
168
|
const ISSUE_REF_AT_START_RE = /^(https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/issues\/[0-9]+(?:[/?#][^\s,)]*)?|[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[0-9]+|#[0-9]+)/;
|
|
158
169
|
const REF_SEPARATOR_RE = /^(?:\s*,\s*|\s+(?:and|&)\s+)/i;
|
|
@@ -400,6 +411,10 @@ export async function handleLandedIssueLifecycle(input: LandedIssueLifecycleInpu
|
|
|
400
411
|
}> {
|
|
401
412
|
if ((input.workspace.baseRef ?? "main") !== "main") return { source: "commit", closed: [], reopened: [], skipped: ["non-default-branch"] };
|
|
402
413
|
const resolved = await resolveIssueRefsForLand(input);
|
|
414
|
+
const taskIds = associatedTaskIds(input.workspace);
|
|
415
|
+
if (resolved.refs.length === 0 && taskIds.length > 0) {
|
|
416
|
+
logFailure(opts, `issue lifecycle found landed task(s) without linked issue refs: tasks=${taskIds.join(",")} workspace=${input.workspace.id} branch=${input.workspace.branch ?? "unknown"}`);
|
|
417
|
+
}
|
|
403
418
|
const closed: CloseIssueLifecycleResult[] = [];
|
|
404
419
|
for (const ref of resolved.refs) {
|
|
405
420
|
closed.push(await closeIssueRefForLand(ref, resolved.source, input, opts));
|