Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,32 @@ RFC; a release that bumps one says so here.

## [Unreleased]

### Installers (`vibemon-install.sh` 0.2.15+install.2)

Driven by the 67 failed install reports of 2026-09-05..10-11 (43 distinct
client addresses; 28 never reported a later success).

- **A systemd user service that refuses to register no longer ends the
install.** `attempt daemon install` failed on five Linux machines whose
`systemctl --user` had passed the pre-check, *after* the pairing token was
spent and the hooks were in: a paired machine that uploaded nothing, and the
same command could not be pasted again. Where the session supervisor can run
(`nohup`, `setsid`, `flock`) the script now continues with it, reports the
refusal as its own `daemon` report (with the failing command's words and the
environment facts) and finishes `done` with the fallback named in `error`.
Where it cannot (macOS, or a tool missing) it stops as before and now says
that the machine is paired and to run the same command again without a token.
- **The network steps get a second and third try**: the legacy-key exchange
(11 reports of "This operation was aborted"; a key the web does not know is
still answered once), the pairing check (no answer or 5xx) and the first
upload (4 reports of `Network Err`). Every request is bounded
(`--connect-timeout`, `--max-time`); before, the exchange and the check had no
limit and a dead server hung the script. `VIBEMON_INSTALL_RETRY_DELAY`
(default 3 s) scales the wait between tries.
- A run that stops at the gate that changes nothing (no token, never connected)
is reported `ok` whatever its exit status; two Linux reports said `exit 1`
there (not reproduced on macOS with dash, sh or bash).

## [0.2.15] — 2026-10-07

A server release: the client binaries differ from 0.2.14 only in their version
Expand Down
22 changes: 22 additions & 0 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

Execution log for `TODO.md`. Newest session first. Read this before working.

## 2026-10-12 — installer failures: fallback and retries (not released)

Read the 67 failed install reports (`attemptdb_install_reports`, 355 total, vibemon Supabase
`sirpdtcwawcidhgtltps`) against the script. Nothing new was breaking after 0.2.15 was served
(10/8 21:24 KST: 5 successes, 1 non-bug), but three causes were still in the code.

**Changed (working tree, uncommitted, not tagged, not pushed).** `docs/migration/vibemon-install.sh`
(`0.2.15+install.2`): systemd registration failure falls back to the session supervisor and is
reported on its own; key exchange / pairing check / first upload retried three times and bounded;
the no-credentials gate is reported ok. `tests/installers/test_vibemon_install.py`: 13 new tests
(9 fail on the old script), the failing-daemon tests now remove `setsid` so they still mean "no
fallback"; full `tests/installers` 160 OK (7 CI-only skips), also under dash. vibemon-web (separate
repo, uncommitted): `mintPairing` 15 s + one retry on no-answer/5xx, `maxDuration = 60` on the pair
route, test.

**Not done.** To ship the script: commit, CI + `Install and sync smoke` green, new immutable
`install-YYYY-MM-DD.N` tag, vibemon-web `install-pins.ts` ref bump (the Windows script is unchanged,
so `INSTALLER_REF` stays). The web change deploys on merge to `main` (E2E has been red since 10/8,
unrelated). The `exit 1` on the no-credentials gate (2 Linux reports) was not reproduced; the report
fix is a guard, not a cause. Linux `init` (2) and `connect` (2) failures have no log: 0.2.15 will
capture the next ones.

## 2026-10-08 — 0.2.15 released and deployed; installers moved to it

Follows the 2026-10-07 install check (see the memory note and git history for the rehearsal
Expand Down
14 changes: 14 additions & 0 deletions docs/migration/vibemon-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ container/WSL hints and free disk on Linux; the GUI domain on macOS; the
Windows build, admin and the Scheduled Task on Windows. No file contents, no
secrets. The admin install-reports page shows it.

### What the script retries and what it falls back to

- The legacy-key exchange, the pairing check and the first upload are tried up
to three times (a slow or restarting server is not a failed install); each
request is bounded. A key the web does not know, and a spent pairing token,
are answers and are not tried again.
- If `attempt daemon install` fails on Linux (systemd answered the pre-check
but would not register the unit) and `nohup`, `setsid` and `flock` exist, the
script continues with the session supervisor and says so. Two reports result:
a `daemon` failure carrying the command's output and the environment facts,
then `done` with the fallback in `error`. Without those tools, and on macOS,
it stops, and the saved connection is finished by running the same command
again **without** a token.

## Removing `~/.vibemon`

`vibemon-install.sh --purge-legacy` deletes the directory only after checking
Expand Down
143 changes: 116 additions & 27 deletions docs/migration/vibemon-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# key, proven by an authenticated handshake, saved only on success
# 5. installs the agent hooks next to any existing ones
# 6. registers the background daemon (launchd / systemd --user), or
# starts a Linux session supervisor when no user service is available
# starts a Linux session supervisor when systemd --user cannot do it
# 7. uploads once and requires the server to accept it
# 8. only then removes the legacy VibeMon hooks (~/.vibemon/notify.sh)
# 9. shows `attempt doctor`
Expand Down Expand Up @@ -108,8 +108,12 @@ INSTALL_TMP=""
# ATTEMPTDB_VERSION to the workspace version so a release cannot leave it
# behind again. A newer `attempt` already on the machine is kept.
ATTEMPTDB_VERSION="${ATTEMPTDB_VERSION:-0.2.15}"
INSTALLER_VERSION="0.2.15+install.1"
INSTALLER_VERSION="0.2.15+install.2"
INSTALLER_REF="install-2026-10-07.1"
# Seconds between tries of the network steps that are worth a second try (the
# key exchange, the pairing check, the first upload): n x this. Tests set 0.
RETRY_DELAY="${VIBEMON_INSTALL_RETRY_DELAY:-3}"
case "$RETRY_DELAY" in ""|*[!0-9]*) RETRY_DELAY=3 ;; esac
ATTEMPTDB_INSTALLER="${ATTEMPTDB_INSTALLER:-https://raw.githubusercontent.com/nullarch/attemptdb/v${ATTEMPTDB_VERSION}/install.sh}"
export ATTEMPTDB_VERSION

Expand Down Expand Up @@ -262,16 +266,34 @@ run_live() {
pair_check() {
if [ "$LOG_MODE" = tee ]; then
_ce="$LOG.$$.curl"
_cs="$(curl -sS -o /dev/null -w '%{http_code}' "$1" 2>"$_ce")" || _cs=000
_cs="$(curl -sS --connect-timeout 10 --max-time 30 -o /dev/null -w '%{http_code}' "$1" 2>"$_ce")" || _cs=000
if [ -s "$_ce" ]; then cat "$_ce" >&2; cat "$_ce" >>"$LOG" 2>/dev/null || true; fi
rm -f "$_ce" 2>/dev/null || true
else
_cs="$(curl -sS -o /dev/null -w '%{http_code}' "$1")" || _cs=000
_cs="$(curl -sS --connect-timeout 10 --max-time 30 -o /dev/null -w '%{http_code}' "$1")" || _cs=000
fi
case "$_cs" in 000|[1-5][0-9][0-9]) ;; *) _cs=000 ;; esac
printf '%s' "$_cs"
return 0
}
# The same check, tried up to three times when the request got no answer or a
# 5xx: a slow or restarting server is not a reason to throw the pairing away.
# A 2xx/4xx is an answer and is returned at once.
pair_check_retry() {
_pn=1
while :; do
_pc="$(pair_check "$1")"
case "$_pc" in
000|5[0-9][0-9]) [ "$_pn" -lt 3 ] || break ;;
*) break ;;
esac
say "vibemon: the sync server did not answer the pairing check (try $_pn of 3); trying again" >&2
sleep $((_pn * RETRY_DELAY))
_pn=$((_pn + 1))
done
printf '%s' "$_pc"
return 0
}
case "$PROFILE" in
metadata_only|semantic|messages|full) ;;
*) fail "unknown --profile $PROFILE (metadata_only | semantic | messages | full)" ;;
Expand Down Expand Up @@ -339,22 +361,42 @@ fingerprint() {
probe attempt attempt --version
return 0
}
# On a failed service step, write the facts above. With no log to write to
# they go to a temporary file the report reads instead.
diagnose() {
[ "$1" -ne 0 ] || return 0
case "$STEP" in daemon|environment) ;; *) return 0 ;; esac
[ "$DRY_RUN" -eq 0 ] || return 0
# Write the facts above after whatever the log holds now. With no log to
# write to they go to a temporary file the report reads instead.
record_fingerprint() {
_dst="$LOG"
if [ -z "$_dst" ]; then
FPTMP="$(mktemp 2>/dev/null)" || FPTMP=""
[ -n "$FPTMP" ] || { FPTMP="$(mktemp 2>/dev/null)" || FPTMP=""; }
_dst="$FPTMP"
fi
[ -n "$_dst" ] || return 0
fingerprint >>"$_dst" 2>/dev/null || true
[ -n "$REPORT_SRC" ] || REPORT_SRC="$_dst"
return 0
}
# On a failed service step, write the facts above.
diagnose() {
[ "$1" -ne 0 ] || return 0
case "$STEP" in daemon|environment) ;; *) return 0 ;; esac
[ "$DRY_RUN" -eq 0 ] || return 0
record_fingerprint
return 0
}
# What the Linux session runtime needs that this machine lacks: the reason,
# or nothing when it can run. It is the way out when systemd --user is missing
# and also when it is there but cannot register the daemon.
session_tools_error() {
for tool in nohup setsid flock; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'Linux session sync requires nohup, setsid and flock; missing %s' "$tool"
return 0
fi
done
if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then
printf 'Linux session sync requires sha256sum or shasum'
fi
return 0
}

# The end of the log as a report field: control characters dropped, keys,
# tokens, credentials and home/temp paths blanked, JSON-escaped, the last
Expand All @@ -373,12 +415,14 @@ build_tail() {
report() {
code="$1"
[ "$REPORT" -eq 1 ] && [ "$DRY_RUN" -eq 0 ] || return 0
ok=false; [ "$code" -eq 0 ] && ok=true
# The gate that changes nothing (step noop) ends with exit 0 and has nothing
# after it that can fail; two Linux reports said `exit 1` there anyway.
ok=false; if [ "$code" -eq 0 ] || [ "$STEP" = noop ]; then ok=true; fi
unattended=false; [ "$UNATTENDED" -eq 1 ] && unattended=true
os="$(uname -s 2>/dev/null || echo unknown)"
arch="$(uname -m 2>/dev/null || echo unknown)"
av="$(attempt --version 2>/dev/null | sed -n 's/^attempt //p' | head -n 1)"
if [ "$code" -ne 0 ] && [ -z "$LAST_ERROR" ]; then
if [ "$ok" = false ] && [ -z "$LAST_ERROR" ]; then
LAST_ERROR="command failed during $STEP (${LAST_CMD:-exit $code}); see the install log"
fi
err="$(printf '%s' "$LAST_ERROR" | head -n 1 | tr -d '"\\' | tr '\t\r' ' ' \
Expand Down Expand Up @@ -492,12 +536,7 @@ if [ "$DRY_RUN" -eq 0 ]; then
Linux)
if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show-environment >/dev/null 2>&1; then
RUNTIME=session
for tool in nohup setsid flock; do
command -v "$tool" >/dev/null 2>&1 || service_error="Linux session sync requires nohup, setsid and flock; missing $tool"
done
if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then
service_error="Linux session sync requires sha256sum or shasum"
fi
service_error="$(session_tools_error)"
say "vibemon: systemd user service unavailable; selecting Linux session sync"
fi
;;
Expand Down Expand Up @@ -529,11 +568,28 @@ if [ -n "$LEGACY_KEY" ] && [ -z "$TOKEN" ] && [ "$connected" -eq 0 ]; then
say "+ curl -fsS -X POST $WEB/api/attemptdb/pair (vbm_… → pair_…)"
TOKEN="pair_dryrun"
else
resp="$(curl -sS -X POST -H 'Content-Type: application/json' \
--data "{\"api_key\":\"$LEGACY_KEY\"}" "$WEB/api/attemptdb/pair" 2>/dev/null || true)"
TOKEN="$(printf '%s' "$resp" | sed -n 's/.*"token": *"\(pair_[A-Za-z0-9_-]*\)".*/\1/p')"
if [ -z "$TOKEN" ]; then
# The web asks the sync server for the token with an 8 s limit, so a
# slow server shows up here as "This operation was aborted" (11 failed
# installs, 2026-09 to 2026-10). Nothing has changed yet, so a try or
# two more is free; an answer that says the key itself is wrong is not
# tried again. Each request is bounded, so a dead server cannot hang
# the script.
_xn=1
while :; do
resp="$(curl -sS --connect-timeout 10 --max-time 40 -X POST -H 'Content-Type: application/json' \
--data "{\"api_key\":\"$LEGACY_KEY\"}" "$WEB/api/attemptdb/pair" 2>/dev/null || true)"
TOKEN="$(printf '%s' "$resp" | sed -n 's/.*"token": *"\(pair_[A-Za-z0-9_-]*\)".*/\1/p')"
[ -z "$TOKEN" ] || break
reason="$(printf '%s' "$resp" | sed -n 's/.*"error": *"\([^"]*\)".*/\1/p')"
case "$reason" in
"unknown API key"*|"api_key must be"*|"account deleted"*|"device pairing is not enabled"*) break ;;
esac
[ "$_xn" -lt 3 ] || break
say "vibemon: the web did not answer the key exchange (${reason:-no usable answer}); trying again" >&2
sleep $((_xn * RETRY_DELAY))
_xn=$((_xn + 1))
done
if [ -z "$TOKEN" ]; then
fail "the web did not accept this API key: ${reason:-no usable answer from $WEB} (nothing changed; get a command at $WEB/devices)"
fi
web_server="$(printf '%s' "$resp" | sed -n 's/.*"sync_url": *"\([^"]*\)".*/\1/p')"
Expand All @@ -557,7 +613,7 @@ if [ -n "$TOKEN" ]; then
if [ "$DRY_RUN" -eq 1 ]; then
say "+ curl -fsS $SERVER/v1/pair/$TOKEN"
else
code="$(pair_check "$SERVER/v1/pair/$TOKEN")"
code="$(pair_check_retry "$SERVER/v1/pair/$TOKEN")"
case "$code" in
200) ;;
410) fail "the pairing token has expired or was already used; get a new one at https://vibemon.dev/devices" ;;
Expand Down Expand Up @@ -727,15 +783,48 @@ STEP=daemon
# every few seconds. Re-running re-registers.
if [ "$RUNTIME" = session ]; then
attempt daemon status >/dev/null 2>&1 || fail "Linux session daemon stopped; legacy hooks unchanged"
else
run attempt daemon install || fail "background service registration failed; legacy hooks unchanged (see the install log)"
elif ! run attempt daemon install; then
# By now the pairing token is spent and the hooks are in, so stopping here
# leaves a paired machine that uploads nothing, and the same command cannot
# be pasted again. Five Linux installs ended this way between 2026-09-26
# and 2026-10-08 (systemd answered the pre-check, then refused the unit).
# Where the session supervisor can run, run it: the person gets a working
# install that lasts as long as the environment, and the report keeps what
# systemd said. Elsewhere (macOS, or no nohup/setsid/flock) stop, and say
# how to finish without a new token.
_why="$(session_tools_error)"
if [ "$(uname -s)" = Linux ] && [ -z "$_why" ]; then
LAST_ERROR="background service registration failed; continued with Linux session sync (see the install log)"
say "vibemon: the systemd user service could not be registered; continuing with Linux session sync instead"
record_fingerprint
report 1
RUNTIME=session
start_session_runtime
else
say "vibemon: this machine is paired and its hooks are installed, but nothing uploads until a background" >&2
say " service runs. Fix the cause above, then run the same install command again without a pairing" >&2
say " token: the saved connection is kept and the remaining steps run." >&2
fail "background service registration failed; legacy hooks unchanged (see the install log)"
fi
fi

STEP=upload
# 7. One upload now; the server must accept it before anything is removed.
# Three tries: a network blip or a restarting server (four installs ended
# here on `Network Err`) should not leave the switch half done.
upload_now() {
_un=1
while :; do
if logged attempt sync now; then return 0; fi
[ "$_un" -lt 3 ] || return 1
say "vibemon: the first upload did not go through (try $_un of 3); trying again" >&2
sleep $((_un * RETRY_DELAY))
_un=$((_un + 1))
done
}
if [ "$DRY_RUN" -eq 1 ]; then
say "+ attempt sync now"
elif ! logged attempt sync now; then
elif ! upload_now; then
say "" >&2
say "vibemon: the first upload did not go through. AttemptDB is installed and hooks are in place," >&2
say " but the legacy VibeMon hooks were left untouched so collection continues as before." >&2
Expand Down
Loading
Loading