grov 0.2.2 → 0.5.2

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.
@@ -0,0 +1,176 @@
1
+ // Cloud sync logic - Upload memories from local database to API
2
+ // Handles batching, retries, and conversion from Task to Memory format
3
+ import { getSyncStatus, getAccessToken } from './credentials.js';
4
+ import { syncMemories, sleep, getApiUrl } from './api-client.js';
5
+ // Sync configuration
6
+ const SYNC_CONFIG = {
7
+ batchSize: 10, // Number of memories per batch
8
+ retryAttempts: 3, // Number of retry attempts per batch
9
+ retryDelay: 1000, // Base delay between retries (ms)
10
+ };
11
+ /**
12
+ * Convert local Task to CreateMemoryInput for API
13
+ */
14
+ export function taskToMemory(task) {
15
+ return {
16
+ client_task_id: task.id,
17
+ project_path: task.project_path,
18
+ original_query: task.original_query,
19
+ goal: task.goal,
20
+ reasoning_trace: task.reasoning_trace,
21
+ files_touched: task.files_touched,
22
+ decisions: task.decisions,
23
+ constraints: task.constraints,
24
+ tags: task.tags,
25
+ status: task.status,
26
+ linked_commit: task.linked_commit,
27
+ };
28
+ }
29
+ /**
30
+ * Check if sync is enabled and configured
31
+ */
32
+ export function isSyncEnabled() {
33
+ const status = getSyncStatus();
34
+ return status?.enabled === true && !!status.teamId;
35
+ }
36
+ /**
37
+ * Get the configured team ID for sync
38
+ */
39
+ export function getSyncTeamId() {
40
+ const status = getSyncStatus();
41
+ return status?.teamId || null;
42
+ }
43
+ /**
44
+ * Sync a single task to the cloud
45
+ * Called when a task is completed
46
+ */
47
+ export async function syncTask(task) {
48
+ if (!isSyncEnabled()) {
49
+ return false;
50
+ }
51
+ const teamId = getSyncTeamId();
52
+ if (!teamId) {
53
+ return false;
54
+ }
55
+ const token = await getAccessToken();
56
+ if (!token) {
57
+ return false;
58
+ }
59
+ try {
60
+ const memory = taskToMemory(task);
61
+ const result = await syncMemories(teamId, { memories: [memory] });
62
+ return result.synced === 1;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * Sync multiple tasks with batching and retry
70
+ */
71
+ export async function syncTasks(tasks) {
72
+ if (!isSyncEnabled()) {
73
+ return {
74
+ synced: 0,
75
+ failed: tasks.length,
76
+ errors: [`Sync is not enabled. Run "grov sync --enable --team <team-id>" first. (API: ${getApiUrl()})`],
77
+ syncedIds: [],
78
+ failedIds: tasks.map(t => t.id),
79
+ };
80
+ }
81
+ const teamId = getSyncTeamId();
82
+ if (!teamId) {
83
+ return {
84
+ synced: 0,
85
+ failed: tasks.length,
86
+ errors: [`No team configured. Run "grov sync --enable --team <team-id>" first. (API: ${getApiUrl()})`],
87
+ syncedIds: [],
88
+ failedIds: tasks.map(t => t.id),
89
+ };
90
+ }
91
+ const token = await getAccessToken();
92
+ if (!token) {
93
+ return {
94
+ synced: 0,
95
+ failed: tasks.length,
96
+ errors: [`Not authenticated. Run "grov login" first. (API: ${getApiUrl()})`],
97
+ syncedIds: [],
98
+ failedIds: tasks.map(t => t.id),
99
+ };
100
+ }
101
+ // Convert tasks to memories
102
+ const memories = tasks.map(taskToMemory);
103
+ // Batch and sync
104
+ const batches = [];
105
+ for (let i = 0; i < memories.length; i += SYNC_CONFIG.batchSize) {
106
+ batches.push(memories.slice(i, i + SYNC_CONFIG.batchSize));
107
+ }
108
+ let totalSynced = 0;
109
+ let totalFailed = 0;
110
+ const allErrors = [];
111
+ const syncedIds = [];
112
+ const failedIds = [];
113
+ for (const batch of batches) {
114
+ const batchResult = await syncBatchWithRetry(teamId, batch);
115
+ totalSynced += batchResult.synced;
116
+ totalFailed += batchResult.failed;
117
+ if (batchResult.errors) {
118
+ allErrors.push(...batchResult.errors);
119
+ }
120
+ const batchIds = batch.map((m) => m.client_task_id || '');
121
+ if (batchResult.synced === batch.length) {
122
+ syncedIds.push(...batchIds);
123
+ }
124
+ else if (batchResult.failed === batch.length) {
125
+ failedIds.push(...batchIds);
126
+ }
127
+ }
128
+ return {
129
+ synced: totalSynced,
130
+ failed: totalFailed,
131
+ errors: allErrors,
132
+ syncedIds: syncedIds.filter(Boolean),
133
+ failedIds: failedIds.filter(Boolean),
134
+ };
135
+ }
136
+ /**
137
+ * Sync a batch with retry logic
138
+ */
139
+ async function syncBatchWithRetry(teamId, memories) {
140
+ let lastError;
141
+ for (let attempt = 0; attempt < SYNC_CONFIG.retryAttempts; attempt++) {
142
+ try {
143
+ return await syncMemories(teamId, { memories });
144
+ }
145
+ catch (err) {
146
+ lastError = err instanceof Error ? err.message : 'Unknown error';
147
+ // Exponential backoff
148
+ if (attempt < SYNC_CONFIG.retryAttempts - 1) {
149
+ const delay = SYNC_CONFIG.retryDelay * Math.pow(2, attempt);
150
+ await sleep(delay);
151
+ }
152
+ }
153
+ }
154
+ // All retries failed
155
+ return {
156
+ synced: 0,
157
+ failed: memories.length,
158
+ errors: [lastError || 'Sync failed after retries'],
159
+ };
160
+ }
161
+ /**
162
+ * Get sync status summary
163
+ */
164
+ export function getSyncStatusSummary() {
165
+ const status = getSyncStatus();
166
+ if (!status) {
167
+ return 'Not logged in';
168
+ }
169
+ if (!status.enabled) {
170
+ return 'Sync disabled';
171
+ }
172
+ if (!status.teamId) {
173
+ return 'No team configured';
174
+ }
175
+ return `Syncing to team: ${status.teamId}`;
176
+ }
@@ -0,0 +1,53 @@
1
+ export interface Credentials {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_at: string;
5
+ user_id: string;
6
+ email: string;
7
+ team_id?: string;
8
+ sync_enabled: boolean;
9
+ }
10
+ /**
11
+ * Read credentials from disk
12
+ * @returns Credentials or null if not found/invalid
13
+ */
14
+ export declare function readCredentials(): Credentials | null;
15
+ /**
16
+ * Write credentials to disk with secure permissions
17
+ */
18
+ export declare function writeCredentials(creds: Credentials): void;
19
+ /**
20
+ * Clear credentials (logout)
21
+ */
22
+ export declare function clearCredentials(): void;
23
+ /**
24
+ * Check if user is authenticated (has valid credentials)
25
+ */
26
+ export declare function isAuthenticated(): boolean;
27
+ /**
28
+ * Get a valid access token, refreshing if necessary
29
+ * @returns Access token or null if not authenticated
30
+ */
31
+ export declare function getAccessToken(): Promise<string | null>;
32
+ /**
33
+ * Set the team ID for sync
34
+ */
35
+ export declare function setTeamId(teamId: string): void;
36
+ /**
37
+ * Enable or disable sync
38
+ */
39
+ export declare function setSyncEnabled(enabled: boolean): void;
40
+ /**
41
+ * Get current sync status
42
+ */
43
+ export declare function getSyncStatus(): {
44
+ enabled: boolean;
45
+ teamId: string | undefined;
46
+ } | null;
47
+ /**
48
+ * Get current user info
49
+ */
50
+ export declare function getCurrentUser(): {
51
+ id: string;
52
+ email: string;
53
+ } | null;
@@ -0,0 +1,201 @@
1
+ // Secure credential storage for CLI authentication
2
+ // Stores tokens at ~/.grov/credentials.json with 0o600 permissions
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, chmodSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ import { request } from 'undici';
7
+ const GROV_DIR = join(homedir(), '.grov');
8
+ const CREDENTIALS_PATH = join(GROV_DIR, 'credentials.json');
9
+ /**
10
+ * Ensure .grov directory exists with proper permissions
11
+ */
12
+ function ensureGrovDir() {
13
+ if (!existsSync(GROV_DIR)) {
14
+ mkdirSync(GROV_DIR, { recursive: true, mode: 0o700 });
15
+ }
16
+ }
17
+ /**
18
+ * Read credentials from disk
19
+ * @returns Credentials or null if not found/invalid
20
+ */
21
+ export function readCredentials() {
22
+ if (!existsSync(CREDENTIALS_PATH)) {
23
+ return null;
24
+ }
25
+ try {
26
+ const content = readFileSync(CREDENTIALS_PATH, 'utf-8');
27
+ const creds = JSON.parse(content);
28
+ // Validate required fields
29
+ if (!creds.access_token || !creds.refresh_token || !creds.expires_at) {
30
+ return null;
31
+ }
32
+ return creds;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Write credentials to disk with secure permissions
40
+ */
41
+ export function writeCredentials(creds) {
42
+ ensureGrovDir();
43
+ // Write with restrictive permissions (owner read/write only)
44
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
45
+ // Ensure permissions are correct even if file existed
46
+ chmodSync(CREDENTIALS_PATH, 0o600);
47
+ }
48
+ /**
49
+ * Clear credentials (logout)
50
+ */
51
+ export function clearCredentials() {
52
+ if (existsSync(CREDENTIALS_PATH)) {
53
+ unlinkSync(CREDENTIALS_PATH);
54
+ }
55
+ }
56
+ /**
57
+ * Check if user is authenticated (has valid credentials)
58
+ */
59
+ export function isAuthenticated() {
60
+ const creds = readCredentials();
61
+ return creds !== null;
62
+ }
63
+ /**
64
+ * Check if access token is expired or will expire soon (within 5 minutes)
65
+ */
66
+ function isTokenExpiringSoon(expiresAt) {
67
+ const expiryTime = new Date(expiresAt).getTime();
68
+ const bufferTime = 5 * 60 * 1000; // 5 minutes
69
+ return Date.now() > expiryTime - bufferTime;
70
+ }
71
+ /**
72
+ * Refresh tokens using the API
73
+ * @returns New credentials or null if refresh failed
74
+ */
75
+ async function refreshTokens(refreshToken, apiUrl) {
76
+ try {
77
+ const response = await request(`${apiUrl}/auth/refresh`, {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ body: JSON.stringify({ refresh_token: refreshToken }),
83
+ });
84
+ if (response.statusCode !== 200) {
85
+ return null;
86
+ }
87
+ const data = await response.body.json();
88
+ // Decode user info from new token (basic decode, no verification needed here)
89
+ const payload = decodeTokenPayload(data.access_token);
90
+ if (!payload) {
91
+ return null;
92
+ }
93
+ // Read existing credentials to preserve team_id and sync_enabled
94
+ const existing = readCredentials();
95
+ return {
96
+ access_token: data.access_token,
97
+ refresh_token: data.refresh_token,
98
+ expires_at: data.expires_at,
99
+ user_id: payload.sub,
100
+ email: payload.email,
101
+ team_id: existing?.team_id,
102
+ sync_enabled: existing?.sync_enabled ?? false,
103
+ };
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Decode JWT payload without verification (for extracting user info)
111
+ * WARNING: Do not use for authentication - tokens are verified server-side
112
+ */
113
+ function decodeTokenPayload(token) {
114
+ try {
115
+ const parts = token.split('.');
116
+ if (parts.length !== 3)
117
+ return null;
118
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
119
+ return {
120
+ sub: payload.sub,
121
+ email: payload.email,
122
+ };
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Get a valid access token, refreshing if necessary
130
+ * @returns Access token or null if not authenticated
131
+ */
132
+ export async function getAccessToken() {
133
+ const creds = readCredentials();
134
+ if (!creds) {
135
+ return null;
136
+ }
137
+ // Check if token needs refresh
138
+ if (isTokenExpiringSoon(creds.expires_at)) {
139
+ const apiUrl = process.env.GROV_API_URL || 'https://api.grov.dev';
140
+ const newCreds = await refreshTokens(creds.refresh_token, apiUrl);
141
+ if (newCreds) {
142
+ writeCredentials(newCreds);
143
+ return newCreds.access_token;
144
+ }
145
+ // Refresh failed - user needs to login again
146
+ return null;
147
+ }
148
+ return creds.access_token;
149
+ }
150
+ /**
151
+ * Set the team ID for sync
152
+ */
153
+ export function setTeamId(teamId) {
154
+ const creds = readCredentials();
155
+ if (!creds) {
156
+ throw new Error('Not authenticated. Please run: grov login');
157
+ }
158
+ writeCredentials({
159
+ ...creds,
160
+ team_id: teamId,
161
+ });
162
+ }
163
+ /**
164
+ * Enable or disable sync
165
+ */
166
+ export function setSyncEnabled(enabled) {
167
+ const creds = readCredentials();
168
+ if (!creds) {
169
+ throw new Error('Not authenticated. Please run: grov login');
170
+ }
171
+ writeCredentials({
172
+ ...creds,
173
+ sync_enabled: enabled,
174
+ });
175
+ }
176
+ /**
177
+ * Get current sync status
178
+ */
179
+ export function getSyncStatus() {
180
+ const creds = readCredentials();
181
+ if (!creds) {
182
+ return null;
183
+ }
184
+ return {
185
+ enabled: creds.sync_enabled,
186
+ teamId: creds.team_id,
187
+ };
188
+ }
189
+ /**
190
+ * Get current user info
191
+ */
192
+ export function getCurrentUser() {
193
+ const creds = readCredentials();
194
+ if (!creds) {
195
+ return null;
196
+ }
197
+ return {
198
+ id: creds.user_id,
199
+ email: creds.email,
200
+ };
201
+ }
@@ -58,7 +58,7 @@ export declare function isSummaryAvailable(): boolean;
58
58
  * Generate session summary for CLEAR operation
59
59
  * Reference: plan_proxy_local.md Section 2.3, 4.5
60
60
  */
61
- export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[]): Promise<string>;
61
+ export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[], maxTokens?: number): Promise<string>;
62
62
  /**
63
63
  * Task analysis result from Haiku
64
64
  */
@@ -456,23 +456,30 @@ export function isSummaryAvailable() {
456
456
  * Generate session summary for CLEAR operation
457
457
  * Reference: plan_proxy_local.md Section 2.3, 4.5
458
458
  */
459
- export async function generateSessionSummary(sessionState, steps) {
459
+ export async function generateSessionSummary(sessionState, steps, maxTokens = 800 // Default 800, CLEAR mode uses 15000
460
+ ) {
460
461
  const client = getAnthropicClient();
462
+ // For larger summaries, include more steps
463
+ const stepLimit = maxTokens > 5000 ? 50 : 20;
464
+ const wordLimit = Math.min(Math.floor(maxTokens / 2), 10000); // ~2 tokens per word
461
465
  const stepsText = steps
462
466
  .filter(s => s.is_validated)
463
- .slice(-20)
467
+ .slice(-stepLimit)
464
468
  .map(step => {
465
469
  let desc = `- ${step.action_type}`;
466
470
  if (step.files.length > 0) {
467
471
  desc += `: ${step.files.join(', ')}`;
468
472
  }
469
473
  if (step.command) {
470
- desc += ` (${step.command.substring(0, 50)})`;
474
+ desc += ` (${step.command.substring(0, 100)})`;
475
+ }
476
+ if (step.reasoning && maxTokens > 5000) {
477
+ desc += `\n Reasoning: ${step.reasoning.substring(0, 200)}`;
471
478
  }
472
479
  return desc;
473
480
  })
474
481
  .join('\n');
475
- const prompt = `Create a concise summary of this coding session for context continuation.
482
+ const prompt = `Create a ${maxTokens > 5000 ? 'comprehensive' : 'concise'} summary of this coding session for context continuation.
476
483
 
477
484
  ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
478
485
 
@@ -483,18 +490,19 @@ CONSTRAINTS: ${sessionState.constraints.join(', ') || 'None'}
483
490
  ACTIONS TAKEN:
484
491
  ${stepsText || 'No actions recorded'}
485
492
 
486
- Create a summary with these sections (keep total under 500 words):
487
- 1. ORIGINAL GOAL: (1 sentence)
488
- 2. PROGRESS: (2-3 bullet points of what was accomplished)
489
- 3. KEY DECISIONS: (any important choices made)
490
- 4. FILES MODIFIED: (list of files)
491
- 5. CURRENT STATE: (where the work left off)
492
- 6. NEXT STEPS: (recommended next actions)
493
+ Create a summary with these sections (keep total under ${wordLimit} words):
494
+ 1. ORIGINAL GOAL: (1-2 sentences)
495
+ 2. PROGRESS: (${maxTokens > 5000 ? '5-10' : '2-3'} bullet points of what was accomplished)
496
+ 3. KEY DECISIONS: (important architectural/design choices made, with reasoning)
497
+ 4. FILES MODIFIED: (list of files with brief description of changes)
498
+ 5. CURRENT STATE: (detailed status of where the work left off)
499
+ 6. NEXT STEPS: (recommended next actions to continue)
500
+ ${maxTokens > 5000 ? '7. IMPORTANT CONTEXT: (any critical information that must not be lost)' : ''}
493
501
 
494
502
  Format as plain text, not JSON.`;
495
503
  const response = await client.messages.create({
496
504
  model: 'claude-haiku-4-5-20251001',
497
- max_tokens: 800,
505
+ max_tokens: maxTokens,
498
506
  messages: [{ role: 'user', content: prompt }],
499
507
  });
500
508
  const content = response.content?.[0];
@@ -544,10 +552,10 @@ export async function analyzeTaskContext(currentSession, latestUserMessage, rece
544
552
  // Check if we need to compress reasoning
545
553
  const needsCompression = assistantResponse.length > 1000;
546
554
  const compressionInstruction = needsCompression
547
- ? `\n "step_reasoning": "compressed summary of assistant's actions and reasoning (max 800 chars)"`
555
+ ? `\n "step_reasoning": "Extract CONCLUSIONS and SPECIFIC RECOMMENDATIONS only. Include: exact file paths (e.g., src/lib/utils.ts), function/component names, architectural patterns discovered, and WHY decisions were made. DO NOT write process descriptions like 'explored' or 'analyzed'. Max 800 chars."`
548
556
  : '';
549
557
  const compressionRule = needsCompression
550
- ? '\n- step_reasoning: Summarize what the assistant did and WHY in a concise way (max 800 chars)'
558
+ ? '\n- step_reasoning: Extract CONCLUSIONS (specific files, patterns, decisions) NOT process descriptions. Example GOOD: "Utilities belong in src/lib/utils.ts alongside cn(), formatDate()". Example BAD: "Explored codebase structure".'
551
559
  : '';
552
560
  // Extract topic keywords from goal for comparison
553
561
  const currentGoalKeywords = currentSession?.original_goal
@@ -660,40 +668,51 @@ export async function extractReasoningAndDecisions(stepsReasoning, originalGoal)
660
668
  if (combinedReasoning.length < 50) {
661
669
  return { reasoning_trace: [], decisions: [] };
662
670
  }
663
- const prompt = `Analyze Claude's work session and extract structured reasoning and decisions.
671
+ const prompt = `Extract CONCLUSIONS and KNOWLEDGE from Claude's work - NOT process descriptions.
664
672
 
665
673
  ORIGINAL GOAL:
666
674
  ${originalGoal || 'Not specified'}
667
675
 
668
- CLAUDE'S WORK (reasoning from each step):
676
+ CLAUDE'S RESPONSE:
669
677
  ${combinedReasoning}
670
678
 
671
679
  ═══════════════════════════════════════════════════════════════
672
- EXTRACT TWO THINGS:
680
+ EXTRACT ACTIONABLE CONCLUSIONS - NOT PROCESS
673
681
  ═══════════════════════════════════════════════════════════════
674
682
 
675
- 1. REASONING TRACE (what was done and WHY):
676
- - Each entry: "[ACTION] [target] because/to [reason]"
677
- - Include specific file names, function names when mentioned
678
- - Focus on WHY decisions were made, not just what
679
- - Max 10 entries, most important first
680
-
681
- 2. DECISIONS (choices made between alternatives):
682
- - Only include actual choices/tradeoffs
683
- - Each must have: what was chosen, why it was chosen
684
- - Examples: "Chose X over Y because Z"
683
+ GOOD examples (specific, reusable knowledge):
684
+ - "Utility functions belong in frontend/lib/utils.ts - existing utils: cn(), formatDate(), debounce()"
685
+ - "Auth tokens stored in localStorage with 15min expiry for long form sessions"
686
+ - "API routes follow REST pattern in /api/v1/ with Zod validation"
687
+ - "Database migrations go in prisma/migrations/ using prisma migrate"
688
+
689
+ BAD examples (process descriptions - DO NOT EXTRACT THESE):
690
+ - "Explored the codebase structure"
691
+ - "Analyzed several approaches"
692
+ - "Searched for utility directories"
693
+ - "Looked at the file organization"
694
+
695
+ 1. REASONING TRACE (conclusions and recommendations):
696
+ - WHAT was discovered or decided (specific file paths, patterns)
697
+ - WHY this is the right approach
698
+ - WHERE this applies in the codebase
699
+ - Max 10 entries, prioritize specific file/function recommendations
700
+
701
+ 2. DECISIONS (architectural choices):
702
+ - Only significant choices that affect future work
703
+ - What was chosen and why
685
704
  - Max 5 decisions
686
705
 
687
706
  Return JSON:
688
707
  {
689
708
  "reasoning_trace": [
690
- "Created auth/token-store.ts to separate storage logic from validation",
691
- "Used Map instead of Object for O(1) lookup performance",
692
- "Added expiry check in validateToken to prevent stale token usage"
709
+ "Utility functions belong in frontend/lib/utils.ts alongside cn(), formatDate(), debounce(), generateId()",
710
+ "Backend utilities go in backend/app/utils/ with domain-specific files like validation.py",
711
+ "The @/lib/utils import alias is configured for frontend utility access"
693
712
  ],
694
713
  "decisions": [
695
- {"choice": "Used SHA256 for token hashing", "reason": "Fast, secure enough for this use case, no external deps"},
696
- {"choice": "Split into separate files", "reason": "Better testability and single responsibility"}
714
+ {"choice": "Add to existing utils.ts rather than new file", "reason": "Maintains established pattern, easier discoverability"},
715
+ {"choice": "Use frontend/lib/ over src/utils/", "reason": "Follows Next.js conventions used throughout project"}
697
716
  ]
698
717
  }
699
718
 
@@ -701,7 +720,8 @@ RESPONSE RULES:
701
720
  - English only
702
721
  - No emojis
703
722
  - Valid JSON only
704
- - Be specific, not vague (bad: "Fixed bug", good: "Fixed null check in validateToken")`;
723
+ - Extract WHAT and WHERE, not just WHAT was done
724
+ - If no specific conclusions found, return empty arrays`;
705
725
  debugLLM('extractReasoningAndDecisions', `Analyzing ${stepsReasoning.length} steps, ${combinedReasoning.length} chars`);
706
726
  try {
707
727
  const response = await client.messages.create({
@@ -22,6 +22,8 @@ export interface Task {
22
22
  turn_number?: number;
23
23
  tags: string[];
24
24
  created_at: string;
25
+ synced_at?: string | null;
26
+ sync_error?: string | null;
25
27
  }
26
28
  export interface CreateTaskInput {
27
29
  project_path: string;
@@ -89,6 +91,11 @@ interface ProxyFields {
89
91
  completed_at?: string;
90
92
  parent_session_id?: string;
91
93
  task_type?: TaskType;
94
+ pending_correction?: string;
95
+ pending_forced_recovery?: string;
96
+ pending_clear_summary?: string;
97
+ cached_injection?: string;
98
+ final_response?: string;
92
99
  }
93
100
  export interface SessionState extends SessionStateBase, HookFields, ProxyFields {
94
101
  }
@@ -217,6 +224,18 @@ export declare function updateTaskStatus(id: string, status: TaskStatus): void;
217
224
  * Get task count for a project
218
225
  */
219
226
  export declare function getTaskCount(projectPath: string): number;
227
+ /**
228
+ * Get unsynced tasks for a project (synced_at is NULL)
229
+ */
230
+ export declare function getUnsyncedTasks(projectPath: string, limit?: number): Task[];
231
+ /**
232
+ * Mark a task as synced and clear any previous sync error
233
+ */
234
+ export declare function markTaskSynced(id: string): void;
235
+ /**
236
+ * Record a sync error for a task
237
+ */
238
+ export declare function setTaskSyncError(id: string, error: string): void;
220
239
  /**
221
240
  * Create a new session state.
222
241
  * FIXED: Uses INSERT OR IGNORE to handle race conditions safely.
@@ -306,6 +325,16 @@ export declare function getRecentSteps(sessionId: string, count?: number): StepR
306
325
  * Get validated steps only (for summary generation)
307
326
  */
308
327
  export declare function getValidatedSteps(sessionId: string): StepRecord[];
328
+ /**
329
+ * Get key decision steps for a session (is_key_decision = 1)
330
+ * Used for user message injection - important decisions with reasoning
331
+ */
332
+ export declare function getKeyDecisions(sessionId: string, limit?: number): StepRecord[];
333
+ /**
334
+ * Get edited files for a session (action_type IN ('edit', 'write'))
335
+ * Used for user message injection - prevent re-work
336
+ */
337
+ export declare function getEditedFiles(sessionId: string): string[];
309
338
  /**
310
339
  * Delete steps for a session
311
340
  */
@@ -346,9 +375,10 @@ export declare function getStepsByKeywords(sessionId: string, keywords: string[]
346
375
  export declare function getKeyDecisionSteps(sessionId: string, limit?: number): StepRecord[];
347
376
  /**
348
377
  * Get steps reasoning by file path (for proxy team memory injection)
349
- * Searches across ALL sessions, returns file-level reasoning from steps table
378
+ * Searches across sessions, returns file-level reasoning from steps table
379
+ * @param excludeSessionId - Optional session ID to exclude (for filtering current session)
350
380
  */
351
- export declare function getStepsReasoningByPath(filePath: string, limit?: number): Array<{
381
+ export declare function getStepsReasoningByPath(filePath: string, limit?: number, excludeSessionId?: string): Array<{
352
382
  file_path: string;
353
383
  reasoning: string;
354
384
  anchor?: string;