@ephemushroom/opencode-claude-mem 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,7 +16,6 @@ var __export = (target, all) => {
16
16
 
17
17
  // src/shared.ts
18
18
  var MAX_OBSERVATION_BYTES = 24 * 1024;
19
- var MAX_TAG_REPLACEMENTS = 100;
20
19
  var META_TOOLS = new Set([
21
20
  "askuserquestion",
22
21
  "getmcpresource",
@@ -29,26 +28,27 @@ var META_TOOLS = new Set([
29
28
  "slashcommand",
30
29
  "todowrite"
31
30
  ]);
32
- var PRIVATE_TAG_REGEX = /<private>[\s\S]*?<\/private>/g;
33
- var CONTEXT_TAG_REGEX = /<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g;
34
31
  function stripTaggedContent(text) {
35
- if (!text) {
36
- return text;
37
- }
38
- let result = text;
39
- let replacements = 0;
40
- while (replacements < MAX_TAG_REPLACEMENTS && PRIVATE_TAG_REGEX.test(result)) {
41
- PRIVATE_TAG_REGEX.lastIndex = 0;
42
- result = result.replace(PRIVATE_TAG_REGEX, "");
43
- replacements++;
32
+ let result = "";
33
+ let offset = 0;
34
+ const stack = [];
35
+ for (const match of text.matchAll(/<(\/?)(private|claude-mem-context)\b[^>]*>/gi)) {
36
+ if (stack.length === 0) {
37
+ result += text.slice(offset, match.index);
38
+ }
39
+ const name = match[2].toLowerCase();
40
+ if (match[1] === "/") {
41
+ if (stack.at(-1) === name) {
42
+ stack.pop();
43
+ }
44
+ } else {
45
+ stack.push(name);
46
+ }
47
+ offset = match.index + match[0].length;
44
48
  }
45
- PRIVATE_TAG_REGEX.lastIndex = 0;
46
- while (replacements < MAX_TAG_REPLACEMENTS && CONTEXT_TAG_REGEX.test(result)) {
47
- CONTEXT_TAG_REGEX.lastIndex = 0;
48
- result = result.replace(CONTEXT_TAG_REGEX, "");
49
- replacements++;
49
+ if (stack.length === 0) {
50
+ result += text.slice(offset);
50
51
  }
51
- CONTEXT_TAG_REGEX.lastIndex = 0;
52
52
  return result.trim();
53
53
  }
54
54
  function sanitizeObservationValue(value) {
@@ -90,7 +90,7 @@ function shouldSkipObservationTool(toolName) {
90
90
  return true;
91
91
  }
92
92
  const normalizedName = toolName.toLowerCase().replaceAll("_", "-");
93
- if (normalizedName.includes("mcp-search") || /(?:^|[-.])mem-(?:search|timeline|get-observations)$/.test(normalizedName)) {
93
+ if (normalizedName.includes("mcp-search") || /(?:^|[-.])mem-(?:save|search|timeline|get-observations)$/.test(normalizedName)) {
94
94
  return true;
95
95
  }
96
96
  return META_TOOLS.has(normalizedName);
@@ -12436,6 +12436,44 @@ function tool(input) {
12436
12436
  return input;
12437
12437
  }
12438
12438
  tool.schema = exports_external;
12439
+ // src/memory-turn.ts
12440
+ function memoryTurn(id, text, media = false) {
12441
+ const cleaned = stripTaggedContent(text.replace(/<(system-reminder|system_instruction|system-instruction|persisted-output)\b[^>]*>[\s\S]*?<\/\1>/gi, ""));
12442
+ const internal = /^\s*<task-notification\b[^>]*>[\s\S]*?<\/task-notification>\s*$/i.test(text);
12443
+ return {
12444
+ id,
12445
+ text: truncateUtf8Bytes(cleaned || (media ? "[media prompt]" : ""), 256 * 1024),
12446
+ capture: !internal && (Boolean(cleaned) || media && !text.trim()),
12447
+ semantic: !internal && cleaned.length >= 20
12448
+ };
12449
+ }
12450
+ var deliveredRecord = object({
12451
+ id: string2(),
12452
+ type: string2(),
12453
+ text: string2().optional(),
12454
+ files: array(unknown()).optional(),
12455
+ metadata: object({ synthetic: boolean2().optional(), agentId: string2().optional() }).optional()
12456
+ });
12457
+ var v1Record = object({
12458
+ info: object({ id: string2(), role: string2() }),
12459
+ parts: array(unknown())
12460
+ });
12461
+ function resumedV1Turn(messages) {
12462
+ if (!Array.isArray(messages)) {
12463
+ return;
12464
+ }
12465
+ for (const raw of messages.toReversed()) {
12466
+ const parsed = v1Record.safeParse(raw);
12467
+ if (parsed.success && parsed.data.info.role === "user") {
12468
+ return memoryTurn(parsed.data.info.id, extractTextFromParts(parsed.data.parts), parsed.data.parts.some((part) => typeof part === "object" && part !== null && ("type" in part) && part.type === "file"));
12469
+ }
12470
+ }
12471
+ return;
12472
+ }
12473
+
12474
+ // src/file-context.ts
12475
+ import { relative, resolve } from "path";
12476
+
12439
12477
  // src/worker-client.ts
12440
12478
  import { existsSync, readFileSync } from "fs";
12441
12479
  import { homedir } from "os";
@@ -12636,7 +12674,16 @@ class WorkerClient {
12636
12674
  if (!response.ok) {
12637
12675
  return null;
12638
12676
  }
12639
- return await response.json();
12677
+ const value = await response.json();
12678
+ if (!isRecord(value) || typeof value.sessionDbId !== "number" || typeof value.promptNumber !== "number" || !Number.isFinite(value.sessionDbId) || !Number.isFinite(value.promptNumber)) {
12679
+ return null;
12680
+ }
12681
+ return {
12682
+ sessionDbId: value.sessionDbId,
12683
+ promptNumber: value.promptNumber,
12684
+ skipped: value.skipped === true,
12685
+ reason: typeof value.reason === "string" ? value.reason : undefined
12686
+ };
12640
12687
  } catch {
12641
12688
  return null;
12642
12689
  }
@@ -12670,8 +12717,8 @@ class WorkerClient {
12670
12717
  headers: { "Content-Type": "application/json" },
12671
12718
  body: JSON.stringify({
12672
12719
  contentSessionId,
12673
- last_user_message: lastUserMessage,
12674
- last_assistant_message: lastAssistantMessage,
12720
+ last_user_message: stripTaggedContent(lastUserMessage),
12721
+ last_assistant_message: stripTaggedContent(lastAssistantMessage),
12675
12722
  platformSource: PLATFORM_SOURCE,
12676
12723
  observedModel: metadata.observedModel,
12677
12724
  observedBilling: metadata.observedBilling
@@ -12684,7 +12731,7 @@ class WorkerClient {
12684
12731
  }
12685
12732
  static async getContext(project) {
12686
12733
  try {
12687
- const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`);
12734
+ const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`, { signal: AbortSignal.timeout(2000) });
12688
12735
  if (!response.ok) {
12689
12736
  return null;
12690
12737
  }
@@ -12835,6 +12882,376 @@ class WorkerClient {
12835
12882
  }
12836
12883
  }
12837
12884
 
12885
+ // src/memory-worker.ts
12886
+ var saveInput = object({
12887
+ text: string2().min(1).max(MAX_OBSERVATION_BYTES),
12888
+ title: string2().max(MAX_OBSERVATION_BYTES).optional(),
12889
+ project: string2().trim().min(1).max(1000).optional()
12890
+ });
12891
+ var saveResponse = object({
12892
+ success: literal(true),
12893
+ id: number2().int().positive(),
12894
+ title: string2(),
12895
+ project: string2(),
12896
+ message: string2()
12897
+ });
12898
+ var semanticResponse = object({ context: string2(), count: number2().int().nonnegative() });
12899
+ var fileResponse = object({
12900
+ observations: array(object({
12901
+ id: number2().int().positive(),
12902
+ title: string2().nullable(),
12903
+ type: string2(),
12904
+ created_at_epoch: number2().finite(),
12905
+ memory_session_id: string2().nullable().optional()
12906
+ })),
12907
+ count: number2().int().nonnegative()
12908
+ });
12909
+
12910
+ class MemoryWorker {
12911
+ static baseUrl = getWorkerBaseUrl();
12912
+ static async json(path, init) {
12913
+ try {
12914
+ const response = await fetch(`${this.baseUrl}${path}`, init);
12915
+ if (!response.ok) {
12916
+ return null;
12917
+ }
12918
+ const reader = response.body?.getReader();
12919
+ if (!reader) {
12920
+ return null;
12921
+ }
12922
+ const chunks = [];
12923
+ let size = 0;
12924
+ try {
12925
+ while (true) {
12926
+ const { done, value } = await reader.read();
12927
+ if (done) {
12928
+ break;
12929
+ }
12930
+ size += value.byteLength;
12931
+ if (size > 512 * 1024) {
12932
+ return null;
12933
+ }
12934
+ chunks.push(value);
12935
+ }
12936
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
12937
+ } finally {
12938
+ await reader.cancel();
12939
+ }
12940
+ } catch {
12941
+ return null;
12942
+ }
12943
+ }
12944
+ static async save(input, project) {
12945
+ const parsed = saveInput.safeParse(input);
12946
+ if (!parsed.success) {
12947
+ return "Memory not saved: invalid or oversized input.";
12948
+ }
12949
+ const text = stripTaggedContent(parsed.data.text);
12950
+ const title = parsed.data.title === undefined ? undefined : stripTaggedContent(parsed.data.title);
12951
+ if (!text || title !== undefined && !title || Buffer.byteLength(text) > MAX_OBSERVATION_BYTES || title !== undefined && Buffer.byteLength(title) > MAX_OBSERVATION_BYTES) {
12952
+ return "Memory not saved: content is empty, private, or exceeds 24 KiB.";
12953
+ }
12954
+ const result = saveResponse.safeParse(await this.json("/api/memory/save", {
12955
+ method: "POST",
12956
+ signal: AbortSignal.timeout(1e4),
12957
+ headers: { "Content-Type": "application/json" },
12958
+ body: JSON.stringify({
12959
+ text,
12960
+ title,
12961
+ project: parsed.data.project ?? project,
12962
+ metadata: { platformSource: "opencode" }
12963
+ })
12964
+ }));
12965
+ return result.success ? JSON.stringify(result.data) : "Memory save not confirmed. The Worker may be unavailable or unsupported; check before retrying to avoid duplicates.";
12966
+ }
12967
+ static async semantic(prompt, project, options) {
12968
+ const q = stripTaggedContent(prompt);
12969
+ if (!options.enabled || q.length < 20) {
12970
+ return "";
12971
+ }
12972
+ const result = semanticResponse.safeParse(await this.json("/api/context/semantic", {
12973
+ method: "POST",
12974
+ signal: AbortSignal.timeout(options.timeoutMs),
12975
+ headers: { "Content-Type": "application/json" },
12976
+ body: JSON.stringify({ q, project, limit: options.limit })
12977
+ }));
12978
+ return result.success ? stripTaggedContent(result.data.context).slice(0, options.maxChars) : "";
12979
+ }
12980
+ static async byFile(paths, project, options) {
12981
+ const lookahead = Math.max(40, options.limit);
12982
+ const params = new URLSearchParams({ projects: project, limit: String(lookahead) });
12983
+ for (const path of new Set(paths.map((item) => item.replaceAll("\\", "/")))) {
12984
+ params.append("path", path);
12985
+ }
12986
+ const result = fileResponse.safeParse(await this.json(`/api/observations/by-file?${params}`, {
12987
+ signal: AbortSignal.timeout(options.timeoutMs)
12988
+ }));
12989
+ return result.success ? result.data.observations.slice(0, lookahead) : [];
12990
+ }
12991
+ }
12992
+
12993
+ // src/file-context.ts
12994
+ import { stat } from "fs/promises";
12995
+ var readInput = object({
12996
+ filePath: string2().optional(),
12997
+ file_path: string2().optional(),
12998
+ filePaths: array(string2()).optional(),
12999
+ file_paths: array(string2()).optional(),
13000
+ path: string2().optional()
13001
+ }).passthrough();
13002
+
13003
+ class FileContext {
13004
+ sessions = new Map;
13005
+ options;
13006
+ worker;
13007
+ constructor(options, worker = MemoryWorker) {
13008
+ this.options = options;
13009
+ this.worker = worker;
13010
+ }
13011
+ async read(session, input, location) {
13012
+ if (!this.options.enabled || !/(?:^|[._-])read(?:_file|_files)?$/i.test(input.tool)) {
13013
+ return "";
13014
+ }
13015
+ const parsed = readInput.safeParse(input.args);
13016
+ if (!parsed.success) {
13017
+ return "";
13018
+ }
13019
+ const args = parsed.data;
13020
+ const paths = [
13021
+ ...new Set([
13022
+ args.filePath,
13023
+ args.file_path,
13024
+ args.path,
13025
+ ...args.filePaths ?? [],
13026
+ ...args.file_paths ?? []
13027
+ ].filter((path) => path !== undefined))
13028
+ ].slice(0, 10);
13029
+ let seen = this.sessions.get(session);
13030
+ if (!seen) {
13031
+ seen = new Map;
13032
+ this.sessions.set(session, seen);
13033
+ }
13034
+ const revisions = seen;
13035
+ const attempted = new Map;
13036
+ const deadline = Date.now() + this.options.timeoutMs;
13037
+ let active = true;
13038
+ const timeout = Promise.withResolvers();
13039
+ const timer = setTimeout(() => timeout.resolve(""), this.options.timeoutMs);
13040
+ const pending = (async () => {
13041
+ const results = await Promise.all(paths.map(async (path) => {
13042
+ const absolute = resolve(location.directory, path);
13043
+ const file = await stat(absolute).catch(() => null);
13044
+ if (!active || this.sessions.get(session) !== revisions || !file?.isFile() || file.size < 1500) {
13045
+ return [];
13046
+ }
13047
+ const revision = `${file.mtimeMs}:${file.size}`;
13048
+ const inFlight = `pending:${revision}`;
13049
+ if (revisions.get(absolute) === revision || revisions.get(absolute) === inFlight) {
13050
+ return [];
13051
+ }
13052
+ attempted.set(absolute, inFlight);
13053
+ revisions.set(absolute, inFlight);
13054
+ const rows = await this.worker.byFile([absolute, relative(location.directory, absolute)], location.project, { ...this.options, timeoutMs: Math.max(1, deadline - Date.now()) });
13055
+ if (!active || this.sessions.get(session) !== revisions || !rows.length || file.mtimeMs >= Math.max(...rows.map((row) => row.created_at_epoch))) {
13056
+ return [];
13057
+ }
13058
+ return rows.map((row) => ({ row, path: absolute, revision }));
13059
+ }));
13060
+ const sessions = new Set;
13061
+ const selected = results.flat().toSorted((a, b) => b.row.created_at_epoch - a.row.created_at_epoch).filter(({ row }) => {
13062
+ const key = row.memory_session_id ?? `observation:${row.id}`;
13063
+ if (sessions.has(key)) {
13064
+ return false;
13065
+ }
13066
+ sessions.add(key);
13067
+ return true;
13068
+ }).slice(0, this.options.limit);
13069
+ const delivered = new Map;
13070
+ let text = "";
13071
+ for (const { row, path, revision } of selected) {
13072
+ const separator = text ? `
13073
+ ` : "";
13074
+ const available = this.options.maxChars - text.length - separator.length;
13075
+ if (available <= 0) {
13076
+ break;
13077
+ }
13078
+ const line = `#${row.id} ${stripTaggedContent(row.type)}: ${stripTaggedContent(row.title ?? "")}`;
13079
+ text += separator + line.slice(0, available);
13080
+ delivered.set(path, revision);
13081
+ }
13082
+ if (text && active && this.sessions.get(session) === revisions) {
13083
+ for (const [path, revision] of delivered) {
13084
+ revisions.set(path, revision);
13085
+ }
13086
+ }
13087
+ return text ? `
13088
+
13089
+ <claude-mem-context>
13090
+ Supplementary file history (past observations, not file contents):
13091
+ ${text}
13092
+ </claude-mem-context>` : "";
13093
+ })();
13094
+ try {
13095
+ return await Promise.race([pending, timeout.promise]);
13096
+ } catch {
13097
+ return "";
13098
+ } finally {
13099
+ active = false;
13100
+ clearTimeout(timer);
13101
+ for (const [path, revision] of attempted) {
13102
+ if (revisions.get(path) === revision) {
13103
+ revisions.delete(path);
13104
+ }
13105
+ }
13106
+ }
13107
+ }
13108
+ delete(session) {
13109
+ this.sessions.delete(session);
13110
+ }
13111
+ clear() {
13112
+ this.sessions.clear();
13113
+ }
13114
+ }
13115
+
13116
+ // src/memory-session.ts
13117
+ class MemorySessions {
13118
+ sessions = new Map;
13119
+ deleted = new Set;
13120
+ options;
13121
+ worker;
13122
+ constructor(options, worker = MemoryWorker) {
13123
+ this.options = options;
13124
+ this.worker = worker;
13125
+ }
13126
+ state(id) {
13127
+ let state = this.sessions.get(id);
13128
+ if (!state) {
13129
+ state = {
13130
+ turns: new Map,
13131
+ registered: new Set,
13132
+ blocked: new Set,
13133
+ pending: [],
13134
+ semantic: new Map
13135
+ };
13136
+ this.sessions.set(id, state);
13137
+ }
13138
+ return state;
13139
+ }
13140
+ select(session, turn) {
13141
+ if (this.isDeleted(session)) {
13142
+ return false;
13143
+ }
13144
+ const state = this.state(session);
13145
+ const known = state.turns.get(turn.id);
13146
+ if (known && state.current !== known) {
13147
+ return false;
13148
+ }
13149
+ state.current = known ?? turn;
13150
+ if (!known) {
13151
+ state.turns.set(turn.id, turn);
13152
+ if (turn.capture) {
13153
+ state.pending.push(turn);
13154
+ }
13155
+ }
13156
+ return !known;
13157
+ }
13158
+ hasTurn(session) {
13159
+ return Boolean(this.sessions.get(session)?.current);
13160
+ }
13161
+ hasMessage(session, id) {
13162
+ return this.sessions.get(session)?.turns.has(id) ?? false;
13163
+ }
13164
+ isDeleted(session) {
13165
+ return this.deleted.has(session);
13166
+ }
13167
+ canCapture(session) {
13168
+ const state = this.sessions.get(session);
13169
+ const turn = state?.current;
13170
+ return Boolean(turn?.capture && state?.registered.has(turn.id) && !state.blocked.has(turn.id));
13171
+ }
13172
+ async register(session, project) {
13173
+ const state = this.sessions.get(session);
13174
+ if (!state?.current?.capture) {
13175
+ return false;
13176
+ }
13177
+ if (state.registration) {
13178
+ return state.registration;
13179
+ }
13180
+ const pending = (async () => {
13181
+ while (state.pending.length) {
13182
+ const [turn] = state.pending;
13183
+ if (!turn || this.sessions.get(session) !== state) {
13184
+ return false;
13185
+ }
13186
+ const result = await WorkerClient.sessionInit(session, project, turn.text);
13187
+ if (!result || this.sessions.get(session) !== state) {
13188
+ return false;
13189
+ }
13190
+ if (result.skipped && result.reason !== "duplicate") {
13191
+ state.blocked.add(turn.id);
13192
+ }
13193
+ state.registered.add(turn.id);
13194
+ state.pending.shift();
13195
+ }
13196
+ return this.canCapture(session);
13197
+ })();
13198
+ state.registration = pending;
13199
+ try {
13200
+ return await pending;
13201
+ } finally {
13202
+ state.registration = undefined;
13203
+ }
13204
+ }
13205
+ async semantic(session, project) {
13206
+ const state = this.sessions.get(session);
13207
+ const turn = state?.current;
13208
+ if (!state || !turn?.semantic || !this.canCapture(session)) {
13209
+ return "";
13210
+ }
13211
+ let pending = state.semantic.get(turn.id);
13212
+ if (!pending) {
13213
+ pending = this.worker.semantic(turn.text, project, this.options.semanticInjection);
13214
+ state.semantic.set(turn.id, pending);
13215
+ }
13216
+ const result = await pending;
13217
+ return this.sessions.get(session) === state && state.current === turn ? result : "";
13218
+ }
13219
+ async context(session, project) {
13220
+ if (this.isDeleted(session)) {
13221
+ return null;
13222
+ }
13223
+ const state = this.state(session);
13224
+ if (!state.context) {
13225
+ state.context = WorkerClient.getContext(project);
13226
+ }
13227
+ const pending = state.context;
13228
+ const value = await pending;
13229
+ if (this.sessions.get(session) !== state || state.context !== pending) {
13230
+ return null;
13231
+ }
13232
+ if (value === null) {
13233
+ state.context = undefined;
13234
+ }
13235
+ return value === null ? null : stripTaggedContent(value).slice(0, 24000);
13236
+ }
13237
+ invalidate(session) {
13238
+ const state = this.sessions.get(session);
13239
+ if (state) {
13240
+ state.context = undefined;
13241
+ }
13242
+ }
13243
+ delete(session) {
13244
+ this.deleted.add(session);
13245
+ this.sessions.delete(session);
13246
+ }
13247
+ clear() {
13248
+ for (const session of this.sessions.keys()) {
13249
+ this.deleted.add(session);
13250
+ }
13251
+ this.sessions.clear();
13252
+ }
13253
+ }
13254
+
12838
13255
  // src/tui-registration.ts
12839
13256
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12840
13257
  import { homedir as homedir2 } from "os";
@@ -12904,10 +13321,73 @@ function ensureTuiPluginEntry() {
12904
13321
  }
12905
13322
  }
12906
13323
 
13324
+ // src/memory-options.ts
13325
+ import { homedir as homedir3 } from "os";
13326
+ import { join as join3 } from "path";
13327
+ import { readFileSync as readFileSync3 } from "fs";
13328
+ var group = object({
13329
+ enabled: boolean2().optional().catch(undefined),
13330
+ limit: number2().int().min(1).max(50).optional().catch(undefined),
13331
+ maxChars: number2().int().min(256).max(24000).optional().catch(undefined),
13332
+ timeoutMs: number2().int().min(100).max(1e4).optional().catch(undefined)
13333
+ }).catch({});
13334
+ var optionsSchema = object({
13335
+ semanticInjection: group.optional(),
13336
+ fileContext: group.optional()
13337
+ }).catch({});
13338
+ var settingsSchema = object({
13339
+ CLAUDE_MEM_SEMANTIC_INJECT: unknown().optional(),
13340
+ CLAUDE_MEM_SEMANTIC_INJECT_LIMIT: unknown().optional()
13341
+ }).catch({});
13342
+ function booleanSetting(value) {
13343
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
13344
+ if (normalized === true || normalized === "true" || normalized === "1") {
13345
+ return true;
13346
+ }
13347
+ if (normalized === false || normalized === "false" || normalized === "0") {
13348
+ return false;
13349
+ }
13350
+ return;
13351
+ }
13352
+ function limitSetting(value) {
13353
+ const parsed = exports_coerce.number().int().min(1).max(50).safeParse(value);
13354
+ return parsed.success ? parsed.data : undefined;
13355
+ }
13356
+ function parseMemoryOptions(options, env, settings) {
13357
+ const explicit = optionsSchema.parse(options);
13358
+ const stored = settingsSchema.parse(settings);
13359
+ return {
13360
+ semanticInjection: {
13361
+ enabled: explicit.semanticInjection?.enabled ?? booleanSetting(env.CLAUDE_MEM_SEMANTIC_INJECT) ?? booleanSetting(stored.CLAUDE_MEM_SEMANTIC_INJECT) ?? false,
13362
+ limit: explicit.semanticInjection?.limit ?? limitSetting(env.CLAUDE_MEM_SEMANTIC_INJECT_LIMIT) ?? limitSetting(stored.CLAUDE_MEM_SEMANTIC_INJECT_LIMIT) ?? 5,
13363
+ maxChars: explicit.semanticInjection?.maxChars ?? 6000,
13364
+ timeoutMs: explicit.semanticInjection?.timeoutMs ?? 2000
13365
+ },
13366
+ fileContext: {
13367
+ enabled: explicit.fileContext?.enabled ?? true,
13368
+ limit: explicit.fileContext?.limit ?? 15,
13369
+ maxChars: explicit.fileContext?.maxChars ?? 6000,
13370
+ timeoutMs: explicit.fileContext?.timeoutMs ?? 1500
13371
+ }
13372
+ };
13373
+ }
13374
+ function resolveMemoryOptions(options) {
13375
+ let settings = {};
13376
+ try {
13377
+ settings = JSON.parse(readFileSync3(join3(homedir3(), ".claude-mem", "settings.json"), "utf8"));
13378
+ } catch {}
13379
+ return parseMemoryOptions(options, process.env, settings);
13380
+ }
13381
+
12907
13382
  // src/index.ts
12908
13383
  var ASSISTANT_FLUSH_DEBOUNCE_MS = 250;
12909
- var ClaudeMemPlugin = async (ctx) => {
13384
+ var ClaudeMemPlugin = async (ctx, options) => {
12910
13385
  const { project, directory, client } = ctx;
13386
+ const config = resolveMemoryOptions(options);
13387
+ const memory = new MemorySessions(config);
13388
+ const files = new FileContext(config.fileContext);
13389
+ const resumed = new Map;
13390
+ const subagentSessions = new Set;
12911
13391
  ensureTuiPluginEntry();
12912
13392
  const projectRoot = directory || process.cwd();
12913
13393
  const projectName = project?.worktree ? project.worktree.split(/[\\/]/).findLast(Boolean) || "unknown-project" : "unknown-project";
@@ -12918,17 +13398,6 @@ var ClaudeMemPlugin = async (ctx) => {
12918
13398
  });
12919
13399
  } catch {}
12920
13400
  }
12921
- let contextCache = undefined;
12922
- async function getCachedContext() {
12923
- if (contextCache !== undefined) {
12924
- return contextCache;
12925
- }
12926
- const context = await WorkerClient.getContext(projectName);
12927
- if (context !== null) {
12928
- contextCache = context;
12929
- }
12930
- return context;
12931
- }
12932
13401
  let workerHealthy = null;
12933
13402
  let initToastShown = false;
12934
13403
  async function checkWorkerAndToast() {
@@ -12941,9 +13410,17 @@ var ClaudeMemPlugin = async (ctx) => {
12941
13410
  return workerHealthy;
12942
13411
  }
12943
13412
  let currentSessionId = null;
12944
- const initializedSessions = new Set;
12945
13413
  const assistantBuffers = new Map;
13414
+ const assistantFlushes = new Map;
12946
13415
  async function flushAssistantBuffer(sessionId) {
13416
+ const existing = assistantFlushes.get(sessionId);
13417
+ if (existing) {
13418
+ await existing;
13419
+ }
13420
+ if (!memory.canCapture(sessionId)) {
13421
+ discardAssistantBuffer(sessionId);
13422
+ return;
13423
+ }
12947
13424
  const buf = assistantBuffers.get(sessionId);
12948
13425
  if (!buf || !buf.messageId) {
12949
13426
  return;
@@ -12954,13 +13431,23 @@ var ClaudeMemPlugin = async (ctx) => {
12954
13431
  }
12955
13432
  const { messageId } = buf;
12956
13433
  buf.messageId = null;
13434
+ const pending = (async () => {
13435
+ try {
13436
+ const text = await fetchAssistantMessageText(sessionId, messageId);
13437
+ const sanitized = truncateUtf8Bytes(stripTaggedContent(text), MAX_OBSERVATION_BYTES);
13438
+ if (sanitized) {
13439
+ await WorkerClient.sendObservation(sessionId, "assistant_message", { messageId }, sanitized, projectRoot);
13440
+ }
13441
+ } catch {}
13442
+ })();
13443
+ assistantFlushes.set(sessionId, pending);
12957
13444
  try {
12958
- const text = await fetchAssistantMessageText(sessionId, messageId);
12959
- const sanitized = truncateUtf8Bytes(stripTaggedContent(text), MAX_OBSERVATION_BYTES);
12960
- if (sanitized) {
12961
- await WorkerClient.sendObservation(sessionId, "assistant_message", { messageId }, sanitized, projectRoot);
13445
+ await pending;
13446
+ } finally {
13447
+ if (assistantFlushes.get(sessionId) === pending) {
13448
+ assistantFlushes.delete(sessionId);
12962
13449
  }
12963
- } catch {}
13450
+ }
12964
13451
  }
12965
13452
  function scheduleAssistantFlush(sessionId) {
12966
13453
  const buf = assistantBuffers.get(sessionId);
@@ -12982,23 +13469,34 @@ var ClaudeMemPlugin = async (ctx) => {
12982
13469
  }
12983
13470
  assistantBuffers.delete(sessionId);
12984
13471
  }
12985
- async function ensureSessionInit(sessionId, prompt) {
12986
- if (initializedSessions.has(sessionId)) {
12987
- return true;
12988
- }
12989
- const isHealthy = await checkWorkerAndToast();
12990
- if (!isHealthy) {
13472
+ async function ensureSessionInit(sessionId) {
13473
+ if (memory.isDeleted(sessionId)) {
12991
13474
  return false;
12992
13475
  }
12993
13476
  try {
12994
- const result = await WorkerClient.sessionInit(sessionId, projectName, prompt || "SESSION_START");
12995
- if (!result) {
13477
+ if (!memory.hasTurn(sessionId)) {
13478
+ let pending = resumed.get(sessionId);
13479
+ if (!pending) {
13480
+ pending = (async () => {
13481
+ const result = await client.session.messages({ path: { id: sessionId } });
13482
+ const turn = resumedV1Turn(result.data);
13483
+ if (turn && !memory.hasTurn(sessionId) && resumed.has(sessionId)) {
13484
+ memory.select(sessionId, turn);
13485
+ }
13486
+ })();
13487
+ resumed.set(sessionId, pending);
13488
+ }
13489
+ await pending;
13490
+ if (!memory.hasTurn(sessionId)) {
13491
+ resumed.delete(sessionId);
13492
+ }
13493
+ }
13494
+ if (!memory.hasTurn(sessionId) || !await checkWorkerAndToast()) {
12996
13495
  return false;
12997
13496
  }
12998
- initializedSessions.add(sessionId);
12999
- currentSessionId = sessionId;
13000
- return true;
13497
+ return await memory.register(sessionId, projectName);
13001
13498
  } catch {
13499
+ resumed.delete(sessionId);
13002
13500
  return false;
13003
13501
  }
13004
13502
  }
@@ -13042,6 +13540,15 @@ var ClaudeMemPlugin = async (ctx) => {
13042
13540
  }
13043
13541
  return {
13044
13542
  tool: {
13543
+ "mem-save": tool({
13544
+ description: "Save an explicit memory to Claude-Mem. Private and injected context are removed. Check an uncertain result before retrying.",
13545
+ args: {
13546
+ text: tool.schema.string().min(1).max(MAX_OBSERVATION_BYTES),
13547
+ title: tool.schema.string().max(MAX_OBSERVATION_BYTES).optional(),
13548
+ project: tool.schema.string().min(1).optional()
13549
+ },
13550
+ execute: async (input) => MemoryWorker.save(input, projectName)
13551
+ }),
13045
13552
  "mem-search": tool({
13046
13553
  description: "Search Claude-Mem persistent memory. Supports query, project, platformSource, type, obs_type, dateStart, dateEnd, offset, and orderBy filters.",
13047
13554
  args: {
@@ -13136,7 +13643,10 @@ var ClaudeMemPlugin = async (ctx) => {
13136
13643
  return;
13137
13644
  }
13138
13645
  currentSessionId = sessionId;
13139
- contextCache = undefined;
13646
+ if (event.properties?.info?.parentID) {
13647
+ subagentSessions.add(sessionId);
13648
+ }
13649
+ memory.invalidate(sessionId);
13140
13650
  await checkWorkerAndToast();
13141
13651
  return;
13142
13652
  }
@@ -13186,7 +13696,11 @@ var ClaudeMemPlugin = async (ctx) => {
13186
13696
  if (!sessionId) {
13187
13697
  return;
13188
13698
  }
13699
+ memory.invalidate(sessionId);
13189
13700
  await flushAssistantBuffer(sessionId);
13701
+ if (!await ensureSessionInit(sessionId)) {
13702
+ return;
13703
+ }
13190
13704
  const isHealthy = await checkWorkerAndToast();
13191
13705
  if (!isHealthy) {
13192
13706
  return;
@@ -13203,6 +13717,9 @@ var ClaudeMemPlugin = async (ctx) => {
13203
13717
  return;
13204
13718
  }
13205
13719
  await flushAssistantBuffer(sessionId);
13720
+ if (!await ensureSessionInit(sessionId)) {
13721
+ return;
13722
+ }
13206
13723
  try {
13207
13724
  const { user, assistant, observedModel } = await fetchLastMessages(sessionId);
13208
13725
  if (await WorkerClient.summarize(sessionId, user, assistant, { observedModel })) {
@@ -13218,7 +13735,10 @@ var ClaudeMemPlugin = async (ctx) => {
13218
13735
  }
13219
13736
  await flushAssistantBuffer(sessionId);
13220
13737
  discardAssistantBuffer(sessionId);
13221
- initializedSessions.delete(sessionId);
13738
+ memory.delete(sessionId);
13739
+ files.delete(sessionId);
13740
+ resumed.delete(sessionId);
13741
+ subagentSessions.delete(sessionId);
13222
13742
  if (currentSessionId === sessionId) {
13223
13743
  currentSessionId = null;
13224
13744
  }
@@ -13233,11 +13753,23 @@ var ClaudeMemPlugin = async (ctx) => {
13233
13753
  const sessionId = input.sessionID;
13234
13754
  if (sessionId) {
13235
13755
  const userPrompt = extractTextFromParts(output.parts);
13236
- await ensureSessionInit(sessionId, userPrompt || undefined);
13756
+ const id = input.messageID ?? output.message.id;
13757
+ if (!id) {
13758
+ return;
13759
+ }
13760
+ if (!memory.hasMessage(sessionId, id)) {
13761
+ await flushAssistantBuffer(sessionId);
13762
+ }
13763
+ currentSessionId = sessionId;
13764
+ memory.select(sessionId, memoryTurn(id, userPrompt, output.parts.some((part) => part.type === "file")));
13765
+ await ensureSessionInit(sessionId);
13237
13766
  }
13238
13767
  },
13239
13768
  "experimental.chat.system.transform": async (input, output) => {
13240
- const sessionId = input.sessionID;
13769
+ const sessionId = input.sessionID ?? currentSessionId;
13770
+ if (!sessionId) {
13771
+ return;
13772
+ }
13241
13773
  if (sessionId) {
13242
13774
  await ensureSessionInit(sessionId);
13243
13775
  }
@@ -13246,7 +13778,16 @@ var ClaudeMemPlugin = async (ctx) => {
13246
13778
  return;
13247
13779
  }
13248
13780
  try {
13249
- const context = await getCachedContext();
13781
+ const base = await memory.context(sessionId, projectName);
13782
+ const semantic = await memory.semantic(sessionId, projectName);
13783
+ const context = [base, semantic].filter(Boolean).join(`
13784
+
13785
+ `);
13786
+ for (let i = output.system.length - 1;i >= 0; i--) {
13787
+ if (output.system[i].startsWith("<claude-mem-context>")) {
13788
+ output.system.splice(i, 1);
13789
+ }
13790
+ }
13250
13791
  if (context) {
13251
13792
  output.system.push(`<claude-mem-context>
13252
13793
  [Claude-Mem] Memory Active. Previous Context:
@@ -13257,6 +13798,7 @@ ${context}
13257
13798
  },
13258
13799
  "experimental.session.compacting": async (input, output) => {
13259
13800
  const sessionId = input.sessionID;
13801
+ memory.invalidate(sessionId);
13260
13802
  if (sessionId) {
13261
13803
  await ensureSessionInit(sessionId);
13262
13804
  }
@@ -13265,7 +13807,7 @@ ${context}
13265
13807
  return;
13266
13808
  }
13267
13809
  try {
13268
- const context = await getCachedContext();
13810
+ const context = await memory.context(sessionId, projectName);
13269
13811
  if (context) {
13270
13812
  output.context.push(`<claude-mem-context>
13271
13813
  [Claude-Mem] Memory Active. Previous Context:
@@ -13289,6 +13831,8 @@ ${context}
13289
13831
  const sanitizedToolInput = sanitizeObservationValue(input.args || {});
13290
13832
  const sanitizedToolOutput = truncateUtf8Bytes(stripTaggedContent(normalizeToolOutput(output.output)), MAX_OBSERVATION_BYTES);
13291
13833
  await WorkerClient.sendObservation(sessionId, input.tool, sanitizedToolInput, sanitizedToolOutput, projectRoot, input.callID);
13834
+ const history = subagentSessions.has(sessionId) ? "" : await files.read(sessionId, { tool: input.tool, args: input.args }, { directory: projectRoot, project: projectName });
13835
+ output.output += history;
13292
13836
  } catch {}
13293
13837
  }
13294
13838
  };