fix: MCP reconnection, type safety, and bug fixes

- Add MCP reconnection functionality (isHealthy, reconnect, reconnectAll, healthCheckAndReconnect)
- Add auto-reconnect on MCP tool fetch failure
- Add MCP reconnection API endpoints (/mcp/:name/reconnect, /mcp/reconnect-all, /mcp/health-check)
- Fix EventEmitter memory leak warning in GlobalBus
- Fix silent error swallowing in cache cleanup
- Fix type safety issues in processor.ts, transform.ts, prompt.ts
- Add debug status command (agent-core debug status)
- Add reload.sh script for daemon management
- Add comprehensive docs (OPS.md, BUGS.md)
- Update CLAUDE.md with ops warning
This commit is contained in:
Artur Do Lago
2026-01-12 21:49:02 +01:00
parent c225d71117
commit c23eccacb8
19 changed files with 1610 additions and 69 deletions
+174
View File
@@ -0,0 +1,174 @@
# Comprehensive Bug Fix Implementation Plan
**Date:** 2026-01-12
**Scope:** MCP Reconnection, Type Safety, Code Patterns
---
## Part 1: MCP Server Reconnection After Daemon Restart
### Problem Analysis
When the daemon restarts:
1. MCP client connections in the daemon are lost
2. TUI continues to display "Connection closed" status
3. No automatic reconnection mechanism exists
4. User must manually restart TUI to re-establish MCP connections
### Root Causes
1. MCP state is stored per-Instance in `Instance.state()` (line 156)
2. When daemon restarts, Instance state is recreated fresh
3. MCP clients show "connected" initially but fail when actually used
4. The `tools()` function (line 542) catches errors and marks as failed, but only AFTER a tool call fails
### Solution Design
1. **Add health check for MCP connections** - Ping MCP servers before reporting "connected"
2. **Add reconnection logic** - Attempt reconnect when connection fails
3. **Add retry with exponential backoff** - Graceful reconnection
4. **Expose reconnect API** - Allow TUI/API to trigger reconnection
### Implementation Steps
#### Step 1.1: Add connection health check function
```typescript
// In MCP namespace
export async function isHealthy(name: string): Promise<boolean>
```
#### Step 1.2: Add reconnect function
```typescript
export async function reconnect(name: string): Promise<Status>
```
#### Step 1.3: Add automatic reconnection on tool failure
When `tools()` catches an error, attempt reconnect before fully failing.
#### Step 1.4: Add periodic health check (optional)
Background interval to check MCP health and reconnect if needed.
---
## Part 2: Fix Type Safety Issues (`as any`)
### Problem Analysis
35+ occurrences of `as any` throughout the codebase bypass TypeScript's type checking:
- Potential runtime errors not caught at compile time
- Makes refactoring harder
- Reduces IDE assistance
### High-Priority Files
1. `src/provider/provider.ts` - 2 occurrences
2. `src/provider/transform.ts` - 1 occurrence
3. `src/session/prompt.ts` - 2 occurrences
4. `src/session/processor.ts` - 1 occurrence
### Solution Strategy
For each `as any`:
1. **Determine if it's necessary** - Some are valid workarounds for external lib types
2. **Add proper type definitions** - Most can be fixed with correct types
3. **Use type guards** - For runtime type checking
4. **Add `@ts-expect-error` with comments** - For external lib workarounds
### Implementation Steps
#### Step 2.1: Fix provider.ts Auth.get type mismatch
```typescript
// Current:
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, ...)
// Fix: Define proper return type for loader callback
```
#### Step 2.2: Fix transform.ts providerOptions access
```typescript
// Current:
...(msg.providerOptions as any)?.openaiCompatible
// Fix: Define proper type for providerOptions
```
#### Step 2.3: Fix session/prompt.ts schema handling
```typescript
// Current:
inputSchema: jsonSchema(schema as any)
// Fix: Use proper JSONSchema type
```
#### Step 2.4: Fix session/processor.ts error handling
```typescript
// Current:
error: (value.error as any).toString()
// Fix: Add type guard for error
```
---
## Part 3: Additional Code Pattern Issues
### 3.1: Missing unref() on intervals
**Location:** Some setInterval calls don't use .unref()
**Impact:** May prevent process from exiting cleanly
**Fix:** Add .unref() to background intervals that shouldn't keep process alive
### 3.2: Unsafe object property access
**Pattern:** `obj[key]` where key could be undefined
**Fix:** Add null checks or use optional chaining
### 3.3: Silent error handling
**Pattern:** `.catch(() => {})` or `catch (e) {}`
**Fix:** At minimum log errors, or only catch specific expected errors
### 3.4: Race conditions in async operations
**Pattern:** State modifications without proper locking
**Fix:** Add state guards or use atomic operations
---
## Execution Order
1. **Part 1: MCP Reconnection** (High priority - user-visible bug)
- 1.1 Add health check
- 1.2 Add reconnect function
- 1.3 Add auto-reconnect on failure
2. **Part 2: Type Safety** (Medium priority - code quality)
- 2.1 Fix provider.ts
- 2.2 Fix transform.ts
- 2.3 Fix prompt.ts
- 2.4 Fix processor.ts
3. **Part 3: Code Patterns** (Lower priority - code quality)
- 3.1 Add missing unref()
- 3.2 Review and document remaining as any uses
---
## Testing Plan
After implementation:
1. Run `bun run build` to verify compilation
2. Run `./scripts/reload.sh` to restart daemon
3. Verify MCP reconnection works by restarting daemon while TUI is running
4. Run existing tests if available
+152
View File
@@ -0,0 +1,152 @@
/**
* Canvas Tools - WezTerm-native canvas rendering
*
* Provides canvas display capabilities for all personas.
* Canvas types: text, calendar, document, table, diagram, graph, mindmap
*/
import { tool } from "@opencode-ai/plugin"
// Canvas spawn tool
export const canvasSpawn = tool({
description: `Spawn a canvas to display content in a WezTerm pane.
Canvas types:
- text: Simple text display with title and content
- calendar: Monthly calendar view with events
- document: Markdown-like document rendering
- table: Tabular data display
- diagram: Flowchart/architecture diagrams
- graph: Nodes and edges visualization
- mindmap: Hierarchical tree view
Config options by kind:
- text: { title: string, content: string }
- calendar: { date?: "YYYY-MM-DD", events?: [{ date: string, title: string }] }
- document: { title: string, content: string (markdown) }
- table: { title: string, headers: string[], rows: string[][] }
Examples:
- Display poem: { kind: "text", id: "poem", config: '{"title": "My Poem", "content": "Roses are red..."}' }
- Show calendar: { kind: "calendar", id: "cal", config: '{"date": "2026-01-15", "events": []}' }
- Show table: { kind: "table", id: "data", config: '{"title": "Portfolio", "headers": ["Symbol", "Value"], "rows": [["AAPL", "$100"]]}' }`,
args: {
kind: tool.schema
.enum(["text", "calendar", "document", "table", "diagram", "graph", "mindmap"])
.describe("Canvas type"),
id: tool.schema.string().describe("Unique canvas identifier"),
config: tool.schema.string().describe("JSON configuration for the canvas content"),
},
async execute(args) {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")
let config: Record<string, unknown>
try {
config = JSON.parse(args.config)
} catch {
return `Invalid JSON config: ${args.config}`
}
try {
const result = await requestDaemon<{ paneId: string; id: string; kind: string }>(
"canvas:spawn",
{ kind: args.kind, id: args.id, config }
)
return `Canvas "${args.id}" (${args.kind}) displayed in pane ${result.paneId}.
Content:
${JSON.stringify(config, null, 2)}`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to spawn canvas: ${msg}
Note: Canvas requires the agent-core daemon to be running.
Start it with: agent-core daemon`
}
},
})
// Canvas update tool
export const canvasUpdate = tool({
description: `Update an existing canvas's content.
Examples:
- Update text: { id: "poem", config: '{"content": "New poem content"}' }
- Update calendar: { id: "cal", config: '{"events": [{"date": "2026-01-20", "title": "Meeting"}]}' }`,
args: {
id: tool.schema.string().describe("Canvas identifier to update"),
config: tool.schema.string().describe("New JSON configuration"),
},
async execute(args) {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")
let config: Record<string, unknown>
try {
config = JSON.parse(args.config)
} catch {
return `Invalid JSON config: ${args.config}`
}
try {
await requestDaemon("canvas:update", { id: args.id, config })
return `Canvas "${args.id}" updated.
New content:
${JSON.stringify(config, null, 2)}`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to update canvas: ${msg}`
}
},
})
// Canvas close tool
export const canvasClose = tool({
description: `Close a canvas pane.`,
args: {
id: tool.schema.string().describe("Canvas identifier to close"),
},
async execute(args) {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")
try {
await requestDaemon("canvas:close", { id: args.id })
return `Canvas "${args.id}" closed.`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to close canvas: ${msg}`
}
},
})
// Canvas list tool
export const canvasList = tool({
description: `List all active canvases.`,
args: {},
async execute() {
const { requestDaemon } = await import("../../../src/daemon/ipc-client.js")
try {
const canvases = await requestDaemon<
Array<{
id: string
kind: string
paneId: string
createdAt: number
}>
>("canvas:list", {})
if (canvases.length === 0) {
return "No active canvases."
}
const list = canvases.map((c) => `- ${c.id} (${c.kind}) in pane ${c.paneId}`).join("\n")
return `${canvases.length} active canvas(es):
${list}`
} catch (error) {
const msg = error instanceof Error ? error.message : String(error)
return `Failed to list canvases: ${msg}`
}
},
})
+52 -20
View File
@@ -1,12 +1,12 @@
# Agent-Core - The Engine
This is the **engine** that powers Artur's Agent System. agent-core is a fork of OpenCode with custom personas.
This is the **engine** that powers Agent-Core, a fork of OpenCode.
## CRITICAL: Naming Convention
**NEVER use "opencode" in new code, documentation, or user-facing text.**
This project is `agent-core`, not OpenCode. While we're grateful to the original OpenCode maintainers, users must be able to run both projects without confusion:
This project is `agent-core`, not OpenCode. We are grateful to the OpenCode developers and believe users must be able to run both projects without confusion:
- CLI command: `agent-core` (not `opencode`)
- Config directory: `~/.config/agent-core/`
@@ -20,15 +20,15 @@ Existing upstream code may still contain "opencode" references - that's fine. Bu
**ALWAYS read these before making changes:**
1. **Tiara** (`vendor/tiara/`) - The orchestration submodule with claude-flow
1. **Tiara** (`vendor/tiara/`) - The orchestration submodule
- `vendor/tiara/CLAUDE.md` - SPARC methodology, concurrent execution rules
- `vendor/tiara/docs/` - Architecture, integrations, roadmaps
2. **The Triad** (`.claude/skills/`) - The three personas:
- `.claude/skills/zee/SKILL.md` - Personal assistant (memory, messaging, calendar)
- `.claude/skills/stanley/SKILL.md` - Investing system (markets, portfolio, NautilusTrader)
- `.claude/skills/johny/SKILL.md` - Learning system (knowledge graph, spaced repetition)
- `.claude/skills/zee/SKILL.md` - Personal assistant (memory, messaging, calendar, and more)
- `.claude/skills/stanley/SKILL.md` - Investing assistant with access to a full platform (NautilusTrader, OpenBB, own GUI in rust) of APIS integration
- `.claude/skills/johny/SKILL.md` - Study assistant focused on diliberate practice, with knowledge graph and spaced repetition
- Each persona has its own configuration and capabilities, all have access to Tiara's orchestration offers
3. **Shared capabilities** (`.claude/skills/shared/`, `.claude/skills/personas/`)
- Orchestration, WezTerm integration, drone spawning
@@ -63,24 +63,28 @@ You are part of the **Personas** - three AI personas that share a common orchest
### Personas Capabilities (ALL Personas Have These)
**1. Spawn Drones** - You can spawn background workers (drones) that:
- Maintain your persona identity (a "Zee drone" acts like Zee)
- Execute tasks in parallel while you continue the conversation
- Report results back to you
- Run in separate WezTerm panes for visibility
**2. Shared Memory** - All personas share:
- Qdrant vector memory for semantic search
- Conversation continuity state (survives compacting)
- Plan and objectives across sessions
- Key facts extracted from conversations
**3. Conversation Continuity** - When context gets compacted:
- A summary is saved to Qdrant automatically
- Key facts are extracted and preserved
- Plan/objectives persist across sessions
- You can restore context from previous sessions
**4. WezTerm Pane Management** - Visual orchestration:
- Each drone gets its own pane
- Status pane shows Personas state
- You can see what all workers are doing
@@ -88,6 +92,7 @@ You are part of the **Personas** - three AI personas that share a common orchest
### How to Use Personas Capabilities
**Spawning a Drone:**
```
When you need to do heavy background work, you can spawn a drone:
1. Decide what task needs background processing
@@ -97,6 +102,7 @@ When you need to do heavy background work, you can spawn a drone:
```
**Preserving Continuity:**
```
Before context is compacted:
1. Summarize the conversation state
@@ -106,6 +112,7 @@ Before context is compacted:
```
**Checking State:**
```
You can always check:
- What drones are running
@@ -176,14 +183,6 @@ You can always check:
No generic "build" or "plan" agents. Every interaction goes through a persona with domain expertise. The personas share orchestration (tiara) and memory (Qdrant) but have distinct purposes.
## Personas
| Persona | Inspiration | Domain | Skills Location |
|---------|-------------|--------|-----------------|
| **Johny** | von Neumann | Study, learning | `.claude/skills/johny/` |
| **Stanley** | Druckenmiller | Trading, markets | `.claude/skills/stanley/` |
| **Zee** | Personal | Memory, messaging | `.claude/skills/zee/` |
## Key Directories
```
@@ -208,6 +207,7 @@ agent-core/
## Integration
Skills are loaded from `.claude/skills/` and `~/.config/agent-core/skills/`:
```
.claude/skills/johny/ → Johny persona
.claude/skills/stanley/ → Stanley persona
@@ -226,6 +226,7 @@ Skills are loaded from `.claude/skills/` and `~/.config/agent-core/skills/`:
## Experimental Features
This system has experimental features enabled:
- LLM Council for multi-model deliberation
- Knowledge graph with FIRe (Fractional Implicit Repetition)
- Semantic memory via Qdrant
@@ -233,27 +234,53 @@ This system has experimental features enabled:
## State Management
| Data | Location |
|------|----------|
| Johny profile | `~/.zee/johny/profile.json` |
| Data | Location |
| ----------------- | ------------------------------- |
| Johny profile | `~/.zee/johny/profile.json` |
| Stanley portfolio | `~/.zee/stanley/portfolio.json` |
| Zee memories | `~/.zee/zee/memories.json` |
| Credentials | `~/.zee/credentials/` |
| Zee memories | `~/.zee/zee/memories.json` |
| Credentials | `~/.zee/credentials/` |
## Running Processes & Binary Updates
> ⚠️ **CRITICAL: Read `docs/OPS.md` before debugging fixes that "don't take effect"**
>
> The #1 cause of confusion is **not knowing which binary is running**:
>
> - **Dev mode** (`bun run dev`): Uses source files directly, restart takes effect
> - **Production** (`~/bin/agent-core`): Uses compiled binary, must rebuild + copy + restart
>
> Use `./scripts/reload.sh --status` to see what's running and if source is newer than binary.
### Repository Location
**Source code:** `~/.local/src/agent-core`
This hidden location prevents accidentally running from the repo directory (which can cause config confusion).
### Binary Location
**Installed binary:** `~/bin/agent-core` (also `$AGENT_CORE_BIN`)
**Run from anywhere:** The binary can be launched from any directory. Just `cd` to your project folder and run `agent-core`.
### Rebuilding
**Recommended:** Use the reload script (handles kill, build, copy, restart):
```bash
# Full reload - kill all, rebuild, restart daemon
./scripts/reload.sh
# Just check status (what's running, version info)
./scripts/reload.sh --status
# Restart without rebuilding (config changes only)
./scripts/reload.sh --no-build
```
**Manual method** (if script unavailable):
```bash
# Build from repo
cd ~/.local/src/agent-core/packages/agent-core && bun run build
@@ -264,6 +291,7 @@ cp ~/.local/src/agent-core/packages/agent-core/dist/agent-core-linux-x64/bin/age
```
### Common Processes
When updating the binary, you may encounter "Text file busy". Check for:
```bash
@@ -323,6 +351,7 @@ pgrep -af agent-core
```
**Key Points:**
- **Zee Gateway** = Transport layer only (handles WhatsApp/Telegram/Signal connections)
- **agent-core daemon** = All agent logic, personas, memory, tools
- **Persona routing** = Messages mentioning `@stanley` or `@johny` are routed to those personas
@@ -331,11 +360,13 @@ pgrep -af agent-core
### Running the External Gateway Architecture
1. **Start agent-core daemon with external gateway mode:**
```bash
agent-core daemon --external-gateway --hostname 127.0.0.1 --port 3210
```
2. **Start zee gateway:**
```bash
cd ~/Repositories/personas/zee && pnpm zee gateway
```
@@ -348,6 +379,7 @@ pgrep -af agent-core
### Architecture Decision
Built-in messaging gateways have been **removed** from agent-core to:
1. Avoid duplication with zee gateway (ClawdBot fork)
2. Keep agent-core clean for upstream OpenCode
3. Centralize messaging transport in one place
+15 -15
View File
@@ -22,7 +22,7 @@
},
"packages/agent-core": {
"name": "agent-core",
"version": "0.1.20250112",
"version": "0.1.20260112",
"bin": {
"agent-core": "./bin/agent-core",
},
@@ -121,7 +121,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -169,7 +169,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -198,7 +198,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -225,7 +225,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -249,7 +249,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -273,7 +273,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -302,7 +302,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -331,7 +331,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -347,7 +347,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -367,7 +367,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.1.20250112",
"version": "0.1.20260112",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -378,7 +378,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -391,7 +391,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -430,7 +430,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"zod": "catalog:",
},
@@ -441,7 +441,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.1.20250112",
"version": "0.1.20260112",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+92
View File
@@ -0,0 +1,92 @@
# Agent-Core Bug Tracker
> Last Updated: 2026-01-12
## Fixed in This Session
### 1. ✅ Silent Error Swallowing in Cache Cleanup
- **File:** `src/global/index.ts:74`
- **Issue:** Empty `catch (e) {}` block silently swallowed all errors
- **Fix:** Now only ignores ENOENT (expected), logs other errors
### 2. ✅ EventEmitter Memory Leak Warning
- **File:** `src/bus/global.ts`
- **Issue:** GlobalBus had default 10 max listeners, could trigger memory leak warning with many SSE connections
- **Fix:** Added `GlobalBus.setMaxListeners(100)`
### 3. ✅ topP Parameter Error for Claude Thinking Models
- **File:** `src/session/llm.ts:189-195`
- **Issue:** Undefined topP was being passed to provider APIs, causing errors for Claude thinking models
- **Fix:** Changed to spread-only-if-defined pattern: `...(params.topP !== undefined && { topP: params.topP })`
### 4. ✅ MCP Servers Don't Reconnect After Daemon Restart
- **File:** `src/mcp/index.ts`
- **Issue:** When daemon restarts, MCP connections were lost with no recovery
- **Fix:** Added:
- `isHealthy(name)` - Check if MCP connection is still alive
- `reconnect(name)` - Reconnect a single failed MCP server
- `reconnectAll()` - Reconnect all failed MCP servers
- `healthCheckAndReconnect()` - Health check and reconnect all
- Auto-reconnect on tool fetch failure
- **File:** `src/server/server.ts`
- **Fix:** Added API endpoints:
- `POST /mcp/:name/reconnect` - Reconnect single server
- `POST /mcp/reconnect-all` - Reconnect all failed
- `POST /mcp/health-check` - Health check and reconnect
### 5. ✅ Type Safety Issues Fixed
- **File:** `src/session/processor.ts:234`
- **Issue:** `(value.error as any).toString()`
- **Fix:** `value.error instanceof Error ? value.error.message : String(value.error)`
- **File:** `src/provider/transform.ts:125`
- **Issue:** `(msg.providerOptions as any)?.openaiCompatible`
- **Fix:** Type-safe access with `Record<string, unknown>` and `typeof` check
- **File:** `src/session/prompt.ts:690,692`
- **Issue:** `as any` for AI SDK type compatibility
- **Fix:** Added explanatory comments documenting why assertions are needed
### 6. ✅ Debug Status Command Type Errors
- **File:** `src/cli/cmd/debug/status.ts`
- **Issue:** Incorrect access to `config.provider?.default` and `config.model?.default`
- **Fix:** Correctly parse `config.model` string (format: `provider/model`)
## Known Issues (Not Fixed Yet)
### 1. 🟡 Type Assertions (`as any`) Remaining
- **Count:** ~30 occurrences (reduced from 35+)
- **Risk:** Type safety bypassed in some places
- **Note:** Many are necessary for AI SDK compatibility and now documented
### 2. 🟡 setInterval Without .unref() in Some Places
- **File:** `src/cli/cmd/debug/errors.ts:154`
- **Issue:** `setInterval(checkForNewErrors, 1000)` doesn't call `.unref()`
- **Impact:** Low (only affects `debug errors --follow`, which runs indefinitely by design)
## Testing Notes
To verify fixes:
```bash
# Type check
bun run typecheck
# Rebuild and restart
./scripts/reload.sh
# Check status
./scripts/reload.sh --status
# Or via CLI
agent-core debug status -v
# Test MCP reconnection
curl -X POST http://127.0.0.1:3210/mcp/health-check
```
+243
View File
@@ -0,0 +1,243 @@
# Agent-Core Operations Guide
> **CRITICAL LESSON LEARNED (2026-01-12):** The biggest source of confusion when debugging is **not knowing which binary is running**. Fixes made to source code won't take effect if:
>
> 1. You're running `bun run dev` (dev mode) instead of the compiled binary
> 2. The daemon is still running an old version
> 3. The TUI was started before the binary was updated
## Quick Reference
| Command | Purpose |
| -------------------------------- | ------------------------------------ |
| `./scripts/reload.sh` | Full rebuild, restart daemon, verify |
| `./scripts/reload.sh --status` | Show what's running and diagnostics |
| `./scripts/reload.sh --no-build` | Restart without rebuild |
| `agent-core debug status` | CLI diagnostics (after install) |
## The Two Execution Modes
### 1. Development Mode (`bun run dev`)
```bash
cd ~/.local/src/agent-core/packages/agent-core
bun run dev --print-logs
```
**Characteristics:**
- Runs directly from TypeScript source
- Changes take effect on restart (no build needed)
- Process shows as: `bun run dev --print-logs` or `bun run --conditions=browser ./src/index.ts`
- Useful for rapid iteration
**How to identify:**
```bash
pgrep -af "bun.*print-logs"
```
### 2. Production Mode (Compiled Binary)
```bash
~/bin/agent-core --print-logs # TUI
~/bin/agent-core daemon --port 3210 # Daemon
```
**Characteristics:**
- Runs from compiled binary at `~/bin/agent-core`
- Requires rebuild (`bun run build`) and copy to take effect
- Process shows as: `/home/artur/bin/agent-core`
- What gets deployed and used in production
**How to identify:**
```bash
pgrep -af "/bin/agent-core"
```
## Why Fixes "Don't Take Effect"
### Root Cause Analysis
```
┌─────────────────────────────────────────────────────────────────────────┐
│ WHY FIXES DON'T TAKE EFFECT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ You edit: ~/.local/src/agent-core/packages/agent-core/src/foo.ts │
│ │
│ BUT your TUI is running: │
│ │
│ CASE A: bun run dev (dev mode) │
│ ├── Process: bun run --conditions=browser ./src/index.ts │
│ ├── Uses: Source files directly │
│ └── Fix: Just restart the TUI │
│ │
│ CASE B: ~/bin/agent-core (compiled binary) │
│ ├── Process: /home/artur/bin/agent-core │
│ ├── Uses: Bundled code from WHEN IT WAS BUILT │
│ └── Fix: Must rebuild, copy, then restart │
│ │
│ CASE C: Daemon is separate │
│ ├── TUI connects to daemon via HTTP │
│ ├── Daemon runs its own bundled code │
│ └── Fix: Must restart daemon too │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### Common Mistakes
| Mistake | Why It Happens | Fix |
| -------------------------------------- | ------------------------------------- | ----------------------------------- |
| Edit source, but TUI uses old code | Running compiled binary, not dev mode | Run `./scripts/reload.sh` |
| Kill daemon but fixes still don't work | TUI has its own embedded code | Kill TUI too, restart everything |
| Binary says "Text file busy" | Process still using the file | Kill ALL agent-core processes first |
| Version mismatch TUI vs daemon | Started at different times | Restart both from same build |
## The Reload Script
Located at: `~/.local/src/agent-core/scripts/reload.sh`
### What It Does
1. **Kills ALL agent-core processes** (daemon, TUI binary, AND dev mode)
2. **Rebuilds** from source (unless `--no-build`)
3. **Copies** new binary to `~/bin/agent-core`
4. **Starts daemon** (unless `--no-daemon`)
5. **Verifies** everything is working
### Usage
```bash
# Full reload (recommended after code changes)
./scripts/reload.sh
# Just check status
./scripts/reload.sh --status
# Restart without rebuilding (for config changes only)
./scripts/reload.sh --no-build
# Rebuild but don't start daemon
./scripts/reload.sh --no-daemon
```
### Status Output Explained
```
═══════════════════════════════════════════════════════════════
AGENT-CORE STATUS
═══════════════════════════════════════════════════════════════
Binary: /home/artur/bin/agent-core
[ OK ] Exists (modified: 2026-01-12 20:17:35) ← When binary was last updated
Processes:
[ OK ] Daemon: PID 2454325 ← Daemon running
[ OK ] TUI (dev): PID 656637 ← Dev mode TUI (bun run dev)
[ OK ] TUI: PID 123456 ← Binary TUI
Daemon API: http://127.0.0.1:3210
[ OK ] Healthy (version: 0.0.0-main-202601121917) ← Daemon version
Tool directories:
[ OK ] /home/artur/.config/agent-core/tool (1 tools)
- canvas.ts ← Custom tools loaded
Source timestamps:
[ OK ] transform.ts (19:05:23) ← Source file modification times
[ WARN ] llm.ts (19:17:00) - NEWER than binary, rebuild needed! ← Source newer than binary!
```
## Process Hierarchy
```
When running bun run dev:
shell
└── bun run dev --print-logs (PID: 656637)
└── bun run ./src/index.ts (PID: 656638, child)
When running compiled binary:
shell
└── /home/artur/bin/agent-core --print-logs (PID: 123456)
Daemon (always compiled binary):
nohup
└── /home/artur/bin/agent-core daemon (PID: 234567)
└── (gateway subprocess if --gateway) (PID: 234568)
```
## Debugging Checklist
When a fix doesn't take effect, check in order:
- [ ] **1. Which mode am I running?**
```bash
pgrep -af "bun.*print-logs" # Dev mode
pgrep -af "/bin/agent-core" # Compiled binary
```
- [ ] **2. What version is the daemon?**
```bash
curl -s http://127.0.0.1:3210/global/health | jq .version
```
- [ ] **3. When was the binary built?**
```bash
ls -la ~/bin/agent-core
```
- [ ] **4. When was the source file modified?**
```bash
ls -la ~/.local/src/agent-core/packages/agent-core/src/path/to/file.ts
```
- [ ] **5. Is source newer than binary?**
```bash
./scripts/reload.sh --status # Shows warnings for newer source files
```
- [ ] **6. Nuclear option - kill everything and restart**
```bash
./scripts/reload.sh
```
## Location Reference
| What | Path |
| ----------------- | -------------------------------------------------------------------------------------- |
| Source repository | `~/.local/src/agent-core` |
| Package source | `~/.local/src/agent-core/packages/agent-core/src/` |
| Compiled binary | `~/bin/agent-core` |
| Build output | `~/.local/src/agent-core/packages/agent-core/dist/agent-core-linux-x64/bin/agent-core` |
| Reload script | `~/.local/src/agent-core/scripts/reload.sh` |
| Config directory | `~/.config/agent-core/` |
| Custom tools | `~/.config/agent-core/tool/` |
| Daemon logs | `/tmp/agent-core-daemon.log` |
## MCP Servers Note
MCP servers (memory, calendar, portfolio) connect when the **daemon** starts. If they show "Connection closed":
1. The daemon was restarted and MCP connections were lost
2. The TUI still has stale connection handles
3. **Fix:** Restart the TUI after restarting the daemon
## Version Strings
Version format: `0.0.0-main-YYYYMMDDHHMM`
- Built from git commit at build time
- Can identify exactly when binary was built
- Compare TUI version vs daemon version to spot mismatches
+4
View File
@@ -8,3 +8,7 @@ export const GlobalBus = new EventEmitter<{
},
]
}>()
// Prevent memory leak warning during long daemon sessions with many SSE connections
GlobalBus.setMaxListeners(100)
@@ -12,6 +12,7 @@ import { RipgrepCommand } from "./ripgrep"
import { ScrapCommand } from "./scrap"
import { SkillCommand } from "./skill"
import { SnapshotCommand } from "./snapshot"
import { StatusCommand } from "./status"
import { TasksCommand } from "./tasks"
import { AgentCommand } from "./agent"
@@ -33,6 +34,7 @@ export const DebugCommand = cmd({
.command(SnapshotCommand)
.command(TasksCommand)
.command(AgentCommand)
.command(StatusCommand)
.command(PathsCommand)
.command(FlagsCommand)
.command({
@@ -0,0 +1,352 @@
import { cmd } from "../cmd"
import { bootstrap } from "../../bootstrap"
import { Installation } from "../../../installation"
import { Config } from "../../../config/config"
import { Global } from "../../../global"
import { Flag } from "../../../flag/flag"
import fs from "fs/promises"
import path from "path"
export const StatusCommand = cmd({
command: "status",
describe: "show agent-core system status and diagnostics",
builder: (yargs) =>
yargs
.option("json", {
type: "boolean",
default: false,
describe: "output as JSON",
})
.option("verbose", {
alias: "v",
type: "boolean",
default: false,
describe: "show verbose details",
}),
async handler(args) {
await bootstrap(process.cwd(), async () => {
const status = await collectStatus(args.verbose)
if (args.json) {
console.log(JSON.stringify(status, null, 2))
return
}
printStatus(status, args.verbose)
})
},
})
interface SystemStatus {
version: string
binary: {
path: string
exists: boolean
modifiedAt?: string
modifiedTs?: number
}
daemon: {
running: boolean
pid?: number
port?: number
version?: string
healthy?: boolean
error?: string
}
processes: Array<{
pid: number
type: string
cmd: string
}>
tools: {
directories: string[]
loaded: string[]
}
config: {
directories: string[]
provider?: string
model?: string
}
sources: Array<{
file: string
modifiedAt: string
modifiedTs: number
newerThanBinary: boolean
}>
issues: string[]
}
async function collectStatus(verbose: boolean): Promise<SystemStatus> {
const status: SystemStatus = {
version: Installation.VERSION,
binary: {
path: process.execPath,
exists: false,
},
daemon: {
running: false,
},
processes: [],
tools: {
directories: [],
loaded: [],
},
config: {
directories: [],
},
sources: [],
issues: [],
}
// Binary info
const binaryPath = path.join(process.env.HOME || "", "bin", "agent-core")
try {
const stat = await fs.stat(binaryPath)
status.binary = {
path: binaryPath,
exists: true,
modifiedAt: stat.mtime.toISOString(),
modifiedTs: stat.mtime.getTime(),
}
} catch {
status.binary = { path: binaryPath, exists: false }
status.issues.push("Binary not found at ~/bin/agent-core")
}
// Check for running processes
try {
const { execSync } = await import("child_process")
const psOutput = execSync("pgrep -af agent-core 2>/dev/null || true", { encoding: "utf-8" })
const lines = psOutput.trim().split("\n").filter(Boolean)
for (const line of lines) {
const match = line.match(/^(\d+)\s+(.*)$/)
if (!match) continue
const pid = parseInt(match[1])
const cmd = match[2]
// Skip ourselves and grep
if (cmd.includes("pgrep") || cmd.includes("status")) continue
let type = "unknown"
if (cmd.includes("daemon")) type = "daemon"
else if (cmd.includes("print-logs") || cmd.match(/\/bin\/agent-core$/)) type = "tui"
status.processes.push({ pid, type, cmd })
if (type === "daemon") {
status.daemon.running = true
status.daemon.pid = pid
}
}
} catch {
// Ignore process check errors
}
// Daemon health check
const daemonPort = parseInt(process.env.AGENT_CORE_PORT || "3210")
const daemonHost = process.env.AGENT_CORE_HOST || "127.0.0.1"
status.daemon.port = daemonPort
try {
const response = await fetch(`http://${daemonHost}:${daemonPort}/global/health`, {
signal: AbortSignal.timeout(2000),
})
if (response.ok) {
const health = (await response.json()) as { healthy: boolean; version: string }
status.daemon.healthy = health.healthy
status.daemon.version = health.version
// Check version mismatch
if (health.version !== Installation.VERSION) {
status.issues.push(
`Daemon version (${health.version}) differs from binary (${Installation.VERSION}) - restart needed`,
)
}
}
} catch (e) {
status.daemon.healthy = false
status.daemon.error = e instanceof Error ? e.message : String(e)
if (status.daemon.running) {
status.issues.push("Daemon process running but health check failed")
}
}
// Tool directories
const configDirs = await Config.directories()
status.config.directories = configDirs
for (const dir of configDirs) {
const toolDir = path.join(dir, "tool")
try {
const files = await fs.readdir(toolDir)
const tsFiles = files.filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
if (tsFiles.length > 0) {
status.tools.directories.push(toolDir)
status.tools.loaded.push(...tsFiles.map((f) => path.join(toolDir, f)))
}
} catch {
// Tool directory doesn't exist, that's fine
}
}
// Check source file timestamps (for rebuild detection)
if (verbose && status.binary.modifiedTs) {
const srcRoot = path.join(process.env.HOME || "", ".local", "src", "agent-core", "packages", "agent-core", "src")
const keyFiles = ["provider/transform.ts", "provider/provider.ts", "server/server.ts"]
for (const file of keyFiles) {
const fullPath = path.join(srcRoot, file)
try {
const stat = await fs.stat(fullPath)
const newerThanBinary = stat.mtime.getTime() > status.binary.modifiedTs!
status.sources.push({
file,
modifiedAt: stat.mtime.toISOString(),
modifiedTs: stat.mtime.getTime(),
newerThanBinary,
})
if (newerThanBinary) {
status.issues.push(`Source ${file} is newer than binary - rebuild needed`)
}
} catch {
// File doesn't exist
}
}
}
// Config info
try {
const config = await Config.get()
// config.model is a string in format "provider/model", e.g. "anthropic/claude-2"
if (config.model) {
const [providerPart, modelPart] = config.model.split("/")
status.config.provider = providerPart
status.config.model = modelPart || config.model
}
} catch {
// Ignore config errors
}
return status
}
function printStatus(status: SystemStatus, verbose: boolean) {
const GREEN = "\x1b[32m"
const RED = "\x1b[31m"
const YELLOW = "\x1b[33m"
const BLUE = "\x1b[34m"
const DIM = "\x1b[2m"
const RESET = "\x1b[0m"
const ok = (s: string) => `${GREEN}${RESET} ${s}`
const err = (s: string) => `${RED}${RESET} ${s}`
const warn = (s: string) => `${YELLOW}!${RESET} ${s}`
console.log("")
console.log("═══════════════════════════════════════════════════════════════")
console.log(" AGENT-CORE STATUS")
console.log("═══════════════════════════════════════════════════════════════")
console.log("")
// Version
console.log(`${BLUE}Version:${RESET} ${status.version}`)
console.log("")
// Binary
console.log(`${BLUE}Binary:${RESET}`)
if (status.binary.exists) {
console.log(` ${ok(`${status.binary.path}`)}`)
if (status.binary.modifiedAt) {
console.log(` ${DIM}Modified: ${new Date(status.binary.modifiedAt).toLocaleString()}${RESET}`)
}
} else {
console.log(` ${err("Not found at " + status.binary.path)}`)
}
console.log("")
// Processes
console.log(`${BLUE}Processes:${RESET}`)
if (status.processes.length === 0) {
console.log(` ${warn("No agent-core processes running")}`)
} else {
for (const proc of status.processes) {
const typeLabel = proc.type.charAt(0).toUpperCase() + proc.type.slice(1)
console.log(` ${ok(`${typeLabel}: PID ${proc.pid}`)}`)
if (verbose) {
console.log(` ${DIM}${proc.cmd}${RESET}`)
}
}
}
console.log("")
// Daemon
console.log(`${BLUE}Daemon:${RESET}`)
console.log(` Port: ${status.daemon.port}`)
if (status.daemon.healthy) {
console.log(` ${ok(`Healthy (version: ${status.daemon.version})`)}`)
} else if (status.daemon.running) {
console.log(` ${warn("Process running but not healthy")}`)
if (status.daemon.error) {
console.log(` ${DIM}Error: ${status.daemon.error}${RESET}`)
}
} else {
console.log(` ${err("Not running")}`)
}
console.log("")
// Tools
console.log(`${BLUE}Tools:${RESET}`)
if (status.tools.directories.length === 0) {
console.log(` ${warn("No tool directories found")}`)
} else {
for (const dir of status.tools.directories) {
const tools = status.tools.loaded.filter((t) => t.startsWith(dir))
console.log(` ${ok(dir)} (${tools.length} tools)`)
if (verbose) {
for (const tool of tools) {
console.log(` ${DIM}- ${path.basename(tool)}${RESET}`)
}
}
}
}
console.log("")
// Source timestamps (verbose only)
if (verbose && status.sources.length > 0) {
console.log(`${BLUE}Sources:${RESET}`)
for (const src of status.sources) {
const modified = new Date(src.modifiedAt).toLocaleTimeString()
if (src.newerThanBinary) {
console.log(` ${warn(`${src.file} (${modified}) - NEWER than binary`)}`)
} else {
console.log(` ${ok(`${src.file} (${modified})`)}`)
}
}
console.log("")
}
// Issues
if (status.issues.length > 0) {
console.log(`${BLUE}Issues:${RESET}`)
for (const issue of status.issues) {
console.log(` ${err(issue)}`)
}
console.log("")
}
console.log("═══════════════════════════════════════════════════════════════")
// Quick fix suggestions
if (status.issues.length > 0) {
console.log("")
console.log(`${BLUE}Quick fixes:${RESET}`)
if (status.issues.some((i) => i.includes("rebuild"))) {
console.log(" Rebuild: ~/.local/src/agent-core/scripts/reload.sh")
}
if (status.issues.some((i) => i.includes("restart"))) {
console.log(" Restart: ~/.local/src/agent-core/scripts/reload.sh --no-build")
}
}
}
+7 -1
View File
@@ -71,6 +71,12 @@ if (version !== CACHE_VERSION) {
}),
),
)
} catch (e) {}
} catch (e) {
// Ignore ENOENT (cache dir doesn't exist) - expected on first run
// Log other errors for debugging
if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
console.error("[agent-core] Cache cleanup failed:", e)
}
}
await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION)
}
+148 -9
View File
@@ -539,6 +539,129 @@ export namespace MCP {
s.status[name] = { status: "disabled" }
}
/**
* Check if an MCP server connection is healthy by attempting to list tools.
* Returns true if connected and responsive, false otherwise.
*/
export async function isHealthy(name: string): Promise<boolean> {
const s = await state()
const client = s.clients[name]
if (!client) {
return false
}
if (s.status[name]?.status !== "connected") {
return false
}
try {
// Attempt a simple operation to verify connection is alive
await withTimeout(client.listTools(), 5000)
return true
} catch (e) {
log.warn("MCP health check failed", { name, error: e instanceof Error ? e.message : String(e) })
return false
}
}
/**
* Reconnect to an MCP server that has failed or disconnected.
* Returns the new status after reconnection attempt.
*/
export async function reconnect(name: string): Promise<Status> {
const cfg = await Config.get()
const mcpConfig = cfg.mcp?.[name]
if (!mcpConfig) {
log.error("MCP config not found for reconnect", { name })
return { status: "failed", error: "MCP config not found" }
}
if (!isMcpConfigured(mcpConfig)) {
log.error("MCP config invalid for reconnect", { name })
return { status: "failed", error: "Invalid MCP configuration" }
}
// Close existing client if any
const s = await state()
const existingClient = s.clients[name]
if (existingClient) {
await existingClient.close().catch((error) => {
log.debug("Failed to close existing MCP client during reconnect", { name, error })
})
delete s.clients[name]
}
log.info("Attempting MCP reconnection", { name })
// Create new connection
const result = await create(name, { ...mcpConfig, enabled: true })
if (!result) {
s.status[name] = { status: "failed", error: "Unknown error during reconnection" }
return s.status[name]
}
s.status[name] = result.status
if (result.mcpClient) {
s.clients[name] = result.mcpClient
log.info("MCP reconnection successful", { name })
} else {
log.warn("MCP reconnection failed", { name, status: result.status })
}
return result.status
}
/**
* Attempt to reconnect all failed MCP servers.
* Returns a map of server names to their new statuses.
*/
export async function reconnectAll(): Promise<Record<string, Status>> {
const s = await state()
const results: Record<string, Status> = {}
for (const [name, currentStatus] of Object.entries(s.status)) {
if (currentStatus.status === "failed") {
results[name] = await reconnect(name)
} else {
results[name] = currentStatus
}
}
return results
}
/**
* Check health of all connected MCPs and reconnect any that have failed.
* This can be called periodically or after daemon restart.
*/
export async function healthCheckAndReconnect(): Promise<Record<string, Status>> {
const s = await state()
const results: Record<string, Status> = {}
for (const [name, currentStatus] of Object.entries(s.status)) {
if (currentStatus.status === "connected") {
// Check if still healthy
const healthy = await isHealthy(name)
if (!healthy) {
log.warn("MCP connection unhealthy, attempting reconnect", { name })
results[name] = await reconnect(name)
} else {
results[name] = currentStatus
}
} else if (currentStatus.status === "failed") {
// Attempt to reconnect failed connections
results[name] = await reconnect(name)
} else {
results[name] = currentStatus
}
}
return results
}
export async function tools() {
const result: Record<string, Tool> = {}
const s = await state()
@@ -550,23 +673,39 @@ export namespace MCP {
continue
}
const toolsResult = await client.listTools().catch((e) => {
log.error("failed to get tools", { clientName, error: e.message })
const failedStatus = {
status: "failed" as const,
error: e instanceof Error ? e.message : String(e),
}
s.status[clientName] = failedStatus
delete s.clients[clientName]
let toolsResult = await client.listTools().catch((e) => {
log.warn("failed to get tools, will attempt reconnect", { clientName, error: e.message })
return undefined
})
// If initial fetch failed, attempt reconnection
if (!toolsResult) {
const reconnectStatus = await reconnect(clientName)
if (reconnectStatus.status === "connected") {
// Try again with new client
const newClient = s.clients[clientName]
if (newClient) {
toolsResult = await newClient.listTools().catch((e) => {
log.error("failed to get tools after reconnect", { clientName, error: e.message })
const failedStatus = {
status: "failed" as const,
error: e instanceof Error ? e.message : String(e),
}
s.status[clientName] = failedStatus
delete s.clients[clientName]
return undefined
})
}
}
}
if (!toolsResult) {
continue
}
for (const mcpTool of toolsResult.tools) {
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client)
result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, s.clients[clientName] ?? client)
}
}
return result
+2 -2
View File
@@ -847,7 +847,7 @@ export namespace Provider {
},
status: "active",
headers: {},
options: { topP: 0.95 }, // Claude thinking requires topP >= 0.95
options: {}, // topP unset - Claude thinking requires topP >= 0.95 or unset
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 200000, output: 64000 },
capabilities: {
@@ -903,7 +903,7 @@ export namespace Provider {
},
status: "active",
headers: {},
options: { topP: 0.95 }, // Claude thinking requires topP >= 0.95
options: {}, // topP unset - Claude thinking requires topP >= 0.95 or unset
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 200000, output: 64000 },
capabilities: {
@@ -116,13 +116,15 @@ export namespace ProviderTransform {
// Include reasoning_content directly on the message for all assistant messages
if (reasoningText) {
// Type assertion: providerOptions is Record<string, unknown> but we need to spread existing openaiCompatible options
const existingOptions = (msg.providerOptions as Record<string, unknown>)?.openaiCompatible
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...(msg.providerOptions as any)?.openaiCompatible,
...(typeof existingOptions === "object" ? existingOptions : {}),
reasoning_content: reasoningText,
},
},
@@ -254,14 +256,14 @@ export namespace ProviderTransform {
export function topP(model: Provider.Model) {
const id = model.id.toLowerCase()
// Claude thinking models (extended thinking) require topP >= 0.95 OR unset
// Return undefined to leave it unset and let Claude use its default
if (id.includes("claude") && id.includes("thinking")) return undefined
if (id.includes("qwen")) return 1
if (id.includes("minimax-m2")) {
return 0.95
}
if (id.includes("gemini")) return 0.95
// Claude thinking models (extended thinking) require topP >= 0.95
// This applies to any provider serving Claude thinking models (Antigravity, Vertex, etc.)
if (id.includes("claude") && id.includes("thinking")) return 0.95
return undefined
}
+90 -10
View File
@@ -2010,10 +2010,18 @@ export namespace Server {
c.status(200)
c.header("Content-Type", "application/json")
return stream(c, async (stream) => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msg = await SessionPrompt.prompt({ ...body, sessionID })
stream.write(JSON.stringify(msg))
try {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msg = await SessionPrompt.prompt({ ...body, sessionID })
stream.write(JSON.stringify(msg))
} catch (err) {
// Ensure we always write valid JSON even on error
// This prevents "Unexpected end of JSON input" on the client
const errorMsg = err instanceof Error ? err.message : String(err)
Log.Default.error("session.prompt stream error", { error: errorMsg })
stream.write(JSON.stringify({ error: errorMsg, info: null, parts: [] }))
}
})
},
)
@@ -2039,13 +2047,17 @@ export namespace Server {
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
async (c) => {
c.status(204)
c.header("Content-Type", "application/json")
return stream(c, async () => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
SessionPrompt.prompt({ ...body, sessionID })
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
// Fire-and-forget: start the prompt but don't wait for it
SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
Log.Default.error("session.prompt_async error", {
error: err instanceof Error ? err.message : String(err),
sessionID,
})
})
// 204 No Content - no body, no Content-Type header needed
return c.body(null, 204)
},
)
.post(
@@ -2996,6 +3008,74 @@ export namespace Server {
return c.json(true)
},
)
.post(
"/mcp/:name/reconnect",
describeRoute({
summary: "Reconnect an MCP server",
description: "Attempt to reconnect to an MCP server that has failed or disconnected.",
operationId: "mcp.reconnect",
responses: {
200: {
description: "MCP reconnection result",
content: {
"application/json": {
schema: resolver(MCP.Status),
},
},
},
},
}),
validator("param", z.object({ name: z.string() })),
async (c) => {
const { name } = c.req.valid("param")
const status = await MCP.reconnect(name)
return c.json(status)
},
)
.post(
"/mcp/reconnect-all",
describeRoute({
summary: "Reconnect all failed MCP servers",
description: "Attempt to reconnect to all MCP servers that are in a failed state.",
operationId: "mcp.reconnectAll",
responses: {
200: {
description: "MCP reconnection results",
content: {
"application/json": {
schema: resolver(z.record(z.string(), MCP.Status)),
},
},
},
},
}),
async (c) => {
const results = await MCP.reconnectAll()
return c.json(results)
},
)
.post(
"/mcp/health-check",
describeRoute({
summary: "Health check and reconnect MCP servers",
description: "Check health of all connected MCPs and attempt to reconnect any that have failed.",
operationId: "mcp.healthCheckAndReconnect",
responses: {
200: {
description: "MCP health check results",
content: {
"application/json": {
schema: resolver(z.record(z.string(), MCP.Status)),
},
},
},
},
}),
async (c) => {
const results = await MCP.healthCheckAndReconnect()
return c.json(results)
},
)
.get(
"/experimental/resource",
describeRoute({
+7 -6
View File
@@ -186,12 +186,13 @@ export namespace LLM {
toolName: "invalid",
}
},
temperature: params.temperature,
topP: params.topP,
topK: params.topK,
frequencyPenalty: params.frequencyPenalty,
presencePenalty: params.presencePenalty,
seed: params.seed,
// Only include sampling parameters if defined - some providers (Google) reject undefined values
...(params.temperature !== undefined && { temperature: params.temperature }),
...(params.topP !== undefined && { topP: params.topP }),
...(params.topK !== undefined && { topK: params.topK }),
...(params.frequencyPenalty !== undefined && { frequencyPenalty: params.frequencyPenalty }),
...(params.presencePenalty !== undefined && { presencePenalty: params.presencePenalty }),
...(params.seed !== undefined && { seed: params.seed }),
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
tools,
+1 -1
View File
@@ -231,7 +231,7 @@ export namespace SessionProcessor {
state: {
status: "error",
input: value.input,
error: (value.error as any).toString(),
error: value.error instanceof Error ? value.error.message : String(value.error),
time: {
start: match.state.time.start,
end: Date.now(),
@@ -687,8 +687,10 @@ export namespace SessionPrompt {
for (const item of await ToolRegistry.tools(input.model.providerID, input.agent)) {
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
tools[item.id] = tool({
// Type assertion needed: AI SDK tool() requires specific string literal types, but our dynamic IDs are string variables
id: item.id as any,
description: item.description,
// Type assertion needed: AI SDK jsonSchema() expects JSONSchema7 but zod's toJSONSchema output has subtle type differences
inputSchema: jsonSchema(schema as any),
async execute(args, options) {
const ctx = context(args, options)
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env bash
#
# agent-core reload script
# Usage: ./scripts/reload.sh [--no-build] [--no-daemon] [--status]
#
# This script:
# 1. Kills all agent-core processes
# 2. Rebuilds from source (unless --no-build)
# 3. Copies binary to ~/bin/agent-core
# 4. Starts daemon (unless --no-daemon)
# 5. Verifies everything is working
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
PKG_DIR="$REPO_ROOT/packages/agent-core"
BINARY_SRC="$PKG_DIR/dist/agent-core-linux-x64/bin/agent-core"
BINARY_DST="$HOME/bin/agent-core"
DAEMON_PORT="${AGENT_CORE_PORT:-3210}"
DAEMON_HOST="${AGENT_CORE_HOST:-127.0.0.1}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log() { echo -e "${BLUE}[reload]${NC} $*"; }
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
warn() { echo -e "${YELLOW}[ WARN ]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*"; }
# Parse args
NO_BUILD=false
NO_DAEMON=false
STATUS_ONLY=false
for arg in "$@"; do
case $arg in
--no-build) NO_BUILD=true ;;
--no-daemon) NO_DAEMON=true ;;
--status) STATUS_ONLY=true ;;
--help|-h)
echo "Usage: $0 [--no-build] [--no-daemon] [--status]"
echo ""
echo "Options:"
echo " --no-build Skip rebuilding (just restart)"
echo " --no-daemon Don't start daemon after reload"
echo " --status Show status and diagnostics only"
exit 0
;;
esac
done
# Status/diagnostics function
show_status() {
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " AGENT-CORE STATUS"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# Binary info
echo "Binary: $BINARY_DST"
if [[ -f "$BINARY_DST" ]]; then
local mod_time=$(stat -c "%Y" "$BINARY_DST" 2>/dev/null || stat -f "%m" "$BINARY_DST" 2>/dev/null)
local mod_date=$(date -d "@$mod_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$mod_time" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
ok "Exists (modified: $mod_date)"
else
err "Not found"
fi
echo ""
# Running processes
echo "Processes:"
local procs=$(pgrep -af "agent-core|bun.*print-logs" 2>/dev/null | grep -v "reload.sh" | grep -v "pgrep" || true)
if [[ -n "$procs" ]]; then
echo "$procs" | while read -r line; do
local pid=$(echo "$line" | awk '{print $1}')
local cmd=$(echo "$line" | cut -d' ' -f2-)
if [[ "$cmd" == *"daemon"* ]]; then
ok "Daemon: PID $pid"
elif [[ "$cmd" == *"bun"*"print-logs"* ]]; then
ok "TUI (dev): PID $pid"
elif [[ "$cmd" == *"print-logs"* ]] || [[ "$cmd" == *"/bin/agent-core" ]]; then
ok "TUI: PID $pid"
else
echo " Other: PID $pid - $cmd"
fi
done
else
warn "No agent-core processes running"
fi
echo ""
# Daemon health
echo "Daemon API: http://$DAEMON_HOST:$DAEMON_PORT"
local health=$(curl -sf "http://$DAEMON_HOST:$DAEMON_PORT/global/health" 2>/dev/null || echo "")
if [[ -n "$health" ]]; then
local version=$(echo "$health" | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
ok "Healthy (version: $version)"
else
warn "Not responding"
fi
echo ""
# Tool directories
echo "Tool directories:"
for dir in "$HOME/.config/agent-core/tool" "$REPO_ROOT/.agent-core/tool"; do
if [[ -d "$dir" ]]; then
local count=$(ls -1 "$dir"/*.ts 2>/dev/null | wc -l || echo 0)
ok "$dir ($count tools)"
ls -1 "$dir"/*.ts 2>/dev/null | while read -r f; do
echo " - $(basename "$f")"
done
else
echo " $dir (not found)"
fi
done
echo ""
# Source vs binary timestamps
echo "Source timestamps:"
local src_files=(
"$PKG_DIR/src/provider/transform.ts"
"$PKG_DIR/src/provider/provider.ts"
"$PKG_DIR/src/server/server.ts"
"$PKG_DIR/src/session/llm.ts"
)
local binary_time=$(stat -c "%Y" "$BINARY_DST" 2>/dev/null || echo 0)
for src in "${src_files[@]}"; do
if [[ -f "$src" ]]; then
local src_time=$(stat -c "%Y" "$src" 2>/dev/null || echo 0)
local src_date=$(date -d "@$src_time" "+%H:%M:%S" 2>/dev/null || date -r "$src_time" "+%H:%M:%S" 2>/dev/null)
local name=$(basename "$src")
if [[ $src_time -gt $binary_time ]]; then
warn "$name ($src_date) - NEWER than binary, rebuild needed!"
else
ok "$name ($src_date)"
fi
fi
done
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
if $STATUS_ONLY; then
show_status
exit 0
fi
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " AGENT-CORE RELOAD"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# Step 1: Kill all agent-core processes
log "Stopping all agent-core processes..."
# Kill daemon
if pgrep -f "agent-core daemon" > /dev/null 2>&1; then
pkill -9 -f "agent-core daemon" 2>/dev/null
ok "Killed daemon"
else
warn "No daemon to kill"
fi
# Kill TUI processes (compiled binary)
if pgrep -f "agent-core.*print-logs" > /dev/null 2>&1; then
pkill -9 -f "agent-core.*print-logs" 2>/dev/null
ok "Killed TUI (binary)"
else
warn "No binary TUI to kill"
fi
# Kill dev mode TUI (bun run dev)
if pgrep -f "bun.*print-logs" > /dev/null 2>&1; then
pkill -9 -f "bun.*print-logs" 2>/dev/null
ok "Killed TUI (dev mode)"
else
warn "No dev TUI to kill"
fi
# Give processes time to die
sleep 1
# Verify nothing is running
remaining=$(pgrep -f "agent-core" 2>/dev/null | grep -v $$ | grep -v "reload" || true)
if [[ -n "$remaining" ]]; then
warn "Some processes still running, force killing..."
echo "$remaining" | xargs -r kill -9 2>/dev/null || true
sleep 1
fi
ok "All processes stopped"
# Step 2: Rebuild
if ! $NO_BUILD; then
log "Rebuilding agent-core..."
cd "$PKG_DIR"
if bun run build 2>&1 | tail -5; then
ok "Build complete"
else
err "Build failed!"
exit 1
fi
else
warn "Skipping build (--no-build)"
fi
# Step 3: Copy binary
log "Installing binary..."
if [[ -f "$BINARY_SRC" ]]; then
cp "$BINARY_SRC" "$BINARY_DST"
chmod +x "$BINARY_DST"
ok "Installed to $BINARY_DST"
else
err "Binary not found at $BINARY_SRC"
exit 1
fi
# Step 4: Start daemon
if ! $NO_DAEMON; then
log "Starting daemon..."
nohup "$BINARY_DST" daemon --hostname "$DAEMON_HOST" --port "$DAEMON_PORT" --gateway > /tmp/agent-core-daemon.log 2>&1 &
DAEMON_PID=$!
sleep 2
# Verify daemon started
if kill -0 "$DAEMON_PID" 2>/dev/null; then
# Check health endpoint
for i in {1..5}; do
health=$(curl -sf "http://$DAEMON_HOST:$DAEMON_PORT/global/health" 2>/dev/null || echo "")
if [[ -n "$health" ]]; then
version=$(echo "$health" | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
ok "Daemon started (PID: $DAEMON_PID, version: $version)"
break
fi
sleep 1
done
if [[ -z "$health" ]]; then
warn "Daemon started but health check failed"
fi
else
err "Daemon failed to start! Check /tmp/agent-core-daemon.log"
tail -20 /tmp/agent-core-daemon.log
exit 1
fi
else
warn "Skipping daemon start (--no-daemon)"
fi
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " RELOAD COMPLETE"
echo "═══════════════════════════════════════════════════════════════"
show_status
+1 -1