Repository navigation
[Bug]: AgentProtocolTaskStore.resume() rejects a task that just reached awaiting_confirm #3552
Description
What happens
AgentProtocolTaskStoreHitlTest.submit_pausesOnPermissionAsking_andResumeDenyCompletes intermittently fails on the build (ubuntu-latest) lane:
[ERROR] Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.242 s <<< FAILURE! -- in io.agentscope.extensions.agentprotocol.AgentProtocolTaskStoreHitlTest
[ERROR] io.agentscope.extensions.agentprotocol.AgentProtocolTaskStoreHitlTest.submit_pausesOnPermissionAsking_andResumeDenyCompletes -- Time elapsed: 0.107 s <<< ERROR!
java.lang.IllegalStateException: task is already running: hitl-1
[ERROR] AgentProtocolTaskStoreHitlTest.submit_pausesOnPermissionAsking_andResumeDenyCompletes:116 » IllegalState task is already running: hitl-1
Observed on https://github.com/agentscope-ai/agentscope-java/actions/runs/38519276141 (job 115705004481, head 09582f5e3, 2026-10-14T05:55Z). That is the ubuntu lane of PR #3533, whose diff only touches LocalSessionTurnGate — nothing in the protocol module.
Root cause
resume() rejects a task whose previous run future is not done yet:
CompletableFuture<String> prior = futures.get(taskId);
if (prior != null && !prior.isDone()) {
throw new IllegalStateException("task is already running: " + taskId);
}But a run that pauses for HITL persists awaiting_confirm inside runAgent, just before it returns its sentinel:
markAwaitingConfirm(taskId, agentId, pending); // persists awaitingConfirm = true
...
eventBus.publish(taskId, require);
return AWAITING_CONFIRM_SENTINEL; // <- the future completes only hereand the future is removed only from the completion callback:
f.whenComplete((r, ex) -> futures.remove(taskId));So between "awaiting_confirm is persisted" and "the future completes", snapshot() already reports awaiting_confirm while futures.get(taskId) is still not done. A client that reacts to that status and calls resume() inside that window gets a spurious task is already running, even though the task is paused and not executing.
There is a second, latent defect in the same window: futures.remove(taskId) is unconditional, so the settling run's callback can evict the future that resume() just installed for the same task id — leaving waitFor / cancel / snapshot pointed at a future that is no longer tracked.
Deterministic reproduction
The window is tiny in practice (it needs the run thread to be descheduled between the persist and the return), which is why it only shows up on a loaded runner. It can be widened deterministically by making the event bus slow only for the REQUIRE_CONFIRM publish, which sits exactly between the persist and the return:
AgentProtocolEventBus slowBus = new AgentProtocolEventBus() {
private final AgentProtocolTaskEventBus delegate = new AgentProtocolTaskEventBus();
@Override public RemoteAgentEvent publish(String taskId, RemoteAgentEvent event) {
if (event.getType() == RemoteEventType.REQUIRE_CONFIRM) {
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
return delegate.publish(taskId, event);
}
@Override public Flux<RemoteAgentEvent> subscribe(String taskId, long fromSeq) { return delegate.subscribe(taskId, fromSeq); }
@Override public void complete(String taskId) { delegate.complete(taskId); }
};With that bus, and the existing HITL mock agent (asking reply on the first streamEvents call, completed reply on the second), this fails every time on current main:
store.submit("hitl-race", "worker", "please run bash", Map.of("detail", "full"));
awaitCondition(() -> "awaiting_confirm".equals(store.snapshot("hitl-race").get("status")), 5_000);
store.resume("hitl-race", List.of(new RemoteConfirmDecision("tc-ask", false, "not allowed")));
// -> IllegalStateException: task is already running: hitl-raceSuggested fix
A task that is awaiting confirmation is by definition not executing, so the in-flight-future guard must not reject it, and the cleanup must be identity-checked:
- if (prior != null && !prior.isDone()) {
+ if (prior != null && !prior.isDone() && !rec.isAwaitingConfirm()) {
throw new IllegalStateException("task is already running: " + taskId);
}
...
- f.whenComplete((r, ex) -> futures.remove(taskId));
+ f.whenComplete((r, ex) -> futures.remove(taskId, f));Verified locally on main: the reproducer above passes deterministically (~0.2 s) and the whole agentscope-extensions-agent-protocol suite is green (33/33, JDK 17).
Note on the area
AgentProtocolTaskStore.java is currently touched by two open PRs (#3013, #2177), so I am reporting this rather than opening a competing PR. Happy to submit the fix above if the area frees up.
Activity