@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,7 +1,8 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import path from "node:path"
3
3
  import type { SearchMode } from "../config"
4
- import { renderTranscript, transcriptDir } from "./render"
4
+ import { transcriptDir } from "./render"
5
+ import type { RenderDone, RenderJob } from "./render-worker"
5
6
 
6
7
  export interface TranscriptRef {
7
8
  mode: SearchMode
@@ -32,13 +33,53 @@ function indexWithQmd(dir: string) {
32
33
  return next
33
34
  }
34
35
 
36
+ const queued = new Map<string, { mode: SearchMode; sessionFile: string }>()
37
+ let renderer: Worker | null = null
38
+ let rendering: { dir: string; mode: SearchMode } | null = null
39
+ let drained: Array<() => void> = []
40
+
41
+ function finished(changed: boolean) {
42
+ if (rendering?.mode === "qmd" && (changed || !isIndexed(rendering.dir))) void indexWithQmd(rendering.dir)
43
+ rendering = null
44
+ renderNext()
45
+ }
46
+
47
+ function startRenderer(): Worker {
48
+ const worker = new Worker(new URL("./render-worker.ts", import.meta.url))
49
+ worker.onmessage = ({ data }: MessageEvent<RenderDone>) => finished(data.changed)
50
+ worker.onerror = () => {
51
+ renderer = null
52
+ finished(false)
53
+ }
54
+ ;(worker as Worker & { unref(): void }).unref()
55
+ return worker
56
+ }
57
+
58
+ function renderNext() {
59
+ const next = queued.entries().next()
60
+ if (next.done) {
61
+ const waiting = drained
62
+ drained = []
63
+ for (const resolve of waiting) resolve()
64
+ return
65
+ }
66
+ const [dir, { mode, sessionFile }] = next.value
67
+ queued.delete(dir)
68
+ rendering = { dir, mode }
69
+ renderer ??= startRenderer()
70
+ renderer.postMessage({ dir, sessionFile, format: mode === "xml" ? "xml" : "markdown" } satisfies RenderJob)
71
+ }
72
+
73
+ export function flushTranscripts(): Promise<void> {
74
+ if (!rendering && queued.size === 0) return Promise.resolve()
75
+ return new Promise((resolve) => drained.push(resolve))
76
+ }
77
+
35
78
  export function prepareTranscript(mode: SearchMode, sessionFile: string, key: string): TranscriptRef {
36
79
  const dir = transcriptDir(key)
37
80
  if (mode !== "jsonl") {
38
- try {
39
- const { changed } = renderTranscript(sessionFile, key, dir, mode === "xml" ? "xml" : "markdown")
40
- if (mode === "qmd" && (changed || !isIndexed(dir))) void indexWithQmd(dir)
41
- } catch {}
81
+ queued.set(dir, { mode, sessionFile })
82
+ if (!rendering) renderNext()
42
83
  }
43
84
  return { mode, jsonl: sessionFile, dir }
44
85
  }
@@ -2,10 +2,12 @@ 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"
7
+ import { readSessionEntries } from "./entries"
6
8
  import { listEntries } from "./locate"
7
9
  import { sessionMessages } from "./messages"
8
- import { entryMessages, parseJsonl, type SessionEntry } from "./parse"
10
+ import { entryMessages, type SessionEntry } from "./parse"
9
11
 
10
12
  export interface SubagentRun {
11
13
  id: string
@@ -20,7 +22,7 @@ export interface SubagentRun {
20
22
 
21
23
  const INLINE_ID = "sidechain"
22
24
 
23
- const entryCache = new Map<string, { mtimeMs: number; entries: SessionEntry[] }>()
25
+ const runCache = new Map<string, SubagentRun>()
24
26
  const chunkCache = new Map<string, { mtimeMs: number; parentTurn: number; chunks: RecallChunk[] }>()
25
27
 
26
28
  function mtimeOf(file: string): number | null {
@@ -31,16 +33,6 @@ function mtimeOf(file: string): number | null {
31
33
  }
32
34
  }
33
35
 
34
- function readEntries(file: string): SessionEntry[] {
35
- const mtimeMs = mtimeOf(file)
36
- if (mtimeMs === null) return []
37
- const hit = entryCache.get(file)
38
- if (hit?.mtimeMs === mtimeMs) return hit.entries
39
- const entries = parseJsonl(readFileSync(file, "utf8"))
40
- entryCache.set(file, { mtimeMs, entries })
41
- return entries
42
- }
43
-
44
36
  function firstPrompt(entries: SessionEntry[]): string {
45
37
  for (const entry of entries) {
46
38
  if (entry.type !== "user" || !entry.message) continue
@@ -72,10 +64,13 @@ function fileRuns(sessionFile: string): SubagentRun[] {
72
64
  .filter((name) => name.startsWith("agent-") && name.endsWith(".jsonl"))
73
65
  .map((name) => {
74
66
  const file = path.join(dir, name)
75
- const entries = readEntries(file)
67
+ const mtimeMs = mtimeOf(file) ?? 0
68
+ const known = runCache.get(file)
69
+ if (known?.mtimeMs === mtimeMs) return known
70
+ const entries = readSessionEntries(file)
76
71
  const meta = readMeta(file)
77
72
  const prompt = firstPrompt(entries)
78
- return {
73
+ const run: SubagentRun = {
79
74
  id: name.slice("agent-".length, -".jsonl".length),
80
75
  file,
81
76
  inline: false,
@@ -83,17 +78,21 @@ function fileRuns(sessionFile: string): SubagentRun[] {
83
78
  toolUseId: meta.toolUseId ? String(meta.toolUseId) : null,
84
79
  prompt,
85
80
  started: String(entries[0]?.timestamp ?? ""),
86
- mtimeMs: mtimeOf(file) ?? 0,
81
+ mtimeMs,
87
82
  }
83
+ runCache.set(file, run)
84
+ return run
88
85
  })
89
86
  }
90
87
 
91
88
  function inlineRuns(sessionFile: string, known: Set<string>): SubagentRun[] {
92
89
  const byAgent = new Map<string, SessionEntry[]>()
93
- for (const entry of readEntries(sessionFile)) {
90
+ for (const entry of readSessionEntries(sessionFile)) {
94
91
  if (!entry.isSidechain) continue
95
92
  const id = entry.agentId ?? INLINE_ID
96
- byAgent.set(id, [...(byAgent.get(id) ?? []), entry])
93
+ const group = byAgent.get(id)
94
+ if (group) group.push(entry)
95
+ else byAgent.set(id, [entry])
97
96
  }
98
97
  return [...byAgent]
99
98
  .filter(([id]) => !known.has(id))
@@ -110,7 +109,7 @@ export function subagentRuns(sessionFile: string): SubagentRun[] {
110
109
  }
111
110
 
112
111
  export function subagentEntries(run: SubagentRun): SessionEntry[] {
113
- const entries = readEntries(run.file)
112
+ const entries = readSessionEntries(run.file)
114
113
  if (!run.inline) return entries
115
114
  return entries.filter((entry) => entry.isSidechain && (entry.agentId ?? INLINE_ID) === run.id)
116
115
  }
@@ -119,31 +118,53 @@ export function subagentMessages(run: SubagentRun): Message[] {
119
118
  return entryMessages(subagentEntries(run))
120
119
  }
121
120
 
122
- function launchedBy(run: SubagentRun, call: { id?: unknown; name?: unknown; prompt?: unknown }): boolean {
123
- if (run.toolUseId) return call.id === run.toolUseId
124
- return (call.name === "Agent" || call.name === "Task") && String(call.prompt ?? "") === run.prompt
121
+ interface Launches {
122
+ byId: Map<unknown, number>
123
+ byPrompt: Map<string, number>
124
+ }
125
+
126
+ function launchesIn(parentMessages: Message[]): Launches {
127
+ const launches: Launches = { byId: new Map(), byPrompt: new Map() }
128
+ splitTurns(parentMessages).turns.forEach((turn, index) => {
129
+ for (const message of turn) {
130
+ if (message.role !== "assistant") continue
131
+ for (const block of blocks(message)) {
132
+ if (block.type !== "tool_use") continue
133
+ if (!launches.byId.has(block.id)) launches.byId.set(block.id, index + 1)
134
+ const prompt = String(block.input?.prompt ?? "")
135
+ if ((block.name === "Agent" || block.name === "Task") && !launches.byPrompt.has(prompt)) launches.byPrompt.set(prompt, index + 1)
136
+ }
137
+ }
138
+ })
139
+ return launches
125
140
  }
126
141
 
142
+ const launchTurn = (run: SubagentRun, launches: Launches) => (run.toolUseId ? launches.byId.get(run.toolUseId) : launches.byPrompt.get(run.prompt)) ?? 0
143
+
127
144
  export function parentTurnOf(run: SubagentRun, parentMessages: Message[]): number {
128
- const { turns } = splitTurns(parentMessages)
129
- const index = turns.findIndex((turn) =>
130
- turn
131
- .filter((message) => message.role === "assistant")
132
- .flatMap(blocks)
133
- .some((block) => block.type === "tool_use" && launchedBy(run, { id: block.id, name: block.name, prompt: block.input?.prompt })),
134
- )
135
- return index + 1
145
+ return launchTurn(run, launchesIn(parentMessages))
146
+ }
147
+
148
+ const KEPT_LAUNCHES = 8
149
+ const launchCache = new Map<Message[], Launches>()
150
+
151
+ function cachedLaunches(parentMessages: Message[]): Launches {
152
+ const known = launchCache.get(parentMessages)
153
+ if (known) return known
154
+ const launches = launchesIn(parentMessages)
155
+ launchCache.set(parentMessages, launches)
156
+ if (launchCache.size > KEPT_LAUNCHES) launchCache.delete(launchCache.keys().next().value as Message[])
157
+ return launches
136
158
  }
137
159
 
138
160
  export function subagentChunks(sessionFile: string): RecallChunk[] {
139
- const parent = sessionMessages(sessionFile)
161
+ const launches = cachedLaunches(sessionMessages(sessionFile))
140
162
  return subagentRuns(sessionFile).flatMap((run) => {
141
163
  const key = `${run.file}#${run.id}`
142
- const parentTurn = parentTurnOf(run, parent)
164
+ const parentTurn = launchTurn(run, launches)
143
165
  const hit = chunkCache.get(key)
144
166
  if (hit && hit.mtimeMs === run.mtimeMs && hit.parentTurn === parentTurn) return hit.chunks
145
- const from = `subagent "${run.description}"`
146
- 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)
147
168
  chunkCache.set(key, { mtimeMs: run.mtimeMs, parentTurn, chunks })
148
169
  return chunks
149
170
  })
File without changes
File without changes