agent-relay-server 0.119.10 → 0.119.11
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 +29 -1
- package/package.json +2 -2
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/routes/index.ts +2 -1
- package/src/routes/tasks.ts +19 -1
- package/src/services/task-done-when.ts +1 -0
- package/src/services/task-entity.ts +148 -3
- package/src/services/task-lifecycle.ts +12 -2
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.119.
|
|
5
|
+
"version": "0.119.11",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -9440,6 +9440,34 @@
|
|
|
9440
9440
|
]
|
|
9441
9441
|
}
|
|
9442
9442
|
},
|
|
9443
|
+
"/api/tasks/reconcile-auto-scaffolded": {
|
|
9444
|
+
"post": {
|
|
9445
|
+
"operationId": "postTaskReconcileAutoScaffolded",
|
|
9446
|
+
"summary": "Post Task Reconcile Auto Scaffolded",
|
|
9447
|
+
"tags": [
|
|
9448
|
+
"Tasks"
|
|
9449
|
+
],
|
|
9450
|
+
"responses": {
|
|
9451
|
+
"200": {
|
|
9452
|
+
"description": "Success",
|
|
9453
|
+
"content": {
|
|
9454
|
+
"application/json": {}
|
|
9455
|
+
}
|
|
9456
|
+
}
|
|
9457
|
+
},
|
|
9458
|
+
"security": [
|
|
9459
|
+
{
|
|
9460
|
+
"bearerAuth": []
|
|
9461
|
+
},
|
|
9462
|
+
{
|
|
9463
|
+
"tokenHeader": []
|
|
9464
|
+
},
|
|
9465
|
+
{
|
|
9466
|
+
"tokenQuery": []
|
|
9467
|
+
}
|
|
9468
|
+
]
|
|
9469
|
+
}
|
|
9470
|
+
},
|
|
9443
9471
|
"/api/tasks/{id}": {
|
|
9444
9472
|
"get": {
|
|
9445
9473
|
"operationId": "getTaskById",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.119.
|
|
3
|
+
"version": "0.119.11",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"CONTRIBUTING.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"agent-relay-channels-host": "0.119.
|
|
40
|
+
"agent-relay-channels-host": "0.119.11",
|
|
41
41
|
"agent-relay-providers": "0.104.3",
|
|
42
42
|
"agent-relay-sdk": "0.2.106",
|
|
43
43
|
"ajv": "^8.20.0"
|
package/src/routes/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, get
|
|
|
45
45
|
import { getSettingRoute, getSettingsNamespaceRoute, getSettingsRoute, putSettingRoute, putSettingsDefaultRoute } from "./settings";
|
|
46
46
|
import { postNotificationPreviewRoute } from "./notifications";
|
|
47
47
|
import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
|
|
48
|
-
import { getTaskById, getTaskEntities, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim, postTaskCreate, getTaskDetailRoute, getTaskMemoryRoute, getTaskHistoryRoute, patchTaskEntity, postTaskEntityStatus, postTaskEntityStage, postTaskEntityGate, postTaskDependency, deleteTaskDependency, postTaskRef, deleteTaskRef, postTaskAssign, postTaskComment, getTaskCloseBridge, postTaskDispatch } from "./tasks";
|
|
48
|
+
import { getTaskById, getTaskEntities, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim, postTaskCreate, postTaskReconcileAutoScaffolded, getTaskDetailRoute, getTaskMemoryRoute, getTaskHistoryRoute, patchTaskEntity, postTaskEntityStatus, postTaskEntityStage, postTaskEntityGate, postTaskDependency, deleteTaskDependency, postTaskRef, deleteTaskRef, postTaskAssign, postTaskComment, getTaskCloseBridge, postTaskDispatch } from "./tasks";
|
|
49
49
|
import { getMyTeamRoute, getTeamOutcomesRoute, getTeamsRoute, postMyTeamDissolveRoute, putMyTeamBriefRoute } from "./teams";
|
|
50
50
|
import { postTeamDissolveByIdRoute } from "./teams";
|
|
51
51
|
import { deleteTeamTemplateRoute, getTeamTemplateRoute, getTeamTemplatesRoute, postTeamTemplateRoute, putTeamTemplateRoute } from "./team-templates";
|
|
@@ -353,6 +353,7 @@ const routes: Route[] = [
|
|
|
353
353
|
route("GET", "/api/task-entities", getTaskEntities),
|
|
354
354
|
route("GET", "/api/tasks", getTasks),
|
|
355
355
|
route("POST", "/api/tasks", postTaskCreate),
|
|
356
|
+
route("POST", "/api/tasks/reconcile-auto-scaffolded", postTaskReconcileAutoScaffolded),
|
|
356
357
|
route("GET", "/api/tasks/:id", getTaskById),
|
|
357
358
|
route("GET", "/api/tasks/:id/detail", getTaskDetailRoute),
|
|
358
359
|
route("GET", "/api/tasks/:id/memory", getTaskMemoryRoute),
|
package/src/routes/tasks.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { ValidationError, claimTask, getMessage, getTask, listTaskEvents, listTa
|
|
|
9
9
|
import { readTaskMemoryEnvelope } from "../db/task-memory";
|
|
10
10
|
import {
|
|
11
11
|
addTaskEntityDependency, addTaskEntityRef, assignTaskEntityAgent, createTaskEntity, getTaskEntity,
|
|
12
|
-
listTaskEntitySummaries, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, taskEntityHistory, updateTaskEntity,
|
|
12
|
+
listTaskEntitySummaries, reconcileAutoScaffoldedTaskReaps, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, taskEntityHistory, updateTaskEntity,
|
|
13
13
|
} from "../services/task-entity";
|
|
14
14
|
import { CommentThroughError, commentOnTaskThrough, taskCloseBridge } from "../services/task-issue-weave";
|
|
15
15
|
import { captureTaskResultMemory, injectMemoryForTaskClaim } from "../memory-service";
|
|
@@ -86,6 +86,24 @@ export const getTaskEntities: Handler = (req) => {
|
|
|
86
86
|
}));
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
export const postTaskReconcileAutoScaffolded: Handler = async (req) => {
|
|
90
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
91
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
92
|
+
const body = parsed.body ?? {};
|
|
93
|
+
if (!isRecord(body)) return error("JSON object body required");
|
|
94
|
+
const limit = body.limit === undefined ? undefined : Number(body.limit);
|
|
95
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 500)) return error("limit must be an integer between 1 and 500");
|
|
96
|
+
try {
|
|
97
|
+
return json(reconcileAutoScaffoldedTaskReaps({
|
|
98
|
+
dryRun: body.dryRun !== false,
|
|
99
|
+
limit,
|
|
100
|
+
actor: actorOf(req),
|
|
101
|
+
}));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return entityError(e);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
89
107
|
// --- #834 durable Task entity (kind='deliverable') HTTP surface ---
|
|
90
108
|
|
|
91
109
|
export const postTaskCreate: Handler = async (req) => {
|
|
@@ -15,6 +15,7 @@ export function doneWhenTargetStage(doneWhen: string | undefined): TaskDeliverab
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function stageSatisfiesDoneWhen(stage: TaskDeliverableStage, doneWhen: string | undefined): boolean {
|
|
18
|
+
if (isAgentExitDoneWhen(doneWhen)) return false;
|
|
18
19
|
const current = STAGE_INDEX.get(stage) ?? 0;
|
|
19
20
|
const target = STAGE_INDEX.get(doneWhenTargetStage(doneWhen)) ?? STAGE_INDEX.get("landed-on-main")!;
|
|
20
21
|
return current >= target;
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
deliverableTaskIdsBySpawnRequestId,
|
|
12
12
|
getAgent,
|
|
13
13
|
getTaskDetail,
|
|
14
|
+
listAgents,
|
|
14
15
|
listDeliverableTaskSummaries,
|
|
15
16
|
listDeliverableTasks,
|
|
16
17
|
listTaskEvents,
|
|
18
|
+
listWorkspaces,
|
|
17
19
|
markTaskAgentReaped,
|
|
18
20
|
recordGate,
|
|
19
21
|
recordTaskDispatchDecision,
|
|
@@ -32,8 +34,9 @@ import {
|
|
|
32
34
|
type SetTaskStatusInput,
|
|
33
35
|
type UpdateDeliverableTaskInput,
|
|
34
36
|
} from "../db";
|
|
37
|
+
import { findSpawnCommandByRequestId } from "../commands-db";
|
|
35
38
|
import { emitTaskChanged } from "../sse";
|
|
36
|
-
import type { Task, TaskDetail, TaskEvent, TaskGateState, TaskListItem } from "../types";
|
|
39
|
+
import type { CommandStatus, Task, TaskDetail, TaskEvent, TaskGateState, TaskListItem, TaskStatus, WorkspaceRecord } from "../types";
|
|
37
40
|
import { removeSubscriptionsForTask } from "../db/notification-subscriptions";
|
|
38
41
|
import { emitRelayEvent } from "../events";
|
|
39
42
|
import { routeNotification } from "../notification-router";
|
|
@@ -190,6 +193,146 @@ export function taskEntityHistory(id: number): TaskEvent[] {
|
|
|
190
193
|
return listTaskEvents(id);
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
const AUTO_SCAFFOLD_RECONCILE_STATUSES: TaskStatus[] = ["in_progress"];
|
|
197
|
+
const AUTO_SCAFFOLD_RECONCILE_MIN_AGE_MS = 15 * 60_000;
|
|
198
|
+
const SPAWN_IN_FLIGHT_STATUSES: CommandStatus[] = ["pending", "accepted", "running"];
|
|
199
|
+
const SPAWN_TERMINAL_STATUSES: CommandStatus[] = ["succeeded", "failed", "timed_out", "rejected", "canceled"];
|
|
200
|
+
const LANDED_STAGES = new Set(["landed-on-main", "deploying", "deployed"]);
|
|
201
|
+
|
|
202
|
+
function hasLiveTaskOwner(task: Pick<Task, "ownerAgentId">): boolean {
|
|
203
|
+
const owner = task.ownerAgentId?.trim();
|
|
204
|
+
if (!owner) return false;
|
|
205
|
+
const agent = getAgent(owner);
|
|
206
|
+
return Boolean(agent && agent.status !== "offline");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isAutoScaffoldedAgentExitTask(task: Pick<Task, "source" | "doneWhen">): boolean {
|
|
210
|
+
return task.source === "spawn" && isAgentExitDoneWhen(task.doneWhen);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function latestTaskActivityAt(task: Pick<Task, "createdAt" | "updatedAt" | "lastSeenAt">): number {
|
|
214
|
+
return Math.max(task.createdAt, task.updatedAt, task.lastSeenAt);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function spawnRequestIdsForTask(taskId: number): string[] {
|
|
218
|
+
return listTaskEvents(taskId)
|
|
219
|
+
.filter((event) => event.type === "agent.spawn_requested")
|
|
220
|
+
.map((event) => typeof event.metadata.spawnRequestId === "string" ? event.metadata.spawnRequestId.trim() : "")
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hasLiveSpawnAgent(spawnRequestId: string): boolean {
|
|
225
|
+
return listAgents().some((agent) => agent.meta?.spawnRequestId === spawnRequestId && agent.status !== "offline");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function ownerlessSpawnTerminal(taskId: number): { ok: boolean; reason?: string } {
|
|
229
|
+
const spawnRequestIds = spawnRequestIdsForTask(taskId);
|
|
230
|
+
if (spawnRequestIds.length === 0) return { ok: false, reason: "ownerless-no-spawn-request" };
|
|
231
|
+
|
|
232
|
+
let sawTerminal = false;
|
|
233
|
+
for (const spawnRequestId of spawnRequestIds) {
|
|
234
|
+
if (hasLiveSpawnAgent(spawnRequestId)) return { ok: false, reason: "live-spawn-agent" };
|
|
235
|
+
if (findSpawnCommandByRequestId(spawnRequestId, SPAWN_IN_FLIGHT_STATUSES)) {
|
|
236
|
+
return { ok: false, reason: "spawn-in-flight" };
|
|
237
|
+
}
|
|
238
|
+
if (findSpawnCommandByRequestId(spawnRequestId, SPAWN_TERMINAL_STATUSES)) {
|
|
239
|
+
sawTerminal = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
return { ok: false, reason: "spawn-state-unknown" };
|
|
243
|
+
}
|
|
244
|
+
return sawTerminal ? { ok: true } : { ok: false, reason: "ownerless-no-spawn-request" };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function workspaceLanded(workspace: WorkspaceRecord): boolean {
|
|
248
|
+
return workspace.status === "merged" || typeof workspace.metadata.mergedAt === "number";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function landedEvidenceFor(task: Task, workspaces: WorkspaceRecord[]): string | undefined {
|
|
252
|
+
if (task.deliverableStage && LANDED_STAGES.has(task.deliverableStage)) return `stage:${task.deliverableStage}`;
|
|
253
|
+
const owner = task.ownerAgentId?.trim();
|
|
254
|
+
const branch = task.branch?.trim();
|
|
255
|
+
const workspace = workspaces.find((ws) => workspaceLanded(ws) && (
|
|
256
|
+
(owner && ws.ownerAgentId === owner) ||
|
|
257
|
+
(branch && ws.branch === branch)
|
|
258
|
+
));
|
|
259
|
+
if (!workspace) return undefined;
|
|
260
|
+
return `workspace:${workspace.id}:${workspace.status}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function candidateAutoScaffoldedTasks(limit: number): Task[] {
|
|
264
|
+
const byId = new Map<number, Task>();
|
|
265
|
+
for (const status of AUTO_SCAFFOLD_RECONCILE_STATUSES) {
|
|
266
|
+
for (const task of listDeliverableTasks({ status, limit })) {
|
|
267
|
+
if (isAutoScaffoldedAgentExitTask(task)) byId.set(task.id, task);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return [...byId.values()].sort((a, b) => a.id - b.id);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface ReconcileAutoScaffoldedTaskReapsOptions {
|
|
274
|
+
dryRun?: boolean;
|
|
275
|
+
limit?: number;
|
|
276
|
+
actor?: string;
|
|
277
|
+
now?: number;
|
|
278
|
+
minAgeMs?: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface ReconcileAutoScaffoldedTaskReapsResult {
|
|
282
|
+
dryRun: boolean;
|
|
283
|
+
scanned: number;
|
|
284
|
+
completed: number[];
|
|
285
|
+
canceled: number[];
|
|
286
|
+
skipped: Array<{ taskId: number; reason: string }>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function reconcileAutoScaffoldedTaskReaps(opts: ReconcileAutoScaffoldedTaskReapsOptions = {}): ReconcileAutoScaffoldedTaskReapsResult {
|
|
290
|
+
const dryRun = opts.dryRun !== false;
|
|
291
|
+
const limit = Math.min(Math.max(opts.limit ?? 500, 1), 500);
|
|
292
|
+
const workspaces = listWorkspaces();
|
|
293
|
+
const completed: number[] = [];
|
|
294
|
+
const canceled: number[] = [];
|
|
295
|
+
const skipped: Array<{ taskId: number; reason: string }> = [];
|
|
296
|
+
const actor = opts.actor ?? "task-reconcile";
|
|
297
|
+
const now = opts.now ?? Date.now();
|
|
298
|
+
const minAgeMs = Math.max(opts.minAgeMs ?? AUTO_SCAFFOLD_RECONCILE_MIN_AGE_MS, 0);
|
|
299
|
+
|
|
300
|
+
for (const task of candidateAutoScaffoldedTasks(limit)) {
|
|
301
|
+
if (latestTaskActivityAt(task) > now - minAgeMs) {
|
|
302
|
+
skipped.push({ taskId: task.id, reason: "recent-task" });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (hasLiveTaskOwner(task)) {
|
|
306
|
+
skipped.push({ taskId: task.id, reason: "live-owner" });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!task.ownerAgentId?.trim()) {
|
|
310
|
+
const ownerless = ownerlessSpawnTerminal(task.id);
|
|
311
|
+
if (!ownerless.ok) {
|
|
312
|
+
skipped.push({ taskId: task.id, reason: ownerless.reason ?? "ownerless-spawn-active" });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const landedEvidence = landedEvidenceFor(task, workspaces);
|
|
317
|
+
const status = landedEvidence ? "done" : "canceled";
|
|
318
|
+
if (!dryRun) {
|
|
319
|
+
if (task.ownerAgentId && !task.reapedAt) {
|
|
320
|
+
markTaskAgentReaped(task.id, { agentId: task.ownerAgentId, reason: landedEvidence ? "reconciled-landed-worker" : "reconciled-reaped-without-land" });
|
|
321
|
+
}
|
|
322
|
+
setTaskEntityStatus(task.id, {
|
|
323
|
+
status,
|
|
324
|
+
note: landedEvidence
|
|
325
|
+
? `auto-scaffolded worker already landed (${landedEvidence}); reconciled doneWhen=agent-exit`
|
|
326
|
+
: "auto-scaffolded worker is no longer live and no landed workspace evidence was found; canceled",
|
|
327
|
+
actor,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
(status === "done" ? completed : canceled).push(task.id);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { dryRun, scanned: completed.length + canceled.length + skipped.length, completed, canceled, skipped };
|
|
334
|
+
}
|
|
335
|
+
|
|
193
336
|
// #889 — finalize bound tasks for a reaped agent whose doneWhen is "agent-exit" / "reaped".
|
|
194
337
|
// Called by the transient reaper after requesting shutdown so ephemeral run-and-exit workers
|
|
195
338
|
// don't leave their auto-scaffolded Task stuck in_progress forever.
|
|
@@ -208,8 +351,10 @@ export function finalizeEphemeralTasksOnReap(agentId: string, opts: { spawnReque
|
|
|
208
351
|
if (!isAgentExitDoneWhen(task.doneWhen)) continue;
|
|
209
352
|
if (opts.producedWork === false) {
|
|
210
353
|
try {
|
|
211
|
-
|
|
354
|
+
if (!task.reapedAt) markTaskAgentReaped(taskId, { agentId, reason: opts.reason ?? "agent-exit" });
|
|
355
|
+
setTaskEntityStatus(taskId, { status: "canceled", note: "worker reaped without landing or producing work; task canceled", actor: "task-lifecycle" });
|
|
212
356
|
notifyEmptyTransientReap(agentId, task, opts);
|
|
357
|
+
finalized.push(taskId);
|
|
213
358
|
} catch {
|
|
214
359
|
// best-effort; failures must not surface into the reaper scan
|
|
215
360
|
}
|
|
@@ -247,7 +392,7 @@ function notifyEmptyTransientReap(agentId: string, task: TaskDetail, opts: { spa
|
|
|
247
392
|
recipients,
|
|
248
393
|
message: {
|
|
249
394
|
subject: "Worker reaped without producing work",
|
|
250
|
-
body: `Worker \`${agentId}\` was reaped without producing work for Task #${task.id} (${task.title}). The task was
|
|
395
|
+
body: `Worker \`${agentId}\` was reaped without producing work for Task #${task.id} (${task.title}). The task was canceled instead of being marked done.`,
|
|
251
396
|
payload,
|
|
252
397
|
idempotencyKey: `task:${task.id}:empty-reap:${agentId}:${opts.reason ?? "agent-exit"}`,
|
|
253
398
|
replyExpected: false,
|
|
@@ -30,13 +30,13 @@ import {
|
|
|
30
30
|
getTaskDetail,
|
|
31
31
|
markTaskAgentReaped,
|
|
32
32
|
} from "../db";
|
|
33
|
-
import { setTaskEntityStage } from "./task-entity";
|
|
33
|
+
import { setTaskEntityStage, setTaskEntityStatus } from "./task-entity";
|
|
34
34
|
import { postTaskMilestoneComment, taskCloseBridge, type TaskCloseBridgeEntry } from "./task-issue-weave";
|
|
35
35
|
import { requestTransientAgentShutdown } from "./transient-agent-reaper";
|
|
36
36
|
import { emitRelayEvent } from "../events";
|
|
37
37
|
import { routeNotification } from "../notification-router";
|
|
38
38
|
import { resolveCoordinatorAgentId } from "../steward-identity";
|
|
39
|
-
import { stageSatisfiesDoneWhen } from "./task-done-when";
|
|
39
|
+
import { isAgentExitDoneWhen, stageSatisfiesDoneWhen } from "./task-done-when";
|
|
40
40
|
import { emitTaskSemanticNotification, type TaskSemanticTransition } from "./task-semantic-events";
|
|
41
41
|
import type { IssueRef, TaskDeliverableStage, TaskGateState, WorkspaceRecord } from "../types";
|
|
42
42
|
import type { IssueTrackerAdapter } from "../issue-tracker";
|
|
@@ -253,6 +253,16 @@ function finalizeReap(outcomes: TaskLandOutcome[], reap: ReapResult, log: LogFn)
|
|
|
253
253
|
if (reap.reaped && reap.agentId) {
|
|
254
254
|
try {
|
|
255
255
|
markTaskAgentReaped(outcome.taskId, { agentId: reap.agentId, reason: "deliverable-landed" });
|
|
256
|
+
const task = getTaskDetail(outcome.taskId);
|
|
257
|
+
if (task && task.status !== "done" && isAgentExitDoneWhen(task.doneWhen)) {
|
|
258
|
+
setTaskEntityStatus(outcome.taskId, {
|
|
259
|
+
status: "done",
|
|
260
|
+
note: "doneWhen=agent-exit satisfied by worker reap after land",
|
|
261
|
+
actor: "task-lifecycle",
|
|
262
|
+
});
|
|
263
|
+
outcome.doneWhenSatisfied = true;
|
|
264
|
+
outcome.statusAdvanced = true;
|
|
265
|
+
}
|
|
256
266
|
} catch (error) {
|
|
257
267
|
log(`task reap marker failed for task ${outcome.taskId}: ${errMessage(error)}`);
|
|
258
268
|
}
|