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
41 changes: 36 additions & 5 deletions new_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,11 @@ def get_newest_jsonl(logs_dir):


def verify_claude_working(project_dir, timeout=45):
"""Verify new Claude session is working by watching transcript logs.

Returns the path to the new/active JSONL file on success, or None on timeout.
The returned path is truthy, so callers using `if verify_claude_working(...)` still work.
"""
logs_dir = get_project_logs_dir(project_dir)
baseline_file, baseline_size = get_newest_jsonl(logs_dir)
log(f"Phase 2: watching transcript logs in {logs_dir}")
Expand All @@ -1022,13 +1027,34 @@ def verify_claude_working(project_dir, timeout=45):

if current_file and current_file != baseline_file:
log(f"Verified: new session transcript detected ({os.path.basename(current_file)})")
return True
return current_file

if current_file and current_size > baseline_size:
log(f"Verified: transcript growing (+{current_size - baseline_size} bytes)")
return True
return current_file

return None


return False
def record_session_chain(project_dir, old_jsonl, new_jsonl):
"""Append an old->new session transition record to session-chain.jsonl.

Enables chat-export to stitch context-reset jumps into a continuous narrative.
"""
if not old_jsonl and not new_jsonl:
return
logs_dir = get_project_logs_dir(project_dir)
os.makedirs(logs_dir, exist_ok=True)
chain_file = os.path.join(logs_dir, "session-chain.jsonl")
record = {
"old_session": os.path.basename(old_jsonl) if old_jsonl else None,
"new_session": os.path.basename(new_jsonl) if new_jsonl else None,
"project_dir": os.path.abspath(project_dir),
"timestamp": datetime.now().isoformat(),
}
with open(chain_file, "a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
log(f"Recorded session chain: {record['old_session']} -> {record['new_session']}")


# ============ Main ============
Expand Down Expand Up @@ -1212,10 +1238,15 @@ def _remove_lock():
_remove_lock()
return

# Capture old session JSONL before verify (for chain recording)
old_jsonl, _ = get_newest_jsonl(get_project_logs_dir(launch_dir))

# Phase 2: Verify working (check target project's logs, not source)
working = verify_claude_working(launch_dir, timeout=args.timeout)
if working:
new_jsonl = verify_claude_working(launch_dir, timeout=args.timeout)
if new_jsonl:
log("New Claude confirmed working")
# Record the session chain for chat-export stitching
record_session_chain(launch_dir, old_jsonl, new_jsonl)
shell_pid = find_shell_pid()
if shell_pid:
log(f"Closing old tab (shell PID {shell_pid})")
Expand Down
58 changes: 55 additions & 3 deletions scripts/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,11 @@ def write_new_file():
t.start()
result = context_reset.verify_claude_working(fake_project, timeout=5)
t.join()
test("detects new transcript file", result is True)
test("detects new transcript file", result is not None and "session-new.jsonl" in result)

# Simulate: no new activity within timeout
result2 = context_reset.verify_claude_working(fake_project, timeout=2)
test("times out when no new activity", result2 is False)
test("times out when no new activity", result2 is None)

# Simulate: existing file grows
existing = os.path.join(fake_logs, "session-grow.jsonl")
Expand All @@ -331,7 +331,7 @@ def grow_file():
t2.start()
result3 = context_reset.verify_claude_working(fake_project, timeout=5)
t2.join()
test("detects transcript growth", result3 is True)
test("detects transcript growth", result3 is not None and "session-grow.jsonl" in result3)

context_reset.get_project_logs_dir = orig_fn

Expand Down Expand Up @@ -369,6 +369,58 @@ def grow_file():
test("dry-run exits 0", result.returncode == 0)
test("dry-run prints command", "DRY RUN" in result.stdout)

# --- record_session_chain ---
print("\n=== record_session_chain ===")
with tempfile.TemporaryDirectory() as d:
fake_project = os.path.join(d, "chain-project")
os.makedirs(fake_project)
logs_slug = os.path.abspath(fake_project).replace("\\", "-").replace("/", "-").replace(":", "-")
if logs_slug.startswith("-"):
logs_slug = logs_slug[1:]
fake_logs = os.path.join(d, "dotclaude", "projects", logs_slug)
os.makedirs(fake_logs)

orig_fn = context_reset.get_project_logs_dir
context_reset.get_project_logs_dir = lambda proj: fake_logs

# Test: writes correct JSONL record
context_reset.record_session_chain(fake_project, "/logs/old-session.jsonl", "/logs/new-session.jsonl")
chain_file = os.path.join(fake_logs, "session-chain.jsonl")
test("creates session-chain.jsonl", os.path.exists(chain_file))
with open(chain_file) as fh:
lines = fh.readlines()
test("writes one JSONL line", len(lines) == 1)
record = json.loads(lines[0])
test("old_session is basename only", record["old_session"] == "old-session.jsonl")
test("new_session is basename only", record["new_session"] == "new-session.jsonl")
test("has project_dir", "chain-project" in record["project_dir"])
test("has timestamp", "T" in record["timestamp"])

# Test: appends (doesn't overwrite)
context_reset.record_session_chain(fake_project, "/logs/second-old.jsonl", "/logs/second-new.jsonl")
with open(chain_file) as fh:
lines = fh.readlines()
test("appends second record", len(lines) == 2)
record2 = json.loads(lines[1])
test("second record has correct old", record2["old_session"] == "second-old.jsonl")

# Test: handles None old_jsonl (first session in project)
context_reset.record_session_chain(fake_project, None, "/logs/first.jsonl")
with open(chain_file) as fh:
lines = fh.readlines()
test("handles None old_jsonl", len(lines) == 3)
record3 = json.loads(lines[2])
test("old_session is null when None", record3["old_session"] is None)
test("new_session still recorded", record3["new_session"] == "first.jsonl")

# Test: skips when both are None
context_reset.record_session_chain(fake_project, None, None)
with open(chain_file) as fh:
lines = fh.readlines()
test("skips when both None", len(lines) == 3)

context_reset.get_project_logs_dir = orig_fn

# --- Summary ---
print(f"\n{'='*40}")
print(f"Results: {PASS} passed, {FAIL} failed")
Expand Down
Loading