Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
24f781e
fix: bound session cold loads and narrow lazy field reads (#4739)
z0d0dOr Oct 25, 2026
7b1ecc2
fix: bound ordinary session loads and restore lazy fields with narrow…
Oct 25, 2026
ba50d87
Merge remote-tracking branch 'origin/ci-fixes' into pr-4750-review
Oct 25, 2026
b217d79
perf: verify stored sessions in bounded batches (#4538)
z0d0dOr Oct 24, 2026
41422f3
test: pin the sessionViewRows pointer-scoped contract on both storage…
Oct 25, 2026
c3619a0
fix: preserve blog metadata and launcher checks on Windows
z0d0dOr Oct 25, 2026
c12bee2
test: verify fork restart under legacy and full storage
z0d0dOr Oct 25, 2026
8576277
test(storage): account for persisted engine lane in render goldens
z0d0dOr Oct 25, 2026
9397bc8
test(storage): account for one-time ledger ref migration on restart
z0d0dOr Oct 25, 2026
66297ff
test: honor Windows cmd wrapper for inline OpenCode launch
z0d0dOr Oct 25, 2026
610b164
test: close fork session stores before Windows cleanup
z0d0dOr Oct 25, 2026
22ecc98
merge ci-fixes into 2026-10-26_bounded-session-load (#4599 help-merge…
Oct 25, 2026
a08e722
merge ci-fixes into 2026-10-26_bounded-session-load (#4599 help-merge…
Oct 26, 2026
f16d366
fix: stream storage verification digests (#4529)
z0d0dOr Oct 24, 2026
41a718f
test(storage): pin streaming digest byte-identity at MB scale and chu…
Oct 24, 2026
fe790cb
fix: preserve blog metadata and launcher checks on Windows
z0d0dOr Oct 25, 2026
8518a73
test: verify fork restart under legacy and full storage
z0d0dOr Oct 25, 2026
f8d91aa
test(storage): account for one-time ledger ref migration on restart
z0d0dOr Oct 25, 2026
97641cb
Merge remote-tracking branch 'origin/master' into pr4532-sync2
Oct 28, 2026
e683df6
Merge remote-tracking branch 'origin/master' into pr4532-sync2
Oct 28, 2026
2006902
Merge remote-tracking branch 'origin/ci-fixes' into pr4532-sync2
Oct 28, 2026
b7c35b4
Merge master into batched storage verification PR
z0d0dOr Oct 28, 2026
0072fb6
Merge master into bounded session loading PR
z0d0dOr Oct 28, 2026
4668191
Merge commit 'b0b0e66ff085b08e8e26c02f1f79ad2a8849a496' into 2026-10-…
z0d0dOr Oct 28, 2026
01082e9
Merge commit 'b0b0e66ff085b08e8e26c02f1f79ad2a8849a496' into 2026-10-…
z0d0dOr Oct 28, 2026
2fdd701
Merge remote-tracking branch 'origin/master' into 2026-10-25_bounded-…
z0d0dOr Oct 28, 2026
3a4e2b2
test: fix Windows graceful shutdown and fixture teardown
z0d0dOr Oct 28, 2026
858cb70
test: fix Windows graceful shutdown and fixture teardown
z0d0dOr Oct 28, 2026
8c03ff1
test: fix Windows graceful shutdown and fixture teardown
z0d0dOr Oct 28, 2026
3f78187
test: handle reset health probes in silent TCP fixture
z0d0dOr Oct 28, 2026
47b54b3
test: handle reset health probes in silent TCP fixture
z0d0dOr Oct 28, 2026
aec79ef
test: handle reset health probes in silent TCP fixture
z0d0dOr Oct 28, 2026
7eae0fd
Merge upstream atomic fork initialization into bounded session loading
z0d0dOr Oct 28, 2026
d87d43e
Merge master to retain atomic fork publication with batched verification
z0d0dOr Oct 28, 2026
46ad111
Merge master into bounded session loading PR
Oct 29, 2026
4b759b3
Merge master into PR head (#4540 conflict resolution)
Oct 29, 2026
39c0d91
Merge remote-tracking branch 'origin/master' into pr4532-sync2
Oct 29, 2026
c5a7b5f
Merge remote-tracking branch 'origin/master' into pr4540-sync
Oct 29, 2026
7ece6e6
perf: 增量车道每轮不再重渲染全历史、不再同步落盘 (#5155)
Oct 29, 2026
77751bf
Merge branch '2026-10-24_streaming-storage-digest' of https://github.…
Oct 29, 2026
9a31c61
Merge remote-tracking branch 'origin/master' into pr4532-sync2
Oct 29, 2026
911c776
Merge remote-tracking branch 'origin/master' into pr4750-updatebranch
Oct 29, 2026
bc4f8c1
Merge remote-tracking branch 'origin/master' into 2026-10-29_incremen…
Oct 29, 2026
f13707f
test: 适配 #5155 异步落盘时序 (含 master 合并)
Oct 29, 2026
b54dafd
fix(storage): align streaming digest key order with canonicalize (#4532)
Oct 30, 2026
158a94c
Merge remote-tracking branch 'origin/master' into pr4750-updatebranch
Oct 30, 2026
44aca70
Merge branch 'p4532' into 2026-10-30_classC-memory-consolidation
Oct 30, 2026
41d4680
Merge branch 'p4750' into 2026-10-30_classC-memory-consolidation
Oct 30, 2026
9a1217d
Merge branch 'p4765' into 2026-10-30_classC-memory-consolidation
Oct 30, 2026
70d8f97
Merge branch 'p4540' into 2026-10-30_classC-memory-consolidation
Oct 30, 2026
db0b022
Merge branch 'p5156' into 2026-10-30_classC-memory-consolidation
Oct 30, 2026
ccf51f0
fix(types): split getBlockContents into true overloads after union of…
Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 175 additions & 42 deletions src/incremental-lane.ts

Large diffs are not rendered by default.

186 changes: 173 additions & 13 deletions src/persist.ts

Large diffs are not rendered by default.

33 changes: 27 additions & 6 deletions src/storage/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type {
SessionSummaryRow,
WebSummaryRow,
IngestSessionInput,
SessionViewRows,
} from "./store.js";
import type {
SessionRow,
Expand All @@ -44,6 +45,7 @@ import type {
SessionSummaryRow,
WebSummaryRow,
IngestSessionInput,
SessionViewRows,
} from "./store.js";

/** Everything a storage backend must provide. Mirrors the operation surface
Expand Down Expand Up @@ -75,10 +77,15 @@ export interface StorageBackend {
seqByRaw(sessionId: string): Promise<Map<string, number> | null>;
/** #4316 PR3/3: fork adoption row clone (idempotent). */
cloneSessionRows(parentId: string, childId: string, throughSeq: number, initial?: IngestSessionInput): Promise<boolean>;
getMessages(sessionId: string): Promise<(MessageRow & { contentHash: string })[]>;
getBlocks(sessionId: string): Promise<(BlockRow & { summaryHash: string; oneHash: string | null })[]>;
getMessages(sessionId: string, opts?: { includeRetired?: boolean; seqs?: readonly number[] }): Promise<(MessageRow & { contentHash: string })[]>;
getBlocks(sessionId: string, opts?: { limit: number; offset?: number }): Promise<(BlockRow & { summaryHash: string; oneHash: string | null })[]>;
/** #4739: the viewFor bundle from ONE consistent read snapshot (messages
* fetched only at the tail/pluginSnapshot pointer seqs). */
sessionViewRows(sessionId: string): Promise<SessionViewRows>;
getDeadRefs(sessionId: string): Promise<{ ord: number; ref: string; refNum: number; recordedAt: number }[]>;
getRefs(sessionId: string): Promise<RefRow[]>;
/** All reads, including CAS hydration, use one short read-only snapshot. */
readSnapshot<T>(read: (reader: StorageReadView) => Promise<T>): Promise<T>;
getCcr(sessionId: string): Promise<MessageContentStore | null>;
getCcrEntries(sessionId: string): Promise<CcrEntryRow[]>;

Expand Down Expand Up @@ -141,6 +148,8 @@ export interface StorageBackend {
init?(): Promise<void>;
}

export type StorageReadView = Pick<StorageBackend, "getSessionRow" | "getMessages" | "getBlocks" | "getDeadRefs" | "getRefs">;

/** The sqlite backend: the existing UnifiedStore behind the async contract.
* Synchronous under the hood; `Promise.resolve` wrapping only. */
export class SqliteBackend implements StorageBackend {
Expand Down Expand Up @@ -190,18 +199,30 @@ export class SqliteBackend implements StorageBackend {
cloneSessionRows(parentId: string, childId: string, throughSeq: number, initial?: IngestSessionInput): Promise<boolean> {
return Promise.resolve(this.store.cloneSessionRows(parentId, childId, throughSeq, initial));
}
getMessages(sessionId: string): Promise<(MessageRow & { contentHash: string })[]> {
return Promise.resolve(this.store.getMessages(sessionId));
getMessages(sessionId: string, opts?: { includeRetired?: boolean; seqs?: readonly number[] }): Promise<(MessageRow & { contentHash: string })[]> {
return Promise.resolve(this.store.getMessages(sessionId, opts));
}
getBlocks(sessionId: string, opts?: { limit: number; offset?: number }): Promise<(BlockRow & { summaryHash: string; oneHash: string | null })[]> {
return Promise.resolve(this.store.getBlocks(sessionId, opts));
}
getBlocks(sessionId: string): Promise<(BlockRow & { summaryHash: string; oneHash: string | null })[]> {
return Promise.resolve(this.store.getBlocks(sessionId));
sessionViewRows(sessionId: string): Promise<SessionViewRows> {
return Promise.resolve(this.store.sessionViewRows(sessionId));
}
getDeadRefs(sessionId: string): Promise<{ ord: number; ref: string; refNum: number; recordedAt: number }[]> {
return Promise.resolve(this.store.getDeadRefs(sessionId));
}
getRefs(sessionId: string): Promise<RefRow[]> {
return Promise.resolve(this.store.getRefs(sessionId));
}
readSnapshot<T>(read: (reader: StorageReadView) => Promise<T>): Promise<T> {
return this.store.readSnapshotAsync((store) => read({
getSessionRow: async (id) => store.getSessionRow(id),
getMessages: async (id, opts) => store.getMessages(id, opts),
getBlocks: async (id, opts) => store.getBlocks(id, opts),
getDeadRefs: async (id) => store.getDeadRefs(id),
getRefs: async (id) => store.getRefs(id),
}));
}
getCcr(sessionId: string): Promise<MessageContentStore | null> {
return Promise.resolve(this.store.getCcr(sessionId));
}
Expand Down
96 changes: 96 additions & 0 deletions src/storage/canonical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,108 @@ function canonicalizeWithContext(value: unknown, key: string | undefined): unkno
}
return out;
}
return canonicalScalar(value, key);
}

function canonicalScalar(value: unknown, key: string | undefined): unknown {
if (key === "biliVersion" && typeof value === "string") return "__VER__";
if (typeof value === "number" && Number.isFinite(value) && value > 1_600_000_000_000) return "__TS__";
if (typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) return "__UUID__";
return value;
}

// Spec array index: "0" .. 2^32-2, no leading zeros.
function isArrayIndexKey(k: string): boolean {
return /^(0|[1-9]\d*)$/.test(k) && Number(k) <= 4_294_967_294;
}

export function sha256(s: string): string {
return createHash("sha256").update(s, "utf8").digest("hex");
}

/** Hash a storage view without materializing its canonical copy or JSON string. */
export function canonicalDigest(value: unknown): string {
if (value === undefined || typeof value === "function" || typeof value === "symbol") return sha256(JSON.stringify(value));
const hash = createHash("sha256");
const chunkSize = 64 * 1024;
let pending = "";
function write(text: string): void {
pending += text;
if (pending.length >= chunkSize) {
hash.update(pending, "utf8");
pending = "";
}
}
function writeString(text: string): void {
write('"');
for (let start = 0; start < text.length;) {
let end = Math.min(start + chunkSize, text.length);
const last = text.charCodeAt(end - 1);
// Keep a surrogate pair together so JSON escaping and UTF-8 stay exact.
if (end < text.length && last >= 0xd800 && last <= 0xdbff) end--;
write(JSON.stringify(text.slice(start, end)).slice(1, -1));
start = end;
}
write('"');
}
function visit(current: unknown, key?: string): void {
if (Array.isArray(current)) {
write("[");
for (let i = 0; i < current.length; i++) {
if (i) write(",");
visit(current[i]);
}
write("]");
} else if (current instanceof Map) {
write('{"__map__":[');
let separator = "";
for (const [k, v] of current) {
write(separator + "[");
// canonicalize leaves Map keys untouched.
write(JSON.stringify(k) ?? "null");
write(",");
visit(v);
write("]");
separator = ",";
}
write("]}");
} else if (current instanceof Set) {
write('{"__set__":[');
let separator = "";
for (const v of current) {
write(separator);
visit(v);
separator = ",";
}
write("]}");
} else if (current && typeof current === "object") {
write("{");
let separator = "";
// Effective order of JSON.stringify(canonicalize(v)): array-index keys
// first, ascending numeric (spec enumeration overrides canonicalize's
// lexicographic sort), then the remaining keys lexicographic.
const keys = Object.keys(current);
const orderedKeys = keys.filter(isArrayIndexKey).sort((a, b) => Number(a) - Number(b))
.concat(keys.filter((k) => !isArrayIndexKey(k)).sort());
for (const k of orderedKeys) {
// Assigning __proto__ to canonicalize's plain output creates no JSON property.
if (Object.hasOwn(DIGEST_SKIPPED_KEYS, k) || k === "__proto__") continue;
const v = (current as Record<string, unknown>)[k];
if (v === undefined || typeof v === "function" || typeof v === "symbol") continue;
write(separator);
writeString(k);
write(":");
visit(v, k);
separator = ",";
}
write("}");
} else {
const normalized = canonicalScalar(current, key);
if (typeof normalized === "string") writeString(normalized);
else write(JSON.stringify(normalized) ?? "null");
}
}
visit(value);
hash.update(pending, "utf8");
return hash.digest("hex");
}
45 changes: 25 additions & 20 deletions src/storage/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface SqliteDatabase {
prepare(sql: string): SqliteStatement;
/** Runs fn inside BEGIN/COMMIT; rolls back on throw (including nested calls via savepoints). */
transaction<T>(fn: () => T): T;
/** Deferred snapshot: readers do not reserve the writer lock. */
readTransaction<T>(fn: () => T): T;
close(): void;
readonly open: boolean;
}
Expand Down Expand Up @@ -87,6 +89,7 @@ function wrapBetterSqlite3(db: BetterSqlite3Database): SqliteDatabase {
// cannot resolve. Taking the write lock up front makes concurrent
// writers queue on busy_timeout instead (#2690 review blocking 5).
transaction: (fn) => db.transaction(fn).immediate(),
readTransaction: (fn) => db.transaction(fn).deferred(),
close: () => void db.close(),
get open() {
return db.open;
Expand All @@ -104,31 +107,33 @@ function openNodeSqlite(path: string): SqliteDatabase {
};
const db = new DatabaseSync(path);
let depth = 0;
const begin = () => db.exec(depth === 0 ? "BEGIN IMMEDIATE" : `SAVEPOINT sp${depth}`); // IMMEDIATE at top level: multi-process SQLITE_BUSY_SNAPSHOT guard (see openBetterSqlite3)
const begin = (readOnly: boolean) => db.exec(depth === 0 ? (readOnly ? "BEGIN DEFERRED" : "BEGIN IMMEDIATE") : `SAVEPOINT sp${depth}`);
const commit = () => db.exec(depth === 1 ? "COMMIT" : `RELEASE sp${depth - 1}`);
const rollback = () => db.exec(depth === 1 ? "ROLLBACK" : `ROLLBACK TO sp${depth - 1}`);
const transaction = <T>(fn: () => T, readOnly: boolean): T => {
if (db.getAutoCommit?.() === false && depth === 0) {
// Already inside an outer transaction driven directly by
// DatabaseSync — just run inline; the outer COMMIT covers us.
return fn();
}
begin(readOnly);
depth++;
try {
const out = fn();
commit();
return out;
} catch (err) {
rollback();
throw err;
} finally {
depth--;
}
};
return {
exec: (sql) => void db.exec(sql),
prepare: (sql) => adaptNodeSqliteStatement(db.prepare(sql)),
transaction: <T>(fn: () => T): T => {
if (db.getAutoCommit?.() === false && depth === 0) {
// Already inside an outer transaction driven directly by
// DatabaseSync — just run inline; the outer COMMIT covers us.
return fn();
}
begin();
depth++;
try {
const out = fn();
commit();
return out;
} catch (err) {
rollback();
throw err;
} finally {
depth--;
}
},
transaction: (fn) => transaction(fn, false),
readTransaction: (fn) => transaction(fn, true),
close: () => void db.close(),
get open() {
return db.open;
Expand Down
Loading
Loading