* fix(web): make Done/Terminated dashboard section scrollable (#1923)
The collapsible Done/Terminated cards grid had no max-height or
overflow handling, so it grew unbounded and was clipped by the
ancestor's overflow:hidden once enough terminated sessions
accumulated. The regression came from the Warm Terminal design
refresh (#927) which added `flex: 1` to `.kanban-board-wrap`,
defeating the body-level `overflow-y: auto` that previously
allowed natural page scroll past the done section.
Mirror the existing `.kanban-column-body` pattern by giving the
cards grid its own internal scroll container and matching thin
scrollbar styling. Each scrolling region owns its own scroll;
the Warm Terminal flex layout is preserved.
* style(web): document done-bar__cards max-height magic number
Addresses greptile P2 review feedback on PR #1925. The 320px in
calc(100vh - 320px) is the assumed chrome above the cards
(dashboard header + subhead + body padding + done-bar toggle and
margins). Adding a comment makes the implicit ancestor-height
contract explicit, and points to the kanban-board rule that
uses the same viewport-anchored pattern.
* fix(web): show Restore button for every exited session, including pr_merged
The Restore button was hidden for sessions exited with `pr_merged` reason
(legacy status `cleanup`) on the dashboard kanban and absent altogether
from the session-detail "Terminal ended" panel. The core `isRestorable()`
helper already allowed restoring these sessions; the dashboard helpers
were out of sync, compensating for a non-existent core constraint.
- `isDashboardSessionRestorable` now gates on `NON_RESTORABLE_STATUSES`
only, matching core's `isRestorable`. Merged-but-running sessions
(runtime still alive) remain non-restorable.
- `DoneCard` no longer hides Restore for merged sessions.
- `SessionEndedSummary` now exposes a prominent `Restore session` button
alongside `Open PR` / `Back to dashboard`, so users don't have to find
the small icon button in the header.
Closes#1907
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): add :focus-visible styles to ended-summary action buttons
Address PR #1909 review feedback. The new Restore session button (and
existing Open PR / Back to dashboard pills) lacked a visible focus ring,
making keyboard navigation invisible. Apply the same accent outline used
elsewhere in the dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): wire activity events into CLI commands and supervisor lifecycle (#1654)
Adds "cli" to ActivityEventSource and emits ~30 activity events across the CLI
surface so `ao events list --source cli` can answer RCA questions like:
- Did AO start cleanly? When? (cli.start_invoked / cli.start_failed)
- Was AO shut down gracefully or did it crash? (cli.shutdown_signal /
cli.shutdown_completed / cli.shutdown_force_exit / cli.stale_running_pruned)
- Did ao spawn / ao update / ao stop / ao migrate-storage fail and why?
- Did the auto-restore prompt actually restore sessions?
Instrumented files:
- packages/core/src/activity-events.ts (cli source)
- packages/cli/src/lib/shutdown.ts (signal/completed/failed/force_exit/session_kill_failed)
- packages/cli/src/commands/start.ts (start_invoked/start_failed/restore_*/stop_*/daemon_*/last_stop_* /config_migrated)
- packages/cli/src/commands/spawn.ts (spawn_invoked/spawn_failed)
- packages/cli/src/commands/update.ts (update_invoked/update_failed)
- packages/cli/src/commands/setup.ts (setup_failed)
- packages/cli/src/commands/migrate-storage.ts (migration_completed/migration_failed)
- packages/cli/src/commands/project.ts (project_register_failed)
- packages/cli/src/lib/resolve-project.ts (project_resolve_failed/config_recovered/config_recovery_failed)
- packages/cli/src/lib/running-state.ts (lock_timeout/stale_running_pruned)
- packages/cli/src/lib/credential-resolver.ts (credential_load_failed)
All emits are sync, never wrapped in try/catch (recordActivityEvent never
throws), and put raw error text in data.errorMessage (not summary, which is
FTS-indexed and not credential-sanitized).
Tests:
- 16 new instrumentation tests across shutdown, migrate-storage, update,
resolve-project, and start/stop action paths covering MUST emits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: correct CLI activity event semantics
* fix(cli): emit migrate-storage invocation event
* fix(cli): record last-stop write failures
* fix(linear): retry transient API failures
* fix(cli): stabilize update instrumentation test
* fix(cli): stub process probes in stop instrumentation tests
* chore(ci): retrigger checks
* Fix CLI failure event review issues
* fix: bound linear integration cleanup
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: whoisasx <adil.business4064@gmail.com>
Co-authored-by: Adil Shaikh <106678504+whoisasx@users.noreply.github.com>
* feat(core): round out session-manager activity event instrumentation (#1657)
PR #1620 wired activity events for the worker-spawn happy path
(spawn_started/spawned/spawn_failed/killed). This extends the same
pattern across the rest of session-manager's failure surfaces so RCA
can answer questions today's logs can't:
- Did the orchestrator spawn fail? (orchestrator path had no AE
envelope at all; now wrapped like worker spawn)
- Did the agent's startup prompt deliver, or did all 3 retries fail?
- Did `runtime.destroy` / `workspace.destroy` succeed during kill, or
silently leak?
- Did `runtime_lost` reconciliation fire for this session?
- Did `ao send` actually deliver?
New event kinds (added to ActivityEventKind union):
session.kill_started, session.prompt_delivery_failed, session.send_failed,
session.restore_failed, session.restore_fallback, session.rollback_started,
session.rollback_step_failed, session.workspace_hooks_failed,
session.cleanup_error, session.orchestrator_conflict,
runtime.lost_detected, runtime.lost_persist_failed, runtime.destroy_failed,
workspace.destroy_failed, agent.opencode_purge_failed,
tracker.issue_fetch_failed, tracker.generate_prompt_failed,
metadata.corrupt_detected
Coverage (per #1657 spec):
- All 8 MUST emits implemented and have a regression test in
session-manager-instrumentation.test.ts
- 13 SHOULD emits implemented (cleanup loop, orchestrator subpaths,
kill silent catches, send/restore failure paths, etc.)
- 4 COULD emits implemented
- Out-of-scope per-list enrichment paths intentionally NOT instrumented
Invariants preserved (extends #1620's B1-B16):
- B1: state mutation BEFORE event emission (runtime_lost persist
happens before the AE fires; verified by test)
- B2: never wrap recordActivityEvent in try/catch
- B11: prompt content excluded from prompt_delivery_failed data
(verified by test that asserts the prompt string is absent)
- B16: failure-only — emit on final retry exhaustion only, not per
attempt
- B25 (new): cleanup-stack rollbacks emit per-step via the onError
callback hook, not in aggregate
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: address session activity event review
* fix: address session activity event review
* fix(core): avoid duplicate restore failure events
* chore(ci): retrigger checks
* Fix orchestrator activity event instrumentation
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: whoisasx <adil.business4064@gmail.com>
* chore(npm): suppress prebuild-install deprecation warning
The prebuild-install package is no longer maintained, but it's a transitive
dependency of better-sqlite3 (optional) and node-pty (optional). Both packages
continue to use it reliably for downloading prebuilt binaries across platforms.
Suppressing this warning avoids noise in npm install output while we await
upstream changes. See https://github.com/ComposioHQ/agent-orchestrator/issues/1752
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(cli): warn users when spawning in permissionless mode
When agentConfig.permissions is unset, the schema defaults to
"permissionless", which passes --dangerously-skip-permissions to Claude
Code. Claude then shows a one-time confirmation prompt inside the tmux
session; if dismissed, the session exits silently with no user-facing
error — making it very hard to debug.
Add a post-spawn warning in ao spawn output explaining the mode and
how to either accept the prompt or switch to interactive mode. Also
improve the config-instruction docs to describe each permissions value
and the silent-exit risk.
Fixes#1754
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* fix(cli): direct users to dashboard terminal for permissionless prompt
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* removed unwanted changes
* fix(cli): fall back to local config when global config is missing in project supervisor
There are two separate things reading config:
- Dashboard (Next.js web server): loadDashboardConfig() — tries global config, falls back to local.
- Project supervisor (CLI process): loadConfig(getGlobalConfigPath()) only — no fallback.
When a user runs ao start with a local agent-orchestrator.yaml that has never been
registered in ~/.agent-orchestrator/config.yaml, the supervisor silently returned
with 0 lifecycle workers, leaving sessions frozen in their last known state and
PR status never updating.
loadSupervisorConfig() mirrors the dashboard fallback: try global config first,
fall back to loadConfig() (local discovery) on ENOENT. ConfigNotFoundError (no
config anywhere) is handled by isMissingGlobalConfigError so the supervisor still
exits cleanly when AO has never been configured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): address review feedback and fix failing supervisor tests
Review feedback from greptile:
- Remove unreachable ConfigNotFoundError branch from loadSupervisorConfig's
catch — loadConfig(globalConfigPath) cannot throw it with a non-null path.
The error still propagates from the fallback loadConfig() (no args) and is
caught by isMissingConfigError at the outer scope.
- Rename isMissingGlobalConfigError → isMissingConfigError. After extending it
to catch ConfigNotFoundError (the "no config anywhere" case), the old name
was misleading.
Test fix (CI was failing):
- Add ConfigNotFoundError to the vi.mock factory so tests that exercise the
fallback path compile.
- Add coverage for the new fallback paths:
- ENOENT on global config → falls back to loadConfig() (local discovery)
- Non-missing-config errors (e.g. invalid yaml) propagate up
- No config anywhere → supervisor exits cleanly, no workers attached
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(cli): drop unused config-instruction module
The getConfigInstruction() helper and its config-help subcommand were the
module's only consumers, so remove both. The schema URL referenced via
CONFIG_SCHEMA_URL still lives in core and is reachable from the yaml's
$schema field for editor-side completion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(cli): tighten loadSupervisorConfig comment
Replace the rationale paragraph with a one-line description of what the
function does. The "why" lives in the PR description and commit history.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): scope loadSupervisorConfig ENOENT guard to the global config path
Review feedback from greptile: the ENOENT catch was broader than the matching
guard in isMissingConfigError. If the global config exists but references a
missing nested file, the supervisor would silently fall back to the local
config instead of surfacing the configuration error.
Mirror the path check from isMissingConfigError so only ENOENT for the global
config path itself triggers the fallback. Add a test covering the nested-file
case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: restore trailing newline in .npmrc
Review feedback from greptile. The trailing newline was accidentally dropped
by an earlier "removed unwanted changes" commit on this branch and is
unrelated to the supervisor fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Revert "chore(cli): drop unused config-instruction module"
This reverts commit cb58e15d02.
* revert the unwanted comments
* Addressing comments to make the resolved path as a fallback for supervisor
* test(cli): assert local fallback configPath propagates distinct from global
Reviewer nit: the ENOENT-fallback test was returning makeConfig with the
default `/tmp/global-config.yaml` path, so the assertion on
ensureLifecycleWorker could pass even if the supervisor accidentally
propagated the global path. Use a distinct local path in the fallback
fixture so the regression coverage is honest.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
* feat(core): record activity events for config, plugin-registry, and storage migration
Closes#1658.
Surfaces failures that previously left no trace in `ao events list`:
- Config: project_resolve_failed (degraded project), project_malformed,
project_invalid, migrated. Local-config errors carry the project's
projectId so `ao events list --project <id>` finds them.
- Plugin registry: load_failed (built-in and external), validation_failed,
specifier_failed. External plugin failures are now queryable instead of
living only in stderr.
- Migration: blocked (active sessions), project_failed (per-project),
rename_failed (.migrated rename), completed (one summary with totals),
rollback_skipped (post-migration sessions present).
- Agent report: api.agent_report.transition_rejected and apply_failed for
observability into rejected reports.
Migration emits at most one event per milestone (per project failure +
single completion summary) so event volume scales with errors, not input
size. Config events redact sensitive keys via the existing sanitizer.
Adds regression tests for all four MUST emits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(core): avoid duplicate config activity events
* test(core): cover blocked storage migration event
* test(core): satisfy lint in migration event mock
* fix(core): emit migration completed for no-op runs
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: whoisasx <adil.business4064@gmail.com>
* feat(web): activity events for webhooks and mux WebSocket (#1656)
Closes#1656 — adds the 10 activity events called out in the issue, covering
webhook ingress (4) and the mux WebSocket terminal server (6). Builds on the
ActivityEvent infrastructure landed in #1620.
Webhook events (api source):
- api.webhook_unverified (warn) — 401 signature verification failure
- api.webhook_rejected (warn) — 413 payload exceeds maxBodyBytes
- api.webhook_received (info|warn) — 202 success, with parse/lifecycle error counts
- api.webhook_failed (error) — 500 outer catch / pipeline crash
Mux WS events (ui source — Node-side server only):
- ui.terminal_connected — one per mux WS connection
- ui.terminal_disconnected — one per close
- ui.terminal_heartbeat_lost (warn) — once on 3 missed pongs (was console-only)
- ui.terminal_pty_lost (warn) — fires only when subscribers are still attached,
distinguishing "PTY actually died" from "user closed browser"
- ui.terminal_protocol_error (warn) — invalid mux client message
- ui.session_broadcast_failed (warn) — emitted on the healthy→failing transition
only; re-arms after a successful poll so a long outage yields one event, not 20/min
Invariants honored: no raw payloads or signatures in `data`, no client IPs in
`summary` (kept in `data` only), no per-keystroke / per-pong fan-out — only on
state transitions. `api.webhook_unverified` is the security-audit event; data
captures `slug` and `remoteAddr` but never the failed signature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): harden webhook and PTY activity events
* chore(ci): retrigger checks
* fix(web): record PTY loss across mux exit paths
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: whoisasx <adil.business4064@gmail.com>
* feat(web): record activity events for API mutation routes
Wire recordActivityEvent calls into all POST/PATCH/DELETE handlers under
packages/web/src/app/api/ so RCA can answer "did the user click X?" beyond
just success/failure status codes. Source: "api" (added in #1620).
Coverage:
- session mutations: spawn, kill, send, message, restore, remap (with
failure variants for SessionNotRestorable/WorkspaceMissing/etc.)
- orchestrator + PR: orchestrator spawn, PR merge (with rejected/failed)
- project + config: add, update, remove, repair, reload
- issue + verify + labels: issue create, verify, label setup
Sanitization rules preserved:
- never include request/response bodies
- *_message_* events include messageLength only, never the message text
- project_updated records changedKeys, never values (config can carry tokens)
Tests: regression coverage for the 12 MUST emits across two new test
files, plus negative-path sanitization assertions.
Closes#1655
* fix(web): refine api activity failure events
* fix(web): align project activity event tests
* fix(web): avoid orchestrator spawn failure double emit
---------
Co-authored-by: whoisasx <adil.business4064@gmail.com>
* feat(core): activity events for recovery, metadata corruption, agent-report
Wires activity events into three forensic-critical paths so RCA can
reconstruct what happened after the fact:
1. recovery subsystem
- recovery.session_failed (MUST) per failed session in runRecovery
- recovery.action_failed (SHOULD) on recoverSession outer catch
Adds "recovery" to ActivityEventSource so `ao events list --source
recovery` reconstructs an `ao recover` invocation timeline.
2. metadata corruption
- metadata.corrupt_detected (MUST) when mutateMetadata renames a
corrupt session-metadata file to .corrupt-{ts}. Includes
data.renamedTo and a 200-char data.contentSample (B11) for forensic
recovery. Previously only console.warn — silent overwrites had no
queryable signal.
3. agent-report apply path
- api.agent_report.transition_rejected (SHOULD)
- api.agent_report.session_not_found (COULD)
Per B22, recovery events fire per session, not per probe step. Per B11,
metadata.corrupt_detected truncates contentSample to 200 chars (full
file would exceed the 16KB sanitizer cap).
Closes#1660
* fix(core): align forensic activity event metadata
* fix(core): cover single-session recovery failures
* fix(core): improve corrupt metadata event attribution
* fix(cli): expose activity event source filter
---------
Co-authored-by: whoisasx <adil.business4064@gmail.com>
PR #1819 accidentally introduced ao-pr1483, hermes-agent, and libkrunfw
as submodule entries (mode 160000) with no .gitmodules. These are
orphaned refs that serve no purpose and should not be on main.
Co-authored-by: AO Bot <ao-bot@composio.dev>
* fix(cli): rebuild better-sqlite3 when binding is missing
* fix(cli): support Windows postinstall rebuild shims
---------
Co-authored-by: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com>
* fix: recover native session restore fallback
Persist native agent restore metadata from dead runtimes and fall back to a fresh launch when native restore context is missing, so stuck sessions do not permanently block startup.
* fix: avoid redundant dead-session metadata discovery
* test: harden Linear list issue polling
* perf(web): eliminate sidebar re-renders on every SSE tick
Resolves#1844
The SSE hook delivers a new sessions array reference every 5 seconds
even when content is identical, causing sessionsByProject to recompute
and all session rows to re-render on every tick.
Three fixes in ProjectSidebar.tsx:
* sessionsKey + sessionsRef: replace unstable array reference in memo
deps with a content-derived string; memo only fires on real changes
* SessionRow memoized component: rows skip re-renders when props are
unchanged; navigate and startRename stabilised with useCallback
* SessionDot memoized: status indicator skips re-renders when level
prop is unchanged
A quiet SSE tick now touches zero React components in the sidebar.
* fix(web): add displayName, displayNameUserSet, branch to sessionsKey hash
Without these fields, a session rename delivered via SSE did not
trigger sessionsByProject to recompute. The stale session object
held the old displayName, and once the optimistic pendingRename
was cleared the sidebar silently reverted to the pre-rename title.
Addresses review feedback on PR #1846.
* feat(web): sidebar and dashboard header UI/UX polish
Removes state text labels from sidebar session rows so the colored dot
is the sole status indicator, matching the intended design. Fixes the
sidebar compact header height to align with the 48px main header.
Adds session count summary pills to the dashboard project header.
Converts CopyDebugBundleButton to an icon-only compact form so the
actions row stays vertically centered. Fixes the project page wrapper
missing flex-1 which caused a right-side viewport gap in the horizontal
shell layout.
* fix(web): resolve lint errors from UI/UX polish
Remove LEVEL_LABELS, _isLoading prefix for unused loading var, and dead
title variable from ProjectSidebar. Remove unused isDashboardSessionStatus
and isActivityStateValue from the project session page. Remove
react-hooks/exhaustive-deps eslint-disable comments for a rule not in the
ESLint config. Stabilize startRename via pendingRenamesRef so the callback
does not recreate on every rename state change, preventing unnecessary
SessionRow re-renders. Remove non-null assertion in Dashboard.tsx
handleToggleSidebar with a null guard.
* fix(web): replace native Node 25 localStorage stub with full in-memory mock
Node.js 25 exposes a native localStorage via --localstorage-file that lacks
.clear() and .key(), causing all UpdateBanner tests to throw TypeError.
Replace the global with a complete in-memory implementation so test suites
work across all Node versions.
* fix(web): seed sidebar with all sessions on hard refresh and eliminate per-project layout re-render
- Hoist sidebar layout from projects/[projectId]/layout.tsx to projects/layout.tsx so it renders
once for the entire /projects/* subtree and never re-mounts when switching between projects
- Pass getDashboardPageData("all") so initial sessions cover every project, not just the primary
- Extract ProjectLayoutClient from the old client layout for clean server/client split
- Simplify per-project empty state to "No active sessions" only, removing the second hint line
- Restore accidentally deleted Dashboard empty-state test for zero-projects install
- Fix Reflect.deleteProperty lint error in localStorage mock; drop unused AttentionLevel import
and dead effectiveDisplayName/pending variables from ProjectSidebar editing block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): address PR review — orchestrators prop, sessionsKey, badge count, mobile overlay, tests
* fix(web): remove duplicate skeleton sidebar from project loading state
* fix(web): restore orchestrator button and eliminate Session unavailable flash
Re-add the orchestrator icon + menu item that were removed during the merge
conflict resolution. Convert the icon from a <Link> to an <a> that calls
navigate() with the full session object so ProjectSessionPage gets an
instant sessionStorage cache hit instead of starting with session=null
and briefly showing the "Session unavailable" error card.
* fix(web): eliminate Session unavailable flash on orchestrator navigation
React Strict Mode aborts the first fetchSession() during its unmount/remount
cycle. The aborted finally reset fetchingSessionRef but set loading=false,
briefly showing the error card before the retry completed. Fix: keep loading=true
on abort (no session yet), and immediately retry via fetchSession() once the
ref is clear. mountedRef guards the retry so it only fires on Strict Mode
remounts — not on genuine navigation-away unmounts, which would leak requests.
* fix(web): fix working pill count and layout of error states in project session page
- Dashboard topbar "working" pill now counts only actively working sessions,
not working + pending (which inflated the number incorrectly)
- Error/loading/missing states in ProjectSessionPage wrapped in
dashboard-main--desktop so they fill the flex shell beside the sidebar
instead of shrinking to content width
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web,core): "Launch Orchestrator (clean context)" button
Adds a dashboard action that replaces the project's canonical
orchestrator with a fresh one — killing any existing orchestrator,
deleting its metadata, and spawning a new session with no carryover.
Why: users had no way to start an orchestrator with a clean slate from
the dashboard. Previous orchestrator context (conversation history,
stale state) silently carried over via the existing "Open Orchestrator"
flow, which only worked for first-time spawn anyway.
- core: new SessionManager.relaunchOrchestrator(config) that kills +
deletes existing metadata then calls spawnOrchestrator. Ignores
project.orchestratorSessionStrategy — replacement is the whole point.
Coalesces concurrent calls via a dedicated relaunchOrchestratorPromises
map (separate from ensureOrchestratorPromises since the semantics
differ — a relaunch behind an ensure must not return the existing
session).
- web: POST /api/orchestrators accepts { clean: true } to route to the
new method. OrchestratorSelector renders a "Launch Orchestrator
(clean context)" button that uses window.confirm() before discarding
an existing orchestrator; no confirm when none exists.
Closes#1900, closes#1080.
* fix(core,web): address PR #1904 review
- core: cross-map race between ensureOrchestrator and relaunchOrchestrator.
Each now awaits the other's in-flight promise (keyed by sessionId) before
proceeding. Prevents (a) relaunch skipping the kill while ensure's
spawnOrchestrator is mid-reservation, and (b) ensure returning a session
that relaunch is about to kill. Adds two race regression tests.
- web: align handleSpawnNew with handleRelaunchClean via the void expression
form; add "Launching..." in-progress label to the clean-context button and
a test that asserts it renders during POST.
* refactor(web): rip out Orchestrator Selector page; relocate clean-launch action
There is only ever one orchestrator per project, so the /orchestrators
selector page is meaningless. Delete it along with its component, tests,
and the unused mapSessionsToOrchestrators util. Drop GET /api/orchestrators
(only consumer was the deleted page). Remove /orchestrators from project
revalidate lists.
The "Launch Orchestrator (clean context)" action that previously lived on
the deleted page now appears in two places:
- Dashboard header: a "Relaunch (clean)" button renders alongside the
Orchestrator link whenever a project orchestrator exists. Uses
window.confirm before discarding state.
- Orchestrator session page: a "Relaunch (clean)" button in the
SessionDetailHeader for live orchestrator sessions, calling
POST /api/orchestrators with clean:true and reloading the session view.
* refactor(web): remove Relaunch (clean) action from the Dashboard
Keep the clean-launch action only on the orchestrator session page —
that's where the user has the context to decide on a destructive
restart. The Dashboard header just links to the orchestrator (or shows
the existing Spawn Orchestrator button when none exists).
* fix(web): surface relaunch failures with an inline error banner
After confirm + POST /api/orchestrators with clean:true, the previous
implementation only logged failures to console.error — leaving the user
on a stale page with no signal that the destructive action partially
executed. relaunchOrchestrator kills before respawning, so a failed
respawn means the server has no orchestrator while the client still
renders the old session view.
Add local relaunchError state, set it on catch (parsed from the JSON
error response when available), and render a dismissible error banner
above the terminal area. The banner explicitly warns the user that the
previous orchestrator may already be terminated and points them at the
project dashboard to retry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web,core): address PR #1904 review from @i-trytoohard
- web: navigate to the new orchestrator's session path (from POST
response) instead of window.location.reload(). Orchestrator session
IDs are fixed per project so the path is the same in practice, but
reading from the response is the right contract and a hard nav forces
the terminal WebSocket to reconnect cleanly against the new tmux.
- web: remove the `!terminalEnded` gate on the Relaunch (clean) button
in SessionDetailHeader. Terminated orchestrators are exactly when the
user wants to relaunch — hiding the button there was wrong.
- core: log a warning instead of silently swallowing when an in-flight
cross-map promise (ensure waiting on relaunch, or relaunch waiting on
ensure) rejects before its caller proceeds. The catch-and-continue
semantics are correct (the caller will re-check state anyway) but
invisible failures were a debugging hazard.
Adds a regression test that the button stays visible on terminated
orchestrator sessions and that successful relaunch navigates via
window.location.href.
* fix(agent-claude-code): add AO-JSONL safety net cascade
claude-code now has the same AO-JSONL fallback cascade as codex; protects against ~/.claude/projects slug drift (cf. #1611, commit 582c5373).
Closes#1897. References #1899.
* fix(agent-claude-code): let stale native activity use AO fallback
When Claude's latest native JSONL entry predates the AO session, fall through to the AO activity JSONL cascade before preserving the prior idle default. This keeps terminal-derived waiting_input from being hidden by an old native file.
Addresses review on #1903. References #1899 and closes#1897.
* fix(core): sm.list() no longer writes terminated state to disk (#1735)
sm.list() was bypassing the lifecycle manager's probe decision matrix by
persisting terminated state immediately on a single isAlive() failure.
The dashboard's 3s poll via /api/sessions/patches called sm.list() ~10x
more often than the lifecycle manager, so a transient runtime failure
would permanently kill the session before the lifecycle manager could
evaluate all three probes (runtime, process, activity).
Changes:
- sm.list() now persists "detecting" instead of "terminated" when it
detects a dead runtime, so the lifecycle manager's resolveProbeDecision
pipeline remains the single authority on terminal decisions.
- /api/sessions/patches now calls listCached() instead of list(),
preventing the dashboard's 3s poll from probing runtimes directly.
The cache TTL (35s) aligns with the lifecycle manager's 30s poll.
- Updated CLAUDE.md invariants to reflect the new behavior.
* fix(core): skip re-persisting detecting state on subsequent list() calls
Check the on-disk lifecycle state (raw metadata) instead of the
in-memory state when deciding whether to persist. Enrichment already
sets detecting in-memory, so the previous guard always skipped the
persist block. Using the on-disk state ensures:
- First detection: persists detecting + lastTransitionAt
- Subsequent calls: skips re-write, preserving the original timestamp
* feat(core): add CI failure summary SCM contract
Add an optional SCM getCIFailureSummary hook and shared CIFailureSummary shape so providers can return failed jobs, failed steps, run URLs, and bounded log tails without changing existing SCM implementations.
* feat(scm-github): summarize failed CI logs
Use failed GitHub check URLs to locate Actions run/job IDs, fetch gh run view --log-failed output, parse the step column from the gh log format, and cap each failed-job log tail at 120 lines. Return null when no failed run logs can be fetched so lifecycle can fall back cleanly.
* feat(lifecycle): send actionable CI failure details
Prefer SCM CI failure summaries when composing ci-failed agent messages, including failed job/step, run URL, and log tail. Fall back to check names/statuses for SCM plugins without getCIFailureSummary, and remove the generic default ci-failed text so default handling relies on the lifecycle-composed payload.
State invariants preserved: this only changes ci-failed reaction payload composition and dispatch hashing. It does not change lifecycle status decisions, state-machine transitions, ci-failed persistence, retry/escalation thresholds, or stable-passing tracker reset semantics.
* fix(lifecycle): harden CI failure summaries
Address review feedback: escape log-tail lines that could close markdown fences, report the last failed-step column from gh failed logs, and pass already-known failed checks into getCIFailureSummary to avoid duplicate CI check fetches.
State invariants preserved: lifecycle transition decisions, retry/escalation budgets, persistent ci-failed tracker behavior, and stable CI passing reset semantics remain unchanged.
* refactor(lifecycle): centralize CI failure check lookup
Deduplicate CI failure check lookup, filtering, and fingerprint generation across transition-time enrichment and follow-up CI-failure dispatch while preserving the existing fetch/no-fetch split.
State invariants preserved: lifecycle transition decisions, reaction retry/escalation behavior, ci-failed tracker persistence, and stable-passing reset semantics are unchanged.
* fix(scm-github): fetch in-progress failed job logs
* fix(lifecycle): label CI failure URLs explicitly
---------
Co-authored-by: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com>
* fix(core): clear terminal markers on non-terminal lifecycle transitions
Clear stale completedAt/terminatedAt whenever lifecycle helpers settle on a non-terminal session state, including restore-to-working paths.
Preserved invariants: terminal-state idempotence remains intact because done/terminated states keep their terminal markers; session state transitions still flow through existing code that updates lastTransitionAt before marker hygiene runs.
* fix(core): share terminal marker hygiene
Move non-terminal marker clearing into lifecycle-state so restore and lifecycle-transition paths use one terminal-state source of truth.
Preserved invariants: terminal-state idempotence still keeps done/terminated markers; existing transition code continues to update lastTransitionAt before marker hygiene runs.
---------
Co-authored-by: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com>