forked from op7418/CodePilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheckin-processor.ts
More file actions
168 lines (139 loc) · 6.64 KB
/
checkin-processor.ts
File metadata and controls
168 lines (139 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
* Core check-in processing logic, extracted from the API route
* so it can be called directly from server-side completion detection
* without an HTTP round-trip.
*/
import fs from 'fs';
import path from 'path';
import { getSetting, getSession } from '@/lib/db';
import { resolveProvider } from '@/lib/provider-resolver';
import { loadState, saveState, writeDailyMemory } from '@/lib/assistant-workspace';
import { getLocalDateString } from '@/lib/utils';
import { generateTextFromProvider } from '@/lib/text-generator';
const CHECK_IN_LABELS = [
'What did you work on or accomplish today?',
'Any changes to your current priorities or goals?',
'Anything you\'d like me to remember going forward?',
];
/**
* Process check-in completion. Generates daily memory, promotes stable facts,
* updates user profile.
*
* Idempotent for today: if state.lastCheckInDate is already today, returns early.
*
* @throws Error if workspace path is not configured or processing fails
*/
export async function processCheckin(
answers: Record<string, string>,
sessionId?: string,
): Promise<void> {
const workspacePath = getSetting('assistant_workspace_path');
if (!workspacePath) {
throw new Error('No workspace path configured');
}
const today = getLocalDateString();
// Idempotent check: skip if already checked in today
const currentState = loadState(workspacePath);
if (currentState.lastCheckInDate === today) {
return;
}
// Look up the calling session for provider/model context
let session: ReturnType<typeof getSession> | undefined;
if (sessionId) {
session = getSession(sessionId) ?? undefined;
if (session && session.working_directory !== workspacePath) {
throw new Error('Session does not belong to current workspace');
}
}
const qaText = CHECK_IN_LABELS.map((q, i) => {
const key = `q${i + 1}`;
return `Q: ${q}\nA: ${answers[key] || '(skipped)'}`;
}).join('\n\n');
// Read existing files for context
const memoryPath = path.join(workspacePath, 'memory.md');
const userPath = path.join(workspacePath, 'user.md');
let existingMemory = '';
let existingUser = '';
try { existingMemory = fs.readFileSync(memoryPath, 'utf-8'); } catch { /* new file */ }
try { existingUser = fs.readFileSync(userPath, 'utf-8'); } catch { /* new file */ }
try {
const resolved = resolveProvider({
sessionProviderId: session?.provider_id || undefined,
sessionModel: session?.model || undefined,
});
const providerId = resolved.provider?.id || 'env';
const model = resolved.upstreamModel || resolved.model || getSetting('default_model') || 'claude-sonnet-4-20250514';
const dailyMemoryPrompt = `You maintain daily memory entries for an AI assistant. Given the user's daily check-in answers, generate a daily memory entry for ${today}.
Format it with these sections:
## Work Log
(What the user accomplished today)
## Priority Changes
(Any shifts in goals or priorities)
## To Remember
(Things the user wants remembered)
## Candidate Long-Term Memory
(Facts that seem stable enough to promote to long-term memory — only include genuinely persistent facts, not transient updates)
Keep under 2000 characters.
Today's check-in (${today}):
${qaText}`;
const promotionPrompt = `You maintain a long-term memory file for an AI assistant. Given the user's check-in and the existing memory, output ONLY new stable facts that should be APPENDED to memory.md. These must be genuinely persistent facts (user preferences, recurring patterns, important relationships), NOT daily transients.
If there's nothing worth promoting, output exactly: (nothing to promote)
Existing memory.md:
${existingMemory || '(empty)'}
Today's check-in (${today}):
${qaText}`;
const userPrompt = `You maintain a user.md profile for an AI assistant. Given the user's daily check-in answers and the existing profile, generate an UPDATED user.md. Only update sections affected by today's answers. Keep it organized with markdown headers. Keep under 2000 characters.
Existing user.md:
${existingUser || '(empty)'}
Today's check-in (${today}):
${qaText}`;
const [dailyContent, promotionContent, newUser] = await Promise.all([
generateTextFromProvider({ providerId, model, system: 'You maintain knowledge files for AI assistants. Output only the file content, no explanations.', prompt: dailyMemoryPrompt }),
generateTextFromProvider({ providerId, model, system: 'You maintain knowledge files for AI assistants. Output only the content to append, no explanations.', prompt: promotionPrompt }),
generateTextFromProvider({ providerId, model, system: 'You maintain user profile documents. Output only the file content, no explanations.', prompt: userPrompt }),
]);
// Write daily memory file (episodic, per-day)
if (dailyContent.trim()) {
writeDailyMemory(workspacePath, today, dailyContent);
}
// Promote stable facts to memory.md (additive append, NOT rewrite)
// Dedup: skip if today's promotion already exists in memory.md
if (promotionContent.trim() && !promotionContent.includes('(nothing to promote)')) {
const currentMemory = fs.existsSync(memoryPath) ? fs.readFileSync(memoryPath, 'utf-8') : '';
if (!currentMemory.includes(`## Promoted from ${today}`)) {
const appendText = `\n\n## Promoted from ${today}\n${promotionContent.trim()}\n`;
fs.appendFileSync(memoryPath, appendText, 'utf-8');
}
}
// Incremental user.md update
if (existingUser && newUser.trim()) {
fs.writeFileSync(userPath, newUser, 'utf-8');
}
// Archive old daily memories and promote candidates
try {
const { archiveDailyMemories, promoteDailyToLongTerm } = await import('@/lib/workspace-organizer');
archiveDailyMemories(workspacePath);
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const dailyDir = path.join(workspacePath, 'memory', 'daily');
if (fs.existsSync(dailyDir)) {
const oldFiles = fs.readdirSync(dailyDir)
.filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
.filter(f => f.replace('.md', '') <= getLocalDateString(sevenDaysAgo));
for (const f of oldFiles) {
promoteDailyToLongTerm(workspacePath, f.replace('.md', ''));
}
}
} catch {
// organizer module not available, skip archival
}
} catch (e) {
console.warn('[checkin-processor] AI generation failed, writing raw daily entry:', e);
const rawDailyContent = `# Daily Check-in ${today}\n\n${qaText}\n`;
writeDailyMemory(workspacePath, today, rawDailyContent);
}
// Update state
const state = loadState(workspacePath);
state.lastCheckInDate = today;
saveState(workspacePath, state);
}