The depth limit (default maxDepth=3) was being silently bypassed when
sync-task.ts could not reach the manager's spawn enforcement methods --
the fallback hardcoded childDepth: 1, allowing infinite recursion of
delegate_task calls in degraded environments.
This was hard to catch because:
1. The fallback path took the dangerous default silently (no log).
2. There were no end-to-end smoke tests asserting that the depth value
coming back from reserveSubagentSpawn is actually used.
3. The unit tests for resolveSubagentSpawnContext only covered error
cases, not the actual depth calculation.
Changes:
- sync-task.ts: split the spawnContext fallback into an explicit if/else
with a WARNING log when the manager is missing enforcement methods.
This makes the dangerous path observable in logs.
- subagent-spawn-limits.test.ts: add depth calculation regression tests
(root, depth-1, depth-2, depth at max, parent cycle detection).
- sync-task.test.ts: add two regression smoke tests:
1. depth limit error from reserveSubagentSpawn must be propagated and
must NOT create the session.
2. spawnDepth recorded in metadata must equal what reserveSubagentSpawn
returns -- guards against silent fallback to childDepth: 1.
15 new spawn-limits tests + 2 new sync-task tests pass.
Full suite: 5105 pass, 0 fail.
Structured plans (with ## TODOs section) use strict numbered-label
parsing. Simple plans (without sections) fall back to regex checkbox
counting. This fixes 9 test failures from the #3066 merge.
Added await for session ready state before sending prompt in
background-agent/manager.ts. Also improved image resizer error handling.
132 tests pass, tsc clean.
Closes#2932
Added 'trusted' option to expandEnvVars. Skill MCPs are user-controlled
and now bypass the security allowlist. 3 files changed.
TDD verified. tsc clean.
Closes#3168
Display names with parentheses like 'Atlas (Plan Executor)' cause HTTP
header validation errors in x-opencode-agent-name. This was blocking
Atlas/Prometheus from working via /start-work and auto-retry.
Changes:
- Display names: parens -> dashes ('Atlas - Plan Executor')
- Hooks (start-work, no-hephaestus-non-gpt, no-sisyphus-gpt): use
config keys ('atlas', 'sisyphus', 'hephaestus') for agent API fields
- auto-retry: use config key instead of display name for promptAsync
- agent-override-protection: handle dash-suffix normalization
- Updated all test expectations to match new format
Closes#3138
- install.test.ts: save and restore globalThis.fetch in beforeEach/afterEach
to prevent leaking a mock fetch (without .preconnect) into subsequent test files
- provider.test.ts: guard against originalFetch missing .preconnect when captured
from a leaked mock (defensive null-safe binding)
- discovery.test.ts: add writable:true to all Object.defineProperty fetch assignments
so downstream plain assignments (globalThis.fetch = ...) are not silently ignored
Root cause: install.test.ts set globalThis.fetch = mock(...) inside test bodies
without restoring it, leaving a mock fetch (no .preconnect method) that caused
provider.test.ts refresh tests to throw TypeError at the fetchMock construction
Prevent ZWSP sort prefixes from leaking into stored agent names, config
key lookups, and override-protection normalization. Ensures prefixed
list-display names resolve correctly throughout the pipeline.
🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
- provider.test.ts: reset discovery cache before/after refresh tests to prevent
cross-test contamination via module-level discoveryCache singleton
- completion-promise-session-negative.test.ts: update negative test case to use
non-oracle agent (hephaestus) since oracle tool_result VERIFIED detection was
intentionally added in 56f2a9df
Bug fixes:
1. OAuth token refresh (#3149): buildHttpRequestInit() now attempts silent refresh
via refresh_token before triggering full browser re-auth. Added refresh() method
to McpOAuthProvider. Includes test isolation fix for discovery mock.
2. Quota error STOP (#3126): Added STOP_MESSAGE_PATTERNS in model-error-classifier
that take precedence over RETRYABLE_MESSAGE_PATTERNS. Message-only quota errors
now non-retryable. Runtime-fallback: quota_exceeded with 'retrying in' signal
still triggers fallback (provider-managed auto-retry). Restored removed patterns.
3. Compaction loop (#3127): MAX_RECOVERY_ATTEMPTS=3 cap + additional suppression
guard from opencode session in degradation monitor.
Also: refactored extractAutoRetrySignal to auto-retry-signal.ts, new regression
tests for quota classifier and compaction degradation monitor.
- Add session_origins field to BoulderState type
- Track direct vs appended session origins in storage layer
- Add migration logic for existing state files
- Add comprehensive tests for session origin tracking
🤖 Generated with assistance of OhMyOpenCode
- Add lineage-aware continuation injection logic
- Support for tracking multiple session types (direct vs appended)
- Update tests for new lineage continuation scenarios
- Add session origin validation in continuation flow
🤖 Generated with assistance of OhMyOpenCode
- Add session_origins field to BoulderState type
- Track direct vs appended session origins in storage layer
- Add migration logic for existing state files
- Add comprehensive tests for session origin tracking
🤖 Generated with assistance of OhMyOpenCode
OpenCode's fromPlugin wrapper skips Zod validation for plugin tools, so
LLMs can omit required args like description without getting an error.
When Atlas orchestrates and the model omits description, it flows through
as undefined to manager.launch() and background task notifications show
'undefined' for all completed tasks.
Two fixes:
- Add runtime validation for description in delegate-task tool (matches
existing run_in_background and load_skills validation pattern)
- Defensive fallback in notification template: use task ID when
description is missing instead of rendering 'undefined'
The (message.includes("agent") && message.includes("undefined"))
pattern could match unrelated errors like "The agent returned undefined
for the configuration", triggering a false fallback that hides the real
failure.
The two precise patterns are sufficient:
- "Agent not found" — canonical SDK validation error
- "agent.name" — property access error on undefined agent config
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. isAgentNotFoundError now handles:
- Plain objects with .message field (not just Error instances)
- "agent.name"/"undefined" error variants from SDK validation
- The original "Agent not found" format
2. New tests:
- agent.name/undefined error variant triggers fallback
- Plain object errors with .message field trigger fallback
- "fallback also fails" test now verifies retry was attempted (callCount=2)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. [HIGH] Tool restrictions recomputed for fallback agent via
buildFallbackBody() — no longer inherits original agent's restrictions.
2. [HIGH] Double-retry race prevented — handleSessionErrorEvent now
returns early for agent-not-found errors, since the prompt catch
block already handles them with agent fallback. This prevents
tryFallbackRetry from racing with a model-level retry on the same
error (the "not found" pattern in RETRYABLE_MESSAGE_PATTERNS).
3. [MEDIUM] task.agent updated to FALLBACK_AGENT after successful
fallback — notifications, toast, and logging reflect actual agent.
4. [MEDIUM] FALLBACK_AGENT exported from spawner.ts and imported into
manager.ts — single source of truth.
5. [LOW] resumeTask fallback now uses promptWithModelSuggestionRetry
(consistent with startTask), getting timeout protection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a model/mode switch happens while a background task is in-flight,
the oh-my-openagent agent registry can be rebuilt without custom agents
(e.g., Sisyphus-Junior). The SDK then rejects the promptAsync call with
"Agent not found", killing the task.
This adds retry logic: when promptAsync fails with "Agent not found",
retry with the "general" agent (always available in opencode). The
original prompt, model, and skill content are preserved — only the
agent routing changes.
Fixes both the spawner (startTask/resumeTask) and manager (inline
launch) code paths. Also improves the error message detection in
manager.ts to recognize "Agent not found" alongside the existing
"agent.name"/"undefined" checks.
Related: #2052, #2875, #2882
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When run-ci-tests.ts groups the claude-code-plugin-loader directory,
loader.test.ts mocks ./discovery with name: 'demo'. This mock leaked
into discovery.test.ts because both ran in the same process.
Fix: dynamic import with cache-busting query string ensures each test
gets a fresh module instance, immune to sibling test mocks.
The discovery tests were failing in CI with "demo" plugin name instead
of expected names. The root cause was test directory structure:
**The Bug:**
Original test code created installPath as a subdirectory:
```typescript
const installPath = join(createTemporaryDirectory("omo-plugin-install-"), "oh-my-openagent")
```
This created: `/tmp/omo-plugin-install-XXXXXX/oh-my-openagent`
If another test created `/tmp/omo-plugin-install-YYYYYY/.claude-plugin/plugin.json`
with name "demo", and the test execution order caused the discovery test
to pick up the wrong temp directory, it would read the manifest with "demo".
**The Fix:**
Changed tests to use unique temp directories directly:
```typescript
const installPath = createTemporaryDirectory("omo-npm-plugin-")
```
This creates: `/tmp/omo-npm-plugin-XXXXXX`
Each test now has its own unique temp directory that cannot be contaminated
by other tests.
**Also included:**
- mock.module() for process isolation in CI runner
- pluginsHomeOverride parameter for plugins database isolation
Fixes CI failure on dev branch.
The discovery tests were failing in CI with "demo" plugin name instead of
expected names. This happened because:
1. The CI test runner (run-ci-tests.ts) groups tests by directory
2. Tests using mock.module() are run in isolated processes
3. Tests without mock.module run in a shared batch
4. Other tests in the shared batch were creating plugin state that
contaminated the discovery tests
Fix adds mock.module() to discovery tests:
- Mocks the logger module to avoid noise
- Forces CI runner to run these tests in isolated process
- Prevents cross-test contamination
Combined with previous pluginsHomeOverride parameter fix, this ensures
tests are properly isolated both at the parameter level and process level.
Also removes debug logging that was added for troubleshooting.
Fixes CI failure on dev branch.
The discovery.test.ts was using process.env.CLAUDE_PLUGINS_HOME to set
the plugins directory, but this global state could be affected by other
tests running in parallel, causing flaky failures with errors like:
Expected: "oh-my-openagent"
Received: "demo"
Changes:
- Added pluginsHomeOverride option to PluginLoaderOptions type
- Modified discoverInstalledPlugins to accept optional pluginsHomeOverride
- Modified loadInstalledPlugins to accept optional pluginsBaseDir
- Updated all 3 discovery tests to use pluginsHomeOverride instead of
relying on global process.env.CLAUDE_PLUGINS_HOME
This makes the tests properly isolated and deterministic regardless of
test execution order or parallelization.
Fixes CI failure on dev branch.
- Change mock.module target from "../../shared" to "../../shared/claude-config-dir"
- Narrows mock scope to prevent barrel export contamination
🤖 GENERATED WITH ASSISTANCE OF OhMyOpenCode
Three test files were mocking the entire '../../shared' barrel, which
corrupted exports for subsequent test files in the same batch run.
Narrow mocks to specific submodules (logger, connected-providers-cache).
Also reverts Bun version pin since the root cause was mock scope, not Bun.