chainlesschain 0.42.2 → 0.43.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.
@@ -40,10 +40,15 @@ export class CLIContextEngineering {
40
40
  * @param {object} options
41
41
  * @param {object|null} options.db - Database instance (null for graceful degradation)
42
42
  * @param {object|null} options.permanentMemory - CLIPermanentMemory instance (optional)
43
+ * @param {object|null} options.scope - Scoping options for sub-agent isolation
44
+ * @param {string} [options.scope.taskId] - Task/sub-agent ID
45
+ * @param {string} [options.scope.role] - Sub-agent role
46
+ * @param {string} [options.scope.parentObjective] - Parent task objective
43
47
  */
44
- constructor({ db, permanentMemory } = {}) {
48
+ constructor({ db, permanentMemory, scope } = {}) {
45
49
  this.db = db || null;
46
50
  this.permanentMemory = permanentMemory || null;
51
+ this.scope = scope || null;
47
52
  this.errorHistory = [];
48
53
  this.taskContext = null;
49
54
  this._bm25 = null;
@@ -52,6 +57,15 @@ export class CLIContextEngineering {
52
57
  this._compactionSummaries = [];
53
58
  // Stable prefix cache: { hash, cleanedPrefix }
54
59
  this._prefixCache = null;
60
+
61
+ // When scoped, auto-set task context from scope
62
+ if (this.scope && this.scope.parentObjective) {
63
+ this.taskContext = {
64
+ objective: this.scope.parentObjective,
65
+ steps: [],
66
+ currentStep: 0,
67
+ };
68
+ }
55
69
  }
56
70
 
57
71
  /**
@@ -91,31 +105,46 @@ export class CLIContextEngineering {
91
105
  }
92
106
  }
93
107
 
94
- // 3. Memory injection
108
+ // 3. Memory injection (scoped: higher threshold, namespace-aware)
95
109
  if (this.db && userQuery) {
96
110
  try {
97
- const memories = _deps.recallMemory(this.db, userQuery, { limit: 5 });
111
+ const memoryQuery = this.scope
112
+ ? `[${this.scope.role}] ${userQuery}`
113
+ : userQuery;
114
+ const memoryOpts = { limit: 5 };
115
+ if (this.scope) {
116
+ memoryOpts.namespace = this.scope.taskId;
117
+ }
118
+ const memories = _deps.recallMemory(this.db, memoryQuery, memoryOpts);
98
119
  if (memories && memories.length > 0) {
99
- const lines = memories.map(
100
- (m) =>
101
- `- [${m.layer}] ${m.content} (retention: ${(m.retention * 100).toFixed(0)}%)`,
102
- );
103
- result.push({
104
- role: "system",
105
- content: `## Relevant Memories\n${lines.join("\n")}`,
106
- });
120
+ // When scoped, apply higher relevance threshold to reduce noise
121
+ const threshold = this.scope ? 0.6 : 0.3;
122
+ const filtered = memories.filter((m) => m.retention >= threshold);
123
+ if (filtered.length > 0) {
124
+ const lines = filtered.map(
125
+ (m) =>
126
+ `- [${m.layer}] ${m.content} (retention: ${(m.retention * 100).toFixed(0)}%)`,
127
+ );
128
+ result.push({
129
+ role: "system",
130
+ content: `## Relevant Memories\n${lines.join("\n")}`,
131
+ });
132
+ }
107
133
  }
108
134
  } catch (_err) {
109
135
  // Memory injection failed — skip silently
110
136
  }
111
137
  }
112
138
 
113
- // 4. Notes injection (BM25 search)
139
+ // 4. Notes injection (BM25 search — scoped: role-prefixed query)
114
140
  if (this.db && userQuery) {
115
141
  try {
116
142
  this._ensureNotesIndex();
117
143
  if (this._bm25 && this._bm25.totalDocs > 0) {
118
- const hits = this._bm25.search(userQuery, {
144
+ const notesQuery = this.scope
145
+ ? `[${this.scope.role}] ${userQuery}`
146
+ : userQuery;
147
+ const hits = this._bm25.search(notesQuery, {
119
148
  topK: 3,
120
149
  threshold: 0.5,
121
150
  });
@@ -135,10 +164,14 @@ export class CLIContextEngineering {
135
164
  }
136
165
  }
137
166
 
138
- // 5. Permanent memory injection
167
+ // 5. Permanent memory injection (scoped: reduced results)
139
168
  if (this.permanentMemory && userQuery) {
140
169
  try {
141
- const pmResults = this.permanentMemory.getRelevantContext(userQuery, 3);
170
+ const pmLimit = this.scope ? 2 : 3;
171
+ const pmResults = this.permanentMemory.getRelevantContext(
172
+ userQuery,
173
+ pmLimit,
174
+ );
142
175
  if (pmResults && pmResults.length > 0) {
143
176
  const lines = pmResults.map(
144
177
  (r) => `- [${r.source || "memory"}] ${r.content}`,
@@ -91,6 +91,16 @@ export async function startDebate({
91
91
  }
92
92
 
93
93
  // Phase 2: Moderator synthesizes final verdict
94
+ // Summarize each reviewer's output to reduce context pollution for the moderator
95
+ const REVIEW_SUMMARY_MAX = 300;
96
+ const reviewSummaries = reviews.map((r) => {
97
+ const summarized =
98
+ r.review.length <= REVIEW_SUMMARY_MAX
99
+ ? r.review
100
+ : r.review.substring(0, REVIEW_SUMMARY_MAX) + "... [truncated]";
101
+ return { ...r, reviewSummary: summarized };
102
+ });
103
+
94
104
  const moderatorMessages = [
95
105
  {
96
106
  role: "system",
@@ -99,8 +109,8 @@ export async function startDebate({
99
109
  },
100
110
  {
101
111
  role: "user",
102
- content: `Multiple reviewers analyzed this code. Synthesize their findings into a final verdict.\n\nTarget: ${target}\n\n${reviews
103
- .map((r) => `### ${r.role} (${r.verdict})\n${r.review}`)
112
+ content: `Multiple reviewers analyzed this code. Synthesize their findings into a final verdict.\n\nTarget: ${target}\n\n${reviewSummaries
113
+ .map((r) => `### ${r.role} (${r.verdict})\n${r.reviewSummary}`)
104
114
  .join(
105
115
  "\n\n---\n\n",
106
116
  )}\n\nProvide:\n1. Final Verdict: APPROVE / NEEDS_WORK / REJECT\n2. Consensus Score: 0-100 (how much the reviewers agree)\n3. Summary of key findings across all perspectives\n4. Priority action items (if any)`,
@@ -15,9 +15,83 @@ export const MEMORY_CONFIG = {
15
15
  };
16
16
 
17
17
  // ─── In-memory layers ────────────────────────────────────────────
18
- // Map<id, { id, content, type, importance, accessCount, createdAt, lastAccessed }>
19
- export const _working = new Map();
20
- export const _shortTerm = new Map();
18
+ // Internal storage: Map<namespace, Map<id, entry>>
19
+ // Default namespace "global" preserves backward compatibility.
20
+ const _workingNS = new Map();
21
+ const _shortTermNS = new Map();
22
+ const DEFAULT_NS = "global";
23
+
24
+ function _getWorkingNS(namespace) {
25
+ const ns = namespace || DEFAULT_NS;
26
+ if (!_workingNS.has(ns)) _workingNS.set(ns, new Map());
27
+ return _workingNS.get(ns);
28
+ }
29
+
30
+ function _getShortTermNS(namespace) {
31
+ const ns = namespace || DEFAULT_NS;
32
+ if (!_shortTermNS.has(ns)) _shortTermNS.set(ns, new Map());
33
+ return _shortTermNS.get(ns);
34
+ }
35
+
36
+ // ─── Backward-compatible proxy ──────────────────────────────────
37
+ // Existing code (and tests) access _working/_shortTerm as flat Maps:
38
+ // _working.size, _working.get(id), _working.clear(), _working.delete(id)
39
+ // We proxy these to the default namespace while exposing namespaced internals.
40
+ function _createCompatProxy(nsMap, getNS) {
41
+ return {
42
+ // Flat access — routes to default namespace
43
+ get size() {
44
+ const ns = getNS(DEFAULT_NS);
45
+ return ns.size;
46
+ },
47
+ get(key) {
48
+ const ns = getNS(DEFAULT_NS);
49
+ return ns.get(key);
50
+ },
51
+ set(key, value) {
52
+ const ns = getNS(DEFAULT_NS);
53
+ return ns.set(key, value);
54
+ },
55
+ has(key) {
56
+ const ns = getNS(DEFAULT_NS);
57
+ return ns.has(key);
58
+ },
59
+ delete(key) {
60
+ const ns = getNS(DEFAULT_NS);
61
+ return ns.delete(key);
62
+ },
63
+ values() {
64
+ const ns = getNS(DEFAULT_NS);
65
+ return ns.values();
66
+ },
67
+ entries() {
68
+ const ns = getNS(DEFAULT_NS);
69
+ return ns.entries();
70
+ },
71
+ keys() {
72
+ const ns = getNS(DEFAULT_NS);
73
+ return ns.keys();
74
+ },
75
+ forEach(callback) {
76
+ const ns = getNS(DEFAULT_NS);
77
+ return ns.forEach(callback);
78
+ },
79
+ [Symbol.iterator]() {
80
+ const ns = getNS(DEFAULT_NS);
81
+ return ns[Symbol.iterator]();
82
+ },
83
+ // Clear ALL namespaces (for test cleanup)
84
+ clear() {
85
+ nsMap.clear();
86
+ },
87
+ // ─── Namespace-aware internals (used by this module) ──────
88
+ _nsMap: nsMap,
89
+ _getNS: getNS,
90
+ };
91
+ }
92
+
93
+ export const _working = _createCompatProxy(_workingNS, _getWorkingNS);
94
+ export const _shortTerm = _createCompatProxy(_shortTermNS, _getShortTermNS);
21
95
 
22
96
  // ─── Helpers ─────────────────────────────────────────────────────
23
97
  function generateId() {
@@ -91,6 +165,17 @@ export function ensureMemoryTables(db) {
91
165
  * Store a memory at the appropriate layer based on importance.
92
166
  * core >= 0.9, long-term >= 0.6, short-term >= 0.3, working < 0.3
93
167
  */
168
+ /**
169
+ * Store a memory at the appropriate layer based on importance.
170
+ * core >= 0.9, long-term >= 0.6, short-term >= 0.3, working < 0.3
171
+ *
172
+ * @param {object} db - Database instance
173
+ * @param {string} content - Memory content
174
+ * @param {object} [options]
175
+ * @param {number} [options.importance=0.5]
176
+ * @param {string} [options.type="episodic"]
177
+ * @param {string} [options.namespace] - Namespace for in-memory isolation (default: "global")
178
+ */
94
179
  export function storeMemory(db, content, options = {}) {
95
180
  if (!content || !content.trim()) {
96
181
  throw new Error("Memory content cannot be empty");
@@ -101,6 +186,7 @@ export function storeMemory(db, content, options = {}) {
101
186
  Math.min(1, parseFloat(options.importance) || 0.5),
102
187
  );
103
188
  const type = options.type || "episodic";
189
+ const namespace = options.namespace || DEFAULT_NS;
104
190
  const id = generateId();
105
191
  const now = nowISO();
106
192
 
@@ -119,14 +205,15 @@ export function storeMemory(db, content, options = {}) {
119
205
  ).run(id, content, type, importance, "long-term", now, now);
120
206
  } else if (importance >= 0.3) {
121
207
  layer = "short-term";
122
- if (_shortTerm.size >= MEMORY_CONFIG.shortTermCapacity) {
208
+ const nsMap = _getShortTermNS(namespace);
209
+ if (nsMap.size >= MEMORY_CONFIG.shortTermCapacity) {
123
210
  // Evict oldest
124
- const oldest = [..._shortTerm.entries()].sort(
211
+ const oldest = [...nsMap.entries()].sort(
125
212
  (a, b) => new Date(a[1].lastAccessed) - new Date(b[1].lastAccessed),
126
213
  )[0];
127
- if (oldest) _shortTerm.delete(oldest[0]);
214
+ if (oldest) nsMap.delete(oldest[0]);
128
215
  }
129
- _shortTerm.set(id, {
216
+ nsMap.set(id, {
130
217
  id,
131
218
  content,
132
219
  type,
@@ -137,13 +224,14 @@ export function storeMemory(db, content, options = {}) {
137
224
  });
138
225
  } else {
139
226
  layer = "working";
140
- if (_working.size >= MEMORY_CONFIG.workingCapacity) {
141
- const oldest = [..._working.entries()].sort(
227
+ const nsMap = _getWorkingNS(namespace);
228
+ if (nsMap.size >= MEMORY_CONFIG.workingCapacity) {
229
+ const oldest = [...nsMap.entries()].sort(
142
230
  (a, b) => new Date(a[1].lastAccessed) - new Date(b[1].lastAccessed),
143
231
  )[0];
144
- if (oldest) _working.delete(oldest[0]);
232
+ if (oldest) nsMap.delete(oldest[0]);
145
233
  }
146
- _working.set(id, {
234
+ nsMap.set(id, {
147
235
  id,
148
236
  content,
149
237
  type,
@@ -162,16 +250,22 @@ export function storeMemory(db, content, options = {}) {
162
250
  /**
163
251
  * Search all memory layers with Ebbinghaus forgetting curve.
164
252
  * Strengthens recalled memories (spacing effect).
253
+ *
254
+ * When options.namespace is set, searches that namespace's in-memory maps
255
+ * plus the shared long-term/core DB layers. Without namespace, searches
256
+ * the default "global" namespace (backward compatible).
165
257
  */
166
258
  export function recallMemory(db, query, options = {}) {
167
259
  if (!query || !query.trim()) return [];
168
260
 
169
261
  const limit = Math.max(1, parseInt(options.limit) || 20);
170
262
  const pattern = query.toLowerCase();
263
+ const namespace = options.namespace || DEFAULT_NS;
171
264
  const results = [];
172
265
 
173
- // Search working memory
174
- for (const mem of _working.values()) {
266
+ // Search working memory (namespace-scoped)
267
+ const workingNS = _getWorkingNS(namespace);
268
+ for (const mem of workingNS.values()) {
175
269
  if (mem.content.toLowerCase().includes(pattern)) {
176
270
  const retention = calcRetention(mem.lastAccessed);
177
271
  if (retention >= MEMORY_CONFIG.recallThreshold) {
@@ -182,8 +276,9 @@ export function recallMemory(db, query, options = {}) {
182
276
  }
183
277
  }
184
278
 
185
- // Search short-term memory
186
- for (const mem of _shortTerm.values()) {
279
+ // Search short-term memory (namespace-scoped)
280
+ const shortTermNS = _getShortTermNS(namespace);
281
+ for (const mem of shortTermNS.values()) {
187
282
  if (mem.content.toLowerCase().includes(pattern)) {
188
283
  const retention = calcRetention(mem.lastAccessed);
189
284
  if (retention >= MEMORY_CONFIG.recallThreshold) {
@@ -260,41 +355,46 @@ export function consolidateMemory(db) {
260
355
  let promoted = 0;
261
356
  let forgotten = 0;
262
357
 
263
- // Promote working → short-term
264
- for (const [id, mem] of _working) {
265
- const retention = calcRetention(mem.lastAccessed);
266
- if (retention < MEMORY_CONFIG.recallThreshold) {
267
- _working.delete(id);
268
- forgotten++;
269
- } else if (mem.accessCount >= 3) {
270
- _working.delete(id);
271
- _shortTerm.set(id, { ...mem, lastAccessed: nowISO() });
272
- promoted++;
358
+ // Promote working → short-term (across all namespaces)
359
+ for (const [ns, nsMap] of _workingNS) {
360
+ const shortTermNS = _getShortTermNS(ns);
361
+ for (const [id, mem] of nsMap) {
362
+ const retention = calcRetention(mem.lastAccessed);
363
+ if (retention < MEMORY_CONFIG.recallThreshold) {
364
+ nsMap.delete(id);
365
+ forgotten++;
366
+ } else if (mem.accessCount >= 3) {
367
+ nsMap.delete(id);
368
+ shortTermNS.set(id, { ...mem, lastAccessed: nowISO() });
369
+ promoted++;
370
+ }
273
371
  }
274
372
  }
275
373
 
276
- // Promote short-term → long-term
277
- for (const [id, mem] of _shortTerm) {
278
- const retention = calcRetention(mem.lastAccessed);
279
- if (retention < MEMORY_CONFIG.recallThreshold) {
280
- _shortTerm.delete(id);
281
- forgotten++;
282
- } else if (mem.accessCount >= 5) {
283
- _shortTerm.delete(id);
284
- const now = nowISO();
285
- db.prepare(
286
- `INSERT INTO memory_long_term (id, content, type, importance, access_count, layer, created_at, last_accessed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
287
- ).run(
288
- id,
289
- mem.content,
290
- mem.type,
291
- mem.importance,
292
- mem.accessCount,
293
- "long-term",
294
- mem.createdAt,
295
- now,
296
- );
297
- promoted++;
374
+ // Promote short-term → long-term (across all namespaces)
375
+ for (const [, nsMap] of _shortTermNS) {
376
+ for (const [id, mem] of nsMap) {
377
+ const retention = calcRetention(mem.lastAccessed);
378
+ if (retention < MEMORY_CONFIG.recallThreshold) {
379
+ nsMap.delete(id);
380
+ forgotten++;
381
+ } else if (mem.accessCount >= 5) {
382
+ nsMap.delete(id);
383
+ const now = nowISO();
384
+ db.prepare(
385
+ `INSERT INTO memory_long_term (id, content, type, importance, access_count, layer, created_at, last_accessed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
386
+ ).run(
387
+ id,
388
+ mem.content,
389
+ mem.type,
390
+ mem.importance,
391
+ mem.accessCount,
392
+ "long-term",
393
+ mem.createdAt,
394
+ now,
395
+ );
396
+ promoted++;
397
+ }
298
398
  }
299
399
  }
300
400
 
@@ -324,15 +424,19 @@ function _searchByType(db, query, type, options = {}) {
324
424
  const pattern = query.toLowerCase();
325
425
  const results = [];
326
426
 
327
- // In-memory layers
328
- for (const mem of _working.values()) {
329
- if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
330
- results.push({ ...mem, layer: "working" });
427
+ // In-memory layers (search all namespaces)
428
+ for (const [, nsMap] of _workingNS) {
429
+ for (const mem of nsMap.values()) {
430
+ if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
431
+ results.push({ ...mem, layer: "working" });
432
+ }
331
433
  }
332
434
  }
333
- for (const mem of _shortTerm.values()) {
334
- if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
335
- results.push({ ...mem, layer: "short-term" });
435
+ for (const [, nsMap] of _shortTermNS) {
436
+ for (const mem of nsMap.values()) {
437
+ if (mem.type === type && mem.content.toLowerCase().includes(pattern)) {
438
+ results.push({ ...mem, layer: "short-term" });
439
+ }
336
440
  }
337
441
  }
338
442
 
@@ -412,19 +516,23 @@ export function pruneMemory(db, options = {}) {
412
516
  const maxAgeHours = parseFloat(options.maxAge) || 720; // 30 days default
413
517
  let pruned = 0;
414
518
 
415
- // Prune in-memory layers by retention
416
- for (const [id, mem] of _working) {
417
- const retention = calcRetention(mem.lastAccessed);
418
- if (retention < MEMORY_CONFIG.recallThreshold) {
419
- _working.delete(id);
420
- pruned++;
519
+ // Prune in-memory layers by retention (across all namespaces)
520
+ for (const [, nsMap] of _workingNS) {
521
+ for (const [id, mem] of nsMap) {
522
+ const retention = calcRetention(mem.lastAccessed);
523
+ if (retention < MEMORY_CONFIG.recallThreshold) {
524
+ nsMap.delete(id);
525
+ pruned++;
526
+ }
421
527
  }
422
528
  }
423
- for (const [id, mem] of _shortTerm) {
424
- const retention = calcRetention(mem.lastAccessed);
425
- if (retention < MEMORY_CONFIG.recallThreshold) {
426
- _shortTerm.delete(id);
427
- pruned++;
529
+ for (const [, nsMap] of _shortTermNS) {
530
+ for (const [id, mem] of nsMap) {
531
+ const retention = calcRetention(mem.lastAccessed);
532
+ if (retention < MEMORY_CONFIG.recallThreshold) {
533
+ nsMap.delete(id);
534
+ pruned++;
535
+ }
428
536
  }
429
537
  }
430
538
 
@@ -460,12 +568,22 @@ export function getMemoryStats(db) {
460
568
  .prepare(`SELECT COUNT(*) as count FROM memory_sharing`)
461
569
  .get();
462
570
 
571
+ // Sum across all namespaces
572
+ let workingTotal = 0;
573
+ for (const [, nsMap] of _workingNS) workingTotal += nsMap.size;
574
+ let shortTermTotal = 0;
575
+ for (const [, nsMap] of _shortTermNS) shortTermTotal += nsMap.size;
576
+
463
577
  return {
464
- working: _working.size,
465
- shortTerm: _shortTerm.size,
578
+ working: workingTotal,
579
+ shortTerm: shortTermTotal,
466
580
  longTerm: ltCount.count,
467
581
  core: coreCount.count,
468
582
  shared: shareCount.count,
469
- total: _working.size + _shortTerm.size + ltCount.count + coreCount.count,
583
+ namespaces: {
584
+ working: [..._workingNS.keys()],
585
+ shortTerm: [..._shortTermNS.keys()],
586
+ },
587
+ total: workingTotal + shortTermTotal + ltCount.count + coreCount.count,
470
588
  };
471
589
  }