opencode-claude-memory 1.6.3 → 1.6.5

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.
@@ -473,6 +473,41 @@ get_latest_session_id() {
473
473
  fi
474
474
  }
475
475
 
476
+ get_opencode_db_path() {
477
+ printf '%s\n' "$HOME/.local/share/opencode/opencode.db"
478
+ }
479
+
480
+ get_session_title_from_db() {
481
+ local session_id="$1"
482
+ local db_path
483
+ db_path=$(get_opencode_db_path)
484
+
485
+ if [ -z "$session_id" ] || [ ! -f "$db_path" ] || ! command -v python3 >/dev/null 2>&1; then
486
+ return 1
487
+ fi
488
+
489
+ python3 - "$db_path" "$session_id" <<'PY'
490
+ import sqlite3
491
+ import sys
492
+
493
+ db_path, session_id = sys.argv[1:3]
494
+
495
+ try:
496
+ conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
497
+ row = conn.execute("SELECT title FROM session WHERE id = ? LIMIT 1", (session_id,)).fetchone()
498
+ except Exception:
499
+ raise SystemExit(1)
500
+ finally:
501
+ try:
502
+ conn.close()
503
+ except Exception:
504
+ pass
505
+
506
+ if row and row[0]:
507
+ print(row[0])
508
+ PY
509
+ }
510
+
476
511
  get_session_target_id() {
477
512
  local before_json="$1"
478
513
  local started_at_ms="$2"
@@ -834,6 +869,78 @@ wait_for_session_target_id() {
834
869
  wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds"
835
870
  }
836
871
 
872
+ get_fork_cleanup_candidate_id() {
873
+ local started_at_ms="$1"
874
+ local parent_title="$2"
875
+ local workdir="$3"
876
+ local project_dir="$4"
877
+ local db_path
878
+ db_path=$(get_opencode_db_path)
879
+
880
+ if [ -z "$started_at_ms" ] || [ -z "$parent_title" ] || [ ! -f "$db_path" ] || ! command -v python3 >/dev/null 2>&1; then
881
+ return 1
882
+ fi
883
+
884
+ python3 - "$db_path" "$started_at_ms" "$parent_title" "$workdir" "$project_dir" <<'PY'
885
+ import os
886
+ import re
887
+ import sqlite3
888
+ import sys
889
+
890
+ db_path, started_at_ms_raw, parent_title, workdir, project_dir = sys.argv[1:6]
891
+ started_at_ms = int(started_at_ms_raw or "0")
892
+ scope = {os.path.realpath(path) for path in (workdir, project_dir) if path}
893
+ title_pattern = re.compile(rf"^{re.escape(parent_title)} \(fork #\d+\)$")
894
+
895
+ try:
896
+ conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
897
+ rows = conn.execute(
898
+ "SELECT id, title, directory, time_created FROM session WHERE time_created >= ? ORDER BY time_created DESC",
899
+ (started_at_ms,),
900
+ ).fetchall()
901
+ except Exception:
902
+ raise SystemExit(1)
903
+ finally:
904
+ try:
905
+ conn.close()
906
+ except Exception:
907
+ pass
908
+
909
+ matches = []
910
+ for session_id, title, directory, time_created in rows:
911
+ if not session_id or not title or not directory or not time_created:
912
+ continue
913
+ if os.path.realpath(directory) not in scope:
914
+ continue
915
+ if not title_pattern.match(title):
916
+ continue
917
+ matches.append(session_id)
918
+
919
+ if len(matches) == 1:
920
+ print(matches[0])
921
+ PY
922
+ }
923
+
924
+ wait_for_fork_cleanup_candidate_id() {
925
+ local started_at_ms="$1"
926
+ local parent_title="$2"
927
+ local wait_seconds="${3:-5}"
928
+ local attempt=0
929
+ local session_id=""
930
+
931
+ while [ "$attempt" -lt "$wait_seconds" ]; do
932
+ session_id=$(get_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
933
+ if [ -n "$session_id" ]; then
934
+ printf '%s\n' "$session_id"
935
+ return 0
936
+ fi
937
+ sleep 1
938
+ attempt=$((attempt + 1))
939
+ done
940
+
941
+ return 1
942
+ }
943
+
837
944
  file_mtime_secs() {
838
945
  local file="$1"
839
946
  if [ ! -f "$file" ]; then
@@ -973,11 +1080,10 @@ rollback_consolidation_lock() {
973
1080
  }
974
1081
 
975
1082
  cleanup_forked_sessions() {
976
- local before_json="$1"
977
- local started_at_ms="$2"
978
- local timestamp_file="$3"
1083
+ local started_at_ms="$1"
1084
+ local parent_title="$2"
979
1085
 
980
- if [ -z "$before_json" ] || [ -z "$started_at_ms" ] || [ -z "$timestamp_file" ]; then
1086
+ if [ -z "$started_at_ms" ] || [ -z "$parent_title" ]; then
981
1087
  return 0
982
1088
  fi
983
1089
 
@@ -993,7 +1099,7 @@ PY
993
1099
  fi
994
1100
 
995
1101
  local fork_id
996
- fork_id=$(wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$timestamp_file" "$SESSION_WAIT_SECONDS" 0 || true)
1102
+ fork_id=$(wait_for_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$SESSION_WAIT_SECONDS" || true)
997
1103
 
998
1104
  [ -n "$fork_id" ] || return 0
999
1105
 
@@ -1040,6 +1146,9 @@ run_extraction_if_needed() {
1040
1146
  log "Extracting memories from session $session_id..."
1041
1147
  log "Extraction log: $EXTRACT_LOG_FILE"
1042
1148
 
1149
+ local parent_session_title
1150
+ parent_session_title=$(get_session_title_from_db "$session_id" || true)
1151
+
1043
1152
  local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
1044
1153
  if [ -n "$EXTRACT_MODEL" ]; then
1045
1154
  cmd+=(-m "$EXTRACT_MODEL")
@@ -1049,10 +1158,6 @@ run_extraction_if_needed() {
1049
1158
  fi
1050
1159
  cmd+=("$EXTRACT_PROMPT")
1051
1160
 
1052
- local pre_fork_json
1053
- pre_fork_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)
1054
- local fork_timestamp_file
1055
- fork_timestamp_file=$(mktemp)
1056
1161
  local fork_started_at_ms
1057
1162
  fork_started_at_ms=$(( $(date +%s) * 1000 ))
1058
1163
 
@@ -1063,8 +1168,7 @@ run_extraction_if_needed() {
1063
1168
  log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
1064
1169
  fi
1065
1170
 
1066
- cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file"
1067
- rm -f "$fork_timestamp_file"
1171
+ cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
1068
1172
  release_simple_lock "$EXTRACT_LOCK_FILE"
1069
1173
  }
1070
1174
 
@@ -1107,6 +1211,9 @@ run_autodream_if_needed() {
1107
1211
  log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
1108
1212
  log "Auto-dream log: $AUTODREAM_LOG_FILE"
1109
1213
 
1214
+ local parent_session_title
1215
+ parent_session_title=$(get_session_title_from_db "$session_id" || true)
1216
+
1110
1217
  local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
1111
1218
  if [ -n "$AUTODREAM_MODEL" ]; then
1112
1219
  cmd+=(-m "$AUTODREAM_MODEL")
@@ -1116,10 +1223,6 @@ run_autodream_if_needed() {
1116
1223
  fi
1117
1224
  cmd+=("$AUTODREAM_PROMPT")
1118
1225
 
1119
- local pre_fork_json
1120
- pre_fork_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)
1121
- local fork_timestamp_file
1122
- fork_timestamp_file=$(mktemp)
1123
1226
  local fork_started_at_ms
1124
1227
  fork_started_at_ms=$(( $(date +%s) * 1000 ))
1125
1228
 
@@ -1132,8 +1235,7 @@ run_autodream_if_needed() {
1132
1235
  rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
1133
1236
  fi
1134
1237
 
1135
- cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file"
1136
- rm -f "$fork_timestamp_file"
1238
+ cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
1137
1239
  }
1138
1240
 
1139
1241
  run_post_session_tasks() {
package/dist/index.js CHANGED
@@ -98,9 +98,67 @@ function extractRecentTools(messages) {
98
98
  }
99
99
  return tools;
100
100
  }
101
+ // Tracks how many memory entries a memory_list call saw so tool.execute.after
102
+ // can render a meaningful title without re-reading the filesystem. Keyed by
103
+ // callID, which uniquely identifies a single tool invocation.
104
+ const memoryListCountByCallID = new Map();
105
+ const memorySearchCountByCallID = new Map();
106
+ function buildMemoryToolTitle(toolID, args, callID) {
107
+ switch (toolID) {
108
+ case "memory_save": {
109
+ const type = typeof args?.type === "string" ? args.type : "";
110
+ const name = typeof args?.name === "string" ? args.name : "";
111
+ if (type && name)
112
+ return `${type}: ${name}`;
113
+ if (name)
114
+ return name;
115
+ return undefined;
116
+ }
117
+ case "memory_delete":
118
+ case "memory_read": {
119
+ const fileName = typeof args?.file_name === "string" ? args.file_name : "";
120
+ return fileName || undefined;
121
+ }
122
+ case "memory_list": {
123
+ const count = callID ? memoryListCountByCallID.get(callID) : undefined;
124
+ if (callID)
125
+ memoryListCountByCallID.delete(callID);
126
+ if (count === undefined)
127
+ return "list memories";
128
+ return `${count} ${count === 1 ? "memory" : "memories"}`;
129
+ }
130
+ case "memory_search": {
131
+ const query = typeof args?.query === "string" ? args.query : "";
132
+ const count = callID ? memorySearchCountByCallID.get(callID) : undefined;
133
+ if (callID)
134
+ memorySearchCountByCallID.delete(callID);
135
+ if (query && count !== undefined) {
136
+ return `"${query}" · ${count} ${count === 1 ? "match" : "matches"}`;
137
+ }
138
+ if (query)
139
+ return `"${query}"`;
140
+ return undefined;
141
+ }
142
+ default:
143
+ return undefined;
144
+ }
145
+ }
146
+ function getCallID(ctx) {
147
+ if (!ctx || typeof ctx !== "object")
148
+ return undefined;
149
+ const v = ctx.callID;
150
+ return typeof v === "string" ? v : undefined;
151
+ }
101
152
  export const MemoryPlugin = async ({ worktree }) => {
102
153
  getMemoryDir(worktree);
103
154
  return {
155
+ "tool.execute.after": async (input, output) => {
156
+ if (!input.tool.startsWith("memory_"))
157
+ return;
158
+ const title = buildMemoryToolTitle(input.tool, input.args, input.callID);
159
+ if (title)
160
+ output.title = title;
161
+ },
104
162
  "experimental.chat.messages.transform": async (_input, output) => {
105
163
  const { query, sessionID } = getLastUserQuery(output.messages);
106
164
  if (sessionID) {
@@ -177,7 +235,7 @@ export const MemoryPlugin = async ({ worktree }) => {
177
235
  .string()
178
236
  .describe("Memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines"),
179
237
  },
180
- async execute(args) {
238
+ async execute(args, _ctx) {
181
239
  const filePath = saveMemory(worktree, args.file_name, args.name, args.description, args.type, args.content);
182
240
  return `Memory saved to ${filePath}`;
183
241
  },
@@ -187,7 +245,7 @@ export const MemoryPlugin = async ({ worktree }) => {
187
245
  args: {
188
246
  file_name: tool.schema.string().describe("File name of the memory to delete (with or without .md extension)"),
189
247
  },
190
- async execute(args) {
248
+ async execute(args, _ctx) {
191
249
  const deleted = deleteMemory(worktree, args.file_name);
192
250
  return deleted ? `Memory "${args.file_name}" deleted.` : `Memory "${args.file_name}" not found.`;
193
251
  },
@@ -197,8 +255,11 @@ export const MemoryPlugin = async ({ worktree }) => {
197
255
  "Use this to check what memories exist before saving a new one (to avoid duplicates) " +
198
256
  "or when you need to recall what's been stored.",
199
257
  args: {},
200
- async execute() {
258
+ async execute(_args, ctx) {
201
259
  const entries = listMemories(worktree);
260
+ const callID = getCallID(ctx);
261
+ if (callID)
262
+ memoryListCountByCallID.set(callID, entries.length);
202
263
  if (entries.length === 0) {
203
264
  return "No memories saved yet.";
204
265
  }
@@ -212,8 +273,11 @@ export const MemoryPlugin = async ({ worktree }) => {
212
273
  args: {
213
274
  query: tool.schema.string().describe("Search query — searches across name, description, and content"),
214
275
  },
215
- async execute(args) {
276
+ async execute(args, ctx) {
216
277
  const results = searchMemories(worktree, args.query);
278
+ const callID = getCallID(ctx);
279
+ if (callID)
280
+ memorySearchCountByCallID.set(callID, results.length);
217
281
  if (results.length === 0) {
218
282
  return `No memories matching "${args.query}".`;
219
283
  }
@@ -226,7 +290,7 @@ export const MemoryPlugin = async ({ worktree }) => {
226
290
  args: {
227
291
  file_name: tool.schema.string().describe("File name of the memory to read (with or without .md extension)"),
228
292
  },
229
- async execute(args) {
293
+ async execute(args, _ctx) {
230
294
  const entry = readMemory(worktree, args.file_name);
231
295
  if (!entry) {
232
296
  return `Memory "${args.file_name}" not found.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "type": "module",
5
5
  "description": "Claude Code-compatible memory compatibility layer for OpenCode — zero config, local-first, no migration",
6
6
  "main": "dist/index.js",