opencode-claude-memory 1.6.1 → 1.6.3

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.
@@ -478,17 +478,18 @@ get_session_target_id() {
478
478
  local started_at_ms="$2"
479
479
  local workdir="$3"
480
480
  local project_dir="$4"
481
+ local allow_existing_fallback="${5:-1}"
481
482
  local after_json
482
483
 
483
484
  after_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || return 1
484
485
 
485
486
  if command -v python3 >/dev/null 2>&1; then
486
- python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" <<'PY'
487
+ python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" "$allow_existing_fallback" <<'PY'
487
488
  import json
488
489
  import os
489
490
  import sys
490
491
 
491
- before_raw, after_raw, started_at_ms_raw, workdir, project_dir = sys.argv[1:6]
492
+ before_raw, after_raw, started_at_ms_raw, workdir, project_dir, allow_existing_fallback_raw = sys.argv[1:7]
492
493
 
493
494
  def parse(raw):
494
495
  try:
@@ -522,6 +523,7 @@ def normalize(path):
522
523
  before = parse(before_raw)
523
524
  after = parse(after_raw)
524
525
  started_at_ms = int(started_at_ms_raw or "0")
526
+ allow_existing_fallback = allow_existing_fallback_raw == "1"
525
527
  before_ids = {item.get("id") for item in before if item.get("id")}
526
528
  workdir = normalize(workdir)
527
529
  project_dir = normalize(project_dir)
@@ -544,11 +546,15 @@ def choose(candidates):
544
546
  new_sessions = [item for item in after if item.get("id") not in before_ids]
545
547
  updated_sessions = [item for item in after if timestamp(item) > started_at_ms]
546
548
 
547
- for pool in (
549
+ candidate_pools = [
548
550
  [item for item in new_sessions if in_scope(item)],
549
551
  [item for item in updated_sessions if in_scope(item)],
550
- [item for item in after if in_scope(item)],
551
- ):
552
+ ]
553
+
554
+ if allow_existing_fallback:
555
+ candidate_pools.append([item for item in after if in_scope(item)])
556
+
557
+ for pool in candidate_pools:
552
558
  if choose(pool):
553
559
  break
554
560
  PY
@@ -795,17 +801,19 @@ main_prompt_requests_ignore_memory() {
795
801
  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
802
  }
797
803
 
798
- wait_for_session_target_id() {
804
+ wait_for_scoped_session_id_since() {
799
805
  local before_json="$1"
800
806
  local started_at_ms="$2"
801
- local wait_seconds="${3:-5}"
807
+ local timestamp_file="$3"
808
+ local wait_seconds="${4:-5}"
809
+ local allow_existing_fallback="${5:-1}"
802
810
  local attempt=0
803
811
  local session_id=""
804
812
 
805
813
  while [ "$attempt" -lt "$wait_seconds" ]; do
806
- session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
814
+ session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" "$allow_existing_fallback" || true)
807
815
  if [ -z "$session_id" ]; then
808
- session_id=$(get_scoped_artifact_session_id_since "$TIMESTAMP_FILE" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
816
+ session_id=$(get_scoped_artifact_session_id_since "$timestamp_file" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
809
817
  fi
810
818
  if [ -n "$session_id" ]; then
811
819
  printf '%s\n' "$session_id"
@@ -818,6 +826,14 @@ wait_for_session_target_id() {
818
826
  return 1
819
827
  }
820
828
 
829
+ wait_for_session_target_id() {
830
+ local before_json="$1"
831
+ local started_at_ms="$2"
832
+ local wait_seconds="${3:-5}"
833
+
834
+ wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds"
835
+ }
836
+
821
837
  file_mtime_secs() {
822
838
  local file="$1"
823
839
  if [ ! -f "$file" ]; then
@@ -956,6 +972,54 @@ rollback_consolidation_lock() {
956
972
  set_file_mtime_secs "$path" "$prior_mtime"
957
973
  }
958
974
 
975
+ cleanup_forked_sessions() {
976
+ local before_json="$1"
977
+ local started_at_ms="$2"
978
+ local timestamp_file="$3"
979
+
980
+ if [ -z "$before_json" ] || [ -z "$started_at_ms" ] || [ -z "$timestamp_file" ]; then
981
+ return 0
982
+ fi
983
+
984
+ if ! command -v python3 >/dev/null 2>&1; then
985
+ return 0
986
+ fi
987
+
988
+ if ! python3 - <<'PY' >/dev/null 2>&1
989
+ pass
990
+ PY
991
+ then
992
+ return 0
993
+ fi
994
+
995
+ local fork_id
996
+ fork_id=$(wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$timestamp_file" "$SESSION_WAIT_SECONDS" 0 || true)
997
+
998
+ [ -n "$fork_id" ] || return 0
999
+
1000
+ if "$REAL_OPENCODE" session delete "$fork_id" >/dev/null 2>&1; then
1001
+ log "Cleaned up forked session $fork_id"
1002
+ fi
1003
+ }
1004
+
1005
+ session_has_conversation() {
1006
+ local session_id="$1"
1007
+ local transcript_file
1008
+ transcript_file="$(get_transcripts_dir)/${session_id}.jsonl"
1009
+
1010
+ if [ -f "$transcript_file" ]; then
1011
+ local line_count
1012
+ line_count=$(wc -l < "$transcript_file")
1013
+ # Empty sessions have at most 1 line (the initial user entry with no content).
1014
+ # Real sessions have 3+ lines (user + tool_use + tool_result at minimum).
1015
+ [ "$line_count" -gt 1 ]
1016
+ return $?
1017
+ fi
1018
+
1019
+ # Transcript not found — assume content exists to avoid skipping a valid session.
1020
+ return 0
1021
+ }
1022
+
959
1023
  run_extraction_if_needed() {
960
1024
  local session_id="$1"
961
1025
  local memory_written_during_session="$2"
@@ -985,6 +1049,13 @@ run_extraction_if_needed() {
985
1049
  fi
986
1050
  cmd+=("$EXTRACT_PROMPT")
987
1051
 
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
+ local fork_started_at_ms
1057
+ fork_started_at_ms=$(( $(date +%s) * 1000 ))
1058
+
988
1059
  if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
989
1060
  log "Memory extraction completed successfully"
990
1061
  else
@@ -992,6 +1063,8 @@ run_extraction_if_needed() {
992
1063
  log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
993
1064
  fi
994
1065
 
1066
+ cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file"
1067
+ rm -f "$fork_timestamp_file"
995
1068
  release_simple_lock "$EXTRACT_LOCK_FILE"
996
1069
  }
997
1070
 
@@ -1043,6 +1116,13 @@ run_autodream_if_needed() {
1043
1116
  fi
1044
1117
  cmd+=("$AUTODREAM_PROMPT")
1045
1118
 
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
+ local fork_started_at_ms
1124
+ fork_started_at_ms=$(( $(date +%s) * 1000 ))
1125
+
1046
1126
  if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
1047
1127
  log "Auto-dream consolidation completed successfully"
1048
1128
  # Keep lock mtime at "now" to represent last consolidated timestamp.
@@ -1050,8 +1130,10 @@ run_autodream_if_needed() {
1050
1130
  local code=$?
1051
1131
  log "Auto-dream consolidation failed (exit code $code). Rolling back gate timestamp"
1052
1132
  rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
1053
- return 0
1054
1133
  fi
1134
+
1135
+ cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file"
1136
+ rm -f "$fork_timestamp_file"
1055
1137
  }
1056
1138
 
1057
1139
  run_post_session_tasks() {
@@ -1093,6 +1175,13 @@ if [ -z "$session_id" ]; then
1093
1175
  exit $opencode_exit
1094
1176
  fi
1095
1177
 
1178
+ # Step 3.5: Skip if session had no real conversation (e.g. user opened TUI and exited)
1179
+ if ! session_has_conversation "$session_id"; then
1180
+ log "Session $session_id has no conversation, skipping post-session memory maintenance"
1181
+ cleanup_timestamp
1182
+ exit $opencode_exit
1183
+ fi
1184
+
1096
1185
  # Step 4: Check whether main session already wrote memory files
1097
1186
  memory_written_during_session=0
1098
1187
  if has_new_memories; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
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",