bantamkit-mcp 0.29.2 → 0.31.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/README.md +83 -2
- package/assets/pricing/default.json +7 -0
- package/assets/schemas/shiftwork-checkpoint.json +12 -1
- package/assets/tools/memory_dream.json +20 -0
- package/assets/tools/repo_map.json +44 -0
- package/assets/tools/shiftwork_clock_out.json +1 -1
- package/assets/tools/token_ledger.json +40 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +135 -4
- package/dist/eventlog.js +6 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/mcp/identity.d.ts +80 -0
- package/dist/mcp/identity.js +466 -4
- package/dist/mcp/pyargs.d.ts +1 -1
- package/dist/mcp/pyargs.js +78 -0
- package/dist/mcp/server.d.ts +37 -6
- package/dist/mcp/server.js +209 -5
- package/dist/mcp/status.d.ts +54 -10
- package/dist/mcp/status.js +86 -10
- package/dist/memory/cli.js +1 -1
- package/dist/memory/component.d.ts +73 -2
- package/dist/memory/component.js +157 -5
- package/dist/memory/dream.d.ts +247 -0
- package/dist/memory/dream.js +903 -0
- package/dist/memory/store.d.ts +101 -1
- package/dist/memory/store.js +178 -4
- package/dist/pricing.d.ts +100 -0
- package/dist/pricing.js +357 -0
- package/dist/repomap.d.ts +247 -0
- package/dist/repomap.js +1040 -0
- package/dist/selfupdate.d.ts +320 -0
- package/dist/selfupdate.js +594 -0
- package/dist/shiftwork.d.ts +5 -3
- package/dist/shiftwork.js +82 -4
- package/dist/tokenledger.d.ts +91 -0
- package/dist/tokenledger.js +372 -0
- package/package.json +1 -1
package/dist/mcp/status.js
CHANGED
|
@@ -33,6 +33,8 @@ import { statSync } from 'node:fs';
|
|
|
33
33
|
import { join } from 'node:path';
|
|
34
34
|
import { AssetNotFound, assetsRoot } from '../assets.js';
|
|
35
35
|
import { Memory } from '../memory/component.js';
|
|
36
|
+
import { INDEX_PRESSURE_PERCENT } from '../memory/store.js';
|
|
37
|
+
import { Undetermined, currentInstall, originStat } from './identity.js';
|
|
36
38
|
/**
|
|
37
39
|
* The tool AND the prompt answer to this one name. Deliberately the same word: the operator
|
|
38
40
|
* who read `bantamkit_status` in a footer must find it in their prompt menu without
|
|
@@ -68,14 +70,13 @@ export const STATUS_PROMPT_TAIL = 'Show me that report as it stands. If it says
|
|
|
68
70
|
export const SERVED_PROMPTS = 1;
|
|
69
71
|
export const SERVED_RESOURCE_TEMPLATES = 2;
|
|
70
72
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* on opposite sides of the line through a float they rounded differently.
|
|
73
|
+
* `INDEX_PRESSURE_PERCENT` is re-exported here, where it used to be DEFINED, so that
|
|
74
|
+
* `import { INDEX_PRESSURE_PERCENT } from './mcp/status.js'` keeps resolving. It moved down
|
|
75
|
+
* to `memory/store.ts` in job46 (J46-5, mirroring the reference's J46-4):
|
|
76
|
+
* `MemoryStore.compact` is the remedy the sentence below names, and it cannot clear a warning
|
|
77
|
+
* whose line it cannot see. The comment that says why the number is 90 moved with it.
|
|
77
78
|
*/
|
|
78
|
-
export
|
|
79
|
+
export { INDEX_PRESSURE_PERCENT };
|
|
79
80
|
/** `1 prompt` / `2 prompts`. Spelled once so the two runtimes cannot disagree twice. */
|
|
80
81
|
export function plural(count, word) {
|
|
81
82
|
return count === 1 ? `${count} ${word}` : `${count} ${word}s`;
|
|
@@ -221,6 +222,20 @@ export function unreadableLayerCondition(memory) {
|
|
|
221
222
|
* not a divergence and is not fixed here; it is registered in `docs/porting.md`. The sentence
|
|
222
223
|
* says "archive or shorten facts", which is what the operator has to do either way.
|
|
223
224
|
*
|
|
225
|
+
* AMENDMENT (job46, J46-5): THE PARAGRAPH ABOVE IS A RECORD OF A DEFECT THAT IS NOW CLOSED,
|
|
226
|
+
* and it is left standing because it is the measurement that motivated the fix. What it says
|
|
227
|
+
* was true until this job: the band was real, and on this machine's own project store
|
|
228
|
+
* (101 facts, index.md 21819 bytes of a 24000-byte budget = 90.91%, largest index line 361
|
|
229
|
+
* bytes so the old target was 23639 = 98.50%) `compact()` answered `archived: []` with this
|
|
230
|
+
* very sentence on screen. What has changed is the second half — "is not fixed here". It is
|
|
231
|
+
* fixed now, in the layer that owns it: `MemoryStore.compact`'s default `reserve` is measured
|
|
232
|
+
* from `undegradedIndexCeiling(budget)` rather than from `budget`, so running the command
|
|
233
|
+
* this sentence names clears the condition this sentence reports. Both runtimes moved in the
|
|
234
|
+
* same job (the reference in `87cc1f7`), because a band that closed on one side only would be
|
|
235
|
+
* the divergence the paragraph above correctly said this was not. The register entry's own
|
|
236
|
+
* closure is not written here — that is a `docs/porting.md` edit, and it waits on a
|
|
237
|
+
* conformance case comparing the two answers.
|
|
238
|
+
*
|
|
224
239
|
* THIS SENTENCE IS FOR THE OPERATOR; THE MODEL HAS ITS OWN REMEDY. Since job42 the model
|
|
225
240
|
* can call `memory_compact` (the ninth served tool) when a save is actually refused, and
|
|
226
241
|
* the refused-budget reply in `memory/component.ts` names it. That does not change this
|
|
@@ -261,13 +276,71 @@ export function eventLogCondition(log) {
|
|
|
261
276
|
'whether its directory is writable.',
|
|
262
277
|
};
|
|
263
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* This install came from a path on this machine, and that path is gone.
|
|
281
|
+
*
|
|
282
|
+
* THE ONE CONDITION THAT NAMES A PATH, and the exception is deliberate. Every other sentence
|
|
283
|
+
* here refuses one because `assetsRoot()` resolves differently in the two runtimes by
|
|
284
|
+
* construction, so a path would be an uncomparable value bought for nothing. This path is not
|
|
285
|
+
* the server's own location — it is the origin THE INSTALLER WROTE DOWN, it is the entire
|
|
286
|
+
* actionable content of the finding (AS-7's measured case is a tarball under a
|
|
287
|
+
* `/private/tmp/.../scratchpad` that no longer exists), and a sentence saying "something is
|
|
288
|
+
* missing" without saying what would be a sentence nobody can act on.
|
|
289
|
+
*
|
|
290
|
+
* THE REMEDY IS THE ONE THAT ACTUALLY MOVES SOMETHING. J46-4 spent a unit removing a condition
|
|
291
|
+
* whose remedy exited 0 having changed nothing, and this is exactly the shape that invites
|
|
292
|
+
* another: `npm update` where a dangling `file:` install lives is a no-op BY CONSTRUCTION. So
|
|
293
|
+
* the sentence sends the reader at a reinstall BY NAME from a registry, which replaces the
|
|
294
|
+
* install rather than trying to refresh it in place. `bantamkit` is the PRODUCT name and is
|
|
295
|
+
* copied verbatim from the reference — it is not the npm package id, and turning it into one
|
|
296
|
+
* is how `git_commit` diverged on a noun.
|
|
297
|
+
*
|
|
298
|
+
* THE DERIVED HALF IS MEMOISED AND THIS HALF IS NOT: the shape cannot change under a running
|
|
299
|
+
* process, the path's existence can, and it is the one that has to be read now.
|
|
300
|
+
*/
|
|
301
|
+
export function installSourceCondition(install) {
|
|
302
|
+
if (install === null || install.source === null)
|
|
303
|
+
return null;
|
|
304
|
+
// `present: null` is a check that could not be made, and the reference reports no condition
|
|
305
|
+
// for one either: an origin nobody may stat is not, on that evidence, an origin that is gone.
|
|
306
|
+
if (originStat(install.source).present !== false)
|
|
307
|
+
return null;
|
|
308
|
+
return {
|
|
309
|
+
key: 'install-source-missing',
|
|
310
|
+
sentence: `this server was installed from ${install.source}, which no longer exists, so nothing ` +
|
|
311
|
+
'can be refreshed in place there — reinstall bantamkit by name from a package registry ' +
|
|
312
|
+
'and restart the server.',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The shape, or nothing. A shape that could not be derived is a REPORTED gap.
|
|
317
|
+
*
|
|
318
|
+
* `buildIdentity` is where that gap is named, with its reason. It is not a degraded condition:
|
|
319
|
+
* an install this code cannot classify is not, on that evidence, an install that is broken, and
|
|
320
|
+
* a footer on every tool call saying otherwise would be the noise `degradedNotice` exists to
|
|
321
|
+
* avoid.
|
|
322
|
+
*/
|
|
323
|
+
function currentInstallOrNull() {
|
|
324
|
+
try {
|
|
325
|
+
return currentInstall();
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
if (!(e instanceof Undetermined))
|
|
329
|
+
throw e;
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
264
333
|
/**
|
|
265
334
|
* Everything wrong right now, worst first. An empty array means healthy.
|
|
266
335
|
*
|
|
267
336
|
* ORDER IS SEVERITY AND IT IS LOAD-BEARING, because the footer shows the first one: a pack
|
|
268
337
|
* that vanished breaks every asset-backed surface; an unreadable layer makes recall ANSWER
|
|
269
338
|
* WRONGLY rather than fail; a full index refuses the next save; a broken event log costs
|
|
270
|
-
* diagnostics only
|
|
339
|
+
* diagnostics only; and a dangling install origin costs nothing AT ALL right now — the server
|
|
340
|
+
* is serving correctly, and what is broken is the next attempt to update it. That is why it is
|
|
341
|
+
* last despite being the one that went unnoticed for five releases
|
|
342
|
+
* (`docs/roadmap-agent-stack.md` AS-7): severity here is what is failing, not what has been
|
|
343
|
+
* failing longest.
|
|
271
344
|
*
|
|
272
345
|
* EVERY CONDITION IS OBSERVED, NOT INFERRED — no heartbeat, no timer, no last-seen timestamp.
|
|
273
346
|
* Each one is a state a test can construct and then watch this report: delete the pack, make a
|
|
@@ -276,8 +349,10 @@ export function eventLogCondition(log) {
|
|
|
276
349
|
* claimed.
|
|
277
350
|
*
|
|
278
351
|
* THE COST, because this runs on every tool call: one `stat` for the index, one `stat` for the
|
|
279
|
-
* pack, one directory listing per memory layer (two to four),
|
|
280
|
-
* fact file is opened
|
|
352
|
+
* pack, one directory listing per memory layer (two to four), a field read for the log, and one
|
|
353
|
+
* `stat` for the install origin. No fact file is opened, no index is parsed, and
|
|
354
|
+
* `node_modules` is NOT walked for a hidden lockfile — `currentInstall` does that once per
|
|
355
|
+
* process, because the bytes that were imported cannot change under a running one.
|
|
281
356
|
*/
|
|
282
357
|
export function degradedConditions(memory, log) {
|
|
283
358
|
return [
|
|
@@ -285,6 +360,7 @@ export function degradedConditions(memory, log) {
|
|
|
285
360
|
unreadableLayerCondition(memory),
|
|
286
361
|
indexPressureCondition(memory),
|
|
287
362
|
eventLogCondition(log),
|
|
363
|
+
installSourceCondition(currentInstallOrNull()),
|
|
288
364
|
].filter((condition) => condition !== null);
|
|
289
365
|
}
|
|
290
366
|
/**
|
package/dist/memory/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `docs/memory.md` states, as a design decision, that `lint`, `archived` and `restore`
|
|
6
6
|
* are **not** agent tools — "lifecycle is an operator decision, not a model decision" —
|
|
7
|
-
* and the
|
|
7
|
+
* and the fourteen-tool surface holds to it; `compact` is the one exception since job42
|
|
8
8
|
* (`memory_compact`, the on-refusal path the model reaches itself). `runtime-py` made that
|
|
9
9
|
* position coherent in August 2026 by giving the operator `python -m bantamkit.memory`.
|
|
10
10
|
* `runtime-ts` did not. MEASURED 2026-08-24 before this file existed: `node dist/cli.js -h`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type StoreBinding } from './layers.js';
|
|
2
2
|
import { MemoryStore } from './store.js';
|
|
3
|
+
import type { DreamResult } from './dream.js';
|
|
3
4
|
/**
|
|
4
5
|
* The model's spelling adapted to the store's contract: lowercase, `_`/space -> `-`.
|
|
5
6
|
*
|
|
@@ -97,6 +98,30 @@ export interface RecallOutcome {
|
|
|
97
98
|
readonly source: string | null;
|
|
98
99
|
readonly unreadable: number;
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* What `dream` DID, beside the sentence it says about it.
|
|
103
|
+
*
|
|
104
|
+
* The same seam as `SaveOutcome`, `RecallOutcome` and `CompactOutcome`: `status` is read off
|
|
105
|
+
* a decision the pass already made — `DreamResult.applied`, `.overBudget`, `.changes` — and
|
|
106
|
+
* never off the reply. `result` carries the whole diff for a caller that wants the numbers
|
|
107
|
+
* rather than the prose.
|
|
108
|
+
*
|
|
109
|
+
* `status` is one of `consolidated`, `previewed`, `nothing-to-consolidate`,
|
|
110
|
+
* `refused-budget`, `no-profile-layer`.
|
|
111
|
+
*/
|
|
112
|
+
export interface DreamOutcome {
|
|
113
|
+
readonly reply: string;
|
|
114
|
+
readonly status: string;
|
|
115
|
+
readonly dryRun: boolean;
|
|
116
|
+
readonly merged: number;
|
|
117
|
+
readonly consumed: number;
|
|
118
|
+
readonly absolutised: number;
|
|
119
|
+
readonly superseded: number;
|
|
120
|
+
readonly indexBefore: number;
|
|
121
|
+
readonly indexAfter: number;
|
|
122
|
+
readonly budget: number;
|
|
123
|
+
readonly result: DreamResult | null;
|
|
124
|
+
}
|
|
100
125
|
export declare class Memory {
|
|
101
126
|
readonly store: MemoryStore;
|
|
102
127
|
readonly k: number;
|
|
@@ -140,7 +165,7 @@ export declare class Memory {
|
|
|
140
165
|
* fact. The floor is the operator's configured default, not a constant, so a consumer who
|
|
141
166
|
* really wants top-1 says so once at construction.
|
|
142
167
|
*/
|
|
143
|
-
recall(query: string, k?: number | null): string;
|
|
168
|
+
recall(query: string, k?: number | null, minRatio?: number): string;
|
|
144
169
|
/**
|
|
145
170
|
* `recall`, carrying the walk it performed as numbers rather than as prose.
|
|
146
171
|
*
|
|
@@ -157,8 +182,18 @@ export declare class Memory {
|
|
|
157
182
|
* refuses to list contributes nothing and raises the `unreadable` count instead of being
|
|
158
183
|
* scored as empty; that distinction is the whole subject of `nothingToReport` below and
|
|
159
184
|
* must not be undone here.
|
|
185
|
+
*
|
|
186
|
+
* `minRatio` (roadmap #6) rides through to every layer's `MemoryStore.recall` unchanged,
|
|
187
|
+
* so the gate is measured against EACH LAYER's own best score and never across layers: a
|
|
188
|
+
* profile fact does not have to out-score the project store's top hit to be admitted,
|
|
189
|
+
* because the two stores are answering as two stores. Its default is
|
|
190
|
+
* `RECALL_MIN_SCORE_RATIO` = 0.0, which keeps every fact `recall` was going to return,
|
|
191
|
+
* and nothing on the tool path passes anything else today. A ratio outside `[0.0, 1.0]`
|
|
192
|
+
* raises out of the FIRST layer, which is the writable project store, so it surfaces as
|
|
193
|
+
* the error it is rather than as an `unreadable` count — the read-only-layer `catch`
|
|
194
|
+
* below would otherwise file a caller's bad argument as a corrupt grant.
|
|
160
195
|
*/
|
|
161
|
-
recallOutcome(query: string, k?: number | null): RecallOutcome;
|
|
196
|
+
recallOutcome(query: string, k?: number | null, minRatio?: number): RecallOutcome;
|
|
162
197
|
/**
|
|
163
198
|
* Archive the stalest facts until a fact as large as the biggest one kept will fit.
|
|
164
199
|
*
|
|
@@ -179,6 +214,42 @@ export declare class Memory {
|
|
|
179
214
|
* is — `MemoryStore.compact` builds it with `pyJoin`.
|
|
180
215
|
*/
|
|
181
216
|
compactOutcome(reserve?: number | null): CompactOutcome;
|
|
217
|
+
/** Consolidate what the project and profile layers hold under the same name. */
|
|
218
|
+
dream(dryRun?: boolean): string;
|
|
219
|
+
/**
|
|
220
|
+
* `dream`, with the decision it took carried beside the sentence it wrote.
|
|
221
|
+
*
|
|
222
|
+
* ONLY THE PROFILE LAYER IS CONSUMED. `layers` also carries read-only GRANTS, and a grant
|
|
223
|
+
* is another operator's store: consolidating a fact out of one is not this person's move
|
|
224
|
+
* to make, so `dream` never looks at them. The label is matched exactly (`profile`), never
|
|
225
|
+
* by prefix, because a grant is labelled `extra:<name>` and a prefix match on a directory
|
|
226
|
+
* called `profile-something` would reach one.
|
|
227
|
+
*
|
|
228
|
+
* A `Memory` constructed directly — not through `layered` — has no profile layer at all,
|
|
229
|
+
* and that is `no-profile-layer` rather than an error: there is nothing to consolidate
|
|
230
|
+
* ACROSS when only one layer is bound.
|
|
231
|
+
*
|
|
232
|
+
* `dryRun` DEFAULTS TO TRUE. This is the only op in this component that writes into the
|
|
233
|
+
* user's home directory, and it is the only one whose effect is machine-wide: a fact
|
|
234
|
+
* archived out of the profile store stops answering for every other project on this
|
|
235
|
+
* machine that has no store of its own. A destructive consolidation nobody can preview is
|
|
236
|
+
* not shippable, so the safe call is the short one.
|
|
237
|
+
*/
|
|
238
|
+
dreamOutcome(dryRun?: boolean): DreamOutcome;
|
|
239
|
+
/**
|
|
240
|
+
* The whole diff as prose: what merged, what was dated, what moved, what it cost.
|
|
241
|
+
*
|
|
242
|
+
* THE HONESTY SENTENCE AT THE END IS NOT DECORATION. Row 5 was planned as a token saving
|
|
243
|
+
* and J45-1 measured that it is not one: the profile store has no `index.md` on disk, its
|
|
244
|
+
* index is derived at read time, and nothing loads it into a prompt — so deduplicating it
|
|
245
|
+
* frees approximately zero prompt bytes. The value is that one ruling now has one copy
|
|
246
|
+
* instead of two that can disagree, and the reply says which of those two things the
|
|
247
|
+
* operator just bought.
|
|
248
|
+
*
|
|
249
|
+
* `formatFixed3` and not `toFixed(3)` for the two similarity figures: see the header of
|
|
250
|
+
* `dream.ts`. CPython's `.3f` rounds ties to EVEN and they are reachable.
|
|
251
|
+
*/
|
|
252
|
+
private static dreamReply;
|
|
182
253
|
/**
|
|
183
254
|
* `(index bytes, budget)` for the writable project store; bytes may be `null`.
|
|
184
255
|
*
|
package/dist/memory/component.js
CHANGED
|
@@ -29,7 +29,8 @@ import { BantamError } from '../errors.js';
|
|
|
29
29
|
import { pyStrip } from './factfile.js';
|
|
30
30
|
import { countFacts, loadGrants, MEMORY_DIR_ENV, PROJECT_STORE, resolveProjectStore, } from './layers.js';
|
|
31
31
|
import { pyHome, pyJoin, pyName, pyParent, pyResolve, PyOSError, PyUnicodeDecodeError } from './pyfs.js';
|
|
32
|
-
import { DEFAULT_INDEX_BUDGET, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyHashKey, pyText, } from './store.js';
|
|
32
|
+
import { DEFAULT_INDEX_BUDGET, MemoryBudgetExceeded, MemoryStore, MemoryValidationError, pyHashKey, pyText, RECALL_MIN_SCORE_RATIO, } from './store.js';
|
|
33
|
+
import { dream as runDream, formatFixed3, SUPERSEDED_HEADING } from './dream.js';
|
|
33
34
|
/**
|
|
34
35
|
* The model's spelling adapted to the store's contract: lowercase, `_`/space -> `-`.
|
|
35
36
|
*
|
|
@@ -177,8 +178,8 @@ export class Memory {
|
|
|
177
178
|
* fact. The floor is the operator's configured default, not a constant, so a consumer who
|
|
178
179
|
* really wants top-1 says so once at construction.
|
|
179
180
|
*/
|
|
180
|
-
recall(query, k = null) {
|
|
181
|
-
return this.recallOutcome(query, k).reply;
|
|
181
|
+
recall(query, k = null, minRatio = RECALL_MIN_SCORE_RATIO) {
|
|
182
|
+
return this.recallOutcome(query, k, minRatio).reply;
|
|
182
183
|
}
|
|
183
184
|
/**
|
|
184
185
|
* `recall`, carrying the walk it performed as numbers rather than as prose.
|
|
@@ -196,8 +197,18 @@ export class Memory {
|
|
|
196
197
|
* refuses to list contributes nothing and raises the `unreadable` count instead of being
|
|
197
198
|
* scored as empty; that distinction is the whole subject of `nothingToReport` below and
|
|
198
199
|
* must not be undone here.
|
|
200
|
+
*
|
|
201
|
+
* `minRatio` (roadmap #6) rides through to every layer's `MemoryStore.recall` unchanged,
|
|
202
|
+
* so the gate is measured against EACH LAYER's own best score and never across layers: a
|
|
203
|
+
* profile fact does not have to out-score the project store's top hit to be admitted,
|
|
204
|
+
* because the two stores are answering as two stores. Its default is
|
|
205
|
+
* `RECALL_MIN_SCORE_RATIO` = 0.0, which keeps every fact `recall` was going to return,
|
|
206
|
+
* and nothing on the tool path passes anything else today. A ratio outside `[0.0, 1.0]`
|
|
207
|
+
* raises out of the FIRST layer, which is the writable project store, so it surfaces as
|
|
208
|
+
* the error it is rather than as an `unreadable` count — the read-only-layer `catch`
|
|
209
|
+
* below would otherwise file a caller's bad argument as a corrupt grant.
|
|
199
210
|
*/
|
|
200
|
-
recallOutcome(query, k = null) {
|
|
211
|
+
recallOutcome(query, k = null, minRatio = RECALL_MIN_SCORE_RATIO) {
|
|
201
212
|
const budget = k === null ? this.k : Math.max(k, this.k);
|
|
202
213
|
const picked = [];
|
|
203
214
|
const seen = new Set();
|
|
@@ -210,7 +221,7 @@ export class Memory {
|
|
|
210
221
|
reached += 1;
|
|
211
222
|
let facts;
|
|
212
223
|
try {
|
|
213
|
-
facts = store.recall(query, budget, writable);
|
|
224
|
+
facts = store.recall(query, budget, writable, minRatio);
|
|
214
225
|
}
|
|
215
226
|
catch (e) {
|
|
216
227
|
if (!(e instanceof BantamError || e instanceof PyOSError || e instanceof PyUnicodeDecodeError)) {
|
|
@@ -312,6 +323,147 @@ export class Memory {
|
|
|
312
323
|
budget: result.budget,
|
|
313
324
|
};
|
|
314
325
|
}
|
|
326
|
+
/** Consolidate what the project and profile layers hold under the same name. */
|
|
327
|
+
dream(dryRun = true) {
|
|
328
|
+
return this.dreamOutcome(dryRun).reply;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* `dream`, with the decision it took carried beside the sentence it wrote.
|
|
332
|
+
*
|
|
333
|
+
* ONLY THE PROFILE LAYER IS CONSUMED. `layers` also carries read-only GRANTS, and a grant
|
|
334
|
+
* is another operator's store: consolidating a fact out of one is not this person's move
|
|
335
|
+
* to make, so `dream` never looks at them. The label is matched exactly (`profile`), never
|
|
336
|
+
* by prefix, because a grant is labelled `extra:<name>` and a prefix match on a directory
|
|
337
|
+
* called `profile-something` would reach one.
|
|
338
|
+
*
|
|
339
|
+
* A `Memory` constructed directly — not through `layered` — has no profile layer at all,
|
|
340
|
+
* and that is `no-profile-layer` rather than an error: there is nothing to consolidate
|
|
341
|
+
* ACROSS when only one layer is bound.
|
|
342
|
+
*
|
|
343
|
+
* `dryRun` DEFAULTS TO TRUE. This is the only op in this component that writes into the
|
|
344
|
+
* user's home directory, and it is the only one whose effect is machine-wide: a fact
|
|
345
|
+
* archived out of the profile store stops answering for every other project on this
|
|
346
|
+
* machine that has no store of its own. A destructive consolidation nobody can preview is
|
|
347
|
+
* not shippable, so the safe call is the short one.
|
|
348
|
+
*/
|
|
349
|
+
dreamOutcome(dryRun = true) {
|
|
350
|
+
const profile = this.layers.find(([label]) => label === 'profile')?.[1] ?? null;
|
|
351
|
+
if (profile === null) {
|
|
352
|
+
return {
|
|
353
|
+
reply: 'nothing to consolidate: no profile layer is bound, so the project ' +
|
|
354
|
+
`store ${this.store.root} is the only layer there is.`,
|
|
355
|
+
status: 'no-profile-layer',
|
|
356
|
+
dryRun,
|
|
357
|
+
merged: 0,
|
|
358
|
+
consumed: 0,
|
|
359
|
+
absolutised: 0,
|
|
360
|
+
superseded: 0,
|
|
361
|
+
indexBefore: 0,
|
|
362
|
+
indexAfter: 0,
|
|
363
|
+
budget: this.store.indexBudget,
|
|
364
|
+
result: null,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const result = runDream(this.store, profile, dryRun);
|
|
368
|
+
let status;
|
|
369
|
+
if (result.overBudget)
|
|
370
|
+
status = 'refused-budget';
|
|
371
|
+
else if (result.changes === 0)
|
|
372
|
+
status = 'nothing-to-consolidate';
|
|
373
|
+
else if (result.applied)
|
|
374
|
+
status = 'consolidated';
|
|
375
|
+
else
|
|
376
|
+
status = 'previewed';
|
|
377
|
+
return {
|
|
378
|
+
reply: Memory.dreamReply(status, result),
|
|
379
|
+
status,
|
|
380
|
+
dryRun: result.dryRun,
|
|
381
|
+
merged: result.merged.length,
|
|
382
|
+
consumed: result.consumed.length,
|
|
383
|
+
absolutised: result.absolutised.length,
|
|
384
|
+
superseded: result.superseded.length,
|
|
385
|
+
indexBefore: result.indexBefore,
|
|
386
|
+
indexAfter: result.indexAfter,
|
|
387
|
+
budget: result.budget,
|
|
388
|
+
result,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The whole diff as prose: what merged, what was dated, what moved, what it cost.
|
|
393
|
+
*
|
|
394
|
+
* THE HONESTY SENTENCE AT THE END IS NOT DECORATION. Row 5 was planned as a token saving
|
|
395
|
+
* and J45-1 measured that it is not one: the profile store has no `index.md` on disk, its
|
|
396
|
+
* index is derived at read time, and nothing loads it into a prompt — so deduplicating it
|
|
397
|
+
* frees approximately zero prompt bytes. The value is that one ruling now has one copy
|
|
398
|
+
* instead of two that can disagree, and the reply says which of those two things the
|
|
399
|
+
* operator just bought.
|
|
400
|
+
*
|
|
401
|
+
* `formatFixed3` and not `toFixed(3)` for the two similarity figures: see the header of
|
|
402
|
+
* `dream.ts`. CPython's `.3f` rounds ties to EVEN and they are reachable.
|
|
403
|
+
*/
|
|
404
|
+
static dreamReply(status, result) {
|
|
405
|
+
const lines = [];
|
|
406
|
+
if (status === 'nothing-to-consolidate') {
|
|
407
|
+
lines.push(`nothing to consolidate: ${result.projectRoot} and ${result.profileRoot} ` +
|
|
408
|
+
'hold no fact under the same name, and no project fact carries a relative ' +
|
|
409
|
+
'date this pass will resolve.');
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const identical = result.merged.filter((merge) => merge.kind === 'identical').length;
|
|
413
|
+
const diverged = result.merged.length - identical;
|
|
414
|
+
const verb = status === 'consolidated' ? 'consolidated' : 'would consolidate';
|
|
415
|
+
lines.push(`${verb} ${result.merged.length} cross-layer name collision(s) — ${identical} ` +
|
|
416
|
+
`byte-identical, ${diverged} diverged and unioned — and rewrote ` +
|
|
417
|
+
`${result.rewritten.length} project fact(s). The survivor stays in the ` +
|
|
418
|
+
`project layer; the profile copy moves to ${result.archiveDir} and is NOT ` +
|
|
419
|
+
'deleted: restore it by name.');
|
|
420
|
+
}
|
|
421
|
+
for (const merge of result.merged) {
|
|
422
|
+
lines.push(`- ${pyText(merge.name)} (${merge.kind}, name+description similarity ` +
|
|
423
|
+
`${formatFixed3(merge.jaccard)}) — body ${merge.bodyBefore} -> ${merge.bodyAfter} ` +
|
|
424
|
+
`bytes, ${merge.blocksAdded} block(s) kept from the ${merge.consumedLayer} ` +
|
|
425
|
+
`copy, survivor in ${merge.survivorLayer}`);
|
|
426
|
+
}
|
|
427
|
+
for (const record of result.superseded) {
|
|
428
|
+
lines.push(`- superseded '${record.subject}': the ${record.lostLayer} copy ` +
|
|
429
|
+
`(${record.lostDate}) lost to the ${record.keptLayer} copy ` +
|
|
430
|
+
`(${record.keptDate}); the older claim is kept verbatim under ` +
|
|
431
|
+
`'${SUPERSEDED_HEADING}'`);
|
|
432
|
+
}
|
|
433
|
+
for (const hit of result.absolutised) {
|
|
434
|
+
lines.push(`- dated ${pyText(hit.name)} (${hit.layer}): '${hit.term}' -> ${hit.resolved}, ` +
|
|
435
|
+
`resolved against that fact's own mtime ${hit.basis}, not today`);
|
|
436
|
+
}
|
|
437
|
+
for (const hit of result.unresolved) {
|
|
438
|
+
lines.push(`- left alone in ${pyText(hit.name)} (${hit.layer}): '${hit.term}' — no exact day ` +
|
|
439
|
+
'follows from an mtime, so nothing was substituted');
|
|
440
|
+
}
|
|
441
|
+
for (const [name, reason] of result.refused) {
|
|
442
|
+
lines.push(`- refused ${pyText(name)}: ${reason}`);
|
|
443
|
+
}
|
|
444
|
+
for (const pair of result.similarUnmerged) {
|
|
445
|
+
lines.push(`- similar but NOT merged: ${pyText(pair.projectName)} (project) and ` +
|
|
446
|
+
`${pyText(pair.profileName)} (profile) score ${formatFixed3(pair.jaccard)}; this pass merges ` +
|
|
447
|
+
'on name equality only, so nothing was done about it');
|
|
448
|
+
}
|
|
449
|
+
lines.push(`project index ${result.indexBefore} -> ${result.indexAfter} bytes against a ` +
|
|
450
|
+
`${result.budget}-byte budget; the profile index (derived, no file on disk) ` +
|
|
451
|
+
`${result.profileIndexBefore} -> ${result.profileIndexAfter}; fact bytes ` +
|
|
452
|
+
`across both layers ${result.factBytesBefore} -> ${result.factBytesAfter}.`);
|
|
453
|
+
if (status === 'refused-budget') {
|
|
454
|
+
lines.push(`NOTHING WAS WRITTEN: the merged index would be ${result.indexAfter} bytes ` +
|
|
455
|
+
`against a ${result.budget}-byte budget. Call \`memory_compact\` to archive ` +
|
|
456
|
+
'the stalest facts, then run this again.');
|
|
457
|
+
}
|
|
458
|
+
else if (result.dryRun) {
|
|
459
|
+
lines.push('DRY RUN: nothing was written. Re-run with dry_run false to apply it.');
|
|
460
|
+
}
|
|
461
|
+
lines.push('This is a correctness pass, not a token saving: the profile index is derived ' +
|
|
462
|
+
'at read time and is not loaded from a file, so consolidating it frees ' +
|
|
463
|
+
'approximately no prompt bytes. What it buys is one copy of a ruling instead ' +
|
|
464
|
+
'of two that can diverge.');
|
|
465
|
+
return lines.join('\n');
|
|
466
|
+
}
|
|
315
467
|
/**
|
|
316
468
|
* `(index bytes, budget)` for the writable project store; bytes may be `null`.
|
|
317
469
|
*
|