claude-multiacc 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/claude CHANGED
@@ -1046,13 +1046,27 @@ argv_set_model() { # $1 = model id
1046
1046
  PARK_SOFT_AUTH=3600 # 1h: a mis-parked healthy account is back within the hour
1047
1047
  PARK_SOFT_ORG=21600 # 6h: an org policy will not change in minutes
1048
1048
 
1049
+ # True when the tail of stdout carries a stream-json result marked is_error. Bounded
1050
+ # to the tail because that is where the final object lands, and because a task log
1051
+ # runs to hundreds of KB of transcript that must not be rescanned per attempt.
1052
+ stream_reported_error() {
1053
+ tail -c 8192 "$tmpd/out" 2>/dev/null \
1054
+ | LC_ALL=C grep -qE '"type"[[:space:]]*:[[:space:]]*"result"' 2>/dev/null || return 1
1055
+ tail -c 8192 "$tmpd/out" 2>/dev/null \
1056
+ | LC_ALL=C grep -qE '"is_error"[[:space:]]*:[[:space:]]*true' 2>/dev/null
1057
+ }
1058
+
1049
1059
  attempt=1
1050
1060
  cur="$pick"
1051
1061
  rc=0
1062
+ stream_err=0 # the run exited 0 but the stream said otherwise
1052
1063
  rotated=0 # at most one account rotation, exactly as before
1053
1064
  model_fb_used=0 # ...and at most one model fallback after it
1054
1065
  ARGV=("$@")
1055
1066
  while :; do
1067
+ # Per ATTEMPT: a stream error seen on an earlier account must never decide the
1068
+ # exit status of a later one that failed for its own, real reason.
1069
+ stream_err=0
1056
1070
  tok="$(acct_token "$cur")"
1057
1071
  if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
1058
1072
  if [ -n "$tok" ]; then
@@ -1062,6 +1076,17 @@ while :; do
1062
1076
  fi
1063
1077
  rc=$?
1064
1078
  exec 3<&-
1079
+ # A `--output-format stream-json` run reports an API error INSIDE the stream and
1080
+ # still exits 0: the failure arrives as the final result object carrying
1081
+ # "is_error":true, not as a non-zero status. EVERY app-robot task takes that path,
1082
+ # so gating the retry branch on $rc alone meant the pool never rotated and never
1083
+ # fell back for the exact failures it exists for — the shim saw a clean success
1084
+ # every time while the task died in a second having done no work.
1085
+ #
1086
+ # Only the FINAL result counts. A 429 that appears mid-stream and is retried by
1087
+ # the client itself ends in a healthy result, and re-running that would throw away
1088
+ # a completed run.
1089
+ if [ "$rc" -eq 0 ] && stream_reported_error; then rc=1; stream_err=1; fi
1065
1090
  if [ "$rc" -ne 0 ] && { [ "$rotated" = 0 ] || [ "$model_fb_used" = 0 ]; } \
1066
1091
  && grep -qiE "$ERRPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
1067
1092
  # Atomic marker writes: a reader must never observe a half-written marker
@@ -1143,4 +1168,7 @@ done
1143
1168
 
1144
1169
  cat "$tmpd/out"
1145
1170
  cat "$tmpd/err" >&2
1171
+ # The exit status belongs to the CLI, not to us: a stream-json caller parses the
1172
+ # stream and expects 0 here. We only borrowed rc to decide whether to retry.
1173
+ [ "$stream_err" = 1 ] && [ "$rc" = 1 ] && rc=0
1146
1174
  exit "$rc"
@@ -2206,6 +2206,13 @@ cmd_post_sync() {
2206
2206
  cmd_verify --quick
2207
2207
  }
2208
2208
 
2209
+ # Version of the copy at $1, read from its package.json on disk — the only answer
2210
+ # that describes what will actually execute.
2211
+ pkg_version_at() {
2212
+ [ -f "$1/package.json" ] || return 0
2213
+ sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1/package.json" | head -1
2214
+ }
2215
+
2209
2216
  cmd_self_update() {
2210
2217
  # Update the addon in place. npm global install => npm i -g @latest (its postinstall
2211
2218
  # re-runs install.sh). git checkout => git pull + ./install.sh. Anything else is a
@@ -2217,16 +2224,32 @@ cmd_self_update() {
2217
2224
  case "$REPO_DIR" in
2218
2225
  */node_modules/claude-multiacc|*/node_modules/claude-multiacc/*)
2219
2226
  command -v npm >/dev/null 2>&1 || { ulog "self-update: npm not found; skipping"; return 0; }
2220
- local cur lat
2221
- cur="$(npm ls -g --depth=0 claude-multiacc 2>/dev/null | sed -n 's/.*claude-multiacc@//p' | head -1)"
2227
+ local cur lat prefix after
2228
+ # Update THE COPY THAT IS RUNNING, not whichever one the ambient npm prefix
2229
+ # happens to point at. my-mini had two global installs — homebrew's on PATH and
2230
+ # nvm's under `npm root -g` — and self-update kept upgrading the nvm one and
2231
+ # reporting success while every `claude` invocation ran the stale homebrew copy.
2232
+ # It sat eleven versions behind for weeks and said "already latest" throughout.
2233
+ # A deploy nobody runs is not a deploy, and one that announces success is worse
2234
+ # than one that fails.
2235
+ prefix="${REPO_DIR%/lib/node_modules/claude-multiacc*}"
2236
+ cur="$(pkg_version_at "$REPO_DIR")"
2222
2237
  lat="$(npm view claude-multiacc version 2>/dev/null)"
2223
2238
  if [ -n "$lat" ] && [ "$cur" = "$lat" ]; then
2224
2239
  ulog "self-update: already latest ($cur)"
2225
2240
  return 0
2226
2241
  fi
2227
- ulog "self-update: npm $cur -> ${lat:-latest}"
2228
- if npm install -g claude-multiacc@latest >>"$ACC_ROOT/update.log" 2>&1; then
2229
- ulog "self-update: npm update ok"
2242
+ ulog "self-update: npm $cur -> ${lat:-latest} (prefix $prefix)"
2243
+ if npm install -g --prefix "$prefix" claude-multiacc@latest \
2244
+ >>"$ACC_ROOT/update.log" 2>&1; then
2245
+ # Verify by re-reading the file on disk. npm reporting success says nothing
2246
+ # about which tree it wrote to.
2247
+ after="$(pkg_version_at "$REPO_DIR")"
2248
+ if [ -n "$lat" ] && [ "$after" != "$lat" ]; then
2249
+ ulog "self-update: npm reported success but $REPO_DIR is still $after, not $lat — this install is NOT being updated"
2250
+ return 1
2251
+ fi
2252
+ ulog "self-update: npm update ok ($after)"
2230
2253
  else
2231
2254
  ulog "self-update: npm update FAILED (see update.log)"
2232
2255
  return 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -90,6 +90,17 @@ if [ -f "$ctl" ] && grep -qx "orgfail:$acct" "$ctl" 2>/dev/null; then
90
90
  fi
91
91
  # A limit scoped to ONE MODEL, on EVERY account: rotating cannot help, only
92
92
  # switching model can. Satisfied the moment the run carries the fallback model.
93
+ # The shape every app-robot task actually produces: --output-format stream-json
94
+ # reports the API error INSIDE the stream and exits 0.
95
+ if [ -f "$ctl" ] && grep -qx "streamlimit" "$ctl" 2>/dev/null; then
96
+ case " $* " in
97
+ *" claude-opus-5 "*|*"--model=claude-opus-5"*)
98
+ echo '{"type":"result","subtype":"success","is_error":false,"result":"done"}' ;;
99
+ *)
100
+ echo '{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"result":"You'"'"'ve reached your Fable 5 limit. Switch to another model, or manage usage credits to continue."}'
101
+ exit 0 ;;
102
+ esac
103
+ fi
93
104
  if [ -f "$ctl" ] && grep -qx "modellimit" "$ctl" 2>/dev/null; then
94
105
  case " $* " in
95
106
  *" claude-opus-5 "*|*"--model=claude-opus-5"*) : ;;
@@ -601,6 +612,31 @@ case "$out" in
601
612
  esac
602
613
  rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited
603
614
 
615
+ # ---- 12a2. an in-stream API error still triggers recovery --------------------
616
+ # With --output-format stream-json the CLI exits 0 and reports the 429 as the final
617
+ # result object. Every app-robot task takes that path, so gating retry on the exit
618
+ # status alone meant the pool never rotated and never fell back for the exact
619
+ # failures it exists for — while the shim recorded a clean success every time.
620
+ rm -f "$ACC"/acct-*/.limited; : > "$ACC/selection.log"
621
+ echo "streamlimit" > "$FAKE_CTL"
622
+ out="$(claude -p --model claude-fable-5 --output-format stream-json hello < /dev/null 2>/dev/null)"
623
+ rc=$?
624
+ case "$out" in
625
+ *'"is_error":false'*) t_ok "an in-stream API error is recovered, not reported as success" ;;
626
+ *) t_fail "stream error recovery" "rc=$rc out=$out" ;;
627
+ esac
628
+ [ "$rc" = "0" ] && t_ok "...and the caller still gets exit 0, as the CLI would give" \
629
+ || t_fail "stream error exit status" "rc=$rc"
630
+
631
+ # A stream that merely MENTIONS a limit mid-run and then completes must be left alone:
632
+ # re-running it would throw away a finished task.
633
+ rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited; : > "$ACC/selection.log"
634
+ out="$(claude -p --model claude-fable-5 hello < /dev/null 2>/dev/null)"
635
+ grep -q "retry from=" "$ACC/selection.log" 2>/dev/null \
636
+ && t_fail "spurious retry" "a healthy run was retried" \
637
+ || t_ok "a healthy run is never retried"
638
+ rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited
639
+
604
640
  # ---- 12b. pipe stdin skips retry buffering but passes bytes through -----------
605
641
  out="$(printf 'pipe-data' | claude -p --echo-stdin 2>/dev/null)"
606
642
  [ "$out" = "pipe-data" ] && t_ok "pipe stdin passes through (no retry buffering)" || t_fail "pipe stdin passthrough" "got: $out"