Prevent duplicate CI completion notifications in paired rooms by checking
for existing active watchers with the same channel+provider+metadata before
creating a new one. Remove Task ID from watcher prompt construction to
avoid router secret redaction (task- IDs contain sk- pattern), passing
EJCLAW_RUNTIME_TASK_ID via env var instead and making cancel_task
auto-resolve when task_id is omitted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update add-compact, add-voice-transcription, customize, and
update-skills SKILL.md files to reference EJClaw instead of NanoClaw.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove bump-version, update-tokens, and merge-forward-skills workflows
inherited from upstream nanoclaw. These require APP_ID/APP_PRIVATE_KEY
secrets not configured for EJClaw and are unnecessary for the current
direct-deploy model. Only ci.yml (PR checks) is retained.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both Claude and Codex platform prompts now include a short
section explaining when to use structured GitHub fields
(ci_provider, ci_repo, ci_run_id) for the host-driven fast
path instead of the generic LLM-per-tick watcher.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace LLM-per-tick polling with direct `gh api` calls for GitHub
Actions watchers. New `ci_provider` discriminator column routes tasks
to either the host-driven path (zero LLM tokens) or the existing
generic LLM path. GitHub watchers poll at 15s intervals (min 10s)
via `checkGitHubActionsRun()` in the scheduler, with completion
messages sent directly to chat on terminal state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Watcher tasks get task-scoped IPC dirs (data/ipc/{group}/tasks/{taskId}/),
but the MCP server used HOST_IPC_DIR to write IPC files. Without
EJCLAW_HOST_IPC_DIR, it fell back to the task-scoped path, writing
cancel_task/send_message files to nested dirs the IPC watcher never scans.
This caused 11 zombie watchers stuck in infinite loops — self-cancel
calls silently failed with ~4000 stranded IPC files accumulating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous regex used \w+ for descriptor tokens between the function
name and JSON body, which only matches ASCII word characters. Model
hallucinations often inject CJK/non-ASCII text (e.g. 彩神争霸) in this
position. Switch to (?:\s+[^\s{}]+)+ to match any whitespace-separated
non-brace tokens regardless of character set.
Also removes stale backward-compat re-exports from index.ts and updates
the test import for composeDashboardContent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace scattered reason string literals with a centralized type hierarchy:
- AgentTriggerReason: all possible trigger reasons
- ClaudeRotationReason, CodexRotationReason, NoFallbackCooldownReason:
derived subtypes for specific contexts
- AgentErrorClassification, FallbackTriggerResult, CodexRotationTriggerResult:
discriminated unions for compile-time narrowing
Remove dead export isUsageExhausted() (superseded by
isPrimaryNoFallbackCooldownActive). Replace NO_FALLBACK_COOLDOWN_REASONS
Set with isNoFallbackCooldownReason() type guard.
Add tests for agent-error-detection and provider-retry (423 total passing).
Known limitation: 6 `as CodexRotationReason` casts in Codex rotation paths
where streamed trigger reason is AgentTriggerReason but runtime value is
always CodexRotationReason-compatible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Claude account is suspended, the API returns "Your organization does
not have access to Claude" on the success path or "Failed to authenticate.
API Error: 403 terminated" on the error path. Both are now classified as
'org-access-denied', suppressed from Discord output, and trigger automatic
token rotation. If all tokens are exhausted, enters cooldown without Kimi
fallback (same as usage-exhausted and auth-expired).
Changes:
- agent-error-detection: add isClaudeOrgAccessDeniedMessage(), expand
classifyClaudeAuthError() and shouldRotateClaudeToken()
- streamed-output-evaluator: detect org-access-denied in success-path chain
- provider-fallback: add NO_FALLBACK_COOLDOWN_REASONS set and
isPrimaryNoFallbackCooldownActive() helper
- provider-retry: handle org-access-denied in rotation loop
- message-agent-executor / task-scheduler: use generalized no-fallback
cooldown check
- Tests: +8 test cases across 5 test files (415 total passing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The streamed-output-evaluator used 'claude' while the rest of the
codebase used 'claude-code'. Align the evaluator type and all call
sites (message-agent-executor, task-scheduler) to use the canonical
'claude-code' | 'codex' domain consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When Codex enters a degenerate loop, it emits tool-call intent as
plaintext (e.g. `to=functions.exec_command code {"cmd":"..."}`) instead
of actual tool calls. Add stripToolCallLeaks() defense layer to remove
these patterns before they reach Discord. Pure garbage output becomes
empty and is suppressed entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace `active/closingStdin/isTaskProcess` booleans with a single `runPhase`
enum (idle | running_messages | running_task | closing_messages). Add
`resetRunState()` to eliminate 7-field reset duplication, and
`assertRunPhaseInvariants()` for runtime validation of field consistency
after every phase transition. Add 3 transition tests covering the full
lifecycle, closeStdin during tasks, and sendMessage phase gating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract dashboard-usage-rows.ts (UsageRow, formatResetRemaining,
mergeClaudeDashboardAccounts, buildClaudeUsageRows, extractCodexUsageRows)
and codex-usage-collector.ts (fetchCodexUsage, applyCodexUsageToAccount,
buildCodexUsageRowsFromState, refresh functions returning data instead of
mutating module state). unified-dashboard.ts drops from 926 to 563 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two leak paths caused Discord typing indicators to persist indefinitely:
1. If runAgent() threw before turnController.finish(), setTyping(false)
was never called. Wrap the agent run in try/finally to guarantee cleanup.
2. Follow-up message piping called setTyping(true) fire-and-forget. If
the Promise resolved after finish()'s setTyping(false), the typing
interval was recreated with nobody to clear it. Await the call to
serialize the on/off sequence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove OPENAI_API_KEY from .env and Codex child process env to prevent
API billing when subscription quota is exhausted
- Remove writeCodexApiKeyAuth entirely — Codex now uses OAuth only
- Add 9-pattern secret redaction in formatOutbound() to prevent key leaks
- Fix Codex usage bucket aggregation: use 'codex' bucket only instead of
max across all buckets (bengalfox = Codex Spark, not needed)
- Add d7≥100% auto-rotation in updateCodexAccountUsage to skip exhausted
accounts and prevent API billing fallback
- Add findNextCodexAvailable that checks both rate-limits and d7 usage
- Clean stale apikey auth.json files from all session directories
- Update tests: OAuth-only auth, d7 auto-skip (3 new tests), dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, intermediate text was sent as separate messages when
subagents were active, causing message floods. Now always edit
the progress message regardless of subagent state.