fix: resume ACP sessions without history replay (#3859)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
+1
-1
@@ -148,4 +148,4 @@ require (
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect
|
||||
)
|
||||
|
||||
replace github.com/coder/acp-go-sdk => github.com/kdlbs/acp-go-sdk v0.13.6-0.20260821092416-13e3f4dc12c2
|
||||
replace github.com/coder/acp-go-sdk => github.com/kdlbs/acp-go-sdk v0.13.6-0.20260922084253-07a3e52f8179
|
||||
|
||||
+2
-2
@@ -243,8 +243,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kdlbs/acp-go-sdk v0.13.6-0.20260821092416-13e3f4dc12c2 h1:aWN8W+iAeSlt5iIf+YwzyZ55VevQO0jKp6TvhL818vs=
|
||||
github.com/kdlbs/acp-go-sdk v0.13.6-0.20260821092416-13e3f4dc12c2/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
|
||||
github.com/kdlbs/acp-go-sdk v0.13.6-0.20260922084253-07a3e52f8179 h1:pRuzXC7qPQMAseD8bxcukDqRVJ89wwtmQ1T0uVL8fw8=
|
||||
github.com/kdlbs/acp-go-sdk v0.13.6-0.20260922084253-07a3e52f8179/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
|
||||
@@ -140,7 +140,7 @@ type InitializeResult struct {
|
||||
// It handles the initialize handshake and session creation/loading based on config.
|
||||
//
|
||||
// Session behavior:
|
||||
// - If agentConfig.Runtime().SessionConfig.NativeSessionResume is true AND existingSessionID is provided: use session/load
|
||||
// - If agentConfig.Runtime().SessionConfig.NativeSessionResume is true AND existingSessionID is provided: restore via the adapter
|
||||
// - If NativeSessionResume is false (CLI handles resume): always use session/new
|
||||
// - Otherwise: use session/new
|
||||
func (sm *SessionManager) InitializeSession(
|
||||
@@ -295,7 +295,7 @@ func (sm *SessionManager) getResumeContextPrompt(agentConfig agents.Agent, taskS
|
||||
return resumePrompt
|
||||
}
|
||||
|
||||
// loadSession loads an existing session via ACP session/load
|
||||
// loadSession restores an existing session using the adapter's compatible ACP request.
|
||||
func (sm *SessionManager) loadSession(
|
||||
ctx context.Context,
|
||||
client *agentctl.Client,
|
||||
@@ -303,7 +303,7 @@ func (sm *SessionManager) loadSession(
|
||||
sessionID string,
|
||||
mcpServers []agentctltypes.McpServer,
|
||||
) (string, error) {
|
||||
sm.logger.Info("sending ACP session/load request",
|
||||
sm.logger.Info("restoring existing ACP session",
|
||||
zap.String("agent_type", agentConfig.ID()),
|
||||
zap.String("session_id", sessionID))
|
||||
|
||||
@@ -315,12 +315,12 @@ func (sm *SessionManager) loadSession(
|
||||
// classification. The caller still classifies canceled loads as
|
||||
// transport-dead and skips the session/new fallback.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
sm.logger.Warn("ACP session/load aborted by context",
|
||||
sm.logger.Warn("ACP session restoration aborted by context",
|
||||
zap.String("agent_type", agentConfig.ID()),
|
||||
zap.String("session_id", sessionID),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
sm.logger.Error("ACP session/load failed",
|
||||
sm.logger.Error("ACP session restoration failed",
|
||||
zap.String("agent_type", agentConfig.ID()),
|
||||
zap.String("session_id", sessionID),
|
||||
zap.Error(err))
|
||||
@@ -328,7 +328,7 @@ func (sm *SessionManager) loadSession(
|
||||
return "", fmt.Errorf("session/load failed: %w", err)
|
||||
}
|
||||
|
||||
sm.logger.Info("ACP session loaded successfully",
|
||||
sm.logger.Info("ACP session restored successfully",
|
||||
zap.String("agent_type", agentConfig.ID()),
|
||||
zap.String("session_id", sessionID))
|
||||
|
||||
@@ -365,7 +365,7 @@ func (sm *SessionManager) createNewSession(
|
||||
|
||||
// InitializeAndPrompt performs full ACP session initialization and sends the initial prompt.
|
||||
// This offices:
|
||||
// 1. Session initialization (initialize + session/new or session/load)
|
||||
// 1. Session initialization (initialize + creation or restoration through the adapter)
|
||||
// 2. Publishing ACP session created event
|
||||
// 3. Connecting WebSocket streams
|
||||
// 4. Sending the initial task prompt (if provided)
|
||||
|
||||
@@ -86,7 +86,9 @@ Grok ACP currently exposes neither per-turn cost nor subscription quota/reset va
|
||||
|
||||
## ACP Protocol
|
||||
|
||||
JSON-RPC 2.0 over stdin/stdout between agentctl and agent process. Requests: `initialize`, `session/new`, `session/load`, `session/prompt`, `session/cancel`, `session/close`. Notifications: `session/update` with types `message_chunk`, `tool_call`, `tool_update`, `complete`, `error`, `permission_request`, `context_window`.
|
||||
JSON-RPC 2.0 over stdin/stdout between agentctl and agent process. Requests: `initialize`, `session/new`, `session/load`, `session/resume`, `session/prompt`, `session/cancel`, `session/close`. Notifications: `session/update` with types `message_chunk`, `tool_call`, `tool_update`, `complete`, `error`, `permission_request`, `context_window`.
|
||||
|
||||
The adapter prefers advertised `session/resume` for any agent to restore the saved conversation without replaying its history. Both resume and load responses preserve typed configuration and legacy model state. If an agent advertises resume but returns method-not-found, the adapter uses `session/load` only when advertised and the context is still active. Other errors preserve the saved identity. Restore traces contain separate `session.resume` and `session.load` spans for the actual requests.
|
||||
|
||||
### ACP permission identity and injected MCP approval
|
||||
|
||||
|
||||
@@ -439,8 +439,8 @@ func mapToHTTPHeaders(headers map[string]string) []acp.HttpHeader {
|
||||
return hdrs
|
||||
}
|
||||
|
||||
// LoadSession resumes an existing session.
|
||||
// Returns an error if the agent does not support session loading (LoadSession capability).
|
||||
// LoadSession restores an existing session, preferring advertised session/resume
|
||||
// without history replay and otherwise using session/load.
|
||||
// mcpServers are passed to the agent so it can reconnect to MCP servers on the new
|
||||
// agentctl instance (critical for agents that receive MCP configs via the protocol).
|
||||
//
|
||||
@@ -453,15 +453,14 @@ func (a *Adapter) LoadSession(ctx context.Context, sessionID string, mcpServers
|
||||
|
||||
a.mu.Lock()
|
||||
conn := a.acpConn
|
||||
supportsLoad := a.capabilities.LoadSession
|
||||
capabilities := a.capabilities
|
||||
a.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("adapter not initialized")
|
||||
}
|
||||
|
||||
// Check if the agent supports session loading
|
||||
if !supportsLoad {
|
||||
if !capabilities.LoadSession && capabilities.SessionCapabilities.Resume == nil {
|
||||
a.logger.Debug("session/load rejected: agent does not advertise LoadSession capability",
|
||||
zap.String("session_id", sessionID))
|
||||
return fmt.Errorf("agent does not support session loading (LoadSession capability is false)")
|
||||
@@ -481,7 +480,7 @@ func (a *Adapter) LoadSession(ctx context.Context, sessionID string, mcpServers
|
||||
a.mu.Unlock()
|
||||
a.cancelAllAsyncTurnCompletes()
|
||||
|
||||
ctx, span := shared.TraceProtocolRequest(ctx, shared.ProtocolACP, a.agentID, "session.load")
|
||||
ctx, span := shared.TraceProtocolRequest(ctx, shared.ProtocolACP, a.agentID, "session.restore")
|
||||
defer span.End()
|
||||
|
||||
// Filter MCP servers by agent capabilities (same logic as NewSession).
|
||||
@@ -505,7 +504,7 @@ func (a *Adapter) LoadSession(ctx context.Context, sessionID string, mcpServers
|
||||
delete(a.usageBySession, sessionID)
|
||||
a.mu.Unlock()
|
||||
|
||||
resp, err := conn.LoadSession(ctx, acp.LoadSessionRequest{
|
||||
resp, err := a.restoreSessionState(ctx, conn, capabilities, acp.LoadSessionRequest{
|
||||
SessionId: acp.SessionId(sessionID),
|
||||
Cwd: a.cfg.WorkDir,
|
||||
McpServers: toACPMcpServers(filteredServers),
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/coder/acp-go-sdk"
|
||||
"github.com/kandev/kandev/internal/agentctl/server/adapter/transport/shared"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// restoreSessionState uses advertised session/resume to restore the provider
|
||||
// conversation without transferring history that Kandev already stores.
|
||||
func (a *Adapter) restoreSessionState(
|
||||
ctx context.Context,
|
||||
conn *acp.ClientSideConnection,
|
||||
capabilities acp.AgentCapabilities,
|
||||
req acp.LoadSessionRequest,
|
||||
) (acp.LoadSessionResponse, error) {
|
||||
if capabilities.SessionCapabilities.Resume == nil {
|
||||
return a.loadSessionWithReplay(ctx, conn, req)
|
||||
}
|
||||
a.logger.Info("resuming session without history replay", zap.String("session_id", string(req.SessionId)))
|
||||
resumeCtx, span := shared.TraceProtocolRequest(ctx, shared.ProtocolACP, a.agentID, "session.resume")
|
||||
span.SetAttributes(attribute.String("session_id", string(req.SessionId)))
|
||||
resp, err := conn.ResumeSession(resumeCtx, acp.ResumeSessionRequest{
|
||||
SessionId: req.SessionId,
|
||||
Cwd: req.Cwd,
|
||||
McpServers: req.McpServers,
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
var requestErr *acp.RequestError
|
||||
if ctx.Err() == nil && capabilities.LoadSession && errors.As(err, &requestErr) && requestErr.Code == -32601 {
|
||||
a.logger.Warn("advertised session/resume unsupported, loading session history",
|
||||
zap.String("session_id", string(req.SessionId)))
|
||||
return a.loadSessionWithReplay(ctx, conn, req)
|
||||
}
|
||||
return acp.LoadSessionResponse(resp), err
|
||||
}
|
||||
|
||||
// loadSessionWithReplay traces the history transfer separately from a preceding resume attempt.
|
||||
func (a *Adapter) loadSessionWithReplay(
|
||||
ctx context.Context,
|
||||
conn *acp.ClientSideConnection,
|
||||
req acp.LoadSessionRequest,
|
||||
) (acp.LoadSessionResponse, error) {
|
||||
ctx, span := shared.TraceProtocolRequest(ctx, shared.ProtocolACP, a.agentID, "session.load")
|
||||
defer span.End()
|
||||
span.SetAttributes(attribute.String("session_id", string(req.SessionId)))
|
||||
resp, err := conn.LoadSession(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
acpsdk "github.com/coder/acp-go-sdk"
|
||||
acpclient "github.com/kandev/kandev/internal/agentctl/server/acp"
|
||||
"github.com/kandev/kandev/internal/agentctl/types"
|
||||
"github.com/kandev/kandev/internal/agentctl/types/streams"
|
||||
)
|
||||
|
||||
type sessionResumeAgent struct {
|
||||
sessionRequestCaptureAgent
|
||||
resumeRequest acpsdk.ResumeSessionRequest
|
||||
resumeError error
|
||||
resumeResponse *acpsdk.ResumeSessionResponse
|
||||
resumeCalls int
|
||||
loadResponse acpsdk.LoadSessionResponse
|
||||
}
|
||||
|
||||
func (a *sessionResumeAgent) LoadSession(ctx context.Context, req acpsdk.LoadSessionRequest) (acpsdk.LoadSessionResponse, error) {
|
||||
_, err := a.sessionRequestCaptureAgent.LoadSession(ctx, req)
|
||||
return a.loadResponse, err
|
||||
}
|
||||
|
||||
func (a *sessionResumeAgent) ResumeSession(_ context.Context, req acpsdk.ResumeSessionRequest) (acpsdk.ResumeSessionResponse, error) {
|
||||
a.resumeCalls++
|
||||
a.resumeRequest = req
|
||||
if a.resumeResponse != nil {
|
||||
return *a.resumeResponse, a.resumeError
|
||||
}
|
||||
var response acpsdk.ResumeSessionResponse
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"modes":{"currentModeId":"code","availableModes":[{"id":"code","name":"Code"}]},
|
||||
"configOptions":[{"type":"select","id":"model","name":"Model","category":"model",
|
||||
"currentValue":"saved-model","options":[{"value":"saved-model","name":"Saved model"}]}],
|
||||
"_meta":{"restored":true}
|
||||
}`), &response); err != nil {
|
||||
return response, err
|
||||
}
|
||||
return response, a.resumeError
|
||||
}
|
||||
|
||||
func newSessionResumeAdapter(t *testing.T, load, resume bool) (*Adapter, *sessionResumeAgent) {
|
||||
t.Helper()
|
||||
toAgent, fromClient := io.Pipe()
|
||||
t.Cleanup(func() { _ = toAgent.Close(); _ = fromClient.Close() })
|
||||
toClient, fromAgent := io.Pipe()
|
||||
t.Cleanup(func() { _ = toClient.Close(); _ = fromAgent.Close() })
|
||||
fake := &sessionResumeAgent{}
|
||||
conn := acpsdk.NewClientSideConnection(acpclient.NewClient(), fromClient, toClient)
|
||||
_ = acpsdk.NewAgentSideConnection(fake, fromAgent, toAgent)
|
||||
a := newTestAdapter()
|
||||
t.Cleanup(func() { _ = a.Close() })
|
||||
a.agentID = codexAgentID
|
||||
a.dialect = newACPDialect(codexAgentID)
|
||||
a.acpConn = conn
|
||||
a.cfg.WorkDir = t.TempDir()
|
||||
a.capabilities.LoadSession = load
|
||||
a.capabilities.McpCapabilities = acpsdk.McpCapabilities{Http: true, Sse: true}
|
||||
if resume {
|
||||
a.capabilities.SessionCapabilities.Resume = &acpsdk.SessionResumeCapabilities{}
|
||||
}
|
||||
return a, fake
|
||||
}
|
||||
|
||||
func TestLoadSessionPrefersAdvertisedResume(t *testing.T) {
|
||||
for _, load := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("load_supported_%t", load), func(t *testing.T) {
|
||||
a, fake := newSessionResumeAdapter(t, load, true)
|
||||
servers := []types.McpServer{
|
||||
{Name: "kandev", Type: "http", URL: "http://localhost:10005/mcp"},
|
||||
{Name: "kandev", Type: "sse", URL: "http://localhost:10005/sse"},
|
||||
}
|
||||
if err := a.LoadSession(t.Context(), "saved-session", servers); err != nil {
|
||||
t.Fatalf("LoadSession: %v", err)
|
||||
}
|
||||
if fake.resumeCalls != 1 || fake.loadRequest.SessionId != "" || fake.sessionCounter != 0 {
|
||||
t.Fatalf("want one resume without replay or replacement, got resume=%d load=%q new=%d",
|
||||
fake.resumeCalls, fake.loadRequest.SessionId, fake.sessionCounter)
|
||||
}
|
||||
if fake.resumeRequest.SessionId != "saved-session" || fake.resumeRequest.Cwd != a.cfg.WorkDir {
|
||||
t.Fatalf("resume lost session identity or cwd: %+v", fake.resumeRequest)
|
||||
}
|
||||
assertCapturedKandevTransport(t, fake.resumeRequest.McpServers, "http")
|
||||
if a.GetSessionID() != "saved-session" || a.isLoadingSession {
|
||||
t.Fatal("resume did not settle the saved session")
|
||||
}
|
||||
events := drainEvents(a)
|
||||
models := findSessionModelsEvent(t, events)
|
||||
if models.CurrentModelID != "saved-model" || len(models.ConfigOptions) != 1 {
|
||||
t.Fatalf("resume lost model/config state: %+v", models)
|
||||
}
|
||||
if state := a.GetSessionModelState(); state == nil || state.CurrentModelID != "saved-model" {
|
||||
t.Fatalf("cached model state = %+v", state)
|
||||
}
|
||||
var resumed, mode bool
|
||||
for _, event := range events {
|
||||
resumed = resumed || event.SessionStatus == streams.SessionStatusResumed
|
||||
mode = mode || event.CurrentModeID == "code"
|
||||
}
|
||||
if !resumed || !mode {
|
||||
t.Fatalf("resume events missing: resumed=%t mode=%t", resumed, mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSessionFallsBackToReplayOnlyWhenResumeUnsupported(t *testing.T) {
|
||||
for _, advertised := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("resume_advertised_%t", advertised), func(t *testing.T) {
|
||||
a, fake := newSessionResumeAdapter(t, true, advertised)
|
||||
fake.resumeError = acpsdk.NewMethodNotFound(acpsdk.AgentMethodSessionResume)
|
||||
fake.loadResponse.LegacyModels = &acpsdk.LegacyModels{
|
||||
CurrentModelId: "legacy-model",
|
||||
AvailableModels: []acpsdk.LegacyModelInfo{{ModelId: "legacy-model", Name: "Legacy model"}},
|
||||
}
|
||||
if err := a.LoadSession(t.Context(), "saved-session", nil); err != nil {
|
||||
t.Fatalf("LoadSession: %v", err)
|
||||
}
|
||||
if fake.loadRequest.SessionId != "saved-session" || fake.sessionCounter != 0 {
|
||||
t.Fatalf("want replay of saved session, got load=%q new=%d", fake.loadRequest.SessionId, fake.sessionCounter)
|
||||
}
|
||||
if advertised != (fake.resumeCalls == 1) {
|
||||
t.Fatalf("resume calls=%d advertised=%t", fake.resumeCalls, advertised)
|
||||
}
|
||||
if event := findSessionModelsEvent(t, drainEvents(a)); event.CurrentModelID != "legacy-model" {
|
||||
t.Fatalf("load fallback lost legacy model state: %+v", event)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSessionResumesLegacyModelsAcrossAgents(t *testing.T) {
|
||||
for _, agentID := range []string{"auggie", claudeAgentID, "unknown-agent"} {
|
||||
t.Run(agentID, func(t *testing.T) {
|
||||
a, fake := newSessionResumeAdapter(t, true, true)
|
||||
a.agentID = agentID
|
||||
a.dialect = newACPDialect(agentID)
|
||||
fake.resumeResponse = &acpsdk.ResumeSessionResponse{}
|
||||
if err := json.Unmarshal([]byte(`{"models":{
|
||||
"currentModelId":"legacy-model",
|
||||
"availableModels":[{"modelId":"legacy-model","name":"Legacy model"}]
|
||||
}}`), fake.resumeResponse); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.LoadSession(t.Context(), "saved-session", nil); err != nil {
|
||||
t.Fatalf("LoadSession: %v", err)
|
||||
}
|
||||
if fake.resumeCalls != 1 || fake.loadRequest.SessionId != "" || fake.sessionCounter != 0 {
|
||||
t.Fatalf("want resume without replay, got resume=%d load=%q new=%d",
|
||||
fake.resumeCalls, fake.loadRequest.SessionId, fake.sessionCounter)
|
||||
}
|
||||
models := findSessionModelsEvent(t, drainEvents(a))
|
||||
if models.CurrentModelID != "legacy-model" || len(models.SessionModels) != 1 {
|
||||
t.Fatalf("resume lost legacy model state: %+v", models)
|
||||
}
|
||||
if state := a.GetSessionModelState(); state == nil || len(state.Models) != 1 || state.Models[0].ModelID != "legacy-model" {
|
||||
t.Fatalf("cached model state = %+v", state)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSessionDoesNotLoadWhenReplayUnsupported(t *testing.T) {
|
||||
a, fake := newSessionResumeAdapter(t, false, true)
|
||||
a.dialect = newACPDialect("auggie")
|
||||
a.agentID = "auggie"
|
||||
a.sessionID = "prior-session"
|
||||
fake.resumeError = acpsdk.NewMethodNotFound(acpsdk.AgentMethodSessionResume)
|
||||
if err := a.LoadSession(t.Context(), "saved-session", nil); err == nil {
|
||||
t.Fatal("expected unsupported session loading")
|
||||
}
|
||||
if fake.resumeCalls != 1 || fake.loadRequest.SessionId != "" || fake.sessionCounter != 0 {
|
||||
t.Fatal("failed resume retried an unsupported method")
|
||||
}
|
||||
if a.GetSessionID() != "prior-session" || a.isLoadingSession {
|
||||
t.Fatal("failed resume changed identity or retained replay suppression")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSessionResumeFailurePreservesIdentity(t *testing.T) {
|
||||
for _, cause := range []error{
|
||||
acpsdk.NewInternalError(map[string]any{"error": "context deadline exceeded"}),
|
||||
acpsdk.NewAuthRequired(nil),
|
||||
acpsdk.NewRequestCancelled(nil),
|
||||
} {
|
||||
t.Run(cause.Error(), func(t *testing.T) {
|
||||
a, fake := newSessionResumeAdapter(t, true, true)
|
||||
fake.resumeError = cause
|
||||
a.sessionID = "prior-session"
|
||||
if err := a.LoadSession(t.Context(), "saved-session", nil); err == nil {
|
||||
t.Fatal("expected resume error")
|
||||
}
|
||||
if fake.resumeCalls != 1 || fake.loadRequest.SessionId != "" || fake.sessionCounter != 0 {
|
||||
t.Fatalf("inconclusive error retried: resume=%d load=%q new=%d",
|
||||
fake.resumeCalls, fake.loadRequest.SessionId, fake.sessionCounter)
|
||||
}
|
||||
if a.GetSessionID() != "prior-session" || a.isLoadingSession {
|
||||
t.Fatal("failed resume changed identity or retained replay suppression")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user