Repository navigation
Conversation
| } | ||
| return err == nil, err | ||
| })) | ||
| _, err = fixture.sessions.ResumeSession(fixture.ctx, &apiv1.ResumeSessionRequest{SessionId: fixture.sessionID}) |
There was a problem hiding this comment.
Once #3409 lands first, SuspendSession, ResumeSession, and their request messages are gone, so this E2E package no longer compiles. Can we keep the automatic snapshot established above and wake the Actor through a normal A2A interaction before inducing worker loss? That exercises the lifecycle available after the rebase while preserving the healthy-runtime rejection, Actor identity, history, and post-revert interaction checks.
There was a problem hiding this comment.
Done in the rebase onto #3409 (08019da1). The E2E no longer calls Suspend or Resume. It keeps the automatic snapshot and holds a second A2A turn open at the model, so the Actor is running when its worker is deleted. It also keeps the checks for healthy-runtime rejection, Actor UID, retained history and the follow-up turn. f0251704 additionally checks the refusal reason on the healthy revert, and on a revert retried after recovery. On a kind cluster with gVisor workers, it passes for all four harnesses:
--- PASS: TestSessionRevertCrashedRuntime (89.75s)
--- PASS: TestSessionRevertCrashedRuntime/kagent (23.78s)
--- PASS: TestSessionRevertCrashedRuntime/codex (26.63s)
--- PASS: TestSessionRevertCrashedRuntime/claude (21.71s)
--- PASS: TestSessionRevertCrashedRuntime/byo-adk (17.64s)
bc2b0f4 to
dbd3b00
Compare
0a916a7 to
c55f3cf
Compare
dbd3b00 to
aac74f8
Compare
c55f3cf to
489dcb0
Compare
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
aac74f8 to
dfc3bcf
Compare
489dcb0 to
08019da
Compare
jjamroga
left a comment
There was a problem hiding this comment.
The RPC itself makes sense to me. After #3409 there's no other way to say "restore a confirmed-crashed runtime," and an explicit action beats overloading an existing one. Auth, validation, the handler and generated code all look good (make proto-generate is clean). My questions are mostly about status codes, since docs/lifecycle-retries.md turns them into client retry behavior.
|
|
||
| Retry an interrupted revert against the same Session ID. Pending recovery retains its operation across API restarts. | ||
| The lifecycle worker retries already admitted recoveries; it does not initiate recovery for other crashed Sessions. | ||
| After recovery succeeds, another revert returns `FAILED_PRECONDITION` because the Actor is healthy. |
There was a problem hiding this comment.
Telling "already recovered" apart from "can't recover": if the UNAVAILABLE → FAILED_PRECONDITION change on service.go:298 lands, FAILED_PRECONDITION means two different things here: "the Actor is healthy" (including a retry after a revert whose response got lost) and "the Actor is gone/replaced, nothing to recover." The first is basically success for the client, the second isn't. Should this say to check GetSession (READY with no Failure = done), or would distinct messages/details be better?
There was a problem hiding this comment.
I went with distinct details. Checking GetSession cannot separate the two cases: a Session whose Actor was replaced is also READY with no Failure. Each refusal in f0251704 now carries a google.rpc.ErrorInfo in domain kagent.dev. KAGENT_SESSION_RUNTIME_NOT_CRASHED covers a healthy or suspended runtime, including a retry after a lost success response. KAGENT_SESSION_RUNTIME_UNRECOVERABLE covers a missing or replaced Actor. docs/lifecycle-retries.md gives the client action for each, so clients need not parse the message. On the cluster, a revert retried after a successful recovery returns:
step=crashed-actor code=OK state=RUNTIME_STATE_READY
step=retry-after-recovery code=FailedPrecondition reasons=[kagent.dev/KAGENT_SESSION_RUNTIME_NOT_CRASHED]
| if errors.Is(err, database.ErrConflict) { | ||
| return nil, serviceerrors.NewAborted(err.Error(), err) | ||
| } |
There was a problem hiding this comment.
Small one: ActorWorkflow.Revert returns ErrConflict for anything that isn't READY/NONE (recovery.go:61-63), which lands here as ABORTED. That includes a Session suspended by scheduled-run deadline cleanup, where nothing is in progress, so retrying never converges. FAILED_PRECONDITION seems like a better fit there, keeping ABORTED for real contention.
There was a problem hiding this comment.
Good catch, fixed in f0251704. Revert now returns ABORTED only while a lifecycle operation is active, which a retry outlasts. A Session at rest outside READY returns FAILED_PRECONDITION with KAGENT_SESSION_RUNTIME_NOT_CRASHED. That includes one suspended by deadline cleanup, which waits for an interaction to resume it. TestExplicitRevertSeparatesContentionFromRestingState pins a SUSPENDED Session at rest as a precondition failure, and suspension, resume and creation in progress as conflicts:
--- PASS: TestExplicitRevertSeparatesContentionFromRestingState/suspended_by_deadline_cleanup
--- PASS: TestExplicitRevertSeparatesContentionFromRestingState/suspension_in_progress
--- PASS: TestExplicitRevertSeparatesContentionFromRestingState/resume_in_progress
--- PASS: TestExplicitRevertSeparatesContentionFromRestingState/creation_in_progress
| return nil, serviceerrors.NewAborted(err.Error(), err) | ||
| } | ||
| if err != nil { | ||
| return nil, serviceerrors.NewUnavailable("Failed to revert Session", err) |
There was a problem hiding this comment.
+1, and the tests now lock this in: session_recovery_test.go:151 and :155 expect Unavailable for a same-name replacement and a missing Actor. The architecture doc says these "cannot authorize a revert" (runtime-and-lifecycle.md:212), but the errors table tells clients to retry UNAVAILABLE, so a client following the docs just retries until its deadline.
Should the READY branch in ActorWorkflow.Revert wrap these with ErrFailedPrecondition? Background retries go through the FAILED/RESUME branch, so they're unaffected, and transient Substrate/DB errors still map to UNAVAILABLE. I tried it locally (+11/−1 in recovery.go):
actor, err := w.actors.GetActor(ctx, revision.ActorTemplateAtespace, substrate.ActorName(session.Id))
if status.Code(err) == codes.NotFound {
return nil, fmt.Errorf("runtime actor is missing: %w", database.ErrFailedPrecondition)
}
if err != nil {
return nil, err
}
if !validActorIdentity(actor, revision, substrate.ActorName(session.Id)) {
return nil, fmt.Errorf("runtime actor identity or template changed: %w", database.ErrFailedPrecondition)
}
if err := w.verifyActor(ctx, session, revision, actor); errors.Is(err, database.ErrNotFound) {
return nil, fmt.Errorf("runtime actor was replaced: %w", database.ErrFailedPrecondition)
} else if err != nil {
return nil, err
}With the two assertions flipped to FailedPrecondition, TestRevertSessionThroughGRPC (all 7 cases) and the session service tests pass.
…ce worker 0
Start ran the crash-recovery scan inline on quiescence worker 0, between idle
claims. A slow idle suspension on that worker held every recovery retry for as
long as the suspension took, and a slow revert held up idle work in turn. No
test exercised the scan through Start: the existing tests called
recoverSessionPage directly, and the Start-level store stub returned no
candidates.
The scan now runs in its own goroutine with its own ticker, at the same
recovery interval. The four quiescence workers return to their shape before
recovery existed. The page cursor still carries across ticks, so a backlog
larger than one page cannot starve the Sessions past it.
TestStartRetriesAdmittedRecoveryWhileIdleWorkBlocks admits a recovery whose
first revert fails, then starts the workflow with all four quiescence workers
blocked in idle suspensions. The Session must reach READY while they stay
blocked.
Verified: the new test fails on the previous Start ("Condition never
satisfied") and passes with this change. go test for
./internal/service/..., ./internal/substrate/... and ./internal/database/...
passes against Postgres; golangci-lint reports 0 issues.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
… the claim Crash recovery had its own copy of execute: claim, GetActor, verify, revert, finish. It claimed before its reads, and a released claim keeps executor_id. One failed GetActor therefore made Delete refuse the Session with "begin Session operation: resource conflict" until recovery succeeded. A deleted Actor made that permanent. Execute also sent every operation on a crashed Session into the recovery copy, so even a Delete that superseded the recovery would have returned a conflict. Recovery now runs through execute like suspend, resume and delete. substrate.PrepareActorRevert does the read-only checks before the claim, and ApplyActorTransition issues RevertActor, which moves onto LifecycleClient. The checks are unchanged: the Actor keeps its UID, ends SUSPENDED, and restores the durable snapshot observed before the revert. A SUSPENDED Actor still completes a recovery whose revert response was lost. A failed preparation keeps the RESUME intent instead of clearing it, because admission already failed the interrupted turn. Nothing was issued, so Delete can supersede it. Rejected: keeping the separate path and only moving its reads above the claim. That fixes the stuck Delete, but fixes to execute would still have to land twice. Verified: TestDeleteSupersedesUnissuedCrashRecovery covers an unreadable and a missing Actor; both fail on the previous code with "resource conflict" and pass here. TestActorRevertRestoresTheObservedSnapshotInPlace covers the revert transition's states and its UID and snapshot checks. go test for ./internal/service/..., ./internal/substrate/... and ./internal/database/... passes against Postgres; golangci-lint reports 0 issues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
…t UNAVAILABLE RevertSession reported a missing or same-name-replaced Actor as `UNAVAILABLE`. The retry guide tells clients to retry that code, so a client that followed it retried a runtime no revert can restore until its deadline. These refusals now return `FAILED_PRECONDITION`. Transient Substrate and database failures still return `UNAVAILABLE`. `FAILED_PRECONDITION` then meant two opposite things: the runtime is already healthy, which includes a retry after a lost success response, or it can never be recovered. A `google.rpc.ErrorInfo` detail in domain `kagent.dev` now names `KAGENT_SESSION_RUNTIME_NOT_CRASHED` or `KAGENT_SESSION_RUNTIME_UNRECOVERABLE`, and the retry guide gives the client action for each. Telling clients to read `GetSession` lost: a Session whose Actor was replaced is also READY with no failure, so the Session alone cannot separate the two. A revert of a Session at rest outside READY returned `ABORTED`. A Session suspended by deadline cleanup stays suspended until an interaction resumes it, so the retry `ABORTED` invites never converged. Only an active lifecycle operation is now contention; a resting Session returns `KAGENT_SESSION_RUNTIME_NOT_CRASHED`. The admitted-recovery retry path is unchanged: it enters through the FAILED/RESUME branch, which none of these checks reach. Verified: the session and grpcserver suites pass against PostgreSQL, apart from TestSandboxTemplateCatalog, which needs envtest binaries. That includes TestRevertSessionThroughGRPC, which now pins the code and reason for a healthy, replaced, missing and already-recovered runtime. golangci-lint reports 0 issues and gofmt is clean. On a kind cluster with gVisor workers, the worker-loss revert E2E passes for all four harnesses. On the same cluster, a revert after the Actor was deleted, or replaced under the same name, returned `UNAVAILABLE` before this change and now returns `FAILED_PRECONDITION` with `KAGENT_SESSION_RUNTIME_UNRECOVERABLE`. A revert retried after recovery returns `KAGENT_SESSION_RUNTIME_NOT_CRASHED`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <ada.lundhe@solo.io>
…ery-workflow Bring 05cfb0c into the revert RPC branch. That commit moves crash recovery into `execute` through `substrate.PrepareActorRevert` and `ApplyActorTransition`, so a failed Actor read leaves no claim and Delete can still supersede the recovery. Conflicts resolved: - `go/core/internal/service/session/recovery.go` imports: the workflow branch dropped `time`, `uuid` and `proto` with `recoverCrashedSession`. Keep `codes` and `status`, which `Revert` uses to report a missing Actor as FAILED_PRECONDITION. `proto` has no remaining user here. `Revert` merged without a textual conflict. It keeps this branch's refusals: FAILED_PRECONDITION with `errRuntimeNotCrashed` or `errRuntimeUnrecoverable`, and ABORTED only for an active lifecycle operation. The admitted operation then runs through the workflow branch's `execute` path. Verified on linux/amd64 with Go 1.27.3: `go build` and `go vet` over `./core/...` and `./api/...`, pinned `gofmt` on the touched files, `make lint` on the touched packages, and `go test` over the service, substrate, database, grpcserver and mcp packages against Postgres testcontainers. Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y-workflow Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Main renamed SnapshotStorage.status to state (SnapshotStorageState). Update the recovery and substrate snapshot tests to the new field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Resolve the service.go import conflict by keeping both the status/errdetails imports this PR needs and the proto import the base added. Regenerate sessions.pb.go and sessions_pb.ts from the merged sessions.proto instead of hand-merging generated output. Follow the base's API changes in this PR's tests: SnapshotStorage now reports State instead of Status, and Session.agent is an AgentProvenance whose namespace lives under ref. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
…ce worker 0
Start ran the crash-recovery scan inline on quiescence worker 0, between idle
claims. A slow idle suspension on that worker held every recovery retry for as
long as the suspension took, and a slow revert held up idle work in turn. No
test exercised the scan through Start: the existing tests called
recoverSessionPage directly, and the Start-level store stub returned no
candidates.
The scan now runs in its own goroutine with its own ticker, at the same
recovery interval. The four quiescence workers return to their shape before
recovery existed. The page cursor still carries across ticks, so a backlog
larger than one page cannot starve the Sessions past it.
TestStartRetriesAdmittedRecoveryWhileIdleWorkBlocks admits a recovery whose
first revert fails, then starts the workflow with all four quiescence workers
blocked in idle suspensions. The Session must reach READY while they stay
blocked.
Verified: the new test fails on the previous Start ("Condition never
satisfied") and passes with this change. go test for
./internal/service/..., ./internal/substrate/... and ./internal/database/...
passes against Postgres; golangci-lint reports 0 issues.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
… the claim Crash recovery had its own copy of execute: claim, GetActor, verify, revert, finish. It claimed before its reads, and a released claim keeps executor_id. One failed GetActor therefore made Delete refuse the Session with "begin Session operation: resource conflict" until recovery succeeded. A deleted Actor made that permanent. Execute also sent every operation on a crashed Session into the recovery copy, so even a Delete that superseded the recovery would have returned a conflict. Recovery now runs through execute like suspend, resume and delete. substrate.PrepareActorRevert does the read-only checks before the claim, and ApplyActorTransition issues RevertActor, which moves onto LifecycleClient. The checks are unchanged: the Actor keeps its UID, ends SUSPENDED, and restores the durable snapshot observed before the revert. A SUSPENDED Actor still completes a recovery whose revert response was lost. A failed preparation keeps the RESUME intent instead of clearing it, because admission already failed the interrupted turn. Nothing was issued, so Delete can supersede it. Rejected: keeping the separate path and only moving its reads above the claim. That fixes the stuck Delete, but fixes to execute would still have to land twice. Verified: TestDeleteSupersedesUnissuedCrashRecovery covers an unreadable and a missing Actor; both fail on the previous code with "resource conflict" and pass here. TestActorRevertRestoresTheObservedSnapshotInPlace covers the revert transition's states and its UID and snapshot checks. go test for ./internal/service/..., ./internal/substrate/... and ./internal/database/... passes against Postgres; golangci-lint reports 0 issues. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
Main renamed SnapshotStorage.status to state (SnapshotStorageState). Update the recovery and substrate snapshot tests to the new field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
2e4e461 to
7ee6d63
Compare
7ee6d63 to
72d68a3
Compare
…-workflow Merge the rebased fix/session-recovery-workflow, which now sits on main with the Substrate v1.0.0-rc2 upgrade. recovery.go and recovery_test.go conflicted as add/add because both sides added them after the rebase. Resolved with a three-way merge using the pre-rebase fix/session-recovery-workflow (2e4e461) as the base; the rebase left both files unchanged, so this branch's versions stand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ada Lundhe <adalundhe@lundhe.audio>
5916c92 to
965013c
Compare
72d68a3 to
9f76a9a
Compare
Summary
Expose
SessionService.RevertSessionthrough the public API and generated clients.The service requires Session update access and uses the existing workflow. RPC tests cover ownership, read-only shares, identity checks, durable results and retry outcomes. The worker-loss E2E exercises explicit recovery and a subsequent completed message.
Stack
Split requested in the review discussion.
Part 3 of 6. 15 files, +753/-111 against the immediate parent.
Parts 2–6 form a GitHub stack on
main, so CI runs on each part. A stack cannot include a fork branch, so part 2 targetsmainand also shows part 1's commit until #3255 merges. Parts 3–6 target their immediate parents for a focused diff. The stack is rebased ontomainafter #3409, which removed the public Session suspend and resume controls.Testing
On
mainafter #3409,RevertSessionis the only Session RPC this part adds. Protobuf lint and official generation pass. The worker-loss E2E holds a turn open to keep the Actor running, because only an interaction runs it now; E2E compiles. Session, gRPC and MCP race suites pass at the top of the stack, exceptTestSandboxTemplateCatalog, which needs envtest binaries.Live cluster E2E runs in this PR's CI.
Upstream CI runs on this PR as part of the stack on
main.🤖 Generated with Claude Code