mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* fix(doctor): preserve current sessions after restored-index replay Use recorded restore provenance at the Doctor import boundary so an unchanged restored index reconciles history without replacing current SQLite metadata. Revalidate source and receipt after staging and preserve fresh-index imports; unreadable recovery evidence refuses before writes. Keep config selection with the existing target-discovery owner without changing its behavior. Preserve archive mutation coverage at its actual publication boundary rather than an identity-read count. Refs #156240. Co-authored-by: Vincent Koc <vincentkoc@ieee.org> * fix(doctor): admit fresh imports with unrelated unreadable history Check the empty-destination requirement inside the existing SQLite import transaction before any row writes. Use one disk-backed staged batch only for this exceptional recovery path; retain normal 256-row batching. Prove more than 256 fresh rows import and a destination occupied after staging refuses atomically. Preserve current-state refusal and existing source, authority, transcript and rollback checks. * fix(doctor): bind restored indexes to their selected import owner Reuse explicit restore target admission instead of cleanup discovery eligibility. A shared original may have multiple owners; require the selected owner's receipt and revalidate every matching manifest. Preserve supported external custom stores without extending cleanup ownership. Cover current metadata for both shared owners, missing selected receipts, and external completed restore/reimport. * test(update): qualify restored sessions through the published updater * test(update): retain restored-index evidence in hosted diagnostics * test(update): clear stale restored-index observations before capture * chore: register the restored-index fixture executable * fix(doctor): preserve verified sources during file-era repair After pending plugin migrations cleared, legacy session normalization could rewrite an index still bound by a deferred-import receipt. Keep those originals with the existing receipt owner until verified archival completes, including configured SQLite bindings for a shared legacy root. Extend the existing deferred-import cases through registered Doctor migration and preserve archival, current metadata, and deleted-session assertions. Move the unchanged index-reverification case into its retained-source suite and simplify the equivalent account-id trim guard to keep existing line caps. The published 9.4 failure remains evidence: canonical metadata survived, but archive settlement did not. The corrected package still requires its installed upgrade/reimport/rollback cell. * fix(doctor): retain the migration caller environment Resolve configured legacy targets using the same environment already held by Doctor. Reconstructing process.env missed a durable receipt when a supplied home expanded the same custom store through a tilde path. Keep the detected state-directory override. The existing custom-store case now proves that explicit environment through registered migration; it failed before this correction. All eight layout cases pass with archival and no-replay assertions unchanged. * test(update): follow retained archive receipts after restore The schema observer selected a completed move whose archive had already been consumed by official restore, hiding the later verified reimport receipt. Filter only each manifest's consumed moves while keeping owner, existence, content, session and schema assertions intact. Move the existing schema suite into its own file and prove both restored/reimported success and refusal of consumed-only evidence. The installed first-hop and explicit reimport passed; the failed schema observation and unfinished rollback remain recorded. * test(update): honor published backup transcript exclusions * fix(e2e): stage the complete survivor diagnostics closure --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
87 lines
2.9 KiB
JavaScript
87 lines
2.9 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import zlib from "node:zlib";
|
|
|
|
// Release proofs decode the persisted contract independently of candidate runtime code.
|
|
const MAX_COMPRESSED_EVENT_BYTES = 4 * 1024 * 1024;
|
|
const payloadSchemas = new WeakMap();
|
|
const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
|
|
function hasCompressedPayloads(database) {
|
|
if (!payloadSchemas.has(database)) {
|
|
payloadSchemas.set(
|
|
database,
|
|
Boolean(
|
|
database
|
|
.prepare("SELECT 1 FROM pragma_table_info('transcript_events') WHERE name = 'event_zstd'")
|
|
.get(),
|
|
),
|
|
);
|
|
}
|
|
return payloadSchemas.get(database);
|
|
}
|
|
|
|
/** Read published legacy fixtures and current candidate databases without migrating either. */
|
|
export function sqliteTranscriptPayloadColumns(database) {
|
|
return hasCompressedPayloads(database)
|
|
? "event_json, event_zstd, event_utf8_bytes"
|
|
: "event_json";
|
|
}
|
|
|
|
/** Bound materialization before decoding any compressed event. */
|
|
export function sqliteTranscriptPayloadBytesSql(database) {
|
|
return hasCompressedPayloads(database)
|
|
? "coalesce(event_utf8_bytes, octet_length(event_json))"
|
|
: "octet_length(event_json)";
|
|
}
|
|
|
|
/** Preserve exact JSON bytes for survivor, backup, and cold-restoration comparisons. */
|
|
export function readSqliteTranscriptPayload(row) {
|
|
if (typeof row.event_json === "string" && row.event_zstd == null) {
|
|
return row.event_json;
|
|
}
|
|
const bytes = row.event_zstd;
|
|
const expectedBytes = row.event_utf8_bytes;
|
|
if (
|
|
row.event_json !== null ||
|
|
!(bytes instanceof Uint8Array) ||
|
|
bytes.byteLength === 0 ||
|
|
bytes.byteLength > MAX_COMPRESSED_EVENT_BYTES ||
|
|
!Number.isSafeInteger(expectedBytes) ||
|
|
expectedBytes < 1 ||
|
|
expectedBytes > MAX_COMPRESSED_EVENT_BYTES
|
|
) {
|
|
throw new Error("Invalid persisted transcript payload");
|
|
}
|
|
if (typeof zlib.zstdDecompressSync !== "function") {
|
|
throw new Error("Transcript verification requires a runtime with Zstd support");
|
|
}
|
|
const decoded = zlib.zstdDecompressSync(bytes, { maxOutputLength: expectedBytes });
|
|
if (decoded.byteLength !== expectedBytes) {
|
|
throw new Error("Persisted transcript payload differs from its recorded UTF-8 size");
|
|
}
|
|
return utf8Decoder.decode(decoded);
|
|
}
|
|
|
|
export function transcriptIdentity(event) {
|
|
// Doctor repairs metadata; the fixture's text-only turn must retain event IDs and messages.
|
|
return {
|
|
type: event.type,
|
|
id: event.id,
|
|
...(event.type === "message"
|
|
? {
|
|
role: event.message.role,
|
|
textHash: createHash("sha256")
|
|
.update(
|
|
JSON.stringify(
|
|
typeof event.message.content === "string"
|
|
? [event.message.content]
|
|
: event.message.content
|
|
.filter((part) => part.type === "text")
|
|
.map((part) => part.text),
|
|
),
|
|
)
|
|
.digest("hex"),
|
|
}
|
|
: {}),
|
|
};
|
|
}
|