grov 0.5.11 → 0.6.13

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 (190) hide show
  1. package/dist/cli/agents/registry.d.ts +17 -0
  2. package/dist/cli/agents/registry.js +132 -0
  3. package/dist/cli/commands/agents.d.ts +1 -0
  4. package/dist/cli/commands/agents.js +48 -0
  5. package/dist/cli/commands/disable.d.ts +1 -0
  6. package/dist/cli/commands/disable.js +179 -0
  7. package/dist/cli/commands/doctor.d.ts +1 -0
  8. package/dist/cli/commands/doctor.js +157 -0
  9. package/dist/{commands → cli/commands}/drift-test.js +39 -26
  10. package/dist/cli/commands/init.d.ts +1 -0
  11. package/dist/cli/commands/init.js +90 -0
  12. package/dist/{commands → cli/commands}/login.js +19 -18
  13. package/dist/{commands → cli/commands}/logout.js +1 -1
  14. package/dist/{commands → cli/commands}/proxy-status.js +1 -1
  15. package/dist/cli/commands/setup.d.ts +6 -0
  16. package/dist/cli/commands/setup.js +309 -0
  17. package/dist/{commands → cli/commands}/status.js +1 -1
  18. package/dist/{commands → cli/commands}/sync.d.ts +1 -0
  19. package/dist/{commands → cli/commands}/sync.js +59 -4
  20. package/dist/{commands → cli/commands}/uninstall.js +2 -2
  21. package/dist/cli/index.js +270 -0
  22. package/dist/{lib → core/cloud}/cloud-sync.d.ts +3 -3
  23. package/dist/{lib → core/cloud}/cloud-sync.js +10 -10
  24. package/dist/{lib → core/extraction}/correction-builder-proxy.d.ts +1 -1
  25. package/dist/{lib → core/extraction}/correction-builder-proxy.js +0 -4
  26. package/dist/{lib → core/extraction}/drift-checker-proxy.d.ts +13 -9
  27. package/dist/core/extraction/drift-checker-proxy.js +510 -0
  28. package/dist/{lib → core/extraction}/llm-extractor.d.ts +8 -38
  29. package/dist/{lib → core/extraction}/llm-extractor.js +132 -220
  30. package/dist/{lib → core}/store/sessions.js +3 -19
  31. package/dist/core/store/store.d.ts +1 -0
  32. package/dist/{lib → core/store}/store.js +1 -1
  33. package/dist/{lib → core}/store/types.d.ts +0 -4
  34. package/dist/integrations/mcp/cache.d.ts +27 -0
  35. package/dist/integrations/mcp/cache.js +106 -0
  36. package/dist/integrations/mcp/capture/antigravity-parser.d.ts +26 -0
  37. package/dist/integrations/mcp/capture/antigravity-parser.js +272 -0
  38. package/dist/integrations/mcp/capture/antigravity-scanner.d.ts +24 -0
  39. package/dist/integrations/mcp/capture/antigravity-scanner.js +153 -0
  40. package/dist/integrations/mcp/capture/antigravity-sync-tracker.d.ts +29 -0
  41. package/dist/integrations/mcp/capture/antigravity-sync-tracker.js +115 -0
  42. package/dist/integrations/mcp/capture/cli-extractor.d.ts +18 -0
  43. package/dist/integrations/mcp/capture/cli-extractor.js +258 -0
  44. package/dist/integrations/mcp/capture/cli-synced.d.ts +4 -0
  45. package/dist/integrations/mcp/capture/cli-synced.js +62 -0
  46. package/dist/integrations/mcp/capture/cli-transform.d.ts +30 -0
  47. package/dist/integrations/mcp/capture/cli-transform.js +62 -0
  48. package/dist/integrations/mcp/capture/cli-watcher.d.ts +31 -0
  49. package/dist/integrations/mcp/capture/cli-watcher.js +106 -0
  50. package/dist/integrations/mcp/capture/hook-handler.d.ts +2 -0
  51. package/dist/integrations/mcp/capture/hook-handler.js +157 -0
  52. package/dist/integrations/mcp/capture/sqlite-reader.d.ts +35 -0
  53. package/dist/integrations/mcp/capture/sqlite-reader.js +388 -0
  54. package/dist/integrations/mcp/capture/sync-tracker.d.ts +16 -0
  55. package/dist/integrations/mcp/capture/sync-tracker.js +102 -0
  56. package/dist/integrations/mcp/clients/cursor/rules-installer.d.ts +19 -0
  57. package/dist/integrations/mcp/clients/cursor/rules-installer.js +123 -0
  58. package/dist/integrations/mcp/index.d.ts +1 -0
  59. package/dist/integrations/mcp/index.js +94 -0
  60. package/dist/integrations/mcp/logger.d.ts +8 -0
  61. package/dist/integrations/mcp/logger.js +50 -0
  62. package/dist/integrations/mcp/server.d.ts +5 -0
  63. package/dist/integrations/mcp/server.js +58 -0
  64. package/dist/integrations/mcp/tools/expand.d.ts +1 -0
  65. package/dist/integrations/mcp/tools/expand.js +53 -0
  66. package/dist/integrations/mcp/tools/preview.d.ts +1 -0
  67. package/dist/integrations/mcp/tools/preview.js +64 -0
  68. package/dist/integrations/proxy/agents/base.d.ts +43 -0
  69. package/dist/integrations/proxy/agents/base.js +13 -0
  70. package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.d.ts +4 -8
  71. package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.js +4 -33
  72. package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.d.ts +1 -1
  73. package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.js +22 -6
  74. package/dist/integrations/proxy/agents/claude/index.d.ts +43 -0
  75. package/dist/integrations/proxy/agents/claude/index.js +386 -0
  76. package/dist/{proxy/action-parser.d.ts → integrations/proxy/agents/claude/parser.d.ts} +1 -1
  77. package/dist/integrations/proxy/agents/codex/extractors.d.ts +6 -0
  78. package/dist/integrations/proxy/agents/codex/extractors.js +49 -0
  79. package/dist/integrations/proxy/agents/codex/forwarder.d.ts +9 -0
  80. package/dist/integrations/proxy/agents/codex/forwarder.js +125 -0
  81. package/dist/integrations/proxy/agents/codex/index.d.ts +44 -0
  82. package/dist/integrations/proxy/agents/codex/index.js +371 -0
  83. package/dist/integrations/proxy/agents/codex/parser.d.ts +11 -0
  84. package/dist/integrations/proxy/agents/codex/parser.js +104 -0
  85. package/dist/integrations/proxy/agents/codex/patch.d.ts +12 -0
  86. package/dist/integrations/proxy/agents/codex/patch.js +40 -0
  87. package/dist/integrations/proxy/agents/codex/settings.d.ts +18 -0
  88. package/dist/integrations/proxy/agents/codex/settings.js +73 -0
  89. package/dist/integrations/proxy/agents/codex/types.d.ts +59 -0
  90. package/dist/integrations/proxy/agents/codex/types.js +2 -0
  91. package/dist/integrations/proxy/agents/index.d.ts +11 -0
  92. package/dist/integrations/proxy/agents/index.js +25 -0
  93. package/dist/integrations/proxy/agents/types.d.ts +77 -0
  94. package/dist/integrations/proxy/agents/types.js +2 -0
  95. package/dist/{proxy → integrations/proxy/cache}/extended-cache.js +2 -6
  96. package/dist/{proxy → integrations/proxy}/config.js +1 -1
  97. package/dist/{proxy → integrations/proxy}/handlers/preprocess.d.ts +3 -3
  98. package/dist/integrations/proxy/handlers/preprocess.js +194 -0
  99. package/dist/integrations/proxy/index.js +20 -0
  100. package/dist/integrations/proxy/injection/memory-injection.d.ts +56 -0
  101. package/dist/integrations/proxy/injection/memory-injection.js +252 -0
  102. package/dist/integrations/proxy/orchestrator.d.ts +30 -0
  103. package/dist/integrations/proxy/orchestrator.js +954 -0
  104. package/dist/integrations/proxy/request-processor.d.ts +14 -0
  105. package/dist/integrations/proxy/request-processor.js +68 -0
  106. package/dist/{proxy → integrations/proxy}/response-processor.d.ts +4 -3
  107. package/dist/{proxy → integrations/proxy}/response-processor.js +51 -43
  108. package/dist/{proxy → integrations/proxy}/server.d.ts +0 -1
  109. package/dist/integrations/proxy/server.js +146 -0
  110. package/dist/{proxy → integrations/proxy}/types.d.ts +4 -0
  111. package/dist/{proxy → integrations/proxy}/utils/logging.d.ts +1 -0
  112. package/dist/{proxy → integrations/proxy}/utils/logging.js +5 -0
  113. package/package.json +31 -10
  114. package/postinstall.js +62 -6
  115. package/dist/cli.js +0 -149
  116. package/dist/commands/capture.d.ts +0 -6
  117. package/dist/commands/capture.js +0 -324
  118. package/dist/commands/disable.d.ts +0 -1
  119. package/dist/commands/disable.js +0 -14
  120. package/dist/commands/doctor.d.ts +0 -1
  121. package/dist/commands/doctor.js +0 -89
  122. package/dist/commands/init.d.ts +0 -1
  123. package/dist/commands/init.js +0 -52
  124. package/dist/commands/inject.d.ts +0 -5
  125. package/dist/commands/inject.js +0 -88
  126. package/dist/commands/prompt-inject.d.ts +0 -4
  127. package/dist/commands/prompt-inject.js +0 -451
  128. package/dist/commands/unregister.d.ts +0 -1
  129. package/dist/commands/unregister.js +0 -28
  130. package/dist/lib/anchor-extractor.d.ts +0 -30
  131. package/dist/lib/anchor-extractor.js +0 -296
  132. package/dist/lib/correction-builder.d.ts +0 -10
  133. package/dist/lib/correction-builder.js +0 -226
  134. package/dist/lib/drift-checker-proxy.js +0 -373
  135. package/dist/lib/drift-checker.d.ts +0 -66
  136. package/dist/lib/drift-checker.js +0 -341
  137. package/dist/lib/hooks.d.ts +0 -38
  138. package/dist/lib/hooks.js +0 -291
  139. package/dist/lib/jsonl-parser.d.ts +0 -87
  140. package/dist/lib/jsonl-parser.js +0 -281
  141. package/dist/lib/session-parser.d.ts +0 -44
  142. package/dist/lib/session-parser.js +0 -256
  143. package/dist/lib/store.d.ts +0 -1
  144. package/dist/proxy/cache.d.ts +0 -32
  145. package/dist/proxy/cache.js +0 -47
  146. package/dist/proxy/handlers/preprocess.js +0 -186
  147. package/dist/proxy/index.js +0 -30
  148. package/dist/proxy/injection/delta-tracking.d.ts +0 -11
  149. package/dist/proxy/injection/delta-tracking.js +0 -94
  150. package/dist/proxy/injection/injectors.d.ts +0 -7
  151. package/dist/proxy/injection/injectors.js +0 -139
  152. package/dist/proxy/request-processor.d.ts +0 -27
  153. package/dist/proxy/request-processor.js +0 -233
  154. package/dist/proxy/server.js +0 -1289
  155. /package/dist/{commands → cli/commands}/drift-test.d.ts +0 -0
  156. /package/dist/{commands → cli/commands}/login.d.ts +0 -0
  157. /package/dist/{commands → cli/commands}/logout.d.ts +0 -0
  158. /package/dist/{commands → cli/commands}/proxy-status.d.ts +0 -0
  159. /package/dist/{commands → cli/commands}/status.d.ts +0 -0
  160. /package/dist/{commands → cli/commands}/uninstall.d.ts +0 -0
  161. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  162. /package/dist/{lib → core/cloud}/api-client.d.ts +0 -0
  163. /package/dist/{lib → core/cloud}/api-client.js +0 -0
  164. /package/dist/{lib → core/cloud}/credentials.d.ts +0 -0
  165. /package/dist/{lib → core/cloud}/credentials.js +0 -0
  166. /package/dist/{lib → core}/store/convenience.d.ts +0 -0
  167. /package/dist/{lib → core}/store/convenience.js +0 -0
  168. /package/dist/{lib → core}/store/database.d.ts +0 -0
  169. /package/dist/{lib → core}/store/database.js +0 -0
  170. /package/dist/{lib → core}/store/drift.d.ts +0 -0
  171. /package/dist/{lib → core}/store/drift.js +0 -0
  172. /package/dist/{lib → core}/store/index.d.ts +0 -0
  173. /package/dist/{lib → core}/store/index.js +0 -0
  174. /package/dist/{lib → core}/store/sessions.d.ts +0 -0
  175. /package/dist/{lib → core}/store/steps.d.ts +0 -0
  176. /package/dist/{lib → core}/store/steps.js +0 -0
  177. /package/dist/{lib → core}/store/tasks.d.ts +0 -0
  178. /package/dist/{lib → core}/store/tasks.js +0 -0
  179. /package/dist/{lib → core}/store/types.js +0 -0
  180. /package/dist/{proxy/action-parser.js → integrations/proxy/agents/claude/parser.js} +0 -0
  181. /package/dist/{lib → integrations/proxy/agents/claude}/settings.d.ts +0 -0
  182. /package/dist/{lib → integrations/proxy/agents/claude}/settings.js +0 -0
  183. /package/dist/{proxy → integrations/proxy/cache}/extended-cache.d.ts +0 -0
  184. /package/dist/{proxy → integrations/proxy}/config.d.ts +0 -0
  185. /package/dist/{proxy → integrations/proxy}/index.d.ts +0 -0
  186. /package/dist/{proxy → integrations/proxy}/types.js +0 -0
  187. /package/dist/{lib → utils}/debug.d.ts +0 -0
  188. /package/dist/{lib → utils}/debug.js +0 -0
  189. /package/dist/{lib → utils}/utils.d.ts +0 -0
  190. /package/dist/{lib → utils}/utils.js +0 -0
@@ -1,1289 +0,0 @@
1
- // Grov Proxy Server - Fastify + undici
2
- // Intercepts Claude Code <-> Anthropic API traffic for drift detection and context injection
3
- import Fastify from 'fastify';
4
- import { config, buildSafeHeaders } from './config.js';
5
- import { forwardToAnthropic, isForwardError } from './forwarder.js';
6
- import { extendedCache, evictOldestCacheEntry, checkExtendedCache, log } from './extended-cache.js';
7
- import { setDebugMode, getNextRequestId, taskLog, proxyLog, logTokenUsage } from './utils/logging.js';
8
- import { detectKeyDecision, extractTextContent, extractProjectPath, extractGoalFromMessages, extractConversationHistory } from './utils/extractors.js';
9
- import { appendToLastUserMessage, injectIntoRawBody } from './injection/injectors.js';
10
- import { preProcessRequest, setPendingPlanClear } from './handlers/preprocess.js';
11
- import { parseToolUseBlocks, extractTokenUsage } from './action-parser.js';
12
- import { createSessionState, getSessionState, updateSessionState, createStep, updateTokenCount, logDriftEvent, getRecentSteps, getValidatedSteps, updateSessionMode, markWaitingForRecovery, incrementEscalation, updateLastChecked, getActiveSessionForUser, deleteSessionState, deleteStepsForSession, updateRecentStepsReasoning, markSessionCompleted, getCompletedSessionForProject, cleanupOldCompletedSessions, cleanupStaleActiveSessions, clearStalePendingCorrections, cleanupFailedSyncTasks, } from '../lib/store.js';
13
- import { checkDrift, scoreToCorrectionLevel, shouldSkipSteps, isDriftCheckAvailable, checkRecoveryAlignment, generateForcedRecovery, } from '../lib/drift-checker-proxy.js';
14
- import { buildCorrection, formatCorrectionForInjection } from '../lib/correction-builder-proxy.js';
15
- import { generateSessionSummary, isSummaryAvailable, extractIntent, isIntentExtractionAvailable, analyzeTaskContext, isTaskAnalysisAvailable, } from '../lib/llm-extractor.js';
16
- import { saveToTeamMemory } from './response-processor.js';
17
- import { randomUUID } from 'crypto';
18
- // Store last drift result for recovery alignment check
19
- const lastDriftResults = new Map();
20
- // Server logger reference (set in startServer)
21
- let serverLog = null;
22
- // Track last messageCount per session to detect retries vs new turns
23
- const lastMessageCount = new Map();
24
- // Session tracking (in-memory for active sessions)
25
- const activeSessions = new Map();
26
- /**
27
- * Create and configure the Fastify server
28
- */
29
- export function createServer() {
30
- const fastify = Fastify({
31
- logger: false, // Disabled - all debug goes to ~/.grov/debug.log
32
- bodyLimit: config.BODY_LIMIT,
33
- });
34
- // Custom JSON parser that preserves raw bytes for cache preservation
35
- fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
36
- // Store raw bytes on request for later use
37
- req.rawBody = body;
38
- try {
39
- const json = JSON.parse(body.toString('utf-8'));
40
- done(null, json);
41
- }
42
- catch (err) {
43
- done(err, undefined);
44
- }
45
- });
46
- // Health check endpoint
47
- fastify.get('/health', async () => {
48
- return { status: 'ok', timestamp: new Date().toISOString() };
49
- });
50
- // Main messages endpoint
51
- fastify.post('/v1/messages', handleMessages);
52
- // Catch-all for other Anthropic endpoints (pass through)
53
- fastify.all('/*', async (request, reply) => {
54
- fastify.log.warn(`Unhandled endpoint: ${request.method} ${request.url}`);
55
- return reply.status(404).send({ error: 'Not found' });
56
- });
57
- return fastify;
58
- }
59
- /**
60
- * Handle /v1/messages requests
61
- */
62
- async function handleMessages(request, reply) {
63
- const logger = request.log;
64
- const startTime = Date.now();
65
- const model = request.body.model;
66
- if (model.includes('haiku')) {
67
- logger.info({ msg: 'Skipping Haiku subagent', model });
68
- try {
69
- // Force non-streaming for Haiku too
70
- const haikusBody = { ...request.body, stream: false };
71
- const result = await forwardToAnthropic(haikusBody, request.headers, logger);
72
- return reply
73
- .status(result.statusCode)
74
- .header('content-type', 'application/json')
75
- .headers(filterResponseHeaders(result.headers))
76
- .send(JSON.stringify(result.body));
77
- }
78
- catch (error) {
79
- logger.error({ msg: 'Haiku forward error', error: String(error) });
80
- return reply
81
- .status(502)
82
- .header('content-type', 'application/json')
83
- .send(JSON.stringify({ error: { type: 'proxy_error', message: 'Bad gateway' } }));
84
- }
85
- }
86
- // === MAIN MODEL TRACKING (Opus/Sonnet) ===
87
- // Get or create session (async for intent extraction)
88
- const sessionInfo = await getOrCreateSession(request, logger);
89
- sessionInfo.promptCount++;
90
- // Update in-memory map
91
- activeSessions.set(sessionInfo.sessionId, {
92
- sessionId: sessionInfo.sessionId,
93
- promptCount: sessionInfo.promptCount,
94
- projectPath: sessionInfo.projectPath,
95
- });
96
- const currentRequestId = getNextRequestId();
97
- logger.info({
98
- msg: 'Incoming request',
99
- sessionId: sessionInfo.sessionId.substring(0, 8),
100
- promptCount: sessionInfo.promptCount,
101
- model: request.body.model,
102
- messageCount: request.body.messages?.length || 0,
103
- });
104
- // Log REQUEST to file
105
- const rawBodySize = request.rawBody?.length || 0;
106
- proxyLog({
107
- requestId: currentRequestId,
108
- type: 'REQUEST',
109
- sessionId: sessionInfo.sessionId.substring(0, 8),
110
- data: {
111
- model: request.body.model,
112
- messageCount: request.body.messages?.length || 0,
113
- promptCount: sessionInfo.promptCount,
114
- rawBodySize,
115
- },
116
- });
117
- // Process request to get injection text
118
- // __grovInjection = team memory (system prompt, cached)
119
- // __grovUserMsgInjection = dynamic content (user message, delta only)
120
- const processedBody = await preProcessRequest(request.body, sessionInfo, logger, detectRequestType);
121
- const systemInjection = processedBody.__grovInjection;
122
- const userMsgInjection = processedBody.__grovUserMsgInjection;
123
- // Get raw body bytes
124
- const rawBody = request.rawBody;
125
- let rawBodyStr = rawBody?.toString('utf-8') || '';
126
- // Track injection sizes for logging
127
- let systemInjectionSize = 0;
128
- let userMsgInjectionSize = 0;
129
- let systemSuccess = false;
130
- let userMsgSuccess = false;
131
- // 1. Inject team memory into SYSTEM prompt (cached, constant)
132
- if (systemInjection && rawBodyStr) {
133
- const result = injectIntoRawBody(rawBodyStr, '\n\n' + systemInjection);
134
- rawBodyStr = result.modified;
135
- systemInjectionSize = systemInjection.length;
136
- systemSuccess = result.success;
137
- }
138
- // 2. Inject dynamic content into LAST USER MESSAGE (delta only)
139
- if (userMsgInjection && rawBodyStr) {
140
- rawBodyStr = appendToLastUserMessage(rawBodyStr, userMsgInjection);
141
- userMsgInjectionSize = userMsgInjection.length;
142
- userMsgSuccess = true; // appendToLastUserMessage doesn't return success flag
143
- }
144
- // Determine final body to send
145
- let finalBodyToSend;
146
- if (systemInjection || userMsgInjection) {
147
- finalBodyToSend = rawBodyStr;
148
- // Log INJECTION to file with full details
149
- const wasCached = processedBody.__grovInjectionCached;
150
- proxyLog({
151
- requestId: currentRequestId,
152
- type: 'INJECTION',
153
- sessionId: sessionInfo.sessionId.substring(0, 8),
154
- data: {
155
- systemInjectionSize,
156
- userMsgInjectionSize,
157
- totalInjectionSize: systemInjectionSize + userMsgInjectionSize,
158
- originalSize: rawBody?.length || 0,
159
- finalSize: rawBodyStr.length,
160
- systemSuccess,
161
- userMsgSuccess,
162
- teamMemoryCached: wasCached,
163
- // Include actual content for debugging (truncated for log readability)
164
- systemInjectionPreview: systemInjection ? systemInjection.substring(0, 200) + (systemInjection.length > 200 ? '...' : '') : null,
165
- userMsgInjectionContent: userMsgInjection || null, // Full content since it's small
166
- },
167
- });
168
- }
169
- else if (rawBody) {
170
- // No injection, use original raw bytes
171
- finalBodyToSend = rawBody;
172
- }
173
- else {
174
- // Fallback to re-serialization (shouldn't happen normally)
175
- finalBodyToSend = JSON.stringify(processedBody);
176
- }
177
- const forwardStart = Date.now();
178
- try {
179
- // Forward: raw bytes (with injection inserted) or original raw bytes
180
- const result = await forwardToAnthropic(processedBody, request.headers, logger, typeof finalBodyToSend === 'string' ? Buffer.from(finalBodyToSend, 'utf-8') : finalBodyToSend);
181
- const forwardLatency = Date.now() - forwardStart;
182
- // FIRE-AND-FORGET: Don't block response to Claude Code
183
- // This prevents retry loops caused by Haiku calls adding latency
184
- if (result.statusCode === 200 && isAnthropicResponse(result.body)) {
185
- // Prepare extended cache data (only if enabled)
186
- const extendedCacheData = config.EXTENDED_CACHE_ENABLED ? {
187
- headers: buildSafeHeaders(request.headers),
188
- rawBody: typeof finalBodyToSend === 'string' ? Buffer.from(finalBodyToSend, 'utf-8') : finalBodyToSend,
189
- } : undefined;
190
- postProcessResponse(result.body, sessionInfo, request.body, logger, extendedCacheData)
191
- .catch(err => console.error('[GROV] postProcess error:', err));
192
- }
193
- const latency = Date.now() - startTime;
194
- const filteredHeaders = filterResponseHeaders(result.headers);
195
- // Log token usage (always to console, file only in debug mode)
196
- if (isAnthropicResponse(result.body)) {
197
- const usage = extractTokenUsage(result.body);
198
- // Console: compact token summary (always shown)
199
- logTokenUsage(currentRequestId, usage, latency);
200
- // File: detailed response log (debug mode only)
201
- proxyLog({
202
- requestId: currentRequestId,
203
- type: 'RESPONSE',
204
- sessionId: sessionInfo.sessionId.substring(0, 8),
205
- data: {
206
- statusCode: result.statusCode,
207
- latencyMs: latency,
208
- forwardLatencyMs: forwardLatency,
209
- inputTokens: usage.inputTokens,
210
- outputTokens: usage.outputTokens,
211
- cacheCreation: usage.cacheCreation,
212
- cacheRead: usage.cacheRead,
213
- cacheHitRatio: usage.cacheRead > 0 ? (usage.cacheRead / (usage.cacheRead + usage.cacheCreation)).toFixed(2) : '0.00',
214
- wasSSE: result.wasSSE,
215
- },
216
- });
217
- }
218
- // If response was SSE, forward raw SSE to Claude Code (it expects streaming)
219
- // Otherwise, send JSON
220
- const isSSEResponse = result.wasSSE;
221
- const responseContentType = isSSEResponse ? 'text/event-stream; charset=utf-8' : 'application/json';
222
- const responseBody = isSSEResponse ? result.rawBody : JSON.stringify(result.body);
223
- logger.info({
224
- msg: 'Request complete',
225
- statusCode: result.statusCode,
226
- latencyMs: latency,
227
- wasSSE: isSSEResponse,
228
- });
229
- return reply
230
- .status(result.statusCode)
231
- .header('content-type', responseContentType)
232
- .headers(filteredHeaders)
233
- .send(responseBody);
234
- }
235
- catch (error) {
236
- if (isForwardError(error)) {
237
- logger.error({
238
- msg: 'Forward error',
239
- type: error.type,
240
- message: error.message,
241
- });
242
- return reply
243
- .status(error.statusCode || 502)
244
- .header('content-type', 'application/json')
245
- .send(JSON.stringify({
246
- error: {
247
- type: 'proxy_error',
248
- message: error.type === 'timeout' ? 'Gateway timeout' : 'Bad gateway',
249
- },
250
- }));
251
- }
252
- logger.error({
253
- msg: 'Unexpected error',
254
- error: String(error),
255
- });
256
- return reply
257
- .status(500)
258
- .header('content-type', 'application/json')
259
- .send(JSON.stringify({
260
- error: {
261
- type: 'internal_error',
262
- message: 'Internal proxy error',
263
- },
264
- }));
265
- }
266
- }
267
- /**
268
- * Get or create session info for this request
269
- */
270
- async function getOrCreateSession(request, logger) {
271
- // Determine project path from request
272
- const projectPath = extractProjectPath(request.body) || process.cwd();
273
- // Try to find existing active session for this project
274
- // Task orchestration will happen in postProcessResponse using analyzeTaskContext
275
- const existingSession = getActiveSessionForUser(projectPath);
276
- if (existingSession) {
277
- // Found active session - will be validated by task orchestration later
278
- let sessionInfo = activeSessions.get(existingSession.session_id);
279
- if (!sessionInfo) {
280
- sessionInfo = {
281
- sessionId: existingSession.session_id,
282
- promptCount: 0,
283
- projectPath,
284
- };
285
- activeSessions.set(existingSession.session_id, sessionInfo);
286
- }
287
- logger.info({
288
- msg: 'Found existing session',
289
- sessionId: existingSession.session_id.substring(0, 8),
290
- goal: existingSession.original_goal?.substring(0, 50),
291
- });
292
- return { ...sessionInfo, isNew: false, currentSession: existingSession, completedSession: null };
293
- }
294
- // No active session - check for recently completed session (for new_task detection)
295
- const completedSession = getCompletedSessionForProject(projectPath);
296
- if (completedSession) {
297
- logger.info({
298
- msg: 'Found recently completed session for comparison',
299
- sessionId: completedSession.session_id.substring(0, 8),
300
- goal: completedSession.original_goal?.substring(0, 50),
301
- });
302
- }
303
- // No existing session - create placeholder, real session will be created in postProcessResponse
304
- const tempSessionId = randomUUID();
305
- const sessionInfo = {
306
- sessionId: tempSessionId,
307
- promptCount: 0,
308
- projectPath,
309
- };
310
- activeSessions.set(tempSessionId, sessionInfo);
311
- // Note: team memory is now GLOBAL (not per session), no propagation needed
312
- logger.info({ msg: 'No existing session, will create after task analysis' });
313
- return { ...sessionInfo, isNew: true, currentSession: null, completedSession };
314
- }
315
- /**
316
- * Detect request type: 'first', 'continuation', or 'retry'
317
- * - first: new user message (messageCount changed, last msg is user without tool_result)
318
- * - continuation: tool result (messageCount changed, last msg has tool_result)
319
- * - retry: same messageCount as before
320
- */
321
- function detectRequestType(messages, projectPath) {
322
- const currentCount = messages?.length || 0;
323
- const lastCount = lastMessageCount.get(projectPath);
324
- lastMessageCount.set(projectPath, currentCount);
325
- // Same messageCount = retry
326
- if (lastCount !== undefined && currentCount === lastCount) {
327
- return 'retry';
328
- }
329
- // No messages or no last message = first
330
- if (!messages || messages.length === 0)
331
- return 'first';
332
- const lastMessage = messages[messages.length - 1];
333
- // Check if last message is tool_result (continuation)
334
- if (lastMessage.role === 'user') {
335
- const content = lastMessage.content;
336
- if (Array.isArray(content)) {
337
- const hasToolResult = content.some((block) => typeof block === 'object' && block !== null && block.type === 'tool_result');
338
- if (hasToolResult)
339
- return 'continuation';
340
- }
341
- }
342
- return 'first';
343
- }
344
- /**
345
- * Post-process response after receiving from Anthropic
346
- * - Task orchestration (new/continue/subtask/complete)
347
- * - Parse tool_use blocks
348
- * - Update token count
349
- * - Save step to DB
350
- * - Drift check (every N prompts)
351
- * - Recovery alignment check (Section 4.4)
352
- * - Team memory triggers (Section 4.6)
353
- */
354
- async function postProcessResponse(response, sessionInfo, requestBody, logger, extendedCacheData) {
355
- // Parse tool_use blocks
356
- const actions = parseToolUseBlocks(response);
357
- // Extract text content for analysis
358
- const textContent = extractTextContent(response);
359
- // Extract latest user message from request
360
- const latestUserMessage = extractGoalFromMessages(requestBody.messages) || '';
361
- // DEBUG: Commented out for cleaner terminal - uncomment when debugging
362
- // console.log(`[DEBUG] latestUserMessage extracted: "${latestUserMessage.substring(0, 80)}..." (${latestUserMessage.length} chars)`);
363
- // Get recent steps for context
364
- const recentSteps = sessionInfo.currentSession
365
- ? getRecentSteps(sessionInfo.currentSession.session_id, 5)
366
- : [];
367
- // === TASK ORCHESTRATION (Part 8) ===
368
- let activeSessionId = sessionInfo.sessionId;
369
- let activeSession = sessionInfo.currentSession;
370
- // Only run task orchestration on end_turn (when Claude finishes responding to user)
371
- // This reduces Haiku calls from ~11 per prompt to ~1-2
372
- const isEndTurn = response.stop_reason === 'end_turn';
373
- // Skip Warmup messages (Claude Code internal initialization)
374
- const isWarmup = latestUserMessage.toLowerCase().trim() === 'warmup';
375
- if (isWarmup) {
376
- return;
377
- }
378
- // === EXTENDED CACHE: Capture for keep-alive ===
379
- // Only capture on end_turn (user idle starts now, not during tool_use loops)
380
- if (isEndTurn && extendedCacheData) {
381
- const rawStr = extendedCacheData.rawBody.toString('utf-8');
382
- const hasSystem = rawStr.includes('"system"');
383
- const hasTools = rawStr.includes('"tools"');
384
- const hasCacheCtrl = rawStr.includes('"cache_control"');
385
- const msgMatch = rawStr.match(/"messages"\s*:\s*\[/);
386
- const msgPos = msgMatch?.index ?? -1;
387
- // Use projectPath as key (one entry per conversation, not per task)
388
- const cacheKey = sessionInfo.projectPath;
389
- // Evict oldest if at capacity (only for NEW entries, not updates)
390
- if (!extendedCache.has(cacheKey)) {
391
- evictOldestCacheEntry();
392
- }
393
- extendedCache.set(cacheKey, {
394
- headers: extendedCacheData.headers,
395
- rawBody: extendedCacheData.rawBody,
396
- timestamp: Date.now(),
397
- keepAliveCount: 0,
398
- });
399
- // Cache entry captured silently
400
- }
401
- // If not end_turn (tool_use in progress), skip task orchestration but keep session
402
- if (!isEndTurn) {
403
- // Use existing session or create minimal one without LLM calls
404
- if (sessionInfo.currentSession) {
405
- activeSessionId = sessionInfo.currentSession.session_id;
406
- activeSession = sessionInfo.currentSession;
407
- // console.log(`[DEBUG] PATH A: Reusing existing session ${activeSessionId}, raw_user_prompt="${activeSession.raw_user_prompt?.substring(0, 50)}"`);
408
- }
409
- else if (!activeSession) {
410
- // First request, create session without task analysis
411
- // NOTE: Don't set original_goal to user prompt - let analyzeTaskContext synthesize it
412
- // If we set it here, Haiku will "keep it stable" instead of synthesizing
413
- const newSessionId = randomUUID();
414
- // console.log(`[DEBUG] PATH B: Creating NEW session, raw_user_prompt will be: "${latestUserMessage.substring(0, 50)}"`);
415
- activeSession = createSessionState({
416
- session_id: newSessionId,
417
- project_path: sessionInfo.projectPath,
418
- original_goal: '', // Empty - will be synthesized by analyzeTaskContext later
419
- raw_user_prompt: latestUserMessage.substring(0, 500),
420
- task_type: 'main',
421
- });
422
- // console.log(`[DEBUG] PATH B: Session created, raw_user_prompt="${activeSession.raw_user_prompt?.substring(0, 50)}"`);
423
- activeSessionId = newSessionId;
424
- activeSessions.set(newSessionId, {
425
- sessionId: newSessionId,
426
- promptCount: 1,
427
- projectPath: sessionInfo.projectPath,
428
- });
429
- // Note: team memory is now GLOBAL (not per session), no propagation needed
430
- }
431
- }
432
- else if (isTaskAnalysisAvailable()) {
433
- // Use completed session for comparison if no active session
434
- const sessionForComparison = sessionInfo.currentSession || sessionInfo.completedSession;
435
- // Extract conversation history for context-aware task analysis
436
- const conversationHistory = extractConversationHistory(requestBody.messages || []);
437
- try {
438
- const taskAnalysis = await analyzeTaskContext(sessionForComparison, latestUserMessage, recentSteps, textContent, conversationHistory);
439
- logger.info({
440
- msg: 'Task analysis',
441
- action: taskAnalysis.action,
442
- task_type: taskAnalysis.task_type,
443
- goal: taskAnalysis.current_goal?.substring(0, 50),
444
- reasoning: taskAnalysis.reasoning,
445
- });
446
- // TASK LOG: Analysis result
447
- taskLog('TASK_ANALYSIS', {
448
- sessionId: sessionInfo.sessionId,
449
- action: taskAnalysis.action,
450
- task_type: taskAnalysis.task_type,
451
- goal: taskAnalysis.current_goal || '',
452
- reasoning: taskAnalysis.reasoning || '',
453
- userMessage: latestUserMessage.substring(0, 80),
454
- hasCurrentSession: !!sessionInfo.currentSession,
455
- hasCompletedSession: !!sessionInfo.completedSession,
456
- });
457
- // Update recent steps with reasoning (backfill from end_turn response)
458
- if (taskAnalysis.step_reasoning && activeSessionId) {
459
- const updatedCount = updateRecentStepsReasoning(activeSessionId, taskAnalysis.step_reasoning);
460
- // TASK LOG: Step reasoning update
461
- taskLog('STEP_REASONING', {
462
- sessionId: activeSessionId,
463
- stepsUpdated: updatedCount,
464
- reasoningEntries: Object.keys(taskAnalysis.step_reasoning).length,
465
- stepIds: Object.keys(taskAnalysis.step_reasoning).join(','),
466
- });
467
- }
468
- // Handle task orchestration based on analysis
469
- switch (taskAnalysis.action) {
470
- case 'continue':
471
- // Use existing session or reactivate completed session
472
- if (sessionInfo.currentSession) {
473
- activeSessionId = sessionInfo.currentSession.session_id;
474
- activeSession = sessionInfo.currentSession;
475
- // Update goal if Haiku detected a new instruction from user
476
- // (same task/topic, but new specific instruction)
477
- if (taskAnalysis.current_goal &&
478
- taskAnalysis.current_goal !== activeSession.original_goal &&
479
- latestUserMessage.length > 30) {
480
- updateSessionState(activeSessionId, {
481
- original_goal: taskAnalysis.current_goal,
482
- });
483
- activeSession.original_goal = taskAnalysis.current_goal;
484
- }
485
- // TASK LOG: Continue existing session
486
- taskLog('ORCHESTRATION_CONTINUE', {
487
- sessionId: activeSessionId,
488
- source: 'current_session',
489
- goal: activeSession.original_goal,
490
- goalUpdated: taskAnalysis.current_goal !== activeSession.original_goal,
491
- });
492
- }
493
- else if (sessionInfo.completedSession) {
494
- // Reactivate completed session (user wants to continue/add to it)
495
- activeSessionId = sessionInfo.completedSession.session_id;
496
- activeSession = sessionInfo.completedSession;
497
- updateSessionState(activeSessionId, {
498
- status: 'active',
499
- original_goal: taskAnalysis.current_goal || activeSession.original_goal,
500
- });
501
- activeSession.status = 'active';
502
- activeSessions.set(activeSessionId, {
503
- sessionId: activeSessionId,
504
- promptCount: 1,
505
- projectPath: sessionInfo.projectPath,
506
- });
507
- // Note: team memory is now GLOBAL (not per session), no propagation needed
508
- // TASK LOG: Reactivate completed session
509
- taskLog('ORCHESTRATION_CONTINUE', {
510
- sessionId: activeSessionId,
511
- source: 'reactivated_completed',
512
- goal: activeSession.original_goal,
513
- });
514
- }
515
- break;
516
- case 'new_task': {
517
- // Clean up completed session if it exists (it was kept for comparison)
518
- if (sessionInfo.completedSession) {
519
- deleteStepsForSession(sessionInfo.completedSession.session_id);
520
- deleteSessionState(sessionInfo.completedSession.session_id);
521
- }
522
- // Extract full intent for new task (goal, scope, constraints, keywords)
523
- let intentData = {
524
- goal: taskAnalysis.current_goal,
525
- expected_scope: [],
526
- constraints: [],
527
- keywords: [],
528
- };
529
- if (isIntentExtractionAvailable() && latestUserMessage.length > 10) {
530
- try {
531
- intentData = await extractIntent(latestUserMessage);
532
- logger.info({ msg: 'Intent extracted for new task', scopeCount: intentData.expected_scope.length });
533
- // TASK LOG: Intent extraction for new_task
534
- taskLog('INTENT_EXTRACTION', {
535
- sessionId: sessionInfo.sessionId,
536
- context: 'new_task',
537
- goal: intentData.goal,
538
- scopeCount: intentData.expected_scope.length,
539
- scope: intentData.expected_scope.join(', '),
540
- constraints: intentData.constraints.join(', '),
541
- keywords: intentData.keywords.join(', '),
542
- });
543
- }
544
- catch (err) {
545
- logger.info({ msg: 'Intent extraction failed, using basic goal', error: String(err) });
546
- taskLog('INTENT_EXTRACTION_FAILED', {
547
- sessionId: sessionInfo.sessionId,
548
- context: 'new_task',
549
- error: String(err),
550
- });
551
- }
552
- }
553
- const newSessionId = randomUUID();
554
- // DEBUG: Commented out for cleaner terminal - uncomment when debugging
555
- // console.log(`[DEBUG] PATH C (new_task): Creating session, latestUserMessage="${latestUserMessage.substring(0, 50)}", intentData.goal="${intentData.goal?.substring(0, 50)}"`);
556
- activeSession = createSessionState({
557
- session_id: newSessionId,
558
- project_path: sessionInfo.projectPath,
559
- original_goal: intentData.goal,
560
- raw_user_prompt: latestUserMessage.substring(0, 500),
561
- expected_scope: intentData.expected_scope,
562
- constraints: intentData.constraints,
563
- keywords: intentData.keywords,
564
- task_type: 'main',
565
- });
566
- // console.log(`[DEBUG] PATH C: Session created, raw_user_prompt="${activeSession.raw_user_prompt?.substring(0, 50)}"`);
567
- activeSessionId = newSessionId;
568
- activeSessions.set(newSessionId, {
569
- sessionId: newSessionId,
570
- promptCount: 1,
571
- projectPath: sessionInfo.projectPath,
572
- });
573
- logger.info({ msg: 'Created new task session', sessionId: newSessionId.substring(0, 8) });
574
- // TASK LOG: New task created
575
- taskLog('ORCHESTRATION_NEW_TASK', {
576
- sessionId: newSessionId,
577
- goal: intentData.goal,
578
- scopeCount: intentData.expected_scope.length,
579
- keywordsCount: intentData.keywords.length,
580
- });
581
- // Q&A AUTO-SAVE: If this is an information request with a substantive answer
582
- // AND no tool calls, save immediately since pure Q&A completes in a single turn.
583
- // If there ARE tool calls (e.g., Read for "Analyze X"), wait for them to complete
584
- // so steps get captured properly before saving.
585
- if (taskAnalysis.task_type === 'information' && textContent.length > 100 && actions.length === 0) {
586
- logger.info({ msg: 'Q&A detected (pure text) - saving immediately', sessionId: newSessionId.substring(0, 8) });
587
- taskLog('QA_AUTO_SAVE', {
588
- sessionId: newSessionId,
589
- goal: intentData.goal,
590
- responseLength: textContent.length,
591
- toolCalls: 0,
592
- });
593
- // Store the response for reasoning extraction
594
- updateSessionState(newSessionId, {
595
- final_response: textContent.substring(0, 10000),
596
- });
597
- // Save to team memory and mark complete
598
- await saveToTeamMemory(newSessionId, 'complete', taskAnalysis.task_type);
599
- markSessionCompleted(newSessionId);
600
- }
601
- else if (taskAnalysis.task_type === 'information' && actions.length > 0) {
602
- // Q&A with tool calls - don't auto-save, let it continue until task_complete
603
- logger.info({ msg: 'Q&A with tool calls - waiting for completion', sessionId: newSessionId.substring(0, 8), toolCalls: actions.length });
604
- taskLog('QA_DEFERRED', {
605
- sessionId: newSessionId,
606
- goal: intentData.goal,
607
- toolCalls: actions.length,
608
- });
609
- }
610
- break;
611
- }
612
- case 'subtask': {
613
- // Extract intent for subtask
614
- let intentData = {
615
- goal: taskAnalysis.current_goal,
616
- expected_scope: [],
617
- constraints: [],
618
- keywords: [],
619
- };
620
- if (isIntentExtractionAvailable() && latestUserMessage.length > 10) {
621
- try {
622
- intentData = await extractIntent(latestUserMessage);
623
- taskLog('INTENT_EXTRACTION', {
624
- sessionId: sessionInfo.sessionId,
625
- context: 'subtask',
626
- goal: intentData.goal,
627
- scope: intentData.expected_scope.join(', '),
628
- keywords: intentData.keywords.join(', '),
629
- });
630
- }
631
- catch (err) {
632
- taskLog('INTENT_EXTRACTION_FAILED', { sessionId: sessionInfo.sessionId, context: 'subtask', error: String(err) });
633
- }
634
- }
635
- const parentId = sessionInfo.currentSession?.session_id || taskAnalysis.parent_task_id;
636
- const subtaskId = randomUUID();
637
- activeSession = createSessionState({
638
- session_id: subtaskId,
639
- project_path: sessionInfo.projectPath,
640
- original_goal: intentData.goal,
641
- raw_user_prompt: latestUserMessage.substring(0, 500),
642
- expected_scope: intentData.expected_scope,
643
- constraints: intentData.constraints,
644
- keywords: intentData.keywords,
645
- task_type: 'subtask',
646
- parent_session_id: parentId,
647
- });
648
- activeSessionId = subtaskId;
649
- activeSessions.set(subtaskId, {
650
- sessionId: subtaskId,
651
- promptCount: 1,
652
- projectPath: sessionInfo.projectPath,
653
- });
654
- logger.info({ msg: 'Created subtask session', sessionId: subtaskId.substring(0, 8), parent: parentId?.substring(0, 8) });
655
- // TASK LOG: Subtask created
656
- taskLog('ORCHESTRATION_SUBTASK', {
657
- sessionId: subtaskId,
658
- parentId: parentId || 'none',
659
- goal: intentData.goal,
660
- });
661
- break;
662
- }
663
- case 'parallel_task': {
664
- // Extract intent for parallel task
665
- let intentData = {
666
- goal: taskAnalysis.current_goal,
667
- expected_scope: [],
668
- constraints: [],
669
- keywords: [],
670
- };
671
- if (isIntentExtractionAvailable() && latestUserMessage.length > 10) {
672
- try {
673
- intentData = await extractIntent(latestUserMessage);
674
- taskLog('INTENT_EXTRACTION', {
675
- sessionId: sessionInfo.sessionId,
676
- context: 'parallel_task',
677
- goal: intentData.goal,
678
- scope: intentData.expected_scope.join(', '),
679
- keywords: intentData.keywords.join(', '),
680
- });
681
- }
682
- catch (err) {
683
- taskLog('INTENT_EXTRACTION_FAILED', { sessionId: sessionInfo.sessionId, context: 'parallel_task', error: String(err) });
684
- }
685
- }
686
- const parentId = sessionInfo.currentSession?.session_id || taskAnalysis.parent_task_id;
687
- const parallelId = randomUUID();
688
- activeSession = createSessionState({
689
- session_id: parallelId,
690
- project_path: sessionInfo.projectPath,
691
- original_goal: intentData.goal,
692
- raw_user_prompt: latestUserMessage.substring(0, 500),
693
- expected_scope: intentData.expected_scope,
694
- constraints: intentData.constraints,
695
- keywords: intentData.keywords,
696
- task_type: 'parallel',
697
- parent_session_id: parentId,
698
- });
699
- activeSessionId = parallelId;
700
- activeSessions.set(parallelId, {
701
- sessionId: parallelId,
702
- promptCount: 1,
703
- projectPath: sessionInfo.projectPath,
704
- });
705
- logger.info({ msg: 'Created parallel task session', sessionId: parallelId.substring(0, 8), parent: parentId?.substring(0, 8) });
706
- // TASK LOG: Parallel task created
707
- taskLog('ORCHESTRATION_PARALLEL', {
708
- sessionId: parallelId,
709
- parentId: parentId || 'none',
710
- goal: intentData.goal,
711
- });
712
- break;
713
- }
714
- case 'task_complete': {
715
- // Save to team memory and mark as completed (don't delete yet - keep for new_task detection)
716
- if (sessionInfo.currentSession) {
717
- try {
718
- // Update goal if Haiku synthesized one and current is empty
719
- if (taskAnalysis.current_goal && !sessionInfo.currentSession.original_goal) {
720
- updateSessionState(sessionInfo.currentSession.session_id, {
721
- original_goal: taskAnalysis.current_goal,
722
- });
723
- sessionInfo.currentSession.original_goal = taskAnalysis.current_goal;
724
- }
725
- // Set final_response BEFORE saving so reasoning extraction has the data
726
- updateSessionState(sessionInfo.currentSession.session_id, {
727
- final_response: textContent.substring(0, 10000),
728
- });
729
- await saveToTeamMemory(sessionInfo.currentSession.session_id, 'complete', taskAnalysis.task_type);
730
- markSessionCompleted(sessionInfo.currentSession.session_id);
731
- activeSessions.delete(sessionInfo.currentSession.session_id);
732
- lastDriftResults.delete(sessionInfo.currentSession.session_id);
733
- // TASK LOG: Task completed
734
- taskLog('ORCHESTRATION_TASK_COMPLETE', {
735
- sessionId: sessionInfo.currentSession.session_id,
736
- goal: sessionInfo.currentSession.original_goal,
737
- });
738
- // PLANNING COMPLETE: Trigger CLEAR-like reset for implementation phase
739
- // This ensures next request starts fresh with planning context from team memory
740
- if (taskAnalysis.task_type === 'planning' && isSummaryAvailable()) {
741
- try {
742
- const allSteps = getValidatedSteps(sessionInfo.currentSession.session_id);
743
- const planSummary = await generateSessionSummary(sessionInfo.currentSession, allSteps, 2000);
744
- // Store for next request to trigger CLEAR
745
- setPendingPlanClear({
746
- projectPath: sessionInfo.projectPath,
747
- summary: planSummary,
748
- });
749
- // Cache invalidation happens in response-processor.ts after syncTask completes
750
- logger.info({
751
- msg: 'PLANNING_CLEAR triggered',
752
- sessionId: sessionInfo.currentSession.session_id.substring(0, 8),
753
- summaryLen: planSummary.length,
754
- });
755
- }
756
- catch {
757
- // Silent fail - planning CLEAR is optional enhancement
758
- }
759
- }
760
- logger.info({ msg: 'Task complete - saved to team memory, marked completed' });
761
- }
762
- catch (err) {
763
- logger.info({ msg: 'Failed to save completed task', error: String(err) });
764
- }
765
- }
766
- else if (textContent.length > 100) {
767
- // NEW: Handle "instant complete" - task that's new AND immediately complete
768
- // This happens for simple Q&A when Haiku says task_complete without existing session
769
- // Example: user asks clarification question, answer is provided in single turn
770
- try {
771
- const newSessionId = randomUUID();
772
- // DEBUG: Commented out for cleaner terminal - uncomment when debugging
773
- // console.log(`[DEBUG] PATH D (instant_complete): latestUserMessage="${latestUserMessage.substring(0, 50)}"`);
774
- const instantSession = createSessionState({
775
- session_id: newSessionId,
776
- project_path: sessionInfo.projectPath,
777
- original_goal: taskAnalysis.current_goal || '', // Don't fallback to user prompt
778
- raw_user_prompt: latestUserMessage.substring(0, 500),
779
- task_type: 'main',
780
- });
781
- // Set final_response for reasoning extraction
782
- updateSessionState(newSessionId, {
783
- final_response: textContent.substring(0, 10000),
784
- });
785
- await saveToTeamMemory(newSessionId, 'complete', taskAnalysis.task_type);
786
- markSessionCompleted(newSessionId);
787
- logger.info({ msg: 'Instant complete - new task saved immediately', sessionId: newSessionId.substring(0, 8) });
788
- // TASK LOG: Instant complete (new task that finished in one turn)
789
- taskLog('ORCHESTRATION_TASK_COMPLETE', {
790
- sessionId: newSessionId,
791
- goal: taskAnalysis.current_goal || '',
792
- source: 'instant_complete',
793
- });
794
- }
795
- catch (err) {
796
- logger.info({ msg: 'Failed to save instant complete task', error: String(err) });
797
- }
798
- }
799
- return; // Done, no more processing needed
800
- }
801
- case 'subtask_complete': {
802
- // Save subtask and mark completed, return to parent
803
- if (sessionInfo.currentSession) {
804
- const parentId = sessionInfo.currentSession.parent_session_id;
805
- try {
806
- await saveToTeamMemory(sessionInfo.currentSession.session_id, 'complete', taskAnalysis.task_type);
807
- markSessionCompleted(sessionInfo.currentSession.session_id);
808
- activeSessions.delete(sessionInfo.currentSession.session_id);
809
- lastDriftResults.delete(sessionInfo.currentSession.session_id);
810
- // Switch to parent session
811
- if (parentId) {
812
- const parentSession = getSessionState(parentId);
813
- if (parentSession) {
814
- activeSessionId = parentId;
815
- activeSession = parentSession;
816
- logger.info({ msg: 'Subtask complete - returning to parent', parent: parentId.substring(0, 8) });
817
- // TASK LOG: Subtask completed
818
- taskLog('ORCHESTRATION_SUBTASK_COMPLETE', {
819
- sessionId: sessionInfo.currentSession.session_id,
820
- parentId: parentId,
821
- goal: sessionInfo.currentSession.original_goal,
822
- });
823
- }
824
- }
825
- }
826
- catch (err) {
827
- logger.info({ msg: 'Failed to save completed subtask', error: String(err) });
828
- }
829
- }
830
- break;
831
- }
832
- }
833
- }
834
- catch (error) {
835
- logger.info({ msg: 'Task analysis failed, using existing session', error: String(error) });
836
- // Fall back to existing session or create new with intent extraction
837
- if (!sessionInfo.currentSession) {
838
- let intentData = {
839
- goal: '', // Don't copy user prompt - let extractIntent synthesize or leave empty
840
- expected_scope: [],
841
- constraints: [],
842
- keywords: [],
843
- };
844
- if (isIntentExtractionAvailable() && latestUserMessage.length > 10) {
845
- try {
846
- intentData = await extractIntent(latestUserMessage);
847
- taskLog('INTENT_EXTRACTION', {
848
- sessionId: sessionInfo.sessionId,
849
- context: 'fallback_analysis_failed',
850
- goal: intentData.goal,
851
- scope: intentData.expected_scope.join(', '),
852
- });
853
- }
854
- catch (err) {
855
- taskLog('INTENT_EXTRACTION_FAILED', { sessionId: sessionInfo.sessionId, context: 'fallback_analysis_failed', error: String(err) });
856
- }
857
- }
858
- const newSessionId = randomUUID();
859
- activeSession = createSessionState({
860
- session_id: newSessionId,
861
- project_path: sessionInfo.projectPath,
862
- original_goal: intentData.goal,
863
- raw_user_prompt: latestUserMessage.substring(0, 500),
864
- expected_scope: intentData.expected_scope,
865
- constraints: intentData.constraints,
866
- keywords: intentData.keywords,
867
- task_type: 'main',
868
- });
869
- activeSessionId = newSessionId;
870
- }
871
- }
872
- }
873
- else {
874
- // No task analysis available - fallback with intent extraction
875
- taskLog('TASK_ANALYSIS_UNAVAILABLE', {
876
- sessionId: sessionInfo.sessionId,
877
- hasCurrentSession: !!sessionInfo.currentSession,
878
- userMessage: latestUserMessage.substring(0, 80),
879
- });
880
- if (!sessionInfo.currentSession) {
881
- let intentData = {
882
- goal: '', // Don't copy user prompt - let extractIntent synthesize or leave empty
883
- expected_scope: [],
884
- constraints: [],
885
- keywords: [],
886
- };
887
- if (isIntentExtractionAvailable() && latestUserMessage.length > 10) {
888
- try {
889
- intentData = await extractIntent(latestUserMessage);
890
- logger.info({ msg: 'Intent extracted (fallback)', scopeCount: intentData.expected_scope.length });
891
- taskLog('INTENT_EXTRACTION', {
892
- sessionId: sessionInfo.sessionId,
893
- context: 'no_analysis_available',
894
- goal: intentData.goal,
895
- scope: intentData.expected_scope.join(', '),
896
- });
897
- }
898
- catch (err) {
899
- taskLog('INTENT_EXTRACTION_FAILED', { sessionId: sessionInfo.sessionId, context: 'no_analysis_available', error: String(err) });
900
- }
901
- }
902
- const newSessionId = randomUUID();
903
- activeSession = createSessionState({
904
- session_id: newSessionId,
905
- project_path: sessionInfo.projectPath,
906
- original_goal: intentData.goal,
907
- raw_user_prompt: latestUserMessage.substring(0, 500),
908
- expected_scope: intentData.expected_scope,
909
- constraints: intentData.constraints,
910
- keywords: intentData.keywords,
911
- task_type: 'main',
912
- });
913
- activeSessionId = newSessionId;
914
- }
915
- else {
916
- activeSession = sessionInfo.currentSession;
917
- activeSessionId = sessionInfo.currentSession.session_id;
918
- }
919
- }
920
- // NOTE: Auto-save on every end_turn was REMOVED
921
- // Task saving is now controlled by Haiku's task analysis:
922
- // - task_complete: Haiku detected task is done (Q&A answered, implementation verified, planning confirmed)
923
- // - subtask_complete: Haiku detected subtask is done
924
- // This ensures we only save when work is actually complete, not on every Claude response.
925
- // See analyzeTaskContext() in llm-extractor.ts for the decision logic.
926
- // Extract token usage
927
- const usage = extractTokenUsage(response);
928
- // Use cache metrics as actual context size (cacheCreation + cacheRead)
929
- // This is what Anthropic bills for and what determines CLEAR threshold
930
- const actualContextSize = usage.cacheCreation + usage.cacheRead;
931
- if (activeSession) {
932
- // Set to actual context size (not cumulative - context size IS the total)
933
- updateTokenCount(activeSessionId, actualContextSize);
934
- }
935
- logger.info({
936
- msg: 'Token usage',
937
- input: usage.inputTokens,
938
- output: usage.outputTokens,
939
- total: usage.totalTokens,
940
- cacheCreation: usage.cacheCreation,
941
- cacheRead: usage.cacheRead,
942
- actualContextSize,
943
- activeSession: activeSessionId.substring(0, 8),
944
- });
945
- // === CLEAR MODE PRE-COMPUTE (85% threshold) ===
946
- // Pre-compute summary before hitting 100% threshold to avoid blocking Haiku call
947
- const preComputeThreshold = Math.floor(config.TOKEN_CLEAR_THRESHOLD * 0.85);
948
- // DEBUG: Commented out for cleaner terminal - uncomment when debugging
949
- // console.log('[CLEAR-PRECOMPUTE] ═══════════════════════════════════════');
950
- // console.log('[CLEAR-PRECOMPUTE] actualContextSize:', actualContextSize);
951
- // console.log('[CLEAR-PRECOMPUTE] preComputeThreshold:', preComputeThreshold);
952
- // console.log('[CLEAR-PRECOMPUTE] hasActiveSession:', !!activeSession);
953
- // console.log('[CLEAR-PRECOMPUTE] hasPendingSummary:', !!activeSession?.pending_clear_summary);
954
- // console.log('[CLEAR-PRECOMPUTE] isSummaryAvailable:', isSummaryAvailable());
955
- // console.log('[CLEAR-PRECOMPUTE] shouldPrecompute:',
956
- // !!activeSession &&
957
- // actualContextSize > preComputeThreshold &&
958
- // !activeSession?.pending_clear_summary &&
959
- // isSummaryAvailable());
960
- // console.log('[CLEAR-PRECOMPUTE] ═══════════════════════════════════════');
961
- // Use actualContextSize (cacheCreation + cacheRead) as the real context size
962
- if (activeSession &&
963
- actualContextSize > preComputeThreshold &&
964
- !activeSession.pending_clear_summary &&
965
- isSummaryAvailable()) {
966
- // console.log('[CLEAR-PRECOMPUTE] >>> STARTING SUMMARY GENERATION <<<');
967
- // Get all validated steps for comprehensive summary
968
- const allSteps = getValidatedSteps(activeSessionId);
969
- // console.log('[CLEAR-PRECOMPUTE] validatedSteps:', allSteps.length);
970
- // Generate summary asynchronously (fire-and-forget)
971
- generateSessionSummary(activeSession, allSteps, 15000).then(summary => {
972
- updateSessionState(activeSessionId, { pending_clear_summary: summary });
973
- // console.log('[CLEAR-PRECOMPUTE] >>> SUMMARY SAVED <<<', summary.length, 'chars');
974
- logger.info({
975
- msg: 'CLEAR summary pre-computed',
976
- actualContextSize,
977
- threshold: preComputeThreshold,
978
- summaryLength: summary.length,
979
- });
980
- }).catch(err => {
981
- // console.log('[CLEAR-PRECOMPUTE] >>> SUMMARY FAILED <<<', String(err));
982
- logger.info({ msg: 'CLEAR summary generation failed', error: String(err) });
983
- });
984
- }
985
- // Capture final_response for ALL end_turn responses (not just Q&A)
986
- // This preserves Claude's analysis even when tools were used
987
- if (isEndTurn && textContent.length > 100 && activeSessionId) {
988
- updateSessionState(activeSessionId, {
989
- final_response: textContent.substring(0, 10000),
990
- });
991
- }
992
- if (actions.length === 0) {
993
- // Final response (no tool calls)
994
- // NOTE: Task saving is controlled by Haiku's task analysis (see switch case 'task_complete' above)
995
- return;
996
- }
997
- logger.info({
998
- msg: 'Actions parsed',
999
- count: actions.length,
1000
- tools: actions.map(a => a.toolName),
1001
- });
1002
- // Recovery alignment check (Section 4.4)
1003
- if (activeSession && activeSession.waiting_for_recovery) {
1004
- const lastDrift = lastDriftResults.get(activeSessionId);
1005
- const recoveryPlan = lastDrift?.recoverySteps ? { steps: lastDrift.recoverySteps } : undefined;
1006
- for (const action of actions) {
1007
- const alignment = checkRecoveryAlignment({ actionType: action.actionType, files: action.files, command: action.command }, recoveryPlan, activeSession);
1008
- if (alignment.aligned) {
1009
- // Recovered! Reset to normal
1010
- updateSessionMode(activeSessionId, 'normal');
1011
- markWaitingForRecovery(activeSessionId, false);
1012
- updateSessionState(activeSessionId, { escalation_count: 0 });
1013
- lastDriftResults.delete(activeSessionId);
1014
- logger.info({
1015
- msg: 'Recovery alignment SUCCESS - resuming normal mode',
1016
- reason: alignment.reason,
1017
- });
1018
- }
1019
- else {
1020
- incrementEscalation(activeSessionId);
1021
- logger.info({
1022
- msg: 'Recovery alignment FAILED - escalating',
1023
- reason: alignment.reason,
1024
- escalation: activeSession.escalation_count + 1,
1025
- });
1026
- }
1027
- }
1028
- }
1029
- // Run drift check every N prompts
1030
- let driftScore;
1031
- let skipSteps = false;
1032
- const memSessionInfo = activeSessions.get(activeSessionId);
1033
- const promptCount = memSessionInfo?.promptCount || sessionInfo.promptCount;
1034
- if (promptCount % config.DRIFT_CHECK_INTERVAL === 0 && isDriftCheckAvailable()) {
1035
- if (activeSession) {
1036
- const stepsForDrift = getRecentSteps(activeSessionId, 10);
1037
- const driftResult = await checkDrift({ sessionState: activeSession, recentSteps: stepsForDrift, latestUserMessage });
1038
- lastDriftResults.set(activeSessionId, driftResult);
1039
- driftScore = driftResult.score;
1040
- skipSteps = shouldSkipSteps(driftScore);
1041
- logger.info({
1042
- msg: 'Drift check',
1043
- score: driftResult.score,
1044
- type: driftResult.driftType,
1045
- diagnostic: driftResult.diagnostic,
1046
- });
1047
- const correctionLevel = scoreToCorrectionLevel(driftScore);
1048
- if (correctionLevel === 'intervene' || correctionLevel === 'halt') {
1049
- updateSessionMode(activeSessionId, 'drifted');
1050
- markWaitingForRecovery(activeSessionId, true);
1051
- incrementEscalation(activeSessionId);
1052
- // Pre-compute correction for next request (fire-and-forget pattern)
1053
- // This avoids blocking Haiku calls in preProcessRequest
1054
- const correction = buildCorrection(driftResult, activeSession, correctionLevel);
1055
- const correctionText = formatCorrectionForInjection(correction);
1056
- updateSessionState(activeSessionId, { pending_correction: correctionText });
1057
- logger.info({
1058
- msg: 'Pre-computed correction saved',
1059
- level: correctionLevel,
1060
- correctionLength: correctionText.length,
1061
- });
1062
- }
1063
- else if (correctionLevel) {
1064
- // Nudge or correct level - still save correction but don't change mode
1065
- const correction = buildCorrection(driftResult, activeSession, correctionLevel);
1066
- const correctionText = formatCorrectionForInjection(correction);
1067
- updateSessionState(activeSessionId, { pending_correction: correctionText });
1068
- logger.info({
1069
- msg: 'Pre-computed mild correction saved',
1070
- level: correctionLevel,
1071
- });
1072
- }
1073
- else if (driftScore >= 8) {
1074
- updateSessionMode(activeSessionId, 'normal');
1075
- markWaitingForRecovery(activeSessionId, false);
1076
- lastDriftResults.delete(activeSessionId);
1077
- // Clear any pending correction since drift is resolved
1078
- updateSessionState(activeSessionId, { pending_correction: undefined });
1079
- }
1080
- // FORCED MODE: escalation >= 3 triggers Haiku-generated recovery
1081
- const currentEscalation = activeSession.escalation_count || 0;
1082
- if (currentEscalation >= 3 && driftScore < 8) {
1083
- updateSessionMode(activeSessionId, 'forced');
1084
- // Generate forced recovery asynchronously (fire-and-forget within fire-and-forget)
1085
- generateForcedRecovery(activeSession, recentSteps.map(s => ({ actionType: s.action_type, files: s.files })), driftResult).then(forcedRecovery => {
1086
- updateSessionState(activeSessionId, {
1087
- pending_forced_recovery: forcedRecovery.injectionText,
1088
- });
1089
- logger.info({
1090
- msg: 'Pre-computed forced recovery saved',
1091
- escalation: currentEscalation,
1092
- mandatoryAction: forcedRecovery.mandatoryAction?.substring(0, 50),
1093
- });
1094
- }).catch(err => {
1095
- logger.info({ msg: 'Forced recovery generation failed', error: String(err) });
1096
- });
1097
- }
1098
- updateLastChecked(activeSessionId, Date.now());
1099
- if (skipSteps) {
1100
- for (const action of actions) {
1101
- logDriftEvent({
1102
- session_id: activeSessionId,
1103
- action_type: action.actionType,
1104
- files: action.files,
1105
- drift_score: driftScore,
1106
- drift_reason: driftResult.diagnostic,
1107
- recovery_plan: driftResult.recoverySteps ? { steps: driftResult.recoverySteps } : undefined,
1108
- });
1109
- }
1110
- logger.info({
1111
- msg: 'Actions logged to drift_log (skipped steps)',
1112
- reason: 'score < 5',
1113
- });
1114
- return;
1115
- }
1116
- }
1117
- }
1118
- // Save each action as a step (with reasoning from Claude's text)
1119
- // When multiple actions come from the same Claude response, they share identical reasoning.
1120
- // We store reasoning only on the first action and set NULL for subsequent ones to avoid duplication.
1121
- // At query time, we group steps by reasoning (non-NULL starts a group, NULLs continue it)
1122
- // and reconstruct the full context: reasoning + all associated files/actions.
1123
- let previousReasoning = null;
1124
- logger.info({ msg: 'DEDUP_DEBUG', actionsCount: actions.length, textContentLen: textContent.length });
1125
- for (const action of actions) {
1126
- const currentReasoning = textContent.substring(0, 1000);
1127
- const isDuplicate = currentReasoning === previousReasoning;
1128
- logger.info({
1129
- msg: 'DEDUP_STEP',
1130
- actionType: action.actionType,
1131
- isDuplicate,
1132
- prevLen: previousReasoning?.length || 0,
1133
- currLen: currentReasoning.length
1134
- });
1135
- // Detect key decisions based on action type and reasoning content
1136
- const isKeyDecision = !isDuplicate && detectKeyDecision(action, textContent);
1137
- createStep({
1138
- session_id: activeSessionId,
1139
- action_type: action.actionType,
1140
- files: action.files,
1141
- folders: action.folders,
1142
- command: action.command,
1143
- reasoning: isDuplicate ? undefined : currentReasoning,
1144
- drift_score: driftScore,
1145
- is_validated: !skipSteps,
1146
- is_key_decision: isKeyDecision,
1147
- });
1148
- previousReasoning = currentReasoning;
1149
- if (isKeyDecision) {
1150
- logger.info({
1151
- msg: 'Key decision detected',
1152
- actionType: action.actionType,
1153
- files: action.files.slice(0, 3),
1154
- });
1155
- }
1156
- }
1157
- }
1158
- /**
1159
- * Filter response headers for forwarding to client
1160
- */
1161
- function filterResponseHeaders(headers) {
1162
- const filtered = {};
1163
- const allowedHeaders = [
1164
- 'content-type',
1165
- 'x-request-id',
1166
- 'request-id',
1167
- 'x-should-retry',
1168
- 'retry-after',
1169
- 'retry-after-ms',
1170
- 'anthropic-ratelimit-requests-limit',
1171
- 'anthropic-ratelimit-requests-remaining',
1172
- 'anthropic-ratelimit-requests-reset',
1173
- 'anthropic-ratelimit-tokens-limit',
1174
- 'anthropic-ratelimit-tokens-remaining',
1175
- 'anthropic-ratelimit-tokens-reset',
1176
- ];
1177
- for (const header of allowedHeaders) {
1178
- const value = headers[header];
1179
- if (value) {
1180
- filtered[header] = Array.isArray(value) ? value[0] : value;
1181
- }
1182
- }
1183
- return filtered;
1184
- }
1185
- /**
1186
- * Type guard for AnthropicResponse
1187
- */
1188
- function isAnthropicResponse(body) {
1189
- return (typeof body === 'object' &&
1190
- body !== null &&
1191
- 'type' in body &&
1192
- body.type === 'message' &&
1193
- 'content' in body &&
1194
- 'usage' in body);
1195
- }
1196
- /**
1197
- * Start the proxy server
1198
- * @param options.debug - Enable debug logging to grov-proxy.log
1199
- */
1200
- export async function startServer(options = {}) {
1201
- // Set debug mode based on flag
1202
- if (options.debug) {
1203
- setDebugMode(true);
1204
- // DEBUG: Commented out for cleaner terminal - uncomment when debugging
1205
- // console.log('[DEBUG] Logging to grov-proxy.log');
1206
- }
1207
- const server = createServer();
1208
- // Set server logger for background tasks
1209
- serverLog = server.log;
1210
- // Startup cleanup
1211
- cleanupOldCompletedSessions();
1212
- cleanupFailedSyncTasks();
1213
- // Cleanup stale active sessions (no activity for 1 hour)
1214
- // Prevents old sessions from being reused in fresh Claude sessions
1215
- const staleCount = cleanupStaleActiveSessions();
1216
- if (staleCount > 0) {
1217
- log(`Cleaned up ${staleCount} stale active session(s)`);
1218
- }
1219
- // Start extended cache timer if enabled
1220
- let extendedCacheTimer = null;
1221
- // Track active connections for graceful shutdown
1222
- const activeConnections = new Set();
1223
- let isShuttingDown = false;
1224
- // Graceful shutdown handler (works with or without extended cache)
1225
- const gracefulShutdown = () => {
1226
- if (isShuttingDown)
1227
- return;
1228
- isShuttingDown = true;
1229
- log('Shutdown initiated...');
1230
- // 1. Stop extended cache timer if running
1231
- if (extendedCacheTimer) {
1232
- clearInterval(extendedCacheTimer);
1233
- extendedCacheTimer = null;
1234
- }
1235
- // 2. Clear sensitive cache data
1236
- if (extendedCache.size > 0) {
1237
- for (const entry of extendedCache.values()) {
1238
- for (const key of Object.keys(entry.headers)) {
1239
- entry.headers[key] = '';
1240
- }
1241
- entry.rawBody = Buffer.alloc(0);
1242
- }
1243
- extendedCache.clear();
1244
- }
1245
- // 3. Stop accepting new connections
1246
- server.close();
1247
- // 4. Grace period (500ms) then force close remaining connections
1248
- setTimeout(() => {
1249
- if (activeConnections.size > 0) {
1250
- log(`Force closing ${activeConnections.size} connection(s)`);
1251
- for (const socket of activeConnections) {
1252
- socket.destroy();
1253
- }
1254
- }
1255
- log('Goodbye!');
1256
- process.exit(0);
1257
- }, 500);
1258
- };
1259
- process.on('SIGTERM', gracefulShutdown);
1260
- process.on('SIGINT', gracefulShutdown);
1261
- if (config.EXTENDED_CACHE_ENABLED) {
1262
- extendedCacheTimer = setInterval(checkExtendedCache, 60_000);
1263
- log('Extended cache: enabled (keep-alive timer started)');
1264
- }
1265
- // Clear stale pending corrections from previous sessions
1266
- // Prevents stuck HALT states from blocking new sessions
1267
- clearStalePendingCorrections();
1268
- try {
1269
- await server.listen({
1270
- host: config.HOST,
1271
- port: config.PORT,
1272
- });
1273
- // Track connections for graceful shutdown
1274
- server.server.on('connection', (socket) => {
1275
- activeConnections.add(socket);
1276
- socket.on('close', () => activeConnections.delete(socket));
1277
- });
1278
- console.log(`Grov Proxy: http://${config.HOST}:${config.PORT} -> ${config.ANTHROPIC_BASE_URL}`);
1279
- return server;
1280
- }
1281
- catch (err) {
1282
- server.log.error(err);
1283
- process.exit(1);
1284
- }
1285
- }
1286
- // CLI entry point
1287
- if (import.meta.url === `file://${process.argv[1]}`) {
1288
- startServer();
1289
- }