agent-relay-server 0.107.0 → 0.109.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 +589 -1
- package/package.json +4 -4
- package/public/assets/MessageBubble-XgN61quY.js +4 -0
- package/public/assets/{MessageBubble-PEdvVh_m.js.map → MessageBubble-XgN61quY.js.map} +1 -1
- package/public/assets/PlanGraphPanel-LY-msOsr.js +2 -0
- package/public/assets/PlanGraphPanel-LY-msOsr.js.map +1 -0
- package/public/assets/activity-D0EotVU9.js +2 -0
- package/public/assets/activity-D0EotVU9.js.map +1 -0
- package/public/assets/{agent-profiles-CuTQtmV6.js → agent-profiles-CAGvryb9.js} +2 -2
- package/public/assets/{agent-profiles-CuTQtmV6.js.map → agent-profiles-CAGvryb9.js.map} +1 -1
- package/public/assets/agents-CXXZ_sU9.js +2 -0
- package/public/assets/{agents-CgRFJyks.js.map → agents-CXXZ_sU9.js.map} +1 -1
- package/public/assets/{analytics-C3OTFIXo.js → analytics-Q6Dl_CE9.js} +3 -3
- package/public/assets/{analytics-C3OTFIXo.js.map → analytics-Q6Dl_CE9.js.map} +1 -1
- package/public/assets/{automation-CH9YGYoE.js → automation-zig0cp23.js} +2 -2
- package/public/assets/{automation-CH9YGYoE.js.map → automation-zig0cp23.js.map} +1 -1
- package/public/assets/{badge-D8fABzMs.js → badge-kdQDWO6e.js} +2 -2
- package/public/assets/{badge-D8fABzMs.js.map → badge-kdQDWO6e.js.map} +1 -1
- package/public/assets/{branch-state-badge-CcLZ_u86.js → branch-state-badge-BRjUWyMT.js} +2 -2
- package/public/assets/{branch-state-badge-CcLZ_u86.js.map → branch-state-badge-BRjUWyMT.js.map} +1 -1
- package/public/assets/{button-KASZcscA.js → button-BKFvNXa3.js} +2 -2
- package/public/assets/{button-KASZcscA.js.map → button-BKFvNXa3.js.map} +1 -1
- package/public/assets/card-B0y1i_2D.js +2 -0
- package/public/assets/{card-Cds0MWME.js.map → card-B0y1i_2D.js.map} +1 -1
- package/public/assets/{channels-CcSnLzCG.js → channels-DOYB1iiA.js} +2 -2
- package/public/assets/{channels-CcSnLzCG.js.map → channels-DOYB1iiA.js.map} +1 -1
- package/public/assets/chat-C3QlsFBd.js +2 -0
- package/public/assets/{chat-BLcm2Aht.js.map → chat-C3QlsFBd.js.map} +1 -1
- package/public/assets/{connectors-DbPOlQhL.js → connectors-C5CeK-2M.js} +2 -2
- package/public/assets/{connectors-DbPOlQhL.js.map → connectors-C5CeK-2M.js.map} +1 -1
- package/public/assets/{copy-button-d9DFNX9v.js → copy-button-aLaiAkv2.js} +2 -2
- package/public/assets/{copy-button-d9DFNX9v.js.map → copy-button-aLaiAkv2.js.map} +1 -1
- package/public/assets/display-_5kFrHJh.js +3 -0
- package/public/assets/display-_5kFrHJh.js.map +1 -0
- package/public/assets/{dist-DOAjFOS4.js → dist-BDYU37NZ.js} +2 -2
- package/public/assets/{dist-DOAjFOS4.js.map → dist-BDYU37NZ.js.map} +1 -1
- package/public/assets/dist-BnfqT2cl.js +2 -0
- package/public/assets/{dist-DC-TqTz2.js.map → dist-BnfqT2cl.js.map} +1 -1
- package/public/assets/dist-CF28Y5wI.js +2 -0
- package/public/assets/{dist-CBVZk9U8.js.map → dist-CF28Y5wI.js.map} +1 -1
- package/public/assets/{dist-DTXKbk2K.js → dist-DDh8a_SK.js} +2 -2
- package/public/assets/{dist-DTXKbk2K.js.map → dist-DDh8a_SK.js.map} +1 -1
- package/public/assets/{dist-BbUJ1Vht.js → dist-DKoRwEZq.js} +2 -2
- package/public/assets/{dist-BbUJ1Vht.js.map → dist-DKoRwEZq.js.map} +1 -1
- package/public/assets/{dist-UhEAFdvZ.js → dist-DQPz52Zx.js} +2 -2
- package/public/assets/{dist-UhEAFdvZ.js.map → dist-DQPz52Zx.js.map} +1 -1
- package/public/assets/{dist-CgqfdHSt.js → dist-D_qcpfbI.js} +2 -2
- package/public/assets/{dist-CgqfdHSt.js.map → dist-D_qcpfbI.js.map} +1 -1
- package/public/assets/{dist-DybScJmm.js → dist-DnyQI0qt.js} +2 -2
- package/public/assets/{dist-DybScJmm.js.map → dist-DnyQI0qt.js.map} +1 -1
- package/public/assets/{dist-BwwlmyBq.js → dist-EO8jbi2F.js} +2 -2
- package/public/assets/{dist-BwwlmyBq.js.map → dist-EO8jbi2F.js.map} +1 -1
- package/public/assets/{dist-BlZvUZZB.js → dist-avJPT3qF.js} +2 -2
- package/public/assets/{dist-BlZvUZZB.js.map → dist-avJPT3qF.js.map} +1 -1
- package/public/assets/dist-cyag2tir.js +2 -0
- package/public/assets/{dist-DreO5AcZ.js.map → dist-cyag2tir.js.map} +1 -1
- package/public/assets/{es2015-DUjFyjDp.js → es2015-C9FSEGd7.js} +2 -2
- package/public/assets/{es2015-DUjFyjDp.js.map → es2015-C9FSEGd7.js.map} +1 -1
- package/public/assets/{formatted-body-impl-BJTthVHg.js → formatted-body-impl-DCQSVyWB.js} +2 -2
- package/public/assets/{formatted-body-impl-BJTthVHg.js.map → formatted-body-impl-DCQSVyWB.js.map} +1 -1
- package/public/assets/index-D6hQkte1.css +2 -0
- package/public/assets/index-RCbIyk26.js +23 -0
- package/public/assets/index-RCbIyk26.js.map +1 -0
- package/public/assets/{input-CtHfLn1i.js → input-DfN6sUbX.js} +2 -2
- package/public/assets/{input-CtHfLn1i.js.map → input-DfN6sUbX.js.map} +1 -1
- package/public/assets/{insights-DklrIhb6.js → insights-CsLs6NbL.js} +2 -2
- package/public/assets/{insights-DklrIhb6.js.map → insights-CsLs6NbL.js.map} +1 -1
- package/public/assets/{integrations-hkSW8CzM.js → integrations-CztQfrI7.js} +2 -2
- package/public/assets/{integrations-hkSW8CzM.js.map → integrations-CztQfrI7.js.map} +1 -1
- package/public/assets/{lib-l-JkMVR9.js → lib-DRRHEqRZ.js} +2 -2
- package/public/assets/{lib-l-JkMVR9.js.map → lib-DRRHEqRZ.js.map} +1 -1
- package/public/assets/{lucide-react-C4eXYCwx.js → lucide-react-DmbfpRvJ.js} +2 -2
- package/public/assets/{lucide-react-C4eXYCwx.js.map → lucide-react-DmbfpRvJ.js.map} +1 -1
- package/public/assets/{maintenance-FAAMGpuZ.js → maintenance-BN5F0Lnc.js} +2 -2
- package/public/assets/{maintenance-FAAMGpuZ.js.map → maintenance-BN5F0Lnc.js.map} +1 -1
- package/public/assets/managed-agents-R_LI9qS5.js +4 -0
- package/public/assets/{managed-agents-CVca-s3t.js.map → managed-agents-R_LI9qS5.js.map} +1 -1
- package/public/assets/markdown-preview-impl-DQlmK_ev.js +2 -0
- package/public/assets/{markdown-preview-impl-BPW4OQo9.js.map → markdown-preview-impl-DQlmK_ev.js.map} +1 -1
- package/public/assets/memory-kn4bgMXC.js +2 -0
- package/public/assets/{memory-BzuDTHC5.js.map → memory-kn4bgMXC.js.map} +1 -1
- package/public/assets/{messages-_W24PNvT.js → messages-Dv3Tv7zC.js} +2 -2
- package/public/assets/{messages-_W24PNvT.js.map → messages-Dv3Tv7zC.js.map} +1 -1
- package/public/assets/{orchestrators-Du6I_n8i.js → orchestrators-DcNLRMzk.js} +2 -2
- package/public/assets/{orchestrators-Du6I_n8i.js.map → orchestrators-DcNLRMzk.js.map} +1 -1
- package/public/assets/overview-BOLzPSSC.js +2 -0
- package/public/assets/{overview-Dyvm1qpT.js.map → overview-BOLzPSSC.js.map} +1 -1
- package/public/assets/{pairs-B8x1RCNd.js → pairs-B4r0egpx.js} +2 -2
- package/public/assets/{pairs-B8x1RCNd.js.map → pairs-B4r0egpx.js.map} +1 -1
- package/public/assets/projects-cOeuiMjl.js +2 -0
- package/public/assets/{projects-vt63bwHN.js.map → projects-cOeuiMjl.js.map} +1 -1
- package/public/assets/prompt-settings-E7-6Osj4.js +2 -0
- package/public/assets/{prompt-settings-xVXmvxB5.js.map → prompt-settings-E7-6Osj4.js.map} +1 -1
- package/public/assets/{react-dom-BMF4hNzG.js → react-dom-jQ4Vs380.js} +2 -2
- package/public/assets/{react-dom-BMF4hNzG.js.map → react-dom-jQ4Vs380.js.map} +1 -1
- package/public/assets/registry-CTN3jOnb.js +2 -0
- package/public/assets/{registry-ypiICauZ.js.map → registry-CTN3jOnb.js.map} +1 -1
- package/public/assets/{security-BfOQ6T5r.js → security-BctIcBZb.js} +2 -2
- package/public/assets/{security-BfOQ6T5r.js.map → security-BctIcBZb.js.map} +1 -1
- package/public/assets/{select-Dx8Ro-A3.js → select-BndvVb0A.js} +2 -2
- package/public/assets/{select-Dx8Ro-A3.js.map → select-BndvVb0A.js.map} +1 -1
- package/public/assets/settings-B_tRq-P4.js +6 -0
- package/public/assets/{settings-87clTx4N.js.map → settings-B_tRq-P4.js.map} +1 -1
- package/public/assets/store-DDWg67Pg.js +9 -0
- package/public/assets/store-DDWg67Pg.js.map +1 -0
- package/public/assets/tasks-BgTTxVET.js +2 -0
- package/public/assets/tasks-BgTTxVET.js.map +1 -0
- package/public/assets/teams-DJiLBEgF.js +2 -0
- package/public/assets/{teams-DJWp4AKi.js.map → teams-DJiLBEgF.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DgMuRAe_.js → terminal-viewer-impl-BLdiyCNR.js} +3 -3
- package/public/assets/{terminal-viewer-impl-DgMuRAe_.js.map → terminal-viewer-impl-BLdiyCNR.js.map} +1 -1
- package/public/assets/types-DSwhOFcJ.js +2 -0
- package/public/assets/types-DSwhOFcJ.js.map +1 -0
- package/public/assets/{use-keyboard-viewport-RYMALvu1.js → use-keyboard-viewport-Dw2CHZ50.js} +2 -2
- package/public/assets/{use-keyboard-viewport-RYMALvu1.js.map → use-keyboard-viewport-Dw2CHZ50.js.map} +1 -1
- package/public/assets/user-avatar-rt59cEsc.js +2 -0
- package/public/assets/{user-avatar-CfAUS3PZ.js.map → user-avatar-rt59cEsc.js.map} +1 -1
- package/public/assets/{work-queue-D-Otxd9u.js → work-queue-BYRdBuLR.js} +2 -2
- package/public/assets/{work-queue-D-Otxd9u.js.map → work-queue-BYRdBuLR.js.map} +1 -1
- package/public/assets/workspaces-DCRT3l9H.js +3 -0
- package/public/assets/workspaces-DCRT3l9H.js.map +1 -0
- package/public/index.html +23 -23
- 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 +112 -2
- package/src/db/notification-subscriptions.ts +1 -1
- package/src/db/prompt-templates.ts +19 -0
- package/src/db/schema.ts +74 -2
- 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 +193 -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 +115 -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 +17 -1
- package/src/routes/messages.ts +6 -6
- package/src/routes/plans.ts +10 -2
- package/src/routes/tasks.ts +335 -2
- package/src/routing-policy-store.ts +147 -2
- package/src/security.ts +4 -0
- 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-authz.ts +52 -0
- package/src/task-dispatcher.ts +461 -0
- package/src/upgrade-install-verify.ts +20 -1
- package/public/assets/MessageBubble-PEdvVh_m.js +0 -4
- package/public/assets/PlanGraphPanel-DSts7Rtq.js +0 -2
- package/public/assets/PlanGraphPanel-DSts7Rtq.js.map +0 -1
- package/public/assets/activity-BcYf8YNO.js +0 -2
- package/public/assets/activity-BcYf8YNO.js.map +0 -1
- package/public/assets/agents-CgRFJyks.js +0 -2
- package/public/assets/card-Cds0MWME.js +0 -2
- package/public/assets/chat-BLcm2Aht.js +0 -2
- package/public/assets/display-BWi8DQCp.js +0 -3
- package/public/assets/display-BWi8DQCp.js.map +0 -1
- package/public/assets/dist-CBVZk9U8.js +0 -2
- package/public/assets/dist-DC-TqTz2.js +0 -2
- package/public/assets/dist-DreO5AcZ.js +0 -2
- package/public/assets/index-3ydeNO0c.js +0 -23
- package/public/assets/index-3ydeNO0c.js.map +0 -1
- package/public/assets/index-BjjzI2LT.css +0 -2
- package/public/assets/managed-agents-CVca-s3t.js +0 -4
- package/public/assets/markdown-preview-impl-BPW4OQo9.js +0 -2
- package/public/assets/memory-BzuDTHC5.js +0 -2
- package/public/assets/overview-Dyvm1qpT.js +0 -2
- package/public/assets/projects-vt63bwHN.js +0 -2
- package/public/assets/prompt-settings-xVXmvxB5.js +0 -2
- package/public/assets/registry-ypiICauZ.js +0 -2
- package/public/assets/settings-87clTx4N.js +0 -6
- package/public/assets/store-DUfrGvOE.js +0 -9
- package/public/assets/store-DUfrGvOE.js.map +0 -1
- package/public/assets/tasks-1Kvp53wh.js +0 -2
- package/public/assets/tasks-1Kvp53wh.js.map +0 -1
- package/public/assets/teams-DJWp4AKi.js +0 -2
- package/public/assets/types-DorDDvEb.js +0 -2
- package/public/assets/types-DorDDvEb.js.map +0 -1
- package/public/assets/user-avatar-CfAUS3PZ.js +0 -2
- package/public/assets/workspaces-CIsQqQby.js +0 -3
- package/public/assets/workspaces-CIsQqQby.js.map +0 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
|
-
import { createPlanRow, deletePlan, getAgent, getEntry, getPlan, getPlanByTeam, getPlanByThread, listPlansWithBoundOwner, planThreadExists, replacePlan, ValidationError } from "../db";
|
|
2
|
+
import { addTaskDependency, createPlanRow, deletePlan, getAgent, getEntry, getPlan, getPlanByTeam, getPlanByThread, getTaskDetail, listPlansWithBoundOwner, planThreadExists, removeTaskDependency, replacePlan, ValidationError } from "../db";
|
|
3
3
|
import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/issue-associations";
|
|
4
4
|
import { resolveIssueRefFromString } from "../db/issue-refs";
|
|
5
5
|
import { emitRelayEvent } from "../events";
|
|
6
6
|
import { cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
7
7
|
import { defaultIssueRepo } from "../config";
|
|
8
|
+
import { buildTaskGraphView, parseTaskId, planStatusToTaskStatus, projectPlanOverTasks } from "./task-plan-view";
|
|
9
|
+
import { createTaskEntity, setTaskEntityStatus } from "./task-entity";
|
|
8
10
|
import type { AuthContext } from "./auth-context";
|
|
9
11
|
import type { AgentCard, Plan, PlanEdge, PlanNode, PlanOwnerRef, PlanStatus } from "../types";
|
|
10
12
|
|
|
@@ -82,8 +84,20 @@ function cleanRefs(value: unknown): PlanNode["refs"] | undefined {
|
|
|
82
84
|
if (issueId) {
|
|
83
85
|
out.issueId = resolveIssueRefFromString({ ref: issueId, defaultRepo: defaultIssueRepo() }).id;
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
// #836: refs.taskId is the live binding seam to a durable Task. Validate it resolves to a
|
|
88
|
+
// deliverable Task; store the canonical numeric id as a string. Once bound, the node's
|
|
89
|
+
// label/scopeBlurb/status PROJECT from the Task (see projectPlanOverTasks) — no double-keeping.
|
|
90
|
+
const taskIdRaw = ref.taskId;
|
|
91
|
+
const taskIdStr = typeof taskIdRaw === "number" ? String(taskIdRaw) : cleanString(taskIdRaw, "refs.taskId", { max: 240 });
|
|
92
|
+
if (taskIdStr) {
|
|
93
|
+
const taskId = parseTaskId(taskIdStr);
|
|
94
|
+
if (taskId === undefined) throw new ValidationError("refs.taskId must be a positive task id");
|
|
95
|
+
if (!getTaskDetail(taskId)) throw new ValidationError(`refs.taskId ${taskId} is not a deliverable task`);
|
|
96
|
+
out.taskId = String(taskId);
|
|
97
|
+
}
|
|
98
|
+
// agentId/landingRef remain populated by live binding in later slices (#838); reject here.
|
|
99
|
+
const unsupported = ["agentId", "landingRef"].filter((key) => cleanString(ref[key], `refs.${key}`, { max: 240 }));
|
|
100
|
+
if (unsupported.length > 0) throw new ValidationError("agentId/landingRef refs are populated by live binding in a later slice");
|
|
87
101
|
return Object.keys(out).length ? out : undefined;
|
|
88
102
|
}
|
|
89
103
|
|
|
@@ -300,11 +314,39 @@ function optionalProp<K extends keyof PlanNode>(key: K, value: PlanNode[K] | und
|
|
|
300
314
|
return value === undefined ? { [key]: undefined } as Partial<PlanNode> : { [key]: value } as Partial<PlanNode>;
|
|
301
315
|
}
|
|
302
316
|
|
|
317
|
+
// The Task this node is bound to (#836), or undefined for an unbound node / missing node.
|
|
318
|
+
function boundTaskId(plan: Plan, nodeId: string): number | undefined {
|
|
319
|
+
const node = plan.nodes.find((item) => item.id === nodeId);
|
|
320
|
+
return node ? parseTaskId(node.refs?.taskId) : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Cycle safety across BOTH edge stores (#836 review). Edges live in two places — the JSON
|
|
324
|
+
// `plan.edges` (legacy/mixed) and task_dependencies (bound pairs) — and each store's own
|
|
325
|
+
// checker sees only half the graph, so a cycle that SPANS the two (e.g. task edge A→B plus
|
|
326
|
+
// JSON edges B→C, C→A) would slip through. Validate the COMBINED projected graph (task deps ∪
|
|
327
|
+
// JSON edges) with the prospective edge before mutating either store. projectPlanOverTasks
|
|
328
|
+
// already unions both stores into `edges`, so this is the single source of truth for the graph.
|
|
329
|
+
function assertEdgeKeepsAcyclic(plan: Plan, edge: PlanEdge): void {
|
|
330
|
+
const projected = projectPlanOverTasks(plan);
|
|
331
|
+
validateEdges(projected.nodes, dedupeEdges([...projected.edges, edge]));
|
|
332
|
+
}
|
|
333
|
+
|
|
303
334
|
export function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Plan {
|
|
304
335
|
const plan = existingPlan(planId);
|
|
305
336
|
const edge = cleanEdge(edgeInput);
|
|
337
|
+
assertEdgeKeepsAcyclic(plan, edge);
|
|
338
|
+
// #836: when BOTH endpoints are Task-bound, the canonical edge store is task_dependencies,
|
|
339
|
+
// not the plan JSON. Plan edge {from,to} means `to` depends on `from`. The combined-graph
|
|
340
|
+
// pre-check above already rejected cross-store cycles; the edge surfaces back via projection.
|
|
341
|
+
const fromTask = boundTaskId(plan, edge.from);
|
|
342
|
+
const toTask = boundTaskId(plan, edge.to);
|
|
343
|
+
if (fromTask !== undefined && toTask !== undefined) {
|
|
344
|
+
addTaskDependency(toTask, fromTask, actorId(ctx));
|
|
345
|
+
const projected = projectPlanOverTasks(existingPlan(planId));
|
|
346
|
+
emitPlan("plan.changed", projected, actorId(ctx));
|
|
347
|
+
return projected;
|
|
348
|
+
}
|
|
306
349
|
const edges = dedupeEdges([...plan.edges, edge]);
|
|
307
|
-
validateEdges(plan.nodes, edges);
|
|
308
350
|
const updated = replacePlan({ ...plan, edges }, actorId(ctx));
|
|
309
351
|
emitPlan("plan.changed", updated, actorId(ctx));
|
|
310
352
|
return updated;
|
|
@@ -327,6 +369,15 @@ export function removePlanNode(planId: string, nodeId: string, ctx: AuthContext)
|
|
|
327
369
|
export function removePlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Plan {
|
|
328
370
|
const plan = existingPlan(planId);
|
|
329
371
|
const edge = cleanEdge(edgeInput);
|
|
372
|
+
// #836: a bound-pair edge lives in task_dependencies. Try that store first; fall through to
|
|
373
|
+
// the JSON path so a legacy edge stored before the nodes were bound is still removable.
|
|
374
|
+
const fromTask = boundTaskId(plan, edge.from);
|
|
375
|
+
const toTask = boundTaskId(plan, edge.to);
|
|
376
|
+
if (fromTask !== undefined && toTask !== undefined && removeTaskDependency(toTask, fromTask, actorId(ctx))) {
|
|
377
|
+
const projected = projectPlanOverTasks(existingPlan(planId));
|
|
378
|
+
emitPlan("plan.changed", projected, actorId(ctx));
|
|
379
|
+
return projected;
|
|
380
|
+
}
|
|
330
381
|
const edges = plan.edges.filter((item) => item.from !== edge.from || item.to !== edge.to);
|
|
331
382
|
if (edges.length === plan.edges.length) throw new ValidationError(`edge ${edge.from}->${edge.to} not found`);
|
|
332
383
|
const updated = replacePlan({ ...plan, edges }, actorId(ctx));
|
|
@@ -367,11 +418,58 @@ export function updateBoundPlanNodesFromAgent(agentId: string, actor = "server")
|
|
|
367
418
|
return status ? updateBoundPlanNodesForAgent(agentId, status, actor) : [];
|
|
368
419
|
}
|
|
369
420
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
421
|
+
// #836: `view: "tasks"` returns the pure task-backed DAG synthesized from a team's deliverable
|
|
422
|
+
// Tasks (no plan row needed) — the endgame view. Otherwise a stored plan is fetched and overlaid
|
|
423
|
+
// with its Task bindings via projectPlanOverTasks (coexistence: unbound plans pass through).
|
|
424
|
+
export function getPlanGraph(input: { planId?: unknown; threadId?: unknown; teamId?: unknown; channel?: unknown; view?: unknown }): Plan | null {
|
|
425
|
+
const view = cleanString(input.view, "view", { max: 40 });
|
|
373
426
|
const teamId = cleanString(input.teamId, "teamId", { max: 120 });
|
|
374
|
-
if (
|
|
427
|
+
if (view === "tasks") {
|
|
428
|
+
if (!teamId) throw new ValidationError("view 'tasks' requires teamId");
|
|
429
|
+
return buildTaskGraphView({ teamId });
|
|
430
|
+
}
|
|
431
|
+
const planId = cleanString(input.planId, "planId", { max: 120 });
|
|
432
|
+
if (planId) { const plan = getPlan(planId); return plan ? projectPlanOverTasks(plan) : null; }
|
|
433
|
+
if (teamId) { const plan = getPlanByTeam(teamId, cleanString(input.channel, "channel", { max: 120 })); return plan ? projectPlanOverTasks(plan) : null; }
|
|
375
434
|
const threadId = cleanThreadId(input.threadId);
|
|
376
|
-
|
|
435
|
+
const plan = getPlanByThread(threadId, cleanString(input.channel, "channel", { max: 120 }));
|
|
436
|
+
return plan ? projectPlanOverTasks(plan) : null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// --- #836 migration: plan node → Task. The "no double-bookkeeping" path. Either links an
|
|
440
|
+
// existing deliverable Task, or mints one from the node's label/scopeBlurb/status and binds it.
|
|
441
|
+
// After binding, the node's display fields project from the Task (projectPlanOverTasks).
|
|
442
|
+
export interface BindPlanNodeInput {
|
|
443
|
+
taskId?: number;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function bindPlanNodeToTask(planId: string, nodeId: string, input: BindPlanNodeInput, ctx: AuthContext): { plan: Plan; taskId: number } {
|
|
447
|
+
const plan = existingPlan(planId);
|
|
448
|
+
const node = plan.nodes.find((item) => item.id === nodeId);
|
|
449
|
+
if (!node) throw new ValidationError(`node ${nodeId} not found`);
|
|
450
|
+
if (node.refs?.taskId) throw new ValidationError(`node ${nodeId} is already bound to task ${node.refs.taskId}`);
|
|
451
|
+
const actor = actorId(ctx);
|
|
452
|
+
let taskId = input.taskId;
|
|
453
|
+
if (taskId === undefined) {
|
|
454
|
+
const created = createTaskEntity({
|
|
455
|
+
title: node.label,
|
|
456
|
+
...(node.scopeBlurb ? { intent: node.scopeBlurb } : {}),
|
|
457
|
+
...(plan.teamId ? { teamId: plan.teamId } : {}),
|
|
458
|
+
createdBy: actor,
|
|
459
|
+
});
|
|
460
|
+
taskId = created.id;
|
|
461
|
+
// Carry the plan node's coarse status onto the new Task so the migrated board reads true.
|
|
462
|
+
const status = planStatusToTaskStatus(node.status);
|
|
463
|
+
if (status !== "open") {
|
|
464
|
+
setTaskEntityStatus(taskId, {
|
|
465
|
+
status,
|
|
466
|
+
...(status === "blocked" ? { blockedBy: [{ type: "external", note: "migrated from plan node" }] } : {}),
|
|
467
|
+
actor,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
} else if (!getTaskDetail(taskId)) {
|
|
471
|
+
throw new ValidationError(`task ${taskId} not found or not a deliverable task`);
|
|
472
|
+
}
|
|
473
|
+
const updated = updatePlanNode(planId, nodeId, { refs: { ...(node.refs ?? {}), taskId: String(taskId) } }, ctx);
|
|
474
|
+
return { plan: projectPlanOverTasks(updated), taskId };
|
|
377
475
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getTaskDetail, listDeliverableTasks } from "../db/task-entity";
|
|
2
|
+
import type { TaskCommitRef, TaskDetail, TaskIssueLink } from "../types";
|
|
3
|
+
|
|
4
|
+
export type ReleaseBump = "none" | "patch" | "minor" | "major";
|
|
5
|
+
|
|
6
|
+
export interface ReleaseReadinessEvidence {
|
|
7
|
+
source: "task-title" | "issue-title" | "commit-subject" | "label" | "fallback";
|
|
8
|
+
value: string;
|
|
9
|
+
bump: ReleaseBump;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReleaseReadinessTask {
|
|
14
|
+
taskId: number;
|
|
15
|
+
title: string;
|
|
16
|
+
status: string;
|
|
17
|
+
deliverableStage: string;
|
|
18
|
+
issues: TaskIssueLink[];
|
|
19
|
+
commits: TaskCommitRef[];
|
|
20
|
+
commitRange?: string;
|
|
21
|
+
suggestedBump: ReleaseBump;
|
|
22
|
+
evidence: ReleaseReadinessEvidence[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReleaseReadinessReport {
|
|
26
|
+
generatedAt: number;
|
|
27
|
+
unreleasedCount: number;
|
|
28
|
+
suggestedBump: ReleaseBump;
|
|
29
|
+
commitRange?: string;
|
|
30
|
+
tasks: ReleaseReadinessTask[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ReleaseReadinessOptions {
|
|
34
|
+
teamId?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PATCH_TYPES = new Set(["fix", "chore", "docs", "refactor", "perf", "test", "build", "ci"]);
|
|
39
|
+
|
|
40
|
+
function bumpRank(bump: ReleaseBump): number {
|
|
41
|
+
if (bump === "major") return 3;
|
|
42
|
+
if (bump === "minor") return 2;
|
|
43
|
+
if (bump === "patch") return 1;
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function maxBump(values: ReleaseBump[]): ReleaseBump {
|
|
48
|
+
return values.reduce((best, bump) => bumpRank(bump) > bumpRank(best) ? bump : best, "none" as ReleaseBump);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function commitRange(commits: TaskCommitRef[]): string | undefined {
|
|
52
|
+
const shas = commits.map((commit) => commit.sha).filter(Boolean);
|
|
53
|
+
if (shas.length === 0) return undefined;
|
|
54
|
+
if (shas.length === 1) return shas[0];
|
|
55
|
+
return `${shas[0]}..${shas[shas.length - 1]}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function evidenceFromConventionalPrefix(source: ReleaseReadinessEvidence["source"], value: string): ReleaseReadinessEvidence | undefined {
|
|
59
|
+
const normalized = value.trim();
|
|
60
|
+
const match = normalized.match(/^([a-z][a-z0-9-]*)(?:\([^)]+\))?(!)?:/i);
|
|
61
|
+
if (!match) return undefined;
|
|
62
|
+
const type = match[1]!.toLowerCase();
|
|
63
|
+
const breaking = match[2] === "!";
|
|
64
|
+
if (breaking) return { source, value: normalized, bump: "major", reason: "conventional commit breaking marker" };
|
|
65
|
+
if (type === "feat") return { source, value: normalized, bump: "minor", reason: "conventional feat prefix" };
|
|
66
|
+
if (PATCH_TYPES.has(type)) return { source, value: normalized, bump: "patch", reason: `conventional ${type} prefix` };
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function evidenceFromLabel(label: string): ReleaseReadinessEvidence | undefined {
|
|
71
|
+
const value = label.trim();
|
|
72
|
+
const normalized = value.toLowerCase();
|
|
73
|
+
if (!normalized) return undefined;
|
|
74
|
+
if (["breaking", "breaking-change", "major"].includes(normalized)) {
|
|
75
|
+
return { source: "label", value, bump: "major", reason: "release label" };
|
|
76
|
+
}
|
|
77
|
+
if (["feat", "feature", "minor"].includes(normalized)) {
|
|
78
|
+
return { source: "label", value, bump: "minor", reason: "release label" };
|
|
79
|
+
}
|
|
80
|
+
if (["fix", "bug", "bugfix", "chore", "patch"].includes(normalized)) {
|
|
81
|
+
return { source: "label", value, bump: "patch", reason: "release label" };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function labelsFromTask(task: TaskDetail): string[] {
|
|
87
|
+
const raw = task.metadata.labels ?? task.metadata.issueLabels ?? task.metadata.releaseLabels;
|
|
88
|
+
if (!Array.isArray(raw)) return [];
|
|
89
|
+
return raw.filter((item): item is string => typeof item === "string");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function classifyTask(task: TaskDetail): { bump: ReleaseBump; evidence: ReleaseReadinessEvidence[] } {
|
|
93
|
+
const evidence = [
|
|
94
|
+
evidenceFromConventionalPrefix("task-title", task.title),
|
|
95
|
+
...task.issues.map((issue) => issue.title ? evidenceFromConventionalPrefix("issue-title", issue.title) : undefined),
|
|
96
|
+
...task.commits?.map((commit) => commit.subject ? evidenceFromConventionalPrefix("commit-subject", commit.subject) : undefined) ?? [],
|
|
97
|
+
...labelsFromTask(task).map(evidenceFromLabel),
|
|
98
|
+
].filter((item): item is ReleaseReadinessEvidence => item !== undefined);
|
|
99
|
+
|
|
100
|
+
const bump = maxBump(evidence.map((item) => item.bump));
|
|
101
|
+
if (bump !== "none") return { bump, evidence };
|
|
102
|
+
return {
|
|
103
|
+
bump: "patch",
|
|
104
|
+
evidence: [{ source: "fallback", value: task.title, bump: "patch", reason: "landed deliverable without feat/fix/chore evidence" }],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function releaseReadinessReport(options: ReleaseReadinessOptions = {}): ReleaseReadinessReport {
|
|
109
|
+
const landed = listDeliverableTasks({
|
|
110
|
+
deliverableStage: "landed-on-main",
|
|
111
|
+
teamId: options.teamId,
|
|
112
|
+
limit: options.limit ?? 500,
|
|
113
|
+
})
|
|
114
|
+
.map((task) => getTaskDetail(task.id))
|
|
115
|
+
.filter((task): task is TaskDetail => Boolean(task))
|
|
116
|
+
.filter((task) => task.deliverableStage === "landed-on-main");
|
|
117
|
+
|
|
118
|
+
const tasks = landed.map((task): ReleaseReadinessTask => {
|
|
119
|
+
const classification = classifyTask(task);
|
|
120
|
+
const commits = task.commits ?? [];
|
|
121
|
+
return {
|
|
122
|
+
taskId: task.id,
|
|
123
|
+
title: task.title,
|
|
124
|
+
status: task.status,
|
|
125
|
+
deliverableStage: task.deliverableStage ?? "landed-on-main",
|
|
126
|
+
issues: task.issues,
|
|
127
|
+
commits,
|
|
128
|
+
...(commitRange(commits) ? { commitRange: commitRange(commits) } : {}),
|
|
129
|
+
suggestedBump: classification.bump,
|
|
130
|
+
evidence: classification.evidence,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const allCommits = tasks.flatMap((task) => task.commits);
|
|
135
|
+
return {
|
|
136
|
+
generatedAt: Date.now(),
|
|
137
|
+
unreleasedCount: tasks.length,
|
|
138
|
+
suggestedBump: tasks.length === 0 ? "none" : maxBump(tasks.map((task) => task.suggestedBump)),
|
|
139
|
+
...(commitRange(allCommits) ? { commitRange: commitRange(allCommits) } : {}),
|
|
140
|
+
tasks,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { canonicalHumanAgentId, isHumanAgentId, isMechanicalMessageKind, isRecord, isReservedAgentId } from "agent-relay-sdk";
|
|
25
25
|
import {
|
|
26
26
|
ValidationError,
|
|
27
|
+
getAgent,
|
|
27
28
|
getMessage,
|
|
28
29
|
listAgents,
|
|
29
30
|
promoteLineageCapturedResponse,
|
|
@@ -185,6 +186,8 @@ export function sendMessageService(
|
|
|
185
186
|
?? spawnObligationAnsweredBy(input.from, input.to);
|
|
186
187
|
if (answered !== null) input.replyTo = answered;
|
|
187
188
|
}
|
|
189
|
+
const suppressedParent = suppressResultsOnlyAck(input);
|
|
190
|
+
if (suppressedParent) return { message: suppressedParent, created: false, receipt };
|
|
188
191
|
const result = sendMessageWithResult(input);
|
|
189
192
|
if (result.created) {
|
|
190
193
|
if (result.message.deliveryStatus === "queued") {
|
|
@@ -207,3 +210,31 @@ export function sendMessageService(
|
|
|
207
210
|
}
|
|
208
211
|
return { message: result.message, created: result.created, receipt };
|
|
209
212
|
}
|
|
213
|
+
|
|
214
|
+
function suppressResultsOnlyAck(input: SendMessageInput): Message | null {
|
|
215
|
+
if (input.kind && input.kind !== "chat" && input.kind !== "session") return null;
|
|
216
|
+
const sender = getAgent(input.from);
|
|
217
|
+
const profile = isRecord(sender?.meta?.agentProfile) ? sender.meta.agentProfile : undefined;
|
|
218
|
+
const notifications = isRecord(profile?.notifications) ? profile.notifications : undefined;
|
|
219
|
+
if (notifications?.responseMode !== "results-only") return null;
|
|
220
|
+
if (!isPassiveAck(input.body)) return null;
|
|
221
|
+
if (isRecord(input.payload?.responseCapture)) return null;
|
|
222
|
+
const session = isRecord(input.payload?.session) ? input.payload.session : undefined;
|
|
223
|
+
if (session?.final === true) return null;
|
|
224
|
+
return input.replyTo ? getMessage(input.replyTo) : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isPassiveAck(body: string): boolean {
|
|
228
|
+
const text = body.toLowerCase().replace(/[.!]+/g, "").replace(/\s+/g, " ").trim();
|
|
229
|
+
return [
|
|
230
|
+
"noted",
|
|
231
|
+
"noted standing by",
|
|
232
|
+
"acknowledged",
|
|
233
|
+
"acknowledged standing by",
|
|
234
|
+
"standing by",
|
|
235
|
+
"ok standing by",
|
|
236
|
+
"okay standing by",
|
|
237
|
+
"understood",
|
|
238
|
+
"understood standing by",
|
|
239
|
+
].includes(text);
|
|
240
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// deliberately out of scope here — this slice converges the two external TRANSPORTS.
|
|
26
26
|
import type { SpawnProvider } from "agent-relay-sdk";
|
|
27
27
|
import { DEFAULT_USER_ID, errMessage, isRecord, stringValue } from "agent-relay-sdk";
|
|
28
|
-
import { countLiveSpawnedAgents, createActivityEvent, ensureTeam, getAgent, listAgents, listWorkspaces, resolveAgentOwnership, resolveProjectForCwd, resolveScopedAnchor, setAgentTeam, ValidationError, withResolvedProvisioning } from "../db";
|
|
28
|
+
import { assignTaskAgent, countLiveSpawnedAgents, createActivityEvent, ensureTeam, getAgent, listAgents, listWorkspaces, recordTaskSpawnRequested, resolveAgentOwnership, resolveProjectForCwd, resolveScopedAnchor, setAgentTeam, ValidationError, withResolvedProvisioning } from "../db";
|
|
29
29
|
import { createCommand } from "../commands-db";
|
|
30
30
|
import { emitCommandEvent } from "../command-events";
|
|
31
31
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } from "../spawn-command";
|
|
@@ -38,6 +38,8 @@ import type { ProjectSettingsPolicy } from "../project-settings";
|
|
|
38
38
|
import type { ProjectSkillsPolicy } from "../project-skills";
|
|
39
39
|
import { applyDefaultSharedMcp, type SharedMcpSpawnOverride } from "../shared-mcp";
|
|
40
40
|
import { autoSeedProjectForCwd } from "./project";
|
|
41
|
+
import { hydrateTaskContext, projectTaskHydration } from "./task-context-hydration";
|
|
42
|
+
import { dispatchTask } from "../task-dispatcher";
|
|
41
43
|
import { hasComponentScope, isComponentAuthorizedFor } from "../security";
|
|
42
44
|
import { isPathWithinBase } from "../utils";
|
|
43
45
|
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
@@ -70,6 +72,16 @@ export interface SpawnAgentInput {
|
|
|
70
72
|
profile?: string;
|
|
71
73
|
prompt?: string;
|
|
72
74
|
systemPromptAppend?: string;
|
|
75
|
+
/**
|
|
76
|
+
* #838 — spawn-with-task. When set, relay hydrates the worker's FIRST-TURN context FROM the
|
|
77
|
+
* durable Task #834 (intent + linked-issue spec/acceptance + recorded decisions + history +
|
|
78
|
+
* attribution) instead of the caller hand-writing a brief, records the spawn on the Task for
|
|
79
|
+
* attribution, and (when model/effort are omitted) MAY resolve them from the Task's dispatch
|
|
80
|
+
* work-shape (#839). Absent = today's behavior, byte-identical.
|
|
81
|
+
*/
|
|
82
|
+
taskId?: number;
|
|
83
|
+
/** #838 — the latest one-line steer; the actionable instruction layered over the hydrated Task context. */
|
|
84
|
+
steer?: string;
|
|
73
85
|
/**
|
|
74
86
|
* #409 — project instruction projection. When set, relay composes an instruction
|
|
75
87
|
* preamble from the spawn's resolved project (own entries + inherited `contains`-
|
|
@@ -194,7 +206,45 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
194
206
|
const inheritedCwd = callerCwd && isPathWithinBase(callerCwd, orchestrator.baseDir) ? callerCwd : undefined;
|
|
195
207
|
const resolvedCwd = input.cwd || inheritedCwd || orchestrator.baseDir;
|
|
196
208
|
|
|
197
|
-
|
|
209
|
+
// #838 — spawn-with-task hydration. When a taskId is given, relay assembles the worker's
|
|
210
|
+
// first-turn context FROM the durable Task (intent + linked-issue spec/acceptance + recorded
|
|
211
|
+
// decisions + history + latest steer), resolved server-side ONCE, and projects it onto the
|
|
212
|
+
// provider-appropriate channel (Claude → systemPromptAppend; Codex/other → first message). The
|
|
213
|
+
// caller-supplied systemPromptAppend/prompt are preserved. Absent (no taskId) = byte-identical
|
|
214
|
+
// to today: `resolvedSystemPromptAppend`/`resolvedPrompt` stay the raw inputs.
|
|
215
|
+
let resolvedSystemPromptAppend = input.systemPromptAppend;
|
|
216
|
+
let resolvedPrompt = input.prompt;
|
|
217
|
+
let hydratedTask: Awaited<ReturnType<typeof hydrateTaskContext>> | undefined;
|
|
218
|
+
if (input.taskId !== undefined) {
|
|
219
|
+
hydratedTask = await hydrateTaskContext({ taskId: input.taskId, steer: input.steer });
|
|
220
|
+
const projection = projectTaskHydration(
|
|
221
|
+
input.provider,
|
|
222
|
+
hydratedTask.context,
|
|
223
|
+
hydratedTask.steer,
|
|
224
|
+
input.systemPromptAppend,
|
|
225
|
+
input.prompt,
|
|
226
|
+
);
|
|
227
|
+
resolvedSystemPromptAppend = projection.systemPromptAppend;
|
|
228
|
+
resolvedPrompt = projection.prompt;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// #838/#839 — OPTIONAL dispatch composition: when a Task is given and the caller pinned NEITHER
|
|
232
|
+
// model NOR effort, resolve them from the Task's dispatch work-shape (provider stays the explicit
|
|
233
|
+
// pin). Best-effort: a dispatch failure never blocks the spawn. Explicit model/effort always win,
|
|
234
|
+
// and a no-taskId spawn is untouched — the coupling is opt-in, not hard-wired.
|
|
235
|
+
let dispatchModel = input.model;
|
|
236
|
+
let dispatchEffort = input.effort;
|
|
237
|
+
if (hydratedTask && !input.model && !input.effort && hydratedTask.task.dispatch) {
|
|
238
|
+
try {
|
|
239
|
+
const decision = dispatchTask(hydratedTask.task.dispatch, { provider: input.provider });
|
|
240
|
+
dispatchModel = decision.target.model ?? dispatchModel;
|
|
241
|
+
dispatchEffort = decision.target.effort ?? dispatchEffort;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.warn(`task ${input.taskId} dispatch resolution skipped: ${errMessage(err)}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const modelParams = resolveSpawnModelParams(input.provider, dispatchModel, dispatchEffort);
|
|
198
248
|
|
|
199
249
|
// #331 — default the child's approval mode to the CALLER's, not a hardcoded `guarded` (a
|
|
200
250
|
// headless `guarded` child wedges on its first tool-call approval prompt). Explicit always
|
|
@@ -333,7 +383,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
333
383
|
...(userAnchor ? { userAnchor } : {}),
|
|
334
384
|
...(agentProfile ? { profile: agentProfile } : {}),
|
|
335
385
|
...(callerId ? { spawnedBy: callerId } : {}),
|
|
336
|
-
...(
|
|
386
|
+
...(resolvedSystemPromptAppend ? { callerSystemPromptAppend: resolvedSystemPromptAppend } : {}),
|
|
337
387
|
});
|
|
338
388
|
const systemPromptAppend = composedInstructions.systemPromptAppend;
|
|
339
389
|
// TODO(#569): comms:delivered emitter — deferred, lives on the #575 delivery path.
|
|
@@ -384,7 +434,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
384
434
|
approvalMode,
|
|
385
435
|
permissionMode: approvalMode,
|
|
386
436
|
providerArgs: input.providerArgs ?? [],
|
|
387
|
-
prompt:
|
|
437
|
+
prompt: resolvedPrompt,
|
|
388
438
|
systemPromptAppend,
|
|
389
439
|
relayInjectionEvents: composedInstructions.relayInjectionEvents,
|
|
390
440
|
landStrategy: input.landStrategy,
|
|
@@ -431,10 +481,37 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
431
481
|
},
|
|
432
482
|
});
|
|
433
483
|
|
|
484
|
+
// #838 — record the spawn on the Task FOR attribution, keyed on spawnRequestId. Done now (not
|
|
485
|
+
// after registration) so even a fire-and-forget spawn leaves the durable hook #831 reads to
|
|
486
|
+
// drive one-shot reap. Best-effort: an attribution write never fails the spawn.
|
|
487
|
+
if (input.taskId !== undefined) {
|
|
488
|
+
try {
|
|
489
|
+
recordTaskSpawnRequested(input.taskId, {
|
|
490
|
+
spawnRequestId,
|
|
491
|
+
provider: input.provider,
|
|
492
|
+
requestedBy,
|
|
493
|
+
...(hydratedTask?.steer ? { steer: hydratedTask.steer } : {}),
|
|
494
|
+
});
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.warn(`task ${input.taskId} spawn attribution skipped: ${errMessage(err)}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
434
500
|
// #255 — resolve the spawned child's id once it registers back to THIS relay (same DB) with
|
|
435
501
|
// meta.spawnRequestId set. Bounded poll; 0 opts out (pure fire-and-forget, e.g. the HTTP 202).
|
|
436
502
|
const waitMs = Math.min(input.waitForRegistrationMs ?? 0, 30_000);
|
|
437
503
|
const agentId = waitMs > 0 ? await waitForSpawnedAgent(spawnRequestId, waitMs) : null;
|
|
504
|
+
|
|
505
|
+
// #838 — once the child's id is known (waitForRegistrationMs > 0), stamp it as the Task owner
|
|
506
|
+
// via the #834 attribution API. Fire-and-forget spawns resolve the id later; #831 closes that
|
|
507
|
+
// gap by reading the spawn_requested event above. Best-effort, never fails the spawn.
|
|
508
|
+
if (input.taskId !== undefined && agentId) {
|
|
509
|
+
try {
|
|
510
|
+
assignTaskAgent(input.taskId, agentId, "worker", requestedBy);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
console.warn(`task ${input.taskId} owner attribution skipped: ${errMessage(err)}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
438
515
|
return {
|
|
439
516
|
ok: true,
|
|
440
517
|
spawnRequestId,
|