@chroxify/eunoe 1.2.1 → 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 (70) hide show
  1. package/README.md +143 -5
  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/serve.ts +2 -0
  31. package/src/cli/commands/status.ts +1 -1
  32. package/src/cli/proxy/stats.ts +1 -1
  33. package/src/cli/replay/index.ts +3 -3
  34. package/src/cli/sessions/index.ts +2 -2
  35. package/src/config/index.ts +4 -27
  36. package/src/context/content.ts +64 -1
  37. package/src/context/fold/strategies.ts +55 -10
  38. package/src/context/fold/turns.ts +22 -2
  39. package/src/context/prompts.ts +4 -3
  40. package/src/context/recall/index.ts +259 -35
  41. package/src/context/recall/rerank.ts +0 -8
  42. package/src/{proxy → core}/compaction.ts +74 -24
  43. package/src/core/config.ts +31 -0
  44. package/src/core/fold.ts +69 -0
  45. package/src/core/forward.ts +192 -0
  46. package/src/{proxy → core}/handoff.ts +23 -23
  47. package/src/core/index.ts +14 -0
  48. package/src/{proxy → core}/recall.ts +18 -20
  49. package/src/core/rewrite.ts +189 -0
  50. package/src/core/state.ts +40 -0
  51. package/src/core/subagents.ts +28 -0
  52. package/src/core/transcript.ts +44 -0
  53. package/src/{proxy → core}/usage.ts +34 -1
  54. package/src/core/window.ts +39 -0
  55. package/src/proxy/rewrite.ts +22 -139
  56. package/src/proxy/server.ts +24 -137
  57. package/src/proxy/sessions.ts +1 -1
  58. package/src/proxy/sources.ts +35 -0
  59. package/src/proxy/state.ts +32 -21
  60. package/src/proxy/warm.ts +68 -0
  61. package/src/proxy/window.ts +3 -33
  62. package/src/transcript/entries.ts +114 -0
  63. package/src/transcript/format.ts +1 -2
  64. package/src/transcript/messages.ts +14 -11
  65. package/src/transcript/render-worker.ts +24 -0
  66. package/src/transcript/render.ts +12 -3
  67. package/src/transcript/search.ts +46 -5
  68. package/src/transcript/subagents.ts +55 -34
  69. /package/src/{proxy → core}/estimate.ts +0 -0
  70. /package/src/{proxy → core}/fingerprint.ts +0 -0
@@ -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,109 +52,31 @@ 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
- export async function handle(request: Request): Promise<Response> {
97
- try {
98
- return await route(request)
99
- } catch (error) {
100
- logRequest({ path: new URL(request.url).pathname, status: 502, action: "upstream-unreachable", error: clipped(String(error)) })
101
- return Response.json({ type: "error", error: { type: "api_error", message: `eunoe could not reach the API: ${String(error)}` } }, { status: 502 })
102
- }
63
+ interface Timing {
64
+ countTokensTimeoutMs: number
103
65
  }
104
66
 
105
- async function route(request: Request): Promise<Response> {
67
+ export function handle(request: Request, timing: Timing = { countTokensTimeoutMs: 10_000 }): Promise<Response> {
106
68
  const config = readConfig()
107
- const url = new URL(request.url)
108
- if (url.pathname === HEALTH_PATH) return Response.json({ ok: true, version: VERSION, mode: config.mode, pid: process.pid, startedAt } satisfies Health)
109
- const headers = new Headers(request.headers)
110
- for (const name of ["host", "content-length", "accept-encoding"]) headers.delete(name)
111
-
112
- let givenText: string | undefined
113
- if (request.method !== "GET" && request.method !== "HEAD") givenText = await request.text()
114
- const isMessages = request.method === "POST" && url.pathname.startsWith("/v1/messages")
115
- const isCountTokens = url.pathname.includes("count_tokens")
116
-
117
- const build = async (): Promise<Outgoing> => {
118
- const passthrough: Outgoing = { note: { action: "pass" }, threadKey: null, bodyText: givenText, sentSize: 0, givenSize: 0, fixedSize: 0, sent: null }
119
- if (!givenText || !isMessages) return passthrough
120
- try {
121
- const given = JSON.parse(givenText) as Body
122
- const canonicalFile = trustedHeader(request, config, "canonical")
123
- const canonical = canonicalFile ? applyCanonical(given, canonicalFile) : null
124
- const result = await prepare(request, canonical?.body ?? given, config, isCountTokens)
125
- const sentSize = sizeOf(result.body)
126
- return {
127
- note: canonical ? { ...result.note, canonical: canonical.status } : result.note,
128
- threadKey: result.threadKey,
129
- bodyText: result.body !== given ? JSON.stringify(result.body, wellFormed) : givenText,
130
- sentSize,
131
- givenSize: result.body === given ? sentSize : sizeOf(given),
132
- fixedSize: sizeOf({ ...result.body, messages: [] }),
133
- sent: config.eval ? { session: sessionIdOf(request.headers, given), ...contextFingerprint(result.body), given: contextFingerprint(given) } : null,
134
- }
135
- } catch (error) {
136
- return { ...passthrough, note: { action: "error-passthrough", error: String(error) } }
137
- }
138
- }
139
- const send = (body: string | undefined) => fetch(config.upstream + url.pathname + url.search, { method: request.method, headers, body })
140
-
141
- let out = await build()
142
- let upstream = await send(out.bodyText)
143
- const errorResponse = (text: string) => new Response(text, { status: upstream.status, statusText: upstream.statusText, headers: forwardableHeaders(upstream.headers) })
144
-
145
- if (upstream.status >= 400 && out.note.action !== "pass") {
146
- const errorText = await upstream.text()
147
- const overflow = overflowTokens(errorText)
148
- const logged = { path: url.pathname, status: upstream.status, thread: out.threadKey, ...out.note }
149
- if (!overflow || !out.threadKey || out.sentSize === 0 || isCountTokens) {
150
- logRequest({ ...logged, error: clipped(errorText) })
151
- return errorResponse(errorText)
152
- }
153
- const sentSize = out.sentSize
154
- saveRatio(out.threadKey, (previous) => Math.max(previous ?? 0, overflow / sentSize) * 1.1)
155
- logRequest({ ...logged, overflow, error: clipped(errorText) })
156
- out = await build()
157
- out = { ...out, note: { ...out.note, healed: overflow } }
158
- upstream = await send(out.bodyText)
159
- if (upstream.status >= 400) {
160
- const retryError = await upstream.text()
161
- logRequest({ path: url.pathname, status: upstream.status, thread: out.threadKey, ...out.note, error: clipped(retryError) })
162
- return errorResponse(retryError)
163
- }
164
- }
165
-
166
- const { note, threadKey, sentSize, givenSize, fixedSize, sent } = out
167
- const response = new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: forwardableHeaders(upstream.headers) })
168
- if (note.action === "pass" || !isMessages || isCountTokens) {
169
- if (note.action !== "pass" || sent) logRequest({ path: url.pathname, status: upstream.status, ...note, ...(sent && { context: sent }) })
170
- return response
171
- }
172
-
173
- return watchUsage(response, (usage) => {
174
- const input = inputTokens(usage)
175
- logRequest({
176
- path: url.pathname,
177
- status: upstream.status,
178
- thread: threadKey,
179
- ...note,
180
- input,
181
- cacheRead: usage.cache_read_input_tokens ?? 0,
182
- cacheWrite: usage.cache_creation_input_tokens ?? 0,
183
- uncached: usage.input_tokens ?? 0,
184
- charsIn: givenSize,
185
- charsOut: sentSize,
186
- charsFixed: fixedSize,
187
- ...(sent && { context: sent }),
188
- })
189
- 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,
190
76
  })
77
+ return forward(request)
191
78
  }
192
79
 
193
80
  export function serve(port: number) {
194
- return Bun.serve({ port, hostname: "127.0.0.1", idleTimeout: 255, fetch: handle })
81
+ return Bun.serve({ port, hostname: "127.0.0.1", idleTimeout: 255, fetch: (request) => handle(request) })
195
82
  }
@@ -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,29 +1,41 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
1
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
2
2
  import path from "node:path"
3
3
  import { paths } from "../config/paths"
4
+ import type { ThreadState, ThreadStore } from "../core/state"
4
5
 
5
- export interface Handoff {
6
- label: string
7
- source: string
8
- text: string
6
+ function stateFile(key: string) {
7
+ return path.join(paths.threads, `${key.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`)
9
8
  }
10
9
 
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>
10
+ const KEPT_THREADS = 64
11
+
12
+ const known = new Map<string, { version: string; state: ThreadState }>()
13
+
14
+ function versionOf(file: string): string | null {
15
+ try {
16
+ const { mtimeMs, size } = statSync(file)
17
+ return `${mtimeMs}:${size}`
18
+ } catch {
19
+ return null
20
+ }
18
21
  }
19
22
 
20
- function stateFile(key: string) {
21
- return path.join(paths.threads, `${key.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`)
23
+ function remember(file: string, version: string, state: ThreadState) {
24
+ known.delete(file)
25
+ known.set(file, { version, state })
26
+ if (known.size > KEPT_THREADS) known.delete(known.keys().next().value as string)
22
27
  }
23
28
 
24
29
  export function readState(key: string): ThreadState {
30
+ const file = stateFile(key)
31
+ const version = versionOf(file)
32
+ if (version === null) return {}
33
+ const hit = known.get(file)
34
+ if (hit?.version === version) return structuredClone(hit.state)
25
35
  try {
26
- return JSON.parse(readFileSync(stateFile(key), "utf8"))
36
+ const state = JSON.parse(readFileSync(file, "utf8")) as ThreadState
37
+ remember(file, version, state)
38
+ return structuredClone(state)
27
39
  } catch {
28
40
  return {}
29
41
  }
@@ -31,11 +43,10 @@ export function readState(key: string): ThreadState {
31
43
 
32
44
  export function saveState(key: string, state: ThreadState) {
33
45
  mkdirSync(paths.threads, { recursive: true })
34
- writeFileSync(stateFile(key), JSON.stringify(state))
46
+ const file = stateFile(key)
47
+ writeFileSync(file, JSON.stringify(state))
48
+ const version = versionOf(file)
49
+ if (version !== null) remember(file, version, structuredClone(state))
35
50
  }
36
51
 
37
- export function resetCut(state: ThreadState) {
38
- delete state.cut
39
- delete state.keep
40
- delete state.cleared
41
- }
52
+ export const fileStore: ThreadStore = { read: readState, save: saveState }
@@ -0,0 +1,68 @@
1
+ import path from "node:path"
2
+ import { type Config, claudeConfigDirs } from "../config"
3
+ import type { Message } from "../context/content"
4
+ import { splitTurns } from "../context/fold/turns"
5
+ import { recallChunks, warmIndex } from "../context/recall"
6
+ import { readSessionSlice } from "../transcript/entries"
7
+ import { findSessionFile, listEntries } from "../transcript/locate"
8
+ import { sessionMessages } from "../transcript/messages"
9
+ import { subagentChunks, subagentMessages, subagentRuns, subagentsDir } from "../transcript/subagents"
10
+ import { seenSessions } from "./sessions"
11
+
12
+ const SLICE_MS = 8
13
+ const SLICE_BYTES = 2 * 1024 * 1024
14
+ const RECENT_MS = 24 * 60 * 60 * 1000
15
+ const MAX_SESSIONS = 16
16
+ const TURNS_PER_SLICE = 40
17
+
18
+ let deadline = 0
19
+
20
+ async function breathe() {
21
+ if (performance.now() < deadline) return
22
+ await new Promise((resolve) => setTimeout(resolve, 1))
23
+ deadline = performance.now() + SLICE_MS
24
+ }
25
+
26
+ const closingTurn: Message = { role: "user", content: "" }
27
+
28
+ async function warmSession(file: string) {
29
+ while (!readSessionSlice(file, SLICE_BYTES)) await breathe()
30
+ await breathe()
31
+ for (const name of listEntries(subagentsDir(file))) {
32
+ if (!name.endsWith(".jsonl")) continue
33
+ while (!readSessionSlice(path.join(subagentsDir(file), name), SLICE_BYTES)) await breathe()
34
+ await breathe()
35
+ }
36
+ const messages = sessionMessages(file)
37
+ await breathe()
38
+ const { preamble, turns } = splitTurns(messages)
39
+ let end = preamble.length
40
+ for (let i = 0; i < turns.length; i += 1) {
41
+ end += turns[i].length
42
+ if ((i + 1) % TURNS_PER_SLICE !== 0 && i !== turns.length - 1) continue
43
+ recallChunks([...messages.slice(0, end), closingTurn], new Map())
44
+ await breathe()
45
+ }
46
+ for (const run of subagentRuns(file)) {
47
+ recallChunks([...subagentMessages(run), closingTurn], new Map())
48
+ await breathe()
49
+ }
50
+ const corpus = [...recallChunks([...messages, closingTurn], new Map()), ...subagentChunks(file)]
51
+ await breathe()
52
+ while (!warmIndex(corpus, performance.now() + SLICE_MS)) await breathe()
53
+ }
54
+
55
+ export async function warmRecentSessions(config: Config, now = Date.now()) {
56
+ deadline = performance.now() + SLICE_MS
57
+ const recent = seenSessions()
58
+ .filter(([, seen]) => seen.mode !== "off" && now - Date.parse(seen.lastSeen) < RECENT_MS)
59
+ .slice(0, MAX_SESSIONS)
60
+ for (const [id] of recent) {
61
+ const file = findSessionFile(id, claudeConfigDirs(config))
62
+ if (!file) continue
63
+ try {
64
+ await warmSession(file)
65
+ } catch {}
66
+ await breathe()
67
+ }
68
+ }
@@ -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 })
@@ -0,0 +1,114 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs"
2
+ import type { SessionEntry } from "./parse"
3
+
4
+ interface Parsed {
5
+ size: number
6
+ mtimeMs: number
7
+ offset: number
8
+ seam: Buffer
9
+ committed: SessionEntry[]
10
+ entries: SessionEntry[]
11
+ }
12
+
13
+ const BUDGET_BYTES = 768 * 1024 * 1024
14
+ const NEWLINE = 0x0a
15
+ const SEAM_BYTES = 64
16
+
17
+ const parsed = new Map<string, Parsed>()
18
+ let heldBytes = 0
19
+
20
+ function parseLines(text: string): SessionEntry[] {
21
+ const out: SessionEntry[] = []
22
+ for (const line of text.split("\n")) {
23
+ if (!line) continue
24
+ try {
25
+ out.push(JSON.parse(line) as SessionEntry)
26
+ } catch {}
27
+ }
28
+ return out
29
+ }
30
+
31
+ function readRange(file: string, from: number, to: number): Buffer {
32
+ const buffer = Buffer.allocUnsafe(Math.max(0, to - from))
33
+ const fd = openSync(file, "r")
34
+ try {
35
+ let read = 0
36
+ while (read < buffer.length) {
37
+ const n = readSync(fd, buffer, read, buffer.length - read, from + read)
38
+ if (n === 0) break
39
+ read += n
40
+ }
41
+ return buffer.subarray(0, read)
42
+ } finally {
43
+ closeSync(fd)
44
+ }
45
+ }
46
+
47
+ function remember(file: string, next: Parsed) {
48
+ heldBytes += next.size - (parsed.get(file)?.size ?? 0)
49
+ parsed.delete(file)
50
+ parsed.set(file, next)
51
+ for (const [key, value] of parsed) {
52
+ if (heldBytes <= BUDGET_BYTES || key === file) break
53
+ parsed.delete(key)
54
+ heldBytes -= value.size
55
+ }
56
+ }
57
+
58
+ function appendable(file: string, known: Parsed | undefined, size: number): known is Parsed {
59
+ if (!known || size < known.size || known.offset === 0) return false
60
+ return readRange(file, known.offset - known.seam.length, known.offset).equals(known.seam)
61
+ }
62
+
63
+ export function readSessionSlice(file: string, maxBytes: number): boolean {
64
+ let size: number
65
+ let mtimeMs: number
66
+ try {
67
+ ;({ size, mtimeMs } = statSync(file))
68
+ } catch {
69
+ return true
70
+ }
71
+ const known = parsed.get(file)
72
+ if (known && known.size === size && known.mtimeMs === mtimeMs) return true
73
+ const appended = appendable(file, known, size)
74
+ const from = appended ? known.offset : 0
75
+ const bytes = from + maxBytes < size ? readRange(file, from, from + maxBytes) : null
76
+ const complete = bytes ? bytes.lastIndexOf(NEWLINE) + 1 : 0
77
+ if (!bytes || complete === 0) {
78
+ readSessionEntries(file)
79
+ return true
80
+ }
81
+ const added = parseLines(bytes.subarray(0, complete).toString("utf8"))
82
+ const committed = appended ? known.committed.concat(added) : added
83
+ const offset = from + complete
84
+ remember(file, { size: offset, mtimeMs: 0, offset, seam: readRange(file, Math.max(0, offset - SEAM_BYTES), offset), committed, entries: committed })
85
+ return false
86
+ }
87
+
88
+ export function readSessionEntries(file: string): SessionEntry[] {
89
+ let size: number
90
+ let mtimeMs: number
91
+ try {
92
+ ;({ size, mtimeMs } = statSync(file))
93
+ } catch {
94
+ return []
95
+ }
96
+ const known = parsed.get(file)
97
+ if (known && known.size === size && known.mtimeMs === mtimeMs) {
98
+ parsed.delete(file)
99
+ parsed.set(file, known)
100
+ return known.entries
101
+ }
102
+ const appended = appendable(file, known, size)
103
+ const from = appended ? known.offset : 0
104
+ const bytes = readRange(file, from, size)
105
+ const complete = bytes.lastIndexOf(NEWLINE) + 1
106
+ const added = parseLines(bytes.subarray(0, complete).toString("utf8"))
107
+ const committed = appended ? (added.length > 0 ? known.committed.concat(added) : known.committed) : added
108
+ const tail = parseLines(bytes.subarray(complete).toString("utf8"))
109
+ const entries = tail.length > 0 ? committed.concat(tail) : committed
110
+ const offset = from + complete
111
+ const seam = readRange(file, Math.max(0, offset - SEAM_BYTES), offset)
112
+ remember(file, { size, mtimeMs, offset, seam, committed, entries })
113
+ return entries
114
+ }
@@ -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
@@ -1,18 +1,21 @@
1
- import { readFileSync, statSync } from "node:fs"
2
1
  import type { Message } from "../context/content"
3
- import { entryMessages, parseJsonl } from "./parse"
2
+ import { readSessionEntries } from "./entries"
3
+ import { entryMessages, type SessionEntry } from "./parse"
4
4
 
5
- let cached: { file: string; mtimeMs: number; messages: Message[] } | null = null
5
+ const KEPT_SESSIONS = 32
6
+
7
+ const cached = new Map<string, { entries: SessionEntry[]; messages: Message[] }>()
6
8
 
7
9
  export function sessionMessages(file: string): Message[] {
8
- let mtimeMs: number
9
- try {
10
- mtimeMs = statSync(file).mtimeMs
11
- } catch {
12
- return []
10
+ const entries = readSessionEntries(file)
11
+ const known = cached.get(file)
12
+ cached.delete(file)
13
+ if (known?.entries === entries) {
14
+ cached.set(file, known)
15
+ return known.messages
13
16
  }
14
- if (cached?.file === file && cached.mtimeMs === mtimeMs) return cached.messages
15
- const messages = entryMessages(parseJsonl(readFileSync(file, "utf8")).filter((entry) => !entry.isSidechain))
16
- cached = { file, mtimeMs, messages }
17
+ const messages = entryMessages(entries.filter((entry) => !entry.isSidechain))
18
+ cached.set(file, { entries, messages })
19
+ if (cached.size > KEPT_SESSIONS) cached.delete(cached.keys().next().value as string)
17
20
  return messages
18
21
  }
@@ -0,0 +1,24 @@
1
+ import path from "node:path"
2
+ import type { TranscriptFormat } from "./parse"
3
+ import { renderTranscript } from "./render"
4
+
5
+ declare const self: Worker
6
+
7
+ export interface RenderJob {
8
+ dir: string
9
+ sessionFile: string
10
+ format: TranscriptFormat
11
+ }
12
+
13
+ export interface RenderDone {
14
+ dir: string
15
+ changed: boolean
16
+ }
17
+
18
+ self.onmessage = ({ data }: MessageEvent<RenderJob>) => {
19
+ let changed = false
20
+ try {
21
+ changed = renderTranscript(data.sessionFile, path.basename(data.dir), data.dir, data.format).changed
22
+ } catch {}
23
+ self.postMessage({ dir: data.dir, changed } satisfies RenderDone)
24
+ }
@@ -1,8 +1,9 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
2
2
  import path from "node:path"
3
3
  import { paths } from "../config/paths"
4
+ import { readSessionEntries } from "./entries"
4
5
  import { renderIndex, renderMarkdown, renderSubagentMarkdown, renderSubagentXml, renderXml, type SubagentRecord, subagentFile, turnFile } from "./format"
5
- import { parseJsonl, parseTurns, type ToolPart, type TranscriptFormat, type Turn } from "./parse"
6
+ import { parseTurns, type ToolPart, type TranscriptFormat, type Turn } from "./parse"
6
7
  import { type SubagentRun, subagentEntries, subagentRuns } from "./subagents"
7
8
 
8
9
  interface RenderResult {
@@ -11,6 +12,8 @@ interface RenderResult {
11
12
  turns: number
12
13
  }
13
14
 
15
+ const written = new Map<string, number | bigint>()
16
+
14
17
  export function transcriptDir(sessionId: string) {
15
18
  return path.join(paths.transcripts, sessionId)
16
19
  }
@@ -45,13 +48,19 @@ export function renderTranscript(sessionFile: string, sessionId: string, outDir
45
48
  const stamp = [statSync(sessionFile).mtimeMs, ...runs.map((run) => `${run.id}:${run.mtimeMs}`)].join("|")
46
49
  if (existsSync(stampFile) && readFileSync(stampFile, "utf8") === stamp) return { dir: outDir, changed: false, turns: Number(readFileSync(countFile, "utf8") || 0) }
47
50
 
48
- const turns = parseTurns(parseJsonl(readFileSync(sessionFile, "utf8")))
51
+ const turns = parseTurns(readSessionEntries(sessionFile))
49
52
  const records = subagentRecords(turns, runs, format)
50
53
  let changed = false
51
54
  const write = (file: string, content: string) => {
52
55
  const target = path.join(outDir, file)
53
- if (existsSync(target) && readFileSync(target, "utf8") === content) return
56
+ const hash = Bun.hash(content)
57
+ if (written.get(target) === hash && existsSync(target)) return
58
+ if (!written.has(target) && existsSync(target) && readFileSync(target, "utf8") === content) {
59
+ written.set(target, hash)
60
+ return
61
+ }
54
62
  writeFileSync(target, content)
63
+ written.set(target, hash)
55
64
  changed = true
56
65
  }
57
66
  for (const turn of turns) write(turnFile(turn, format), format === "xml" ? renderXml(turn) : renderMarkdown(turn))