@loreai/gateway 0.14.0 → 0.14.1
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/bin.cjs +27 -0
- package/dist/index.cjs +1042 -0
- package/dist/index.d.cts +21 -0
- package/package.json +10 -10
- package/dist/index.js +0 -50087
- package/src/auth.ts +0 -133
- package/src/batch-queue.ts +0 -575
- package/src/cache-analytics.ts +0 -344
- package/src/cli/agents.ts +0 -107
- package/src/cli/bin.ts +0 -11
- package/src/cli/help.ts +0 -55
- package/src/cli/lib/binary.ts +0 -353
- package/src/cli/lib/bspatch.ts +0 -306
- package/src/cli/lib/delta-upgrade.ts +0 -790
- package/src/cli/lib/errors.ts +0 -48
- package/src/cli/lib/ghcr.ts +0 -389
- package/src/cli/lib/patch-cache.ts +0 -342
- package/src/cli/lib/upgrade.ts +0 -454
- package/src/cli/lib/version-check.ts +0 -385
- package/src/cli/main.ts +0 -152
- package/src/cli/run.ts +0 -181
- package/src/cli/start.ts +0 -82
- package/src/cli/upgrade.ts +0 -311
- package/src/cli/version.ts +0 -22
- package/src/compaction.ts +0 -195
- package/src/config.ts +0 -199
- package/src/idle.ts +0 -240
- package/src/index.ts +0 -41
- package/src/llm-adapter.ts +0 -182
- package/src/pipeline.ts +0 -1681
- package/src/recall.ts +0 -433
- package/src/recorder.ts +0 -192
- package/src/server.ts +0 -250
- package/src/session.ts +0 -207
- package/src/stream/anthropic.ts +0 -708
- package/src/temporal-adapter.ts +0 -310
- package/src/translate/anthropic.ts +0 -469
- package/src/translate/openai.ts +0 -536
- package/src/translate/types.ts +0 -222
- package/src/worker-model.ts +0 -408
package/src/server.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP server for the Lore gateway proxy.
|
|
3
|
-
*
|
|
4
|
-
* Routes:
|
|
5
|
-
* POST /v1/messages → Anthropic protocol (Phase 1)
|
|
6
|
-
* POST /v1/chat/completions → OpenAI protocol (Phase 2 stub)
|
|
7
|
-
* GET /v1/models → Passthrough to upstream
|
|
8
|
-
* GET /health → Health check
|
|
9
|
-
*
|
|
10
|
-
* Uses `Bun.serve()` — this package targets Bun exclusively.
|
|
11
|
-
*/
|
|
12
|
-
import type { GatewayConfig } from "./config";
|
|
13
|
-
import type { GatewayRequest } from "./translate/types";
|
|
14
|
-
import { parseAnthropicRequest } from "./translate/anthropic";
|
|
15
|
-
import { parseOpenAIRequest, buildOpenAIResponse } from "./translate/openai";
|
|
16
|
-
import { accumulateSSEResponse } from "./stream/anthropic";
|
|
17
|
-
import { handleRequest } from "./pipeline";
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Version — best-effort from package.json, falls back gracefully
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
let version = "unknown";
|
|
24
|
-
try {
|
|
25
|
-
// Bun resolves JSON imports; use require for sync + no top-level await
|
|
26
|
-
const pkg = require("../package.json") as { version?: string };
|
|
27
|
-
if (pkg.version) version = pkg.version;
|
|
28
|
-
} catch {
|
|
29
|
-
// Not critical — health endpoint will report "unknown"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// CORS headers — permissive for localhost development
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const CORS_HEADERS: Record<string, string> = {
|
|
37
|
-
"access-control-allow-origin": "*",
|
|
38
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
39
|
-
"access-control-allow-headers": "*",
|
|
40
|
-
"access-control-max-age": "86400",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function withCors(response: Response): Response {
|
|
44
|
-
for (const [key, value] of Object.entries(CORS_HEADERS)) {
|
|
45
|
-
response.headers.set(key, value);
|
|
46
|
-
}
|
|
47
|
-
return response;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Helpers
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/** Convert Bun's Headers object to a plain Record<string, string>. */
|
|
55
|
-
function headersToRecord(headers: Headers): Record<string, string> {
|
|
56
|
-
const record: Record<string, string> = {};
|
|
57
|
-
headers.forEach((value, key) => {
|
|
58
|
-
record[key] = value;
|
|
59
|
-
});
|
|
60
|
-
return record;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function jsonResponse(body: unknown, status = 200): Response {
|
|
64
|
-
return withCors(
|
|
65
|
-
new Response(JSON.stringify(body), {
|
|
66
|
-
status,
|
|
67
|
-
headers: { "content-type": "application/json" },
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function errorResponse(
|
|
73
|
-
status: number,
|
|
74
|
-
type: string,
|
|
75
|
-
message: string,
|
|
76
|
-
): Response {
|
|
77
|
-
return jsonResponse(
|
|
78
|
-
{
|
|
79
|
-
type: "error",
|
|
80
|
-
error: { type, message },
|
|
81
|
-
},
|
|
82
|
-
status,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// Route handlers
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
async function handleAnthropicMessages(
|
|
91
|
-
req: Request,
|
|
92
|
-
config: GatewayConfig,
|
|
93
|
-
): Promise<Response> {
|
|
94
|
-
let body: unknown;
|
|
95
|
-
try {
|
|
96
|
-
body = await req.json();
|
|
97
|
-
} catch {
|
|
98
|
-
return errorResponse(400, "invalid_request_error", "Invalid JSON body");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let gatewayReq: GatewayRequest;
|
|
102
|
-
try {
|
|
103
|
-
gatewayReq = parseAnthropicRequest(body, headersToRecord(req.headers));
|
|
104
|
-
} catch (e) {
|
|
105
|
-
const msg = e instanceof Error ? e.message : "Failed to parse request";
|
|
106
|
-
return errorResponse(400, "invalid_request_error", msg);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const result = await handleRequest(gatewayReq, config);
|
|
111
|
-
// Pipeline returns a Response directly (streaming or non-streaming)
|
|
112
|
-
return withCors(result);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
const msg = e instanceof Error ? e.message : "Pipeline error";
|
|
115
|
-
console.error(`[lore] pipeline error: ${msg}`);
|
|
116
|
-
return errorResponse(502, "api_error", `Gateway pipeline error: ${msg}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function handleModelsPassthrough(config: GatewayConfig): Promise<Response> {
|
|
121
|
-
try {
|
|
122
|
-
const upstream = await fetch(`${config.upstreamAnthropic}/v1/models`, {
|
|
123
|
-
headers: { "content-type": "application/json" },
|
|
124
|
-
});
|
|
125
|
-
// Clone to a new Response so we can append CORS headers
|
|
126
|
-
const response = new Response(upstream.body, {
|
|
127
|
-
status: upstream.status,
|
|
128
|
-
statusText: upstream.statusText,
|
|
129
|
-
headers: new Headers(upstream.headers),
|
|
130
|
-
});
|
|
131
|
-
return withCors(response);
|
|
132
|
-
} catch (e) {
|
|
133
|
-
const msg = e instanceof Error ? e.message : "Upstream unreachable";
|
|
134
|
-
return errorResponse(502, "api_error", `Failed to fetch models: ${msg}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function handleHealth(): Response {
|
|
139
|
-
return jsonResponse({ status: "ok", version });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function handleOpenAIChatCompletions(
|
|
143
|
-
req: Request,
|
|
144
|
-
config: GatewayConfig,
|
|
145
|
-
): Promise<Response> {
|
|
146
|
-
let body: unknown;
|
|
147
|
-
try {
|
|
148
|
-
body = await req.json();
|
|
149
|
-
} catch {
|
|
150
|
-
return errorResponse(400, "invalid_request_error", "Invalid JSON body");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let gatewayReq: GatewayRequest;
|
|
154
|
-
try {
|
|
155
|
-
gatewayReq = parseOpenAIRequest(body, headersToRecord(req.headers));
|
|
156
|
-
} catch (e) {
|
|
157
|
-
const msg = e instanceof Error ? e.message : "Failed to parse request";
|
|
158
|
-
return errorResponse(400, "invalid_request_error", msg);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let pipelineResp: Response;
|
|
162
|
-
try {
|
|
163
|
-
pipelineResp = await handleRequest(gatewayReq, config);
|
|
164
|
-
} catch (e) {
|
|
165
|
-
const msg = e instanceof Error ? e.message : "Pipeline error";
|
|
166
|
-
console.error(`[lore] pipeline error: ${msg}`);
|
|
167
|
-
return errorResponse(502, "api_error", `Gateway pipeline error: ${msg}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Pipeline always returns internal Anthropic-format response.
|
|
171
|
-
// Translate back to OpenAI format before returning to the client.
|
|
172
|
-
if (!pipelineResp.ok) {
|
|
173
|
-
// Upstream or pipeline error — forward as-is
|
|
174
|
-
return withCors(pipelineResp);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const contentType = pipelineResp.headers.get("content-type") ?? "";
|
|
178
|
-
if (contentType.includes("text/event-stream")) {
|
|
179
|
-
// Streaming: accumulate the internal SSE then re-emit as OpenAI SSE
|
|
180
|
-
const accumulated = await accumulateSSEResponse(pipelineResp);
|
|
181
|
-
return withCors(buildOpenAIResponse(accumulated, true));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Non-streaming: translate JSON body
|
|
185
|
-
const respBody = await pipelineResp.json();
|
|
186
|
-
return withCors(buildOpenAIResponse(respBody, false));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
// Server
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
export function startServer(config: GatewayConfig): {
|
|
194
|
-
stop: () => void;
|
|
195
|
-
port: number;
|
|
196
|
-
} {
|
|
197
|
-
const server = Bun.serve({
|
|
198
|
-
port: config.port,
|
|
199
|
-
hostname: config.host,
|
|
200
|
-
|
|
201
|
-
async fetch(req: Request): Promise<Response> {
|
|
202
|
-
const url = new URL(req.url);
|
|
203
|
-
const { pathname } = url;
|
|
204
|
-
const method = req.method;
|
|
205
|
-
|
|
206
|
-
// CORS preflight
|
|
207
|
-
if (method === "OPTIONS") {
|
|
208
|
-
return withCors(new Response(null, { status: 204 }));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (config.debug) {
|
|
212
|
-
console.error(`[lore] ${method} ${pathname}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
// POST /v1/messages — Anthropic protocol
|
|
217
|
-
if (method === "POST" && pathname === "/v1/messages") {
|
|
218
|
-
return await handleAnthropicMessages(req, config);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// POST /v1/chat/completions — OpenAI protocol
|
|
222
|
-
if (method === "POST" && pathname === "/v1/chat/completions") {
|
|
223
|
-
return await handleOpenAIChatCompletions(req, config);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// GET /v1/models — passthrough
|
|
227
|
-
if (method === "GET" && pathname === "/v1/models") {
|
|
228
|
-
return await handleModelsPassthrough(config);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// GET /health — health check
|
|
232
|
-
if (method === "GET" && pathname === "/health") {
|
|
233
|
-
return handleHealth();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// 404 for everything else
|
|
237
|
-
return errorResponse(404, "not_found", `No route for ${method} ${pathname}`);
|
|
238
|
-
} catch (e) {
|
|
239
|
-
const msg = e instanceof Error ? e.message : "Internal server error";
|
|
240
|
-
console.error(`[lore] uncaught error: ${msg}`);
|
|
241
|
-
return errorResponse(500, "api_error", msg);
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
stop: () => server.stop(),
|
|
248
|
-
port: server.port ?? config.port,
|
|
249
|
-
};
|
|
250
|
-
}
|
package/src/session.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session identification for the Lore gateway proxy.
|
|
3
|
-
*
|
|
4
|
-
* Raw LLM API requests carry no session ID, so the gateway injects a
|
|
5
|
-
* text-block marker `[lore:<base62>]` into the first response of a new
|
|
6
|
-
* session. Subsequent requests from the same session echo it back in
|
|
7
|
-
* the message history, allowing the gateway to correlate turns.
|
|
8
|
-
*
|
|
9
|
-
* The session ID packs 8 random bytes + 4 bytes of unix timestamp
|
|
10
|
-
* (seconds, big-endian) into 12 bytes, then base62-encodes them to a
|
|
11
|
-
* compact alphanumeric string (~17 chars).
|
|
12
|
-
*
|
|
13
|
-
* A SHA-256 fingerprint of the first user message serves as a
|
|
14
|
-
* belt-and-suspenders fallback for sessions that haven't received their
|
|
15
|
-
* marker yet (e.g. the very first request before any response).
|
|
16
|
-
*
|
|
17
|
-
* This module has zero dependencies on `@loreai/core` — pure utility.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Base62 encoding
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
const BASE62_ALPHABET =
|
|
25
|
-
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
26
|
-
const BASE = 62n;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Encode a byte array to a base62 string.
|
|
30
|
-
*
|
|
31
|
-
* Interprets `bytes` as an unsigned big-endian integer, then repeatedly
|
|
32
|
-
* divmods by 62, mapping each remainder to `BASE62_ALPHABET`. The result
|
|
33
|
-
* is reversed so the most-significant digit comes first and zero-padded
|
|
34
|
-
* to `minLength` for consistent output width.
|
|
35
|
-
*/
|
|
36
|
-
export function base62Encode(bytes: Uint8Array, minLength = 0): string {
|
|
37
|
-
let n = 0n;
|
|
38
|
-
for (const b of bytes) {
|
|
39
|
-
n = (n << 8n) | BigInt(b);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (n === 0n) return BASE62_ALPHABET[0].repeat(Math.max(1, minLength));
|
|
43
|
-
|
|
44
|
-
const chars: string[] = [];
|
|
45
|
-
while (n > 0n) {
|
|
46
|
-
chars.push(BASE62_ALPHABET[Number(n % BASE)]);
|
|
47
|
-
n /= BASE;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
chars.reverse();
|
|
51
|
-
|
|
52
|
-
// Pad to minLength for consistent width
|
|
53
|
-
while (chars.length < minLength) {
|
|
54
|
-
chars.unshift(BASE62_ALPHABET[0]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return chars.join("");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Session ID generation
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/** 12 bytes → base62 → at most 17 alphanumeric characters. */
|
|
65
|
-
const SESSION_ID_MIN_LENGTH = 17;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Generate a new session ID.
|
|
69
|
-
*
|
|
70
|
-
* Layout (12 bytes):
|
|
71
|
-
* [0..7] — 8 random bytes (session hash)
|
|
72
|
-
* [8..11] — 4 bytes unix timestamp (seconds, big-endian)
|
|
73
|
-
*/
|
|
74
|
-
export function generateSessionID(): string {
|
|
75
|
-
const buf = new Uint8Array(12);
|
|
76
|
-
crypto.getRandomValues(buf.subarray(0, 8));
|
|
77
|
-
|
|
78
|
-
const ts = Math.floor(Date.now() / 1000);
|
|
79
|
-
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
80
|
-
view.setUint32(8, ts >>> 0, false); // big-endian
|
|
81
|
-
|
|
82
|
-
return base62Encode(buf, SESSION_ID_MIN_LENGTH);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Marker formatting / parsing
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
const MARKER_RE = /\[lore:([a-zA-Z0-9]+)\]/;
|
|
90
|
-
|
|
91
|
-
/** Format a session ID as the injectable text marker. */
|
|
92
|
-
export function formatMarker(sessionID: string): string {
|
|
93
|
-
return `[lore:${sessionID}]`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Extract a session ID from a marker string, or `null` if the text
|
|
98
|
-
* does not contain a valid marker.
|
|
99
|
-
*/
|
|
100
|
-
export function parseMarker(text: string): string | null {
|
|
101
|
-
const m = MARKER_RE.exec(text);
|
|
102
|
-
return m ? m[1] : null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Message scanning
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Extract text from a single message's content field.
|
|
111
|
-
*
|
|
112
|
-
* Handles both Anthropic-style content (array of `{type:"text", text}` blocks)
|
|
113
|
-
* and OpenAI-style content (plain string).
|
|
114
|
-
*/
|
|
115
|
-
function extractTextParts(content: unknown): string[] {
|
|
116
|
-
if (typeof content === "string") return [content];
|
|
117
|
-
|
|
118
|
-
if (Array.isArray(content)) {
|
|
119
|
-
const texts: string[] = [];
|
|
120
|
-
for (const block of content) {
|
|
121
|
-
if (
|
|
122
|
-
block &&
|
|
123
|
-
typeof block === "object" &&
|
|
124
|
-
"type" in block &&
|
|
125
|
-
block.type === "text" &&
|
|
126
|
-
"text" in block &&
|
|
127
|
-
typeof block.text === "string"
|
|
128
|
-
) {
|
|
129
|
-
texts.push(block.text);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return texts;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Scan a message array for a `[lore:<sessionID>]` marker inside any
|
|
140
|
-
* text content block. Returns the extracted session ID or `null`.
|
|
141
|
-
*/
|
|
142
|
-
export function scanForMarker(
|
|
143
|
-
messages: Array<{ role: string; content: unknown }>,
|
|
144
|
-
): string | null {
|
|
145
|
-
for (const msg of messages) {
|
|
146
|
-
for (const text of extractTextParts(msg.content)) {
|
|
147
|
-
const id = parseMarker(text);
|
|
148
|
-
if (id) return id;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Fingerprinting (fallback)
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Compute a SHA-256 fingerprint from the first user message's content,
|
|
160
|
-
* optionally incorporating the model name and an auth credential suffix.
|
|
161
|
-
*
|
|
162
|
-
* Returns the first 16 hex characters of the hash. Used as the primary
|
|
163
|
-
* session correlator — combined with message-count proximity to
|
|
164
|
-
* disambiguate forked sessions that share the same first message.
|
|
165
|
-
*
|
|
166
|
-
* Including `model` and `authSuffix` ensures that a key change or model
|
|
167
|
-
* switch creates a new session rather than reusing an existing one.
|
|
168
|
-
*/
|
|
169
|
-
export async function fingerprintMessages(
|
|
170
|
-
messages: Array<{ role: string; content: unknown }>,
|
|
171
|
-
extras?: { model?: string; authSuffix?: string },
|
|
172
|
-
): Promise<string> {
|
|
173
|
-
let firstUserContent = "";
|
|
174
|
-
for (const msg of messages) {
|
|
175
|
-
if (msg.role === "user") {
|
|
176
|
-
const texts = extractTextParts(msg.content);
|
|
177
|
-
firstUserContent = texts.join("");
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const material =
|
|
183
|
-
firstUserContent + (extras?.model ?? "") + (extras?.authSuffix ?? "");
|
|
184
|
-
const encoded = new TextEncoder().encode(material);
|
|
185
|
-
const hash = await crypto.subtle.digest("SHA-256", encoded);
|
|
186
|
-
const bytes = new Uint8Array(hash);
|
|
187
|
-
|
|
188
|
-
// First 16 hex chars (8 bytes)
|
|
189
|
-
let hex = "";
|
|
190
|
-
for (let i = 0; i < 8; i++) {
|
|
191
|
-
hex += bytes[i].toString(16).padStart(2, "0");
|
|
192
|
-
}
|
|
193
|
-
return hex;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// Message-count proximity matching
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Maximum message count difference for two requests to be considered
|
|
202
|
-
* part of the same session. Normal turns add 2–6 messages (user +
|
|
203
|
-
* assistant + tool calls); a forked session drops to the fork point.
|
|
204
|
-
* A threshold of 20 accommodates bursts of tool-call messages while
|
|
205
|
-
* reliably distinguishing forks (which typically differ by 50+).
|
|
206
|
-
*/
|
|
207
|
-
export const MESSAGE_COUNT_PROXIMITY_THRESHOLD = 20;
|