@cleocode/core 2026.4.14 → 2026.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/credentials.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +1 -1
- package/dist/hooks/payload-schemas.d.ts.map +1 -1
- package/dist/index.js +34095 -31432
- package/dist/index.js.map +4 -4
- package/dist/memory/brain-retrieval.d.ts +4 -0
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/embedding-local.d.ts +5 -5
- package/dist/memory/engine-compat.d.ts +4 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/mental-model-injection.d.ts +52 -0
- package/dist/memory/mental-model-injection.d.ts.map +1 -0
- package/dist/memory/mental-model-queue.d.ts +75 -0
- package/dist/memory/mental-model-queue.d.ts.map +1 -0
- package/dist/orchestration/index.d.ts +2 -0
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/paths.d.ts +65 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/store/brain-accessor.d.ts +2 -0
- package/dist/store/brain-accessor.d.ts.map +1 -1
- package/dist/store/brain-schema.d.ts +16 -0
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/nexus-validation-schemas.d.ts +1 -1
- package/dist/store/nexus-validation-schemas.d.ts.map +1 -1
- package/dist/store/validation-schemas.d.ts +1 -1
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/migrations/drizzle-brain/20260408000001_t417-agent-field/migration.sql +13 -0
- package/migrations/drizzle-brain/20260408000001_t417-agent-field/snapshot.json +28 -0
- package/package.json +15 -15
- package/src/__tests__/ct-master-tac-install.test.ts +168 -0
- package/src/crypto/credentials.ts +28 -0
- package/src/hooks/payload-schemas.ts +1 -1
- package/src/memory/__tests__/mental-model-wave-8.test.ts +355 -0
- package/src/memory/brain-retrieval.ts +55 -29
- package/src/memory/embedding-local.ts +5 -5
- package/src/memory/engine-compat.ts +24 -2
- package/src/memory/mental-model-injection.ts +87 -0
- package/src/memory/mental-model-queue.ts +291 -0
- package/src/orchestration/index.ts +2 -0
- package/src/paths.ts +79 -0
- package/src/store/brain-accessor.ts +5 -0
- package/src/store/brain-schema.ts +4 -0
- package/src/store/nexus-validation-schemas.ts +3 -3
- package/src/store/validation-schemas.ts +3 -3
- package/src/validation/protocols/cant/architecture-decision.cant +12 -2
- package/src/validation/protocols/cant/artifact-publish.cant +11 -1
- package/src/validation/protocols/cant/consensus.cant +12 -1
- package/src/validation/protocols/cant/contribution.cant +11 -1
- package/src/validation/protocols/cant/decomposition.cant +11 -1
- package/src/validation/protocols/cant/implementation.cant +11 -1
- package/src/validation/protocols/cant/provenance.cant +13 -1
- package/src/validation/protocols/cant/release.cant +12 -1
- package/src/validation/protocols/cant/research.cant +12 -1
- package/src/validation/protocols/cant/specification.cant +11 -1
- package/src/validation/protocols/cant/testing.cant +12 -1
- package/src/validation/protocols/cant/validation.cant +11 -1
|
@@ -58,6 +58,8 @@ export interface SearchBrainCompactParams {
|
|
|
58
58
|
tables?: Array<'decisions' | 'patterns' | 'learnings' | 'observations'>;
|
|
59
59
|
dateStart?: string;
|
|
60
60
|
dateEnd?: string;
|
|
61
|
+
/** T418: filter results to observations produced by a specific agent (Wave 8 mental models). */
|
|
62
|
+
agent?: string;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** Result from searchBrainCompact. */
|
|
@@ -121,6 +123,8 @@ export interface ObserveBrainParams {
|
|
|
121
123
|
project?: string;
|
|
122
124
|
sourceSessionId?: string;
|
|
123
125
|
sourceType?: BrainObservationSourceType;
|
|
126
|
+
/** T417: agent provenance — the name of the spawned agent producing this observation. */
|
|
127
|
+
agent?: string;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/** Result from observeBrain. */
|
|
@@ -149,15 +153,21 @@ export async function searchBrainCompact(
|
|
|
149
153
|
projectRoot: string,
|
|
150
154
|
params: SearchBrainCompactParams,
|
|
151
155
|
): Promise<SearchBrainCompactResult> {
|
|
152
|
-
const { query, limit, tables, dateStart, dateEnd } = params;
|
|
156
|
+
const { query, limit, tables, dateStart, dateEnd, agent } = params;
|
|
153
157
|
|
|
154
158
|
if (!query || !query.trim()) {
|
|
155
159
|
return { results: [], total: 0, tokensEstimated: 0 };
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
// T418: when agent filter is set, restrict search to observations table only
|
|
163
|
+
const effectiveTables =
|
|
164
|
+
agent !== undefined && agent !== null
|
|
165
|
+
? (['observations'] as Array<'decisions' | 'patterns' | 'learnings' | 'observations'>)
|
|
166
|
+
: tables;
|
|
167
|
+
|
|
158
168
|
const searchResult = await searchBrain(projectRoot, query, {
|
|
159
169
|
limit: limit ?? 10,
|
|
160
|
-
tables,
|
|
170
|
+
tables: effectiveTables,
|
|
161
171
|
});
|
|
162
172
|
|
|
163
173
|
// Project full results to compact format.
|
|
@@ -166,38 +176,45 @@ export async function searchBrainCompact(
|
|
|
166
176
|
// We handle both naming conventions for robustness.
|
|
167
177
|
let results: BrainCompactHit[] = [];
|
|
168
178
|
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
if (!agent) {
|
|
180
|
+
for (const d of searchResult.decisions) {
|
|
181
|
+
const raw = d as Record<string, unknown>;
|
|
182
|
+
results.push({
|
|
183
|
+
id: d.id,
|
|
184
|
+
type: 'decision',
|
|
185
|
+
title: d.decision.slice(0, 80),
|
|
186
|
+
date: (d.createdAt ?? (raw['created_at'] as string)) || '',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
for (const p of searchResult.patterns) {
|
|
191
|
+
const raw = p as Record<string, unknown>;
|
|
192
|
+
results.push({
|
|
193
|
+
id: p.id,
|
|
194
|
+
type: 'pattern',
|
|
195
|
+
title: p.pattern.slice(0, 80),
|
|
196
|
+
date: (p.extractedAt ?? (raw['extracted_at'] as string)) || '',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
188
199
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
for (const l of searchResult.learnings) {
|
|
201
|
+
const raw = l as Record<string, unknown>;
|
|
202
|
+
results.push({
|
|
203
|
+
id: l.id,
|
|
204
|
+
type: 'learning',
|
|
205
|
+
title: l.insight.slice(0, 80),
|
|
206
|
+
date: (l.createdAt ?? (raw['created_at'] as string)) || '',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
for (const o of searchResult.observations) {
|
|
200
212
|
const raw = o as Record<string, unknown>;
|
|
213
|
+
// T418: apply agent post-filter when specified
|
|
214
|
+
if (agent) {
|
|
215
|
+
const rowAgent = o.agent ?? (raw['agent'] as string | null) ?? null;
|
|
216
|
+
if (rowAgent !== agent) continue;
|
|
217
|
+
}
|
|
201
218
|
results.push({
|
|
202
219
|
id: o.id,
|
|
203
220
|
type: 'observation',
|
|
@@ -524,7 +541,15 @@ export async function observeBrain(
|
|
|
524
541
|
projectRoot: string,
|
|
525
542
|
params: ObserveBrainParams,
|
|
526
543
|
): Promise<ObserveBrainResult> {
|
|
527
|
-
const {
|
|
544
|
+
const {
|
|
545
|
+
text,
|
|
546
|
+
title: titleParam,
|
|
547
|
+
type: typeParam,
|
|
548
|
+
project,
|
|
549
|
+
sourceSessionId,
|
|
550
|
+
sourceType,
|
|
551
|
+
agent,
|
|
552
|
+
} = params;
|
|
528
553
|
|
|
529
554
|
if (!text || !text.trim()) {
|
|
530
555
|
throw new Error('Observation text is required');
|
|
@@ -588,6 +613,7 @@ export async function observeBrain(
|
|
|
588
613
|
project: project ?? null,
|
|
589
614
|
sourceSessionId: validSessionId,
|
|
590
615
|
sourceType: sourceType ?? 'agent',
|
|
616
|
+
agent: agent ?? null,
|
|
591
617
|
createdAt: now,
|
|
592
618
|
});
|
|
593
619
|
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* @task T136
|
|
11
11
|
* @why Ship vector search out-of-the-box without external API keys
|
|
12
12
|
* @what Local embedding provider using @huggingface/transformers all-MiniLM-L6-v2
|
|
13
|
-
* @remarks
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* @remarks Brain embeddings are a FIRST-CLASS CLEO feature — the transformers
|
|
14
|
+
* package is a regular dependency of `@cleocode/core`, not optional.
|
|
15
|
+
* Migrated from `@xenova/transformers` v2 to `@huggingface/transformers`
|
|
16
|
+
* v4 (upstream rename, same author) which drops the deprecated
|
|
17
|
+
* `prebuild-install` transitive via `sharp@0.34+`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import type { EmbeddingProvider } from './brain-embedding.js';
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
searchLearnings,
|
|
36
36
|
storeLearning,
|
|
37
37
|
} from './learnings.js';
|
|
38
|
+
// T419: async reinforcement queue for mental-model writes (ULTRAPLAN L5)
|
|
39
|
+
import { isMentalModelObservation, mentalModelQueue } from './mental-model-queue.js';
|
|
38
40
|
// BRAIN memory imports (T4770)
|
|
39
41
|
import {
|
|
40
42
|
patternStats,
|
|
@@ -412,6 +414,8 @@ export async function memoryFind(
|
|
|
412
414
|
tables?: string[];
|
|
413
415
|
dateStart?: string;
|
|
414
416
|
dateEnd?: string;
|
|
417
|
+
/** T418: filter results to observations produced by a specific agent. */
|
|
418
|
+
agent?: string;
|
|
415
419
|
},
|
|
416
420
|
projectRoot?: string,
|
|
417
421
|
): Promise<EngineResult> {
|
|
@@ -425,6 +429,7 @@ export async function memoryFind(
|
|
|
425
429
|
| undefined,
|
|
426
430
|
dateStart: params.dateStart,
|
|
427
431
|
dateEnd: params.dateEnd,
|
|
432
|
+
agent: params.agent,
|
|
428
433
|
});
|
|
429
434
|
return { success: true, data: result };
|
|
430
435
|
} catch (error) {
|
|
@@ -536,19 +541,36 @@ export async function memoryObserve(
|
|
|
536
541
|
project?: string;
|
|
537
542
|
sourceSessionId?: string;
|
|
538
543
|
sourceType?: string;
|
|
544
|
+
/** T417: agent provenance — name of the spawned agent producing this observation. */
|
|
545
|
+
agent?: string;
|
|
539
546
|
},
|
|
540
547
|
projectRoot?: string,
|
|
541
548
|
): Promise<EngineResult> {
|
|
542
549
|
try {
|
|
543
550
|
const root = resolveRoot(projectRoot);
|
|
544
|
-
const
|
|
551
|
+
const observeParams: ObserveBrainParams = {
|
|
545
552
|
text: params.text,
|
|
546
553
|
title: params.title,
|
|
547
554
|
type: params.type as ObserveBrainParams['type'],
|
|
548
555
|
project: params.project,
|
|
549
556
|
sourceSessionId: params.sourceSessionId,
|
|
550
557
|
sourceType: params.sourceType as ObserveBrainParams['sourceType'],
|
|
551
|
-
|
|
558
|
+
agent: params.agent,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// T419: route mental-model observations (agent-tagged, relevant type) through
|
|
562
|
+
// the async reinforcement queue for non-blocking writes (ULTRAPLAN L5).
|
|
563
|
+
// All other observations use the existing synchronous path.
|
|
564
|
+
let result: Awaited<ReturnType<typeof observeBrain>>;
|
|
565
|
+
if (isMentalModelObservation(observeParams) && observeParams.agent) {
|
|
566
|
+
result = await mentalModelQueue.enqueue(root, {
|
|
567
|
+
...observeParams,
|
|
568
|
+
agent: observeParams.agent,
|
|
569
|
+
});
|
|
570
|
+
} else {
|
|
571
|
+
result = await observeBrain(root, observeParams);
|
|
572
|
+
}
|
|
573
|
+
|
|
552
574
|
return { success: true, data: result };
|
|
553
575
|
} catch (error) {
|
|
554
576
|
return {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for validate-on-load mental-model injection.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are used by:
|
|
5
|
+
* - The Pi CANT bridge (cleo-cant-bridge.ts) to build the system-prompt block
|
|
6
|
+
* - T421 empirical tests to assert on preamble content without a real Pi runtime
|
|
7
|
+
*
|
|
8
|
+
* No I/O. Safe to call in tests without a real DB or Pi extension context.
|
|
9
|
+
*
|
|
10
|
+
* @task T420
|
|
11
|
+
* @epic T377
|
|
12
|
+
* @wave W8
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/** Minimal observation shape returned by memoryFind / searchBrainCompact. */
|
|
20
|
+
export interface MentalModelObservation {
|
|
21
|
+
/** Brain DB observation ID (O- prefix). */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Observation type: discovery, change, feature, decision, bugfix, refactor, etc. */
|
|
24
|
+
type: string;
|
|
25
|
+
/** Short observation title or truncated text. */
|
|
26
|
+
title: string;
|
|
27
|
+
/** ISO date string for display. */
|
|
28
|
+
date?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Constants
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Preamble text injected into the Pi system prompt when an agent has a
|
|
37
|
+
* `mental_model:` CANT block. The agent MUST re-evaluate each observation
|
|
38
|
+
* against the current project state before acting.
|
|
39
|
+
*
|
|
40
|
+
* Exported so empirical tests (T421) can assert on its presence.
|
|
41
|
+
*/
|
|
42
|
+
export const VALIDATE_ON_LOAD_PREAMBLE =
|
|
43
|
+
'===== MENTAL MODEL (validate-on-load) =====\n' +
|
|
44
|
+
'These are your prior observations, patterns, and learnings for this project.\n' +
|
|
45
|
+
'Before acting, you MUST re-evaluate each entry against current project state.\n' +
|
|
46
|
+
'If an entry is stale, note it and proceed with fresh understanding.';
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Pure helpers
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the validate-on-load mental-model injection string.
|
|
54
|
+
*
|
|
55
|
+
* Pure function — no I/O, safe to call in tests without a real DB.
|
|
56
|
+
*
|
|
57
|
+
* @param agentName - Name of the spawned agent (used in the header line).
|
|
58
|
+
* @param observations - Prior mental-model observations to list.
|
|
59
|
+
* @returns System-prompt block containing the preamble and numbered observations,
|
|
60
|
+
* or an empty string when `observations` is empty.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const block = buildMentalModelInjection('my-agent', [
|
|
65
|
+
* { id: 'O-abc1', type: 'discovery', title: 'Auth uses JWT', date: '2026-04-08' },
|
|
66
|
+
* ]);
|
|
67
|
+
* // block contains VALIDATE_ON_LOAD_PREAMBLE + numbered list
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function buildMentalModelInjection(
|
|
71
|
+
agentName: string,
|
|
72
|
+
observations: MentalModelObservation[],
|
|
73
|
+
): string {
|
|
74
|
+
if (observations.length === 0) return '';
|
|
75
|
+
|
|
76
|
+
const lines: string[] = ['', `// Agent: ${agentName}`, VALIDATE_ON_LOAD_PREAMBLE, ''];
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < observations.length; i++) {
|
|
79
|
+
const obs = observations[i];
|
|
80
|
+
const datePart = obs.date ? ` [${obs.date}]` : '';
|
|
81
|
+
lines.push(`${i + 1}. [${obs.id}] (${obs.type})${datePart}: ${obs.title}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push('===== END MENTAL MODEL =====');
|
|
85
|
+
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async reinforcement queue for non-blocking mental-model writes.
|
|
3
|
+
*
|
|
4
|
+
* ULTRAPLAN L5 compliance: observations tagged with an `agent` provenance and a
|
|
5
|
+
* mental-model-relevant type ('discovery', 'change', 'feature', 'decision') are
|
|
6
|
+
* routed through this queue instead of writing synchronously to brain.db. This
|
|
7
|
+
* decouples the hot path (agent execution) from I/O latency.
|
|
8
|
+
*
|
|
9
|
+
* The queue is drained to brain.db either:
|
|
10
|
+
* 1. Periodically — every {@link FLUSH_INTERVAL_MS} milliseconds via a timer.
|
|
11
|
+
* 2. On high watermark — when the queue exceeds {@link FLUSH_WATERMARK} entries.
|
|
12
|
+
* 3. On process exit — SIGINT, SIGTERM, and 'exit' hooks perform a best-effort
|
|
13
|
+
* synchronous flush so no observations are lost.
|
|
14
|
+
*
|
|
15
|
+
* Observations without an `agent` field continue to use the existing synchronous
|
|
16
|
+
* path in observeBrain() and are never routed here.
|
|
17
|
+
*
|
|
18
|
+
* @task T383/T419
|
|
19
|
+
* @epic T377
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ObserveBrainParams, ObserveBrainResult } from './brain-retrieval.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Drain interval in milliseconds. */
|
|
29
|
+
const FLUSH_INTERVAL_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
/** Drain when queue exceeds this many entries, regardless of timer. */
|
|
32
|
+
const FLUSH_WATERMARK = 50;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Types
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Queued observation entry with its write callback. */
|
|
39
|
+
interface QueuedObservation {
|
|
40
|
+
/** Project root the observation is scoped to. */
|
|
41
|
+
projectRoot: string;
|
|
42
|
+
/** Full observation parameters, including the required `agent` field. */
|
|
43
|
+
params: ObserveBrainParams & { agent: string };
|
|
44
|
+
/** Resolve callback — called with the persisted result after flush. */
|
|
45
|
+
resolve: (result: ObserveBrainResult) => void;
|
|
46
|
+
/** Reject callback — called if the observation cannot be persisted. */
|
|
47
|
+
reject: (err: Error) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Public interface for the mental-model queue singleton.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const q = getMentalModelQueue();
|
|
56
|
+
* await q.enqueue(projectRoot, { text: 'Agent learned X', agent: 'my-agent', ... });
|
|
57
|
+
* const remaining = q.size();
|
|
58
|
+
* await q.flush();
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export interface MentalModelQueue {
|
|
62
|
+
/**
|
|
63
|
+
* Enqueue a mental-model observation for async write.
|
|
64
|
+
*
|
|
65
|
+
* Returns a Promise that resolves with the persisted {@link ObserveBrainResult}
|
|
66
|
+
* once the batch flush runs. Non-blocking for the caller.
|
|
67
|
+
*
|
|
68
|
+
* @param projectRoot - Project root directory for the brain.db path.
|
|
69
|
+
* @param params - Observation parameters. MUST include `agent`.
|
|
70
|
+
*/
|
|
71
|
+
enqueue(
|
|
72
|
+
projectRoot: string,
|
|
73
|
+
params: ObserveBrainParams & { agent: string },
|
|
74
|
+
): Promise<ObserveBrainResult>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Drain the queue immediately.
|
|
78
|
+
*
|
|
79
|
+
* Writes all pending observations to brain.db.
|
|
80
|
+
* Safe to call concurrently — duplicate calls are serialised internally.
|
|
81
|
+
*
|
|
82
|
+
* @returns The number of observations successfully drained.
|
|
83
|
+
*/
|
|
84
|
+
flush(): Promise<number>;
|
|
85
|
+
|
|
86
|
+
/** Current number of pending observations in the queue. */
|
|
87
|
+
size(): number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Observation types that route through the mental-model queue.
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Observation types that are considered mental-model relevant and therefore
|
|
96
|
+
* eligible for async queuing when produced by a named agent.
|
|
97
|
+
*/
|
|
98
|
+
const MENTAL_MODEL_TYPES = new Set<string>([
|
|
99
|
+
'discovery',
|
|
100
|
+
'change',
|
|
101
|
+
'feature',
|
|
102
|
+
'decision',
|
|
103
|
+
'bugfix',
|
|
104
|
+
'refactor',
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Queue implementation
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/** In-memory queue of pending mental-model observations. */
|
|
112
|
+
const _queue: QueuedObservation[] = [];
|
|
113
|
+
|
|
114
|
+
/** Whether a flush is currently in progress (prevents re-entrant flushes). */
|
|
115
|
+
let _flushing = false;
|
|
116
|
+
|
|
117
|
+
/** Whether process-exit hooks have been registered. */
|
|
118
|
+
let _hooksRegistered = false;
|
|
119
|
+
|
|
120
|
+
/** Handle for the periodic flush timer (undefined = no active timer). */
|
|
121
|
+
let _timer: ReturnType<typeof setInterval> | undefined;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Drain the queue synchronously where possible, or fall back to async writes.
|
|
125
|
+
* Returns when all observations in the current batch have been persisted.
|
|
126
|
+
*/
|
|
127
|
+
async function drainQueue(): Promise<number> {
|
|
128
|
+
if (_queue.length === 0) return 0;
|
|
129
|
+
|
|
130
|
+
// Snapshot current batch and clear the queue
|
|
131
|
+
const batch = _queue.splice(0, _queue.length);
|
|
132
|
+
let count = 0;
|
|
133
|
+
|
|
134
|
+
// Import observeBrain lazily to avoid circular dependencies at module load
|
|
135
|
+
const { observeBrain } = await import('./brain-retrieval.js');
|
|
136
|
+
|
|
137
|
+
for (const entry of batch) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await observeBrain(entry.projectRoot, entry.params);
|
|
140
|
+
entry.resolve(result);
|
|
141
|
+
count++;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
144
|
+
entry.reject(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return count;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Best-effort synchronous exit flush.
|
|
153
|
+
* Used in 'exit' event handler where async I/O is not guaranteed.
|
|
154
|
+
* Falls back to fire-and-forget if the environment does not support
|
|
155
|
+
* synchronous-style promises (i.e., in environments where the event
|
|
156
|
+
* loop may already be draining).
|
|
157
|
+
*/
|
|
158
|
+
function exitFlush(): void {
|
|
159
|
+
if (_queue.length === 0) return;
|
|
160
|
+
// We can't reliably await in a synchronous exit handler. The best we
|
|
161
|
+
// can do is trigger the drain and hope the pending writes complete.
|
|
162
|
+
drainQueue().catch(() => {
|
|
163
|
+
// Silently swallow — process is terminating anyway
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Register process-exit hooks once.
|
|
169
|
+
* Ensures that no observations are silently dropped on graceful shutdown.
|
|
170
|
+
*/
|
|
171
|
+
function registerExitHooks(): void {
|
|
172
|
+
if (_hooksRegistered) return;
|
|
173
|
+
_hooksRegistered = true;
|
|
174
|
+
|
|
175
|
+
process.on('exit', exitFlush);
|
|
176
|
+
|
|
177
|
+
process.once('SIGINT', () => {
|
|
178
|
+
drainQueue()
|
|
179
|
+
.catch(() => {
|
|
180
|
+
/* best-effort */
|
|
181
|
+
})
|
|
182
|
+
.finally(() => {
|
|
183
|
+
process.exit(130); // 128 + SIGINT
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
process.once('SIGTERM', () => {
|
|
188
|
+
drainQueue()
|
|
189
|
+
.catch(() => {
|
|
190
|
+
/* best-effort */
|
|
191
|
+
})
|
|
192
|
+
.finally(() => {
|
|
193
|
+
process.exit(143); // 128 + SIGTERM
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Start the periodic flush timer if it isn't already running.
|
|
200
|
+
*/
|
|
201
|
+
function ensureTimer(): void {
|
|
202
|
+
if (_timer !== undefined) return;
|
|
203
|
+
_timer = setInterval(() => {
|
|
204
|
+
if (_queue.length === 0) return;
|
|
205
|
+
if (_flushing) return;
|
|
206
|
+
_flushing = true;
|
|
207
|
+
drainQueue()
|
|
208
|
+
.catch(() => {
|
|
209
|
+
/* best-effort */
|
|
210
|
+
})
|
|
211
|
+
.finally(() => {
|
|
212
|
+
_flushing = false;
|
|
213
|
+
});
|
|
214
|
+
}, FLUSH_INTERVAL_MS);
|
|
215
|
+
// Unref so the timer doesn't prevent process exit when queue is idle
|
|
216
|
+
if (typeof _timer.unref === 'function') {
|
|
217
|
+
_timer.unref();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Public singleton
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Mental-model queue singleton.
|
|
227
|
+
*
|
|
228
|
+
* Use this instead of calling observeBrain() directly when writing agent-tagged
|
|
229
|
+
* observations that should be queued for async persistence (ULTRAPLAN L5).
|
|
230
|
+
*/
|
|
231
|
+
export const mentalModelQueue: MentalModelQueue = {
|
|
232
|
+
enqueue(
|
|
233
|
+
projectRoot: string,
|
|
234
|
+
params: ObserveBrainParams & { agent: string },
|
|
235
|
+
): Promise<ObserveBrainResult> {
|
|
236
|
+
registerExitHooks();
|
|
237
|
+
ensureTimer();
|
|
238
|
+
|
|
239
|
+
return new Promise<ObserveBrainResult>((resolve, reject) => {
|
|
240
|
+
_queue.push({ projectRoot, params, resolve, reject });
|
|
241
|
+
|
|
242
|
+
// High-watermark flush
|
|
243
|
+
if (_queue.length >= FLUSH_WATERMARK && !_flushing) {
|
|
244
|
+
_flushing = true;
|
|
245
|
+
drainQueue()
|
|
246
|
+
.catch(() => {
|
|
247
|
+
/* best-effort */
|
|
248
|
+
})
|
|
249
|
+
.finally(() => {
|
|
250
|
+
_flushing = false;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async flush(): Promise<number> {
|
|
257
|
+
if (_flushing) {
|
|
258
|
+
// Wait for the current flush to settle then flush again
|
|
259
|
+
await new Promise<void>((r) => setTimeout(r, 50));
|
|
260
|
+
}
|
|
261
|
+
_flushing = true;
|
|
262
|
+
try {
|
|
263
|
+
return await drainQueue();
|
|
264
|
+
} finally {
|
|
265
|
+
_flushing = false;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
size(): number {
|
|
270
|
+
return _queue.length;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Helpers
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Determine whether an observation should be routed through the mental-model
|
|
280
|
+
* queue rather than written synchronously.
|
|
281
|
+
*
|
|
282
|
+
* Returns `true` when the observation has a non-empty `agent` field AND a
|
|
283
|
+
* mental-model-relevant type.
|
|
284
|
+
*
|
|
285
|
+
* @param params - Observation parameters to evaluate.
|
|
286
|
+
*/
|
|
287
|
+
export function isMentalModelObservation(params: ObserveBrainParams): boolean {
|
|
288
|
+
if (!params.agent) return false;
|
|
289
|
+
const type = params.type ?? 'discovery';
|
|
290
|
+
return MENTAL_MODEL_TYPES.has(type);
|
|
291
|
+
}
|