diff --git a/codeframe/core/blockers.py b/codeframe/core/blockers.py
index 31198232..f00bdb63 100644
--- a/codeframe/core/blockers.py
+++ b/codeframe/core/blockers.py
@@ -10,7 +10,7 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
-from typing import Optional
+from typing import Optional, Union
from codeframe.core.workspace import Workspace, get_db_connection
from codeframe.core import events, runtime, tasks
@@ -69,7 +69,7 @@ def create(
workspace: Workspace,
question: str,
task_id: Optional[str] = None,
- created_by: str = "human",
+ created_by: Union[BlockerOrigin, str] = BlockerOrigin.HUMAN,
) -> Blocker:
"""Create a new blocker.
diff --git a/codeframe/core/react_agent.py b/codeframe/core/react_agent.py
index e5e60524..4978824c 100644
--- a/codeframe/core/react_agent.py
+++ b/codeframe/core/react_agent.py
@@ -51,6 +51,10 @@
PRESERVE_RECENT_PAIRS = 5
DEFAULT_CONTEXT_WINDOW = 200_000 # All Claude 4.x models
+# Reason string emitted when a stall timeout triggers a blocker — used to
+# set the correct BlockerOrigin ("system") vs agent-generated blockers.
+_REASON_STALL_DETECTED = "stall_detected"
+
# Map tool names to agent phases for progress reporting.
_TOOL_PHASE_MAP = {
"read_file": AgentPhase.EXPLORING,
@@ -220,7 +224,7 @@ def run(self, task_id: str) -> AgentStatus:
try:
status = self._react_loop(system_prompt)
if status == AgentStatus.FAILED:
- reason = "stall_detected" if self._stall_triggered.is_set() else "max_iterations_reached"
+ reason = _REASON_STALL_DETECTED if self._stall_triggered.is_set() else "max_iterations_reached"
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": reason,
@@ -244,7 +248,7 @@ def run(self, task_id: str) -> AgentStatus:
self._emit_stream_completion(task_id)
return AgentStatus.COMPLETED
- if reason == "escalated_to_blocker" or reason == "stall_detected":
+ if reason == "escalated_to_blocker" or reason == _REASON_STALL_DETECTED:
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "blocked",
@@ -433,7 +437,7 @@ def _react_loop(self, system_prompt: str) -> AgentStatus:
# StallAction.BLOCKER (default)
self._create_text_blocker(
stall_ctx or "Agent stalled with no tool activity",
- "stall_detected",
+ _REASON_STALL_DETECTED,
)
return AgentStatus.BLOCKED
@@ -689,7 +693,7 @@ def _run_final_verification(
)
elif self._stall_action == StallAction.FAIL:
return (False, "stall_failed")
- return (False, "stall_detected")
+ return (False, _REASON_STALL_DETECTED)
self._verbose_print("[ReactAgent] Running final verification...")
self._emit_progress(
@@ -1209,7 +1213,7 @@ def _create_text_blocker(self, text: str, reason: str) -> None:
f"Agent detected a blocker: {reason}\n\n"
f"Context:\n{text[:500]}"
)
- origin = "system" if reason == "stall_detected" else "agent"
+ origin = "system" if reason == _REASON_STALL_DETECTED else "agent"
blocker = blockers.create(
workspace=self.workspace,
question=question,
diff --git a/codeframe/ui/routers/blockers_v2.py b/codeframe/ui/routers/blockers_v2.py
index c4051eec..3170a834 100644
--- a/codeframe/ui/routers/blockers_v2.py
+++ b/codeframe/ui/routers/blockers_v2.py
@@ -20,7 +20,7 @@
from codeframe.core.workspace import Workspace
from codeframe.lib.rate_limiter import rate_limit_standard
from codeframe.core import blockers
-from codeframe.core.blockers import BlockerStatus
+from codeframe.core.blockers import BlockerStatus, BlockerOrigin
from codeframe.ui.dependencies import get_v2_workspace
from codeframe.ui.response_models import api_error, ErrorCodes
@@ -45,7 +45,7 @@ class BlockerResponse(BaseModel):
status: str
created_at: str
answered_at: Optional[str]
- created_by: str = "human"
+ created_by: BlockerOrigin = BlockerOrigin.HUMAN
class BlockerListResponse(BaseModel):
diff --git a/tests/core/test_blocker_origin.py b/tests/core/test_blocker_origin.py
index f7e90f4d..ed54e23c 100644
--- a/tests/core/test_blocker_origin.py
+++ b/tests/core/test_blocker_origin.py
@@ -61,7 +61,7 @@ def test_list_all_includes_created_by(self, workspace):
class TestExistingBlockersMigration:
def test_blockers_without_created_by_default_to_human(self, workspace):
- """Simulate a pre-migration blocker row with no created_by value."""
+ """COALESCE fallback: rows inserted without created_by read back as HUMAN."""
from codeframe.core.workspace import get_db_connection
import uuid
from datetime import datetime, timezone
@@ -82,3 +82,48 @@ def test_blockers_without_created_by_default_to_human(self, workspace):
fetched = blockers.get(workspace, old_id)
assert fetched is not None
assert fetched.created_by == BlockerOrigin.HUMAN
+
+ def test_alter_table_migration_adds_created_by_column(self, tmp_path: Path):
+ """ALTER TABLE migration: initializing a DB without created_by column adds it."""
+ import sqlite3
+ from codeframe.core.workspace import _init_database, CODEFRAME_DIR, STATE_DB_NAME
+
+ # Create a DB with the old blockers schema (no created_by column)
+ repo = tmp_path / "old-repo"
+ repo.mkdir()
+ codeframe_dir = repo / CODEFRAME_DIR
+ codeframe_dir.mkdir()
+ db_path = codeframe_dir / STATE_DB_NAME
+
+ conn = sqlite3.connect(str(db_path))
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS blockers (
+ id TEXT PRIMARY KEY,
+ workspace_id TEXT NOT NULL,
+ task_id TEXT,
+ question TEXT NOT NULL,
+ answer TEXT,
+ status TEXT NOT NULL DEFAULT 'OPEN',
+ created_at TEXT NOT NULL,
+ answered_at TEXT
+ )
+ """)
+ conn.commit()
+ conn.close()
+
+ # Confirm column is absent before migration
+ conn = sqlite3.connect(str(db_path))
+ cursor = conn.execute("PRAGMA table_info(blockers)")
+ columns_before = {row[1] for row in cursor.fetchall()}
+ conn.close()
+ assert "created_by" not in columns_before
+
+ # Run the full init (triggers the ALTER TABLE migration)
+ _init_database(db_path)
+
+ # Confirm column is present after migration
+ conn = sqlite3.connect(str(db_path))
+ cursor = conn.execute("PRAGMA table_info(blockers)")
+ columns_after = {row[1] for row in cursor.fetchall()}
+ conn.close()
+ assert "created_by" in columns_after
diff --git a/web-ui/__tests__/components/blockers/BlockerCard.test.tsx b/web-ui/__tests__/components/blockers/BlockerCard.test.tsx
index b87adad6..963e4cc4 100644
--- a/web-ui/__tests__/components/blockers/BlockerCard.test.tsx
+++ b/web-ui/__tests__/components/blockers/BlockerCard.test.tsx
@@ -259,7 +259,7 @@ describe('BlockerCard', () => {
render(
);
- expect(screen.getByText('Agent was inactive for too long. Answer to continue or retry execution.')).toBeInTheDocument();
+ expect(screen.getByText('System-initiated pause. Review the context and answer to continue.')).toBeInTheDocument();
});
});
});
diff --git a/web-ui/src/components/blockers/BlockerCard.tsx b/web-ui/src/components/blockers/BlockerCard.tsx
index 17eb336c..9e87ad30 100644
--- a/web-ui/src/components/blockers/BlockerCard.tsx
+++ b/web-ui/src/components/blockers/BlockerCard.tsx
@@ -27,7 +27,7 @@ const ORIGIN_CONFIG: Record