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.
Files changed (202) 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/README.md +23 -9
  37. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  38. package/deploy/workspace/codex.config.toml +1 -1
  39. package/deploy/workspace/entrypoint.sh +20 -79
  40. package/deploy/workspace/gh-relay +156 -0
  41. package/deploy/workspace/git-credential-relay +5 -1
  42. package/dist/bridge/multi-project-client.js +13 -10
  43. package/dist/bridge/spawner.d.ts +2 -0
  44. package/dist/bridge/spawner.js +58 -76
  45. package/dist/bridge/types.d.ts +2 -0
  46. package/dist/cli/index.d.ts +8 -6
  47. package/dist/cli/index.js +297 -30
  48. package/dist/cloud/api/admin.js +16 -3
  49. package/dist/cloud/api/codex-auth-helper.js +28 -8
  50. package/dist/cloud/api/consensus.d.ts +13 -0
  51. package/dist/cloud/api/consensus.js +259 -0
  52. package/dist/cloud/api/daemons.js +205 -1
  53. package/dist/cloud/api/git.js +37 -7
  54. package/dist/cloud/api/onboarding.js +4 -1
  55. package/dist/cloud/api/provider-env.d.ts +5 -0
  56. package/dist/cloud/api/provider-env.js +27 -0
  57. package/dist/cloud/api/providers.js +2 -0
  58. package/dist/cloud/api/test-helpers.js +130 -0
  59. package/dist/cloud/api/workspaces.js +38 -3
  60. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  61. package/dist/cloud/db/bulk-ingest.js +268 -0
  62. package/dist/cloud/db/drizzle.d.ts +33 -0
  63. package/dist/cloud/db/drizzle.js +174 -2
  64. package/dist/cloud/db/index.d.ts +24 -5
  65. package/dist/cloud/db/index.js +19 -4
  66. package/dist/cloud/db/schema.d.ts +397 -3
  67. package/dist/cloud/db/schema.js +75 -1
  68. package/dist/cloud/provisioner/index.d.ts +8 -0
  69. package/dist/cloud/provisioner/index.js +256 -50
  70. package/dist/cloud/server.js +47 -3
  71. package/dist/cloud/services/index.d.ts +1 -0
  72. package/dist/cloud/services/index.js +2 -0
  73. package/dist/cloud/services/nango.d.ts +3 -4
  74. package/dist/cloud/services/nango.js +11 -33
  75. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  76. package/dist/cloud/services/workspace-keepalive.js +234 -0
  77. package/dist/config/relay-config.d.ts +23 -0
  78. package/dist/config/relay-config.js +23 -0
  79. package/dist/daemon/agent-manager.d.ts +20 -1
  80. package/dist/daemon/agent-manager.js +51 -0
  81. package/dist/daemon/agent-registry.js +4 -4
  82. package/dist/daemon/agent-signing.d.ts +158 -0
  83. package/dist/daemon/agent-signing.js +523 -0
  84. package/dist/daemon/api.js +18 -1
  85. package/dist/daemon/cli-auth.d.ts +4 -1
  86. package/dist/daemon/cli-auth.js +55 -11
  87. package/dist/daemon/cloud-sync.d.ts +47 -1
  88. package/dist/daemon/cloud-sync.js +152 -3
  89. package/dist/daemon/connection.d.ts +28 -0
  90. package/dist/daemon/connection.js +113 -22
  91. package/dist/daemon/consensus-integration.d.ts +167 -0
  92. package/dist/daemon/consensus-integration.js +371 -0
  93. package/dist/daemon/consensus.d.ts +271 -0
  94. package/dist/daemon/consensus.js +632 -0
  95. package/dist/daemon/delivery-tracker.d.ts +34 -0
  96. package/dist/daemon/delivery-tracker.js +104 -0
  97. package/dist/daemon/enhanced-features.d.ts +118 -0
  98. package/dist/daemon/enhanced-features.js +178 -0
  99. package/dist/daemon/index.d.ts +4 -0
  100. package/dist/daemon/index.js +5 -0
  101. package/dist/daemon/rate-limiter.d.ts +68 -0
  102. package/dist/daemon/rate-limiter.js +130 -0
  103. package/dist/daemon/router.d.ts +18 -11
  104. package/dist/daemon/router.js +57 -113
  105. package/dist/daemon/server.d.ts +13 -1
  106. package/dist/daemon/server.js +71 -9
  107. package/dist/daemon/sync-queue.d.ts +116 -0
  108. package/dist/daemon/sync-queue.js +361 -0
  109. package/dist/dashboard/out/404.html +1 -1
  110. package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
  111. package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  112. package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
  113. package/dist/dashboard/out/_next/static/chunks/app/app/{page-c617745b81344f4f.js → page-7f64824ae7d06707.js} +1 -1
  114. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
  115. package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/app/{page-dc786c183425c2ac.js → page-814efc4d77b4191d.js} +1 -1
  117. package/dist/dashboard/out/_next/static/chunks/{main-2ee6beb2ae96d210.js → main-5a40a5ae29646e1b.js} +1 -1
  118. package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
  119. package/dist/dashboard/out/app/onboarding.html +1 -1
  120. package/dist/dashboard/out/app/onboarding.txt +1 -1
  121. package/dist/dashboard/out/app.html +1 -1
  122. package/dist/dashboard/out/app.txt +2 -2
  123. package/dist/dashboard/out/cloud/link.html +1 -0
  124. package/dist/dashboard/out/cloud/link.txt +7 -0
  125. package/dist/dashboard/out/connect-repos.html +1 -1
  126. package/dist/dashboard/out/connect-repos.txt +1 -1
  127. package/dist/dashboard/out/history.html +1 -1
  128. package/dist/dashboard/out/history.txt +2 -2
  129. package/dist/dashboard/out/index.html +1 -1
  130. package/dist/dashboard/out/index.txt +2 -2
  131. package/dist/dashboard/out/login.html +2 -3
  132. package/dist/dashboard/out/login.txt +2 -2
  133. package/dist/dashboard/out/metrics.html +1 -1
  134. package/dist/dashboard/out/metrics.txt +2 -2
  135. package/dist/dashboard/out/pricing.html +2 -2
  136. package/dist/dashboard/out/pricing.txt +1 -1
  137. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  138. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  139. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  140. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  141. package/dist/dashboard/out/providers.html +1 -1
  142. package/dist/dashboard/out/providers.txt +1 -1
  143. package/dist/dashboard/out/signup.html +2 -2
  144. package/dist/dashboard/out/signup.txt +1 -1
  145. package/dist/dashboard-server/server.js +244 -28
  146. package/dist/health-worker-manager.d.ts +62 -0
  147. package/dist/health-worker-manager.js +144 -0
  148. package/dist/health-worker.d.ts +9 -0
  149. package/dist/health-worker.js +79 -0
  150. package/dist/index.d.ts +2 -1
  151. package/dist/index.js +5 -1
  152. package/dist/memory/context-compaction.d.ts +156 -0
  153. package/dist/memory/context-compaction.js +453 -0
  154. package/dist/memory/index.d.ts +1 -0
  155. package/dist/memory/index.js +1 -0
  156. package/dist/protocol/channels.js +4 -4
  157. package/dist/protocol/framing.d.ts +72 -10
  158. package/dist/protocol/framing.js +194 -25
  159. package/dist/storage/adapter.d.ts +8 -1
  160. package/dist/storage/adapter.js +11 -0
  161. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  162. package/dist/storage/batched-sqlite-adapter.js +183 -0
  163. package/dist/storage/dead-letter-queue.d.ts +196 -0
  164. package/dist/storage/dead-letter-queue.js +427 -0
  165. package/dist/storage/dlq-adapter.d.ts +195 -0
  166. package/dist/storage/dlq-adapter.js +664 -0
  167. package/dist/trajectory/config.d.ts +32 -14
  168. package/dist/trajectory/config.js +38 -16
  169. package/dist/trajectory/integration.js +217 -64
  170. package/dist/utils/git-remote.d.ts +47 -0
  171. package/dist/utils/git-remote.js +125 -0
  172. package/dist/utils/id-generator.d.ts +35 -0
  173. package/dist/utils/id-generator.js +60 -0
  174. package/dist/utils/index.d.ts +1 -0
  175. package/dist/utils/index.js +1 -0
  176. package/dist/utils/precompiled-patterns.d.ts +110 -0
  177. package/dist/utils/precompiled-patterns.js +322 -0
  178. package/dist/wrapper/auth-detection.js +1 -1
  179. package/dist/wrapper/base-wrapper.d.ts +40 -0
  180. package/dist/wrapper/base-wrapper.js +60 -6
  181. package/dist/wrapper/client.d.ts +14 -4
  182. package/dist/wrapper/client.js +89 -31
  183. package/dist/wrapper/idle-detector.d.ts +102 -0
  184. package/dist/wrapper/idle-detector.js +279 -0
  185. package/dist/wrapper/parser.d.ts +4 -0
  186. package/dist/wrapper/parser.js +19 -1
  187. package/dist/wrapper/pty-wrapper.d.ts +14 -2
  188. package/dist/wrapper/pty-wrapper.js +132 -32
  189. package/dist/wrapper/shared.d.ts +1 -1
  190. package/dist/wrapper/shared.js +1 -1
  191. package/dist/wrapper/tmux-wrapper.d.ts +20 -2
  192. package/dist/wrapper/tmux-wrapper.js +163 -40
  193. package/package.json +3 -1
  194. package/scripts/run-migrations.js +43 -0
  195. package/scripts/verify-schema.js +134 -0
  196. package/tests/benchmarks/protocol.bench.ts +310 -0
  197. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  198. package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +0 -1
  199. package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +0 -1
  200. package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +0 -1
  201. /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_buildManifest.js +0 -0
  202. /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_ssgManifest.js +0 -0
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Context Compaction for Long Agent Sessions
3
+ *
4
+ * Manages conversation context to prevent token limit exhaustion.
5
+ * Provides token counting, message summarization, and context pruning.
6
+ *
7
+ * Inspired by russian-code-ts context management targets:
8
+ * - Token estimation: <20ms
9
+ * - Embeddings-based semantic search for relevant context
10
+ *
11
+ * Strategies:
12
+ * 1. Fast token estimation (character-based heuristic)
13
+ * 2. Importance-weighted message retention
14
+ * 3. Sliding window with summary injection
15
+ * 4. Semantic deduplication of similar messages
16
+ */
17
+ // =============================================================================
18
+ // Default Configuration
19
+ // =============================================================================
20
+ const DEFAULT_CONFIG = {
21
+ maxTokens: 100000, // 100k tokens (Claude's typical limit)
22
+ targetUsage: 0.7, // Target 70% after compaction
23
+ compactionThreshold: 0.85, // Trigger at 85% usage
24
+ minImportanceRetain: 30, // Keep messages with importance >= 30
25
+ keepRecentCount: 10, // Always keep last 10 messages
26
+ enableSummarization: true,
27
+ enableDeduplication: true,
28
+ deduplicationThreshold: 0.85,
29
+ };
30
+ // =============================================================================
31
+ // Token Estimation
32
+ // =============================================================================
33
+ /**
34
+ * Fast token estimation using character-based heuristic.
35
+ * Targets <20ms latency for large texts.
36
+ *
37
+ * Heuristic: ~4 characters per token for English text.
38
+ * Adjusts for code (more tokens per char) and whitespace.
39
+ */
40
+ export function estimateTokens(text) {
41
+ if (!text)
42
+ return 0;
43
+ const length = text.length;
44
+ // Fast path for short texts
45
+ if (length < 100) {
46
+ return Math.ceil(length / 3.5);
47
+ }
48
+ // Sample-based estimation for longer texts
49
+ // Count different character types in sample
50
+ const sampleSize = Math.min(1000, length);
51
+ const sample = text.substring(0, sampleSize);
52
+ let codeChars = 0;
53
+ let whitespaceChars = 0;
54
+ let _punctuationChars = 0;
55
+ for (let i = 0; i < sample.length; i++) {
56
+ const char = sample[i];
57
+ if (/\s/.test(char)) {
58
+ whitespaceChars++;
59
+ }
60
+ else if (/[{}[\]();:,.<>!=+\-*/&|^~`@#$%]/.test(char)) {
61
+ _punctuationChars++;
62
+ codeChars++;
63
+ }
64
+ }
65
+ const codeRatio = codeChars / sampleSize;
66
+ const whitespaceRatio = whitespaceChars / sampleSize;
67
+ // Adjust chars per token based on content type
68
+ // Heuristics based on tokenization patterns:
69
+ // - Base prose: ~4 chars/token (average English text)
70
+ // - Code: ~3 chars/token (more tokens due to symbols/structure)
71
+ // - High whitespace: ~3.5 chars/token (more word boundaries = more tokens)
72
+ const baseCharsPerToken = 4;
73
+ const codeAdjustment = codeRatio * 1.5; // Code reduces chars/token (more tokens)
74
+ const whitespaceAdjustment = whitespaceRatio * 0.5; // Whitespace reduces chars/token (more word boundaries)
75
+ const charsPerToken = baseCharsPerToken - codeAdjustment - whitespaceAdjustment;
76
+ const adjustedCharsPerToken = Math.max(2.5, Math.min(5, charsPerToken));
77
+ return Math.ceil(length / adjustedCharsPerToken);
78
+ }
79
+ /**
80
+ * Estimate tokens for a message (uses caching).
81
+ */
82
+ export function estimateMessageTokens(message) {
83
+ if (message.tokenCount !== undefined) {
84
+ return message.tokenCount;
85
+ }
86
+ // Role overhead: ~4 tokens for role markers
87
+ const roleOverhead = 4;
88
+ const contentTokens = estimateTokens(message.content);
89
+ message.tokenCount = roleOverhead + contentTokens;
90
+ return message.tokenCount;
91
+ }
92
+ /**
93
+ * Estimate tokens for entire context.
94
+ */
95
+ export function estimateContextTokens(messages) {
96
+ let total = 0;
97
+ for (const msg of messages) {
98
+ total += estimateMessageTokens(msg);
99
+ }
100
+ // Add overhead for message separators (~2 tokens per message)
101
+ total += messages.length * 2;
102
+ return total;
103
+ }
104
+ // =============================================================================
105
+ // Importance Scoring
106
+ // =============================================================================
107
+ /**
108
+ * Calculate importance score for a message.
109
+ * Higher scores = more important to retain.
110
+ */
111
+ export function calculateImportance(message, index, total) {
112
+ let score = 50; // Base score
113
+ // Recency bonus (0-20 points)
114
+ const recencyRatio = index / total;
115
+ score += recencyRatio * 20;
116
+ // System messages are important
117
+ if (message.role === 'system') {
118
+ score += 30;
119
+ }
120
+ // Check for important content patterns
121
+ const content = message.content.toLowerCase();
122
+ // Task-related keywords
123
+ if (/\b(todo|task|implement|fix|bug|error|important|critical|urgent)\b/.test(content)) {
124
+ score += 15;
125
+ }
126
+ // Code blocks are often important context
127
+ if (/```[\s\S]*```/.test(message.content)) {
128
+ score += 10;
129
+ }
130
+ // Questions that might need answers retained
131
+ if (/\?/.test(content) && message.role === 'user') {
132
+ score += 10;
133
+ }
134
+ // Acknowledgments and status updates can be lower priority
135
+ if (/^(ok|ack|got it|understood|done|completed)/i.test(content)) {
136
+ score -= 20;
137
+ }
138
+ // Very short messages are usually less important
139
+ if (message.content.length < 50) {
140
+ score -= 10;
141
+ }
142
+ // Summaries should be kept
143
+ if (message.isSummary) {
144
+ score += 25;
145
+ }
146
+ // User-specified importance overrides
147
+ if (message.importance !== undefined) {
148
+ score = (score + message.importance) / 2;
149
+ }
150
+ return Math.max(0, Math.min(100, score));
151
+ }
152
+ // =============================================================================
153
+ // Similarity Detection
154
+ // =============================================================================
155
+ /**
156
+ * Simple similarity score between two strings (Jaccard on word set).
157
+ * Returns 0-1 where 1 = identical.
158
+ */
159
+ export function calculateSimilarity(a, b) {
160
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
161
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
162
+ if (wordsA.size === 0 || wordsB.size === 0) {
163
+ return 0;
164
+ }
165
+ let intersection = 0;
166
+ for (const word of wordsA) {
167
+ if (wordsB.has(word)) {
168
+ intersection++;
169
+ }
170
+ }
171
+ const union = wordsA.size + wordsB.size - intersection;
172
+ return intersection / union;
173
+ }
174
+ /**
175
+ * Find duplicate/similar messages.
176
+ */
177
+ export function findDuplicates(messages, threshold = 0.85) {
178
+ const duplicates = new Map();
179
+ for (let i = 0; i < messages.length; i++) {
180
+ for (let j = i + 1; j < messages.length; j++) {
181
+ const similarity = calculateSimilarity(messages[i].content, messages[j].content);
182
+ if (similarity >= threshold) {
183
+ const key = messages[i].id;
184
+ const existing = duplicates.get(key) ?? [];
185
+ existing.push(messages[j].id);
186
+ duplicates.set(key, existing);
187
+ }
188
+ }
189
+ }
190
+ return duplicates;
191
+ }
192
+ // =============================================================================
193
+ // Summarization
194
+ // =============================================================================
195
+ /**
196
+ * Create a summary of multiple messages.
197
+ * This is a simple extractive summary - in production, use an LLM.
198
+ */
199
+ export function createSummary(messages) {
200
+ const messageCount = messages.length;
201
+ const roles = new Set(messages.map(m => m.role));
202
+ const threads = new Set(messages.filter(m => m.thread).map(m => m.thread));
203
+ // Extract key sentences (first sentence of each message, or first 100 chars)
204
+ const keyPoints = [];
205
+ for (const msg of messages.slice(0, 5)) { // Take up to 5 key points
206
+ const firstSentence = msg.content.split(/[.!?]\s/)[0];
207
+ if (firstSentence && firstSentence.length < 200) {
208
+ keyPoints.push(`- ${firstSentence}`);
209
+ }
210
+ }
211
+ const content = [
212
+ `[Summary of ${messageCount} messages]`,
213
+ `Participants: ${Array.from(roles).join(', ')}`,
214
+ threads.size > 0 ? `Threads: ${Array.from(threads).join(', ')}` : '',
215
+ 'Key points:',
216
+ ...keyPoints,
217
+ `[End summary]`,
218
+ ].filter(Boolean).join('\n');
219
+ return {
220
+ id: `summary_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
221
+ role: 'system',
222
+ content,
223
+ timestamp: Date.now(),
224
+ importance: 70,
225
+ isSummary: true,
226
+ summarizes: messages.map(m => m.id),
227
+ };
228
+ }
229
+ // =============================================================================
230
+ // Context Compaction
231
+ // =============================================================================
232
+ /**
233
+ * Context compaction manager.
234
+ */
235
+ export class ContextCompactor {
236
+ config;
237
+ constructor(config = {}) {
238
+ this.config = { ...DEFAULT_CONFIG, ...config };
239
+ }
240
+ /**
241
+ * Get current context window status.
242
+ */
243
+ getContextWindow(messages) {
244
+ const totalTokens = estimateContextTokens(messages);
245
+ return {
246
+ messages,
247
+ totalTokens,
248
+ maxTokens: this.config.maxTokens,
249
+ usagePercent: totalTokens / this.config.maxTokens,
250
+ };
251
+ }
252
+ /**
253
+ * Check if compaction is needed.
254
+ */
255
+ needsCompaction(messages) {
256
+ const window = this.getContextWindow(messages);
257
+ return window.usagePercent >= this.config.compactionThreshold;
258
+ }
259
+ /**
260
+ * Perform context compaction.
261
+ */
262
+ compact(messages) {
263
+ const window = this.getContextWindow(messages);
264
+ // No compaction needed
265
+ if (window.usagePercent < this.config.compactionThreshold) {
266
+ return {
267
+ messages,
268
+ messagesRemoved: 0,
269
+ tokensSaved: 0,
270
+ strategy: 'none',
271
+ };
272
+ }
273
+ const targetTokens = Math.floor(this.config.maxTokens * this.config.targetUsage);
274
+ let result = [...messages];
275
+ let strategy = 'none';
276
+ const originalTokens = window.totalTokens;
277
+ // Calculate importance for all messages
278
+ const importanceMap = new Map();
279
+ for (let i = 0; i < result.length; i++) {
280
+ importanceMap.set(result[i].id, calculateImportance(result[i], i, result.length));
281
+ }
282
+ // Strategy 1: Deduplicate similar messages
283
+ if (this.config.enableDeduplication) {
284
+ const duplicates = findDuplicates(result, this.config.deduplicationThreshold);
285
+ if (duplicates.size > 0) {
286
+ const toRemove = new Set();
287
+ for (const [, dups] of duplicates) {
288
+ for (const id of dups) {
289
+ toRemove.add(id);
290
+ }
291
+ }
292
+ result = result.filter(m => !toRemove.has(m.id));
293
+ if (toRemove.size > 0) {
294
+ strategy = 'deduplicate';
295
+ }
296
+ }
297
+ }
298
+ // Check if we've met target
299
+ if (estimateContextTokens(result) <= targetTokens) {
300
+ return {
301
+ messages: result,
302
+ messagesRemoved: messages.length - result.length,
303
+ tokensSaved: originalTokens - estimateContextTokens(result),
304
+ strategy,
305
+ };
306
+ }
307
+ // Strategy 2: Remove low-importance messages (keep recent)
308
+ const recentIds = new Set(result.slice(-this.config.keepRecentCount).map(m => m.id));
309
+ result = result.filter(m => {
310
+ if (recentIds.has(m.id))
311
+ return true;
312
+ if (m.isSummary)
313
+ return true;
314
+ if (m.role === 'system')
315
+ return true;
316
+ const importance = importanceMap.get(m.id) ?? 50;
317
+ return importance >= this.config.minImportanceRetain;
318
+ });
319
+ if (result.length < messages.length) {
320
+ strategy = 'trim_low_importance';
321
+ }
322
+ // Check if we've met target
323
+ if (estimateContextTokens(result) <= targetTokens) {
324
+ return {
325
+ messages: result,
326
+ messagesRemoved: messages.length - result.length,
327
+ tokensSaved: originalTokens - estimateContextTokens(result),
328
+ strategy,
329
+ };
330
+ }
331
+ // Strategy 3: Summarize old messages
332
+ if (this.config.enableSummarization) {
333
+ const messagesToSummarize = result.slice(0, -this.config.keepRecentCount)
334
+ .filter(m => !m.isSummary && m.role !== 'system');
335
+ if (messagesToSummarize.length >= 3) {
336
+ const summary = createSummary(messagesToSummarize);
337
+ const summaryIds = new Set(messagesToSummarize.map(m => m.id));
338
+ result = [
339
+ summary,
340
+ ...result.filter(m => !summaryIds.has(m.id)),
341
+ ];
342
+ strategy = 'summarize';
343
+ return {
344
+ messages: result,
345
+ messagesRemoved: messages.length - result.length,
346
+ tokensSaved: originalTokens - estimateContextTokens(result),
347
+ summaryAdded: summary,
348
+ strategy,
349
+ };
350
+ }
351
+ }
352
+ // Strategy 4: Aggressive trim (last resort)
353
+ while (estimateContextTokens(result) > targetTokens && result.length > this.config.keepRecentCount + 1) {
354
+ // Remove oldest non-system, non-summary message
355
+ const removeIndex = result.findIndex(m => !m.isSummary && m.role !== 'system');
356
+ if (removeIndex === -1)
357
+ break;
358
+ result.splice(removeIndex, 1);
359
+ }
360
+ strategy = 'aggressive';
361
+ return {
362
+ messages: result,
363
+ messagesRemoved: messages.length - result.length,
364
+ tokensSaved: originalTokens - estimateContextTokens(result),
365
+ strategy,
366
+ };
367
+ }
368
+ /**
369
+ * Add a message to context with automatic compaction if needed.
370
+ */
371
+ addMessage(messages, newMessage) {
372
+ const updated = [...messages, newMessage];
373
+ if (this.needsCompaction(updated)) {
374
+ const result = this.compact(updated);
375
+ return {
376
+ messages: result.messages,
377
+ compacted: true,
378
+ result,
379
+ };
380
+ }
381
+ return {
382
+ messages: updated,
383
+ compacted: false,
384
+ };
385
+ }
386
+ /**
387
+ * Get token budget remaining.
388
+ */
389
+ getTokenBudget(messages) {
390
+ const used = estimateContextTokens(messages);
391
+ return {
392
+ used,
393
+ remaining: this.config.maxTokens - used,
394
+ percentUsed: (used / this.config.maxTokens) * 100,
395
+ };
396
+ }
397
+ }
398
+ // =============================================================================
399
+ // Factory Function
400
+ // =============================================================================
401
+ /**
402
+ * Create a context compactor with the given configuration.
403
+ */
404
+ export function createContextCompactor(config) {
405
+ return new ContextCompactor(config);
406
+ }
407
+ // =============================================================================
408
+ // Utilities
409
+ // =============================================================================
410
+ /**
411
+ * Format token count for display.
412
+ */
413
+ export function formatTokenCount(tokens) {
414
+ if (tokens >= 1000000) {
415
+ return `${(tokens / 1000000).toFixed(1)}M`;
416
+ }
417
+ if (tokens >= 1000) {
418
+ return `${(tokens / 1000).toFixed(1)}k`;
419
+ }
420
+ return tokens.toString();
421
+ }
422
+ /**
423
+ * Benchmark token estimation performance.
424
+ */
425
+ export function benchmarkTokenEstimation(iterations = 10000) {
426
+ const testTexts = [
427
+ 'Hello world',
428
+ 'This is a longer piece of text that contains multiple sentences and should take more time to process.',
429
+ '```typescript\nfunction hello() {\n console.log("Hello");\n}\n```',
430
+ 'A'.repeat(10000), // 10k chars
431
+ ];
432
+ let maxNs = 0;
433
+ let totalTokens = 0;
434
+ const start = process.hrtime.bigint();
435
+ for (let i = 0; i < iterations; i++) {
436
+ for (const text of testTexts) {
437
+ const s = process.hrtime.bigint();
438
+ const tokens = estimateTokens(text);
439
+ totalTokens += tokens;
440
+ const elapsed = Number(process.hrtime.bigint() - s);
441
+ if (elapsed > maxNs)
442
+ maxNs = elapsed;
443
+ }
444
+ }
445
+ const totalNs = Number(process.hrtime.bigint() - start);
446
+ const totalMs = totalNs / 1_000_000;
447
+ return {
448
+ avgNs: totalNs / (iterations * testTexts.length),
449
+ maxNs,
450
+ tokensPerMs: totalTokens / totalMs,
451
+ };
452
+ }
453
+ //# sourceMappingURL=context-compaction.js.map
@@ -29,4 +29,5 @@ export * from './adapters/index.js';
29
29
  export { createMemoryAdapter, getMemoryConfigFromEnv } from './factory.js';
30
30
  export { createMemoryService } from './service.js';
31
31
  export { createMemoryHooks, getMemoryHooks } from './memory-hooks.js';
32
+ export * from './context-compaction.js';
32
33
  //# sourceMappingURL=index.d.ts.map
@@ -29,4 +29,5 @@ export * from './adapters/index.js';
29
29
  export { createMemoryAdapter, getMemoryConfigFromEnv } from './factory.js';
30
30
  export { createMemoryService } from './service.js';
31
31
  export { createMemoryHooks, getMemoryHooks } from './memory-hooks.js';
32
+ export * from './context-compaction.js';
32
33
  //# sourceMappingURL=index.js.map
@@ -6,7 +6,7 @@
6
6
  * - Channels for group communication
7
7
  * - Direct messaging between any combination of users and agents
8
8
  */
9
- import { v4 as uuid } from 'uuid';
9
+ import { generateId } from '../utils/id-generator.js';
10
10
  import { PROTOCOL_VERSION } from './types.js';
11
11
  // Re-export PROTOCOL_VERSION for convenience
12
12
  export { PROTOCOL_VERSION };
@@ -48,7 +48,7 @@ export function createChannelJoinEnvelope(from, channel, options) {
48
48
  return {
49
49
  v: PROTOCOL_VERSION,
50
50
  type: 'CHANNEL_JOIN',
51
- id: uuid(),
51
+ id: generateId(),
52
52
  ts: Date.now(),
53
53
  from,
54
54
  payload: {
@@ -65,7 +65,7 @@ export function createChannelLeaveEnvelope(from, channel, reason) {
65
65
  return {
66
66
  v: PROTOCOL_VERSION,
67
67
  type: 'CHANNEL_LEAVE',
68
- id: uuid(),
68
+ id: generateId(),
69
69
  ts: Date.now(),
70
70
  from,
71
71
  payload: {
@@ -81,7 +81,7 @@ export function createChannelMessageEnvelope(from, channel, body, options) {
81
81
  return {
82
82
  v: PROTOCOL_VERSION,
83
83
  type: 'CHANNEL_MESSAGE',
84
- id: uuid(),
84
+ id: generateId(),
85
85
  ts: Date.now(),
86
86
  from,
87
87
  payload: {
@@ -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