@axplusb/kepler 2.3.0 → 2.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axplusb/kepler",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Kepler — AI coding agent with operating brief, preflight planning, and sub-agents. SWE-bench Lite evaluated.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }
@@ -0,0 +1,287 @@
1
+ function compact(value) {
2
+ return String(value || '').trim();
3
+ }
4
+
5
+ function lower(value) {
6
+ return compact(value).toLowerCase();
7
+ }
8
+
9
+ const PROVIDER_ALIASES = new Map([
10
+ ['openrouter', 'openrouter'],
11
+ ['openrouterv2', 'openrouter'],
12
+ ['openroutergateway', 'openrouter'],
13
+ ['openrouterv2gateway', 'openrouter'],
14
+ ['anthropic', 'anthropic'],
15
+ ['claude', 'anthropic'],
16
+ ['claudegateway', 'anthropic'],
17
+ ['openai', 'openai'],
18
+ ['openaigateway', 'openai'],
19
+ ['google', 'googleai'],
20
+ ['googleai', 'googleai'],
21
+ ['googlegateway', 'googleai'],
22
+ ['googleaigateway', 'googleai'],
23
+ ['gemini', 'googleai'],
24
+ ['azure', 'azureopenai'],
25
+ ['azureopenai', 'azureopenai'],
26
+ ['azureopenaigateway', 'azureopenai'],
27
+ ['bedrock', 'bedrock'],
28
+ ['aws', 'bedrock'],
29
+ ['bedrockgateway', 'bedrock'],
30
+ ['databricks', 'databricks'],
31
+ ['databricksgateway', 'databricks'],
32
+ ['custom', 'custom'],
33
+ ['byom', 'custom'],
34
+ ['openai-compatible', 'custom'],
35
+ ['deepseek', 'deepseek'],
36
+ ['deepseekgateway', 'deepseek'],
37
+ ['dashscope', 'dashscope'],
38
+ ['dashscopegateway', 'dashscope'],
39
+ ['qwen', 'dashscope'],
40
+ ['zhipu', 'zhipu'],
41
+ ['zhipugateway', 'zhipu'],
42
+ ['glm', 'zhipu'],
43
+ ['moonshot', 'moonshot'],
44
+ ['moonshotgateway', 'moonshot'],
45
+ ['kimi', 'moonshot'],
46
+ ['xai', 'xai'],
47
+ ['xaigateway', 'xai'],
48
+ ['grok', 'xai'],
49
+ ['mistral', 'mistral'],
50
+ ['mistralgateway', 'mistral'],
51
+ ]);
52
+
53
+ const PROVIDER_GUIDANCE = {
54
+ openrouter: {
55
+ label: 'OpenRouter',
56
+ lines: [
57
+ 'Check that your OpenRouter API key is saved and has enough credits/quota.',
58
+ 'Verify the selected model is available on OpenRouter and that provider routing is allowed for it.',
59
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
60
+ ],
61
+ },
62
+ anthropic: {
63
+ label: 'Anthropic',
64
+ lines: [
65
+ 'Check that your Anthropic API key is saved and active.',
66
+ 'Verify the selected Claude model is enabled for your Anthropic account and region.',
67
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
68
+ ],
69
+ },
70
+ openai: {
71
+ label: 'OpenAI',
72
+ lines: [
73
+ 'Check that your OpenAI API key is saved and has access to the selected model.',
74
+ 'Verify project billing, model permissions, and rate limits in your OpenAI account.',
75
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
76
+ ],
77
+ },
78
+ googleai: {
79
+ label: 'Google AI',
80
+ lines: [
81
+ 'Check that your Google AI API key is saved and active.',
82
+ 'Verify the selected Gemini model is available for that key and region.',
83
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
84
+ ],
85
+ },
86
+ azureopenai: {
87
+ label: 'Azure OpenAI',
88
+ lines: [
89
+ 'Check that Azure OpenAI API key, endpoint, API version, and deployment/model name are saved.',
90
+ 'Verify the deployment exists in Azure AI Foundry and the key has access to that resource.',
91
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
92
+ ],
93
+ },
94
+ bedrock: {
95
+ label: 'AWS Bedrock',
96
+ lines: [
97
+ 'Check that AWS Access Key ID, Secret Access Key, optional Session Token, and Region are saved.',
98
+ 'Verify the IAM principal can call bedrock-runtime:InvokeModel for the selected model and region.',
99
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
100
+ ],
101
+ },
102
+ databricks: {
103
+ label: 'Databricks',
104
+ lines: [
105
+ 'Check that your Databricks host/workspace URL and token are saved.',
106
+ 'Verify the selected serving endpoint exists and the token can query it.',
107
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
108
+ ],
109
+ },
110
+ custom: {
111
+ label: 'custom OpenAI-compatible provider',
112
+ lines: [
113
+ 'Check that base URL, API key, and model name are saved for the custom provider.',
114
+ 'Verify the endpoint supports OpenAI-compatible chat completions for the selected model.',
115
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
116
+ ],
117
+ },
118
+ deepseek: {
119
+ label: 'DeepSeek',
120
+ lines: [
121
+ 'Check that your DeepSeek API key is saved and active.',
122
+ 'Verify the selected DeepSeek model is available and your account has quota.',
123
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
124
+ ],
125
+ },
126
+ dashscope: {
127
+ label: 'DashScope/Qwen',
128
+ lines: [
129
+ 'Check that your DashScope API key is saved and active.',
130
+ 'Verify the selected Qwen model is available for your account and region.',
131
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
132
+ ],
133
+ },
134
+ zhipu: {
135
+ label: 'Zhipu/GLM',
136
+ lines: [
137
+ 'Check that your Zhipu API key is saved and active.',
138
+ 'Verify the selected GLM model is enabled and your account has quota.',
139
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
140
+ ],
141
+ },
142
+ moonshot: {
143
+ label: 'Moonshot/Kimi',
144
+ lines: [
145
+ 'Check that your Moonshot API key is saved and active.',
146
+ 'Verify the selected Kimi model is enabled and your account has quota.',
147
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
148
+ ],
149
+ },
150
+ xai: {
151
+ label: 'xAI',
152
+ lines: [
153
+ 'Check that your xAI API key is saved and active.',
154
+ 'Verify the selected Grok model is enabled and your account has quota.',
155
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
156
+ ],
157
+ },
158
+ mistral: {
159
+ label: 'Mistral',
160
+ lines: [
161
+ 'Check that your Mistral API key is saved and active.',
162
+ 'Verify the selected Mistral model is enabled and your account has quota.',
163
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
164
+ ],
165
+ },
166
+ };
167
+
168
+ export function isBedrockMissingCredentials(data = {}) {
169
+ const haystack = [
170
+ data.message,
171
+ data.error,
172
+ data.code,
173
+ data.phase,
174
+ data.provider,
175
+ data.gateway,
176
+ data.gateway_type,
177
+ data.exception_type,
178
+ ].map(lower).join(' ');
179
+
180
+ const mentionsBedrock = haystack.includes('bedrock');
181
+ const mentionsAwsCreds = haystack.includes('aws credentials')
182
+ || haystack.includes('aws credential')
183
+ || haystack.includes('aws_access_key')
184
+ || haystack.includes('aws_secret')
185
+ || haystack.includes('access key')
186
+ || haystack.includes('secret access key');
187
+
188
+ return mentionsBedrock && mentionsAwsCreds;
189
+ }
190
+
191
+ export function normalizeGatewayProvider(data = {}) {
192
+ const direct = [
193
+ data.provider,
194
+ data.gateway,
195
+ data.gateway_type,
196
+ data.gatewayType,
197
+ ].map(normalizeProviderToken).find(Boolean);
198
+ if (direct) return direct;
199
+
200
+ const haystack = [
201
+ data.message,
202
+ data.error,
203
+ data.code,
204
+ data.phase,
205
+ data.operation,
206
+ data.exception_type,
207
+ data.model,
208
+ ].map(lower).join(' ');
209
+
210
+ for (const [alias, provider] of PROVIDER_ALIASES.entries()) {
211
+ if (haystack.includes(alias)) return provider;
212
+ }
213
+ return '';
214
+ }
215
+
216
+ export function formatAgentErrorGuidance(data = {}) {
217
+ const message = compact(data.message || data.error || 'Agent execution failed.');
218
+ const code = compact(data.code);
219
+ const phase = compact(data.phase);
220
+ const provider = normalizeGatewayProvider(data);
221
+ const providerMeta = provider || compact(data.provider || data.gateway || data.gateway_type);
222
+ const taskId = compact(data.task_id);
223
+ const retryAfter = data.retry_after != null ? compact(data.retry_after) : '';
224
+ const retryable = data.retryable === true;
225
+
226
+ if (isBedrockMissingCredentials(data)) {
227
+ return {
228
+ title: 'AWS Bedrock credentials are missing.',
229
+ lines: [
230
+ 'Kepler reached the Bedrock gateway, but the backend did not receive AWS Access Key ID and Secret Access Key.',
231
+ 'Open Kepler/AppStak model settings, re-save the AWS Bedrock provider with Access Key ID, Secret Access Key, and Region, then retry.',
232
+ 'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
233
+ ],
234
+ meta: buildMeta({ code, phase, provider: provider || 'bedrock', taskId, retryAfter, retryable }),
235
+ };
236
+ }
237
+
238
+ const lines = [];
239
+ const providerGuidance = PROVIDER_GUIDANCE[provider];
240
+ if (phase === 'gateway' || code.includes('gateway')) {
241
+ const label = providerGuidance?.label || 'provider';
242
+ lines.push(`The ${label} gateway failed before the agent could respond.`);
243
+ if (providerGuidance) {
244
+ lines.push(...providerGuidance.lines);
245
+ } else {
246
+ lines.push('Check the selected provider, model, and BYOK credentials in settings, then retry.');
247
+ }
248
+ } else if (/authentication|token/i.test(message)) {
249
+ lines.push('Run /login to re-authenticate.');
250
+ } else if (/api key|openrouter/i.test(message)) {
251
+ lines.push('Run /config to set up or refresh your provider settings.');
252
+ } else if (/backend|network/i.test(message)) {
253
+ lines.push('Check that the backend is reachable, then retry.');
254
+ }
255
+
256
+ if (retryable && retryAfter) {
257
+ lines.push(`This looks retryable after ${retryAfter}s.`);
258
+ } else if (retryable) {
259
+ lines.push('This looks retryable.');
260
+ }
261
+
262
+ return {
263
+ title: message,
264
+ lines,
265
+ meta: buildMeta({ code, phase, provider: providerMeta, taskId, retryAfter, retryable }),
266
+ };
267
+ }
268
+
269
+ function normalizeProviderToken(value) {
270
+ const raw = lower(value);
271
+ if (!raw) return '';
272
+ const compacted = raw.replace(/[^a-z0-9]/g, '');
273
+ return PROVIDER_ALIASES.get(raw)
274
+ || PROVIDER_ALIASES.get(compacted)
275
+ || '';
276
+ }
277
+
278
+ function buildMeta({ code, phase, provider, taskId, retryAfter, retryable }) {
279
+ const parts = [];
280
+ if (provider) parts.push(`provider=${provider}`);
281
+ if (phase) parts.push(`phase=${phase}`);
282
+ if (code) parts.push(`code=${code}`);
283
+ if (retryable) parts.push(`retryable=true`);
284
+ if (retryAfter) parts.push(`retry_after=${retryAfter}s`);
285
+ if (taskId) parts.push(`task=${taskId}`);
286
+ return parts;
287
+ }
@@ -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() {
@@ -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' — every turn sent verbatim (unchanged)
535
- // 'tail-N' recap block as system prime + last N conversation messages so the
536
- // agent has real recent conversation to reference.
537
- // 'summary' — recap block only. Cheapest continuity, biggest lossiness.
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);
@@ -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':
@@ -40,6 +40,7 @@ import { TarangAuth } from '../auth/tarang-auth.mjs';
40
40
  import { ApprovalManager } from '../core/approval.mjs';
41
41
  import { resolveBackendUrl } from '../core/backend-url.mjs';
42
42
  import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
43
+ import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
43
44
  import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
44
45
  import { SessionManager } from '../core/session-manager.mjs';
45
46
  import { parseArgs } from '../config/cli-args.mjs';
@@ -49,6 +50,7 @@ import { buildContextEnvelope } from '../core/context-envelope.mjs';
49
50
  import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
50
51
  import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
51
52
  import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
53
+ import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
52
54
  import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
53
55
  import { createOrbit } from '../state/orbit.mjs';
54
56
  import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
@@ -196,6 +198,18 @@ function formatResumeCheckpointStatus(session) {
196
198
  return c.dim(` · summarized${pct}`);
197
199
  }
198
200
 
201
+ function formatResumeContextStatus(session) {
202
+ const full = formatCtxTokens(session?.contextTokens || 0);
203
+ const marker = session?.resumeSummary;
204
+ if (!marker || !Number(marker.sourceMessageCount)) {
205
+ return c.dim(`${full.padStart(5, ' ')} ctx`);
206
+ }
207
+ const resumable = formatCtxTokens(projectedTokensForChoice('checkpoint-full', session.contextTokens || 0, {
208
+ resumeSummary: marker,
209
+ }));
210
+ return c.dim(`${resumable.padStart(5, ' ')} resumable · ${full} full`) + formatResumeCheckpointStatus(session);
211
+ }
212
+
199
213
  function formatRelativeTime(iso) {
200
214
  if (!iso) return '';
201
215
  const t = Date.parse(iso);
@@ -250,7 +264,7 @@ async function pickResumableSession(resumable, ctx) {
250
264
  const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
251
265
  const status = endStatusMarker(s.endStatus);
252
266
  const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
253
- const ctx = c.dim(`${formatCtxTokens(s.contextTokens).padStart(5, ' ')} ctx`) + formatResumeCheckpointStatus(s);
267
+ const ctx = formatResumeContextStatus(s);
254
268
  const cost = formatSessionCost(s.costUsd);
255
269
  const partial = s.partial ? c.yellow(' ⚠partial') : '';
256
270
  const instr = oneLineInstruction(s.instruction, 48);
@@ -308,8 +322,14 @@ async function chooseThresholdMode(ctx, decision) {
308
322
  if (rl) rl.pause();
309
323
 
310
324
  const canFull = decision.mode !== 'no-full-allowed';
325
+ const hasCheckpoint = Boolean(decision.resumeSummary?.sourceMessageCount);
326
+ const firstOption = canFull
327
+ ? { key: 'f', value: 'full', label: 'full transcript', enabled: true }
328
+ : hasCheckpoint
329
+ ? { key: 'f', value: 'checkpoint-full', label: 'checkpointed transcript', enabled: true }
330
+ : { key: 'f', value: 'full', label: 'full transcript', enabled: false };
311
331
  const options = [
312
- { key: 'f', value: 'full', label: 'full transcript', enabled: canFull },
332
+ firstOption,
313
333
  { key: 's', value: 'summary', label: 'summary only', enabled: true },
314
334
  { key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
315
335
  { key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
@@ -328,7 +348,8 @@ async function chooseThresholdMode(ctx, decision) {
328
348
  const projected = formatCtxTokens(decision.projected);
329
349
  const win = formatCtxTokens(decision.windowSize);
330
350
  const lines = [];
331
- lines.push(` This session would use ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
351
+ const rawLabel = hasCheckpoint ? 'Raw full transcript would use' : 'This session would use';
352
+ lines.push(` ${rawLabel} ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
332
353
  if (decision.resumeSummary?.sourceMessageCount) {
333
354
  const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
334
355
  const full = Number(decision.resumeSummary.fullMessageCount) || 0;
@@ -337,14 +358,18 @@ async function chooseThresholdMode(ctx, decision) {
337
358
  }
338
359
  lines.push(canFull
339
360
  ? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
340
- : ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
361
+ : hasCheckpoint
362
+ ? ` ${c.yellow('⚠')} ${c.dim('raw full is over hardCap — checkpoint/tail modes are available:')}`
363
+ : ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
341
364
  lines.push('');
342
365
  for (let i = 0; i < options.length; i++) {
343
366
  const o = options[i];
344
367
  const disabled = !o.enabled;
345
368
  const marker = i === selected && !disabled ? c.brand('▸') : ' ';
346
369
  const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
347
- const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected));
370
+ const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected, {
371
+ resumeSummary: decision.resumeSummary,
372
+ }));
348
373
  const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
349
374
  const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
350
375
  const suffix = disabled ? c.dim(' (over hardCap)') : '';
@@ -389,6 +414,7 @@ async function chooseThresholdMode(ctx, decision) {
389
414
  }
390
415
 
391
416
  function resumeModeLabel(mode = 'full') {
417
+ if (mode === 'checkpoint-full') return 'checkpointed transcript';
392
418
  if (mode === 'summary') return 'summary only';
393
419
  const tailTurns = resumeTailTurnCount(mode);
394
420
  if (tailTurns) return `summary + last ${tailTurns} turns`;
@@ -434,7 +460,7 @@ async function previewResumeSession(session, ctx) {
434
460
  if (scrollOffset > maxOffset) scrollOffset = maxOffset;
435
461
 
436
462
  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')}`);
463
+ 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
464
  lines.push(` ${c.dim('─'.repeat(60))}`);
439
465
  for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
440
466
  lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
@@ -460,7 +486,7 @@ async function previewResumeSession(session, ctx) {
460
486
  if (key === '') { scrollOffset += 1; render(); return; }
461
487
  if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
462
488
  if (key === '[6~') { scrollOffset += 10; render(); return; }
463
- if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
489
+ if (low === 'f') { mode = session.resumeSummary?.sourceMessageCount ? 'checkpoint-full' : 'full'; history = rich(); scrollOffset = 0; render(); return; }
464
490
  if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
465
491
  if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
466
492
  if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
@@ -659,6 +685,120 @@ async function summarizeResumeTranscript({
659
685
  }
660
686
  }
661
687
 
688
+ async function compactCurrentSession(ctx, rest = '') {
689
+ const tailCount = parseCompactTailCount(rest, 8);
690
+ const preparedLive = prepareCompactHistory({
691
+ agentHistory: session.agentHistory,
692
+ tailCount,
693
+ });
694
+ if (!preparedLive.ok) {
695
+ process.stderr.write(` ${c.gray(`Nothing to compact — ${preparedLive.reason}.`)}\n`);
696
+ return;
697
+ }
698
+
699
+ const progress = startResumeProgress('compact');
700
+ progress.update('preparing compact summary', 18);
701
+ let sourceMessages = preparedLive.sourceMessages;
702
+ let priorSummary = preparedLive.previousSummary || '';
703
+ let previousSourceMessageCount = 0;
704
+ let fullMessageCount = preparedLive.beforeCount;
705
+ let projectPath = safeCwd();
706
+ let sourceFrom = 'live';
707
+ let summaryWarning = '';
708
+
709
+ try {
710
+ if (session.id && ctx.jsonlWriter?.flush) {
711
+ progress.update('reading transcript checkpoint', 28);
712
+ await ctx.jsonlWriter.flush();
713
+ const detail = await getSessionDetail(session.id, { filePath: ctx.jsonlWriter.transcriptPath });
714
+ if (detail) {
715
+ const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, `tail-${tailCount}`);
716
+ if (richHistory.sourceMessages?.length) {
717
+ sourceMessages = richHistory.sourceMessages;
718
+ priorSummary = richHistory.priorSummary || '';
719
+ previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
720
+ fullMessageCount = richHistory.fullMessageCount || sourceMessages.length;
721
+ projectPath = detail.meta?.project || safeCwd();
722
+ sourceFrom = 'transcript';
723
+ } else if (richHistory.priorSummary) {
724
+ priorSummary = richHistory.priorSummary;
725
+ previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
726
+ fullMessageCount = richHistory.fullMessageCount || preparedLive.beforeCount;
727
+ }
728
+ }
729
+ }
730
+
731
+ if (!sourceMessages.length) {
732
+ progress.stop();
733
+ process.stderr.write(` ${c.gray('Nothing new to compact.')}\n`);
734
+ return;
735
+ }
736
+
737
+ progress.update('summarizing compacted history', 46);
738
+ const backendSummary = await summarizeResumeTranscript({
739
+ auth: ctx.auth,
740
+ toolExecutor: ctx.toolExecutor,
741
+ sessionId: session.id,
742
+ projectPath,
743
+ messages: sourceMessages,
744
+ });
745
+ let summarySource = backendSummary?.summary ? (backendSummary.source || 'backend') : 'local fallback';
746
+ let deltaSummary = backendSummary?.summary || '';
747
+ if (!deltaSummary) {
748
+ summaryWarning = backendSummary?.reason || 'backend summary unavailable';
749
+ deltaSummary = localCompactSummary(sourceMessages);
750
+ }
751
+ const summary = combineResumeSummaries(priorSummary, deltaSummary);
752
+
753
+ progress.update('rewriting live context', 74);
754
+ const applied = applyCompactSummary({
755
+ prepared: { ...preparedLive, sourceMessages },
756
+ summary,
757
+ sessionId: session.id,
758
+ cwd: projectPath,
759
+ originalRequest: session.history.find(m => m.role === 'user')?.content || session.lastTask || '',
760
+ previousSourceMessageCount,
761
+ });
762
+ session.agentHistory = applied.agentHistory;
763
+ session.compactSummary = summary;
764
+ session.compactSourceMessageCount = applied.sourceMessageCount;
765
+
766
+ if (ctx.jsonlWriter) {
767
+ progress.update('writing summary checkpoint', 88);
768
+ ctx.jsonlWriter.writeKeplerEvent({
769
+ type: 'resume_summary',
770
+ data: {
771
+ session_id: session.id || null,
772
+ mode: 'compact',
773
+ mode_label: '/compact',
774
+ summary,
775
+ summary_source: summarySource,
776
+ summary_warning: summaryWarning || null,
777
+ source: sourceFrom,
778
+ source_message_count: applied.sourceMessageCount,
779
+ previous_source_message_count: previousSourceMessageCount,
780
+ full_message_count: fullMessageCount,
781
+ retained_tail_messages: applied.retainedCount,
782
+ live_before_messages: applied.beforeCount,
783
+ live_after_messages: applied.afterCount,
784
+ },
785
+ });
786
+ await ctx.jsonlWriter.flush?.();
787
+ }
788
+
789
+ progress.stop();
790
+ process.stderr.write(
791
+ ` ${c.green('✓')} ${c.dim(`Compacted context: ${applied.beforeCount} → ${applied.afterCount} live messages · retained ${applied.retainedCount} · summary ${summarySource}`)}\n`
792
+ );
793
+ if (summaryWarning) {
794
+ process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — used local summary (${summaryWarning})`)}\n`);
795
+ }
796
+ } catch (err) {
797
+ progress.stop();
798
+ process.stderr.write(` ${c.red(`Compact failed: ${err?.message || String(err)}`)}\n`);
799
+ }
800
+ }
801
+
662
802
  function resumeTailTurnCount(mode = '') {
663
803
  const match = String(mode || '').match(/^tail-(\d+)$/);
664
804
  if (!match) return null;
@@ -1224,10 +1364,12 @@ function updateStatusBar() {
1224
1364
  // buffered head as a regular two-line shape first so the interleaving
1225
1365
  // content lands below it.
1226
1366
  let _pendingHead = null; // { callId, head, indent }
1367
+ let _lastRenderedBlock = null; // 'tool' | 'content' | null
1227
1368
 
1228
1369
  function flushPendingHead() {
1229
1370
  if (!_pendingHead) return;
1230
- process.stderr.write(`\n${_pendingHead.head}\n`);
1371
+ process.stderr.write(`${_pendingHead.head}\n`);
1372
+ _lastRenderedBlock = 'tool';
1231
1373
  _pendingHead = null;
1232
1374
  }
1233
1375
 
@@ -1312,7 +1454,8 @@ function renderToolResult(data, eventType = 'tool_result') {
1312
1454
  const cols = process.stderr.columns || 120;
1313
1455
  const combined = `${_pendingHead.head} ${outcome}`;
1314
1456
  if (stripAnsi(combined).length <= cols) {
1315
- process.stderr.write(`\n${combined}\n`);
1457
+ process.stderr.write(`${combined}\n`);
1458
+ _lastRenderedBlock = 'tool';
1316
1459
  _pendingHead = null;
1317
1460
  return;
1318
1461
  }
@@ -1326,6 +1469,7 @@ function renderToolResult(data, eventType = 'tool_result') {
1326
1469
 
1327
1470
  // Two-line shape: gutter under the (already-printed or just-flushed) head.
1328
1471
  process.stderr.write(`${gutter}${outcome}\n`);
1472
+ _lastRenderedBlock = 'tool';
1329
1473
 
1330
1474
  // Lint warnings stay visible alongside writes.
1331
1475
  if (hasLint) {
@@ -1448,6 +1592,7 @@ function startContentStream() {
1448
1592
  _streamedPartialText = '';
1449
1593
  _renderedToolResults.clear();
1450
1594
  _renderedContentThisTurn = false;
1595
+ _lastRenderedBlock = null;
1451
1596
  stopSpinner();
1452
1597
  }
1453
1598
 
@@ -1472,12 +1617,14 @@ function flushContent() {
1472
1617
  // Any buffered tool head needs to land BEFORE this content so the order
1473
1618
  // is preserved on screen.
1474
1619
  flushPendingHead();
1620
+ if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
1475
1621
  const rendered = renderMarkdown(_streamBuffer);
1476
1622
  for (const line of rendered.split('\n')) {
1477
1623
  process.stdout.write(` ${line}\n`);
1478
1624
  }
1479
1625
  _streamBuffer = '';
1480
1626
  _renderedContentThisTurn = true;
1627
+ _lastRenderedBlock = 'content';
1481
1628
  }
1482
1629
 
1483
1630
  // ── Event Renderer ──
@@ -1529,11 +1676,13 @@ function renderEvent(event) {
1529
1676
  }
1530
1677
  }
1531
1678
  if (text) {
1679
+ if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
1532
1680
  const rendered = renderMarkdown(text);
1533
1681
  for (const line of rendered.split('\n')) {
1534
1682
  process.stdout.write(` ${line}\n`);
1535
1683
  }
1536
1684
  _renderedContentThisTurn = true;
1685
+ _lastRenderedBlock = 'content';
1537
1686
  }
1538
1687
  break;
1539
1688
  }
@@ -1760,9 +1909,15 @@ function renderEvent(event) {
1760
1909
  case 'error':
1761
1910
  stopSpinner();
1762
1911
  flushContent();
1763
- process.stderr.write(`\n ${c.red('✗')} ${data?.message || 'Unknown error'}\n`);
1764
- if ((data?.message || '').includes('Authentication')) {
1765
- process.stderr.write(` ${c.dim('Run /login to re-authenticate')}\n`);
1912
+ {
1913
+ const guidance = formatAgentErrorGuidance(data || {});
1914
+ process.stderr.write(`\n ${c.red('')} ${guidance.title}\n`);
1915
+ for (const line of guidance.lines) {
1916
+ process.stderr.write(` ${c.dim(line)}\n`);
1917
+ }
1918
+ if (guidance.meta.length) {
1919
+ process.stderr.write(` ${c.dim(guidance.meta.join(' · '))}\n`);
1920
+ }
1766
1921
  }
1767
1922
  break;
1768
1923
 
@@ -2576,10 +2731,7 @@ async function handleCommand(input, ctx) {
2576
2731
  }
2577
2732
 
2578
2733
  case '/compact': {
2579
- const before = session.agentHistory.length || session.history.length;
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`);
2734
+ await compactCurrentSession(ctx, rest);
2583
2735
  return;
2584
2736
  }
2585
2737
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
12
12
  import { formatMessageWindow } from '../core/rate-limit-display.mjs';
13
+ import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
13
14
 
14
15
  const RESET = '\x1b[0m';
15
16
  const BOLD = '\x1b[1m';
@@ -309,16 +310,13 @@ export class EventFormatter {
309
310
  }
310
311
 
311
312
  _error(data) {
312
- const msg = data?.message || 'Unknown error';
313
- process.stderr.write(`\n ${RED}✗ ${msg}${RESET}\n`);
314
-
315
- // Helpful suggestions for common errors
316
- if (msg.includes('Authentication') || msg.includes('token')) {
317
- process.stderr.write(` ${DIM}Run /login to re-authenticate${RESET}\n`);
318
- } else if (msg.includes('API key') || msg.includes('OpenRouter')) {
319
- process.stderr.write(` ${DIM}Run /config to set up your provider${RESET}\n`);
320
- } else if (msg.includes('Backend') || msg.includes('Network')) {
321
- process.stderr.write(` ${DIM}Check if the backend is running at ${this.sessionInfo?.backend || 'localhost:8150'}${RESET}\n`);
313
+ const guidance = formatAgentErrorGuidance(data || {});
314
+ process.stderr.write(`\n ${RED}✗ ${guidance.title}${RESET}\n`);
315
+ for (const line of guidance.lines) {
316
+ process.stderr.write(` ${DIM}${line}${RESET}\n`);
317
+ }
318
+ if (guidance.meta.length) {
319
+ process.stderr.write(` ${DIM}${guidance.meta.join(' · ')}${RESET}\n`);
322
320
  }
323
321
  }
324
322