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
@@ -0,0 +1,954 @@
1
+ // Grov Proxy Orchestrator - Shared business logic for all agents
2
+ // Handles session management, task orchestration, drift detection, and memory injection
3
+ import { randomUUID } from 'crypto';
4
+ import { config, buildSafeHeaders } from './config.js';
5
+ import { extendedCache, evictOldestCacheEntry } from './cache/extended-cache.js';
6
+ import { getNextRequestId, taskLog, proxyLog, logTokenUsage } from './utils/logging.js';
7
+ import { preProcessRequest, setPendingPlanClear } from './handlers/preprocess.js';
8
+ import { createSessionState, getSessionState, updateSessionState, createStep, updateTokenCount, logDriftEvent, getRecentSteps, getValidatedSteps, updateSessionMode, markWaitingForRecovery, incrementEscalation, updateLastChecked, getActiveSessionForUser, deleteSessionState, deleteStepsForSession, updateRecentStepsReasoning, markSessionCompleted, getCompletedSessionForProject, } from '../../core/store/store.js';
9
+ import { checkDrift, scoreToCorrectionLevel, shouldSkipSteps, checkRecoveryAlignment, } from '../../core/extraction/drift-checker-proxy.js';
10
+ import { buildCorrection, formatCorrectionForInjection } from '../../core/extraction/correction-builder-proxy.js';
11
+ import { generateSessionSummary, analyzeTaskContext, } from '../../core/extraction/llm-extractor.js';
12
+ import { saveToTeamMemory } from './response-processor.js';
13
+ import { getCachedMemoryById, buildExpandedMemory, addInjectionRecord, hasToolCycleAtPosition, } from './injection/memory-injection.js';
14
+ // In-memory state
15
+ const lastDriftResults = new Map();
16
+ const lastMessageCount = new Map();
17
+ const activeSessions = new Map();
18
+ /**
19
+ * Main entry point for handling agent requests
20
+ */
21
+ export async function handleAgentRequest(context) {
22
+ const { adapter, body, headers, rawBody, logger } = context;
23
+ const startTime = Date.now();
24
+ // Extract model from body
25
+ const requestBody = body;
26
+ const model = requestBody.model || '';
27
+ // Skip subagent models (Haiku for Claude, mini for Codex)
28
+ if (adapter.isSubagentModel(model)) {
29
+ logger.info({ msg: 'Skipping subagent', model });
30
+ try {
31
+ const result = await adapter.forward({ ...requestBody, stream: false }, headers, rawBody);
32
+ return {
33
+ statusCode: result.statusCode,
34
+ contentType: 'application/json',
35
+ headers: adapter.filterResponseHeaders(result.headers),
36
+ body: JSON.stringify(result.body),
37
+ };
38
+ }
39
+ catch (error) {
40
+ logger.error?.({ msg: 'Subagent forward error', error: String(error) });
41
+ return {
42
+ statusCode: 502,
43
+ contentType: 'application/json',
44
+ headers: {},
45
+ body: JSON.stringify({ error: { type: 'proxy_error', message: 'Bad gateway' } }),
46
+ };
47
+ }
48
+ }
49
+ // Get or create session
50
+ const sessionInfo = await getOrCreateSession(adapter, body, logger);
51
+ sessionInfo.promptCount++;
52
+ activeSessions.set(sessionInfo.sessionId, {
53
+ sessionId: sessionInfo.sessionId,
54
+ promptCount: sessionInfo.promptCount,
55
+ projectPath: sessionInfo.projectPath,
56
+ });
57
+ const currentRequestId = getNextRequestId();
58
+ logger.info({
59
+ msg: 'Incoming request',
60
+ agent: adapter.name,
61
+ sessionId: sessionInfo.sessionId.substring(0, 8),
62
+ promptCount: sessionInfo.promptCount,
63
+ model,
64
+ messageCount: getMessageCount(body),
65
+ });
66
+ proxyLog({
67
+ requestId: currentRequestId,
68
+ type: 'REQUEST',
69
+ sessionId: sessionInfo.sessionId.substring(0, 8),
70
+ data: {
71
+ agent: adapter.name,
72
+ model,
73
+ messageCount: getMessageCount(body),
74
+ promptCount: sessionInfo.promptCount,
75
+ rawBodySize: rawBody?.length || 0,
76
+ },
77
+ });
78
+ // Pre-process request for memory injection (agent-agnostic)
79
+ const processedBody = await preProcessRequest(adapter, requestBody, sessionInfo, logger, detectRequestType);
80
+ const systemInjection = processedBody.__grovInjection;
81
+ const userMsgInjection = processedBody.__grovUserMsgInjection;
82
+ const rawUserPrompt = processedBody.__grovRawUserPrompt;
83
+ // Build final body with injections using adapter methods
84
+ let rawBodyStr = rawBody?.toString('utf-8') || '';
85
+ let systemInjectionSize = 0;
86
+ let userMsgInjectionSize = 0;
87
+ let systemSuccess = false;
88
+ let userMsgSuccess = false;
89
+ if (systemInjection && rawBodyStr) {
90
+ const result = adapter.injectIntoRawSystemPrompt(rawBodyStr, '\n\n' + systemInjection);
91
+ rawBodyStr = result.modified;
92
+ systemInjectionSize = systemInjection.length;
93
+ systemSuccess = result.success;
94
+ }
95
+ if (userMsgInjection && rawBodyStr) {
96
+ const beforeLen = rawBodyStr.length;
97
+ rawBodyStr = adapter.injectIntoRawUserMessage(rawBodyStr, userMsgInjection);
98
+ const afterLen = rawBodyStr.length;
99
+ userMsgInjectionSize = userMsgInjection.length;
100
+ userMsgSuccess = afterLen > beforeLen;
101
+ }
102
+ // Inject grov_expand tool if needed
103
+ const hasGrovExpandInProcessed = processedBody.tools &&
104
+ Array.isArray(processedBody.tools) &&
105
+ processedBody.tools.some(t => t.name === 'grov_expand');
106
+ if (hasGrovExpandInProcessed && rawBodyStr) {
107
+ const toolDef = adapter.buildGrovExpandTool();
108
+ const result = adapter.injectToolIntoRawBody(rawBodyStr, toolDef);
109
+ if (result.success) {
110
+ rawBodyStr = result.modified;
111
+ }
112
+ }
113
+ // Determine final body
114
+ let finalBodyToSend;
115
+ const reconstructedCount = processedBody.__grovReconstructedCount || 0;
116
+ if (systemInjection || userMsgInjection) {
117
+ finalBodyToSend = Buffer.from(rawBodyStr, 'utf-8');
118
+ const wasCached = processedBody.__grovInjectionCached;
119
+ proxyLog({
120
+ requestId: currentRequestId,
121
+ type: 'INJECTION',
122
+ sessionId: sessionInfo.sessionId.substring(0, 8),
123
+ data: {
124
+ systemInjectionSize,
125
+ userMsgInjectionSize,
126
+ totalInjectionSize: systemInjectionSize + userMsgInjectionSize,
127
+ originalSize: rawBody?.length || 0,
128
+ finalSize: rawBodyStr.length,
129
+ systemSuccess,
130
+ userMsgSuccess,
131
+ teamMemoryCached: wasCached,
132
+ systemInjectionPreview: systemInjection ? systemInjection.substring(0, 200) + (systemInjection.length > 200 ? '...' : '') : null,
133
+ userMsgInjectionContent: userMsgInjection || null,
134
+ },
135
+ });
136
+ }
137
+ else if (reconstructedCount > 0) {
138
+ const { __grovInjection, __grovUserMsgInjection, __grovInjectionCached, __grovReconstructedCount, __grovOriginalLastUserPos, __grovRawUserPrompt: _rawPrompt1, ...cleanBody } = processedBody;
139
+ finalBodyToSend = Buffer.from(JSON.stringify(cleanBody), 'utf-8');
140
+ }
141
+ else if (rawBody) {
142
+ finalBodyToSend = rawBody;
143
+ }
144
+ else {
145
+ finalBodyToSend = Buffer.from(JSON.stringify(processedBody), 'utf-8');
146
+ }
147
+ const forwardStart = Date.now();
148
+ try {
149
+ // Forward to upstream API
150
+ let result = await adapter.forward(processedBody, headers, finalBodyToSend);
151
+ // Handle grov_expand internal tool loop
152
+ let loopCount = 0;
153
+ const maxLoops = 5;
154
+ const { __grovInjection: _i, __grovUserMsgInjection: _u, __grovInjectionCached: _c, __grovReconstructedCount: _r, __grovOriginalLastUserPos, __grovRawUserPrompt: _p, ...cleanProcessedBody } = processedBody;
155
+ let accumulatedBody = cleanProcessedBody;
156
+ while (result.statusCode === 200 &&
157
+ adapter.isValidResponse(result.body) &&
158
+ adapter.isToolUse(result.body) &&
159
+ loopCount < maxLoops) {
160
+ const grovExpandBlock = adapter.findInternalToolUse(result.body, 'grov_expand');
161
+ if (!grovExpandBlock)
162
+ break;
163
+ loopCount++;
164
+ const allToolUseBlocks = adapter.getToolUseBlocks(result.body);
165
+ const toolResultBlocks = [];
166
+ let grovExpandResult = '';
167
+ for (const block of allToolUseBlocks) {
168
+ if (block.name === 'grov_expand') {
169
+ const ids = block.input?.ids || [];
170
+ const expandedParts = [];
171
+ for (const id of ids) {
172
+ const memory = getCachedMemoryById(sessionInfo.projectPath, id);
173
+ if (memory) {
174
+ expandedParts.push(buildExpandedMemory(memory));
175
+ }
176
+ else {
177
+ expandedParts.push(`Memory #${id} not found - it may be from an older conversation. Only expand IDs from the CURRENT knowledge base.`);
178
+ }
179
+ }
180
+ if (ids.length > 0) {
181
+ const expandedCount = expandedParts.filter(p => !p.includes('not found')).length;
182
+ console.log(`[MEMORY] Expanded ${expandedCount}/${ids.length} memories`);
183
+ }
184
+ grovExpandResult = expandedParts.join('\n\n');
185
+ toolResultBlocks.push({
186
+ type: 'tool_result',
187
+ tool_use_id: block.id,
188
+ content: grovExpandResult,
189
+ });
190
+ }
191
+ else {
192
+ toolResultBlocks.push({
193
+ type: 'tool_result',
194
+ tool_use_id: block.id,
195
+ content: 'This tool call was deferred. Use the expanded project knowledge base context above, then call this tool separately if still needed.',
196
+ });
197
+ }
198
+ }
199
+ const pos = __grovOriginalLastUserPos ?? 0;
200
+ if (!hasToolCycleAtPosition(sessionInfo.projectPath, pos)) {
201
+ addInjectionRecord(sessionInfo.projectPath, {
202
+ position: pos,
203
+ type: 'tool_cycle',
204
+ toolUse: { id: grovExpandBlock.id, name: 'grov_expand', input: grovExpandBlock.input },
205
+ toolResult: grovExpandResult,
206
+ });
207
+ }
208
+ const assistantContent = getAssistantContent(result.body, adapter);
209
+ const messages = [...accumulatedBody.messages];
210
+ messages.push({ role: 'assistant', content: assistantContent });
211
+ messages.push({ role: 'user', content: toolResultBlocks });
212
+ accumulatedBody = { ...accumulatedBody, messages };
213
+ result = await adapter.forward(accumulatedBody, headers);
214
+ }
215
+ const forwardLatency = Date.now() - forwardStart;
216
+ // Fire-and-forget post-processing
217
+ if (result.statusCode === 200 && adapter.isValidResponse(result.body)) {
218
+ const extendedCacheData = config.EXTENDED_CACHE_ENABLED ? {
219
+ headers: buildSafeHeaders(headers),
220
+ rawBody: finalBodyToSend,
221
+ } : undefined;
222
+ postProcessResponse(adapter, result.body, sessionInfo, processedBody, logger, extendedCacheData, headers).catch(err => console.error('[GROV] postProcess error:', err));
223
+ }
224
+ const latency = Date.now() - startTime;
225
+ const filteredHeaders = adapter.filterResponseHeaders(result.headers);
226
+ if (adapter.isValidResponse(result.body)) {
227
+ const usage = adapter.extractUsage(result.body);
228
+ logTokenUsage(currentRequestId, usage, latency);
229
+ proxyLog({
230
+ requestId: currentRequestId,
231
+ type: 'RESPONSE',
232
+ sessionId: sessionInfo.sessionId.substring(0, 8),
233
+ data: {
234
+ statusCode: result.statusCode,
235
+ latencyMs: latency,
236
+ forwardLatencyMs: forwardLatency,
237
+ inputTokens: usage.inputTokens,
238
+ outputTokens: usage.outputTokens,
239
+ cacheCreation: usage.cacheCreation,
240
+ cacheRead: usage.cacheRead,
241
+ cacheHitRatio: usage.cacheRead > 0 ? (usage.cacheRead / (usage.cacheRead + usage.cacheCreation)).toFixed(2) : '0.00',
242
+ wasSSE: result.wasSSE,
243
+ grovExpandLoops: loopCount > 0 ? loopCount : undefined,
244
+ },
245
+ });
246
+ }
247
+ logger.info({
248
+ msg: 'Request complete',
249
+ statusCode: result.statusCode,
250
+ latencyMs: latency,
251
+ wasSSE: result.wasSSE,
252
+ grovExpandLoops: loopCount > 0 ? loopCount : undefined,
253
+ });
254
+ return {
255
+ statusCode: result.statusCode,
256
+ contentType: adapter.getResponseContentType(result.wasSSE || false),
257
+ headers: filteredHeaders,
258
+ body: result.wasSSE ? result.rawBody : JSON.stringify(result.body),
259
+ };
260
+ }
261
+ catch (error) {
262
+ const err = error;
263
+ if (err.type === 'timeout' || err.type === 'network') {
264
+ logger.error?.({
265
+ msg: 'Forward error',
266
+ type: err.type,
267
+ message: err.message,
268
+ });
269
+ return {
270
+ statusCode: err.statusCode || 502,
271
+ contentType: 'application/json',
272
+ headers: {},
273
+ body: JSON.stringify({
274
+ error: {
275
+ type: 'proxy_error',
276
+ message: err.type === 'timeout' ? 'Gateway timeout' : 'Bad gateway',
277
+ },
278
+ }),
279
+ };
280
+ }
281
+ logger.error?.({
282
+ msg: 'Unexpected error',
283
+ error: String(error),
284
+ });
285
+ return {
286
+ statusCode: 500,
287
+ contentType: 'application/json',
288
+ headers: {},
289
+ body: JSON.stringify({
290
+ error: {
291
+ type: 'internal_error',
292
+ message: 'Internal proxy error',
293
+ },
294
+ }),
295
+ };
296
+ }
297
+ }
298
+ /**
299
+ * Get or create session for this request
300
+ */
301
+ async function getOrCreateSession(adapter, body, logger) {
302
+ const projectPath = adapter.extractProjectPath(body) || process.cwd();
303
+ const existingSession = getActiveSessionForUser(projectPath);
304
+ if (existingSession) {
305
+ let sessionInfo = activeSessions.get(existingSession.session_id);
306
+ if (!sessionInfo) {
307
+ sessionInfo = {
308
+ sessionId: existingSession.session_id,
309
+ promptCount: 0,
310
+ projectPath,
311
+ };
312
+ activeSessions.set(existingSession.session_id, sessionInfo);
313
+ }
314
+ logger.info({
315
+ msg: 'Found existing session',
316
+ sessionId: existingSession.session_id.substring(0, 8),
317
+ goal: existingSession.original_goal?.substring(0, 50),
318
+ });
319
+ return { ...sessionInfo, isNew: false, currentSession: existingSession, completedSession: null };
320
+ }
321
+ const completedSession = getCompletedSessionForProject(projectPath);
322
+ if (completedSession) {
323
+ logger.info({
324
+ msg: 'Found recently completed session for comparison',
325
+ sessionId: completedSession.session_id.substring(0, 8),
326
+ goal: completedSession.original_goal?.substring(0, 50),
327
+ });
328
+ }
329
+ const tempSessionId = randomUUID();
330
+ const sessionInfo = {
331
+ sessionId: tempSessionId,
332
+ promptCount: 0,
333
+ projectPath,
334
+ };
335
+ activeSessions.set(tempSessionId, sessionInfo);
336
+ logger.info({ msg: 'No existing session, will create after task analysis' });
337
+ return { ...sessionInfo, isNew: true, currentSession: null, completedSession };
338
+ }
339
+ /**
340
+ * Detect request type: first, continuation, or retry
341
+ */
342
+ function detectRequestType(messages, projectPath) {
343
+ const currentCount = messages?.length || 0;
344
+ const lastCount = lastMessageCount.get(projectPath);
345
+ lastMessageCount.set(projectPath, currentCount);
346
+ if (lastCount !== undefined && currentCount === lastCount) {
347
+ return 'retry';
348
+ }
349
+ if (!messages || messages.length === 0)
350
+ return 'first';
351
+ const lastMessage = messages[messages.length - 1];
352
+ if (lastMessage.role === 'user') {
353
+ const content = lastMessage.content;
354
+ if (Array.isArray(content)) {
355
+ const hasToolResult = content.some((block) => typeof block === 'object' && block !== null && block.type === 'tool_result');
356
+ if (hasToolResult)
357
+ return 'continuation';
358
+ }
359
+ }
360
+ return 'first';
361
+ }
362
+ /**
363
+ * Post-process response - task orchestration, drift detection, step recording
364
+ */
365
+ async function postProcessResponse(adapter, response, sessionInfo, requestBody, logger, extendedCacheData, requestHeaders) {
366
+ const actions = adapter.parseActions(response);
367
+ const textContent = adapter.extractTextContent(response);
368
+ const messages = getMessages(requestBody);
369
+ const latestUserMessage = adapter.extractGoal(messages) || '';
370
+ const rawUserPrompt = requestBody.__grovRawUserPrompt;
371
+ const recentSteps = sessionInfo.currentSession
372
+ ? getRecentSteps(sessionInfo.currentSession.session_id, 5)
373
+ : [];
374
+ let activeSessionId = sessionInfo.sessionId;
375
+ let activeSession = sessionInfo.currentSession;
376
+ const isEndTurn = adapter.isEndTurn(response);
377
+ const isWarmup = latestUserMessage.toLowerCase().trim() === 'warmup';
378
+ if (isWarmup) {
379
+ return;
380
+ }
381
+ // Extended cache capture on end_turn
382
+ if (isEndTurn && extendedCacheData) {
383
+ const cacheKey = sessionInfo.projectPath;
384
+ if (!extendedCache.has(cacheKey)) {
385
+ evictOldestCacheEntry();
386
+ }
387
+ extendedCache.set(cacheKey, {
388
+ headers: extendedCacheData.headers,
389
+ rawBody: extendedCacheData.rawBody,
390
+ timestamp: Date.now(),
391
+ keepAliveCount: 0,
392
+ });
393
+ }
394
+ // Skip task orchestration if not end_turn
395
+ if (!isEndTurn) {
396
+ if (sessionInfo.currentSession) {
397
+ activeSessionId = sessionInfo.currentSession.session_id;
398
+ activeSession = sessionInfo.currentSession;
399
+ }
400
+ else if (!activeSession) {
401
+ const newSessionId = randomUUID();
402
+ activeSession = createSessionState({
403
+ session_id: newSessionId,
404
+ project_path: sessionInfo.projectPath,
405
+ original_goal: '',
406
+ raw_user_prompt: rawUserPrompt,
407
+ task_type: 'main',
408
+ });
409
+ activeSessionId = newSessionId;
410
+ activeSessions.set(newSessionId, {
411
+ sessionId: newSessionId,
412
+ promptCount: 1,
413
+ projectPath: sessionInfo.projectPath,
414
+ });
415
+ }
416
+ }
417
+ else {
418
+ const sessionForComparison = sessionInfo.currentSession || sessionInfo.completedSession;
419
+ const conversationHistory = adapter.extractHistory(messages);
420
+ try {
421
+ const taskAnalysis = await analyzeTaskContext(sessionForComparison, latestUserMessage, recentSteps, textContent, conversationHistory, requestHeaders);
422
+ logger.info({
423
+ msg: 'Task analysis',
424
+ action: taskAnalysis.action,
425
+ task_type: taskAnalysis.task_type,
426
+ goal: taskAnalysis.current_goal?.substring(0, 50),
427
+ reasoning: taskAnalysis.reasoning,
428
+ });
429
+ taskLog('TASK_ANALYSIS', {
430
+ sessionId: sessionInfo.sessionId,
431
+ action: taskAnalysis.action,
432
+ task_type: taskAnalysis.task_type,
433
+ goal: taskAnalysis.current_goal || '',
434
+ reasoning: taskAnalysis.reasoning || '',
435
+ userMessage: latestUserMessage.substring(0, 80),
436
+ hasCurrentSession: !!sessionInfo.currentSession,
437
+ hasCompletedSession: !!sessionInfo.completedSession,
438
+ });
439
+ if (taskAnalysis.step_reasoning && activeSessionId) {
440
+ const updatedCount = updateRecentStepsReasoning(activeSessionId, taskAnalysis.step_reasoning);
441
+ taskLog('STEP_REASONING', {
442
+ sessionId: activeSessionId,
443
+ stepsUpdated: updatedCount,
444
+ reasoningEntries: Object.keys(taskAnalysis.step_reasoning).length,
445
+ stepIds: Object.keys(taskAnalysis.step_reasoning).join(','),
446
+ });
447
+ }
448
+ // Task orchestration switch
449
+ switch (taskAnalysis.action) {
450
+ case 'continue':
451
+ if (sessionInfo.currentSession) {
452
+ activeSessionId = sessionInfo.currentSession.session_id;
453
+ activeSession = sessionInfo.currentSession;
454
+ if (taskAnalysis.current_goal &&
455
+ taskAnalysis.current_goal !== activeSession.original_goal &&
456
+ latestUserMessage.length > 30) {
457
+ updateSessionState(activeSessionId, {
458
+ original_goal: taskAnalysis.current_goal,
459
+ });
460
+ activeSession.original_goal = taskAnalysis.current_goal;
461
+ }
462
+ taskLog('ORCHESTRATION_CONTINUE', {
463
+ sessionId: activeSessionId,
464
+ source: 'current_session',
465
+ goal: activeSession.original_goal,
466
+ goalUpdated: taskAnalysis.current_goal !== activeSession.original_goal,
467
+ });
468
+ }
469
+ else if (sessionInfo.completedSession) {
470
+ activeSessionId = sessionInfo.completedSession.session_id;
471
+ activeSession = sessionInfo.completedSession;
472
+ updateSessionState(activeSessionId, {
473
+ status: 'active',
474
+ original_goal: taskAnalysis.current_goal || activeSession.original_goal,
475
+ });
476
+ activeSession.status = 'active';
477
+ activeSessions.set(activeSessionId, {
478
+ sessionId: activeSessionId,
479
+ promptCount: 1,
480
+ projectPath: sessionInfo.projectPath,
481
+ });
482
+ taskLog('ORCHESTRATION_CONTINUE', {
483
+ sessionId: activeSessionId,
484
+ source: 'reactivated_completed',
485
+ goal: activeSession.original_goal,
486
+ });
487
+ }
488
+ break;
489
+ case 'new_task': {
490
+ if (sessionInfo.completedSession) {
491
+ deleteStepsForSession(sessionInfo.completedSession.session_id);
492
+ deleteSessionState(sessionInfo.completedSession.session_id);
493
+ }
494
+ const newSessionId = randomUUID();
495
+ activeSession = createSessionState({
496
+ session_id: newSessionId,
497
+ project_path: sessionInfo.projectPath,
498
+ original_goal: taskAnalysis.current_goal,
499
+ raw_user_prompt: rawUserPrompt,
500
+ constraints: taskAnalysis.constraints || [],
501
+ task_type: 'main',
502
+ });
503
+ activeSessionId = newSessionId;
504
+ activeSessions.set(newSessionId, {
505
+ sessionId: newSessionId,
506
+ promptCount: 1,
507
+ projectPath: sessionInfo.projectPath,
508
+ });
509
+ logger.info({ msg: 'Created new task session', sessionId: newSessionId.substring(0, 8) });
510
+ taskLog('ORCHESTRATION_NEW_TASK', {
511
+ sessionId: newSessionId,
512
+ goal: taskAnalysis.current_goal,
513
+ constraintsCount: (taskAnalysis.constraints || []).length,
514
+ });
515
+ // Q&A auto-save
516
+ if (taskAnalysis.task_type === 'information' && textContent.length > 100 && actions.length === 0) {
517
+ logger.info({ msg: 'Q&A detected (pure text) - saving immediately', sessionId: newSessionId.substring(0, 8) });
518
+ taskLog('QA_AUTO_SAVE', {
519
+ sessionId: newSessionId,
520
+ goal: taskAnalysis.current_goal,
521
+ responseLength: textContent.length,
522
+ toolCalls: 0,
523
+ });
524
+ updateSessionState(newSessionId, {
525
+ final_response: textContent.substring(0, 10000),
526
+ });
527
+ await saveToTeamMemory(newSessionId, 'complete', taskAnalysis.task_type, requestHeaders);
528
+ markSessionCompleted(newSessionId);
529
+ }
530
+ else if (taskAnalysis.task_type === 'information' && actions.length > 0) {
531
+ logger.info({ msg: 'Q&A with tool calls - waiting for completion', sessionId: newSessionId.substring(0, 8), toolCalls: actions.length });
532
+ taskLog('QA_DEFERRED', {
533
+ sessionId: newSessionId,
534
+ goal: taskAnalysis.current_goal,
535
+ toolCalls: actions.length,
536
+ });
537
+ }
538
+ break;
539
+ }
540
+ case 'subtask': {
541
+ const parentId = sessionInfo.currentSession?.session_id || taskAnalysis.parent_task_id;
542
+ const subtaskId = randomUUID();
543
+ activeSession = createSessionState({
544
+ session_id: subtaskId,
545
+ project_path: sessionInfo.projectPath,
546
+ original_goal: taskAnalysis.current_goal,
547
+ raw_user_prompt: rawUserPrompt,
548
+ constraints: taskAnalysis.constraints || [],
549
+ task_type: 'subtask',
550
+ parent_session_id: parentId,
551
+ });
552
+ activeSessionId = subtaskId;
553
+ activeSessions.set(subtaskId, {
554
+ sessionId: subtaskId,
555
+ promptCount: 1,
556
+ projectPath: sessionInfo.projectPath,
557
+ });
558
+ logger.info({ msg: 'Created subtask session', sessionId: subtaskId.substring(0, 8), parent: parentId?.substring(0, 8) });
559
+ taskLog('ORCHESTRATION_SUBTASK', {
560
+ sessionId: subtaskId,
561
+ parentId: parentId || 'none',
562
+ goal: taskAnalysis.current_goal,
563
+ });
564
+ break;
565
+ }
566
+ case 'parallel_task': {
567
+ const parentId = sessionInfo.currentSession?.session_id || taskAnalysis.parent_task_id;
568
+ const parallelId = randomUUID();
569
+ activeSession = createSessionState({
570
+ session_id: parallelId,
571
+ project_path: sessionInfo.projectPath,
572
+ original_goal: taskAnalysis.current_goal,
573
+ raw_user_prompt: rawUserPrompt,
574
+ constraints: taskAnalysis.constraints || [],
575
+ task_type: 'parallel',
576
+ parent_session_id: parentId,
577
+ });
578
+ activeSessionId = parallelId;
579
+ activeSessions.set(parallelId, {
580
+ sessionId: parallelId,
581
+ promptCount: 1,
582
+ projectPath: sessionInfo.projectPath,
583
+ });
584
+ logger.info({ msg: 'Created parallel task session', sessionId: parallelId.substring(0, 8), parent: parentId?.substring(0, 8) });
585
+ taskLog('ORCHESTRATION_PARALLEL', {
586
+ sessionId: parallelId,
587
+ parentId: parentId || 'none',
588
+ goal: taskAnalysis.current_goal,
589
+ });
590
+ break;
591
+ }
592
+ case 'task_complete': {
593
+ if (sessionInfo.currentSession) {
594
+ try {
595
+ if (taskAnalysis.current_goal && !sessionInfo.currentSession.original_goal) {
596
+ updateSessionState(sessionInfo.currentSession.session_id, {
597
+ original_goal: taskAnalysis.current_goal,
598
+ });
599
+ sessionInfo.currentSession.original_goal = taskAnalysis.current_goal;
600
+ }
601
+ updateSessionState(sessionInfo.currentSession.session_id, {
602
+ final_response: textContent.substring(0, 10000),
603
+ });
604
+ await saveToTeamMemory(sessionInfo.currentSession.session_id, 'complete', taskAnalysis.task_type, requestHeaders);
605
+ markSessionCompleted(sessionInfo.currentSession.session_id);
606
+ activeSessions.delete(sessionInfo.currentSession.session_id);
607
+ lastDriftResults.delete(sessionInfo.currentSession.session_id);
608
+ taskLog('ORCHESTRATION_TASK_COMPLETE', {
609
+ sessionId: sessionInfo.currentSession.session_id,
610
+ goal: sessionInfo.currentSession.original_goal,
611
+ });
612
+ if (taskAnalysis.task_type === 'planning') {
613
+ try {
614
+ const allSteps = getValidatedSteps(sessionInfo.currentSession.session_id);
615
+ const planSummary = await generateSessionSummary(sessionInfo.currentSession, allSteps, 2000, requestHeaders);
616
+ setPendingPlanClear({
617
+ projectPath: sessionInfo.projectPath,
618
+ summary: planSummary,
619
+ });
620
+ logger.info({
621
+ msg: 'PLANNING_CLEAR triggered',
622
+ sessionId: sessionInfo.currentSession.session_id.substring(0, 8),
623
+ summaryLen: planSummary.length,
624
+ });
625
+ }
626
+ catch {
627
+ // Silent fail - planning CLEAR is optional
628
+ }
629
+ }
630
+ logger.info({ msg: 'Task complete - saved to team memory, marked completed' });
631
+ }
632
+ catch (err) {
633
+ logger.info({ msg: 'Failed to save completed task', error: String(err) });
634
+ }
635
+ }
636
+ else if (textContent.length > 100) {
637
+ // Instant complete
638
+ try {
639
+ const newSessionId = randomUUID();
640
+ const instantSession = createSessionState({
641
+ session_id: newSessionId,
642
+ project_path: sessionInfo.projectPath,
643
+ original_goal: taskAnalysis.current_goal || '',
644
+ raw_user_prompt: rawUserPrompt,
645
+ task_type: 'main',
646
+ });
647
+ updateSessionState(newSessionId, {
648
+ final_response: textContent.substring(0, 10000),
649
+ });
650
+ await saveToTeamMemory(newSessionId, 'complete', taskAnalysis.task_type, requestHeaders);
651
+ markSessionCompleted(newSessionId);
652
+ logger.info({ msg: 'Instant complete - new task saved immediately', sessionId: newSessionId.substring(0, 8) });
653
+ taskLog('ORCHESTRATION_TASK_COMPLETE', {
654
+ sessionId: newSessionId,
655
+ goal: taskAnalysis.current_goal || '',
656
+ source: 'instant_complete',
657
+ });
658
+ }
659
+ catch (err) {
660
+ logger.info({ msg: 'Failed to save instant complete task', error: String(err) });
661
+ }
662
+ }
663
+ return;
664
+ }
665
+ case 'subtask_complete': {
666
+ if (sessionInfo.currentSession) {
667
+ const parentId = sessionInfo.currentSession.parent_session_id;
668
+ try {
669
+ await saveToTeamMemory(sessionInfo.currentSession.session_id, 'complete', taskAnalysis.task_type, requestHeaders);
670
+ markSessionCompleted(sessionInfo.currentSession.session_id);
671
+ activeSessions.delete(sessionInfo.currentSession.session_id);
672
+ lastDriftResults.delete(sessionInfo.currentSession.session_id);
673
+ if (parentId) {
674
+ const parentSession = getSessionState(parentId);
675
+ if (parentSession) {
676
+ activeSessionId = parentId;
677
+ activeSession = parentSession;
678
+ logger.info({ msg: 'Subtask complete - returning to parent', parent: parentId.substring(0, 8) });
679
+ taskLog('ORCHESTRATION_SUBTASK_COMPLETE', {
680
+ sessionId: sessionInfo.currentSession.session_id,
681
+ parentId: parentId,
682
+ goal: sessionInfo.currentSession.original_goal,
683
+ });
684
+ }
685
+ }
686
+ }
687
+ catch (err) {
688
+ logger.info({ msg: 'Failed to save completed subtask', error: String(err) });
689
+ }
690
+ }
691
+ break;
692
+ }
693
+ }
694
+ }
695
+ catch (error) {
696
+ logger.info({ msg: 'Task analysis failed, creating fallback session', error: String(error) });
697
+ if (!sessionInfo.currentSession) {
698
+ const newSessionId = randomUUID();
699
+ activeSession = createSessionState({
700
+ session_id: newSessionId,
701
+ project_path: sessionInfo.projectPath,
702
+ original_goal: latestUserMessage.substring(0, 200),
703
+ raw_user_prompt: rawUserPrompt,
704
+ constraints: [],
705
+ task_type: 'main',
706
+ });
707
+ activeSessionId = newSessionId;
708
+ taskLog('FALLBACK_SESSION_CREATED', {
709
+ sessionId: newSessionId,
710
+ reason: 'task_analysis_failed',
711
+ goal: latestUserMessage.substring(0, 80),
712
+ });
713
+ }
714
+ }
715
+ }
716
+ // Token usage and CLEAR mode
717
+ const usage = adapter.extractUsage(response);
718
+ const actualContextSize = usage.cacheCreation + usage.cacheRead;
719
+ if (activeSession) {
720
+ updateTokenCount(activeSessionId, actualContextSize);
721
+ }
722
+ logger.info({
723
+ msg: 'Token usage',
724
+ input: usage.inputTokens,
725
+ output: usage.outputTokens,
726
+ total: usage.totalTokens,
727
+ cacheCreation: usage.cacheCreation,
728
+ cacheRead: usage.cacheRead,
729
+ actualContextSize,
730
+ activeSession: activeSessionId.substring(0, 8),
731
+ });
732
+ // CLEAR mode pre-compute
733
+ const preComputeThreshold = Math.floor(config.TOKEN_CLEAR_THRESHOLD * 0.85);
734
+ if (activeSession &&
735
+ actualContextSize > preComputeThreshold &&
736
+ !activeSession.pending_clear_summary) {
737
+ const allSteps = getValidatedSteps(activeSessionId);
738
+ generateSessionSummary(activeSession, allSteps, 15000, requestHeaders).then(summary => {
739
+ updateSessionState(activeSessionId, { pending_clear_summary: summary });
740
+ logger.info({
741
+ msg: 'CLEAR summary pre-computed',
742
+ actualContextSize,
743
+ threshold: preComputeThreshold,
744
+ summaryLength: summary.length,
745
+ });
746
+ }).catch(err => {
747
+ logger.info({ msg: 'CLEAR summary generation failed', error: String(err) });
748
+ });
749
+ }
750
+ // Capture final_response
751
+ if (isEndTurn && textContent.length > 100 && activeSessionId) {
752
+ updateSessionState(activeSessionId, {
753
+ final_response: textContent.substring(0, 10000),
754
+ });
755
+ }
756
+ if (actions.length === 0) {
757
+ return;
758
+ }
759
+ const toolNames = actions.map(a => a.toolName);
760
+ logger.info({
761
+ msg: 'Actions parsed',
762
+ count: actions.length,
763
+ tools: toolNames,
764
+ });
765
+ // Recovery alignment check
766
+ if (activeSession && activeSession.waiting_for_recovery) {
767
+ const lastDrift = lastDriftResults.get(activeSessionId);
768
+ const recoveryPlan = lastDrift?.recoverySteps ? { steps: lastDrift.recoverySteps } : undefined;
769
+ for (const action of actions) {
770
+ const alignment = checkRecoveryAlignment({ actionType: action.actionType, files: action.files, command: action.command }, recoveryPlan, activeSession);
771
+ if (alignment.aligned) {
772
+ updateSessionMode(activeSessionId, 'normal');
773
+ markWaitingForRecovery(activeSessionId, false);
774
+ updateSessionState(activeSessionId, { escalation_count: 0 });
775
+ lastDriftResults.delete(activeSessionId);
776
+ logger.info({
777
+ msg: 'Recovery alignment SUCCESS - resuming normal mode',
778
+ reason: alignment.reason,
779
+ });
780
+ }
781
+ else {
782
+ incrementEscalation(activeSessionId);
783
+ logger.info({
784
+ msg: 'Recovery alignment FAILED - escalating',
785
+ reason: alignment.reason,
786
+ escalation: activeSession.escalation_count + 1,
787
+ });
788
+ }
789
+ }
790
+ }
791
+ // Drift check
792
+ let driftScore;
793
+ let skipSteps = false;
794
+ const memSessionInfo = activeSessions.get(activeSessionId);
795
+ const promptCount = memSessionInfo?.promptCount || sessionInfo.promptCount;
796
+ const recentStepsForCheck = activeSessionId ? getRecentSteps(activeSessionId, 10) : [];
797
+ const hasFileModifications = recentStepsForCheck.some(s => s.action_type === 'edit' || s.action_type === 'write');
798
+ const hasValidGoal = activeSession?.original_goal &&
799
+ activeSession.original_goal.length > 10 &&
800
+ !activeSession.original_goal.includes('the original task');
801
+ if (hasValidGoal && hasFileModifications && promptCount % config.DRIFT_CHECK_INTERVAL === 0) {
802
+ if (activeSession) {
803
+ const driftResult = await checkDrift({ sessionState: activeSession, recentSteps: recentStepsForCheck, latestUserMessage }, requestHeaders);
804
+ lastDriftResults.set(activeSessionId, driftResult);
805
+ driftScore = driftResult.score;
806
+ skipSteps = shouldSkipSteps(driftScore);
807
+ logger.info({
808
+ msg: 'Drift check',
809
+ score: driftResult.score,
810
+ type: driftResult.driftType,
811
+ diagnostic: driftResult.diagnostic,
812
+ });
813
+ const correctionLevel = scoreToCorrectionLevel(driftScore);
814
+ const currentEscalation = activeSession.escalation_count || 0;
815
+ const maxCorrectionAttempts = 2;
816
+ const shouldCorrect = correctionLevel && currentEscalation < maxCorrectionAttempts;
817
+ if (shouldCorrect && (correctionLevel === 'intervene' || correctionLevel === 'halt')) {
818
+ updateSessionMode(activeSessionId, 'drifted');
819
+ markWaitingForRecovery(activeSessionId, true);
820
+ incrementEscalation(activeSessionId);
821
+ const correction = buildCorrection(driftResult, activeSession, correctionLevel);
822
+ const correctionText = formatCorrectionForInjection(correction);
823
+ updateSessionState(activeSessionId, { pending_correction: correctionText });
824
+ logger.info({
825
+ msg: 'Pre-computed correction saved',
826
+ level: correctionLevel,
827
+ correctionLength: correctionText.length,
828
+ attempt: currentEscalation + 1,
829
+ });
830
+ }
831
+ else if (shouldCorrect && correctionLevel) {
832
+ incrementEscalation(activeSessionId);
833
+ const correction = buildCorrection(driftResult, activeSession, correctionLevel);
834
+ const correctionText = formatCorrectionForInjection(correction);
835
+ updateSessionState(activeSessionId, { pending_correction: correctionText });
836
+ logger.info({
837
+ msg: 'Pre-computed mild correction saved',
838
+ level: correctionLevel,
839
+ attempt: currentEscalation + 1,
840
+ });
841
+ }
842
+ else if (currentEscalation >= maxCorrectionAttempts && correctionLevel) {
843
+ logger.info({
844
+ msg: 'Max correction attempts reached - giving up',
845
+ attempts: currentEscalation,
846
+ lastScore: driftScore,
847
+ });
848
+ updateSessionMode(activeSessionId, 'normal');
849
+ markWaitingForRecovery(activeSessionId, false);
850
+ updateSessionState(activeSessionId, {
851
+ pending_correction: undefined,
852
+ escalation_count: 0,
853
+ });
854
+ }
855
+ else if (driftScore >= 5) {
856
+ updateSessionMode(activeSessionId, 'normal');
857
+ markWaitingForRecovery(activeSessionId, false);
858
+ lastDriftResults.delete(activeSessionId);
859
+ updateSessionState(activeSessionId, {
860
+ pending_correction: undefined,
861
+ escalation_count: 0,
862
+ });
863
+ }
864
+ updateLastChecked(activeSessionId, Date.now());
865
+ if (skipSteps) {
866
+ for (const action of actions) {
867
+ logDriftEvent({
868
+ session_id: activeSessionId,
869
+ action_type: action.actionType,
870
+ files: action.files,
871
+ drift_score: driftScore,
872
+ drift_reason: driftResult.diagnostic,
873
+ recovery_plan: driftResult.recoverySteps ? { steps: driftResult.recoverySteps } : undefined,
874
+ });
875
+ }
876
+ logger.info({
877
+ msg: 'Actions logged to drift_log (skipped steps)',
878
+ reason: 'score < 5',
879
+ });
880
+ return;
881
+ }
882
+ }
883
+ }
884
+ // Save steps
885
+ let previousReasoning = null;
886
+ for (const action of actions) {
887
+ const currentReasoning = textContent.substring(0, 1000);
888
+ const isDuplicate = currentReasoning === previousReasoning;
889
+ const isKeyDecision = !isDuplicate && detectKeyDecision(action, textContent);
890
+ createStep({
891
+ session_id: activeSessionId,
892
+ action_type: action.actionType,
893
+ files: action.files,
894
+ folders: action.folders,
895
+ command: action.command,
896
+ reasoning: isDuplicate ? undefined : currentReasoning,
897
+ drift_score: driftScore,
898
+ is_validated: !skipSteps,
899
+ is_key_decision: isKeyDecision,
900
+ });
901
+ previousReasoning = currentReasoning;
902
+ if (isKeyDecision) {
903
+ logger.info({
904
+ msg: 'Key decision detected',
905
+ actionType: action.actionType,
906
+ files: action.files.slice(0, 3),
907
+ });
908
+ }
909
+ }
910
+ }
911
+ // Helper functions
912
+ function getMessages(body) {
913
+ const b = body;
914
+ if (Array.isArray(b.messages))
915
+ return b.messages;
916
+ if (Array.isArray(b.input))
917
+ return b.input;
918
+ return [];
919
+ }
920
+ function getMessageCount(body) {
921
+ return getMessages(body).length;
922
+ }
923
+ function getAssistantContent(response, adapter) {
924
+ const blocks = adapter.getToolUseBlocks(response);
925
+ if (blocks.length > 0) {
926
+ // Return the content array for Claude, or serialized for Codex
927
+ const r = response;
928
+ if (r.content)
929
+ return r.content;
930
+ if (r.output)
931
+ return r.output;
932
+ }
933
+ return [];
934
+ }
935
+ function detectKeyDecision(action, reasoning) {
936
+ if (action.actionType === 'edit' || action.actionType === 'write') {
937
+ return true;
938
+ }
939
+ const decisionKeywords = [
940
+ 'decision', 'decided', 'chose', 'chosen', 'selected', 'picked',
941
+ 'approach', 'strategy', 'solution', 'implementation',
942
+ 'because', 'reason', 'rationale', 'trade-off', 'tradeoff',
943
+ 'instead of', 'rather than', 'prefer', 'opted',
944
+ 'conclusion', 'determined', 'resolved'
945
+ ];
946
+ const reasoningLower = reasoning.toLowerCase();
947
+ const hasDecisionKeyword = decisionKeywords.some(kw => reasoningLower.includes(kw));
948
+ if (hasDecisionKeyword && reasoning.length > 200) {
949
+ return true;
950
+ }
951
+ return false;
952
+ }
953
+ // Export for server.ts startup/cleanup
954
+ export { activeSessions, lastDriftResults, lastMessageCount };