linksee-memory 0.0.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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/bin/import-sessions.d.ts +2 -0
- package/dist/bin/import-sessions.js +258 -0
- package/dist/bin/sync-session.d.ts +2 -0
- package/dist/bin/sync-session.js +97 -0
- package/dist/db/migrate.d.ts +4 -0
- package/dist/db/migrate.js +48 -0
- package/dist/db/schema.sql +199 -0
- package/dist/lib/consolidate.d.ts +12 -0
- package/dist/lib/consolidate.js +148 -0
- package/dist/lib/file-chunker.d.ts +12 -0
- package/dist/lib/file-chunker.js +250 -0
- package/dist/lib/forgetting.d.ts +12 -0
- package/dist/lib/forgetting.js +31 -0
- package/dist/lib/heat-index.d.ts +13 -0
- package/dist/lib/heat-index.js +29 -0
- package/dist/lib/momentum.d.ts +19 -0
- package/dist/lib/momentum.js +75 -0
- package/dist/lib/session-extractor.d.ts +34 -0
- package/dist/lib/session-extractor.js +263 -0
- package/dist/lib/session-parser.d.ts +40 -0
- package/dist/lib/session-parser.js +303 -0
- package/dist/mcp/read-smart.d.ts +5 -0
- package/dist/mcp/read-smart.js +132 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +449 -0
- package/package.json +67 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Ported from sales-intelligence-os/lib/heat-index.ts (human-facing names → agent-memory names).
|
|
2
|
+
// Drives "importance hierarchy" (Michie's memory principle 3).
|
|
3
|
+
export function computeHeat(input) {
|
|
4
|
+
const last30 = Math.min(input.accessesLast30d * 3, 30);
|
|
5
|
+
const last90 = Math.min(input.accessesLast90d * 1, 20);
|
|
6
|
+
let recencyBonus = 0;
|
|
7
|
+
if (input.daysSinceLastAccess <= 7)
|
|
8
|
+
recencyBonus = 20;
|
|
9
|
+
else if (input.daysSinceLastAccess <= 30)
|
|
10
|
+
recencyBonus = 10;
|
|
11
|
+
else if (input.daysSinceLastAccess <= 90)
|
|
12
|
+
recencyBonus = 0;
|
|
13
|
+
else
|
|
14
|
+
recencyBonus = -10;
|
|
15
|
+
const tenureBonus = Math.min(input.totalAccesses * 0.5, 15);
|
|
16
|
+
const importanceBoost = (input.baseImportance ?? 0.5) * 15; // 0-15 extra from user mark
|
|
17
|
+
const score = Math.max(0, Math.min(100, last30 + last90 + recencyBonus + tenureBonus + importanceBoost));
|
|
18
|
+
let band;
|
|
19
|
+
if (score >= 70)
|
|
20
|
+
band = 'hot';
|
|
21
|
+
else if (score >= 40)
|
|
22
|
+
band = 'warm';
|
|
23
|
+
else if (score >= 20)
|
|
24
|
+
band = 'cold';
|
|
25
|
+
else
|
|
26
|
+
band = 'frozen';
|
|
27
|
+
return { score, band };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=heat-index.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export interface MomentumInput {
|
|
3
|
+
eventsLast24h: number;
|
|
4
|
+
eventsLast7d: number;
|
|
5
|
+
eventsHistorical: number;
|
|
6
|
+
daysObserved: number;
|
|
7
|
+
avgImportanceRecent: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MomentumResult {
|
|
10
|
+
score: number;
|
|
11
|
+
band: 'surging' | 'active' | 'quiet' | 'dormant';
|
|
12
|
+
breakdown: {
|
|
13
|
+
quality: number;
|
|
14
|
+
velocity: number;
|
|
15
|
+
relativeVolume: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function computeMomentum(input: MomentumInput): MomentumResult;
|
|
19
|
+
export declare function refreshMomentumForEntity(db: Database.Database, entityId: number): MomentumResult;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Ported from sales-intelligence-os/lib/momentum-calculator-v3.ts
|
|
2
|
+
// Original: detects "this company has momentum" from news/signal bursts.
|
|
3
|
+
// Agent-memory adaptation: detects "this entity is currently active in the agent's context."
|
|
4
|
+
// Drives "context-dependent retrieval" (Michie's memory principle 5).
|
|
5
|
+
export function computeMomentum(input) {
|
|
6
|
+
// --- velocity (30%): how fast is activity arriving ---
|
|
7
|
+
let velocity = 0;
|
|
8
|
+
if (input.eventsLast24h >= 3)
|
|
9
|
+
velocity = 10;
|
|
10
|
+
else if (input.eventsLast24h >= 1)
|
|
11
|
+
velocity = 7;
|
|
12
|
+
else if (input.eventsLast7d >= 3)
|
|
13
|
+
velocity = 4;
|
|
14
|
+
else if (input.eventsLast7d >= 1)
|
|
15
|
+
velocity = 2;
|
|
16
|
+
else
|
|
17
|
+
velocity = 0;
|
|
18
|
+
// --- relativeVolume (30%): recent vs historical baseline ---
|
|
19
|
+
const weeklyAvg = input.daysObserved > 0 ? (input.eventsHistorical / input.daysObserved) * 7 : 0;
|
|
20
|
+
let relativeVolume = 0;
|
|
21
|
+
if (weeklyAvg === 0 && input.eventsLast7d > 0)
|
|
22
|
+
relativeVolume = 5; // brand-new activity
|
|
23
|
+
else if (weeklyAvg > 0) {
|
|
24
|
+
const ratio = input.eventsLast7d / weeklyAvg;
|
|
25
|
+
if (ratio >= 3)
|
|
26
|
+
relativeVolume = 10;
|
|
27
|
+
else if (ratio >= 2)
|
|
28
|
+
relativeVolume = 7;
|
|
29
|
+
else if (ratio >= 1)
|
|
30
|
+
relativeVolume = 4;
|
|
31
|
+
else
|
|
32
|
+
relativeVolume = Math.max(0, ratio * 4);
|
|
33
|
+
}
|
|
34
|
+
// --- quality (40%): avg importance of what's arriving ---
|
|
35
|
+
const quality = Math.min(10, input.avgImportanceRecent * 10);
|
|
36
|
+
const weighted = quality * 0.4 + velocity * 0.3 + relativeVolume * 0.3;
|
|
37
|
+
const score = Math.max(0, Math.min(10, weighted));
|
|
38
|
+
let band;
|
|
39
|
+
if (score >= 7)
|
|
40
|
+
band = 'surging';
|
|
41
|
+
else if (score >= 4)
|
|
42
|
+
band = 'active';
|
|
43
|
+
else if (score >= 1.5)
|
|
44
|
+
band = 'quiet';
|
|
45
|
+
else
|
|
46
|
+
band = 'dormant';
|
|
47
|
+
return { score, band, breakdown: { quality, velocity, relativeVolume } };
|
|
48
|
+
}
|
|
49
|
+
// Compute and cache momentum for an entity using DB state.
|
|
50
|
+
export function refreshMomentumForEntity(db, entityId) {
|
|
51
|
+
const now = Math.floor(Date.now() / 1000);
|
|
52
|
+
const dayAgo = now - 86400;
|
|
53
|
+
const weekAgo = now - 7 * 86400;
|
|
54
|
+
const stats = db
|
|
55
|
+
.prepare(`
|
|
56
|
+
SELECT
|
|
57
|
+
(SELECT COUNT(*) FROM events WHERE entity_id = ? AND occurred_at >= ?) as e24,
|
|
58
|
+
(SELECT COUNT(*) FROM events WHERE entity_id = ? AND occurred_at >= ?) as e7d,
|
|
59
|
+
(SELECT COUNT(*) FROM events WHERE entity_id = ?) as eAll,
|
|
60
|
+
(SELECT created_at FROM entities WHERE id = ?) as createdAt,
|
|
61
|
+
(SELECT COALESCE(AVG(importance), 0.5) FROM memories WHERE entity_id = ? AND created_at >= ?) as avgImpRecent
|
|
62
|
+
`)
|
|
63
|
+
.get(entityId, dayAgo, entityId, weekAgo, entityId, entityId, entityId, weekAgo);
|
|
64
|
+
const daysObserved = stats.createdAt ? Math.max(1, (now - stats.createdAt) / 86400) : 1;
|
|
65
|
+
const result = computeMomentum({
|
|
66
|
+
eventsLast24h: stats.e24,
|
|
67
|
+
eventsLast7d: stats.e7d,
|
|
68
|
+
eventsHistorical: stats.eAll,
|
|
69
|
+
daysObserved,
|
|
70
|
+
avgImportanceRecent: stats.avgImpRecent,
|
|
71
|
+
});
|
|
72
|
+
db.prepare('UPDATE entities SET momentum_score = ?, momentum_at = ? WHERE id = ?').run(result.score, now, entityId);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=momentum.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ParsedSession } from './session-parser.js';
|
|
2
|
+
export interface ExtractedMemory {
|
|
3
|
+
layer: 'goal' | 'context' | 'emotion' | 'implementation' | 'caveat' | 'learning';
|
|
4
|
+
content: string;
|
|
5
|
+
importance: number;
|
|
6
|
+
source: {
|
|
7
|
+
session_id: string;
|
|
8
|
+
turn_uuid?: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface ExtractedFileEdit {
|
|
13
|
+
session_id: string;
|
|
14
|
+
file_path: string;
|
|
15
|
+
operation: 'read' | 'edit' | 'write' | 'bash' | 'other';
|
|
16
|
+
turn_uuid?: string;
|
|
17
|
+
occurred_at: number;
|
|
18
|
+
context_snippet: string;
|
|
19
|
+
memory_content?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ExtractionResult {
|
|
22
|
+
project_name: string;
|
|
23
|
+
project_cwd: string;
|
|
24
|
+
session_id: string;
|
|
25
|
+
memories: ExtractedMemory[];
|
|
26
|
+
file_edits: ExtractedFileEdit[];
|
|
27
|
+
stats: {
|
|
28
|
+
turns_total: number;
|
|
29
|
+
turns_meaningful_user: number;
|
|
30
|
+
file_ops_raw: number;
|
|
31
|
+
file_ops_unique_paths: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function extractSession(session: ParsedSession, projectName: string): ExtractionResult;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Extract 6-layer memories + file_edit links from a parsed session.
|
|
2
|
+
// Core mission: break the Mem0 "flat metatag" wall by attaching each memory
|
|
3
|
+
// to its intent context. A memory like "edited server.ts" becomes
|
|
4
|
+
// "edited server.ts BECAUSE the user wanted the FTS5 + LIKE merge fix".
|
|
5
|
+
import { isMetaOrNoise, isAutomatedSession, isPastedExternalContent } from './session-parser.js';
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Intent detection — first non-noise user message in the session.
|
|
8
|
+
// ============================================================
|
|
9
|
+
function findFirstIntent(session) {
|
|
10
|
+
for (const t of session.turns) {
|
|
11
|
+
if (t.role !== 'user')
|
|
12
|
+
continue;
|
|
13
|
+
if (t.tool_results && t.tool_results.length > 0)
|
|
14
|
+
continue; // tool result carriers, not intents
|
|
15
|
+
if (isMetaOrNoise(t.text))
|
|
16
|
+
continue;
|
|
17
|
+
if (t.text.trim().length < 20)
|
|
18
|
+
continue; // "ok" / "yes" / "next"
|
|
19
|
+
return t;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// ============================================================
|
|
24
|
+
// Decisions & learnings — user messages containing explicit commitment words.
|
|
25
|
+
// ============================================================
|
|
26
|
+
const DECISION_PATTERNS = [
|
|
27
|
+
/決めた|採用|確定|これで(いい|進め)|OK進めて|やろう|行こう/,
|
|
28
|
+
/learn(ed)?|decide|chose|picked|going with/i,
|
|
29
|
+
];
|
|
30
|
+
const FAILURE_PATTERNS = [
|
|
31
|
+
/失敗|バグ|エラー|直して|修正|戻して/,
|
|
32
|
+
/error|bug|fail|broken|revert|rollback/i,
|
|
33
|
+
];
|
|
34
|
+
const CAVEAT_PATTERNS = [
|
|
35
|
+
/注意|気をつけ|避けて|やらない|禁止/,
|
|
36
|
+
/do not|never|avoid|watch out/i,
|
|
37
|
+
];
|
|
38
|
+
function matchesAny(text, patterns) {
|
|
39
|
+
return patterns.some((p) => p.test(text));
|
|
40
|
+
}
|
|
41
|
+
// Dedupe successive file edits to the same path within N seconds —
|
|
42
|
+
// they're usually the same logical change.
|
|
43
|
+
// NOTE: this only dedupes for memory CREATION (1 implementation memory per file
|
|
44
|
+
// per logical edit cluster). The session_file_edits table preserves ALL physical
|
|
45
|
+
// edits with their individual timestamps so cross-file/cross-session queries stay accurate.
|
|
46
|
+
function dedupeEdits(edits) {
|
|
47
|
+
const WINDOW = 10; // sec — tighter so legitimate sequential edits are preserved
|
|
48
|
+
const seen = new Map(); // path → last timestamp kept
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const e of edits) {
|
|
51
|
+
const key = `${e.operation}::${e.path}`;
|
|
52
|
+
const last = seen.get(key);
|
|
53
|
+
if (last && Math.abs(e.timestamp - last) < WINDOW)
|
|
54
|
+
continue;
|
|
55
|
+
seen.set(key, e.timestamp);
|
|
56
|
+
out.push(e);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
export function extractSession(session, projectName) {
|
|
61
|
+
const memories = [];
|
|
62
|
+
const file_edits = [];
|
|
63
|
+
// Detect fully-automated sessions (e.g. scheduled cron tasks) — no user intent to extract
|
|
64
|
+
const firstRawUserText = session.turns.find((t) => t.role === 'user' && !t.tool_results)?.text ?? '';
|
|
65
|
+
const automated = isAutomatedSession(firstRawUserText);
|
|
66
|
+
// 1) Goal layer — the first REAL intent (or synthetic marker for automated sessions)
|
|
67
|
+
const firstIntent = findFirstIntent(session);
|
|
68
|
+
if (firstIntent) {
|
|
69
|
+
memories.push({
|
|
70
|
+
layer: 'goal',
|
|
71
|
+
content: JSON.stringify({
|
|
72
|
+
intent: firstIntent.text.slice(0, 1000),
|
|
73
|
+
when: new Date(firstIntent.timestamp * 1000).toISOString(),
|
|
74
|
+
session_id: session.session_id,
|
|
75
|
+
git_branch: session.git_branch,
|
|
76
|
+
}, null, 2),
|
|
77
|
+
importance: automated ? 0.3 : 0.8, // lower for automated runs
|
|
78
|
+
source: { session_id: session.session_id, turn_uuid: firstIntent.uuid, kind: 'first_intent' },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (automated) {
|
|
82
|
+
// Synthetic goal so the session is still discoverable
|
|
83
|
+
const match = firstRawUserText.match(/<scheduled-task\s+name="([^"]+)"/);
|
|
84
|
+
const taskName = match ? match[1] : 'unknown';
|
|
85
|
+
memories.push({
|
|
86
|
+
layer: 'goal',
|
|
87
|
+
content: JSON.stringify({
|
|
88
|
+
intent: `Automated scheduled task run: ${taskName}`,
|
|
89
|
+
automated: true,
|
|
90
|
+
when: new Date(session.started_at * 1000).toISOString(),
|
|
91
|
+
session_id: session.session_id,
|
|
92
|
+
git_branch: session.git_branch,
|
|
93
|
+
}, null, 2),
|
|
94
|
+
importance: 0.2,
|
|
95
|
+
source: { session_id: session.session_id, kind: 'automated_task' },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// 2) Context layer — subsequent clarifying user messages (up to 3 more)
|
|
99
|
+
let clarifyCount = 0;
|
|
100
|
+
for (const t of session.turns) {
|
|
101
|
+
if (clarifyCount >= 3)
|
|
102
|
+
break;
|
|
103
|
+
if (t === firstIntent)
|
|
104
|
+
continue;
|
|
105
|
+
if (t.role !== 'user')
|
|
106
|
+
continue;
|
|
107
|
+
if (t.tool_results && t.tool_results.length > 0)
|
|
108
|
+
continue;
|
|
109
|
+
if (isMetaOrNoise(t.text))
|
|
110
|
+
continue;
|
|
111
|
+
if (t.text.trim().length < 40)
|
|
112
|
+
continue;
|
|
113
|
+
clarifyCount++;
|
|
114
|
+
memories.push({
|
|
115
|
+
layer: 'context',
|
|
116
|
+
content: JSON.stringify({
|
|
117
|
+
message: t.text.slice(0, 600),
|
|
118
|
+
when: new Date(t.timestamp * 1000).toISOString(),
|
|
119
|
+
session_id: session.session_id,
|
|
120
|
+
}),
|
|
121
|
+
importance: 0.5,
|
|
122
|
+
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'clarification' },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// 3) Implementation layer + file_edits — one memory per unique file touched
|
|
126
|
+
const uniqueOps = dedupeEdits(session.file_ops).filter((e) => e.operation === 'edit' || e.operation === 'write');
|
|
127
|
+
const byPath = new Map();
|
|
128
|
+
for (const e of uniqueOps) {
|
|
129
|
+
const arr = byPath.get(e.path) ?? [];
|
|
130
|
+
arr.push(e);
|
|
131
|
+
byPath.set(e.path, arr);
|
|
132
|
+
}
|
|
133
|
+
for (const [path, ops] of byPath) {
|
|
134
|
+
const first = ops[0];
|
|
135
|
+
const opsKinds = Array.from(new Set(ops.map((o) => o.operation))).join('+');
|
|
136
|
+
const why = (first.preceding_user_text || '(no explicit preceding intent)').slice(0, 400);
|
|
137
|
+
const contentSnippet = ops.map((o) => o.tool_input_preview).slice(0, 2).join(' | ').slice(0, 500);
|
|
138
|
+
const memoryContent = JSON.stringify({
|
|
139
|
+
file: path,
|
|
140
|
+
ops: opsKinds,
|
|
141
|
+
op_count: ops.length,
|
|
142
|
+
why_extracted: why,
|
|
143
|
+
sample_change: contentSnippet,
|
|
144
|
+
when: new Date(first.timestamp * 1000).toISOString(),
|
|
145
|
+
session_id: session.session_id,
|
|
146
|
+
}, null, 2);
|
|
147
|
+
memories.push({
|
|
148
|
+
layer: 'implementation',
|
|
149
|
+
content: memoryContent,
|
|
150
|
+
importance: 0.6,
|
|
151
|
+
source: { session_id: session.session_id, turn_uuid: first.turn_uuid, kind: 'file_edit' },
|
|
152
|
+
});
|
|
153
|
+
// Create a file_edit link record for EACH physical op (NOT deduped),
|
|
154
|
+
// all pointing at this memory's content. This preserves the TRUE timeline
|
|
155
|
+
// of edits (session_file_edits) while keeping memories at a useful granularity.
|
|
156
|
+
const allOpsForPath = session.file_ops.filter((o) => o.path === path && (o.operation === 'edit' || o.operation === 'write'));
|
|
157
|
+
for (const op of allOpsForPath) {
|
|
158
|
+
file_edits.push({
|
|
159
|
+
session_id: session.session_id,
|
|
160
|
+
file_path: op.path,
|
|
161
|
+
operation: op.operation,
|
|
162
|
+
turn_uuid: op.turn_uuid,
|
|
163
|
+
occurred_at: op.timestamp,
|
|
164
|
+
context_snippet: (op.preceding_user_text || '').slice(0, 300),
|
|
165
|
+
memory_content: memoryContent, // linker will resolve to memory_id after insert
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 4) Caveat layer — user messages matching caveat/failure patterns
|
|
170
|
+
// Stricter filter: must NOT be pasted external content.
|
|
171
|
+
for (const t of session.turns) {
|
|
172
|
+
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
173
|
+
continue;
|
|
174
|
+
if (t.tool_results && t.tool_results.length > 0)
|
|
175
|
+
continue;
|
|
176
|
+
if (isPastedExternalContent(t.text))
|
|
177
|
+
continue;
|
|
178
|
+
if (matchesAny(t.text, CAVEAT_PATTERNS) && t.text.length > 20) {
|
|
179
|
+
memories.push({
|
|
180
|
+
layer: 'caveat',
|
|
181
|
+
content: JSON.stringify({
|
|
182
|
+
rule_or_warning: t.text.slice(0, 500),
|
|
183
|
+
when: new Date(t.timestamp * 1000).toISOString(),
|
|
184
|
+
session_id: session.session_id,
|
|
185
|
+
}),
|
|
186
|
+
importance: 0.75,
|
|
187
|
+
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'caveat' },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 5) Learning layer — messages matching decision patterns
|
|
192
|
+
// Same strict filter applies.
|
|
193
|
+
for (const t of session.turns) {
|
|
194
|
+
if (t.role !== 'user' || isMetaOrNoise(t.text))
|
|
195
|
+
continue;
|
|
196
|
+
if (t.tool_results && t.tool_results.length > 0)
|
|
197
|
+
continue;
|
|
198
|
+
if (isPastedExternalContent(t.text))
|
|
199
|
+
continue;
|
|
200
|
+
if (matchesAny(t.text, DECISION_PATTERNS) && t.text.length > 15) {
|
|
201
|
+
memories.push({
|
|
202
|
+
layer: 'learning',
|
|
203
|
+
content: JSON.stringify({
|
|
204
|
+
decision: t.text.slice(0, 500),
|
|
205
|
+
when: new Date(t.timestamp * 1000).toISOString(),
|
|
206
|
+
session_id: session.session_id,
|
|
207
|
+
}),
|
|
208
|
+
importance: 0.7,
|
|
209
|
+
source: { session_id: session.session_id, turn_uuid: t.uuid, kind: 'decision' },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 6) Error-recovery patterns — only if NOT already covered by an extracted caveat,
|
|
214
|
+
// and skip the boilerplate wording. Now writes to 'context' layer (not caveat)
|
|
215
|
+
// so caveat stays reserved for genuine user-stated rules.
|
|
216
|
+
if (session.errors_count > 3) {
|
|
217
|
+
memories.push({
|
|
218
|
+
layer: 'context',
|
|
219
|
+
content: JSON.stringify({
|
|
220
|
+
why_now: `This session had ${session.errors_count} tool errors across ${session.turns.length} turns.`,
|
|
221
|
+
triggering_event: 'high_error_rate',
|
|
222
|
+
when: new Date(session.started_at * 1000).toISOString(),
|
|
223
|
+
session_id: session.session_id,
|
|
224
|
+
}),
|
|
225
|
+
importance: 0.4,
|
|
226
|
+
source: { session_id: session.session_id, kind: 'error_recovery' },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// 7) Session summary — one meta-implementation memory per session for overview
|
|
230
|
+
if (memories.length > 0) {
|
|
231
|
+
memories.push({
|
|
232
|
+
layer: 'implementation',
|
|
233
|
+
content: JSON.stringify({
|
|
234
|
+
summary_kind: 'session_overview',
|
|
235
|
+
session_id: session.session_id,
|
|
236
|
+
started_at: new Date(session.started_at * 1000).toISOString(),
|
|
237
|
+
ended_at: new Date(session.ended_at * 1000).toISOString(),
|
|
238
|
+
duration_min: Math.round((session.ended_at - session.started_at) / 60),
|
|
239
|
+
turns_user: session.turn_count_user,
|
|
240
|
+
turns_assistant: session.turn_count_assistant,
|
|
241
|
+
files_touched: byPath.size,
|
|
242
|
+
errors: session.errors_count,
|
|
243
|
+
git_branch: session.git_branch,
|
|
244
|
+
}, null, 2),
|
|
245
|
+
importance: 0.4,
|
|
246
|
+
source: { session_id: session.session_id, kind: 'session_summary' },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
project_name: projectName,
|
|
251
|
+
project_cwd: session.project_cwd,
|
|
252
|
+
session_id: session.session_id,
|
|
253
|
+
memories,
|
|
254
|
+
file_edits,
|
|
255
|
+
stats: {
|
|
256
|
+
turns_total: session.turns.length,
|
|
257
|
+
turns_meaningful_user: session.turns.filter((t) => t.role === 'user' && !isMetaOrNoise(t.text)).length,
|
|
258
|
+
file_ops_raw: session.file_ops.length,
|
|
259
|
+
file_ops_unique_paths: byPath.size,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=session-extractor.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface SessionTurn {
|
|
2
|
+
uuid: string;
|
|
3
|
+
role: 'user' | 'assistant';
|
|
4
|
+
timestamp: number;
|
|
5
|
+
text: string;
|
|
6
|
+
tool_calls?: Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
input: any;
|
|
9
|
+
tool_use_id: string;
|
|
10
|
+
}>;
|
|
11
|
+
tool_results?: Array<{
|
|
12
|
+
tool_use_id: string;
|
|
13
|
+
is_error: boolean;
|
|
14
|
+
output_preview: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface ParsedSession {
|
|
18
|
+
session_id: string;
|
|
19
|
+
project_cwd: string;
|
|
20
|
+
started_at: number;
|
|
21
|
+
ended_at: number;
|
|
22
|
+
git_branch?: string;
|
|
23
|
+
turns: SessionTurn[];
|
|
24
|
+
file_ops: Array<{
|
|
25
|
+
operation: 'read' | 'edit' | 'write' | 'bash' | 'other';
|
|
26
|
+
path: string;
|
|
27
|
+
turn_uuid: string;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
preceding_user_text: string;
|
|
30
|
+
tool_input_preview: string;
|
|
31
|
+
}>;
|
|
32
|
+
turn_count_user: number;
|
|
33
|
+
turn_count_assistant: number;
|
|
34
|
+
errors_count: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function isMetaOrNoise(text: string): boolean;
|
|
37
|
+
export declare function isPastedExternalContent(text: string): boolean;
|
|
38
|
+
export declare function isAutomatedSession(firstUserText: string): boolean;
|
|
39
|
+
export declare function parseSessionFile(jsonlPath: string): ParsedSession | null;
|
|
40
|
+
export declare function projectNameFromCwd(cwd: string): string;
|