agent-relay-server 0.119.6 → 0.119.9

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 (85) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-Azda2GIf.js → MessageBubble-U5sX3SNZ.js} +2 -2
  4. package/public/assets/{MessageBubble-Azda2GIf.js.map → MessageBubble-U5sX3SNZ.js.map} +1 -1
  5. package/public/assets/PlanGraphPanel-Bi2gvXeE.js +2 -0
  6. package/public/assets/PlanGraphPanel-Bi2gvXeE.js.map +1 -0
  7. package/public/assets/{activity-B0hz4XPy.js → activity-Cwkz51pG.js} +2 -2
  8. package/public/assets/{activity-B0hz4XPy.js.map → activity-Cwkz51pG.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BF0oSOTq.js → agent-profiles-BgddVqRp.js} +2 -2
  10. package/public/assets/{agent-profiles-BF0oSOTq.js.map → agent-profiles-BgddVqRp.js.map} +1 -1
  11. package/public/assets/{agents-CW4--j3Q.js → agents-CiGYBw0h.js} +2 -2
  12. package/public/assets/{agents-CW4--j3Q.js.map → agents-CiGYBw0h.js.map} +1 -1
  13. package/public/assets/{automation-S6_bQ2t-.js → automation-BBJB4RaE.js} +2 -2
  14. package/public/assets/{automation-S6_bQ2t-.js.map → automation-BBJB4RaE.js.map} +1 -1
  15. package/public/assets/{chat-hoFhhvgy.js → chat-DS578gZy.js} +2 -2
  16. package/public/assets/{chat-hoFhhvgy.js.map → chat-DS578gZy.js.map} +1 -1
  17. package/public/assets/{formatted-body-BHm145gP.js → formatted-body-DYUVqWym.js} +3 -3
  18. package/public/assets/{formatted-body-BHm145gP.js.map → formatted-body-DYUVqWym.js.map} +1 -1
  19. package/public/assets/{formatted-body-impl-CvE31j9F.js → formatted-body-impl-DdcyxVO0.js} +2 -2
  20. package/public/assets/{formatted-body-impl-CvE31j9F.js.map → formatted-body-impl-DdcyxVO0.js.map} +1 -1
  21. package/public/assets/{index-TamtghSr.js → index-CqcAqQKW.js} +6 -6
  22. package/public/assets/{index-TamtghSr.js.map → index-CqcAqQKW.js.map} +1 -1
  23. package/public/assets/{insights-Ci9ftVfl.js → insights-BunHbsTA.js} +2 -2
  24. package/public/assets/{insights-Ci9ftVfl.js.map → insights-BunHbsTA.js.map} +1 -1
  25. package/public/assets/{maintenance-bVhNbr33.js → maintenance-cICBVcpJ.js} +2 -2
  26. package/public/assets/{maintenance-bVhNbr33.js.map → maintenance-cICBVcpJ.js.map} +1 -1
  27. package/public/assets/{managed-agents-D5HO4dhI.js → managed-agents-BK9_Gi-M.js} +2 -2
  28. package/public/assets/{managed-agents-D5HO4dhI.js.map → managed-agents-BK9_Gi-M.js.map} +1 -1
  29. package/public/assets/{markdown-preview-impl-DO-p83dz.js → markdown-preview-impl-EZfcTHG9.js} +2 -2
  30. package/public/assets/{markdown-preview-impl-DO-p83dz.js.map → markdown-preview-impl-EZfcTHG9.js.map} +1 -1
  31. package/public/assets/{memory-D72o8umQ.js → memory-Vi-8HvK3.js} +2 -2
  32. package/public/assets/{memory-D72o8umQ.js.map → memory-Vi-8HvK3.js.map} +1 -1
  33. package/public/assets/{messages-5L_Bc_9w.js → messages-DScIiBMI.js} +2 -2
  34. package/public/assets/{messages-5L_Bc_9w.js.map → messages-DScIiBMI.js.map} +1 -1
  35. package/public/assets/{orchestrators-CnfgkQpl.js → orchestrators-DLogldXx.js} +2 -2
  36. package/public/assets/{orchestrators-CnfgkQpl.js.map → orchestrators-DLogldXx.js.map} +1 -1
  37. package/public/assets/{overview-Bu39NIyR.js → overview-B_RQAQR8.js} +2 -2
  38. package/public/assets/{overview-Bu39NIyR.js.map → overview-B_RQAQR8.js.map} +1 -1
  39. package/public/assets/{pairs-WuamP5W5.js → pairs-CSiPQ7E2.js} +2 -2
  40. package/public/assets/{pairs-WuamP5W5.js.map → pairs-CSiPQ7E2.js.map} +1 -1
  41. package/public/assets/{prompt-settings-BCrQreU6.js → prompt-settings-DsDi_Xpd.js} +2 -2
  42. package/public/assets/{prompt-settings-BCrQreU6.js.map → prompt-settings-DsDi_Xpd.js.map} +1 -1
  43. package/public/assets/{registry-kHKn0xwg.js → registry-DHFUWIdb.js} +2 -2
  44. package/public/assets/{registry-kHKn0xwg.js.map → registry-DHFUWIdb.js.map} +1 -1
  45. package/public/assets/{security-Dk5t3r_K.js → security-D2nHkJH6.js} +2 -2
  46. package/public/assets/{security-Dk5t3r_K.js.map → security-D2nHkJH6.js.map} +1 -1
  47. package/public/assets/{select-BdsZQfU2.js → select-BjNcommA.js} +2 -2
  48. package/public/assets/{select-BdsZQfU2.js.map → select-BjNcommA.js.map} +1 -1
  49. package/public/assets/{settings-EpjzQHnZ.js → settings-DiUEq54n.js} +2 -2
  50. package/public/assets/{settings-EpjzQHnZ.js.map → settings-DiUEq54n.js.map} +1 -1
  51. package/public/assets/{tasks-CedMQHbg.js → tasks-Dx8GVQiR.js} +2 -2
  52. package/public/assets/{tasks-CedMQHbg.js.map → tasks-Dx8GVQiR.js.map} +1 -1
  53. package/public/assets/{teams-BlHBo5Qt.js → teams-DQtf7bha.js} +2 -2
  54. package/public/assets/{teams-BlHBo5Qt.js.map → teams-DQtf7bha.js.map} +1 -1
  55. package/public/assets/{terminal-viewer-impl-gGqYpsjq.js → terminal-viewer-impl-D5rhXV4u.js} +2 -2
  56. package/public/assets/{terminal-viewer-impl-gGqYpsjq.js.map → terminal-viewer-impl-D5rhXV4u.js.map} +1 -1
  57. package/public/assets/types-uVOXgvMT.js.map +1 -1
  58. package/public/assets/{work-queue-D7opMqtU.js → work-queue-BfWNDp7U.js} +2 -2
  59. package/public/assets/{work-queue-D7opMqtU.js.map → work-queue-BfWNDp7U.js.map} +1 -1
  60. package/public/index.html +2 -2
  61. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  62. package/src/commands-db.ts +72 -20
  63. package/src/db/index.ts +1 -0
  64. package/src/db/migrations.ts +7 -0
  65. package/src/db/orchestrators.ts +1 -1
  66. package/src/db/provider-quotas.ts +2 -34
  67. package/src/db/schema.ts +2 -0
  68. package/src/db/work-evidence.ts +38 -0
  69. package/src/db/workspaces.ts +48 -3
  70. package/src/lifecycle-manager.ts +2 -4
  71. package/src/maintenance/jobs.ts +10 -1
  72. package/src/maintenance/workspaces.ts +71 -13
  73. package/src/routes/commands.ts +20 -4
  74. package/src/routes/workspaces.ts +6 -11
  75. package/src/services/shutdown-agent.ts +6 -1
  76. package/src/services/spawn-agent.ts +50 -7
  77. package/src/services/transient-agent-reaper.ts +82 -23
  78. package/src/workspace-actions.ts +26 -16
  79. package/src/workspace-auto-merge.ts +95 -13
  80. package/src/workspace-host.ts +55 -0
  81. package/src/workspace-merge.ts +3 -4
  82. package/src/workspace-orphans.ts +78 -55
  83. package/src/workspace-probe.ts +2 -3
  84. package/public/assets/PlanGraphPanel-B9TN_KQr.js +0 -2
  85. package/public/assets/PlanGraphPanel-B9TN_KQr.js.map +0 -1
@@ -10,12 +10,14 @@
10
10
  // the Command and the caller emits via its own `emitCommand` (kept module-local).
11
11
 
12
12
  import { createCommand } from "./commands-db";
13
- import { isPathWithinBase } from "./utils";
13
+ import { workspaceOwnerOrchestrator } from "./workspace-host";
14
14
  import {
15
15
  createActivityEvent,
16
16
  getWorkspace,
17
+ claimWorkspaceMetadata,
17
18
  listOrchestrators,
18
19
  patchWorkspaceMetadata,
20
+ releaseWorkspaceClaimMetadata,
19
21
  updateWorkspaceStatus,
20
22
  } from "./db";
21
23
  import { emitActivityEvent } from "./sse";
@@ -148,8 +150,17 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
148
150
  // so deterministic landing can't race a steward mid-validation. No status change.
149
151
  if (action === "claim" || action === "release-claim") {
150
152
  const release = action === "release-claim";
151
- const updated = patchWorkspaceMetadata(workspace.id, claimMetadataPatch(release, agentId ?? "steward", input.purpose));
152
- if (!updated) return { ok: false, httpStatus: 404, error: "workspace not found" };
153
+ const claimant = agentId ?? "steward";
154
+ const result = release
155
+ ? releaseWorkspaceClaimMetadata(workspace.id, claimant, claimMetadataPatch(true), Date.now())
156
+ : claimWorkspaceMetadata(workspace.id, claimant, claimMetadataPatch(false, claimant, input.purpose), Date.now());
157
+ if (!result.ok) {
158
+ if (result.reason === "not-found") return { ok: false, httpStatus: 404, error: "workspace not found" };
159
+ const active = result.workspace ? workspaceActiveClaim(result.workspace) : null;
160
+ const owner = active?.by ? ` by ${active.by}` : "";
161
+ return { ok: false, httpStatus: 409, error: `workspace ${workspace.id} is already claimed${owner}` };
162
+ }
163
+ const updated = result.workspace;
153
164
  recordWorkspaceAudit({
154
165
  action,
155
166
  workspace: updated,
@@ -231,6 +242,14 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
231
242
  };
232
243
  }
233
244
 
245
+ let command: Command | undefined;
246
+ if (requiresCommand) {
247
+ // Only `cleanup` reaches here — `merge` returned early via the shared helper.
248
+ const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
249
+ if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
250
+ command = built.command;
251
+ }
252
+
234
253
  const updated = updateWorkspaceStatus(workspace.id, nextStatus, {
235
254
  ...metadata,
236
255
  ...(detail ? { detail } : {}),
@@ -250,13 +269,6 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
250
269
  // + lease-serialized; a non-landable row is a harmless no-op (autoMergeForRepo re-checks).
251
270
  if (READY_TO_LAND_STATUSES.has(nextStatus)) scheduleRepoMerge(updated.repoRoot);
252
271
 
253
- let command: Command | undefined;
254
- if (requiresCommand) {
255
- // Only `cleanup` reaches here — `merge` returned early via the shared helper.
256
- const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
257
- if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
258
- command = built.command;
259
- }
260
272
  recordWorkspaceAudit({
261
273
  action,
262
274
  workspace: updated,
@@ -289,9 +301,8 @@ export function buildWorkspaceCleanupCommand(
289
301
  error: `workspace ${workspace.id} owner is still online; pass force:true to clean it up intentionally`,
290
302
  };
291
303
  }
292
- const owners = listOrchestrators().filter((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
293
- const owner = owners.find((candidate) => candidate.status === "online") ?? owners[0];
294
- if (!owner) return { ok: false, status: 409, error: "no orchestrator owns this workspace path; use DELETE /api/workspaces/:id to purge the record" };
304
+ const owner = workspaceOwnerOrchestrator(workspace);
305
+ if (!owner) return { ok: false, status: 409, error: "no online orchestrator owns this workspace path; retry when the host is online or use DELETE /api/workspaces/:id to purge the record" };
295
306
  const command = createCommand({
296
307
  type: "workspace.cleanup",
297
308
  source: "system",
@@ -308,7 +319,7 @@ export function buildWorkspaceCleanupCommand(
308
319
  requestedBy,
309
320
  requestedAt: Date.now(),
310
321
  deleteBranch: true,
311
- queued: owner.status !== "online",
322
+ queued: false,
312
323
  },
313
324
  });
314
325
  return { ok: true, command };
@@ -321,8 +332,7 @@ function buildWorkspaceDepsRefreshCommand(
321
332
  requestedBy: string,
322
333
  checkOnly: boolean,
323
334
  ): { ok: true; command: Command } | { ok: false; status: number; error: string } {
324
- const owners = listOrchestrators().filter((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
325
- const owner = owners.find((candidate) => candidate.status === "online") ?? owners[0];
335
+ const owner = workspaceOwnerOrchestrator(workspace, listOrchestrators(), { onlineOnly: false });
326
336
  if (!owner) return { ok: false, status: 409, error: "no online orchestrator owns this workspace path" };
327
337
  const command = createCommand({
328
338
  type: "workspace.deps-refresh",
@@ -154,6 +154,8 @@ export const MAX_NO_PROGRESS_MERGE_ATTEMPTS = 3;
154
154
 
155
155
  // Don't re-escalate a backed-off lane more than once per this window.
156
156
  const NO_PROGRESS_ESCALATION_COOLDOWN_MS = 30 * 60 * 1000;
157
+ export const AUTO_MERGE_ERROR_BACKOFF_MS = 3_000;
158
+ export const AUTO_MERGE_WATCHDOG_STALE_MS = 5 * 60 * 1000;
157
159
 
158
160
  /** The recorded count of consecutive no-progress merge attempts for a workspace (#638). */
159
161
  export function mergeNoProgressCount(metadata: Record<string, unknown>): number {
@@ -377,30 +379,105 @@ async function landCandidates(
377
379
  // in flight for the repo just flags a rerun, so concurrent readies don't fan out
378
380
  // redundant host probes. Errors are swallowed: the runOnStart backstop is the
379
381
  // self-healer for anything a dropped run strands.
380
- const repoMergeRunning = new Map<string, { rerun: boolean }>();
382
+ type RepoMergeRunState = {
383
+ rerun: boolean;
384
+ lastProgressAt: number;
385
+ };
386
+
387
+ const repoMergeRunning = new Map<string, RepoMergeRunState>();
388
+ const repoMergeLastCompletedAt = new Map<string, number>();
389
+ let autoMergeWatchdogStartedAt = Date.now();
390
+
391
+ function errMessage(error: unknown): string {
392
+ if (error instanceof Error) return `${error.name}: ${error.message}`;
393
+ return String(error);
394
+ }
395
+
396
+ function landableWorkspaces(repoRoot?: string): WorkspaceRecord[] {
397
+ return listWorkspaces(repoRoot ? { repoRoot } : undefined).filter(
398
+ (ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && READY_TO_LAND_STATUSES.has(ws.status),
399
+ );
400
+ }
401
+
402
+ function noteAutoMergeCycleCompleted(repoRoot: string): void {
403
+ const completedAt = Date.now();
404
+ repoMergeLastCompletedAt.set(repoRoot, completedAt);
405
+ const state = repoMergeRunning.get(repoRoot);
406
+ if (state) state.lastProgressAt = completedAt;
407
+ }
408
+
409
+ async function sleep(ms: number): Promise<void> {
410
+ await new Promise<void>((resolve) => {
411
+ const timer = setTimeout(resolve, ms);
412
+ timer.unref?.();
413
+ });
414
+ }
381
415
 
382
416
  export function scheduleRepoMerge(repoRoot: string | undefined): void {
383
417
  if (!repoRoot) return;
384
418
  if (!workspaceAutoMergeEnabled()) return;
385
419
  const existing = repoMergeRunning.get(repoRoot);
386
420
  if (existing) { existing.rerun = true; return; }
387
- const state = { rerun: false };
421
+ const state: RepoMergeRunState = { rerun: false, lastProgressAt: Date.now() };
388
422
  repoMergeRunning.set(repoRoot, state);
389
423
  void (async () => {
424
+ let retriedAfterError = false;
390
425
  try {
391
426
  do {
392
427
  state.rerun = false;
393
- await autoMergeForRepo(repoRoot);
428
+ state.lastProgressAt = Date.now();
429
+ try {
430
+ await autoMergeForRepo(repoRoot);
431
+ } catch (err) {
432
+ console.warn(`[workspace-auto-merge] Repo auto-merge cycle failed for ${repoRoot}: ${errMessage(err)}`);
433
+ if (!retriedAfterError) {
434
+ retriedAfterError = true;
435
+ await sleep(AUTO_MERGE_ERROR_BACKOFF_MS);
436
+ state.rerun = true;
437
+ }
438
+ }
394
439
  } while (state.rerun);
395
440
  } catch {
396
441
  // Swallow — the periodic backstop (runOnStart + interval) is the self-healer.
397
442
  } finally {
398
- repoMergeRunning.delete(repoRoot);
443
+ if (repoMergeRunning.get(repoRoot) === state) repoMergeRunning.delete(repoRoot);
399
444
  }
400
445
  })();
401
446
  }
402
447
 
403
- export function resetRepoMergeSchedulerForTests(): void { repoMergeRunning.clear(); }
448
+ export function resetRepoMergeSchedulerForTests(): void {
449
+ repoMergeRunning.clear();
450
+ repoMergeLastCompletedAt.clear();
451
+ autoMergeWatchdogStartedAt = Date.now();
452
+ }
453
+
454
+ export function runAutoMergeWatchdog(
455
+ opts: { now?: number; staleMs?: number; log?: (message: string) => void } = {},
456
+ ): Record<string, unknown> {
457
+ const now = opts.now ?? Date.now();
458
+ const staleMs = opts.staleMs ?? AUTO_MERGE_WATCHDOG_STALE_MS;
459
+ const ready = landableWorkspaces();
460
+ const repoRoots = [...new Set(ready.map((ws) => ws.repoRoot).filter(Boolean))];
461
+ const repoStaleForMs = Object.fromEntries(repoRoots.map((repoRoot) => {
462
+ const state = repoMergeRunning.get(repoRoot);
463
+ const lastProgressAt = state?.lastProgressAt ?? repoMergeLastCompletedAt.get(repoRoot) ?? autoMergeWatchdogStartedAt;
464
+ return [repoRoot, now - lastProgressAt];
465
+ }));
466
+ const staleForMs = Object.values(repoStaleForMs).reduce((max, value) => Math.max(max, value), 0);
467
+ if (!ready.length) return { readyCount: 0, staleForMs, repoStaleForMs, restartedRepoRoots: [] };
468
+
469
+ const restartedRepoRoots = repoRoots.filter((repoRoot) => repoStaleForMs[repoRoot]! >= staleMs);
470
+ if (!restartedRepoRoots.length) return { readyCount: ready.length, staleForMs, repoStaleForMs, restartedRepoRoots: [] };
471
+
472
+ const log = opts.log ?? console.warn;
473
+ log(`[workspace-auto-merge] Watchdog: no auto-merge cycle completed for ${staleForMs}ms while ${ready.length} ready workspace(s) exist; restarting ${restartedRepoRoots.length} repo merge loop(s)`);
474
+ for (const repoRoot of restartedRepoRoots) {
475
+ const existing = repoMergeRunning.get(repoRoot);
476
+ if (existing) existing.rerun = true;
477
+ else scheduleRepoMerge(repoRoot);
478
+ }
479
+ return { readyCount: ready.length, staleForMs, repoStaleForMs, restartedRepoRoots };
480
+ }
404
481
 
405
482
  // Drain the landable rows for ONE repo (the event-driven fast path). Scoped to the
406
483
  // repo whose ready/settle event fired so it doesn't re-probe every other repo's worktrees.
@@ -408,10 +485,12 @@ async function autoMergeForRepo(repoRoot: string): Promise<LandSweepResult> {
408
485
  if (!workspaceAutoMergeEnabled()) return emptyLandSweep();
409
486
  const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
410
487
  if (!orchestrators.length) return emptyLandSweep();
411
- const candidates = listWorkspaces({ repoRoot }).filter(
412
- (ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && READY_TO_LAND_STATUSES.has(ws.status),
413
- );
414
- return landCandidates(candidates, orchestrators);
488
+ const candidates = landableWorkspaces(repoRoot);
489
+ try {
490
+ return await landCandidates(candidates, orchestrators);
491
+ } finally {
492
+ noteAutoMergeCycleCompleted(repoRoot);
493
+ }
415
494
  }
416
495
 
417
496
  // Self-healing backstop (#237): the periodic resync (workspace-auto-merge maintenance
@@ -424,10 +503,13 @@ export async function autoMergeCleanFastForwards(): Promise<Record<string, unkno
424
503
  const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.apiUrl);
425
504
  if (!orchestrators.length) return { scanned: 0, skipped: "no online orchestrators" };
426
505
 
427
- const candidates = listWorkspaces().filter(
428
- (ws) => ws.mode === "isolated" && Boolean(ws.worktreePath) && READY_TO_LAND_STATUSES.has(ws.status),
429
- );
506
+ const candidates = landableWorkspaces();
507
+ const repoRoots = [...new Set(candidates.map((ws) => ws.repoRoot).filter(Boolean))];
430
508
  // Spread to a plain Record for the maintenance-job result contract (the named
431
509
  // LandSweepResult interface has no index signature).
432
- return { ...(await landCandidates(candidates, orchestrators)) };
510
+ try {
511
+ return { ...(await landCandidates(candidates, orchestrators)) };
512
+ } finally {
513
+ for (const repoRoot of repoRoots) noteAutoMergeCycleCompleted(repoRoot);
514
+ }
433
515
  }
@@ -0,0 +1,55 @@
1
+ import type { Command, Orchestrator, WorkspaceRecord } from "./types";
2
+ import { getOrchestrator, listOrchestrators } from "./db";
3
+ import { isPathWithinBase } from "./utils";
4
+
5
+ interface WorkspaceHost {
6
+ id: string;
7
+ status?: string;
8
+ apiUrl?: string;
9
+ baseDir?: string;
10
+ agentId?: string;
11
+ }
12
+
13
+ interface WorkspaceHostOptions {
14
+ onlineOnly?: boolean;
15
+ requireApiUrl?: boolean;
16
+ }
17
+
18
+ function hostUsable<T extends WorkspaceHost>(orch: T | undefined, opts: WorkspaceHostOptions): orch is T {
19
+ if (!orch) return false;
20
+ if (opts.onlineOnly !== false && orch.status !== "online") return false;
21
+ if (opts.requireApiUrl && !orch.apiUrl) return false;
22
+ return true;
23
+ }
24
+
25
+ export function workspaceOwnerOrchestrators<T extends WorkspaceHost = Orchestrator>(
26
+ workspace: WorkspaceRecord,
27
+ orchestrators?: T[],
28
+ opts: WorkspaceHostOptions = {},
29
+ ): T[] {
30
+ const hosts = orchestrators ?? (listOrchestrators() as unknown as T[]);
31
+ if (workspace.orchestratorId) {
32
+ const owner = hosts.find((candidate) => candidate.id === workspace.orchestratorId);
33
+ return hostUsable(owner, opts) ? [owner] : [];
34
+ }
35
+ return hosts.filter((candidate) =>
36
+ hostUsable(candidate, opts) && Boolean(candidate.baseDir) && isPathWithinBase(workspace.sourceCwd, candidate.baseDir!),
37
+ );
38
+ }
39
+
40
+ export function workspaceOwnerOrchestrator<T extends WorkspaceHost = Orchestrator>(
41
+ workspace: WorkspaceRecord,
42
+ orchestrators?: T[],
43
+ opts: WorkspaceHostOptions = {},
44
+ ): T | undefined {
45
+ return workspaceOwnerOrchestrators(workspace, orchestrators, opts)[0];
46
+ }
47
+
48
+ export function commandTargetsWorkspaceOwner(command: Command, workspace: WorkspaceRecord): boolean {
49
+ const ownerId = workspace.orchestratorId;
50
+ if (!ownerId) return true;
51
+ const owner = getOrchestrator(ownerId);
52
+ return command.target === ownerId
53
+ || command.target === `orchestrator-${ownerId}`
54
+ || (owner?.agentId ? command.target === owner.agentId : false);
55
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  } from "./types";
23
23
  import { AUTO_MERGE_POLICIES } from "./types";
24
24
  import { isPathWithinBase } from "./utils";
25
+ import { workspaceOwnerOrchestrators } from "./workspace-host";
25
26
 
26
27
  // Re-export so consumers can import the tuple from the strategy home without a
27
28
  // second import hop.
@@ -164,11 +165,9 @@ export function workspaceMergeOrchestrators(
164
165
  workspace: WorkspaceRecord,
165
166
  orchestrators: Orchestrator[] = listOrchestrators(),
166
167
  ): Orchestrator[] {
167
- const candidates = orchestrators.filter((candidate) =>
168
- candidate.status === "online" && isPathWithinBase(workspace.sourceCwd, candidate.baseDir),
169
- );
168
+ const candidates = workspaceOwnerOrchestrators(workspace, orchestrators);
170
169
  const ownerKeys = workspaceOwnerHostKeys(workspace);
171
- if (ownerKeys.size === 0) return candidates;
170
+ if (workspace.orchestratorId || ownerKeys.size === 0) return candidates;
172
171
  return [...candidates].sort((a, b) => {
173
172
  const aOwner = ownerKeys.has(a.id) || ownerKeys.has(a.hostname) || ownerKeys.has(a.agentId) ? 0 : 1;
174
173
  const bOwner = ownerKeys.has(b.id) || ownerKeys.has(b.hostname) || ownerKeys.has(b.agentId) ? 0 : 1;
@@ -28,6 +28,7 @@ import { isPathWithinBase } from "./utils";
28
28
  import { TERMINAL_WORKSPACE_STATUSES, worktreeReapable, type WorktreeReapState } from "./workspace-phase";
29
29
  import { isOwnerAlive, liveAgentCwds, worktreeInUseByLiveAgent } from "./workspace-merge";
30
30
  import { applyWorkspaceAction } from "./workspace-actions";
31
+ import { workspaceOwnerOrchestrator } from "./workspace-host";
31
32
  import { UNLANDED_FLAG_COOLDOWN_MS, orphanGraceMs, orphanWorktreeReapEnabled, relayToken } from "./config";
32
33
 
33
34
  // Don't re-flag the same un-landed orphan every sweep — surface it once, then
@@ -139,6 +140,17 @@ function knownRepoRoots(workspaces: WorkspaceRecord[]): string[] {
139
140
  return [...new Set(workspaces.map((ws) => ws.repoRoot).filter(Boolean))];
140
141
  }
141
142
 
143
+ function repoProbeHosts(repoRoot: string, workspaces: WorkspaceRecord[], orchestrators: OnlineOrchestrator[]): OnlineOrchestrator[] {
144
+ const ownerIds = new Set(
145
+ workspaces
146
+ .filter((ws) => ws.orchestratorId && resolve(ws.repoRoot) === resolve(repoRoot))
147
+ .map((ws) => ws.orchestratorId!),
148
+ );
149
+ if (ownerIds.size > 0) return orchestrators.filter((candidate) => ownerIds.has(candidate.id));
150
+ const legacy = orchestrators.find((candidate) => isPathWithinBase(repoRoot, candidate.baseDir));
151
+ return legacy ? [legacy] : [];
152
+ }
153
+
142
154
  interface CollectOrphansResult {
143
155
  orphans: WorkspaceOrphan[];
144
156
  /** Live isolated rows whose worktree is missing on disk (DB→disk drift). */
@@ -196,64 +208,72 @@ export async function collectWorkspaceOrphans(): Promise<CollectOrphansResult> {
196
208
  // (their worktree lives under a different repo's probe than the one iterating). #278
197
209
  const onDiskAll = new Set<string>();
198
210
  const probedRepoRoots = new Set<string>();
211
+ const onDiskByOwner = new Set<string>();
212
+ const probedOwnerRepoRoots = new Set<string>();
199
213
 
200
214
  for (const repoRoot of knownRepoRoots(all)) {
201
- const orch = orchestrators.find((candidate) => candidate.apiUrl && isPathWithinBase(repoRoot, candidate.baseDir));
202
- if (!orch?.apiUrl) continue;
203
- const probe = await fetchHostProbe(orch.apiUrl, repoRoot);
204
- if (!probe?.worktrees) continue;
205
-
206
- probedRepoRoots.add(resolve(repoRoot));
207
- for (const wt of probe.worktrees) if (wt.path) onDiskAll.add(resolve(wt.path));
208
-
209
- // disk→DB drift: a worktree on disk with no live row → orphan. A worktree with a
210
- // live isolated row is NOT an orphan, but if its owner has died it's the #279
211
- // dead-owner case — surface it for the reaper to evaluate (liveness + grace).
212
- for (const worktree of probe.worktrees) {
213
- if (!worktree.path || resolve(worktree.path) === resolve(repoRoot)) continue;
214
- // Only agent-relay-created worktrees (agent/* branches) are reclaimable —
215
- // never touch a user's own linked worktrees.
216
- if (!worktree.branch?.startsWith("agent/")) continue;
217
- const row = rowsByPath.get(resolve(worktree.path));
218
- if (row && !TERMINAL_WORKSPACE_STATUSES.has(row.status)) {
219
- if (row.mode === "isolated") {
220
- deadOwnerWorkspaces.push({
221
- workspaceId: row.id,
222
- worktreePath: worktree.path,
223
- repoRoot: row.repoRoot,
224
- branch: row.branch ?? worktree.branch,
225
- baseRef: row.baseRef,
226
- baseSha: row.baseSha,
227
- ownerAgentId: row.ownerAgentId,
228
- });
229
- }
230
- continue; // tracked & live — not an orphan
215
+ for (const orch of repoProbeHosts(repoRoot, all, orchestrators)) {
216
+ if (!orch.apiUrl) continue;
217
+ const probe = await fetchHostProbe(orch.apiUrl, repoRoot);
218
+ if (!probe?.worktrees) continue;
219
+
220
+ probedRepoRoots.add(resolve(repoRoot));
221
+ probedOwnerRepoRoots.add(`${orch.id}:${resolve(repoRoot)}`);
222
+ for (const wt of probe.worktrees) {
223
+ if (!wt.path) continue;
224
+ onDiskAll.add(resolve(wt.path));
225
+ onDiskByOwner.add(`${orch.id}:${resolve(wt.path)}`);
231
226
  }
232
227
 
233
- const orphan: WorkspaceOrphan = {
234
- worktreePath: worktree.path,
235
- repoRoot,
236
- branch: worktree.branch,
237
- headSha: worktree.headSha,
238
- baseRef: probe.branch,
239
- hadTerminalRow: Boolean(row),
240
- };
241
- const preview = await fetchWorktreeReapState(orch.apiUrl, worktree.path, probe.branch);
242
- if (preview && !preview.missing && !preview.error) {
243
- const state: WorktreeReapState = {
244
- landed: preview.landed,
245
- ahead: preview.ahead,
246
- unmergedAhead: preview.unmergedAhead,
247
- dirtyCount: preview.dirtyCount,
228
+ // disk→DB drift: a worktree on disk with no live row → orphan. A worktree with a
229
+ // live isolated row is NOT an orphan, but if its owner has died it's the #279
230
+ // dead-owner case — surface it for the reaper to evaluate (liveness + grace).
231
+ for (const worktree of probe.worktrees) {
232
+ if (!worktree.path || resolve(worktree.path) === resolve(repoRoot)) continue;
233
+ // Only agent-relay-created worktrees (agent/* branches) are reclaimable —
234
+ // never touch a user's own linked worktrees.
235
+ if (!worktree.branch?.startsWith("agent/")) continue;
236
+ const row = rowsByPath.get(resolve(worktree.path));
237
+ if (row && !TERMINAL_WORKSPACE_STATUSES.has(row.status)) {
238
+ if (row.mode === "isolated") {
239
+ deadOwnerWorkspaces.push({
240
+ workspaceId: row.id,
241
+ worktreePath: worktree.path,
242
+ repoRoot: row.repoRoot,
243
+ branch: row.branch ?? worktree.branch,
244
+ baseRef: row.baseRef,
245
+ baseSha: row.baseSha,
246
+ ownerAgentId: row.ownerAgentId,
247
+ });
248
+ }
249
+ continue; // tracked & live — not an orphan
250
+ }
251
+
252
+ const orphan: WorkspaceOrphan = {
253
+ worktreePath: worktree.path,
254
+ repoRoot,
255
+ branch: worktree.branch,
256
+ headSha: worktree.headSha,
257
+ baseRef: probe.branch,
258
+ hadTerminalRow: Boolean(row),
248
259
  };
249
- orphan.landed = preview.landed;
250
- orphan.ahead = preview.ahead;
251
- orphan.unmergedAhead = preview.unmergedAhead;
252
- orphan.dirty = (preview.dirtyCount ?? 0) > 0;
253
- orphan.safeToReap = worktreeReapable(state);
260
+ const preview = await fetchWorktreeReapState(orch.apiUrl, worktree.path, probe.branch);
261
+ if (preview && !preview.missing && !preview.error) {
262
+ const state: WorktreeReapState = {
263
+ landed: preview.landed,
264
+ ahead: preview.ahead,
265
+ unmergedAhead: preview.unmergedAhead,
266
+ dirtyCount: preview.dirtyCount,
267
+ };
268
+ orphan.landed = preview.landed;
269
+ orphan.ahead = preview.ahead;
270
+ orphan.unmergedAhead = preview.unmergedAhead;
271
+ orphan.dirty = (preview.dirtyCount ?? 0) > 0;
272
+ orphan.safeToReap = worktreeReapable(state);
273
+ }
274
+ // No probe → safeToReap stays undefined (treated as not-safe by callers).
275
+ orphans.push(orphan);
254
276
  }
255
- // No probe → safeToReap stays undefined (treated as not-safe by callers).
256
- orphans.push(orphan);
257
277
  }
258
278
  }
259
279
 
@@ -268,7 +288,10 @@ export async function collectWorkspaceOrphans(): Promise<CollectOrphansResult> {
268
288
  if (ws.mode !== "isolated" || !ws.worktreePath) continue;
269
289
  if (TERMINAL_WORKSPACE_STATUSES.has(ws.status)) continue;
270
290
  if (onDiskAll.has(resolve(ws.worktreePath))) continue;
271
- const coverable = probedRepoRoots.has(resolve(ws.repoRoot)) || onDiskAll.has(resolve(ws.repoRoot));
291
+ const coverable = ws.orchestratorId
292
+ ? probedOwnerRepoRoots.has(`${ws.orchestratorId}:${resolve(ws.repoRoot)}`)
293
+ || onDiskByOwner.has(`${ws.orchestratorId}:${resolve(ws.repoRoot)}`)
294
+ : probedRepoRoots.has(resolve(ws.repoRoot)) || onDiskAll.has(resolve(ws.repoRoot));
272
295
  if (!coverable) continue;
273
296
  missingWorktrees.push({
274
297
  workspaceId: ws.id,
@@ -420,7 +443,7 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
420
443
  continue;
421
444
  }
422
445
 
423
- const orch = orchestrators.find((candidate) => candidate.apiUrl && isPathWithinBase(workspace.repoRoot, candidate.baseDir));
446
+ const orch = workspaceOwnerOrchestrator(workspace, orchestrators, { onlineOnly: false, requireApiUrl: true });
424
447
  const probe = orch?.apiUrl
425
448
  ? await fetchBranchReapState(orch.apiUrl, workspace.repoRoot, workspace.branch, workspace.baseRef, workspace.baseSha)
426
449
  : { kind: "unavailable" } as const;
@@ -520,7 +543,7 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
520
543
  if (now - firstSeenDeadAt < graceMs) continue;
521
544
 
522
545
  // Grace elapsed + owner dead. Gate on land-state — "nothing would be lost".
523
- const orch = orchestrators.find((candidate) => candidate.apiUrl && isPathWithinBase(workspace.repoRoot, candidate.baseDir));
546
+ const orch = workspaceOwnerOrchestrator(workspace, orchestrators, { onlineOnly: false, requireApiUrl: true });
524
547
  const preview = orch?.apiUrl
525
548
  ? await fetchWorktreeReapState(orch.apiUrl, workspace.worktreePath, workspace.baseRef)
526
549
  : null;
@@ -17,6 +17,7 @@ import {
17
17
  patchWorkspaceMetadata,
18
18
  } from "./db";
19
19
  import { isPathWithinBase } from "./utils";
20
+ import { workspaceOwnerOrchestrator } from "./workspace-host";
20
21
  import { deriveBranchState } from "./workspace-phase";
21
22
  import type { AgentCard, WorkspaceMergePreview, WorkspaceRecord } from "./types";
22
23
  import { relayToken } from "./config";
@@ -95,9 +96,7 @@ export async function probeWorkspaceState(agentId: string): Promise<ProbeResult>
95
96
  const ws = ownedIsolatedWorkspace(agentId);
96
97
  if (!ws || !ws.worktreePath) return { probed: false, skipped: "no live isolated workspace" };
97
98
  if (ws.status !== "active") return { probed: false, workspaceId: ws.id, skipped: `status ${ws.status} derives badge without git counts` };
98
- const orch = listOrchestrators().find(
99
- (candidate) => candidate.status === "online" && candidate.apiUrl && workspacePathWithinBase(ws.sourceCwd, candidate.baseDir),
100
- );
99
+ const orch = workspaceOwnerOrchestrator(ws, listOrchestrators(), { requireApiUrl: true });
101
100
  if (!orch?.apiUrl) return { probed: false, workspaceId: ws.id, skipped: "no online orchestrator owns this path" };
102
101
  // checkPr:false — a badge probe never needs the gh round-trip (#304).
103
102
  const preview = await fetchHostMergePreview(orch.apiUrl, ws, { checkPr: false });
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{Mt as t,Qn as n,Zn as r,er as i,hr as a,mt as o,nr as s,rr as c,wn as l,xn as u}from"./lucide-react-Dc3ZwCBi.js";import{i as d,r as f,t as p}from"./store-OLe8gFh0.js";import{t as m}from"./task-chip-kxk4eNm1.js";import{J as h}from"./index-TamtghSr.js";var g=e(a(),1),_=`(max-width: 767px)`;function v(e){if(typeof window>`u`)return()=>{};let t=window.matchMedia(_);return t.addEventListener(`change`,e),()=>t.removeEventListener(`change`,e)}function y(){return(0,g.useSyncExternalStore)(v,()=>typeof window<`u`?window.matchMedia(_).matches:!1,()=>!1)}var b=s(),x=196,S=84,C=52,w=16,T=14,E=[`landed`,`in-progress`,`pending`,`blocked`],D={landed:{label:`landed`,dot:`bg-emerald-400`,accent:`border-l-emerald-400`,text:`text-emerald-300`,wire:`#34d399`},"in-progress":{label:`in progress`,dot:`bg-sky-400`,accent:`border-l-sky-400`,text:`text-sky-300`,wire:`#38bdf8`},pending:{label:`pending`,dot:`bg-amber-400`,accent:`border-l-amber-400`,text:`text-amber-300`,wire:`#fbbf24`},blocked:{label:`blocked`,dot:`bg-red-400`,accent:`border-l-red-400`,text:`text-red-300`,wire:`#f87171`}};function O(e){return e?e.label||(e.policyName?`policy:${e.policyName}`:e.agentId||``):``}function k(e){let t=new Map(e.nodes.map(e=>[e.id,e])),n=new Map(e.nodes.map((e,t)=>[e.id,t])),r=new Map,i=new Map(e.nodes.map(e=>[e.id,0]));for(let n of e.edges)!t.has(n.from)||!t.has(n.to)||(r.set(n.from,[...r.get(n.from)??[],n.to]),i.set(n.to,(i.get(n.to)??0)+1));let a=new Map(e.nodes.map(e=>[e.id,0])),o=e.nodes.filter(e=>(i.get(e.id)??0)===0).map(e=>e.id),s=new Set;for(;o.length;){let e=o.shift();s.add(e);let t=a.get(e)??0;for(let n of r.get(e)??[])a.set(n,Math.max(a.get(n)??0,t+1)),i.set(n,(i.get(n)??0)-1),(i.get(n)??0)===0&&o.push(n)}let c=Math.max(0,...Array.from(a.values()))+1;for(let t of e.nodes)s.has(t.id)||a.set(t.id,c++);let l=new Map;for(let t of e.nodes){let e=a.get(t.id)??0;l.set(e,[...l.get(e)??[],t])}let u=[...l.entries()].sort(([e],[t])=>e-t),d=Math.max(1,...u.map(([,e])=>e.length)),f=S+w,p=[];u.forEach(([e,t],r)=>{let i=[...t].sort((e,t)=>(e.lane||``).localeCompare(t.lane||``)||(n.get(e.id)??0)-(n.get(t.id)??0)),a=(d-i.length)*f/2;i.forEach((t,n)=>{p.push({node:t,layer:e,x:T+r*(x+C),y:T+a+n*f})})});let m=u.length;return{nodes:p,width:Math.max(320,T*2+m*x+Math.max(0,m-1)*C),height:Math.max(S+T*2,T*2+d*S+Math.max(0,d-1)*w)}}function A(e,t){let n=e.x+x,r=e.y+S/2,i=t.x,a=t.y+S/2,o=n+Math.max(20,(i-n)/2);return`M ${n} ${r} C ${o} ${r}, ${o} ${a}, ${i} ${a}`}function j({threadId:e,teamId:a,channel:o}){let s=a?d(a,o):e?f(e,o):``,c=p(e=>a?e.plansByTeam[s]:s?e.plansByThread[s]:null),_=p(e=>e.fetchPlan),v=p(e=>e.setPlanArchived),C=p(e=>e.planCollapsed),w=p(e=>e.setPlanCollapsed),T=p(e=>e.issueRefsById),j=p(e=>e.deliverableTasks),M=p(e=>e.openTaskDetailById),N=y(),P=C??N;(0,g.useEffect)(()=>{if(a){_({teamId:a,channel:o});return}e&&_({threadId:e,channel:o})},[o,_,a,e]);let F=(0,g.useMemo)(()=>c?k(c):null,[c]),I=(0,g.useMemo)(()=>new Map(F?.nodes.map(e=>[e.node.id,e])??[]),[F]),L=(0,g.useMemo)(()=>{if(!c)return[];let e=new Map;for(let t of c.nodes)e.set(t.status,(e.get(t.status)??0)+1);return E.filter(t=>e.has(t)).map(t=>({status:t,count:e.get(t)??0}))},[c]);if(!c||!F)return null;if(c.archived)return(0,b.jsx)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2`,children:[(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,b.jsx)(r,{className:`h-3 w-3 text-muted-foreground`})}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,b.jsx)(`div`,{className:`truncate text-[13px] font-semibold leading-tight text-muted-foreground`,children:c.title||`Plan`}),(0,b.jsx)(`div`,{className:`truncate text-[11px] leading-tight text-muted-foreground`,children:`Archived plan`})]}),(0,b.jsx)(`span`,{className:`shrink-0 rounded border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground`,children:`archived`}),(0,b.jsx)(`button`,{type:`button`,onClick:()=>void v(c.id,!1),title:`Unarchive plan`,"aria-label":`Unarchive plan`,className:`flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:(0,b.jsx)(n,{className:`h-3.5 w-3.5`})})]})});let R=c.nodes.length,z=L.find(e=>e.status===`landed`)?.count??0,B=`${R} node${R===1?``:`s`}${z>0?` · ${z} landed`:``}`;return(0,b.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,b.jsxs)(`div`,{className:i(`flex min-w-0 items-center gap-2`,!P&&`mb-2`),children:[(0,b.jsxs)(`button`,{type:`button`,onClick:()=>w(!P),"aria-expanded":!P,"aria-label":P?`Expand plan graph`:`Collapse plan graph`,className:`flex min-w-0 flex-1 items-center gap-2 text-left`,children:[(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,b.jsx)(t,{className:`h-3 w-3 text-muted-foreground`})}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,b.jsx)(`div`,{className:`truncate text-[13px] font-semibold leading-tight text-foreground`,children:c.title||`Plan`}),(0,b.jsx)(`div`,{className:`truncate text-[11px] leading-tight text-muted-foreground`,children:P?B:c.objective||B})]})]}),L.length>0&&(0,b.jsx)(`div`,{className:`hidden shrink-0 items-center gap-2.5 sm:flex`,children:L.map(({status:e,count:t})=>(0,b.jsxs)(`span`,{className:`flex items-center gap-1 text-[10px] text-muted-foreground`,children:[(0,b.jsx)(`span`,{className:i(`h-1.5 w-1.5 rounded-full`,D[e].dot)}),(0,b.jsx)(`span`,{className:i(`font-medium tabular-nums`,D[e].text),children:t}),(0,b.jsx)(`span`,{children:D[e].label})]},e))}),(0,b.jsxs)(`span`,{className:`shrink-0 rounded bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground`,children:[`v`,c.version]}),(0,b.jsx)(`button`,{type:`button`,onClick:()=>void v(c.id,!0),title:`Archive plan`,"aria-label":`Archive plan`,className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:(0,b.jsx)(r,{className:`h-3.5 w-3.5`})}),(0,b.jsx)(`button`,{type:`button`,onClick:()=>w(!P),"aria-expanded":!P,"aria-label":P?`Expand plan graph`:`Collapse plan graph`,className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:P?(0,b.jsx)(l,{className:`h-3.5 w-3.5`}):(0,b.jsx)(u,{className:`h-3.5 w-3.5`})})]}),P?null:c.nodes.length===0?(0,b.jsx)(`div`,{className:`rounded-md border border-dashed border-border px-3 py-3 text-center text-xs text-muted-foreground`,children:`No nodes yet — the plan graph fills in as the team is composed.`}):(0,b.jsx)(`div`,{className:`max-h-[min(46vh,360px)] overflow-auto`,children:(0,b.jsxs)(`div`,{className:`relative`,style:{width:F.width,height:F.height},children:[(0,b.jsxs)(`svg`,{className:`pointer-events-none absolute inset-0`,width:F.width,height:F.height,"aria-hidden":`true`,children:[(0,b.jsx)(`defs`,{children:E.map(e=>(0,b.jsx)(`marker`,{id:`plan-arrow-${e}`,viewBox:`0 0 8 8`,refX:`6.5`,refY:`4`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`,children:(0,b.jsx)(`path`,{d:`M0 0 L8 4 L0 8 z`,fill:D[e].wire})},e))}),c.edges.map(e=>{let t=I.get(e.from),n=I.get(e.to);if(!t||!n)return null;let r=D[t.node.status];return(0,b.jsx)(`path`,{d:A(t,n),stroke:r.wire,strokeOpacity:.7,strokeWidth:2,fill:`none`,strokeLinecap:`round`,markerEnd:`url(#plan-arrow-${t.node.status})`},`${e.from}->${e.to}`)})]}),F.nodes.map(({node:e,x:t,y:n})=>{let r=D[e.status],a=O(e.ownerRef),o=e.refs?.issueId?T[e.refs.issueId]:null,s=e.refs?.taskId?parseInt(e.refs.taskId,10):null,c=s==null?void 0:j.find(e=>e.id===s)?.title;return(0,b.jsxs)(`article`,{className:i(`group absolute flex flex-col justify-between rounded-md border border-l-[3px] bg-background/95 p-2`,`shadow-sm transition-all hover:-translate-y-px hover:border-border hover:shadow-md hover:bg-background`,r.accent),style:{left:t,top:n,width:x,height:S},title:e.scopeBlurb||e.label,children:[(0,b.jsx)(`div`,{className:`truncate text-xs font-semibold leading-tight text-foreground`,children:e.label}),(e.scopeBlurb||e.id)&&(0,b.jsx)(`div`,{className:`truncate text-[10px] leading-tight text-muted-foreground`,children:e.scopeBlurb||e.id}),(o||s!=null)&&(0,b.jsxs)(`div`,{className:`flex min-w-0 flex-wrap gap-1`,children:[o&&(0,b.jsx)(h,{issueRef:o,compact:!0,className:`max-w-full`}),s!=null&&(0,b.jsx)(m,{id:s,title:c,onClick:()=>M(s),className:`max-w-full`})]}),(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center justify-between gap-1.5`,children:[(0,b.jsxs)(`div`,{className:`flex shrink-0 items-center gap-1.5`,children:[(0,b.jsx)(`span`,{className:i(`h-1.5 w-1.5 shrink-0 rounded-full`,r.dot)}),(0,b.jsx)(`span`,{className:i(`text-[10px] font-medium`,r.text),children:r.label})]}),(0,b.jsxs)(`div`,{className:`flex min-w-0 items-center gap-1.5`,children:[a&&(0,b.jsx)(`span`,{className:`truncate text-[10px] text-muted-foreground`,children:a}),e.lane&&(0,b.jsx)(`span`,{className:`shrink-0 rounded-sm bg-muted/70 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground`,children:e.lane})]})]})]},e.id)})]})})]})}function M({agentId:e}){let t=p(e=>e.openTaskDetailById),n=p(e=>e.sessionTasksCollapsed),r=p(e=>e.setSessionTasksCollapsed),[a,s]=(0,g.useState)([]);return(0,g.useEffect)(()=>{c(`GET`,`/task-entities?createdBy=`+encodeURIComponent(e)).then(e=>s(e)).catch(()=>{})},[e]),a.length===0?null:(0,b.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,b.jsxs)(`button`,{type:`button`,onClick:()=>r(!n),"aria-expanded":!n,"aria-label":n?`Expand session tasks`:`Collapse session tasks`,className:i(`flex w-full min-w-0 items-center gap-2 text-left`,!n&&`mb-1.5`),children:[(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,b.jsx)(o,{className:`h-3 w-3 text-muted-foreground`})}),(0,b.jsxs)(`span`,{className:`flex-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground`,children:[`Tasks from this session (`,a.length,`)`]}),(0,b.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:n?(0,b.jsx)(l,{className:`h-3.5 w-3.5`}):(0,b.jsx)(u,{className:`h-3.5 w-3.5`})})]}),!n&&(0,b.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:a.map(e=>(0,b.jsx)(m,{id:e.id,title:e.title,status:e.status,onClick:()=>t(e.id),compact:!0},e.id))})]})}export{M as n,j as t};
2
- //# sourceMappingURL=PlanGraphPanel-B9TN_KQr.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PlanGraphPanel-B9TN_KQr.js","names":[],"sources":["../../dashboard/src/components/chat/hooks/useIsMobile.ts","../../dashboard/src/components/chat/PlanGraphPanel.tsx"],"sourcesContent":["import { useSyncExternalStore } from 'react'\n\n// Reactive narrow-viewport flag. Subscribes to the same `max-width: 767px`\n// breakpoint the chat single-pane layout uses, via useSyncExternalStore — an\n// external-store subscription, not effect-driven derived state — so it stays\n// correct across resize/rotate without a useEffect.\nconst QUERY = '(max-width: 767px)'\n\nfunction subscribe(cb: () => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const mql = window.matchMedia(QUERY)\n mql.addEventListener('change', cb)\n return () => mql.removeEventListener('change', cb)\n}\n\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(\n subscribe,\n () => (typeof window !== 'undefined' ? window.matchMedia(QUERY).matches : false),\n () => false,\n )\n}\n","import { useEffect, useMemo, useState } from 'react'\nimport { Archive, ArchiveRestore, ChevronDown, ChevronUp, GitBranch, ListTodo } from 'lucide-react'\nimport { useRelayStore } from '@/store'\nimport type { Plan, PlanNode, PlanOwnerRef, PlanStatus, Task } from '@/types'\nimport { cn } from '@/lib/utils'\nimport { planLookupKey, planTeamLookupKey } from '@/store/slices/plans'\nimport { IssueRefChip } from '@/components/shared/issue-ref-chip'\nimport { TaskChip } from '@/components/shared/task-chip'\nimport { useIsMobile } from './hooks/useIsMobile'\nimport { api } from '@/lib/api'\n\n// Left-to-right DAG: dependency depth flows along x (sequential gates), nodes\n// that share a depth stack along y (parallel lanes). This keeps the panel SHORT\n// — it's an operator overlay above the chat, not a full view — and lets the\n// gate-vs-lane structure read at a glance. Height is capped + scrollable below.\nconst NODE_W = 196\nconst NODE_H = 84\nconst COL_GAP = 52\nconst ROW_GAP = 16\nconst PAD = 14\n\nconst STATUS_ORDER: PlanStatus[] = ['landed', 'in-progress', 'pending', 'blocked']\n\nconst STATUS_STYLE: Record<\n PlanStatus,\n { label: string; dot: string; accent: string; text: string; wire: string }\n> = {\n landed: { label: 'landed', dot: 'bg-emerald-400', accent: 'border-l-emerald-400', text: 'text-emerald-300', wire: '#34d399' },\n 'in-progress': { label: 'in progress', dot: 'bg-sky-400', accent: 'border-l-sky-400', text: 'text-sky-300', wire: '#38bdf8' },\n pending: { label: 'pending', dot: 'bg-amber-400', accent: 'border-l-amber-400', text: 'text-amber-300', wire: '#fbbf24' },\n blocked: { label: 'blocked', dot: 'bg-red-400', accent: 'border-l-red-400', text: 'text-red-300', wire: '#f87171' },\n}\n\ninterface LayoutNode {\n node: PlanNode\n x: number\n y: number\n layer: number\n}\n\nfunction ownerLabel(owner?: PlanOwnerRef): string {\n if (!owner) return ''\n return owner.label || (owner.policyName ? `policy:${owner.policyName}` : owner.agentId || '')\n}\n\nfunction layerPlan(plan: Plan): { nodes: LayoutNode[]; width: number; height: number } {\n const nodeById = new Map(plan.nodes.map((node) => [node.id, node]))\n const indexById = new Map(plan.nodes.map((node, index) => [node.id, index]))\n const outgoing = new Map<string, string[]>()\n const indegree = new Map(plan.nodes.map((node) => [node.id, 0]))\n for (const edge of plan.edges) {\n if (!nodeById.has(edge.from) || !nodeById.has(edge.to)) continue\n outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to])\n indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1)\n }\n\n const layerById = new Map(plan.nodes.map((node) => [node.id, 0]))\n const queue = plan.nodes\n .filter((node) => (indegree.get(node.id) ?? 0) === 0)\n .map((node) => node.id)\n const visited = new Set<string>()\n\n while (queue.length) {\n const id = queue.shift()!\n visited.add(id)\n const fromLayer = layerById.get(id) ?? 0\n for (const to of outgoing.get(id) ?? []) {\n layerById.set(to, Math.max(layerById.get(to) ?? 0, fromLayer + 1))\n indegree.set(to, (indegree.get(to) ?? 0) - 1)\n if ((indegree.get(to) ?? 0) === 0) queue.push(to)\n }\n }\n\n let fallbackLayer = Math.max(0, ...Array.from(layerById.values())) + 1\n for (const node of plan.nodes) {\n if (visited.has(node.id)) continue\n layerById.set(node.id, fallbackLayer++)\n }\n\n const layers = new Map<number, PlanNode[]>()\n for (const node of plan.nodes) {\n const layer = layerById.get(node.id) ?? 0\n layers.set(layer, [...(layers.get(layer) ?? []), node])\n }\n\n const orderedLayers = [...layers.entries()].sort(([a], [b]) => a - b)\n const maxRows = Math.max(1, ...orderedLayers.map(([, nodes]) => nodes.length))\n const rowPitch = NODE_H + ROW_GAP\n\n const layoutNodes: LayoutNode[] = []\n orderedLayers.forEach(([layer, nodes], col) => {\n const ordered = [...nodes].sort((a, b) => {\n const lane = (a.lane || '').localeCompare(b.lane || '')\n return lane || ((indexById.get(a.id) ?? 0) - (indexById.get(b.id) ?? 0))\n })\n // Center each column vertically against the tallest column so the spine\n // runs through the middle and the parallel fan reads as a fan.\n const offset = ((maxRows - ordered.length) * rowPitch) / 2\n ordered.forEach((node, row) => {\n layoutNodes.push({\n node,\n layer,\n x: PAD + col * (NODE_W + COL_GAP),\n y: PAD + offset + row * rowPitch,\n })\n })\n })\n\n const cols = orderedLayers.length\n return {\n nodes: layoutNodes,\n width: Math.max(320, PAD * 2 + cols * NODE_W + Math.max(0, cols - 1) * COL_GAP),\n height: Math.max(NODE_H + PAD * 2, PAD * 2 + maxRows * NODE_H + Math.max(0, maxRows - 1) * ROW_GAP),\n }\n}\n\nfunction edgePath(from: LayoutNode, to: LayoutNode): string {\n const x1 = from.x + NODE_W\n const y1 = from.y + NODE_H / 2\n const x2 = to.x\n const y2 = to.y + NODE_H / 2\n const midX = x1 + Math.max(20, (x2 - x1) / 2)\n return `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`\n}\n\nexport function PlanGraphPanel({ threadId, teamId, channel }: { threadId?: string | number | null; teamId?: string | null; channel?: string }) {\n const key = teamId ? planTeamLookupKey(teamId, channel) : threadId ? planLookupKey(threadId, channel) : ''\n const plan = useRelayStore((s) => teamId ? s.plansByTeam[key] : key ? s.plansByThread[key] : null)\n const fetchPlan = useRelayStore((s) => s.fetchPlan)\n const setPlanArchived = useRelayStore((s) => s.setPlanArchived)\n const planCollapsed = useRelayStore((s) => s.planCollapsed)\n const setPlanCollapsed = useRelayStore((s) => s.setPlanCollapsed)\n const issueRefsById = useRelayStore((s) => s.issueRefsById)\n const deliverableTasks = useRelayStore((s) => s.deliverableTasks)\n const openTaskDetailById = useRelayStore((s) => s.openTaskDetailById)\n const isMobile = useIsMobile()\n // null = no manual toggle yet → follow the viewport default. The graph\n // dominates a phone screen, so it starts collapsed there and expanded on\n // desktop; an explicit user toggle (persisted) overrides the default.\n const collapsed = planCollapsed ?? isMobile\n\n useEffect(() => {\n if (teamId) {\n void fetchPlan({ teamId, channel })\n return\n }\n if (threadId) void fetchPlan({ threadId, channel })\n }, [channel, fetchPlan, teamId, threadId])\n\n const layout = useMemo(() => plan ? layerPlan(plan) : null, [plan])\n const nodeById = useMemo(() => new Map(layout?.nodes.map((node) => [node.node.id, node]) ?? []), [layout])\n\n const legend = useMemo(() => {\n if (!plan) return []\n const counts = new Map<PlanStatus, number>()\n for (const node of plan.nodes) counts.set(node.status, (counts.get(node.status) ?? 0) + 1)\n return STATUS_ORDER.filter((status) => counts.has(status)).map((status) => ({\n status,\n count: counts.get(status) ?? 0,\n }))\n }, [plan])\n\n if (!plan || !layout) return null\n\n if (plan.archived) {\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <Archive className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[13px] font-semibold leading-tight text-muted-foreground\">{plan.title || 'Plan'}</div>\n <div className=\"truncate text-[11px] leading-tight text-muted-foreground\">Archived plan</div>\n </div>\n <span className=\"shrink-0 rounded border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n archived\n </span>\n <button\n type=\"button\"\n onClick={() => void setPlanArchived(plan.id, false)}\n title=\"Unarchive plan\"\n aria-label=\"Unarchive plan\"\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n >\n <ArchiveRestore className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </section>\n )\n }\n\n const nodeCount = plan.nodes.length\n const landedCount = legend.find((entry) => entry.status === 'landed')?.count ?? 0\n // Thin summary for the collapsed bar (the legend dots are hidden on mobile),\n // e.g. \"5 nodes · 4 landed\".\n const summary = `${nodeCount} node${nodeCount === 1 ? '' : 's'}${landedCount > 0 ? ` · ${landedCount} landed` : ''}`\n\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <div className={cn('flex min-w-0 items-center gap-2', !collapsed && 'mb-2')}>\n <button\n type=\"button\"\n onClick={() => setPlanCollapsed(!collapsed)}\n aria-expanded={!collapsed}\n aria-label={collapsed ? 'Expand plan graph' : 'Collapse plan graph'}\n className=\"flex min-w-0 flex-1 items-center gap-2 text-left\"\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <GitBranch className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[13px] font-semibold leading-tight text-foreground\">{plan.title || 'Plan'}</div>\n <div className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {collapsed ? summary : (plan.objective || summary)}\n </div>\n </div>\n </button>\n {legend.length > 0 && (\n <div className=\"hidden shrink-0 items-center gap-2.5 sm:flex\">\n {legend.map(({ status, count }) => (\n <span key={status} className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n <span className={cn('h-1.5 w-1.5 rounded-full', STATUS_STYLE[status].dot)} />\n <span className={cn('font-medium tabular-nums', STATUS_STYLE[status].text)}>{count}</span>\n <span>{STATUS_STYLE[status].label}</span>\n </span>\n ))}\n </div>\n )}\n <span className=\"shrink-0 rounded bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground\">\n v{plan.version}\n </span>\n <button\n type=\"button\"\n onClick={() => void setPlanArchived(plan.id, true)}\n title=\"Archive plan\"\n aria-label=\"Archive plan\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n >\n <Archive className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setPlanCollapsed(!collapsed)}\n aria-expanded={!collapsed}\n aria-label={collapsed ? 'Expand plan graph' : 'Collapse plan graph'}\n className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\"\n >\n {collapsed ? <ChevronDown className=\"h-3.5 w-3.5\" /> : <ChevronUp className=\"h-3.5 w-3.5\" />}\n </button>\n </div>\n {collapsed ? null : plan.nodes.length === 0 ? (\n <div className=\"rounded-md border border-dashed border-border px-3 py-3 text-center text-xs text-muted-foreground\">\n No nodes yet — the plan graph fills in as the team is composed.\n </div>\n ) : (\n <div className=\"max-h-[min(46vh,360px)] overflow-auto\">\n <div className=\"relative\" style={{ width: layout.width, height: layout.height }}>\n <svg className=\"pointer-events-none absolute inset-0\" width={layout.width} height={layout.height} aria-hidden=\"true\">\n <defs>\n {STATUS_ORDER.map((status) => (\n <marker\n key={status}\n id={`plan-arrow-${status}`}\n viewBox=\"0 0 8 8\"\n refX=\"6.5\"\n refY=\"4\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M0 0 L8 4 L0 8 z\" fill={STATUS_STYLE[status].wire} />\n </marker>\n ))}\n </defs>\n {plan.edges.map((edge) => {\n const from = nodeById.get(edge.from)\n const to = nodeById.get(edge.to)\n if (!from || !to) return null\n const wire = STATUS_STYLE[from.node.status]\n return (\n <path\n key={`${edge.from}->${edge.to}`}\n d={edgePath(from, to)}\n stroke={wire.wire}\n strokeOpacity={0.7}\n strokeWidth={2}\n fill=\"none\"\n strokeLinecap=\"round\"\n markerEnd={`url(#plan-arrow-${from.node.status})`}\n />\n )\n })}\n </svg>\n {layout.nodes.map(({ node, x, y }) => {\n const status = STATUS_STYLE[node.status]\n const owner = ownerLabel(node.ownerRef)\n const issueRef = node.refs?.issueId ? issueRefsById[node.refs.issueId] : null\n const taskNumId = node.refs?.taskId ? parseInt(node.refs.taskId, 10) : null\n const taskTitle = taskNumId != null ? deliverableTasks.find((t) => t.id === taskNumId)?.title : undefined\n return (\n <article\n key={node.id}\n className={cn(\n 'group absolute flex flex-col justify-between rounded-md border border-l-[3px] bg-background/95 p-2',\n 'shadow-sm transition-all hover:-translate-y-px hover:border-border hover:shadow-md hover:bg-background',\n status.accent,\n )}\n style={{ left: x, top: y, width: NODE_W, height: NODE_H }}\n title={node.scopeBlurb || node.label}\n >\n <div className=\"truncate text-xs font-semibold leading-tight text-foreground\">{node.label}</div>\n {(node.scopeBlurb || node.id) && (\n <div className=\"truncate text-[10px] leading-tight text-muted-foreground\">{node.scopeBlurb || node.id}</div>\n )}\n {(issueRef || taskNumId != null) && (\n <div className=\"flex min-w-0 flex-wrap gap-1\">\n {issueRef && <IssueRefChip issueRef={issueRef} compact className=\"max-w-full\" />}\n {taskNumId != null && (\n <TaskChip id={taskNumId} title={taskTitle} onClick={() => openTaskDetailById(taskNumId)} className=\"max-w-full\" />\n )}\n </div>\n )}\n <div className=\"flex min-w-0 items-center justify-between gap-1.5\">\n <div className=\"flex shrink-0 items-center gap-1.5\">\n <span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', status.dot)} />\n <span className={cn('text-[10px] font-medium', status.text)}>{status.label}</span>\n </div>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n {owner && <span className=\"truncate text-[10px] text-muted-foreground\">{owner}</span>}\n {node.lane && (\n <span className=\"shrink-0 rounded-sm bg-muted/70 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground\">\n {node.lane}\n </span>\n )}\n </div>\n </div>\n </article>\n )\n })}\n </div>\n </div>\n )}\n </section>\n )\n}\n\n// Tasks created/triggered from a session (#856, #860). Fetches server-side via createdBy filter.\nexport function SessionTasksPanel({ agentId }: { agentId: string }) {\n const openTaskDetailById = useRelayStore((s) => s.openTaskDetailById)\n const sessionTasksCollapsed = useRelayStore((s) => s.sessionTasksCollapsed)\n const setSessionTasksCollapsed = useRelayStore((s) => s.setSessionTasksCollapsed)\n const [sessionTasks, setSessionTasks] = useState<Task[]>([])\n useEffect(() => {\n void api<import('@/types').Task[]>('GET', '/task-entities?createdBy=' + encodeURIComponent(agentId))\n .then((tasks) => setSessionTasks(tasks))\n .catch(() => {})\n }, [agentId])\n if (sessionTasks.length === 0) return null\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <button\n type=\"button\"\n onClick={() => setSessionTasksCollapsed(!sessionTasksCollapsed)}\n aria-expanded={!sessionTasksCollapsed}\n aria-label={sessionTasksCollapsed ? 'Expand session tasks' : 'Collapse session tasks'}\n className={cn('flex w-full min-w-0 items-center gap-2 text-left', !sessionTasksCollapsed && 'mb-1.5')}\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <ListTodo className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <span className=\"flex-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n Tasks from this session ({sessionTasks.length})\n </span>\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\">\n {sessionTasksCollapsed ? <ChevronDown className=\"h-3.5 w-3.5\" /> : <ChevronUp className=\"h-3.5 w-3.5\" />}\n </span>\n </button>\n {!sessionTasksCollapsed && (\n <div className=\"flex flex-wrap gap-1\">\n {sessionTasks.map((task) => (\n <TaskChip\n key={task.id}\n id={task.id}\n title={task.title}\n status={task.status}\n onClick={() => openTaskDetailById(task.id)}\n compact\n />\n ))}\n </div>\n )}\n </section>\n )\n}\n"],"mappings":"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"}