Skip to content

Commit f445fb3

Browse files
phernandezclaude
andcommitted
merge: resolve conflicts with main for workspace routing
Resolved conflicts in cloud_utils.py, upload_command.py, async_client.py, and related tests. Adopted main's resolve_configured_workspace() helper as the centralized workspace resolution pattern, replacing inline config lookups from the PR branch. Removed superseded direct _workspace_headers tests in favor of main's more comprehensive workspace resolution tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
2 parents 0496948 + fe04a0b commit f445fb3

42 files changed

Lines changed: 3469 additions & 411 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ concurrency:
66

77
on:
88
push:
9-
branches: [ "main" ]
109
pull_request:
1110
branches: [ "main" ]
1211

@@ -52,7 +51,6 @@ jobs:
5251
test-sqlite-unit:
5352
name: Test SQLite Unit (${{ matrix.os }}, Python ${{ matrix.python-version }})
5453
timeout-minutes: 30
55-
needs: [static-checks]
5654
strategy:
5755
fail-fast: false
5856
matrix:
@@ -99,7 +97,6 @@ jobs:
9997
test-sqlite-integration:
10098
name: Test SQLite Integration (${{ matrix.os }}, Python ${{ matrix.python-version }})
10199
timeout-minutes: 45
102-
needs: [static-checks]
103100
strategy:
104101
fail-fast: false
105102
matrix:
@@ -146,7 +143,6 @@ jobs:
146143
test-postgres-unit:
147144
name: Test Postgres Unit (Python ${{ matrix.python-version }})
148145
timeout-minutes: 30
149-
needs: [static-checks]
150146
strategy:
151147
fail-fast: false
152148
matrix:
@@ -155,8 +151,22 @@ jobs:
155151
- python-version: "3.13"
156152
- python-version: "3.14"
157153
runs-on: ubuntu-latest
158-
159-
# Note: No services section needed - testcontainers handles Postgres in Docker
154+
services:
155+
postgres:
156+
image: pgvector/pgvector:pg16
157+
env:
158+
POSTGRES_USER: basic_memory_user
159+
POSTGRES_PASSWORD: dev_password
160+
POSTGRES_DB: basic_memory_test
161+
ports:
162+
- 5432:5432
163+
options: >-
164+
--health-cmd "pg_isready -U basic_memory_user -d basic_memory_test"
165+
--health-interval 10s
166+
--health-timeout 5s
167+
--health-retries 5
168+
env:
169+
BASIC_MEMORY_TEST_POSTGRES_URL: postgresql://basic_memory_user:dev_password@127.0.0.1:5432/basic_memory_test
160170

161171
steps:
162172
- uses: actions/checkout@v4
@@ -190,7 +200,6 @@ jobs:
190200
test-postgres-integration:
191201
name: Test Postgres Integration (Python ${{ matrix.python-version }})
192202
timeout-minutes: 45
193-
needs: [static-checks]
194203
strategy:
195204
fail-fast: false
196205
matrix:
@@ -199,8 +208,22 @@ jobs:
199208
- python-version: "3.13"
200209
- python-version: "3.14"
201210
runs-on: ubuntu-latest
202-
203-
# Note: No services section needed - testcontainers handles Postgres in Docker
211+
services:
212+
postgres:
213+
image: pgvector/pgvector:pg16
214+
env:
215+
POSTGRES_USER: basic_memory_user
216+
POSTGRES_PASSWORD: dev_password
217+
POSTGRES_DB: basic_memory_test
218+
ports:
219+
- 5432:5432
220+
options: >-
221+
--health-cmd "pg_isready -U basic_memory_user -d basic_memory_test"
222+
--health-interval 10s
223+
--health-timeout 5s
224+
--health-retries 5
225+
env:
226+
BASIC_MEMORY_TEST_POSTGRES_URL: postgresql://basic_memory_user:dev_password@127.0.0.1:5432/basic_memory_test
204227

205228
steps:
206229
- uses: actions/checkout@v4
@@ -234,7 +257,6 @@ jobs:
234257
test-semantic:
235258
name: Test Semantic (Python 3.12)
236259
timeout-minutes: 45
237-
needs: [static-checks]
238260
runs-on: ubuntu-latest
239261

240262
steps:

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,5 +442,9 @@ With GitHub integration, the development workflow includes:
442442
3. **Branch management** - Claude can create feature branches for implementations
443443
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
444444
5. **Code Commits**: ALWAYS sign off commits with `git commit -s`
445+
6. **Pull Request Titles**: PR titles must follow the semantic format enforced by `.github/workflows/pr-title.yml`: `type(scope): summary`
446+
- Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`
447+
- Allowed scopes: `core`, `cli`, `api`, `mcp`, `sync`, `ui`, `deps`, `installer`
448+
- Example: `fix(cli): propagate cloud workspace routing`
445449

446450
This level of integration represents a new paradigm in AI-human collaboration, where the AI assistant becomes a full-fledged team member rather than just a tool for generating code snippets.

src/basic_memory/alembic/env.py

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666

6767

6868
# Add this function to tell Alembic what to include/exclude
69-
def include_object(object, name, type_, reflected, compare_to):
69+
def include_object(obj, name, type_, reflected, compare_to):
7070
# Ignore SQLite FTS tables
7171
if type_ == "table" and name.startswith("search_index"):
7272
return False
@@ -118,6 +118,54 @@ async def run_async_migrations(connectable):
118118
await connectable.dispose()
119119

120120

121+
def _run_async_migrations_with_asyncio_run(connectable) -> None:
122+
"""Run async migrations with asyncio.run while closing failed coroutines.
123+
124+
Trigger: asyncio.run() may reject execution when another event loop is already active.
125+
Why: Python raises before awaiting the coroutine, which otherwise leaks a
126+
RuntimeWarning about an un-awaited coroutine.
127+
Outcome: close the pending coroutine before bubbling the RuntimeError to the
128+
fallback path.
129+
"""
130+
migration_coro = run_async_migrations(connectable)
131+
try:
132+
asyncio.run(migration_coro)
133+
except RuntimeError:
134+
migration_coro.close()
135+
raise
136+
137+
138+
def _run_async_migrations_in_thread(connectable) -> None:
139+
"""Run async migrations in a dedicated thread with its own event loop."""
140+
import concurrent.futures
141+
142+
def run_in_thread():
143+
"""Run async migrations in a new event loop in a separate thread."""
144+
new_loop = asyncio.new_event_loop()
145+
asyncio.set_event_loop(new_loop)
146+
try:
147+
new_loop.run_until_complete(run_async_migrations(connectable))
148+
finally:
149+
new_loop.close()
150+
151+
with concurrent.futures.ThreadPoolExecutor() as executor:
152+
future = executor.submit(run_in_thread)
153+
future.result() # Wait for completion and re-raise any exceptions
154+
155+
156+
def _run_async_engine_migrations(connectable) -> None:
157+
"""Run async-engine migrations with a running-loop fallback."""
158+
try:
159+
_run_async_migrations_with_asyncio_run(connectable)
160+
except RuntimeError as e:
161+
if "cannot be called from a running event loop" in str(e):
162+
# We're in a running event loop (likely uvloop or Python 3.14+ tests).
163+
# Switch to a dedicated thread so Alembic can finish without nesting loops.
164+
_run_async_migrations_in_thread(connectable)
165+
else:
166+
raise
167+
168+
121169
def run_migrations_online() -> None:
122170
"""Run migrations in 'online' mode.
123171
@@ -148,30 +196,10 @@ def run_migrations_online() -> None:
148196

149197
# Handle async engines (PostgreSQL with asyncpg)
150198
if isinstance(connectable, AsyncEngine):
151-
# Try to run async migrations
152-
# nest_asyncio allows asyncio.run() from within event loops, but doesn't work with uvloop
153-
try:
154-
asyncio.run(run_async_migrations(connectable))
155-
except RuntimeError as e:
156-
if "cannot be called from a running event loop" in str(e):
157-
# We're in a running event loop (likely uvloop) - need to use a different approach
158-
# Create a new thread to run the async migrations
159-
import concurrent.futures
160-
161-
def run_in_thread():
162-
"""Run async migrations in a new event loop in a separate thread."""
163-
new_loop = asyncio.new_event_loop()
164-
asyncio.set_event_loop(new_loop)
165-
try:
166-
new_loop.run_until_complete(run_async_migrations(connectable))
167-
finally:
168-
new_loop.close()
169-
170-
with concurrent.futures.ThreadPoolExecutor() as executor:
171-
future = executor.submit(run_in_thread)
172-
future.result() # Wait for completion and re-raise any exceptions
173-
else:
174-
raise
199+
# Trigger: async engines need Alembic work to cross the sync/async boundary.
200+
# Why: most callers can use asyncio.run(), but running-loop contexts need a thread fallback.
201+
# Outcome: migrations complete without leaking un-awaited coroutines.
202+
_run_async_engine_migrations(connectable)
175203
else:
176204
# Handle sync engines (SQLite) or sync connections
177205
if hasattr(connectable, "connect"):
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Add note_content table
2+
3+
Revision ID: l5g6h7i8j9k0
4+
Revises: k4e5f6g7h8i9
5+
Create Date: 2026-04-04 12:00:00.000000
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "l5g6h7i8j9k0"
17+
down_revision: Union[str, None] = "k4e5f6g7h8i9"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
"""Create note_content for materialized note content and sync state."""
24+
op.create_table(
25+
"note_content",
26+
sa.Column("entity_id", sa.Integer(), nullable=False),
27+
sa.Column("project_id", sa.Integer(), nullable=False),
28+
sa.Column("external_id", sa.String(), nullable=False),
29+
sa.Column("file_path", sa.String(), nullable=False),
30+
sa.Column("markdown_content", sa.Text(), nullable=False),
31+
sa.Column("db_version", sa.BigInteger(), nullable=False),
32+
sa.Column("db_checksum", sa.String(), nullable=False),
33+
sa.Column("file_version", sa.BigInteger(), nullable=True),
34+
sa.Column("file_checksum", sa.String(), nullable=True),
35+
sa.Column("file_write_status", sa.String(), nullable=False),
36+
sa.Column("last_source", sa.String(), nullable=True),
37+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
38+
sa.Column("file_updated_at", sa.DateTime(timezone=True), nullable=True),
39+
sa.Column("last_materialization_error", sa.Text(), nullable=True),
40+
sa.Column("last_materialization_attempt_at", sa.DateTime(timezone=True), nullable=True),
41+
sa.CheckConstraint(
42+
"file_write_status IN ("
43+
"'pending', "
44+
"'writing', "
45+
"'synced', "
46+
"'failed', "
47+
"'external_change_detected'"
48+
")",
49+
name="ck_note_content_file_write_status",
50+
),
51+
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"], ondelete="CASCADE"),
52+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
53+
sa.PrimaryKeyConstraint("entity_id"),
54+
)
55+
op.create_index("ix_note_content_project_id", "note_content", ["project_id"], unique=False)
56+
op.create_index("ix_note_content_file_path", "note_content", ["file_path"], unique=False)
57+
op.create_index("ix_note_content_external_id", "note_content", ["external_id"], unique=True)
58+
59+
60+
def downgrade() -> None:
61+
"""Drop note_content and its supporting indexes."""
62+
op.drop_index("ix_note_content_external_id", table_name="note_content")
63+
op.drop_index("ix_note_content_file_path", table_name="note_content")
64+
op.drop_index("ix_note_content_project_id", table_name="note_content")
65+
op.drop_table("note_content")

0 commit comments

Comments
 (0)