opencode-claude-memory 1.6.2 → 1.6.4

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 CHANGED
@@ -29,6 +29,30 @@ Claude Code writes memory → OpenCode reads it. OpenCode writes memory → Clau
29
29
 
30
30
  ## 🚀 Quick Start
31
31
 
32
+ ### Prerequisites
33
+
34
+ - `opencode`
35
+ - `python3` available in `PATH`
36
+
37
+ `python3` is a runtime dependency for the wrapper's scoped session detection and fork cleanup logic.
38
+ If it is missing or not executable, post-session maintenance becomes less reliable: session targeting can fall back to less precise heuristics, and fork cleanup is skipped for safety.
39
+
40
+ Common install commands:
41
+
42
+ ```bash
43
+ # macOS (Homebrew)
44
+ brew install python
45
+
46
+ # Ubuntu / Debian
47
+ sudo apt-get update && sudo apt-get install -y python3
48
+
49
+ # Fedora
50
+ sudo dnf install -y python3
51
+
52
+ # Arch Linux
53
+ sudo pacman -S python
54
+ ```
55
+
32
56
  ### 1. Install
33
57
 
34
58
  ```bash
@@ -41,6 +65,8 @@ This installs:
41
65
  - The `opencode-memory` **CLI** — wraps opencode with automatic memory extraction + auto-dream consolidation
42
66
  - A **shell hook** — defines an `opencode()` function in your `.zshrc`/`.bashrc` that delegates to `opencode-memory`
43
67
 
68
+ If `python3` is not installed yet, install it first using the commands above before enabling the shell hook.
69
+
44
70
  ### 2. Configure
45
71
 
46
72
  ```jsonc
@@ -118,6 +144,18 @@ The shell hook defines an `opencode()` function that delegates to `opencode-memo
118
144
  8. Maintenance runs **in the background** unless `OPENCODE_MEMORY_FOREGROUND=1`
119
145
  9. Terminal maintenance logs are shown in foreground mode by default, or can be forced on/off with `OPENCODE_MEMORY_TERMINAL_LOG=1|0`
120
146
 
147
+ ### Runtime dependencies
148
+
149
+ The wrapper expects `python3` to be available at runtime.
150
+
151
+ It is used for:
152
+
153
+ - scoped session selection from `opencode session list`
154
+ - parsing `opencode export` output to resolve session directories
155
+ - safely identifying and cleaning up forked extraction / auto-dream sessions
156
+
157
+ Without `python3`, the plugin tools still load, but wrapper maintenance is degraded and fork cleanup is intentionally skipped to avoid deleting the wrong session.
158
+
121
159
  ### Compatibility details
122
160
 
123
161
  The implementation ports core logic from Claude Code for path hashing, git-root/worktree handling, memory format, and memory prompting behavior, so both tools can operate on the same files safely.
@@ -473,22 +473,58 @@ 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"
479
514
  local workdir="$3"
480
515
  local project_dir="$4"
516
+ local allow_existing_fallback="${5:-1}"
481
517
  local after_json
482
518
 
483
519
  after_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || return 1
484
520
 
485
521
  if command -v python3 >/dev/null 2>&1; then
486
- python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" <<'PY'
522
+ python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" "$allow_existing_fallback" <<'PY'
487
523
  import json
488
524
  import os
489
525
  import sys
490
526
 
491
- before_raw, after_raw, started_at_ms_raw, workdir, project_dir = sys.argv[1:6]
527
+ before_raw, after_raw, started_at_ms_raw, workdir, project_dir, allow_existing_fallback_raw = sys.argv[1:7]
492
528
 
493
529
  def parse(raw):
494
530
  try:
@@ -522,6 +558,7 @@ def normalize(path):
522
558
  before = parse(before_raw)
523
559
  after = parse(after_raw)
524
560
  started_at_ms = int(started_at_ms_raw or "0")
561
+ allow_existing_fallback = allow_existing_fallback_raw == "1"
525
562
  before_ids = {item.get("id") for item in before if item.get("id")}
526
563
  workdir = normalize(workdir)
527
564
  project_dir = normalize(project_dir)
@@ -544,11 +581,15 @@ def choose(candidates):
544
581
  new_sessions = [item for item in after if item.get("id") not in before_ids]
545
582
  updated_sessions = [item for item in after if timestamp(item) > started_at_ms]
546
583
 
547
- for pool in (
584
+ candidate_pools = [
548
585
  [item for item in new_sessions if in_scope(item)],
549
586
  [item for item in updated_sessions if in_scope(item)],
550
- [item for item in after if in_scope(item)],
551
- ):
587
+ ]
588
+
589
+ if allow_existing_fallback:
590
+ candidate_pools.append([item for item in after if in_scope(item)])
591
+
592
+ for pool in candidate_pools:
552
593
  if choose(pool):
553
594
  break
554
595
  PY
@@ -795,18 +836,100 @@ main_prompt_requests_ignore_memory() {
795
836
  printf '%s\n' "$joined" | grep -Eq "(ignore|don't use|do not use|without|skip)[[:space:]]+(the[[:space:]]+)?memory|memory[[:space:]]+((should|must)[[:space:]]+be[[:space:]]+)?ignored"
796
837
  }
797
838
 
798
- wait_for_session_target_id() {
839
+ wait_for_scoped_session_id_since() {
799
840
  local before_json="$1"
800
841
  local started_at_ms="$2"
801
- local wait_seconds="${3:-5}"
842
+ local timestamp_file="$3"
843
+ local wait_seconds="${4:-5}"
844
+ local allow_existing_fallback="${5:-1}"
802
845
  local attempt=0
803
846
  local session_id=""
804
847
 
805
848
  while [ "$attempt" -lt "$wait_seconds" ]; do
806
- session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
849
+ session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" "$allow_existing_fallback" || true)
807
850
  if [ -z "$session_id" ]; then
808
- session_id=$(get_scoped_artifact_session_id_since "$TIMESTAMP_FILE" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
851
+ session_id=$(get_scoped_artifact_session_id_since "$timestamp_file" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
852
+ fi
853
+ if [ -n "$session_id" ]; then
854
+ printf '%s\n' "$session_id"
855
+ return 0
809
856
  fi
857
+ sleep 1
858
+ attempt=$((attempt + 1))
859
+ done
860
+
861
+ return 1
862
+ }
863
+
864
+ wait_for_session_target_id() {
865
+ local before_json="$1"
866
+ local started_at_ms="$2"
867
+ local wait_seconds="${3:-5}"
868
+
869
+ wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds"
870
+ }
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)
810
933
  if [ -n "$session_id" ]; then
811
934
  printf '%s\n' "$session_id"
812
935
  return 0
@@ -957,59 +1080,32 @@ rollback_consolidation_lock() {
957
1080
  }
958
1081
 
959
1082
  cleanup_forked_sessions() {
960
- local before_json="$1"
1083
+ local started_at_ms="$1"
1084
+ local parent_title="$2"
961
1085
 
962
- if ! command -v python3 >/dev/null 2>&1; then
1086
+ if [ -z "$started_at_ms" ] || [ -z "$parent_title" ]; then
963
1087
  return 0
964
1088
  fi
965
1089
 
966
- local after_json
967
- after_json=$(get_session_list_json 10 2>/dev/null || true)
968
-
969
- if [ -z "$before_json" ] || [ -z "$after_json" ]; then
1090
+ if ! command -v python3 >/dev/null 2>&1; then
970
1091
  return 0
971
1092
  fi
972
1093
 
973
- local fork_ids
974
- fork_ids=$(python3 - "$before_json" "$after_json" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" <<'PY'
975
- import json
976
- import os
977
- import sys
978
-
979
- def parse(raw):
980
- try:
981
- data = json.loads(raw)
982
- return data if isinstance(data, list) else []
983
- except Exception:
984
- return []
985
-
986
- before_raw, after_raw, workdir, project_dir = sys.argv[1:5]
987
- before = parse(before_raw)
988
- after = parse(after_raw)
1094
+ if ! python3 - <<'PY' >/dev/null 2>&1
1095
+ pass
1096
+ PY
1097
+ then
1098
+ return 0
1099
+ fi
989
1100
 
990
- before_ids = {item.get("id") for item in before if item.get("id")}
991
- workdir = os.path.realpath(workdir)
992
- project_dir = os.path.realpath(project_dir)
1101
+ local fork_id
1102
+ fork_id=$(wait_for_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$SESSION_WAIT_SECONDS" || true)
993
1103
 
994
- for item in after:
995
- sid = item.get("id", "")
996
- if not sid or sid in before_ids:
997
- continue
998
- directory = item.get("directory", "")
999
- if not directory:
1000
- continue
1001
- d = os.path.realpath(directory)
1002
- if d in (workdir, project_dir):
1003
- print(sid)
1004
- PY
1005
- ) || return 0
1104
+ [ -n "$fork_id" ] || return 0
1006
1105
 
1007
- while IFS= read -r fork_id; do
1008
- [ -n "$fork_id" ] || continue
1009
- if "$REAL_OPENCODE" session delete "$fork_id" >/dev/null 2>&1; then
1010
- log "Cleaned up forked session $fork_id"
1011
- fi
1012
- done <<< "$fork_ids"
1106
+ if "$REAL_OPENCODE" session delete "$fork_id" >/dev/null 2>&1; then
1107
+ log "Cleaned up forked session $fork_id"
1108
+ fi
1013
1109
  }
1014
1110
 
1015
1111
  session_has_conversation() {
@@ -1050,6 +1146,9 @@ run_extraction_if_needed() {
1050
1146
  log "Extracting memories from session $session_id..."
1051
1147
  log "Extraction log: $EXTRACT_LOG_FILE"
1052
1148
 
1149
+ local parent_session_title
1150
+ parent_session_title=$(get_session_title_from_db "$session_id" || true)
1151
+
1053
1152
  local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
1054
1153
  if [ -n "$EXTRACT_MODEL" ]; then
1055
1154
  cmd+=(-m "$EXTRACT_MODEL")
@@ -1059,8 +1158,8 @@ run_extraction_if_needed() {
1059
1158
  fi
1060
1159
  cmd+=("$EXTRACT_PROMPT")
1061
1160
 
1062
- local pre_fork_json
1063
- pre_fork_json=$(get_session_list_json 5 2>/dev/null || true)
1161
+ local fork_started_at_ms
1162
+ fork_started_at_ms=$(( $(date +%s) * 1000 ))
1064
1163
 
1065
1164
  if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
1066
1165
  log "Memory extraction completed successfully"
@@ -1069,7 +1168,7 @@ run_extraction_if_needed() {
1069
1168
  log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
1070
1169
  fi
1071
1170
 
1072
- cleanup_forked_sessions "$pre_fork_json"
1171
+ cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
1073
1172
  release_simple_lock "$EXTRACT_LOCK_FILE"
1074
1173
  }
1075
1174
 
@@ -1112,6 +1211,9 @@ run_autodream_if_needed() {
1112
1211
  log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
1113
1212
  log "Auto-dream log: $AUTODREAM_LOG_FILE"
1114
1213
 
1214
+ local parent_session_title
1215
+ parent_session_title=$(get_session_title_from_db "$session_id" || true)
1216
+
1115
1217
  local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
1116
1218
  if [ -n "$AUTODREAM_MODEL" ]; then
1117
1219
  cmd+=(-m "$AUTODREAM_MODEL")
@@ -1121,8 +1223,8 @@ run_autodream_if_needed() {
1121
1223
  fi
1122
1224
  cmd+=("$AUTODREAM_PROMPT")
1123
1225
 
1124
- local pre_fork_json
1125
- pre_fork_json=$(get_session_list_json 5 2>/dev/null || true)
1226
+ local fork_started_at_ms
1227
+ fork_started_at_ms=$(( $(date +%s) * 1000 ))
1126
1228
 
1127
1229
  if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
1128
1230
  log "Auto-dream consolidation completed successfully"
@@ -1133,7 +1235,7 @@ run_autodream_if_needed() {
1133
1235
  rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
1134
1236
  fi
1135
1237
 
1136
- cleanup_forked_sessions "$pre_fork_json"
1238
+ cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
1137
1239
  }
1138
1240
 
1139
1241
  run_post_session_tasks() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
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",