Summary
The activity callback added in #1867 acknowledges before its Slack status write happens, so a session that completes in the same window can be left showing Working… permanently.
Mechanism
In packages/slack-bot/src/callbacks.ts:
c.executionCtx.waitUntil(
setAssistantThreadStatusBestEffort(
c.env,
valid.context.channel,
valid.context.threadTs,
ASSISTANT_WORKING_STATUS,
{ event: "refresh", traceId, sessionId: valid.sessionId }
)
);
The handler then logs and returns 200 without awaiting that task. So the ordering between the deferred Working… write and the completion path is unconstrained across workers:
- sandbox heartbeat →
POST /callbacks/activity;
- handler schedules the status write via
waitUntil and returns 200;
- the completion queue delivers the final reply and clears the thread status;
- the deferred task from step 2 now runs and re-asserts
ASSISTANT_WORKING_STATUS.
After step 4 nothing clears it again — the completion path has already run, and the next clear would only come from another session on the same thread. The control-plane's check before sending the callback cannot close this, because the window opens after the callback is acknowledged.
The stale-timestamp guard above it (ACTIVITY_CALLBACK_MAX_AGE_MS) bounds how old the callback may be, not how long the deferred write may be queued, so it does not constrain this ordering either.
Why it is worth fixing rather than tolerating
setAssistantThreadStatusBestEffort is deliberately best-effort, which is right for a transient indicator — but the failure here is not a dropped update, it is a durable wrong state on a finished thread. The user sees an assistant that appears to still be working with no further output.
Possible directions
Both involve a tradeoff I do not think an outside contributor should pick unilaterally, since it belongs to the heartbeat contract introduced in #1867:
- Await the status write before acknowledging. Simple and removes the window, but costs callback ack latency — presumably the reason
waitUntil was chosen.
- Re-check liveness inside the deferred task, immediately before writing, and skip the write if the session is no longer active. Keeps the fast ack; needs a cheap authoritative liveness read and still leaves a much smaller window unless the status write and the check are ordered against the completion path.
A third option is to make the completion path authoritative by having it record a monotonic marker the activity write compares against, so a late Working… write cannot overwrite a newer completion — analogous to the status-revision approach used for session archiving.
Provenance
Found while reviewing an upstream sync into a downstream fork; the file is unmodified from upstream, so this is an upstream report rather than a fork-local divergence. I have not shipped a fix.
Summary
The activity callback added in #1867 acknowledges before its Slack status write happens, so a session that completes in the same window can be left showing
Working…permanently.Mechanism
In
packages/slack-bot/src/callbacks.ts:The handler then logs and returns
200without awaiting that task. So the ordering between the deferredWorking…write and the completion path is unconstrained across workers:POST /callbacks/activity;waitUntiland returns 200;ASSISTANT_WORKING_STATUS.After step 4 nothing clears it again — the completion path has already run, and the next clear would only come from another session on the same thread. The control-plane's check before sending the callback cannot close this, because the window opens after the callback is acknowledged.
The stale-timestamp guard above it (
ACTIVITY_CALLBACK_MAX_AGE_MS) bounds how old the callback may be, not how long the deferred write may be queued, so it does not constrain this ordering either.Why it is worth fixing rather than tolerating
setAssistantThreadStatusBestEffortis deliberately best-effort, which is right for a transient indicator — but the failure here is not a dropped update, it is a durable wrong state on a finished thread. The user sees an assistant that appears to still be working with no further output.Possible directions
Both involve a tradeoff I do not think an outside contributor should pick unilaterally, since it belongs to the heartbeat contract introduced in #1867:
waitUntilwas chosen.A third option is to make the completion path authoritative by having it record a monotonic marker the activity write compares against, so a late
Working…write cannot overwrite a newer completion — analogous to the status-revision approach used for session archiving.Provenance
Found while reviewing an upstream sync into a downstream fork; the file is unmodified from upstream, so this is an upstream report rather than a fork-local divergence. I have not shipped a fix.