@ekkos/cli 0.2.8 → 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/dist/utils/session-words.json +30 -111
- 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
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ekkOS Fast /continue - Local Session Store
|
|
2
|
+
* ekkOS Fast /continue - Local Session Store (Instance-Aware)
|
|
3
3
|
*
|
|
4
4
|
* Tier 0 of the 3-tier restore chain.
|
|
5
5
|
* Provides ultra-low latency (<20ms) turn storage using append-only JSONL files.
|
|
6
6
|
*
|
|
7
|
+
* Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
|
|
8
|
+
* - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
|
|
9
|
+
* - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
10
|
+
* - Backward compatibility: missing instanceId treated as 'default'
|
|
11
|
+
*
|
|
7
12
|
* Features:
|
|
13
|
+
* - Instance-scoped storage (prevents multi-session cross-talk)
|
|
8
14
|
* - Atomic writes (write to .tmp, rename)
|
|
9
15
|
* - Crash recovery (handle partial JSONL lines)
|
|
10
16
|
* - ACK-based safe pruning
|
|
11
17
|
* - Multi-session support with LRU eviction
|
|
18
|
+
* - Legacy path fallback for reads (backward compatibility)
|
|
12
19
|
*/
|
|
13
20
|
import { Turn, SessionMeta, LocalCacheConfig, SessionListEntry, CacheResult } from './types.js';
|
|
14
21
|
/**
|
|
15
|
-
* LocalSessionStore - Fast local cache for conversation turns
|
|
22
|
+
* LocalSessionStore - Fast local cache for conversation turns (Instance-Aware)
|
|
16
23
|
*/
|
|
17
24
|
export declare class LocalSessionStore {
|
|
18
25
|
private config;
|
|
19
|
-
private
|
|
20
|
-
private indexPath;
|
|
26
|
+
private instanceId;
|
|
21
27
|
private indexCache;
|
|
22
28
|
private indexCacheTime;
|
|
23
29
|
private readonly INDEX_CACHE_TTL;
|
|
24
|
-
constructor(config?: Partial<LocalCacheConfig
|
|
25
|
-
/**
|
|
26
|
-
* Ensure cache directories exist
|
|
27
|
-
*/
|
|
28
|
-
private ensureDirectories;
|
|
30
|
+
constructor(config?: Partial<LocalCacheConfig>, instanceId?: string);
|
|
29
31
|
/**
|
|
30
|
-
* Get the
|
|
32
|
+
* Get the current instance ID
|
|
31
33
|
*/
|
|
32
|
-
|
|
34
|
+
getInstanceId(): string;
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
36
|
+
* Set the instance ID (useful for switching contexts)
|
|
35
37
|
*/
|
|
36
|
-
|
|
38
|
+
setInstanceId(instanceId: string): void;
|
|
37
39
|
/**
|
|
38
40
|
* Read the session index (with caching)
|
|
39
41
|
*/
|
|
@@ -47,21 +49,24 @@ export declare class LocalSessionStore {
|
|
|
47
49
|
*/
|
|
48
50
|
private updateIndexEntry;
|
|
49
51
|
/**
|
|
50
|
-
* Read session metadata
|
|
52
|
+
* Read session metadata (checks instance path, then legacy path)
|
|
51
53
|
*/
|
|
52
54
|
getSessionMeta(sessionId: string): SessionMeta | null;
|
|
53
55
|
/**
|
|
54
|
-
* Write session metadata atomically
|
|
56
|
+
* Write session metadata atomically (always to instance-scoped path)
|
|
55
57
|
*/
|
|
56
58
|
private writeSessionMeta;
|
|
57
59
|
/**
|
|
58
60
|
* Append a turn to the session's JSONL file
|
|
59
61
|
* This is the hot path - must be fast
|
|
62
|
+
*
|
|
63
|
+
* Per v1.2 ADDENDUM: All records include instanceId, sessionId, sessionName
|
|
60
64
|
*/
|
|
61
65
|
appendTurn(sessionId: string, sessionName: string, turn: Turn, projectPath?: string): CacheResult<void>;
|
|
62
66
|
/**
|
|
63
67
|
* Get the last N turns from a session
|
|
64
68
|
* Handles crash recovery (partial lines)
|
|
69
|
+
* Checks instance path first, then legacy path as read-only fallback
|
|
65
70
|
*/
|
|
66
71
|
getLastTurns(sessionId: string, n?: number): CacheResult<Turn[]>;
|
|
67
72
|
/**
|
|
@@ -89,19 +94,25 @@ export declare class LocalSessionStore {
|
|
|
89
94
|
*/
|
|
90
95
|
prune(sessionId: string): CacheResult<number>;
|
|
91
96
|
/**
|
|
92
|
-
* List all sessions, sorted by last active (newest first)
|
|
97
|
+
* List all sessions for current instance, sorted by last active (newest first)
|
|
93
98
|
*/
|
|
94
99
|
listSessions(): SessionListEntry[];
|
|
95
100
|
/**
|
|
96
|
-
*
|
|
101
|
+
* List all sessions across ALL instances (for global queries)
|
|
102
|
+
*/
|
|
103
|
+
listAllSessions(): Array<SessionListEntry & {
|
|
104
|
+
instance_id: string;
|
|
105
|
+
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Get session ID from session name (searches current instance)
|
|
97
108
|
*/
|
|
98
109
|
getSessionId(sessionName: string): string | null;
|
|
99
110
|
/**
|
|
100
|
-
* Get session name from session ID
|
|
111
|
+
* Get session name from session ID (searches current instance)
|
|
101
112
|
*/
|
|
102
113
|
getSessionName(sessionId: string): string | null;
|
|
103
114
|
/**
|
|
104
|
-
* Check if a session exists in local cache
|
|
115
|
+
* Check if a session exists in local cache (current instance or legacy)
|
|
105
116
|
*/
|
|
106
117
|
hasSession(sessionIdOrName: string): boolean;
|
|
107
118
|
/**
|
|
@@ -114,16 +125,18 @@ export declare class LocalSessionStore {
|
|
|
114
125
|
*/
|
|
115
126
|
evictOldSessions(): number;
|
|
116
127
|
/**
|
|
117
|
-
* Get cache statistics
|
|
128
|
+
* Get cache statistics for current instance
|
|
118
129
|
*/
|
|
119
130
|
getStats(): {
|
|
120
131
|
session_count: number;
|
|
121
132
|
total_turns: number;
|
|
122
133
|
cache_size_bytes: number;
|
|
134
|
+
instance_id: string;
|
|
123
135
|
};
|
|
124
136
|
/**
|
|
125
|
-
* Clear all local cache (use with caution)
|
|
137
|
+
* Clear all local cache for current instance (use with caution)
|
|
126
138
|
*/
|
|
127
139
|
clearAll(): void;
|
|
128
140
|
}
|
|
141
|
+
export declare function createLocalSessionStore(instanceId?: string): LocalSessionStore;
|
|
129
142
|
export declare const localCache: LocalSessionStore;
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* ekkOS Fast /continue - Local Session Store
|
|
3
|
+
* ekkOS Fast /continue - Local Session Store (Instance-Aware)
|
|
4
4
|
*
|
|
5
5
|
* Tier 0 of the 3-tier restore chain.
|
|
6
6
|
* Provides ultra-low latency (<20ms) turn storage using append-only JSONL files.
|
|
7
7
|
*
|
|
8
|
+
* Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
|
|
9
|
+
* - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
|
|
10
|
+
* - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
11
|
+
* - Backward compatibility: missing instanceId treated as 'default'
|
|
12
|
+
*
|
|
8
13
|
* Features:
|
|
14
|
+
* - Instance-scoped storage (prevents multi-session cross-talk)
|
|
9
15
|
* - Atomic writes (write to .tmp, rename)
|
|
10
16
|
* - Crash recovery (handle partial JSONL lines)
|
|
11
17
|
* - ACK-based safe pruning
|
|
12
18
|
* - Multi-session support with LRU eviction
|
|
19
|
+
* - Legacy path fallback for reads (backward compatibility)
|
|
13
20
|
*/
|
|
14
21
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
22
|
if (k2 === undefined) k2 = k;
|
|
@@ -46,9 +53,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
53
|
})();
|
|
47
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
55
|
exports.localCache = exports.LocalSessionStore = void 0;
|
|
56
|
+
exports.createLocalSessionStore = createLocalSessionStore;
|
|
49
57
|
const fs = __importStar(require("fs"));
|
|
50
58
|
const path = __importStar(require("path"));
|
|
51
59
|
const os = __importStar(require("os"));
|
|
60
|
+
const paths_js_1 = require("../utils/paths.js");
|
|
52
61
|
// Default configuration
|
|
53
62
|
const DEFAULT_CONFIG = {
|
|
54
63
|
cache_dir: path.join(os.homedir(), '.ekkos', 'cache'),
|
|
@@ -58,40 +67,31 @@ const DEFAULT_CONFIG = {
|
|
|
58
67
|
flush_interval_ms: 5000,
|
|
59
68
|
};
|
|
60
69
|
/**
|
|
61
|
-
* LocalSessionStore - Fast local cache for conversation turns
|
|
70
|
+
* LocalSessionStore - Fast local cache for conversation turns (Instance-Aware)
|
|
62
71
|
*/
|
|
63
72
|
class LocalSessionStore {
|
|
64
|
-
constructor(config = {}) {
|
|
73
|
+
constructor(config = {}, instanceId) {
|
|
65
74
|
this.indexCache = null;
|
|
66
75
|
this.indexCacheTime = 0;
|
|
67
76
|
this.INDEX_CACHE_TTL = 1000; // 1 second
|
|
68
77
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
this.
|
|
78
|
+
this.instanceId = (0, paths_js_1.normalizeInstanceId)(instanceId || process.env.EKKOS_INSTANCE_ID);
|
|
79
|
+
(0, paths_js_1.ensureBaseDirs)();
|
|
80
|
+
(0, paths_js_1.ensureInstanceDir)(this.instanceId);
|
|
72
81
|
}
|
|
73
82
|
/**
|
|
74
|
-
*
|
|
83
|
+
* Get the current instance ID
|
|
75
84
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
// Directory might already exist
|
|
82
|
-
}
|
|
85
|
+
getInstanceId() {
|
|
86
|
+
return this.instanceId;
|
|
83
87
|
}
|
|
84
88
|
/**
|
|
85
|
-
*
|
|
89
|
+
* Set the instance ID (useful for switching contexts)
|
|
86
90
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
* Get the metadata file path for a session
|
|
92
|
-
*/
|
|
93
|
-
getMetaPath(sessionId) {
|
|
94
|
-
return path.join(this.sessionsDir, `${sessionId}.meta.json`);
|
|
91
|
+
setInstanceId(instanceId) {
|
|
92
|
+
this.instanceId = (0, paths_js_1.normalizeInstanceId)(instanceId);
|
|
93
|
+
this.indexCache = null; // Clear cache when switching instances
|
|
94
|
+
(0, paths_js_1.ensureInstanceDir)(this.instanceId);
|
|
95
95
|
}
|
|
96
96
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
97
|
// INDEX OPERATIONS
|
|
@@ -104,9 +104,10 @@ class LocalSessionStore {
|
|
|
104
104
|
if (this.indexCache && now - this.indexCacheTime < this.INDEX_CACHE_TTL) {
|
|
105
105
|
return this.indexCache;
|
|
106
106
|
}
|
|
107
|
+
const indexPath = (0, paths_js_1.getIndexPath)(this.instanceId);
|
|
107
108
|
try {
|
|
108
|
-
if (fs.existsSync(
|
|
109
|
-
const content = fs.readFileSync(
|
|
109
|
+
if (fs.existsSync(indexPath)) {
|
|
110
|
+
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
110
111
|
this.indexCache = JSON.parse(content);
|
|
111
112
|
this.indexCacheTime = now;
|
|
112
113
|
return this.indexCache;
|
|
@@ -124,10 +125,11 @@ class LocalSessionStore {
|
|
|
124
125
|
* Write the session index atomically
|
|
125
126
|
*/
|
|
126
127
|
writeIndex(index) {
|
|
127
|
-
const
|
|
128
|
+
const indexPath = (0, paths_js_1.getIndexPath)(this.instanceId);
|
|
129
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
128
130
|
try {
|
|
129
131
|
fs.writeFileSync(tmpPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
130
|
-
fs.renameSync(tmpPath,
|
|
132
|
+
fs.renameSync(tmpPath, indexPath);
|
|
131
133
|
this.indexCache = index;
|
|
132
134
|
this.indexCacheTime = Date.now();
|
|
133
135
|
}
|
|
@@ -154,26 +156,52 @@ class LocalSessionStore {
|
|
|
154
156
|
// SESSION METADATA
|
|
155
157
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
158
|
/**
|
|
157
|
-
* Read session metadata
|
|
159
|
+
* Read session metadata (checks instance path, then legacy path)
|
|
158
160
|
*/
|
|
159
161
|
getSessionMeta(sessionId) {
|
|
160
|
-
|
|
162
|
+
// Try instance-scoped path first
|
|
163
|
+
const metaPath = (0, paths_js_1.getMetaPath)(this.instanceId, sessionId);
|
|
161
164
|
try {
|
|
162
165
|
if (fs.existsSync(metaPath)) {
|
|
163
166
|
const content = fs.readFileSync(metaPath, 'utf-8');
|
|
164
|
-
|
|
167
|
+
const meta = JSON.parse(content);
|
|
168
|
+
// Ensure instance_id is set
|
|
169
|
+
if (!meta.instance_id) {
|
|
170
|
+
meta.instance_id = this.instanceId;
|
|
171
|
+
}
|
|
172
|
+
return meta;
|
|
165
173
|
}
|
|
166
174
|
}
|
|
167
175
|
catch (err) {
|
|
168
176
|
console.error('[LocalSessionStore] Meta read error:', err);
|
|
169
177
|
}
|
|
178
|
+
// Try legacy path (read-only fallback)
|
|
179
|
+
const legacyPath = (0, paths_js_1.getLegacyMetaPath)(sessionId);
|
|
180
|
+
try {
|
|
181
|
+
if (fs.existsSync(legacyPath)) {
|
|
182
|
+
const content = fs.readFileSync(legacyPath, 'utf-8');
|
|
183
|
+
const meta = JSON.parse(content);
|
|
184
|
+
// Mark as default instance for legacy data
|
|
185
|
+
if (!meta.instance_id) {
|
|
186
|
+
meta.instance_id = paths_js_1.DEFAULT_INSTANCE_ID;
|
|
187
|
+
}
|
|
188
|
+
return meta;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Ignore legacy read errors
|
|
193
|
+
}
|
|
170
194
|
return null;
|
|
171
195
|
}
|
|
172
196
|
/**
|
|
173
|
-
* Write session metadata atomically
|
|
197
|
+
* Write session metadata atomically (always to instance-scoped path)
|
|
174
198
|
*/
|
|
175
199
|
writeSessionMeta(meta) {
|
|
176
|
-
|
|
200
|
+
// Ensure instance_id is set
|
|
201
|
+
if (!meta.instance_id) {
|
|
202
|
+
meta.instance_id = this.instanceId;
|
|
203
|
+
}
|
|
204
|
+
const metaPath = (0, paths_js_1.getMetaPath)(this.instanceId, meta.session_id);
|
|
177
205
|
const tmpPath = `${metaPath}.tmp`;
|
|
178
206
|
try {
|
|
179
207
|
fs.writeFileSync(tmpPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
@@ -195,13 +223,24 @@ class LocalSessionStore {
|
|
|
195
223
|
/**
|
|
196
224
|
* Append a turn to the session's JSONL file
|
|
197
225
|
* This is the hot path - must be fast
|
|
226
|
+
*
|
|
227
|
+
* Per v1.2 ADDENDUM: All records include instanceId, sessionId, sessionName
|
|
198
228
|
*/
|
|
199
229
|
appendTurn(sessionId, sessionName, turn, projectPath) {
|
|
200
230
|
const startTime = Date.now();
|
|
201
|
-
|
|
231
|
+
// Ensure instance directory exists
|
|
232
|
+
(0, paths_js_1.ensureInstanceDir)(this.instanceId);
|
|
233
|
+
const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
|
|
202
234
|
try {
|
|
235
|
+
// Add instance namespacing fields to turn
|
|
236
|
+
const enrichedTurn = {
|
|
237
|
+
...turn,
|
|
238
|
+
instance_id: this.instanceId,
|
|
239
|
+
session_id: sessionId,
|
|
240
|
+
session_name: sessionName,
|
|
241
|
+
};
|
|
203
242
|
// Append turn as JSONL line (atomic append on most filesystems)
|
|
204
|
-
const line = JSON.stringify(
|
|
243
|
+
const line = JSON.stringify(enrichedTurn) + '\n';
|
|
205
244
|
fs.appendFileSync(turnsPath, line, 'utf-8');
|
|
206
245
|
// Update metadata
|
|
207
246
|
let meta = this.getSessionMeta(sessionId);
|
|
@@ -209,6 +248,7 @@ class LocalSessionStore {
|
|
|
209
248
|
meta = {
|
|
210
249
|
session_id: sessionId,
|
|
211
250
|
session_name: sessionName,
|
|
251
|
+
instance_id: this.instanceId,
|
|
212
252
|
acked_turn_id: 0,
|
|
213
253
|
last_flush_ts: new Date().toISOString(),
|
|
214
254
|
turn_count: 0,
|
|
@@ -218,12 +258,14 @@ class LocalSessionStore {
|
|
|
218
258
|
}
|
|
219
259
|
meta.turn_count = turn.turn_id;
|
|
220
260
|
meta.last_flush_ts = new Date().toISOString();
|
|
261
|
+
meta.instance_id = this.instanceId;
|
|
221
262
|
if (projectPath)
|
|
222
263
|
meta.project_path = projectPath;
|
|
223
264
|
this.writeSessionMeta(meta);
|
|
224
265
|
// Update index
|
|
225
266
|
this.updateIndexEntry(sessionName, {
|
|
226
267
|
session_id: sessionId,
|
|
268
|
+
instance_id: this.instanceId,
|
|
227
269
|
last_active_ts: new Date().toISOString(),
|
|
228
270
|
last_turn_id: turn.turn_id,
|
|
229
271
|
acked_turn_id: meta.acked_turn_id,
|
|
@@ -247,11 +289,17 @@ class LocalSessionStore {
|
|
|
247
289
|
/**
|
|
248
290
|
* Get the last N turns from a session
|
|
249
291
|
* Handles crash recovery (partial lines)
|
|
292
|
+
* Checks instance path first, then legacy path as read-only fallback
|
|
250
293
|
*/
|
|
251
294
|
getLastTurns(sessionId, n = 10) {
|
|
252
295
|
const startTime = Date.now();
|
|
253
|
-
|
|
254
|
-
|
|
296
|
+
// Try instance-scoped path first
|
|
297
|
+
let turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
|
|
298
|
+
let isLegacy = false;
|
|
299
|
+
if (!fs.existsSync(turnsPath)) {
|
|
300
|
+
// Try legacy path as fallback
|
|
301
|
+
turnsPath = (0, paths_js_1.getLegacyTurnsPath)(sessionId);
|
|
302
|
+
isLegacy = true;
|
|
255
303
|
if (!fs.existsSync(turnsPath)) {
|
|
256
304
|
return {
|
|
257
305
|
success: false,
|
|
@@ -260,6 +308,8 @@ class LocalSessionStore {
|
|
|
260
308
|
latency_ms: Date.now() - startTime,
|
|
261
309
|
};
|
|
262
310
|
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
263
313
|
const content = fs.readFileSync(turnsPath, 'utf-8');
|
|
264
314
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
265
315
|
const turns = [];
|
|
@@ -267,6 +317,13 @@ class LocalSessionStore {
|
|
|
267
317
|
for (const line of lines) {
|
|
268
318
|
try {
|
|
269
319
|
const turn = JSON.parse(line);
|
|
320
|
+
// Ensure instance fields are present (for legacy data)
|
|
321
|
+
if (!turn.instance_id) {
|
|
322
|
+
turn.instance_id = isLegacy ? paths_js_1.DEFAULT_INSTANCE_ID : this.instanceId;
|
|
323
|
+
}
|
|
324
|
+
if (!turn.session_id) {
|
|
325
|
+
turn.session_id = sessionId;
|
|
326
|
+
}
|
|
270
327
|
turns.push(turn);
|
|
271
328
|
}
|
|
272
329
|
catch {
|
|
@@ -312,8 +369,11 @@ class LocalSessionStore {
|
|
|
312
369
|
*/
|
|
313
370
|
updateTurnResponse(sessionId, turnId, response, toolsUsed, filesReferenced) {
|
|
314
371
|
const startTime = Date.now();
|
|
315
|
-
|
|
316
|
-
|
|
372
|
+
// Try instance-scoped path first
|
|
373
|
+
let turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
|
|
374
|
+
if (!fs.existsSync(turnsPath)) {
|
|
375
|
+
// Try legacy path
|
|
376
|
+
turnsPath = (0, paths_js_1.getLegacyTurnsPath)(sessionId);
|
|
317
377
|
if (!fs.existsSync(turnsPath)) {
|
|
318
378
|
return {
|
|
319
379
|
success: false,
|
|
@@ -322,6 +382,8 @@ class LocalSessionStore {
|
|
|
322
382
|
latency_ms: Date.now() - startTime,
|
|
323
383
|
};
|
|
324
384
|
}
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
325
387
|
const content = fs.readFileSync(turnsPath, 'utf-8');
|
|
326
388
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
327
389
|
const turns = [];
|
|
@@ -348,11 +410,20 @@ class LocalSessionStore {
|
|
|
348
410
|
turns[turnIndex].tools_used = toolsUsed;
|
|
349
411
|
if (filesReferenced)
|
|
350
412
|
turns[turnIndex].files_referenced = filesReferenced;
|
|
351
|
-
//
|
|
413
|
+
// Ensure instance fields
|
|
414
|
+
if (!turns[turnIndex].instance_id) {
|
|
415
|
+
turns[turnIndex].instance_id = this.instanceId;
|
|
416
|
+
}
|
|
417
|
+
if (!turns[turnIndex].session_id) {
|
|
418
|
+
turns[turnIndex].session_id = sessionId;
|
|
419
|
+
}
|
|
420
|
+
// Rewrite the file atomically (always to instance-scoped path)
|
|
421
|
+
const instanceTurnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
|
|
422
|
+
(0, paths_js_1.ensureInstanceDir)(this.instanceId);
|
|
352
423
|
const newContent = turns.map((t) => JSON.stringify(t)).join('\n') + '\n';
|
|
353
|
-
const tmpPath = `${
|
|
424
|
+
const tmpPath = `${instanceTurnsPath}.tmp`;
|
|
354
425
|
fs.writeFileSync(tmpPath, newContent, 'utf-8');
|
|
355
|
-
fs.renameSync(tmpPath,
|
|
426
|
+
fs.renameSync(tmpPath, instanceTurnsPath);
|
|
356
427
|
return {
|
|
357
428
|
success: true,
|
|
358
429
|
source: 'local',
|
|
@@ -390,12 +461,14 @@ class LocalSessionStore {
|
|
|
390
461
|
if (ackedTurnId > meta.acked_turn_id) {
|
|
391
462
|
meta.acked_turn_id = ackedTurnId;
|
|
392
463
|
meta.last_flush_ts = new Date().toISOString();
|
|
464
|
+
meta.instance_id = this.instanceId;
|
|
393
465
|
this.writeSessionMeta(meta);
|
|
394
466
|
// Update index too
|
|
395
467
|
const index = this.readIndex();
|
|
396
468
|
const sessionName = Object.keys(index).find((name) => index[name].session_id === sessionId);
|
|
397
469
|
if (sessionName) {
|
|
398
470
|
index[sessionName].acked_turn_id = ackedTurnId;
|
|
471
|
+
index[sessionName].instance_id = this.instanceId;
|
|
399
472
|
this.writeIndex(index);
|
|
400
473
|
}
|
|
401
474
|
}
|
|
@@ -435,6 +508,7 @@ class LocalSessionStore {
|
|
|
435
508
|
if (ackedTurnId > currentSupabaseAck) {
|
|
436
509
|
meta.supabase_acked_turn_id = ackedTurnId;
|
|
437
510
|
meta.last_flush_ts = new Date().toISOString();
|
|
511
|
+
meta.instance_id = this.instanceId;
|
|
438
512
|
this.writeSessionMeta(meta);
|
|
439
513
|
}
|
|
440
514
|
return {
|
|
@@ -459,7 +533,7 @@ class LocalSessionStore {
|
|
|
459
533
|
*/
|
|
460
534
|
prune(sessionId) {
|
|
461
535
|
const startTime = Date.now();
|
|
462
|
-
const turnsPath =
|
|
536
|
+
const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
|
|
463
537
|
try {
|
|
464
538
|
const meta = this.getSessionMeta(sessionId);
|
|
465
539
|
if (!meta) {
|
|
@@ -532,7 +606,7 @@ class LocalSessionStore {
|
|
|
532
606
|
// SESSION MANAGEMENT
|
|
533
607
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
534
608
|
/**
|
|
535
|
-
* List all sessions, sorted by last active (newest first)
|
|
609
|
+
* List all sessions for current instance, sorted by last active (newest first)
|
|
536
610
|
*/
|
|
537
611
|
listSessions() {
|
|
538
612
|
const index = this.readIndex();
|
|
@@ -553,14 +627,47 @@ class LocalSessionStore {
|
|
|
553
627
|
return entries;
|
|
554
628
|
}
|
|
555
629
|
/**
|
|
556
|
-
*
|
|
630
|
+
* List all sessions across ALL instances (for global queries)
|
|
631
|
+
*/
|
|
632
|
+
listAllSessions() {
|
|
633
|
+
const allEntries = [];
|
|
634
|
+
const instanceIds = (0, paths_js_1.listInstanceIds)();
|
|
635
|
+
for (const instId of instanceIds) {
|
|
636
|
+
const indexPath = (0, paths_js_1.getIndexPath)(instId);
|
|
637
|
+
try {
|
|
638
|
+
if (fs.existsSync(indexPath)) {
|
|
639
|
+
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
640
|
+
const index = JSON.parse(content);
|
|
641
|
+
for (const [sessionName, entry] of Object.entries(index)) {
|
|
642
|
+
allEntries.push({
|
|
643
|
+
session_name: sessionName,
|
|
644
|
+
session_id: entry.session_id,
|
|
645
|
+
instance_id: instId,
|
|
646
|
+
last_active_ts: entry.last_active_ts,
|
|
647
|
+
turn_count: entry.last_turn_id,
|
|
648
|
+
project_path: entry.project_path,
|
|
649
|
+
is_current: false,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
// Ignore errors for individual instances
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Sort by last active (newest first)
|
|
659
|
+
allEntries.sort((a, b) => new Date(b.last_active_ts).getTime() - new Date(a.last_active_ts).getTime());
|
|
660
|
+
return allEntries;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get session ID from session name (searches current instance)
|
|
557
664
|
*/
|
|
558
665
|
getSessionId(sessionName) {
|
|
559
666
|
const index = this.readIndex();
|
|
560
667
|
return index[sessionName]?.session_id || null;
|
|
561
668
|
}
|
|
562
669
|
/**
|
|
563
|
-
* Get session name from session ID
|
|
670
|
+
* Get session name from session ID (searches current instance)
|
|
564
671
|
*/
|
|
565
672
|
getSessionName(sessionId) {
|
|
566
673
|
const index = this.readIndex();
|
|
@@ -572,7 +679,7 @@ class LocalSessionStore {
|
|
|
572
679
|
return null;
|
|
573
680
|
}
|
|
574
681
|
/**
|
|
575
|
-
* Check if a session exists in local cache
|
|
682
|
+
* Check if a session exists in local cache (current instance or legacy)
|
|
576
683
|
*/
|
|
577
684
|
hasSession(sessionIdOrName) {
|
|
578
685
|
const index = this.readIndex();
|
|
@@ -584,6 +691,10 @@ class LocalSessionStore {
|
|
|
584
691
|
if (entry.session_id === sessionIdOrName)
|
|
585
692
|
return true;
|
|
586
693
|
}
|
|
694
|
+
// Check legacy paths
|
|
695
|
+
const legacyTurns = (0, paths_js_1.getLegacyTurnsPath)(sessionIdOrName);
|
|
696
|
+
if (fs.existsSync(legacyTurns))
|
|
697
|
+
return true;
|
|
587
698
|
return false;
|
|
588
699
|
}
|
|
589
700
|
/**
|
|
@@ -592,9 +703,9 @@ class LocalSessionStore {
|
|
|
592
703
|
deleteSession(sessionId) {
|
|
593
704
|
const startTime = Date.now();
|
|
594
705
|
try {
|
|
595
|
-
// Remove files
|
|
596
|
-
const turnsPath =
|
|
597
|
-
const metaPath =
|
|
706
|
+
// Remove instance-scoped files
|
|
707
|
+
const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, sessionId);
|
|
708
|
+
const metaPath = (0, paths_js_1.getMetaPath)(this.instanceId, sessionId);
|
|
598
709
|
if (fs.existsSync(turnsPath))
|
|
599
710
|
fs.unlinkSync(turnsPath);
|
|
600
711
|
if (fs.existsSync(metaPath))
|
|
@@ -648,7 +759,7 @@ class LocalSessionStore {
|
|
|
648
759
|
// UTILITIES
|
|
649
760
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
650
761
|
/**
|
|
651
|
-
* Get cache statistics
|
|
762
|
+
* Get cache statistics for current instance
|
|
652
763
|
*/
|
|
653
764
|
getStats() {
|
|
654
765
|
const sessions = this.listSessions();
|
|
@@ -656,7 +767,7 @@ class LocalSessionStore {
|
|
|
656
767
|
let cacheSize = 0;
|
|
657
768
|
for (const session of sessions) {
|
|
658
769
|
totalTurns += session.turn_count;
|
|
659
|
-
const turnsPath =
|
|
770
|
+
const turnsPath = (0, paths_js_1.getTurnsPath)(this.instanceId, session.session_id);
|
|
660
771
|
try {
|
|
661
772
|
const stats = fs.statSync(turnsPath);
|
|
662
773
|
cacheSize += stats.size;
|
|
@@ -669,10 +780,11 @@ class LocalSessionStore {
|
|
|
669
780
|
session_count: sessions.length,
|
|
670
781
|
total_turns: totalTurns,
|
|
671
782
|
cache_size_bytes: cacheSize,
|
|
783
|
+
instance_id: this.instanceId,
|
|
672
784
|
};
|
|
673
785
|
}
|
|
674
786
|
/**
|
|
675
|
-
* Clear all local cache (use with caution)
|
|
787
|
+
* Clear all local cache for current instance (use with caution)
|
|
676
788
|
*/
|
|
677
789
|
clearAll() {
|
|
678
790
|
const sessions = this.listSessions();
|
|
@@ -684,5 +796,9 @@ class LocalSessionStore {
|
|
|
684
796
|
}
|
|
685
797
|
}
|
|
686
798
|
exports.LocalSessionStore = LocalSessionStore;
|
|
687
|
-
// Export singleton instance
|
|
799
|
+
// Export factory function instead of singleton (to support instance-scoped usage)
|
|
800
|
+
function createLocalSessionStore(instanceId) {
|
|
801
|
+
return new LocalSessionStore({}, instanceId);
|
|
802
|
+
}
|
|
803
|
+
// Export default instance using EKKOS_INSTANCE_ID from env
|
|
688
804
|
exports.localCache = new LocalSessionStore();
|
package/dist/cache/capture.d.ts
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* ekkOS Fast Capture & Restore - CLI for local cache operations
|
|
3
|
+
* ekkOS Fast Capture & Restore - CLI for local cache operations (Instance-Aware)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
|
|
6
|
+
* - All Tier 0 cache paths MUST be: ~/.ekkos/cache/sessions/{instanceId}/{sessionId}.jsonl
|
|
7
|
+
* - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
8
|
+
*
|
|
9
|
+
* Capture Commands (NEW format with instanceId):
|
|
10
|
+
* capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
|
|
11
|
+
* capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
|
|
12
|
+
*
|
|
13
|
+
* Capture Commands (LEGACY format - backward compatible, uses default instanceId):
|
|
6
14
|
* capture user <session_id> <session_name> <turn_id> <query> [project_path]
|
|
7
15
|
* capture response <session_id> <turn_id> <response> [tools] [files]
|
|
8
16
|
*
|
|
9
17
|
* Restore Commands:
|
|
10
|
-
* capture restore [session_name] [--json|--markdown|--n=N]
|
|
18
|
+
* capture restore [session_name] [--json|--markdown|--n=N] [--instance=ID]
|
|
11
19
|
*
|
|
12
|
-
* ACK & Sync Commands
|
|
13
|
-
* capture ack <session_id> <turn_id>
|
|
14
|
-
* capture sync [session_id]
|
|
15
|
-
* capture prune <session_id>
|
|
16
|
-
* capture cleanup
|
|
20
|
+
* ACK & Sync Commands:
|
|
21
|
+
* capture ack <session_id> <turn_id> [--instance=ID]
|
|
22
|
+
* capture sync [session_id] [--instance=ID]
|
|
23
|
+
* capture prune <session_id> [--instance=ID]
|
|
24
|
+
* capture cleanup [--instance=ID]
|
|
17
25
|
*
|
|
18
26
|
* Query Commands:
|
|
19
|
-
* capture list
|
|
20
|
-
* capture get <session_id> [n]
|
|
21
|
-
* capture stats
|
|
27
|
+
* capture list [--instance=ID|--all]
|
|
28
|
+
* capture get <session_id> [n] [--instance=ID]
|
|
29
|
+
* capture stats [--instance=ID]
|
|
22
30
|
*
|
|
23
31
|
* This is a lightweight script designed for hook integration.
|
|
24
32
|
* Writes to local JSONL cache with minimal latency.
|