@ekkos/cli 0.2.9 → 0.2.10

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.
Files changed (35) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +34 -21
  2. package/dist/cache/LocalSessionStore.js +169 -53
  3. package/dist/cache/capture.d.ts +19 -11
  4. package/dist/cache/capture.js +243 -76
  5. package/dist/cache/types.d.ts +14 -1
  6. package/dist/commands/doctor.d.ts +10 -0
  7. package/dist/commands/doctor.js +148 -73
  8. package/dist/commands/hooks.d.ts +109 -0
  9. package/dist/commands/hooks.js +668 -0
  10. package/dist/commands/run.d.ts +1 -0
  11. package/dist/commands/run.js +69 -21
  12. package/dist/index.js +42 -1
  13. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  14. package/dist/restore/RestoreOrchestrator.js +64 -22
  15. package/dist/utils/paths.d.ts +125 -0
  16. package/dist/utils/paths.js +283 -0
  17. package/package.json +1 -1
  18. package/templates/ekkos-manifest.json +223 -0
  19. package/templates/helpers/json-parse.cjs +101 -0
  20. package/templates/hooks/assistant-response.ps1 +256 -0
  21. package/templates/hooks/assistant-response.sh +124 -64
  22. package/templates/hooks/session-start.ps1 +107 -2
  23. package/templates/hooks/session-start.sh +201 -166
  24. package/templates/hooks/stop.ps1 +124 -3
  25. package/templates/hooks/stop.sh +470 -843
  26. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  27. package/templates/hooks/user-prompt-submit.sh +403 -393
  28. package/templates/project-stubs/session-start.ps1 +63 -0
  29. package/templates/project-stubs/session-start.sh +55 -0
  30. package/templates/project-stubs/stop.ps1 +63 -0
  31. package/templates/project-stubs/stop.sh +55 -0
  32. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  33. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  34. package/templates/shared/hooks-enabled.json +22 -0
  35. package/templates/shared/session-words.json +45 -0
@@ -62,9 +62,10 @@ function getConfig(options) {
62
62
  postEnterDelayMs: options.postEnterDelayMs ??
63
63
  parseInt(process.env.EKKOS_POST_ENTER_DELAY_MS || '300', 10), // was 500
64
64
  clearWaitMs: options.clearWaitMs ??
65
- parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '2500', 10), // was 5000
65
+ parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '1111', 10), // 1111 = symbolic ✨
66
66
  idlePromptMs: parseInt(process.env.EKKOS_IDLE_PROMPT_MS || '250', 10),
67
67
  paletteRetryMs: parseInt(process.env.EKKOS_PALETTE_RETRY_MS || '400', 10), // was 500
68
+ maxIdleWaitMs: parseInt(process.env.EKKOS_MAX_IDLE_WAIT_MS || '2000', 10), // was 10000
68
69
  debugLogPath: options.debugLogPath ??
69
70
  process.env.EKKOS_DEBUG_LOG_PATH ??
70
71
  path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log')
@@ -167,7 +168,8 @@ async function typeSlowly(shell, text, charDelayMs) {
167
168
  * that hasn't changed for idlePromptMs. This prevents injecting while Claude
168
169
  * is busy generating ("Channelling...").
169
170
  */
170
- async function waitForIdlePrompt(getOutputBuffer, config, maxWaitMs = 10000) {
171
+ async function waitForIdlePrompt(getOutputBuffer, config) {
172
+ const maxWaitMs = config.maxIdleWaitMs;
171
173
  const startTime = Date.now();
172
174
  let lastOutput = '';
173
175
  let stableTime = 0;
@@ -380,9 +382,45 @@ async function emergencyCapture(transcriptPath, sessionId) {
380
382
  dlog('Warning: Could not capture context');
381
383
  }
382
384
  }
385
+ /**
386
+ * Generate a unique instance ID for this run
387
+ * Used for namespacing storage paths
388
+ */
389
+ function generateInstanceId() {
390
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
391
+ }
392
+ /**
393
+ * Write/update instance file
394
+ */
395
+ function writeInstanceFile(instanceId, data) {
396
+ const instancesDir = path.join(state_1.EKKOS_DIR, 'instances');
397
+ if (!fs.existsSync(instancesDir)) {
398
+ fs.mkdirSync(instancesDir, { recursive: true });
399
+ }
400
+ const instanceFile = path.join(instancesDir, `${instanceId}.json`);
401
+ fs.writeFileSync(instanceFile, JSON.stringify(data, null, 2));
402
+ }
403
+ /**
404
+ * Clean up instance file on exit
405
+ */
406
+ function cleanupInstanceFile(instanceId) {
407
+ try {
408
+ const instanceFile = path.join(state_1.EKKOS_DIR, 'instances', `${instanceId}.json`);
409
+ if (fs.existsSync(instanceFile)) {
410
+ fs.unlinkSync(instanceFile);
411
+ }
412
+ }
413
+ catch {
414
+ // Ignore cleanup errors
415
+ }
416
+ }
383
417
  async function run(options) {
384
418
  const verbose = options.verbose || false;
385
419
  const bypass = options.bypass || false;
420
+ const noInject = options.noInject || false;
421
+ // Generate instance ID for this run
422
+ const instanceId = generateInstanceId();
423
+ process.env.EKKOS_INSTANCE_ID = instanceId;
386
424
  // ══════════════════════════════════════════════════════════════════════════
387
425
  // PRE-FLIGHT DIAGNOSTICS (--doctor flag)
388
426
  // ══════════════════════════════════════════════════════════════════════════
@@ -502,7 +540,7 @@ async function run(options) {
502
540
  }
503
541
  if (verbose) {
504
542
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
505
- console.log(chalk_1.default.gray(` ⏱ Timing: slash=${config.slashOpenDelayMs}ms, char=${config.charDelayMs}ms, clear=${config.clearWaitMs}ms`));
543
+ console.log(chalk_1.default.gray(` ⏱ Timing: clear=${config.clearWaitMs}ms, idleMax=${config.maxIdleWaitMs}ms (~${Math.round((config.clearWaitMs + config.maxIdleWaitMs * 2 + 1700) / 1000)}s total)`));
506
544
  }
507
545
  console.log('');
508
546
  // Ensure .ekkos directory exists
@@ -511,6 +549,18 @@ async function run(options) {
511
549
  (0, state_1.clearAutoClearFlag)();
512
550
  // Track state
513
551
  let currentSession = options.session || (0, state_1.getCurrentSessionName)();
552
+ // Write initial instance file
553
+ const startedAt = new Date().toISOString();
554
+ writeInstanceFile(instanceId, {
555
+ pid: process.pid,
556
+ projectPath: process.cwd(),
557
+ startedAt,
558
+ lastHeartbeat: startedAt,
559
+ sessionName: currentSession || undefined
560
+ });
561
+ if (verbose) {
562
+ console.log(chalk_1.default.gray(` 🆔 Instance: ${instanceId}`));
563
+ }
514
564
  // ════════════════════════════════════════════════════════════════════════════
515
565
  // MULTI-SESSION SUPPORT: Register this process as an active session
516
566
  // This prevents state collision when multiple Claude Code instances run
@@ -530,7 +580,8 @@ async function run(options) {
530
580
  let currentSessionId = null;
531
581
  // Stream tailer for mid-turn context capture
532
582
  let streamTailer = null;
533
- const streamCacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
583
+ // Instance-namespaced cache directory per spec v1.2
584
+ const streamCacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions', instanceId);
534
585
  // Helper to start stream tailer when we have transcript path
535
586
  function startStreamTailer(tPath, sId, sName) {
536
587
  if (streamTailer)
@@ -604,33 +655,28 @@ async function run(options) {
604
655
  // Determine which mode to use
605
656
  const usePty = pty !== null;
606
657
  // ══════════════════════════════════════════════════════════════════════════
607
- // WINDOWS: HARD FAIL WITHOUT PTY
658
+ // WINDOWS: MONITOR-ONLY MODE WITHOUT PTY (Per Spec v1.2 Addendum)
608
659
  // Without node-pty/ConPTY, auto-continue cannot work on Windows.
609
- // Claude switches to --print mode without a real PTY, breaking the TUI.
660
+ // Instead of hard-failing, we enter monitor-only mode.
610
661
  // ══════════════════════════════════════════════════════════════════════════
662
+ const monitorOnlyMode = noInject || (isWindows && !usePty);
611
663
  if (isWindows && !usePty) {
612
664
  console.log('');
613
- console.log(chalk_1.default.red.bold(' ekkos run requires PTY support on Windows'));
665
+ console.log(chalk_1.default.yellow.bold('⚠️ Monitor-only mode (PTY not available)'));
614
666
  console.log('');
615
- console.log(chalk_1.default.yellow('Without node-pty (ConPTY), Claude Code runs in --print mode'));
616
- console.log(chalk_1.default.yellow('which prevents auto-continue from working.'));
667
+ console.log(chalk_1.default.gray('Without node-pty (ConPTY), auto-continue cannot inject commands.'));
668
+ console.log(chalk_1.default.gray('ekkOS will monitor context usage and provide instructions when needed.'));
617
669
  console.log('');
618
- console.log(chalk_1.default.cyan('To fix:'));
619
- console.log('');
620
- console.log(chalk_1.default.white(' Option 1: Use Node 20 or 22 LTS (recommended)'));
670
+ console.log(chalk_1.default.cyan('To enable auto-continue:'));
671
+ console.log(chalk_1.default.white(' Option 1: Use Node 20 or 22 LTS'));
621
672
  console.log(chalk_1.default.gray(' winget install OpenJS.NodeJS.LTS'));
622
- console.log(chalk_1.default.gray(' npm install -g @ekkos/cli'));
623
- console.log('');
624
- console.log(chalk_1.default.white(' Option 2: Install prebuilt PTY'));
625
- console.log(chalk_1.default.gray(' npm install node-pty-prebuilt-multiarch'));
626
- console.log('');
627
- console.log(chalk_1.default.white(' Option 3: Build node-pty from source'));
628
- console.log(chalk_1.default.gray(' 1. Install VS Build Tools (Desktop C++ workload)'));
629
- console.log(chalk_1.default.gray(' 2. npm rebuild node-pty --build-from-source'));
673
+ console.log(chalk_1.default.white(' Option 2: npm install node-pty-prebuilt-multiarch'));
630
674
  console.log('');
631
675
  console.log(chalk_1.default.gray('Run `ekkos doctor` for detailed diagnostics.'));
632
676
  console.log('');
633
- process.exit(1);
677
+ }
678
+ else if (noInject) {
679
+ console.log(chalk_1.default.yellow(' Monitor-only mode (--no-inject)'));
634
680
  }
635
681
  if (verbose) {
636
682
  console.log(chalk_1.default.gray(` 💻 PTY mode: ${usePty ? 'node-pty' : 'spawn+script (fallback)'}`));
@@ -982,6 +1028,7 @@ async function run(options) {
982
1028
  (0, state_1.clearAutoClearFlag)();
983
1029
  stopStreamTailer(); // Stop stream capture
984
1030
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
1031
+ cleanupInstanceFile(instanceId); // Clean up instance file
985
1032
  // Restore terminal
986
1033
  if (process.stdin.isTTY) {
987
1034
  process.stdin.setRawMode(false);
@@ -996,6 +1043,7 @@ async function run(options) {
996
1043
  (0, state_1.clearAutoClearFlag)();
997
1044
  stopStreamTailer(); // Stop stream capture
998
1045
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
1046
+ cleanupInstanceFile(instanceId); // Clean up instance file
999
1047
  if (process.stdin.isTTY) {
1000
1048
  process.stdin.setRawMode(false);
1001
1049
  }
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ const status_1 = require("./commands/status");
11
11
  const run_1 = require("./commands/run");
12
12
  const doctor_1 = require("./commands/doctor");
13
13
  const stream_1 = require("./commands/stream");
14
+ const hooks_1 = require("./commands/hooks");
14
15
  const state_1 = require("./utils/state");
15
16
  const chalk_1 = __importDefault(require("chalk"));
16
17
  commander_1.program
@@ -46,12 +47,14 @@ commander_1.program
46
47
  .option('-b, --bypass', 'Enable bypass permissions mode (dangerously skip all permission checks)')
47
48
  .option('-v, --verbose', 'Show debug output')
48
49
  .option('-d, --doctor', 'Run diagnostics before starting')
50
+ .option('--no-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
49
51
  .action((options) => {
50
52
  (0, run_1.run)({
51
53
  session: options.session,
52
54
  bypass: options.bypass,
53
55
  verbose: options.verbose,
54
- doctor: options.doctor
56
+ doctor: options.doctor,
57
+ noInject: options.noInject
55
58
  });
56
59
  });
57
60
  // Doctor command - check system prerequisites
@@ -86,6 +89,44 @@ streamCmd
86
89
  .action(() => {
87
90
  (0, stream_1.streamList)();
88
91
  });
92
+ // Hooks command - install, verify, status
93
+ const hooksCmd = commander_1.program
94
+ .command('hooks')
95
+ .description('Manage ekkOS hooks (install, verify, status)');
96
+ hooksCmd
97
+ .command('install')
98
+ .description('Install ekkOS hooks to ~/.claude/hooks/ (global) or project')
99
+ .option('-g, --global', 'Install globally (default)')
100
+ .option('-p, --project', 'Install to current project only')
101
+ .option('-v, --verbose', 'Show detailed output')
102
+ .action((options) => {
103
+ (0, hooks_1.hooksInstall)({
104
+ global: options.global !== false && !options.project,
105
+ project: options.project,
106
+ verbose: options.verbose
107
+ });
108
+ });
109
+ hooksCmd
110
+ .command('verify')
111
+ .description('Verify hook installation and checksums')
112
+ .option('-g, --global', 'Verify global installation (default)')
113
+ .option('-p, --project', 'Verify project installation')
114
+ .option('-v, --verbose', 'Show detailed output')
115
+ .action(async (options) => {
116
+ const result = await (0, hooks_1.hooksVerify)({
117
+ global: options.global !== false && !options.project,
118
+ project: options.project,
119
+ verbose: options.verbose
120
+ });
121
+ process.exit(result.status === 'FAIL' ? 1 : 0);
122
+ });
123
+ hooksCmd
124
+ .command('status')
125
+ .description('Show hook installation status and enablement')
126
+ .option('-v, --verbose', 'Show detailed output')
127
+ .action((options) => {
128
+ (0, hooks_1.hooksStatus)({ verbose: options.verbose });
129
+ });
89
130
  // Sessions command - list active Claude Code sessions (swarm support)
90
131
  commander_1.program
91
132
  .command('sessions')
@@ -1,21 +1,31 @@
1
1
  /**
2
- * ekkOS Fast /continue - Restore Orchestrator
2
+ * ekkOS Fast /continue - Restore Orchestrator (Instance-Aware)
3
3
  *
4
4
  * Implements the 3-tier restore chain for near-zero context loss:
5
+ * - Tier -1: Stream log (real-time, has mid-turn content)
5
6
  * - Tier 0: Local JSONL cache (~20ms)
6
7
  * - Tier 1: Redis hot cache (~150ms)
7
8
  * - Tier 2: Supabase cold store (~500ms)
8
9
  *
10
+ * Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
11
+ * - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
12
+ * - All persisted records MUST include: instanceId, sessionId, sessionName
13
+ *
9
14
  * Falls back through tiers on miss, tracks which tier succeeded.
10
15
  */
11
16
  import { RestorePayload, RestoreOptions, CacheResult } from '../cache/types.js';
12
17
  /**
13
- * RestoreOrchestrator - Tiered restore for /continue
18
+ * RestoreOrchestrator - Tiered restore for /continue (Instance-Aware)
14
19
  */
15
20
  export declare class RestoreOrchestrator {
16
21
  private localStore;
17
22
  private authToken;
18
- constructor();
23
+ private instanceId;
24
+ constructor(instanceId?: string);
25
+ /**
26
+ * Get the current instance ID
27
+ */
28
+ getInstanceId(): string;
19
29
  /**
20
30
  * Main restore function - attempts tiers in order
21
31
  */
@@ -24,6 +34,8 @@ export declare class RestoreOrchestrator {
24
34
  * Tier -1: Restore from stream log (has mid-turn content)
25
35
  * This is checked FIRST because stream logs have the most recent data,
26
36
  * including in-progress turns that haven't been sealed yet.
37
+ *
38
+ * Checks instance-scoped path first, then legacy path as fallback.
27
39
  */
28
40
  private restoreFromStreamLog;
29
41
  /**
@@ -49,6 +61,8 @@ export declare class RestoreOrchestrator {
49
61
  local_sessions: number;
50
62
  local_turns: number;
51
63
  local_size_bytes: number;
64
+ instance_id: string;
52
65
  };
53
66
  }
67
+ export declare function createRestoreOrchestrator(instanceId?: string): RestoreOrchestrator;
54
68
  export declare const restoreOrchestrator: RestoreOrchestrator;
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  /**
3
- * ekkOS Fast /continue - Restore Orchestrator
3
+ * ekkOS Fast /continue - Restore Orchestrator (Instance-Aware)
4
4
  *
5
5
  * Implements the 3-tier restore chain for near-zero context loss:
6
+ * - Tier -1: Stream log (real-time, has mid-turn content)
6
7
  * - Tier 0: Local JSONL cache (~20ms)
7
8
  * - Tier 1: Redis hot cache (~150ms)
8
9
  * - Tier 2: Supabase cold store (~500ms)
9
10
  *
11
+ * Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
12
+ * - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
13
+ * - All persisted records MUST include: instanceId, sessionId, sessionName
14
+ *
10
15
  * Falls back through tiers on miss, tracks which tier succeeded.
11
16
  */
12
17
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -44,16 +49,17 @@ var __importStar = (this && this.__importStar) || (function () {
44
49
  })();
45
50
  Object.defineProperty(exports, "__esModule", { value: true });
46
51
  exports.restoreOrchestrator = exports.RestoreOrchestrator = void 0;
52
+ exports.createRestoreOrchestrator = createRestoreOrchestrator;
47
53
  const fs = __importStar(require("fs"));
48
54
  const path = __importStar(require("path"));
49
55
  const os = __importStar(require("os"));
50
56
  const LocalSessionStore_js_1 = require("../cache/LocalSessionStore.js");
51
57
  const types_js_1 = require("../cache/types.js");
52
58
  const stream_tailer_js_1 = require("../capture/stream-tailer.js");
59
+ const paths_js_1 = require("../utils/paths.js");
53
60
  // API configuration
54
61
  const MEMORY_API_URL = process.env.EKKOS_API_URL || 'https://api.ekkos.dev';
55
62
  const CONFIG_PATH = path.join(os.homedir(), '.ekkos', 'config.json');
56
- const STREAM_CACHE_DIR = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
57
63
  /**
58
64
  * Load auth token from config
59
65
  */
@@ -70,19 +76,31 @@ function loadAuthToken() {
70
76
  return '';
71
77
  }
72
78
  /**
73
- * RestoreOrchestrator - Tiered restore for /continue
79
+ * RestoreOrchestrator - Tiered restore for /continue (Instance-Aware)
74
80
  */
75
81
  class RestoreOrchestrator {
76
- constructor() {
77
- this.localStore = new LocalSessionStore_js_1.LocalSessionStore();
82
+ constructor(instanceId) {
83
+ this.instanceId = (0, paths_js_1.normalizeInstanceId)(instanceId || process.env.EKKOS_INSTANCE_ID);
84
+ this.localStore = (0, LocalSessionStore_js_1.createLocalSessionStore)(this.instanceId);
78
85
  this.authToken = loadAuthToken();
79
86
  }
87
+ /**
88
+ * Get the current instance ID
89
+ */
90
+ getInstanceId() {
91
+ return this.instanceId;
92
+ }
80
93
  /**
81
94
  * Main restore function - attempts tiers in order
82
95
  */
83
96
  async restore(options = {}) {
84
97
  const startTime = Date.now();
85
98
  const lastN = options.last_n || 10;
99
+ // Use provided instanceId or fall back to constructor instanceId
100
+ const instanceId = options.instance_id || this.instanceId;
101
+ if (instanceId !== this.instanceId) {
102
+ this.localStore.setInstanceId(instanceId);
103
+ }
86
104
  // Resolve session
87
105
  let sessionId = options.session_id;
88
106
  let sessionName = options.session_name;
@@ -105,7 +123,7 @@ class RestoreOrchestrator {
105
123
  };
106
124
  }
107
125
  // Try Tier -1: Stream log (has mid-turn content, most recent data)
108
- const streamResult = await this.restoreFromStreamLog(sessionId, sessionName || '', lastN);
126
+ const streamResult = await this.restoreFromStreamLog(instanceId, sessionId, sessionName || '', lastN);
109
127
  if (streamResult.success && streamResult.data) {
110
128
  return {
111
129
  ...streamResult,
@@ -113,7 +131,7 @@ class RestoreOrchestrator {
113
131
  };
114
132
  }
115
133
  // Try Tier 0: Local cache
116
- const localResult = await this.restoreFromLocal(sessionId, sessionName || '', lastN);
134
+ const localResult = await this.restoreFromLocal(instanceId, sessionId, sessionName || '', lastN);
117
135
  if (localResult.success && localResult.data) {
118
136
  return {
119
137
  ...localResult,
@@ -121,7 +139,7 @@ class RestoreOrchestrator {
121
139
  };
122
140
  }
123
141
  // Try Tier 1: Redis
124
- const redisResult = await this.restoreFromRedis(sessionName || sessionId, lastN);
142
+ const redisResult = await this.restoreFromRedis(sessionName || sessionId, lastN, instanceId);
125
143
  if (redisResult.success && redisResult.data) {
126
144
  return {
127
145
  ...redisResult,
@@ -129,7 +147,7 @@ class RestoreOrchestrator {
129
147
  };
130
148
  }
131
149
  // Try Tier 2: Supabase
132
- const supabaseResult = await this.restoreFromSupabase(sessionId, lastN);
150
+ const supabaseResult = await this.restoreFromSupabase(sessionId, lastN, instanceId);
133
151
  if (supabaseResult.success && supabaseResult.data) {
134
152
  return {
135
153
  ...supabaseResult,
@@ -147,12 +165,14 @@ class RestoreOrchestrator {
147
165
  * Tier -1: Restore from stream log (has mid-turn content)
148
166
  * This is checked FIRST because stream logs have the most recent data,
149
167
  * including in-progress turns that haven't been sealed yet.
168
+ *
169
+ * Checks instance-scoped path first, then legacy path as fallback.
150
170
  */
151
- async restoreFromStreamLog(sessionId, sessionName, lastN) {
171
+ async restoreFromStreamLog(instanceId, sessionId, sessionName, lastN) {
152
172
  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)) {
173
+ // Find stream log file (instance-scoped first, then legacy)
174
+ const streamLogFile = (0, paths_js_1.findStreamLogFile)(instanceId, sessionId);
175
+ if (!streamLogFile) {
156
176
  return {
157
177
  success: false,
158
178
  error: 'No stream log found',
@@ -161,7 +181,7 @@ class RestoreOrchestrator {
161
181
  };
162
182
  }
163
183
  try {
164
- const { turns: streamTurns, latestTurnId } = await (0, stream_tailer_js_1.reconstructTurnFromEvents)(streamLogPath);
184
+ const { turns: streamTurns, latestTurnId } = await (0, stream_tailer_js_1.reconstructTurnFromEvents)(streamLogFile.path);
165
185
  if (streamTurns.size === 0) {
166
186
  return {
167
187
  success: false,
@@ -193,6 +213,9 @@ class RestoreOrchestrator {
193
213
  tools_used: turn.tools.filter(t => t.kind === 'tool_use').map(t => t.name || 'unknown'),
194
214
  files_referenced: [],
195
215
  is_complete: turn.status === 'complete',
216
+ instance_id: streamLogFile.isLegacy ? paths_js_1.DEFAULT_INSTANCE_ID : instanceId,
217
+ session_id: sessionId,
218
+ session_name: sessionName,
196
219
  });
197
220
  }
198
221
  // Sort by turn_id
@@ -213,6 +236,7 @@ class RestoreOrchestrator {
213
236
  const payload = {
214
237
  session_id: sessionId,
215
238
  session_name: sessionName || 'unknown',
239
+ instance_id: streamLogFile.isLegacy ? paths_js_1.DEFAULT_INSTANCE_ID : instanceId,
216
240
  source: 'stream',
217
241
  restored_turns: restoredTurns,
218
242
  latest: {
@@ -257,7 +281,7 @@ class RestoreOrchestrator {
257
281
  /**
258
282
  * Tier 0: Restore from local JSONL cache
259
283
  */
260
- async restoreFromLocal(sessionId, sessionName, lastN) {
284
+ async restoreFromLocal(instanceId, sessionId, sessionName, lastN) {
261
285
  const startTime = Date.now();
262
286
  const turnsResult = this.localStore.getLastTurns(sessionId, lastN);
263
287
  if (!turnsResult.success || !turnsResult.data || turnsResult.data.length === 0) {
@@ -272,6 +296,9 @@ class RestoreOrchestrator {
272
296
  const allTurns = turnsResult.data.map((t) => ({
273
297
  ...t,
274
298
  is_complete: (0, types_js_1.isValidAssistantResponse)(t.assistant_response),
299
+ instance_id: t.instance_id || instanceId,
300
+ session_id: t.session_id || sessionId,
301
+ session_name: t.session_name || sessionName,
275
302
  }));
276
303
  // Separate complete turns from incomplete (pending) turns
277
304
  const completeTurns = allTurns.filter((t) => t.is_complete);
@@ -304,6 +331,7 @@ class RestoreOrchestrator {
304
331
  const payload = {
305
332
  session_id: sessionId,
306
333
  session_name: sessionName || this.localStore.getSessionName(sessionId) || 'unknown',
334
+ instance_id: meta?.instance_id || instanceId,
307
335
  source: 'local',
308
336
  restored_turns: turns,
309
337
  latest: {
@@ -337,7 +365,7 @@ class RestoreOrchestrator {
337
365
  /**
338
366
  * Tier 1: Restore from Redis via API
339
367
  */
340
- async restoreFromRedis(sessionName, lastN) {
368
+ async restoreFromRedis(sessionName, lastN, instanceId) {
341
369
  const startTime = Date.now();
342
370
  if (!this.authToken) {
343
371
  return {
@@ -375,6 +403,9 @@ class RestoreOrchestrator {
375
403
  tools_used: t.agent?.tools_used || [],
376
404
  files_referenced: t.user?.files_referenced || [],
377
405
  is_complete: (0, types_js_1.isValidAssistantResponse)(t.agent?.response),
406
+ instance_id: t.instance_id || instanceId,
407
+ session_id: data.session_id || sessionName,
408
+ session_name: data.session_name || sessionName,
378
409
  }));
379
410
  // Separate complete turns from pending
380
411
  const completeTurns = allTurns.filter((t) => t.is_complete);
@@ -395,6 +426,7 @@ class RestoreOrchestrator {
395
426
  const payload = {
396
427
  session_id: data.session_id || sessionName,
397
428
  session_name: data.session_name || sessionName,
429
+ instance_id: data.instance_id || instanceId,
398
430
  source: 'redis',
399
431
  restored_turns: turns,
400
432
  latest: {
@@ -444,7 +476,7 @@ class RestoreOrchestrator {
444
476
  /**
445
477
  * Tier 2: Restore from Supabase via API
446
478
  */
447
- async restoreFromSupabase(sessionId, lastN) {
479
+ async restoreFromSupabase(sessionId, lastN, instanceId) {
448
480
  const startTime = Date.now();
449
481
  if (!this.authToken) {
450
482
  return {
@@ -483,6 +515,9 @@ class RestoreOrchestrator {
483
515
  tools_used: [],
484
516
  files_referenced: c.metadata?.file_changes?.map((f) => f.path) || [],
485
517
  is_complete: (0, types_js_1.isValidAssistantResponse)(c.assistant_response),
518
+ instance_id: c.metadata?.instance_id || instanceId,
519
+ session_id: sessionId,
520
+ session_name: data.session_name || 'unknown',
486
521
  }));
487
522
  // Separate complete turns from pending
488
523
  const completeTurns = allTurns.filter((t) => t.is_complete);
@@ -503,6 +538,7 @@ class RestoreOrchestrator {
503
538
  const payload = {
504
539
  session_id: sessionId,
505
540
  session_name: data.session_name || 'unknown',
541
+ instance_id: instanceId,
506
542
  source: 'supabase',
507
543
  restored_turns: turns,
508
544
  latest: {
@@ -560,6 +596,7 @@ class RestoreOrchestrator {
560
596
  '<system-reminder>',
561
597
  'CONTEXT RESTORED (ekkOS /continue)',
562
598
  `Session: ${payload.session_name} (${payload.session_id})`,
599
+ `Instance: ${payload.instance_id || paths_js_1.DEFAULT_INSTANCE_ID}`,
563
600
  `Source: ${payload.source}${isStreamRestore ? ' (real-time stream)' : ''}`,
564
601
  isInProgress ? `Status: IN_PROGRESS (mid-turn restore)` : `Turns restored: ${payload.restored_turns.length}`,
565
602
  '',
@@ -570,7 +607,7 @@ class RestoreOrchestrator {
570
607
  if (openLoops && openLoops.length > 0) {
571
608
  lines.push('## Open Loops (machine-derived)');
572
609
  for (const loop of openLoops) {
573
- lines.push(`- ⚠️ ${loop.detail}`);
610
+ lines.push(`- Warning: ${loop.detail}`);
574
611
  }
575
612
  lines.push('');
576
613
  }
@@ -585,7 +622,7 @@ class RestoreOrchestrator {
585
622
  // Special instruction for mid-turn resume
586
623
  lines.push('INSTRUCTION: Continue from the exact point shown above.');
587
624
  lines.push('Do not recap. Do not restart. Continue the sentence/action if mid-sentence.');
588
- lines.push('Start your response with: "Continuing -"');
625
+ lines.push('Start your response with: "Continuing -"');
589
626
  }
590
627
  else {
591
628
  // Standard restore format
@@ -598,7 +635,7 @@ class RestoreOrchestrator {
598
635
  lines.push('\n[...truncated...]');
599
636
  }
600
637
  lines.push('');
601
- lines.push('## Recent Turns (older newer)');
638
+ lines.push('## Recent Turns (older to newer)');
602
639
  // Add turn summaries (skip last one since it's shown in detail above)
603
640
  const turnsToShow = payload.restored_turns.slice(0, -1);
604
641
  for (let i = 0; i < turnsToShow.length; i++) {
@@ -611,7 +648,7 @@ class RestoreOrchestrator {
611
648
  lines.push('');
612
649
  lines.push('INSTRUCTION: Resume seamlessly where you left off.');
613
650
  lines.push('Do not ask "what were we doing?"');
614
- lines.push('Start your response with: "Continuing -"');
651
+ lines.push('Start your response with: "Continuing -"');
615
652
  }
616
653
  lines.push('</system-reminder>');
617
654
  return lines.join('\n');
@@ -625,9 +662,14 @@ class RestoreOrchestrator {
625
662
  local_sessions: stats.session_count,
626
663
  local_turns: stats.total_turns,
627
664
  local_size_bytes: stats.cache_size_bytes,
665
+ instance_id: stats.instance_id,
628
666
  };
629
667
  }
630
668
  }
631
669
  exports.RestoreOrchestrator = RestoreOrchestrator;
632
- // Export singleton instance
670
+ // Export factory function
671
+ function createRestoreOrchestrator(instanceId) {
672
+ return new RestoreOrchestrator(instanceId);
673
+ }
674
+ // Export default instance using EKKOS_INSTANCE_ID from env
633
675
  exports.restoreOrchestrator = new RestoreOrchestrator();