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.
- package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
- package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
- package/.trajectories/index.json +140 -1
- package/TRAIL_GIT_AUTH_FIX.md +113 -0
- package/deploy/workspace/codex.config.toml +1 -1
- package/deploy/workspace/entrypoint.sh +20 -79
- package/deploy/workspace/gh-relay +156 -0
- package/deploy/workspace/git-credential-relay +5 -1
- package/dist/bridge/multi-project-client.js +13 -10
- package/dist/bridge/spawner.d.ts +2 -0
- package/dist/bridge/spawner.js +19 -1
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +115 -69
- package/dist/cloud/api/admin.js +16 -3
- package/dist/cloud/api/codex-auth-helper.js +28 -8
- package/dist/cloud/api/consensus.d.ts +13 -0
- package/dist/cloud/api/consensus.js +259 -0
- package/dist/cloud/api/daemons.js +205 -1
- package/dist/cloud/api/git.js +37 -7
- package/dist/cloud/api/onboarding.js +4 -1
- package/dist/cloud/api/provider-env.d.ts +5 -0
- package/dist/cloud/api/provider-env.js +27 -0
- package/dist/cloud/api/providers.js +2 -0
- package/dist/cloud/api/test-helpers.js +130 -0
- package/dist/cloud/api/workspaces.js +38 -3
- package/dist/cloud/db/bulk-ingest.d.ts +88 -0
- package/dist/cloud/db/bulk-ingest.js +268 -0
- package/dist/cloud/db/drizzle.d.ts +33 -0
- package/dist/cloud/db/drizzle.js +174 -2
- package/dist/cloud/db/index.d.ts +24 -5
- package/dist/cloud/db/index.js +19 -4
- package/dist/cloud/db/schema.d.ts +397 -3
- package/dist/cloud/db/schema.js +75 -1
- package/dist/cloud/provisioner/index.d.ts +8 -0
- package/dist/cloud/provisioner/index.js +256 -50
- package/dist/cloud/server.js +47 -3
- package/dist/cloud/services/index.d.ts +1 -0
- package/dist/cloud/services/index.js +2 -0
- package/dist/cloud/services/nango.d.ts +3 -4
- package/dist/cloud/services/nango.js +11 -33
- package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
- package/dist/cloud/services/workspace-keepalive.js +234 -0
- package/dist/config/relay-config.d.ts +23 -0
- package/dist/config/relay-config.js +23 -0
- package/dist/daemon/agent-manager.d.ts +20 -1
- package/dist/daemon/agent-manager.js +47 -0
- package/dist/daemon/agent-registry.js +4 -4
- package/dist/daemon/agent-signing.d.ts +158 -0
- package/dist/daemon/agent-signing.js +523 -0
- package/dist/daemon/api.js +18 -1
- package/dist/daemon/cli-auth.d.ts +4 -1
- package/dist/daemon/cli-auth.js +55 -11
- package/dist/daemon/cloud-sync.d.ts +47 -1
- package/dist/daemon/cloud-sync.js +152 -3
- package/dist/daemon/connection.d.ts +28 -0
- package/dist/daemon/connection.js +98 -15
- package/dist/daemon/consensus-integration.d.ts +167 -0
- package/dist/daemon/consensus-integration.js +371 -0
- package/dist/daemon/consensus.d.ts +271 -0
- package/dist/daemon/consensus.js +632 -0
- package/dist/daemon/delivery-tracker.d.ts +34 -0
- package/dist/daemon/delivery-tracker.js +104 -0
- package/dist/daemon/enhanced-features.d.ts +118 -0
- package/dist/daemon/enhanced-features.js +178 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.js +5 -0
- package/dist/daemon/rate-limiter.d.ts +68 -0
- package/dist/daemon/rate-limiter.js +130 -0
- package/dist/daemon/router.d.ts +18 -11
- package/dist/daemon/router.js +55 -111
- package/dist/daemon/server.d.ts +13 -1
- package/dist/daemon/server.js +71 -9
- package/dist/daemon/sync-queue.d.ts +116 -0
- package/dist/daemon/sync-queue.js +361 -0
- package/dist/health-worker-manager.d.ts +62 -0
- package/dist/health-worker-manager.js +144 -0
- package/dist/health-worker.d.ts +9 -0
- package/dist/health-worker.js +79 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/memory/context-compaction.d.ts +156 -0
- package/dist/memory/context-compaction.js +453 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/protocol/channels.js +4 -4
- package/dist/protocol/framing.d.ts +72 -10
- package/dist/protocol/framing.js +194 -25
- package/dist/storage/adapter.d.ts +8 -1
- package/dist/storage/adapter.js +11 -0
- package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
- package/dist/storage/batched-sqlite-adapter.js +183 -0
- package/dist/storage/dead-letter-queue.d.ts +196 -0
- package/dist/storage/dead-letter-queue.js +427 -0
- package/dist/storage/dlq-adapter.d.ts +195 -0
- package/dist/storage/dlq-adapter.js +664 -0
- package/dist/trajectory/config.d.ts +32 -14
- package/dist/trajectory/config.js +38 -16
- package/dist/trajectory/integration.js +217 -64
- package/dist/utils/git-remote.d.ts +47 -0
- package/dist/utils/git-remote.js +125 -0
- package/dist/utils/id-generator.d.ts +35 -0
- package/dist/utils/id-generator.js +60 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/precompiled-patterns.d.ts +110 -0
- package/dist/utils/precompiled-patterns.js +322 -0
- package/dist/wrapper/auth-detection.js +1 -1
- package/dist/wrapper/base-wrapper.d.ts +36 -0
- package/dist/wrapper/base-wrapper.js +48 -2
- package/dist/wrapper/client.d.ts +14 -4
- package/dist/wrapper/client.js +84 -31
- package/dist/wrapper/idle-detector.d.ts +102 -0
- package/dist/wrapper/idle-detector.js +279 -0
- package/dist/wrapper/parser.d.ts +4 -0
- package/dist/wrapper/parser.js +19 -1
- package/dist/wrapper/pty-wrapper.d.ts +7 -1
- package/dist/wrapper/pty-wrapper.js +51 -27
- package/dist/wrapper/tmux-wrapper.d.ts +12 -1
- package/dist/wrapper/tmux-wrapper.js +65 -17
- package/package.json +5 -5
- package/scripts/run-migrations.js +43 -0
- package/scripts/verify-schema.js +134 -0
- package/tests/benchmarks/protocol.bench.ts +310 -0
- package/dist/dashboard/out/404.html +0 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_buildManifest.js +0 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +0 -2
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/history/page-8c8bed33beb2bf1c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-16f3b49e55b1e0ed.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-4a5938c18a11a654.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-547dd0ca55ecd0ba.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
- package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-2ee6beb2ae96d210.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
- package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +0 -1
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +0 -1
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
- package/dist/dashboard/out/alt-logos/logo.svg +0 -38
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
- package/dist/dashboard/out/app/onboarding.html +0 -1
- package/dist/dashboard/out/app/onboarding.txt +0 -7
- package/dist/dashboard/out/app.html +0 -1
- package/dist/dashboard/out/app.txt +0 -7
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +0 -1
- package/dist/dashboard/out/connect-repos.txt +0 -7
- package/dist/dashboard/out/history.html +0 -1
- package/dist/dashboard/out/history.txt +0 -7
- package/dist/dashboard/out/index.html +0 -1
- package/dist/dashboard/out/index.txt +0 -7
- package/dist/dashboard/out/login.html +0 -6
- package/dist/dashboard/out/login.txt +0 -7
- package/dist/dashboard/out/metrics.html +0 -1
- package/dist/dashboard/out/metrics.txt +0 -7
- package/dist/dashboard/out/pricing.html +0 -13
- package/dist/dashboard/out/pricing.txt +0 -7
- package/dist/dashboard/out/providers/setup/claude.html +0 -1
- package/dist/dashboard/out/providers/setup/claude.txt +0 -8
- package/dist/dashboard/out/providers/setup/codex.html +0 -1
- package/dist/dashboard/out/providers/setup/codex.txt +0 -8
- package/dist/dashboard/out/providers.html +0 -1
- package/dist/dashboard/out/providers.txt +0 -7
- package/dist/dashboard/out/signup.html +0 -6
- package/dist/dashboard/out/signup.txt +0 -7
- package/dist/dashboard-server/metrics.d.ts +0 -105
- package/dist/dashboard-server/metrics.js +0 -193
- package/dist/dashboard-server/needs-attention.d.ts +0 -24
- package/dist/dashboard-server/needs-attention.js +0 -78
- package/dist/dashboard-server/server.d.ts +0 -15
- package/dist/dashboard-server/server.js +0 -3776
- package/dist/dashboard-server/start.d.ts +0 -6
- package/dist/dashboard-server/start.js +0 -13
- package/dist/dashboard-server/user-bridge.d.ts +0 -103
- 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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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 {
|
|
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-${
|
|
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-${
|
|
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-${
|
|
171
|
+
id: raw.id ?? `agent-${generateId()}`,
|
|
172
172
|
name: raw.name,
|
|
173
173
|
cli: raw.cli,
|
|
174
174
|
program: raw.program,
|