@lh8ppl/claude-memory-kit 0.3.4 → 0.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lh8ppl/claude-memory-kit",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "cmk — the CLI for claude-memory-kit. Per-project, in-repo memory system for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,31 @@
37
37
  // a time, gated by the cooldown), so there is no herd to avoid — a plain exponential
38
38
  // backoff is sufficient and keeps the timing deterministic for tests.
39
39
 
40
+ // The timeout for compress callers that have NO outer hook ceiling — the cron /
41
+ // detached-lazy children (daily-distill, weekly-curate, the lazy compressSession).
42
+ // The D-92/F-2 composition rule: a ceiling-free caller must NOT inherit the
43
+ // hook-sized 50s bound (which is sized under the 60s SessionEnd ceiling, §8.5).
44
+ // 120s is chosen against MEASURED `claude --print` latency: it runs ~18-27s when
45
+ // fast but was observed at 78s (Task 163 live, on a 4.7KB input) and 89s (the v0.3.4
46
+ // cut-gate, 10KB) in slow-Haiku windows — environmental, not size-driven (D-174).
47
+ // 120s clears those with headroom; the 50s budget killed them needlessly, leaving
48
+ // `recent.md` stale (D-179). One constant so the family can't drift back to 50s.
49
+ // (auto-extract uses its own 90s under the Stop hook — a separate detached path.)
50
+ export const CEILING_FREE_TIMEOUT_MS = 120_000;
51
+
52
+ // The backoff BETWEEN retries on the ceiling-free paths. The default baseBackoffMs
53
+ // (600ms) is far too short for the kit's failure mode: `claude --print` slowness is
54
+ // a transient WINDOW (slow for a stretch, then fine — D-174), and the whole point of
55
+ // backoff is to let that window PASS before retrying. A 600ms wait retries while
56
+ // still INSIDE the same slow window, so attempt 2 hits the same slowness and also
57
+ // times out. The field waits SECONDS for exactly this reason (graphiti 5-120s,
58
+ // Letta cap 10s, mempalace 2-8s — all checked across 19 systems; NONE use sub-second
59
+ // backoff, and NONE escalate the timeout itself). 5s is the field's low end — one
60
+ // 5s wait between the 2 ceiling-free attempts gives the slow window room to clear.
61
+ // Safe on every path: the HOOK path is maxAttempts:1 (no retry → backoff never
62
+ // fires); the ceiling-free paths run detached/cron, so a multi-second wait is free.
63
+ export const CEILING_FREE_BACKOFF_MS = 5_000;
64
+
40
65
  /**
41
66
  * Classify a compress() rejection as transient (worth a retry) or deterministic
42
67
  * (a re-call re-fails identically — don't waste the attempt or the budget).
@@ -232,6 +232,16 @@ export async function compressSession({
232
232
  // caller (runLazyCompress) passes maxAttempts:2 to opt into one retry; the hook
233
233
  // keeps its restore-on-failure (D-79) and delegates the retry to that lazy path.
234
234
  maxAttempts = 1,
235
+ // DEFAULT 50s = the SessionEnd-hook budget (sized under the 60s ceiling, §8.5).
236
+ // The ceiling-free LAZY caller (runLazyCompress, a detached SessionStart child
237
+ // with NO outer ceiling) passes 120s so a slow-but-not-broken `claude --print`
238
+ // window doesn't time out needlessly — the D-92/F-2 composition rule: a
239
+ // ceiling-free caller must not inherit a ceiling-sized timeout.
240
+ timeoutMs = 50_000,
241
+ // Backoff between retries (only the lazy maxAttempts:2 path retries). DEFAULT
242
+ // undefined → compressWithRetry's 600ms; the ceiling-free LAZY caller passes the
243
+ // 5s ceiling-free backoff so a retry lands AFTER the slow-Haiku window (D-179).
244
+ baseBackoffMs,
235
245
  } = {}) {
236
246
  const ts = now ?? nowIso();
237
247
  const date = dateFromIso(ts);
@@ -343,9 +353,11 @@ export async function compressSession({
343
353
  instructions,
344
354
  preserveCitationIds: true,
345
355
  maxOutputBytes,
346
- timeoutMs: 50_000,
356
+ timeoutMs,
347
357
  },
348
- { maxAttempts, onRetry: () => { retries += 1; } },
358
+ // baseBackoffMs only forwarded when the caller set it (the lazy ceiling-free
359
+ // path passes the 5s backoff); undefined → compressWithRetry's 600ms default.
360
+ { maxAttempts, ...(baseBackoffMs != null ? { baseBackoffMs } : {}), onRetry: () => { retries += 1; } },
349
361
  );
350
362
  } catch (err) {
351
363
  // Distinguish HAIKU_TIMEOUT (slow Anthropic) from COMPRESS_FAILED
@@ -28,7 +28,7 @@ import { join } from 'node:path';
28
28
  import { nowIso } from './audit-log.mjs';
29
29
  import { ERROR_CATEGORIES } from './result-shapes.mjs';
30
30
  import { HaikuTimeoutError } from './compressor.mjs';
31
- import { compressWithRetry } from './compress-retry.mjs';
31
+ import { compressWithRetry, CEILING_FREE_TIMEOUT_MS, CEILING_FREE_BACKOFF_MS } from './compress-retry.mjs';
32
32
  import {
33
33
  DEFAULT_COOLDOWN_MS,
34
34
  isCooldownActive,
@@ -209,9 +209,13 @@ export async function dailyDistill({
209
209
  instructions,
210
210
  preserveCitationIds: true,
211
211
  maxOutputBytes,
212
- timeoutMs: 50_000,
212
+ // Ceiling-free (cron / detached lazy child, NO 60s hook ceiling) → the
213
+ // generous ceiling-free timeout, NOT the hook-sized 50s (D-92/F-2 + D-179).
214
+ timeoutMs: CEILING_FREE_TIMEOUT_MS,
213
215
  },
214
- { maxAttempts: 2, onRetry: () => { retries += 1; } },
216
+ // 5s backoff between the 2 attempts (NOT the 600ms default) so a retry lands
217
+ // AFTER the transient slow-Haiku window, not inside it (D-179).
218
+ { maxAttempts: 2, baseBackoffMs: CEILING_FREE_BACKOFF_MS, onRetry: () => { retries += 1; } },
215
219
  );
216
220
  touchCooldownMarker({ projectRoot, now: ts });
217
221
  } catch (err) {
@@ -44,6 +44,7 @@ import {
44
44
  import { dailyDistill } from './daily-distill.mjs';
45
45
  import { weeklyCurate } from './weekly-curate.mjs';
46
46
  import { compressSession } from './compress-session.mjs';
47
+ import { CEILING_FREE_TIMEOUT_MS, CEILING_FREE_BACKOFF_MS } from './compress-retry.mjs';
47
48
  import { syncDecisionsJournal } from './decisions-journal.mjs';
48
49
 
49
50
  const DEFAULT_DAILY_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -416,6 +417,11 @@ export async function runLazyCompress({
416
417
  // is where the SessionEnd-hook's failed roll (which restored now.md, D-79) gets
417
418
  // its real bounded retry.
418
419
  maxAttempts: 2,
420
+ // Ceiling-free (detached child, no 60s ceiling) → the generous timeout + the 5s
421
+ // backoff so a retry lands AFTER the slow-Haiku window (D-92/F-2 + D-179; matches
422
+ // daily-distill / weekly-curate). compressSession forwards both to compressWithRetry.
423
+ timeoutMs: CEILING_FREE_TIMEOUT_MS,
424
+ baseBackoffMs: CEILING_FREE_BACKOFF_MS,
419
425
  });
420
426
  } else if (verdict.action === 'stale-weekly') {
421
427
  delegatedTo = 'weekly-curate';
@@ -39,7 +39,7 @@ import { canonicalize } from '@lh8ppl/cmk-canonicalize';
39
39
  import { nowIso } from './audit-log.mjs';
40
40
  import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
41
41
  import { HaikuTimeoutError } from './compressor.mjs';
42
- import { compressWithRetry } from './compress-retry.mjs';
42
+ import { compressWithRetry, CEILING_FREE_TIMEOUT_MS, CEILING_FREE_BACKOFF_MS } from './compress-retry.mjs';
43
43
  import {
44
44
  DEFAULT_COOLDOWN_MS,
45
45
  isCooldownActive,
@@ -344,7 +344,8 @@ export async function weeklyCurate({
344
344
  // corpus (heavier than a session summary) and runs as a cron/lazy child
345
345
  // with no 60s hook ceiling — give the classifier headroom so a large
346
346
  // corpus doesn't time out. (Corpus is byte-capped at PERSONA_CORPUS_BYTES.)
347
- timeoutMs: 120_000,
347
+ // The shared ceiling-free timeout (D-92/F-2; was the original 120s literal).
348
+ timeoutMs: CEILING_FREE_TIMEOUT_MS,
348
349
  });
349
350
  }
350
351
 
@@ -400,9 +401,11 @@ export async function weeklyCurate({
400
401
  instructions,
401
402
  preserveCitationIds: true,
402
403
  maxOutputBytes: archiveMaxBytes,
403
- timeoutMs: 50_000,
404
+ // Ceiling-free → the generous timeout, NOT the hook-sized 50s (D-92/F-2 + D-179).
405
+ timeoutMs: CEILING_FREE_TIMEOUT_MS,
404
406
  },
405
- { maxAttempts: 2, onRetry: () => { retries += 1; } },
407
+ // 5s backoff so a retry lands after the slow-Haiku window, not inside it (D-179).
408
+ { maxAttempts: 2, baseBackoffMs: CEILING_FREE_BACKOFF_MS, onRetry: () => { retries += 1; } },
406
409
  );
407
410
  touchCooldownMarker({ projectRoot, now: ts });
408
411
  } catch (err) {