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
|
@@ -20,6 +20,7 @@ import type { ParsedCommand, ParsedSummary } from './parser.js';
|
|
|
20
20
|
import type { SendPayload, SendMeta, SpeakOnTrigger } from '../protocol/types.js';
|
|
21
21
|
import { type QueuedMessage, type InjectionMetrics, type CliType } from './shared.js';
|
|
22
22
|
import { type ContinuityManager } from '../continuity/index.js';
|
|
23
|
+
import { UniversalIdleDetector } from './idle-detector.js';
|
|
23
24
|
/**
|
|
24
25
|
* Base configuration shared by all wrapper types
|
|
25
26
|
*/
|
|
@@ -55,6 +56,14 @@ export interface BaseWrapperConfig {
|
|
|
55
56
|
/** Shadow configuration */
|
|
56
57
|
shadowOf?: string;
|
|
57
58
|
shadowSpeakOn?: SpeakOnTrigger[];
|
|
59
|
+
/** Milliseconds of idle time before injection is allowed (default: 1500) */
|
|
60
|
+
idleBeforeInjectMs?: number;
|
|
61
|
+
/** Confidence threshold for idle detection (0-1, default: 0.7) */
|
|
62
|
+
idleConfidenceThreshold?: number;
|
|
63
|
+
/** Skip initial instruction injection (when using --append-system-prompt) */
|
|
64
|
+
skipInstructions?: boolean;
|
|
65
|
+
/** Skip continuity loading (for spawned agents that don't need session recovery) */
|
|
66
|
+
skipContinuity?: boolean;
|
|
58
67
|
}
|
|
59
68
|
/**
|
|
60
69
|
* Abstract base class for agent wrappers
|
|
@@ -86,6 +95,7 @@ export declare abstract class BaseWrapper extends EventEmitter {
|
|
|
86
95
|
completedTasks?: string[];
|
|
87
96
|
};
|
|
88
97
|
protected lastSummaryRawContent: string;
|
|
98
|
+
protected idleDetector: UniversalIdleDetector;
|
|
89
99
|
constructor(config: BaseWrapperConfig);
|
|
90
100
|
/** Start the agent process */
|
|
91
101
|
abstract start(): Promise<void>;
|
|
@@ -102,6 +112,36 @@ export declare abstract class BaseWrapper extends EventEmitter {
|
|
|
102
112
|
successRate: number;
|
|
103
113
|
};
|
|
104
114
|
get pendingMessageCount(): number;
|
|
115
|
+
/**
|
|
116
|
+
* Set the PID for process state inspection (Linux only).
|
|
117
|
+
* Call this after the agent process is started.
|
|
118
|
+
*/
|
|
119
|
+
protected setIdleDetectorPid(pid: number): void;
|
|
120
|
+
/**
|
|
121
|
+
* Feed output to the idle detector.
|
|
122
|
+
* Call this whenever new output is received from the agent.
|
|
123
|
+
*/
|
|
124
|
+
protected feedIdleDetectorOutput(output: string): void;
|
|
125
|
+
/**
|
|
126
|
+
* Check if the agent is idle and ready for injection.
|
|
127
|
+
* Returns idle state with confidence signals.
|
|
128
|
+
*/
|
|
129
|
+
protected checkIdleForInjection(): {
|
|
130
|
+
isIdle: boolean;
|
|
131
|
+
confidence: number;
|
|
132
|
+
signals: Array<{
|
|
133
|
+
source: string;
|
|
134
|
+
confidence: number;
|
|
135
|
+
}>;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Wait for the agent to become idle.
|
|
139
|
+
* Returns when idle or after timeout.
|
|
140
|
+
*/
|
|
141
|
+
protected waitForIdleState(timeoutMs?: number, pollMs?: number): Promise<{
|
|
142
|
+
isIdle: boolean;
|
|
143
|
+
confidence: number;
|
|
144
|
+
}>;
|
|
105
145
|
/**
|
|
106
146
|
* Handle incoming message from relay
|
|
107
147
|
*/
|
|
@@ -18,7 +18,9 @@ import { EventEmitter } from 'node:events';
|
|
|
18
18
|
import { RelayClient } from './client.js';
|
|
19
19
|
import { isPlaceholderTarget } from './parser.js';
|
|
20
20
|
import { getDefaultRelayPrefix, detectCliType, createInjectionMetrics, } from './shared.js';
|
|
21
|
+
import { DEFAULT_IDLE_BEFORE_INJECT_MS, DEFAULT_IDLE_CONFIDENCE_THRESHOLD, } from '../config/relay-config.js';
|
|
21
22
|
import { getContinuityManager, parseContinuityCommand, hasContinuityCommand, } from '../continuity/index.js';
|
|
23
|
+
import { UniversalIdleDetector } from './idle-detector.js';
|
|
22
24
|
/**
|
|
23
25
|
* Abstract base class for agent wrappers
|
|
24
26
|
*/
|
|
@@ -45,6 +47,8 @@ export class BaseWrapper extends EventEmitter {
|
|
|
45
47
|
sessionEndProcessed = false;
|
|
46
48
|
sessionEndData;
|
|
47
49
|
lastSummaryRawContent = '';
|
|
50
|
+
// Universal idle detection (shared across all wrapper types)
|
|
51
|
+
idleDetector;
|
|
48
52
|
constructor(config) {
|
|
49
53
|
super();
|
|
50
54
|
this.config = config;
|
|
@@ -59,8 +63,15 @@ export class BaseWrapper extends EventEmitter {
|
|
|
59
63
|
workingDirectory: config.cwd,
|
|
60
64
|
quiet: true,
|
|
61
65
|
});
|
|
62
|
-
// Initialize continuity manager
|
|
63
|
-
|
|
66
|
+
// Initialize continuity manager (skip for spawned agents that don't need session recovery)
|
|
67
|
+
if (!config.skipContinuity) {
|
|
68
|
+
this.continuity = getContinuityManager({ defaultCli: this.cliType });
|
|
69
|
+
}
|
|
70
|
+
// Initialize universal idle detector for robust injection timing
|
|
71
|
+
this.idleDetector = new UniversalIdleDetector({
|
|
72
|
+
minSilenceMs: config.idleBeforeInjectMs ?? DEFAULT_IDLE_BEFORE_INJECT_MS,
|
|
73
|
+
confidenceThreshold: config.idleConfidenceThreshold ?? DEFAULT_IDLE_CONFIDENCE_THRESHOLD,
|
|
74
|
+
});
|
|
64
75
|
// Set up message handler
|
|
65
76
|
this.client.onMessage = (from, payload, messageId, meta, originalTo) => {
|
|
66
77
|
this.handleIncomingMessage(from, payload, messageId, meta, originalTo);
|
|
@@ -93,6 +104,39 @@ export class BaseWrapper extends EventEmitter {
|
|
|
93
104
|
return this.messageQueue.length;
|
|
94
105
|
}
|
|
95
106
|
// =========================================================================
|
|
107
|
+
// Idle detection (shared across all wrapper types)
|
|
108
|
+
// =========================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Set the PID for process state inspection (Linux only).
|
|
111
|
+
* Call this after the agent process is started.
|
|
112
|
+
*/
|
|
113
|
+
setIdleDetectorPid(pid) {
|
|
114
|
+
this.idleDetector.setPid(pid);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Feed output to the idle detector.
|
|
118
|
+
* Call this whenever new output is received from the agent.
|
|
119
|
+
*/
|
|
120
|
+
feedIdleDetectorOutput(output) {
|
|
121
|
+
this.idleDetector.onOutput(output);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Check if the agent is idle and ready for injection.
|
|
125
|
+
* Returns idle state with confidence signals.
|
|
126
|
+
*/
|
|
127
|
+
checkIdleForInjection() {
|
|
128
|
+
return this.idleDetector.checkIdle({
|
|
129
|
+
minSilenceMs: this.config.idleBeforeInjectMs ?? 1500,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Wait for the agent to become idle.
|
|
134
|
+
* Returns when idle or after timeout.
|
|
135
|
+
*/
|
|
136
|
+
async waitForIdleState(timeoutMs = 30000, pollMs = 200) {
|
|
137
|
+
return this.idleDetector.waitForIdle(timeoutMs, pollMs);
|
|
138
|
+
}
|
|
139
|
+
// =========================================================================
|
|
96
140
|
// Message handling
|
|
97
141
|
// =========================================================================
|
|
98
142
|
/**
|
|
@@ -126,12 +170,16 @@ export class BaseWrapper extends EventEmitter {
|
|
|
126
170
|
*/
|
|
127
171
|
sendRelayCommand(cmd) {
|
|
128
172
|
// Validate target
|
|
129
|
-
if (isPlaceholderTarget(cmd.to))
|
|
173
|
+
if (isPlaceholderTarget(cmd.to)) {
|
|
174
|
+
console.error(`[base-wrapper] Skipped message - placeholder target: ${cmd.to}`);
|
|
130
175
|
return;
|
|
176
|
+
}
|
|
131
177
|
// Create hash for deduplication (use first 100 chars of body)
|
|
132
178
|
const hash = `${cmd.to}:${cmd.body.substring(0, 100)}`;
|
|
133
|
-
if (this.sentMessageHashes.has(hash))
|
|
179
|
+
if (this.sentMessageHashes.has(hash)) {
|
|
180
|
+
console.error(`[base-wrapper] Skipped duplicate message to ${cmd.to}`);
|
|
134
181
|
return;
|
|
182
|
+
}
|
|
135
183
|
this.sentMessageHashes.add(hash);
|
|
136
184
|
// Limit hash set size
|
|
137
185
|
if (this.sentMessageHashes.size > 500) {
|
|
@@ -140,9 +188,15 @@ export class BaseWrapper extends EventEmitter {
|
|
|
140
188
|
this.sentMessageHashes.delete(oldest);
|
|
141
189
|
}
|
|
142
190
|
// Only send if client ready
|
|
143
|
-
if (this.client.state !== 'READY')
|
|
191
|
+
if (this.client.state !== 'READY') {
|
|
192
|
+
console.error(`[base-wrapper] Skipped message to ${cmd.to} - client not ready (state: ${this.client.state})`);
|
|
144
193
|
return;
|
|
145
|
-
|
|
194
|
+
}
|
|
195
|
+
console.log(`[base-wrapper] Sending message to ${cmd.to}: "${cmd.body.substring(0, 50)}..."`);
|
|
196
|
+
const sent = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread);
|
|
197
|
+
if (!sent) {
|
|
198
|
+
console.error(`[base-wrapper] Failed to send message to ${cmd.to} - sendMessage returned false`);
|
|
199
|
+
}
|
|
146
200
|
}
|
|
147
201
|
// =========================================================================
|
|
148
202
|
// Spawn/release handling
|
package/dist/wrapper/client.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Relay Client
|
|
3
3
|
* Connects to the daemon and handles message sending/receiving.
|
|
4
|
+
*
|
|
5
|
+
* Optimizations:
|
|
6
|
+
* - Monotonic ID generation (faster than UUID)
|
|
7
|
+
* - Write coalescing (batch socket writes)
|
|
8
|
+
* - Circular dedup cache (O(1) eviction)
|
|
4
9
|
*/
|
|
5
10
|
import { type SendPayload, type SendMeta, type PayloadKind, type SpeakOnTrigger, type EntityType } from '../protocol/types.js';
|
|
6
11
|
export type ClientState = 'DISCONNECTED' | 'CONNECTING' | 'HANDSHAKING' | 'READY' | 'BACKOFF';
|
|
@@ -41,9 +46,9 @@ export declare class RelayClient {
|
|
|
41
46
|
private reconnectDelay;
|
|
42
47
|
private reconnectTimer?;
|
|
43
48
|
private _destroyed;
|
|
44
|
-
private
|
|
45
|
-
private
|
|
46
|
-
private
|
|
49
|
+
private dedupeCache;
|
|
50
|
+
private writeQueue;
|
|
51
|
+
private writeScheduled;
|
|
47
52
|
/**
|
|
48
53
|
* Handler for incoming messages.
|
|
49
54
|
* @param from - The sender agent name
|
|
@@ -123,6 +128,10 @@ export declare class RelayClient {
|
|
|
123
128
|
private setState;
|
|
124
129
|
private sendHello;
|
|
125
130
|
private send;
|
|
131
|
+
/**
|
|
132
|
+
* Flush all queued writes in a single syscall.
|
|
133
|
+
*/
|
|
134
|
+
private flushWrites;
|
|
126
135
|
private handleData;
|
|
127
136
|
private processFrame;
|
|
128
137
|
private handleWelcome;
|
|
@@ -133,7 +142,8 @@ export declare class RelayClient {
|
|
|
133
142
|
private handleError;
|
|
134
143
|
private scheduleReconnect;
|
|
135
144
|
/**
|
|
136
|
-
*
|
|
145
|
+
* Check if message was already delivered (deduplication).
|
|
146
|
+
* Uses circular buffer for O(1) eviction.
|
|
137
147
|
* @returns true if the message has already been seen.
|
|
138
148
|
*/
|
|
139
149
|
private markDelivered;
|
package/dist/wrapper/client.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Relay Client
|
|
3
3
|
* Connects to the daemon and handles message sending/receiving.
|
|
4
|
+
*
|
|
5
|
+
* Optimizations:
|
|
6
|
+
* - Monotonic ID generation (faster than UUID)
|
|
7
|
+
* - Write coalescing (batch socket writes)
|
|
8
|
+
* - Circular dedup cache (O(1) eviction)
|
|
4
9
|
*/
|
|
5
10
|
import net from 'node:net';
|
|
6
|
-
import {
|
|
11
|
+
import { generateId } from '../utils/id-generator.js';
|
|
7
12
|
import { PROTOCOL_VERSION, } from '../protocol/types.js';
|
|
8
|
-
import {
|
|
13
|
+
import { encodeFrameLegacy, FrameParser } from '../protocol/framing.js';
|
|
9
14
|
import { DEFAULT_SOCKET_PATH } from '../daemon/server.js';
|
|
10
15
|
const DEFAULT_CLIENT_CONFIG = {
|
|
11
16
|
socketPath: DEFAULT_SOCKET_PATH,
|
|
@@ -17,6 +22,40 @@ const DEFAULT_CLIENT_CONFIG = {
|
|
|
17
22
|
reconnectDelayMs: 100,
|
|
18
23
|
reconnectMaxDelayMs: 30000,
|
|
19
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Circular buffer for O(1) deduplication with bounded memory.
|
|
27
|
+
*/
|
|
28
|
+
class CircularDedupeCache {
|
|
29
|
+
ids = new Set();
|
|
30
|
+
ring;
|
|
31
|
+
head = 0;
|
|
32
|
+
capacity;
|
|
33
|
+
constructor(capacity = 2000) {
|
|
34
|
+
this.capacity = capacity;
|
|
35
|
+
this.ring = new Array(capacity);
|
|
36
|
+
}
|
|
37
|
+
/** Returns true if duplicate (already seen) */
|
|
38
|
+
check(id) {
|
|
39
|
+
if (this.ids.has(id))
|
|
40
|
+
return true;
|
|
41
|
+
// Evict oldest if at capacity
|
|
42
|
+
if (this.ids.size >= this.capacity) {
|
|
43
|
+
const oldest = this.ring[this.head];
|
|
44
|
+
if (oldest)
|
|
45
|
+
this.ids.delete(oldest);
|
|
46
|
+
}
|
|
47
|
+
// Add new ID
|
|
48
|
+
this.ring[this.head] = id;
|
|
49
|
+
this.ids.add(id);
|
|
50
|
+
this.head = (this.head + 1) % this.capacity;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
clear() {
|
|
54
|
+
this.ids.clear();
|
|
55
|
+
this.ring = new Array(this.capacity);
|
|
56
|
+
this.head = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
20
59
|
export class RelayClient {
|
|
21
60
|
config;
|
|
22
61
|
socket;
|
|
@@ -28,9 +67,11 @@ export class RelayClient {
|
|
|
28
67
|
reconnectDelay;
|
|
29
68
|
reconnectTimer;
|
|
30
69
|
_destroyed = false;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
// Circular dedup cache (O(1) eviction vs O(n) array shift)
|
|
71
|
+
dedupeCache = new CircularDedupeCache(2000);
|
|
72
|
+
// Write coalescing: batch multiple writes into single syscall
|
|
73
|
+
writeQueue = [];
|
|
74
|
+
writeScheduled = false;
|
|
34
75
|
// Event handlers
|
|
35
76
|
/**
|
|
36
77
|
* Handler for incoming messages.
|
|
@@ -46,6 +87,7 @@ export class RelayClient {
|
|
|
46
87
|
constructor(config = {}) {
|
|
47
88
|
this.config = { ...DEFAULT_CLIENT_CONFIG, ...config };
|
|
48
89
|
this.parser = new FrameParser();
|
|
90
|
+
this.parser.setLegacyMode(true); // Use 4-byte header for backwards compatibility
|
|
49
91
|
this.reconnectDelay = this.config.reconnectDelayMs;
|
|
50
92
|
}
|
|
51
93
|
get state() {
|
|
@@ -124,7 +166,7 @@ export class RelayClient {
|
|
|
124
166
|
this.send({
|
|
125
167
|
v: PROTOCOL_VERSION,
|
|
126
168
|
type: 'BYE',
|
|
127
|
-
id:
|
|
169
|
+
id: generateId(),
|
|
128
170
|
ts: Date.now(),
|
|
129
171
|
payload: {},
|
|
130
172
|
});
|
|
@@ -156,7 +198,7 @@ export class RelayClient {
|
|
|
156
198
|
const envelope = {
|
|
157
199
|
v: PROTOCOL_VERSION,
|
|
158
200
|
type: 'SEND',
|
|
159
|
-
id:
|
|
201
|
+
id: generateId(),
|
|
160
202
|
ts: Date.now(),
|
|
161
203
|
to,
|
|
162
204
|
payload: {
|
|
@@ -184,7 +226,7 @@ export class RelayClient {
|
|
|
184
226
|
return this.send({
|
|
185
227
|
v: PROTOCOL_VERSION,
|
|
186
228
|
type: 'SUBSCRIBE',
|
|
187
|
-
id:
|
|
229
|
+
id: generateId(),
|
|
188
230
|
ts: Date.now(),
|
|
189
231
|
topic,
|
|
190
232
|
payload: {},
|
|
@@ -199,7 +241,7 @@ export class RelayClient {
|
|
|
199
241
|
return this.send({
|
|
200
242
|
v: PROTOCOL_VERSION,
|
|
201
243
|
type: 'UNSUBSCRIBE',
|
|
202
|
-
id:
|
|
244
|
+
id: generateId(),
|
|
203
245
|
ts: Date.now(),
|
|
204
246
|
topic,
|
|
205
247
|
payload: {},
|
|
@@ -217,7 +259,7 @@ export class RelayClient {
|
|
|
217
259
|
return this.send({
|
|
218
260
|
v: PROTOCOL_VERSION,
|
|
219
261
|
type: 'SHADOW_BIND',
|
|
220
|
-
id:
|
|
262
|
+
id: generateId(),
|
|
221
263
|
ts: Date.now(),
|
|
222
264
|
payload: {
|
|
223
265
|
primaryAgent,
|
|
@@ -237,7 +279,7 @@ export class RelayClient {
|
|
|
237
279
|
return this.send({
|
|
238
280
|
v: PROTOCOL_VERSION,
|
|
239
281
|
type: 'SHADOW_UNBIND',
|
|
240
|
-
id:
|
|
282
|
+
id: generateId(),
|
|
241
283
|
ts: Date.now(),
|
|
242
284
|
payload: {
|
|
243
285
|
primaryAgent,
|
|
@@ -257,7 +299,7 @@ export class RelayClient {
|
|
|
257
299
|
const envelope = {
|
|
258
300
|
v: PROTOCOL_VERSION,
|
|
259
301
|
type: 'LOG',
|
|
260
|
-
id:
|
|
302
|
+
id: generateId(),
|
|
261
303
|
ts: Date.now(),
|
|
262
304
|
payload: {
|
|
263
305
|
data,
|
|
@@ -276,7 +318,7 @@ export class RelayClient {
|
|
|
276
318
|
const hello = {
|
|
277
319
|
v: PROTOCOL_VERSION,
|
|
278
320
|
type: 'HELLO',
|
|
279
|
-
id:
|
|
321
|
+
id: generateId(),
|
|
280
322
|
ts: Date.now(),
|
|
281
323
|
payload: {
|
|
282
324
|
agent: this.config.agentName,
|
|
@@ -303,8 +345,13 @@ export class RelayClient {
|
|
|
303
345
|
if (!this.socket)
|
|
304
346
|
return false;
|
|
305
347
|
try {
|
|
306
|
-
const frame =
|
|
307
|
-
this.
|
|
348
|
+
const frame = encodeFrameLegacy(envelope);
|
|
349
|
+
this.writeQueue.push(frame);
|
|
350
|
+
// Coalesce writes: schedule flush on next tick if not already scheduled
|
|
351
|
+
if (!this.writeScheduled) {
|
|
352
|
+
this.writeScheduled = true;
|
|
353
|
+
setImmediate(() => this.flushWrites());
|
|
354
|
+
}
|
|
308
355
|
return true;
|
|
309
356
|
}
|
|
310
357
|
catch (err) {
|
|
@@ -312,6 +359,23 @@ export class RelayClient {
|
|
|
312
359
|
return false;
|
|
313
360
|
}
|
|
314
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Flush all queued writes in a single syscall.
|
|
364
|
+
*/
|
|
365
|
+
flushWrites() {
|
|
366
|
+
this.writeScheduled = false;
|
|
367
|
+
if (this.writeQueue.length === 0 || !this.socket)
|
|
368
|
+
return;
|
|
369
|
+
if (this.writeQueue.length === 1) {
|
|
370
|
+
// Single frame - write directly (no concat needed)
|
|
371
|
+
this.socket.write(this.writeQueue[0]);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// Multiple frames - batch into single write
|
|
375
|
+
this.socket.write(Buffer.concat(this.writeQueue));
|
|
376
|
+
}
|
|
377
|
+
this.writeQueue = [];
|
|
378
|
+
}
|
|
315
379
|
handleData(data) {
|
|
316
380
|
try {
|
|
317
381
|
const frames = this.parser.push(data);
|
|
@@ -353,11 +417,12 @@ export class RelayClient {
|
|
|
353
417
|
}
|
|
354
418
|
}
|
|
355
419
|
handleDeliver(envelope) {
|
|
420
|
+
console.log(`[relay-client:${this.config.agentName}] Received DELIVER from ${envelope.from}: "${envelope.payload.body?.substring(0, 40)}..."`);
|
|
356
421
|
// Send ACK
|
|
357
422
|
this.send({
|
|
358
423
|
v: PROTOCOL_VERSION,
|
|
359
424
|
type: 'ACK',
|
|
360
|
-
id:
|
|
425
|
+
id: generateId(),
|
|
361
426
|
ts: Date.now(),
|
|
362
427
|
payload: {
|
|
363
428
|
ack_id: envelope.id,
|
|
@@ -366,6 +431,7 @@ export class RelayClient {
|
|
|
366
431
|
});
|
|
367
432
|
const duplicate = this.markDelivered(envelope.id);
|
|
368
433
|
if (duplicate) {
|
|
434
|
+
console.log(`[relay-client:${this.config.agentName}] Duplicate delivery, skipping`);
|
|
369
435
|
return;
|
|
370
436
|
}
|
|
371
437
|
// Notify handler
|
|
@@ -373,12 +439,15 @@ export class RelayClient {
|
|
|
373
439
|
if (this.onMessage && envelope.from) {
|
|
374
440
|
this.onMessage(envelope.from, envelope.payload, envelope.id, envelope.payload_meta, envelope.delivery.originalTo);
|
|
375
441
|
}
|
|
442
|
+
else {
|
|
443
|
+
console.log(`[relay-client:${this.config.agentName}] No onMessage handler or no from field`);
|
|
444
|
+
}
|
|
376
445
|
}
|
|
377
446
|
handlePing(envelope) {
|
|
378
447
|
this.send({
|
|
379
448
|
v: PROTOCOL_VERSION,
|
|
380
449
|
type: 'PONG',
|
|
381
|
-
id:
|
|
450
|
+
id: generateId(),
|
|
382
451
|
ts: Date.now(),
|
|
383
452
|
payload: envelope.payload ?? {},
|
|
384
453
|
});
|
|
@@ -435,23 +504,12 @@ export class RelayClient {
|
|
|
435
504
|
}, delay);
|
|
436
505
|
}
|
|
437
506
|
/**
|
|
438
|
-
*
|
|
507
|
+
* Check if message was already delivered (deduplication).
|
|
508
|
+
* Uses circular buffer for O(1) eviction.
|
|
439
509
|
* @returns true if the message has already been seen.
|
|
440
510
|
*/
|
|
441
511
|
markDelivered(id) {
|
|
442
|
-
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
this.deliveredIds.add(id);
|
|
446
|
-
this.deliveredOrder.push(id);
|
|
447
|
-
// Simple FIFO eviction to keep memory bounded
|
|
448
|
-
if (this.deliveredOrder.length > this.deliveredCacheLimit) {
|
|
449
|
-
const oldest = this.deliveredOrder.shift();
|
|
450
|
-
if (oldest) {
|
|
451
|
-
this.deliveredIds.delete(oldest);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return false;
|
|
512
|
+
return this.dedupeCache.check(id);
|
|
455
513
|
}
|
|
456
514
|
}
|
|
457
515
|
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UniversalIdleDetector - Detect when an agent is waiting for input
|
|
3
|
+
*
|
|
4
|
+
* Works across all CLI tools (Claude, Codex, Gemini, Aider, etc.) by combining:
|
|
5
|
+
* 1. Process state inspection via /proc/{pid}/stat (Linux, 95% confidence)
|
|
6
|
+
* 2. Output silence analysis (cross-platform, 60-80% confidence)
|
|
7
|
+
* 3. Natural ending detection (heuristic, 60% confidence)
|
|
8
|
+
*
|
|
9
|
+
* The hybrid approach ensures reliable idle detection regardless of CLI type.
|
|
10
|
+
*/
|
|
11
|
+
export interface IdleSignal {
|
|
12
|
+
source: 'process_state' | 'output_silence' | 'natural_ending';
|
|
13
|
+
confidence: number;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
details?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface IdleResult {
|
|
18
|
+
isIdle: boolean;
|
|
19
|
+
confidence: number;
|
|
20
|
+
signals: IdleSignal[];
|
|
21
|
+
}
|
|
22
|
+
export interface IdleDetectorConfig {
|
|
23
|
+
/** Minimum silence duration to consider for idle (ms) */
|
|
24
|
+
minSilenceMs?: number;
|
|
25
|
+
/** Output buffer size limit */
|
|
26
|
+
bufferLimit?: number;
|
|
27
|
+
/** Confidence threshold for idle detection (0-1) */
|
|
28
|
+
confidenceThreshold?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Universal idle detector for any CLI-based agent.
|
|
32
|
+
*/
|
|
33
|
+
export declare class UniversalIdleDetector {
|
|
34
|
+
private lastOutputTime;
|
|
35
|
+
private outputBuffer;
|
|
36
|
+
private pid;
|
|
37
|
+
private config;
|
|
38
|
+
constructor(config?: IdleDetectorConfig);
|
|
39
|
+
/**
|
|
40
|
+
* Set the PID of the agent process to monitor.
|
|
41
|
+
* Required for Linux process state inspection.
|
|
42
|
+
*/
|
|
43
|
+
setPid(pid: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get the current PID being monitored.
|
|
46
|
+
*/
|
|
47
|
+
getPid(): number | null;
|
|
48
|
+
/**
|
|
49
|
+
* Process output chunk from the agent.
|
|
50
|
+
* Call this for every output received from the agent process.
|
|
51
|
+
*/
|
|
52
|
+
onOutput(chunk: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Check if the agent process is blocked on read (waiting for input).
|
|
55
|
+
* This is the most reliable signal - the OS knows when a process is waiting.
|
|
56
|
+
*
|
|
57
|
+
* Linux-only; returns null on other platforms.
|
|
58
|
+
*/
|
|
59
|
+
private isProcessWaitingForInput;
|
|
60
|
+
/**
|
|
61
|
+
* Get milliseconds since last output.
|
|
62
|
+
*/
|
|
63
|
+
private getOutputSilenceMs;
|
|
64
|
+
/**
|
|
65
|
+
* Check if the last output ends "naturally" (complete thought vs mid-sentence).
|
|
66
|
+
* Helps distinguish between pauses in output and waiting for input.
|
|
67
|
+
*/
|
|
68
|
+
private hasNaturalEnding;
|
|
69
|
+
/**
|
|
70
|
+
* Determine if the agent is idle and ready for input.
|
|
71
|
+
* Combines multiple signals for reliability across all CLI types.
|
|
72
|
+
*/
|
|
73
|
+
checkIdle(options?: {
|
|
74
|
+
minSilenceMs?: number;
|
|
75
|
+
}): IdleResult;
|
|
76
|
+
/**
|
|
77
|
+
* Wait for idle state with timeout.
|
|
78
|
+
* Returns the idle result when achieved or after timeout.
|
|
79
|
+
*/
|
|
80
|
+
waitForIdle(timeoutMs?: number, pollMs?: number): Promise<IdleResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Reset state (call when agent starts new response).
|
|
83
|
+
*/
|
|
84
|
+
reset(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Get time since last output in milliseconds.
|
|
87
|
+
*/
|
|
88
|
+
getTimeSinceLastOutput(): number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get the PID of a process running in a tmux pane.
|
|
92
|
+
* Uses tmux list-panes with format specifier.
|
|
93
|
+
*/
|
|
94
|
+
export declare function getTmuxPanePid(tmuxPath: string, sessionName: string): Promise<number | null>;
|
|
95
|
+
/**
|
|
96
|
+
* Create an idle detector configured for the current platform.
|
|
97
|
+
* Logs a warning on non-Linux platforms where process state inspection isn't available.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createIdleDetector(config?: IdleDetectorConfig, options?: {
|
|
100
|
+
quiet?: boolean;
|
|
101
|
+
}): UniversalIdleDetector;
|
|
102
|
+
//# sourceMappingURL=idle-detector.d.ts.map
|