opencode-claude-memory 1.2.0 → 1.3.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/README.md CHANGED
@@ -24,6 +24,8 @@ Claude Code writes memory → OpenCode reads it. OpenCode writes memory → Clau
24
24
  Install + enable plugin, then keep using `opencode` as usual.
25
25
  - **Local-first, no migration**
26
26
  Memory stays as local Markdown files in the same directory Claude Code already uses.
27
+ - **Auto-dream consolidation**
28
+ Periodically runs a background memory consolidation pass (Claude-style auto-dream gating).
27
29
 
28
30
  ## 🚀 Quick Start
29
31
 
@@ -36,7 +38,7 @@ opencode-memory install # one-time: installs shell hook
36
38
 
37
39
  This installs:
38
40
  - The **plugin** — memory tools + system prompt injection
39
- - The `opencode-memory` **CLI** — wraps opencode with post-session memory extraction
41
+ - The `opencode-memory` **CLI** — wraps opencode with automatic memory extraction + auto-dream consolidation
40
42
  - A **shell hook** — defines an `opencode()` function in your `.zshrc`/`.bashrc` that delegates to `opencode-memory`
41
43
 
42
44
  ### 2. Configure
@@ -54,7 +56,7 @@ This installs:
54
56
  opencode
55
57
  ```
56
58
 
57
- That’s it. Memory extraction runs in the background after each session.
59
+ That’s it. Memory extraction runs in the background after each session, and auto-dream consolidation is checked with time/session gates.
58
60
 
59
61
  To uninstall:
60
62
 
@@ -92,8 +94,10 @@ graph LR
92
94
  B --> C[opencode-memory finds real binary]
93
95
  C --> D[Runs opencode normally]
94
96
  D --> E[You exit]
95
- E --> F[Fork session + extract memories]
96
- F --> G[Memories saved to ~/.claude/projects/]
97
+ E --> F[Extract memories if needed]
98
+ F --> G[Evaluate auto-dream gate]
99
+ G --> H[Consolidate memories if gate passes]
100
+ H --> I[Memories saved to ~/.claude/projects/]
97
101
  ```
98
102
 
99
103
  The shell hook defines an `opencode()` function that delegates to `opencode-memory`:
@@ -101,8 +105,11 @@ The shell hook defines an `opencode()` function that delegates to `opencode-memo
101
105
  1. Shell function intercepts `opencode` command (higher priority than PATH)
102
106
  2. `opencode-memory` finds the real `opencode` binary in PATH
103
107
  3. Runs it with all your arguments
104
- 4. After you exit, forks the session with a memory extraction prompt
105
- 5. Extraction runs **in the background** you're never blocked
108
+ 4. After you exit, it checks whether the session already wrote memory files
109
+ 5. If needed, it forks the session with a memory extraction prompt
110
+ 6. It evaluates the auto-dream gate (default: at least 24h since last consolidation and 5 touched sessions)
111
+ 7. If the gate passes, it runs a background consolidation pass to merge/prune memories
112
+ 8. Maintenance runs **in the background** unless `OPENCODE_MEMORY_FOREGROUND=1`
106
113
 
107
114
  ### Compatibility details
108
115
 
@@ -137,22 +144,35 @@ File-based memory is transparent, local-first, easy to inspect/diff/back up, and
137
144
 
138
145
  Yes. Set `OPENCODE_MEMORY_EXTRACT=0`.
139
146
 
147
+ ### Can I disable auto-dream?
148
+
149
+ Yes. Set `OPENCODE_MEMORY_AUTODREAM=0`. You can also tune gates with:
150
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_HOURS`
151
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS`
152
+
140
153
  ## 🔧 Configuration
141
154
 
142
155
  ### Environment variables
143
156
 
144
- - `OPENCODE_MEMORY_EXTRACT` (default `1`): set `0` to disable auto-extraction
145
- - `OPENCODE_MEMORY_FOREGROUND` (default `0`): set `1` to run extraction in foreground
157
+ - `OPENCODE_MEMORY_EXTRACT` (default `1`): set `0` to disable automatic memory extraction
158
+ - `OPENCODE_MEMORY_FOREGROUND` (default `0`): set `1` to run maintenance in foreground
146
159
  - `OPENCODE_MEMORY_MODEL`: override model used for extraction
147
160
  - `OPENCODE_MEMORY_AGENT`: override agent used for extraction
161
+ - `OPENCODE_MEMORY_AUTODREAM` (default `1`): set `0` to disable auto-dream consolidation
162
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_HOURS` (default `24`): min hours between consolidation runs
163
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS` (default `5`): min touched sessions since last consolidation
164
+ - `OPENCODE_MEMORY_AUTODREAM_MODEL`: override model used for auto-dream
165
+ - `OPENCODE_MEMORY_AUTODREAM_AGENT`: override agent used for auto-dream
148
166
 
149
167
  ### Logs
150
168
 
151
- Extraction logs are written to `$TMPDIR/opencode-memory-logs/extract-*.log`.
169
+ Logs are written to `$TMPDIR/opencode-memory-logs/`:
170
+ - `extract-*.log`: automatic memory extraction
171
+ - `dream-*.log`: auto-dream consolidation
152
172
 
153
173
  ### Concurrency safety
154
174
 
155
- A file lock prevents multiple extractions from running simultaneously on the same project. Stale locks are cleaned up automatically.
175
+ Lock files prevent concurrent extraction/consolidation runs per project root. Stale locks are cleaned up automatically.
156
176
 
157
177
  ## 📝 Memory format
158
178
 
@@ -1,34 +1,39 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # opencode-memory — Wrapper for OpenCode with automatic memory extraction.
3
+ # opencode-memory — Wrapper for OpenCode with post-session memory maintenance.
4
4
  #
5
5
  # Installs a shell hook (function) that intercepts the `opencode` command,
6
- # then wraps the real binary with post-session memory extraction.
6
+ # then wraps the real binary with post-session extraction and auto-dream.
7
7
  #
8
8
  # Subcommands:
9
9
  # opencode-memory install — Install shell hook to ~/.zshrc or ~/.bashrc
10
10
  # opencode-memory uninstall — Remove shell hook
11
- # opencode-memory [args...] — Run opencode with memory extraction
11
+ # opencode-memory [args...] — Run opencode with post-session memory maintenance
12
12
  #
13
13
  # How it works:
14
14
  # 1. Shell hook defines `opencode()` function that delegates to `opencode-memory`
15
15
  # 2. `opencode-memory` finds the real `opencode` binary in PATH
16
16
  # 3. Runs it normally with all your arguments
17
17
  # 4. After you exit, finds the most recent session
18
- # 5. Forks that session and sends a memory extraction prompt
19
- # 6. The extraction runs in the background so you're not blocked
18
+ # 5. Optionally runs memory extraction (conversation -> memories)
19
+ # 6. Optionally runs auto-dream consolidation (memory pruning/merge)
20
20
  #
21
21
  # Requirements:
22
22
  # - Real `opencode` CLI reachable in PATH
23
- # - `jq` for JSON parsing
24
- # - The opencode-memory plugin installed (provides memory_save tool)
23
+ # - `jq` for auto-dream gate/session counting
24
+ # - The opencode-memory plugin installed (provides memory_* tools)
25
25
  #
26
26
  # Environment variables:
27
- # OPENCODE_MEMORY_EXTRACT=0 — Disable auto-extraction
28
- # OPENCODE_MEMORY_FOREGROUND=1 — Run extraction in foreground (for debugging)
29
- # OPENCODE_MEMORY_MODEL=... Override model for extraction (e.g., "anthropic/claude-sonnet-4-20250514")
30
- # OPENCODE_MEMORY_AGENT=... Override agent for extraction
31
- # OPENCODE_MEMORY_DIR=... Override working directory for opencode
27
+ # OPENCODE_MEMORY_EXTRACT=0 — Disable post-session extraction
28
+ # OPENCODE_MEMORY_FOREGROUND=1 — Run maintenance in foreground (debug)
29
+ # OPENCODE_MEMORY_MODEL=... Extraction model override
30
+ # OPENCODE_MEMORY_AGENT=... Extraction agent override
31
+ # OPENCODE_MEMORY_AUTODREAM=0 Disable auto-dream consolidation
32
+ # OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=24 — Min hours between auto-dream runs
33
+ # OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=5 — Min touched sessions since last consolidation
34
+ # OPENCODE_MEMORY_AUTODREAM_MODEL=... — Auto-dream model override
35
+ # OPENCODE_MEMORY_AUTODREAM_AGENT=... — Auto-dream agent override
36
+ # OPENCODE_MEMORY_DIR=... — Override working directory for opencode
32
37
  #
33
38
 
34
39
  set -euo pipefail
@@ -161,20 +166,51 @@ EXTRACT_ENABLED="${OPENCODE_MEMORY_EXTRACT:-1}"
161
166
  FOREGROUND="${OPENCODE_MEMORY_FOREGROUND:-0}"
162
167
  EXTRACT_MODEL="${OPENCODE_MEMORY_MODEL:-}"
163
168
  EXTRACT_AGENT="${OPENCODE_MEMORY_AGENT:-}"
169
+
170
+ AUTODREAM_ENABLED="${OPENCODE_MEMORY_AUTODREAM:-1}"
171
+ AUTODREAM_MIN_HOURS="${OPENCODE_MEMORY_AUTODREAM_MIN_HOURS:-24}"
172
+ AUTODREAM_MIN_SESSIONS="${OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS:-5}"
173
+ AUTODREAM_SCAN_LIMIT="${OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT:-200}"
174
+ AUTODREAM_MODEL="${OPENCODE_MEMORY_AUTODREAM_MODEL:-$EXTRACT_MODEL}"
175
+ AUTODREAM_AGENT="${OPENCODE_MEMORY_AUTODREAM_AGENT:-$EXTRACT_AGENT}"
176
+ AUTODREAM_STALE_LOCK_SECS=$((60 * 60))
177
+
164
178
  WORKING_DIR="${OPENCODE_MEMORY_DIR:-$(pwd)}"
165
179
 
166
- # Lock file to prevent concurrent extractions on the same project
167
- LOCK_DIR="${TMPDIR:-/tmp}/opencode-memory-locks"
180
+ TMP_BASE_DIR="${TMPDIR:-/tmp}"
181
+ while [ "$TMP_BASE_DIR" != "/" ] && [ "${TMP_BASE_DIR%/}" != "$TMP_BASE_DIR" ]; do
182
+ TMP_BASE_DIR="${TMP_BASE_DIR%/}"
183
+ done
184
+ if [ -z "$TMP_BASE_DIR" ]; then
185
+ TMP_BASE_DIR="/"
186
+ fi
187
+
188
+ # Scope lock files at project root granularity (not per-subdirectory).
189
+ PROJECT_SCOPE_DIR="$WORKING_DIR"
190
+ if git -C "$WORKING_DIR" rev-parse --show-toplevel >/dev/null 2>&1; then
191
+ PROJECT_SCOPE_DIR="$(git -C "$WORKING_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$WORKING_DIR")"
192
+ fi
193
+
194
+ PROJECT_KEY="$(printf '%s' "$PROJECT_SCOPE_DIR" | cksum | awk '{print $1}')"
195
+
196
+ # Lock files (prevent concurrent work on the same project)
197
+ LOCK_DIR="$TMP_BASE_DIR/opencode-memory-locks"
168
198
  mkdir -p "$LOCK_DIR"
169
- LOCK_FILE="$LOCK_DIR/$(echo "$WORKING_DIR" | sed 's/[^a-zA-Z0-9]/-/g').lock"
199
+ EXTRACT_LOCK_FILE="$LOCK_DIR/${PROJECT_KEY}.extract.lock"
170
200
 
171
- # Log file for background extraction
172
- LOG_DIR="${TMPDIR:-/tmp}/opencode-memory-logs"
201
+ STATE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/opencode-memory"
202
+ mkdir -p "$STATE_DIR"
203
+ CONSOLIDATION_LOCK_FILE="$STATE_DIR/${PROJECT_KEY}.consolidate-lock"
204
+
205
+ # Logs
206
+ LOG_DIR="$TMP_BASE_DIR/opencode-memory-logs"
173
207
  mkdir -p "$LOG_DIR"
174
- LOG_FILE="$LOG_DIR/extract-$(date +%Y%m%d-%H%M%S).log"
208
+ TASK_LOG_PREFIX="$(date +%Y%m%d-%H%M%S)-${PROJECT_KEY}"
209
+ EXTRACT_LOG_FILE="$LOG_DIR/extract-${TASK_LOG_PREFIX}.log"
210
+ AUTODREAM_LOG_FILE="$LOG_DIR/dream-${TASK_LOG_PREFIX}.log"
175
211
 
176
212
  # ============================================================================
177
- # Extraction Prompt
213
+ # Prompts
178
214
  # ============================================================================
179
215
 
180
216
  # Adapted from Claude Code's extraction prompt, simplified for OpenCode's
@@ -217,6 +253,44 @@ For each memory worth saving, call `memory_save` with:
217
253
  5. Be selective: 0-3 memories per session is typical. Quality over quantity.
218
254
  6. Do NOT save a memory about the extraction process itself.'
219
255
 
256
+ # Periodic memory consolidation inspired by Claude Code auto-dream.
257
+ AUTODREAM_PROMPT="$(cat <<'EOF'
258
+ You are performing an auto-dream memory consolidation pass.
259
+
260
+ Goal: tighten and de-duplicate memory files so future sessions can orient faster.
261
+
262
+ ## Available tools
263
+ - memory_list
264
+ - memory_search
265
+ - memory_read
266
+ - memory_save
267
+ - memory_delete
268
+
269
+ ## Phase 1 — Orient
270
+ 1. Use memory_list to inspect current memory inventory.
271
+ 2. Identify overlapping or stale entries that can be merged/updated/deleted.
272
+
273
+ ## Phase 2 — Consolidate
274
+ 1. Merge duplicates into a single stronger memory using memory_save.
275
+ 2. Rewrite vague descriptions so retrieval is easier and more precise.
276
+ 3. For feedback/project entries, ensure content is structured as:
277
+ - main rule/fact
278
+ - **Why:**
279
+ - **How to apply:**
280
+
281
+ ## Phase 3 — Prune
282
+ 1. Delete memories that are clearly obsolete, contradictory, or low-value.
283
+ 2. Keep total memory set concise and high signal.
284
+
285
+ ## Guardrails
286
+ - Do NOT invent facts.
287
+ - If confidence is low, keep existing memory instead of guessing.
288
+ - If memory quality is already strong, make no changes and explicitly say so.
289
+
290
+ Return a short summary of what you updated, merged, or removed.
291
+ EOF
292
+ )"
293
+
220
294
  # ============================================================================
221
295
  # Helper Functions
222
296
  # ============================================================================
@@ -225,85 +299,310 @@ log() {
225
299
  echo "[opencode-memory] $*" >&2
226
300
  }
227
301
 
302
+ is_positive_int() {
303
+ [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -gt 0 ]
304
+ }
305
+
306
+ if ! is_positive_int "$AUTODREAM_MIN_HOURS"; then
307
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=$AUTODREAM_MIN_HOURS, using default 24"
308
+ AUTODREAM_MIN_HOURS=24
309
+ fi
310
+ if ! is_positive_int "$AUTODREAM_MIN_SESSIONS"; then
311
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=$AUTODREAM_MIN_SESSIONS, using default 5"
312
+ AUTODREAM_MIN_SESSIONS=5
313
+ fi
314
+ if ! is_positive_int "$AUTODREAM_SCAN_LIMIT"; then
315
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT=$AUTODREAM_SCAN_LIMIT, using default 200"
316
+ AUTODREAM_SCAN_LIMIT=200
317
+ fi
318
+
228
319
  has_new_memories() {
229
- # Check if any memory file was modified during the session
230
- # Checks all projects' memory directories for files newer than the timestamp marker
320
+ # Check if any memory file was modified during the session.
231
321
  local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
232
-
233
322
  if [ ! -d "$mem_base" ]; then
234
323
  return 1
235
324
  fi
236
325
 
237
- # Find any .md file under projects/*/memory/ newer than our timestamp
238
326
  local newer_files
239
327
  newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1)
240
-
241
328
  [ -n "$newer_files" ]
242
329
  }
243
330
 
331
+ cleanup_timestamp() {
332
+ rm -f "$TIMESTAMP_FILE"
333
+ }
334
+
335
+ get_session_list_json() {
336
+ local limit="$1"
337
+ local output
338
+
339
+ if output=$("$REAL_OPENCODE" session list --format json -n "$limit" 2>/dev/null); then
340
+ echo "$output"
341
+ return 0
342
+ fi
343
+
344
+ if output=$("$REAL_OPENCODE" session list --format json 2>/dev/null); then
345
+ echo "$output"
346
+ return 0
347
+ fi
348
+
349
+ return 1
350
+ }
351
+
244
352
  get_latest_session_id() {
245
353
  local session_json
246
- session_json=$("$REAL_OPENCODE" session list --format json -n 1 2>/dev/null) || return 1
354
+ session_json=$(get_session_list_json 1) || return 1
247
355
 
248
- # Parse with jq if available, fallback to grep
356
+ # Parse with jq if available, fallback to grep.
249
357
  if command -v jq &>/dev/null; then
250
358
  echo "$session_json" | jq -r '.[0].id // empty'
251
359
  else
252
- # Rough fallback: extract first "id" value
253
360
  echo "$session_json" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'
254
361
  fi
255
362
  }
256
363
 
257
- acquire_lock() {
258
- if [ -f "$LOCK_FILE" ]; then
364
+ file_mtime_secs() {
365
+ local file="$1"
366
+ if [ ! -f "$file" ]; then
367
+ echo 0
368
+ return 0
369
+ fi
370
+
371
+ if stat -c %Y "$file" >/dev/null 2>&1; then
372
+ stat -c %Y "$file"
373
+ return 0
374
+ fi
375
+
376
+ stat -f %m "$file"
377
+ }
378
+
379
+ # Claude-style lock/mtime semantics:
380
+ # - lock file CONTENT (PID) = current holder
381
+ # - lock file MTIME = last successful consolidation timestamp
382
+ read_last_consolidated_at_secs() {
383
+ file_mtime_secs "$CONSOLIDATION_LOCK_FILE"
384
+ }
385
+
386
+ set_file_mtime_secs() {
387
+ local file="$1"
388
+ local secs="$2"
389
+
390
+ if command -v python3 >/dev/null 2>&1; then
391
+ python3 - "$file" "$secs" <<'PY'
392
+ import os
393
+ import sys
394
+
395
+ path = sys.argv[1]
396
+ secs = int(sys.argv[2])
397
+ os.utime(path, (secs, secs))
398
+ PY
399
+ return 0
400
+ fi
401
+
402
+ # Best effort fallback; may not be portable across all environments.
403
+ touch -d "@$secs" "$file" >/dev/null 2>&1 || true
404
+ }
405
+
406
+ acquire_simple_lock() {
407
+ local lock_file="$1"
408
+ local lock_name="$2"
409
+
410
+ if [ -f "$lock_file" ]; then
259
411
  local lock_pid
260
- lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || true)
412
+ lock_pid=$(cat "$lock_file" 2>/dev/null || true)
261
413
  if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
262
- log "Another extraction is already running (PID $lock_pid), skipping"
414
+ log "Another $lock_name is already running (PID $lock_pid), skipping"
263
415
  return 1
264
416
  fi
265
- # Stale lock — remove it
266
- rm -f "$LOCK_FILE"
417
+ rm -f "$lock_file"
267
418
  fi
268
- echo $$ > "$LOCK_FILE"
419
+
420
+ echo $$ > "$lock_file"
269
421
  return 0
270
422
  }
271
423
 
272
- release_lock() {
273
- rm -f "$LOCK_FILE"
424
+ release_simple_lock() {
425
+ local lock_file="$1"
426
+ rm -f "$lock_file"
274
427
  }
275
428
 
276
- cleanup_timestamp() {
277
- rm -f "$TIMESTAMP_FILE"
429
+ count_sessions_touched_since_ms() {
430
+ local since_ms="$1"
431
+ local exclude_session_id="$2"
432
+
433
+ if ! command -v jq >/dev/null 2>&1; then
434
+ echo ""
435
+ return 1
436
+ fi
437
+
438
+ local session_json
439
+ session_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || {
440
+ echo ""
441
+ return 1
442
+ }
443
+
444
+ echo "$session_json" | jq -r --argjson since "$since_ms" --arg exclude "$exclude_session_id" '
445
+ [ .[]
446
+ | select((.id // "") != $exclude)
447
+ | select(((.time.updated // .time.created // 0) | tonumber) > $since)
448
+ ] | length
449
+ '
278
450
  }
279
451
 
280
- run_extraction() {
452
+ CONSOLIDATION_PRIOR_MTIME=0
453
+
454
+ try_acquire_consolidation_lock() {
455
+ local path="$CONSOLIDATION_LOCK_FILE"
456
+ local now mtime holder age
457
+
458
+ # Returns prior mtime via CONSOLIDATION_PRIOR_MTIME so callers can rollback
459
+ # on failure (preserving last successful consolidation time semantics).
460
+
461
+ now=$(date +%s)
462
+ mtime=0
463
+ holder=""
464
+
465
+ if [ -f "$path" ]; then
466
+ mtime=$(file_mtime_secs "$path")
467
+ holder=$(cat "$path" 2>/dev/null || true)
468
+ age=$((now - mtime))
469
+
470
+ if [ "$age" -lt "$AUTODREAM_STALE_LOCK_SECS" ] && [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; then
471
+ log "Auto-dream lock held by live PID $holder (${age}s old), skipping"
472
+ return 1
473
+ fi
474
+ fi
475
+
476
+ mkdir -p "$(dirname "$path")"
477
+ echo $$ > "$path"
478
+
479
+ local verify
480
+ verify=$(cat "$path" 2>/dev/null || true)
481
+ if [ "$verify" != "$$" ]; then
482
+ return 1
483
+ fi
484
+
485
+ CONSOLIDATION_PRIOR_MTIME="$mtime"
486
+ return 0
487
+ }
488
+
489
+ rollback_consolidation_lock() {
490
+ local prior_mtime="$1"
491
+ local path="$CONSOLIDATION_LOCK_FILE"
492
+
493
+ if [ "$prior_mtime" -eq 0 ]; then
494
+ rm -f "$path"
495
+ return 0
496
+ fi
497
+
498
+ : > "$path"
499
+ set_file_mtime_secs "$path" "$prior_mtime"
500
+ }
501
+
502
+ run_extraction_if_needed() {
281
503
  local session_id="$1"
504
+ local memory_written_during_session="$2"
505
+
506
+ if [ "$EXTRACT_ENABLED" = "0" ]; then
507
+ return 0
508
+ fi
509
+
510
+ if [ "$memory_written_during_session" = "1" ]; then
511
+ log "Main agent already wrote memories during session, skipping extraction"
512
+ return 0
513
+ fi
514
+
515
+ if ! acquire_simple_lock "$EXTRACT_LOCK_FILE" "memory extraction"; then
516
+ return 0
517
+ fi
282
518
 
283
519
  log "Extracting memories from session $session_id..."
284
- log "Log file: $LOG_FILE"
520
+ log "Extraction log: $EXTRACT_LOG_FILE"
285
521
 
286
- # Build the opencode run command
287
522
  local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
288
-
289
523
  if [ -n "$EXTRACT_MODEL" ]; then
290
524
  cmd+=(-m "$EXTRACT_MODEL")
291
525
  fi
292
-
293
526
  if [ -n "$EXTRACT_AGENT" ]; then
294
527
  cmd+=(--agent "$EXTRACT_AGENT")
295
528
  fi
296
-
297
529
  cmd+=("$EXTRACT_PROMPT")
298
530
 
299
- # Execute
300
- if "${cmd[@]}" >> "$LOG_FILE" 2>&1; then
531
+ if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
301
532
  log "Memory extraction completed successfully"
302
533
  else
303
- log "Memory extraction failed (exit code $?). Check $LOG_FILE for details"
534
+ local code=$?
535
+ log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
536
+ fi
537
+
538
+ release_simple_lock "$EXTRACT_LOCK_FILE"
539
+ }
540
+
541
+ run_autodream_if_needed() {
542
+ local session_id="$1"
543
+
544
+ if [ "$AUTODREAM_ENABLED" = "0" ]; then
545
+ return 0
546
+ fi
547
+
548
+ if ! command -v jq >/dev/null 2>&1; then
549
+ log "jq not found, skipping auto-dream (requires JSON session parsing)"
550
+ return 0
551
+ fi
552
+
553
+ local last_at now hours_since since_ms touched_count
554
+ last_at=$(read_last_consolidated_at_secs)
555
+ now=$(date +%s)
556
+
557
+ hours_since=$(((now - last_at) / 3600))
558
+ if [ "$hours_since" -lt "$AUTODREAM_MIN_HOURS" ]; then
559
+ return 0
560
+ fi
561
+
562
+ since_ms=$((last_at * 1000))
563
+ touched_count=$(count_sessions_touched_since_ms "$since_ms" "$session_id" || true)
564
+ if [ -z "$touched_count" ]; then
565
+ log "Unable to inspect touched sessions, skipping auto-dream"
566
+ return 0
567
+ fi
568
+
569
+ if [ "$touched_count" -lt "$AUTODREAM_MIN_SESSIONS" ]; then
570
+ return 0
571
+ fi
572
+
573
+ if ! try_acquire_consolidation_lock; then
574
+ return 0
575
+ fi
576
+
577
+ log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
578
+ log "Auto-dream log: $AUTODREAM_LOG_FILE"
579
+
580
+ local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
581
+ if [ -n "$AUTODREAM_MODEL" ]; then
582
+ cmd+=(-m "$AUTODREAM_MODEL")
583
+ fi
584
+ if [ -n "$AUTODREAM_AGENT" ]; then
585
+ cmd+=(--agent "$AUTODREAM_AGENT")
586
+ fi
587
+ cmd+=("$AUTODREAM_PROMPT")
588
+
589
+ if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
590
+ log "Auto-dream consolidation completed successfully"
591
+ # Keep lock mtime at "now" to represent last consolidated timestamp.
592
+ else
593
+ local code=$?
594
+ log "Auto-dream consolidation failed (exit code $code). Rolling back gate timestamp"
595
+ rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
596
+ return 0
304
597
  fi
598
+ }
599
+
600
+ run_post_session_tasks() {
601
+ local session_id="$1"
602
+ local memory_written_during_session="$2"
305
603
 
306
- release_lock
604
+ run_extraction_if_needed "$session_id" "$memory_written_during_session"
605
+ run_autodream_if_needed "$session_id"
307
606
  }
308
607
 
309
608
  # ============================================================================
@@ -317,45 +616,36 @@ TIMESTAMP_FILE=$(mktemp)
317
616
  opencode_exit=0
318
617
  "$REAL_OPENCODE" "$@" || opencode_exit=$?
319
618
 
320
- # Step 2: Check if extraction is enabled
321
- if [ "$EXTRACT_ENABLED" = "0" ]; then
619
+ # Step 2: If no maintenance task is enabled, exit early
620
+ if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
322
621
  cleanup_timestamp
323
622
  exit $opencode_exit
324
623
  fi
325
624
 
326
625
  # Step 3: Get the most recent session ID
327
- session_id=$(get_latest_session_id)
328
-
626
+ session_id=$(get_latest_session_id || true)
329
627
  if [ -z "$session_id" ]; then
330
- log "No session found, skipping memory extraction"
628
+ log "No session found, skipping post-session memory maintenance"
331
629
  cleanup_timestamp
332
630
  exit $opencode_exit
333
631
  fi
334
632
 
335
- # Step 3.5: Check if memories were already written during the session
633
+ # Step 4: Check whether main session already wrote memory files
634
+ memory_written_during_session=0
336
635
  if has_new_memories; then
337
- log "Main agent already wrote memories during session, skipping extraction"
338
- cleanup_timestamp
339
- exit $opencode_exit
636
+ memory_written_during_session=1
340
637
  fi
341
638
 
342
- # Step 4: Acquire lock (prevent concurrent extractions)
343
- if ! acquire_lock; then
344
- cleanup_timestamp
345
- exit $opencode_exit
346
- fi
639
+ # Timestamp file is no longer needed after the check above.
640
+ cleanup_timestamp
347
641
 
348
- # Step 5: Run extraction
642
+ # Step 5: Run tasks (foreground for debug, background by default)
349
643
  if [ "$FOREGROUND" = "1" ]; then
350
- # Foreground mode (for debugging)
351
- run_extraction "$session_id"
352
- cleanup_timestamp
644
+ run_post_session_tasks "$session_id" "$memory_written_during_session"
353
645
  else
354
- # Background mode (default) — user isn't blocked
355
- run_extraction "$session_id" &
646
+ run_post_session_tasks "$session_id" "$memory_written_during_session" &
356
647
  disown
357
- log "Memory extraction started in background (PID $!)"
358
- cleanup_timestamp
648
+ log "Post-session memory maintenance started in background (PID $!)"
359
649
  fi
360
650
 
361
651
  exit $opencode_exit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "Claude Code-compatible memory compatibility layer for OpenCode — zero config, local-first, no migration",
6
6
  "main": "src/index.ts",