@chroxify/eunoe 1.2.2 → 1.3.0

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 (63) hide show
  1. package/README.md +139 -2
  2. package/dist/core/index.js +2235 -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 +14 -0
  8. package/dist/types/context/prompts.d.ts +24 -0
  9. package/dist/types/context/recall/index.d.ts +44 -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 +33 -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 +8 -0
  20. package/dist/types/core/rewrite.d.ts +63 -0
  21. package/dist/types/core/state.d.ts +22 -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 +7 -0
  25. package/dist/types/core/window.d.ts +5 -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 +55 -10
  37. package/src/context/fold/turns.ts +22 -2
  38. package/src/context/prompts.ts +4 -3
  39. package/src/context/recall/index.ts +2 -3
  40. package/src/context/recall/rerank.ts +0 -8
  41. package/src/{proxy → core}/compaction.ts +56 -24
  42. package/src/core/config.ts +31 -0
  43. package/src/core/fold.ts +69 -0
  44. package/src/core/forward.ts +192 -0
  45. package/src/{proxy → core}/handoff.ts +23 -23
  46. package/src/core/index.ts +14 -0
  47. package/src/{proxy → core}/recall.ts +18 -20
  48. package/src/core/rewrite.ts +189 -0
  49. package/src/core/state.ts +40 -0
  50. package/src/core/subagents.ts +28 -0
  51. package/src/core/transcript.ts +44 -0
  52. package/src/{proxy → core}/usage.ts +34 -1
  53. package/src/core/window.ts +39 -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 → core}/estimate.ts +0 -0
  63. /package/src/{proxy → core}/fingerprint.ts +0 -0
@@ -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,32 @@ 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 overflowTokens(errorText: string): number | null {
71
+ const tokens = Number(errorText.match(/prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)/i)?.[1])
72
+ return Number.isFinite(tokens) && tokens > 0 ? tokens : null
73
+ }
74
+
75
+ export function recordUsage(store: ThreadStore, threadKey: string, sent: Body | number, usage: Usage): boolean {
76
+ const input = inputTokens(usage)
77
+ const chars = charsOf(sent)
78
+ if (input <= 0 || chars <= 0) return false
79
+ const state = store.read(threadKey)
80
+ state.ratio = input / chars
81
+ store.save(threadKey, state)
82
+ return true
83
+ }
84
+
85
+ export function recordOverflow(store: ThreadStore, threadKey: string, sent: Body | number, errorText: string): number | null {
86
+ const tokens = overflowTokens(errorText)
87
+ const chars = charsOf(sent)
88
+ if (!tokens || chars <= 0) return null
89
+ const state = store.read(threadKey)
90
+ state.ratio = Math.max(state.ratio ?? 0, tokens / chars) * OVERFLOW_MARGIN
91
+ store.save(threadKey, state)
92
+ return tokens
93
+ }
@@ -0,0 +1,39 @@
1
+ export type WindowLookup = (model: unknown, requestHeaders: Headers, upstream?: string) => Promise<number | null>
2
+
3
+ const LOOKUP_TIMEOUT_MS = 5_000
4
+
5
+ function lookupHeaders(requestHeaders: Headers): Headers {
6
+ const headers = new Headers()
7
+ for (const name of ["authorization", "x-api-key", "anthropic-version", "anthropic-beta"]) {
8
+ const value = requestHeaders.get(name)
9
+ if (value) headers.set(name, value)
10
+ }
11
+ if (!headers.has("anthropic-version")) headers.set("anthropic-version", "2023-06-01")
12
+ return headers
13
+ }
14
+
15
+ export function createWindowLookup({ known = new Map<string, number>(), onLearn }: { known?: Map<string, number>; onLearn?: (windows: Map<string, number>) => void } = {}): WindowLookup {
16
+ const lookups = new Map<string, Promise<number | null>>()
17
+ const lookup = async (id: string, requestHeaders: Headers, upstream: string): Promise<number | null> => {
18
+ const response = await fetch(`${upstream}/v1/models/${encodeURIComponent(id)}`, { headers: lookupHeaders(requestHeaders), signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) })
19
+ if (!response.ok) return null
20
+ const info = (await response.json()) as { max_input_tokens?: number }
21
+ if (typeof info.max_input_tokens !== "number" || info.max_input_tokens <= 0) return null
22
+ known.set(id, info.max_input_tokens)
23
+ onLearn?.(known)
24
+ return info.max_input_tokens
25
+ }
26
+ return (model, requestHeaders, upstream = "https://api.anthropic.com") => {
27
+ const id = String(model ?? "").replace(/\[[^\]]*\]$/, "")
28
+ if (!id) return Promise.resolve(null)
29
+ const window = known.get(id)
30
+ if (window) return Promise.resolve(window)
31
+ const inFlight = lookups.get(id)
32
+ if (inFlight) return inFlight
33
+ const pending = lookup(id, requestHeaders, upstream)
34
+ .catch(() => null)
35
+ .finally(() => lookups.delete(id))
36
+ lookups.set(id, pending)
37
+ return pending
38
+ }
39
+ }
@@ -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
@@ -0,0 +1,35 @@
1
+ import { type Config, claudeConfigDirs } from "../config"
2
+ import type { Message } from "../context/content"
3
+ import { searchGuide } from "../context/prompts"
4
+ import type { ThreadSources } from "../core/rewrite"
5
+ import { findSessionByPrefix } from "../transcript/locate"
6
+ import { sessionMessages } from "../transcript/messages"
7
+ import type { TranscriptRef } from "../transcript/search"
8
+ import { subagentChunks } from "../transcript/subagents"
9
+ import { titleOf } from "./sessions"
10
+
11
+ function readSession(file: string): Message[] {
12
+ try {
13
+ return sessionMessages(file)
14
+ } catch {
15
+ return []
16
+ }
17
+ }
18
+
19
+ export function sessionSources(transcript: TranscriptRef | null, config: Config, sessionId: string | null): ThreadSources {
20
+ const handoffs = {
21
+ findHandoff: (id: string) => {
22
+ const file = findSessionByPrefix(id, claudeConfigDirs(config), sessionId ?? undefined)
23
+ return file ? { label: titleOf(file) ?? id, source: file } : null
24
+ },
25
+ readHandoff: readSession,
26
+ }
27
+ if (!transcript) return handoffs
28
+ return {
29
+ ...handoffs,
30
+ guide: searchGuide(transcript),
31
+ transcriptDir: transcript.mode !== "jsonl" ? transcript.dir : null,
32
+ readHistory: () => sessionMessages(transcript.jsonl),
33
+ listSubagentChunks: () => subagentChunks(transcript.jsonl),
34
+ }
35
+ }
@@ -1,21 +1,7 @@
1
1
  import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
2
2
  import path from "node:path"
3
3
  import { paths } from "../config/paths"
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
- ratio?: number
17
- recalls?: Record<string, string>
18
- }
4
+ import type { ThreadState, ThreadStore } from "../core/state"
19
5
 
20
6
  function stateFile(key: string) {
21
7
  return path.join(paths.threads, `${key.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`)
@@ -63,8 +49,4 @@ export function saveState(key: string, state: ThreadState) {
63
49
  if (version !== null) remember(file, version, structuredClone(state))
64
50
  }
65
51
 
66
- export function resetCut(state: ThreadState) {
67
- delete state.cut
68
- delete state.keep
69
- delete state.cleared
70
- }
52
+ export const fileStore: ThreadStore = { read: readState, save: saveState }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
2
  import { EUNOE_DIR, paths } from "../config/paths"
3
+ import { createWindowLookup } from "../core/window"
3
4
 
4
5
  function readCache(): Map<string, number> {
5
6
  try {
@@ -9,40 +10,9 @@ function readCache(): Map<string, number> {
9
10
  }
10
11
  }
11
12
 
12
- const windows = readCache()
13
- const lookups = new Map<string, Promise<number | null>>()
14
-
15
- function lookupHeaders(requestHeaders: Headers): Headers {
16
- const headers = new Headers()
17
- for (const name of ["authorization", "x-api-key", "anthropic-version", "anthropic-beta"]) {
18
- const value = requestHeaders.get(name)
19
- if (value) headers.set(name, value)
20
- }
21
- if (!headers.has("anthropic-version")) headers.set("anthropic-version", "2023-06-01")
22
- return headers
23
- }
24
-
25
- async function lookup(id: string, requestHeaders: Headers, upstream: string): Promise<number | null> {
26
- const response = await fetch(`${upstream}/v1/models/${encodeURIComponent(id)}`, { headers: lookupHeaders(requestHeaders), signal: AbortSignal.timeout(5_000) })
27
- if (!response.ok) return null
28
- const info = (await response.json()) as { max_input_tokens?: number }
29
- if (typeof info.max_input_tokens !== "number" || info.max_input_tokens <= 0) return null
30
- windows.set(id, info.max_input_tokens)
13
+ function saveCache(windows: Map<string, number>) {
31
14
  mkdirSync(EUNOE_DIR, { recursive: true })
32
15
  writeFileSync(paths.models, JSON.stringify(Object.fromEntries(windows), null, 2))
33
- return info.max_input_tokens
34
16
  }
35
17
 
36
- export function resolveWindow(model: unknown, requestHeaders: Headers, upstream: string): Promise<number | null> {
37
- const id = String(model ?? "").replace(/\[[^\]]*\]$/, "")
38
- if (!id) return Promise.resolve(null)
39
- const known = windows.get(id)
40
- if (known) return Promise.resolve(known)
41
- const inFlight = lookups.get(id)
42
- if (inFlight) return inFlight
43
- const pending = lookup(id, requestHeaders, upstream)
44
- .catch(() => null)
45
- .finally(() => lookups.delete(id))
46
- lookups.set(id, pending)
47
- return pending
48
- }
18
+ export const resolveWindow = createWindowLookup({ known: readCache(), onLearn: saveCache })
@@ -1,9 +1,8 @@
1
1
  import { clip } from "../context/content"
2
2
  import type { ToolPart, TranscriptFormat, Turn } from "./parse"
3
- import type { SubagentRun } from "./subagents"
4
3
 
5
4
  export interface SubagentRecord {
6
- run: SubagentRun
5
+ run: { description: string }
7
6
  turns: Turn[]
8
7
  parentTurn: number
9
8
  file: string
@@ -2,7 +2,8 @@ import { readFileSync, statSync } from "node:fs"
2
2
  import path from "node:path"
3
3
  import { blocks, clip, contentBlocks, type Message } from "../context/content"
4
4
  import { splitTurns } from "../context/fold/turns"
5
- import { type RecallChunk, recallChunks } from "../context/recall"
5
+ import type { RecallChunk } from "../context/recall"
6
+ import { subagentRecall } from "../core/subagents"
6
7
  import { readSessionEntries } from "./entries"
7
8
  import { listEntries } from "./locate"
8
9
  import { sessionMessages } from "./messages"
@@ -163,8 +164,7 @@ export function subagentChunks(sessionFile: string): RecallChunk[] {
163
164
  const parentTurn = launchTurn(run, launches)
164
165
  const hit = chunkCache.get(key)
165
166
  if (hit && hit.mtimeMs === run.mtimeMs && hit.parentTurn === parentTurn) return hit.chunks
166
- const from = `subagent "${run.description}"`
167
- const chunks = recallChunks([...subagentMessages(run), { role: "user", content: "" }], new Map()).map((chunk) => ({ ...chunk, from, turn: parentTurn }))
167
+ const chunks = subagentRecall(subagentMessages(run), run.description, parentTurn)
168
168
  chunkCache.set(key, { mtimeMs: run.mtimeMs, parentTurn, chunks })
169
169
  return chunks
170
170
  })
File without changes
File without changes