chasm-cli 1.5.4

Universal chat session manager - harvest, merge, and analyze AI chat history from VS Code, Cursor, and other editors
Documentation
"""
Fix the Agentic workspace:
1. Remove injected test sessions (e1f50715, 18bc4b1b) — these have bad data
2. Add 6be29cba to the agentSessions.model.cache (it's in the index but not in the cache)
3. Verify the fix

ROOT CAUSE: agentSessions.model.cache determines what sessions appear in the Chat panel.
chasm-cli repaired JSONL data and updated the index, but never populated the model cache.
"""
import sqlite3
import json
import os
import base64

WS_STORAGE = r"C:\Users\adamm\AppData\Roaming\Code\User\workspaceStorage"
BROKEN_HASH = "5ec71800c69c79b96b06a37e38537907"

DB_PATH = os.path.join(WS_STORAGE, BROKEN_HASH, "state.vscdb")
SESSIONS_DIR = os.path.join(WS_STORAGE, BROKEN_HASH, "chatSessions")
EDITING_DIR = os.path.join(WS_STORAGE, BROKEN_HASH, "chatEditingSessions")

# Test sessions to remove
TEST_SESSIONS = [
    "e1f50715-8971-412f-943d-70bf0d7b5718",
    "18bc4b1b-b98c-42e6-9317-3bfecfccc4c3",
]

def get_db_value(key):
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("SELECT value FROM ItemTable WHERE key = ?", (key,))
    row = cursor.fetchone()
    conn.close()
    return row[0] if row else None

def set_db_value(key, value):
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)", (key, value))
    conn.commit()
    conn.close()

def session_id_to_base64(session_id):
    """Encode session ID as base64 for model cache resource URI."""
    return base64.b64encode(session_id.encode()).decode()

def build_model_cache_entry(session_id, label, creation_date, last_request_started=None, last_request_ended=None):
    """Build a model cache entry matching VS Code's expected format."""
    b64_id = session_id_to_base64(session_id)
    timing = {"created": creation_date}
    if last_request_started:
        timing["lastRequestStarted"] = last_request_started
    if last_request_ended:
        timing["lastRequestEnded"] = last_request_ended
    
    return {
        "providerType": "local",
        "providerLabel": "Local",
        "resource": f"vscode-chat-session://local/{b64_id}",
        "icon": "vm",
        "label": label,
        "status": 1,
        "timing": timing
    }

print("=== FIXING AGENTIC WORKSPACE ===\n")

# Step 1: Remove test session files
print("Step 1: Removing test session files...")
for sid in TEST_SESSIONS:
    jsonl_path = os.path.join(SESSIONS_DIR, f"{sid}.jsonl")
    if os.path.exists(jsonl_path):
        os.remove(jsonl_path)
        print(f"  Removed {jsonl_path}")
    else:
        print(f"  Not found: {jsonl_path}")
    
    # Also remove chatEditingSessions dir if exists
    edit_dir = os.path.join(EDITING_DIR, sid)
    if os.path.isdir(edit_dir):
        import shutil
        shutil.rmtree(edit_dir)
        print(f"  Removed editing dir: {edit_dir}")

# Step 2: Read the existing index and clean up test entries
print("\nStep 2: Cleaning session index...")
raw_index = get_db_value("chat.ChatSessionStore.index")
if raw_index:
    index = json.loads(raw_index)
    entries = index.get("entries", {})
    original_count = len(entries)
    for sid in TEST_SESSIONS:
        if sid in entries:
            del entries[sid]
            print(f"  Removed {sid} from index")
    
    print(f"  Index: {original_count} -> {len(entries)} entries")
    index["entries"] = entries
    set_db_value("chat.ChatSessionStore.index", json.dumps(index))
else:
    print("  No index found!")

# Step 3: Read remaining session files and build model cache
print("\nStep 3: Building model cache from session files...")
model_cache = []
remaining_files = [f for f in os.listdir(SESSIONS_DIR) if f.endswith('.jsonl')]
print(f"  Remaining session files: {len(remaining_files)}")

for fname in sorted(remaining_files):
    fpath = os.path.join(SESSIONS_DIR, fname)
    sid = fname.replace('.jsonl', '')
    
    with open(fpath, 'r', encoding='utf-8') as f:
        content = f.read()
    
    decoder = json.JSONDecoder()
    try:
        obj, _ = decoder.raw_decode(content.strip())
        v = obj.get("v", {})
        title = v.get("customTitle", "Untitled Session")
        creation_date = v.get("creationDate")
        requests = v.get("requests", [])
        
        # Find last request timestamp
        last_req_started = None
        last_req_ended = None
        for req in requests:
            ts = req.get("timestamp")
            if ts:
                if last_req_started is None or ts > last_req_started:
                    last_req_started = ts
                model_state = req.get("modelState", {})
                completed = model_state.get("completedAt")
                if completed:
                    if last_req_ended is None or completed > last_req_ended:
                        last_req_ended = completed
        
        entry = build_model_cache_entry(
            sid, title, creation_date,
            last_req_started, last_req_ended
        )
        model_cache.append(entry)
        
        print(f"  Added {sid}: '{title}' ({len(requests)} reqs, {os.path.getsize(fpath)} bytes)")
    except Exception as e:
        print(f"  ERROR parsing {sid}: {e}")

# Step 4: Write the model cache
print(f"\nStep 4: Writing model cache with {len(model_cache)} entries...")
set_db_value("agentSessions.model.cache", json.dumps(model_cache))
print("  Done!")

# Step 5: Also update the index to reflect all remaining sessions
print("\nStep 5: Updating index to include all session files...")
raw_index = get_db_value("chat.ChatSessionStore.index")
if raw_index:
    index = json.loads(raw_index)
    entries = index.get("entries", {})
    
    for fname in remaining_files:
        sid = fname.replace('.jsonl', '')
        if sid not in entries:
            fpath = os.path.join(SESSIONS_DIR, fname)
            with open(fpath, 'r', encoding='utf-8') as f:
                content = f.read()
            decoder = json.JSONDecoder()
            obj, _ = decoder.raw_decode(content.strip())
            v = obj.get("v", {})
            
            title = v.get("customTitle", "Untitled Session")
            creation_date = v.get("creationDate", 0)
            
            entries[sid] = {
                "sessionId": sid,
                "title": title,
                "lastMessageDate": creation_date,
                "timing": {
                    "created": creation_date
                }
            }
            print(f"  Added {sid} to index")
    
    index["entries"] = entries
    set_db_value("chat.ChatSessionStore.index", json.dumps(index))
    print(f"  Final index: {len(entries)} entries")

# Step 6: Verify
print("\n\n=== VERIFICATION ===")
raw_index = get_db_value("chat.ChatSessionStore.index")
raw_cache = get_db_value("agentSessions.model.cache")

index = json.loads(raw_index)
cache = json.loads(raw_cache)

print(f"\nIndex: {len(index.get('entries', {}))} entries")
for sid, entry in index.get("entries", {}).items():
    print(f"  {sid}: {entry.get('title', '?')}")

print(f"\nModel cache: {len(cache)} entries")
for entry in cache:
    resource = entry.get("resource", "?")
    b64 = resource.split("/")[-1] if "/" in resource else "?"
    try:
        decoded_id = base64.b64decode(b64).decode()
    except:
        decoded_id = "?"
    print(f"  {decoded_id}: {entry.get('label', '?')}")

print(f"\nSession files:")
for fname in sorted(os.listdir(SESSIONS_DIR)):
    if fname.endswith('.jsonl'):
        sid = fname.replace('.jsonl', '')
        size = os.path.getsize(os.path.join(SESSIONS_DIR, fname))
        in_index = sid in index.get("entries", {})
        in_cache = any(
            base64.b64decode(e.get("resource", "").split("/")[-1]).decode() == sid
            for e in cache
            if "/" in e.get("resource", "")
        )
        print(f"  {sid}: {size}b, inIndex={in_index}, inCache={in_cache}")

print("\n=== DONE. Please restart VS Code and check the Agentic workspace. ===")