grov 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/cli.js +8 -0
- package/dist/lib/api-client.d.ts +18 -1
- package/dist/lib/api-client.js +57 -0
- package/dist/lib/llm-extractor.d.ts +14 -38
- package/dist/lib/llm-extractor.js +380 -406
- package/dist/lib/store/convenience.d.ts +40 -0
- package/dist/lib/store/convenience.js +104 -0
- package/dist/lib/store/database.d.ts +22 -0
- package/dist/lib/store/database.js +375 -0
- package/dist/lib/store/drift.d.ts +9 -0
- package/dist/lib/store/drift.js +89 -0
- package/dist/lib/store/index.d.ts +7 -0
- package/dist/lib/store/index.js +13 -0
- package/dist/lib/store/sessions.d.ts +32 -0
- package/dist/lib/store/sessions.js +240 -0
- package/dist/lib/store/steps.d.ts +40 -0
- package/dist/lib/store/steps.js +161 -0
- package/dist/lib/store/tasks.d.ts +33 -0
- package/dist/lib/store/tasks.js +133 -0
- package/dist/lib/store/types.d.ts +167 -0
- package/dist/lib/store/types.js +2 -0
- package/dist/lib/store.d.ts +1 -436
- package/dist/lib/store.js +2 -1478
- package/dist/proxy/cache.d.ts +36 -0
- package/dist/proxy/cache.js +51 -0
- package/dist/proxy/config.d.ts +1 -0
- package/dist/proxy/config.js +2 -0
- package/dist/proxy/extended-cache.d.ts +10 -0
- package/dist/proxy/extended-cache.js +155 -0
- package/dist/proxy/handlers/preprocess.d.ts +20 -0
- package/dist/proxy/handlers/preprocess.js +169 -0
- package/dist/proxy/injection/delta-tracking.d.ts +11 -0
- package/dist/proxy/injection/delta-tracking.js +93 -0
- package/dist/proxy/injection/injectors.d.ts +7 -0
- package/dist/proxy/injection/injectors.js +139 -0
- package/dist/proxy/request-processor.d.ts +18 -4
- package/dist/proxy/request-processor.js +151 -30
- package/dist/proxy/response-processor.js +93 -45
- package/dist/proxy/server.d.ts +0 -1
- package/dist/proxy/server.js +342 -566
- package/dist/proxy/types.d.ts +13 -0
- package/dist/proxy/types.js +2 -0
- package/dist/proxy/utils/extractors.d.ts +18 -0
- package/dist/proxy/utils/extractors.js +109 -0
- package/dist/proxy/utils/logging.d.ts +18 -0
- package/dist/proxy/utils/logging.js +42 -0
- package/package.json +5 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global team memory cache
|
|
3
|
+
* - Calculated ONCE on first request, reused for ALL subsequent requests
|
|
4
|
+
* - Invalidated only on: sync completion (in .then() callback), proxy restart
|
|
5
|
+
* - Ensures system prompt prefix stays CONSTANT for Anthropic cache preservation
|
|
6
|
+
*/
|
|
7
|
+
export declare let globalTeamMemoryCache: {
|
|
8
|
+
projectPath: string;
|
|
9
|
+
content: string;
|
|
10
|
+
} | null;
|
|
11
|
+
/**
|
|
12
|
+
* Invalidate the global team memory cache
|
|
13
|
+
* Called after successful sync to cloud (in .then() callback)
|
|
14
|
+
* This ensures cache is only invalidated AFTER data is in cloud
|
|
15
|
+
*/
|
|
16
|
+
export declare function invalidateTeamMemoryCache(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Set the global team memory cache
|
|
19
|
+
* @param projectPath - Project path for cache key
|
|
20
|
+
* @param content - Formatted team memory content
|
|
21
|
+
*/
|
|
22
|
+
export declare function setTeamMemoryCache(projectPath: string, content: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Get the current cache content if it matches the project path
|
|
25
|
+
* @param projectPath - Project path to check
|
|
26
|
+
* @returns Cached content or null if not cached/different project
|
|
27
|
+
*/
|
|
28
|
+
export declare function getTeamMemoryCache(projectPath: string): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Check if cache exists for a specific project
|
|
31
|
+
*/
|
|
32
|
+
export declare function hasCacheForProject(projectPath: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Get current cache project path (for logging/debugging)
|
|
35
|
+
*/
|
|
36
|
+
export declare function getCacheProjectPath(): string | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Phase 0 verified
|
|
2
|
+
// Cache management for team memory injection
|
|
3
|
+
// Shared between server.ts and response-processor.ts to avoid circular dependencies
|
|
4
|
+
/**
|
|
5
|
+
* Global team memory cache
|
|
6
|
+
* - Calculated ONCE on first request, reused for ALL subsequent requests
|
|
7
|
+
* - Invalidated only on: sync completion (in .then() callback), proxy restart
|
|
8
|
+
* - Ensures system prompt prefix stays CONSTANT for Anthropic cache preservation
|
|
9
|
+
*/
|
|
10
|
+
export let globalTeamMemoryCache = null;
|
|
11
|
+
/**
|
|
12
|
+
* Invalidate the global team memory cache
|
|
13
|
+
* Called after successful sync to cloud (in .then() callback)
|
|
14
|
+
* This ensures cache is only invalidated AFTER data is in cloud
|
|
15
|
+
*/
|
|
16
|
+
export function invalidateTeamMemoryCache() {
|
|
17
|
+
globalTeamMemoryCache = null;
|
|
18
|
+
console.log('[CACHE] Team memory cache invalidated');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Set the global team memory cache
|
|
22
|
+
* @param projectPath - Project path for cache key
|
|
23
|
+
* @param content - Formatted team memory content
|
|
24
|
+
*/
|
|
25
|
+
export function setTeamMemoryCache(projectPath, content) {
|
|
26
|
+
globalTeamMemoryCache = { projectPath, content };
|
|
27
|
+
console.log(`[CACHE] Team memory cache set for project: ${projectPath} (${content.length} chars)`);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the current cache content if it matches the project path
|
|
31
|
+
* @param projectPath - Project path to check
|
|
32
|
+
* @returns Cached content or null if not cached/different project
|
|
33
|
+
*/
|
|
34
|
+
export function getTeamMemoryCache(projectPath) {
|
|
35
|
+
if (globalTeamMemoryCache && globalTeamMemoryCache.projectPath === projectPath) {
|
|
36
|
+
return globalTeamMemoryCache.content;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if cache exists for a specific project
|
|
42
|
+
*/
|
|
43
|
+
export function hasCacheForProject(projectPath) {
|
|
44
|
+
return globalTeamMemoryCache?.projectPath === projectPath;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get current cache project path (for logging/debugging)
|
|
48
|
+
*/
|
|
49
|
+
export function getCacheProjectPath() {
|
|
50
|
+
return globalTeamMemoryCache?.projectPath || null;
|
|
51
|
+
}
|
package/dist/proxy/config.d.ts
CHANGED
package/dist/proxy/config.js
CHANGED
|
@@ -19,6 +19,8 @@ export const config = {
|
|
|
19
19
|
// Logging
|
|
20
20
|
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
|
21
21
|
LOG_REQUESTS: process.env.LOG_REQUESTS !== 'false',
|
|
22
|
+
// Extended Cache - preserve Anthropic prompt cache during idle
|
|
23
|
+
EXTENDED_CACHE_ENABLED: process.env.GROV_EXTENDED_CACHE === 'true',
|
|
22
24
|
};
|
|
23
25
|
// Headers to forward to Anthropic (whitelist approach)
|
|
24
26
|
export const FORWARD_HEADERS = [
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ExtendedCacheEntry {
|
|
2
|
+
headers: Record<string, string>;
|
|
3
|
+
rawBody: Buffer;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
keepAliveCount: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const extendedCache: Map<string, ExtendedCacheEntry>;
|
|
8
|
+
export declare function log(msg: string): void;
|
|
9
|
+
export declare function evictOldestCacheEntry(): void;
|
|
10
|
+
export declare function checkExtendedCache(): Promise<void>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Extended Cache - Keep Anthropic cache alive during idle
|
|
2
|
+
// Sends minimal keep-alive requests to prevent cache TTL expiration
|
|
3
|
+
import { forwardToAnthropic } from './forwarder.js';
|
|
4
|
+
export const extendedCache = new Map();
|
|
5
|
+
// Timing constants
|
|
6
|
+
const EXTENDED_CACHE_IDLE_THRESHOLD = 4 * 60 * 1000; // 4 minutes (under 5-min TTL)
|
|
7
|
+
const EXTENDED_CACHE_MAX_IDLE = 10 * 60 * 1000; // 10 minutes total
|
|
8
|
+
const EXTENDED_CACHE_MAX_KEEPALIVES = 2;
|
|
9
|
+
const EXTENDED_CACHE_MAX_ENTRIES = 100; // Max concurrent sessions (memory cap)
|
|
10
|
+
export function log(msg) {
|
|
11
|
+
console.log(`[CACHE] ${msg}`);
|
|
12
|
+
}
|
|
13
|
+
export function evictOldestCacheEntry() {
|
|
14
|
+
if (extendedCache.size < EXTENDED_CACHE_MAX_ENTRIES)
|
|
15
|
+
return;
|
|
16
|
+
let oldestId = null;
|
|
17
|
+
let oldestTime = Infinity;
|
|
18
|
+
for (const [id, entry] of extendedCache) {
|
|
19
|
+
if (entry.timestamp < oldestTime) {
|
|
20
|
+
oldestTime = entry.timestamp;
|
|
21
|
+
oldestId = id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (oldestId) {
|
|
25
|
+
extendedCache.delete(oldestId);
|
|
26
|
+
log(`Extended cache: evicted ${oldestId.substring(0, 8)} (capacity limit)`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function sendExtendedCacheKeepAlive(projectPath, entry) {
|
|
30
|
+
const projectName = projectPath.split('/').pop() || projectPath;
|
|
31
|
+
let rawBodyStr = entry.rawBody.toString('utf-8');
|
|
32
|
+
// 1. Find messages array and add "." message before closing bracket
|
|
33
|
+
const messagesMatch = rawBodyStr.match(/"messages"\s*:\s*\[/);
|
|
34
|
+
if (!messagesMatch || messagesMatch.index === undefined) {
|
|
35
|
+
throw new Error('Cannot find messages array in rawBody');
|
|
36
|
+
}
|
|
37
|
+
// Find closing bracket of messages array (handling nested arrays/objects)
|
|
38
|
+
const messagesStart = messagesMatch.index + messagesMatch[0].length;
|
|
39
|
+
let bracketDepth = 1; // We're inside the [ already
|
|
40
|
+
let braceDepth = 0; // Track {} for objects
|
|
41
|
+
let inString = false; // Track if we're inside a string
|
|
42
|
+
let messagesEnd = messagesStart;
|
|
43
|
+
for (let i = messagesStart; i < rawBodyStr.length && bracketDepth > 0; i++) {
|
|
44
|
+
const char = rawBodyStr[i];
|
|
45
|
+
const prevChar = i > 0 ? rawBodyStr[i - 1] : '';
|
|
46
|
+
// Handle string boundaries (skip escaped quotes)
|
|
47
|
+
if (char === '"' && prevChar !== '\\') {
|
|
48
|
+
inString = !inString;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Skip everything inside strings
|
|
52
|
+
if (inString)
|
|
53
|
+
continue;
|
|
54
|
+
// Track brackets and braces
|
|
55
|
+
if (char === '[')
|
|
56
|
+
bracketDepth++;
|
|
57
|
+
else if (char === ']')
|
|
58
|
+
bracketDepth--;
|
|
59
|
+
else if (char === '{')
|
|
60
|
+
braceDepth++;
|
|
61
|
+
else if (char === '}')
|
|
62
|
+
braceDepth--;
|
|
63
|
+
// Found the closing bracket of messages array
|
|
64
|
+
if (bracketDepth === 0) {
|
|
65
|
+
messagesEnd = i;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Safety check: did we find the end?
|
|
70
|
+
if (bracketDepth !== 0) {
|
|
71
|
+
throw new Error(`Could not find closing bracket of messages array (depth=${bracketDepth})`);
|
|
72
|
+
}
|
|
73
|
+
// Check if array has content (anything between messagesStart and messagesEnd)
|
|
74
|
+
const arrayContent = rawBodyStr.slice(messagesStart, messagesEnd).trim();
|
|
75
|
+
const messagesIsEmpty = arrayContent.length === 0;
|
|
76
|
+
// Insert minimal user message before closing bracket
|
|
77
|
+
const keepAliveMsg = messagesIsEmpty
|
|
78
|
+
? '{"role":"user","content":"."}'
|
|
79
|
+
: ',{"role":"user","content":"."}';
|
|
80
|
+
log(`Extended cache: SEND keep-alive project=${projectName} msg_array_size=${messagesEnd - messagesStart}`);
|
|
81
|
+
rawBodyStr = rawBodyStr.slice(0, messagesEnd) + keepAliveMsg + rawBodyStr.slice(messagesEnd);
|
|
82
|
+
// NOTE: We do NOT modify max_tokens or stream!
|
|
83
|
+
// Keeping them identical preserves the cache prefix for byte-exact matching.
|
|
84
|
+
// Claude will respond briefly to "." anyway, and forwarder handles streaming.
|
|
85
|
+
// 2. Validate JSON after manipulation
|
|
86
|
+
try {
|
|
87
|
+
JSON.parse(rawBodyStr);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
throw new Error(`Invalid JSON after modifications: ${e instanceof Error ? e.message : 'unknown'}`);
|
|
91
|
+
}
|
|
92
|
+
// 3. Forward to Anthropic using same undici path as regular requests
|
|
93
|
+
const result = await forwardToAnthropic({}, entry.headers, undefined, Buffer.from(rawBodyStr, 'utf-8'));
|
|
94
|
+
if (result.statusCode !== 200) {
|
|
95
|
+
throw new Error(`Keep-alive failed: ${result.statusCode}`);
|
|
96
|
+
}
|
|
97
|
+
// Log cache metrics
|
|
98
|
+
const usage = result.body.usage;
|
|
99
|
+
const cacheRead = usage?.cache_read_input_tokens || 0;
|
|
100
|
+
const cacheCreate = usage?.cache_creation_input_tokens || 0;
|
|
101
|
+
const inputTokens = usage?.input_tokens || 0;
|
|
102
|
+
log(`Extended cache: keep-alive for ${projectName} - cache_read=${cacheRead}, cache_create=${cacheCreate}, input=${inputTokens}`);
|
|
103
|
+
}
|
|
104
|
+
export async function checkExtendedCache() {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const projectsToKeepAlive = [];
|
|
107
|
+
// First pass: cleanup stale/maxed entries, collect projects needing keep-alive
|
|
108
|
+
for (const [projectPath, entry] of extendedCache) {
|
|
109
|
+
const idleTime = now - entry.timestamp;
|
|
110
|
+
const projectName = projectPath.split('/').pop() || projectPath;
|
|
111
|
+
// Stale cleanup: user left after 10 minutes
|
|
112
|
+
if (idleTime > EXTENDED_CACHE_MAX_IDLE) {
|
|
113
|
+
extendedCache.delete(projectPath);
|
|
114
|
+
log(`Extended cache: cleared ${projectName} (stale)`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Skip if not idle enough yet
|
|
118
|
+
if (idleTime < EXTENDED_CACHE_IDLE_THRESHOLD) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Skip if already sent max keep-alives
|
|
122
|
+
if (entry.keepAliveCount >= EXTENDED_CACHE_MAX_KEEPALIVES) {
|
|
123
|
+
extendedCache.delete(projectPath);
|
|
124
|
+
log(`Extended cache: cleared ${projectName} (max retries)`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
projectsToKeepAlive.push({ projectPath, entry });
|
|
128
|
+
}
|
|
129
|
+
// Second pass: send all keep-alives in PARALLEL
|
|
130
|
+
const keepAlivePromises = [];
|
|
131
|
+
for (const { projectPath, entry } of projectsToKeepAlive) {
|
|
132
|
+
const projectName = projectPath.split('/').pop() || projectPath;
|
|
133
|
+
const promise = sendExtendedCacheKeepAlive(projectPath, entry)
|
|
134
|
+
.then(() => {
|
|
135
|
+
entry.timestamp = Date.now();
|
|
136
|
+
entry.keepAliveCount++;
|
|
137
|
+
})
|
|
138
|
+
.catch((err) => {
|
|
139
|
+
extendedCache.delete(projectPath);
|
|
140
|
+
// Handle both Error instances and ForwardError objects
|
|
141
|
+
const errMsg = err instanceof Error
|
|
142
|
+
? err.message
|
|
143
|
+
: (err && typeof err === 'object' && 'message' in err)
|
|
144
|
+
? String(err.message)
|
|
145
|
+
: JSON.stringify(err);
|
|
146
|
+
const errType = err && typeof err === 'object' && 'type' in err ? ` [${err.type}]` : '';
|
|
147
|
+
log(`Extended cache: cleared ${projectName} (error${errType}: ${errMsg})`);
|
|
148
|
+
});
|
|
149
|
+
keepAlivePromises.push(promise);
|
|
150
|
+
}
|
|
151
|
+
// Wait for all keep-alives to complete
|
|
152
|
+
if (keepAlivePromises.length > 0) {
|
|
153
|
+
await Promise.all(keepAlivePromises);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MessagesRequestBody } from '../types.js';
|
|
2
|
+
export declare function getPendingPlanClear(): {
|
|
3
|
+
projectPath: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
} | null;
|
|
6
|
+
export declare function setPendingPlanClear(value: {
|
|
7
|
+
projectPath: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
}): void;
|
|
10
|
+
export declare function clearPendingPlan(): void;
|
|
11
|
+
export declare function preProcessRequest(body: MessagesRequestBody, sessionInfo: {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
promptCount: number;
|
|
14
|
+
projectPath: string;
|
|
15
|
+
}, logger: {
|
|
16
|
+
info: (data: Record<string, unknown>) => void;
|
|
17
|
+
}, detectRequestType: (messages: Array<{
|
|
18
|
+
role: string;
|
|
19
|
+
content: unknown;
|
|
20
|
+
}>, projectPath: string) => 'first' | 'continuation' | 'retry'): Promise<MessagesRequestBody>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Pre-process requests before forwarding to Anthropic
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
import { extractLastUserPrompt, extractFilesFromMessages, buildTeamMemoryContextCloud } from '../request-processor.js';
|
|
4
|
+
import { getSessionState, updateSessionState, markCleared, } from '../../lib/store.js';
|
|
5
|
+
import { isSyncEnabled, getSyncTeamId } from '../../lib/cloud-sync.js';
|
|
6
|
+
import { globalTeamMemoryCache, setTeamMemoryCache, invalidateTeamMemoryCache } from '../cache.js';
|
|
7
|
+
import { buildDynamicInjection, clearSessionTracking } from '../injection/delta-tracking.js';
|
|
8
|
+
import { appendToSystemPrompt } from '../injection/injectors.js';
|
|
9
|
+
// Pending plan summary state - triggers CLEAR-like reset after planning task completes
|
|
10
|
+
let pendingPlanClear = null;
|
|
11
|
+
export function getPendingPlanClear() {
|
|
12
|
+
return pendingPlanClear;
|
|
13
|
+
}
|
|
14
|
+
export function setPendingPlanClear(value) {
|
|
15
|
+
pendingPlanClear = value;
|
|
16
|
+
}
|
|
17
|
+
export function clearPendingPlan() {
|
|
18
|
+
pendingPlanClear = null;
|
|
19
|
+
}
|
|
20
|
+
export async function preProcessRequest(body, sessionInfo, logger, detectRequestType) {
|
|
21
|
+
const modified = { ...body };
|
|
22
|
+
// Skip warmup requests - Claude Code sends "Warmup" as health check
|
|
23
|
+
// No need to do semantic search or cache operations for these
|
|
24
|
+
const earlyUserPrompt = extractLastUserPrompt(modified.messages || []);
|
|
25
|
+
if (earlyUserPrompt === 'Warmup') {
|
|
26
|
+
console.log('[INJECT] Skipping warmup request (no search, no cache)');
|
|
27
|
+
return modified;
|
|
28
|
+
}
|
|
29
|
+
// Detect request type: first, continuation, or retry
|
|
30
|
+
const requestType = detectRequestType(modified.messages || [], sessionInfo.projectPath);
|
|
31
|
+
// === NEW ARCHITECTURE: Separate static and dynamic injection ===
|
|
32
|
+
//
|
|
33
|
+
// STATIC (system prompt, cached):
|
|
34
|
+
// - Team memory from PAST sessions only
|
|
35
|
+
// - CLEAR summary when triggered
|
|
36
|
+
// -> Uses __grovInjection + injectIntoRawBody()
|
|
37
|
+
//
|
|
38
|
+
// DYNAMIC (user message, delta only):
|
|
39
|
+
// - Files edited in current session
|
|
40
|
+
// - Key decisions with reasoning
|
|
41
|
+
// - Drift correction, forced recovery
|
|
42
|
+
// -> Uses __grovUserMsgInjection + appendToLastUserMessage()
|
|
43
|
+
// Get session state
|
|
44
|
+
const sessionState = getSessionState(sessionInfo.sessionId);
|
|
45
|
+
// === PLANNING CLEAR: Reset after planning task completes ===
|
|
46
|
+
// This ensures implementation phase starts fresh with planning context from team memory
|
|
47
|
+
if (pendingPlanClear && pendingPlanClear.projectPath === sessionInfo.projectPath) {
|
|
48
|
+
// 1. Empty messages array (fresh start)
|
|
49
|
+
modified.messages = [];
|
|
50
|
+
// 2. Inject planning summary into system prompt
|
|
51
|
+
appendToSystemPrompt(modified, pendingPlanClear.summary);
|
|
52
|
+
// 3. Rebuild team memory NOW (includes the just-saved planning task)
|
|
53
|
+
const mentionedFiles = extractFilesFromMessages(modified.messages || []);
|
|
54
|
+
const userPrompt = extractLastUserPrompt(modified.messages || []);
|
|
55
|
+
// Use cloud-first approach if sync is enabled
|
|
56
|
+
let teamContext = null;
|
|
57
|
+
const teamId = getSyncTeamId();
|
|
58
|
+
if (isSyncEnabled() && teamId) {
|
|
59
|
+
console.log(`[INJECT] PLANNING_CLEAR: Using cloud team memory (teamId=${teamId.substring(0, 8)}...)`);
|
|
60
|
+
teamContext = await buildTeamMemoryContextCloud(teamId, sessionInfo.projectPath, mentionedFiles, userPrompt // For hybrid semantic search
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Sync not enabled - no injection (cloud-first approach)
|
|
65
|
+
console.log('[INJECT] Sync not enabled. Enable sync for team memory injection.');
|
|
66
|
+
teamContext = null;
|
|
67
|
+
}
|
|
68
|
+
if (teamContext) {
|
|
69
|
+
modified.__grovInjection = teamContext;
|
|
70
|
+
modified.__grovInjectionCached = false;
|
|
71
|
+
// Update cache with fresh team memory
|
|
72
|
+
setTeamMemoryCache(sessionInfo.projectPath, teamContext);
|
|
73
|
+
}
|
|
74
|
+
// 4. Clear the pending plan (one-time use)
|
|
75
|
+
pendingPlanClear = null;
|
|
76
|
+
// 5. Clear tracking (fresh start)
|
|
77
|
+
clearSessionTracking(sessionInfo.sessionId);
|
|
78
|
+
return modified; // Skip other injections - this is a complete reset
|
|
79
|
+
}
|
|
80
|
+
// === CLEAR MODE (100% threshold) ===
|
|
81
|
+
// If token count exceeds threshold AND we have a pre-computed summary, apply CLEAR
|
|
82
|
+
if (sessionState) {
|
|
83
|
+
const currentTokenCount = sessionState.token_count || 0;
|
|
84
|
+
if (currentTokenCount > config.TOKEN_CLEAR_THRESHOLD &&
|
|
85
|
+
sessionState.pending_clear_summary) {
|
|
86
|
+
logger.info({
|
|
87
|
+
msg: 'CLEAR MODE ACTIVATED - resetting conversation',
|
|
88
|
+
tokenCount: currentTokenCount,
|
|
89
|
+
threshold: config.TOKEN_CLEAR_THRESHOLD,
|
|
90
|
+
summaryLength: sessionState.pending_clear_summary.length,
|
|
91
|
+
});
|
|
92
|
+
// 1. Empty messages array (fundamental reset)
|
|
93
|
+
modified.messages = [];
|
|
94
|
+
// 2. Inject summary into system prompt (this will cause cache miss - intentional)
|
|
95
|
+
appendToSystemPrompt(modified, sessionState.pending_clear_summary);
|
|
96
|
+
// 3. Mark session as cleared
|
|
97
|
+
markCleared(sessionInfo.sessionId);
|
|
98
|
+
// 4. Clear pending summary and invalidate GLOBAL team memory cache (new baseline)
|
|
99
|
+
updateSessionState(sessionInfo.sessionId, { pending_clear_summary: undefined });
|
|
100
|
+
invalidateTeamMemoryCache(); // Force recalculation on next request (CLEAR mode)
|
|
101
|
+
// 5. Clear tracking (fresh start after CLEAR)
|
|
102
|
+
clearSessionTracking(sessionInfo.sessionId);
|
|
103
|
+
logger.info({ msg: 'CLEAR complete - conversation reset with summary' });
|
|
104
|
+
return modified; // Skip other injections - this is a complete reset
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// === STATIC INJECTION: Team memory (PAST sessions only) ===
|
|
108
|
+
// Cached per session - identical across all requests for cache preservation
|
|
109
|
+
// GLOBAL cache: same team memory for ALL requests (regardless of sessionId changes)
|
|
110
|
+
// Only recalculate on: first request ever, CLEAR/Summary, project change, proxy restart
|
|
111
|
+
const isSameProject = globalTeamMemoryCache?.projectPath === sessionInfo.projectPath;
|
|
112
|
+
if (globalTeamMemoryCache && isSameProject) {
|
|
113
|
+
// Reuse GLOBAL cached team memory (constant for entire conversation)
|
|
114
|
+
modified.__grovInjection = globalTeamMemoryCache.content;
|
|
115
|
+
modified.__grovInjectionCached = true;
|
|
116
|
+
console.log(`[CACHE] Using global team memory cache, size=${globalTeamMemoryCache.content.length}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// First request OR project changed OR cache was invalidated: compute team memory
|
|
120
|
+
const mentionedFiles = extractFilesFromMessages(modified.messages || []);
|
|
121
|
+
const userPrompt = extractLastUserPrompt(modified.messages || []);
|
|
122
|
+
// Use cloud-first approach if sync is enabled
|
|
123
|
+
let teamContext = null;
|
|
124
|
+
const teamId = getSyncTeamId();
|
|
125
|
+
if (isSyncEnabled() && teamId) {
|
|
126
|
+
console.log(`[INJECT] First/cache miss: Using cloud team memory (teamId=${teamId.substring(0, 8)}...)`);
|
|
127
|
+
teamContext = await buildTeamMemoryContextCloud(teamId, sessionInfo.projectPath, mentionedFiles, userPrompt // For hybrid semantic search
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Sync not enabled - no injection (cloud-first approach)
|
|
132
|
+
console.log('[INJECT] Sync not enabled. Enable sync for team memory injection.');
|
|
133
|
+
teamContext = null;
|
|
134
|
+
}
|
|
135
|
+
console.log(`[CACHE] Computing team memory (first/new), files=${mentionedFiles.length}, result=${teamContext ? teamContext.length : 'null'}`);
|
|
136
|
+
if (teamContext) {
|
|
137
|
+
modified.__grovInjection = teamContext;
|
|
138
|
+
modified.__grovInjectionCached = false;
|
|
139
|
+
// Store in GLOBAL cache - stays constant until CLEAR or restart
|
|
140
|
+
setTeamMemoryCache(sessionInfo.projectPath, teamContext);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// No team memory available - clear global cache for this project
|
|
144
|
+
if (isSameProject) {
|
|
145
|
+
invalidateTeamMemoryCache();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// SKIP dynamic injection for retries and continuations
|
|
150
|
+
if (requestType !== 'first') {
|
|
151
|
+
return modified;
|
|
152
|
+
}
|
|
153
|
+
// === DYNAMIC INJECTION: User message (delta only) ===
|
|
154
|
+
// Includes: edited files, key decisions, drift correction, forced recovery
|
|
155
|
+
// This goes into the LAST user message, not system prompt
|
|
156
|
+
const dynamicInjection = buildDynamicInjection(sessionInfo.sessionId, sessionState, logger);
|
|
157
|
+
if (dynamicInjection) {
|
|
158
|
+
modified.__grovUserMsgInjection = dynamicInjection;
|
|
159
|
+
logger.info({ msg: 'Dynamic injection ready for user message', size: dynamicInjection.length });
|
|
160
|
+
// Clear pending corrections after building injection
|
|
161
|
+
if (sessionState?.pending_correction || sessionState?.pending_forced_recovery) {
|
|
162
|
+
updateSessionState(sessionInfo.sessionId, {
|
|
163
|
+
pending_correction: undefined,
|
|
164
|
+
pending_forced_recovery: undefined,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return modified;
|
|
169
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionState } from '../../lib/store.js';
|
|
2
|
+
export interface SessionInjectionTracking {
|
|
3
|
+
files: Set<string>;
|
|
4
|
+
decisionIds: Set<string>;
|
|
5
|
+
reasonings: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function getOrCreateTracking(sessionId: string): SessionInjectionTracking;
|
|
8
|
+
export declare function clearSessionTracking(sessionId: string): void;
|
|
9
|
+
export declare function buildDynamicInjection(sessionId: string, sessionState: SessionState | null, logger?: {
|
|
10
|
+
info: (data: Record<string, unknown>) => void;
|
|
11
|
+
}): string | null;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Delta tracking - avoid duplicate injections across requests
|
|
2
|
+
import { getEditedFiles, getKeyDecisions } from '../../lib/store.js';
|
|
3
|
+
import { smartTruncate } from '../../lib/utils.js';
|
|
4
|
+
const sessionInjectionTracking = new Map();
|
|
5
|
+
export function getOrCreateTracking(sessionId) {
|
|
6
|
+
if (!sessionInjectionTracking.has(sessionId)) {
|
|
7
|
+
sessionInjectionTracking.set(sessionId, {
|
|
8
|
+
files: new Set(),
|
|
9
|
+
decisionIds: new Set(),
|
|
10
|
+
reasonings: new Set(),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return sessionInjectionTracking.get(sessionId);
|
|
14
|
+
}
|
|
15
|
+
export function clearSessionTracking(sessionId) {
|
|
16
|
+
sessionInjectionTracking.delete(sessionId);
|
|
17
|
+
}
|
|
18
|
+
export function buildDynamicInjection(sessionId, sessionState, logger) {
|
|
19
|
+
const tracking = getOrCreateTracking(sessionId);
|
|
20
|
+
const parts = [];
|
|
21
|
+
const debugInfo = {};
|
|
22
|
+
// 1. Get edited files (delta - not already injected)
|
|
23
|
+
const allEditedFiles = getEditedFiles(sessionId);
|
|
24
|
+
const newFiles = allEditedFiles.filter(f => !tracking.files.has(f));
|
|
25
|
+
debugInfo.totalEditedFiles = allEditedFiles.length;
|
|
26
|
+
debugInfo.newEditedFiles = newFiles.length;
|
|
27
|
+
debugInfo.alreadyTrackedFiles = tracking.files.size;
|
|
28
|
+
if (newFiles.length > 0) {
|
|
29
|
+
// Track and add to injection
|
|
30
|
+
newFiles.forEach(f => tracking.files.add(f));
|
|
31
|
+
const fileNames = newFiles.slice(0, 5).map(f => f.split('/').pop());
|
|
32
|
+
parts.push(`[EDITED: ${fileNames.join(', ')}]`);
|
|
33
|
+
debugInfo.editedFilesInjected = fileNames;
|
|
34
|
+
}
|
|
35
|
+
// 2. Get key decisions with reasoning (delta - not already injected)
|
|
36
|
+
const keyDecisions = getKeyDecisions(sessionId, 5);
|
|
37
|
+
debugInfo.totalKeyDecisions = keyDecisions.length;
|
|
38
|
+
debugInfo.alreadyTrackedDecisions = tracking.decisionIds.size;
|
|
39
|
+
const newDecisions = keyDecisions.filter(d => !tracking.decisionIds.has(d.id) &&
|
|
40
|
+
d.reasoning &&
|
|
41
|
+
!tracking.reasonings.has(d.reasoning));
|
|
42
|
+
debugInfo.newKeyDecisions = newDecisions.length;
|
|
43
|
+
for (const decision of newDecisions.slice(0, 3)) {
|
|
44
|
+
tracking.decisionIds.add(decision.id);
|
|
45
|
+
tracking.reasonings.add(decision.reasoning);
|
|
46
|
+
const truncated = smartTruncate(decision.reasoning, 120);
|
|
47
|
+
parts.push(`[DECISION: ${truncated}]`);
|
|
48
|
+
// Log the original and truncated reasoning for debugging
|
|
49
|
+
if (logger) {
|
|
50
|
+
logger.info({
|
|
51
|
+
msg: 'Key decision reasoning extracted',
|
|
52
|
+
originalLength: decision.reasoning.length,
|
|
53
|
+
truncatedLength: truncated.length,
|
|
54
|
+
original: decision.reasoning.substring(0, 200) + (decision.reasoning.length > 200 ? '...' : ''),
|
|
55
|
+
truncated,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
debugInfo.decisionsInjected = newDecisions.slice(0, 3).length;
|
|
60
|
+
// 3. Add drift correction if pending
|
|
61
|
+
if (sessionState?.pending_correction) {
|
|
62
|
+
parts.push(`[DRIFT: ${sessionState.pending_correction}]`);
|
|
63
|
+
debugInfo.hasDriftCorrection = true;
|
|
64
|
+
debugInfo.driftCorrectionLength = sessionState.pending_correction.length;
|
|
65
|
+
}
|
|
66
|
+
// 4. Add forced recovery if pending
|
|
67
|
+
if (sessionState?.pending_forced_recovery) {
|
|
68
|
+
parts.push(`[RECOVERY: ${sessionState.pending_forced_recovery}]`);
|
|
69
|
+
debugInfo.hasForcedRecovery = true;
|
|
70
|
+
debugInfo.forcedRecoveryLength = sessionState.pending_forced_recovery.length;
|
|
71
|
+
}
|
|
72
|
+
// Log debug info
|
|
73
|
+
if (logger) {
|
|
74
|
+
logger.info({
|
|
75
|
+
msg: 'Dynamic injection build details',
|
|
76
|
+
...debugInfo,
|
|
77
|
+
partsCount: parts.length,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (parts.length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const injection = '---\n[GROV CONTEXT]\n' + parts.join('\n');
|
|
84
|
+
// Log final injection content
|
|
85
|
+
if (logger) {
|
|
86
|
+
logger.info({
|
|
87
|
+
msg: 'Dynamic injection content',
|
|
88
|
+
size: injection.length,
|
|
89
|
+
content: injection,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return injection;
|
|
93
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MessagesRequestBody } from '../types.js';
|
|
2
|
+
export declare function appendToLastUserMessage(rawBody: string, injection: string): string;
|
|
3
|
+
export declare function appendToSystemPrompt(body: MessagesRequestBody, textToAppend: string): void;
|
|
4
|
+
export declare function injectIntoRawBody(rawBody: string, injectionText: string): {
|
|
5
|
+
modified: string;
|
|
6
|
+
success: boolean;
|
|
7
|
+
};
|