Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Key modules ported from Claude Code's `src/memdir/`:
| Module | Source | Purpose |
|---|---|---|
| `memoryScan.ts` | `memoryScan.ts` | Recursive directory scan + frontmatter header parsing |
| `recall.ts` | `findRelevantMemories.ts` | Memory recall via keyword scoring (heuristic, no LLM side-query) |
| `recall.ts` + `recallSelector.ts` | `findRelevantMemories.ts` | LLM-selected memory recall + selected memory formatting |
| `prompt.ts` | `memoryTypes.ts` + `memdir.ts` | System prompt sections, type taxonomy, truncation |
| `memory.ts` | `memdir.ts` | `truncateEntrypoint()` aligned with `truncateEntrypointContent()` |

Expand Down Expand Up @@ -213,6 +213,8 @@ Yes. Set `OPENCODE_MEMORY_AUTODREAM=0`. You can also tune gates with:
- `OPENCODE_MEMORY_TERMINAL_LOG` (default `foreground-only`): set `1` to force terminal logs on, `0` to force them off
- `OPENCODE_MEMORY_MODEL`: override model used for extraction
- `OPENCODE_MEMORY_AGENT`: override agent used for extraction
- `OPENCODE_MEMORY_RECALL_MODEL`: override model used for LLM memory recall selection
- `OPENCODE_MEMORY_RECALL_AGENT` (default `opencode-memory-recall`): override agent used for LLM memory recall selection
- `OPENCODE_MEMORY_AUTODREAM` (default `1`): set `0` to disable auto-dream consolidation
- `OPENCODE_MEMORY_AUTODREAM_MIN_HOURS` (default `24`): min hours between consolidation runs
- `OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS` (default `5`): min touched sessions since last consolidation
Expand Down
187 changes: 159 additions & 28 deletions bin/opencode-memory
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ has_new_memories() {
}

cleanup_timestamp() {
rm -f "$TIMESTAMP_FILE"
rm -f "$TIMESTAMP_FILE" "${TRANSCRIPT_CHECKPOINT_FILE:-}"
}

get_session_list_json() {
Expand Down Expand Up @@ -1126,6 +1126,122 @@ session_has_conversation() {
return 0
}

transcript_fingerprint() {
local transcript_file="$1"
local stat_output

if [ ! -f "$transcript_file" ]; then
return 1
fi

if command -v python3 >/dev/null 2>&1; then
python3 - "$transcript_file" <<'PY'
import os
import sys

path = sys.argv[1]

try:
file_stat = os.stat(path)
except OSError:
raise SystemExit(1)

mtime = getattr(file_stat, "st_mtime_ns", int(file_stat.st_mtime * 1_000_000_000))
print(f"{file_stat.st_size}\t{mtime}")
PY
return $?
fi

if stat_output=$(stat -f '%z %m' "$transcript_file" 2>/dev/null); then
printf '%s\n' "$stat_output"
return 0
fi

if stat_output=$(stat -c '%s %Y' "$transcript_file" 2>/dev/null); then
printf '%s\n' "$stat_output"
return 0
fi

return 1
}

write_transcript_checkpoint() {
local output_file="$1"
local transcripts_dir
transcripts_dir="$(get_transcripts_dir)"

: > "$output_file"
[ -d "$transcripts_dir" ] || return 0

if command -v python3 >/dev/null 2>&1; then
python3 - "$transcripts_dir" > "$output_file" <<'PY'
import os
import stat
import sys

transcripts_dir = sys.argv[1]

try:
filenames = os.listdir(transcripts_dir)
except OSError:
raise SystemExit(0)

for filename in filenames:
if not filename.endswith(".jsonl"):
continue
path = os.path.join(transcripts_dir, filename)
try:
file_stat = os.stat(path)
except OSError:
continue
if not stat.S_ISREG(file_stat.st_mode):
continue
session_id = filename[:-6]
if not session_id:
continue
mtime = getattr(file_stat, "st_mtime_ns", int(file_stat.st_mtime * 1_000_000_000))
print(f"{session_id}\t{file_stat.st_size}\t{mtime}")
PY
return 0
fi

find "$transcripts_dir" -maxdepth 1 -type f -name '*.jsonl' -print 2>/dev/null | while IFS= read -r transcript_file; do
local filename session_id fingerprint
filename="$(basename "$transcript_file")"
session_id="${filename%.jsonl}"
fingerprint="$(transcript_fingerprint "$transcript_file" || true)"
[ -n "$session_id" ] && [ -n "$fingerprint" ] || continue
printf '%s\t%s\n' "$session_id" "$fingerprint"
done > "$output_file"
}

read_transcript_checkpoint() {
local session_id="$1"

[ -f "$TRANSCRIPT_CHECKPOINT_FILE" ] || return 1
awk -F '\t' -v id="$session_id" '$1 == id { print $2 "\t" $3; found=1; exit } END { exit found ? 0 : 1 }' "$TRANSCRIPT_CHECKPOINT_FILE"
}

session_has_incremental_activity() {
local session_id="$1"
local transcript_file previous current
transcript_file="$(get_transcripts_dir)/${session_id}.jsonl"

if [ -f "$transcript_file" ]; then
current="$(transcript_fingerprint "$transcript_file" || true)"
previous="$(read_transcript_checkpoint "$session_id" || true)"
if [ -z "$previous" ]; then
return 0
fi
[ "$current" != "$previous" ]
return $?
fi

# If no transcript is available, keep the existing conservative behavior:
# session_diff/session-list discovery may still have found a valid new turn.
return 0
}

run_extraction_if_needed() {
local session_id="$1"
local memory_written_during_session="$2"
Expand Down Expand Up @@ -1246,12 +1362,51 @@ run_post_session_tasks() {
run_autodream_if_needed "$session_id"
}

run_post_session_for_invocation() {
local session_id
local memory_written_during_session

# Capture the session ID for this invocation. In background mode this wait
# must not delay the visible exit of the wrapped opencode process.
session_id=$(wait_for_session_target_id "$PRE_SESSION_JSON" "$SESSION_CAPTURE_STARTED_AT_MS" "$SESSION_WAIT_SECONDS" || true)
if [ -z "$session_id" ]; then
log "No session found, skipping post-session memory maintenance"
cleanup_timestamp
return 0
fi

# Skip if session had no real conversation (e.g. user opened TUI and exited).
if ! session_has_conversation "$session_id"; then
log "Session $session_id has no conversation, skipping post-session memory maintenance"
cleanup_timestamp
return 0
fi

if ! session_has_incremental_activity "$session_id"; then
log "Session $session_id has no new transcript activity, skipping post-session memory maintenance"
cleanup_timestamp
return 0
fi

memory_written_during_session=0
if has_new_memories; then
memory_written_during_session=1
fi

# Timestamp file is no longer needed after the check above.
cleanup_timestamp

run_post_session_tasks "$session_id" "$memory_written_during_session"
}

# ============================================================================
# Main
# ============================================================================

# Step 0: Create timestamp marker before running opencode
TIMESTAMP_FILE=$(mktemp)
TRANSCRIPT_CHECKPOINT_FILE=$(mktemp)
write_transcript_checkpoint "$TRANSCRIPT_CHECKPOINT_FILE"
SESSION_CAPTURE_STARTED_AT_MS=$(( $(date +%s) * 1000 ))
PRE_SESSION_JSON=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)

Expand All @@ -1269,35 +1424,11 @@ if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
exit $opencode_exit
fi

# Step 3: Capture the session ID for this invocation
session_id=$(wait_for_session_target_id "$PRE_SESSION_JSON" "$SESSION_CAPTURE_STARTED_AT_MS" "$SESSION_WAIT_SECONDS" || true)
if [ -z "$session_id" ]; then
log "No session found, skipping post-session memory maintenance"
cleanup_timestamp
exit $opencode_exit
fi

# Step 3.5: Skip if session had no real conversation (e.g. user opened TUI and exited)
if ! session_has_conversation "$session_id"; then
log "Session $session_id has no conversation, skipping post-session memory maintenance"
cleanup_timestamp
exit $opencode_exit
fi

# Step 4: Check whether main session already wrote memory files
memory_written_during_session=0
if has_new_memories; then
memory_written_during_session=1
fi

# Timestamp file is no longer needed after the check above.
cleanup_timestamp

# Step 5: Run tasks (foreground for debug, background by default)
# Step 3: Run post-session maintenance (foreground for debug, background by default).
if [ "$FOREGROUND" = "1" ]; then
run_post_session_tasks "$session_id" "$memory_written_during_session"
run_post_session_for_invocation
else
run_post_session_tasks "$session_id" "$memory_written_during_session" &
run_post_session_for_invocation &
disown
log "Post-session memory maintenance started in background (PID $!)"
fi
Expand Down
Loading
Loading