diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index a5f0eca4..892ad9c6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,7 +1,7 @@ {"id":"agent-relay-0bn","title":"Dashboard doesn't show real-time connection status","description":"Dashboard shows agent status from messages but doesn't show live connection status (connected/disconnected). The daemon tracks this in agents.json but dashboard could show online/offline indicators with last-seen timestamps.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:04.647965+01:00","updated_at":"2025-12-22T22:07:18.220968+01:00","closed_at":"2025-12-22T22:07:18.220968+01:00"} {"id":"agent-relay-0g4","title":"Add agent portability (export/import)","description":"Enable exporting agent state and history as portable packages (like AI Maestro's .zip export). Include conversation history, learned patterns, configuration. Enables agent migration between machines and backup/restore workflows.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:04:41.847187+01:00","updated_at":"2025-12-23T17:04:41.847187+01:00"} -{"id":"agent-relay-0uh","title":"Add session discovery for better status output","description":"Auto-discover relay sessions via 'tmux list-sessions'. Filter to relay-* sessions and extract agent names. Enhance 'agent-relay status' command output. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:28:49.058578+01:00","updated_at":"2025-12-20T21:36:23.10406+01:00","closed_at":"2025-12-20T21:36:23.10406+01:00"} -{"id":"agent-relay-1ek","title":"Add agent registry for persistence across restarts","description":"Store agent metadata in persistent registry (agents.json): id, name, cli, workingDirectory, firstSeen, lastSeen, messagesSent, messagesReceived. Enables agent history and stats. See docs/TMUX_IMPROVEMENTS.md for implementation details.","notes":"No progress today; agent registry idea remains open. Consider aligning with Swarm Mail 'agents' projection + last_active tracking.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-20T21:28:50.235444+01:00","updated_at":"2025-12-22T17:08:50.330703+01:00","closed_at":"2025-12-22T17:08:50.330703+01:00"} +{"id":"agent-relay-0uh","title":"Add session discovery for better status output","description":"Auto-discover relay sessions via 'tmux list-sessions'. Filter to relay-* sessions and extract agent names. Enhance 'agent-relay status' command output. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","assignee":"LeadDev","created_at":"2025-12-20T21:28:49.058578+01:00","updated_at":"2025-12-20T21:36:23.10406+01:00","closed_at":"2025-12-20T21:36:23.10406+01:00"} +{"id":"agent-relay-1ek","title":"Add agent registry for persistence across restarts","description":"Store agent metadata in persistent registry (agents.json): id, name, cli, workingDirectory, firstSeen, lastSeen, messagesSent, messagesReceived. Enables agent history and stats. See docs/TMUX_IMPROVEMENTS.md for implementation details.","notes":"No progress today; agent registry idea remains open. Consider aligning with Swarm Mail 'agents' projection + last_active tracking.","status":"closed","priority":3,"issue_type":"feature","assignee":"Implementer","created_at":"2025-12-20T21:28:50.235444+01:00","updated_at":"2025-12-22T17:08:50.330703+01:00","closed_at":"2025-12-22T17:08:50.330703+01:00"} {"id":"agent-relay-1ie","title":"Fix Gemini CLI message injection and handling","description":"Investigate and fix issues where Gemini agent messages are not being correctly picked up by the tmux-wrapper. Ensure the special printf injection path is working and that standard LLM output is captured correctly.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T12:11:57.073531+01:00","updated_at":"2025-12-22T12:17:27.434451+01:00","closed_at":"2025-12-22T12:17:27.434456+01:00"} {"id":"agent-relay-1le","title":"Agent name truncated in tmux pane/status bar","description":"Agent name gets cut off in tmux display. May need to adjust tmux status bar width or session naming convention. Check: tmux status-left-length, session name format (relay-{name}-{pid}).","status":"closed","priority":3,"issue_type":"bug","created_at":"2025-12-20T22:11:15.795453+01:00","updated_at":"2025-12-22T14:55:02.472816+01:00","closed_at":"2025-12-22T14:55:02.472818+01:00"} {"id":"agent-relay-1t7","title":"Project name display in bridge header","description":"Show selected project name in the bridge interface header for clarity","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-24T11:47:39.531513+01:00","updated_at":"2025-12-24T11:59:16.98688+01:00","closed_at":"2025-12-24T11:59:16.98688+01:00"} @@ -14,17 +14,17 @@ {"id":"agent-relay-296","title":"Fix scroll to latest message on incoming message","description":"The dashboard should auto-scroll to show the latest message when new messages arrive. Current logic has issues - messages may not be visible without manual scrolling.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-29T14:56:37.61964278Z","updated_at":"2025-12-29T15:36:18.718745-05:00","closed_at":"2025-12-29T15:32:33.242914-05:00"} {"id":"agent-relay-297","title":"Cross-project thread syntax support","description":"Extend parser to support [thread:projectId:threadId] syntax for threads spanning bridged projects. Adds threadProject field to ParsedCommand.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-29T15:45:13.172148225Z","updated_at":"2025-12-29T15:51:28.157935691Z","closed_at":"2025-12-29T15:51:28.157935691Z"} {"id":"agent-relay-298","title":"Spawn workflow: wait for ACK before sending instructions","description":"Current workflow sends spawn + instructions together, but spawned agent may not be ready. New workflow should: 1. Spawn agent 2. Wait for agent to connect and ACK 3. Then send initial instructions. This ensures reliable instruction delivery to spawned agents.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-29T15:26:45.376721-05:00","updated_at":"2025-12-29T15:37:09.847741-05:00","closed_at":"2025-12-29T15:37:02.37033-05:00"} -{"id":"agent-relay-299","title":"Threads: verify end-to-end functionality","description":"## Status\\n\\n**Backend: Complete**\\n- Parser: [thread:id] syntax works\\n- Storage: thread column with index\\n- Protocol: thread in payloads\\n\\n**Frontend: Needs work**\\n- MessageList needs thread badge display\\n- Needs thread filter UI\\n\\nWaiting for Frontend to complete Tailwind work first.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T15:33:42.130652-05:00","updated_at":"2025-12-29T15:53:54.083505-05:00","closed_at":"2025-12-29T15:49:55.229555-05:00"} -{"id":"agent-relay-2lw","title":"Add agent metadata tracking","description":"Track program, model, task description in agents.json. Better agent discovery.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.741328+01:00","updated_at":"2025-12-22T17:17:25.919228+01:00","closed_at":"2025-12-22T17:17:25.919228+01:00"} +{"id":"agent-relay-299","title":"Threads: verify end-to-end functionality","description":"## Status\\n\\n**Backend: Complete**\\n- Parser: [thread:id] syntax works\\n- Storage: thread column with index\\n- Protocol: thread in payloads\\n\\n**Frontend: Needs work**\\n- MessageList needs thread badge display\\n- Needs thread filter UI\\n\\nWaiting for Frontend to complete Tailwind work first.","status":"closed","priority":1,"issue_type":"task","assignee":"Protocol","created_at":"2025-12-29T15:33:42.130652-05:00","updated_at":"2025-12-29T15:53:54.083505-05:00","closed_at":"2025-12-29T15:49:55.229555-05:00"} +{"id":"agent-relay-2lw","title":"Add agent metadata tracking","description":"Track program, model, task description in agents.json. Better agent discovery.","status":"closed","priority":2,"issue_type":"feature","assignee":"Implementer","created_at":"2025-12-20T21:36:19.741328+01:00","updated_at":"2025-12-22T17:17:25.919228+01:00","closed_at":"2025-12-22T17:17:25.919228+01:00"} {"id":"agent-relay-2sn","title":"Competitive Analysis: mcp_agent_mail vs agent-relay","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T21:34:44.49366+01:00","updated_at":"2025-12-20T21:36:26.362391+01:00","closed_at":"2025-12-20T21:36:26.362391+01:00"} -{"id":"agent-relay-2uf","title":"Add message threading support","description":"@relay:Bob [thread:feature-123] pattern. Group related messages for better context.","notes":"Current state: thread parsing + protocol/storage support implemented (ParsedCommand.thread, SendPayload.thread, DB messages.thread + filter). Remaining: wire cmd.thread through tmux-wrapper sendRelayCommand -\u003e RelayClient.sendMessage(..., thread) and include thread in injected display/Inbox. See src/wrapper/tmux-wrapper.ts sendRelayCommand + handleIncomingMessage.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.631448+01:00","updated_at":"2025-12-22T17:17:42.876826+01:00","closed_at":"2025-12-22T17:17:42.876826+01:00"} -{"id":"agent-relay-2z1","title":"ACK messages not used for reliability","description":"In connection.ts:114-116, ACK messages are accepted but not processed. The protocol supports reliable delivery with ACK/NACK but it's not implemented. Need to: (1) Track unACKed messages, (2) Implement retry logic, (3) Add configurable TTL for messages.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:17:43.615251+01:00","updated_at":"2025-12-20T21:56:07.202292+01:00","closed_at":"2025-12-20T21:56:07.202292+01:00"} -{"id":"agent-relay-300","title":"Dashboard: view previous conversations from database","description":"Add ability to view previous conversations in depth from the dashboard. Use the adapter pattern to load historical conversations from the database. Features: browse past sessions, view full message history, search/filter conversations, see agent interactions over time.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-29T15:39:29.401085-05:00","updated_at":"2025-12-29T15:52:36.077836-05:00","closed_at":"2025-12-29T15:51:30.767209-05:00"} -{"id":"agent-relay-301","title":"Update agent communication protocol to use threads","description":"All agents should use threads whenever possible to help focus conversations. Update CLAUDE.md, agent-relay-snippet.md, and agent documentation to encourage thread usage. Threads help keep related messages grouped and reduce noise in the general channel.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T15:48:36.264668-05:00","updated_at":"2025-12-29T15:52:36.056066-05:00","closed_at":"2025-12-29T15:52:36.056066-05:00"} +{"id":"agent-relay-2uf","title":"Add message threading support","description":"@relay:Bob [thread:feature-123] pattern. Group related messages for better context.","notes":"Current state: thread parsing + protocol/storage support implemented (ParsedCommand.thread, SendPayload.thread, DB messages.thread + filter). Remaining: wire cmd.thread through tmux-wrapper sendRelayCommand -\u003e RelayClient.sendMessage(..., thread) and include thread in injected display/Inbox. See src/wrapper/tmux-wrapper.ts sendRelayCommand + handleIncomingMessage.","status":"closed","priority":2,"issue_type":"feature","assignee":"SecondLead","created_at":"2025-12-20T21:36:19.631448+01:00","updated_at":"2025-12-22T17:17:42.876826+01:00","closed_at":"2025-12-22T17:17:42.876826+01:00"} +{"id":"agent-relay-2z1","title":"ACK messages not used for reliability","description":"In connection.ts:114-116, ACK messages are accepted but not processed. The protocol supports reliable delivery with ACK/NACK but it's not implemented. Need to: (1) Track unACKed messages, (2) Implement retry logic, (3) Add configurable TTL for messages.","status":"closed","priority":2,"issue_type":"feature","assignee":"LeadDev","created_at":"2025-12-20T00:17:43.615251+01:00","updated_at":"2025-12-20T21:56:07.202292+01:00","closed_at":"2025-12-20T21:56:07.202292+01:00"} +{"id":"agent-relay-300","title":"Dashboard: view previous conversations from database","description":"Add ability to view previous conversations in depth from the dashboard. Use the adapter pattern to load historical conversations from the database. Features: browse past sessions, view full message history, search/filter conversations, see agent interactions over time.","status":"closed","priority":1,"issue_type":"feature","assignee":"Backend","created_at":"2025-12-29T15:39:29.401085-05:00","updated_at":"2025-12-29T15:52:36.077836-05:00","closed_at":"2025-12-29T15:51:30.767209-05:00"} +{"id":"agent-relay-301","title":"Update agent communication protocol to use threads","description":"All agents should use threads whenever possible to help focus conversations. Update CLAUDE.md, agent-relay-snippet.md, and agent documentation to encourage thread usage. Threads help keep related messages grouped and reduce noise in the general channel.","status":"closed","priority":1,"issue_type":"task","assignee":"Frontend","created_at":"2025-12-29T15:48:36.264668-05:00","updated_at":"2025-12-29T15:52:36.056066-05:00","closed_at":"2025-12-29T15:52:36.056066-05:00"} {"id":"agent-relay-302","title":"History page: dedicated frontend view for conversation history","description":"Create a dedicated History page in the dashboard (not just a modal). Should be a full page view with proper navigation. Coordinate with Backend on existing ConversationHistory component and API endpoints. Features: browse sessions, search/filter, view full conversations.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-29T15:52:45.782267-05:00","updated_at":"2025-12-29T16:11:27.097909-05:00","closed_at":"2025-12-29T16:05:07.11927-05:00"} -{"id":"agent-relay-303","title":"Message received indicator: show ACK before agent responds","description":"Dashboard should show when a message was received by an agent before they start responding.\n\nCurrent state: ThinkingIndicator shows when agent is actively processing/responding.\n\nGap: No indication between message sent → message received by agent.\n\nNeeded:\n1. Show 'received' indicator immediately when ACK comes back from agent\n2. Transition to 'thinking/typing' indicator when agent starts processing\n3. Clear indicator when response is sent\n\nUI states: Sending → Received (checkmark) → Thinking (pulsing) → Response\n\nThis gives users confidence their message was delivered even if agent takes time to respond.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-29T15:57:03.967858-05:00","updated_at":"2025-12-30T21:17:31.064722+01:00","closed_at":"2025-12-30T21:13:03.683869+01:00"} -{"id":"agent-relay-304","title":"Dashboard: live agent logs viewer","description":"Add ability to view live logs of an individual agent in the dashboard.\n\nOptions:\n1. Integrated in chat - show agent's terminal output inline with messages\n2. Dedicated logs page/panel - full terminal view for selected agent\n\nFeatures needed:\n- Real-time log streaming from agent's PTY\n- Filter/search logs\n- Scroll back through history\n- Toggle between chat view and logs view\n\nThis helps users see what an agent is actually doing (tool calls, errors, thinking) beyond just relay messages.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-29T16:00:22.791859-05:00","updated_at":"2025-12-29T16:11:01.354186-05:00","closed_at":"2025-12-29T16:11:01.354186-05:00"} -{"id":"agent-relay-305","title":"Implement shadow-as-subagent architecture","description":"Replace separate shadow agent processes with Claude Code/OpenCode subagent model.\n\n## Proposal Document\nSee: docs/proposals/shadow-as-subagent.md\n\n## Summary\n- Shadow agents run as Task tool subagents when primary is Claude/OpenCode\n- Fallback to process mode for other CLIs (using authenticated Claude/OpenCode)\n- ~50% resource reduction, instant spawn, direct context sharing\n\n## Key Deliverables\n1. Shadow CLI selection logic (src/bridge/shadow-cli.ts)\n2. Shadow agent profiles for Claude Code and OpenCode\n3. PRPM package for distribution\n4. Updated SpawnModal UI\n5. Integration with existing relay architecture","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-12-29T20:28:22.077554-05:00","updated_at":"2026-01-03T16:52:20.695863+01:00"} +{"id":"agent-relay-303","title":"Message received indicator: show ACK before agent responds","description":"Dashboard should show when a message was received by an agent before they start responding.\n\nCurrent state: ThinkingIndicator shows when agent is actively processing/responding.\n\nGap: No indication between message sent → message received by agent.\n\nNeeded:\n1. Show 'received' indicator immediately when ACK comes back from agent\n2. Transition to 'thinking/typing' indicator when agent starts processing\n3. Clear indicator when response is sent\n\nUI states: Sending → Received (checkmark) → Thinking (pulsing) → Response\n\nThis gives users confidence their message was delivered even if agent takes time to respond.","status":"closed","priority":1,"issue_type":"feature","assignee":"Messenger","created_at":"2025-12-29T15:57:03.967858-05:00","updated_at":"2025-12-30T21:17:31.064722+01:00","closed_at":"2025-12-30T21:13:03.683869+01:00"} +{"id":"agent-relay-304","title":"Dashboard: live agent logs viewer","description":"Add ability to view live logs of an individual agent in the dashboard.\n\nOptions:\n1. Integrated in chat - show agent's terminal output inline with messages\n2. Dedicated logs page/panel - full terminal view for selected agent\n\nFeatures needed:\n- Real-time log streaming from agent's PTY\n- Filter/search logs\n- Scroll back through history\n- Toggle between chat view and logs view\n\nThis helps users see what an agent is actually doing (tool calls, errors, thinking) beyond just relay messages.","status":"closed","priority":1,"issue_type":"feature","assignee":"Backend","created_at":"2025-12-29T16:00:22.791859-05:00","updated_at":"2025-12-29T16:11:01.354186-05:00","closed_at":"2025-12-29T16:11:01.354186-05:00"} +{"id":"agent-relay-305","title":"Implement shadow-as-subagent architecture","description":"Replace separate shadow agent processes with Claude Code/OpenCode subagent model.\n\n## Proposal Document\nSee: docs/proposals/shadow-as-subagent.md\n\n## Summary\n- Shadow agents run as Task tool subagents when primary is Claude/OpenCode\n- Fallback to process mode for other CLIs (using authenticated Claude/OpenCode)\n- ~50% resource reduction, instant spawn, direct context sharing\n\n## Key Deliverables\n1. Shadow CLI selection logic (src/bridge/shadow-cli.ts)\n2. Shadow agent profiles for Claude Code and OpenCode\n3. PRPM package for distribution\n4. Updated SpawnModal UI\n5. Integration with existing relay architecture","status":"in_progress","priority":1,"issue_type":"feature","assignee":"Investigator","created_at":"2025-12-29T20:28:22.077554-05:00","updated_at":"2026-01-03T16:52:20.695863+01:00"} {"id":"agent-relay-306","title":"Add 'Start Conversation' UI to dashboard","description":"Currently users can only reply to existing messages. Add ability to start a new conversation/thread with an agent directly.\n\n## Requirements\n- Add 'New Message' or 'Start Conversation' button\n- Allow selecting target agent\n- Create thread on first message\n- Integrate with existing MessageComposer\n\n## Files\n- src/dashboard/react-components/App.tsx\n- src/dashboard/react-components/BroadcastComposer.tsx (reference)\n- src/dashboard/react-components/ThreadPanel.tsx","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-29T20:34:48.598318-05:00","updated_at":"2025-12-29T20:50:07.219241-05:00","closed_at":"2025-12-29T20:50:07.219241-05:00"} {"id":"agent-relay-307","title":"Stream logs for daemon-connected agents (not just spawned)","description":"Currently logs only stream for workers spawned via AgentSpawner. Agents that connect directly to the daemon (via relay CLI) don't have their logs available in the dashboard.\n\n## Problem\n- spawner.hasWorker(agentName) returns false for daemon-connected agents\n- No PTY wrapper exists for these agents to capture output\n\n## Potential Solutions\n1. Have daemon-connected agents send log output via relay protocol\n2. Add AGENT_LOG message type to protocol\n3. Router forwards logs to dashboard WebSocket\n\n## Files\n- src/daemon/server.ts (line 847 - spawner check)\n- src/protocol/types.ts (add LOG message type?)\n- src/wrapper/client.ts (add log sending capability)","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-29T20:35:29.626072-05:00","updated_at":"2025-12-29T20:50:07.365284-05:00","closed_at":"2025-12-29T20:50:07.365284-05:00"} {"id":"agent-relay-308","title":"Cloud stack hardening","description":"Group fixes to cloud workspace runtime, credential handling, API security, and schema/provisioning resilience surfaced in recent architecture review.","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-30T12:21:10.819956+01:00","updated_at":"2025-12-30T12:21:10.819956+01:00"} @@ -33,7 +33,7 @@ {"id":"agent-relay-308.3","title":"Cloud API security hardening","description":"Server promises CSRF, rate limiting, audit logging, and secure cookies but they are absent. Add CSRF protection, rate limiting, audit logs, trust-proxy/sameSite/secure cookie settings, and session/Redis resilience; surface agent/daemon auth plan if applicable.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T12:21:40.511937+01:00","updated_at":"2025-12-30T12:31:32.785415+01:00","closed_at":"2025-12-30T12:31:32.785415+01:00","dependencies":[{"issue_id":"agent-relay-308.3","depends_on_id":"agent-relay-308","type":"parent-child","created_at":"2025-12-30T21:17:31.068283+01:00","created_by":"import"}]} {"id":"agent-relay-308.4","title":"DB/migration alignment and provisioning resilience","description":"Init SQL diverges from Drizzle schema, and provisioning/state management lacks persistence/retries. Generate migrations that match current schema, retire or update init-db.sql, persist device-flow state in Redis, and add retry/backoff/health checks for provisioning + daemon health reporting.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-30T12:21:44.700579+01:00","updated_at":"2025-12-30T12:36:37.902328+01:00","closed_at":"2025-12-30T12:36:37.902328+01:00","dependencies":[{"issue_id":"agent-relay-308.4","depends_on_id":"agent-relay-308","type":"parent-child","created_at":"2025-12-30T21:17:31.06881+01:00","created_by":"import"}]} {"id":"agent-relay-309","title":"Complete agent logs rendering in dashboard","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-30T13:34:02.250273+01:00","updated_at":"2026-01-02T20:34:28.247002+01:00","closed_at":"2026-01-02T20:32:08.341497+01:00"} -{"id":"agent-relay-30fu","title":"Fix dashboard conversation routing logic","description":"Conversation routing in the dashboard needs fixing: 1) If conversation starts in DM, it should continue in DM. 2) If conversation starts in general channel, it should flow to general. 3) Messages without '@' should naturally broadcast to everyone.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-28T22:05:12.683854-05:00","updated_at":"2025-12-28T22:06:29.421436-05:00","closed_at":"2025-12-28T22:06:29.421436-05:00"} +{"id":"agent-relay-30fu","title":"Fix dashboard conversation routing logic","description":"Conversation routing in the dashboard needs fixing: 1) If conversation starts in DM, it should continue in DM. 2) If conversation starts in general channel, it should flow to general. 3) Messages without '@' should naturally broadcast to everyone.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-28T22:05:12.683854-05:00","updated_at":"2025-12-28T22:06:29.421436-05:00","closed_at":"2025-12-28T22:06:29.421436-05:00","close_reason":"Fixed by Frontend agent"} {"id":"agent-relay-310","title":"Wire up TrajectoryViewer component in dashboard","description":"TrajectoryViewer component exists but is currently not imported or used anywhere in the dashboard. Need to wire it up to display agent decision trajectories/paths. Component location: src/dashboard/react-components/TrajectoryViewer.tsx","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-30T20:51:48.418742+01:00","updated_at":"2026-01-03T16:37:15.162257+01:00","closed_at":"2026-01-03T16:37:07.902322+01:00"} {"id":"agent-relay-311","title":"Wire up TrajectoryViewer component","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-30T20:52:15.248077+01:00","updated_at":"2026-01-03T14:29:10.795531+01:00","closed_at":"2026-01-03T14:28:46.805229+01:00"} {"id":"agent-relay-312","title":"agent-relay: running non-existent command creates orphan agent connection","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-30T21:07:03.601723+01:00","updated_at":"2026-01-02T20:34:28.248346+01:00","closed_at":"2026-01-02T20:34:13.342772+01:00"} @@ -69,12 +69,12 @@ {"id":"agent-relay-406","title":"Fix thinking indicator showing on all messages instead of just latest","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T11:18:43.769168+01:00","updated_at":"2026-01-02T11:20:37.288298+01:00","closed_at":"2026-01-02T11:20:37.288298+01:00"} {"id":"agent-relay-407","title":"Implement optimistic message delivery for snappier UI","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-02T11:22:06.265912+01:00","updated_at":"2026-01-02T11:24:35.583854+01:00","closed_at":"2026-01-02T11:24:35.583854+01:00"} {"id":"agent-relay-408","title":"Retrospective: CLI-Agnostic Continuity System (agent-relay-317)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T20:46:09.680712+01:00","updated_at":"2026-01-02T20:46:09.680712+01:00"} -{"id":"agent-relay-409","title":"Add trajectory instructions to agent-relay-snippet.md","description":"Add trail CLI instructions to docs/agent-relay-snippet.md so agents know to use trail start, trail decision, and trail complete. Include: when to start trajectories (on task pickup), when to record decisions, when to complete. Reference npx trail commands.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T21:05:56.326201+01:00","updated_at":"2026-01-02T21:48:14.364348+01:00","closed_at":"2026-01-02T21:47:50.764025+01:00"} +{"id":"agent-relay-409","title":"Add trajectory instructions to agent-relay-snippet.md","description":"Add trail CLI instructions to docs/agent-relay-snippet.md so agents know to use trail start, trail decision, and trail complete. Include: when to start trajectories (on task pickup), when to record decisions, when to complete. Reference npx trail commands.","status":"closed","priority":1,"issue_type":"task","assignee":"Worker","created_at":"2026-01-02T21:05:56.326201+01:00","updated_at":"2026-01-02T21:48:14.364348+01:00","closed_at":"2026-01-02T21:47:50.764025+01:00"} {"id":"agent-relay-410","title":"Add continuity instructions to agent-relay-snippet.md","description":"Add -\u003econtinuity: pattern instructions to docs/agent-relay-snippet.md. Include: -\u003econtinuity:save for saving state, -\u003econtinuity:load for loading previous context, -\u003econtinuity:search for searching handoffs. Explain when to save (before long ops, periodically, on task completion).","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T21:05:58.937842+01:00","updated_at":"2026-01-02T21:09:31.061436+01:00","closed_at":"2026-01-02T21:09:31.061436+01:00"} -{"id":"agent-relay-411","title":"Enable auto-save on agent exit in wrappers","description":"Wire up autoSave() method in TmuxWrapper and PtyWrapper to be called on agent exit. Should create handoff with triggerReason='session_end' when agent cleanly exits, or 'crash' on unexpected termination. Also ensure continuity directory is created on daemon start.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T21:06:01.865667+01:00","updated_at":"2026-01-03T17:00:02.894447+01:00","closed_at":"2026-01-03T16:59:41.381635+01:00"} +{"id":"agent-relay-411","title":"Enable auto-save on agent exit in wrappers","description":"Wire up autoSave() method in TmuxWrapper and PtyWrapper to be called on agent exit. Should create handoff with triggerReason='session_end' when agent cleanly exits, or 'crash' on unexpected termination. Also ensure continuity directory is created on daemon start.","status":"closed","priority":2,"issue_type":"task","assignee":"Backend","created_at":"2026-01-02T21:06:01.865667+01:00","updated_at":"2026-01-03T17:00:02.894447+01:00","closed_at":"2026-01-03T16:59:41.381635+01:00"} {"id":"agent-relay-412","title":"[Bug] Fix continuity command parsing in PtyWrapper - use rawBuffer not immediate chunk","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T21:24:44.900064+01:00","updated_at":"2026-01-02T21:24:54.052753+01:00","closed_at":"2026-01-02T21:24:54.052753+01:00"} -{"id":"agent-relay-413","title":"[Agents] Enforce trajectory recording via trail CLI in agent workflows","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-02T21:48:01.429879+01:00","updated_at":"2026-01-02T21:53:22.891637+01:00","closed_at":"2026-01-02T21:53:12.705983+01:00"} -{"id":"agent-relay-414","title":"[Bug] Continuity message causes agent infinite loop","description":"Some agents go into infinite loop when continuity messages are processed. Need to investigate wrapper continuity handling, message injection, and deduplication.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-02T22:07:13.912988+01:00","updated_at":"2026-01-02T22:19:46.427693+01:00","closed_at":"2026-01-02T22:19:46.427693+01:00"} +{"id":"agent-relay-413","title":"[Agents] Enforce trajectory recording via trail CLI in agent workflows","status":"closed","priority":1,"issue_type":"task","assignee":"Worker","created_at":"2026-01-02T21:48:01.429879+01:00","updated_at":"2026-01-02T21:53:22.891637+01:00","closed_at":"2026-01-02T21:53:12.705983+01:00"} +{"id":"agent-relay-414","title":"[Bug] Continuity message causes agent infinite loop","description":"Some agents go into infinite loop when continuity messages are processed. Need to investigate wrapper continuity handling, message injection, and deduplication.","status":"closed","priority":1,"issue_type":"bug","assignee":"Frontend","created_at":"2026-01-02T22:07:13.912988+01:00","updated_at":"2026-01-02T22:19:46.427693+01:00","closed_at":"2026-01-02T22:19:46.427693+01:00"} {"id":"agent-relay-415","title":"[Cleanup] Replace console.log with structured logger in router.ts","description":"router.ts uses console.log/console.error throughout instead of the structured daemonLog logger. Should use log.debug/log.info/log.error for consistency with server.ts","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-02T23:20:33.545749+01:00","updated_at":"2026-01-03T13:01:34.574157+01:00","closed_at":"2026-01-03T13:01:27.712357+01:00"} {"id":"agent-relay-416","title":"[Type Safety] Remove 'any' cast in server.ts handleCrossMachineMessage","description":"Line 299 in server.ts casts to 'any' when calling router.route(). Should properly type the connection/envelope parameters.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-02T23:20:34.919559+01:00","updated_at":"2026-01-03T12:51:17.376266+01:00","closed_at":"2026-01-03T12:46:36.283293+01:00"} {"id":"agent-relay-417","title":"[Consistency] Use uuid() instead of Math.random() for cross-machine message IDs","description":"Line 283 in server.ts uses Math.random().toString(36).slice(2) for ID generation. Should use uuid() for consistency with rest of codebase.","status":"closed","priority":3,"issue_type":"bug","created_at":"2026-01-02T23:20:36.327308+01:00","updated_at":"2026-01-03T16:37:15.164467+01:00","closed_at":"2026-01-03T16:25:24.434868+01:00"} @@ -105,176 +105,176 @@ {"id":"agent-relay-440","title":"Vault: Type safety for provider config access","description":"src/cloud/vault/index.ts lines 232-238 use (providerConfig as any)?.clientId and (providerConfig as any)?.clientSecret. Should define proper types for the provider config structure instead of casting to any.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T23:29:51.493905+01:00","updated_at":"2026-01-02T23:29:51.493905+01:00"} {"id":"agent-relay-441","title":"Test coverage: Cloud API routes need tests","description":"The entire src/cloud/api/ directory (13 files) has no test coverage. Critical files needing tests: auth.ts, billing.ts, providers.ts, repos.ts, workspaces.ts, webhooks.ts, daemons.ts, teams.ts, coordinators.ts, onboarding.ts, usage.ts, github-app.ts, nango-auth.ts. These handle authentication, payments, repository management, and external integrations.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T23:31:46.101927+01:00","updated_at":"2026-01-02T23:31:46.101927+01:00"} {"id":"agent-relay-442","title":"Test coverage: Vault and billing need tests","description":"src/cloud/vault/index.ts handles credential encryption (AES-256-GCM) and has no tests. src/cloud/billing/*.ts (4 files) handle Stripe payments and have no tests. These are security-critical modules that need thorough test coverage for encryption, token refresh, checkout, subscriptions, and webhooks.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2026-01-02T23:32:06.753578+01:00","updated_at":"2026-01-03T12:51:17.376961+01:00"} -{"id":"agent-relay-443","title":"[Bug] Spawned agents incorrectly try to spawn other agents","description":"When agents are spawned via -\u003erelay:spawn, they receive the relay protocol snippet in their context which includes the spawn syntax. The spawned agents then parse this and attempt to spawn agents themselves, causing: 1) Duplicate spawn attempts (Worker X already exists), 2) Random agent names like IndigoLightning being created. Fix: Spawned agents should either not receive spawn capability, or the spawner should ignore spawn commands from non-Lead agents.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-03T12:27:48.213495+01:00","updated_at":"2026-01-03T12:31:59.714585+01:00","closed_at":"2026-01-03T12:31:59.714585+01:00"} -{"id":"agent-relay-444","title":"[Bug] Spawn command parser too greedy - matches text that looks like commands","description":"The spawn command parser is matching text that resembles spawn syntax but isn't actual commands. Examples: 1) Empty name detection: 'Spawn command has suspiciously short name', 2) Parsing 'syntax' as agent name from '-\u003erelay:spawn syntax' in message text. Parser needs stricter validation - require own line, specific format, etc.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-03T12:33:29.283502+01:00","updated_at":"2026-01-03T12:39:16.617909+01:00","closed_at":"2026-01-03T12:39:16.617909+01:00"} +{"id":"agent-relay-443","title":"[Bug] Spawned agents incorrectly try to spawn other agents","description":"When agents are spawned via -\u003erelay:spawn, they receive the relay protocol snippet in their context which includes the spawn syntax. The spawned agents then parse this and attempt to spawn agents themselves, causing: 1) Duplicate spawn attempts (Worker X already exists), 2) Random agent names like IndigoLightning being created. Fix: Spawned agents should either not receive spawn capability, or the spawner should ignore spawn commands from non-Lead agents.","status":"closed","priority":1,"issue_type":"bug","assignee":"Backend","created_at":"2026-01-03T12:27:48.213495+01:00","updated_at":"2026-01-03T12:31:59.714585+01:00","closed_at":"2026-01-03T12:31:59.714585+01:00"} +{"id":"agent-relay-444","title":"[Bug] Spawn command parser too greedy - matches text that looks like commands","description":"The spawn command parser is matching text that resembles spawn syntax but isn't actual commands. Examples: 1) Empty name detection: 'Spawn command has suspiciously short name', 2) Parsing 'syntax' as agent name from '-\u003erelay:spawn syntax' in message text. Parser needs stricter validation - require own line, specific format, etc.","status":"closed","priority":2,"issue_type":"bug","assignee":"Backend","created_at":"2026-01-03T12:33:29.283502+01:00","updated_at":"2026-01-03T12:39:16.617909+01:00","closed_at":"2026-01-03T12:39:16.617909+01:00"} {"id":"agent-relay-445","title":"[Dashboard] LogViewer panel fixes - collapse, spacing, Codex parsing","description":"Three issues in LogViewer panel: 1) Collapse behavior wrong - should collapse agent summary at top, not within panel, 2) Too much spacing/empty space between messages, sometimes empty lines, 3) Codex agent (CodexAgent) shows extraneous characters - parsing needs cleanup","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-03T12:40:26.185322+01:00","updated_at":"2026-01-03T12:52:58.358463+01:00","closed_at":"2026-01-03T12:52:51.767941+01:00"} {"id":"agent-relay-446","title":"Thread selection shows 'No messages yet' when thread has messages","description":"When selecting a thread in the sidebar (e.g. 'i still see some issues with s...'), the main content area shows 'No messages yet' and 'Broadcast messages will app...' even though the thread clearly has messages between Dashboard and Lead. The thread messages should display in the main area when selected.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-03T12:40:59.759659+01:00","updated_at":"2026-01-03T13:12:57.095388+01:00","closed_at":"2026-01-03T13:12:47.453896+01:00"} {"id":"agent-relay-447","title":"Message display concatenates text without space (e.g. 'codebase80%')","description":"In message cards on the #general channel, text appears concatenated without proper spacing. Example: 'Explain this codebase80% context left' should be 'Explain this codebase 80% context left'. Likely a missing space in the message rendering template.","status":"open","priority":3,"issue_type":"bug","created_at":"2026-01-03T12:41:01.237873+01:00","updated_at":"2026-01-03T12:41:15.134402+01:00"} {"id":"agent-relay-448","title":"Shadow Mode text truncated in Spawn Agent modal","description":"In the Spawn New Agent modal, the Shadow Mode section shows truncated text: 'Shadow execution: Subagent (in-proc...' - the full description is cut off. Consider adding a tooltip, expanding the width, or using text wrapping.","status":"open","priority":4,"issue_type":"bug","created_at":"2026-01-03T12:41:02.287032+01:00","updated_at":"2026-01-03T12:41:16.493967+01:00"} -{"id":"agent-relay-449","title":"[Dashboard] Fleet info panel showing incorrect agent count","description":"Fleet info panel shows 11 agents but there are only 5 connected. The agent count calculation is wrong - likely counting disconnected/stale agents or duplicates.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-03T12:45:18.652325+01:00","updated_at":"2026-01-03T15:56:25.662049+01:00","closed_at":"2026-01-03T15:02:00+01:00"} +{"id":"agent-relay-449","title":"[Dashboard] Fleet info panel showing incorrect agent count","description":"Fleet info panel shows 11 agents but there are only 5 connected. The agent count calculation is wrong - likely counting disconnected/stale agents or duplicates.","status":"closed","priority":2,"issue_type":"bug","assignee":"Frontend","created_at":"2026-01-03T12:45:18.652325+01:00","updated_at":"2026-01-03T15:56:25.662049+01:00","closed_at":"2026-01-03T15:02:00+01:00"} {"id":"agent-relay-450","title":"Auto-inject continuity context on agent spawn","description":"When agents spawn via wrapper, auto-inject their previous continuity context (ledger + latest handoff) using getStartupContext(). Currently agents must manually call -\u003econtinuity:load. This should happen automatically on spawn so agents resume with full context. Related: Dashboard question about agents checking continuity on restart.","status":"open","priority":2,"issue_type":"feature","created_at":"2026-01-03T13:09:33.536797+01:00","updated_at":"2026-01-03T13:10:07.426747+01:00"} {"id":"agent-relay-451","title":"Fix empty continuity handoff files - parse SESSION_END content","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-03T14:27:49.747598+01:00","updated_at":"2026-01-03T14:33:27.122823+01:00","closed_at":"2026-01-03T14:33:21.048043+01:00"} {"id":"agent-relay-452","title":"Trajectories should populate agents array with agent who started it","description":"When trail start is called, the trajectory's agents array is empty. It should automatically associate the agent who started the trajectory.","status":"completed","priority":2,"issue_type":"bug","created_at":"2026-01-03T14:28:39.57+01:00","updated_at":"2026-01-03T15:56:25.663159+01:00"} -{"id":"agent-relay-453","title":"BUG: Spawn command fails silently when CLI not specified","description":"Users can send `-\u003erelay:spawn WorkerName` without a CLI type, but the parser silently ignores it because it requires both name AND cli. \n\nParse code at pty-wrapper.ts:931 checks `parts.length \u003e= 2` which fails for commands like:\n- `-\u003erelay:spawn Investigator`\n\nShould either:\n1. Make CLI optional with sensible default (claude)\n2. Provide error feedback when CLI is missing\n\nThis blocks relay spawn/release functionality entirely.","status":"closed","priority":0,"issue_type":"bug","created_at":"2026-01-03T16:43:37.927258+01:00","updated_at":"2026-01-03T16:50:11.02666+01:00","closed_at":"2026-01-03T16:50:11.02666+01:00"} +{"id":"agent-relay-453","title":"BUG: Spawn command fails silently when CLI not specified","description":"Users can send `-\u003erelay:spawn WorkerName` without a CLI type, but the parser silently ignores it because it requires both name AND cli. \n\nParse code at pty-wrapper.ts:931 checks `parts.length \u003e= 2` which fails for commands like:\n- `-\u003erelay:spawn Investigator`\n\nShould either:\n1. Make CLI optional with sensible default (claude)\n2. Provide error feedback when CLI is missing\n\nThis blocks relay spawn/release functionality entirely.","status":"closed","priority":0,"issue_type":"bug","assignee":"Backend","created_at":"2026-01-03T16:43:37.927258+01:00","updated_at":"2026-01-03T16:50:11.02666+01:00","closed_at":"2026-01-03T16:50:11.02666+01:00"} {"id":"agent-relay-454","title":"OpenCode headless mode integration","description":"Integrate OpenCode's headless mode (opencode run) with Agent Relay. Options: 1) Create MCP server adapter for agent-relay that OpenCode can use, 2) Document OpenCode config to work with relay. See: https://github.com/anomalyco/opencode/issues/953","status":"open","priority":3,"issue_type":"feature","created_at":"2026-01-04T01:01:55.715466+01:00","updated_at":"2026-01-04T01:01:55.715466+01:00"} {"id":"agent-relay-455","title":"Create shared types package between backend and frontend","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-04T21:03:08.485997+01:00","updated_at":"2026-01-04T21:03:08.485997+01:00"} {"id":"agent-relay-456","title":"Create dedicated /app/onboarding route for workspace deletion flow","description":"Currently when a workspace is deleted, we redirect to /app which handles multiple states (workspace selection, no-workspaces, etc). This works but could be more robust with a dedicated onboarding route.\n\n**Current behavior:**\n- Workspace deleted → redirect to /app\n- /app re-initializes and shows appropriate state based on remaining workspaces/repos\n\n**Proposed improvements:**\n1. Create /app/onboarding route with explicit onboarding UI\n2. Different UX for first-time users vs users who deleted their workspace\n3. Better analytics tracking for onboarding funnel\n4. Cleaner separation of concerns between workspace selection and onboarding\n\n**Files involved:**\n- src/dashboard/app/app/page.tsx (current state machine)\n- src/dashboard/react-components/settings/WorkspaceSettingsPanel.tsx (deletion redirect)","status":"closed","priority":3,"issue_type":"task","created_at":"2026-01-05T17:10:00.072986+01:00","updated_at":"2026-01-05T21:08:00.860915+01:00","closed_at":"2026-01-05T21:08:00.860915+01:00"} {"id":"agent-relay-457","title":"Responsive: metrics/trajectories missing from hamburger menu","description":"On mobile/responsive views, the navigation options for metrics and trajectories disappear entirely instead of being added to the hamburger menu. Users cannot access these features on smaller screens. Expected: Nav items should collapse into hamburger menu at mobile breakpoints.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-01-05T20:45:18.983136+01:00","updated_at":"2026-01-05T20:59:32.834122+01:00","closed_at":"2026-01-05T20:59:32.834122+01:00"} {"id":"agent-relay-458","title":"Implement Free to Paid plan upgrade flow","description":"Users on the Free plan need a way to upgrade to paid plans (Pro, Team). Includes: upgrade UI in dashboard, Stripe checkout integration, plan limit updates, and success confirmation flow.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-05T21:03:16.765398+01:00","updated_at":"2026-01-05T21:10:36.286785+01:00","closed_at":"2026-01-05T21:10:36.286785+01:00"} {"id":"agent-relay-459","title":"Header status text overflow - truncate with hover tooltip","description":"Status text in the header overflows and gets cut off when too long. Need to: 1) Limit/truncate the status text with ellipsis 2) Show full text on hover via tooltip. See screenshot - text like 'Frontend starting onboarding route work...' overflows into the New Message button area.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-05T21:06:15.400581+01:00","updated_at":"2026-01-05T21:19:17.729021+01:00","closed_at":"2026-01-05T21:19:17.729021+01:00"} -{"id":"agent-relay-460","title":"Wire up CloudPersistenceService to populate agent_sessions and agent_summaries tables","description":"The agent_sessions and agent_summaries tables are empty in production because CloudPersistenceService is never wired up. The service exists in src/cloud/services/persistence.ts but no code calls bindToPtyWrapper() or createPersistenceService(). Fix: When spawning agents via PtyWrapper in cloud mode, call createPersistenceService(workspaceId) and persistence.bindToPtyWrapper(ptyWrapper). This will populate tables when agents emit [[SUMMARY]] and [[SESSION_END]] blocks.","status":"open","priority":1,"issue_type":"bug","created_at":"2026-01-05T22:46:11.194635+01:00","updated_at":"2026-01-05T22:46:11.194635+01:00"} +{"id":"agent-relay-460","title":"Wire up CloudPersistenceService to populate agent_sessions and agent_summaries tables","description":"The agent_sessions and agent_summaries tables are empty in production because CloudPersistenceService is never wired up. The service exists in src/cloud/services/persistence.ts but no code calls bindToPtyWrapper() or createPersistenceService(). Fix: When spawning agents via PtyWrapper in cloud mode, call createPersistenceService(workspaceId) and persistence.bindToPtyWrapper(ptyWrapper). This will populate tables when agents emit [[SUMMARY]] and [[SESSION_END]] blocks.","status":"open","priority":1,"issue_type":"bug","created_at":"2026-01-05T22:02:49.457321+01:00","updated_at":"2026-01-06T14:07:38.445085+01:00"} {"id":"agent-relay-461","title":"Collaborative Workspaces: Multi-user Access and Messaging","status":"open","priority":1,"issue_type":"epic","created_at":"2026-01-06T00:11:22.651042+01:00","updated_at":"2026-01-06T00:11:22.651042+01:00"} -{"id":"agent-relay-462","title":"Auto Workspace Access from GitHub Repo Permissions","description":"# Feature: Auto Workspace Access from GitHub Repo Permissions\n\nWhen a user authenticates and has GitHub access to a repository that belongs to a workspace, they should automatically gain access to that workspace's dashboard, chat, and agent communication.\n\n## Current State\n- Nango service has `checkUserRepoAccess()` and `listUserAccessibleRepos()`\n- `repositories` table has `github_full_name` and links to `workspace_id`\n- `workspace_members` table exists but requires explicit invitation\n- No automatic access granting based on repo permissions\n\n## Desired Behavior\n\n### Flow 1: Dashboard Load\n1. User logs into dashboard\n2. System fetches user's accessible GitHub repos via Nango\n3. System queries `repositories` table for matches on `github_full_name`\n4. For each matching repo, find associated `workspace_id`\n5. User sees all matched workspaces in their dashboard\n6. No explicit `workspace_member` record required (just-in-time access)\n\n### Flow 2: Workspace Route Access\n1. User navigates to `/workspace/:id`\n2. Middleware checks: does user have GitHub access to ANY repo in this workspace?\n3. If yes → allow access with \"contributor\" role\n4. If no → 403 Forbidden\n\n### Flow 3: Real-time Sync\n1. When user's GitHub permissions change (added/removed from repo)\n2. Next dashboard load reflects new workspace access\n3. Consider: webhook from GitHub for real-time updates (future)\n\n## Technical Spec\n\n### New Middleware: `requireWorkspaceAccess`\nLocation: `src/cloud/api/workspaces.ts`\n\n```typescript\nasync function requireWorkspaceAccess(req, res, next) {\n const userId = req.session.userId;\n const workspaceId = req.params.workspaceId;\n \n // 1. Check if user is workspace owner\n const workspace = await db.workspaces.findById(workspaceId);\n if (workspace?.userId === userId) return next();\n \n // 2. Check explicit workspace_members\n const member = await db.workspaceMembers.findByUserAndWorkspace(userId, workspaceId);\n if (member) return next();\n \n // 3. Check GitHub repo access (just-in-time)\n const repos = await db.repositories.findByWorkspace(workspaceId);\n const user = await db.users.findById(userId);\n \n for (const repo of repos) {\n const access = await nango.checkUserRepoAccess(\n user.nangoConnectionId, \n repo.githubFullName\n );\n if (access.permission !== 'none') {\n // Cache this for performance (optional: create temp member)\n return next();\n }\n }\n \n return res.status(403).json({ error: 'No access to this workspace' });\n}\n```\n\n### New API Endpoint: `GET /api/workspaces/accessible`\nReturns all workspaces user can access (owned + member + repo-based)\n\n```typescript\n// Response shape\n{\n workspaces: [\n {\n id: string,\n name: string,\n accessType: 'owner' | 'member' | 'contributor', // contributor = via repo\n repositories: [{ fullName: string, permission: string }],\n status: string\n }\n ]\n}\n```\n\n### Database Considerations\nOption A: Just-in-time checks (no schema change)\n- Check GitHub access on every request\n- Cache results in Redis/memory with TTL\n- Pro: Always up-to-date\n- Con: API latency, rate limits\n\nOption B: Sync workspace_members periodically\n- Background job syncs GitHub permissions to workspace_members\n- Add `accessType` column: 'invited' | 'github_repo'\n- Pro: Fast reads\n- Con: Can be stale\n\n**Recommendation**: Start with Option A + caching, move to Option B if performance issues.\n\n### Caching Strategy\n```typescript\n// Cache key: `workspace_access:${userId}:${workspaceId}`\n// TTL: 5 minutes\n// Invalidate on: explicit member changes, user re-auth\n\nconst cacheKey = `workspace_access:${userId}:${workspaceId}`;\nconst cached = await redis.get(cacheKey);\nif (cached) return cached === 'true';\n\n// ... check access ...\nawait redis.setex(cacheKey, 300, hasAccess ? 'true' : 'false');\n```\n\n### Dashboard Changes\n- Update workspace list API call to use `/api/workspaces/accessible`\n- Show access type badge (Owner, Member, Contributor)\n- Contributor = read-only for workspace settings, full access to chat/agents\n\n### Permission Levels\n| GitHub Permission | Workspace Access |\n|-------------------|------------------|\n| admin | Full access (can manage workspace) |\n| write | Can interact with agents, send messages |\n| read | Can view dashboard, read messages |\n| none | No access |\n\n## Edge Cases\n1. User loses GitHub repo access → next request returns 403\n2. Repo removed from workspace → user loses access (if no other repos)\n3. User has access to multiple repos in workspace → still one access grant\n4. Private repo access revoked → immediate loss of workspace access\n5. GitHub API rate limited → fallback to cached access or deny\n\n## Testing Plan\n1. Unit tests for `requireWorkspaceAccess` middleware\n2. Integration tests for `/api/workspaces/accessible`\n3. E2E test: user with repo access sees workspace\n4. E2E test: user without repo access gets 403\n5. Cache invalidation tests\n\n## Rollout Plan\n1. Implement middleware with feature flag\n2. Add `/api/workspaces/accessible` endpoint\n3. Update dashboard to use new endpoint\n4. Enable for beta users\n5. Monitor GitHub API usage / rate limits\n6. Full rollout","status":"open","priority":1,"issue_type":"feature","created_at":"2026-01-06T00:12:10.120238+01:00","updated_at":"2026-01-06T00:12:10.120238+01:00","dependencies":[{"issue_id":"agent-relay-462","depends_on_id":"agent-relay-461","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-463","title":"Human-to-Human Messaging and DMs","description":"# Feature: Human-to-Human Messaging and DMs\n\nEnable human users to send direct messages to each other and to agents within a workspace. Messages are persisted, searchable, and delivered in real-time.\n\n## Current State\n- WebSocket presence system exists (`/ws/presence`)\n- Shows online users, typing indicators\n- No message storage or delivery\n- Existing `Message` type is for agent-to-agent only\n- No DM database tables\n\n## Desired Behavior\n\n### User Stories\n1. As a user, I can send a DM to another online user in my workspace\n2. As a user, I can send a DM to an agent (delivered via relay)\n3. As a user, I see unread message count badge\n4. As a user, I can view message history with any user/agent\n5. As a user, I see real-time message delivery (no refresh needed)\n6. As a user, I can search my message history\n\n## Technical Spec\n\n### Database Schema\n\n```sql\n-- Conversations (DM threads between two parties)\nCREATE TABLE conversations (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n workspace_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,\n -- Participants stored as sorted pair for uniqueness\n participant_a UUID NOT NULL, -- Always lower UUID\n participant_b UUID NOT NULL, -- Always higher UUID\n participant_a_type VARCHAR(20) NOT NULL, -- 'user' | 'agent'\n participant_b_type VARCHAR(20) NOT NULL, -- 'user' | 'agent'\n last_message_at TIMESTAMP,\n created_at TIMESTAMP DEFAULT NOW(),\n UNIQUE(workspace_id, participant_a, participant_b)\n);\n\n-- Messages\nCREATE TABLE direct_messages (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,\n sender_id VARCHAR(255) NOT NULL, -- User UUID or agent name\n sender_type VARCHAR(20) NOT NULL, -- 'user' | 'agent'\n content TEXT NOT NULL,\n content_type VARCHAR(20) DEFAULT 'text', -- 'text' | 'code' | 'markdown'\n metadata JSONB DEFAULT '{}', -- For future: attachments, reactions\n created_at TIMESTAMP DEFAULT NOW(),\n edited_at TIMESTAMP,\n deleted_at TIMESTAMP -- Soft delete\n);\nCREATE INDEX idx_dm_conversation ON direct_messages(conversation_id);\nCREATE INDEX idx_dm_created ON direct_messages(created_at);\n\n-- Read receipts / unread tracking\nCREATE TABLE message_read_state (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n user_id UUID REFERENCES users(id) ON DELETE CASCADE,\n conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,\n last_read_at TIMESTAMP NOT NULL,\n last_read_message_id UUID REFERENCES direct_messages(id),\n UNIQUE(user_id, conversation_id)\n);\nCREATE INDEX idx_read_state_user ON message_read_state(user_id);\n```\n\n### Drizzle Schema\n\n```typescript\n// src/cloud/db/schema.ts\n\nexport const conversations = pgTable('conversations', {\n id: uuid('id').primaryKey().defaultRandom(),\n workspaceId: uuid('workspace_id').references(() =\u003e workspaces.id, { onDelete: 'cascade' }),\n participantA: uuid('participant_a').notNull(),\n participantB: uuid('participant_b').notNull(),\n participantAType: varchar('participant_a_type', { length: 20 }).notNull(),\n participantBType: varchar('participant_b_type', { length: 20 }).notNull(),\n lastMessageAt: timestamp('last_message_at'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n}, (table) =\u003e ({\n uniqueParticipants: unique().on(table.workspaceId, table.participantA, table.participantB),\n}));\n\nexport const directMessages = pgTable('direct_messages', {\n id: uuid('id').primaryKey().defaultRandom(),\n conversationId: uuid('conversation_id').references(() =\u003e conversations.id, { onDelete: 'cascade' }).notNull(),\n senderId: varchar('sender_id', { length: 255 }).notNull(),\n senderType: varchar('sender_type', { length: 20 }).notNull(),\n content: text('content').notNull(),\n contentType: varchar('content_type', { length: 20 }).default('text'),\n metadata: jsonb('metadata').default({}),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n editedAt: timestamp('edited_at'),\n deletedAt: timestamp('deleted_at'),\n}, (table) =\u003e ({\n conversationIdx: index('idx_dm_conversation').on(table.conversationId),\n createdIdx: index('idx_dm_created').on(table.createdAt),\n}));\n\nexport const messageReadState = pgTable('message_read_state', {\n id: uuid('id').primaryKey().defaultRandom(),\n userId: uuid('user_id').references(() =\u003e users.id, { onDelete: 'cascade' }).notNull(),\n conversationId: uuid('conversation_id').references(() =\u003e conversations.id, { onDelete: 'cascade' }).notNull(),\n lastReadAt: timestamp('last_read_at').notNull(),\n lastReadMessageId: uuid('last_read_message_id').references(() =\u003e directMessages.id),\n}, (table) =\u003e ({\n uniqueUserConvo: unique().on(table.userId, table.conversationId),\n userIdx: index('idx_read_state_user').on(table.userId),\n}));\n```\n\n### API Endpoints\n\n```typescript\n// src/cloud/api/messages.ts\n\n// GET /api/messages/conversations\n// List all conversations for current user in a workspace\n{\n conversations: [{\n id: string,\n otherParticipant: { id: string, type: 'user'|'agent', name: string, avatarUrl?: string },\n lastMessage: { content: string, createdAt: string, senderId: string },\n unreadCount: number\n }]\n}\n\n// GET /api/messages/conversations/:conversationId\n// Get messages in a conversation (paginated)\n{\n messages: [{\n id: string,\n senderId: string,\n senderType: 'user' | 'agent',\n senderName: string,\n content: string,\n contentType: string,\n createdAt: string,\n editedAt?: string\n }],\n hasMore: boolean,\n nextCursor?: string\n}\n\n// POST /api/messages/send\n// Send a message\n{\n recipientId: string, // User UUID or agent name\n recipientType: 'user' | 'agent',\n workspaceId: string,\n content: string,\n contentType?: 'text' | 'code' | 'markdown'\n}\n// Returns: { message: Message, conversationId: string }\n\n// POST /api/messages/conversations/:conversationId/read\n// Mark conversation as read\n// Returns: { success: true, lastReadAt: string }\n\n// GET /api/messages/unread-count\n// Get total unread count for badge\n{ unreadCount: number }\n```\n\n### WebSocket Enhancement\n\nExtend existing presence WebSocket to handle DM delivery:\n\n```typescript\n// New message types for /ws/presence\n\n// Client -\u003e Server: Send DM\n{\n type: 'dm_send',\n recipientId: string,\n recipientType: 'user' | 'agent',\n content: string,\n tempId: string // Client-side ID for optimistic UI\n}\n\n// Server -\u003e Client: DM received\n{\n type: 'dm_received',\n message: {\n id: string,\n conversationId: string,\n senderId: string,\n senderType: string,\n senderName: string,\n senderAvatarUrl?: string,\n content: string,\n createdAt: string\n }\n}\n\n// Server -\u003e Client: DM sent confirmation\n{\n type: 'dm_sent',\n tempId: string, // Matches client tempId\n message: Message\n}\n\n// Server -\u003e Client: Typing indicator (already exists, extend)\n{\n type: 'dm_typing',\n conversationId: string,\n userId: string,\n userName: string\n}\n```\n\n### Agent Message Bridging\n\nWhen sending DM to an agent:\n1. Store message in `direct_messages` table\n2. Forward to agent via relay daemon WebSocket\n3. Agent responses come back via relay → stored as agent message\n\n```typescript\n// Bridge handler in server.ts\nif (recipientType === 'agent') {\n // Find workspace daemon connection\n const daemon = await findDaemonForWorkspace(workspaceId);\n if (daemon?.ws) {\n // Forward as relay message\n daemon.ws.send(JSON.stringify({\n type: 'DELIVER',\n payload: {\n from: { name: 'Dashboard', kind: 'user', userId: senderId },\n to: recipientId, // Agent name\n body: content,\n kind: 'dm'\n }\n }));\n }\n}\n```\n\n### Dashboard Components\n\n```typescript\n// New components needed\n\n// 1. ConversationList - Sidebar showing all DM threads\ninterface ConversationListProps {\n workspaceId: string;\n onSelectConversation: (id: string) =\u003e void;\n selectedId?: string;\n}\n\n// 2. MessageThread - Main chat view\ninterface MessageThreadProps {\n conversationId: string;\n onSendMessage: (content: string) =\u003e void;\n}\n\n// 3. MessageInput - Text input with send button\ninterface MessageInputProps {\n onSend: (content: string) =\u003e void;\n onTyping: () =\u003e void;\n disabled?: boolean;\n}\n\n// 4. UnreadBadge - Shows unread count\ninterface UnreadBadgeProps {\n count: number;\n}\n\n// 5. UserSelector - For starting new DM\ninterface UserSelectorProps {\n workspaceId: string;\n onSelectUser: (user: User | Agent) =\u003e void;\n}\n```\n\n### UI Layout\n\n```\n+------------------+------------------------+\n| Workspace Nav | |\n+------------------+ |\n| Conversations | Message Thread |\n| - User A (2) | |\n| - Agent: Lead | [Messages here] |\n| - User B | |\n| | |\n| [+ New DM] | [Input box] |\n+------------------+------------------------+\n```\n\n## Edge Cases\n\n1. **Offline recipient**: Store message, deliver when they reconnect\n2. **Agent not running**: Queue message, show \"Agent offline\" status\n3. **User blocked (future)**: Don't allow message send\n4. **Rate limiting**: Max 60 messages/minute per user\n5. **Large message**: Max 10KB content, reject larger\n6. **Deleted user**: Show \"[Deleted User]\" in history\n7. **Cross-workspace DM**: Not allowed - workspace scoped only\n\n## Performance Considerations\n\n1. **Message pagination**: 50 messages per page, cursor-based\n2. **Conversation list**: Limit to 100 most recent, lazy load more\n3. **Unread count**: Cached in Redis, invalidated on new message\n4. **Typing indicator**: Debounced, expires after 3s\n5. **WebSocket reconnect**: Re-fetch unread on reconnect\n\n## Security\n\n1. **Access control**: Only workspace members can DM within workspace\n2. **Content sanitization**: Strip HTML, escape special chars\n3. **Rate limiting**: Per-user limits to prevent spam\n4. **Audit logging**: Log message sends for compliance (optional)\n\n## Testing Plan\n\n1. Unit tests for message service\n2. Integration tests for API endpoints\n3. WebSocket delivery tests\n4. E2E test: send DM, see in recipient's UI\n5. E2E test: agent DM bridging\n6. Load test: 100 concurrent users messaging\n\n## Rollout Plan\n\n1. Schema migration (non-breaking)\n2. Implement API endpoints\n3. Add WebSocket handlers\n4. Build UI components\n5. Feature flag: enable for beta\n6. Monitor performance metrics\n7. Full rollout","status":"open","priority":1,"issue_type":"feature","created_at":"2026-01-06T00:13:16.622612+01:00","updated_at":"2026-01-06T00:13:16.622612+01:00","dependencies":[{"issue_id":"agent-relay-463","depends_on_id":"agent-relay-461","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"agent-relay-463","depends_on_id":"agent-relay-462","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-464","title":"Implement requireWorkspaceAccess middleware","description":"Create middleware that checks if user can access a workspace based on:\n1. Workspace ownership (userId matches)\n2. Explicit workspace_members record\n3. GitHub repo access (just-in-time check via Nango)\n\nLocation: src/cloud/api/workspaces.ts\n\nInclude:\n- Caching layer (5 min TTL) to reduce GitHub API calls\n- Proper error handling for Nango failures\n- Logging for access decisions\n\nTests:\n- Unit tests with mocked Nango service\n- Integration tests with test database","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:13:42.610203+01:00","updated_at":"2026-01-06T00:13:42.610203+01:00","dependencies":[{"issue_id":"agent-relay-464","depends_on_id":"agent-relay-462","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-465","title":"Add GET /api/workspaces/accessible endpoint","description":"Create endpoint that returns all workspaces a user can access:\n- Workspaces they own\n- Workspaces where they're a member\n- Workspaces containing repos they have GitHub access to\n\nResponse shape:\n{\n workspaces: [{\n id, name, status,\n accessType: 'owner' | 'member' | 'contributor',\n repositories: [{ fullName, permission }]\n }]\n}\n\nPerformance:\n- Batch GitHub repo access checks\n- Use existing listUserAccessibleRepos() from Nango service\n- Cache results\n\nLocation: src/cloud/api/workspaces.ts","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:13:53.283204+01:00","updated_at":"2026-01-06T00:13:53.283204+01:00","dependencies":[{"issue_id":"agent-relay-465","depends_on_id":"agent-relay-462","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-466","title":"Update dashboard to use /api/workspaces/accessible","description":"Update the dashboard workspace list to:\n1. Call new /api/workspaces/accessible endpoint\n2. Display access type badge (Owner, Member, Contributor)\n3. Show contributor workspaces in the list\n4. Handle \"contributor\" role permissions (read-only settings)\n\nFiles to update:\n- src/dashboard/react-components/WorkspaceList.tsx (or equivalent)\n- src/dashboard/lib/api.ts (add new API call)\n\nUI changes:\n- Add badge component for access type\n- Contributor badge: \"Via GitHub\" or similar\n- Different styling for owned vs contributed workspaces","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:02.829368+01:00","updated_at":"2026-01-06T00:14:02.829368+01:00","dependencies":[{"issue_id":"agent-relay-466","depends_on_id":"agent-relay-465","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-467","title":"Add messaging database schema (conversations, direct_messages, read_state)","description":"Add three new tables to Drizzle schema:\n\n1. conversations\n - id, workspace_id, participant_a, participant_b\n - participant_a_type, participant_b_type ('user' | 'agent')\n - last_message_at, created_at\n - Unique constraint on (workspace_id, participant_a, participant_b)\n\n2. direct_messages\n - id, conversation_id, sender_id, sender_type\n - content, content_type ('text' | 'code' | 'markdown')\n - metadata (JSONB), created_at, edited_at, deleted_at\n - Indexes on conversation_id, created_at\n\n3. message_read_state\n - id, user_id, conversation_id\n - last_read_at, last_read_message_id\n - Unique constraint on (user_id, conversation_id)\n\nLocation: src/cloud/db/schema.ts\nGenerate migration: npm run db:generate","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:20.392798+01:00","updated_at":"2026-01-06T00:14:20.392798+01:00","dependencies":[{"issue_id":"agent-relay-467","depends_on_id":"agent-relay-463","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-468","title":"Implement messages API endpoints","description":"Create new router: src/cloud/api/messages.ts\n\nEndpoints:\n1. GET /api/messages/conversations\n - List all conversations for user in workspace\n - Include unread count per conversation\n - Sort by last_message_at descending\n\n2. GET /api/messages/conversations/:conversationId\n - Paginated message list (50 per page, cursor-based)\n - Include sender name/avatar lookup\n\n3. POST /api/messages/send\n - Body: { recipientId, recipientType, workspaceId, content, contentType? }\n - Create conversation if doesn't exist\n - Store message\n - Return message + conversationId\n\n4. POST /api/messages/conversations/:conversationId/read\n - Update message_read_state\n - Return success\n\n5. GET /api/messages/unread-count\n - Total unread across all conversations\n - For badge display\n\nAll endpoints require workspace access (use requireWorkspaceAccess middleware)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:32.043028+01:00","updated_at":"2026-01-06T00:14:32.043028+01:00","dependencies":[{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-467","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-464","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-469","title":"Extend presence WebSocket for real-time DM delivery","description":"Extend /ws/presence WebSocket handler to support DM delivery:\n\nNew message types:\n1. dm_send (client-\u003eserver)\n - recipientId, recipientType, content, tempId\n - Store message via messages API\n - Forward to recipient's WebSocket connection\n\n2. dm_received (server-\u003eclient)\n - Full message object\n - Sent to recipient when they're online\n\n3. dm_sent (server-\u003eclient)\n - Confirmation with tempId for optimistic UI\n - Sent back to sender\n\n4. dm_typing (both directions)\n - conversationId, userId, userName\n - Debounced, auto-expires after 3s\n\nAgent bridging:\n- If recipientType='agent', forward via daemon WebSocket\n- Store message for history\n- Handle agent responses coming back\n\nLocation: src/cloud/server.ts (presence WebSocket section)\n\nConsider: separate /ws/messages endpoint vs extending presence","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:44.150375+01:00","updated_at":"2026-01-06T00:14:44.150375+01:00","dependencies":[{"issue_id":"agent-relay-469","depends_on_id":"agent-relay-468","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} -{"id":"agent-relay-470","title":"Build messaging UI components for dashboard","description":"Create React components for messaging:\n\n1. ConversationList\n - Sidebar showing all DM threads\n - Unread count badges\n - Click to select conversation\n - \"New DM\" button\n\n2. MessageThread\n - Main chat view\n - Infinite scroll for history\n - New message indicator\n - Typing indicator display\n\n3. MessageInput\n - Text input with send button\n - Typing indicator emission\n - Markdown/code toggle (optional v1)\n\n4. UserSelector\n - Modal/dropdown to start new DM\n - Show online workspace members + agents\n - Search/filter\n\n5. UnreadBadge\n - Notification count component\n - Used in navigation\n\nIntegration:\n- Add to workspace dashboard layout\n- Connect to WebSocket for real-time\n- Use API for initial data + history\n\nLocation: src/dashboard/react-components/messaging/\nStyling: Follow existing Tailwind patterns","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:56.997803+01:00","updated_at":"2026-01-06T00:14:56.997803+01:00","dependencies":[{"issue_id":"agent-relay-470","depends_on_id":"agent-relay-469","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agent-relay-462","title":"Auto Workspace Access from GitHub Repo Permissions","description":"# Feature: Auto Workspace Access from GitHub Repo Permissions\n\nWhen a user authenticates and has GitHub access to a repository that belongs to a workspace, they should automatically gain access to that workspace's dashboard, chat, and agent communication.\n\n## Current State\n- Nango service has `checkUserRepoAccess()` and `listUserAccessibleRepos()`\n- `repositories` table has `github_full_name` and links to `workspace_id`\n- `workspace_members` table exists but requires explicit invitation\n- No automatic access granting based on repo permissions\n\n## Desired Behavior\n\n### Flow 1: Dashboard Load\n1. User logs into dashboard\n2. System fetches user's accessible GitHub repos via Nango\n3. System queries `repositories` table for matches on `github_full_name`\n4. For each matching repo, find associated `workspace_id`\n5. User sees all matched workspaces in their dashboard\n6. No explicit `workspace_member` record required (just-in-time access)\n\n### Flow 2: Workspace Route Access\n1. User navigates to `/workspace/:id`\n2. Middleware checks: does user have GitHub access to ANY repo in this workspace?\n3. If yes → allow access with \"contributor\" role\n4. If no → 403 Forbidden\n\n### Flow 3: Real-time Sync\n1. When user's GitHub permissions change (added/removed from repo)\n2. Next dashboard load reflects new workspace access\n3. Consider: webhook from GitHub for real-time updates (future)\n\n## Technical Spec\n\n### New Middleware: `requireWorkspaceAccess`\nLocation: `src/cloud/api/workspaces.ts`\n\n```typescript\nasync function requireWorkspaceAccess(req, res, next) {\n const userId = req.session.userId;\n const workspaceId = req.params.workspaceId;\n \n // 1. Check if user is workspace owner\n const workspace = await db.workspaces.findById(workspaceId);\n if (workspace?.userId === userId) return next();\n \n // 2. Check explicit workspace_members\n const member = await db.workspaceMembers.findByUserAndWorkspace(userId, workspaceId);\n if (member) return next();\n \n // 3. Check GitHub repo access (just-in-time)\n const repos = await db.repositories.findByWorkspace(workspaceId);\n const user = await db.users.findById(userId);\n \n for (const repo of repos) {\n const access = await nango.checkUserRepoAccess(\n user.nangoConnectionId, \n repo.githubFullName\n );\n if (access.permission !== 'none') {\n // Cache this for performance (optional: create temp member)\n return next();\n }\n }\n \n return res.status(403).json({ error: 'No access to this workspace' });\n}\n```\n\n### New API Endpoint: `GET /api/workspaces/accessible`\nReturns all workspaces user can access (owned + member + repo-based)\n\n```typescript\n// Response shape\n{\n workspaces: [\n {\n id: string,\n name: string,\n accessType: 'owner' | 'member' | 'contributor', // contributor = via repo\n repositories: [{ fullName: string, permission: string }],\n status: string\n }\n ]\n}\n```\n\n### Database Considerations\nOption A: Just-in-time checks (no schema change)\n- Check GitHub access on every request\n- Cache results in Redis/memory with TTL\n- Pro: Always up-to-date\n- Con: API latency, rate limits\n\nOption B: Sync workspace_members periodically\n- Background job syncs GitHub permissions to workspace_members\n- Add `accessType` column: 'invited' | 'github_repo'\n- Pro: Fast reads\n- Con: Can be stale\n\n**Recommendation**: Start with Option A + caching, move to Option B if performance issues.\n\n### Caching Strategy\n```typescript\n// Cache key: `workspace_access:${userId}:${workspaceId}`\n// TTL: 5 minutes\n// Invalidate on: explicit member changes, user re-auth\n\nconst cacheKey = `workspace_access:${userId}:${workspaceId}`;\nconst cached = await redis.get(cacheKey);\nif (cached) return cached === 'true';\n\n// ... check access ...\nawait redis.setex(cacheKey, 300, hasAccess ? 'true' : 'false');\n```\n\n### Dashboard Changes\n- Update workspace list API call to use `/api/workspaces/accessible`\n- Show access type badge (Owner, Member, Contributor)\n- Contributor = read-only for workspace settings, full access to chat/agents\n\n### Permission Levels\n| GitHub Permission | Workspace Access |\n|-------------------|------------------|\n| admin | Full access (can manage workspace) |\n| write | Can interact with agents, send messages |\n| read | Can view dashboard, read messages |\n| none | No access |\n\n## Edge Cases\n1. User loses GitHub repo access → next request returns 403\n2. Repo removed from workspace → user loses access (if no other repos)\n3. User has access to multiple repos in workspace → still one access grant\n4. Private repo access revoked → immediate loss of workspace access\n5. GitHub API rate limited → fallback to cached access or deny\n\n## Testing Plan\n1. Unit tests for `requireWorkspaceAccess` middleware\n2. Integration tests for `/api/workspaces/accessible`\n3. E2E test: user with repo access sees workspace\n4. E2E test: user without repo access gets 403\n5. Cache invalidation tests\n\n## Rollout Plan\n1. Implement middleware with feature flag\n2. Add `/api/workspaces/accessible` endpoint\n3. Update dashboard to use new endpoint\n4. Enable for beta users\n5. Monitor GitHub API usage / rate limits\n6. Full rollout","status":"open","priority":1,"issue_type":"feature","created_at":"2026-01-06T00:12:10.120238+01:00","updated_at":"2026-01-06T00:12:10.120238+01:00","dependencies":[{"issue_id":"agent-relay-462","depends_on_id":"agent-relay-461","type":"blocks","created_at":"2026-01-06T14:07:38.450932+01:00","created_by":"import"}]} +{"id":"agent-relay-463","title":"Human-to-Human Messaging and DMs","description":"# Feature: Human-to-Human Messaging and DMs\n\nEnable human users to send direct messages to each other and to agents within a workspace. Messages are persisted, searchable, and delivered in real-time.\n\n## Current State\n- WebSocket presence system exists (`/ws/presence`)\n- Shows online users, typing indicators\n- No message storage or delivery\n- Existing `Message` type is for agent-to-agent only\n- No DM database tables\n\n## Desired Behavior\n\n### User Stories\n1. As a user, I can send a DM to another online user in my workspace\n2. As a user, I can send a DM to an agent (delivered via relay)\n3. As a user, I see unread message count badge\n4. As a user, I can view message history with any user/agent\n5. As a user, I see real-time message delivery (no refresh needed)\n6. As a user, I can search my message history\n\n## Technical Spec\n\n### Database Schema\n\n```sql\n-- Conversations (DM threads between two parties)\nCREATE TABLE conversations (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n workspace_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,\n -- Participants stored as sorted pair for uniqueness\n participant_a UUID NOT NULL, -- Always lower UUID\n participant_b UUID NOT NULL, -- Always higher UUID\n participant_a_type VARCHAR(20) NOT NULL, -- 'user' | 'agent'\n participant_b_type VARCHAR(20) NOT NULL, -- 'user' | 'agent'\n last_message_at TIMESTAMP,\n created_at TIMESTAMP DEFAULT NOW(),\n UNIQUE(workspace_id, participant_a, participant_b)\n);\n\n-- Messages\nCREATE TABLE direct_messages (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,\n sender_id VARCHAR(255) NOT NULL, -- User UUID or agent name\n sender_type VARCHAR(20) NOT NULL, -- 'user' | 'agent'\n content TEXT NOT NULL,\n content_type VARCHAR(20) DEFAULT 'text', -- 'text' | 'code' | 'markdown'\n metadata JSONB DEFAULT '{}', -- For future: attachments, reactions\n created_at TIMESTAMP DEFAULT NOW(),\n edited_at TIMESTAMP,\n deleted_at TIMESTAMP -- Soft delete\n);\nCREATE INDEX idx_dm_conversation ON direct_messages(conversation_id);\nCREATE INDEX idx_dm_created ON direct_messages(created_at);\n\n-- Read receipts / unread tracking\nCREATE TABLE message_read_state (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n user_id UUID REFERENCES users(id) ON DELETE CASCADE,\n conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,\n last_read_at TIMESTAMP NOT NULL,\n last_read_message_id UUID REFERENCES direct_messages(id),\n UNIQUE(user_id, conversation_id)\n);\nCREATE INDEX idx_read_state_user ON message_read_state(user_id);\n```\n\n### Drizzle Schema\n\n```typescript\n// src/cloud/db/schema.ts\n\nexport const conversations = pgTable('conversations', {\n id: uuid('id').primaryKey().defaultRandom(),\n workspaceId: uuid('workspace_id').references(() =\u003e workspaces.id, { onDelete: 'cascade' }),\n participantA: uuid('participant_a').notNull(),\n participantB: uuid('participant_b').notNull(),\n participantAType: varchar('participant_a_type', { length: 20 }).notNull(),\n participantBType: varchar('participant_b_type', { length: 20 }).notNull(),\n lastMessageAt: timestamp('last_message_at'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n}, (table) =\u003e ({\n uniqueParticipants: unique().on(table.workspaceId, table.participantA, table.participantB),\n}));\n\nexport const directMessages = pgTable('direct_messages', {\n id: uuid('id').primaryKey().defaultRandom(),\n conversationId: uuid('conversation_id').references(() =\u003e conversations.id, { onDelete: 'cascade' }).notNull(),\n senderId: varchar('sender_id', { length: 255 }).notNull(),\n senderType: varchar('sender_type', { length: 20 }).notNull(),\n content: text('content').notNull(),\n contentType: varchar('content_type', { length: 20 }).default('text'),\n metadata: jsonb('metadata').default({}),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n editedAt: timestamp('edited_at'),\n deletedAt: timestamp('deleted_at'),\n}, (table) =\u003e ({\n conversationIdx: index('idx_dm_conversation').on(table.conversationId),\n createdIdx: index('idx_dm_created').on(table.createdAt),\n}));\n\nexport const messageReadState = pgTable('message_read_state', {\n id: uuid('id').primaryKey().defaultRandom(),\n userId: uuid('user_id').references(() =\u003e users.id, { onDelete: 'cascade' }).notNull(),\n conversationId: uuid('conversation_id').references(() =\u003e conversations.id, { onDelete: 'cascade' }).notNull(),\n lastReadAt: timestamp('last_read_at').notNull(),\n lastReadMessageId: uuid('last_read_message_id').references(() =\u003e directMessages.id),\n}, (table) =\u003e ({\n uniqueUserConvo: unique().on(table.userId, table.conversationId),\n userIdx: index('idx_read_state_user').on(table.userId),\n}));\n```\n\n### API Endpoints\n\n```typescript\n// src/cloud/api/messages.ts\n\n// GET /api/messages/conversations\n// List all conversations for current user in a workspace\n{\n conversations: [{\n id: string,\n otherParticipant: { id: string, type: 'user'|'agent', name: string, avatarUrl?: string },\n lastMessage: { content: string, createdAt: string, senderId: string },\n unreadCount: number\n }]\n}\n\n// GET /api/messages/conversations/:conversationId\n// Get messages in a conversation (paginated)\n{\n messages: [{\n id: string,\n senderId: string,\n senderType: 'user' | 'agent',\n senderName: string,\n content: string,\n contentType: string,\n createdAt: string,\n editedAt?: string\n }],\n hasMore: boolean,\n nextCursor?: string\n}\n\n// POST /api/messages/send\n// Send a message\n{\n recipientId: string, // User UUID or agent name\n recipientType: 'user' | 'agent',\n workspaceId: string,\n content: string,\n contentType?: 'text' | 'code' | 'markdown'\n}\n// Returns: { message: Message, conversationId: string }\n\n// POST /api/messages/conversations/:conversationId/read\n// Mark conversation as read\n// Returns: { success: true, lastReadAt: string }\n\n// GET /api/messages/unread-count\n// Get total unread count for badge\n{ unreadCount: number }\n```\n\n### WebSocket Enhancement\n\nExtend existing presence WebSocket to handle DM delivery:\n\n```typescript\n// New message types for /ws/presence\n\n// Client -\u003e Server: Send DM\n{\n type: 'dm_send',\n recipientId: string,\n recipientType: 'user' | 'agent',\n content: string,\n tempId: string // Client-side ID for optimistic UI\n}\n\n// Server -\u003e Client: DM received\n{\n type: 'dm_received',\n message: {\n id: string,\n conversationId: string,\n senderId: string,\n senderType: string,\n senderName: string,\n senderAvatarUrl?: string,\n content: string,\n createdAt: string\n }\n}\n\n// Server -\u003e Client: DM sent confirmation\n{\n type: 'dm_sent',\n tempId: string, // Matches client tempId\n message: Message\n}\n\n// Server -\u003e Client: Typing indicator (already exists, extend)\n{\n type: 'dm_typing',\n conversationId: string,\n userId: string,\n userName: string\n}\n```\n\n### Agent Message Bridging\n\nWhen sending DM to an agent:\n1. Store message in `direct_messages` table\n2. Forward to agent via relay daemon WebSocket\n3. Agent responses come back via relay → stored as agent message\n\n```typescript\n// Bridge handler in server.ts\nif (recipientType === 'agent') {\n // Find workspace daemon connection\n const daemon = await findDaemonForWorkspace(workspaceId);\n if (daemon?.ws) {\n // Forward as relay message\n daemon.ws.send(JSON.stringify({\n type: 'DELIVER',\n payload: {\n from: { name: 'Dashboard', kind: 'user', userId: senderId },\n to: recipientId, // Agent name\n body: content,\n kind: 'dm'\n }\n }));\n }\n}\n```\n\n### Dashboard Components\n\n```typescript\n// New components needed\n\n// 1. ConversationList - Sidebar showing all DM threads\ninterface ConversationListProps {\n workspaceId: string;\n onSelectConversation: (id: string) =\u003e void;\n selectedId?: string;\n}\n\n// 2. MessageThread - Main chat view\ninterface MessageThreadProps {\n conversationId: string;\n onSendMessage: (content: string) =\u003e void;\n}\n\n// 3. MessageInput - Text input with send button\ninterface MessageInputProps {\n onSend: (content: string) =\u003e void;\n onTyping: () =\u003e void;\n disabled?: boolean;\n}\n\n// 4. UnreadBadge - Shows unread count\ninterface UnreadBadgeProps {\n count: number;\n}\n\n// 5. UserSelector - For starting new DM\ninterface UserSelectorProps {\n workspaceId: string;\n onSelectUser: (user: User | Agent) =\u003e void;\n}\n```\n\n### UI Layout\n\n```\n+------------------+------------------------+\n| Workspace Nav | |\n+------------------+ |\n| Conversations | Message Thread |\n| - User A (2) | |\n| - Agent: Lead | [Messages here] |\n| - User B | |\n| | |\n| [+ New DM] | [Input box] |\n+------------------+------------------------+\n```\n\n## Edge Cases\n\n1. **Offline recipient**: Store message, deliver when they reconnect\n2. **Agent not running**: Queue message, show \"Agent offline\" status\n3. **User blocked (future)**: Don't allow message send\n4. **Rate limiting**: Max 60 messages/minute per user\n5. **Large message**: Max 10KB content, reject larger\n6. **Deleted user**: Show \"[Deleted User]\" in history\n7. **Cross-workspace DM**: Not allowed - workspace scoped only\n\n## Performance Considerations\n\n1. **Message pagination**: 50 messages per page, cursor-based\n2. **Conversation list**: Limit to 100 most recent, lazy load more\n3. **Unread count**: Cached in Redis, invalidated on new message\n4. **Typing indicator**: Debounced, expires after 3s\n5. **WebSocket reconnect**: Re-fetch unread on reconnect\n\n## Security\n\n1. **Access control**: Only workspace members can DM within workspace\n2. **Content sanitization**: Strip HTML, escape special chars\n3. **Rate limiting**: Per-user limits to prevent spam\n4. **Audit logging**: Log message sends for compliance (optional)\n\n## Testing Plan\n\n1. Unit tests for message service\n2. Integration tests for API endpoints\n3. WebSocket delivery tests\n4. E2E test: send DM, see in recipient's UI\n5. E2E test: agent DM bridging\n6. Load test: 100 concurrent users messaging\n\n## Rollout Plan\n\n1. Schema migration (non-breaking)\n2. Implement API endpoints\n3. Add WebSocket handlers\n4. Build UI components\n5. Feature flag: enable for beta\n6. Monitor performance metrics\n7. Full rollout","status":"open","priority":1,"issue_type":"feature","created_at":"2026-01-06T00:13:16.622612+01:00","updated_at":"2026-01-06T00:13:16.622612+01:00","dependencies":[{"issue_id":"agent-relay-463","depends_on_id":"agent-relay-461","type":"blocks","created_at":"2026-01-06T14:07:38.45176+01:00","created_by":"import"},{"issue_id":"agent-relay-463","depends_on_id":"agent-relay-462","type":"blocks","created_at":"2026-01-06T14:07:38.452492+01:00","created_by":"import"}]} +{"id":"agent-relay-464","title":"Implement requireWorkspaceAccess middleware","description":"Create middleware that checks if user can access a workspace based on:\n1. Workspace ownership (userId matches)\n2. Explicit workspace_members record\n3. GitHub repo access (just-in-time check via Nango)\n\nLocation: src/cloud/api/workspaces.ts\n\nInclude:\n- Caching layer (5 min TTL) to reduce GitHub API calls\n- Proper error handling for Nango failures\n- Logging for access decisions\n\nTests:\n- Unit tests with mocked Nango service\n- Integration tests with test database","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:13:42.610203+01:00","updated_at":"2026-01-06T00:13:42.610203+01:00","dependencies":[{"issue_id":"agent-relay-464","depends_on_id":"agent-relay-462","type":"blocks","created_at":"2026-01-06T14:07:38.453374+01:00","created_by":"import"}]} +{"id":"agent-relay-465","title":"Add GET /api/workspaces/accessible endpoint","description":"Create endpoint that returns all workspaces a user can access:\n- Workspaces they own\n- Workspaces where they're a member\n- Workspaces containing repos they have GitHub access to\n\nResponse shape:\n{\n workspaces: [{\n id, name, status,\n accessType: 'owner' | 'member' | 'contributor',\n repositories: [{ fullName, permission }]\n }]\n}\n\nPerformance:\n- Batch GitHub repo access checks\n- Use existing listUserAccessibleRepos() from Nango service\n- Cache results\n\nLocation: src/cloud/api/workspaces.ts","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:13:53.283204+01:00","updated_at":"2026-01-06T00:13:53.283204+01:00","dependencies":[{"issue_id":"agent-relay-465","depends_on_id":"agent-relay-462","type":"blocks","created_at":"2026-01-06T14:07:38.454163+01:00","created_by":"import"}]} +{"id":"agent-relay-466","title":"Update dashboard to use /api/workspaces/accessible","description":"Update the dashboard workspace list to:\n1. Call new /api/workspaces/accessible endpoint\n2. Display access type badge (Owner, Member, Contributor)\n3. Show contributor workspaces in the list\n4. Handle \"contributor\" role permissions (read-only settings)\n\nFiles to update:\n- src/dashboard/react-components/WorkspaceList.tsx (or equivalent)\n- src/dashboard/lib/api.ts (add new API call)\n\nUI changes:\n- Add badge component for access type\n- Contributor badge: \"Via GitHub\" or similar\n- Different styling for owned vs contributed workspaces","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:02.829368+01:00","updated_at":"2026-01-06T00:14:02.829368+01:00","dependencies":[{"issue_id":"agent-relay-466","depends_on_id":"agent-relay-465","type":"blocks","created_at":"2026-01-06T14:07:38.454971+01:00","created_by":"import"}]} +{"id":"agent-relay-467","title":"Add messaging database schema (conversations, direct_messages, read_state)","description":"Add three new tables to Drizzle schema:\n\n1. conversations\n - id, workspace_id, participant_a, participant_b\n - participant_a_type, participant_b_type ('user' | 'agent')\n - last_message_at, created_at\n - Unique constraint on (workspace_id, participant_a, participant_b)\n\n2. direct_messages\n - id, conversation_id, sender_id, sender_type\n - content, content_type ('text' | 'code' | 'markdown')\n - metadata (JSONB), created_at, edited_at, deleted_at\n - Indexes on conversation_id, created_at\n\n3. message_read_state\n - id, user_id, conversation_id\n - last_read_at, last_read_message_id\n - Unique constraint on (user_id, conversation_id)\n\nLocation: src/cloud/db/schema.ts\nGenerate migration: npm run db:generate","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:20.392798+01:00","updated_at":"2026-01-06T00:14:20.392798+01:00","dependencies":[{"issue_id":"agent-relay-467","depends_on_id":"agent-relay-463","type":"blocks","created_at":"2026-01-06T14:07:38.456256+01:00","created_by":"import"}]} +{"id":"agent-relay-468","title":"Implement messages API endpoints","description":"Create new router: src/cloud/api/messages.ts\n\nEndpoints:\n1. GET /api/messages/conversations\n - List all conversations for user in workspace\n - Include unread count per conversation\n - Sort by last_message_at descending\n\n2. GET /api/messages/conversations/:conversationId\n - Paginated message list (50 per page, cursor-based)\n - Include sender name/avatar lookup\n\n3. POST /api/messages/send\n - Body: { recipientId, recipientType, workspaceId, content, contentType? }\n - Create conversation if doesn't exist\n - Store message\n - Return message + conversationId\n\n4. POST /api/messages/conversations/:conversationId/read\n - Update message_read_state\n - Return success\n\n5. GET /api/messages/unread-count\n - Total unread across all conversations\n - For badge display\n\nAll endpoints require workspace access (use requireWorkspaceAccess middleware)","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:32.043028+01:00","updated_at":"2026-01-06T00:14:32.043028+01:00","dependencies":[{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-467","type":"blocks","created_at":"2026-01-06T14:07:38.45694+01:00","created_by":"import"},{"issue_id":"agent-relay-468","depends_on_id":"agent-relay-464","type":"blocks","created_at":"2026-01-06T14:07:38.457577+01:00","created_by":"import"}]} +{"id":"agent-relay-469","title":"Extend presence WebSocket for real-time DM delivery","description":"Extend /ws/presence WebSocket handler to support DM delivery:\n\nNew message types:\n1. dm_send (client-\u003eserver)\n - recipientId, recipientType, content, tempId\n - Store message via messages API\n - Forward to recipient's WebSocket connection\n\n2. dm_received (server-\u003eclient)\n - Full message object\n - Sent to recipient when they're online\n\n3. dm_sent (server-\u003eclient)\n - Confirmation with tempId for optimistic UI\n - Sent back to sender\n\n4. dm_typing (both directions)\n - conversationId, userId, userName\n - Debounced, auto-expires after 3s\n\nAgent bridging:\n- If recipientType='agent', forward via daemon WebSocket\n- Store message for history\n- Handle agent responses coming back\n\nLocation: src/cloud/server.ts (presence WebSocket section)\n\nConsider: separate /ws/messages endpoint vs extending presence","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:44.150375+01:00","updated_at":"2026-01-06T00:14:44.150375+01:00","dependencies":[{"issue_id":"agent-relay-469","depends_on_id":"agent-relay-468","type":"blocks","created_at":"2026-01-06T14:07:38.458202+01:00","created_by":"import"}]} +{"id":"agent-relay-470","title":"Build messaging UI components for dashboard","description":"Create React components for messaging:\n\n1. ConversationList\n - Sidebar showing all DM threads\n - Unread count badges\n - Click to select conversation\n - \"New DM\" button\n\n2. MessageThread\n - Main chat view\n - Infinite scroll for history\n - New message indicator\n - Typing indicator display\n\n3. MessageInput\n - Text input with send button\n - Typing indicator emission\n - Markdown/code toggle (optional v1)\n\n4. UserSelector\n - Modal/dropdown to start new DM\n - Show online workspace members + agents\n - Search/filter\n\n5. UnreadBadge\n - Notification count component\n - Used in navigation\n\nIntegration:\n- Add to workspace dashboard layout\n- Connect to WebSocket for real-time\n- Use API for initial data + history\n\nLocation: src/dashboard/react-components/messaging/\nStyling: Follow existing Tailwind patterns","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T00:14:56.997803+01:00","updated_at":"2026-01-06T00:14:56.997803+01:00","dependencies":[{"issue_id":"agent-relay-470","depends_on_id":"agent-relay-469","type":"blocks","created_at":"2026-01-06T14:07:38.458868+01:00","created_by":"import"}]} {"id":"agent-relay-47z","title":"Express 5 may have breaking changes from Express 4 patterns","description":"package.json uses express@5.2.1 which is a major version with breaking changes from Express 4. Verify: (1) Error handling middleware patterns, (2) Router behavior, (3) Body parsing (express.json vs body-parser).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:49.269841+01:00","updated_at":"2025-12-20T00:18:49.269841+01:00"} -{"id":"agent-relay-4e0","title":"Fix message truncation - messages cut off at source","description":"Root cause found: parser.ts:40 inline regex only captures single line. Multi-line messages are split by parsePassThrough() at line 206. Fix options: (1) Allow continuation lines in inline format, (2) Use block format for multi-line, (3) Add heuristic to join lines until next @relay pattern.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-19T23:40:35.082717+01:00","updated_at":"2025-12-20T00:03:54.806087+01:00","closed_at":"2025-12-20T00:03:54.806087+01:00"} -{"id":"agent-relay-4ft","title":"Merge project info into status command","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T21:59:52.685495+01:00","updated_at":"2025-12-19T22:06:44.276187+01:00","closed_at":"2025-12-19T22:06:44.276187+01:00"} +{"id":"agent-relay-4e0","title":"Fix message truncation - messages cut off at source","description":"Root cause found: parser.ts:40 inline regex only captures single line. Multi-line messages are split by parsePassThrough() at line 206. Fix options: (1) Allow continuation lines in inline format, (2) Use block format for multi-line, (3) Add heuristic to join lines until next @relay pattern.","status":"closed","priority":2,"issue_type":"bug","assignee":"MistyShelter","created_at":"2025-12-19T23:40:35.082717+01:00","updated_at":"2025-12-20T00:03:54.806087+01:00","closed_at":"2025-12-20T00:03:54.806087+01:00"} +{"id":"agent-relay-4ft","title":"Merge project info into status command","status":"closed","priority":2,"issue_type":"task","assignee":"Pruner","created_at":"2025-12-19T21:59:52.685495+01:00","updated_at":"2025-12-19T22:06:44.276187+01:00","closed_at":"2025-12-19T22:06:44.276187+01:00"} {"id":"agent-relay-4jv","title":"Add session discovery from existing Claude Code","description":"Auto-discover and import existing Claude Code sessions (like Maestro). Detect running claude processes, offer to wrap them with agent-relay messaging. Reduces friction for adoption - users don't have to restart their agents.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:05:01.003851+01:00","updated_at":"2025-12-23T17:05:01.003851+01:00"} {"id":"agent-relay-4oy","title":"Fix beads sync prefix mismatch errors","description":"During bd sync, getting 'prefix mismatch detected: database uses agent-relay- but found issues with prefixes: agent- (5 issues)'. Need to investigate and fix the prefix inconsistency or use --rename-on-import flag.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-23T23:10:12.720183+01:00","updated_at":"2025-12-23T23:10:12.720183+01:00"} {"id":"agent-relay-4vl","title":"PR-9 Review: Add unit tests for AgentSpawner","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-22T21:54:03.195295+01:00","updated_at":"2025-12-22T22:00:57.315234+01:00","closed_at":"2025-12-22T22:00:57.315234+01:00"} {"id":"agent-relay-4xs","title":"Dashboard: Enter to send, Shift+Enter for newline (Slack-style)","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-24T15:25:01.248707+01:00","updated_at":"2025-12-24T15:26:24.619384+01:00","closed_at":"2025-12-24T15:26:24.619384+01:00"} -{"id":"agent-relay-51u","title":"Outgoing messages truncated before delivery","description":"Agent messages are being cut off mid-sentence. Examples: 'Updates for mcl/2z1:' and 'Signing off. Progress report:' - both end abruptly. May be related to capture-pane buffer limits, parser issues, or injection timing. Investigate: (1) capture-pane -S - scrollback limits, (2) parser line joining, (3) message storage truncation.","notes":"PROGRESS: Reproduced truncation in tmux wrapper/relay flow. Likely due to display truncation on injection length and terminal capture limitations. No code changes yet.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-20T21:50:11.411952+01:00","updated_at":"2025-12-22T13:48:45.264136+01:00","closed_at":"2025-12-22T13:48:45.264136+01:00"} -{"id":"agent-relay-52d","title":"Add metrics/observability for daemon","description":"No way to monitor daemon health, message throughput, or agent activity. Add: (1) /metrics endpoint for Prometheus, (2) Message count/rate stats, (3) Connection lifecycle events, (4) Error rate tracking.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:48.378728+01:00","updated_at":"2025-12-25T13:59:43.376287+01:00","closed_at":"2025-12-25T13:59:43.376287+01:00"} +{"id":"agent-relay-51u","title":"Outgoing messages truncated before delivery","description":"Agent messages are being cut off mid-sentence. Examples: 'Updates for mcl/2z1:' and 'Signing off. Progress report:' - both end abruptly. May be related to capture-pane buffer limits, parser issues, or injection timing. Investigate: (1) capture-pane -S - scrollback limits, (2) parser line joining, (3) message storage truncation.","notes":"PROGRESS: Reproduced truncation in tmux wrapper/relay flow. Likely due to display truncation on injection length and terminal capture limitations. No code changes yet.","status":"closed","priority":1,"issue_type":"bug","assignee":"Implementer","created_at":"2025-12-20T21:50:11.411952+01:00","updated_at":"2025-12-22T13:48:45.264136+01:00","closed_at":"2025-12-22T13:48:45.264136+01:00"} +{"id":"agent-relay-52d","title":"Add metrics/observability for daemon","description":"No way to monitor daemon health, message throughput, or agent activity. Add: (1) /metrics endpoint for Prometheus, (2) Message count/rate stats, (3) Connection lifecycle events, (4) Error rate tracking.","status":"closed","priority":2,"issue_type":"feature","assignee":"FEDev","created_at":"2025-12-20T00:18:48.378728+01:00","updated_at":"2025-12-25T13:59:43.376287+01:00","closed_at":"2025-12-25T13:59:43.376287+01:00"} {"id":"agent-relay-52e","title":"Unify bridge view with dashboard UI","description":"Make bridge view feel like the main dashboard with project tabs, same agent/messaging UI, spawn capability at bridge level, and ability to click into individual project dashboards","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-25T13:44:11.491787+01:00","updated_at":"2025-12-25T13:49:36.007645+01:00","closed_at":"2025-12-25T13:49:36.007645+01:00"} {"id":"agent-relay-5af","title":"Hook doesn't integrate with daemon-based messaging","description":"hooks/inbox-check/hook.ts reads from file-based inbox but the daemon uses SQLite. When using daemon mode, the hook won't see messages. Need to: (1) Query daemon storage, (2) Or ensure inbox files are written in daemon mode too.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-20T00:18:35.503078+01:00","updated_at":"2025-12-20T00:18:35.503078+01:00"} -{"id":"agent-relay-5fa","title":"Add exponential backoff for daemon reconnection","description":"Implement graceful reconnection with exponential backoff delays [100, 500, 1000, 2000, 5000ms]. After max attempts, operate offline gracefully. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:28:48.055013+01:00","updated_at":"2025-12-20T21:33:42.229756+01:00","closed_at":"2025-12-20T21:33:42.229756+01:00"} +{"id":"agent-relay-5fa","title":"Add exponential backoff for daemon reconnection","description":"Implement graceful reconnection with exponential backoff delays [100, 500, 1000, 2000, 5000ms]. After max attempts, operate offline gracefully. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","assignee":"LeadDev","created_at":"2025-12-20T21:28:48.055013+01:00","updated_at":"2025-12-20T21:33:42.229756+01:00","closed_at":"2025-12-20T21:33:42.229756+01:00"} {"id":"agent-relay-5g0","title":"Heartbeat timeout could be more configurable","description":"In connection.ts:196, heartbeat timeout is hardcoded as 2x heartbeatMs. This should be independently configurable. Also, heartbeat failures immediately kill the connection - could implement exponential backoff for transient issues.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:03.556614+01:00","updated_at":"2025-12-23T23:03:07.563273+01:00","closed_at":"2025-12-23T23:03:07.563273+01:00"} -{"id":"agent-relay-68a","title":"Ask pattern: request/response messaging (replyTo + deferred)","description":"Implement an RPC-like ask() over agent-relay: sender creates a short-lived deferred handle, sends message with replyTo, receiver responds resolving the deferred. Similar to swarm-mail DurableDeferred + ask/respond.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T21:44:33.930531+01:00","updated_at":"2025-12-20T21:44:33.930531+01:00"} +{"id":"agent-relay-68a","title":"Ask pattern: request/response messaging (replyTo + deferred)","description":"Implement an RPC-like ask() over agent-relay: sender creates a short-lived deferred handle, sends message with replyTo, receiver responds resolving the deferred. Similar to swarm-mail DurableDeferred + ask/respond.","acceptance_criteria":"- ask() returns response or times out\\n- Deferreds are persisted with TTL\\n- Works across process restarts/reconnects","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T21:44:33.930531+01:00","updated_at":"2025-12-20T21:44:33.930531+01:00","labels":["coordination","protocol"]} {"id":"agent-relay-6dl8","title":"Multi-line relay messages truncated in dashboard display","description":"Multi-line messages sent via -\u003erelay: are being cut off after first line when displayed in dashboard. This blocks communication between agents. Need to: (1) Verify daemon sends full message, (2) Fix dashboard rendering to handle newlines properly.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-26T12:28:39.746942+01:00","updated_at":"2025-12-26T12:38:36.844034+01:00","closed_at":"2025-12-26T12:38:36.844034+01:00"} {"id":"agent-relay-6mo","title":"Lead spawn capability not working (@relay:spawn pattern)","description":"The lead command advertises spawn capability via @relay:spawn pattern but it's not fully implemented. Agents started with 'agent-relay lead' should be able to spawn workers, but the parser extension mentioned in the code is not implemented.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-23T12:14:59.492237+01:00","updated_at":"2025-12-23T12:18:14.114911+01:00","closed_at":"2025-12-23T12:18:14.114911+01:00"} -{"id":"agent-relay-6nx","title":"Add activity state tracking for better injection timing","description":"Track active/idle/disconnected state with timestamps. When session goes idle (30s no activity), trigger message injection opportunity. Improves injection timing vs current fixed 1.5s wait. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:28:46.993856+01:00","updated_at":"2025-12-20T21:33:42.223042+01:00","closed_at":"2025-12-20T21:33:42.223042+01:00"} +{"id":"agent-relay-6nx","title":"Add activity state tracking for better injection timing","description":"Track active/idle/disconnected state with timestamps. When session goes idle (30s no activity), trigger message injection opportunity. Improves injection timing vs current fixed 1.5s wait. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","assignee":"LeadDev","created_at":"2025-12-20T21:28:46.993856+01:00","updated_at":"2025-12-20T21:33:42.223042+01:00","closed_at":"2025-12-20T21:33:42.223042+01:00"} {"id":"agent-relay-6ny","title":"Fix message truncation: store full message in SQLite first, include ID in relay","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-19T22:04:36.168862+01:00","updated_at":"2025-12-19T22:08:28.207532+01:00","closed_at":"2025-12-19T22:08:28.207532+01:00"} {"id":"agent-relay-6rz","title":"Message injection timing can cause race conditions","description":"In tmux-wrapper.ts:564-569, injection waits for 'idle' (1.5s since last output) but this is fragile. If agent produces output during injection, messages could interleave. Consider: (1) Input buffer detection, (2) Bracketed paste mode, (3) Agent-specific injection strategies.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-20T00:18:17.76865+01:00","updated_at":"2025-12-25T14:31:59.858855+01:00","closed_at":"2025-12-25T14:31:59.858855+01:00"} -{"id":"agent-relay-7b2","title":"CRITICAL: Agents cycling between offline/online status","description":"Dashboard reports agents cycling between offline/online. Root cause analysis: Heartbeat timeout in connection.ts line 230 - if PONG not received within 10 seconds (2x heartbeat interval), connection is killed. This is too aggressive for AI agents that may be processing. Fix: increase timeout tolerance or make configurable. See connection.ts:223-244.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-23T22:57:53.052261+01:00","updated_at":"2025-12-23T23:02:41.925425+01:00","closed_at":"2025-12-23T23:02:41.925425+01:00"} +{"id":"agent-relay-7b2","title":"CRITICAL: Agents cycling between offline/online status","description":"Dashboard reports agents cycling between offline/online. Root cause analysis: Heartbeat timeout in connection.ts line 230 - if PONG not received within 10 seconds (2x heartbeat interval), connection is killed. This is too aggressive for AI agents that may be processing. Fix: increase timeout tolerance or make configurable. See connection.ts:223-244.","status":"closed","priority":0,"issue_type":"bug","assignee":"Lead","created_at":"2025-12-23T22:57:53.052261+01:00","updated_at":"2025-12-23T23:02:41.925425+01:00","closed_at":"2025-12-23T23:02:41.925425+01:00"} {"id":"agent-relay-7bp","title":"Memory storage adapter has fixed 1000 message limit","description":"In storage/adapter.ts:60-63, MemoryStorageAdapter hard-codes 1000 message limit. This should be configurable and potentially use LRU eviction instead of slice.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:17:46.132735+01:00","updated_at":"2025-12-20T00:17:46.132735+01:00"} {"id":"agent-relay-7fx","title":"PR-9 Review: Fix @relay vs -\u003erelay syntax inconsistency in docs","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-22T22:02:02.782958+01:00","updated_at":"2025-12-22T22:04:02.664058+01:00","closed_at":"2025-12-22T22:04:02.664058+01:00"} {"id":"agent-relay-7mm","title":"Add orphaned tmux session cleanup","description":"When agent-relay wrapper crashes or gets SIGKILL, the tmux session remains. Need: 1) CLI command to gc orphaned sessions, 2) Daemon-side cleanup on client disconnect","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-22T15:52:01.58781+01:00","updated_at":"2025-12-22T15:54:59.840769+01:00","closed_at":"2025-12-22T15:54:59.840769+01:00"} -{"id":"agent-relay-7tk","title":"Dashboard truncates multi-line messages","description":"Messages containing line breaks only show the first line in the dashboard. Need to render newlines properly - either with CSS white-space or by converting newlines to \u003cbr\u003e tags.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-23T23:05:03.86361+01:00","updated_at":"2025-12-23T23:05:59.96346+01:00","closed_at":"2025-12-23T23:05:59.96346+01:00"} -{"id":"agent-relay-7tu","title":"Fix storage portability: better-sqlite3 native binding failures","description":"The read command fails in some Node versions (e.g., Node 25) due to missing better-sqlite3 bindings. Decide path: pin supported Node LTS + ensure rebuild, or replace with a non-native option (e.g., Node sqlite, wasm, or a pure JS adapter).","notes":"Implemented sqlite portability: dynamic better-sqlite3 import + fallback to node:sqlite (env override AGENT_RELAY_SQLITE_DRIVER). Updated sqlite adapter to use positional params for compatibility; added/updated tests; npx vitest run passes (216).","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:44:57.868091+01:00","updated_at":"2025-12-20T21:56:13.384399+01:00","closed_at":"2025-12-20T21:56:13.3844+01:00"} -{"id":"agent-relay-7yo","title":"Update CLAUDE.md with new CLI commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:00:02.661859+01:00","updated_at":"2025-12-19T22:07:27.766701+01:00","closed_at":"2025-12-19T22:07:27.766701+01:00","dependencies":[{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-bd0","type":"blocks","created_at":"2025-12-19T22:00:25.80776+01:00","created_by":"daemon"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-f3q","type":"blocks","created_at":"2025-12-19T22:00:25.843131+01:00","created_by":"daemon"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-85z","type":"blocks","created_at":"2025-12-19T22:00:25.731921+01:00","created_by":"daemon"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-4ft","type":"blocks","created_at":"2025-12-19T22:00:25.772241+01:00","created_by":"daemon"}]} -{"id":"agent-relay-85z","title":"Merge dashboard into start command","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T21:59:51.61716+01:00","updated_at":"2025-12-19T22:06:44.27487+01:00","closed_at":"2025-12-19T22:06:44.27487+01:00"} +{"id":"agent-relay-7tk","title":"Dashboard truncates multi-line messages","description":"Messages containing line breaks only show the first line in the dashboard. Need to render newlines properly - either with CSS white-space or by converting newlines to \u003cbr\u003e tags.","status":"closed","priority":1,"issue_type":"bug","assignee":"FE-Dev","created_at":"2025-12-23T23:05:03.86361+01:00","updated_at":"2025-12-23T23:05:59.96346+01:00","closed_at":"2025-12-23T23:05:59.96346+01:00"} +{"id":"agent-relay-7tu","title":"Fix storage portability: better-sqlite3 native binding failures","description":"The read command fails in some Node versions (e.g., Node 25) due to missing better-sqlite3 bindings. Decide path: pin supported Node LTS + ensure rebuild, or replace with a non-native option (e.g., Node sqlite, wasm, or a pure JS adapter).","design":"Implement driver selection/fallback: try better-sqlite3, fallback to node:sqlite DatabaseSync when bindings missing. Add env override AGENT_RELAY_SQLITE_DRIVER=node|better-sqlite3. Convert queries to positional params so both drivers share code.","acceptance_criteria":"- dist CLI read works on supported Node versions\\n- CI/build docs reflect supported runtime or replacement adapter","notes":"Implemented sqlite portability: dynamic better-sqlite3 import + fallback to node:sqlite (env override AGENT_RELAY_SQLITE_DRIVER). Updated sqlite adapter to use positional params for compatibility; added/updated tests; npx vitest run passes (216).","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:44:57.868091+01:00","updated_at":"2025-12-20T21:56:13.384399+01:00","closed_at":"2025-12-20T21:56:13.3844+01:00","labels":["build","storage"]} +{"id":"agent-relay-7yo","title":"Update CLAUDE.md with new CLI commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:00:02.661859+01:00","updated_at":"2025-12-19T22:07:27.766701+01:00","closed_at":"2025-12-19T22:07:27.766701+01:00","dependencies":[{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-85z","type":"blocks","created_at":"2025-12-19T22:00:25.731921+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-4ft","type":"blocks","created_at":"2025-12-19T22:00:25.772241+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-bd0","type":"blocks","created_at":"2025-12-19T22:00:25.80776+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-f3q","type":"blocks","created_at":"2025-12-19T22:00:25.843131+01:00","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-85z","title":"Merge dashboard into start command","status":"closed","priority":2,"issue_type":"task","assignee":"InterfaceManager","created_at":"2025-12-19T21:59:51.61716+01:00","updated_at":"2025-12-19T22:06:44.27487+01:00","closed_at":"2025-12-19T22:06:44.27487+01:00"} {"id":"agent-relay-8ap","title":"CRITICAL: send command triggers infinite reconnection loop","description":"When sending relay messages to Gem agent, the send command triggers an infinite reconnection loop. Client repeatedly connects with RESUME_TOO_OLD errors, never actually delivers the message. Each connection gets a new session ID and immediately reconnects (~90-110ms apart). This is a critical client/daemon issue, not just Gemini-specific.","notes":"CLI send command removed (keeping surface area small). Root cause still needs investigation - client reconnection loop with RESUME_TOO_OLD errors.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-22T12:18:13.272843+01:00","updated_at":"2025-12-22T14:37:19.435751+01:00","closed_at":"2025-12-22T14:37:19.435751+01:00"} -{"id":"agent-relay-8fc","title":"Agent auto-complete menu is transparent/invisible","description":"The agent auto-complete dropdown menu in the dashboard appears to be transparent or has visibility issues, making it unusable. Need to debug CSS/z-index/display issues in bridge.html agent selector component.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-24T15:04:53.390674+01:00","updated_at":"2025-12-24T15:30:23.72431+01:00","closed_at":"2025-12-24T15:30:23.72431+01:00"} +{"id":"agent-relay-8fc","title":"Agent auto-complete menu is transparent/invisible","description":"The agent auto-complete dropdown menu in the dashboard appears to be transparent or has visibility issues, making it unusable. Need to debug CSS/z-index/display issues in bridge.html agent selector component.","status":"closed","priority":2,"issue_type":"bug","assignee":"FE-Dev","created_at":"2025-12-24T15:04:53.390674+01:00","updated_at":"2025-12-24T15:30:23.72431+01:00","closed_at":"2025-12-24T15:30:23.72431+01:00"} {"id":"agent-relay-8ff","title":"Add agent list command to CLI","description":"CLI has up/down/status/read but no way to list connected agents or see message history from command line. Add: (1) agent-relay agents - list connected agents, (2) agent-relay history - show recent messages, (3) agent-relay send \u003cagent\u003e \u003cmsg\u003e - send from CLI.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:34.400432+01:00","updated_at":"2025-12-22T15:35:44.695605+01:00","closed_at":"2025-12-22T15:35:44.695605+01:00"} {"id":"agent-relay-8nx","title":"SIGINT/SIGTERM handlers don't await cleanup","description":"In cli/index.ts:74-77 and daemon setup, SIGINT handlers call stop() but process.exit(0) runs immediately. The stop() is async but not awaited, potentially leaving socket files or incomplete shutdown.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-20T00:18:19.189514+01:00","updated_at":"2025-12-20T00:18:19.189514+01:00"} -{"id":"agent-relay-8z1","title":"Add CLI tests for new command structure","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:00:04.561793+01:00","updated_at":"2025-12-22T17:10:28.100961+01:00","closed_at":"2025-12-22T17:10:28.100961+01:00","dependencies":[{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-85z","type":"blocks","created_at":"2025-12-19T22:00:27.632396+01:00","created_by":"daemon"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-4ft","type":"blocks","created_at":"2025-12-19T22:00:27.671868+01:00","created_by":"daemon"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-bd0","type":"blocks","created_at":"2025-12-19T22:00:27.713889+01:00","created_by":"daemon"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-f3q","type":"blocks","created_at":"2025-12-19T22:00:27.752052+01:00","created_by":"daemon"}]} -{"id":"agent-relay-90h","title":"Show 'needs attention' indicator when agent is waiting for permissions","description":"Frontend complete: needsAttention field, pulsing indicator, badge. Backend piece still needed: detect when agents are waiting for permissions and set the flag.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-23T22:55:26.87753+01:00","updated_at":"2025-12-24T11:48:32.679452+01:00","closed_at":"2025-12-24T11:48:32.679452+01:00"} +{"id":"agent-relay-8z1","title":"Add CLI tests for new command structure","status":"closed","priority":2,"issue_type":"task","assignee":"Lead","created_at":"2025-12-19T22:00:04.561793+01:00","updated_at":"2025-12-22T17:10:28.100961+01:00","closed_at":"2025-12-22T17:10:28.100961+01:00","dependencies":[{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-85z","type":"blocks","created_at":"2025-12-19T22:00:27.632396+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-4ft","type":"blocks","created_at":"2025-12-19T22:00:27.671868+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-bd0","type":"blocks","created_at":"2025-12-19T22:00:27.713889+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-f3q","type":"blocks","created_at":"2025-12-19T22:00:27.752052+01:00","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-90h","title":"Show 'needs attention' indicator when agent is waiting for permissions","description":"Frontend complete: needsAttention field, pulsing indicator, badge. Backend piece still needed: detect when agents are waiting for permissions and set the flag.","status":"closed","priority":1,"issue_type":"feature","assignee":"FE-Dev","created_at":"2025-12-23T22:55:26.87753+01:00","updated_at":"2025-12-24T11:48:32.679452+01:00","closed_at":"2025-12-24T11:48:32.679452+01:00"} {"id":"agent-relay-9igw","title":"Agents not closing multi-line relay messages with \u003e\u003e\u003e","description":"Agent Relay requires multi-line messages to be wrapped in \u003c\u003c\u003c \u003e\u003e\u003e format with \u003e\u003e\u003e on its own line. Agents are sending multi-line messages without proper closure. This breaks message parsing and relay communication quality.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-28T19:29:43.950026-05:00","updated_at":"2025-12-29T12:00:58.120195-05:00","closed_at":"2025-12-29T15:03:53.917852946Z"} -{"id":"agent-relay-9kc","title":"Add skip-permissions flag for spawned Claude agents","description":"When spawning Claude agents via agent-relay, automatically use --dangerously-skip-permissions to avoid permission dialog delays. Update spawn command in CLI to pass this flag to spawned sessions.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-25T14:09:00.07082+01:00","updated_at":"2025-12-25T14:15:45.925839+01:00","closed_at":"2025-12-25T14:15:45.925839+01:00"} +{"id":"agent-relay-9kc","title":"Add skip-permissions flag for spawned Claude agents","description":"When spawning Claude agents via agent-relay, automatically use --dangerously-skip-permissions to avoid permission dialog delays. Update spawn command in CLI to pass this flag to spawned sessions.","status":"closed","priority":1,"issue_type":"task","assignee":"FEDev","created_at":"2025-12-25T14:09:00.07082+01:00","updated_at":"2025-12-25T14:15:45.925839+01:00","closed_at":"2025-12-25T14:15:45.925839+01:00"} {"id":"agent-relay-9kw","title":"Add multi-machine agent support","description":"Enable agents to run across multiple machines with centralized coordination. AI Maestro scales to 100+ agents across machines. Extend bridge mode to support remote daemon connections over TCP/WebSocket. Critical for scaling beyond single machine.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:04:35.521241+01:00","updated_at":"2025-12-23T17:04:35.521241+01:00"} {"id":"agent-relay-9qa","title":"Dashboard websocket 'Invalid frame header' error on localhost:3888","description":"Websocket connection to ws://localhost:3888/ws fails with 'Invalid frame header' error from browser. Blocking dashboard functionality.","notes":"WS handshake fixed: restored default WebSocket compression (removed perMessageDeflate:false), removed HTTP polling fallback. FE-Dev confirmed 101 upgrade on 127.0.0.1:3888 after rebuild/refresh.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-23T16:42:29.342087+01:00","updated_at":"2025-12-23T17:04:04.246054+01:00","closed_at":"2025-12-23T17:04:04.246054+01:00"} {"id":"agent-relay-9uq","title":"Project chat targeting in bridge","description":"Enable sending messages to agents in a selected project from the bridge interface","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-24T11:47:33.46287+01:00","updated_at":"2025-12-24T11:56:54.182321+01:00","closed_at":"2025-12-24T11:56:54.182321+01:00"} -{"id":"agent-relay-9uy","title":"Durable inbox state (unread/read/acked per recipient)","description":"Implement per-recipient inbox state (unread/read/acked) similar to swarm-mail’s message_recipients projection. Persist and query unread/urgent; expose to dashboard + CLI read/lookup.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:43:46.201965+01:00","updated_at":"2025-12-22T14:25:41.862138+01:00","closed_at":"2025-12-22T14:25:41.862138+01:00"} +{"id":"agent-relay-9uy","title":"Durable inbox state (unread/read/acked per recipient)","description":"Implement per-recipient inbox state (unread/read/acked) similar to swarm-mail’s message_recipients projection. Persist and query unread/urgent; expose to dashboard + CLI read/lookup.","acceptance_criteria":"- Can mark message read and acked per recipient\\n- Dashboard/API can query unreadOnly and urgentOnly\\n- State survives daemon restart","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:43:46.201965+01:00","updated_at":"2025-12-22T14:25:41.862138+01:00","closed_at":"2025-12-22T14:25:41.862138+01:00","labels":["durability","storage"]} {"id":"agent-relay-9w0","title":"Auto-detect agent role from .claude/agents or .openagents on init","description":"When an agent connects with a name (e.g., 'Lead'), check .claude/agents/ and .openagents/ directories for a matching agent definition file. If found, include in the initialization message that the agent should assume that role. This enables automatic role assignment without manual configuration.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-23T16:26:27.510393+01:00","updated_at":"2026-01-02T20:34:28.248936+01:00","closed_at":"2026-01-02T20:31:57.135349+01:00"} {"id":"agent-relay-a4k","title":"Add safety mechanisms for dangerous commands","description":"Implement safety layer for dangerous operations (like ACFS's two-person rule). Require confirmation from second agent or human for destructive commands (rm -rf, git push -f, DROP TABLE). Configurable blocklist and approval workflow.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:05:21.032206+01:00","updated_at":"2025-12-23T17:05:21.032206+01:00"} {"id":"agent-relay-ahe","title":"Session resume not implemented - RESUME_TOO_OLD always sent","description":"In connection.ts:140-143, session resume tokens are received but not persisted or validated. The server always responds with RESUME_TOO_OLD. This breaks the resume capability advertised in the protocol. Need to implement token persistence and session state recovery.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-20T00:17:30.605649+01:00","updated_at":"2025-12-22T15:31:05.795905+01:00","closed_at":"2025-12-22T15:31:05.795905+01:00"} -{"id":"agent-relay-aq2","title":"Implement agent-to-agent spawning via relay","description":"Allow agents to spawn new agents through relay messaging. Enable -\u003erelay:spawn pattern so agents can dynamically create specialized sub-agents. This unblocks multi-agent workflows and team hierarchies.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T14:14:02.110506+01:00","updated_at":"2025-12-25T14:32:10.400627+01:00","closed_at":"2025-12-25T14:32:10.400627+01:00"} -{"id":"agent-relay-aqy","title":"Display cross-project messages with project badge in dashboard","description":"When messages use @project:agent format, display them in the dashboard with a project badge/indicator. Parse project:agent from the 'to' field and render as [ProjectBadge] @AgentName. Depends on agent-relay-cross-project-parser completing first.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-24T10:29:37.528325+01:00","updated_at":"2025-12-24T11:46:39.341486+01:00","closed_at":"2025-12-24T11:46:39.341486+01:00","dependencies":[{"issue_id":"agent-relay-aqy","depends_on_id":"agent-relay-ytl","type":"blocks","created_at":"2025-12-24T10:29:48.712573+01:00","created_by":"khaliqgant"}]} +{"id":"agent-relay-aq2","title":"Implement agent-to-agent spawning via relay","description":"Allow agents to spawn new agents through relay messaging. Enable -\u003erelay:spawn pattern so agents can dynamically create specialized sub-agents. This unblocks multi-agent workflows and team hierarchies.","status":"closed","priority":1,"issue_type":"feature","assignee":"BESpecialist","created_at":"2025-12-25T14:14:02.110506+01:00","updated_at":"2025-12-25T14:32:10.400627+01:00","closed_at":"2025-12-25T14:32:10.400627+01:00"} +{"id":"agent-relay-aqy","title":"Display cross-project messages with project badge in dashboard","description":"When messages use @project:agent format, display them in the dashboard with a project badge/indicator. Parse project:agent from the 'to' field and render as [ProjectBadge] @AgentName. Depends on agent-relay-cross-project-parser completing first.","status":"closed","priority":2,"issue_type":"feature","assignee":"FE-Dev","created_at":"2025-12-24T10:29:37.528325+01:00","updated_at":"2025-12-24T11:46:39.341486+01:00","closed_at":"2025-12-24T11:46:39.341486+01:00","dependencies":[{"issue_id":"agent-relay-aqy","depends_on_id":"agent-relay-ytl","type":"blocks","created_at":"2025-12-24T10:29:48.712573+01:00","created_by":"khaliqgant","metadata":"{}"}]} {"id":"agent-relay-b11","title":"Cannot scroll in agent tmux window","description":"User unable to scroll in the tmux window when running agent-relay wrapped agents. Need to fix mouse scroll or keyboard scroll behavior.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-23T15:01:55.791186+01:00","updated_at":"2025-12-23T15:04:51.896303+01:00","closed_at":"2025-12-23T15:04:51.896303+01:00"} -{"id":"agent-relay-b919","title":"Fix Cmd+N spawn shortcut conflict","description":"Cmd+N shortcut for spawning agents has a conflict that needs to be resolved","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T22:08:23.539814-05:00","updated_at":"2025-12-28T22:09:20.683421-05:00","closed_at":"2025-12-28T22:09:20.683421-05:00"} +{"id":"agent-relay-b919","title":"Fix Cmd+N spawn shortcut conflict","description":"Cmd+N shortcut for spawning agents has a conflict that needs to be resolved","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T22:08:23.539814-05:00","updated_at":"2025-12-28T22:09:20.683421-05:00","closed_at":"2025-12-28T22:09:20.683421-05:00","close_reason":"Fixed by Frontend - changed shortcut to Cmd+Shift+S with keyboard handler"} {"id":"agent-relay-bai","title":"Fix bridge reconnection loop caused by agent name conflict","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-25T13:26:46.025963+01:00","updated_at":"2025-12-25T13:27:04.323001+01:00","closed_at":"2025-12-25T13:27:04.323001+01:00"} {"id":"agent-relay-bd0","title":"Consolidate team-* commands under team subcommand","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T21:59:54.102815+01:00","updated_at":"2025-12-19T22:06:16.220013+01:00","closed_at":"2025-12-19T22:06:16.220013+01:00"} -{"id":"agent-relay-bei","title":"Gemini (CleverBeacon) bash syntax errors - unexpected EOF","description":"Gemini agent gets bash errors: 'unexpected EOF while looking for matching quote' and 'syntax error: unexpected end of file' when running shell commands. Possibly related to quote escaping in the relay wrapper.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-19T23:40:36.464079+01:00","updated_at":"2025-12-19T23:45:46.05609+01:00","closed_at":"2025-12-19T23:45:46.05609+01:00"} +{"id":"agent-relay-bei","title":"Gemini (CleverBeacon) bash syntax errors - unexpected EOF","description":"Gemini agent gets bash errors: 'unexpected EOF while looking for matching quote' and 'syntax error: unexpected end of file' when running shell commands. Possibly related to quote escaping in the relay wrapper.","status":"closed","priority":2,"issue_type":"bug","assignee":"GraniteElk","created_at":"2025-12-19T23:40:36.464079+01:00","updated_at":"2025-12-19T23:45:46.05609+01:00","closed_at":"2025-12-19T23:45:46.05609+01:00"} {"id":"agent-relay-blj","title":"Add metrics and observability endpoint","description":"Add metrics collection and observability for production monitoring. Include: message throughput, agent health, latency percentiles, error rates, queue depths. Expose via HTTP endpoint (Prometheus format) and/or dashboard integration. Currently flying blind in production.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:03:41.964359+01:00","updated_at":"2025-12-25T13:33:56.132695+01:00","closed_at":"2025-12-25T13:33:56.132695+01:00"} {"id":"agent-relay-bx0","title":"Filter stale/internal agents from agents list","description":"agent-relay agents shows __status__ and cli agents which are temporary internal connections. Should filter these out by default, with --all flag to show everything.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-22T15:44:51.944084+01:00","updated_at":"2025-12-22T15:45:44.262049+01:00","closed_at":"2025-12-22T15:45:44.262049+01:00"} {"id":"agent-relay-cloud1","title":"[Cloud] Implement durable persistence via PtyWrapper events","description":"## Summary\n\nPtyWrapper now emits 'summary' and 'session-end' events when agents output [[SUMMARY]] and [[SESSION_END]] blocks. Cloud services should implement persistence handlers for these events using durable storage (PostgreSQL, Redis) instead of SQLite.\n\n## Current State\n\n- TmuxWrapper has SQLite storage integration for summaries and session state\n- PtyWrapper now emits events but has no storage (by design - cloud handles it)\n- Cloud deployments use PtyWrapper exclusively (no tmux)\n\n## Event Interface\n\n```typescript\ninterface SummaryEvent {\n agentName: string;\n summary: ParsedSummary; // currentTask, completedTasks, decisions, context, files\n}\n\ninterface SessionEndEvent {\n agentName: string;\n marker: SessionEndMarker; // summary, completedTasks\n}\n\n// PtyWrapperEvents emits:\n// - 'summary': (event: SummaryEvent) =\u003e void\n// - 'session-end': (event: SessionEndEvent) =\u003e void\n```\n\n## Implementation Requirements\n\n### Cloud Persistence Service\n\n```typescript\nclass CloudPersistenceService {\n constructor(\n private db: PostgresClient | RedisClient,\n private sessionId: string\n ) {}\n\n bindToPtyWrapper(wrapper: PtyWrapper): void {\n wrapper.on('summary', (event) =\u003e this.handleSummary(event));\n wrapper.on('session-end', (event) =\u003e this.handleSessionEnd(event));\n }\n\n private async handleSummary(event: SummaryEvent): Promise\u003cvoid\u003e {\n await this.db.query(`\n INSERT INTO agent_summaries (session_id, agent_name, summary, created_at)\n VALUES ($1, $2, $3, NOW())\n ON CONFLICT (session_id, agent_name) \n DO UPDATE SET summary = $3, updated_at = NOW()\n `, [this.sessionId, event.agentName, JSON.stringify(event.summary)]);\n }\n\n private async handleSessionEnd(event: SessionEndEvent): Promise\u003cvoid\u003e {\n await this.db.query(`\n UPDATE sessions SET ended_at = NOW(), end_marker = $2 WHERE id = $1\n `, [this.sessionId, JSON.stringify(event.marker)]);\n \n // Clean up resources\n await this.cleanupSession(this.sessionId);\n }\n}\n```\n\n### PostgreSQL Schema\n\n```sql\nCREATE TABLE agent_summaries (\n id SERIAL PRIMARY KEY,\n session_id UUID NOT NULL REFERENCES sessions(id),\n agent_name VARCHAR(255) NOT NULL,\n summary JSONB NOT NULL,\n created_at TIMESTAMPTZ DEFAULT NOW(),\n updated_at TIMESTAMPTZ DEFAULT NOW(),\n UNIQUE(session_id, agent_name)\n);\n\nCREATE TABLE sessions (\n id UUID PRIMARY KEY,\n user_id UUID NOT NULL,\n started_at TIMESTAMPTZ DEFAULT NOW(),\n ended_at TIMESTAMPTZ,\n end_marker JSONB\n);\n\nCREATE INDEX idx_summaries_session ON agent_summaries(session_id);\n```\n\n### Redis Alternative\n\nFor high-frequency summary updates:\n\n```typescript\n// Store latest summary per agent\nawait redis.hset(`session:${sessionId}:summaries`, agentName, JSON.stringify(summary));\n\n// TTL for automatic cleanup\nawait redis.expire(`session:${sessionId}:summaries`, 86400); // 24h\n```\n\n## Files to Create\n\n1. `src/cloud/persistence-service.ts` - Event handler for cloud persistence\n2. `src/cloud/schema.sql` - PostgreSQL schema for cloud deployment\n3. `src/cloud/index.ts` - Cloud service initialization\n\n## Testing\n\n1. Unit test: SummaryEvent emitted when [[SUMMARY]] detected\n2. Unit test: SessionEndEvent emitted when [[SESSION_END]] detected \n3. Unit test: Deduplication works (same summary not emitted twice)\n4. Integration: PostgreSQL persistence handler\n5. Integration: Full session lifecycle with events\n\n## Success Criteria\n\n- Cloud services can persist summaries to PostgreSQL/Redis\n- No SQLite dependency in cloud deployments\n- Event-based architecture allows flexible storage backends\n- Session cleanup triggered by session-end events","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-02T16:00:00Z","updated_at":"2026-01-02T16:00:00Z","closed_at":"2026-01-02T12:00:00Z"} {"id":"agent-relay-coh","title":"Add tests for heartbeat timeout behavior","description":"The heartbeat timeout fix (10s-\u003e30s) was made but no new tests were added. Add unit tests for: 1) configurable heartbeatTimeoutMultiplier, 2) connection survives slow pong responses, 3) connection dies after timeout exceeded.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-23T23:10:18.599194+01:00","updated_at":"2025-12-25T13:31:23.910414+01:00","closed_at":"2025-12-25T13:31:23.910414+01:00"} {"id":"agent-relay-cuer","title":"Single agent flickering between active/inactive on dashboard","description":"When a single agent is spawned, the dashboard flickers rapidly between showing the agent as active and inactive. This blocks reliable agent monitoring. Need to investigate: (1) WebSocket connection stability, (2) Status update frequency, (3) State sync race conditions.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-26T16:53:20.385225+01:00","updated_at":"2025-12-26T17:20:58.613208+01:00","closed_at":"2025-12-26T17:20:58.613208+01:00"} {"id":"agent-relay-cw8","title":"Add procedural memory for learned patterns","description":"Implement procedural memory system (like ACFS's cass-memory) to store learned patterns and successful approaches. Agents can query 'how did we solve X before?' Enable cross-session learning and pattern reuse.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:05:14.667585+01:00","updated_at":"2025-12-23T17:05:14.667585+01:00"} -{"id":"agent-relay-d07","title":"Message metadata: subject/thread/importance/replyTo","description":"Add first-class metadata fields to messages (subject, threadId, importance, ackRequired, replyTo/correlationId). Carry them via SendPayload.data and/or [[RELAY]] blocks; persist in storage; update injection formatting + dashboard filters.","notes":"SecondLead: Implemented thread support end-to-end. SendMeta added to client.sendMessage(). tmux-wrapper converts ParsedMessageMetadata to SendMeta. Injection shows [thread:xxx] hint. Dashboard displays thread badge. REMAINING: importance field requires protocol update - DeliverEnvelope needs payload_meta to carry importance through to receivers.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T21:44:18.094598+01:00","updated_at":"2025-12-22T17:15:37.261368+01:00","closed_at":"2025-12-22T17:15:37.261368+01:00"} +{"id":"agent-relay-d07","title":"Message metadata: subject/thread/importance/replyTo","description":"Add first-class metadata fields to messages (subject, threadId, importance, ackRequired, replyTo/correlationId). Carry them via SendPayload.data and/or [[RELAY]] blocks; persist in storage; update injection formatting + dashboard filters.","acceptance_criteria":"- Can send/receive messages with threadId + importance\\n- Dashboard can filter/group by threadId\\n- Injection shows importance/thread hint without breaking TUIs","notes":"SecondLead: Implemented thread support end-to-end. SendMeta added to client.sendMessage(). tmux-wrapper converts ParsedMessageMetadata to SendMeta. Injection shows [thread:xxx] hint. Dashboard displays thread badge. REMAINING: importance field requires protocol update - DeliverEnvelope needs payload_meta to carry importance through to receivers.","status":"closed","priority":2,"issue_type":"task","assignee":"SecondLead","created_at":"2025-12-20T21:44:18.094598+01:00","updated_at":"2025-12-22T17:15:37.261368+01:00","closed_at":"2025-12-22T17:15:37.261368+01:00","labels":["protocol","ux"]} {"id":"agent-relay-d2f","title":"Add optional Git audit trail","description":"Commit messages to .agent-relay/messages/ directory. Recoverable history, compliance-friendly.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.689795+01:00","updated_at":"2025-12-20T21:36:19.689795+01:00"} {"id":"agent-relay-d5vn","title":"Add ability to kill spawned agents from dashboard","description":"Currently there is no way to kill/terminate a spawned agent from the dashboard UI. Need to add a kill/terminate button or action for spawned agents.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T22:11:48.358972-05:00","updated_at":"2025-12-29T15:31:48.126836-05:00","closed_at":"2025-12-29T15:26:08.567794-05:00"} {"id":"agent-relay-dd8","title":"Add file reservation system (from mcp_agent_mail)","description":"Advisory locks with TTL, @relay:lock pattern, pre-commit hook integration. Critical for multi-agent file editing.","notes":"No progress today; added as future work from swarm-mail comparison: implement reservations/locks with TTL + conflict detection. See swarm-mail reservations + locks patterns.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.573504+01:00","updated_at":"2025-12-20T22:00:37.608017+01:00"} {"id":"agent-relay-dkx","title":"Add metrics dashboard for system health monitoring","description":"Track agent metrics (throughput, uptime, online status), session lifecycle (active/closed/error rates), and per-agent message counts. Provide Prometheus-format export for monitoring integration. Includes metrics.ts utilities and metrics.html dashboard page.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-25T14:07:28.416696+01:00","updated_at":"2025-12-26T11:41:20.321264+01:00","closed_at":"2025-12-26T11:41:20.321264+01:00"} {"id":"agent-relay-dyr","title":"No authentication between agents","description":"Any process can connect to the daemon socket and impersonate any agent name. Consider: (1) Per-agent tokens/secrets, (2) Socket permission checks, (3) Optional TLS for non-localhost deployments.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:16.215889+01:00","updated_at":"2025-12-20T00:18:16.215889+01:00"} {"id":"agent-relay-eek","title":"Change relay prefix from @relay to \u003e\u003erelay","description":"Unify the relay protocol prefix to \u003e\u003erelay (instead of @relay) so it works for all agents including Gemini. Update parser, documentation, and any hardcoded references.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T12:20:35.35848+01:00","updated_at":"2025-12-22T12:30:21.696239+01:00","closed_at":"2025-12-22T12:30:21.696239+01:00"} -{"id":"agent-relay-ekk9","title":"Add @-mention autocomplete to v2 message composer","description":"V1 dashboard has @-mention autocomplete in the message composer. When typing @A, it shows a dropdown with matching agents (@Alice, @Admin, etc.). V2's MessageComposer lacks this feature. Need to port the autocomplete logic from v1's components.ts (showMentionAutocomplete, completeMention functions) to v2.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-27T05:36:28.283162+01:00","updated_at":"2025-12-27T05:44:05.597544+01:00","closed_at":"2025-12-27T05:44:05.597544+01:00"} +{"id":"agent-relay-ekk9","title":"Add @-mention autocomplete to v2 message composer","description":"V1 dashboard has @-mention autocomplete in the message composer. When typing @A, it shows a dropdown with matching agents (@Alice, @Admin, etc.). V2's MessageComposer lacks this feature. Need to port the autocomplete logic from v1's components.ts (showMentionAutocomplete, completeMention functions) to v2.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-27T05:36:28.283162+01:00","updated_at":"2025-12-27T05:44:05.597544+01:00","closed_at":"2025-12-27T05:44:05.597544+01:00","close_reason":"Added missing mention-autocomplete CSS styles to globals.css"} {"id":"agent-relay-ex7","title":"Competitive analysis: Agent-to-agent messaging solutions","description":"Research and compare 17 multi-agent orchestration tools against agent-relay. Analyze: agent communication patterns, pros/cons, server/mobile support, memory architecture.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-23T16:34:31.174109+01:00","updated_at":"2025-12-23T16:55:19.264056+01:00","closed_at":"2025-12-23T16:55:19.264056+01:00"} -{"id":"agent-relay-ey9","title":"Dashboard message sending not working","description":"User reports sending messages from dashboard doesn't work. Need to investigate.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-22T15:54:48.975146+01:00","updated_at":"2025-12-22T17:07:48.705786+01:00","closed_at":"2025-12-22T17:07:48.705786+01:00"} +{"id":"agent-relay-ey9","title":"Dashboard message sending not working","description":"User reports sending messages from dashboard doesn't work. Need to investigate.","status":"closed","priority":2,"issue_type":"bug","assignee":"SecondLead","created_at":"2025-12-22T15:54:48.975146+01:00","updated_at":"2025-12-22T17:07:48.705786+01:00","closed_at":"2025-12-22T17:07:48.705786+01:00"} {"id":"agent-relay-f3q","title":"Make msg-read a subcommand of send or message","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T21:59:55.123772+01:00","updated_at":"2025-12-19T22:04:51.112763+01:00","closed_at":"2025-12-19T22:04:51.112763+01:00"} {"id":"agent-relay-f9k","title":"Fix WebSocket Invalid frame header error","description":"Dashboard WebSocket throws Invalid frame header with 415KB payload. Missing wss.on('connection') handler causes race condition.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-23T17:17:26.412812+01:00","updated_at":"2025-12-23T17:18:30.4994+01:00","closed_at":"2025-12-23T17:18:30.4994+01:00"} -{"id":"agent-relay-fb3","title":"Add configurable relay prefix for Gemini compatibility","description":"Gemini CLI uses @ for file references, conflicting with @relay:. Add --prefix flag to CLI and relayPrefix config option. Auto-detect CLI type and use appropriate default (\u003e\u003e for Gemini, @relay: for Claude/Codex). Update parser to use dynamic prefix. See docs/TMUX_IMPROVEMENTS.md for full implementation plan.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:28:52.685002+01:00","updated_at":"2025-12-20T21:40:13.066766+01:00","closed_at":"2025-12-20T21:40:13.066766+01:00"} -{"id":"agent-relay-fuob","title":"Fix spawner tests for resolved tmux path","description":"Update src/bridge/spawner.test.ts mocks to expect resolved tmux binary path (e.g., \"/opt/homebrew/bin/tmux\") instead of just \"tmux\". 9 tests failing due to exact command string assertions.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T19:28:46.381693-05:00","updated_at":"2025-12-28T19:32:17.034494-05:00","closed_at":"2025-12-28T19:32:17.034494-05:00"} +{"id":"agent-relay-fb3","title":"Add configurable relay prefix for Gemini compatibility","description":"Gemini CLI uses @ for file references, conflicting with @relay:. Add --prefix flag to CLI and relayPrefix config option. Auto-detect CLI type and use appropriate default (\u003e\u003e for Gemini, @relay: for Claude/Codex). Update parser to use dynamic prefix. See docs/TMUX_IMPROVEMENTS.md for full implementation plan.","status":"closed","priority":2,"issue_type":"feature","assignee":"Coordinator","created_at":"2025-12-20T21:28:52.685002+01:00","updated_at":"2025-12-20T21:40:13.066766+01:00","closed_at":"2025-12-20T21:40:13.066766+01:00"} +{"id":"agent-relay-fuob","title":"Fix spawner tests for resolved tmux path","description":"Update src/bridge/spawner.test.ts mocks to expect resolved tmux binary path (e.g., \"/opt/homebrew/bin/tmux\") instead of just \"tmux\". 9 tests failing due to exact command string assertions.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T19:28:46.381693-05:00","updated_at":"2025-12-28T19:32:17.034494-05:00","closed_at":"2025-12-28T19:32:17.034494-05:00","close_reason":"Tests updated to mock tmux-resolver - all 514 tests pass"} {"id":"agent-relay-ghy","title":"Team config: auto-spawn agents or auto-assign names from teams.json","description":"When a project has a teams.json config file, agent-relay up should either:\n\n1. **Auto-spawn option**: Automatically kick off terminal sessions for each agent defined in teams.json\n2. **Auto-assign option**: When users manually start sessions with `agent-relay -n \u003cname\u003e claude`, validate the name against teams.json and auto-assign roles/permissions\n\n## teams.json format\n```json\n{\n \"team\": \"my-project\",\n \"agents\": [\n {\"name\": \"Coordinator\", \"cli\": \"claude\", \"role\": \"coordinator\"},\n {\"name\": \"LeadDev\", \"cli\": \"claude\", \"role\": \"developer\"},\n {\"name\": \"Reviewer\", \"cli\": \"claude\", \"role\": \"reviewer\"}\n ],\n \"autoSpawn\": false\n}\n```\n\n## Behavior\n- If `autoSpawn: true`, `agent-relay up` spawns tmux sessions for each agent\n- If `autoSpawn: false`, validate names against config when agents connect\n- Store teams.json in project root or .agent-relay/teams.json\n\n## Commands\n- `agent-relay up --spawn` - force spawn all agents\n- `agent-relay up --no-spawn` - just start daemon, manual agent starts","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-19T23:13:02.482971+01:00","updated_at":"2025-12-22T22:11:38.639588+01:00"} {"id":"agent-relay-go9","title":"PostgreSQL storage adapter not implemented","description":"In storage/adapter.ts:152-162, PostgreSQL is listed as a storage option but throws 'not yet implemented'. For production multi-node deployments, SQLite won't scale. Implement PostgreSQL adapter for distributed storage.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:17:45.065487+01:00","updated_at":"2025-12-20T00:17:45.065487+01:00"} {"id":"agent-relay-gst1","title":"[Dashboard] Add stuck agent detection","description":"Detect when an agent received a message but hasn't responded within a configurable threshold (default 5 min). Show 'stuck' indicator in dashboard UI. This is a communication-layer concern - helps surface when agents are blocked or crashed mid-response. Implementation: track lastMessageReceived and lastOutputTime per agent, compare in dashboard polling.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-02T12:00:00Z","updated_at":"2026-01-02T22:05:08.041335+01:00","closed_at":"2026-01-02T22:01:56.415067+01:00"} {"id":"agent-relay-gst2","title":"[Dashboard] Show message delivery/ACK status","description":"Surface ACK status in dashboard message list. Currently ACKs exist in protocol but aren't visible to users. Show: pending (sent, no ACK), delivered (ACK received), failed (NACK or timeout). Helps debug 'did my message arrive?' issues. Implementation: track ACK state in router, expose via dashboard WebSocket.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-02T12:00:00Z","updated_at":"2026-01-03T12:41:22.401995+01:00","closed_at":"2026-01-03T12:34:06.701959+01:00"} {"id":"agent-relay-gst3","title":"[Research] Evaluate deeper Beads integration","description":"Research task: Evaluate how Agent Relay could integrate more deeply with Beads for task management. Questions to answer: (1) Should Relay auto-update Beads status when agents send DONE messages? (2) Can Relay read from Beads to show task context in dashboard? (3) What's the right boundary - Beads for work state, Relay for communication? Document findings in docs/BEADS_INTEGRATION.md.","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-02T12:00:00Z","updated_at":"2026-01-02T12:00:00Z"} -{"id":"agent-relay-hgd","title":"Reliable delivery: resume/replay via cursor/checkpoint","description":"Make ACK/RESUME real: persist a per-agent delivery cursor (last delivered/acked seq) and on reconnect replay missed messages from storage. Use existing delivery.seq as stream position, similar to swarm-mail DurableCursor checkpointing.","notes":"Persistent resume tokens with stored cursors; replay unacked messages on reconnect; ack tracking + dedup; tests passing.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:44:02.016428+01:00","updated_at":"2025-12-29T15:31:48.127373-05:00","closed_at":"2025-12-29T15:26:08.592051-05:00"} +{"id":"agent-relay-hgd","title":"Reliable delivery: resume/replay via cursor/checkpoint","description":"Make ACK/RESUME real: persist a per-agent delivery cursor (last delivered/acked seq) and on reconnect replay missed messages from storage. Use existing delivery.seq as stream position, similar to swarm-mail DurableCursor checkpointing.","acceptance_criteria":"- Client can reconnect and receive missed messages\\n- Duplicate suppression is deterministic (id/seq based)\\n- Cursor persists across daemon restarts","notes":"Persistent resume tokens with stored cursors; replay unacked messages on reconnect; ack tracking + dedup; tests passing.","status":"closed","priority":1,"issue_type":"task","assignee":"BE-Dev","created_at":"2025-12-20T21:44:02.016428+01:00","updated_at":"2025-12-29T15:31:48.127373-05:00","closed_at":"2025-12-29T15:26:08.592051-05:00","labels":["durability","protocol"]} {"id":"agent-relay-hgn","title":"Agent team hierarchies with specialized sub-agents","description":"Each main agent should be able to spawn and coordinate a team of specialized sub-agents:\n\n- **Lead** → Operations Consultant (monitors behavior, provides retrospective feedback on coordination)\n- **Implementer** → Code Reviewer (reviews code before commits, suggests improvements)\n- **Designer** → UX Reviewer (validates design decisions)\n- **Architect** → Security Auditor (checks for vulnerabilities)\n\nImplementation ideas:\n1. Define agent profiles with 'team' configurations in .claude/agents/\n2. When an agent starts, it can auto-spawn its team members\n3. Sub-agents monitor the parent agent's work and provide feedback\n4. At session end, sub-agents provide retrospective summaries\n5. Use -\u003erelay:spawn to create sub-agents with specific roles\n\nThis enables each agent to have a dedicated support team for quality assurance and improvement.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T15:03:56.608847+01:00","updated_at":"2025-12-23T15:03:56.608847+01:00"} {"id":"agent-relay-hgw","title":"PR-9 Review: Add reconnection logic to MultiProjectClient","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-22T21:54:09.712003+01:00","updated_at":"2025-12-24T11:50:55.708361+01:00","closed_at":"2025-12-24T11:50:55.708361+01:00"} -{"id":"agent-relay-hks","title":"Increase test coverage for daemon/server.ts, dashboard, and CLI","description":"Current coverage is only 39% overall. Key files with 0% coverage: daemon/server.ts, dashboard/server.ts, cli/index.ts, wrapper/client.ts, wrapper/tmux-wrapper.ts, utils/project-namespace.ts. Need integration tests for the daemon startup/shutdown lifecycle and CLI commands.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T00:17:29.603137+01:00","updated_at":"2025-12-22T17:14:41.611717+01:00","closed_at":"2025-12-22T17:14:41.611717+01:00"} +{"id":"agent-relay-hks","title":"Increase test coverage for daemon/server.ts, dashboard, and CLI","description":"Current coverage is only 39% overall. Key files with 0% coverage: daemon/server.ts, dashboard/server.ts, cli/index.ts, wrapper/client.ts, wrapper/tmux-wrapper.ts, utils/project-namespace.ts. Need integration tests for the daemon startup/shutdown lifecycle and CLI commands.","status":"closed","priority":2,"issue_type":"task","assignee":"Lead","created_at":"2025-12-20T00:17:29.603137+01:00","updated_at":"2025-12-22T17:14:41.611717+01:00","closed_at":"2025-12-22T17:14:41.611717+01:00"} {"id":"agent-relay-hr1","title":"Show project reconnection status in dashboard","description":"Display project connection status (Connected/Reconnecting/Disconnected) in dashboard header. Use onProjectStateChange callbacks from MultiProjectClient to drive 'Connected/Disconnecting' indicators. Show per-project status and overall relay connection health. Color code: green (connected), yellow (reconnecting), red (disconnected).","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-24T11:51:32.74308+01:00","updated_at":"2025-12-24T11:52:22.778492+01:00","closed_at":"2025-12-24T11:52:22.778492+01:00"} {"id":"agent-relay-hvm","title":"Dashboard: Add ability to send messages to agents","description":"Add a message compose UI in the dashboard to send messages to any connected agent. Should include: (1) Agent selector dropdown, (2) Message input field, (3) Send button that dispatches via the relay protocol. This complements the existing message viewing capability.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-22T15:31:07.022117+01:00","updated_at":"2025-12-22T15:36:18.78037+01:00","closed_at":"2025-12-22T15:36:18.78037+01:00"} -{"id":"agent-relay-i5f","title":"Fix rendering and display issues","description":"Relay messages display with garbled/overlapping text in terminal. Characters appear scattered and mixed with other terminal content. Example: message text like 'Ping me when you're ready' renders with letters displaced across the line. PR #10 added input detection (isInputClear, waitForClearInput, getCursorX) to prevent this, but it's not working effectively. Need to investigate why the input detection/wait logic isn't preventing message injection during active typing.","notes":"Parser improvements for continuation lines (bullets, numbered lists, box drawing chars for Gemini). Debug flag set to false.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-22T11:59:53.254169+01:00","updated_at":"2025-12-22T14:21:32.043374+01:00","closed_at":"2025-12-22T14:21:32.043374+01:00"} -{"id":"agent-relay-i6gt","title":"Debug thinking block filter - tests fail despite correct patterns","notes":"Investigation notes:\n\nWhat was implemented:\n- Added THINKING_START and THINKING_END patterns to parser.ts (lines 69-71)\n- Added inThinkingBlock state variable (line 157)\n- Added detection logic in parsePassThrough (lines 445-465)\n- Added reset logic in flush() and reset() methods\n\nThe problem:\nTests show the thinking block content is NOT being filtered - it passes through to output.\nThe patterns match correctly when tested standalone in Node.\n\nWhat to investigate:\n1. The check is at lines 456-465 in parser.ts - why doesn't it match at runtime?\n2. Could be an issue with how the for loop flows or where the check is placed\n3. Try adding console.log in the detection code to trace execution\n4. The compiled code at dist/wrapper/parser.js lines 380-390 looks correct\n\nFiles modified:\n- src/wrapper/parser.ts - thinking block filter implementation\n- Tests were written but reverted (failing)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-27T11:42:23.020797+01:00","updated_at":"2026-01-02T22:55:06.194884+01:00","closed_at":"2026-01-02T22:11:21.484362+01:00"} +{"id":"agent-relay-i5f","title":"Fix rendering and display issues","description":"Relay messages display with garbled/overlapping text in terminal. Characters appear scattered and mixed with other terminal content. Example: message text like 'Ping me when you're ready' renders with letters displaced across the line. PR #10 added input detection (isInputClear, waitForClearInput, getCursorX) to prevent this, but it's not working effectively. Need to investigate why the input detection/wait logic isn't preventing message injection during active typing.","notes":"Parser improvements for continuation lines (bullets, numbered lists, box drawing chars for Gemini). Debug flag set to false.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-22T11:59:53.254169+01:00","updated_at":"2025-12-22T14:21:32.043374+01:00","closed_at":"2025-12-22T14:21:32.043374+01:00","labels":["review"]} +{"id":"agent-relay-i6gt","title":"Debug thinking block filter - tests fail despite correct patterns","notes":"Investigation notes:\n\nWhat was implemented:\n- Added THINKING_START and THINKING_END patterns to parser.ts (lines 69-71)\n- Added inThinkingBlock state variable (line 157)\n- Added detection logic in parsePassThrough (lines 445-465)\n- Added reset logic in flush() and reset() methods\n\nThe problem:\nTests show the thinking block content is NOT being filtered - it passes through to output.\nThe patterns match correctly when tested standalone in Node.\n\nWhat to investigate:\n1. The check is at lines 456-465 in parser.ts - why doesn't it match at runtime?\n2. Could be an issue with how the for loop flows or where the check is placed\n3. Try adding console.log in the detection code to trace execution\n4. The compiled code at dist/wrapper/parser.js lines 380-390 looks correct\n\nFiles modified:\n- src/wrapper/parser.ts - thinking block filter implementation\n- Tests were written but reverted (failing)","status":"closed","priority":2,"issue_type":"bug","assignee":"Fixer","created_at":"2025-12-27T11:42:23.020797+01:00","updated_at":"2026-01-02T22:55:06.194884+01:00","closed_at":"2026-01-02T22:11:21.484362+01:00"} {"id":"agent-relay-i7s","title":"Add priority-based message queuing","description":"Implement message priority levels (urgent/high/normal/low) like AI Maestro. Urgent messages jump the queue and interrupt agents. Enables time-sensitive coordination in production scenarios.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:04:48.031237+01:00","updated_at":"2025-12-23T17:04:48.031237+01:00"} {"id":"agent-relay-iblw","title":"Allow agents to spawn other agents via agent-relay API","description":"Agents should be able to spawn new agent processes programmatically via agent-relay API. Enables dynamic team scaling and parallel agent coordination. Implement: (1) Agent spawn endpoint, (2) API wrapper for agent code, (3) Spawn coordination, (4) Resource limits, (5) Tests.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-26T17:08:22.05611+01:00","updated_at":"2025-12-26T17:08:22.05611+01:00"} {"id":"agent-relay-iic5","title":"Multi-line relay messages truncated in dashboard UI (regression)","description":"Multi-line relay messages are only displaying the first line in the dashboard UI, even though the parser is capturing all lines. Root cause investigation needed: (1) Dashboard rendering (may be truncating display), (2) WebSocket message transmission, (3) Message state management. Previously marked as fixed but still broken.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-26T17:05:33.146646+01:00","updated_at":"2025-12-26T17:20:58.611383+01:00","closed_at":"2025-12-26T17:20:58.611383+01:00"} {"id":"agent-relay-inj1","title":"[Core] Harden message injection with verification and retry","description":"## Summary\nMessage injection is core to Relay's real-time communication. Currently both TmuxWrapper and PtyWrapper inject messages blindly without verification. This bead adds injection confirmation, retry logic, and fallback mechanisms.\n\n## Current State\n\n### TmuxWrapper (interactive sessions)\n- Uses `tmux send-keys` for injection\n- Has idle detection (1.5s wait after output)\n- Has output stability check (pane content stops changing)\n- Has cursor position check (prompt detection)\n- Has bracketed paste to prevent interleaving\n- **Missing**: No verification that message appeared in scrollback\n- **Missing**: No retry on failure\n\n### PtyWrapper (spawned workers)\n- Uses `ptyProcess.write()` for direct injection\n- Has message queue with sequential processing\n- Has 500ms delay between messages\n- **Missing**: No idle detection (just blindly writes)\n- **Missing**: No verification\n- **Missing**: No retry logic\n\n## Implementation Plan\n\n### Phase 1: Injection Verification\n\n**TmuxWrapper changes:**\n```typescript\nprivate async verifyInjection(msg: QueuedMessage): Promise\u003cboolean\u003e {\n // Wait for injection to settle\n await this.sleep(200);\n \n // Capture pane content\n const paneContent = await this.capturePane();\n \n // Check if our message appears in recent output\n // Look for 'Relay message from {from} [{shortId}]'\n const expectedPattern = `Relay message from ${msg.from} [${msg.messageId.substring(0, 8)}]`;\n return paneContent.includes(expectedPattern);\n}\n```\n\n**PtyWrapper changes:**\n```typescript\nprivate async verifyInjection(msg: QueuedMessage): Promise\u003cboolean\u003e {\n // Wait for output to include our message\n const startTime = Date.now();\n const timeout = 2000;\n const expectedPattern = `Relay message from ${msg.from} [${msg.messageId.substring(0, 8)}]`;\n \n while (Date.now() - startTime \u003c timeout) {\n if (this.rawBuffer.includes(expectedPattern)) {\n return true;\n }\n await this.sleep(100);\n }\n return false;\n}\n```\n\n### Phase 2: Retry with Backoff\n\n```typescript\nprivate async injectWithRetry(\n msg: QueuedMessage,\n maxRetries: number = 3\n): Promise\u003cInjectionResult\u003e {\n for (let attempt = 0; attempt \u003c maxRetries; attempt++) {\n // Inject the message\n await this.performInjection(msg);\n \n // Verify it landed\n if (await this.verifyInjection(msg)) {\n return { success: true, attempts: attempt + 1 };\n }\n \n // Log retry attempt\n this.logStderr(`Injection not verified, retry ${attempt + 1}/${maxRetries}`);\n \n // Backoff before retry\n await this.sleep(500 * (attempt + 1));\n }\n \n // All retries failed\n return { success: false, attempts: maxRetries };\n}\n```\n\n### Phase 3: Fallback to Inbox\n\nIf injection fails after retries, write to file inbox as durable fallback:\n\n```typescript\nprivate async injectNextMessage(): Promise\u003cvoid\u003e {\n const msg = this.messageQueue.shift();\n if (!msg) return;\n \n const result = await this.injectWithRetry(msg);\n \n if (!result.success) {\n // Fallback: write to inbox file\n this.inbox?.addMessage(msg.from, msg.body);\n this.logStderr(`Injection failed after ${result.attempts} attempts, wrote to inbox`);\n \n // Emit event for dashboard to show warning\n this.emit('injection-failed', {\n messageId: msg.messageId,\n from: msg.from,\n fallback: 'inbox'\n });\n }\n}\n```\n\n### Phase 4: PtyWrapper Idle Detection\n\nAdd output stability check to PtyWrapper (currently missing):\n\n```typescript\nprivate async waitForOutputStable(timeoutMs: number = 2000): Promise\u003cboolean\u003e {\n const pollInterval = 200;\n const requiredStablePolls = 2;\n let lastLength = this.rawBuffer.length;\n let stableCount = 0;\n const startTime = Date.now();\n \n while (Date.now() - startTime \u003c timeoutMs) {\n await this.sleep(pollInterval);\n \n if (this.rawBuffer.length === lastLength) {\n stableCount++;\n if (stableCount \u003e= requiredStablePolls) {\n return true;\n }\n } else {\n stableCount = 0;\n lastLength = this.rawBuffer.length;\n }\n }\n \n return false;\n}\n\nprivate async processMessageQueue(): Promise\u003cvoid\u003e {\n // ... existing checks ...\n \n // NEW: Wait for output to stabilize before injecting\n const stable = await this.waitForOutputStable();\n if (!stable) {\n // Re-queue message and try later\n this.messageQueue.unshift(msg);\n setTimeout(() =\u003e this.processMessageQueue(), 500);\n return;\n }\n \n // Proceed with injection\n // ...\n}\n```\n\n### Phase 5: Health Check Before Injection\n\n**TmuxWrapper:**\n```typescript\nprivate async isAgentAlive(): Promise\u003cboolean\u003e {\n // Check if tmux session still exists\n try {\n await execAsync(`tmux has-session -t ${this.sessionName}`);\n return true;\n } catch {\n return false;\n }\n}\n```\n\n**PtyWrapper:**\n```typescript\nprivate isAgentAlive(): boolean {\n return this.running \u0026\u0026 this.ptyProcess !== undefined;\n}\n```\n\n### Phase 6: Dashboard Integration\n\nSurface injection status in dashboard:\n\n1. Add `injection_status` field to message records: `pending`, `delivered`, `failed`\n2. Show visual indicator in message list (green check, yellow pending, red X)\n3. Add alert for failed injections\n4. Show retry count in message details\n\n### Phase 7: Metrics \u0026 Observability\n\nAdd metrics for monitoring injection reliability:\n\n```typescript\ninterface InjectionMetrics {\n total_injections: number;\n successful_first_try: number;\n successful_with_retry: number;\n failed_to_inbox: number;\n average_retry_count: number;\n}\n```\n\nExpose via `/metrics` endpoint for Prometheus scraping.\n\n## Files to Modify\n\n1. `src/wrapper/tmux-wrapper.ts` - Add verification, retry, fallback\n2. `src/wrapper/pty-wrapper.ts` - Add idle detection, verification, retry, fallback\n3. `src/wrapper/inbox.ts` - Ensure atomic writes, add message ID tracking\n4. `src/dashboard-server/server.ts` - Add injection status to WebSocket updates\n5. `src/storage/adapter.ts` - Add injection_status field to message schema\n6. `src/dashboard/` - UI for injection status indicators\n\n## Testing Plan\n\n1. Unit tests for verification logic\n2. Integration test: inject during active output, verify retry works\n3. Integration test: inject to dead session, verify fallback to inbox\n4. Load test: rapid message injection, measure success rate\n5. Manual test: kill agent mid-injection, verify graceful handling\n\n## Success Criteria\n\n- Injection success rate \u003e 99% under normal conditions\n- Failed injections always fall back to inbox (no message loss)\n- Dashboard shows clear status for all message deliveries\n- Metrics available for monitoring injection health","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-02T13:00:00Z","updated_at":"2026-01-02T20:36:32.875138+01:00","closed_at":"2026-01-02T20:36:32.875138+01:00"} {"id":"agent-relay-j7y","title":"Implement composer toolbar buttons (bold, emoji)","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-23T16:26:23.598781+01:00","updated_at":"2025-12-23T16:27:53.92751+01:00","closed_at":"2025-12-23T16:27:53.92751+01:00"} -{"id":"agent-relay-j9z","title":"Message injection corrupts human input in progress","description":"When a relay message arrives while the human is actively typing, the injection (Esc + Ctrl-U + message + Enter) destroys their partial input. Need to detect active input state before injecting. Options: (1) Check tmux pane input buffer, (2) Queue messages until prompt detected, (3) Use tmux display-message instead of send-keys for non-intrusive notification, (4) Add visual indicator that message is pending.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-20T21:48:03.280298+01:00","updated_at":"2026-01-02T21:14:36.354295+01:00","closed_at":"2026-01-02T21:04:04.208607+01:00","dependencies":[{"issue_id":"agent-relay-j9z","depends_on_id":"agent-relay-6rz","type":"blocks","created_at":"2025-12-20T21:48:10.118963+01:00","created_by":"daemon"}]} -{"id":"agent-relay-jmwh","title":"Bundle tmux or provide fallback so users don't need to install separately","description":"User installation failed: tmux command not found. Currently agent-relay requires tmux to be pre-installed on the system. Need to bundle tmux or provide a fallback so installation works out-of-the-box.\n\nOptions to explore:\n1. NPM package that includes tmux binaries (npm tmux package or similar)\n2. Postinstall script that downloads/installs tmux\n3. Fallback to alternative shell mechanism if tmux unavailable\n4. Docker-based approach (out of scope but document)\n\nAcceptance: Installation should succeed and agent-relay should work without requiring manual tmux install","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-12-28T18:51:04.556744-05:00","updated_at":"2025-12-28T19:32:54.921952-05:00","closed_at":"2025-12-28T19:32:54.921952-05:00"} +{"id":"agent-relay-j9z","title":"Message injection corrupts human input in progress","description":"When a relay message arrives while the human is actively typing, the injection (Esc + Ctrl-U + message + Enter) destroys their partial input. Need to detect active input state before injecting. Options: (1) Check tmux pane input buffer, (2) Queue messages until prompt detected, (3) Use tmux display-message instead of send-keys for non-intrusive notification, (4) Add visual indicator that message is pending.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-20T21:48:03.280298+01:00","updated_at":"2026-01-02T21:14:36.354295+01:00","closed_at":"2026-01-02T21:04:04.208607+01:00","dependencies":[{"issue_id":"agent-relay-j9z","depends_on_id":"agent-relay-6rz","type":"blocks","created_at":"2025-12-20T21:48:10.118963+01:00","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-jmwh","title":"Bundle tmux or provide fallback so users don't need to install separately","description":"User installation failed: tmux command not found. Currently agent-relay requires tmux to be pre-installed on the system. Need to bundle tmux or provide a fallback so installation works out-of-the-box.\n\nOptions to explore:\n1. NPM package that includes tmux binaries (npm tmux package or similar)\n2. Postinstall script that downloads/installs tmux\n3. Fallback to alternative shell mechanism if tmux unavailable\n4. Docker-based approach (out of scope but document)\n\nAcceptance: Installation should succeed and agent-relay should work without requiring manual tmux install","status":"closed","priority":0,"issue_type":"feature","assignee":"FullStack","created_at":"2025-12-28T18:51:04.556744-05:00","updated_at":"2025-12-28T19:32:54.921952-05:00","closed_at":"2025-12-28T19:32:54.921952-05:00","close_reason":"Implemented: postinstall script downloads tmux, tmux-resolver provides system→bundled fallback, all code uses resolved path, tests pass"} {"id":"agent-relay-k8t","title":"Consider backend-focused agent for daemon work","description":"Session retrospective: only had FE-Dev specialist, so backend work (permission detection, daemon issues) fell to Lead. Consider spawning a dedicated backend agent for daemon/server work in future sessions.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-23T23:10:24.756163+01:00","updated_at":"2025-12-23T23:10:24.756163+01:00"} -{"id":"agent-relay-kaj","title":"Show project connection status in dashboard (Connected/Reconnecting/Disconnected)","description":"Use onProjectStateChange callbacks from MultiProjectClient to show connection status in dashboard. When onProjectStateChange(id, false) fires, show 'Disconnected' or 'Reconnecting...' indicator. When onProjectStateChange(id, true) fires, show 'Connected'. Could use a status badge in the sidebar or header.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-24T11:51:40.774105+01:00","updated_at":"2025-12-24T11:55:00.944949+01:00","closed_at":"2025-12-24T11:55:00.944949+01:00"} +{"id":"agent-relay-kaj","title":"Show project connection status in dashboard (Connected/Reconnecting/Disconnected)","description":"Use onProjectStateChange callbacks from MultiProjectClient to show connection status in dashboard. When onProjectStateChange(id, false) fires, show 'Disconnected' or 'Reconnecting...' indicator. When onProjectStateChange(id, true) fires, show 'Connected'. Could use a status badge in the sidebar or header.","status":"closed","priority":2,"issue_type":"feature","assignee":"FE-Dev","created_at":"2025-12-24T11:51:40.774105+01:00","updated_at":"2025-12-24T11:55:00.944949+01:00","closed_at":"2025-12-24T11:55:00.944949+01:00"} {"id":"agent-relay-kto","title":"Add topic-based pub/sub messaging","description":"Add topic subscription alongside direct addressing. Agents can subscribe to topics (e.g., 'errors', 'deploys', 'reviews') and receive all messages on those topics. Complements existing -\u003erelay:Name with -\u003erelay:#topic pattern. Pattern from Claude-Flow's message bus.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:05:27.213842+01:00","updated_at":"2025-12-23T17:05:27.213842+01:00"} -{"id":"agent-relay-kw80","title":"Plan and execute Dashboard v1 → v2 migration","description":"## Dashboard v1 → v2 Migration Plan\n\n### Feature Parity Analysis (COMPLETED)\n\n**V1 Features in V2:** ✅ ALL FEATURES PORTED\n- Agent list with online/offline status\n- Message list with date dividers \n- Channel selection (general + DMs)\n- Command palette (Cmd+K)\n- Spawn agent modal\n- Thread support\n- Fleet view (multi-server)\n- Release agent button\n- Needs attention badge\n- ✅ @-mention autocomplete (agent-relay-ekk9 - DONE)\n\n**V2-Only Features (Enhancements):**\n- Theme support (light/dark/system)\n- Settings panel\n- Trajectory viewer\n- Decision queue\n- Enhanced broadcast composer with templates\n- Notification toasts\n\n### Migration Steps\n\n1. **[agent-relay-lls5]** Configure parallel running (v1:4280, v2:4281) - READY\n2. **[agent-relay-s8hg]** Add --dashboard=v1|v2 CLI flag - blocked by lls5\n3. ~~**[agent-relay-ekk9]** Fix @-mention autocomplete~~ ✅ DONE\n4. **[agent-relay-qkey]** Switch default to v2 - blocked by s8hg\n\n### Rollback Procedure\n\n1. Use `agent-relay up --dashboard=v1` to revert instantly\n2. V1 code remains in place until v2 is stable\n3. No data migration needed - both share same backend/storage","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-26T17:01:58.36924+01:00","updated_at":"2025-12-29T01:52:36.152813-05:00","closed_at":"2025-12-29T05:46:32.847338602Z","dependencies":[{"issue_id":"agent-relay-kw80","depends_on_id":"agent-relay-cuer","type":"blocks","created_at":"2025-12-26T17:02:04.103774+01:00","created_by":"daemon"},{"issue_id":"agent-relay-kw80","depends_on_id":"agent-relay-ekk9","type":"blocks","created_at":"2025-12-27T05:36:37.645846+01:00","created_by":"daemon"}]} +{"id":"agent-relay-kw80","title":"Plan and execute Dashboard v1 → v2 migration","description":"## Dashboard v1 → v2 Migration Plan\n\n### Feature Parity Analysis (COMPLETED)\n\n**V1 Features in V2:** ✅ ALL FEATURES PORTED\n- Agent list with online/offline status\n- Message list with date dividers \n- Channel selection (general + DMs)\n- Command palette (Cmd+K)\n- Spawn agent modal\n- Thread support\n- Fleet view (multi-server)\n- Release agent button\n- Needs attention badge\n- ✅ @-mention autocomplete (agent-relay-ekk9 - DONE)\n\n**V2-Only Features (Enhancements):**\n- Theme support (light/dark/system)\n- Settings panel\n- Trajectory viewer\n- Decision queue\n- Enhanced broadcast composer with templates\n- Notification toasts\n\n### Migration Steps\n\n1. **[agent-relay-lls5]** Configure parallel running (v1:4280, v2:4281) - READY\n2. **[agent-relay-s8hg]** Add --dashboard=v1|v2 CLI flag - blocked by lls5\n3. ~~**[agent-relay-ekk9]** Fix @-mention autocomplete~~ ✅ DONE\n4. **[agent-relay-qkey]** Switch default to v2 - blocked by s8hg\n\n### Rollback Procedure\n\n1. Use `agent-relay up --dashboard=v1` to revert instantly\n2. V1 code remains in place until v2 is stable\n3. No data migration needed - both share same backend/storage","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-26T17:01:58.36924+01:00","updated_at":"2025-12-29T01:52:36.152813-05:00","closed_at":"2025-12-29T05:46:32.847338602Z","dependencies":[{"issue_id":"agent-relay-kw80","depends_on_id":"agent-relay-cuer","type":"blocks","created_at":"2025-12-26T17:02:04.103774+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"agent-relay-kw80","depends_on_id":"agent-relay-ekk9","type":"blocks","created_at":"2025-12-27T05:36:37.645846+01:00","created_by":"daemon"}]} {"id":"agent-relay-kzw","title":"Project namespace uses /tmp which can be cleared on reboot","description":"In utils/project-namespace.ts:13, BASE_DIR is /tmp/agent-relay. On macOS/Linux, /tmp is cleared on reboot, losing all message history. Consider: (1) XDG_DATA_HOME fallback, (2) ~/.agent-relay option, (3) Per-project .agent-relay folder.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:33.224965+01:00","updated_at":"2025-12-20T00:18:33.224965+01:00"} -{"id":"agent-relay-l99","title":"Add output filtering for cleaner logs","description":"Filter noisy patterns from logs: thinking indicators [1/418], empty ANSI-only lines, etc. Add config.filterLogs option. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"open","priority":4,"issue_type":"feature","created_at":"2025-12-20T21:28:51.199736+01:00","updated_at":"2025-12-20T21:28:51.199736+01:00","dependencies":[{"issue_id":"agent-relay-l99","depends_on_id":"agent-relay-1ek","type":"blocks","created_at":"2025-12-20T21:29:26.987526+01:00","created_by":"daemon"}]} -{"id":"agent-relay-lfky","title":"Add 'agent thinking/processing' indicator to dashboard","description":"When an agent receives a message, the dashboard should show a visual indicator that the agent is processing/thinking. Similar to 'typing...' indicators in chat apps.\n\n**Requirements:**\n1. Backend: Track agent status (idle/processing/responding)\n2. Protocol: Agents report when they start/finish processing a message\n3. Dashboard UI: Show pulsing/animated indicator next to agent when processing\n4. Timeout: Auto-clear indicator if no response after X seconds\n\n**Implementation options:**\n- Agents send STATUS: PROCESSING / STATUS: IDLE messages\n- Or use heartbeat with embedded state\n- Or infer from message timestamps (less reliable)\n\nRequested by: Dashboard agent during v1→v2 migration planning","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-27T05:38:19.022332+01:00","updated_at":"2025-12-27T10:59:41.070383+01:00","closed_at":"2025-12-27T10:59:41.070383+01:00"} +{"id":"agent-relay-l99","title":"Add output filtering for cleaner logs","description":"Filter noisy patterns from logs: thinking indicators [1/418], empty ANSI-only lines, etc. Add config.filterLogs option. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"open","priority":4,"issue_type":"feature","created_at":"2025-12-20T21:28:51.199736+01:00","updated_at":"2025-12-20T21:28:51.199736+01:00","dependencies":[{"issue_id":"agent-relay-l99","depends_on_id":"agent-relay-1ek","type":"blocks","created_at":"2025-12-20T21:29:26.987526+01:00","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-lfky","title":"Add 'agent thinking/processing' indicator to dashboard","description":"When an agent receives a message, the dashboard should show a visual indicator that the agent is processing/thinking. Similar to 'typing...' indicators in chat apps.\n\n**Requirements:**\n1. Backend: Track agent status (idle/processing/responding)\n2. Protocol: Agents report when they start/finish processing a message\n3. Dashboard UI: Show pulsing/animated indicator next to agent when processing\n4. Timeout: Auto-clear indicator if no response after X seconds\n\n**Implementation options:**\n- Agents send STATUS: PROCESSING / STATUS: IDLE messages\n- Or use heartbeat with embedded state\n- Or infer from message timestamps (less reliable)\n\nRequested by: Dashboard agent during v1→v2 migration planning","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-27T05:38:19.022332+01:00","updated_at":"2025-12-27T10:59:41.070383+01:00","closed_at":"2025-12-27T10:59:41.070383+01:00","close_reason":"Processing indicator implementation complete with router tracking and dashboard color support"} {"id":"agent-relay-lls5","title":"Configure daemon to serve v2 dashboard on alt port","description":"Add daemon configuration to serve v2 dashboard (Next.js) on port 4281 while keeping v1 on 4280. This enables parallel running during migration period.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-27T05:36:49.475705+01:00","updated_at":"2025-12-29T01:52:36.153625-05:00","closed_at":"2025-12-29T05:47:40.979797502Z"} {"id":"agent-relay-lto","title":"Command palette channel/chat selection","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-23T16:15:32.818842+01:00","updated_at":"2025-12-23T16:18:00.952354+01:00","closed_at":"2025-12-23T16:18:00.952354+01:00"} -{"id":"agent-relay-mcl","title":"Gemini agent cannot send messages properly","description":"Gemini agent cannot autonomously send relay messages. When Gemini outputs @relay: patterns, it enters shell mode instead of being intercepted by the relay wrapper. Manual typing works fine. This suggests a PTY parsing issue specific to how Gemini outputs the @relay: pattern - possibly timing, escaping, or output buffering differences compared to Claude.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-20T00:29:15.797552+01:00","updated_at":"2025-12-20T21:56:07.20143+01:00","closed_at":"2025-12-20T21:56:07.20143+01:00"} -{"id":"agent-relay-mkz","title":"Fix multi-line message display in dashboard","description":"Multi-line messages are not displaying properly in the dashboard UI. This is a critical UX issue affecting message readability. Need to fix the frontend rendering of message content to properly handle newlines, formatting, and multi-line text.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-25T14:11:25.663841+01:00","updated_at":"2025-12-26T11:51:07.14395+01:00","closed_at":"2025-12-26T11:51:07.14395+01:00"} -{"id":"agent-relay-mplo","title":"Parser: Fix overcapturing text in multi-line relay messages","description":"Recent parser improvements (agent-relay-6dl8 fix) are still overcapturing text in multi-line relay messages. Dashboard reports text is being included beyond message boundaries. Need to review parser.ts finishBlock() and handle parsing edge cases.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-26T21:06:42.006611+01:00","updated_at":"2025-12-27T05:43:25.432704+01:00","closed_at":"2025-12-27T05:43:25.432704+01:00"} +{"id":"agent-relay-mcl","title":"Gemini agent cannot send messages properly","description":"Gemini agent cannot autonomously send relay messages. When Gemini outputs @relay: patterns, it enters shell mode instead of being intercepted by the relay wrapper. Manual typing works fine. This suggests a PTY parsing issue specific to how Gemini outputs the @relay: pattern - possibly timing, escaping, or output buffering differences compared to Claude.","status":"closed","priority":2,"issue_type":"bug","assignee":"LeadDev","created_at":"2025-12-20T00:29:15.797552+01:00","updated_at":"2025-12-20T21:56:07.20143+01:00","closed_at":"2025-12-20T21:56:07.20143+01:00"} +{"id":"agent-relay-mkz","title":"Fix multi-line message display in dashboard","description":"Multi-line messages are not displaying properly in the dashboard UI. This is a critical UX issue affecting message readability. Need to fix the frontend rendering of message content to properly handle newlines, formatting, and multi-line text.","status":"closed","priority":1,"issue_type":"bug","assignee":"Frontend","created_at":"2025-12-25T14:11:25.663841+01:00","updated_at":"2025-12-26T11:51:07.14395+01:00","closed_at":"2025-12-26T11:51:07.14395+01:00"} +{"id":"agent-relay-mplo","title":"Parser: Fix overcapturing text in multi-line relay messages","description":"Recent parser improvements (agent-relay-6dl8 fix) are still overcapturing text in multi-line relay messages. Dashboard reports text is being included beyond message boundaries. Need to review parser.ts finishBlock() and handle parsing edge cases.","status":"closed","priority":1,"issue_type":"bug","assignee":"FullStack","created_at":"2025-12-26T21:06:42.006611+01:00","updated_at":"2025-12-27T05:43:25.432704+01:00","closed_at":"2025-12-27T05:43:25.432704+01:00","close_reason":"Fixed: Reverted parser to simpler logic - inline messages are single-line only, only indented lines continue (TUI wrapping). Multi-line messages should use [[RELAY]] block format."} {"id":"agent-relay-n36","title":"Add metrics dashboard for system health monitoring","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-25T14:07:36.499133+01:00","updated_at":"2025-12-26T11:41:20.33149+01:00","closed_at":"2025-12-26T11:41:20.33149+01:00"} {"id":"agent-relay-nox","title":"Dashboard not showing live agents or messages","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-23T12:11:48.614811+01:00","updated_at":"2025-12-23T12:26:51.322956+01:00","closed_at":"2025-12-23T12:26:51.322956+01:00"} {"id":"agent-relay-oiw","title":"BUG: No per-agent inbox files created","description":"Expected /tmp/agent-relay/\u003cproject\u003e/\u003cagent\u003e/inbox.md but only team/agents.json exists. Agents cannot read their inbox via file.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-20T21:46:24.826442+01:00","updated_at":"2025-12-22T14:29:46.830632+01:00","closed_at":"2025-12-22T14:29:46.830632+01:00"} {"id":"agent-relay-p0k","title":"Gemini agents cannot send relay messages","description":"Gem (Gemini CLI) can receive messages but cannot send them. Tried: -\u003erelay:, \u003e\u003erelay:, escaping with backticks, splitting lines. Messages don't get captured/sent. Possibly Gemini CLI output handling differs from Claude/Codex. Need to investigate how Gemini outputs text and whether the tmux capture-pane is picking it up correctly.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-22T13:52:43.407612+01:00","updated_at":"2025-12-22T13:55:11.068344+01:00","closed_at":"2025-12-22T13:55:11.068344+01:00"} -{"id":"agent-relay-pu6h","title":"Align metrics page styling with dashboard","description":"The metrics page styling is completely different from the main dashboard. Need to update it to match the dashboard's look and feel.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:09:13.958899-05:00","updated_at":"2025-12-28T22:13:36.714948-05:00","closed_at":"2025-12-28T22:13:36.714948-05:00"} +{"id":"agent-relay-pu6h","title":"Align metrics page styling with dashboard","description":"The metrics page styling is completely different from the main dashboard. Need to update it to match the dashboard's look and feel.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T22:09:13.958899-05:00","updated_at":"2025-12-28T22:13:36.714948-05:00","closed_at":"2025-12-28T22:13:36.714948-05:00","close_reason":"Fixed by Frontend - updated colors, fonts, removed sci-fi effects to match dashboard"} {"id":"agent-relay-pvx","title":"Dashboard agent detection debugging","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-23T17:14:36.376736+01:00","updated_at":"2025-12-23T17:16:13.115531+01:00","closed_at":"2025-12-23T17:16:13.115531+01:00"} -{"id":"agent-relay-qg4e","title":"Fix line break display in dashboard messages","description":"Line breaks in messages display as extra spaces instead of actual line breaks. Messages should render with proper newlines.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-28T22:09:53.92514-05:00","updated_at":"2025-12-28T22:11:31.098231-05:00","closed_at":"2025-12-28T22:11:31.098231-05:00"} +{"id":"agent-relay-qg4e","title":"Fix line break display in dashboard messages","description":"Line breaks in messages display as extra spaces instead of actual line breaks. Messages should render with proper newlines.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-28T22:09:53.92514-05:00","updated_at":"2025-12-28T22:11:31.098231-05:00","closed_at":"2025-12-28T22:11:31.098231-05:00","close_reason":"Fixed by Frontend - added normalization for literal \\n strings, \\r\\n, and \\r"} {"id":"agent-relay-qkey","title":"Switch default dashboard to v2","description":"After feature parity is verified and parallel running period is complete, switch the default dashboard from v1 to v2. Update agent-relay up command to serve v2 by default.","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-27T05:36:52.036031+01:00","updated_at":"2025-12-29T01:52:36.154125-05:00","closed_at":"2025-12-29T05:47:57.245409495Z","dependencies":[{"issue_id":"agent-relay-qkey","depends_on_id":"agent-relay-s8hg","type":"blocks","created_at":"2025-12-27T05:37:00.746565+01:00","created_by":"daemon"},{"issue_id":"agent-relay-qkey","depends_on_id":"agent-relay-ekk9","type":"blocks","created_at":"2025-12-27T05:37:00.817799+01:00","created_by":"daemon"}]} {"id":"agent-relay-rm7","title":"Add scheduling strategies for task distribution","description":"Implement scheduling strategies for distributing tasks across agents. Start with round-robin, then add capability-based, least-loaded, and affinity strategies. Inspired by Claude-Flow's 4-strategy approach. Enables autonomous task assignment instead of manual coordination.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:03:22.014068+01:00","updated_at":"2025-12-23T17:03:22.014068+01:00"} {"id":"agent-relay-ruv","title":"Add work stealing for load balancing","description":"Implement work stealing so idle agents can claim tasks from overloaded peers. Key production feature for better resource utilization. Pattern from Claude-Flow's WorkStealingCoordinator. Requires task queue visibility and agent load metrics.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:03:28.153623+01:00","updated_at":"2025-12-23T17:03:28.153623+01:00"} {"id":"agent-relay-s8hg","title":"Add dashboard version switch to CLI","description":"Add --dashboard=v1|v2 flag to agent-relay up command to choose which dashboard version to run. Default should remain v1 until migration is complete.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-27T05:36:51.0113+01:00","updated_at":"2025-12-29T01:52:36.154656-05:00","closed_at":"2025-12-29T05:47:41.00431035Z","dependencies":[{"issue_id":"agent-relay-s8hg","depends_on_id":"agent-relay-lls5","type":"blocks","created_at":"2025-12-27T05:37:00.684918+01:00","created_by":"daemon"}]} -{"id":"agent-relay-sio","title":"Add graceful degradation when relay daemon is unavailable","description":"In wrapper/tmux-wrapper.ts:195-197, daemon connection failures are silently caught. Consider: (1) Periodic reconnection attempts, (2) Queueing messages for later delivery, (3) Visual indicator in terminal showing connection status.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:17:32.600333+01:00","updated_at":"2025-12-22T17:12:29.892345+01:00","closed_at":"2025-12-22T17:12:29.892345+01:00"} +{"id":"agent-relay-sio","title":"Add graceful degradation when relay daemon is unavailable","description":"In wrapper/tmux-wrapper.ts:195-197, daemon connection failures are silently caught. Consider: (1) Periodic reconnection attempts, (2) Queueing messages for later delivery, (3) Visual indicator in terminal showing connection status.","status":"closed","priority":2,"issue_type":"feature","assignee":"Implementer","created_at":"2025-12-20T00:17:32.600333+01:00","updated_at":"2025-12-22T17:12:29.892345+01:00","closed_at":"2025-12-22T17:12:29.892345+01:00"} {"id":"agent-relay-t4b","title":"Add spawn agent UI to dashboard","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-25T13:20:54.452959+01:00","updated_at":"2025-12-25T13:25:05.019339+01:00","closed_at":"2025-12-25T13:25:05.019339+01:00"} -{"id":"agent-relay-tun","title":"Debug output leaking to terminal during relay message send","description":"When sending relay messages (observed with relay:Gem), debug output is leaking to the terminal repeatedly. Output shows: 'relay:Gem] isInputClear: lastLine=\"\" ~/.../agent-relay (main*)' and 'ear=false' (truncated 'isInputClear=false'). Lines are split/corrupted and repeating in a loop. Likely issue in the tmux wrapper or message injection code where debug logging isn't being suppressed properly.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-22T13:40:28.576361+01:00","updated_at":"2025-12-22T13:42:16.746166+01:00","closed_at":"2025-12-22T13:42:16.746166+01:00"} +{"id":"agent-relay-tun","title":"Debug output leaking to terminal during relay message send","description":"When sending relay messages (observed with relay:Gem), debug output is leaking to the terminal repeatedly. Output shows: 'relay:Gem] isInputClear: lastLine=\"\" ~/.../agent-relay (main*)' and 'ear=false' (truncated 'isInputClear=false'). Lines are split/corrupted and repeating in a loop. Likely issue in the tmux wrapper or message injection code where debug logging isn't being suppressed properly.","status":"closed","priority":1,"issue_type":"bug","assignee":"Lead","created_at":"2025-12-22T13:40:28.576361+01:00","updated_at":"2025-12-22T13:42:16.746166+01:00","closed_at":"2025-12-22T13:42:16.746166+01:00"} {"id":"agent-relay-tx9","title":"Add circuit breaker for unresponsive agents","description":"Implement circuit breaker pattern to handle unresponsive or failing agents. Auto-detect agents that stop responding, temporarily remove from routing, attempt recovery, alert on persistent failures. Prevents cascade failures in production. Pattern from Claude-Flow's CircuitBreakerManager.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:03:48.162055+01:00","updated_at":"2025-12-23T17:03:48.162055+01:00"} -{"id":"agent-relay-u2z","title":"Add @-mention autocomplete with Tab in dashboard chat input","description":"When a user types '@' in the dashboard chat input, show a dropdown of available/connected agents. Tab key should autocomplete the selected agent name. This enables easy @-mentioning of agents for direct messages.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-23T22:52:34.749062+01:00","updated_at":"2025-12-23T22:57:11.634365+01:00","closed_at":"2025-12-23T22:57:11.634365+01:00"} +{"id":"agent-relay-u2z","title":"Add @-mention autocomplete with Tab in dashboard chat input","description":"When a user types '@' in the dashboard chat input, show a dropdown of available/connected agents. Tab key should autocomplete the selected agent name. This enables easy @-mentioning of agents for direct messages.","status":"closed","priority":1,"issue_type":"feature","assignee":"FE-Dev","created_at":"2025-12-23T22:52:34.749062+01:00","updated_at":"2025-12-23T22:57:11.634365+01:00","closed_at":"2025-12-23T22:57:11.634365+01:00"} {"id":"agent-relay-ucw","title":"Dashboard: multi-project navigation or dynamic port allocation","description":"When the dashboard is already running for one project, users should be able to either: (1) Navigate between different projects in a single dashboard view, OR (2) Start a new dashboard instance on an automatically allocated available port for a different project. Currently if a dashboard is running, starting another project conflicts.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-19T23:40:18.667766+01:00","updated_at":"2025-12-20T00:18:05.10495+01:00","closed_at":"2025-12-20T00:18:05.10495+01:00"} -{"id":"agent-relay-ude","title":"Complete agent-relay-90h backend: detect when agents need input","description":"Frontend for permission indicator is done (needsAttention field, pulsing indicator, badge). Backend needs to detect when agents are waiting for permissions/input and set the needsAttention flag. Could monitor stdin, parse terminal output for permission prompts, or have agents send explicit STATUS messages.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-23T23:10:06.438813+01:00","updated_at":"2025-12-24T11:48:43.798848+01:00","closed_at":"2025-12-24T11:48:43.798848+01:00"} -{"id":"agent-relay-ujq","title":"Add @-mention autocomplete in chat input","description":"When typing @ in the chat input, show autocomplete dropdown with available agent names. Tab to complete selection, arrow keys to navigate through options. Should filter as user types after @.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-24T08:01:03.793267+01:00","updated_at":"2025-12-24T08:02:51.638981+01:00","closed_at":"2025-12-24T08:02:51.638981+01:00"} -{"id":"agent-relay-v57","title":"No message expiration/cleanup in SQLite storage","description":"SQLite adapter has no TTL or cleanup mechanism for old messages. Over time, the database will grow unbounded. Add: (1) Configurable message retention period, (2) Automatic cleanup job, (3) Index on ts column is there but no cleanup uses it.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:01.86766+01:00","updated_at":"2025-12-22T17:17:41.867951+01:00","closed_at":"2025-12-22T17:17:41.867951+01:00"} +{"id":"agent-relay-ude","title":"Complete agent-relay-90h backend: detect when agents need input","description":"Frontend for permission indicator is done (needsAttention field, pulsing indicator, badge). Backend needs to detect when agents are waiting for permissions/input and set the needsAttention flag. Could monitor stdin, parse terminal output for permission prompts, or have agents send explicit STATUS messages.","status":"closed","priority":1,"issue_type":"task","assignee":"BE-Dev","created_at":"2025-12-23T23:10:06.438813+01:00","updated_at":"2025-12-24T11:48:43.798848+01:00","closed_at":"2025-12-24T11:48:43.798848+01:00"} +{"id":"agent-relay-ujq","title":"Add @-mention autocomplete in chat input","description":"When typing @ in the chat input, show autocomplete dropdown with available agent names. Tab to complete selection, arrow keys to navigate through options. Should filter as user types after @.","status":"closed","priority":1,"issue_type":"feature","assignee":"FE-Dev","created_at":"2025-12-24T08:01:03.793267+01:00","updated_at":"2025-12-24T08:02:51.638981+01:00","closed_at":"2025-12-24T08:02:51.638981+01:00"} +{"id":"agent-relay-v57","title":"No message expiration/cleanup in SQLite storage","description":"SQLite adapter has no TTL or cleanup mechanism for old messages. Over time, the database will grow unbounded. Add: (1) Configurable message retention period, (2) Automatic cleanup job, (3) Index on ts column is there but no cleanup uses it.","status":"closed","priority":2,"issue_type":"task","assignee":"Lead","created_at":"2025-12-20T00:18:01.86766+01:00","updated_at":"2025-12-22T17:17:41.867951+01:00","closed_at":"2025-12-22T17:17:41.867951+01:00"} {"id":"agent-relay-v7f","title":"Add conflict detection and resolution","description":"Implement conflict detection when multiple agents work on same files/resources. Add resolution strategies: priority-based, timestamp-based, voting-based. Prevents race conditions in production. Pattern from Claude-Flow's ConflictResolver and OptimisticLockManager.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:03:34.498546+01:00","updated_at":"2025-12-23T17:03:34.498546+01:00"} -{"id":"agent-relay-v9rc","title":"Fix light/dark mode toggle in settings","description":"The light mode / dark mode switch from the settings panel doesn't work","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T22:05:41.332721-05:00","updated_at":"2025-12-28T22:08:21.904724-05:00","closed_at":"2025-12-28T22:08:21.904724-05:00"} -{"id":"agent-relay-vxc","title":"Navigation consistency between bridge and project views","description":"Ensure navigation patterns are consistent when switching between bridge and project dashboard views","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-24T11:47:46.62539+01:00","updated_at":"2025-12-24T12:00:15.805473+01:00","closed_at":"2025-12-24T12:00:15.805473+01:00"} -{"id":"agent-relay-wrap1","title":"[Arch] Extract BaseWrapper abstract class for TmuxWrapper and PtyWrapper","description":"## Summary\nExtract common functionality between TmuxWrapper and PtyWrapper into an abstract base class to:\n1. Ensure feature parity between the two implementations\n2. Make adding new features easier (add once in base, implement specifics in subclasses)\n3. Prevent divergence over time\n4. Reduce code duplication\n\n## Current State Analysis\n\n### Common Code (should be in base class):\n- `RelayClient` initialization and lifecycle\n- `messageQueue` management (same structure in both)\n- `isInjecting` state flag\n- `sentMessageHashes: Set\u003cstring\u003e` for deduplication\n- `processedSpawnCommands` and `processedReleaseCommands` Sets\n- `stripAnsi()` method (nearly identical implementations)\n- `sleep()` helper (identical)\n- `sendRelayCommand()` method (similar pattern)\n- `handleIncomingMessage()` signature and queue push\n- Injection string format: `Relay message from ${from} [${shortId}]${hints}: ${body}`\n- Thread/importance/channel hint building\n- Spawn/release command parsing\n- `relayPrefix` handling\n\n### Diverged Code (TmuxWrapper has, PtyWrapper missing):\n- Storage integration (SQLite for summaries)\n- Activity state tracking (active/idle/disconnected)\n- Summary parsing (`[[SUMMARY]]` blocks)\n- Session end handling (`[[SESSION_END]]` blocks)\n- Sophisticated input clearing (cursor stability check)\n- Bracketed paste for safer injection\n- Continuation line joining for TUI output\n\n### Diverged Code (PtyWrapper has, TmuxWrapper missing):\n- EventEmitter extension (TmuxWrapper should also emit events)\n- `injection-failed` event emission\n- `getInjectionMetrics()` method\n- `pendingMessageCount` getter\n- Auto-accept prompts for Claude --dangerously-skip-permissions\n- Terminal escape sequence handling\n- Log file streaming to disk\n\n## Proposed Interface\n\n```typescript\nabstract class BaseWrapper extends EventEmitter {\n // Common state\n protected config: WrapperConfig;\n protected client: RelayClient;\n protected running = false;\n protected messageQueue: QueuedMessage[] = [];\n protected isInjecting = false;\n protected readyForMessages = false;\n protected sentMessageHashes = new Set\u003cstring\u003e();\n protected processedSpawnCommands = new Set\u003cstring\u003e();\n protected processedReleaseCommands = new Set\u003cstring\u003e();\n protected relayPrefix: string;\n protected lastOutputTime = 0;\n protected injectionMetrics = {...};\n\n // Abstract methods (subclasses must implement)\n abstract start(): Promise\u003cvoid\u003e;\n abstract stop(): void;\n protected abstract performInjection(message: string): Promise\u003cvoid\u003e;\n protected abstract verifyInjection(shortId: string, from: string): Promise\u003cboolean\u003e;\n protected abstract getRawOutput(): string;\n protected abstract isAgentAlive(): boolean;\n\n // Common implementations\n protected stripAnsi(str: string): string;\n protected sleep(ms: number): Promise\u003cvoid\u003e;\n protected buildInjectionString(msg: QueuedMessage): string;\n protected sendRelayCommand(cmd: ParsedCommand): void;\n protected handleIncomingMessage(...): void;\n protected async processMessageQueue(): Promise\u003cvoid\u003e;\n protected async injectWithRetry(...): Promise\u003cInjectionResult\u003e;\n protected async waitForOutputStable(): Promise\u003cboolean\u003e;\n protected parseSpawnReleaseCommands(content: string): void;\n\n // Common getters\n get isRunning(): boolean;\n get name(): string;\n getInjectionMetrics(): InjectionMetrics;\n get pendingMessageCount(): number;\n}\n```\n\n## Implementation Plan\n\n### Phase 1: Create base class (src/wrapper/base-wrapper.ts)\n1. Create BaseWrapper abstract class with EventEmitter extension\n2. Move common types: QueuedMessage, InjectionResult, InjectionMetrics\n3. Implement common methods: stripAnsi, sleep, buildInjectionString\n4. Implement message queue processing with injection retry\n5. Implement spawn/release parsing\n\n### Phase 2: Refactor PtyWrapper to extend BaseWrapper\n1. Remove duplicated code\n2. Implement abstract methods: performInjection, verifyInjection, getRawOutput\n3. Keep PTY-specific code: node-pty integration, log files, auto-accept\n\n### Phase 3: Refactor TmuxWrapper to extend BaseWrapper\n1. Add EventEmitter extension (via base class)\n2. Remove duplicated code\n3. Implement abstract methods\n4. Keep tmux-specific code: session management, attach, capture-pane\n\n### Phase 4: Port missing features\n1. Add injection metrics to TmuxWrapper (inherit from base)\n2. Add `injection-failed` event to TmuxWrapper\n\n### Phase 5: Add shared tests\n1. Create base-wrapper.test.ts with common behavior tests\n2. Test injection retry logic\n3. Test message queue processing\n\n## Files to Create/Modify\n- `src/wrapper/base-wrapper.ts` (NEW)\n- `src/wrapper/pty-wrapper.ts` (MODIFY - extend base)\n- `src/wrapper/tmux-wrapper.ts` (MODIFY - extend base)\n- `src/wrapper/types.ts` (NEW - shared types)\n- `src/wrapper/base-wrapper.test.ts` (NEW)\n\n## Success Criteria\n- Both wrappers extend BaseWrapper\n- All common code lives in base class\n- Adding injection improvements updates both wrappers\n- Injection metrics available in both wrappers\n- No breaking changes to external API\n- All existing tests pass","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-02T15:00:00Z","updated_at":"2026-01-02T15:00:00Z"} +{"id":"agent-relay-v9rc","title":"Fix light/dark mode toggle in settings","description":"The light mode / dark mode switch from the settings panel doesn't work","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-28T22:05:41.332721-05:00","updated_at":"2025-12-28T22:08:21.904724-05:00","closed_at":"2025-12-28T22:08:21.904724-05:00","close_reason":"Fixed by Frontend - added light theme CSS variables and useEffect for data-theme attribute"} +{"id":"agent-relay-vxc","title":"Navigation consistency between bridge and project views","description":"Ensure navigation patterns are consistent when switching between bridge and project dashboard views","status":"closed","priority":2,"issue_type":"task","assignee":"FE-Dev","created_at":"2025-12-24T11:47:46.62539+01:00","updated_at":"2025-12-24T12:00:15.805473+01:00","closed_at":"2025-12-24T12:00:15.805473+01:00"} +{"id":"agent-relay-wrap1","title":"[Arch] Extract BaseWrapper abstract class for TmuxWrapper and PtyWrapper","description":"## Summary\nExtract common functionality between TmuxWrapper and PtyWrapper into an abstract base class to:\n1. Ensure feature parity between the two implementations\n2. Make adding new features easier (add once in base, implement specifics in subclasses)\n3. Prevent divergence over time\n4. Reduce code duplication\n\n## Current State Analysis\n\n### Common Code (should be in base class):\n- `RelayClient` initialization and lifecycle\n- `messageQueue` management (same structure in both)\n- `isInjecting` state flag\n- `sentMessageHashes: Set\u003cstring\u003e` for deduplication\n- `processedSpawnCommands` and `processedReleaseCommands` Sets\n- `stripAnsi()` method (nearly identical implementations)\n- `sleep()` helper (identical)\n- `sendRelayCommand()` method (similar pattern)\n- `handleIncomingMessage()` signature and queue push\n- Injection string format: `Relay message from ${from} [${shortId}]${hints}: ${body}`\n- Thread/importance/channel hint building\n- Spawn/release command parsing\n- `relayPrefix` handling\n\n### Diverged Code (TmuxWrapper has, PtyWrapper missing):\n- Storage integration (SQLite for summaries)\n- Activity state tracking (active/idle/disconnected)\n- Summary parsing (`[[SUMMARY]]` blocks)\n- Session end handling (`[[SESSION_END]]` blocks)\n- Sophisticated input clearing (cursor stability check)\n- Bracketed paste for safer injection\n- Continuation line joining for TUI output\n\n### Diverged Code (PtyWrapper has, TmuxWrapper missing):\n- EventEmitter extension (TmuxWrapper should also emit events)\n- `injection-failed` event emission\n- `getInjectionMetrics()` method\n- `pendingMessageCount` getter\n- Auto-accept prompts for Claude --dangerously-skip-permissions\n- Terminal escape sequence handling\n- Log file streaming to disk\n\n## Proposed Interface\n\n```typescript\nabstract class BaseWrapper extends EventEmitter {\n // Common state\n protected config: WrapperConfig;\n protected client: RelayClient;\n protected running = false;\n protected messageQueue: QueuedMessage[] = [];\n protected isInjecting = false;\n protected readyForMessages = false;\n protected sentMessageHashes = new Set\u003cstring\u003e();\n protected processedSpawnCommands = new Set\u003cstring\u003e();\n protected processedReleaseCommands = new Set\u003cstring\u003e();\n protected relayPrefix: string;\n protected lastOutputTime = 0;\n protected injectionMetrics = {...};\n\n // Abstract methods (subclasses must implement)\n abstract start(): Promise\u003cvoid\u003e;\n abstract stop(): void;\n protected abstract performInjection(message: string): Promise\u003cvoid\u003e;\n protected abstract verifyInjection(shortId: string, from: string): Promise\u003cboolean\u003e;\n protected abstract getRawOutput(): string;\n protected abstract isAgentAlive(): boolean;\n\n // Common implementations\n protected stripAnsi(str: string): string;\n protected sleep(ms: number): Promise\u003cvoid\u003e;\n protected buildInjectionString(msg: QueuedMessage): string;\n protected sendRelayCommand(cmd: ParsedCommand): void;\n protected handleIncomingMessage(...): void;\n protected async processMessageQueue(): Promise\u003cvoid\u003e;\n protected async injectWithRetry(...): Promise\u003cInjectionResult\u003e;\n protected async waitForOutputStable(): Promise\u003cboolean\u003e;\n protected parseSpawnReleaseCommands(content: string): void;\n\n // Common getters\n get isRunning(): boolean;\n get name(): string;\n getInjectionMetrics(): InjectionMetrics;\n get pendingMessageCount(): number;\n}\n```\n\n## Implementation Plan\n\n### Phase 1: Create base class (src/wrapper/base-wrapper.ts)\n1. Create BaseWrapper abstract class with EventEmitter extension\n2. Move common types: QueuedMessage, InjectionResult, InjectionMetrics\n3. Implement common methods: stripAnsi, sleep, buildInjectionString\n4. Implement message queue processing with injection retry\n5. Implement spawn/release parsing\n\n### Phase 2: Refactor PtyWrapper to extend BaseWrapper\n1. Remove duplicated code\n2. Implement abstract methods: performInjection, verifyInjection, getRawOutput\n3. Keep PTY-specific code: node-pty integration, log files, auto-accept\n\n### Phase 3: Refactor TmuxWrapper to extend BaseWrapper\n1. Add EventEmitter extension (via base class)\n2. Remove duplicated code\n3. Implement abstract methods\n4. Keep tmux-specific code: session management, attach, capture-pane\n\n### Phase 4: Port missing features\n1. Add injection metrics to TmuxWrapper (inherit from base)\n2. Add `injection-failed` event to TmuxWrapper\n\n### Phase 5: Add shared tests\n1. Create base-wrapper.test.ts with common behavior tests\n2. Test injection retry logic\n3. Test message queue processing\n\n## Files to Create/Modify\n- `src/wrapper/base-wrapper.ts` (NEW)\n- `src/wrapper/pty-wrapper.ts` (MODIFY - extend base)\n- `src/wrapper/tmux-wrapper.ts` (MODIFY - extend base)\n- `src/wrapper/types.ts` (NEW - shared types)\n- `src/wrapper/base-wrapper.test.ts` (NEW)\n\n## Success Criteria\n- Both wrappers extend BaseWrapper\n- All common code lives in base class\n- Adding injection improvements updates both wrappers\n- Injection metrics available in both wrappers\n- No breaking changes to external API\n- All existing tests pass","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T15:00:00Z","updated_at":"2026-01-06T14:07:38.447031+01:00","closed_at":"2026-01-06T13:58:30.606595+01:00"} {"id":"agent-relay-wsd","title":"URGENT: Gemini interprets relay messages as shell commands","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-12-22T14:23:05.989732+01:00","updated_at":"2025-12-22T14:24:18.714626+01:00","closed_at":"2025-12-22T14:24:18.714626+01:00"} {"id":"agent-relay-xn1","title":"Add remote access via Cloudflare tunnels","description":"Enable remote/mobile access to agent-relay dashboard and control. Use Cloudflare tunnels (like Maestro) for secure access without port forwarding. Include QR code generation for easy mobile connection. Major gap vs competitors - AI Maestro and Maestro both have this.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-23T17:04:28.788302+01:00","updated_at":"2025-12-23T17:04:28.788302+01:00"} {"id":"agent-relay-ylf","title":"Add knowledge graph for persistent memory","description":"Implement knowledge graph storage (like Mimir's Neo4j approach) for persistent cross-session context. Store entities, relationships, and semantic embeddings. Enable agents to learn from past sessions and share knowledge. Could use SQLite with FTS5 or optional Neo4j adapter.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-23T17:05:08.367875+01:00","updated_at":"2025-12-23T17:05:08.367875+01:00"} -{"id":"agent-relay-ytj","title":"Filter chat view to show only messages with selected agent","description":"When clicking on a particular agent in the dashboard sidebar, the chat view should filter to only show messages between Dashboard and that specific agent. This provides a focused conversation view instead of showing all messages.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-23T22:53:31.325805+01:00","updated_at":"2025-12-23T22:58:41.407339+01:00","closed_at":"2025-12-23T22:58:41.407339+01:00"} +{"id":"agent-relay-ytj","title":"Filter chat view to show only messages with selected agent","description":"When clicking on a particular agent in the dashboard sidebar, the chat view should filter to only show messages between Dashboard and that specific agent. This provides a focused conversation view instead of showing all messages.","status":"closed","priority":2,"issue_type":"feature","assignee":"FE-Dev","created_at":"2025-12-23T22:53:31.325805+01:00","updated_at":"2025-12-23T22:58:41.407339+01:00","closed_at":"2025-12-23T22:58:41.407339+01:00"} {"id":"agent-relay-ytk","title":"Simplify agent-relay CLI interface","description":"The CLI has too many commands. Consolidate into a clean, simple interface:\n\n1. `relay start` should also kick off the dashboard automatically\n2. Merge redundant team-* commands\n3. Remove rarely-used commands or make them subcommands\n4. Target: 5-7 top-level commands max\n\nCurrent commands to evaluate:\n- start, stop, status (keep)\n- wrap (keep)\n- project (keep or merge into status)\n- send (keep)\n- team-setup, team-status, team-send, team-check, team-listen, team-start (consolidate)\n- msg-read (make subcommand or integrate)\n- dashboard (merge into start)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T21:51:14.010849+01:00","updated_at":"2025-12-22T21:51:14.010849+01:00","closed_at":"2025-12-19T22:08:44.107992+01:00"} {"id":"agent-relay-ytl","title":"Add cross-project message syntax to parser","description":"Extend the parser (src/wrapper/parser.ts) to recognize @relay:project:agent syntax for cross-project messaging. Currently only @relay:agent is parsed. Need to add pattern matching for the project:agent format.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-21T10:45:00Z","updated_at":"2025-12-24T11:46:51.622328+01:00","closed_at":"2025-12-24T11:46:51.622328+01:00"} {"id":"agent-relay-ytm","title":"Enhance dashboard for multi-project bridge view","description":"Add a bridge view to the dashboard that shows all connected projects, their leads, workers, and cross-project message flow. Should be accessible at /bridge?projects=... See docs/DESIGN_BRIDGE_STAFFING.md for mockups.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-21T10:45:00Z","updated_at":"2025-12-22T22:04:18.511591+01:00","closed_at":"2025-12-22T22:04:18.511591+01:00"} -{"id":"agent-relay-ytn","title":"Shadow agent pairing: CLI support","description":"Add --shadow and --shadow-role CLI flags to spawn paired agents. Example: `agent-relay -n Lead --shadow Auditor claude` spawns Lead with an Auditor shadow that observes and provides feedback. The shadow receives all messages the primary sends/receives.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T15:31:48.127919-05:00","closed_at":"2025-12-29T15:31:10.671423-05:00"} -{"id":"agent-relay-yto","title":"Shadow agent pairing: Config file support","description":"Support shadow agent configuration in .agent-relay.json. Define pairs (primary-\u003eshadow mappings) and roles (prompt templates like performance-auditor, integration-tester). Config format: { pairs: { Lead: { shadow: 'Auditor', shadowRole: 'performance-auditor' } }, roles: { 'performance-auditor': { prompt: '...', speakOn: ['SESSION_END'] } } }","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T15:53:54.085077-05:00","closed_at":"2025-12-29T15:35:56.100259-05:00","dependencies":[{"issue_id":"agent-relay-yto","depends_on_id":"agent-relay-ytn","type":"blocks","created_at":"2025-12-25T08:00:00Z","created_by":"daemon"}]} -{"id":"agent-relay-ytp","title":"Shadow agent pairing: spawnWithShadow() API","description":"Programmatic API for spawning paired agents. Example: `const { primary, shadow } = await spawnWithShadow({ primary: { name: 'Lead', command: 'claude' }, shadow: { name: 'Auditor', role: 'performance-auditor', speakOn: ['SESSION_END'] } })`. Returns handles to both agents for orchestration.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T16:11:27.099022-05:00","closed_at":"2025-12-29T15:53:36.751585-05:00","dependencies":[{"issue_id":"agent-relay-ytp","depends_on_id":"agent-relay-ytn","type":"blocks","created_at":"2025-12-25T08:00:00Z","created_by":"daemon"}]} -{"id":"agent-relay-ytq","title":"Shadow agent pairing: Built-in roles","description":"Ship built-in shadow roles: (1) performance-auditor - observes silently, provides feedback at session end; (2) integration-tester - writes tests as primary writes code; (3) code-reviewer - reviews changes before commit; (4) security-auditor - flags security issues in real-time. Roles define prompts and speakOn triggers.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-25T08:00:00Z","dependencies":[{"issue_id":"agent-relay-ytq","depends_on_id":"agent-relay-yto","type":"blocks","created_at":"2025-12-25T08:00:00Z","created_by":"daemon"}]} -{"id":"agent-relay-ytr","title":"Shadow agent pairing: Message routing","description":"Implement shadow message routing: shadows receive copies of all messages their primary sends/receives. Add 'shadow' subscription type in router. Shadows can be configured to speakOn specific triggers (SESSION_END, CODE_WRITTEN, REVIEW_REQUEST) or stay silent until asked.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T01:52:36.155128-05:00","closed_at":"2025-12-29T06:22:44.996313457Z"} +{"id":"agent-relay-ytn","title":"Shadow agent pairing: CLI support","description":"Add --shadow and --shadow-role CLI flags to spawn paired agents. Example: `agent-relay -n Lead --shadow Auditor claude` spawns Lead with an Auditor shadow that observes and provides feedback. The shadow receives all messages the primary sends/receives.","status":"closed","priority":1,"issue_type":"feature","assignee":"Backend","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T15:31:48.127919-05:00","closed_at":"2025-12-29T15:31:10.671423-05:00","labels":["shadow-agents"]} +{"id":"agent-relay-yto","title":"Shadow agent pairing: Config file support","description":"Support shadow agent configuration in .agent-relay.json. Define pairs (primary-\u003eshadow mappings) and roles (prompt templates like performance-auditor, integration-tester). Config format: { pairs: { Lead: { shadow: 'Auditor', shadowRole: 'performance-auditor' } }, roles: { 'performance-auditor': { prompt: '...', speakOn: ['SESSION_END'] } } }","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T15:53:54.085077-05:00","closed_at":"2025-12-29T15:35:56.100259-05:00","labels":["shadow-agents"],"dependencies":[{"issue_id":"agent-relay-yto","depends_on_id":"agent-relay-ytn","type":"blocks","created_at":"2025-12-25T08:00:00Z","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-ytp","title":"Shadow agent pairing: spawnWithShadow() API","description":"Programmatic API for spawning paired agents. Example: `const { primary, shadow } = await spawnWithShadow({ primary: { name: 'Lead', command: 'claude' }, shadow: { name: 'Auditor', role: 'performance-auditor', speakOn: ['SESSION_END'] } })`. Returns handles to both agents for orchestration.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T16:11:27.099022-05:00","closed_at":"2025-12-29T15:53:36.751585-05:00","labels":["shadow-agents"],"dependencies":[{"issue_id":"agent-relay-ytp","depends_on_id":"agent-relay-ytn","type":"blocks","created_at":"2025-12-25T08:00:00Z","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-ytq","title":"Shadow agent pairing: Built-in roles","description":"Ship built-in shadow roles: (1) performance-auditor - observes silently, provides feedback at session end; (2) integration-tester - writes tests as primary writes code; (3) code-reviewer - reviews changes before commit; (4) security-auditor - flags security issues in real-time. Roles define prompts and speakOn triggers.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-25T08:00:00Z","labels":["shadow-agents"],"dependencies":[{"issue_id":"agent-relay-ytq","depends_on_id":"agent-relay-yto","type":"blocks","created_at":"2025-12-25T08:00:00Z","created_by":"daemon","metadata":"{}"}]} +{"id":"agent-relay-ytr","title":"Shadow agent pairing: Message routing","description":"Implement shadow message routing: shadows receive copies of all messages their primary sends/receives. Add 'shadow' subscription type in router. Shadows can be configured to speakOn specific triggers (SESSION_END, CODE_WRITTEN, REVIEW_REQUEST) or stay silent until asked.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-25T08:00:00Z","updated_at":"2025-12-29T01:52:36.155128-05:00","closed_at":"2025-12-29T06:22:44.996313457Z","labels":["shadow-agents"]} {"id":"agent-relay-yts","title":"Wire up spawn/release command handling in lead mode","description":"Lead agents can output @relay:spawn and @relay:release commands, but the handler needs to be wired up to actually call the AgentSpawner. The spawner exists in src/bridge/spawner.ts, need to intercept these commands in the TmuxWrapper or add a message handler.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-21T10:45:00Z","updated_at":"2025-12-24T15:09:22.953849+01:00","closed_at":"2025-12-24T15:09:22.953849+01:00"} {"id":"agent-relay-ytt","title":"Implement threaded conversations (Slack-style)","description":"Add thread replies with parent_message_id tracking in storage, nested thread UI display in dashboard, thread notification badges, and expand/collapse thread views. Backend: add parent_message_id column, thread reply routing. Frontend: inline thread expansion, reply count badges, thread-specific compose.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-23T12:00:00Z","updated_at":"2025-12-23T16:37:22.987643+01:00","closed_at":"2025-12-23T16:37:22.987643+01:00"} {"id":"agent-relay-ytu","title":"[Control Plane] Design Control API (REST + WebSocket)","description":"Design API for human control plane: POST /tasks, GET /agents, POST /agents/:id/msg, WS /stream. OpenAPI spec. Authentication endpoints. See ai-maestro's manager/worker pattern for reference.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T14:00:00Z","updated_at":"2025-12-21T14:00:00Z"} {"id":"agent-relay-ytv","title":"[Control Plane] Implement Lead Agent orchestration","description":"Lead Agent receives human intent, breaks into tasks, assigns to specialized agents based on skills, monitors progress, escalates decisions. Can be AI-powered or rule-based initially.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T14:00:00Z","updated_at":"2025-12-21T14:00:00Z"} -{"id":"agent-relay-ytw","title":"[Control Plane] Build web dashboard v2 (fleet control)","description":"Next.js dashboard with: fleet overview, task assignment UI, agent status cards, trajectory viewer, decision queue. Learn from ai-maestro's hierarchical naming and color coding.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-12-21T14:00:00Z","updated_at":"2026-01-02T20:34:28.24946+01:00"} +{"id":"agent-relay-ytw","title":"[Control Plane] Build web dashboard v2 (fleet control)","description":"Next.js dashboard with: fleet overview, task assignment UI, agent status cards, trajectory viewer, decision queue. Learn from ai-maestro's hierarchical naming and color coding.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Frontend","created_at":"2025-12-21T14:00:00Z","updated_at":"2026-01-02T20:34:28.24946+01:00"} {"id":"agent-relay-ytx","title":"[Control Plane] Human authentication (OAuth/magic link)","description":"Secure human access to control plane. OAuth2 (GitHub/Google) or magic link email auth. JWT tokens for API access. Role-based permissions (admin/operator/viewer).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-21T14:00:00Z","updated_at":"2025-12-21T14:00:00Z"} {"id":"agent-relay-yty","title":"[Control Plane] Push notification service","description":"Real-time notifications to mobile/web: APNs for iOS, FCM for Android/web. Notify on: task completion, agent questions, escalations, errors.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-21T14:00:00Z","updated_at":"2025-12-21T14:00:00Z"} {"id":"agent-relay-ytz","title":"[Control Plane] iPhone app MVP","description":"React Native or Swift app: fleet status view, task list, agent messaging, push notifications, decision queue with quick actions. Minimal viable version.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-21T14:00:00Z","updated_at":"2025-12-21T14:00:00Z"} diff --git a/.trajectories/completed/2026-01/traj_xy9vifpqet80.json b/.trajectories/completed/2026-01/traj_xy9vifpqet80.json new file mode 100644 index 00000000..02dd22ee --- /dev/null +++ b/.trajectories/completed/2026-01/traj_xy9vifpqet80.json @@ -0,0 +1,65 @@ +{ + "id": "traj_xy9vifpqet80", + "version": 1, + "task": { + "title": "Extract BaseWrapper from PtyWrapper and TmuxWrapper", + "source": { + "system": "plain", + "id": "agent-relay-wrap1" + } + }, + "status": "completed", + "startedAt": "2026-01-06T12:27:13.004Z", + "agents": [ + { + "name": "khaliqgant", + "role": "lead", + "joinedAt": "2026-01-06T12:27:13.007Z" + } + ], + "chapters": [ + { + "id": "chap_8t7vbaqwiz3f", + "title": "Work", + "agentName": "default", + "startedAt": "2026-01-06T12:27:45.698Z", + "events": [ + { + "ts": 1767702465699, + "type": "decision", + "content": "Using abstract class with protected methods for shared implementation: Using abstract class with protected methods for shared implementation", + "raw": { + "question": "Using abstract class with protected methods for shared implementation", + "chosen": "Using abstract class with protected methods for shared implementation", + "alternatives": [], + "reasoning": "Allows subclasses to override while providing default behavior" + }, + "significance": "high" + }, + { + "ts": 1767703025592, + "type": "decision", + "content": "Created BaseWrapper abstract class with comprehensive tests: Created BaseWrapper abstract class with comprehensive tests", + "raw": { + "question": "Created BaseWrapper abstract class with comprehensive tests", + "chosen": "Created BaseWrapper abstract class with comprehensive tests", + "alternatives": [], + "reasoning": "Extracted shared functionality from PtyWrapper/TmuxWrapper into base class. Tests cover message queue, spawn/release, continuity, and relay command handling." + }, + "significance": "high" + } + ], + "endedAt": "2026-01-06T12:58:24.003Z" + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", + "tags": [], + "completedAt": "2026-01-06T12:58:24.003Z", + "retrospective": { + "summary": "Extracted BaseWrapper abstract class from PtyWrapper and TmuxWrapper using TDD approach. Created 29 new tests, removed ~900 lines of duplicate code, both wrappers now inherit shared functionality.", + "approach": "Standard approach", + "confidence": 0.9 + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-01/traj_xy9vifpqet80.md b/.trajectories/completed/2026-01/traj_xy9vifpqet80.md new file mode 100644 index 00000000..10c57ebe --- /dev/null +++ b/.trajectories/completed/2026-01/traj_xy9vifpqet80.md @@ -0,0 +1,37 @@ +# Trajectory: Extract BaseWrapper from PtyWrapper and TmuxWrapper + +> **Status:** ✅ Completed +> **Task:** agent-relay-wrap1 +> **Confidence:** 90% +> **Started:** January 6, 2026 at 01:27 PM +> **Completed:** January 6, 2026 at 01:58 PM + +--- + +## Summary + +Extracted BaseWrapper abstract class from PtyWrapper and TmuxWrapper using TDD approach. Created 29 new tests, removed ~900 lines of duplicate code, both wrappers now inherit shared functionality. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Using abstract class with protected methods for shared implementation +- **Chose:** Using abstract class with protected methods for shared implementation +- **Reasoning:** Allows subclasses to override while providing default behavior + +### Created BaseWrapper abstract class with comprehensive tests +- **Chose:** Created BaseWrapper abstract class with comprehensive tests +- **Reasoning:** Extracted shared functionality from PtyWrapper/TmuxWrapper into base class. Tests cover message queue, spawn/release, continuity, and relay command handling. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Using abstract class with protected methods for shared implementation: Using abstract class with protected methods for shared implementation +- Created BaseWrapper abstract class with comprehensive tests: Created BaseWrapper abstract class with comprehensive tests diff --git a/.trajectories/index.json b/.trajectories/index.json index 8a3cd8d4..f71b20ad 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-01-06T08:25:20.797Z", + "lastUpdated": "2026-01-06T12:58:24.029Z", "trajectories": { "traj_ozd98si6a7ns": { "title": "Fix thinking indicator showing on all messages", @@ -344,6 +344,13 @@ "startedAt": "2026-01-06T08:24:36.222Z", "completedAt": "2026-01-06T08:25:20.777Z", "path": "/home/user/relay/.trajectories/completed/2026-01/traj_ui9b4tqxoa7j.json" + }, + "traj_xy9vifpqet80": { + "title": "Extract BaseWrapper from PtyWrapper and TmuxWrapper", + "status": "completed", + "startedAt": "2026-01-06T12:27:13.004Z", + "completedAt": "2026-01-06T12:58:24.003Z", + "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_xy9vifpqet80.json" } } } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index ca3c6ce8..652db41c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -525,3 +525,31 @@ The dashboard automatically tracks: Agents can view their status at the dashboard URL provided at startup. + +# Git Workflow Rules + +## NEVER Push Directly to Main + +**CRITICAL: Agents must NEVER push directly to the main branch.** + +- Always work on a feature branch +- Commit and push to the feature branch only +- Let the user decide when to merge to main +- Do not merge to main without explicit user approval + +```bash +# CORRECT workflow +git checkout -b feature/my-feature +# ... do work ... +git add . +git commit -m "My changes" +git push origin feature/my-feature +# STOP HERE - let user merge + +# WRONG - never do this +git checkout main +git merge feature/my-feature +git push origin main # NO! +``` + +This ensures the user maintains control over what goes into the main branch. diff --git a/src/cloud/api/onboarding.ts b/src/cloud/api/onboarding.ts index 6e6d6435..92287103 100644 --- a/src/cloud/api/onboarding.ts +++ b/src/cloud/api/onboarding.ts @@ -343,30 +343,67 @@ onboardingRouter.post('/cli/:provider/complete/:sessionId', async (req: Request, session.status = 'success'; } - // Fetch credentials from workspace + // Fetch credentials from workspace with retry + // Credentials may not be immediately available after OAuth completes if (!accessToken) { - try { - const credsResponse = await fetch( - `${session.workspaceUrl}/auth/cli/${provider}/creds/${session.workspaceSessionId}` - ); - if (credsResponse.ok) { - const creds = await credsResponse.json() as { - token?: string; - refreshToken?: string; - expiresAt?: string; + const MAX_CREDS_RETRIES = 5; + const CREDS_RETRY_DELAY = 1000; // 1 second between retries + + for (let attempt = 1; attempt <= MAX_CREDS_RETRIES; attempt++) { + try { + console.log(`[onboarding] Fetching credentials from workspace (attempt ${attempt}/${MAX_CREDS_RETRIES})`); + const credsResponse = await fetch( + `${session.workspaceUrl}/auth/cli/${provider}/creds/${session.workspaceSessionId}` + ); + + if (credsResponse.ok) { + const creds = await credsResponse.json() as { + token?: string; + refreshToken?: string; + tokenExpiresAt?: string; + }; + accessToken = creds.token; + refreshToken = creds.refreshToken; + if (creds.tokenExpiresAt) { + tokenExpiresAt = new Date(creds.tokenExpiresAt); + } + console.log('[onboarding] Fetched credentials from workspace:', { + hasToken: !!accessToken, + hasRefreshToken: !!refreshToken, + attempt, + }); + break; // Success, exit retry loop + } + + // Check if it's an error state (not just "not ready yet") + const errorBody = await credsResponse.json().catch(() => ({})) as { + status?: string; + error?: string; + errorHint?: string; + recoverable?: boolean; }; - accessToken = creds.token; - refreshToken = creds.refreshToken; - if (creds.expiresAt) { - tokenExpiresAt = new Date(creds.expiresAt); + + if (errorBody.status === 'error') { + // Auth failed, don't retry + console.error('[onboarding] Auth failed in workspace:', errorBody); + return res.status(400).json({ + error: errorBody.error || 'Authentication failed', + errorHint: errorBody.errorHint, + recoverable: errorBody.recoverable, + }); + } + + // If not ready yet and we have more retries, wait and try again + if (attempt < MAX_CREDS_RETRIES) { + console.log(`[onboarding] Credentials not ready yet, retrying in ${CREDS_RETRY_DELAY}ms...`); + await new Promise(resolve => setTimeout(resolve, CREDS_RETRY_DELAY)); + } + } catch (err) { + console.error(`[onboarding] Failed to get credentials from workspace (attempt ${attempt}):`, err); + if (attempt < MAX_CREDS_RETRIES) { + await new Promise(resolve => setTimeout(resolve, CREDS_RETRY_DELAY)); } - console.log('[onboarding] Fetched credentials from workspace:', { - hasToken: !!accessToken, - hasRefreshToken: !!refreshToken, - }); } - } catch (err) { - console.error('[onboarding] Failed to get credentials from workspace:', err); } } } diff --git a/src/cloud/api/workspaces.ts b/src/cloud/api/workspaces.ts index 3a409d07..6f4d78e2 100644 --- a/src/cloud/api/workspaces.ts +++ b/src/cloud/api/workspaces.ts @@ -61,6 +61,247 @@ function _invalidateCachedAccess(userId: string, workspaceId?: string): void { } } +// ============================================================================ +// GitHub Repos Cache (for /accessible endpoint) +// ============================================================================ + +interface CachedRepo { + fullName: string; + permissions: { admin: boolean; push: boolean; pull: boolean }; +} + +interface CachedUserRepos { + repositories: CachedRepo[]; + cachedAt: number; + isComplete: boolean; // Whether we've fetched all pages + refreshInProgress: boolean; +} + +// Cache keyed by nangoConnectionId +const userReposCache = new Map(); +const USER_REPOS_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes - hard expiry +const STALE_WHILE_REVALIDATE_MS = 5 * 60 * 1000; // Trigger background refresh after 5 minutes +const MAX_CACHE_ENTRIES = 500; // Prevent unbounded growth + +/** + * Evict oldest cache entries if we exceed the limit + */ +function evictOldestCacheEntries(): void { + if (userReposCache.size <= MAX_CACHE_ENTRIES) return; + + // Convert to array, sort by cachedAt (oldest first), delete oldest entries + const entries = Array.from(userReposCache.entries()) + .sort((a, b) => a[1].cachedAt - b[1].cachedAt); + + const toEvict = entries.slice(0, entries.length - MAX_CACHE_ENTRIES); + for (const [key] of toEvict) { + console.log(`[repos-cache] Evicting oldest cache entry: ${key.substring(0, 8)}`); + userReposCache.delete(key); + } +} + +/** + * Background refresh function that paginates through ALL user repos + */ +async function refreshUserReposInBackground(nangoConnectionId: string): Promise { + const cached = userReposCache.get(nangoConnectionId); + + // Don't start if refresh already in progress + if (cached?.refreshInProgress) { + console.log(`[repos-cache] Background refresh already in progress for ${nangoConnectionId.substring(0, 8)}`); + return; + } + + // Mark as refreshing + if (cached) { + cached.refreshInProgress = true; + } else { + // Create placeholder entry + userReposCache.set(nangoConnectionId, { + repositories: [], + cachedAt: Date.now(), + isComplete: false, + refreshInProgress: true, + }); + } + + console.log(`[repos-cache] Starting background refresh for ${nangoConnectionId.substring(0, 8)}`); + + try { + const allRepos: CachedRepo[] = []; + let page = 1; + let hasMore = true; + const MAX_PAGES = 20; // Safety limit: 20 pages * 100 repos = 2000 repos max + + while (hasMore && page <= MAX_PAGES) { + const result = await nangoService.listUserAccessibleRepos(nangoConnectionId, { + perPage: 100, + page, + type: 'all', + }); + + allRepos.push(...result.repositories.map(r => ({ + fullName: r.fullName, + permissions: r.permissions, + }))); + + hasMore = result.hasMore; + page++; + + // Small delay between pages to avoid rate limiting + if (hasMore) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + console.log(`[repos-cache] Background refresh complete for ${nangoConnectionId.substring(0, 8)}: ${allRepos.length} repos across ${page - 1} pages`); + + userReposCache.set(nangoConnectionId, { + repositories: allRepos, + cachedAt: Date.now(), + isComplete: true, + refreshInProgress: false, + }); + evictOldestCacheEntries(); + } catch (err) { + console.error(`[repos-cache] Background refresh failed for ${nangoConnectionId.substring(0, 8)}:`, err); + + // Mark refresh as done even on error, keep existing data if any + const existing = userReposCache.get(nangoConnectionId); + if (existing) { + existing.refreshInProgress = false; + } + } +} + +/** + * Get cached user repos, triggering background refresh if stale + * Returns null if no cache exists (caller should fetch first page synchronously) + */ +function getCachedUserRepos(nangoConnectionId: string): CachedUserRepos | null { + const cached = userReposCache.get(nangoConnectionId); + if (!cached) return null; + + const age = Date.now() - cached.cachedAt; + + // If expired, delete and return null + if (age > USER_REPOS_CACHE_TTL_MS) { + console.log(`[repos-cache] Cache expired for ${nangoConnectionId.substring(0, 8)}`); + userReposCache.delete(nangoConnectionId); + return null; + } + + // If stale but valid, trigger background refresh + if (age > STALE_WHILE_REVALIDATE_MS && !cached.refreshInProgress) { + console.log(`[repos-cache] Cache stale for ${nangoConnectionId.substring(0, 8)}, triggering background refresh`); + // Fire and forget - don't await + refreshUserReposInBackground(nangoConnectionId).catch(() => {}); + } + + return cached; +} + +// Track in-flight initializations to prevent duplicate API calls +const initializingConnections = new Set(); + +/** + * Initialize cache with first page and trigger background refresh for rest + * Returns the first page of repos immediately + */ +async function initializeUserReposCache(nangoConnectionId: string): Promise { + // Check if another request is already initializing this connection + if (initializingConnections.has(nangoConnectionId)) { + console.log(`[repos-cache] Another request is initializing ${nangoConnectionId.substring(0, 8)}, waiting...`); + // Wait a bit and check cache again + await new Promise(resolve => setTimeout(resolve, 500)); + const cached = userReposCache.get(nangoConnectionId); + if (cached) { + return cached.repositories; + } + // Still no cache, fall through to initialize (previous request may have failed) + } + + initializingConnections.add(nangoConnectionId); + + try { + // Fetch first page synchronously + const firstPage = await nangoService.listUserAccessibleRepos(nangoConnectionId, { + perPage: 100, + page: 1, + type: 'all', + }); + + const repos = firstPage.repositories.map(r => ({ + fullName: r.fullName, + permissions: r.permissions, + })); + + // Store first page immediately + userReposCache.set(nangoConnectionId, { + repositories: repos, + cachedAt: Date.now(), + isComplete: !firstPage.hasMore, + refreshInProgress: firstPage.hasMore, // Will be refreshing if there's more + }); + evictOldestCacheEntries(); + + // If there are more pages, trigger background refresh to get the rest + if (firstPage.hasMore) { + console.log(`[repos-cache] First page has ${repos.length} repos, more available - triggering background pagination`); + // Fire and forget - reuse the shared background refresh function + // But start from page 2 with the existing repos + (async () => { + try { + const allRepos = [...repos]; + let page = 2; + let hasMore = true; + const MAX_PAGES = 20; + + while (hasMore && page <= MAX_PAGES) { + const result = await nangoService.listUserAccessibleRepos(nangoConnectionId, { + perPage: 100, + page, + type: 'all', + }); + + allRepos.push(...result.repositories.map(r => ({ + fullName: r.fullName, + permissions: r.permissions, + }))); + + hasMore = result.hasMore; + page++; + + if (hasMore) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + console.log(`[repos-cache] Background pagination complete: ${allRepos.length} total repos`); + + userReposCache.set(nangoConnectionId, { + repositories: allRepos, + cachedAt: Date.now(), + isComplete: true, + refreshInProgress: false, + }); + evictOldestCacheEntries(); + } catch (err) { + console.error('[repos-cache] Background pagination failed:', err); + const existing = userReposCache.get(nangoConnectionId); + if (existing) { + existing.refreshInProgress = false; + } + } + })(); + } + + return repos; + } finally { + initializingConnections.delete(nangoConnectionId); + } +} + // ============================================================================ // Workspace Access Middleware // ============================================================================ @@ -423,9 +664,12 @@ workspacesRouter.get('/accessible', async (req: Request, res: Response) => { // 1. Get owned workspaces const ownedWorkspaces = await db.workspaces.findByUserId(userId); - // 2. Get workspaces where user is a member + // 2. Get workspaces where user is a member (excluding owned ones to prevent duplicates) + const ownedWorkspaceIds = new Set(ownedWorkspaces.map((w) => w.id)); const memberships = await db.workspaceMembers.findByUserId(userId); - const memberWorkspaceIds = memberships.map((m) => m.workspaceId); + const memberWorkspaceIds = memberships + .map((m) => m.workspaceId) + .filter((wsId) => !ownedWorkspaceIds.has(wsId)); // Exclude owned workspaces // Fetch member workspaces const memberWorkspaces: Workspace[] = []; @@ -435,28 +679,38 @@ workspacesRouter.get('/accessible', async (req: Request, res: Response) => { } // 3. Get workspaces via GitHub repo access (if user has Nango connection) + // Uses background caching to handle users with many repos (>100) const contributorWorkspaces: Array = []; + let cacheStatus: 'hit' | 'miss' | 'initializing' = 'miss'; if (user.nangoConnectionId) { - // Get all repos user has access to via GitHub try { console.log(`[workspaces/accessible] Checking GitHub repo access for user ${userId.substring(0, 8)} with nangoConnectionId ${user.nangoConnectionId.substring(0, 8)}...`); - const reposResult = await nangoService.listUserAccessibleRepos(user.nangoConnectionId, { - perPage: 100, - type: 'all', - }); - - console.log(`[workspaces/accessible] User has access to ${reposResult.repositories.length} GitHub repos`); + // Try to get cached repos first + let userRepos: CachedRepo[]; + const cached = getCachedUserRepos(user.nangoConnectionId); + + if (cached) { + userRepos = cached.repositories; + cacheStatus = 'hit'; + console.log(`[workspaces/accessible] Cache ${cached.isComplete ? 'hit (complete)' : 'hit (partial)'}: ${userRepos.length} repos`); + } else { + // No cache - initialize with first page and trigger background refresh + userRepos = await initializeUserReposCache(user.nangoConnectionId); + cacheStatus = 'initializing'; + console.log(`[workspaces/accessible] Cache miss - initialized with ${userRepos.length} repos (background refresh may add more)`); + } // Get workspaces that aren't owned or membered + // Reuse ownedWorkspaceIds and add member workspace IDs const knownWorkspaceIds = new Set([ - ...ownedWorkspaces.map((w) => w.id), + ...ownedWorkspaceIds, ...memberWorkspaceIds, ]); // Get all repo full names from user's accessible repos - for (const repo of reposResult.repositories) { + for (const repo of userRepos) { // Find repos in our DB that match this full name (case-insensitive) const dbRepos = await db.repositories.findByGithubFullName(repo.fullName); @@ -485,7 +739,7 @@ workspacesRouter.get('/accessible', async (req: Request, res: Response) => { } } - console.log(`[workspaces/accessible] Found ${contributorWorkspaces.length} contributor workspaces`); + console.log(`[workspaces/accessible] Found ${contributorWorkspaces.length} contributor workspaces (cache: ${cacheStatus})`); } catch (err) { console.warn('[workspaces/accessible] Failed to check GitHub repo access:', err); // Continue without contributor workspaces @@ -1100,8 +1354,9 @@ workspacesRouter.all('/:id/proxy/{*proxyPath}', async (req: Request, res: Respon (req as any)._proxyTargetUrl = targetUrl; // Add timeout to prevent hanging requests + // 45s timeout to accommodate Fly.io machine cold starts (can take 20-30s) const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 15000); // 15s timeout + const timeout = setTimeout(() => controller.abort(), 45000); const fetchOptions: RequestInit = { method: req.method, @@ -1141,7 +1396,7 @@ workspacesRouter.all('/:id/proxy/{*proxyPath}', async (req: Request, res: Respon if (error instanceof Error && error.name === 'AbortError') { res.status(504).json({ error: 'Workspace request timed out', - details: 'The workspace did not respond within 15 seconds', + details: 'The workspace did not respond within 45 seconds', targetUrl: targetUrl, }); return; diff --git a/src/daemon/api.ts b/src/daemon/api.ts index d892e3f5..9687daff 100644 --- a/src/daemon/api.ts +++ b/src/daemon/api.ts @@ -371,13 +371,36 @@ export class DaemonApi extends EventEmitter { if (!session) { return { status: 404, body: { error: 'Session not found' } }; } - if (session.status !== 'success') { - return { status: 400, body: { error: 'Auth not complete', status: session.status } }; + // Check for error state first + if (session.status === 'error') { + return { + status: 400, + body: { + error: session.error || 'Authentication failed', + errorHint: session.errorHint, + recoverable: session.recoverable, + status: session.status, + }, + }; + } + // Check if auth is complete AND we have credentials + // Status can be 'success' before credentials are extracted (race condition) + if (session.status !== 'success' || !session.token) { + return { + status: 400, + body: { + error: 'Auth not complete or credentials not yet available', + status: session.status, + hasToken: !!session.token, + }, + }; } return { status: 200, body: { token: session.token, + refreshToken: session.refreshToken, + tokenExpiresAt: session.tokenExpiresAt, provider: session.provider, }, }; diff --git a/src/daemon/cli-auth.ts b/src/daemon/cli-auth.ts index 170ca9d3..d67d29cd 100644 --- a/src/daemon/cli-auth.ts +++ b/src/daemon/cli-auth.ts @@ -199,28 +199,47 @@ export async function startCLIAuth( proc.onData((data: string) => { session.output += data; + const cleanText = stripAnsiCodes(data); - // Handle prompts - const matchingPrompt = findMatchingPrompt(data, config.prompts, respondedPrompts); - if (matchingPrompt) { - respondedPrompts.add(matchingPrompt.description); - session.promptsHandled.push(matchingPrompt.description); - logger.info('Auto-responding to prompt', { description: matchingPrompt.description }); + // Check for error patterns FIRST - if error detected, don't auto-respond to prompts + // This prevents us from auto-responding to "Press Enter to retry" in error messages + const matchedError = findMatchingError(data, config.errorPatterns); + if (matchedError && session.status !== 'error') { + logger.warn('Auth error detected', { + provider, + sessionId, + errorMessage: matchedError.message, + recoverable: matchedError.recoverable, + }); + session.status = 'error'; + session.error = matchedError.message; + session.errorHint = matchedError.hint; + session.recoverable = matchedError.recoverable; + } - const delay = matchingPrompt.delay ?? 100; - setTimeout(() => { - try { - proc.write(matchingPrompt.response); - } catch { - // Process may have exited - } - }, delay); + // Don't auto-respond to prompts if we're in error state + // This prevents responding to "Press Enter to retry" after an error + if (session.status !== 'error') { + const matchingPrompt = findMatchingPrompt(data, config.prompts, respondedPrompts); + if (matchingPrompt) { + respondedPrompts.add(matchingPrompt.description); + session.promptsHandled.push(matchingPrompt.description); + logger.info('Auto-responding to prompt', { description: matchingPrompt.description }); + + const delay = matchingPrompt.delay ?? 100; + setTimeout(() => { + try { + proc.write(matchingPrompt.response); + } catch { + // Process may have exited + } + }, delay); + } } - // Extract auth URL - const cleanText = stripAnsiCodes(data); + // Extract auth URL (only if not in error state and don't have URL yet) const match = cleanText.match(config.urlPattern); - if (match && match[1] && !session.authUrl) { + if (match && match[1] && !session.authUrl && session.status !== 'error') { session.authUrl = match[1]; session.status = 'waiting_auth'; logger.info('Auth URL captured', { provider, url: session.authUrl }); @@ -231,7 +250,7 @@ export async function startCLIAuth( // Log all output after auth URL is captured (for debugging) if (session.authUrl) { - const trimmedData = stripAnsiCodes(data).trim(); + const trimmedData = cleanText.trim(); if (trimmedData.length > 0) { logger.info('PTY output after auth URL', { provider, @@ -241,31 +260,20 @@ export async function startCLIAuth( } } - // Check for error patterns BEFORE checking success (error may appear first) - const matchedError = findMatchingError(data, config.errorPatterns); - if (matchedError && session.status !== 'error') { - logger.warn('Auth error detected', { - provider, - sessionId, - errorMessage: matchedError.message, - recoverable: matchedError.recoverable, - }); - session.status = 'error'; - session.error = matchedError.message; - session.errorHint = matchedError.hint; - session.recoverable = matchedError.recoverable; - // Don't exit yet - let the CLI handle retries if it wants to - // The error info will be returned when status is polled - } - // Check for success and try to extract credentials - if (matchesSuccessPattern(data, config.successPatterns)) { + // Don't override error status - if there was an error, keep it + if (session.status !== 'error' && matchesSuccessPattern(data, config.successPatterns)) { session.status = 'success'; logger.info('Success pattern detected, attempting credential extraction', { provider }); // Try to extract credentials immediately (CLI may not exit after success) // Use a small delay to let the CLI finish writing the file setTimeout(async () => { + // Don't extract if status changed to error (e.g., error detected after success pattern) + if (session.status === 'error') { + logger.info('Skipping credential extraction - session is in error state', { provider }); + return; + } try { const creds = await extractCredentials(provider, config); if (creds) { @@ -302,8 +310,9 @@ export async function startCLIAuth( outputTail: cleanOutput.slice(-500), }); - // Try to extract credentials - if (session.authUrl || exitCode === 0) { + // Try to extract credentials (but don't override error status) + // CLI might exit cleanly (code 0) even after an OAuth error + if ((session.authUrl || exitCode === 0) && session.status !== 'error') { try { const creds = await extractCredentials(provider, config); if (creds) { @@ -397,11 +406,14 @@ export async function submitAuthCode( }); // Try to extract credentials as a fallback - maybe auth completed in browser + // But don't override error status const config = CLI_AUTH_CONFIG[session.provider]; - if (config) { + if (config && session.status !== 'error') { try { const creds = await extractCredentials(session.provider, config); - if (creds) { + // Re-check status after async operation (race condition protection) + // Use type assertion because TypeScript narrowing doesn't account for async race conditions + if (creds && (session.status as AuthSession['status']) !== 'error') { session.token = creds.token; session.refreshToken = creds.refreshToken; session.tokenExpiresAt = creds.expiresAt; @@ -484,6 +496,14 @@ async function pollForCredentials(session: AuthSession, config: CLIAuthConfig): try { const creds = await extractCredentials(session.provider, config); if (creds) { + // Double-check we're not in error state (race condition protection) + // Use type assertion because TypeScript narrowing doesn't account for async race conditions + if ((session.status as AuthSession['status']) === 'error') { + logger.info('Credentials found but session is in error state, not overriding', { + provider: session.provider, + }); + return; + } session.token = creds.token; session.refreshToken = creds.refreshToken; session.tokenExpiresAt = creds.expiresAt; @@ -535,9 +555,18 @@ export async function completeAuthSession(sessionId: string): Promise<{ const pollInterval = 1000; for (let i = 0; i < maxAttempts; i++) { + // Check if session went into error state + if (session.status === 'error') { + return { success: false, error: session.error || 'Authentication failed' }; + } try { const creds = await extractCredentials(session.provider, config); if (creds) { + // Double-check we're not in error state (race condition protection) + // Use type assertion because TypeScript narrowing doesn't account for async race conditions + if ((session.status as AuthSession['status']) === 'error') { + return { success: false, error: session.error || 'Authentication failed' }; + } session.token = creds.token; session.refreshToken = creds.refreshToken; session.tokenExpiresAt = creds.expiresAt; diff --git a/src/shared/cli-auth-config.ts b/src/shared/cli-auth-config.ts index f4f93762..c2d7188e 100644 --- a/src/shared/cli-auth-config.ts +++ b/src/shared/cli-auth-config.ts @@ -130,6 +130,8 @@ export const CLI_AUTH_CONFIG: Record = { { // Fallback: Any "press enter" or "enter to confirm/continue" prompt // Keep this LAST so more specific handlers match first + // NOTE: Error messages like "Press Enter to retry" are handled by checking + // error patterns FIRST in the PTY handler, so this won't trigger on errors pattern: /press\s*enter|enter\s*to\s*(confirm|continue|proceed)|hit\s*enter/i, response: '\r', delay: 300, diff --git a/src/wrapper/base-wrapper.test.ts b/src/wrapper/base-wrapper.test.ts index 048f837f..d6212465 100644 --- a/src/wrapper/base-wrapper.test.ts +++ b/src/wrapper/base-wrapper.test.ts @@ -6,7 +6,7 @@ */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { BaseWrapper, type BaseWrapperConfig } from './base-wrapper.js'; +import { BaseWrapper } from './base-wrapper.js'; import type { QueuedMessage } from './shared.js'; import type { ParsedSummary } from './parser.js'; import type { SendPayload, SendMeta } from '../protocol/types.js'; diff --git a/src/wrapper/pty-wrapper.ts b/src/wrapper/pty-wrapper.ts index a4990c1d..22bef6bc 100644 --- a/src/wrapper/pty-wrapper.ts +++ b/src/wrapper/pty-wrapper.ts @@ -22,7 +22,6 @@ import { findAgentConfig } from '../utils/agent-config.js'; import { HookRegistry, createTrajectoryHooks, type LifecycleHooks } from '../hooks/index.js'; import { parseContinuityCommand, hasContinuityCommand } from '../continuity/index.js'; import { - type QueuedMessage, type InjectionCallbacks, INJECTION_CONSTANTS, stripAnsi, diff --git a/src/wrapper/tmux-wrapper.ts b/src/wrapper/tmux-wrapper.ts index a1000f2f..61134c24 100644 --- a/src/wrapper/tmux-wrapper.ts +++ b/src/wrapper/tmux-wrapper.ts @@ -16,7 +16,7 @@ import { exec, execSync, spawn, ChildProcess } from 'node:child_process'; import crypto from 'node:crypto'; import { promisify } from 'node:util'; import { BaseWrapper, type BaseWrapperConfig } from './base-wrapper.js'; -import { OutputParser, type ParsedCommand, type ParsedSummary, parseSummaryWithDetails, parseSessionEndFromOutput, type SessionEndMarker } from './parser.js'; +import { OutputParser, type ParsedCommand, type ParsedSummary, parseSummaryWithDetails, parseSessionEndFromOutput } from './parser.js'; import { hasContinuityCommand, parseContinuityCommand, @@ -39,7 +39,6 @@ import { } from '../trajectory/integration.js'; import { escapeForShell } from '../bridge/utils.js'; import { - type QueuedMessage, type CliType, type InjectionCallbacks, stripAnsi, @@ -52,7 +51,6 @@ import { } from './shared.js'; const execAsync = promisify(exec); -const escapeRegex = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Constants for cursor stability detection in waitForClearInput /** Number of consecutive polls with stable cursor before assuming input is clear */