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
package/src/db/migrations.ts
CHANGED
|
@@ -165,7 +165,7 @@ export function applyMigrations(): void {
|
|
|
165
165
|
getDb().run(`
|
|
166
166
|
CREATE TABLE IF NOT EXISTS issue_associations (
|
|
167
167
|
issue_ref_id TEXT NOT NULL REFERENCES issue_refs(id) ON DELETE CASCADE,
|
|
168
|
-
entity_type TEXT NOT NULL CHECK (entity_type IN ('team', 'plan-node', 'epic')),
|
|
168
|
+
entity_type TEXT NOT NULL CHECK (entity_type IN ('team', 'plan-node', 'epic', 'task')),
|
|
169
169
|
entity_id TEXT NOT NULL,
|
|
170
170
|
created_at INTEGER NOT NULL,
|
|
171
171
|
PRIMARY KEY (issue_ref_id, entity_type, entity_id)
|
|
@@ -265,7 +265,10 @@ export function applyMigrations(): void {
|
|
|
265
265
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_thread ON messages(thread_id)");
|
|
266
266
|
getDb().run("CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_idempotency ON messages(from_agent, idempotency_key) WHERE idempotency_key IS NOT NULL");
|
|
267
267
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_delivery_status ON messages(delivery_status)");
|
|
268
|
-
|
|
268
|
+
// (#846) composite replaces the old single-column idx_msg_resolved_to_agent; created
|
|
269
|
+
// here — AFTER the resolved_to_agent ALTER above — so existing DBs without the column
|
|
270
|
+
// don't crash (same hazard as idx_msg_from_kind_id / #483/#559).
|
|
271
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_resolved_to_id ON messages(resolved_to_agent, id)");
|
|
269
272
|
|
|
270
273
|
getDb().run(`
|
|
271
274
|
CREATE TABLE IF NOT EXISTS agent_continuations (
|
|
@@ -294,6 +297,20 @@ export function applyMigrations(): void {
|
|
|
294
297
|
ON continuation_archives(agent_id, generation, created_at DESC)
|
|
295
298
|
`);
|
|
296
299
|
ensureContinuationArchiveSearchSchema();
|
|
300
|
+
getDb().run(`
|
|
301
|
+
CREATE TABLE IF NOT EXISTS task_memories (
|
|
302
|
+
task_id INTEGER PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
|
303
|
+
objective TEXT NOT NULL,
|
|
304
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
305
|
+
ledger TEXT NOT NULL DEFAULT '[]',
|
|
306
|
+
decisions TEXT NOT NULL DEFAULT '[]',
|
|
307
|
+
working_state TEXT NOT NULL DEFAULT '',
|
|
308
|
+
archive_refs TEXT NOT NULL DEFAULT '[]',
|
|
309
|
+
budget TEXT NOT NULL,
|
|
310
|
+
created_at INTEGER NOT NULL,
|
|
311
|
+
updated_at INTEGER NOT NULL
|
|
312
|
+
)
|
|
313
|
+
`);
|
|
297
314
|
|
|
298
315
|
const tokenCols = getDb().query("PRAGMA table_info(tokens)").all() as any[];
|
|
299
316
|
const tokenColNames = tokenCols.map((c: any) => c.name);
|
|
@@ -1013,4 +1030,97 @@ export function applyMigrations(): void {
|
|
|
1013
1030
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_scheduled_timers_creator ON scheduled_timers(created_by, enabled, next_run_at)");
|
|
1014
1031
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_scheduled_timers_correlation ON scheduled_timers(correlation_id)");
|
|
1015
1032
|
|
|
1033
|
+
// #834 — grow `tasks` into the durable Task entity. Additive, kind-fenced columns:
|
|
1034
|
+
// existing rows default to kind='claim'/deliverable_stage='none', so every legacy
|
|
1035
|
+
// claim path is byte-for-byte unaffected. The claim-lease machinery filters kind='claim'.
|
|
1036
|
+
const durableTaskCols = (getDb().query("PRAGMA table_info(tasks)").all() as any[]).map((c: any) => c.name);
|
|
1037
|
+
const addTaskCol = (name: string, ddl: string) => {
|
|
1038
|
+
if (!durableTaskCols.includes(name)) getDb().run(`ALTER TABLE tasks ADD COLUMN ${ddl}`);
|
|
1039
|
+
};
|
|
1040
|
+
addTaskCol("kind", "kind TEXT NOT NULL DEFAULT 'claim'");
|
|
1041
|
+
addTaskCol("intent", "intent TEXT");
|
|
1042
|
+
addTaskCol("latest_steer", "latest_steer TEXT");
|
|
1043
|
+
addTaskCol("deliverable_stage", "deliverable_stage TEXT NOT NULL DEFAULT 'none'");
|
|
1044
|
+
addTaskCol("blocked_by", "blocked_by TEXT");
|
|
1045
|
+
addTaskCol("branch", "branch TEXT");
|
|
1046
|
+
addTaskCol("commits", "commits TEXT");
|
|
1047
|
+
addTaskCol("done_when", "done_when TEXT");
|
|
1048
|
+
addTaskCol("team_id", "team_id TEXT");
|
|
1049
|
+
addTaskCol("created_by", "created_by TEXT");
|
|
1050
|
+
addTaskCol("owner_agent_id", "owner_agent_id TEXT");
|
|
1051
|
+
addTaskCol("dispatch", "dispatch TEXT");
|
|
1052
|
+
addTaskCol("reaped_at", "reaped_at INTEGER");
|
|
1053
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_kind ON tasks(kind, status, updated_at)");
|
|
1054
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_branch ON tasks(branch) WHERE branch IS NOT NULL");
|
|
1055
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id) WHERE team_id IS NOT NULL");
|
|
1056
|
+
|
|
1057
|
+
getDb().run(`
|
|
1058
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
1059
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1060
|
+
depends_on_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1061
|
+
created_at INTEGER NOT NULL,
|
|
1062
|
+
PRIMARY KEY (task_id, depends_on_task_id)
|
|
1063
|
+
)
|
|
1064
|
+
`);
|
|
1065
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_task_dependencies_depended ON task_dependencies(depends_on_task_id)");
|
|
1066
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_task_events_task_type ON task_events(task_id, type, id DESC)");
|
|
1067
|
+
|
|
1068
|
+
// #833 — comment-through idempotency outbox (reserve→post→finalize). Existing DBs gain
|
|
1069
|
+
// the table here; the UNIQUE claim is what makes the write-through concurrency/crash-safe.
|
|
1070
|
+
getDb().run(`
|
|
1071
|
+
CREATE TABLE IF NOT EXISTS task_comment_throughs (
|
|
1072
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1073
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1074
|
+
kind TEXT NOT NULL,
|
|
1075
|
+
idempotency_key TEXT NOT NULL,
|
|
1076
|
+
status TEXT NOT NULL,
|
|
1077
|
+
event_id INTEGER,
|
|
1078
|
+
created_at INTEGER NOT NULL,
|
|
1079
|
+
updated_at INTEGER NOT NULL,
|
|
1080
|
+
UNIQUE (task_id, kind, idempotency_key)
|
|
1081
|
+
)
|
|
1082
|
+
`);
|
|
1083
|
+
|
|
1084
|
+
// #834 — issue_associations.entity_type gained 'task'. The CHECK constraint is baked into
|
|
1085
|
+
// the table SQL, so a TS-only enum change would fail at runtime; rebuild the table (SQLite
|
|
1086
|
+
// can't ALTER a CHECK). Guarded on the absence of 'task' in the stored DDL so it runs once.
|
|
1087
|
+
const issueAssocSql = (getDb().query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'issue_associations'").get() as { sql?: string } | undefined)?.sql;
|
|
1088
|
+
if (issueAssocSql && !issueAssocSql.includes("'task'")) {
|
|
1089
|
+
getDb().transaction(() => {
|
|
1090
|
+
getDb().run("ALTER TABLE issue_associations RENAME TO issue_associations_old");
|
|
1091
|
+
getDb().run(`
|
|
1092
|
+
CREATE TABLE issue_associations (
|
|
1093
|
+
issue_ref_id TEXT NOT NULL REFERENCES issue_refs(id) ON DELETE CASCADE,
|
|
1094
|
+
entity_type TEXT NOT NULL CHECK (entity_type IN ('team', 'plan-node', 'epic', 'task')),
|
|
1095
|
+
entity_id TEXT NOT NULL,
|
|
1096
|
+
created_at INTEGER NOT NULL,
|
|
1097
|
+
PRIMARY KEY (issue_ref_id, entity_type, entity_id)
|
|
1098
|
+
)
|
|
1099
|
+
`);
|
|
1100
|
+
getDb().run(`
|
|
1101
|
+
INSERT INTO issue_associations (issue_ref_id, entity_type, entity_id, created_at)
|
|
1102
|
+
SELECT issue_ref_id, entity_type, entity_id, created_at FROM issue_associations_old
|
|
1103
|
+
`);
|
|
1104
|
+
getDb().run("DROP TABLE issue_associations_old");
|
|
1105
|
+
})();
|
|
1106
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_issue_associations_entity ON issue_associations(entity_type, entity_id, created_at DESC)");
|
|
1107
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_issue_associations_issue ON issue_associations(issue_ref_id, created_at DESC)");
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// #846 — drop the superseded single-column indexes so the planner is forced to
|
|
1111
|
+
// choose the composite replacements (idx_msg_to_id / idx_msg_resolved_to_id).
|
|
1112
|
+
// When both old and new exist the planner may prefer the old single-column index,
|
|
1113
|
+
// silently defeating the ORDER BY id optimisation. DROP INDEX IF EXISTS is
|
|
1114
|
+
// idempotent and safe on any populated DB. Guard ANALYZE on the presence of the
|
|
1115
|
+
// old idx_msg_to: it exists on pre-#846 DBs and is absent once this block has run,
|
|
1116
|
+
// so ANALYZE executes exactly once per DB (on first boot with the new code).
|
|
1117
|
+
// Fresh DBs never create idx_msg_to (removed from base schema), so the guard is
|
|
1118
|
+
// false and ANALYZE is skipped (the first-boot `if (!hasStats)` path in initDb
|
|
1119
|
+
// covers fresh DBs instead).
|
|
1120
|
+
const hasOldToIdx = !!getDb()
|
|
1121
|
+
.query("SELECT 1 FROM sqlite_master WHERE type='index' AND name='idx_msg_to'")
|
|
1122
|
+
.get();
|
|
1123
|
+
getDb().run("DROP INDEX IF EXISTS idx_msg_to");
|
|
1124
|
+
getDb().run("DROP INDEX IF EXISTS idx_msg_resolved_to_agent");
|
|
1125
|
+
if (hasOldToIdx) getDb().run("ANALYZE messages");
|
|
1016
1126
|
}
|
|
@@ -48,7 +48,7 @@ interface NotificationSubscriptionRow {
|
|
|
48
48
|
created_at: number;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const SCOPE_KEYS = ["author", "agentId", "parent", "teamId", "planId", "repoRoot", "workspaceId", "paths"] as const;
|
|
51
|
+
const SCOPE_KEYS = ["author", "agentId", "parent", "teamId", "planId", "taskId", "transition", "landKey", "repoRoot", "workspaceId", "paths"] as const;
|
|
52
52
|
|
|
53
53
|
export function addSubscription(input: {
|
|
54
54
|
agentId: string;
|
|
@@ -67,6 +67,12 @@ export function seedTemplates(templates: SeedTemplate[]): void {
|
|
|
67
67
|
slug, category, title, description, template, default_template, variables, updated_at, created_at
|
|
68
68
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
69
69
|
`);
|
|
70
|
+
const current = getDb().query("SELECT * FROM prompt_templates WHERE slug = ?");
|
|
71
|
+
const update = getDb().query(`
|
|
72
|
+
UPDATE prompt_templates
|
|
73
|
+
SET category = ?, title = ?, description = ?, template = ?, default_template = ?, variables = ?, updated_at = ?
|
|
74
|
+
WHERE slug = ?
|
|
75
|
+
`);
|
|
70
76
|
const tx = getDb().transaction((items: SeedTemplate[]) => {
|
|
71
77
|
for (const item of items) {
|
|
72
78
|
insert.run(
|
|
@@ -80,6 +86,19 @@ export function seedTemplates(templates: SeedTemplate[]): void {
|
|
|
80
86
|
now,
|
|
81
87
|
now,
|
|
82
88
|
);
|
|
89
|
+
const row = current.get(item.slug) as PromptTemplateRow | null;
|
|
90
|
+
if (!row) continue;
|
|
91
|
+
const template = row.template === row.default_template ? item.template : row.template;
|
|
92
|
+
update.run(
|
|
93
|
+
item.category,
|
|
94
|
+
item.title,
|
|
95
|
+
item.description ?? null,
|
|
96
|
+
template,
|
|
97
|
+
item.defaultTemplate,
|
|
98
|
+
JSON.stringify(item.variables),
|
|
99
|
+
now,
|
|
100
|
+
item.slug,
|
|
101
|
+
);
|
|
83
102
|
}
|
|
84
103
|
});
|
|
85
104
|
tx(templates);
|
package/src/db/schema.ts
CHANGED
|
@@ -157,7 +157,13 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
157
157
|
occurred_at INTEGER
|
|
158
158
|
);
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
-- (#846) composite covers ORDER BY id DESC for the to_target branch of
|
|
161
|
+
-- listAgentChatHistory without a filesort. The composite is a leftmost-prefix
|
|
162
|
+
-- superset of the old idx_msg_to single-column index, so all WHERE to_target=?
|
|
163
|
+
-- queries (delivery, polling, etc.) are served identically.
|
|
164
|
+
-- idx_msg_resolved_to_id is created in applyMigrations() AFTER the
|
|
165
|
+
-- resolved_to_agent ALTER — never inline here (same hazard as #483/#559).
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_msg_to_id ON messages(to_target, id);
|
|
161
167
|
CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(created_at);
|
|
162
168
|
CREATE INDEX IF NOT EXISTS idx_msg_channel ON messages(channel);
|
|
163
169
|
-- (reply_to, from_agent) powers the reply-obligation NOT EXISTS subquery in
|
|
@@ -395,6 +401,23 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
395
401
|
CREATE INDEX IF NOT EXISTS idx_continuation_archives_agent_generation
|
|
396
402
|
ON continuation_archives(agent_id, generation, created_at DESC);
|
|
397
403
|
|
|
404
|
+
-- #832 — Task-bound memory is the task-local slice of the #815 scoped-knowledge
|
|
405
|
+
-- chain. It mirrors the self-resume continuation envelope shape now; when #815
|
|
406
|
+
-- lands, this table becomes the task/session scope adapter instead of a parallel
|
|
407
|
+
-- memory system.
|
|
408
|
+
CREATE TABLE IF NOT EXISTS task_memories (
|
|
409
|
+
task_id INTEGER PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
|
410
|
+
objective TEXT NOT NULL,
|
|
411
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
412
|
+
ledger TEXT NOT NULL DEFAULT '[]',
|
|
413
|
+
decisions TEXT NOT NULL DEFAULT '[]',
|
|
414
|
+
working_state TEXT NOT NULL DEFAULT '',
|
|
415
|
+
archive_refs TEXT NOT NULL DEFAULT '[]',
|
|
416
|
+
budget TEXT NOT NULL,
|
|
417
|
+
created_at INTEGER NOT NULL,
|
|
418
|
+
updated_at INTEGER NOT NULL
|
|
419
|
+
);
|
|
420
|
+
|
|
398
421
|
CREATE TABLE IF NOT EXISTS memories (
|
|
399
422
|
id TEXT PRIMARY KEY,
|
|
400
423
|
type TEXT NOT NULL,
|
|
@@ -619,13 +642,58 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
619
642
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
620
643
|
created_at INTEGER NOT NULL,
|
|
621
644
|
updated_at INTEGER NOT NULL,
|
|
622
|
-
last_seen_at INTEGER NOT NULL
|
|
645
|
+
last_seen_at INTEGER NOT NULL,
|
|
646
|
+
-- #834 durable-Task facet. kind='claim' (default) is the historical claimable
|
|
647
|
+
-- work-item lane; kind='deliverable' is the durable coordination Task. The claim-
|
|
648
|
+
-- lease machinery is fenced to kind='claim' (see claimTaskWhere in task-core.ts).
|
|
649
|
+
kind TEXT NOT NULL DEFAULT 'claim',
|
|
650
|
+
intent TEXT,
|
|
651
|
+
latest_steer TEXT,
|
|
652
|
+
deliverable_stage TEXT NOT NULL DEFAULT 'none',
|
|
653
|
+
blocked_by TEXT,
|
|
654
|
+
branch TEXT,
|
|
655
|
+
commits TEXT,
|
|
656
|
+
done_when TEXT,
|
|
657
|
+
team_id TEXT,
|
|
658
|
+
created_by TEXT,
|
|
659
|
+
owner_agent_id TEXT,
|
|
660
|
+
dispatch TEXT,
|
|
661
|
+
reaped_at INTEGER
|
|
623
662
|
);
|
|
624
663
|
|
|
625
664
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_source_dedupe ON tasks(source, dedupe_key) WHERE dedupe_key IS NOT NULL AND status NOT IN ('done', 'failed', 'canceled');
|
|
626
665
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
627
666
|
CREATE INDEX IF NOT EXISTS idx_tasks_target ON tasks(target);
|
|
628
667
|
CREATE INDEX IF NOT EXISTS idx_tasks_updated ON tasks(updated_at);
|
|
668
|
+
-- idx_tasks_kind / idx_tasks_branch / idx_tasks_team reference the #834 columns and are
|
|
669
|
+
-- created in applyMigrations() AFTER the ALTERs — never inline here: an existing pre-#834
|
|
670
|
+
-- tasks table makes CREATE TABLE IF NOT EXISTS a no-op, so an inline index over a column
|
|
671
|
+
-- it doesn't yet have crashes initDb (same hazard as idx_projects_scope_anchor above).
|
|
672
|
+
|
|
673
|
+
-- #834 task→task dependency DAG (the edges the plan DAG collapses into; #835).
|
|
674
|
+
-- Cycle-rejected transactionally on insert (src/db/task-entity.ts).
|
|
675
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
676
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
677
|
+
depends_on_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
678
|
+
created_at INTEGER NOT NULL,
|
|
679
|
+
PRIMARY KEY (task_id, depends_on_task_id)
|
|
680
|
+
);
|
|
681
|
+
CREATE INDEX IF NOT EXISTS idx_task_dependencies_depended ON task_dependencies(depends_on_task_id);
|
|
682
|
+
|
|
683
|
+
-- #833 comment-through idempotency outbox: reserve→post→finalize so a Task comment
|
|
684
|
+
-- written THROUGH to GitHub is concurrency- and crash-safe (UNIQUE claim before the
|
|
685
|
+
-- adapter append; receipt only after it succeeds). Logic in src/db/task-comment-through.ts.
|
|
686
|
+
CREATE TABLE IF NOT EXISTS task_comment_throughs (
|
|
687
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
688
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
689
|
+
kind TEXT NOT NULL,
|
|
690
|
+
idempotency_key TEXT NOT NULL,
|
|
691
|
+
status TEXT NOT NULL,
|
|
692
|
+
event_id INTEGER,
|
|
693
|
+
created_at INTEGER NOT NULL,
|
|
694
|
+
updated_at INTEGER NOT NULL,
|
|
695
|
+
UNIQUE (task_id, kind, idempotency_key)
|
|
696
|
+
);
|
|
629
697
|
|
|
630
698
|
CREATE TABLE IF NOT EXISTS automations (
|
|
631
699
|
id TEXT PRIMARY KEY,
|
|
@@ -682,6 +750,10 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
682
750
|
created_at INTEGER NOT NULL
|
|
683
751
|
);
|
|
684
752
|
|
|
753
|
+
-- #834 — supports the deterministic latest-per-gate projection (type LIKE 'gate:%')
|
|
754
|
+
-- and append-history reads. The audit ledger stays append-only; this is just the index.
|
|
755
|
+
CREATE INDEX IF NOT EXISTS idx_task_events_task_type ON task_events(task_id, type, id DESC);
|
|
756
|
+
|
|
685
757
|
CREATE TABLE IF NOT EXISTS task_callback_deliveries (
|
|
686
758
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
687
759
|
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
package/src/db/stats.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { listAgents } from "./agents.ts";
|
|
|
18
18
|
import { listChannels } from "./channels.ts";
|
|
19
19
|
import { countExpiredUndeliveredMessages } from "./delivery.ts";
|
|
20
20
|
import { messageTaskClosed } from "./claim-sql.ts";
|
|
21
|
+
import { claimTaskWhere } from "./task-core.ts";
|
|
21
22
|
import { getDb } from "./connection.ts";
|
|
22
23
|
import type {
|
|
23
24
|
AgentCard,
|
|
@@ -428,7 +429,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
428
429
|
});
|
|
429
430
|
|
|
430
431
|
const expiredTaskClaims = (getDb()
|
|
431
|
-
.query(
|
|
432
|
+
.query(`SELECT COUNT(*) as c FROM tasks 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')`)
|
|
432
433
|
.get(now) as any).c as number;
|
|
433
434
|
checks.push({
|
|
434
435
|
name: "expired-task-claims",
|
|
@@ -442,7 +443,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
442
443
|
SELECT COUNT(*) as c
|
|
443
444
|
FROM tasks t
|
|
444
445
|
JOIN agents a ON a.id = t.claimed_by
|
|
445
|
-
WHERE t.status IN ('claimed', 'in_progress', 'blocked') AND a.status = 'offline'
|
|
446
|
+
WHERE ${claimTaskWhere("t.")} AND t.status IN ('claimed', 'in_progress', 'blocked') AND a.status = 'offline'
|
|
446
447
|
`)
|
|
447
448
|
.get() as any).c as number;
|
|
448
449
|
checks.push({
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// #833 — the comment-through idempotency outbox. The Task↔issue weave writes discussion
|
|
2
|
+
// THROUGH to GitHub (the backing store); to keep "Task is a lens, GitHub is the store"
|
|
3
|
+
// honest, a write-through must be reserve→post→finalize, never scan→post→insert:
|
|
4
|
+
// - a UNIQUE(task_id, kind, idempotency_key) claim is taken TRANSACTIONALLY before the
|
|
5
|
+
// awaited GitHub append, so two concurrent same-key requests can't both post.
|
|
6
|
+
// - a receipt (status='posted') is only written AFTER the adapter succeeds, so a failed
|
|
7
|
+
// GitHub post never leaves a local "posted" record that a retry would dedupe against.
|
|
8
|
+
// A caught failure flips the row to 'failed' (reclaimable — a retry re-posts). A reservation
|
|
9
|
+
// abandoned past RESERVATION_TTL_MS (crash mid-flight) is reclaimable too; within the TTL a
|
|
10
|
+
// concurrent attempt sees 'in_flight' and stands down (at-most-once in the ambiguous window).
|
|
11
|
+
import { getDb, ValidationError } from "./connection.ts";
|
|
12
|
+
|
|
13
|
+
// A crashed reservation older than this is treated as abandoned and reclaimable. Inside the
|
|
14
|
+
// window, a same-key attempt stands down rather than risk a duplicate GitHub comment.
|
|
15
|
+
export const RESERVATION_TTL_MS = 120_000;
|
|
16
|
+
|
|
17
|
+
export type TaskCommentThroughKind = "discussion" | "milestone";
|
|
18
|
+
export type TaskCommentThroughStatus = "reserved" | "posted" | "failed";
|
|
19
|
+
|
|
20
|
+
export interface TaskCommentThroughRecord {
|
|
21
|
+
id: number;
|
|
22
|
+
taskId: number;
|
|
23
|
+
kind: TaskCommentThroughKind;
|
|
24
|
+
idempotencyKey: string;
|
|
25
|
+
status: TaskCommentThroughStatus;
|
|
26
|
+
eventId?: number;
|
|
27
|
+
createdAt: number;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TaskCommentThroughRow {
|
|
32
|
+
id: number;
|
|
33
|
+
task_id: number;
|
|
34
|
+
kind: TaskCommentThroughKind;
|
|
35
|
+
idempotency_key: string;
|
|
36
|
+
status: TaskCommentThroughStatus;
|
|
37
|
+
event_id: number | null;
|
|
38
|
+
created_at: number;
|
|
39
|
+
updated_at: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rowTo(row: TaskCommentThroughRow): TaskCommentThroughRecord {
|
|
43
|
+
return {
|
|
44
|
+
id: row.id,
|
|
45
|
+
taskId: row.task_id,
|
|
46
|
+
kind: row.kind,
|
|
47
|
+
idempotencyKey: row.idempotency_key,
|
|
48
|
+
status: row.status,
|
|
49
|
+
...(row.event_id !== null ? { eventId: row.event_id } : {}),
|
|
50
|
+
createdAt: row.created_at,
|
|
51
|
+
updatedAt: row.updated_at,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getRow(taskId: number, kind: TaskCommentThroughKind, key: string): TaskCommentThroughRow | undefined {
|
|
56
|
+
return getDb()
|
|
57
|
+
.query("SELECT * FROM task_comment_throughs WHERE task_id = ? AND kind = ? AND idempotency_key = ?")
|
|
58
|
+
.get(taskId, kind, key) as TaskCommentThroughRow | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// reserved → caller holds the claim; proceed to post, then finalize/fail. `reconcileNeeded`
|
|
62
|
+
// is true ONLY when a stale `reserved` row was reclaimed — that case is ambiguous
|
|
63
|
+
// (the prior attempt may have posted to GitHub then crashed before finalize), so
|
|
64
|
+
// the caller MUST reconcile against the issue's comments before re-posting. A
|
|
65
|
+
// `failed` reclaim or a fresh reservation is unambiguous (reconcileNeeded=false).
|
|
66
|
+
// in_flight → another attempt holds a fresh claim; caller must NOT post (dedupe).
|
|
67
|
+
// duplicate → already posted; caller returns the prior receipt.
|
|
68
|
+
export type ReserveOutcome =
|
|
69
|
+
| { outcome: "reserved"; record: TaskCommentThroughRecord; reconcileNeeded: boolean }
|
|
70
|
+
| { outcome: "in_flight"; record: TaskCommentThroughRecord }
|
|
71
|
+
| { outcome: "duplicate"; record: TaskCommentThroughRecord };
|
|
72
|
+
|
|
73
|
+
// Transactional claim. Runs check+insert/update inside a single SQLite write transaction
|
|
74
|
+
// (single-writer ⇒ serialized), so concurrent callers can never both come back 'reserved'.
|
|
75
|
+
export function reserveCommentThrough(
|
|
76
|
+
taskId: number,
|
|
77
|
+
kind: TaskCommentThroughKind,
|
|
78
|
+
idempotencyKey: string,
|
|
79
|
+
now: number,
|
|
80
|
+
): ReserveOutcome {
|
|
81
|
+
const key = idempotencyKey.trim();
|
|
82
|
+
if (!key) throw new ValidationError("idempotencyKey required to reserve a comment-through");
|
|
83
|
+
return getDb().transaction((): ReserveOutcome => {
|
|
84
|
+
const existing = getRow(taskId, kind, key);
|
|
85
|
+
if (existing) {
|
|
86
|
+
if (existing.status === "posted") return { outcome: "duplicate", record: rowTo(existing) };
|
|
87
|
+
const staleReserved = existing.status === "reserved" && now - existing.updated_at >= RESERVATION_TTL_MS;
|
|
88
|
+
const reclaimable = existing.status === "failed" || staleReserved;
|
|
89
|
+
if (!reclaimable) return { outcome: "in_flight", record: rowTo(existing) };
|
|
90
|
+
getDb().query("UPDATE task_comment_throughs SET status = 'reserved', event_id = NULL, updated_at = ? WHERE id = ?").run(now, existing.id);
|
|
91
|
+
// Only a stale-`reserved` reclaim is ambiguous about whether GitHub already got the post.
|
|
92
|
+
return { outcome: "reserved", record: rowTo(getRow(taskId, kind, key)!), reconcileNeeded: staleReserved };
|
|
93
|
+
}
|
|
94
|
+
getDb()
|
|
95
|
+
.query("INSERT INTO task_comment_throughs (task_id, kind, idempotency_key, status, created_at, updated_at) VALUES (?, ?, ?, 'reserved', ?, ?)")
|
|
96
|
+
.run(taskId, kind, key, now, now);
|
|
97
|
+
return { outcome: "reserved", record: rowTo(getRow(taskId, kind, key)!), reconcileNeeded: false };
|
|
98
|
+
})();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function finalizeCommentThrough(id: number, eventId: number, now: number): void {
|
|
102
|
+
getDb().query("UPDATE task_comment_throughs SET status = 'posted', event_id = ?, updated_at = ? WHERE id = ?").run(eventId, now, id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function failCommentThrough(id: number, now: number): void {
|
|
106
|
+
getDb().query("UPDATE task_comment_throughs SET status = 'failed', event_id = NULL, updated_at = ? WHERE id = ?").run(now, id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getCommentThrough(taskId: number, kind: TaskCommentThroughKind, key: string): TaskCommentThroughRecord | null {
|
|
110
|
+
const row = getRow(taskId, kind, key.trim());
|
|
111
|
+
return row ? rowTo(row) : null;
|
|
112
|
+
}
|
package/src/db/task-core.ts
CHANGED
|
@@ -4,6 +4,19 @@ import type { TaskEvent } from "../types";
|
|
|
4
4
|
|
|
5
5
|
export const TASK_SELECT = "SELECT * FROM tasks";
|
|
6
6
|
|
|
7
|
+
// #834 — the claim/deliverable partition is a REAL fence, not a convention. Every claim,
|
|
8
|
+
// lease, orphan, on-demand-settle and health query that touches `claimed_by` MUST be scoped
|
|
9
|
+
// to kind='claim' so the claim-lease machinery can never mutate a durable deliverable Task
|
|
10
|
+
// (e.g. release/orphan one whose owner_agent_id is set). Route ALL such SQL through this.
|
|
11
|
+
export const CLAIM_TASK_KIND = "claim";
|
|
12
|
+
export const DELIVERABLE_TASK_KIND = "deliverable";
|
|
13
|
+
|
|
14
|
+
/** SQL predicate fencing a query to claim-lane tasks. `prefix` is an optional table
|
|
15
|
+
* alias with trailing dot (e.g. `claimTaskWhere("t.")` → `t.kind = 'claim'`). */
|
|
16
|
+
export function claimTaskWhere(prefix = ""): string {
|
|
17
|
+
return `${prefix}kind = '${CLAIM_TASK_KIND}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" | "taskId" | "createdAt">>, now: number): TaskEvent {
|
|
8
21
|
const result = getDb().query(`
|
|
9
22
|
INSERT INTO task_events (task_id, source, type, severity, title, body, metadata, created_at)
|
|
@@ -12,7 +25,7 @@ export function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent,
|
|
|
12
25
|
return getTaskEvent(Number(result.lastInsertRowid))!;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
|
-
function getTaskEvent(id: number): TaskEvent | null {
|
|
28
|
+
export function getTaskEvent(id: number): TaskEvent | null {
|
|
16
29
|
const row = getDb().query("SELECT * FROM task_events WHERE id = ?").get(id) as any;
|
|
17
30
|
return row ? rowToTaskEvent(row) : null;
|
|
18
31
|
}
|