Skip to content

feat: Aether cloud provider — run T3 Code agents in isolated microVMs - #5995

Closed
pranav100000 wants to merge 46 commits into
pingdotgg:mainfrom
pranav100000:aether-cloud-provider
Closed

feat: Aether cloud provider — run T3 Code agents in isolated microVMs#5995
pranav100000 wants to merge 46 commits into
pingdotgg:mainfrom
pranav100000:aether-cloud-provider

Conversation

@pranav100000

@pranav100000pranav100000 commented Aug 10, 2026

Copy link
Copy Markdown

What this adds

A first-class Aether provider for T3 Code: run any T3 Code agent (Claude Code, Codex, …) in an isolated cloud microVM instead of locally — one click, no VPS, no Tailscale, and no long cold start. T3 Code stays the client; the agent, workspace, and a live preview all run in the VM.

Highlights:

  • Provider driver wired into the existing provider registry (settings, model catalog).
  • REST task client + WS transport + full event mapper so cloud turns stream into the normal T3 Code timeline.
  • Turn lifecycle + mirror-sync engine + write guards — durable-authoritative settlement so checkpoint diffs render correctly.
  • Question/plan responses, revert ledger, model switch.
  • Cloud port previews surfaced in the composer (public HTTPS, per-workspace token — no Tailscale).
  • Cloud terminal — attach a real shell in the workspace VM, with reconnect on transient drops.

Shape

Built as a clean, reviewable commit chain:

  1. provider driver skeleton — settings, registration, vendored catalog
  2. REST task client + adapter session core
  3. workspace attach, WS transport, full event mapper
  4. turn lifecycle, mirror sync engine, write guards
  5. question/plan responses, revert ledger, model switch
  6. durable-authoritative turn settlement (checkpoint diffs)
  7. cloud port previews in the composer
  8. cloud terminal — attach a shell in the workspace VM (+ reconnect)

~86 files, largely isolated under apps/server/src/provider/Layers/aether/* plus a thin provider registration; packages/contracts additions are additive.

Opening as a draft to start the conversation — happy to split it or adjust to fit however you'd want this to land.

Note

Add Aether cloud provider to run T3 Code agents in isolated microVMs

  • Introduces a new aether provider driver (AetherDriver.ts) that connects to the Aether cloud API, spawning agent tasks over REST and receiving real-time events via WebSocket.
  • Adds an AetherAdapter (AetherAdapter.ts) implementing the full ProviderAdapterShape: session lifecycle, turn send/interrupt/resume, and git mirror sync that hard-resets local checkouts to match the remote VM state at turn settle.
  • Adds AetherMirrorRegistry and mirror guards (AetherMirrorGuards.ts) that block local VCS and filesystem mutations while a cloud session owns the working directory, surfacing refusals as structured GitCommandError / ProjectWriteFileError.
  • Adds AetherTerminalManager (AetherTerminalManager.ts) to route terminal RPCs (open, write, resize, close) to cloud VM PTY sessions; the WS layer routes per-thread to either the local or cloud manager.
  • Introduces a thread.worktree.attach-managed orchestration command with concurrency guards, replaces the prior thread.meta.update dispatch for bootstrap worktrees, and tears down orphaned bootstrap worktrees when the attach does not apply.
  • Adds port.opened runtime events that surface live port previews in the web and mobile timelines, including an embedded or external preview opener.
  • Adds deterministic AetherTextGeneration for branch names, commit messages, PR content, and thread titles without external model calls.
  • Risk: local VCS mutations (pull, stacked actions, worktree create/remove) will hard-fail with a structured error when an Aether session is active for that directory; this is a new behavioral constraint on the WS RPC layer.

Macroscope summarized 82149c7.


Note

High Risk
Touches orchestration, persistence (new worktree_managed column), and VCS/file write paths with new refusal guards that can block mutations while an Aether session owns a checkout. Incorrect worktreeManaged handling could skip clean-tree preflight on user worktrees and discard uncommitted work.

Overview
Adds a first-class Aether provider so agents can run in isolated cloud microVMs, with local checkouts treated as one-way mirrors of the VM.

Server: new AetherDriver/adapter stack (REST + WS), mirror sync, and AetherMirrorRegistry/AetherMirrorGuards that refuse local VCS/file writes while a session owns the cwd. Introduces server-only thread.worktree.attach-managed plus a persisted worktreeManaged marker so only bootstrap-created worktrees skip clean-tree preflight. Cloud terminals close on thread deletion; port.opened activities surface live preview URLs.

Clients: Aether icon and model selection; new drafts with an Aether model default to worktree mode via a modeUserSet flag so incidental draft writes do not pin the wrong mode. Mobile work log makes port previews tappable.

Also gates upstream-only deploy/nightly jobs on pingdotgg/t3code and switches CI runners to stock GitHub images with longer timeouts.

Reviewed by Cursor Bugbot for commit 82149c7. Bugbot is set up for automated code reviews on this repo. Configure here.

pranav100000and others added 16 commits August 8, 2026 05:54
The path is a committed submodule gitlink with no .gitmodules entry
anywhere in the repo (upstream has no .gitmodules at all). Plain clones
ignore it, and upstream CI sparse-checkout excludes /.repos/, but any
submodule-aware clone fails hard:
fatal: No url found for submodule path
'.repos/alchemy-effect/.vendor/alchemy' in .gitmodules (exit 128)
That broke every Aether workspace clone of this repo. Nothing references
the gitlink; the directory contents were never part of this repository.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
…release (pingdotgg#1)
* ci: run fork CI on GitHub-hosted runners; guard upstream-only deploy/release
Blacksmith runner labels are bound to the pingdotgg account and queue
forever on this fork, so CI jobs move to GitHub-hosted runners
(ubuntu-24.04 / macos-latest) with timeouts widened for the smaller
machines. The relay deploy and the nightly release schedule are
upstream-only and now skip outside pingdotgg/t3code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* ci: widen slow-runner timeout for the image-compression give-up test
The too-large give-up path walks the whole quality/scale ladder and takes
~18s on the 2-core GitHub-hosted runners this fork uses, tripping the 15s
default. Explicit 60s timeout for that one test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* style: oxfmt formatting for the widened test timeout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ored catalog (pingdotgg#2)
* feat(aether): provider driver skeleton — settings, registration, vendored catalog
AetherDriver T1: contracts settings (AetherSettings via
makeProviderSettingsSchema, apiBaseUrl with prod default, API key via
sensitive AETHER_API_KEY env var), driver registration with
makeManagedServerProvider snapshot (GET /profile probe: missing-key /
401 / transport failures all distinguished; catalog models with
reasoning-effort option descriptors on every draft path), typed
not-implemented adapter stubs (real protocol lands in T3-T6),
deterministic textGeneration stubs, and vendored aether knowledge
(catalog, 20-value canonical item-type map, tool-display parser port)
with source paths + sync recipe documented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): keep every snapshot unavailable until the turn protocol exists
Review: a valid-key instance passed isProviderInstancePickerReady and
routed turns into the not-implemented adapter. The draft funnel now
stamps availability=unavailable with an explicit preview reason on every
probe outcome — key validation still works in settings, the picker
excludes Aether until T6 removes the gate. Pinned by test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): route the healthy-probe draft through the availability gate
The success path built its snapshot directly and skipped the gated draft
funnel, leaving a healthy instance picker-visible — exactly the reviewed
defect. All probe outcomes now share the single gated funnel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): gate honors the full unavailable contract on every snapshot
Review round 2: availability=unavailable snapshots MUST set
enabled:false and installed:false (server.ts contract), and mobile's
model options only honor those flags — so the T6 gate now forces all
three on the pending snapshot AND every probe draft through one
gateUntilTurnProtocol helper. Key-validation fidelity stays in
auth/message; top-level status reads disabled while gated. Tests pin the
whole flag set on pending, disabled, and healthy-probe paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(aether): REST task client + adapter session core
T2+T3 of the AetherDriver chain. REST client (restClient.ts +
restSchemas.ts): tasks create/respond/stop/update/remove-from-queue,
task read with status-probe dispatch (unknown-status forward-compat
carrier; known status with malformed payload fails loudly), conversation
messages/delta with pagination, projects, loose additive-tolerant
schemas, tagged errors for 401/402/404/409(code+kind)/4xx/transport/
decode, 30s timeout, caller-driven retry via client_message_id.
Session core: startSession preflight (clean tree, pushed+synced branch,
actionable remediations), repo→project resolution through the shared
normalizeGitRemoteUrl (ssh/https equivalence; ambiguity listed loudly),
resumeCursor {schemaVersion, taskId, latestSequence, turnLedger}
validated against task existence AND project membership (404 → typed
session-not-found), stopSession/stopAll as pure disconnects, minimal
readThread snapshot via vendored classification. sendTurn and the event
pump stay typed not-implemented until T4-T6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): stopAll emits one graceful session.exited per thread
Review: bulk disconnect cleared the session map silently, so ingestion
never saw the per-session exit events it uses to clear active-turn and
liveness state — stale running UI after ProviderService teardown. Both
disconnect paths now share one pure-disconnect helper; test pins one
graceful exit per thread.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…er (pingdotgg#4)
* feat(aether): workspace attach, WS transport, and the full event mapper
T4+T5: the live pipeline. wireEvents parses the 13-kind agent union
loosely (unknown-kind/malformed carriers; the socket never dies on a
frame). eventMapper is the single WS+durable transform: deterministic
event ids from durable identity (crash replay collides idempotently),
durable-wins dedupe, exactly-one settle per turn, WS/REST pending-input
correlation, the ready-not-waiting rule for message-idle, vendored
classification + parseFileChanges for tool cards, todo_list → plan
updates, truncation → warning. workspaceSocket: attach poll with every
terminal branch (errored payload, parked null-context durable-only),
connect union incl. 409-as-data, passive never boots a VM, reconnect
ladder re-running full attach + delta reconciliation from the cursor.
Adapter streams mapped events on resumed sessions; teardown closes
scoped pumps. Golden fixtures for both transports; 150 tests in the
touched surfaces; full suite 2055 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): attribute REST-delta rows of the in-flight turn to its TurnId
Review: durable rows carry no turn field, so rows of the active turn
finalized with turnId null and their settle never owned them. The wire
turn id IS the user row that opened the turn, so that row's sequence is
the turn boundary: rows above it get the mapped aether-turn id, the
previous turn's tail stays unowned. Mutation-verified boundary tests.
The reasoning-shape finding is refuted with evidence in the PR thread:
no driver's reasoning renders today (ingestion reads only
assistant_text/assistant_message; reasoning is not a tool-lifecycle
type; zero UI consumers) — the mapper already emits the ecosystem shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): keep the previous turn's late tail owned across a warm transition
Review round 2: a delta carrying [tail-of-u1, opener-u2, rows-of-u2]
attributed the tail to undefined even when the mapper was already
tracking u1 — it finalized unowned moments before trackTurn(u2) settled
u1. Pre-opener rows now fall back to the tracked turn; only a cold
mapper leaves them unowned. Test pins the reviewer's exact repro
including tail→settle→next-output ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): opener-based turn attribution + cross-task frame guard
Review round 3. Attribution now models the mapper's own invariant
directly: delivered user rows open turns (queued/cancelled park ahead of
theirs and do not), rows before the first opener fall back to the
tracked turn, and opener-less batches sit mid-turn under
activeProcessingTurn — which makes the cold resume to an awaiting task
own both its output rows and the pending-input request (captured before
the settle clears tracking). The workspace socket now drops frames whose
taskId is not the subscribed task, logged once per foreign task. Cold
golden-replay snapshot legitimately gains four owned turnIds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): close the raw socket on every pre-open failure path
Review round 4: an upgrade error/close before open failed openSocket
before the acquireRelease finalizer registered, and the reconnect ladder
retries open failures indefinitely — one leaked socket per attempt. All
pre-open exits (error, close, timeout, interrupt) now close the raw
socket themselves; close() is idempotent. Leak pinned by the open-retry
test asserting both failed sockets closed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): session.exited carries providerInstanceId
Ingestion rewrites the thread session from this event and preserves
instance identity only when the event carries it — every other adapter
emission already stamped it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…er goes live (pingdotgg#5)
* feat(aether): turn lifecycle, mirror sync engine, write guards, gate flip
T6 — Aether becomes selectable end to end. sendTurn: create (composite
slug + effort validation, base_branch from preflight, turn-1 wire id
harvested from the timeline), respond (deterministic epoch-scoped
client_message_id), steer (deferred turn.started until pickup, FIFO).
interruptTurn: stop with discard, re-offered steer texts, read-side
confirmation, interrupted settle through the pipeline. Mirror engine:
fingerprint verify (content-tree via temp index; catches edits,
untracked files, local commits) → git-channel diff over the session
socket → fetch + resolve the diff's own baseRef → reset --hard +
clean -fd → reconstructed unified diff apply (modes, renames, no-newline,
binary via files read) → only then settle; detached settles skip lazily;
pauses are loud, never silent. Acceptance tests run against real temp
git repos. Fork-side guards: refcounted mirror registry + ws.ts refusals
at all seven dispatch sites, removeWorktree keyed on resolved target
(basename bypass covered). The T1 availability gate is removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): router-before-onConnected, turn-start ordering, binary path guard
Review round 1 on T6. The request-response router now drains before
onConnected fires (an onConnected reconcile that requests a git diff
completes instead of deadlocking — regression test with a hang guard);
sendTurn records the turn and emits turn.started before forking the
attach pipeline so a fast first settle cannot precede its start; binary
diff paths (oldPath removal AND newPath write) are validated
repo-relative — absolute paths, '..' segments, and resolved escapes
pause loudly and touch nothing outside the mirror, pinned by sentinel
tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): idempotent first-turn retry; validate binary paths before mutating
Review round 2 on T6. A createTask that succeeded but failed its turn-1
harvest now leaves the session in an explicit firstTurnPending state:
the retry re-enters the first-turn path (no second create, no respond —
the prompt can never double-send), a different-text retry refuses
loudly, and bring-up completion clears the flag. Binary diff application
is two-phase: every oldPath/newPath in the batch validates repo-relative
BEFORE any removal or write, so a rename with a safe oldPath and an
escaping newPath refuses with the mirror untouched. Both pinned by
tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): full dispatch fingerprint on first-turn retry; refuse .git paths
Review round 3 on T6. The pending-first-turn guard now fingerprints
every dispatch-relevant input (prompt, resolved slug, effort,
interaction mode, attachment payloads; length-prefixed control-char
join — compared, never parsed) so a same-text retry with changed
attachments or model refuses instead of silently proceeding. The binary
path validator additionally refuses any '.git' segment — direct writes
bypass git's refusal to track such paths, and .git/hooks would be code
execution on the next git invocation; both cases pinned in the escape
test matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): writeFile guard checks the resolved target, not just cwd
Review round 4 on T6: projects.writeFile resolves relativePath under
cwd, so a parent-project cwd could descend into an active mirror
without owning it. New ownsPathWithin containment check (at-or-under
any claim) guards the resolved target; prefix-sharing neighbours and
siblings stay writable, pinned by tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…olish (pingdotgg#6)
* feat(aether): question/plan responses, revert ledger, model switch, polish
T7: respondToUserInput maps t3 answers to aether's exact ask_user wire
shape (index-keyed answers, -1 custom sentinel + customAnswers); plan
accept/reject rides the fresh-turn route t3 actually uses, as
propose_plan {approved, feedback}; stale requests render t3's
stale-request affordance; 409 bodies surface their decoded message after
a delta re-sync; typed turn→message ledger rides the resume cursor;
rollback is a typed one-way-mirror refusal.
T8: in-session model switch via read-modify-write full-replace PUT
(auto_fix flags read live first); explicit interaction_mode on
plan-mode sends; remote-originated turns surface as warning cards with
the injected text; cancelled steers settle interrupted and re-offer
their text; out-of-band question resolution clears the pending panel
(with a corrective ready when observed into message-idle); mobile
ProviderIcon + server badge parity; load-bearing stopAll copy annotated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): option-only model switch, mobile empty-option questions, reconcile ordering
Review round 1 on T7/T8. (1) The between-turns switch guard now fires on
a resolved reasoning-effort change with the same model slug, not only a
slug change — an option-only switch reaches the task instead of silently
keeping the old effort. (2) apps/mobile threadActivity keeps an
answerable question with zero parsed options (custom-answer-only Aether
question) instead of dropping it, matching the mapper/web contract.
(3) The idle-session eager reconcile no longer lets a stale pre-reconcile
event emit after the reconcile catches the same completion — ordering
made deterministic. Each pinned by a test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…s render (pingdotgg#9)
Fixespingdotgg#7. Aether stamps every live WS frame with turnId = msg.messageId,
a fresh crypto.randomUUID minted per prompt dispatch — distinct from the
durable user-row turn id the driver keys turns by. Keying settlement off
that random live id fragmented one user turn into multiple t3 turns (4
checkpoints; "Latest turn" diff read a post-change-vs-post-change pair
and showed nothing), and no per-turn alias could disambiguate a random
id arriving out of order across turn boundaries (a stale terminal frame
could settle the wrong or next turn).
Settlement is now durable-authoritative: when a durable turn is grounded,
live turn.completed/turn.failed/turn.awaiting_input frames no longer
settle (nor fabricate a runtime.error card) — they only trigger an
immediate durable reconcile so settle latency stays low; the durable
reconcile (task-status flip) emits the single settle, which the adapter
intercepts for mirror-sync-then-forward. A live random id can no longer
settle any turn in the grounded path. The cold mapper-only path keeps
live settlement so unit tests / degenerate resume still terminate.
Net effect: exactly one turn.started/turn.completed per user turn, the
mirror change in that settled segment, so t3's CheckpointReactor captures
baseline+post and the diff panel renders. Regression tests: live terminal
frame with a grounded turn emits no settle; stale-frame-after-next-turn-
start cannot settle the new turn; cold path still settles; mirror change
lands in the settled segment.
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ngdotgg#10)
* fix(aether): skip the clean-tree preflight for driver-owned worktrees
An Aether turn leaves its mirror output as uncommitted working-tree
content (so t3's diff/checkpoint panel shows it), but startSession's
clean-tree preflight then refuses the next thread on that checkout —
even a chat message trips it. When a thread runs in its own worktree,
that friction is pointless: a `git worktree add` branch has no upstream
and (after turn 1) a dirty tree, yet the mirror owns it exclusively and
resets --hard to baseRef every sync, so there is no user work to protect.
The orchestration layer (the authority on worktree ownership) now sets
managedWorktree=true on ProviderSessionStartInput when
thread.worktreePath is non-null; the Aether adapter then uses a
structural-only preflight (is-repo + non-detached) instead of the
clean-tree/upstream checks. The shared "Current checkout" path is
unchanged — it still refuses on uncommitted work, protecting real edits.
Both fresh and resume start paths flow through the shared helper.
Regression tests: a dirty, no-upstream, ahead managed worktree starts
ready; "Current checkout" with uncommitted changes still refuses; the
worktree cwd (not the project root) is what registers with the mirror.
Follow-ups (not blockers): default Aether threads to a fresh worktree in
the composer (primary-agent + live integration test), worktree cleanup
on thread archive, mobile default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): gate managedWorktree on a bootstrap-created marker, not path shape
Review: managedWorktree = (worktreePath !== null) also matched a user's
pre-existing secondary worktree, whose uncommitted edits the mirror would
then reset --hard/clean — data loss. Now the bootstrap prepareWorktree
handler stamps a durable worktreeManaged=true on the thread when it
creates a fresh ephemeral worktree; that marker is plumbed through the
projection (decider → projector → ProjectionThreads + migration 039) to
the read model, and the reactor sets managedWorktree only from it. A
user-attached worktree has no marker → the clean-tree preflight is
enforced and their work is protected. Regression test: worktreePath set
but unmanaged + dirty still refuses; bootstrap-managed still skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* fix(aether): make worktreeManaged server-authoritative and sticky
Review round 2 on T10. Two holes from making the marker a client-writable,
clearable meta field: (1) a client could smuggle worktreeManaged:true onto
a thread.meta.update and skip the dirty-tree preflight on its own worktree;
(2) the first-turn branch rename (a meta update omitting the field) cleared
it off a genuinely-managed worktree. Fix: worktreeManaged is dropped from
the client ThreadMetaUpdateCommand entirely and set only via a dedicated
server-origin ThreadWorktreeAttachManagedCommand the bootstrap emits, so no
client input can set it; and the projection preserves it across meta
updates that keep the same worktree, resetting only when the worktree path
changes. Tests: a smuggled client worktreeManaged is ignored; the
first-turn branch rename keeps the marker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
* test: update server-seam bootstrap sequences for the attach-managed command
The bootstrap now dispatches thread.worktree.attach-managed (server-only)
in place of the thread.meta.update it used to emit for the worktree
marker, so the three server.test.ts command-sequence assertions are
updated to match. Behavior unchanged; the new command still carries
worktreePath. (Missed initially because the scoped test run excluded
src/server.test.ts — full vp run test is green: 2158 passed.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YBSC5kYcomzhv5TJ1cn1h
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…omposer (pingdotgg#11)
A fresh, un-touched local composer draft with an Aether model selected now
defaults its Workspace to a new worktree, so the Aether cloud driver never hits
the clean-working-tree preflight error. This is a render-time overlay
(resolveProviderDefaultsToWorktree) — never persisted — so switching the model
to any non-Aether provider flips the Workspace back to the current checkout, and
every other provider's default is unchanged.
Hardening (from live testing + adversarial review):
- The overlay never leaks into persisted draft.envMode: the branch auto-seed
path uses the sticky (persisted) mode, not the effective overlay value.
- Explicit workspace picks and PR-checkout drafts are marked user-set; legacy
drafts (absent flag) are treated as user-set so upgrades never surprise-flip.
- The auto-worktree honors the newWorktreesStartFromOrigin preference.
…t the local scratch branch (pingdotgg#12)
A driver-owned worktree sits on a local-only branch that is never pushed; sending it as the cloud task base_branch failed workspace startup with remote_ref_missing (404) — the common path once new Aether drafts default to a worktree. The adapter now bases new tasks on the fork branch recorded in branch.<head>.gh-merge-base (a real origin branch), and only for new-task starts (resume never sends base_branch).
When the Aether cloud VM opens a port (agent runs a dev server), a 'Port N is live — Open preview' CTA appears in the thread and opens the workspace preview URL. The driver parses the workspace ports channel, builds the preview URL from the connect transport's preview_token ({port}-{workspaceId8}-{token}.preview.runaether.dev), and emits a port.opened runtime event → thread activity → web + mobile timeline. Verified end-to-end in the desktop app (chip URL returns the VM's app, HTTP 200).
…ingdotgg#14)
The 'Port N is live — Open preview' CTA now opens the workspace preview in the desktop embedded browser (right panel) via openPreviewSession + openBrowser, matching how discovered local ports open; falls back to the system browser / new tab on web or if the embedded session fails. Verified live: clicking the chip loads the microVM's app in an in-app webview.
…ngdotgg#15)
* feat(aether): cloud terminal — attach a shell in the workspace VM
Route the integrated terminal to a shell running INSIDE the Aether cloud
VM for cloud-backed threads, over its own tab-scoped workspace WebSocket
(channel:"terminal"), independent of the turn engine's agent stream.
Local threads keep their local PTY. No workspace-service changes — the
VM already serves the PTY.
- CloudTerminalConnector: optional adapter capability (cloud providers only)
- AetherAdapter exposes it using its existing restClient + connect primitives
- terminalConnection: tab-scoped WS carrying create/input/resize/close ->
output/close, reusing resolveTaskWorkspace + connectForTransport(start:true)
- AetherTerminalManager: session lifecycle + scrollback; a per-session lock
guarantees snapshot-then-live-output ordering on (re)attach
- ws.ts routes each per-thread terminal RPC by provider (cached per thread)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(aether): tear down cloud terminal socket on shell exit + archive
Addresses the Aether Review on pingdotgg#15 — two lifecycle leaks:
- The drain now closes the session's connection scope when the VM shell
exits (previously the tab-scoped workspace WS was held open until the
user closed/reopened the terminal). The close is forked onto the
manager scope so the drain — which lives in the session scope — does
not interrupt its own scope-close.
- Archiving a thread now closes BOTH the local and the Aether terminal
managers for it (closing the one with no sessions is a no-op), so an
archived Aether thread's cloud socket is torn down too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(aether): stub AetherTerminalManager.close in server-test mock
Archive now tears down both terminal managers; the mock only stubbed
handles, so close hit Layer.mock's die-on-unstubbed guard (a defect
Effect.catch does not catch), failing the 6 server.test archive cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(aether): cloud terminal lifecycle gaps from deep review
Follow-ups to pingdotgg#15 surfaced by a deep investigation:
- Keepalive (BLOCKER): ping {channel:"activity",type:"user_activity"} every
30s while a terminal is attached. The VM's interactive idle lease is renewed
ONLY by the activity channel — terminal I/O does not count — so without this
the VM suspended ~15 min after connect, mid-session, with the socket still
looking live.
- Shutdown finalizer: close every open session's connection scope on manager
teardown (session scopes are standalone Scope.make; nothing else reaped them).
- thread.delete now tears down terminals too (was archive-only): a deleted
cloud thread otherwise leaked its VM socket and the keepalive held it warm.
- Per-connection ingress queue: a torn-down connection's in-flight frames can
no longer bleed into a later one (e.g. a stale "closed" after restart).
- Connect-error re-emitted on attach: open()->error happens before the listener
attaches, so the snapshot carried status but not the message; attach now
replays the error event so "run a turn first" / missing-key surfaces instead
of a blank errored terminal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(aether): apply initial terminal size on connect
Send a resize with the requested cols/rows right after create so the VM PTY
isn't stuck at its default 80x24 until the UI emits a resize — the first
command's output otherwise wraps incorrectly (e.g. 120x30 project-script
launches). Addresses the Aether Review on pingdotgg#15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ingdotgg#16)
* feat(aether): reconnect the cloud terminal on transient socket drops
The terminal previously died on any WS drop (network blip, VM restart),
forcing a manual reopen. Now the connection runs a forked reconnect loop:
a socket drop transparently re-attaches with backoff (a fresh shell —
the VM reaps the PTY on disconnect — with the client's scrollback kept and
a [reconnecting…] marker), while a shell exit or an exhausted budget ends
it. The first attach still fails loudly. Adds a fake-socket test covering
first-attach sizing, reconnect-on-drop, and stop-on-shell-exit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(aether): reconnect the terminal at the latest size, not the initial
Track cols/rows in the connection closure (updated on every resize) and use
them for the fresh PTY's initial resize, so a resize followed by a socket
drop recreates the shell at the size the user is actually looking at rather
than the stale initial dimensions. Adds a regression test that resizes,
drops the socket, and asserts the second socket is sized to the new value.
Addresses the Aether Review on pingdotgg#16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…st (pingdotgg#17)
* feat(aether): surface cloud terminals in the cross-thread terminal list
The AetherTerminalManager now implements subscribeMetadata — an initial
snapshot of its sessions plus upsert on open/status-change and remove on
close — and the ws subscribeTerminalMetadata handler merges it with the
local manager's stream, folding the Aether snapshot into upserts so cloud
terminals augment (not replace) the local list. Previously cloud terminals
never appeared in the cross-thread terminal list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(aether): release terminal-metadata subscriptions on interrupt
Two interrupt-safety leaks from the Aether Review on pingdotgg#17:
- AetherTerminalManager.subscribeMetadata added the listener before the
initial snapshot; an interrupt mid-snapshot left it registered. Drop it
via onInterrupt.
- The ws subscribeTerminalMetadata handler combined both subscriptions into
one acquire; an interrupt after the local subscription acquired but before
the Aether one left the local listener registered. Split into two nested
acquireReleases so each finalizer registers independently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5366a41b-f7da-48a8-a4de-abdd0d01d15b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added the vouch:unvouched PR author is not yet trusted in the VOUCHED list. label Aug 10, 2026
Comment threadapps/server/src/provider/Layers/aether/portPreview.ts Outdated
Comment threadapps/server/src/provider/Layers/aether/mirrorSync.ts
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts
Comment threadapps/server/src/provider/Layers/aether/mirrorSync.ts
Comment threadapps/server/src/provider/AetherMirrorRegistry.ts Outdated
Comment threadapps/server/src/textGeneration/AetherTextGeneration.ts
Comment threadapps/server/src/provider/Layers/aether/terminalConnection.ts Outdated
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts
Comment threadapps/server/src/provider/Layers/aether/mirrorSync.ts
Comment threadapps/server/src/provider/Layers/aether/workspaceSocket.ts
@github-actionsgithub-actionsBot added the size:XXL 1,000+ changed lines (additions + deletions). label Aug 10, 2026

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the new Aether provider service code against the Effect service conventions. Six concrete issues, all in files added by this PR: a standalone service-shape interface, service instances injected into a production factory, Effect.catchTag instead of Effect.catchTags, and three error-modeling sites that stringify a cause into detail/message or drop the underlying error entirely. Everything else (namespace subpath imports, make/layer exports, no ManagedRuntime/runPromise in domain code) looks consistent with the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/terminal/AetherTerminalManager.ts Outdated
Comment threadapps/server/src/provider/CloudTerminalConnector.ts
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/aether/restClient.ts Outdated
Comment threadapps/server/src/provider/Layers/aether/workspaceSocket.ts Outdated
Comment threadapps/server/src/provider/Layers/aether/restClient.ts
pranav100000and others added 2 commits August 10, 2026 19:18
Resolve conflicts: renumber driver migration 039->041 (upstream took 39/40); keep upstream's parkingCommand session-stop + the driver's dual-manager terminal cleanup in ws.ts; adopt upstream's new-thread draft helpers in useHandleNewThread.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- workspaceSocket: Effect.catchTag -> Effect.catchTags for statically-known tags
- CloudTerminal errors + AetherApiRequestError: preserve structured cause (Schema.Defect)
- AetherAdapter: propagate the structured terminal error across the translation boundary
- AetherTerminalManager: inline the Context.Service shape, referenced as AetherTerminalManager["Service"]
- AetherAdapter: express git + mirrorRegistry as Effect requirements (Context.Service tags) instead of instance options
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment threadapps/server/src/terminal/AetherTerminalManager.ts
Comment threadapps/server/src/provider/Layers/aether/workspaceSocket.ts
Comment threadapps/server/src/terminal/AetherTerminalManager.ts
AetherApiTransportError / AetherApiRequestError put a stable phrase in `detail`
and let the preserved `cause` carry the dynamic error, matching the decode
errors' style (Macroscope Effect Service Conventions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review of the Aether provider changes at this head.

The findings from the previous run (standalone AetherTerminalManagerShape, service instances passed into makeAetherAdapter, catchTag, and the errors that lost their cause) are unchanged at this commit and are not re-posted — later commits on this branch address them.

One new item below, in packages/contracts/src/project.ts.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/project.ts Outdated
Add a ProjectFileFailure "aether_mirror_read_only" literal and derive the
ProjectRead/WriteFileError message from it (AETHER_MIRROR_REFUSAL now lives in
contracts), dropping the message-override field. Failure mode is encoded
structurally, not in a prose string (Macroscope Effect Service Conventions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review of the convention fixes since the last run (4427f33, 8155f4c, 3ae2e17). Three items remain in the changed scope: two wrapper detail fields still derive their message from the stringified/underlying error, and the mirror-refusal refactor dropped the legacy decoded-message passthrough for the project file errors.

Posted via Macroscope — Effect Service Conventions

Comment threadpackages/contracts/src/project.ts Outdated
Comment threadapps/server/src/provider/Layers/aether/terminalConnection.ts Outdated
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts Outdated
…ecode fallback
- CloudTerminalWriteError (terminalConnection) + CloudTerminalTransportError
(AetherAdapter boundary): `detail` is a stable structural phrase; the dynamic
error stays in the preserved `cause`.
- project.ts: restore the decodedProjectErrorMessage fallback so legacy
{_tag, message} payloads still decode their message (mirror case still derives
structurally from the failure literal); restore the covering test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts Outdated
Comment threadapps/server/src/provider/Layers/AetherAdapter.ts
Comment threadapps/server/src/ws.ts
Comment threadapps/mobile/src/state/use-composer-drafts.ts
Comment threadapps/server/src/ws.ts
Comment threadapps/mobile/src/state/use-composer-drafts.ts

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:9bfa5c9ae1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

updateComposerDraftSettings(selectedProjectDraftKey, {
workspaceSelection: {
mode: workspaceMode,
modeUserSet: workspaceModeUserSet,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Queue mobile tasks with the effective workspace mode

When a branch selection stores modeUserSet: false and the user subsequently changes providers—for example, selecting a branch under a local provider and then switching to Aether—the displayed workspaceMode changes to worktree, but buildPendingTaskMessage still prefers the stale workspaceSelection.mode. Fresh evidence is this newly added path that deliberately persists a provisional mode while the queue serializer ignores the marker. The task is consequently queued in local mode, bypassing the Aether isolation default and allowing its one-way mirror to claim the shared checkout; use the effective mode whenever modeUserSet is false.

Useful? React with 👍 / 👎.

* draft written before this field existed: treated as user-set, so an
* existing pick is never surprise-flipped.
*/
readonly modeUserSet?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist modeUserSet in the mobile draft schema

After the mobile app restarts, ComposerDraftWorkspaceSelectionSchema decodes persisted drafts without the newly added modeUserSet field, so Effect's struct decoder drops it and the flow interprets the resulting absence as true. Fresh evidence is that this flag now controls the Aether worktree safety default but was added only to the TypeScript interface, not the persistence schema. An incidental stored local mode can therefore become permanently user-pinned after hydration and prevent Aether from selecting an isolated worktree; add the optional field to the schema.

Useful? React with 👍 / 👎.

Comment on lines +1412 to +1414
if (message._tag === "change" && message.action === "close") {
context.emittedPorts.delete(message.port);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retract preview activities when cloud ports close

When the workspace sends a port-close notification, this branch only removes the in-memory dedupe key; the persisted port.opened activity and its active-looking CTA remain visible on web and mobile, and reopening the same port appends another CTA while the stale one remains. Emit and project a close/removal state (and reconcile snapshots) so users do not keep opening dead preview URLs.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

pranav100000and others added 3 commits August 11, 2026 18:04
Two halves of the same round-4 mistake — the flag was added to the TypeScript
interface but never taught to the persistence schema or the queue serializer,
so the Aether worktree safety default it gates could be defeated two ways.
The decoder strips keys the struct does not declare, so a persisted
`modeUserSet: false` vanished on hydrate — and absence reads as user-set, which
pinned the mode and silently disabled the provider-derived default after every
app restart. It is declared now, with a test that both `false` and `true`
survive a decode.
The queue serializer still preferred the stored mode outright, so selecting a
branch under a local provider (which stores `local` as a carried-along default)
and then switching to Aether queued the task in `local` — bypassing isolation
and letting the one-way mirror claim the shared checkout on drain. It now uses
the effective mode whenever the stored one was never an explicit pick, matching
what the composer displays. Reopening a queued task for editing keeps its mode
pinned: that decision was already made for it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tation
The write guards read ownership and then ran the mutation as two unsynchronized
steps, so an Aether session could register the checkout in between: the guard
answered "not owned", the claim landed, and the local write then reached what
was by then an active one-way mirror — silently corrupting the next
reset-and-apply.
The registry now exposes `whileClaimsFrozen`, and all three guards (VCS
mutation, removeWorktree, and projects.writeFile — the last of which was an
open-coded check at the dispatch site, now a guard like the others) run their
check and their mutation inside it. Frozen regions are concurrent with one
another, so guarded mutations keep their parallelism; only register/deregister
waits, and only for the mutations already in flight — which is exactly the
precondition for claiming a checkout as a mirror. No re-entrancy: nothing
inside a guarded mutation registers, so the lock cannot deadlock against
itself.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Aether binding only appears with a thread's first turn, so a terminal
opened before then routes to the local PTY — and once the binding exists every
later write/resize/close routes to the cloud manager instead. The local PTY was
left running and unreachable, inside the very checkout the mirror is about to
claim, where its shell can dirty the tree the next sync resets.
The first time a thread routes to the cloud manager, its local sessions are now
closed. Routing by the thread's SELECTED provider at open time (so the terminal
never lands locally at all) is the fuller fix and is NOT done here — see the
report.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
// with an Aether model selected defaults to an isolated worktree, the same
// safety default the web composer applies. An explicit pick still wins:
// `workspaceSelection.mode` is read ahead of this default below.
const selectedProviderIsAether =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highthreads/new-task-flow-provider.tsx:428

When the server config is unavailable, selectedProviderIsAether returns false even if the stored model selection targets an Aether driver, so defaultWorkspaceMode resolves to local instead of worktree. This lets a task be queued against the shared checkout before the config arrives, and when the Aether driver later settles its one-way mirror it discards the user's local work. The fallback option synthesized by buildModelOptions sets providerDriver to the provider instance ID rather than the actual driver kind, so the AETHER_DRIVER_KIND comparison fails. Consider tracking the driver kind alongside the stored selection or preventing queuing until the driver can be reliably classified.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/new-task-flow-provider.tsx around line 428:
When the server config is unavailable, `selectedProviderIsAether` returns `false` even if the stored model selection targets an Aether driver, so `defaultWorkspaceMode` resolves to `local` instead of `worktree`. This lets a task be queued against the shared checkout before the config arrives, and when the Aether driver later settles its one-way mirror it discards the user's local work. The fallback option synthesized by `buildModelOptions` sets `providerDriver` to the provider instance ID rather than the actual driver kind, so the `AETHER_DRIVER_KIND` comparison fails. Consider tracking the driver kind alongside the stored selection or preventing queuing until the driver can be reliably classified.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:5747a1e10c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadapps/server/src/ws.ts Outdated
}),
),
Stream.unwrap(
Effect.map(aetherMirrorRegistry.ownsCwd(input.cwd), (owned) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hold the claim lock while running stacked actions

In a multi-client race where one client starts git.runStackedAction while another starts the first Aether turn for the same checkout, this ownsCwd read completes before the callback executes and does not use whileClaimsFrozen; the adapter can therefore register the mirror between this check and a commit/reset performed by the stacked action. The action then mutates an owned one-way mirror, causing the next settlement to detect divergence and pause synchronization, so the claim must remain frozen for the lifetime of the stream action rather than only being checked up front.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Comment threadapps/server/src/ws.ts
Comment on lines +386 to +390
cwd: string,
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E | GitCommandError, R> =>
guardAetherVcsMutation(aetherMirrorRegistry, operation, cwd, effect);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard pull-request checkout mutations

When an Aether thread owns the project's current checkout and another client opens a draft for that project and chooses Local in the pull-request dialog, the gitPreparePullRequestThread handler bypasses this new mutation guard; GitManager.preparePullRequestThread then calls checkoutChangeRequest with force: true on the same cwd. That replaces the branch beneath the active mirror and makes its next settle pause on divergence, so this handler must also run under the cwd mutation guard.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Comment threadapps/server/src/ws.ts
Round 5 converted writeFile / vcs.* / removeWorktree to the frozen region but
missed `git.runStackedAction`, which open-coded its own ownership gate. It
therefore held NO reader permit, so the exclusive registration never waited for
it: `ownsCwd` could answer false, an Aether session could claim the checkout,
and the action's commits, branches and pushes would then land in a live one-way
mirror and corrupt the next reset-and-apply. The registry's own module header
has always listed this among the sites that must refuse.
Its mutation reports progress through a queue rather than its own error
channel, so it gets a guard shaped for that — `guardAetherQueuedMutation` —
instead of another inline check. Extracting it is what makes the guarantee
testable: the regression test drives a registration against an in-flight run
and fails when the frozen region is removed. Every mutating RPC in the header's
list now goes through a guard; no raw ownership check remains at a dispatch
site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review. The previously flagged items are resolved at this head: the cloud-terminal errors and the REST-client wrappers now carry a stable structural detail plus the real cause, AetherTerminalManagerShape is inlined into the Context.Service declaration and referenced as AetherTerminalManager["Service"], the adapter's git/mirror dependencies are Effect requirements, catchTags replaced catchTag, and the mirror read-only refusal is modelled as a ProjectFileFailure literal with the legacy decode fallback restored.

One remaining item in the new driver module, noted inline.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/provider/Drivers/AetherDriver.ts Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:a0ef50966f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2163 to +2165
if (context.taskId === undefined) {
const created = yield* restClient
.createTask({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize first-turn task creation per thread

When two clients submit turns to a fresh Aether thread before the first createTask returns, both calls observe context.taskId === undefined and create separate paid cloud tasks; whichever response lands last overwrites context.taskId, orphaning the other task. Fresh evidence beyond the lost-response retry already reported is that ProviderCommandReactor.ts:1191-1193 forks each sendTurn, allowing the worker to dispatch the next request concurrently, while this adapter has no per-thread creation lock.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Comment on lines +2814 to +2815
rollbackThread: (threadId) =>
Effect.fail(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject Aether checkpoint reverts before restoring files

When a user reverts a settled Aether thread, CheckpointReactor.ts:758-780 restores the local checkpoint before calling this method, which then always fails. The remote conversation and VM tree therefore remain at the newer turn while the checkout has already been rewritten; the next mirror settlement detects that as local divergence and pauses synchronization. Either prevent this provider from entering the generic revert flow before filesystem restoration or implement the remote rollback atomically with it.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

Comment on lines +1854 to +1856
sessions.set(input.threadId, context);
// The fork-side write guard owns this cwd for the thread's lifetime.
yield* mirrorRegistry.register(cwd, registryKey(input.threadId));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate the checkout while acquiring mirror ownership

If another client performs a guarded file/VCS mutation while startSession is resolving the Aether project or rebuilding a resume timeline, the mutation is allowed because this checkout is not registered until here. Registration merely waits for an in-flight guarded mutation to finish; it does not rerun the status/fingerprint checks captured at lines 1652-1704, so the session claims a checkout whose baseline is already stale and its first settlement pauses on divergence. Fresh evidence beyond the earlier mutation-lock report is the long preflight-to-registration gap: ownership acquisition and final validation still are not one atomic operation.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

The claim lock was one process-global semaphore, which was tolerable while the
guards only wrapped local filesystem work — but `git.runStackedAction` holds a
reader across its network PUSH, so a 30-second (or hung) push on one checkout
blocked `register`/`deregister` for EVERY checkout: an Aether session starting
on an unrelated project waited on it, and a hung push would stall every session
start and stop in the process.
Correctness only ever needed same-checkout serialization. Each guard now
declares the paths it writes and holds a reader on those paths AND their
ancestors, while a registration takes its own canonical path exclusively. A
claim therefore conflicts with exactly the mutations writing at or under it,
and unrelated checkouts share no key. Acquisition is sorted and de-duplicated
so two overlapping mutations cannot deadlock each other.
`projects.writeFile` declares the resolved FILE rather than its cwd, which is
what keeps the descend-into-a-mirror case serialized (cwd=/repo,
relativePath=.worktrees/mirror/app.ts); `removeWorktree` declares both its cwd
and the worktree it deletes. A mutation in an ANCESTOR of a claim deliberately
does not conflict: git operations in a parent repository do not write its
linked worktrees, and the two guards that can reach a descendant name it.
Both directions are pinned by tests: an unrelated checkout's registration does
not wait on an in-flight push, and a registration over a path a write descends
into still does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
// and unrelated checkouts share no key. `makeUnsafe` so handing a lock out
// cannot yield between the map's get and set.
const pathLocks = new Map<string, Semaphore.Semaphore>();
const lockFor = (path: string): Semaphore.Semaphore => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highprovider/AetherMirrorRegistry.ts:159

lockFor inserts every canonical path it receives into the pathLocks map and never removes any entry. Every call to whileClaimsFrozen (and thus every guarded write) creates permanent Semaphore objects for the written path and all of its ancestors, so a server that processes many distinct file paths accumulates locks in pathLocks indefinitely — an unbounded memory leak that persists for the lifetime of the process. Consider evicting entries from pathLocks when the last permit is released, or use a weak/ephemeral lock store so unused semaphores can be garbage-collected.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/AetherMirrorRegistry.ts around line 159:
`lockFor` inserts every canonical path it receives into the `pathLocks` map and never removes any entry. Every call to `whileClaimsFrozen` (and thus every guarded write) creates permanent `Semaphore` objects for the written path and all of its ancestors, so a server that processes many distinct file paths accumulates locks in `pathLocks` indefinitely — an unbounded memory leak that persists for the lifetime of the process. Consider evicting entries from `pathLocks` when the last permit is released, or use a weak/ephemeral lock store so unused semaphores can be garbage-collected.

Comment threadapps/server/src/provider/AetherMirrorGuards.ts
Comment threadapps/server/src/provider/AetherMirrorGuards.ts Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:713f2cd647

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

): Effect.Effect<A, E | GitCommandError, R> =>
registry.whileClaimsFrozen(
// Both: the repository the command runs in AND the worktree it deletes.
[input.cwd, input.path],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve removal targets before freezing claims

When vcs.removeWorktree receives a relative path, this freezes input.path relative to the server process directory, while ownsTargetPath resolves it relative to input.cwd; GitVcsDriverCore.removeWorktree then forwards that same <worktree> operand to git worktree remove under input.cwd (matching the git worktree remove [-f] <worktree> help). The actual target's claim lock is therefore not held, so another client can register that checkout after ownsTarget returns false and before Git deletes it. Freeze the cwd-relative resolved target, including any basename-matched claimed path.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

const key = sessionKey(input.threadId, input.terminalId);
const session = sessions.get(key);
if (session) {
yield* teardownConnection(session);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize cloud-terminal restarts by session key

When two clients restart the same cloud terminal concurrently, both enter this unsynchronized existing-session path and call teardownConnection/establishConnection; each establish creates a standalone scope and assigns session.scope and session.connection, so the later assignment can orphan the first live socket or let one drain close the other restart's scope. Route restart (and competing open/close operations) through the same per-session lock used by open so only one connection lifecycle can replace the session at a time.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Scoping the claim lock to the paths a mutation writes was right for the
path-based checks, but it made the freeze narrower than the state ONE check
consults. `ownsTargetPath` also refuses on a bare BASENAME match against every
active claim, because `git worktree remove <name>` resolves a bare component to
a worktree whose real location the request never spells out. So
`removeWorktree{cwd:/repos/projA, path:"feature-x"}` locked only those two
paths, while a session registering a mirror at <anywhere>/feature-x took a
different key entirely, landed between the check and the exec, and git deleted
the live mirror it had just claimed.
A sentinel now guards claim CHANGES independently of location: registrations
take it as readers (so they never block each other), and removeWorktree — the
only guard whose answer is location-independent — takes it exclusively for its
critical section. The path-scoped guards keep their readers and never touch it,
which is what preserves the liveness fix: a stacked action's push still cannot
block a registration, and a removal waits only on registrations, which are
short. The sentinel is always acquired OUTSIDE any path lock, so the two lock
families cannot form a cycle.
Both new properties are pinned by tests, and both directions verified: making
removeWorktree path-scoped again reopens the basename race, and putting a slow
mutation under the sentinel reintroduces the process-wide stall.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
}
};

export const make = Effect.sync(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highprovider/AetherMirrorRegistry.ts:170

register canonicalizes cwd twice: once in withClaimsExclusive to acquire the path lock and again inside the generator to key the claim map. If a symlink in cwd is retargeted between those two async realpath calls, the registration holds the lock for the old canonical path but publishes the claim under the new canonical path. A concurrent whileClaimsFrozen guard on the new path acquires a different lock, does not conflict with this one, and can pass its ownership check while the claim is still being installed — defeating the check-then-mutate freeze.

deregister recomputes the canonical key from current filesystem state rather than reusing the key that register stored. If a session was registered through a symlink that is later removed or retargeted, canonicalize(cwd) returns a different path, so claims.get(normalized) misses the original entry. The checkout stays falsely claimed and all guarded writes and VCS operations against it are refused until the server restarts.

Both defects stem from canonicalizing the same cwd multiple times. Consider canonicalizing once per call and threading the result (or the original key string) through register/deregister so the lock and the claim map always agree.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/AetherMirrorRegistry.ts around line 170:
`register` canonicalizes `cwd` twice: once in `withClaimsExclusive` to acquire the path lock and again inside the generator to key the claim map. If a symlink in `cwd` is retargeted between those two async `realpath` calls, the registration holds the lock for the old canonical path but publishes the claim under the new canonical path. A concurrent `whileClaimsFrozen` guard on the new path acquires a different lock, does not conflict with this one, and can pass its ownership check while the claim is still being installed — defeating the check-then-mutate freeze.
`deregister` recomputes the canonical key from current filesystem state rather than reusing the key that `register` stored. If a session was registered through a symlink that is later removed or retargeted, `canonicalize(cwd)` returns a different path, so `claims.get(normalized)` misses the original entry. The checkout stays falsely claimed and all guarded writes and VCS operations against it are refused until the server restarts.
Both defects stem from canonicalizing the same `cwd` multiple times. Consider canonicalizing once per call and threading the result (or the original key string) through `register`/`deregister` so the lock and the claim map always agree.

return await NodeFSP.realpath(candidate);
} catch (cause) {
const code = (cause as NodeJS.ErrnoException).code;
if (code === "ENOENT" || code === "ENOTDIR") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highprovider/AetherMirrorRegistry.ts:42

whileClaimsFrozen calls canonicalize on each write path, and canonicalize's realpathIfExists rethrows any errno other than ENOENT/ENOTDIR. A path with a symlink loop (ELOOP) or a permission error (EACCES) therefore rejects the Promise inside Effect.promise. Per Effect's contract for promise, an unexpected rejection is a defect, so the RPC dies with an untyped defect instead of letting the underlying filesystem/VCS operation surface its normal typed error. Consider catching additional transient/path-related errnos (e.g. ELOOP, EACCES) in realpathIfExists and treating them as a failure to canonicalize rather than rethrowing.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/AetherMirrorRegistry.ts around line 42:
`whileClaimsFrozen` calls `canonicalize` on each write path, and `canonicalize`'s `realpathIfExists` rethrows any `errno` other than `ENOENT`/`ENOTDIR`. A path with a symlink loop (`ELOOP`) or a permission error (`EACCES`) therefore rejects the `Promise` inside `Effect.promise`. Per Effect's contract for `promise`, an unexpected rejection is a defect, so the RPC dies with an untyped defect instead of letting the underlying filesystem/VCS operation surface its normal typed error. Consider catching additional transient/path-related errnos (e.g. `ELOOP`, `EACCES`) in `realpathIfExists` and treating them as a failure to canonicalize rather than rethrowing.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:64ff102ade

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2311 to +2313
const current = yield* restClient
.getTask(taskId)
.pipe(Effect.mapError(toRestRequestError("sendTurn")));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize follow-ups across the model-switch await

When one client submits a model-changing follow-up while another client sends to the same idle thread, both reactor fibers can pass this pendingSend check before the model-changing call resumes from getTask/updateTask. They then dispatch different payloads with the same ordinal and client_message_id; Aether deduplicates one away, while both callers may advance sentCount and record the same returned turn inconsistently. Serialize sendTurn per thread or atomically recheck and claim the ordinal immediately before every /respond.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Comment on lines +245 to +246
case "added":
parts.push("new file mode 100644");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve executable bits for added files

When the cloud agent adds an executable script or binary, this unconditionally reconstructs it as mode 100644. The settled local mirror and checkpoint therefore differ from the VM tree, and invoking the mirrored file directly fails with permission denied; the existing modeOnlySkipped warning does not cover added files. Carry the file mode through the workspace diff, or at least pause/warn instead of silently synthesizing a non-executable mode.

Useful? React with 👍 / 👎.

claimSentinel.withPermits(1)(
canonicalize(cwd).pipe(
Effect.flatMap((path) => lockFor(path).withPermits(CLAIM_LOCK_PERMITS)(effect)),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sentinel held during path wait

Medium Severity

withClaimsExclusive takes a claimSentinel reader permit before waiting on the path exclusive lock. A register blocked behind a slow path-scoped mutation (push/pull) therefore keeps a sentinel permit, so whileAllClaimsFrozen cannot gather all permits and every vcs.removeWorktree stalls until that unrelated I/O finishes. The adjacent comment claims a path-blocked registration does not hold what the location-independent guard needs, but the acquisition order does exactly that.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 64ff102. Configure here.

… (review)
Macroscope's Effect service conventions flagged the one remaining wrapper
whose caller-visible message came from its cause: ProviderDriverError built
detail from cause.message. The cause is already preserved on the error, so
detail is now a stable phrase and the dynamic text stays in cause, matching
the other driver wrappers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 511c5d9. Configure here.

instanceId,
// Stable structural phrase; the dynamic cause is preserved below, not folded
// into the caller-visible message (Effect service conventions).
detail: "Failed to build the Aether provider snapshot.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create failure loses root cause

Medium Severity

ProviderDriverError.detail is now a fixed phrase while the real failure sits only on cause. The instance registry logs and builds the unavailable shadow from failure.detail alone and never reads cause, so Aether create failures surface as a generic reason with no actionable root cause. Sibling drivers still embed cause.message in detail.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 511c5d9. Configure here.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:511c5d9324

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (rest.length === 0 || rest.startsWith("/")) {
throw new Error(`API base URL has no host: ${apiBaseUrl}`);
}
return `${wsScheme}${rest}${websocketPath}?token=${encodeURIComponent(apiKey)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve absolute WebSocket paths against the API origin

When a custom API base URL contains a path, this concatenates that path with a websocket_path that the function itself treats as absolute. For example, https://host/aether plus /workspaces/ws-1/ws becomes wss://host/aether/workspaces/ws-1/ws rather than wss://host/workspaces/ws-1/ws; a server returning an already-prefixed path similarly gets the prefix duplicated. The REST probe can therefore succeed through a path-based reverse proxy while agent streaming and cloud terminals continually fail to connect, so resolve websocket_path against the API URL's origin instead of concatenating it with the full base.

Useful? React with 👍 / 👎.

AetherSocketOpenError
> =>
Effect.gen(function* () {
const socket = factory(url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Convert WebSocket constructor throws into typed open failures

If the runtime WebSocket constructor or an injected factory throws synchronously—for example because a custom endpoint produces a URL the constructor rejects—this call becomes an Effect defect rather than AetherSocketOpenError. The agent reconnect handler therefore cannot retry it, and, more seriously, the cloud-terminal lifecycle only catches typed failures before awaiting its ready deferred, so the terminal-open RPC can remain pending indefinitely. Wrap factory construction in Effect.try and map the exception to the existing typed open error.

Useful? React with 👍 / 👎.

pranav100000and others added 2 commits August 12, 2026 00:11
The upstream merge was textually clean but semantically conflicting here:
upstream replaced `PendingUserInputDraftAnswer.selectedOptionLabel` with a
`selectedOptionLabels` LIST, while the driver's own test still built the
singular form — so the tree merged and did not compile.
The call sites now pass a single-entry list. A single-select question resolves
to the first entry, so the assertions are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:872dc5f1ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1307 to +1309
context.reconcile = Effect.gen(function* () {
const afterSequence = mapper.latestSequence();
const delta = yield* restClient.getConversationDelta(taskId, afterSequence);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize durable reconciliation per session

When the settle poll and a socket callback invoke context.reconcile concurrently, both requests can start from the same cursor and complete out of order. Although old rows are skipped by sequence, reconcileDelta still applies the older response's task status; for example, a delayed awaiting_input response arriving after a newer processing response can settle the newly active turn and run mirror/checkpoint settlement while that turn is still executing. Protect the complete fetch-and-apply exchange with a per-session single-flight lock.

Useful? React with 👍 / 👎.

Comment on lines +225 to +228
const normalized = yield* canonicalize(cwd);
const keys = claims.get(normalized) ?? new Set<string>();
keys.add(key);
claims.set(normalized, keys);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make mirror ownership exclusive per checkout

When two Aether threads select the same Current checkout, this adds both keys to one claim instead of rejecting the second session. The registry blocks local RPC mutations but does not serialize the two mirror engines, so each task can independently reset --hard and apply its own cloud diff to the same tree; one session can overwrite the other's result or permanently pause it as divergent. Permit repeat registration only for the same owner and reject a distinct owner for an already-claimed checkout.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

// on the branch it was FORKED from, which createWorktree recorded in
// `branch.<head>.gh-merge-base`. Passing the scratch branch is exactly what
// fails cloud startup with remote_ref_missing (404).
let baseBranch = status.branch ?? undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive the cloud base from the validated upstream

When a local branch tracks a differently named branch or a remote other than origin, the preflight still passes because it checks only that some upstream exists and is in sync, but this sends the local branch name to the Aether project matched from remote.origin.url. That ref need not exist in the linked repository, so the first task fails to start with a missing remote ref despite passing preflight; validate an origin/<branch> upstream or derive a base branch that is known to exist in the matched origin.

Useful? React with 👍 / 👎.

The driver merge carried a line over the width limit; Aether-Runtime CI's
format check (vp check) flags it though upstream's Macroscope does not. Pure
formatting, no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:82149c740e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

new PauseSync(`Mirror re-baseline failed (${args.join(" ")}): ${error.message}`),
),
);
yield* mutate("aether.mirror.reset", ["reset", "--hard", resolvedBase]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the last good mirror tree when synchronization fails

When rebuilding or applying the cumulative diff fails after this reset—for example, git apply rejects a hunk or a binary transfer/write fails—the previous successfully mirrored tree has already been discarded, but the PauseSync path performs no rollback and makes the pause sticky. The adapter subsequently emits turn.completed, allowing CheckpointReactor to capture this base-only or partially applied checkout as the turn result; users therefore lose the last good mirrored state locally and receive a corrupt checkpoint. Stage the replacement transactionally or restore the pre-sync tree before returning a paused/skipped outcome.

AGENTS.md reference: AGENTS.md:L122-L124

Useful? React with 👍 / 👎.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Adds a separate Aether cloud and microVM hosting platform across 94 files.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranav100000@t3dotgg