@massu/core 2.5.6 → 2.5.7

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/CHANGELOG.md CHANGED
@@ -10,6 +10,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
  > redistributed under the **Apache License 2.0**. Full license + NOTICE:
11
11
  > `packages/core/assets/embedder/MODEL-LICENSE`.
12
12
 
13
+ ## [2.5.7] - 2026-11-10
14
+
15
+ ### Fixed
16
+
17
+ - **Cloud sync no longer queues uploads the server refuses.** When the massu cloud refused an upload
18
+ for a reason that retrying cannot change (for example `403 Sync not available for current plan
19
+ status`), massu queued it and retried forever. One project's memory database grew to 8.5 GB this way,
20
+ 7.6 GB of it refused uploads, and every reply re-sent a ~179 KB upload that was always refused. Now:
21
+ - a refusal that cannot succeed is never queued or retried;
22
+ - when the account itself is refused (401 or 403), massu records the server's reason, pauses sync,
23
+ clears the queue (learned rules are kept), and shows `MASSU CLOUD SYNC PAUSED` with that reason at
24
+ session start. It checks again once a day and resumes on its own when the account allows it;
25
+ - temporary failures (network, 5xx, 429) still queue, but the queue now holds at most 500 items and
26
+ nothing older than 30 days, and every item gets its turn.
27
+ - **`massu install-hooks` now upgrades a project completely.** It also moves the massu MCP server in
28
+ `.mcp.json` to the same version as the hooks (it never did before, so every upgrade left the server
29
+ behind), and it replaces hook entries written by much older installers, which used to stay beside the
30
+ current ones and fail on every tool call. Your own hooks and any hand-written massu entry are left alone.
31
+ - **A hook launched through a symlinked path now runs.** Started through a symlink, a hook decided it was
32
+ not the program being run and exited without doing anything; for the safety gate that meant allowing
33
+ the call. The commands `massu init` writes were not affected.
34
+
35
+ ### Added
36
+
37
+ - **The first prompt of a session is faster.** Session start now loads the memory-search model in the
38
+ background, so the first prompt no longer pays for it (measured: about 1.5 s for the first search,
39
+ 0.6 s after). Turn it off with `MASSU_RECALL_WARMUP=0`.
40
+ - **`scripts/massu-db-size-audit.sh`** (in the repository) reports where a massu database's space goes:
41
+ live data, space freed but not yet returned to the disk, the largest tables, and the sync queue.
42
+
13
43
  ## [2.5.6] - 2026-11-09
14
44
 
15
45
  ### Security
package/dist/cli.js CHANGED
@@ -2490,6 +2490,8 @@ __export(memory_db_exports, {
2490
2490
  MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
2491
2491
  MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
2492
2492
  MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
2493
+ PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
2494
+ PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
2493
2495
  TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
2494
2496
  USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
2495
2497
  addConversationTurn: () => addConversationTurn,
@@ -2498,6 +2500,7 @@ __export(memory_db_exports, {
2498
2500
  addSummary: () => addSummary,
2499
2501
  addToolCallDetail: () => addToolCallDetail,
2500
2502
  addUserPrompt: () => addUserPrompt,
2503
+ allPendingSync: () => allPendingSync,
2501
2504
  appendIncidentToFailureClass: () => appendIncidentToFailureClass,
2502
2505
  armUsageCounter: () => armUsageCounter,
2503
2506
  assignImportance: () => assignImportance,
@@ -2505,6 +2508,7 @@ __export(memory_db_exports, {
2505
2508
  createSession: () => createSession,
2506
2509
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
2507
2510
  dequeuePendingSync: () => dequeuePendingSync,
2511
+ discardPendingSync: () => discardPendingSync,
2508
2512
  embedMissingObservations: () => embedMissingObservations,
2509
2513
  endSession: () => endSession,
2510
2514
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -3487,6 +3491,35 @@ function initMemorySchema(db) {
3487
3491
  }
3488
3492
  function enqueueSyncPayload(db, payload) {
3489
3493
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
3494
+ const overflow = db.prepare(
3495
+ `SELECT id, retry_count, last_error, payload FROM pending_sync
3496
+ WHERE created_at < datetime('now', ?)
3497
+ OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
3498
+ LIMIT 10000`
3499
+ ).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
3500
+ if (overflow.length > 0) {
3501
+ discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
3502
+ }
3503
+ }
3504
+ function discardPendingSync(db, rows, why) {
3505
+ if (rows.length === 0) return 0;
3506
+ const rescued = rescueLearningFromStalePayloads(db, rows);
3507
+ for (let i = 0; i < rows.length; i += 500) {
3508
+ const ids = rows.slice(i, i + 500).map((r3) => r3.id);
3509
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
3510
+ }
3511
+ if (rescued > 0) {
3512
+ process.stderr.write(
3513
+ `[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
3514
+ `
3515
+ );
3516
+ }
3517
+ process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
3518
+ `);
3519
+ return rows.length;
3520
+ }
3521
+ function allPendingSync(db) {
3522
+ return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
3490
3523
  }
3491
3524
  function getMemoryMeta(db, key) {
3492
3525
  const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
@@ -3689,7 +3722,9 @@ function dequeuePendingSync(db, limit = 10) {
3689
3722
  }
3690
3723
  }
3691
3724
  return db.prepare(
3692
- "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
3725
+ // Least-retried first: oldest-first retried the same 10 items until they were discarded
3726
+ // while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
3727
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
3693
3728
  ).all(limit);
3694
3729
  }
3695
3730
  function removePendingSync(db, id) {
@@ -4362,7 +4397,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
4362
4397
  }
4363
4398
  return bestMatch;
4364
4399
  }
4365
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4400
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4366
4401
  var init_memory_db = __esm({
4367
4402
  "src/memory-db.ts"() {
4368
4403
  "use strict";
@@ -4377,6 +4412,8 @@ var init_memory_db = __esm({
4377
4412
  init_memory_embed_sweep();
4378
4413
  MEMORY_SCHEMA_VERSION = 2;
4379
4414
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4415
+ PENDING_SYNC_MAX_ITEMS = 500;
4416
+ PENDING_SYNC_MAX_AGE_DAYS = 30;
4380
4417
  MAX_DRAFT_TEXT_LEN = 16384;
4381
4418
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
4382
4419
  _temporalColCache = /* @__PURE__ */ new WeakMap();
@@ -16446,6 +16483,7 @@ ${import_picocolors2.default.gray(m2)} ${s}
16446
16483
  // src/commands/init.ts
16447
16484
  var init_exports = {};
16448
16485
  __export(init_exports, {
16486
+ SUPERSEDED_MASSU_HOOKS: () => SUPERSEDED_MASSU_HOOKS,
16449
16487
  applyVariantTemplate: () => applyVariantTemplate,
16450
16488
  buildConfigFromDetection: () => buildConfigFromDetection,
16451
16489
  buildHooksConfig: () => buildHooksConfig,
@@ -16468,6 +16506,7 @@ __export(init_exports, {
16468
16506
  mergeHooksConfig: () => mergeHooksConfig,
16469
16507
  parseInitArgs: () => parseInitArgs,
16470
16508
  printInitHelp: () => printInitHelp,
16509
+ refreshMcpServerPin: () => refreshMcpServerPin,
16471
16510
  registerMcpServer: () => registerMcpServer,
16472
16511
  renderConfigYaml: () => renderConfigYaml,
16473
16512
  resolveHooksDir: () => resolveHooksDir,
@@ -17052,6 +17091,32 @@ function getInstallerVersion() {
17052
17091
  "getInstallerVersion: could not resolve @massu/core package.json. This indicates a corrupt install. Re-install via `npx -y @massu/core init`."
17053
17092
  );
17054
17093
  }
17094
+ function refreshMcpServerPin(projectRoot) {
17095
+ const mcpPath = resolve12(projectRoot, ".mcp.json");
17096
+ if (!existsSync21(mcpPath)) return "absent";
17097
+ let doc;
17098
+ try {
17099
+ doc = JSON.parse(readFileSync17(mcpPath, "utf-8"));
17100
+ } catch {
17101
+ return "custom";
17102
+ }
17103
+ const servers = doc.mcpServers ?? {};
17104
+ const cur = servers.massu;
17105
+ if (!cur) return "absent";
17106
+ const args2 = Array.isArray(cur.args) ? cur.args.map(String) : [];
17107
+ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
17108
+ const shimForm = typeof cur.command === "string" && /[/\\]massu-hook$/.test(cur.command) && args2.length === 1 && SEMVER.test(args2[0]);
17109
+ const npxForm = cur.command === "npx" && args2.length === 2 && args2[0] === "-y" && /^@massu\/core@/.test(args2[1]);
17110
+ if (!shimForm && !npxForm) return "custom";
17111
+ const version = getInstallerVersion();
17112
+ const shim = resolveMassuShim();
17113
+ const next = shim !== null ? { type: "stdio", command: shim, args: [version] } : { type: "stdio", command: "npx", args: ["-y", `@massu/core@${version}`] };
17114
+ if (JSON.stringify({ ...cur, ...next }) === JSON.stringify(cur)) return "current";
17115
+ servers.massu = { ...cur, ...next };
17116
+ doc.mcpServers = servers;
17117
+ writeFileSync5(mcpPath, JSON.stringify(doc, null, 2) + "\n", "utf-8");
17118
+ return "updated";
17119
+ }
17055
17120
  function registerMcpServer(projectRoot) {
17056
17121
  const mcpPath = resolve12(projectRoot, ".mcp.json");
17057
17122
  let existing = {};
@@ -17316,12 +17381,33 @@ function mergeHooksConfig(existing, additions) {
17316
17381
  }
17317
17382
  merged[event] = Array.from(byMatcher.values());
17318
17383
  }
17384
+ const emitted = /* @__PURE__ */ new Set();
17385
+ for (const groups of Object.values(additions ?? {})) {
17386
+ for (const g2 of groups) {
17387
+ for (const h2 of g2.hooks ?? []) {
17388
+ const id = typeof h2?.command === "string" ? massuHookIdentity(h2.command) : null;
17389
+ if (id) emitted.add(id);
17390
+ }
17391
+ }
17392
+ }
17393
+ for (const event of Object.keys(merged)) {
17394
+ merged[event] = merged[event].map((g2) => ({
17395
+ ...g2,
17396
+ hooks: (g2.hooks ?? []).filter((h2) => {
17397
+ const id = typeof h2?.command === "string" ? massuHookIdentity(h2.command) : null;
17398
+ const by = id ? SUPERSEDED_MASSU_HOOKS[id] : void 0;
17399
+ return !(by && emitted.has(by));
17400
+ })
17401
+ })).filter((g2) => (g2.hooks ?? []).length > 0);
17402
+ }
17319
17403
  return merged;
17320
17404
  }
17321
17405
  function massuHookIdentity(command) {
17322
17406
  if (!/@massu\/core|[/\\]\.?massu[/\\]/.test(command)) return null;
17323
17407
  const m3 = /\bhook-runner\s+([A-Za-z0-9._-]+)/.exec(command);
17324
- return m3 ? m3[1] : null;
17408
+ if (m3) return m3[1];
17409
+ const legacy = /@massu[/\\]core[/\\]dist[/\\]hooks[/\\]([a-z0-9-]+)\.js/.exec(command);
17410
+ return legacy ? legacy[1] : null;
17325
17411
  }
17326
17412
  function mergeHookEntries(existing, additions) {
17327
17413
  const seenCommands = /* @__PURE__ */ new Set();
@@ -17747,7 +17833,7 @@ async function promptStackConfirm() {
17747
17833
  function capitalize(str) {
17748
17834
  return str.charAt(0).toUpperCase() + str.slice(1);
17749
17835
  }
17750
- var __filename2, __dirname2, FRAMEWORK_TO_TEMPLATE_ID, TEMPLATE_NAMES;
17836
+ var __filename2, __dirname2, FRAMEWORK_TO_TEMPLATE_ID, TEMPLATE_NAMES, SUPERSEDED_MASSU_HOOKS;
17751
17837
  var init_init = __esm({
17752
17838
  "src/commands/init.ts"() {
17753
17839
  "use strict";
@@ -17780,6 +17866,10 @@ var init_init = __esm({
17780
17866
  "swift-ios",
17781
17867
  "multi-runtime"
17782
17868
  ];
17869
+ SUPERSEDED_MASSU_HOOKS = Object.freeze({
17870
+ "security-gate": "pre-tool-use-gate",
17871
+ "pre-delete-check": "pre-tool-use-gate"
17872
+ });
17783
17873
  }
17784
17874
  });
17785
17875
 
@@ -19319,6 +19409,10 @@ async function runInstallHooks() {
19319
19409
  }
19320
19410
  const { count } = installHooks(projectRoot);
19321
19411
  console.log(` Installed ${count} hooks in .claude/settings.local.json`);
19412
+ const mcp = refreshMcpServerPin(projectRoot);
19413
+ if (mcp === "updated") console.log(` Updated the massu MCP server in .mcp.json to ${version} (commit this file)`);
19414
+ else if (mcp === "current") console.log(` .mcp.json already runs massu ${version}`);
19415
+ else if (mcp === "custom") console.log(" .mcp.json has a hand-written massu entry \u2014 left unchanged; check its version");
19322
19416
  console.log("");
19323
19417
  console.log("Hooks will activate on your next Claude Code session.");
19324
19418
  console.log("");
@@ -2086,6 +2086,8 @@ __export(memory_db_exports, {
2086
2086
  MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
2087
2087
  MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
2088
2088
  MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
2089
+ PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
2090
+ PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
2089
2091
  TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
2090
2092
  USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
2091
2093
  addConversationTurn: () => addConversationTurn,
@@ -2094,6 +2096,7 @@ __export(memory_db_exports, {
2094
2096
  addSummary: () => addSummary,
2095
2097
  addToolCallDetail: () => addToolCallDetail,
2096
2098
  addUserPrompt: () => addUserPrompt,
2099
+ allPendingSync: () => allPendingSync,
2097
2100
  appendIncidentToFailureClass: () => appendIncidentToFailureClass,
2098
2101
  armUsageCounter: () => armUsageCounter,
2099
2102
  assignImportance: () => assignImportance,
@@ -2101,6 +2104,7 @@ __export(memory_db_exports, {
2101
2104
  createSession: () => createSession,
2102
2105
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
2103
2106
  dequeuePendingSync: () => dequeuePendingSync,
2107
+ discardPendingSync: () => discardPendingSync,
2104
2108
  embedMissingObservations: () => embedMissingObservations,
2105
2109
  endSession: () => endSession,
2106
2110
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -3083,6 +3087,35 @@ function initMemorySchema(db) {
3083
3087
  }
3084
3088
  function enqueueSyncPayload(db, payload) {
3085
3089
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
3090
+ const overflow = db.prepare(
3091
+ `SELECT id, retry_count, last_error, payload FROM pending_sync
3092
+ WHERE created_at < datetime('now', ?)
3093
+ OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
3094
+ LIMIT 10000`
3095
+ ).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
3096
+ if (overflow.length > 0) {
3097
+ discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
3098
+ }
3099
+ }
3100
+ function discardPendingSync(db, rows, why) {
3101
+ if (rows.length === 0) return 0;
3102
+ const rescued = rescueLearningFromStalePayloads(db, rows);
3103
+ for (let i = 0; i < rows.length; i += 500) {
3104
+ const ids = rows.slice(i, i + 500).map((r) => r.id);
3105
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
3106
+ }
3107
+ if (rescued > 0) {
3108
+ process.stderr.write(
3109
+ `[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
3110
+ `
3111
+ );
3112
+ }
3113
+ process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
3114
+ `);
3115
+ return rows.length;
3116
+ }
3117
+ function allPendingSync(db) {
3118
+ return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
3086
3119
  }
3087
3120
  function getMemoryMeta(db, key) {
3088
3121
  const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
@@ -3285,7 +3318,9 @@ function dequeuePendingSync(db, limit = 10) {
3285
3318
  }
3286
3319
  }
3287
3320
  return db.prepare(
3288
- "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
3321
+ // Least-retried first: oldest-first retried the same 10 items until they were discarded
3322
+ // while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
3323
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
3289
3324
  ).all(limit);
3290
3325
  }
3291
3326
  function removePendingSync(db, id) {
@@ -3958,7 +3993,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3958
3993
  }
3959
3994
  return bestMatch;
3960
3995
  }
3961
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3996
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3962
3997
  var init_memory_db = __esm({
3963
3998
  "src/memory-db.ts"() {
3964
3999
  "use strict";
@@ -3973,6 +4008,8 @@ var init_memory_db = __esm({
3973
4008
  init_memory_embed_sweep();
3974
4009
  MEMORY_SCHEMA_VERSION = 2;
3975
4010
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4011
+ PENDING_SYNC_MAX_ITEMS = 500;
4012
+ PENDING_SYNC_MAX_AGE_DAYS = 30;
3976
4013
  MAX_DRAFT_TEXT_LEN = 16384;
3977
4014
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3978
4015
  _temporalColCache = /* @__PURE__ */ new WeakMap();
@@ -4058,7 +4095,7 @@ var init_hook_failure_signal = __esm({
4058
4095
  init_config();
4059
4096
  import { execFileSync } from "child_process";
4060
4097
  import { createHash } from "crypto";
4061
- import { existsSync as existsSync10, readFileSync as readFileSync7, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
4098
+ import { existsSync as existsSync11, readFileSync as readFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync2, statSync as statSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
4062
4099
  import { tmpdir as tmpdir2 } from "os";
4063
4100
  import { join as join6, dirname as dirname8 } from "path";
4064
4101
 
@@ -4100,20 +4137,48 @@ function writeHookContext(hookEventName, message) {
4100
4137
  // src/hooks/auto-learning-pipeline.ts
4101
4138
  init_hook_failure_signal();
4102
4139
 
4140
+ // src/lib/safe-write.ts
4141
+ import {
4142
+ writeFileSync as writeFileSync3,
4143
+ renameSync,
4144
+ openSync as openSync2,
4145
+ fsyncSync as fsyncSync2,
4146
+ closeSync as closeSync2,
4147
+ existsSync as existsSync9,
4148
+ realpathSync,
4149
+ unlinkSync as unlinkSync2,
4150
+ accessSync as accessSync2,
4151
+ statSync as statSync2,
4152
+ chmodSync as chmodSync3,
4153
+ constants
4154
+ } from "fs";
4155
+ function isSameRealPath(a, b) {
4156
+ try {
4157
+ return realpathSync(a) === realpathSync(b);
4158
+ } catch {
4159
+ return false;
4160
+ }
4161
+ }
4162
+
4103
4163
  // src/hooks/lib/is-direct-invocation.ts
4104
- import { pathToFileURL } from "url";
4164
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4105
4165
  function isDirectInvocation(moduleUrl) {
4106
4166
  const entry = process.argv[1];
4107
4167
  if (!entry) return false;
4108
4168
  try {
4109
- return moduleUrl === pathToFileURL(entry).href;
4169
+ if (moduleUrl === pathToFileURL(entry).href) return true;
4170
+ } catch {
4171
+ return false;
4172
+ }
4173
+ try {
4174
+ return isSameRealPath(fileURLToPath2(moduleUrl), entry);
4110
4175
  } catch {
4111
4176
  return false;
4112
4177
  }
4113
4178
  }
4114
4179
 
4115
4180
  // src/hooks/lib/session-touched-files.ts
4116
- import { readFileSync as readFileSync6, existsSync as existsSync9, realpathSync } from "fs";
4181
+ import { readFileSync as readFileSync6, existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
4117
4182
  import { relative, isAbsolute, resolve as resolve5 } from "path";
4118
4183
  var FILE_WRITING_TOOLS = Object.freeze({
4119
4184
  Edit: ["file_path"],
@@ -4123,7 +4188,7 @@ var FILE_WRITING_TOOLS = Object.freeze({
4123
4188
  });
4124
4189
  function canonical(p) {
4125
4190
  try {
4126
- return realpathSync(p);
4191
+ return realpathSync2(p);
4127
4192
  } catch {
4128
4193
  return p;
4129
4194
  }
@@ -4171,7 +4236,7 @@ function transcriptTouchedFiles(root, transcriptPath) {
4171
4236
  return out;
4172
4237
  }
4173
4238
  function flagFileTouchedFiles(root, flagPath) {
4174
- if (!existsSync9(flagPath)) return null;
4239
+ if (!existsSync10(flagPath)) return null;
4175
4240
  let raw;
4176
4241
  try {
4177
4242
  raw = readFileSync6(flagPath, "utf-8");
@@ -4592,7 +4657,7 @@ function readShownKeys(ackPath) {
4592
4657
  }
4593
4658
  function writeShownKeys(ackPath, keys) {
4594
4659
  mkdirSync7(dirname8(ackPath), { recursive: true });
4595
- writeFileSync3(ackPath, JSON.stringify({ shown: [...keys].sort() }), "utf-8");
4660
+ writeFileSync4(ackPath, JSON.stringify({ shown: [...keys].sort() }), "utf-8");
4596
4661
  }
4597
4662
  function freshMatches(matchKeys, shown) {
4598
4663
  return [...new Set(matchKeys)].filter((k) => !shown.has(k));
@@ -4602,7 +4667,7 @@ function recentIncidentFiles(dir, nowMs = Date.now()) {
4602
4667
  try {
4603
4668
  return readdirSync2(dir).filter((f) => f.endsWith(".md")).filter((f) => {
4604
4669
  try {
4605
- return nowMs - statSync2(join6(dir, f)).mtimeMs <= RECENT_INCIDENT_WINDOW_MS;
4670
+ return nowMs - statSync3(join6(dir, f)).mtimeMs <= RECENT_INCIDENT_WINDOW_MS;
4606
4671
  } catch {
4607
4672
  return false;
4608
4673
  }
@@ -4616,7 +4681,7 @@ function readPostImageFrom(root) {
4616
4681
  return (rel) => {
4617
4682
  try {
4618
4683
  const abs = join6(root, rel);
4619
- if (statSync2(abs).size > MAX_POST_IMAGE_BYTES) return null;
4684
+ if (statSync3(abs).size > MAX_POST_IMAGE_BYTES) return null;
4620
4685
  return readFileSync7(abs, "utf-8");
4621
4686
  } catch {
4622
4687
  return null;
@@ -4729,7 +4794,7 @@ async function main() {
4729
4794
  const autoLearn = config.autoLearning;
4730
4795
  const flagPath = getSessionFlagPath(hookInput.session_id);
4731
4796
  let sessionFixes = [];
4732
- if (existsSync10(flagPath)) {
4797
+ if (existsSync11(flagPath)) {
4733
4798
  try {
4734
4799
  sessionFixes = readFileSync7(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
4735
4800
  } catch {
@@ -4821,16 +4886,16 @@ async function main() {
4821
4886
  }
4822
4887
  function cleanup(flagPath) {
4823
4888
  try {
4824
- if (existsSync10(flagPath)) unlinkSync2(flagPath);
4889
+ if (existsSync11(flagPath)) unlinkSync3(flagPath);
4825
4890
  const dir = join6(tmpdir2(), "massu-auto-learning");
4826
- if (existsSync10(dir)) {
4891
+ if (existsSync11(dir)) {
4827
4892
  const now = Date.now();
4828
4893
  for (const file of readdirSync2(dir)) {
4829
4894
  const fullPath = join6(dir, file);
4830
4895
  try {
4831
- const stat = statSync2(fullPath);
4896
+ const stat = statSync3(fullPath);
4832
4897
  if (now - stat.mtimeMs > 864e5) {
4833
- unlinkSync2(fullPath);
4898
+ unlinkSync3(fullPath);
4834
4899
  }
4835
4900
  } catch {
4836
4901
  }
@@ -2157,6 +2157,8 @@ __export(memory_db_exports, {
2157
2157
  MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
2158
2158
  MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
2159
2159
  MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
2160
+ PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
2161
+ PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
2160
2162
  TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
2161
2163
  USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
2162
2164
  addConversationTurn: () => addConversationTurn,
@@ -2165,6 +2167,7 @@ __export(memory_db_exports, {
2165
2167
  addSummary: () => addSummary,
2166
2168
  addToolCallDetail: () => addToolCallDetail,
2167
2169
  addUserPrompt: () => addUserPrompt,
2170
+ allPendingSync: () => allPendingSync,
2168
2171
  appendIncidentToFailureClass: () => appendIncidentToFailureClass,
2169
2172
  armUsageCounter: () => armUsageCounter,
2170
2173
  assignImportance: () => assignImportance,
@@ -2172,6 +2175,7 @@ __export(memory_db_exports, {
2172
2175
  createSession: () => createSession,
2173
2176
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
2174
2177
  dequeuePendingSync: () => dequeuePendingSync,
2178
+ discardPendingSync: () => discardPendingSync,
2175
2179
  embedMissingObservations: () => embedMissingObservations,
2176
2180
  endSession: () => endSession,
2177
2181
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -3154,6 +3158,35 @@ function initMemorySchema(db) {
3154
3158
  }
3155
3159
  function enqueueSyncPayload(db, payload) {
3156
3160
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
3161
+ const overflow = db.prepare(
3162
+ `SELECT id, retry_count, last_error, payload FROM pending_sync
3163
+ WHERE created_at < datetime('now', ?)
3164
+ OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
3165
+ LIMIT 10000`
3166
+ ).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
3167
+ if (overflow.length > 0) {
3168
+ discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
3169
+ }
3170
+ }
3171
+ function discardPendingSync(db, rows, why) {
3172
+ if (rows.length === 0) return 0;
3173
+ const rescued = rescueLearningFromStalePayloads(db, rows);
3174
+ for (let i = 0; i < rows.length; i += 500) {
3175
+ const ids = rows.slice(i, i + 500).map((r) => r.id);
3176
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
3177
+ }
3178
+ if (rescued > 0) {
3179
+ process.stderr.write(
3180
+ `[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
3181
+ `
3182
+ );
3183
+ }
3184
+ process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
3185
+ `);
3186
+ return rows.length;
3187
+ }
3188
+ function allPendingSync(db) {
3189
+ return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
3157
3190
  }
3158
3191
  function getMemoryMeta(db, key) {
3159
3192
  const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
@@ -3356,7 +3389,9 @@ function dequeuePendingSync(db, limit = 10) {
3356
3389
  }
3357
3390
  }
3358
3391
  return db.prepare(
3359
- "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
3392
+ // Least-retried first: oldest-first retried the same 10 items until they were discarded
3393
+ // while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
3394
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
3360
3395
  ).all(limit);
3361
3396
  }
3362
3397
  function removePendingSync(db, id) {
@@ -4029,7 +4064,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
4029
4064
  }
4030
4065
  return bestMatch;
4031
4066
  }
4032
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4067
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4033
4068
  var init_memory_db = __esm({
4034
4069
  "src/memory-db.ts"() {
4035
4070
  "use strict";
@@ -4044,6 +4079,8 @@ var init_memory_db = __esm({
4044
4079
  init_memory_embed_sweep();
4045
4080
  MEMORY_SCHEMA_VERSION = 2;
4046
4081
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4082
+ PENDING_SYNC_MAX_ITEMS = 500;
4083
+ PENDING_SYNC_MAX_AGE_DAYS = 30;
4047
4084
  MAX_DRAFT_TEXT_LEN = 16384;
4048
4085
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
4049
4086
  _temporalColCache = /* @__PURE__ */ new WeakMap();
@@ -4057,7 +4094,7 @@ var init_memory_db = __esm({
4057
4094
  // src/hooks/classify-failure.ts
4058
4095
  init_config();
4059
4096
  init_memory_db();
4060
- import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
4097
+ import { existsSync as existsSync10, readFileSync as readFileSync6, readdirSync as readdirSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
4061
4098
  import { tmpdir as tmpdir2 } from "os";
4062
4099
  import { join as join6, basename as basename3 } from "path";
4063
4100
 
@@ -4109,13 +4146,41 @@ function writeHookContext(hookEventName, message) {
4109
4146
  // src/hooks/classify-failure.ts
4110
4147
  init_hook_failure_signal();
4111
4148
 
4149
+ // src/lib/safe-write.ts
4150
+ import {
4151
+ writeFileSync as writeFileSync3,
4152
+ renameSync,
4153
+ openSync as openSync2,
4154
+ fsyncSync as fsyncSync2,
4155
+ closeSync as closeSync2,
4156
+ existsSync as existsSync9,
4157
+ realpathSync,
4158
+ unlinkSync as unlinkSync2,
4159
+ accessSync as accessSync2,
4160
+ statSync as statSync2,
4161
+ chmodSync as chmodSync3,
4162
+ constants
4163
+ } from "fs";
4164
+ function isSameRealPath(a, b) {
4165
+ try {
4166
+ return realpathSync(a) === realpathSync(b);
4167
+ } catch {
4168
+ return false;
4169
+ }
4170
+ }
4171
+
4112
4172
  // src/hooks/lib/is-direct-invocation.ts
4113
- import { pathToFileURL } from "url";
4173
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4114
4174
  function isDirectInvocation(moduleUrl) {
4115
4175
  const entry = process.argv[1];
4116
4176
  if (!entry) return false;
4117
4177
  try {
4118
- return moduleUrl === pathToFileURL(entry).href;
4178
+ if (moduleUrl === pathToFileURL(entry).href) return true;
4179
+ } catch {
4180
+ return false;
4181
+ }
4182
+ try {
4183
+ return isSameRealPath(fileURLToPath2(moduleUrl), entry);
4119
4184
  } catch {
4120
4185
  return false;
4121
4186
  }
@@ -4160,7 +4225,7 @@ function cleanupFailureContextFiles() {
4160
4225
  const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
4161
4226
  for (const file of files) {
4162
4227
  try {
4163
- unlinkSync2(join6(dir, file));
4228
+ unlinkSync3(join6(dir, file));
4164
4229
  } catch {
4165
4230
  }
4166
4231
  }
@@ -4226,12 +4291,12 @@ async function main() {
4226
4291
  return;
4227
4292
  }
4228
4293
  const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
4229
- if (existsSync9(dedupeMarker)) {
4294
+ if (existsSync10(dedupeMarker)) {
4230
4295
  process.exit(0);
4231
4296
  return;
4232
4297
  }
4233
4298
  try {
4234
- writeFileSync3(dedupeMarker, "1");
4299
+ writeFileSync4(dedupeMarker, "1");
4235
4300
  } catch {
4236
4301
  }
4237
4302
  const db = getMemoryDb();