@orquentra/codebuddy 2.9.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/CLAUDE_CODEX.md +33 -0
- package/README.md +114 -9
- package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
- package/dist/{chunk-GXOML4UZ.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-GKNYMNFM.js → chunk-7IPSDDO5.js} +10 -3
- package/dist/chunk-7IPSDDO5.js.map +1 -0
- package/dist/chunk-BTTQWSDS.js +372 -0
- package/dist/chunk-BTTQWSDS.js.map +1 -0
- package/dist/{chunk-2WH2Q4TA.js → chunk-D5E7SGI5.js} +304 -77
- 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-GQKWUHMA.js +20 -0
- package/dist/chunk-GQKWUHMA.js.map +1 -0
- package/dist/chunk-ITXE27SZ.js +358 -0
- package/dist/chunk-ITXE27SZ.js.map +1 -0
- package/dist/{chunk-UK6XTS2Y.js → chunk-JJQDVGF7.js} +23 -23
- package/dist/chunk-JJQDVGF7.js.map +1 -0
- package/dist/{chunk-F4BJ3DXW.js → chunk-KQCP3ZYH.js} +1273 -331
- package/dist/chunk-KQCP3ZYH.js.map +1 -0
- package/dist/{chunk-QN76UJP7.js → chunk-KYRM3HPP.js} +89 -21
- package/dist/chunk-KYRM3HPP.js.map +1 -0
- package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
- package/dist/chunk-M75SN4D4.js.map +1 -0
- package/dist/{chunk-J4T6HHI4.js → chunk-RZ266NHE.js} +538 -317
- package/dist/chunk-RZ266NHE.js.map +1 -0
- 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-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
- package/dist/chunk-UOTKHSQ5.js.map +1 -0
- package/dist/{chunk-TLWGZVPC.js → chunk-UR3DF74I.js} +1007 -132
- 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-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
- package/dist/chunk-V7PT6ER5.js.map +1 -0
- package/dist/chunk-WHKXUXHE.js +156 -0
- package/dist/chunk-WHKXUXHE.js.map +1 -0
- package/dist/{chunk-SRA4MDVG.js → chunk-YMXFSHXT.js} +978 -950
- package/dist/chunk-YMXFSHXT.js.map +1 -0
- package/dist/cli/index.js +23 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
- 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.d.ts +6 -4
- package/dist/index.js +25 -23
- package/dist/install-E3JWINTN.js +31 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-MLTAFS5L.js +29 -0
- package/dist/local-75DJECVR.js +48 -0
- package/dist/local-llm-MECVJTVU.js +19 -0
- package/dist/{main-YJKSYFPT.js → main-RLMKQZW4.js} +999 -328
- 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-OG4ISWIM.js → operations-STR7S5UO.js} +8 -7
- package/dist/operations-STR7S5UO.js.map +1 -0
- package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
- package/dist/pretool-command-B5GY5T6E.js.map +1 -0
- package/dist/prompt-gate-TDTFFQW2.js +8 -0
- package/dist/prompt-gate-TDTFFQW2.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-GCSEIAVD.js → runtime-SWFWGTQ4.js} +18 -9
- 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-XDZNZ2OL.js → zvec-F73IBWO5.js} +13 -6
- package/dist/zvec-F73IBWO5.js.map +1 -0
- package/package.json +13 -3
- package/dist/chunk-2WH2Q4TA.js.map +0 -1
- package/dist/chunk-6NZEZJYD.js +0 -206
- package/dist/chunk-6NZEZJYD.js.map +0 -1
- package/dist/chunk-CAEPHLHC.js.map +0 -1
- package/dist/chunk-ENKDFLOO.js.map +0 -1
- package/dist/chunk-ESZXCZIN.js.map +0 -1
- package/dist/chunk-F4BJ3DXW.js.map +0 -1
- package/dist/chunk-GKNYMNFM.js.map +0 -1
- package/dist/chunk-GXOML4UZ.js.map +0 -1
- package/dist/chunk-ITQO632W.js +0 -673
- package/dist/chunk-ITQO632W.js.map +0 -1
- package/dist/chunk-J4T6HHI4.js.map +0 -1
- package/dist/chunk-KNR2P2UY.js.map +0 -1
- package/dist/chunk-QN76UJP7.js.map +0 -1
- package/dist/chunk-SFMIMLXR.js +0 -63
- package/dist/chunk-SFMIMLXR.js.map +0 -1
- package/dist/chunk-SRA4MDVG.js.map +0 -1
- package/dist/chunk-TLWGZVPC.js.map +0 -1
- package/dist/chunk-UK6XTS2Y.js.map +0 -1
- package/dist/chunk-W7GAXL6C.js.map +0 -1
- package/dist/config-GEZFLXK6.js +0 -12
- package/dist/install-BXM56QCD.js +0 -19
- package/dist/lean-command-7DFW32MA.js +0 -19
- package/dist/main-YJKSYFPT.js.map +0 -1
- package/dist/summaries-ML53R7EL.js +0 -21
- /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
- /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
- /package/dist/{install-BXM56QCD.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
- /package/dist/{lean-command-7DFW32MA.js.map → index-facts-4MTEHKMN.js.map} +0 -0
- /package/dist/{operations-OG4ISWIM.js.map → index-pass-TBDUEZCB.js.map} +0 -0
- /package/dist/{pretool-command-PRFOIHMW.js.map → install-E3JWINTN.js.map} +0 -0
- /package/dist/{runtime-GCSEIAVD.js.map → lean-MLTAFS5L.js.map} +0 -0
- /package/dist/{summaries-ML53R7EL.js.map → local-75DJECVR.js.map} +0 -0
- /package/dist/{zvec-XDZNZ2OL.js.map → local-llm-MECVJTVU.js.map} +0 -0
|
@@ -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":[]}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
redactLlmKeys
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WHKXUXHE.js";
|
|
4
4
|
import {
|
|
5
5
|
writeAtomic
|
|
6
6
|
} from "./chunk-CG5B3LYG.js";
|
|
7
|
+
import {
|
|
8
|
+
localChatJson
|
|
9
|
+
} from "./chunk-BTTQWSDS.js";
|
|
7
10
|
|
|
8
11
|
// src/llm/budget.ts
|
|
9
12
|
import { mkdir, readFile } from "fs/promises";
|
|
@@ -64,6 +67,7 @@ async function storeResponse(root, key, value) {
|
|
|
64
67
|
var DEFAULT_COOLDOWN_SECONDS = 60;
|
|
65
68
|
async function chatJson(config, request) {
|
|
66
69
|
const started = Date.now();
|
|
70
|
+
if (config.local) return localCall(config.local, request);
|
|
67
71
|
try {
|
|
68
72
|
return await call(config, request, started);
|
|
69
73
|
} catch (error) {
|
|
@@ -121,7 +125,9 @@ async function call(config, request, started) {
|
|
|
121
125
|
],
|
|
122
126
|
temperature: 0,
|
|
123
127
|
max_tokens: request.maxTokens,
|
|
124
|
-
response_format: { type: "json_object" }
|
|
128
|
+
response_format: { type: "json_object" },
|
|
129
|
+
// Qwen3 reasons by default on some hosts; cards and reranks need none.
|
|
130
|
+
...config.provider === "groq" && /^qwen\//.test(config.model) ? { reasoning_effort: "none" } : {}
|
|
125
131
|
}),
|
|
126
132
|
signal: AbortSignal.timeout(request.deadlineMs)
|
|
127
133
|
});
|
|
@@ -176,7 +182,28 @@ async function call(config, request, started) {
|
|
|
176
182
|
cached: false
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
|
-
function
|
|
185
|
+
async function localCall(spec, request) {
|
|
186
|
+
const started = Date.now();
|
|
187
|
+
const cacheKey = request.cacheKey && `local:${spec.id}:${request.cacheKey}`;
|
|
188
|
+
if (cacheKey) {
|
|
189
|
+
const parsed = request.schema.safeParse(await cachedResponse(request.repositoryRoot, cacheKey));
|
|
190
|
+
if (parsed.success) {
|
|
191
|
+
return {
|
|
192
|
+
ok: true,
|
|
193
|
+
value: parsed.data,
|
|
194
|
+
model: spec.id,
|
|
195
|
+
latencyMs: Date.now() - started,
|
|
196
|
+
tokens: 0,
|
|
197
|
+
cached: true
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const result = await localChatJson(spec, request);
|
|
202
|
+
if (result.ok && cacheKey) await storeResponse(request.repositoryRoot, cacheKey, result.value);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
function parseJsonContent(raw) {
|
|
206
|
+
const content = raw.replace(/<think>[\s\S]*?<\/think>/g, "");
|
|
180
207
|
const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
181
208
|
try {
|
|
182
209
|
return JSON.parse((fenced ?? content).trim());
|
|
@@ -188,7 +215,8 @@ function estimateTokens(request, content) {
|
|
|
188
215
|
return Math.ceil((request.system.length + request.user.length + content.length) / 4);
|
|
189
216
|
}
|
|
190
217
|
function redact(config, text) {
|
|
191
|
-
|
|
218
|
+
const redacted = redactLlmKeys(text);
|
|
219
|
+
return config.apiKey ? redacted.split(config.apiKey).join("[REDACTED]") : redacted;
|
|
192
220
|
}
|
|
193
221
|
|
|
194
222
|
export {
|
|
@@ -196,4 +224,4 @@ export {
|
|
|
196
224
|
coolingDown,
|
|
197
225
|
chatJson
|
|
198
226
|
};
|
|
199
|
-
//# sourceMappingURL=chunk-
|
|
227
|
+
//# sourceMappingURL=chunk-6YI2D5F5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/llm/budget.ts","../src/llm/client.ts"],"sourcesContent":["import { mkdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\n\n/**\n * Free-tier hygiene for the LLM layer, all in `.codebuddy/cache/` (gitignored,\n * rebuildable): a per-day token ledger per provider:model, a cooldown after\n * 429s, and a response cache. Background work stops at 70% of the daily\n * budget so prompt-time calls keep a reserved 30%.\n *\n * ponytail: read-modify-write JSON without a lock; two hook processes can lose\n * one update. Fine for a soft budget; add the staging lock if it ever matters.\n */\nconst BACKGROUND_SHARE = 0.7;\nconst CACHE_LIMIT = 2_000;\n\nconst file = (root: string, name: string) => join(root, \".codebuddy\", \"cache\", name);\n\nasync function readJson<T>(path: string, fallback: T): Promise<T> {\n try {\n return JSON.parse(await readFile(path, \"utf8\")) as T;\n } catch {\n return fallback;\n }\n}\n\nasync function writeJson(path: string, value: unknown): Promise<void> {\n await mkdir(join(path, \"..\"), { recursive: true });\n await writeAtomic(path, `${JSON.stringify(value)}\\n`);\n}\n\ntype Ledger = { day: string; used: Record<string, number> };\nconst utcDay = (now: Date) => now.toISOString().slice(0, 10);\n\nexport async function usageToday(root: string, now = new Date()): Promise<Record<string, number>> {\n const ledger = await readJson<Ledger>(file(root, \"llm-usage.json\"), { day: \"\", used: {} });\n return ledger.day === utcDay(now) ? ledger.used : {};\n}\n\nexport async function recordUsage(\n root: string,\n key: string,\n tokens: number,\n now = new Date(),\n): Promise<void> {\n const used = await usageToday(root, now);\n used[key] = (used[key] ?? 0) + tokens;\n await writeJson(file(root, \"llm-usage.json\"), { day: utcDay(now), used });\n}\n\n/** Whether `estimate` more tokens fit today's share for this purpose. */\nexport async function reserveBudget(\n root: string,\n key: string,\n purpose: \"prompt\" | \"background\",\n estimate: number,\n dailyBudget: number,\n now = new Date(),\n): Promise<boolean> {\n const limit = purpose === \"background\" ? dailyBudget * BACKGROUND_SHARE : dailyBudget;\n return ((await usageToday(root, now))[key] ?? 0) + estimate <= limit;\n}\n\n/** Milliseconds of cooldown left for this provider:model (0 = none). */\nexport async function coolingDown(root: string, key: string, now = new Date()): Promise<number> {\n const until = (await readJson<Record<string, number>>(file(root, \"llm-cooldown.json\"), {}))[key];\n return until ? Math.max(0, until - now.getTime()) : 0;\n}\n\nexport async function startCooldown(\n root: string,\n key: string,\n seconds: number,\n now = new Date(),\n): Promise<void> {\n const path = file(root, \"llm-cooldown.json\");\n const cooldowns = await readJson<Record<string, number>>(path, {});\n cooldowns[key] = now.getTime() + seconds * 1_000;\n await writeJson(path, cooldowns);\n}\n\nexport async function cachedResponse(root: string, key: string): Promise<unknown | undefined> {\n return (await readJson<Record<string, unknown>>(file(root, \"llm-responses.json\"), {}))[key];\n}\n\n/** Insertion-ordered; the oldest entries go once the cache holds CACHE_LIMIT. */\nexport async function storeResponse(root: string, key: string, value: unknown): Promise<void> {\n const path = file(root, \"llm-responses.json\");\n const cache = await readJson<Record<string, unknown>>(path, {});\n delete cache[key];\n cache[key] = value;\n const keys = Object.keys(cache);\n for (const old of keys.slice(0, Math.max(0, keys.length - CACHE_LIMIT))) delete cache[old];\n await writeJson(path, cache);\n}\n","import type { z } from \"zod\";\nimport {\n cachedResponse,\n coolingDown,\n recordUsage,\n reserveBudget,\n startCooldown,\n storeResponse,\n} from \"./budget.js\";\nimport { type EnabledLlmConfig, redactLlmKeys } from \"./config.js\";\nimport { type LocalModelSpec, localChatJson } from \"./local.js\";\n\n/** Seconds to back off after a 429 without a usable retry-after header. */\nconst DEFAULT_COOLDOWN_SECONDS = 60;\n\n/**\n * One OpenAI-compatible chat call that must answer with JSON (Groq and\n * OpenRouter share the API). It never throws: every failure is a reason the\n * caller turns into today's deterministic behaviour plus an explain line.\n */\nexport type LlmFailure =\n | \"disabled\"\n | \"timeout\"\n | \"rate-limited\"\n | \"cooldown\"\n | \"budget\"\n | \"http\"\n | \"invalid\";\n\nexport type LlmResult<T> =\n | { ok: true; value: T; model: string; latencyMs: number; tokens: number; cached: boolean }\n | { ok: false; reason: LlmFailure; detail: string; latencyMs: number };\n\nexport type ChatJsonRequest<T> = {\n system: string;\n user: string;\n schema: z.ZodType<T>;\n maxTokens: number;\n deadlineMs: number;\n /** Prompt-path calls get the reserved budget share; background work stops earlier. */\n purpose: \"prompt\" | \"background\";\n /**\n * Local backend only: a JSON schema for the generation grammar (with\n * `maxItems`/`maxLength` bounds) instead of one derived from `schema`.\n */\n jsonSchema?: object;\n /** Cache identity (inputs + prompt version); omitted = never cached. */\n cacheKey?: string;\n repositoryRoot: string;\n};\n\nexport async function chatJson<T>(\n config: EnabledLlmConfig,\n request: ChatJsonRequest<T>,\n): Promise<LlmResult<T>> {\n const started = Date.now();\n if (config.local) return localCall(config.local, request);\n try {\n return await call(config, request, started);\n } catch (error) {\n // Structural guarantee: an unexpected body shape is an invalid answer, not a crash.\n return {\n ok: false,\n reason: \"invalid\",\n detail: redact(config, String((error as Error)?.message ?? error)).slice(0, 300),\n latencyMs: Date.now() - started,\n };\n }\n}\n\nasync function call<T>(\n config: EnabledLlmConfig,\n request: ChatJsonRequest<T>,\n started: number,\n): Promise<LlmResult<T>> {\n const fail = (reason: LlmFailure, detail: string): LlmResult<T> => ({\n ok: false,\n reason,\n detail: redact(config, detail).slice(0, 300),\n latencyMs: Date.now() - started,\n });\n const root = request.repositoryRoot;\n const ledgerKey = `${config.provider}:${config.model}`;\n const cooling = await coolingDown(root, ledgerKey);\n if (cooling > 0) return fail(\"cooldown\", `rate limited; retry in ${Math.ceil(cooling / 1000)} s`);\n const cacheKey = request.cacheKey && `${ledgerKey}:${request.cacheKey}`;\n if (cacheKey) {\n const parsed = request.schema.safeParse(await cachedResponse(root, cacheKey));\n if (parsed.success) {\n return {\n ok: true,\n value: parsed.data,\n model: config.model,\n latencyMs: Date.now() - started,\n tokens: 0,\n cached: true,\n };\n }\n }\n const estimate = estimateTokens(request, \"\") + request.maxTokens;\n if (!(await reserveBudget(root, ledgerKey, request.purpose, estimate, config.dailyTokenBudget))) {\n return fail(\"budget\", `daily ${request.purpose} budget for ${ledgerKey} is spent`);\n }\n let response: Response;\n try {\n response = await fetch(`${config.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${config.apiKey}`,\n \"content-type\": \"application/json\",\n ...(config.provider === \"openrouter\" ? { \"x-title\": \"CodeBuddy\" } : {}),\n },\n body: JSON.stringify({\n model: config.model,\n messages: [\n { role: \"system\", content: request.system },\n { role: \"user\", content: request.user },\n ],\n temperature: 0,\n max_tokens: request.maxTokens,\n response_format: { type: \"json_object\" },\n // Qwen3 reasons by default on some hosts; cards and reranks need none.\n ...(config.provider === \"groq\" && /^qwen\\//.test(config.model)\n ? { reasoning_effort: \"none\" }\n : {}),\n }),\n signal: AbortSignal.timeout(request.deadlineMs),\n });\n } catch (error) {\n const name = (error as Error)?.name;\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return fail(\"timeout\", `no answer within ${request.deadlineMs} ms`);\n }\n return fail(\"http\", (error as Error)?.message ?? String(error));\n }\n let body: string;\n try {\n body = await response.text();\n } catch (error) {\n const name = (error as Error)?.name;\n return fail(\n name === \"TimeoutError\" || name === \"AbortError\" ? \"timeout\" : \"http\",\n String(error),\n );\n }\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get(\"retry-after\"));\n await startCooldown(\n root,\n ledgerKey,\n Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : DEFAULT_COOLDOWN_SECONDS,\n );\n return fail(\"rate-limited\", body);\n }\n if (!response.ok) return fail(\"http\", `HTTP ${response.status}: ${body}`);\n\n let completion: {\n model?: unknown;\n choices?: Array<{ message?: { content?: unknown } }>;\n usage?: { total_tokens?: unknown };\n } | null;\n try {\n completion = JSON.parse(body);\n } catch {\n return fail(\"invalid\", \"response is not JSON\");\n }\n const raw = completion?.choices?.[0]?.message?.content;\n if (typeof raw !== \"string\") return fail(\"invalid\", \"answer has no text content\");\n const content = raw;\n const reported = completion?.usage?.total_tokens;\n const tokens =\n typeof reported === \"number\" && Number.isFinite(reported)\n ? reported\n : estimateTokens(request, content);\n await recordUsage(root, ledgerKey, tokens);\n const parsed = request.schema.safeParse(parseJsonContent(content));\n if (!parsed.success) return fail(\"invalid\", `output failed validation: ${content.slice(0, 120)}`);\n if (cacheKey) await storeResponse(root, cacheKey, parsed.data);\n return {\n ok: true,\n value: parsed.data,\n model: typeof completion?.model === \"string\" ? completion.model : config.model,\n latencyMs: Date.now() - started,\n tokens,\n cached: false,\n };\n}\n\n/** Local generation: same response cache, no budget ledger or cooldown (no cost, no quota). */\nasync function localCall<T>(\n spec: LocalModelSpec,\n request: ChatJsonRequest<T>,\n): Promise<LlmResult<T>> {\n const started = Date.now();\n const cacheKey = request.cacheKey && `local:${spec.id}:${request.cacheKey}`;\n if (cacheKey) {\n const parsed = request.schema.safeParse(await cachedResponse(request.repositoryRoot, cacheKey));\n if (parsed.success) {\n return {\n ok: true,\n value: parsed.data,\n model: spec.id,\n latencyMs: Date.now() - started,\n tokens: 0,\n cached: true,\n };\n }\n }\n const result = await localChatJson(spec, request);\n if (result.ok && cacheKey) await storeResponse(request.repositoryRoot, cacheKey, result.value);\n return result;\n}\n\n/** Free models often wrap JSON in a ```json fence despite json_object mode. */\nfunction parseJsonContent(raw: string): unknown {\n // A Qwen3 host may still prepend an (empty) reasoning block.\n const content = raw.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\");\n const fenced = content.match(/```(?:json)?\\s*([\\s\\S]*?)```/i)?.[1];\n try {\n return JSON.parse((fenced ?? content).trim());\n } catch {\n return undefined;\n }\n}\n\nfunction estimateTokens(request: { system: string; user: string }, content: string): number {\n return Math.ceil((request.system.length + request.user.length + content.length) / 4);\n}\n\nfunction redact(config: EnabledLlmConfig, text: string): string {\n const redacted = redactLlmKeys(text);\n return config.apiKey ? redacted.split(config.apiKey).join(\"[REDACTED]\") : redacted;\n}\n"],"mappings":"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","names":["parsed"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
countTokens
|
|
2
|
+
countTokens
|
|
3
|
+
} from "./chunk-57642NGU.js";
|
|
4
|
+
import {
|
|
3
5
|
generateEntityId
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EMWGLKPR.js";
|
|
5
7
|
import {
|
|
6
8
|
writeAtomic
|
|
7
9
|
} from "./chunk-CG5B3LYG.js";
|
|
@@ -69,6 +71,11 @@ var SessionCapsuleLedger = class {
|
|
|
69
71
|
capsules.filter((capsule) => ledger.records[capsule.key]?.hash === digest(capsule.text)).map((capsule) => capsule.key)
|
|
70
72
|
);
|
|
71
73
|
}
|
|
74
|
+
/** Whether anything was delivered in this session yet (a later prompt is a follow-up). */
|
|
75
|
+
async delivered(sessionId, now = /* @__PURE__ */ new Date()) {
|
|
76
|
+
const ledger = await this.read(this.path(sessionId), now);
|
|
77
|
+
return Object.keys(ledger.records).length > 0;
|
|
78
|
+
}
|
|
72
79
|
/**
|
|
73
80
|
* Record capsules as delivered. Call only after their text actually reached
|
|
74
81
|
* the client, so capsules that were clipped or failed to write stay unseen
|
|
@@ -326,4 +333,4 @@ export {
|
|
|
326
333
|
coveringTestCandidates,
|
|
327
334
|
detectTestCommand
|
|
328
335
|
};
|
|
329
|
-
//# sourceMappingURL=chunk-
|
|
336
|
+
//# sourceMappingURL=chunk-7IPSDDO5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/savings/tokens.ts","../src/context/session-ledger.ts","../src/context/tests.ts"],"sourcesContent":["import { countTokens } from \"../planner/budget.js\";\n\n/**\n * The one place token counts are computed. Strings use the same deterministic\n * cl100k tokenizer as planner budgets; objects are measured by their JSON\n * serialization, which matches how they travel over MCP.\n */\n\nexport function estimateTokens(value: unknown): number {\n if (value === undefined || value === null) return 0;\n const text = typeof value === \"string\" ? value : JSON.stringify(value);\n if (!text) return 0;\n return countTokens(text);\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile, rm, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { generateEntityId } from \"../core/ids.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { estimateTokens } from \"../savings/tokens.js\";\n\nconst VERSION = 2;\nconst MAX_CAPSULES = 128;\nconst MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;\nconst MAX_PENDING = 8;\nconst PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\nconst STALE_LOCK_MS = 60_000;\n/** Acknowledged versions kept per section, so switching back to a task reuses them. */\nconst MAX_ACKNOWLEDGED_PER_KEY = 8;\n\ntype LedgerRecord = { hash: string; lastSentAt: string };\n/** A delivery sent to a client that has not yet confirmed receipt. */\ntype PendingDelivery = { createdAt: string; records: Record<string, string> };\n/** One acknowledged version of an `exchange` section: content hash and when it was sent. */\ntype AcknowledgedVersion = { hash: string; sentAt: string };\ntype LedgerFile = {\n version: 2;\n updatedAt: string;\n /** `deliver`/`markSent` capsules (hooks): one current hash per key. */\n records: Record<string, LedgerRecord>;\n pending?: Record<string, PendingDelivery>;\n /** `exchange` sections (MCP): every recently acknowledged version, newest first. */\n acknowledged?: Record<string, AcknowledgedVersion[]>;\n};\n\nexport type ContextCapsule = { key: string; text: string };\nexport type CapsuleDelivery = {\n /** Full text of new or changed capsules, in input order. */\n lines: string[];\n /** Keys of capsules already sent this session and not resent. */\n unchanged: string[];\n full: number;\n referenced: number;\n sentTokens: number;\n avoidedTokens: number;\n};\n\nexport type SectionHash = { key: string; hash: string };\nexport type AcknowledgedExchange = {\n deliveryId: string;\n /** Keys whose content the client confirmed receiving and that have not changed. */\n unchanged: string[];\n ack: \"none\" | \"accepted\" | \"unknown\";\n};\n\nexport class SessionCapsuleLedger {\n private readonly directory: string;\n\n constructor(repositoryRoot = process.cwd()) {\n this.directory = join(resolve(repositoryRoot), \".codebuddy\", \"cache\", \"session\");\n }\n\n async deliver(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<CapsuleDelivery> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n const lines: string[] = [];\n const unchanged: string[] = [];\n let full = 0;\n let referenced = 0;\n let sentTokens = 0;\n let avoidedTokens = 0;\n\n for (const capsule of capsules) {\n const hash = digest(capsule.text);\n const existing = ledger.records[capsule.key];\n if (existing?.hash === hash) {\n unchanged.push(capsule.key);\n referenced++;\n avoidedTokens += estimateTokens(capsule.text);\n } else {\n lines.push(capsule.text);\n full++;\n sentTokens += estimateTokens(capsule.text);\n }\n ledger.records[capsule.key] = { hash, lastSentAt: now.toISOString() };\n }\n\n await this.write(path, ledger, now);\n return { lines, unchanged, full, referenced, sentTokens, avoidedTokens };\n } finally {\n await release();\n }\n }\n\n /** Keys of capsules whose current text was already recorded as sent this session. */\n async seen(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<Set<string>> {\n const ledger = await this.read(this.path(sessionId), now);\n return new Set(\n capsules\n .filter((capsule) => ledger.records[capsule.key]?.hash === digest(capsule.text))\n .map((capsule) => capsule.key),\n );\n }\n\n /** Whether anything was delivered in this session yet (a later prompt is a follow-up). */\n async delivered(sessionId: string, now = new Date()): Promise<boolean> {\n const ledger = await this.read(this.path(sessionId), now);\n return Object.keys(ledger.records).length > 0;\n }\n\n /**\n * Record capsules as delivered. Call only after their text actually reached\n * the client, so capsules that were clipped or failed to write stay unseen\n * and are delivered by a later prompt.\n */\n async markSent(sessionId: string, capsules: ContextCapsule[], now = new Date()): Promise<void> {\n if (capsules.length === 0) return;\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n for (const capsule of capsules) {\n ledger.records[capsule.key] = { hash: digest(capsule.text), lastSentAt: now.toISOString() };\n }\n await this.write(path, ledger, now);\n } finally {\n await release();\n }\n }\n\n /**\n * Acknowledged delivery for clients that can lose context (MCP). Unlike\n * `deliver`, sending marks nothing as seen: the sent hashes are held as a\n * pending delivery until the client echoes its `deliveryId` back as `ack` on\n * a later call. Only acknowledged, unchanged sections sent within `ttlMs` are\n * reported unchanged, so a dropped response or a compacted client context\n * fails safe to a full resend.\n *\n * A section keeps its last few acknowledged versions (by content hash), not\n * just the latest, so returning to an earlier task in the same session does\n * not resend what the client already holds. `full` means the client lost its\n * context: every acknowledged version and unconfirmed delivery is forgotten.\n */\n async exchange(\n sessionId: string,\n sections: SectionHash[],\n options: {\n ack?: string;\n ttlMs: number;\n /** The client lost its context: nothing acknowledged before this call counts. */\n full?: boolean;\n now?: Date;\n /** Finalize the response before recording hashes of the actual sent sections. */\n prepare?: (exchange: AcknowledgedExchange) => SectionHash[];\n },\n ): Promise<AcknowledgedExchange> {\n const now = options.now ?? new Date();\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n let pending = ledger.pending ?? {};\n let acknowledged = ledger.acknowledged ?? {};\n\n let ack: AcknowledgedExchange[\"ack\"] = \"none\";\n if (options.ack !== undefined) {\n const confirmed = pending[options.ack];\n if (confirmed) {\n for (const [key, hash] of Object.entries(confirmed.records)) {\n acknowledged[key] = [\n { hash, sentAt: confirmed.createdAt },\n ...(acknowledged[key] ?? []).filter((version) => version.hash !== hash),\n ]\n .sort((left, right) => right.sentAt.localeCompare(left.sentAt))\n .slice(0, MAX_ACKNOWLEDGED_PER_KEY);\n }\n delete pending[options.ack];\n ack = \"accepted\";\n } else {\n ack = \"unknown\";\n }\n }\n if (options.full) {\n acknowledged = {};\n pending = {};\n }\n\n const unchanged: string[] = [];\n const sent: Record<string, string> = {};\n for (const section of sections) {\n const held = acknowledged[section.key]?.some((version) => {\n const age = now.getTime() - Date.parse(version.sentAt);\n return version.hash === section.hash && age >= 0 && age <= options.ttlMs;\n });\n if (held) {\n unchanged.push(section.key);\n } else {\n sent[section.key] = section.hash;\n }\n }\n\n const deliveryId = generateEntityId(\"dlv\", now.getTime());\n const exchange = { deliveryId, unchanged, ack };\n const delivered = options.prepare?.(exchange);\n pending[deliveryId] = {\n createdAt: now.toISOString(),\n records: delivered\n ? Object.fromEntries(delivered.map(({ key, hash }) => [key, hash]))\n : sent,\n };\n ledger.pending = Object.fromEntries(\n Object.entries(pending)\n .filter(([, item]) => now.getTime() - Date.parse(item.createdAt) <= PENDING_MAX_AGE_MS)\n .sort((left, right) => right[1].createdAt.localeCompare(left[1].createdAt))\n .slice(0, MAX_PENDING),\n );\n ledger.acknowledged = Object.fromEntries(\n Object.entries(acknowledged)\n .map(([key, versions]): [string, AcknowledgedVersion[]] => [\n key,\n versions.filter((version) => now.getTime() - Date.parse(version.sentAt) <= MAX_AGE_MS),\n ])\n .filter(([, versions]) => versions.length > 0)\n .slice(0, MAX_CAPSULES),\n );\n await this.write(path, ledger, now);\n return { deliveryId, unchanged, ack };\n } finally {\n await release();\n }\n }\n\n /** Forget everything sent in a session, e.g. after the client compacted its context. */\n async reset(sessionId: string): Promise<void> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n await rm(this.path(sessionId), { force: true });\n } finally {\n await release();\n }\n }\n\n /** Remove expired disposable state; locked sessions are left for the next pass. */\n async pruneExpired(now = new Date()): Promise<number> {\n let removed = 0;\n for (const entry of await readdir(this.directory).catch(() => [])) {\n if (!entry.endsWith(\".json\")) continue;\n const path = join(this.directory, entry);\n try {\n await stat(`${path}.lock`);\n continue;\n } catch {\n // No active lock.\n }\n try {\n const metadata = await stat(path);\n if (now.getTime() - metadata.mtimeMs > MAX_AGE_MS) {\n await rm(path, { force: true });\n removed++;\n }\n } catch {\n // Another cleanup may have removed it already.\n }\n }\n return removed;\n }\n\n private async write(path: string, ledger: LedgerFile, now: Date): Promise<void> {\n ledger.updatedAt = now.toISOString();\n ledger.records = Object.fromEntries(\n Object.entries(ledger.records)\n .sort((left, right) => right[1].lastSentAt.localeCompare(left[1].lastSentAt))\n .slice(0, MAX_CAPSULES),\n );\n await writeAtomic(path, `${JSON.stringify(ledger, null, 2)}\\n`);\n }\n\n private async read(path: string, now: Date): Promise<LedgerFile> {\n try {\n const parsed = JSON.parse(await readFile(path, \"utf8\")) as LedgerFile;\n if (parsed.version !== VERSION || typeof parsed.records !== \"object\") return empty(now);\n const records = Object.fromEntries(\n Object.entries(parsed.records).filter(([, record]) => {\n const age = now.getTime() - Date.parse(record.lastSentAt);\n return Number.isFinite(age) && age <= MAX_AGE_MS;\n }),\n );\n const pending =\n parsed.pending && typeof parsed.pending === \"object\" ? parsed.pending : undefined;\n const acknowledged =\n parsed.acknowledged && typeof parsed.acknowledged === \"object\"\n ? Object.fromEntries(\n Object.entries(parsed.acknowledged).filter(([, versions]) => Array.isArray(versions)),\n )\n : undefined;\n return {\n version: VERSION,\n updatedAt: parsed.updatedAt,\n records,\n ...(pending ? { pending } : {}),\n ...(acknowledged ? { acknowledged } : {}),\n };\n } catch {\n return empty(now);\n }\n }\n\n private path(sessionId: string): string {\n // The full identity participates in the key. Legacy sanitized filenames\n // are intentionally ignored, so a fresh session cannot inherit collisions.\n return join(this.directory, `${digest(sessionId)}.json`);\n }\n\n private async acquire(sessionId: string): Promise<() => Promise<void>> {\n const lock = `${this.path(sessionId)}.lock`;\n for (let attempt = 0; ; attempt++) {\n try {\n await mkdir(lock);\n return async () => rm(lock, { recursive: true, force: true });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt >= 200) throw error;\n try {\n const age = Date.now() - (await stat(lock)).mtimeMs;\n if (age > STALE_LOCK_MS) {\n await rm(lock, { recursive: true, force: true });\n continue;\n }\n } catch {\n // The owner may have released the lock between checks.\n }\n await new Promise((resolveWait) => setTimeout(resolveWait, 10));\n }\n }\n }\n}\n\nfunction empty(now: Date): LedgerFile {\n return { version: VERSION, updatedAt: now.toISOString(), records: {} };\n}\n\nfunction digest(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n","import { access, readFile } from \"node:fs/promises\";\nimport { basename, dirname, extname, join } from \"node:path\";\n\n/**\n * Which test covers a file and how this repo runs its tests, from naming\n * conventions and project markers only. Agents otherwise spend turns finding\n * both; a wrong guess costs one failed command, so first match wins.\n */\nexport function findCoveringTest(path: string, paths: ReadonlySet<string>): string | null {\n return coveringTestCandidates(path).find((candidate) => paths.has(candidate)) ?? null;\n}\n\n/** Conventional covering-test paths for `path`, most specific first. */\nexport function coveringTestCandidates(path: string): string[] {\n const ext = extname(path);\n const stem = basename(path, ext);\n const dir = dirname(path);\n const join2 = (...parts: string[]) => parts.filter((part) => part && part !== \".\").join(\"/\");\n // `src/a/x.ts` mirrors to `test/a/x.test.ts`: drop a leading source root.\n const mirrored = dir.replace(/^(src|lib|app)(\\/|$)/, \"\");\n return ext === \".py\"\n ? [\n join2(dir, `test_${stem}.py`),\n join2(dir, `${stem}_test.py`),\n join2(dir, \"tests\", `test_${stem}.py`),\n // `src/pkg/sub/x.py` → `tests/pkg/sub/test_x.py`, `tests/sub/test_x.py`, `tests/test_x.py`.\n join2(\"tests\", mirrored, `test_${stem}.py`),\n join2(\"tests\", mirrored.replace(/^[^/]+\\/?/, \"\"), `test_${stem}.py`),\n join2(\"tests\", `test_${stem}.py`),\n ]\n : ext === \".go\"\n ? [join2(dir, `${stem}_test.go`)]\n : [\n join2(dir, `${stem}.test${ext}`),\n join2(dir, `${stem}.spec${ext}`),\n join2(dir, \"__tests__\", `${stem}.test${ext}`),\n join2(dir, \"__tests__\", `${stem}${ext}`),\n join2(\"test\", mirrored, `${stem}.test${ext}`),\n join2(\"tests\", mirrored, `${stem}.test${ext}`),\n ];\n}\n\nconst NPM_PLACEHOLDER = /no test specified/;\n\nexport async function detectTestCommand(repositoryRoot: string): Promise<string | null> {\n const exists = (file: string) =>\n access(join(repositoryRoot, file)).then(\n () => true,\n () => false,\n );\n try {\n const pkg = JSON.parse(await readFile(join(repositoryRoot, \"package.json\"), \"utf8\")) as {\n scripts?: Record<string, unknown>;\n packageManager?: unknown;\n };\n const script = pkg.scripts?.test;\n if (typeof script === \"string\" && script.trim() && !NPM_PLACEHOLDER.test(script)) {\n const declared =\n typeof pkg.packageManager === \"string\" ? pkg.packageManager.split(/[\\s@]/, 1)[0] : \"\";\n // `bun test` is Bun's own runner, not the package's test script.\n if (declared === \"bun\") return \"bun run test\";\n if (declared && [\"npm\", \"pnpm\", \"yarn\"].includes(declared)) return `${declared} test`;\n if (await exists(\"pnpm-lock.yaml\")) return \"pnpm test\";\n if (await exists(\"yarn.lock\")) return \"yarn test\";\n if ((await exists(\"bun.lockb\")) || (await exists(\"bun.lock\"))) return \"bun run test\";\n return \"npm test\";\n }\n } catch {\n // No or unreadable package.json: try other ecosystems.\n }\n for (const marker of [\"pyproject.toml\", \"pytest.ini\", \"setup.cfg\"]) {\n if (await exists(marker)) return \"pytest\";\n }\n if (await exists(\"go.mod\")) return \"go test ./...\";\n if (await exists(\"Cargo.toml\")) return \"cargo test\";\n return null;\n}\n"],"mappings":"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","names":["readFile","join","join2"]}
|