Recovery: HybridClaw - recovery and local commands#150
Recovery: HybridClaw - recovery and local commands#150
Conversation
There was a problem hiding this comment.
Pull request overview
Adds local-only recovery/admin command flows for the HybridClaw agent, improving operator visibility (auth status), plugin lifecycle control (enable/disable), and retry/error messaging around HybridAI premium-model permission failures.
Changes:
- Add local-only
/auth status hybridaiand wire it through the command registry, TUI menu, and gateway command handler. - Add
plugin enable|disableacross CLI/TUI/gateway, backed by a newsetPluginEnabled()runtime-config writer and plugin runtime reload behavior. - Improve HybridAI error parsing/messaging and retry/fallback decisions for premium-model permission errors; persist resolved chatbot ID in the session.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/tui-slash-menu.test.ts | Verifies new TUI menu entry for /auth status hybridai. |
| tests/tui-slash-command.test.ts | Adds coverage for mapping auth status hybridai and plugin disable ... to gateway args. |
| tests/hybridai-retry.test.ts | Ensures premium permission errors don’t trigger fallback/retry classification changes. |
| tests/hybridai-request-error.test.ts | Confirms premium permission errors are rewritten into an actionable message. |
| tests/gateway-status.test.ts | Adds end-to-end coverage for auth status hybridai and local-only restriction; minor expectation tweak. |
| tests/gateway-service.plugins.test.ts | Adds mocks and tests for gateway plugin disable + restriction + help text. |
| tests/gateway-service.bot-auth.test.ts | Verifies chatbot-id fallback is cached into the session. |
| tests/command-registry.test.ts | Ensures auth is TUI-only and adds parsing for plugin disable. |
| tests/cli.test.ts | Adds CLI test coverage for plugin disable. |
| src/tui.ts | Treats `plugin enable |
| src/plugins/plugin-config.ts | Introduces setPluginEnabled() and plugin existence checks for enabled-state changes. |
| src/gateway/gateway-service.ts | Adds auth status hybridai, plugin enable/disable handling + reload, and caches fallback chatbot id. |
| src/command-registry.ts | Registers auth as TUI-only; adds `plugin enable |
| src/cli/plugin-command.ts | Implements `hybridclaw plugin enable |
| src/cli/help.ts | Updates CLI help/usage text for plugin enable/disable. |
| docs/chat.html | Adjusts web chat error handling on /api/chat failures (removes banner update). |
| container/src/providers/shared.ts | Adds structured provider error parsing, premium-permission detection, and improved summaries/messages. |
| container/src/model-retry.ts | Avoids fallback on premium-model permission errors. |
| container/src/index.ts | Emits cleaner HybridAIRequestError messages without double-prefixing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ? `${enabled ? 'Enabled' : 'Disabled'} plugin ${result.pluginId}.` | ||
| : `Plugin ${result.pluginId} was already ${enabled ? 'enabled' : 'disabled'}.`, | ||
| ); | ||
| console.log(`Updated runtime config at ${result.configPath}.`); |
There was a problem hiding this comment.
In the enable/disable path, the CLI always prints "Updated runtime config at ..." even when setPluginEnabled() returns changed: false. In particular, setPluginEnabled() short-circuits for enable when there is no existing config entry (plugin already enabled by default) and does not write the config file, so this message is misleading. Consider only printing the "Updated runtime config" line when a write actually occurred (or have setPluginEnabled return an explicit written/changedConfig flag).
| console.log(`Updated runtime config at ${result.configPath}.`); | |
| if (result.changed) { | |
| console.log(`Updated runtime config at ${result.configPath}.`); | |
| } |
| ): Promise<PluginEnabledWriteResult> { | ||
| const normalizedPluginId = normalizePluginId(pluginId); | ||
| const nextConfig = cloneConfig(getRuntimeConfig()); | ||
| await ensurePluginExistsForConfig(normalizedPluginId, nextConfig); |
There was a problem hiding this comment.
setPluginEnabled() always calls ensurePluginExistsForConfig() before changing state. This prevents disabling a plugin when it cannot be discovered (e.g., plugin directory removed/corrupt, or a configured path is now invalid), even though disabling is a safe recovery action to silence repeated discovery warnings and let the gateway start cleanly. Consider only requiring discovery when enabling, or allow disabling when the plugin has an existing config entry (even if discovery fails).
| await ensurePluginExistsForConfig(normalizedPluginId, nextConfig); | |
| if (enabled) { | |
| await ensurePluginExistsForConfig(normalizedPluginId, nextConfig); | |
| } |
| const text = err instanceof Error ? err.message : String(err); | ||
| if (options.clearPendingMedia !== false) { | ||
| pendingMedia = [...queuedMedia, ...pendingMedia]; | ||
| renderPendingMedia(); | ||
| } | ||
| setError(text); | ||
| addMessage('system', `Error: ${text}`); | ||
| } finally { |
There was a problem hiding this comment.
Removing the setError(text) call in the submitPrompt catch block means request failures from /api/chat no longer update the error banner (errorEl), while other flows still rely on it. This can make send failures easy to miss (only a system chat message is added) and can also leave a stale banner from a previous error. If the banner is still intended, restore setError(text) here; if not, consider clearing it explicitly or removing the banner usage for consistency.
|
@claude Do a thorough review of this PR in terms of: KISS, YAGNI, DRY, Fail Fast, Secure by Default and performance/speed |
|
Claude finished @furukama's task in 2m 46s —— View job PR Review: KISS, YAGNI, DRY, Fail Fast, Secure by Default, PerformanceTasks:
Overall AssessmentThe PR adds coherent, well-tested features. Most of the code quality issues are medium severity — no show-stoppers, but there are a few worth fixing before merge, particularly the security boundary expansion and a few meaningful DRY/YAGNI violations. KISS — Keep It Simple StupidRedundant pre-conditions before The guard block checks // before the call site:
toolCalls.length === 0 &&
!visibleAssistantText &&
parseRalphChoice(choice.message.content) === null &&
isHybridAIEmptyVisibleCompletion(response) // ← re-checks tool_calls + visible text internallyEither inline the remaining conditions into Complex rollback in gateway plugin enable/disable ( The YAGNI — You Aren't Gonna Need It
The interface defines interface ParsedProviderErrorBody {
message: string | null;
type: string | null;
code: number | string | null; // ← extracted but never consumed
}Over-engineered debug serialization (
The DRY — Don't Repeat YourselfLocal-session restriction guard duplicated ( The exact same check appears twice within 300 lines: req.guildId !== null ||
(req.channelId !== 'web' && req.channelId !== 'tui')Both function isLocalSession(req: GatewayCommandRequest): boolean {
return req.guildId === null &&
(req.channelId === 'web' || req.channelId === 'tui');
}Help-text entries duplicated (with/without slash prefix) ( The PR extends the pre-existing pattern of maintaining two parallel help-text arrays (one for bare commands, one for slash-prefixed). While this pattern predates this PR, each new command now needs to be added in two places. This is worth noting as tech debt — a single source-of-truth Fail Fast
export async function setPluginEnabled(pluginId, enabled) {
const normalizedPluginId = normalizePluginId(pluginId);
const nextConfig = cloneConfig(getRuntimeConfig());
await ensurePluginExistsForConfig(normalizedPluginId, nextConfig); // ← full plugin scan
const existing = findPluginEntry(nextConfig, normalizedPluginId);
if (enabled && !existing) {
return { changed: false, ... }; // ← no-op early return
}
...
}
const existing = findPluginEntry(nextConfig, normalizedPluginId);
if (enabled && !existing) return { changed: false, ... }; // fast no-op
if (enabled) await ensurePluginExistsForConfig(...); // only when actually neededSecure by Default
if (PROJECT_ROOT && isWithinRoot(resolvedActual, PROJECT_ROOT)) {
return resolvedActual; // ← allowed, bypasses workspace root check
}If the gateway runs from a directory that contains sensitive files (e.g.,
The new
Performance / SpeedDouble-parsing of error body in When class HybridAIRequestError extends Error {
readonly parsedBody: ParsedProviderErrorBody | null;
constructor(message, status, body) {
...
this.parsedBody = parseProviderErrorBody(body);
}
}
The fallback at the end of the path-candidate loop calls
This doubles maximum output token budget per turn, which directly increases per-request latency and cost. No mention of this in the PR description. Worth confirming this is intentional and that the default model supports 8192 output tokens without performance regression. Summary Table
|
Automated PR to merge recovery changes and local command flows for HybridClaw AI agent into main.