fraim 2.0.325 → 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, activeUserEmail) {
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.activeUserEmail);
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
- const decision = shouldDelete(candidate, retentionValue, now);
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
- node_fs_1.default.unlinkSync(candidate.absolutePath);
283
- action = 'deleted';
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.325",
3
+ "version": "2.0.326",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {