Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ direct libSQL authority also support Node 24 serverless runtimes. Install the
current immutable release directly from GitHub:

```sh
bun add --global github:hraness/oh#v0.2.1
bun add --global github:hraness/oh#v0.2.2
oh --help
```

Expand Down Expand Up @@ -100,7 +100,7 @@ For a project dependency, pin the same immutable release in `package.json`:
```json
{
"dependencies": {
"@hraness/oh": "github:hraness/oh#v0.2.1"
"@hraness/oh": "github:hraness/oh#v0.2.2"
}
}
```
Expand Down Expand Up @@ -162,7 +162,7 @@ import { createClient } from "@libsql/client";
import {
bootstrapOhLibSqlAuthorityV1,
createOhLibSqlStoreAuthorityV1,
openExistingOhLibSqlStoreAuthorityV1,
purgeOhLibSqlWorkingSpaceV1,
} from "@hraness/oh/libsql";
import { OH_WORKING_STORE_PROFILE_V1 } from "@hraness/oh/store";

Expand All @@ -188,20 +188,19 @@ const authority = await createOhLibSqlStoreAuthorityV1(runtimeClient, {
const store = authority.store;
console.log(await store.head());

// A separately held purge worker can fail closed unless the exact binding
// already exists. Opening performs reads only; it cannot create a space.
// A separately held purge worker either purges the exact existing binding or
// writes an empty-head tombstone when creation never completed. It cannot
// create a space or binding, and a delayed creator cannot resurrect custody.
const purgeClient = createClient({
authToken: process.env.OH_PURGE_TOKEN!,
url: process.env.OH_DATABASE_URL!,
});
const existing = await openExistingOhLibSqlStoreAuthorityV1(purgeClient, {
await purgeOhLibSqlWorkingSpaceV1(purgeClient, {
closeClient: true,
profile: OH_WORKING_STORE_PROFILE_V1,
realmId: "tenant:example/thread:research",
spaceId: "thread:research",
});
await existing.host.purgeWorkingSpace({});
await existing.store.close();
purgeClient.close();
```

The working profile disables operation replication. Dependency-closure export
Expand Down Expand Up @@ -467,7 +466,7 @@ keep remote sync explicit.
You can also give an agent this prompt:

```text
Install hraness/oh and its Oh Agent Skill from the immutable v0.2.1 tag at
Install hraness/oh and its Oh Agent Skill from the immutable v0.2.2 tag at
https://github.com/hraness/oh. Verify the CLI with `oh --help` and `oh version`.
Do not create or modify an Oh database until I name its path and ask you to.
```
Expand Down
2 changes: 1 addition & 1 deletion dist/cli.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
export declare const OH_PACKAGE_VERSION: "0.2.1";
export declare const OH_PACKAGE_VERSION: "0.2.2";
export declare function runOhCli(arguments_: readonly string[]): Promise<number>;
//# sourceMappingURL=cli.d.ts.map
2 changes: 1 addition & 1 deletion dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3072,7 +3072,7 @@ class Oh {

// src/cli.ts
import { readFile } from "fs/promises";
var OH_PACKAGE_VERSION = "0.2.1";
var OH_PACKAGE_VERSION = "0.2.2";
var KNOWN_OPTIONS = new Set([
"actor",
"after",
Expand Down
11 changes: 10 additions & 1 deletion dist/libsql.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Sha256Hex } from "./canonical";
import { type OhStoreAuthorityV1, type OhStoreProfileV1 } from "./store";
import { type OhSpacePurgeReceiptV1, type OhStoreAuthorityV1, type OhStoreProfileV1 } from "./store";
export type OhLibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string;
export type OhLibSqlStatementV1 = Readonly<{
args?: readonly OhLibSqlValueV1[];
Expand Down Expand Up @@ -45,4 +45,13 @@ export declare function createOhLibSqlStoreAuthorityV1(client: OhLibSqlClientV1,
* must fail closed instead of acquiring space-creation authority.
*/
export declare function openExistingOhLibSqlStoreAuthorityV1(client: OhLibSqlClientV1, options?: OhLibSqlStoreAuthorityOptionsV1): Promise<OhStoreAuthorityV1>;
/**
* Purges an existing working authority or atomically fences its exact binding
* when creation never completed. The empty-space receipt prevents a delayed
* creator from resurrecting abandoned custody without granting the purge
* credential permission to create a space or binding.
*/
export declare function purgeOhLibSqlWorkingSpaceV1(client: OhLibSqlClientV1, options?: OhLibSqlStoreAuthorityOptionsV1 & Readonly<{
purgedAt?: string;
}>): Promise<OhSpacePurgeReceiptV1>;
//# sourceMappingURL=libsql.d.ts.map
2 changes: 1 addition & 1 deletion dist/libsql.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

210 changes: 174 additions & 36 deletions dist/libsql.js
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,8 @@ var PURGE_ROW_SELECT = `SELECT space_id, binding_sha256, prior_operation_sha256,
FROM oh_authority_purges WHERE space_id = ?`;
var BINDING_ROW_SELECT = `SELECT space_id, realm_id, profile_id, profile_kind,
profile_sha256, binding_sha256, binding_json FROM oh_authority_bindings WHERE space_id = ?`;
var SPACE_PURGE_PROOF_SELECT = `SELECT generation, graph_revision_sha256, head_operation_sha256,
records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`;
var OPERATION_ROW_COLUMNS = `operation_sha256, space_id, sequence, operation_id,
parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`;
var OPERATION_RESPONSE_BYTES = `2 * length(CAST(operation.operation_json AS BLOB))
Expand Down Expand Up @@ -1399,6 +1401,41 @@ async function requireExistingSpace(client, binding) {
}
parseHeadRow(spaceRow);
}
var PURGE_PAYLOAD_TABLES = [
"oh_authority_spaces",
"oh_authority_bindings",
"oh_authority_operations",
"oh_authority_operation_records",
"oh_authority_records",
"oh_authority_dependencies"
];
async function assertRemotePurgeComplete(client, binding, expected) {
const results = await client.batch([
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] },
...PURGE_PAYLOAD_TABLES.map((table) => ({
sql: `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`,
args: [binding.spaceId]
})),
{ sql: `SELECT count(*) AS count FROM oh_authority_operation_records AS materialized
LEFT JOIN oh_authority_operations AS operation
ON operation.operation_sha256 = materialized.operation_sha256
WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id` }
], "read");
const receiptRow = results[0]?.rows[0];
if (receiptRow === undefined || canonicalJson(parsePurgeReceiptRow(receiptRow, binding.spaceId, binding.bindingSha256)) !== canonicalJson(expected)) {
throw new OhIntegrityError("The remote purge receipt differs from the requested purge.");
}
for (let index = 0;index < PURGE_PAYLOAD_TABLES.length; index += 1) {
const countRow = results[index + 1]?.rows[0];
if (countRow === undefined || integer(rowValue(countRow, "count", 0)) !== 0) {
throw new OhIntegrityError(`Remote purge left rows in ${PURGE_PAYLOAD_TABLES[index]}.`);
}
}
const orphanRow = results[PURGE_PAYLOAD_TABLES.length + 1]?.rows[0];
if (orphanRow === undefined || integer(rowValue(orphanRow, "count", 0)) !== 0) {
throw new OhIntegrityError("Remote purge left an orphaned or cross-space operation record.");
}
}

class OhLibSqlStoreV1 {
binding;
Expand Down Expand Up @@ -2319,42 +2356,7 @@ class OhLibSqlStoreV1 {
};
}
async#assertPurgeComplete(expected) {
const tables = [
"oh_authority_spaces",
"oh_authority_bindings",
"oh_authority_operations",
"oh_authority_operation_records",
"oh_authority_records",
"oh_authority_dependencies"
];
const results = await this.#client.batch([
{
sql: PURGE_ROW_SELECT,
args: [this.binding.spaceId]
},
...tables.map((table) => ({
sql: `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`,
args: [this.binding.spaceId]
})),
{ sql: `SELECT count(*) AS count FROM oh_authority_operation_records AS materialized
LEFT JOIN oh_authority_operations AS operation
ON operation.operation_sha256 = materialized.operation_sha256
WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id` }
], "read");
const receiptRow = results[0]?.rows[0];
if (receiptRow === undefined || canonicalJson(parsePurgeReceiptRow(receiptRow, this.binding.spaceId, this.binding.bindingSha256)) !== canonicalJson(expected)) {
throw new OhIntegrityError("The remote purge receipt differs from the requested purge.");
}
for (let index = 0;index < tables.length; index += 1) {
const countRow = results[index + 1]?.rows[0];
if (countRow === undefined || integer(rowValue(countRow, "count", 0)) !== 0) {
throw new OhIntegrityError(`Remote purge left rows in ${tables[index]}.`);
}
}
const orphanRow = results[tables.length + 1]?.rows[0];
if (orphanRow === undefined || integer(rowValue(orphanRow, "count", 0)) !== 0) {
throw new OhIntegrityError("Remote purge left an orphaned or cross-space operation record.");
}
await assertRemotePurgeComplete(this.#client, this.binding, expected);
}
async purgeWorkingSpace(purgedAt) {
this.#assertOpen();
Expand Down Expand Up @@ -2516,7 +2518,143 @@ async function openExistingOhLibSqlStoreAuthorityV1(client, options = {}) {
await requireExistingSpace(client, binding);
return bindOhLibSqlStoreAuthorityV1(client, binding, profile, options.closeClient ?? false);
}
async function purgeOhLibSqlWorkingSpaceV1(client, options = {}) {
const closeClient = options.closeClient ?? false;
try {
const profile = parseOhStoreProfileV1(options.profile ?? OH_WORKING_STORE_PROFILE_V1);
if (profile === null)
throw new TypeError("Invalid libSQL store profile.");
if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) {
throw new OhProfileError("Whole-space purge requires a bound working profile.");
}
const spaceId = options.spaceId ?? "default";
const binding = createOhStoreBindingV1({
profile,
realmId: options.realmId ?? `realm:${spaceId}`,
spaceId,
v: 1
});
const purgedAt = options.purgedAt ?? canonicalNow();
await verifyAuthoritySchema(client);
for (let attempt = 0;attempt < 3; attempt += 1) {
const proof = await client.batch([
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
{ sql: SPACE_PURGE_PROOF_SELECT, args: [binding.spaceId] },
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] }
], "read");
if (proof.length !== 3) {
throw new OhIntegrityError("The remote authority returned an incomplete purge proof.");
}
const [bindingResult, spaceResult, purgeResult] = proof;
const existingPurge = purgeResult.rows[0];
if (existingPurge !== undefined) {
const receipt2 = parsePurgeReceiptRow(existingPurge, binding.spaceId, binding.bindingSha256);
await assertRemotePurgeComplete(client, binding, receipt2);
return receipt2;
}
const bindingRow = bindingResult.rows[0];
const spaceRow = spaceResult.rows[0];
if (bindingRow === undefined !== (spaceRow === undefined)) {
throw new OhIntegrityError("The remote authority has only half of its space binding.");
}
if (bindingRow !== undefined && spaceRow !== undefined) {
const persisted2 = parseBindingRow(bindingRow, binding.spaceId);
if (canonicalJson(persisted2) !== canonicalJson(binding)) {
throw new OhProfileError("The remote space is bound to a different realm or profile.");
}
if (rowValue(spaceRow, "contract_id", 5) !== OH_CONTRACT_MANIFEST_V1.contractId) {
throw new OhIntegrityError("The existing remote space uses a different Oh contract.");
}
parseHeadRow(spaceRow);
const authority = new OhLibSqlStoreV1(client, binding, false);
return await authority.purgeWorkingSpace(purgedAt);
}
const receipt = createOhSpacePurgeReceiptV1({
binding,
priorHead: emptyOhHeadV1(),
purgedAt
});
const receiptJson = canonicalJson(receipt);
try {
await client.batch([
{
sql: `INSERT INTO oh_authority_purges(space_id, binding_sha256,
prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json)
SELECT ?, ?, NULL, 0, ?, ?, ?
WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
AND NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
ON CONFLICT(space_id) DO NOTHING`,
args: [
binding.spaceId,
binding.bindingSha256,
receipt.purgedAt,
receipt.receiptSha256,
receiptJson,
binding.spaceId,
binding.spaceId
]
},
{
sql: `INSERT INTO oh_authority_commit_guards(value)
SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?)
OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?)
OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?)
OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?)
OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?)
OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?)
OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized
LEFT JOIN oh_authority_operations AS operation
ON operation.operation_sha256 = materialized.operation_sha256
WHERE operation.operation_sha256 IS NULL
OR operation.space_id <> materialized.space_id)
OR NOT EXISTS (SELECT 1 FROM oh_authority_purges
WHERE space_id = ? AND receipt_sha256 = ?)`,
args: [
binding.spaceId,
binding.spaceId,
binding.spaceId,
binding.spaceId,
binding.spaceId,
binding.spaceId,
binding.spaceId,
receipt.receiptSha256
]
}
], "write");
} catch (error) {
const recovery = await client.batch([
{ sql: BINDING_ROW_SELECT, args: [binding.spaceId] },
{ sql: SPACE_PURGE_PROOF_SELECT, args: [binding.spaceId] },
{ sql: PURGE_ROW_SELECT, args: [binding.spaceId] }
], "read");
if (recovery.length !== 3) {
throw new OhIntegrityError("The remote authority returned an incomplete purge recovery proof.");
}
const raced = recovery[2]?.rows[0];
if (raced !== undefined) {
const persisted2 = parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256);
await assertRemotePurgeComplete(client, binding, persisted2);
return persisted2;
}
if (recovery[0]?.rows[0] !== undefined || recovery[1]?.rows[0] !== undefined)
continue;
throw error;
}
const persisted = await queryOne(client, { sql: PURGE_ROW_SELECT, args: [binding.spaceId] });
if (persisted === null)
continue;
const exact = parsePurgeReceiptRow(persisted, binding.spaceId, binding.bindingSha256);
await assertRemotePurgeComplete(client, binding, exact);
return exact;
}
throw new OhConflictError("The remote working space changed repeatedly while fencing purge.");
} finally {
if (closeClient)
client.close?.();
}
}
export {
purgeOhLibSqlWorkingSpaceV1,
openExistingOhLibSqlStoreAuthorityV1,
createOhLibSqlStoreAuthorityV1,
bootstrapOhLibSqlAuthorityV1,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hraness/oh",
"version": "0.2.1",
"version": "0.2.2",
"description": "open-source tools for agentic research",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oh-site",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"packageManager": "bun@1.3.14",
"engines": {
Expand Down
7 changes: 7 additions & 0 deletions site/public/spec/v1/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ purged, or differently bound space and never inserts, updates, or deletes data
during open. A provider credential can therefore omit space and binding
creation while retaining only the data actions required by its later task.

`purgeOhLibSqlWorkingSpaceV1` is the one-shot lifecycle-worker boundary. It
purges an existing exact working binding or atomically writes an empty-head
purge receipt when creation never completed. Creation and absent-space fencing
race in the same authority transaction: whichever wins, the result converges
to one complete purge receipt, and a delayed creator cannot resurrect the
space. The credential still needs no permission to create spaces or bindings.

Runtime open verifies the exact installed table, index, and trigger set, not
only a schema marker. Every operation, binding, and purge receipt read parses
its canonical JSON and cross-checks each duplicated SQL column. Current reads
Expand Down
Loading