@massu/core 2.4.0 → 2.5.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.
@@ -1362,6 +1362,16 @@ var init_memory_vector = __esm({
1362
1362
  }
1363
1363
  });
1364
1364
 
1365
+ // src/lib/timestamps.ts
1366
+ function nowIso() {
1367
+ return (/* @__PURE__ */ new Date()).toISOString();
1368
+ }
1369
+ var init_timestamps = __esm({
1370
+ "src/lib/timestamps.ts"() {
1371
+ "use strict";
1372
+ }
1373
+ });
1374
+
1365
1375
  // src/db-backup.ts
1366
1376
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1367
1377
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3693,8 +3703,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3693
3703
  }
3694
3704
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3695
3705
  const result = db.prepare(`
3696
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3697
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3706
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3707
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3698
3708
  `).run(
3699
3709
  sessionId,
3700
3710
  turnNumber,
@@ -3703,14 +3713,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3703
3713
  toolCallsJson,
3704
3714
  toolCallCount,
3705
3715
  promptTokens,
3706
- responseTokens
3716
+ responseTokens,
3717
+ nowIso()
3707
3718
  );
3708
3719
  return Number(result.lastInsertRowid);
3709
3720
  }
3710
3721
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3711
3722
  db.prepare(`
3712
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3713
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3723
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
3724
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3714
3725
  `).run(
3715
3726
  sessionId,
3716
3727
  turnNumber,
@@ -3719,7 +3730,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3719
3730
  inputSize,
3720
3731
  outputSize,
3721
3732
  success ? 1 : 0,
3722
- filesInvolved ? JSON.stringify(filesInvolved) : null
3733
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3734
+ nowIso()
3723
3735
  );
3724
3736
  }
3725
3737
  function getLastProcessedLine(db, sessionId) {
@@ -3978,6 +3990,7 @@ var init_memory_db = __esm({
3978
3990
  init_db_driver();
3979
3991
  init_config();
3980
3992
  init_memory_vector();
3993
+ init_timestamps();
3981
3994
  init_db_backup();
3982
3995
  init_rule_delivery();
3983
3996
  init_rule_candidate_store();
@@ -4985,6 +4998,7 @@ var init_memory_files_config = __esm({
4985
4998
  // src/memory-renderer.ts
4986
4999
  var memory_renderer_exports = {};
4987
5000
  __export(memory_renderer_exports, {
5001
+ RenderAccountingLeak: () => RenderAccountingLeak,
4988
5002
  composeFile: () => composeFile,
4989
5003
  renderMemoryFiles: () => renderMemoryFiles,
4990
5004
  stripFrontmatter: () => stripFrontmatter
@@ -4996,7 +5010,7 @@ import { createHash as createHash5 } from "crypto";
4996
5010
  function renderMemoryFiles(db, candidates, opts) {
4997
5011
  const config = opts.config ?? resolveMemoryFilesConfig();
4998
5012
  if (!config.renderEnabled) {
4999
- return EMPTY("render_disabled");
5013
+ return EMPTY("render_disabled", candidates.length);
5000
5014
  }
5001
5015
  const dryRun = opts.dryRun === true;
5002
5016
  const now = opts.now ?? Date.now();
@@ -5005,7 +5019,7 @@ function renderMemoryFiles(db, candidates, opts) {
5005
5019
  });
5006
5020
  const { memoryDir } = opts;
5007
5021
  if (!existsSync19(memoryDir)) {
5008
- return EMPTY("no_memory_dir");
5022
+ return EMPTY("no_memory_dir", candidates.length);
5009
5023
  }
5010
5024
  try {
5011
5025
  return withMemoryIndexLock(
@@ -5014,10 +5028,10 @@ function renderMemoryFiles(db, candidates, opts) {
5014
5028
  );
5015
5029
  } catch (err) {
5016
5030
  if (err instanceof MemoryIndexLockBusy) {
5017
- return EMPTY("lock_busy");
5031
+ return EMPTY("lock_busy", candidates.length);
5018
5032
  }
5019
5033
  audit("memory_file_render_refused", { reason: "unexpected_error", error: String(err) });
5020
- return EMPTY("error");
5034
+ return EMPTY("error", candidates.length);
5021
5035
  }
5022
5036
  }
5023
5037
  function renderLocked(db, candidates, o) {
@@ -5027,11 +5041,14 @@ function renderLocked(db, candidates, o) {
5027
5041
  try {
5028
5042
  tombstones = readTombstones(memoryDir);
5029
5043
  } catch {
5030
- return EMPTY("tombstone_ledger_unreadable");
5044
+ return EMPTY("tombstone_ledger_unreadable", candidates.length);
5031
5045
  }
5032
5046
  const ownerOf = relPathOwnerLookup(db);
5033
5047
  const planned = [];
5034
- for (const c of candidates) {
5048
+ let unchanged = 0;
5049
+ let capped = 0;
5050
+ for (let i = 0; i < candidates.length; i++) {
5051
+ const c = candidates[i];
5035
5052
  if (!isLocalOrigin(c.origin)) {
5036
5053
  refusals.push({ name: c.name, reason: "non_local_origin", detail: c.origin || "unknown" });
5037
5054
  audit("memory_file_render_refused", { name: c.name, reason: "non_local_origin" });
@@ -5087,7 +5104,10 @@ function renderLocked(db, candidates, o) {
5087
5104
  continue;
5088
5105
  }
5089
5106
  const next = composeFile(c, home);
5090
- if (next !== null && next === existing) continue;
5107
+ if (next !== null && next === existing) {
5108
+ unchanged++;
5109
+ continue;
5110
+ }
5091
5111
  }
5092
5112
  const content = composeFile(c, home);
5093
5113
  if (content === null) {
@@ -5095,9 +5115,24 @@ function renderLocked(db, candidates, o) {
5095
5115
  continue;
5096
5116
  }
5097
5117
  planned.push({ c, relPath, absPath, content });
5098
- if (planned.length >= config.renderMaxFilesPerSession) break;
5118
+ if (planned.length >= config.renderMaxFilesPerSession) {
5119
+ capped = candidates.length - (i + 1);
5120
+ break;
5121
+ }
5122
+ }
5123
+ const accounted = planned.length + refusals.length + unchanged + capped;
5124
+ if (accounted !== candidates.length) {
5125
+ audit("memory_file_render_refused", {
5126
+ reason: "accounting_leak",
5127
+ considered: candidates.length,
5128
+ accounted
5129
+ });
5130
+ throw new RenderAccountingLeak(
5131
+ `render did not account for every candidate: considered=${candidates.length} but accounted=${accounted} (planned=${planned.length} refused=${refusals.length} unchanged=${unchanged} capped=${capped}). A candidate was dropped without a bucket.`
5132
+ );
5099
5133
  }
5100
5134
  const indexLines = buildIndexLines(db, memoryDir, planned, config);
5135
+ const books = { considered: candidates.length, unchanged, capped };
5101
5136
  if (dryRun) {
5102
5137
  return {
5103
5138
  enabled: true,
@@ -5105,11 +5140,20 @@ function renderLocked(db, candidates, o) {
5105
5140
  written: planned.map((p) => p.relPath),
5106
5141
  refusals,
5107
5142
  indexLines,
5108
- bytesWritten: 0
5143
+ bytesWritten: 0,
5144
+ ...books
5109
5145
  };
5110
5146
  }
5111
5147
  if (planned.length === 0 && indexLines.length === 0) {
5112
- return { enabled: true, dryRun: false, written: [], refusals, indexLines: [], bytesWritten: 0 };
5148
+ return {
5149
+ enabled: true,
5150
+ dryRun: false,
5151
+ written: [],
5152
+ refusals,
5153
+ indexLines: [],
5154
+ bytesWritten: 0,
5155
+ ...books
5156
+ };
5113
5157
  }
5114
5158
  if (!hasFreshBackup(memoryDir, home)) {
5115
5159
  try {
@@ -5119,7 +5163,7 @@ function renderLocked(db, candidates, o) {
5119
5163
  reason: "backup_failed",
5120
5164
  error: err instanceof BackupError ? err.message : String(err)
5121
5165
  });
5122
- return EMPTY("backup_failed");
5166
+ return EMPTY("backup_failed", candidates.length);
5123
5167
  }
5124
5168
  }
5125
5169
  const indexPath = o.indexPath ?? join13(memoryDir, "MEMORY.md");
@@ -5187,7 +5231,10 @@ function renderLocked(db, candidates, o) {
5187
5231
  errors: restored.errors
5188
5232
  });
5189
5233
  return {
5190
- ...EMPTY("rollback_incomplete"),
5234
+ ...EMPTY("rollback_incomplete", candidates.length),
5235
+ // The books were balanced before the write began; keep them rather than zeroing.
5236
+ unchanged,
5237
+ capped,
5191
5238
  enabled: true,
5192
5239
  refusals: [
5193
5240
  ...refusals,
@@ -5196,9 +5243,9 @@ function renderLocked(db, candidates, o) {
5196
5243
  };
5197
5244
  }
5198
5245
  audit("memory_file_render_refused", { reason: "write_failed", error: String(err) });
5199
- return { ...EMPTY("write_failed"), enabled: true, refusals };
5246
+ return { ...EMPTY("write_failed", candidates.length), enabled: true, refusals, unchanged, capped };
5200
5247
  }
5201
- return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten };
5248
+ return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten, ...books };
5202
5249
  }
5203
5250
  function sha256(s) {
5204
5251
  return createHash5("sha256").update(s, "utf8").digest("hex");
@@ -5267,7 +5314,7 @@ function buildIndexLines(db, memoryDir, planned, config) {
5267
5314
  function oneLine(s) {
5268
5315
  return s.replace(/[\r\n]+/g, " ").slice(0, 120);
5269
5316
  }
5270
- var EMPTY;
5317
+ var RenderAccountingLeak, EMPTY;
5271
5318
  var init_memory_renderer = __esm({
5272
5319
  "src/memory-renderer.ts"() {
5273
5320
  "use strict";
@@ -5282,14 +5329,22 @@ var init_memory_renderer = __esm({
5282
5329
  init_memory_index_region();
5283
5330
  init_memory_files_config();
5284
5331
  init_memory_file_ingest();
5285
- EMPTY = (reason) => ({
5332
+ RenderAccountingLeak = class extends Error {
5333
+ };
5334
+ EMPTY = (reason, considered = 0) => ({
5286
5335
  enabled: false,
5287
5336
  dryRun: false,
5288
5337
  written: [],
5289
5338
  refusals: [],
5290
5339
  indexLines: [],
5291
5340
  bytesWritten: 0,
5292
- skippedReason: reason
5341
+ skippedReason: reason,
5342
+ // An early gate refused, so every candidate was dropped — by that ONE named reason.
5343
+ // Reporting `considered` here is what distinguishes "nothing to do" from "walked away
5344
+ // from 59 rows because the lock was busy".
5345
+ considered,
5346
+ unchanged: 0,
5347
+ capped: 0
5293
5348
  });
5294
5349
  }
5295
5350
  });
@@ -5297,31 +5352,54 @@ var init_memory_renderer = __esm({
5297
5352
  // src/memory-render-candidates.ts
5298
5353
  var memory_render_candidates_exports = {};
5299
5354
  __export(memory_render_candidates_exports, {
5355
+ CandidateAccountingLeak: () => CandidateAccountingLeak,
5356
+ EXCLUSION_MEMORY_FILE_REINGEST: () => EXCLUSION_MEMORY_FILE_REINGEST,
5300
5357
  RENDERABLE_MEMORY_TYPES: () => RENDERABLE_MEMORY_TYPES,
5301
5358
  loadRenderCandidates: () => loadRenderCandidates
5302
5359
  });
5303
5360
  function loadRenderCandidates(db) {
5304
5361
  const cfg = resolveMemoryFilesConfig();
5305
5362
  const typePlaceholders = RENDERABLE_MEMORY_TYPES.map(() => "?").join(", ");
5363
+ const WHERE = `WHERE importance >= ?
5364
+ AND COALESCE(expired_at_epoch, 0) = 0
5365
+ AND type IN (${typePlaceholders})`;
5366
+ const { population } = db.prepare(`SELECT COUNT(*) AS population FROM observations ${WHERE}`).get(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
5306
5367
  const rows = db.prepare(
5307
5368
  `SELECT id, title, detail, importance, COALESCE(origin, 'local') AS origin
5308
5369
  FROM observations
5309
- WHERE importance >= ?
5310
- AND COALESCE(expired_at_epoch, 0) = 0
5311
- AND type IN (${typePlaceholders})
5370
+ ${WHERE}
5312
5371
  ORDER BY importance DESC, created_at_epoch DESC
5313
- LIMIT 50`
5372
+ LIMIT ${CANDIDATE_WINDOW}`
5314
5373
  ).all(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
5315
- return rows.filter((r) => !r.title.startsWith("[memory-file]")).map((r) => ({
5316
- observationId: r.id,
5317
- name: r.title,
5318
- title: r.title,
5319
- body: r.detail ?? "",
5320
- importance: r.importance,
5321
- origin: r.origin
5322
- }));
5374
+ const reingest = rows.filter((r) => r.title.startsWith("[memory-file]"));
5375
+ const kept = rows.filter((r) => !r.title.startsWith("[memory-file]"));
5376
+ const excluded = reingest.length ? [{ reason: EXCLUSION_MEMORY_FILE_REINGEST, count: reingest.length }] : [];
5377
+ const ledger = {
5378
+ population,
5379
+ windowed: rows.length,
5380
+ truncatedByWindow: Math.max(0, population - rows.length),
5381
+ excluded,
5382
+ returned: kept.length
5383
+ };
5384
+ const accounted = ledger.returned + ledger.truncatedByWindow + excluded.reduce((n, e) => n + e.count, 0);
5385
+ if (accounted !== population) {
5386
+ throw new CandidateAccountingLeak(
5387
+ `candidate ledger does not balance: population=${population} but accounted=${accounted} (returned=${ledger.returned} truncated=${ledger.truncatedByWindow} excluded=${JSON.stringify(excluded)}). A row was dropped without a bucket.`
5388
+ );
5389
+ }
5390
+ return {
5391
+ ledger,
5392
+ candidates: kept.map((r) => ({
5393
+ observationId: r.id,
5394
+ name: r.title,
5395
+ title: r.title,
5396
+ body: r.detail ?? "",
5397
+ importance: r.importance,
5398
+ origin: r.origin
5399
+ }))
5400
+ };
5323
5401
  }
5324
- var RENDERABLE_MEMORY_TYPES;
5402
+ var RENDERABLE_MEMORY_TYPES, CANDIDATE_WINDOW, EXCLUSION_MEMORY_FILE_REINGEST, CandidateAccountingLeak;
5325
5403
  var init_memory_render_candidates = __esm({
5326
5404
  "src/memory-render-candidates.ts"() {
5327
5405
  "use strict";
@@ -5332,6 +5410,10 @@ var init_memory_render_candidates = __esm({
5332
5410
  "incident_near_miss",
5333
5411
  "cr_violation"
5334
5412
  ];
5413
+ CANDIDATE_WINDOW = 50;
5414
+ EXCLUSION_MEMORY_FILE_REINGEST = "memory_file_reingest";
5415
+ CandidateAccountingLeak = class extends Error {
5416
+ };
5335
5417
  }
5336
5418
  });
5337
5419
 
@@ -14149,7 +14231,8 @@ Session memory, code intelligence, and governance are now active.
14149
14231
  if (memoryDir) {
14150
14232
  const { renderMemoryFiles: renderMemoryFiles2 } = await Promise.resolve().then(() => (init_memory_renderer(), memory_renderer_exports));
14151
14233
  const { loadRenderCandidates: loadRenderCandidates2 } = await Promise.resolve().then(() => (init_memory_render_candidates(), memory_render_candidates_exports));
14152
- renderMemoryFiles2(db, loadRenderCandidates2(db), { memoryDir });
14234
+ const { candidates } = loadRenderCandidates2(db);
14235
+ renderMemoryFiles2(db, candidates, { memoryDir });
14153
14236
  }
14154
14237
  } catch (_renderErr) {
14155
14238
  }
@@ -1339,6 +1339,16 @@ var init_memory_vector = __esm({
1339
1339
  }
1340
1340
  });
1341
1341
 
1342
+ // src/lib/timestamps.ts
1343
+ function nowIso() {
1344
+ return (/* @__PURE__ */ new Date()).toISOString();
1345
+ }
1346
+ var init_timestamps = __esm({
1347
+ "src/lib/timestamps.ts"() {
1348
+ "use strict";
1349
+ }
1350
+ });
1351
+
1342
1352
  // src/db-backup.ts
1343
1353
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1344
1354
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3695,8 +3705,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3695
3705
  }
3696
3706
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3697
3707
  const result = db.prepare(`
3698
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3699
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3708
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3709
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3700
3710
  `).run(
3701
3711
  sessionId,
3702
3712
  turnNumber,
@@ -3705,14 +3715,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3705
3715
  toolCallsJson,
3706
3716
  toolCallCount,
3707
3717
  promptTokens,
3708
- responseTokens
3718
+ responseTokens,
3719
+ nowIso()
3709
3720
  );
3710
3721
  return Number(result.lastInsertRowid);
3711
3722
  }
3712
3723
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3713
3724
  db.prepare(`
3714
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3715
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3725
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
3726
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3716
3727
  `).run(
3717
3728
  sessionId,
3718
3729
  turnNumber,
@@ -3721,7 +3732,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3721
3732
  inputSize,
3722
3733
  outputSize,
3723
3734
  success ? 1 : 0,
3724
- filesInvolved ? JSON.stringify(filesInvolved) : null
3735
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3736
+ nowIso()
3725
3737
  );
3726
3738
  }
3727
3739
  function getLastProcessedLine(db, sessionId) {
@@ -3980,6 +3992,7 @@ var init_memory_db = __esm({
3980
3992
  init_db_driver();
3981
3993
  init_config();
3982
3994
  init_memory_vector();
3995
+ init_timestamps();
3983
3996
  init_db_backup();
3984
3997
  init_rule_delivery();
3985
3998
  init_rule_candidate_store();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 56 workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "./dist/cli.js",