grov 0.6.16 → 0.6.18
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/core/cloud/api-client.d.ts +2 -1
- package/dist/core/cloud/api-client.js +16 -0
- package/dist/core/cloud/cloud-sync.d.ts +2 -8
- package/dist/core/cloud/cloud-sync.js +12 -16
- package/dist/integrations/proxy/handlers/preprocess.js +17 -0
- package/dist/integrations/proxy/orchestrator.js +22 -0
- package/dist/integrations/proxy/utils/usage-warnings.d.ts +2 -0
- package/dist/integrations/proxy/utils/usage-warnings.js +40 -0
- package/package.json +2 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Team, Memory, MemorySyncRequest, MemorySyncResponse, DeviceFlowStartResponse, DeviceFlowPollResponse, ReasoningTraceEntry } from '@grov/shared';
|
|
1
|
+
import type { Team, Memory, MemorySyncRequest, MemorySyncResponse, DeviceFlowStartResponse, DeviceFlowPollResponse, ReasoningTraceEntry, RecordInjectionRequest, RecordInjectionResponse } from '@grov/shared';
|
|
2
2
|
export interface ApiResponse<T> {
|
|
3
3
|
data?: T;
|
|
4
4
|
error?: string;
|
|
@@ -88,6 +88,7 @@ export interface MatchResponse {
|
|
|
88
88
|
* @returns Match response with memory and score
|
|
89
89
|
*/
|
|
90
90
|
export declare function fetchMatch(teamId: string, data: MatchInput): Promise<MatchResponse>;
|
|
91
|
+
export declare function reportInjection(event: RecordInjectionRequest): Promise<RecordInjectionResponse | null>;
|
|
91
92
|
/**
|
|
92
93
|
* Sleep helper for polling
|
|
93
94
|
*/
|
|
@@ -148,6 +148,12 @@ export async function fetchTeamMemories(teamId, projectPath, options) {
|
|
|
148
148
|
if (!response.data || !response.data.memories) {
|
|
149
149
|
return [];
|
|
150
150
|
}
|
|
151
|
+
// Check if blocked due to quota
|
|
152
|
+
if (response.data.blocked) {
|
|
153
|
+
console.log('\n[grov] \x1b[33m⚠️ Free quota exceeded (110%). Upgrade to continue using memory injection.\x1b[0m');
|
|
154
|
+
console.log('[grov] Manage plan: https://app.grov.dev/settings\n');
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
151
157
|
return response.data.memories;
|
|
152
158
|
}
|
|
153
159
|
catch (err) {
|
|
@@ -186,6 +192,16 @@ export async function fetchMatch(teamId, data) {
|
|
|
186
192
|
return { match: null };
|
|
187
193
|
}
|
|
188
194
|
}
|
|
195
|
+
// ============= Usage Tracking =============
|
|
196
|
+
export async function reportInjection(event) {
|
|
197
|
+
try {
|
|
198
|
+
const response = await apiRequest('POST', '/usage/injection', event);
|
|
199
|
+
return response.data || null;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
189
205
|
// ============= Utility Functions =============
|
|
190
206
|
/**
|
|
191
207
|
* Sleep helper for polling
|
|
@@ -36,18 +36,12 @@ export interface UpdateMemoryInput extends CreateMemoryInput {
|
|
|
36
36
|
/**
|
|
37
37
|
* Convert local Task to CreateMemoryInput for API
|
|
38
38
|
*/
|
|
39
|
-
export declare function taskToMemory(task: Task): CreateMemoryInput;
|
|
39
|
+
export declare function taskToMemory(task: Task, taskType?: 'information' | 'planning' | 'implementation'): CreateMemoryInput;
|
|
40
40
|
/**
|
|
41
41
|
* Prepare sync payload for UPDATE path
|
|
42
42
|
* Merges existing memory with new data based on shouldUpdateMemory result
|
|
43
|
-
*
|
|
44
|
-
* @param existingMemory - The memory that was matched
|
|
45
|
-
* @param newData - Extracted reasoning and decisions from current session
|
|
46
|
-
* @param updateResult - Result from shouldUpdateMemory Haiku call
|
|
47
|
-
* @param task - The current task being synced
|
|
48
|
-
* @returns Payload ready for sync with memory_id for UPDATE
|
|
49
43
|
*/
|
|
50
|
-
export declare function prepareSyncPayload(existingMemory: Memory, newData: ExtractedReasoningAndDecisions, updateResult: ShouldUpdateResult, task: Task): UpdateMemoryInput;
|
|
44
|
+
export declare function prepareSyncPayload(existingMemory: Memory, newData: ExtractedReasoningAndDecisions, updateResult: ShouldUpdateResult, task: Task, taskType?: 'information' | 'planning' | 'implementation'): UpdateMemoryInput;
|
|
51
45
|
/**
|
|
52
46
|
* Check if sync is enabled and configured
|
|
53
47
|
*/
|
|
@@ -12,13 +12,13 @@ const SYNC_CONFIG = {
|
|
|
12
12
|
/**
|
|
13
13
|
* Convert local Task to CreateMemoryInput for API
|
|
14
14
|
*/
|
|
15
|
-
export function taskToMemory(task) {
|
|
15
|
+
export function taskToMemory(task, taskType) {
|
|
16
16
|
return {
|
|
17
17
|
client_task_id: task.id,
|
|
18
18
|
project_path: task.project_path,
|
|
19
19
|
original_query: task.original_query,
|
|
20
20
|
goal: task.goal,
|
|
21
|
-
system_name: task.system_name,
|
|
21
|
+
system_name: task.system_name,
|
|
22
22
|
summary: task.summary,
|
|
23
23
|
reasoning_trace: task.reasoning_trace,
|
|
24
24
|
files_touched: task.files_touched,
|
|
@@ -26,6 +26,7 @@ export function taskToMemory(task) {
|
|
|
26
26
|
constraints: task.constraints,
|
|
27
27
|
tags: task.tags,
|
|
28
28
|
status: task.status,
|
|
29
|
+
task_type: taskType,
|
|
29
30
|
linked_commit: task.linked_commit,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -38,14 +39,8 @@ function getToday() {
|
|
|
38
39
|
/**
|
|
39
40
|
* Prepare sync payload for UPDATE path
|
|
40
41
|
* Merges existing memory with new data based on shouldUpdateMemory result
|
|
41
|
-
*
|
|
42
|
-
* @param existingMemory - The memory that was matched
|
|
43
|
-
* @param newData - Extracted reasoning and decisions from current session
|
|
44
|
-
* @param updateResult - Result from shouldUpdateMemory Haiku call
|
|
45
|
-
* @param task - The current task being synced
|
|
46
|
-
* @returns Payload ready for sync with memory_id for UPDATE
|
|
47
42
|
*/
|
|
48
|
-
export function prepareSyncPayload(existingMemory, newData, updateResult, task) {
|
|
43
|
+
export function prepareSyncPayload(existingMemory, newData, updateResult, task, taskType) {
|
|
49
44
|
const today = getToday();
|
|
50
45
|
// 1. Get existing decisions with proper typing
|
|
51
46
|
const existingDecisions = (existingMemory.decisions || []);
|
|
@@ -109,18 +104,19 @@ export function prepareSyncPayload(existingMemory, newData, updateResult, task)
|
|
|
109
104
|
const finalReasoningEvolution = reasoningEvolution.slice(-MAX_REASONING_EVOLUTION);
|
|
110
105
|
// 8. Build final payload
|
|
111
106
|
return {
|
|
112
|
-
memory_id: existingMemory.id,
|
|
107
|
+
memory_id: existingMemory.id,
|
|
113
108
|
client_task_id: task.id,
|
|
114
109
|
project_path: task.project_path,
|
|
115
110
|
original_query: task.original_query,
|
|
116
111
|
goal: task.goal,
|
|
117
|
-
system_name: newData.system_name || task.system_name,
|
|
118
|
-
reasoning_trace: newData.reasoning_trace,
|
|
112
|
+
system_name: newData.system_name || task.system_name,
|
|
113
|
+
reasoning_trace: newData.reasoning_trace,
|
|
119
114
|
files_touched: task.files_touched,
|
|
120
115
|
decisions: finalDecisions,
|
|
121
116
|
constraints: task.constraints,
|
|
122
117
|
tags: task.tags,
|
|
123
118
|
status: task.status,
|
|
119
|
+
task_type: taskType,
|
|
124
120
|
linked_commit: task.linked_commit,
|
|
125
121
|
evolution_steps: finalEvolutionSteps,
|
|
126
122
|
reasoning_evolution: finalReasoningEvolution,
|
|
@@ -188,7 +184,7 @@ export async function syncTask(task, extractedData, taskType, headers) {
|
|
|
188
184
|
});
|
|
189
185
|
// Step 2: If no match, INSERT as new memory
|
|
190
186
|
if (!matchResult.match) {
|
|
191
|
-
const memory = taskToMemory(task);
|
|
187
|
+
const memory = taskToMemory(task, taskType);
|
|
192
188
|
const result = await syncMemories(teamId, { memories: [memory] });
|
|
193
189
|
console.log(`[SYNC TO CLOUD] ${taskId} -> INSERT (${taskType || 'unknown'})`);
|
|
194
190
|
return result.synced === 1;
|
|
@@ -197,7 +193,7 @@ export async function syncTask(task, extractedData, taskType, headers) {
|
|
|
197
193
|
const score = matchResult.combined_score?.toFixed(3) || '-';
|
|
198
194
|
// If no extracted data, INSERT anyway
|
|
199
195
|
if (!effectiveExtractedData) {
|
|
200
|
-
const memory = taskToMemory(task);
|
|
196
|
+
const memory = taskToMemory(task, taskType);
|
|
201
197
|
const result = await syncMemories(teamId, { memories: [memory] });
|
|
202
198
|
console.log(`[SYNC TO CLOUD] ${taskId} -> INSERT (${taskType || 'unknown'})`);
|
|
203
199
|
return result.synced === 1;
|
|
@@ -223,7 +219,7 @@ export async function syncTask(task, extractedData, taskType, headers) {
|
|
|
223
219
|
return true;
|
|
224
220
|
}
|
|
225
221
|
// Prepare payload for UPDATE
|
|
226
|
-
const payload = prepareSyncPayload(matchResult.match, effectiveExtractedData, updateResult, task);
|
|
222
|
+
const payload = prepareSyncPayload(matchResult.match, effectiveExtractedData, updateResult, task, taskType);
|
|
227
223
|
// Sync with memory_id for UPDATE path
|
|
228
224
|
const result = await syncMemories(teamId, { memories: [payload] });
|
|
229
225
|
console.log(`[SYNC TO CLOUD] ${taskId} -> UPDATE (${taskType || 'unknown'}) [matched: ${matchedId}]`);
|
|
@@ -269,7 +265,7 @@ export async function syncTasks(tasks) {
|
|
|
269
265
|
};
|
|
270
266
|
}
|
|
271
267
|
// Convert tasks to memories
|
|
272
|
-
const memories = tasks.map(taskToMemory);
|
|
268
|
+
const memories = tasks.map(t => taskToMemory(t));
|
|
273
269
|
// Batch and sync
|
|
274
270
|
const batches = [];
|
|
275
271
|
for (let i = 0; i < memories.length; i += SYNC_CONFIG.batchSize) {
|
|
@@ -4,7 +4,10 @@ import { extractFilesFromMessages } from '../request-processor.js';
|
|
|
4
4
|
import { fetchTeamMemories } from '../../../core/cloud/api-client.js';
|
|
5
5
|
import { getSessionState, updateSessionState, markCleared, } from '../../../core/store/store.js';
|
|
6
6
|
import { isSyncEnabled, getSyncTeamId } from '../../../core/cloud/cloud-sync.js';
|
|
7
|
+
import { getCurrentUser } from '../../../core/cloud/credentials.js';
|
|
8
|
+
import { reportInjection } from '../../../core/cloud/api-client.js';
|
|
7
9
|
import { clearSessionState, cacheMemories, buildMemoryPreview, buildToolDescription, buildDriftRecoveryInjection, reconstructMessages, addInjectionRecord, commitPendingRecords, setCachedPreview, getCachedPreview, } from '../injection/memory-injection.js';
|
|
10
|
+
import { handleInjectionResponse } from '../utils/usage-warnings.js';
|
|
8
11
|
let pendingPlanClear = null;
|
|
9
12
|
export function getPendingPlanClear() {
|
|
10
13
|
return pendingPlanClear;
|
|
@@ -141,6 +144,20 @@ export async function preProcessRequest(adapter, body, sessionInfo, logger, dete
|
|
|
141
144
|
previewSize: preview?.length || 0,
|
|
142
145
|
hasDriftRecovery: !!driftRecovery,
|
|
143
146
|
});
|
|
147
|
+
const user = getCurrentUser();
|
|
148
|
+
if (user && teamId) {
|
|
149
|
+
reportInjection({
|
|
150
|
+
team_id: teamId,
|
|
151
|
+
user_id: user.id,
|
|
152
|
+
session_id: sessionInfo.sessionId,
|
|
153
|
+
event_id: `${sessionInfo.sessionId}:${Date.now()}:preview`,
|
|
154
|
+
injection_type: 'preview',
|
|
155
|
+
memory_ids: memories.map(m => m.id),
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
})
|
|
158
|
+
.then(res => handleInjectionResponse(res, teamId))
|
|
159
|
+
.catch(() => { });
|
|
160
|
+
}
|
|
144
161
|
}
|
|
145
162
|
else {
|
|
146
163
|
// No memories found - inject explicit "no entries" so Claude doesn't use old previews
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Handles session management, task orchestration, drift detection, and memory injection
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { config, buildSafeHeaders } from './config.js';
|
|
5
|
+
import { getSyncTeamId } from '../../core/cloud/cloud-sync.js';
|
|
6
|
+
import { getCurrentUser } from '../../core/cloud/credentials.js';
|
|
7
|
+
import { reportInjection } from '../../core/cloud/api-client.js';
|
|
5
8
|
import { extendedCache, evictOldestCacheEntry } from './cache/extended-cache.js';
|
|
6
9
|
import { getNextRequestId, taskLog, proxyLog, logTokenUsage } from './utils/logging.js';
|
|
7
10
|
import { preProcessRequest, setPendingPlanClear } from './handlers/preprocess.js';
|
|
@@ -11,6 +14,7 @@ import { buildCorrection, formatCorrectionForInjection } from '../../core/extrac
|
|
|
11
14
|
import { generateSessionSummary, analyzeTaskContext, } from '../../core/extraction/llm-extractor.js';
|
|
12
15
|
import { saveToTeamMemory } from './response-processor.js';
|
|
13
16
|
import { getCachedMemoryById, buildExpandedMemory, addInjectionRecord, hasToolCycleAtPosition, } from './injection/memory-injection.js';
|
|
17
|
+
import { handleInjectionResponse } from './utils/usage-warnings.js';
|
|
14
18
|
// In-memory state
|
|
15
19
|
const lastDriftResults = new Map();
|
|
16
20
|
const lastMessageCount = new Map();
|
|
@@ -180,6 +184,24 @@ export async function handleAgentRequest(context) {
|
|
|
180
184
|
if (ids.length > 0) {
|
|
181
185
|
const expandedCount = expandedParts.filter(p => !p.includes('not found')).length;
|
|
182
186
|
console.log(`[MEMORY] Expanded ${expandedCount}/${ids.length} memories`);
|
|
187
|
+
const teamId = getSyncTeamId();
|
|
188
|
+
const user = getCurrentUser();
|
|
189
|
+
if (teamId && user && expandedCount > 0) {
|
|
190
|
+
const expandedIds = ids.filter((_, i) => !expandedParts[i].includes('not found'));
|
|
191
|
+
for (const memoryId of expandedIds) {
|
|
192
|
+
reportInjection({
|
|
193
|
+
team_id: teamId,
|
|
194
|
+
user_id: user.id,
|
|
195
|
+
session_id: sessionInfo.sessionId,
|
|
196
|
+
event_id: `${sessionInfo.sessionId}:${Date.now()}:expand:${memoryId}`,
|
|
197
|
+
injection_type: 'expand',
|
|
198
|
+
memory_ids: [memoryId],
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
})
|
|
201
|
+
.then(res => handleInjectionResponse(res, teamId))
|
|
202
|
+
.catch(() => { });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
183
205
|
}
|
|
184
206
|
grovExpandResult = expandedParts.join('\n\n');
|
|
185
207
|
toolResultBlocks.push({
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Usage warning display for CLI
|
|
2
|
+
// Shows warnings when approaching or exceeding monthly quota
|
|
3
|
+
const SEVERITY = {
|
|
4
|
+
normal: 0,
|
|
5
|
+
warning_80: 1,
|
|
6
|
+
warning_100: 2,
|
|
7
|
+
overage: 3,
|
|
8
|
+
};
|
|
9
|
+
// Cache: teamId -> last warned status (prevents duplicate warnings)
|
|
10
|
+
const warnedStatus = new Map();
|
|
11
|
+
export function handleInjectionResponse(response, teamId) {
|
|
12
|
+
if (!response)
|
|
13
|
+
return;
|
|
14
|
+
// Period reset - clear cache
|
|
15
|
+
if (response.status === 'normal') {
|
|
16
|
+
warnedStatus.delete(teamId);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const lastSeverity = SEVERITY[warnedStatus.get(teamId) ?? 'normal'];
|
|
20
|
+
const currentSeverity = SEVERITY[response.status];
|
|
21
|
+
// Only warn on escalation
|
|
22
|
+
if (currentSeverity <= lastSeverity)
|
|
23
|
+
return;
|
|
24
|
+
warnedStatus.set(teamId, response.status);
|
|
25
|
+
const percent = Math.round((response.current_count / response.quota) * 100);
|
|
26
|
+
const usage = `${response.current_count}/${response.quota}`;
|
|
27
|
+
switch (response.status) {
|
|
28
|
+
case 'warning_80':
|
|
29
|
+
console.log(`[grov] ⚠️ Usage at ${percent}% of monthly quota (${usage} injections)`);
|
|
30
|
+
break;
|
|
31
|
+
case 'warning_100':
|
|
32
|
+
console.log(`[grov] ⚠️ Monthly quota reached (${usage} injections)`);
|
|
33
|
+
console.log(`[grov] Overage charges apply after 110% usage`);
|
|
34
|
+
break;
|
|
35
|
+
case 'overage':
|
|
36
|
+
console.log(`[grov] ⚠️ Overage billing now active (${usage} injections)`);
|
|
37
|
+
console.log(`[grov] Manage usage: https://grov.dev/settings`);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED