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
@@ -275,6 +275,22 @@ export class DaemonApi extends EventEmitter {
275
275
  }
276
276
  return { status: 200, body: { success: true } };
277
277
  });
278
+ // Interrupt agent by ID (send Ctrl+C to break out of stuck loops)
279
+ this.routes.set('POST /agents/:id/interrupt', async (req) => {
280
+ const interrupted = this.agentManager.interrupt(req.params.id);
281
+ if (!interrupted) {
282
+ return { status: 404, body: { error: 'Agent not found' } };
283
+ }
284
+ return { status: 200, body: { success: true } };
285
+ });
286
+ // Interrupt agent by name (for dashboard where only name is available)
287
+ this.routes.set('POST /agents/by-name/:name/interrupt', async (req) => {
288
+ const interrupted = this.agentManager.interruptByName(req.params.name);
289
+ if (!interrupted) {
290
+ return { status: 404, body: { error: 'Agent not found' } };
291
+ }
292
+ return { status: 200, body: { success: true } };
293
+ });
278
294
  // === All Agents ===
279
295
  // List all agents
280
296
  this.routes.set('GET /agents', async () => {
@@ -449,8 +465,9 @@ export class DaemonApi extends EventEmitter {
449
465
  // Check if provider is authenticated (credentials exist)
450
466
  this.routes.set('GET /auth/cli/:provider/check', async (req) => {
451
467
  const { provider } = req.params;
468
+ const userId = req.query.userId;
452
469
  const { checkProviderAuth } = await import('./cli-auth.js');
453
- const authenticated = await checkProviderAuth(provider);
470
+ const authenticated = await checkProviderAuth(provider, userId);
454
471
  return { status: 200, body: { authenticated } };
455
472
  });
456
473
  // === Repository Management ===
@@ -14,6 +14,7 @@ export type { CLIAuthConfig, PromptHandler };
14
14
  interface AuthSession {
15
15
  id: string;
16
16
  provider: string;
17
+ userId?: string;
17
18
  status: 'starting' | 'waiting_auth' | 'success' | 'error';
18
19
  authUrl?: string;
19
20
  token?: string;
@@ -32,6 +33,8 @@ interface AuthSession {
32
33
  export interface StartCLIAuthOptions {
33
34
  /** Use device flow instead of standard OAuth (if provider supports it) */
34
35
  useDeviceFlow?: boolean;
36
+ /** User ID for per-user credential storage (multi-user workspaces) */
37
+ userId?: string;
35
38
  }
36
39
  /**
37
40
  * Start CLI auth flow
@@ -75,5 +78,5 @@ export declare function cancelAuthSession(sessionId: string): boolean;
75
78
  * Check if a provider is authenticated (credentials exist)
76
79
  * Used by the auth check endpoint for SSH tunnel flow
77
80
  */
78
- export declare function checkProviderAuth(provider: string): Promise<boolean>;
81
+ export declare function checkProviderAuth(provider: string, userId?: string): Promise<boolean>;
79
82
  //# sourceMappingURL=cli-auth.d.ts.map
@@ -10,6 +10,7 @@ import * as fs from 'fs/promises';
10
10
  import * as os from 'os';
11
11
  import { createLogger } from '../resiliency/logger.js';
12
12
  import { CLI_AUTH_CONFIG, stripAnsiCodes, matchesSuccessPattern, findMatchingPrompt, findMatchingError, getSupportedProviders, } from '../shared/cli-auth-config.js';
13
+ import { getUserDirectoryService } from './user-directory.js';
13
14
  const logger = createLogger('cli-auth');
14
15
  // Re-export for consumers
15
16
  export { CLI_AUTH_CONFIG, getSupportedProviders };
@@ -47,6 +48,7 @@ export async function startCLIAuth(provider, options = {}) {
47
48
  const session = {
48
49
  id: sessionId,
49
50
  provider,
51
+ userId: options.userId,
50
52
  status: 'starting',
51
53
  output: '',
52
54
  promptsHandled: [],
@@ -61,7 +63,7 @@ export async function startCLIAuth(provider, options = {}) {
61
63
  });
62
64
  // Check if already authenticated (credentials exist)
63
65
  try {
64
- const existingCreds = await extractCredentials(provider, config);
66
+ const existingCreds = await extractCredentials(provider, config, options.userId);
65
67
  if (existingCreds?.token) {
66
68
  logger.info('Already authenticated - existing credentials found', { provider, sessionId });
67
69
  session.status = 'success';
@@ -97,6 +99,27 @@ export async function startCLIAuth(provider, options = {}) {
97
99
  resolveAuthUrl();
98
100
  }, AUTH_URL_WAIT_TIMEOUT);
99
101
  try {
102
+ // Get per-user environment if userId provided (for multi-user workspaces)
103
+ // This sets HOME to /data/users/{userId} so CLI stores credentials per-user
104
+ let userEnv = {};
105
+ if (options.userId) {
106
+ try {
107
+ const userDirService = getUserDirectoryService();
108
+ userEnv = userDirService.getUserEnvironment(options.userId);
109
+ logger.info('Using per-user environment for CLI auth', {
110
+ provider,
111
+ userId: options.userId,
112
+ home: userEnv.HOME,
113
+ });
114
+ }
115
+ catch (err) {
116
+ logger.warn('Failed to get user environment, using default', {
117
+ provider,
118
+ userId: options.userId,
119
+ error: err instanceof Error ? err.message : String(err),
120
+ });
121
+ }
122
+ }
100
123
  const proc = pty.spawn(config.command, args, {
101
124
  name: 'xterm-256color',
102
125
  cols: 120,
@@ -104,6 +127,7 @@ export async function startCLIAuth(provider, options = {}) {
104
127
  cwd: process.cwd(),
105
128
  env: {
106
129
  ...process.env,
130
+ ...userEnv, // Override HOME for per-user credential storage
107
131
  NO_COLOR: '1',
108
132
  TERM: 'xterm-256color',
109
133
  // Don't set BROWSER - let CLI fail to open browser and fall back to manual paste mode
@@ -198,7 +222,7 @@ export async function startCLIAuth(provider, options = {}) {
198
222
  return;
199
223
  }
200
224
  try {
201
- const creds = await extractCredentials(provider, config);
225
+ const creds = await extractCredentials(provider, config, session.userId);
202
226
  if (creds) {
203
227
  session.token = creds.token;
204
228
  session.refreshToken = creds.refreshToken;
@@ -233,7 +257,7 @@ export async function startCLIAuth(provider, options = {}) {
233
257
  // CLI might exit cleanly (code 0) even after an OAuth error
234
258
  if ((session.authUrl || exitCode === 0) && session.status !== 'error') {
235
259
  try {
236
- const creds = await extractCredentials(provider, config);
260
+ const creds = await extractCredentials(provider, config, session.userId);
237
261
  if (creds) {
238
262
  session.token = creds.token;
239
263
  session.refreshToken = creds.refreshToken;
@@ -321,7 +345,7 @@ export async function submitAuthCode(sessionId, code, state) {
321
345
  const config = CLI_AUTH_CONFIG[session.provider];
322
346
  if (config && session.status !== 'error') {
323
347
  try {
324
- const creds = await extractCredentials(session.provider, config);
348
+ const creds = await extractCredentials(session.provider, config, session.userId);
325
349
  // Re-check status after async operation (race condition protection)
326
350
  // Use type assertion because TypeScript narrowing doesn't account for async race conditions
327
351
  if (creds && session.status !== 'error') {
@@ -465,7 +489,7 @@ async function pollForCredentials(session, config) {
465
489
  return;
466
490
  }
467
491
  try {
468
- const creds = await extractCredentials(session.provider, config);
492
+ const creds = await extractCredentials(session.provider, config, session.userId);
469
493
  if (creds) {
470
494
  // Double-check we're not in error state (race condition protection)
471
495
  // Use type assertion because TypeScript narrowing doesn't account for async race conditions
@@ -522,7 +546,7 @@ export async function completeAuthSession(sessionId) {
522
546
  return { success: false, error: session.error || 'Authentication failed' };
523
547
  }
524
548
  try {
525
- const creds = await extractCredentials(session.provider, config);
549
+ const creds = await extractCredentials(session.provider, config, session.userId);
526
550
  if (creds) {
527
551
  // Double-check we're not in error state (race condition protection)
528
552
  // Use type assertion because TypeScript narrowing doesn't account for async race conditions
@@ -568,14 +592,34 @@ export function cancelAuthSession(sessionId) {
568
592
  sessions.delete(sessionId);
569
593
  return true;
570
594
  }
595
+ function resolveCredentialPath(provider, config, userId) {
596
+ if (!config.credentialPath)
597
+ return null;
598
+ if (!userId) {
599
+ return config.credentialPath.replace('~', os.homedir());
600
+ }
601
+ try {
602
+ const userDirService = getUserDirectoryService();
603
+ const userHome = userDirService.getUserHome(userId);
604
+ return config.credentialPath.replace('~', userHome);
605
+ }
606
+ catch (err) {
607
+ logger.warn('Failed to resolve per-user credential path, using default', {
608
+ provider,
609
+ userId,
610
+ error: err instanceof Error ? err.message : String(err),
611
+ });
612
+ return config.credentialPath.replace('~', os.homedir());
613
+ }
614
+ }
571
615
  /**
572
616
  * Extract credentials from CLI credential file
573
617
  */
574
- async function extractCredentials(provider, config) {
575
- if (!config.credentialPath)
618
+ async function extractCredentials(provider, config, userId) {
619
+ const credPath = resolveCredentialPath(provider, config, userId);
620
+ if (!credPath)
576
621
  return null;
577
622
  try {
578
- const credPath = config.credentialPath.replace('~', os.homedir());
579
623
  const content = await fs.readFile(credPath, 'utf8');
580
624
  const creds = JSON.parse(content);
581
625
  // Extract token based on provider
@@ -640,13 +684,13 @@ async function extractCredentials(provider, config) {
640
684
  * Check if a provider is authenticated (credentials exist)
641
685
  * Used by the auth check endpoint for SSH tunnel flow
642
686
  */
643
- export async function checkProviderAuth(provider) {
687
+ export async function checkProviderAuth(provider, userId) {
644
688
  const config = CLI_AUTH_CONFIG[provider];
645
689
  if (!config) {
646
690
  return false;
647
691
  }
648
692
  try {
649
- const creds = await extractCredentials(provider, config);
693
+ const creds = await extractCredentials(provider, config, userId);
650
694
  return !!creds?.token;
651
695
  }
652
696
  catch {
@@ -8,11 +8,23 @@
8
8
  * - Credential sync from cloud
9
9
  */
10
10
  import { EventEmitter } from 'events';
11
+ import type { StorageAdapter, StoredMessage } from '../storage/adapter.js';
12
+ import { type SyncQueueConfig, type SyncQueueStats } from './sync-queue.js';
11
13
  export interface CloudSyncConfig {
12
14
  apiKey?: string;
13
15
  cloudUrl: string;
14
16
  heartbeatInterval: number;
15
17
  enabled: boolean;
18
+ /** Enable message sync to cloud (default: true if connected) */
19
+ messageSyncEnabled?: boolean;
20
+ /** Batch size for message sync (default: 100) */
21
+ messageSyncBatchSize?: number;
22
+ /** Use optimized sync queue with compression and spillover (default: true) */
23
+ useOptimizedSync?: boolean;
24
+ /** Sync queue configuration */
25
+ syncQueue?: Partial<SyncQueueConfig>;
26
+ /** Project directory for git remote detection (defaults to cwd) */
27
+ projectDirectory?: string;
16
28
  }
17
29
  export interface RemoteAgent {
18
30
  name: string;
@@ -39,6 +51,12 @@ export declare class CloudSyncService extends EventEmitter {
39
51
  private localAgents;
40
52
  private remoteAgents;
41
53
  private connected;
54
+ private storage;
55
+ private lastMessageSyncTs;
56
+ private messageSyncInProgress;
57
+ private projectDirectory;
58
+ private repoFullName;
59
+ private syncQueue;
42
60
  constructor(config?: Partial<CloudSyncConfig>);
43
61
  /**
44
62
  * Get or create a persistent machine ID
@@ -51,7 +69,7 @@ export declare class CloudSyncService extends EventEmitter {
51
69
  /**
52
70
  * Stop the cloud sync service
53
71
  */
54
- stop(): void;
72
+ stop(): Promise<void>;
55
73
  /**
56
74
  * Update local agent list (called by daemon when agents change)
57
75
  */
@@ -96,6 +114,34 @@ export declare class CloudSyncService extends EventEmitter {
96
114
  * Get machine ID
97
115
  */
98
116
  getMachineIdentifier(): string;
117
+ /**
118
+ * Set the storage adapter for message sync
119
+ */
120
+ setStorage(storage: StorageAdapter): void;
121
+ /**
122
+ * Queue a single message for sync to cloud.
123
+ * Use this for real-time sync as messages are created.
124
+ * Falls back to batch sync if optimized queue is not enabled.
125
+ */
126
+ queueMessageForSync(message: StoredMessage): Promise<void>;
127
+ /**
128
+ * Get sync queue statistics (if optimized sync is enabled).
129
+ */
130
+ getSyncQueueStats(): SyncQueueStats | null;
131
+ /**
132
+ * Force flush the sync queue.
133
+ */
134
+ flushSyncQueue(): Promise<void>;
135
+ /**
136
+ * Sync local messages to cloud storage
137
+ *
138
+ * Reads messages from local SQLite since last sync and posts them
139
+ * to the cloud API for centralized storage and search.
140
+ */
141
+ syncMessagesToCloud(): Promise<{
142
+ synced: number;
143
+ duplicates: number;
144
+ }>;
99
145
  }
100
146
  export declare function getCloudSync(config?: Partial<CloudSyncConfig>): CloudSyncService;
101
147
  //# sourceMappingURL=cloud-sync.d.ts.map
@@ -13,6 +13,8 @@ import * as path from 'path';
13
13
  import * as os from 'os';
14
14
  import { randomBytes } from 'crypto';
15
15
  import { createLogger } from '../utils/logger.js';
16
+ import { SyncQueue } from './sync-queue.js';
17
+ import { getRepoFullNameFromPath } from '../utils/git-remote.js';
16
18
  const log = createLogger('cloud-sync');
17
19
  export class CloudSyncService extends EventEmitter {
18
20
  config;
@@ -21,16 +23,41 @@ export class CloudSyncService extends EventEmitter {
21
23
  localAgents = new Map();
22
24
  remoteAgents = [];
23
25
  connected = false;
26
+ storage = null;
27
+ lastMessageSyncTs = 0;
28
+ messageSyncInProgress = false;
29
+ // Project context for workspace resolution
30
+ projectDirectory;
31
+ repoFullName = null;
32
+ // Optimized sync queue
33
+ syncQueue = null;
24
34
  constructor(config = {}) {
25
35
  super();
26
36
  this.config = {
27
37
  apiKey: config.apiKey || process.env.AGENT_RELAY_API_KEY,
28
- cloudUrl: config.cloudUrl || process.env.AGENT_RELAY_CLOUD_URL || 'https://api.agent-relay.com',
38
+ cloudUrl: config.cloudUrl || process.env.AGENT_RELAY_CLOUD_URL || 'https://agent-relay.com',
29
39
  heartbeatInterval: config.heartbeatInterval || 30000, // 30 seconds
30
40
  enabled: config.enabled ?? true,
41
+ useOptimizedSync: config.useOptimizedSync ?? true,
42
+ syncQueue: config.syncQueue,
43
+ projectDirectory: config.projectDirectory,
31
44
  };
32
45
  // Generate or load machine ID for consistent identification
33
46
  this.machineId = this.getMachineId();
47
+ // Initialize project context for workspace resolution
48
+ this.projectDirectory = this.config.projectDirectory || process.cwd();
49
+ this.repoFullName = getRepoFullNameFromPath(this.projectDirectory);
50
+ if (this.repoFullName) {
51
+ log.info('Detected git repository', { repoFullName: this.repoFullName });
52
+ }
53
+ // Initialize optimized sync queue if enabled and API key is available
54
+ if (this.config.useOptimizedSync && this.config.apiKey) {
55
+ this.syncQueue = new SyncQueue({
56
+ cloudUrl: this.config.cloudUrl,
57
+ apiKey: this.config.apiKey,
58
+ ...this.config.syncQueue,
59
+ });
60
+ }
34
61
  }
35
62
  /**
36
63
  * Get or create a persistent machine ID
@@ -64,6 +91,13 @@ export class CloudSyncService extends EventEmitter {
64
91
  return;
65
92
  }
66
93
  log.info('Starting cloud sync', { url: this.config.cloudUrl });
94
+ // Recover any spilled messages from previous runs
95
+ if (this.syncQueue) {
96
+ const { recovered, failed } = await this.syncQueue.recoverSpilledMessages();
97
+ if (recovered > 0 || failed > 0) {
98
+ log.info('Recovered spilled messages', { recovered, failed });
99
+ }
100
+ }
67
101
  // Initial heartbeat
68
102
  await this.sendHeartbeat();
69
103
  // Start periodic heartbeat
@@ -74,11 +108,15 @@ export class CloudSyncService extends EventEmitter {
74
108
  /**
75
109
  * Stop the cloud sync service
76
110
  */
77
- stop() {
111
+ async stop() {
78
112
  if (this.heartbeatTimer) {
79
113
  clearInterval(this.heartbeatTimer);
80
114
  this.heartbeatTimer = undefined;
81
115
  }
116
+ // Gracefully close sync queue (flushes pending messages)
117
+ if (this.syncQueue) {
118
+ await this.syncQueue.close();
119
+ }
82
120
  this.connected = false;
83
121
  this.emit('disconnected');
84
122
  }
@@ -167,10 +205,11 @@ export class CloudSyncService extends EventEmitter {
167
205
  this.emit('command', cmd);
168
206
  }
169
207
  }
170
- // Fetch messages and sync agents
208
+ // Fetch messages, sync agents, and sync local messages to cloud
171
209
  await Promise.all([
172
210
  this.fetchMessages(),
173
211
  this.syncAgents(),
212
+ this.syncMessagesToCloud(),
174
213
  ]);
175
214
  }
176
215
  catch (error) {
@@ -251,6 +290,116 @@ export class CloudSyncService extends EventEmitter {
251
290
  getMachineIdentifier() {
252
291
  return this.machineId;
253
292
  }
293
+ /**
294
+ * Set the storage adapter for message sync
295
+ */
296
+ setStorage(storage) {
297
+ this.storage = storage;
298
+ log.info('Storage adapter configured for message sync');
299
+ }
300
+ /**
301
+ * Queue a single message for sync to cloud.
302
+ * Use this for real-time sync as messages are created.
303
+ * Falls back to batch sync if optimized queue is not enabled.
304
+ */
305
+ async queueMessageForSync(message) {
306
+ if (!this.connected || this.config.messageSyncEnabled === false) {
307
+ return;
308
+ }
309
+ if (this.syncQueue) {
310
+ await this.syncQueue.enqueue(message);
311
+ }
312
+ // If no sync queue, messages will be synced on next heartbeat via syncMessagesToCloud
313
+ }
314
+ /**
315
+ * Get sync queue statistics (if optimized sync is enabled).
316
+ */
317
+ getSyncQueueStats() {
318
+ return this.syncQueue?.getStats() ?? null;
319
+ }
320
+ /**
321
+ * Force flush the sync queue.
322
+ */
323
+ async flushSyncQueue() {
324
+ if (this.syncQueue) {
325
+ await this.syncQueue.flush();
326
+ }
327
+ }
328
+ /**
329
+ * Sync local messages to cloud storage
330
+ *
331
+ * Reads messages from local SQLite since last sync and posts them
332
+ * to the cloud API for centralized storage and search.
333
+ */
334
+ async syncMessagesToCloud() {
335
+ // Skip if disabled, not connected, no storage, or sync in progress
336
+ if (!this.connected || !this.storage || this.messageSyncInProgress) {
337
+ return { synced: 0, duplicates: 0 };
338
+ }
339
+ if (this.config.messageSyncEnabled === false) {
340
+ return { synced: 0, duplicates: 0 };
341
+ }
342
+ this.messageSyncInProgress = true;
343
+ try {
344
+ const batchSize = this.config.messageSyncBatchSize || 100;
345
+ // Get messages since last sync
346
+ const messages = await this.storage.getMessages({
347
+ sinceTs: this.lastMessageSyncTs > 0 ? this.lastMessageSyncTs : undefined,
348
+ limit: batchSize,
349
+ order: 'asc',
350
+ });
351
+ if (messages.length === 0) {
352
+ return { synced: 0, duplicates: 0 };
353
+ }
354
+ // Transform to API format
355
+ const syncPayload = messages.map((msg) => ({
356
+ id: msg.id,
357
+ ts: msg.ts,
358
+ from: msg.from,
359
+ to: msg.to,
360
+ body: msg.body,
361
+ kind: msg.kind,
362
+ topic: msg.topic,
363
+ thread: msg.thread,
364
+ is_broadcast: msg.is_broadcast,
365
+ is_urgent: msg.is_urgent,
366
+ data: msg.data,
367
+ payload_meta: msg.payloadMeta,
368
+ }));
369
+ // Post to cloud with repo context for workspace resolution
370
+ const response = await fetch(`${this.config.cloudUrl}/api/daemons/messages/sync`, {
371
+ method: 'POST',
372
+ headers: {
373
+ Authorization: `Bearer ${this.config.apiKey}`,
374
+ 'Content-Type': 'application/json',
375
+ },
376
+ body: JSON.stringify({
377
+ messages: syncPayload,
378
+ repoFullName: this.repoFullName,
379
+ }),
380
+ });
381
+ if (!response.ok) {
382
+ const errorText = await response.text();
383
+ throw new Error(`Message sync failed: ${response.status} - ${errorText}`);
384
+ }
385
+ const result = await response.json();
386
+ // Update last sync timestamp to the newest message we synced
387
+ if (messages.length > 0) {
388
+ this.lastMessageSyncTs = Math.max(...messages.map((m) => m.ts));
389
+ }
390
+ if (result.synced > 0) {
391
+ log.info(`Synced ${result.synced} messages to cloud`, { duplicates: result.duplicates });
392
+ }
393
+ return result;
394
+ }
395
+ catch (error) {
396
+ log.error('Message sync error', { error: String(error) });
397
+ return { synced: 0, duplicates: 0 };
398
+ }
399
+ finally {
400
+ this.messageSyncInProgress = false;
401
+ }
402
+ }
254
403
  }
255
404
  // Singleton instance
256
405
  let _cloudSync = null;
@@ -30,6 +30,12 @@ export interface ConnectionConfig {
30
30
  } | null>;
31
31
  /** Optional callback to check if agent is currently processing (exempts from heartbeat timeout) */
32
32
  isProcessing?: (agentName: string) => boolean;
33
+ /** Maximum messages in write queue before dropping (default: 2000) */
34
+ maxWriteQueueSize?: number;
35
+ /** High water mark - emit backpressure signal when queue exceeds this (default: 1500) */
36
+ writeQueueHighWaterMark?: number;
37
+ /** Low water mark - release backpressure when queue drops below this (default: 500) */
38
+ writeQueueLowWaterMark?: number;
33
39
  }
34
40
  export declare const DEFAULT_CONFIG: ConnectionConfig;
35
41
  export declare class Connection {
@@ -53,12 +59,18 @@ export declare class Connection {
53
59
  private heartbeatTimer?;
54
60
  private lastPongReceived?;
55
61
  private sequences;
62
+ private writeQueue;
63
+ private draining;
64
+ private _backpressured;
65
+ private socketDrainHandler?;
56
66
  onMessage?: (envelope: Envelope) => void;
57
67
  onClose?: () => void;
58
68
  onError?: (error: Error) => void;
59
69
  onActive?: () => void;
60
70
  onAck?: (envelope: Envelope<AckPayload>) => void;
61
71
  onPong?: () => void;
72
+ /** Fires when write queue crosses high/low water marks */
73
+ onBackpressure?: (backpressured: boolean) => void;
62
74
  constructor(socket: net.Socket, config?: Partial<ConnectionConfig>);
63
75
  get state(): ConnectionState;
64
76
  get agentName(): string | undefined;
@@ -73,6 +85,10 @@ export declare class Connection {
73
85
  get sessionId(): string;
74
86
  get resumeToken(): string;
75
87
  get isResumed(): boolean;
88
+ /** Whether this connection is currently backpressured (write queue above high water mark) */
89
+ get backpressured(): boolean;
90
+ /** Current number of messages queued for writing */
91
+ get writeQueueLength(): number;
76
92
  private setupSocketHandlers;
77
93
  private handleData;
78
94
  private processFrame;
@@ -91,8 +107,20 @@ export declare class Connection {
91
107
  getNextSeq(topic: string, peer: string): number;
92
108
  /**
93
109
  * Send an envelope to this connection.
110
+ *
111
+ * Uses a write queue to prevent blocking on slow consumers.
112
+ * Returns false if the connection is closed or the queue is full.
94
113
  */
95
114
  send(envelope: Envelope): boolean;
115
+ /**
116
+ * Schedule the drain loop to run on next tick if not already running.
117
+ */
118
+ private scheduleDrain;
119
+ /**
120
+ * Drain the write queue to the socket.
121
+ * Respects socket backpressure by waiting for 'drain' events.
122
+ */
123
+ private drain;
96
124
  private sendError;
97
125
  private handleClose;
98
126
  private handleError;