@ekkos/cli 0.2.6 → 0.2.8

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,340 @@
1
+ "use strict";
2
+ /**
3
+ * ekkos stream - Stream capture status and management
4
+ *
5
+ * Shows live status of stream capture for debugging and support.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.streamStatus = streamStatus;
45
+ exports.streamList = streamList;
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const os = __importStar(require("os"));
50
+ const STREAM_CACHE_DIR = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
51
+ /**
52
+ * Get the state machine file path for a session
53
+ */
54
+ function getStateMachinePath(sessionId) {
55
+ return path.join(STREAM_CACHE_DIR, `${sessionId}.state.json`);
56
+ }
57
+ /**
58
+ * Get the checkpoint file path for a session
59
+ */
60
+ function getCheckpointPath(sessionId) {
61
+ return path.join(STREAM_CACHE_DIR, `${sessionId}.checkpoint.json`);
62
+ }
63
+ /**
64
+ * Get the stream log path for a session
65
+ */
66
+ function getStreamLogPath(sessionId) {
67
+ return path.join(STREAM_CACHE_DIR, `${sessionId}.stream.jsonl`);
68
+ }
69
+ /**
70
+ * Find most recent session with stream data
71
+ */
72
+ function findMostRecentSession() {
73
+ try {
74
+ if (!fs.existsSync(STREAM_CACHE_DIR))
75
+ return null;
76
+ const files = fs.readdirSync(STREAM_CACHE_DIR)
77
+ .filter(f => f.endsWith('.stream.jsonl'))
78
+ .map(f => ({
79
+ sessionId: f.replace('.stream.jsonl', ''),
80
+ mtime: fs.statSync(path.join(STREAM_CACHE_DIR, f)).mtimeMs
81
+ }))
82
+ .sort((a, b) => b.mtime - a.mtime);
83
+ return files.length > 0 ? files[0].sessionId : null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * Load stream state machine from disk
91
+ */
92
+ function loadStateMachine(sessionId) {
93
+ try {
94
+ const statePath = getStateMachinePath(sessionId);
95
+ if (fs.existsSync(statePath)) {
96
+ return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
97
+ }
98
+ }
99
+ catch {
100
+ // Ignore errors
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Load checkpoint from disk
106
+ */
107
+ function loadCheckpoint(sessionId) {
108
+ try {
109
+ const checkpointPath = getCheckpointPath(sessionId);
110
+ if (fs.existsSync(checkpointPath)) {
111
+ return JSON.parse(fs.readFileSync(checkpointPath, 'utf-8'));
112
+ }
113
+ }
114
+ catch {
115
+ // Ignore errors
116
+ }
117
+ return null;
118
+ }
119
+ /**
120
+ * Get stream log stats
121
+ */
122
+ function getStreamLogStats(sessionId) {
123
+ const logPath = getStreamLogPath(sessionId);
124
+ if (!fs.existsSync(logPath)) {
125
+ return { exists: false, size: 0, lines: 0 };
126
+ }
127
+ const stat = fs.statSync(logPath);
128
+ let lines = 0;
129
+ try {
130
+ const content = fs.readFileSync(logPath, 'utf-8');
131
+ lines = content.split('\n').filter(l => l.trim()).length;
132
+ }
133
+ catch {
134
+ // Ignore errors
135
+ }
136
+ return { exists: true, size: stat.size, lines };
137
+ }
138
+ /**
139
+ * Format state with color
140
+ */
141
+ function formatState(state) {
142
+ switch (state) {
143
+ case 'idle':
144
+ return chalk_1.default.gray('● idle');
145
+ case 'assistant_streaming':
146
+ return chalk_1.default.green('◉ streaming');
147
+ case 'tool_running':
148
+ return chalk_1.default.yellow('◉ tool running');
149
+ case 'interrupted':
150
+ return chalk_1.default.red('○ interrupted');
151
+ case 'wall_hit':
152
+ return chalk_1.default.red('✗ wall hit');
153
+ default:
154
+ return chalk_1.default.gray('? unknown');
155
+ }
156
+ }
157
+ /**
158
+ * Format bytes as human-readable
159
+ */
160
+ function formatBytes(bytes) {
161
+ if (bytes < 1024)
162
+ return `${bytes} B`;
163
+ if (bytes < 1024 * 1024)
164
+ return `${(bytes / 1024).toFixed(1)} KB`;
165
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
166
+ }
167
+ /**
168
+ * Format timestamp as relative time
169
+ */
170
+ function formatRelativeTime(ts) {
171
+ const now = Date.now();
172
+ const then = new Date(ts).getTime();
173
+ const diff = now - then;
174
+ if (diff < 1000)
175
+ return 'just now';
176
+ if (diff < 60000)
177
+ return `${Math.floor(diff / 1000)}s ago`;
178
+ if (diff < 3600000)
179
+ return `${Math.floor(diff / 60000)}m ago`;
180
+ if (diff < 86400000)
181
+ return `${Math.floor(diff / 3600000)}h ago`;
182
+ return `${Math.floor(diff / 86400000)}d ago`;
183
+ }
184
+ /**
185
+ * Display stream status
186
+ */
187
+ async function streamStatus(options) {
188
+ const sessionId = options.session || findMostRecentSession();
189
+ if (!sessionId) {
190
+ console.log(chalk_1.default.yellow('No active stream sessions found.'));
191
+ console.log(chalk_1.default.gray('Start a session with: ekkos run'));
192
+ return;
193
+ }
194
+ const state = loadStateMachine(sessionId);
195
+ const checkpoint = loadCheckpoint(sessionId);
196
+ const logStats = getStreamLogStats(sessionId);
197
+ if (options.json) {
198
+ console.log(JSON.stringify({
199
+ session_id: sessionId,
200
+ state,
201
+ checkpoint,
202
+ log_stats: logStats,
203
+ }, null, 2));
204
+ return;
205
+ }
206
+ // Header
207
+ console.log('');
208
+ console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════'));
209
+ console.log(chalk_1.default.cyan.bold(' ekkOS Stream Status'));
210
+ console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════'));
211
+ console.log('');
212
+ // Session info
213
+ console.log(chalk_1.default.white.bold('Session'));
214
+ console.log(chalk_1.default.gray(` ID: ${sessionId}`));
215
+ if (state?.session_name) {
216
+ console.log(chalk_1.default.gray(` Name: ${state.session_name}`));
217
+ }
218
+ console.log('');
219
+ // State machine
220
+ console.log(chalk_1.default.white.bold('State Machine'));
221
+ if (state) {
222
+ console.log(` State: ${formatState(state.state)}`);
223
+ console.log(chalk_1.default.gray(` Entered: ${formatRelativeTime(state.state_entered_at)}`));
224
+ console.log(chalk_1.default.gray(` Current turn: ${state.current_turn_id}`));
225
+ console.log(chalk_1.default.gray(` Last complete: ${state.last_complete_turn_id}`));
226
+ }
227
+ else {
228
+ console.log(chalk_1.default.gray(' No state machine found'));
229
+ }
230
+ console.log('');
231
+ // In-progress content
232
+ if (state && state.in_progress_total_chars > 0) {
233
+ console.log(chalk_1.default.white.bold('In-Progress Content'));
234
+ console.log(chalk_1.default.gray(` Total chars: ${state.in_progress_total_chars.toLocaleString()}`));
235
+ if (state.in_progress_text_head) {
236
+ console.log(chalk_1.default.gray(` Head (512 chars): "${state.in_progress_text_head.slice(0, 60)}..."`));
237
+ }
238
+ if (state.in_progress_text_tail) {
239
+ const tailPreview = state.in_progress_text_tail.slice(-60);
240
+ console.log(chalk_1.default.gray(` Tail (8KB): "...${tailPreview}"`));
241
+ }
242
+ console.log('');
243
+ }
244
+ // Open loops
245
+ if (state && state.open_loops.length > 0) {
246
+ console.log(chalk_1.default.yellow.bold('⚠️ Open Loops'));
247
+ for (const loop of state.open_loops) {
248
+ console.log(chalk_1.default.yellow(` • ${loop.type}: ${loop.name} (${formatRelativeTime(loop.started_at)})`));
249
+ }
250
+ console.log('');
251
+ }
252
+ // Stream log stats
253
+ console.log(chalk_1.default.white.bold('Stream Log'));
254
+ if (logStats.exists) {
255
+ console.log(chalk_1.default.gray(` Path: ${getStreamLogPath(sessionId)}`));
256
+ console.log(chalk_1.default.gray(` Size: ${formatBytes(logStats.size)}`));
257
+ console.log(chalk_1.default.gray(` Events: ${logStats.lines.toLocaleString()}`));
258
+ }
259
+ else {
260
+ console.log(chalk_1.default.gray(' No stream log found'));
261
+ }
262
+ console.log('');
263
+ // Checkpoint
264
+ console.log(chalk_1.default.white.bold('Last Checkpoint'));
265
+ if (checkpoint) {
266
+ console.log(chalk_1.default.gray(` Time: ${formatRelativeTime(checkpoint.checkpoint_ts)}`));
267
+ console.log(chalk_1.default.gray(` Turn: ${checkpoint.turn_id}`));
268
+ console.log(chalk_1.default.gray(` Chars: ${checkpoint.total_chars.toLocaleString()}`));
269
+ console.log(chalk_1.default.gray(` Events since: ${checkpoint.events_since_last_checkpoint}`));
270
+ }
271
+ else {
272
+ console.log(chalk_1.default.gray(' No checkpoint found'));
273
+ }
274
+ console.log('');
275
+ // Metrics
276
+ if (state) {
277
+ console.log(chalk_1.default.white.bold('Metrics'));
278
+ console.log(chalk_1.default.gray(` Bytes captured: ${formatBytes(state.stream_bytes_captured)}`));
279
+ console.log(chalk_1.default.gray(` Events captured: ${state.events_captured.toLocaleString()}`));
280
+ console.log(chalk_1.default.gray(` Last event: ${formatRelativeTime(state.last_event_ts)}`));
281
+ console.log('');
282
+ }
283
+ // Footer
284
+ console.log(chalk_1.default.cyan.bold('═══════════════════════════════════════════════════════════════'));
285
+ console.log('');
286
+ // Watch mode
287
+ if (options.watch) {
288
+ console.log(chalk_1.default.gray('Watching for changes... (Ctrl+C to exit)'));
289
+ const interval = setInterval(async () => {
290
+ console.clear();
291
+ await streamStatus({ ...options, watch: false });
292
+ console.log(chalk_1.default.gray('Watching for changes... (Ctrl+C to exit)'));
293
+ }, 1000);
294
+ process.on('SIGINT', () => {
295
+ clearInterval(interval);
296
+ process.exit(0);
297
+ });
298
+ // Keep process alive
299
+ await new Promise(() => { });
300
+ }
301
+ }
302
+ /**
303
+ * List all sessions with stream data
304
+ */
305
+ function streamList() {
306
+ console.log('');
307
+ console.log(chalk_1.default.cyan.bold('Stream Sessions'));
308
+ console.log('');
309
+ if (!fs.existsSync(STREAM_CACHE_DIR)) {
310
+ console.log(chalk_1.default.gray('No stream cache directory found.'));
311
+ return;
312
+ }
313
+ const files = fs.readdirSync(STREAM_CACHE_DIR)
314
+ .filter(f => f.endsWith('.stream.jsonl'))
315
+ .map(f => {
316
+ const sessionId = f.replace('.stream.jsonl', '');
317
+ const stat = fs.statSync(path.join(STREAM_CACHE_DIR, f));
318
+ const state = loadStateMachine(sessionId);
319
+ return {
320
+ sessionId,
321
+ sessionName: state?.session_name,
322
+ state: state?.state || 'unknown',
323
+ mtime: stat.mtimeMs,
324
+ size: stat.size,
325
+ };
326
+ })
327
+ .sort((a, b) => b.mtime - a.mtime);
328
+ if (files.length === 0) {
329
+ console.log(chalk_1.default.gray('No stream sessions found.'));
330
+ return;
331
+ }
332
+ for (const file of files) {
333
+ const name = file.sessionName || file.sessionId.slice(0, 8);
334
+ const stateStr = formatState(file.state);
335
+ const sizeStr = formatBytes(file.size);
336
+ const timeStr = formatRelativeTime(new Date(file.mtime).toISOString());
337
+ console.log(` ${stateStr} ${chalk_1.default.white(name)} ${chalk_1.default.gray(`(${sizeStr}, ${timeStr})`)}`);
338
+ }
339
+ console.log('');
340
+ }
package/dist/index.js CHANGED
@@ -10,11 +10,13 @@ const test_1 = require("./commands/test");
10
10
  const status_1 = require("./commands/status");
11
11
  const run_1 = require("./commands/run");
12
12
  const doctor_1 = require("./commands/doctor");
13
+ const stream_1 = require("./commands/stream");
14
+ const state_1 = require("./utils/state");
13
15
  const chalk_1 = __importDefault(require("chalk"));
14
16
  commander_1.program
15
17
  .name('ekkos')
16
18
  .description('ekkOS memory CLI for AI coding assistants')
17
- .version('0.2.0')
19
+ .version('0.2.8')
18
20
  .addHelpText('beforeAll', chalk_1.default.cyan('\n made by ekkOS_ with ❤️\n'));
19
21
  // Main init command (combined auth + setup)
20
22
  commander_1.program
@@ -61,6 +63,61 @@ commander_1.program
61
63
  .action((options) => {
62
64
  (0, doctor_1.doctor)({ fix: options.fix, json: options.json });
63
65
  });
66
+ // Stream command - stream capture status and management
67
+ const streamCmd = commander_1.program
68
+ .command('stream')
69
+ .description('Stream capture status and management');
70
+ streamCmd
71
+ .command('status')
72
+ .description('Show stream capture status for current session')
73
+ .option('-s, --session <id>', 'Session ID to check')
74
+ .option('-w, --watch', 'Watch mode - refresh every second')
75
+ .option('-j, --json', 'Output machine-readable JSON')
76
+ .action((options) => {
77
+ (0, stream_1.streamStatus)({
78
+ session: options.session,
79
+ watch: options.watch,
80
+ json: options.json
81
+ });
82
+ });
83
+ streamCmd
84
+ .command('list')
85
+ .description('List all sessions with stream data')
86
+ .action(() => {
87
+ (0, stream_1.streamList)();
88
+ });
89
+ // Sessions command - list active Claude Code sessions (swarm support)
90
+ commander_1.program
91
+ .command('sessions')
92
+ .description('List active Claude Code sessions (for swarm/multi-session support)')
93
+ .option('-j, --json', 'Output machine-readable JSON')
94
+ .action((options) => {
95
+ const sessions = (0, state_1.getActiveSessions)();
96
+ if (options.json) {
97
+ console.log(JSON.stringify(sessions, null, 2));
98
+ return;
99
+ }
100
+ console.log('');
101
+ console.log(chalk_1.default.cyan.bold('🐝 Active ekkOS Sessions'));
102
+ console.log('');
103
+ if (sessions.length === 0) {
104
+ console.log(chalk_1.default.gray(' No active sessions found.'));
105
+ console.log(chalk_1.default.gray(' Run `ekkos run` to start a session.'));
106
+ }
107
+ else {
108
+ console.log(chalk_1.default.gray(` ${sessions.length} active session${sessions.length > 1 ? 's' : ''}:`));
109
+ console.log('');
110
+ for (const session of sessions) {
111
+ const age = Math.round((Date.now() - new Date(session.startedAt).getTime()) / 1000 / 60);
112
+ const lastBeat = Math.round((Date.now() - new Date(session.lastHeartbeat).getTime()) / 1000);
113
+ console.log(` ${chalk_1.default.green('●')} ${chalk_1.default.bold(session.sessionName)}`);
114
+ console.log(` ${chalk_1.default.gray('PID:')} ${session.pid}`);
115
+ console.log(` ${chalk_1.default.gray('Path:')} ${session.projectPath}`);
116
+ console.log(` ${chalk_1.default.gray('Age:')} ${age} min ${chalk_1.default.gray('Last seen:')} ${lastBeat}s ago`);
117
+ console.log('');
118
+ }
119
+ }
120
+ });
64
121
  // Deprecated setup command (redirects to init)
65
122
  commander_1.program
66
123
  .command('setup')
@@ -20,6 +20,12 @@ export declare class RestoreOrchestrator {
20
20
  * Main restore function - attempts tiers in order
21
21
  */
22
22
  restore(options?: RestoreOptions): Promise<CacheResult<RestorePayload>>;
23
+ /**
24
+ * Tier -1: Restore from stream log (has mid-turn content)
25
+ * This is checked FIRST because stream logs have the most recent data,
26
+ * including in-progress turns that haven't been sealed yet.
27
+ */
28
+ private restoreFromStreamLog;
23
29
  /**
24
30
  * Tier 0: Restore from local JSONL cache
25
31
  */
@@ -49,9 +49,11 @@ const path = __importStar(require("path"));
49
49
  const os = __importStar(require("os"));
50
50
  const LocalSessionStore_js_1 = require("../cache/LocalSessionStore.js");
51
51
  const types_js_1 = require("../cache/types.js");
52
+ const stream_tailer_js_1 = require("../capture/stream-tailer.js");
52
53
  // API configuration
53
54
  const MEMORY_API_URL = process.env.EKKOS_API_URL || 'https://api.ekkos.dev';
54
55
  const CONFIG_PATH = path.join(os.homedir(), '.ekkos', 'config.json');
56
+ const STREAM_CACHE_DIR = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
55
57
  /**
56
58
  * Load auth token from config
57
59
  */
@@ -102,6 +104,14 @@ class RestoreOrchestrator {
102
104
  latency_ms: Date.now() - startTime,
103
105
  };
104
106
  }
107
+ // Try Tier -1: Stream log (has mid-turn content, most recent data)
108
+ const streamResult = await this.restoreFromStreamLog(sessionId, sessionName || '', lastN);
109
+ if (streamResult.success && streamResult.data) {
110
+ return {
111
+ ...streamResult,
112
+ latency_ms: Date.now() - startTime,
113
+ };
114
+ }
105
115
  // Try Tier 0: Local cache
106
116
  const localResult = await this.restoreFromLocal(sessionId, sessionName || '', lastN);
107
117
  if (localResult.success && localResult.data) {
@@ -133,6 +143,117 @@ class RestoreOrchestrator {
133
143
  latency_ms: Date.now() - startTime,
134
144
  };
135
145
  }
146
+ /**
147
+ * Tier -1: Restore from stream log (has mid-turn content)
148
+ * This is checked FIRST because stream logs have the most recent data,
149
+ * including in-progress turns that haven't been sealed yet.
150
+ */
151
+ async restoreFromStreamLog(sessionId, sessionName, lastN) {
152
+ const startTime = Date.now();
153
+ // Find stream log file
154
+ const streamLogPath = path.join(STREAM_CACHE_DIR, `${sessionId}.stream.jsonl`);
155
+ if (!fs.existsSync(streamLogPath)) {
156
+ return {
157
+ success: false,
158
+ error: 'No stream log found',
159
+ source: 'stream',
160
+ latency_ms: Date.now() - startTime,
161
+ };
162
+ }
163
+ try {
164
+ const { turns: streamTurns, latestTurnId } = await (0, stream_tailer_js_1.reconstructTurnFromEvents)(streamLogPath);
165
+ if (streamTurns.size === 0) {
166
+ return {
167
+ success: false,
168
+ error: 'Stream log empty',
169
+ source: 'stream',
170
+ latency_ms: Date.now() - startTime,
171
+ };
172
+ }
173
+ // Get the latest turn (may be in_progress)
174
+ const latestTurn = streamTurns.get(latestTurnId);
175
+ if (!latestTurn) {
176
+ return {
177
+ success: false,
178
+ error: 'Could not find latest turn in stream',
179
+ source: 'stream',
180
+ latency_ms: Date.now() - startTime,
181
+ };
182
+ }
183
+ // Check if latest turn is in_progress (mid-generation)
184
+ const isInProgress = latestTurn.status === 'in_progress';
185
+ // Build turns array
186
+ const turnsArray = [];
187
+ for (const [turnId, turn] of streamTurns) {
188
+ turnsArray.push({
189
+ turn_id: turnId,
190
+ ts: new Date().toISOString(),
191
+ user_query: turn.user_query,
192
+ assistant_response: turn.assistant_stream, // Use stream content, not sealed response
193
+ tools_used: turn.tools.filter(t => t.kind === 'tool_use').map(t => t.name || 'unknown'),
194
+ files_referenced: [],
195
+ is_complete: turn.status === 'complete',
196
+ });
197
+ }
198
+ // Sort by turn_id
199
+ turnsArray.sort((a, b) => a.turn_id - b.turn_id);
200
+ // Get last N turns
201
+ const restoredTurns = turnsArray.slice(-lastN);
202
+ // For in-progress turns, apply middle-truncation (preserve tail)
203
+ let latestAssistantStream = latestTurn.assistant_stream;
204
+ if (isInProgress && latestAssistantStream.length > 12000) {
205
+ latestAssistantStream = (0, stream_tailer_js_1.middleTruncate)(latestAssistantStream, {
206
+ headChars: 2000,
207
+ tailChars: 8000,
208
+ maxTotal: 12000,
209
+ });
210
+ }
211
+ // Detect open loops (incomplete tools)
212
+ const openLoops = (0, stream_tailer_js_1.detectOpenLoops)(latestTurn.tools);
213
+ const payload = {
214
+ session_id: sessionId,
215
+ session_name: sessionName || 'unknown',
216
+ source: 'stream',
217
+ restored_turns: restoredTurns,
218
+ latest: {
219
+ user_query: latestTurn.user_query,
220
+ assistant_response: latestAssistantStream,
221
+ },
222
+ pending_turn: isInProgress
223
+ ? {
224
+ turn_id: latestTurnId,
225
+ user_query: latestTurn.user_query,
226
+ ts: new Date().toISOString(),
227
+ }
228
+ : undefined,
229
+ directives: [],
230
+ patterns: [],
231
+ metadata: {
232
+ acked_turn_id: latestTurnId,
233
+ last_flush_ts: new Date().toISOString(),
234
+ token_estimate: Math.ceil(latestAssistantStream.length / 4),
235
+ complete_turn_count: turnsArray.filter(t => t.is_complete).length,
236
+ has_pending: isInProgress,
237
+ is_stream_restore: true,
238
+ open_loops: openLoops,
239
+ },
240
+ };
241
+ return {
242
+ success: true,
243
+ data: payload,
244
+ source: 'stream',
245
+ latency_ms: Date.now() - startTime,
246
+ };
247
+ }
248
+ catch (err) {
249
+ return {
250
+ success: false,
251
+ error: err instanceof Error ? err.message : String(err),
252
+ source: 'stream',
253
+ latency_ms: Date.now() - startTime,
254
+ };
255
+ }
256
+ }
136
257
  /**
137
258
  * Tier 0: Restore from local JSONL cache
138
259
  */
@@ -432,35 +553,66 @@ class RestoreOrchestrator {
432
553
  * Format RestorePayload as system-reminder markdown
433
554
  */
434
555
  formatAsSystemReminder(payload) {
556
+ const isStreamRestore = payload.metadata?.is_stream_restore;
557
+ const isInProgress = payload.metadata?.has_pending && isStreamRestore;
558
+ const openLoops = payload.metadata?.open_loops;
435
559
  const lines = [
436
560
  '<system-reminder>',
437
561
  'CONTEXT RESTORED (ekkOS /continue)',
438
562
  `Session: ${payload.session_name} (${payload.session_id})`,
439
- `Source: ${payload.source}`,
440
- `Turns restored: ${payload.restored_turns.length}`,
441
- '',
442
- '## Last User Request',
443
- payload.latest.user_query,
444
- '',
445
- '## Last Assistant Response',
446
- payload.latest.assistant_response.slice(0, 2000),
447
- payload.latest.assistant_response.length > 2000 ? '\n[...truncated...]' : '',
563
+ `Source: ${payload.source}${isStreamRestore ? ' (real-time stream)' : ''}`,
564
+ isInProgress ? `Status: IN_PROGRESS (mid-turn restore)` : `Turns restored: ${payload.restored_turns.length}`,
448
565
  '',
449
- '## Recent Turns (older → newer)',
450
566
  ];
451
- // Add turn summaries (skip last one since it's shown in detail above)
452
- const turnsToShow = payload.restored_turns.slice(0, -1);
453
- for (let i = 0; i < turnsToShow.length; i++) {
454
- const turn = turnsToShow[i];
455
- const userSnippet = turn.user_query.slice(0, 100) + (turn.user_query.length > 100 ? '...' : '');
456
- const assistantSnippet = turn.assistant_response.slice(0, 100) + (turn.assistant_response.length > 100 ? '...' : '');
457
- lines.push(`${i + 1}) U: ${userSnippet}`);
458
- lines.push(` A: ${assistantSnippet}`);
567
+ // For stream restores with in-progress turns, show special format
568
+ if (isInProgress && isStreamRestore) {
569
+ // Show open loops if any
570
+ if (openLoops && openLoops.length > 0) {
571
+ lines.push('## Open Loops (machine-derived)');
572
+ for (const loop of openLoops) {
573
+ lines.push(`- ⚠️ ${loop.detail}`);
574
+ }
575
+ lines.push('');
576
+ }
577
+ // Show last user request
578
+ lines.push('## Last User Request');
579
+ lines.push(payload.latest.user_query);
580
+ lines.push('');
581
+ // Show assistant stream with tail preserved
582
+ lines.push('## Assistant Stream (tail-preserved for resume)');
583
+ lines.push(payload.latest.assistant_response);
584
+ lines.push('');
585
+ // Special instruction for mid-turn resume
586
+ lines.push('INSTRUCTION: Continue from the exact point shown above.');
587
+ lines.push('Do not recap. Do not restart. Continue the sentence/action if mid-sentence.');
588
+ lines.push('Start your response with: "✓ Continuing -"');
589
+ }
590
+ else {
591
+ // Standard restore format
592
+ lines.push('## Last User Request');
593
+ lines.push(payload.latest.user_query);
594
+ lines.push('');
595
+ lines.push('## Last Assistant Response');
596
+ lines.push(payload.latest.assistant_response.slice(0, 2000));
597
+ if (payload.latest.assistant_response.length > 2000) {
598
+ lines.push('\n[...truncated...]');
599
+ }
600
+ lines.push('');
601
+ lines.push('## Recent Turns (older → newer)');
602
+ // Add turn summaries (skip last one since it's shown in detail above)
603
+ const turnsToShow = payload.restored_turns.slice(0, -1);
604
+ for (let i = 0; i < turnsToShow.length; i++) {
605
+ const turn = turnsToShow[i];
606
+ const userSnippet = turn.user_query.slice(0, 100) + (turn.user_query.length > 100 ? '...' : '');
607
+ const assistantSnippet = turn.assistant_response.slice(0, 100) + (turn.assistant_response.length > 100 ? '...' : '');
608
+ lines.push(`${i + 1}) U: ${userSnippet}`);
609
+ lines.push(` A: ${assistantSnippet}`);
610
+ }
611
+ lines.push('');
612
+ lines.push('INSTRUCTION: Resume seamlessly where you left off.');
613
+ lines.push('Do not ask "what were we doing?"');
614
+ lines.push('Start your response with: "✓ Continuing -"');
459
615
  }
460
- lines.push('');
461
- lines.push('INSTRUCTION: Resume seamlessly where you left off.');
462
- lines.push('Do not ask "what were we doing?"');
463
- lines.push('Start your response with: "✓ Continuing -"');
464
616
  lines.push('</system-reminder>');
465
617
  return lines.join('\n');
466
618
  }