fraim 2.0.339 → 2.0.341
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/dist/registry/scripts/learning/completion-state.js +161 -0
- package/dist/src/cli/commands/cleanup-artifacts.js +2 -2
- package/dist/src/core/ai-mentor.js +1 -1
- package/dist/src/core/handoff-contracts.js +165 -37
- package/dist/src/core/proposal-set-contract.js +214 -0
- package/dist/src/core/run-recovery-state.js +9 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +36 -9
- package/dist/src/local-mcp-server/learning-context-builder.js +224 -20
- package/dist/src/local-mcp-server/learning-usage-analysis.js +35 -19
- package/dist/src/local-mcp-server/learning-usage-command.js +13 -14
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +59 -0
- package/dist/src/local-mcp-server/learning-usage-events.js +63 -57
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -1
- package/dist/src/local-mcp-server/learning-usage-store.js +235 -79
- package/dist/src/local-mcp-server/learning-usage-verify.js +13 -26
- package/dist/src/local-mcp-server/stdio-server.js +295 -112
- package/dist/src/local-mcp-server/usage-collector.js +5 -29
- package/dist/src/mcp/tool-schemas.js +2 -2
- package/dist/src/routes/analytics.js +10 -3
- package/dist/src/services/mcp-service.js +71 -11
- package/dist/src/services/usage-analytics-service.js +6 -3
- package/package.json +2 -1
|
@@ -21,15 +21,10 @@ const learning_usage_attestation_1 = require("./learning-usage-attestation");
|
|
|
21
21
|
const learning_usage_verify_1 = require("./learning-usage-verify");
|
|
22
22
|
const pack_home_1 = require("../cli/utils/pack-home");
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* The record is derived (#1103 R17), so the fix is to rebuild it rather than to
|
|
27
|
-
* reconcile it in place. Measured on a 733-file retrospective corpus: a rebuild
|
|
28
|
-
* lands with zero split identities and one model bucket, because the identity
|
|
29
|
-
* resolution and the model normalization fix the causes and a rebuilt store
|
|
30
|
-
* carries no legacy shape.
|
|
24
|
+
* Preserve checkpointed history and distinguish backup recovery from attestation backfill.
|
|
25
|
+
* Backfill supplements older firings; it cannot reconstruct missing offers.
|
|
31
26
|
*/
|
|
32
|
-
const REBUILD_NOTE =
|
|
27
|
+
const REBUILD_NOTE = "Keep `learning-usage.json`, its `.checkpoint` backup, `events/` and any conflict copies. After consolidation, the snapshot and checkpoint backup preserve lifetime counters and already-counted source markers; the remaining journal cannot rebuild that history. A missing or torn snapshot is read from its checkpoint backup automatically. Restore matching checkpoint and journal files from backup if both copies are damaged. Inspect split identities and conflict copies before repairing them; do not delete the snapshot to clear a warning. Separately, `learning-usage backfill` can recover older firings from retrospectives; raw learnings and retrospectives cannot restore runtime offers.";
|
|
33
28
|
function resolveWorkspaceRoot(options) {
|
|
34
29
|
return node_path_1.default.resolve(options.workspaceRoot || options.root || process.cwd());
|
|
35
30
|
}
|
|
@@ -282,7 +277,8 @@ const reportCommand = addModelOption(addCommonOptions(new commander_1.Command('r
|
|
|
282
277
|
});
|
|
283
278
|
emit(report, options.json, () => {
|
|
284
279
|
const lines = [
|
|
285
|
-
|
|
280
|
+
report.totals.entriesOffered === null ? 'Corpus totals are unavailable; listed entries have independent usable evidence.'
|
|
281
|
+
: `${report.totals.entriesOffered} entries were offered. ${report.totals.entriesWithFirings} fired. ${report.totals.entriesNeverFired} have never fired.`,
|
|
286
282
|
];
|
|
287
283
|
if (report.workingHardest.length) {
|
|
288
284
|
lines.push('', 'Working hardest');
|
|
@@ -300,7 +296,7 @@ const reportCommand = addModelOption(addCommonOptions(new commander_1.Command('r
|
|
|
300
296
|
report.ignoredWhileInContext.forEach((e, i) => lines.push(`${i + 1}. ${e.title}. Attested ignored ${e.ignored} time${e.ignored === 1 ? '' : 's'} (fired ${e.fired}). In context and the work went against it, so the entry needs work rather than another recurrence.`));
|
|
301
297
|
}
|
|
302
298
|
if (report.integrityFlagged > 0) {
|
|
303
|
-
lines.push('', `${report.integrityFlagged}
|
|
299
|
+
lines.push('', `${report.integrityFlagged} entries were excluded from usage conclusions. Run \`learning-usage verify\` for diagnostic details.`);
|
|
304
300
|
}
|
|
305
301
|
lines.push('', 'Nothing is retired from this view. Run sleep-on-learnings to decide.');
|
|
306
302
|
return lines.join('\n');
|
|
@@ -455,7 +451,8 @@ const pruneCommand = addCommonOptions(new commander_1.Command('prune'))
|
|
|
455
451
|
const workspaceRoot = resolveWorkspaceRoot(options);
|
|
456
452
|
const limits = (0, learning_usage_store_1.resolveUsageLimits)(workspaceRoot);
|
|
457
453
|
const result = (0, learning_usage_store_1.pruneUsageStore)(limits);
|
|
458
|
-
emit({ ...result, storePath: (0, learning_usage_store_1.resolveUsageStorePath)(), limits }, options.json, () =>
|
|
454
|
+
emit({ ...result, storePath: (0, learning_usage_store_1.resolveUsageStorePath)(), limits }, options.json, () => result.maintenanceStatus === 'deferred' ? `Maintenance deferred; history unchanged. ${result.evidence?.observations.join(', ') ?? ''}`
|
|
455
|
+
: `Pruned ${result.prunedOfferRecords} offer records and ${result.prunedFiringRecords} firing records across ${result.entries} entries. Aggregates unchanged.`);
|
|
459
456
|
});
|
|
460
457
|
function mb(bytes) {
|
|
461
458
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -477,9 +474,11 @@ const verifyCommand = addModelOption(addCommonOptions(new commander_1.Command('v
|
|
|
477
474
|
'',
|
|
478
475
|
];
|
|
479
476
|
const finding = (count, singular, plural, remedy) => count === 0 ? ` none: ${singular}` : ` ${count} ${count === 1 ? singular : plural}. ${remedy}`;
|
|
477
|
+
if (report.evidence)
|
|
478
|
+
lines.push(`Evidence: ${report.evidence.status}. ${report.evidence.observations.join(', ')}.`, ...(report.evidence.recovery ? [`Recovered from ${report.evidence.recovery}; primary replacement was not persisted by this read.`] : []), '');
|
|
480
479
|
lines.push('Identity', finding(report.twinPairs.length, 'lesson tracked under two keys', 'lessons tracked under two keys', 'Its offers and its firings are on different records, so every count about it is partial.'), report.unmatchedTitleKeys.length
|
|
481
480
|
? ` ${report.unmatchedTitleKeys.length} title-keyed record${report.unmatchedTitleKeys.length === 1 ? ' has' : 's have'} no id twin. Expected for entries no tier has stamped; run \`stamp-ids\` to reduce it.`
|
|
482
|
-
: ' every title-keyed record has been matched or has no twin', '', 'Counters', finding(report.invariantViolations.length, 'entry holds more attested firings than recorded offers', 'entries hold more attested firings than recorded offers', '
|
|
481
|
+
: ' every title-keyed record has been matched or has no twin', '', 'Counters', finding(report.invariantViolations.length, 'entry holds more attested firings than recorded offers', 'entries hold more attested firings than recorded offers', 'The counts are inconsistent. This observation does not establish their cause.'), finding(report.caseVariantModelBuckets.length, 'model bucket differs only by case', 'model buckets differ only by case', 'One model spelled two ways splits its own denominator, so every per-model conclusion about it is computed on a fraction of its traffic.'), finding(report.unknownModelAttribution.length, 'entry has incomplete model attribution', 'entries have incomplete model attribution', 'Affected model conclusions are withheld. The cause is not established; unknown historical attribution cannot be inferred from the current model.'), '', 'Storage');
|
|
483
482
|
if (report.storeCopies.length === 0) {
|
|
484
483
|
lines.push(' one store file, no unmerged copies');
|
|
485
484
|
}
|
|
@@ -488,14 +487,14 @@ const verifyCommand = addModelOption(addCommonOptions(new commander_1.Command('v
|
|
|
488
487
|
for (const copy of report.storeCopies) {
|
|
489
488
|
lines.push(` ${copy.path}: ${copy.readable ? `${copy.entries} entries, ${copy.offers} offers, ${copy.firings} firings` : `unreadable (${copy.error ?? 'unknown error'})`}`);
|
|
490
489
|
}
|
|
491
|
-
lines.push(' Nothing reads these.
|
|
490
|
+
lines.push(' Nothing reads these. Preserve them and the checkpoint backup for inspection; do not delete snapshots to clear this warning.');
|
|
492
491
|
}
|
|
493
492
|
lines.push(` event logs folded: ${report.eventLogs.length === 0 ? 'none' : report.eventLogs.map((l) => `${l.machineId} (${l.bytes} bytes)`).join(', ')}`);
|
|
494
493
|
for (const [machineId, lag] of Object.entries(report.checkpointLag)) {
|
|
495
494
|
lines.push(` checkpoint lag for ${machineId}: ${lag} bytes${lag > 0 ? ' behind the log head' : ''}`);
|
|
496
495
|
}
|
|
497
496
|
lines.push(report.compactionAdvised
|
|
498
|
-
? ` log size ${mb(report.totalLogBytes)} is past the ${mb(report.compactionAdvisoryBytes)} point where a cold fold stops fitting the context-injection budget. Run \`learning-usage prune\` to
|
|
497
|
+
? ` log size ${mb(report.totalLogBytes)} is past the ${mb(report.compactionAdvisoryBytes)} point where a cold fold stops fitting the context-injection budget. Run \`learning-usage prune\` to consolidate processed events into the snapshot and checkpoint backup, then clear them independently of retention.`
|
|
499
498
|
: ` log size ${mb(report.totalLogBytes)} of ${mb(report.compactionAdvisoryBytes)} before compaction is worth running.`);
|
|
500
499
|
if (report.rebuildAdvised)
|
|
501
500
|
lines.push('', REBUILD_NOTE);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.modelAttributionIncomplete = modelAttributionIncomplete;
|
|
4
|
+
exports.countEvidenceProblem = countEvidenceProblem;
|
|
5
|
+
exports.usageEvidenceExclusions = usageEvidenceExclusions;
|
|
6
|
+
const learning_usage_store_1 = require("./learning-usage-store");
|
|
7
|
+
const validCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
8
|
+
/** Missing attribution is unknown, including a known bucket beside unattributed history. */
|
|
9
|
+
function modelAttributionIncomplete(record) {
|
|
10
|
+
const buckets = [record.offered.byModel, record.fired.byModel];
|
|
11
|
+
if (buckets.some(map => !map || Object.entries(map).some(([name, n]) => !validCount(n) || !(0, learning_usage_store_1.normalizeModelName)(name) || (0, learning_usage_store_1.normalizeModelName)(name) !== name)))
|
|
12
|
+
return true;
|
|
13
|
+
return Object.values(record.offered.byModel).reduce((a, b) => a + b, 0) !== record.offered.total
|
|
14
|
+
|| Object.values(record.fired.byModel).reduce((a, b) => a + b, 0)
|
|
15
|
+
!== record.fired.byOutcome.applied + record.fired.byOutcome.prevented;
|
|
16
|
+
}
|
|
17
|
+
/** Validate counts independently of a cached integrity flag. Never alter the record. */
|
|
18
|
+
function countEvidenceProblem(record, offersEraStart) {
|
|
19
|
+
const counts = [record.offered.total, record.offered.aboveThreshold, record.fired.total,
|
|
20
|
+
...Object.values(record.offered.byModel), ...Object.values(record.fired.byModel),
|
|
21
|
+
...Object.values(record.fired.byOutcome)];
|
|
22
|
+
if (counts.some(n => !validCount(n)) || learning_usage_store_1.FIRING_OUTCOMES.some(outcome => !validCount(record.fired.byOutcome[outcome]))
|
|
23
|
+
|| record.offered.aboveThreshold > record.offered.total
|
|
24
|
+
|| Object.values(record.fired.byOutcome).reduce((a, b) => a + b, 0) !== record.fired.total) {
|
|
25
|
+
return 'Usage counters are inconsistent.';
|
|
26
|
+
}
|
|
27
|
+
const violation = record.integrity ?? (0, learning_usage_store_1.integrityViolation)(record, offersEraStart);
|
|
28
|
+
if (!offersEraStart && record.offered.total > 0 && record.fired.total > record.offered.total) {
|
|
29
|
+
return 'Excess uses lack provenance establishing the offer-recording era.';
|
|
30
|
+
}
|
|
31
|
+
return violation ? violation.detail : null;
|
|
32
|
+
}
|
|
33
|
+
/** Read-time exclusions follow the actual record, never ambient UI or model state. */
|
|
34
|
+
function usageEvidenceExclusions(store) {
|
|
35
|
+
const excluded = new Map();
|
|
36
|
+
for (const key of store.evidence?.excludedKeys ?? [])
|
|
37
|
+
excluded.set(key, 'Unreadable usage record.');
|
|
38
|
+
const byTitle = new Map();
|
|
39
|
+
for (const record of Object.values(store.entries)) {
|
|
40
|
+
const problem = countEvidenceProblem(record, store.offersEraStart);
|
|
41
|
+
if (store.evidence?.status === 'unavailable')
|
|
42
|
+
excluded.set(record.key, 'Usage history is unavailable.');
|
|
43
|
+
else if (problem)
|
|
44
|
+
excluded.set(record.key, problem);
|
|
45
|
+
if (!record.fileType)
|
|
46
|
+
continue;
|
|
47
|
+
for (const title of record.titles) {
|
|
48
|
+
const titleKey = (0, learning_usage_store_1.titleUsageKey)(record.fileType, title);
|
|
49
|
+
byTitle.set(titleKey, [...new Set([...(byTitle.get(titleKey) ?? []), record.key])]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const keys of byTitle.values()) {
|
|
53
|
+
if (keys.length > 1 && keys.some(key => key.startsWith('T:'))) {
|
|
54
|
+
for (const key of keys)
|
|
55
|
+
excluded.set(key, 'Ambiguous duplicate usage identity.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return excluded;
|
|
59
|
+
}
|
|
@@ -7,43 +7,17 @@ exports.listEventLogs = listEventLogs;
|
|
|
7
7
|
exports.readEventsSince = readEventsSince;
|
|
8
8
|
exports.compactEventLogs = compactEventLogs;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Per-machine usage journals: job runs append complete JSON lines; sleep folds
|
|
11
|
+
* them into a recoverable snapshot and removes processed records.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* A cleared journal contains only {_usageJournalThroughBytes:N}. This framing
|
|
14
|
+
* marker preserves its logical position across atomic replacement, so a crash
|
|
15
|
+
* between checkpoint publication and journal cleanup cannot reset offsets or
|
|
16
|
+
* count an event twice. It contains no activity details. New appends continue
|
|
17
|
+
* from N; an older/missing snapshot cannot silently cold-fold compacted history.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* OFFER/FIRING event → appendFileSync of one whole JSON line
|
|
21
|
-
* usage/learning-usage.json → a derived snapshot with a byte checkpoint
|
|
22
|
-
*
|
|
23
|
-
* Three measurements decided the shape, and each closed off an alternative:
|
|
24
|
-
*
|
|
25
|
-
* S2 16 processes x 1000 lines on the real OneDrive-synced path produced
|
|
26
|
-
* 16000/16000 complete parseable lines, 0 interleaved, 0 conflict copies.
|
|
27
|
-
* That is what removes the need for a same-machine lock. The per-machine
|
|
28
|
-
* filename is what removes the cross-machine question, because two
|
|
29
|
-
* machines then never write one file.
|
|
30
|
-
* S3 `renameSync` over a target any process holds open fails EPERM 100 times
|
|
31
|
-
* in 100 on Windows, on a synced path *and* a local one, because Node's
|
|
32
|
-
* `openSync(path,'r')` does not request FILE_SHARE_DELETE. The usage store
|
|
33
|
-
* is read by concurrent MCP processes by design, so rename-publish is not
|
|
34
|
-
* available here. Recorded so it is not re-proposed. See also #820.
|
|
35
|
-
* S4b A full fold is linear: 84 ms at today's volume, 794 ms at ten times it.
|
|
36
|
-
* A snapshot plus a tail fold is 2.6 ms at both 9.9 MB and 79 MB. Flat.
|
|
37
|
-
*
|
|
38
|
-
* This module owns the log files and nothing else: framing, appending, reading a
|
|
39
|
-
* tail from a byte offset, and compaction. It knows a line is a JSON object and
|
|
40
|
-
* knows nothing about what a usage record looks like. That is deliberate — the
|
|
41
|
-
* record shape lives in `learning-usage-store.ts`, which imports this, so there
|
|
42
|
-
* is no cycle between them.
|
|
43
|
-
*
|
|
44
|
-
* The same primitive is already used by `src/ai-hub/raw-event-log-store.ts`
|
|
45
|
-
* (append one JSON line, track byte offsets); this is the learning-usage
|
|
46
|
-
* equivalent rather than a second general mechanism.
|
|
19
|
+
* Maintenance requires no concurrent writers or sync activity. Ordinary writers
|
|
20
|
+
* append one payload; different machines use different filenames.
|
|
47
21
|
*/
|
|
48
22
|
const fs_1 = require("fs");
|
|
49
23
|
const os_1 = require("os");
|
|
@@ -51,6 +25,7 @@ const path_1 = require("path");
|
|
|
51
25
|
/** Subdirectory of the usage folder holding the per-machine logs. */
|
|
52
26
|
const EVENT_DIR_NAME = 'events';
|
|
53
27
|
const LOG_SUFFIX = '.jsonl';
|
|
28
|
+
const CHECKPOINT_MARKER = '_usageJournalThroughBytes';
|
|
54
29
|
/** The log directory that belongs to a snapshot: `<usage>/events/`. */
|
|
55
30
|
function resolveEventDir(snapshotPath) {
|
|
56
31
|
return (0, path_1.join)((0, path_1.dirname)(snapshotPath), EVENT_DIR_NAME);
|
|
@@ -136,10 +111,25 @@ function listEventLogs(eventDir) {
|
|
|
136
111
|
continue;
|
|
137
112
|
const path = (0, path_1.join)(eventDir, name);
|
|
138
113
|
let size = 0;
|
|
114
|
+
let baseOffset = 0;
|
|
115
|
+
let headerBytes = 0;
|
|
139
116
|
try {
|
|
140
117
|
const fd = (0, fs_1.openSync)(path, 'r');
|
|
141
118
|
try {
|
|
142
119
|
size = (0, fs_1.fstatSync)(fd).size;
|
|
120
|
+
const prefix = Buffer.alloc(Math.min(size, 128));
|
|
121
|
+
(0, fs_1.readSync)(fd, prefix, 0, prefix.length, 0);
|
|
122
|
+
const newline = prefix.indexOf(0x0a);
|
|
123
|
+
if (newline >= 0) {
|
|
124
|
+
try {
|
|
125
|
+
const header = JSON.parse(prefix.subarray(0, newline).toString('utf8'));
|
|
126
|
+
if (Object.keys(header).length === 1 && Number.isSafeInteger(header[CHECKPOINT_MARKER]) && header[CHECKPOINT_MARKER] >= 0) {
|
|
127
|
+
baseOffset = header[CHECKPOINT_MARKER];
|
|
128
|
+
headerBytes = newline + 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch { /* An ordinary first event, not a checkpoint marker. */ }
|
|
132
|
+
}
|
|
143
133
|
}
|
|
144
134
|
finally {
|
|
145
135
|
(0, fs_1.closeSync)(fd);
|
|
@@ -148,14 +138,20 @@ function listEventLogs(eventDir) {
|
|
|
148
138
|
catch {
|
|
149
139
|
continue;
|
|
150
140
|
}
|
|
151
|
-
logs.push({ machineId: name.slice(0, -LOG_SUFFIX.length), path, size });
|
|
141
|
+
logs.push({ machineId: name.slice(0, -LOG_SUFFIX.length), path, size, baseOffset, headerBytes });
|
|
152
142
|
}
|
|
153
143
|
return logs.sort((a, b) => a.machineId.localeCompare(b.machineId));
|
|
154
144
|
}
|
|
155
145
|
function readTail(log, from) {
|
|
156
|
-
const
|
|
146
|
+
const end = log.baseOffset + log.size - log.headerBytes;
|
|
147
|
+
if (from < log.baseOffset)
|
|
148
|
+
throw new Error('Usage checkpoint is missing or older than the consolidated journal. Restore learning-usage.json or its .checkpoint backup; raw retrospectives cannot restore runtime offers.');
|
|
149
|
+
const restarted = from > end;
|
|
150
|
+
if (restarted && log.baseOffset > 0)
|
|
151
|
+
throw new Error('Usage journal is behind its checkpoint; restore matching files before continuing.');
|
|
157
152
|
const start = restarted ? 0 : Math.max(0, from);
|
|
158
|
-
const
|
|
153
|
+
const physicalStart = start - log.baseOffset + log.headerBytes;
|
|
154
|
+
const length = log.size - physicalStart;
|
|
159
155
|
if (length <= 0) {
|
|
160
156
|
return { ...log, records: [], skippedLines: 0, through: start, restarted };
|
|
161
157
|
}
|
|
@@ -164,7 +160,7 @@ function readTail(log, from) {
|
|
|
164
160
|
const fd = (0, fs_1.openSync)(log.path, 'r');
|
|
165
161
|
try {
|
|
166
162
|
const buffer = Buffer.alloc(length);
|
|
167
|
-
const read = (0, fs_1.readSync)(fd, buffer, 0, length,
|
|
163
|
+
const read = (0, fs_1.readSync)(fd, buffer, 0, length, physicalStart);
|
|
168
164
|
text = buffer.subarray(0, read).toString('utf8');
|
|
169
165
|
}
|
|
170
166
|
finally {
|
|
@@ -202,7 +198,7 @@ function readTail(log, from) {
|
|
|
202
198
|
skippedLines += 1;
|
|
203
199
|
}
|
|
204
200
|
}
|
|
205
|
-
return { ...log, records, skippedLines, through:
|
|
201
|
+
return { ...log, records, skippedLines, through: end - partialBytes, restarted };
|
|
206
202
|
}
|
|
207
203
|
/**
|
|
208
204
|
* Read every log's tail past its checkpoint.
|
|
@@ -220,32 +216,42 @@ function readEventsSince(eventDir, checkpoints) {
|
|
|
220
216
|
};
|
|
221
217
|
}
|
|
222
218
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* cold fold rare, not bounded.
|
|
228
|
-
*
|
|
229
|
-
* Published by writing a temp file and renaming it. That is safe here and not on
|
|
230
|
-
* the snapshot (S3), because nothing holds a compacted log open for reading in
|
|
231
|
-
* the way concurrent MCP processes hold the snapshot: this runs from the CLI, and
|
|
232
|
-
* a failed rename leaves the original log untouched.
|
|
219
|
+
* Clear fully checkpointed logs whose records are all understood by this reader.
|
|
220
|
+
* Preserve unknown, malformed, partial or newly appended logs byte-for-byte.
|
|
221
|
+
* Flush then atomically replace each log with its logical-position header; the
|
|
222
|
+
* checkpoint saved before this call remains correct on either side of a crash.
|
|
233
223
|
*/
|
|
234
|
-
function compactEventLogs(eventDir, keep) {
|
|
235
|
-
const result = { scanned: 0, kept: 0, dropped: 0, skippedLines: 0, logs: [] };
|
|
224
|
+
function compactEventLogs(eventDir, keep, checkpoints) {
|
|
225
|
+
const result = { scanned: 0, kept: 0, dropped: 0, skippedLines: 0, logs: [], checkpoints: { ...checkpoints } };
|
|
236
226
|
for (const log of listEventLogs(eventDir)) {
|
|
237
|
-
const read = readTail(log,
|
|
227
|
+
const read = readTail(log, log.baseOffset);
|
|
238
228
|
result.scanned += read.records.length;
|
|
239
229
|
result.skippedLines += read.skippedLines;
|
|
230
|
+
result.kept += read.records.length;
|
|
231
|
+
// The snapshot knows nothing about a later append or torn
|
|
232
|
+
// tail. Leave that log and its old checkpoint alone for the next fold.
|
|
233
|
+
if (read.through !== log.baseOffset + log.size - log.headerBytes
|
|
234
|
+
|| read.through !== (checkpoints[log.machineId] ?? 0) || read.skippedLines > 0)
|
|
235
|
+
continue;
|
|
240
236
|
const retained = read.records.filter(keep);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (retained.length
|
|
237
|
+
// Unknown records may belong to a newer reader. Leave their whole log
|
|
238
|
+
// intact so their original positions remain meaningful.
|
|
239
|
+
if (retained.length > 0 || read.records.length === 0)
|
|
244
240
|
continue;
|
|
245
241
|
const temp = `${log.path}.compacting`;
|
|
246
242
|
try {
|
|
247
|
-
|
|
243
|
+
const payload = `${JSON.stringify({ [CHECKPOINT_MARKER]: read.through })}\n`;
|
|
244
|
+
(0, fs_1.writeFileSync)(temp, payload, { encoding: 'utf8', flush: true });
|
|
245
|
+
if ((0, fs_1.statSync)(log.path).size !== log.size) {
|
|
246
|
+
(0, fs_1.rmSync)(temp, { force: true });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
248
249
|
(0, fs_1.renameSync)(temp, log.path);
|
|
250
|
+
const dropped = read.records.length - retained.length;
|
|
251
|
+
result.kept -= dropped;
|
|
252
|
+
result.dropped += dropped;
|
|
253
|
+
// The logical checkpoint remains unchanged across the atomic replacement.
|
|
254
|
+
result.checkpoints[log.machineId] = read.through;
|
|
249
255
|
}
|
|
250
256
|
catch {
|
|
251
257
|
// The log is authoritative and the original is still intact, so a failed
|
|
@@ -15,6 +15,7 @@ exports.resolveCurrentModel = resolveCurrentModel;
|
|
|
15
15
|
* Nothing here writes.
|
|
16
16
|
*/
|
|
17
17
|
const learning_usage_store_1 = require("./learning-usage-store");
|
|
18
|
+
const learning_usage_eligibility_1 = require("./learning-usage-eligibility");
|
|
18
19
|
function toLookupValue(record) {
|
|
19
20
|
return {
|
|
20
21
|
key: record.key,
|
|
@@ -24,7 +25,7 @@ function toLookupValue(record) {
|
|
|
24
25
|
firedTotalAttestations: record.fired.total,
|
|
25
26
|
lastFired: record.fired.lastFiredAt,
|
|
26
27
|
lastOffered: record.offered.lastOfferedAt,
|
|
27
|
-
firedUnder: Object.keys(record.fired.byModel),
|
|
28
|
+
firedUnder: (0, learning_usage_eligibility_1.modelAttributionIncomplete)(record) ? undefined : Object.keys(record.fired.byModel),
|
|
28
29
|
byOutcome: record.fired.byOutcome,
|
|
29
30
|
log: record.fired.log,
|
|
30
31
|
};
|
|
@@ -36,7 +37,10 @@ function toLookupValue(record) {
|
|
|
36
37
|
*/
|
|
37
38
|
function buildUsageLookup(store) {
|
|
38
39
|
const lookup = new Map();
|
|
40
|
+
const excluded = (0, learning_usage_eligibility_1.usageEvidenceExclusions)(store);
|
|
39
41
|
for (const record of Object.values(store.entries)) {
|
|
42
|
+
if (excluded.has(record.key))
|
|
43
|
+
continue;
|
|
40
44
|
const value = toLookupValue(record);
|
|
41
45
|
lookup.set(record.key, value);
|
|
42
46
|
if (record.fileType) {
|