clementine-agent 1.15.0 → 1.16.0
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/agent/assistant.d.ts +21 -0
- package/dist/agent/assistant.js +119 -12
- package/dist/memory/store.d.ts +7 -0
- package/dist/memory/store.js +24 -0
- package/package.json +1 -1
|
@@ -200,6 +200,27 @@ export declare class PersonalAssistant {
|
|
|
200
200
|
* to avoid blocking the user's query.
|
|
201
201
|
*/
|
|
202
202
|
private buildLocalSummary;
|
|
203
|
+
private buildLocalSummaryFromTurns;
|
|
204
|
+
/**
|
|
205
|
+
* Walk a chronological list of transcript turns and pair adjacent
|
|
206
|
+
* user→assistant rows. Drops 'system' rows and orphan tail user turns
|
|
207
|
+
* (which represent in-flight messages with no reply yet).
|
|
208
|
+
*/
|
|
209
|
+
private pairTranscriptTurns;
|
|
210
|
+
/**
|
|
211
|
+
* Build a short summary of older turns (older than what's already cached
|
|
212
|
+
* in `lastExchanges`) for restart-restore prompt injection. Returns ''
|
|
213
|
+
* if there's nothing older or no memory store. Capped at 600 chars.
|
|
214
|
+
*/
|
|
215
|
+
private buildOlderTurnsContext;
|
|
216
|
+
/**
|
|
217
|
+
* Reconstruct context after the SDK reports the session is dead
|
|
218
|
+
* ("no conversation found"). Pulls last N turns from the transcripts
|
|
219
|
+
* table (hydrating `lastExchanges` if the cache is too thin) and
|
|
220
|
+
* builds a recovery prefix that gets prepended to the retry prompt.
|
|
221
|
+
* Mirrors the buildContextRecoveredPrompt pattern used by autocompact.
|
|
222
|
+
*/
|
|
223
|
+
private buildSessionDeathRecoveryPrompt;
|
|
203
224
|
/**
|
|
204
225
|
* Auto-save a lightweight handoff file when a session rotates.
|
|
205
226
|
* Uses in-memory exchange history — no LLM call.
|
package/dist/agent/assistant.js
CHANGED
|
@@ -2368,13 +2368,19 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2368
2368
|
if (key && this.restoredSessions.has(key)) {
|
|
2369
2369
|
const exchanges = this.lastExchanges.get(key) ?? [];
|
|
2370
2370
|
if (exchanges.length > 0) {
|
|
2371
|
+
const olderSummary = this.buildOlderTurnsContext(key, exchanges);
|
|
2371
2372
|
const historyLines = [];
|
|
2372
2373
|
for (const ex of exchanges.slice(-5)) {
|
|
2373
2374
|
historyLines.push(`You said: ${ex.user.slice(0, 800)}`);
|
|
2374
2375
|
historyLines.push(`I replied: ${ex.assistant.slice(0, 800)}`);
|
|
2375
2376
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2377
|
+
const blocks = [];
|
|
2378
|
+
if (olderSummary)
|
|
2379
|
+
blocks.push(`[Older session summary: ${olderSummary}]`);
|
|
2380
|
+
blocks.push(`[Conversation context from before restart (our recent messages):\n${historyLines.join('\n')}]`);
|
|
2381
|
+
const prefix = blocks.join('\n\n');
|
|
2382
|
+
logger.debug({ sessionKey: key, prefixLen: prefix.length, hasOlder: !!olderSummary }, 'Restart restore prefix assembled');
|
|
2383
|
+
effectivePrompt = `${prefix}\n\n${effectivePrompt}`;
|
|
2378
2384
|
}
|
|
2379
2385
|
this.restoredSessions.delete(key); // Only inject once per restored session
|
|
2380
2386
|
}
|
|
@@ -2838,10 +2844,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2838
2844
|
break;
|
|
2839
2845
|
}
|
|
2840
2846
|
else if (lower.includes('no conversation found') || lower.includes('conversation not found') || lower.includes('session not found')) {
|
|
2841
|
-
// Stale session — clear and
|
|
2842
|
-
logger.warn({ sessionKey }, 'Stale session ID —
|
|
2847
|
+
// Stale session — clear and reconstruct context from transcripts before retrying
|
|
2848
|
+
logger.warn({ sessionKey }, 'Stale session ID — reconstructing context from transcripts');
|
|
2843
2849
|
if (sessionKey) {
|
|
2844
2850
|
this.sessions.delete(sessionKey);
|
|
2851
|
+
prompt = this.buildSessionDeathRecoveryPrompt(prompt, sessionKey);
|
|
2845
2852
|
}
|
|
2846
2853
|
staleSession = true;
|
|
2847
2854
|
break; // Break inner stream loop — staleSession flag triggers retry
|
|
@@ -2947,10 +2954,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2947
2954
|
"I've reset the session. Try again — I'll keep result sets smaller this time.");
|
|
2948
2955
|
}
|
|
2949
2956
|
else if (errStr.includes('no conversation found') || errStr.includes('conversation not found') || errStr.includes('session not found')) {
|
|
2950
|
-
// Stale session — clear and
|
|
2951
|
-
logger.warn({ sessionKey }, 'Stale session ID (exception) —
|
|
2957
|
+
// Stale session — clear and reconstruct context from transcripts before retrying
|
|
2958
|
+
logger.warn({ sessionKey }, 'Stale session ID (exception) — reconstructing context from transcripts');
|
|
2952
2959
|
if (sessionKey) {
|
|
2953
2960
|
this.sessions.delete(sessionKey);
|
|
2961
|
+
prompt = this.buildSessionDeathRecoveryPrompt(prompt, sessionKey);
|
|
2954
2962
|
}
|
|
2955
2963
|
continue; // Retry with fresh session
|
|
2956
2964
|
}
|
|
@@ -3312,17 +3320,116 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3312
3320
|
* to avoid blocking the user's query.
|
|
3313
3321
|
*/
|
|
3314
3322
|
buildLocalSummary(sessionKey) {
|
|
3315
|
-
|
|
3316
|
-
|
|
3323
|
+
return this.buildLocalSummaryFromTurns(this.lastExchanges.get(sessionKey) ?? []);
|
|
3324
|
+
}
|
|
3325
|
+
buildLocalSummaryFromTurns(turns, opts) {
|
|
3326
|
+
if (turns.length === 0)
|
|
3317
3327
|
return '';
|
|
3318
|
-
const
|
|
3328
|
+
const take = opts?.take ?? 5;
|
|
3329
|
+
const userMax = opts?.userMax ?? 200;
|
|
3330
|
+
const assistantMax = opts?.assistantMax ?? 300;
|
|
3331
|
+
const recent = turns.slice(-take);
|
|
3332
|
+
const baseIndex = opts?.startIndex ?? (turns.length - recent.length);
|
|
3319
3333
|
const lines = recent.map((ex, i) => {
|
|
3320
|
-
const userSnippet = ex.user.slice(0,
|
|
3321
|
-
const assistantSnippet = ex.assistant.slice(0,
|
|
3322
|
-
return `- Exchange ${
|
|
3334
|
+
const userSnippet = ex.user.slice(0, userMax).replace(/\n/g, ' ');
|
|
3335
|
+
const assistantSnippet = ex.assistant.slice(0, assistantMax).replace(/\n/g, ' ');
|
|
3336
|
+
return `- Exchange ${baseIndex + i + 1}: User asked about "${userSnippet}" / I responded "${assistantSnippet}"`;
|
|
3323
3337
|
});
|
|
3324
3338
|
return lines.join('\n');
|
|
3325
3339
|
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Walk a chronological list of transcript turns and pair adjacent
|
|
3342
|
+
* user→assistant rows. Drops 'system' rows and orphan tail user turns
|
|
3343
|
+
* (which represent in-flight messages with no reply yet).
|
|
3344
|
+
*/
|
|
3345
|
+
pairTranscriptTurns(turns) {
|
|
3346
|
+
const pairs = [];
|
|
3347
|
+
let pendingUser = null;
|
|
3348
|
+
for (const turn of turns) {
|
|
3349
|
+
if (turn.role === 'user') {
|
|
3350
|
+
pendingUser = turn.content;
|
|
3351
|
+
}
|
|
3352
|
+
else if (turn.role === 'assistant' && pendingUser !== null) {
|
|
3353
|
+
pairs.push({ user: pendingUser, assistant: turn.content });
|
|
3354
|
+
pendingUser = null;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
return pairs;
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Build a short summary of older turns (older than what's already cached
|
|
3361
|
+
* in `lastExchanges`) for restart-restore prompt injection. Returns ''
|
|
3362
|
+
* if there's nothing older or no memory store. Capped at 600 chars.
|
|
3363
|
+
*/
|
|
3364
|
+
buildOlderTurnsContext(sessionKey, cachedExchanges) {
|
|
3365
|
+
if (cachedExchanges.length === 0)
|
|
3366
|
+
return '';
|
|
3367
|
+
if (!this.memoryStore || typeof this.memoryStore.getTranscriptTail !== 'function')
|
|
3368
|
+
return '';
|
|
3369
|
+
try {
|
|
3370
|
+
const skipTurns = cachedExchanges.length * 2;
|
|
3371
|
+
const older = this.memoryStore.getTranscriptTail(sessionKey, skipTurns, 40);
|
|
3372
|
+
if (!older || older.length === 0)
|
|
3373
|
+
return '';
|
|
3374
|
+
const pairs = this.pairTranscriptTurns(older);
|
|
3375
|
+
if (pairs.length === 0)
|
|
3376
|
+
return '';
|
|
3377
|
+
const summary = this.buildLocalSummaryFromTurns(pairs, {
|
|
3378
|
+
take: pairs.length,
|
|
3379
|
+
userMax: 120,
|
|
3380
|
+
assistantMax: 180,
|
|
3381
|
+
});
|
|
3382
|
+
return summary.slice(0, 600);
|
|
3383
|
+
}
|
|
3384
|
+
catch (err) {
|
|
3385
|
+
logger.debug({ err, sessionKey }, 'buildOlderTurnsContext failed — non-fatal');
|
|
3386
|
+
return '';
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Reconstruct context after the SDK reports the session is dead
|
|
3391
|
+
* ("no conversation found"). Pulls last N turns from the transcripts
|
|
3392
|
+
* table (hydrating `lastExchanges` if the cache is too thin) and
|
|
3393
|
+
* builds a recovery prefix that gets prepended to the retry prompt.
|
|
3394
|
+
* Mirrors the buildContextRecoveredPrompt pattern used by autocompact.
|
|
3395
|
+
*/
|
|
3396
|
+
buildSessionDeathRecoveryPrompt(prompt, sessionKey) {
|
|
3397
|
+
if (!sessionKey || !this.memoryStore)
|
|
3398
|
+
return prompt;
|
|
3399
|
+
try {
|
|
3400
|
+
let exchanges = this.lastExchanges.get(sessionKey) ?? [];
|
|
3401
|
+
// Hydrate from transcripts if the cache is too thin
|
|
3402
|
+
if (exchanges.length < 3 && typeof this.memoryStore.getTranscriptTail === 'function') {
|
|
3403
|
+
const recent = this.memoryStore.getTranscriptTail(sessionKey, 0, SESSION_EXCHANGE_HISTORY_SIZE * 2);
|
|
3404
|
+
const hydrated = this.pairTranscriptTurns(recent ?? []);
|
|
3405
|
+
if (hydrated.length > exchanges.length) {
|
|
3406
|
+
exchanges = hydrated;
|
|
3407
|
+
this.lastExchanges.set(sessionKey, hydrated.slice(-SESSION_EXCHANGE_HISTORY_SIZE));
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
const olderSummary = this.buildOlderTurnsContext(sessionKey, exchanges);
|
|
3411
|
+
const recentLines = [];
|
|
3412
|
+
for (const ex of exchanges.slice(-5)) {
|
|
3413
|
+
recentLines.push(`You said: ${ex.user.slice(0, 800)}`);
|
|
3414
|
+
recentLines.push(`I replied: ${ex.assistant.slice(0, 800)}`);
|
|
3415
|
+
}
|
|
3416
|
+
if (!olderSummary && recentLines.length === 0)
|
|
3417
|
+
return prompt;
|
|
3418
|
+
const blocks = ['[Recovering context after session expired.'];
|
|
3419
|
+
if (olderSummary)
|
|
3420
|
+
blocks.push(`Older session summary: ${olderSummary}`);
|
|
3421
|
+
if (recentLines.length > 0) {
|
|
3422
|
+
blocks.push(`Recent messages:\n${recentLines.join('\n')}`);
|
|
3423
|
+
}
|
|
3424
|
+
const prefix = blocks.join('\n') + ']';
|
|
3425
|
+
logger.debug({ sessionKey, prefixLen: prefix.length }, 'Session-death recovery prefix assembled');
|
|
3426
|
+
return `${prefix}\n\n${prompt}`;
|
|
3427
|
+
}
|
|
3428
|
+
catch (err) {
|
|
3429
|
+
logger.debug({ err, sessionKey }, 'buildSessionDeathRecoveryPrompt failed — non-fatal');
|
|
3430
|
+
return prompt;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3326
3433
|
/**
|
|
3327
3434
|
* Auto-save a lightweight handoff file when a session rotates.
|
|
3328
3435
|
* Uses in-memory exchange history — no LLM call.
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -473,6 +473,13 @@ export declare class MemoryStore {
|
|
|
473
473
|
* Get all turns for a given session, ordered chronologically.
|
|
474
474
|
*/
|
|
475
475
|
getSessionTranscript(sessionKey: string): TranscriptTurn[];
|
|
476
|
+
/**
|
|
477
|
+
* Get user/assistant transcript turns from the tail of a session, skipping
|
|
478
|
+
* the most recent `skipFromTail` turns. Returned chronologically. Used to
|
|
479
|
+
* reconstruct context older than what's already in the in-memory cache,
|
|
480
|
+
* for restart restore and SDK session-death recovery.
|
|
481
|
+
*/
|
|
482
|
+
getTranscriptTail(sessionKey: string, skipFromTail: number, limit?: number): TranscriptTurn[];
|
|
476
483
|
/**
|
|
477
484
|
* Get recent transcript activity across all sessions since a given timestamp.
|
|
478
485
|
* Returns a compact summary of what happened (sessions, message counts, snippets).
|
package/dist/memory/store.js
CHANGED
|
@@ -2393,6 +2393,30 @@ export class MemoryStore {
|
|
|
2393
2393
|
createdAt: row.created_at,
|
|
2394
2394
|
}));
|
|
2395
2395
|
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Get user/assistant transcript turns from the tail of a session, skipping
|
|
2398
|
+
* the most recent `skipFromTail` turns. Returned chronologically. Used to
|
|
2399
|
+
* reconstruct context older than what's already in the in-memory cache,
|
|
2400
|
+
* for restart restore and SDK session-death recovery.
|
|
2401
|
+
*/
|
|
2402
|
+
getTranscriptTail(sessionKey, skipFromTail, limit = 40) {
|
|
2403
|
+
const rows = this.conn
|
|
2404
|
+
.prepare(`SELECT session_key, role, content, model, created_at
|
|
2405
|
+
FROM transcripts
|
|
2406
|
+
WHERE session_key = ? AND role IN ('user','assistant')
|
|
2407
|
+
ORDER BY created_at DESC, id DESC
|
|
2408
|
+
LIMIT ? OFFSET ?`)
|
|
2409
|
+
.all(sessionKey, limit, Math.max(0, skipFromTail));
|
|
2410
|
+
return rows
|
|
2411
|
+
.map((row) => ({
|
|
2412
|
+
sessionKey: row.session_key,
|
|
2413
|
+
role: row.role,
|
|
2414
|
+
content: row.content,
|
|
2415
|
+
model: row.model,
|
|
2416
|
+
createdAt: row.created_at,
|
|
2417
|
+
}))
|
|
2418
|
+
.reverse();
|
|
2419
|
+
}
|
|
2396
2420
|
/**
|
|
2397
2421
|
* Get recent transcript activity across all sessions since a given timestamp.
|
|
2398
2422
|
* Returns a compact summary of what happened (sessions, message counts, snippets).
|