@chroxify/eunoe 1.0.0 → 1.2.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.
- package/README.md +68 -9
- package/package.json +18 -2
- package/src/cli/{types.ts → command.ts} +0 -4
- package/src/cli/commands/index.ts +13 -1
- package/src/cli/commands/load.ts +44 -0
- package/src/cli/commands/mode.ts +41 -33
- package/src/cli/commands/peek.ts +22 -0
- package/src/cli/commands/replay.ts +245 -0
- package/src/cli/commands/serve.ts +22 -7
- package/src/cli/commands/sessions.ts +70 -0
- package/src/cli/commands/settings.ts +79 -60
- package/src/cli/commands/start.ts +56 -21
- package/src/cli/commands/stats.ts +233 -0
- package/src/cli/commands/status.ts +98 -55
- package/src/cli/commands/stop.ts +8 -6
- package/src/cli/commands/uninstall.ts +10 -12
- package/src/cli/commands/watch.ts +16 -0
- package/src/cli/logo/art.ts +99 -0
- package/src/cli/logo/index.ts +12 -18
- package/src/cli/main.ts +3 -3
- package/src/cli/proxy/claude-settings.ts +69 -0
- package/src/cli/proxy/inspect.ts +67 -0
- package/src/cli/proxy/launchd.ts +53 -0
- package/src/cli/proxy/live.ts +57 -0
- package/src/cli/proxy/service.ts +53 -0
- package/src/cli/{stats.ts → proxy/stats.ts} +29 -16
- package/src/cli/proxy/watch.ts +60 -0
- package/src/cli/replay/index.ts +146 -0
- package/src/cli/replay/worker.ts +20 -0
- package/src/cli/sessions/browse.ts +88 -0
- package/src/cli/sessions/index.ts +76 -0
- package/src/cli/sessions/source.ts +74 -0
- package/src/cli/{help.ts → terminal/help.ts} +24 -8
- package/src/cli/terminal/prompt.ts +114 -0
- package/src/cli/{ui.ts → terminal/ui.ts} +55 -24
- package/src/cli/usage/index.ts +151 -0
- package/src/config/index.ts +99 -34
- package/src/config/paths.ts +17 -0
- package/src/context/content.ts +40 -14
- package/src/context/fold/evidence.ts +145 -0
- package/src/context/fold/index.ts +65 -0
- package/src/context/fold/strategies.ts +94 -0
- package/src/context/fold/turns.ts +76 -0
- package/src/context/prompts.ts +141 -22
- package/src/context/recall/index.ts +214 -0
- package/src/context/recall/rerank.ts +71 -0
- package/src/proxy/canonical.ts +17 -9
- package/src/proxy/compaction.ts +106 -53
- package/src/proxy/estimate.ts +14 -9
- package/src/proxy/fingerprint.ts +23 -9
- package/src/proxy/handoff.ts +71 -0
- package/src/proxy/log.ts +2 -2
- package/src/proxy/recall.ts +58 -0
- package/src/proxy/rewrite.ts +141 -119
- package/src/proxy/server.ts +108 -51
- package/src/proxy/sessions.ts +32 -14
- package/src/proxy/state.ts +17 -3
- package/src/proxy/usage.ts +50 -34
- package/src/proxy/window.ts +32 -34
- package/src/transcript/format.ts +88 -25
- package/src/transcript/locate.ts +38 -17
- package/src/transcript/messages.ts +3 -14
- package/src/transcript/parse.ts +137 -56
- package/src/transcript/render.ts +44 -17
- package/src/transcript/search.ts +20 -56
- package/src/transcript/subagents.ts +150 -0
- package/src/cli/claude-settings.ts +0 -42
- package/src/cli/constants.ts +0 -52
- package/src/cli/inspect.ts +0 -41
- package/src/cli/launchd.ts +0 -39
- package/src/cli/logo/pixels.ts +0 -34
- package/src/cli/prompt.ts +0 -91
- package/src/cli/service.ts +0 -33
- package/src/cli/sessions.ts +0 -25
- package/src/config/constants.ts +0 -39
- package/src/config/types.ts +0 -50
- package/src/context/constants.ts +0 -42
- package/src/context/evidence.ts +0 -92
- package/src/context/recall.ts +0 -155
- package/src/context/rerank.ts +0 -44
- package/src/context/strategies.ts +0 -89
- package/src/context/turns.ts +0 -87
- package/src/context/types.ts +0 -66
- package/src/proxy/constants.ts +0 -29
- package/src/proxy/types.ts +0 -82
- package/src/transcript/constants.ts +0 -15
- package/src/transcript/types.ts +0 -43
- /package/src/cli/{args.ts → terminal/args.ts} +0 -0
package/src/proxy/compaction.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import type
|
|
8
|
-
|
|
1
|
+
import type { Config } from "../config"
|
|
2
|
+
import type { Body, Message } from "../context/content"
|
|
3
|
+
import { type CompactOptions, capTurns, clearToolResults, compactMessages, countToolResults } from "../context/fold/strategies"
|
|
4
|
+
import { splitTurns, turnFingerprint } from "../context/fold/turns"
|
|
5
|
+
import { clearedOutput } from "../context/prompts"
|
|
6
|
+
import { DEFAULT_RATIO, type Estimator, estimator, targetSize } from "./estimate"
|
|
7
|
+
import { resetCut, type ThreadState } from "./state"
|
|
8
|
+
|
|
9
|
+
export interface CompactInput {
|
|
10
|
+
body: Body
|
|
11
|
+
config: Config
|
|
12
|
+
state: ThreadState
|
|
13
|
+
window: number | null
|
|
14
|
+
guide: string | null
|
|
15
|
+
transcriptDir?: string | null
|
|
16
|
+
mayCompact: boolean
|
|
17
|
+
omittedBefore?: number
|
|
18
|
+
decorate?: (messages: Message[]) => Message[]
|
|
19
|
+
reduce?: CompactOptions["reduce"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CompactResult {
|
|
23
|
+
messages: Message[]
|
|
24
|
+
stateChanged: boolean
|
|
25
|
+
note: Record<string, unknown>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Rolling {
|
|
29
|
+
keep: number
|
|
30
|
+
budget: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PROTECTED_OUTPUTS = 5
|
|
9
34
|
|
|
10
35
|
function settledCut(turns: Message[][], index: number) {
|
|
11
36
|
let cut = index
|
|
@@ -13,80 +38,107 @@ function settledCut(turns: Message[][], index: number) {
|
|
|
13
38
|
return cut
|
|
14
39
|
}
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
const { turns } = splitTurns(body.messages)
|
|
18
|
-
let stateChanged = false
|
|
19
|
-
|
|
41
|
+
function resolveCut(turns: Message[][], state: ThreadState, config: Config, rollingKeep: number | undefined) {
|
|
20
42
|
let cutIndex = state.cut ? turns.findIndex((turn) => turnFingerprint(turn) === state.cut) : -1
|
|
43
|
+
let stateChanged = false
|
|
21
44
|
if (state.cut && cutIndex === -1) {
|
|
22
45
|
resetCut(state)
|
|
23
46
|
stateChanged = true
|
|
24
47
|
}
|
|
25
|
-
const rolling = config.eval?.rolling ?? (config.mode === "default" ? { keep: config.keep, budget: config.budget } : undefined)
|
|
26
48
|
if (cutIndex <= 0 && config.eval?.forceCut && turns.length >= 3) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
state.cut = turnFingerprint(turns[forced])
|
|
49
|
+
cutIndex = settledCut(turns, Math.max(1, turns.length - 1 - (rollingKeep ?? 1)))
|
|
50
|
+
state.cut = turnFingerprint(turns[cutIndex])
|
|
30
51
|
stateChanged = true
|
|
31
52
|
}
|
|
53
|
+
return { cutIndex, stateChanged }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shrinkOnce(state: ThreadState, cutIndex: number, configuredKeep: number, liveResults: () => number): string {
|
|
57
|
+
const keep = Math.min(state.keep ?? configuredKeep, cutIndex - 1)
|
|
58
|
+
if (cutIndex > 0 && keep > 0) {
|
|
59
|
+
state.keep = keep - 1
|
|
60
|
+
return `keep=${state.keep}`
|
|
61
|
+
}
|
|
62
|
+
const remaining = liveResults() - (state.cleared ?? 0) - PROTECTED_OUTPUTS
|
|
63
|
+
if (remaining <= 0) return "stuck"
|
|
64
|
+
state.cleared = (state.cleared ?? 0) + Math.ceil(remaining / 2)
|
|
65
|
+
return `cleared=${state.cleared}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dueRoll(turns: Message[][], cutIndex: number, rolling: Rolling, size: Estimator): number | null {
|
|
69
|
+
const next = settledCut(turns, turns.length - 1 - rolling.keep)
|
|
70
|
+
const backlog = size.of(turns.slice(Math.max(cutIndex, 0), Math.max(next, 0)).flat()) - size.fixed
|
|
71
|
+
return next >= 1 && next > cutIndex && backlog > rolling.budget ? next : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shrinkToTarget(over: () => boolean, step: () => string, actions: string[], rebuild: () => void) {
|
|
75
|
+
while (over()) {
|
|
76
|
+
const action = step()
|
|
77
|
+
actions.push(action)
|
|
78
|
+
if (action === "stuck") return
|
|
79
|
+
rebuild()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function compact({ body, config, state, window, guide, transcriptDir, mayCompact, omittedBefore = 0, decorate = (m) => m, reduce }: CompactInput): CompactResult {
|
|
84
|
+
const { turns } = splitTurns(body.messages)
|
|
85
|
+
const rolling = config.eval?.rolling ?? (config.mode === "default" ? { keep: config.keep, budget: config.budget } : undefined)
|
|
86
|
+
const resolved = resolveCut(turns, state, config, rolling?.keep)
|
|
87
|
+
let cutIndex = resolved.cutIndex
|
|
88
|
+
let stateChanged = resolved.stateChanged
|
|
32
89
|
|
|
33
90
|
const configuredKeep = config.keepTurns === "all" ? Number.MAX_SAFE_INTEGER : config.keepTurns
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
91
|
+
const ratio = state.ratio ?? DEFAULT_RATIO
|
|
92
|
+
const options = { guide, omittedBefore, evidence: config.eval?.evidence ?? true, reduce, capChars: config.turnCap === "off" ? undefined : config.turnCap / ratio }
|
|
93
|
+
const liveStart = (list: Message[]) =>
|
|
94
|
+
list.length -
|
|
95
|
+
splitTurns(list)
|
|
96
|
+
.turns.slice(-(turns.length - Math.max(cutIndex, 0)))
|
|
97
|
+
.flat().length
|
|
98
|
+
const compacted = () => capTurns(cutIndex > 0 ? compactMessages(body.messages, cutIndex, { ...options, keepTurns: state.keep ?? configuredKeep }) : body.messages, { ...options, keepTurns: 0 })
|
|
38
99
|
const build = () => {
|
|
39
100
|
const messages = compacted()
|
|
40
|
-
return state.cleared ? clearToolResults(messages, messages
|
|
101
|
+
return state.cleared ? clearToolResults(messages, liveStart(messages), state.cleared, clearedOutput(transcriptDir ?? null)) : messages
|
|
102
|
+
}
|
|
103
|
+
const liveResults = () => {
|
|
104
|
+
const uncleared = compacted()
|
|
105
|
+
return countToolResults(uncleared, liveStart(uncleared))
|
|
41
106
|
}
|
|
42
107
|
|
|
43
|
-
const size = estimator(body,
|
|
108
|
+
const size = estimator(body, ratio)
|
|
44
109
|
const limit = window ? config.compactAt * window : Number.POSITIVE_INFINITY
|
|
45
110
|
const target = window ? targetSize(size, window) : Number.POSITIVE_INFINITY
|
|
46
111
|
|
|
47
112
|
let messages = build()
|
|
48
|
-
let estimate = size.of(messages)
|
|
113
|
+
let estimate = size.of(decorate(messages))
|
|
49
114
|
const actions: string[] = []
|
|
50
|
-
const
|
|
115
|
+
const rebuild = () => {
|
|
116
|
+
messages = build()
|
|
117
|
+
estimate = size.of(decorate(messages))
|
|
118
|
+
}
|
|
51
119
|
const moveCut = (index: number, action: string) => {
|
|
52
120
|
cutIndex = index
|
|
53
121
|
resetCut(state)
|
|
54
122
|
state.cut = turnFingerprint(turns[index])
|
|
55
123
|
actions.push(action)
|
|
56
|
-
|
|
57
|
-
estimate = size.of(messages)
|
|
124
|
+
rebuild()
|
|
58
125
|
}
|
|
59
126
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
moveCut(next, "roll")
|
|
65
|
-
stateChanged = true
|
|
66
|
-
}
|
|
127
|
+
const roll = mayCompact && rolling ? dueRoll(turns, cutIndex, rolling, size) : null
|
|
128
|
+
if (roll !== null) {
|
|
129
|
+
moveCut(roll, "roll")
|
|
130
|
+
stateChanged = true
|
|
67
131
|
}
|
|
68
132
|
|
|
69
133
|
if (mayCompact && estimate > limit) {
|
|
134
|
+
const current = turns.length - 1
|
|
70
135
|
if (current > 0 && current > cutIndex) moveCut(current, "cut")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const unclearedLive = compacted()
|
|
78
|
-
const total = countToolResults(unclearedLive, unclearedLive.length - liveLength())
|
|
79
|
-
const remaining = total - (state.cleared ?? 0) - PROTECTED_OUTPUTS
|
|
80
|
-
if (remaining <= 0) {
|
|
81
|
-
actions.push("stuck")
|
|
82
|
-
break
|
|
83
|
-
}
|
|
84
|
-
state.cleared = (state.cleared ?? 0) + Math.ceil(remaining / 2)
|
|
85
|
-
actions.push(`cleared=${state.cleared}`)
|
|
86
|
-
}
|
|
87
|
-
messages = build()
|
|
88
|
-
estimate = size.of(messages)
|
|
89
|
-
}
|
|
136
|
+
shrinkToTarget(
|
|
137
|
+
() => estimate > target,
|
|
138
|
+
() => shrinkOnce(state, cutIndex, configuredKeep, liveResults),
|
|
139
|
+
actions,
|
|
140
|
+
rebuild,
|
|
141
|
+
)
|
|
90
142
|
stateChanged = true
|
|
91
143
|
}
|
|
92
144
|
|
|
@@ -96,8 +148,9 @@ export function compact({ body, config, state, window, guide, mayCompact, omitte
|
|
|
96
148
|
note: {
|
|
97
149
|
action: cutIndex > 0 ? "compact" : "full",
|
|
98
150
|
compacted: actions.includes("cut") || actions.includes("roll"),
|
|
99
|
-
actions: actions.length ? actions : undefined,
|
|
151
|
+
actions: actions.length > 0 ? actions : undefined,
|
|
100
152
|
cutTurn: cutIndex > 0 ? cutIndex : null,
|
|
153
|
+
omitted: cutIndex > 0 ? Math.max(1, cutIndex - (state.keep ?? configuredKeep)) - 1 : 0,
|
|
101
154
|
keep: state.keep,
|
|
102
155
|
cleared: state.cleared,
|
|
103
156
|
turns: turns.length,
|
package/src/proxy/estimate.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import type { Body, Message } from "../context/
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { Body, Message } from "../context/content"
|
|
2
|
+
|
|
3
|
+
export interface Estimator {
|
|
4
|
+
of: (messages: Message[]) => number
|
|
5
|
+
fixed: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_RATIO = 1 / 3.6
|
|
9
|
+
|
|
10
|
+
const MEDIA_CHARS = 5_800
|
|
4
11
|
|
|
5
12
|
export function sizeOf(body: Pick<Body, "system" | "tools" | "messages">): number {
|
|
6
13
|
let media = 0
|
|
7
14
|
const json = JSON.stringify({ system: body.system, tools: body.tools, messages: body.messages }, (key, value) => {
|
|
8
|
-
if (key
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
return value
|
|
15
|
+
if (key !== "data" || typeof value !== "string" || value.length <= 2_000) return value
|
|
16
|
+
media += 1
|
|
17
|
+
return ""
|
|
13
18
|
})
|
|
14
19
|
return json.length + media * MEDIA_CHARS
|
|
15
20
|
}
|
|
@@ -22,5 +27,5 @@ export function estimator(body: Body, ratio: number): Estimator {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function targetSize(size: Estimator, window: number): number {
|
|
25
|
-
return size.fixed +
|
|
30
|
+
return size.fixed + 0.3 * Math.max(0, window - size.fixed)
|
|
26
31
|
}
|
package/src/proxy/fingerprint.ts
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return hasher.digest("hex").slice(0, FINGERPRINT_LENGTH)
|
|
1
|
+
import { type Body, blocks, type Message, shortHash, withoutCacheControl } from "../context/content"
|
|
2
|
+
|
|
3
|
+
export interface ContextFingerprint {
|
|
4
|
+
system: string
|
|
5
|
+
tools: string
|
|
6
|
+
skills: string[]
|
|
7
|
+
given?: ContextFingerprint
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
export const isInjected = (text: unknown) => /^<(?:recalled-context|handoff)[\s>]/.test(String(text ?? ""))
|
|
11
|
+
|
|
12
|
+
export function messageFingerprint(message: Message) {
|
|
13
|
+
const own = blocks(message)
|
|
14
|
+
.filter((b) => !(b.type === "text" && isInjected(b.text)))
|
|
15
|
+
.map(withoutCacheControl)
|
|
16
|
+
return shortHash(JSON.stringify(own), 24)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hash = (value: unknown) =>
|
|
20
|
+
shortHash(
|
|
21
|
+
JSON.stringify(value ?? null, (key, inner) => (key === "cache_control" ? undefined : inner)),
|
|
22
|
+
12,
|
|
23
|
+
)
|
|
24
|
+
|
|
11
25
|
export function contextFingerprint(body: Body): ContextFingerprint {
|
|
12
26
|
const skills = new Set<string>()
|
|
13
|
-
for (const match of JSON.stringify(body.messages ?? []).matchAll(
|
|
27
|
+
for (const match of JSON.stringify(body.messages ?? []).matchAll(/Base directory for this skill: ([^\s"\\]+)/g)) skills.add(match[1].split("/").at(-1) ?? match[1])
|
|
14
28
|
return { system: hash(body.system), tools: hash(body.tools), skills: [...skills].sort() }
|
|
15
29
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { statSync } from "node:fs"
|
|
2
|
+
import { type Config, claudeConfigDirs } from "../config"
|
|
3
|
+
import { blocks, type Message } from "../context/content"
|
|
4
|
+
import { FOLD_CAP_CHARS, foldedView, sourceChunks } from "../context/fold"
|
|
5
|
+
import { isPrompt } from "../context/fold/turns"
|
|
6
|
+
import { handoffBlock } from "../context/prompts"
|
|
7
|
+
import type { RecallChunk } from "../context/recall"
|
|
8
|
+
import { findSessionByPrefix } from "../transcript/locate"
|
|
9
|
+
import { sessionMessages } from "../transcript/messages"
|
|
10
|
+
import { isInjected, messageFingerprint } from "./fingerprint"
|
|
11
|
+
import { titleOf } from "./sessions"
|
|
12
|
+
import type { Handoff, ThreadState } from "./state"
|
|
13
|
+
|
|
14
|
+
const sourceCache = new Map<string, { mtime: number; chunks: RecallChunk[] }>()
|
|
15
|
+
|
|
16
|
+
export const continueToken = (id: string) => `{ee:continue:${id}}`
|
|
17
|
+
|
|
18
|
+
export function continueIdOf(message: Message): string | null {
|
|
19
|
+
if (!isPrompt(message)) return null
|
|
20
|
+
const own = blocks(message)
|
|
21
|
+
.filter((b) => b.type === "text" && !isInjected(b.text))
|
|
22
|
+
.map((b) => String(b.text ?? ""))
|
|
23
|
+
.join("\n")
|
|
24
|
+
return own.match(/\{ee:continue:([A-Za-z0-9-]{8,})\}/)?.[1] ?? null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function activeHandoffs(handoffs: Record<string, Handoff | null> = {}): Record<string, Handoff> {
|
|
28
|
+
return Object.fromEntries(Object.entries(handoffs).filter((entry): entry is [string, Handoff] => Boolean(entry[1])))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function attachHandoffs(messages: Message[], config: Config, sessionId: string | null, state: ThreadState): boolean {
|
|
32
|
+
let changed = false
|
|
33
|
+
for (const message of messages) {
|
|
34
|
+
const id = continueIdOf(message)
|
|
35
|
+
if (!id) continue
|
|
36
|
+
const key = messageFingerprint(message)
|
|
37
|
+
if (state.handoffs && key in state.handoffs) continue
|
|
38
|
+
state.handoffs ??= {}
|
|
39
|
+
const file = findSessionByPrefix(id, claudeConfigDirs(config), sessionId ?? undefined)
|
|
40
|
+
const already = Object.values(state.handoffs).some((h) => h?.source === file)
|
|
41
|
+
state.handoffs[key] = file && !already ? { label: titleOf(file) ?? id, source: file, text: foldedView(sessionMessages(file), FOLD_CAP_CHARS) } : null
|
|
42
|
+
changed = true
|
|
43
|
+
}
|
|
44
|
+
return changed
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function withHandoffs(messages: Message[], handoffs: Record<string, Handoff>): Message[] {
|
|
48
|
+
if (Object.keys(handoffs).length === 0) return messages
|
|
49
|
+
return messages.map((message) => {
|
|
50
|
+
if (!isPrompt(message)) return message
|
|
51
|
+
const handoff = handoffs[messageFingerprint(message)]
|
|
52
|
+
return handoff ? { ...message, content: [{ type: "text", text: handoffBlock(handoff.label, handoff.text) }, ...blocks(message)] } : message
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function handoffChunks(handoffs: Record<string, Handoff>): RecallChunk[] {
|
|
57
|
+
const sources = new Map<string, string>()
|
|
58
|
+
for (const handoff of Object.values(handoffs)) if (!sources.has(handoff.source)) sources.set(handoff.source, handoff.label)
|
|
59
|
+
return [...sources].flatMap(([source, label]) => {
|
|
60
|
+
let mtime: number
|
|
61
|
+
try {
|
|
62
|
+
mtime = statSync(source).mtimeMs
|
|
63
|
+
} catch {
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
const cached = sourceCache.get(source)
|
|
67
|
+
const chunks = cached?.mtime === mtime ? cached.chunks : sourceChunks(sessionMessages(source), label, true)
|
|
68
|
+
sourceCache.set(source, { mtime, chunks })
|
|
69
|
+
return chunks
|
|
70
|
+
})
|
|
71
|
+
}
|
package/src/proxy/log.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs"
|
|
2
|
-
import { EUNOE_DIR, paths } from "../config/
|
|
2
|
+
import { EUNOE_DIR, paths } from "../config/paths"
|
|
3
3
|
|
|
4
4
|
export function logRequest(record: Record<string, unknown>) {
|
|
5
5
|
try {
|
|
6
6
|
mkdirSync(EUNOE_DIR, { recursive: true })
|
|
7
|
-
appendFileSync(paths.requests, JSON.stringify({ ts: new Date().toISOString(), ...record })
|
|
7
|
+
appendFileSync(paths.requests, `${JSON.stringify({ ts: new Date().toISOString(), ...record })}\n`)
|
|
8
8
|
} catch {}
|
|
9
9
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Body, blocks, resultText } from "../context/content"
|
|
2
|
+
import { livePrompt, type RecallChunk, type Reranker, recallChunks, recallFor, recallForWith, withRecall } from "../context/recall"
|
|
3
|
+
import { sessionMessages } from "../transcript/messages"
|
|
4
|
+
import { subagentChunks } from "../transcript/subagents"
|
|
5
|
+
import { messageFingerprint } from "./fingerprint"
|
|
6
|
+
import { activeHandoffs, handoffChunks } from "./handoff"
|
|
7
|
+
import type { Rewrite, RewriteInput } from "./rewrite"
|
|
8
|
+
import { readState, saveState } from "./state"
|
|
9
|
+
|
|
10
|
+
export const wantsRecall = (input: RewriteInput) => (input.config.eval?.recall ?? true) && input.config.mode !== "off"
|
|
11
|
+
|
|
12
|
+
export const activeRecalls = (recalls: Record<string, string> = {}) => Object.fromEntries(Object.entries(recalls).filter(([, text]) => text))
|
|
13
|
+
|
|
14
|
+
const touched = (input: RewriteInput, result: Rewrite) => result.threadKey !== null && !(result.body === input.body && result.note.action === "pass")
|
|
15
|
+
|
|
16
|
+
function droppedChunks(input: RewriteInput, result: Rewrite): RecallChunk[] {
|
|
17
|
+
const transcript = input.transcript?.jsonl ?? null
|
|
18
|
+
const kept = new Map<string, string>()
|
|
19
|
+
for (const message of result.body.messages) for (const b of blocks(message)) if (b.type === "tool_result") kept.set(b.tool_use_id, resultText(b))
|
|
20
|
+
const own = recallChunks(input.body.messages, kept)
|
|
21
|
+
const session = own.length > 0 || !transcript ? own : recallChunks([...sessionMessages(transcript), { role: "user", content: "" }], kept)
|
|
22
|
+
const withSubagents = transcript ? [...session, ...subagentChunks(transcript)] : session
|
|
23
|
+
const handoffs = result.threadKey ? activeHandoffs(readState(result.threadKey).handoffs) : {}
|
|
24
|
+
return [...handoffChunks(handoffs), ...withSubagents]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function recallTarget(original: Body, result: Rewrite): { key: string; text: string } | null {
|
|
28
|
+
const live = livePrompt(original.messages)
|
|
29
|
+
return live && result.threadKey ? { key: messageFingerprint(original.messages[live.index]), text: live.text } : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function applyRecall(input: RewriteInput, result: Rewrite): Rewrite {
|
|
33
|
+
if (!touched(input, result) || !result.threadKey) return result
|
|
34
|
+
const state = readState(result.threadKey)
|
|
35
|
+
state.recalls ??= {}
|
|
36
|
+
const target = recallTarget(input.body, result)
|
|
37
|
+
let note = result.note
|
|
38
|
+
if (target && !(target.key in state.recalls)) {
|
|
39
|
+
state.recalls[target.key] = recallFor(target.text, droppedChunks(input, result), input.config.eval?.recallOptions) ?? ""
|
|
40
|
+
saveState(result.threadKey, state)
|
|
41
|
+
note = { ...note, recalled: state.recalls[target.key].length }
|
|
42
|
+
}
|
|
43
|
+
const messages = withRecall(result.body.messages, messageFingerprint, activeRecalls(state.recalls))
|
|
44
|
+
return messages === result.body.messages ? { ...result, note } : { ...result, body: { ...result.body, messages }, note }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function prefetchFor(input: RewriteInput, result: Rewrite, rerank: Reranker): Promise<number | null> {
|
|
48
|
+
if (!touched(input, result) || !result.threadKey) return null
|
|
49
|
+
const target = recallTarget(input.body, result)
|
|
50
|
+
if (!target) return null
|
|
51
|
+
const state = readState(result.threadKey)
|
|
52
|
+
state.recalls ??= {}
|
|
53
|
+
if (target.key in state.recalls) return null
|
|
54
|
+
const options = { candidates: input.config.eval?.rerank?.candidates, ...input.config.eval?.recallOptions }
|
|
55
|
+
state.recalls[target.key] = (await recallForWith(target.text, droppedChunks(input, result), rerank, options)) ?? ""
|
|
56
|
+
saveState(result.threadKey, state)
|
|
57
|
+
return state.recalls[target.key].length
|
|
58
|
+
}
|