@chroxify/eunoe 1.2.2 → 1.3.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.
Files changed (64) hide show
  1. package/README.md +150 -6
  2. package/dist/core/index.js +2354 -0
  3. package/dist/types/context/content.d.ts +31 -0
  4. package/dist/types/context/fold/evidence.d.ts +20 -0
  5. package/dist/types/context/fold/index.d.ts +11 -0
  6. package/dist/types/context/fold/strategies.d.ts +20 -0
  7. package/dist/types/context/fold/turns.d.ts +16 -0
  8. package/dist/types/context/prompts.d.ts +24 -0
  9. package/dist/types/context/recall/index.d.ts +47 -0
  10. package/dist/types/context/recall/rerank.d.ts +3 -0
  11. package/dist/types/core/compaction.d.ts +32 -0
  12. package/dist/types/core/config.d.ts +34 -0
  13. package/dist/types/core/estimate.d.ts +9 -0
  14. package/dist/types/core/fingerprint.d.ts +10 -0
  15. package/dist/types/core/fold.d.ts +18 -0
  16. package/dist/types/core/forward.d.ts +24 -0
  17. package/dist/types/core/handoff.d.ts +10 -0
  18. package/dist/types/core/index.d.ts +14 -0
  19. package/dist/types/core/recall.d.ts +9 -0
  20. package/dist/types/core/rewrite.d.ts +63 -0
  21. package/dist/types/core/state.d.ts +25 -0
  22. package/dist/types/core/subagents.d.ts +10 -0
  23. package/dist/types/core/transcript.d.ts +9 -0
  24. package/dist/types/core/usage.d.ts +8 -0
  25. package/dist/types/core/window.d.ts +8 -0
  26. package/dist/types/transcript/format.d.ts +16 -0
  27. package/dist/types/transcript/parse.d.ts +45 -0
  28. package/package.json +15 -2
  29. package/src/cli/commands/mode.ts +2 -1
  30. package/src/cli/commands/status.ts +1 -1
  31. package/src/cli/proxy/stats.ts +1 -1
  32. package/src/cli/replay/index.ts +3 -3
  33. package/src/cli/sessions/index.ts +2 -2
  34. package/src/config/index.ts +4 -27
  35. package/src/context/content.ts +64 -1
  36. package/src/context/fold/strategies.ts +63 -10
  37. package/src/context/fold/turns.ts +34 -2
  38. package/src/context/prompts.ts +4 -3
  39. package/src/context/recall/index.ts +8 -4
  40. package/src/context/recall/rerank.ts +0 -8
  41. package/src/{proxy → core}/compaction.ts +85 -28
  42. package/src/core/config.ts +31 -0
  43. package/src/core/fold.ts +71 -0
  44. package/src/core/forward.ts +194 -0
  45. package/src/{proxy → core}/handoff.ts +23 -23
  46. package/src/core/index.ts +14 -0
  47. package/src/core/recall.ts +80 -0
  48. package/src/core/rewrite.ts +201 -0
  49. package/src/core/state.ts +50 -0
  50. package/src/core/subagents.ts +28 -0
  51. package/src/core/transcript.ts +44 -0
  52. package/src/{proxy → core}/usage.ts +43 -1
  53. package/src/core/window.ts +69 -0
  54. package/src/proxy/rewrite.ts +22 -139
  55. package/src/proxy/server.ts +21 -159
  56. package/src/proxy/sessions.ts +1 -1
  57. package/src/proxy/sources.ts +35 -0
  58. package/src/proxy/state.ts +2 -20
  59. package/src/proxy/window.ts +3 -33
  60. package/src/transcript/format.ts +1 -2
  61. package/src/transcript/subagents.ts +3 -3
  62. package/src/proxy/recall.ts +0 -58
  63. /package/src/{proxy → core}/estimate.ts +0 -0
  64. /package/src/{proxy → core}/fingerprint.ts +0 -0
@@ -0,0 +1,50 @@
1
+ import type { Message } from "../context/content"
2
+ import { isPrompt } from "../context/fold/turns"
3
+ import { messageFingerprint } from "./fingerprint"
4
+
5
+ export interface Handoff {
6
+ label: string
7
+ source: string
8
+ text: string
9
+ }
10
+
11
+ export interface ThreadState {
12
+ handoffs?: Record<string, Handoff | null>
13
+ cut?: string
14
+ keep?: number
15
+ cleared?: number
16
+ stuck?: number
17
+ host?: boolean
18
+ pins?: string[]
19
+ ratio?: number
20
+ limits?: Record<string, number>
21
+ recalls?: Record<string, string>
22
+ }
23
+
24
+ export interface ThreadStore {
25
+ read: (key: string) => ThreadState
26
+ save: (key: string, state: ThreadState) => void
27
+ }
28
+
29
+ export function memoryStore(initial: Record<string, ThreadState> = {}): ThreadStore {
30
+ const threads = new Map(Object.entries(initial).map(([key, state]) => [key, structuredClone(state)]))
31
+ return {
32
+ read: (key) => structuredClone(threads.get(key) ?? {}),
33
+ save: (key, state) => {
34
+ threads.set(key, structuredClone(state))
35
+ },
36
+ }
37
+ }
38
+
39
+ export function retireRecalls(state: ThreadState, folded: Message[][]) {
40
+ if (!state.recalls) return
41
+ for (const message of folded.flat()) if (isPrompt(message)) delete state.recalls[messageFingerprint(message)]
42
+ }
43
+
44
+ export function resetCut(state: ThreadState) {
45
+ delete state.cut
46
+ delete state.keep
47
+ delete state.cleared
48
+ delete state.stuck
49
+ delete state.host
50
+ }
@@ -0,0 +1,28 @@
1
+ import { blocks, type Message } from "../context/content"
2
+ import { splitTurns } from "../context/fold/turns"
3
+ import { type RecallChunk, recallChunks } from "../context/recall"
4
+
5
+ export interface Subagent {
6
+ description: string
7
+ messages: Message[]
8
+ toolUseId?: string
9
+ }
10
+
11
+ const closingTurn: Message = { role: "user", content: "" }
12
+
13
+ export function launchTurnOf(parentMessages: Message[], toolUseId: string | undefined): number {
14
+ if (!toolUseId) return 0
15
+ const index = splitTurns(parentMessages).turns.findIndex((turn) => turn.some((message) => message.role === "assistant" && blocks(message).some((b) => b.type === "tool_use" && b.id === toolUseId)))
16
+ return index + 1
17
+ }
18
+
19
+ const subagentLabel = (description: string) => `subagent "${description}"`
20
+
21
+ export function subagentRecall(messages: Message[], description: string, parentTurn: number): RecallChunk[] {
22
+ const from = subagentLabel(description)
23
+ return recallChunks([...messages, closingTurn], new Map()).map((chunk) => ({ ...chunk, from, turn: parentTurn }))
24
+ }
25
+
26
+ export function listSubagentRecall(parentMessages: Message[], subagents: Subagent[]): RecallChunk[] {
27
+ return subagents.flatMap((subagent) => subagentRecall(subagent.messages, subagent.description, launchTurnOf(parentMessages, subagent.toolUseId)))
28
+ }
@@ -0,0 +1,44 @@
1
+ import { blocks, type Message } from "../context/content"
2
+ import { isPrompt } from "../context/fold/turns"
3
+ import { searchGuide } from "../context/prompts"
4
+ import { renderIndex, renderMarkdown, renderSubagentMarkdown, renderSubagentXml, renderXml, type SubagentRecord, subagentFile, turnFile } from "../transcript/format"
5
+ import { parseTurns, type SessionEntry, type TranscriptFormat, type Turn } from "../transcript/parse"
6
+ import { launchTurnOf, type Subagent } from "./subagents"
7
+
8
+ export interface TranscriptOptions {
9
+ format?: TranscriptFormat
10
+ subagents?: Subagent[]
11
+ }
12
+
13
+ const isNote = (message: Message) => message.role === "user" && !isPrompt(message) && !blocks(message).some((b) => b.type === "tool_result")
14
+
15
+ const turnsOf = (messages: Message[], sidechain: boolean | "any" = false): Turn[] =>
16
+ parseTurns(
17
+ messages.map((message): SessionEntry => ({ type: message.role, isMeta: isNote(message), message })),
18
+ sidechain,
19
+ )
20
+
21
+ function subagentRecords(messages: Message[], turns: Turn[], subagents: Subagent[], format: TranscriptFormat): SubagentRecord[] {
22
+ const perTurn = new Map<number, number>()
23
+ return subagents.map((subagent) => {
24
+ const parentTurn = launchTurnOf(messages, subagent.toolUseId)
25
+ const k = (perTurn.get(parentTurn) ?? 0) + 1
26
+ perTurn.set(parentTurn, k)
27
+ const file = subagentFile(parentTurn, k, format)
28
+ const launcher = turns[parentTurn - 1]?.parts.find((part) => part.kind === "tool" && part.id === subagent.toolUseId)
29
+ if (launcher?.kind === "tool") launcher.record = file
30
+ return { run: { description: subagent.description }, turns: turnsOf(subagent.messages, "any"), parentTurn, file }
31
+ })
32
+ }
33
+
34
+ export function renderTranscriptFiles(messages: Message[], { format = "markdown", subagents = [] }: TranscriptOptions = {}): Record<string, string> {
35
+ const turns = turnsOf(messages)
36
+ const records = subagentRecords(messages, turns, subagents, format)
37
+ const files: Record<string, string> = {}
38
+ for (const turn of turns) files[turnFile(turn, format)] = format === "xml" ? renderXml(turn) : renderMarkdown(turn)
39
+ for (const record of records) files[record.file] = format === "xml" ? renderSubagentXml(record) : renderSubagentMarkdown(record)
40
+ files["index.md"] = renderIndex(turns, format, records)
41
+ return files
42
+ }
43
+
44
+ export const transcriptGuide = (dir: string, format: TranscriptFormat = "markdown") => searchGuide({ mode: format, dir, jsonl: "" })
@@ -1,4 +1,8 @@
1
- type Usage = Record<string, number>
1
+ import type { Body } from "../context/content"
2
+ import { sizeOf } from "./estimate"
3
+ import type { ThreadStore } from "./state"
4
+
5
+ export type Usage = Record<string, number>
2
6
 
3
7
  function usageIn(line: string): Usage | null {
4
8
  if (!line.startsWith("data:") || !line.includes("message_start")) return null
@@ -58,3 +62,41 @@ export function watchUsage(response: Response, onUsage: (usage: Usage) => void):
58
62
  export function inputTokens(usage: Usage) {
59
63
  return (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0)
60
64
  }
65
+
66
+ const OVERFLOW_MARGIN = 1.1
67
+
68
+ const charsOf = (sent: Body | number) => (typeof sent === "number" ? sent : sizeOf(sent))
69
+
70
+ function overflowOf(errorText: string): { tokens: number; maximum: number | null } | null {
71
+ const match = errorText.match(/prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i)
72
+ const tokens = Number(match?.[1])
73
+ const maximum = Number(match?.[2])
74
+ if (!Number.isFinite(tokens) || tokens <= 0) return null
75
+ return { tokens, maximum: Number.isFinite(maximum) && maximum > 0 ? maximum : null }
76
+ }
77
+
78
+ export function recordUsage(store: ThreadStore, threadKey: string, sent: Body | number, usage: Usage): boolean {
79
+ const input = inputTokens(usage)
80
+ const chars = charsOf(sent)
81
+ if (input <= 0 || chars <= 0) return false
82
+ const state = store.read(threadKey)
83
+ state.ratio = input / chars
84
+ store.save(threadKey, state)
85
+ return true
86
+ }
87
+
88
+ export const limitKey = (model: unknown) => String(model ?? "")
89
+
90
+ export function recordOverflow(store: ThreadStore, threadKey: string, sent: Body | number, errorText: string, model: unknown = typeof sent === "number" ? undefined : sent.model): number | null {
91
+ const overflow = overflowOf(errorText)
92
+ const chars = charsOf(sent)
93
+ if (!overflow || chars <= 0) return null
94
+ const state = store.read(threadKey)
95
+ state.ratio = Math.max(state.ratio ?? 0, overflow.tokens / chars) * OVERFLOW_MARGIN
96
+ if (overflow.maximum) {
97
+ const key = limitKey(model)
98
+ state.limits = { ...state.limits, [key]: Math.min(state.limits?.[key] ?? Number.POSITIVE_INFINITY, overflow.maximum) }
99
+ }
100
+ store.save(threadKey, state)
101
+ return overflow.tokens
102
+ }
@@ -0,0 +1,69 @@
1
+ export type WindowLookup = (model: unknown, requestHeaders: Headers, upstream?: string) => Promise<number | null>
2
+
3
+ export interface WindowLookupOptions {
4
+ known?: Map<string, number>
5
+ onLearn?: (windows: Map<string, number>) => void
6
+ timeoutMs?: number
7
+ retryAfterMs?: number
8
+ }
9
+
10
+ const LOOKUP_TIMEOUT_MS = 1_500
11
+
12
+ const RETRY_AFTER_MS = 10 * 60_000
13
+
14
+ const LONG_SUFFIX = /\[1m\]$/i
15
+
16
+ const SUFFIX = /\[[^\]]*\]$/
17
+
18
+ function lookupHeaders(requestHeaders: Headers): Headers {
19
+ const headers = new Headers()
20
+ for (const name of ["authorization", "x-api-key", "anthropic-version", "anthropic-beta"]) {
21
+ const value = requestHeaders.get(name)
22
+ if (value) headers.set(name, value)
23
+ }
24
+ if (!headers.has("anthropic-version")) headers.set("anthropic-version", "2023-06-01")
25
+ return headers
26
+ }
27
+
28
+ function familyWindow(model: string): number | null {
29
+ if (LONG_SUFFIX.test(model)) return 1_000_000
30
+ const id = model.replace(SUFFIX, "")
31
+ if (/^claude-(?:(?:opus|sonnet|haiku|fable)-5|opus-4-8)/.test(id)) return 1_000_000
32
+ if (id.startsWith("claude-")) return 200_000
33
+ return null
34
+ }
35
+
36
+ export function createWindowLookup({ known = new Map<string, number>(), onLearn, timeoutMs = LOOKUP_TIMEOUT_MS, retryAfterMs = RETRY_AFTER_MS }: WindowLookupOptions = {}): WindowLookup {
37
+ const lookups = new Map<string, Promise<number | null>>()
38
+ const failedAt = new Map<string, number>()
39
+ const lookup = async (id: string, requestHeaders: Headers, upstream: string): Promise<number | null> => {
40
+ const response = await fetch(`${upstream}/v1/models/${encodeURIComponent(id)}`, { headers: lookupHeaders(requestHeaders), signal: AbortSignal.timeout(timeoutMs) })
41
+ if (!response.ok) return null
42
+ const info = (await response.json()) as { max_input_tokens?: number }
43
+ if (typeof info.max_input_tokens !== "number" || info.max_input_tokens <= 0) return null
44
+ known.set(id, info.max_input_tokens)
45
+ onLearn?.(known)
46
+ return info.max_input_tokens
47
+ }
48
+ return (model, requestHeaders, upstream = "https://api.anthropic.com") => {
49
+ const given = String(model ?? "")
50
+ const id = given.replace(SUFFIX, "")
51
+ if (!id) return Promise.resolve(null)
52
+ const window = known.get(id)
53
+ if (window) return Promise.resolve(window)
54
+ const fallback = familyWindow(given)
55
+ const failed = failedAt.get(id)
56
+ if (failed !== undefined && Date.now() - failed < retryAfterMs) return Promise.resolve(fallback)
57
+ const pending =
58
+ lookups.get(id) ??
59
+ lookup(id, requestHeaders, upstream)
60
+ .catch(() => null)
61
+ .then((found) => {
62
+ if (found === null) failedAt.set(id, Date.now())
63
+ return found
64
+ })
65
+ .finally(() => lookups.delete(id))
66
+ lookups.set(id, pending)
67
+ return pending.then((found) => found ?? fallback)
68
+ }
69
+ }
@@ -1,20 +1,15 @@
1
+ import { existsSync, readFileSync } from "node:fs"
1
2
  import type { Config } from "../config"
2
- import type { Body, Message } from "../context/content"
3
- import { appendSystem, tailMessages, trimMessages } from "../context/fold/strategies"
4
- import { splitTurns, turnFingerprint } from "../context/fold/turns"
5
- import { SYSTEM_PROMPT, searchGuide, searchOnlyInstructions, trimInstructions } from "../context/prompts"
6
- import { identityReranker, type Reranker, withRecall } from "../context/recall"
7
- import { jevReranker, rerankKey } from "../context/recall/rerank"
3
+ import { paths } from "../config/paths"
4
+ import type { Reranker } from "../context/recall"
5
+ import { jevReranker } from "../context/recall/rerank"
6
+ import { type RewriteInput as CoreInput, prefetchRecall as prefetchCore, type Rewrite, rewrite as rewriteCore, rewriteAsync as rewriteCoreAsync } from "../core/rewrite"
8
7
  import type { TranscriptRef } from "../transcript/search"
9
- import { compact } from "./compaction"
10
- import { DEFAULT_RATIO, estimator, sizeOf, targetSize } from "./estimate"
11
- import { messageFingerprint } from "./fingerprint"
12
- import { activeHandoffs, attachHandoffs, withHandoffs } from "./handoff"
13
- import { activeRecalls, applyRecall, prefetchFor, wantsRecall } from "./recall"
14
- import { readState, saveState } from "./state"
8
+ import { sessionSources } from "./sources"
9
+ import { fileStore } from "./state"
15
10
 
16
11
  export interface RewriteInput {
17
- body: Body
12
+ body: CoreInput["body"]
18
13
  sessionId: string | null
19
14
  config: Config
20
15
  transcript: TranscriptRef | null
@@ -23,146 +18,34 @@ export interface RewriteInput {
23
18
  omittedBefore?: number
24
19
  }
25
20
 
26
- export interface Rewrite {
27
- body: Body
28
- threadKey: string | null
29
- note: Record<string, unknown>
21
+ export function rerankKey(): string | null {
22
+ const env = process.env.TYPESAFE_API_KEY?.trim()
23
+ if (env) return env
24
+ return existsSync(paths.typesafeKey) ? readFileSync(paths.typesafeKey, "utf8").trim() : null
30
25
  }
31
26
 
32
- interface Prefetched {
33
- reranked: boolean
34
- rerankError: string | null
35
- recalled: number
36
- }
37
-
38
- const pass = (body: Body): Rewrite => ({ body, threadKey: null, note: { action: "pass" } })
39
-
40
- const isAgentThread = (body: Body) => Array.isArray(body.tools) && body.tools.length > 0
41
-
42
- const withSystemPrompt = (system: Body["system"], extra?: string | null) => appendSystem(extra ? appendSystem(system, extra) : system, SYSTEM_PROMPT)
43
-
44
- const ratioOf = (threadKey: string) => readState(threadKey).ratio ?? DEFAULT_RATIO
45
-
46
27
  function rerankModel(config: Config): string | null {
47
28
  const model = config.eval?.rerank?.model ?? config.rerank
48
29
  return model && model !== "off" ? model : null
49
30
  }
50
31
 
51
- function evalStrategy(input: RewriteInput, turns: Message[][], threadKey: string, guide: string | null): Rewrite | null {
52
- const { body, config } = input
53
- const strategy = config.eval?.strategy
54
- if (strategy !== "guide" && strategy !== "trim" && strategy !== "tail") return null
55
- if (!isAgentThread(body)) return pass(body)
56
- const counts = { turns: turns.length, messagesIn: body.messages.length }
57
- if (strategy === "guide") {
58
- return { body: { ...body, system: withSystemPrompt(body.system, guide ? searchOnlyInstructions(guide) : null) }, threadKey, note: { action: "guide", ...counts } }
59
- }
60
- if (strategy === "trim") {
61
- const messages = trimMessages(body.messages, { evidence: config.eval?.evidence ?? true })
62
- return { body: { ...body, system: withSystemPrompt(body.system, trimInstructions(guide)), messages }, threadKey, note: { action: "trim", ...counts, messagesOut: messages.length } }
63
- }
64
- const based = { ...body, system: withSystemPrompt(body.system) }
65
- const size = estimator(based, ratioOf(threadKey))
66
- let from = 1
67
- if (input.window) {
68
- const target = targetSize(size, input.window)
69
- from = turns.length - 1
70
- while (from > 1 && size.of(tailMessages(body.messages, from - 1, guide)) <= target) from -= 1
71
- }
72
- const messages = tailMessages(body.messages, from, guide)
73
- return { body: { ...based, messages }, threadKey, note: { action: "tail", keptTurns: turns.length - from, ...counts, messagesOut: messages.length, estimate: size.of(messages) } }
74
- }
75
-
76
- function strategize(input: RewriteInput): Rewrite {
77
- const { body, sessionId, config, transcript } = input
78
- if (config.mode === "off" || !sessionId || !Array.isArray(body.messages) || body.messages.length === 0) return pass(body)
79
- const { turns } = splitTurns(body.messages)
80
- if (turns.length === 0) return pass(body)
81
- const threadKey = `${sessionId}-${turnFingerprint(turns[0])}`
82
- const guide = transcript ? searchGuide(transcript) : null
83
- const state = readState(threadKey)
84
- if (attachHandoffs(body.messages, config, sessionId, state)) saveState(threadKey, state)
85
-
86
- const forced = evalStrategy(input, turns, threadKey, guide)
87
- if (forced) return forced
88
-
89
- const based = isAgentThread(body) ? { ...body, system: withSystemPrompt(body.system) } : body
90
- if (body.messages.length < 2) return { body: based, threadKey, note: { action: "full", turns: turns.length } }
91
- const handoffs = activeHandoffs(state.handoffs)
92
- const recalls = wantsRecall(input) ? activeRecalls(state.recalls) : {}
93
- const result = compact({
94
- body: based,
95
- config,
96
- state,
97
- window: input.window,
98
- guide,
99
- transcriptDir: transcript && transcript.mode !== "jsonl" ? transcript.dir : null,
100
- mayCompact: input.mayCompact,
101
- omittedBefore: input.omittedBefore,
102
- decorate: (messages) => withRecall(withHandoffs(messages, handoffs), messageFingerprint, recalls),
103
- })
104
- if (result.stateChanged) saveState(threadKey, state)
105
- return { body: result.messages === body.messages ? based : { ...based, messages: result.messages }, threadKey, note: result.note }
106
- }
107
-
108
- function withAttachedHandoffs(result: Rewrite): Rewrite {
109
- if (!result.threadKey) return result
110
- const handoffs = activeHandoffs(readState(result.threadKey).handoffs)
111
- const messages = withHandoffs(result.body.messages, handoffs)
112
- if (messages === result.body.messages) return result
113
- const attached = Object.values(handoffs)
114
- const note = { ...result.note, handoff: attached.reduce((sum, h) => sum + h.text.length, 0), handoffFrom: attached.map((h) => h.label).join(", ") }
115
- return { ...result, body: { ...result.body, messages }, note }
116
- }
117
-
118
- function once(input: RewriteInput): Rewrite {
119
- const result = withAttachedHandoffs(strategize(input))
120
- return wantsRecall(input) ? applyRecall(input, result) : result
121
- }
122
-
123
- function sentEstimate(result: Rewrite): number {
124
- return Math.round(sizeOf(result.body) * (result.threadKey ? ratioOf(result.threadKey) : DEFAULT_RATIO))
125
- }
126
-
127
- function overLimit(input: RewriteInput, result: Rewrite): boolean {
128
- if (!input.window || !result.threadKey || input.mayCompact === false || result.note.action === "pass") return false
129
- return sentEstimate(result) > input.config.compactAt * input.window
130
- }
131
-
132
- function carried(from: Rewrite, to: Rewrite): Rewrite {
133
- const actionsOf = ({ note }: Rewrite) => (note.actions as string[] | undefined) ?? []
134
- const actions = [...actionsOf(from), ...actionsOf(to)]
135
- if (!from.note.compacted && actions.length === 0) return to
136
- return { ...to, note: { ...to.note, compacted: Boolean(to.note.compacted || from.note.compacted), actions } }
137
- }
32
+ const toCore = ({ sessionId, transcript, ...input }: RewriteInput): CoreInput => ({ ...input, threadId: sessionId, store: fileStore, sources: sessionSources(transcript, input.config, sessionId) })
138
33
 
139
- export function rewrite(input: RewriteInput, before?: Rewrite): Rewrite {
140
- const result = before ? carried(before, once(input)) : once(input)
141
- if (!overLimit(input, result)) return result
142
- const recut = carried(result, once(input))
143
- if (!overLimit(input, recut)) return { ...recut, note: { ...recut.note, fit: "recut" } }
144
- const bare = carried(recut, once({ ...input, config: { ...input.config, eval: { ...input.config.eval, recall: false } } }))
145
- return { ...bare, note: { ...bare.note, fit: overLimit(input, bare) ? "over" : "bare", fitEstimate: sentEstimate(bare) } }
146
- }
34
+ export const rewrite = (input: RewriteInput): Rewrite => rewriteCore(toCore(input))
147
35
 
148
- export async function prefetchRecall(input: RewriteInput, rerank: Reranker): Promise<Prefetched | null> {
149
- if (!wantsRecall(input)) return null
150
- const recalled = await prefetchFor(input, strategize(input), rerank)
151
- return recalled === null ? null : { reranked: rerank !== identityReranker, rerankError: null, recalled }
152
- }
36
+ export const prefetchRecall = (input: RewriteInput, rerank: Reranker) => prefetchCore(toCore(input), rerank)
153
37
 
154
- export async function rewriteAsync(input: RewriteInput): Promise<Rewrite> {
155
- const model = wantsRecall(input) ? rerankModel(input.config) : null
38
+ export async function rewriteAsync(input: RewriteInput): Promise<Rewrite & { note: { reranked?: string; rerankError?: string } }> {
39
+ const core = toCore(input)
40
+ const model = core.config.mode !== "off" && (core.config.eval?.recall ?? true) ? rerankModel(input.config) : null
156
41
  const key = model ? rerankKey() : null
157
- if (!model || !key) return rewrite(input)
42
+ if (!model || !key) return rewriteCore(core)
158
43
  const failure = { reason: null as string | null }
159
44
  const reranker = jevReranker(model, key, input.config.eval?.rerank?.endpoint, (reason) => {
160
45
  failure.reason = reason
161
46
  })
162
- const first = strategize(input)
163
- const recalled = await prefetchFor(input, first, reranker)
164
- const final = rewrite(input, first)
165
- if (recalled === null) return final
47
+ const result = await rewriteCoreAsync(core, reranker)
48
+ if (result.note.recalled === undefined) return result
166
49
  const outcome = failure.reason ? { rerankError: `${model}: ${failure.reason}` } : { reranked: model }
167
- return { ...final, note: { ...final.note, recalled, ...outcome } }
50
+ return { ...result, note: { ...result.note, ...outcome } }
168
51
  }
@@ -1,17 +1,16 @@
1
1
  import path from "node:path"
2
2
  import { type Config, claudeConfigDirs, readConfig, settingsFor } from "../config"
3
- import { type Body, wellFormed } from "../context/content"
3
+ import type { Body } from "../context/content"
4
+ import { contextFingerprint } from "../core/fingerprint"
5
+ import { claudeCodeSessionId, createForwarder, type Prepared } from "../core/forward"
4
6
  import { findSessionFile } from "../transcript/locate"
5
7
  import { prepareTranscript } from "../transcript/search"
6
8
  import { VERSION } from "../version"
7
9
  import { applyCanonical } from "./canonical"
8
- import { sizeOf } from "./estimate"
9
- import { type ContextFingerprint, contextFingerprint } from "./fingerprint"
10
10
  import { logRequest } from "./log"
11
11
  import { rewriteAsync } from "./rewrite"
12
12
  import { recordSession } from "./sessions"
13
- import { readState, saveState } from "./state"
14
- import { inputTokens, watchUsage } from "./usage"
13
+ import { fileStore } from "./state"
15
14
  import { resolveWindow } from "./window"
16
15
 
17
16
  export interface Health {
@@ -22,48 +21,14 @@ export interface Health {
22
21
  startedAt?: number
23
22
  }
24
23
 
25
- interface Outgoing {
26
- note: Record<string, unknown>
27
- threadKey: string | null
28
- bodyText: string | undefined
29
- sentSize: number
30
- givenSize: number
31
- fixedSize: number
32
- sent: (ContextFingerprint & { session: string | null }) | null
33
- }
34
-
35
24
  export const HEALTH_PATH = "/_eunoe/health"
36
25
 
37
26
  const startedAt = Date.now()
38
27
 
39
28
  const trustedHeader = (request: Request, config: Config, name: string) => (config.eval?.trustHeaders === true ? request.headers.get(`x-eunoe-${name}`) : null)
40
29
 
41
- const clipped = (error: string) => error.slice(0, 500)
42
-
43
- function sessionIdOf(headers: Headers, body: Body): string | null {
44
- const header = headers.get("x-claude-code-session-id")
45
- if (header) return header
46
- try {
47
- return JSON.parse(body.metadata?.user_id ?? "{}").session_id ?? null
48
- } catch {
49
- return null
50
- }
51
- }
52
-
53
- function forwardableHeaders(headers: Headers) {
54
- const out = new Headers(headers)
55
- out.delete("content-encoding")
56
- out.delete("content-length")
57
- return out
58
- }
59
-
60
- function overflowTokens(errorText: string): number | null {
61
- const tokens = Number(errorText.match(/prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i)?.[1])
62
- return Number.isFinite(tokens) && tokens > 0 ? tokens : null
63
- }
64
-
65
- async function prepare(request: Request, body: Body, base: Config, isCountTokens: boolean) {
66
- const sessionId = sessionIdOf(request.headers, body)
30
+ async function prepareSession(request: Request, body: Body, base: Config, isCountTokens: boolean) {
31
+ const sessionId = claudeCodeSessionId(request.headers, body)
67
32
  const config = settingsFor(base, sessionId)
68
33
  const trusted = config.eval?.trustHeaders === true
69
34
  const transcriptOverride = trustedHeader(request, config, "transcript")
@@ -87,132 +52,29 @@ async function prepare(request: Request, body: Body, base: Config, isCountTokens
87
52
  })
88
53
  }
89
54
 
90
- function saveRatio(threadKey: string, ratio: (previous: number | undefined) => number) {
91
- const state = readState(threadKey)
92
- state.ratio = ratio(state.ratio)
93
- saveState(threadKey, state)
55
+ async function prepare(request: Request, given: Body, config: Config, mayCompact: boolean): Promise<Prepared> {
56
+ const canonicalFile = trustedHeader(request, config, "canonical")
57
+ const canonical = canonicalFile ? applyCanonical(given, canonicalFile) : null
58
+ const result = await prepareSession(request, canonical?.body ?? given, config, !mayCompact)
59
+ const context = config.eval ? { context: { session: claudeCodeSessionId(request.headers, given), ...contextFingerprint(result.body), given: contextFingerprint(given) } } : {}
60
+ return { ...result, note: { ...result.note, ...(canonical && { canonical: canonical.status }), ...context } }
94
61
  }
95
62
 
96
63
  interface Timing {
97
64
  countTokensTimeoutMs: number
98
65
  }
99
66
 
100
- const COUNT_TOKENS_ATTEMPTS = 3
101
-
102
- export async function handle(request: Request, timing: Timing = { countTokensTimeoutMs: 10_000 }): Promise<Response> {
103
- const began = Date.now()
104
- try {
105
- return await route(request, began, timing)
106
- } catch (error) {
107
- logRequest({ path: new URL(request.url).pathname, status: 502, action: "upstream-unreachable", error: clipped(String(error)), ms: Date.now() - began })
108
- return Response.json({ type: "error", error: { type: "api_error", message: `eunoe could not reach the API: ${String(error)}` } }, { status: 502 })
109
- }
110
- }
111
-
112
- async function route(request: Request, began: number, timing: Timing): Promise<Response> {
113
- let stalled = 0
114
- let prep = 0
115
- let upload = 0
116
- const log = (record: Record<string, unknown>) => logRequest({ ...record, ms: Date.now() - began, prep, upload, ...(stalled > 0 && { stalled }) })
67
+ export function handle(request: Request, timing: Timing = { countTokensTimeoutMs: 10_000 }): Promise<Response> {
117
68
  const config = readConfig()
118
- const url = new URL(request.url)
119
- if (url.pathname === HEALTH_PATH) return Response.json({ ok: true, version: VERSION, mode: config.mode, pid: process.pid, startedAt } satisfies Health)
120
- const headers = new Headers(request.headers)
121
- for (const name of ["host", "content-length", "accept-encoding"]) headers.delete(name)
122
-
123
- let givenText: string | undefined
124
- if (request.method !== "GET" && request.method !== "HEAD") givenText = await request.text()
125
- upload = Date.now() - began
126
- const isMessages = request.method === "POST" && url.pathname.startsWith("/v1/messages")
127
- const isCountTokens = url.pathname.includes("count_tokens")
128
-
129
- const build = async (): Promise<Outgoing> => {
130
- const passthrough: Outgoing = { note: { action: "pass" }, threadKey: null, bodyText: givenText, sentSize: 0, givenSize: 0, fixedSize: 0, sent: null }
131
- if (!givenText || !isMessages) return passthrough
132
- try {
133
- const given = JSON.parse(givenText) as Body
134
- const canonicalFile = trustedHeader(request, config, "canonical")
135
- const canonical = canonicalFile ? applyCanonical(given, canonicalFile) : null
136
- const result = await prepare(request, canonical?.body ?? given, config, isCountTokens)
137
- const sentSize = sizeOf(result.body)
138
- return {
139
- note: canonical ? { ...result.note, canonical: canonical.status } : result.note,
140
- threadKey: result.threadKey,
141
- bodyText: result.body !== given ? JSON.stringify(result.body, wellFormed) : givenText,
142
- sentSize,
143
- givenSize: result.body === given ? sentSize : sizeOf(given),
144
- fixedSize: sizeOf({ ...result.body, messages: [] }),
145
- sent: config.eval ? { session: sessionIdOf(request.headers, given), ...contextFingerprint(result.body), given: contextFingerprint(given) } : null,
146
- }
147
- } catch (error) {
148
- return { ...passthrough, note: { action: "error-passthrough", error: String(error) } }
149
- }
150
- }
151
- const target = config.upstream + url.pathname + url.search
152
- const send = async (body: string | undefined): Promise<Response> => {
153
- if (!isCountTokens) return fetch(target, { method: request.method, headers, body })
154
- for (;;) {
155
- try {
156
- return await fetch(target, { method: request.method, headers, body, signal: AbortSignal.timeout(timing.countTokensTimeoutMs) })
157
- } catch (error) {
158
- const timedOut = error instanceof Error && error.name === "TimeoutError"
159
- if (!timedOut || stalled + 1 >= COUNT_TOKENS_ATTEMPTS) throw error
160
- stalled += 1
161
- }
162
- }
163
- }
164
-
165
- let out = await build()
166
- prep = Date.now() - began - upload
167
- let upstream = await send(out.bodyText)
168
- const errorResponse = (text: string) => new Response(text, { status: upstream.status, statusText: upstream.statusText, headers: forwardableHeaders(upstream.headers) })
169
-
170
- if (upstream.status >= 400 && out.note.action !== "pass") {
171
- const errorText = await upstream.text()
172
- const overflow = overflowTokens(errorText)
173
- const logged = { path: url.pathname, status: upstream.status, thread: out.threadKey, ...out.note }
174
- if (!overflow || !out.threadKey || out.sentSize === 0 || isCountTokens) {
175
- log({ ...logged, error: clipped(errorText) })
176
- return errorResponse(errorText)
177
- }
178
- const sentSize = out.sentSize
179
- saveRatio(out.threadKey, (previous) => Math.max(previous ?? 0, overflow / sentSize) * 1.1)
180
- log({ ...logged, overflow, error: clipped(errorText) })
181
- out = await build()
182
- out = { ...out, note: { ...out.note, healed: overflow } }
183
- upstream = await send(out.bodyText)
184
- if (upstream.status >= 400) {
185
- const retryError = await upstream.text()
186
- log({ path: url.pathname, status: upstream.status, thread: out.threadKey, ...out.note, error: clipped(retryError) })
187
- return errorResponse(retryError)
188
- }
189
- }
190
-
191
- const { note, threadKey, sentSize, givenSize, fixedSize, sent } = out
192
- const response = new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: forwardableHeaders(upstream.headers) })
193
- if (note.action === "pass" || !isMessages || isCountTokens) {
194
- if (note.action !== "pass" || sent) log({ path: url.pathname, status: upstream.status, ...note, ...(sent && { context: sent }) })
195
- return response
196
- }
197
-
198
- return watchUsage(response, (usage) => {
199
- const input = inputTokens(usage)
200
- log({
201
- path: url.pathname,
202
- status: upstream.status,
203
- thread: threadKey,
204
- ...note,
205
- input,
206
- cacheRead: usage.cache_read_input_tokens ?? 0,
207
- cacheWrite: usage.cache_creation_input_tokens ?? 0,
208
- uncached: usage.input_tokens ?? 0,
209
- charsIn: givenSize,
210
- charsOut: sentSize,
211
- charsFixed: fixedSize,
212
- ...(sent && { context: sent }),
213
- })
214
- if (threadKey && input > 0 && sentSize > 0) saveRatio(threadKey, () => input / sentSize)
69
+ if (new URL(request.url).pathname === HEALTH_PATH) return Promise.resolve(Response.json({ ok: true, version: VERSION, mode: config.mode, pid: process.pid, startedAt } satisfies Health))
70
+ const forward = createForwarder({
71
+ store: fileStore,
72
+ upstream: config.upstream,
73
+ prepare: (incoming, body, mayCompact) => prepare(incoming, body, config, mayCompact),
74
+ record: logRequest,
75
+ countTokensTimeoutMs: timing.countTokensTimeoutMs,
215
76
  })
77
+ return forward(request)
216
78
  }
217
79
 
218
80
  export function serve(port: number) {
@@ -1,6 +1,6 @@
1
1
  import { closeSync, mkdirSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs"
2
- import type { Mode } from "../config"
3
2
  import { EUNOE_DIR, paths } from "../config/paths"
3
+ import type { Mode } from "../core/config"
4
4
 
5
5
  interface SeenSession {
6
6
  lastSeen: string