@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.
- package/dist/cache/LocalSessionStore.d.ts +34 -21
- package/dist/cache/LocalSessionStore.js +169 -53
- package/dist/cache/capture.d.ts +19 -11
- package/dist/cache/capture.js +243 -76
- package/dist/cache/types.d.ts +14 -1
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +148 -73
- package/dist/commands/hooks.d.ts +109 -0
- package/dist/commands/hooks.js +668 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +69 -21
- package/dist/index.js +42 -1
- package/dist/restore/RestoreOrchestrator.d.ts +17 -3
- package/dist/restore/RestoreOrchestrator.js +64 -22
- package/dist/utils/paths.d.ts +125 -0
- package/dist/utils/paths.js +283 -0
- package/package.json +1 -1
- package/templates/ekkos-manifest.json +223 -0
- package/templates/helpers/json-parse.cjs +101 -0
- package/templates/hooks/assistant-response.ps1 +256 -0
- package/templates/hooks/assistant-response.sh +124 -64
- package/templates/hooks/session-start.ps1 +107 -2
- package/templates/hooks/session-start.sh +201 -166
- package/templates/hooks/stop.ps1 +124 -3
- package/templates/hooks/stop.sh +470 -843
- package/templates/hooks/user-prompt-submit.ps1 +107 -22
- package/templates/hooks/user-prompt-submit.sh +403 -393
- package/templates/project-stubs/session-start.ps1 +63 -0
- package/templates/project-stubs/session-start.sh +55 -0
- package/templates/project-stubs/stop.ps1 +63 -0
- package/templates/project-stubs/stop.sh +55 -0
- package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
- package/templates/project-stubs/user-prompt-submit.sh +55 -0
- package/templates/shared/hooks-enabled.json +22 -0
- package/templates/shared/session-words.json +45 -0
package/dist/commands/run.js
CHANGED
|
@@ -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 || '
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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.
|
|
665
|
+
console.log(chalk_1.default.yellow.bold('⚠️ Monitor-only mode (PTY not available)'));
|
|
614
666
|
console.log('');
|
|
615
|
-
console.log(chalk_1.default.
|
|
616
|
-
console.log(chalk_1.default.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
155
|
-
if (!
|
|
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)(
|
|
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(`-
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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();
|