Skip to content

feat(rc): #229 release-gate prerequisites — real producers, host pump, truthful status - #292

Merged
AetherAI3 merged 12 commits into
mainfrom
feat/rc-release-gate-229
Oct 6, 2026
Merged

AetherAI3 merged 12 commits into
mainfrom
feat/rc-release-gate-229

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

Lands every Agent-side prerequisite of the #229 live CLI-to-phone gate in one integration branch, one commit per issue (merge with rebase to keep them). Each lane was implemented test-first, adversarially reviewed, then replayed onto current main (incl. #289) with conflicts resolved semantically.

Commit Issue What
test(rc): pin the viewer profile… #228 Agent profile pinned byte-for-byte to Aether Code's observer manifest (Cloud side: AETHER-CLOUD#1932, deployed)
publish measured checkout diff summaries #218 counts always present (binary = 0 lines, untracked counted, unknown → no summary); fixes Cloud-400 outbox wedge. Supersedes #240
publish test verification outcomes #219 real verifier readings; no invented counts; executor-killed → unknown (follows #289's marker rule)
publish CI and PR status from Action Rail receipts #220 issued vs reconciled from typed receipt fields only
refuse home-relative tool targets — pre-existing wedge: ~-prefixed tool targets → Cloud 400
publish generated artifact metadata #221 stable artifact_id, prompt/model/path/signed-URL never leave
publish preview lifecycle #222 phase transitions; URL only via strict public projection (Cloud "no public link" state in #1932)
make rc start and revoke fail closed #227 live only after receipted first append; rollback + durable revoke tombstone
keep host heartbeat and outbox delivery alive #223 5 s heartbeat pump, bounded batches, jittered backoff, unref'd timers, bounded final flush
report authoritative exposure, viewers, health #226 rc status/exposure/viewers read Cloud GET …/status (AETHER-CLOUD#1931, deployed); unknown, never active, when Cloud can't answer
reconcile producers with the host pump — one delivery path (rc/publish.ts), outbox file = single truth (cross-writer races fixed), coverage 13/13

Closes #218, closes #219, closes #220, closes #221, closes #222, closes #223, closes #226, closes #227. Refs #228, #229 (closed after the live journey).

Test plan

Known follow-ups (not regressions): checkoutDiffSummary starts with synchronous git calls at run end; cross-process load→rename window has no file lock; concurrent rc start can orphan a Cloud session without an idempotency key.

🤖 Generated with Claude Code

@AetherAI3 AetherAI3 closed this Oct 6, 2026
@AetherAI3 AetherAI3 reopened this Oct 6, 2026
AetherAI3 added a commit that referenced this pull request Oct 6, 2026
…scan

Self-hosted Linux CI on #292 failed four tests that pass on Windows.

rc_action_receipts (3 tests): test defect, no leak. On Linux the temp
project root was found ONLY at the outbox record's top-level
`project_root` -- the field the harness itself writes via createOutbox,
which loadOutbox needs to re-relativize paths after a restart and
flushOutbox never sends (the append body is device_id + events only).
No queued event payload and no append body carried it (probed on VPS6:
wire hit = false, queue empty, disk hit = $.project_root only). The
check scanned the raw file text, so it failed on Linux for local
bookkeeping and was vacuous on Windows, where JSON escapes the root's
backslashes and a raw-path search can never match. The leak check now
scans every append body whole (not just event_type/payload), plus the
outbox record minus `project_root`, for the root both raw and
JSON-escaped; asserts `project_root` is exactly the local root; and
asserts no append body names `project_root`. Strictly stronger on both
platforms.

rc_preview "a cancelled start publishes stopped only once the
supervisor's state is gone": product bug, Unix only. slow.mjs never
exits on its own (it times out normally, empty log). The "exit 1" was
the supervisor's own cancel: SIGTERM reaches the detached supervisor,
which SIGTERMs the child's group; the signal death closes with no code
and `closed = code ?? 1` records it as 1. Two defects followed:
- preview_supervisor: a stop before readiness (signal or POST /stop)
  fell into the not-ready branch, wrote phase "failed: dev command
  exited before readiness (exit 1)" and never removed its state. So
  `preview stop` while starting reported "cleanup was not confirmed"
  (exit 25, on every platform) and the next status/start republished
  a failure. It now waits for the child and runs the same
  identity-checked cleanup as a stop after readiness, exiting 0.
- preview start: the wait loop checked `cancelled` only at its top, so
  a cancel landing during sleep() or the /status request let the next
  read report the supervisor's reaction to that cancel -- exit 23 and
  RC phase "failed" instead of 130 and "stopping". It now re-checks
  after each await.
Windows never reached either: taskkill /F kills the supervisor before
it can write anything. Verified on VPS6 at SIGTERM +0/50/250/400ms:
exit 130, state removed, then "stopped". New preview.test covers the
POST /stop path (RED before the fix on Linux: 25 !== 0).

Gates: Linux (VPS6, Node 24.21) rc_* 380/380, preview 15/15;
Windows (int229) build, rc_* 380/380, preview 15/15,
generate-docs --check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
src/core/rc/viewer_profile.ts has long cited test/rc_viewer_profile.test.ts as
the pin against the Cloud viewer manifest, but the file never existed, so the
Agent's observe-only profile and Aether Code's /rc routes could drift apart
silently.

- test/fixtures/rc-viewer-profile-v1.json is a byte-identical copy of Aether
  Code's site/components/remote-session/rc-viewer-profile-v1.json
  (sha256 57731a2b...ab7a1, LF-pinned in .gitattributes).
- test/rc_viewer_profile.test.ts pins the raw bytes, asserts viewerManifest()
  and EXCLUDED_EVENT_TYPES equal the manifest, re-checks every viewer route and
  API path token by token against FORBIDDEN_VIEWER_TERMS, allows only one
  non-GET call (redeeming the observe grant), and proves the link `aether rc`
  prints lands on a manifest viewer route while operator surfaces stay gated
  and outside /rc.
- docs/REMOTE_VIEWING.md documents the observer-only viewer and the separate
  operator route, and adds the missing `aether rc viewers` row.

Refs #228

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AetherAI3 and others added 11 commits October 6, 2026 11:51
Publish a display/1 diff_summary to the active RC session when a real
checkout diff is established: in `rc start`'s opening batch, and after a
local coding run settles. Paths come from `git status`; counts come only
from git numstat and bounded direct reads, never from prose or model
output. Publication is best-effort: every failure is swallowed and never
changes the local result.

Final behaviour:
- All three counts (files_changed, insertions, deletions) are always
  present, as Cloud's display/1 contract requires. A clean tree is 0/0/0.
- A binary file is a changed file with 0 lines; git has no line count
  for binary ("-" in numstat).
- Untracked text files are counted as git would count them once added,
  with bounded direct reads (8 MiB per file, 32 MiB and 1000 files per
  snapshot) that never follow a link out of the checkout and that honour
  .gitattributes (-diff/binary, diff, filter, working-tree-encoding)
  through `git check-attr`, chunked to fit the Windows command line.
- Any unknown count means no summary rather than a misstated one: a
  failed git read, a changed tracked path numstat did not measure, or an
  untracked file that is too large, external, unreadable or
  filter-converted, or bounds exceeded.
- Unsafe roots and paths are refused: a project root that is not the
  checkout toplevel, and any absolute, traversal, drive-qualified or
  leading-"~" path (Cloud reads a leading "~" as a home path).
- `files` is a prefix of the sorted paths, bounded to 64 entries and to
  16 KiB of JSON as the broker measures it; files_changed counts every
  path.
- The outbox sanitizer refuses a diff_summary without valid
  non-negative integer counts or with an unsafe file path, and sizes
  every payload the way the broker does (ensure_ascii JSON, not UTF-8),
  so no payload Cloud would answer 400 reaches durable storage and
  wedges the batch behind it.

Supersedes #240.
Closes #218.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host's own verifier readings now reach an active RC session's Tests
panel (#219): the final gate of `aether agent` (through the run's own
coding observer), `aether review verify`, and stored readings shown by
`aether review` (the source of "stale").

- testsEvent publishes the verifier's status plus a closed-vocabulary
  reason and, for a failure, the exit code; no command, output, local
  reason or parsed counts.
- Killed runs (130/124 or an aborted signal), a tree that moved during
  the run, and a tree that cannot be fully identified are "unknown" and
  are never recorded or read back as a pass.
- A frame field the outbox sanitizer would rewrite is dropped; without
  its status the reading is not sent, so no 400 can wedge the outbox.
- The coding observer reloads the outbox before enqueueing when no
  upload is in flight, so a standalone review's queued reading is not
  saved over.
- RC failures never change verdicts or exit codes; nothing is awaited.

Integrated onto #289 (874b36b): whether a check completed is the
CheckReading the agent footer renders (verify_gate readCheck), so a kill is
read from the executor's own markers or an aborted signal, never an exit
code alone; verifyAndRecord maps cancelled/timed_out onto these causes. In
`aether agent` the gate's reading is the one verifyAndRecord records for the
review rail (recordingRunner hands it over), published by
verifyCodeTurnInCheckout; without a recorded run the CheckReading alone is
published (no command, not run, could not start, killed, or unattributed).
A check that cannot start reads "the check could not start" (new
launch_failed cause). publishDiff (#218) shares the observer's single
reload-before-enqueue path.

The review-rail kill test follows #289's executor-marker rule: a command that exits 124/130 by itself is a completed, recorded failure; only an executor kill ([aborted]/[timeout after Ns]) is unknown and unrecorded.

Integrated with #299: recordingRunner keeps #299's onResult callback, which
receives the whole VerifyRunResult. verifyCodeTurnInCheckout captures the
recorded {reading, written} once, publishes the reading to RC as before, and
returns it as recordedCheck; cmdCode hands that receipt to the session log
and to the goal-phase runObserver exactly as #299 did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When `aether github pr create|update` or `ci rerun` executes an approved
Action Rail plan and the Cloud returns its receipt, queue that receipt's
CI or PR identity for the active RC session of this checkout through the
existing adapters (ciEvent, prStatusEvent) and the existing outbox seam:
sanitize at durable enqueue, and move the cursor only on proven receipts.

- Only a receipt that binds to the plan just executed is published:
  plan_id, action_digest and action_type must match, the action must be a
  rail mutation, and `reconciled` must be a real boolean. Issued versus
  reconciled is the one status a receipt states, so it is never guessed.
- ci carries {provider: "github", status: issued|reconciled, run_id} with
  a numeric run id only. pr_status carries {repo, number,
  state: issued|reconciled, url}; the link is built from the validated
  owner/name and the numeric number, never copied from html_url. No
  title, checks, deployment or merge state is inferred.
- Provider payloads, html_url, refs, workflow paths, the provider's own
  status, actor, token fingerprint, PR body and logs never reach the
  outbox. A workflow dispatch receipt has no run identity and publishes
  nothing.
- A field the sanitizer would rewrite is dropped rather than published
  altered, and a PR link survives only while it still matches repo and
  number, so no payload the broker would answer 400 can wedge the outbox.
- Fire-and-forget: the projection is durable before the command returns,
  the upload is started and never awaited, and every RC failure is
  swallowed. The command's output and exit code are unchanged. Without an
  active, unrevoked session for this checkout nothing is written.

Closes #220.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A tool_activity whose target is "~" or begins with "~" (list_dir("~"), an
Office lock file such as "~$Report.docx", "~user/x") kept target:"~…" through
the outbox sanitizer. Cloud's display/1 identifier rule refuses ANY leading
"~" ("absolute local paths are forbidden"), and RC_EVENT_REJECTED keeps the
rejected batch at the head of the outbox, so a single such event wedged
delivery for the rest of the session.

sanitizePathIdentifier now maps a leading "~" to "[external-path]", the same
identifier every other external path gets, before durable enqueue. A "~"
inside a project-relative name (src/a~b.ts) is unaffected. The regression
test drives the real producer (mapBrainEventToRc) into the real outbox, and
the reviewer's repro now passes Cloud's own validator for all three shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a media-history entry is durably committed, the active RC session for
the launch project receives one `artifact` display/1 event: a session-scoped
artifact id, the kind, a safe display label and (only when measured) the size.

- Commit seam: appendEntry takes an optional onCommitted observer that runs
  after the verified commit and outside the file lock; a failed append is
  never observed and an observer throw cannot undo the commit. recordOutput
  threads it, and every production call site (aether image/video and the
  /photogen, /re-frame, /videogen, /animate, /re-cut and storyboard render
  paths) passes rcArtifactObserver(ctx); a source guard pins that.
- Identity: artifact_id = artifact-<24 hex of sha256(session, entry id)>, so a
  resend or replay updates one viewer card and the local history id stays
  local.
- Privacy: path, URL, prompt, model and metadata never leave. The title is the
  file's bounded leaf name, cut on a code-point boundary, and falls back to a
  generic "Image #N" when it could carry what is dropped on purpose: the
  model, ANY meaningful prompt word (reordered, article-less or concatenated
  slugs included), or a capability-shaped object key (UUID, long random run,
  hex digest) taken from a media URL.
- No outbox wedge: title is never empty, unmeasured sizes send no summary, and
  a payload whose required keys would not survive sanitization is never
  enqueued.
- Delivery is fire-and-forget through the existing loadOutbox -> enqueueEvent
  -> saveOutbox -> flushOutbox seam: durable before any network call, never
  awaited by the media command, no timers. Overlapping commits in one process
  share one in-memory record and delivery loop; an idle publisher always
  starts from the outbox on disk, so it never erases another writer's queue.

Tests (test/rc_artifacts.test.ts, 19): commit seam, projection and leak
canaries, prompt-slug and capability-key labels, surrogate-safe truncation,
offline/lost-response replay deduped to one artifact, in-flight overlap,
coexistence with the coding observer while the broker is offline, and
/photogen end to end, all against a broker stub that enforces the Cloud
display/1 contract.

Closes #221.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`aether preview` (and /preview in the REPL) now reports the phases it
observes or causes to the active RC session for the project: starting, ready
(only once the control channel verifies it), failed, stopping and stopped
(only once the supervisor's state file is gone). Payloads are
{projection_version, phase, instance_id[, url]}; instance_id is
preview-<24 hex of sha256(session, supervisor instance id)>, stable across one
launch. Loopback URLs, ports, PIDs, argv and child error text never leave.

- Viewer link: only for the declared preview while it is ready, and only from
  an operator-declared `publicUrl` in .aether/preview.json that passes
  previewDisplayUrl: HTTPS origin + path; no userinfo, query or fragment; no
  IP literal; no single-label, private, tailnet (.ts.net), reserved
  (.test/.invalid), onion or loopback-resolving host (localtest.me, nip.io
  style, names spelling an IPv4 address); no capability-shaped segment (long
  random runs, UUIDs, hex digests); nothing the inline scrubber would rewrite.
- A refused publicUrl stops `preview start` only while an RC session is
  active; without one it is ignored with a warning that never echoes it.
- `preview start` publishes a stale supervisor failure (e.g. exit after ready)
  before replacing its state, so the viewer stops showing it as ready.
- Cancel and the concurrent-stop timeout publish `stopping`, then `stopped`
  from a bounded, unref'd poll once cleanup is proven; a forced kill that
  leaves the state file keeps the viewer at `stopping`.
- previewOptionsFromFlags passes `args` only when --arg is given; the empty
  list used to override the declaration, launching the bare executable on the
  real CLI path so a declared publicUrl could never match.
- Publisher: durable enqueue through the existing outbox seam, upload never
  awaited, no timers held. Between uploads it re-reads the outbox before every
  enqueue, so it never saves a stale copy over events another writer (the
  coding observer, orchestra or media publishers) queued meanwhile.

Tests (test/rc_preview.test.ts, 18) run the real supervisor and the real CLI
registry flag path against a broker stub enforcing the Cloud display/1
contract: loopback, declared public, malformed declaration (with and without
an RC session), malformed state, failed launch while the broker is down,
stale failure via status and via start, cancel, two writers sharing one
outbox while offline, and the URL projection.

Refs #222 — closes together with the AETHER-CLOUD PreviewPanel no-link state.

Integrated after #221: both lanes added a private capabilityShaped() to
producers.ts with different rules (artifact filename stems vs URL path
parts); kept both, renamed stemCapabilityShaped / urlPartCapabilityShaped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`rc start` now reports a session live only after a valid register, a valid
attach, durable local state and a receipted first append. An outage on the
first append leaves the session pending (opening events durable, delivered
later) and exits non-zero; a terminal answer, an attach failure or any local
write failure rolls the Cloud session back, and a revoke the Cloud cannot
confirm leaves a durable pending-revoke tombstone that the next rc command
retries.

loadOutbox no longer turns an unreadable, unparseable or incompatible file
into a silent blank: it returns an explicit recovery condition that
saveOutbox refuses to write over and `rc start` refuses to start past.
`rc off` revokes the session the damaged bytes still name and sets the file
aside, never claiming a revocation it did not get.

`rc off` with nothing running no longer writes a tombstone naming no session
(which blocked every later start and made the next rc command claim a Cloud
revocation that never happened). When the local revoke marker cannot be
written, the Cloud revoke is still attempted, because the active record left
on disk means nothing local keeps publication off; the result names which half
happened instead of promising RC "will not resume automatically". A second
start over a pending session says it is pending, not running.

A read or rename that fails on a transient Windows lock (EBUSY, EPERM,
EACCES, EAGAIN) is retried briefly, at most 60 ms, before anything is
concluded. State that still cannot be READ is an I/O condition that nothing
acts on (no start over it, no `rc off` setting it aside); only unparseable or
incompatible bytes take the recovery path. Only the Cloud's own 404 "session
not found", or a 409, confirms a revoke; any other 404 keeps the tombstone.
`rc link`, and a failed observer-link mint, on a session the Cloud refuses
say so and name `aether rc off` instead of reporting "pending" or "running".

Also classifies real ApiClient HttpErrors by FastAPI's body.detail, which
previously turned every 409 into RC_SESSION_TERMINAL.

Integrated after #218/#219: the measured checkout diff_summary (#218) rides
in the opening batch of the new fail-closed start, best-effort as before; the
coding observer keeps #219's shared publish path and gates on isPublishable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A coding run that finds an attached RC session now binds one host lifetime
to itself (#223). The new host pump (src/core/rc/pump.ts) heartbeats the
session every 5 s (the Cloud's HEARTBEAT_INTERVAL_S), drains the durable
outbox in at most 32 bounded batches per tick whenever events are queued,
backs off on retryDelayMs's capped jittered curve (1 s to 60 s) on outages,
rate limits and unproven receipts, and stops for good on terminal,
revoked/expired/closed or ownership answers and on a local revoke. Every
timer is unref'd, nothing throws into the run, and the run's `finally` makes
one final flush bounded by a 1.5 s deadline before aborting whatever is still
in flight. Events left queued stay durable, and the next run resumes
delivering them once a heartbeat proves the session is still this host's.

The outbox file is now the single truth between its writers. The coding
observer read-modify-writes the file instead of saving a long-lived copy, the
pump adopts the file on every tick, and flushOutbox reloads it before sending
and again before committing receipts (receipts are validated against the
cursor at send time; the cursor never moves backwards). Previously the
observer, /orchestra and a receipt landing mid-batch could each save over
events another writer had queued, losing them for good while the broker was
offline.

Heartbeat and delivery run on independent clocks. Delivery backing off (a
refusing or rate-limited events route) never silences the 5 s heartbeat, a
long drain heartbeats between batches, and delivery only runs on a session a
heartbeat proved live. A local read or receipt write that fails on a lock
backs off instead of stopping the host; damaged bytes still stop it. The
close deadline timer is ref'd so a run cannot exit mid-await.

Tests run on a virtual clock: a 90 s outage with backoff and no lost events,
reconnect and duplicate-safe replay (host_event_id dedupe, cursor advancing
only on full receipts), termination on revoked/expired/not-found, a bounded
final flush against a hung broker, resume on the next run, and both
multi-writer races with the broker offline and with a receipt in flight.

Further regression tests: heartbeat cadence under delivery backoff, an
unreadable-then-restored and then damaged outbox, an unwritable receipt that
is replayed, and two writers flushing overlapping batches without wedging the
cursor.

Integrated after #218/#219: the observer's publish() (the verify gate's
tests reading) and publishDiff() use the same read-modify-write-the-file path
as feed() and wake the pump, replacing #219's reload-unless-flushing copy.
The run-end diff measurement is no longer awaited by the run; close() waits
for it only inside its one 1.5 s deadline, then makes the final flush
(tests: close delivers a diff that finishes in time; a measurement that never
finishes cannot hold close past the deadline). `aether review` publishes
through a one-shot seam (rc/publish.ts: load, isPublishable, enqueue, save,
flushOutbox) instead of opening a coding observer, whose pump would heartbeat
for the life of a REPL. #219's broker stub answers heartbeats.

Integrated with #299: the hoisted rcObserver sits beside #299's external
abort-signal wiring, and the outer finally removes that abort listener before
the bounded rcObserver.close().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`rc status`, `rc exposure` and `rc viewers` now ask the Cloud's owner-scoped
GET /remote/sessions/{id}/status (#226) with a short bound (3 s, backed by an
unref'd local deadline) instead of rendering placeholders. A verified answer
(schema aether.remote_session_status.v1, matching session id, a known state,
ISO-8601 Z datetimes, non-negative counts) is combined with the local record:
`active` needs both a receipted first append locally and the Cloud saying
live, and reconnecting, offline, revoked, expired, closed, pending,
pending-revoke and not-found are each named. A timeout, an outage, a Cloud
without the route (404 "Not Found"), disabled remote sessions, a refusal, a
rate limit or an answer that does not verify is an explicit `unknown` with a
reason, never `active` and never zero observers. A missing or foreign session
(404 "session not found") is reported as not-found.

The surfaces show real expiry, last heartbeat, the last accepted receipt
against the Cloud's last sequence, queued/dropped/quarantined counts, the
event categories exposed right now, and observers as count / cap. `rc
viewers` exits non-zero when the Cloud could not answer. JSON output for all
three adds the verified Cloud fields, the unknown reason, the counters and the
exposed categories, and is still built field by field from a view that holds
no credential, grant token, absolute path or event payload.

"Viewer events N / M available" no longer overclaims: diff_summary, tests,
ci, pr_status, artifact and preview have adapters but no production caller on
this base, so they move to RC_UNPRODUCED_EVENT_TYPES (7 / 13). A source test
now requires a type to be listed as produced exactly when production code uses
its adapter, so the producer lanes (#218-#222) must move their types back when
they land.

While the session is pending or the Cloud cannot be asked, the exposed
categories are labelled as not confirmed by the Cloud (`exposure_confirmed`
in JSON). The source test also checks that mapBrainEventToRc really emits each
type it is credited with.

Integrated after the producer lanes (#218-#222), which are all on this
branch: diff_summary, tests, ci, pr_status, artifact and preview each have a
production caller, so they are listed as produced again and coverage reads
13 / 13; the source test proves each caller, and main's "all thirteen viewer
event types now have a producer" test is restored beside it.
docs/REMOTE_VIEWING.md states `rc viewers` once (Cloud count / cap, `unknown`
never zero, exit 1 when the Cloud cannot answer) and keeps the #219 tests
and #223 heartbeat bullets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Converge every standalone RC producer on #223's outbox model: the outbox
FILE is the single truth, producers read it, queue into what they read and
save it, and flushOutbox re-reads it before sending and again before
committing receipts.

- rc/publish.ts (introduced with #223 for `aether review`) is now the one
  seam outside a coding run: isPublishable (#227), read-modify-write the
  file, durable before any upload, delivery never awaited. Within a process
  there is one delivery loop per outbox; a publication made while it runs
  joins it (never its record), and the loop re-reads the file once more
  before ending so a late joiner is still carried.
- Action Rail receipts (#220), media-history artifacts (#221) and preview
  phases (#222) publish through it. Deleted: #221's shared in-memory record
  and per-outbox record joining, #222's reload-unless-flushing copy and its
  own flush loop, and the three ad-hoc "active session" checks, which would
  have published from a session `rc start` had only registered.
- Every lane's multi-writer regression test passes unchanged (artifact
  committed while an upload is in flight, upload succeeding after a later
  commit, idle publishers never erasing another writer's events), as do
  LIFE's rc_pump multi-writer tests. New: an unattached session publishes
  nothing from the standalone producers either.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…scan

Self-hosted Linux CI on #292 failed four tests that pass on Windows.

rc_action_receipts (3 tests): test defect, no leak. On Linux the temp
project root was found ONLY at the outbox record's top-level
`project_root` -- the field the harness itself writes via createOutbox,
which loadOutbox needs to re-relativize paths after a restart and
flushOutbox never sends (the append body is device_id + events only).
No queued event payload and no append body carried it (probed on VPS6:
wire hit = false, queue empty, disk hit = $.project_root only). The
check scanned the raw file text, so it failed on Linux for local
bookkeeping and was vacuous on Windows, where JSON escapes the root's
backslashes and a raw-path search can never match. The leak check now
scans every append body whole (not just event_type/payload), plus the
outbox record minus `project_root`, for the root both raw and
JSON-escaped; asserts `project_root` is exactly the local root; and
asserts no append body names `project_root`. Strictly stronger on both
platforms.

rc_preview "a cancelled start publishes stopped only once the
supervisor's state is gone": product bug, Unix only. slow.mjs never
exits on its own (it times out normally, empty log). The "exit 1" was
the supervisor's own cancel: SIGTERM reaches the detached supervisor,
which SIGTERMs the child's group; the signal death closes with no code
and `closed = code ?? 1` records it as 1. Two defects followed:
- preview_supervisor: a stop before readiness (signal or POST /stop)
  fell into the not-ready branch, wrote phase "failed: dev command
  exited before readiness (exit 1)" and never removed its state. So
  `preview stop` while starting reported "cleanup was not confirmed"
  (exit 25, on every platform) and the next status/start republished
  a failure. It now waits for the child and runs the same
  identity-checked cleanup as a stop after readiness, exiting 0.
- preview start: the wait loop checked `cancelled` only at its top, so
  a cancel landing during sleep() or the /status request let the next
  read report the supervisor's reaction to that cancel -- exit 23 and
  RC phase "failed" instead of 130 and "stopping". It now re-checks
  after each await.
Windows never reached either: taskkill /F kills the supervisor before
it can write anything. Verified on VPS6 at SIGTERM +0/50/250/400ms:
exit 130, state removed, then "stopped". New preview.test covers the
POST /stop path (RED before the fix on Linux: 25 !== 0).

Gates: Linux (VPS6, Node 24.21) rc_* 380/380, preview 15/15;
Windows (int229) build, rc_* 380/380, preview 15/15,
generate-docs --check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AetherAI3
AetherAI3 force-pushed the feat/rc-release-gate-229 branch from 2ab15dd to 90f6810 Compare October 6, 2026 16:46
@AetherAI3 AetherAI3 closed this Oct 6, 2026
@AetherAI3 AetherAI3 reopened this Oct 6, 2026
@AetherAI3
AetherAI3 merged commit a4681bc into main Oct 6, 2026
4 of 16 checks passed
@AetherAI3
AetherAI3 deleted the feat/rc-release-gate-229 branch October 6, 2026 17:19
AetherAI3 added a commit that referenced this pull request Oct 6, 2026
Evidence for the deployed remote-viewing gate, run 2026-10-06 against exact
revisions: the Agent at PR #292's head (same packed bytes on a Windows and a
Linux host), the Cloud API and the observer-only Aether Code web build.

- Journey J1-J6 (enroll, start, phone scan, live coding events, viewers,
  revoke) passed on both hosts.
- P1-P5, P8, P10 passed; P6 (duplicate-safe reconnect) passed after the
  Cloud CORS fix for Last-Event-ID; P7 was not exercised live and P9 only
  partly - both listed as follow-ups.
- Two owner phone screenshots with the account chip redacted.
- Follow-up issues: #300-#303 and four AETHER-CLOUD issues.

Account names, user/device/session ids and infrastructure hostnames are
deliberately omitted from this public record.

Refs #229

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment