fraim 2.0.324 → 2.0.326
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.
|
@@ -627,6 +627,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
627
627
|
},
|
|
628
628
|
"learning_usage": {
|
|
629
629
|
"kind": "number"
|
|
630
|
+
},
|
|
631
|
+
"hub_event_logs": {
|
|
632
|
+
"kind": "number"
|
|
630
633
|
}
|
|
631
634
|
}
|
|
632
635
|
}
|
|
@@ -768,5 +771,6 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
768
771
|
"artifact_retention_period.evidence",
|
|
769
772
|
"artifact_retention_period.feedback",
|
|
770
773
|
"artifact_retention_period.cleanup_manifests",
|
|
771
|
-
"artifact_retention_period.learning_usage"
|
|
774
|
+
"artifact_retention_period.learning_usage",
|
|
775
|
+
"artifact_retention_period.hub_event_logs"
|
|
772
776
|
];
|
|
@@ -8,6 +8,7 @@ exports.resolveArtifactRetentionConfig = resolveArtifactRetentionConfig;
|
|
|
8
8
|
exports.runArtifactCleanup = runArtifactCleanup;
|
|
9
9
|
exports.planArtifactCleanup = planArtifactCleanup;
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
11
12
|
const node_child_process_1 = require("node:child_process");
|
|
12
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
14
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
@@ -25,6 +26,7 @@ exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
|
25
26
|
// deleted. Aggregate counters are never pruned; deleting them would misreport
|
|
26
27
|
// history rather than minimise it.
|
|
27
28
|
'learning_usage',
|
|
29
|
+
'hub_event_logs',
|
|
28
30
|
];
|
|
29
31
|
function readWorkspaceConfig(workspaceRoot) {
|
|
30
32
|
try {
|
|
@@ -69,7 +71,7 @@ function resolveArtifactRetentionConfig(workspaceRoot) {
|
|
|
69
71
|
for (const category of exports.ARTIFACT_RETENTION_CATEGORIES) {
|
|
70
72
|
values[category] = category in rawRetention
|
|
71
73
|
? parseRetentionValue(category, rawRetention[category], defaultValue, invalidValues)
|
|
72
|
-
: defaultValue;
|
|
74
|
+
: category === 'hub_event_logs' && !('default' in rawRetention) ? 7 : defaultValue;
|
|
73
75
|
}
|
|
74
76
|
return { values, defaultValue, invalidValues };
|
|
75
77
|
}
|
|
@@ -172,7 +174,8 @@ function evidenceEligibilityDate(workspaceRoot, absolutePath, gitDates) {
|
|
|
172
174
|
const relativePath = normalizeRelative(workspaceRoot, absolutePath);
|
|
173
175
|
return gitDates.get(relativePath) ?? fileMtime(absolutePath);
|
|
174
176
|
}
|
|
175
|
-
function collectCandidates(workspaceRoot,
|
|
177
|
+
function collectCandidates(workspaceRoot, options) {
|
|
178
|
+
const { activeUserEmail } = options;
|
|
176
179
|
const candidates = [];
|
|
177
180
|
const add = (category, absolutePath, eligible, reason, eligibilityDate) => {
|
|
178
181
|
candidates.push({
|
|
@@ -224,8 +227,82 @@ function collectCandidates(workspaceRoot, activeUserEmail) {
|
|
|
224
227
|
}
|
|
225
228
|
add('evidence', filePath, true, 'Evidence file.', eligibilityDate);
|
|
226
229
|
}
|
|
230
|
+
// A workspace policy owns only its project bucket. Manager/company archives
|
|
231
|
+
// and other projects must not inherit whichever project is being cleaned.
|
|
232
|
+
const logRoot = node_path_1.default.resolve(options.hubEventLogRoot ?? node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-event-logs'));
|
|
233
|
+
// Project archive keys use the resolved path; keep core cleanup independent
|
|
234
|
+
// of the separately shipped Hub package that also uses this convention.
|
|
235
|
+
const bucket = node_crypto_1.default.createHash('sha256').update(node_path_1.default.resolve(workspaceRoot)).digest('hex').slice(0, 24);
|
|
236
|
+
const bucketDir = node_path_1.default.join(logRoot, bucket);
|
|
237
|
+
try {
|
|
238
|
+
if (!node_fs_1.default.lstatSync(bucketDir).isDirectory())
|
|
239
|
+
return candidates;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return candidates;
|
|
243
|
+
}
|
|
244
|
+
for (const filePath of listFiles(bucketDir)) {
|
|
245
|
+
if (!filePath.endsWith('.jsonl'))
|
|
246
|
+
continue;
|
|
247
|
+
const state = readHubArchiveState(logRoot, filePath);
|
|
248
|
+
if (!state)
|
|
249
|
+
continue;
|
|
250
|
+
add('hub_event_logs', filePath, state.eligible, state.reason, state.eligibilityDate);
|
|
251
|
+
Object.assign(candidates[candidates.length - 1], state);
|
|
252
|
+
}
|
|
227
253
|
return candidates;
|
|
228
254
|
}
|
|
255
|
+
function readHubArchiveState(root, filePath) {
|
|
256
|
+
if (!isConfinedRegularFile(root, filePath))
|
|
257
|
+
return null;
|
|
258
|
+
try {
|
|
259
|
+
const metaPath = `${filePath}.meta.json`;
|
|
260
|
+
const hasMeta = isConfinedRegularFile(root, metaPath);
|
|
261
|
+
let meta = {};
|
|
262
|
+
if (hasMeta) {
|
|
263
|
+
try {
|
|
264
|
+
meta = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf8')) ?? {};
|
|
265
|
+
}
|
|
266
|
+
catch { /* Crashed or legacy archive: use mtime. */ }
|
|
267
|
+
}
|
|
268
|
+
const endedAt = typeof meta.endedAt === 'string' ? parseDate(meta.endedAt) : undefined;
|
|
269
|
+
const active = !endedAt && isLiveProcess(meta.ownerPid);
|
|
270
|
+
const stat = node_fs_1.default.statSync(filePath);
|
|
271
|
+
return {
|
|
272
|
+
eligible: !active,
|
|
273
|
+
reason: active ? 'Archive writer is still running.' : endedAt ? 'Run has ended.' : 'Archive has no terminal stamp; using last write.',
|
|
274
|
+
eligibilityDate: endedAt ?? stat.mtime,
|
|
275
|
+
sidecarPath: hasMeta ? metaPath : undefined,
|
|
276
|
+
hubArchive: { root, realPath: node_fs_1.default.realpathSync(filePath), mtimeMs: stat.mtimeMs, size: stat.size },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function isConfinedRegularFile(root, filePath) {
|
|
284
|
+
try {
|
|
285
|
+
if (!node_fs_1.default.lstatSync(filePath).isFile())
|
|
286
|
+
return false;
|
|
287
|
+
const relative = node_path_1.default.relative(node_fs_1.default.realpathSync(root), node_fs_1.default.realpathSync(filePath));
|
|
288
|
+
return relative !== '' && !relative.startsWith(`..${node_path_1.default.sep}`) && relative !== '..' && !node_path_1.default.isAbsolute(relative);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function isLiveProcess(pid) {
|
|
295
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
|
|
296
|
+
return false;
|
|
297
|
+
try {
|
|
298
|
+
process.kill(pid, 0);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// Access denied still means a process exists. PID reuse errs toward retention.
|
|
303
|
+
return error.code !== 'ESRCH';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
229
306
|
function daysBetween(from, to) {
|
|
230
307
|
return Math.floor((to.getTime() - from.getTime()) / 86_400_000);
|
|
231
308
|
}
|
|
@@ -254,7 +331,7 @@ function incrementCount(report, action) {
|
|
|
254
331
|
function runArtifactCleanup(workspaceRoot, options) {
|
|
255
332
|
const now = options.now ?? new Date();
|
|
256
333
|
const retention = resolveArtifactRetentionConfig(workspaceRoot);
|
|
257
|
-
const candidates = collectCandidates(workspaceRoot, options
|
|
334
|
+
const candidates = collectCandidates(workspaceRoot, options);
|
|
258
335
|
const report = {
|
|
259
336
|
generatedAt: now.toISOString(),
|
|
260
337
|
apply: options.apply,
|
|
@@ -265,7 +342,7 @@ function runArtifactCleanup(workspaceRoot, options) {
|
|
|
265
342
|
};
|
|
266
343
|
for (const candidate of candidates) {
|
|
267
344
|
const retentionValue = retention.values[candidate.category];
|
|
268
|
-
|
|
345
|
+
let decision = shouldDelete(candidate, retentionValue, now);
|
|
269
346
|
let action;
|
|
270
347
|
let error;
|
|
271
348
|
if (!candidate.eligible) {
|
|
@@ -279,8 +356,31 @@ function runArtifactCleanup(workspaceRoot, options) {
|
|
|
279
356
|
}
|
|
280
357
|
else {
|
|
281
358
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
359
|
+
if (candidate.category === 'hub_event_logs' && candidate.hubArchive) {
|
|
360
|
+
const prior = candidate.hubArchive;
|
|
361
|
+
const current = readHubArchiveState(prior.root, candidate.absolutePath);
|
|
362
|
+
// A terminal run can resume while other candidates are being collected.
|
|
363
|
+
// Revalidate immediately before unlink; a changed archive waits for the
|
|
364
|
+
// next cleanup, even when its new retention setting would be immediate.
|
|
365
|
+
if (!current || !current.eligible || current.hubArchive?.realPath !== prior.realPath
|
|
366
|
+
|| current.hubArchive?.mtimeMs !== prior.mtimeMs || current.hubArchive?.size !== prior.size
|
|
367
|
+
|| current.eligibilityDate?.getTime() !== candidate.eligibilityDate?.getTime()
|
|
368
|
+
|| current.sidecarPath !== candidate.sidecarPath) {
|
|
369
|
+
decision = { delete: false, reason: 'Archive changed or resumed after enumeration; retained for a later cleanup.' };
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
decision = shouldDelete({ ...candidate, ...current }, retentionValue, now);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!decision.delete) {
|
|
376
|
+
action = 'retained';
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
node_fs_1.default.unlinkSync(candidate.absolutePath);
|
|
380
|
+
if (candidate.sidecarPath)
|
|
381
|
+
node_fs_1.default.unlinkSync(candidate.sidecarPath);
|
|
382
|
+
action = 'deleted';
|
|
383
|
+
}
|
|
284
384
|
}
|
|
285
385
|
catch (err) {
|
|
286
386
|
action = 'failed';
|