Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
# new endpoint file because `pytest tests/` would also re-collect the
# 178 unittest.TestCase subclasses already run in the step above —
# ~2× the CI minutes for zero extra signal.
run: python -m pytest tests/test_api_endpoints.py -v --tb=short
run: python -m pytest tests/test_api_endpoints.py tests/test_parse_failure_logging.py -v --tb=short

# ── PyInstaller desktop build (Windows only, once per workflow) ────────
# Closes #44. Builds the onedir bundle and smoke-tests --help so the
Expand Down
32 changes: 24 additions & 8 deletions api/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ def get_logs():
try:
bubble = json.loads(row["value"])
chat_map.setdefault(chat_id, []).append(bubble)
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to decode bubble row %s: %s",
row["key"],
e,
)

for chat_id, bubbles in chat_map.items():
bubbles = [b for b in bubbles if isinstance(b, dict)]
Expand Down Expand Up @@ -90,8 +94,12 @@ def get_logs():
with open(wj_path, "r", encoding="utf-8") as f:
wd = json.load(f)
workspace_folder = wd.get("folder")
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to read workspace.json for %s: %s",
name,
e,
)

try:
# closing() guarantees .close() on scope exit (issue #17).
Expand Down Expand Up @@ -130,10 +138,18 @@ def get_logs():
"type": "composer",
"messageCount": len(c.get("conversation") or []),
})
except Exception:
pass
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to read logs from workspace %s: %s",
name,
e,
)
except Exception as e:
_logger.warning(
"Failed to iterate workspaces under %s: %s",
workspace_path,
e,
)

logs.sort(key=lambda log: log.get("timestamp") or 0, reverse=True)
return jsonify({"logs": logs})
Expand Down
63 changes: 48 additions & 15 deletions api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,18 @@ def search():
fn = parts[-1] if parts else None
if fn:
ws_id_to_name[name] = _url_unquote(fn)
except Exception:
pass
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to read workspace.json for %s: %s",
name,
e,
)
except Exception as e:
_logger.warning(
"Failed to list workspace entries under %s: %s",
workspace_path,
e,
)

# Build composer → workspace mapping
composer_id_to_ws = {}
Expand All @@ -139,8 +147,12 @@ def search():
cid = c.get("composerId") if isinstance(c, dict) else None
if cid:
composer_id_to_ws[cid] = entry["name"]
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to load composer mapping from workspace %s: %s",
entry["name"],
e,
)

# Load bubble text for searching
bubble_map = {}
Expand Down Expand Up @@ -261,8 +273,12 @@ def search():
"matchingText": matching_text,
"type": "composer",
})
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to process Composer from composerData:%s during search: %s",
composer_id,
e,
)

except Exception:
_logger.exception("Error searching global storage")
Expand All @@ -288,8 +304,12 @@ def search():
with open(wj_path, "r", encoding="utf-8") as f:
wd = json.load(f)
workspace_folder = wd.get("folder")
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to read workspace.json for %s: %s",
name,
e,
)
workspace_name = _workspace_display_name_from_folder(workspace_folder, fallback=name)

# try/finally guarantees .close() on every exit path (issue #17).
Expand Down Expand Up @@ -362,13 +382,21 @@ def search():
"type": "chat",
})

except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to search legacy workspace %s: %s",
name,
e,
)
finally:
if conn is not None:
conn.close()
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to iterate legacy workspaces under %s: %s",
workspace_path,
e,
)

# ---------------------------------------------------------------
# Search Cursor CLI sessions (only for type=all)
Expand All @@ -386,7 +414,12 @@ def search():

try:
messages = traverse_blobs(session["db_path"])
except Exception:
except Exception as e:
_logger.warning(
"Failed to traverse CLI session blobs for %s: %s",
session_id,
e,
)
continue

bubbles = messages_to_bubbles(messages, created_ms)
Expand Down
7 changes: 6 additions & 1 deletion api/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ def get_workspace(workspace_id):
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
if inferred:
workspace_name = inferred
except Exception:
except Exception as e:
_logger.warning(
"Failed to read workspace.json for %s: %s",
workspace_id,
e,
)
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
if inferred:
workspace_name = inferred
Expand Down
2 changes: 1 addition & 1 deletion requirements-lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Lock is generated on Linux (CI / update-lock.yml). Windows-only transitives (e.g.
# colorama via click) are omitted — pip still installs them on Windows when needed.
blinker==1.9.0 # via flask
click==8.4.0 # via flask
click==8.4.1 # via flask
defusedxml==0.7.1 # via fpdf2
flask==3.1.3 # via -r requirements.txt
fonttools==4.63.0 # via fpdf2
Expand Down
60 changes: 51 additions & 9 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

import json
import logging
import os
import sqlite3
from datetime import datetime, timezone

_logger = logging.getLogger(__name__)

from utils.cli_chat_reader import list_cli_projects
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.path_helpers import (
Expand All @@ -14,6 +17,7 @@
)
from utils.workspace_descriptor import read_json_file
from utils.workspace_path import get_cli_chats_path
from models import Composer, SchemaError
from services.workspace_db import (
_build_composer_id_to_workspace_id,
_collect_invalid_workspace_ids,
Expand Down Expand Up @@ -72,7 +76,32 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
for row in composer_rows:
cid = row["key"].split(":")[1]
try:
cd = json.loads(row["value"])
parsed = json.loads(row["value"])
except (json.JSONDecodeError, TypeError, ValueError) as e:
_logger.warning(
"Failed to decode Composer from composerData:%s: %s",
cid,
e,
)
continue
if not isinstance(parsed, dict):
_logger.warning(
"Failed to parse Composer from composerData:%s: expected object, got %s",
cid,
type(parsed).__name__,
)
continue
try:
composer = Composer.from_dict(parsed, composer_id=cid)
except SchemaError as e:
_logger.warning(
"Failed to parse Composer from composerData:%s: %s",
cid,
e,
)
continue
cd = composer.raw
try:
pid = _determine_project_for_conversation(
cd, cid, project_layouts_map,
project_name_map, workspace_path_map,
Expand All @@ -98,10 +127,14 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
"lastUpdatedAt": to_epoch_ms(cd.get("lastUpdatedAt")) or to_epoch_ms(cd.get("createdAt")) or 0,
"createdAt": to_epoch_ms(cd.get("createdAt")) or 0,
})
except Exception:
pass
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to process Composer from composerData:%s: %s",
cid,
e,
)
except Exception as e:
_logger.error("Failed to load composer rows from global storage: %s", e)
Comment thread
bradjin8 marked this conversation as resolved.
Outdated

# Group workspace entries by normalized folder path
folder_to_entries: dict[str, list] = {}
Expand All @@ -114,8 +147,12 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
first_folder = folders[0] if folders else None
if first_folder:
norm_folder = normalize_file_path(first_folder)
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to read workspace.json for %s: %s",
entry["name"],
e,
)
if not norm_folder:
norm_folder = entry["name"] # fallback to workspace ID
entry_folder_map[entry["name"]] = norm_folder
Expand All @@ -139,7 +176,12 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
for e in group
if os.path.isfile(os.path.join(workspace_path, e["name"], "state.vscdb"))
)
except Exception:
except Exception as e:
_logger.warning(
"Failed to resolve mtime for workspace folder %s: %s",
norm_folder,
e,
)
mtime = 0

workspace_name = _get_workspace_display_name(workspace_path, primary["name"])
Expand Down Expand Up @@ -238,7 +280,7 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
"source": "cli",
})
except Exception as e:
print(f"Failed to load CLI projects: {e}")
_logger.warning("Failed to load CLI projects: %s", e)

projects.sort(key=lambda p: p["lastModified"], reverse=True)
return projects
Loading
Loading