@feng0531/dsh-code-review 0.85.13 → 0.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,15 +21,17 @@ function budgetError(kind) {
21
21
  }
22
22
 
23
23
  // This capability reads only caller-approved text entries, never the filesystem.
24
- export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024 * 1024, maxCalls = 50, maxOutputBytes = 256 * 1024, signal } = {}) {
24
+ export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024 * 1024, maxCalls = 50, maxOutputBytes = 256 * 1024, signal, reuseResults = false } = {}) {
25
25
  if (typeof snapshotId !== 'string' || !snapshotId || snapshotId.length > 200) throw new Error('Invalid snapshot identity');
26
26
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 16 * 1024 * 1024) throw new Error('Invalid reader budget');
27
27
  if (!Array.isArray(entries) || entries.length > 500) throw new Error('Invalid reader entries');
28
28
  for (const [value, cap] of [[maxCalls, 1000], [maxOutputBytes, 4 * 1024 * 1024]]) {
29
29
  if (!Number.isSafeInteger(value) || value < 1 || value > cap) throw new Error('Invalid reader budget');
30
30
  }
31
+ if (typeof reuseResults !== 'boolean') throw new Error('Invalid reader reuse option');
31
32
  signal?.throwIfAborted();
32
33
 
34
+ let cache = new Map(), cacheBytes = 0, cacheHits = 0, deliveries = 0;
33
35
  let calls = 0, outputBytes = 0, inTransaction = false;
34
36
  const begin = () => {
35
37
  signal?.throwIfAborted();
@@ -103,16 +105,66 @@ export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024
103
105
  return { snapshotId, sourceId: id, hash: source.hash, start, count: selectedCount, text, endOfSource: endOffset === source.text.length };
104
106
  };
105
107
 
108
+ const validate = (kind, request) => {
109
+ if (!request || typeof request !== 'object') throw new Error('Invalid reader request');
110
+ if (kind === 'read') {
111
+ if (!sources.has(request.id)) throw new Error('Source not authorized');
112
+ if (!Number.isSafeInteger(request.start) || request.start < 1 || !Number.isSafeInteger(request.count) || request.count < 1 || request.count > 200) throw new Error('Invalid line range');
113
+ } else {
114
+ if (!validQuery(request.query, request.limit === undefined ? 20 : request.limit)) throw new Error('Invalid query request');
115
+ if (kind === 'search' && request.sourceIds !== undefined && (!Array.isArray(request.sourceIds) || !request.sourceIds.length || request.sourceIds.length > 20 || new Set(request.sourceIds).size !== request.sourceIds.length || request.sourceIds.some(id => !sources.has(id)))) throw new Error('Invalid search source filter');
116
+ }
117
+ };
118
+ const keyOf = (kind, request) => JSON.stringify(kind === 'read' ? [kind, request.id, request.start, request.count] : kind === 'find' ? [kind, request.query, request.limit ?? 20] : [kind, request.query, request.limit ?? 20, request.sourceIds === undefined ? null : [...request.sourceIds].sort()]);
119
+ const materialize = (kind, request) => kind === 'read' ? read(request) : kind === 'find' ? find(request) : search(request);
120
+ const deliver = (kind, request, decorate = value => value) => {
121
+ signal?.throwIfAborted();
122
+ // Validation is never bypassed by canonicalization (e.g. duplicate filters).
123
+ if (reuseResults) validate(kind, request);
124
+ const key = reuseResults ? keyOf(kind, request) : null;
125
+ const cached = reuseResults && cache.get(key);
126
+ if (!cached) begin();
127
+ const raw = cached ? structuredClone(cached.result) : materialize(kind, request);
128
+ const result = finish(decorate(structuredClone(raw)));
129
+ if (reuseResults) {
130
+ deliveries++;
131
+ if (cached) cacheHits++;
132
+ else {
133
+ const size = resultSize(raw);
134
+ if (cache.size < maxCalls && cacheBytes + size <= maxOutputBytes) { cache.set(key, { result: structuredClone(raw), size }); cacheBytes += size; }
135
+ }
136
+ }
137
+ return result;
138
+ };
139
+ const preflight = requests => {
140
+ signal?.throwIfAborted();
141
+ if (!Array.isArray(requests) || !requests.length || requests.length > 8) throw new Error('Invalid retrieval batch');
142
+ const staged = new Map(cache); let stagedBytes = cacheBytes, needed = 0, size = 0, hits = 0;
143
+ for (const request of requests) {
144
+ if (!['read', 'find', 'search'].includes(request?.kind)) throw new Error('Unsupported retrieval operation');
145
+ // Dry-run validates against immutable approved sources; it grants no receipts.
146
+ validate(request.kind, request);
147
+ const key = reuseResults ? keyOf(request.kind, request) : null;
148
+ const raw = reuseResults && staged.has(key) ? staged.get(key).result : materialize(request.kind, request), bytes = resultSize(raw);
149
+ if (reuseResults && staged.has(key)) hits++;
150
+ else { needed++; if (reuseResults && staged.size < maxCalls && stagedBytes + bytes <= maxOutputBytes) { staged.set(key, { result: raw, size: bytes }); stagedBytes += bytes; } }
151
+ size += bytes;
152
+ }
153
+ if (calls + needed > maxCalls) throw budgetError('call');
154
+ if (outputBytes + size > maxOutputBytes) throw budgetError('output');
155
+ return { calls: needed, outputBytes: size, cacheHits: hits };
156
+ };
106
157
  return Object.freeze({
107
- usage: () => ({ calls, outputBytes }),
108
- budget: () => ({ maxCalls, maxOutputBytes, remainingCalls: maxCalls - calls, remainingOutputBytes: maxOutputBytes - outputBytes, maxResultBytes: 64 * 1024 }),
158
+ usage: () => ({ calls, outputBytes, ...(reuseResults ? { cacheHits, deliveries, cacheEntries: cache.size, cacheBytes } : {}) }),
159
+ budget: () => ({ maxCalls, maxOutputBytes, remainingCalls: maxCalls - calls, remainingOutputBytes: maxOutputBytes - outputBytes, maxResultBytes: 64 * 1024, ...(reuseResults ? { reusableResults: cache.size > 0 } : {}) }),
160
+ canServe(requests) { try { preflight(requests); return true; } catch { return false; } },
109
161
  // A synchronous host-only batch commits its ledger only as a whole. This
110
162
  // also covers decorated response bytes that preflight cannot predict.
111
163
  // Callers must not publish results/audit records before this returns.
112
164
  transaction(execute) {
113
165
  signal?.throwIfAborted();
114
166
  if (inTransaction || typeof execute !== 'function') throw new Error('Invalid reader transaction');
115
- const previousCalls = calls, previousBytes = outputBytes;
167
+ const previousCalls = calls, previousBytes = outputBytes, previousCache = new Map(cache), previousCacheBytes = cacheBytes, previousHits = cacheHits, previousDeliveries = deliveries;
116
168
  inTransaction = true;
117
169
  try {
118
170
  const result = execute();
@@ -120,29 +172,17 @@ export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024
120
172
  signal?.throwIfAborted();
121
173
  return result;
122
174
  } catch (error) {
123
- calls = previousCalls; outputBytes = previousBytes;
175
+ calls = previousCalls; outputBytes = previousBytes; cache = previousCache; cacheBytes = previousCacheBytes; cacheHits = previousHits; deliveries = previousDeliveries;
124
176
  throw error;
125
177
  } finally { inTransaction = false; }
126
178
  },
127
179
  // Host-only dry run. Validate ALL ranges and sources before charging anything.
128
180
  // No evidence or receipts escape this preflight. Decorated reads are still
129
181
  // measured by finish; this never reserves or increases the shared budget.
130
- preflight(requests) {
131
- signal?.throwIfAborted();
132
- if (!Array.isArray(requests) || !requests.length || requests.length > 8) throw new Error('Invalid retrieval batch');
133
- const results = requests.map(request => {
134
- if (request?.kind === 'read') return read(request);
135
- if (request?.kind === 'find') return find(request);
136
- if (request?.kind === 'search') return search(request);
137
- throw new Error('Unsupported retrieval operation');
138
- });
139
- if (calls + requests.length > maxCalls) throw budgetError('call');
140
- const size = results.reduce((sum, result) => sum + resultSize(result), 0);
141
- if (outputBytes + size > maxOutputBytes) throw budgetError('output');
142
- },
143
- find(request) { begin(); return finish(find(request)); },
144
- search(request) { begin(); return finish(search(request)); },
145
- // decorate is a host-only callback; model requests never supply it.
146
- read(request, decorate = value => value) { begin(); return finish(decorate(read(request))); },
182
+ preflight,
183
+ find(request) { return deliver('find', request); },
184
+ search(request) { return deliver('search', request); },
185
+ // Only immutable raw text is cached; each delivery creates fresh decorations.
186
+ read(request, decorate) { return deliver('read', request, decorate); },
147
187
  });
148
188
  }
@@ -1,6 +1,7 @@
1
1
  import { VERIFICATION_INSTRUCTIONS } from './prompts.mjs';
2
2
  import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
3
3
  import { defectGuidance } from './defect-guidance.mjs';
4
+ import { modelWireBudget } from './model-wire-budget.mjs';
4
5
  import { createEvidenceReceipts } from './evidence-receipts.mjs';
5
6
  import { retrievalLoop, retrievalRequest } from './retrieval-loop.mjs';
6
7
  import { verificationSeeds } from './verification-seeds.mjs';
@@ -28,7 +29,7 @@ export async function verificationLoop(model, request, candidates, scope, option
28
29
  onReceipt: (id, resolve) => seedReceipts.set(id, resolve),
29
30
  fits: seeds => {
30
31
  const next = retrievalRequest(verificationRequest, scope, seeds);
31
- return Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= (options.maxInputBytes ?? 96 * 1024);
32
+ return request.nativeOutputTools === true ? modelWireBudget(next, options.maxInputBytes ?? 96 * 1024).fits : Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= (options.maxInputBytes ?? 96 * 1024);
32
33
  },
33
34
  });
34
35
  const receipts = createEvidenceReceipts(scope.read, { limit: 50 - seedReceipts.size });
@@ -35,8 +35,10 @@ export function verificationSeeds(candidates, scope, { signal, fits, onReceipt =
35
35
  const budget = scope.budget?.();
36
36
  // At least one exact final validation read per candidate must remain. This
37
37
  // reserves no new authority; additional evidence can still exhaust budgets.
38
- if (budget && (budget.remainingCalls <= candidates.length || budget.remainingOutputBytes <= finalEvidenceBytes)) break;
38
+ if (budget && budget.remainingOutputBytes <= finalEvidenceBytes) break;
39
39
  try {
40
+ const cached = budget?.reusableResults === true && scope.preflight?.([request])?.calls === 0 && scope.canServe?.([request]) === true;
41
+ if (budget && !cached && budget.remainingCalls <= candidates.length) continue;
40
42
  // Failed transactions discard their receipt table too, not just the ledger.
41
43
  const receipts = createEvidenceReceipts(scope.read, { limit: 1 });
42
44
  const item = scope.transaction(() => {
@@ -48,7 +50,22 @@ export function verificationSeeds(candidates, scope, { signal, fits, onReceipt =
48
50
  const proposed = [...seeds, { request, result }];
49
51
  const nextBudget = scope.budget?.();
50
52
  const reserve = finalEvidenceBytes + Buffer.byteLength(JSON.stringify(result)) * references;
53
+ if (cached) {
54
+ // Validation reads the actual returned count, not the seed's requested
55
+ // EOF-clamped count. These are different exact cache keys. Reserve all
56
+ // final reads explicitly before admitting a zero-call cached seed.
57
+ let calls = 0;
58
+ for (const item of proposed) {
59
+ const validation = { kind: 'read', id: item.result.sourceId, start: item.result.start, count: item.result.count };
60
+ const cost = scope.preflight([validation]);
61
+ if (!Number.isSafeInteger(cost?.calls) || cost.calls < 0) throw capacityError();
62
+ const matching = [...ranges.values()].find(range => JSON.stringify(range.request) === JSON.stringify(item.request));
63
+ calls += cost.calls * (matching?.references ?? 1);
64
+ }
65
+ if (nextBudget && calls > nextBudget.remainingCalls) throw capacityError();
66
+ }
51
67
  if (Buffer.byteLength(JSON.stringify(proposed)) > MAX_SEED_BYTES || !fits(proposed) || (nextBudget && nextBudget.remainingOutputBytes < reserve)) throw capacityError();
68
+ signal?.throwIfAborted();
52
69
  return { request, result };
53
70
  });
54
71
  // Publish only after the approved reader has committed the transaction.