opcode-pg-memory 2.2.8 → 2.3.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.
- package/dist/cli.js +232 -214
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -21006
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.js +319 -302
- package/dist/mcp-server.js.map +1 -0
- package/dist/src/cache/semantic-cache.js +399 -0
- package/dist/src/cache/semantic-cache.js.map +1 -0
- package/dist/src/cli.js +404 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +89 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/db/init-db.js +545 -0
- package/dist/src/db/init-db.js.map +1 -0
- package/dist/src/hooks/message-part-updated.js +203 -0
- package/dist/src/hooks/message-part-updated.js.map +1 -0
- package/dist/src/hooks/message-updated.js +347 -0
- package/dist/src/hooks/message-updated.js.map +1 -0
- package/dist/src/hooks/session-compacting.js +179 -0
- package/dist/src/hooks/session-compacting.js.map +1 -0
- package/dist/src/hooks/session-completed.js +337 -0
- package/dist/src/hooks/session-completed.js.map +1 -0
- package/dist/src/hooks/session-created.js +206 -0
- package/dist/src/hooks/session-created.js.map +1 -0
- package/dist/src/hooks/tool-execute.js +267 -0
- package/dist/src/hooks/tool-execute.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +643 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/hindsight-reflect-omo.js +318 -0
- package/dist/src/mcp/hindsight-reflect-omo.js.map +1 -0
- package/dist/src/mcp/hindsight-reflect.js +838 -0
- package/dist/src/mcp/hindsight-reflect.js.map +1 -0
- package/dist/src/mcp/recall-memory-omo.js +263 -0
- package/dist/src/mcp/recall-memory-omo.js.map +1 -0
- package/dist/src/mcp/recall-memory.d.ts +6 -0
- package/dist/src/mcp/recall-memory.d.ts.map +1 -1
- package/dist/src/mcp/recall-memory.js +900 -0
- package/dist/src/mcp/recall-memory.js.map +1 -0
- package/dist/src/omo/adapter.js +583 -0
- package/dist/src/omo/adapter.js.map +1 -0
- package/dist/src/omo/types.js +44 -0
- package/dist/src/omo/types.js.map +1 -0
- package/dist/src/services/db-polling.d.ts +30 -0
- package/dist/src/services/db-polling.d.ts.map +1 -0
- package/dist/src/services/db-polling.js +97 -0
- package/dist/src/services/db-polling.js.map +1 -0
- package/dist/src/services/event-synchronizer.d.ts +15 -0
- package/dist/src/services/event-synchronizer.d.ts.map +1 -0
- package/dist/src/services/event-synchronizer.js +119 -0
- package/dist/src/services/event-synchronizer.js.map +1 -0
- package/dist/src/services/keyword.js +29 -0
- package/dist/src/services/keyword.js.map +1 -0
- package/dist/src/services/logger.js +42 -0
- package/dist/src/services/logger.js.map +1 -0
- package/dist/src/services/opencode-schema-adapter.d.ts +34 -0
- package/dist/src/services/opencode-schema-adapter.d.ts.map +1 -0
- package/dist/src/services/opencode-schema-adapter.js +96 -0
- package/dist/src/services/opencode-schema-adapter.js.map +1 -0
- package/dist/src/services/privacy.js +23 -0
- package/dist/src/services/privacy.js.map +1 -0
- package/dist/src/topic/segment-manager.js +447 -0
- package/dist/src/topic/segment-manager.js.map +1 -0
- package/dist/src/types.d.ts +20 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +8 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/embedding.js +180 -0
- package/dist/src/utils/embedding.js.map +1 -0
- package/dist/src/utils/token-budget.js +152 -0
- package/dist/src/utils/token-budget.js.map +1 -0
- package/package.json +6 -5
|
@@ -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
|