pi-automem-bridge 0.2.0 → 0.2.2
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/README.md +75 -29
- package/examples/config.advanced.json +61 -59
- package/package.json +61 -58
- package/src/config.ts +262 -251
- package/src/mcp-client.ts +401 -361
- package/src/project-detect.ts +96 -94
- package/src/recall.ts +283 -254
- package/src/tools/memory-tools.ts +307 -307
- package/src/tools/relationship-tools.ts +121 -114
- package/src/write-policy.ts +148 -142
|
@@ -1,114 +1,121 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { loadConfig, type MemoryType } from "../config";
|
|
4
|
-
import { automemStore, automemAssociate, setAutoMemMcpServerName } from "../mcp-client";
|
|
5
|
-
import { evaluateWritePolicy, type MemoryCandidate } from "../write-policy";
|
|
6
|
-
|
|
7
|
-
const LinkParams = Type.Object({
|
|
8
|
-
memoryId1: Type.String({ description: "ID of the first memory." }),
|
|
9
|
-
memoryId2: Type.String({ description: "ID of the second memory." }),
|
|
10
|
-
relationship: Type.String({ description: "Relationship type: RELATES_TO, LEADS_TO, OCCURRED_BEFORE, PREFERS_OVER, EXEMPLIFIES, CONTRADICTS, REINFORCES, INVALIDATED_BY, EVOLVED_INTO, DERIVED_FROM, PART_OF" }),
|
|
11
|
-
strength: Type.Optional(Type.Number({ description: "Relationship weight 0–1. Default 0.5." })),
|
|
12
|
-
approvedByUser: Type.Boolean({ description: "Must be true to execute. Prevents accidental linking." }),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const CorrectParams = Type.Object({
|
|
16
|
-
memoryId: Type.String({ description: "ID of the memory being corrected." }),
|
|
17
|
-
correction: Type.String({ description: "New correct content to store." }),
|
|
18
|
-
type: Type.Optional(Type.String({ description: "Memory type for the new memory. Default: Context." })),
|
|
19
|
-
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for the new memory." })),
|
|
20
|
-
importance: Type.Optional(Type.Number({ description: "Importance 0–1 for the new memory." })),
|
|
21
|
-
relationship: Type.Optional(Type.String({ description: "EVOLVED_INTO or CONTRADICTS. Default: EVOLVED_INTO." })),
|
|
22
|
-
approvedByUser: Type.Boolean({ description: "Must be true to execute." }),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export function registerRelationshipTools(pi: ExtensionAPI) {
|
|
26
|
-
pi.registerTool({
|
|
27
|
-
name: "automem_link_memories",
|
|
28
|
-
label: "AutoMem Link Memories",
|
|
29
|
-
description: "Create a typed relationship between two existing AutoMem memories.",
|
|
30
|
-
promptSnippet: "Use after identifying that two memories are related. Requires both memory IDs and the relationship type.",
|
|
31
|
-
parameters: LinkParams,
|
|
32
|
-
async execute(_toolCallId: string, params: any) {
|
|
33
|
-
const config = loadConfig();
|
|
34
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
35
|
-
|
|
36
|
-
if (!params.approvedByUser) {
|
|
37
|
-
return {
|
|
38
|
-
content: [{ type: "text" as const, text: "Confirmation required before linking memories. Re-run with approvedByUser=true only after explicit user approval.\n\nWould link:\n " + params.memoryId1 + " → " + params.relationship + " → " + params.memoryId2 }],
|
|
39
|
-
isError: true,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const strength = typeof params.strength === "number" ? params.strength : 0.5;
|
|
44
|
-
const result = await automemAssociate(params.memoryId1, params.memoryId2, params.relationship, strength);
|
|
45
|
-
const text = result.content?.[0]?.text || "Association created.";
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text" as const, text: "Linked " + params.memoryId1 + " → " + params.relationship + " → " + params.memoryId2 + " (strength: " + strength + ").\n\n" + text }],
|
|
48
|
-
details: { memoryId1: params.memoryId1, memoryId2: params.memoryId2, relationship: params.relationship, strength },
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
pi.registerTool({
|
|
54
|
-
name: "automem_correct_memory",
|
|
55
|
-
label: "AutoMem Correct Memory",
|
|
56
|
-
description: "Store a correction to an existing memory and link old → new with a provenance relationship. Preserves history; use automem_update_memory for simple in-place edits.",
|
|
57
|
-
promptSnippet: "Use when a memory was wrong or outdated and you want to preserve history. Stores new content as a separate memory, then links old → EVOLVED_INTO/CONTRADICTS → new.",
|
|
58
|
-
parameters: CorrectParams,
|
|
59
|
-
async execute(_toolCallId: string, params: any) {
|
|
60
|
-
const config = loadConfig();
|
|
61
|
-
setAutoMemMcpServerName(config.mcpServerName);
|
|
62
|
-
|
|
63
|
-
const candidate: MemoryCandidate = {
|
|
64
|
-
content: params.correction,
|
|
65
|
-
type: (params.type || "Context") as MemoryType,
|
|
66
|
-
tags: Array.isArray(params.tags) ? params.tags : [],
|
|
67
|
-
importance: params.importance,
|
|
68
|
-
};
|
|
69
|
-
const decision = evaluateWritePolicy(candidate, config);
|
|
70
|
-
if (decision.action === "block") {
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text" as const, text: "Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n") }],
|
|
73
|
-
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings },
|
|
74
|
-
isError: true,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!params.approvedByUser) {
|
|
79
|
-
return {
|
|
80
|
-
content: [{ type: "text" as const, text: "Confirmation required before correcting memory. Re-run with approvedByUser=true only after explicit user approval.\n\nWould correct memory " + params.memoryId + " with:\n " + params.correction }],
|
|
81
|
-
isError: true,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const rel = params.relationship === "CONTRADICTS" ? "CONTRADICTS" : "EVOLVED_INTO";
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { loadConfig, type MemoryType } from "../config";
|
|
4
|
+
import { automemStore, automemAssociate, setAutoMemMcpServerName } from "../mcp-client";
|
|
5
|
+
import { evaluateWritePolicy, type MemoryCandidate } from "../write-policy";
|
|
6
|
+
|
|
7
|
+
const LinkParams = Type.Object({
|
|
8
|
+
memoryId1: Type.String({ description: "ID of the first memory." }),
|
|
9
|
+
memoryId2: Type.String({ description: "ID of the second memory." }),
|
|
10
|
+
relationship: Type.String({ description: "Relationship type: RELATES_TO, LEADS_TO, OCCURRED_BEFORE, PREFERS_OVER, EXEMPLIFIES, CONTRADICTS, REINFORCES, INVALIDATED_BY, EVOLVED_INTO, DERIVED_FROM, PART_OF" }),
|
|
11
|
+
strength: Type.Optional(Type.Number({ description: "Relationship weight 0–1. Default 0.5." })),
|
|
12
|
+
approvedByUser: Type.Boolean({ description: "Must be true to execute. Prevents accidental linking." }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const CorrectParams = Type.Object({
|
|
16
|
+
memoryId: Type.String({ description: "ID of the memory being corrected." }),
|
|
17
|
+
correction: Type.String({ description: "New correct content to store." }),
|
|
18
|
+
type: Type.Optional(Type.String({ description: "Memory type for the new memory. Default: Context." })),
|
|
19
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for the new memory." })),
|
|
20
|
+
importance: Type.Optional(Type.Number({ description: "Importance 0–1 for the new memory." })),
|
|
21
|
+
relationship: Type.Optional(Type.String({ description: "EVOLVED_INTO or CONTRADICTS. Default: EVOLVED_INTO." })),
|
|
22
|
+
approvedByUser: Type.Boolean({ description: "Must be true to execute." }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function registerRelationshipTools(pi: ExtensionAPI) {
|
|
26
|
+
pi.registerTool({
|
|
27
|
+
name: "automem_link_memories",
|
|
28
|
+
label: "AutoMem Link Memories",
|
|
29
|
+
description: "Create a typed relationship between two existing AutoMem memories.",
|
|
30
|
+
promptSnippet: "Use after identifying that two memories are related. Requires both memory IDs and the relationship type.",
|
|
31
|
+
parameters: LinkParams,
|
|
32
|
+
async execute(_toolCallId: string, params: any) {
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
setAutoMemMcpServerName(config.mcpServerName);
|
|
35
|
+
|
|
36
|
+
if (!params.approvedByUser) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text" as const, text: "Confirmation required before linking memories. Re-run with approvedByUser=true only after explicit user approval.\n\nWould link:\n " + params.memoryId1 + " → " + params.relationship + " → " + params.memoryId2 }],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const strength = typeof params.strength === "number" ? params.strength : 0.5;
|
|
44
|
+
const result = await automemAssociate(params.memoryId1, params.memoryId2, params.relationship, strength);
|
|
45
|
+
const text = result.content?.[0]?.text || "Association created.";
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text" as const, text: "Linked " + params.memoryId1 + " → " + params.relationship + " → " + params.memoryId2 + " (strength: " + strength + ").\n\n" + text }],
|
|
48
|
+
details: { memoryId1: params.memoryId1, memoryId2: params.memoryId2, relationship: params.relationship, strength },
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.registerTool({
|
|
54
|
+
name: "automem_correct_memory",
|
|
55
|
+
label: "AutoMem Correct Memory",
|
|
56
|
+
description: "Store a correction to an existing memory and link old → new with a provenance relationship. Preserves history; use automem_update_memory for simple in-place edits.",
|
|
57
|
+
promptSnippet: "Use when a memory was wrong or outdated and you want to preserve history. Stores new content as a separate memory, then links old → EVOLVED_INTO/CONTRADICTS → new.",
|
|
58
|
+
parameters: CorrectParams,
|
|
59
|
+
async execute(_toolCallId: string, params: any) {
|
|
60
|
+
const config = loadConfig();
|
|
61
|
+
setAutoMemMcpServerName(config.mcpServerName);
|
|
62
|
+
|
|
63
|
+
const candidate: MemoryCandidate = {
|
|
64
|
+
content: params.correction,
|
|
65
|
+
type: (params.type || "Context") as MemoryType,
|
|
66
|
+
tags: Array.isArray(params.tags) ? params.tags : [],
|
|
67
|
+
importance: params.importance,
|
|
68
|
+
};
|
|
69
|
+
const decision = evaluateWritePolicy(candidate, config);
|
|
70
|
+
if (decision.action === "block") {
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text" as const, text: "Blocked by AutoMem write policy.\n" + decision.reasons.map((r: string) => "- " + r).join("\n") }],
|
|
73
|
+
details: { action: decision.action, reasons: decision.reasons, findings: decision.findings },
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!params.approvedByUser) {
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text" as const, text: "Confirmation required before correcting memory. Re-run with approvedByUser=true only after explicit user approval.\n\nWould correct memory " + params.memoryId + " with:\n " + params.correction }],
|
|
81
|
+
isError: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rel = params.relationship === "CONTRADICTS" ? "CONTRADICTS" : "EVOLVED_INTO";
|
|
86
|
+
// Store the normalized candidate so corrections get the same alwaysTag,
|
|
87
|
+
// source, and content normalization as every other write path.
|
|
88
|
+
const storeResult = await automemStore(
|
|
89
|
+
decision.normalized.content,
|
|
90
|
+
decision.normalized.type,
|
|
91
|
+
decision.normalized.tags,
|
|
92
|
+
{
|
|
93
|
+
source: decision.normalized.source,
|
|
94
|
+
importance: decision.normalized.importance,
|
|
95
|
+
confidence: decision.normalized.confidence,
|
|
96
|
+
metadata: decision.normalized.metadata,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const storeText = storeResult.content?.[0]?.text || "";
|
|
101
|
+
const uuidMatch = storeText.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
102
|
+
|
|
103
|
+
if (!uuidMatch) {
|
|
104
|
+
console.warn("[automem] automem_correct_memory: could not extract new memory ID from store response — skipping link");
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text" as const, text: "Stored correction (ID unknown — link not created). Store response: " + storeText.slice(0, 300) }],
|
|
107
|
+
details: { storeText },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const newId = uuidMatch[0];
|
|
112
|
+
const assocResult = await automemAssociate(params.memoryId, newId, rel, 0.9);
|
|
113
|
+
const assocText = assocResult.content?.[0]?.text || "Association created.";
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text" as const, text: "Stored correction as " + newId + ". Linked " + params.memoryId + " → " + rel + " → " + newId + ".\n\n" + assocText }],
|
|
117
|
+
details: { originalId: params.memoryId, newId, relationship: rel },
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
package/src/write-policy.ts
CHANGED
|
@@ -1,142 +1,148 @@
|
|
|
1
|
-
import type { AutoMemConfig, MemoryType } from "./config";
|
|
2
|
-
import { scanForSecrets, type SecretFinding } from "./secret-scan";
|
|
3
|
-
|
|
4
|
-
export type WriteMode = "off" | "propose" | "safe-auto" | "confirm-all";
|
|
5
|
-
export type WriteAction = "block" | "propose" | "confirm" | "auto";
|
|
6
|
-
|
|
7
|
-
export interface MemoryCandidate {
|
|
8
|
-
content: string;
|
|
9
|
-
type: MemoryType;
|
|
10
|
-
tags: string[];
|
|
11
|
-
importance: number;
|
|
12
|
-
confidence?: number;
|
|
13
|
-
source?: string;
|
|
14
|
-
category?: string;
|
|
15
|
-
metadata?: Record<string, unknown>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface PolicyDecision {
|
|
19
|
-
action: WriteAction;
|
|
20
|
-
reasons: string[];
|
|
21
|
-
findings: SecretFinding[];
|
|
22
|
-
normalized: MemoryCandidate;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const VALID_TYPES = new Set(["Decision", "Pattern", "Preference", "Style", "Habit", "Insight", "Context"]);
|
|
26
|
-
|
|
27
|
-
export function normalizeCandidate(input: MemoryCandidate, config: AutoMemConfig): MemoryCandidate {
|
|
28
|
-
const tags = Array.from(new Set([
|
|
29
|
-
...(config.writePolicy.alwaysTag || []),
|
|
30
|
-
...(input.tags || []),
|
|
31
|
-
].map(t => String(t).trim().toLowerCase()).filter(Boolean)));
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
content: String(input.content || "").replace(/\s+/g, " ").trim(),
|
|
35
|
-
type: input.type,
|
|
36
|
-
tags,
|
|
37
|
-
importance: clampNumber(input.importance, 0, 1, defaultImportanceForType(input.type)),
|
|
38
|
-
confidence: clampNumber(input.confidence, 0, 1, 0.9),
|
|
39
|
-
source: input.source || config.writePolicy.defaultSource || "pi-session",
|
|
40
|
-
category: (input.category || inferCategory(input.type, tags)).toLowerCase(),
|
|
41
|
-
metadata: input.metadata,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function evaluateWritePolicy(input: MemoryCandidate, config: AutoMemConfig): PolicyDecision {
|
|
46
|
-
const normalized = normalizeCandidate(input, config);
|
|
47
|
-
const reasons: string[] = [];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
if (reasons.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (normalized.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
case "
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
1
|
+
import type { AutoMemConfig, MemoryType } from "./config";
|
|
2
|
+
import { scanForSecrets, type SecretFinding } from "./secret-scan";
|
|
3
|
+
|
|
4
|
+
export type WriteMode = "off" | "propose" | "safe-auto" | "confirm-all";
|
|
5
|
+
export type WriteAction = "block" | "propose" | "confirm" | "auto";
|
|
6
|
+
|
|
7
|
+
export interface MemoryCandidate {
|
|
8
|
+
content: string;
|
|
9
|
+
type: MemoryType;
|
|
10
|
+
tags: string[];
|
|
11
|
+
importance: number;
|
|
12
|
+
confidence?: number;
|
|
13
|
+
source?: string;
|
|
14
|
+
category?: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PolicyDecision {
|
|
19
|
+
action: WriteAction;
|
|
20
|
+
reasons: string[];
|
|
21
|
+
findings: SecretFinding[];
|
|
22
|
+
normalized: MemoryCandidate;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VALID_TYPES = new Set(["Decision", "Pattern", "Preference", "Style", "Habit", "Insight", "Context"]);
|
|
26
|
+
|
|
27
|
+
export function normalizeCandidate(input: MemoryCandidate, config: AutoMemConfig): MemoryCandidate {
|
|
28
|
+
const tags = Array.from(new Set([
|
|
29
|
+
...(config.writePolicy.alwaysTag || []),
|
|
30
|
+
...(input.tags || []),
|
|
31
|
+
].map(t => String(t).trim().toLowerCase()).filter(Boolean)));
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
content: String(input.content || "").replace(/\s+/g, " ").trim(),
|
|
35
|
+
type: input.type,
|
|
36
|
+
tags,
|
|
37
|
+
importance: clampNumber(input.importance, 0, 1, defaultImportanceForType(input.type)),
|
|
38
|
+
confidence: clampNumber(input.confidence, 0, 1, 0.9),
|
|
39
|
+
source: input.source || config.writePolicy.defaultSource || "pi-session",
|
|
40
|
+
category: (input.category || inferCategory(input.type, tags)).toLowerCase(),
|
|
41
|
+
metadata: input.metadata,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function evaluateWritePolicy(input: MemoryCandidate, config: AutoMemConfig): PolicyDecision {
|
|
46
|
+
const normalized = normalizeCandidate(input, config);
|
|
47
|
+
const reasons: string[] = [];
|
|
48
|
+
// Scan content, tags, AND metadata — metadata is model-supplied (Type.Any)
|
|
49
|
+
// and is stored verbatim, so a secret placed there must not bypass the scan.
|
|
50
|
+
let metadataText = "";
|
|
51
|
+
if (normalized.metadata) {
|
|
52
|
+
try { metadataText = "\n" + JSON.stringify(normalized.metadata); } catch (_e) { /* unserializable */ }
|
|
53
|
+
}
|
|
54
|
+
const findings = scanForSecrets(normalized.content + "\n" + normalized.tags.join("\n") + metadataText);
|
|
55
|
+
const mode = ((config.writePolicy as any).mode || "propose") as WriteMode;
|
|
56
|
+
const preferredMax = config.behavior.preferredContentLength || 500;
|
|
57
|
+
const hardMax = config.behavior.maxContentLength || 2000;
|
|
58
|
+
const minImportance = Number((config.writePolicy as any).minImportanceToWrite ?? 0.7);
|
|
59
|
+
|
|
60
|
+
if (mode === "off") reasons.push("write policy mode is off");
|
|
61
|
+
if (!normalized.content) reasons.push("content is empty");
|
|
62
|
+
if (!VALID_TYPES.has(normalized.type)) reasons.push("invalid memory type");
|
|
63
|
+
if (normalized.content.length > hardMax) reasons.push("content exceeds hard length limit");
|
|
64
|
+
if (findings.length > 0) reasons.push("secret/privacy scanner found blocked content");
|
|
65
|
+
if (normalized.importance < minImportance) reasons.push("importance is below configured write threshold");
|
|
66
|
+
if (isBlockedCategory(normalized, config)) reasons.push("category is blocked by write policy");
|
|
67
|
+
|
|
68
|
+
if (reasons.length > 0) {
|
|
69
|
+
return { action: "block", reasons, findings, normalized };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (normalized.content.length > preferredMax) {
|
|
73
|
+
reasons.push("content exceeds preferred embedding length; consider shortening before commit");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (mode === "confirm-all") {
|
|
77
|
+
return { action: "confirm", reasons: ["write policy requires confirmation for all memories", ...reasons], findings, normalized };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isConfirmCategory(normalized, config)) {
|
|
81
|
+
return { action: "confirm", reasons: ["category requires user confirmation", ...reasons], findings, normalized };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (mode === "safe-auto" && isAutoCategory(normalized, config)) {
|
|
85
|
+
return { action: "auto", reasons: ["category is eligible for safe automatic write", ...reasons], findings, normalized };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { action: "propose", reasons: ["candidate should be proposed for approval before storing", ...reasons], findings, normalized };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatCandidate(candidate: MemoryCandidate): string {
|
|
92
|
+
return (
|
|
93
|
+
"Content: " + candidate.content + "\n" +
|
|
94
|
+
"Type: " + candidate.type + "\n" +
|
|
95
|
+
"Importance: " + candidate.importance + "\n" +
|
|
96
|
+
"Tags: " + (candidate.tags.join(", ") || "(none)") + "\n" +
|
|
97
|
+
"Category: " + (candidate.category || "(inferred)")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isBlockedCategory(candidate: MemoryCandidate, config: AutoMemConfig): boolean {
|
|
102
|
+
const blocked = new Set((config.writePolicy.blockedCategories || []).map(c => c.toLowerCase()));
|
|
103
|
+
return blocked.has(candidate.category || "") || candidate.tags.some(t => blocked.has(t));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isConfirmCategory(candidate: MemoryCandidate, config: AutoMemConfig): boolean {
|
|
107
|
+
const confirm = new Set((config.writePolicy.confirmCategories || []).map(c => c.toLowerCase()));
|
|
108
|
+
return confirm.has(candidate.category || "") || candidate.tags.some(t => confirm.has(t));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isAutoCategory(candidate: MemoryCandidate, config: AutoMemConfig): boolean {
|
|
112
|
+
const auto = new Set((config.writePolicy.autoWriteCategories || []).map(c => c.toLowerCase()));
|
|
113
|
+
return auto.has(candidate.category || "") || auto.has(candidate.type.toLowerCase()) || candidate.tags.some(t => auto.has(t));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function inferCategory(type: MemoryType, tags: string[]): string {
|
|
117
|
+
if (tags.includes("preference")) return "preference";
|
|
118
|
+
if (tags.includes("decision")) return "technical-decision";
|
|
119
|
+
if (tags.includes("bug-fix")) return "bug-fix";
|
|
120
|
+
if (tags.includes("pattern")) return "agent-pattern";
|
|
121
|
+
if (tags.includes("private") || tags.includes("personal")) return "private";
|
|
122
|
+
switch (type) {
|
|
123
|
+
case "Decision": return "technical-decision";
|
|
124
|
+
case "Preference": return "preference";
|
|
125
|
+
case "Pattern": return "agent-pattern";
|
|
126
|
+
case "Insight": return "tooling-lesson";
|
|
127
|
+
default: return "context";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function defaultImportanceForType(type: MemoryType): number {
|
|
132
|
+
switch (type) {
|
|
133
|
+
case "Decision": return 0.9;
|
|
134
|
+
case "Preference": return 0.85;
|
|
135
|
+
case "Pattern": return 0.8;
|
|
136
|
+
case "Insight": return 0.8;
|
|
137
|
+
case "Style": return 0.7;
|
|
138
|
+
case "Habit": return 0.65;
|
|
139
|
+
case "Context": return 0.6;
|
|
140
|
+
default: return 0.5;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
145
|
+
const n = Number(value);
|
|
146
|
+
if (!Number.isFinite(n)) return fallback;
|
|
147
|
+
return Math.min(max, Math.max(min, n));
|
|
148
|
+
}
|