@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.
package/dist/cli.js CHANGED
@@ -1548,6 +1548,16 @@ var init_memory_vector = __esm({
1548
1548
  }
1549
1549
  });
1550
1550
 
1551
+ // src/lib/timestamps.ts
1552
+ function nowIso() {
1553
+ return (/* @__PURE__ */ new Date()).toISOString();
1554
+ }
1555
+ var init_timestamps = __esm({
1556
+ "src/lib/timestamps.ts"() {
1557
+ "use strict";
1558
+ }
1559
+ });
1560
+
1551
1561
  // src/db-backup.ts
1552
1562
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1553
1563
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3972,8 +3982,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3972
3982
  }
3973
3983
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3974
3984
  const result = db.prepare(`
3975
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3976
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3985
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3986
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3977
3987
  `).run(
3978
3988
  sessionId,
3979
3989
  turnNumber,
@@ -3982,14 +3992,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3982
3992
  toolCallsJson,
3983
3993
  toolCallCount,
3984
3994
  promptTokens,
3985
- responseTokens
3995
+ responseTokens,
3996
+ nowIso()
3986
3997
  );
3987
3998
  return Number(result.lastInsertRowid);
3988
3999
  }
3989
4000
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3990
4001
  db.prepare(`
3991
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3992
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4002
+ 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)
4003
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3993
4004
  `).run(
3994
4005
  sessionId,
3995
4006
  turnNumber,
@@ -3998,7 +4009,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3998
4009
  inputSize,
3999
4010
  outputSize,
4000
4011
  success ? 1 : 0,
4001
- filesInvolved ? JSON.stringify(filesInvolved) : null
4012
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
4013
+ nowIso()
4002
4014
  );
4003
4015
  }
4004
4016
  function getLastProcessedLine(db, sessionId) {
@@ -4257,6 +4269,7 @@ var init_memory_db = __esm({
4257
4269
  init_db_driver();
4258
4270
  init_config();
4259
4271
  init_memory_vector();
4272
+ init_timestamps();
4260
4273
  init_db_backup();
4261
4274
  init_rule_delivery();
4262
4275
  init_rule_candidate_store();
@@ -15980,14 +15993,23 @@ __export(init_exports, {
15980
15993
  getInstallerVersion: () => getInstallerVersion,
15981
15994
  initMemoryDir: () => initMemoryDir,
15982
15995
  installHooks: () => installHooks,
15996
+ installMassuShim: () => installMassuShim,
15983
15997
  isTemplateName: () => isTemplateName,
15984
15998
  listTemplates: () => listTemplates,
15999
+ massuHookIdentity: () => massuHookIdentity,
16000
+ massuRuntimeCliPath: () => massuRuntimeCliPath,
16001
+ massuRuntimeDir: () => massuRuntimeDir,
16002
+ massuShimBody: () => massuShimBody,
16003
+ massuShimPath: () => massuShimPath,
16004
+ materializeMassuRuntime: () => materializeMassuRuntime,
15985
16005
  mergeHooksConfig: () => mergeHooksConfig,
15986
16006
  parseInitArgs: () => parseInitArgs,
15987
16007
  printInitHelp: () => printInitHelp,
15988
16008
  registerMcpServer: () => registerMcpServer,
15989
16009
  renderConfigYaml: () => renderConfigYaml,
15990
16010
  resolveHooksDir: () => resolveHooksDir,
16011
+ resolveMassuRuntimeCli: () => resolveMassuRuntimeCli,
16012
+ resolveMassuShim: () => resolveMassuShim,
15991
16013
  resolveTemplatesDir: () => resolveTemplatesDir,
15992
16014
  runInit: () => runInit,
15993
16015
  validateWrittenConfig: () => validateWrittenConfig,
@@ -15997,6 +16019,7 @@ import { closeSync as closeSync4, existsSync as existsSync19, fsyncSync as fsync
15997
16019
  import { resolve as resolve9, basename as basename6, dirname as dirname12 } from "path";
15998
16020
  import { fileURLToPath as fileURLToPath3 } from "url";
15999
16021
  import { homedir as homedir7 } from "os";
16022
+ import { execFileSync as execFileSync2 } from "child_process";
16000
16023
  import { stringify as yamlStringify, parse as yamlParse } from "yaml";
16001
16024
  function detectFramework(projectRoot) {
16002
16025
  const result = {
@@ -16570,11 +16593,8 @@ function registerMcpServer(projectRoot) {
16570
16593
  return false;
16571
16594
  }
16572
16595
  const version = getInstallerVersion();
16573
- servers.massu = {
16574
- type: "stdio",
16575
- command: "npx",
16576
- args: ["-y", `@massu/core@${version}`]
16577
- };
16596
+ const shim = resolveMassuShim();
16597
+ servers.massu = shim !== null ? { type: "stdio", command: shim, args: [version] } : { type: "stdio", command: "npx", args: ["-y", `@massu/core@${version}`] };
16578
16598
  existing.mcpServers = servers;
16579
16599
  writeFileSync5(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
16580
16600
  return true;
@@ -16591,7 +16611,98 @@ function resolveHooksDir() {
16591
16611
  }
16592
16612
  return "node_modules/@massu/core/dist/hooks";
16593
16613
  }
16594
- function hookCmd(version, hookName) {
16614
+ function massuRuntimeDir(version) {
16615
+ return resolve9(homedir7(), ".massu", "runtime", version);
16616
+ }
16617
+ function massuRuntimeCliPath(version) {
16618
+ return resolve9(massuRuntimeDir(version), "node_modules", "@massu", "core", "dist", "cli.js");
16619
+ }
16620
+ function resolveMassuRuntimeCli(version) {
16621
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return null;
16622
+ const cli = massuRuntimeCliPath(version);
16623
+ if (!existsSync19(cli)) return null;
16624
+ try {
16625
+ execFileSync2(process.execPath, [cli, "--version"], { stdio: "ignore", timeout: 2e4 });
16626
+ return cli;
16627
+ } catch {
16628
+ return null;
16629
+ }
16630
+ }
16631
+ function materializeMassuRuntime(version) {
16632
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return false;
16633
+ if (resolveMassuRuntimeCli(version) !== null) return true;
16634
+ const dir = massuRuntimeDir(version);
16635
+ try {
16636
+ mkdirSync10(dir, { recursive: true });
16637
+ execFileSync2(
16638
+ "npm",
16639
+ ["install", "--prefix", dir, `@massu/core@${version}`, "--no-audit", "--no-fund", "--loglevel=error"],
16640
+ { stdio: "ignore", timeout: 3e5 }
16641
+ );
16642
+ } catch {
16643
+ return false;
16644
+ }
16645
+ return resolveMassuRuntimeCli(version) !== null;
16646
+ }
16647
+ function massuShimPath() {
16648
+ return resolve9(homedir7(), ".massu", "bin", "massu-hook");
16649
+ }
16650
+ function massuShimBody() {
16651
+ return `#!/bin/sh
16652
+ # massu launcher shim \u2014 GENERATED, do not edit. Recreated by \`massu install-hooks\`.
16653
+ #
16654
+ # usage: massu-hook <version> <subcommand...>
16655
+ #
16656
+ # Resolves the massu runtime AT FIRE TIME so a deleted/moved runtime degrades to npx
16657
+ # instead of silently 404-ing the hook (the P-003 failure class). exec preserves stdin,
16658
+ # stdout, stderr and the exit code.
16659
+ set -u
16660
+ if [ "$#" -lt 1 ]; then
16661
+ echo "massu-hook: missing <version> argument" >&2
16662
+ exit 2
16663
+ fi
16664
+ MASSU_VERSION="$1"
16665
+ shift
16666
+ MASSU_CLI="\${HOME}/.massu/runtime/\${MASSU_VERSION}/node_modules/@massu/core/dist/cli.js"
16667
+ if [ -f "$MASSU_CLI" ]; then
16668
+ exec node "$MASSU_CLI" "$@"
16669
+ fi
16670
+ exec npx -y "@massu/core@\${MASSU_VERSION}" "$@"
16671
+ `;
16672
+ }
16673
+ function installMassuShim() {
16674
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return false;
16675
+ if (process.platform === "win32") return false;
16676
+ const shim = massuShimPath();
16677
+ try {
16678
+ mkdirSync10(dirname12(shim), { recursive: true });
16679
+ writeFileSync5(shim, massuShimBody(), "utf-8");
16680
+ chmodSync5(shim, 493);
16681
+ } catch {
16682
+ return false;
16683
+ }
16684
+ try {
16685
+ execFileSync2(shim, [], { stdio: "ignore", timeout: 1e4 });
16686
+ return false;
16687
+ } catch (e2) {
16688
+ const status = e2.status;
16689
+ return status === 2;
16690
+ }
16691
+ }
16692
+ function resolveMassuShim() {
16693
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return null;
16694
+ if (process.platform === "win32") return null;
16695
+ const shim = massuShimPath();
16696
+ if (!existsSync19(shim)) return null;
16697
+ try {
16698
+ execFileSync2(shim, [], { stdio: "ignore", timeout: 1e4 });
16699
+ return null;
16700
+ } catch (e2) {
16701
+ return e2.status === 2 ? shim : null;
16702
+ }
16703
+ }
16704
+ function hookCmd(version, hookName, shim) {
16705
+ if (shim !== null) return `"${shim}" ${version} hook-runner ${hookName}`;
16595
16706
  return `npx -y @massu/core@${version} hook-runner ${hookName}`;
16596
16707
  }
16597
16708
  function getHookTimeout(name) {
@@ -16599,11 +16710,12 @@ function getHookTimeout(name) {
16599
16710
  }
16600
16711
  function buildHooksConfig(_hooksDir) {
16601
16712
  const version = getInstallerVersion();
16713
+ const shim = resolveMassuShim();
16602
16714
  return {
16603
16715
  SessionStart: [
16604
16716
  {
16605
16717
  hooks: [
16606
- { type: "command", command: hookCmd(version, "session-start"), timeout: getHookTimeout("session-start") }
16718
+ { type: "command", command: hookCmd(version, "session-start", shim), timeout: getHookTimeout("session-start") }
16607
16719
  ]
16608
16720
  }
16609
16721
  ],
@@ -16618,26 +16730,26 @@ function buildHooksConfig(_hooksDir) {
16618
16730
  {
16619
16731
  matcher: "Bash|Write|Edit",
16620
16732
  hooks: [
16621
- { type: "command", command: hookCmd(version, "pre-tool-use-gate"), timeout: getHookTimeout("pre-tool-use-gate") }
16733
+ { type: "command", command: hookCmd(version, "pre-tool-use-gate", shim), timeout: getHookTimeout("pre-tool-use-gate") }
16622
16734
  ]
16623
16735
  }
16624
16736
  ],
16625
16737
  PostToolUse: [
16626
16738
  {
16627
16739
  hooks: [
16628
- { type: "command", command: hookCmd(version, "post-tool-use"), timeout: getHookTimeout("post-tool-use") },
16629
- { type: "command", command: hookCmd(version, "quality-event"), timeout: getHookTimeout("quality-event") },
16630
- { type: "command", command: hookCmd(version, "cost-tracker"), timeout: getHookTimeout("cost-tracker") }
16740
+ { type: "command", command: hookCmd(version, "post-tool-use", shim), timeout: getHookTimeout("post-tool-use") },
16741
+ { type: "command", command: hookCmd(version, "quality-event", shim), timeout: getHookTimeout("quality-event") },
16742
+ { type: "command", command: hookCmd(version, "cost-tracker", shim), timeout: getHookTimeout("cost-tracker") }
16631
16743
  ]
16632
16744
  },
16633
16745
  {
16634
16746
  matcher: "Edit|Write",
16635
16747
  hooks: [
16636
- { type: "command", command: hookCmd(version, "post-edit-context"), timeout: getHookTimeout("post-edit-context") },
16748
+ { type: "command", command: hookCmd(version, "post-edit-context", shim), timeout: getHookTimeout("post-edit-context") },
16637
16749
  // Auto-learning pipeline — classifies failures and detects fixes on
16638
16750
  // file changes. See Phase 5-6 of the autodetect plan.
16639
- { type: "command", command: hookCmd(version, "fix-detector"), timeout: getHookTimeout("fix-detector") },
16640
- { type: "command", command: hookCmd(version, "classify-failure"), timeout: getHookTimeout("classify-failure") }
16751
+ { type: "command", command: hookCmd(version, "fix-detector", shim), timeout: getHookTimeout("fix-detector") },
16752
+ { type: "command", command: hookCmd(version, "classify-failure", shim), timeout: getHookTimeout("classify-failure") }
16641
16753
  ]
16642
16754
  },
16643
16755
  {
@@ -16645,35 +16757,35 @@ function buildHooksConfig(_hooksDir) {
16645
16757
  hooks: [
16646
16758
  // Incident + rule enforcement pipelines fire on Write-only (incidents
16647
16759
  // are authored as .md files; rules are enforced after new-file drops).
16648
- { type: "command", command: hookCmd(version, "incident-pipeline"), timeout: getHookTimeout("incident-pipeline") },
16649
- { type: "command", command: hookCmd(version, "rule-enforcement-pipeline"), timeout: getHookTimeout("rule-enforcement-pipeline") }
16760
+ { type: "command", command: hookCmd(version, "incident-pipeline", shim), timeout: getHookTimeout("incident-pipeline") },
16761
+ { type: "command", command: hookCmd(version, "rule-enforcement-pipeline", shim), timeout: getHookTimeout("rule-enforcement-pipeline") }
16650
16762
  ]
16651
16763
  }
16652
16764
  ],
16653
16765
  Stop: [
16654
16766
  {
16655
16767
  hooks: [
16656
- { type: "command", command: hookCmd(version, "session-end"), timeout: getHookTimeout("session-end") },
16768
+ { type: "command", command: hookCmd(version, "session-end", shim), timeout: getHookTimeout("session-end") },
16657
16769
  // Session-end auto-learning aggregation (failure-class roll-up).
16658
- { type: "command", command: hookCmd(version, "auto-learning-pipeline"), timeout: getHookTimeout("auto-learning-pipeline") }
16770
+ { type: "command", command: hookCmd(version, "auto-learning-pipeline", shim), timeout: getHookTimeout("auto-learning-pipeline") }
16659
16771
  ]
16660
16772
  }
16661
16773
  ],
16662
16774
  PreCompact: [
16663
16775
  {
16664
16776
  hooks: [
16665
- { type: "command", command: hookCmd(version, "pre-compact"), timeout: getHookTimeout("pre-compact") }
16777
+ { type: "command", command: hookCmd(version, "pre-compact", shim), timeout: getHookTimeout("pre-compact") }
16666
16778
  ]
16667
16779
  }
16668
16780
  ],
16669
16781
  UserPromptSubmit: [
16670
16782
  {
16671
16783
  hooks: [
16672
- { type: "command", command: hookCmd(version, "user-prompt"), timeout: getHookTimeout("user-prompt") },
16673
- { type: "command", command: hookCmd(version, "intent-suggester"), timeout: getHookTimeout("intent-suggester") },
16784
+ { type: "command", command: hookCmd(version, "user-prompt", shim), timeout: getHookTimeout("user-prompt") },
16785
+ { type: "command", command: hookCmd(version, "intent-suggester", shim), timeout: getHookTimeout("intent-suggester") },
16674
16786
  // plan-living-memory-slice-1 P3-002: automatic relevant-recall.
16675
16787
  // Injects a compact "🧠 Relevant memory" block; fails open (empty).
16676
- { type: "command", command: hookCmd(version, "memory-recall"), timeout: getHookTimeout("memory-recall") }
16788
+ { type: "command", command: hookCmd(version, "memory-recall", shim), timeout: getHookTimeout("memory-recall") }
16677
16789
  ]
16678
16790
  }
16679
16791
  ]
@@ -16711,21 +16823,31 @@ function mergeHooksConfig(existing, additions) {
16711
16823
  }
16712
16824
  return merged;
16713
16825
  }
16826
+ function massuHookIdentity(command) {
16827
+ if (!/@massu\/core|[/\\]\.?massu[/\\]/.test(command)) return null;
16828
+ const m3 = /\bhook-runner\s+([A-Za-z0-9._-]+)/.exec(command);
16829
+ return m3 ? m3[1] : null;
16830
+ }
16714
16831
  function mergeHookEntries(existing, additions) {
16715
- const seen = /* @__PURE__ */ new Set();
16832
+ const seenCommands = /* @__PURE__ */ new Set();
16833
+ const seenIdentities = /* @__PURE__ */ new Set();
16716
16834
  const result = [];
16717
- for (const entry of additions ?? []) {
16718
- if (!entry || typeof entry.command !== "string") continue;
16719
- if (seen.has(entry.command)) continue;
16720
- seen.add(entry.command);
16721
- result.push(entry);
16722
- }
16723
- for (const entry of existing ?? []) {
16724
- if (!entry || typeof entry.command !== "string") continue;
16725
- if (seen.has(entry.command)) continue;
16726
- seen.add(entry.command);
16835
+ const take = (entry) => {
16836
+ if (!entry || typeof entry.command !== "string") return;
16837
+ const identity = massuHookIdentity(entry.command);
16838
+ if (identity !== null) {
16839
+ if (seenIdentities.has(identity)) return;
16840
+ seenIdentities.add(identity);
16841
+ seenCommands.add(entry.command);
16842
+ result.push(entry);
16843
+ return;
16844
+ }
16845
+ if (seenCommands.has(entry.command)) return;
16846
+ seenCommands.add(entry.command);
16727
16847
  result.push(entry);
16728
- }
16848
+ };
16849
+ for (const entry of additions ?? []) take(entry);
16850
+ for (const entry of existing ?? []) take(entry);
16729
16851
  return result;
16730
16852
  }
16731
16853
  function installHooks(projectRoot) {
@@ -18716,6 +18838,16 @@ async function runInstallHooks() {
18716
18838
  console.log("Massu AI - Hook Installation");
18717
18839
  console.log("============================");
18718
18840
  console.log("");
18841
+ const version = getInstallerVersion();
18842
+ const shimOk = installMassuShim();
18843
+ const runtimeOk = materializeMassuRuntime(version);
18844
+ if (shimOk && runtimeOk) {
18845
+ console.log(" Launcher: shim + runtime ready (fast path, ~9x cheaper per hook)");
18846
+ } else if (shimOk) {
18847
+ console.log(" Launcher: shim ready, runtime unavailable \u2014 shim will fall back to npx at fire time");
18848
+ } else {
18849
+ console.log(" Launcher: npx (shim unavailable \u2014 works, ~0.9s slower per hook)");
18850
+ }
18719
18851
  const { count } = installHooks(projectRoot);
18720
18852
  console.log(` Installed ${count} hooks in .claude/settings.local.json`);
18721
18853
  console.log("");
@@ -20454,7 +20586,7 @@ import { createHash as createHash8 } from "crypto";
20454
20586
  function renderMemoryFiles(db, candidates, opts) {
20455
20587
  const config = opts.config ?? resolveMemoryFilesConfig();
20456
20588
  if (!config.renderEnabled) {
20457
- return EMPTY("render_disabled");
20589
+ return EMPTY("render_disabled", candidates.length);
20458
20590
  }
20459
20591
  const dryRun = opts.dryRun === true;
20460
20592
  const now = opts.now ?? Date.now();
@@ -20463,7 +20595,7 @@ function renderMemoryFiles(db, candidates, opts) {
20463
20595
  });
20464
20596
  const { memoryDir } = opts;
20465
20597
  if (!existsSync33(memoryDir)) {
20466
- return EMPTY("no_memory_dir");
20598
+ return EMPTY("no_memory_dir", candidates.length);
20467
20599
  }
20468
20600
  try {
20469
20601
  return withMemoryIndexLock(
@@ -20472,10 +20604,10 @@ function renderMemoryFiles(db, candidates, opts) {
20472
20604
  );
20473
20605
  } catch (err) {
20474
20606
  if (err instanceof MemoryIndexLockBusy) {
20475
- return EMPTY("lock_busy");
20607
+ return EMPTY("lock_busy", candidates.length);
20476
20608
  }
20477
20609
  audit("memory_file_render_refused", { reason: "unexpected_error", error: String(err) });
20478
- return EMPTY("error");
20610
+ return EMPTY("error", candidates.length);
20479
20611
  }
20480
20612
  }
20481
20613
  function renderLocked(db, candidates, o) {
@@ -20485,11 +20617,14 @@ function renderLocked(db, candidates, o) {
20485
20617
  try {
20486
20618
  tombstones = readTombstones(memoryDir);
20487
20619
  } catch {
20488
- return EMPTY("tombstone_ledger_unreadable");
20620
+ return EMPTY("tombstone_ledger_unreadable", candidates.length);
20489
20621
  }
20490
20622
  const ownerOf = relPathOwnerLookup(db);
20491
20623
  const planned = [];
20492
- for (const c2 of candidates) {
20624
+ let unchanged = 0;
20625
+ let capped = 0;
20626
+ for (let i = 0; i < candidates.length; i++) {
20627
+ const c2 = candidates[i];
20493
20628
  if (!isLocalOrigin(c2.origin)) {
20494
20629
  refusals.push({ name: c2.name, reason: "non_local_origin", detail: c2.origin || "unknown" });
20495
20630
  audit("memory_file_render_refused", { name: c2.name, reason: "non_local_origin" });
@@ -20545,7 +20680,10 @@ function renderLocked(db, candidates, o) {
20545
20680
  continue;
20546
20681
  }
20547
20682
  const next = composeFile(c2, home);
20548
- if (next !== null && next === existing) continue;
20683
+ if (next !== null && next === existing) {
20684
+ unchanged++;
20685
+ continue;
20686
+ }
20549
20687
  }
20550
20688
  const content = composeFile(c2, home);
20551
20689
  if (content === null) {
@@ -20553,9 +20691,24 @@ function renderLocked(db, candidates, o) {
20553
20691
  continue;
20554
20692
  }
20555
20693
  planned.push({ c: c2, relPath, absPath, content });
20556
- if (planned.length >= config.renderMaxFilesPerSession) break;
20694
+ if (planned.length >= config.renderMaxFilesPerSession) {
20695
+ capped = candidates.length - (i + 1);
20696
+ break;
20697
+ }
20698
+ }
20699
+ const accounted = planned.length + refusals.length + unchanged + capped;
20700
+ if (accounted !== candidates.length) {
20701
+ audit("memory_file_render_refused", {
20702
+ reason: "accounting_leak",
20703
+ considered: candidates.length,
20704
+ accounted
20705
+ });
20706
+ throw new RenderAccountingLeak(
20707
+ `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.`
20708
+ );
20557
20709
  }
20558
20710
  const indexLines = buildIndexLines(db, memoryDir, planned, config);
20711
+ const books = { considered: candidates.length, unchanged, capped };
20559
20712
  if (dryRun) {
20560
20713
  return {
20561
20714
  enabled: true,
@@ -20563,11 +20716,20 @@ function renderLocked(db, candidates, o) {
20563
20716
  written: planned.map((p19) => p19.relPath),
20564
20717
  refusals,
20565
20718
  indexLines,
20566
- bytesWritten: 0
20719
+ bytesWritten: 0,
20720
+ ...books
20567
20721
  };
20568
20722
  }
20569
20723
  if (planned.length === 0 && indexLines.length === 0) {
20570
- return { enabled: true, dryRun: false, written: [], refusals, indexLines: [], bytesWritten: 0 };
20724
+ return {
20725
+ enabled: true,
20726
+ dryRun: false,
20727
+ written: [],
20728
+ refusals,
20729
+ indexLines: [],
20730
+ bytesWritten: 0,
20731
+ ...books
20732
+ };
20571
20733
  }
20572
20734
  if (!hasFreshBackup(memoryDir, home)) {
20573
20735
  try {
@@ -20577,7 +20739,7 @@ function renderLocked(db, candidates, o) {
20577
20739
  reason: "backup_failed",
20578
20740
  error: err instanceof BackupError ? err.message : String(err)
20579
20741
  });
20580
- return EMPTY("backup_failed");
20742
+ return EMPTY("backup_failed", candidates.length);
20581
20743
  }
20582
20744
  }
20583
20745
  const indexPath = o.indexPath ?? join19(memoryDir, "MEMORY.md");
@@ -20645,7 +20807,10 @@ function renderLocked(db, candidates, o) {
20645
20807
  errors: restored.errors
20646
20808
  });
20647
20809
  return {
20648
- ...EMPTY("rollback_incomplete"),
20810
+ ...EMPTY("rollback_incomplete", candidates.length),
20811
+ // The books were balanced before the write began; keep them rather than zeroing.
20812
+ unchanged,
20813
+ capped,
20649
20814
  enabled: true,
20650
20815
  refusals: [
20651
20816
  ...refusals,
@@ -20654,9 +20819,9 @@ function renderLocked(db, candidates, o) {
20654
20819
  };
20655
20820
  }
20656
20821
  audit("memory_file_render_refused", { reason: "write_failed", error: String(err) });
20657
- return { ...EMPTY("write_failed"), enabled: true, refusals };
20822
+ return { ...EMPTY("write_failed", candidates.length), enabled: true, refusals, unchanged, capped };
20658
20823
  }
20659
- return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten };
20824
+ return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten, ...books };
20660
20825
  }
20661
20826
  function sha2562(s) {
20662
20827
  return createHash8("sha256").update(s, "utf8").digest("hex");
@@ -20725,7 +20890,7 @@ function buildIndexLines(db, memoryDir, planned, config) {
20725
20890
  function oneLine(s) {
20726
20891
  return s.replace(/[\r\n]+/g, " ").slice(0, 120);
20727
20892
  }
20728
- var EMPTY;
20893
+ var RenderAccountingLeak, EMPTY;
20729
20894
  var init_memory_renderer = __esm({
20730
20895
  "src/memory-renderer.ts"() {
20731
20896
  "use strict";
@@ -20740,14 +20905,22 @@ var init_memory_renderer = __esm({
20740
20905
  init_memory_index_region();
20741
20906
  init_memory_files_config();
20742
20907
  init_memory_file_ingest();
20743
- EMPTY = (reason) => ({
20908
+ RenderAccountingLeak = class extends Error {
20909
+ };
20910
+ EMPTY = (reason, considered = 0) => ({
20744
20911
  enabled: false,
20745
20912
  dryRun: false,
20746
20913
  written: [],
20747
20914
  refusals: [],
20748
20915
  indexLines: [],
20749
20916
  bytesWritten: 0,
20750
- skippedReason: reason
20917
+ skippedReason: reason,
20918
+ // An early gate refused, so every candidate was dropped — by that ONE named reason.
20919
+ // Reporting `considered` here is what distinguishes "nothing to do" from "walked away
20920
+ // from 59 rows because the lock was busy".
20921
+ considered,
20922
+ unchanged: 0,
20923
+ capped: 0
20751
20924
  });
20752
20925
  }
20753
20926
  });
@@ -20938,25 +21111,46 @@ var init_memory_render_cli = __esm({
20938
21111
  function loadRenderCandidates(db) {
20939
21112
  const cfg = resolveMemoryFilesConfig();
20940
21113
  const typePlaceholders = RENDERABLE_MEMORY_TYPES.map(() => "?").join(", ");
21114
+ const WHERE = `WHERE importance >= ?
21115
+ AND COALESCE(expired_at_epoch, 0) = 0
21116
+ AND type IN (${typePlaceholders})`;
21117
+ const { population } = db.prepare(`SELECT COUNT(*) AS population FROM observations ${WHERE}`).get(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
20941
21118
  const rows = db.prepare(
20942
21119
  `SELECT id, title, detail, importance, COALESCE(origin, 'local') AS origin
20943
21120
  FROM observations
20944
- WHERE importance >= ?
20945
- AND COALESCE(expired_at_epoch, 0) = 0
20946
- AND type IN (${typePlaceholders})
21121
+ ${WHERE}
20947
21122
  ORDER BY importance DESC, created_at_epoch DESC
20948
- LIMIT 50`
21123
+ LIMIT ${CANDIDATE_WINDOW}`
20949
21124
  ).all(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
20950
- return rows.filter((r2) => !r2.title.startsWith("[memory-file]")).map((r2) => ({
20951
- observationId: r2.id,
20952
- name: r2.title,
20953
- title: r2.title,
20954
- body: r2.detail ?? "",
20955
- importance: r2.importance,
20956
- origin: r2.origin
20957
- }));
21125
+ const reingest = rows.filter((r2) => r2.title.startsWith("[memory-file]"));
21126
+ const kept = rows.filter((r2) => !r2.title.startsWith("[memory-file]"));
21127
+ const excluded = reingest.length ? [{ reason: EXCLUSION_MEMORY_FILE_REINGEST, count: reingest.length }] : [];
21128
+ const ledger = {
21129
+ population,
21130
+ windowed: rows.length,
21131
+ truncatedByWindow: Math.max(0, population - rows.length),
21132
+ excluded,
21133
+ returned: kept.length
21134
+ };
21135
+ const accounted = ledger.returned + ledger.truncatedByWindow + excluded.reduce((n, e2) => n + e2.count, 0);
21136
+ if (accounted !== population) {
21137
+ throw new CandidateAccountingLeak(
21138
+ `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.`
21139
+ );
21140
+ }
21141
+ return {
21142
+ ledger,
21143
+ candidates: kept.map((r2) => ({
21144
+ observationId: r2.id,
21145
+ name: r2.title,
21146
+ title: r2.title,
21147
+ body: r2.detail ?? "",
21148
+ importance: r2.importance,
21149
+ origin: r2.origin
21150
+ }))
21151
+ };
20958
21152
  }
20959
- var RENDERABLE_MEMORY_TYPES;
21153
+ var RENDERABLE_MEMORY_TYPES, CANDIDATE_WINDOW, EXCLUSION_MEMORY_FILE_REINGEST, CandidateAccountingLeak;
20960
21154
  var init_memory_render_candidates = __esm({
20961
21155
  "src/memory-render-candidates.ts"() {
20962
21156
  "use strict";
@@ -20967,6 +21161,10 @@ var init_memory_render_candidates = __esm({
20967
21161
  "incident_near_miss",
20968
21162
  "cr_violation"
20969
21163
  ];
21164
+ CANDIDATE_WINDOW = 50;
21165
+ EXCLUSION_MEMORY_FILE_REINGEST = "memory_file_reingest";
21166
+ CandidateAccountingLeak = class extends Error {
21167
+ };
20970
21168
  }
20971
21169
  });
20972
21170
 
@@ -21046,13 +21244,13 @@ function pruneNoiseObservations(db, opts = {}) {
21046
21244
  const counts = countNoise(db);
21047
21245
  if (dryRun || counts.total === 0) return { counts, expired: 0, dryRun };
21048
21246
  const nowSec = opts.now ?? Math.floor(Date.now() / 1e3);
21049
- const nowIso = new Date(nowSec * 1e3).toISOString();
21247
+ const nowIso2 = new Date(nowSec * 1e3).toISOString();
21050
21248
  const res = db.prepare(
21051
21249
  `UPDATE observations
21052
21250
  SET expired_at = ?, expired_at_epoch = ?, valid_to = ?, valid_to_epoch = ?
21053
21251
  WHERE COALESCE(expired_at_epoch, 0) = 0
21054
21252
  AND ((${DA_PREDICATE}) OR (${FT_DECISION_PREDICATE}) OR (${DB_PREDICATE}))`
21055
- ).run(nowIso, nowSec, nowIso, nowSec);
21253
+ ).run(nowIso2, nowSec, nowIso2, nowSec);
21056
21254
  return { counts, expired: res.changes, dryRun: false };
21057
21255
  }
21058
21256
  var DA_PREDICATE, FT_DECISION_PREDICATE, DB_PREDICATE;
@@ -21097,7 +21295,7 @@ async function runMemoryRenderCli(sub, args2) {
21097
21295
  };
21098
21296
  }
21099
21297
  const cfg = resolveMemoryFilesConfig();
21100
- const candidates = loadRenderCandidates(db);
21298
+ const { candidates, ledger } = loadRenderCandidates(db);
21101
21299
  const result = renderMemoryFiles(db, candidates, {
21102
21300
  memoryDir,
21103
21301
  dryRun: true,
@@ -21112,6 +21310,19 @@ async function runMemoryRenderCli(sub, args2) {
21112
21310
  }
21113
21311
  lines.push(`Massu would write ${result.written.length} file(s):`);
21114
21312
  for (const f2 of result.written) lines.push(` ${f2}`);
21313
+ lines.push("", "Where every row went:");
21314
+ lines.push(` candidate population (type+importance+expiry) : ${ledger.population}`);
21315
+ if (ledger.truncatedByWindow > 0) {
21316
+ lines.push(` truncated by the row window : ${ledger.truncatedByWindow}`);
21317
+ }
21318
+ for (const e2 of ledger.excluded) {
21319
+ lines.push(` excluded: ${e2.reason.padEnd(30)} : ${e2.count}`);
21320
+ }
21321
+ lines.push(` -> reached the renderer : ${ledger.returned}`);
21322
+ lines.push(
21323
+ ` renderer: considered ${result.considered} = written ${result.written.length} + refused ${result.refusals.length} + unchanged ${result.unchanged} + capped ${result.capped}`
21324
+ );
21325
+ lines.push(` skipped: ${result.skippedReason ?? "(none)"}`);
21115
21326
  if (result.indexLines.length > 0) {
21116
21327
  lines.push("", "MEMORY.md \u2014 inside the massu:learned region only:");
21117
21328
  for (const l2 of result.indexLines) lines.push(` ${l2}`);
@@ -23270,7 +23481,7 @@ async function pullInstalledPackRules(db, opts = {}) {
23270
23481
  }
23271
23482
  const packs = parsePacks(envelope.packs_json);
23272
23483
  const result = { ...ZERO };
23273
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
23484
+ const nowIso2 = (/* @__PURE__ */ new Date()).toISOString();
23274
23485
  for (const pack of packs) {
23275
23486
  if (!isValidInstalledPack(pack)) continue;
23276
23487
  for (const rawRule of pack.rules) {
@@ -23287,7 +23498,7 @@ async function pullInstalledPackRules(db, opts = {}) {
23287
23498
  result.skipped += 1;
23288
23499
  continue;
23289
23500
  }
23290
- materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, signedOrgId, nowIso, opts.home ?? homedir20());
23501
+ materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, signedOrgId, nowIso2, opts.home ?? homedir20());
23291
23502
  result.materialized += 1;
23292
23503
  }
23293
23504
  }
@@ -23334,7 +23545,7 @@ function alreadyApplied(db, promptHash) {
23334
23545
  return false;
23335
23546
  }
23336
23547
  }
23337
- function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, orgId, nowIso, home) {
23548
+ function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack, orgId, nowIso2, home) {
23338
23549
  const promptText = `${rule.title}: ${rule.description}`.replace(/\n+/g, " ").slice(0, 200) || `pack rule ${promptHash}`;
23339
23550
  const draftText = rule.pattern ?? rule.check ?? rule.description;
23340
23551
  const hardened = isExecutableDestination(rule.destination);
@@ -23350,7 +23561,7 @@ function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack
23350
23561
  score: 0,
23351
23562
  signals: [],
23352
23563
  prior_turn_files: [],
23353
- timestamp: nowIso,
23564
+ timestamp: nowIso2,
23354
23565
  session_id: `pack:${pack.slug}`,
23355
23566
  // Provenance (P2-002/P2-003): the applier's team-origin / hardened apply-gate
23356
23567
  // keys on this. `origin:'pack'` rides the SAME gate as a team origin
@@ -23362,7 +23573,7 @@ function materializeCandidate(projectRoot, candidatePath, promptHash, rule, pack
23362
23573
  origin: "pack",
23363
23574
  org_id: orgId,
23364
23575
  promoted_by: `pack:${pack.slug}`,
23365
- promoted_at: nowIso,
23576
+ promoted_at: nowIso2,
23366
23577
  signature_verified: true,
23367
23578
  ...applyMac ? { apply_mac: applyMac } : {},
23368
23579
  pack_slug: pack.slug,
@@ -34562,7 +34773,7 @@ function indexAllKnowledge(db) {
34562
34773
  files.push(...docsFiles);
34563
34774
  }
34564
34775
  const now = /* @__PURE__ */ new Date();
34565
- const nowIso = now.toISOString();
34776
+ const nowIso2 = now.toISOString();
34566
34777
  const nowEpoch = now.getTime();
34567
34778
  const transaction = db.transaction(() => {
34568
34779
  db.exec("DROP TRIGGER IF EXISTS kc_fts_delete");
@@ -34606,7 +34817,7 @@ function indexAllKnowledge(db) {
34606
34817
  const category = categorizeFile(filePath);
34607
34818
  const title = extractTitle(content, filePath);
34608
34819
  const description = extractDescription2(content);
34609
- const result = insertDoc.run(relPath, category, title, description, hash, nowIso, nowEpoch);
34820
+ const result = insertDoc.run(relPath, category, title, description, hash, nowIso2, nowEpoch);
34610
34821
  const docId = result.lastInsertRowid;
34611
34822
  stats.filesIndexed++;
34612
34823
  const sections = parseSections(content, filePath);