Skip to content

fix(memory-sync): only queue failures we attribute to the remote#68

Merged
LanNguyenSi merged 2 commits into
masterfrom
feat/memory-sync-watch-precheck-hardening
Jul 27, 2026
Merged

fix(memory-sync): only queue failures we attribute to the remote#68
LanNguyenSi merged 2 commits into
masterfrom
feat/memory-sync-watch-precheck-hardening

Conversation

@LanNguyenSi

Copy link
Copy Markdown
Owner

Closes task 1b63070d.

The finding that mattered

The task's AC1 said non-network errors must still crash loud, so launchd
respawns. The code did not guarantee that. performPush's catch had no
classification and no rethrow, so everything raised inside the try block
became "remote unavailable; queued" with exit 0: ENOSPC and EACCES while
writing the working copy, a failing commit, a broken git config,
prepareWorkingCopy and StateStore failures alike.

On a real machine that is the worst possible shape. A full disk logs a benign
queued line every tick, exits 0, is never respawned by KeepAlive, and never
syncs again. Invisible by construction.

The reviewer offered a waiver. I declined it — this runs on both machines
against the real memory remote, and shipping AC1 as met would have been a false
claim about a production safety property.

The fix

RemoteUnavailableError, thrown from lookupRemoteHead and push.
performPush rethrows anything else. Discriminating on exitCode was not
viable: the generic git fallback also throws exitCode 4 for any failing
subcommand, so the code does not carry the information. Message matching was
rejected as revert-sensitive.

Be precise about the boundary, because it is a choice, not a discovery.
prepareWorkingCopy also fetches, and that is equally remote-attributable in
principle. It is deliberately not attributed, because a fetch failure can
just as easily be local (a full disk writing the packfile). So a transient drop
in the window after ls-remote succeeds now crashes rather than queues.
Measured with a stub git: status 0 before, status 4 after.

That is bounded and safe: a sustained outage never reaches the fetch, because
the reachability precheck queues first, and schemes the precheck waves through
are still caught by ls-remote (verified with an unreachable https remote
queueing cleanly at exit 0). It fails loudly and restartably.

Verification

The boundary test uses a stub git binary forwarding everything to real git
except commit, forced to exit 17, raising a non-network failure inside the
try window after the working copy is prepared against a reachable remote.

Confirmed red before the fix — exit 0 with the false "remote unavailable"
note — and the reviewer re-confirmed by stripping the guard from a scratch copy
and getting exactly that failure back.

107 tests, 106 pass. Coverage 94.66 / 75.10 / 93.33 against the 86/65/86 gate.

Also in this PR

  • Docs named a class ("a config/data error") that was only true of one
    example. README, machine-setup and the watch plist header now name the
    mechanism.
  • restore captures the resolved full sha, so --output json reports the
    commit actually restored from rather than the abbreviation typed.
  • resolveLocalCommit no longer advertises branch/tag input it was never safe
    for; the hex precondition enforced elsewhere is now stated at the method.
  • A test title that contradicted its body is fixed, the element-shape case it
    hinted at is added, and empty-as-unset is pinned and documented — answering
    the original incident, where an operator tried to disable the probe and there
    was no documented way to do it.

Accepted waiver

AC4's "suite green" is not met and is not restated as satisfied anywhere.
The pre-existing cross-machine-profiles test fails on a hardcoded
/Users/lannguyensi path from a committed profile; confirmed byte-identical on
master and untouched here. Tracked as 112a0864.

After merge

Rebuild and restart the watch and sync launchd services on both machines.
Note this checkout is what the live com.agent-memory-sync.watch.macbook
service runs from, and its dist/ currently holds this branch's build.

Follow-up filed

11424b5e — the mirror image of the fixed bug still exists: a permanently
misconfigured remote is correctly classified and therefore queues silently
forever at exit 0, indistinguishable from a laptop on a plane. Also pins the
deliberate fetch boundary, which no test currently guards.

…short shas

Three follow-ups from the activation run (PR #63).

AGENT_MEMORY_SYNC_REACHABILITY_CHECK_COMMAND had two silent failure modes.
`false` parsed as a JSON boolean, then fell through a falsy check and returned
null, so the default ssh probe ran while the operator believed they had
disabled it. A genuinely malformed value threw an uncaught SyntaxError that
took the whole CLI down. Both now print one visible warning naming the
offending value and the expected shape, and fall back to the default probe.

`restore <short-sha>` failed with "could not fetch ref", because git only
serves full object names to remotes. Restore now resolves locally first via
git rev-parse; the working copy already fetches full branch history, so the
common case costs no extra round trip. When a short-looking sha stays
unresolved both locally and remotely, the error says to use the full
40-character sha.

The watch offline behaviour needed no production change: since PR #65 watch
goes through performPush, which already commits locally and skips the push
with one log line when the remote is unreachable, while genuine config and
data errors still crash loud for the supervisor. Only the fail-loud half was
untested and the watch plist template still documented the opposite. Both
fixed.

Note for both machines: rebuild after merge, and restart the watch and sync
launchd services so they pick up the loader change.

Refs: 1b63070d-9ea1-4a38-bba0-e58a4678b596
Review found the supervisor contract was asserted, not implemented.
performPush's catch had no classification and no rethrow, so every error
raised inside the try block became "remote unavailable; queued" with exit 0:
ENOSPC and EACCES while writing the working copy, a failing commit, a broken
git config, prepareWorkingCopy and StateStore failures alike. Under launchd
that is the worst possible shape. A machine with a full disk logs a benign
queued line every tick, exits 0, is never respawned by KeepAlive, and never
syncs again. Invisible by construction.

Adds RemoteUnavailableError, thrown from lookupRemoteHead and push.
performPush rethrows anything else. Discriminating on exitCode was not viable:
the generic git fallback also throws exitCode 4 for any failing subcommand, so
the code does not carry the information. Matching on message text was rejected
as revert-sensitive.

Note precisely what the boundary is, since it is a choice and not a discovery.
prepareWorkingCopy also fetches, and that call is equally remote-attributable
in principle. It is deliberately NOT attributed, because a fetch failure can
just as easily be local, a full disk writing the packfile. So a transient drop
in the window after ls-remote succeeds now crashes rather than queues. That is
bounded: a sustained outage never reaches the fetch, because the reachability
precheck queues first, and schemes the precheck waves through are still caught
by ls-remote. It fails loudly and restartably, which is the safe direction.

The boundary test uses a stub git binary that forwards everything to real git
except commit, which exits 17, so a non-network failure is raised inside the
try window after the working copy is already prepared against a reachable
remote. Confirmed red before this change: exit 0 with the false "remote
unavailable" note.

Also from review: the docs named a class ("a config/data error") that was only
true of one example, so README, machine-setup and the watch plist header now
name the mechanism instead. restore captures the resolved full sha rather than
discarding it, so --output json reports the commit actually restored from
instead of the abbreviation typed. resolveLocalCommit no longer advertises
branch and tag input it was never safe for; the hex precondition enforced in
restore is now stated at the method. A test title that contradicted its body
is fixed, with the element-shape case it hinted at added, plus a test pinning
empty-as-unset. The README documents that convention and, answering the
original incident, how to actually disable the probe.

Note for both machines: rebuild and restart the watch and sync services after
merge.

Refs: 1b63070d-9ea1-4a38-bba0-e58a4678b596
@LanNguyenSi LanNguyenSi added review:tests-pass merge-approval gate prerequisite review:checklist-complete merge-approval gate prerequisite review:comments-resolved merge-approval gate prerequisite review:scope-matches-task merge-approval gate prerequisite review:evidence-logged merge-approval gate prerequisite labels Jul 27, 2026
@LanNguyenSi
LanNguyenSi merged commit 83dc33c into master Jul 27, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:checklist-complete merge-approval gate prerequisite review:comments-resolved merge-approval gate prerequisite review:evidence-logged merge-approval gate prerequisite review:scope-matches-task merge-approval gate prerequisite review:tests-pass merge-approval gate prerequisite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant