opcode-pg-memory 2.2.8 → 2.3.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 (76) hide show
  1. package/dist/cli.js +232 -214
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +30 -21006
  5. package/dist/index.js.map +1 -0
  6. package/dist/mcp-server.js +319 -302
  7. package/dist/mcp-server.js.map +1 -0
  8. package/dist/src/cache/semantic-cache.js +399 -0
  9. package/dist/src/cache/semantic-cache.js.map +1 -0
  10. package/dist/src/cli.js +404 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/config.d.ts +5 -0
  13. package/dist/src/config.d.ts.map +1 -1
  14. package/dist/src/config.js +89 -0
  15. package/dist/src/config.js.map +1 -0
  16. package/dist/src/db/init-db.js +545 -0
  17. package/dist/src/db/init-db.js.map +1 -0
  18. package/dist/src/hooks/message-part-updated.js +203 -0
  19. package/dist/src/hooks/message-part-updated.js.map +1 -0
  20. package/dist/src/hooks/message-updated.js +347 -0
  21. package/dist/src/hooks/message-updated.js.map +1 -0
  22. package/dist/src/hooks/session-compacting.js +179 -0
  23. package/dist/src/hooks/session-compacting.js.map +1 -0
  24. package/dist/src/hooks/session-completed.js +337 -0
  25. package/dist/src/hooks/session-completed.js.map +1 -0
  26. package/dist/src/hooks/session-created.js +206 -0
  27. package/dist/src/hooks/session-created.js.map +1 -0
  28. package/dist/src/hooks/tool-execute.js +267 -0
  29. package/dist/src/hooks/tool-execute.js.map +1 -0
  30. package/dist/src/index.d.ts +1 -0
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/index.js +642 -0
  33. package/dist/src/index.js.map +1 -0
  34. package/dist/src/mcp/hindsight-reflect-omo.js +318 -0
  35. package/dist/src/mcp/hindsight-reflect-omo.js.map +1 -0
  36. package/dist/src/mcp/hindsight-reflect.js +838 -0
  37. package/dist/src/mcp/hindsight-reflect.js.map +1 -0
  38. package/dist/src/mcp/recall-memory-omo.js +263 -0
  39. package/dist/src/mcp/recall-memory-omo.js.map +1 -0
  40. package/dist/src/mcp/recall-memory.d.ts +6 -0
  41. package/dist/src/mcp/recall-memory.d.ts.map +1 -1
  42. package/dist/src/mcp/recall-memory.js +900 -0
  43. package/dist/src/mcp/recall-memory.js.map +1 -0
  44. package/dist/src/omo/adapter.js +583 -0
  45. package/dist/src/omo/adapter.js.map +1 -0
  46. package/dist/src/omo/types.js +44 -0
  47. package/dist/src/omo/types.js.map +1 -0
  48. package/dist/src/services/db-polling.d.ts +33 -0
  49. package/dist/src/services/db-polling.d.ts.map +1 -0
  50. package/dist/src/services/db-polling.js +104 -0
  51. package/dist/src/services/db-polling.js.map +1 -0
  52. package/dist/src/services/event-synchronizer.d.ts +15 -0
  53. package/dist/src/services/event-synchronizer.d.ts.map +1 -0
  54. package/dist/src/services/event-synchronizer.js +119 -0
  55. package/dist/src/services/event-synchronizer.js.map +1 -0
  56. package/dist/src/services/keyword.js +29 -0
  57. package/dist/src/services/keyword.js.map +1 -0
  58. package/dist/src/services/logger.js +42 -0
  59. package/dist/src/services/logger.js.map +1 -0
  60. package/dist/src/services/opencode-schema-adapter.d.ts +100 -0
  61. package/dist/src/services/opencode-schema-adapter.d.ts.map +1 -0
  62. package/dist/src/services/opencode-schema-adapter.js +192 -0
  63. package/dist/src/services/opencode-schema-adapter.js.map +1 -0
  64. package/dist/src/services/privacy.js +23 -0
  65. package/dist/src/services/privacy.js.map +1 -0
  66. package/dist/src/topic/segment-manager.js +447 -0
  67. package/dist/src/topic/segment-manager.js.map +1 -0
  68. package/dist/src/types.d.ts +20 -2
  69. package/dist/src/types.d.ts.map +1 -1
  70. package/dist/src/types.js +8 -0
  71. package/dist/src/types.js.map +1 -0
  72. package/dist/src/utils/embedding.js +180 -0
  73. package/dist/src/utils/embedding.js.map +1 -0
  74. package/dist/src/utils/token-budget.js +152 -0
  75. package/dist/src/utils/token-budget.js.map +1 -0
  76. package/package.json +5 -4
@@ -0,0 +1,838 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_AGGREGATE_USER_PROMPT = exports.DEFAULT_PER_SEGMENT_USER_PROMPT = exports.DEFAULT_REFLECTION_SYSTEM_PROMPT = void 0;
4
+ exports.hindsightReflect = hindsightReflect;
5
+ exports.isOffPeakHour = isOffPeakHour;
6
+ exports.getReflectionStats = getReflectionStats;
7
+ const logger_1 = require("../services/logger");
8
+ const logger = (0, logger_1.createLogger)('hindsight-reflect');
9
+ // ============================================================
10
+ // Configurable prompts (not hardcoded — passed via config)
11
+ // Must be declared BEFORE DEFAULT_CONFIG which references them
12
+ // ============================================================
13
+ exports.DEFAULT_REFLECTION_SYSTEM_PROMPT = `You are a reflection engine that analyzes coding session observations to extract reusable patterns and insights.
14
+
15
+ ## Task
16
+ Analyze the provided observations from a coding session and generate structured reflections that capture:
17
+ 1. Error patterns and their root causes
18
+ 2. Successful approaches and best practices
19
+ 3. User preferences and coding style
20
+ 4. Technical stack patterns
21
+ 5. Cross-session applicable insights
22
+
23
+ ## Input Format
24
+ Observations will be provided as a JSON array with fields:
25
+ - id: Observation identifier
26
+ - tool_name: The tool that was used
27
+ - tool_input_summary: Summary of inputs
28
+ - tool_output_summary: Summary of outputs
29
+ - importance: Importance rating (1-5)
30
+ - created_at: Timestamp
31
+ - metadata: Additional metadata
32
+
33
+ ## Output Format
34
+ Respond with a JSON object containing:
35
+ {
36
+ "summary": "High-level summary of the session insights (2-3 sentences)",
37
+ "patterns": [
38
+ {
39
+ "pattern_type": "error_pattern|success_pattern|preference|technical_stack|workflow|tool_preference|insight",
40
+ "description": "Detailed description of the pattern (1-2 sentences)",
41
+ "confidence": 0.0-1.0,
42
+ "source_observation_ids": ["id1", "id2"],
43
+ "applicability": "When this pattern applies"
44
+ }
45
+ ],
46
+ "recommendations": [
47
+ "Actionable recommendation based on patterns"
48
+ ],
49
+ "technical_stack": {
50
+ "languages": ["detected languages"],
51
+ "frameworks": ["detected frameworks"],
52
+ "tools": ["frequently used tools"]
53
+ }
54
+ }
55
+
56
+ ## Rules
57
+ - Only include patterns with confidence >= 0.6
58
+ - Focus on cross-session applicable insights
59
+ - Be specific about technical details (file names, function names, etc.)
60
+ - Avoid generic advice like "write clean code"
61
+ - Consider the sequence of observations for causal relationships
62
+ - Identify recurring errors and their solutions
63
+ - Note user preferences (indentation style, naming conventions, etc.)
64
+
65
+ ## Pattern Types
66
+ - error_pattern: Recurring errors and how they were resolved
67
+ - success_pattern: Approaches that worked well
68
+ - preference: User's coding style and preferences
69
+ - technical_stack: Technologies and tools used
70
+ - workflow: Development workflow patterns
71
+ - tool_preference: Frequently used tools and preferences
72
+ - insight: Notable technical discoveries or lessons`;
73
+ exports.DEFAULT_PER_SEGMENT_USER_PROMPT = `You are a technical retrospective assistant. Analyze the following tool execution records from topic: "{topic_summary}", and summarize:
74
+ 1. Recurring patterns or workflows
75
+ 2. Notable technical discoveries or lessons
76
+ 3. Recommendations for future similar tasks
77
+
78
+ Session context: {session_context}
79
+
80
+ Observations:
81
+ {observations_json}`;
82
+ exports.DEFAULT_AGGREGATE_USER_PROMPT = `You are a technical retrospective assistant. Analyze the following tool execution records across multiple topics and sessions, and summarize:
83
+ 1. Cross-topic recurring patterns or workflows
84
+ 2. Notable technical discoveries or lessons that span topics
85
+ 3. Recommendations for future similar tasks
86
+
87
+ Topics covered: {topics_summary}
88
+
89
+ Observations:
90
+ {observations_json}`;
91
+ const DEFAULT_CONFIG = {
92
+ observationThreshold: 30,
93
+ segmentThreshold: 10,
94
+ minThreshold: 30,
95
+ maxThreshold: 50,
96
+ modelSize: '7b',
97
+ offPeakHours: [1, 2, 3, 4, 5],
98
+ minConfidence: 0.6,
99
+ maxObservationsPerSegment: 100,
100
+ maxObservationsAggregate: 200,
101
+ reflectionBatchSize: 10,
102
+ prompts: {
103
+ systemPrompt: exports.DEFAULT_REFLECTION_SYSTEM_PROMPT,
104
+ perSegmentUserPrompt: exports.DEFAULT_PER_SEGMENT_USER_PROMPT,
105
+ aggregateUserPrompt: exports.DEFAULT_AGGREGATE_USER_PROMPT,
106
+ },
107
+ };
108
+ // ============================================================
109
+ // MCP Tool: hindsight_reflect
110
+ // ============================================================
111
+ /**
112
+ * Enhanced hindsight_reflect with topic_segment-based reflection,
113
+ * omo_task_id support, and aggregate mode.
114
+ *
115
+ * Scope resolution:
116
+ * 1. topic_segment_id → reflect on that single segment
117
+ * 2. session_id + aggregate=false → group by topic_segment, reflect per segment
118
+ * 3. session_id + aggregate=true → reflect on all observations across all segments
119
+ * 4. omo_task_id → find all session_map entries for this task, reflect across their segments
120
+ *
121
+ * Backward compatible: session_id-only input falls back to legacy path
122
+ * (sessions table + session-scoped reflection).
123
+ */
124
+ async function hindsightReflect(input, pool, config = {}) {
125
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
126
+ const startTime = Date.now();
127
+ let totalTokens = { input: 0, output: 0, total: 0 };
128
+ logger.info(`hindsight_reflect called: session=${input.session_id || 'none'}, ` +
129
+ `omo_task=${input.omo_task_id || 'none'}, ` +
130
+ `topic_segment=${input.topic_segment_id || 'none'}, ` +
131
+ `aggregate=${input.aggregate ?? false}`);
132
+ try {
133
+ // ── Step 1: Resolve target scope ──────────────────────────────────
134
+ const scope = await resolveScope(input, pool);
135
+ if (!scope.observable) {
136
+ logger.info('hindsight_reflect: no observations available for scope');
137
+ return {
138
+ generated_reflections: [],
139
+ token_usage: totalTokens,
140
+ duration_ms: Date.now() - startTime,
141
+ };
142
+ }
143
+ // ── Step 2: Collect observations ──────────────────────────────────
144
+ const observations = await collectObservations(input, scope, mergedConfig, pool);
145
+ if (observations.length === 0) {
146
+ logger.info('hindsight_reflect: zero observations collected');
147
+ return {
148
+ generated_reflections: [],
149
+ token_usage: totalTokens,
150
+ duration_ms: Date.now() - startTime,
151
+ };
152
+ }
153
+ logger.info(`Fetched ${observations.length} observations for reflection`);
154
+ // ── Threshold check (skip if below threshold and not manual) ──────
155
+ const threshold = mergedConfig.observationThreshold;
156
+ if (observations.length < threshold && input.trigger_type !== 'manual') {
157
+ logger.info(`hindsight_reflect: observation count (${observations.length}) ` +
158
+ `below threshold (${threshold}), skipping`);
159
+ return {
160
+ generated_reflections: [],
161
+ token_usage: totalTokens,
162
+ duration_ms: Date.now() - startTime,
163
+ };
164
+ }
165
+ // ── Step 3: Group observations by topic_segment ───────────────────
166
+ const shouldAggregate = input.aggregate === true ||
167
+ (!!input.omo_task_id && input.aggregate !== false); // omo_task_id defaults to aggregate
168
+ const segments = groupObservationsBySegment(observations, shouldAggregate);
169
+ logger.info(`Grouped into ${segments.length} segment(s) ` +
170
+ `(aggregate=${shouldAggregate})`);
171
+ // ── Step 4: Reflect on each segment group ─────────────────────────
172
+ const generatedReflections = [];
173
+ for (const segment of segments) {
174
+ const segmentReflections = await reflectOnSegment(segment, scope, input, mergedConfig, pool);
175
+ generatedReflections.push(...segmentReflections);
176
+ // Track token usage (approximate from LLM call within reflectOnSegment)
177
+ // The actual token counts would come from the LLM API response
178
+ totalTokens.input += segment.observations.length * 50; // rough estimate
179
+ totalTokens.output += segmentReflections.length * 100; // rough estimate
180
+ }
181
+ // ── Step 7: Update session_map.reflection_last_at ─────────────────
182
+ await updateReflectionTimestamp(scope, pool);
183
+ const elapsed = Date.now() - startTime;
184
+ logger.info(`hindsight_reflect completed: ` +
185
+ `${generatedReflections.length} reflections in ${elapsed}ms`);
186
+ return {
187
+ generated_reflections: generatedReflections,
188
+ token_usage: totalTokens,
189
+ duration_ms: elapsed,
190
+ };
191
+ }
192
+ catch (error) {
193
+ const elapsed = Date.now() - startTime;
194
+ logger.error('hindsight_reflect error:', error);
195
+ // Record error in reflection_errors table
196
+ await logReflectionError(input, error, pool);
197
+ return {
198
+ generated_reflections: [],
199
+ token_usage: totalTokens,
200
+ duration_ms: elapsed,
201
+ };
202
+ }
203
+ }
204
+ async function resolveScope(input, pool) {
205
+ const scope = {
206
+ observable: false,
207
+ sessionInternalIds: [],
208
+ sessionMapIds: [],
209
+ opencodeSessionIds: [],
210
+ topicSegmentId: input.topic_segment_id || null,
211
+ usesNewSchema: false,
212
+ omoTaskId: input.omo_task_id || null,
213
+ };
214
+ // Check if session_map table exists (new schema detection)
215
+ const hasSessionMap = await tableExists(pool, 'session_map');
216
+ const hasTopicSegments = await tableExists(pool, 'topic_segments');
217
+ scope.usesNewSchema = hasSessionMap && hasTopicSegments;
218
+ // ── Path A: omo_task_id → find all session_map entries ──────────
219
+ if (input.omo_task_id && scope.usesNewSchema) {
220
+ const smResult = await pool.query(`SELECT id, opencode_session_id FROM session_map WHERE omo_task_id = $1`, [input.omo_task_id]);
221
+ if (smResult.rows.length > 0) {
222
+ scope.sessionMapIds = smResult.rows.map((r) => r.id);
223
+ scope.opencodeSessionIds = smResult.rows.map((r) => r.opencode_session_id);
224
+ scope.observable = true;
225
+ }
226
+ return scope;
227
+ }
228
+ // ── Path B: session_id (backward compat or new schema) ─────────
229
+ if (input.session_id) {
230
+ if (scope.usesNewSchema) {
231
+ // Look up in session_map
232
+ const smResult = await pool.query(`SELECT id, opencode_session_id FROM session_map WHERE opencode_session_id = $1`, [input.session_id]);
233
+ if (smResult.rows.length > 0) {
234
+ scope.sessionMapIds = smResult.rows.map((r) => r.id);
235
+ scope.opencodeSessionIds = [input.session_id];
236
+ scope.observable = true;
237
+ return scope;
238
+ }
239
+ }
240
+ // Legacy fallback: look up in sessions table
241
+ const sessResult = await pool.query(`SELECT id FROM sessions WHERE external_id = $1`, [input.session_id]);
242
+ if (sessResult.rows.length > 0) {
243
+ scope.sessionInternalIds = sessResult.rows.map((r) => r.id);
244
+ scope.opencodeSessionIds = [input.session_id];
245
+ scope.usesNewSchema = false;
246
+ scope.observable = true;
247
+ return scope;
248
+ }
249
+ }
250
+ // ── Path C: topic_segment_id only ──────────────────────────────
251
+ if (input.topic_segment_id && scope.usesNewSchema) {
252
+ // Verify the segment exists
253
+ const tsResult = await pool.query(`SELECT id, session_map_id FROM topic_segments WHERE id = $1`, [input.topic_segment_id]);
254
+ if (tsResult.rows.length > 0) {
255
+ scope.sessionMapIds = [tsResult.rows[0].session_map_id];
256
+ scope.topicSegmentId = input.topic_segment_id;
257
+ scope.observable = true;
258
+ }
259
+ return scope;
260
+ }
261
+ return scope;
262
+ }
263
+ async function collectObservations(input, scope, config, pool) {
264
+ // ── Path A: Single topic_segment ────────────────────────────────
265
+ if (scope.topicSegmentId && scope.usesNewSchema) {
266
+ try {
267
+ return await collectObservationsForSegment(scope.topicSegmentId, config, pool);
268
+ }
269
+ catch (err) {
270
+ logger.warn('New-schema segment collection failed, falling back:', err);
271
+ }
272
+ }
273
+ // ── Path B: omo_task_id across multiple session_maps ────────────
274
+ if (scope.omoTaskId && scope.usesNewSchema && scope.sessionMapIds.length > 0) {
275
+ try {
276
+ return await collectObservationsForOmoTask(scope.omoTaskId, config, pool);
277
+ }
278
+ catch (err) {
279
+ logger.warn('New-schema omo_task collection failed, falling back:', err);
280
+ }
281
+ }
282
+ // ── Path C: session_map(s) with topic_segments ─────────────────
283
+ if (scope.usesNewSchema && scope.sessionMapIds.length > 0) {
284
+ try {
285
+ return await collectObservationsForSessionMaps(scope.sessionMapIds, config, pool);
286
+ }
287
+ catch (err) {
288
+ logger.warn('New-schema session_map collection failed, falling back:', err);
289
+ }
290
+ }
291
+ // ── Path D: Legacy sessions table ──────────────────────────────
292
+ return collectObservationsLegacy(scope.sessionInternalIds, config, pool);
293
+ }
294
+ /** Collect observations for a single topic_segment */
295
+ async function collectObservationsForSegment(topicSegmentId, config, pool) {
296
+ const result = await pool.query(`SELECT o.*, ts.id as segment_id, ts.summary as topic_summary
297
+ FROM observations o
298
+ JOIN topic_segments ts ON o.topic_segment_id = ts.id
299
+ WHERE o.topic_segment_id = $1
300
+ ORDER BY o.importance DESC, o.created_at DESC
301
+ LIMIT $2`, [topicSegmentId, config.maxObservationsPerSegment]);
302
+ return result.rows.map(mapEnrichedObservation);
303
+ }
304
+ /** Collect observations for an omo_task_id across all session_maps */
305
+ async function collectObservationsForOmoTask(omoTaskId, config, pool) {
306
+ const result = await pool.query(`SELECT o.*, ts.id as segment_id, ts.summary as topic_summary,
307
+ sm.opencode_session_id
308
+ FROM observations o
309
+ JOIN session_map sm ON o.session_map_id = sm.id
310
+ JOIN topic_segments ts ON o.topic_segment_id = ts.id
311
+ WHERE sm.omo_task_id = $1
312
+ ORDER BY o.importance DESC
313
+ LIMIT $2`, [omoTaskId, config.maxObservationsAggregate]);
314
+ return result.rows.map(mapEnrichedObservation);
315
+ }
316
+ /** Collect observations for specific session_map IDs (new schema) */
317
+ async function collectObservationsForSessionMaps(sessionMapIds, config, pool) {
318
+ const result = await pool.query(`SELECT o.*, ts.id as segment_id, ts.summary as topic_summary,
319
+ sm.opencode_session_id, sm.id as sm_id
320
+ FROM observations o
321
+ JOIN session_map sm ON o.session_map_id = sm.id
322
+ LEFT JOIN topic_segments ts ON o.topic_segment_id = ts.id
323
+ WHERE o.session_map_id = ANY($1::uuid[])
324
+ ORDER BY o.importance DESC, o.created_at DESC
325
+ LIMIT $2`, [sessionMapIds, config.maxObservationsAggregate]);
326
+ return result.rows.map((row) => ({
327
+ ...mapEnrichedObservation(row),
328
+ session_map_id: row.sm_id || row.session_map_id,
329
+ }));
330
+ }
331
+ /** Legacy path: observations from sessions table (no topic_segments) */
332
+ async function collectObservationsLegacy(sessionInternalIds, config, pool) {
333
+ const query = `
334
+ SELECT id, tool_name, tool_input_summary, tool_output_summary,
335
+ importance, created_at, metadata
336
+ FROM observations
337
+ WHERE session_id = ANY($1::uuid[])
338
+ ORDER BY importance DESC, created_at DESC
339
+ LIMIT $2
340
+ `;
341
+ const result = await pool.query(query, [
342
+ sessionInternalIds,
343
+ config.maxObservationsAggregate,
344
+ ]);
345
+ return result.rows.map((row) => ({
346
+ id: row.id,
347
+ tool_name: row.tool_name || '',
348
+ tool_input_summary: row.tool_input_summary || '',
349
+ tool_output_summary: row.tool_output_summary || '',
350
+ importance: row.importance,
351
+ created_at: row.created_at,
352
+ metadata: row.metadata || {},
353
+ }));
354
+ }
355
+ function mapEnrichedObservation(row) {
356
+ return {
357
+ id: row.id,
358
+ tool_name: row.tool_name || '',
359
+ tool_input_summary: row.tool_input_summary || '',
360
+ tool_output_summary: row.tool_output_summary || '',
361
+ importance: row.importance ?? 3,
362
+ created_at: row.created_at,
363
+ metadata: row.metadata || {},
364
+ segment_id: row.segment_id,
365
+ topic_summary: row.topic_summary,
366
+ opencode_session_id: row.opencode_session_id,
367
+ session_map_id: row.session_map_id,
368
+ };
369
+ }
370
+ function groupObservationsBySegment(observations, aggregate) {
371
+ if (aggregate || observations.length === 0) {
372
+ // Single aggregate group spanning all segments
373
+ return [
374
+ {
375
+ segmentId: '__aggregate__',
376
+ topicSummary: collectAllTopicSummaries(observations),
377
+ observations,
378
+ opencodeSessionIds: dedupe(observations.map((o) => o.opencode_session_id).filter(Boolean)),
379
+ sessionMapIds: dedupe(observations.map((o) => o.session_map_id).filter(Boolean)),
380
+ },
381
+ ];
382
+ }
383
+ // Group by segment_id; fallback to a single unnamed group if no segments
384
+ const groups = new Map();
385
+ for (const obs of observations) {
386
+ const key = obs.segment_id || '__no_segment__';
387
+ if (!groups.has(key))
388
+ groups.set(key, []);
389
+ groups.get(key).push(obs);
390
+ }
391
+ const result = [];
392
+ for (const [segmentId, obs] of groups) {
393
+ result.push({
394
+ segmentId,
395
+ topicSummary: obs[0]?.topic_summary ||
396
+ (segmentId === '__no_segment__' ? 'Unsegmented observations' : `Segment ${segmentId}`),
397
+ observations: obs,
398
+ opencodeSessionIds: dedupe(obs.map((o) => o.opencode_session_id).filter(Boolean)),
399
+ sessionMapIds: dedupe(obs.map((o) => o.session_map_id).filter(Boolean)),
400
+ });
401
+ }
402
+ return result;
403
+ }
404
+ function collectAllTopicSummaries(observations) {
405
+ const summaries = dedupe(observations.map((o) => o.topic_summary).filter(Boolean));
406
+ return summaries.length > 0 ? summaries.join('; ') : 'Cross-topic aggregate';
407
+ }
408
+ // ============================================================
409
+ // Step 4: Reflect on each segment
410
+ // ============================================================
411
+ async function reflectOnSegment(segment, scope, input, config, pool) {
412
+ const reflections = [];
413
+ // Sort by importance, take top N
414
+ const sorted = [...segment.observations]
415
+ .sort((a, b) => b.importance - a.importance)
416
+ .slice(0, config.maxObservationsPerSegment);
417
+ // Batch into groups of reflectionBatchSize
418
+ const batches = [];
419
+ for (let i = 0; i < sorted.length; i += config.reflectionBatchSize) {
420
+ batches.push(sorted.slice(i, i + config.reflectionBatchSize));
421
+ }
422
+ for (const batch of batches) {
423
+ // Build context-aware prompt (used when real LLM integration is active)
424
+ const prompt = buildReflectionPrompt(segment, batch, config, input);
425
+ // Call LLM (with heuristic fallback)
426
+ const llmResult = await performReflectionWithLLM(batch, segment.topicSummary, config, prompt);
427
+ // Convert patterns to Reflection records and store
428
+ for (const pattern of llmResult.patterns) {
429
+ if (pattern.confidence >= config.minConfidence) {
430
+ const reflection = await storeReflection(pattern, segment, scope, pool, config, input);
431
+ if (reflection) {
432
+ reflections.push(reflection);
433
+ }
434
+ }
435
+ }
436
+ }
437
+ return reflections;
438
+ }
439
+ /**
440
+ * Build a reflection prompt with topic context.
441
+ * Prompts are configurable via HindsightReflectConfig.prompts.
442
+ */
443
+ function buildReflectionPrompt(segment, batch, config, _input) {
444
+ const isAggregate = segment.segmentId === '__aggregate__';
445
+ const template = isAggregate
446
+ ? config.prompts.aggregateUserPrompt
447
+ : config.prompts.perSegmentUserPrompt;
448
+ const observationsJson = JSON.stringify(batch.map((o) => ({
449
+ id: o.id,
450
+ tool_name: o.tool_name,
451
+ tool_input_summary: o.tool_input_summary,
452
+ tool_output_summary: o.tool_output_summary,
453
+ importance: o.importance,
454
+ metadata: o.metadata,
455
+ })));
456
+ const sessionContext = segment.opencodeSessionIds.length > 0
457
+ ? segment.opencodeSessionIds.join(', ')
458
+ : 'unknown';
459
+ return template
460
+ .replace('{topic_summary}', segment.topicSummary)
461
+ .replace('{session_context}', sessionContext)
462
+ .replace('{topics_summary}', segment.topicSummary)
463
+ .replace('{observations_json}', observationsJson);
464
+ }
465
+ async function performReflectionWithLLM(observations, topicSummary, config, prompt) {
466
+ logger.info(`Performing reflection with ${config.modelSize} model ` +
467
+ `on ${observations.length} observations (topic: ${topicSummary})`);
468
+ // In production, this would call an LLM API (OpenAI, DeepSeek, etc.)
469
+ // using the provided prompt built from configurable templates.
470
+ // Example:
471
+ // const response = await openai.chat.completions.create({
472
+ // model: config.modelSize === '7b' ? 'gpt-4o-mini' : 'gpt-4o',
473
+ // messages: [
474
+ // { role: 'system', content: config.prompts.systemPrompt },
475
+ // { role: 'user', content: prompt },
476
+ // ],
477
+ // response_format: { type: 'json_object' },
478
+ // });
479
+ // return parseLLMResponse(response);
480
+ //
481
+ // For now, use heuristic rule-based reflection as fallback.
482
+ void prompt; // used when LLM integration is activated
483
+ return performHeuristicReflection(observations, topicSummary);
484
+ }
485
+ /**
486
+ * Heuristic fallback when LLM is unavailable.
487
+ * Detects basic patterns:
488
+ * - error_pattern: observations with importance >= 4 or containing "error"
489
+ * - tool_preference: frequently used tools (>= 5 uses)
490
+ * - success_pattern: observations with "success" or "completed"
491
+ */
492
+ function performHeuristicReflection(observations, topicSummary) {
493
+ const patterns = [];
494
+ const recommendations = [];
495
+ // 1. Error pattern detection
496
+ const errorObs = observations.filter((obs) => obs.importance >= 4 ||
497
+ obs.tool_output_summary?.toLowerCase().includes('error') ||
498
+ obs.tool_output_summary?.toLowerCase().includes('exception') ||
499
+ obs.tool_output_summary?.toLowerCase().includes('failed'));
500
+ if (errorObs.length >= 2) {
501
+ patterns.push({
502
+ pattern_type: 'error_pattern',
503
+ description: `[${topicSummary}] Encountered ${errorObs.length} error situations. Review error handling patterns.`,
504
+ confidence: 0.75,
505
+ source_observation_ids: errorObs.map((o) => o.id).slice(0, 5),
506
+ applicability: 'Future error-prone operations',
507
+ });
508
+ recommendations.push(`[${topicSummary}] Consider adding more robust error handling and validation.`);
509
+ }
510
+ // 2. Tool preference detection (>= 5 uses)
511
+ const toolUsage = {};
512
+ for (const obs of observations) {
513
+ if (obs.tool_name) {
514
+ if (!toolUsage[obs.tool_name]) {
515
+ toolUsage[obs.tool_name] = { count: 0, ids: [] };
516
+ }
517
+ toolUsage[obs.tool_name].count++;
518
+ toolUsage[obs.tool_name].ids.push(obs.id);
519
+ }
520
+ }
521
+ const frequentTools = Object.entries(toolUsage)
522
+ .filter(([, data]) => data.count >= 5)
523
+ .sort((a, b) => b[1].count - a[1].count);
524
+ for (const [toolName, data] of frequentTools) {
525
+ patterns.push({
526
+ pattern_type: 'tool_preference',
527
+ description: `[${topicSummary}] Frequent use of ${toolName} (${data.count} times) indicates this is a preferred tool.`,
528
+ confidence: Math.min(0.6 + data.count * 0.02, 0.95),
529
+ source_observation_ids: data.ids.slice(0, 5),
530
+ applicability: 'Similar development tasks',
531
+ });
532
+ }
533
+ // Also detect general workflow pattern from top tool
534
+ const topTools = Object.entries(toolUsage)
535
+ .filter(([, data]) => data.count >= 3)
536
+ .sort((a, b) => b[1].count - a[1].count);
537
+ if (topTools.length > 0 && frequentTools.length === 0) {
538
+ const topTool = topTools[0];
539
+ patterns.push({
540
+ pattern_type: 'workflow',
541
+ description: `[${topicSummary}] Frequent use of ${topTool[0]} (${topTool[1].count} times) indicates this is a core workflow tool.`,
542
+ confidence: 0.7,
543
+ source_observation_ids: topTool[1].ids.slice(0, 5),
544
+ applicability: 'Similar development tasks',
545
+ });
546
+ }
547
+ // 3. Success pattern detection
548
+ const successObs = observations.filter((obs) => obs.tool_output_summary?.toLowerCase().includes('success') ||
549
+ obs.tool_output_summary?.toLowerCase().includes('completed') ||
550
+ obs.tool_output_summary?.toLowerCase().includes('done'));
551
+ if (successObs.length >= 3) {
552
+ patterns.push({
553
+ pattern_type: 'success_pattern',
554
+ description: `[${topicSummary}] Session showed consistent successful execution patterns (${successObs.length} successes).`,
555
+ confidence: 0.7,
556
+ source_observation_ids: successObs.map((o) => o.id).slice(0, 5),
557
+ applicability: 'Similar task types',
558
+ });
559
+ }
560
+ // 4. Technical stack detection
561
+ const techStack = detectTechnicalStack(observations);
562
+ if (techStack.languages.length > 0 || techStack.frameworks.length > 0) {
563
+ patterns.push({
564
+ pattern_type: 'technical_stack',
565
+ description: `[${topicSummary}] Primary technologies: ${[...techStack.languages, ...techStack.frameworks].join(', ')}`,
566
+ confidence: 0.85,
567
+ source_observation_ids: observations.slice(0, 5).map((o) => o.id),
568
+ applicability: 'Project-wide development',
569
+ });
570
+ }
571
+ // Generate summary
572
+ const summary = generateReflectionSummary(topicSummary, patterns, observations.length);
573
+ return {
574
+ summary,
575
+ patterns,
576
+ recommendations,
577
+ technical_stack: techStack,
578
+ };
579
+ }
580
+ // ============================================================
581
+ // Step 5: Store reflection results
582
+ // ============================================================
583
+ async function storeReflection(pattern, segment, scope, pool, config, input) {
584
+ try {
585
+ const isAggregate = segment.segmentId === '__aggregate__';
586
+ const topicSegmentId = isAggregate ? null : segment.segmentId || null;
587
+ const metadata = {
588
+ applicability: pattern.applicability,
589
+ generatedAt: new Date().toISOString(),
590
+ modelSize: config.modelSize,
591
+ observationCount: segment.observations.length,
592
+ triggerType: input.trigger_type || 'threshold',
593
+ topicSummary: segment.topicSummary,
594
+ isAggregate,
595
+ };
596
+ let insertResult;
597
+ // Try new-schema INSERT first (with session_map_id, topic_segment_id)
598
+ if (scope.usesNewSchema && segment.sessionMapIds.length > 0) {
599
+ try {
600
+ insertResult = await pool.query(`INSERT INTO reflections (
601
+ session_id, session_map_id, topic_segment_id,
602
+ summary, source_observation_ids,
603
+ confidence, pattern_type, metadata
604
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
605
+ RETURNING id`, [
606
+ scope.opencodeSessionIds[0] || null,
607
+ segment.sessionMapIds[0],
608
+ topicSegmentId,
609
+ pattern.description,
610
+ pattern.source_observation_ids,
611
+ pattern.confidence,
612
+ pattern.pattern_type,
613
+ JSON.stringify(metadata),
614
+ ]);
615
+ }
616
+ catch {
617
+ // New columns may not exist yet — fall through to legacy INSERT
618
+ logger.warn('New schema INSERT failed, falling back to legacy');
619
+ }
620
+ }
621
+ // Legacy fallback: store with session_id only
622
+ if (!insertResult && scope.sessionInternalIds.length > 0) {
623
+ insertResult = await pool.query(`INSERT INTO reflections (
624
+ session_id, summary, source_observation_ids,
625
+ confidence, pattern_type, metadata
626
+ ) VALUES ($1, $2, $3, $4, $5, $6)
627
+ RETURNING id`, [
628
+ scope.sessionInternalIds[0],
629
+ pattern.description,
630
+ pattern.source_observation_ids,
631
+ pattern.confidence,
632
+ pattern.pattern_type,
633
+ JSON.stringify(metadata),
634
+ ]);
635
+ }
636
+ if (!insertResult) {
637
+ logger.warn('No valid session reference to store reflection');
638
+ return null;
639
+ }
640
+ return {
641
+ id: insertResult.rows[0].id,
642
+ session_id: scope.opencodeSessionIds[0] || scope.sessionInternalIds[0] || '',
643
+ topic_segment_id: topicSegmentId || undefined,
644
+ summary: pattern.description,
645
+ source_observation_ids: pattern.source_observation_ids,
646
+ confidence: pattern.confidence,
647
+ pattern_type: pattern.pattern_type,
648
+ created_at: new Date(),
649
+ metadata,
650
+ };
651
+ }
652
+ catch (error) {
653
+ logger.error('Failed to store reflection:', error);
654
+ return null;
655
+ }
656
+ }
657
+ // ============================================================
658
+ // Step 6: Update reflection timestamp
659
+ // ============================================================
660
+ async function updateReflectionTimestamp(scope, pool) {
661
+ try {
662
+ if (scope.usesNewSchema && scope.sessionMapIds.length > 0) {
663
+ // Update session_map reflection timestamp
664
+ // Use a dynamic ALTER-safe approach — try UPDATE, ignore if column missing
665
+ for (const smId of scope.sessionMapIds) {
666
+ try {
667
+ await pool.query(`UPDATE session_map SET metadata =
668
+ jsonb_set(COALESCE(metadata, '{}'), '{reflection_last_at}', $2::jsonb)
669
+ WHERE id = $1`, [smId, JSON.stringify(new Date().toISOString())]);
670
+ }
671
+ catch {
672
+ // Column or table might not support this yet — non-critical
673
+ }
674
+ // Also try direct column update if it exists
675
+ try {
676
+ await pool.query(`UPDATE session_map SET last_active_at = NOW() WHERE id = $1`, [smId]);
677
+ }
678
+ catch {
679
+ // Ignore
680
+ }
681
+ }
682
+ }
683
+ // Legacy: update sessions.reflection_last_at
684
+ for (const internalId of scope.sessionInternalIds) {
685
+ await pool.query(`UPDATE sessions SET reflection_last_at = NOW(),
686
+ metadata = metadata - 'pendingReflection'
687
+ WHERE id = $1`, [internalId]);
688
+ }
689
+ }
690
+ catch (error) {
691
+ logger.warn('Failed to update reflection timestamp:', error);
692
+ }
693
+ }
694
+ // ============================================================
695
+ // Error logging
696
+ // ============================================================
697
+ async function logReflectionError(input, error, pool) {
698
+ try {
699
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
700
+ const errorStack = error instanceof Error ? error.stack : '';
701
+ // Try to resolve session_id for the error record
702
+ let sessionId = null;
703
+ if (input.session_id) {
704
+ const sessResult = await pool.query(`SELECT id FROM sessions WHERE external_id = $1`, [input.session_id]);
705
+ if (sessResult.rows.length > 0) {
706
+ sessionId = sessResult.rows[0].id;
707
+ }
708
+ }
709
+ await pool.query(`INSERT INTO reflection_errors (
710
+ session_id, error_message, error_stack,
711
+ observation_count, retry_count
712
+ ) VALUES ($1, $2, $3, $4, $5)`, [sessionId, errorMessage, errorStack, 0, 0]);
713
+ }
714
+ catch (logError) {
715
+ logger.error('Failed to log reflection error:', logError);
716
+ }
717
+ }
718
+ // ============================================================
719
+ // Utility functions
720
+ // ============================================================
721
+ /** Check if a table exists in the database */
722
+ async function tableExists(pool, tableName) {
723
+ try {
724
+ const result = await pool.query(`SELECT EXISTS (
725
+ SELECT FROM information_schema.tables
726
+ WHERE table_schema = 'public' AND table_name = $1
727
+ ) as exists`, [tableName]);
728
+ return result.rows[0]?.exists === true;
729
+ }
730
+ catch {
731
+ return false;
732
+ }
733
+ }
734
+ /** Deduplicate string array */
735
+ function dedupe(arr) {
736
+ return [...new Set(arr)];
737
+ }
738
+ /**
739
+ * 检测技术栈
740
+ */
741
+ function detectTechnicalStack(observations) {
742
+ const allText = [
743
+ ...observations.map((o) => o.tool_output_summary),
744
+ ...observations.map((o) => o.tool_name),
745
+ ].join(' ').toLowerCase();
746
+ const languages = [];
747
+ const frameworks = [];
748
+ const tools = [];
749
+ const langPatterns = [
750
+ { name: 'TypeScript', pattern: /typescript|\.ts\b/ },
751
+ { name: 'JavaScript', pattern: /javascript|\.js\b/ },
752
+ { name: 'Python', pattern: /python|\.py\b/ },
753
+ { name: 'Rust', pattern: /rust|\.rs\b/ },
754
+ { name: 'Go', pattern: /\bgo\b|golang|\.go\b/ },
755
+ { name: 'Java', pattern: /\bjava\b|\.java\b/ },
756
+ ];
757
+ for (const { name, pattern } of langPatterns) {
758
+ if (pattern.test(allText))
759
+ languages.push(name);
760
+ }
761
+ const frameworkPatterns = [
762
+ { name: 'React', pattern: /react/ },
763
+ { name: 'Vue', pattern: /vue/ },
764
+ { name: 'Angular', pattern: /angular/ },
765
+ { name: 'Express', pattern: /express/ },
766
+ { name: 'FastAPI', pattern: /fastapi/ },
767
+ { name: 'Django', pattern: /django/ },
768
+ ];
769
+ for (const { name, pattern } of frameworkPatterns) {
770
+ if (pattern.test(allText))
771
+ frameworks.push(name);
772
+ }
773
+ const toolPatterns = [
774
+ { name: 'Git', pattern: /git\b/ },
775
+ { name: 'Docker', pattern: /docker/ },
776
+ { name: 'npm', pattern: /\bnpm\b/ },
777
+ { name: 'yarn', pattern: /\byarn\b/ },
778
+ { name: 'webpack', pattern: /webpack/ },
779
+ { name: 'vite', pattern: /\bvite\b/ },
780
+ ];
781
+ for (const { name, pattern } of toolPatterns) {
782
+ if (pattern.test(allText))
783
+ tools.push(name);
784
+ }
785
+ return { languages, frameworks, tools };
786
+ }
787
+ /**
788
+ * 生成反思总结
789
+ */
790
+ function generateReflectionSummary(topicSummary, patterns, observationCount) {
791
+ if (patterns.length === 0) {
792
+ return `[${topicSummary}] Analyzed ${observationCount} observations. No significant patterns detected.`;
793
+ }
794
+ const patternTypes = patterns.map((p) => p.pattern_type).join(', ');
795
+ return `[${topicSummary}] Analyzed ${observationCount} observations and identified ${patterns.length} patterns: ${patternTypes}. Key insights available for future sessions.`;
796
+ }
797
+ /**
798
+ * 检查是否是低峰期
799
+ */
800
+ function isOffPeakHour(config) {
801
+ const hour = new Date().getHours();
802
+ const offPeakHours = config?.offPeakHours || [1, 2, 3, 4, 5];
803
+ return offPeakHours.includes(hour);
804
+ }
805
+ /**
806
+ * 获取反思统计
807
+ */
808
+ async function getReflectionStats(sessionId, pool) {
809
+ const sessionResult = await pool.query('SELECT id, reflection_last_at FROM sessions WHERE external_id = $1', [sessionId]);
810
+ if (sessionResult.rows.length === 0) {
811
+ return {
812
+ totalReflections: 0,
813
+ patternTypes: {},
814
+ averageConfidence: 0,
815
+ lastReflectionAt: null,
816
+ };
817
+ }
818
+ const internalId = sessionResult.rows[0].id;
819
+ const [countResult, patternResult, confidenceResult] = await Promise.all([
820
+ pool.query('SELECT COUNT(*) as count FROM reflections WHERE session_id = $1', [internalId]),
821
+ pool.query(`SELECT pattern_type, COUNT(*) as count
822
+ FROM reflections
823
+ WHERE session_id = $1 AND pattern_type IS NOT NULL
824
+ GROUP BY pattern_type`, [internalId]),
825
+ pool.query('SELECT AVG(confidence) as avg FROM reflections WHERE session_id = $1', [internalId]),
826
+ ]);
827
+ const patternTypes = {};
828
+ for (const row of patternResult.rows) {
829
+ patternTypes[row.pattern_type] = parseInt(row.count, 10);
830
+ }
831
+ return {
832
+ totalReflections: parseInt(countResult.rows[0].count, 10),
833
+ patternTypes,
834
+ averageConfidence: parseFloat(confidenceResult.rows[0]?.avg || 0),
835
+ lastReflectionAt: sessionResult.rows[0].reflection_last_at,
836
+ };
837
+ }
838
+ //# sourceMappingURL=hindsight-reflect.js.map