pi-harness-runtime 1.1.375 → 1.1.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +68 -39
  2. package/package.json +2 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/okf-indexer/package.json +1 -1
  59. package/packages/paperclip-integration/package.json +1 -1
  60. package/packages/performance-optimizer/package.json +1 -1
  61. package/packages/permission-mode/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/relay/dist/client.d.ts +56 -35
  77. package/packages/relay/dist/client.js +278 -71
  78. package/packages/relay/dist/index.d.ts +12 -5
  79. package/packages/relay/dist/index.js +12 -4
  80. package/packages/relay/dist/server.d.ts +58 -20
  81. package/packages/relay/dist/server.js +344 -49
  82. package/packages/relay/dist/types.d.ts +137 -1
  83. package/packages/relay/dist/types.js +21 -2
  84. package/packages/relay/dist/ws-server.d.ts +48 -0
  85. package/packages/relay/dist/ws-server.js +130 -0
  86. package/packages/relay/package.json +1 -1
  87. package/packages/relay/src/client.ts +375 -81
  88. package/packages/relay/src/index.ts +42 -5
  89. package/packages/relay/src/server.ts +452 -54
  90. package/packages/relay/src/types.ts +174 -1
  91. package/packages/relay/src/ws-server.ts +165 -0
  92. package/packages/release-manager/package.json +1 -1
  93. package/packages/requirement-compiler/package.json +1 -1
  94. package/packages/runtime/package.json +1 -1
  95. package/packages/scheduler/package.json +1 -1
  96. package/packages/scheduler-adapter/package.json +1 -1
  97. package/packages/session/package.json +1 -1
  98. package/packages/session-api/package.json +1 -1
  99. package/packages/session-export/package.json +1 -1
  100. package/packages/shared-context/package.json +1 -1
  101. package/packages/skill-mcp-client/package.json +1 -1
  102. package/packages/skill-registry/package.json +1 -1
  103. package/packages/skills/package.json +1 -1
  104. package/packages/sprint-planner/package.json +1 -1
  105. package/packages/subscription-engine/package.json +1 -1
  106. package/packages/task-compiler/package.json +1 -1
  107. package/packages/telegram/package.json +1 -1
  108. package/packages/tencentdb-memory/package.json +1 -1
  109. package/packages/tencentdb-sync/package.json +1 -1
  110. package/packages/test-data-generator/package.json +1 -1
  111. package/packages/test-generator/package.json +1 -1
  112. package/packages/todo-bd-sync/package.json +1 -1
  113. package/packages/token-estimation/package.json +1 -1
  114. package/packages/token-optimizer/package.json +1 -1
  115. package/packages/trajectory/package.json +1 -1
  116. package/packages/tui/package.json +1 -1
  117. package/packages/types/package.json +1 -1
  118. package/packages/voice-api/package.json +1 -1
  119. package/packages/webmcp-tester/package.json +1 -1
  120. package/packages/wiki/package.json +1 -1
  121. package/packages/workflow-events/package.json +1 -1
  122. package/packages/workspace-scanner/package.json +1 -1
  123. package/packages/worktree/package.json +1 -1
  124. package/packages/write-review/package.json +1 -1
  125. package/scripts/relay-client.ts +184 -0
  126. package/scripts/sync-callback-queue.ts +57 -0
  127. package/scripts/telegram-callback-processor.ts +16 -0
  128. package/scripts/unified-webhook-server.ts +160 -145
  129. package/scripts/ws-server.ts +81 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * WebSocket Relay Server - Pushes callbacks to connected clients in real-time
3
+ *
4
+ * Architecture:
5
+ * Telegram/LINE → Webhook → This Server → WebSocket → Local Client
6
+ *
7
+ * No file queue needed - real-time streaming!
8
+ */
9
+ import { createServer } from "node:http";
10
+ import { WebSocketServer, WebSocket } from "ws";
11
+ import { randomUUID } from "node:crypto";
12
+ const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
13
+ /**
14
+ * WebSocket Relay Server
15
+ */
16
+ export class WsRelayServer {
17
+ server = null;
18
+ wss = null;
19
+ clients = new Map();
20
+ port;
21
+ running = false;
22
+ constructor(port = 8444) {
23
+ this.port = port;
24
+ }
25
+ /**
26
+ * Start the WebSocket server
27
+ */
28
+ async start() {
29
+ if (this.running)
30
+ return;
31
+ this.server = createServer();
32
+ this.wss = new WebSocketServer({ server: this.server });
33
+ this.wss.on("connection", (ws, req) => {
34
+ const clientId = randomUUID();
35
+ console.log(`[WsRelay] Client connected: ${clientId} from ${req.socket.remoteAddress}`);
36
+ // Don't auth yet - just accept connection
37
+ // Client will send auth message
38
+ ws.on("message", (data) => {
39
+ try {
40
+ const msg = JSON.parse(data.toString());
41
+ this.handleMessage(ws, clientId, msg);
42
+ }
43
+ catch (err) {
44
+ console.error("[WsRelay] Invalid message:", err);
45
+ }
46
+ });
47
+ ws.on("close", () => {
48
+ this.clients.delete(clientId);
49
+ console.log(`[WsRelay] Client disconnected: ${clientId}`);
50
+ });
51
+ ws.on("error", (err) => {
52
+ console.error(`[WsRelay] Client error: ${clientId}`, err);
53
+ this.clients.delete(clientId);
54
+ });
55
+ // Send welcome
56
+ ws.send(JSON.stringify({ type: "connected", payload: { clientId } }));
57
+ });
58
+ return new Promise((resolve) => {
59
+ this.server.listen(this.port, () => {
60
+ this.running = true;
61
+ console.log(`[WsRelay] Server started on port ${this.port}`);
62
+ resolve();
63
+ });
64
+ });
65
+ }
66
+ handleMessage(ws, clientId, msg) {
67
+ switch (msg.type) {
68
+ case "auth":
69
+ if (msg.payload === AUTH_TOKEN) {
70
+ this.clients.set(clientId, ws);
71
+ ws.send(JSON.stringify({ type: "auth", payload: { success: true } }));
72
+ console.log(`[WsRelay] Client authenticated: ${clientId}`);
73
+ }
74
+ else {
75
+ ws.send(JSON.stringify({ type: "auth", payload: { success: false, error: "Invalid token" } }));
76
+ ws.close();
77
+ }
78
+ break;
79
+ case "ping":
80
+ ws.send(JSON.stringify({ type: "pong", payload: {} }));
81
+ break;
82
+ }
83
+ }
84
+ /**
85
+ * Broadcast callback to all connected clients
86
+ */
87
+ broadcastCallback(callback) {
88
+ const msg = JSON.stringify({ type: "callback", payload: callback });
89
+ let count = 0;
90
+ for (const [clientId, ws] of this.clients) {
91
+ if (ws.readyState === WebSocket.OPEN) {
92
+ ws.send(msg);
93
+ count++;
94
+ }
95
+ }
96
+ console.log(`[WsRelay] Broadcast callback to ${count} client(s): ${callback.action} ${callback.targetId}`);
97
+ }
98
+ /**
99
+ * Stop the server
100
+ */
101
+ async stop() {
102
+ if (!this.running)
103
+ return;
104
+ for (const ws of this.clients.values()) {
105
+ ws.close();
106
+ }
107
+ this.clients.clear();
108
+ return new Promise((resolve) => {
109
+ this.server.close(() => {
110
+ this.running = false;
111
+ console.log("[WsRelay] Server stopped");
112
+ resolve();
113
+ });
114
+ });
115
+ }
116
+ isRunning() {
117
+ return this.running;
118
+ }
119
+ getClientCount() {
120
+ return this.clients.size;
121
+ }
122
+ }
123
+ // Export singleton for use in unified webhook server
124
+ let singleton = null;
125
+ export function getRelayServer() {
126
+ if (!singleton) {
127
+ singleton = new WsRelayServer();
128
+ }
129
+ return singleton;
130
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/relay",
3
- "version": "1.1.375",
3
+ "version": "1.1.377",
4
4
  "type": "module",
5
5
  "description": "Cloud relay for remote access - T3 Connect style for pi-harness",
6
6
  "main": "./dist/index.js",
@@ -1,119 +1,413 @@
1
1
  /**
2
- * Relay Client - Connect to relay server for remote access
2
+ * Relay Client - Two-way streaming WebSocket client
3
+ *
4
+ * Implements t3code-style streaming with:
5
+ * - Connection state machine
6
+ * - Snapshot + live event handling
7
+ * - Typed message protocol
8
+ * - Automatic reconnection with backoff
3
9
  */
4
10
 
5
- import type { RelayConfig, RelayTunnel, RelayMessage } from './types.js';
11
+ import WebSocket from "ws";
12
+ import {
13
+ type RelayConfig,
14
+ type RelayTunnel,
15
+ type RelayMessage,
16
+ type ConnectionState,
17
+ type ConnectionStateChange,
18
+ type CallbackPayload,
19
+ type ServerMessage,
20
+ type ClientMessage,
21
+ type SnapshotMessage,
22
+ type SynchronizedMessage,
23
+ type CallbackMessage,
24
+ type StatusMessage,
25
+ type AuthResponseMessage,
26
+ type ConnectedMessage,
27
+ PROTOCOL_VERSION,
28
+ generateMessageId,
29
+ createTimestamp,
30
+ } from './types.js';
6
31
 
7
- /**
8
- * Generate a UUID v4
9
- */
10
- function generateUUID(): string {
11
- // Use crypto.randomUUID if available (Node.js 14.17+)
12
- const crypto = globalThis as { crypto?: { randomUUID?: () => string } };
13
- if (crypto.crypto?.randomUUID) {
14
- return crypto.crypto.randomUUID();
15
- }
16
- // Fallback: simple UUID generation
17
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c: string) => {
18
- const r = (Math.random() * 16) | 0;
19
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
20
- return v.toString(16);
21
- });
22
- }
32
+ // ============================================================================
33
+ // Constants
34
+ // ============================================================================
23
35
 
24
- /**
25
- * Relay Client options
26
- */
27
- export interface RelayClientOptions {
28
- /** Called when connected */
29
- onConnected?: () => void;
36
+ const DEFAULT_RECONNECT_DELAY_MS = 1000;
37
+ const MAX_RECONNECT_DELAY_MS = 30000;
38
+ const HEARTBEAT_INTERVAL_MS = 30000;
39
+ const CONNECT_TIMEOUT_MS = 10000;
40
+
41
+ // ============================================================================
42
+ // Client Callbacks
43
+ // ============================================================================
44
+
45
+ export interface RelayClientCallbacks {
46
+ /** Called when connected and authenticated */
47
+ onConnected?: (sessionId: string) => void;
30
48
  /** Called when disconnected */
31
49
  onDisconnected?: () => void;
32
- /** Called when tunnel is established */
33
- onTunnel?: (tunnel: RelayTunnel) => void;
50
+ /** Called when snapshot is received (initial state) */
51
+ onSnapshot?: (snapshot: SnapshotMessage['payload']) => void;
52
+ /** Called when synchronized marker received (live events start) */
53
+ onSynchronized?: () => void;
54
+ /** Called when a callback event is received */
55
+ onCallback?: (callback: CallbackPayload) => void;
56
+ /** Called when a status message is received */
57
+ onStatus?: (status: StatusMessage['payload']) => void;
34
58
  /** Called on error */
35
59
  onError?: (error: Error) => void;
60
+ /** Called when connection state changes */
61
+ onStateChange?: (change: ConnectionStateChange) => void;
36
62
  }
37
63
 
38
- /**
39
- * Relay Client
40
- *
41
- * Connects to a relay server and establishes tunnels for remote access.
42
- */
64
+ // ============================================================================
65
+ // Relay Client - Two-way Streaming Implementation
66
+ // ============================================================================
67
+
43
68
  export class RelayClient {
69
+ private ws: WebSocket | null = null;
44
70
  private config: RelayConfig;
45
- private options: RelayClientOptions;
46
- private connected = false;
47
- private tunnel?: RelayTunnel;
71
+ private callbacks: RelayClientCallbacks;
72
+
73
+ // State machine
74
+ private state: ConnectionState = { state: 'disconnected' };
75
+ private sessionId: string | null = null;
76
+ private clientId: string | null = null;
77
+
78
+ // Reconnection
79
+ private reconnectAttempt = 0;
80
+ private reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
81
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
82
+ private connectTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
83
+
84
+ // Heartbeat
85
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
86
+
87
+ // Snapshot tracking (t3code pattern)
88
+ private snapshotReceived = false;
89
+ private synchronizedReceived = false;
48
90
 
49
- constructor(config: RelayConfig, options: RelayClientOptions) {
91
+ constructor(config: RelayConfig, callbacks: RelayClientCallbacks = {}) {
50
92
  this.config = config;
51
- this.options = options;
93
+ this.callbacks = callbacks;
94
+ }
95
+
96
+ // ========================================================================
97
+ // Connection State Machine
98
+ // ========================================================================
99
+
100
+ private setState(newState: ConnectionState): void {
101
+ const previous = this.state;
102
+ this.state = newState;
103
+
104
+ this.callbacks.onStateChange?.({
105
+ previous,
106
+ current: newState,
107
+ timestamp: createTimestamp(),
108
+ });
52
109
  }
53
110
 
111
+ private isConnected(): boolean {
112
+ return this.ws?.readyState === WebSocket.OPEN &&
113
+ this.state.state === 'connected';
114
+ }
115
+
116
+ // ========================================================================
117
+ // Lifecycle
118
+ // ========================================================================
119
+
54
120
  /**
55
- * Connect to relay server
121
+ * Connect to relay server with automatic authentication
56
122
  */
57
123
  async connect(): Promise<void> {
58
- if (this.connected) return;
59
-
60
- try {
61
- // In a real implementation, this would establish WebSocket connection
62
- // to the relay server and authenticate
63
- this.connected = true;
64
- this.options.onConnected?.();
65
- } catch (error) {
66
- this.options.onError?.(error as Error);
67
- throw error;
68
- }
124
+ if (this.isConnected()) return;
125
+
126
+ // Clear any existing connection
127
+ this.cleanup();
128
+
129
+ this.setState({ state: 'connecting' });
130
+
131
+ const url = this.config.serverUrl;
132
+ console.log(`[RelayClient] Connecting to ${url}...`);
133
+
134
+ return new Promise((resolve, reject) => {
135
+ // Set connect timeout
136
+ this.connectTimeoutTimer = setTimeout(() => {
137
+ if (this.state.state === 'connecting') {
138
+ this.ws?.close();
139
+ reject(new Error('Connection timeout'));
140
+ }
141
+ }, CONNECT_TIMEOUT_MS);
142
+
143
+ this.ws = new WebSocket(url);
144
+
145
+ this.ws.on("open", () => {
146
+ console.log("[RelayClient] WebSocket opened");
147
+ this.setState({ state: 'authenticating' });
148
+
149
+ // Send authentication
150
+ this.send({
151
+ version: PROTOCOL_VERSION,
152
+ type: 'auth',
153
+ payload: { token: this.config.token || '' },
154
+ timestamp: createTimestamp(),
155
+ });
156
+ });
157
+
158
+ this.ws.on("message", (data) => {
159
+ this.handleMessage(data.toString());
160
+ });
161
+
162
+ this.ws.on("close", () => {
163
+ console.log("[RelayClient] WebSocket closed");
164
+ this.handleDisconnect();
165
+ });
166
+
167
+ this.ws.on("error", (err) => {
168
+ console.error("[RelayClient] WebSocket error:", err.message);
169
+ this.callbacks.onError?.(err);
170
+
171
+ if (this.state.state === 'connecting' || this.state.state === 'authenticating') {
172
+ clearTimeout(this.connectTimeoutTimer!);
173
+ reject(err);
174
+ }
175
+
176
+ this.handleDisconnect();
177
+ });
178
+ });
69
179
  }
70
180
 
71
181
  /**
72
182
  * Disconnect from relay server
73
183
  */
74
- async disconnect(): Promise<void> {
75
- this.connected = false;
76
- this.tunnel = undefined;
77
- this.options.onDisconnected?.();
184
+ disconnect(): void {
185
+ console.log("[RelayClient] Disconnecting...");
186
+ this.cleanup();
187
+ this.setState({ state: 'disconnected' });
188
+ this.callbacks.onDisconnected?.();
78
189
  }
79
190
 
80
191
  /**
81
- * Create a tunnel
192
+ * Send a message through the WebSocket
82
193
  */
83
- async createTunnel(localPort: number): Promise<RelayTunnel> {
84
- const tunnel: RelayTunnel = {
85
- id: generateUUID(),
86
- url: `${this.config.serverUrl}/tunnel/${localPort}`,
87
- localPort,
88
- status: this.connected ? 'connected' : 'disconnected',
89
- createdAt: new Date().toISOString(),
90
- };
91
- this.tunnel = tunnel;
92
- this.options.onTunnel?.(tunnel);
93
- return tunnel;
194
+ send(message: ClientMessage): void {
195
+ if (this.isConnected()) {
196
+ this.ws!.send(JSON.stringify(message));
197
+ }
94
198
  }
95
199
 
96
- /**
97
- * Send a message through the tunnel
98
- */
99
- async send(_message: RelayMessage): Promise<void> {
100
- if (!this.connected) {
101
- throw new Error('Not connected to relay server');
200
+ // ========================================================================
201
+ // Message Handling
202
+ // ========================================================================
203
+
204
+ private handleMessage(data: string): void {
205
+ try {
206
+ const msg = JSON.parse(data) as ServerMessage & { type: string };
207
+
208
+ switch (msg.type) {
209
+ case 'connected':
210
+ this.handleConnected(msg as ConnectedMessage);
211
+ break;
212
+
213
+ case 'auth':
214
+ this.handleAuth(msg as AuthResponseMessage);
215
+ break;
216
+
217
+ case 'snapshot':
218
+ this.handleSnapshot(msg as SnapshotMessage);
219
+ break;
220
+
221
+ case 'synchronized':
222
+ this.handleSynchronized(msg as SynchronizedMessage);
223
+ break;
224
+
225
+ case 'callback':
226
+ this.handleCallback(msg as CallbackMessage);
227
+ break;
228
+
229
+ case 'status':
230
+ this.handleStatus(msg as StatusMessage);
231
+ break;
232
+
233
+ case 'pong':
234
+ // Heartbeat response - connection is alive
235
+ break;
236
+
237
+ default: {
238
+ const unknownMsg = msg as { type: string };
239
+ console.warn(`[RelayClient] Unknown message type: ${unknownMsg.type}`);
240
+ }
241
+ }
242
+ } catch (err) {
243
+ console.error("[RelayClient] Failed to parse message:", err);
244
+ this.callbacks.onError?.(err as Error);
102
245
  }
103
- // In a real implementation, this would send the message over WebSocket
104
246
  }
105
247
 
106
- /**
107
- * Check if connected
108
- */
109
- isConnected(): boolean {
110
- return this.connected;
248
+ private handleConnected(msg: ConnectedMessage): void {
249
+ this.clientId = msg.payload.clientId;
250
+ console.log(`[RelayClient] Connected, clientId: ${this.clientId}`);
111
251
  }
112
252
 
113
- /**
114
- * Get current tunnel
115
- */
116
- getTunnel(): RelayTunnel | undefined {
117
- return this.tunnel;
253
+ private handleAuth(msg: AuthResponseMessage): void {
254
+ if (msg.payload.success) {
255
+ this.sessionId = msg.payload.sessionId || null;
256
+ this.setState({ state: 'connected', sessionId: this.sessionId || '' });
257
+
258
+ // Reset reconnection state
259
+ this.reconnectAttempt = 0;
260
+ this.reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
261
+
262
+ // Start heartbeat
263
+ this.startHeartbeat();
264
+
265
+ // Notify callback
266
+ this.callbacks.onConnected?.(this.sessionId || '');
267
+ console.log(`[RelayClient] Authenticated, sessionId: ${this.sessionId}`);
268
+ } else {
269
+ console.error(`[RelayClient] Auth failed: ${msg.payload.error}`);
270
+ this.callbacks.onError?.(new Error(msg.payload.error || 'Authentication failed'));
271
+ this.disconnect();
272
+ }
273
+ }
274
+
275
+ private handleSnapshot(msg: SnapshotMessage): void {
276
+ console.log(`[RelayClient] Snapshot received: ${msg.payload.recentCallbacks.length} recent callbacks`);
277
+ this.snapshotReceived = true;
278
+
279
+ // Notify callback
280
+ this.callbacks.onSnapshot?.(msg.payload);
281
+ }
282
+
283
+ private handleSynchronized(_msg: SynchronizedMessage): void {
284
+ console.log("[RelayClient] Synchronized - live events starting");
285
+ this.synchronizedReceived = true;
286
+
287
+ // Notify callback
288
+ this.callbacks.onSynchronized?.();
289
+ }
290
+
291
+ private handleCallback(msg: CallbackMessage): void {
292
+ // Only process callbacks after synchronized
293
+ if (!this.synchronizedReceived) {
294
+ console.warn("[RelayClient] Ignoring callback before synchronized");
295
+ return;
296
+ }
297
+
298
+ this.callbacks.onCallback?.(msg.payload);
299
+ }
300
+
301
+ private handleStatus(msg: StatusMessage): void {
302
+ console.log(`[RelayClient] Status [${msg.payload.kind}]: ${msg.payload.code} - ${msg.payload.message}`);
303
+ this.callbacks.onStatus?.(msg.payload);
304
+ }
305
+
306
+ // ========================================================================
307
+ // Disconnect & Reconnect
308
+ // ========================================================================
309
+
310
+ private handleDisconnect(): void {
311
+ this.stopHeartbeat();
312
+
313
+ const wasConnected = this.state.state === 'connected';
314
+ this.setState({ state: 'disconnected' });
315
+
316
+ if (wasConnected) {
317
+ this.callbacks.onDisconnected?.();
318
+ this.scheduleReconnect();
319
+ }
320
+ }
321
+
322
+ private scheduleReconnect(): void {
323
+ if (this.reconnectTimer) return;
324
+
325
+ this.reconnectAttempt++;
326
+ this.setState({ state: 'reconnecting', attempt: this.reconnectAttempt });
327
+
328
+ console.log(`[RelayClient] Scheduling reconnect attempt ${this.reconnectAttempt} in ${this.reconnectDelay}ms`);
329
+
330
+ this.reconnectTimer = setTimeout(() => {
331
+ this.reconnectTimer = null;
332
+ this.connect().catch((err) => {
333
+ console.error(`[RelayClient] Reconnect failed: ${err.message}`);
334
+ });
335
+ }, this.reconnectDelay);
336
+
337
+ // Exponential backoff
338
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
339
+ }
340
+
341
+ // ========================================================================
342
+ // Heartbeat
343
+ // ========================================================================
344
+
345
+ private startHeartbeat(): void {
346
+ this.stopHeartbeat();
347
+
348
+ this.heartbeatTimer = setInterval(() => {
349
+ if (this.isConnected()) {
350
+ this.send({
351
+ version: PROTOCOL_VERSION,
352
+ type: 'ping',
353
+ payload: {},
354
+ timestamp: createTimestamp(),
355
+ });
356
+ }
357
+ }, HEARTBEAT_INTERVAL_MS);
358
+ }
359
+
360
+ private stopHeartbeat(): void {
361
+ if (this.heartbeatTimer) {
362
+ clearInterval(this.heartbeatTimer);
363
+ this.heartbeatTimer = null;
364
+ }
365
+ }
366
+
367
+ // ========================================================================
368
+ // Cleanup
369
+ // ========================================================================
370
+
371
+ private cleanup(): void {
372
+ // Clear timers
373
+ if (this.reconnectTimer) {
374
+ clearTimeout(this.reconnectTimer);
375
+ this.reconnectTimer = null;
376
+ }
377
+ if (this.connectTimeoutTimer) {
378
+ clearTimeout(this.connectTimeoutTimer);
379
+ this.connectTimeoutTimer = null;
380
+ }
381
+ this.stopHeartbeat();
382
+
383
+ // Reset snapshot state
384
+ this.snapshotReceived = false;
385
+ this.synchronizedReceived = false;
386
+
387
+ // Close WebSocket
388
+ if (this.ws) {
389
+ this.ws.close();
390
+ this.ws = null;
391
+ }
392
+ }
393
+
394
+ // ========================================================================
395
+ // Public API
396
+ // ========================================================================
397
+
398
+ getState(): ConnectionState {
399
+ return this.state;
400
+ }
401
+
402
+ isReady(): boolean {
403
+ return this.isConnected() && this.synchronizedReceived;
404
+ }
405
+
406
+ getSessionId(): string | null {
407
+ return this.sessionId;
408
+ }
409
+
410
+ getClientId(): string | null {
411
+ return this.clientId;
118
412
  }
119
413
  }
@@ -1,10 +1,47 @@
1
1
  /**
2
- * Cloud Relay - T3 Connect Style
2
+ * Cloud Relay - Two-way Streaming Communication
3
3
  *
4
- * Enables remote access to pi-harness without router/port forwarding.
5
- * Based on T3Code's T3 Connect architecture.
4
+ * Enables real-time two-way communication with pi-harness without router/port forwarding.
5
+ * Based on T3Code's streaming architecture:
6
+ * - Snapshot + Live event pattern
7
+ * - Typed message protocol
8
+ * - Connection state machine
9
+ * - Stream coalescing
6
10
  */
7
11
 
8
- export { RelayServer } from './server.js';
12
+ // Server
13
+ export { RelayServer, createRelayServer } from './server.js';
14
+ export type { RelayServerOptions } from './server.js';
15
+
16
+ // Client
9
17
  export { RelayClient } from './client.js';
10
- export { RelayConfig, RelayTunnel } from './types.js';
18
+ export type { RelayClientCallbacks } from './client.js';
19
+
20
+ // Types
21
+ export {
22
+ PROTOCOL_VERSION,
23
+ generateMessageId,
24
+ createTimestamp,
25
+ } from './types.js';
26
+ export type {
27
+ RelayConfig,
28
+ RelayTunnel,
29
+ ConnectionState,
30
+ ConnectionStateChange,
31
+ BaseMessage,
32
+ SnapshotMessage,
33
+ SynchronizedMessage,
34
+ CallbackPayload,
35
+ CallbackMessage,
36
+ StatusKind,
37
+ StatusMessage,
38
+ PingMessage,
39
+ PongMessage,
40
+ AuthRequestMessage,
41
+ AuthResponseMessage,
42
+ SubscribeMessage,
43
+ ConnectedMessage,
44
+ ServerMessage,
45
+ ClientMessage,
46
+ RelayMessage,
47
+ } from './types.js';