docs: update AGENTS.md hierarchy with openclaw, runtime-fallback, skill-mcp-manager
- Add src/openclaw/AGENTS.md: bidirectional Discord/Telegram/webhook integration - Add src/hooks/runtime-fallback/AGENTS.md: reactive provider error recovery - Add src/features/skill-mcp-manager/AGENTS.md: tier-3 MCP lifecycle - Update root AGENTS.md: refresh commit hash, add openclaw/IntentGate/Hashline refs - Fix src/features/AGENTS.md: skill-mcp-manager file count 14→18, complexity MEDIUM→HIGH Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -15,8 +15,8 @@ Standalone feature modules wired into plugin/ layer. Each is self-contained with
|
||||
| **tmux-subagent** | 34 | HIGH | Tmux pane management, grid planning, session orchestration |
|
||||
| **mcp-oauth** | 18 | HIGH | OAuth 2.0 + PKCE + DCR (RFC 7591) for MCP servers |
|
||||
| **builtin-skills** | 17 | LOW | 8 skills: git-master, playwright, playwright-cli, agent-browser, dev-browser, frontend-ui-ux, review-work, ai-slop-remover |
|
||||
| **skill-mcp-manager** | 14 | MEDIUM | MCP client lifecycle per session (stdio + HTTP) |
|
||||
| **claude-code-plugin-loader** | 10 | MEDIUM | Unified plugin discovery from .opencode/plugins/ |
|
||||
| **skill-mcp-manager** | 18 | HIGH | Tier-3 MCP client lifecycle per session (stdio + HTTP + OAuth step-up) |
|
||||
| **claude-code-plugin-loader** | 15 | MEDIUM | Unified plugin discovery from .opencode/plugins/ |
|
||||
| **builtin-commands** | 11 | LOW | Command templates: refactor, init-deep, handoff, etc. |
|
||||
| **claude-tasks** | 7 | MEDIUM | Task schema + file storage + OpenCode todo sync |
|
||||
| **claude-code-mcp-loader** | 6 | MEDIUM | .mcp.json loading with ${VAR} env expansion |
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# src/features/skill-mcp-manager/ — Skill-Embedded MCP Client Lifecycle
|
||||
|
||||
**Generated:** 2026-04-09
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
18 files. Manages **tier 3** of the MCP system: skill-embedded MCP servers declared in SKILL.md YAML frontmatter. Per-session client isolation, dual transport (stdio + HTTP), OAuth 2.0 with step-up authentication, idle cleanup.
|
||||
|
||||
## THREE-TIER MCP CONTEXT
|
||||
|
||||
| Tier | Manager | Scope |
|
||||
|------|---------|-------|
|
||||
| 1. Built-in | `createBuiltinMcps()` (src/mcp/) | Global, 3 remote HTTP |
|
||||
| 2. Claude Code | `claude-code-mcp-loader` (src/features/) | From `.mcp.json` |
|
||||
| 3. **Skill-embedded** | **`SkillMcpManager` (this module)** | **Per-session, from SKILL.md YAML** |
|
||||
|
||||
## CLIENT KEY FORMAT
|
||||
|
||||
```
|
||||
${sessionID}:${skillName}:${serverName}
|
||||
```
|
||||
|
||||
Enables: per-session isolation, same skill usable in multiple sessions concurrently, multiple servers per skill.
|
||||
|
||||
## DUAL TRANSPORT
|
||||
|
||||
| Type | File | Backend |
|
||||
|------|------|---------|
|
||||
| **stdio** | `stdio-client.ts` | `StdioClientTransport` (local process) |
|
||||
| **http** | `http-client.ts` | `StreamableHTTPClientTransport` (remote) |
|
||||
|
||||
**Detection** (connection-type.ts): explicit `type` field → URL presence → command presence. Legacy `"sse"` mapped to http.
|
||||
|
||||
## STATE
|
||||
|
||||
```typescript
|
||||
interface SkillMcpManagerState {
|
||||
clients: Map<clientKey, ManagedClient> // Active connections
|
||||
pendingConnections: Map<clientKey, Promise<Client>> // Race prevention
|
||||
disconnectedSessions: Map<sessionID, generation> // Stale connection detection
|
||||
authProviders: Map<url, OAuthProvider> // OAuth state per server
|
||||
inFlightConnections: Map<sessionID, count> // Connection counting
|
||||
}
|
||||
```
|
||||
|
||||
## KEY FILES
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `manager.ts` | `SkillMcpManager` class — main API (getOrCreateClient, disconnectSession, listTools, callTool, etc.) |
|
||||
| `types.ts` | `ManagedStdioClient`, `ManagedHttpClient`, `SkillMcpManagerState`, `ConnectionType` |
|
||||
| `connection.ts` | Client factory with race prevention, retry, env var expansion |
|
||||
| `connection-type.ts` | Detect stdio vs http from config (legacy sse → http) |
|
||||
| `stdio-client.ts` | Stdio transport factory |
|
||||
| `http-client.ts` | HTTP transport factory |
|
||||
| `cleanup.ts` | SIGINT/SIGTERM handlers, idle timer (60s interval, 5min TTL) |
|
||||
| `oauth-handler.ts` | OAuth token management, refresh, step-up (403 scope escalation) |
|
||||
| `env-cleaner.ts` | Filter npm/pnpm/yarn config + 25+ secret patterns (_KEY, _SECRET, _TOKEN) |
|
||||
| `error-redaction.ts` | Redact sensitive data from error messages before logging |
|
||||
|
||||
## LIFECYCLE INTEGRATION
|
||||
|
||||
**Hook**: `src/plugin/event.ts` on `session.deleted`:
|
||||
```typescript
|
||||
await managers.skillMcpManager.disconnectSession(sessionInfo.id)
|
||||
```
|
||||
|
||||
## LIFECYCLE FLOW
|
||||
|
||||
```
|
||||
1. session.created → No action (lazy connection)
|
||||
2. First MCP tool call → getOrCreateClient() creates + caches
|
||||
3. Ongoing use → lastUsedAt timestamp updated
|
||||
4. Idle >5min → cleanup timer removes
|
||||
5. session.deleted → disconnectSession() closes session clients
|
||||
6. Process exit → disconnectAll() via SIGINT/SIGTERM handlers
|
||||
```
|
||||
|
||||
## RACE CONDITION PREVENTION
|
||||
|
||||
- **pendingConnections**: Deduplicates concurrent connection attempts for same key
|
||||
- **inFlightConnections**: Per-session counter, prevents premature cleanup during connection setup
|
||||
- **shutdownGeneration**: Counter-based stale connection detection after disconnect
|
||||
|
||||
## PUBLIC API
|
||||
|
||||
```typescript
|
||||
class SkillMcpManager {
|
||||
constructor(options?: { createOAuthProvider? })
|
||||
getOrCreateClient(info, config): Promise<Client>
|
||||
disconnectSession(sessionID): Promise<void>
|
||||
disconnectAll(): Promise<void>
|
||||
listTools/Resources/Prompts(info, context): Promise<...[]>
|
||||
callTool(info, context, name, args): Promise<unknown>
|
||||
readResource(info, context, uri): Promise<unknown>
|
||||
getPrompt(info, context, name, args): Promise<unknown>
|
||||
getConnectedServers(): string[]
|
||||
isConnected(info): boolean
|
||||
}
|
||||
```
|
||||
|
||||
## RETRY SEMANTICS
|
||||
|
||||
- `getOrCreateClientWithRetry()` — 3 attempts with force reconnect on failure
|
||||
- `withOperationRetry()` — OAuth-aware wrapper: step-up on 403, token refresh on 401
|
||||
|
||||
## SECURITY
|
||||
|
||||
- **env-cleaner.ts** — strips npm/pnpm config vars (prevents pnpm project isolation issues) and secret patterns before stdio spawn
|
||||
- **error-redaction.ts** — masks tokens/secrets in error messages before logger.log
|
||||
- **OAuth isolation** — auth providers keyed by server URL, tokens never cross servers
|
||||
@@ -0,0 +1,102 @@
|
||||
# src/hooks/runtime-fallback/ — Reactive Provider Error Recovery
|
||||
|
||||
**Generated:** 2026-04-09
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
32 files. Session Tier hook that **reactively** switches to fallback models when API providers return errors at runtime (429, 503, quota exhausted, cooldown signals). Distinct from `model-fallback` (which applies preemptively at chat.params).
|
||||
|
||||
## RUNTIME-FALLBACK vs MODEL-FALLBACK
|
||||
|
||||
| Aspect | runtime-fallback | model-fallback |
|
||||
|--------|-----------------|----------------|
|
||||
| **Trigger** | Reactive — after error occurs | Proactive — at request time |
|
||||
| **Event** | session.error, message.updated, session.status | chat.params |
|
||||
| **Config source** | `categories[].fallback_models`, `agents[].fallback_models` | `AGENT_MODEL_REQUIREMENTS` hardcoded chains |
|
||||
| **State** | Per-session FallbackState + cooldown tracking | Module-global pendingModelFallbacks |
|
||||
| **Use case** | Provider errors during execution | Pre-configured agent fallback chains |
|
||||
|
||||
They operate **independently** — no direct integration.
|
||||
|
||||
## ERROR DETECTION
|
||||
|
||||
### HTTP Status Codes (configurable)
|
||||
Default retry codes: `429, 500, 502, 503, 504`
|
||||
|
||||
### Error Message Patterns (constants.ts)
|
||||
```
|
||||
/rate.?limit/i, /too.?many.?requests/i, /quota.*reset.*after/i,
|
||||
/exhausted.*capacity/i, /all.*credentials.*for.*model/i,
|
||||
/cool(?:ing)?.?down/i, /model.*not.*supported/i,
|
||||
/service.?unavailable/i, /overloaded/i, /temporarily.?unavailable/i
|
||||
```
|
||||
|
||||
### Error Type Classification (error-classifier.ts)
|
||||
- `missing_api_key` — provider rejects auth
|
||||
- `model_not_found` — model unavailable
|
||||
- `quota_exceeded` — billing/quota hit
|
||||
- Auto-retry signal detection via `auto-retry-signal.ts` — extracts "retrying in ~2 weeks" style signals, triggers immediate fallback
|
||||
|
||||
## FALLBACK STATE MACHINE
|
||||
|
||||
```typescript
|
||||
interface FallbackState {
|
||||
originalModel: string
|
||||
currentModel: string
|
||||
fallbackIndex: number
|
||||
failedModels: Map<string, number> // model → cooldown-until timestamp
|
||||
attemptCount: number
|
||||
pendingFallbackModel?: string
|
||||
}
|
||||
```
|
||||
|
||||
## FALLBACK CHAIN RESOLUTION (fallback-models.ts)
|
||||
|
||||
Priority order:
|
||||
1. **Session category** (via SessionCategoryRegistry)
|
||||
2. **Agent config** `fallback_models`
|
||||
3. **Agent's category** `fallback_models`
|
||||
4. **Session ID pattern match** (detect agent from session ID format)
|
||||
|
||||
## RETRY FLOW
|
||||
|
||||
```
|
||||
session.error / message.updated (with error) / session.status (retry signal)
|
||||
→ isRetryableError(error)?
|
||||
→ getFallbackModelsForSession(sessionID, agent)
|
||||
→ findNextAvailableFallback() — skip cooldown models
|
||||
→ prepareFallback() — update state, mark current failed
|
||||
→ dispatchFallbackRetry() — toast notification + promptAsync with new model
|
||||
→ 30s timeout — abort and try next if exceeded
|
||||
```
|
||||
|
||||
## COOLDOWN MECHANISM
|
||||
|
||||
Failed models enter 60s cooldown. `findNextAvailableFallback()` skips models in cooldown, preventing thrashing on persistently failing models.
|
||||
|
||||
## KEY FILES
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `hook.ts` | `createRuntimeFallbackHook()` — composes all handlers |
|
||||
| `event-handler.ts` | Route session lifecycle (created, error, stop, idle) |
|
||||
| `message-update-handler.ts` | Handle error parts in `message.updated` |
|
||||
| `session-status-handler.ts` | Handle provider retry signals in session.status |
|
||||
| `chat-message-handler.ts` | Apply fallback model override on chat.message |
|
||||
| `error-classifier.ts` | `isRetryableError()`, `classifyErrorType()` |
|
||||
| `auto-retry-signal.ts` | Extract "retrying in..." signals |
|
||||
| `fallback-state.ts` | State machine: createFallbackState, prepareFallback, findNextAvailableFallback, isModelInCooldown |
|
||||
| `fallback-models.ts` | Resolve chain from config hierarchy (strings + raw objects) |
|
||||
| `fallback-bootstrap-model.ts` | Derive initial model when state missing |
|
||||
| `fallback-retry-dispatcher.ts` | Toast + dispatch retry orchestration |
|
||||
| `auto-retry.ts` | Abort, timeout scheduling, cleanup |
|
||||
| `agent-resolver.ts` | Session → agent name normalization |
|
||||
| `retry-model-payload.ts` | Build model payload (providerID/modelID/variant/reasoningEffort) |
|
||||
| `visible-assistant-response.ts` | Detect if assistant produced real output vs just errors |
|
||||
| `last-user-retry-parts.ts` | Extract last user message parts for retry |
|
||||
|
||||
## NOTES
|
||||
|
||||
- Cooldown and failure tracking are **per-session** — concurrent sessions don't share state
|
||||
- `visible-assistant-response.ts` prevents retry if the assistant already produced a partial valid response
|
||||
- Runtime-fallback is registered in the Session Tier via `create-session-hooks.ts`
|
||||
@@ -0,0 +1,82 @@
|
||||
# src/openclaw/ — Bidirectional External Integration
|
||||
|
||||
**Generated:** 2026-04-09
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
18 files. Bidirectional integration system: **outbound** session event notifications (Discord/Telegram/HTTP webhook/shell command) AND **inbound** reply handling (daemon polls chat apps, injects replies back into tmux session). Named "claw" because it reaches out from OpenCode and pulls replies back in.
|
||||
|
||||
## BIDIRECTIONAL FLOW
|
||||
|
||||
### Outbound (OpenCode → External)
|
||||
```
|
||||
OpenCode session event → dispatchOpenClawEvent()
|
||||
→ runtime-dispatch.ts: map event to OpenClaw event
|
||||
→ dispatcher.ts: execute gateway (HTTP POST or shell command)
|
||||
→ session-registry.ts: record message ID ↔ sessionID ↔ tmux pane
|
||||
```
|
||||
|
||||
### Inbound (External → OpenCode)
|
||||
```
|
||||
Discord/Telegram API → reply-listener daemon (separate Bun process)
|
||||
→ reply-listener-{discord,telegram}.ts: poll every 3s
|
||||
→ session-registry.ts: look up target tmux session from message ID
|
||||
→ reply-listener-injection.ts: send-keys into tmux pane (rate limited)
|
||||
```
|
||||
|
||||
## KEY FILES
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.ts` | `wakeOpenClaw()`, `initializeOpenClaw()` — main entry |
|
||||
| `types.ts` | `OpenClawConfig`, `OpenClawPayload`, `WakeResult` types |
|
||||
| `config.ts` | Gateway resolution + URL validation (HTTPS required, localhost exception) |
|
||||
| `dispatcher.ts` | HTTP POST + shell command execution with variable interpolation |
|
||||
| `runtime-dispatch.ts` | Maps OpenCode events → OpenClaw events, orchestrates dispatch |
|
||||
| `session-registry.ts` | JSONL registry correlating message IDs ↔ sessions ↔ panes (file-locked) |
|
||||
| `reply-listener.ts` | Daemon lifecycle: start/stop, poll loop, state persistence |
|
||||
| `reply-listener-discord.ts` | Discord API polling |
|
||||
| `reply-listener-telegram.ts` | Telegram API polling |
|
||||
| `reply-listener-injection.ts` | Inject received reply into tmux pane (rate limiting + user filtering) |
|
||||
| `reply-listener-state.ts` | Daemon state: PID, config signature, poll tracking |
|
||||
| `daemon.ts` | Daemon entry point (runs as detached Bun process) |
|
||||
| `tmux.ts` | `capturePane()`, `sendToPane()` utilities |
|
||||
|
||||
## GATEWAY TYPES
|
||||
|
||||
| Type | Config | Execution |
|
||||
|------|--------|-----------|
|
||||
| **HTTP webhook** | `url` field | POST with JSON payload |
|
||||
| **Shell command** | `command` field | Execute with env vars (OPENCLAW_*) |
|
||||
|
||||
## PAYLOAD VARIABLES (interpolation)
|
||||
|
||||
`{sessionId}`, `{projectPath}`, `{tmuxSession}`, `{timestamp}`, `{eventType}` (session.created/deleted/idle), `{messageContent}`, `{promptSummary}`
|
||||
|
||||
## INTEGRATION POINTS
|
||||
|
||||
- `src/index.ts` — calls `initializeOpenClaw(pluginConfig.openclaw)` at plugin startup (if `enabled`)
|
||||
- `src/plugin/event.ts` — calls `dispatchOpenClawEvent()` for session.created/deleted/idle
|
||||
- `src/config/schema/openclaw.ts` — Zod config schema
|
||||
|
||||
## DAEMON LIFECYCLE
|
||||
|
||||
```
|
||||
initializeOpenClaw(config)
|
||||
→ wakeOpenClaw() if reply_listener.enabled
|
||||
→ spawn daemon.ts as detached process
|
||||
→ daemon writes PID to .opencode/openclaw.state.json
|
||||
→ daemon polls Discord/Telegram every 3s
|
||||
→ on reply: lookup in session-registry → inject into tmux via send-keys
|
||||
```
|
||||
|
||||
## SECURITY
|
||||
|
||||
- **URL validation**: HTTPS required except localhost (config.ts)
|
||||
- **Authorized users**: Inbound replies filtered by allowed user ID list
|
||||
- **Token redaction**: Secrets masked in logs and error messages
|
||||
- **Rate limiting**: Reply injection throttled per pane
|
||||
|
||||
## TESTING NOTE
|
||||
|
||||
`reply-listener-discord.test.ts` is **always isolated** in CI (listed in `ALWAYS_ISOLATED_TEST_FILES` of `script/run-ci-tests.ts`). Reason: mocks `globalThis.fetch` for Discord API simulation — needs process isolation to avoid interference with shared test batch.
|
||||
Reference in New Issue
Block a user