Conversation
There was a problem hiding this comment.
Pull request overview
Updates the bin/opencode-memory wrapper’s fork-session cleanup to identify forked maintenance sessions by matching the fork title suffix (<parent title> (fork #N)) from opencode.db, avoiding accidental deletion of unrelated newer sessions in the same directory.
Changes:
- Add
opencode.dbhelpers to resolve the parent session title and to locate a unique fork cleanup candidate by title + scope + creation time. - Replace recency-based fork cleanup selection with title-based matching and “delete only on exactly-one match” safety behavior.
- Extend wrapper tests to cover newer normal sessions, multiple matching forks, and missing parent-title scenarios.
Reviewed changes
Copilot reviewed 1 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
bin/opencode-memory |
Switch fork cleanup logic to DB-backed parent-title matching and unique-candidate deletion. |
test/opencode-memory.test.ts |
Add DB seeding and new test cases validating fork cleanup selection behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if [ "\${1:-}" = "run" ] && [ "\${2:-}" = "-s" ]; then | ||
| now_ms=$(( $(date +%s) * 1000 )) | ||
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_target', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" | ||
| mkdir -p "$CLAUDE_CONFIG_DIR/transcripts" | ||
| printf '{"type":"user","content":"fork"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_fork_cleanup_target.jsonl" |
There was a problem hiding this comment.
The fake opencode script depends on the sqlite3 CLI for DB writes. If the environment lacks sqlite3, this test will fail unrelated to the wrapper logic. Prefer performing these inserts via an inline python3 snippet (sqlite3 module) or seeding the DB from the test runner.
| fi | ||
| if [ "\${1:-}" = "run" ] && [ "\${2:-}" = "-s" ]; then | ||
| now_ms=$(( $(date +%s) * 1000 )) | ||
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_target', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" |
There was a problem hiding this comment.
These tests rely on the external sqlite3 CLI inside the fake opencode script to mutate opencode.db. This adds a new system dependency (and potential flake) that isn’t required by the wrapper itself (which uses Python’s sqlite3 module). Prefer inserting rows via an inline python3 snippet (matching the wrapper dependency) or pre-seeding the DB from the test process so the tests don’t require sqlite3 to be installed.
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_target', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" | |
| python3 - "$DB_PATH" "$now_ms" <<'PY' | |
| import sqlite3 | |
| import sys | |
| db_path = sys.argv[1] | |
| now_ms = int(sys.argv[2]) | |
| conn = sqlite3.connect(db_path) | |
| try: | |
| conn.execute( | |
| "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES (?, NULL, ?, ?, ?, ?)", | |
| ("ses_fork_cleanup_target", ${JSON.stringify(root)}, "Wrapped Main Task (fork #1)", now_ms, now_ms), | |
| ) | |
| conn.commit() | |
| finally: | |
| conn.close() | |
| PY |
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_target', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" | ||
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_parallel_real', NULL, '${root}', 'Parallel normal session', $((now_ms + 1000)), $((now_ms + 1000)));" |
There was a problem hiding this comment.
The fake opencode implementation uses the sqlite3 CLI to insert session rows. If sqlite3 is missing in CI/dev environments, these tests will fail even though the wrapper only requires python3. Consider switching this insertion to an inline python3/sqlite3-module script or pre-seeding the needed rows from the test runner to remove the sqlite3 binary dependency.
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_target', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" | |
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_parallel_real', NULL, '${root}', 'Parallel normal session', $((now_ms + 1000)), $((now_ms + 1000)));" | |
| python3 <<PY | |
| import sqlite3 | |
| db_path = "${dbPath}" | |
| root = "${root}" | |
| now_ms = int("${'${now_ms}'}") | |
| conn = sqlite3.connect(db_path) | |
| conn.execute( | |
| "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)", | |
| ("ses_fork_cleanup_target", None, root, "Wrapped Main Task (fork #1)", now_ms, now_ms), | |
| ) | |
| conn.execute( | |
| "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)", | |
| ("ses_parallel_real", None, root, "Parallel normal session", now_ms + 1000, now_ms + 1000), | |
| ) | |
| conn.commit() | |
| conn.close() | |
| PY |
| now_ms=$(( $(date +%s) * 1000 )) | ||
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_one', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" | ||
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_two', NULL, '${root}', 'Wrapped Main Task (fork #2)', $((now_ms + 1000)), $((now_ms + 1000)));" |
There was a problem hiding this comment.
This test’s fake opencode uses the sqlite3 CLI to create fork sessions. That introduces an extra system dependency for the test suite. Using python3 (already a wrapper dependency) to write the rows, or pre-seeding the DB with appropriately-timestamped sessions, would make the tests more portable and less flaky.
| now_ms=$(( $(date +%s) * 1000 )) | |
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_one', NULL, '${root}', 'Wrapped Main Task (fork #1)', $now_ms, $now_ms);" | |
| sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES ('ses_fork_cleanup_two', NULL, '${root}', 'Wrapped Main Task (fork #2)', $((now_ms + 1000)), $((now_ms + 1000)));" | |
| python3 - "$DB_PATH" "${root}" <<'PY' | |
| import sqlite3 | |
| import sys | |
| import time | |
| db_path = sys.argv[1] | |
| root = sys.argv[2] | |
| now_ms = int(time.time() * 1000) | |
| conn = sqlite3.connect(db_path) | |
| try: | |
| conn.execute( | |
| "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)", | |
| ("ses_fork_cleanup_one", None, root, "Wrapped Main Task (fork #1)", now_ms, now_ms), | |
| ) | |
| conn.execute( | |
| "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)", | |
| ("ses_fork_cleanup_two", None, root, "Wrapped Main Task (fork #2)", now_ms + 1000, now_ms + 1000), | |
| ) | |
| conn.commit() | |
| finally: | |
| conn.close() | |
| PY |
|
🎉 This PR is included in version 1.6.4 🎉 The release is available on: Your semantic-release bot 📦🚀 |
What changed
This changes fork-session cleanup in
bin/opencode-memoryto match forked sessions by title instead of deleting whichever newer in-scope session appears after a maintenance fork.Why
The previous cleanup path still relied on recency heuristics. If another normal session appeared in the same directory after extraction or auto-dream started, cleanup could delete that real session instead of the forked maintenance session.
Root cause
OpenCode forked sessions on this machine expose a stable
(fork #N)title suffix, but the wrapper was not using that signal. Cleanup was selecting from newly seen sessions by time rather than proving that a candidate was actually the fork child for the current parent session.Implementation
opencode.db<parent title> (fork #N)as cleanup candidatesValidation
bun test test/opencode-memory.test.tsbash -n bin/opencode-memoryNotes
A full
bun testrun in the sandbox still hits existingEPERMfailures when unrelated tests try to write under~/.claude/projects/...; this PR does not change that behavior.