memory-braid 0.4.6 → 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 +650 -30
- 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,6 +97,75 @@ function resolveScopeFromHookContext(ctx: {
|
|
|
63
97
|
};
|
|
64
98
|
}
|
|
65
99
|
|
|
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;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveCommandScope(config?: unknown): {
|
|
110
|
+
workspaceHash: string;
|
|
111
|
+
agentId?: string;
|
|
112
|
+
sessionKey?: string;
|
|
113
|
+
} {
|
|
114
|
+
return {
|
|
115
|
+
workspaceHash: workspaceHashFromDir(resolveWorkspaceDirFromConfig(config)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveLatestUserTurnSignature(messages?: unknown[]): string | undefined {
|
|
120
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const normalized = normalizeHookMessages(messages);
|
|
125
|
+
for (let i = normalized.length - 1; i >= 0; i -= 1) {
|
|
126
|
+
const message = normalized[i];
|
|
127
|
+
if (!message || message.role !== "user") {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const hashSource = normalizeForHash(message.text);
|
|
131
|
+
if (!hashSource) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
return `${i}:${sha256(hashSource)}`;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolvePromptTurnSignature(prompt: string): string | undefined {
|
|
140
|
+
const normalized = normalizeForHash(prompt);
|
|
141
|
+
if (!normalized) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return `prompt:${sha256(normalized)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveRunScopeKey(ctx: { agentId?: string; sessionKey?: string }): string {
|
|
148
|
+
const agentId = (ctx.agentId ?? "main").trim() || "main";
|
|
149
|
+
const sessionKey = (ctx.sessionKey ?? "main").trim() || "main";
|
|
150
|
+
return `${agentId}|${sessionKey}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isExcludedAutoMemorySession(sessionKey?: string): boolean {
|
|
154
|
+
const normalized = (sessionKey ?? "").trim().toLowerCase();
|
|
155
|
+
if (!normalized) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return (
|
|
159
|
+
normalized.startsWith("cron:") ||
|
|
160
|
+
normalized.includes(":cron:") ||
|
|
161
|
+
normalized.includes(":subagent:") ||
|
|
162
|
+
normalized.startsWith("subagent:") ||
|
|
163
|
+
normalized.includes(":acp:") ||
|
|
164
|
+
normalized.startsWith("acp:") ||
|
|
165
|
+
normalized.startsWith("temp:")
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
66
169
|
function formatRelevantMemories(results: MemoryBraidResult[], maxChars = 600): string {
|
|
67
170
|
const lines = results.map((entry, index) => {
|
|
68
171
|
const sourceLabel = entry.source === "local" ? "local" : "mem0";
|
|
@@ -563,15 +666,19 @@ function applyTemporalDecayToMem0(params: {
|
|
|
563
666
|
}
|
|
564
667
|
|
|
565
668
|
function resolveLifecycleReferenceTs(entry: LifecycleEntry, reinforceOnRecall: boolean): number {
|
|
566
|
-
const capturedTs =
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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;
|
|
571
675
|
if (!reinforceOnRecall) {
|
|
572
676
|
return capturedTs;
|
|
573
677
|
}
|
|
574
|
-
const recalledTs =
|
|
678
|
+
const recalledTs =
|
|
679
|
+
typeof entry.lastRecalledAt === "number" && Number.isFinite(entry.lastRecalledAt)
|
|
680
|
+
? entry.lastRecalledAt
|
|
681
|
+
: 0;
|
|
575
682
|
return Math.max(capturedTs, recalledTs);
|
|
576
683
|
}
|
|
577
684
|
|
|
@@ -751,7 +858,7 @@ async function runHybridRecall(params: {
|
|
|
751
858
|
cfg: ReturnType<typeof parseConfig>;
|
|
752
859
|
mem0: Mem0Adapter;
|
|
753
860
|
log: MemoryBraidLogger;
|
|
754
|
-
ctx:
|
|
861
|
+
ctx: ToolContext;
|
|
755
862
|
statePaths?: StatePaths | null;
|
|
756
863
|
query: string;
|
|
757
864
|
toolCallId?: string;
|
|
@@ -809,9 +916,21 @@ async function runHybridRecall(params: {
|
|
|
809
916
|
scope,
|
|
810
917
|
runId: params.runId,
|
|
811
918
|
});
|
|
919
|
+
const remediationState = params.statePaths
|
|
920
|
+
? await readRemediationState(params.statePaths)
|
|
921
|
+
: undefined;
|
|
922
|
+
let quarantinedFiltered = 0;
|
|
812
923
|
const mem0Search = mem0Raw.filter((result) => {
|
|
813
924
|
const sourceType = asRecord(result.metadata).sourceType;
|
|
814
|
-
|
|
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;
|
|
815
934
|
});
|
|
816
935
|
let mem0ForMerge = mem0Search;
|
|
817
936
|
if (params.cfg.timeDecay.enabled) {
|
|
@@ -861,6 +980,7 @@ async function runHybridRecall(params: {
|
|
|
861
980
|
sessionKey: scope.sessionKey,
|
|
862
981
|
workspaceHash: scope.workspaceHash,
|
|
863
982
|
inputCount: mem0Search.length,
|
|
983
|
+
quarantinedFiltered,
|
|
864
984
|
adjusted: qualityAdjusted.adjusted,
|
|
865
985
|
overlapBoosted: qualityAdjusted.overlapBoosted,
|
|
866
986
|
overlapPenalized: qualityAdjusted.overlapPenalized,
|
|
@@ -929,6 +1049,188 @@ async function runHybridRecall(params: {
|
|
|
929
1049
|
};
|
|
930
1050
|
}
|
|
931
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
|
+
|
|
932
1234
|
const memoryBraidPlugin = {
|
|
933
1235
|
id: "memory-braid",
|
|
934
1236
|
name: "Memory Braid",
|
|
@@ -944,6 +1246,10 @@ const memoryBraidPlugin = {
|
|
|
944
1246
|
const entityExtraction = new EntityExtractionManager(cfg.entityExtraction, log, {
|
|
945
1247
|
stateDir: initialStateDir,
|
|
946
1248
|
});
|
|
1249
|
+
const recallSeenByScope = new Map<string, string>();
|
|
1250
|
+
const captureSeenByScope = new Map<string, string>();
|
|
1251
|
+
const pendingInboundTurns = new Map<string, PendingInboundTurn>();
|
|
1252
|
+
const usageByRunScope = new Map<string, UsageWindowEntry[]>();
|
|
947
1253
|
|
|
948
1254
|
let lifecycleTimer: NodeJS.Timeout | null = null;
|
|
949
1255
|
let statePaths: StatePaths | null = null;
|
|
@@ -1032,8 +1338,10 @@ const memoryBraidPlugin = {
|
|
|
1032
1338
|
};
|
|
1033
1339
|
|
|
1034
1340
|
const getTool = {
|
|
1035
|
-
...local.getTool,
|
|
1036
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,
|
|
1037
1345
|
execute: async (
|
|
1038
1346
|
toolCallId: string,
|
|
1039
1347
|
args: Record<string, unknown>,
|
|
@@ -1059,14 +1367,14 @@ const memoryBraidPlugin = {
|
|
|
1059
1367
|
},
|
|
1060
1368
|
};
|
|
1061
1369
|
|
|
1062
|
-
return [searchTool, getTool];
|
|
1370
|
+
return [searchTool, getTool] as never;
|
|
1063
1371
|
},
|
|
1064
1372
|
{ names: ["memory_search", "memory_get"] },
|
|
1065
1373
|
);
|
|
1066
1374
|
|
|
1067
1375
|
api.registerCommand({
|
|
1068
1376
|
name: "memorybraid",
|
|
1069
|
-
description: "Memory Braid status, stats, lifecycle cleanup, and entity extraction warmup.",
|
|
1377
|
+
description: "Memory Braid status, stats, remediation, lifecycle cleanup, and entity extraction warmup.",
|
|
1070
1378
|
acceptsArgs: true,
|
|
1071
1379
|
handler: async (ctx) => {
|
|
1072
1380
|
const args = ctx.args?.trim() ?? "";
|
|
@@ -1140,7 +1448,15 @@ const memoryBraidPlugin = {
|
|
|
1140
1448
|
`- mem0AddAttempts: ${capture.mem0AddAttempts}`,
|
|
1141
1449
|
`- mem0AddWithId: ${capture.mem0AddWithId} (${mem0SuccessRate})`,
|
|
1142
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}`,
|
|
1143
1458
|
`- lastRunAt: ${capture.lastRunAt ?? "n/a"}`,
|
|
1459
|
+
`- lastRemediationAt: ${capture.lastRemediationAt ?? "n/a"}`,
|
|
1144
1460
|
"",
|
|
1145
1461
|
"Lifecycle:",
|
|
1146
1462
|
`- enabled: ${cfg.lifecycle.enabled}`,
|
|
@@ -1158,6 +1474,44 @@ const memoryBraidPlugin = {
|
|
|
1158
1474
|
};
|
|
1159
1475
|
}
|
|
1160
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
|
+
|
|
1161
1515
|
if (action === "cleanup") {
|
|
1162
1516
|
if (!cfg.lifecycle.enabled) {
|
|
1163
1517
|
return {
|
|
@@ -1224,18 +1578,167 @@ const memoryBraidPlugin = {
|
|
|
1224
1578
|
}
|
|
1225
1579
|
|
|
1226
1580
|
return {
|
|
1227
|
-
text:
|
|
1581
|
+
text:
|
|
1582
|
+
"Usage: /memorybraid [status|stats|audit|remediate <audit|quarantine|delete|purge-all-captured> [--apply] [--limit N] [--sample N]|cleanup|warmup [--force]]",
|
|
1228
1583
|
};
|
|
1229
1584
|
},
|
|
1230
1585
|
});
|
|
1231
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
|
+
|
|
1232
1697
|
api.on("before_agent_start", async (event, ctx) => {
|
|
1233
1698
|
const runId = log.newRunId();
|
|
1699
|
+
const scope = resolveScopeFromHookContext(ctx);
|
|
1700
|
+
if (isExcludedAutoMemorySession(ctx.sessionKey)) {
|
|
1701
|
+
log.debug("memory_braid.search.skip", {
|
|
1702
|
+
runId,
|
|
1703
|
+
reason: "session_scope_excluded",
|
|
1704
|
+
workspaceHash: scope.workspaceHash,
|
|
1705
|
+
agentId: scope.agentId,
|
|
1706
|
+
sessionKey: scope.sessionKey,
|
|
1707
|
+
});
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1234
1711
|
const recallQuery = sanitizeRecallQuery(event.prompt);
|
|
1235
1712
|
if (!recallQuery) {
|
|
1236
1713
|
return;
|
|
1237
1714
|
}
|
|
1238
|
-
const
|
|
1715
|
+
const scopeKey = resolveRunScopeKey(ctx);
|
|
1716
|
+
const userTurnSignature =
|
|
1717
|
+
resolveLatestUserTurnSignature(event.messages) ?? resolvePromptTurnSignature(recallQuery);
|
|
1718
|
+
if (!userTurnSignature) {
|
|
1719
|
+
log.debug("memory_braid.search.skip", {
|
|
1720
|
+
runId,
|
|
1721
|
+
reason: "no_user_turn_signature",
|
|
1722
|
+
workspaceHash: scope.workspaceHash,
|
|
1723
|
+
agentId: scope.agentId,
|
|
1724
|
+
sessionKey: scope.sessionKey,
|
|
1725
|
+
});
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
const previousSignature = recallSeenByScope.get(scopeKey);
|
|
1729
|
+
if (previousSignature === userTurnSignature) {
|
|
1730
|
+
log.debug("memory_braid.search.skip", {
|
|
1731
|
+
runId,
|
|
1732
|
+
reason: "no_new_user_turn",
|
|
1733
|
+
workspaceHash: scope.workspaceHash,
|
|
1734
|
+
agentId: scope.agentId,
|
|
1735
|
+
sessionKey: scope.sessionKey,
|
|
1736
|
+
});
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
recallSeenByScope.set(scopeKey, userTurnSignature);
|
|
1740
|
+
|
|
1741
|
+
const toolCtx: ToolContext = {
|
|
1239
1742
|
config: api.config,
|
|
1240
1743
|
workspaceDir: ctx.workspaceDir,
|
|
1241
1744
|
agentId: ctx.agentId,
|
|
@@ -1264,7 +1767,6 @@ const memoryBraidPlugin = {
|
|
|
1264
1767
|
limit: cfg.recall.injectTopK,
|
|
1265
1768
|
});
|
|
1266
1769
|
if (selected.injected.length === 0) {
|
|
1267
|
-
const scope = resolveScopeFromHookContext(ctx);
|
|
1268
1770
|
log.debug("memory_braid.search.inject", {
|
|
1269
1771
|
runId,
|
|
1270
1772
|
agentId: scope.agentId,
|
|
@@ -1281,7 +1783,6 @@ const memoryBraidPlugin = {
|
|
|
1281
1783
|
}
|
|
1282
1784
|
|
|
1283
1785
|
const prependContext = formatRelevantMemories(selected.injected, cfg.debug.maxSnippetChars);
|
|
1284
|
-
const scope = resolveScopeFromHookContext(ctx);
|
|
1285
1786
|
log.debug("memory_braid.search.inject", {
|
|
1286
1787
|
runId,
|
|
1287
1788
|
agentId: scope.agentId,
|
|
@@ -1306,20 +1807,112 @@ const memoryBraidPlugin = {
|
|
|
1306
1807
|
}
|
|
1307
1808
|
const runId = log.newRunId();
|
|
1308
1809
|
const scope = resolveScopeFromHookContext(ctx);
|
|
1309
|
-
|
|
1810
|
+
if (isExcludedAutoMemorySession(ctx.sessionKey)) {
|
|
1811
|
+
log.debug("memory_braid.capture.skip", {
|
|
1812
|
+
runId,
|
|
1813
|
+
reason: "session_scope_excluded",
|
|
1814
|
+
workspaceHash: scope.workspaceHash,
|
|
1815
|
+
agentId: scope.agentId,
|
|
1816
|
+
sessionKey: scope.sessionKey,
|
|
1817
|
+
});
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const scopeKey = resolveRunScopeKey(ctx);
|
|
1822
|
+
const pendingInboundTurn = pendingInboundTurns.get(scopeKey);
|
|
1823
|
+
const userTurnSignature =
|
|
1824
|
+
pendingInboundTurn?.messageHash ?? resolveLatestUserTurnSignature(event.messages);
|
|
1825
|
+
if (!userTurnSignature) {
|
|
1826
|
+
pendingInboundTurns.delete(scopeKey);
|
|
1827
|
+
log.debug("memory_braid.capture.skip", {
|
|
1828
|
+
runId,
|
|
1829
|
+
reason: "no_user_turn_signature",
|
|
1830
|
+
workspaceHash: scope.workspaceHash,
|
|
1831
|
+
agentId: scope.agentId,
|
|
1832
|
+
sessionKey: scope.sessionKey,
|
|
1833
|
+
});
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const previousSignature = captureSeenByScope.get(scopeKey);
|
|
1837
|
+
if (previousSignature === userTurnSignature) {
|
|
1838
|
+
pendingInboundTurns.delete(scopeKey);
|
|
1839
|
+
log.debug("memory_braid.capture.skip", {
|
|
1840
|
+
runId,
|
|
1841
|
+
reason: "no_new_user_turn",
|
|
1842
|
+
workspaceHash: scope.workspaceHash,
|
|
1843
|
+
agentId: scope.agentId,
|
|
1844
|
+
sessionKey: scope.sessionKey,
|
|
1845
|
+
});
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
captureSeenByScope.set(scopeKey, userTurnSignature);
|
|
1849
|
+
pendingInboundTurns.delete(scopeKey);
|
|
1850
|
+
|
|
1851
|
+
const captureInput = assembleCaptureInput({
|
|
1310
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
|
+
})),
|
|
1311
1872
|
cfg,
|
|
1312
1873
|
log,
|
|
1313
1874
|
runId,
|
|
1314
1875
|
});
|
|
1315
1876
|
const runtimeStatePaths = await ensureRuntimeStatePaths();
|
|
1316
|
-
|
|
1317
|
-
|
|
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) {
|
|
1318
1907
|
if (runtimeStatePaths) {
|
|
1319
1908
|
await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
1320
1909
|
const stats = await readStatsState(runtimeStatePaths);
|
|
1321
1910
|
stats.capture.runs += 1;
|
|
1322
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;
|
|
1323
1916
|
stats.capture.lastRunAt = new Date().toISOString();
|
|
1324
1917
|
await writeStatsState(runtimeStatePaths, stats);
|
|
1325
1918
|
});
|
|
@@ -1327,6 +1920,10 @@ const memoryBraidPlugin = {
|
|
|
1327
1920
|
log.debug("memory_braid.capture.skip", {
|
|
1328
1921
|
runId,
|
|
1329
1922
|
reason: "no_candidates",
|
|
1923
|
+
capturePath: captureInput.capturePath,
|
|
1924
|
+
fallbackUsed: captureInput.fallbackUsed,
|
|
1925
|
+
provenanceSkipped,
|
|
1926
|
+
transcriptShapeSkipped,
|
|
1330
1927
|
workspaceHash: scope.workspaceHash,
|
|
1331
1928
|
agentId: scope.agentId,
|
|
1332
1929
|
sessionKey: scope.sessionKey,
|
|
@@ -1346,11 +1943,6 @@ const memoryBraidPlugin = {
|
|
|
1346
1943
|
}
|
|
1347
1944
|
|
|
1348
1945
|
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
1349
|
-
const candidateEntries = candidates.map((candidate) => ({
|
|
1350
|
-
candidate,
|
|
1351
|
-
hash: sha256(normalizeForHash(candidate.text)),
|
|
1352
|
-
}));
|
|
1353
|
-
|
|
1354
1946
|
const prepared = await withStateLock(runtimeStatePaths.stateLockFile, async () => {
|
|
1355
1947
|
const dedupe = await readCaptureDedupeState(runtimeStatePaths);
|
|
1356
1948
|
const now = Date.now();
|
|
@@ -1390,6 +1982,8 @@ const memoryBraidPlugin = {
|
|
|
1390
1982
|
let mem0AddAttempts = 0;
|
|
1391
1983
|
let mem0AddWithId = 0;
|
|
1392
1984
|
let mem0AddWithoutId = 0;
|
|
1985
|
+
let remoteQuarantineFiltered = 0;
|
|
1986
|
+
const remediationState = await readRemediationState(runtimeStatePaths);
|
|
1393
1987
|
const successfulAdds: Array<{
|
|
1394
1988
|
memoryId: string;
|
|
1395
1989
|
hash: string;
|
|
@@ -1397,7 +1991,7 @@ const memoryBraidPlugin = {
|
|
|
1397
1991
|
}> = [];
|
|
1398
1992
|
|
|
1399
1993
|
for (const entry of prepared.pending) {
|
|
1400
|
-
const { candidate, hash } = entry;
|
|
1994
|
+
const { candidate, hash, matchedSource } = entry;
|
|
1401
1995
|
const metadata: Record<string, unknown> = {
|
|
1402
1996
|
sourceType: "capture",
|
|
1403
1997
|
workspaceHash: scope.workspaceHash,
|
|
@@ -1406,6 +2000,11 @@ const memoryBraidPlugin = {
|
|
|
1406
2000
|
category: candidate.category,
|
|
1407
2001
|
captureScore: candidate.score,
|
|
1408
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,
|
|
1409
2008
|
contentHash: hash,
|
|
1410
2009
|
indexedAt: new Date().toISOString(),
|
|
1411
2010
|
};
|
|
@@ -1423,6 +2022,17 @@ const memoryBraidPlugin = {
|
|
|
1423
2022
|
}
|
|
1424
2023
|
}
|
|
1425
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
|
+
|
|
1426
2036
|
mem0AddAttempts += 1;
|
|
1427
2037
|
const addResult = await mem0.addMemory({
|
|
1428
2038
|
text: candidate.text,
|
|
@@ -1498,6 +2108,11 @@ const memoryBraidPlugin = {
|
|
|
1498
2108
|
stats.capture.mem0AddWithoutId += mem0AddWithoutId;
|
|
1499
2109
|
stats.capture.entityAnnotatedCandidates += entityAnnotatedCandidates;
|
|
1500
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;
|
|
1501
2116
|
stats.capture.lastRunAt = new Date(now).toISOString();
|
|
1502
2117
|
|
|
1503
2118
|
await writeCaptureDedupeState(runtimeStatePaths, dedupe);
|
|
@@ -1511,9 +2126,14 @@ const memoryBraidPlugin = {
|
|
|
1511
2126
|
workspaceHash: scope.workspaceHash,
|
|
1512
2127
|
agentId: scope.agentId,
|
|
1513
2128
|
sessionKey: scope.sessionKey,
|
|
2129
|
+
capturePath: captureInput.capturePath,
|
|
2130
|
+
fallbackUsed: captureInput.fallbackUsed,
|
|
1514
2131
|
candidates: candidates.length,
|
|
1515
2132
|
pending: prepared.pending.length,
|
|
1516
2133
|
dedupeSkipped: prepared.dedupeSkipped,
|
|
2134
|
+
provenanceSkipped,
|
|
2135
|
+
transcriptShapeSkipped,
|
|
2136
|
+
quarantinedFiltered: remoteQuarantineFiltered,
|
|
1517
2137
|
persisted,
|
|
1518
2138
|
mem0AddAttempts,
|
|
1519
2139
|
mem0AddWithId,
|