memory-braid 0.4.7 → 0.5.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/README.md +59 -0
- package/package.json +1 -1
- package/src/capture.ts +251 -0
- package/src/extract.ts +7 -0
- package/src/index.ts +521 -76
- package/src/local-memory.ts +9 -4
- package/src/logger.ts +6 -1
- package/src/mem0-client.ts +295 -45
- package/src/observability.ts +269 -0
- package/src/remediation.ts +257 -0
- package/src/state.ts +30 -0
- package/src/types.ts +47 -0
package/src/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import {
|
|
4
|
+
assembleCaptureInput,
|
|
5
|
+
getPendingInboundTurn,
|
|
6
|
+
isLikelyTranscriptLikeText,
|
|
7
|
+
isOversizedAtomicMemory,
|
|
8
|
+
matchCandidateToCaptureInput,
|
|
9
|
+
normalizeHookMessages,
|
|
10
|
+
} from "./capture.js";
|
|
6
11
|
import { parseConfig, pluginConfigSchema } from "./config.js";
|
|
7
12
|
import { stagedDedupe } from "./dedupe.js";
|
|
8
13
|
import { EntityExtractionManager } from "./entities.js";
|
|
@@ -11,21 +16,50 @@ import { MemoryBraidLogger } from "./logger.js";
|
|
|
11
16
|
import { resolveLocalTools, runLocalGet, runLocalSearch } from "./local-memory.js";
|
|
12
17
|
import { Mem0Adapter } from "./mem0-client.js";
|
|
13
18
|
import { mergeWithRrf } from "./merge.js";
|
|
19
|
+
import {
|
|
20
|
+
appendUsageWindow,
|
|
21
|
+
createUsageSnapshot,
|
|
22
|
+
summarizeUsageWindow,
|
|
23
|
+
type UsageWindowEntry,
|
|
24
|
+
} from "./observability.js";
|
|
25
|
+
import {
|
|
26
|
+
buildAuditSummary,
|
|
27
|
+
buildQuarantineMetadata,
|
|
28
|
+
formatAuditSummary,
|
|
29
|
+
isQuarantinedMemory,
|
|
30
|
+
selectRemediationTargets,
|
|
31
|
+
type RemediationAction,
|
|
32
|
+
} from "./remediation.js";
|
|
14
33
|
import {
|
|
15
34
|
createStatePaths,
|
|
16
35
|
ensureStateDir,
|
|
17
36
|
readCaptureDedupeState,
|
|
18
37
|
readLifecycleState,
|
|
38
|
+
readRemediationState,
|
|
19
39
|
readStatsState,
|
|
20
40
|
type StatePaths,
|
|
21
41
|
withStateLock,
|
|
22
42
|
writeCaptureDedupeState,
|
|
23
43
|
writeLifecycleState,
|
|
44
|
+
writeRemediationState,
|
|
24
45
|
writeStatsState,
|
|
25
46
|
} from "./state.js";
|
|
26
|
-
import type {
|
|
47
|
+
import type {
|
|
48
|
+
LifecycleEntry,
|
|
49
|
+
MemoryBraidResult,
|
|
50
|
+
PendingInboundTurn,
|
|
51
|
+
ScopeKey,
|
|
52
|
+
} from "./types.js";
|
|
53
|
+
import { PLUGIN_CAPTURE_VERSION } from "./types.js";
|
|
27
54
|
import { normalizeForHash, normalizeWhitespace, sha256 } from "./chunking.js";
|
|
28
55
|
|
|
56
|
+
type ToolContext = {
|
|
57
|
+
config?: unknown;
|
|
58
|
+
workspaceDir?: string;
|
|
59
|
+
agentId?: string;
|
|
60
|
+
sessionKey?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
29
63
|
function jsonToolResult(payload: unknown) {
|
|
30
64
|
return {
|
|
31
65
|
content: [
|
|
@@ -43,7 +77,7 @@ function workspaceHashFromDir(workspaceDir?: string): string {
|
|
|
43
77
|
return sha256(base.toLowerCase());
|
|
44
78
|
}
|
|
45
79
|
|
|
46
|
-
function resolveScopeFromToolContext(ctx:
|
|
80
|
+
function resolveScopeFromToolContext(ctx: ToolContext): ScopeKey {
|
|
47
81
|
return {
|
|
48
82
|
workspaceHash: workspaceHashFromDir(ctx.workspaceDir),
|
|
49
83
|
agentId: (ctx.agentId ?? "main").trim() || "main",
|
|
@@ -63,55 +97,23 @@ function resolveScopeFromHookContext(ctx: {
|
|
|
63
97
|
};
|
|
64
98
|
}
|
|
65
99
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const parts: string[] = [];
|
|
75
|
-
for (const block of content) {
|
|
76
|
-
if (!block || typeof block !== "object") {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const item = block as { type?: unknown; text?: unknown };
|
|
80
|
-
if (item.type === "text" && typeof item.text === "string") {
|
|
81
|
-
const normalized = normalizeWhitespace(item.text);
|
|
82
|
-
if (normalized) {
|
|
83
|
-
parts.push(normalized);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return parts.join(" ");
|
|
100
|
+
function resolveWorkspaceDirFromConfig(config?: unknown): string | undefined {
|
|
101
|
+
const root = asRecord(config);
|
|
102
|
+
const agents = asRecord(root.agents);
|
|
103
|
+
const defaults = asRecord(agents.defaults);
|
|
104
|
+
const workspace =
|
|
105
|
+
typeof defaults.workspace === "string" ? defaults.workspace.trim() : "";
|
|
106
|
+
return workspace || undefined;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (typeof direct.role === "string") {
|
|
99
|
-
const text = extractHookMessageText(direct.content);
|
|
100
|
-
if (text) {
|
|
101
|
-
out.push({ role: direct.role, text });
|
|
102
|
-
}
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const wrapped = entry as { message?: { role?: unknown; content?: unknown } };
|
|
107
|
-
if (wrapped.message && typeof wrapped.message.role === "string") {
|
|
108
|
-
const text = extractHookMessageText(wrapped.message.content);
|
|
109
|
-
if (text) {
|
|
110
|
-
out.push({ role: wrapped.message.role, text });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
109
|
+
function resolveCommandScope(config?: unknown): {
|
|
110
|
+
workspaceHash: string;
|
|
111
|
+
agentId?: string;
|
|
112
|
+
sessionKey?: string;
|
|
113
|
+
} {
|
|
114
|
+
return {
|
|
115
|
+
workspaceHash: workspaceHashFromDir(resolveWorkspaceDirFromConfig(config)),
|
|
116
|
+
};
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
function resolveLatestUserTurnSignature(messages?: unknown[]): string | undefined {
|
|
@@ -664,15 +666,19 @@ function applyTemporalDecayToMem0(params: {
|
|
|
664
666
|
}
|
|
665
667
|
|
|
666
668
|
function resolveLifecycleReferenceTs(entry: LifecycleEntry, reinforceOnRecall: boolean): number {
|
|
667
|
-
const capturedTs =
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
669
|
+
const capturedTs =
|
|
670
|
+
typeof entry.lastCapturedAt === "number" && Number.isFinite(entry.lastCapturedAt)
|
|
671
|
+
? entry.lastCapturedAt
|
|
672
|
+
: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt)
|
|
673
|
+
? entry.createdAt
|
|
674
|
+
: 0;
|
|
672
675
|
if (!reinforceOnRecall) {
|
|
673
676
|
return capturedTs;
|
|
674
677
|
}
|
|
675
|
-
const recalledTs =
|
|
678
|
+
const recalledTs =
|
|
679
|
+
typeof entry.lastRecalledAt === "number" && Number.isFinite(entry.lastRecalledAt)
|
|
680
|
+
? entry.lastRecalledAt
|
|
681
|
+
: 0;
|
|
676
682
|
return Math.max(capturedTs, recalledTs);
|
|
677
683
|
}
|
|
678
684
|
|
|
@@ -852,7 +858,7 @@ async function runHybridRecall(params: {
|
|
|
852
858
|
cfg: ReturnType<typeof parseConfig>;
|
|
853
859
|
mem0: Mem0Adapter;
|
|
854
860
|
log: MemoryBraidLogger;
|
|
855
|
-
ctx:
|
|
861
|
+
ctx: ToolContext;
|
|
856
862
|
statePaths?: StatePaths | null;
|
|
857
863
|
query: string;
|
|
858
864
|
toolCallId?: string;
|
|
@@ -910,9 +916,21 @@ async function runHybridRecall(params: {
|
|
|
910
916
|
scope,
|
|
911
917
|
runId: params.runId,
|
|
912
918
|
});
|
|
919
|
+
const remediationState = params.statePaths
|
|
920
|
+
? await readRemediationState(params.statePaths)
|
|
921
|
+
: undefined;
|
|
922
|
+
let quarantinedFiltered = 0;
|
|
913
923
|
const mem0Search = mem0Raw.filter((result) => {
|
|
914
924
|
const sourceType = asRecord(result.metadata).sourceType;
|
|
915
|
-
|
|
925
|
+
if (sourceType === "markdown" || sourceType === "session") {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
const quarantine = isQuarantinedMemory(result, remediationState);
|
|
929
|
+
if (quarantine.quarantined) {
|
|
930
|
+
quarantinedFiltered += 1;
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
return true;
|
|
916
934
|
});
|
|
917
935
|
let mem0ForMerge = mem0Search;
|
|
918
936
|
if (params.cfg.timeDecay.enabled) {
|
|
@@ -962,6 +980,7 @@ async function runHybridRecall(params: {
|
|
|
962
980
|
sessionKey: scope.sessionKey,
|
|
963
981
|
workspaceHash: scope.workspaceHash,
|
|
964
982
|
inputCount: mem0Search.length,
|
|
983
|
+
quarantinedFiltered,
|
|
965
984
|
adjusted: qualityAdjusted.adjusted,
|
|
966
985
|
overlapBoosted: qualityAdjusted.overlapBoosted,
|
|
967
986
|
overlapPenalized: qualityAdjusted.overlapPenalized,
|
|
@@ -1030,6 +1049,188 @@ async function runHybridRecall(params: {
|
|
|
1030
1049
|
};
|
|
1031
1050
|
}
|
|
1032
1051
|
|
|
1052
|
+
function parseIntegerFlag(tokens: string[], flag: string, fallback: number): number {
|
|
1053
|
+
const index = tokens.findIndex((token) => token === flag);
|
|
1054
|
+
if (index < 0 || index === tokens.length - 1) {
|
|
1055
|
+
return fallback;
|
|
1056
|
+
}
|
|
1057
|
+
const raw = Number(tokens[index + 1]);
|
|
1058
|
+
if (!Number.isFinite(raw)) {
|
|
1059
|
+
return fallback;
|
|
1060
|
+
}
|
|
1061
|
+
return Math.max(1, Math.round(raw));
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function resolveRecordScope(
|
|
1065
|
+
memory: MemoryBraidResult,
|
|
1066
|
+
fallbackScope: { workspaceHash: string; agentId?: string; sessionKey?: string },
|
|
1067
|
+
): ScopeKey {
|
|
1068
|
+
const metadata = asRecord(memory.metadata);
|
|
1069
|
+
const workspaceHash =
|
|
1070
|
+
typeof metadata.workspaceHash === "string" && metadata.workspaceHash.trim()
|
|
1071
|
+
? metadata.workspaceHash
|
|
1072
|
+
: fallbackScope.workspaceHash;
|
|
1073
|
+
const agentId =
|
|
1074
|
+
typeof metadata.agentId === "string" && metadata.agentId.trim()
|
|
1075
|
+
? metadata.agentId
|
|
1076
|
+
: fallbackScope.agentId ?? "main";
|
|
1077
|
+
const sessionKey =
|
|
1078
|
+
typeof metadata.sessionKey === "string" && metadata.sessionKey.trim()
|
|
1079
|
+
? metadata.sessionKey
|
|
1080
|
+
: fallbackScope.sessionKey;
|
|
1081
|
+
return {
|
|
1082
|
+
workspaceHash,
|
|
1083
|
+
agentId,
|
|
1084
|
+
sessionKey,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function runRemediationAction(params: {
|
|
1089
|
+
action: RemediationAction;
|
|
1090
|
+
apply: boolean;
|
|
1091
|
+
mem0: Mem0Adapter;
|
|
1092
|
+
statePaths: StatePaths;
|
|
1093
|
+
scope: { workspaceHash: string; agentId?: string; sessionKey?: string };
|
|
1094
|
+
log: MemoryBraidLogger;
|
|
1095
|
+
runId: string;
|
|
1096
|
+
fetchLimit: number;
|
|
1097
|
+
sampleLimit: number;
|
|
1098
|
+
}): Promise<string> {
|
|
1099
|
+
const memories = await params.mem0.getAllMemories({
|
|
1100
|
+
scope: params.scope,
|
|
1101
|
+
limit: params.fetchLimit,
|
|
1102
|
+
runId: params.runId,
|
|
1103
|
+
});
|
|
1104
|
+
const remediationState = await readRemediationState(params.statePaths);
|
|
1105
|
+
const summary = buildAuditSummary({
|
|
1106
|
+
records: memories,
|
|
1107
|
+
remediationState,
|
|
1108
|
+
sampleLimit: params.sampleLimit,
|
|
1109
|
+
});
|
|
1110
|
+
if (params.action === "audit") {
|
|
1111
|
+
return formatAuditSummary(summary);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const targets = selectRemediationTargets(summary, params.action);
|
|
1115
|
+
if (!params.apply) {
|
|
1116
|
+
return [
|
|
1117
|
+
formatAuditSummary(summary),
|
|
1118
|
+
"",
|
|
1119
|
+
`Dry run: ${params.action}`,
|
|
1120
|
+
`- targets: ${targets.length}`,
|
|
1121
|
+
"Add --apply to mutate Mem0 state.",
|
|
1122
|
+
].join("\n");
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const nowIso = new Date().toISOString();
|
|
1126
|
+
let updated = 0;
|
|
1127
|
+
let remoteTagged = 0;
|
|
1128
|
+
let deleted = 0;
|
|
1129
|
+
const quarantinedUpdates: Array<{
|
|
1130
|
+
id: string;
|
|
1131
|
+
reason: string;
|
|
1132
|
+
updatedRemotely: boolean;
|
|
1133
|
+
}> = [];
|
|
1134
|
+
const deletedIds = new Set<string>();
|
|
1135
|
+
|
|
1136
|
+
if (params.action === "quarantine") {
|
|
1137
|
+
for (const target of targets) {
|
|
1138
|
+
const memoryId = target.memory.id;
|
|
1139
|
+
if (!memoryId) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const reason = target.suspiciousReasons.join(",");
|
|
1143
|
+
const updatedRemotely = await params.mem0.updateMemoryMetadata({
|
|
1144
|
+
memoryId,
|
|
1145
|
+
scope: resolveRecordScope(target.memory, params.scope),
|
|
1146
|
+
text: target.memory.snippet,
|
|
1147
|
+
metadata: buildQuarantineMetadata(asRecord(target.memory.metadata), reason, nowIso),
|
|
1148
|
+
runId: params.runId,
|
|
1149
|
+
});
|
|
1150
|
+
quarantinedUpdates.push({
|
|
1151
|
+
id: memoryId,
|
|
1152
|
+
reason,
|
|
1153
|
+
updatedRemotely,
|
|
1154
|
+
});
|
|
1155
|
+
updated += 1;
|
|
1156
|
+
if (updatedRemotely) {
|
|
1157
|
+
remoteTagged += 1;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
await withStateLock(params.statePaths.stateLockFile, async () => {
|
|
1162
|
+
const nextRemediation = await readRemediationState(params.statePaths);
|
|
1163
|
+
const stats = await readStatsState(params.statePaths);
|
|
1164
|
+
for (const update of quarantinedUpdates) {
|
|
1165
|
+
nextRemediation.quarantined[update.id] = {
|
|
1166
|
+
memoryId: update.id,
|
|
1167
|
+
reason: update.reason,
|
|
1168
|
+
quarantinedAt: nowIso,
|
|
1169
|
+
updatedRemotely: update.updatedRemotely,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
stats.capture.remediationQuarantined += quarantinedUpdates.length;
|
|
1173
|
+
stats.capture.lastRemediationAt = nowIso;
|
|
1174
|
+
await writeRemediationState(params.statePaths, nextRemediation);
|
|
1175
|
+
await writeStatsState(params.statePaths, stats);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
return [
|
|
1179
|
+
formatAuditSummary(summary),
|
|
1180
|
+
"",
|
|
1181
|
+
"Remediation applied.",
|
|
1182
|
+
`- action: quarantine`,
|
|
1183
|
+
`- targets: ${targets.length}`,
|
|
1184
|
+
`- quarantined: ${updated}`,
|
|
1185
|
+
`- remoteMetadataUpdated: ${remoteTagged}`,
|
|
1186
|
+
`- localQuarantineState: ${quarantinedUpdates.length}`,
|
|
1187
|
+
].join("\n");
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
for (const target of targets) {
|
|
1191
|
+
const memoryId = target.memory.id;
|
|
1192
|
+
if (!memoryId) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const ok = await params.mem0.deleteMemory({
|
|
1196
|
+
memoryId,
|
|
1197
|
+
scope: resolveRecordScope(target.memory, params.scope),
|
|
1198
|
+
runId: params.runId,
|
|
1199
|
+
});
|
|
1200
|
+
if (!ok) {
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
deleted += 1;
|
|
1204
|
+
deletedIds.add(memoryId);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
await withStateLock(params.statePaths.stateLockFile, async () => {
|
|
1208
|
+
const nextRemediation = await readRemediationState(params.statePaths);
|
|
1209
|
+
const lifecycle = await readLifecycleState(params.statePaths);
|
|
1210
|
+
const stats = await readStatsState(params.statePaths);
|
|
1211
|
+
|
|
1212
|
+
for (const memoryId of deletedIds) {
|
|
1213
|
+
delete nextRemediation.quarantined[memoryId];
|
|
1214
|
+
delete lifecycle.entries[memoryId];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
stats.capture.remediationDeleted += deletedIds.size;
|
|
1218
|
+
stats.capture.lastRemediationAt = nowIso;
|
|
1219
|
+
await writeRemediationState(params.statePaths, nextRemediation);
|
|
1220
|
+
await writeLifecycleState(params.statePaths, lifecycle);
|
|
1221
|
+
await writeStatsState(params.statePaths, stats);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
return [
|
|
1225
|
+
formatAuditSummary(summary),
|
|
1226
|
+
"",
|
|
1227
|
+
"Remediation applied.",
|
|
1228
|
+
`- action: ${params.action}`,
|
|
1229
|
+
`- targets: ${targets.length}`,
|
|
1230
|
+
`- deleted: ${deleted}`,
|
|
1231
|
+
].join("\n");
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1033
1234
|
const memoryBraidPlugin = {
|
|
1034
1235
|
id: "memory-braid",
|
|
1035
1236
|
name: "Memory Braid",
|
|
@@ -1047,6 +1248,8 @@ const memoryBraidPlugin = {
|
|
|
1047
1248
|
});
|
|
1048
1249
|
const recallSeenByScope = new Map<string, string>();
|
|
1049
1250
|
const captureSeenByScope = new Map<string, string>();
|
|
1251
|
+
const pendingInboundTurns = new Map<string, PendingInboundTurn>();
|
|
1252
|
+
const usageByRunScope = new Map<string, UsageWindowEntry[]>();
|
|
1050
1253
|
|
|
1051
1254
|
let lifecycleTimer: NodeJS.Timeout | null = null;
|
|
1052
1255
|
let statePaths: StatePaths | null = null;
|
|
@@ -1135,8 +1338,10 @@ const memoryBraidPlugin = {
|
|
|
1135
1338
|
};
|
|
1136
1339
|
|
|
1137
1340
|
const getTool = {
|
|
1138
|
-
...local.getTool,
|
|
1139
1341
|
name: "memory_get",
|
|
1342
|
+
label: local.getTool.label ?? "Memory Get",
|
|
1343
|
+
description: local.getTool.description ?? "Read a specific local memory entry.",
|
|
1344
|
+
parameters: local.getTool.parameters,
|
|
1140
1345
|
execute: async (
|
|
1141
1346
|
toolCallId: string,
|
|
1142
1347
|
args: Record<string, unknown>,
|
|
@@ -1162,14 +1367,14 @@ const memoryBraidPlugin = {
|
|
|
1162
1367
|
},
|
|
1163
1368
|
};
|
|
1164
1369
|
|
|
1165
|
-
return [searchTool, getTool];
|
|
1370
|
+
return [searchTool, getTool] as never;
|
|
1166
1371
|
},
|
|
1167
1372
|
{ names: ["memory_search", "memory_get"] },
|
|
1168
1373
|
);
|
|
1169
1374
|
|
|
1170
1375
|
api.registerCommand({
|
|
1171
1376
|
name: "memorybraid",
|
|
1172
|
-
description: "Memory Braid status, stats, lifecycle cleanup, and entity extraction warmup.",
|
|
1377
|
+
description: "Memory Braid status, stats, remediation, lifecycle cleanup, and entity extraction warmup.",
|
|
1173
1378
|
acceptsArgs: true,
|
|
1174
1379
|
handler: async (ctx) => {
|
|
1175
1380
|
const args = ctx.args?.trim() ?? "";
|
|
@@ -1243,7 +1448,15 @@ const memoryBraidPlugin = {
|
|
|
1243
1448
|
`- mem0AddAttempts: ${capture.mem0AddAttempts}`,
|
|
1244
1449
|
`- mem0AddWithId: ${capture.mem0AddWithId} (${mem0SuccessRate})`,
|
|
1245
1450
|
`- mem0AddWithoutId: ${capture.mem0AddWithoutId} (${mem0NoIdRate})`,
|
|
1451
|
+
`- trustedTurns: ${capture.trustedTurns}`,
|
|
1452
|
+
`- fallbackTurnSlices: ${capture.fallbackTurnSlices}`,
|
|
1453
|
+
`- provenanceSkipped: ${capture.provenanceSkipped}`,
|
|
1454
|
+
`- transcriptShapeSkipped: ${capture.transcriptShapeSkipped}`,
|
|
1455
|
+
`- quarantinedFiltered: ${capture.quarantinedFiltered}`,
|
|
1456
|
+
`- remediationQuarantined: ${capture.remediationQuarantined}`,
|
|
1457
|
+
`- remediationDeleted: ${capture.remediationDeleted}`,
|
|
1246
1458
|
`- lastRunAt: ${capture.lastRunAt ?? "n/a"}`,
|
|
1459
|
+
`- lastRemediationAt: ${capture.lastRemediationAt ?? "n/a"}`,
|
|
1247
1460
|
"",
|
|
1248
1461
|
"Lifecycle:",
|
|
1249
1462
|
`- enabled: ${cfg.lifecycle.enabled}`,
|
|
@@ -1261,6 +1474,44 @@ const memoryBraidPlugin = {
|
|
|
1261
1474
|
};
|
|
1262
1475
|
}
|
|
1263
1476
|
|
|
1477
|
+
if (action === "audit" || action === "remediate") {
|
|
1478
|
+
const subAction = action === "audit" ? "audit" : (tokens[1] ?? "audit").toLowerCase();
|
|
1479
|
+
if (
|
|
1480
|
+
subAction !== "audit" &&
|
|
1481
|
+
subAction !== "quarantine" &&
|
|
1482
|
+
subAction !== "delete" &&
|
|
1483
|
+
subAction !== "purge-all-captured"
|
|
1484
|
+
) {
|
|
1485
|
+
return {
|
|
1486
|
+
text:
|
|
1487
|
+
"Usage: /memorybraid remediate [audit|quarantine|delete|purge-all-captured] [--apply] [--limit N] [--sample N]",
|
|
1488
|
+
isError: true,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const paths = await ensureRuntimeStatePaths();
|
|
1493
|
+
if (!paths) {
|
|
1494
|
+
return {
|
|
1495
|
+
text: "Remediation unavailable: state directory is not ready.",
|
|
1496
|
+
isError: true,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
return {
|
|
1501
|
+
text: await runRemediationAction({
|
|
1502
|
+
action: subAction as RemediationAction,
|
|
1503
|
+
apply: tokens.includes("--apply"),
|
|
1504
|
+
mem0,
|
|
1505
|
+
statePaths: paths,
|
|
1506
|
+
scope: resolveCommandScope(ctx.config),
|
|
1507
|
+
log,
|
|
1508
|
+
runId: log.newRunId(),
|
|
1509
|
+
fetchLimit: parseIntegerFlag(tokens, "--limit", 500),
|
|
1510
|
+
sampleLimit: parseIntegerFlag(tokens, "--sample", 5),
|
|
1511
|
+
}),
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1264
1515
|
if (action === "cleanup") {
|
|
1265
1516
|
if (!cfg.lifecycle.enabled) {
|
|
1266
1517
|
return {
|
|
@@ -1327,11 +1578,122 @@ const memoryBraidPlugin = {
|
|
|
1327
1578
|
}
|
|
1328
1579
|
|
|
1329
1580
|
return {
|
|
1330
|
-
text:
|
|
1581
|
+
text:
|
|
1582
|
+
"Usage: /memorybraid [status|stats|audit|remediate <audit|quarantine|delete|purge-all-captured> [--apply] [--limit N] [--sample N]|cleanup|warmup [--force]]",
|
|
1331
1583
|
};
|
|
1332
1584
|
},
|
|
1333
1585
|
});
|
|
1334
1586
|
|
|
1587
|
+
api.on("before_message_write", (event) => {
|
|
1588
|
+
const pending = getPendingInboundTurn(event.message);
|
|
1589
|
+
if (!pending) {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const scopeKey = resolveRunScopeKey({
|
|
1593
|
+
agentId: event.agentId,
|
|
1594
|
+
sessionKey: event.sessionKey,
|
|
1595
|
+
});
|
|
1596
|
+
pendingInboundTurns.set(scopeKey, pending);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
api.on("llm_output", (event, ctx) => {
|
|
1600
|
+
if (!cfg.debug.enabled || !event.usage) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const scope = resolveScopeFromHookContext(ctx);
|
|
1605
|
+
const scopeKey = `${scope.workspaceHash}|${scope.agentId}|${ctx.sessionKey ?? event.sessionId}|${event.provider}|${event.model}`;
|
|
1606
|
+
const snapshot = createUsageSnapshot({
|
|
1607
|
+
provider: event.provider,
|
|
1608
|
+
model: event.model,
|
|
1609
|
+
usage: event.usage,
|
|
1610
|
+
});
|
|
1611
|
+
const entry: UsageWindowEntry = {
|
|
1612
|
+
...snapshot,
|
|
1613
|
+
at: Date.now(),
|
|
1614
|
+
runId: event.runId,
|
|
1615
|
+
};
|
|
1616
|
+
const history = appendUsageWindow(usageByRunScope.get(scopeKey) ?? [], entry);
|
|
1617
|
+
usageByRunScope.set(scopeKey, history);
|
|
1618
|
+
const summary = summarizeUsageWindow(history);
|
|
1619
|
+
|
|
1620
|
+
log.debug("memory_braid.cost.turn", {
|
|
1621
|
+
runId: event.runId,
|
|
1622
|
+
workspaceHash: scope.workspaceHash,
|
|
1623
|
+
agentId: scope.agentId,
|
|
1624
|
+
sessionKey: ctx.sessionKey,
|
|
1625
|
+
provider: event.provider,
|
|
1626
|
+
model: event.model,
|
|
1627
|
+
input: snapshot.input,
|
|
1628
|
+
output: snapshot.output,
|
|
1629
|
+
cacheRead: snapshot.cacheRead,
|
|
1630
|
+
cacheWrite: snapshot.cacheWrite,
|
|
1631
|
+
promptTokens: snapshot.promptTokens,
|
|
1632
|
+
cacheHitRate: Number(snapshot.cacheHitRate.toFixed(4)),
|
|
1633
|
+
cacheWriteRate: Number(snapshot.cacheWriteRate.toFixed(4)),
|
|
1634
|
+
estimatedCostUsd:
|
|
1635
|
+
typeof snapshot.estimatedCostUsd === "number"
|
|
1636
|
+
? Number(snapshot.estimatedCostUsd.toFixed(6))
|
|
1637
|
+
: undefined,
|
|
1638
|
+
costEstimateBasis: snapshot.costEstimateBasis,
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
log.debug("memory_braid.cost.window", {
|
|
1642
|
+
runId: event.runId,
|
|
1643
|
+
workspaceHash: scope.workspaceHash,
|
|
1644
|
+
agentId: scope.agentId,
|
|
1645
|
+
sessionKey: ctx.sessionKey,
|
|
1646
|
+
provider: event.provider,
|
|
1647
|
+
model: event.model,
|
|
1648
|
+
turnsSeen: summary.turnsSeen,
|
|
1649
|
+
window5PromptTokens: Math.round(summary.window5.avgPromptTokens),
|
|
1650
|
+
window5CacheRead: Math.round(summary.window5.avgCacheRead),
|
|
1651
|
+
window5CacheWrite: Math.round(summary.window5.avgCacheWrite),
|
|
1652
|
+
window5CacheHitRate: Number(summary.window5.avgCacheHitRate.toFixed(4)),
|
|
1653
|
+
window5CacheWriteRate: Number(summary.window5.avgCacheWriteRate.toFixed(4)),
|
|
1654
|
+
window5EstimatedCostUsd:
|
|
1655
|
+
typeof summary.window5.avgEstimatedCostUsd === "number"
|
|
1656
|
+
? Number(summary.window5.avgEstimatedCostUsd.toFixed(6))
|
|
1657
|
+
: undefined,
|
|
1658
|
+
window20PromptTokens: Math.round(summary.window20.avgPromptTokens),
|
|
1659
|
+
window20CacheRead: Math.round(summary.window20.avgCacheRead),
|
|
1660
|
+
window20CacheWrite: Math.round(summary.window20.avgCacheWrite),
|
|
1661
|
+
window20CacheHitRate: Number(summary.window20.avgCacheHitRate.toFixed(4)),
|
|
1662
|
+
window20CacheWriteRate: Number(summary.window20.avgCacheWriteRate.toFixed(4)),
|
|
1663
|
+
window20EstimatedCostUsd:
|
|
1664
|
+
typeof summary.window20.avgEstimatedCostUsd === "number"
|
|
1665
|
+
? Number(summary.window20.avgEstimatedCostUsd.toFixed(6))
|
|
1666
|
+
: undefined,
|
|
1667
|
+
cacheWriteTrend: summary.trends.cacheWriteRate,
|
|
1668
|
+
cacheHitTrend: summary.trends.cacheHitRate,
|
|
1669
|
+
promptTokensTrend: summary.trends.promptTokens,
|
|
1670
|
+
estimatedCostTrend: summary.trends.estimatedCostUsd,
|
|
1671
|
+
costEstimateBasis: snapshot.costEstimateBasis,
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
if (summary.alerts.length > 0) {
|
|
1675
|
+
log.debug("memory_braid.cost.alert", {
|
|
1676
|
+
runId: event.runId,
|
|
1677
|
+
workspaceHash: scope.workspaceHash,
|
|
1678
|
+
agentId: scope.agentId,
|
|
1679
|
+
sessionKey: ctx.sessionKey,
|
|
1680
|
+
provider: event.provider,
|
|
1681
|
+
model: event.model,
|
|
1682
|
+
alerts: summary.alerts,
|
|
1683
|
+
cacheWriteTrend: summary.trends.cacheWriteRate,
|
|
1684
|
+
promptTokensTrend: summary.trends.promptTokens,
|
|
1685
|
+
estimatedCostTrend: summary.trends.estimatedCostUsd,
|
|
1686
|
+
window5CacheWriteRate: Number(summary.window5.avgCacheWriteRate.toFixed(4)),
|
|
1687
|
+
window5PromptTokens: Math.round(summary.window5.avgPromptTokens),
|
|
1688
|
+
window5EstimatedCostUsd:
|
|
1689
|
+
typeof summary.window5.avgEstimatedCostUsd === "number"
|
|
1690
|
+
? Number(summary.window5.avgEstimatedCostUsd.toFixed(6))
|
|
1691
|
+
: undefined,
|
|
1692
|
+
costEstimateBasis: snapshot.costEstimateBasis,
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1335
1697
|
api.on("before_agent_start", async (event, ctx) => {
|
|
1336
1698
|
const runId = log.newRunId();
|
|
1337
1699
|
const scope = resolveScopeFromHookContext(ctx);
|
|
@@ -1376,7 +1738,7 @@ const memoryBraidPlugin = {
|
|
|
1376
1738
|
}
|
|
1377
1739
|
recallSeenByScope.set(scopeKey, userTurnSignature);
|
|
1378
1740
|
|
|
1379
|
-
const toolCtx:
|
|
1741
|
+
const toolCtx: ToolContext = {
|
|
1380
1742
|
config: api.config,
|
|
1381
1743
|
workspaceDir: ctx.workspaceDir,
|
|
1382
1744
|
agentId: ctx.agentId,
|
|
@@ -1457,8 +1819,11 @@ const memoryBraidPlugin = {
|
|
|
1457
1819
|
}
|
|
1458
1820
|
|
|
1459
1821
|
const scopeKey = resolveRunScopeKey(ctx);
|
|
1460
|
-
const
|
|
1822
|
+
const pendingInboundTurn = pendingInboundTurns.get(scopeKey);
|
|
1823
|
+
const userTurnSignature =
|
|
1824
|
+
pendingInboundTurn?.messageHash ?? resolveLatestUserTurnSignature(event.messages);
|
|
1461
1825
|
if (!userTurnSignature) {
|
|
1826
|
+
pendingInboundTurns.delete(scopeKey);
|
|
1462
1827
|
log.debug("memory_braid.capture.skip", {
|
|
1463
1828
|
runId,
|
|
1464
1829
|
reason: "no_user_turn_signature",
|
|
@@ -1470,6 +1835,7 @@ const memoryBraidPlugin = {
|
|
|
1470
1835
|
}
|
|
1471
1836
|
const previousSignature = captureSeenByScope.get(scopeKey);
|
|
1472
1837
|
if (previousSignature === userTurnSignature) {
|
|
1838
|
+
pendingInboundTurns.delete(scopeKey);
|
|
1473
1839
|
log.debug("memory_braid.capture.skip", {
|
|
1474
1840
|
runId,
|
|
1475
1841
|
reason: "no_new_user_turn",
|
|
@@ -1480,21 +1846,73 @@ const memoryBraidPlugin = {
|
|
|
1480
1846
|
return;
|
|
1481
1847
|
}
|
|
1482
1848
|
captureSeenByScope.set(scopeKey, userTurnSignature);
|
|
1849
|
+
pendingInboundTurns.delete(scopeKey);
|
|
1483
1850
|
|
|
1484
|
-
const
|
|
1851
|
+
const captureInput = assembleCaptureInput({
|
|
1485
1852
|
messages: event.messages,
|
|
1853
|
+
includeAssistant: cfg.capture.includeAssistant,
|
|
1854
|
+
pendingInboundTurn,
|
|
1855
|
+
});
|
|
1856
|
+
if (!captureInput) {
|
|
1857
|
+
log.debug("memory_braid.capture.skip", {
|
|
1858
|
+
runId,
|
|
1859
|
+
reason: "no_capture_input",
|
|
1860
|
+
workspaceHash: scope.workspaceHash,
|
|
1861
|
+
agentId: scope.agentId,
|
|
1862
|
+
sessionKey: scope.sessionKey,
|
|
1863
|
+
});
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const candidates = await extractCandidates({
|
|
1868
|
+
messages: captureInput.messages.map((message) => ({
|
|
1869
|
+
role: message.role,
|
|
1870
|
+
content: message.text,
|
|
1871
|
+
})),
|
|
1486
1872
|
cfg,
|
|
1487
1873
|
log,
|
|
1488
1874
|
runId,
|
|
1489
1875
|
});
|
|
1490
1876
|
const runtimeStatePaths = await ensureRuntimeStatePaths();
|
|
1491
|
-
|
|
1492
|
-
|
|
1877
|
+
let provenanceSkipped = 0;
|
|
1878
|
+
let transcriptShapeSkipped = 0;
|
|
1879
|
+
const candidateEntries = candidates
|
|
1880
|
+
.map((candidate) => {
|
|
1881
|
+
if (isLikelyTranscriptLikeText(candidate.text) || isOversizedAtomicMemory(candidate.text)) {
|
|
1882
|
+
transcriptShapeSkipped += 1;
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
const matchedSource = matchCandidateToCaptureInput(candidate.text, captureInput.messages);
|
|
1886
|
+
if (!matchedSource) {
|
|
1887
|
+
provenanceSkipped += 1;
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
candidate,
|
|
1892
|
+
matchedSource,
|
|
1893
|
+
hash: sha256(normalizeForHash(candidate.text)),
|
|
1894
|
+
};
|
|
1895
|
+
})
|
|
1896
|
+
.filter(
|
|
1897
|
+
(
|
|
1898
|
+
entry,
|
|
1899
|
+
): entry is {
|
|
1900
|
+
candidate: (typeof candidates)[number];
|
|
1901
|
+
matchedSource: (typeof captureInput.messages)[number];
|
|
1902
|
+
hash: string;
|
|
1903
|
+
} => Boolean(entry),
|
|
1904
|
+
);
|
|
1905
|
+
|
|
1906
|
+
if (candidateEntries.length === 0) {
|
|
1493
1907
|
if (runtimeStatePaths) {
|
|
1494
1908
|
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
1495
1909
|
const stats = await readStatsState(runtimeStatePaths);
|
|
1496
1910
|
stats.capture.runs += 1;
|
|
1497
1911
|
stats.capture.runsNoCandidates += 1;
|
|
1912
|
+
stats.capture.trustedTurns += 1;
|
|
1913
|
+
stats.capture.fallbackTurnSlices += captureInput.fallbackUsed ? 1 : 0;
|
|
1914
|
+
stats.capture.provenanceSkipped += provenanceSkipped;
|
|
1915
|
+
stats.capture.transcriptShapeSkipped += transcriptShapeSkipped;
|
|
1498
1916
|
stats.capture.lastRunAt = new Date().toISOString();
|
|
1499
1917
|
await writeStatsState(runtimeStatePaths, stats);
|
|
1500
1918
|
});
|
|
@@ -1502,6 +1920,10 @@ const memoryBraidPlugin = {
|
|
|
1502
1920
|
log.debug("memory_braid.capture.skip", {
|
|
1503
1921
|
runId,
|
|
1504
1922
|
reason: "no_candidates",
|
|
1923
|
+
capturePath: captureInput.capturePath,
|
|
1924
|
+
fallbackUsed: captureInput.fallbackUsed,
|
|
1925
|
+
provenanceSkipped,
|
|
1926
|
+
transcriptShapeSkipped,
|
|
1505
1927
|
workspaceHash: scope.workspaceHash,
|
|
1506
1928
|
agentId: scope.agentId,
|
|
1507
1929
|
sessionKey: scope.sessionKey,
|
|
@@ -1521,11 +1943,6 @@ const memoryBraidPlugin = {
|
|
|
1521
1943
|
}
|
|
1522
1944
|
|
|
1523
1945
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
1524
|
-
const candidateEntries = candidates.map((candidate) => ({
|
|
1525
|
-
candidate,
|
|
1526
|
-
hash: sha256(normalizeForHash(candidate.text)),
|
|
1527
|
-
}));
|
|
1528
|
-
|
|
1529
1946
|
const prepared = await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
1530
1947
|
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
1531
1948
|
const now = Date.now();
|
|
@@ -1565,6 +1982,8 @@ const memoryBraidPlugin = {
|
|
|
1565
1982
|
let mem0AddAttempts = 0;
|
|
1566
1983
|
let mem0AddWithId = 0;
|
|
1567
1984
|
let mem0AddWithoutId = 0;
|
|
1985
|
+
let remoteQuarantineFiltered = 0;
|
|
1986
|
+
const remediationState = await readRemediationState(runtimeStatePaths);
|
|
1568
1987
|
const successfulAdds: Array<{
|
|
1569
1988
|
memoryId: string;
|
|
1570
1989
|
hash: string;
|
|
@@ -1572,7 +1991,7 @@ const memoryBraidPlugin = {
|
|
|
1572
1991
|
}> = [];
|
|
1573
1992
|
|
|
1574
1993
|
for (const entry of prepared.pending) {
|
|
1575
|
-
const { candidate, hash } = entry;
|
|
1994
|
+
const { candidate, hash, matchedSource } = entry;
|
|
1576
1995
|
const metadata: Record<string, unknown> = {
|
|
1577
1996
|
sourceType: "capture",
|
|
1578
1997
|
workspaceHash: scope.workspaceHash,
|
|
@@ -1581,6 +2000,11 @@ const memoryBraidPlugin = {
|
|
|
1581
2000
|
category: candidate.category,
|
|
1582
2001
|
captureScore: candidate.score,
|
|
1583
2002
|
extractionSource: candidate.source,
|
|
2003
|
+
captureOrigin: matchedSource.origin,
|
|
2004
|
+
captureMessageHash: matchedSource.messageHash,
|
|
2005
|
+
captureTurnHash: captureInput.turnHash,
|
|
2006
|
+
capturePath: captureInput.capturePath,
|
|
2007
|
+
pluginCaptureVersion: PLUGIN_CAPTURE_VERSION,
|
|
1584
2008
|
contentHash: hash,
|
|
1585
2009
|
indexedAt: new Date().toISOString(),
|
|
1586
2010
|
};
|
|
@@ -1598,6 +2022,17 @@ const memoryBraidPlugin = {
|
|
|
1598
2022
|
}
|
|
1599
2023
|
}
|
|
1600
2024
|
|
|
2025
|
+
const quarantine = isQuarantinedMemory({
|
|
2026
|
+
...entry.candidate,
|
|
2027
|
+
source: "mem0",
|
|
2028
|
+
snippet: entry.candidate.text,
|
|
2029
|
+
metadata,
|
|
2030
|
+
}, remediationState);
|
|
2031
|
+
if (quarantine.quarantined) {
|
|
2032
|
+
remoteQuarantineFiltered += 1;
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1601
2036
|
mem0AddAttempts += 1;
|
|
1602
2037
|
const addResult = await mem0.addMemory({
|
|
1603
2038
|
text: candidate.text,
|
|
@@ -1673,6 +2108,11 @@ const memoryBraidPlugin = {
|
|
|
1673
2108
|
stats.capture.mem0AddWithoutId += mem0AddWithoutId;
|
|
1674
2109
|
stats.capture.entityAnnotatedCandidates += entityAnnotatedCandidates;
|
|
1675
2110
|
stats.capture.totalEntitiesAttached += totalEntitiesAttached;
|
|
2111
|
+
stats.capture.trustedTurns += 1;
|
|
2112
|
+
stats.capture.fallbackTurnSlices += captureInput.fallbackUsed ? 1 : 0;
|
|
2113
|
+
stats.capture.provenanceSkipped += provenanceSkipped;
|
|
2114
|
+
stats.capture.transcriptShapeSkipped += transcriptShapeSkipped;
|
|
2115
|
+
stats.capture.quarantinedFiltered += remoteQuarantineFiltered;
|
|
1676
2116
|
stats.capture.lastRunAt = new Date(now).toISOString();
|
|
1677
2117
|
|
|
1678
2118
|
await writeCaptureDedupeState(runtimeStatePaths, dedupe);
|
|
@@ -1686,9 +2126,14 @@ const memoryBraidPlugin = {
|
|
|
1686
2126
|
workspaceHash: scope.workspaceHash,
|
|
1687
2127
|
agentId: scope.agentId,
|
|
1688
2128
|
sessionKey: scope.sessionKey,
|
|
2129
|
+
capturePath: captureInput.capturePath,
|
|
2130
|
+
fallbackUsed: captureInput.fallbackUsed,
|
|
1689
2131
|
candidates: candidates.length,
|
|
1690
2132
|
pending: prepared.pending.length,
|
|
1691
2133
|
dedupeSkipped: prepared.dedupeSkipped,
|
|
2134
|
+
provenanceSkipped,
|
|
2135
|
+
transcriptShapeSkipped,
|
|
2136
|
+
quarantinedFiltered: remoteQuarantineFiltered,
|
|
1692
2137
|
persisted,
|
|
1693
2138
|
mem0AddAttempts,
|
|
1694
2139
|
mem0AddWithId,
|