@axplusb/kepler 2.3.0 → 2.3.1
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/package.json +1 -1
- package/src/core/compact-history.mjs +127 -0
- package/src/core/jsonl-writer.mjs +9 -0
- package/src/core/local-store.mjs +20 -4
- package/src/core/resume-mode.mjs +11 -1
- package/src/terminal/repl.mjs +158 -13
package/package.json
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const SUMMARY_PREFIX = 'Session continuity summary after /compact:';
|
|
2
|
+
|
|
3
|
+
export function parseCompactTailCount(rest = '', fallback = 8) {
|
|
4
|
+
const text = String(rest || '').trim();
|
|
5
|
+
const match = text.match(/(?:^|\s)(?:--tail=|--tail\s+)?(\d+)(?:\s|$)/);
|
|
6
|
+
const n = match ? Number(match[1]) : Number(fallback);
|
|
7
|
+
if (!Number.isFinite(n)) return fallback;
|
|
8
|
+
return Math.max(2, Math.min(50, Math.floor(n)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isCompactHistory(history = []) {
|
|
12
|
+
return typeof history?.[2]?.content === 'string'
|
|
13
|
+
&& history[2].content.startsWith(SUMMARY_PREFIX);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extractCompactSummary(history = []) {
|
|
17
|
+
const content = String(history?.[2]?.content || '');
|
|
18
|
+
if (!content.startsWith(SUMMARY_PREFIX)) return '';
|
|
19
|
+
return content.slice(SUMMARY_PREFIX.length).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function prepareCompactHistory({
|
|
23
|
+
agentHistory = [],
|
|
24
|
+
tailCount = 8,
|
|
25
|
+
minSourceMessages = 2,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const history = Array.isArray(agentHistory)
|
|
28
|
+
? agentHistory.filter(msg => msg && typeof msg.content === 'string' && msg.content.trim())
|
|
29
|
+
: [];
|
|
30
|
+
const beforeCount = history.length;
|
|
31
|
+
const retainedCount = Math.min(Math.max(2, Number(tailCount) || 8), Math.max(0, beforeCount));
|
|
32
|
+
const prefixCount = isCompactHistory(history) ? 3 : 0;
|
|
33
|
+
const sourceEnd = Math.max(prefixCount, beforeCount - retainedCount);
|
|
34
|
+
const sourceMessages = history.slice(prefixCount, sourceEnd);
|
|
35
|
+
const tail = history.slice(sourceEnd);
|
|
36
|
+
|
|
37
|
+
if (sourceMessages.length < minSourceMessages) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: beforeCount <= prefixCount + retainedCount
|
|
41
|
+
? 'not enough history beyond the retained tail'
|
|
42
|
+
: 'not enough compactable messages',
|
|
43
|
+
beforeCount,
|
|
44
|
+
prefixCount,
|
|
45
|
+
sourceMessages,
|
|
46
|
+
tail,
|
|
47
|
+
retainedCount,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
beforeCount,
|
|
54
|
+
prefixCount,
|
|
55
|
+
sourceMessages,
|
|
56
|
+
tail,
|
|
57
|
+
retainedCount: tail.length,
|
|
58
|
+
previousSummary: extractCompactSummary(history),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyCompactSummary({
|
|
63
|
+
prepared,
|
|
64
|
+
summary,
|
|
65
|
+
sessionId = '',
|
|
66
|
+
cwd = '',
|
|
67
|
+
originalRequest = '',
|
|
68
|
+
previousSourceMessageCount = 0,
|
|
69
|
+
now = new Date(),
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (!prepared?.ok) throw new Error(prepared?.reason || 'history is not compactable');
|
|
72
|
+
const compactSummary = String(summary || '').trim();
|
|
73
|
+
if (!compactSummary) throw new Error('summary is required');
|
|
74
|
+
const priorCount = Math.max(0, Number(previousSourceMessageCount) || 0);
|
|
75
|
+
const sourceMessageCount = priorCount + prepared.sourceMessages.length;
|
|
76
|
+
const timestamp = now instanceof Date ? now.toISOString() : String(now || new Date().toISOString());
|
|
77
|
+
const firstUser = originalRequest || firstUserMessage(prepared.sourceMessages) || firstUserMessage(prepared.tail) || '(unknown)';
|
|
78
|
+
|
|
79
|
+
const metadata = [
|
|
80
|
+
'Compact metadata.',
|
|
81
|
+
sessionId ? `Session: ${sessionId}` : '',
|
|
82
|
+
cwd ? `Project: ${cwd}` : '',
|
|
83
|
+
`Compacted at: ${timestamp}`,
|
|
84
|
+
`Covered live messages: ${sourceMessageCount}`,
|
|
85
|
+
`Retained tail messages: ${prepared.tail.length}`,
|
|
86
|
+
].filter(Boolean).join('\n');
|
|
87
|
+
|
|
88
|
+
const agentHistory = [
|
|
89
|
+
{ role: 'user', content: metadata },
|
|
90
|
+
{ role: 'user', content: `Original user request from this compacted session:\n${firstUser}` },
|
|
91
|
+
{ role: 'user', content: `${SUMMARY_PREFIX}\n${compactSummary}` },
|
|
92
|
+
...prepared.tail,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
agentHistory,
|
|
97
|
+
summary: compactSummary,
|
|
98
|
+
sourceMessageCount,
|
|
99
|
+
previousSourceMessageCount: priorCount,
|
|
100
|
+
beforeCount: prepared.beforeCount,
|
|
101
|
+
afterCount: agentHistory.length,
|
|
102
|
+
retainedCount: prepared.tail.length,
|
|
103
|
+
compactedCount: prepared.sourceMessages.length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function localCompactSummary(messages = []) {
|
|
108
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
109
|
+
const userMessages = list.filter(m => m.role === 'user').map(m => String(m.content || '').trim()).filter(Boolean);
|
|
110
|
+
const assistantMessages = list.filter(m => m.role === 'assistant').map(m => String(m.content || '').trim()).filter(Boolean);
|
|
111
|
+
return [
|
|
112
|
+
'Local compact summary from live conversation context.',
|
|
113
|
+
userMessages.length ? `User requests (${userMessages.length}):` : '',
|
|
114
|
+
...userMessages.slice(-8).map(text => `- ${truncate(text, 500)}`),
|
|
115
|
+
assistantMessages.length ? `Assistant progress (${assistantMessages.length}):` : '',
|
|
116
|
+
...assistantMessages.slice(-8).map(text => `- ${truncate(text, 700)}`),
|
|
117
|
+
].filter(Boolean).join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstUserMessage(messages = []) {
|
|
121
|
+
return messages.find(m => m?.role === 'user' && typeof m.content === 'string')?.content || '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function truncate(text, max) {
|
|
125
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
126
|
+
return value.length > max ? value.slice(0, max - 3) + '...' : value;
|
|
127
|
+
}
|
|
@@ -317,6 +317,15 @@ export class JsonlWriter {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
async flush() {
|
|
321
|
+
await this._flush();
|
|
322
|
+
if (this._flushPromise) await this._flushPromise;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
get transcriptPath() {
|
|
326
|
+
return this._transcriptPath;
|
|
327
|
+
}
|
|
328
|
+
|
|
320
329
|
// ── Internal ──
|
|
321
330
|
|
|
322
331
|
_resetTurn() {
|
package/src/core/local-store.mjs
CHANGED
|
@@ -531,10 +531,10 @@ export function buildResumeHistory(detail, mode = 'compact') {
|
|
|
531
531
|
);
|
|
532
532
|
|
|
533
533
|
// PRD-068 §5.14.4: resume mode picker.
|
|
534
|
-
// 'full'
|
|
535
|
-
// '
|
|
536
|
-
//
|
|
537
|
-
// 'summary'
|
|
534
|
+
// 'full' — every turn sent verbatim (unchanged)
|
|
535
|
+
// 'checkpoint-full' — latest summary checkpoint + every message after it
|
|
536
|
+
// 'tail-N' — recap block + last N conversation messages
|
|
537
|
+
// 'summary' — recap block only. Cheapest continuity, biggest lossiness.
|
|
538
538
|
let agentHistory;
|
|
539
539
|
let sourceMessages = fullAgentHistory;
|
|
540
540
|
let summaryMessageIndex = -1;
|
|
@@ -543,6 +543,22 @@ export function buildResumeHistory(detail, mode = 'compact') {
|
|
|
543
543
|
if (mode === 'full') {
|
|
544
544
|
agentHistory = fullAgentHistory;
|
|
545
545
|
summaryCoveredMessageCount = fullAgentHistory.length;
|
|
546
|
+
} else if (mode === 'checkpoint-full') {
|
|
547
|
+
sourceMessages = [];
|
|
548
|
+
const tail = fullAgentHistory.slice(summaryCheckpointMessageCount);
|
|
549
|
+
activeSummary = priorSummary
|
|
550
|
+
|| summaryForMessages(fullAgentHistory.slice(0, summaryCheckpointMessageCount), {
|
|
551
|
+
fallback: summary,
|
|
552
|
+
label: 'Summary checkpoint',
|
|
553
|
+
});
|
|
554
|
+
summaryCoveredMessageCount = summaryCheckpointMessageCount;
|
|
555
|
+
agentHistory = [
|
|
556
|
+
{ role: 'user', content: metadata },
|
|
557
|
+
{ role: 'user', content: `Original user request from this resumed session:\n${originalRequest || '(unknown)'}` },
|
|
558
|
+
{ role: 'user', content: activeSummary || summary },
|
|
559
|
+
...tail,
|
|
560
|
+
];
|
|
561
|
+
summaryMessageIndex = 2;
|
|
546
562
|
} else if (mode === 'recap+tail' || /^tail-\d+$/.test(String(mode || ''))) {
|
|
547
563
|
const tailTurns = tailTurnsForMode(mode, detail);
|
|
548
564
|
const { tail, startIndex } = tailHistorySliceByRecentMessages(fullAgentHistory, tailTurns);
|
package/src/core/resume-mode.mjs
CHANGED
|
@@ -104,7 +104,7 @@ export function decideResumeMode({
|
|
|
104
104
|
* Estimate the context tokens for a candidate mode. Used by the tri-choice
|
|
105
105
|
* overlay to render "62k / 14k / 2k" per option before the user commits.
|
|
106
106
|
*
|
|
107
|
-
* @param {'full' | 'summary' | 'tail-10' | 'tail-20' | 'recap+tail'} choice
|
|
107
|
+
* @param {'full' | 'checkpoint-full' | 'summary' | 'tail-10' | 'tail-20' | 'recap+tail'} choice
|
|
108
108
|
* @param {number} fullTokens — projected transcript size in full mode
|
|
109
109
|
* @param {object} [opts]
|
|
110
110
|
* @returns {number} projected tokens for the chosen mode
|
|
@@ -116,11 +116,21 @@ export function projectedTokensForChoice(choice, fullTokens, opts = {}) {
|
|
|
116
116
|
tailBaseTokens = 2000,
|
|
117
117
|
perTailTurnTokens = 500,
|
|
118
118
|
summaryBaseTokens = 2000,
|
|
119
|
+
resumeSummary = null,
|
|
119
120
|
} = opts;
|
|
120
121
|
|
|
121
122
|
switch (choice) {
|
|
122
123
|
case 'full':
|
|
123
124
|
return Math.max(0, Number(fullTokens) || 0);
|
|
125
|
+
case 'checkpoint-full': {
|
|
126
|
+
const full = Math.max(0, Number(fullTokens) || 0);
|
|
127
|
+
const fullMessages = Math.max(0, Number(resumeSummary?.fullMessageCount) || 0);
|
|
128
|
+
const covered = Math.max(0, Number(resumeSummary?.sourceMessageCount) || 0);
|
|
129
|
+
if (!fullMessages || covered <= 0) return full;
|
|
130
|
+
const remainingMessages = Math.max(0, fullMessages - covered);
|
|
131
|
+
if (remainingMessages === 0) return summaryBaseTokens;
|
|
132
|
+
return summaryBaseTokens + (perTailTurnTokens * remainingMessages);
|
|
133
|
+
}
|
|
124
134
|
case 'tail-10':
|
|
125
135
|
return tailBaseTokens + (10 * perTailTurnTokens);
|
|
126
136
|
case 'tail-20':
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
|
49
49
|
import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
|
|
50
50
|
import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
|
|
51
51
|
import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
|
|
52
|
+
import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
|
|
52
53
|
import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
53
54
|
import { createOrbit } from '../state/orbit.mjs';
|
|
54
55
|
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
@@ -196,6 +197,18 @@ function formatResumeCheckpointStatus(session) {
|
|
|
196
197
|
return c.dim(` · summarized${pct}`);
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
function formatResumeContextStatus(session) {
|
|
201
|
+
const full = formatCtxTokens(session?.contextTokens || 0);
|
|
202
|
+
const marker = session?.resumeSummary;
|
|
203
|
+
if (!marker || !Number(marker.sourceMessageCount)) {
|
|
204
|
+
return c.dim(`${full.padStart(5, ' ')} ctx`);
|
|
205
|
+
}
|
|
206
|
+
const resumable = formatCtxTokens(projectedTokensForChoice('checkpoint-full', session.contextTokens || 0, {
|
|
207
|
+
resumeSummary: marker,
|
|
208
|
+
}));
|
|
209
|
+
return c.dim(`${resumable.padStart(5, ' ')} resumable · ${full} full`) + formatResumeCheckpointStatus(session);
|
|
210
|
+
}
|
|
211
|
+
|
|
199
212
|
function formatRelativeTime(iso) {
|
|
200
213
|
if (!iso) return '';
|
|
201
214
|
const t = Date.parse(iso);
|
|
@@ -250,7 +263,7 @@ async function pickResumableSession(resumable, ctx) {
|
|
|
250
263
|
const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
|
|
251
264
|
const status = endStatusMarker(s.endStatus);
|
|
252
265
|
const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
|
|
253
|
-
const ctx =
|
|
266
|
+
const ctx = formatResumeContextStatus(s);
|
|
254
267
|
const cost = formatSessionCost(s.costUsd);
|
|
255
268
|
const partial = s.partial ? c.yellow(' ⚠partial') : '';
|
|
256
269
|
const instr = oneLineInstruction(s.instruction, 48);
|
|
@@ -308,8 +321,14 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
308
321
|
if (rl) rl.pause();
|
|
309
322
|
|
|
310
323
|
const canFull = decision.mode !== 'no-full-allowed';
|
|
324
|
+
const hasCheckpoint = Boolean(decision.resumeSummary?.sourceMessageCount);
|
|
325
|
+
const firstOption = canFull
|
|
326
|
+
? { key: 'f', value: 'full', label: 'full transcript', enabled: true }
|
|
327
|
+
: hasCheckpoint
|
|
328
|
+
? { key: 'f', value: 'checkpoint-full', label: 'checkpointed transcript', enabled: true }
|
|
329
|
+
: { key: 'f', value: 'full', label: 'full transcript', enabled: false };
|
|
311
330
|
const options = [
|
|
312
|
-
|
|
331
|
+
firstOption,
|
|
313
332
|
{ key: 's', value: 'summary', label: 'summary only', enabled: true },
|
|
314
333
|
{ key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
|
|
315
334
|
{ key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
|
|
@@ -328,7 +347,8 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
328
347
|
const projected = formatCtxTokens(decision.projected);
|
|
329
348
|
const win = formatCtxTokens(decision.windowSize);
|
|
330
349
|
const lines = [];
|
|
331
|
-
|
|
350
|
+
const rawLabel = hasCheckpoint ? 'Raw full transcript would use' : 'This session would use';
|
|
351
|
+
lines.push(` ${rawLabel} ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
|
|
332
352
|
if (decision.resumeSummary?.sourceMessageCount) {
|
|
333
353
|
const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
|
|
334
354
|
const full = Number(decision.resumeSummary.fullMessageCount) || 0;
|
|
@@ -337,14 +357,18 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
337
357
|
}
|
|
338
358
|
lines.push(canFull
|
|
339
359
|
? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
|
|
340
|
-
:
|
|
360
|
+
: hasCheckpoint
|
|
361
|
+
? ` ${c.yellow('⚠')} ${c.dim('raw full is over hardCap — checkpoint/tail modes are available:')}`
|
|
362
|
+
: ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
|
|
341
363
|
lines.push('');
|
|
342
364
|
for (let i = 0; i < options.length; i++) {
|
|
343
365
|
const o = options[i];
|
|
344
366
|
const disabled = !o.enabled;
|
|
345
367
|
const marker = i === selected && !disabled ? c.brand('▸') : ' ';
|
|
346
368
|
const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
|
|
347
|
-
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected
|
|
369
|
+
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected, {
|
|
370
|
+
resumeSummary: decision.resumeSummary,
|
|
371
|
+
}));
|
|
348
372
|
const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
|
|
349
373
|
const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
|
|
350
374
|
const suffix = disabled ? c.dim(' (over hardCap)') : '';
|
|
@@ -389,6 +413,7 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
389
413
|
}
|
|
390
414
|
|
|
391
415
|
function resumeModeLabel(mode = 'full') {
|
|
416
|
+
if (mode === 'checkpoint-full') return 'checkpointed transcript';
|
|
392
417
|
if (mode === 'summary') return 'summary only';
|
|
393
418
|
const tailTurns = resumeTailTurnCount(mode);
|
|
394
419
|
if (tailTurns) return `summary + last ${tailTurns} turns`;
|
|
@@ -434,7 +459,7 @@ async function previewResumeSession(session, ctx) {
|
|
|
434
459
|
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
435
460
|
|
|
436
461
|
const lines = [];
|
|
437
|
-
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens)) + ' ctx')}`);
|
|
462
|
+
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens, { resumeSummary: session.resumeSummary })) + ' ctx')}`);
|
|
438
463
|
lines.push(` ${c.dim('─'.repeat(60))}`);
|
|
439
464
|
for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
|
|
440
465
|
lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
|
|
@@ -460,7 +485,7 @@ async function previewResumeSession(session, ctx) {
|
|
|
460
485
|
if (key === '[B') { scrollOffset += 1; render(); return; }
|
|
461
486
|
if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
|
|
462
487
|
if (key === '[6~') { scrollOffset += 10; render(); return; }
|
|
463
|
-
if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
488
|
+
if (low === 'f') { mode = session.resumeSummary?.sourceMessageCount ? 'checkpoint-full' : 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
464
489
|
if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
|
|
465
490
|
if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
|
|
466
491
|
if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
|
|
@@ -659,6 +684,120 @@ async function summarizeResumeTranscript({
|
|
|
659
684
|
}
|
|
660
685
|
}
|
|
661
686
|
|
|
687
|
+
async function compactCurrentSession(ctx, rest = '') {
|
|
688
|
+
const tailCount = parseCompactTailCount(rest, 8);
|
|
689
|
+
const preparedLive = prepareCompactHistory({
|
|
690
|
+
agentHistory: session.agentHistory,
|
|
691
|
+
tailCount,
|
|
692
|
+
});
|
|
693
|
+
if (!preparedLive.ok) {
|
|
694
|
+
process.stderr.write(` ${c.gray(`Nothing to compact — ${preparedLive.reason}.`)}\n`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const progress = startResumeProgress('compact');
|
|
699
|
+
progress.update('preparing compact summary', 18);
|
|
700
|
+
let sourceMessages = preparedLive.sourceMessages;
|
|
701
|
+
let priorSummary = preparedLive.previousSummary || '';
|
|
702
|
+
let previousSourceMessageCount = 0;
|
|
703
|
+
let fullMessageCount = preparedLive.beforeCount;
|
|
704
|
+
let projectPath = safeCwd();
|
|
705
|
+
let sourceFrom = 'live';
|
|
706
|
+
let summaryWarning = '';
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
if (session.id && ctx.jsonlWriter?.flush) {
|
|
710
|
+
progress.update('reading transcript checkpoint', 28);
|
|
711
|
+
await ctx.jsonlWriter.flush();
|
|
712
|
+
const detail = await getSessionDetail(session.id, { filePath: ctx.jsonlWriter.transcriptPath });
|
|
713
|
+
if (detail) {
|
|
714
|
+
const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, `tail-${tailCount}`);
|
|
715
|
+
if (richHistory.sourceMessages?.length) {
|
|
716
|
+
sourceMessages = richHistory.sourceMessages;
|
|
717
|
+
priorSummary = richHistory.priorSummary || '';
|
|
718
|
+
previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
|
|
719
|
+
fullMessageCount = richHistory.fullMessageCount || sourceMessages.length;
|
|
720
|
+
projectPath = detail.meta?.project || safeCwd();
|
|
721
|
+
sourceFrom = 'transcript';
|
|
722
|
+
} else if (richHistory.priorSummary) {
|
|
723
|
+
priorSummary = richHistory.priorSummary;
|
|
724
|
+
previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
|
|
725
|
+
fullMessageCount = richHistory.fullMessageCount || preparedLive.beforeCount;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!sourceMessages.length) {
|
|
731
|
+
progress.stop();
|
|
732
|
+
process.stderr.write(` ${c.gray('Nothing new to compact.')}\n`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
progress.update('summarizing compacted history', 46);
|
|
737
|
+
const backendSummary = await summarizeResumeTranscript({
|
|
738
|
+
auth: ctx.auth,
|
|
739
|
+
toolExecutor: ctx.toolExecutor,
|
|
740
|
+
sessionId: session.id,
|
|
741
|
+
projectPath,
|
|
742
|
+
messages: sourceMessages,
|
|
743
|
+
});
|
|
744
|
+
let summarySource = backendSummary?.summary ? (backendSummary.source || 'backend') : 'local fallback';
|
|
745
|
+
let deltaSummary = backendSummary?.summary || '';
|
|
746
|
+
if (!deltaSummary) {
|
|
747
|
+
summaryWarning = backendSummary?.reason || 'backend summary unavailable';
|
|
748
|
+
deltaSummary = localCompactSummary(sourceMessages);
|
|
749
|
+
}
|
|
750
|
+
const summary = combineResumeSummaries(priorSummary, deltaSummary);
|
|
751
|
+
|
|
752
|
+
progress.update('rewriting live context', 74);
|
|
753
|
+
const applied = applyCompactSummary({
|
|
754
|
+
prepared: { ...preparedLive, sourceMessages },
|
|
755
|
+
summary,
|
|
756
|
+
sessionId: session.id,
|
|
757
|
+
cwd: projectPath,
|
|
758
|
+
originalRequest: session.history.find(m => m.role === 'user')?.content || session.lastTask || '',
|
|
759
|
+
previousSourceMessageCount,
|
|
760
|
+
});
|
|
761
|
+
session.agentHistory = applied.agentHistory;
|
|
762
|
+
session.compactSummary = summary;
|
|
763
|
+
session.compactSourceMessageCount = applied.sourceMessageCount;
|
|
764
|
+
|
|
765
|
+
if (ctx.jsonlWriter) {
|
|
766
|
+
progress.update('writing summary checkpoint', 88);
|
|
767
|
+
ctx.jsonlWriter.writeKeplerEvent({
|
|
768
|
+
type: 'resume_summary',
|
|
769
|
+
data: {
|
|
770
|
+
session_id: session.id || null,
|
|
771
|
+
mode: 'compact',
|
|
772
|
+
mode_label: '/compact',
|
|
773
|
+
summary,
|
|
774
|
+
summary_source: summarySource,
|
|
775
|
+
summary_warning: summaryWarning || null,
|
|
776
|
+
source: sourceFrom,
|
|
777
|
+
source_message_count: applied.sourceMessageCount,
|
|
778
|
+
previous_source_message_count: previousSourceMessageCount,
|
|
779
|
+
full_message_count: fullMessageCount,
|
|
780
|
+
retained_tail_messages: applied.retainedCount,
|
|
781
|
+
live_before_messages: applied.beforeCount,
|
|
782
|
+
live_after_messages: applied.afterCount,
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
await ctx.jsonlWriter.flush?.();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
progress.stop();
|
|
789
|
+
process.stderr.write(
|
|
790
|
+
` ${c.green('✓')} ${c.dim(`Compacted context: ${applied.beforeCount} → ${applied.afterCount} live messages · retained ${applied.retainedCount} · summary ${summarySource}`)}\n`
|
|
791
|
+
);
|
|
792
|
+
if (summaryWarning) {
|
|
793
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — used local summary (${summaryWarning})`)}\n`);
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
progress.stop();
|
|
797
|
+
process.stderr.write(` ${c.red(`Compact failed: ${err?.message || String(err)}`)}\n`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
662
801
|
function resumeTailTurnCount(mode = '') {
|
|
663
802
|
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
664
803
|
if (!match) return null;
|
|
@@ -1224,10 +1363,12 @@ function updateStatusBar() {
|
|
|
1224
1363
|
// buffered head as a regular two-line shape first so the interleaving
|
|
1225
1364
|
// content lands below it.
|
|
1226
1365
|
let _pendingHead = null; // { callId, head, indent }
|
|
1366
|
+
let _lastRenderedBlock = null; // 'tool' | 'content' | null
|
|
1227
1367
|
|
|
1228
1368
|
function flushPendingHead() {
|
|
1229
1369
|
if (!_pendingHead) return;
|
|
1230
|
-
process.stderr.write(
|
|
1370
|
+
process.stderr.write(`${_pendingHead.head}\n`);
|
|
1371
|
+
_lastRenderedBlock = 'tool';
|
|
1231
1372
|
_pendingHead = null;
|
|
1232
1373
|
}
|
|
1233
1374
|
|
|
@@ -1312,7 +1453,8 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
1312
1453
|
const cols = process.stderr.columns || 120;
|
|
1313
1454
|
const combined = `${_pendingHead.head} ${outcome}`;
|
|
1314
1455
|
if (stripAnsi(combined).length <= cols) {
|
|
1315
|
-
process.stderr.write(
|
|
1456
|
+
process.stderr.write(`${combined}\n`);
|
|
1457
|
+
_lastRenderedBlock = 'tool';
|
|
1316
1458
|
_pendingHead = null;
|
|
1317
1459
|
return;
|
|
1318
1460
|
}
|
|
@@ -1326,6 +1468,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
1326
1468
|
|
|
1327
1469
|
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
1328
1470
|
process.stderr.write(`${gutter}${outcome}\n`);
|
|
1471
|
+
_lastRenderedBlock = 'tool';
|
|
1329
1472
|
|
|
1330
1473
|
// Lint warnings stay visible alongside writes.
|
|
1331
1474
|
if (hasLint) {
|
|
@@ -1448,6 +1591,7 @@ function startContentStream() {
|
|
|
1448
1591
|
_streamedPartialText = '';
|
|
1449
1592
|
_renderedToolResults.clear();
|
|
1450
1593
|
_renderedContentThisTurn = false;
|
|
1594
|
+
_lastRenderedBlock = null;
|
|
1451
1595
|
stopSpinner();
|
|
1452
1596
|
}
|
|
1453
1597
|
|
|
@@ -1472,12 +1616,14 @@ function flushContent() {
|
|
|
1472
1616
|
// Any buffered tool head needs to land BEFORE this content so the order
|
|
1473
1617
|
// is preserved on screen.
|
|
1474
1618
|
flushPendingHead();
|
|
1619
|
+
if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
|
|
1475
1620
|
const rendered = renderMarkdown(_streamBuffer);
|
|
1476
1621
|
for (const line of rendered.split('\n')) {
|
|
1477
1622
|
process.stdout.write(` ${line}\n`);
|
|
1478
1623
|
}
|
|
1479
1624
|
_streamBuffer = '';
|
|
1480
1625
|
_renderedContentThisTurn = true;
|
|
1626
|
+
_lastRenderedBlock = 'content';
|
|
1481
1627
|
}
|
|
1482
1628
|
|
|
1483
1629
|
// ── Event Renderer ──
|
|
@@ -1529,11 +1675,13 @@ function renderEvent(event) {
|
|
|
1529
1675
|
}
|
|
1530
1676
|
}
|
|
1531
1677
|
if (text) {
|
|
1678
|
+
if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
|
|
1532
1679
|
const rendered = renderMarkdown(text);
|
|
1533
1680
|
for (const line of rendered.split('\n')) {
|
|
1534
1681
|
process.stdout.write(` ${line}\n`);
|
|
1535
1682
|
}
|
|
1536
1683
|
_renderedContentThisTurn = true;
|
|
1684
|
+
_lastRenderedBlock = 'content';
|
|
1537
1685
|
}
|
|
1538
1686
|
break;
|
|
1539
1687
|
}
|
|
@@ -2576,10 +2724,7 @@ async function handleCommand(input, ctx) {
|
|
|
2576
2724
|
}
|
|
2577
2725
|
|
|
2578
2726
|
case '/compact': {
|
|
2579
|
-
|
|
2580
|
-
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
2581
|
-
session.agentHistory.splice(2, session.agentHistory.length - 6);
|
|
2582
|
-
process.stderr.write(` ${c.gray(`Compacted agent context: ${before} → ${session.agentHistory.length} messages`)}\n`);
|
|
2727
|
+
await compactCurrentSession(ctx, rest);
|
|
2583
2728
|
return;
|
|
2584
2729
|
}
|
|
2585
2730
|
|