@orquentra/codebuddy 2.14.0 → 2.15.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/ARCHITECTURE.md +98 -0
- package/README.md +87 -6
- package/dist/{chunk-LW75SKZR.js → chunk-4BSO3HST.js} +126 -18
- package/dist/chunk-4BSO3HST.js.map +1 -0
- package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
- package/dist/chunk-57642NGU.js.map +1 -0
- package/dist/{chunk-ESZXCZIN.js → chunk-6YI2D5F5.js} +33 -5
- package/dist/chunk-6YI2D5F5.js.map +1 -0
- package/dist/{chunk-4DKYKDHI.js → chunk-7IPSDDO5.js} +5 -3
- package/dist/{chunk-4DKYKDHI.js.map → chunk-7IPSDDO5.js.map} +1 -1
- package/dist/chunk-BTTQWSDS.js +372 -0
- package/dist/chunk-BTTQWSDS.js.map +1 -0
- package/dist/{chunk-YJPCS2GP.js → chunk-D5E7SGI5.js} +57 -18
- package/dist/chunk-D5E7SGI5.js.map +1 -0
- package/dist/chunk-EMWGLKPR.js +45 -0
- package/dist/chunk-EMWGLKPR.js.map +1 -0
- package/dist/chunk-ITXE27SZ.js +358 -0
- package/dist/chunk-ITXE27SZ.js.map +1 -0
- package/dist/{chunk-LRQNPYCK.js → chunk-JJQDVGF7.js} +13 -7
- package/dist/chunk-JJQDVGF7.js.map +1 -0
- package/dist/{chunk-XSNW6Y3X.js → chunk-KQCP3ZYH.js} +28 -24
- package/dist/chunk-KQCP3ZYH.js.map +1 -0
- package/dist/{chunk-YWDH6DQ6.js → chunk-KYRM3HPP.js} +21 -19
- package/dist/{chunk-YWDH6DQ6.js.map → chunk-KYRM3HPP.js.map} +1 -1
- package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
- package/dist/chunk-M75SN4D4.js.map +1 -0
- package/dist/{chunk-L6BRYZ6G.js → chunk-RZ266NHE.js} +7 -7
- package/dist/chunk-U7KDM62P.js +43 -0
- package/dist/chunk-U7KDM62P.js.map +1 -0
- package/dist/chunk-UATJCD2T.js +368 -0
- package/dist/chunk-UATJCD2T.js.map +1 -0
- package/dist/{chunk-YNSTIMCF.js → chunk-UR3DF74I.js} +352 -157
- package/dist/chunk-UR3DF74I.js.map +1 -0
- package/dist/chunk-UTPC66MV.js +128 -0
- package/dist/chunk-UTPC66MV.js.map +1 -0
- package/dist/chunk-WHKXUXHE.js +156 -0
- package/dist/chunk-WHKXUXHE.js.map +1 -0
- package/dist/{chunk-7QMHIJBA.js → chunk-YMXFSHXT.js} +969 -968
- package/dist/chunk-YMXFSHXT.js.map +1 -0
- package/dist/cli/index.js +4 -4
- package/dist/config-TXQJCCVI.js +15 -0
- package/dist/expand-KWYGNCKZ.js +98 -0
- package/dist/expand-KWYGNCKZ.js.map +1 -0
- package/dist/file-vectors-MKXTSGGH.js +19 -0
- package/dist/index-facts-4MTEHKMN.js +38 -0
- package/dist/index-pass-TBDUEZCB.js +14 -0
- package/dist/index.js +24 -22
- package/dist/{install-EU35ERK5.js → install-E3JWINTN.js} +11 -10
- package/dist/{lean-43ADYTBW.js → lean-MLTAFS5L.js} +8 -7
- package/dist/local-75DJECVR.js +48 -0
- package/dist/local-llm-MECVJTVU.js +19 -0
- package/dist/local-llm-MECVJTVU.js.map +1 -0
- package/dist/{main-SFC5LV4E.js → main-RLMKQZW4.js} +332 -123
- package/dist/main-RLMKQZW4.js.map +1 -0
- package/dist/{memory-TGHVTBME.js → memory-PFFVMTCH.js} +9 -6
- package/dist/{memory-TGHVTBME.js.map → memory-PFFVMTCH.js.map} +1 -1
- package/dist/{operations-6Z6ZDVBP.js → operations-STR7S5UO.js} +8 -7
- package/dist/operations-STR7S5UO.js.map +1 -0
- package/dist/{rerank-IQAEWLQR.js → rerank-XXKWDI5Q.js} +7 -4
- package/dist/{rerank-IQAEWLQR.js.map → rerank-XXKWDI5Q.js.map} +1 -1
- package/dist/{runtime-W5AW32SD.js → runtime-SWFWGTQ4.js} +9 -8
- package/dist/runtime-SWFWGTQ4.js.map +1 -0
- package/dist/summaries-QXWFFDUZ.js +31 -0
- package/dist/summaries-QXWFFDUZ.js.map +1 -0
- package/dist/{zvec-24HLAFGN.js → zvec-F73IBWO5.js} +13 -5
- package/dist/zvec-F73IBWO5.js.map +1 -0
- package/package.json +3 -2
- package/dist/chunk-7QMHIJBA.js.map +0 -1
- package/dist/chunk-CAEPHLHC.js.map +0 -1
- package/dist/chunk-ESC4Y3L7.js +0 -204
- package/dist/chunk-ESC4Y3L7.js.map +0 -1
- package/dist/chunk-ESZXCZIN.js.map +0 -1
- package/dist/chunk-KNR2P2UY.js.map +0 -1
- package/dist/chunk-LRQNPYCK.js.map +0 -1
- package/dist/chunk-LW75SKZR.js.map +0 -1
- package/dist/chunk-SFMIMLXR.js +0 -63
- package/dist/chunk-SFMIMLXR.js.map +0 -1
- package/dist/chunk-XSNW6Y3X.js.map +0 -1
- package/dist/chunk-YJPCS2GP.js.map +0 -1
- package/dist/chunk-YNSTIMCF.js.map +0 -1
- package/dist/config-GEZFLXK6.js +0 -12
- package/dist/main-SFC5LV4E.js.map +0 -1
- package/dist/summaries-32PHOB6L.js +0 -20
- /package/dist/{chunk-L6BRYZ6G.js.map → chunk-RZ266NHE.js.map} +0 -0
- /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
- /package/dist/{install-EU35ERK5.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
- /package/dist/{lean-43ADYTBW.js.map → index-facts-4MTEHKMN.js.map} +0 -0
- /package/dist/{operations-6Z6ZDVBP.js.map → index-pass-TBDUEZCB.js.map} +0 -0
- /package/dist/{runtime-W5AW32SD.js.map → install-E3JWINTN.js.map} +0 -0
- /package/dist/{summaries-32PHOB6L.js.map → lean-MLTAFS5L.js.map} +0 -0
- /package/dist/{zvec-24HLAFGN.js.map → local-75DJECVR.js.map} +0 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -113,6 +113,104 @@ Rationale:
|
|
|
113
113
|
|
|
114
114
|
- better to return slightly stale ranking than block on Hugging Face cold starts or rate limits
|
|
115
115
|
|
|
116
|
+
## Local LLM and Intent Cards (2.15)
|
|
117
|
+
|
|
118
|
+
**Runtime: node-llama-cpp.** The local small model (Qwen3-0.6B Q4_K_M by
|
|
119
|
+
default; Qwen3-1.7B as `better`; Qwen only) runs through
|
|
120
|
+
node-llama-cpp 3.22 (llama.cpp bindings). It was compared with Cactus on the
|
|
121
|
+
four criteria in the 2.15 brief:
|
|
122
|
+
|
|
123
|
+
| | node-llama-cpp 3.22.3 | Cactus |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Install size | 41 MB package + one prebuilt binary (19 MB mac-arm64-metal, 33–34 MB x64) | npm ships `cactus-react-native` (152 MB, mobile) and `cactus-needle` (tool-calling model only) |
|
|
126
|
+
| Cold start (M-series, Qwen3-0.6B) | 441 ms load, measured | no Node server binding to measure |
|
|
127
|
+
| Tokens/s (M-series, Metal) | 100–200 tok/s measured; ~2 s per card incl. JSON grammar | n/a |
|
|
128
|
+
| Prebuilt targets | macOS arm64/x64, Linux x64/arm64, Windows x64 (+CUDA/Vulkan) | iOS/Android focus |
|
|
129
|
+
|
|
130
|
+
Cactus has no maintained Node.js binding for a desktop/server process, and its
|
|
131
|
+
Node package hosts only the Needle tool-calling model, which the brief excludes
|
|
132
|
+
(the job is plain English → code vocabulary, not tool selection). So
|
|
133
|
+
node-llama-cpp. It is not a package dependency: `codebuddy use` (or
|
|
134
|
+
`codebuddy models add local-llm`) installs it with `npm -g` and downloads the pinned GGUF into
|
|
135
|
+
`~/.cache/codebuddy/models/gguf/`, checking size and sha256 before moving it
|
|
136
|
+
into place. Forcing `gpu: false` on macOS makes node-llama-cpp compile llama.cpp
|
|
137
|
+
from source (34 s first load), so the backend uses `gpu: "auto"` (Metal on Apple
|
|
138
|
+
Silicon, CPU elsewhere) with `threads` capped at cores − 1
|
|
139
|
+
(`CODEBUDDY_LLM_THREADS` overrides).
|
|
140
|
+
|
|
141
|
+
**Where it runs.** Generation happens on llama.cpp's native threads inside
|
|
142
|
+
`codebuddy serve` (background project sync) or `codebuddy index --cards`, never
|
|
143
|
+
on a request path except the bounded query expansion below. A single context
|
|
144
|
+
sequence serialises calls. Hooks and MCP tool calls touch
|
|
145
|
+
`.codebuddy/cache/foreground`; the card and fact passes wait (up to 15 s) while
|
|
146
|
+
it is fresher than 2 s. Output is constrained by a JSON-schema grammar with
|
|
147
|
+
`maxItems`/`maxLength` bounds (without them the 0.6B model writes paragraphs
|
|
148
|
+
until the token cap cuts the JSON), then validated with zod; invalid output is
|
|
149
|
+
retried once, then the file is skipped.
|
|
150
|
+
|
|
151
|
+
**Lifetime.** The model is loaded on demand and unloaded `IDLE_UNLOAD_MS`
|
|
152
|
+
(60 s) after its last generation; measured on Qwen3-0.6B, resident memory went
|
|
153
|
+
from 1,274 MB to 452 MB (the rest is the llama.cpp/Metal runtime, freed when
|
|
154
|
+
the process exits). `serve` frees it on shutdown, and the detached
|
|
155
|
+
`index --cards` that setup starts exits when its pass ends. Setup skips the
|
|
156
|
+
local model on machines under 4 GB RAM or with one core (`machineCanCard`) and
|
|
157
|
+
uses cloud Qwen there when a key is set.
|
|
158
|
+
|
|
159
|
+
**Model size.** The same four-file card test with the grammar bounds:
|
|
160
|
+
SmolLM2-135M Q4_K_M (105 MB) wrote `...` summaries and raw code lines as
|
|
161
|
+
symptoms; Gemma 3 270M Q4_K_M (253 MB) wrote one-word summaries and term lists
|
|
162
|
+
as symptoms; Qwen3-0.6B Q4_K_M (397 MB) wrote usable summaries and symptoms.
|
|
163
|
+
So CodeBuddy ships Qwen only. A machine below `machineCanCard` gets Qwen from
|
|
164
|
+
the cloud instead (BYOK: `qwen/qwen3.8-27b` on Groq or OpenRouter, sent with
|
|
165
|
+
`reasoning_effort: "none"`; `<think>` blocks are stripped), configured by
|
|
166
|
+
setup only when a provider key is already in the environment.
|
|
167
|
+
|
|
168
|
+
**Cards** extend the existing `llm-summaries.json` store (prompt version 2), so
|
|
169
|
+
the BYOK and local backends fill the same cache. Each card is used three ways:
|
|
170
|
+
a card vector per file in `file-vectors.json` (granite, next to the chunk
|
|
171
|
+
vectors), one Markdown file per source file in `.codebuddy/cache/cards/`, which
|
|
172
|
+
is its own zvec-grep workspace (English model `local/bge-small-en-v1.5`,
|
|
173
|
+
`--no-ignore`, so the project's ignore rules never hide it), and card terms and
|
|
174
|
+
symptoms in the content index at half weight.
|
|
175
|
+
|
|
176
|
+
**Agreement rule** (`context/discovery.ts`). 2.14 dropped every vector hit
|
|
177
|
+
outside the keyword set unless it shared task terms with the source and cleared
|
|
178
|
+
`zvec.minScore`. That deleted exactly the hits a reworded task needs. In 2.15 a
|
|
179
|
+
candidate is admitted when at least two signals put it in their top k (default
|
|
180
|
+
5): zvec-grep (code and card workspaces fused), the source-chunk vector, the
|
|
181
|
+
card vector, and the old term-overlap rule as one more vote. Every ranking has
|
|
182
|
+
a top k even for an unrelated task, so a vector vote also needs its cosine at
|
|
183
|
+
or above a floor (`chunkFloor` 0.80, `cardFloor` 0.75). zg prints no
|
|
184
|
+
similarity, so its vote is rank-only, and two rank-only votes never agree.
|
|
185
|
+
Abstention returns nothing only when no candidate passes the rule and keyword
|
|
186
|
+
evidence is below the floor. Card text alone never admits a file; card
|
|
187
|
+
evidence counts when another signal agrees. The floors are uncalibrated
|
|
188
|
+
defaults. Tune them in `.codebuddy/config.json` under `discovery.agreement`
|
|
189
|
+
against the unrelated-task files (the 6 in the Litestar harness plus
|
|
190
|
+
`evals/unrelated-2.15.json`).
|
|
191
|
+
|
|
192
|
+
**Confidence.** Discovery returns `high` (anchor, exact identifier, CLI
|
|
193
|
+
command, or ≥ 3 votes counting a strong keyword match as two), `medium` (two),
|
|
194
|
+
or `low` (dense signals present but no returned file has two). `low` returns no
|
|
195
|
+
paths: the hook emits nothing, and `context_pack` adds a note telling the agent
|
|
196
|
+
to search normally. Without any dense signal the result is 2.14's keyword path
|
|
197
|
+
and is never suppressed.
|
|
198
|
+
|
|
199
|
+
**Expansion.** When a task found nothing confident and names no identifier,
|
|
200
|
+
discovery asks the warm local model for 5–8 code terms (≤ 800 ms in
|
|
201
|
+
`context_pack`, ≤ 300 ms in the hook) and ranks once more. Answers are cached
|
|
202
|
+
per normalised task in `.codebuddy/cache/expansions.json` (LRU, 200). The hook
|
|
203
|
+
process never has a warm model, so it only benefits from cached expansions.
|
|
204
|
+
|
|
205
|
+
**Index facts** (`llm/index-facts.ts`) are Markdown facts with
|
|
206
|
+
`createdByAgent: codebuddy-index`, `paths: [source]`,
|
|
207
|
+
`verification: ["source:Lline"]` and an `index:` idempotency key.
|
|
208
|
+
Manifest and CI facts are read deterministically. Prose facts come from the
|
|
209
|
+
model and must share words with the cited line and read as a statement.
|
|
210
|
+
Sensitive candidates are dropped. Re-extraction replaces only index facts
|
|
211
|
+
whose source hash changed. The cap is 40 per repository, and near-duplicates
|
|
212
|
+
(Jaccard ≥ 0.8) of any existing fact are skipped.
|
|
213
|
+
|
|
116
214
|
## Sharing Model
|
|
117
215
|
|
|
118
216
|
Shares are references by default.
|
package/README.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
# CodeBuddy
|
|
1
|
+
# CodeBuddy by Orquentra
|
|
2
2
|
|
|
3
3
|
Local-first project context and memory for AI coding agents.
|
|
4
4
|
|
|
5
|
+
Published by Orquentra as
|
|
6
|
+
[`@orquentra/codebuddy` on npm](https://www.npmjs.com/package/@orquentra/codebuddy).
|
|
7
|
+
|
|
5
8
|
CodeBuddy helps agents find relevant code, understand project constraints, and
|
|
6
9
|
remember decisions across sessions. It works through MCP with Claude Code,
|
|
7
10
|
Codex, OpenCode, Hermes, and other compatible clients.
|
|
8
11
|
|
|
12
|
+
[npm package](https://www.npmjs.com/package/@orquentra/codebuddy) ·
|
|
13
|
+
[Documentation](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/guide.md) ·
|
|
14
|
+
[Source and issues](https://github.com/Orquentra-Inc/CodeBuddy)
|
|
15
|
+
|
|
9
16
|
## Install
|
|
10
17
|
|
|
11
18
|
Requires Node.js 20 or later.
|
|
@@ -16,9 +23,12 @@ cd your-project
|
|
|
16
23
|
codebuddy
|
|
17
24
|
```
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
That one command sets everything up: PostgreSQL (the bundled Docker database
|
|
27
|
+
if none is running), the local embedding model (~90 MB), zvec-grep (installed
|
|
28
|
+
and indexed), the local Qwen model (~400 MB; cloud Qwen with a key you enter on
|
|
29
|
+
machines under 4 GB RAM), agent wiring for Claude and Codex, and a background
|
|
30
|
+
pass that builds file vectors, intent cards and project facts. Skip pieces with
|
|
31
|
+
`--no-local-llm`, `--no-graphify` or `--no-docker`.
|
|
22
32
|
|
|
23
33
|
Run setup inside the project you want CodeBuddy to read. Claude Desktop
|
|
24
34
|
registration saves that folder's absolute path, so restarting the desktop app
|
|
@@ -28,13 +38,27 @@ Older desktop registrations without a project path can be repaired by running
|
|
|
28
38
|
`codebuddy claude install` from the initialized project folder, then restarting
|
|
29
39
|
Claude Desktop.
|
|
30
40
|
|
|
41
|
+
### Verify the package
|
|
42
|
+
|
|
43
|
+
The npm package name is **`@orquentra/codebuddy`**; the installed command is
|
|
44
|
+
**`codebuddy`**. Open the [npm package page](https://www.npmjs.com/package/@orquentra/codebuddy)
|
|
45
|
+
directly to find its published versions, or query the public registry:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm view @orquentra/codebuddy version --registry=https://registry.npmjs.org/
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Search engine results and AI summaries may lag behind npm releases. If an
|
|
52
|
+
installation fails, include the exact npm error and requested version in a
|
|
53
|
+
[GitHub issue](https://github.com/Orquentra-Inc/CodeBuddy/issues).
|
|
54
|
+
|
|
31
55
|
## What it does
|
|
32
56
|
|
|
33
57
|
- **Task context:** relevant files, symbols, code excerpts, nearby dependencies,
|
|
34
58
|
and covering tests within a token budget.
|
|
35
59
|
- **Project memory:** decisions, rules, and handoffs stored as readable Markdown,
|
|
36
60
|
recalled for relevant tasks by hybrid lexical + vector search
|
|
37
|
-
([how it works](docs/semantic-memory.md)).
|
|
61
|
+
([how it works](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/semantic-memory.md)).
|
|
38
62
|
- **Continuity:** a local journal of each session's goal, edits, checks and
|
|
39
63
|
findings, re-injected after compaction and offered to the next session when
|
|
40
64
|
a task was left unfinished. Native bridges cover Claude Code, Codex, OpenCode
|
|
@@ -50,6 +74,63 @@ Claude Desktop.
|
|
|
50
74
|
- **Optional retrieval:** zvec semantic search, Graphify architecture maps, and
|
|
51
75
|
a bring-your-own LLM layer.
|
|
52
76
|
|
|
77
|
+
## Optional local LLM (2.15)
|
|
78
|
+
|
|
79
|
+
A small local model can write a plain-English **intent card** for every source
|
|
80
|
+
file, so a task worded nothing like the code (for example "the browser forgets
|
|
81
|
+
how long to wait before reconnecting") still finds the right file. `codebuddy`
|
|
82
|
+
/ `codebuddy use` installs it as part of setup (Qwen3-0.6B Q4_K_M, ~400 MB,
|
|
83
|
+
checksum-verified; skip with `--no-local-llm` or `CODEBUDDY_LOCAL_LLM=0`), and it
|
|
84
|
+
runs offline after that. To manage it by hand:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
codebuddy models add local-llm # Qwen3-0.6B Q4_K_M, ~400 MB, checksum-verified
|
|
88
|
+
codebuddy models add local-llm --size better # Qwen3-1.7B, ~1.1 GB
|
|
89
|
+
codebuddy models remove local-llm # back to keyword + vector discovery
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Setup also installs the runtime (`node-llama-cpp`, prebuilt for macOS, Linux
|
|
93
|
+
and Windows; `models add` asks first). CodeBuddy uses Qwen only. On a machine
|
|
94
|
+
under 4 GB of RAM or with one CPU core, setup uses Qwen in the cloud instead
|
|
95
|
+
(`qwen/qwen3.8-27b` on Groq, free-tier key; or OpenRouter). It uses
|
|
96
|
+
`GROQ_API_KEY` / `OPENROUTER_API_KEY` from the environment, or asks for a Groq
|
|
97
|
+
key (free at console.groq.com/keys) and saves it in `~/.codebuddy/global.json`
|
|
98
|
+
(mode 0600, never in the project). Bounded, redacted file heads and
|
|
99
|
+
signatures are then sent to that provider ([details](docs/llm-byok.md)). Skip
|
|
100
|
+
the prompt and keyword + vector discovery keep working.
|
|
101
|
+
The model runs inside `codebuddy serve` (not in Docker; Docker only hosts
|
|
102
|
+
PostgreSQL). It is unloaded 60 s after its last card and freed when the agent
|
|
103
|
+
closes `serve`, so it never keeps running in the background.
|
|
104
|
+
|
|
105
|
+
Qwen3-0.6B is the smallest model that wrote usable cards in our checks:
|
|
106
|
+
SmolLM2-135M (105 MB) returned placeholders and raw code lines, and Gemma 3
|
|
107
|
+
270M returned keyword lists instead of descriptions, so neither ships.
|
|
108
|
+
Nothing leaves your machine, and without the model every feature falls back to
|
|
109
|
+
the 2.14 behaviour.
|
|
110
|
+
|
|
111
|
+
- **Intent cards:** a summary, 3–6 "symptoms" in bug-report wording, the code
|
|
112
|
+
terms to search for, and a line per main symbol. Written in the background by
|
|
113
|
+
`codebuddy serve` (or `codebuddy index --cards`) for new and changed source
|
|
114
|
+
files only; tests, docs, generated, vendored and secret-bearing files are
|
|
115
|
+
skipped. Cards feed a second vector per file, a zvec-grep card workspace, and
|
|
116
|
+
(down-weighted) the keyword index. Progress: `codebuddy models`, `codebuddy
|
|
117
|
+
storage`.
|
|
118
|
+
- **Agreement rule:** a file the keywords missed is admitted when two
|
|
119
|
+
independent signals agree (zvec-grep, source-chunk vectors, card vectors,
|
|
120
|
+
term overlap). When no returned file has two, CodeBuddy says so instead of
|
|
121
|
+
guessing: `context_pack` returns `confidence: "low"` with a note to search
|
|
122
|
+
normally, and the prompt hook stays silent.
|
|
123
|
+
- **Index facts:** up to 40 project facts drawn from `package.json`,
|
|
124
|
+
`pyproject.toml`, CI workflows, README / CONTRIBUTING / AGENTS.md /
|
|
125
|
+
CLAUDE.md and deprecation or release policies, each with its source line.
|
|
126
|
+
They are ordinary Markdown facts; `codebuddy memory list --origin index`
|
|
127
|
+
shows them and `codebuddy memory prune --origin index` removes them. Facts a
|
|
128
|
+
person or agent wrote are never replaced.
|
|
129
|
+
|
|
130
|
+
`codebuddy use` also builds the zvec-grep index and enables it when `zg` 0.2.x
|
|
131
|
+
is installed, and `codebuddy serve` keeps a `zg server` running so the prompt
|
|
132
|
+
hook gets vector candidates within its deadline.
|
|
133
|
+
|
|
53
134
|
## What it costs
|
|
54
135
|
|
|
55
136
|
Context is not free: tool schemas, the rules block and the injected context are
|
|
@@ -98,7 +179,7 @@ needs more detail. Inspect missing or truncated evidence before editing.
|
|
|
98
179
|
|
|
99
180
|
## Documentation
|
|
100
181
|
|
|
101
|
-
- [Full guide and examples](https://github.com/Orquentra-Inc/CodeBuddy/blob/
|
|
182
|
+
- [Full guide and examples](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/guide.md)
|
|
102
183
|
- [Telemetry and the admin panel](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/telemetry.md)
|
|
103
184
|
- [Token budgets](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/token-budgets.md)
|
|
104
185
|
- [Optional LLM setup](https://github.com/Orquentra-Inc/CodeBuddy/blob/main/docs/llm-byok.md)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
asksForChangelog
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UR3DF74I.js";
|
|
4
4
|
import {
|
|
5
5
|
readConfigFile
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-M75SN4D4.js";
|
|
7
7
|
import {
|
|
8
8
|
writeAtomic
|
|
9
9
|
} from "./chunk-CG5B3LYG.js";
|
|
@@ -52,10 +52,10 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
|
|
|
52
52
|
const config = await zvecConfig(root);
|
|
53
53
|
if (!config.enabled) return none("disabled", "zvec-grep not enabled");
|
|
54
54
|
const { deadlineAt } = options;
|
|
55
|
-
const run = (args) => {
|
|
55
|
+
const run = (args, cwd = root) => {
|
|
56
56
|
const ms = deadlineAt === void 0 ? config.timeoutMs : Math.min(config.timeoutMs, deadlineAt - Date.now());
|
|
57
|
-
if (ms <= 0) return Promise.resolve({ stdout: "", error: deadlineError() });
|
|
58
|
-
return runZg(config.binary, args,
|
|
57
|
+
if (ms <= 0) return Promise.resolve({ stdout: "", stderr: "", error: deadlineError() });
|
|
58
|
+
return runZg(config.binary, args, cwd, ms);
|
|
59
59
|
};
|
|
60
60
|
const missedDeadline = (status) => deadlineAt !== void 0 && status === "timeout" && Date.now() >= deadlineAt - 5;
|
|
61
61
|
const cached = await readCachedReadiness(root, config.binary);
|
|
@@ -73,7 +73,7 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
|
|
|
73
73
|
const limit = Math.min(20, Math.max(1, Math.floor(options.limit ?? 10)));
|
|
74
74
|
const chunkLimit = limit * 8;
|
|
75
75
|
const globs = zvecExcludeGlobs(task);
|
|
76
|
-
const
|
|
76
|
+
const query = [
|
|
77
77
|
"query",
|
|
78
78
|
"--vector",
|
|
79
79
|
task,
|
|
@@ -84,13 +84,20 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
|
|
|
84
84
|
"--limit",
|
|
85
85
|
String(chunkLimit),
|
|
86
86
|
...globs.flatMap((glob) => ["-g", glob])
|
|
87
|
-
]
|
|
87
|
+
];
|
|
88
|
+
const cardQuery = queryCards(root, task, limit, (args, cwd) => run(args, cwd));
|
|
89
|
+
let result = await run(query);
|
|
90
|
+
if (result.error && isBusy(result) && (deadlineAt === void 0 || deadlineAt - Date.now() > 400)) {
|
|
91
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, BUSY_RETRY_MS));
|
|
92
|
+
result = await run(query);
|
|
93
|
+
}
|
|
88
94
|
if (result.error) {
|
|
89
|
-
const failure = fromError(result.error, "zvec-grep query");
|
|
90
|
-
if (!missedDeadline(failure.status)) await resetZvecCache(root);
|
|
95
|
+
const failure = fromError(result.error, "zvec-grep query", result.stderr);
|
|
96
|
+
if (!missedDeadline(failure.status) && failure.status !== "busy") await resetZvecCache(root);
|
|
91
97
|
return failure;
|
|
92
98
|
}
|
|
93
99
|
const hits = parseHits(result.stdout).slice(0, chunkLimit);
|
|
100
|
+
const cardPaths = await cardQuery;
|
|
94
101
|
if (hits.length === 0 && /\bzg index\b/.test(result.stdout)) {
|
|
95
102
|
const notReady = none("index-not-ready", NOT_READY);
|
|
96
103
|
await markReadiness(root, config.binary, notReady);
|
|
@@ -133,7 +140,8 @@ async function zvecCandidates(repositoryRoot, task, options = {}) {
|
|
|
133
140
|
stale,
|
|
134
141
|
changed,
|
|
135
142
|
minScore: config.minScore,
|
|
136
|
-
|
|
143
|
+
...cardPaths.length > 0 ? { cardPaths } : {},
|
|
144
|
+
reason: `zvec-grep vector search: ${paths.length} file(s)` + (globs.length > 0 ? "; non-source paths excluded" : "") + (stale > 0 ? `, ${stale} stale hit(s) dropped` : "") + (changed > 0 ? `, ${changed} hit(s) changed since the zg index dropped` : "") + (cardPaths.length > 0 ? `; ${cardPaths.length} intent card match(es)` : "") + (indexedAt === null ? "; index age unknown, hits unverified (run `codebuddy zvec index`)" : "")
|
|
137
145
|
};
|
|
138
146
|
}
|
|
139
147
|
async function refreshZvecIndex(repositoryRoot, options = {}) {
|
|
@@ -229,6 +237,90 @@ async function buildZvecIndex(repositoryRoot, options = {}) {
|
|
|
229
237
|
return code;
|
|
230
238
|
}
|
|
231
239
|
var NOT_READY = "zvec-grep index missing or not ready; run `codebuddy zvec index`";
|
|
240
|
+
var CARDS_DIR = join(".codebuddy", "cache", "cards");
|
|
241
|
+
var DEFAULT_CARD_MODEL = "local/bge-small-en-v1.5";
|
|
242
|
+
async function queryCards(root, task, limit, run) {
|
|
243
|
+
const cards = join(root, CARDS_DIR);
|
|
244
|
+
if (!await stat(join(cards, ".zvec-grep")).catch(() => null)) return [];
|
|
245
|
+
const result = await run(
|
|
246
|
+
["query", "--vector", task, "--mode", "auto", "--refresh", "off", "--limit", String(limit)],
|
|
247
|
+
cards
|
|
248
|
+
);
|
|
249
|
+
if (result.error) return [];
|
|
250
|
+
const paths = [];
|
|
251
|
+
for (const hit of parseHits(result.stdout)) {
|
|
252
|
+
const card = toRepoRelative(cards, hit.path);
|
|
253
|
+
if (!card?.endsWith(".md")) continue;
|
|
254
|
+
const path = card.slice(0, -3);
|
|
255
|
+
if (paths.includes(path)) continue;
|
|
256
|
+
if (!(await stat(resolve(root, path)).catch(() => null))?.isFile()) continue;
|
|
257
|
+
paths.push(path);
|
|
258
|
+
}
|
|
259
|
+
return paths.slice(0, limit);
|
|
260
|
+
}
|
|
261
|
+
async function refreshZvecCardIndex(repositoryRoot, options = {}) {
|
|
262
|
+
const root = resolve(repositoryRoot);
|
|
263
|
+
const config = await zvecConfig(root);
|
|
264
|
+
if (!config.enabled) return { status: "disabled", reason: "zvec-grep not enabled" };
|
|
265
|
+
const cards = join(root, CARDS_DIR);
|
|
266
|
+
if (!await stat(cards).catch(() => null))
|
|
267
|
+
return { status: "ok", reason: "no intent cards yet" };
|
|
268
|
+
const exists = await stat(join(cards, ".zvec-grep")).catch(() => null);
|
|
269
|
+
const result = await runZg(
|
|
270
|
+
config.binary,
|
|
271
|
+
[
|
|
272
|
+
"index",
|
|
273
|
+
cards,
|
|
274
|
+
...exists ? [] : ["--embedding", config.cardModel],
|
|
275
|
+
"--no-ignore",
|
|
276
|
+
"--mode",
|
|
277
|
+
"auto"
|
|
278
|
+
],
|
|
279
|
+
cards,
|
|
280
|
+
REFRESH_TIMEOUT_MS,
|
|
281
|
+
options.signal
|
|
282
|
+
);
|
|
283
|
+
if (result.error) {
|
|
284
|
+
const failure = fromError(result.error, "zvec-grep card index", result.stderr);
|
|
285
|
+
return { status: failure.status, reason: failure.reason };
|
|
286
|
+
}
|
|
287
|
+
await writeFile(join(cards, ".zvec-grep", ".gitignore"), "*\n", { flag: "wx" }).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
return { status: "ok", reason: "zvec-grep card index refreshed" };
|
|
290
|
+
}
|
|
291
|
+
async function ensureZvecServer(repositoryRoot) {
|
|
292
|
+
const root = resolve(repositoryRoot);
|
|
293
|
+
const config = await zvecConfig(root);
|
|
294
|
+
const noop = async () => {
|
|
295
|
+
};
|
|
296
|
+
if (!config.enabled) return { started: false, reason: "zvec-grep not enabled", stop: noop };
|
|
297
|
+
const run = (args) => runZg(config.binary, args, root, Math.max(config.timeoutMs, 5e3));
|
|
298
|
+
const readiness = await checkReadiness(run);
|
|
299
|
+
if (readiness) return { started: false, reason: readiness.reason, stop: noop };
|
|
300
|
+
const status = await run(["server", "status"]);
|
|
301
|
+
if (!status.error && /^Server: ready\b/m.test(status.stdout)) {
|
|
302
|
+
return { started: false, reason: "zg server already running (reused)", stop: noop };
|
|
303
|
+
}
|
|
304
|
+
const on = await run(["server", "on"]);
|
|
305
|
+
await rm(serverPath(root), { force: true });
|
|
306
|
+
if (on.error) {
|
|
307
|
+
return {
|
|
308
|
+
started: false,
|
|
309
|
+
reason: fromError(on.error, "zg server on", on.stderr).reason,
|
|
310
|
+
stop: noop
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
started: true,
|
|
315
|
+
reason: "zg server started",
|
|
316
|
+
// ponytail: the daemon is shared; a second `codebuddy serve` that reused it
|
|
317
|
+
// loses vectors when this one exits. Refcount via ZVEC_GREP_HOME if that matters.
|
|
318
|
+
stop: async () => {
|
|
319
|
+
await runZg(config.binary, ["server", "off"], root, 5e3);
|
|
320
|
+
await rm(serverPath(root), { force: true });
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
232
324
|
async function checkReadiness(run) {
|
|
233
325
|
const version = await run(["version"]);
|
|
234
326
|
if (version.error) return fromError(version.error, "zvec-grep version check");
|
|
@@ -306,7 +398,8 @@ async function zvecConfig(root) {
|
|
|
306
398
|
enabled: env !== void 0 ? env === "1" || env === "true" : file.zvec?.enabled ?? false,
|
|
307
399
|
binary: file.zvec?.binary ?? "zg",
|
|
308
400
|
timeoutMs: file.zvec?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
309
|
-
minScore: file.zvec?.minScore ?? DEFAULT_ZVEC_MIN_SCORE
|
|
401
|
+
minScore: file.zvec?.minScore ?? DEFAULT_ZVEC_MIN_SCORE,
|
|
402
|
+
cardModel: file.zvec?.cardModel ?? DEFAULT_CARD_MODEL
|
|
310
403
|
};
|
|
311
404
|
}
|
|
312
405
|
function runZg(binary, args, cwd, timeoutMs, signal) {
|
|
@@ -321,12 +414,16 @@ function runZg(binary, args, cwd, timeoutMs, signal) {
|
|
|
321
414
|
killSignal: "SIGKILL",
|
|
322
415
|
...signal ? { signal } : {}
|
|
323
416
|
},
|
|
324
|
-
(error, stdout) => {
|
|
417
|
+
(error, stdout, stderr) => {
|
|
325
418
|
if (error && error.killed) {
|
|
326
|
-
resolveRun({
|
|
419
|
+
resolveRun({
|
|
420
|
+
stdout: "",
|
|
421
|
+
stderr: String(stderr),
|
|
422
|
+
error: Object.assign(error, { code: "ETIMEDOUT" })
|
|
423
|
+
});
|
|
327
424
|
return;
|
|
328
425
|
}
|
|
329
|
-
resolveRun({ stdout: String(stdout), error });
|
|
426
|
+
resolveRun({ stdout: String(stdout), stderr: String(stderr), error });
|
|
330
427
|
}
|
|
331
428
|
);
|
|
332
429
|
});
|
|
@@ -360,10 +457,17 @@ async function serverRunning(root, binary, run) {
|
|
|
360
457
|
}
|
|
361
458
|
return running;
|
|
362
459
|
}
|
|
363
|
-
function fromError(error, what) {
|
|
460
|
+
function fromError(error, what, stderr = "") {
|
|
364
461
|
if (error.code === "ENOENT") return none("not-installed", "zvec-grep (`zg`) not found on PATH");
|
|
365
462
|
if (error.code === "ETIMEDOUT") return none("timeout", `${what} timed out`);
|
|
366
|
-
return none("
|
|
463
|
+
if (BUSY.test(stderr)) return none("busy", `${what}: index busy (a zg index refresh is running)`);
|
|
464
|
+
const code = stderr.match(/\bCode:\s*(\S+)/)?.[1];
|
|
465
|
+
return none("failed", `${what} failed${code ? ` (${code})` : ""}`);
|
|
466
|
+
}
|
|
467
|
+
var BUSY = /ZVEC_GREP\.ENGINE\.LOCK\.BUSY/;
|
|
468
|
+
var BUSY_RETRY_MS = 150;
|
|
469
|
+
function isBusy(result) {
|
|
470
|
+
return BUSY.test(result.stderr);
|
|
367
471
|
}
|
|
368
472
|
function none(status, reason) {
|
|
369
473
|
return { status, paths: [], hits: [], reason, stale: 0, changed: 0 };
|
|
@@ -384,6 +488,10 @@ export {
|
|
|
384
488
|
parseHits,
|
|
385
489
|
resetZvecCache,
|
|
386
490
|
zvecStatus,
|
|
387
|
-
buildZvecIndex
|
|
491
|
+
buildZvecIndex,
|
|
492
|
+
CARDS_DIR,
|
|
493
|
+
DEFAULT_CARD_MODEL,
|
|
494
|
+
refreshZvecCardIndex,
|
|
495
|
+
ensureZvecServer
|
|
388
496
|
};
|
|
389
|
-
//# sourceMappingURL=chunk-
|
|
497
|
+
//# sourceMappingURL=chunk-4BSO3HST.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/retrieval/zvec.ts"],"sourcesContent":["import { execFile, spawn } from \"node:child_process\";\nimport { mkdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { asksForChangelog } from \"../context/discovery.js\";\nimport { readConfigFile } from \"../core/config-file.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\n\n/**\n * Optional semantic candidate source backed by zvec-grep (`zg`), a local\n * code-search CLI. It only *suggests* files: hits are normalized to\n * repository-relative paths, checked against the working tree, and fused with\n * the keyword ranking by the caller. Risk, policy and incident evidence are\n * never derived from it.\n *\n * Off unless `.codebuddy/config.json` sets `zvec.enabled` (or\n * `CODEBUDDY_ZVEC=1`). It never builds an index implicitly: an unready index,\n * a missing or unsupported binary, a timeout, or unparseable output all return\n * no candidates plus a reason, and callers fall back to keyword ranking.\n *\n * Queries exclude non-source paths (docs, Markdown, evals, scripts, tests,\n * CodeBuddy's own state) unless the task asks about them: otherwise prose that\n * quotes a task, such as audits or eval prompts, outranks the code it describes.\n *\n * Benchmarked: vector-only queries beat zg's default hybrid route on code\n * tasks, so only `--vector` is used. Results are parsed from zg's agent\n * markdown (`#N matchedBy=… path:start-end`), as 0.2.x has no JSON output.\n *\n * Queries use `--mode auto`: a running `zg server` answers in ~200ms, otherwise\n * zg searches directly (~400ms). Auto mode never starts the daemon. The\n * version/readiness check (~400ms) is cached on disk for a few minutes so the\n * per-prompt hook process doesn't repeat it. A failed check (not installed,\n * index not ready, timeout) is cached too, but only for 30 s and only until the\n * workspace's `.zvec-grep/` changes, so an unready index costs one probe per\n * window instead of one per call while a fresh `zg index` is still picked up.\n *\n * Freshness: queries keep `--refresh off` so the prompt path never waits on\n * embedding. Instead `refreshZvecIndex` (run by project sync after index\n * passes, off the request path) and `codebuddy zvec index` record when the zg\n * index was last brought up to date, and hits whose file changed after that are\n * dropped rather than presented with their old meaning.\n */\n\n/** zvec-grep versions whose CLI and output format this adapter was tested against. */\nconst SUPPORTED = /^0\\.2\\.\\d+$/;\nconst DEFAULT_TIMEOUT_MS = 1_500;\nconst READY_CACHE_MS = 10 * 60_000;\n/** A failed readiness check is reused this long, so a fresh `zg index` is noticed quickly. */\nconst NOT_READY_CACHE_MS = 30_000;\n/** An incremental `zg index` runs in the background; only a runaway is killed. */\nconst REFRESH_TIMEOUT_MS = 10 * 60_000;\n/** Small, fast local model; indexes this repo in seconds. */\nexport const DEFAULT_ZVEC_MODEL = \"local/potion-code-16m-v2\";\n/**\n * Default `zvec.minScore`: a vector-only file must cover this share of the\n * task's IDF mass in its current source. zg 0.2.x prints no similarity (its\n * `--trace` score is 1/(60+rank)), so the floor is CodeBuddy's own measure. 0.3\n * is the content gate's value (`MIN_IDF_COVERAGE`), not calibrated on vector\n * hits: see docs/zvec-floor.md to calibrate it.\n */\nexport const DEFAULT_ZVEC_MIN_SCORE = 0.3;\nconst DOC_GLOBS = [\"!**/*.md\", \"!**/*.mdx\", \"!**/*.rst\", \"!**/*.adoc\", \"!docs/**\"];\n/** zg globs are case-sensitive: the usual spellings of changelogs and release notes. */\nconst CHANGELOG_GLOBS = [\n \"!**/CHANGES*\",\n \"!**/CHANGELOG*\",\n \"!**/HISTORY*\",\n \"!**/NEWS*\",\n \"!**/RELEASE*\",\n \"!**/changelog*\",\n \"!**/Changelog*\",\n \"!**/ChangeLog*\",\n];\nconst TEST_GLOBS = [\n \"!**/*.test.*\",\n \"!**/*.spec.*\",\n \"!**/__tests__/**\",\n \"!**/test/**\",\n \"!**/tests/**\",\n];\nconst ALWAYS_EXCLUDED_GLOBS = [\"!evals/**\", \"!scripts/**\", \"!.codebuddy/**\", \"!**/fixtures/**\"];\n\n/** zg `-g` exclusions for a task: docs and tests stay searchable when the task names them. */\nexport function zvecExcludeGlobs(task: string): string[] {\n return [\n ...(/\\b(docs?|readme|documentation|markdown)\\b/i.test(task) ? [] : DOC_GLOBS),\n ...(/\\b(tests?|spec)\\b/i.test(task) ? [] : TEST_GLOBS),\n ...(asksForChangelog(task) ? [] : CHANGELOG_GLOBS),\n ...ALWAYS_EXCLUDED_GLOBS,\n ];\n}\n\nconst EXCLUDED_PREFIXES = [\".codebuddy/cache/\", \".zvec-grep/\", \".git/\", \"node_modules/\"];\n\nexport type ZvecStatus =\n | \"ok\"\n | \"disabled\"\n | \"not-installed\"\n | \"unsupported-version\"\n | \"index-not-ready\"\n | \"timeout\"\n | \"failed\"\n | \"no-server\"\n /** Another zg process (a background `zg index`) holds the index lock. */\n | \"busy\";\n\n/** zg provides no content hash: a valid range alone cannot prove index freshness. */\nexport type RetrievalHit = {\n path: string;\n startLine: number;\n endLine: number;\n rank: number;\n /** `anchor`: a line the task itself names (path:line, stack frame, error string). */\n source: \"zvec\" | \"anchor\";\n freshness: \"unverified\";\n};\n\nexport type ZvecCandidates = {\n status: ZvecStatus;\n /** Repo-relative paths of existing files, best first. */\n paths: string[];\n /** Every chunk hit inside `paths`, in rank order (several per file). */\n hits: RetrievalHit[];\n reason: string;\n /** Hits dropped because the file no longer exists (stale index). */\n stale: number;\n /** Hits dropped because the file changed after the zg index was refreshed. */\n changed: number;\n /** `zvec.minScore` for vector-only files; set on every `ok` result. */\n minScore?: number;\n /**\n * Source paths whose intent card (`.codebuddy/cache/cards/<path>.md`, written\n * by the card pass) matched the task in the card workspace, best first.\n * Paths only: a card's line ranges say nothing about its source file.\n */\n cardPaths?: string[];\n};\n\nexport async function zvecCandidates(\n repositoryRoot: string,\n task: string,\n options: {\n limit?: number;\n /**\n * Hard stop (epoch ms) for every zg call: each one is killed at the\n * deadline and the caller falls back to keyword ranking. A miss is not\n * cached, so a slow prompt does not turn zvec off for the next one.\n */\n deadlineAt?: number;\n /** Query only while a `zg server` is running (checked at most every 30 s). */\n requireServer?: boolean;\n } = {},\n): Promise<ZvecCandidates> {\n const root = resolve(repositoryRoot);\n const config = await zvecConfig(root);\n if (!config.enabled) return none(\"disabled\", \"zvec-grep not enabled\");\n const { deadlineAt } = options;\n const run = (args: string[], cwd = root): Promise<RunResult> => {\n const ms =\n deadlineAt === undefined\n ? config.timeoutMs\n : Math.min(config.timeoutMs, deadlineAt - Date.now());\n if (ms <= 0) return Promise.resolve({ stdout: \"\", stderr: \"\", error: deadlineError() });\n return runZg(config.binary, args, cwd, ms);\n };\n const missedDeadline = (status: ZvecStatus) =>\n deadlineAt !== undefined && status === \"timeout\" && Date.now() >= deadlineAt - 5;\n\n const cached = await readCachedReadiness(root, config.binary);\n if (cached !== \"ready\") {\n if (cached) return cached;\n const readiness = await checkReadiness(run);\n if (!(readiness && missedDeadline(readiness.status))) {\n await markReadiness(root, config.binary, readiness);\n }\n if (readiness) return readiness;\n }\n if (options.requireServer && !(await serverRunning(root, config.binary, run))) {\n return none(\"no-server\", \"no running `zg server` (this path never starts one)\");\n }\n\n // zg ranks chunks, and one file can own many of them: oversample chunks so\n // `limit` distinct files survive de-duplication.\n const limit = Math.min(20, Math.max(1, Math.floor(options.limit ?? 10)));\n const chunkLimit = limit * 8;\n const globs = zvecExcludeGlobs(task);\n const query = [\n \"query\",\n \"--vector\",\n task,\n \"--mode\",\n \"auto\",\n \"--refresh\",\n \"off\",\n \"--limit\",\n String(chunkLimit),\n ...globs.flatMap((glob) => [\"-g\", glob]),\n ];\n // The card workspace is queried in parallel; it never delays or fails the code query.\n const cardQuery = queryCards(root, task, limit, (args, cwd) => run(args, cwd));\n let result = await run(query);\n // A background `zg index` (project sync) holds the lock for a moment: one retry.\n if (\n result.error &&\n isBusy(result) &&\n (deadlineAt === undefined || deadlineAt - Date.now() > 400)\n ) {\n await new Promise((resolveWait) => setTimeout(resolveWait, BUSY_RETRY_MS));\n result = await run(query);\n }\n if (result.error) {\n const failure = fromError(result.error, \"zvec-grep query\", result.stderr);\n if (!missedDeadline(failure.status) && failure.status !== \"busy\") await resetZvecCache(root);\n return failure;\n }\n\n const hits = parseHits(result.stdout).slice(0, chunkLimit);\n const cardPaths = await cardQuery;\n // With no index, zg answers with a \"run zg index\" hint instead of hits.\n if (hits.length === 0 && /\\bzg index\\b/.test(result.stdout)) {\n const notReady = none(\"index-not-ready\", NOT_READY);\n await markReadiness(root, config.binary, notReady);\n return notReady;\n }\n const indexedAt = await readIndexedAt(root);\n const paths: string[] = [];\n const candidates: RetrievalHit[] = [];\n const seen = new Set<string>();\n let stale = 0;\n let changed = 0;\n for (const hit of hits) {\n const path = toRepoRelative(root, hit.path);\n if (!path) continue;\n // Later chunks of an accepted file keep their spans; a rejected file stays rejected.\n if (seen.has(path)) {\n if (paths.includes(path)) candidates.push({ ...hit, path });\n continue;\n }\n if (paths.length >= limit) continue;\n seen.add(path);\n if (EXCLUDED_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;\n try {\n const info = await stat(resolve(root, path));\n if (!info.isFile()) continue;\n // ctime too: a rename over the path keeps the old mtime.\n if (indexedAt !== null && Math.max(info.mtimeMs, info.ctimeMs) > indexedAt) {\n changed++;\n continue;\n }\n } catch {\n stale++;\n continue;\n }\n paths.push(path);\n candidates.push({ ...hit, path });\n }\n return {\n status: \"ok\",\n paths,\n hits: candidates,\n stale,\n changed,\n minScore: config.minScore,\n ...(cardPaths.length > 0 ? { cardPaths } : {}),\n reason:\n `zvec-grep vector search: ${paths.length} file(s)` +\n (globs.length > 0 ? \"; non-source paths excluded\" : \"\") +\n (stale > 0 ? `, ${stale} stale hit(s) dropped` : \"\") +\n (changed > 0 ? `, ${changed} hit(s) changed since the zg index dropped` : \"\") +\n (cardPaths.length > 0 ? `; ${cardPaths.length} intent card match(es)` : \"\") +\n (indexedAt === null\n ? \"; index age unknown, hits unverified (run `codebuddy zvec index`)\"\n : \"\"),\n };\n}\n\nexport type ZvecRefresh = { status: ZvecStatus; reason: string };\n\n/**\n * Incrementally refresh an existing zg index (no `--rebuild`, no embedding\n * flag) and record when it started, which is the freshness bound applied to\n * later hits. Never builds a missing index and never starts a zg server; every\n * failure is returned as a status for the caller to log.\n */\nexport async function refreshZvecIndex(\n repositoryRoot: string,\n options: { signal?: AbortSignal } = {},\n): Promise<ZvecRefresh> {\n const root = resolve(repositoryRoot);\n const config = await zvecConfig(root);\n if (!config.enabled) return { status: \"disabled\", reason: \"zvec-grep not enabled\" };\n const readiness = await checkReadiness((args) =>\n runZg(config.binary, args, root, config.timeoutMs),\n );\n if (readiness) return { status: readiness.status, reason: readiness.reason };\n const startedAt = Date.now();\n const result = await runZg(\n config.binary,\n [\"index\", root, \"--mode\", \"auto\"],\n root,\n REFRESH_TIMEOUT_MS,\n options.signal,\n );\n if (result.error) {\n const failure = fromError(result.error, \"zvec-grep refresh\");\n return { status: failure.status, reason: failure.reason };\n }\n await markIndexed(root, startedAt);\n return { status: \"ok\", reason: \"zvec-grep index refreshed\" };\n}\n\n/** Extract bounded, validated line ranges in retrieval rank order. */\nexport function parseHits(output: string): RetrievalHit[] {\n return [...output.matchAll(/^#(\\d+) matchedBy=\\S+ (.+?):(\\d+)(?:-(\\d+))?\\s*$/gm)]\n .flatMap((match) => {\n const rank = Number(match[1]);\n const startLine = Number(match[3]);\n const endLine = Number(match[4] ?? match[3]);\n if (startLine < 1 || endLine < startLine) return [];\n return [\n {\n path: match[2] as string,\n rank,\n startLine,\n endLine,\n source: \"zvec\" as const,\n freshness: \"unverified\" as const,\n },\n ];\n })\n .sort((left, right) => left.rank - right.rank);\n}\n\n/** Forget the cached readiness check, e.g. after `zg index` or a failed query. */\nexport async function resetZvecCache(repositoryRoot: string): Promise<void> {\n await rm(readyPath(resolve(repositoryRoot)), { force: true });\n}\n\nexport type ZvecReport = {\n enabled: boolean;\n binary: string;\n version: string | null;\n supported: boolean;\n indexReady: boolean;\n server: \"running\" | \"stopped\" | \"unknown\";\n reason: string;\n};\n\n/** Everything `codebuddy zvec status` shows. Read-only; never builds an index. */\nexport async function zvecStatus(repositoryRoot: string): Promise<ZvecReport> {\n const root = resolve(repositoryRoot);\n const config = await zvecConfig(root);\n const run = (args: string[]) =>\n runZg(config.binary, args, root, Math.max(config.timeoutMs, 5_000));\n const version = await run([\"version\"]);\n const found = version.error ? null : (version.stdout.trim().split(/\\s+/).pop() ?? null);\n const supported = found !== null && SUPPORTED.test(found);\n const readiness = supported ? await checkReadiness(run) : null;\n const server = supported ? await run([\"server\", \"status\"]) : null;\n return {\n enabled: config.enabled,\n binary: config.binary,\n version: found,\n supported,\n indexReady: supported && readiness === null,\n server:\n server === null || server.error\n ? \"unknown\"\n : /running|ready/i.test(server.stdout)\n ? \"running\"\n : \"stopped\",\n reason: version.error\n ? fromError(version.error, \"zvec-grep version check\").reason\n : !supported\n ? `zvec-grep ${found ?? \"unknown\"} is not 0.2.x`\n : (readiness?.reason ?? \"ready\"),\n };\n}\n\n/**\n * Build or incrementally refresh the zg index for this repository, streaming\n * zg's progress. Explicit by design: CodeBuddy never indexes implicitly.\n */\nexport async function buildZvecIndex(\n repositoryRoot: string,\n options: {\n model?: string;\n rebuild?: boolean /** Discard zg's progress output. */;\n quiet?: boolean;\n } = {},\n): Promise<number> {\n const root = resolve(repositoryRoot);\n const config = await zvecConfig(root);\n const args = [\n \"index\",\n root,\n \"--embedding\",\n options.model ?? DEFAULT_ZVEC_MODEL,\n \"--mode\",\n \"auto\",\n ];\n if (options.rebuild) args.push(\"--rebuild\");\n const startedAt = Date.now();\n const code = await new Promise<number>((resolveRun, reject) => {\n const child = spawn(config.binary, args, {\n cwd: root,\n stdio: options.quiet ? \"ignore\" : \"inherit\",\n });\n child.on(\"error\", reject);\n child.on(\"close\", (exit) => resolveRun(exit ?? 1));\n });\n await resetZvecCache(root);\n if (code === 0) {\n // zg's index is binary and rebuildable: keep it out of git (and so out of\n // the uncommitted-file targets) without touching the project's .gitignore.\n await writeFile(join(root, \".zvec-grep\", \".gitignore\"), \"*\\n\", { flag: \"wx\" }).catch(() => {});\n await markIndexed(root, startedAt);\n }\n return code;\n}\n\nconst NOT_READY = \"zvec-grep index missing or not ready; run `codebuddy zvec index`\";\n\n/**\n * Intent cards live in their own zg workspace (`.codebuddy/cache/cards/`, one\n * Markdown file per source file at `<path>.md`), so the project's ignore rules\n * never hide them and the code index never sees them. Cards are plain English,\n * so they get an English sentence model instead of the code model.\n */\nexport const CARDS_DIR = join(\".codebuddy\", \"cache\", \"cards\");\nexport const DEFAULT_CARD_MODEL = \"local/bge-small-en-v1.5\";\n\n/** Source paths whose card matched the task; empty when there is no card index or it fails. */\nasync function queryCards(\n root: string,\n task: string,\n limit: number,\n run: (args: string[], cwd: string) => Promise<RunResult>,\n): Promise<string[]> {\n const cards = join(root, CARDS_DIR);\n if (!(await stat(join(cards, \".zvec-grep\")).catch(() => null))) return [];\n const result = await run(\n [\"query\", \"--vector\", task, \"--mode\", \"auto\", \"--refresh\", \"off\", \"--limit\", String(limit)],\n cards,\n );\n if (result.error) return [];\n const paths: string[] = [];\n for (const hit of parseHits(result.stdout)) {\n const card = toRepoRelative(cards, hit.path);\n if (!card?.endsWith(\".md\")) continue;\n const path = card.slice(0, -3);\n if (paths.includes(path)) continue;\n if (!(await stat(resolve(root, path)).catch(() => null))?.isFile()) continue;\n paths.push(path);\n }\n return paths.slice(0, limit);\n}\n\n/**\n * Build or refresh the card workspace's zg index (background only: after a\n * card pass). No-op without cards or with zvec off; never throws.\n */\nexport async function refreshZvecCardIndex(\n repositoryRoot: string,\n options: { signal?: AbortSignal } = {},\n): Promise<ZvecRefresh> {\n const root = resolve(repositoryRoot);\n const config = await zvecConfig(root);\n if (!config.enabled) return { status: \"disabled\", reason: \"zvec-grep not enabled\" };\n const cards = join(root, CARDS_DIR);\n if (!(await stat(cards).catch(() => null)))\n return { status: \"ok\", reason: \"no intent cards yet\" };\n const exists = await stat(join(cards, \".zvec-grep\")).catch(() => null);\n const result = await runZg(\n config.binary,\n [\n \"index\",\n cards,\n ...(exists ? [] : [\"--embedding\", config.cardModel]),\n \"--no-ignore\",\n \"--mode\",\n \"auto\",\n ],\n cards,\n REFRESH_TIMEOUT_MS,\n options.signal,\n );\n if (result.error) {\n const failure = fromError(result.error, \"zvec-grep card index\", result.stderr);\n return { status: failure.status, reason: failure.reason };\n }\n await writeFile(join(cards, \".zvec-grep\", \".gitignore\"), \"*\\n\", { flag: \"wx\" }).catch(() => {});\n return { status: \"ok\", reason: \"zvec-grep card index refreshed\" };\n}\n\n/**\n * Start the shared `zg server` for this project when it is enabled and ready,\n * so the prompt hook (which only queries a running server) gets vector\n * candidates within its deadline. Returns a stop function that stops the\n * server only if this call started it.\n */\nexport async function ensureZvecServer(repositoryRoot: string): Promise<{\n started: boolean;\n reason: string;\n stop: () => Promise<void>;\n}> {\n const root = resolve(repositoryRoot);\n const config = await zvecConfig(root);\n const noop = async () => {};\n if (!config.enabled) return { started: false, reason: \"zvec-grep not enabled\", stop: noop };\n const run = (args: string[]) =>\n runZg(config.binary, args, root, Math.max(config.timeoutMs, 5_000));\n const readiness = await checkReadiness(run);\n if (readiness) return { started: false, reason: readiness.reason, stop: noop };\n const status = await run([\"server\", \"status\"]);\n if (!status.error && /^Server: ready\\b/m.test(status.stdout)) {\n return { started: false, reason: \"zg server already running (reused)\", stop: noop };\n }\n const on = await run([\"server\", \"on\"]);\n // The hook's 30 s server-status cache would otherwise keep saying \"stopped\".\n await rm(serverPath(root), { force: true });\n if (on.error) {\n return {\n started: false,\n reason: fromError(on.error, \"zg server on\", on.stderr).reason,\n stop: noop,\n };\n }\n return {\n started: true,\n reason: \"zg server started\",\n // ponytail: the daemon is shared; a second `codebuddy serve` that reused it\n // loses vectors when this one exits. Refcount via ZVEC_GREP_HOME if that matters.\n stop: async () => {\n await runZg(config.binary, [\"server\", \"off\"], root, 5_000);\n await rm(serverPath(root), { force: true });\n },\n };\n}\n\n/** Version + index readiness. Returns null when ready, else the failure. */\nasync function checkReadiness(\n run: (args: string[]) => Promise<RunResult>,\n): Promise<ZvecCandidates | null> {\n const version = await run([\"version\"]);\n if (version.error) return fromError(version.error, \"zvec-grep version check\");\n const found = version.stdout.trim().split(/\\s+/).pop() ?? \"\";\n if (!SUPPORTED.test(found)) {\n return none(\"unsupported-version\", `zvec-grep ${found || \"unknown\"} is not 0.2.x`);\n }\n const ready = await run([\"status\", \"--check-ready\", \"--mode\", \"auto\"]);\n if (!ready.error) return null;\n return ready.error.code === \"ETIMEDOUT\"\n ? none(\"timeout\", \"zvec-grep status timed out\")\n : none(\"index-not-ready\", NOT_READY);\n}\n\nfunction readyPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"zvec-ready.json\");\n}\n\ntype CachedReadiness = {\n binary?: string;\n checkedAt?: string;\n /** Absent in caches written before failures were cached: those were always ready. */\n failure?: { status: ZvecStatus; reason: string };\n};\n\n/**\n * \"ready\", a cached failure (reason marked as cached), or null when the check\n * must run. A failure is dropped once `.zvec-grep/` changed after it was\n * recorded, i.e. someone (re)built the index outside CodeBuddy.\n */\nasync function readCachedReadiness(\n root: string,\n binary: string,\n): Promise<\"ready\" | ZvecCandidates | null> {\n let cached: CachedReadiness;\n try {\n cached = JSON.parse(await readFile(readyPath(root), \"utf8\")) as CachedReadiness;\n } catch {\n return null;\n }\n const checkedAt = Date.parse(cached.checkedAt ?? \"\");\n const age = Date.now() - checkedAt;\n if (cached.binary !== binary || !(age >= 0)) return null;\n if (!cached.failure) return age <= READY_CACHE_MS ? \"ready\" : null;\n if (age > NOT_READY_CACHE_MS) return null;\n const workspace = await stat(join(root, \".zvec-grep\")).catch(() => null);\n if (workspace && workspace.mtimeMs > checkedAt) return null;\n const { status, reason } = cached.failure;\n return none(status, `${reason}; cached check from ${Math.round(age / 1000)}s ago`);\n}\n\n/** Cache a readiness result: null = ready. */\nasync function markReadiness(\n root: string,\n binary: string,\n failure: ZvecCandidates | null,\n): Promise<void> {\n try {\n const path = readyPath(root);\n await mkdir(dirname(path), { recursive: true, mode: 0o700 });\n const record: CachedReadiness = {\n binary,\n checkedAt: new Date().toISOString(),\n ...(failure ? { failure: { status: failure.status, reason: failure.reason } } : {}),\n };\n await writeAtomic(path, `${JSON.stringify(record)}\\n`);\n } catch {\n // A read-only cache only costs a repeated readiness check.\n }\n}\n\nfunction indexedPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"zvec-index.json\");\n}\n\n/** When the zg index was last refreshed by CodeBuddy, or null if unknown. */\nasync function readIndexedAt(root: string): Promise<number | null> {\n try {\n const parsed = JSON.parse(await readFile(indexedPath(root), \"utf8\")) as { indexedAt?: string };\n const at = Date.parse(parsed.indexedAt ?? \"\");\n return Number.isFinite(at) ? at : null;\n } catch {\n return null;\n }\n}\n\nasync function markIndexed(root: string, at: number): Promise<void> {\n try {\n const path = indexedPath(root);\n await mkdir(dirname(path), { recursive: true, mode: 0o700 });\n await writeAtomic(path, `${JSON.stringify({ indexedAt: new Date(at).toISOString() })}\\n`);\n } catch {\n // Without the record, later hits are reported as unverified, not trusted.\n }\n}\n\nasync function zvecConfig(root: string): Promise<{\n enabled: boolean;\n binary: string;\n timeoutMs: number;\n minScore: number;\n cardModel: string;\n}> {\n let file: Awaited<ReturnType<typeof readConfigFile>> = {};\n try {\n file = await readConfigFile(root);\n } catch {\n // A broken config must not break context assembly; treat zvec as disabled.\n }\n const env = process.env.CODEBUDDY_ZVEC;\n return {\n enabled: env !== undefined ? env === \"1\" || env === \"true\" : (file.zvec?.enabled ?? false),\n binary: file.zvec?.binary ?? \"zg\",\n timeoutMs: file.zvec?.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n minScore: file.zvec?.minScore ?? DEFAULT_ZVEC_MIN_SCORE,\n cardModel: file.zvec?.cardModel ?? DEFAULT_CARD_MODEL,\n };\n}\n\ntype RunResult = {\n stdout: string;\n stderr: string;\n error: (Error & { code?: string | number | null | undefined }) | null;\n};\n\nfunction runZg(\n binary: string,\n args: string[],\n cwd: string,\n timeoutMs: number,\n signal?: AbortSignal,\n): Promise<RunResult> {\n return new Promise((resolveRun) => {\n execFile(\n binary,\n args,\n {\n cwd,\n timeout: timeoutMs,\n maxBuffer: 1024 * 1024,\n killSignal: \"SIGKILL\",\n ...(signal ? { signal } : {}),\n },\n (error, stdout, stderr) => {\n if (error && (error as { killed?: boolean }).killed) {\n resolveRun({\n stdout: \"\",\n stderr: String(stderr),\n error: Object.assign(error, { code: \"ETIMEDOUT\" }),\n });\n return;\n }\n resolveRun({ stdout: String(stdout), stderr: String(stderr), error });\n },\n );\n });\n}\n\nfunction deadlineError(): NonNullable<RunResult[\"error\"]> {\n return Object.assign(new Error(\"hook deadline reached\"), { code: \"ETIMEDOUT\" });\n}\n\nfunction serverPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"zvec-server.json\");\n}\n\n/** `zg server status` says ready; the answer (either way) is reused for 30 s. */\nasync function serverRunning(\n root: string,\n binary: string,\n run: (args: string[]) => Promise<RunResult>,\n): Promise<boolean> {\n try {\n const cached = JSON.parse(await readFile(serverPath(root), \"utf8\")) as {\n binary?: string;\n checkedAt?: number;\n running?: boolean;\n };\n const age = Date.now() - (cached.checkedAt ?? 0);\n if (cached.binary === binary && age >= 0 && age <= NOT_READY_CACHE_MS) {\n return cached.running === true;\n }\n } catch {\n // No record yet: ask zg.\n }\n const status = await run([\"server\", \"status\"]);\n // A deadline miss says nothing about the server: not cached.\n if (status.error?.code === \"ETIMEDOUT\") return false;\n const running = !status.error && /^Server: ready\\b/m.test(status.stdout);\n try {\n await mkdir(dirname(serverPath(root)), { recursive: true, mode: 0o700 });\n await writeAtomic(\n serverPath(root),\n `${JSON.stringify({ binary, checkedAt: Date.now(), running })}\\n`,\n );\n } catch {\n // An unwritable cache only repeats the check.\n }\n return running;\n}\n\nfunction fromError(\n error: NonNullable<RunResult[\"error\"]>,\n what: string,\n stderr = \"\",\n): ZvecCandidates {\n if (error.code === \"ENOENT\") return none(\"not-installed\", \"zvec-grep (`zg`) not found on PATH\");\n if (error.code === \"ETIMEDOUT\") return none(\"timeout\", `${what} timed out`);\n if (BUSY.test(stderr)) return none(\"busy\", `${what}: index busy (a zg index refresh is running)`);\n // zg prints `Code: ZVEC_GREP.…` on failure; keep it so the explain line is diagnosable.\n const code = stderr.match(/\\bCode:\\s*(\\S+)/)?.[1];\n return none(\"failed\", `${what} failed${code ? ` (${code})` : \"\"}`);\n}\n\n/** zg's lock error while another process (a background `zg index`) writes the index. */\nconst BUSY = /ZVEC_GREP\\.ENGINE\\.LOCK\\.BUSY/;\nconst BUSY_RETRY_MS = 150;\n\nfunction isBusy(result: RunResult): boolean {\n return BUSY.test(result.stderr);\n}\n\nfunction none(status: ZvecStatus, reason: string): ZvecCandidates {\n return { status, paths: [], hits: [], reason, stale: 0, changed: 0 };\n}\n\nfunction toRepoRelative(root: string, path: string): string | null {\n const absolute = isAbsolute(path) ? path : resolve(root, path);\n const rel = relative(root, absolute);\n if (rel === \"\" || rel === \"..\" || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join(\"/\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -58,50 +58,8 @@ function clampBudgetForCallerModel(requestedBudget, callerModel) {
|
|
|
58
58
|
return { budget: requestedBudget, clamped: false };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// src/core/ids.ts
|
|
62
|
-
import { createHash, randomBytes } from "crypto";
|
|
63
|
-
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
64
|
-
function generateUlid(now = Date.now()) {
|
|
65
|
-
let timePart = "";
|
|
66
|
-
let ts = now;
|
|
67
|
-
for (let i = 0; i < 10; i++) {
|
|
68
|
-
timePart = (CROCKFORD[ts % 32] ?? "0") + timePart;
|
|
69
|
-
ts = Math.floor(ts / 32);
|
|
70
|
-
}
|
|
71
|
-
const bytes = randomBytes(10);
|
|
72
|
-
let randPart = "";
|
|
73
|
-
for (let i = 0; i < 16; i++) {
|
|
74
|
-
const bitOffset = i * 5;
|
|
75
|
-
const byteIndex = Math.floor(bitOffset / 8);
|
|
76
|
-
const bitInByte = bitOffset % 8;
|
|
77
|
-
const hi = bytes[byteIndex] ?? 0;
|
|
78
|
-
const lo = bytes[byteIndex + 1] ?? 0;
|
|
79
|
-
const combined = (hi << 8 | lo) >>> 0;
|
|
80
|
-
const shift = 16 - bitInByte - 5;
|
|
81
|
-
const value = combined >>> shift & 31;
|
|
82
|
-
randPart += CROCKFORD[value] ?? "0";
|
|
83
|
-
}
|
|
84
|
-
return timePart + randPart;
|
|
85
|
-
}
|
|
86
|
-
function generateSessionId(now) {
|
|
87
|
-
return `sess_${generateUlid(now).toLowerCase()}`;
|
|
88
|
-
}
|
|
89
|
-
function generateEntityId(prefix, now) {
|
|
90
|
-
return `${prefix}_${generateUlid(now).toLowerCase()}`;
|
|
91
|
-
}
|
|
92
|
-
function computeContentHash(namespace, sessionId, content) {
|
|
93
|
-
return createHash("sha256").update(`${namespace}${sessionId}${content}`).digest("hex");
|
|
94
|
-
}
|
|
95
|
-
function computeFactHash(namespace, content) {
|
|
96
|
-
return createHash("sha256").update(`${namespace}fact${content}`).digest("hex");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
61
|
export {
|
|
100
62
|
countTokens,
|
|
101
|
-
clampBudgetForCallerModel
|
|
102
|
-
generateSessionId,
|
|
103
|
-
generateEntityId,
|
|
104
|
-
computeContentHash,
|
|
105
|
-
computeFactHash
|
|
63
|
+
clampBudgetForCallerModel
|
|
106
64
|
};
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
65
|
+
//# sourceMappingURL=chunk-57642NGU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/planner/budget.ts"],"sourcesContent":["import { Tiktoken } from \"js-tiktoken/lite\";\nimport cl100kBase from \"js-tiktoken/ranks/cl100k_base\";\nimport { getModelContextWindow } from \"../providers/models.js\";\n\nlet encoder: Tiktoken | null = null;\n\nfunction getEncoder(): Tiktoken {\n if (!encoder) {\n encoder = new Tiktoken(cl100kBase);\n }\n return encoder;\n}\n\n/** cl100k pre-tokenizer: BPE merges never cross these pieces, so counts add up. */\nconst PIECE = new RegExp(cl100kBase.pat_str, \"gu\");\nconst SPECIAL_TOKENS = Object.keys(cl100kBase.special_tokens);\n// ponytail: cleared when full rather than LRU; source and JSON pieces repeat\n// heavily, so a simple bound keeps the hit rate high.\nconst MAX_CACHED_PIECES = 200_000;\nconst pieceTokens = new Map<string, number>();\nconst MAX_PIECE_CHARS = 256;\n\n/**\n * Exact cl100k token count. A context request tokenizes nearly the same JSON\n * several times (stats, stages, budget fixed point, delivery), so BPE results\n * are memoized per pre-tokenized piece; the sum equals `encode(text).length`.\n */\nexport function countTokens(text: string): number {\n if (!text) return 0;\n try {\n // `encode` rejects special-token text; keep the same fallback for it.\n if (SPECIAL_TOKENS.some((token) => text.includes(token))) throw new Error(\"special token\");\n let total = 0;\n for (const [whole] of text.matchAll(PIECE)) {\n // BPE is quadratic in piece length: one 32 KB run of \"x\" took over a\n // minute and blocked the MCP server. Long pieces are counted in chunks.\n // ponytail: may over-count by ≤ 1 token per chunk on pieces > 256 chars\n // (real text almost never has them); exact again if js-tiktoken gets linear.\n for (let start = 0; start < whole.length; start += MAX_PIECE_CHARS) {\n const piece = whole.slice(start, start + MAX_PIECE_CHARS);\n let count = pieceTokens.get(piece);\n if (count === undefined) {\n count = getEncoder().encode(piece).length;\n if (pieceTokens.size >= MAX_CACHED_PIECES) pieceTokens.clear();\n pieceTokens.set(piece, count);\n }\n total += count;\n }\n }\n return total;\n } catch {\n return Math.max(1, Math.ceil(text.length / 4));\n }\n}\n\nexport type BudgetClampResult = {\n budget: number;\n clamped: boolean;\n reason?: string;\n};\n\nexport function clampBudgetForCallerModel(\n requestedBudget: number,\n callerModel: string | undefined,\n): BudgetClampResult {\n if (!callerModel) {\n return { budget: requestedBudget, clamped: false };\n }\n const ctx = getModelContextWindow(callerModel);\n if (!ctx) {\n return { budget: requestedBudget, clamped: false };\n }\n const headroom = Math.floor(ctx * 0.75);\n if (requestedBudget > headroom) {\n return {\n budget: headroom,\n clamped: true,\n reason: `budget ${requestedBudget} exceeds 75% of ${callerModel} context window (${ctx}); clamped to ${headroom}.`,\n };\n }\n return { budget: requestedBudget, clamped: false };\n}\n"],"mappings":";;;;;AAAA,SAAS,gBAAgB;AACzB,OAAO,gBAAgB;AAGvB,IAAI,UAA2B;AAE/B,SAAS,aAAuB;AAC9B,MAAI,CAAC,SAAS;AACZ,cAAU,IAAI,SAAS,UAAU;AAAA,EACnC;AACA,SAAO;AACT;AAGA,IAAM,QAAQ,IAAI,OAAO,WAAW,SAAS,IAAI;AACjD,IAAM,iBAAiB,OAAO,KAAK,WAAW,cAAc;AAG5D,IAAM,oBAAoB;AAC1B,IAAM,cAAc,oBAAI,IAAoB;AAC5C,IAAM,kBAAkB;AAOjB,SAAS,YAAY,MAAsB;AAChD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AAEF,QAAI,eAAe,KAAK,CAAC,UAAU,KAAK,SAAS,KAAK,CAAC,EAAG,OAAM,IAAI,MAAM,eAAe;AACzF,QAAI,QAAQ;AACZ,eAAW,CAAC,KAAK,KAAK,KAAK,SAAS,KAAK,GAAG;AAK1C,eAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,iBAAiB;AAClE,cAAM,QAAQ,MAAM,MAAM,OAAO,QAAQ,eAAe;AACxD,YAAI,QAAQ,YAAY,IAAI,KAAK;AACjC,YAAI,UAAU,QAAW;AACvB,kBAAQ,WAAW,EAAE,OAAO,KAAK,EAAE;AACnC,cAAI,YAAY,QAAQ,kBAAmB,aAAY,MAAM;AAC7D,sBAAY,IAAI,OAAO,KAAK;AAAA,QAC9B;AACA,iBAAS;AAAA,MACX;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,SAAS,CAAC,CAAC;AAAA,EAC/C;AACF;AAQO,SAAS,0BACd,iBACA,aACmB;AACnB,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,QAAQ,iBAAiB,SAAS,MAAM;AAAA,EACnD;AACA,QAAM,MAAM,sBAAsB,WAAW;AAC7C,MAAI,CAAC,KAAK;AACR,WAAO,EAAE,QAAQ,iBAAiB,SAAS,MAAM;AAAA,EACnD;AACA,QAAM,WAAW,KAAK,MAAM,MAAM,IAAI;AACtC,MAAI,kBAAkB,UAAU;AAC9B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,QAAQ,UAAU,eAAe,mBAAmB,WAAW,oBAAoB,GAAG,iBAAiB,QAAQ;AAAA,IACjH;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,iBAAiB,SAAS,MAAM;AACnD;","names":[]}
|