From 387b131f82a074643e9516664a6f7265936afbe9 Mon Sep 17 00:00:00 2001 From: kuitos Date: Wed, 15 Apr 2026 00:08:07 +0800 Subject: [PATCH 1/2] fix: match fork cleanup by title suffix --- bin/opencode-memory | 136 ++++++++++++++-- test/opencode-memory.test.ts | 303 ++++++++++++++++++++++++++++++++++- 2 files changed, 421 insertions(+), 18 deletions(-) diff --git a/bin/opencode-memory b/bin/opencode-memory index 2c70682..506ee0f 100755 --- a/bin/opencode-memory +++ b/bin/opencode-memory @@ -473,6 +473,41 @@ get_latest_session_id() { fi } +get_opencode_db_path() { + printf '%s\n' "$HOME/.local/share/opencode/opencode.db" +} + +get_session_title_from_db() { + local session_id="$1" + local db_path + db_path=$(get_opencode_db_path) + + if [ -z "$session_id" ] || [ ! -f "$db_path" ] || ! command -v python3 >/dev/null 2>&1; then + return 1 + fi + + python3 - "$db_path" "$session_id" <<'PY' +import sqlite3 +import sys + +db_path, session_id = sys.argv[1:3] + +try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + row = conn.execute("SELECT title FROM session WHERE id = ? LIMIT 1", (session_id,)).fetchone() +except Exception: + raise SystemExit(1) +finally: + try: + conn.close() + except Exception: + pass + +if row and row[0]: + print(row[0]) +PY +} + get_session_target_id() { local before_json="$1" local started_at_ms="$2" @@ -834,6 +869,78 @@ wait_for_session_target_id() { wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds" } +get_fork_cleanup_candidate_id() { + local started_at_ms="$1" + local parent_title="$2" + local workdir="$3" + local project_dir="$4" + local db_path + db_path=$(get_opencode_db_path) + + if [ -z "$started_at_ms" ] || [ -z "$parent_title" ] || [ ! -f "$db_path" ] || ! command -v python3 >/dev/null 2>&1; then + return 1 + fi + + python3 - "$db_path" "$started_at_ms" "$parent_title" "$workdir" "$project_dir" <<'PY' +import os +import re +import sqlite3 +import sys + +db_path, started_at_ms_raw, parent_title, workdir, project_dir = sys.argv[1:6] +started_at_ms = int(started_at_ms_raw or "0") +scope = {os.path.realpath(path) for path in (workdir, project_dir) if path} +title_pattern = re.compile(rf"^{re.escape(parent_title)} \(fork #\d+\)$") + +try: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + rows = conn.execute( + "SELECT id, title, directory, time_created FROM session WHERE time_created >= ? ORDER BY time_created DESC", + (started_at_ms,), + ).fetchall() +except Exception: + raise SystemExit(1) +finally: + try: + conn.close() + except Exception: + pass + +matches = [] +for session_id, title, directory, time_created in rows: + if not session_id or not title or not directory or not time_created: + continue + if os.path.realpath(directory) not in scope: + continue + if not title_pattern.match(title): + continue + matches.append(session_id) + +if len(matches) == 1: + print(matches[0]) +PY +} + +wait_for_fork_cleanup_candidate_id() { + local started_at_ms="$1" + local parent_title="$2" + local wait_seconds="${3:-5}" + local attempt=0 + local session_id="" + + while [ "$attempt" -lt "$wait_seconds" ]; do + session_id=$(get_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true) + if [ -n "$session_id" ]; then + printf '%s\n' "$session_id" + return 0 + fi + sleep 1 + attempt=$((attempt + 1)) + done + + return 1 +} + file_mtime_secs() { local file="$1" if [ ! -f "$file" ]; then @@ -973,11 +1080,10 @@ rollback_consolidation_lock() { } cleanup_forked_sessions() { - local before_json="$1" - local started_at_ms="$2" - local timestamp_file="$3" + local started_at_ms="$1" + local parent_title="$2" - if [ -z "$before_json" ] || [ -z "$started_at_ms" ] || [ -z "$timestamp_file" ]; then + if [ -z "$started_at_ms" ] || [ -z "$parent_title" ]; then return 0 fi @@ -993,7 +1099,7 @@ PY fi local fork_id - fork_id=$(wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$timestamp_file" "$SESSION_WAIT_SECONDS" 0 || true) + fork_id=$(wait_for_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$SESSION_WAIT_SECONDS" || true) [ -n "$fork_id" ] || return 0 @@ -1040,6 +1146,9 @@ run_extraction_if_needed() { log "Extracting memories from session $session_id..." log "Extraction log: $EXTRACT_LOG_FILE" + local parent_session_title + parent_session_title=$(get_session_title_from_db "$session_id" || true) + local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR") if [ -n "$EXTRACT_MODEL" ]; then cmd+=(-m "$EXTRACT_MODEL") @@ -1049,10 +1158,6 @@ run_extraction_if_needed() { fi cmd+=("$EXTRACT_PROMPT") - local pre_fork_json - pre_fork_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true) - local fork_timestamp_file - fork_timestamp_file=$(mktemp) local fork_started_at_ms fork_started_at_ms=$(( $(date +%s) * 1000 )) @@ -1063,8 +1168,7 @@ run_extraction_if_needed() { log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details" fi - cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file" - rm -f "$fork_timestamp_file" + cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title" release_simple_lock "$EXTRACT_LOCK_FILE" } @@ -1107,6 +1211,9 @@ run_autodream_if_needed() { log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)" log "Auto-dream log: $AUTODREAM_LOG_FILE" + local parent_session_title + parent_session_title=$(get_session_title_from_db "$session_id" || true) + local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR") if [ -n "$AUTODREAM_MODEL" ]; then cmd+=(-m "$AUTODREAM_MODEL") @@ -1116,10 +1223,6 @@ run_autodream_if_needed() { fi cmd+=("$AUTODREAM_PROMPT") - local pre_fork_json - pre_fork_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true) - local fork_timestamp_file - fork_timestamp_file=$(mktemp) local fork_started_at_ms fork_started_at_ms=$(( $(date +%s) * 1000 )) @@ -1132,8 +1235,7 @@ run_autodream_if_needed() { rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME" fi - cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file" - rm -f "$fork_timestamp_file" + cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title" } run_post_session_tasks() { diff --git a/test/opencode-memory.test.ts b/test/opencode-memory.test.ts index 8f11cfc..a3c5196 100644 --- a/test/opencode-memory.test.ts +++ b/test/opencode-memory.test.ts @@ -1,3 +1,4 @@ +import { Database } from "bun:sqlite" import { afterEach, describe, expect, test } from "bun:test" import { chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "fs" import { tmpdir } from "os" @@ -18,6 +19,48 @@ function writeExecutable(filePath: string, content: string): void { chmodSync(filePath, 0o755) } +function seedSessionDb( + homeDir: string, + rows: Array<{ + id: string + title: string + directory: string + parentId?: string | null + timeCreated?: number + timeUpdated?: number + }>, +): string { + const dbDir = join(homeDir, ".local", "share", "opencode") + const dbPath = join(dbDir, "opencode.db") + + mkdirSync(dbDir, { recursive: true }) + + const db = new Database(dbPath) + db.exec(` + CREATE TABLE IF NOT EXISTS session ( + id TEXT PRIMARY KEY, + parent_id TEXT, + directory TEXT NOT NULL, + title TEXT NOT NULL, + time_created INTEGER NOT NULL, + time_updated INTEGER NOT NULL + ) + `) + + const insert = db.query( + "INSERT OR REPLACE INTO session (id, parent_id, directory, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)", + ) + + for (const row of rows) { + const created = row.timeCreated ?? Date.now() + const updated = row.timeUpdated ?? created + insert.run(row.id, row.parentId ?? null, row.directory, row.title, created, updated) + } + + db.close() + return dbPath +} + afterEach(() => { while (tempRoots.length > 0) { const root = tempRoots.pop() @@ -766,6 +809,15 @@ exit 0 const tmpDir = join(root, "tmp") const claudeDir = join(root, "claude") const stateFile = join(root, "delete-log") + const dbPath = seedSessionDb(homeDir, [ + { + id: "ses_wrapped_target", + title: "Wrapped Main Task", + directory: root, + timeCreated: 1, + timeUpdated: 1, + }, + ]) mkdirSync(fakeBin, { recursive: true }) mkdirSync(homeDir, { recursive: true }) @@ -777,8 +829,9 @@ exit 0 `#!/usr/bin/env bash set -euo pipefail DELETE_LOG="${stateFile}" +DB_PATH="${dbPath}" if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then - echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}"}]' + echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"}]' exit 0 fi if [ "\${1:-}" = "export" ]; then @@ -804,6 +857,8 @@ if [ "\${1:-}" = "run" ] && [ "\${2:-}" != "-s" ]; then exit 0 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);" 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" echo "forked cleanup run" @@ -833,6 +888,252 @@ exit 0 expect(readFileSync(stateFile, "utf-8")).toContain("ses_fork_cleanup_target") }) + test("cleans up only the matching fork-titled session when a newer normal session exists", () => { + const root = makeTempRoot() + const fakeBin = join(root, "bin") + const homeDir = join(root, "home") + const tmpDir = join(root, "tmp") + const claudeDir = join(root, "claude") + const deleteLog = join(root, "delete-log") + const stateFile = join(root, "state") + const dbPath = seedSessionDb(homeDir, [ + { + id: "ses_wrapped_target", + title: "Wrapped Main Task", + directory: root, + timeCreated: 1, + timeUpdated: 1, + }, + ]) + + mkdirSync(fakeBin, { recursive: true }) + mkdirSync(homeDir, { recursive: true }) + mkdirSync(tmpDir, { recursive: true }) + mkdirSync(claudeDir, { recursive: true }) + + writeExecutable( + join(fakeBin, "opencode"), + `#!/usr/bin/env bash +set -euo pipefail +DELETE_LOG="${deleteLog}" +STATE_FILE="${stateFile}" +DB_PATH="${dbPath}" +if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then + if [ ! -f "$STATE_FILE" ]; then + echo '[{"id":"ses_existing_old","updated":1,"created":1,"directory":"${root}","title":"Existing Session"}]' + else + echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"},{"id":"ses_existing_old","updated":1,"created":1,"directory":"${root}","title":"Existing Session"}]' + fi + exit 0 +fi +if [ "\${1:-}" = "export" ]; then + cat <<'JSON' +{"info":{"directory":"${root}"}} +JSON + exit 0 +fi +if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "delete" ]; then + printf '%s\n' "\${3:-}" >> "$DELETE_LOG" + exit 0 +fi +if [ "\${1:-}" != "session" ] && ! { [ "\${1:-}" = "run" ] && [ "\${2:-}" = "-s" ]; }; then + echo wrapped > "$STATE_FILE" + mkdir -p "$CLAUDE_CONFIG_DIR/transcripts" + printf '{"type":"user","content":"wrapped"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_wrapped_target.jsonl" + echo "main run ok" + exit 0 +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);" + 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)));" + 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" + sleep 1 + printf '{"type":"user","content":"parallel"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_parallel_real.jsonl" + echo "forked cleanup run" + exit 0 +fi +exit 0 +`, + ) + + const result = spawnSync("bash", [scriptPath, "--help"], { + cwd: root, + encoding: "utf-8", + env: { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + HOME: homeDir, + TMPDIR: tmpDir, + CLAUDE_CONFIG_DIR: claudeDir, + OPENCODE_MEMORY_SESSION_WAIT_SECONDS: "1", + OPENCODE_MEMORY_FOREGROUND: "1", + OPENCODE_MEMORY_AUTODREAM: "0", + }, + }) + + expect(result.status).toBe(0) + expect(existsSync(deleteLog)).toBe(true) + + const deletedIds = readFileSync(deleteLog, "utf-8") + expect(deletedIds).toContain("ses_fork_cleanup_target") + expect(deletedIds).not.toContain("ses_parallel_real") + }) + + test("skips cleanup when multiple fork-titled sessions match the parent title", () => { + const root = makeTempRoot() + const fakeBin = join(root, "bin") + const homeDir = join(root, "home") + const tmpDir = join(root, "tmp") + const claudeDir = join(root, "claude") + const deleteLog = join(root, "delete-log") + const dbPath = seedSessionDb(homeDir, [ + { + id: "ses_wrapped_target", + title: "Wrapped Main Task", + directory: root, + timeCreated: 1, + timeUpdated: 1, + }, + ]) + + mkdirSync(fakeBin, { recursive: true }) + mkdirSync(homeDir, { recursive: true }) + mkdirSync(tmpDir, { recursive: true }) + mkdirSync(claudeDir, { recursive: true }) + + writeExecutable( + join(fakeBin, "opencode"), + `#!/usr/bin/env bash +set -euo pipefail +DELETE_LOG="${deleteLog}" +DB_PATH="${dbPath}" +if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then + echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"}]' + exit 0 +fi +if [ "\${1:-}" = "export" ]; then + cat <<'JSON' +{"info":{"directory":"${root}"}} +JSON + exit 0 +fi +if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "delete" ]; then + printf '%s\n' "\${3:-}" >> "$DELETE_LOG" + exit 0 +fi +if [ "\${1:-}" = "run" ] && [ "\${2:-}" != "-s" ]; then + mkdir -p "$CLAUDE_CONFIG_DIR/transcripts" + printf '{"type":"user","content":"wrapped"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_wrapped_target.jsonl" + echo "main run ok" + exit 0 +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_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)));" + mkdir -p "$CLAUDE_CONFIG_DIR/transcripts" + printf '{"type":"user","content":"fork-one"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_fork_cleanup_one.jsonl" + sleep 1 + printf '{"type":"user","content":"fork-two"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_fork_cleanup_two.jsonl" + echo "forked cleanup run" + exit 0 +fi +exit 0 +`, + ) + + const result = spawnSync("bash", [scriptPath, "run", "hello"], { + cwd: root, + encoding: "utf-8", + env: { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + HOME: homeDir, + TMPDIR: tmpDir, + CLAUDE_CONFIG_DIR: claudeDir, + OPENCODE_MEMORY_SESSION_WAIT_SECONDS: "1", + OPENCODE_MEMORY_FOREGROUND: "1", + OPENCODE_MEMORY_AUTODREAM: "0", + }, + }) + + expect(result.status).toBe(0) + expect(existsSync(deleteLog)).toBe(false) + }) + + test("skips cleanup when the parent session title cannot be resolved", () => { + const root = makeTempRoot() + const fakeBin = join(root, "bin") + const homeDir = join(root, "home") + const tmpDir = join(root, "tmp") + const claudeDir = join(root, "claude") + const deleteLog = join(root, "delete-log") + const dbPath = seedSessionDb(homeDir, []) + + mkdirSync(fakeBin, { recursive: true }) + mkdirSync(homeDir, { recursive: true }) + mkdirSync(tmpDir, { recursive: true }) + mkdirSync(claudeDir, { recursive: true }) + + writeExecutable( + join(fakeBin, "opencode"), + `#!/usr/bin/env bash +set -euo pipefail +DELETE_LOG="${deleteLog}" +DB_PATH="${dbPath}" +if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then + echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"}]' + exit 0 +fi +if [ "\${1:-}" = "export" ]; then + cat <<'JSON' +{"info":{"directory":"${root}"}} +JSON + exit 0 +fi +if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "delete" ]; then + printf '%s\n' "\${3:-}" >> "$DELETE_LOG" + exit 0 +fi +if [ "\${1:-}" = "run" ] && [ "\${2:-}" != "-s" ]; then + mkdir -p "$CLAUDE_CONFIG_DIR/transcripts" + printf '{"type":"user","content":"wrapped"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_wrapped_target.jsonl" + echo "main run ok" + exit 0 +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);" + 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" + echo "forked cleanup run" + exit 0 +fi +exit 0 +`, + ) + + const result = spawnSync("bash", [scriptPath, "run", "hello"], { + cwd: root, + encoding: "utf-8", + env: { + ...process.env, + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + HOME: homeDir, + TMPDIR: tmpDir, + CLAUDE_CONFIG_DIR: claudeDir, + OPENCODE_MEMORY_SESSION_WAIT_SECONDS: "1", + OPENCODE_MEMORY_FOREGROUND: "1", + OPENCODE_MEMORY_AUTODREAM: "0", + }, + }) + + expect(result.status).toBe(0) + expect(existsSync(deleteLog)).toBe(false) + }) + test("skips fork cleanup safely when python3 is unavailable", () => { const root = makeTempRoot() const fakeBin = join(root, "bin") From 5b74dff6150b097b611bfc4c34a2d5474112ec53 Mon Sep 17 00:00:00 2001 From: kuitos Date: Wed, 15 Apr 2026 12:12:59 +0800 Subject: [PATCH 2/2] test: remove sqlite3 cli dependency from wrapper tests --- test/opencode-memory.test.ts | 77 +++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/test/opencode-memory.test.ts b/test/opencode-memory.test.ts index a3c5196..74b3e52 100644 --- a/test/opencode-memory.test.ts +++ b/test/opencode-memory.test.ts @@ -809,7 +809,8 @@ exit 0 const tmpDir = join(root, "tmp") const claudeDir = join(root, "claude") const stateFile = join(root, "delete-log") - const dbPath = seedSessionDb(homeDir, [ + const futureBaseMs = Date.now() + 60_000 + seedSessionDb(homeDir, [ { id: "ses_wrapped_target", title: "Wrapped Main Task", @@ -817,6 +818,13 @@ exit 0 timeCreated: 1, timeUpdated: 1, }, + { + id: "ses_fork_cleanup_target", + title: "Wrapped Main Task (fork #1)", + directory: root, + timeCreated: futureBaseMs, + timeUpdated: futureBaseMs, + }, ]) mkdirSync(fakeBin, { recursive: true }) @@ -829,7 +837,6 @@ exit 0 `#!/usr/bin/env bash set -euo pipefail DELETE_LOG="${stateFile}" -DB_PATH="${dbPath}" if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"}]' exit 0 @@ -857,8 +864,6 @@ if [ "\${1:-}" = "run" ] && [ "\${2:-}" != "-s" ]; then exit 0 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);" 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" echo "forked cleanup run" @@ -868,6 +873,13 @@ exit 0 `, ) + writeExecutable( + join(fakeBin, "sqlite3"), + `#!/usr/bin/env bash +exit 127 +`, + ) + const result = spawnSync("bash", [scriptPath, "run", "hello"], { cwd: root, encoding: "utf-8", @@ -884,6 +896,7 @@ exit 0 }) expect(result.status).toBe(0) + expect(existsSync(join(claudeDir, "transcripts", "ses_fork_cleanup_target.jsonl"))).toBe(true) expect(existsSync(stateFile)).toBe(true) expect(readFileSync(stateFile, "utf-8")).toContain("ses_fork_cleanup_target") }) @@ -896,7 +909,8 @@ exit 0 const claudeDir = join(root, "claude") const deleteLog = join(root, "delete-log") const stateFile = join(root, "state") - const dbPath = seedSessionDb(homeDir, [ + const futureBaseMs = Date.now() + 60_000 + seedSessionDb(homeDir, [ { id: "ses_wrapped_target", title: "Wrapped Main Task", @@ -904,6 +918,20 @@ exit 0 timeCreated: 1, timeUpdated: 1, }, + { + id: "ses_fork_cleanup_target", + title: "Wrapped Main Task (fork #1)", + directory: root, + timeCreated: futureBaseMs, + timeUpdated: futureBaseMs, + }, + { + id: "ses_parallel_real", + title: "Parallel normal session", + directory: root, + timeCreated: futureBaseMs + 1000, + timeUpdated: futureBaseMs + 1000, + }, ]) mkdirSync(fakeBin, { recursive: true }) @@ -917,7 +945,6 @@ exit 0 set -euo pipefail DELETE_LOG="${deleteLog}" STATE_FILE="${stateFile}" -DB_PATH="${dbPath}" if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then if [ ! -f "$STATE_FILE" ]; then echo '[{"id":"ses_existing_old","updated":1,"created":1,"directory":"${root}","title":"Existing Session"}]' @@ -944,9 +971,6 @@ if [ "\${1:-}" != "session" ] && ! { [ "\${1:-}" = "run" ] && [ "\${2:-}" = "-s" exit 0 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);" - 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)));" 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" sleep 1 @@ -988,7 +1012,8 @@ exit 0 const tmpDir = join(root, "tmp") const claudeDir = join(root, "claude") const deleteLog = join(root, "delete-log") - const dbPath = seedSessionDb(homeDir, [ + const futureBaseMs = Date.now() + 60_000 + seedSessionDb(homeDir, [ { id: "ses_wrapped_target", title: "Wrapped Main Task", @@ -996,6 +1021,20 @@ exit 0 timeCreated: 1, timeUpdated: 1, }, + { + id: "ses_fork_cleanup_one", + title: "Wrapped Main Task (fork #1)", + directory: root, + timeCreated: futureBaseMs, + timeUpdated: futureBaseMs, + }, + { + id: "ses_fork_cleanup_two", + title: "Wrapped Main Task (fork #2)", + directory: root, + timeCreated: futureBaseMs + 1000, + timeUpdated: futureBaseMs + 1000, + }, ]) mkdirSync(fakeBin, { recursive: true }) @@ -1008,7 +1047,6 @@ exit 0 `#!/usr/bin/env bash set -euo pipefail DELETE_LOG="${deleteLog}" -DB_PATH="${dbPath}" if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"}]' exit 0 @@ -1030,9 +1068,6 @@ if [ "\${1:-}" = "run" ] && [ "\${2:-}" != "-s" ]; then exit 0 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_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)));" mkdir -p "$CLAUDE_CONFIG_DIR/transcripts" printf '{"type":"user","content":"fork-one"}\n{"type":"tool_use","content":""}\n' > "$CLAUDE_CONFIG_DIR/transcripts/ses_fork_cleanup_one.jsonl" sleep 1 @@ -1070,7 +1105,16 @@ exit 0 const tmpDir = join(root, "tmp") const claudeDir = join(root, "claude") const deleteLog = join(root, "delete-log") - const dbPath = seedSessionDb(homeDir, []) + const futureBaseMs = Date.now() + 60_000 + seedSessionDb(homeDir, [ + { + id: "ses_fork_cleanup_target", + title: "Wrapped Main Task (fork #1)", + directory: root, + timeCreated: futureBaseMs, + timeUpdated: futureBaseMs, + }, + ]) mkdirSync(fakeBin, { recursive: true }) mkdirSync(homeDir, { recursive: true }) @@ -1082,7 +1126,6 @@ exit 0 `#!/usr/bin/env bash set -euo pipefail DELETE_LOG="${deleteLog}" -DB_PATH="${dbPath}" if [ "\${1:-}" = "session" ] && [ "\${2:-}" = "list" ]; then echo '[{"id":"ses_wrapped_target","updated":20,"created":20,"directory":"${root}","title":"Wrapped Main Task"}]' exit 0 @@ -1104,8 +1147,6 @@ if [ "\${1:-}" = "run" ] && [ "\${2:-}" != "-s" ]; then exit 0 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);" 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" echo "forked cleanup run"