Skip to content

Commit 434cdf2

Browse files
phernandezclaude
andauthored
feat: Add circuit breaker for file sync failures (#364)
Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 7f9c1a9 commit 434cdf2

File tree

5 files changed

+495
-13
lines changed

5 files changed

+495
-13
lines changed

src/basic_memory/cli/commands/status.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def display_changes(
9494
"""Display changes using Rich for better visualization."""
9595
tree = Tree(f"{project_name}: {title}")
9696

97-
if changes.total == 0:
97+
if changes.total == 0 and not changes.skipped_files:
9898
tree.add("No changes")
9999
console.print(Panel(tree, expand=False))
100100
return
@@ -114,6 +114,13 @@ def display_changes(
114114
if changes.deleted:
115115
del_branch = tree.add("[red]Deleted[/red]")
116116
add_files_to_tree(del_branch, changes.deleted, "red")
117+
if changes.skipped_files:
118+
skip_branch = tree.add("[red]⚠️ Skipped (Circuit Breaker)[/red]")
119+
for skipped in sorted(changes.skipped_files, key=lambda x: x.path):
120+
skip_branch.add(
121+
f"[red]{skipped.path}[/red] "
122+
f"(failures: {skipped.failure_count}, reason: {skipped.reason})"
123+
)
117124
else:
118125
# Show directory summaries
119126
by_dir = group_changes_by_directory(changes)
@@ -122,6 +129,14 @@ def display_changes(
122129
if summary: # Only show directories with changes
123130
tree.add(f"[bold]{dir_name}/[/bold] {summary}")
124131

132+
# Show skipped files summary in non-verbose mode
133+
if changes.skipped_files:
134+
skip_count = len(changes.skipped_files)
135+
tree.add(
136+
f"[red]⚠️ {skip_count} file{'s' if skip_count != 1 else ''} "
137+
f"skipped due to repeated failures[/red]"
138+
)
139+
125140
console.print(Panel(tree, expand=False))
126141

127142

src/basic_memory/importers/claude_conversations_importer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ def _format_chat_markdown(
155155
if msg.get("content"):
156156
# Filter out None values before joining
157157
content = " ".join(
158-
str(c.get("text", "")) for c in msg["content"]
158+
str(c.get("text", ""))
159+
for c in msg["content"]
159160
if c and c.get("text") is not None
160161
)
161162
lines.append(content)

src/basic_memory/schemas/sync_report.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Pydantic schemas for sync report responses."""
22

3-
from typing import TYPE_CHECKING, Dict, Set
3+
from datetime import datetime
4+
from typing import TYPE_CHECKING, Dict, List, Set
45

56
from pydantic import BaseModel, Field
67

@@ -9,6 +10,17 @@
910
from basic_memory.sync.sync_service import SyncReport
1011

1112

13+
class SkippedFileResponse(BaseModel):
14+
"""Information about a file that was skipped due to repeated failures."""
15+
16+
path: str = Field(description="File path relative to project root")
17+
reason: str = Field(description="Error message from last failure")
18+
failure_count: int = Field(description="Number of consecutive failures")
19+
first_failed: datetime = Field(description="Timestamp of first failure")
20+
21+
model_config = {"from_attributes": True}
22+
23+
1224
class SyncReportResponse(BaseModel):
1325
"""Report of file changes found compared to database state.
1426
@@ -24,6 +36,9 @@ class SyncReportResponse(BaseModel):
2436
checksums: Dict[str, str] = Field(
2537
default_factory=dict, description="Current file checksums (path -> checksum)"
2638
)
39+
skipped_files: List[SkippedFileResponse] = Field(
40+
default_factory=list, description="Files skipped due to repeated failures"
41+
)
2742
total: int = Field(description="Total number of changes")
2843

2944
@classmethod
@@ -42,6 +57,15 @@ def from_sync_report(cls, report: "SyncReport") -> "SyncReportResponse":
4257
deleted=report.deleted,
4358
moves=report.moves,
4459
checksums=report.checksums,
60+
skipped_files=[
61+
SkippedFileResponse(
62+
path=skipped.path,
63+
reason=skipped.reason,
64+
failure_count=skipped.failure_count,
65+
first_failed=skipped.first_failed,
66+
)
67+
for skipped in report.skipped_files
68+
],
4569
total=report.total,
4670
)
4771

0 commit comments

Comments
 (0)