anthropic.lib.environments._poller acknowledges every work item before yielding it to the consumer (_poller.py:234; docstring at :66: "Each yielded BetaSelfHostedWork has already been ack'd"). There is no deferred-ack option and no nack/fail API. Verified identical from 0.109.1 through 0.116.0.
For self-hosted managed-agents environments this makes any consumer-side exception after the yield permanently strand that session:
- the acked item freezes in
state='starting' with latest_heartbeat_at=null;
reclaim_older_than_ms cannot reach it (it only reaches unacknowledged items), and lease-TTL re-queue never fires (requires a first heartbeat);
- the platform keeps one work item per session, so all subsequent
user.message events park in requires_action with no new work enqueued — the session is wedged until an operator runs work.stop(work_id, force=True) and sends a user.interrupt.
We hit this in production twice in one day (different consumer exceptions, same terminal state) and separately counted 29 items stuck in starting with null heartbeats.
Ask
Either of:
- Deferred ack: an opt-in poller mode that acks after the consumer callback returns successfully (at-least-once semantics), or
- nack/fail API: a way for a consumer to return a failed item to a re-pendable state.
Related, happy to file separately: documenting which work-item states re-enqueue on which user events (empirically: stopped re-pends on message/interrupt; a dead-heartbeat started item only after a server-side stop ~5 min later; an acked-but-never-heartbeated starting item never), and a server-side janitor for starting + null-heartbeat items older than a TTL.
Workaround we ship today
Consumer retry + work.stop(force=True) on final failure, plus a scheduled sweep that force-stops stale starting items and interrupts sessions parked in requires_action with no live worker. Effective, but every self-hosted-environment consumer will independently rediscover this failure class — ack-after-success semantics in the SDK would eliminate it.
anthropic.lib.environments._polleracknowledges every work item before yielding it to the consumer (_poller.py:234; docstring at:66: "Each yieldedBetaSelfHostedWorkhas already been ack'd"). There is no deferred-ack option and no nack/fail API. Verified identical from 0.109.1 through 0.116.0.For self-hosted managed-agents environments this makes any consumer-side exception after the yield permanently strand that session:
state='starting'withlatest_heartbeat_at=null;reclaim_older_than_mscannot reach it (it only reaches unacknowledged items), and lease-TTL re-queue never fires (requires a first heartbeat);user.messageevents park inrequires_actionwith no new work enqueued — the session is wedged until an operator runswork.stop(work_id, force=True)and sends auser.interrupt.We hit this in production twice in one day (different consumer exceptions, same terminal state) and separately counted 29 items stuck in
startingwith null heartbeats.Ask
Either of:
Related, happy to file separately: documenting which work-item states re-enqueue on which user events (empirically:
stoppedre-pends on message/interrupt; a dead-heartbeatstarteditem only after a server-side stop ~5 min later; an acked-but-never-heartbeatedstartingitem never), and a server-side janitor forstarting+ null-heartbeat items older than a TTL.Workaround we ship today
Consumer retry +
work.stop(force=True)on final failure, plus a scheduled sweep that force-stops stalestartingitems and interrupts sessions parked inrequires_actionwith no live worker. Effective, but every self-hosted-environment consumer will independently rediscover this failure class — ack-after-success semantics in the SDK would eliminate it.