opencode-claude-memory 1.2.0 → 1.3.0

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,43 @@ 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
180
+ # Scope lock files at project root granularity (not per-subdirectory).
181
+ PROJECT_SCOPE_DIR="$WORKING_DIR"
182
+ if git -C "$WORKING_DIR" rev-parse --show-toplevel >/dev/null 2>&1; then
183
+ PROJECT_SCOPE_DIR="$(git -C "$WORKING_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$WORKING_DIR")"
184
+ fi
185
+
186
+ PROJECT_KEY="$(printf '%s' "$PROJECT_SCOPE_DIR" | cksum | awk '{print $1}')"
187
+
188
+ # Lock files (prevent concurrent work on the same project)
167
189
  LOCK_DIR="${TMPDIR:-/tmp}/opencode-memory-locks"
168
190
  mkdir -p "$LOCK_DIR"
169
- LOCK_FILE="$LOCK_DIR/$(echo "$WORKING_DIR" | sed 's/[^a-zA-Z0-9]/-/g').lock"
191
+ EXTRACT_LOCK_FILE="$LOCK_DIR/${PROJECT_KEY}.extract.lock"
170
192
 
171
- # Log file for background extraction
193
+ STATE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/opencode-memory"
194
+ mkdir -p "$STATE_DIR"
195
+ CONSOLIDATION_LOCK_FILE="$STATE_DIR/${PROJECT_KEY}.consolidate-lock"
196
+
197
+ # Logs
172
198
  LOG_DIR="${TMPDIR:-/tmp}/opencode-memory-logs"
173
199
  mkdir -p "$LOG_DIR"
174
- LOG_FILE="$LOG_DIR/extract-$(date +%Y%m%d-%H%M%S).log"
200
+ TASK_LOG_PREFIX="$(date +%Y%m%d-%H%M%S)-${PROJECT_KEY}"
201
+ EXTRACT_LOG_FILE="$LOG_DIR/extract-${TASK_LOG_PREFIX}.log"
202
+ AUTODREAM_LOG_FILE="$LOG_DIR/dream-${TASK_LOG_PREFIX}.log"
175
203
 
176
204
  # ============================================================================
177
- # Extraction Prompt
205
+ # Prompts
178
206
  # ============================================================================
179
207
 
180
208
  # Adapted from Claude Code's extraction prompt, simplified for OpenCode's
@@ -217,6 +245,44 @@ For each memory worth saving, call `memory_save` with:
217
245
  5. Be selective: 0-3 memories per session is typical. Quality over quantity.
218
246
  6. Do NOT save a memory about the extraction process itself.'
219
247
 
248
+ # Periodic memory consolidation inspired by Claude Code auto-dream.
249
+ AUTODREAM_PROMPT="$(cat <<'EOF'
250
+ You are performing an auto-dream memory consolidation pass.
251
+
252
+ Goal: tighten and de-duplicate memory files so future sessions can orient faster.
253
+
254
+ ## Available tools
255
+ - memory_list
256
+ - memory_search
257
+ - memory_read
258
+ - memory_save
259
+ - memory_delete
260
+
261
+ ## Phase 1 — Orient
262
+ 1. Use memory_list to inspect current memory inventory.
263
+ 2. Identify overlapping or stale entries that can be merged/updated/deleted.
264
+
265
+ ## Phase 2 — Consolidate
266
+ 1. Merge duplicates into a single stronger memory using memory_save.
267
+ 2. Rewrite vague descriptions so retrieval is easier and more precise.
268
+ 3. For feedback/project entries, ensure content is structured as:
269
+ - main rule/fact
270
+ - **Why:**
271
+ - **How to apply:**
272
+
273
+ ## Phase 3 — Prune
274
+ 1. Delete memories that are clearly obsolete, contradictory, or low-value.
275
+ 2. Keep total memory set concise and high signal.
276
+
277
+ ## Guardrails
278
+ - Do NOT invent facts.
279
+ - If confidence is low, keep existing memory instead of guessing.
280
+ - If memory quality is already strong, make no changes and explicitly say so.
281
+
282
+ Return a short summary of what you updated, merged, or removed.
283
+ EOF
284
+ )"
285
+
220
286
  # ============================================================================
221
287
  # Helper Functions
222
288
  # ============================================================================
@@ -225,85 +291,310 @@ log() {
225
291
  echo "[opencode-memory] $*" >&2
226
292
  }
227
293
 
294
+ is_positive_int() {
295
+ [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -gt 0 ]
296
+ }
297
+
298
+ if ! is_positive_int "$AUTODREAM_MIN_HOURS"; then
299
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=$AUTODREAM_MIN_HOURS, using default 24"
300
+ AUTODREAM_MIN_HOURS=24
301
+ fi
302
+ if ! is_positive_int "$AUTODREAM_MIN_SESSIONS"; then
303
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=$AUTODREAM_MIN_SESSIONS, using default 5"
304
+ AUTODREAM_MIN_SESSIONS=5
305
+ fi
306
+ if ! is_positive_int "$AUTODREAM_SCAN_LIMIT"; then
307
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT=$AUTODREAM_SCAN_LIMIT, using default 200"
308
+ AUTODREAM_SCAN_LIMIT=200
309
+ fi
310
+
228
311
  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
312
+ # Check if any memory file was modified during the session.
231
313
  local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
232
-
233
314
  if [ ! -d "$mem_base" ]; then
234
315
  return 1
235
316
  fi
236
317
 
237
- # Find any .md file under projects/*/memory/ newer than our timestamp
238
318
  local newer_files
239
319
  newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1)
240
-
241
320
  [ -n "$newer_files" ]
242
321
  }
243
322
 
323
+ cleanup_timestamp() {
324
+ rm -f "$TIMESTAMP_FILE"
325
+ }
326
+
327
+ get_session_list_json() {
328
+ local limit="$1"
329
+ local output
330
+
331
+ if output=$("$REAL_OPENCODE" session list --format json -n "$limit" 2>/dev/null); then
332
+ echo "$output"
333
+ return 0
334
+ fi
335
+
336
+ if output=$("$REAL_OPENCODE" session list --format json 2>/dev/null); then
337
+ echo "$output"
338
+ return 0
339
+ fi
340
+
341
+ return 1
342
+ }
343
+
244
344
  get_latest_session_id() {
245
345
  local session_json
246
- session_json=$("$REAL_OPENCODE" session list --format json -n 1 2>/dev/null) || return 1
346
+ session_json=$(get_session_list_json 1) || return 1
247
347
 
248
- # Parse with jq if available, fallback to grep
348
+ # Parse with jq if available, fallback to grep.
249
349
  if command -v jq &>/dev/null; then
250
350
  echo "$session_json" | jq -r '.[0].id // empty'
251
351
  else
252
- # Rough fallback: extract first "id" value
253
352
  echo "$session_json" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'
254
353
  fi
255
354
  }
256
355
 
257
- acquire_lock() {
258
- if [ -f "$LOCK_FILE" ]; then
356
+ file_mtime_secs() {
357
+ local file="$1"
358
+ if [ ! -f "$file" ]; then
359
+ echo 0
360
+ return 0
361
+ fi
362
+
363
+ if stat -c %Y "$file" >/dev/null 2>&1; then
364
+ stat -c %Y "$file"
365
+ return 0
366
+ fi
367
+
368
+ stat -f %m "$file"
369
+ }
370
+
371
+ # Claude-style lock/mtime semantics:
372
+ # - lock file CONTENT (PID) = current holder
373
+ # - lock file MTIME = last successful consolidation timestamp
374
+ read_last_consolidated_at_secs() {
375
+ file_mtime_secs "$CONSOLIDATION_LOCK_FILE"
376
+ }
377
+
378
+ set_file_mtime_secs() {
379
+ local file="$1"
380
+ local secs="$2"
381
+
382
+ if command -v python3 >/dev/null 2>&1; then
383
+ python3 - "$file" "$secs" <<'PY'
384
+ import os
385
+ import sys
386
+
387
+ path = sys.argv[1]
388
+ secs = int(sys.argv[2])
389
+ os.utime(path, (secs, secs))
390
+ PY
391
+ return 0
392
+ fi
393
+
394
+ # Best effort fallback; may not be portable across all environments.
395
+ touch -d "@$secs" "$file" >/dev/null 2>&1 || true
396
+ }
397
+
398
+ acquire_simple_lock() {
399
+ local lock_file="$1"
400
+ local lock_name="$2"
401
+
402
+ if [ -f "$lock_file" ]; then
259
403
  local lock_pid
260
- lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || true)
404
+ lock_pid=$(cat "$lock_file" 2>/dev/null || true)
261
405
  if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
262
- log "Another extraction is already running (PID $lock_pid), skipping"
406
+ log "Another $lock_name is already running (PID $lock_pid), skipping"
263
407
  return 1
264
408
  fi
265
- # Stale lock — remove it
266
- rm -f "$LOCK_FILE"
409
+ rm -f "$lock_file"
267
410
  fi
268
- echo $$ > "$LOCK_FILE"
411
+
412
+ echo $$ > "$lock_file"
269
413
  return 0
270
414
  }
271
415
 
272
- release_lock() {
273
- rm -f "$LOCK_FILE"
416
+ release_simple_lock() {
417
+ local lock_file="$1"
418
+ rm -f "$lock_file"
274
419
  }
275
420
 
276
- cleanup_timestamp() {
277
- rm -f "$TIMESTAMP_FILE"
421
+ count_sessions_touched_since_ms() {
422
+ local since_ms="$1"
423
+ local exclude_session_id="$2"
424
+
425
+ if ! command -v jq >/dev/null 2>&1; then
426
+ echo ""
427
+ return 1
428
+ fi
429
+
430
+ local session_json
431
+ session_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || {
432
+ echo ""
433
+ return 1
434
+ }
435
+
436
+ echo "$session_json" | jq -r --argjson since "$since_ms" --arg exclude "$exclude_session_id" '
437
+ [ .[]
438
+ | select((.id // "") != $exclude)
439
+ | select(((.time.updated // .time.created // 0) | tonumber) > $since)
440
+ ] | length
441
+ '
278
442
  }
279
443
 
280
- run_extraction() {
444
+ CONSOLIDATION_PRIOR_MTIME=0
445
+
446
+ try_acquire_consolidation_lock() {
447
+ local path="$CONSOLIDATION_LOCK_FILE"
448
+ local now mtime holder age
449
+
450
+ # Returns prior mtime via CONSOLIDATION_PRIOR_MTIME so callers can rollback
451
+ # on failure (preserving last successful consolidation time semantics).
452
+
453
+ now=$(date +%s)
454
+ mtime=0
455
+ holder=""
456
+
457
+ if [ -f "$path" ]; then
458
+ mtime=$(file_mtime_secs "$path")
459
+ holder=$(cat "$path" 2>/dev/null || true)
460
+ age=$((now - mtime))
461
+
462
+ if [ "$age" -lt "$AUTODREAM_STALE_LOCK_SECS" ] && [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; then
463
+ log "Auto-dream lock held by live PID $holder (${age}s old), skipping"
464
+ return 1
465
+ fi
466
+ fi
467
+
468
+ mkdir -p "$(dirname "$path")"
469
+ echo $$ > "$path"
470
+
471
+ local verify
472
+ verify=$(cat "$path" 2>/dev/null || true)
473
+ if [ "$verify" != "$$" ]; then
474
+ return 1
475
+ fi
476
+
477
+ CONSOLIDATION_PRIOR_MTIME="$mtime"
478
+ return 0
479
+ }
480
+
481
+ rollback_consolidation_lock() {
482
+ local prior_mtime="$1"
483
+ local path="$CONSOLIDATION_LOCK_FILE"
484
+
485
+ if [ "$prior_mtime" -eq 0 ]; then
486
+ rm -f "$path"
487
+ return 0
488
+ fi
489
+
490
+ : > "$path"
491
+ set_file_mtime_secs "$path" "$prior_mtime"
492
+ }
493
+
494
+ run_extraction_if_needed() {
281
495
  local session_id="$1"
496
+ local memory_written_during_session="$2"
497
+
498
+ if [ "$EXTRACT_ENABLED" = "0" ]; then
499
+ return 0
500
+ fi
501
+
502
+ if [ "$memory_written_during_session" = "1" ]; then
503
+ log "Main agent already wrote memories during session, skipping extraction"
504
+ return 0
505
+ fi
506
+
507
+ if ! acquire_simple_lock "$EXTRACT_LOCK_FILE" "memory extraction"; then
508
+ return 0
509
+ fi
282
510
 
283
511
  log "Extracting memories from session $session_id..."
284
- log "Log file: $LOG_FILE"
512
+ log "Extraction log: $EXTRACT_LOG_FILE"
285
513
 
286
- # Build the opencode run command
287
514
  local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
288
-
289
515
  if [ -n "$EXTRACT_MODEL" ]; then
290
516
  cmd+=(-m "$EXTRACT_MODEL")
291
517
  fi
292
-
293
518
  if [ -n "$EXTRACT_AGENT" ]; then
294
519
  cmd+=(--agent "$EXTRACT_AGENT")
295
520
  fi
296
-
297
521
  cmd+=("$EXTRACT_PROMPT")
298
522
 
299
- # Execute
300
- if "${cmd[@]}" >> "$LOG_FILE" 2>&1; then
523
+ if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
301
524
  log "Memory extraction completed successfully"
302
525
  else
303
- log "Memory extraction failed (exit code $?). Check $LOG_FILE for details"
526
+ local code=$?
527
+ log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
304
528
  fi
305
529
 
306
- release_lock
530
+ release_simple_lock "$EXTRACT_LOCK_FILE"
531
+ }
532
+
533
+ run_autodream_if_needed() {
534
+ local session_id="$1"
535
+
536
+ if [ "$AUTODREAM_ENABLED" = "0" ]; then
537
+ return 0
538
+ fi
539
+
540
+ if ! command -v jq >/dev/null 2>&1; then
541
+ log "jq not found, skipping auto-dream (requires JSON session parsing)"
542
+ return 0
543
+ fi
544
+
545
+ local last_at now hours_since since_ms touched_count
546
+ last_at=$(read_last_consolidated_at_secs)
547
+ now=$(date +%s)
548
+
549
+ hours_since=$(((now - last_at) / 3600))
550
+ if [ "$hours_since" -lt "$AUTODREAM_MIN_HOURS" ]; then
551
+ return 0
552
+ fi
553
+
554
+ since_ms=$((last_at * 1000))
555
+ touched_count=$(count_sessions_touched_since_ms "$since_ms" "$session_id" || true)
556
+ if [ -z "$touched_count" ]; then
557
+ log "Unable to inspect touched sessions, skipping auto-dream"
558
+ return 0
559
+ fi
560
+
561
+ if [ "$touched_count" -lt "$AUTODREAM_MIN_SESSIONS" ]; then
562
+ return 0
563
+ fi
564
+
565
+ if ! try_acquire_consolidation_lock; then
566
+ return 0
567
+ fi
568
+
569
+ log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
570
+ log "Auto-dream log: $AUTODREAM_LOG_FILE"
571
+
572
+ local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
573
+ if [ -n "$AUTODREAM_MODEL" ]; then
574
+ cmd+=(-m "$AUTODREAM_MODEL")
575
+ fi
576
+ if [ -n "$AUTODREAM_AGENT" ]; then
577
+ cmd+=(--agent "$AUTODREAM_AGENT")
578
+ fi
579
+ cmd+=("$AUTODREAM_PROMPT")
580
+
581
+ if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
582
+ log "Auto-dream consolidation completed successfully"
583
+ # Keep lock mtime at "now" to represent last consolidated timestamp.
584
+ else
585
+ local code=$?
586
+ log "Auto-dream consolidation failed (exit code $code). Rolling back gate timestamp"
587
+ rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
588
+ return 0
589
+ fi
590
+ }
591
+
592
+ run_post_session_tasks() {
593
+ local session_id="$1"
594
+ local memory_written_during_session="$2"
595
+
596
+ run_extraction_if_needed "$session_id" "$memory_written_during_session"
597
+ run_autodream_if_needed "$session_id"
307
598
  }
308
599
 
309
600
  # ============================================================================
@@ -317,45 +608,36 @@ TIMESTAMP_FILE=$(mktemp)
317
608
  opencode_exit=0
318
609
  "$REAL_OPENCODE" "$@" || opencode_exit=$?
319
610
 
320
- # Step 2: Check if extraction is enabled
321
- if [ "$EXTRACT_ENABLED" = "0" ]; then
611
+ # Step 2: If no maintenance task is enabled, exit early
612
+ if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
322
613
  cleanup_timestamp
323
614
  exit $opencode_exit
324
615
  fi
325
616
 
326
617
  # Step 3: Get the most recent session ID
327
- session_id=$(get_latest_session_id)
328
-
618
+ session_id=$(get_latest_session_id || true)
329
619
  if [ -z "$session_id" ]; then
330
- log "No session found, skipping memory extraction"
620
+ log "No session found, skipping post-session memory maintenance"
331
621
  cleanup_timestamp
332
622
  exit $opencode_exit
333
623
  fi
334
624
 
335
- # Step 3.5: Check if memories were already written during the session
625
+ # Step 4: Check whether main session already wrote memory files
626
+ memory_written_during_session=0
336
627
  if has_new_memories; then
337
- log "Main agent already wrote memories during session, skipping extraction"
338
- cleanup_timestamp
339
- exit $opencode_exit
628
+ memory_written_during_session=1
340
629
  fi
341
630
 
342
- # Step 4: Acquire lock (prevent concurrent extractions)
343
- if ! acquire_lock; then
344
- cleanup_timestamp
345
- exit $opencode_exit
346
- fi
631
+ # Timestamp file is no longer needed after the check above.
632
+ cleanup_timestamp
347
633
 
348
- # Step 5: Run extraction
634
+ # Step 5: Run tasks (foreground for debug, background by default)
349
635
  if [ "$FOREGROUND" = "1" ]; then
350
- # Foreground mode (for debugging)
351
- run_extraction "$session_id"
352
- cleanup_timestamp
636
+ run_post_session_tasks "$session_id" "$memory_written_during_session"
353
637
  else
354
- # Background mode (default) — user isn't blocked
355
- run_extraction "$session_id" &
638
+ run_post_session_tasks "$session_id" "$memory_written_during_session" &
356
639
  disown
357
- log "Memory extraction started in background (PID $!)"
358
- cleanup_timestamp
640
+ log "Post-session memory maintenance started in background (PID $!)"
359
641
  fi
360
642
 
361
643
  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.0",
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",