opencode-claude-memory 1.7.7 → 2.0.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 +104 -214
- package/dist/agents.d.ts +12 -0
- package/dist/agents.js +69 -0
- package/dist/config.d.ts +112 -0
- package/dist/config.js +79 -0
- package/dist/extraction/ExtractionCoordinator.d.ts +62 -0
- package/dist/extraction/ExtractionCoordinator.js +386 -0
- package/dist/extraction/autodream.d.ts +33 -0
- package/dist/extraction/autodream.js +85 -0
- package/dist/extraction/forkSession.d.ts +37 -0
- package/dist/extraction/forkSession.js +133 -0
- package/dist/extraction/lock.d.ts +29 -0
- package/dist/extraction/lock.js +191 -0
- package/dist/extraction/prompts.d.ts +5 -0
- package/dist/extraction/prompts.js +78 -0
- package/dist/extraction/state.d.ts +38 -0
- package/dist/extraction/state.js +159 -0
- package/dist/hooks/ignore.d.ts +6 -0
- package/dist/hooks/ignore.js +53 -0
- package/dist/hooks/messages.d.ts +14 -0
- package/dist/hooks/messages.js +96 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +50 -665
- package/dist/prompt/sections.d.ts +7 -0
- package/dist/{prompt.js → prompt/sections.js} +16 -72
- package/dist/prompt/systemPrompt.d.ts +7 -0
- package/dist/prompt/systemPrompt.js +64 -0
- package/dist/recall/RecallCoordinator.d.ts +36 -0
- package/dist/recall/RecallCoordinator.js +140 -0
- package/dist/recall/format.d.ts +17 -0
- package/dist/{recall.js → recall/format.js} +13 -36
- package/dist/{recallSelector.d.ts → recall/selector.d.ts} +26 -16
- package/dist/recall/selector.js +92 -0
- package/dist/sdk.d.ts +12 -0
- package/dist/sdk.js +8 -0
- package/dist/store/MemoryStore.d.ts +41 -0
- package/dist/store/MemoryStore.js +105 -0
- package/dist/store/frontmatter.d.ts +25 -0
- package/dist/store/frontmatter.js +61 -0
- package/dist/store/indexFile.d.ts +15 -0
- package/dist/store/indexFile.js +153 -0
- package/dist/{paths.d.ts → store/paths.d.ts} +7 -6
- package/dist/store/paths.js +187 -0
- package/dist/store/scan.d.ts +29 -0
- package/dist/store/scan.js +112 -0
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +142 -0
- package/dist/util/exclusiveFile.d.ts +11 -0
- package/dist/util/exclusiveFile.js +108 -0
- package/dist/util/legacyShellHook.d.ts +3 -0
- package/dist/util/legacyShellHook.js +23 -0
- package/dist/util/log.d.ts +6 -0
- package/dist/util/log.js +29 -0
- package/dist/util/ownedSessions.d.ts +7 -0
- package/dist/util/ownedSessions.js +39 -0
- package/dist/util/timeout.d.ts +5 -0
- package/dist/util/timeout.js +39 -0
- package/package.json +15 -8
- package/bin/opencode-memory +0 -1436
- package/dist/memory.d.ts +0 -31
- package/dist/memory.js +0 -224
- package/dist/memoryScan.d.ts +0 -20
- package/dist/memoryScan.js +0 -106
- package/dist/nativeExtraction.d.ts +0 -9
- package/dist/nativeExtraction.js +0 -80
- package/dist/paths.js +0 -141
- package/dist/prompt.d.ts +0 -4
- package/dist/recall.d.ts +0 -12
- package/dist/recallSelector.js +0 -154
package/bin/opencode-memory
DELETED
|
@@ -1,1436 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# opencode-memory — Wrapper for OpenCode with post-session memory maintenance.
|
|
4
|
-
#
|
|
5
|
-
# Installs a shell hook (function) that intercepts the `opencode` command,
|
|
6
|
-
# then wraps the real binary with post-session extraction and auto-dream.
|
|
7
|
-
#
|
|
8
|
-
# Subcommands:
|
|
9
|
-
# opencode-memory install — Install shell hook to ~/.zshrc or ~/.bashrc
|
|
10
|
-
# opencode-memory uninstall — Remove shell hook
|
|
11
|
-
# opencode-memory self -v — Print the wrapper package version
|
|
12
|
-
# opencode-memory [args...] — Run opencode with post-session memory maintenance
|
|
13
|
-
#
|
|
14
|
-
# How it works:
|
|
15
|
-
# 1. Shell hook defines `opencode()` function that delegates to `opencode-memory`
|
|
16
|
-
# 2. `opencode-memory` finds the real `opencode` binary in PATH
|
|
17
|
-
# 3. Runs it normally with all your arguments
|
|
18
|
-
# 4. After you exit, finds the most recent session
|
|
19
|
-
# 5. Optionally runs memory extraction (conversation -> memories)
|
|
20
|
-
# 6. Optionally runs auto-dream consolidation (memory pruning/merge)
|
|
21
|
-
#
|
|
22
|
-
# Requirements:
|
|
23
|
-
# - Real `opencode` CLI reachable in PATH
|
|
24
|
-
# - `jq` for auto-dream gate/session counting
|
|
25
|
-
# - The opencode-memory plugin installed (provides memory_* tools)
|
|
26
|
-
#
|
|
27
|
-
# Environment variables:
|
|
28
|
-
# OPENCODE_MEMORY_EXTRACT=0 — Disable post-session extraction
|
|
29
|
-
# OPENCODE_MEMORY_FOREGROUND=1 — Run maintenance in foreground (debug)
|
|
30
|
-
# OPENCODE_MEMORY_TERMINAL_LOG=1|0 — Force-enable/disable terminal logs (default: foreground only)
|
|
31
|
-
# OPENCODE_MEMORY_MODEL=... — Extraction model override
|
|
32
|
-
# OPENCODE_MEMORY_AGENT=... — Extraction agent override
|
|
33
|
-
# OPENCODE_MEMORY_AUTODREAM=0 — Disable auto-dream consolidation
|
|
34
|
-
# OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=24 — Min hours between auto-dream runs
|
|
35
|
-
# OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=5 — Min touched sessions since last consolidation
|
|
36
|
-
# OPENCODE_MEMORY_AUTODREAM_MODEL=... — Auto-dream model override
|
|
37
|
-
# OPENCODE_MEMORY_AUTODREAM_AGENT=... — Auto-dream agent override
|
|
38
|
-
# OPENCODE_MEMORY_DIR=... — Override working directory for opencode
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
set -euo pipefail
|
|
42
|
-
|
|
43
|
-
resolve_script_path() {
|
|
44
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
45
|
-
python3 - "${BASH_SOURCE[0]}" <<'PY'
|
|
46
|
-
import os
|
|
47
|
-
import sys
|
|
48
|
-
|
|
49
|
-
print(os.path.realpath(sys.argv[1]))
|
|
50
|
-
PY
|
|
51
|
-
return 0
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
printf '%s\n' "${BASH_SOURCE[0]}"
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
SCRIPT_PATH="$(resolve_script_path)"
|
|
58
|
-
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
|
59
|
-
PACKAGE_JSON="$SCRIPT_DIR/../package.json"
|
|
60
|
-
|
|
61
|
-
resolve_package_json() {
|
|
62
|
-
if [ -f "$PACKAGE_JSON" ]; then
|
|
63
|
-
printf '%s\n' "$PACKAGE_JSON"
|
|
64
|
-
return 0
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
if command -v npm >/dev/null 2>&1; then
|
|
68
|
-
local npm_root
|
|
69
|
-
npm_root=$(npm root -g 2>/dev/null || true)
|
|
70
|
-
if [ -n "$npm_root" ] && [ -f "$npm_root/opencode-claude-memory/package.json" ]; then
|
|
71
|
-
printf '%s\n' "$npm_root/opencode-claude-memory/package.json"
|
|
72
|
-
return 0
|
|
73
|
-
fi
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
printf '%s\n' "$PACKAGE_JSON"
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
print_wrapper_version() {
|
|
80
|
-
local version
|
|
81
|
-
local package_json
|
|
82
|
-
|
|
83
|
-
package_json="$(resolve_package_json)"
|
|
84
|
-
|
|
85
|
-
version=$(awk -F'"' '/^[[:space:]]*"version"[[:space:]]*:/ { print $4; exit }' "$package_json")
|
|
86
|
-
|
|
87
|
-
if [ -z "$version" ]; then
|
|
88
|
-
echo "[opencode-memory] ERROR: Cannot read package version from $package_json" >&2
|
|
89
|
-
exit 1
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
printf '%s\n' "$version"
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
# ============================================================================
|
|
96
|
-
# Shell Hook Management
|
|
97
|
-
# ============================================================================
|
|
98
|
-
|
|
99
|
-
HOOK_START_MARKER='# >>> opencode-memory auto-initialization >>>'
|
|
100
|
-
HOOK_END_MARKER='# <<< opencode-memory auto-initialization <<<'
|
|
101
|
-
|
|
102
|
-
detect_shell_rc() {
|
|
103
|
-
local shell_name
|
|
104
|
-
shell_name="$(basename "${SHELL:-}")"
|
|
105
|
-
|
|
106
|
-
case "$shell_name" in
|
|
107
|
-
zsh)
|
|
108
|
-
echo "$HOME/.zshrc"
|
|
109
|
-
;;
|
|
110
|
-
bash)
|
|
111
|
-
echo "$HOME/.bashrc"
|
|
112
|
-
;;
|
|
113
|
-
*)
|
|
114
|
-
if [ -f "$HOME/.zshrc" ]; then
|
|
115
|
-
echo "$HOME/.zshrc"
|
|
116
|
-
elif [ -f "$HOME/.bashrc" ]; then
|
|
117
|
-
echo "$HOME/.bashrc"
|
|
118
|
-
else
|
|
119
|
-
echo "$HOME/.zshrc"
|
|
120
|
-
fi
|
|
121
|
-
;;
|
|
122
|
-
esac
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
install_hook() {
|
|
126
|
-
local rc_file
|
|
127
|
-
rc_file=$(detect_shell_rc)
|
|
128
|
-
|
|
129
|
-
if grep -qF "$HOOK_START_MARKER" "$rc_file" 2>/dev/null; then
|
|
130
|
-
echo "[opencode-memory] Hook already installed in $rc_file"
|
|
131
|
-
return 0
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
cat >> "$rc_file" << 'HOOK'
|
|
135
|
-
|
|
136
|
-
# >>> opencode-memory auto-initialization >>>
|
|
137
|
-
opencode() {
|
|
138
|
-
command opencode-memory "$@"
|
|
139
|
-
}
|
|
140
|
-
# <<< opencode-memory auto-initialization <<<
|
|
141
|
-
HOOK
|
|
142
|
-
|
|
143
|
-
echo "[opencode-memory] Shell hook installed in $rc_file"
|
|
144
|
-
echo "[opencode-memory] Restart your shell or run: source $rc_file"
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
remove_hook_from_rc() {
|
|
148
|
-
local rc_file="$1"
|
|
149
|
-
local tmp_file
|
|
150
|
-
tmp_file=$(mktemp)
|
|
151
|
-
|
|
152
|
-
awk -v start="$HOOK_START_MARKER" -v end="$HOOK_END_MARKER" '
|
|
153
|
-
$0 == start { skip=1; next }
|
|
154
|
-
$0 == end { skip=0; next }
|
|
155
|
-
!skip
|
|
156
|
-
' "$rc_file" > "$tmp_file"
|
|
157
|
-
|
|
158
|
-
mv "$tmp_file" "$rc_file"
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
uninstall_hook() {
|
|
162
|
-
local removed=0
|
|
163
|
-
local rc_file
|
|
164
|
-
|
|
165
|
-
for rc_file in "$HOME/.zshrc" "$HOME/.bashrc"; do
|
|
166
|
-
[ -f "$rc_file" ] || continue
|
|
167
|
-
|
|
168
|
-
if grep -qF "$HOOK_START_MARKER" "$rc_file" 2>/dev/null; then
|
|
169
|
-
remove_hook_from_rc "$rc_file"
|
|
170
|
-
echo "[opencode-memory] Shell hook removed from $rc_file"
|
|
171
|
-
removed=1
|
|
172
|
-
fi
|
|
173
|
-
done
|
|
174
|
-
|
|
175
|
-
if [ "$removed" -eq 0 ]; then
|
|
176
|
-
rc_file=$(detect_shell_rc)
|
|
177
|
-
echo "[opencode-memory] Hook not found in $rc_file"
|
|
178
|
-
return 0
|
|
179
|
-
fi
|
|
180
|
-
|
|
181
|
-
echo "[opencode-memory] Restart your shell or run: source <your rc file>"
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
# Handle subcommands before any opencode resolution
|
|
185
|
-
case "${1:-}" in
|
|
186
|
-
install)
|
|
187
|
-
install_hook
|
|
188
|
-
exit 0
|
|
189
|
-
;;
|
|
190
|
-
uninstall)
|
|
191
|
-
uninstall_hook
|
|
192
|
-
exit 0
|
|
193
|
-
;;
|
|
194
|
-
self)
|
|
195
|
-
case "${2:-}" in
|
|
196
|
-
-v|--version|version)
|
|
197
|
-
print_wrapper_version
|
|
198
|
-
exit 0
|
|
199
|
-
;;
|
|
200
|
-
esac
|
|
201
|
-
;;
|
|
202
|
-
esac
|
|
203
|
-
|
|
204
|
-
# ============================================================================
|
|
205
|
-
# Resolve the real opencode binary
|
|
206
|
-
# ============================================================================
|
|
207
|
-
|
|
208
|
-
find_real_opencode() {
|
|
209
|
-
# Since this script is named `opencode-memory` (not `opencode`),
|
|
210
|
-
# `command -v opencode` finds the real binary without ambiguity.
|
|
211
|
-
local real
|
|
212
|
-
real=$(command -v opencode 2>/dev/null) || true
|
|
213
|
-
if [ -z "$real" ] || [ ! -x "$real" ]; then
|
|
214
|
-
echo "[opencode-memory] ERROR: Cannot find opencode binary in PATH" >&2
|
|
215
|
-
echo "[opencode-memory] Make sure opencode is installed: https://opencode.ai" >&2
|
|
216
|
-
exit 1
|
|
217
|
-
fi
|
|
218
|
-
echo "$real"
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
REAL_OPENCODE="$(find_real_opencode)"
|
|
222
|
-
|
|
223
|
-
is_opencode_subcommand() {
|
|
224
|
-
case "$1" in
|
|
225
|
-
completion|acp|mcp|attach|run|debug|providers|auth|agent|upgrade|uninstall|serve|web|models|stats|export|import|github|pr|session|plugin|plug|db)
|
|
226
|
-
return 0
|
|
227
|
-
;;
|
|
228
|
-
*)
|
|
229
|
-
return 1
|
|
230
|
-
;;
|
|
231
|
-
esac
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
extract_working_dir_from_args() {
|
|
235
|
-
local previous=""
|
|
236
|
-
for arg in "$@"; do
|
|
237
|
-
if [ "$previous" = "--dir" ]; then
|
|
238
|
-
printf '%s\n' "$arg"
|
|
239
|
-
return 0
|
|
240
|
-
fi
|
|
241
|
-
previous="$arg"
|
|
242
|
-
done
|
|
243
|
-
|
|
244
|
-
if [ "$#" -gt 0 ] && [ -n "${1:-}" ] && [[ "${1:-}" != -* ]] && ! is_opencode_subcommand "$1" && [ -d "$1" ]; then
|
|
245
|
-
printf '%s\n' "$1"
|
|
246
|
-
return 0
|
|
247
|
-
fi
|
|
248
|
-
|
|
249
|
-
return 1
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
# ============================================================================
|
|
253
|
-
# Configuration
|
|
254
|
-
# ============================================================================
|
|
255
|
-
|
|
256
|
-
EXTRACT_ENABLED="${OPENCODE_MEMORY_EXTRACT:-1}"
|
|
257
|
-
FOREGROUND="${OPENCODE_MEMORY_FOREGROUND:-0}"
|
|
258
|
-
LOG_ENABLED="${OPENCODE_MEMORY_TERMINAL_LOG:-}"
|
|
259
|
-
EXTRACT_MODEL="${OPENCODE_MEMORY_MODEL:-}"
|
|
260
|
-
EXTRACT_AGENT="${OPENCODE_MEMORY_AGENT:-}"
|
|
261
|
-
|
|
262
|
-
AUTODREAM_ENABLED="${OPENCODE_MEMORY_AUTODREAM:-1}"
|
|
263
|
-
AUTODREAM_MIN_HOURS="${OPENCODE_MEMORY_AUTODREAM_MIN_HOURS:-24}"
|
|
264
|
-
AUTODREAM_MIN_SESSIONS="${OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS:-5}"
|
|
265
|
-
AUTODREAM_SCAN_LIMIT="${OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT:-200}"
|
|
266
|
-
AUTODREAM_MODEL="${OPENCODE_MEMORY_AUTODREAM_MODEL:-$EXTRACT_MODEL}"
|
|
267
|
-
AUTODREAM_AGENT="${OPENCODE_MEMORY_AUTODREAM_AGENT:-$EXTRACT_AGENT}"
|
|
268
|
-
AUTODREAM_STALE_LOCK_SECS=$((60 * 60))
|
|
269
|
-
SESSION_WAIT_SECONDS="${OPENCODE_MEMORY_SESSION_WAIT_SECONDS:-5}"
|
|
270
|
-
|
|
271
|
-
WORKING_DIR="${OPENCODE_MEMORY_DIR:-$(extract_working_dir_from_args "$@" || pwd)}"
|
|
272
|
-
|
|
273
|
-
TMP_BASE_DIR="${TMPDIR:-/tmp}"
|
|
274
|
-
while [ "$TMP_BASE_DIR" != "/" ] && [ "${TMP_BASE_DIR%/}" != "$TMP_BASE_DIR" ]; do
|
|
275
|
-
TMP_BASE_DIR="${TMP_BASE_DIR%/}"
|
|
276
|
-
done
|
|
277
|
-
if [ -z "$TMP_BASE_DIR" ]; then
|
|
278
|
-
TMP_BASE_DIR="/"
|
|
279
|
-
fi
|
|
280
|
-
|
|
281
|
-
# Scope lock files at project root granularity (not per-subdirectory).
|
|
282
|
-
PROJECT_SCOPE_DIR="$WORKING_DIR"
|
|
283
|
-
if git -C "$WORKING_DIR" rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
284
|
-
PROJECT_SCOPE_DIR="$(git -C "$WORKING_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$WORKING_DIR")"
|
|
285
|
-
fi
|
|
286
|
-
|
|
287
|
-
PROJECT_KEY="$(printf '%s' "$PROJECT_SCOPE_DIR" | cksum | awk '{print $1}')"
|
|
288
|
-
|
|
289
|
-
# Lock files (prevent concurrent work on the same project)
|
|
290
|
-
LOCK_DIR="$TMP_BASE_DIR/opencode-memory-locks"
|
|
291
|
-
mkdir -p "$LOCK_DIR"
|
|
292
|
-
EXTRACT_LOCK_FILE="$LOCK_DIR/${PROJECT_KEY}.extract.lock"
|
|
293
|
-
|
|
294
|
-
STATE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/opencode-memory"
|
|
295
|
-
mkdir -p "$STATE_DIR"
|
|
296
|
-
CONSOLIDATION_LOCK_FILE="$STATE_DIR/${PROJECT_KEY}.consolidate-lock"
|
|
297
|
-
|
|
298
|
-
# Logs
|
|
299
|
-
LOG_DIR="$TMP_BASE_DIR/opencode-memory-logs"
|
|
300
|
-
mkdir -p "$LOG_DIR"
|
|
301
|
-
TASK_LOG_PREFIX="$(date +%Y%m%d-%H%M%S)-${PROJECT_KEY}"
|
|
302
|
-
EXTRACT_LOG_FILE="$LOG_DIR/extract-${TASK_LOG_PREFIX}.log"
|
|
303
|
-
AUTODREAM_LOG_FILE="$LOG_DIR/dream-${TASK_LOG_PREFIX}.log"
|
|
304
|
-
|
|
305
|
-
# ============================================================================
|
|
306
|
-
# Prompts
|
|
307
|
-
# ============================================================================
|
|
308
|
-
|
|
309
|
-
# Adapted from Claude Code's extraction prompt, simplified for OpenCode's
|
|
310
|
-
# headless run mode. The model sees the full conversation context via --fork.
|
|
311
|
-
EXTRACT_PROMPT='You are now acting as the memory extraction subagent. Review the entire conversation above and extract any information worth remembering for future sessions.
|
|
312
|
-
|
|
313
|
-
## What to save
|
|
314
|
-
|
|
315
|
-
Use the `memory_save` tool to persist memories. There are four types:
|
|
316
|
-
|
|
317
|
-
1. **user** — Who the user is: role, expertise, preferences, communication style. Helps tailor future interactions.
|
|
318
|
-
2. **feedback** — Guidance on how to work: corrections ("don'\''t do X"), confirmations ("yes, keep doing that"), approach preferences. Include *why* so edge cases can be judged.
|
|
319
|
-
3. **project** — Ongoing work context: goals, deadlines, initiatives, decisions, bugs. NOT derivable from code/git. Convert relative dates to absolute.
|
|
320
|
-
4. **reference** — Pointers to external resources: URLs, tool names, where to find information outside the codebase.
|
|
321
|
-
|
|
322
|
-
## What NOT to save
|
|
323
|
-
|
|
324
|
-
- Code patterns, architecture, file structure — derivable from the codebase
|
|
325
|
-
- Git history, recent changes — use `git log`/`git blame`
|
|
326
|
-
- Debugging solutions — the fix is in the code
|
|
327
|
-
- Anything already in AGENTS.md / project config files
|
|
328
|
-
- Ephemeral task details or current conversation context
|
|
329
|
-
- Information that was already saved in a previous extraction
|
|
330
|
-
|
|
331
|
-
## How to save
|
|
332
|
-
|
|
333
|
-
For each memory worth saving, call `memory_save` with:
|
|
334
|
-
- `file_name`: descriptive slug (e.g., `user_role`, `feedback_testing_approach`)
|
|
335
|
-
- `name`: short title
|
|
336
|
-
- `description`: one-line description (used for relevance matching in future sessions)
|
|
337
|
-
- `type`: one of user, feedback, project, reference
|
|
338
|
-
- `content`: the memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines.
|
|
339
|
-
|
|
340
|
-
## Instructions
|
|
341
|
-
|
|
342
|
-
1. Analyze the conversation for memorable information
|
|
343
|
-
2. Check existing memories first (use `memory_list`) to avoid duplicates — update existing ones if needed
|
|
344
|
-
3. Save each distinct memory as a separate entry
|
|
345
|
-
4. If the conversation was trivial (e.g., just "hello" or a quick lookup), save nothing — that'\''s fine
|
|
346
|
-
5. Be selective: 0-3 memories per session is typical. Quality over quantity.
|
|
347
|
-
6. Do NOT save a memory about the extraction process itself.'
|
|
348
|
-
|
|
349
|
-
# Periodic memory consolidation inspired by Claude Code auto-dream.
|
|
350
|
-
AUTODREAM_PROMPT="$(cat <<'EOF'
|
|
351
|
-
You are performing an auto-dream memory consolidation pass.
|
|
352
|
-
|
|
353
|
-
Goal: tighten and de-duplicate memory files so future sessions can orient faster.
|
|
354
|
-
|
|
355
|
-
## Available tools
|
|
356
|
-
- memory_list
|
|
357
|
-
- memory_search
|
|
358
|
-
- memory_read
|
|
359
|
-
- memory_save
|
|
360
|
-
- memory_delete
|
|
361
|
-
|
|
362
|
-
## Phase 1 — Orient
|
|
363
|
-
1. Use memory_list to inspect current memory inventory.
|
|
364
|
-
2. Identify overlapping or stale entries that can be merged/updated/deleted.
|
|
365
|
-
|
|
366
|
-
## Phase 2 — Consolidate
|
|
367
|
-
1. Merge duplicates into a single stronger memory using memory_save.
|
|
368
|
-
2. Rewrite vague descriptions so retrieval is easier and more precise.
|
|
369
|
-
3. For feedback/project entries, ensure content is structured as:
|
|
370
|
-
- main rule/fact
|
|
371
|
-
- **Why:**
|
|
372
|
-
- **How to apply:**
|
|
373
|
-
|
|
374
|
-
## Phase 3 — Prune
|
|
375
|
-
1. Delete memories that are clearly obsolete, contradictory, or low-value.
|
|
376
|
-
2. Keep total memory set concise and high signal.
|
|
377
|
-
|
|
378
|
-
## Guardrails
|
|
379
|
-
- Do NOT invent facts.
|
|
380
|
-
- If confidence is low, keep existing memory instead of guessing.
|
|
381
|
-
- If memory quality is already strong, make no changes and explicitly say so.
|
|
382
|
-
|
|
383
|
-
Return a short summary of what you updated, merged, or removed.
|
|
384
|
-
EOF
|
|
385
|
-
)"
|
|
386
|
-
|
|
387
|
-
# ============================================================================
|
|
388
|
-
# Helper Functions
|
|
389
|
-
# ============================================================================
|
|
390
|
-
|
|
391
|
-
should_log() {
|
|
392
|
-
if [ -n "$LOG_ENABLED" ]; then
|
|
393
|
-
[ "$LOG_ENABLED" = "1" ]
|
|
394
|
-
return $?
|
|
395
|
-
fi
|
|
396
|
-
|
|
397
|
-
[ "$FOREGROUND" = "1" ]
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
log() {
|
|
401
|
-
should_log || return 0
|
|
402
|
-
echo "[opencode-memory] $*" >&2
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
is_positive_int() {
|
|
406
|
-
[[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -gt 0 ]
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
is_bool_flag() {
|
|
410
|
-
[ "$1" = "0" ] || [ "$1" = "1" ]
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if [ -n "$LOG_ENABLED" ] && ! is_bool_flag "$LOG_ENABLED"; then
|
|
414
|
-
echo "[opencode-memory] Invalid OPENCODE_MEMORY_TERMINAL_LOG=$LOG_ENABLED, expected 0 or 1; defaulting to foreground-only logging" >&2
|
|
415
|
-
LOG_ENABLED=""
|
|
416
|
-
fi
|
|
417
|
-
|
|
418
|
-
if ! is_positive_int "$AUTODREAM_MIN_HOURS"; then
|
|
419
|
-
log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=$AUTODREAM_MIN_HOURS, using default 24"
|
|
420
|
-
AUTODREAM_MIN_HOURS=24
|
|
421
|
-
fi
|
|
422
|
-
if ! is_positive_int "$AUTODREAM_MIN_SESSIONS"; then
|
|
423
|
-
log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=$AUTODREAM_MIN_SESSIONS, using default 5"
|
|
424
|
-
AUTODREAM_MIN_SESSIONS=5
|
|
425
|
-
fi
|
|
426
|
-
if ! is_positive_int "$AUTODREAM_SCAN_LIMIT"; then
|
|
427
|
-
log "Invalid OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT=$AUTODREAM_SCAN_LIMIT, using default 200"
|
|
428
|
-
AUTODREAM_SCAN_LIMIT=200
|
|
429
|
-
fi
|
|
430
|
-
|
|
431
|
-
has_new_memories() {
|
|
432
|
-
# Check if any memory file was modified during the session.
|
|
433
|
-
local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
|
|
434
|
-
if [ ! -d "$mem_base" ]; then
|
|
435
|
-
return 1
|
|
436
|
-
fi
|
|
437
|
-
|
|
438
|
-
local newer_files
|
|
439
|
-
newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1)
|
|
440
|
-
[ -n "$newer_files" ]
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
cleanup_timestamp() {
|
|
444
|
-
rm -f "$TIMESTAMP_FILE" "${TRANSCRIPT_CHECKPOINT_FILE:-}"
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
get_session_list_json() {
|
|
448
|
-
local limit="$1"
|
|
449
|
-
local output
|
|
450
|
-
|
|
451
|
-
if output=$("$REAL_OPENCODE" session list --format json -n "$limit" 2>/dev/null); then
|
|
452
|
-
echo "$output"
|
|
453
|
-
return 0
|
|
454
|
-
fi
|
|
455
|
-
|
|
456
|
-
if output=$("$REAL_OPENCODE" session list --format json 2>/dev/null); then
|
|
457
|
-
echo "$output"
|
|
458
|
-
return 0
|
|
459
|
-
fi
|
|
460
|
-
|
|
461
|
-
return 1
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
get_latest_session_id() {
|
|
465
|
-
local session_json
|
|
466
|
-
session_json=$(get_session_list_json 1) || return 1
|
|
467
|
-
|
|
468
|
-
# Parse with jq if available, fallback to grep.
|
|
469
|
-
if command -v jq &>/dev/null; then
|
|
470
|
-
echo "$session_json" | jq -r '.[0].id // empty'
|
|
471
|
-
else
|
|
472
|
-
echo "$session_json" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'
|
|
473
|
-
fi
|
|
474
|
-
}
|
|
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
|
-
|
|
511
|
-
get_session_target_id() {
|
|
512
|
-
local before_json="$1"
|
|
513
|
-
local started_at_ms="$2"
|
|
514
|
-
local workdir="$3"
|
|
515
|
-
local project_dir="$4"
|
|
516
|
-
local allow_existing_fallback="${5:-1}"
|
|
517
|
-
local after_json
|
|
518
|
-
|
|
519
|
-
after_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || return 1
|
|
520
|
-
|
|
521
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
522
|
-
python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" "$allow_existing_fallback" <<'PY'
|
|
523
|
-
import json
|
|
524
|
-
import os
|
|
525
|
-
import sys
|
|
526
|
-
|
|
527
|
-
before_raw, after_raw, started_at_ms_raw, workdir, project_dir, allow_existing_fallback_raw = sys.argv[1:7]
|
|
528
|
-
|
|
529
|
-
def parse(raw):
|
|
530
|
-
try:
|
|
531
|
-
data = json.loads(raw)
|
|
532
|
-
return data if isinstance(data, list) else []
|
|
533
|
-
except Exception:
|
|
534
|
-
return []
|
|
535
|
-
|
|
536
|
-
def timestamp(item):
|
|
537
|
-
time_obj = item.get("time") if isinstance(item.get("time"), dict) else {}
|
|
538
|
-
for key in ("updated", "created"):
|
|
539
|
-
value = item.get(key)
|
|
540
|
-
if value is not None:
|
|
541
|
-
try:
|
|
542
|
-
return int(value)
|
|
543
|
-
except Exception:
|
|
544
|
-
pass
|
|
545
|
-
value = time_obj.get(key)
|
|
546
|
-
if value is not None:
|
|
547
|
-
try:
|
|
548
|
-
return int(value)
|
|
549
|
-
except Exception:
|
|
550
|
-
pass
|
|
551
|
-
return 0
|
|
552
|
-
|
|
553
|
-
def normalize(path):
|
|
554
|
-
if not path:
|
|
555
|
-
return ""
|
|
556
|
-
return os.path.realpath(path)
|
|
557
|
-
|
|
558
|
-
before = parse(before_raw)
|
|
559
|
-
after = parse(after_raw)
|
|
560
|
-
started_at_ms = int(started_at_ms_raw or "0")
|
|
561
|
-
allow_existing_fallback = allow_existing_fallback_raw == "1"
|
|
562
|
-
before_ids = {item.get("id") for item in before if item.get("id")}
|
|
563
|
-
workdir = normalize(workdir)
|
|
564
|
-
project_dir = normalize(project_dir)
|
|
565
|
-
|
|
566
|
-
def in_scope(item):
|
|
567
|
-
directory = normalize(item.get("directory", ""))
|
|
568
|
-
return directory != "" and directory in {workdir, project_dir}
|
|
569
|
-
|
|
570
|
-
def choose(candidates):
|
|
571
|
-
ranked = sorted(
|
|
572
|
-
[item for item in candidates if item.get("id")],
|
|
573
|
-
key=lambda item: timestamp(item),
|
|
574
|
-
reverse=True,
|
|
575
|
-
)
|
|
576
|
-
if ranked:
|
|
577
|
-
print(ranked[0]["id"])
|
|
578
|
-
return True
|
|
579
|
-
return False
|
|
580
|
-
|
|
581
|
-
new_sessions = [item for item in after if item.get("id") not in before_ids]
|
|
582
|
-
updated_sessions = [item for item in after if timestamp(item) > started_at_ms]
|
|
583
|
-
|
|
584
|
-
candidate_pools = [
|
|
585
|
-
[item for item in new_sessions if in_scope(item)],
|
|
586
|
-
[item for item in updated_sessions if in_scope(item)],
|
|
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:
|
|
593
|
-
if choose(pool):
|
|
594
|
-
break
|
|
595
|
-
PY
|
|
596
|
-
return 0
|
|
597
|
-
fi
|
|
598
|
-
|
|
599
|
-
get_latest_session_id
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
get_transcripts_dir() {
|
|
603
|
-
printf '%s\n' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/transcripts"
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
get_storage_session_diff_dir() {
|
|
607
|
-
printf '%s\n' "$HOME/.local/share/opencode/storage/session_diff"
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
resolve_session_directory() {
|
|
611
|
-
local session_id="$1"
|
|
612
|
-
local output_file
|
|
613
|
-
|
|
614
|
-
output_file=$(mktemp)
|
|
615
|
-
if ! "$REAL_OPENCODE" export "$session_id" >"$output_file" 2>/dev/null; then
|
|
616
|
-
rm -f "$output_file"
|
|
617
|
-
return 1
|
|
618
|
-
fi
|
|
619
|
-
|
|
620
|
-
if [ ! -s "$output_file" ]; then
|
|
621
|
-
rm -f "$output_file"
|
|
622
|
-
return 1
|
|
623
|
-
fi
|
|
624
|
-
|
|
625
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
626
|
-
python3 - "$output_file" <<'PY'
|
|
627
|
-
import json
|
|
628
|
-
from pathlib import Path
|
|
629
|
-
import sys
|
|
630
|
-
|
|
631
|
-
raw = Path(sys.argv[1]).read_text()
|
|
632
|
-
start = raw.find('{')
|
|
633
|
-
if start == -1:
|
|
634
|
-
raise SystemExit(1)
|
|
635
|
-
|
|
636
|
-
try:
|
|
637
|
-
data = json.loads(raw[start:])
|
|
638
|
-
except Exception:
|
|
639
|
-
raise SystemExit(1)
|
|
640
|
-
|
|
641
|
-
directory = ((data.get("info") or {}).get("directory") or "")
|
|
642
|
-
if directory:
|
|
643
|
-
print(directory)
|
|
644
|
-
PY
|
|
645
|
-
local status=$?
|
|
646
|
-
rm -f "$output_file"
|
|
647
|
-
return $status
|
|
648
|
-
fi
|
|
649
|
-
|
|
650
|
-
rm -f "$output_file"
|
|
651
|
-
return 1
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
get_scoped_artifact_session_id_since() {
|
|
655
|
-
local timestamp_file="$1"
|
|
656
|
-
local workdir="$2"
|
|
657
|
-
local project_dir="$3"
|
|
658
|
-
|
|
659
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
660
|
-
return 1
|
|
661
|
-
fi
|
|
662
|
-
|
|
663
|
-
local candidates
|
|
664
|
-
candidates=$(python3 - "$timestamp_file" "$HOME" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" <<'PY'
|
|
665
|
-
import os
|
|
666
|
-
import sys
|
|
667
|
-
|
|
668
|
-
timestamp_file, home_dir, claude_dir = sys.argv[1:4]
|
|
669
|
-
|
|
670
|
-
try:
|
|
671
|
-
threshold = os.path.getmtime(timestamp_file)
|
|
672
|
-
except OSError:
|
|
673
|
-
raise SystemExit(1)
|
|
674
|
-
|
|
675
|
-
sources = [
|
|
676
|
-
(os.path.join(home_dir, '.local', 'share', 'opencode', 'storage', 'session_diff'), '.json'),
|
|
677
|
-
(os.path.join(claude_dir, 'transcripts'), '.jsonl'),
|
|
678
|
-
]
|
|
679
|
-
|
|
680
|
-
latest = {}
|
|
681
|
-
for directory, suffix in sources:
|
|
682
|
-
if not os.path.isdir(directory):
|
|
683
|
-
continue
|
|
684
|
-
for entry in os.scandir(directory):
|
|
685
|
-
if not entry.is_file() or not entry.name.endswith(suffix):
|
|
686
|
-
continue
|
|
687
|
-
try:
|
|
688
|
-
mtime = entry.stat().st_mtime
|
|
689
|
-
except OSError:
|
|
690
|
-
continue
|
|
691
|
-
if mtime <= threshold:
|
|
692
|
-
continue
|
|
693
|
-
session_id = entry.name[:-len(suffix)]
|
|
694
|
-
latest[session_id] = max(latest.get(session_id, -1), mtime)
|
|
695
|
-
|
|
696
|
-
for session_id, mtime in sorted(latest.items(), key=lambda item: item[1], reverse=True):
|
|
697
|
-
print(session_id)
|
|
698
|
-
PY
|
|
699
|
-
) || return 1
|
|
700
|
-
|
|
701
|
-
local session_id=""
|
|
702
|
-
local session_dir=""
|
|
703
|
-
local normalized_session_dir=""
|
|
704
|
-
local normalized_workdir=""
|
|
705
|
-
local normalized_project_dir=""
|
|
706
|
-
normalized_workdir=$(python3 - "$workdir" <<'PY'
|
|
707
|
-
import os, sys
|
|
708
|
-
print(os.path.realpath(sys.argv[1]))
|
|
709
|
-
PY
|
|
710
|
-
)
|
|
711
|
-
normalized_project_dir=$(python3 - "$project_dir" <<'PY'
|
|
712
|
-
import os, sys
|
|
713
|
-
print(os.path.realpath(sys.argv[1]))
|
|
714
|
-
PY
|
|
715
|
-
)
|
|
716
|
-
|
|
717
|
-
while IFS= read -r session_id; do
|
|
718
|
-
[ -n "$session_id" ] || continue
|
|
719
|
-
session_dir=$(resolve_session_directory "$session_id" || true)
|
|
720
|
-
[ -n "$session_dir" ] || continue
|
|
721
|
-
normalized_session_dir=$(python3 - "$session_dir" <<'PY'
|
|
722
|
-
import os, sys
|
|
723
|
-
print(os.path.realpath(sys.argv[1]))
|
|
724
|
-
PY
|
|
725
|
-
)
|
|
726
|
-
if [ "$normalized_session_dir" = "$normalized_workdir" ] || [ "$normalized_session_dir" = "$normalized_project_dir" ]; then
|
|
727
|
-
printf '%s\n' "$session_id"
|
|
728
|
-
return 0
|
|
729
|
-
fi
|
|
730
|
-
done <<EOF
|
|
731
|
-
$candidates
|
|
732
|
-
EOF
|
|
733
|
-
|
|
734
|
-
return 1
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
get_latest_storage_session_id_since() {
|
|
738
|
-
local timestamp_file="$1"
|
|
739
|
-
local storage_dir
|
|
740
|
-
storage_dir=$(get_storage_session_diff_dir)
|
|
741
|
-
|
|
742
|
-
if [ ! -d "$storage_dir" ]; then
|
|
743
|
-
return 1
|
|
744
|
-
fi
|
|
745
|
-
|
|
746
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
747
|
-
python3 - "$storage_dir" "$timestamp_file" <<'PY'
|
|
748
|
-
import os
|
|
749
|
-
import sys
|
|
750
|
-
|
|
751
|
-
storage_dir, timestamp_file = sys.argv[1:3]
|
|
752
|
-
|
|
753
|
-
try:
|
|
754
|
-
threshold = os.path.getmtime(timestamp_file)
|
|
755
|
-
except OSError:
|
|
756
|
-
raise SystemExit(1)
|
|
757
|
-
|
|
758
|
-
latest = None
|
|
759
|
-
latest_mtime = -1.0
|
|
760
|
-
|
|
761
|
-
for entry in os.scandir(storage_dir):
|
|
762
|
-
if not entry.is_file() or not entry.name.endswith('.json'):
|
|
763
|
-
continue
|
|
764
|
-
try:
|
|
765
|
-
mtime = entry.stat().st_mtime
|
|
766
|
-
except OSError:
|
|
767
|
-
continue
|
|
768
|
-
if mtime <= threshold:
|
|
769
|
-
continue
|
|
770
|
-
if mtime > latest_mtime:
|
|
771
|
-
latest_mtime = mtime
|
|
772
|
-
latest = entry.name[:-5]
|
|
773
|
-
|
|
774
|
-
if latest:
|
|
775
|
-
print(latest)
|
|
776
|
-
PY
|
|
777
|
-
return 0
|
|
778
|
-
fi
|
|
779
|
-
|
|
780
|
-
return 1
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
get_latest_transcript_session_id_since() {
|
|
784
|
-
local timestamp_file="$1"
|
|
785
|
-
local transcripts_dir
|
|
786
|
-
transcripts_dir=$(get_transcripts_dir)
|
|
787
|
-
|
|
788
|
-
if [ ! -d "$transcripts_dir" ]; then
|
|
789
|
-
return 1
|
|
790
|
-
fi
|
|
791
|
-
|
|
792
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
793
|
-
python3 - "$transcripts_dir" "$timestamp_file" <<'PY'
|
|
794
|
-
import os
|
|
795
|
-
import sys
|
|
796
|
-
|
|
797
|
-
transcripts_dir, timestamp_file = sys.argv[1:3]
|
|
798
|
-
|
|
799
|
-
try:
|
|
800
|
-
threshold = os.path.getmtime(timestamp_file)
|
|
801
|
-
except OSError:
|
|
802
|
-
raise SystemExit(1)
|
|
803
|
-
|
|
804
|
-
latest = None
|
|
805
|
-
latest_mtime = -1.0
|
|
806
|
-
|
|
807
|
-
for entry in os.scandir(transcripts_dir):
|
|
808
|
-
if not entry.is_file() or not entry.name.endswith('.jsonl'):
|
|
809
|
-
continue
|
|
810
|
-
try:
|
|
811
|
-
mtime = entry.stat().st_mtime
|
|
812
|
-
except OSError:
|
|
813
|
-
continue
|
|
814
|
-
if mtime <= threshold:
|
|
815
|
-
continue
|
|
816
|
-
if mtime > latest_mtime:
|
|
817
|
-
latest_mtime = mtime
|
|
818
|
-
latest = entry.name[:-6]
|
|
819
|
-
|
|
820
|
-
if latest:
|
|
821
|
-
print(latest)
|
|
822
|
-
PY
|
|
823
|
-
return 0
|
|
824
|
-
fi
|
|
825
|
-
|
|
826
|
-
return 1
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
main_prompt_requests_ignore_memory() {
|
|
830
|
-
if [ "${1:-}" != "run" ]; then
|
|
831
|
-
return 1
|
|
832
|
-
fi
|
|
833
|
-
|
|
834
|
-
local joined
|
|
835
|
-
joined=$(printf '%s\n' "$*" | tr '[:upper:]' '[:lower:]')
|
|
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"
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
wait_for_scoped_session_id_since() {
|
|
840
|
-
local before_json="$1"
|
|
841
|
-
local started_at_ms="$2"
|
|
842
|
-
local timestamp_file="$3"
|
|
843
|
-
local wait_seconds="${4:-5}"
|
|
844
|
-
local allow_existing_fallback="${5:-1}"
|
|
845
|
-
local attempt=0
|
|
846
|
-
local session_id=""
|
|
847
|
-
|
|
848
|
-
while [ "$attempt" -lt "$wait_seconds" ]; do
|
|
849
|
-
session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" "$allow_existing_fallback" || true)
|
|
850
|
-
if [ -z "$session_id" ]; then
|
|
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
|
|
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)
|
|
933
|
-
if [ -n "$session_id" ]; then
|
|
934
|
-
printf '%s\n' "$session_id"
|
|
935
|
-
return 0
|
|
936
|
-
fi
|
|
937
|
-
sleep 1
|
|
938
|
-
attempt=$((attempt + 1))
|
|
939
|
-
done
|
|
940
|
-
|
|
941
|
-
return 1
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
file_mtime_secs() {
|
|
945
|
-
local file="$1"
|
|
946
|
-
if [ ! -f "$file" ]; then
|
|
947
|
-
echo 0
|
|
948
|
-
return 0
|
|
949
|
-
fi
|
|
950
|
-
|
|
951
|
-
if stat -c %Y "$file" >/dev/null 2>&1; then
|
|
952
|
-
stat -c %Y "$file"
|
|
953
|
-
return 0
|
|
954
|
-
fi
|
|
955
|
-
|
|
956
|
-
stat -f %m "$file"
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
# Claude-style lock/mtime semantics:
|
|
960
|
-
# - lock file CONTENT (PID) = current holder
|
|
961
|
-
# - lock file MTIME = last successful consolidation timestamp
|
|
962
|
-
read_last_consolidated_at_secs() {
|
|
963
|
-
file_mtime_secs "$CONSOLIDATION_LOCK_FILE"
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
set_file_mtime_secs() {
|
|
967
|
-
local file="$1"
|
|
968
|
-
local secs="$2"
|
|
969
|
-
|
|
970
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
971
|
-
python3 - "$file" "$secs" <<'PY'
|
|
972
|
-
import os
|
|
973
|
-
import sys
|
|
974
|
-
|
|
975
|
-
path = sys.argv[1]
|
|
976
|
-
secs = int(sys.argv[2])
|
|
977
|
-
os.utime(path, (secs, secs))
|
|
978
|
-
PY
|
|
979
|
-
return 0
|
|
980
|
-
fi
|
|
981
|
-
|
|
982
|
-
# Best effort fallback; may not be portable across all environments.
|
|
983
|
-
touch -d "@$secs" "$file" >/dev/null 2>&1 || true
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
acquire_simple_lock() {
|
|
987
|
-
local lock_file="$1"
|
|
988
|
-
local lock_name="$2"
|
|
989
|
-
|
|
990
|
-
if [ -f "$lock_file" ]; then
|
|
991
|
-
local lock_pid
|
|
992
|
-
lock_pid=$(cat "$lock_file" 2>/dev/null || true)
|
|
993
|
-
if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
|
|
994
|
-
log "Another $lock_name is already running (PID $lock_pid), skipping"
|
|
995
|
-
return 1
|
|
996
|
-
fi
|
|
997
|
-
rm -f "$lock_file"
|
|
998
|
-
fi
|
|
999
|
-
|
|
1000
|
-
echo $$ > "$lock_file"
|
|
1001
|
-
return 0
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
release_simple_lock() {
|
|
1005
|
-
local lock_file="$1"
|
|
1006
|
-
rm -f "$lock_file"
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
count_sessions_touched_since_ms() {
|
|
1010
|
-
local since_ms="$1"
|
|
1011
|
-
local exclude_session_id="$2"
|
|
1012
|
-
|
|
1013
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
1014
|
-
echo ""
|
|
1015
|
-
return 1
|
|
1016
|
-
fi
|
|
1017
|
-
|
|
1018
|
-
local session_json
|
|
1019
|
-
session_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || {
|
|
1020
|
-
echo ""
|
|
1021
|
-
return 1
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
echo "$session_json" | jq -r --argjson since "$since_ms" --arg exclude "$exclude_session_id" '
|
|
1025
|
-
[ .[]
|
|
1026
|
-
| select((.id // "") != $exclude)
|
|
1027
|
-
| select(((.time.updated // .time.created // 0) | tonumber) > $since)
|
|
1028
|
-
] | length
|
|
1029
|
-
'
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
CONSOLIDATION_PRIOR_MTIME=0
|
|
1033
|
-
|
|
1034
|
-
try_acquire_consolidation_lock() {
|
|
1035
|
-
local path="$CONSOLIDATION_LOCK_FILE"
|
|
1036
|
-
local now mtime holder age
|
|
1037
|
-
|
|
1038
|
-
# Returns prior mtime via CONSOLIDATION_PRIOR_MTIME so callers can rollback
|
|
1039
|
-
# on failure (preserving last successful consolidation time semantics).
|
|
1040
|
-
|
|
1041
|
-
now=$(date +%s)
|
|
1042
|
-
mtime=0
|
|
1043
|
-
holder=""
|
|
1044
|
-
|
|
1045
|
-
if [ -f "$path" ]; then
|
|
1046
|
-
mtime=$(file_mtime_secs "$path")
|
|
1047
|
-
holder=$(cat "$path" 2>/dev/null || true)
|
|
1048
|
-
age=$((now - mtime))
|
|
1049
|
-
|
|
1050
|
-
if [ "$age" -lt "$AUTODREAM_STALE_LOCK_SECS" ] && [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; then
|
|
1051
|
-
log "Auto-dream lock held by live PID $holder (${age}s old), skipping"
|
|
1052
|
-
return 1
|
|
1053
|
-
fi
|
|
1054
|
-
fi
|
|
1055
|
-
|
|
1056
|
-
mkdir -p "$(dirname "$path")"
|
|
1057
|
-
echo $$ > "$path"
|
|
1058
|
-
|
|
1059
|
-
local verify
|
|
1060
|
-
verify=$(cat "$path" 2>/dev/null || true)
|
|
1061
|
-
if [ "$verify" != "$$" ]; then
|
|
1062
|
-
return 1
|
|
1063
|
-
fi
|
|
1064
|
-
|
|
1065
|
-
CONSOLIDATION_PRIOR_MTIME="$mtime"
|
|
1066
|
-
return 0
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
rollback_consolidation_lock() {
|
|
1070
|
-
local prior_mtime="$1"
|
|
1071
|
-
local path="$CONSOLIDATION_LOCK_FILE"
|
|
1072
|
-
|
|
1073
|
-
if [ "$prior_mtime" -eq 0 ]; then
|
|
1074
|
-
rm -f "$path"
|
|
1075
|
-
return 0
|
|
1076
|
-
fi
|
|
1077
|
-
|
|
1078
|
-
: > "$path"
|
|
1079
|
-
set_file_mtime_secs "$path" "$prior_mtime"
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
cleanup_forked_sessions() {
|
|
1083
|
-
local started_at_ms="$1"
|
|
1084
|
-
local parent_title="$2"
|
|
1085
|
-
|
|
1086
|
-
if [ -z "$started_at_ms" ] || [ -z "$parent_title" ]; then
|
|
1087
|
-
return 0
|
|
1088
|
-
fi
|
|
1089
|
-
|
|
1090
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
1091
|
-
return 0
|
|
1092
|
-
fi
|
|
1093
|
-
|
|
1094
|
-
if ! python3 - <<'PY' >/dev/null 2>&1
|
|
1095
|
-
pass
|
|
1096
|
-
PY
|
|
1097
|
-
then
|
|
1098
|
-
return 0
|
|
1099
|
-
fi
|
|
1100
|
-
|
|
1101
|
-
local fork_id
|
|
1102
|
-
fork_id=$(wait_for_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$SESSION_WAIT_SECONDS" || true)
|
|
1103
|
-
|
|
1104
|
-
[ -n "$fork_id" ] || return 0
|
|
1105
|
-
|
|
1106
|
-
if "$REAL_OPENCODE" session delete "$fork_id" >/dev/null 2>&1; then
|
|
1107
|
-
log "Cleaned up forked session $fork_id"
|
|
1108
|
-
fi
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
session_has_conversation() {
|
|
1112
|
-
local session_id="$1"
|
|
1113
|
-
local transcript_file
|
|
1114
|
-
transcript_file="$(get_transcripts_dir)/${session_id}.jsonl"
|
|
1115
|
-
|
|
1116
|
-
if [ -f "$transcript_file" ]; then
|
|
1117
|
-
local line_count
|
|
1118
|
-
line_count=$(wc -l < "$transcript_file")
|
|
1119
|
-
# Empty sessions have at most 1 line (the initial user entry with no content).
|
|
1120
|
-
# Real sessions have 3+ lines (user + tool_use + tool_result at minimum).
|
|
1121
|
-
[ "$line_count" -gt 1 ]
|
|
1122
|
-
return $?
|
|
1123
|
-
fi
|
|
1124
|
-
|
|
1125
|
-
# Transcript not found — assume content exists to avoid skipping a valid session.
|
|
1126
|
-
return 0
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
transcript_fingerprint() {
|
|
1130
|
-
local transcript_file="$1"
|
|
1131
|
-
local stat_output
|
|
1132
|
-
|
|
1133
|
-
if [ ! -f "$transcript_file" ]; then
|
|
1134
|
-
return 1
|
|
1135
|
-
fi
|
|
1136
|
-
|
|
1137
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
1138
|
-
python3 - "$transcript_file" <<'PY'
|
|
1139
|
-
import os
|
|
1140
|
-
import sys
|
|
1141
|
-
|
|
1142
|
-
path = sys.argv[1]
|
|
1143
|
-
|
|
1144
|
-
try:
|
|
1145
|
-
file_stat = os.stat(path)
|
|
1146
|
-
except OSError:
|
|
1147
|
-
raise SystemExit(1)
|
|
1148
|
-
|
|
1149
|
-
mtime = getattr(file_stat, "st_mtime_ns", int(file_stat.st_mtime * 1_000_000_000))
|
|
1150
|
-
print(f"{file_stat.st_size}\t{mtime}")
|
|
1151
|
-
PY
|
|
1152
|
-
return $?
|
|
1153
|
-
fi
|
|
1154
|
-
|
|
1155
|
-
if stat_output=$(stat -f '%z %m' "$transcript_file" 2>/dev/null); then
|
|
1156
|
-
printf '%s\n' "$stat_output"
|
|
1157
|
-
return 0
|
|
1158
|
-
fi
|
|
1159
|
-
|
|
1160
|
-
if stat_output=$(stat -c '%s %Y' "$transcript_file" 2>/dev/null); then
|
|
1161
|
-
printf '%s\n' "$stat_output"
|
|
1162
|
-
return 0
|
|
1163
|
-
fi
|
|
1164
|
-
|
|
1165
|
-
return 1
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
write_transcript_checkpoint() {
|
|
1169
|
-
local output_file="$1"
|
|
1170
|
-
local transcripts_dir
|
|
1171
|
-
transcripts_dir="$(get_transcripts_dir)"
|
|
1172
|
-
|
|
1173
|
-
: > "$output_file"
|
|
1174
|
-
[ -d "$transcripts_dir" ] || return 0
|
|
1175
|
-
|
|
1176
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
1177
|
-
python3 - "$transcripts_dir" > "$output_file" <<'PY'
|
|
1178
|
-
import os
|
|
1179
|
-
import stat
|
|
1180
|
-
import sys
|
|
1181
|
-
|
|
1182
|
-
transcripts_dir = sys.argv[1]
|
|
1183
|
-
|
|
1184
|
-
try:
|
|
1185
|
-
filenames = os.listdir(transcripts_dir)
|
|
1186
|
-
except OSError:
|
|
1187
|
-
raise SystemExit(0)
|
|
1188
|
-
|
|
1189
|
-
for filename in filenames:
|
|
1190
|
-
if not filename.endswith(".jsonl"):
|
|
1191
|
-
continue
|
|
1192
|
-
path = os.path.join(transcripts_dir, filename)
|
|
1193
|
-
try:
|
|
1194
|
-
file_stat = os.stat(path)
|
|
1195
|
-
except OSError:
|
|
1196
|
-
continue
|
|
1197
|
-
if not stat.S_ISREG(file_stat.st_mode):
|
|
1198
|
-
continue
|
|
1199
|
-
session_id = filename[:-6]
|
|
1200
|
-
if not session_id:
|
|
1201
|
-
continue
|
|
1202
|
-
mtime = getattr(file_stat, "st_mtime_ns", int(file_stat.st_mtime * 1_000_000_000))
|
|
1203
|
-
print(f"{session_id}\t{file_stat.st_size}\t{mtime}")
|
|
1204
|
-
PY
|
|
1205
|
-
return 0
|
|
1206
|
-
fi
|
|
1207
|
-
|
|
1208
|
-
find "$transcripts_dir" -maxdepth 1 -type f -name '*.jsonl' -print 2>/dev/null | while IFS= read -r transcript_file; do
|
|
1209
|
-
local filename session_id fingerprint
|
|
1210
|
-
filename="$(basename "$transcript_file")"
|
|
1211
|
-
session_id="${filename%.jsonl}"
|
|
1212
|
-
fingerprint="$(transcript_fingerprint "$transcript_file" || true)"
|
|
1213
|
-
[ -n "$session_id" ] && [ -n "$fingerprint" ] || continue
|
|
1214
|
-
printf '%s\t%s\n' "$session_id" "$fingerprint"
|
|
1215
|
-
done > "$output_file"
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
read_transcript_checkpoint() {
|
|
1219
|
-
local session_id="$1"
|
|
1220
|
-
|
|
1221
|
-
[ -f "$TRANSCRIPT_CHECKPOINT_FILE" ] || return 1
|
|
1222
|
-
awk -F '\t' -v id="$session_id" '$1 == id { print $2 "\t" $3; found=1; exit } END { exit found ? 0 : 1 }' "$TRANSCRIPT_CHECKPOINT_FILE"
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
session_has_incremental_activity() {
|
|
1226
|
-
local session_id="$1"
|
|
1227
|
-
local transcript_file previous current
|
|
1228
|
-
transcript_file="$(get_transcripts_dir)/${session_id}.jsonl"
|
|
1229
|
-
|
|
1230
|
-
if [ -f "$transcript_file" ]; then
|
|
1231
|
-
current="$(transcript_fingerprint "$transcript_file" || true)"
|
|
1232
|
-
previous="$(read_transcript_checkpoint "$session_id" || true)"
|
|
1233
|
-
if [ -z "$previous" ]; then
|
|
1234
|
-
return 0
|
|
1235
|
-
fi
|
|
1236
|
-
[ "$current" != "$previous" ]
|
|
1237
|
-
return $?
|
|
1238
|
-
fi
|
|
1239
|
-
|
|
1240
|
-
# If no transcript is available, keep the existing conservative behavior:
|
|
1241
|
-
# session_diff/session-list discovery may still have found a valid new turn.
|
|
1242
|
-
return 0
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
run_extraction_if_needed() {
|
|
1246
|
-
local session_id="$1"
|
|
1247
|
-
local memory_written_during_session="$2"
|
|
1248
|
-
|
|
1249
|
-
if [ "$EXTRACT_ENABLED" = "0" ]; then
|
|
1250
|
-
return 0
|
|
1251
|
-
fi
|
|
1252
|
-
|
|
1253
|
-
if [ "$memory_written_during_session" = "1" ]; then
|
|
1254
|
-
log "Main agent already wrote memories during session, skipping extraction"
|
|
1255
|
-
return 0
|
|
1256
|
-
fi
|
|
1257
|
-
|
|
1258
|
-
if ! acquire_simple_lock "$EXTRACT_LOCK_FILE" "memory extraction"; then
|
|
1259
|
-
return 0
|
|
1260
|
-
fi
|
|
1261
|
-
|
|
1262
|
-
log "Extracting memories from session $session_id..."
|
|
1263
|
-
log "Extraction log: $EXTRACT_LOG_FILE"
|
|
1264
|
-
|
|
1265
|
-
local parent_session_title
|
|
1266
|
-
parent_session_title=$(get_session_title_from_db "$session_id" || true)
|
|
1267
|
-
|
|
1268
|
-
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
1269
|
-
if [ -n "$EXTRACT_MODEL" ]; then
|
|
1270
|
-
cmd+=(-m "$EXTRACT_MODEL")
|
|
1271
|
-
fi
|
|
1272
|
-
if [ -n "$EXTRACT_AGENT" ]; then
|
|
1273
|
-
cmd+=(--agent "$EXTRACT_AGENT")
|
|
1274
|
-
fi
|
|
1275
|
-
cmd+=("$EXTRACT_PROMPT")
|
|
1276
|
-
|
|
1277
|
-
local fork_started_at_ms
|
|
1278
|
-
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1279
|
-
|
|
1280
|
-
if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
|
|
1281
|
-
log "Memory extraction completed successfully"
|
|
1282
|
-
else
|
|
1283
|
-
local code=$?
|
|
1284
|
-
log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
|
|
1285
|
-
fi
|
|
1286
|
-
|
|
1287
|
-
cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
|
|
1288
|
-
release_simple_lock "$EXTRACT_LOCK_FILE"
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
run_autodream_if_needed() {
|
|
1292
|
-
local session_id="$1"
|
|
1293
|
-
|
|
1294
|
-
if [ "$AUTODREAM_ENABLED" = "0" ]; then
|
|
1295
|
-
return 0
|
|
1296
|
-
fi
|
|
1297
|
-
|
|
1298
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
1299
|
-
log "jq not found, skipping auto-dream (requires JSON session parsing)"
|
|
1300
|
-
return 0
|
|
1301
|
-
fi
|
|
1302
|
-
|
|
1303
|
-
local last_at now hours_since since_ms touched_count
|
|
1304
|
-
last_at=$(read_last_consolidated_at_secs)
|
|
1305
|
-
now=$(date +%s)
|
|
1306
|
-
|
|
1307
|
-
hours_since=$(((now - last_at) / 3600))
|
|
1308
|
-
if [ "$hours_since" -lt "$AUTODREAM_MIN_HOURS" ]; then
|
|
1309
|
-
return 0
|
|
1310
|
-
fi
|
|
1311
|
-
|
|
1312
|
-
since_ms=$((last_at * 1000))
|
|
1313
|
-
touched_count=$(count_sessions_touched_since_ms "$since_ms" "$session_id" || true)
|
|
1314
|
-
if [ -z "$touched_count" ]; then
|
|
1315
|
-
log "Unable to inspect touched sessions, skipping auto-dream"
|
|
1316
|
-
return 0
|
|
1317
|
-
fi
|
|
1318
|
-
|
|
1319
|
-
if [ "$touched_count" -lt "$AUTODREAM_MIN_SESSIONS" ]; then
|
|
1320
|
-
return 0
|
|
1321
|
-
fi
|
|
1322
|
-
|
|
1323
|
-
if ! try_acquire_consolidation_lock; then
|
|
1324
|
-
return 0
|
|
1325
|
-
fi
|
|
1326
|
-
|
|
1327
|
-
log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
|
|
1328
|
-
log "Auto-dream log: $AUTODREAM_LOG_FILE"
|
|
1329
|
-
|
|
1330
|
-
local parent_session_title
|
|
1331
|
-
parent_session_title=$(get_session_title_from_db "$session_id" || true)
|
|
1332
|
-
|
|
1333
|
-
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
1334
|
-
if [ -n "$AUTODREAM_MODEL" ]; then
|
|
1335
|
-
cmd+=(-m "$AUTODREAM_MODEL")
|
|
1336
|
-
fi
|
|
1337
|
-
if [ -n "$AUTODREAM_AGENT" ]; then
|
|
1338
|
-
cmd+=(--agent "$AUTODREAM_AGENT")
|
|
1339
|
-
fi
|
|
1340
|
-
cmd+=("$AUTODREAM_PROMPT")
|
|
1341
|
-
|
|
1342
|
-
local fork_started_at_ms
|
|
1343
|
-
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1344
|
-
|
|
1345
|
-
if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
|
|
1346
|
-
log "Auto-dream consolidation completed successfully"
|
|
1347
|
-
# Keep lock mtime at "now" to represent last consolidated timestamp.
|
|
1348
|
-
else
|
|
1349
|
-
local code=$?
|
|
1350
|
-
log "Auto-dream consolidation failed (exit code $code). Rolling back gate timestamp"
|
|
1351
|
-
rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
|
|
1352
|
-
fi
|
|
1353
|
-
|
|
1354
|
-
cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
run_post_session_tasks() {
|
|
1358
|
-
local session_id="$1"
|
|
1359
|
-
local memory_written_during_session="$2"
|
|
1360
|
-
|
|
1361
|
-
run_extraction_if_needed "$session_id" "$memory_written_during_session"
|
|
1362
|
-
run_autodream_if_needed "$session_id"
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
run_post_session_for_invocation() {
|
|
1366
|
-
local session_id
|
|
1367
|
-
local memory_written_during_session
|
|
1368
|
-
|
|
1369
|
-
# Capture the session ID for this invocation. In background mode this wait
|
|
1370
|
-
# must not delay the visible exit of the wrapped opencode process.
|
|
1371
|
-
session_id=$(wait_for_session_target_id "$PRE_SESSION_JSON" "$SESSION_CAPTURE_STARTED_AT_MS" "$SESSION_WAIT_SECONDS" || true)
|
|
1372
|
-
if [ -z "$session_id" ]; then
|
|
1373
|
-
log "No session found, skipping post-session memory maintenance"
|
|
1374
|
-
cleanup_timestamp
|
|
1375
|
-
return 0
|
|
1376
|
-
fi
|
|
1377
|
-
|
|
1378
|
-
# Skip if session had no real conversation (e.g. user opened TUI and exited).
|
|
1379
|
-
if ! session_has_conversation "$session_id"; then
|
|
1380
|
-
log "Session $session_id has no conversation, skipping post-session memory maintenance"
|
|
1381
|
-
cleanup_timestamp
|
|
1382
|
-
return 0
|
|
1383
|
-
fi
|
|
1384
|
-
|
|
1385
|
-
if ! session_has_incremental_activity "$session_id"; then
|
|
1386
|
-
log "Session $session_id has no new transcript activity, skipping post-session memory maintenance"
|
|
1387
|
-
cleanup_timestamp
|
|
1388
|
-
return 0
|
|
1389
|
-
fi
|
|
1390
|
-
|
|
1391
|
-
memory_written_during_session=0
|
|
1392
|
-
if has_new_memories; then
|
|
1393
|
-
memory_written_during_session=1
|
|
1394
|
-
fi
|
|
1395
|
-
|
|
1396
|
-
# Timestamp file is no longer needed after the check above.
|
|
1397
|
-
cleanup_timestamp
|
|
1398
|
-
|
|
1399
|
-
run_post_session_tasks "$session_id" "$memory_written_during_session"
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
# ============================================================================
|
|
1403
|
-
# Main
|
|
1404
|
-
# ============================================================================
|
|
1405
|
-
|
|
1406
|
-
# Step 0: Create timestamp marker before running opencode
|
|
1407
|
-
TIMESTAMP_FILE=$(mktemp)
|
|
1408
|
-
TRANSCRIPT_CHECKPOINT_FILE=$(mktemp)
|
|
1409
|
-
write_transcript_checkpoint "$TRANSCRIPT_CHECKPOINT_FILE"
|
|
1410
|
-
SESSION_CAPTURE_STARTED_AT_MS=$(( $(date +%s) * 1000 ))
|
|
1411
|
-
PRE_SESSION_JSON=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)
|
|
1412
|
-
|
|
1413
|
-
# Step 1: Run the real opencode with all original arguments, capture exit code
|
|
1414
|
-
opencode_exit=0
|
|
1415
|
-
if main_prompt_requests_ignore_memory "$@"; then
|
|
1416
|
-
OPENCODE_MEMORY_IGNORE=1 "$REAL_OPENCODE" "$@" || opencode_exit=$?
|
|
1417
|
-
else
|
|
1418
|
-
"$REAL_OPENCODE" "$@" || opencode_exit=$?
|
|
1419
|
-
fi
|
|
1420
|
-
|
|
1421
|
-
# Step 2: If no maintenance task is enabled, exit early
|
|
1422
|
-
if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
|
|
1423
|
-
cleanup_timestamp
|
|
1424
|
-
exit $opencode_exit
|
|
1425
|
-
fi
|
|
1426
|
-
|
|
1427
|
-
# Step 3: Run post-session maintenance (foreground for debug, background by default).
|
|
1428
|
-
if [ "$FOREGROUND" = "1" ]; then
|
|
1429
|
-
run_post_session_for_invocation
|
|
1430
|
-
else
|
|
1431
|
-
run_post_session_for_invocation &
|
|
1432
|
-
disown
|
|
1433
|
-
log "Post-session memory maintenance started in background (PID $!)"
|
|
1434
|
-
fi
|
|
1435
|
-
|
|
1436
|
-
exit $opencode_exit
|