Killed "active" session dependency:

1. roboco/mcp/schemas/__init__.py:
    - SendMessageInput.task_id → required
    - AskQuestionInput.task_id → required
    - ReportBlockerInput.task_id → required
  2. roboco/mcp/message_server.py:
    - Removed _get_active_session() entirely (dead code)
    - Simplified _handle_message_send() - no fallback, just uses task's linked session
    - Updated tool signatures for roboco_ask_question and roboco_report_blocker
  3. roboco/api/routes/tasks.py (earlier fix):
    - Added subtask validation before parent completion

  New behavior:
  - All messages MUST have task_id
  - Task's linked session is the only source of truth
  - No session? → Clear error: "Cell PM must create one"
  - No more "active" status guessing
This commit is contained in:
Renn F
2025-12-23 22:58:07 +01:00
parent 204b959733
commit 06720b1978
9 changed files with 66 additions and 89 deletions
+4
View File
@@ -78,3 +78,7 @@ alembic/versions/*.pyc
.DS_Store
.AppleDouble
.LSOverride
/docs
/data
/#recycle
+3
View File
@@ -225,6 +225,7 @@ Tell the team what you did:
```json
{
"channel_slug": "backend-cell",
"task_id": "{task_id}",
"content": "Triaged TASK-XXX. Created 3 subtasks, assigned to BE-Dev-1.",
"message_type": "action"
}
@@ -317,6 +318,7 @@ roboco_task_claim("TASK-042")
# Announce in channel
roboco_message_send({
"channel_slug": "backend-cell",
"task_id": "TASK-042",
"content": "Triaging TASK-042: Implement rate limiting",
"message_type": "action"
})
@@ -363,6 +365,7 @@ roboco_task_create({
# 8. COMMUNICATE
roboco_message_send({
"channel_slug": "backend-cell",
"task_id": "TASK-042",
"content": "TASK-042 triaged. 3 subtasks created, assigned to BE-Dev-1.",
"message_type": "action"
})
+2
View File
@@ -161,6 +161,7 @@ roboco_task_qa_pass(task_id, {
```json
{
"channel_slug": "backend-cell",
"task_id": "{task_id}",
"content": "QA PASS for TASK-XXX. Proceeding to documenter, then PM review.",
"message_type": "action"
}
@@ -200,6 +201,7 @@ roboco_task_qa_fail(task_id, {
```json
{
"channel_slug": "backend-cell",
"task_id": "{task_id}",
"content": "QA FAIL for TASK-XXX. Issues: [list]. Returning to dev.",
"message_type": "blocker"
}
+3
View File
@@ -217,6 +217,7 @@ Tell the team what you did:
```json
{
"channel_slug": "frontend-cell",
"task_id": "{task_id}",
"content": "Triaged TASK-XXX. Created 3 subtasks, assigned to FE-Dev-1.",
"message_type": "action"
}
@@ -325,6 +326,7 @@ roboco_task_scan(team="frontend")
roboco_task_claim("TASK-055")
roboco_message_send({
"channel_slug": "frontend-cell",
"task_id": "TASK-055",
"content": "Triaging TASK-055: User preferences modal",
"message_type": "action"
})
@@ -363,6 +365,7 @@ roboco_task_assign("TASK-055", "fe-dev-1")
# 8. COMMUNICATE
roboco_message_send({
"channel_slug": "frontend-cell",
"task_id": "TASK-055",
"content": "TASK-055 assigned to FE-Dev-1. Design ready, API available.",
"message_type": "action"
})
+3
View File
@@ -217,6 +217,7 @@ Tell the team what you did:
```json
{
"channel_slug": "uxui-cell",
"task_id": "{task_id}",
"content": "Triaged TASK-XXX. Assigned to UX-Dev.",
"message_type": "action"
}
@@ -330,6 +331,7 @@ roboco_task_scan(team="ux_ui")
roboco_task_claim("TASK-055")
roboco_message_send({
"channel_slug": "uxui-cell",
"task_id": "TASK-055",
"content": "Triaging TASK-055: User preferences modal design",
"message_type": "action"
})
@@ -368,6 +370,7 @@ roboco_task_assign("TASK-055", "ux-dev")
# 8. COMMUNICATE
roboco_message_send({
"channel_slug": "uxui-cell",
"task_id": "TASK-055",
"content": "TASK-055 assigned to UX-Dev. Frontend needs by Friday.",
"message_type": "action"
})
+7 -7
View File
@@ -39,15 +39,15 @@ services:
retries: 5
# ==========================================================================
# Agent Image Builder (pre-builds the agent image at compose time)
# Agent Base Image Builder (specialized images built on-demand by orchestrator)
# ==========================================================================
agent-image:
agent-base-image:
build:
context: .
dockerfile: docker/agent.Dockerfile
image: roboco-agent
container_name: roboco-agent-builder
entrypoint: ["/bin/sh", "-c", "echo 'Agent image built successfully'"]
dockerfile: docker/agent-base.Dockerfile
image: roboco-agent-base
container_name: roboco-agent-base-builder
entrypoint: ["/bin/sh", "-c", "echo 'Agent base image built successfully'"]
restart: "no"
# ==========================================================================
@@ -92,7 +92,7 @@ services:
condition: service_healthy
redis:
condition: service_healthy
agent-image:
agent-base-image:
condition: service_completed_successfully
# Default agents to spawn (override in .env or command line)
# command: ["--spawn", "main-pm", "be-dev-1", "be-qa"]
+21
View File
@@ -943,6 +943,27 @@ async def complete_task(
detail="Only PMs can complete tasks",
)
# Check for incomplete subtasks before completing parent
subtasks = await service.get_subtasks(task_id)
incomplete_subtasks = [
st
for st in subtasks
if st.status not in (TaskStatus.completed, TaskStatus.cancelled)
]
if incomplete_subtasks:
max_titles_shown = 3
incomplete_titles = [st.title for st in incomplete_subtasks[:max_titles_shown]]
detail = (
f"Cannot complete task - {len(incomplete_subtasks)} subtask(s) "
f"still pending: {', '.join(incomplete_titles)}"
)
if len(incomplete_subtasks) > max_titles_shown:
detail += f" (+{len(incomplete_subtasks) - max_titles_shown} more)"
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=detail,
)
task = await service.complete(task_id)
if not task:
raise HTTPException(
+20 -79
View File
@@ -163,55 +163,6 @@ async def _get_default_group(
return str(groups[0]["id"])
async def _get_active_session(
client: ApiClient,
channel_id: str,
agent_id: str | None = None,
channel_slug: str | None = None,
) -> str | dict[str, Any]:
"""Get active session for channel. Returns session_id or error dict.
NOTE: This function does NOT create sessions. Sessions must be created
by PMs using roboco_session_create_for_tasks. If no active session exists,
returns an error guiding the agent to use task_id or ask their PM.
"""
# First get the default group for this channel
group_result = await _get_default_group(
client, channel_id, agent_id=agent_id, channel_slug=channel_slug
)
if isinstance(group_result, dict):
return group_result # Error response (NO_GROUPS with guidance)
group_id = group_result
# Check if group has an active session
resp = await client.get("/sessions", params={"group_id": group_id, "limit": 10})
if resp.ok:
data = resp.json()
items = data.get("items", [])
# Find an active session
for session in items:
if session.get("status") == "active":
return str(session["id"])
# NO auto-creation! Return helpful error instead
return format_error_response(
"NO_ACTIVE_SESSION",
"No active session in this channel. Sessions are created by PMs for tasks.",
{
"guidance": (
"To send messages, you should:\n"
"1. Include task_id in your message call if working on a task "
"(routes to task's session)\n"
"2. If no task session exists, ask your PM to create one using "
"roboco_session_create_for_tasks\n"
"3. All work communication should happen within task sessions"
),
"channel_id": channel_id,
},
)
# =============================================================================
# TOOL IMPLEMENTATIONS
# =============================================================================
@@ -394,29 +345,23 @@ async def _handle_message_send(
):
return validation_error
session_id: str | None = None
routed_to_task_session = False
# If task_id provided, try to route to task's primary session
if data.task_id:
session_id = await _get_task_primary_session(client, data.task_id)
if session_id:
routed_to_task_session = True
# Fall back to channel's active session (NO auto-creation)
# task_id is required - use task's linked session
session_id = await _get_task_primary_session(client, data.task_id)
if not session_id:
# Get channel by slug
channel_result = await _get_channel_by_slug(client, data.channel_slug)
if isinstance(channel_result, dict):
return channel_result # Error response
channel_id = channel_result
session_result = await _get_active_session(
client, channel_id, agent_id=agent_id, channel_slug=data.channel_slug
# Task has no linked session - PM setup issue
return format_error_response(
"NO_TASK_SESSION",
f"Task {data.task_id} has no linked session.",
{
"guidance": (
"This task doesn't have a work session yet.\n"
"Cell PM must create one using "
"roboco_session_create_for_tasks.\n"
"Escalate to your PM if you need a session for this task."
),
"task_id": data.task_id,
},
)
if isinstance(session_result, dict):
return session_result # Returns NO_GROUPS/NO_ACTIVE_SESSION with guidance
session_id = session_result
# Resolve mentions (slugs) to UUIDs using shared cache
resolved_mentions: list[str] = []
@@ -444,16 +389,12 @@ async def _handle_message_send(
"SEND_FAILED", "Failed to send message", {"api_error": resp.text}
)
guidance = "Message sent successfully."
if routed_to_task_session:
guidance = f"Message sent to task {data.task_id}'s session."
return {
"status": "sent",
"message": resp.json(),
"channel": data.channel_slug,
"routed_to_task_session": routed_to_task_session,
"guidance": guidance,
"task_id": data.task_id,
"guidance": f"Message sent to task {data.task_id}'s session.",
}
@@ -590,8 +531,8 @@ def create_message_mcp_server(agent_id: str) -> FastMCP:
async def roboco_ask_question(
channel_slug: str,
question: str,
task_id: str,
context: str | None = None,
task_id: str | None = None,
) -> dict[str, Any]:
"""
Ask a question in a channel.
@@ -601,8 +542,8 @@ def create_message_mcp_server(agent_id: str) -> FastMCP:
data = AskQuestionInput(
channel_slug=channel_slug,
question=question,
context=context,
task_id=task_id,
context=context,
)
return await _handle_ask_question(client, agent_id, data)
@@ -611,7 +552,7 @@ def create_message_mcp_server(agent_id: str) -> FastMCP:
channel_slug: str,
blocker_description: str,
what_needed: str,
task_id: str | None = None,
task_id: str,
) -> dict[str, Any]:
"""
Report a blocker in a channel.
+3 -3
View File
@@ -111,11 +111,11 @@ class SendMessageInput(BaseModel):
channel_slug: str = Field(..., description="Channel slug (e.g., 'backend-cell')")
content: str = Field(..., description="Message content")
task_id: str = Field(..., description="Task ID (routes to task's session)")
message_type: str = Field(
default="dialogue",
description="Type: reasoning, dialogue, decision, action, blocker, technical",
)
task_id: str | None = Field(default=None, description="Optional related task ID")
reply_to: str | None = Field(default=None, description="Message ID to reply to")
mentions: list[str] = Field(default_factory=list, description="Agents to mention")
@@ -125,8 +125,8 @@ class AskQuestionInput(BaseModel):
channel_slug: str
question: str
task_id: str # Required - routes to task's session
context: str | None = None
task_id: str | None = None
class ReportBlockerInput(BaseModel):
@@ -135,7 +135,7 @@ class ReportBlockerInput(BaseModel):
channel_slug: str
blocker_description: str
what_needed: str
task_id: str | None = None
task_id: str # Required - routes to task's session
# =============================================================================