@soleri/core 9.14.4 → 9.15.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.
Files changed (89) hide show
  1. package/dist/brain/brain.d.ts +9 -0
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +11 -1
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts.map +1 -1
  6. package/dist/brain/intelligence.js +24 -0
  7. package/dist/brain/intelligence.js.map +1 -1
  8. package/dist/brain/types.d.ts +1 -0
  9. package/dist/brain/types.d.ts.map +1 -1
  10. package/dist/chat/chat-session.d.ts +6 -0
  11. package/dist/chat/chat-session.d.ts.map +1 -1
  12. package/dist/chat/chat-session.js +68 -17
  13. package/dist/chat/chat-session.js.map +1 -1
  14. package/dist/curator/curator.d.ts +6 -0
  15. package/dist/curator/curator.d.ts.map +1 -1
  16. package/dist/curator/curator.js +138 -0
  17. package/dist/curator/curator.js.map +1 -1
  18. package/dist/curator/types.d.ts +10 -0
  19. package/dist/curator/types.d.ts.map +1 -1
  20. package/dist/engine/bin/soleri-engine.js +0 -0
  21. package/dist/flows/types.d.ts +16 -16
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/intake/content-classifier.d.ts +10 -4
  27. package/dist/intake/content-classifier.d.ts.map +1 -1
  28. package/dist/intake/content-classifier.js +19 -5
  29. package/dist/intake/content-classifier.js.map +1 -1
  30. package/dist/intake/text-ingester.d.ts +18 -0
  31. package/dist/intake/text-ingester.d.ts.map +1 -1
  32. package/dist/intake/text-ingester.js +37 -13
  33. package/dist/intake/text-ingester.js.map +1 -1
  34. package/dist/planning/planner.d.ts +3 -0
  35. package/dist/planning/planner.d.ts.map +1 -1
  36. package/dist/planning/planner.js +43 -4
  37. package/dist/planning/planner.js.map +1 -1
  38. package/dist/plugins/types.d.ts +2 -2
  39. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  40. package/dist/runtime/admin-setup-ops.js +59 -20
  41. package/dist/runtime/admin-setup-ops.js.map +1 -1
  42. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  43. package/dist/runtime/facades/orchestrate-facade.js +28 -1
  44. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  45. package/dist/runtime/runtime.d.ts.map +1 -1
  46. package/dist/runtime/runtime.js +16 -0
  47. package/dist/runtime/runtime.js.map +1 -1
  48. package/dist/runtime/types.d.ts +19 -0
  49. package/dist/runtime/types.d.ts.map +1 -1
  50. package/dist/skills/validate-skills.d.ts +32 -0
  51. package/dist/skills/validate-skills.d.ts.map +1 -0
  52. package/dist/skills/validate-skills.js +396 -0
  53. package/dist/skills/validate-skills.js.map +1 -0
  54. package/dist/vault/default-canonical-tags.d.ts +15 -0
  55. package/dist/vault/default-canonical-tags.d.ts.map +1 -0
  56. package/dist/vault/default-canonical-tags.js +65 -0
  57. package/dist/vault/default-canonical-tags.js.map +1 -0
  58. package/dist/vault/tag-normalizer.d.ts +42 -0
  59. package/dist/vault/tag-normalizer.d.ts.map +1 -0
  60. package/dist/vault/tag-normalizer.js +157 -0
  61. package/dist/vault/tag-normalizer.js.map +1 -0
  62. package/package.json +5 -1
  63. package/src/__tests__/embeddings.test.ts +3 -3
  64. package/src/brain/brain.ts +25 -1
  65. package/src/brain/intelligence.ts +25 -0
  66. package/src/brain/types.ts +1 -0
  67. package/src/chat/chat-session.ts +75 -17
  68. package/src/chat/chat-transport.test.ts +31 -1
  69. package/src/curator/curator.ts +180 -0
  70. package/src/curator/types.ts +10 -0
  71. package/src/index.ts +7 -0
  72. package/src/intake/content-classifier.ts +22 -4
  73. package/src/intake/text-ingester.ts +61 -12
  74. package/src/planning/planner.test.ts +86 -90
  75. package/src/planning/planner.ts +48 -4
  76. package/src/runtime/admin-setup-ops.test.ts +44 -0
  77. package/src/runtime/admin-setup-ops.ts +59 -20
  78. package/src/runtime/facades/orchestrate-facade.ts +27 -1
  79. package/src/runtime/runtime.ts +18 -0
  80. package/src/runtime/types.ts +19 -0
  81. package/src/skills/validate-skills.test.ts +205 -0
  82. package/src/skills/validate-skills.ts +470 -0
  83. package/src/vault/default-canonical-tags.ts +64 -0
  84. package/src/vault/tag-normalizer.test.ts +214 -0
  85. package/src/vault/tag-normalizer.ts +188 -0
  86. package/dist/embeddings/index.d.ts +0 -5
  87. package/dist/embeddings/index.d.ts.map +0 -1
  88. package/dist/embeddings/index.js +0 -3
  89. package/dist/embeddings/index.js.map +0 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Default canonical tag taxonomy for Soleri agents.
3
+ *
4
+ * These tags represent the most common knowledge domains. When a vault is
5
+ * configured with tagConstraintMode 'suggest' or 'enforce', incoming tags
6
+ * are mapped to the nearest entry in this list via edit-distance matching.
7
+ *
8
+ * To use these defaults in your agent runtime config:
9
+ * import { DEFAULT_CANONICAL_TAGS } from '@soleri/core';
10
+ * // ...
11
+ * canonicalTags: DEFAULT_CANONICAL_TAGS,
12
+ * tagConstraintMode: 'suggest',
13
+ */
14
+ export const DEFAULT_CANONICAL_TAGS = [
15
+ 'architecture',
16
+ 'typescript',
17
+ 'react',
18
+ 'testing',
19
+ 'workflow',
20
+ 'design-tokens',
21
+ 'accessibility',
22
+ 'performance',
23
+ 'security',
24
+ 'planning',
25
+ 'soleri',
26
+ 'vault',
27
+ 'mcp',
28
+ 'claude-code',
29
+ 'ai',
30
+ 'learning',
31
+ 'gamification',
32
+ 'education',
33
+ 'adhd',
34
+ 'routing',
35
+ 'orchestration',
36
+ 'skills',
37
+ 'automation',
38
+ 'git',
39
+ 'database',
40
+ 'api',
41
+ 'authentication',
42
+ 'subagent',
43
+ 'design-system',
44
+ 'component',
45
+ 'frontend',
46
+ 'backend',
47
+ 'tooling',
48
+ 'monorepo',
49
+ 'refactoring',
50
+ 'debugging',
51
+ 'deployment',
52
+ 'configuration',
53
+ 'documentation',
54
+ 'pattern',
55
+ 'anti-pattern',
56
+ 'principle',
57
+ 'decision',
58
+ 'migration',
59
+ 'plugin',
60
+ 'hook',
61
+ 'schema',
62
+ 'pipeline',
63
+ 'ingestion',
64
+ ];
65
+ //# sourceMappingURL=default-canonical-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-canonical-tags.js","sourceRoot":"","sources":["../../src/vault/default-canonical-tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAa;IAC9C,cAAc;IACd,YAAY;IACZ,OAAO;IACP,SAAS;IACT,UAAU;IACV,eAAe;IACf,eAAe;IACf,aAAa;IACb,UAAU;IACV,UAAU;IACV,QAAQ;IACR,OAAO;IACP,KAAK;IACL,aAAa;IACb,IAAI;IACJ,UAAU;IACV,cAAc;IACd,WAAW;IACX,MAAM;IACN,SAAS;IACT,eAAe;IACf,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,UAAU;IACV,KAAK;IACL,gBAAgB;IAChB,UAAU;IACV,eAAe;IACf,WAAW;IACX,UAAU;IACV,SAAS;IACT,SAAS;IACT,UAAU;IACV,aAAa;IACb,WAAW;IACX,YAAY;IACZ,eAAe;IACf,eAAe;IACf,SAAS;IACT,cAAc;IACd,WAAW;IACX,UAAU;IACV,WAAW;IACX,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,UAAU;IACV,WAAW;CACZ,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tag Normalizer — canonical tag taxonomy enforcement.
3
+ *
4
+ * Maps raw tags to nearest canonical via Levenshtein edit-distance.
5
+ * Strips noise words (version strings, single generic words).
6
+ * Respects metadata tag prefixes (e.g. 'source:').
7
+ *
8
+ * Three modes:
9
+ * - 'enforce': must match within edit-distance 3, else drop (return null)
10
+ * - 'suggest': map to nearest canonical within edit-distance 2 (passthrough if no match)
11
+ * - 'off': no normalization — return tag as-is
12
+ */
13
+ /**
14
+ * Compute Levenshtein edit distance between two strings.
15
+ * O(m*n) time, O(n) space using two-row DP.
16
+ */
17
+ export declare function computeEditDistance(a: string, b: string): number;
18
+ /**
19
+ * Returns true if the tag starts with any of the given prefixes.
20
+ * Metadata tags (e.g. 'source:article') are exempt from canonical normalization.
21
+ */
22
+ export declare function isMetadataTag(tag: string, prefixes: string[]): boolean;
23
+ /**
24
+ * Normalize a single tag against a canonical list.
25
+ *
26
+ * @param tag - Raw tag to normalize
27
+ * @param canonical - Canonical tag list to map against
28
+ * @param mode - Constraint mode: 'enforce' | 'suggest' | 'off'
29
+ * @returns Normalized tag string, or null if the tag should be dropped.
30
+ */
31
+ export declare function normalizeTag(tag: string, canonical: string[], mode: 'enforce' | 'suggest' | 'off'): string | null;
32
+ /**
33
+ * Normalize a batch of tags against a canonical list.
34
+ * Filters out nulls (dropped tags). Deduplicates the result.
35
+ *
36
+ * @param tags - Raw tags
37
+ * @param canonical - Canonical tag list
38
+ * @param mode - Constraint mode
39
+ * @param metadataPrefixes - Tags with these prefixes bypass normalization
40
+ */
41
+ export declare function normalizeTags(tags: string[], canonical: string[], mode: 'enforce' | 'suggest' | 'off', metadataPrefixes?: string[]): string[];
42
+ //# sourceMappingURL=tag-normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-normalizer.d.ts","sourceRoot":"","sources":["../../src/vault/tag-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqCH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAsBhE;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAEtE;AAID;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,EACnB,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,KAAK,GAClC,MAAM,GAAG,IAAI,CA6Cf;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,EAAE,EACnB,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,KAAK,EACnC,gBAAgB,GAAE,MAAM,EAAgB,GACvC,MAAM,EAAE,CAwBV"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tag Normalizer — canonical tag taxonomy enforcement.
3
+ *
4
+ * Maps raw tags to nearest canonical via Levenshtein edit-distance.
5
+ * Strips noise words (version strings, single generic words).
6
+ * Respects metadata tag prefixes (e.g. 'source:').
7
+ *
8
+ * Three modes:
9
+ * - 'enforce': must match within edit-distance 3, else drop (return null)
10
+ * - 'suggest': map to nearest canonical within edit-distance 2 (passthrough if no match)
11
+ * - 'off': no normalization — return tag as-is
12
+ */
13
+ // ─── Noise filter ────────────────────────────────────────────────────────────
14
+ /**
15
+ * Version-string pattern: v1.2, v10, v1.2.3, etc.
16
+ */
17
+ const VERSION_PATTERN = /^v\d+(\.\d+)*/i;
18
+ /**
19
+ * Generic single words that add no signal.
20
+ */
21
+ const NOISE_WORDS = new Set([
22
+ 'one',
23
+ 'via',
24
+ 'new',
25
+ 'full',
26
+ 'actual',
27
+ 'raw',
28
+ 'the',
29
+ 'and',
30
+ 'for',
31
+ 'with',
32
+ 'this',
33
+ 'that',
34
+ 'from',
35
+ 'into',
36
+ ]);
37
+ function isNoisy(tag) {
38
+ if (VERSION_PATTERN.test(tag))
39
+ return true;
40
+ if (NOISE_WORDS.has(tag.toLowerCase()))
41
+ return true;
42
+ return false;
43
+ }
44
+ // ─── Levenshtein edit distance ───────────────────────────────────────────────
45
+ /**
46
+ * Compute Levenshtein edit distance between two strings.
47
+ * O(m*n) time, O(n) space using two-row DP.
48
+ */
49
+ export function computeEditDistance(a, b) {
50
+ if (a === b)
51
+ return 0;
52
+ if (a.length === 0)
53
+ return b.length;
54
+ if (b.length === 0)
55
+ return a.length;
56
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
57
+ let curr = Array.from({ length: b.length + 1 });
58
+ for (let i = 1; i <= a.length; i++) {
59
+ curr[0] = i;
60
+ for (let j = 1; j <= b.length; j++) {
61
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
62
+ curr[j] = Math.min(curr[j - 1] + 1, // insertion
63
+ prev[j] + 1, // deletion
64
+ prev[j - 1] + cost);
65
+ }
66
+ [prev, curr] = [curr, prev];
67
+ }
68
+ return prev[b.length];
69
+ }
70
+ // ─── Metadata tag check ──────────────────────────────────────────────────────
71
+ /**
72
+ * Returns true if the tag starts with any of the given prefixes.
73
+ * Metadata tags (e.g. 'source:article') are exempt from canonical normalization.
74
+ */
75
+ export function isMetadataTag(tag, prefixes) {
76
+ return prefixes.some((prefix) => tag.startsWith(prefix));
77
+ }
78
+ // ─── Single tag normalization ────────────────────────────────────────────────
79
+ /**
80
+ * Normalize a single tag against a canonical list.
81
+ *
82
+ * @param tag - Raw tag to normalize
83
+ * @param canonical - Canonical tag list to map against
84
+ * @param mode - Constraint mode: 'enforce' | 'suggest' | 'off'
85
+ * @returns Normalized tag string, or null if the tag should be dropped.
86
+ */
87
+ export function normalizeTag(tag, canonical, mode) {
88
+ if (mode === 'off')
89
+ return tag;
90
+ const lower = tag.toLowerCase().trim();
91
+ // Always drop noise words
92
+ if (isNoisy(lower))
93
+ return null;
94
+ // Derive lowercase canonical for matching; preserve original casing for return
95
+ const canonicalLower = canonical.map((x) => x.toLowerCase());
96
+ // Exact match in canonical list — return canonical form (original casing)
97
+ const exactIdx = canonicalLower.indexOf(lower);
98
+ if (exactIdx !== -1)
99
+ return canonical[exactIdx];
100
+ if (canonical.length === 0) {
101
+ // No canonical list configured — pass through in suggest, drop in enforce
102
+ return mode === 'enforce' ? null : lower;
103
+ }
104
+ // Find nearest canonical by edit distance
105
+ let bestMatch = null;
106
+ let bestDist = Infinity;
107
+ for (let i = 0; i < canonicalLower.length; i++) {
108
+ const dist = computeEditDistance(lower, canonicalLower[i]);
109
+ if (dist < bestDist) {
110
+ bestDist = dist;
111
+ bestMatch = canonical[i];
112
+ }
113
+ }
114
+ const threshold = mode === 'enforce' ? 3 : 2;
115
+ if (bestDist <= threshold && bestMatch !== null) {
116
+ return bestMatch;
117
+ }
118
+ // No close match found
119
+ if (mode === 'enforce') {
120
+ return null; // drop the tag
121
+ }
122
+ // 'suggest' mode — keep original tag unchanged (passthrough)
123
+ return lower;
124
+ }
125
+ // ─── Batch tag normalization ─────────────────────────────────────────────────
126
+ /**
127
+ * Normalize a batch of tags against a canonical list.
128
+ * Filters out nulls (dropped tags). Deduplicates the result.
129
+ *
130
+ * @param tags - Raw tags
131
+ * @param canonical - Canonical tag list
132
+ * @param mode - Constraint mode
133
+ * @param metadataPrefixes - Tags with these prefixes bypass normalization
134
+ */
135
+ export function normalizeTags(tags, canonical, mode, metadataPrefixes = ['source:']) {
136
+ if (mode === 'off')
137
+ return tags;
138
+ const seen = new Set();
139
+ const result = [];
140
+ for (const tag of tags) {
141
+ // Metadata tags bypass canonical normalization but are still kept
142
+ if (isMetadataTag(tag, metadataPrefixes)) {
143
+ if (!seen.has(tag)) {
144
+ seen.add(tag);
145
+ result.push(tag);
146
+ }
147
+ continue;
148
+ }
149
+ const normalized = normalizeTag(tag, canonical, mode);
150
+ if (normalized !== null && !seen.has(normalized)) {
151
+ seen.add(normalized);
152
+ result.push(normalized);
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+ //# sourceMappingURL=tag-normalizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-normalizer.js","sourceRoot":"","sources":["../../src/vault/tag-normalizer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/core",
3
- "version": "9.14.4",
3
+ "version": "9.15.0",
4
4
  "description": "Shared engine for Soleri agents — vault, brain, planner, LLM utilities, and facade infrastructure.",
5
5
  "keywords": [
6
6
  "agent",
@@ -39,6 +39,10 @@
39
39
  "./personas": {
40
40
  "types": "./dist/persona/defaults.d.ts",
41
41
  "import": "./dist/persona/defaults.js"
42
+ },
43
+ "./skills/validate-skills": {
44
+ "types": "./dist/skills/validate-skills.d.ts",
45
+ "import": "./dist/skills/validate-skills.js"
42
46
  }
43
47
  },
44
48
  "publishConfig": {
@@ -388,7 +388,7 @@ describe('EmbeddingPipeline', () => {
388
388
 
389
389
  expect(result.embedded).toBe(2);
390
390
  expect(result.failed).toBe(0);
391
- expect(result.tokensUsed).toBeGreaterThan(0);
391
+ expect(result.tokensUsed).toBe(10); // mock provider returns texts.length * 5 = 2 * 5
392
392
 
393
393
  // Both entries should now have vectors
394
394
  expect(getVector(persistence, 'e1')).toBeTruthy();
@@ -405,7 +405,7 @@ describe('EmbeddingPipeline', () => {
405
405
  onProgress: (completed, total) => progress.push([completed, total]),
406
406
  });
407
407
 
408
- expect(progress.length).toBeGreaterThan(0);
408
+ expect(progress.length).toBe(1); // default batchSize=100 processes both entries in one batch, firing onProgress once
409
409
  // Last progress call should have completed == total
410
410
  const last = progress[progress.length - 1];
411
411
  expect(last[0]).toBe(last[1]);
@@ -513,7 +513,7 @@ describe('Brain hybrid search compatibility', () => {
513
513
 
514
514
  // Should not throw — returns results from FTS only
515
515
  expect(Array.isArray(results)).toBe(true);
516
- expect(results.length).toBeGreaterThan(0);
516
+ expect(results.length).toBe(1); // single seeded entry matches FTS query 'backward compatibility'
517
517
  });
518
518
 
519
519
  it('Brain.setEmbeddingProvider can set and clear provider', () => {
@@ -3,6 +3,7 @@ import type { SearchResult } from '../vault/vault.js';
3
3
  import type { VaultManager } from '../vault/vault-manager.js';
4
4
  import type { IntelligenceEntry } from '../intelligence/types.js';
5
5
  import { computeContentHash } from '../vault/content-hash.js';
6
+ import { normalizeTags as normalizeTagsCanonical } from '../vault/tag-normalizer.js';
6
7
  import {
7
8
  tokenize,
8
9
  calculateTf,
@@ -76,12 +77,20 @@ const DUPLICATE_BLOCK_THRESHOLD = 0.8;
76
77
  const DUPLICATE_WARN_THRESHOLD = 0.6;
77
78
  const RECENCY_HALF_LIFE_DAYS = 365;
78
79
 
80
+ /** Canonical tag taxonomy config, injected from AgentRuntimeConfig. */
81
+ export interface CanonicalTagConfig {
82
+ canonicalTags: string[];
83
+ tagConstraintMode: 'enforce' | 'suggest' | 'off';
84
+ metadataTagPrefixes: string[];
85
+ }
86
+
79
87
  export class Brain {
80
88
  private vault: Vault;
81
89
  private vaultManager: VaultManager | undefined;
82
90
  private embeddingProvider: EmbeddingProvider | undefined;
83
91
  private vocabulary: Map<string, number> = new Map();
84
92
  private weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
93
+ private canonicalTagConfig: CanonicalTagConfig | undefined;
85
94
 
86
95
  constructor(vault: Vault, vaultManager?: VaultManager, embeddingProvider?: EmbeddingProvider) {
87
96
  this.vault = vault;
@@ -91,6 +100,11 @@ export class Brain {
91
100
  this.recomputeWeights();
92
101
  }
93
102
 
103
+ /** Configure canonical tag taxonomy. Called by createAgentRuntime when config provides canonicalTags. */
104
+ setCanonicalTagConfig(cfg: CanonicalTagConfig): void {
105
+ this.canonicalTagConfig = cfg;
106
+ }
107
+
94
108
  /** Set or replace the embedding provider at runtime. */
95
109
  setEmbeddingProvider(provider: EmbeddingProvider | undefined): void {
96
110
  this.embeddingProvider = provider;
@@ -260,7 +274,17 @@ export class Brain {
260
274
  },
261
275
  ): CaptureResult {
262
276
  const autoTags = this.generateTags(entry.title, entry.description, entry.context);
263
- const mergedTags = Array.from(new Set([...(entry.tags ?? []), ...autoTags]));
277
+ let mergedTags = Array.from(new Set([...(entry.tags ?? []), ...autoTags]));
278
+
279
+ // Apply canonical tag normalization if configured
280
+ if (this.canonicalTagConfig && this.canonicalTagConfig.tagConstraintMode !== 'off') {
281
+ mergedTags = normalizeTagsCanonical(
282
+ mergedTags,
283
+ this.canonicalTagConfig.canonicalTags,
284
+ this.canonicalTagConfig.tagConstraintMode,
285
+ this.canonicalTagConfig.metadataTagPrefixes,
286
+ );
287
+ }
264
288
 
265
289
  const duplicate = this.detectDuplicate(entry.title, entry.domain);
266
290
 
@@ -1129,6 +1129,30 @@ export class BrainIntelligence {
1129
1129
  // ─── Intelligence Pipeline ────────────────────────────────────────
1130
1130
 
1131
1131
  buildIntelligence(): BuildIntelligenceResult {
1132
+ // Step 0: GC — close orphaned sessions with no execution signal older than 24h
1133
+ const TTL_MS = 24 * 60 * 60 * 1000;
1134
+ const cutoff = new Date(Date.now() - TTL_MS).toISOString();
1135
+ const activeSessions = this.listSessions({ active: true, limit: 1000 });
1136
+ let gcClosed = 0;
1137
+ for (const s of activeSessions) {
1138
+ const isOld = s.startedAt < cutoff;
1139
+ const hasNoSignal =
1140
+ s.toolsUsed.length === 0 && s.filesModified.length === 0 && s.planOutcome === null;
1141
+ if (isOld && hasNoSignal) {
1142
+ try {
1143
+ this.lifecycle({
1144
+ action: 'end',
1145
+ sessionId: s.id,
1146
+ planOutcome: 'abandoned',
1147
+ context: 'auto-gc: no execution signal after TTL',
1148
+ });
1149
+ gcClosed++;
1150
+ } catch {
1151
+ // GC must never break the intelligence pipeline
1152
+ }
1153
+ }
1154
+ }
1155
+
1132
1156
  // Step 1: Compute and persist strengths
1133
1157
  const strengths = this.computeStrengths();
1134
1158
 
@@ -1154,6 +1178,7 @@ export class BrainIntelligence {
1154
1178
  strengthsComputed: strengths.length,
1155
1179
  globalPatterns,
1156
1180
  domainProfiles,
1181
+ gcClosed,
1157
1182
  };
1158
1183
  }
1159
1184
 
@@ -198,6 +198,7 @@ export interface BuildIntelligenceResult {
198
198
  strengthsComputed: number;
199
199
  globalPatterns: number;
200
200
  domainProfiles: number;
201
+ gcClosed: number;
201
202
  }
202
203
 
203
204
  export interface BrainIntelligenceStats {
@@ -16,6 +16,7 @@ const DEFAULT_TTL_MS = 7_200_000; // 2 hours
16
16
  const DEFAULT_COMPACTION_THRESHOLD = 100;
17
17
  const DEFAULT_COMPACTION_KEEP = 40;
18
18
  const REAPER_INTERVAL_MS = 60_000; // 1 minute
19
+ const SESSION_SUBDIR = 'sessions';
19
20
 
20
21
  export class ChatSessionManager {
21
22
  private sessions = new Map<string, ChatSession>();
@@ -31,6 +32,7 @@ export class ChatSessionManager {
31
32
  };
32
33
 
33
34
  mkdirSync(this.config.storageDir, { recursive: true });
35
+ mkdirSync(this.sessionDir(), { recursive: true });
34
36
  }
35
37
 
36
38
  // ─── Lifecycle ──────────────────────────────────────────────────
@@ -78,7 +80,8 @@ export class ChatSessionManager {
78
80
  */
79
81
  has(sessionId: string): boolean {
80
82
  if (this.sessions.has(sessionId)) return true;
81
- return existsSync(this.sessionPath(sessionId));
83
+ if (existsSync(this.sessionPath(sessionId))) return true;
84
+ return this.loadSessionFile(this.legacySessionPath(sessionId)) !== undefined;
82
85
  }
83
86
 
84
87
  // ─── Message Management ─────────────────────────────────────────
@@ -159,15 +162,11 @@ export class ChatSessionManager {
159
162
  */
160
163
  listAll(): string[] {
161
164
  const memoryIds = new Set(this.sessions.keys());
162
- try {
163
- const files = readdirSync(this.config.storageDir);
164
- for (const f of files) {
165
- if (f.endsWith('.json')) {
166
- memoryIds.add(f.replace('.json', ''));
167
- }
168
- }
169
- } catch {
170
- // Directory may not exist yet
165
+ for (const id of this.readPersistedSessionIds(this.sessionDir())) {
166
+ memoryIds.add(id);
167
+ }
168
+ for (const id of this.readPersistedSessionIds(this.config.storageDir)) {
169
+ memoryIds.add(id);
171
170
  }
172
171
  return [...memoryIds];
173
172
  }
@@ -246,38 +245,97 @@ export class ChatSessionManager {
246
245
  session.messages = session.messages.slice(-keep);
247
246
  }
248
247
 
248
+ private sessionDir(): string {
249
+ return join(this.config.storageDir, SESSION_SUBDIR);
250
+ }
251
+
249
252
  private sessionPath(sessionId: string): string {
250
253
  // Sanitize ID for filesystem safety
254
+ const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
255
+ return join(this.sessionDir(), `${safe}.json`);
256
+ }
257
+
258
+ private legacySessionPath(sessionId: string): string {
251
259
  const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
252
260
  return join(this.config.storageDir, `${safe}.json`);
253
261
  }
254
262
 
255
263
  private persistToDisk(session: ChatSession): void {
256
264
  try {
265
+ mkdirSync(this.sessionDir(), { recursive: true });
257
266
  writeFileSync(this.sessionPath(session.id), JSON.stringify(session), 'utf-8');
267
+
268
+ // Clean up valid legacy session files after migrating to the namespaced path.
269
+ const legacyPath = this.legacySessionPath(session.id);
270
+ if (this.loadSessionFile(legacyPath)) {
271
+ this.removeFile(legacyPath);
272
+ }
258
273
  } catch {
259
274
  // Disk write failure is non-critical — session lives in memory
260
275
  }
261
276
  }
262
277
 
263
278
  private loadFromDisk(sessionId: string): ChatSession | undefined {
264
- const path = this.sessionPath(sessionId);
279
+ const current = this.loadSessionFile(this.sessionPath(sessionId));
280
+ if (current) return current;
281
+
282
+ const legacyPath = this.legacySessionPath(sessionId);
283
+ const legacy = this.loadSessionFile(legacyPath);
284
+ if (legacy) {
285
+ this.persistToDisk(legacy);
286
+ return legacy;
287
+ }
288
+
289
+ return undefined;
290
+ }
291
+
292
+ private removeFromDisk(sessionId: string): void {
293
+ this.removeFile(this.sessionPath(sessionId));
294
+ const legacyPath = this.legacySessionPath(sessionId);
295
+ if (this.loadSessionFile(legacyPath)) {
296
+ this.removeFile(legacyPath);
297
+ }
298
+ }
299
+
300
+ private removeFile(path: string): void {
301
+ try {
302
+ rmSync(path, { force: true });
303
+ } catch {
304
+ // Removal failure is non-critical
305
+ }
306
+ }
307
+
308
+ private loadSessionFile(path: string): ChatSession | undefined {
265
309
  if (!existsSync(path)) return undefined;
266
310
 
267
311
  try {
268
- const data = readFileSync(path, 'utf-8');
269
- return JSON.parse(data) as ChatSession;
312
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
313
+ return this.isChatSession(data) ? data : undefined;
270
314
  } catch {
271
315
  return undefined;
272
316
  }
273
317
  }
274
318
 
275
- private removeFromDisk(sessionId: string): void {
276
- const path = this.sessionPath(sessionId);
319
+ private readPersistedSessionIds(dir: string): string[] {
277
320
  try {
278
- rmSync(path, { force: true });
321
+ return readdirSync(dir).flatMap((file) => {
322
+ if (!file.endsWith('.json')) return [];
323
+ const session = this.loadSessionFile(join(dir, file));
324
+ return session ? [session.id] : [];
325
+ });
279
326
  } catch {
280
- // Removal failure is non-critical
327
+ return [];
281
328
  }
282
329
  }
330
+
331
+ private isChatSession(value: unknown): value is ChatSession {
332
+ if (!value || typeof value !== 'object') return false;
333
+ const session = value as Partial<ChatSession>;
334
+ return (
335
+ typeof session.id === 'string' &&
336
+ Array.isArray(session.messages) &&
337
+ typeof session.createdAt === 'number' &&
338
+ typeof session.lastActiveAt === 'number'
339
+ );
340
+ }
283
341
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { describe, test, expect, beforeEach, afterEach } from 'vitest';
7
- import { mkdtempSync, rmSync } from 'node:fs';
7
+ import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { ChatSessionManager } from './chat-session.js';
@@ -136,6 +136,19 @@ describe('ChatSessionManager', () => {
136
136
  expect(all).toContain('chat-2');
137
137
  });
138
138
 
139
+ test('listAll ignores non-session JSON files in the storage root', () => {
140
+ manager.getOrCreate('chat-1');
141
+ writeFileSync(
142
+ join(dir, 'plans.json'),
143
+ JSON.stringify({ version: '1.0', plans: [] }),
144
+ 'utf-8',
145
+ );
146
+
147
+ const all = manager.listAll();
148
+ expect(all).toContain('chat-1');
149
+ expect(all).not.toContain('plans');
150
+ });
151
+
139
152
  test('setMeta updates metadata', () => {
140
153
  manager.getOrCreate('chat-1');
141
154
  manager.setMeta('chat-1', { mood: 'happy' });
@@ -162,6 +175,23 @@ describe('ChatSessionManager', () => {
162
175
  manager2.close();
163
176
  });
164
177
 
178
+ test('session files are namespaced away from plans.json collisions', () => {
179
+ writeFileSync(
180
+ join(dir, 'plans.json'),
181
+ JSON.stringify({ version: '1.0', plans: [{ id: 'plan-1' }] }),
182
+ 'utf-8',
183
+ );
184
+
185
+ const session = manager.getOrCreate('plans');
186
+
187
+ expect(session.messages).toEqual([]);
188
+ expect(JSON.parse(readFileSync(join(dir, 'plans.json'), 'utf-8'))).toEqual({
189
+ version: '1.0',
190
+ plans: [{ id: 'plan-1' }],
191
+ });
192
+ expect(existsSync(join(dir, 'sessions', 'plans.json'))).toBe(true);
193
+ });
194
+
165
195
  test('delete removes from disk', () => {
166
196
  manager.getOrCreate('chat-1');
167
197
  manager.delete('chat-1');