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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// #832 — Task-bound memory: task-local slice of the #815 scoped-knowledge hierarchy.
|
|
2
|
+
//
|
|
3
|
+
// This deliberately mirrors the self-resume continuation envelope primitive:
|
|
4
|
+
// replaceable working state, append-only ruled-out ledger, append-only decisions.
|
|
5
|
+
// When #815 lands, this module is the adapter from durable Task ids into the
|
|
6
|
+
// session/task scope in that chain; do not grow a second memory system beside it.
|
|
7
|
+
import { parseJson } from "../utils";
|
|
8
|
+
import { getDb, ValidationError } from "./connection.ts";
|
|
9
|
+
import { getTaskDetail } from "./task-entity.ts";
|
|
10
|
+
import { isContinuationDecisionEntry, isContinuationLedgerEntry } from "./continuations.ts";
|
|
11
|
+
import type { ContinuationBudget, ContinuationDecisionEntry, ContinuationEnvelope, ContinuationLedgerEntry, TaskDetail } from "../types";
|
|
12
|
+
|
|
13
|
+
interface TaskMemoryRow {
|
|
14
|
+
task_id: number;
|
|
15
|
+
objective: string;
|
|
16
|
+
generation: number;
|
|
17
|
+
ledger: string;
|
|
18
|
+
decisions: string;
|
|
19
|
+
working_state: string;
|
|
20
|
+
archive_refs: string;
|
|
21
|
+
budget: string;
|
|
22
|
+
created_at: number;
|
|
23
|
+
updated_at: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TaskMemoryEnvelope = ContinuationEnvelope & { taskId: number };
|
|
27
|
+
|
|
28
|
+
export function getTaskMemoryEnvelope(taskId: number): TaskMemoryEnvelope | null {
|
|
29
|
+
const row = getDb().query("SELECT * FROM task_memories WHERE task_id = ?").get(taskId) as TaskMemoryRow | undefined;
|
|
30
|
+
return row ? rowToTaskMemoryEnvelope(row) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readTaskMemoryEnvelope(taskId: number): TaskMemoryEnvelope {
|
|
34
|
+
const task = requireTask(taskId);
|
|
35
|
+
return getTaskMemoryEnvelope(taskId) ?? emptyTaskMemoryEnvelope(task, Date.now());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function replaceTaskMemoryWorkingState(input: { taskId: number; workingState: string; now?: number }): TaskMemoryEnvelope {
|
|
39
|
+
const existing = ensureTaskMemoryEnvelope(input.taskId, input.now);
|
|
40
|
+
const workingState = input.workingState.trim();
|
|
41
|
+
if (!workingState) throw new ValidationError("workingState required");
|
|
42
|
+
const now = input.now ?? Date.now();
|
|
43
|
+
getDb().query(`
|
|
44
|
+
UPDATE task_memories
|
|
45
|
+
SET generation = ?, working_state = ?, budget = ?, updated_at = ?
|
|
46
|
+
WHERE task_id = ?
|
|
47
|
+
`).run(existing.generation + 1, workingState, JSON.stringify(nextBudget(existing, now)), now, input.taskId);
|
|
48
|
+
return readTaskMemoryEnvelope(input.taskId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function appendTaskMemoryRuledOut(input: {
|
|
52
|
+
taskId: number;
|
|
53
|
+
entries: Array<string | { ruledOut: string; why?: string }>;
|
|
54
|
+
now?: number;
|
|
55
|
+
}): TaskMemoryEnvelope {
|
|
56
|
+
const existing = ensureTaskMemoryEnvelope(input.taskId, input.now);
|
|
57
|
+
if (!Array.isArray(input.entries) || input.entries.length === 0) throw new ValidationError("ruledOut must contain at least one entry");
|
|
58
|
+
const now = input.now ?? Date.now();
|
|
59
|
+
const generation = existing.generation + 1;
|
|
60
|
+
const entries = input.entries.map((entry, index) => ruledOutEntry(entry, index, generation, now));
|
|
61
|
+
getDb().query(`
|
|
62
|
+
UPDATE task_memories
|
|
63
|
+
SET generation = ?, ledger = ?, budget = ?, updated_at = ?
|
|
64
|
+
WHERE task_id = ?
|
|
65
|
+
`).run(generation, JSON.stringify([...existing.ledger, ...entries]), JSON.stringify(nextBudget(existing, now)), now, input.taskId);
|
|
66
|
+
return readTaskMemoryEnvelope(input.taskId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function appendTaskMemoryDecision(input: {
|
|
70
|
+
taskId: number;
|
|
71
|
+
decision: string;
|
|
72
|
+
rationale?: string;
|
|
73
|
+
by?: string;
|
|
74
|
+
now?: number;
|
|
75
|
+
}): TaskMemoryEnvelope {
|
|
76
|
+
const existing = ensureTaskMemoryEnvelope(input.taskId, input.now);
|
|
77
|
+
const decision = input.decision.trim();
|
|
78
|
+
if (!decision) throw new ValidationError("decision required");
|
|
79
|
+
const rationale = input.rationale?.trim() ?? "";
|
|
80
|
+
const now = input.now ?? Date.now();
|
|
81
|
+
const entry: ContinuationDecisionEntry = {
|
|
82
|
+
gen: existing.generation + 1,
|
|
83
|
+
decision,
|
|
84
|
+
rationale,
|
|
85
|
+
...(input.by?.trim() ? { by: input.by.trim() } : {}),
|
|
86
|
+
at: now,
|
|
87
|
+
};
|
|
88
|
+
getDb().query(`
|
|
89
|
+
UPDATE task_memories
|
|
90
|
+
SET generation = ?, decisions = ?, budget = ?, updated_at = ?
|
|
91
|
+
WHERE task_id = ?
|
|
92
|
+
`).run(entry.gen, JSON.stringify([...existing.decisions, entry]), JSON.stringify(nextBudget(existing, now)), now, input.taskId);
|
|
93
|
+
return readTaskMemoryEnvelope(input.taskId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureTaskMemoryEnvelope(taskId: number, now = Date.now()): TaskMemoryEnvelope {
|
|
97
|
+
const existing = getTaskMemoryEnvelope(taskId);
|
|
98
|
+
if (existing) return existing;
|
|
99
|
+
const task = requireTask(taskId);
|
|
100
|
+
const empty = emptyTaskMemoryEnvelope(task, now);
|
|
101
|
+
getDb().query(`
|
|
102
|
+
INSERT INTO task_memories (task_id, objective, generation, ledger, decisions, working_state, archive_refs, budget, created_at, updated_at)
|
|
103
|
+
VALUES (?, ?, 0, '[]', '[]', '', '[]', ?, ?, ?)
|
|
104
|
+
`).run(taskId, empty.objective, JSON.stringify(empty.budget), now, now);
|
|
105
|
+
return readTaskMemoryEnvelope(taskId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requireTask(taskId: number): TaskDetail {
|
|
109
|
+
const task = getTaskDetail(taskId);
|
|
110
|
+
if (!task) throw new ValidationError(`task ${taskId} not found or not a deliverable task`);
|
|
111
|
+
return task;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emptyTaskMemoryEnvelope(task: TaskDetail, now: number): TaskMemoryEnvelope {
|
|
115
|
+
return {
|
|
116
|
+
taskId: task.id,
|
|
117
|
+
agentId: `task:${task.id}`,
|
|
118
|
+
objective: `Task #${task.id}: ${task.title}`,
|
|
119
|
+
generation: 0,
|
|
120
|
+
ledger: [],
|
|
121
|
+
decisions: [],
|
|
122
|
+
workingState: "",
|
|
123
|
+
archiveRefs: [],
|
|
124
|
+
budget: emptyBudget(),
|
|
125
|
+
createdAt: now,
|
|
126
|
+
updatedAt: now,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rowToTaskMemoryEnvelope(row: TaskMemoryRow): TaskMemoryEnvelope {
|
|
131
|
+
return {
|
|
132
|
+
taskId: row.task_id,
|
|
133
|
+
agentId: `task:${row.task_id}`,
|
|
134
|
+
objective: row.objective,
|
|
135
|
+
generation: row.generation,
|
|
136
|
+
ledger: parseJson<ContinuationLedgerEntry[]>(row.ledger, []).filter(isContinuationLedgerEntry),
|
|
137
|
+
decisions: parseJson<ContinuationDecisionEntry[]>(row.decisions, []).filter(isContinuationDecisionEntry),
|
|
138
|
+
workingState: row.working_state,
|
|
139
|
+
archiveRefs: parseJson<string[]>(row.archive_refs, []).filter((value): value is string => typeof value === "string"),
|
|
140
|
+
budget: parseJson<ContinuationBudget>(row.budget, emptyBudget()),
|
|
141
|
+
createdAt: row.created_at,
|
|
142
|
+
updatedAt: row.updated_at,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ruledOutEntry(
|
|
147
|
+
value: string | { ruledOut: string; why?: string },
|
|
148
|
+
index: number,
|
|
149
|
+
generation: number,
|
|
150
|
+
now: number,
|
|
151
|
+
): ContinuationLedgerEntry {
|
|
152
|
+
if (typeof value !== "string" && (typeof value !== "object" || value === null || Array.isArray(value))) {
|
|
153
|
+
throw new ValidationError(`ruledOut[${index}] must be a string or object`);
|
|
154
|
+
}
|
|
155
|
+
const ruledOut = typeof value === "string" ? value.trim() : value.ruledOut?.trim();
|
|
156
|
+
if (!ruledOut) throw new ValidationError(`ruledOut[${index}] required`);
|
|
157
|
+
const why = typeof value === "string" ? "" : value.why?.trim() ?? "";
|
|
158
|
+
return { gen: generation, ruledOut, why, at: now };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nextBudget(existing: TaskMemoryEnvelope, now: number): ContinuationBudget {
|
|
162
|
+
return {
|
|
163
|
+
...existing.budget,
|
|
164
|
+
spent: {
|
|
165
|
+
...existing.budget.spent,
|
|
166
|
+
generations: existing.generation + 1,
|
|
167
|
+
wallClockMs: Math.max(0, existing.budget.spent.wallClockMs) + Math.max(0, now - existing.updatedAt),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emptyBudget(): ContinuationBudget {
|
|
173
|
+
return {
|
|
174
|
+
maxGenerations: 0,
|
|
175
|
+
maxWallClockMs: 0,
|
|
176
|
+
maxTokens: 0,
|
|
177
|
+
hardStopMultiplier: 1,
|
|
178
|
+
spent: { generations: 0, wallClockMs: 0, tokens: 0 },
|
|
179
|
+
};
|
|
180
|
+
}
|
package/src/db/tasks.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { messageTaskClosed } from "./claim-sql.ts";
|
|
|
19
19
|
import { ClaimError, getDb } from "./connection.ts";
|
|
20
20
|
import { rowToTask, rowToTaskEvent } from "./mappers.ts";
|
|
21
21
|
import { claimMessageRow } from "./message-claims.ts";
|
|
22
|
-
import { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
|
|
22
|
+
import { TASK_SELECT, insertTaskEvent, claimTaskWhere, DELIVERABLE_TASK_KIND } from "./task-core.ts";
|
|
23
23
|
import { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
|
|
24
24
|
import { renderTemplate } from "../prompt-resolver";
|
|
25
25
|
import type {
|
|
@@ -85,7 +85,7 @@ import type {
|
|
|
85
85
|
WorkspaceStatus,
|
|
86
86
|
} from "../types";
|
|
87
87
|
|
|
88
|
-
export { TASK_SELECT, insertTaskEvent } from "./task-core.ts";
|
|
88
|
+
export { TASK_SELECT, insertTaskEvent, claimTaskWhere, CLAIM_TASK_KIND, DELIVERABLE_TASK_KIND } from "./task-core.ts";
|
|
89
89
|
|
|
90
90
|
export function findOpenTaskByDedupe(source: string, dedupeKey: string): Task | null {
|
|
91
91
|
const row = getDb()
|
|
@@ -112,9 +112,16 @@ export function getTask(id: number): Task | null {
|
|
|
112
112
|
return row ? rowToTask(row) : null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
export function listTasks(filter?: { status?: string; source?: string; target?: string; limit?: number }): Task[] {
|
|
115
|
+
export function listTasks(filter?: { status?: string; source?: string; target?: string; kind?: "claim" | "deliverable" | "all"; limit?: number }): Task[] {
|
|
116
116
|
const conditions: string[] = [];
|
|
117
117
|
const params: any[] = [];
|
|
118
|
+
// Default to the claim lane so legacy queues/views (dashboard work queue, GET /api/tasks)
|
|
119
|
+
// never get polluted by durable deliverable Tasks (#834). Pass kind:'all' to span both.
|
|
120
|
+
const kind = filter?.kind ?? "claim";
|
|
121
|
+
if (kind !== "all") {
|
|
122
|
+
conditions.push("kind = ?");
|
|
123
|
+
params.push(kind);
|
|
124
|
+
}
|
|
118
125
|
if (filter?.status) {
|
|
119
126
|
conditions.push("status = ?");
|
|
120
127
|
params.push(filter.status);
|
|
@@ -165,7 +172,7 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
|
|
|
165
172
|
.query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
|
|
166
173
|
.all(now) as any[];
|
|
167
174
|
const taskRows = getDb()
|
|
168
|
-
.query(`${TASK_SELECT} WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
|
|
175
|
+
.query(`${TASK_SELECT} WHERE ${claimTaskWhere()} AND claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
|
|
169
176
|
.all(now) as any[];
|
|
170
177
|
|
|
171
178
|
if (messageRows.length > 0) {
|
|
@@ -176,7 +183,7 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
|
|
|
176
183
|
|
|
177
184
|
if (taskRows.length > 0) {
|
|
178
185
|
getDb()
|
|
179
|
-
.query(
|
|
186
|
+
.query(`UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE ${claimTaskWhere()} AND claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
|
|
180
187
|
.run(now, now);
|
|
181
188
|
|
|
182
189
|
for (const row of taskRows) {
|
|
@@ -201,14 +208,14 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
|
|
|
201
208
|
export function orphanTasksForAgent(agentId: string, now: number = Date.now()): Task[] {
|
|
202
209
|
return getDb().transaction(() => {
|
|
203
210
|
const rows = getDb()
|
|
204
|
-
.query(`${TASK_SELECT} WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')`)
|
|
211
|
+
.query(`${TASK_SELECT} WHERE ${claimTaskWhere()} AND claimed_by = ? AND status IN ('claimed', 'in_progress')`)
|
|
205
212
|
.all(agentId) as any[];
|
|
206
213
|
if (rows.length === 0) return [];
|
|
207
214
|
|
|
208
215
|
getDb().query(`
|
|
209
216
|
UPDATE tasks
|
|
210
217
|
SET status = 'orphaned', updated_at = ?, last_seen_at = ?
|
|
211
|
-
WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')
|
|
218
|
+
WHERE ${claimTaskWhere()} AND claimed_by = ? AND status IN ('claimed', 'in_progress')
|
|
212
219
|
`).run(now, now, agentId);
|
|
213
220
|
|
|
214
221
|
for (const row of rows) {
|
|
@@ -231,14 +238,14 @@ export function releaseOrphanedTasks(graceMs = 120_000, now: number = Date.now()
|
|
|
231
238
|
const cutoff = now - graceMs;
|
|
232
239
|
settleSingleTargetOnDemandTasks("status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?", [cutoff], now, "orphan-grace-elapsed");
|
|
233
240
|
const rows = getDb()
|
|
234
|
-
.query(`${TASK_SELECT} WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
|
|
241
|
+
.query(`${TASK_SELECT} WHERE ${claimTaskWhere()} AND status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
|
|
235
242
|
.all(cutoff) as any[];
|
|
236
243
|
if (rows.length === 0) return [];
|
|
237
244
|
|
|
238
245
|
getDb().query(`
|
|
239
246
|
UPDATE tasks
|
|
240
247
|
SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ?
|
|
241
|
-
WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
|
|
248
|
+
WHERE ${claimTaskWhere()} AND status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
|
|
242
249
|
`).run(now, cutoff);
|
|
243
250
|
|
|
244
251
|
for (const row of rows) {
|
|
@@ -265,6 +272,8 @@ export function claimTask(taskId: number, agentId: string, guard?: AgentSessionG
|
|
|
265
272
|
if (agent.status === "offline") return { ok: false, error: "claiming agent is offline" };
|
|
266
273
|
const task = getTask(taskId);
|
|
267
274
|
if (!task) return { ok: false, error: "task not found" };
|
|
275
|
+
// Fence: deliverable Tasks are never claimable through the claim-lease lane (#834).
|
|
276
|
+
if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "task is not claimable" };
|
|
268
277
|
if (!["open", "blocked"].includes(task.status)) return { ok: false, error: `task is ${task.status}` };
|
|
269
278
|
|
|
270
279
|
try {
|
|
@@ -273,7 +282,7 @@ export function claimTask(taskId: number, agentId: string, guard?: AgentSessionG
|
|
|
273
282
|
const expiresAt = now + CLAIM_LEASE_MS;
|
|
274
283
|
const result = getDb().query(`
|
|
275
284
|
UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
|
|
276
|
-
WHERE id = ? AND status IN ('open', 'blocked')
|
|
285
|
+
WHERE id = ? AND ${claimTaskWhere()} AND status IN ('open', 'blocked')
|
|
277
286
|
`).run(agentId, now, expiresAt, now, taskId);
|
|
278
287
|
if (result.changes === 0) return { ok: false, error: "claim race — already claimed" };
|
|
279
288
|
if (task.messageId) {
|
|
@@ -305,6 +314,7 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
|
|
|
305
314
|
if (agent.status === "offline") return { ok: false, error: "claiming agent is offline" };
|
|
306
315
|
const task = getTask(taskId);
|
|
307
316
|
if (!task) return { ok: false, error: "task not found" };
|
|
317
|
+
if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "task is not claimable" };
|
|
308
318
|
if (task.claimedBy !== agentId) return { ok: false, error: task.claimedBy ? `claimed by ${task.claimedBy}` : "task is not claimed" };
|
|
309
319
|
if (!["claimed", "in_progress", "blocked"].includes(task.status)) return { ok: false, error: `task is ${task.status}` };
|
|
310
320
|
|
|
@@ -320,6 +330,10 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
|
|
|
320
330
|
export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok: boolean; error?: string; task?: Task; event?: TaskEvent } {
|
|
321
331
|
const task = getTask(taskId);
|
|
322
332
|
if (!task) return { ok: false, error: "task not found" };
|
|
333
|
+
// Fence: the claim-lane status updater writes `claimed_by`; deliverable Tasks must never
|
|
334
|
+
// be mutated here. They transition through setTaskStatus (src/db/task-entity.ts), which
|
|
335
|
+
// never touches claimed_by. (#834)
|
|
336
|
+
if (task.kind === DELIVERABLE_TASK_KIND) return { ok: false, error: "deliverable task: use the task-entity status API" };
|
|
323
337
|
const now = Date.now();
|
|
324
338
|
const agentId = input.agentId ?? task.claimedBy ?? null;
|
|
325
339
|
if (agentId) {
|
|
@@ -5,6 +5,15 @@ export interface IssueTrackerStatus {
|
|
|
5
5
|
title: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// #838 — the linked-issue spec read used by relay to hydrate a spawned Task agent's
|
|
9
|
+
// first-turn context server-side ONCE (so the worker never burns its own context reading
|
|
10
|
+
// the issue tracker). Extends the status read with the issue BODY (intent/spec/acceptance).
|
|
11
|
+
export interface IssueTrackerIssueDetail {
|
|
12
|
+
status: IssueRefStatus;
|
|
13
|
+
title: string;
|
|
14
|
+
body: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export interface IssueTrackerCloseOptions {
|
|
9
18
|
comment?: string;
|
|
10
19
|
labels?: string[];
|
|
@@ -26,9 +35,15 @@ export interface IssueTrackerIssueSummary {
|
|
|
26
35
|
export interface IssueTrackerAdapter {
|
|
27
36
|
resolveLink(ref: IssueRef): string;
|
|
28
37
|
getStatus(ref: IssueRef): Promise<IssueTrackerStatus>;
|
|
38
|
+
/** #838 — read the issue's full spec (title + body) for relay-side Task hydration. Optional:
|
|
39
|
+
* adapters that cannot fetch a body degrade gracefully (hydration falls back to title+url). */
|
|
40
|
+
getDetail?(ref: IssueRef): Promise<IssueTrackerIssueDetail>;
|
|
29
41
|
listOpenSubIssues?(ref: IssueRef): Promise<IssueTrackerIssueSummary[]>;
|
|
30
42
|
close(ref: IssueRef, options?: IssueTrackerCloseOptions): Promise<void>;
|
|
31
43
|
reopen(ref: IssueRef, options?: IssueTrackerReopenOptions): Promise<void>;
|
|
32
44
|
comment(ref: IssueRef, body: string): Promise<void>;
|
|
45
|
+
// List existing comment bodies on the issue. Used to RECONCILE a Task comment-through after
|
|
46
|
+
// a post-before-finalize crash (#833): match the relay marker rather than blindly re-post.
|
|
47
|
+
listComments?(ref: IssueRef): Promise<string[]>;
|
|
33
48
|
applyLabels(ref: IssueRef, labels: string[]): Promise<void>;
|
|
34
49
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { IssueRef, IssueRefStatus } from "../types";
|
|
2
|
-
import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerIssueSummary, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
|
|
2
|
+
import type { IssueTrackerAdapter, IssueTrackerCloseOptions, IssueTrackerIssueDetail, IssueTrackerIssueSummary, IssueTrackerReopenOptions, IssueTrackerStatus } from "./adapter";
|
|
3
3
|
|
|
4
4
|
export const GITHUB_FIXED_LABEL = "fixed";
|
|
5
5
|
const GITHUB_CREDENTIAL_REQUIREMENT = "GitHub issue tracker adapter requires gh authenticated with a token that has issues:write scope";
|
|
6
|
+
// #838 — hard bound on the spawn-time issue read so hydration never stalls the spawn.
|
|
7
|
+
const ISSUE_DETAIL_READ_TIMEOUT_MS = 10_000;
|
|
6
8
|
|
|
7
9
|
type GitHubApiMethod = "GET" | "PATCH" | "POST";
|
|
8
10
|
|
|
@@ -10,6 +12,10 @@ export interface GitHubApiRequest {
|
|
|
10
12
|
method: GitHubApiMethod;
|
|
11
13
|
path: string;
|
|
12
14
|
body?: Record<string, unknown>;
|
|
15
|
+
/** #838 — bound the spawned `gh` subprocess so a hung/slow call can't stall the caller
|
|
16
|
+
* (e.g. relay_spawn_agent hydration). On timeout the process is killed and the call throws,
|
|
17
|
+
* which the hydration path catches and degrades to partial "spec unavailable". */
|
|
18
|
+
timeoutMs?: number;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
type GitHubApi = (request: GitHubApiRequest) => Promise<unknown>;
|
|
@@ -18,6 +24,7 @@ interface GitHubIssuePayload {
|
|
|
18
24
|
state?: unknown;
|
|
19
25
|
title?: unknown;
|
|
20
26
|
html_url?: unknown;
|
|
27
|
+
body?: unknown;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
interface GitHubSubIssueNode {
|
|
@@ -76,16 +83,20 @@ async function ghApi(request: GitHubApiRequest): Promise<unknown> {
|
|
|
76
83
|
// auth backed by a GitHub token with `issues:write` scope.
|
|
77
84
|
const args = ["gh", "api", "--method", request.method, request.path, "--header", "Accept: application/vnd.github+json"];
|
|
78
85
|
appendBodyArgs(args, request.body);
|
|
86
|
+
// `Bun.spawnSync` blocks the event loop synchronously, so a JS-level timeout race can't preempt
|
|
87
|
+
// a hung `gh`; the subprocess `timeout` (kills with SIGTERM on expiry) is the only effective bound.
|
|
79
88
|
const proc = Bun.spawnSync(args, {
|
|
80
89
|
stdin: "ignore",
|
|
81
90
|
stdout: "pipe",
|
|
82
91
|
stderr: "pipe",
|
|
83
92
|
env: process.env,
|
|
93
|
+
...(request.timeoutMs ? { timeout: request.timeoutMs } : {}),
|
|
84
94
|
});
|
|
85
95
|
const stdout = textFromBytes(proc.stdout).trim();
|
|
86
96
|
if (proc.exitCode !== 0) {
|
|
87
97
|
const stderr = textFromBytes(proc.stderr).trim();
|
|
88
|
-
|
|
98
|
+
const timedOut = request.timeoutMs && (proc.signalCode === "SIGTERM" || proc.exitCode === null);
|
|
99
|
+
throw new Error(`${GITHUB_CREDENTIAL_REQUIREMENT}; gh api ${timedOut ? `timed out after ${request.timeoutMs}ms` : "failed"}: ${stderr || stdout || `exit ${proc.exitCode}`}`);
|
|
89
100
|
}
|
|
90
101
|
if (!stdout) return {};
|
|
91
102
|
try {
|
|
@@ -152,6 +163,19 @@ class GitHubIssueTrackerAdapter implements IssueTrackerAdapter {
|
|
|
152
163
|
return { status: statusFromState(issue.state), title: titleFromIssue(issue) };
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
// #838 — same GET as getStatus, but also surfaces the issue body (intent/spec/acceptance) so
|
|
167
|
+
// relay can hydrate a spawned Task agent server-side. ONE issue read at spawn time; the worker
|
|
168
|
+
// never touches the issue tracker itself. Bounded by a timeout so a hung `gh` degrades the
|
|
169
|
+
// hydration to partial "spec unavailable" rather than stalling relay_spawn_agent.
|
|
170
|
+
async getDetail(ref: IssueRef): Promise<IssueTrackerIssueDetail> {
|
|
171
|
+
const issue = await this.api({ method: "GET", path: githubIssuePath(ref), timeoutMs: ISSUE_DETAIL_READ_TIMEOUT_MS }) as GitHubIssuePayload;
|
|
172
|
+
return {
|
|
173
|
+
status: statusFromState(issue.state),
|
|
174
|
+
title: titleFromIssue(issue),
|
|
175
|
+
body: typeof issue.body === "string" ? issue.body.trim() : "",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
155
179
|
async listOpenSubIssues(ref: IssueRef): Promise<IssueTrackerIssueSummary[]> {
|
|
156
180
|
assertGitHubIssueRef(ref);
|
|
157
181
|
const [owner, repo] = ref.repo.split("/") as [string, string];
|
|
@@ -203,6 +227,20 @@ class GitHubIssueTrackerAdapter implements IssueTrackerAdapter {
|
|
|
203
227
|
await this.api({ method: "POST", path: `${githubIssuePath(ref)}/comments`, body: { body: trimmed } });
|
|
204
228
|
}
|
|
205
229
|
|
|
230
|
+
async listComments(ref: IssueRef): Promise<string[]> {
|
|
231
|
+
const out: string[] = [];
|
|
232
|
+
for (let page = 1; page <= 10; page++) {
|
|
233
|
+
const batch = await this.api({ method: "GET", path: `${githubIssuePath(ref)}/comments?per_page=100&page=${page}` });
|
|
234
|
+
if (!Array.isArray(batch) || batch.length === 0) break;
|
|
235
|
+
for (const node of batch) {
|
|
236
|
+
const body = node && typeof node === "object" ? (node as { body?: unknown }).body : undefined;
|
|
237
|
+
if (typeof body === "string") out.push(body);
|
|
238
|
+
}
|
|
239
|
+
if (batch.length < 100) break;
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
206
244
|
async applyLabels(ref: IssueRef, labels: string[]): Promise<void> {
|
|
207
245
|
const cleaned = uniqueLabels(labels);
|
|
208
246
|
if (cleaned.length === 0) return;
|
package/src/mcp/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ import { spawnCapablePrimer } from "../spawn-targets";
|
|
|
5
5
|
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
6
6
|
import { createActivityEvent, ValidationError } from "../db";
|
|
7
7
|
import { PLAN_TOOLS, relayPlanTool } from "../mcp-plan-tools";
|
|
8
|
+
import { TASK_TOOLS, relayTaskTool } from "../mcp-task-tools";
|
|
9
|
+
import { TASK_MEMORY_TOOLS, relayTaskMemoryTool } from "../mcp-task-memory-tools";
|
|
10
|
+
import { DISPATCH_TOOLS, relayDispatchTool } from "../mcp-dispatch-tools";
|
|
11
|
+
import { TASK_WEAVE_TOOLS, relayTaskWeaveTool } from "../mcp-task-weave-tools";
|
|
12
|
+
import { RELEASE_TOOLS, relayReleaseTool } from "../mcp-release-tools";
|
|
8
13
|
import { TEAM_TOOLS, relayTeamTool } from "../mcp-team-tools";
|
|
9
14
|
import { BLACKBOARD_TOOLS, relayBlackboardTool } from "../mcp-blackboard-tools";
|
|
10
15
|
import { PROJECT_TOOLS, relayProjectTool } from "../mcp-project-tools"; import { MEMORY_TOOLS, relayMemoryTool } from "../mcp-memory-tools"; import { ISSUE_REF_TOOLS, relayIssueRefTool } from "../mcp-issue-ref-tools"; import { ISSUE_ASSOCIATION_TOOLS, relayIssueAssociationTool } from "../mcp-issue-association-tools"; import { ISSUE_ACTIVITY_TOOLS, relayIssueActivityTool } from "../mcp-issue-activity-tools"; import { ISSUE_LIFECYCLE_TOOLS, relayIssueLifecycleTool } from "../mcp-issue-lifecycle-tools";
|
|
@@ -32,7 +37,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
32
37
|
AGENT_ACTION_TOOL,
|
|
33
38
|
...WORKSPACE_TOOLS,
|
|
34
39
|
...MERGE_TOOLS, ...TEAM_TOOLS,
|
|
35
|
-
...PLAN_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
|
|
40
|
+
...PLAN_TOOLS, ...TASK_TOOLS, ...TASK_MEMORY_TOOLS, ...DISPATCH_TOOLS, ...TASK_WEAVE_TOOLS, ...RELEASE_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
|
|
36
41
|
...BLACKBOARD_TOOLS,
|
|
37
42
|
...PROJECT_TOOLS,
|
|
38
43
|
...MEMORY_TOOLS,
|
|
@@ -50,6 +55,7 @@ export const RELAY_TOOL_GROUPS: Record<string, readonly string[]> = {
|
|
|
50
55
|
workspace: ["relay_workspace_status", "relay_workspace_ready", "relay_workspace_deps", "relay_workspace_list", "relay_workspace_claim", "relay_workspace_release", "relay_workspace_land", "relay_merge_pause", "relay_merge_pauses", "relay_merge_renew", "relay_merge_resume"],
|
|
51
56
|
"spawn-lifecycle": ["relay_spawn_agent", "relay_spawn_targets", "relay_my_agents", "relay_shutdown_agent", "relay_agent_action", "relay_agent_status", "relay_agent_crash_report", "relay_schedule_timer", "relay_list_timers", "relay_cancel_timer"],
|
|
52
57
|
plans: ["relay_plan_create", "relay_plan_add_node", "relay_plan_update_node", "relay_plan_add_edge", "relay_plan_remove_node", "relay_plan_remove_edge", "relay_plan_delete", "relay_plan_get"],
|
|
58
|
+
tasks: ["relay_task_create", "relay_task_get", "relay_task_list", "relay_task_update", "relay_task_set_status", "relay_task_set_stage", "relay_task_record_gate", "relay_task_add_dependency", "relay_task_remove_dependency", "relay_task_add_ref", "relay_task_remove_ref", "relay_task_assign_agent", "relay_task_history", "relay_task_memory_read", "relay_task_memory_replace_working_state", "relay_task_memory_append_ruled_out", "relay_task_memory_append_decision", "relay_task_dispatch", "relay_task_comment", "relay_task_close_bridge", "relay_release_readiness"],
|
|
53
59
|
teams: ["relay_team_get", "relay_team_set_brief", "relay_team_claim", "relay_team_dissolve"],
|
|
54
60
|
blackboard: ["relay_blackboard_write", "relay_blackboard_get", "relay_blackboard_read", "relay_blackboard_search", "relay_blackboard_retract"],
|
|
55
61
|
project: ["relay_project_list", "relay_project_get", "relay_project_add_root", "relay_project_write_entry", "relay_project_read", "relay_project_search", "relay_project_retract", "relay_project_add_edge", "relay_project_remove_edge", "relay_project_promote_entry", "relay_project_ingest_claudemd"],
|
|
@@ -167,6 +173,11 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
167
173
|
else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
|
|
168
174
|
else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
|
|
169
175
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
176
|
+
else if (name.startsWith("relay_task_memory_")) result = relayTaskMemoryTool(auth, name, args);
|
|
177
|
+
else if (name === "relay_task_dispatch") result = relayDispatchTool(auth, name, args);
|
|
178
|
+
else if (name === "relay_task_comment" || name === "relay_task_close_bridge") result = await relayTaskWeaveTool(auth, name, args);
|
|
179
|
+
else if (name === "relay_release_readiness") result = relayReleaseTool(name, args);
|
|
180
|
+
else if (name.startsWith("relay_task_")) result = relayTaskTool(auth, name, args);
|
|
170
181
|
else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
|
|
171
182
|
else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args); else if (name.startsWith("relay_memory_")) result = relayMemoryTool(auth, name, args); else if (name.startsWith("relay_issue_ref_")) result = relayIssueRefTool(auth, name, args); else if (name.startsWith("relay_issue_association_")) result = relayIssueAssociationTool(auth, name, args); else if (name.startsWith("relay_issue_activity_")) result = relayIssueActivityTool(auth, name, args); else if (name.startsWith("relay_issue_lifecycle_")) result = await relayIssueLifecycleTool(auth, name, args);
|
|
172
183
|
else if (name === "relay_subscribe" || name === "relay_unsubscribe" || name === "relay_list_subscriptions" || name === "relay_why_notified" || name === "relay_notification_preview") result = relayNotificationSubscriptionTool(auth, name, args);
|
package/src/mcp/tools-spawn.ts
CHANGED
|
@@ -37,6 +37,8 @@ export const SPAWN_TOOLS: ToolDefinition[] = [
|
|
|
37
37
|
effort: { type: "string", enum: VALID_EFFORTS },
|
|
38
38
|
approvalMode: { type: "string", enum: APPROVAL_MODES, description: "Permission posture for the worker. Defaults to YOUR OWN approval mode when omitted (a headless `guarded` worker wedges on the first approval prompt), so a coordinator running `open` spawns workers that can act autonomously. Pass an explicit value to narrow a child (e.g. `read-only` reviewer)." },
|
|
39
39
|
prompt: { type: "string", description: "Initial task/message delivered to the agent on launch — spawn and hand it its first instruction in one call (no separate follow-up message needed)." },
|
|
40
|
+
taskId: { type: "integer", minimum: 1, description: "#838 — spawn the worker bound to a durable Task. Relay hydrates the worker's FIRST-TURN context FROM the Task (intent + linked-issue spec/acceptance + recorded decisions + history + attribution), resolved server-side ONCE, so you do NOT hand-write a long brief and the worker does NOT read the issue tracker. Records the spawn on the Task for attribution. Pair with `steer` for the one-line actionable instruction. When model/effort are omitted, relay MAY resolve them from the Task's dispatch work-shape." },
|
|
41
|
+
steer: { type: "string", description: "#838 — the latest one-line steer: the actionable instruction layered over the hydrated Task context (used with `taskId`). Overrides the Task's stored latest steer for this spawn." },
|
|
40
42
|
expectReply: { type: "boolean", description: "Legacy compatibility flag. Spawned workers now report up automatically: each turn-final response is routed to the spawner by lineage, without a reply obligation or idle nag." },
|
|
41
43
|
systemPromptAppend: { type: "string" },
|
|
42
44
|
projectInstructions: { type: "object", description: "#409 — project instruction projection (default OFF). When set, relay composes a knowledge preamble from the spawn cwd's resolved project (own + inherited entries) and prepends it to systemPromptAppend, instead of relying on host CLAUDE.md. `policy: \"all\"` includes ingested host content; `\"vanilla\"` excludes it (manual+promoted only). Omit to keep current behavior.", properties: { policy: { type: "string", enum: PROJECT_INSTRUCTION_POLICY_NAMES } }, additionalProperties: false },
|
|
@@ -143,6 +145,8 @@ export async function relaySpawnAgent(auth: McpAuthContext, args: Record<string,
|
|
|
143
145
|
policyName: optionalString(args.policyName, "policyName", 120),
|
|
144
146
|
profile: optionalString(args.profile, "profile", 120),
|
|
145
147
|
prompt: optionalString(args.prompt, "prompt", 16_000),
|
|
148
|
+
taskId: optionalPositiveInt(args.taskId, "taskId"),
|
|
149
|
+
steer: optionalString(args.steer, "steer", 16_000),
|
|
146
150
|
expectReply: typeof args.expectReply === "boolean" ? args.expectReply : undefined,
|
|
147
151
|
systemPromptAppend: optionalString(args.systemPromptAppend, "systemPromptAppend", 64_000),
|
|
148
152
|
projectInstructions: parseProjectInstructions(args.projectInstructions, (m) => new ValidationError(m)),
|