Repository navigation
fix(sessions): agents start without a broken managed skill instead of not starting at all - #1139
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts from starting The pre-spawn managed-skills reconcile rethrew for any session with TLDR or Goal, which is nearly every pane, so one broken skill stopped every agent of every provider (two outages from unrelated causes on Sept 18 and 19). Agents now launch without the broken skill. The MCP tools still carry their own server instructions, so what is lost is guidance, not the capability. - AgentCodeManagedSkillsService.prepareForAgentSpawn runs the audit and each requested product skill independently and reports failures instead of throwing, so a broken TLDR skill no longer also skips Goal. - SessionManager journals each failure (with the skill) and emits managed-skills-unavailable. The forwarder broadcasts it, and GlobalToast names the skill and points to Settings > Agents > Custom Skills. - recover.failed carries a closed-enum cause (missing-workspace, cli-not-found, provider-launch, unknown). Raw error text stays out of the stream. Fixes #1133 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hrottled Review round on #1139: - The conventions.pre_spawn_reconcile.error rows now carry ids.sessionId, like the .degraded row, so triage can join them. recordError takes an optional ids argument for this. - .degraded and the managed-skills-unavailable warning are emitted only after the post-reconcile cancellation check, so a restore cancelled during the reconcile no longer journals a false "launching without skills" row. - Corrected the WHY comment: before #1133 an audit throw shared the single catch and also aborted TLDR/Goal launches, so it was not "always nonfatal". - GlobalToast holds back a repeat of the same skill-set warning for 60 s, so a skill that stays broken does not re-toast on every later launch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SessionManagerruns the managed-skills reconcile before every agent spawn. When it threw,if (hasReportingDomain(options.builtInMcpDomains)) throw erroraborted the spawn for any session with TLDR or Goal enabled, and that is nearly every pane. So one managed-skill failure meant no agent in any provider could start. The panes still restored their transcripts, so the workspace looked healthy while nothing was running. The journal then recordedrecover.failed code=start-failedwith no cause.This happened twice in two days, from unrelated causes. On Sept 18, "TLDR skill deployment failed" appeared on a build before grok, and the cause was lost. On Sept 19, grok's unsupported-provider health hit the gate (#1014, fixed in #1017 and #1037). In the Sept 19 journals, every reconcile error is followed at
seq+1byrecover.failed(15/15 and 14/14). #1017 and #1037 fixed specific failure modes. This PR limits the blast radius for any future cause.Implemented behavior
AgentCodeManagedSkillsService.prepareForAgentSpawn(domains)runs the audit, TLDR and Goal as separate steps and returns the failures. Before, a broken TLDR skill (or a failed audit) also skipped Goal.SessionManageremitsmanaged-skills-unavailable. This happens only after the post-reconcile cancellation check, so a restore cancelled during the reconcile gets no toast and no false row. The forwarder broadcastsmanaged-skills:unavailable { skills }, andGlobalToastProvidershows a 10 s click-to-dismiss toast. The same skill set is not re-toasted within 60 s. For example: "TLDR skill could not be prepared, so agents started without it. Review Settings › Agents › Custom Skills." That Settings section is where TLDR/Goal appear as "Managed by … MCP" rows with their health and target status.conventions.pre_spawn_reconcile.errorname, now withdata.skillandids.sessionId, so current greps keep working. A newconventions.pre_spawn_reconcile.degraded(warn, sameids.sessionId) records which skills the session is launching without, so the two rows join per pane.AppRunJournal.recordErrorgained an optionalidsargument for this.recover.failedhas a typedcause.SESSION_START_FAILURE_CAUSESinsrc/shared/lifecycle/events.tshas four values:missing-workspace,cli-not-found,provider-launchandunknown. Raw exception text still stays out of the stream and out of IPC.Design decisions and tradeoffs
Launching without the skill is the default the user accepted in bug(sessions): one failing managed-skill reconcile stops every agent from starting #1133. It can be changed. The old strict rule treated the skill as part of the capability. The rewritten WHY comment (
runPreSpawnSkillReconcile) records the tradeoff: a missing skill costs guidance about when to call the tools, while the old rule turned any skill failure into a workspace-wide outage. If a strict mode is ever wanted, it should be an explicit setting, not the silent default.Why the global toast. I checked the existing channels first:
session:*/sendToSessionWindowwould quarantine the event for ids that no window has claimed, and record a false routing gap.GlobalToastalready takes a main→renderer push (extensions:notification) and is app-wide. That fits, because skill health is machine-wide. Its single slot collapses the N identical warnings from a restore into one.The new IPC channel only feeds this existing UI.
A hook that throws still warns. The production hook reports failures and does not throw. If it throws anyway, no one knows which skills were prepared, so every requested reporting skill is treated as unavailable. Under-warning would bring back the silent omission the strict rule was meant to prevent.
Audit-only failures do not toast. Before this PR that was not nonfatal. An audit throw shared the hook's single catch, so it also aborted every TLDR/Goal launch. Only sessions without reporting domains survived it. The Sept 18 outage could therefore have been an audit failure as easily as a skill deploy failure; the
.errorrow's message tells them apart. Now it is journal-only: the audit never removes a skill the agent asked for (the product skills run as their own steps), and its degraded health already shows in Settings.Repeat window. Main warns on every launch while a skill stays broken.
GlobalToastholds back a repeat of the same skill set for 60 s, per window, so the toast does not come back all day or push out extension notifications. A new skill set shows at once, and the journal still records every occurrence.No
conventions-gatecause. After this change the gate cannot fail a start, so the value could never be emitted. A dead value would look like a live failure mode during triage.How causes are classified.
ProviderCliNotFoundErroris a new class that keeps the old message unchanged.provider-launchis attached through a WeakMap tag at the providerstart()catch sites instead of wrapping the error, because Codex replacement compensation and callers depend on the original error's identity and message. Nothing matches on message substrings.Tests and verification
Each new test fails on origin/main (verified by stashing the source changes) and passes on this branch:
sessionManager.wake.test.ts: the two tests that pinned the old abort now pin the new contract:.errorand.degraded;['tldr','goal']in fixed order;.degradedrow, but still gets the.errorrow;sessionManager.lifecycle.test.ts:recover.failed.causeismissing-workspace,cli-not-found,provider-launchorunknown, and the provider error text (TOKEN=secret-value) does not appear in the event.AgentCodeCustomSkillsService.system.test.ts(real service, real files): with a user-owned file blocking TLDR,prepareForAgentSpawn(['tldr','goal'])reports the TLDR failure, leaves the user's file alone, and still gets Goal toactive.forwarder.test.ts: the warning is broadcast with domain names only.src/shared/types/tldr.test.ts: the message text names the skill(s) and points to Settings.Verification on Node 24.14.1:
npx tsc -b: clean.vitest runon the touched suites plus the recover and codexReplacement suites: 132 passed.AppRunJournal.completeness: 4 passed.The app was not launched.
Limitations
recover.failedwith its own cause).GlobalToast).causeis added only torecover.failed. Freshsession:spawnfailures still reject to the caller as before, and there is no lifecycle event for them..errorwithskill: 'tldr', not as an outage.Implementation plan:
docs/superpowers/plans/2026-09-23-skill-gate-blast-radius.md(first commit, updated for the review round). Review fixes:7bef296e.Fixes #1133
🤖 Generated with Claude Code