@orquentra/codebuddy 0.0.0-stage → 2.4.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 +269 -0
- package/CLAUDE_CODEX.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1118 -2
- package/WORKFLOW.md +152 -0
- package/dist/chunk-3ZELDCR3.js +204 -0
- package/dist/chunk-3ZELDCR3.js.map +1 -0
- package/dist/chunk-5OWZVZSO.js +476 -0
- package/dist/chunk-5OWZVZSO.js.map +1 -0
- package/dist/chunk-DSN5CEHO.js +175 -0
- package/dist/chunk-DSN5CEHO.js.map +1 -0
- package/dist/chunk-GDJH7XJC.js +119 -0
- package/dist/chunk-GDJH7XJC.js.map +1 -0
- package/dist/chunk-IBMJYIEP.js +1578 -0
- package/dist/chunk-IBMJYIEP.js.map +1 -0
- package/dist/chunk-IE2OUO54.js +199 -0
- package/dist/chunk-IE2OUO54.js.map +1 -0
- package/dist/chunk-IHQM2ZDV.js +19 -0
- package/dist/chunk-IHQM2ZDV.js.map +1 -0
- package/dist/chunk-ITGRANYW.js +63 -0
- package/dist/chunk-ITGRANYW.js.map +1 -0
- package/dist/chunk-K63EOA4B.js +416 -0
- package/dist/chunk-K63EOA4B.js.map +1 -0
- package/dist/chunk-PQK3GKZH.js +98 -0
- package/dist/chunk-PQK3GKZH.js.map +1 -0
- package/dist/chunk-PYLH4MH5.js +34 -0
- package/dist/chunk-PYLH4MH5.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-SYHB3AMM.js +8580 -0
- package/dist/chunk-SYHB3AMM.js.map +1 -0
- package/dist/chunk-TI5TQD6D.js +324 -0
- package/dist/chunk-TI5TQD6D.js.map +1 -0
- package/dist/chunk-WHZSVY2L.js +144 -0
- package/dist/chunk-WHZSVY2L.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +15 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/codebuddy-D2xJ6hFk.d.ts +841 -0
- package/dist/config-S2TULOMK.js +11 -0
- package/dist/config-S2TULOMK.js.map +1 -0
- package/dist/index.d.ts +2527 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/langgraph/index.d.ts +53 -0
- package/dist/langgraph/index.js +143 -0
- package/dist/langgraph/index.js.map +1 -0
- package/dist/lean-command-D6TZJEFX.js +13 -0
- package/dist/lean-command-D6TZJEFX.js.map +1 -0
- package/dist/main-ZZ3EQLYA.js +4575 -0
- package/dist/main-ZZ3EQLYA.js.map +1 -0
- package/dist/memory-NIBFAMY4.js +57 -0
- package/dist/memory-NIBFAMY4.js.map +1 -0
- package/dist/pretool-command-MQQ2UNQM.js +10 -0
- package/dist/pretool-command-MQQ2UNQM.js.map +1 -0
- package/dist/rerank-Q7MIFNKA.js +60 -0
- package/dist/rerank-Q7MIFNKA.js.map +1 -0
- package/dist/summaries-TSE44KUQ.js +18 -0
- package/dist/summaries-TSE44KUQ.js.map +1 -0
- package/dist/zvec-F27AR357.js +23 -0
- package/dist/zvec-F27AR357.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/examples/claude-code/README.md +67 -0
- package/examples/claude-desktop/README.md +54 -0
- package/examples/claude-desktop/claude_desktop_config.json +13 -0
- package/examples/langgraph-agent/README.md +32 -0
- package/examples/langgraph-agent/index.ts +71 -0
- package/examples/multi-agent-handoff/README.md +35 -0
- package/examples/multi-agent-handoff/index.ts +68 -0
- package/package.json +108 -4
- package/src/db/migrations/0000_cultured_satana.sql +144 -0
- package/src/db/migrations/0001_embedding_lease.sql +3 -0
- package/src/db/migrations/0002_embedding_lease_token.sql +1 -0
- package/src/db/migrations/0003_embedding_space.sql +9 -0
- package/src/db/migrations/0004_markdown_reconcile.sql +5 -0
- package/src/db/migrations/0005_fact_propositions.sql +1 -0
- package/src/db/migrations/meta/0000_snapshot.json +1169 -0
- package/src/db/migrations/meta/0002_snapshot.json +1181 -0
- package/src/db/migrations/meta/0003_snapshot.json +1214 -0
- package/src/db/migrations/meta/0004_snapshot.json +1225 -0
- package/src/db/migrations/meta/0005_snapshot.json +1231 -0
- package/src/db/migrations/meta/_journal.json +48 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeAtomic
|
|
3
|
+
} from "./chunk-K63EOA4B.js";
|
|
4
|
+
|
|
5
|
+
// src/planner/budget.ts
|
|
6
|
+
import { Tiktoken } from "js-tiktoken/lite";
|
|
7
|
+
import cl100kBase from "js-tiktoken/ranks/cl100k_base";
|
|
8
|
+
|
|
9
|
+
// src/providers/models.ts
|
|
10
|
+
var EMBEDDING_DIMENSIONS = 384;
|
|
11
|
+
var DEFAULT_EMBEDDING_MODELS = [
|
|
12
|
+
{
|
|
13
|
+
id: "sentence-transformers/all-MiniLM-L6-v2",
|
|
14
|
+
role: "primary",
|
|
15
|
+
kind: "embedding",
|
|
16
|
+
dimensions: EMBEDDING_DIMENSIONS,
|
|
17
|
+
dailyCallBudget: 1e3,
|
|
18
|
+
supportsBatch: true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "BAAI/bge-small-en-v1.5",
|
|
22
|
+
role: "fallback",
|
|
23
|
+
kind: "embedding",
|
|
24
|
+
dimensions: EMBEDDING_DIMENSIONS,
|
|
25
|
+
dailyCallBudget: 1e3,
|
|
26
|
+
supportsBatch: true
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
var DEFAULT_LLM_MODELS = [
|
|
30
|
+
{
|
|
31
|
+
id: "meta-llama/Llama-3.2-3B-Instruct",
|
|
32
|
+
role: "primary",
|
|
33
|
+
kind: "llm",
|
|
34
|
+
contextWindow: 131072,
|
|
35
|
+
dailyCallBudget: 300
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "Qwen/Qwen2.5-7B-Instruct",
|
|
39
|
+
role: "fallback",
|
|
40
|
+
kind: "llm",
|
|
41
|
+
contextWindow: 32768,
|
|
42
|
+
dailyCallBudget: 300
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "mistralai/Mistral-7B-Instruct-v0.3",
|
|
46
|
+
role: "last_resort",
|
|
47
|
+
kind: "llm",
|
|
48
|
+
contextWindow: 32768,
|
|
49
|
+
dailyCallBudget: 300
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
var MODEL_REGISTRY = [...DEFAULT_EMBEDDING_MODELS, ...DEFAULT_LLM_MODELS];
|
|
53
|
+
function getModelDefinition(modelId) {
|
|
54
|
+
return MODEL_REGISTRY.find((model) => model.id === modelId);
|
|
55
|
+
}
|
|
56
|
+
function getModelContextWindow(modelId) {
|
|
57
|
+
return getModelDefinition(modelId)?.contextWindow;
|
|
58
|
+
}
|
|
59
|
+
function assertCompatibleEmbeddingModels(models) {
|
|
60
|
+
const mismatchedModel = models.find((model) => model.dimensions !== EMBEDDING_DIMENSIONS);
|
|
61
|
+
if (mismatchedModel) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Embedding model ${mismatchedModel.id} has ${mismatchedModel.dimensions ?? "unknown"} dimensions; expected ${EMBEDDING_DIMENSIONS}.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/planner/budget.ts
|
|
69
|
+
var encoder = null;
|
|
70
|
+
function getEncoder() {
|
|
71
|
+
if (!encoder) {
|
|
72
|
+
encoder = new Tiktoken(cl100kBase);
|
|
73
|
+
}
|
|
74
|
+
return encoder;
|
|
75
|
+
}
|
|
76
|
+
var PIECE = new RegExp(cl100kBase.pat_str, "gu");
|
|
77
|
+
var SPECIAL_TOKENS = Object.keys(cl100kBase.special_tokens);
|
|
78
|
+
var MAX_CACHED_PIECES = 2e5;
|
|
79
|
+
var pieceTokens = /* @__PURE__ */ new Map();
|
|
80
|
+
function countTokens(text) {
|
|
81
|
+
if (!text) return 0;
|
|
82
|
+
try {
|
|
83
|
+
if (SPECIAL_TOKENS.some((token) => text.includes(token))) throw new Error("special token");
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const [piece] of text.matchAll(PIECE)) {
|
|
86
|
+
let count = pieceTokens.get(piece);
|
|
87
|
+
if (count === void 0) {
|
|
88
|
+
count = getEncoder().encode(piece).length;
|
|
89
|
+
if (pieceTokens.size >= MAX_CACHED_PIECES) pieceTokens.clear();
|
|
90
|
+
pieceTokens.set(piece, count);
|
|
91
|
+
}
|
|
92
|
+
total += count;
|
|
93
|
+
}
|
|
94
|
+
return total;
|
|
95
|
+
} catch {
|
|
96
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function clampBudgetForCallerModel(requestedBudget, callerModel) {
|
|
100
|
+
if (!callerModel) {
|
|
101
|
+
return { budget: requestedBudget, clamped: false };
|
|
102
|
+
}
|
|
103
|
+
const ctx = getModelContextWindow(callerModel);
|
|
104
|
+
if (!ctx) {
|
|
105
|
+
return { budget: requestedBudget, clamped: false };
|
|
106
|
+
}
|
|
107
|
+
const headroom = Math.floor(ctx * 0.75);
|
|
108
|
+
if (requestedBudget > headroom) {
|
|
109
|
+
return {
|
|
110
|
+
budget: headroom,
|
|
111
|
+
clamped: true,
|
|
112
|
+
reason: `budget ${requestedBudget} exceeds 75% of ${callerModel} context window (${ctx}); clamped to ${headroom}.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { budget: requestedBudget, clamped: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/core/ids.ts
|
|
119
|
+
import { createHash, randomBytes } from "crypto";
|
|
120
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
121
|
+
function generateUlid(now = Date.now()) {
|
|
122
|
+
let timePart = "";
|
|
123
|
+
let ts = now;
|
|
124
|
+
for (let i = 0; i < 10; i++) {
|
|
125
|
+
timePart = (CROCKFORD[ts % 32] ?? "0") + timePart;
|
|
126
|
+
ts = Math.floor(ts / 32);
|
|
127
|
+
}
|
|
128
|
+
const bytes = randomBytes(10);
|
|
129
|
+
let randPart = "";
|
|
130
|
+
for (let i = 0; i < 16; i++) {
|
|
131
|
+
const bitOffset = i * 5;
|
|
132
|
+
const byteIndex = Math.floor(bitOffset / 8);
|
|
133
|
+
const bitInByte = bitOffset % 8;
|
|
134
|
+
const hi = bytes[byteIndex] ?? 0;
|
|
135
|
+
const lo = bytes[byteIndex + 1] ?? 0;
|
|
136
|
+
const combined = (hi << 8 | lo) >>> 0;
|
|
137
|
+
const shift = 16 - bitInByte - 5;
|
|
138
|
+
const value = combined >>> shift & 31;
|
|
139
|
+
randPart += CROCKFORD[value] ?? "0";
|
|
140
|
+
}
|
|
141
|
+
return timePart + randPart;
|
|
142
|
+
}
|
|
143
|
+
function generateSessionId(now) {
|
|
144
|
+
return `sess_${generateUlid(now).toLowerCase()}`;
|
|
145
|
+
}
|
|
146
|
+
function generateEntityId(prefix, now) {
|
|
147
|
+
return `${prefix}_${generateUlid(now).toLowerCase()}`;
|
|
148
|
+
}
|
|
149
|
+
function computeContentHash(namespace, sessionId, content) {
|
|
150
|
+
return createHash("sha256").update(`${namespace}${sessionId}${content}`).digest("hex");
|
|
151
|
+
}
|
|
152
|
+
function computeFactHash(namespace, content) {
|
|
153
|
+
return createHash("sha256").update(`${namespace}fact${content}`).digest("hex");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/savings/tokens.ts
|
|
157
|
+
function estimateTokens(value) {
|
|
158
|
+
if (value === void 0 || value === null) return 0;
|
|
159
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
160
|
+
if (!text) return 0;
|
|
161
|
+
return countTokens(text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/context/session-ledger.ts
|
|
165
|
+
import { createHash as createHash2 } from "crypto";
|
|
166
|
+
import { mkdir, readdir, readFile, rm, stat } from "fs/promises";
|
|
167
|
+
import { join, resolve } from "path";
|
|
168
|
+
var VERSION = 2;
|
|
169
|
+
var MAX_CAPSULES = 128;
|
|
170
|
+
var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
171
|
+
var MAX_PENDING = 8;
|
|
172
|
+
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
173
|
+
var STALE_LOCK_MS = 6e4;
|
|
174
|
+
var MAX_ACKNOWLEDGED_PER_KEY = 8;
|
|
175
|
+
var SessionCapsuleLedger = class {
|
|
176
|
+
directory;
|
|
177
|
+
constructor(repositoryRoot = process.cwd()) {
|
|
178
|
+
this.directory = join(resolve(repositoryRoot), ".codebuddy", "cache", "session");
|
|
179
|
+
}
|
|
180
|
+
async deliver(sessionId, capsules, now = /* @__PURE__ */ new Date()) {
|
|
181
|
+
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
182
|
+
const release = await this.acquire(sessionId);
|
|
183
|
+
try {
|
|
184
|
+
const path = this.path(sessionId);
|
|
185
|
+
const ledger = await this.read(path, now);
|
|
186
|
+
const lines = [];
|
|
187
|
+
const unchanged = [];
|
|
188
|
+
let full = 0;
|
|
189
|
+
let referenced = 0;
|
|
190
|
+
let sentTokens = 0;
|
|
191
|
+
let avoidedTokens = 0;
|
|
192
|
+
for (const capsule of capsules) {
|
|
193
|
+
const hash = digest(capsule.text);
|
|
194
|
+
const existing = ledger.records[capsule.key];
|
|
195
|
+
if (existing?.hash === hash) {
|
|
196
|
+
unchanged.push(capsule.key);
|
|
197
|
+
referenced++;
|
|
198
|
+
avoidedTokens += estimateTokens(capsule.text);
|
|
199
|
+
} else {
|
|
200
|
+
lines.push(capsule.text);
|
|
201
|
+
full++;
|
|
202
|
+
sentTokens += estimateTokens(capsule.text);
|
|
203
|
+
}
|
|
204
|
+
ledger.records[capsule.key] = { hash, lastSentAt: now.toISOString() };
|
|
205
|
+
}
|
|
206
|
+
await this.write(path, ledger, now);
|
|
207
|
+
return { lines, unchanged, full, referenced, sentTokens, avoidedTokens };
|
|
208
|
+
} finally {
|
|
209
|
+
await release();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Keys of capsules whose current text was already recorded as sent this session. */
|
|
213
|
+
async seen(sessionId, capsules, now = /* @__PURE__ */ new Date()) {
|
|
214
|
+
const ledger = await this.read(this.path(sessionId), now);
|
|
215
|
+
return new Set(
|
|
216
|
+
capsules.filter((capsule) => ledger.records[capsule.key]?.hash === digest(capsule.text)).map((capsule) => capsule.key)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Record capsules as delivered. Call only after their text actually reached
|
|
221
|
+
* the client, so capsules that were clipped or failed to write stay unseen
|
|
222
|
+
* and are delivered by a later prompt.
|
|
223
|
+
*/
|
|
224
|
+
async markSent(sessionId, capsules, now = /* @__PURE__ */ new Date()) {
|
|
225
|
+
if (capsules.length === 0) return;
|
|
226
|
+
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
227
|
+
const release = await this.acquire(sessionId);
|
|
228
|
+
try {
|
|
229
|
+
const path = this.path(sessionId);
|
|
230
|
+
const ledger = await this.read(path, now);
|
|
231
|
+
for (const capsule of capsules) {
|
|
232
|
+
ledger.records[capsule.key] = { hash: digest(capsule.text), lastSentAt: now.toISOString() };
|
|
233
|
+
}
|
|
234
|
+
await this.write(path, ledger, now);
|
|
235
|
+
} finally {
|
|
236
|
+
await release();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Acknowledged delivery for clients that can lose context (MCP). Unlike
|
|
241
|
+
* `deliver`, sending marks nothing as seen: the sent hashes are held as a
|
|
242
|
+
* pending delivery until the client echoes its `deliveryId` back as `ack` on
|
|
243
|
+
* a later call. Only acknowledged, unchanged sections sent within `ttlMs` are
|
|
244
|
+
* reported unchanged, so a dropped response or a compacted client context
|
|
245
|
+
* fails safe to a full resend.
|
|
246
|
+
*
|
|
247
|
+
* A section keeps its last few acknowledged versions (by content hash), not
|
|
248
|
+
* just the latest, so returning to an earlier task in the same session does
|
|
249
|
+
* not resend what the client already holds. `full` means the client lost its
|
|
250
|
+
* context: every acknowledged version and unconfirmed delivery is forgotten.
|
|
251
|
+
*/
|
|
252
|
+
async exchange(sessionId, sections, options) {
|
|
253
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
254
|
+
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
255
|
+
const release = await this.acquire(sessionId);
|
|
256
|
+
try {
|
|
257
|
+
const path = this.path(sessionId);
|
|
258
|
+
const ledger = await this.read(path, now);
|
|
259
|
+
let pending = ledger.pending ?? {};
|
|
260
|
+
let acknowledged = ledger.acknowledged ?? {};
|
|
261
|
+
let ack = "none";
|
|
262
|
+
if (options.ack !== void 0) {
|
|
263
|
+
const confirmed = pending[options.ack];
|
|
264
|
+
if (confirmed) {
|
|
265
|
+
for (const [key, hash] of Object.entries(confirmed.records)) {
|
|
266
|
+
acknowledged[key] = [
|
|
267
|
+
{ hash, sentAt: confirmed.createdAt },
|
|
268
|
+
...(acknowledged[key] ?? []).filter((version) => version.hash !== hash)
|
|
269
|
+
].sort((left, right) => right.sentAt.localeCompare(left.sentAt)).slice(0, MAX_ACKNOWLEDGED_PER_KEY);
|
|
270
|
+
}
|
|
271
|
+
delete pending[options.ack];
|
|
272
|
+
ack = "accepted";
|
|
273
|
+
} else {
|
|
274
|
+
ack = "unknown";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (options.full) {
|
|
278
|
+
acknowledged = {};
|
|
279
|
+
pending = {};
|
|
280
|
+
}
|
|
281
|
+
const unchanged = [];
|
|
282
|
+
const sent = {};
|
|
283
|
+
for (const section of sections) {
|
|
284
|
+
const held = acknowledged[section.key]?.some((version) => {
|
|
285
|
+
const age = now.getTime() - Date.parse(version.sentAt);
|
|
286
|
+
return version.hash === section.hash && age >= 0 && age <= options.ttlMs;
|
|
287
|
+
});
|
|
288
|
+
if (held) {
|
|
289
|
+
unchanged.push(section.key);
|
|
290
|
+
} else {
|
|
291
|
+
sent[section.key] = section.hash;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const deliveryId = generateEntityId("dlv", now.getTime());
|
|
295
|
+
const exchange = { deliveryId, unchanged, ack };
|
|
296
|
+
const delivered = options.prepare?.(exchange);
|
|
297
|
+
pending[deliveryId] = {
|
|
298
|
+
createdAt: now.toISOString(),
|
|
299
|
+
records: delivered ? Object.fromEntries(delivered.map(({ key, hash }) => [key, hash])) : sent
|
|
300
|
+
};
|
|
301
|
+
ledger.pending = Object.fromEntries(
|
|
302
|
+
Object.entries(pending).filter(([, item]) => now.getTime() - Date.parse(item.createdAt) <= PENDING_MAX_AGE_MS).sort((left, right) => right[1].createdAt.localeCompare(left[1].createdAt)).slice(0, MAX_PENDING)
|
|
303
|
+
);
|
|
304
|
+
ledger.acknowledged = Object.fromEntries(
|
|
305
|
+
Object.entries(acknowledged).map(([key, versions]) => [
|
|
306
|
+
key,
|
|
307
|
+
versions.filter((version) => now.getTime() - Date.parse(version.sentAt) <= MAX_AGE_MS)
|
|
308
|
+
]).filter(([, versions]) => versions.length > 0).slice(0, MAX_CAPSULES)
|
|
309
|
+
);
|
|
310
|
+
await this.write(path, ledger, now);
|
|
311
|
+
return { deliveryId, unchanged, ack };
|
|
312
|
+
} finally {
|
|
313
|
+
await release();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Forget everything sent in a session, e.g. after the client compacted its context. */
|
|
317
|
+
async reset(sessionId) {
|
|
318
|
+
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
319
|
+
const release = await this.acquire(sessionId);
|
|
320
|
+
try {
|
|
321
|
+
await rm(this.path(sessionId), { force: true });
|
|
322
|
+
} finally {
|
|
323
|
+
await release();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** Remove expired disposable state; locked sessions are left for the next pass. */
|
|
327
|
+
async pruneExpired(now = /* @__PURE__ */ new Date()) {
|
|
328
|
+
let removed = 0;
|
|
329
|
+
for (const entry of await readdir(this.directory).catch(() => [])) {
|
|
330
|
+
if (!entry.endsWith(".json")) continue;
|
|
331
|
+
const path = join(this.directory, entry);
|
|
332
|
+
try {
|
|
333
|
+
await stat(`${path}.lock`);
|
|
334
|
+
continue;
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const metadata = await stat(path);
|
|
339
|
+
if (now.getTime() - metadata.mtimeMs > MAX_AGE_MS) {
|
|
340
|
+
await rm(path, { force: true });
|
|
341
|
+
removed++;
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return removed;
|
|
347
|
+
}
|
|
348
|
+
async write(path, ledger, now) {
|
|
349
|
+
ledger.updatedAt = now.toISOString();
|
|
350
|
+
ledger.records = Object.fromEntries(
|
|
351
|
+
Object.entries(ledger.records).sort((left, right) => right[1].lastSentAt.localeCompare(left[1].lastSentAt)).slice(0, MAX_CAPSULES)
|
|
352
|
+
);
|
|
353
|
+
await writeAtomic(path, `${JSON.stringify(ledger, null, 2)}
|
|
354
|
+
`);
|
|
355
|
+
}
|
|
356
|
+
async read(path, now) {
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
359
|
+
if (parsed.version !== VERSION || typeof parsed.records !== "object") return empty(now);
|
|
360
|
+
const records = Object.fromEntries(
|
|
361
|
+
Object.entries(parsed.records).filter(([, record]) => {
|
|
362
|
+
const age = now.getTime() - Date.parse(record.lastSentAt);
|
|
363
|
+
return Number.isFinite(age) && age <= MAX_AGE_MS;
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
const pending = parsed.pending && typeof parsed.pending === "object" ? parsed.pending : void 0;
|
|
367
|
+
const acknowledged = parsed.acknowledged && typeof parsed.acknowledged === "object" ? Object.fromEntries(
|
|
368
|
+
Object.entries(parsed.acknowledged).filter(([, versions]) => Array.isArray(versions))
|
|
369
|
+
) : void 0;
|
|
370
|
+
return {
|
|
371
|
+
version: VERSION,
|
|
372
|
+
updatedAt: parsed.updatedAt,
|
|
373
|
+
records,
|
|
374
|
+
...pending ? { pending } : {},
|
|
375
|
+
...acknowledged ? { acknowledged } : {}
|
|
376
|
+
};
|
|
377
|
+
} catch {
|
|
378
|
+
return empty(now);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
path(sessionId) {
|
|
382
|
+
return join(this.directory, `${digest(sessionId)}.json`);
|
|
383
|
+
}
|
|
384
|
+
async acquire(sessionId) {
|
|
385
|
+
const lock = `${this.path(sessionId)}.lock`;
|
|
386
|
+
for (let attempt = 0; ; attempt++) {
|
|
387
|
+
try {
|
|
388
|
+
await mkdir(lock);
|
|
389
|
+
return async () => rm(lock, { recursive: true, force: true });
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error.code !== "EEXIST" || attempt >= 200) throw error;
|
|
392
|
+
try {
|
|
393
|
+
const age = Date.now() - (await stat(lock)).mtimeMs;
|
|
394
|
+
if (age > STALE_LOCK_MS) {
|
|
395
|
+
await rm(lock, { recursive: true, force: true });
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 10));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
function empty(now) {
|
|
406
|
+
return { version: VERSION, updatedAt: now.toISOString(), records: {} };
|
|
407
|
+
}
|
|
408
|
+
function digest(text) {
|
|
409
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/context/tests.ts
|
|
413
|
+
import { access, readFile as readFile2 } from "fs/promises";
|
|
414
|
+
import { basename, dirname, extname, join as join2 } from "path";
|
|
415
|
+
function findCoveringTest(path, paths) {
|
|
416
|
+
const ext = extname(path);
|
|
417
|
+
const stem = basename(path, ext);
|
|
418
|
+
const dir = dirname(path);
|
|
419
|
+
const join22 = (...parts) => parts.filter((part) => part && part !== ".").join("/");
|
|
420
|
+
const mirrored = dir.replace(/^(src|lib|app)(\/|$)/, "");
|
|
421
|
+
const candidates = ext === ".py" ? [
|
|
422
|
+
join22(dir, `test_${stem}.py`),
|
|
423
|
+
join22("tests", `test_${stem}.py`),
|
|
424
|
+
join22(dir, "tests", `test_${stem}.py`),
|
|
425
|
+
join22(dir, `${stem}_test.py`)
|
|
426
|
+
] : ext === ".go" ? [join22(dir, `${stem}_test.go`)] : [
|
|
427
|
+
join22(dir, `${stem}.test${ext}`),
|
|
428
|
+
join22(dir, `${stem}.spec${ext}`),
|
|
429
|
+
join22(dir, "__tests__", `${stem}.test${ext}`),
|
|
430
|
+
join22(dir, "__tests__", `${stem}${ext}`),
|
|
431
|
+
join22("test", mirrored, `${stem}.test${ext}`),
|
|
432
|
+
join22("tests", mirrored, `${stem}.test${ext}`)
|
|
433
|
+
];
|
|
434
|
+
return candidates.find((candidate) => paths.has(candidate)) ?? null;
|
|
435
|
+
}
|
|
436
|
+
var NPM_PLACEHOLDER = /no test specified/;
|
|
437
|
+
async function detectTestCommand(repositoryRoot) {
|
|
438
|
+
const exists = (file) => access(join2(repositoryRoot, file)).then(
|
|
439
|
+
() => true,
|
|
440
|
+
() => false
|
|
441
|
+
);
|
|
442
|
+
try {
|
|
443
|
+
const pkg = JSON.parse(await readFile2(join2(repositoryRoot, "package.json"), "utf8"));
|
|
444
|
+
const script = pkg.scripts?.test;
|
|
445
|
+
if (typeof script === "string" && script.trim() && !NPM_PLACEHOLDER.test(script)) {
|
|
446
|
+
if (await exists("pnpm-lock.yaml")) return "pnpm test";
|
|
447
|
+
if (await exists("yarn.lock")) return "yarn test";
|
|
448
|
+
return "npm test";
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
for (const marker of ["pyproject.toml", "pytest.ini", "setup.cfg"]) {
|
|
453
|
+
if (await exists(marker)) return "pytest";
|
|
454
|
+
}
|
|
455
|
+
if (await exists("go.mod")) return "go test ./...";
|
|
456
|
+
if (await exists("Cargo.toml")) return "cargo test";
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export {
|
|
461
|
+
EMBEDDING_DIMENSIONS,
|
|
462
|
+
DEFAULT_EMBEDDING_MODELS,
|
|
463
|
+
DEFAULT_LLM_MODELS,
|
|
464
|
+
assertCompatibleEmbeddingModels,
|
|
465
|
+
countTokens,
|
|
466
|
+
clampBudgetForCallerModel,
|
|
467
|
+
generateSessionId,
|
|
468
|
+
generateEntityId,
|
|
469
|
+
computeContentHash,
|
|
470
|
+
computeFactHash,
|
|
471
|
+
estimateTokens,
|
|
472
|
+
SessionCapsuleLedger,
|
|
473
|
+
findCoveringTest,
|
|
474
|
+
detectTestCommand
|
|
475
|
+
};
|
|
476
|
+
//# sourceMappingURL=chunk-5OWZVZSO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/planner/budget.ts","../src/providers/models.ts","../src/core/ids.ts","../src/savings/tokens.ts","../src/context/session-ledger.ts","../src/context/tests.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>();\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 [piece] of text.matchAll(PIECE)) {\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 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","export type ModelRole = \"primary\" | \"fallback\" | \"last_resort\";\nexport type ModelKind = \"embedding\" | \"llm\";\n\nexport type ModelDefinition = {\n id: string;\n role: ModelRole;\n kind: ModelKind;\n dimensions?: number;\n /** Actual deployment revision metadata; this does not pin Hugging Face requests. */\n revision?: string;\n schema?: string;\n contextWindow?: number;\n dailyCallBudget?: number;\n supportsBatch?: boolean;\n};\n\nexport const EMBEDDING_DIMENSIONS = 384;\n\nexport const DEFAULT_EMBEDDING_MODELS = [\n {\n id: \"sentence-transformers/all-MiniLM-L6-v2\",\n role: \"primary\",\n kind: \"embedding\",\n dimensions: EMBEDDING_DIMENSIONS,\n dailyCallBudget: 1_000,\n supportsBatch: true,\n },\n {\n id: \"BAAI/bge-small-en-v1.5\",\n role: \"fallback\",\n kind: \"embedding\",\n dimensions: EMBEDDING_DIMENSIONS,\n dailyCallBudget: 1_000,\n supportsBatch: true,\n },\n] satisfies ModelDefinition[];\n\nexport const DEFAULT_LLM_MODELS = [\n {\n id: \"meta-llama/Llama-3.2-3B-Instruct\",\n role: \"primary\",\n kind: \"llm\",\n contextWindow: 131_072,\n dailyCallBudget: 300,\n },\n {\n id: \"Qwen/Qwen2.5-7B-Instruct\",\n role: \"fallback\",\n kind: \"llm\",\n contextWindow: 32_768,\n dailyCallBudget: 300,\n },\n {\n id: \"mistralai/Mistral-7B-Instruct-v0.3\",\n role: \"last_resort\",\n kind: \"llm\",\n contextWindow: 32_768,\n dailyCallBudget: 300,\n },\n] satisfies ModelDefinition[];\n\nexport const MODEL_REGISTRY = [...DEFAULT_EMBEDDING_MODELS, ...DEFAULT_LLM_MODELS];\n\nexport function getModelDefinition(modelId: string): ModelDefinition | undefined {\n return MODEL_REGISTRY.find((model) => model.id === modelId);\n}\n\nexport function getModelContextWindow(modelId: string): number | undefined {\n return getModelDefinition(modelId)?.contextWindow;\n}\n\nexport function assertCompatibleEmbeddingModels(models: ModelDefinition[]): void {\n const mismatchedModel = models.find((model) => model.dimensions !== EMBEDDING_DIMENSIONS);\n\n if (mismatchedModel) {\n throw new Error(\n `Embedding model ${mismatchedModel.id} has ${mismatchedModel.dimensions ?? \"unknown\"} dimensions; expected ${EMBEDDING_DIMENSIONS}.`,\n );\n }\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nconst CROCKFORD = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nexport function generateUlid(now: number = Date.now()): string {\n let timePart = \"\";\n let ts = now;\n for (let i = 0; i < 10; i++) {\n timePart = (CROCKFORD[ts % 32] ?? \"0\") + timePart;\n ts = Math.floor(ts / 32);\n }\n\n const bytes = randomBytes(10);\n let randPart = \"\";\n for (let i = 0; i < 16; i++) {\n const bitOffset = i * 5;\n const byteIndex = Math.floor(bitOffset / 8);\n const bitInByte = bitOffset % 8;\n const hi = bytes[byteIndex] ?? 0;\n const lo = bytes[byteIndex + 1] ?? 0;\n const combined = ((hi << 8) | lo) >>> 0;\n const shift = 16 - bitInByte - 5;\n const value = (combined >>> shift) & 0x1f;\n randPart += CROCKFORD[value] ?? \"0\";\n }\n\n return timePart + randPart;\n}\n\nexport function generateSessionId(now?: number): string {\n return `sess_${generateUlid(now).toLowerCase()}`;\n}\n\nexport function generateEntityId(prefix: string, now?: number): string {\n return `${prefix}_${generateUlid(now).toLowerCase()}`;\n}\n\nexport function computeContentHash(namespace: string, sessionId: string, content: string): string {\n return createHash(\"sha256\").update(`${namespace}\\x1f${sessionId}\\x1f${content}`).digest(\"hex\");\n}\n\nexport function computeFactHash(namespace: string, content: string): string {\n return createHash(\"sha256\").update(`${namespace}\\x1ffact\\x1f${content}`).digest(\"hex\");\n}\n","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 /**\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 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 const candidates =\n ext === \".py\"\n ? [\n join2(dir, `test_${stem}.py`),\n join2(\"tests\", `test_${stem}.py`),\n join2(dir, \"tests\", `test_${stem}.py`),\n join2(dir, `${stem}_test.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 return candidates.find((candidate) => paths.has(candidate)) ?? null;\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 };\n const script = pkg.scripts?.test;\n if (typeof script === \"string\" && script.trim() && !NPM_PLACEHOLDER.test(script)) {\n if (await exists(\"pnpm-lock.yaml\")) return \"pnpm test\";\n if (await exists(\"yarn.lock\")) return \"yarn 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":["createHash","createHash","readFile","join","join2"]}
|