Skip to content

Reliability fixes: history rollback, async race, 429 retry#44

Open
JNK234 wants to merge 2 commits into
mainfrom
fix/reliability-bugs
Open

Reliability fixes: history rollback, async race, 429 retry#44
JNK234 wants to merge 2 commits into
mainfrom
fix/reliability-bugs

Conversation

@JNK234

@JNK234 JNK234 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes 3 reliability bugs from the July report. Closes #33, #32, #37.

What now works:

  1. llm:chat-with-thinking — a failed call leaves history untouched (was: orphaned prompt corrupted the agent's conversation).
  2. llm:chat-async — overlapping calls on one agent can't interleave, drop, or corrupt history (was: unlocked writes from background threads).
  3. HTTP 429 — retries 3× (1s/2s/4s, honors Retry-After, capped 10s) instead of halting the run. Shared by all providers incl. Gemini.

Scope: LLMExtension.scala (per-agent history lock), BaseHttpProvider.scala + GeminiProvider.scala (retry with backoff), plus tests.

Base: rebased onto main (NetLogo 7.0.0-2486d1e). Intentionally NOT on the v1.0.0 line, whose 7.0.4 retarget breaks test compilation under Scala 3.7 — see #45 (separate build/infra issue, does not affect these fixes).

Verified: CI green (sbt test deterministic); history fixes also confirmed via headless NetLogo smoke test.

Fixes #33
Fixes #32
Fixes #37

JNK234 added 2 commits July 20, 2026 12:49
Two related history-correctness fixes surfaced by the demos and workshop.

#33: llm:chat-with-thinking appended the user prompt to history before the
provider call and never rolled it back on failure, so a timeout or network
error permanently corrupted that agent's conversation. It now snapshots
history for the request and commits the user/assistant pair only on success,
matching the other reporters.

#32: async replies were written back to the shared per-agent ArrayBuffer from
ExecutionContext.global threads with no synchronization. Overlapping
llm:chat-async calls on the same agent could interleave pairs, lose writes, or
corrupt the buffer mid-resize, and async writes raced the sync history
primitives. All history reads/writes now go through a single lock, with each
user/assistant exchange committed atomically. Cross-pair order follows
completion order (documented): an overlapping request's context never included
the other pair regardless, so completion order is the honest, simplest choice.

Adds __TEST_FAIL and __TEST_DELAY triggers to the deterministic test provider
plus failure-path and overlapping-async regression tests.

Fixes #33
Fixes #32
Any provider rate-limit response (HTTP 429) previously threw an immediate hard
error that halted the NetLogo run — the exact failure high-frequency demos
(e.g. llm:choose every tick) hit against provider TPM limits.

Rate-limited requests now retry up to 3 times with exponential backoff
(1s, 2s, 4s). The server's Retry-After header is honored when it asks for a
longer wait than the backoff, capped at 10s so a large value can't exceed the
request timeout budget. Only 429 / rate_limit errors retry; all other errors
still fail immediately, now with a message naming the attempts made.

The retry loop lives in a shared BaseHttpProvider.executeWithRetry helper that
both the base send path and Gemini's URL-specific override use. Backoff delays
run on a single daemon-scheduled Future so they never block NetLogo's thread or
starve the global execution context. The backend is now lazy and overridable so
tests can substitute a stub.

Adds RetrySpec covering retry-then-succeed, exhausted-retries, immediate
failure on non-rate-limit errors, and Retry-After handling.

Fixes #37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant