Skip to content

Bug: custom tools (memory, permissions, etc.) are silently unreachable #169

Description

@josephfung

Bug: custom tools (memory, permissions, etc.) are silently unreachable

Description

All custom tools registered via _createCustomTools() — memory tools (memory_save, memory_search, memory_forget), recall_context, permission tools (check_permissions, request_permissions), skill tools, plan workflow tools, and subagent tools — are unreachable during task execution. The agent sees them listed in the system prompt but cannot invoke them.

This affects all execution modes equally: zora-agent ask, zora-agent start (dashboard), Docker, and native.

Root Cause

There are three issues, two of which compound to make tools completely unreachable:

1. ClaudeProvider.execute() passes custom tools via a mechanism the SDK ignores

In src/providers/claude-provider.ts, custom tools are passed as sdkOptions['customTools']:

sdkOptions['customTools'] = task.customTools.map(t => ({
  name: t.name,
  description: t.description,
  input_schema: t.input_schema,
}));

The SDK silently ignores this property. The correct approach (already implemented in ExecutionLoop) is to wrap them as an in-process MCP server via createSdkMcpServer(). However, ExecutionLoop is only used for ancillary paths (heartbeat, compression, memory extraction) — not for main task execution, which goes through ClaudeProvider.execute() directly.

Task execution path: Orchestrator.submitTask()_executeWithProvider()provider.execute() → SDK query() — custom tools are passed but ignored.

2. SDK's createSdkMcpServer expects Zod schemas, not JSON Schema

Even after fixing issue #1, the SdkMcpToolDefinition type requires Zod schemas for inputSchema. The custom tools in Zora define input_schema as raw JSON Schema objects. Passing these to createSdkMcpServer causes Q.safeParseAsync is not a function errors at runtime when the SDK attempts to validate tool inputs.

The SDK exports a tool() helper that accepts Zod schemas and produces properly typed SdkMcpToolDefinition objects.

Note: ExecutionLoop has the same bug — it passes raw JSON Schema to inputSchema — but it's never triggered because the tools registered there (heartbeat, compression) are never actually called by the agent.

3. System prompt references bare tool names

MemoryManager.loadContext() tells the agent to use bare names (memory_save, memory_search, recall_context). Tools registered via MCP server are prefixed as mcp__zora-tools__<name>. The prompt needs to reference the prefixed names. Additionally, memory_forget is missing from the prompt entirely.

Steps to Reproduce

  1. Start Zora (any mode — zora-agent ask, dashboard, or Docker)
  2. Ask: "Remember that my name is Joseph"
  3. Observe: the agent attempts to find memory_save, fails, and reports the tool is unavailable

Expected Behavior

The agent should invoke the memory save tool and confirm the memory was stored.

Suggested Fix

Issue #1 — Provider tool registration:

In ClaudeProvider.execute(), wrap custom tools as an MCP server using createSdkMcpServer(), and add the MCP-prefixed tool names to allowedTools.

Issue #2 — Zod schema requirement:

Use the SDK's tool() helper to create MCP tool definitions. Convert each JSON Schema property to z.any().describe(...) so the SDK gets a valid Zod shape while letting the tool's own handler perform real validation.

Issue #3 — System prompt naming:

Update src/memory/memory-manager.ts loadContext() to reference mcp__zora-tools__* prefixed names and add memory_forget.

Files

  • src/providers/claude-provider.ts — passes custom tools via ignored sdkOptions['customTools'] (primary bug)
  • src/memory/memory-manager.ts — system prompt with bare tool names
  • src/orchestrator/execution-loop.ts — has the same JSON Schema bug but untriggered (reference for MCP pattern)
  • src/orchestrator/orchestrator.ts_createCustomTools() builds the tool array, _executeWithProvider() is the main execution path
  • src/tools/memory-tools.ts — memory tool definitions

Related issues (discovered, out of scope)

During testing of the above fix, two additional pre-existing issues were observed:

Memory search index not rebuilt after writes

memory_save and the extraction pipeline successfully write items to disk (~/.zora/memory/items/mem_*.json), but memory_search returns empty results. The MiniSearch index (stored in ~/.zora/memory/index/minisearch.json) appears to not be rebuilt when new items are added, so newly saved memories are invisible to search until the index is manually rebuilt or the process restarts.

Dashboard messages have no conversation continuity

Each message submitted through the dashboard is handled as an independent task (POST /api/task). The agent loses all context between messages — it cannot see its own previous responses or the user's earlier messages in the same session. This makes multi-turn interactions (like "Yes please do" in response to an offer) impossible, as the agent has no record of what it offered.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions