@massu/core 2.5.0 → 2.5.2

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.
@@ -1167,6 +1167,13 @@ var init_config = __esm({
1167
1167
  });
1168
1168
  LanguageFrameworkEntrySchema = z2.object({
1169
1169
  framework: z2.string().optional(),
1170
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1171
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1172
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1173
+ // derives the index builders' candidate set from it, so it is typed here rather than
1174
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1175
+ // from silently contributing `undefined` to a path predicate.
1176
+ source_dirs: z2.array(z2.string()).optional(),
1170
1177
  test_framework: z2.string().optional(),
1171
1178
  test: z2.string().optional(),
1172
1179
  runtime: z2.string().optional(),
@@ -1339,6 +1346,16 @@ var init_memory_vector = __esm({
1339
1346
  }
1340
1347
  });
1341
1348
 
1349
+ // src/lib/timestamps.ts
1350
+ function nowIso() {
1351
+ return (/* @__PURE__ */ new Date()).toISOString();
1352
+ }
1353
+ var init_timestamps = __esm({
1354
+ "src/lib/timestamps.ts"() {
1355
+ "use strict";
1356
+ }
1357
+ });
1358
+
1342
1359
  // src/db-backup.ts
1343
1360
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1344
1361
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3599,8 +3616,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3599
3616
  }
3600
3617
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3601
3618
  const result = db.prepare(`
3602
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3603
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3619
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3620
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3604
3621
  `).run(
3605
3622
  sessionId,
3606
3623
  turnNumber,
@@ -3609,14 +3626,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3609
3626
  toolCallsJson,
3610
3627
  toolCallCount,
3611
3628
  promptTokens,
3612
- responseTokens
3629
+ responseTokens,
3630
+ nowIso()
3613
3631
  );
3614
3632
  return Number(result.lastInsertRowid);
3615
3633
  }
3616
3634
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3617
3635
  db.prepare(`
3618
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3619
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3636
+ 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)
3637
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3620
3638
  `).run(
3621
3639
  sessionId,
3622
3640
  turnNumber,
@@ -3625,7 +3643,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3625
3643
  inputSize,
3626
3644
  outputSize,
3627
3645
  success ? 1 : 0,
3628
- filesInvolved ? JSON.stringify(filesInvolved) : null
3646
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3647
+ nowIso()
3629
3648
  );
3630
3649
  }
3631
3650
  function getLastProcessedLine(db, sessionId) {
@@ -3884,6 +3903,7 @@ var init_memory_db = __esm({
3884
3903
  init_db_driver();
3885
3904
  init_config();
3886
3905
  init_memory_vector();
3906
+ init_timestamps();
3887
3907
  init_db_backup();
3888
3908
  init_rule_delivery();
3889
3909
  init_rule_candidate_store();
@@ -3973,12 +3993,57 @@ var init_hook_failure_signal = __esm({
3973
3993
  });
3974
3994
 
3975
3995
  // src/hooks/lib/write-hook-message.ts
3976
- function writeHookMessage(message) {
3977
- process.stdout.write(JSON.stringify({ message }) + "\n");
3996
+ var HOOK_EVENTS = [
3997
+ "UserPromptSubmit",
3998
+ "UserPromptExpansion",
3999
+ "SessionStart",
4000
+ "PreToolUse",
4001
+ "PostToolUse",
4002
+ "Stop",
4003
+ "SubagentStop",
4004
+ "PreCompact"
4005
+ ];
4006
+ var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4007
+ function isHookEvent(value) {
4008
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
4009
+ }
4010
+ function writeHookContext(hookEventName, message) {
4011
+ if (!isHookEvent(hookEventName)) {
4012
+ process.stderr.write(
4013
+ `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4014
+ `
4015
+ );
4016
+ return;
4017
+ }
4018
+ if (typeof message !== "string" || message.length === 0) return;
4019
+ let additionalContext = message;
4020
+ if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4021
+ const marker = `
4022
+ [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4023
+ additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4024
+ }
4025
+ process.stdout.write(
4026
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4027
+ );
3978
4028
  }
3979
4029
 
3980
4030
  // src/hooks/security-gate.ts
3981
4031
  init_hook_failure_signal();
4032
+
4033
+ // src/hooks/lib/is-direct-invocation.ts
4034
+ import { pathToFileURL } from "url";
4035
+ function isDirectInvocation(moduleUrl) {
4036
+ const entry = process.argv[1];
4037
+ if (!entry) return false;
4038
+ try {
4039
+ return moduleUrl === pathToFileURL(entry).href;
4040
+ } catch {
4041
+ return false;
4042
+ }
4043
+ }
4044
+
4045
+ // src/hooks/security-gate.ts
4046
+ var HOOK_EVENT = "PreToolUse";
3982
4047
  var DANGEROUS_BASH_PATTERNS = [
3983
4048
  // BLOCK — destructive and irreversible, or arbitrary remote code execution.
3984
4049
  { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
@@ -4098,7 +4163,7 @@ async function main() {
4098
4163
  const input = await readStdin();
4099
4164
  const hookInput = JSON.parse(input);
4100
4165
  const messages = runSecurityGateChecks(hookInput);
4101
- for (const msg of messages) writeHookMessage(msg);
4166
+ for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
4102
4167
  } catch (err) {
4103
4168
  recordHookFailure("security-gate", err);
4104
4169
  process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
@@ -4117,7 +4182,7 @@ function readStdin() {
4117
4182
  setTimeout(() => resolve5(data), 400);
4118
4183
  });
4119
4184
  }
4120
- if (process.argv[1]?.endsWith("security-gate.js") || process.argv[1]?.endsWith("security-gate")) {
4185
+ if (isDirectInvocation(import.meta.url)) {
4121
4186
  main();
4122
4187
  }
4123
4188
  export {
@@ -1167,6 +1167,13 @@ var init_config = __esm({
1167
1167
  });
1168
1168
  LanguageFrameworkEntrySchema = z2.object({
1169
1169
  framework: z2.string().optional(),
1170
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1171
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1172
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1173
+ // derives the index builders' candidate set from it, so it is typed here rather than
1174
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1175
+ // from silently contributing `undefined` to a path predicate.
1176
+ source_dirs: z2.array(z2.string()).optional(),
1170
1177
  test_framework: z2.string().optional(),
1171
1178
  test: z2.string().optional(),
1172
1179
  runtime: z2.string().optional(),
@@ -1360,6 +1367,16 @@ var init_memory_vector = __esm({
1360
1367
  }
1361
1368
  });
1362
1369
 
1370
+ // src/lib/timestamps.ts
1371
+ function nowIso() {
1372
+ return (/* @__PURE__ */ new Date()).toISOString();
1373
+ }
1374
+ var init_timestamps = __esm({
1375
+ "src/lib/timestamps.ts"() {
1376
+ "use strict";
1377
+ }
1378
+ });
1379
+
1363
1380
  // src/db-backup.ts
1364
1381
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1365
1382
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3853,8 +3870,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3853
3870
  }
3854
3871
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3855
3872
  const result = db.prepare(`
3856
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3857
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3873
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3874
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3858
3875
  `).run(
3859
3876
  sessionId,
3860
3877
  turnNumber,
@@ -3863,14 +3880,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3863
3880
  toolCallsJson,
3864
3881
  toolCallCount,
3865
3882
  promptTokens,
3866
- responseTokens
3883
+ responseTokens,
3884
+ nowIso()
3867
3885
  );
3868
3886
  return Number(result.lastInsertRowid);
3869
3887
  }
3870
3888
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3871
3889
  db.prepare(`
3872
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3873
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3890
+ 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)
3891
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3874
3892
  `).run(
3875
3893
  sessionId,
3876
3894
  turnNumber,
@@ -3879,7 +3897,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3879
3897
  inputSize,
3880
3898
  outputSize,
3881
3899
  success ? 1 : 0,
3882
- filesInvolved ? JSON.stringify(filesInvolved) : null
3900
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3901
+ nowIso()
3883
3902
  );
3884
3903
  }
3885
3904
  function getLastProcessedLine(db, sessionId) {
@@ -4138,6 +4157,7 @@ var init_memory_db = __esm({
4138
4157
  init_db_driver();
4139
4158
  init_config();
4140
4159
  init_memory_vector();
4160
+ init_timestamps();
4141
4161
  init_db_backup();
4142
4162
  init_rule_delivery();
4143
4163
  init_rule_candidate_store();
@@ -7348,6 +7368,20 @@ async function runConsolidation(db, opts = {}) {
7348
7368
 
7349
7369
  // src/hooks/session-end.ts
7350
7370
  init_hook_failure_signal();
7371
+
7372
+ // src/hooks/lib/is-direct-invocation.ts
7373
+ import { pathToFileURL } from "url";
7374
+ function isDirectInvocation(moduleUrl) {
7375
+ const entry = process.argv[1];
7376
+ if (!entry) return false;
7377
+ try {
7378
+ return moduleUrl === pathToFileURL(entry).href;
7379
+ } catch {
7380
+ return false;
7381
+ }
7382
+ }
7383
+
7384
+ // src/hooks/session-end.ts
7351
7385
  async function main() {
7352
7386
  try {
7353
7387
  const input = await readStdin();
@@ -7707,4 +7741,6 @@ function readStdin() {
7707
7741
  setTimeout(() => resolve8(data), 5e3);
7708
7742
  });
7709
7743
  }
7710
- main();
7744
+ if (isDirectInvocation(import.meta.url)) {
7745
+ main();
7746
+ }
@@ -1190,6 +1190,13 @@ var init_config = __esm({
1190
1190
  });
1191
1191
  LanguageFrameworkEntrySchema = z2.object({
1192
1192
  framework: z2.string().optional(),
1193
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1194
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1195
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1196
+ // derives the index builders' candidate set from it, so it is typed here rather than
1197
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1198
+ // from silently contributing `undefined` to a path predicate.
1199
+ source_dirs: z2.array(z2.string()).optional(),
1193
1200
  test_framework: z2.string().optional(),
1194
1201
  test: z2.string().optional(),
1195
1202
  runtime: z2.string().optional(),
@@ -1362,6 +1369,16 @@ var init_memory_vector = __esm({
1362
1369
  }
1363
1370
  });
1364
1371
 
1372
+ // src/lib/timestamps.ts
1373
+ function nowIso() {
1374
+ return (/* @__PURE__ */ new Date()).toISOString();
1375
+ }
1376
+ var init_timestamps = __esm({
1377
+ "src/lib/timestamps.ts"() {
1378
+ "use strict";
1379
+ }
1380
+ });
1381
+
1365
1382
  // src/db-backup.ts
1366
1383
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1367
1384
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3693,8 +3710,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3693
3710
  }
3694
3711
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3695
3712
  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 (?, ?, ?, ?, ?, ?, ?, ?)
3713
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3714
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3698
3715
  `).run(
3699
3716
  sessionId,
3700
3717
  turnNumber,
@@ -3703,14 +3720,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3703
3720
  toolCallsJson,
3704
3721
  toolCallCount,
3705
3722
  promptTokens,
3706
- responseTokens
3723
+ responseTokens,
3724
+ nowIso()
3707
3725
  );
3708
3726
  return Number(result.lastInsertRowid);
3709
3727
  }
3710
3728
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3711
3729
  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 (?, ?, ?, ?, ?, ?, ?, ?)
3730
+ 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)
3731
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3714
3732
  `).run(
3715
3733
  sessionId,
3716
3734
  turnNumber,
@@ -3719,7 +3737,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3719
3737
  inputSize,
3720
3738
  outputSize,
3721
3739
  success ? 1 : 0,
3722
- filesInvolved ? JSON.stringify(filesInvolved) : null
3740
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3741
+ nowIso()
3723
3742
  );
3724
3743
  }
3725
3744
  function getLastProcessedLine(db, sessionId) {
@@ -3978,6 +3997,7 @@ var init_memory_db = __esm({
3978
3997
  init_db_driver();
3979
3998
  init_config();
3980
3999
  init_memory_vector();
4000
+ init_timestamps();
3981
4001
  init_db_backup();
3982
4002
  init_rule_delivery();
3983
4003
  init_rule_candidate_store();
@@ -4985,6 +5005,7 @@ var init_memory_files_config = __esm({
4985
5005
  // src/memory-renderer.ts
4986
5006
  var memory_renderer_exports = {};
4987
5007
  __export(memory_renderer_exports, {
5008
+ RenderAccountingLeak: () => RenderAccountingLeak,
4988
5009
  composeFile: () => composeFile,
4989
5010
  renderMemoryFiles: () => renderMemoryFiles,
4990
5011
  stripFrontmatter: () => stripFrontmatter
@@ -4996,7 +5017,7 @@ import { createHash as createHash5 } from "crypto";
4996
5017
  function renderMemoryFiles(db, candidates, opts) {
4997
5018
  const config = opts.config ?? resolveMemoryFilesConfig();
4998
5019
  if (!config.renderEnabled) {
4999
- return EMPTY("render_disabled");
5020
+ return EMPTY("render_disabled", candidates.length);
5000
5021
  }
5001
5022
  const dryRun = opts.dryRun === true;
5002
5023
  const now = opts.now ?? Date.now();
@@ -5005,7 +5026,7 @@ function renderMemoryFiles(db, candidates, opts) {
5005
5026
  });
5006
5027
  const { memoryDir } = opts;
5007
5028
  if (!existsSync19(memoryDir)) {
5008
- return EMPTY("no_memory_dir");
5029
+ return EMPTY("no_memory_dir", candidates.length);
5009
5030
  }
5010
5031
  try {
5011
5032
  return withMemoryIndexLock(
@@ -5014,10 +5035,10 @@ function renderMemoryFiles(db, candidates, opts) {
5014
5035
  );
5015
5036
  } catch (err) {
5016
5037
  if (err instanceof MemoryIndexLockBusy) {
5017
- return EMPTY("lock_busy");
5038
+ return EMPTY("lock_busy", candidates.length);
5018
5039
  }
5019
5040
  audit("memory_file_render_refused", { reason: "unexpected_error", error: String(err) });
5020
- return EMPTY("error");
5041
+ return EMPTY("error", candidates.length);
5021
5042
  }
5022
5043
  }
5023
5044
  function renderLocked(db, candidates, o) {
@@ -5027,11 +5048,14 @@ function renderLocked(db, candidates, o) {
5027
5048
  try {
5028
5049
  tombstones = readTombstones(memoryDir);
5029
5050
  } catch {
5030
- return EMPTY("tombstone_ledger_unreadable");
5051
+ return EMPTY("tombstone_ledger_unreadable", candidates.length);
5031
5052
  }
5032
5053
  const ownerOf = relPathOwnerLookup(db);
5033
5054
  const planned = [];
5034
- for (const c of candidates) {
5055
+ let unchanged = 0;
5056
+ let capped = 0;
5057
+ for (let i = 0; i < candidates.length; i++) {
5058
+ const c = candidates[i];
5035
5059
  if (!isLocalOrigin(c.origin)) {
5036
5060
  refusals.push({ name: c.name, reason: "non_local_origin", detail: c.origin || "unknown" });
5037
5061
  audit("memory_file_render_refused", { name: c.name, reason: "non_local_origin" });
@@ -5087,7 +5111,10 @@ function renderLocked(db, candidates, o) {
5087
5111
  continue;
5088
5112
  }
5089
5113
  const next = composeFile(c, home);
5090
- if (next !== null && next === existing) continue;
5114
+ if (next !== null && next === existing) {
5115
+ unchanged++;
5116
+ continue;
5117
+ }
5091
5118
  }
5092
5119
  const content = composeFile(c, home);
5093
5120
  if (content === null) {
@@ -5095,9 +5122,24 @@ function renderLocked(db, candidates, o) {
5095
5122
  continue;
5096
5123
  }
5097
5124
  planned.push({ c, relPath, absPath, content });
5098
- if (planned.length >= config.renderMaxFilesPerSession) break;
5125
+ if (planned.length >= config.renderMaxFilesPerSession) {
5126
+ capped = candidates.length - (i + 1);
5127
+ break;
5128
+ }
5129
+ }
5130
+ const accounted = planned.length + refusals.length + unchanged + capped;
5131
+ if (accounted !== candidates.length) {
5132
+ audit("memory_file_render_refused", {
5133
+ reason: "accounting_leak",
5134
+ considered: candidates.length,
5135
+ accounted
5136
+ });
5137
+ throw new RenderAccountingLeak(
5138
+ `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.`
5139
+ );
5099
5140
  }
5100
5141
  const indexLines = buildIndexLines(db, memoryDir, planned, config);
5142
+ const books = { considered: candidates.length, unchanged, capped };
5101
5143
  if (dryRun) {
5102
5144
  return {
5103
5145
  enabled: true,
@@ -5105,11 +5147,20 @@ function renderLocked(db, candidates, o) {
5105
5147
  written: planned.map((p) => p.relPath),
5106
5148
  refusals,
5107
5149
  indexLines,
5108
- bytesWritten: 0
5150
+ bytesWritten: 0,
5151
+ ...books
5109
5152
  };
5110
5153
  }
5111
5154
  if (planned.length === 0 && indexLines.length === 0) {
5112
- return { enabled: true, dryRun: false, written: [], refusals, indexLines: [], bytesWritten: 0 };
5155
+ return {
5156
+ enabled: true,
5157
+ dryRun: false,
5158
+ written: [],
5159
+ refusals,
5160
+ indexLines: [],
5161
+ bytesWritten: 0,
5162
+ ...books
5163
+ };
5113
5164
  }
5114
5165
  if (!hasFreshBackup(memoryDir, home)) {
5115
5166
  try {
@@ -5119,7 +5170,7 @@ function renderLocked(db, candidates, o) {
5119
5170
  reason: "backup_failed",
5120
5171
  error: err instanceof BackupError ? err.message : String(err)
5121
5172
  });
5122
- return EMPTY("backup_failed");
5173
+ return EMPTY("backup_failed", candidates.length);
5123
5174
  }
5124
5175
  }
5125
5176
  const indexPath = o.indexPath ?? join13(memoryDir, "MEMORY.md");
@@ -5187,7 +5238,10 @@ function renderLocked(db, candidates, o) {
5187
5238
  errors: restored.errors
5188
5239
  });
5189
5240
  return {
5190
- ...EMPTY("rollback_incomplete"),
5241
+ ...EMPTY("rollback_incomplete", candidates.length),
5242
+ // The books were balanced before the write began; keep them rather than zeroing.
5243
+ unchanged,
5244
+ capped,
5191
5245
  enabled: true,
5192
5246
  refusals: [
5193
5247
  ...refusals,
@@ -5196,9 +5250,9 @@ function renderLocked(db, candidates, o) {
5196
5250
  };
5197
5251
  }
5198
5252
  audit("memory_file_render_refused", { reason: "write_failed", error: String(err) });
5199
- return { ...EMPTY("write_failed"), enabled: true, refusals };
5253
+ return { ...EMPTY("write_failed", candidates.length), enabled: true, refusals, unchanged, capped };
5200
5254
  }
5201
- return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten };
5255
+ return { enabled: true, dryRun: false, written, refusals, indexLines, bytesWritten, ...books };
5202
5256
  }
5203
5257
  function sha256(s) {
5204
5258
  return createHash5("sha256").update(s, "utf8").digest("hex");
@@ -5267,7 +5321,7 @@ function buildIndexLines(db, memoryDir, planned, config) {
5267
5321
  function oneLine(s) {
5268
5322
  return s.replace(/[\r\n]+/g, " ").slice(0, 120);
5269
5323
  }
5270
- var EMPTY;
5324
+ var RenderAccountingLeak, EMPTY;
5271
5325
  var init_memory_renderer = __esm({
5272
5326
  "src/memory-renderer.ts"() {
5273
5327
  "use strict";
@@ -5282,14 +5336,22 @@ var init_memory_renderer = __esm({
5282
5336
  init_memory_index_region();
5283
5337
  init_memory_files_config();
5284
5338
  init_memory_file_ingest();
5285
- EMPTY = (reason) => ({
5339
+ RenderAccountingLeak = class extends Error {
5340
+ };
5341
+ EMPTY = (reason, considered = 0) => ({
5286
5342
  enabled: false,
5287
5343
  dryRun: false,
5288
5344
  written: [],
5289
5345
  refusals: [],
5290
5346
  indexLines: [],
5291
5347
  bytesWritten: 0,
5292
- skippedReason: reason
5348
+ skippedReason: reason,
5349
+ // An early gate refused, so every candidate was dropped — by that ONE named reason.
5350
+ // Reporting `considered` here is what distinguishes "nothing to do" from "walked away
5351
+ // from 59 rows because the lock was busy".
5352
+ considered,
5353
+ unchanged: 0,
5354
+ capped: 0
5293
5355
  });
5294
5356
  }
5295
5357
  });
@@ -5297,31 +5359,54 @@ var init_memory_renderer = __esm({
5297
5359
  // src/memory-render-candidates.ts
5298
5360
  var memory_render_candidates_exports = {};
5299
5361
  __export(memory_render_candidates_exports, {
5362
+ CandidateAccountingLeak: () => CandidateAccountingLeak,
5363
+ EXCLUSION_MEMORY_FILE_REINGEST: () => EXCLUSION_MEMORY_FILE_REINGEST,
5300
5364
  RENDERABLE_MEMORY_TYPES: () => RENDERABLE_MEMORY_TYPES,
5301
5365
  loadRenderCandidates: () => loadRenderCandidates
5302
5366
  });
5303
5367
  function loadRenderCandidates(db) {
5304
5368
  const cfg = resolveMemoryFilesConfig();
5305
5369
  const typePlaceholders = RENDERABLE_MEMORY_TYPES.map(() => "?").join(", ");
5370
+ const WHERE = `WHERE importance >= ?
5371
+ AND COALESCE(expired_at_epoch, 0) = 0
5372
+ AND type IN (${typePlaceholders})`;
5373
+ const { population } = db.prepare(`SELECT COUNT(*) AS population FROM observations ${WHERE}`).get(cfg.renderMinImportance, ...RENDERABLE_MEMORY_TYPES);
5306
5374
  const rows = db.prepare(
5307
5375
  `SELECT id, title, detail, importance, COALESCE(origin, 'local') AS origin
5308
5376
  FROM observations
5309
- WHERE importance >= ?
5310
- AND COALESCE(expired_at_epoch, 0) = 0
5311
- AND type IN (${typePlaceholders})
5377
+ ${WHERE}
5312
5378
  ORDER BY importance DESC, created_at_epoch DESC
5313
- LIMIT 50`
5379
+ LIMIT ${CANDIDATE_WINDOW}`
5314
5380
  ).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
- }));
5381
+ const reingest = rows.filter((r) => r.title.startsWith("[memory-file]"));
5382
+ const kept = rows.filter((r) => !r.title.startsWith("[memory-file]"));
5383
+ const excluded = reingest.length ? [{ reason: EXCLUSION_MEMORY_FILE_REINGEST, count: reingest.length }] : [];
5384
+ const ledger = {
5385
+ population,
5386
+ windowed: rows.length,
5387
+ truncatedByWindow: Math.max(0, population - rows.length),
5388
+ excluded,
5389
+ returned: kept.length
5390
+ };
5391
+ const accounted = ledger.returned + ledger.truncatedByWindow + excluded.reduce((n, e) => n + e.count, 0);
5392
+ if (accounted !== population) {
5393
+ throw new CandidateAccountingLeak(
5394
+ `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.`
5395
+ );
5396
+ }
5397
+ return {
5398
+ ledger,
5399
+ candidates: kept.map((r) => ({
5400
+ observationId: r.id,
5401
+ name: r.title,
5402
+ title: r.title,
5403
+ body: r.detail ?? "",
5404
+ importance: r.importance,
5405
+ origin: r.origin
5406
+ }))
5407
+ };
5323
5408
  }
5324
- var RENDERABLE_MEMORY_TYPES;
5409
+ var RENDERABLE_MEMORY_TYPES, CANDIDATE_WINDOW, EXCLUSION_MEMORY_FILE_REINGEST, CandidateAccountingLeak;
5325
5410
  var init_memory_render_candidates = __esm({
5326
5411
  "src/memory-render-candidates.ts"() {
5327
5412
  "use strict";
@@ -5332,6 +5417,10 @@ var init_memory_render_candidates = __esm({
5332
5417
  "incident_near_miss",
5333
5418
  "cr_violation"
5334
5419
  ];
5420
+ CANDIDATE_WINDOW = 50;
5421
+ EXCLUSION_MEMORY_FILE_REINGEST = "memory_file_reingest";
5422
+ CandidateAccountingLeak = class extends Error {
5423
+ };
5335
5424
  }
5336
5425
  });
5337
5426
 
@@ -14106,6 +14195,20 @@ function checkWindows(pid) {
14106
14195
 
14107
14196
  // src/hooks/session-start.ts
14108
14197
  init_hook_failure_signal();
14198
+
14199
+ // src/hooks/lib/is-direct-invocation.ts
14200
+ import { pathToFileURL } from "url";
14201
+ function isDirectInvocation(moduleUrl) {
14202
+ const entry = process.argv[1];
14203
+ if (!entry) return false;
14204
+ try {
14205
+ return moduleUrl === pathToFileURL(entry).href;
14206
+ } catch {
14207
+ return false;
14208
+ }
14209
+ }
14210
+
14211
+ // src/hooks/session-start.ts
14109
14212
  async function main() {
14110
14213
  try {
14111
14214
  const input = await readStdin();
@@ -14149,7 +14252,8 @@ Session memory, code intelligence, and governance are now active.
14149
14252
  if (memoryDir) {
14150
14253
  const { renderMemoryFiles: renderMemoryFiles2 } = await Promise.resolve().then(() => (init_memory_renderer(), memory_renderer_exports));
14151
14254
  const { loadRenderCandidates: loadRenderCandidates2 } = await Promise.resolve().then(() => (init_memory_render_candidates(), memory_render_candidates_exports));
14152
- renderMemoryFiles2(db, loadRenderCandidates2(db), { memoryDir });
14255
+ const { candidates } = loadRenderCandidates2(db);
14256
+ renderMemoryFiles2(db, candidates, { memoryDir });
14153
14257
  }
14154
14258
  } catch (_renderErr) {
14155
14259
  }
@@ -14463,7 +14567,9 @@ function formatAge(ms) {
14463
14567
  const hr = Math.round(min / 60);
14464
14568
  return `${hr}h`;
14465
14569
  }
14466
- main();
14570
+ if (isDirectInvocation(import.meta.url)) {
14571
+ main();
14572
+ }
14467
14573
  /*! Bundled license information:
14468
14574
 
14469
14575
  is-extglob/index.js: