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 +30 -10
- package/bin/opencode-memory +350 -68
- package/package.json +1 -1
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
|
|
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[
|
|
96
|
-
F --> G[
|
|
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,
|
|
105
|
-
5.
|
|
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
|
|
145
|
-
- `OPENCODE_MEMORY_FOREGROUND` (default `0`): set `1` to run
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/bin/opencode-memory
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# opencode-memory — Wrapper for OpenCode with
|
|
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
|
|
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
|
|
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.
|
|
19
|
-
# 6.
|
|
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
|
|
24
|
-
# - The opencode-memory plugin installed (provides
|
|
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
|
|
28
|
-
# OPENCODE_MEMORY_FOREGROUND=1
|
|
29
|
-
# OPENCODE_MEMORY_MODEL=...
|
|
30
|
-
# OPENCODE_MEMORY_AGENT=...
|
|
31
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
191
|
+
EXTRACT_LOCK_FILE="$LOCK_DIR/${PROJECT_KEY}.extract.lock"
|
|
170
192
|
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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=$(
|
|
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
|
-
|
|
258
|
-
|
|
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 "$
|
|
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
|
|
406
|
+
log "Another $lock_name is already running (PID $lock_pid), skipping"
|
|
263
407
|
return 1
|
|
264
408
|
fi
|
|
265
|
-
|
|
266
|
-
rm -f "$LOCK_FILE"
|
|
409
|
+
rm -f "$lock_file"
|
|
267
410
|
fi
|
|
268
|
-
|
|
411
|
+
|
|
412
|
+
echo $$ > "$lock_file"
|
|
269
413
|
return 0
|
|
270
414
|
}
|
|
271
415
|
|
|
272
|
-
|
|
273
|
-
|
|
416
|
+
release_simple_lock() {
|
|
417
|
+
local lock_file="$1"
|
|
418
|
+
rm -f "$lock_file"
|
|
274
419
|
}
|
|
275
420
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
526
|
+
local code=$?
|
|
527
|
+
log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
|
|
304
528
|
fi
|
|
305
529
|
|
|
306
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
338
|
-
cleanup_timestamp
|
|
339
|
-
exit $opencode_exit
|
|
628
|
+
memory_written_during_session=1
|
|
340
629
|
fi
|
|
341
630
|
|
|
342
|
-
#
|
|
343
|
-
|
|
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
|
|
634
|
+
# Step 5: Run tasks (foreground for debug, background by default)
|
|
349
635
|
if [ "$FOREGROUND" = "1" ]; then
|
|
350
|
-
|
|
351
|
-
run_extraction "$session_id"
|
|
352
|
-
cleanup_timestamp
|
|
636
|
+
run_post_session_tasks "$session_id" "$memory_written_during_session"
|
|
353
637
|
else
|
|
354
|
-
|
|
355
|
-
run_extraction "$session_id" &
|
|
638
|
+
run_post_session_tasks "$session_id" "$memory_written_during_session" &
|
|
356
639
|
disown
|
|
357
|
-
log "
|
|
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