agent-relay-server 0.107.0 → 0.108.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 +475 -1
- package/package.json +4 -4
- package/public/assets/{MessageBubble-PEdvVh_m.js → MessageBubble-DYJezSjU.js} +2 -2
- package/public/assets/{MessageBubble-PEdvVh_m.js.map → MessageBubble-DYJezSjU.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DSts7Rtq.js → PlanGraphPanel-MWcVw38W.js} +2 -2
- package/public/assets/{PlanGraphPanel-DSts7Rtq.js.map → PlanGraphPanel-MWcVw38W.js.map} +1 -1
- package/public/assets/{activity-BcYf8YNO.js → activity-Db3X3dg8.js} +2 -2
- package/public/assets/{activity-BcYf8YNO.js.map → activity-Db3X3dg8.js.map} +1 -1
- package/public/assets/{agent-profiles-CuTQtmV6.js → agent-profiles-Di2w5U1o.js} +2 -2
- package/public/assets/{agent-profiles-CuTQtmV6.js.map → agent-profiles-Di2w5U1o.js.map} +1 -1
- package/public/assets/{agents-CgRFJyks.js → agents-GSJXvmU1.js} +2 -2
- package/public/assets/{agents-CgRFJyks.js.map → agents-GSJXvmU1.js.map} +1 -1
- package/public/assets/{analytics-C3OTFIXo.js → analytics-B9USQLl5.js} +2 -2
- package/public/assets/{analytics-C3OTFIXo.js.map → analytics-B9USQLl5.js.map} +1 -1
- package/public/assets/{automation-CH9YGYoE.js → automation-XRePs1dT.js} +2 -2
- package/public/assets/{automation-CH9YGYoE.js.map → automation-XRePs1dT.js.map} +1 -1
- package/public/assets/{branch-state-badge-CcLZ_u86.js → branch-state-badge-C_VNyER3.js} +2 -2
- package/public/assets/{branch-state-badge-CcLZ_u86.js.map → branch-state-badge-C_VNyER3.js.map} +1 -1
- package/public/assets/{channels-CcSnLzCG.js → channels-DACvNoC2.js} +2 -2
- package/public/assets/{channels-CcSnLzCG.js.map → channels-DACvNoC2.js.map} +1 -1
- package/public/assets/{chat-BLcm2Aht.js → chat-D_0uHgB6.js} +2 -2
- package/public/assets/{chat-BLcm2Aht.js.map → chat-D_0uHgB6.js.map} +1 -1
- package/public/assets/{connectors-DbPOlQhL.js → connectors-C0B38LUK.js} +2 -2
- package/public/assets/{connectors-DbPOlQhL.js.map → connectors-C0B38LUK.js.map} +1 -1
- package/public/assets/display-1J__Ti1K.js +3 -0
- package/public/assets/display-1J__Ti1K.js.map +1 -0
- package/public/assets/{formatted-body-impl-BJTthVHg.js → formatted-body-impl-BKWCAjBd.js} +2 -2
- package/public/assets/{formatted-body-impl-BJTthVHg.js.map → formatted-body-impl-BKWCAjBd.js.map} +1 -1
- package/public/assets/index-DlBSuzd2.js +23 -0
- package/public/assets/index-DlBSuzd2.js.map +1 -0
- package/public/assets/{insights-DklrIhb6.js → insights-BzNoYvDe.js} +2 -2
- package/public/assets/{insights-DklrIhb6.js.map → insights-BzNoYvDe.js.map} +1 -1
- package/public/assets/{integrations-hkSW8CzM.js → integrations-DLzJpt9G.js} +2 -2
- package/public/assets/{integrations-hkSW8CzM.js.map → integrations-DLzJpt9G.js.map} +1 -1
- package/public/assets/{maintenance-FAAMGpuZ.js → maintenance-B6_0-ECm.js} +2 -2
- package/public/assets/{maintenance-FAAMGpuZ.js.map → maintenance-B6_0-ECm.js.map} +1 -1
- package/public/assets/{managed-agents-CVca-s3t.js → managed-agents-CpzKXDtV.js} +2 -2
- package/public/assets/{managed-agents-CVca-s3t.js.map → managed-agents-CpzKXDtV.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BPW4OQo9.js → markdown-preview-impl-DxBnlJ2k.js} +2 -2
- package/public/assets/{markdown-preview-impl-BPW4OQo9.js.map → markdown-preview-impl-DxBnlJ2k.js.map} +1 -1
- package/public/assets/{memory-BzuDTHC5.js → memory-Bnm7ILWL.js} +2 -2
- package/public/assets/{memory-BzuDTHC5.js.map → memory-Bnm7ILWL.js.map} +1 -1
- package/public/assets/{messages-_W24PNvT.js → messages-D9LkocTc.js} +2 -2
- package/public/assets/{messages-_W24PNvT.js.map → messages-D9LkocTc.js.map} +1 -1
- package/public/assets/{orchestrators-Du6I_n8i.js → orchestrators-C__JfrNr.js} +2 -2
- package/public/assets/{orchestrators-Du6I_n8i.js.map → orchestrators-C__JfrNr.js.map} +1 -1
- package/public/assets/{overview-Dyvm1qpT.js → overview-BQ0b0V7Q.js} +2 -2
- package/public/assets/{overview-Dyvm1qpT.js.map → overview-BQ0b0V7Q.js.map} +1 -1
- package/public/assets/{pairs-B8x1RCNd.js → pairs-ChyeBs0Z.js} +2 -2
- package/public/assets/{pairs-B8x1RCNd.js.map → pairs-ChyeBs0Z.js.map} +1 -1
- package/public/assets/{projects-vt63bwHN.js → projects-v5Kp1Oaq.js} +2 -2
- package/public/assets/{projects-vt63bwHN.js.map → projects-v5Kp1Oaq.js.map} +1 -1
- package/public/assets/{prompt-settings-xVXmvxB5.js → prompt-settings-YhAOlYc6.js} +2 -2
- package/public/assets/{prompt-settings-xVXmvxB5.js.map → prompt-settings-YhAOlYc6.js.map} +1 -1
- package/public/assets/{registry-ypiICauZ.js → registry-DZoAfrzx.js} +2 -2
- package/public/assets/{registry-ypiICauZ.js.map → registry-DZoAfrzx.js.map} +1 -1
- package/public/assets/{security-BfOQ6T5r.js → security-DRCj00eA.js} +2 -2
- package/public/assets/{security-BfOQ6T5r.js.map → security-DRCj00eA.js.map} +1 -1
- package/public/assets/{select-Dx8Ro-A3.js → select-ZOpLw2Fd.js} +2 -2
- package/public/assets/{select-Dx8Ro-A3.js.map → select-ZOpLw2Fd.js.map} +1 -1
- package/public/assets/{settings-87clTx4N.js → settings-DGgwr9WH.js} +2 -2
- package/public/assets/{settings-87clTx4N.js.map → settings-DGgwr9WH.js.map} +1 -1
- package/public/assets/{store-DUfrGvOE.js → store-Bnag1kY-.js} +2 -2
- package/public/assets/{store-DUfrGvOE.js.map → store-Bnag1kY-.js.map} +1 -1
- package/public/assets/{tasks-1Kvp53wh.js → tasks-DUTu8ZhA.js} +2 -2
- package/public/assets/{tasks-1Kvp53wh.js.map → tasks-DUTu8ZhA.js.map} +1 -1
- package/public/assets/{teams-DJWp4AKi.js → teams-BJpaoG2m.js} +2 -2
- package/public/assets/{teams-DJWp4AKi.js.map → teams-BJpaoG2m.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DgMuRAe_.js → terminal-viewer-impl-CQhh6oXa.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DgMuRAe_.js.map → terminal-viewer-impl-CQhh6oXa.js.map} +1 -1
- package/public/assets/types-BiJFuTiL.js +2 -0
- package/public/assets/types-BiJFuTiL.js.map +1 -0
- package/public/assets/{user-avatar-CfAUS3PZ.js → user-avatar-BYGrUx1U.js} +2 -2
- package/public/assets/{user-avatar-CfAUS3PZ.js.map → user-avatar-BYGrUx1U.js.map} +1 -1
- package/public/assets/{work-queue-D-Otxd9u.js → work-queue-BH2-mARc.js} +2 -2
- package/public/assets/{work-queue-D-Otxd9u.js.map → work-queue-BH2-mARc.js.map} +1 -1
- package/public/assets/{workspaces-CIsQqQby.js → workspaces-DldYyXZR.js} +2 -2
- package/public/assets/{workspaces-CIsQqQby.js.map → workspaces-DldYyXZR.js.map} +1 -1
- package/public/index.html +4 -4
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +6 -0
- package/src/branch-landed.ts +17 -3
- package/src/config-store.ts +12 -1
- package/src/db/agents.ts +3 -2
- package/src/db/continuations.ts +13 -3
- package/src/db/index.ts +3 -0
- package/src/db/issue-lifecycle.ts +12 -0
- package/src/db/mappers.ts +17 -0
- package/src/db/messages.ts +9 -4
- package/src/db/migrations.ts +91 -1
- package/src/db/notification-subscriptions.ts +1 -1
- package/src/db/prompt-templates.ts +19 -0
- package/src/db/schema.ts +67 -1
- package/src/db/stats.ts +3 -2
- package/src/db/task-comment-through.ts +112 -0
- package/src/db/task-core.ts +14 -1
- package/src/db/task-entity.ts +629 -0
- package/src/db/task-maintenance.ts +4 -3
- package/src/db/task-memory.ts +180 -0
- package/src/db/tasks.ts +24 -10
- package/src/issue-tracker/adapter.ts +15 -0
- package/src/issue-tracker/github.ts +40 -2
- package/src/issue-tracker/index.ts +1 -0
- package/src/mcp/index.ts +12 -1
- package/src/mcp/tools-spawn.ts +4 -0
- package/src/mcp-dispatch-tools.ts +164 -0
- package/src/mcp-notification-subscription-tools.ts +62 -4
- package/src/mcp-plan-tools.ts +30 -3
- package/src/mcp-release-tools.ts +33 -0
- package/src/mcp-task-memory-tools.ts +132 -0
- package/src/mcp-task-tools.ts +232 -0
- package/src/mcp-task-weave-tools.ts +66 -0
- package/src/notification-router.ts +24 -2
- package/src/notification-rules.ts +4 -0
- package/src/notification-types.ts +23 -0
- package/src/prompt-resolver.ts +66 -3
- package/src/routes/agents-spawn.ts +10 -0
- package/src/routes/index.ts +14 -1
- package/src/routes/plans.ts +10 -2
- package/src/routes/tasks.ts +276 -2
- package/src/routing-policy-store.ts +147 -2
- package/src/self-resume.ts +13 -2
- package/src/services/issue-lifecycle.ts +9 -1
- package/src/services/plan-graph.ts +107 -9
- package/src/services/release-readiness.ts +142 -0
- package/src/services/send-message.ts +31 -0
- package/src/services/spawn-agent.ts +81 -4
- package/src/services/task-context-hydration.ts +361 -0
- package/src/services/task-entity.ts +149 -0
- package/src/services/task-issue-weave.ts +558 -0
- package/src/services/task-lifecycle.ts +465 -0
- package/src/services/task-plan-view.ts +178 -0
- package/src/services/task-semantic-events.ts +137 -0
- package/src/services/transient-agent-reaper.ts +55 -22
- package/src/spawn-targets.ts +27 -2
- package/src/task-dispatcher.ts +461 -0
- package/src/upgrade-install-verify.ts +20 -1
- package/public/assets/display-BWi8DQCp.js +0 -3
- package/public/assets/display-BWi8DQCp.js.map +0 -1
- package/public/assets/index-3ydeNO0c.js +0 -23
- package/public/assets/index-3ydeNO0c.js.map +0 -1
- package/public/assets/types-DorDDvEb.js +0 -2
- package/public/assets/types-DorDDvEb.js.map +0 -1
package/src/routes/tasks.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: tasks.
|
|
2
2
|
import { MAX_BODY_BYTES } from "../config";
|
|
3
|
-
import { VALID_TASK_STATUSES, agentSessionStatus, auditEvent, dispatchTaskCallbacks, emitCommand, error, json, normalizeAgentSessionGuard, parseBody, parseId, parseQueryInt, type Handler } from "./_shared";
|
|
3
|
+
import { VALID_TASK_STATUSES, agentSessionStatus, auditEvent, dispatchTaskCallbacks, emitCommand, error, json, normalizeAgentSessionGuard, parseBody, parseId, parseQueryInt, resolveActorLabel, type Handler } from "./_shared";
|
|
4
4
|
import { ValidationError, claimTask, getMessage, getTask, listTaskEvents, listTasks, recordTaskEvent, renewTaskClaim, updateTaskStatus } from "../db";
|
|
5
|
+
import {
|
|
6
|
+
addTaskEntityDependency, addTaskEntityRef, assignTaskEntityAgent, createTaskEntity, getTaskEntity,
|
|
7
|
+
listTaskEntities, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, updateTaskEntity,
|
|
8
|
+
} from "../services/task-entity";
|
|
9
|
+
import { CommentThroughError, commentOnTaskThrough, taskCloseBridge } from "../services/task-issue-weave";
|
|
5
10
|
import { captureTaskResultMemory, injectMemoryForTaskClaim } from "../memory-service";
|
|
6
11
|
import { cleanEpoch, cleanMeta, cleanString, optionalEnum } from "../validation";
|
|
7
12
|
import { emitMessageClaimed, emitTaskChanged } from "../sse";
|
|
8
|
-
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
13
|
+
import { errMessage, isRecord, TASK_DELIVERABLE_STAGES, type TaskBlocker, type TaskDispatch, type TaskGateStatus } from "agent-relay-sdk";
|
|
9
14
|
import { type AgentSessionGuard, type TaskStatus, type TaskStatusInput } from "../types";
|
|
10
15
|
|
|
16
|
+
function actorOf(req: Request, fallback?: unknown): string {
|
|
17
|
+
return resolveActorLabel(req) ?? (typeof fallback === "string" && fallback.trim() ? fallback.trim() : "dashboard");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function taskDetailOr404(id: number) {
|
|
21
|
+
const detail = getTaskEntity(id);
|
|
22
|
+
return detail ? json(detail) : error("deliverable task not found", 404);
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
function normalizeTaskStatusInput(body: unknown): TaskStatusInput {
|
|
12
26
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
13
27
|
const status = optionalEnum(body.status, "status", VALID_TASK_STATUSES);
|
|
@@ -27,14 +41,274 @@ export const getTasks: Handler = (req) => {
|
|
|
27
41
|
const url = new URL(req.url);
|
|
28
42
|
const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
|
|
29
43
|
if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
|
|
44
|
+
// The legacy queue defaults to the claim lane (#834). `kind=deliverable` returns the
|
|
45
|
+
// durable Tasks (optionally filtered by deliverable stage / team).
|
|
46
|
+
const kind = url.searchParams.get("kind") ?? undefined;
|
|
47
|
+
if (kind === "deliverable") {
|
|
48
|
+
return json(listTaskEntities({
|
|
49
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
50
|
+
deliverableStage: url.searchParams.get("deliverableStage") ?? undefined,
|
|
51
|
+
teamId: url.searchParams.get("teamId") ?? undefined,
|
|
52
|
+
limit: limitRaw ?? 100,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
30
55
|
return json(listTasks({
|
|
31
56
|
status: url.searchParams.get("status") ?? undefined,
|
|
32
57
|
source: url.searchParams.get("source") ?? undefined,
|
|
33
58
|
target: url.searchParams.get("target") ?? undefined,
|
|
59
|
+
kind: kind === "all" ? "all" : "claim",
|
|
34
60
|
limit: limitRaw ?? 100,
|
|
35
61
|
}));
|
|
36
62
|
};
|
|
37
63
|
|
|
64
|
+
// --- #834 durable Task entity (kind='deliverable') HTTP surface ---
|
|
65
|
+
|
|
66
|
+
export const postTaskCreate: Handler = async (req) => {
|
|
67
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
68
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
69
|
+
const body = parsed.body;
|
|
70
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
71
|
+
try {
|
|
72
|
+
const created = createTaskEntity({
|
|
73
|
+
title: body.title as string,
|
|
74
|
+
intent: cleanString(body.intent, "intent", { max: MAX_BODY_BYTES }),
|
|
75
|
+
doneWhen: cleanString(body.doneWhen, "doneWhen", { max: MAX_BODY_BYTES }),
|
|
76
|
+
teamId: cleanString(body.teamId, "teamId", { max: 200 }),
|
|
77
|
+
dispatch: body.dispatch as TaskDispatch | undefined,
|
|
78
|
+
createdBy: actorOf(req, body.createdBy),
|
|
79
|
+
});
|
|
80
|
+
if (Array.isArray(body.issueRefs)) {
|
|
81
|
+
for (const ref of body.issueRefs) {
|
|
82
|
+
if (typeof ref === "string" && ref.trim()) addTaskEntityRef(created.id, { issue: ref, defaultRepo: cleanString(body.defaultRepo, "defaultRepo", { max: 500 }), actor: actorOf(req, body.createdBy) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(body.dependsOn)) {
|
|
86
|
+
for (const dep of body.dependsOn) {
|
|
87
|
+
const depId = parseId(String(dep));
|
|
88
|
+
if (depId !== null) addTaskEntityDependency(created.id, depId, actorOf(req, body.createdBy));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return json(getTaskEntity(created.id), 201);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const getTaskDetailRoute: Handler = (_req, params) => {
|
|
99
|
+
const id = parseId(params.id);
|
|
100
|
+
if (id === null) return error("invalid task id");
|
|
101
|
+
return taskDetailOr404(id);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const patchTaskEntity: Handler = async (req, params) => {
|
|
105
|
+
const id = parseId(params.id);
|
|
106
|
+
if (id === null) return error("invalid task id");
|
|
107
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
108
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
109
|
+
const body = parsed.body;
|
|
110
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
111
|
+
try {
|
|
112
|
+
const detail = updateTaskEntity(id, {
|
|
113
|
+
...(body.title !== undefined ? { title: cleanString(body.title, "title", { max: 500 }) } : {}),
|
|
114
|
+
...(body.intent !== undefined ? { intent: cleanString(body.intent, "intent", { max: MAX_BODY_BYTES }) ?? "" } : {}),
|
|
115
|
+
...(body.latestSteer !== undefined ? { latestSteer: cleanString(body.latestSteer, "latestSteer", { max: MAX_BODY_BYTES }) ?? "" } : {}),
|
|
116
|
+
...(body.doneWhen !== undefined ? { doneWhen: cleanString(body.doneWhen, "doneWhen", { max: MAX_BODY_BYTES }) ?? "" } : {}),
|
|
117
|
+
...(body.dispatch !== undefined ? { dispatch: body.dispatch as TaskDispatch } : {}),
|
|
118
|
+
...(body.teamId !== undefined ? { teamId: cleanString(body.teamId, "teamId", { max: 200 }) ?? "" } : {}),
|
|
119
|
+
actor: actorOf(req),
|
|
120
|
+
});
|
|
121
|
+
return json(detail);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return entityError(e);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const postTaskEntityStatus: Handler = async (req, params) => {
|
|
128
|
+
const id = parseId(params.id);
|
|
129
|
+
if (id === null) return error("invalid task id");
|
|
130
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
131
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
132
|
+
const body = parsed.body;
|
|
133
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
134
|
+
try {
|
|
135
|
+
const detail = setTaskEntityStatus(id, {
|
|
136
|
+
status: body.status as TaskStatus,
|
|
137
|
+
blockedBy: body.blockedBy as TaskBlocker[] | undefined,
|
|
138
|
+
note: cleanString(body.note, "note", { max: 4000 }),
|
|
139
|
+
actor: actorOf(req),
|
|
140
|
+
});
|
|
141
|
+
return json(detail);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return entityError(e);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const postTaskEntityStage: Handler = async (req, params) => {
|
|
148
|
+
const id = parseId(params.id);
|
|
149
|
+
if (id === null) return error("invalid task id");
|
|
150
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
151
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
152
|
+
const body = parsed.body;
|
|
153
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
154
|
+
try {
|
|
155
|
+
const detail = setTaskEntityStage(id, {
|
|
156
|
+
deliverableStage: optionalEnum(body.deliverableStage, "deliverableStage", TASK_DELIVERABLE_STAGES) as any,
|
|
157
|
+
branch: cleanString(body.branch, "branch", { max: 500 }),
|
|
158
|
+
commit: isRecord(body.commit) ? { sha: body.commit.sha as string, subject: body.commit.subject as string | undefined } : undefined,
|
|
159
|
+
actor: actorOf(req),
|
|
160
|
+
});
|
|
161
|
+
return json(detail);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return entityError(e);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const postTaskEntityGate: Handler = async (req, params) => {
|
|
168
|
+
const id = parseId(params.id);
|
|
169
|
+
if (id === null) return error("invalid task id");
|
|
170
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
171
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
172
|
+
const body = parsed.body;
|
|
173
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
174
|
+
try {
|
|
175
|
+
return json(recordTaskEntityGate(id, {
|
|
176
|
+
gate: cleanString(body.gate, "gate", { required: true, max: 120 })!,
|
|
177
|
+
status: body.status as TaskGateStatus,
|
|
178
|
+
detail: cleanString(body.detail, "detail", { max: 4000 }),
|
|
179
|
+
commitSha: cleanString(body.commitSha, "commitSha", { max: 80 }),
|
|
180
|
+
runId: cleanString(body.runId, "runId", { max: 240 }),
|
|
181
|
+
actor: actorOf(req),
|
|
182
|
+
}));
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return entityError(e);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const postTaskDependency: Handler = async (req, params) => {
|
|
189
|
+
const id = parseId(params.id);
|
|
190
|
+
if (id === null) return error("invalid task id");
|
|
191
|
+
const parsed = await parseBody<{ dependsOnTaskId?: number }>(req);
|
|
192
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
193
|
+
const dep = parseId(String(parsed.body?.dependsOnTaskId));
|
|
194
|
+
if (dep === null) return error("dependsOnTaskId required");
|
|
195
|
+
try {
|
|
196
|
+
addTaskEntityDependency(id, dep, actorOf(req));
|
|
197
|
+
return taskDetailOr404(id);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
return entityError(e);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const deleteTaskDependency: Handler = async (req, params) => {
|
|
204
|
+
const id = parseId(params.id);
|
|
205
|
+
if (id === null) return error("invalid task id");
|
|
206
|
+
const parsed = await parseBody<{ dependsOnTaskId?: number }>(req);
|
|
207
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
208
|
+
const dep = parseId(String(parsed.body?.dependsOnTaskId));
|
|
209
|
+
if (dep === null) return error("dependsOnTaskId required");
|
|
210
|
+
removeTaskEntityDependency(id, dep, actorOf(req));
|
|
211
|
+
return taskDetailOr404(id);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const postTaskRef: Handler = async (req, params) => {
|
|
215
|
+
const id = parseId(params.id);
|
|
216
|
+
if (id === null) return error("invalid task id");
|
|
217
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
218
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
219
|
+
const body = parsed.body;
|
|
220
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
221
|
+
try {
|
|
222
|
+
const detail = addTaskEntityRef(id, {
|
|
223
|
+
issue: cleanString(body.issue, "issue", { max: 1000 }),
|
|
224
|
+
defaultRepo: cleanString(body.defaultRepo, "defaultRepo", { max: 500 }),
|
|
225
|
+
branch: cleanString(body.branch, "branch", { max: 500 }),
|
|
226
|
+
commit: isRecord(body.commit) ? { sha: body.commit.sha as string, subject: body.commit.subject as string | undefined } : undefined,
|
|
227
|
+
actor: actorOf(req),
|
|
228
|
+
});
|
|
229
|
+
return json(detail);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return entityError(e);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const deleteTaskRef: Handler = async (req, params) => {
|
|
236
|
+
const id = parseId(params.id);
|
|
237
|
+
if (id === null) return error("invalid task id");
|
|
238
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
239
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
240
|
+
const body = parsed.body;
|
|
241
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
242
|
+
try {
|
|
243
|
+
const detail = removeTaskEntityRef(id, {
|
|
244
|
+
issueRefId: cleanString(body.issueRefId, "issueRefId", { max: 120 }),
|
|
245
|
+
branch: body.branch === true,
|
|
246
|
+
commitSha: cleanString(body.commitSha, "commitSha", { max: 80 }),
|
|
247
|
+
actor: actorOf(req),
|
|
248
|
+
});
|
|
249
|
+
return json(detail);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return entityError(e);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export const postTaskAssign: Handler = async (req, params) => {
|
|
256
|
+
const id = parseId(params.id);
|
|
257
|
+
if (id === null) return error("invalid task id");
|
|
258
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
259
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
260
|
+
const body = parsed.body;
|
|
261
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
262
|
+
try {
|
|
263
|
+
const detail = assignTaskEntityAgent(id, cleanString(body.agentId, "agentId", { required: true, max: 200 })!, cleanString(body.role, "role", { max: 120 }), actorOf(req));
|
|
264
|
+
return json(detail);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return entityError(e);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// #833 — comment-on-Task writes THROUGH to the linked GitHub issue (discussion lives in
|
|
271
|
+
// GitHub, never a relay table). Execution telemetry never reaches this path.
|
|
272
|
+
export const postTaskComment: Handler = async (req, params) => {
|
|
273
|
+
const id = parseId(params.id);
|
|
274
|
+
if (id === null) return error("invalid task id");
|
|
275
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
276
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
277
|
+
const body = parsed.body;
|
|
278
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
279
|
+
try {
|
|
280
|
+
const result = await commentOnTaskThrough({
|
|
281
|
+
taskId: id,
|
|
282
|
+
body: cleanString(body.body, "body", { required: true, max: MAX_BODY_BYTES })!,
|
|
283
|
+
clientId: cleanString(body.clientId, "clientId", { max: 240 }),
|
|
284
|
+
author: actorOf(req, body.author),
|
|
285
|
+
});
|
|
286
|
+
return json(result);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Upstream GitHub write failed (well-formed request, no receipt written) → 502, not 400/500.
|
|
289
|
+
if (e instanceof CommentThroughError) return error(e.message, 502);
|
|
290
|
+
return entityError(e);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// #833 — read the #482/#474 close-bridge projection for a Task's linked issues.
|
|
295
|
+
export const getTaskCloseBridge: Handler = (_req, params) => {
|
|
296
|
+
const id = parseId(params.id);
|
|
297
|
+
if (id === null) return error("invalid task id");
|
|
298
|
+
try {
|
|
299
|
+
return json({ taskId: id, bridge: taskCloseBridge(id) });
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return entityError(e);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
function entityError(e: unknown): Response {
|
|
306
|
+
if (e instanceof ValidationError) {
|
|
307
|
+
return error(e.message, /not found/.test(e.message) ? 404 : 400);
|
|
308
|
+
}
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
|
|
38
312
|
export const getTaskById: Handler = (_req, params) => {
|
|
39
313
|
const id = parseId(params.id);
|
|
40
314
|
if (id === null) return error("invalid task id");
|
|
@@ -9,8 +9,25 @@
|
|
|
9
9
|
import { ValidationError } from "./db";
|
|
10
10
|
import { getConfig, setConfig } from "./config-store";
|
|
11
11
|
import { notifyRoutingPolicyChanged } from "./routing-policy-push";
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
isRecord,
|
|
14
|
+
SPAWN_PROVIDERS,
|
|
15
|
+
TASK_DISPATCH_COMPLEXITIES,
|
|
16
|
+
TASK_DISPATCH_CRITICALITIES,
|
|
17
|
+
TASK_DISPATCH_FOOTPRINTS,
|
|
18
|
+
TASK_DISPATCH_TYPES,
|
|
19
|
+
} from "agent-relay-sdk";
|
|
20
|
+
import type {
|
|
21
|
+
DispatchAutoReview,
|
|
22
|
+
DispatchMatch,
|
|
23
|
+
DispatchModelTarget,
|
|
24
|
+
DispatchPolicy,
|
|
25
|
+
DispatchRule,
|
|
26
|
+
ProviderQuotaRoutingConfig,
|
|
27
|
+
QuotaBandThresholds,
|
|
28
|
+
RoutingPolicy,
|
|
29
|
+
RoutingRule,
|
|
30
|
+
} from "agent-relay-sdk";
|
|
14
31
|
|
|
15
32
|
export const ROUTING_POLICY_NAMESPACE = "routing-policy";
|
|
16
33
|
export const ROUTING_POLICY_KEY = "default";
|
|
@@ -21,6 +38,37 @@ export const DEFAULT_ROUTING_POLICY: RoutingPolicy = {
|
|
|
21
38
|
rules: [],
|
|
22
39
|
};
|
|
23
40
|
|
|
41
|
+
/**
|
|
42
|
+
* #839 — built-in dispatch policy. Encodes the coordinator routing heuristics that used to
|
|
43
|
+
* live as tribal knowledge in memory files, so they are one inspectable + editable place that
|
|
44
|
+
* survives compaction. Used by the dispatcher whenever `RoutingPolicy.dispatch` is unset.
|
|
45
|
+
* Rules are evaluated top-down; the first match wins, so put the most specific rules first.
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_DISPATCH_POLICY: DispatchPolicy = {
|
|
48
|
+
rules: [
|
|
49
|
+
{ label: "review → Codex (reviewer edge)", match: { type: "review" }, target: { provider: "codex", effort: "high" } },
|
|
50
|
+
{ label: "debug → Codex high", match: { type: "debug" }, target: { provider: "codex", effort: "high" } },
|
|
51
|
+
{ label: "ui → Sonnet", match: { type: "ui" }, target: { provider: "claude", model: "sonnet-4.6", effort: "medium" } },
|
|
52
|
+
{ label: "complex build → Opus 4.8 high", match: { type: "build", complexity: "complex" }, target: { provider: "claude", model: "opus-4.8", effort: "high" } },
|
|
53
|
+
{ label: "research → Opus 4.8 high", match: { type: "research" }, target: { provider: "claude", model: "opus-4.8", effort: "high" } },
|
|
54
|
+
{ label: "refactor → Opus 4.8 medium", match: { type: "refactor" }, target: { provider: "claude", model: "opus-4.8", effort: "medium" } },
|
|
55
|
+
{ label: "moderate build → Sonnet medium", match: { type: "build", complexity: "moderate" }, target: { provider: "claude", model: "sonnet-4.6", effort: "medium" } },
|
|
56
|
+
{ label: "ops → Sonnet low", match: { type: "ops" }, target: { provider: "claude", model: "sonnet-4.6", effort: "low" } },
|
|
57
|
+
{ label: "trivial → Sonnet low", match: { complexity: "trivial" }, target: { provider: "claude", model: "sonnet-4.6", effort: "low" } },
|
|
58
|
+
],
|
|
59
|
+
fallback: { provider: "claude", model: "sonnet-4.6", effort: "medium" },
|
|
60
|
+
// Large footprint demands a 1M-window model; the dispatcher escalates same-family (e.g.
|
|
61
|
+
// opus-4.8 → opus-4.8[1m]) when the matched model's window is below this.
|
|
62
|
+
footprintMinContextTokens: { large: 1_000_000 },
|
|
63
|
+
// The winning build+review pattern, made automatic: high criticality OR complex builds get
|
|
64
|
+
// a dependent adversarial review Task, routed to Codex per the review rule above.
|
|
65
|
+
autoReview: {
|
|
66
|
+
whenCriticality: ["high"],
|
|
67
|
+
whenComplexity: ["complex"],
|
|
68
|
+
target: { provider: "codex", effort: "high" },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
24
72
|
/** Retrieve the stored routing policy, falling back to the default when absent. */
|
|
25
73
|
export function getRoutingPolicy(): RoutingPolicy {
|
|
26
74
|
const entry = getConfig<RoutingPolicy>(ROUTING_POLICY_NAMESPACE, ROUTING_POLICY_KEY);
|
|
@@ -59,9 +107,106 @@ export function normalizeRoutingPolicy(value: unknown): RoutingPolicy {
|
|
|
59
107
|
rules: normalizeRules(input.rules),
|
|
60
108
|
...(input.quotaBands !== undefined ? { quotaBands: normalizeQuotaBands(input.quotaBands) } : {}),
|
|
61
109
|
...(input.providerQuota !== undefined ? { providerQuota: normalizeProviderQuota(input.providerQuota) } : {}),
|
|
110
|
+
...(input.dispatch !== undefined ? { dispatch: normalizeDispatchPolicy(input.dispatch) } : {}),
|
|
62
111
|
};
|
|
63
112
|
}
|
|
64
113
|
|
|
114
|
+
// --- #839 dispatch-policy normalizer ---------------------------------------
|
|
115
|
+
|
|
116
|
+
function normalizeDispatchTarget(raw: unknown, ctx: string): DispatchModelTarget {
|
|
117
|
+
if (!isRecord(raw)) throw new ValidationError(`${ctx} must be an object`);
|
|
118
|
+
if (typeof raw.provider !== "string" || !(SPAWN_PROVIDERS as readonly string[]).includes(raw.provider)) {
|
|
119
|
+
throw new ValidationError(`${ctx}.provider must be one of: ${SPAWN_PROVIDERS.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
provider: raw.provider as DispatchModelTarget["provider"],
|
|
123
|
+
...(typeof raw.model === "string" && raw.model ? { model: raw.model } : {}),
|
|
124
|
+
...(typeof raw.effort === "string" && raw.effort ? { effort: raw.effort } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Preserves the input shape (scalar in → scalar out) so a stored policy round-trips identically
|
|
129
|
+
// and no-op saves stay silent. Both forms are accepted by the dispatch matcher.
|
|
130
|
+
function normalizeMatchDimension(raw: unknown, allowed: readonly string[], ctx: string): string | string[] | undefined {
|
|
131
|
+
if (raw === undefined) return undefined;
|
|
132
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
133
|
+
for (const v of values) {
|
|
134
|
+
if (typeof v !== "string" || !allowed.includes(v)) {
|
|
135
|
+
throw new ValidationError(`${ctx} must be one of: ${allowed.join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return (Array.isArray(raw) ? values : values[0]) as string | string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeDispatchMatch(raw: unknown, ctx: string): DispatchMatch {
|
|
142
|
+
if (!isRecord(raw)) throw new ValidationError(`${ctx} must be an object`);
|
|
143
|
+
const match: DispatchMatch = {};
|
|
144
|
+
const complexity = normalizeMatchDimension(raw.complexity, TASK_DISPATCH_COMPLEXITIES, `${ctx}.complexity`);
|
|
145
|
+
const type = normalizeMatchDimension(raw.type, TASK_DISPATCH_TYPES, `${ctx}.type`);
|
|
146
|
+
const footprint = normalizeMatchDimension(raw.contextFootprint, TASK_DISPATCH_FOOTPRINTS, `${ctx}.contextFootprint`);
|
|
147
|
+
const criticality = normalizeMatchDimension(raw.criticality, TASK_DISPATCH_CRITICALITIES, `${ctx}.criticality`);
|
|
148
|
+
if (complexity) match.complexity = complexity as DispatchMatch["complexity"];
|
|
149
|
+
if (type) match.type = type as DispatchMatch["type"];
|
|
150
|
+
if (footprint) match.contextFootprint = footprint as DispatchMatch["contextFootprint"];
|
|
151
|
+
if (criticality) match.criticality = criticality as DispatchMatch["criticality"];
|
|
152
|
+
return match;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeDispatchRule(raw: unknown, idx: number): DispatchRule {
|
|
156
|
+
if (!isRecord(raw)) throw new ValidationError(`dispatch.rules[${idx}] must be an object`);
|
|
157
|
+
const rule: DispatchRule = {
|
|
158
|
+
match: normalizeDispatchMatch(raw.match ?? {}, `dispatch.rules[${idx}].match`),
|
|
159
|
+
target: normalizeDispatchTarget(raw.target, `dispatch.rules[${idx}].target`),
|
|
160
|
+
};
|
|
161
|
+
if (typeof raw.label === "string" && raw.label) rule.label = raw.label;
|
|
162
|
+
if (typeof raw.enabled === "boolean") rule.enabled = raw.enabled;
|
|
163
|
+
return rule;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeAutoReview(raw: unknown): DispatchAutoReview {
|
|
167
|
+
if (!isRecord(raw)) throw new ValidationError("dispatch.autoReview must be an object");
|
|
168
|
+
const out: DispatchAutoReview = {};
|
|
169
|
+
const whenCriticality = normalizeMatchDimension(raw.whenCriticality, TASK_DISPATCH_CRITICALITIES, "dispatch.autoReview.whenCriticality");
|
|
170
|
+
const whenComplexity = normalizeMatchDimension(raw.whenComplexity, TASK_DISPATCH_COMPLEXITIES, "dispatch.autoReview.whenComplexity");
|
|
171
|
+
// These are always arrays (the dispatcher calls .includes); coerce a scalar form up.
|
|
172
|
+
if (whenCriticality !== undefined) out.whenCriticality = (Array.isArray(whenCriticality) ? whenCriticality : [whenCriticality]) as DispatchAutoReview["whenCriticality"];
|
|
173
|
+
if (whenComplexity !== undefined) out.whenComplexity = (Array.isArray(whenComplexity) ? whenComplexity : [whenComplexity]) as DispatchAutoReview["whenComplexity"];
|
|
174
|
+
if (raw.target !== undefined) out.target = normalizeDispatchTarget(raw.target, "dispatch.autoReview.target");
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function normalizeDispatchPolicy(raw: unknown): DispatchPolicy {
|
|
179
|
+
if (!isRecord(raw)) throw new ValidationError("dispatch must be an object");
|
|
180
|
+
if (!Array.isArray(raw.rules)) throw new ValidationError("dispatch.rules must be an array");
|
|
181
|
+
const policy: DispatchPolicy = {
|
|
182
|
+
rules: raw.rules.map((r, i) => normalizeDispatchRule(r, i)),
|
|
183
|
+
fallback: normalizeDispatchTarget(raw.fallback, "dispatch.fallback"),
|
|
184
|
+
};
|
|
185
|
+
if (raw.footprintMinContextTokens !== undefined) {
|
|
186
|
+
if (!isRecord(raw.footprintMinContextTokens)) throw new ValidationError("dispatch.footprintMinContextTokens must be an object");
|
|
187
|
+
const out: Partial<Record<string, number>> = {};
|
|
188
|
+
for (const band of TASK_DISPATCH_FOOTPRINTS) {
|
|
189
|
+
const v = raw.footprintMinContextTokens[band];
|
|
190
|
+
if (v === undefined) continue;
|
|
191
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
|
|
192
|
+
throw new ValidationError(`dispatch.footprintMinContextTokens.${band} must be a positive number`);
|
|
193
|
+
}
|
|
194
|
+
out[band] = v;
|
|
195
|
+
}
|
|
196
|
+
policy.footprintMinContextTokens = out as DispatchPolicy["footprintMinContextTokens"];
|
|
197
|
+
}
|
|
198
|
+
if (raw.autoReview !== undefined) policy.autoReview = normalizeAutoReview(raw.autoReview);
|
|
199
|
+
return policy;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The effective dispatch policy: the stored `RoutingPolicy.dispatch`, or the built-in
|
|
204
|
+
* {@link DEFAULT_DISPATCH_POLICY} when none is configured. The dispatcher reads this.
|
|
205
|
+
*/
|
|
206
|
+
export function getDispatchPolicy(): DispatchPolicy {
|
|
207
|
+
return getRoutingPolicy().dispatch ?? DEFAULT_DISPATCH_POLICY;
|
|
208
|
+
}
|
|
209
|
+
|
|
65
210
|
function normalizeRules(raw: unknown): RoutingRule[] {
|
|
66
211
|
if (!Array.isArray(raw)) return [];
|
|
67
212
|
const rules: RoutingRule[] = [];
|
package/src/self-resume.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { emitRelayEvent } from "./events";
|
|
15
15
|
import { routeNotification } from "./notification-router";
|
|
16
16
|
import { renderTemplate } from "./prompt-resolver";
|
|
17
|
-
import type { AgentCard, Command, ContinuationEnvelope, ContinuationLedgerEntry, SelfResumeMode } from "./types";
|
|
17
|
+
import type { AgentCard, Command, ContinuationDecisionEntry, ContinuationEnvelope, ContinuationLedgerEntry, SelfResumeMode } from "./types";
|
|
18
18
|
|
|
19
19
|
interface CompactAndResumeInput {
|
|
20
20
|
agentId: string;
|
|
@@ -146,19 +146,30 @@ export function enqueueContinuationInjectionAfterClear(command: Command): Comman
|
|
|
146
146
|
return enqueueContinuationInjectionAfterSelfResume(command);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function renderContinuation(
|
|
149
|
+
export function renderContinuation(
|
|
150
|
+
envelope: ContinuationEnvelope,
|
|
151
|
+
input: { intro?: string } = {},
|
|
152
|
+
): string {
|
|
150
153
|
const ledger = envelope.ledger.length
|
|
151
154
|
? envelope.ledger.map((entry) => `- gen ${entry.gen} @ ${new Date(entry.at).toISOString()}: ${entry.ruledOut}${entry.why ? `; why: ${entry.why}` : ""}`).join("\n")
|
|
152
155
|
: "- none";
|
|
156
|
+
const decisions = envelope.decisions.length ? envelope.decisions.map(renderDecisionEntry).join("\n") : "- none";
|
|
153
157
|
return renderTemplate("continuation.envelope", {
|
|
158
|
+
intro: input.intro ?? `You have been resumed by Agent Relay — generation ${envelope.generation}.`,
|
|
154
159
|
generation: String(envelope.generation),
|
|
155
160
|
objective: envelope.objective,
|
|
156
161
|
ruledOut: ledger,
|
|
162
|
+
decisions,
|
|
157
163
|
workingState: envelope.workingState,
|
|
158
164
|
archiveRefs: envelope.archiveRefs.length ? envelope.archiveRefs.join("\n") : "- none",
|
|
159
165
|
});
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
function renderDecisionEntry(entry: ContinuationDecisionEntry): string {
|
|
169
|
+
const by = entry.by ? ` · ${entry.by}` : "";
|
|
170
|
+
return `- gen ${entry.gen} @ ${new Date(entry.at).toISOString()}${by}: ${entry.decision}${entry.rationale ? `; rationale: ${entry.rationale}` : ""}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
function assertResumeSupported(agent: AgentCard, mode: SelfResumeMode): void {
|
|
163
174
|
const context = agent.providerCapabilities?.context;
|
|
164
175
|
const resume = context?.resume ?? (context?.clear && context?.inject ? "clear-inject" : "none");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errMessage } from "agent-relay-sdk";
|
|
2
|
-
import { getAgent, listPlansWithBoundOwner } from "../db";
|
|
2
|
+
import { getAgent, listPlansWithBoundOwner, deliverableTaskIdsByBranch } from "../db";
|
|
3
3
|
import { listIssuesForEntity, planNodeIssueEntityId } from "../db/issue-associations";
|
|
4
4
|
import {
|
|
5
5
|
getSuccessfulIssueLifecycleAction,
|
|
@@ -142,6 +142,14 @@ function associatedIssueRefs(workspace: LifecycleWorkspace): IssueRef[] {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
// #834 — task-aware resolution: a deliverable Task owns its branch, so the landed branch
|
|
146
|
+
// resolves to its linked issue(s). This is the resolution PATH; lifecycle automation that
|
|
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
|
+
}
|
|
152
|
+
}
|
|
145
153
|
return issueRefMap(refs);
|
|
146
154
|
}
|
|
147
155
|
|