@ekkos/cli 0.2.18 → 0.3.3
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/LICENSE +21 -0
- package/dist/capture/eviction-client.d.ts +139 -0
- package/dist/capture/eviction-client.js +454 -0
- package/dist/capture/index.d.ts +2 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/jsonl-rewriter.d.ts +96 -0
- package/dist/capture/jsonl-rewriter.js +1369 -0
- package/dist/capture/transcript-repair.d.ts +50 -0
- package/dist/capture/transcript-repair.js +308 -0
- package/dist/commands/doctor.js +23 -1
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +1229 -293
- package/dist/commands/usage.d.ts +7 -0
- package/dist/commands/usage.js +214 -0
- package/dist/cron/index.d.ts +7 -0
- package/dist/cron/index.js +13 -0
- package/dist/cron/promoter.d.ts +70 -0
- package/dist/cron/promoter.js +403 -0
- package/dist/index.js +24 -3
- package/dist/lib/usage-monitor.d.ts +47 -0
- package/dist/lib/usage-monitor.js +124 -0
- package/dist/lib/usage-parser.d.ts +72 -0
- package/dist/lib/usage-parser.js +238 -0
- package/dist/restore/RestoreOrchestrator.d.ts +4 -0
- package/dist/restore/RestoreOrchestrator.js +118 -30
- package/package.json +12 -12
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +2 -2
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/session-start.sh +0 -0
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/agents/README.md +0 -182
- package/templates/agents/code-reviewer.md +0 -166
- package/templates/agents/debug-detective.md +0 -169
- package/templates/agents/ekkOS_Vercel.md +0 -99
- package/templates/agents/extension-manager.md +0 -229
- package/templates/agents/git-companion.md +0 -185
- package/templates/agents/github-test-agent.md +0 -321
- package/templates/agents/railway-manager.md +0 -215
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ekkos Technologies Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVICTION CLIENT
|
|
3
|
+
* ================
|
|
4
|
+
*
|
|
5
|
+
* Client module for the Handshake Eviction Protocol.
|
|
6
|
+
* Provides functions to call ekkOS_EvictPrepare and ekkOS_EvictConfirm
|
|
7
|
+
* ensuring zero data loss during context eviction.
|
|
8
|
+
*
|
|
9
|
+
* Protocol:
|
|
10
|
+
* 1. prepareEviction() - Write to R2, get ACK
|
|
11
|
+
* 2. (caller deletes locally)
|
|
12
|
+
* 3. confirmEviction() - Mark as committed
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Retry with exponential backoff
|
|
16
|
+
* - Timeout handling
|
|
17
|
+
* - Fallback detection
|
|
18
|
+
* - Idempotency via clientNonce
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Queue a failed eviction for retry when R2 reconnects
|
|
22
|
+
*/
|
|
23
|
+
export declare function queueEvictionForRetry(messages: Message[], indices: number[], estimatedTokens: number, reason: 'threshold' | 'emergency' | 'manual', options: HandshakeEvictionOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Drain the retry queue — called after a successful handshake
|
|
26
|
+
* Non-blocking: runs in background, doesn't delay current eviction
|
|
27
|
+
*/
|
|
28
|
+
export declare function drainRetryQueue(apiUrl: string, authToken: string): Promise<{
|
|
29
|
+
drained: number;
|
|
30
|
+
failed: number;
|
|
31
|
+
remaining: number;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Get retry queue stats (for monitoring)
|
|
35
|
+
*/
|
|
36
|
+
export declare function getRetryQueueStats(): {
|
|
37
|
+
size: number;
|
|
38
|
+
oldestAge: number | null;
|
|
39
|
+
};
|
|
40
|
+
interface ContentBlock {
|
|
41
|
+
type: 'text' | 'image' | 'tool_use' | 'tool_result' | 'thinking';
|
|
42
|
+
text?: string;
|
|
43
|
+
id?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
input?: Record<string, unknown>;
|
|
46
|
+
tool_use_id?: string;
|
|
47
|
+
content?: string | ContentBlock[];
|
|
48
|
+
thinking?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface Message {
|
|
51
|
+
role: 'user' | 'assistant';
|
|
52
|
+
content: string | ContentBlock[];
|
|
53
|
+
}
|
|
54
|
+
interface EvictionManifest {
|
|
55
|
+
evictionId: string;
|
|
56
|
+
messageIndices: number[];
|
|
57
|
+
fingerprints: string[];
|
|
58
|
+
estimatedTokens: number;
|
|
59
|
+
evictionReason: 'threshold' | 'emergency' | 'manual';
|
|
60
|
+
}
|
|
61
|
+
interface PrepareResult {
|
|
62
|
+
success: boolean;
|
|
63
|
+
evictionId: string;
|
|
64
|
+
r2Key?: string;
|
|
65
|
+
bytesWritten?: number;
|
|
66
|
+
checksum?: string;
|
|
67
|
+
status?: 'prepared' | 'already_exists';
|
|
68
|
+
error?: string;
|
|
69
|
+
clientNonce: string;
|
|
70
|
+
}
|
|
71
|
+
interface ConfirmResult {
|
|
72
|
+
success: boolean;
|
|
73
|
+
status?: 'committed' | 'already_committed' | 'not_found';
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create deterministic eviction ID from messages
|
|
78
|
+
* Same messages = same evictionId (enables dedup)
|
|
79
|
+
*/
|
|
80
|
+
export declare function createEvictionId(messages: Message[]): string;
|
|
81
|
+
/**
|
|
82
|
+
* Create fingerprint for a message
|
|
83
|
+
*/
|
|
84
|
+
export declare function createFingerprint(msg: Message): string;
|
|
85
|
+
/**
|
|
86
|
+
* Check if the eviction API is available
|
|
87
|
+
* Returns true if proxy is reachable and healthy
|
|
88
|
+
*/
|
|
89
|
+
export declare function checkEvictionHealth(apiUrl: string, authToken: string): Promise<boolean>;
|
|
90
|
+
/**
|
|
91
|
+
* Prepare eviction by writing to R2 via the proxy.
|
|
92
|
+
* Returns ACK with evictionId if successful.
|
|
93
|
+
* Caller must NOT delete locally until this succeeds.
|
|
94
|
+
*/
|
|
95
|
+
export declare function prepareEviction(apiUrl: string, authToken: string, sessionId: string, sessionName: string, userId: string, tenantId: string, messages: Message[], manifest: EvictionManifest, projectPath?: string): Promise<PrepareResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Confirm eviction after local deletion.
|
|
98
|
+
* This is optional but recommended for audit completeness.
|
|
99
|
+
* Failure here is non-critical - data is already safe in R2.
|
|
100
|
+
*/
|
|
101
|
+
export declare function confirmEviction(apiUrl: string, authToken: string, evictionId: string, clientNonce: string, localDeletedCount: number): Promise<ConfirmResult>;
|
|
102
|
+
export interface HandshakeEvictionOptions {
|
|
103
|
+
apiUrl: string;
|
|
104
|
+
authToken: string;
|
|
105
|
+
sessionId: string;
|
|
106
|
+
sessionName: string;
|
|
107
|
+
userId: string;
|
|
108
|
+
tenantId: string;
|
|
109
|
+
projectPath?: string;
|
|
110
|
+
}
|
|
111
|
+
export interface HandshakeEvictionResult {
|
|
112
|
+
success: boolean;
|
|
113
|
+
evictionId: string;
|
|
114
|
+
r2Key?: string;
|
|
115
|
+
bytesWritten?: number;
|
|
116
|
+
status: 'prepared' | 'committed' | 'failed' | 'skipped';
|
|
117
|
+
error?: string;
|
|
118
|
+
clientNonce?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Full handshake eviction - call this instead of fire-and-forget
|
|
122
|
+
*
|
|
123
|
+
* Returns:
|
|
124
|
+
* - success=true, status='prepared' → Safe to delete locally, then call confirmEviction
|
|
125
|
+
* - success=false → DO NOT delete locally
|
|
126
|
+
*/
|
|
127
|
+
export declare function handshakeEviction(messages: Message[], indices: number[], estimatedTokens: number, reason: 'threshold' | 'emergency' | 'manual', options: HandshakeEvictionOptions): Promise<HandshakeEvictionResult>;
|
|
128
|
+
declare const _default: {
|
|
129
|
+
prepareEviction: typeof prepareEviction;
|
|
130
|
+
confirmEviction: typeof confirmEviction;
|
|
131
|
+
handshakeEviction: typeof handshakeEviction;
|
|
132
|
+
checkEvictionHealth: typeof checkEvictionHealth;
|
|
133
|
+
createEvictionId: typeof createEvictionId;
|
|
134
|
+
createFingerprint: typeof createFingerprint;
|
|
135
|
+
queueEvictionForRetry: typeof queueEvictionForRetry;
|
|
136
|
+
drainRetryQueue: typeof drainRetryQueue;
|
|
137
|
+
getRetryQueueStats: typeof getRetryQueueStats;
|
|
138
|
+
};
|
|
139
|
+
export default _default;
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* EVICTION CLIENT
|
|
4
|
+
* ================
|
|
5
|
+
*
|
|
6
|
+
* Client module for the Handshake Eviction Protocol.
|
|
7
|
+
* Provides functions to call ekkOS_EvictPrepare and ekkOS_EvictConfirm
|
|
8
|
+
* ensuring zero data loss during context eviction.
|
|
9
|
+
*
|
|
10
|
+
* Protocol:
|
|
11
|
+
* 1. prepareEviction() - Write to R2, get ACK
|
|
12
|
+
* 2. (caller deletes locally)
|
|
13
|
+
* 3. confirmEviction() - Mark as committed
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Retry with exponential backoff
|
|
17
|
+
* - Timeout handling
|
|
18
|
+
* - Fallback detection
|
|
19
|
+
* - Idempotency via clientNonce
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.queueEvictionForRetry = queueEvictionForRetry;
|
|
56
|
+
exports.drainRetryQueue = drainRetryQueue;
|
|
57
|
+
exports.getRetryQueueStats = getRetryQueueStats;
|
|
58
|
+
exports.createEvictionId = createEvictionId;
|
|
59
|
+
exports.createFingerprint = createFingerprint;
|
|
60
|
+
exports.checkEvictionHealth = checkEvictionHealth;
|
|
61
|
+
exports.prepareEviction = prepareEviction;
|
|
62
|
+
exports.confirmEviction = confirmEviction;
|
|
63
|
+
exports.handshakeEviction = handshakeEviction;
|
|
64
|
+
const crypto_1 = require("crypto");
|
|
65
|
+
const fs = __importStar(require("fs"));
|
|
66
|
+
const path = __importStar(require("path"));
|
|
67
|
+
const os = __importStar(require("os"));
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
// RETRY QUEUE - Auto-sync when R2 reconnects
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
//
|
|
72
|
+
// When handshake fails (R2/proxy down), evictions are queued locally.
|
|
73
|
+
// On next successful handshake, the queue is drained automatically.
|
|
74
|
+
// Bounded: max 50 entries, oldest dropped on overflow.
|
|
75
|
+
//
|
|
76
|
+
const RETRY_QUEUE_PATH = path.join(os.homedir(), '.ekkos', 'eviction-retry-queue.jsonl');
|
|
77
|
+
const MAX_RETRY_QUEUE_SIZE = 50;
|
|
78
|
+
let isDraining = false;
|
|
79
|
+
/**
|
|
80
|
+
* Queue a failed eviction for retry when R2 reconnects
|
|
81
|
+
*/
|
|
82
|
+
function queueEvictionForRetry(messages, indices, estimatedTokens, reason, options) {
|
|
83
|
+
try {
|
|
84
|
+
const dir = path.dirname(RETRY_QUEUE_PATH);
|
|
85
|
+
if (!fs.existsSync(dir))
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
const entry = {
|
|
88
|
+
messages,
|
|
89
|
+
indices,
|
|
90
|
+
estimatedTokens,
|
|
91
|
+
reason,
|
|
92
|
+
options,
|
|
93
|
+
queuedAt: Date.now(),
|
|
94
|
+
attempts: 0,
|
|
95
|
+
};
|
|
96
|
+
// Read existing queue
|
|
97
|
+
let entries = [];
|
|
98
|
+
if (fs.existsSync(RETRY_QUEUE_PATH)) {
|
|
99
|
+
entries = fs.readFileSync(RETRY_QUEUE_PATH, 'utf-8')
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter(l => l.trim());
|
|
102
|
+
}
|
|
103
|
+
// Enforce max size (drop oldest)
|
|
104
|
+
if (entries.length >= MAX_RETRY_QUEUE_SIZE) {
|
|
105
|
+
entries = entries.slice(entries.length - MAX_RETRY_QUEUE_SIZE + 1);
|
|
106
|
+
}
|
|
107
|
+
entries.push(JSON.stringify(entry));
|
|
108
|
+
fs.writeFileSync(RETRY_QUEUE_PATH, entries.join('\n') + '\n');
|
|
109
|
+
console.log(`[EvictionRetry] Queued eviction for retry (${messages.length} msgs, queue size: ${entries.length})`);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.warn('[EvictionRetry] Failed to queue:', err instanceof Error ? err.message : err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Drain the retry queue — called after a successful handshake
|
|
117
|
+
* Non-blocking: runs in background, doesn't delay current eviction
|
|
118
|
+
*/
|
|
119
|
+
async function drainRetryQueue(apiUrl, authToken) {
|
|
120
|
+
if (isDraining)
|
|
121
|
+
return { drained: 0, failed: 0, remaining: -1 };
|
|
122
|
+
if (!fs.existsSync(RETRY_QUEUE_PATH))
|
|
123
|
+
return { drained: 0, failed: 0, remaining: 0 };
|
|
124
|
+
isDraining = true;
|
|
125
|
+
let drained = 0;
|
|
126
|
+
let failed = 0;
|
|
127
|
+
try {
|
|
128
|
+
const content = fs.readFileSync(RETRY_QUEUE_PATH, 'utf-8');
|
|
129
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
130
|
+
if (lines.length === 0) {
|
|
131
|
+
isDraining = false;
|
|
132
|
+
return { drained: 0, failed: 0, remaining: 0 };
|
|
133
|
+
}
|
|
134
|
+
console.log(`[EvictionRetry] Draining ${lines.length} queued evictions...`);
|
|
135
|
+
const remaining = [];
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
try {
|
|
138
|
+
const entry = JSON.parse(line);
|
|
139
|
+
// Skip entries older than 24 hours (data likely stale)
|
|
140
|
+
if (Date.now() - entry.queuedAt > 24 * 60 * 60 * 1000) {
|
|
141
|
+
console.log(`[EvictionRetry] Dropping stale entry (${((Date.now() - entry.queuedAt) / 3600000).toFixed(1)}h old)`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Skip entries that failed too many times
|
|
145
|
+
if (entry.attempts >= 3) {
|
|
146
|
+
console.log(`[EvictionRetry] Dropping entry after 3 failed attempts`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// Attempt handshake
|
|
150
|
+
const result = await handshakeEviction(entry.messages, entry.indices, entry.estimatedTokens, entry.reason, { ...entry.options, apiUrl, authToken });
|
|
151
|
+
if (result.success) {
|
|
152
|
+
drained++;
|
|
153
|
+
console.log(`[EvictionRetry] Successfully synced queued eviction ${result.evictionId}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
failed++;
|
|
157
|
+
entry.attempts++;
|
|
158
|
+
remaining.push(JSON.stringify(entry));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
failed++;
|
|
163
|
+
remaining.push(line); // Keep unparseable entries for manual review
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Rewrite queue with remaining entries
|
|
167
|
+
if (remaining.length > 0) {
|
|
168
|
+
fs.writeFileSync(RETRY_QUEUE_PATH, remaining.join('\n') + '\n');
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Queue fully drained — remove file
|
|
172
|
+
try {
|
|
173
|
+
fs.unlinkSync(RETRY_QUEUE_PATH);
|
|
174
|
+
}
|
|
175
|
+
catch { /* ok */ }
|
|
176
|
+
}
|
|
177
|
+
console.log(`[EvictionRetry] Drain complete: ${drained} synced, ${failed} failed, ${remaining.length} remaining`);
|
|
178
|
+
return { drained, failed, remaining: remaining.length };
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.warn('[EvictionRetry] Drain error:', err instanceof Error ? err.message : err);
|
|
182
|
+
return { drained, failed, remaining: -1 };
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
isDraining = false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get retry queue stats (for monitoring)
|
|
190
|
+
*/
|
|
191
|
+
function getRetryQueueStats() {
|
|
192
|
+
try {
|
|
193
|
+
if (!fs.existsSync(RETRY_QUEUE_PATH))
|
|
194
|
+
return { size: 0, oldestAge: null };
|
|
195
|
+
const content = fs.readFileSync(RETRY_QUEUE_PATH, 'utf-8');
|
|
196
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
197
|
+
if (lines.length === 0)
|
|
198
|
+
return { size: 0, oldestAge: null };
|
|
199
|
+
const oldest = JSON.parse(lines[0]);
|
|
200
|
+
return {
|
|
201
|
+
size: lines.length,
|
|
202
|
+
oldestAge: Date.now() - oldest.queuedAt,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return { size: 0, oldestAge: null };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
// CONFIGURATION
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
+
const EVICTION_TIMEOUT_MS = 30000; // 30 second timeout
|
|
213
|
+
const MAX_RETRIES = 3;
|
|
214
|
+
const HEALTH_CHECK_TIMEOUT_MS = 5000;
|
|
215
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
216
|
+
// UTILITY FUNCTIONS
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
+
function sleep(ms) {
|
|
219
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Create deterministic eviction ID from messages
|
|
223
|
+
* Same messages = same evictionId (enables dedup)
|
|
224
|
+
*/
|
|
225
|
+
function createEvictionId(messages) {
|
|
226
|
+
const data = JSON.stringify(messages);
|
|
227
|
+
return (0, crypto_1.createHash)('sha256').update(data).digest('hex').slice(0, 12);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Create fingerprint for a message
|
|
231
|
+
*/
|
|
232
|
+
function createFingerprint(msg) {
|
|
233
|
+
const data = msg.role + JSON.stringify(msg.content);
|
|
234
|
+
return (0, crypto_1.createHash)('sha256').update(data).digest('hex').slice(0, 16);
|
|
235
|
+
}
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
237
|
+
// HEALTH CHECK
|
|
238
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
239
|
+
/**
|
|
240
|
+
* Check if the eviction API is available
|
|
241
|
+
* Returns true if proxy is reachable and healthy
|
|
242
|
+
*/
|
|
243
|
+
async function checkEvictionHealth(apiUrl, authToken) {
|
|
244
|
+
try {
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
|
247
|
+
const response = await fetch(`${apiUrl}/api/v1/mcp/health`, {
|
|
248
|
+
method: 'GET',
|
|
249
|
+
headers: {
|
|
250
|
+
'Authorization': `Bearer ${authToken}`,
|
|
251
|
+
},
|
|
252
|
+
signal: controller.signal,
|
|
253
|
+
});
|
|
254
|
+
clearTimeout(timeoutId);
|
|
255
|
+
return response.ok;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
262
|
+
// PREPARE EVICTION (PHASE 1)
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
/**
|
|
265
|
+
* Prepare eviction by writing to R2 via the proxy.
|
|
266
|
+
* Returns ACK with evictionId if successful.
|
|
267
|
+
* Caller must NOT delete locally until this succeeds.
|
|
268
|
+
*/
|
|
269
|
+
async function prepareEviction(apiUrl, authToken, sessionId, sessionName, userId, tenantId, messages, manifest, projectPath) {
|
|
270
|
+
const clientNonce = (0, crypto_1.randomUUID)();
|
|
271
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
272
|
+
try {
|
|
273
|
+
const controller = new AbortController();
|
|
274
|
+
const timeoutId = setTimeout(() => controller.abort(), EVICTION_TIMEOUT_MS);
|
|
275
|
+
const response = await fetch(`${apiUrl}/api/v1/mcp/call`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
'Authorization': `Bearer ${authToken}`,
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
tool: 'ekkOS_EvictPrepare',
|
|
283
|
+
args: {
|
|
284
|
+
sessionId,
|
|
285
|
+
sessionName,
|
|
286
|
+
userId,
|
|
287
|
+
tenantId,
|
|
288
|
+
projectPath: projectPath || process.cwd(),
|
|
289
|
+
messages,
|
|
290
|
+
manifest,
|
|
291
|
+
clientNonce,
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
signal: controller.signal,
|
|
295
|
+
});
|
|
296
|
+
clearTimeout(timeoutId);
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
299
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
300
|
+
}
|
|
301
|
+
const data = await response.json();
|
|
302
|
+
if (!data.success) {
|
|
303
|
+
throw new Error(data.error || 'Prepare failed');
|
|
304
|
+
}
|
|
305
|
+
const result = data.result || data;
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
evictionId: result.evictionId || manifest.evictionId,
|
|
309
|
+
r2Key: result.r2Key,
|
|
310
|
+
bytesWritten: result.bytesWritten,
|
|
311
|
+
checksum: result.checksum,
|
|
312
|
+
status: result.status,
|
|
313
|
+
clientNonce,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
318
|
+
console.warn(`[EvictionClient] Prepare attempt ${attempt}/${MAX_RETRIES} failed: ${errorMessage}`);
|
|
319
|
+
if (attempt === MAX_RETRIES) {
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
evictionId: manifest.evictionId,
|
|
323
|
+
error: `Failed after ${MAX_RETRIES} attempts: ${errorMessage}`,
|
|
324
|
+
clientNonce,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// Exponential backoff: 1s, 2s, 4s
|
|
328
|
+
await sleep(Math.pow(2, attempt - 1) * 1000);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Should not reach here
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
evictionId: manifest.evictionId,
|
|
335
|
+
error: 'Unexpected failure',
|
|
336
|
+
clientNonce,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
340
|
+
// CONFIRM EVICTION (PHASE 2)
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
342
|
+
/**
|
|
343
|
+
* Confirm eviction after local deletion.
|
|
344
|
+
* This is optional but recommended for audit completeness.
|
|
345
|
+
* Failure here is non-critical - data is already safe in R2.
|
|
346
|
+
*/
|
|
347
|
+
async function confirmEviction(apiUrl, authToken, evictionId, clientNonce, localDeletedCount) {
|
|
348
|
+
try {
|
|
349
|
+
const controller = new AbortController();
|
|
350
|
+
const timeoutId = setTimeout(() => controller.abort(), EVICTION_TIMEOUT_MS);
|
|
351
|
+
const response = await fetch(`${apiUrl}/api/v1/mcp/call`, {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: {
|
|
354
|
+
'Content-Type': 'application/json',
|
|
355
|
+
'Authorization': `Bearer ${authToken}`,
|
|
356
|
+
},
|
|
357
|
+
body: JSON.stringify({
|
|
358
|
+
tool: 'ekkOS_EvictConfirm',
|
|
359
|
+
args: {
|
|
360
|
+
evictionId,
|
|
361
|
+
clientNonce,
|
|
362
|
+
localDeletedCount,
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
signal: controller.signal,
|
|
366
|
+
});
|
|
367
|
+
clearTimeout(timeoutId);
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: `HTTP ${response.status}`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const data = await response.json();
|
|
375
|
+
if (!data.success) {
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
error: data.error || 'Confirm failed',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const result = data.result || data;
|
|
382
|
+
return {
|
|
383
|
+
success: true,
|
|
384
|
+
status: result.status,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
// Non-critical - data is already safe in R2
|
|
389
|
+
console.warn(`[EvictionClient] Confirm failed (non-critical): ${err instanceof Error ? err.message : err}`);
|
|
390
|
+
return {
|
|
391
|
+
success: false,
|
|
392
|
+
error: err instanceof Error ? err.message : String(err),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Full handshake eviction - call this instead of fire-and-forget
|
|
398
|
+
*
|
|
399
|
+
* Returns:
|
|
400
|
+
* - success=true, status='prepared' → Safe to delete locally, then call confirmEviction
|
|
401
|
+
* - success=false → DO NOT delete locally
|
|
402
|
+
*/
|
|
403
|
+
async function handshakeEviction(messages, indices, estimatedTokens, reason, options) {
|
|
404
|
+
// Build manifest
|
|
405
|
+
const evictionId = createEvictionId(messages);
|
|
406
|
+
const fingerprints = messages.map(createFingerprint);
|
|
407
|
+
const manifest = {
|
|
408
|
+
evictionId,
|
|
409
|
+
messageIndices: indices,
|
|
410
|
+
fingerprints,
|
|
411
|
+
estimatedTokens,
|
|
412
|
+
evictionReason: reason,
|
|
413
|
+
};
|
|
414
|
+
// Check health first (fast fail)
|
|
415
|
+
const healthy = await checkEvictionHealth(options.apiUrl, options.authToken);
|
|
416
|
+
if (!healthy) {
|
|
417
|
+
console.warn('[EvictionClient] Proxy unavailable - handshake eviction skipped');
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
evictionId,
|
|
421
|
+
status: 'skipped',
|
|
422
|
+
error: 'Proxy unavailable',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Phase 1: Prepare
|
|
426
|
+
const prepareResult = await prepareEviction(options.apiUrl, options.authToken, options.sessionId, options.sessionName, options.userId, options.tenantId, messages, manifest, options.projectPath);
|
|
427
|
+
if (!prepareResult.success) {
|
|
428
|
+
return {
|
|
429
|
+
success: false,
|
|
430
|
+
evictionId,
|
|
431
|
+
status: 'failed',
|
|
432
|
+
error: prepareResult.error,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
success: true,
|
|
437
|
+
evictionId: prepareResult.evictionId,
|
|
438
|
+
r2Key: prepareResult.r2Key,
|
|
439
|
+
bytesWritten: prepareResult.bytesWritten,
|
|
440
|
+
status: 'prepared',
|
|
441
|
+
clientNonce: prepareResult.clientNonce,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
exports.default = {
|
|
445
|
+
prepareEviction,
|
|
446
|
+
confirmEviction,
|
|
447
|
+
handshakeEviction,
|
|
448
|
+
checkEvictionHealth,
|
|
449
|
+
createEvictionId,
|
|
450
|
+
createFingerprint,
|
|
451
|
+
queueEvictionForRetry,
|
|
452
|
+
drainRetryQueue,
|
|
453
|
+
getRetryQueueStats,
|
|
454
|
+
};
|
package/dist/capture/index.d.ts
CHANGED
package/dist/capture/index.js
CHANGED
|
@@ -22,3 +22,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
__exportStar(require("./types"), exports);
|
|
24
24
|
__exportStar(require("./stream-tailer"), exports);
|
|
25
|
+
__exportStar(require("./jsonl-rewriter"), exports);
|
|
26
|
+
__exportStar(require("./transcript-repair"), exports);
|