@pcircle/memesh 4.1.7 → 4.2.1

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.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.de.md +48 -10
  4. package/README.es.md +82 -17
  5. package/README.fr.md +49 -11
  6. package/README.ja.md +83 -17
  7. package/README.ko.md +86 -20
  8. package/README.md +12 -7
  9. package/README.pt.md +48 -10
  10. package/README.th.md +46 -8
  11. package/README.vi.md +48 -10
  12. package/README.zh-CN.md +83 -17
  13. package/README.zh-TW.md +110 -44
  14. package/dashboard/dist/index.html +9 -9
  15. package/dist/cli/view-live.js +2 -2
  16. package/dist/cli/view.d.ts.map +1 -1
  17. package/dist/cli/view.js +15 -12
  18. package/dist/cli/view.js.map +1 -1
  19. package/dist/core/analytics.d.ts +17 -0
  20. package/dist/core/analytics.d.ts.map +1 -1
  21. package/dist/core/analytics.js +40 -0
  22. package/dist/core/analytics.js.map +1 -1
  23. package/dist/core/auto-tagger.d.ts +7 -2
  24. package/dist/core/auto-tagger.d.ts.map +1 -1
  25. package/dist/core/auto-tagger.js +12 -4
  26. package/dist/core/auto-tagger.js.map +1 -1
  27. package/dist/core/config.d.ts +2 -0
  28. package/dist/core/config.d.ts.map +1 -1
  29. package/dist/core/config.js +19 -11
  30. package/dist/core/config.js.map +1 -1
  31. package/dist/core/consolidator.d.ts.map +1 -1
  32. package/dist/core/consolidator.js +13 -4
  33. package/dist/core/consolidator.js.map +1 -1
  34. package/dist/core/digest-validator.d.ts +18 -0
  35. package/dist/core/digest-validator.d.ts.map +1 -0
  36. package/dist/core/digest-validator.js +79 -0
  37. package/dist/core/digest-validator.js.map +1 -0
  38. package/dist/core/doctor.d.ts.map +1 -1
  39. package/dist/core/doctor.js +28 -11
  40. package/dist/core/doctor.js.map +1 -1
  41. package/dist/core/dreamer.d.ts +8 -1
  42. package/dist/core/dreamer.d.ts.map +1 -1
  43. package/dist/core/dreamer.js +68 -14
  44. package/dist/core/dreamer.js.map +1 -1
  45. package/dist/core/embedder.d.ts.map +1 -1
  46. package/dist/core/embedder.js +2 -2
  47. package/dist/core/embedder.js.map +1 -1
  48. package/dist/core/extractor.d.ts.map +1 -1
  49. package/dist/core/extractor.js +2 -1
  50. package/dist/core/extractor.js.map +1 -1
  51. package/dist/core/failure-analyzer.d.ts +6 -1
  52. package/dist/core/failure-analyzer.d.ts.map +1 -1
  53. package/dist/core/failure-analyzer.js +10 -2
  54. package/dist/core/failure-analyzer.js.map +1 -1
  55. package/dist/core/install-hooks.d.ts.map +1 -1
  56. package/dist/core/install-hooks.js +1 -7
  57. package/dist/core/install-hooks.js.map +1 -1
  58. package/dist/core/install-id.d.ts.map +1 -1
  59. package/dist/core/install-id.js +2 -3
  60. package/dist/core/install-id.js.map +1 -1
  61. package/dist/core/kg-backfill.d.ts +39 -0
  62. package/dist/core/kg-backfill.d.ts.map +1 -0
  63. package/dist/core/kg-backfill.js +362 -0
  64. package/dist/core/kg-backfill.js.map +1 -0
  65. package/dist/core/llm-client.d.ts +13 -0
  66. package/dist/core/llm-client.d.ts.map +1 -1
  67. package/dist/core/llm-client.js +63 -3
  68. package/dist/core/llm-client.js.map +1 -1
  69. package/dist/core/llm-telemetry.d.ts +35 -0
  70. package/dist/core/llm-telemetry.d.ts.map +1 -0
  71. package/dist/core/llm-telemetry.js +96 -0
  72. package/dist/core/llm-telemetry.js.map +1 -0
  73. package/dist/core/llm-validator.d.ts.map +1 -1
  74. package/dist/core/llm-validator.js +3 -3
  75. package/dist/core/llm-validator.js.map +1 -1
  76. package/dist/core/operations.d.ts.map +1 -1
  77. package/dist/core/operations.js +4 -35
  78. package/dist/core/operations.js.map +1 -1
  79. package/dist/core/paths.d.ts +6 -0
  80. package/dist/core/paths.d.ts.map +1 -0
  81. package/dist/core/paths.js +27 -0
  82. package/dist/core/paths.js.map +1 -0
  83. package/dist/core/prompt-safety.d.ts.map +1 -1
  84. package/dist/core/prompt-safety.js.map +1 -1
  85. package/dist/core/scoring.d.ts +5 -0
  86. package/dist/core/scoring.d.ts.map +1 -1
  87. package/dist/core/scoring.js +8 -0
  88. package/dist/core/scoring.js.map +1 -1
  89. package/dist/core/serializer.js +1 -1
  90. package/dist/core/serializer.js.map +1 -1
  91. package/dist/core/skill-usage-log.js +2 -2
  92. package/dist/core/skill-usage-log.js.map +1 -1
  93. package/dist/core/types.d.ts +2 -2
  94. package/dist/core/types.d.ts.map +1 -1
  95. package/dist/core/verifier.d.ts.map +1 -1
  96. package/dist/core/verifier.js +4 -4
  97. package/dist/core/verifier.js.map +1 -1
  98. package/dist/core/version-check.d.ts.map +1 -1
  99. package/dist/core/version-check.js +4 -3
  100. package/dist/core/version-check.js.map +1 -1
  101. package/dist/db.d.ts.map +1 -1
  102. package/dist/db.js +71 -14
  103. package/dist/db.js.map +1 -1
  104. package/dist/knowledge-graph.d.ts +1 -1
  105. package/dist/knowledge-graph.d.ts.map +1 -1
  106. package/dist/knowledge-graph.js +1 -1
  107. package/dist/knowledge-graph.js.map +1 -1
  108. package/dist/skills-manifest.json +16 -16
  109. package/dist/storage/fts-index.js +1 -1
  110. package/dist/storage/fts-index.js.map +1 -1
  111. package/dist/transports/cli/cli.js +128 -6
  112. package/dist/transports/cli/cli.js.map +1 -1
  113. package/dist/transports/http/server.d.ts.map +1 -1
  114. package/dist/transports/http/server.js +204 -25
  115. package/dist/transports/http/server.js.map +1 -1
  116. package/dist/transports/mcp/handlers.d.ts +1 -1
  117. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  118. package/dist/transports/mcp/handlers.js +1 -1
  119. package/dist/transports/mcp/handlers.js.map +1 -1
  120. package/package.json +2 -2
  121. package/scripts/hooks/_shared.js +177 -14
  122. package/scripts/hooks/post-commit.js +50 -8
  123. package/scripts/hooks/pre-bash-orchestration-nudge.js +8 -3
  124. package/scripts/hooks/pre-compact.js +13 -16
  125. package/scripts/hooks/pre-edit-recall.js +28 -13
  126. package/scripts/hooks/session-start.js +194 -184
  127. package/scripts/hooks/session-summary.js +376 -41
  128. package/dist/core/query-expander.d.ts +0 -4
  129. package/dist/core/query-expander.d.ts.map +0 -1
  130. package/dist/core/query-expander.js +0 -53
  131. package/dist/core/query-expander.js.map +0 -1
@@ -4,18 +4,20 @@ import { createRequire } from 'module';
4
4
  import { spawn } from 'child_process';
5
5
  import { createHash } from 'crypto';
6
6
  import { homedir } from 'os';
7
- import { join, basename } from 'path';
7
+ import { join } from 'path';
8
8
  import { pathToFileURL } from 'url';
9
9
  import { existsSync, readFileSync, unlinkSync, rmSync, appendFileSync, chmodSync } from 'fs';
10
10
  import {
11
- buildReferenceContext,
12
11
  ensurePrivateDir,
13
- getMemeshDir,
12
+ getDbPath,
13
+ getMemeshDirFromDbPath,
14
+ getProjectName,
14
15
  isAgenticOrchestrationEnabled,
15
16
  isTrustedForAutoContext,
16
17
  readUpdateCheckCache,
17
18
  resolvePluginRoot,
18
19
  resolveSessionLimit,
20
+ tryRequireBetterSqlite,
19
21
  writePrivateJson,
20
22
  } from './_shared.js';
21
23
 
@@ -38,8 +40,8 @@ try {
38
40
  }
39
41
  } catch { /* best-effort — fall through to 'unknown' channel */ }
40
42
 
41
- const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
42
- const memeshDir = getMemeshDir(process.env);
43
+ const dbPath = getDbPath();
44
+ const memeshDir = getMemeshDirFromDbPath();
43
45
  const throttlePath = join(memeshDir, 'session-recalled-files.json');
44
46
  const nudgeFlagsDir = join(memeshDir, 'agent-nudge-flags');
45
47
 
@@ -178,7 +180,13 @@ function detectInstallChannelHook(pluginRoot) {
178
180
  if (!_installChannelMod) return 'unknown';
179
181
  try {
180
182
  return _installChannelMod.getCurrentInstallChannel({ packageRoot: pluginRoot });
181
- } catch {
183
+ } catch (err) {
184
+ // Falling back to 'unknown' silences the deprecation banner's
185
+ // remediation hint (since it's gated on channel detection). When
186
+ // there's an active security-advisory deprecation, that's a
187
+ // user-visible regression — surface to stderr at least once per
188
+ // process so the reason is visible.
189
+ try { process.stderr.write(`[memesh session-start] install-channel detection: ${err?.message || err}\n`); } catch {}
182
190
  return 'unknown';
183
191
  }
184
192
  }
@@ -342,7 +350,7 @@ process.stdin.on('end', async () => {
342
350
  try {
343
351
  try {
344
352
  const data = JSON.parse(input);
345
- const projectName = basename(data.cwd || process.cwd());
353
+ const projectName = getProjectName(data.cwd);
346
354
 
347
355
  // Clear per-session throttle files from previous session
348
356
  try {
@@ -364,11 +372,16 @@ process.stdin.on('end', async () => {
364
372
  // Combine deprecation banner (if any) into the same
365
373
  // systemMessage so stdout stays a single JSON document. Outer
366
374
  // finally runs runPostBannerUpdateTasks().
367
- output(combineWithBanner('MeMesh: No database found. Memories will be created as you work.'));
375
+ output(combineWithBanner('MeMesh ready · no database yet, memories will be created as you work'));
368
376
  return;
369
377
  }
370
378
 
371
- const Database = require('better-sqlite3');
379
+ // Native module unavailable (typical for plugin-marketplace cache
380
+ // installs that ship without node_modules). Silently skip — the
381
+ // plugin's own MCP server runs via npx and a sibling registered
382
+ // copy of this hook (npm-global / dev path) supplies the summary.
383
+ const Database = tryRequireBetterSqlite();
384
+ if (!Database) return;
372
385
  const db = new Database(dbPath, { readonly: true });
373
386
  try {
374
387
  db.pragma('journal_mode = WAL');
@@ -378,7 +391,7 @@ process.stdin.on('end', async () => {
378
391
  "SELECT name FROM sqlite_master WHERE type='table' AND name='entities'"
379
392
  ).get();
380
393
  if (!tableCheck) {
381
- output(combineWithBanner('MeMesh: Database exists but no memories stored yet.'));
394
+ output(combineWithBanner('MeMesh ready · database initialised but no memories stored yet'));
382
395
  return;
383
396
  }
384
397
 
@@ -390,190 +403,164 @@ process.stdin.on('end', async () => {
390
403
  const hasScoringCols = colNames.has('access_count') && colNames.has('last_accessed_at') && colNames.has('confidence');
391
404
 
392
405
  const statusFilter = hasStatus ? "AND e.status = 'active'" : '';
393
- const recentStatusFilter = hasStatus ? "WHERE status = 'active'" : '';
406
+ // (Note: a previous `recentStatusFilter` constant lived here; the
407
+ // refactor that introduced `buildScoringQuery` aliased the table
408
+ // as `e` for both the project- and recent-pool queries, so the
409
+ // bare-column `WHERE status = 'active'` form was replaced by the
410
+ // qualified `WHERE e.status = 'active'` computed inline below.)
394
411
 
395
412
  // Configurable limit: how many top-N entities to load per section.
396
413
  // Env > config.sessionLimit > default 10.
397
414
  const sessionLimit = resolveSessionLimit(process.env);
398
415
 
399
- // Build scoring ORDER BY clause (or fallback to insertion order)
400
- const scoringOrderBy = hasScoringCols
401
- ? `ORDER BY
402
- CASE WHEN e.confidence IS NULL THEN 0.5 ELSE e.confidence END * 0.4
416
+ // Scoring math is aligned to src/core/scoring.ts exactly:
417
+ // - confidence weight 0.2833 (core 0.17 / 0.60 sub-total)
418
+ // - frequency weight 0.3000 (core 0.18 / 0.60)
419
+ // - recency weight 0.4167 (core 0.25 / 0.60)
420
+ // Sub-total excludes searchRelevance + impact, which session-start
421
+ // can't compute without an FTS query. The renormalised ratios are
422
+ // exported from core/scoring.ts as `SESSION_START_WEIGHT_RATIO`;
423
+ // a drift-guard test in tests/core/scoring.test.ts asserts the SQL
424
+ // here stays in sync.
425
+ //
426
+ // Functions:
427
+ // - frequency: log(c+1) / log(max(maxAccess,1) + 1) (matches frequencyScore)
428
+ // - recency: exp(-(now - lastAccessed_days) / 30) (matches recencyScore)
429
+ // SQLite >= 3.35 with -DSQLITE_ENABLE_MATH_FUNCTIONS provides exp/log;
430
+ // better-sqlite3 v8+ ships with this flag enabled by default. We probe
431
+ // once per process and fall back to the legacy linear/rational forms
432
+ // if a stripped-down build is detected, so ranking degrades gracefully
433
+ // rather than throwing.
434
+ // Test-only seam: force the legacy linear/rational fallback so the
435
+ // pre-math-functions code path is reachable in CI on builds where
436
+ // exp/log ARE available. Production callers never set this.
437
+ let hasSqliteMath = false;
438
+ if (process.env.MEMESH_TEST_FORCE_LEGACY_SCORING_SQL !== '1') {
439
+ try {
440
+ db.prepare('SELECT exp(1.0), log(2.0)').get();
441
+ hasSqliteMath = true;
442
+ } catch {
443
+ // Legacy SQLite build without math functions — keep linear fallback.
444
+ }
445
+ }
446
+
447
+ // The legacy schema (createTestDb in tests, plus very old installs)
448
+ // doesn't have confidence/access_count/last_accessed_at, so the
449
+ // SELECT can't reference them. Build the column list to match
450
+ // what the schema actually supports.
451
+ const baseCols = 'e.id, e.name, e.type, e.created_at, e.metadata';
452
+ const scoringCols = hasScoringCols
453
+ ? `, e.confidence, e.access_count, e.last_accessed_at`
454
+ : '';
455
+
456
+ const buildScoringQuery = (joinClause, whereClause) => {
457
+ const poolSelect = `SELECT DISTINCT ${baseCols}${scoringCols} FROM entities e ${joinClause}`;
458
+ if (!hasScoringCols) {
459
+ return `${poolSelect} ${whereClause} ORDER BY e.id DESC LIMIT ?`;
460
+ }
461
+ if (hasSqliteMath) {
462
+ return `WITH pool AS (
463
+ ${poolSelect} ${whereClause}
464
+ ),
465
+ pool_stats AS (
466
+ SELECT COALESCE(MAX(access_count), 0) AS max_access FROM pool
467
+ )
468
+ SELECT p.id, p.name, p.type, p.created_at, p.metadata
469
+ FROM pool p, pool_stats s
470
+ ORDER BY
471
+ COALESCE(p.confidence, 1.0) * 0.2833
472
+ + (CASE WHEN s.max_access <= 0 THEN 0
473
+ ELSE log(COALESCE(p.access_count, 0) + 1) / log(max(s.max_access, 1) + 1) END) * 0.3000
474
+ + (CASE WHEN p.last_accessed_at IS NULL THEN 0.5
475
+ ELSE exp(-(julianday('now') - julianday(p.last_accessed_at)) / 30.0) END) * 0.4167
476
+ DESC
477
+ LIMIT ?`;
478
+ }
479
+ // Legacy fallback (SQLite without math functions): linear cap + rational decay.
480
+ // Same direction as core ranking; absolute scores differ slightly.
481
+ return `${poolSelect} ${whereClause}
482
+ ORDER BY
483
+ COALESCE(e.confidence, 1.0) * 0.2833
403
484
  + CASE WHEN e.access_count IS NULL THEN 0
404
- ELSE MIN(CAST(e.access_count AS REAL) / 50.0, 1.0) END * 0.3
405
- + CASE WHEN e.last_accessed_at IS NULL THEN 0.3
406
- ELSE MIN(1.0, 1.0 / (1.0 + (julianday('now') - julianday(e.last_accessed_at)) / 30.0)) END * 0.3
407
- DESC`
408
- : 'ORDER BY e.id DESC';
409
-
410
- const recentScoringOrderBy = hasScoringCols
411
- ? `ORDER BY
412
- CASE WHEN confidence IS NULL THEN 0.5 ELSE confidence END * 0.4
413
- + CASE WHEN access_count IS NULL THEN 0
414
- ELSE MIN(CAST(access_count AS REAL) / 50.0, 1.0) END * 0.3
415
- + CASE WHEN last_accessed_at IS NULL THEN 0.3
416
- ELSE MIN(1.0, 1.0 / (1.0 + (julianday('now') - julianday(last_accessed_at)) / 30.0)) END * 0.3
417
- DESC`
418
- : 'ORDER BY id DESC';
419
-
420
- // Query project-specific top-N entities by relevance score
485
+ ELSE MIN(CAST(e.access_count AS REAL) / 50.0, 1.0) END * 0.3000
486
+ + CASE WHEN e.last_accessed_at IS NULL THEN 0.5
487
+ ELSE MIN(1.0, 1.0 / (1.0 + (julianday('now') - julianday(e.last_accessed_at)) / 30.0)) END * 0.4167
488
+ DESC
489
+ LIMIT ?`;
490
+ };
491
+
421
492
  const projectTag = `project:${projectName}`;
422
- const projectEntities = db.prepare(`
423
- SELECT DISTINCT e.id, e.name, e.type, e.created_at, e.metadata
424
- FROM entities e
425
- JOIN tags t ON t.entity_id = e.id
426
- WHERE t.tag = ?
427
- ${statusFilter}
428
- ${scoringOrderBy}
429
- LIMIT ?
430
- `).all(projectTag, sessionLimit * 3)
493
+ const projectQuery = buildScoringQuery(
494
+ `JOIN tags t ON t.entity_id = e.id`,
495
+ `WHERE t.tag = ? ${statusFilter}`,
496
+ );
497
+ const projectEntities = db.prepare(projectQuery).all(projectTag, sessionLimit * 3)
431
498
  .filter(entity => isTrustedForAutoContext(entity.metadata))
432
499
  .slice(0, sessionLimit);
433
500
 
434
- // Fetch the first observation for each entity (for concise summary)
435
- const getFirstObservation = db.prepare(
436
- 'SELECT content FROM observations WHERE entity_id = ? ORDER BY id ASC LIMIT 1'
437
- );
438
-
439
- // Query global recent/top entities (exclude project-tagged ones for this project)
440
- const recentEntities = db.prepare(`
441
- SELECT id, name, type, created_at, metadata
442
- FROM entities
443
- ${recentStatusFilter}
444
- ${recentScoringOrderBy}
445
- LIMIT 15
446
- `).all()
501
+ // recentStatusFilter is "WHERE status = 'active'" or "" the bare-column
502
+ // form is fine when there's no JOIN, but we now alias the table as `e`,
503
+ // so rewrite to e.status for consistency.
504
+ const recentWhere = hasStatus ? "WHERE e.status = 'active'" : '';
505
+ const recentQuery = buildScoringQuery('', recentWhere);
506
+ const recentEntities = db.prepare(recentQuery).all(15)
447
507
  .filter(entity => isTrustedForAutoContext(entity.metadata))
448
508
  .slice(0, 5);
449
509
 
450
- // Format entity as concise bullet: "• name (type): first observation (truncated)"
451
- function formatEntity(entity) {
452
- const obs = getFirstObservation.get(entity.id);
453
- const snippet = obs ? obs.content.slice(0, 100) : '';
454
- return snippet
455
- ? `• ${entity.name} (${entity.type}): ${snippet}`
456
- : `• ${entity.name} (${entity.type})`;
457
- }
458
-
459
- // Build recall message
460
- const lines = [];
461
- if (projectEntities.length > 0) {
462
- const label = hasScoringCols ? `top ${projectEntities.length} by relevance` : `${projectEntities.length}`;
463
- lines.push(`Project "${projectName}" memories (${label}):`);
464
- for (const e of projectEntities) {
465
- lines.push(formatEntity(e));
466
- }
467
- }
468
- if (recentEntities.length > 0) {
469
- if (lines.length > 0) lines.push('');
470
- lines.push('Recent memories:');
471
- for (const e of recentEntities) {
472
- lines.push(formatEntity(e));
473
- }
474
- }
475
-
476
- // No memories at all — surface only the deprecation banner if
477
- // active (so a flagged install still warns the user) and skip
478
- // the rest of the recall-summary work. The outer finally still
479
- // runs runPostBannerUpdateTasks().
480
- if (lines.length === 0) {
481
- const bannerOnly = combineWithBanner('');
482
- if (bannerOnly && bannerOnly.trim().length > 0) {
483
- output(bannerOnly.trim());
484
- }
485
- return;
486
- }
487
-
488
- let memorySummary = lines.join('\n');
489
-
490
- // --- Proactive lesson warnings ---
510
+ // Lesson count (queried for summary, not listed individually).
511
+ // Status-column gate matches the project/recent queries above —
512
+ // legacy v2.11 schemas don't have e.status and would otherwise
513
+ // throw `no such column: status`, hiding lessons from session-start
514
+ // auto-context indefinitely.
515
+ let lessonCount = 0;
491
516
  try {
492
- const lessonEntities = db.prepare(`
493
- SELECT DISTINCT e.id, e.name, e.confidence, e.metadata
517
+ const lessonRows = db.prepare(`
518
+ SELECT DISTINCT e.id, e.metadata
494
519
  FROM entities e
495
520
  JOIN tags t ON t.entity_id = e.id
496
521
  WHERE e.type = 'lesson_learned'
497
- AND e.status = 'active'
522
+ ${hasStatus ? "AND e.status = 'active'" : ''}
498
523
  AND t.tag = ?
499
- ORDER BY CASE WHEN e.confidence IS NULL THEN 0.5 ELSE e.confidence END DESC,
500
- CASE WHEN e.access_count IS NULL THEN 0 ELSE e.access_count END DESC
501
- LIMIT 15
524
+ LIMIT 50
502
525
  `).all(projectTag).filter(entity => isTrustedForAutoContext(entity.metadata));
503
-
504
- if (lessonEntities.length > 0) {
505
- memorySummary += '\n\n⚠️ Known lessons for this project:\n';
506
- for (const lesson of lessonEntities) {
507
- // Load ALL observations per lesson (not fragile LIKE pattern)
508
- const allObs = db.prepare(
509
- 'SELECT content FROM observations WHERE entity_id = ? ORDER BY id'
510
- ).all(lesson.id);
511
-
512
- // Find the Prevention line
513
- const prevention = allObs.find(o => o.content.startsWith('Prevention:'));
514
- const display = prevention
515
- ? prevention.content.replace(/^Prevention:\s*/, '')
516
- : (allObs[allObs.length - 1]?.content || lesson.name);
517
-
518
- const conf = typeof lesson.confidence === 'number' ? lesson.confidence.toFixed(1) : '1.0';
519
- memorySummary += `• ${display} (confidence: ${conf})\n`;
520
- }
521
- }
522
- } catch {
523
- // Lesson query failed — don't break session start
526
+ lessonCount = lessonRows.length;
527
+ } catch (err) {
528
+ // Real query bug (typo, missing column on a schema older than v2.11)
529
+ // surface to stderr so a maintainer sees it on next session.
530
+ try { process.stderr.write(`[memesh session-start] lesson query: ${err?.message || err}\n`); } catch {}
524
531
  }
525
532
 
526
- // --- Agentic-orchestration mode banner (experimental protocol, opt-in) ---
527
- // memesh ships an experimental working-model protocol alongside its
528
- // memory layer. The banner reminds Claude at session start that the
529
- // suggested default for verifiable work (build/test/lint/migrate/
530
- // refactor/benchmark) is to dispatch a background agent rather than
531
- // block the conversation; strategic work stays foreground.
532
- //
533
- // OPT-IN ONLY: enabled via MEMESH_ENABLE_AGENTIC_ORCHESTRATION=1.
534
- // The default is OFF — main wedge of memesh is local memory; the
535
- // working-model protocol is a separable experiment, and its banner
536
- // would otherwise dominate every session for users who never asked
537
- // for it. Setting the flag also enables local skill-usage telemetry
538
- // (~/.memesh/skill-usage.jsonl) so the protocol can later be
539
- // validated with real usage data.
540
- if (isAgenticOrchestrationEnabled(process.env)) {
541
- try {
542
- memorySummary +=
543
- '\n\n[Experimental working model — protocol; effectiveness still being validated] ' +
544
- 'User=CTO · Claude=Orchestrator · Agents=Engineering team\n' +
545
- 'Verifiable work (build/test/lint/refactor/benchmark) dispatch as background agent (Task with run_in_background:true).\n' +
546
- 'Strategic work → stay foreground. Skill: agentic-orchestration.';
547
-
548
- // Local-only telemetry — never networked. Hook writes the JSONL
549
- // line directly to keep itself self-contained (no dynamic import
550
- // of compiled TS). Only fires when the user has opted in above.
551
- try {
552
- const usagePath = join(homedir(), '.memesh', 'skill-usage.jsonl');
553
- // Hash the cwd so distinct-project counting still works without
554
- // persisting any path fragment. SHA-256 → first 16 hex chars =
555
- // 64 bits, plenty to distinguish projects on one machine.
556
- const cwd = String(data?.cwd || process.cwd());
557
- const cwdHashed = createHash('sha256').update(cwd).digest('hex').slice(0, 16);
558
- const line = JSON.stringify({
559
- ts: new Date().toISOString(),
560
- event: 'agentic_orchestration_banner_injected',
561
- payload: { cwd_hashed: cwdHashed },
562
- }) + '\n';
563
- appendFileSync(usagePath, line);
564
- // Tighten mode — telemetry includes timestamps + per-project
565
- // hashed cwd which can profile user activity. Other local
566
- // users on a shared system should not be able to read it.
567
- try { chmodSync(usagePath, 0o600); } catch { /* non-POSIX */ }
568
- } catch { /* swallow — telemetry must not break session-start */ }
569
- } catch {
570
- // Banner failed — non-critical, continue
533
+ // Build single-line summary with mid-dot separators. Earlier this
534
+ // was a multi-line tree (├─ / └─); switched to a one-liner so the
535
+ // SessionStart system message takes one row in the Claude Code
536
+ // log instead of four. Counts are count-only — no entity bullets.
537
+ const projectCount = projectEntities.length;
538
+ const recentCount = recentEntities.length;
539
+ const memoryFragments = [];
540
+ if (projectCount > 0) memoryFragments.push(`${projectCount} project`);
541
+ if (recentCount > 0) memoryFragments.push(`${recentCount} recent`);
542
+
543
+ let summary;
544
+ if (memoryFragments.length === 0 && lessonCount === 0) {
545
+ summary = `◉ MeMesh ready · no memories for "${projectName}" yet`;
546
+ } else {
547
+ const parts = ['◉ MeMesh'];
548
+ if (memoryFragments.length > 0) {
549
+ parts.push(`${memoryFragments.join(' + ')} memories`);
550
+ }
551
+ if (lessonCount > 0) {
552
+ parts.push(`${lessonCount} active lesson${lessonCount === 1 ? '' : 's'}`);
571
553
  }
554
+ summary = parts.join(' · ');
572
555
  }
573
556
 
574
557
  // --- Record injected entity IDs for recall effectiveness tracking ---
558
+ // The hit/miss tracker excludes entity names found in `injectedContext`
559
+ // from the "user referenced this memory" signal. With the new
560
+ // count-only summary we no longer surface names, so set the field to
561
+ // a sentinel so substring matching is a no-op (any entity name is a
562
+ // genuine hit).
575
563
  try {
576
- // CRITICAL: Deduplicate by entity ID (entity may appear in both project and recent lists)
577
564
  const seenIds = new Set();
578
565
  const allInjected = [...projectEntities, ...recentEntities].filter(e => {
579
566
  if (seenIds.has(e.id)) return false;
@@ -585,7 +572,6 @@ process.stdin.on('end', async () => {
585
572
  const sessionsDir = join(memeshDir, 'sessions');
586
573
  ensurePrivateDir(sessionsDir);
587
574
 
588
- // FIX: Use session-scoped file with unique ID (pid + timestamp)
589
575
  const sessionId = `${process.pid}-${Date.now()}`;
590
576
  writePrivateJson(
591
577
  join(sessionsDir, `${sessionId}.json`),
@@ -594,8 +580,7 @@ process.stdin.on('end', async () => {
594
580
  project: projectName,
595
581
  entityIds: allInjected.map(e => e.id),
596
582
  entityNames: allInjected.map(e => e.name),
597
- // FIX: Save injected context text to exclude from hit detection
598
- injectedContext: memorySummary,
583
+ injectedContext: summary,
599
584
  }
600
585
  );
601
586
 
@@ -613,15 +598,35 @@ process.stdin.on('end', async () => {
613
598
  }
614
599
  } catch {}
615
600
  }
616
- } catch {
617
- // Non-critical — don't break session start
601
+ } catch (err) {
602
+ // Non-critical — sessions-file write powers recall-effectiveness
603
+ // tracking (recall_hits / recall_misses on the dashboard). If it
604
+ // silently breaks, the impact-score factor in core/scoring.ts
605
+ // converges on 0.5 (neutral) for everything. Stderr trace so a
606
+ // permission/serialisation regression is visible.
607
+ try { process.stderr.write(`[memesh session-start] sessions-write: ${err?.message || err}\n`); } catch {}
608
+ }
609
+
610
+ // --- Agentic-orchestration mode (opt-in) ---
611
+ // Banner kept short. Telemetry write preserved for protocol validation.
612
+ if (isAgenticOrchestrationEnabled(process.env)) {
613
+ summary += '\n[AO opt-in: dispatch verifiable work as background agent · skill: agentic-orchestration]';
614
+ try {
615
+ const usagePath = join(homedir(), '.memesh', 'skill-usage.jsonl');
616
+ const cwd = String(data?.cwd || process.cwd());
617
+ const cwdHashed = createHash('sha256').update(cwd).digest('hex').slice(0, 16);
618
+ const line = JSON.stringify({
619
+ ts: new Date().toISOString(),
620
+ event: 'agentic_orchestration_banner_injected',
621
+ payload: { cwd_hashed: cwdHashed },
622
+ }) + '\n';
623
+ appendFileSync(usagePath, line);
624
+ try { chmodSync(usagePath, 0o600); } catch { /* non-POSIX */ }
625
+ } catch { /* swallow — telemetry must not break session-start */ }
618
626
  }
619
627
 
620
- // Deprecation-aware banner. Reads the cache produced by the
621
- // last `getUpdateCheck` (CLI or background refresh). When the
622
- // installed version was flagged by maintainers (typically a
623
- // security advisory), prepend a strong warning so the user sees
624
- // it on every session start until they upgrade.
628
+ // Deprecation banner (security advisory) surfaced even with the
629
+ // short summary so flagged installs still warn on every session.
625
630
  let installedVersion = null;
626
631
  try {
627
632
  const pluginRoot = resolvePluginRoot(import.meta.url);
@@ -634,11 +639,11 @@ process.stdin.on('end', async () => {
634
639
  const deprecationLines = installedVersion
635
640
  ? buildDeprecationBanner(installedVersion, updateCache)
636
641
  : [];
637
- const memorySummaryWithBanner = deprecationLines.length > 0
638
- ? [...deprecationLines, '', ...memorySummary.split('\n')].join('\n')
639
- : memorySummary;
642
+ const finalMessage = deprecationLines.length > 0
643
+ ? [...deprecationLines.filter(l => l.length > 0), '', summary].join('\n')
644
+ : summary;
640
645
 
641
- output(buildReferenceContext(memorySummaryWithBanner.split('\n')));
646
+ output(finalMessage);
642
647
  } finally {
643
648
  db.close();
644
649
  }
@@ -657,8 +662,13 @@ process.stdin.on('end', async () => {
657
662
  } finally {
658
663
  dbMod.closeDatabase();
659
664
  }
660
- } catch {
661
- // Non-critical — noise compression failed, will retry next session
665
+ } catch (err) {
666
+ // Non-critical — noise compression failed, will retry next session.
667
+ // Trace because this catch previously hid an off-by-one regression
668
+ // in resolvePluginRoot (4.0.4-4.1.0) that silently disabled both
669
+ // noise compression AND LLM failure analysis for three minor
670
+ // releases. A one-line stderr would have surfaced it on day 1.
671
+ try { process.stderr.write(`[memesh session-start] noise-compression: ${err?.message || err}\n`); } catch {}
662
672
  }
663
673
 
664
674
  } catch (err) {