claude-mem-lite 6.23.2 → 6.24.0

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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.23.2",
12
+ "version": "6.24.0",
13
13
  "source": "./",
14
14
  "homepage": "https://github.com/sdsrss/claude-mem-lite",
15
15
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.23.2",
3
+ "version": "6.24.0",
4
4
  "description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/README.md CHANGED
@@ -189,7 +189,7 @@ cd claude-mem-lite
189
189
  node install.mjs install
190
190
  ```
191
191
 
192
- Source files stay in the cloned repo. Update via `git pull && node install.mjs install`.
192
+ `install` copies the source files to `~/.claude-mem-lite/`, as the npx method does. Update with `node install.mjs update`, which installs the latest signed release. `git pull && node install.mjs install` installs the checked-out branch instead: it can be ahead of the latest release, and no signature is checked.
193
193
 
194
194
  ### What happens during installation
195
195
 
@@ -237,6 +237,27 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
237
237
  repos/ # Shallow-cloned source repos
238
238
  ```
239
239
 
240
+ ## Upgrading to 6.24.0
241
+
242
+ **If you set `CLAUDE_CONFIG_DIR`, the installer, updates and hooks now use that directory, as
243
+ Claude Code does.** No schema-version change: 6.23.3 still opens the database after this
244
+ release has. Without `CLAUDE_CONFIG_DIR` nothing in this item changes.
245
+
246
+ - **Who is affected.** Claude Code keeps `settings.json`, `plugins/` and `.claude.json` in
247
+ `CLAUDE_CONFIG_DIR` when it is set. The plugin kept using `~/.claude` and `~/.claude.json`:
248
+ `install` wrote its hooks where Claude Code never reads them, and with two profiles the
249
+ session-start setup and `self-update` pruned the other profile's plugin cache and edited its
250
+ `.claude.json`.
251
+ - **What to do.** Installed with npm or npx: run `claude-mem-lite install` once more with the
252
+ variable set, so the hooks land in your config directory. Plugin installs need nothing. Hook
253
+ entries an older version wrote into `~/.claude/settings.json` stay there; if no other profile
254
+ uses `~/.claude`, `env -u CLAUDE_CONFIG_DIR claude-mem-lite cleanup-hooks` removes them.
255
+ - **Also in this release:** `npm install -g claude-mem-lite` and `npx` no longer install the
256
+ development toolchain (96 packages / 57 MB instead of 298 / 541 MB). A confirmed
257
+ `maintain execute --ops purge_stale` now deletes every row its preview counted in one run,
258
+ behind one snapshot. `doctor` warns about a failing update check and about an
259
+ `ANTHROPIC_BASE_URL` that ends in `/v1`. Details in CHANGELOG.md.
260
+
240
261
  ## Upgrading to 6.23.1
241
262
 
242
263
  **If you set `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY`, background LLM calls now go to that
@@ -568,9 +589,13 @@ depends on the Claude Code version. Allow the server once in the `settings.json`
568
589
  { "permissions": { "allow": ["mcp__plugin_claude-mem-lite_mem-lite"] } }
569
590
  ```
570
591
 
571
- That is the server's name under a plugin install. An npx or git-clone install registers it as
592
+ For a single run, `claude -p … --allowedTools "mcp__plugin_claude-mem-lite_mem-lite"` does the
593
+ same (both forms checked on 2.1.292, 2026-10-07: no refusal, and `mem_search` answered). That is
594
+ the server's name under a plugin install. An npx or git-clone install registers it as
572
595
  `mem-lite`, so the rule there is `mcp__mem-lite`. An interactive session asks instead; answering
573
- "always allow" there records a rule of its own.
596
+ "always allow" there records a rule of its own. The plugin does not tell the model to fall back
597
+ to the CLI when a call is refused: the refusal is Claude Code's permission check for a tool you
598
+ have not allowed, and routing the same action through Bash would sidestep it.
574
599
 
575
600
  ### Skill Commands (in Claude Code chat)
576
601
 
@@ -1091,7 +1116,7 @@ claude-mem-lite.
1091
1116
  | `CLAUDE_MEM_DIR` | Custom data directory. All databases, runtime files, and managed resources are stored here. | `~/.claude-mem-lite/` |
1092
1117
  | `CLAUDE_MEM_MODEL` | LLM model for background calls (episode extraction, session summaries). Accepts `haiku` or `sonnet`. | `haiku` |
1093
1118
  | `ANTHROPIC_API_KEY` | Anthropic API key. When set, all background LLM calls go directly to the Anthropic Messages API (with prompt caching) - or to the `ANTHROPIC_BASE_URL` gateway when that is set. Highest priority. | _(unset → CLI)_ |
1094
- | `ANTHROPIC_BASE_URL` | Base URL for the direct Messages API when an Anthropic-compatible gateway serves the models (Azure AI Foundry, LiteLLM, Bedrock/Vertex proxies). No `/v1` suffix — the endpoint path is appended. The `claude -p` fallback reads the same variable, so one value covers both transports. Plain `http://` is accepted only for a loopback host; anything else would send the API key unencrypted. A value that is set but unusable skips the direct API leg, so background calls fall back to the `claude -p` CLI, and doctor warns. | `https://api.anthropic.com` |
1119
+ | `ANTHROPIC_BASE_URL` | Base URL for the direct Messages API when an Anthropic-compatible gateway serves the models (Azure AI Foundry, LiteLLM, Bedrock/Vertex proxies). No `/v1` suffix — the endpoint path is appended. The `claude -p` fallback reads the same variable, so one value covers both transports. Plain `http://` is accepted only for a loopback host; anything else would send the API key unencrypted. That request always goes straight to the loopback host, even when `NODE_USE_ENV_PROXY=1` sends other requests through `HTTP_PROXY`. A value that is set but unusable skips the direct API leg, so background calls fall back to the `claude -p` CLI, and doctor warns. | `https://api.anthropic.com` |
1095
1120
  | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Model ID or gateway deployment name for the `haiku` tier, honored on the direct API **only when `ANTHROPIC_BASE_URL` is set**. Without a base URL it stays CLI-only, so the Bedrock/Vertex form of this alias is never sent to `api.anthropic.com`. Use it when the gateway routes on deployment names rather than Anthropic model IDs (Azure Foundry deployments). The `claude -p` fallback resolves its `--model haiku` alias through it too. | built-in `claude-haiku-4-5-…` |
1096
1121
  | `ANTHROPIC_DEFAULT_SONNET_MODEL` | Same as above for the `sonnet` tier. | built-in `claude-sonnet-4-5-…` |
1097
1122
  | `OPENROUTER_API_KEY` | OpenRouter API key (OpenAI-compatible). Used for background LLM calls when `ANTHROPIC_API_KEY` is **not** set. If neither key is set, calls fall back to the `claude -p` CLI. | _(unset)_ |
package/README.zh-CN.md CHANGED
@@ -147,7 +147,7 @@ cd claude-mem-lite
147
147
  node install.mjs install
148
148
  ```
149
149
 
150
- 源文件保留在克隆的仓库中。通过 `git pull && node install.mjs install` 更新。
150
+ `install` 会把源文件复制到 `~/.claude-mem-lite/`,与 npx 方式相同。用 `node install.mjs update` 更新,它安装最新的已签名发布。`git pull && node install.mjs install` 安装的则是当前检出的分支:可能领先于最新发布,也不校验签名。
151
151
 
152
152
  ### 安装过程
153
153
 
@@ -833,7 +833,7 @@ npm run benchmark:gate # CI 门控:指标回退超过 5% 容差时失败
833
833
  | `CLAUDE_MEM_DIR` | 自定义数据目录。所有数据库与运行时文件均存储在此。 | `~/.claude-mem-lite/` |
834
834
  | `CLAUDE_MEM_MODEL` | 后台 LLM 调用模型(Episode 提取、会话总结、调度)。可选 `haiku` 或 `sonnet`。 | `haiku` |
835
835
  | `ANTHROPIC_API_KEY` | Anthropic API key。设置后所有后台 LLM 调用直连 Anthropic Messages API(带 prompt caching);设了 `ANTHROPIC_BASE_URL` 时改发往那个网关。优先级最高。 | _(未设 → CLI)_ |
836
- | `ANTHROPIC_BASE_URL` | 由兼容 Anthropic 的网关(Azure AI Foundry、LiteLLM、Bedrock/Vertex 代理)提供模型时,直连 Messages API 使用的基础 URL。不要带 `/v1` 后缀,端点路径会自动拼上。`claude -p` 兜底读取同一个变量,所以一个值同时作用于两条路径。明文 `http://` 只接受回环主机,否则 API key 会以明文发送。设了但不可用的值会跳过直连这一路,后台调用改走 `claude -p`,doctor 会提示。 | `https://api.anthropic.com` |
836
+ | `ANTHROPIC_BASE_URL` | 由兼容 Anthropic 的网关(Azure AI Foundry、LiteLLM、Bedrock/Vertex 代理)提供模型时,直连 Messages API 使用的基础 URL。不要带 `/v1` 后缀,端点路径会自动拼上。`claude -p` 兜底读取同一个变量,所以一个值同时作用于两条路径。明文 `http://` 只接受回环主机,否则 API key 会以明文发送。这个请求总是直连回环主机,即使 `NODE_USE_ENV_PROXY=1` 让其他请求走 `HTTP_PROXY`。设了但不可用的值会跳过直连这一路,后台调用改走 `claude -p`,doctor 会提示。 | `https://api.anthropic.com` |
837
837
  | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `haiku` 档的模型 ID 或网关部署名,**仅在设了 `ANTHROPIC_BASE_URL` 时**用于直连 API。没有基础 URL 时只给 CLI 用,所以这个别名的 Bedrock/Vertex 写法不会被发往 `api.anthropic.com`。网关按部署名而不是 Anthropic 模型 ID 路由时(Azure Foundry 部署)使用。`claude -p` 兜底的 `--model haiku` 别名也经由它解析。 | 内置 `claude-haiku-4-5-…` |
838
838
  | `ANTHROPIC_DEFAULT_SONNET_MODEL` | 同上,用于 `sonnet` 档。 | 内置 `claude-sonnet-4-5-…` |
839
839
  | `OPENROUTER_API_KEY` | OpenRouter API key(OpenAI 兼容)。当**未设** `ANTHROPIC_API_KEY` 时用于后台 LLM 调用;两者都未设则回退到 `claude -p` CLI。 | _(未设)_ |
package/cli/activity.mjs CHANGED
@@ -188,7 +188,16 @@ export async function cmdActivity(db, args) {
188
188
  return;
189
189
  }
190
190
 
191
- const result = db.prepare(`DELETE FROM events WHERE id IN (${placeholders})`).run(...ids);
191
+ // The opt-in search telemetry keeps a title per returned result; an explicit delete drops the
192
+ // deleted events' titles, as lib/delete-core.mjs does for observations (D#245 N6).
193
+ const result = db.transaction(() => {
194
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'search_results'").get()) {
195
+ db.prepare(
196
+ `UPDATE search_results SET snapshot_label = NULL WHERE source = 'event' AND result_id IN (${placeholders})`,
197
+ ).run(...ids);
198
+ }
199
+ return db.prepare(`DELETE FROM events WHERE id IN (${placeholders})`).run(...ids);
200
+ })();
192
201
  out(`[mem] Deleted ${result.changes} event(s).`);
193
202
  return;
194
203
  }
package/haiku-client.mjs CHANGED
@@ -20,7 +20,8 @@ import { randomUUID } from 'crypto';
20
20
  import { debugLog, debugCatch, parseJsonFromLLM } from './utils.mjs';
21
21
  import { DB_DIR } from './schema.mjs';
22
22
  import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
23
- import { httpConnectProxyFor, postViaConnectProxy } from './lib/proxy-fetch.mjs';
23
+ import { workerDirGone } from './lib/worker-data-dir.mjs';
24
+ import { httpConnectProxyFor, postViaConnectProxy, postDirectHttp } from './lib/proxy-fetch.mjs';
24
25
  import { resolveAnthropicBaseUrl } from './lib/anthropic-base-url.mjs';
25
26
 
26
27
  /**
@@ -40,6 +41,9 @@ import { resolveAnthropicBaseUrl } from './lib/anthropic-base-url.mjs';
40
41
  */
41
42
  function cliSpawnCwd() {
42
43
  const dir = join(resolveRuntimeDir(DB_DIR), 'cli-cwd');
44
+ // A background worker whose data dir was removed during its API attempt must not re-create
45
+ // it here (D#265); the spawn then fails on the missing cwd, and the worker gives up.
46
+ if (workerDirGone(DB_DIR)) return dir;
43
47
  try {
44
48
  mkdirSync(dir, { recursive: true, mode: 0o700 });
45
49
  } catch {
@@ -553,8 +557,20 @@ async function callModelAPI(prompt, model, { timeout, maxTokens, temperature = D
553
557
  'x-api-key': apiKey,
554
558
  'anthropic-version': '2023-06-01',
555
559
  };
560
+ // Plain http is loopback-only (resolveAnthropicBaseUrl), and loopback must stay on this
561
+ // machine: native fetch would hand it, x-api-key in cleartext, to HTTP_PROXY under
562
+ // NODE_USE_ENV_PROXY=1. (D#250) Parsed, not a prefix test: a prefix test let `HTTP://127.0.0.1`
563
+ // through to the proxy when the resolver still returned the raw text (b59e0cec, D#259).
564
+ const plainHttp = new URL(apiUrl).protocol === 'http:';
556
565
  const send = (payload) => {
557
566
  const json = JSON.stringify(payload);
567
+ if (plainHttp) {
568
+ return postDirectHttp(apiUrl, {
569
+ headers: apiHeaders,
570
+ body: json,
571
+ timeout: Math.max(1, deadline - Date.now()),
572
+ });
573
+ }
558
574
  const apiProxy = httpConnectProxyFor(apiUrl);
559
575
  return apiProxy
560
576
  ? postViaConnectProxy(apiProxy, apiUrl, {
package/hook-optimize.mjs CHANGED
@@ -485,7 +485,7 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
485
485
  // A compression keeper is never hidden: hiding it hides every member compressed into it
486
486
  // (the same rule NOT_COMPRESSION_KEEPER_SQL enforces on the maintenance writers). It falls
487
487
  // through to the normal update instead: floored to importance 1 and stamped, so it is not
488
- // re-sent. In narrow scope that update still replaces its title and narrative.
488
+ // re-sent. That update keeps its stored title and narrative in both scopes (keepStoredText).
489
489
  const isKeeper = !!db
490
490
  .prepare('SELECT 1 FROM observations WHERE compressed_into = ? LIMIT 1')
491
491
  .get(cand.id);
@@ -494,12 +494,8 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
494
494
  const humanSet =
495
495
  (db.prepare('SELECT importance_set_at FROM observations WHERE id = ?').get(cand.id)
496
496
  ?.importance_set_at ?? null) !== null;
497
- if (
498
- (parsed.importance === 0 || parsed.importance === '0') &&
499
- scope !== 'wide' &&
500
- !isKeeper &&
501
- !humanSet
502
- ) {
497
+ const scoredZero = parsed.importance === 0 || parsed.importance === '0';
498
+ if (scoredZero && scope !== 'wide' && !isKeeper && !humanSet) {
503
499
  // D#12, and this one is not a stale-write guard — it is a POINTER guard.
504
500
  // `compressed_into` is the child -> keeper link, and COMPRESSED_AUTO is -1. If a
505
501
  // concurrent cluster-merge or smart-compress adopts this row during the 45 s Haiku
@@ -511,7 +507,8 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
511
507
  const res = db
512
508
  .prepare(
513
509
  `UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}, optimized_at = ?
514
- WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL AND ${NOT_COMPRESSION_KEEPER_SQL}`,
510
+ WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL AND ${NOT_COMPRESSION_KEEPER_SQL}
511
+ AND importance_set_at IS NULL`,
515
512
  )
516
513
  .run(Date.now(), cand.id);
517
514
  if (res.changes === 0) {
@@ -561,9 +558,15 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
561
558
  // For `narrow`, the truncate stays on LLM output but NOT on the preserve-on-empty
562
559
  // fallback — truncating the row's own stored narrative to buy nothing was the same
563
560
  // bug in miniature.
561
+ //
562
+ // A reply that scored the row 0 reaches this point only for a row that may not be hidden
563
+ // (a compression keeper, or an importance a person set). It judged the row worthless, so
564
+ // its title and narrative do not replace the stored ones either (D#207: 'Weekly summary:
565
+ // auth refactor' became 'Weekly summary', narrative 'x').
564
566
  const isWide = scope === 'wide';
565
- const title = isWide ? cand.title : truncate(scrubSecrets(parsed.title || ''), 120);
566
- const narrative = isWide
567
+ const keepStoredText = isWide || scoredZero;
568
+ const title = keepStoredText ? cand.title : truncate(scrubSecrets(parsed.title || ''), 120);
569
+ const narrative = keepStoredText
567
570
  ? cand.narrative
568
571
  : parsed.narrative
569
572
  ? truncate(scrubSecrets(parsed.narrative), 500)
@@ -571,9 +574,10 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
571
574
  // Floor at the stored importance: re-enrich adds a lesson, it must never silently downgrade
572
575
  // a user-set/promoted importance (the UPDATE also sets optimized_at → the loss is permanent).
573
576
  // Upgrades are still honored — except over an importance a person set (D10), which stays.
574
- const importance = humanSet
575
- ? cand.importance
576
- : Math.max(clampImportance(parsed.importance), cand.importance || 1);
577
+ // Decided in the UPDATE itself, on the row as it is then: the candidate's importance was read
578
+ // before the model call, and writing it back undid a person's change made during the call
579
+ // (D#206).
580
+ const modelImportance = clampImportance(parsed.importance);
577
581
 
578
582
  const bigramText = cjkBigrams((title || '') + ' ' + (narrative || ''));
579
583
  const textField = [conceptsText, factsText, searchAliases || '', bigramText].filter(Boolean).join(' ');
@@ -607,10 +611,10 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
607
611
  // Narrow replaces title and narrative with model text, so an explicit save it rewrites (one
608
612
  // whose save-time enrich failed) moves to the re-enrich writer's id, as a cluster-merge
609
613
  // keeper does (D#146, D#138). Wide keeps the stored title and narrative (it fills the lesson
610
- // and the side fields), so the row stays an explicit save. The writer's session row is
611
- // best-effort.
614
+ // and the side fields), so the row stays an explicit save, and so does a row whose stored
615
+ // text a 0 reply left in place. The writer's session row is best-effort.
612
616
  let rewriteSessionId = null;
613
- if (!isWide) {
617
+ if (!keepStoredText) {
614
618
  const cur = db.prepare('SELECT memory_session_id FROM observations WHERE id = ?').get(cand.id);
615
619
  if (cur?.memory_session_id?.startsWith(MANUAL_SESSION_ID_PREFIX)) {
616
620
  const enrichSessionId = writerSessionId('enrich-', cand.project);
@@ -629,7 +633,10 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
629
633
  .prepare(
630
634
  `
631
635
  UPDATE observations SET type=?, title=?, narrative=?, concepts=?, facts=?,
632
- text=?, importance=?, lesson_learned=?, search_aliases=?, minhash_sig=?, optimized_at=?,
636
+ text=?,
637
+ importance = CASE WHEN importance_set_at IS NOT NULL THEN importance
638
+ ELSE MAX(?, COALESCE(NULLIF(importance, 0), 1)) END,
639
+ lesson_learned=?, search_aliases=?, minhash_sig=?, optimized_at=?,
633
640
  scope=COALESCE(?, scope), memory_session_id = COALESCE(?, memory_session_id)
634
641
  WHERE id = ? AND ${liveObsFilterSql('')} AND optimized_at IS NULL
635
642
  `,
@@ -641,7 +648,7 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
641
648
  safe.concepts,
642
649
  safe.facts,
643
650
  safe.text,
644
- importance,
651
+ modelImportance,
645
652
  safe.lesson_learned,
646
653
  safe.search_aliases,
647
654
  minhashSig,
@@ -1300,6 +1307,8 @@ Return ONLY valid JSON:
1300
1307
  const bigramText = cjkBigrams((title || '') + ' ' + (narrative || ''));
1301
1308
  const textField = [conceptsText, factsText, bigramText].filter(Boolean).join(' ');
1302
1309
  const minhashSig = computeMinHash((title || '') + ' ' + (narrative || ''));
1310
+ // An importance a person set on the keeper stays (D10); the UPDATE decides that on the row as
1311
+ // it is then, since a person can set it while this call waits on the model (D#198).
1303
1312
  const importance = Math.max(clampImportance(parsed.importance || 2), maxClusterImportance);
1304
1313
 
1305
1314
  // Scrub LLM-output cluster-merge text fields at the UPDATE boundary.
@@ -1382,7 +1391,8 @@ Return ONLY valid JSON:
1382
1391
  db.prepare(
1383
1392
  `
1384
1393
  UPDATE observations SET title=?, narrative=?, concepts=?, facts=?, text=?,
1385
- importance=?, lesson_learned=?, minhash_sig=?, optimized_at=?,
1394
+ importance = CASE WHEN importance_set_at IS NOT NULL THEN importance ELSE ? END,
1395
+ lesson_learned=?, minhash_sig=?, optimized_at=?,
1386
1396
  memory_session_id = COALESCE(?, memory_session_id)
1387
1397
  WHERE id = ?
1388
1398
  `,
package/hook-shared.mjs CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  } from './lib/schema-skew.mjs';
31
31
  import { isDbUnusableError, DB_UNUSABLE_MARKER_PREFIX } from './lib/db-unusable.mjs';
32
32
  import { shouldRecordOnce } from './lib/record-once.mjs';
33
+ import { backgroundWorkerEnv, workerDirGone } from './lib/worker-data-dir.mjs';
33
34
  import { hookSessionId } from './lib/provenance.mjs';
34
35
  import { PROJECT_REKEY_MARKER_PREFIX } from './lib/project-rekey.mjs';
35
36
  // Audit 2026-09-05 P1-2 (carried from 2026-09-02 P2-9): `callLLM`, the quiet/adoption
@@ -378,9 +379,11 @@ export function sweepStaleProjectMarkers(
378
379
  // exists, so harden here too: create 0700, and chmod a pre-existing dir a prior version
379
380
  // created at the default umask. A 0700 dir blocks traversal to every file inside,
380
381
  // current and future, regardless of individual file mode (audit sec P3-2 2026-07-24).
382
+ // A background worker whose data dir was removed while it ran does not re-create it (D#265).
381
383
  try {
382
- if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true, mode: 0o700 });
383
- else chmodSync(RUNTIME_DIR, 0o700);
384
+ if (!existsSync(RUNTIME_DIR)) {
385
+ if (!workerDirGone(DB_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true, mode: 0o700 });
386
+ } else chmodSync(RUNTIME_DIR, 0o700);
384
387
  } catch {}
385
388
 
386
389
  // ─── Session ID Management ───────────────────────────────────────────────────
@@ -604,7 +607,7 @@ export function spawnBackground(bgEvent, ...extraArgs) {
604
607
  const child = spawn(process.execPath, args, {
605
608
  detached: true,
606
609
  stdio: 'ignore',
607
- env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
610
+ env: backgroundWorkerEnv(),
608
611
  });
609
612
  child.on('error', (err) => {
610
613
  debugCatch(err, 'spawnBackground');