Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
252 lines
8.0 KiB
TypeScript
Executable File
252 lines
8.0 KiB
TypeScript
Executable File
// Replay-shim for the GitHub Copilot agent runtime.
|
|
//
|
|
// Owns three concerns:
|
|
// 1. Pre-call: should this attempt resume an existing SDK session or
|
|
// start a new one? Honours `initialReplayState.sdkSessionId` and
|
|
// `initialReplayState.replayInvalid`.
|
|
// 2. Post-call: if `resumeSession` fails, was the failure recoverable
|
|
// (session-gone) so we should downgrade to `createSession`, or
|
|
// unrecoverable so the error should surface as a prompt error?
|
|
// 3. Result-time: compute the `replayMetadata` to attach to the attempt
|
|
// result, propagating prior state with worst-case-wins semantics so
|
|
// the orchestrator never replays an attempt that may have committed
|
|
// partial side effects.
|
|
//
|
|
// Host back-pointers (NOT imported here to keep the package boundary
|
|
// clean):
|
|
// - `src/agents/pi-embedded-runner/replay-state.ts` — canonical
|
|
// `EmbeddedRunReplayState` / `EmbeddedRunReplayMetadata` shapes
|
|
// and `replayMetadataFromState`.
|
|
// - `src/agents/pi-embedded-runner/run/types.ts` —
|
|
// `AgentHarnessAttemptResult.replayMetadata` field requirement.
|
|
|
|
export type ReplayDecision =
|
|
| {
|
|
readonly action: "resume";
|
|
readonly sdkSessionId: string;
|
|
readonly downgradedFromResume: false;
|
|
}
|
|
| {
|
|
readonly action: "create";
|
|
readonly downgradedFromResume: boolean;
|
|
readonly downgradeReason: "no-replay-state" | "no-sdk-session-id" | "replay-invalid";
|
|
};
|
|
|
|
export interface ReplayShimInput {
|
|
readonly sdkSessionId?: string;
|
|
readonly replayInvalid?: boolean;
|
|
}
|
|
|
|
function normalizeSdkSessionId(value: unknown): string | undefined {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
/**
|
|
* Pure pre-call decision: should attempt.ts call resumeSession or
|
|
* createSession?
|
|
*
|
|
* Rules:
|
|
* - No input → create (no-replay-state)
|
|
* - No (trimmed) sdkSessionId → create (no-sdk-session-id)
|
|
* - sdkSessionId + replayInvalid=true → create (replay-invalid),
|
|
* downgradedFromResume=true
|
|
* - sdkSessionId + replayInvalid=false → resume
|
|
*/
|
|
export function decideReplayAction(input?: ReplayShimInput): ReplayDecision {
|
|
if (!input) {
|
|
return {
|
|
action: "create",
|
|
downgradedFromResume: false,
|
|
downgradeReason: "no-replay-state",
|
|
};
|
|
}
|
|
const sdkSessionId = normalizeSdkSessionId(input.sdkSessionId);
|
|
if (!sdkSessionId) {
|
|
return {
|
|
action: "create",
|
|
downgradedFromResume: false,
|
|
downgradeReason: "no-sdk-session-id",
|
|
};
|
|
}
|
|
if (input.replayInvalid === true) {
|
|
return {
|
|
action: "create",
|
|
downgradedFromResume: true,
|
|
downgradeReason: "replay-invalid",
|
|
};
|
|
}
|
|
return {
|
|
action: "resume",
|
|
sdkSessionId,
|
|
downgradedFromResume: false,
|
|
};
|
|
}
|
|
|
|
export type ResumeFailureKind = "missing" | "unknown";
|
|
|
|
export interface ResumeFailureClassification {
|
|
readonly recoverable: boolean;
|
|
readonly kind: ResumeFailureKind;
|
|
}
|
|
|
|
const MISSING_SESSION_CODES = new Set([
|
|
"SESSION_NOT_FOUND",
|
|
"session_not_found",
|
|
"NotFound",
|
|
"ENOENT",
|
|
]);
|
|
|
|
const MISSING_SESSION_MESSAGE_PATTERNS: readonly RegExp[] = [
|
|
/\bsession not found\b/i,
|
|
/\bsession .* not found\b/i,
|
|
/\bunknown session id\b/i,
|
|
/\bsession id .* (does not exist|not found)\b/i,
|
|
/\bsession .* does not exist\b/i,
|
|
/\bno such session\b/i,
|
|
];
|
|
|
|
function readErrorField(error: unknown, key: string): unknown {
|
|
if (!error || typeof error !== "object") {
|
|
return undefined;
|
|
}
|
|
return (error as Record<string, unknown>)[key];
|
|
}
|
|
|
|
/**
|
|
* Post-call: classify a resumeSession() failure so attempt.ts can
|
|
* decide whether to downgrade silently to createSession.
|
|
*
|
|
* Conservative: only treats clearly session-gone signals as recoverable.
|
|
* Structured signals (status === 404, recognised code strings) are
|
|
* checked first; message matching is a fallback because SDK error
|
|
* messages are not part of the typed contract.
|
|
*
|
|
* Everything else (transport errors, auth failures, generic Error) is
|
|
* unrecoverable and should surface to the outer attempt.ts try/catch
|
|
* which converts it to a prompt error.
|
|
*/
|
|
export function classifyResumeFailure(error: unknown): ResumeFailureClassification {
|
|
if (error === undefined || error === null) {
|
|
return { recoverable: false, kind: "unknown" };
|
|
}
|
|
|
|
const status = readErrorField(error, "status");
|
|
if (status === 404) {
|
|
return { recoverable: true, kind: "missing" };
|
|
}
|
|
const statusCode = readErrorField(error, "statusCode");
|
|
if (statusCode === 404) {
|
|
return { recoverable: true, kind: "missing" };
|
|
}
|
|
|
|
const code = readErrorField(error, "code");
|
|
if (typeof code === "string" && MISSING_SESSION_CODES.has(code)) {
|
|
return { recoverable: true, kind: "missing" };
|
|
}
|
|
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: typeof error === "object"
|
|
? typeof (error as { message?: unknown }).message === "string"
|
|
? (error as { message: string }).message
|
|
: undefined
|
|
: undefined;
|
|
if (typeof message === "string") {
|
|
for (const pattern of MISSING_SESSION_MESSAGE_PATTERNS) {
|
|
if (pattern.test(message)) {
|
|
return { recoverable: true, kind: "missing" };
|
|
}
|
|
}
|
|
}
|
|
|
|
return { recoverable: false, kind: "unknown" };
|
|
}
|
|
|
|
export interface ReplayMetadataComputeInput {
|
|
readonly priorReplayInvalid?: boolean;
|
|
readonly priorHadPotentialSideEffects?: boolean;
|
|
readonly thisAttemptTimedOut?: boolean;
|
|
readonly thisAttemptHadPotentialSideEffects?: boolean;
|
|
readonly thisAttemptDowngradedFromResume?: boolean;
|
|
readonly thisAttemptResumeFailureRecovered?: boolean;
|
|
}
|
|
|
|
export interface ComputedReplayMetadata {
|
|
readonly hadPotentialSideEffects: boolean;
|
|
readonly replaySafe: boolean;
|
|
}
|
|
|
|
/**
|
|
* Compute the `EmbeddedRunReplayMetadata` to attach to the attempt
|
|
* result. Worst-case-wins:
|
|
*
|
|
* hadPotentialSideEffects = priorHadPotentialSideEffects OR timedOut
|
|
* OR thisAttemptHadPotentialSideEffects
|
|
* (timeout means we cannot prove the prompt was not partially
|
|
* committed server-side; treat as side-effecting so the
|
|
* orchestrator will not blindly re-issue the same prompt).
|
|
*
|
|
* replaySafe = NOT (
|
|
* priorReplayInvalid
|
|
* OR thisAttemptDowngradedFromResume
|
|
* OR thisAttemptResumeFailureRecovered
|
|
* OR hadPotentialSideEffects
|
|
* )
|
|
*
|
|
* Matches the parity rule in
|
|
* `src/agents/pi-embedded-runner/replay-state.ts#replayMetadataFromState`.
|
|
*/
|
|
export function computeReplayMetadata(input: ReplayMetadataComputeInput): ComputedReplayMetadata {
|
|
const priorReplayInvalid = input.priorReplayInvalid === true;
|
|
const priorHadPotentialSideEffects = input.priorHadPotentialSideEffects === true;
|
|
const timedOut = input.thisAttemptTimedOut === true;
|
|
const thisAttemptHadPotentialSideEffects = input.thisAttemptHadPotentialSideEffects === true;
|
|
const downgraded = input.thisAttemptDowngradedFromResume === true;
|
|
const recovered = input.thisAttemptResumeFailureRecovered === true;
|
|
const hadPotentialSideEffects =
|
|
priorHadPotentialSideEffects || timedOut || thisAttemptHadPotentialSideEffects;
|
|
const replaySafe = !(priorReplayInvalid || downgraded || recovered || hadPotentialSideEffects);
|
|
return { hadPotentialSideEffects, replaySafe };
|
|
}
|
|
|
|
const COPILOT_REPLAY_SAFE_READ_ONLY_TOOL_NAMES = new Set([
|
|
"get",
|
|
"file_read",
|
|
"glob",
|
|
"grep",
|
|
"inspect",
|
|
"list",
|
|
"ls",
|
|
"memory_get",
|
|
"probe",
|
|
"query",
|
|
"read",
|
|
"search",
|
|
"sessions_history",
|
|
"sessions_list",
|
|
"status",
|
|
"tool_search",
|
|
"update_plan",
|
|
"view",
|
|
"web_fetch",
|
|
"web_search",
|
|
]);
|
|
|
|
export function copilotToolMetasHavePotentialSideEffects(
|
|
toolMetas?: readonly { asyncStarted?: boolean; toolName: string }[],
|
|
): boolean {
|
|
return (toolMetas ?? []).some(
|
|
(entry) => entry.asyncStarted === true || !isReplaySafeReadOnlyToolName(entry.toolName),
|
|
);
|
|
}
|
|
|
|
function isReplaySafeReadOnlyToolName(toolName: string): boolean {
|
|
const normalized = toolName.trim().toLowerCase();
|
|
return COPILOT_REPLAY_SAFE_READ_ONLY_TOOL_NAMES.has(normalized);
|
|
}
|