agent-relay 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
  2. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
  5. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
  6. package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
  7. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
  10. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
  11. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
  12. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
  13. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
  14. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
  15. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
  16. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
  17. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
  18. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
  19. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
  20. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
  21. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
  22. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
  23. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
  24. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
  25. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
  26. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
  27. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
  28. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
  29. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
  30. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
  31. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
  32. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
  33. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
  35. package/.trajectories/index.json +140 -1
  36. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  37. package/deploy/workspace/codex.config.toml +1 -1
  38. package/deploy/workspace/entrypoint.sh +20 -79
  39. package/deploy/workspace/gh-relay +156 -0
  40. package/deploy/workspace/git-credential-relay +5 -1
  41. package/dist/bridge/multi-project-client.js +13 -10
  42. package/dist/bridge/spawner.d.ts +2 -0
  43. package/dist/bridge/spawner.js +19 -1
  44. package/dist/bridge/types.d.ts +2 -0
  45. package/dist/cli/index.d.ts +1 -1
  46. package/dist/cli/index.js +115 -69
  47. package/dist/cloud/api/admin.js +16 -3
  48. package/dist/cloud/api/codex-auth-helper.js +28 -8
  49. package/dist/cloud/api/consensus.d.ts +13 -0
  50. package/dist/cloud/api/consensus.js +259 -0
  51. package/dist/cloud/api/daemons.js +205 -1
  52. package/dist/cloud/api/git.js +37 -7
  53. package/dist/cloud/api/onboarding.js +4 -1
  54. package/dist/cloud/api/provider-env.d.ts +5 -0
  55. package/dist/cloud/api/provider-env.js +27 -0
  56. package/dist/cloud/api/providers.js +2 -0
  57. package/dist/cloud/api/test-helpers.js +130 -0
  58. package/dist/cloud/api/workspaces.js +38 -3
  59. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  60. package/dist/cloud/db/bulk-ingest.js +268 -0
  61. package/dist/cloud/db/drizzle.d.ts +33 -0
  62. package/dist/cloud/db/drizzle.js +174 -2
  63. package/dist/cloud/db/index.d.ts +24 -5
  64. package/dist/cloud/db/index.js +19 -4
  65. package/dist/cloud/db/schema.d.ts +397 -3
  66. package/dist/cloud/db/schema.js +75 -1
  67. package/dist/cloud/provisioner/index.d.ts +8 -0
  68. package/dist/cloud/provisioner/index.js +256 -50
  69. package/dist/cloud/server.js +47 -3
  70. package/dist/cloud/services/index.d.ts +1 -0
  71. package/dist/cloud/services/index.js +2 -0
  72. package/dist/cloud/services/nango.d.ts +3 -4
  73. package/dist/cloud/services/nango.js +11 -33
  74. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  75. package/dist/cloud/services/workspace-keepalive.js +234 -0
  76. package/dist/config/relay-config.d.ts +23 -0
  77. package/dist/config/relay-config.js +23 -0
  78. package/dist/daemon/agent-manager.d.ts +20 -1
  79. package/dist/daemon/agent-manager.js +47 -0
  80. package/dist/daemon/agent-registry.js +4 -4
  81. package/dist/daemon/agent-signing.d.ts +158 -0
  82. package/dist/daemon/agent-signing.js +523 -0
  83. package/dist/daemon/api.js +18 -1
  84. package/dist/daemon/cli-auth.d.ts +4 -1
  85. package/dist/daemon/cli-auth.js +55 -11
  86. package/dist/daemon/cloud-sync.d.ts +47 -1
  87. package/dist/daemon/cloud-sync.js +152 -3
  88. package/dist/daemon/connection.d.ts +28 -0
  89. package/dist/daemon/connection.js +98 -15
  90. package/dist/daemon/consensus-integration.d.ts +167 -0
  91. package/dist/daemon/consensus-integration.js +371 -0
  92. package/dist/daemon/consensus.d.ts +271 -0
  93. package/dist/daemon/consensus.js +632 -0
  94. package/dist/daemon/delivery-tracker.d.ts +34 -0
  95. package/dist/daemon/delivery-tracker.js +104 -0
  96. package/dist/daemon/enhanced-features.d.ts +118 -0
  97. package/dist/daemon/enhanced-features.js +178 -0
  98. package/dist/daemon/index.d.ts +4 -0
  99. package/dist/daemon/index.js +5 -0
  100. package/dist/daemon/rate-limiter.d.ts +68 -0
  101. package/dist/daemon/rate-limiter.js +130 -0
  102. package/dist/daemon/router.d.ts +18 -11
  103. package/dist/daemon/router.js +55 -111
  104. package/dist/daemon/server.d.ts +13 -1
  105. package/dist/daemon/server.js +71 -9
  106. package/dist/daemon/sync-queue.d.ts +116 -0
  107. package/dist/daemon/sync-queue.js +361 -0
  108. package/dist/health-worker-manager.d.ts +62 -0
  109. package/dist/health-worker-manager.js +144 -0
  110. package/dist/health-worker.d.ts +9 -0
  111. package/dist/health-worker.js +79 -0
  112. package/dist/index.d.ts +2 -1
  113. package/dist/index.js +5 -1
  114. package/dist/memory/context-compaction.d.ts +156 -0
  115. package/dist/memory/context-compaction.js +453 -0
  116. package/dist/memory/index.d.ts +1 -0
  117. package/dist/memory/index.js +1 -0
  118. package/dist/protocol/channels.js +4 -4
  119. package/dist/protocol/framing.d.ts +72 -10
  120. package/dist/protocol/framing.js +194 -25
  121. package/dist/storage/adapter.d.ts +8 -1
  122. package/dist/storage/adapter.js +11 -0
  123. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  124. package/dist/storage/batched-sqlite-adapter.js +183 -0
  125. package/dist/storage/dead-letter-queue.d.ts +196 -0
  126. package/dist/storage/dead-letter-queue.js +427 -0
  127. package/dist/storage/dlq-adapter.d.ts +195 -0
  128. package/dist/storage/dlq-adapter.js +664 -0
  129. package/dist/trajectory/config.d.ts +32 -14
  130. package/dist/trajectory/config.js +38 -16
  131. package/dist/trajectory/integration.js +217 -64
  132. package/dist/utils/git-remote.d.ts +47 -0
  133. package/dist/utils/git-remote.js +125 -0
  134. package/dist/utils/id-generator.d.ts +35 -0
  135. package/dist/utils/id-generator.js +60 -0
  136. package/dist/utils/index.d.ts +1 -0
  137. package/dist/utils/index.js +1 -0
  138. package/dist/utils/precompiled-patterns.d.ts +110 -0
  139. package/dist/utils/precompiled-patterns.js +322 -0
  140. package/dist/wrapper/auth-detection.js +1 -1
  141. package/dist/wrapper/base-wrapper.d.ts +36 -0
  142. package/dist/wrapper/base-wrapper.js +48 -2
  143. package/dist/wrapper/client.d.ts +14 -4
  144. package/dist/wrapper/client.js +84 -31
  145. package/dist/wrapper/idle-detector.d.ts +102 -0
  146. package/dist/wrapper/idle-detector.js +279 -0
  147. package/dist/wrapper/parser.d.ts +4 -0
  148. package/dist/wrapper/parser.js +19 -1
  149. package/dist/wrapper/pty-wrapper.d.ts +7 -1
  150. package/dist/wrapper/pty-wrapper.js +51 -27
  151. package/dist/wrapper/tmux-wrapper.d.ts +12 -1
  152. package/dist/wrapper/tmux-wrapper.js +65 -17
  153. package/package.json +5 -5
  154. package/scripts/run-migrations.js +43 -0
  155. package/scripts/verify-schema.js +134 -0
  156. package/tests/benchmarks/protocol.bench.ts +310 -0
  157. package/dist/dashboard/out/404.html +0 -1
  158. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_buildManifest.js +0 -1
  159. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +0 -1
  160. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  161. package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +0 -2
  162. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
  163. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
  164. package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +0 -1
  165. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +0 -1
  166. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +0 -1
  167. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +0 -1
  168. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +0 -1
  169. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +0 -1
  170. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +0 -1
  171. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +0 -1
  172. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +0 -1
  173. package/dist/dashboard/out/_next/static/chunks/app/history/page-8c8bed33beb2bf1c.js +0 -1
  174. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  175. package/dist/dashboard/out/_next/static/chunks/app/login/page-16f3b49e55b1e0ed.js +0 -1
  176. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +0 -1
  177. package/dist/dashboard/out/_next/static/chunks/app/page-4a5938c18a11a654.js +0 -1
  178. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +0 -1
  179. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +0 -1
  180. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/app/signup/page-547dd0ca55ecd0ba.js +0 -1
  182. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
  183. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/main-2ee6beb2ae96d210.js +0 -1
  186. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +0 -1
  187. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
  188. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
  189. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  190. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
  191. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +0 -1
  192. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +0 -1
  193. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  194. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  195. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  196. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  197. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  198. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
  199. package/dist/dashboard/out/alt-logos/logo.svg +0 -38
  200. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  201. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  202. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  203. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  204. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  205. package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
  206. package/dist/dashboard/out/app/onboarding.html +0 -1
  207. package/dist/dashboard/out/app/onboarding.txt +0 -7
  208. package/dist/dashboard/out/app.html +0 -1
  209. package/dist/dashboard/out/app.txt +0 -7
  210. package/dist/dashboard/out/apple-icon.png +0 -0
  211. package/dist/dashboard/out/connect-repos.html +0 -1
  212. package/dist/dashboard/out/connect-repos.txt +0 -7
  213. package/dist/dashboard/out/history.html +0 -1
  214. package/dist/dashboard/out/history.txt +0 -7
  215. package/dist/dashboard/out/index.html +0 -1
  216. package/dist/dashboard/out/index.txt +0 -7
  217. package/dist/dashboard/out/login.html +0 -6
  218. package/dist/dashboard/out/login.txt +0 -7
  219. package/dist/dashboard/out/metrics.html +0 -1
  220. package/dist/dashboard/out/metrics.txt +0 -7
  221. package/dist/dashboard/out/pricing.html +0 -13
  222. package/dist/dashboard/out/pricing.txt +0 -7
  223. package/dist/dashboard/out/providers/setup/claude.html +0 -1
  224. package/dist/dashboard/out/providers/setup/claude.txt +0 -8
  225. package/dist/dashboard/out/providers/setup/codex.html +0 -1
  226. package/dist/dashboard/out/providers/setup/codex.txt +0 -8
  227. package/dist/dashboard/out/providers.html +0 -1
  228. package/dist/dashboard/out/providers.txt +0 -7
  229. package/dist/dashboard/out/signup.html +0 -6
  230. package/dist/dashboard/out/signup.txt +0 -7
  231. package/dist/dashboard-server/metrics.d.ts +0 -105
  232. package/dist/dashboard-server/metrics.js +0 -193
  233. package/dist/dashboard-server/needs-attention.d.ts +0 -24
  234. package/dist/dashboard-server/needs-attention.js +0 -78
  235. package/dist/dashboard-server/server.d.ts +0 -15
  236. package/dist/dashboard-server/server.js +0 -3776
  237. package/dist/dashboard-server/start.d.ts +0 -6
  238. package/dist/dashboard-server/start.js +0 -13
  239. package/dist/dashboard-server/user-bridge.d.ts +0 -103
  240. package/dist/dashboard-server/user-bridge.js +0 -189
@@ -81,48 +81,26 @@ class NangoService {
81
81
  * This is the user-level token (not the installation token).
82
82
  * Use this for operations that require user context (e.g., gh CLI).
83
83
  *
84
- * The user token can be found in:
85
- * 1. getToken() without installation flag
86
- * 2. connection_config.access_token in github-app-oauth
87
- * 3. Separate 'github' user connection
84
+ * The user token is stored in connection_config.userCredentials.access_token
85
+ * by Nango's GitHub App OAuth flow. This is a gho_* or ghu_* token that
86
+ * works for both git operations and gh CLI commands.
88
87
  */
89
88
  async getGithubUserOAuthToken(connectionId) {
90
- // First try: Get token from github-app-oauth connection credentials
91
- try {
92
- const token = await this.client.getToken(NANGO_INTEGRATIONS.GITHUB_APP, connectionId);
93
- if (typeof token === 'string' && token.length > 0) {
94
- return token;
95
- }
96
- if (token && typeof token === 'object') {
97
- const tokenObj = token;
98
- if (tokenObj.access_token) {
99
- return tokenObj.access_token;
100
- }
101
- if (tokenObj.token) {
102
- return tokenObj.token;
103
- }
104
- }
105
- }
106
- catch (err) {
107
- console.log('[nango] getToken for user OAuth failed, trying connection_config:', err);
108
- }
109
- // Second try: Check connection_config for user token
110
89
  try {
111
90
  const connection = await this.client.getConnection(NANGO_INTEGRATIONS.GITHUB_APP, connectionId);
112
91
  const connConfig = connection.connection_config;
113
- if (connConfig?.access_token && typeof connConfig.access_token === 'string') {
114
- return connConfig.access_token;
115
- }
116
- // Also check credentials object
117
- const credentials = connection.credentials;
118
- if (credentials?.access_token) {
119
- return credentials.access_token;
92
+ // Check for userCredentials.access_token (the user's personal OAuth token)
93
+ const userCredentials = connConfig?.userCredentials;
94
+ if (userCredentials?.access_token && typeof userCredentials.access_token === 'string') {
95
+ return userCredentials.access_token;
120
96
  }
97
+ throw new Error('No userCredentials.access_token in connection_config');
121
98
  }
122
99
  catch (err) {
123
- console.log('[nango] connection_config check failed:', err);
100
+ const message = err instanceof Error ? err.message : 'Unknown error';
101
+ console.log('[nango] getGithubUserOAuthToken failed:', message);
102
+ throw new Error('Could not retrieve GitHub user OAuth token');
124
103
  }
125
- throw new Error('Could not retrieve GitHub user OAuth token');
126
104
  }
127
105
  /**
128
106
  * Retrieve the user's OAuth token from a 'github' user connection.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Workspace Keepalive Service
3
+ *
4
+ * Prevents Fly.io from idling workspace machines that have active agents running.
5
+ *
6
+ * Problem: Fly.io uses request-based concurrency tracking to determine when to
7
+ * idle a machine. If a Claude agent is running but no HTTP requests are coming
8
+ * in (e.g., no one has the dashboard open), Fly.io may idle the machine.
9
+ *
10
+ * Solution: The cloud server periodically pings workspace machines that have
11
+ * active agents. This inbound HTTP request counts as activity for Fly.io's
12
+ * idle detection, keeping the machine awake.
13
+ *
14
+ * Flow:
15
+ * 1. Daemons report their running agents via heartbeat
16
+ * 2. This service queries for workspaces with active agents
17
+ * 3. Pings each workspace's /keep-alive endpoint
18
+ * 4. Workspace stays awake as long as agents are active
19
+ */
20
+ import { EventEmitter } from 'events';
21
+ export interface WorkspaceKeepaliveConfig {
22
+ /** How often to ping active workspaces (default: 60s) */
23
+ pingIntervalMs: number;
24
+ /** Request timeout for keep-alive pings (default: 5s) */
25
+ requestTimeoutMs: number;
26
+ /** Consider daemon stale if last heartbeat older than this (default: 2 min) */
27
+ staleThresholdMs: number;
28
+ /** Enable verbose logging (default: false) */
29
+ verbose: boolean;
30
+ }
31
+ export interface KeepaliveStats {
32
+ lastRun: Date | null;
33
+ totalPings: number;
34
+ successfulPings: number;
35
+ failedPings: number;
36
+ activeWorkspaces: number;
37
+ }
38
+ export declare class WorkspaceKeepaliveService extends EventEmitter {
39
+ private config;
40
+ private pingTimer;
41
+ private stats;
42
+ constructor(config?: Partial<WorkspaceKeepaliveConfig>);
43
+ /**
44
+ * Start the keepalive service
45
+ */
46
+ start(): void;
47
+ /**
48
+ * Stop the keepalive service
49
+ */
50
+ stop(): void;
51
+ /**
52
+ * Get current statistics
53
+ */
54
+ getStats(): KeepaliveStats;
55
+ /**
56
+ * Find workspaces with active agents and ping them
57
+ */
58
+ pingActiveWorkspaces(): Promise<void>;
59
+ /**
60
+ * Find all workspaces that have daemons with active agents
61
+ */
62
+ private findWorkspacesWithActiveAgents;
63
+ /**
64
+ * Ping a single workspace's keep-alive endpoint
65
+ */
66
+ private pingWorkspace;
67
+ }
68
+ /**
69
+ * Get or create the keepalive service singleton
70
+ */
71
+ export declare function getWorkspaceKeepaliveService(config?: Partial<WorkspaceKeepaliveConfig>): WorkspaceKeepaliveService;
72
+ /**
73
+ * Create a new keepalive service (for testing)
74
+ */
75
+ export declare function createWorkspaceKeepaliveService(config?: Partial<WorkspaceKeepaliveConfig>): WorkspaceKeepaliveService;
76
+ //# sourceMappingURL=workspace-keepalive.d.ts.map
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Workspace Keepalive Service
3
+ *
4
+ * Prevents Fly.io from idling workspace machines that have active agents running.
5
+ *
6
+ * Problem: Fly.io uses request-based concurrency tracking to determine when to
7
+ * idle a machine. If a Claude agent is running but no HTTP requests are coming
8
+ * in (e.g., no one has the dashboard open), Fly.io may idle the machine.
9
+ *
10
+ * Solution: The cloud server periodically pings workspace machines that have
11
+ * active agents. This inbound HTTP request counts as activity for Fly.io's
12
+ * idle detection, keeping the machine awake.
13
+ *
14
+ * Flow:
15
+ * 1. Daemons report their running agents via heartbeat
16
+ * 2. This service queries for workspaces with active agents
17
+ * 3. Pings each workspace's /keep-alive endpoint
18
+ * 4. Workspace stays awake as long as agents are active
19
+ */
20
+ import { EventEmitter } from 'events';
21
+ import { db } from '../db/index.js';
22
+ const DEFAULT_CONFIG = {
23
+ pingIntervalMs: 60_000, // 1 minute (well under Fly's ~5-10 min idle timeout)
24
+ requestTimeoutMs: 5_000, // 5 seconds
25
+ staleThresholdMs: 2 * 60 * 1000, // 2 minutes
26
+ verbose: false,
27
+ };
28
+ export class WorkspaceKeepaliveService extends EventEmitter {
29
+ config;
30
+ pingTimer = null;
31
+ stats = {
32
+ lastRun: null,
33
+ totalPings: 0,
34
+ successfulPings: 0,
35
+ failedPings: 0,
36
+ activeWorkspaces: 0,
37
+ };
38
+ constructor(config = {}) {
39
+ super();
40
+ this.config = { ...DEFAULT_CONFIG, ...config };
41
+ }
42
+ /**
43
+ * Start the keepalive service
44
+ */
45
+ start() {
46
+ if (this.pingTimer) {
47
+ return; // Already running
48
+ }
49
+ console.log('[keepalive] Starting workspace keepalive service', {
50
+ intervalMs: this.config.pingIntervalMs,
51
+ });
52
+ // Initial ping
53
+ this.pingActiveWorkspaces().catch((err) => {
54
+ console.error('[keepalive] Initial ping failed:', err);
55
+ });
56
+ // Start periodic pings
57
+ this.pingTimer = setInterval(() => {
58
+ this.pingActiveWorkspaces().catch((err) => {
59
+ console.error('[keepalive] Periodic ping failed:', err);
60
+ });
61
+ }, this.config.pingIntervalMs);
62
+ }
63
+ /**
64
+ * Stop the keepalive service
65
+ */
66
+ stop() {
67
+ if (this.pingTimer) {
68
+ clearInterval(this.pingTimer);
69
+ this.pingTimer = null;
70
+ console.log('[keepalive] Stopped workspace keepalive service');
71
+ }
72
+ }
73
+ /**
74
+ * Get current statistics
75
+ */
76
+ getStats() {
77
+ return { ...this.stats };
78
+ }
79
+ /**
80
+ * Find workspaces with active agents and ping them
81
+ */
82
+ async pingActiveWorkspaces() {
83
+ const startTime = Date.now();
84
+ try {
85
+ // Find workspaces with active agents
86
+ const activeWorkspaces = await this.findWorkspacesWithActiveAgents();
87
+ this.stats.activeWorkspaces = activeWorkspaces.length;
88
+ this.stats.lastRun = new Date();
89
+ if (activeWorkspaces.length === 0) {
90
+ if (this.config.verbose) {
91
+ console.log('[keepalive] No active workspaces to ping');
92
+ }
93
+ return;
94
+ }
95
+ if (this.config.verbose) {
96
+ console.log(`[keepalive] Pinging ${activeWorkspaces.length} active workspace(s)`);
97
+ }
98
+ // Ping each workspace in parallel
99
+ const results = await Promise.allSettled(activeWorkspaces.map((ws) => this.pingWorkspace(ws)));
100
+ // Update stats
101
+ for (const result of results) {
102
+ this.stats.totalPings++;
103
+ if (result.status === 'fulfilled' && result.value) {
104
+ this.stats.successfulPings++;
105
+ }
106
+ else {
107
+ this.stats.failedPings++;
108
+ }
109
+ }
110
+ const duration = Date.now() - startTime;
111
+ if (this.config.verbose) {
112
+ console.log(`[keepalive] Ping cycle complete`, {
113
+ workspaces: activeWorkspaces.length,
114
+ durationMs: duration,
115
+ });
116
+ }
117
+ this.emit('ping-cycle', {
118
+ workspaces: activeWorkspaces.length,
119
+ duration,
120
+ results: results.map((r) => r.status === 'fulfilled' && r.value),
121
+ });
122
+ }
123
+ catch (err) {
124
+ console.error('[keepalive] Error in ping cycle:', err);
125
+ this.emit('error', err);
126
+ }
127
+ }
128
+ /**
129
+ * Find all workspaces that have daemons with active agents
130
+ */
131
+ async findWorkspacesWithActiveAgents() {
132
+ const staleThreshold = new Date(Date.now() - this.config.staleThresholdMs);
133
+ // Get all workspaces and check each for active agents
134
+ const allWorkspaces = await db.workspaces.findAll();
135
+ const activeWorkspaces = [];
136
+ for (const workspace of allWorkspaces) {
137
+ // Skip workspaces that aren't running or don't have a URL
138
+ if (workspace.status !== 'running' || !workspace.publicUrl) {
139
+ continue;
140
+ }
141
+ // Get daemons for this workspace
142
+ const daemons = await db.linkedDaemons.findByWorkspaceId(workspace.id);
143
+ for (const daemon of daemons) {
144
+ // Skip offline daemons or those with stale heartbeats
145
+ if (daemon.status !== 'online')
146
+ continue;
147
+ if (daemon.lastSeenAt && daemon.lastSeenAt < staleThreshold)
148
+ continue;
149
+ // Check if daemon has any active agents
150
+ const metadata = daemon.metadata;
151
+ const agents = metadata?.agents || [];
152
+ // Count agents that appear to be active (not offline/disconnected)
153
+ const activeAgents = agents.filter((a) => a.status === 'online' || a.status === 'running' || a.status === 'active');
154
+ if (activeAgents.length > 0) {
155
+ activeWorkspaces.push({
156
+ workspaceId: workspace.id,
157
+ publicUrl: workspace.publicUrl,
158
+ daemonId: daemon.id,
159
+ daemonName: daemon.name,
160
+ agentCount: activeAgents.length,
161
+ });
162
+ // Only need one daemon per workspace to keep it alive
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ return activeWorkspaces;
168
+ }
169
+ /**
170
+ * Ping a single workspace's keep-alive endpoint
171
+ */
172
+ async pingWorkspace(workspace) {
173
+ const url = `${workspace.publicUrl.replace(/\/$/, '')}/keep-alive`;
174
+ try {
175
+ const controller = new AbortController();
176
+ const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
177
+ const response = await fetch(url, {
178
+ method: 'GET',
179
+ signal: controller.signal,
180
+ headers: {
181
+ 'User-Agent': 'AgentRelay-Keepalive/1.0',
182
+ },
183
+ });
184
+ clearTimeout(timeout);
185
+ if (response.ok) {
186
+ const data = await response.json();
187
+ if (this.config.verbose) {
188
+ console.log(`[keepalive] Pinged ${workspace.daemonName}`, {
189
+ workspaceId: workspace.workspaceId,
190
+ activeAgents: data.activeAgents,
191
+ });
192
+ }
193
+ return true;
194
+ }
195
+ else {
196
+ console.warn(`[keepalive] Ping failed for ${workspace.daemonName}:`, {
197
+ status: response.status,
198
+ url,
199
+ });
200
+ return false;
201
+ }
202
+ }
203
+ catch (err) {
204
+ // Don't log aborted requests as errors (timeout is expected for stopped machines)
205
+ if (err instanceof Error && err.name === 'AbortError') {
206
+ if (this.config.verbose) {
207
+ console.log(`[keepalive] Ping timeout for ${workspace.daemonName} (machine may be starting)`);
208
+ }
209
+ }
210
+ else {
211
+ console.warn(`[keepalive] Ping error for ${workspace.daemonName}:`, err);
212
+ }
213
+ return false;
214
+ }
215
+ }
216
+ }
217
+ // Singleton instance
218
+ let _keepaliveService = null;
219
+ /**
220
+ * Get or create the keepalive service singleton
221
+ */
222
+ export function getWorkspaceKeepaliveService(config) {
223
+ if (!_keepaliveService) {
224
+ _keepaliveService = new WorkspaceKeepaliveService(config);
225
+ }
226
+ return _keepaliveService;
227
+ }
228
+ /**
229
+ * Create a new keepalive service (for testing)
230
+ */
231
+ export function createWorkspaceKeepaliveService(config) {
232
+ return new WorkspaceKeepaliveService(config);
233
+ }
234
+ //# sourceMappingURL=workspace-keepalive.js.map
@@ -0,0 +1,23 @@
1
+ export declare const DEFAULT_CONNECTION_CONFIG: {
2
+ readonly maxFrameBytes: number;
3
+ readonly heartbeatMs: 5000;
4
+ readonly heartbeatTimeoutMultiplier: 6;
5
+ readonly maxWriteQueueSize: 2000;
6
+ readonly writeQueueHighWaterMark: 1500;
7
+ readonly writeQueueLowWaterMark: 500;
8
+ };
9
+ export declare const DEFAULT_TMUX_WRAPPER_CONFIG: {
10
+ readonly pollInterval: 200;
11
+ readonly idleBeforeInjectMs: 1500;
12
+ readonly injectRetryMs: 500;
13
+ readonly debug: false;
14
+ readonly debugLogIntervalMs: 0;
15
+ readonly mouseMode: true;
16
+ readonly activityIdleThresholdMs: 30000;
17
+ readonly outputStabilityTimeoutMs: 2000;
18
+ readonly outputStabilityPollMs: 200;
19
+ readonly streamLogs: true;
20
+ };
21
+ export declare const DEFAULT_IDLE_BEFORE_INJECT_MS = 1500;
22
+ export declare const DEFAULT_IDLE_CONFIDENCE_THRESHOLD = 0.7;
23
+ //# sourceMappingURL=relay-config.d.ts.map
@@ -0,0 +1,23 @@
1
+ export const DEFAULT_CONNECTION_CONFIG = {
2
+ maxFrameBytes: 1024 * 1024,
3
+ heartbeatMs: 5000,
4
+ heartbeatTimeoutMultiplier: 6,
5
+ maxWriteQueueSize: 2000,
6
+ writeQueueHighWaterMark: 1500,
7
+ writeQueueLowWaterMark: 500,
8
+ };
9
+ export const DEFAULT_TMUX_WRAPPER_CONFIG = {
10
+ pollInterval: 200,
11
+ idleBeforeInjectMs: 1500,
12
+ injectRetryMs: 500,
13
+ debug: false,
14
+ debugLogIntervalMs: 0,
15
+ mouseMode: true,
16
+ activityIdleThresholdMs: 30_000,
17
+ outputStabilityTimeoutMs: 2000,
18
+ outputStabilityPollMs: 200,
19
+ streamLogs: true,
20
+ };
21
+ export const DEFAULT_IDLE_BEFORE_INJECT_MS = 1500;
22
+ export const DEFAULT_IDLE_CONFIDENCE_THRESHOLD = 0.7;
23
+ //# sourceMappingURL=relay-config.js.map
@@ -3,7 +3,7 @@
3
3
  * Manages agents across workspaces with integrated resiliency.
4
4
  */
5
5
  import { EventEmitter } from 'events';
6
- import { type SummaryEvent, type SessionEndEvent } from '../wrapper/pty-wrapper.js';
6
+ import { PtyWrapper, type SummaryEvent, type SessionEndEvent } from '../wrapper/pty-wrapper.js';
7
7
  import type { Agent, SpawnAgentRequest } from './types.js';
8
8
  /**
9
9
  * Optional cloud persistence handler.
@@ -15,6 +15,9 @@ export interface CloudPersistenceHandler {
15
15
  /** Optional cleanup method for tests and graceful shutdown */
16
16
  destroy?: () => void;
17
17
  }
18
+ interface ManagedAgent extends Agent {
19
+ pty?: PtyWrapper;
20
+ }
18
21
  export declare class AgentManager extends EventEmitter {
19
22
  private agents;
20
23
  private supervisor;
@@ -72,6 +75,21 @@ export declare class AgentManager extends EventEmitter {
72
75
  * Send input to an agent
73
76
  */
74
77
  sendInput(agentId: string, input: string): boolean;
78
+ /**
79
+ * Interrupt an agent by sending Escape key twice.
80
+ * This breaks the agent out of their current task to allow refocusing.
81
+ * Uses Escape instead of Ctrl+C for better CLI compatibility (e.g., Claude CLI).
82
+ */
83
+ interrupt(agentId: string): boolean;
84
+ /**
85
+ * Find agent by name (global search across all workspaces)
86
+ */
87
+ findAgentByName(name: string): ManagedAgent | undefined;
88
+ /**
89
+ * Interrupt an agent by name (searches across all workspaces).
90
+ * Useful for dashboard where only agent name is available.
91
+ */
92
+ interruptByName(name: string): boolean;
75
93
  /**
76
94
  * Restart an agent
77
95
  */
@@ -111,4 +129,5 @@ export declare class AgentManager extends EventEmitter {
111
129
  shutdown(): Promise<void>;
112
130
  }
113
131
  export declare function getAgentManager(dataDir?: string): AgentManager;
132
+ export {};
114
133
  //# sourceMappingURL=agent-manager.d.ts.map
@@ -97,6 +97,12 @@ export class AgentManager extends EventEmitter {
97
97
  args,
98
98
  cwd: workspacePath,
99
99
  logsDir: this.logsDir,
100
+ env: {
101
+ CLOUD_API_URL: process.env.CLOUD_API_URL || '',
102
+ WORKSPACE_TOKEN: process.env.WORKSPACE_TOKEN || '',
103
+ WORKSPACE_ID: workspaceId,
104
+ ...process.env,
105
+ },
100
106
  onExit: (code) => {
101
107
  logger.info('Agent process exited', { name, code });
102
108
  this.handleAgentExit(agent.id, code);
@@ -271,6 +277,41 @@ export class AgentManager extends EventEmitter {
271
277
  agent.pty.write(input);
272
278
  return true;
273
279
  }
280
+ /**
281
+ * Interrupt an agent by sending Escape key twice.
282
+ * This breaks the agent out of their current task to allow refocusing.
283
+ * Uses Escape instead of Ctrl+C for better CLI compatibility (e.g., Claude CLI).
284
+ */
285
+ interrupt(agentId) {
286
+ const agent = this.agents.get(agentId);
287
+ if (!agent?.pty)
288
+ return false;
289
+ logger.info('Interrupting agent', { id: agentId, name: agent.name });
290
+ // Send Escape key twice (ASCII 0x1b) to interrupt current operation
291
+ // Double Escape ensures the CLI exits any nested mode/prompt
292
+ agent.pty.write('\x1b\x1b');
293
+ return true;
294
+ }
295
+ /**
296
+ * Find agent by name (global search across all workspaces)
297
+ */
298
+ findAgentByName(name) {
299
+ return Array.from(this.agents.values()).find((a) => a.name === name);
300
+ }
301
+ /**
302
+ * Interrupt an agent by name (searches across all workspaces).
303
+ * Useful for dashboard where only agent name is available.
304
+ */
305
+ interruptByName(name) {
306
+ const agent = this.findAgentByName(name);
307
+ if (!agent?.pty)
308
+ return false;
309
+ logger.info('Interrupting agent by name', { name, id: agent.id });
310
+ // Send Escape key twice (ASCII 0x1b) to interrupt current operation
311
+ // Double Escape ensures the CLI exits any nested mode/prompt
312
+ agent.pty.write('\x1b\x1b');
313
+ return true;
314
+ }
274
315
  /**
275
316
  * Restart an agent
276
317
  */
@@ -300,6 +341,12 @@ export class AgentManager extends EventEmitter {
300
341
  args,
301
342
  cwd: workspacePath,
302
343
  logsDir: this.logsDir,
344
+ env: {
345
+ CLOUD_API_URL: process.env.CLOUD_API_URL || '',
346
+ WORKSPACE_TOKEN: process.env.WORKSPACE_TOKEN || '',
347
+ WORKSPACE_ID: agent.workspaceId,
348
+ ...process.env,
349
+ },
303
350
  onExit: (code) => {
304
351
  this.handleAgentExit(agent.id, code);
305
352
  },
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { v4 as uuid } from 'uuid';
7
+ import { generateId } from '../utils/id-generator.js';
8
8
  import { createLogger } from '../utils/logger.js';
9
9
  const log = createLogger('registry');
10
10
  export class AgentRegistry {
@@ -48,7 +48,7 @@ export class AgentRegistry {
48
48
  return updated;
49
49
  }
50
50
  const record = {
51
- id: `agent-${uuid()}`,
51
+ id: `agent-${generateId()}`,
52
52
  name: agent.name,
53
53
  cli: agent.cli,
54
54
  program: agent.program,
@@ -138,7 +138,7 @@ export class AgentRegistry {
138
138
  return existing;
139
139
  const now = new Date().toISOString();
140
140
  const record = {
141
- id: `agent-${uuid()}`,
141
+ id: `agent-${generateId()}`,
142
142
  name: agentName,
143
143
  firstSeen: now,
144
144
  lastSeen: now,
@@ -168,7 +168,7 @@ export class AgentRegistry {
168
168
  if (!raw?.name)
169
169
  continue;
170
170
  const record = {
171
- id: raw.id ?? `agent-${uuid()}`,
171
+ id: raw.id ?? `agent-${generateId()}`,
172
172
  name: raw.name,
173
173
  cli: raw.cli,
174
174
  program: raw.program,