agent-relay-server 0.120.4 → 0.120.6
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-Cqi3XsZ7.js → MessageBubble-So86mU9-.js} +2 -2
- package/public/assets/{MessageBubble-Cqi3XsZ7.js.map → MessageBubble-So86mU9-.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-D6Um05oW.js → PlanGraphPanel-DBZYAsww.js} +2 -2
- package/public/assets/{PlanGraphPanel-D6Um05oW.js.map → PlanGraphPanel-DBZYAsww.js.map} +1 -1
- package/public/assets/{activity-B0LsQHEz.js → activity-CZNwmXg1.js} +2 -2
- package/public/assets/{activity-B0LsQHEz.js.map → activity-CZNwmXg1.js.map} +1 -1
- package/public/assets/{agent-profiles-O--pe5Ic.js → agent-profiles-BhadoGwi.js} +2 -2
- package/public/assets/{agent-profiles-O--pe5Ic.js.map → agent-profiles-BhadoGwi.js.map} +1 -1
- package/public/assets/agents-DWn1xx9x.js +2 -0
- package/public/assets/{agents-Ii6XlVwR.js.map → agents-DWn1xx9x.js.map} +1 -1
- package/public/assets/{analytics-BCzdYWRT.js → analytics-DifGnk7q.js} +2 -2
- package/public/assets/{analytics-BCzdYWRT.js.map → analytics-DifGnk7q.js.map} +1 -1
- package/public/assets/{automation-Crr3Xzui.js → automation-BExrtKkb.js} +2 -2
- package/public/assets/{automation-Crr3Xzui.js.map → automation-BExrtKkb.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bhw2rloy.js → branch-state-badge-CyC5Qrhc.js} +2 -2
- package/public/assets/{branch-state-badge-Bhw2rloy.js.map → branch-state-badge-CyC5Qrhc.js.map} +1 -1
- package/public/assets/{channels-CKs9rvsn.js → channels-BLkdt2XR.js} +2 -2
- package/public/assets/{channels-CKs9rvsn.js.map → channels-BLkdt2XR.js.map} +1 -1
- package/public/assets/chat-C7OhKtr6.js +2 -0
- package/public/assets/{chat-BCwEUj5R.js.map → chat-C7OhKtr6.js.map} +1 -1
- package/public/assets/{connectors-D_kQR_vw.js → connectors-C0n12Je8.js} +2 -2
- package/public/assets/{connectors-D_kQR_vw.js.map → connectors-C0n12Je8.js.map} +1 -1
- package/public/assets/{formatted-body-q_qzt1L-.js → formatted-body-BCQT2qSb.js} +3 -3
- package/public/assets/{formatted-body-q_qzt1L-.js.map → formatted-body-BCQT2qSb.js.map} +1 -1
- package/public/assets/{formatted-body-impl-CSRGuMcM.js → formatted-body-impl-Bk7wnESl.js} +2 -2
- package/public/assets/{formatted-body-impl-CSRGuMcM.js.map → formatted-body-impl-Bk7wnESl.js.map} +1 -1
- package/public/assets/index-CRraCeo_.js +25 -0
- package/public/assets/index-CRraCeo_.js.map +1 -0
- package/public/assets/{insights-DUhXqZTZ.js → insights-DEgES6jz.js} +2 -2
- package/public/assets/{insights-DUhXqZTZ.js.map → insights-DEgES6jz.js.map} +1 -1
- package/public/assets/{integrations-lbg5CjtE.js → integrations-v14ehskT.js} +2 -2
- package/public/assets/{integrations-lbg5CjtE.js.map → integrations-v14ehskT.js.map} +1 -1
- package/public/assets/{maintenance-CQgneSVf.js → maintenance-BgVB6_gO.js} +2 -2
- package/public/assets/{maintenance-CQgneSVf.js.map → maintenance-BgVB6_gO.js.map} +1 -1
- package/public/assets/{managed-agents-DsnYf6fm.js → managed-agents-8_NKSkhI.js} +3 -3
- package/public/assets/{managed-agents-DsnYf6fm.js.map → managed-agents-8_NKSkhI.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-C1ZtGJp6.js → markdown-preview-impl-yxQkrd2M.js} +2 -2
- package/public/assets/{markdown-preview-impl-C1ZtGJp6.js.map → markdown-preview-impl-yxQkrd2M.js.map} +1 -1
- package/public/assets/{memory-SSLUTOI2.js → memory-CJD8eekg.js} +2 -2
- package/public/assets/{memory-SSLUTOI2.js.map → memory-CJD8eekg.js.map} +1 -1
- package/public/assets/{messages-BY26OP4K.js → messages-DLHMZbHZ.js} +2 -2
- package/public/assets/{messages-BY26OP4K.js.map → messages-DLHMZbHZ.js.map} +1 -1
- package/public/assets/{orchestrators-OY7yPzVI.js → orchestrators-DEOep7FX.js} +2 -2
- package/public/assets/{orchestrators-OY7yPzVI.js.map → orchestrators-DEOep7FX.js.map} +1 -1
- package/public/assets/{overview-Dj0u1tPG.js → overview-BgpfO6mt.js} +2 -2
- package/public/assets/{overview-Dj0u1tPG.js.map → overview-BgpfO6mt.js.map} +1 -1
- package/public/assets/{pairs-CrUR50ou.js → pairs-8NVKHh96.js} +2 -2
- package/public/assets/{pairs-CrUR50ou.js.map → pairs-8NVKHh96.js.map} +1 -1
- package/public/assets/{projects-B_AtxHtH.js → projects-CZJW35C7.js} +2 -2
- package/public/assets/{projects-B_AtxHtH.js.map → projects-CZJW35C7.js.map} +1 -1
- package/public/assets/{prompt-settings-CPtYSban.js → prompt-settings-fqyLwUa-.js} +2 -2
- package/public/assets/{prompt-settings-CPtYSban.js.map → prompt-settings-fqyLwUa-.js.map} +1 -1
- package/public/assets/{registry-BgUALWn5.js → registry-BKbLL4mO.js} +2 -2
- package/public/assets/{registry-BgUALWn5.js.map → registry-BKbLL4mO.js.map} +1 -1
- package/public/assets/{security-5lHfGk7g.js → security-BlTFTbq0.js} +2 -2
- package/public/assets/{security-5lHfGk7g.js.map → security-BlTFTbq0.js.map} +1 -1
- package/public/assets/{select-Cgx4QSLj.js → select-Zd_qh0uU.js} +2 -2
- package/public/assets/{select-Cgx4QSLj.js.map → select-Zd_qh0uU.js.map} +1 -1
- package/public/assets/{settings-DFGgxyun.js → settings-nHuSVRkL.js} +2 -2
- package/public/assets/{settings-DFGgxyun.js.map → settings-nHuSVRkL.js.map} +1 -1
- package/public/assets/store-nXGxgz2Q.js +9 -0
- package/public/assets/store-nXGxgz2Q.js.map +1 -0
- package/public/assets/{tasks-BvH5rluu.js → tasks-C3JhQ8vB.js} +2 -2
- package/public/assets/{tasks-BvH5rluu.js.map → tasks-C3JhQ8vB.js.map} +1 -1
- package/public/assets/{teams-VZOIOdoE.js → teams-C9E9Yt39.js} +2 -2
- package/public/assets/{teams-VZOIOdoE.js.map → teams-C9E9Yt39.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DYY3Wmoe.js → terminal-viewer-impl-FP0JlsvS.js} +22 -22
- package/public/assets/{terminal-viewer-impl-DYY3Wmoe.js.map → terminal-viewer-impl-FP0JlsvS.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-B6Xk7QNe.js → work-queue-Ema7Hy3s.js} +2 -2
- package/public/assets/{work-queue-B6Xk7QNe.js.map → work-queue-Ema7Hy3s.js.map} +1 -1
- package/public/assets/{workspaces-CPIzAHAe.js → workspaces-DI317fMk.js} +2 -2
- package/public/assets/{workspaces-CPIzAHAe.js.map → workspaces-DI317fMk.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/auto-spawn-dispatch.ts +2 -2
- package/src/commands-db.ts +14 -0
- package/src/db/delivery.ts +85 -3
- package/src/db/index.ts +2 -0
- package/src/db/migrations.ts +2 -1
- package/src/db/provisioning-callmux.ts +180 -0
- package/src/db/provisioning-registry.ts +45 -374
- package/src/db/provisioning-resolve.ts +245 -0
- package/src/db/stats.ts +10 -8
- package/src/db/task-entity.ts +7 -8
- package/src/lifecycle-manager.ts +11 -12
- package/src/maintenance/jobs.ts +3 -1
- 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/routes/commands.ts +8 -245
- package/src/routes/tasks.ts +3 -5
- package/src/routes/workspaces.ts +58 -22
- package/src/services/issue-lifecycle.ts +23 -8
- package/src/services/task-entity.ts +54 -2
- package/src/task-dispatcher.ts +12 -3
- 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 +96 -4
- package/public/assets/agents-Ii6XlVwR.js +0 -2
- package/public/assets/chat-BCwEUj5R.js +0 -2
- package/public/assets/index-Ceiwd02X.js +0 -25
- package/public/assets/index-Ceiwd02X.js.map +0 -1
- package/public/assets/store-BM4zM89B.js +0 -9
- package/public/assets/store-BM4zM89B.js.map +0 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Profile provisioning resolution lives outside provisioning-registry.ts so the
|
|
2
|
+
// registry CRUD/vendoring module stays under the file-size ratchet.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { getManifest, type ProviderManifest } from "agent-relay-providers";
|
|
7
|
+
import { isRecord } from "agent-relay-sdk";
|
|
8
|
+
import type {
|
|
9
|
+
AgentProfile,
|
|
10
|
+
AgentProfileAssetRef,
|
|
11
|
+
AgentProfileProvider,
|
|
12
|
+
PluginAssetDefinition,
|
|
13
|
+
ProvisioningAssetKind,
|
|
14
|
+
ProvisioningMcpServer,
|
|
15
|
+
ProvisioningSkip,
|
|
16
|
+
ResolvedAssetDefinition,
|
|
17
|
+
ResolvedProvisioning,
|
|
18
|
+
SkillAssetDefinition,
|
|
19
|
+
SpawnProvider,
|
|
20
|
+
} from "agent-relay-sdk";
|
|
21
|
+
import { getDb } from "./connection.ts";
|
|
22
|
+
import { parseJson } from "../utils";
|
|
23
|
+
|
|
24
|
+
type ProviderBundledSkillSetDescriptor = NonNullable<NonNullable<ProviderManifest["provisioning"]>["bundledSkills"]>;
|
|
25
|
+
|
|
26
|
+
interface ProvisioningCapabilityResolutionRow {
|
|
27
|
+
id: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
kind: ProvisioningAssetKind;
|
|
30
|
+
status: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ProvisioningAssetResolutionRow {
|
|
34
|
+
kind: ProvisioningAssetKind;
|
|
35
|
+
name: string;
|
|
36
|
+
provider: AgentProfileProvider;
|
|
37
|
+
definition: string;
|
|
38
|
+
enabled: number;
|
|
39
|
+
approval_status?: string | null;
|
|
40
|
+
validation_status?: string | null;
|
|
41
|
+
validation_errors?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RegistryResolution {
|
|
45
|
+
definition: ResolvedAssetDefinition | null;
|
|
46
|
+
skip?: ProvisioningSkip;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function bundledSkillRefs(descriptor: ProviderBundledSkillSetDescriptor): string[] {
|
|
50
|
+
const skillsDir = resolve(import.meta.dir, "../../", descriptor.dir);
|
|
51
|
+
const entryFile = descriptor.entryFile ?? "SKILL.md";
|
|
52
|
+
if (!existsSync(skillsDir)) return [];
|
|
53
|
+
return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(join(skillsDir, entry.name, entryFile)))
|
|
54
|
+
.map((entry) => descriptor.refPrefix + entry.name).sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function stringRecord(value: Record<string, unknown>): Record<string, string> {
|
|
58
|
+
const out: Record<string, string> = {};
|
|
59
|
+
for (const [k, v] of Object.entries(value)) {
|
|
60
|
+
if (typeof v === "string") out[k] = v;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function rowDefinition(row: ProvisioningAssetResolutionRow): ResolvedAssetDefinition {
|
|
66
|
+
return parseJson<ResolvedAssetDefinition>(row.definition, { kind: row.kind, name: row.name } as ResolvedAssetDefinition);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function skip(kind: ProvisioningAssetKind, ref: string, provider: SpawnProvider, reason: ProvisioningSkip["reason"], detail: string): ProvisioningSkip {
|
|
70
|
+
return { kind, ref, provider, reason, detail };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function variantSkipReason(row: ProvisioningAssetResolutionRow): Pick<ProvisioningSkip, "reason" | "detail"> | null {
|
|
74
|
+
if (row.enabled !== 1) return { reason: "disabled", detail: "Registry variant is disabled." };
|
|
75
|
+
if (row.approval_status !== "approved") return { reason: "unapproved", detail: `Registry variant approval_status is ${row.approval_status ?? "unset"}.` };
|
|
76
|
+
if (row.validation_status !== "valid") {
|
|
77
|
+
const errors = parseJson<unknown[]>(row.validation_errors ?? "[]", []);
|
|
78
|
+
const detail = errors.length ? `Registry variant validation_status is ${row.validation_status}; ${JSON.stringify(errors)}` : `Registry variant validation_status is ${row.validation_status ?? "unset"}.`;
|
|
79
|
+
return { reason: "invalid", detail };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve a registry ref for a target provider: an approved/valid/enabled exact
|
|
85
|
+
// provider match wins, else provider-neutral "any". MCP is intentionally
|
|
86
|
+
// provider-neutral in v1, so only "any" MCP variants are candidates.
|
|
87
|
+
function resolveRegistryDefinition(kind: ProvisioningAssetKind, ref: string, provider: SpawnProvider): RegistryResolution {
|
|
88
|
+
const capability = getDb()
|
|
89
|
+
.query("SELECT id, slug, kind, status FROM provisioning_capabilities WHERE kind = ? AND slug = ?")
|
|
90
|
+
.get(kind, ref) as ProvisioningCapabilityResolutionRow | undefined;
|
|
91
|
+
if (!capability) {
|
|
92
|
+
return { definition: null, skip: skip(kind, ref, provider, "missing", "No provisioning capability exists for this ref.") };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rows = getDb()
|
|
96
|
+
.query("SELECT * FROM provisioning_assets WHERE capability_id = ? ORDER BY CASE provider WHEN ? THEN 0 WHEN 'any' THEN 1 ELSE 2 END, provider")
|
|
97
|
+
.all(capability.id, provider) as ProvisioningAssetResolutionRow[];
|
|
98
|
+
const providerCandidates = rows.filter((row) => kind === "mcp" ? row.provider === "any" : row.provider === provider || row.provider === "any");
|
|
99
|
+
if (!providerCandidates.length) {
|
|
100
|
+
const label = getManifest(provider)?.label ?? provider;
|
|
101
|
+
return { definition: null, skip: skip(kind, ref, provider, "provider_mismatch", `No ${label} variant exists for capability ${ref}.`) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const row of providerCandidates) {
|
|
105
|
+
if (!variantSkipReason(row)) return { definition: rowDefinition(row) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const blocked = variantSkipReason(providerCandidates[0]!);
|
|
109
|
+
return { definition: null, skip: skip(kind, ref, provider, blocked?.reason ?? "missing", blocked?.detail ?? "No resolvable registry variant exists.") };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A ref applies to a launch iff it's enabled and its provider scope includes the
|
|
113
|
+
// target ("any"/undefined → all providers).
|
|
114
|
+
function refAppliesToProvider(ref: AgentProfileAssetRef, provider: SpawnProvider): boolean {
|
|
115
|
+
return ref.enabled && (!ref.provider || ref.provider === "any" || ref.provider === provider);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hasAssetRef(refs: AgentProfileAssetRef[], source: AgentProfileAssetRef["source"], ref: string): boolean {
|
|
119
|
+
return refs.some((item) => item.source === source && item.ref === ref);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function relayBundledProvisioningRefs(profile: AgentProfile, provider: SpawnProvider): Pick<AgentProfile, "skills" | "plugins"> {
|
|
123
|
+
const skills = [...(profile.skills ?? [])];
|
|
124
|
+
const plugins = [...(profile.plugins ?? [])];
|
|
125
|
+
const provisioning = getManifest(provider)?.provisioning;
|
|
126
|
+
if (profile.relay.skills !== false && provisioning?.bundledSkills) {
|
|
127
|
+
for (const ref of bundledSkillRefs(provisioning.bundledSkills)) {
|
|
128
|
+
if (!hasAssetRef(skills, "relay", ref)) skills.push({ source: "relay", ref, enabled: true, provider });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (profile.relay.plugins !== false && provisioning?.bundledPlugin && !hasAssetRef(plugins, "relay", provisioning.bundledPlugin.ref)) {
|
|
132
|
+
plugins.push({ source: "relay", ref: provisioning.bundledPlugin.ref, enabled: true, provider });
|
|
133
|
+
}
|
|
134
|
+
return { skills, plugins };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function withRelayBundledProvisioningRefs(profile: AgentProfile, provider: SpawnProvider): AgentProfile {
|
|
138
|
+
const { skills, plugins } = relayBundledProvisioningRefs(profile, provider);
|
|
139
|
+
return { ...profile, skills, plugins };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function inlineFilesFromMeta(meta: Record<string, unknown> | undefined): { path: string; content: string }[] | undefined {
|
|
143
|
+
const raw = meta?.files;
|
|
144
|
+
if (!Array.isArray(raw)) return undefined;
|
|
145
|
+
const files = raw
|
|
146
|
+
.filter(isRecord)
|
|
147
|
+
.filter((f) => typeof f.path === "string" && typeof f.content === "string")
|
|
148
|
+
.map((f) => ({ path: f.path as string, content: f.content as string }));
|
|
149
|
+
return files.length ? files : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Resolve a skill/plugin asset ref into a concrete definition. relay → registry;
|
|
153
|
+
// inline → built from ref.meta (files or dir); repo → a cwd-relative dir token the
|
|
154
|
+
// runner resolves against the launch cwd; provider → skipped (provider built-in).
|
|
155
|
+
function resolveDirAssetRef(
|
|
156
|
+
kind: "skill" | "plugin",
|
|
157
|
+
ref: AgentProfileAssetRef,
|
|
158
|
+
provider: SpawnProvider,
|
|
159
|
+
): { definition: SkillAssetDefinition | PluginAssetDefinition | null; skip?: ProvisioningSkip } {
|
|
160
|
+
if (ref.source === "relay") {
|
|
161
|
+
const resolved = resolveRegistryDefinition(kind, ref.ref, provider);
|
|
162
|
+
const def = resolved.definition;
|
|
163
|
+
return { definition: def && def.kind === kind ? def : null, skip: resolved.skip };
|
|
164
|
+
}
|
|
165
|
+
if (ref.source === "inline") {
|
|
166
|
+
const files = inlineFilesFromMeta(ref.meta);
|
|
167
|
+
const dir = typeof ref.meta?.dir === "string" ? ref.meta.dir : undefined;
|
|
168
|
+
if (!files && !dir) return { definition: null, skip: skip(kind, ref.ref, provider, "invalid_inline", `Inline ${kind} ref requires files or dir metadata.`) };
|
|
169
|
+
return { definition: { kind, name: ref.ref, ...(files ? { files } : {}), ...(dir ? { dir } : {}) } };
|
|
170
|
+
}
|
|
171
|
+
if (ref.source === "repo") {
|
|
172
|
+
return { definition: { kind, name: ref.ref, dir: ref.ref } };
|
|
173
|
+
}
|
|
174
|
+
return { definition: null };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A profile.mcp.servers entry is either an inline server spec or a registry
|
|
178
|
+
// reference (`{ source: "relay", ref }`). Returns the concrete server spec.
|
|
179
|
+
function resolveMcpEntry(name: string, value: unknown, provider: SpawnProvider): { server: ProvisioningMcpServer | null; skip?: ProvisioningSkip } {
|
|
180
|
+
if (!isRecord(value)) return { server: null };
|
|
181
|
+
if (value.source === "relay" && typeof value.ref === "string") {
|
|
182
|
+
const resolved = resolveRegistryDefinition("mcp", value.ref, provider);
|
|
183
|
+
const def = resolved.definition;
|
|
184
|
+
return { server: def && def.kind === "mcp" ? def.server : null, skip: resolved.skip };
|
|
185
|
+
}
|
|
186
|
+
if (value.enabled === false) return { server: null };
|
|
187
|
+
// Inline spec: keep the recognized server fields only.
|
|
188
|
+
const server: ProvisioningMcpServer = {};
|
|
189
|
+
if (typeof value.type === "string") server.type = value.type as ProvisioningMcpServer["type"];
|
|
190
|
+
if (typeof value.command === "string") server.command = value.command;
|
|
191
|
+
if (Array.isArray(value.args)) server.args = value.args.filter((a): a is string => typeof a === "string");
|
|
192
|
+
if (isRecord(value.env)) server.env = stringRecord(value.env);
|
|
193
|
+
if (isRecord(value.configEnv)) server.configEnv = stringRecord(value.configEnv);
|
|
194
|
+
if (typeof value.url === "string") server.url = value.url;
|
|
195
|
+
if (isRecord(value.headers)) server.headers = stringRecord(value.headers);
|
|
196
|
+
return { server: server.command || server.url ? server : null, skip: server.command || server.url ? undefined : skip("mcp", name, provider, "invalid_inline", "Inline MCP server requires command or url.") };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Resolve a profile's declared skill/plugin/MCP refs against the registry into the
|
|
200
|
+
// concrete bundle the runner materializes. Relay-side (needs the DB); the result is
|
|
201
|
+
// attached to the in-flight profile (profile.provisioning) before it's serialized
|
|
202
|
+
// into the spawn env. Pure read — never mutates the registry.
|
|
203
|
+
export function resolveProfileProvisioning(profile: AgentProfile, provider: SpawnProvider): ResolvedProvisioning {
|
|
204
|
+
const skills: SkillAssetDefinition[] = [];
|
|
205
|
+
const skipped: ProvisioningSkip[] = [];
|
|
206
|
+
for (const ref of profile.skills ?? []) {
|
|
207
|
+
if (!refAppliesToProvider(ref, provider)) {
|
|
208
|
+
if (ref.enabled && ref.source === "relay") skipped.push(skip("skill", ref.ref, provider, "provider_mismatch", `Profile ref is scoped to provider ${ref.provider}.`));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const resolved = resolveDirAssetRef("skill", ref, provider);
|
|
212
|
+
if (resolved.definition) skills.push(resolved.definition as SkillAssetDefinition);
|
|
213
|
+
else if (resolved.skip) skipped.push(resolved.skip);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const plugins: PluginAssetDefinition[] = [];
|
|
217
|
+
for (const ref of profile.plugins ?? []) {
|
|
218
|
+
if (!refAppliesToProvider(ref, provider)) {
|
|
219
|
+
if (ref.enabled && ref.source === "relay") skipped.push(skip("plugin", ref.ref, provider, "provider_mismatch", `Profile ref is scoped to provider ${ref.provider}.`));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const resolved = resolveDirAssetRef("plugin", ref, provider);
|
|
223
|
+
if (resolved.definition) plugins.push(resolved.definition as PluginAssetDefinition);
|
|
224
|
+
else if (resolved.skip) skipped.push(resolved.skip);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const mcpServers: Record<string, ProvisioningMcpServer> = {};
|
|
228
|
+
for (const [name, value] of Object.entries(profile.mcp?.servers ?? {})) {
|
|
229
|
+
const resolved = resolveMcpEntry(name, value, provider);
|
|
230
|
+
if (resolved.server) mcpServers[name] = resolved.server;
|
|
231
|
+
else if (resolved.skip) skipped.push(resolved.skip);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { skills, plugins, mcpServers, ...(skipped.length ? { skipped } : {}) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Attach the resolved bundle to a spawn-bound profile. Returns a shallow copy so the
|
|
238
|
+
// cached config-store value is never mutated. undefined passes through (an unresolved
|
|
239
|
+
// profile name stays undefined so callers' not-found guards still fire); the runner
|
|
240
|
+
// treats an absent and an empty bundle identically.
|
|
241
|
+
export function withResolvedProvisioning(profile: AgentProfile | undefined, provider: SpawnProvider): AgentProfile | undefined {
|
|
242
|
+
if (!profile) return undefined;
|
|
243
|
+
const composed = withRelayBundledProvisioningRefs(profile, provider);
|
|
244
|
+
return { ...composed, provisioning: resolveProfileProvisioning(composed, provider) };
|
|
245
|
+
}
|
package/src/db/stats.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
|
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { listAgents } from "./agents.ts";
|
|
18
18
|
import { listChannels } from "./channels.ts";
|
|
19
|
-
import {
|
|
19
|
+
import { summarizeExpiredUndeliveredMessages } from "./delivery.ts";
|
|
20
20
|
import { messageTaskClosed } from "./claim-sql.ts";
|
|
21
21
|
import { claimTaskWhere } from "./task-core.ts";
|
|
22
22
|
import { getDb } from "./connection.ts";
|
|
@@ -489,14 +489,17 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
489
489
|
: undefined,
|
|
490
490
|
});
|
|
491
491
|
|
|
492
|
-
const expiredUndelivered =
|
|
492
|
+
const expiredUndelivered = summarizeExpiredUndeliveredMessages(now);
|
|
493
493
|
checks.push({
|
|
494
494
|
name: "expired-undelivered-messages",
|
|
495
|
-
status: expiredUndelivered > 0 ? "warn" : "ok",
|
|
496
|
-
count: expiredUndelivered,
|
|
497
|
-
detail: expiredUndelivered > 0
|
|
498
|
-
? `${expiredUndelivered} queued message(s) expired undelivered at max_age in the last 24h (silent data loss)`
|
|
499
|
-
:
|
|
495
|
+
status: expiredUndelivered.unexplainedCount > 0 ? "warn" : "ok",
|
|
496
|
+
count: expiredUndelivered.unexplainedCount,
|
|
497
|
+
detail: expiredUndelivered.unexplainedCount > 0
|
|
498
|
+
? `${expiredUndelivered.unexplainedCount} queued message(s) expired undelivered at max_age in the last 24h (silent data loss)${expiredUndelivered.explainedManagedPolicyCount > 0 ? `; ${expiredUndelivered.explainedManagedPolicyCount} managed-policy expiry(s) explained by policy health` : ""}`
|
|
499
|
+
: expiredUndelivered.explainedManagedPolicyCount > 0
|
|
500
|
+
? `${expiredUndelivered.explainedManagedPolicyCount} queued managed-policy message(s) expired while the policy target was unavailable; tracked by spawn-policy health, not silent data loss`
|
|
501
|
+
: undefined,
|
|
502
|
+
subjects: expiredUndelivered.subjects.length > 0 ? expiredUndelivered.subjects : undefined,
|
|
500
503
|
});
|
|
501
504
|
|
|
502
505
|
const rootTokenRuntimeAgents = listAgents().filter((agent) => {
|
|
@@ -539,4 +542,3 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
539
542
|
: "ok";
|
|
540
543
|
return { status, version: VERSION, generatedAt: now, checks };
|
|
541
544
|
}
|
|
542
|
-
|
package/src/db/task-entity.ts
CHANGED
|
@@ -152,7 +152,7 @@ export interface CreateDeliverableTaskInput {
|
|
|
152
152
|
intent: string;
|
|
153
153
|
complexity?: TaskComplexity;
|
|
154
154
|
context?: string;
|
|
155
|
-
doneWhen?: string;
|
|
155
|
+
doneWhen?: string; branch?: string;
|
|
156
156
|
teamId?: string;
|
|
157
157
|
projectId?: string;
|
|
158
158
|
repo?: string;
|
|
@@ -171,25 +171,24 @@ export function createDeliverableTask(input: CreateDeliverableTaskInput): TaskDe
|
|
|
171
171
|
const dispatch = cleanDispatch(input.dispatch);
|
|
172
172
|
const complexity = cleanTaskComplexity(input.complexity ?? dispatch?.complexity, "complexity") ?? DEFAULT_TASK_COMPLEXITY;
|
|
173
173
|
const context = optionalText(input.context, "context", 1_000_000);
|
|
174
|
+
const branch = optionalText(input.branch, "branch", 500);
|
|
174
175
|
const taskProject = cleanTaskProject(input);
|
|
175
176
|
// Legacy NOT-NULL columns have no DB defaults (#834 finding 6) — set them explicitly.
|
|
176
177
|
const result = getDb().query(`
|
|
177
178
|
INSERT INTO tasks (
|
|
178
179
|
source, title, body, severity, status, target, occurrence_count, metadata,
|
|
179
180
|
created_at, updated_at, last_seen_at,
|
|
180
|
-
kind, intent, complexity, context, latest_steer, deliverable_stage, done_when, team_id, project_id, repo, created_by, dispatch
|
|
181
|
-
) VALUES (?, ?, ?, 'info', 'open', ?, 1, '{}', ?, ?, ?, '${DELIVERABLE_TASK_KIND}', ?, ?, ?, ?, 'none', ?, ?, ?, ?, ?, ?)
|
|
181
|
+
kind, intent, complexity, context, latest_steer, deliverable_stage, done_when, branch, team_id, project_id, repo, created_by, dispatch
|
|
182
|
+
) VALUES (?, ?, ?, 'info', 'open', ?, 1, '{}', ?, ?, ?, '${DELIVERABLE_TASK_KIND}', ?, ?, ?, ?, 'none', ?, ?, ?, ?, ?, ?, ?)
|
|
182
183
|
`).run(
|
|
183
184
|
optionalText(input.source, "source", 200) ?? "coordinator",
|
|
184
185
|
title,
|
|
185
186
|
intent,
|
|
186
187
|
optionalText(input.target, "target", 200) ?? createdBy,
|
|
187
|
-
now, now, now,
|
|
188
|
-
intent,
|
|
189
|
-
complexity,
|
|
190
|
-
context ?? null,
|
|
188
|
+
now, now, now, intent, complexity, context ?? null,
|
|
191
189
|
optionalText(input.latestSteer, "latestSteer", 1_000_000) ?? null,
|
|
192
190
|
optionalText(input.doneWhen, "doneWhen", 1_000_000) ?? null,
|
|
191
|
+
branch ?? null,
|
|
193
192
|
optionalText(input.teamId, "teamId", 200) ?? null,
|
|
194
193
|
taskProject.projectId ?? null,
|
|
195
194
|
taskProject.repo ?? null,
|
|
@@ -203,7 +202,7 @@ export function createDeliverableTask(input: CreateDeliverableTaskInput): TaskDe
|
|
|
203
202
|
severity: "info",
|
|
204
203
|
title: `Task created: ${title}`,
|
|
205
204
|
body: intent,
|
|
206
|
-
metadata: { createdBy, complexity, ...(context ? { context } : {}), ...(taskProject.projectId ? { projectId: taskProject.projectId } : {}), ...(taskProject.repo ? { repo: taskProject.repo } : {}), ...(dispatch ? { dispatch } : {}) },
|
|
205
|
+
metadata: { createdBy, complexity, ...(context ? { context } : {}), ...(branch ? { branch } : {}), ...(taskProject.projectId ? { projectId: taskProject.projectId } : {}), ...(taskProject.repo ? { repo: taskProject.repo } : {}), ...(dispatch ? { dispatch } : {}) },
|
|
207
206
|
}, now);
|
|
208
207
|
return getTaskDetail(id)!;
|
|
209
208
|
}
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -2,14 +2,10 @@ import { createCommand } from "./commands-db";
|
|
|
2
2
|
import { workspaceOwnerOrchestrator } from "./workspace-host";
|
|
3
3
|
import { agentProviderState } from "./provider-state";
|
|
4
4
|
import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces } from "./db";
|
|
5
|
-
import {
|
|
6
|
-
getManagedAgentState,
|
|
7
|
-
listSpawnPolicies,
|
|
8
|
-
updateManagedAgentState,
|
|
9
|
-
upsertManagedAgentState,
|
|
10
|
-
} from "./config-store";
|
|
5
|
+
import { getManagedAgentState, listSpawnPolicies, updateManagedAgentState, upsertManagedAgentState } from "./config-store";
|
|
11
6
|
import { emitRelayEvent } from "./events";
|
|
12
7
|
import { alwaysReloadTags, reconcileLifecycleContextPressure, requestAgentCompact } from "./lifecycle-context-pressure";
|
|
8
|
+
import { escalateOfflineOrchestrator } from "./managed-policy-escalation";
|
|
13
9
|
import { markManagedAgentRunning, emitManagedState } from "./services/managed-running";
|
|
14
10
|
import { authContextFromSystem } from "./services/auth-context";
|
|
15
11
|
import { emitCommandEvent } from "./command-events";
|
|
@@ -189,15 +185,18 @@ export class LifecycleManager {
|
|
|
189
185
|
spawnAgent(policy: SpawnPolicy, reason = "reconcile"): Command | null {
|
|
190
186
|
const orch = getOrchestrator(policy.orchestratorId);
|
|
191
187
|
if (!orch || orch.status !== "online") {
|
|
192
|
-
const state =
|
|
188
|
+
const state = getManagedAgentState(policy.name) ?? {
|
|
193
189
|
policyName: policy.name,
|
|
194
|
-
status: "
|
|
190
|
+
status: "stopped" as const,
|
|
195
191
|
orchestratorId: policy.orchestratorId,
|
|
196
192
|
provider: policy.provider,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
193
|
+
restartCount: 0,
|
|
194
|
+
consecutiveFailures: 0,
|
|
195
|
+
updatedAt: this.now(),
|
|
196
|
+
};
|
|
197
|
+
this.markBackoff(policy, state, "orchestrator offline");
|
|
198
|
+
const next = getManagedAgentState(policy.name);
|
|
199
|
+
if (next) escalateOfflineOrchestrator(policy, next, orch);
|
|
201
200
|
return null;
|
|
202
201
|
}
|
|
203
202
|
const spawnRequestId = generateSpawnRequestId();
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -27,10 +27,11 @@ import { isAgentUnavailable } from "../db/agent-predicates";
|
|
|
27
27
|
import { flushQueuedDirectMessages } from "../services/managed-running";
|
|
28
28
|
import { notifyAgentOffline } from "../agent-lifecycle-events";
|
|
29
29
|
import { diagnoseAgentDeath } from "../agent-death-diagnosis";
|
|
30
|
-
import { autoMergeCleanFastForwards, runAutoMergeWatchdog } from "../workspace-auto-merge";
|
|
30
|
+
import { autoMergeCleanFastForwards, runAutoMergeWatchdog, scheduleRepoMerge } from "../workspace-auto-merge";
|
|
31
31
|
import { reapOrphanedWorktrees } from "../workspace-orphans";
|
|
32
32
|
import { getLifecycleManager } from "../lifecycle-manager";
|
|
33
33
|
import { applyCommandToRecipe } from "../recipe-runner";
|
|
34
|
+
import { settleWorkspaceCommand } from "../workspace-command-settle";
|
|
34
35
|
import {
|
|
35
36
|
emitAgentRemoved,
|
|
36
37
|
emitAgentStatus,
|
|
@@ -237,6 +238,7 @@ export const definitions: MaintenanceJobDefinition[] = [
|
|
|
237
238
|
handler() {
|
|
238
239
|
const commands = expireCommands();
|
|
239
240
|
for (const command of commands) {
|
|
241
|
+
settleWorkspaceCommand(command, { onMergeSettled: scheduleRepoMerge });
|
|
240
242
|
applyCommandToRecipe(command);
|
|
241
243
|
emitCommand(command);
|
|
242
244
|
}
|
|
@@ -21,6 +21,7 @@ import { preparePrCompletionScan, reconcileLandedPr } from "../workspace-pr-comp
|
|
|
21
21
|
import { applyBranchFreshness, fetchHostMergePreview, workspacePathWithinBase } from "../workspace-probe";
|
|
22
22
|
import { wakeRepoSteward } from "../workspace-auto-merge";
|
|
23
23
|
import { workspaceOwnerOrchestrator } from "../workspace-host";
|
|
24
|
+
import { routeWorkspaceCustodyEscalation } from "../workspace-escalation";
|
|
24
25
|
import {
|
|
25
26
|
CONFLICT_SCAN_STATUSES,
|
|
26
27
|
DAY_MS,
|
|
@@ -175,74 +176,53 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
|
175
176
|
const escalationTargets: string[] = [];
|
|
176
177
|
const reconciledLandedIds: string[] = [];
|
|
177
178
|
const escalationMs = stewardEscalationMs();
|
|
178
|
-
const fallbackTarget = stewardFallbackTarget();
|
|
179
179
|
for (const ws of all) {
|
|
180
180
|
if (!STRANDABLE_STATUSES.has(ws.status) || ws.mode !== "isolated" || !ws.worktreePath) continue;
|
|
181
181
|
reelectRepoSteward(ws.repoRoot);
|
|
182
182
|
const fresh = getWorkspace(ws.id);
|
|
183
183
|
if (!fresh || !STRANDABLE_STATUSES.has(fresh.status)) continue;
|
|
184
184
|
const meta = fresh.metadata as Record<string, unknown>;
|
|
185
|
-
const steward = fresh.stewardAgentId;
|
|
186
|
-
const stewardOnline = Boolean(steward && getAgent(steward) && getAgent(steward)!.status !== "offline");
|
|
187
185
|
const strandedAt = typeof meta.strandedAt === "number" ? meta.strandedAt : undefined;
|
|
188
186
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
187
|
+
const orch = workspaceOwnerOrchestrator(fresh, listOrchestrators().filter((o) => o.status === "online" && o.apiUrl), { requireApiUrl: true });
|
|
188
|
+
const preview = orch?.apiUrl ? await fetchHostMergePreview(orch.apiUrl, fresh, { checkPr: true }) : null;
|
|
189
|
+
if (preview && !("available" in preview) && !preview.error && !preview.missing) {
|
|
190
|
+
const landed = previewProvesLanded(preview);
|
|
191
|
+
if (landed) {
|
|
192
|
+
reconcileLandedWorkspace(fresh, preview);
|
|
193
|
+
patchWorkspaceMetadata(fresh.id, { strandedAt: undefined, escalatedAt: undefined, strandedNotifiedSteward: undefined });
|
|
194
|
+
reconciledLandedIds.push(fresh.id);
|
|
195
|
+
continue;
|
|
200
196
|
}
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
if (stewardOnline) {
|
|
204
|
-
if (strandedAt !== undefined && meta.strandedNotifiedSteward !== steward) {
|
|
205
|
-
const sent = notifyTarget(
|
|
206
|
-
"workspace.steward_reassigned",
|
|
207
|
-
{ workspaceId: fresh.id, repoRoot: fresh.repoRoot },
|
|
208
|
-
steward!,
|
|
209
|
-
"Workspace stewardship reassigned",
|
|
210
|
-
`You are now steward for ${fresh.repoRoot}. Workspace \`${fresh.branch ?? fresh.id}\` is ${fresh.status} and was stranded without an online steward - please coordinate ${fresh.status === "conflict" ? "conflict resolution" : "review/merge"}.`,
|
|
211
|
-
{ kind: "workspace.steward-reassigned", workspaceId: fresh.id, repoRoot: fresh.repoRoot, branch: fresh.branch, status: fresh.status },
|
|
212
|
-
);
|
|
213
|
-
if (sent) reassignedIds.push(fresh.id);
|
|
214
|
-
}
|
|
215
|
-
patchWorkspaceMetadata(fresh.id, { strandedAt: undefined, escalatedAt: undefined, strandedNotifiedSteward: steward });
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
199
|
if (strandedAt === undefined) { patchWorkspaceMetadata(fresh.id, { strandedAt: now }); continue; }
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
|
|
200
|
+
const escalatedAt = typeof meta.escalatedAt === "number" ? meta.escalatedAt : 0;
|
|
201
|
+
if (now - strandedAt < escalationMs) continue;
|
|
202
|
+
if (escalatedAt && now - escalatedAt < escalationMs) continue;
|
|
203
|
+
|
|
204
|
+
if (getStewardConfig().enabled) {
|
|
205
|
+
wakeRepoSteward(fresh, `workspace ${fresh.status} stranded for ${Math.round((now - strandedAt) / (60 * 60 * 1000))}h`);
|
|
206
|
+
}
|
|
207
|
+
const targets = routeWorkspaceCustodyEscalation({
|
|
208
|
+
workspace: fresh,
|
|
209
|
+
subject: "Stranded workspace needs an owner",
|
|
210
|
+
body: `Workspace \`${fresh.branch ?? fresh.id}\` in ${fresh.repoRoot} is ${fresh.status} and has been stranded for ${Math.round((now - strandedAt) / (60 * 60 * 1000))}h. Please coordinate ${fresh.status === "conflict" ? "conflict resolution" : "review/merge"} or clean up the worktree.`,
|
|
211
|
+
payload: { kind: "workspace.stranded-escalation", workspaceId: fresh.id, repoRoot: fresh.repoRoot, branch: fresh.branch, status: fresh.status, strandedAt },
|
|
212
|
+
idempotencyKey: `workspace-stranded:${fresh.id}:${strandedAt}:${Math.floor(now / escalationMs)}`,
|
|
213
|
+
});
|
|
214
|
+
escalationTargets.push(...targets);
|
|
235
215
|
patchWorkspaceMetadata(fresh.id, { escalatedAt: now });
|
|
236
216
|
escalatedIds.push(fresh.id);
|
|
237
217
|
createActivityEvent({
|
|
238
218
|
clientId: `workspace-gc-escalate-${fresh.id}-${now}`,
|
|
239
219
|
kind: "state",
|
|
240
220
|
title: "Workspace escalated",
|
|
241
|
-
body: `${fresh.branch ?? fresh.id} in ${fresh.repoRoot} - stranded ${fresh.status} escalated${
|
|
221
|
+
body: `${fresh.branch ?? fresh.id} in ${fresh.repoRoot} - stranded ${fresh.status} escalated${targets.length ? ` to ${targets.join(", ")}` : " (no owner, coordinator, or fallback configured)"}`,
|
|
242
222
|
meta: fresh.branch ?? fresh.id,
|
|
243
223
|
icon: "ti-alert-octagon",
|
|
244
224
|
view: "orchestrators",
|
|
245
|
-
metadata: { source: "server", maintenanceJobId: "workspace-gc", workspaceId: fresh.id,
|
|
225
|
+
metadata: { source: "server", maintenanceJobId: "workspace-gc", workspaceId: fresh.id, escalationTargets: targets },
|
|
246
226
|
});
|
|
247
227
|
}
|
|
248
228
|
|
|
@@ -250,6 +230,7 @@ export async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
|
250
230
|
const autoAbandonDeferred: string[] = [];
|
|
251
231
|
const notifiedStewards: string[] = [];
|
|
252
232
|
const reviewOrchestrators = listOrchestrators();
|
|
233
|
+
const fallbackTarget = stewardFallbackTarget();
|
|
253
234
|
for (const ws of all) {
|
|
254
235
|
if (ws.status === "review_requested" && ws.updatedAt < reviewCutoff) {
|
|
255
236
|
const fresh = getWorkspace(ws.id);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getAgent, getDb } from "./db";
|
|
2
|
+
import { getSpawnPolicy } from "./config-store";
|
|
3
|
+
import { stewardFallbackTarget } from "./config";
|
|
4
|
+
import { routeNotification } from "./notification-router";
|
|
5
|
+
import type { ManagedAgentState, Orchestrator, SpawnPolicy } from "./types";
|
|
6
|
+
|
|
7
|
+
const ORCHESTRATOR_OFFLINE_ESCALATION_FAILURES = 3;
|
|
8
|
+
|
|
9
|
+
export function escalateOfflineOrchestrator(policy: SpawnPolicy, state: ManagedAgentState, orch: Orchestrator | null | undefined): void {
|
|
10
|
+
if (state.consecutiveFailures < ORCHESTRATOR_OFFLINE_ESCALATION_FAILURES) return;
|
|
11
|
+
const updatedBy = getSpawnPolicy(policy.name)?.updatedBy?.trim();
|
|
12
|
+
const previousParent = state.agentId ? getAgent(state.agentId)?.spawnedBy?.trim() : undefined;
|
|
13
|
+
const fallback = stewardFallbackTarget();
|
|
14
|
+
const recipients = [...new Set([updatedBy, previousParent, fallback].filter((item): item is string => Boolean(item && item !== "system")))];
|
|
15
|
+
if (!recipients.length) return;
|
|
16
|
+
const host = orch?.hostname ? `${orch.hostname} (${policy.orchestratorId})` : policy.orchestratorId;
|
|
17
|
+
const queuedMessages = queuedPolicyMessageCount(policy.name);
|
|
18
|
+
const nextRetry = state.backoffUntil ? new Date(state.backoffUntil).toISOString() : "not scheduled";
|
|
19
|
+
routeNotification({
|
|
20
|
+
type: "agent.spawn_failed",
|
|
21
|
+
scope: { target: `policy:${policy.name}` },
|
|
22
|
+
recipients,
|
|
23
|
+
message: {
|
|
24
|
+
subject: "Managed policy pinned host offline",
|
|
25
|
+
body: [
|
|
26
|
+
`Managed policy \`${policy.name}\` cannot spawn because pinned orchestrator \`${host}\` is offline.`,
|
|
27
|
+
`Queued messages: ${queuedMessages}. Consecutive failures: ${state.consecutiveFailures}. Next retry: ${nextRetry}.`,
|
|
28
|
+
].join("\n"),
|
|
29
|
+
payload: {
|
|
30
|
+
kind: "managed-policy.orchestrator-offline",
|
|
31
|
+
policyName: policy.name,
|
|
32
|
+
orchestratorId: policy.orchestratorId,
|
|
33
|
+
orchestratorHostname: orch?.hostname,
|
|
34
|
+
queuedMessages,
|
|
35
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
36
|
+
backoffUntil: state.backoffUntil,
|
|
37
|
+
},
|
|
38
|
+
idempotencyKey: `managed-policy-orchestrator-offline:${policy.name}:${policy.orchestratorId}`,
|
|
39
|
+
replyExpected: false,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function queuedPolicyMessageCount(policyName: string): number {
|
|
45
|
+
const row = getDb().query("SELECT COUNT(*) AS count FROM messages WHERE to_target = ? AND delivery_status = 'queued'").get(`policy:${policyName}`) as { count?: number } | undefined;
|
|
46
|
+
return row?.count ?? 0;
|
|
47
|
+
}
|
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
|
}
|