agent-relay-server 0.120.1 → 0.120.3
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 +1 -1
- package/package.json +3 -3
- package/public/assets/MessageBubble-Cqi3XsZ7.js +2 -0
- package/public/assets/{MessageBubble-jpshgh3K.js.map → MessageBubble-Cqi3XsZ7.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CjVG-Jsq.js → PlanGraphPanel-D6Um05oW.js} +2 -2
- package/public/assets/{PlanGraphPanel-CjVG-Jsq.js.map → PlanGraphPanel-D6Um05oW.js.map} +1 -1
- package/public/assets/{activity-DLTFF8Dz.js → activity-B0LsQHEz.js} +2 -2
- package/public/assets/{activity-DLTFF8Dz.js.map → activity-B0LsQHEz.js.map} +1 -1
- package/public/assets/{agent-profiles-BcCLIrON.js → agent-profiles-O--pe5Ic.js} +2 -2
- package/public/assets/{agent-profiles-BcCLIrON.js.map → agent-profiles-O--pe5Ic.js.map} +1 -1
- package/public/assets/{agent-type-icon-BEz393ra.js → agent-type-icon-CeEoMouY.js} +2 -2
- package/public/assets/{agent-type-icon-BEz393ra.js.map → agent-type-icon-CeEoMouY.js.map} +1 -1
- package/public/assets/agents-Ii6XlVwR.js +2 -0
- package/public/assets/{agents-QemvuxKO.js.map → agents-Ii6XlVwR.js.map} +1 -1
- package/public/assets/{analytics-DMLoMwta.js → analytics-BCzdYWRT.js} +2 -2
- package/public/assets/{analytics-DMLoMwta.js.map → analytics-BCzdYWRT.js.map} +1 -1
- package/public/assets/{automation-DACSOffp.js → automation-Crr3Xzui.js} +2 -2
- package/public/assets/{automation-DACSOffp.js.map → automation-Crr3Xzui.js.map} +1 -1
- package/public/assets/{badge-BhUZ1cAb.js → badge-DCIT3Irj.js} +2 -2
- package/public/assets/{badge-BhUZ1cAb.js.map → badge-DCIT3Irj.js.map} +1 -1
- package/public/assets/{branch-state-badge-BYE2U_y4.js → branch-state-badge-Bhw2rloy.js} +2 -2
- package/public/assets/{branch-state-badge-BYE2U_y4.js.map → branch-state-badge-Bhw2rloy.js.map} +1 -1
- package/public/assets/{button-C-V4oij1.js → button-CzdTkRIf.js} +2 -2
- package/public/assets/{button-C-V4oij1.js.map → button-CzdTkRIf.js.map} +1 -1
- package/public/assets/{card-Br1JLjTH.js → card-C3BGuVC6.js} +2 -2
- package/public/assets/{card-Br1JLjTH.js.map → card-C3BGuVC6.js.map} +1 -1
- package/public/assets/{channels-3SlDQL3j.js → channels-CKs9rvsn.js} +2 -2
- package/public/assets/{channels-3SlDQL3j.js.map → channels-CKs9rvsn.js.map} +1 -1
- package/public/assets/chat-BCwEUj5R.js +2 -0
- package/public/assets/chat-BCwEUj5R.js.map +1 -0
- package/public/assets/{connectors-CrEW591Q.js → connectors-D_kQR_vw.js} +2 -2
- package/public/assets/{connectors-CrEW591Q.js.map → connectors-D_kQR_vw.js.map} +1 -1
- package/public/assets/{copy-button-BStzXgtd.js → copy-button-B3o1jpLu.js} +2 -2
- package/public/assets/{copy-button-BStzXgtd.js.map → copy-button-B3o1jpLu.js.map} +1 -1
- package/public/assets/{dist-ChDX_t12.js → dist-B-T7lg0a.js} +2 -2
- package/public/assets/{dist-ChDX_t12.js.map → dist-B-T7lg0a.js.map} +1 -1
- package/public/assets/{dist-DV83j9tm.js → dist-B3e4B9pg.js} +2 -2
- package/public/assets/{dist-DV83j9tm.js.map → dist-B3e4B9pg.js.map} +1 -1
- package/public/assets/{dist-y03IwOrd.js → dist-B6woQ9Tb.js} +2 -2
- package/public/assets/{dist-y03IwOrd.js.map → dist-B6woQ9Tb.js.map} +1 -1
- package/public/assets/{dist-CQ0Z-TDi.js → dist-B9G83DXv.js} +2 -2
- package/public/assets/{dist-CQ0Z-TDi.js.map → dist-B9G83DXv.js.map} +1 -1
- package/public/assets/{dist-Cs8X0P1d.js → dist-BIjlN-xB.js} +2 -2
- package/public/assets/{dist-Cs8X0P1d.js.map → dist-BIjlN-xB.js.map} +1 -1
- package/public/assets/{dist-DyNJNwpi.js → dist-BInckSJC.js} +2 -2
- package/public/assets/{dist-DyNJNwpi.js.map → dist-BInckSJC.js.map} +1 -1
- package/public/assets/{dist-CmFeEXMe.js → dist-BlzVGRhh.js} +2 -2
- package/public/assets/{dist-CmFeEXMe.js.map → dist-BlzVGRhh.js.map} +1 -1
- package/public/assets/{dist-CDZZxxJq.js → dist-C7aTlosb.js} +2 -2
- package/public/assets/{dist-CDZZxxJq.js.map → dist-C7aTlosb.js.map} +1 -1
- package/public/assets/{dist-D2jvo5oA.js → dist-DGfp5dLz.js} +2 -2
- package/public/assets/{dist-D2jvo5oA.js.map → dist-DGfp5dLz.js.map} +1 -1
- package/public/assets/{dist-mm7c7nBt.js → dist-DT-OV6sQ.js} +2 -2
- package/public/assets/{dist-mm7c7nBt.js.map → dist-DT-OV6sQ.js.map} +1 -1
- package/public/assets/{dist-CBMWGhWc.js → dist-OUPTEP6q.js} +2 -2
- package/public/assets/{dist-CBMWGhWc.js.map → dist-OUPTEP6q.js.map} +1 -1
- package/public/assets/{es2015-VnZ2kVIz.js → es2015-CnKXYKpf.js} +2 -2
- package/public/assets/{es2015-VnZ2kVIz.js.map → es2015-CnKXYKpf.js.map} +1 -1
- package/public/assets/formatted-body-impl-CSRGuMcM.js +3 -0
- package/public/assets/formatted-body-impl-CSRGuMcM.js.map +1 -0
- package/public/assets/formatted-body-q_qzt1L-.js +3 -0
- package/public/assets/{formatted-body-Dg9R3RD7.js.map → formatted-body-q_qzt1L-.js.map} +1 -1
- package/public/assets/{index-D_fNJnsI.js → index-Ceiwd02X.js} +10 -10
- package/public/assets/index-Ceiwd02X.js.map +1 -0
- package/public/assets/{input-CtLT7WXi.js → input-B6Q0NWCS.js} +2 -2
- package/public/assets/{input-CtLT7WXi.js.map → input-B6Q0NWCS.js.map} +1 -1
- package/public/assets/{insights-D48n2ojF.js → insights-DUhXqZTZ.js} +2 -2
- package/public/assets/{insights-D48n2ojF.js.map → insights-DUhXqZTZ.js.map} +1 -1
- package/public/assets/{integrations-B-bdliwU.js → integrations-lbg5CjtE.js} +2 -2
- package/public/assets/{integrations-B-bdliwU.js.map → integrations-lbg5CjtE.js.map} +1 -1
- package/public/assets/{lib-ByLQqcD1.js → lib-BHsWPMKB.js} +2 -2
- package/public/assets/{lib-ByLQqcD1.js.map → lib-BHsWPMKB.js.map} +1 -1
- package/public/assets/lucide-react-DuhlF2Sj.js +10 -0
- package/public/assets/lucide-react-DuhlF2Sj.js.map +1 -0
- package/public/assets/{maintenance-LQ_U3Nqy.js → maintenance-CQgneSVf.js} +2 -2
- package/public/assets/{maintenance-LQ_U3Nqy.js.map → maintenance-CQgneSVf.js.map} +1 -1
- package/public/assets/{managed-agents-Cfh8FvY8.js → managed-agents-DsnYf6fm.js} +4 -4
- package/public/assets/{managed-agents-Cfh8FvY8.js.map → managed-agents-DsnYf6fm.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BSa7zYx1.js → markdown-preview-impl-C1ZtGJp6.js} +2 -2
- package/public/assets/{markdown-preview-impl-BSa7zYx1.js.map → markdown-preview-impl-C1ZtGJp6.js.map} +1 -1
- package/public/assets/{memory-Dr1iYKkC.js → memory-SSLUTOI2.js} +2 -2
- package/public/assets/{memory-Dr1iYKkC.js.map → memory-SSLUTOI2.js.map} +1 -1
- package/public/assets/messages-BY26OP4K.js +2 -0
- package/public/assets/{messages-DRKulS-2.js.map → messages-BY26OP4K.js.map} +1 -1
- package/public/assets/{orchestrators-BguyeViG.js → orchestrators-OY7yPzVI.js} +2 -2
- package/public/assets/{orchestrators-BguyeViG.js.map → orchestrators-OY7yPzVI.js.map} +1 -1
- package/public/assets/overview-Dj0u1tPG.js +2 -0
- package/public/assets/{overview-Dbq2lr7Y.js.map → overview-Dj0u1tPG.js.map} +1 -1
- package/public/assets/{pairs-DD7i2scF.js → pairs-CrUR50ou.js} +2 -2
- package/public/assets/{pairs-DD7i2scF.js.map → pairs-CrUR50ou.js.map} +1 -1
- package/public/assets/{projects-DEn85LQb.js → projects-B_AtxHtH.js} +2 -2
- package/public/assets/{projects-DEn85LQb.js.map → projects-B_AtxHtH.js.map} +1 -1
- package/public/assets/{prompt-settings-DwgrPKY3.js → prompt-settings-CPtYSban.js} +2 -2
- package/public/assets/{prompt-settings-DwgrPKY3.js.map → prompt-settings-CPtYSban.js.map} +1 -1
- package/public/assets/{react-dom-cLkVFBsS.js → react-dom-DWBmcawP.js} +2 -2
- package/public/assets/{react-dom-cLkVFBsS.js.map → react-dom-DWBmcawP.js.map} +1 -1
- package/public/assets/{registry-DCR3xFpe.js → registry-BgUALWn5.js} +2 -2
- package/public/assets/{registry-DCR3xFpe.js.map → registry-BgUALWn5.js.map} +1 -1
- package/public/assets/security-5lHfGk7g.js +3 -0
- package/public/assets/security-5lHfGk7g.js.map +1 -0
- package/public/assets/{select-CpyCRBK3.js → select-Cgx4QSLj.js} +2 -2
- package/public/assets/{select-CpyCRBK3.js.map → select-Cgx4QSLj.js.map} +1 -1
- package/public/assets/settings-DFGgxyun.js +6 -0
- package/public/assets/{settings-DYNFGC2-.js.map → settings-DFGgxyun.js.map} +1 -1
- package/public/assets/store-BM4zM89B.js +9 -0
- package/public/assets/store-BM4zM89B.js.map +1 -0
- package/public/assets/{task-chip-kxk4eNm1.js → task-chip-DXfvXBDx.js} +2 -2
- package/public/assets/{task-chip-kxk4eNm1.js.map → task-chip-DXfvXBDx.js.map} +1 -1
- package/public/assets/{tasks-rdy9Nsbf.js → tasks-BvH5rluu.js} +2 -2
- package/public/assets/{tasks-rdy9Nsbf.js.map → tasks-BvH5rluu.js.map} +1 -1
- package/public/assets/{teams-DHloBagJ.js → teams-VZOIOdoE.js} +2 -2
- package/public/assets/{teams-DHloBagJ.js.map → teams-VZOIOdoE.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DWTuNejR.js → terminal-viewer-impl-DYY3Wmoe.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DWTuNejR.js.map → terminal-viewer-impl-DYY3Wmoe.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{use-keyboard-viewport-B06XtRUZ.js → use-keyboard-viewport-CP109w9y.js} +2 -2
- package/public/assets/{use-keyboard-viewport-B06XtRUZ.js.map → use-keyboard-viewport-CP109w9y.js.map} +1 -1
- package/public/assets/{user-avatar-DuwCFrER.js → user-avatar-Dw6mzWhv.js} +2 -2
- package/public/assets/{user-avatar-DuwCFrER.js.map → user-avatar-Dw6mzWhv.js.map} +1 -1
- package/public/assets/{work-queue-CShT7nTC.js → work-queue-B6Xk7QNe.js} +2 -2
- package/public/assets/{work-queue-CShT7nTC.js.map → work-queue-B6Xk7QNe.js.map} +1 -1
- package/public/assets/{workspaces-DjQ6EdrZ.js → workspaces-CPIzAHAe.js} +2 -2
- package/public/assets/{workspaces-DjQ6EdrZ.js.map → workspaces-CPIzAHAe.js.map} +1 -1
- package/public/index.html +23 -23
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/automations/reconcile.ts +69 -4
- package/src/branch-landed.ts +18 -11
- package/src/bus.ts +25 -18
- package/src/db/connection.ts +53 -1
- package/src/db/maintenance-worker.ts +37 -0
- package/src/db/message-reads.ts +86 -45
- package/src/db/migrations.ts +1 -0
- package/src/db/provider-quotas.ts +6 -7
- package/src/db/schema.ts +2 -1
- package/src/maintenance/jobs.ts +19 -13
- package/src/maintenance/scheduler.ts +8 -3
- package/src/maintenance/types.ts +5 -1
- package/src/mcp/index.ts +5 -5
- package/src/mcp/tools-workspace.ts +8 -3
- package/src/routes/orchestrator-proxy.ts +7 -6
- package/src/routes/sse.ts +2 -1
- package/src/routes/tasks.ts +29 -0
- package/src/routes/workspaces.ts +9 -2
- package/src/runtime-tokens.ts +4 -0
- package/src/security.ts +31 -7
- package/src/server.ts +40 -18
- package/src/services/git-process.ts +61 -0
- package/src/services/git-remote.ts +87 -7
- package/src/services/issue-lifecycle.ts +6 -6
- package/src/services/project.ts +47 -1
- package/src/services/send-message.ts +2 -4
- package/src/services/spawn-agent.ts +2 -2
- package/src/services/task-lifecycle.ts +6 -9
- package/src/settings/registry.ts +6 -4
- package/src/spawn-targets.ts +2 -2
- package/src/sse.ts +131 -46
- package/src/steward.ts +10 -6
- package/src/terminal-authz.ts +50 -0
- package/src/token-db.ts +3 -3
- package/src/validation.ts +1 -1
- package/src/workspace-actions.ts +46 -4
- package/src/workspace-auto-merge.ts +120 -13
- package/src/workspace-orphans.ts +1 -1
- package/src/workspace-probe.ts +19 -10
- package/public/assets/MessageBubble-jpshgh3K.js +0 -2
- package/public/assets/agents-QemvuxKO.js +0 -2
- package/public/assets/chat-xKXCafN9.js +0 -2
- package/public/assets/chat-xKXCafN9.js.map +0 -1
- package/public/assets/formatted-body-Dg9R3RD7.js +0 -3
- package/public/assets/formatted-body-impl-DlasTD8X.js +0 -3
- package/public/assets/formatted-body-impl-DlasTD8X.js.map +0 -1
- package/public/assets/index-D_fNJnsI.js.map +0 -1
- package/public/assets/lucide-react-Dc3ZwCBi.js +0 -9
- package/public/assets/lucide-react-Dc3ZwCBi.js.map +0 -1
- package/public/assets/messages-DRKulS-2.js +0 -2
- package/public/assets/overview-Dbq2lr7Y.js +0 -2
- package/public/assets/security-BH8kTdA-.js +0 -3
- package/public/assets/security-BH8kTdA-.js.map +0 -1
- package/public/assets/settings-DYNFGC2-.js +0 -6
- package/public/assets/store-BuuJUn70.js +0 -9
- package/public/assets/store-BuuJUn70.js.map +0 -1
package/public/index.html
CHANGED
|
@@ -12,34 +12,34 @@
|
|
|
12
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
|
|
13
13
|
<link rel="manifest" href="manifest.webmanifest">
|
|
14
14
|
<link rel="apple-touch-icon" href="icons/agent-relay-192.png">
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-Ceiwd02X.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-C_JMBXql.js">
|
|
18
|
-
<link rel="modulepreload" crossorigin href="./assets/lucide-react-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="./assets/react-dom-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
23
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
24
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
25
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
18
|
+
<link rel="modulepreload" crossorigin href="./assets/lucide-react-DuhlF2Sj.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-B-T7lg0a.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-BIjlN-xB.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="./assets/react-dom-DWBmcawP.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-B9G83DXv.js">
|
|
23
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-DT-OV6sQ.js">
|
|
24
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-C7aTlosb.js">
|
|
25
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-B3e4B9pg.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="./assets/dist-BiLuYU6G.js">
|
|
27
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
28
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="./assets/es2015-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
32
|
-
<link rel="modulepreload" crossorigin href="./assets/dist-
|
|
27
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-BInckSJC.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-DGfp5dLz.js">
|
|
29
|
+
<link rel="modulepreload" crossorigin href="./assets/es2015-CnKXYKpf.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-BlzVGRhh.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-OUPTEP6q.js">
|
|
32
|
+
<link rel="modulepreload" crossorigin href="./assets/dist-B6woQ9Tb.js">
|
|
33
33
|
<link rel="modulepreload" crossorigin href="./assets/types-uVOXgvMT.js">
|
|
34
34
|
<link rel="modulepreload" crossorigin href="./assets/display-exQDWum3.js">
|
|
35
|
-
<link rel="modulepreload" crossorigin href="./assets/store-
|
|
36
|
-
<link rel="modulepreload" crossorigin href="./assets/agent-type-icon-
|
|
37
|
-
<link rel="modulepreload" crossorigin href="./assets/button-
|
|
38
|
-
<link rel="modulepreload" crossorigin href="./assets/copy-button-
|
|
39
|
-
<link rel="modulepreload" crossorigin href="./assets/formatted-body-
|
|
40
|
-
<link rel="modulepreload" crossorigin href="./assets/task-chip-
|
|
41
|
-
<link rel="modulepreload" crossorigin href="./assets/badge-
|
|
42
|
-
<link rel="modulepreload" crossorigin href="./assets/input-
|
|
35
|
+
<link rel="modulepreload" crossorigin href="./assets/store-BM4zM89B.js">
|
|
36
|
+
<link rel="modulepreload" crossorigin href="./assets/agent-type-icon-CeEoMouY.js">
|
|
37
|
+
<link rel="modulepreload" crossorigin href="./assets/button-CzdTkRIf.js">
|
|
38
|
+
<link rel="modulepreload" crossorigin href="./assets/copy-button-B3o1jpLu.js">
|
|
39
|
+
<link rel="modulepreload" crossorigin href="./assets/formatted-body-q_qzt1L-.js">
|
|
40
|
+
<link rel="modulepreload" crossorigin href="./assets/task-chip-DXfvXBDx.js">
|
|
41
|
+
<link rel="modulepreload" crossorigin href="./assets/badge-DCIT3Irj.js">
|
|
42
|
+
<link rel="modulepreload" crossorigin href="./assets/input-B6Q0NWCS.js">
|
|
43
43
|
<link rel="modulepreload" crossorigin href="./assets/themes-DjSwmAZN.js">
|
|
44
44
|
<link rel="stylesheet" crossorigin href="./assets/index-DcmG5gLR.css">
|
|
45
45
|
</head>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCommand } from "../commands-db";
|
|
2
|
-
import { getAgent, getTask, listOrchestrators, sendMessage, updateTaskStatus } from "../db";
|
|
2
|
+
import { getAgent, getTask, listAgents, listOrchestrators, sendMessage, updateTaskStatus } from "../db";
|
|
3
3
|
import { renderTemplate } from "../prompt-resolver";
|
|
4
4
|
import { CLOSED_TASK_STATUS } from "./constants";
|
|
5
5
|
import {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
db,
|
|
12
12
|
} from "./storage";
|
|
13
13
|
import type { AutomationReconcileResult } from "./types";
|
|
14
|
-
import type { AutomationRun, AutomationRunStatus, AutomationRuntimeBudget, Command, Message, Task } from "../types";
|
|
14
|
+
import type { AgentCard, AutomationRun, AutomationRunStatus, AutomationRuntimeBudget, Command, Message, Task } from "../types";
|
|
15
15
|
|
|
16
16
|
export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileResult[] {
|
|
17
17
|
ensureAutomationTables();
|
|
@@ -41,6 +41,11 @@ export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileRe
|
|
|
41
41
|
if (!CLOSED_TASK_STATUS.has(task.status)) {
|
|
42
42
|
const updated = getAutomationRun(run.id);
|
|
43
43
|
if (!updated) continue;
|
|
44
|
+
const neverReadyOffline = maybeFailNeverReadyOfflineTarget(updated, task, now);
|
|
45
|
+
if (neverReadyOffline) {
|
|
46
|
+
results.push(neverReadyOffline);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
44
49
|
const timedOut = maybeTimeoutRuntimeBudget(updated, task, now);
|
|
45
50
|
if (timedOut) {
|
|
46
51
|
results.push(timedOut);
|
|
@@ -68,9 +73,8 @@ export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileRe
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
function automationRunRuntimeMeta(run: AutomationRun, task: Task): Record<string, unknown> {
|
|
71
|
-
const agentId
|
|
76
|
+
const { agentId, agent } = resolveAutomationRunAgent(run, task);
|
|
72
77
|
if (!agentId) return {};
|
|
73
|
-
const agent = getAgent(agentId);
|
|
74
78
|
const managed = listOrchestrators()
|
|
75
79
|
.flatMap((orchestrator) => orchestrator.managedAgents.map((managedAgent) => ({ orchestrator, managedAgent })))
|
|
76
80
|
.find(({ managedAgent }) => managedAgent.agentId === agentId);
|
|
@@ -103,6 +107,23 @@ function hasMetaChanges(current: Record<string, unknown>, patch: Record<string,
|
|
|
103
107
|
return Object.entries(patch).some(([key, value]) => current[key] !== value);
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
function resolveAutomationRunAgent(run: AutomationRun, task: Task): { agentId?: string; agent?: AgentCard } {
|
|
111
|
+
const explicitAgentId = task.claimedBy ?? run.targetAgentId ?? run.spawnedAgentId ?? stringMeta(run.meta, "runtimeAgentId");
|
|
112
|
+
if (explicitAgentId) return { agentId: explicitAgentId, agent: getAgent(explicitAgentId) ?? undefined };
|
|
113
|
+
|
|
114
|
+
const onDemandLabel = stringMeta(run.meta, "onDemandLabel");
|
|
115
|
+
const agent = listAgents().find((candidate) =>
|
|
116
|
+
(onDemandLabel !== undefined && candidate.label === onDemandLabel) ||
|
|
117
|
+
stringMeta(candidate.meta, "automationRunId") === run.id
|
|
118
|
+
);
|
|
119
|
+
return agent ? { agentId: agent.id, agent } : {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isOnDemandAutomationRun(run: AutomationRun): boolean {
|
|
123
|
+
if (run.meta.targetMode === "on_demand_agent") return true;
|
|
124
|
+
return getAutomation(run.automationId)?.targetPolicy.mode === "on_demand_agent";
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
function runtimeBudgetForRun(run: AutomationRun): AutomationRuntimeBudget | undefined {
|
|
107
128
|
const automation = getAutomation(run.automationId);
|
|
108
129
|
if (!automation || automation.targetPolicy.mode !== "on_demand_agent") return undefined;
|
|
@@ -163,6 +184,50 @@ function maybeWarnRuntimeBudget(run: AutomationRun, task: Task, now: number): Me
|
|
|
163
184
|
return message;
|
|
164
185
|
}
|
|
165
186
|
|
|
187
|
+
function maybeFailNeverReadyOfflineTarget(run: AutomationRun, task: Task, now: number): AutomationReconcileResult | undefined {
|
|
188
|
+
if (!isOnDemandAutomationRun(run) || task.claimedBy) return undefined;
|
|
189
|
+
const { agentId, agent } = resolveAutomationRunAgent(run, task);
|
|
190
|
+
if (!agentId || !agent || agent.status !== "offline" || agent.ready) return undefined;
|
|
191
|
+
|
|
192
|
+
const reason = "Automation target went offline before becoming ready";
|
|
193
|
+
const taskResult = updateTaskStatus(task.id, {
|
|
194
|
+
status: "canceled",
|
|
195
|
+
agentId,
|
|
196
|
+
result: reason,
|
|
197
|
+
body: reason,
|
|
198
|
+
metadata: {
|
|
199
|
+
automationId: run.automationId,
|
|
200
|
+
automationRunId: run.id,
|
|
201
|
+
automationTargetOffline: true,
|
|
202
|
+
automationTargetNeverReady: true,
|
|
203
|
+
agentId,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const updatedTask = taskResult.task ?? getTask(task.id) ?? task;
|
|
207
|
+
updateRun(run.id, {
|
|
208
|
+
status: "failed",
|
|
209
|
+
finishedAt: now,
|
|
210
|
+
error: reason,
|
|
211
|
+
result: {
|
|
212
|
+
taskStatus: updatedTask.status,
|
|
213
|
+
automationTargetOffline: true,
|
|
214
|
+
automationTargetNeverReady: true,
|
|
215
|
+
agentId,
|
|
216
|
+
},
|
|
217
|
+
targetAgentId: agentId,
|
|
218
|
+
spawnedAgentId: run.spawnedAgentId ?? agentId,
|
|
219
|
+
meta: {
|
|
220
|
+
...run.meta,
|
|
221
|
+
runtimeAgentId: agentId,
|
|
222
|
+
runtimeAgentStatus: agent.status,
|
|
223
|
+
runtimeAgentReady: agent.ready,
|
|
224
|
+
runtimeTargetOfflineAt: now,
|
|
225
|
+
runtimeTargetNeverReady: true,
|
|
226
|
+
},
|
|
227
|
+
}, now);
|
|
228
|
+
return { run: getAutomationRun(run.id)!, task: getTask(task.id) ?? updatedTask };
|
|
229
|
+
}
|
|
230
|
+
|
|
166
231
|
function maybeTimeoutRuntimeBudget(run: AutomationRun, task: Task, now: number): AutomationReconcileResult | undefined {
|
|
167
232
|
const budget = runtimeBudgetForRun(run);
|
|
168
233
|
if (!budget) return undefined;
|
package/src/branch-landed.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveCoordinatorAgentId } from "./steward-identity";
|
|
|
8
8
|
import { stewardFallbackTarget } from "./config";
|
|
9
9
|
import { handleLandedIssueLifecycle } from "./services/issue-lifecycle";
|
|
10
10
|
import { handleLandedTaskLifecycle } from "./services/task-lifecycle";
|
|
11
|
+
import { runGit } from "./services/git-process";
|
|
11
12
|
import type { WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
12
13
|
|
|
13
14
|
// #444 — branch.landed is the first rule-driven type: the on-main "merged" fan-out audience comes
|
|
@@ -46,7 +47,7 @@ export interface BranchLandedInput {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
type BranchLandedLogFn = (message: string) => void;
|
|
49
|
-
type LandedCommitSubjectResolver = (input: BranchLandedInput, log: BranchLandedLogFn) => string[] | undefined
|
|
50
|
+
type LandedCommitSubjectResolver = (input: BranchLandedInput, log: BranchLandedLogFn) => string[] | undefined | Promise<string[] | undefined>;
|
|
50
51
|
|
|
51
52
|
let landedCommitSubjectResolverForTests: LandedCommitSubjectResolver | undefined;
|
|
52
53
|
|
|
@@ -59,7 +60,7 @@ function cleanRef(value: string | undefined): string | undefined {
|
|
|
59
60
|
return cleaned || undefined;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn): string[] | undefined {
|
|
63
|
+
async function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn): Promise<string[] | undefined> {
|
|
63
64
|
if (landedCommitSubjectResolverForTests) return landedCommitSubjectResolverForTests(input, log);
|
|
64
65
|
if (input.prLand) return undefined;
|
|
65
66
|
const repoRoot = cleanRef(input.workspace.repoRoot);
|
|
@@ -67,17 +68,13 @@ function landedCommitSubjects(input: BranchLandedInput, log: BranchLandedLogFn):
|
|
|
67
68
|
const tipSha = cleanRef(input.mergedSha);
|
|
68
69
|
if (!repoRoot || !baseSha || !tipSha || baseSha === tipSha) return undefined;
|
|
69
70
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
stderr: "pipe",
|
|
74
|
-
});
|
|
75
|
-
if (proc.exitCode !== 0) {
|
|
76
|
-
const error = Buffer.from(proc.stderr).toString("utf8").trim() || `exit ${proc.exitCode}`;
|
|
71
|
+
const { stdout, stderr, exitCode, timedOut } = await runGit(["-C", repoRoot, "log", "--format=%B%x00", `${baseSha}..${tipSha}`]);
|
|
72
|
+
if (exitCode !== 0) {
|
|
73
|
+
const error = timedOut ? "timed out" : stderr.trim() || `exit ${exitCode}`;
|
|
77
74
|
log(`issue lifecycle commit range scan failed for ${input.workspace.id} (${baseSha.slice(0, 12)}..${tipSha.slice(0, 12)}): ${error}; falling back to tip subject`);
|
|
78
75
|
return undefined;
|
|
79
76
|
}
|
|
80
|
-
const subjects =
|
|
77
|
+
const subjects = stdout.split("\x00").map((msg) => msg.trim()).filter(Boolean);
|
|
81
78
|
return subjects.length > 0 ? subjects : undefined;
|
|
82
79
|
} catch (error) {
|
|
83
80
|
log(`issue lifecycle commit range scan failed for ${input.workspace.id} (${baseSha.slice(0, 12)}..${tipSha.slice(0, 12)}): ${errMessage(error)}; falling back to tip subject`);
|
|
@@ -94,6 +91,13 @@ function resolveLandedTask(workspace: Pick<WorkspaceRecord, "branch" | "ownerAge
|
|
|
94
91
|
return task ? { taskId: task.id, taskLabel: task.title } : undefined;
|
|
95
92
|
}
|
|
96
93
|
|
|
94
|
+
function landedNotificationIdempotencyKey(input: BranchLandedInput, landedTask: { taskId: number } | undefined, base: string): string | undefined {
|
|
95
|
+
const sha = cleanRef(input.mergedSha);
|
|
96
|
+
if (sha) return `branch-landed:${sha}`;
|
|
97
|
+
if (landedTask) return `branch-landed:task:${landedTask.taskId}:${base}`;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
97
101
|
/**
|
|
98
102
|
* #239 — turn an authoritative land completion into a relay-driven push so the author
|
|
99
103
|
* stops polling to learn it merged. Always emits the durable `branch.landed` event (the
|
|
@@ -108,8 +112,8 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
108
112
|
const landedBranch = workspace.branch;
|
|
109
113
|
const shortSha = input.mergedSha ? input.mergedSha.slice(0, 12) : undefined;
|
|
110
114
|
const lifecycleLog = (message: string) => console.warn(message);
|
|
111
|
-
const commitSubjects = landedCommitSubjects(input, lifecycleLog);
|
|
112
115
|
const landedTask = resolveLandedTask(workspace);
|
|
116
|
+
const idempotencyKey = landedNotificationIdempotencyKey(input, landedTask, base);
|
|
113
117
|
|
|
114
118
|
emitRelayEvent({
|
|
115
119
|
type: "branch.landed",
|
|
@@ -151,6 +155,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
151
155
|
// receipt accurate. Both are best-effort + isolated — a throw in either can never fail the merge
|
|
152
156
|
// (it already landed before notifyBranchLanded runs). A non-task branch makes the Task drive a no-op.
|
|
153
157
|
void (async () => {
|
|
158
|
+
const commitSubjects = await landedCommitSubjects(input, lifecycleLog);
|
|
154
159
|
try {
|
|
155
160
|
await handleLandedIssueLifecycle({ ...input, commitSubjects }, { log: lifecycleLog });
|
|
156
161
|
} catch (error) {
|
|
@@ -208,6 +213,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
208
213
|
continueLabel,
|
|
209
214
|
}),
|
|
210
215
|
payload: { ...payload, notification: "landed-success" },
|
|
216
|
+
idempotencyKey,
|
|
211
217
|
replyExpected: false,
|
|
212
218
|
},
|
|
213
219
|
});
|
|
@@ -231,6 +237,7 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
231
237
|
subject: `Merged to ${base}`,
|
|
232
238
|
body: `🔀 ${branchLabel}${authorLabel} merged to \`${base}\`${shaLabel}${subjectLabel}.${publishLabel}`,
|
|
233
239
|
payload: { ...payload, notification: "landed-success" },
|
|
240
|
+
idempotencyKey,
|
|
234
241
|
replyExpected: false,
|
|
235
242
|
},
|
|
236
243
|
});
|
package/src/bus.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "agent-relay-sdk/protocol";
|
|
32
32
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
33
33
|
import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
|
|
34
|
-
import type { Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
34
|
+
import type { AgentCard, Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
35
35
|
|
|
36
36
|
interface BusSocketData {
|
|
37
37
|
kind: "bus";
|
|
@@ -503,36 +503,43 @@ function withRegistered(
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
function broadcastRelayEvent(event: RelayEvent): void {
|
|
506
|
+
const frame = JSON.stringify({
|
|
507
|
+
type: "event",
|
|
508
|
+
payload: {
|
|
509
|
+
seq: event.seq,
|
|
510
|
+
eventType: event.type,
|
|
511
|
+
source: event.source,
|
|
512
|
+
subject: event.subject,
|
|
513
|
+
data: event.data,
|
|
514
|
+
timestamp: event.timestamp,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
const agentCache = new Map<string, AgentCard | null>();
|
|
506
518
|
for (const conn of busConnections.values()) {
|
|
507
|
-
if (!connectionWantsEvent(conn, event)) continue;
|
|
508
|
-
|
|
509
|
-
type: "event",
|
|
510
|
-
payload: {
|
|
511
|
-
seq: event.seq,
|
|
512
|
-
eventType: event.type,
|
|
513
|
-
source: event.source,
|
|
514
|
-
subject: event.subject,
|
|
515
|
-
data: event.data,
|
|
516
|
-
timestamp: event.timestamp,
|
|
517
|
-
},
|
|
518
|
-
});
|
|
519
|
+
if (!connectionWantsEvent(conn, event, agentCache)) continue;
|
|
520
|
+
conn.ws.send(frame);
|
|
519
521
|
}
|
|
520
522
|
}
|
|
521
523
|
|
|
522
|
-
function
|
|
524
|
+
function fanoutAgent(agentId: string, cache: Map<string, AgentCard | null>): AgentCard | null {
|
|
525
|
+
if (!cache.has(agentId)) cache.set(agentId, getAgent(agentId));
|
|
526
|
+
return cache.get(agentId) ?? null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function connectionWantsEvent(conn: BusConnection, event: RelayEvent, agentCache: Map<string, AgentCard | null>): boolean {
|
|
523
530
|
if (!matchesSubscription(conn.subscriptions, event.type)) return false;
|
|
531
|
+
if (event.type === "agent.status" && conn.agentId) return event.subject === conn.agentId || (typeof event.data.id === "string" && event.data.id === conn.agentId);
|
|
524
532
|
if (event.type === "message.new" && conn.agentId) {
|
|
525
|
-
return messageMatchesAgent(event.data as unknown as Message, conn.agentId,
|
|
533
|
+
return messageMatchesAgent(event.data as unknown as Message, conn.agentId, fanoutAgent(conn.agentId, agentCache));
|
|
526
534
|
}
|
|
527
535
|
if (event.type.startsWith("task.") && conn.agentId) {
|
|
528
536
|
const target = typeof event.data.target === "string" ? event.data.target : "";
|
|
529
|
-
return targetMatchesAgent(target, conn.agentId,
|
|
537
|
+
return targetMatchesAgent(target, conn.agentId, fanoutAgent(conn.agentId, agentCache));
|
|
530
538
|
}
|
|
531
539
|
if (event.type.startsWith("command.")) {
|
|
532
540
|
const command = isRecord(event.data.command) ? event.data.command : undefined;
|
|
533
541
|
if (!command) return true;
|
|
534
|
-
const target = typeof command.target === "string" ? command.target : "";
|
|
535
|
-
const source = typeof command.source === "string" ? command.source : "";
|
|
542
|
+
const target = typeof command.target === "string" ? command.target : "", source = typeof command.source === "string" ? command.source : "";
|
|
536
543
|
return target === conn.componentId || target === conn.agentId || source === conn.componentId || source === conn.agentId;
|
|
537
544
|
}
|
|
538
545
|
return true;
|
package/src/db/connection.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { DB_SLOW_QUERY_MS } from "../config";
|
|
3
4
|
|
|
4
5
|
// The shared SQLite handle. Set once by initDb() (schema.ts) and read everywhere
|
|
5
6
|
// else through getDb(). Kept module-private so the only mutation path is setDb().
|
|
6
7
|
let db: Database;
|
|
8
|
+
let dbPath: string | undefined;
|
|
7
9
|
|
|
8
|
-
export function setDb(conn: Database): void {
|
|
10
|
+
export function setDb(conn: Database, path?: string): void {
|
|
9
11
|
db = conn;
|
|
12
|
+
dbPath = path;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export function getDb(): Database {
|
|
@@ -14,6 +17,10 @@ export function getDb(): Database {
|
|
|
14
17
|
return db;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
export function getDbPath(): string | undefined {
|
|
21
|
+
return dbPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
// Connection-scoped pragmas. journal_mode and auto_vacuum persist in the file;
|
|
18
25
|
// the rest reset per connection, so they must be re-applied on every open.
|
|
19
26
|
export function applyConnectionPragmas(conn: Database): void {
|
|
@@ -67,6 +74,51 @@ export function runDbMaintenance(options: { vacuum?: boolean } = {}): {
|
|
|
67
74
|
return { analyzed: true, checkpointed: true, vacuumed };
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
export async function runDbMaintenanceWorker<T>(operation: string, args: string[], signal?: AbortSignal): Promise<T> {
|
|
78
|
+
if (!dbPath || dbPath === ":memory:") {
|
|
79
|
+
throw new Error("database worker requires a file-backed database");
|
|
80
|
+
}
|
|
81
|
+
if (signal?.aborted) throw new Error("database worker aborted");
|
|
82
|
+
const proc = Bun.spawn([process.execPath, join(import.meta.dir, "maintenance-worker.ts"), operation, dbPath, ...args], {
|
|
83
|
+
stdin: "ignore",
|
|
84
|
+
stdout: "pipe",
|
|
85
|
+
stderr: "pipe",
|
|
86
|
+
});
|
|
87
|
+
let killTimer: Timer | undefined;
|
|
88
|
+
const abort = () => {
|
|
89
|
+
proc.kill("SIGTERM");
|
|
90
|
+
killTimer = setTimeout(() => proc.kill("SIGKILL"), 2_000);
|
|
91
|
+
};
|
|
92
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
93
|
+
try {
|
|
94
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
|
95
|
+
if (signal?.aborted) throw new Error("database worker aborted");
|
|
96
|
+
if (exitCode !== 0) {
|
|
97
|
+
const detail = stderr.trim() || stdout.trim() || `exit ${exitCode}`;
|
|
98
|
+
throw new Error(`database worker ${operation} failed: ${detail}`);
|
|
99
|
+
}
|
|
100
|
+
return JSON.parse(stdout) as T;
|
|
101
|
+
} finally {
|
|
102
|
+
signal?.removeEventListener("abort", abort);
|
|
103
|
+
if (killTimer) clearTimeout(killTimer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function runDbMaintenanceAsync(options: { vacuum?: boolean; signal?: AbortSignal } = {}): Promise<{
|
|
108
|
+
analyzed: boolean;
|
|
109
|
+
checkpointed: boolean;
|
|
110
|
+
vacuumed: boolean;
|
|
111
|
+
offThread: boolean;
|
|
112
|
+
}> {
|
|
113
|
+
if (!dbPath || dbPath === ":memory:") return { ...runDbMaintenance(options), offThread: false };
|
|
114
|
+
const result = await runDbMaintenanceWorker<{ analyzed: boolean; checkpointed: boolean; vacuumed: boolean }>(
|
|
115
|
+
"db-maintenance",
|
|
116
|
+
[options.vacuum ? "vacuum" : "no-vacuum"],
|
|
117
|
+
options.signal,
|
|
118
|
+
);
|
|
119
|
+
return { ...result, offThread: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
70
122
|
// Shared error types thrown across query domains and unwrapped at call sites.
|
|
71
123
|
// `ValidationError` now lives in the SDK (agent-relay-sdk) as the single canonical
|
|
72
124
|
// home shared with the orchestrator package; re-exported here so the ~120 server
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { applyConnectionPragmas } from "./connection";
|
|
3
|
+
|
|
4
|
+
const [, , operation, dbPath, arg] = Bun.argv;
|
|
5
|
+
|
|
6
|
+
if (!operation || !dbPath) {
|
|
7
|
+
console.error("operation and db path are required");
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const db = new Database(dbPath, { create: true });
|
|
12
|
+
try {
|
|
13
|
+
applyConnectionPragmas(db);
|
|
14
|
+
if (operation === "db-maintenance") {
|
|
15
|
+
db.run("ANALYZE");
|
|
16
|
+
db.run("PRAGMA optimize");
|
|
17
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
18
|
+
let vacuumed = false;
|
|
19
|
+
if (arg === "vacuum") {
|
|
20
|
+
db.run("VACUUM");
|
|
21
|
+
vacuumed = true;
|
|
22
|
+
}
|
|
23
|
+
console.log(JSON.stringify({ analyzed: true, checkpointed: true, vacuumed }));
|
|
24
|
+
} else if (operation === "old-message-prune") {
|
|
25
|
+
const cutoff = Number(arg);
|
|
26
|
+
if (!Number.isFinite(cutoff)) throw new Error("old-message-prune requires a cutoff timestamp");
|
|
27
|
+
const prunedMessages = db.transaction(() => {
|
|
28
|
+
db.query("UPDATE messages SET reply_to = NULL WHERE reply_to IN (SELECT id FROM messages WHERE created_at < ?)").run(cutoff);
|
|
29
|
+
return db.query("DELETE FROM messages WHERE created_at < ?").run(cutoff).changes;
|
|
30
|
+
})();
|
|
31
|
+
console.log(JSON.stringify({ prunedMessages }));
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(`unknown maintenance operation: ${operation}`);
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
db.close();
|
|
37
|
+
}
|
package/src/db/message-reads.ts
CHANGED
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { getAgent } from "./agents.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { sendMessageWithResult } from "./messages.ts";
|
|
19
19
|
import { deleteArtifactLinksForEntity } from "./artifacts.ts";
|
|
20
|
-
import { getDb, timedQuery } from "./connection.ts";
|
|
20
|
+
import { getDb, getDbPath, runDbMaintenanceWorker, timedQuery } from "./connection.ts";
|
|
21
21
|
import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
|
|
22
22
|
import { messageTaskClosed } from "./claim-sql.ts";
|
|
23
23
|
import { MSG_SELECT, rowToMessage } from "./mappers.ts";
|
|
@@ -393,25 +393,58 @@ function relayConversationId(message: Message): string | undefined {
|
|
|
393
393
|
return stringField(replyContext?.conversationId) ?? stringField(conversation?.id);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
function
|
|
396
|
+
function messagesCoveredByLaterAgentResponses(messages: Message[], agentId: string): Set<number> {
|
|
397
|
+
if (messages.length === 0) return new Set();
|
|
397
398
|
// Order by id, not created_at: ids are monotonic insertion order, so this is
|
|
398
399
|
// robust when a reply lands in the same millisecond as the message it covers
|
|
399
400
|
// (created_at > … strictly would miss it, leaving the message wrongly pending).
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
401
|
+
const minMessageId = Math.min(...messages.map((message) => message.id));
|
|
402
|
+
const replies = getDb().query(`
|
|
403
|
+
SELECT id, thread_id, channel, reply_to, payload
|
|
404
|
+
FROM messages INDEXED BY idx_msg_from_id
|
|
405
|
+
WHERE from_agent = ?
|
|
406
|
+
AND id > ?
|
|
407
|
+
ORDER BY id ASC
|
|
408
|
+
`).all(agentId, minMessageId) as Array<{ id: number; thread_id: number | null; channel: string | null; reply_to: number | null; payload: string }>;
|
|
409
|
+
if (replies.length === 0) return new Set();
|
|
410
|
+
|
|
411
|
+
const covered = new Set<number>();
|
|
412
|
+
const conversationsById = new Map<number, string | undefined>();
|
|
413
|
+
for (const message of messages) conversationsById.set(message.id, relayConversationId(message));
|
|
414
|
+
|
|
415
|
+
for (const message of messages) {
|
|
416
|
+
const conversationId = conversationsById.get(message.id);
|
|
417
|
+
for (const reply of replies) {
|
|
418
|
+
if (reply.id <= message.id) continue;
|
|
419
|
+
if (reply.reply_to === message.id) {
|
|
420
|
+
covered.add(message.id);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
if (message.threadId !== undefined && reply.thread_id === message.threadId) {
|
|
424
|
+
covered.add(message.id);
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (!message.channel || reply.channel !== message.channel || !conversationId) continue;
|
|
428
|
+
if (relayConversationId({
|
|
429
|
+
id: reply.id,
|
|
430
|
+
from: agentId,
|
|
431
|
+
to: "",
|
|
432
|
+
body: "",
|
|
433
|
+
kind: "chat",
|
|
434
|
+
channel: reply.channel ?? undefined,
|
|
435
|
+
replyTo: reply.reply_to ?? undefined,
|
|
436
|
+
threadId: reply.thread_id ?? undefined,
|
|
437
|
+
payload: parseJson(reply.payload ?? "{}", {}),
|
|
438
|
+
meta: {},
|
|
439
|
+
readBy: [],
|
|
440
|
+
createdAt: 0,
|
|
441
|
+
}) === conversationId) {
|
|
442
|
+
covered.add(message.id);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return covered;
|
|
415
448
|
}
|
|
416
449
|
|
|
417
450
|
function replyObligationFromMessage(message: Message, agentId: string): ReplyObligation {
|
|
@@ -433,11 +466,20 @@ function replyObligationFromMessage(message: Message, agentId: string): ReplyObl
|
|
|
433
466
|
export function listPendingReplyObligations(agentId: string, limit = 20): ReplyObligation[] {
|
|
434
467
|
const scanLimit = Math.max(limit * 5, 50);
|
|
435
468
|
const rows = timedQuery("listPendingReplyObligations", () => getDb().query(`
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
469
|
+
SELECT m.*, (
|
|
470
|
+
SELECT json_group_array(agent_id) FROM message_reads WHERE message_id = m.id
|
|
471
|
+
) AS read_by_agents, (
|
|
472
|
+
SELECT json_group_array(json_object(
|
|
473
|
+
'messageId', message_id,
|
|
474
|
+
'actorId', actor_id,
|
|
475
|
+
'emoji', emoji,
|
|
476
|
+
'createdAt', created_at,
|
|
477
|
+
'updatedAt', updated_at
|
|
478
|
+
)) FROM message_reactions WHERE message_id = m.id
|
|
479
|
+
) AS reactions_json
|
|
480
|
+
FROM message_reads mr INDEXED BY idx_mr_agent
|
|
481
|
+
JOIN messages m ON m.id = mr.message_id
|
|
482
|
+
WHERE mr.agent_id = ?
|
|
441
483
|
AND m.from_agent != ?
|
|
442
484
|
AND NOT EXISTS (
|
|
443
485
|
SELECT 1 FROM messages reply
|
|
@@ -447,10 +489,12 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
|
|
|
447
489
|
ORDER BY m.created_at ASC
|
|
448
490
|
LIMIT ?
|
|
449
491
|
`).all(agentId, agentId, agentId, scanLimit) as any[]);
|
|
450
|
-
|
|
492
|
+
const candidates = rows
|
|
451
493
|
.map(rowToMessage)
|
|
452
|
-
.filter(messageRequiresReply)
|
|
453
|
-
|
|
494
|
+
.filter(messageRequiresReply);
|
|
495
|
+
const covered = messagesCoveredByLaterAgentResponses(candidates, agentId);
|
|
496
|
+
return candidates
|
|
497
|
+
.filter((message) => !covered.has(message.id))
|
|
454
498
|
.slice(0, limit)
|
|
455
499
|
.map((message) => replyObligationFromMessage(message, agentId));
|
|
456
500
|
}
|
|
@@ -484,25 +528,6 @@ export function channelObligationAnsweredBy(
|
|
|
484
528
|
return open.length === 1 ? open[0]!.messageId : null;
|
|
485
529
|
}
|
|
486
530
|
|
|
487
|
-
/**
|
|
488
|
-
* Spawn report-back linking (#492): a spawned worker's actual task is delivered as the provider
|
|
489
|
-
* prompt, while `seedSpawnReplyObligation` creates a short Stop-hook contract message so the worker
|
|
490
|
-
* cannot go idle without reporting back. Workers commonly send their natural completion report
|
|
491
|
-
* directly to the spawning coordinator; when there is exactly one open spawn obligation from that
|
|
492
|
-
* coordinator, that send is the answer. Link it to the obligation at send time so the Stop hook does
|
|
493
|
-
* not nag the worker into re-sending the same verdict through `/reply`.
|
|
494
|
-
*/
|
|
495
|
-
export function spawnObligationAnsweredBy(
|
|
496
|
-
agentId: string,
|
|
497
|
-
spawnerAgentId: string,
|
|
498
|
-
): number | null {
|
|
499
|
-
const open = listPendingReplyObligations(agentId, 50).filter((obligation) => {
|
|
500
|
-
if (obligation.from !== spawnerAgentId) return false;
|
|
501
|
-
return getMessage(obligation.messageId)?.payload?.source === "spawn-obligation";
|
|
502
|
-
});
|
|
503
|
-
return open.length === 1 ? open[0]!.messageId : null;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
531
|
/**
|
|
507
532
|
* #573 — spawned children no longer get a report-back reply obligation. Their turn-final
|
|
508
533
|
* response is promoted server-side to the spawn-parent by lineage, so creating a separate
|
|
@@ -605,6 +630,10 @@ export function getLatestMessageId(): number {
|
|
|
605
630
|
|
|
606
631
|
export function pruneOldMessages(maxAgeMs: number): number {
|
|
607
632
|
const cutoff = Date.now() - maxAgeMs;
|
|
633
|
+
return pruneOldMessagesBefore(cutoff);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function pruneOldMessagesBefore(cutoff: number): number {
|
|
608
637
|
return getDb().transaction(() => {
|
|
609
638
|
getDb()
|
|
610
639
|
.query("UPDATE messages SET reply_to = NULL WHERE reply_to IN (SELECT id FROM messages WHERE created_at < ?)")
|
|
@@ -614,3 +643,15 @@ export function pruneOldMessages(maxAgeMs: number): number {
|
|
|
614
643
|
.run(cutoff).changes;
|
|
615
644
|
})();
|
|
616
645
|
}
|
|
646
|
+
|
|
647
|
+
export async function pruneOldMessagesAsync(maxAgeMs: number, options: { signal?: AbortSignal } = {}): Promise<{
|
|
648
|
+
prunedMessages: number;
|
|
649
|
+
offThread: boolean;
|
|
650
|
+
}> {
|
|
651
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
652
|
+
if (!getDbPath() || getDbPath() === ":memory:") {
|
|
653
|
+
return { prunedMessages: pruneOldMessagesBefore(cutoff), offThread: false };
|
|
654
|
+
}
|
|
655
|
+
const result = await runDbMaintenanceWorker<{ prunedMessages: number }>("old-message-prune", [String(cutoff)], options.signal);
|
|
656
|
+
return { ...result, offThread: true };
|
|
657
|
+
}
|
package/src/db/migrations.ts
CHANGED
|
@@ -411,6 +411,7 @@ export function applyMigrations(): void {
|
|
|
411
411
|
getDb().run("ALTER TABLE messages ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'");
|
|
412
412
|
}
|
|
413
413
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_kind ON messages(kind)");
|
|
414
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_id ON messages(from_agent, id)");
|
|
414
415
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_from_kind_id ON messages(from_agent, kind, id)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_analytics_created ON messages(created_at, kind, from_agent, claimable, reply_to)"); getDb().run("CREATE INDEX IF NOT EXISTS idx_msg_session_analytics ON messages(kind, created_at, json_extract(payload, '$.session.type'), json_extract(payload, '$.session.label'), json_extract(payload, '$.session.turnId'))");
|
|
415
416
|
// (from_agent, idempotency_key) powers findMessageByIdempotencyKey — the retry-dedup lookup
|
|
416
417
|
// AND the session-step in-place upsert (Codex tool running→completed / streamed reasoning),
|