@100xprompt/brain 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// `brain memory ingest <session-id> | search <query> [--project p] [--limit n] | status | forget [--project p] [--source id]`.
|
|
2
|
+
// Plain output, counts first. `resolve` turns a session id into a Session (the coordinator wires the real readers).
|
|
3
|
+
import { realpathSync } from "node:fs"
|
|
4
|
+
import type { Session } from "../session/types.ts"
|
|
5
|
+
import { gitInfo } from "../fsutil.ts"
|
|
6
|
+
import { distill } from "./distill.ts"
|
|
7
|
+
import { projectKey } from "./project.ts"
|
|
8
|
+
import { redact } from "./redact.ts"
|
|
9
|
+
import { openMemory, storeBytes } from "./store.ts"
|
|
10
|
+
import type { ProjectKey } from "./types.ts"
|
|
11
|
+
|
|
12
|
+
function flag(args: string[], name: string): string | undefined {
|
|
13
|
+
const i = args.indexOf(`--${name}`)
|
|
14
|
+
return i >= 0 ? args[i + 1] : undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function positional(args: string[]): string[] {
|
|
18
|
+
const out: string[] = []
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
if (args[i]!.startsWith("--")) i++
|
|
21
|
+
else out.push(args[i]!)
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The project to act on: `--project` as an id or a label of a stored project, else the current folder's. */
|
|
27
|
+
async function pickProject(args: string[], mem: ReturnType<typeof openMemory>): Promise<ProjectKey | { error: string }> {
|
|
28
|
+
const wanted = flag(args, "project")
|
|
29
|
+
if (!wanted) {
|
|
30
|
+
const cwd = (() => {
|
|
31
|
+
try {
|
|
32
|
+
return realpathSync(process.cwd())
|
|
33
|
+
} catch {
|
|
34
|
+
return process.cwd()
|
|
35
|
+
}
|
|
36
|
+
})()
|
|
37
|
+
return projectKey({ cwd, ...gitInfo(cwd) })
|
|
38
|
+
}
|
|
39
|
+
const known = await mem.projects()
|
|
40
|
+
const hit = known.find((p) => p.id === wanted.toLowerCase() || p.id === wanted) ?? known.filter((p) => p.label === wanted)[0]
|
|
41
|
+
if (hit) return { id: hit.id, label: hit.label }
|
|
42
|
+
const labelMatches = known.filter((p) => p.label === wanted)
|
|
43
|
+
if (labelMatches.length > 1) return { error: `"${wanted}" matches several projects; use the id` }
|
|
44
|
+
return { error: `no stored project "${wanted}" (known: ${known.map((p) => p.label).join(", ") || "none"})` }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function memoryCommand(args: string[], home: string, resolve: (id: string) => Promise<Session>, out: (line: string) => void = console.log): Promise<number> {
|
|
48
|
+
const [sub, ...rest] = args
|
|
49
|
+
const mem = openMemory(home)
|
|
50
|
+
try {
|
|
51
|
+
if (sub === "status" || !sub) {
|
|
52
|
+
const s = await mem.status()
|
|
53
|
+
if (!s.available) {
|
|
54
|
+
out("memory: not available")
|
|
55
|
+
out(`why: ${s.why}`)
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
const c = await mem.counts()
|
|
59
|
+
out(`memory: available, ${c.total} items in ${(await mem.projects()).length} projects`)
|
|
60
|
+
out(`store: ${s.store} (${(storeBytes(home) / 1024).toFixed(0)} KB)`)
|
|
61
|
+
out(`kinds: ${Object.entries(c.byKind).map(([k, n]) => `${k} ${n}`).join(", ") || "none"}`)
|
|
62
|
+
for (const p of await mem.projects()) out(` ${p.label} ${p.items} items (${p.id})`)
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (sub === "ingest") {
|
|
67
|
+
const id = positional(rest)[0]
|
|
68
|
+
if (!id) return out("usage: brain memory ingest <session-id>"), 2
|
|
69
|
+
let session: Session
|
|
70
|
+
try {
|
|
71
|
+
session = await resolve(id)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return out(`cannot read session ${id}: ${(e as Error).message}`), 1
|
|
74
|
+
}
|
|
75
|
+
const s = await mem.status()
|
|
76
|
+
if (!s.available) return out(`memory is not available: ${s.why}`), 1
|
|
77
|
+
const items = distill(session)
|
|
78
|
+
const project = projectKey(session.header.project)
|
|
79
|
+
const { stored } = await mem.ingest(project, items)
|
|
80
|
+
const byKind: Record<string, number> = {}
|
|
81
|
+
for (const i of items) byKind[i.kind] = (byKind[i.kind] ?? 0) + 1
|
|
82
|
+
out(`stored ${stored} items from ${session.header.id}`)
|
|
83
|
+
out(`kinds: ${Object.entries(byKind).map(([k, n]) => `${k} ${n}`).join(", ") || "none"}`)
|
|
84
|
+
out(`project: ${project.label} (${project.id})`)
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (sub === "search") {
|
|
89
|
+
const query = positional(rest).join(" ").trim()
|
|
90
|
+
if (!query) return out("usage: brain memory search <query> [--project p] [--limit n]"), 2
|
|
91
|
+
const s = await mem.status()
|
|
92
|
+
if (!s.available) return out(`memory is not available: ${s.why}`), 1
|
|
93
|
+
const project = await pickProject(rest, mem)
|
|
94
|
+
if ("error" in project) return out(project.error), 1
|
|
95
|
+
const limit = Number(flag(rest, "limit")) || 8
|
|
96
|
+
const hits = await mem.recall(project, query, { limit })
|
|
97
|
+
out(`${hits.length} items for "${redact(query)}" in ${project.label}`)
|
|
98
|
+
for (const h of hits) out(`[${h.kind ?? "item"}] ${h.score.toFixed(3)} ${h.text}${h.source ? ` (from ${h.source})` : ""}`)
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (sub === "forget") {
|
|
103
|
+
const s = await mem.status()
|
|
104
|
+
if (!s.available) return out(`memory is not available: ${s.why}`), 1
|
|
105
|
+
const project = await pickProject(rest, mem)
|
|
106
|
+
if ("error" in project) return out(project.error), 1
|
|
107
|
+
const n = await mem.forget(project, { source: flag(rest, "source") })
|
|
108
|
+
out(`forgot ${n} items in ${project.label}${flag(rest, "source") ? ` from ${flag(rest, "source")}` : ""}`)
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
out("usage: brain memory ingest <session-id> | search <query> [--project p] [--limit n] | status | forget [--project p] [--source id]")
|
|
113
|
+
return 2
|
|
114
|
+
} finally {
|
|
115
|
+
mem.close()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Zero-model distillation of a portable session into small memory items: what was decided, what must not be done, which
|
|
2
|
+
// approaches failed, which files changed and why, which commands are reusable, how it ended. Precision over recall: a wrong
|
|
3
|
+
// memory costs tokens in every future session, a missed one costs a re-discovery. Everything passes through `redact`.
|
|
4
|
+
import { createHash } from "node:crypto"
|
|
5
|
+
import { isAbsolute, relative } from "node:path"
|
|
6
|
+
import type { Session, Turn } from "../session/types.ts"
|
|
7
|
+
import { isInjected, withoutInjected } from "../session/util.ts"
|
|
8
|
+
import { projectKey } from "./project.ts"
|
|
9
|
+
import { redact } from "./redact.ts"
|
|
10
|
+
import type { MemoryItem, MemoryKind } from "./types.ts"
|
|
11
|
+
|
|
12
|
+
export interface DistillOptions {
|
|
13
|
+
/** Cap on items per session; the best are kept. Default 40. */
|
|
14
|
+
maxItems?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Scored = MemoryItem & { score: number }
|
|
18
|
+
|
|
19
|
+
const sha = (s: string, n = 12): string => createHash("sha1").update(s).digest("hex").slice(0, n)
|
|
20
|
+
const oneLine = (s: string): string => s.replace(/\s+/g, " ").trim()
|
|
21
|
+
|
|
22
|
+
/** Whitespace-collapsed text clipped to `n` characters at a word edge, with an ellipsis. */
|
|
23
|
+
export function clipText(s: string, n: number): string {
|
|
24
|
+
const t = oneLine(s)
|
|
25
|
+
if (t.length <= n) return t
|
|
26
|
+
const cut = t.slice(0, n - 1)
|
|
27
|
+
const sp = cut.lastIndexOf(" ")
|
|
28
|
+
return `${(sp > n * 0.6 ? cut.slice(0, sp) : cut).trimEnd()}…`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---- user instructions --------------------------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const STRONG = /\b(always|never|from now on|going forward|in the future|we use|we always|we never|we decided|we('re| are) (using|going with)|the rule is|the convention is|prefer|i prefer|i like|i don'?t like|i hate|whenever|every time|should always|should never|must never|must not|mustn'?t|don'?t ever|under no circumstances|no matter what|default to)\b/i
|
|
34
|
+
const MEDIUM = /\b(must|don'?t|do not|dont|make sure|ensure|instead of|instead|only use|only|avoid|stop (?:using|doing|adding|writing)|no need to|keep (?:it|them|the|using)|rather than|not (?:use|using)|use [\w./-]+ not [\w./-]+)\b/i
|
|
35
|
+
const ONE_OFF = /\b(right now|this time|for now|just this|for this (?:task|one|case|session)|today|tonight|at the moment|in this (?:file|case)|the above|the following|above error|this error|that error|this bug)\b/i
|
|
36
|
+
const QUESTION_START = /^(?:what|why|how|when|where|who|which|can|could|would|should|is|are|do|does|did|will|have|has|any idea|is there|are there)\b/i
|
|
37
|
+
const FILLER_START = /^(?:and|also|but|so|ok|okay|yes|yeah|yep|no|now|well|then|please|pls|plz|btw|oh|hey|hi|hmm|alright|right|cool|nice|great)[\s,:.!-]+/i
|
|
38
|
+
const DEICTIC_START = /^(?:it|this|that|these|those|they|them|he|she|its|there|here|the same|same|such)\b/i
|
|
39
|
+
|
|
40
|
+
function splitInstructionCandidates(text: string): string[] {
|
|
41
|
+
const noCode = text.replace(/```[\s\S]*?(?:```|$)/g, " ").replace(/`([^`\n]{1,80})`/g, "$1")
|
|
42
|
+
const out: string[] = []
|
|
43
|
+
for (const line of noCode.split(/\n+/)) {
|
|
44
|
+
const l = line.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").trim()
|
|
45
|
+
if (!l) continue
|
|
46
|
+
if (l.length > 600) continue // a pasted paragraph, log line or stack trace, not an instruction
|
|
47
|
+
if (/^\s*(?:at |\{|\[|<|\/\/|#|\$ |>>|[A-Za-z]+Error:)/.test(l) && !/\s/.test(l.slice(0, 3))) continue
|
|
48
|
+
for (const s of l.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)) {
|
|
49
|
+
const t = s.trim()
|
|
50
|
+
if (t) out.push(t)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function classify(sentence: string): { kind: MemoryKind; strong: boolean } | undefined {
|
|
57
|
+
if (sentence.endsWith("?")) return undefined
|
|
58
|
+
let s = sentence
|
|
59
|
+
for (let i = 0; i < 2; i++) s = s.replace(FILLER_START, "")
|
|
60
|
+
if (s.length < 20 || s.split(/\s+/).length < 4) return undefined
|
|
61
|
+
if (QUESTION_START.test(s) && !/^(?:do not|don'?t|do it)\b/i.test(s)) return undefined
|
|
62
|
+
if (DEICTIC_START.test(s)) return undefined
|
|
63
|
+
if (/^(?:thanks|thank you|sure|great|perfect|good|looks good|lgtm)\b/i.test(s)) return undefined
|
|
64
|
+
const strong = STRONG.test(s)
|
|
65
|
+
const medium = MEDIUM.test(s)
|
|
66
|
+
if (!strong && !medium) return undefined
|
|
67
|
+
if (!strong && ONE_OFF.test(s)) return undefined
|
|
68
|
+
// medium markers inside an imperative about "this/it/the failing test" are one-offs: require a durable-sounding object
|
|
69
|
+
if (!strong && /\b(?:make sure|ensure)\b/i.test(s) && !/\b(?:always|every|all|each|any)\b/i.test(s) && /\b(?:this|it|that|the (?:tests?|build|app|page|server|thing))\b/i.test(s)) return undefined
|
|
70
|
+
const kind: MemoryKind = /\b(never|must not|mustn'?t|don'?t|do not|dont|avoid|stop|no need to|only|no matter what|under no circumstances)\b/i.test(s)
|
|
71
|
+
? "constraint"
|
|
72
|
+
: /\b(prefer|rather than|i like|i don'?t like|i hate|instead|default to)\b/i.test(s)
|
|
73
|
+
? "preference"
|
|
74
|
+
: "decision"
|
|
75
|
+
return { kind, strong }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function instructions(session: Session, at: (t: Turn) => string | undefined): Scored[] {
|
|
79
|
+
const out: Scored[] = []
|
|
80
|
+
const seen = new Set<string>()
|
|
81
|
+
const source = session.header.id
|
|
82
|
+
for (const t of session.turns) {
|
|
83
|
+
if (t.kind !== "user") continue
|
|
84
|
+
if (isInjected(t.text)) continue
|
|
85
|
+
const text = withoutInjected(t.text)
|
|
86
|
+
if (/^This session is being continued from a previous conversation/i.test(text)) continue
|
|
87
|
+
if (text.length > 4000) continue // a pasted blob, not a message
|
|
88
|
+
for (const sentence of splitInstructionCandidates(text)) {
|
|
89
|
+
const c = classify(sentence)
|
|
90
|
+
if (!c) continue
|
|
91
|
+
const clean = redact(clipText(sentence.replace(FILLER_START, ""), 300))
|
|
92
|
+
const key = clean.toLowerCase()
|
|
93
|
+
if (seen.has(key)) continue
|
|
94
|
+
seen.add(key)
|
|
95
|
+
out.push({ id: `${c.kind}-${sha(source + key)}`, kind: c.kind, name: clipText(clean, 70), text: clean, source, occurredAt: at(t), score: c.strong ? 88 : 72 })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- tool turns ---------------------------------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
104
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
105
|
+
|
|
106
|
+
function inputObject(input: unknown): Record<string, any> {
|
|
107
|
+
if (input && typeof input === "object") return input as Record<string, any>
|
|
108
|
+
if (typeof input === "string") {
|
|
109
|
+
try {
|
|
110
|
+
const v = JSON.parse(input)
|
|
111
|
+
if (v && typeof v === "object") return v
|
|
112
|
+
} catch {}
|
|
113
|
+
return { command: input }
|
|
114
|
+
}
|
|
115
|
+
return {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function commandOf(call: Call): string | undefined {
|
|
119
|
+
const o = inputObject(call.input)
|
|
120
|
+
const c = o.command ?? o.cmd ?? o.script
|
|
121
|
+
if (typeof c === "string") return c
|
|
122
|
+
if (Array.isArray(c)) return c.map(String).join(" ")
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pathOf(call: Call): string | undefined {
|
|
127
|
+
const o = inputObject(call.input)
|
|
128
|
+
for (const k of ["file_path", "path", "filePath", "file", "filename", "target_file", "notebook_path"]) if (typeof o[k] === "string" && o[k]) return o[k] as string
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ERROR_LINE = /\b(error|fail(?:ed|ure)?|not found|denied|cannot|can't|no such|exception|panic|fatal|unable|invalid|unexpected|refused|timed? ?out|ENOENT|EACCES|ERR_)\b/i
|
|
133
|
+
const REJECTION = /(user (?:doesn'?t|does not|didn'?t) want|was rejected|rejected|interrupted by user|request was aborted|tool use was|permission to use|has not been read yet|hook (?:blocked|denied)|aborted)/i
|
|
134
|
+
|
|
135
|
+
function firstErrorLine(output: string): string {
|
|
136
|
+
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
137
|
+
const hit = lines.find((l) => ERROR_LINE.test(l) && l.length < 400) ?? lines[0] ?? ""
|
|
138
|
+
return clipText(hit, 160)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `cd x && FOO=1 cmd args` -> the part that says what the command is. */
|
|
142
|
+
function core(command: string): string {
|
|
143
|
+
const first = command.split("\n").find((l) => l.trim() && !l.trim().startsWith("#")) ?? ""
|
|
144
|
+
return first.replace(/^\s*(?:cd\s+[^&;]+(?:&&|;)\s*)+/, "").replace(/^\s*(?:[A-Z_][A-Z0-9_]*=\S*\s+)+/, "").trim()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const failureKey = (c: string): string => core(c).split(/\s+/).slice(0, 2).join(" ")
|
|
148
|
+
|
|
149
|
+
function failures(session: Session, at: (t: Turn) => string | undefined): Scored[] {
|
|
150
|
+
const calls = new Map<string, { call: Call; index: number }>()
|
|
151
|
+
session.turns.forEach((t, i) => t.kind === "tool_call" && calls.set(t.id, { call: t, index: i }))
|
|
152
|
+
const attempts: Array<{ command: string; ok: boolean; index: number }> = []
|
|
153
|
+
const failed: Array<{ command: string; error: string; index: number; at?: string; kind: string }> = []
|
|
154
|
+
session.turns.forEach((t, i) => {
|
|
155
|
+
if (t.kind !== "tool_result") return
|
|
156
|
+
const entry = calls.get(t.callId)
|
|
157
|
+
if (!entry) return
|
|
158
|
+
const { call } = entry
|
|
159
|
+
if (!["shell", "edit", "write", "mcp", "other", "web"].includes(call.toolKind)) return
|
|
160
|
+
const command = commandOf(call) ?? `${call.name}${pathOf(call) ? ` ${pathOf(call)}` : ""}`
|
|
161
|
+
attempts.push({ command, ok: t.ok, index: i })
|
|
162
|
+
if (t.ok || REJECTION.test(t.output.slice(0, 400)) || !t.output.trim()) return
|
|
163
|
+
failed.push({ command, error: firstErrorLine(t.output), index: i, at: t.at, kind: call.toolKind })
|
|
164
|
+
})
|
|
165
|
+
const source = session.header.id
|
|
166
|
+
const out: Scored[] = []
|
|
167
|
+
const seen = new Set<string>()
|
|
168
|
+
for (const f of failed) {
|
|
169
|
+
const key = failureKey(f.command)
|
|
170
|
+
const fixed = attempts.find((a) => a.ok && a.index > f.index && failureKey(a.command) === key)
|
|
171
|
+
const clean = redact(clipText(core(f.command) || f.command, 140))
|
|
172
|
+
if (!clean || seen.has(`${clean}|${f.error}`)) continue
|
|
173
|
+
seen.add(`${clean}|${f.error}`)
|
|
174
|
+
const fix = fixed && core(fixed.command) !== core(f.command) ? `; worked after changing it to \`${redact(clipText(core(fixed.command), 100))}\`` : fixed ? "; worked on retry" : ""
|
|
175
|
+
const text = `Tried \`${clean}\`: failed with "${redact(f.error)}"${fix}`
|
|
176
|
+
out.push({ id: `failed-${sha(source + f.command + f.error)}`, kind: "failed", name: clipText(`failed: ${clean}`, 70), text, source, occurredAt: f.at, score: fixed ? 60 : 30 })
|
|
177
|
+
}
|
|
178
|
+
return out.sort((a, b) => b.score - a.score).slice(0, 6)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const CMD_KEYWORD = /\b(test|tests|build|lint|typecheck|tsc|check|dev|start|serve|deploy|format|fmt|compile|bench|migrate)\b/i
|
|
182
|
+
const CMD_TOOL = /^(?:bun|bunx|npm|npx|pnpm|yarn|cargo|go|pytest|python3?|make|tsc|eslint|prettier|ruff|gradle|\.\/gradlew|mvn|dotnet|deno|vitest|jest|gcloud|docker|terraform|swift|xcodebuild|next|vite|node|uv|poetry|rake|bundle|mix|flutter|gh)\b/
|
|
183
|
+
|
|
184
|
+
function commandLabel(c: string): string {
|
|
185
|
+
if (/\b(test|tests|pytest|vitest|jest)\b/i.test(c)) return "to run the tests"
|
|
186
|
+
if (/\b(typecheck|tsc)\b/i.test(c)) return "to typecheck"
|
|
187
|
+
if (/\b(lint|eslint|ruff|check)\b/i.test(c)) return "to lint"
|
|
188
|
+
if (/\b(deploy)\b/i.test(c)) return "to deploy"
|
|
189
|
+
if (/\b(build|compile)\b/i.test(c)) return "to build"
|
|
190
|
+
if (/\b(format|fmt|prettier)\b/i.test(c)) return "to format"
|
|
191
|
+
if (/\b(dev|start|serve|run)\b/i.test(c)) return "to run the app"
|
|
192
|
+
if (/\bbench\b/i.test(c)) return "to benchmark"
|
|
193
|
+
return `to run ${c.split(/\s+/).slice(0, 2).join(" ")}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function commands(session: Session, at: (t: Turn) => string | undefined): Scored[] {
|
|
197
|
+
const calls = new Map<string, Call>()
|
|
198
|
+
const stats = new Map<string, { n: number; at?: string; command: string }>()
|
|
199
|
+
for (const t of session.turns) {
|
|
200
|
+
if (t.kind === "tool_call") calls.set(t.id, t)
|
|
201
|
+
if (t.kind !== "tool_result" || !t.ok) continue
|
|
202
|
+
const call = calls.get(t.callId)
|
|
203
|
+
if (!call || call.toolKind !== "shell") continue
|
|
204
|
+
const raw = commandOf(call)
|
|
205
|
+
if (!raw || /<<|\|\s*(?:sh|bash)\b/.test(raw)) continue
|
|
206
|
+
const c = core(raw)
|
|
207
|
+
if (c.length < 4 || c.length > 200 || !CMD_TOOL.test(c) || !CMD_KEYWORD.test(c.split(/\s+/).slice(0, 4).join(" "))) continue
|
|
208
|
+
if (/^(?:gh|docker|gcloud)\b/.test(c) && !/\b(deploy|build|test)\b/.test(c)) continue
|
|
209
|
+
const shown = redact(clipText(raw.split("\n")[0]!.replace(/^\s*#.*$/, ""), 160))
|
|
210
|
+
if (shown !== clipText(raw.split("\n")[0]!, 160)) continue // contained a secret: never keep it
|
|
211
|
+
const cur = stats.get(c) ?? { n: 0, command: shown }
|
|
212
|
+
cur.n++
|
|
213
|
+
cur.at = at(t)
|
|
214
|
+
stats.set(c, cur)
|
|
215
|
+
}
|
|
216
|
+
const source = session.header.id
|
|
217
|
+
return [...stats.values()]
|
|
218
|
+
.sort((a, b) => b.n - a.n)
|
|
219
|
+
.slice(0, 8)
|
|
220
|
+
.map((s) => {
|
|
221
|
+
const label = commandLabel(s.command)
|
|
222
|
+
return {
|
|
223
|
+
id: `command-${sha(source + s.command)}`,
|
|
224
|
+
kind: "command" as const,
|
|
225
|
+
name: label,
|
|
226
|
+
text: `${label[0]!.toUpperCase()}${label.slice(1)}: \`${s.command}\``,
|
|
227
|
+
source,
|
|
228
|
+
occurredAt: s.at,
|
|
229
|
+
procedures: [{ trigger: label, action: `\`${s.command}\`` }],
|
|
230
|
+
score: 55 + Math.min(s.n, 5),
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const SKIP_PATH = /(?:^|\/)(?:node_modules|\.git|dist|build|\.next|coverage|\.brain)\/|(?:package-lock\.json|bun\.lockb?|yarn\.lock|pnpm-lock\.yaml|\.DS_Store)$/
|
|
236
|
+
|
|
237
|
+
function files(session: Session, at: (t: Turn) => string | undefined): Scored[] {
|
|
238
|
+
const cwd = session.header.project.cwd
|
|
239
|
+
const rel = (p: string): string => {
|
|
240
|
+
if (cwd && isAbsolute(p)) {
|
|
241
|
+
const r = relative(cwd, p)
|
|
242
|
+
if (r && !r.startsWith("..")) return r
|
|
243
|
+
}
|
|
244
|
+
return p
|
|
245
|
+
}
|
|
246
|
+
const project = projectKey(session.header.project)
|
|
247
|
+
const byPath = new Map<string, { keys: Set<string>; created: boolean; why?: string; plus: number; minus: number; first?: string; at?: string; written: number }>()
|
|
248
|
+
const touch = (path: string, key: string, t: Turn, extra: { created?: boolean; patch?: string; written?: number }, index: number) => {
|
|
249
|
+
if (SKIP_PATH.test(path)) return
|
|
250
|
+
const p = rel(path)
|
|
251
|
+
const cur = byPath.get(p) ?? { keys: new Set<string>(), created: false, plus: 0, minus: 0, written: 0 }
|
|
252
|
+
if (cur.keys.has(key)) return
|
|
253
|
+
cur.keys.add(key)
|
|
254
|
+
if (extra.created) cur.created = true
|
|
255
|
+
if (extra.patch) {
|
|
256
|
+
for (const l of extra.patch.split("\n")) {
|
|
257
|
+
if (l.startsWith("+") && !l.startsWith("+++")) {
|
|
258
|
+
cur.plus++
|
|
259
|
+
if (!cur.first && l.slice(1).trim().length > 6) cur.first = l.slice(1).trim()
|
|
260
|
+
} else if (l.startsWith("-") && !l.startsWith("---")) cur.minus++
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (extra.written) cur.written += extra.written
|
|
264
|
+
const why = nearbyWords(session.turns, index)
|
|
265
|
+
if (why) cur.why = why
|
|
266
|
+
cur.at = at(t)
|
|
267
|
+
byPath.set(p, cur)
|
|
268
|
+
}
|
|
269
|
+
session.turns.forEach((t, i) => {
|
|
270
|
+
if (t.kind === "file_edit") touch(t.path, t.callId ?? `edit${i}`, t, { patch: t.patch }, i)
|
|
271
|
+
else if (t.kind === "tool_call" && (t.toolKind === "edit" || t.toolKind === "write")) {
|
|
272
|
+
const p = pathOf(t)
|
|
273
|
+
if (!p) return
|
|
274
|
+
const o = inputObject(t.input)
|
|
275
|
+
const body = typeof o.content === "string" ? o.content : typeof o.file_text === "string" ? o.file_text : typeof o.new_string === "string" ? o.new_string : typeof o.patch === "string" ? o.patch : undefined
|
|
276
|
+
touch(p, t.id, t, { created: t.toolKind === "write", written: body?.length, patch: typeof o.patch === "string" ? o.patch : undefined }, i)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
const source = session.header.id
|
|
280
|
+
const out: Scored[] = []
|
|
281
|
+
for (const [path, f] of byPath) {
|
|
282
|
+
const stat = f.plus || f.minus ? `+${f.plus}/-${f.minus} lines` : f.written ? `${f.written} characters written` : ""
|
|
283
|
+
const what = f.why ? clipText(f.why, 160) : f.first ? `first changed line: ${clipText(f.first, 100)}` : stat
|
|
284
|
+
const n = f.keys.size
|
|
285
|
+
const text = redact(`${path}: ${f.created && n === 1 ? "created" : n > 1 ? `edited ${n} times` : "edited"}${what ? `; ${what}` : ""}${f.why && stat ? ` (${stat})` : ""}`)
|
|
286
|
+
out.push({
|
|
287
|
+
id: `file-${sha(source + path)}`,
|
|
288
|
+
kind: "file",
|
|
289
|
+
name: path,
|
|
290
|
+
text,
|
|
291
|
+
source,
|
|
292
|
+
occurredAt: f.at,
|
|
293
|
+
entities: [{ name: path, type: "file" }, { name: project.label, type: "project" }],
|
|
294
|
+
relations: [{ from: path, to: project.label, type: "edited_in" }],
|
|
295
|
+
score: 40 + Math.min(n, 8) * 2 + (f.why ? 6 : 0),
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
return out.sort((a, b) => b.score - a.score).slice(0, 12)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The assistant's own words just before an edit, when they are short enough to be a reason ("Fixed the TTL unit"). */
|
|
302
|
+
function nearbyWords(turns: Turn[], index: number): string | undefined {
|
|
303
|
+
for (let j = index - 1; j >= Math.max(0, index - 4); j--) {
|
|
304
|
+
const t = turns[j]!
|
|
305
|
+
if (t.kind === "assistant" && t.text.trim()) {
|
|
306
|
+
const first = oneLine(t.text).split(/(?<=[.!?:])\s/)[0] ?? ""
|
|
307
|
+
if (first.length >= 15 && first.length <= 180 && !/^(?:let me|i'll|i will|now|first|next|ok|sure)\b/i.test(first)) return first.replace(/[:.]$/, "")
|
|
308
|
+
if (first.length >= 15 && first.length <= 180) return first.replace(/^(?:let me|i'll|i will)\s+/i, "").replace(/[:.]$/, "")
|
|
309
|
+
return undefined
|
|
310
|
+
}
|
|
311
|
+
if (t.kind === "user" || t.kind === "compaction") return undefined
|
|
312
|
+
}
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---- summaries and state ------------------------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
const DONE = /\b(summary|done|fixed|implemented|changed|added|completed|shipped|now (?:works|passes|returns|uses)|all \d+ (?:tests )?pass|pass(?:es|ing)?|verified|resolved|created|updated|refactored|removed|wired)\b/i
|
|
319
|
+
|
|
320
|
+
function realUserTurns(session: Session): Array<Extract<Turn, { kind: "user" }>> {
|
|
321
|
+
return session.turns.filter((t): t is Extract<Turn, { kind: "user" }> => t.kind === "user" && !isInjected(t.text) && !/^This session is being continued/i.test(withoutInjected(t.text)) && withoutInjected(t.text).length < 6000)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function summaries(session: Session, at: (t: Turn) => string | undefined): Scored[] {
|
|
325
|
+
const source = session.header.id
|
|
326
|
+
const out: Scored[] = []
|
|
327
|
+
const compactions = session.turns.filter((t): t is Extract<Turn, { kind: "compaction" }> => t.kind === "compaction" && t.summary.trim().length >= 40)
|
|
328
|
+
for (const c of compactions.slice(-2)) {
|
|
329
|
+
const text = redact(clipText(c.summary, 1500))
|
|
330
|
+
out.push({ id: `summary-${sha(source + text)}`, kind: "summary", name: "summary of earlier work", text, source, occurredAt: at(c), score: 90 })
|
|
331
|
+
}
|
|
332
|
+
let last: Extract<Turn, { kind: "assistant" }> | undefined
|
|
333
|
+
let userAfter = false
|
|
334
|
+
for (let i = session.turns.length - 1; i >= 0; i--) {
|
|
335
|
+
const t = session.turns[i]!
|
|
336
|
+
if (t.kind === "user" && !isInjected(t.text)) userAfter = true
|
|
337
|
+
if (t.kind === "assistant" && t.text.trim().length >= 60) {
|
|
338
|
+
last = t
|
|
339
|
+
break
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (last && !userAfter && last.text.length >= 200 && DONE.test(last.text) && !last.text.trim().endsWith("?")) {
|
|
343
|
+
const text = redact(clipText(last.text, 1200))
|
|
344
|
+
out.push({ id: `summary-${sha(source + "last")}`, kind: "summary", name: "how the session ended", text, source, occurredAt: at(last), score: 70 })
|
|
345
|
+
}
|
|
346
|
+
return out
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function state(session: Session, at: (t: Turn) => string | undefined, hasEndSummary: boolean): Scored[] {
|
|
350
|
+
const users = realUserTurns(session)
|
|
351
|
+
const goal = users[0]
|
|
352
|
+
if (!goal) return []
|
|
353
|
+
const lastUser = users[users.length - 1]!
|
|
354
|
+
const lastAssistant = [...session.turns].reverse().find((t): t is Extract<Turn, { kind: "assistant" }> => t.kind === "assistant" && t.text.trim().length >= 20)
|
|
355
|
+
const parts = [`Goal: ${clipText(withoutInjected(goal.text), 300)}`]
|
|
356
|
+
if (lastUser !== goal) parts.push(`Last request: ${clipText(withoutInjected(lastUser.text), 300)}`)
|
|
357
|
+
if (lastAssistant) parts.push(`Where it stands: ${clipText(lastAssistant.text, hasEndSummary ? 160 : 400)}`)
|
|
358
|
+
const text = redact(parts.join("\n"))
|
|
359
|
+
const source = session.header.id
|
|
360
|
+
return [{ id: `state-${sha(source)}`, kind: "state", name: clipText(`state: ${session.header.title ?? withoutInjected(goal.text)}`, 70), text, source, occurredAt: at(session.turns[session.turns.length - 1]!), score: 100 }]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---- entry point --------------------------------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
export function distill(session: Session, opts: DistillOptions = {}): MemoryItem[] {
|
|
366
|
+
const max = opts.maxItems ?? 40
|
|
367
|
+
const fallback = session.header.updatedAt ?? session.header.startedAt
|
|
368
|
+
const at = (t: Turn): string | undefined => ("at" in t && t.at) || fallback
|
|
369
|
+
const sums = summaries(session, at)
|
|
370
|
+
const hasEnd = sums.some((s) => s.name === "how the session ended")
|
|
371
|
+
const all: Scored[] = [...state(session, at, hasEnd), ...sums, ...instructions(session, at), ...failures(session, at), ...files(session, at), ...commands(session, at)]
|
|
372
|
+
const kept = all
|
|
373
|
+
.filter((i) => i.text.replace(/\s+/g, " ").trim().length >= 20)
|
|
374
|
+
.filter((i) => !(i.kind === "file" && /^[\w./-]+:\s*$/.test(i.text)))
|
|
375
|
+
.sort((a, b) => b.score - a.score)
|
|
376
|
+
.slice(0, max)
|
|
377
|
+
const seen = new Set<string>()
|
|
378
|
+
return kept
|
|
379
|
+
.filter((i) => (seen.has(i.id) ? false : (seen.add(i.id), true)))
|
|
380
|
+
.map(({ score: _s, ...item }) => item)
|
|
381
|
+
}
|