@massu/core 2.5.4 → 2.5.6

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.
@@ -951,7 +951,8 @@ var init_config = __esm({
951
951
  input_per_million: z2.number(),
952
952
  output_per_million: z2.number(),
953
953
  cache_read_per_million: z2.number().optional(),
954
- cache_write_per_million: z2.number().optional()
954
+ cache_write_per_million: z2.number().optional(),
955
+ cache_write_1h_per_million: z2.number().optional()
955
956
  });
956
957
  AnalyticsConfigSchema = z2.object({
957
958
  quality: z2.object({
@@ -1618,6 +1619,77 @@ var init_rule_candidate_store = __esm({
1618
1619
  }
1619
1620
  });
1620
1621
 
1622
+ // src/hooks/lib/hook-failure-signal.ts
1623
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1624
+ import { join as join4, dirname as dirname5 } from "path";
1625
+ function resolveFailureLogPath() {
1626
+ const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1627
+ if (explicit) return explicit;
1628
+ let dir = process.cwd();
1629
+ for (let i = 0; i < 12; i++) {
1630
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1631
+ return join4(dir, ".massu", "hook-failures.jsonl");
1632
+ }
1633
+ const parent = dirname5(dir);
1634
+ if (parent === dir) break;
1635
+ dir = parent;
1636
+ }
1637
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1638
+ }
1639
+ function recordHookFailure(hook, error, context) {
1640
+ const record = {
1641
+ hook,
1642
+ error: error instanceof Error ? error.message : String(error),
1643
+ stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
1644
+ context,
1645
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1646
+ };
1647
+ let wroteSomething = false;
1648
+ try {
1649
+ const path = resolveFailureLogPath();
1650
+ mkdirSync5(dirname5(path), { recursive: true });
1651
+ appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
1652
+ wroteSomething = true;
1653
+ } catch {
1654
+ }
1655
+ try {
1656
+ process.stderr.write(
1657
+ `[massu] HOOK FAILURE in ${hook}: ${record.error}
1658
+ [massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
1659
+ `
1660
+ );
1661
+ wroteSomething = true;
1662
+ } catch {
1663
+ }
1664
+ try {
1665
+ recordHookHealthRow(record);
1666
+ } catch {
1667
+ }
1668
+ return wroteSomething;
1669
+ }
1670
+ function recordHookHealthRow(record) {
1671
+ const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
1672
+ const db = getMemoryDb2();
1673
+ try {
1674
+ db.prepare(
1675
+ `INSERT INTO hook_health (hook, error, context_json, occurred_at)
1676
+ VALUES (?, ?, ?, ?)`
1677
+ ).run(
1678
+ record.hook,
1679
+ record.error,
1680
+ record.context ? JSON.stringify(record.context) : null,
1681
+ record.timestamp
1682
+ );
1683
+ } finally {
1684
+ db.close();
1685
+ }
1686
+ }
1687
+ var init_hook_failure_signal = __esm({
1688
+ "src/hooks/lib/hook-failure-signal.ts"() {
1689
+ "use strict";
1690
+ }
1691
+ });
1692
+
1621
1693
  // src/memory-embedder-tokenizer.ts
1622
1694
  import { readFileSync as readFileSync5 } from "fs";
1623
1695
  function loadVocab(vocabPath) {
@@ -1741,8 +1813,8 @@ var init_memory_embedder_tokenizer = __esm({
1741
1813
 
1742
1814
  // src/memory-embedder.ts
1743
1815
  import { fileURLToPath } from "url";
1744
- import { dirname as dirname5, join as join4 } from "path";
1745
- import { existsSync as existsSync6 } from "fs";
1816
+ import { dirname as dirname6, join as join5 } from "path";
1817
+ import { existsSync as existsSync7 } from "fs";
1746
1818
  import { createRequire as createRequire3 } from "module";
1747
1819
  function getActiveEmbedModel() {
1748
1820
  return _activeModel;
@@ -1764,21 +1836,21 @@ function embeddingsDisabled() {
1764
1836
  }
1765
1837
  function resolveModelDir() {
1766
1838
  try {
1767
- let dir = dirname5(fileURLToPath(import.meta.url));
1839
+ let dir = dirname6(fileURLToPath(import.meta.url));
1768
1840
  let root = null;
1769
1841
  for (let i = 0; i < 8; i++) {
1770
- if (existsSync6(join4(dir, "package.json"))) {
1842
+ if (existsSync7(join5(dir, "package.json"))) {
1771
1843
  root = dir;
1772
1844
  break;
1773
1845
  }
1774
- const parent = dirname5(dir);
1846
+ const parent = dirname6(dir);
1775
1847
  if (parent === dir) break;
1776
1848
  dir = parent;
1777
1849
  }
1778
1850
  if (!root) return null;
1779
1851
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1780
- const candidate = join4(root, rel);
1781
- if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1852
+ const candidate = join5(root, rel);
1853
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1782
1854
  }
1783
1855
  return null;
1784
1856
  } catch {
@@ -1788,8 +1860,8 @@ function resolveModelDir() {
1788
1860
  function resolveWasmDir() {
1789
1861
  try {
1790
1862
  const req3 = createRequire3(import.meta.url);
1791
- const main4 = req3.resolve(ORT_PKG);
1792
- return dirname5(main4);
1863
+ const main2 = req3.resolve(ORT_PKG);
1864
+ return dirname6(main2);
1793
1865
  } catch {
1794
1866
  return null;
1795
1867
  }
@@ -1809,8 +1881,8 @@ async function getSession() {
1809
1881
  ort.env.wasm.proxy = false;
1810
1882
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1811
1883
  _ortTensor = ort.Tensor;
1812
- const modelPath = join4(modelDir, "model_quantized.onnx");
1813
- const vocabPath = join4(modelDir, "vocab.txt");
1884
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1885
+ const vocabPath = join5(modelDir, "vocab.txt");
1814
1886
  _vocab = loadVocab(vocabPath);
1815
1887
  const session = await ort.InferenceSession.create(modelPath, {
1816
1888
  executionProviders: ["wasm"],
@@ -2137,6 +2209,7 @@ __export(memory_db_exports, {
2137
2209
  pruneToolCostEvents: () => pruneToolCostEvents,
2138
2210
  recordRecallHits: () => recordRecallHits,
2139
2211
  recordTelemetry: () => recordTelemetry,
2212
+ releaseMetaLease: () => releaseMetaLease,
2140
2213
  removePendingSync: () => removePendingSync,
2141
2214
  sanitizeFts5Query: () => sanitizeFts5Query,
2142
2215
  sanitizeFts5QueryOr: () => sanitizeFts5QueryOr,
@@ -2145,11 +2218,12 @@ __export(memory_db_exports, {
2145
2218
  searchObservations: () => searchObservations,
2146
2219
  setLastProcessedLine: () => setLastProcessedLine,
2147
2220
  setMemoryMeta: () => setMemoryMeta,
2221
+ tryAcquireMetaLease: () => tryAcquireMetaLease,
2148
2222
  upsertObservationEmbedding: () => upsertObservationEmbedding,
2149
2223
  usageWarmupElapsed: () => usageWarmupElapsed
2150
2224
  });
2151
- import { dirname as dirname6, basename as basename2 } from "path";
2152
- import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
2225
+ import { dirname as dirname7, basename as basename2 } from "path";
2226
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
2153
2227
  function sanitizeFts5Query(raw) {
2154
2228
  const trimmed = raw.trim();
2155
2229
  if (!trimmed) return '""';
@@ -2165,11 +2239,11 @@ function sanitizeFts5QueryOr(raw) {
2165
2239
  }
2166
2240
  function getMemoryDb() {
2167
2241
  const dbPath = getResolvedPaths().memoryDbPath;
2168
- const dir = dirname6(dbPath);
2169
- if (!existsSync7(dir)) {
2170
- mkdirSync5(dir, { recursive: true });
2242
+ const dir = dirname7(dbPath);
2243
+ if (!existsSync8(dir)) {
2244
+ mkdirSync6(dir, { recursive: true });
2171
2245
  }
2172
- const preExisting = existsSync7(dbPath);
2246
+ const preExisting = existsSync8(dbPath);
2173
2247
  const db = openDatabase2(dbPath);
2174
2248
  db.pragma("journal_mode = WAL");
2175
2249
  db.pragma("foreign_keys = ON");
@@ -2650,6 +2724,15 @@ function initMemorySchema(db) {
2650
2724
  );
2651
2725
  CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
2652
2726
  `);
2727
+ const costCols = db.prepare(`PRAGMA table_info(session_costs)`).all();
2728
+ const addCostCol = (name, ddl) => {
2729
+ if (!costCols.some((c) => c.name === name)) db.exec(`ALTER TABLE session_costs ADD COLUMN ${ddl}`);
2730
+ };
2731
+ addCostCol("cost_status", `cost_status TEXT NOT NULL DEFAULT 'unknown'`);
2732
+ addCostCol("unpriced_models", `unpriced_models TEXT NOT NULL DEFAULT ''`);
2733
+ addCostCol("priced_responses", `priced_responses INTEGER NOT NULL DEFAULT 0`);
2734
+ addCostCol("unpriced_responses", `unpriced_responses INTEGER NOT NULL DEFAULT 0`);
2735
+ addCostCol("pricing_version", `pricing_version INTEGER NOT NULL DEFAULT 0`);
2653
2736
  db.exec(`
2654
2737
  CREATE TABLE IF NOT EXISTS feature_costs (
2655
2738
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -3079,6 +3162,18 @@ function getMemoryMeta(db, key) {
3079
3162
  function setMemoryMeta(db, key, value) {
3080
3163
  db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
3081
3164
  }
3165
+ function tryAcquireMetaLease(db, key, ttlSec, nowSec = Math.floor(Date.now() / 1e3)) {
3166
+ const r = db.prepare(
3167
+ `INSERT INTO memory_meta (key, value) VALUES (?, ?)
3168
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
3169
+ WHERE memory_meta.value = ''
3170
+ OR CAST(substr(memory_meta.value, instr(memory_meta.value, ':') + 1) AS INTEGER) <= ?`
3171
+ ).run(key, `${process.pid}:${nowSec + ttlSec}`, nowSec);
3172
+ return Number(r.changes) === 1;
3173
+ }
3174
+ function releaseMetaLease(db, key) {
3175
+ db.prepare(`UPDATE memory_meta SET value = '' WHERE key = ? AND value LIKE ?`).run(key, `${process.pid}:%`);
3176
+ }
3082
3177
  function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
3083
3178
  db.prepare(
3084
3179
  `INSERT OR REPLACE INTO observation_embeddings (observation_id, model_id, dim, vec, created_at)
@@ -3947,7 +4042,7 @@ var init_memory_db = __esm({
3947
4042
  init_rule_candidate_store();
3948
4043
  init_hook_failure_signal();
3949
4044
  init_memory_embed_sweep();
3950
- MEMORY_SCHEMA_VERSION = 1;
4045
+ MEMORY_SCHEMA_VERSION = 2;
3951
4046
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
3952
4047
  MAX_DRAFT_TEXT_LEN = 16384;
3953
4048
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
@@ -3959,129 +4054,7 @@ var init_memory_db = __esm({
3959
4054
  }
3960
4055
  });
3961
4056
 
3962
- // src/hooks/lib/hook-failure-signal.ts
3963
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
3964
- import { join as join5, dirname as dirname7 } from "path";
3965
- function resolveFailureLogPath() {
3966
- const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
3967
- if (explicit) return explicit;
3968
- let dir = process.cwd();
3969
- for (let i = 0; i < 12; i++) {
3970
- if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
3971
- return join5(dir, ".massu", "hook-failures.jsonl");
3972
- }
3973
- const parent = dirname7(dir);
3974
- if (parent === dir) break;
3975
- dir = parent;
3976
- }
3977
- return join5(process.cwd(), ".massu", "hook-failures.jsonl");
3978
- }
3979
- function recordHookFailure(hook, error, context) {
3980
- const record = {
3981
- hook,
3982
- error: error instanceof Error ? error.message : String(error),
3983
- stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
3984
- context,
3985
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3986
- };
3987
- let wroteSomething = false;
3988
- try {
3989
- const path = resolveFailureLogPath();
3990
- mkdirSync6(dirname7(path), { recursive: true });
3991
- appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
3992
- wroteSomething = true;
3993
- } catch {
3994
- }
3995
- try {
3996
- process.stderr.write(
3997
- `[massu] HOOK FAILURE in ${hook}: ${record.error}
3998
- [massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
3999
- `
4000
- );
4001
- wroteSomething = true;
4002
- } catch {
4003
- }
4004
- try {
4005
- recordHookHealthRow(record);
4006
- } catch {
4007
- }
4008
- return wroteSomething;
4009
- }
4010
- function recordHookHealthRow(record) {
4011
- const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
4012
- const db = getMemoryDb2();
4013
- try {
4014
- db.prepare(
4015
- `INSERT INTO hook_health (hook, error, context_json, occurred_at)
4016
- VALUES (?, ?, ?, ?)`
4017
- ).run(
4018
- record.hook,
4019
- record.error,
4020
- record.context ? JSON.stringify(record.context) : null,
4021
- record.timestamp
4022
- );
4023
- } finally {
4024
- db.close();
4025
- }
4026
- }
4027
- var init_hook_failure_signal = __esm({
4028
- "src/hooks/lib/hook-failure-signal.ts"() {
4029
- "use strict";
4030
- }
4031
- });
4032
-
4033
- // src/hooks/lib/write-hook-message.ts
4034
- var HOOK_EVENTS = [
4035
- "UserPromptSubmit",
4036
- "UserPromptExpansion",
4037
- "SessionStart",
4038
- "PreToolUse",
4039
- "PostToolUse",
4040
- "Stop",
4041
- "SubagentStop",
4042
- "PreCompact"
4043
- ];
4044
- var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4045
- function isHookEvent(value) {
4046
- return typeof value === "string" && HOOK_EVENTS.includes(value);
4047
- }
4048
- function writeHookContext(hookEventName, message) {
4049
- if (!isHookEvent(hookEventName)) {
4050
- process.stderr.write(
4051
- `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4052
- `
4053
- );
4054
- return;
4055
- }
4056
- if (typeof message !== "string" || message.length === 0) return;
4057
- let additionalContext = message;
4058
- if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4059
- const marker = `
4060
- [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4061
- additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4062
- }
4063
- process.stdout.write(
4064
- JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4065
- );
4066
- }
4067
-
4068
- // src/hooks/security-gate.ts
4069
- init_hook_failure_signal();
4070
-
4071
- // src/hooks/lib/is-direct-invocation.ts
4072
- import { pathToFileURL } from "url";
4073
- function isDirectInvocation(moduleUrl) {
4074
- const entry = process.argv[1];
4075
- if (!entry) return false;
4076
- try {
4077
- return moduleUrl === pathToFileURL(entry).href;
4078
- } catch {
4079
- return false;
4080
- }
4081
- }
4082
-
4083
- // src/hooks/security-gate.ts
4084
- var HOOK_EVENT = "PreToolUse";
4057
+ // src/hooks/lib/security-gate-checks.ts
4085
4058
  var DANGEROUS_BASH_PATTERNS = [
4086
4059
  // BLOCK — destructive and irreversible, or arbitrary remote code execution.
4087
4060
  { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
@@ -4133,6 +4106,7 @@ var SECRET_EXACT_BASENAMES = [
4133
4106
  ];
4134
4107
  var CREDENTIALS_PATH_SUFFIX = "/credentials";
4135
4108
  var DOTENV = ".env";
4109
+ var SECRET_DATA_EXTENSIONS = ["yaml", "yml", "json", "toml", "ini", "env", "conf", "properties", "txt"];
4136
4110
  function looksLikeCode(token) {
4137
4111
  return /[()[\]{};=`$<>|&*?!"']/.test(token) || /\s/.test(token);
4138
4112
  }
@@ -4151,8 +4125,13 @@ function namesSecretFile(token) {
4151
4125
  return DOTENV_TEMPLATE_SUFFIXES.includes(suffix) ? null : `${base} file`;
4152
4126
  }
4153
4127
  if (base.endsWith(DOTENV) && clean.includes("/")) return `${base} file`;
4154
- for (const ext of SECRET_BASENAME_EXTENSIONS) {
4155
- if (base.endsWith(`.${ext}`)) return `${base} file`;
4128
+ for (const ext2 of SECRET_BASENAME_EXTENSIONS) {
4129
+ if (base.endsWith(`.${ext2}`)) return `${base} file`;
4130
+ }
4131
+ const parts = base.toLowerCase().split(".");
4132
+ const ext = parts.length > 1 ? parts[parts.length - 1] : "";
4133
+ if (SECRET_DATA_EXTENSIONS.includes(ext) && parts.slice(0, -1).some((p) => p === "secret" || p === "secrets")) {
4134
+ return `${base} file`;
4156
4135
  }
4157
4136
  return null;
4158
4137
  }
@@ -4311,38 +4290,8 @@ Review carefully before proceeding.`
4311
4290
  }
4312
4291
  return findings;
4313
4292
  }
4314
- function runSecurityGateChecks(hookInput) {
4315
- return runSecurityGateFindings(hookInput).map((f) => f.message);
4316
- }
4317
- async function main() {
4318
- try {
4319
- const input = await readStdin();
4320
- const hookInput = JSON.parse(input);
4321
- const messages = runSecurityGateChecks(hookInput);
4322
- for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
4323
- } catch (err) {
4324
- recordHookFailure("security-gate", err);
4325
- process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
4326
- process.exit(2);
4327
- }
4328
- process.exit(0);
4329
- }
4330
- function readStdin() {
4331
- return new Promise((resolve5) => {
4332
- let data = "";
4333
- process.stdin.setEncoding("utf-8");
4334
- process.stdin.on("data", (chunk) => {
4335
- data += chunk;
4336
- });
4337
- process.stdin.on("end", () => resolve5(data));
4338
- setTimeout(() => resolve5(data), 400);
4339
- });
4340
- }
4341
- if (isDirectInvocation(import.meta.url)) {
4342
- main();
4343
- }
4344
4293
 
4345
- // src/hooks/pre-delete-check.ts
4294
+ // src/hooks/lib/pre-delete-checks.ts
4346
4295
  init_db_driver();
4347
4296
  import { existsSync as existsSync9 } from "fs";
4348
4297
 
@@ -4438,7 +4387,7 @@ function getFeatureImpact(db, filePaths) {
4438
4387
  };
4439
4388
  }
4440
4389
 
4441
- // src/hooks/pre-delete-check.ts
4390
+ // src/hooks/lib/pre-delete-checks.ts
4442
4391
  init_config();
4443
4392
 
4444
4393
  // src/lib/source-layout.ts
@@ -4488,9 +4437,7 @@ function isUnderSourceDir(path, layout = getSourceLayout()) {
4488
4437
  return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
4489
4438
  }
4490
4439
 
4491
- // src/hooks/pre-delete-check.ts
4492
- init_hook_failure_signal();
4493
- var HOOK_EVENT2 = "PreToolUse";
4440
+ // src/hooks/lib/pre-delete-checks.ts
4494
4441
  var PROJECT_ROOT2 = getProjectRoot();
4495
4442
  var KNOWLEDGE_PROTECTED_FILES = [
4496
4443
  "knowledge-db.ts",
@@ -4629,32 +4576,40 @@ function runPreDeleteChecks(hookInput) {
4629
4576
  }
4630
4577
  return messages;
4631
4578
  }
4632
- async function main2() {
4633
- try {
4634
- const input = await readStdin2();
4635
- const hookInput = JSON.parse(input);
4636
- const messages = runPreDeleteChecks(hookInput);
4637
- for (const msg of messages) writeHookContext(HOOK_EVENT2, msg);
4638
- } catch (err) {
4639
- recordHookFailure("pre-delete-check", err);
4640
- process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
4641
- process.exit(2);
4642
- }
4643
- process.exit(0);
4644
- }
4645
- function readStdin2() {
4646
- return new Promise((resolve5) => {
4647
- let data = "";
4648
- process.stdin.setEncoding("utf-8");
4649
- process.stdin.on("data", (chunk) => {
4650
- data += chunk;
4651
- });
4652
- process.stdin.on("end", () => resolve5(data));
4653
- setTimeout(() => resolve5(data), 400);
4654
- });
4579
+
4580
+ // src/hooks/lib/write-hook-message.ts
4581
+ var HOOK_EVENTS = [
4582
+ "UserPromptSubmit",
4583
+ "UserPromptExpansion",
4584
+ "SessionStart",
4585
+ "PreToolUse",
4586
+ "PostToolUse",
4587
+ "Stop",
4588
+ "SubagentStop",
4589
+ "PreCompact"
4590
+ ];
4591
+ var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4592
+ function isHookEvent(value) {
4593
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
4655
4594
  }
4656
- if (isDirectInvocation(import.meta.url)) {
4657
- main2();
4595
+ function writeHookContext(hookEventName, message) {
4596
+ if (!isHookEvent(hookEventName)) {
4597
+ process.stderr.write(
4598
+ `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4599
+ `
4600
+ );
4601
+ return;
4602
+ }
4603
+ if (typeof message !== "string" || message.length === 0) return;
4604
+ let additionalContext = message;
4605
+ if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4606
+ const marker = `
4607
+ [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4608
+ additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4609
+ }
4610
+ process.stdout.write(
4611
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4612
+ );
4658
4613
  }
4659
4614
 
4660
4615
  // src/hooks/lib/read-stdin.ts
@@ -4692,9 +4647,8 @@ function readStdinToEof(deadlineMs = 4e3) {
4692
4647
  });
4693
4648
  }
4694
4649
 
4695
- // src/hooks/pre-tool-use-gate.ts
4650
+ // src/hooks/lib/pre-tool-use-decision.ts
4696
4651
  init_hook_failure_signal();
4697
- var HOOK_EVENT3 = "PreToolUse";
4698
4652
  var EXIT_DENY = 2;
4699
4653
  var EXIT_ALLOW = 0;
4700
4654
  function deny(reason, detail) {
@@ -4710,19 +4664,22 @@ The gate denies by default when it cannot verify a command is safe.
4710
4664
  );
4711
4665
  process.exit(EXIT_DENY);
4712
4666
  }
4713
- async function main3() {
4667
+ async function runPreToolUseDecision(hookName, event) {
4714
4668
  let hookInput;
4669
+ let input = "";
4715
4670
  try {
4716
- const input = await readStdinToEof();
4671
+ input = await readStdinToEof();
4717
4672
  if (!input.trim()) {
4718
- recordHookFailure("pre-tool-use-gate", new Error("empty stdin payload"));
4719
- deny(
4720
- "The gate received an EMPTY payload and could not determine what tool call was requested."
4721
- );
4673
+ recordHookFailure(hookName, new Error("empty stdin payload"));
4674
+ deny("The gate received an EMPTY payload and could not determine what tool call was requested.");
4722
4675
  }
4723
4676
  hookInput = JSON.parse(input);
4724
4677
  } catch (err) {
4725
- recordHookFailure("pre-tool-use-gate", err, { phase: "read-parse" });
4678
+ recordHookFailure(hookName, err, {
4679
+ phase: "read-parse",
4680
+ bytesRead: input.length,
4681
+ endsWithBrace: input.trimEnd().endsWith("}")
4682
+ });
4726
4683
  deny(
4727
4684
  "The gate could not READ or PARSE the tool-call payload, so it cannot know what it was being asked to approve.",
4728
4685
  err instanceof Error ? `Cause: ${err.message}` : void 0
@@ -4731,26 +4688,42 @@ async function main3() {
4731
4688
  let findings;
4732
4689
  try {
4733
4690
  const security = runSecurityGateFindings(hookInput);
4734
- const deletes = runPreDeleteChecks(
4735
- hookInput
4736
- ).map((message) => ({ severity: "warn", message }));
4691
+ const deletes = runPreDeleteChecks(hookInput).map(
4692
+ (message) => ({ severity: "warn", message })
4693
+ );
4737
4694
  findings = [...security, ...deletes];
4738
4695
  } catch (err) {
4739
- recordHookFailure("pre-tool-use-gate", err, { phase: "evaluate" });
4696
+ recordHookFailure(hookName, err, { phase: "evaluate" });
4740
4697
  deny(
4741
4698
  "A security check CRASHED while evaluating this tool call. A check that did not run has not passed.",
4742
4699
  err instanceof Error ? `Cause: ${err.message}` : void 0
4743
4700
  );
4744
4701
  }
4745
- const blocking = findings.filter((f) => f.severity === "block");
4746
4702
  for (const f of findings.filter((x) => x.severity === "warn")) {
4747
- writeHookContext(HOOK_EVENT3, f.message);
4748
- }
4749
- if (blocking.length > 0) {
4750
- deny(blocking.map((f) => f.message).join("\n\n"));
4703
+ writeHookContext(event, f.message);
4751
4704
  }
4705
+ const blocking = findings.filter((f) => f.severity === "block");
4706
+ if (blocking.length > 0) deny(blocking.map((f) => f.message).join("\n\n"));
4752
4707
  process.exit(EXIT_ALLOW);
4753
4708
  }
4709
+
4710
+ // src/hooks/lib/is-direct-invocation.ts
4711
+ import { pathToFileURL } from "url";
4712
+ function isDirectInvocation(moduleUrl) {
4713
+ const entry = process.argv[1];
4714
+ if (!entry) return false;
4715
+ try {
4716
+ return moduleUrl === pathToFileURL(entry).href;
4717
+ } catch {
4718
+ return false;
4719
+ }
4720
+ }
4721
+
4722
+ // src/hooks/pre-tool-use-gate.ts
4723
+ var HOOK_EVENT = "PreToolUse";
4724
+ async function main() {
4725
+ await runPreToolUseDecision("pre-tool-use-gate", HOOK_EVENT);
4726
+ }
4754
4727
  if (isDirectInvocation(import.meta.url)) {
4755
- main3();
4728
+ main();
4756
4729
  }
@@ -951,7 +951,8 @@ var init_config = __esm({
951
951
  input_per_million: z2.number(),
952
952
  output_per_million: z2.number(),
953
953
  cache_read_per_million: z2.number().optional(),
954
- cache_write_per_million: z2.number().optional()
954
+ cache_write_per_million: z2.number().optional(),
955
+ cache_write_1h_per_million: z2.number().optional()
955
956
  });
956
957
  AnalyticsConfigSchema = z2.object({
957
958
  quality: z2.object({
@@ -2208,6 +2209,7 @@ __export(memory_db_exports, {
2208
2209
  pruneToolCostEvents: () => pruneToolCostEvents,
2209
2210
  recordRecallHits: () => recordRecallHits,
2210
2211
  recordTelemetry: () => recordTelemetry,
2212
+ releaseMetaLease: () => releaseMetaLease,
2211
2213
  removePendingSync: () => removePendingSync,
2212
2214
  sanitizeFts5Query: () => sanitizeFts5Query,
2213
2215
  sanitizeFts5QueryOr: () => sanitizeFts5QueryOr,
@@ -2216,6 +2218,7 @@ __export(memory_db_exports, {
2216
2218
  searchObservations: () => searchObservations,
2217
2219
  setLastProcessedLine: () => setLastProcessedLine,
2218
2220
  setMemoryMeta: () => setMemoryMeta,
2221
+ tryAcquireMetaLease: () => tryAcquireMetaLease,
2219
2222
  upsertObservationEmbedding: () => upsertObservationEmbedding,
2220
2223
  usageWarmupElapsed: () => usageWarmupElapsed
2221
2224
  });
@@ -2721,6 +2724,15 @@ function initMemorySchema(db) {
2721
2724
  );
2722
2725
  CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
2723
2726
  `);
2727
+ const costCols = db.prepare(`PRAGMA table_info(session_costs)`).all();
2728
+ const addCostCol = (name, ddl) => {
2729
+ if (!costCols.some((c) => c.name === name)) db.exec(`ALTER TABLE session_costs ADD COLUMN ${ddl}`);
2730
+ };
2731
+ addCostCol("cost_status", `cost_status TEXT NOT NULL DEFAULT 'unknown'`);
2732
+ addCostCol("unpriced_models", `unpriced_models TEXT NOT NULL DEFAULT ''`);
2733
+ addCostCol("priced_responses", `priced_responses INTEGER NOT NULL DEFAULT 0`);
2734
+ addCostCol("unpriced_responses", `unpriced_responses INTEGER NOT NULL DEFAULT 0`);
2735
+ addCostCol("pricing_version", `pricing_version INTEGER NOT NULL DEFAULT 0`);
2724
2736
  db.exec(`
2725
2737
  CREATE TABLE IF NOT EXISTS feature_costs (
2726
2738
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -3150,6 +3162,18 @@ function getMemoryMeta(db, key) {
3150
3162
  function setMemoryMeta(db, key, value) {
3151
3163
  db.prepare("INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)").run(key, value);
3152
3164
  }
3165
+ function tryAcquireMetaLease(db, key, ttlSec, nowSec = Math.floor(Date.now() / 1e3)) {
3166
+ const r = db.prepare(
3167
+ `INSERT INTO memory_meta (key, value) VALUES (?, ?)
3168
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
3169
+ WHERE memory_meta.value = ''
3170
+ OR CAST(substr(memory_meta.value, instr(memory_meta.value, ':') + 1) AS INTEGER) <= ?`
3171
+ ).run(key, `${process.pid}:${nowSec + ttlSec}`, nowSec);
3172
+ return Number(r.changes) === 1;
3173
+ }
3174
+ function releaseMetaLease(db, key) {
3175
+ db.prepare(`UPDATE memory_meta SET value = '' WHERE key = ? AND value LIKE ?`).run(key, `${process.pid}:%`);
3176
+ }
3153
3177
  function upsertObservationEmbedding(db, observationId, vec, modelId, dim) {
3154
3178
  db.prepare(
3155
3179
  `INSERT OR REPLACE INTO observation_embeddings (observation_id, model_id, dim, vec, created_at)
@@ -4018,7 +4042,7 @@ var init_memory_db = __esm({
4018
4042
  init_rule_candidate_store();
4019
4043
  init_hook_failure_signal();
4020
4044
  init_memory_embed_sweep();
4021
- MEMORY_SCHEMA_VERSION = 1;
4045
+ MEMORY_SCHEMA_VERSION = 2;
4022
4046
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4023
4047
  MAX_DRAFT_TEXT_LEN = 16384;
4024
4048
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;