fix(stream): resume before mark-done, safe env parsing

- loadChat now calls requestResumeForAllInProgress BEFORE the
  interrupted-generation mark-done heuristic. That heuristic flips
  done=true when getTaskIdsByChatId returns empty/null, which includes
  API failures; previously that path would prevent resume from being
  attempted even when the stream was still alive server-side. Spurious
  resume requests cost one round trip and clear the fence via empty
  reply, so calling it unconditionally first is strictly safer.

- Env-var float parsing wrapped in a _float_env helper that falls back
  to default with a warning on invalid input instead of raising at
  import time. Previous code would ValueError out of the module and
  prevent socket service startup on operator typo.

Pushed back: bot flagged asyncio.Lock as unable to be stored in a
WeakValueDictionary. Verified directly on CPython — both weakref.ref
and WeakValueDictionary insertion work fine for asyncio.Lock. No
change needed.
This commit is contained in:
Claude 2026-04-15 07:41:05 +00:00
parent cffe1537ec
commit 630f452e93
No known key found for this signature in database
2 changed files with 22 additions and 13 deletions

View file

@ -185,12 +185,19 @@ RESUME_STREAM_TTL_REFRESH_EVERY = 64
# Replay read timeout: looser since a resume is user-blocking anyway
# and silent timeout here is worse than a brief extra wait. Both
# configurable for infra where Redis isn't colocated.
RESUME_STREAM_REDIS_TIMEOUT_SEC = float(
os.environ.get('RESUME_STREAM_REDIS_TIMEOUT_SEC', '0.1')
)
RESUME_STREAM_READ_TIMEOUT_SEC = float(
os.environ.get('RESUME_STREAM_READ_TIMEOUT_SEC', '1.0')
)
def _float_env(name: str, default: float) -> float:
val = os.environ.get(name)
if val is None or val == '':
return default
try:
return float(val)
except (TypeError, ValueError):
log.warning(f'Invalid {name}={val!r}; using default {default}')
return default
RESUME_STREAM_REDIS_TIMEOUT_SEC = _float_env('RESUME_STREAM_REDIS_TIMEOUT_SEC', 0.1)
RESUME_STREAM_READ_TIMEOUT_SEC = _float_env('RESUME_STREAM_READ_TIMEOUT_SEC', 1.0)
# Module-level circuit breaker for the streaming hot path. After N
# consecutive Redis failures/timeouts, short-circuit seq/log calls for

View file

@ -1544,6 +1544,15 @@
taskIds = taskRes.task_ids;
}
// Request resume BEFORE marking messages done. The
// interrupted-generation heuristic below flips `done=true`
// when getTaskIdsByChatId returns empty/null, which can
// happen on API failure too; asking for resume first lets
// us recover when the stream is actually still alive.
// Server replies empty if no log exists, so a spurious
// request costs one round trip.
requestResumeForAllInProgress();
// If no active tasks and current message is incomplete, generation was interrupted
const currentMessage = history.currentId ? history.messages[history.currentId] : null;
if (
@ -1555,13 +1564,6 @@
currentMessage.done = true;
}
// Resume any in-flight streams. Not gated on taskIds —
// that call can fail or race and is not authoritative;
// requestResumeForAllInProgress already filters to
// unfinished assistants and the server no-ops when no
// log exists.
requestResumeForAllInProgress();
await tick();
return true;