agent-relay 1.3.1 → 1.3.3
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/README.md +23 -9
- 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 +58 -76
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.d.ts +8 -6
- package/dist/cli/index.js +297 -30
- 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 +51 -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 +113 -22
- 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 +57 -113
- 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/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/{page-c617745b81344f4f.js → page-7f64824ae7d06707.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/{page-dc786c183425c2ac.js → page-814efc4d77b4191d.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{main-2ee6beb2ae96d210.js → main-5a40a5ae29646e1b.js} +1 -1
- package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -0
- package/dist/dashboard/out/cloud/link.txt +7 -0
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +2 -2
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -3
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +1 -1
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +1 -1
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +1 -1
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +1 -1
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +1 -1
- package/dist/dashboard-server/server.js +244 -28
- 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 +40 -0
- package/dist/wrapper/base-wrapper.js +60 -6
- package/dist/wrapper/client.d.ts +14 -4
- package/dist/wrapper/client.js +89 -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 +14 -2
- package/dist/wrapper/pty-wrapper.js +132 -32
- package/dist/wrapper/shared.d.ts +1 -1
- package/dist/wrapper/shared.js +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +20 -2
- package/dist/wrapper/tmux-wrapper.js +163 -40
- package/package.json +3 -1
- 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/_next/static/chunks/116-2502180def231162.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +0 -1
- package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +0 -1
- /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_ssgManifest.js +0 -0
|
@@ -26,6 +26,8 @@ import { getTrajectoryIntegration, detectPhaseFromContent, detectToolCalls, dete
|
|
|
26
26
|
import { escapeForShell } from '../bridge/utils.js';
|
|
27
27
|
import { detectProviderAuthRevocation } from './auth-detection.js';
|
|
28
28
|
import { stripAnsi, sleep, getDefaultRelayPrefix, buildInjectionString, injectWithRetry as sharedInjectWithRetry, INJECTION_CONSTANTS, CLI_QUIRKS, } from './shared.js';
|
|
29
|
+
import { getTmuxPanePid } from './idle-detector.js';
|
|
30
|
+
import { DEFAULT_TMUX_WRAPPER_CONFIG } from '../config/relay-config.js';
|
|
29
31
|
const execAsync = promisify(exec);
|
|
30
32
|
// Constants for cursor stability detection in waitForClearInput
|
|
31
33
|
/** Number of consecutive polls with stable cursor before assuming input is clear */
|
|
@@ -82,16 +84,7 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
82
84
|
const mergedConfig = {
|
|
83
85
|
cols: process.stdout.columns || 120,
|
|
84
86
|
rows: process.stdout.rows || 40,
|
|
85
|
-
|
|
86
|
-
idleBeforeInjectMs: 1500,
|
|
87
|
-
injectRetryMs: 500,
|
|
88
|
-
debug: false,
|
|
89
|
-
debugLogIntervalMs: 0,
|
|
90
|
-
mouseMode: true, // Enable mouse scroll passthrough by default
|
|
91
|
-
activityIdleThresholdMs: 30_000, // Consider idle after 30s with no output
|
|
92
|
-
outputStabilityTimeoutMs: 2000,
|
|
93
|
-
outputStabilityPollMs: 200,
|
|
94
|
-
streamLogs: true, // Stream output to daemon for dashboard
|
|
87
|
+
...DEFAULT_TMUX_WRAPPER_CONFIG,
|
|
95
88
|
...config,
|
|
96
89
|
};
|
|
97
90
|
// Call parent constructor (initializes client, cliType, relayPrefix, continuity)
|
|
@@ -225,11 +218,18 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
225
218
|
if (!this.config.args || this.config.args.length === 0) {
|
|
226
219
|
return this.config.command;
|
|
227
220
|
}
|
|
228
|
-
// Quote any argument that contains spaces, quotes, or special chars
|
|
221
|
+
// Quote any argument that contains spaces, quotes, or shell special chars
|
|
222
|
+
// Must handle: spaces, quotes, $, <, >, |, &, ;, (, ), `, etc.
|
|
229
223
|
const quotedArgs = this.config.args.map(arg => {
|
|
230
|
-
if (
|
|
231
|
-
// Use double quotes and escape internal quotes
|
|
232
|
-
|
|
224
|
+
if (/[\s"'$<>|&;()`,!\\]/.test(arg)) {
|
|
225
|
+
// Use double quotes and escape internal quotes and special chars
|
|
226
|
+
const escaped = arg
|
|
227
|
+
.replace(/\\/g, '\\\\')
|
|
228
|
+
.replace(/"/g, '\\"')
|
|
229
|
+
.replace(/\$/g, '\\$')
|
|
230
|
+
.replace(/`/g, '\\`')
|
|
231
|
+
.replace(/!/g, '\\!');
|
|
232
|
+
return `"${escaped}"`;
|
|
233
233
|
}
|
|
234
234
|
return arg;
|
|
235
235
|
});
|
|
@@ -337,9 +337,13 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
337
337
|
// Wait for shell to be ready (look for prompt)
|
|
338
338
|
await this.waitForShellReady();
|
|
339
339
|
// Send the command to run
|
|
340
|
+
this.logStderr('Sending command to tmux...');
|
|
340
341
|
await this.sendKeysLiteral(fullCommand);
|
|
341
|
-
await sleep(
|
|
342
|
+
await sleep(300); // Give shell time to process the command literal
|
|
343
|
+
this.logStderr('Sending Enter...');
|
|
342
344
|
await this.sendKeys('Enter');
|
|
345
|
+
await sleep(500); // Ensure Enter is processed and command starts before we continue
|
|
346
|
+
this.logStderr('Command sent');
|
|
343
347
|
}
|
|
344
348
|
catch (err) {
|
|
345
349
|
throw new Error(`Failed to create tmux session: ${err.message}`);
|
|
@@ -353,10 +357,19 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
353
357
|
this.initializeTrajectory();
|
|
354
358
|
// Initialize continuity and get/create agentId
|
|
355
359
|
this.initializeAgentId();
|
|
356
|
-
// Inject instructions for the agent (after a delay to let CLI initialize)
|
|
357
|
-
setTimeout(() => this.injectInstructions(), 3000);
|
|
358
360
|
// Start background polling (silent - no stdout writes)
|
|
359
361
|
this.startSilentPolling();
|
|
362
|
+
// Initialize idle detector with the tmux pane PID for process state inspection
|
|
363
|
+
this.initializeIdleDetectorPid();
|
|
364
|
+
// Wait for agent to be ready, then inject instructions
|
|
365
|
+
// This replaces the fixed 3-second delay with actual readiness detection
|
|
366
|
+
this.waitForAgentReady().then(() => {
|
|
367
|
+
this.injectInstructions();
|
|
368
|
+
}).catch(err => {
|
|
369
|
+
this.logStderr(`Failed to wait for agent ready: ${err.message}`, true);
|
|
370
|
+
// Fall back to injecting after a delay
|
|
371
|
+
setTimeout(() => this.injectInstructions(), 3000);
|
|
372
|
+
});
|
|
360
373
|
// Attach user to tmux session
|
|
361
374
|
// This takes over stdin/stdout - user sees the real terminal
|
|
362
375
|
this.attachToSession();
|
|
@@ -409,12 +422,49 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
409
422
|
this.logStderr(`Failed to initialize agent ID: ${err.message}`, true);
|
|
410
423
|
}
|
|
411
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Initialize the idle detector with the tmux pane PID.
|
|
427
|
+
* This enables process state inspection on Linux for more reliable idle detection.
|
|
428
|
+
*/
|
|
429
|
+
async initializeIdleDetectorPid() {
|
|
430
|
+
try {
|
|
431
|
+
const pid = await getTmuxPanePid(this.tmuxPath, this.sessionName);
|
|
432
|
+
if (pid) {
|
|
433
|
+
this.setIdleDetectorPid(pid);
|
|
434
|
+
this.logStderr(`Idle detector initialized with PID ${pid}`);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
this.logStderr('Could not get pane PID for idle detection (will use output analysis)');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
this.logStderr(`Failed to initialize idle detector PID: ${err.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Wait for the agent to be ready for input.
|
|
446
|
+
* Uses idle detection instead of a fixed delay.
|
|
447
|
+
*/
|
|
448
|
+
async waitForAgentReady() {
|
|
449
|
+
// Minimum wait to ensure the CLI process has started
|
|
450
|
+
await sleep(500);
|
|
451
|
+
// Wait for agent to become idle (CLI fully initialized)
|
|
452
|
+
const result = await this.waitForIdleState(10000, 200);
|
|
453
|
+
if (result.isIdle) {
|
|
454
|
+
this.logStderr(`Agent ready (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this.logStderr('Agent readiness timeout, proceeding anyway');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
412
460
|
/**
|
|
413
461
|
* Inject usage instructions for the agent including persistence protocol
|
|
414
462
|
*/
|
|
415
463
|
async injectInstructions() {
|
|
416
464
|
if (!this.running)
|
|
417
465
|
return;
|
|
466
|
+
if (this.config.skipInstructions)
|
|
467
|
+
return;
|
|
418
468
|
// Use escaped prefix (\->relay:) in examples to prevent parser from treating them as real commands
|
|
419
469
|
const escapedPrefix = '\\' + this.relayPrefix;
|
|
420
470
|
// Build instructions including relay and trail
|
|
@@ -586,6 +636,9 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
586
636
|
if (stdout.length !== this.processedOutputLength) {
|
|
587
637
|
this.lastOutputTime = Date.now();
|
|
588
638
|
this.markActivity();
|
|
639
|
+
// Feed new output to idle detector for more robust idle detection
|
|
640
|
+
const newOutput = stdout.substring(this.processedOutputLength);
|
|
641
|
+
this.feedIdleDetectorOutput(newOutput);
|
|
589
642
|
this.processedOutputLength = stdout.length;
|
|
590
643
|
// Stream new output to daemon for dashboard log viewing
|
|
591
644
|
// Use filtered output to exclude thinking blocks and relay commands
|
|
@@ -713,8 +766,9 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
713
766
|
*/
|
|
714
767
|
sendRelayCommand(cmd) {
|
|
715
768
|
const msgHash = `${cmd.to}:${cmd.body}`;
|
|
716
|
-
// Permanent dedup - never send the same message twice
|
|
769
|
+
// Permanent dedup - never send the same message twice
|
|
717
770
|
if (this.sentMessageHashes.has(msgHash)) {
|
|
771
|
+
this.logStderr(`[DEDUP] Skipped duplicate message to ${cmd.to} (hash already sent)`);
|
|
718
772
|
return;
|
|
719
773
|
}
|
|
720
774
|
// If client not ready, queue for later and return
|
|
@@ -950,20 +1004,23 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
950
1004
|
});
|
|
951
1005
|
}
|
|
952
1006
|
/**
|
|
953
|
-
* Execute spawn via API (if dashboardPort set) or callback
|
|
1007
|
+
* Execute spawn via API (if dashboardPort set) or callback.
|
|
1008
|
+
* After spawning, waits for the agent to come online and sends the task via relay.
|
|
954
1009
|
*/
|
|
955
1010
|
async executeSpawn(name, cli, task) {
|
|
1011
|
+
let spawned = false;
|
|
956
1012
|
if (this.config.dashboardPort) {
|
|
957
1013
|
// Use dashboard API for spawning (works from any context, no terminal required)
|
|
958
1014
|
try {
|
|
959
1015
|
const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawn`, {
|
|
960
1016
|
method: 'POST',
|
|
961
1017
|
headers: { 'Content-Type': 'application/json' },
|
|
962
|
-
body: JSON.stringify({ name, cli, task
|
|
1018
|
+
body: JSON.stringify({ name, cli }), // No task - we send it after agent is online
|
|
963
1019
|
});
|
|
964
1020
|
const result = await response.json();
|
|
965
1021
|
if (result.success) {
|
|
966
1022
|
this.logStderr(`Spawned ${name} via API`);
|
|
1023
|
+
spawned = true;
|
|
967
1024
|
}
|
|
968
1025
|
else {
|
|
969
1026
|
this.logStderr(`Spawn failed: ${result.error}`, true);
|
|
@@ -977,11 +1034,57 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
977
1034
|
// Fall back to callback
|
|
978
1035
|
try {
|
|
979
1036
|
await this.config.onSpawn(name, cli, task);
|
|
1037
|
+
spawned = true;
|
|
980
1038
|
}
|
|
981
1039
|
catch (err) {
|
|
982
1040
|
this.logStderr(`Spawn failed: ${err.message}`, true);
|
|
983
1041
|
}
|
|
984
1042
|
}
|
|
1043
|
+
// If spawn succeeded and we have a task, wait for agent to come online and send it
|
|
1044
|
+
if (spawned && task && task.trim() && this.config.dashboardPort) {
|
|
1045
|
+
await this.waitAndSendTask(name, task);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Wait for a spawned agent to come online, then send the task via relay.
|
|
1050
|
+
* Uses the wrapper's own relay client so the message comes "from" this agent,
|
|
1051
|
+
* not from the dashboard's relay client.
|
|
1052
|
+
*/
|
|
1053
|
+
async waitAndSendTask(agentName, task) {
|
|
1054
|
+
const maxWaitMs = 30000;
|
|
1055
|
+
const pollIntervalMs = 500;
|
|
1056
|
+
const startTime = Date.now();
|
|
1057
|
+
this.logStderr(`Waiting for ${agentName} to come online...`);
|
|
1058
|
+
// Poll for agent to be online using dedicated status endpoint
|
|
1059
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
1060
|
+
try {
|
|
1061
|
+
const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/agents/${encodeURIComponent(agentName)}/online`);
|
|
1062
|
+
const data = await response.json();
|
|
1063
|
+
if (data.online) {
|
|
1064
|
+
this.logStderr(`${agentName} is online, sending task...`);
|
|
1065
|
+
// Send task directly via our relay client (not dashboard API)
|
|
1066
|
+
// This ensures the message comes "from" this agent, not from _DashboardUI
|
|
1067
|
+
if (this.client.state === 'READY') {
|
|
1068
|
+
const sent = this.client.sendMessage(agentName, task, 'message');
|
|
1069
|
+
if (sent) {
|
|
1070
|
+
this.logStderr(`Task sent to ${agentName}`);
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
this.logStderr(`Failed to send task to ${agentName}: sendMessage returned false`, true);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
this.logStderr(`Failed to send task to ${agentName}: relay client not ready (state: ${this.client.state})`, true);
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
catch (err) {
|
|
1083
|
+
// Ignore poll errors, keep trying
|
|
1084
|
+
}
|
|
1085
|
+
await sleep(pollIntervalMs);
|
|
1086
|
+
}
|
|
1087
|
+
this.logStderr(`Timeout waiting for ${agentName} to come online`, true);
|
|
985
1088
|
}
|
|
986
1089
|
/**
|
|
987
1090
|
* Execute release via API (if dashboardPort set) or callback
|
|
@@ -1028,6 +1131,10 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1028
1131
|
// Only process if we have API or callbacks configured
|
|
1029
1132
|
const canSpawn = this.config.dashboardPort || this.config.onSpawn;
|
|
1030
1133
|
const canRelease = this.config.dashboardPort || this.config.onRelease;
|
|
1134
|
+
// Debug: Log spawn capability status
|
|
1135
|
+
if (content.includes('->relay:spawn')) {
|
|
1136
|
+
this.logStderr(`[spawn-debug] canSpawn=${!!canSpawn} dashboardPort=${this.config.dashboardPort} hasOnSpawn=${!!this.config.onSpawn}`);
|
|
1137
|
+
}
|
|
1031
1138
|
if (!canSpawn && !canRelease)
|
|
1032
1139
|
return;
|
|
1033
1140
|
const lines = content.split('\n');
|
|
@@ -1038,6 +1145,12 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1038
1145
|
let trimmed = line.trim();
|
|
1039
1146
|
// Strip common line prefixes (bullets, prompts) before checking for commands
|
|
1040
1147
|
trimmed = trimmed.replace(linePrefixPattern, '');
|
|
1148
|
+
// Fix for over-stripping: the linePrefixPattern includes - and > characters,
|
|
1149
|
+
// which can accidentally strip the -> from ->relay:spawn, leaving just relay:spawn.
|
|
1150
|
+
// If we detect this happened, restore the -> prefix.
|
|
1151
|
+
if (/^(relay|thinking|continuity):/.test(trimmed)) {
|
|
1152
|
+
trimmed = '->' + trimmed;
|
|
1153
|
+
}
|
|
1041
1154
|
// If we're in fenced spawn mode, accumulate lines until we see >>>
|
|
1042
1155
|
if (this.pendingFencedSpawn) {
|
|
1043
1156
|
// Check for fence close (>>> at end of line or on its own line)
|
|
@@ -1177,7 +1290,8 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1177
1290
|
this.checkForInjectionOpportunity();
|
|
1178
1291
|
}
|
|
1179
1292
|
/**
|
|
1180
|
-
* Check if we should inject a message
|
|
1293
|
+
* Check if we should inject a message.
|
|
1294
|
+
* Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
|
|
1181
1295
|
*/
|
|
1182
1296
|
checkForInjectionOpportunity() {
|
|
1183
1297
|
if (this.messageQueue.length === 0)
|
|
@@ -1186,9 +1300,10 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1186
1300
|
return;
|
|
1187
1301
|
if (!this.running)
|
|
1188
1302
|
return;
|
|
1189
|
-
//
|
|
1190
|
-
const
|
|
1191
|
-
if (
|
|
1303
|
+
// Use universal idle detector for more reliable detection (inherited from BaseWrapper)
|
|
1304
|
+
const idleResult = this.checkIdleForInjection();
|
|
1305
|
+
if (!idleResult.isIdle) {
|
|
1306
|
+
// Not idle yet, retry later
|
|
1192
1307
|
const retryMs = this.config.injectRetryMs ?? 500;
|
|
1193
1308
|
setTimeout(() => this.checkForInjectionOpportunity(), retryMs);
|
|
1194
1309
|
return;
|
|
@@ -1204,19 +1319,16 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1204
1319
|
if (!msg)
|
|
1205
1320
|
return;
|
|
1206
1321
|
this.isInjecting = true;
|
|
1207
|
-
this.logStderr(`Injecting message from ${msg.from} (cli: ${this.cliType})`);
|
|
1208
1322
|
try {
|
|
1209
1323
|
const shortId = msg.messageId.substring(0, 8);
|
|
1210
1324
|
// Wait for input to be clear before injecting
|
|
1211
1325
|
// If input is not clear (human typing), re-queue and try later - never clear forcefully!
|
|
1212
|
-
// Fix for agent-relay-j9z: forceful clearing destroys human input in progress
|
|
1213
1326
|
const waitTimeoutMs = this.config.inputWaitTimeoutMs ?? 5000;
|
|
1214
1327
|
const waitPollMs = this.config.inputWaitPollMs ?? 200;
|
|
1215
1328
|
const inputClear = await this.waitForClearInput(waitTimeoutMs, waitPollMs);
|
|
1216
1329
|
if (!inputClear) {
|
|
1217
1330
|
// Input still has text after timeout - DON'T clear forcefully, re-queue instead
|
|
1218
|
-
|
|
1219
|
-
this.logStderr('Input not clear after waiting, re-queuing injection to preserve human input');
|
|
1331
|
+
this.logStderr('Input not clear, re-queuing injection');
|
|
1220
1332
|
this.messageQueue.unshift(msg);
|
|
1221
1333
|
this.isInjecting = false;
|
|
1222
1334
|
setTimeout(() => this.checkForInjectionOpportunity(), this.config.injectRetryMs ?? 1000);
|
|
@@ -1268,7 +1380,9 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1268
1380
|
}
|
|
1269
1381
|
},
|
|
1270
1382
|
performInjection: async (inj) => {
|
|
1271
|
-
|
|
1383
|
+
// Use send-keys -l (literal) instead of paste-buffer
|
|
1384
|
+
// paste-buffer causes issues where Claude shows "[Pasted text]" but content doesn't appear
|
|
1385
|
+
await this.sendKeysLiteral(inj);
|
|
1272
1386
|
await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
|
|
1273
1387
|
await this.sendKeys('Enter');
|
|
1274
1388
|
},
|
|
@@ -1319,7 +1433,15 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1319
1433
|
* Send special keys to tmux
|
|
1320
1434
|
*/
|
|
1321
1435
|
async sendKeys(keys) {
|
|
1322
|
-
|
|
1436
|
+
const cmd = `"${this.tmuxPath}" send-keys -t ${this.sessionName} ${keys}`;
|
|
1437
|
+
try {
|
|
1438
|
+
await execAsync(cmd);
|
|
1439
|
+
this.logStderr(`[sendKeys] Sent: ${keys}`);
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
this.logStderr(`[sendKeys] Failed to send ${keys}: ${err.message}`, true);
|
|
1443
|
+
throw err;
|
|
1444
|
+
}
|
|
1323
1445
|
}
|
|
1324
1446
|
/**
|
|
1325
1447
|
* Send literal text to tmux
|
|
@@ -1334,7 +1456,14 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1334
1456
|
.replace(/\$/g, '\\$')
|
|
1335
1457
|
.replace(/`/g, '\\`')
|
|
1336
1458
|
.replace(/!/g, '\\!');
|
|
1337
|
-
|
|
1459
|
+
try {
|
|
1460
|
+
await execAsync(`"${this.tmuxPath}" send-keys -t ${this.sessionName} -l "${escaped}"`);
|
|
1461
|
+
this.logStderr(`[sendKeysLiteral] Sent ${text.length} chars`);
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
this.logStderr(`[sendKeysLiteral] Failed: ${err.message}`, true);
|
|
1465
|
+
throw err;
|
|
1466
|
+
}
|
|
1338
1467
|
}
|
|
1339
1468
|
/**
|
|
1340
1469
|
* Paste text using tmux buffer with optional bracketed paste to avoid interleaving with ongoing output.
|
|
@@ -1350,15 +1479,9 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
1350
1479
|
.replace(/`/g, '\\`')
|
|
1351
1480
|
.replace(/!/g, '\\!');
|
|
1352
1481
|
// Set tmux buffer then paste
|
|
1353
|
-
|
|
1354
|
-
await execAsync(
|
|
1355
|
-
|
|
1356
|
-
if (useBracketedPaste) {
|
|
1357
|
-
await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName} -p`);
|
|
1358
|
-
}
|
|
1359
|
-
else {
|
|
1360
|
-
await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName}`);
|
|
1361
|
-
}
|
|
1482
|
+
const setBufferCmd = `"${this.tmuxPath}" set-buffer -- "${escaped}"`;
|
|
1483
|
+
await execAsync(setBufferCmd);
|
|
1484
|
+
await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName}`);
|
|
1362
1485
|
}
|
|
1363
1486
|
/**
|
|
1364
1487
|
* Reset session-specific state for wrapper reuse.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Real-time agent-to-agent communication system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"clean": "rm -rf dist",
|
|
42
42
|
"db:generate": "drizzle-kit generate",
|
|
43
43
|
"db:migrate": "drizzle-kit migrate",
|
|
44
|
+
"db:migrate:run": "node scripts/run-migrations.js",
|
|
45
|
+
"db:migrate:verify": "node scripts/verify-schema.js",
|
|
44
46
|
"db:push": "drizzle-kit push",
|
|
45
47
|
"db:studio": "drizzle-kit studio",
|
|
46
48
|
"services:up": "docker compose -f docker-compose.dev.yml up -d postgres redis && echo '✓ Postgres and Redis running'",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run database migrations (standalone)
|
|
4
|
+
*
|
|
5
|
+
* This script is used in CI to verify migrations run successfully.
|
|
6
|
+
* It connects to the database and runs all pending migrations.
|
|
7
|
+
*
|
|
8
|
+
* This is a standalone script that doesn't depend on the cloud config,
|
|
9
|
+
* so it only requires DATABASE_URL to run.
|
|
10
|
+
*
|
|
11
|
+
* Usage: DATABASE_URL=postgres://... node scripts/run-migrations.js
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import pg from 'pg';
|
|
15
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
16
|
+
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
17
|
+
|
|
18
|
+
const { Pool } = pg;
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
console.log('Starting database migrations...');
|
|
22
|
+
console.log(`Database URL: ${process.env.DATABASE_URL?.replace(/:[^:@]+@/, ':***@') || 'not set'}`);
|
|
23
|
+
|
|
24
|
+
if (!process.env.DATABASE_URL) {
|
|
25
|
+
console.error('ERROR: DATABASE_URL environment variable is required');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
30
|
+
const db = drizzle(pool);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await migrate(db, { migrationsFolder: './src/cloud/db/migrations' });
|
|
34
|
+
console.log('All migrations completed successfully');
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Migration failed:', error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
} finally {
|
|
39
|
+
await pool.end();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify database schema after migrations
|
|
4
|
+
*
|
|
5
|
+
* This script verifies that all expected tables exist after migrations.
|
|
6
|
+
* It dynamically reads table definitions from the schema to avoid hardcoding.
|
|
7
|
+
*
|
|
8
|
+
* Usage: DATABASE_URL=postgres://... node scripts/verify-schema.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import pg from 'pg';
|
|
12
|
+
import * as schema from '../dist/cloud/db/schema.js';
|
|
13
|
+
|
|
14
|
+
const { Pool } = pg;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract table names from the schema module.
|
|
18
|
+
* Drizzle pgTable objects store their name in Symbol.for('drizzle:Name').
|
|
19
|
+
*/
|
|
20
|
+
function getTablesFromSchema() {
|
|
21
|
+
const tables = [];
|
|
22
|
+
const drizzleNameSymbol = Symbol.for('drizzle:Name');
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
25
|
+
// Skip relation definitions (they end with 'Relations')
|
|
26
|
+
if (key.endsWith('Relations')) continue;
|
|
27
|
+
|
|
28
|
+
// Drizzle tables have the table name in a Symbol
|
|
29
|
+
if (value && typeof value === 'object' && value[drizzleNameSymbol]) {
|
|
30
|
+
tables.push(value[drizzleNameSymbol]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return tables;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Dynamically get tables from schema
|
|
37
|
+
const SCHEMA_TABLES = getTablesFromSchema();
|
|
38
|
+
const EXPECTED_TABLES = [...SCHEMA_TABLES];
|
|
39
|
+
|
|
40
|
+
// Key columns to spot-check (subset of critical columns)
|
|
41
|
+
const EXPECTED_COLUMNS = {
|
|
42
|
+
users: ['id', 'email', 'created_at'],
|
|
43
|
+
workspaces: ['id', 'user_id', 'name', 'status'],
|
|
44
|
+
linked_daemons: ['id', 'user_id', 'workspace_id', 'status'],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
console.log('Verifying database schema...\n');
|
|
49
|
+
|
|
50
|
+
if (!process.env.DATABASE_URL) {
|
|
51
|
+
console.error('ERROR: DATABASE_URL environment variable is required');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`Found ${SCHEMA_TABLES.length} tables in schema.ts:`);
|
|
56
|
+
console.log(` ${SCHEMA_TABLES.join(', ')}\n`);
|
|
57
|
+
|
|
58
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Get all tables in the public schema
|
|
62
|
+
const tablesResult = await pool.query(`
|
|
63
|
+
SELECT table_name
|
|
64
|
+
FROM information_schema.tables
|
|
65
|
+
WHERE table_schema = 'public'
|
|
66
|
+
ORDER BY table_name
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
const existingTables = tablesResult.rows.map((r) => r.table_name);
|
|
70
|
+
console.log('Existing tables:', existingTables.join(', '));
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
// Check for missing tables
|
|
74
|
+
const missingTables = EXPECTED_TABLES.filter((t) => !existingTables.includes(t));
|
|
75
|
+
if (missingTables.length > 0) {
|
|
76
|
+
console.error('MISSING TABLES:', missingTables.join(', '));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.log(`All ${EXPECTED_TABLES.length} expected tables exist`);
|
|
80
|
+
|
|
81
|
+
// Verify key columns
|
|
82
|
+
console.log('\nVerifying key columns...');
|
|
83
|
+
for (const [table, columns] of Object.entries(EXPECTED_COLUMNS)) {
|
|
84
|
+
const columnsResult = await pool.query(
|
|
85
|
+
`
|
|
86
|
+
SELECT column_name
|
|
87
|
+
FROM information_schema.columns
|
|
88
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
89
|
+
`,
|
|
90
|
+
[table]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const existingColumns = columnsResult.rows.map((r) => r.column_name);
|
|
94
|
+
const missingColumns = columns.filter((c) => !existingColumns.includes(c));
|
|
95
|
+
|
|
96
|
+
if (missingColumns.length > 0) {
|
|
97
|
+
console.error(`Table '${table}' missing columns: ${missingColumns.join(', ')}`);
|
|
98
|
+
console.error(`Existing columns: ${existingColumns.join(', ')}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log(` ${table}: OK (${columns.length} key columns verified)`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check migration history (table may be in public or drizzle schema)
|
|
105
|
+
try {
|
|
106
|
+
// Try public schema first, then drizzle schema
|
|
107
|
+
let migrationsResult;
|
|
108
|
+
try {
|
|
109
|
+
migrationsResult = await pool.query(`
|
|
110
|
+
SELECT id, hash, created_at FROM public.__drizzle_migrations ORDER BY created_at
|
|
111
|
+
`);
|
|
112
|
+
} catch {
|
|
113
|
+
migrationsResult = await pool.query(`
|
|
114
|
+
SELECT id, hash, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
console.log(`\nMigration history: ${migrationsResult.rows.length} migrations applied`);
|
|
118
|
+
for (const row of migrationsResult.rows) {
|
|
119
|
+
console.log(` - ${row.id} (${new Date(Number(row.created_at)).toISOString()})`);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
console.log('\nMigration history: (table not found, but migrations ran successfully)');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log('\nSchema verification passed!');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Schema verification failed:', error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
} finally {
|
|
130
|
+
await pool.end();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main();
|