Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.agentscope.harness.agent.sandbox;

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.harness.agent.sandbox.snapshot.SandboxSnapshotSpec;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
Expand All @@ -26,7 +27,9 @@
*
* <p>Acquire priority: {@link SandboxContext#getExternalSandbox()} &gt; {@link
* SandboxContext#getExternalSandboxState()} &gt; persisted {@link SandboxState} &gt; {@link
* SandboxClient#create}.
* SandboxClient#create}. When a persisted state exists but the resume fails, the fresh-create
* fallback keeps the persisted snapshot id so the workspace restores from the previous archive
* instead of silently resetting.
*
* <p>When a {@link SandboxExecutionGuard} is configured, the manager acquires an execution
* {@link SandboxLease} before sandbox resume/create for isolation keys that are present. The
Expand Down Expand Up @@ -95,6 +98,7 @@ public SandboxAcquireResult acquire(
}

try {
String persistedSnapshotId = null;
if (scopeKey.isPresent()) {
try {
Optional<String> stateJson = stateStore.load(scopeKey.get());
Expand All @@ -109,6 +113,9 @@ public SandboxAcquireResult acquire(
if (sandboxContext.getWorkspaceSpec() != null) {
state.setWorkspaceSpec(sandboxContext.getWorkspaceSpec().copy());
}
if (state.getSnapshot() != null) {
persistedSnapshotId = state.getSnapshot().getId();
}
Sandbox sandbox = client.resume(state);
return SandboxAcquireResult.selfManaged(sandbox, lease);
}
Expand Down Expand Up @@ -136,6 +143,8 @@ public SandboxAcquireResult acquire(
spec,
sandboxContext.getSnapshotSpec(),
sandboxContext.getClientOptions());
carryOverPersistedSnapshotId(
sandbox, persistedSnapshotId, sandboxContext.getSnapshotSpec());
return SandboxAcquireResult.selfManaged(sandbox, lease);

} catch (Exception e) {
Expand All @@ -145,6 +154,33 @@ public SandboxAcquireResult acquire(
}
}

/**
* Keeps workspace continuity when a failed resume falls through to a fresh create: points
* the new sandbox at the snapshot id persisted for this scope, so {@link Sandbox#start()}
* restores the workspace from the previous archive instead of silently resetting it, and
* {@link Sandbox#stop()} overwrites that same archive instead of orphaning it. The snapshot
* is rebuilt from the current spec, so it carries a bound client regardless of how the
* persisted state deserialized.
*/
private static void carryOverPersistedSnapshotId(
Sandbox sandbox, String persistedSnapshotId, SandboxSnapshotSpec snapshotSpec) {
SandboxState state = sandbox.getState();
if (persistedSnapshotId == null
|| persistedSnapshotId.isBlank()
|| snapshotSpec == null
|| state == null
|| state.getSnapshot() == null
|| persistedSnapshotId.equals(state.getSnapshot().getId())) {
return;
}
log.info(
"[sandbox] Resume fell back to fresh create; carrying over snapshot id {} (fresh"
+ " id was {})",
persistedSnapshotId,
state.getSnapshot().getId());
state.setSnapshot(snapshotSpec.build(persistedSnapshotId));
}

public void release(SandboxAcquireResult result) {
if (result == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.harness.agent.IsolationScope;
import io.agentscope.harness.agent.sandbox.snapshot.SandboxSnapshot;
import io.agentscope.harness.agent.sandbox.snapshot.SandboxSnapshotSpec;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -175,6 +176,55 @@ void priority3_stateStoreHit_passesSnapshotSpecToDeserialize() throws Exception
verify(client).deserializeState(STATE_JSON, snapshotSpec);
}

// ---- Priority 3 resume failure → Priority 4 fresh create keeps persisted snapshot id ----

@Test
void priority3_resumeFails_freshCreateCarriesOverPersistedSnapshotId() throws Exception {
SandboxSnapshot persistedSnapshot = mock(SandboxSnapshot.class);
SandboxSnapshot freshSnapshot = mock(SandboxSnapshot.class);
SandboxSnapshot carriedOverSnapshot = mock(SandboxSnapshot.class);
SandboxState freshState = mock(SandboxState.class);
when(persistedSnapshot.getId()).thenReturn("persisted-id");
when(freshSnapshot.getId()).thenReturn("fresh-id");
when(resumedState.getSnapshot()).thenReturn(persistedSnapshot);
when(freshSandbox.getState()).thenReturn(freshState);
when(freshState.getSnapshot()).thenReturn(freshSnapshot);
when(snapshotSpec.build("persisted-id")).thenReturn(carriedOverSnapshot);

SandboxAcquireResult result = acquireAfterFailedResume();

assertSame(freshSandbox, result.getSandbox());
verify(freshState).setSnapshot(carriedOverSnapshot);
}

@Test
void priority3_resumeFails_noPersistedSnapshot_keepsFreshSnapshot() throws Exception {
SandboxState freshState = mock(SandboxState.class);
when(freshSandbox.getState()).thenReturn(freshState);

SandboxAcquireResult result = acquireAfterFailedResume();

assertSame(freshSandbox, result.getSandbox());
verify(freshState, never()).setSnapshot(any());
verify(snapshotSpec, never()).build(any());
}

/** Session-scope acquire where persisted state exists but resume fails (claim/pod gone). */
private SandboxAcquireResult acquireAfterFailedResume() throws Exception {
when(stateStore.load(any())).thenReturn(Optional.of(STATE_JSON));
when(client.deserializeState(STATE_JSON, snapshotSpec)).thenReturn(resumedState);
when(client.resume(resumedState)).thenThrow(new RuntimeException("claim gone"));
when(client.create(any(), any(), any())).thenReturn(freshSandbox);

RuntimeContext rtx = RuntimeContext.builder().sessionId("sess-1").build();
SandboxContext sCtx =
SandboxContext.builder()
.isolationScope(IsolationScope.SESSION)
.snapshotSpec(snapshotSpec)
.build();
return manager.acquire(sCtx, rtx);
}

@Test
void userScope_withUserId_acquiresExecutionGuardWithUserKey() throws Exception {
AtomicReference<SandboxIsolationKey> capturedKey = new AtomicReference<>();
Expand Down
Loading