agent-relay 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
  2. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
  5. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
  6. package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
  7. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
  10. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
  11. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
  12. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
  13. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
  14. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
  15. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
  16. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
  17. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
  18. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
  19. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
  20. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
  21. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
  22. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
  23. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
  24. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
  25. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
  26. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
  27. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
  28. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
  29. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
  30. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
  31. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
  32. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
  33. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
  35. package/.trajectories/index.json +140 -1
  36. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  37. package/deploy/workspace/codex.config.toml +1 -1
  38. package/deploy/workspace/entrypoint.sh +20 -79
  39. package/deploy/workspace/gh-relay +156 -0
  40. package/deploy/workspace/git-credential-relay +5 -1
  41. package/dist/bridge/multi-project-client.js +13 -10
  42. package/dist/bridge/spawner.d.ts +2 -0
  43. package/dist/bridge/spawner.js +19 -1
  44. package/dist/bridge/types.d.ts +2 -0
  45. package/dist/cli/index.d.ts +1 -1
  46. package/dist/cli/index.js +115 -69
  47. package/dist/cloud/api/admin.js +16 -3
  48. package/dist/cloud/api/codex-auth-helper.js +28 -8
  49. package/dist/cloud/api/consensus.d.ts +13 -0
  50. package/dist/cloud/api/consensus.js +259 -0
  51. package/dist/cloud/api/daemons.js +205 -1
  52. package/dist/cloud/api/git.js +37 -7
  53. package/dist/cloud/api/onboarding.js +4 -1
  54. package/dist/cloud/api/provider-env.d.ts +5 -0
  55. package/dist/cloud/api/provider-env.js +27 -0
  56. package/dist/cloud/api/providers.js +2 -0
  57. package/dist/cloud/api/test-helpers.js +130 -0
  58. package/dist/cloud/api/workspaces.js +38 -3
  59. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  60. package/dist/cloud/db/bulk-ingest.js +268 -0
  61. package/dist/cloud/db/drizzle.d.ts +33 -0
  62. package/dist/cloud/db/drizzle.js +174 -2
  63. package/dist/cloud/db/index.d.ts +24 -5
  64. package/dist/cloud/db/index.js +19 -4
  65. package/dist/cloud/db/schema.d.ts +397 -3
  66. package/dist/cloud/db/schema.js +75 -1
  67. package/dist/cloud/provisioner/index.d.ts +8 -0
  68. package/dist/cloud/provisioner/index.js +256 -50
  69. package/dist/cloud/server.js +47 -3
  70. package/dist/cloud/services/index.d.ts +1 -0
  71. package/dist/cloud/services/index.js +2 -0
  72. package/dist/cloud/services/nango.d.ts +3 -4
  73. package/dist/cloud/services/nango.js +11 -33
  74. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  75. package/dist/cloud/services/workspace-keepalive.js +234 -0
  76. package/dist/config/relay-config.d.ts +23 -0
  77. package/dist/config/relay-config.js +23 -0
  78. package/dist/daemon/agent-manager.d.ts +20 -1
  79. package/dist/daemon/agent-manager.js +47 -0
  80. package/dist/daemon/agent-registry.js +4 -4
  81. package/dist/daemon/agent-signing.d.ts +158 -0
  82. package/dist/daemon/agent-signing.js +523 -0
  83. package/dist/daemon/api.js +18 -1
  84. package/dist/daemon/cli-auth.d.ts +4 -1
  85. package/dist/daemon/cli-auth.js +55 -11
  86. package/dist/daemon/cloud-sync.d.ts +47 -1
  87. package/dist/daemon/cloud-sync.js +152 -3
  88. package/dist/daemon/connection.d.ts +28 -0
  89. package/dist/daemon/connection.js +98 -15
  90. package/dist/daemon/consensus-integration.d.ts +167 -0
  91. package/dist/daemon/consensus-integration.js +371 -0
  92. package/dist/daemon/consensus.d.ts +271 -0
  93. package/dist/daemon/consensus.js +632 -0
  94. package/dist/daemon/delivery-tracker.d.ts +34 -0
  95. package/dist/daemon/delivery-tracker.js +104 -0
  96. package/dist/daemon/enhanced-features.d.ts +118 -0
  97. package/dist/daemon/enhanced-features.js +178 -0
  98. package/dist/daemon/index.d.ts +4 -0
  99. package/dist/daemon/index.js +5 -0
  100. package/dist/daemon/rate-limiter.d.ts +68 -0
  101. package/dist/daemon/rate-limiter.js +130 -0
  102. package/dist/daemon/router.d.ts +18 -11
  103. package/dist/daemon/router.js +55 -111
  104. package/dist/daemon/server.d.ts +13 -1
  105. package/dist/daemon/server.js +71 -9
  106. package/dist/daemon/sync-queue.d.ts +116 -0
  107. package/dist/daemon/sync-queue.js +361 -0
  108. package/dist/health-worker-manager.d.ts +62 -0
  109. package/dist/health-worker-manager.js +144 -0
  110. package/dist/health-worker.d.ts +9 -0
  111. package/dist/health-worker.js +79 -0
  112. package/dist/index.d.ts +2 -1
  113. package/dist/index.js +5 -1
  114. package/dist/memory/context-compaction.d.ts +156 -0
  115. package/dist/memory/context-compaction.js +453 -0
  116. package/dist/memory/index.d.ts +1 -0
  117. package/dist/memory/index.js +1 -0
  118. package/dist/protocol/channels.js +4 -4
  119. package/dist/protocol/framing.d.ts +72 -10
  120. package/dist/protocol/framing.js +194 -25
  121. package/dist/storage/adapter.d.ts +8 -1
  122. package/dist/storage/adapter.js +11 -0
  123. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  124. package/dist/storage/batched-sqlite-adapter.js +183 -0
  125. package/dist/storage/dead-letter-queue.d.ts +196 -0
  126. package/dist/storage/dead-letter-queue.js +427 -0
  127. package/dist/storage/dlq-adapter.d.ts +195 -0
  128. package/dist/storage/dlq-adapter.js +664 -0
  129. package/dist/trajectory/config.d.ts +32 -14
  130. package/dist/trajectory/config.js +38 -16
  131. package/dist/trajectory/integration.js +217 -64
  132. package/dist/utils/git-remote.d.ts +47 -0
  133. package/dist/utils/git-remote.js +125 -0
  134. package/dist/utils/id-generator.d.ts +35 -0
  135. package/dist/utils/id-generator.js +60 -0
  136. package/dist/utils/index.d.ts +1 -0
  137. package/dist/utils/index.js +1 -0
  138. package/dist/utils/precompiled-patterns.d.ts +110 -0
  139. package/dist/utils/precompiled-patterns.js +322 -0
  140. package/dist/wrapper/auth-detection.js +1 -1
  141. package/dist/wrapper/base-wrapper.d.ts +36 -0
  142. package/dist/wrapper/base-wrapper.js +48 -2
  143. package/dist/wrapper/client.d.ts +14 -4
  144. package/dist/wrapper/client.js +84 -31
  145. package/dist/wrapper/idle-detector.d.ts +102 -0
  146. package/dist/wrapper/idle-detector.js +279 -0
  147. package/dist/wrapper/parser.d.ts +4 -0
  148. package/dist/wrapper/parser.js +19 -1
  149. package/dist/wrapper/pty-wrapper.d.ts +7 -1
  150. package/dist/wrapper/pty-wrapper.js +51 -27
  151. package/dist/wrapper/tmux-wrapper.d.ts +12 -1
  152. package/dist/wrapper/tmux-wrapper.js +65 -17
  153. package/package.json +5 -5
  154. package/scripts/run-migrations.js +43 -0
  155. package/scripts/verify-schema.js +134 -0
  156. package/tests/benchmarks/protocol.bench.ts +310 -0
  157. package/dist/dashboard/out/404.html +0 -1
  158. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_buildManifest.js +0 -1
  159. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +0 -1
  160. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  161. package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +0 -2
  162. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
  163. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
  164. package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +0 -1
  165. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +0 -1
  166. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +0 -1
  167. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +0 -1
  168. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +0 -1
  169. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +0 -1
  170. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +0 -1
  171. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +0 -1
  172. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +0 -1
  173. package/dist/dashboard/out/_next/static/chunks/app/history/page-8c8bed33beb2bf1c.js +0 -1
  174. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  175. package/dist/dashboard/out/_next/static/chunks/app/login/page-16f3b49e55b1e0ed.js +0 -1
  176. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +0 -1
  177. package/dist/dashboard/out/_next/static/chunks/app/page-4a5938c18a11a654.js +0 -1
  178. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +0 -1
  179. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +0 -1
  180. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/app/signup/page-547dd0ca55ecd0ba.js +0 -1
  182. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
  183. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/main-2ee6beb2ae96d210.js +0 -1
  186. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +0 -1
  187. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
  188. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
  189. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  190. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
  191. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +0 -1
  192. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +0 -1
  193. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  194. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  195. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  196. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  197. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  198. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
  199. package/dist/dashboard/out/alt-logos/logo.svg +0 -38
  200. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  201. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  202. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  203. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  204. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  205. package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
  206. package/dist/dashboard/out/app/onboarding.html +0 -1
  207. package/dist/dashboard/out/app/onboarding.txt +0 -7
  208. package/dist/dashboard/out/app.html +0 -1
  209. package/dist/dashboard/out/app.txt +0 -7
  210. package/dist/dashboard/out/apple-icon.png +0 -0
  211. package/dist/dashboard/out/connect-repos.html +0 -1
  212. package/dist/dashboard/out/connect-repos.txt +0 -7
  213. package/dist/dashboard/out/history.html +0 -1
  214. package/dist/dashboard/out/history.txt +0 -7
  215. package/dist/dashboard/out/index.html +0 -1
  216. package/dist/dashboard/out/index.txt +0 -7
  217. package/dist/dashboard/out/login.html +0 -6
  218. package/dist/dashboard/out/login.txt +0 -7
  219. package/dist/dashboard/out/metrics.html +0 -1
  220. package/dist/dashboard/out/metrics.txt +0 -7
  221. package/dist/dashboard/out/pricing.html +0 -13
  222. package/dist/dashboard/out/pricing.txt +0 -7
  223. package/dist/dashboard/out/providers/setup/claude.html +0 -1
  224. package/dist/dashboard/out/providers/setup/claude.txt +0 -8
  225. package/dist/dashboard/out/providers/setup/codex.html +0 -1
  226. package/dist/dashboard/out/providers/setup/codex.txt +0 -8
  227. package/dist/dashboard/out/providers.html +0 -1
  228. package/dist/dashboard/out/providers.txt +0 -7
  229. package/dist/dashboard/out/signup.html +0 -6
  230. package/dist/dashboard/out/signup.txt +0 -7
  231. package/dist/dashboard-server/metrics.d.ts +0 -105
  232. package/dist/dashboard-server/metrics.js +0 -193
  233. package/dist/dashboard-server/needs-attention.d.ts +0 -24
  234. package/dist/dashboard-server/needs-attention.js +0 -78
  235. package/dist/dashboard-server/server.d.ts +0 -15
  236. package/dist/dashboard-server/server.js +0 -3776
  237. package/dist/dashboard-server/start.d.ts +0 -6
  238. package/dist/dashboard-server/start.js +0 -13
  239. package/dist/dashboard-server/user-bridge.d.ts +0 -103
  240. package/dist/dashboard-server/user-bridge.js +0 -189
@@ -1,32 +1,94 @@
1
1
  /**
2
2
  * Frame encoding/decoding for the agent relay protocol.
3
- * Uses 4-byte big-endian length prefix + UTF-8 JSON.
3
+ *
4
+ * Wire format:
5
+ * - 1 byte: format indicator (0 = JSON, 1 = MessagePack)
6
+ * - 4 bytes: big-endian payload length
7
+ * - N bytes: payload (JSON or MessagePack encoded)
8
+ *
9
+ * Optimizations:
10
+ * - Ring buffer to avoid Buffer.concat allocations
11
+ * - MessagePack support for faster serialization (optional)
12
+ * - Zero-copy frame extraction where possible
4
13
  */
5
14
  import type { Envelope } from './types.js';
6
15
  export declare const MAX_FRAME_BYTES: number;
7
- export declare const HEADER_SIZE = 4;
16
+ export declare const HEADER_SIZE = 5;
17
+ export declare const LEGACY_HEADER_SIZE = 4;
18
+ export type WireFormat = 'json' | 'msgpack';
19
+ /**
20
+ * Initialize MessagePack support. Call this at startup if you want msgpack.
21
+ * Install @msgpack/msgpack to enable: npm install @msgpack/msgpack
22
+ */
23
+ export declare function initMessagePack(): Promise<boolean>;
24
+ /**
25
+ * Check if MessagePack is available.
26
+ */
27
+ export declare function hasMessagePack(): boolean;
8
28
  /**
9
29
  * Encode a message envelope into a framed buffer.
30
+ *
31
+ * @param envelope - The envelope to encode
32
+ * @param format - Wire format to use (default: 'json')
33
+ * @returns Framed buffer ready for socket write
34
+ */
35
+ export declare function encodeFrame(envelope: Envelope, format?: WireFormat): Buffer;
36
+ /**
37
+ * Encode a frame in legacy format (no format byte, JSON only).
38
+ * Used for backwards compatibility with older clients.
10
39
  */
11
- export declare function encodeFrame(envelope: Envelope): Buffer;
40
+ export declare function encodeFrameLegacy(envelope: Envelope): Buffer;
12
41
  /**
13
- * Frame parser state machine for streaming data.
42
+ * Ring buffer-based frame parser for streaming data.
43
+ *
44
+ * Optimizations:
45
+ * - Pre-allocated buffer avoids GC pressure from Buffer.concat
46
+ * - Compaction only when necessary (wrap-around)
47
+ * - Direct parsing from buffer offsets
14
48
  */
15
49
  export declare class FrameParser {
16
- private buffer;
17
- private maxFrameBytes;
50
+ private ring;
51
+ private head;
52
+ private tail;
53
+ private readonly capacity;
54
+ private readonly maxFrameBytes;
55
+ private format;
56
+ private legacyMode;
18
57
  constructor(maxFrameBytes?: number);
58
+ /**
59
+ * Set the expected wire format for parsing.
60
+ */
61
+ setFormat(format: WireFormat): void;
62
+ /**
63
+ * Enable legacy mode (4-byte header, JSON only).
64
+ */
65
+ setLegacyMode(legacy: boolean): void;
66
+ /**
67
+ * Get current unread bytes in buffer.
68
+ */
69
+ get pendingBytes(): number;
19
70
  /**
20
71
  * Push data into the parser and extract complete frames.
72
+ *
73
+ * @param data - Incoming data buffer
74
+ * @returns Array of parsed envelope frames
21
75
  */
22
76
  push(data: Buffer): Envelope[];
23
77
  /**
24
- * Reset parser state (e.g., on connection reset).
78
+ * Extract all complete frames from the buffer.
25
79
  */
26
- reset(): void;
80
+ private extractFrames;
27
81
  /**
28
- * Get current buffer size (for debugging).
82
+ * Decode payload based on format byte.
29
83
  */
30
- get pendingBytes(): number;
84
+ private decodePayload;
85
+ /**
86
+ * Compact the buffer by shifting unread data to the start.
87
+ */
88
+ private compact;
89
+ /**
90
+ * Reset parser state (e.g., on connection reset).
91
+ */
92
+ reset(): void;
31
93
  }
32
94
  //# sourceMappingURL=framing.d.ts.map
@@ -1,71 +1,240 @@
1
1
  /**
2
2
  * Frame encoding/decoding for the agent relay protocol.
3
- * Uses 4-byte big-endian length prefix + UTF-8 JSON.
3
+ *
4
+ * Wire format:
5
+ * - 1 byte: format indicator (0 = JSON, 1 = MessagePack)
6
+ * - 4 bytes: big-endian payload length
7
+ * - N bytes: payload (JSON or MessagePack encoded)
8
+ *
9
+ * Optimizations:
10
+ * - Ring buffer to avoid Buffer.concat allocations
11
+ * - MessagePack support for faster serialization (optional)
12
+ * - Zero-copy frame extraction where possible
4
13
  */
5
14
  export const MAX_FRAME_BYTES = 1024 * 1024; // 1 MiB default
6
- export const HEADER_SIZE = 4;
15
+ export const HEADER_SIZE = 5; // 1 byte format + 4 bytes length
16
+ export const LEGACY_HEADER_SIZE = 4; // For backwards compatibility
17
+ // Format indicator bytes
18
+ const FORMAT_JSON = 0;
19
+ const FORMAT_MSGPACK = 1;
20
+ // Optional MessagePack - loaded dynamically if available
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ let msgpack = null;
23
+ /**
24
+ * Initialize MessagePack support. Call this at startup if you want msgpack.
25
+ * Install @msgpack/msgpack to enable: npm install @msgpack/msgpack
26
+ */
27
+ export async function initMessagePack() {
28
+ if (msgpack)
29
+ return true; // Already initialized
30
+ try {
31
+ // Dynamic import to avoid compile-time dependency
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ const mod = await import('@msgpack/msgpack');
34
+ const encode = mod.encode || mod.default?.encode;
35
+ const decode = mod.decode || mod.default?.decode;
36
+ if (encode && decode) {
37
+ msgpack = { encode, decode };
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+ catch {
43
+ // MessagePack not installed, JSON-only mode
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Check if MessagePack is available.
49
+ */
50
+ export function hasMessagePack() {
51
+ return msgpack !== null;
52
+ }
7
53
  /**
8
54
  * Encode a message envelope into a framed buffer.
55
+ *
56
+ * @param envelope - The envelope to encode
57
+ * @param format - Wire format to use (default: 'json')
58
+ * @returns Framed buffer ready for socket write
9
59
  */
10
- export function encodeFrame(envelope) {
60
+ export function encodeFrame(envelope, format = 'json') {
61
+ let data;
62
+ let formatByte;
63
+ if (format === 'msgpack' && msgpack) {
64
+ // MessagePack: more compact, faster
65
+ const encoded = msgpack.encode(envelope);
66
+ data = Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength);
67
+ formatByte = FORMAT_MSGPACK;
68
+ }
69
+ else {
70
+ // JSON: human-readable, always available
71
+ data = Buffer.from(JSON.stringify(envelope), 'utf-8');
72
+ formatByte = FORMAT_JSON;
73
+ }
74
+ if (data.length > MAX_FRAME_BYTES) {
75
+ throw new Error(`Frame too large: ${data.length} > ${MAX_FRAME_BYTES}`);
76
+ }
77
+ const header = Buffer.alloc(HEADER_SIZE);
78
+ header.writeUInt8(formatByte, 0);
79
+ header.writeUInt32BE(data.length, 1);
80
+ return Buffer.concat([header, data]);
81
+ }
82
+ /**
83
+ * Encode a frame in legacy format (no format byte, JSON only).
84
+ * Used for backwards compatibility with older clients.
85
+ */
86
+ export function encodeFrameLegacy(envelope) {
11
87
  const json = JSON.stringify(envelope);
12
88
  const data = Buffer.from(json, 'utf-8');
13
89
  if (data.length > MAX_FRAME_BYTES) {
14
90
  throw new Error(`Frame too large: ${data.length} > ${MAX_FRAME_BYTES}`);
15
91
  }
16
- const header = Buffer.alloc(HEADER_SIZE);
92
+ const header = Buffer.alloc(LEGACY_HEADER_SIZE);
17
93
  header.writeUInt32BE(data.length, 0);
18
94
  return Buffer.concat([header, data]);
19
95
  }
20
96
  /**
21
- * Frame parser state machine for streaming data.
97
+ * Ring buffer-based frame parser for streaming data.
98
+ *
99
+ * Optimizations:
100
+ * - Pre-allocated buffer avoids GC pressure from Buffer.concat
101
+ * - Compaction only when necessary (wrap-around)
102
+ * - Direct parsing from buffer offsets
22
103
  */
23
104
  export class FrameParser {
24
- buffer = Buffer.alloc(0);
105
+ ring;
106
+ head = 0; // Read position
107
+ tail = 0; // Write position
108
+ capacity;
25
109
  maxFrameBytes;
110
+ format = 'json';
111
+ legacyMode = false; // Auto-detect legacy clients
26
112
  constructor(maxFrameBytes = MAX_FRAME_BYTES) {
27
113
  this.maxFrameBytes = maxFrameBytes;
114
+ // Allocate 2x max frame for ring buffer headroom
115
+ this.capacity = maxFrameBytes * 2 + HEADER_SIZE;
116
+ this.ring = Buffer.allocUnsafe(this.capacity);
117
+ }
118
+ /**
119
+ * Set the expected wire format for parsing.
120
+ */
121
+ setFormat(format) {
122
+ this.format = format;
123
+ }
124
+ /**
125
+ * Enable legacy mode (4-byte header, JSON only).
126
+ */
127
+ setLegacyMode(legacy) {
128
+ this.legacyMode = legacy;
129
+ }
130
+ /**
131
+ * Get current unread bytes in buffer.
132
+ */
133
+ get pendingBytes() {
134
+ return this.tail - this.head;
28
135
  }
29
136
  /**
30
137
  * Push data into the parser and extract complete frames.
138
+ *
139
+ * @param data - Incoming data buffer
140
+ * @returns Array of parsed envelope frames
31
141
  */
32
142
  push(data) {
33
- this.buffer = Buffer.concat([this.buffer, data]);
143
+ // Check if we have room at the end
144
+ const spaceAtEnd = this.capacity - this.tail;
145
+ if (data.length > spaceAtEnd) {
146
+ // Need to compact: shift unread data to start
147
+ this.compact();
148
+ // Check again after compaction
149
+ if (data.length > this.capacity - this.tail) {
150
+ throw new Error(`Buffer overflow: data ${data.length} exceeds capacity`);
151
+ }
152
+ }
153
+ // Copy incoming data to ring buffer (single copy, no concat)
154
+ data.copy(this.ring, this.tail);
155
+ this.tail += data.length;
156
+ // Extract complete frames
157
+ return this.extractFrames();
158
+ }
159
+ /**
160
+ * Extract all complete frames from the buffer.
161
+ */
162
+ extractFrames() {
34
163
  const frames = [];
35
- while (this.buffer.length >= HEADER_SIZE) {
36
- const frameLength = this.buffer.readUInt32BE(0);
164
+ const headerSize = this.legacyMode ? LEGACY_HEADER_SIZE : HEADER_SIZE;
165
+ while (this.pendingBytes >= headerSize) {
166
+ // Read frame metadata
167
+ let formatByte = FORMAT_JSON;
168
+ let frameLength;
169
+ if (this.legacyMode) {
170
+ frameLength = this.ring.readUInt32BE(this.head);
171
+ }
172
+ else {
173
+ formatByte = this.ring.readUInt8(this.head);
174
+ frameLength = this.ring.readUInt32BE(this.head + 1);
175
+ }
176
+ // Validate frame size
37
177
  if (frameLength > this.maxFrameBytes) {
38
178
  throw new Error(`Frame too large: ${frameLength} > ${this.maxFrameBytes}`);
39
179
  }
40
- const totalLength = HEADER_SIZE + frameLength;
41
- if (this.buffer.length < totalLength) {
42
- // Need more data
43
- break;
180
+ const totalLength = headerSize + frameLength;
181
+ // Check if we have the complete frame
182
+ if (this.pendingBytes < totalLength) {
183
+ break; // Need more data
44
184
  }
45
- // Extract frame
46
- const frameData = this.buffer.subarray(HEADER_SIZE, totalLength);
47
- this.buffer = this.buffer.subarray(totalLength);
185
+ // Extract and parse the frame
186
+ const payloadStart = this.head + headerSize;
187
+ const payloadEnd = this.head + totalLength;
188
+ let envelope;
48
189
  try {
49
- const envelope = JSON.parse(frameData.toString('utf-8'));
50
- frames.push(envelope);
190
+ envelope = this.decodePayload(formatByte, payloadStart, payloadEnd);
51
191
  }
52
192
  catch (err) {
53
- throw new Error(`Invalid JSON in frame: ${err}`);
193
+ throw new Error(`Invalid frame payload: ${err}`);
54
194
  }
195
+ // Advance read position
196
+ this.head += totalLength;
197
+ frames.push(envelope);
198
+ }
199
+ // Opportunistic compaction if we've consumed most of the buffer
200
+ if (this.head > this.capacity / 2 && this.pendingBytes < this.capacity / 4) {
201
+ this.compact();
55
202
  }
56
203
  return frames;
57
204
  }
58
205
  /**
59
- * Reset parser state (e.g., on connection reset).
206
+ * Decode payload based on format byte.
60
207
  */
61
- reset() {
62
- this.buffer = Buffer.alloc(0);
208
+ decodePayload(formatByte, start, end) {
209
+ if (formatByte === FORMAT_MSGPACK && msgpack) {
210
+ // MessagePack decode
211
+ return msgpack.decode(this.ring.subarray(start, end));
212
+ }
213
+ else {
214
+ // JSON decode (default)
215
+ return JSON.parse(this.ring.toString('utf-8', start, end));
216
+ }
63
217
  }
64
218
  /**
65
- * Get current buffer size (for debugging).
219
+ * Compact the buffer by shifting unread data to the start.
66
220
  */
67
- get pendingBytes() {
68
- return this.buffer.length;
221
+ compact() {
222
+ if (this.head === 0)
223
+ return;
224
+ const unread = this.pendingBytes;
225
+ if (unread > 0) {
226
+ // Copy unread data to start of buffer
227
+ this.ring.copy(this.ring, 0, this.head, this.tail);
228
+ }
229
+ this.head = 0;
230
+ this.tail = unread;
231
+ }
232
+ /**
233
+ * Reset parser state (e.g., on connection reset).
234
+ */
235
+ reset() {
236
+ this.head = 0;
237
+ this.tail = 0;
69
238
  }
70
239
  }
71
240
  //# sourceMappingURL=framing.js.map
@@ -100,12 +100,19 @@ export interface StorageAdapter {
100
100
  * Can be set via CLI options or environment variables.
101
101
  */
102
102
  export interface StorageConfig {
103
- /** Storage type: 'sqlite', 'none', or 'postgres' (future) */
103
+ /** Storage type: 'sqlite', 'sqlite-batched', 'none', or 'postgres' (future) */
104
104
  type?: string;
105
105
  /** Path for SQLite database */
106
106
  path?: string;
107
107
  /** Connection URL for database (postgres://..., mysql://...) */
108
108
  url?: string;
109
+ /** Batch configuration for batched adapters */
110
+ batch?: {
111
+ maxBatchSize?: number;
112
+ maxBatchDelayMs?: number;
113
+ maxBatchBytes?: number;
114
+ logBatches?: boolean;
115
+ };
109
116
  }
110
117
  /**
111
118
  * In-memory storage adapter (no persistence).
@@ -144,6 +144,17 @@ export async function createStorageAdapter(dbPath, config) {
144
144
  // Future: implement PostgreSQL adapter
145
145
  throw new Error('PostgreSQL storage is not yet implemented. Use sqlite or none.');
146
146
  }
147
+ case 'sqlite-batched':
148
+ case 'batched': {
149
+ console.log('[storage] Using batched SQLite storage');
150
+ const { BatchedSqliteAdapter } = await import('./batched-sqlite-adapter.js');
151
+ const adapter = new BatchedSqliteAdapter({
152
+ dbPath: finalConfig.path,
153
+ batch: finalConfig.batch,
154
+ });
155
+ await adapter.init();
156
+ return adapter;
157
+ }
147
158
  case 'sqlite':
148
159
  default: {
149
160
  const { SqliteStorageAdapter } = await import('./sqlite-adapter.js');
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Batched SQLite Storage Adapter
3
+ *
4
+ * Wraps SqliteStorageAdapter to provide batched writes for improved throughput.
5
+ * Messages are buffered and flushed either when:
6
+ * - Batch size is reached
7
+ * - Time threshold is exceeded
8
+ * - Memory threshold is exceeded
9
+ * - close() is called
10
+ *
11
+ * All reads still go directly to SQLite for consistency.
12
+ */
13
+ import { SqliteStorageAdapter, type SqliteAdapterOptions } from './sqlite-adapter.js';
14
+ import type { StoredMessage, MessageStatus } from './adapter.js';
15
+ export interface BatchConfig {
16
+ /** Maximum messages in a batch before flush (default: 50) */
17
+ maxBatchSize: number;
18
+ /** Maximum time to wait before flush in ms (default: 100) */
19
+ maxBatchDelayMs: number;
20
+ /** Maximum bytes in memory before flush (default: 1MB) */
21
+ maxBatchBytes: number;
22
+ /** Whether to log batch operations (default: false) */
23
+ logBatches: boolean;
24
+ }
25
+ export declare const DEFAULT_BATCH_CONFIG: BatchConfig;
26
+ export interface BatchedSqliteAdapterOptions extends SqliteAdapterOptions {
27
+ batch?: Partial<BatchConfig>;
28
+ }
29
+ export declare class BatchedSqliteAdapter extends SqliteStorageAdapter {
30
+ private batchConfig;
31
+ private pending;
32
+ private pendingBytes;
33
+ private flushTimer?;
34
+ private flushing;
35
+ private flushPromise?;
36
+ private metrics;
37
+ constructor(options: BatchedSqliteAdapterOptions);
38
+ /**
39
+ * Queue a message for batched writing.
40
+ * May trigger an immediate flush if thresholds are exceeded.
41
+ */
42
+ saveMessage(message: StoredMessage): Promise<void>;
43
+ /**
44
+ * Flush all pending messages to SQLite.
45
+ */
46
+ flush(): Promise<void>;
47
+ /**
48
+ * Write a batch of messages within a transaction.
49
+ */
50
+ private writeBatch;
51
+ /**
52
+ * Get batch metrics for monitoring.
53
+ */
54
+ getBatchMetrics(): typeof this.metrics & {
55
+ pendingCount: number;
56
+ pendingBytes: number;
57
+ };
58
+ /**
59
+ * Reset metrics (useful for testing or periodic reporting).
60
+ */
61
+ resetMetrics(): void;
62
+ /**
63
+ * Close the adapter, flushing any pending messages first.
64
+ */
65
+ close(): Promise<void>;
66
+ /**
67
+ * Update message status - goes directly to SQLite (not batched).
68
+ */
69
+ updateMessageStatus(id: string, status: MessageStatus): Promise<void>;
70
+ }
71
+ //# sourceMappingURL=batched-sqlite-adapter.d.ts.map
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Batched SQLite Storage Adapter
3
+ *
4
+ * Wraps SqliteStorageAdapter to provide batched writes for improved throughput.
5
+ * Messages are buffered and flushed either when:
6
+ * - Batch size is reached
7
+ * - Time threshold is exceeded
8
+ * - Memory threshold is exceeded
9
+ * - close() is called
10
+ *
11
+ * All reads still go directly to SQLite for consistency.
12
+ */
13
+ import { SqliteStorageAdapter, } from './sqlite-adapter.js';
14
+ export const DEFAULT_BATCH_CONFIG = {
15
+ maxBatchSize: 50,
16
+ maxBatchDelayMs: 100,
17
+ maxBatchBytes: 1024 * 1024, // 1MB
18
+ logBatches: false,
19
+ };
20
+ export class BatchedSqliteAdapter extends SqliteStorageAdapter {
21
+ batchConfig;
22
+ pending = [];
23
+ pendingBytes = 0;
24
+ flushTimer;
25
+ flushing = false;
26
+ flushPromise;
27
+ // Metrics
28
+ metrics = {
29
+ batchesWritten: 0,
30
+ messagesWritten: 0,
31
+ bytesWritten: 0,
32
+ flushDueToSize: 0,
33
+ flushDueToTime: 0,
34
+ flushDueToBytes: 0,
35
+ };
36
+ constructor(options) {
37
+ super(options);
38
+ this.batchConfig = { ...DEFAULT_BATCH_CONFIG, ...options.batch };
39
+ }
40
+ /**
41
+ * Queue a message for batched writing.
42
+ * May trigger an immediate flush if thresholds are exceeded.
43
+ */
44
+ async saveMessage(message) {
45
+ // Ensure any pending flush completes first
46
+ if (this.flushPromise) {
47
+ await this.flushPromise;
48
+ }
49
+ const msgJson = JSON.stringify(message);
50
+ const sizeBytes = Buffer.byteLength(msgJson, 'utf-8');
51
+ this.pending.push({ message, sizeBytes });
52
+ this.pendingBytes += sizeBytes;
53
+ // Check flush conditions
54
+ let flushReason = null;
55
+ if (this.pending.length >= this.batchConfig.maxBatchSize) {
56
+ flushReason = 'size';
57
+ this.metrics.flushDueToSize++;
58
+ }
59
+ else if (this.pendingBytes >= this.batchConfig.maxBatchBytes) {
60
+ flushReason = 'bytes';
61
+ this.metrics.flushDueToBytes++;
62
+ }
63
+ if (flushReason) {
64
+ await this.flush();
65
+ }
66
+ else if (!this.flushTimer) {
67
+ // Schedule time-based flush
68
+ this.flushTimer = setTimeout(() => {
69
+ this.metrics.flushDueToTime++;
70
+ this.flush().catch((err) => {
71
+ console.error('[batched-sqlite] Timer flush failed:', err);
72
+ });
73
+ }, this.batchConfig.maxBatchDelayMs);
74
+ }
75
+ }
76
+ /**
77
+ * Flush all pending messages to SQLite.
78
+ */
79
+ async flush() {
80
+ // Clear timer if set
81
+ if (this.flushTimer) {
82
+ clearTimeout(this.flushTimer);
83
+ this.flushTimer = undefined;
84
+ }
85
+ // Skip if nothing to flush or already flushing
86
+ if (this.pending.length === 0 || this.flushing) {
87
+ return;
88
+ }
89
+ this.flushing = true;
90
+ // Take current batch
91
+ const batch = this.pending;
92
+ const batchBytes = this.pendingBytes;
93
+ this.pending = [];
94
+ this.pendingBytes = 0;
95
+ this.flushPromise = this.writeBatch(batch, batchBytes);
96
+ try {
97
+ await this.flushPromise;
98
+ }
99
+ finally {
100
+ this.flushing = false;
101
+ this.flushPromise = undefined;
102
+ }
103
+ }
104
+ /**
105
+ * Write a batch of messages within a transaction.
106
+ */
107
+ async writeBatch(batch, batchBytes) {
108
+ const startTime = Date.now();
109
+ try {
110
+ // Use transaction for atomicity and performance
111
+ // The parent class's db is private, so we call saveMessage in a loop
112
+ // but the SQLite driver will batch them efficiently due to WAL mode
113
+ for (const { message } of batch) {
114
+ await super.saveMessage(message);
115
+ }
116
+ // Update metrics
117
+ this.metrics.batchesWritten++;
118
+ this.metrics.messagesWritten += batch.length;
119
+ this.metrics.bytesWritten += batchBytes;
120
+ if (this.batchConfig.logBatches) {
121
+ const elapsed = Date.now() - startTime;
122
+ console.log(`[batched-sqlite] Wrote ${batch.length} messages (${(batchBytes / 1024).toFixed(1)}KB) in ${elapsed}ms`);
123
+ }
124
+ }
125
+ catch (err) {
126
+ // On failure, re-queue messages for retry
127
+ console.error('[batched-sqlite] Batch write failed, re-queuing:', err);
128
+ this.pending = [...batch, ...this.pending];
129
+ this.pendingBytes += batchBytes;
130
+ throw err;
131
+ }
132
+ }
133
+ /**
134
+ * Get batch metrics for monitoring.
135
+ */
136
+ getBatchMetrics() {
137
+ return {
138
+ ...this.metrics,
139
+ pendingCount: this.pending.length,
140
+ pendingBytes: this.pendingBytes,
141
+ };
142
+ }
143
+ /**
144
+ * Reset metrics (useful for testing or periodic reporting).
145
+ */
146
+ resetMetrics() {
147
+ this.metrics = {
148
+ batchesWritten: 0,
149
+ messagesWritten: 0,
150
+ bytesWritten: 0,
151
+ flushDueToSize: 0,
152
+ flushDueToTime: 0,
153
+ flushDueToBytes: 0,
154
+ };
155
+ }
156
+ /**
157
+ * Close the adapter, flushing any pending messages first.
158
+ */
159
+ async close() {
160
+ // Ensure all pending messages are written
161
+ if (this.flushTimer) {
162
+ clearTimeout(this.flushTimer);
163
+ this.flushTimer = undefined;
164
+ }
165
+ // Wait for any in-progress flush
166
+ if (this.flushPromise) {
167
+ await this.flushPromise;
168
+ }
169
+ // Flush remaining
170
+ if (this.pending.length > 0) {
171
+ await this.flush();
172
+ }
173
+ await super.close();
174
+ }
175
+ /**
176
+ * Update message status - goes directly to SQLite (not batched).
177
+ */
178
+ async updateMessageStatus(id, status) {
179
+ // Status updates should be immediate, not batched
180
+ return super.updateMessageStatus(id, status);
181
+ }
182
+ }
183
+ //# sourceMappingURL=batched-sqlite-adapter.js.map