agent-relay-server 0.58.0 → 0.60.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.
Files changed (156) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-CHKzD4RY.js → MessageBubble-BrEKymoV.js} +2 -2
  4. package/public/assets/{MessageBubble-CHKzD4RY.js.map → MessageBubble-BrEKymoV.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BFVTJa0q.js → PlanGraphPanel-CXix1C-H.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BFVTJa0q.js.map → PlanGraphPanel-CXix1C-H.js.map} +1 -1
  7. package/public/assets/{activity-M-dFLU_O.js → activity-CRafQ1Je.js} +2 -2
  8. package/public/assets/{activity-M-dFLU_O.js.map → activity-CRafQ1Je.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CvHSoyCX.js → agent-profiles-DW4o5-ND.js} +2 -2
  10. package/public/assets/{agent-profiles-CvHSoyCX.js.map → agent-profiles-DW4o5-ND.js.map} +1 -1
  11. package/public/assets/agents-Cbegv0Hm.js +2 -0
  12. package/public/assets/{agents-DZBM5dp_.js.map → agents-Cbegv0Hm.js.map} +1 -1
  13. package/public/assets/analytics-DX6T7JX3.js +910 -0
  14. package/public/assets/{analytics-bvjb3Urs.js.map → analytics-DX6T7JX3.js.map} +1 -1
  15. package/public/assets/{automation-CDSn9Uq-.js → automation-CzSBv4gg.js} +2 -2
  16. package/public/assets/{automation-CDSn9Uq-.js.map → automation-CzSBv4gg.js.map} +1 -1
  17. package/public/assets/{badge-Dj6PREuI.js → badge-Cy6qO_Jz.js} +2 -2
  18. package/public/assets/{badge-Dj6PREuI.js.map → badge-Cy6qO_Jz.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-CdYg2HYc.js → branch-state-badge-CRA50r81.js} +2 -2
  20. package/public/assets/{branch-state-badge-CdYg2HYc.js.map → branch-state-badge-CRA50r81.js.map} +1 -1
  21. package/public/assets/{button-Cqpufqam.js → button-73wpRw65.js} +2 -2
  22. package/public/assets/{button-Cqpufqam.js.map → button-73wpRw65.js.map} +1 -1
  23. package/public/assets/{card-OmDF73CE.js → card-BU6PvVHv.js} +2 -2
  24. package/public/assets/{card-OmDF73CE.js.map → card-BU6PvVHv.js.map} +1 -1
  25. package/public/assets/{channels-BkxrdOFZ.js → channels-B1MQCPCT.js} +2 -2
  26. package/public/assets/{channels-BkxrdOFZ.js.map → channels-B1MQCPCT.js.map} +1 -1
  27. package/public/assets/chat-BqIaFYGJ.js +2 -0
  28. package/public/assets/{chat-YzX1z76l.js.map → chat-BqIaFYGJ.js.map} +1 -1
  29. package/public/assets/{connectors-Bg3rHgl_.js → connectors-C9aFJIrh.js} +2 -2
  30. package/public/assets/{connectors-Bg3rHgl_.js.map → connectors-C9aFJIrh.js.map} +1 -1
  31. package/public/assets/{copy-button-DI7b8A7_.js → copy-button-HRopUwui.js} +2 -2
  32. package/public/assets/{copy-button-DI7b8A7_.js.map → copy-button-HRopUwui.js.map} +1 -1
  33. package/public/assets/display-CketawEF.js.map +1 -1
  34. package/public/assets/{dist-CNjnTZEI.js → dist-CC5f7oYw.js} +2 -2
  35. package/public/assets/{dist-CNjnTZEI.js.map → dist-CC5f7oYw.js.map} +1 -1
  36. package/public/assets/{dist-DkeTrbTA.js → dist-CLHMeXYi.js} +2 -2
  37. package/public/assets/{dist-DkeTrbTA.js.map → dist-CLHMeXYi.js.map} +1 -1
  38. package/public/assets/{dist-C5t_OxI0.js → dist-Ci1MM51P.js} +2 -2
  39. package/public/assets/{dist-C5t_OxI0.js.map → dist-Ci1MM51P.js.map} +1 -1
  40. package/public/assets/{dist-CV3sgFiv.js → dist-Cko2KPW8.js} +2 -2
  41. package/public/assets/{dist-CV3sgFiv.js.map → dist-Cko2KPW8.js.map} +1 -1
  42. package/public/assets/dist-D41tzJYg.js +2 -0
  43. package/public/assets/{dist-yYauqYKR.js.map → dist-D41tzJYg.js.map} +1 -1
  44. package/public/assets/{dist-CoUvwLTy.js → dist-DFLciw0h.js} +2 -2
  45. package/public/assets/{dist-CoUvwLTy.js.map → dist-DFLciw0h.js.map} +1 -1
  46. package/public/assets/dist-DUoZnlB2.js +2 -0
  47. package/public/assets/{dist-DmhyU0Na.js.map → dist-DUoZnlB2.js.map} +1 -1
  48. package/public/assets/dist-De2oguBm.js +2 -0
  49. package/public/assets/{dist-DxkMwkrr.js.map → dist-De2oguBm.js.map} +1 -1
  50. package/public/assets/{dist-1x-LE9Uk.js → dist-DmN94rc8.js} +2 -2
  51. package/public/assets/{dist-1x-LE9Uk.js.map → dist-DmN94rc8.js.map} +1 -1
  52. package/public/assets/{dist-DV2tZ2fq.js → dist-Kkpnjx_M.js} +2 -2
  53. package/public/assets/{dist-DV2tZ2fq.js.map → dist-Kkpnjx_M.js.map} +1 -1
  54. package/public/assets/{dist-Ca3ujqG_.js → dist-T7u7IZGJ.js} +2 -2
  55. package/public/assets/{dist-Ca3ujqG_.js.map → dist-T7u7IZGJ.js.map} +1 -1
  56. package/public/assets/{es2015-DcavLcQ0.js → es2015-Df08Opjx.js} +2 -2
  57. package/public/assets/{es2015-DcavLcQ0.js.map → es2015-Df08Opjx.js.map} +1 -1
  58. package/public/assets/{formatted-body-impl-D8i9qqL1.js → formatted-body-impl-DOXH7oDF.js} +2 -2
  59. package/public/assets/{formatted-body-impl-D8i9qqL1.js.map → formatted-body-impl-DOXH7oDF.js.map} +1 -1
  60. package/public/assets/index-DaNOQJci.css +2 -0
  61. package/public/assets/index-br_Y6Cvt.js +21 -0
  62. package/public/assets/{index-Ba2RDJaA.js.map → index-br_Y6Cvt.js.map} +1 -1
  63. package/public/assets/{input-C_dl_9fG.js → input-B2EDz-BE.js} +2 -2
  64. package/public/assets/{input-C_dl_9fG.js.map → input-B2EDz-BE.js.map} +1 -1
  65. package/public/assets/{insights-D1aANwZ2.js → insights-B1edq5lk.js} +2 -2
  66. package/public/assets/{insights-D1aANwZ2.js.map → insights-B1edq5lk.js.map} +1 -1
  67. package/public/assets/{integrations-BrAr9bap.js → integrations-Cm4fjp7_.js} +2 -2
  68. package/public/assets/{integrations-BrAr9bap.js.map → integrations-Cm4fjp7_.js.map} +1 -1
  69. package/public/assets/{lib-B3iMVsf-.js → lib-BZrjIp-o.js} +3 -3
  70. package/public/assets/{lib-B3iMVsf-.js.map → lib-BZrjIp-o.js.map} +1 -1
  71. package/public/assets/lucide-react-H_-8G79A.js +9 -0
  72. package/public/assets/{lucide-react-CMhBract.js.map → lucide-react-H_-8G79A.js.map} +1 -1
  73. package/public/assets/{maintenance-C-xspsMG.js → maintenance-D6zvw0fU.js} +2 -2
  74. package/public/assets/{maintenance-C-xspsMG.js.map → maintenance-D6zvw0fU.js.map} +1 -1
  75. package/public/assets/{managed-agents-Bf3ni0vo.js → managed-agents-CzfEznM1.js} +2 -2
  76. package/public/assets/{managed-agents-Bf3ni0vo.js.map → managed-agents-CzfEznM1.js.map} +1 -1
  77. package/public/assets/{markdown-preview-impl-DKbUBTf_.js → markdown-preview-impl-DZlCaTHQ.js} +2 -2
  78. package/public/assets/{markdown-preview-impl-DKbUBTf_.js.map → markdown-preview-impl-DZlCaTHQ.js.map} +1 -1
  79. package/public/assets/memory-CUUAzJDc.js +2 -0
  80. package/public/assets/{memory-DeLwJFwH.js.map → memory-CUUAzJDc.js.map} +1 -1
  81. package/public/assets/messages-6QZH46LG.js +2 -0
  82. package/public/assets/{messages-Cww6dZnM.js.map → messages-6QZH46LG.js.map} +1 -1
  83. package/public/assets/{orchestrators-DRjwROXw.js → orchestrators-CrcqkPuc.js} +2 -2
  84. package/public/assets/{orchestrators-DRjwROXw.js.map → orchestrators-CrcqkPuc.js.map} +1 -1
  85. package/public/assets/overview-418xXZO_.js +2 -0
  86. package/public/assets/{overview-DjcRomU_.js.map → overview-418xXZO_.js.map} +1 -1
  87. package/public/assets/pairs-CnedATgT.js +2 -0
  88. package/public/assets/{pairs-DT0u2fnW.js.map → pairs-CnedATgT.js.map} +1 -1
  89. package/public/assets/{react-dom-Dtl9-x_1.js → react-dom-WI6TjVe7.js} +2 -2
  90. package/public/assets/{react-dom-Dtl9-x_1.js.map → react-dom-WI6TjVe7.js.map} +1 -1
  91. package/public/assets/{security-Bwg3oFI8.js → security-DS13pbag.js} +2 -2
  92. package/public/assets/{security-Bwg3oFI8.js.map → security-DS13pbag.js.map} +1 -1
  93. package/public/assets/{select-Cariwv69.js → select-BOprGZDS.js} +2 -2
  94. package/public/assets/{select-Cariwv69.js.map → select-BOprGZDS.js.map} +1 -1
  95. package/public/assets/{settings-Bd6Ug3DO.js → settings-BgInn_f3.js} +4 -4
  96. package/public/assets/{settings-Bd6Ug3DO.js.map → settings-BgInn_f3.js.map} +1 -1
  97. package/public/assets/{store-BgJq_lN9.js → store-DHO_a8BD.js} +5 -5
  98. package/public/assets/{store-BgJq_lN9.js.map → store-DHO_a8BD.js.map} +1 -1
  99. package/public/assets/{tasks-DVz1zhOU.js → tasks-B-4oCPuv.js} +2 -2
  100. package/public/assets/{tasks-DVz1zhOU.js.map → tasks-B-4oCPuv.js.map} +1 -1
  101. package/public/assets/teams-sCQAs_QA.js +2 -0
  102. package/public/assets/{teams-TqFntFB_.js.map → teams-sCQAs_QA.js.map} +1 -1
  103. package/public/assets/{terminal-viewer-impl-OsJ5crlx.js → terminal-viewer-impl-BhqS2XpS.js} +3 -3
  104. package/public/assets/{terminal-viewer-impl-OsJ5crlx.js.map → terminal-viewer-impl-BhqS2XpS.js.map} +1 -1
  105. package/public/assets/{use-keyboard-viewport-BYHcL7sP.js → use-keyboard-viewport-fOL_3RHB.js} +2 -2
  106. package/public/assets/{use-keyboard-viewport-BYHcL7sP.js.map → use-keyboard-viewport-fOL_3RHB.js.map} +1 -1
  107. package/public/assets/{work-queue-B4b2jPLR.js → work-queue-BDhjlwUH.js} +2 -2
  108. package/public/assets/{work-queue-B4b2jPLR.js.map → work-queue-BDhjlwUH.js.map} +1 -1
  109. package/public/assets/workspaces-6I-C69nf.js +3 -0
  110. package/public/assets/{workspaces-m97LP4Eu.js.map → workspaces-6I-C69nf.js.map} +1 -1
  111. package/public/index.html +21 -21
  112. package/src/agent-lifecycle-events.ts +41 -21
  113. package/src/branch-landed.ts +46 -35
  114. package/src/channel-target.ts +10 -0
  115. package/src/config.ts +5 -0
  116. package/src/context-advisory.ts +13 -6
  117. package/src/db/message-reads.ts +6 -0
  118. package/src/db/messages.ts +16 -1
  119. package/src/db/migrations.ts +15 -0
  120. package/src/db/notification-subscriptions.ts +198 -0
  121. package/src/db/schema.ts +11 -0
  122. package/src/db/stats.ts +209 -38
  123. package/src/index.ts +2 -0
  124. package/src/maintenance.ts +41 -15
  125. package/src/mcp-notification-subscription-tools.ts +111 -0
  126. package/src/mcp.ts +6 -0
  127. package/src/notification-router.ts +168 -0
  128. package/src/notification-rules.ts +186 -0
  129. package/src/notify.ts +9 -0
  130. package/src/project-instructions.ts +176 -0
  131. package/src/reviewer-pipeline.ts +54 -40
  132. package/src/routes/agents-spawn.ts +2 -0
  133. package/src/routes/commands.ts +11 -5
  134. package/src/self-resume.ts +11 -6
  135. package/src/services/plan-live-bindings.ts +11 -6
  136. package/src/services/rate-limit-resume.ts +17 -10
  137. package/src/services/spawn-agent.ts +26 -1
  138. package/src/services/transient-agent-reaper.ts +11 -6
  139. package/src/token-db.ts +3 -3
  140. package/src/workspace-merge.ts +11 -0
  141. package/src/workspace-pr-completion.ts +72 -9
  142. package/public/assets/agents-DZBM5dp_.js +0 -2
  143. package/public/assets/analytics-bvjb3Urs.js +0 -910
  144. package/public/assets/chat-YzX1z76l.js +0 -2
  145. package/public/assets/dist-DmhyU0Na.js +0 -2
  146. package/public/assets/dist-DxkMwkrr.js +0 -2
  147. package/public/assets/dist-yYauqYKR.js +0 -2
  148. package/public/assets/index-Ba2RDJaA.js +0 -21
  149. package/public/assets/index-CtFXWxBJ.css +0 -2
  150. package/public/assets/lucide-react-CMhBract.js +0 -9
  151. package/public/assets/memory-DeLwJFwH.js +0 -2
  152. package/public/assets/messages-Cww6dZnM.js +0 -2
  153. package/public/assets/overview-DjcRomU_.js +0 -2
  154. package/public/assets/pairs-DT0u2fnW.js +0 -2
  155. package/public/assets/teams-TqFntFB_.js +0 -2
  156. package/public/assets/workspaces-m97LP4Eu.js +0 -3
@@ -6,6 +6,7 @@ import { ValidationError, listAgents } from "../db";
6
6
  import { cleanMeta, cleanNullableString, cleanString, cleanStringArray, optionalEnum } from "../validation";
7
7
  import { registerAgent } from "../services/register-agent";
8
8
  import { spawnAgent, type SpawnAgentInput } from "../services/spawn-agent";
9
+ import { parseProjectInstructions } from "../project-instructions";
9
10
  import { authContextFromRequest } from "../services/auth-context";
10
11
  import { listHostDirectories } from "../agent-spawn";
11
12
  import { rankRouteCandidates, type RouteAdvisorInput } from "../context-router";
@@ -116,6 +117,7 @@ export const postAgentSpawn: Handler = async (req) => {
116
117
  profile: cleanString(parsed.body.profile, "profile", { max: 120 }),
117
118
  prompt: cleanString(parsed.body.prompt, "prompt", { max: 16000 }),
118
119
  systemPromptAppend: cleanString(parsed.body.systemPromptAppend, "systemPromptAppend", { max: 64_000 }),
120
+ projectInstructions: parseProjectInstructions(parsed.body.projectInstructions, (m) => new ValidationError(m)),
119
121
  tags: cleanStringArray(parsed.body.tags, "tags", { itemMax: 80, maxItems: 50 }),
120
122
  capabilities: cleanStringArray(parsed.body.capabilities, "capabilities", { itemMax: 80, maxItems: 50 }),
121
123
  providerArgs: cleanStringArray(parsed.body.providerArgs, "providerArgs", { itemMax: 80, maxItems: 50 }),
@@ -229,15 +229,21 @@ export const patchCommand: Handler = async (req, params) => {
229
229
  if (newBranch) {
230
230
  setWorkspaceBranch(workspaceId, newBranch, baseSha ?? mergedSha);
231
231
  }
232
- // #239 — push the author a "your branch landed" notice (no polling). Only on a
232
+ // #239 — push the author a "your branch landed" notice (no polling). Fires on a
233
233
  // real land; a no-op resolution (#230) merged nothing, so it earns no notice.
234
- if (command.result.merged === true && landedWorkspace) {
234
+ // #423 — a PR-land RECYCLE is a no-op merge (`merged:false`) but a land DID happen
235
+ // on the remote: the relay carries that ground truth in `params.prLanded`, so the
236
+ // notice fires from it with the recycled `newBranch` — full rebase-ff parity.
237
+ const prLanded = isRecord(command.params.prLanded) ? command.params.prLanded : undefined;
238
+ if ((command.result.merged === true || prLanded) && landedWorkspace) {
235
239
  notifyBranchLanded({
236
240
  workspace: landedWorkspace,
237
- mergedSha,
238
- subject: cleanString(command.result.subject, "result.subject", { max: 200 }),
241
+ mergedSha: prLanded ? cleanString(prLanded.sha, "params.prLanded.sha", { max: 64 }) ?? mergedSha : mergedSha,
242
+ subject: prLanded
243
+ ? cleanString(prLanded.subject, "params.prLanded.subject", { max: 200 })
244
+ : cleanString(command.result.subject, "result.subject", { max: 200 }),
239
245
  newBranch,
240
- pushed: typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
246
+ pushed: prLanded ? true : typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
241
247
  });
242
248
  }
243
249
  }
@@ -11,7 +11,7 @@ import {
11
11
  ValidationError,
12
12
  } from "./db";
13
13
  import { emitRelayEvent } from "./events";
14
- import { notifySystemMessage } from "./notify";
14
+ import { routeNotification } from "./notification-router";
15
15
  import type { AgentCard, Command, ContinuationEnvelope, ContinuationLedgerEntry, SelfResumeMode } from "./types";
16
16
 
17
17
  export interface CompactAndResumeInput {
@@ -200,11 +200,16 @@ function notifySelfResumeBudgetExhausted(agent: AgentCard, envelope: Continuatio
200
200
  };
201
201
  emitRelayEvent({ type: "agent.resume_budget_exhausted", source: "server", subject: agent.id, data: payload });
202
202
  if (!agent.spawnedBy) return;
203
- notifySystemMessage(agent.spawnedBy, {
204
- subject: "Self-resume budget exhausted",
205
- body: `Spawned agent \`${agent.id}\` stopped cleanly: ${reason}.`,
206
- payload,
207
- replyExpected: false,
203
+ routeNotification({
204
+ type: "agent.resume_budget_exhausted",
205
+ scope: { agentId: agent.id, parent: agent.spawnedBy },
206
+ recipients: [agent.spawnedBy],
207
+ message: {
208
+ subject: "Self-resume budget exhausted",
209
+ body: `Spawned agent \`${agent.id}\` stopped cleanly: ${reason}.`,
210
+ payload,
211
+ replyExpected: false,
212
+ },
208
213
  });
209
214
  }
210
215
 
@@ -1,6 +1,6 @@
1
1
  import { getTeam } from "../db";
2
2
  import { subscribeRelayEvents, type RelayEvent } from "../events";
3
- import { notifySystemMessage } from "../notify";
3
+ import { routeNotification } from "../notification-router";
4
4
  import { updateBoundPlanNodesForAgent, updateBoundPlanNodesFromAgent } from "./plan-graph";
5
5
 
6
6
  let unsubscribe: (() => void) | undefined;
@@ -48,11 +48,16 @@ function notifyBoundExit(event: RelayEvent, updatedPlans: ReturnType<typeof upda
48
48
  const target = team?.coordinatorId ?? `team:${plan.teamId}`;
49
49
  const blockedNodes = plan.nodes.filter((node) => node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status === "blocked");
50
50
  if (blockedNodes.length === 0) continue;
51
- notifySystemMessage(target, {
52
- subject: "Bound plan node blocked",
53
- body: `Agent \`${agentId}\` exited${reason ? ` (${reason})` : ""}; blocked plan node(s) ${blockedNodes.map((node) => `\`${node.id}\``).join(", ")} in \`${plan.id}\`.`,
54
- payload: { kind: "plan.bound_agent_exited", planId: plan.id, agentId, nodeIds: blockedNodes.map((node) => node.id), reason },
55
- replyExpected: false,
51
+ routeNotification({
52
+ type: "plan.bound_agent_exited",
53
+ scope: { agentId, planId: plan.id, teamId: plan.teamId },
54
+ recipients: [target],
55
+ message: {
56
+ subject: "Bound plan node blocked",
57
+ body: `Agent \`${agentId}\` exited${reason ? ` (${reason})` : ""}; blocked plan node(s) ${blockedNodes.map((node) => `\`${node.id}\``).join(", ")} in \`${plan.id}\`.`,
58
+ payload: { kind: "plan.bound_agent_exited", planId: plan.id, agentId, nodeIds: blockedNodes.map((node) => node.id), reason },
59
+ replyExpected: false,
60
+ },
56
61
  });
57
62
  }
58
63
  }
@@ -1,5 +1,5 @@
1
1
  import { createActivityEvent, listAgents } from "../db";
2
- import { notifySystemMessage } from "../notify";
2
+ import { routeNotification } from "../notification-router";
3
3
  import { agentProviderState, isRateLimitHold, rateLimitResumeAt } from "../provider-state";
4
4
 
5
5
  // #286 Phase 2: auto-resume agents held on a usage/rate-limit stall once their
@@ -52,16 +52,23 @@ export function resumeRateLimitedAgents(): Record<string, unknown> {
52
52
  resumeTracker.set(agent.id, now);
53
53
 
54
54
  try {
55
- notifySystemMessage(agent.id, {
56
- subject: "Usage limit reset resume",
57
- body: RESUME_BODY,
58
- payload: {
59
- kind: "agent.rate_limit_resume",
60
- agentId: agent.id,
61
- resetAt: state?.resetAt,
62
- errorType: state?.errorType,
55
+ // Short default TTL (per-type): a "limit reset, resume" push is pointless once stale — the
56
+ // window has moved on. One recipient per iteration keeps the per-agent rollback below intact.
57
+ routeNotification({
58
+ type: "agent.rate_limit_resume",
59
+ scope: { agentId: agent.id },
60
+ recipients: [agent.id],
61
+ message: {
62
+ subject: "Usage limit reset — resume",
63
+ body: RESUME_BODY,
64
+ payload: {
65
+ kind: "agent.rate_limit_resume",
66
+ agentId: agent.id,
67
+ resetAt: state?.resetAt,
68
+ errorType: state?.errorType,
69
+ },
70
+ replyExpected: false,
63
71
  },
64
- replyExpected: false,
65
72
  });
66
73
  } catch {
67
74
  // A stale/unreachable target must never break the sweep; it store-aheads
@@ -32,6 +32,7 @@ import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } fr
32
32
  import { runnerRuntimeTokenEnv } from "../runtime-tokens";
33
33
  import { selectSpawnOrchestrator } from "../spawn-targets";
34
34
  import { getAgentProfile } from "../config-store";
35
+ import { composeProjectInstructions, type ProjectInstructionPolicy } from "../project-instructions";
35
36
  import { hasComponentScope, isComponentAuthorizedFor } from "../security";
36
37
  import { isPathWithinBase } from "../utils";
37
38
  import { McpAuthError, McpNotFoundError } from "../mcp-errors";
@@ -62,6 +63,16 @@ export interface SpawnAgentInput {
62
63
  profile?: string;
63
64
  prompt?: string;
64
65
  systemPromptAppend?: string;
66
+ /**
67
+ * #409 — project instruction projection. When set, relay composes an instruction
68
+ * preamble from the spawn's resolved project (own entries + inherited `contains`-
69
+ * ancestors) and PREPENDS it to systemPromptAppend. Additive-from-empty (#403
70
+ * decision 4): the block is built only from `project_entries`, never from host
71
+ * CLAUDE.md. Absent (undefined) = OFF → the spawn command is byte-identical to today.
72
+ * `policy` selects the included source set: `all` (manual+promoted+ingested) or
73
+ * `vanilla` (manual+promoted; excludes `ingested` host-CLAUDE.md content).
74
+ */
75
+ projectInstructions?: ProjectInstructionPolicy;
65
76
  /**
66
77
  * #413 — does this spawn carry a report-back contract? When true, relay seeds a pending
67
78
  * reply obligation from the spawner to the child at registration, so the child can't go
@@ -197,6 +208,20 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
197
208
  // resolves it. Absent (graceful) when the cwd is inside no registered root.
198
209
  const callerProject = resolveProjectForCwd(resolvedCwd);
199
210
 
211
+ // #409 — instruction projection (default OFF). Only when the caller opts in AND the
212
+ // spawn cwd resolves to a project do we compose a knowledge preamble and PREPEND it to
213
+ // systemPromptAppend. Absent flag / no project / empty block ⇒ systemPromptAppend is
214
+ // left exactly as the caller passed it, so the spawn command stays byte-identical.
215
+ let systemPromptAppend = input.systemPromptAppend;
216
+ if (input.projectInstructions && callerProject) {
217
+ const block = composeProjectInstructions(callerProject, {
218
+ ...(input.projectInstructions.policy ? { policy: input.projectInstructions.policy } : {}),
219
+ });
220
+ if (block) {
221
+ systemPromptAppend = systemPromptAppend ? `${block}\n\n${systemPromptAppend}` : block;
222
+ }
223
+ }
224
+
200
225
  // Child runner token. `agentInitiated` (set whenever an agent is the caller) forces a
201
226
  // non-spawn-capable child (no grandchildren); `spawnedBy` stamps authoritative lineage the
202
227
  // child can't forge (it registers with spawnedBy = caller, read from the signed token).
@@ -244,7 +269,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
244
269
  permissionMode: approvalMode,
245
270
  providerArgs: input.providerArgs ?? [],
246
271
  prompt: input.prompt,
247
- systemPromptAppend: input.systemPromptAppend,
272
+ systemPromptAppend,
248
273
  ...(expectReply ? { expectReply } : {}),
249
274
  policyName: input.policyName,
250
275
  spawnRequestId,
@@ -1,7 +1,7 @@
1
1
  import { createCommand } from "../commands-db";
2
2
  import { emitRelayEvent } from "../events";
3
3
  import { createActivityEvent, getAgent, listAgents, listWorkspaces } from "../db";
4
- import { notifySystemMessage } from "../notify";
4
+ import { routeNotification } from "../notification-router";
5
5
  import { isProviderBlocked } from "../provider-state";
6
6
  import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
7
7
 
@@ -47,11 +47,16 @@ function notifyTransientHeld(agentId: string, workspaceId: string | undefined, s
47
47
  metadata: { source: "server", maintenanceJobId: "transient-agent-reaper", ...payload },
48
48
  });
49
49
  if (parent) {
50
- notifySystemMessage(parent, {
51
- subject: "Transient worker held pending land",
52
- body: `Transient agent \`${agentId}\` is idle but still unreaped because workspace \`${workspaceId ?? "unknown"}\` is \`${status ?? "unknown"}\` after ${Math.round(waitMs / 60_000)} min. Its work is being preserved; resolve the land before cleanup.`,
53
- payload,
54
- replyExpected: false,
50
+ routeNotification({
51
+ type: "agent.transient_land_hold",
52
+ scope: { agentId, parent, workspaceId },
53
+ recipients: [parent],
54
+ message: {
55
+ subject: "Transient worker held pending land",
56
+ body: `Transient agent \`${agentId}\` is idle but still unreaped because workspace \`${workspaceId ?? "unknown"}\` is \`${status ?? "unknown"}\` after ${Math.round(waitMs / 60_000)} min. Its work is being preserved; resolve the land before cleanup.`,
57
+ payload,
58
+ replyExpected: false,
59
+ },
55
60
  });
56
61
  }
57
62
  }
package/src/token-db.ts CHANGED
@@ -57,7 +57,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
57
57
  name: "Provider Agent",
58
58
  description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
59
59
  role: "provider",
60
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "insights:write"],
60
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
61
61
  ttlSeconds: 24 * 60 * 60,
62
62
  builtIn: true,
63
63
  createdBy: "system",
@@ -67,7 +67,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
67
67
  name: "Provider Child Agent",
68
68
  description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
69
69
  role: "provider",
70
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "insights:write"],
70
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
71
71
  constraints: { canDelegate: false },
72
72
  ttlSeconds: 2 * 60 * 60,
73
73
  builtIn: true,
@@ -78,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
78
78
  name: "Provider Interactive Agent",
79
79
  description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
80
80
  role: "provider",
81
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "insights:write"],
81
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "notifications:read", "notifications:write", "insights:write"],
82
82
  constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
83
83
  ttlSeconds: 30 * 24 * 60 * 60,
84
84
  builtIn: true,
@@ -113,6 +113,16 @@ interface RequestWorkspaceMergeOptions {
113
113
  reviewer?: string;
114
114
  /** Extra metadata merged onto the workspace row when moving to merge_planned. */
115
115
  metadata?: Record<string, unknown>;
116
+ /**
117
+ * Set when this "merge" is really a land-and-continue RECYCLE of a workspace whose PR
118
+ * already merged on the remote out-of-band (#423). The host preview is then a no-op
119
+ * (work is already in base) so `mergeWorkspace` recycles the live owner onto a fresh
120
+ * branch without merging anything — `result.merged` is false. The relay still owns the
121
+ * ground truth that a land happened (it observed `prMerged`), so it carries the merge
122
+ * SHA/subject here; the merge-result handler fires the `branch.landed` notice from it,
123
+ * giving full parity with a rebase-ff land-and-continue.
124
+ */
125
+ prLanded?: { sha?: string; subject?: string };
116
126
  }
117
127
 
118
128
  type RequestWorkspaceMergeResult =
@@ -253,6 +263,7 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
253
263
  prBody,
254
264
  ...(autoMerge !== undefined ? { autoMerge } : {}),
255
265
  ...(opts.reviewer !== undefined ? { reviewer: opts.reviewer } : {}),
266
+ ...(opts.prLanded !== undefined ? { prLanded: opts.prLanded } : {}),
256
267
  requestedBy: opts.requestedBy,
257
268
  requestedAt: Date.now(),
258
269
  },
@@ -1,22 +1,85 @@
1
1
  import { isRecord } from "agent-relay-sdk";
2
2
  import { createCommand, getCommand } from "./commands-db";
3
- import { patchWorkspaceMetadata } from "./db";
3
+ import { createActivityEvent, patchWorkspaceMetadata } from "./db";
4
4
  import { handleReviewerPipelineScan, isAwaitingReviewerApproval } from "./reviewer-pipeline";
5
+ import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
5
6
  import type { Command, WorkspaceAutoMergePolicy, WorkspaceMergePreview, WorkspaceRecord } from "./types";
6
7
 
8
+ /**
9
+ * Decide what to do when a workspace's PR has merged on the remote (`prMerged`/`landed`),
10
+ * out-of-band of the relay's land path. Splits on owner liveness:
11
+ *
12
+ * - LIVE owner → land-and-continue RECYCLE (#423). The rebase-ff land path recycles a live
13
+ * owner onto a fresh `agent/...-N` branch off the advanced base; a PR land never ran through
14
+ * it, stranding the owner on its merged branch at terminal `merged` (`relay_workspace_status`
15
+ * then reports "you don't own an isolated workspace"). Dispatch a `rebase-ff` merge carrying
16
+ * `prLanded` (the merge SHA/subject — our ground truth). The host honors prLanded and recycles
17
+ * the live owner onto a fresh branch cut off the UPDATED upstream base, returning it to
18
+ * `active` in the same CWD. (A bare no-op assumption would NOT hold for a squash merge onto an
19
+ * advanced base — local cherry/tree detection can't see the squash commit, so the host's own
20
+ * no-op derivation reports ahead>0 and would soft-loop via review_requested; prLanded is what
21
+ * short-circuits that, see `mergeWorkspace`.) Explicit `rebase-ff` so a (prMerged-gated,
22
+ * near-impossible) non-recycle never opens a duplicate PR; the merge-result handler fires the
23
+ * `branch.landed` notice from `prLanded` (SHA + subject) since the recycle reports no land.
24
+ * - GONE owner (offline/absent) → finalize terminal `merged` + store-ahead `branch.landed` for
25
+ * its next poll (#304). Signalled by `kind: "finalize"`; the caller invokes the finalizer.
26
+ *
27
+ * `kind: "wait"` means the per-repo merge lease is held this tick (a concurrent merge) — retry
28
+ * next sweep; the caller must NOT finalize, or it re-strands the live owner.
29
+ */
30
+ export function reconcileLandedPr(workspace: WorkspaceRecord, preview: WorkspaceMergePreview):
31
+ | { kind: "recycle"; command: Command }
32
+ | { kind: "wait"; reason: string }
33
+ | { kind: "finalize" } {
34
+ if (!isOwnerAlive(workspace.ownerAgentId)) return { kind: "finalize" };
35
+ const recycle = requestWorkspaceMerge(workspace, {
36
+ strategy: "rebase-ff",
37
+ requestedBy: "pr-land-recycle",
38
+ prLanded: {
39
+ sha: typeof preview.prMergeSha === "string" ? preview.prMergeSha : undefined,
40
+ subject: typeof preview.prMergeSubject === "string" ? preview.prMergeSubject : undefined,
41
+ },
42
+ });
43
+ if (recycle.ok) {
44
+ createActivityEvent({
45
+ clientId: `workspace-pr-recycle-${workspace.id}-${Date.now()}`,
46
+ kind: "state",
47
+ title: "Workspace recycling after PR land",
48
+ body: `${workspace.branch ?? workspace.id}'s PR merged into ${preview.baseRef ?? "base"} — recycling the live owner onto a fresh branch (land-and-continue)`,
49
+ meta: workspace.branch ?? workspace.id,
50
+ icon: "ti-git-merge",
51
+ view: "orchestrators",
52
+ metadata: { source: "server", maintenanceJobId: "workspace-conflict-scan", workspaceId: workspace.id, commandId: recycle.command.id, landedVia: "pr" },
53
+ });
54
+ return { kind: "recycle", command: recycle.command };
55
+ }
56
+ // 409 = lease held by an in-flight merge — retry next sweep, don't finalize (re-strands a
57
+ // live owner). Any other failure (e.g. lost worktree) falls through to finalize so the row
58
+ // doesn't loop forever in a reconcilable status.
59
+ if (recycle.status === 409) return { kind: "wait", reason: "recycle-lease-held" };
60
+ return { kind: "finalize" };
61
+ }
62
+
7
63
  const ACTIVE_COMMAND_STATUSES = new Set(["pending", "accepted", "running"]);
8
64
  const PASSING_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
9
65
  const PASSING_STATES = new Set(["SUCCESS"]);
10
66
  const READY_MERGE_STATES = new Set(["CLEAN", "HAS_HOOKS"]);
11
67
 
12
- function relayPrMergeAlreadyHandled(workspace: WorkspaceRecord): boolean {
68
+ function relayPrMergeAlreadyHandled(workspace: WorkspaceRecord, preview: WorkspaceMergePreview): boolean {
13
69
  const commandId = text(workspace.metadata.relayMergeCommandId);
14
- if (commandId) {
15
- const command = getCommand(commandId);
16
- if (command && ACTIVE_COMMAND_STATUSES.has(command.status)) return true;
17
- if (command?.status === "succeeded" && command.result?.relayMerged === true) return true;
18
- }
19
- return workspace.metadata.relayMerged === true && !commandId;
70
+ if (!commandId) return false;
71
+ const command = getCommand(commandId);
72
+ if (!command) return false;
73
+ // PR-scope the idempotency guard (#453). A land-and-continue recycle (#423) repoints the
74
+ // workspace onto a fresh branch but leaves the PRIOR cycle's relayMerge* metadata on the row,
75
+ // so without this a succeeded merge for an already-closed PR would block every SUBSEQUENT PR
76
+ // on the recycled workspace forever (green PR, never merged, no steward). Only treat the merge
77
+ // as already-handled when the recorded command targets the SAME PR we're now considering.
78
+ const recordedPr = isRecord(command.params) && typeof command.params.prNumber === "number" ? command.params.prNumber : undefined;
79
+ const currentPr = preview.prNumber ?? (typeof workspace.metadata.prNumber === "number" ? workspace.metadata.prNumber : undefined);
80
+ if (typeof recordedPr === "number" && typeof currentPr === "number" && recordedPr !== currentPr) return false;
81
+ if (ACTIVE_COMMAND_STATUSES.has(command.status)) return true;
82
+ return command.status === "succeeded" && command.result?.relayMerged === true;
20
83
  }
21
84
 
22
85
  function nativeAutoMergeArmed(workspace: WorkspaceRecord): boolean {
@@ -43,7 +106,7 @@ function prReadyForRelayMerge(workspace: WorkspaceRecord, preview: WorkspaceMerg
43
106
  if (preview.prMerged === true || preview.prState === "merged") return { ok: false, reason: "already-merged" };
44
107
  if (!preview.prNumber && !preview.prUrl) return { ok: false, reason: "missing-pr" };
45
108
  if (nativeAutoMergeArmed(workspace)) return { ok: false, reason: "native-auto-merge-armed" };
46
- if (relayPrMergeAlreadyHandled(workspace)) return { ok: false, reason: "relay-merge-already-dispatched" };
109
+ if (relayPrMergeAlreadyHandled(workspace, preview)) return { ok: false, reason: "relay-merge-already-dispatched" };
47
110
 
48
111
  const policy = prAutoMergePolicy(workspace, preview);
49
112
  if (policy === "manual") return { ok: false, reason: "manual", policy };
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{At as t,B as n,Cn as r,G as i,I as a,J as o,Kn as s,Mn as c,On as l,P as u,Pn as d,Rt as f,g as p,i as m,l as h,m as g,st as _}from"./lucide-react-CMhBract.js";import{o as ee,t as v}from"./store-BgJq_lN9.js";import{G as y,K as b,U as x,V as S,a as C,d as w,i as te,n as ne}from"./display-CketawEF.js";import{o as T,r as E}from"./dist-DV2tZ2fq.js";import{t as D}from"./dist-CoUvwLTy.js";import{i as O,n as k}from"./dist-CV3sgFiv.js";import{t as A}from"./dist-C5t_OxI0.js";import{t as j}from"./dist-1x-LE9Uk.js";import{a as M,i as N,n as re,o as ie,r as ae,s as oe,t as P}from"./es2015-DcavLcQ0.js";import{a as F,i as se,n as I,r as L,t as R}from"./dist-Ca3ujqG_.js";import{t as z}from"./badge-Dj6PREuI.js";import{t as B}from"./button-Cqpufqam.js";import{t as ce}from"./copy-button-DI7b8A7_.js";import{B as le,H as V,P as ue,S as H,U,V as W,d as de,g as fe,h as pe,l as me,m as he,p as ge,u as _e,x as G}from"./index-Ba2RDJaA.js";import{n as ve,t as ye}from"./card-OmDF73CE.js";import{n as be,t as xe}from"./branch-state-badge-CdYg2HYc.js";var K=e(s(),1),q=d(),J=`Popover`,[Se,Ce]=O(J,[F]),Y=F(),[we,X]=Se(J),Te=e=>{let{__scopePopover:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!1}=e,s=Y(t),c=K.useRef(null),[l,u]=K.useState(!1),[d,f]=A({prop:r,defaultProp:i??!1,onChange:a,caller:J});return(0,q.jsx)(se,{...s,children:(0,q.jsx)(we,{scope:t,contentId:oe(),triggerRef:c,open:d,onOpenChange:f,onOpenToggle:K.useCallback(()=>f(e=>!e),[f]),hasCustomAnchor:l,onCustomAnchorAdd:K.useCallback(()=>u(!0),[]),onCustomAnchorRemove:K.useCallback(()=>u(!1),[]),modal:o,children:n})})};Te.displayName=J;var Ee=`PopoverAnchor`,De=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Ee,n),a=Y(n),{onCustomAnchorAdd:o,onCustomAnchorRemove:s}=i;return K.useEffect(()=>(o(),()=>s()),[o,s]),(0,q.jsx)(R,{...a,...r,ref:t})});De.displayName=Ee;var Oe=`PopoverTrigger`,ke=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Oe,n),a=Y(n),o=T(t,i.triggerRef),s=(0,q.jsx)(D.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":i.open,"aria-controls":i.contentId,"data-state":Ve(i.open),...r,ref:o,onClick:k(e.onClick,i.onOpenToggle)});return i.hasCustomAnchor?s:(0,q.jsx)(R,{asChild:!0,...a,children:s})});ke.displayName=Oe;var Z=`PopoverPortal`,[Ae,je]=Se(Z,{forceMount:void 0}),Me=e=>{let{__scopePopover:t,forceMount:n,children:r,container:i}=e,a=X(Z,t);return(0,q.jsx)(Ae,{scope:t,forceMount:n,children:(0,q.jsx)(j,{present:n||a.open,children:(0,q.jsx)(N,{asChild:!0,container:i,children:r})})})};Me.displayName=Z;var Q=`PopoverContent`,Ne=K.forwardRef((e,t)=>{let n=je(Q,e.__scopePopover),{forceMount:r=n.forceMount,...i}=e,a=X(Q,e.__scopePopover);return(0,q.jsx)(j,{present:r||a.open,children:a.modal?(0,q.jsx)(Fe,{...i,ref:t}):(0,q.jsx)(Ie,{...i,ref:t})})});Ne.displayName=Q;var Pe=E(`PopoverContent.RemoveScroll`),Fe=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(null),i=T(t,r),a=K.useRef(!1);return K.useEffect(()=>{let e=r.current;if(e)return P(e)},[]),(0,q.jsx)(re,{as:Pe,allowPinchZoom:!0,children:(0,q.jsx)(Le,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:k(e.onCloseAutoFocus,e=>{e.preventDefault(),a.current||n.triggerRef.current?.focus()}),onPointerDownOutside:k(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;a.current=t.button===2||n},{checkForDefaultPrevented:!1}),onFocusOutside:k(e.onFocusOutside,e=>e.preventDefault(),{checkForDefaultPrevented:!1})})})}),Ie=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(!1),i=K.useRef(!1);return(0,q.jsx)(Le,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),Le=K.forwardRef((e,t)=>{let{__scopePopover:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:a,disableOutsidePointerEvents:o,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onInteractOutside:u,...d}=e,f=X(Q,n),p=Y(n);return ae(),(0,q.jsx)(M,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:a,children:(0,q.jsx)(ie,{asChild:!0,disableOutsidePointerEvents:o,onInteractOutside:u,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onDismiss:()=>f.onOpenChange(!1),children:(0,q.jsx)(L,{"data-state":Ve(f.open),role:`dialog`,id:f.contentId,...p,...d,ref:t,style:{...d.style,"--radix-popover-content-transform-origin":`var(--radix-popper-transform-origin)`,"--radix-popover-content-available-width":`var(--radix-popper-available-width)`,"--radix-popover-content-available-height":`var(--radix-popper-available-height)`,"--radix-popover-trigger-width":`var(--radix-popper-anchor-width)`,"--radix-popover-trigger-height":`var(--radix-popper-anchor-height)`}})})})}),$=`PopoverClose`,Re=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X($,n);return(0,q.jsx)(D.button,{type:`button`,...r,ref:t,onClick:k(e.onClick,()=>i.onOpenChange(!1))})});Re.displayName=$;var ze=`PopoverArrow`,Be=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=Y(n);return(0,q.jsx)(I,{...i,...r,ref:t})});Be.displayName=ze;function Ve(e){return e?`open`:`closed`}var He=Te,Ue=ke,We=Me,Ge=Ne;function Ke({...e}){return(0,q.jsx)(He,{"data-slot":`popover`,...e})}function qe({...e}){return(0,q.jsx)(Ue,{"data-slot":`popover-trigger`,...e})}function Je({className:e,align:t=`center`,sideOffset:n=4,...r}){return(0,q.jsx)(We,{children:(0,q.jsx)(Ge,{"data-slot":`popover-content`,align:t,sideOffset:n,className:c(`z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r})})}function Ye({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-header`,className:c(`flex flex-col gap-0.5 text-sm`,e),...t})}function Xe({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-title`,className:c(`font-medium`,e),...t})}function Ze({agent:e,childAgents:r=[],parentAgent:a}){let s=ee();v(e=>e.set);let c=v(e=>e.switchView),l=v(e=>e.openInboxThread),d=v(e=>e.openAgentDetail),m=v(e=>e.openPairInvite),T=v(e=>e.openRename),E=v(e=>e.openConfirm),D=v(e=>e.doAgentAction),O=v(e=>e.doDeleteAgent),k=v(e=>e.openFilesForAgent),A=v(e=>e.workspaceAction),j=v(e=>e.openWorkspaceFocus),{terminalOpen:M,terminalTarget:N,terminalOpening:re,openTerminal:ie,closeTerminal:ae}=be(e.id),oe=le(),P=te(s,e,ue(e),oe[e.id]||null),F=C(e,`shutdown`),se=C(e,`compact`),I=C(e,`clearContext`),L=!!(e.providerCapabilities?.terminal?.live?.read||e.providerCapabilities?.terminal?.attach?.create),R=!!(e.providerCapabilities?.terminal?.live?.write||e.providerCapabilities?.model?.provider===`claude`),V=ne(e),U=a?w(a):e.spawnedBy,W=r.length>0;return(0,q.jsxs)(q.Fragment,{children:[(0,q.jsx)(ye,{className:`group hover:border-zinc-600 transition-colors cursor-pointer`,onClick:()=>d(e),children:(0,q.jsxs)(ve,{className:`p-4`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2.5 mb-2.5`,children:[(0,q.jsx)(H,{agent:e,now:s,className:`mt-0.5`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,q.jsx)(`div`,{className:`text-sm font-medium truncate`,children:w(e)}),(0,q.jsx)(`div`,{className:`text-xs mt-0.5 ${y(P.tone)}`,children:P.label}),typeof e.meta?.cwd==`string`&&e.meta.cwd&&(0,q.jsx)(`button`,{type:`button`,className:`mt-0.5 block max-w-full truncate font-mono text-[10px] text-muted-foreground hover:text-foreground`,title:`Open ${e.meta.cwd} in Files`,onClick:t=>{t.stopPropagation(),k(e)},children:S(e.meta.cwd)})]}),e.branchState&&e.branchWorkspaceId&&(0,q.jsx)(xe,{state:e.branchState,className:`mt-0.5 shrink-0`,onClick:t=>{t.stopPropagation(),j(e.branchWorkspaceId)}})]}),P.badges.length>0&&(0,q.jsx)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:P.badges.map(e=>(0,q.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded-full font-medium ${e.className}`,children:e.label},e.label))}),(W||e.spawnedBy&&U)&&(0,q.jsxs)(`div`,{className:`mb-2 flex flex-wrap gap-1.5`,onClick:e=>e.stopPropagation(),children:[W&&(0,q.jsxs)(Ke,{children:[(0,q.jsx)(qe,{asChild:!0,children:(0,q.jsxs)(`button`,{type:`button`,className:`inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border border-sky-500/30 bg-sky-500/10 px-2 text-[10px] font-medium text-sky-300 transition-colors hover:bg-sky-500/15`,title:`${r.length} live child ${r.length===1?`agent`:`agents`}`,children:[(0,q.jsx)(h,{className:`h-3 w-3`}),(0,q.jsx)(`span`,{children:r.length}),(0,q.jsx)(`span`,{className:`flex -space-x-1`,children:r.slice(0,4).map(e=>(0,q.jsx)(H,{agent:e,now:s,className:`h-2 w-2 ring-1 ring-background`},e.id))})]})}),(0,q.jsxs)(Je,{align:`start`,className:`w-80`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(Ye,{children:(0,q.jsx)(Xe,{children:`Team`})}),(0,q.jsx)(`div`,{className:`space-y-1.5`,children:r.map(e=>(0,q.jsxs)(`div`,{className:`rounded-md border border-border bg-card/60 p-2`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,q.jsx)(H,{agent:e,now:s,className:`mt-1`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,q.jsx)(`div`,{className:`truncate text-xs font-medium`,children:w(e)}),(0,q.jsx)(`div`,{className:`truncate text-[10px] text-muted-foreground`,children:e.providerCapabilities?.model?.alias||e.providerCapabilities?.model?.id||e.providerCapabilities?.model?.provider||e.status})]}),(0,q.jsx)(z,{variant:`outline`,className:`h-4 px-1.5 text-[9px]`,children:e.status})]}),(0,q.jsxs)(`div`,{className:`mt-2 flex gap-1`,children:[(0,q.jsx)(B,{size:`xs`,variant:`outline`,onClick:()=>d(e),children:`Open`}),(0,q.jsxs)(B,{size:`xs`,variant:`ghost`,onClick:()=>{l(e.id),c(`chat`)},children:[(0,q.jsx)(o,{className:`h-3 w-3`}),`Chat`]})]})]},e.id))})]})]}),e.spawnedBy&&U&&(0,q.jsx)(z,{variant:`outline`,className:`max-w-full text-[10px] text-muted-foreground`,children:(0,q.jsxs)(`span`,{className:`truncate`,children:[`spawned by `,U]})})]}),(e.tags.length>0||b(e.capabilities).length>0)&&(0,q.jsxs)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:[e.tags.slice(0,3).map(e=>(0,q.jsx)(z,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e)),b(e.capabilities).slice(0,2).map(e=>(0,q.jsx)(z,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))]}),(0,q.jsxs)(`div`,{className:`space-y-2 mb-2`,children:[(0,q.jsx)(pe,{agent:e}),(0,q.jsx)(he,{agent:e,compact:!0}),(0,q.jsx)(fe,{agent:e})]}),(0,q.jsx)(`div`,{className:`text-xs text-muted-foreground`,children:e.id!==`user`&&e.id!==`system`&&x(s,e.lastSeen)}),(0,q.jsxs)(`div`,{className:`flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(ce,{value:e.id,label:`Copy agent ID`,size:`icon`,className:`h-7 w-7`,iconClassName:`w-3 h-3`}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Chat`,onClick:()=>{l(e.id),c(`chat`)},children:(0,q.jsx)(o,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Pair`,onClick:()=>m(e.id),children:(0,q.jsx)(_,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Rename`,onClick:()=>T(e),children:(0,q.jsx)(n,{className:`w-3 h-3`})}),L&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Terminal`,disabled:re,onClick:()=>void ie(),children:(0,q.jsx)(p,{className:`w-3 h-3`})}),(e.branchState===`changes`||e.branchState===`idle`)&&e.branchWorkspaceId&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-amber-400 hover:text-amber-300`,title:`Mark workspace ready — hand off to the auto-merge`,onClick:()=>E(`Mark Workspace Ready`,`Mark ${w(e)}'s branch ready to land? The relay auto-merge will rebase and land it.`,()=>A(e.branchWorkspaceId,`ready`)),children:(0,q.jsx)(t,{className:`w-3 h-3`})}),se&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Compact`,onClick:()=>E(`Compact Agent`,`Compact context for ${w(e)}?`,()=>D(e,`compact`,`agents-grid`)),children:(0,q.jsx)(i,{className:`w-3 h-3`})}),I&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Clear Context`,onClick:()=>E(`Clear Context`,`Clear context for ${w(e)}?`,()=>D(e,`clearContext`,`agents-grid`)),children:(0,q.jsx)(f,{className:`w-3 h-3`})}),F&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Shutdown`,onClick:()=>E(`Shutdown Agent`,`Shutdown ${w(e)}?`,()=>D(e,`shutdown`,`agents-grid`)),children:(0,q.jsx)(u,{className:`w-3 h-3`})}),V&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Forget stale agent`,onClick:()=>E(`Forget Agent`,`Forget ${w(e)}?`,()=>O(e.id)),children:(0,q.jsx)(g,{className:`w-3 h-3`})})]})]})}),N&&(0,q.jsx)(ge,{open:M,onOpenChange:ae,orchestratorId:N.orchestratorId,session:N.session,interactive:N.interactive||R})]})}function Qe(){let e=v(e=>e.agentPresetFilter),t=v(e=>e.agentStatusFilter),n=v(e=>e.agentTagFilter),i=v(e=>e.agentHostFilter),o=v(e=>e.agentSort),s=v(e=>e.agentSortDir),c=v(e=>e.showOffline),u=v(e=>e.set),d=v(e=>e.openAgentSpawn),f=v(e=>e.agentsById),p=ee(),h=W(),g=U(),_=V(),y=me(h,p),b=de(h,p),x=e||t||n||i;function S(){u({agentPresetFilter:``,agentStatusFilter:``,agentTagFilter:``,agentHostFilter:``})}function C(){u({agentSortDir:s===`asc`?`desc`:`asc`})}return(0,q.jsxs)(`div`,{className:`space-y-4`,children:[(0,q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,q.jsxs)(`select`,{value:e,onChange:e=>u({agentPresetFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`All agents`}),(0,q.jsx)(`option`,{value:`active`,children:`Active`}),(0,q.jsx)(`option`,{value:`claude`,children:`Claude`}),(0,q.jsx)(`option`,{value:`codex`,children:`Codex`}),(0,q.jsx)(`option`,{value:`paired`,children:`Paired`}),(0,q.jsx)(`option`,{value:`unpaired`,children:`Unpaired`}),(0,q.jsx)(`option`,{value:`offline_stale`,children:`Offline / Stale`})]}),(0,q.jsxs)(`select`,{value:t,onChange:e=>u({agentStatusFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any status`}),(0,q.jsx)(`option`,{value:`online`,children:`Online`}),(0,q.jsx)(`option`,{value:`idle`,children:`Idle`}),(0,q.jsx)(`option`,{value:`busy`,children:`Busy`}),(0,q.jsx)(`option`,{value:`offline`,children:`Offline`}),(0,q.jsx)(`option`,{value:`starting`,children:`Starting`})]}),g.length>0&&(0,q.jsxs)(`select`,{value:n,onChange:e=>u({agentTagFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any tag`}),g.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),_.length>1&&(0,q.jsxs)(`select`,{value:i,onChange:e=>u({agentHostFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any host`}),_.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),(0,q.jsxs)(`select`,{value:o,onChange:e=>u({agentSort:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:`status`,children:`Sort: Status`}),(0,q.jsx)(`option`,{value:`name`,children:`Sort: Name`}),(0,q.jsx)(`option`,{value:`lastSeen`,children:`Sort: Last seen`}),(0,q.jsx)(`option`,{value:`created`,children:`Sort: Created`})]}),(0,q.jsx)(B,{size:`icon`,variant:`outline`,className:`h-8 w-8`,title:`Toggle sort direction`,onClick:C,children:s===`asc`?(0,q.jsx)(r,{className:`w-3.5 h-3.5`}):(0,q.jsx)(l,{className:`w-3.5 h-3.5`})}),(0,q.jsxs)(`label`,{className:`flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer select-none`,children:[(0,q.jsx)(`input`,{type:`checkbox`,checked:c,onChange:e=>u({showOffline:e.target.checked}),className:`rounded`}),`Show offline`]}),x&&(0,q.jsxs)(B,{size:`sm`,variant:`ghost`,className:`h-8 text-muted-foreground hover:text-foreground gap-1`,onClick:S,children:[(0,q.jsx)(m,{className:`w-3 h-3`}),`Clear filters`]}),(0,q.jsx)(`div`,{className:`ml-auto`,children:(0,q.jsxs)(B,{size:`sm`,onClick:d,className:`gap-1`,children:[(0,q.jsx)(a,{className:`w-3.5 h-3.5`}),`Spawn`]})})]}),b.length===0?(0,q.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No agents match the current filters.`}):(0,q.jsx)(`div`,{className:`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3`,children:b.map(e=>(0,q.jsx)(Ze,{agent:e,parentAgent:e.spawnedBy?f[e.spawnedBy]??null:null,childAgents:_e(y.get(e.id)??[])},e.id))})]})}export{Qe as AgentsView};
2
- //# sourceMappingURL=agents-DZBM5dp_.js.map