/** * The handle a successful `register(...)` returns: the id a later dispatch references. * * It used to also carry `lengthChars` and a `sha256`, described here as letting a caller * "independently verify its content" — a capability no caller ever had. Both surfaces that * register a block (`POST /context-blocks` and the MCP tool) read `.id` and nothing else, so the * hash was computed on every registration and discarded. That is not free: the terminal block for * every read-route execution is the reviewer's raw output, and this file's own warning threshold * anticipates content over 10 MiB, which costs ~12ms to hash for no reader. */ export interface RegisteredBlock { id: string; } /** * Minimal store contract for reusable context blocks. * * The intent is to let a caller register a large brief once, then reference * it by id from many subsequent dispatches without re-transmitting the * content on every call. The unified handler resolves block IDs inline * and prepends the content to the worker payload. */ export interface ContextBlockStore { /** Store `content` under an explicit id (idempotent replace) or a new UUID. Returns the id. */ register(content: string, opts?: { id?: string; ttlMs?: number; }): RegisteredBlock; /** Fetch content by id. Returns `undefined` if the id is unknown or * the entry has expired. Touches the LRU access time on success. */ get(id: string): string | undefined; /** Does a live (unexpired) entry exist? Unlike `get`, this does NOT refresh the * entry's TTL or its LRU position — for callers asking about existence rather * than wanting the content. */ has(id: string): boolean; /** Delete an entry. Returns `true` if the entry existed. */ delete(id: string): boolean; /** Number of entries. Used by status + size-cap checks. */ readonly size: number; /** Increment pin count — holds blocks across an active task dispatch * so they can't be evicted mid-run. */ pin(id: string): void; /** Decrement pin count. */ unpin(id: string): void; /** Current pin count for an entry. Returns 0 if unknown. */ refcount(id: string): number; /** Wipe every entry. Used by project-registry on idle eviction. */ clear(): void; /** Configured idle TTL (ms). Tests + observability surfaces read it. */ readonly ttlMs: number; } export interface InMemoryContextBlockStoreOptions { /** Idle TTL in milliseconds. Defaults to 24 hours; resets on `get()`. */ ttlMs?: number; /** Max number of entries before LRU eviction. Defaults to 500. */ maxEntries?: number; } /** * In-memory implementation with two bounds: * 1. A TTL (time-to-live) from `addedAtMs` — checked lazily on `get`. * 2. An LRU cap on entry count — enforced eagerly after every `register`. * * Both bounds are intentional: the TTL prevents stale briefs from lingering * after a long-running session; the LRU cap prevents memory growth from a * chatty caller that never explicitly deletes anything. The eviction loop * is O(n) per insertion but `n <= maxEntries` (defaults to 500, matching * `server.limits.maxContextBlocksPerProject`), so we * keep the implementation simple. * * `Date.now()` is read directly (not through a clock abstraction) so tests * can drive time forward with Vitest's fake timers. */ export declare class InMemoryContextBlockStore implements ContextBlockStore { private entries; private _ttlMs; private maxEntries; private tick; constructor(opts?: InMemoryContextBlockStoreOptions); register(content: string, opts?: { id?: string; ttlMs?: number; }): RegisteredBlock; get(id: string): string | undefined; /** * Existence without the side effects of `get`. * * `get` deliberately refreshes an entry's TTL and LRU position, which is right for a caller that * wants the content — and wrong for one merely asking whether the id is live. The delete handler * used `get` for its existence check, so a DELETE rejected as `pinned` extended the life of the * block it had just failed to remove and made it the newest entry in LRU order, i.e. the LAST * thing evicted under the per-project cap. A caller polling for a pin to clear kept the block * alive by asking about it. * * An expired entry is dropped here as it is in `get` — reporting it as present would be a lie * with a different expiry rule than the rest of the store. */ has(id: string): boolean; delete(id: string): boolean; /** Increment the pin (reference) count for an entry. Pinned entries are * skipped during LRU eviction. No-op if the entry is unknown. */ pin(id: string): void; /** Decrement the pin count for an entry. No-op if the entry is unknown or * the count is already zero. */ unpin(id: string): void; /** Return the current pin count for an entry, or 0 if unknown. */ refcount(id: string): number; /** Idle TTL (ms) this store was configured with. */ get ttlMs(): number; /** * Live entries only. * * Expiry in this store is LAZY — an entry is dropped when `get` or `has` touches it and finds it * stale. Nothing sweeps in the background, so a block nobody asks about again stays in the Map * forever after it expires. `size` reported those corpses, and three separate decisions are made * from `size`: * * - `ProjectRegistry.evictIdleLRU` refuses to evict a project whose store is non-empty, because * a caller may still reference its blocks by id. With dead blocks counted, a project that * registered one block and was never touched again became permanently un-evictable — and once * `cap` distinct cwds had each left one behind, `reserveProject` returned `project_cap` for * good. That is exactly the "permanent lockout" that method's comment says it exists to * prevent. * - `POST /context-blocks` returns 409 `cap_exhausted` at `size >= maxContextBlocksPerProject`, * so a project could be locked out by 500 blocks that `get` would all refuse to return. * - `GET /status` reports `contextBlockCount` per project, which was counting unreachable ones. * * Sweeping here is O(n) with n bounded by `maxEntries` (500 by default), and `size` is read on cap * checks and status, not per request on a hot path. A getter that mutates is unusual, but it is * the same lazy-expiry contract `get` and `has` already implement — reporting an entry that every * read path would refuse to hand back is the anomaly, not dropping it. */ get size(): number; clear(): void; /** Drop every entry past its TTL. The lazy expiry `get`/`has` do one id at a time. */ private sweepExpired; private evictIfOverBound; } //# sourceMappingURL=context-block-tool.d.ts.map