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.
- package/dist/cli/agents/registry.d.ts +17 -0
- package/dist/cli/agents/registry.js +132 -0
- package/dist/cli/commands/agents.d.ts +1 -0
- package/dist/cli/commands/agents.js +48 -0
- package/dist/cli/commands/disable.d.ts +1 -0
- package/dist/cli/commands/disable.js +179 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +157 -0
- package/dist/{commands → cli/commands}/drift-test.js +39 -26
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +90 -0
- package/dist/{commands → cli/commands}/login.js +19 -18
- package/dist/{commands → cli/commands}/logout.js +1 -1
- package/dist/{commands → cli/commands}/proxy-status.js +1 -1
- package/dist/cli/commands/setup.d.ts +6 -0
- package/dist/cli/commands/setup.js +309 -0
- package/dist/{commands → cli/commands}/status.js +1 -1
- package/dist/{commands → cli/commands}/sync.d.ts +1 -0
- package/dist/{commands → cli/commands}/sync.js +59 -4
- package/dist/{commands → cli/commands}/uninstall.js +2 -2
- package/dist/cli/index.js +270 -0
- package/dist/{lib → core/cloud}/cloud-sync.d.ts +3 -3
- package/dist/{lib → core/cloud}/cloud-sync.js +10 -10
- package/dist/{lib → core/extraction}/correction-builder-proxy.d.ts +1 -1
- package/dist/{lib → core/extraction}/correction-builder-proxy.js +0 -4
- package/dist/{lib → core/extraction}/drift-checker-proxy.d.ts +13 -9
- package/dist/core/extraction/drift-checker-proxy.js +510 -0
- package/dist/{lib → core/extraction}/llm-extractor.d.ts +8 -38
- package/dist/{lib → core/extraction}/llm-extractor.js +132 -220
- package/dist/{lib → core}/store/sessions.js +3 -19
- package/dist/core/store/store.d.ts +1 -0
- package/dist/{lib → core/store}/store.js +1 -1
- package/dist/{lib → core}/store/types.d.ts +0 -4
- package/dist/integrations/mcp/cache.d.ts +27 -0
- package/dist/integrations/mcp/cache.js +106 -0
- package/dist/integrations/mcp/capture/antigravity-parser.d.ts +26 -0
- package/dist/integrations/mcp/capture/antigravity-parser.js +272 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.d.ts +24 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.js +153 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.d.ts +29 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.js +115 -0
- package/dist/integrations/mcp/capture/cli-extractor.d.ts +18 -0
- package/dist/integrations/mcp/capture/cli-extractor.js +258 -0
- package/dist/integrations/mcp/capture/cli-synced.d.ts +4 -0
- package/dist/integrations/mcp/capture/cli-synced.js +62 -0
- package/dist/integrations/mcp/capture/cli-transform.d.ts +30 -0
- package/dist/integrations/mcp/capture/cli-transform.js +62 -0
- package/dist/integrations/mcp/capture/cli-watcher.d.ts +31 -0
- package/dist/integrations/mcp/capture/cli-watcher.js +106 -0
- package/dist/integrations/mcp/capture/hook-handler.d.ts +2 -0
- package/dist/integrations/mcp/capture/hook-handler.js +157 -0
- package/dist/integrations/mcp/capture/sqlite-reader.d.ts +35 -0
- package/dist/integrations/mcp/capture/sqlite-reader.js +388 -0
- package/dist/integrations/mcp/capture/sync-tracker.d.ts +16 -0
- package/dist/integrations/mcp/capture/sync-tracker.js +102 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.d.ts +19 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.js +123 -0
- package/dist/integrations/mcp/index.d.ts +1 -0
- package/dist/integrations/mcp/index.js +94 -0
- package/dist/integrations/mcp/logger.d.ts +8 -0
- package/dist/integrations/mcp/logger.js +50 -0
- package/dist/integrations/mcp/server.d.ts +5 -0
- package/dist/integrations/mcp/server.js +58 -0
- package/dist/integrations/mcp/tools/expand.d.ts +1 -0
- package/dist/integrations/mcp/tools/expand.js +53 -0
- package/dist/integrations/mcp/tools/preview.d.ts +1 -0
- package/dist/integrations/mcp/tools/preview.js +64 -0
- package/dist/integrations/proxy/agents/base.d.ts +43 -0
- package/dist/integrations/proxy/agents/base.js +13 -0
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.d.ts +4 -8
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.js +4 -33
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.d.ts +1 -1
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.js +22 -6
- package/dist/integrations/proxy/agents/claude/index.d.ts +43 -0
- package/dist/integrations/proxy/agents/claude/index.js +386 -0
- package/dist/{proxy/action-parser.d.ts → integrations/proxy/agents/claude/parser.d.ts} +1 -1
- package/dist/integrations/proxy/agents/codex/extractors.d.ts +6 -0
- package/dist/integrations/proxy/agents/codex/extractors.js +49 -0
- package/dist/integrations/proxy/agents/codex/forwarder.d.ts +9 -0
- package/dist/integrations/proxy/agents/codex/forwarder.js +125 -0
- package/dist/integrations/proxy/agents/codex/index.d.ts +44 -0
- package/dist/integrations/proxy/agents/codex/index.js +371 -0
- package/dist/integrations/proxy/agents/codex/parser.d.ts +11 -0
- package/dist/integrations/proxy/agents/codex/parser.js +104 -0
- package/dist/integrations/proxy/agents/codex/patch.d.ts +12 -0
- package/dist/integrations/proxy/agents/codex/patch.js +40 -0
- package/dist/integrations/proxy/agents/codex/settings.d.ts +18 -0
- package/dist/integrations/proxy/agents/codex/settings.js +73 -0
- package/dist/integrations/proxy/agents/codex/types.d.ts +59 -0
- package/dist/integrations/proxy/agents/codex/types.js +2 -0
- package/dist/integrations/proxy/agents/index.d.ts +11 -0
- package/dist/integrations/proxy/agents/index.js +25 -0
- package/dist/integrations/proxy/agents/types.d.ts +77 -0
- package/dist/integrations/proxy/agents/types.js +2 -0
- package/dist/{proxy → integrations/proxy/cache}/extended-cache.js +2 -6
- package/dist/{proxy → integrations/proxy}/config.js +1 -1
- package/dist/{proxy → integrations/proxy}/handlers/preprocess.d.ts +3 -3
- package/dist/integrations/proxy/handlers/preprocess.js +194 -0
- package/dist/integrations/proxy/index.js +20 -0
- package/dist/integrations/proxy/injection/memory-injection.d.ts +56 -0
- package/dist/integrations/proxy/injection/memory-injection.js +252 -0
- package/dist/integrations/proxy/orchestrator.d.ts +30 -0
- package/dist/integrations/proxy/orchestrator.js +954 -0
- package/dist/integrations/proxy/request-processor.d.ts +14 -0
- package/dist/integrations/proxy/request-processor.js +68 -0
- package/dist/{proxy → integrations/proxy}/response-processor.d.ts +4 -3
- package/dist/{proxy → integrations/proxy}/response-processor.js +51 -43
- package/dist/{proxy → integrations/proxy}/server.d.ts +0 -1
- package/dist/integrations/proxy/server.js +146 -0
- package/dist/{proxy → integrations/proxy}/types.d.ts +4 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.d.ts +1 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.js +5 -0
- package/package.json +31 -10
- package/postinstall.js +62 -6
- package/dist/cli.js +0 -149
- package/dist/commands/capture.d.ts +0 -6
- package/dist/commands/capture.js +0 -324
- package/dist/commands/disable.d.ts +0 -1
- package/dist/commands/disable.js +0 -14
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +0 -89
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +0 -52
- package/dist/commands/inject.d.ts +0 -5
- package/dist/commands/inject.js +0 -88
- package/dist/commands/prompt-inject.d.ts +0 -4
- package/dist/commands/prompt-inject.js +0 -451
- package/dist/commands/unregister.d.ts +0 -1
- package/dist/commands/unregister.js +0 -28
- package/dist/lib/anchor-extractor.d.ts +0 -30
- package/dist/lib/anchor-extractor.js +0 -296
- package/dist/lib/correction-builder.d.ts +0 -10
- package/dist/lib/correction-builder.js +0 -226
- package/dist/lib/drift-checker-proxy.js +0 -373
- package/dist/lib/drift-checker.d.ts +0 -66
- package/dist/lib/drift-checker.js +0 -341
- package/dist/lib/hooks.d.ts +0 -38
- package/dist/lib/hooks.js +0 -291
- package/dist/lib/jsonl-parser.d.ts +0 -87
- package/dist/lib/jsonl-parser.js +0 -281
- package/dist/lib/session-parser.d.ts +0 -44
- package/dist/lib/session-parser.js +0 -256
- package/dist/lib/store.d.ts +0 -1
- package/dist/proxy/cache.d.ts +0 -32
- package/dist/proxy/cache.js +0 -47
- package/dist/proxy/handlers/preprocess.js +0 -186
- package/dist/proxy/index.js +0 -30
- package/dist/proxy/injection/delta-tracking.d.ts +0 -11
- package/dist/proxy/injection/delta-tracking.js +0 -94
- package/dist/proxy/injection/injectors.d.ts +0 -7
- package/dist/proxy/injection/injectors.js +0 -139
- package/dist/proxy/request-processor.d.ts +0 -27
- package/dist/proxy/request-processor.js +0 -233
- package/dist/proxy/server.js +0 -1289
- /package/dist/{commands → cli/commands}/drift-test.d.ts +0 -0
- /package/dist/{commands → cli/commands}/login.d.ts +0 -0
- /package/dist/{commands → cli/commands}/logout.d.ts +0 -0
- /package/dist/{commands → cli/commands}/proxy-status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/uninstall.d.ts +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{lib → core/cloud}/api-client.d.ts +0 -0
- /package/dist/{lib → core/cloud}/api-client.js +0 -0
- /package/dist/{lib → core/cloud}/credentials.d.ts +0 -0
- /package/dist/{lib → core/cloud}/credentials.js +0 -0
- /package/dist/{lib → core}/store/convenience.d.ts +0 -0
- /package/dist/{lib → core}/store/convenience.js +0 -0
- /package/dist/{lib → core}/store/database.d.ts +0 -0
- /package/dist/{lib → core}/store/database.js +0 -0
- /package/dist/{lib → core}/store/drift.d.ts +0 -0
- /package/dist/{lib → core}/store/drift.js +0 -0
- /package/dist/{lib → core}/store/index.d.ts +0 -0
- /package/dist/{lib → core}/store/index.js +0 -0
- /package/dist/{lib → core}/store/sessions.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.js +0 -0
- /package/dist/{lib → core}/store/tasks.d.ts +0 -0
- /package/dist/{lib → core}/store/tasks.js +0 -0
- /package/dist/{lib → core}/store/types.js +0 -0
- /package/dist/{proxy/action-parser.js → integrations/proxy/agents/claude/parser.js} +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.d.ts +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.js +0 -0
- /package/dist/{proxy → integrations/proxy/cache}/extended-cache.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/config.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/index.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/types.js +0 -0
- /package/dist/{lib → utils}/debug.d.ts +0 -0
- /package/dist/{lib → utils}/debug.js +0 -0
- /package/dist/{lib → utils}/utils.d.ts +0 -0
- /package/dist/{lib → utils}/utils.js +0 -0
package/dist/proxy/server.js
DELETED
|
@@ -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
|
-
}
|