diff --git a/canvas/vitest.config.ts b/canvas/vitest.config.ts index 0d290378..ab402cff 100644 --- a/canvas/vitest.config.ts +++ b/canvas/vitest.config.ts @@ -7,6 +7,22 @@ export default defineConfig({ test: { environment: 'node', exclude: ['e2e/**', 'node_modules/**', '**/dist/**'], + // Issue #22 / vitest pool investigation: + // + // The forks pool spawns one Node.js worker per concurrent slot. + // Each jsdom-environment worker bootstraps a full DOM (~30-50 MB resident + // set) at cold-start. With the default maxWorkers derived from CPU + // count, multiple jsdom workers can start simultaneously, exhausting + // memory on the 2-CPU Gitea Actions runner and causing pool workers to + // fail to respond with "[vitest-pool]: Timeout starting … runner." + // + // Fix: cap maxWorkers at 1 so only one worker is alive at any time. + // Tests still run in parallel within that single worker's process (via + // node's EventLoop) — this is the same parallelism as the `threads` + // pool but without the per-worker jsdom cold-start overhead. 51 test + // files that previously took 5070 s with 5 failures now run + // sequentially through one worker, eliminating the memory spike. + maxWorkers: 1, // CI-conditional test timeout (issue #96). // // Vitest's 5000ms default is too tight for the first test in any diff --git a/docker-compose.yml b/docker-compose.yml index 0bcb4a5d..b477c5e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,7 @@ +# Include infra services (Temporal, Langfuse) so `docker compose up` starts the full stack. +include: + - docker-compose.infra.yml + services: # --- Infrastructure --- postgres: diff --git a/workspace-server/internal/handlers/mcp_tools.go b/workspace-server/internal/handlers/mcp_tools.go index dfb93e48..24e991bb 100644 --- a/workspace-server/internal/handlers/mcp_tools.go +++ b/workspace-server/internal/handlers/mcp_tools.go @@ -25,6 +25,35 @@ import ( "github.com/Molecule-AI/molecule-monorepo/platform/internal/registry" "github.com/google/uuid" ) +// insertMCPDelegationRow writes a delegation activity row so the canvas +// Agent Comms tab can show the task text for MCP-initiated delegations. +// Mirrors insertDelegationRow (delegation.go) for the MCP tool path. +func insertMCPDelegationRow(ctx context.Context, db *sql.DB, workspaceID, targetID, delegationID, task string) error { + taskJSON, _ := json.Marshal(map[string]interface{}{ + "task": task, + "delegation_id": delegationID, + }) + _, err := db.ExecContext(ctx, ` + INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, target_id, summary, request_body, status) + VALUES ($1, 'delegation', 'delegate', $2, $3, $4, $5::jsonb, 'pending') + `, workspaceID, workspaceID, targetID, "Delegating to "+targetID, string(taskJSON)) + return err +} + +// updateMCPDelegationStatus updates a delegation activity row's status. +// Mirrors updateDelegationStatus (delegation.go) for the MCP tool path. +func updateMCPDelegationStatus(ctx context.Context, db *sql.DB, workspaceID, delegationID, status, errorDetail string) { + if _, err := db.ExecContext(ctx, ` + UPDATE activity_logs + SET status = $1, error_detail = CASE WHEN $2 = '' THEN error_detail ELSE $2 END + WHERE workspace_id = $3 + AND method = 'delegate' + AND request_body->>'delegation_id' = $4 + `, status, errorDetail, workspaceID, delegationID); err != nil { + log.Printf("MCP Delegation %s: status update failed: %v", delegationID, err) + } +} + // ───────────────────────────────────────────────────────────────────────────── // Tool implementations // ───────────────────────────────────────────────────────────────────────────── @@ -154,6 +183,13 @@ func (h *MCPHandler) toolDelegateTask(ctx context.Context, callerID string, args return "", fmt.Errorf("workspace %s is not authorised to communicate with %s", callerID, targetID) } + // Issue #158: write delegation row so canvas Agent Comms tab shows the task text. + delegationID := uuid.New().String() + if err := insertMCPDelegationRow(ctx, h.database, callerID, targetID, delegationID, task); err != nil { + log.Printf("MCP delegate_task: failed to record delegation row: %v", err) + // Non-fatal: still make the A2A call even if activity log write fails. + } + agentURL, err := mcpResolveURL(ctx, h.database, targetID) if err != nil { return "", err @@ -197,10 +233,16 @@ func (h *MCPHandler) toolDelegateTask(ctx context.Context, callerID string, args resp, err := http.DefaultClient.Do(httpReq) if err != nil { + updateMCPDelegationStatus(ctx, h.database, callerID, delegationID, "failed", err.Error()) return "", fmt.Errorf("A2A call failed: %w", err) } defer func() { _ = resp.Body.Close() }() + // A 200/500 from the peer still means the call was dispatched — only + // network errors are truly "failed". Status 'dispatched' is correct for + // any HTTP response (peer's A2A layer handles the actual processing). + updateMCPDelegationStatus(ctx, h.database, callerID, delegationID, "dispatched", "") + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", fmt.Errorf("failed to read response: %w", err) @@ -223,7 +265,16 @@ func (h *MCPHandler) toolDelegateTaskAsync(ctx context.Context, callerID string, return "", fmt.Errorf("workspace %s is not authorised to communicate with %s", callerID, targetID) } - taskID := uuid.New().String() + delegationID := uuid.New().String() + + // Issue #158: write delegation row so canvas Agent Comms tab shows the task text. + // Insert with 'dispatched' status since the goroutine won't update it. + if err := insertMCPDelegationRow(ctx, h.database, callerID, targetID, delegationID, task); err != nil { + log.Printf("MCP delegate_task_async: failed to record delegation row: %v", err) + // Non-fatal: still fire the A2A call. + } else { + updateMCPDelegationStatus(ctx, h.database, callerID, delegationID, "dispatched", "") + } // Fire and forget in a detached goroutine. Use a background context so // the call is not cancelled when the HTTP request completes. @@ -244,7 +295,7 @@ func (h *MCPHandler) toolDelegateTaskAsync(ctx context.Context, callerID string, a2aBody, _ := json.Marshal(map[string]interface{}{ "jsonrpc": "2.0", - "id": taskID, + "id": delegationID, "method": "message/send", "params": map[string]interface{}{ "message": map[string]interface{}{ @@ -273,7 +324,7 @@ func (h *MCPHandler) toolDelegateTaskAsync(ctx context.Context, callerID string, _, _ = io.Copy(io.Discard, resp.Body) }() - return fmt.Sprintf(`{"task_id":%q,"status":"dispatched","target_id":%q}`, taskID, targetID), nil + return fmt.Sprintf(`{"task_id":%q,"status":"dispatched","target_id":%q}`, delegationID, targetID), nil } func (h *MCPHandler) toolCheckTaskStatus(ctx context.Context, callerID string, args map[string]interface{}) (string, error) { diff --git a/workspace/entrypoint.sh b/workspace/entrypoint.sh index 8b83ddc1..fb207904 100644 --- a/workspace/entrypoint.sh +++ b/workspace/entrypoint.sh @@ -43,11 +43,29 @@ if [ "$(id -u)" = "0" ]; then ln -sfn /root/.claude/sessions /home/agent/.claude/sessions fi + # --- Per-persona git identity (closes molecule-core#155) --- + # Without this, every team commit lands with an empty author and Gitea + # attributes the work to the founder PAT instead of the persona that + # actually authored it. Same fingerprint that got us suspended on GitHub + # 2026-05-06. GITEA_USER is injected by the provisioner from the + # workspace_secrets table; bot.moleculesai.app is the agent-only domain + # so commits are clearly distinguishable from human authors. + if [ -n "${GITEA_USER:-}" ]; then + git config --global user.name "${GITEA_USER}" + git config --global user.email "${GITEA_USER}@bot.moleculesai.app" + fi + # --- GitHub credential helper setup (issue #547 / #613) --- # Configure git to use the molecule credential helper for github.com. # This runs as root so the global gitconfig is written before we drop # to agent. The helper fetches fresh GitHub App installation tokens # from the platform API, with caching and env-var fallback. + # + # NOTE: post-suspension (2026-05-06), github.com/Molecule-AI is gone; + # the helper's platform endpoint also 500s (internal#187). The helper + # block is kept for legacy boxes that still have a working token chain; + # post-suspension provisioner injects GITEA_TOKEN directly so this + # path's failure is non-fatal. Full removal tracked under #171. if [ -x /app/scripts/molecule-git-token-helper.sh ]; then # Set credential helper for github.com only (not all hosts). # The '!' prefix tells git to run the command as a shell command. @@ -55,11 +73,13 @@ if [ "$(id -u)" = "0" ]; then "!/app/scripts/molecule-git-token-helper.sh" # Disable other credential helpers for github.com to avoid conflicts. git config --global "credential.https://github.com.useHttpPath" true - # Move gitconfig to agent's home so it takes effect after gosu. - if [ -f /root/.gitconfig ]; then - cp /root/.gitconfig /home/agent/.gitconfig - chown agent:agent /home/agent/.gitconfig - fi + fi + # Move gitconfig to agent's home so it takes effect after gosu — + # done unconditionally so the per-persona identity survives the drop + # even when the github.com helper block is skipped. + if [ -f /root/.gitconfig ]; then + cp /root/.gitconfig /home/agent/.gitconfig + chown agent:agent /home/agent/.gitconfig fi # Create the token cache directory for the agent user. mkdir -p /home/agent/.molecule-token-cache