@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/compaction.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compaction request detection and interception for the Lore gateway.
|
|
3
|
-
*
|
|
4
|
-
* Claude Code (and other clients using the same pattern) sends compaction
|
|
5
|
-
* requests with a distinct system prompt and message structure. The gateway
|
|
6
|
-
* detects these and runs Lore's own distillation instead of forwarding to
|
|
7
|
-
* the upstream API.
|
|
8
|
-
*
|
|
9
|
-
* Detection mirrors the patterns documented in the upstream
|
|
10
|
-
* `packages/opencode/src/agent/prompt/compaction.txt` and the
|
|
11
|
-
* `experimental.session.compacting` hook.
|
|
12
|
-
*
|
|
13
|
-
* This module has zero dependencies on `@loreai/core` — pure detection logic.
|
|
14
|
-
*/
|
|
15
|
-
import type { GatewayRequest, GatewayResponse } from "./translate/types";
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Detection patterns — exported so tests can reference them
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
/** System prompt substrings that identify a compaction agent. */
|
|
22
|
-
export const COMPACTION_SYSTEM_PATTERNS = [
|
|
23
|
-
"anchored context summarization assistant",
|
|
24
|
-
] as const;
|
|
25
|
-
|
|
26
|
-
/** Last user message substrings that indicate a compaction request. */
|
|
27
|
-
export const COMPACTION_USER_PATTERNS = [
|
|
28
|
-
"anchored summary from the conversation history above",
|
|
29
|
-
"Update the anchored summary below",
|
|
30
|
-
"<previous-summary>",
|
|
31
|
-
] as const;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Template section headers found in the `<template>` block of a compaction
|
|
35
|
-
* request. A request matching ≥4 of these (with a `<template>` tag) is
|
|
36
|
-
* considered a compaction request.
|
|
37
|
-
*/
|
|
38
|
-
export const COMPACTION_TEMPLATE_SECTIONS = [
|
|
39
|
-
"## Goal",
|
|
40
|
-
"## Progress",
|
|
41
|
-
"## Key Decisions",
|
|
42
|
-
"## Next Steps",
|
|
43
|
-
"## Critical Context",
|
|
44
|
-
"## Relevant Files",
|
|
45
|
-
] as const;
|
|
46
|
-
|
|
47
|
-
/** Minimum number of template sections that must match (with `<template>` tag). */
|
|
48
|
-
const MIN_TEMPLATE_SECTION_MATCHES = 4;
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Helpers
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Extract the concatenated text content from the last user message.
|
|
56
|
-
* Returns an empty string if there are no user messages or no text blocks.
|
|
57
|
-
*/
|
|
58
|
-
function lastUserText(req: GatewayRequest): string {
|
|
59
|
-
for (let i = req.messages.length - 1; i >= 0; i--) {
|
|
60
|
-
const msg = req.messages[i];
|
|
61
|
-
if (msg.role === "user") {
|
|
62
|
-
return msg.content
|
|
63
|
-
.filter((b) => b.type === "text")
|
|
64
|
-
.map((b) => (b as { type: "text"; text: string }).text)
|
|
65
|
-
.join("\n");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Rough token estimate: ~4 characters per token. */
|
|
72
|
-
function estimateTokens(text: string): number {
|
|
73
|
-
return Math.ceil(text.length / 4);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// isCompactionRequest
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Returns `true` if the request looks like a compaction request.
|
|
82
|
-
*
|
|
83
|
-
* Checks in order:
|
|
84
|
-
* 1. System prompt contains any `COMPACTION_SYSTEM_PATTERNS` → true
|
|
85
|
-
* 2. Tools empty AND last user message contains any `COMPACTION_USER_PATTERNS` → true
|
|
86
|
-
* 3. Last user message has `<template>` tag AND ≥4 template sections → true
|
|
87
|
-
* 4. Otherwise → false
|
|
88
|
-
*/
|
|
89
|
-
export function isCompactionRequest(req: GatewayRequest): boolean {
|
|
90
|
-
// 1. System prompt check — strongest signal, sufficient alone
|
|
91
|
-
const systemLower = req.system.toLowerCase();
|
|
92
|
-
for (const pattern of COMPACTION_SYSTEM_PATTERNS) {
|
|
93
|
-
if (systemLower.includes(pattern.toLowerCase())) return true;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const userText = lastUserText(req);
|
|
97
|
-
|
|
98
|
-
// 2. No tools + user message contains compaction keywords
|
|
99
|
-
if (req.tools.length === 0 && userText) {
|
|
100
|
-
for (const pattern of COMPACTION_USER_PATTERNS) {
|
|
101
|
-
if (userText.includes(pattern)) return true;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 3. <template> tag + ≥4 section headers
|
|
106
|
-
if (userText.includes("<template>")) {
|
|
107
|
-
let matches = 0;
|
|
108
|
-
for (const section of COMPACTION_TEMPLATE_SECTIONS) {
|
|
109
|
-
if (userText.includes(section)) matches++;
|
|
110
|
-
}
|
|
111
|
-
if (matches >= MIN_TEMPLATE_SECTION_MATCHES) return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// extractPreviousSummary
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
/** Regex to extract content from `<previous-summary>` block (dotAll). */
|
|
122
|
-
const PREVIOUS_SUMMARY_RE =
|
|
123
|
-
/<previous-summary>\n(.*?)\n<\/previous-summary>/s;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Extract the content of a `<previous-summary>` block from the last user
|
|
127
|
-
* message, or `undefined` if no such block exists.
|
|
128
|
-
*/
|
|
129
|
-
export function extractPreviousSummary(
|
|
130
|
-
req: GatewayRequest,
|
|
131
|
-
): string | undefined {
|
|
132
|
-
const userText = lastUserText(req);
|
|
133
|
-
const match = PREVIOUS_SUMMARY_RE.exec(userText);
|
|
134
|
-
return match?.[1] ?? undefined;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// isTitleOrSummaryRequest
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
/** Max system prompt length for title/summary agents (chars). */
|
|
142
|
-
const TITLE_SUMMARY_MAX_SYSTEM_LENGTH = 500;
|
|
143
|
-
|
|
144
|
-
/** Max number of tools for a title/summary agent (0 or very few). */
|
|
145
|
-
const TITLE_SUMMARY_MAX_TOOLS = 2;
|
|
146
|
-
|
|
147
|
-
/** Max message count for a title/summary agent (system extracted, so 1–2). */
|
|
148
|
-
const TITLE_SUMMARY_MAX_MESSAGES = 2;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Detect non-conversation requests that should be forwarded without Lore
|
|
152
|
-
* pipeline processing (title generation, summary agents, etc.).
|
|
153
|
-
*
|
|
154
|
-
* These have:
|
|
155
|
-
* - Empty or very few tools (≤2)
|
|
156
|
-
* - Only 1–2 messages (system already extracted to `req.system`)
|
|
157
|
-
* - Short system prompt (< 500 chars)
|
|
158
|
-
* - NOT a compaction request (handled separately)
|
|
159
|
-
*/
|
|
160
|
-
export function isTitleOrSummaryRequest(req: GatewayRequest): boolean {
|
|
161
|
-
// Compaction requests are handled separately — don't classify as title/summary
|
|
162
|
-
if (isCompactionRequest(req)) return false;
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
req.tools.length <= TITLE_SUMMARY_MAX_TOOLS &&
|
|
166
|
-
req.messages.length <= TITLE_SUMMARY_MAX_MESSAGES &&
|
|
167
|
-
req.system.length < TITLE_SUMMARY_MAX_SYSTEM_LENGTH
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// buildCompactionResponse
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Build a `GatewayResponse` wrapping a compaction summary as if it were a
|
|
177
|
-
* normal assistant response. The gateway translates this back to the
|
|
178
|
-
* client's protocol (Anthropic/OpenAI) before sending.
|
|
179
|
-
*/
|
|
180
|
-
export function buildCompactionResponse(
|
|
181
|
-
_sessionID: string,
|
|
182
|
-
summary: string,
|
|
183
|
-
model: string,
|
|
184
|
-
): GatewayResponse {
|
|
185
|
-
return {
|
|
186
|
-
id: `msg_lore_compact_${crypto.randomUUID().slice(0, 8)}`,
|
|
187
|
-
model,
|
|
188
|
-
content: [{ type: "text", text: summary }],
|
|
189
|
-
stopReason: "end_turn",
|
|
190
|
-
usage: {
|
|
191
|
-
inputTokens: 0,
|
|
192
|
-
outputTokens: estimateTokens(summary),
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gateway configuration — loaded from environment variables with sensible
|
|
3
|
-
* defaults. No Zod, no file-based config, no @loreai/core dependency.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Config shape
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
export interface GatewayConfig {
|
|
11
|
-
/** Port to listen on. Default: 6969. Env: LORE_LISTEN_PORT */
|
|
12
|
-
port: number;
|
|
13
|
-
/** Host to bind to. Default: "127.0.0.1". Env: LORE_LISTEN_HOST */
|
|
14
|
-
host: string;
|
|
15
|
-
/** Upstream Anthropic API URL. Default: "https://api.anthropic.com". Env: LORE_UPSTREAM_ANTHROPIC */
|
|
16
|
-
upstreamAnthropic: string;
|
|
17
|
-
/** Upstream OpenAI API URL. Default: "https://api.openai.com". Env: LORE_UPSTREAM_OPENAI */
|
|
18
|
-
upstreamOpenAI: string;
|
|
19
|
-
/** Idle timeout in seconds before triggering background work. Default: 60 */
|
|
20
|
-
idleTimeoutSeconds: number;
|
|
21
|
-
/** Whether to log requests. Default: false. Env: LORE_DEBUG */
|
|
22
|
-
debug: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// loadConfig
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/** Load gateway configuration from environment variables with defaults. */
|
|
30
|
-
export function loadConfig(): GatewayConfig {
|
|
31
|
-
const env = process.env;
|
|
32
|
-
return {
|
|
33
|
-
port: parsePort(env.LORE_LISTEN_PORT, 6969),
|
|
34
|
-
host: env.LORE_LISTEN_HOST || "127.0.0.1",
|
|
35
|
-
upstreamAnthropic: trimTrailingSlash(
|
|
36
|
-
env.LORE_UPSTREAM_ANTHROPIC || "https://api.anthropic.com",
|
|
37
|
-
),
|
|
38
|
-
upstreamOpenAI: trimTrailingSlash(
|
|
39
|
-
env.LORE_UPSTREAM_OPENAI || "https://api.openai.com",
|
|
40
|
-
),
|
|
41
|
-
idleTimeoutSeconds: parsePositiveInt(env.LORE_IDLE_TIMEOUT, 60),
|
|
42
|
-
debug: isTruthy(env.LORE_DEBUG),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Upstream routing — model name → provider URL + protocol
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
export type UpstreamRoute = {
|
|
51
|
-
url: string;
|
|
52
|
-
protocol: "anthropic" | "openai";
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Model prefix → upstream provider routing table.
|
|
57
|
-
*
|
|
58
|
-
* Ordered from most-specific to most-general so that e.g. `claude-3-5-haiku`
|
|
59
|
-
* matches `claude-` before any catch-all. Unknown models fall back to the
|
|
60
|
-
* env-var-configured defaults.
|
|
61
|
-
*/
|
|
62
|
-
const UPSTREAM_ROUTES: Array<{ prefix: string; url: string; protocol: "anthropic" | "openai" }> = [
|
|
63
|
-
// Anthropic
|
|
64
|
-
{ prefix: "claude-", url: "https://api.anthropic.com", protocol: "anthropic" },
|
|
65
|
-
// Nvidia NIM
|
|
66
|
-
{ prefix: "nvidia/", url: "https://integrate.api.nvidia.com", protocol: "openai" },
|
|
67
|
-
{ prefix: "meta/", url: "https://integrate.api.nvidia.com", protocol: "openai" },
|
|
68
|
-
{ prefix: "mistralai/", url: "https://integrate.api.nvidia.com", protocol: "openai" },
|
|
69
|
-
{ prefix: "google/", url: "https://integrate.api.nvidia.com", protocol: "openai" },
|
|
70
|
-
{ prefix: "qwen/", url: "https://integrate.api.nvidia.com", protocol: "openai" },
|
|
71
|
-
{ prefix: "deepseek/", url: "https://integrate.api.nvidia.com", protocol: "openai" },
|
|
72
|
-
// OpenAI
|
|
73
|
-
{ prefix: "gpt-", url: "https://api.openai.com", protocol: "openai" },
|
|
74
|
-
{ prefix: "o1-", url: "https://api.openai.com", protocol: "openai" },
|
|
75
|
-
{ prefix: "o3-", url: "https://api.openai.com", protocol: "openai" },
|
|
76
|
-
{ prefix: "o4-", url: "https://api.openai.com", protocol: "openai" },
|
|
77
|
-
// xAI
|
|
78
|
-
{ prefix: "grok-", url: "https://api.x.ai", protocol: "openai" },
|
|
79
|
-
// Mistral (direct)
|
|
80
|
-
{ prefix: "mistral-", url: "https://api.mistral.ai", protocol: "openai" },
|
|
81
|
-
{ prefix: "codestral-", url: "https://api.mistral.ai", protocol: "openai" },
|
|
82
|
-
// Google (direct)
|
|
83
|
-
{ prefix: "gemini-", url: "https://generativelanguage.googleapis.com", protocol: "openai" },
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Resolve which upstream to use for a given model name.
|
|
88
|
-
*
|
|
89
|
-
* Returns the inferred route, or null if the model doesn't match any known
|
|
90
|
-
* prefix (caller should fall back to env-var-configured defaults).
|
|
91
|
-
*/
|
|
92
|
-
export function resolveUpstreamRoute(model: string): UpstreamRoute | null {
|
|
93
|
-
for (const route of UPSTREAM_ROUTES) {
|
|
94
|
-
if (model.startsWith(route.prefix)) {
|
|
95
|
-
return { url: route.url, protocol: route.protocol };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Project path inference
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Regex patterns to extract an absolute project path from a system prompt.
|
|
107
|
-
*
|
|
108
|
-
* Claude Code embeds absolute paths in several places:
|
|
109
|
-
* - CLAUDE.md content references (`/home/user/project/CLAUDE.md`)
|
|
110
|
-
* - Tool definitions mention cwd (`"cwd": "/home/user/project"`)
|
|
111
|
-
* - Working directory lines (`Working directory: /Users/…/project`)
|
|
112
|
-
*
|
|
113
|
-
* Each pattern captures the directory portion (no trailing filename when
|
|
114
|
-
* possible). Ordered from most-specific to most-general.
|
|
115
|
-
*/
|
|
116
|
-
const PROJECT_PATH_PATTERNS: RegExp[] = [
|
|
117
|
-
// "cwd": "/home/…/project" or "cwd":"/Users/…/project" (JSON-style)
|
|
118
|
-
/["']?cwd["']?\s*[:=]\s*["']?(\/(?:home|Users)\/[^\s"',}]+)/,
|
|
119
|
-
// Working directory: /home/user/project
|
|
120
|
-
/[Ww]orking\s+directory[:=]\s*(\/(?:home|Users)\/[^\s"',]+)/,
|
|
121
|
-
// CLAUDE.md / AGENTS.md / .lore.md file path → take the directory
|
|
122
|
-
/(\/(?:home|Users)\/[^\s"',]+)\/(?:CLAUDE|AGENTS|\.lore)\.md/,
|
|
123
|
-
// Generic absolute path starting with /home/ or /Users/ — first occurrence
|
|
124
|
-
// Captures until whitespace, quote, comma, or bracket.
|
|
125
|
-
/(\/(?:home|Users)\/[\w./-]+)/,
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Try to extract a project path from the system prompt content.
|
|
130
|
-
*
|
|
131
|
-
* Claude Code includes absolute paths in its system prompt (CLAUDE.md
|
|
132
|
-
* content, tool definitions, working directory references). Returns the
|
|
133
|
-
* extracted path or `null` if nothing looks like a project directory.
|
|
134
|
-
*/
|
|
135
|
-
export function inferProjectPath(systemPrompt: string): string | null {
|
|
136
|
-
for (const pattern of PROJECT_PATH_PATTERNS) {
|
|
137
|
-
const match = pattern.exec(systemPrompt);
|
|
138
|
-
if (match?.[1]) {
|
|
139
|
-
// Strip trailing slashes for consistency
|
|
140
|
-
return match[1].replace(/\/+$/, "") || null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// getProjectPath
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve the project path for a request. Checks in order:
|
|
152
|
-
* 1. `X-Lore-Project` header (explicit override)
|
|
153
|
-
* 2. `inferProjectPath(systemPrompt)` (zero-config extraction)
|
|
154
|
-
* 3. `process.cwd()` (last resort fallback)
|
|
155
|
-
*/
|
|
156
|
-
export function getProjectPath(
|
|
157
|
-
systemPrompt: string,
|
|
158
|
-
headers: Record<string, string>,
|
|
159
|
-
): string {
|
|
160
|
-
// 1. Explicit header override
|
|
161
|
-
const headerPath = headers["x-lore-project"];
|
|
162
|
-
if (headerPath) return headerPath;
|
|
163
|
-
|
|
164
|
-
// 2. Infer from system prompt content
|
|
165
|
-
const inferred = inferProjectPath(systemPrompt);
|
|
166
|
-
if (inferred) return inferred;
|
|
167
|
-
|
|
168
|
-
// 3. Fall back to gateway's own cwd
|
|
169
|
-
return process.cwd();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Helpers (not exported — internal only)
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
function parsePort(value: string | undefined, fallback: number): number {
|
|
177
|
-
if (!value) return fallback;
|
|
178
|
-
const n = Number.parseInt(value, 10);
|
|
179
|
-
if (Number.isNaN(n) || n < 0 || n > 65535) return fallback;
|
|
180
|
-
return n;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function parsePositiveInt(
|
|
184
|
-
value: string | undefined,
|
|
185
|
-
fallback: number,
|
|
186
|
-
): number {
|
|
187
|
-
if (!value) return fallback;
|
|
188
|
-
const n = Number.parseInt(value, 10);
|
|
189
|
-
if (Number.isNaN(n) || n <= 0) return fallback;
|
|
190
|
-
return n;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function isTruthy(value: string | undefined): boolean {
|
|
194
|
-
return value === "1" || value?.toLowerCase() === "true";
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function trimTrailingSlash(url: string): string {
|
|
198
|
-
return url.replace(/\/+$/, "");
|
|
199
|
-
}
|
package/src/idle.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Idle detection and background work scheduling for the Lore gateway.
|
|
3
|
-
*
|
|
4
|
-
* Since the gateway doesn't have host lifecycle hooks (like OpenCode's
|
|
5
|
-
* `session.idle` event), it uses a timer-based approach to detect when
|
|
6
|
-
* sessions go idle and trigger background work (distillation, curation,
|
|
7
|
-
* pruning, AGENTS.md export, etc.).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
temporal,
|
|
13
|
-
distillation,
|
|
14
|
-
curator,
|
|
15
|
-
ltm,
|
|
16
|
-
latReader,
|
|
17
|
-
log,
|
|
18
|
-
config as loreConfig,
|
|
19
|
-
getLastTurnAt,
|
|
20
|
-
exportToFile,
|
|
21
|
-
exportLoreFile,
|
|
22
|
-
} from "@loreai/core";
|
|
23
|
-
import type { LLMClient } from "@loreai/core";
|
|
24
|
-
import type { GatewayConfig } from "./config";
|
|
25
|
-
import type { SessionState } from "./translate/types";
|
|
26
|
-
import type { AuthCredential } from "./auth";
|
|
27
|
-
import { maybeValidateWorkerModel, getWorkerModel } from "./worker-model";
|
|
28
|
-
|
|
29
|
-
const POLL_INTERVAL_MS = 30_000;
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// startIdleScheduler
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Start a periodic timer that checks each active session for idle timeout.
|
|
37
|
-
*
|
|
38
|
-
* Every 30 seconds, walks the sessions map and fires `doIdleWork` for any
|
|
39
|
-
* session whose `lastRequestTime` is older than `config.idleTimeoutSeconds`.
|
|
40
|
-
* Tracks in-progress sessions to avoid double-triggering.
|
|
41
|
-
*
|
|
42
|
-
* @returns A cleanup function that clears the interval timer.
|
|
43
|
-
*/
|
|
44
|
-
export function startIdleScheduler(
|
|
45
|
-
config: GatewayConfig,
|
|
46
|
-
sessions: Map<string, SessionState>,
|
|
47
|
-
doIdleWork: (sessionID: string, state: SessionState) => Promise<void>,
|
|
48
|
-
): () => void {
|
|
49
|
-
const inProgress = new Set<string>();
|
|
50
|
-
|
|
51
|
-
const timer = setInterval(() => {
|
|
52
|
-
const now = Date.now();
|
|
53
|
-
const timeoutMs = config.idleTimeoutSeconds * 1000;
|
|
54
|
-
|
|
55
|
-
for (const [sessionID, state] of sessions) {
|
|
56
|
-
if (inProgress.has(sessionID)) continue;
|
|
57
|
-
if (now - state.lastRequestTime < timeoutMs) continue;
|
|
58
|
-
|
|
59
|
-
inProgress.add(sessionID);
|
|
60
|
-
doIdleWork(sessionID, state)
|
|
61
|
-
.catch((e) => log.error(`idle work failed for session ${sessionID}:`, e))
|
|
62
|
-
.finally(() => inProgress.delete(sessionID));
|
|
63
|
-
}
|
|
64
|
-
}, POLL_INTERVAL_MS);
|
|
65
|
-
|
|
66
|
-
return () => clearInterval(timer);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// touchSession
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Update a session's `lastRequestTime` to now. Called on every request.
|
|
75
|
-
*/
|
|
76
|
-
export function touchSession(
|
|
77
|
-
sessions: Map<string, SessionState>,
|
|
78
|
-
sessionID: string,
|
|
79
|
-
): void {
|
|
80
|
-
const state = sessions.get(sessionID);
|
|
81
|
-
if (state) {
|
|
82
|
-
state.lastRequestTime = Date.now();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// buildIdleWorkHandler
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Build the idle work handler that runs the same background tasks as
|
|
92
|
-
* OpenCode/Pi adapters on session idle:
|
|
93
|
-
*
|
|
94
|
-
* 1. Distillation (if enough undistilled messages)
|
|
95
|
-
* 2. Curation (if enough turns since last curation)
|
|
96
|
-
* 3. Consolidation (if entries exceed max)
|
|
97
|
-
* 4. Temporal pruning
|
|
98
|
-
* 5. AGENTS.md export
|
|
99
|
-
* 6. Dead reference cleanup
|
|
100
|
-
* 7. lat.md refresh
|
|
101
|
-
*
|
|
102
|
-
* Each step is independently try/catch'd — one failure won't block the rest.
|
|
103
|
-
*
|
|
104
|
-
* @param projectPath - Resolved project directory path
|
|
105
|
-
* @param llm - LLM client for worker calls (distillation, curation)
|
|
106
|
-
* @param upstreamUrl - Anthropic API base URL (for model discovery)
|
|
107
|
-
* @param getAuth - Callback to resolve auth credentials
|
|
108
|
-
* @param sessionModel - Model ID used for conversation (frontier model)
|
|
109
|
-
*/
|
|
110
|
-
export function buildIdleWorkHandler(
|
|
111
|
-
projectPath: string,
|
|
112
|
-
llm: LLMClient,
|
|
113
|
-
upstreamUrl: string,
|
|
114
|
-
getAuth: () => AuthCredential | null,
|
|
115
|
-
sessionModel: string,
|
|
116
|
-
): (sessionID: string, state: SessionState) => Promise<void> {
|
|
117
|
-
return async (sessionID: string, state: SessionState) => {
|
|
118
|
-
const cfg = loreConfig();
|
|
119
|
-
|
|
120
|
-
// 0. Worker model validation — discover cheaper models for background work.
|
|
121
|
-
// Runs before distillation/curation so the resolved model is up-to-date.
|
|
122
|
-
try {
|
|
123
|
-
const cred = getAuth();
|
|
124
|
-
if (cred) {
|
|
125
|
-
await maybeValidateWorkerModel(
|
|
126
|
-
sessionModel,
|
|
127
|
-
upstreamUrl,
|
|
128
|
-
cred,
|
|
129
|
-
llm,
|
|
130
|
-
projectPath,
|
|
131
|
-
sessionID,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
log.error("idle worker model validation error:", e);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const model = getWorkerModel();
|
|
139
|
-
|
|
140
|
-
// 1. Distillation — force-distill ALL pending messages on idle, even
|
|
141
|
-
// below minMessages. The cache is going cold; aggressive distillation
|
|
142
|
-
// now means a smaller context on the next turn via post-idle compact.
|
|
143
|
-
// Meta-distillation is always allowed on idle (cache is cold anyway).
|
|
144
|
-
try {
|
|
145
|
-
const pending = temporal.undistilledCount(projectPath, sessionID);
|
|
146
|
-
if (pending > 0) {
|
|
147
|
-
await distillation.run({ llm, projectPath, sessionID, model, force: true });
|
|
148
|
-
}
|
|
149
|
-
} catch (e) {
|
|
150
|
-
log.error("idle distillation error:", e);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 2. Curation
|
|
154
|
-
if (cfg.knowledge.enabled && cfg.curator.onIdle) {
|
|
155
|
-
try {
|
|
156
|
-
if (state.turnsSinceCuration >= cfg.curator.afterTurns) {
|
|
157
|
-
await curator.run({ llm, projectPath, sessionID, model });
|
|
158
|
-
state.turnsSinceCuration = 0;
|
|
159
|
-
}
|
|
160
|
-
} catch (e) {
|
|
161
|
-
log.error("idle curation error:", e);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 3. Consolidation — runs after curation so new entries are counted
|
|
166
|
-
if (cfg.knowledge.enabled) {
|
|
167
|
-
try {
|
|
168
|
-
const entries = ltm.forProject(projectPath, false);
|
|
169
|
-
if (entries.length > cfg.curator.maxEntries) {
|
|
170
|
-
log.info(
|
|
171
|
-
`entry count ${entries.length} exceeds maxEntries ${cfg.curator.maxEntries} — running consolidation`,
|
|
172
|
-
);
|
|
173
|
-
const { updated, deleted } = await curator.consolidate({
|
|
174
|
-
llm,
|
|
175
|
-
projectPath,
|
|
176
|
-
sessionID,
|
|
177
|
-
model,
|
|
178
|
-
});
|
|
179
|
-
if (updated > 0 || deleted > 0) {
|
|
180
|
-
log.info(`consolidation: ${updated} updated, ${deleted} deleted`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch (e) {
|
|
184
|
-
log.error("idle consolidation error:", e);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 4. Temporal pruning
|
|
189
|
-
try {
|
|
190
|
-
const { ttlDeleted, capDeleted } = temporal.prune({
|
|
191
|
-
projectPath,
|
|
192
|
-
retentionDays: cfg.pruning.retention,
|
|
193
|
-
maxStorageMB: cfg.pruning.maxStorage,
|
|
194
|
-
});
|
|
195
|
-
if (ttlDeleted > 0 || capDeleted > 0) {
|
|
196
|
-
log.info(
|
|
197
|
-
`pruned temporal messages: ${ttlDeleted} by TTL, ${capDeleted} by size cap`,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
} catch (e) {
|
|
201
|
-
log.error("idle pruning error:", e);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 5. Knowledge export (.lore.md + optional agents file pointer)
|
|
205
|
-
if (cfg.knowledge.enabled) {
|
|
206
|
-
try {
|
|
207
|
-
const entries = ltm.forProject(projectPath, false);
|
|
208
|
-
if (entries.length > 0) {
|
|
209
|
-
if (cfg.agentsFile.enabled) {
|
|
210
|
-
const filePath = join(projectPath, cfg.agentsFile.path);
|
|
211
|
-
exportToFile({ projectPath, filePath });
|
|
212
|
-
} else {
|
|
213
|
-
exportLoreFile(projectPath);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
} catch (e) {
|
|
217
|
-
log.error("idle knowledge export error:", e);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 6. Dead reference cleanup
|
|
222
|
-
if (cfg.knowledge.enabled) {
|
|
223
|
-
try {
|
|
224
|
-
const cleaned = ltm.cleanDeadRefs();
|
|
225
|
-
if (cleaned > 0) {
|
|
226
|
-
log.info(`cleaned ${cleaned} dead knowledge cross-references`);
|
|
227
|
-
}
|
|
228
|
-
} catch (e) {
|
|
229
|
-
log.error("idle dead-ref cleanup error:", e);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 7. lat.md refresh
|
|
234
|
-
try {
|
|
235
|
-
latReader.refresh(projectPath);
|
|
236
|
-
} catch (e) {
|
|
237
|
-
log.error("idle lat-reader refresh error:", e);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lore Gateway — package entry point.
|
|
3
|
-
*
|
|
4
|
-
* Library exports for programmatic use, plus `_cli()` for the CLI binary.
|
|
5
|
-
*
|
|
6
|
-
* Library usage:
|
|
7
|
-
* import { startServer, loadConfig } from "@loreai/gateway";
|
|
8
|
-
*
|
|
9
|
-
* CLI usage (via bin wrapper):
|
|
10
|
-
* lore start
|
|
11
|
-
* lore run claude
|
|
12
|
-
*/
|
|
13
|
-
import "../instrument";
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Library API
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
export { loadConfig } from "./config";
|
|
20
|
-
export type { GatewayConfig } from "./config";
|
|
21
|
-
export { startServer } from "./server";
|
|
22
|
-
export { handleRequest, resetPipelineState } from "./pipeline";
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// CLI entry — called by dist/bin.cjs or `bun run src/index.ts`
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
export { _cli } from "./cli/main";
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Direct execution — `bun run src/index.ts` still works as before
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
if (typeof Bun !== "undefined" && Bun.main === import.meta.path) {
|
|
35
|
-
// Direct execution (e.g. `bun run src/index.ts` from the OpenCode plugin)
|
|
36
|
-
// defaults to server-only mode (`start`), not `run` — there's no TTY and
|
|
37
|
-
// no reason to auto-detect agents when launched as an embedded server.
|
|
38
|
-
// esbuild CJS output drops import.meta to `{}` so the condition is
|
|
39
|
-
// always false in the npm bundle — the await is dead-code-eliminated.
|
|
40
|
-
import("./cli/start").then(({ commandStart }) => commandStart({}));
|
|
41
|
-
}
|