Summary
Define a host-side contract for deterministic record-and-replay of plugin tool calls. Today plugins that need replay-stable output have to invent their own Clock injection and frozen-snapshot patterns from scratch.
Problem to solve
Discovered during the lossless-claw#516 audit. The plugin's lcm_recent tool reads the wall clock in 5+ places and originally also called host RPCs whose responses change between recording and replay. Without a host-side replay subsystem, the same tool invocation can produce different bytes on replay than on the original recording — defeating "lossless" semantics.
A grep across openclaw/openclaw (vanilla v2026.4.23 + the rebase tree), the plugin-sdk dist, and host-hook-docs found:
RecordedClock, FrozenClock, replayClock, hostClock — zero matches
AsyncLocalStorage<{ clock: ... }> — zero matches
replay references in plugin-sdk: all about LLM transcript replay/compaction (provider-owned), not tool-call replay
- No host-side hook documented at
openclaw-host-hook-docs/docs/
So every plugin that wants replay-safe output has to invent its own. Lossless-claw#516 just landed a Clock interface on its own LcmDependencies (Clock { now(): Date }, required) — it works, but it's plugin-local. The host has no way to substitute a frozen impl during replay.
Proposed solution
A small RFC + implementation covering:
1. Clock injection convention. Standardize a Clock interface in plugin-sdk:
export interface Clock {
now(): Date;
// Future: monotonic(): number for perf timers
}
Host injects via plugin deps construction. During recording, the host provides a live impl ({ now: () => new Date() }). During replay, the host provides a frozen impl ({ now: () => recordedTurnStartTimestamp }). Lossless-claw#516's LcmDependencies.clock is a working prototype that could become the standard shape.
2. Frozen-snapshot RPC results during replay. When a plugin tool calls a curated host RPC (per linked issue on plugin-sdk gateway surface), the replay harness should return the recorded value, not call the live RPC. Concrete contract: plugin calls runtime.gateway.getStatus(); recording captures the response; replay returns the captured response.
3. Test/recording harness. A reusable helper for plugins to pin their tool output: record once, byte-diff on every replay. The current ad-hoc per-plugin testing strategy doesn't catch replay drift.
Alternatives considered
- Per-plugin clock injection — what lossless-claw#516 just did. Works but balkanizes; every plugin reinvents.
- Monkey-patch global Date during replay — fragile, leaks across worker boundaries, breaks legitimate
Date use.
- AsyncLocalStorage clock per-call — cleaner but harder to reason about; passing as a dep is more explicit.
Impact
- Affected: any plugin where replay determinism matters. Today: lossless-claw is the canonical case. Future: any plugin that mediates LLM context (planning, memory, tool-result caching).
- Severity: the user has called replay determinism out as a P0 concern multiple times. Without a host contract, every plugin has to solve it independently and wrong.
- Frequency: every replay invocation.
Evidence
- lossless-claw audit replay-determinism report:
audit/pr516/chunk-F-replay.md (in pr516-audit-fixes branch — caught 5 P0 read-path violations all collapsing to one fix surface)
- Investigation report lines 99-130 documenting the absence of any host replay subsystem
- Lossless-claw#516's
Clock interface as a prototype: src/types.ts after commit b2633a8 on the pr516-audit-fixes branch
Notes
This is RFC-shaped work. Happy to draft a longer design doc if there's interest. The smallest viable starting point would be just (1) — standardize the Clock interface in plugin-sdk, document the convention, leave replay-time substitution to follow-up.
Related: #76756 (gateway-RPC plugin-sdk surface).
Summary
Define a host-side contract for deterministic record-and-replay of plugin tool calls. Today plugins that need replay-stable output have to invent their own
Clockinjection and frozen-snapshot patterns from scratch.Problem to solve
Discovered during the lossless-claw#516 audit. The plugin's
lcm_recenttool reads the wall clock in 5+ places and originally also called host RPCs whose responses change between recording and replay. Without a host-side replay subsystem, the same tool invocation can produce different bytes on replay than on the original recording — defeating "lossless" semantics.A grep across
openclaw/openclaw(vanilla v2026.4.23 + the rebase tree), the plugin-sdk dist, and host-hook-docs found:RecordedClock,FrozenClock,replayClock,hostClock— zero matchesAsyncLocalStorage<{ clock: ... }>— zero matchesreplayreferences in plugin-sdk: all about LLM transcript replay/compaction (provider-owned), not tool-call replayopenclaw-host-hook-docs/docs/So every plugin that wants replay-safe output has to invent its own. Lossless-claw#516 just landed a
Clockinterface on its ownLcmDependencies(Clock { now(): Date }, required) — it works, but it's plugin-local. The host has no way to substitute a frozen impl during replay.Proposed solution
A small RFC + implementation covering:
1. Clock injection convention. Standardize a
Clockinterface in plugin-sdk:Host injects via plugin deps construction. During recording, the host provides a live impl (
{ now: () => new Date() }). During replay, the host provides a frozen impl ({ now: () => recordedTurnStartTimestamp }). Lossless-claw#516'sLcmDependencies.clockis a working prototype that could become the standard shape.2. Frozen-snapshot RPC results during replay. When a plugin tool calls a curated host RPC (per linked issue on plugin-sdk gateway surface), the replay harness should return the recorded value, not call the live RPC. Concrete contract: plugin calls
runtime.gateway.getStatus(); recording captures the response; replay returns the captured response.3. Test/recording harness. A reusable helper for plugins to pin their tool output: record once, byte-diff on every replay. The current ad-hoc per-plugin testing strategy doesn't catch replay drift.
Alternatives considered
Dateuse.Impact
Evidence
audit/pr516/chunk-F-replay.md(inpr516-audit-fixesbranch — caught 5 P0 read-path violations all collapsing to one fix surface)Clockinterface as a prototype:src/types.tsafter commitb2633a8on thepr516-audit-fixesbranchNotes
This is RFC-shaped work. Happy to draft a longer design doc if there's interest. The smallest viable starting point would be just (1) — standardize the Clock interface in plugin-sdk, document the convention, leave replay-time substitution to follow-up.
Related: #76756 (gateway-RPC plugin-sdk surface).