Skip to content

feat(harness): migrate runtime suspension to DATA snapshots - #3165

Open
supreme-gg-gg wants to merge 12 commits into
mainfrom
jetc/feat/harness-approval
Open

supreme-gg-gg wants to merge 12 commits into
mainfrom
jetc/feat/harness-approval

Conversation

@supreme-gg-gg

@supreme-gg-gg supreme-gg-gg commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Move runtime suspension from memory snapshots to DATA snapshots for HITL events like approvals and questions.

  • Persist pending input, task ownership, and trace origins across restore.
  • Resume Claude’s native deferred calls (using deferred tool call hooks); execute approved Codex MCP calls in the harness driver and continue with saved results (using client-executed tool flow)
  • Cleanups such as removing Codex’s legacy in-memory approval path. Preserve retries before execution and reject abandoned continuations with a new-session error.
  • Adapt runtime telemetry to durable continuation: link resumed invocations to the original invocation and record deferred tool attempts without duplicating public tool events or request counts.

Motivations:

  • Pause uses node-local non-durable storage, which means approval pending states will be lost if the node is replaced
  • Substrate Lifecycle V2 has already removed Pause API, and the new Suspend / Resume API currently does not allow us to switch between Full vs Data anymore based on agent execution states (terminal vs HITL), which is required for fork
  • Full / Memory snapshots are less portable (especially in Lifecycle V2) than Data / Volume snapshots, and larger is size

Limitations:

  • Parallel approval batching is unsupported. For Claude this is a documented upstream limitation, for Codex this could be a follow-up, but initial testing shows this is hard to support. However, these harnesses usually will just make calls / retry sequentially. The long term solution would be getting Codex to support durable approval in its native flow [Codex App Server] Support durable input requests across restarts openai/codex#42622
  • Codex resumes in a new native turn; the original interrupted call remains in history, so the model may request it again. Protected MCP results are supplied as JSON text (multimedia responses will be ignored).
  • Tool-parent bookkeeping is not retained across suspension. Other still-open tool spans are settled as unfinished; cross-boundary subagent nesting is not restored. Invocation links preserve task continuity. Deferred span durations include harness setup overhead.
  • Failed deferred execution or Claude cancellation cleanup can require a new session. Remote effects cannot be rolled back; broader recovery and reconciliation remain follow-up work.

@supreme-gg-gg supreme-gg-gg changed the title [WIP] feat: switch harness to use data snapshots by implementing durable approval [WIP] feat: switch agents to use data snapshots by implementing durable approval in harness Oct 12, 2026
@blacksmith-sh

This comment has been minimized.

@supreme-gg-gg
supreme-gg-gg force-pushed the jetc/feat/harness-approval branch from a920ed4 to 3666151 Compare October 12, 2026 21:26
@supreme-gg-gg

Copy link
Copy Markdown
Contributor Author

Simple demo with tracing

Screen.Recording.2026-10-12.at.5.40.35.PM.mov

@supreme-gg-gg supreme-gg-gg changed the title [WIP] feat: switch agents to use data snapshots by implementing durable approval in harness [WIP] feat(harness): migrate runtime suspension to DATA snapshots Oct 12, 2026
@supreme-gg-gg
supreme-gg-gg force-pushed the jetc/feat/harness-approval branch from 8e02d7a to f946b63 Compare October 13, 2026 15:47
@supreme-gg-gg supreme-gg-gg changed the title [WIP] feat(harness): migrate runtime suspension to DATA snapshots feat(harness): migrate runtime suspension to DATA snapshots Oct 13, 2026
@github-actions github-actions Bot added the enhancement New feature or request label Oct 13, 2026
@supreme-gg-gg
supreme-gg-gg force-pushed the jetc/feat/harness-approval branch from f946b63 to 0f54e08 Compare October 13, 2026 15:59
@supreme-gg-gg
supreme-gg-gg marked this pull request as ready for review October 13, 2026 16:13
@supreme-gg-gg
supreme-gg-gg requested a review from a team as a code owner October 13, 2026 16:13
@supreme-gg-gg
supreme-gg-gg force-pushed the jetc/feat/harness-approval branch from 0f54e08 to d6293e6 Compare October 13, 2026 20:04

@EItanya EItanya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review.

if err := json.Unmarshal(message.Params, &params); err != nil {
return nil, false, fmt.Errorf("decode Codex MCP approval request: %w", err)
}
if params.ThreadID != translator.threadID || params.TurnID != nil && *params.TurnID != translator.turnID {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep handling requests from known spawned threads here? Shared-agent callbacks use the child's thread and turn, so this check rejects even a single protected MCP approval and consume fails the parent A2A task. approvalTool already matches these calls by thread.

If durable child continuations are out of scope, can we explicitly decline that callback instead of aborting the parent, and document the restriction?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think child continuations are out of scope and I'll decline them here. I believe that our compiler doesn't even allow tools in subagents right now. I think supporting them would be somewhat difficult since there's no client-executed tool flow for subagents (might only be doable with the native approval flow, which means we need FULL snapshot...), but either way, it should definitely be out of scope for this PR.

}
if r.Phase == phaseResult {
if decision, ok := response.(*runtime.ApprovalDecision); ok && !reflect.DeepEqual(r.Decision, decision) {
return runtime.Outcome{}, fmt.Errorf("Codex approval decision changed after execution")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns before the abandonment defer is installed. If a tool result is cached, native startup fails before dispatch, and the user changes their decision on the republished request, the executor marks the task FAILED but the journal stays in result. ReservedTaskID() then restores that failed task as pending and blocks new tasks.

There's a similar path in Claude's response handling: an empty answer selection passes public validation but fails journal validation before cleanup is installed, leaving the failed task in waiting.

Can we reject these responses before starting the task, or keep it input-required? If we return a terminal failure, the journal needs to be marked abandoned on these paths too.

} else {
err = w.Pause(runtimeCtx, work.Session)
}
snapshot, err := w.Quiesce(runtimeCtx, work.Session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add E2E coverage that waits for a verified DATA snapshot and suspended Actor before answering pending input? The approval helper replies immediately after INPUT_REQUIRED, so it can supersede unclaimed quiescence and pass without replacing the harness. The ask-user suspension test also skips Claude and Codex.

Approval, denial, cancellation, and native ask-user responses should exercise that restore boundary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extended E2E coverage

scopes again. A Data snapshot resumes by starting fresh containers from the OCI
image with the saved durable directories; Substrate still restores one as Data when
the Actor's template changed after the snapshot was taken.
Agent and standalone sandbox templates use DATA. Resume starts fresh containers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small docs correction: can we also update the automatic-quiescence section and its sequence diagram? They still say waiting tasks pause on-node while only terminal tasks suspend externally, which contradicts the DATA behavior described here.

b.mu.Lock()
defer b.mu.Unlock()
r, err := loadDeferred(b.directory, int(b.maxBody))
if err == nil && r != nil && r.Phase == "executing" && input != nil && b.protects(input.ToolName) && input.ToolUseID == r.Tool.ID && input.ToolName == r.Tool.Name {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we handle these errors explicitly and log the underlying cause? A journal read or write failure currently falls through to the same denial as an unmatched request, including the suggestion to retry as a single tool call. handleHook also discards the error from resolveHook.

Early error branches would make this easier to follow and distinguish persistence failures from unsupported calls. We should keep the successful HTTP denial response that Claude's hook protocol requires.

return saveDeferred(b.directory, d.config.MaxEventBytes, r)
}

func (p *deferredTurn) Resume(ctx context.Context, response runtime.InputResponse, sink runtime.EventSink) (outcome runtime.Outcome, returnErr error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional cleanup: can we extract the opening lock/load/check/save block into a named transition helper that owns its lock? Resume and Cancel currently mix journal transitions, native execution, and deferred cleanup, which makes it hard to follow which errors leave the input retryable and which abandon it.

A scoped lock would allow early returns without the repeated err == nil checks, while keeping process execution outside the lock. Private phase constants, like the Codex driver uses, would also make the transitions easier to trace.

Comment thread go/harness/runtime/a2a/executor.go Outdated
}

func yieldCancellation(ctx context.Context, reqCtx *a2asrv.ExecutorContext, done <-chan struct{}, yield func(a2atype.Event, error) bool) {
func yieldCancellation(ctx context.Context, reqCtx *a2asrv.ExecutorContext, done <-chan struct{}, knownErr error, yield func(a2atype.Event, error) bool) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small cleanup: do we still need done in this helper? All three callers already know it's closed: the active/canceling paths have received from it, and the parked path closes it before calling here.

We can keep that synchronization at the callers, where it needs to happen before reading cancelErr/err, and have this helper emit the resolved result. That removes the second wait while keeping request-context cancellation handling explicit.

directory := t.TempDir()
executable := filepath.Join(directory, "codex")
capture := filepath.Join(directory, "requests")
script := `#!/bin/sh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional test cleanup: can we share this fake App Server setup with TestDeferredTurnExitsBeforePublishingPendingAndReusesSavedResult? Most of the handshake, approval, interruption, and continuation script is duplicated, so protocol changes need updating in both places and the actual test scenarios are harder to see.

A shared fixture could keep the assertions here. We'd still want to preserve the different interrupt/completion orderings and the cached-result test's graceful-exit check.

Jacob953 added a commit to Jacob953/kagent that referenced this pull request Oct 14, 2026
Verify with pinned DSH that session resume records an interrupted tool outcome without restoring its pending approval or running the command. Document the difference between this safety regression and durable approval continuation, and clarify adoption conditions for kagent kagent-dev#3010, kagent-dev#3011 and kagent-dev#3165.

AI assistance: implementation and validation assisted by OpenAI Codex.

Signed-off-by: Junfeng Yu <64075563+Jacob953@users.noreply.github.com>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
@supreme-gg-gg
supreme-gg-gg force-pushed the jetc/feat/harness-approval branch from d6293e6 to b74541f Compare October 14, 2026 16:03
@blacksmith-sh

This comment has been minimized.

Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
@supreme-gg-gg
supreme-gg-gg requested a review from EItanya October 14, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants