@haaaiawd/second-nature 0.1.39 → 0.1.41
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/index.js +1270 -1270
- package/openclaw.plugin.json +29 -29
- package/package.json +55 -55
- package/runtime/cli/commands/connector-init.js +11 -4
- package/runtime/cli/index.js +6 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +75 -75
- package/runtime/cli/ops/heartbeat-surface.js +97 -97
- package/runtime/cli/ops/ops-router.js +1428 -1428
- package/runtime/cli/ops/workspace-heartbeat-runner.js +236 -236
- package/runtime/connectors/services/connector-executor-adapter.js +192 -41
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.d.ts +1 -1
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.js +2 -1
- package/runtime/core/second-nature/guidance/apply-guidance.d.ts +12 -12
- package/runtime/core/second-nature/guidance/apply-guidance.js +15 -15
- package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -50
- package/runtime/core/second-nature/guidance/user-reply-continuity.js +89 -89
- package/runtime/core/second-nature/orchestrator/intent-planner.js +15 -0
- package/runtime/core/second-nature/runtime/service-entry.d.ts +39 -39
- package/runtime/core/second-nature/runtime/service-entry.js +44 -44
- package/runtime/dream/dream-engine.d.ts +14 -14
- package/runtime/dream/dream-engine.js +306 -306
- package/runtime/dream/dream-input-loader.d.ts +37 -37
- package/runtime/dream/dream-input-loader.js +150 -150
- package/runtime/dream/dream-scheduler.d.ts +75 -75
- package/runtime/dream/dream-scheduler.js +131 -131
- package/runtime/dream/index.d.ts +16 -16
- package/runtime/dream/index.js +14 -14
- package/runtime/dream/insight-extractor.d.ts +32 -32
- package/runtime/dream/insight-extractor.js +135 -135
- package/runtime/dream/memory-consolidator.d.ts +45 -45
- package/runtime/dream/memory-consolidator.js +140 -140
- package/runtime/dream/narrative-update-proposal.d.ts +34 -34
- package/runtime/dream/narrative-update-proposal.js +83 -83
- package/runtime/dream/output-validator.d.ts +20 -20
- package/runtime/dream/output-validator.js +110 -110
- package/runtime/dream/redaction-gate.d.ts +31 -31
- package/runtime/dream/redaction-gate.js +109 -109
- package/runtime/dream/relationship-update-proposal.d.ts +27 -27
- package/runtime/dream/relationship-update-proposal.js +119 -119
- package/runtime/dream/sampler.d.ts +30 -30
- package/runtime/dream/sampler.js +65 -65
- package/runtime/dream/types.d.ts +187 -187
- package/runtime/dream/types.js +11 -11
- package/runtime/guidance/fallback.js +20 -20
- package/runtime/guidance/guidance-assembler.js +76 -76
- package/runtime/guidance/output-guard.d.ts +13 -13
- package/runtime/guidance/output-guard.js +53 -53
- package/runtime/guidance/template-registry.d.ts +20 -20
- package/runtime/guidance/template-registry.js +93 -93
- package/runtime/guidance/types.d.ts +98 -98
- package/runtime/observability/projections/guidance-audit.js +38 -38
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Insight Extractor
|
|
3
|
-
*
|
|
4
|
-
* Core logic: extract source-grounded insight candidates from sampled evidence.
|
|
5
|
-
* Rules-based (no LLM required) using keyword patterns, frequency counts, and
|
|
6
|
-
* temporal clustering. Each insight carries type, summary, sourceRefs, and confidence.
|
|
7
|
-
*
|
|
8
|
-
* - Pattern: recurring themes across multiple evidence entries.
|
|
9
|
-
* - Learning: new skills or behaviors observed over time.
|
|
10
|
-
* - Observation: one-off notable events.
|
|
11
|
-
* - Conflict: contradictory claims or repeated failures.
|
|
12
|
-
* Test coverage: tests/unit/dream/t7-1-3-insight-extraction.test.ts
|
|
13
|
-
*/
|
|
14
|
-
// Keywords that indicate learning/development
|
|
15
|
-
const LEARNING_KEYWORDS = [
|
|
16
|
-
"learned", "learn", "figured out", "discovered", "understood",
|
|
17
|
-
"new approach", "different way", "better method", "improved",
|
|
18
|
-
];
|
|
19
|
-
// Keywords that indicate recurring patterns
|
|
20
|
-
const PATTERN_KEYWORDS = [
|
|
21
|
-
"again", "regularly", "routine", "habit", "consistently",
|
|
22
|
-
"every", "frequently", "pattern", "trend",
|
|
23
|
-
];
|
|
24
|
-
// Keywords that indicate conflict or failure
|
|
25
|
-
const CONFLICT_KEYWORDS = [
|
|
26
|
-
"failed", "failure", "error", "bug", "broke", "broken",
|
|
27
|
-
"conflict", "contradict", "disagree", "mismatch", "unexpected",
|
|
28
|
-
"not working", "does not work", "issue", "problem",
|
|
29
|
-
];
|
|
30
|
-
function countKeywordMatches(text, keywords) {
|
|
31
|
-
const lower = text.toLowerCase();
|
|
32
|
-
return keywords.reduce((count, kw) => {
|
|
33
|
-
const regex = new RegExp(kw.replace(/\s+/g, "\\s+"), "g");
|
|
34
|
-
const matches = lower.match(regex);
|
|
35
|
-
return count + (matches ? matches.length : 0);
|
|
36
|
-
}, 0);
|
|
37
|
-
}
|
|
38
|
-
function groupByDay(items) {
|
|
39
|
-
const groups = new Map();
|
|
40
|
-
for (const item of items) {
|
|
41
|
-
const day = item.createdAt.slice(0, 10);
|
|
42
|
-
const existing = groups.get(day) ?? [];
|
|
43
|
-
existing.push(item.summary);
|
|
44
|
-
groups.set(day, existing);
|
|
45
|
-
}
|
|
46
|
-
return groups;
|
|
47
|
-
}
|
|
48
|
-
function findRecurringThemes(items) {
|
|
49
|
-
const themeCounts = new Map();
|
|
50
|
-
for (const item of items) {
|
|
51
|
-
const words = item.summary.toLowerCase().split(/\s+/).filter((w) => w.length > 4);
|
|
52
|
-
for (const word of words) {
|
|
53
|
-
const entry = themeCounts.get(word) ?? { count: 0, sourceIds: [] };
|
|
54
|
-
entry.count++;
|
|
55
|
-
if (!entry.sourceIds.includes(item.id)) {
|
|
56
|
-
entry.sourceIds.push(item.id);
|
|
57
|
-
}
|
|
58
|
-
themeCounts.set(word, entry);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return Array.from(themeCounts.entries())
|
|
62
|
-
.filter(([, v]) => v.count >= 3 && v.sourceIds.length >= 2)
|
|
63
|
-
.map(([theme, v]) => ({ theme, count: v.count, sourceIds: v.sourceIds }))
|
|
64
|
-
.sort((a, b) => b.count - a.count)
|
|
65
|
-
.slice(0, 5);
|
|
66
|
-
}
|
|
67
|
-
export function extractInsights(input) {
|
|
68
|
-
const allItems = [
|
|
69
|
-
...input.evidenceSummaries.map((e) => ({ ...e, origin: "evidence" })),
|
|
70
|
-
...input.chronicleSummaries.map((c) => ({ ...c, origin: "chronicle" })),
|
|
71
|
-
];
|
|
72
|
-
const insights = [];
|
|
73
|
-
const unsupportedClaims = [];
|
|
74
|
-
if (allItems.length === 0) {
|
|
75
|
-
return { insights: [], unsupportedClaims: ["no_evidence_for_insight"] };
|
|
76
|
-
}
|
|
77
|
-
// 1. Pattern insights — recurring themes
|
|
78
|
-
const themes = findRecurringThemes(allItems);
|
|
79
|
-
for (const theme of themes) {
|
|
80
|
-
insights.push({
|
|
81
|
-
id: `insight:pattern:${theme.theme}:${crypto.randomUUID()}`,
|
|
82
|
-
type: "pattern",
|
|
83
|
-
summary: `Recurring theme "${theme.theme}" observed across ${theme.sourceIds.length} entries`,
|
|
84
|
-
sourceRefs: theme.sourceIds.slice(0, 10),
|
|
85
|
-
confidence: Math.min(0.95, 0.5 + theme.count * 0.05),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
// 2. Learning insights — entries with learning keywords
|
|
89
|
-
const learningItems = allItems.filter((item) => countKeywordMatches(item.summary, LEARNING_KEYWORDS) > 0);
|
|
90
|
-
if (learningItems.length > 0) {
|
|
91
|
-
insights.push({
|
|
92
|
-
id: `insight:learning:${crypto.randomUUID()}`,
|
|
93
|
-
type: "learning",
|
|
94
|
-
summary: `Learning observed in ${learningItems.length} entries: ${learningItems[0].summary.slice(0, 60)}...`,
|
|
95
|
-
sourceRefs: learningItems.map((i) => i.id).slice(0, 10),
|
|
96
|
-
confidence: Math.min(0.9, 0.4 + learningItems.length * 0.1),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
// 3. Conflict insights — entries with conflict/failure keywords
|
|
100
|
-
const conflictItems = allItems.filter((item) => countKeywordMatches(item.summary, CONFLICT_KEYWORDS) > 0);
|
|
101
|
-
if (conflictItems.length >= 2) {
|
|
102
|
-
insights.push({
|
|
103
|
-
id: `insight:conflict:${crypto.randomUUID()}`,
|
|
104
|
-
type: "conflict",
|
|
105
|
-
summary: `Repeated issues observed in ${conflictItems.length} entries`,
|
|
106
|
-
sourceRefs: conflictItems.map((i) => i.id).slice(0, 10),
|
|
107
|
-
confidence: Math.min(0.85, 0.5 + conflictItems.length * 0.05),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
// 4. Observation insights — notable one-off events (high-activity days)
|
|
111
|
-
const byDay = groupByDay(allItems);
|
|
112
|
-
let maxDay = "";
|
|
113
|
-
let maxCount = 0;
|
|
114
|
-
for (const [day, summaries] of byDay) {
|
|
115
|
-
if (summaries.length > maxCount) {
|
|
116
|
-
maxCount = summaries.length;
|
|
117
|
-
maxDay = day;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (maxCount >= 3 && maxDay) {
|
|
121
|
-
const dayItems = allItems.filter((i) => i.createdAt.startsWith(maxDay));
|
|
122
|
-
insights.push({
|
|
123
|
-
id: `insight:observation:${crypto.randomUUID()}`,
|
|
124
|
-
type: "observation",
|
|
125
|
-
summary: `High activity day (${maxDay}): ${maxCount} notable events`,
|
|
126
|
-
sourceRefs: dayItems.map((i) => i.id).slice(0, 10),
|
|
127
|
-
confidence: Math.min(0.8, 0.4 + maxCount * 0.05),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
// If no insights extracted, record unsupported claim
|
|
131
|
-
if (insights.length === 0) {
|
|
132
|
-
unsupportedClaims.push("no_insight_patterns_detected");
|
|
133
|
-
}
|
|
134
|
-
return { insights, unsupportedClaims };
|
|
135
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Insight Extractor
|
|
3
|
+
*
|
|
4
|
+
* Core logic: extract source-grounded insight candidates from sampled evidence.
|
|
5
|
+
* Rules-based (no LLM required) using keyword patterns, frequency counts, and
|
|
6
|
+
* temporal clustering. Each insight carries type, summary, sourceRefs, and confidence.
|
|
7
|
+
*
|
|
8
|
+
* - Pattern: recurring themes across multiple evidence entries.
|
|
9
|
+
* - Learning: new skills or behaviors observed over time.
|
|
10
|
+
* - Observation: one-off notable events.
|
|
11
|
+
* - Conflict: contradictory claims or repeated failures.
|
|
12
|
+
* Test coverage: tests/unit/dream/t7-1-3-insight-extraction.test.ts
|
|
13
|
+
*/
|
|
14
|
+
// Keywords that indicate learning/development
|
|
15
|
+
const LEARNING_KEYWORDS = [
|
|
16
|
+
"learned", "learn", "figured out", "discovered", "understood",
|
|
17
|
+
"new approach", "different way", "better method", "improved",
|
|
18
|
+
];
|
|
19
|
+
// Keywords that indicate recurring patterns
|
|
20
|
+
const PATTERN_KEYWORDS = [
|
|
21
|
+
"again", "regularly", "routine", "habit", "consistently",
|
|
22
|
+
"every", "frequently", "pattern", "trend",
|
|
23
|
+
];
|
|
24
|
+
// Keywords that indicate conflict or failure
|
|
25
|
+
const CONFLICT_KEYWORDS = [
|
|
26
|
+
"failed", "failure", "error", "bug", "broke", "broken",
|
|
27
|
+
"conflict", "contradict", "disagree", "mismatch", "unexpected",
|
|
28
|
+
"not working", "does not work", "issue", "problem",
|
|
29
|
+
];
|
|
30
|
+
function countKeywordMatches(text, keywords) {
|
|
31
|
+
const lower = text.toLowerCase();
|
|
32
|
+
return keywords.reduce((count, kw) => {
|
|
33
|
+
const regex = new RegExp(kw.replace(/\s+/g, "\\s+"), "g");
|
|
34
|
+
const matches = lower.match(regex);
|
|
35
|
+
return count + (matches ? matches.length : 0);
|
|
36
|
+
}, 0);
|
|
37
|
+
}
|
|
38
|
+
function groupByDay(items) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const day = item.createdAt.slice(0, 10);
|
|
42
|
+
const existing = groups.get(day) ?? [];
|
|
43
|
+
existing.push(item.summary);
|
|
44
|
+
groups.set(day, existing);
|
|
45
|
+
}
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
function findRecurringThemes(items) {
|
|
49
|
+
const themeCounts = new Map();
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const words = item.summary.toLowerCase().split(/\s+/).filter((w) => w.length > 4);
|
|
52
|
+
for (const word of words) {
|
|
53
|
+
const entry = themeCounts.get(word) ?? { count: 0, sourceIds: [] };
|
|
54
|
+
entry.count++;
|
|
55
|
+
if (!entry.sourceIds.includes(item.id)) {
|
|
56
|
+
entry.sourceIds.push(item.id);
|
|
57
|
+
}
|
|
58
|
+
themeCounts.set(word, entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Array.from(themeCounts.entries())
|
|
62
|
+
.filter(([, v]) => v.count >= 3 && v.sourceIds.length >= 2)
|
|
63
|
+
.map(([theme, v]) => ({ theme, count: v.count, sourceIds: v.sourceIds }))
|
|
64
|
+
.sort((a, b) => b.count - a.count)
|
|
65
|
+
.slice(0, 5);
|
|
66
|
+
}
|
|
67
|
+
export function extractInsights(input) {
|
|
68
|
+
const allItems = [
|
|
69
|
+
...input.evidenceSummaries.map((e) => ({ ...e, origin: "evidence" })),
|
|
70
|
+
...input.chronicleSummaries.map((c) => ({ ...c, origin: "chronicle" })),
|
|
71
|
+
];
|
|
72
|
+
const insights = [];
|
|
73
|
+
const unsupportedClaims = [];
|
|
74
|
+
if (allItems.length === 0) {
|
|
75
|
+
return { insights: [], unsupportedClaims: ["no_evidence_for_insight"] };
|
|
76
|
+
}
|
|
77
|
+
// 1. Pattern insights — recurring themes
|
|
78
|
+
const themes = findRecurringThemes(allItems);
|
|
79
|
+
for (const theme of themes) {
|
|
80
|
+
insights.push({
|
|
81
|
+
id: `insight:pattern:${theme.theme}:${crypto.randomUUID()}`,
|
|
82
|
+
type: "pattern",
|
|
83
|
+
summary: `Recurring theme "${theme.theme}" observed across ${theme.sourceIds.length} entries`,
|
|
84
|
+
sourceRefs: theme.sourceIds.slice(0, 10),
|
|
85
|
+
confidence: Math.min(0.95, 0.5 + theme.count * 0.05),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// 2. Learning insights — entries with learning keywords
|
|
89
|
+
const learningItems = allItems.filter((item) => countKeywordMatches(item.summary, LEARNING_KEYWORDS) > 0);
|
|
90
|
+
if (learningItems.length > 0) {
|
|
91
|
+
insights.push({
|
|
92
|
+
id: `insight:learning:${crypto.randomUUID()}`,
|
|
93
|
+
type: "learning",
|
|
94
|
+
summary: `Learning observed in ${learningItems.length} entries: ${learningItems[0].summary.slice(0, 60)}...`,
|
|
95
|
+
sourceRefs: learningItems.map((i) => i.id).slice(0, 10),
|
|
96
|
+
confidence: Math.min(0.9, 0.4 + learningItems.length * 0.1),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// 3. Conflict insights — entries with conflict/failure keywords
|
|
100
|
+
const conflictItems = allItems.filter((item) => countKeywordMatches(item.summary, CONFLICT_KEYWORDS) > 0);
|
|
101
|
+
if (conflictItems.length >= 2) {
|
|
102
|
+
insights.push({
|
|
103
|
+
id: `insight:conflict:${crypto.randomUUID()}`,
|
|
104
|
+
type: "conflict",
|
|
105
|
+
summary: `Repeated issues observed in ${conflictItems.length} entries`,
|
|
106
|
+
sourceRefs: conflictItems.map((i) => i.id).slice(0, 10),
|
|
107
|
+
confidence: Math.min(0.85, 0.5 + conflictItems.length * 0.05),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 4. Observation insights — notable one-off events (high-activity days)
|
|
111
|
+
const byDay = groupByDay(allItems);
|
|
112
|
+
let maxDay = "";
|
|
113
|
+
let maxCount = 0;
|
|
114
|
+
for (const [day, summaries] of byDay) {
|
|
115
|
+
if (summaries.length > maxCount) {
|
|
116
|
+
maxCount = summaries.length;
|
|
117
|
+
maxDay = day;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (maxCount >= 3 && maxDay) {
|
|
121
|
+
const dayItems = allItems.filter((i) => i.createdAt.startsWith(maxDay));
|
|
122
|
+
insights.push({
|
|
123
|
+
id: `insight:observation:${crypto.randomUUID()}`,
|
|
124
|
+
type: "observation",
|
|
125
|
+
summary: `High activity day (${maxDay}): ${maxCount} notable events`,
|
|
126
|
+
sourceRefs: dayItems.map((i) => i.id).slice(0, 10),
|
|
127
|
+
confidence: Math.min(0.8, 0.4 + maxCount * 0.05),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// If no insights extracted, record unsupported claim
|
|
131
|
+
if (insights.length === 0) {
|
|
132
|
+
unsupportedClaims.push("no_insight_patterns_detected");
|
|
133
|
+
}
|
|
134
|
+
return { insights, unsupportedClaims };
|
|
135
|
+
}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rules-based memory consolidation.
|
|
3
|
-
*
|
|
4
|
-
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
-
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
-
*
|
|
7
|
-
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
-
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
-
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
-
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
-
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
-
*/
|
|
13
|
-
import type { CanonicalMemoryEntry, SourceRef } from "../storage/memory-store/memory-store-lifecycle.js";
|
|
14
|
-
export interface ConsolidationInput {
|
|
15
|
-
evidenceSummaries: Array<{
|
|
16
|
-
id: string;
|
|
17
|
-
summary: string;
|
|
18
|
-
sourceRefs: SourceRef[];
|
|
19
|
-
createdAt: string;
|
|
20
|
-
sensitivity?: string;
|
|
21
|
-
}>;
|
|
22
|
-
chronicleSummaries: Array<{
|
|
23
|
-
id: string;
|
|
24
|
-
summary: string;
|
|
25
|
-
sourceRefs: SourceRef[];
|
|
26
|
-
createdAt: string;
|
|
27
|
-
}>;
|
|
28
|
-
toolExperienceSummaries?: Array<{
|
|
29
|
-
id: string;
|
|
30
|
-
summary: string;
|
|
31
|
-
sourceRefs: SourceRef[];
|
|
32
|
-
createdAt: string;
|
|
33
|
-
}>;
|
|
34
|
-
existingEntries: CanonicalMemoryEntry[];
|
|
35
|
-
}
|
|
36
|
-
export interface ConsolidationResult {
|
|
37
|
-
entries: CanonicalMemoryEntry[];
|
|
38
|
-
conflicts: Array<{
|
|
39
|
-
entryId: string;
|
|
40
|
-
reason: string;
|
|
41
|
-
}>;
|
|
42
|
-
staleCount: number;
|
|
43
|
-
dedupeCount: number;
|
|
44
|
-
}
|
|
45
|
-
export declare function consolidateMemory(input: ConsolidationInput): ConsolidationResult;
|
|
1
|
+
/**
|
|
2
|
+
* Rules-based memory consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
+
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
+
*
|
|
7
|
+
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
+
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
+
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
+
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
+
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import type { CanonicalMemoryEntry, SourceRef } from "../storage/memory-store/memory-store-lifecycle.js";
|
|
14
|
+
export interface ConsolidationInput {
|
|
15
|
+
evidenceSummaries: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
sourceRefs: SourceRef[];
|
|
19
|
+
createdAt: string;
|
|
20
|
+
sensitivity?: string;
|
|
21
|
+
}>;
|
|
22
|
+
chronicleSummaries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
sourceRefs: SourceRef[];
|
|
26
|
+
createdAt: string;
|
|
27
|
+
}>;
|
|
28
|
+
toolExperienceSummaries?: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
sourceRefs: SourceRef[];
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}>;
|
|
34
|
+
existingEntries: CanonicalMemoryEntry[];
|
|
35
|
+
}
|
|
36
|
+
export interface ConsolidationResult {
|
|
37
|
+
entries: CanonicalMemoryEntry[];
|
|
38
|
+
conflicts: Array<{
|
|
39
|
+
entryId: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
}>;
|
|
42
|
+
staleCount: number;
|
|
43
|
+
dedupeCount: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function consolidateMemory(input: ConsolidationInput): ConsolidationResult;
|
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rules-based memory consolidation.
|
|
3
|
-
*
|
|
4
|
-
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
-
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
-
*
|
|
7
|
-
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
-
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
-
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
-
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
-
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
-
*/
|
|
13
|
-
const STALE_DAYS = 90;
|
|
14
|
-
function isStale(createdAt) {
|
|
15
|
-
const then = new Date(createdAt).getTime();
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
return now - then > STALE_DAYS * 24 * 60 * 60 * 1000;
|
|
18
|
-
}
|
|
19
|
-
function keyForSourceRefs(refs) {
|
|
20
|
-
return refs
|
|
21
|
-
.map((r) => `${r.sourceId}:${r.kind}`)
|
|
22
|
-
.sort()
|
|
23
|
-
.join("|");
|
|
24
|
-
}
|
|
25
|
-
function summariesSimilar(a, b) {
|
|
26
|
-
const norm = (s) => s
|
|
27
|
-
.toLowerCase()
|
|
28
|
-
.replace(/[^a-z0-9\s]/g, "")
|
|
29
|
-
.split(/\s+/)
|
|
30
|
-
.filter(Boolean)
|
|
31
|
-
.slice(0, 5)
|
|
32
|
-
.join(" ");
|
|
33
|
-
return norm(a) === norm(b);
|
|
34
|
-
}
|
|
35
|
-
export function consolidateMemory(input) {
|
|
36
|
-
const allRaw = [
|
|
37
|
-
...input.evidenceSummaries.map((e) => ({
|
|
38
|
-
...e,
|
|
39
|
-
origin: "evidence",
|
|
40
|
-
})),
|
|
41
|
-
...input.chronicleSummaries.map((c) => ({
|
|
42
|
-
...c,
|
|
43
|
-
origin: "chronicle",
|
|
44
|
-
})),
|
|
45
|
-
...(input.toolExperienceSummaries ?? []).map((t) => ({
|
|
46
|
-
...t,
|
|
47
|
-
origin: "tool_experience",
|
|
48
|
-
})),
|
|
49
|
-
...input.existingEntries.map((e) => ({
|
|
50
|
-
id: e.entryId,
|
|
51
|
-
summary: e.summary,
|
|
52
|
-
sourceRefs: e.sourceRefs,
|
|
53
|
-
createdAt: e.createdAt,
|
|
54
|
-
origin: "memory",
|
|
55
|
-
})),
|
|
56
|
-
];
|
|
57
|
-
// 1. Deduplicate by sourceRef key, keep most recent
|
|
58
|
-
const bySourceKey = new Map();
|
|
59
|
-
for (const item of allRaw) {
|
|
60
|
-
const key = keyForSourceRefs(item.sourceRefs);
|
|
61
|
-
const existing = bySourceKey.get(key);
|
|
62
|
-
if (!existing || item.createdAt > existing.createdAt) {
|
|
63
|
-
bySourceKey.set(key, item);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const deduped = Array.from(bySourceKey.values());
|
|
67
|
-
const dedupeCount = allRaw.length - deduped.length;
|
|
68
|
-
// 2. Merge similar summaries
|
|
69
|
-
const merged = [];
|
|
70
|
-
const used = new Set();
|
|
71
|
-
for (const item of deduped) {
|
|
72
|
-
if (used.has(item.id))
|
|
73
|
-
continue;
|
|
74
|
-
const group = [item];
|
|
75
|
-
for (const other of deduped) {
|
|
76
|
-
if (other.id === item.id || used.has(other.id))
|
|
77
|
-
continue;
|
|
78
|
-
if (summariesSimilar(item.summary, other.summary)) {
|
|
79
|
-
group.push(other);
|
|
80
|
-
used.add(other.id);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
used.add(item.id);
|
|
84
|
-
const mergedRefs = [];
|
|
85
|
-
const seenRefKeys = new Set();
|
|
86
|
-
for (const g of group) {
|
|
87
|
-
for (const ref of g.sourceRefs) {
|
|
88
|
-
const rk = `${ref.sourceId}:${ref.kind}`;
|
|
89
|
-
if (!seenRefKeys.has(rk)) {
|
|
90
|
-
seenRefKeys.add(rk);
|
|
91
|
-
mergedRefs.push(ref);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const latestCreatedAt = group
|
|
96
|
-
.map((g) => g.createdAt)
|
|
97
|
-
.sort()
|
|
98
|
-
.at(-1);
|
|
99
|
-
merged.push({
|
|
100
|
-
entryId: `consolidated:${crypto.randomUUID()}`,
|
|
101
|
-
kind: group[0].origin,
|
|
102
|
-
summary: group[0].summary,
|
|
103
|
-
sourceRefs: mergedRefs,
|
|
104
|
-
createdAt: latestCreatedAt,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
// 3. Stale marking
|
|
108
|
-
let staleCount = 0;
|
|
109
|
-
for (const entry of merged) {
|
|
110
|
-
if (isStale(entry.createdAt)) {
|
|
111
|
-
staleCount++;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// 4. Conflict detection: entries with overlapping sourceRefs but different summaries
|
|
115
|
-
const conflicts = [];
|
|
116
|
-
for (let i = 0; i < merged.length; i++) {
|
|
117
|
-
for (let j = i + 1; j < merged.length; j++) {
|
|
118
|
-
const a = merged[i];
|
|
119
|
-
const b = merged[j];
|
|
120
|
-
const aKeys = new Set(a.sourceRefs.map((r) => `${r.sourceId}:${r.kind}`));
|
|
121
|
-
const overlap = b.sourceRefs.some((r) => aKeys.has(`${r.sourceId}:${r.kind}`));
|
|
122
|
-
if (overlap && !summariesSimilar(a.summary, b.summary)) {
|
|
123
|
-
conflicts.push({
|
|
124
|
-
entryId: a.entryId,
|
|
125
|
-
reason: `conflict_with:${b.entryId}`,
|
|
126
|
-
});
|
|
127
|
-
conflicts.push({
|
|
128
|
-
entryId: b.entryId,
|
|
129
|
-
reason: `conflict_with:${a.entryId}`,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
entries: merged,
|
|
136
|
-
conflicts,
|
|
137
|
-
staleCount,
|
|
138
|
-
dedupeCount,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Rules-based memory consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: dedupe, merge, stale cleanup, and conflict marking on evidence,
|
|
5
|
+
* chronicle, and existing memory entries. No LLM required.
|
|
6
|
+
*
|
|
7
|
+
* - Deduplicate by sourceRef id + kind; keep the most recent.
|
|
8
|
+
* - Merge entries with same kind + similar summary (naive prefix match).
|
|
9
|
+
* - Mark entries older than 90 days as stale (retain but flag).
|
|
10
|
+
* - Mark entries with conflicting sourceRefs as conflict.
|
|
11
|
+
* Test coverage: tests/integration/dream/t7-1-1-dream-pipeline.test.ts
|
|
12
|
+
*/
|
|
13
|
+
const STALE_DAYS = 90;
|
|
14
|
+
function isStale(createdAt) {
|
|
15
|
+
const then = new Date(createdAt).getTime();
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
return now - then > STALE_DAYS * 24 * 60 * 60 * 1000;
|
|
18
|
+
}
|
|
19
|
+
function keyForSourceRefs(refs) {
|
|
20
|
+
return refs
|
|
21
|
+
.map((r) => `${r.sourceId}:${r.kind}`)
|
|
22
|
+
.sort()
|
|
23
|
+
.join("|");
|
|
24
|
+
}
|
|
25
|
+
function summariesSimilar(a, b) {
|
|
26
|
+
const norm = (s) => s
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.slice(0, 5)
|
|
32
|
+
.join(" ");
|
|
33
|
+
return norm(a) === norm(b);
|
|
34
|
+
}
|
|
35
|
+
export function consolidateMemory(input) {
|
|
36
|
+
const allRaw = [
|
|
37
|
+
...input.evidenceSummaries.map((e) => ({
|
|
38
|
+
...e,
|
|
39
|
+
origin: "evidence",
|
|
40
|
+
})),
|
|
41
|
+
...input.chronicleSummaries.map((c) => ({
|
|
42
|
+
...c,
|
|
43
|
+
origin: "chronicle",
|
|
44
|
+
})),
|
|
45
|
+
...(input.toolExperienceSummaries ?? []).map((t) => ({
|
|
46
|
+
...t,
|
|
47
|
+
origin: "tool_experience",
|
|
48
|
+
})),
|
|
49
|
+
...input.existingEntries.map((e) => ({
|
|
50
|
+
id: e.entryId,
|
|
51
|
+
summary: e.summary,
|
|
52
|
+
sourceRefs: e.sourceRefs,
|
|
53
|
+
createdAt: e.createdAt,
|
|
54
|
+
origin: "memory",
|
|
55
|
+
})),
|
|
56
|
+
];
|
|
57
|
+
// 1. Deduplicate by sourceRef key, keep most recent
|
|
58
|
+
const bySourceKey = new Map();
|
|
59
|
+
for (const item of allRaw) {
|
|
60
|
+
const key = keyForSourceRefs(item.sourceRefs);
|
|
61
|
+
const existing = bySourceKey.get(key);
|
|
62
|
+
if (!existing || item.createdAt > existing.createdAt) {
|
|
63
|
+
bySourceKey.set(key, item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const deduped = Array.from(bySourceKey.values());
|
|
67
|
+
const dedupeCount = allRaw.length - deduped.length;
|
|
68
|
+
// 2. Merge similar summaries
|
|
69
|
+
const merged = [];
|
|
70
|
+
const used = new Set();
|
|
71
|
+
for (const item of deduped) {
|
|
72
|
+
if (used.has(item.id))
|
|
73
|
+
continue;
|
|
74
|
+
const group = [item];
|
|
75
|
+
for (const other of deduped) {
|
|
76
|
+
if (other.id === item.id || used.has(other.id))
|
|
77
|
+
continue;
|
|
78
|
+
if (summariesSimilar(item.summary, other.summary)) {
|
|
79
|
+
group.push(other);
|
|
80
|
+
used.add(other.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
used.add(item.id);
|
|
84
|
+
const mergedRefs = [];
|
|
85
|
+
const seenRefKeys = new Set();
|
|
86
|
+
for (const g of group) {
|
|
87
|
+
for (const ref of g.sourceRefs) {
|
|
88
|
+
const rk = `${ref.sourceId}:${ref.kind}`;
|
|
89
|
+
if (!seenRefKeys.has(rk)) {
|
|
90
|
+
seenRefKeys.add(rk);
|
|
91
|
+
mergedRefs.push(ref);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const latestCreatedAt = group
|
|
96
|
+
.map((g) => g.createdAt)
|
|
97
|
+
.sort()
|
|
98
|
+
.at(-1);
|
|
99
|
+
merged.push({
|
|
100
|
+
entryId: `consolidated:${crypto.randomUUID()}`,
|
|
101
|
+
kind: group[0].origin,
|
|
102
|
+
summary: group[0].summary,
|
|
103
|
+
sourceRefs: mergedRefs,
|
|
104
|
+
createdAt: latestCreatedAt,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 3. Stale marking
|
|
108
|
+
let staleCount = 0;
|
|
109
|
+
for (const entry of merged) {
|
|
110
|
+
if (isStale(entry.createdAt)) {
|
|
111
|
+
staleCount++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 4. Conflict detection: entries with overlapping sourceRefs but different summaries
|
|
115
|
+
const conflicts = [];
|
|
116
|
+
for (let i = 0; i < merged.length; i++) {
|
|
117
|
+
for (let j = i + 1; j < merged.length; j++) {
|
|
118
|
+
const a = merged[i];
|
|
119
|
+
const b = merged[j];
|
|
120
|
+
const aKeys = new Set(a.sourceRefs.map((r) => `${r.sourceId}:${r.kind}`));
|
|
121
|
+
const overlap = b.sourceRefs.some((r) => aKeys.has(`${r.sourceId}:${r.kind}`));
|
|
122
|
+
if (overlap && !summariesSimilar(a.summary, b.summary)) {
|
|
123
|
+
conflicts.push({
|
|
124
|
+
entryId: a.entryId,
|
|
125
|
+
reason: `conflict_with:${b.entryId}`,
|
|
126
|
+
});
|
|
127
|
+
conflicts.push({
|
|
128
|
+
entryId: b.entryId,
|
|
129
|
+
reason: `conflict_with:${a.entryId}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
entries: merged,
|
|
136
|
+
conflicts,
|
|
137
|
+
staleCount,
|
|
138
|
+
dedupeCount,
|
|
139
|
+
};
|
|
140
|
+
}
|