@hraness/oh 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -114
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +811 -97
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/index.js +676 -61
- package/dist/libsql.d.ts.map +1 -1
- package/dist/libsql.js +162 -35
- package/dist/memory-page.js +2 -2
- package/dist/memory.d.ts +87 -6
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1106 -148
- package/dist/operation.d.ts +3 -1
- package/dist/operation.d.ts.map +1 -1
- package/dist/projection-public.js +2 -2
- package/dist/projection-suss.js +2 -2
- package/dist/sdk.js +780 -88
- package/dist/semantic-cloud.js +2 -2
- package/dist/semantic.js +2 -2
- package/dist/sqlite/index.js +1251 -306
- package/dist/sqlite/port.d.ts +31 -3
- package/dist/sqlite/port.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts +18 -2
- package/dist/sqlite/store.d.ts.map +1 -1
- package/dist/store.d.ts +3 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +154 -32
- package/dist/sync.d.ts +7 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +668 -35
- package/package.json +5 -1
- package/skills/oh/SKILL.md +42 -16
- package/spec/README.md +2 -2
- package/spec/v1/memory.md +134 -16
- package/spec/v1/storage.md +8 -5
- package/spec/v1/store.md +20 -0
- package/spec/v1/sync.md +77 -8
- package/src/cli.test.ts +53 -1
- package/src/cli.ts +34 -8
- package/src/errors.test.ts +87 -0
- package/src/errors.ts +185 -0
- package/src/graph.ts +2 -2
- package/src/libsql.test.ts +36 -0
- package/src/libsql.ts +26 -5
- package/src/memory.test.ts +1488 -18
- package/src/memory.ts +1199 -122
- package/src/operation.ts +13 -3
- package/src/sqlite/port.test.ts +209 -0
- package/src/sqlite/port.ts +118 -4
- package/src/sqlite/store.test.ts +106 -1
- package/src/sqlite/store.ts +168 -30
- package/src/store.test.ts +12 -0
- package/src/store.ts +30 -20
- package/src/sync.test.ts +570 -2
- package/src/sync.ts +586 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/oh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "open-source tools for agentic research",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"types": "./dist/memory-page.d.ts",
|
|
69
69
|
"import": "./dist/memory-page.js"
|
|
70
70
|
},
|
|
71
|
+
"./memory": {
|
|
72
|
+
"types": "./dist/memory.d.ts",
|
|
73
|
+
"import": "./dist/memory.js"
|
|
74
|
+
},
|
|
71
75
|
"./projection": {
|
|
72
76
|
"types": "./dist/projection-public.d.ts",
|
|
73
77
|
"import": "./dist/projection-public.js"
|
package/skills/oh/SKILL.md
CHANGED
|
@@ -30,8 +30,8 @@ oh --help
|
|
|
30
30
|
oh version
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The supported CLI is the exact npm release `@hraness/oh@0.
|
|
34
|
-
tarball and checksum are mirrored by the immutable GitHub Release `v0.
|
|
33
|
+
The supported CLI is the exact npm release `@hraness/oh@0.4.1`. Its identical
|
|
34
|
+
tarball and checksum are mirrored by the immutable GitHub Release `v0.4.1`.
|
|
35
35
|
It requires Bun 1.3.14 or newer. The versioned contract is published at
|
|
36
36
|
<https://oh.computer/spec/>.
|
|
37
37
|
|
|
@@ -168,14 +168,17 @@ and credential source. Never print credentials or embed them in records. Oh
|
|
|
168
168
|
settles fast-forward histories only; preserve both logs when it reports a
|
|
169
169
|
divergence.
|
|
170
170
|
|
|
171
|
-
## Use composite memory only through host bindings
|
|
171
|
+
## Use stable composite memory only through host bindings
|
|
172
172
|
|
|
173
|
-
`@hraness/oh/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
`@hraness/oh/memory` is a stable SDK-only surface. Use
|
|
174
|
+
`createOhMemoryAuthorityV1`, and do not let a model construct its options.
|
|
175
|
+
Trusted application code must bind two distinct authority handles, exact
|
|
176
|
+
binding digests, a pinned canonical head, working codecs, working and adoption
|
|
177
|
+
actors, domain extractor relation ownership and digests, host-purposed named
|
|
178
|
+
rule/query programs, and named nomination routes. Give only the returned
|
|
179
|
+
`authority.agent` object to an agent; retain `authority.host` in trusted
|
|
180
|
+
control-plane code. The old `@hraness/oh/experimental/memory` subpath is a
|
|
181
|
+
compatibility alias, not the preferred import.
|
|
179
182
|
|
|
180
183
|
The agent-facing object may call only `remember`, `query`, `explain`, and
|
|
181
184
|
`nominate`. Never add a tool parameter for a database path or URL, authority,
|
|
@@ -186,18 +189,41 @@ as derived. A nomination may select only a host-registered route and is a
|
|
|
186
189
|
prepared dependency-closure candidate for destination-owned review, not
|
|
187
190
|
permission to write durable knowledge or import the working operation chain.
|
|
188
191
|
|
|
192
|
+
Host adoption must pass the complete prepared nomination back through
|
|
193
|
+
`authority.host.adoptNomination` with the exact canonical head it reviewed.
|
|
194
|
+
The host control re-exports the closure from the bound working store, inserts
|
|
195
|
+
absent records in one compare-and-swap operation, and treats equal digests as
|
|
196
|
+
already present. A different digest fails closed unless trusted host code adds
|
|
197
|
+
a bounded `replacements` claim with that exact logical key and exact reviewed
|
|
198
|
+
prior record digest. Never derive that claim from model input or retry it
|
|
199
|
+
against a new head. Missing, stale, wrong, duplicate, and absent-key claims
|
|
200
|
+
abort every change. The host rejects a prospective canonical snapshot over
|
|
201
|
+
8,192 records or 32 MiB, and reconciles the physical head after the commit so
|
|
202
|
+
an idempotent replay cannot install an older head. Do not suppress its
|
|
203
|
+
structured conflict evidence. Use `advanceCanonical` only for the same pin or
|
|
204
|
+
an exact later head already proven by the bound canonical operation chain.
|
|
205
|
+
Advance more than 16,384 operations in separate reviewed chunks.
|
|
206
|
+
|
|
189
207
|
Use `createOhMemoryAgentV2` only when the host has registered primitive
|
|
190
208
|
query-body parameters and fixed all projection, row, page, and page-byte
|
|
191
209
|
limits. Expose only the exact bindings object, program ID, and continuation to
|
|
192
210
|
the model. Do not expose parameter declarations, page size, or evaluator
|
|
193
211
|
options as tool input. Follow `hasMore` until the continuation is `null`, and
|
|
194
|
-
restart the named query after
|
|
195
|
-
working-head change.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
restart the named query only after `OhMemoryContinuationError`; never combine
|
|
213
|
+
pages across a working-head change. Store, projection, and extractor failures
|
|
214
|
+
are not continuation failures and need their own handling. A V2 `query-limit`
|
|
215
|
+
or `result-bytes` condition is a failed query, not a partial answer. Treat each
|
|
216
|
+
continuation as a bearer cursor: pass it back unchanged only to the exact query
|
|
217
|
+
and do not log or edit it. If the host reconstructs the facade or routes across
|
|
218
|
+
replicas, it must provide the same private 32 through 64 byte
|
|
219
|
+
`continuationKey` in host options; never expose that key as tool input. Keep
|
|
220
|
+
row-level `proofsTruncated` evidence visible.
|
|
221
|
+
Explanation capabilities share one 256-entry, 64 MiB cache and one clock guard
|
|
222
|
+
across canonical rollover; do not build a second token router around the
|
|
223
|
+
authority. Pass only plain JSON data to stable methods. Accessors, symbols,
|
|
224
|
+
proxies, sparse arrays, and non-JSON values are rejected before execution.
|
|
225
|
+
The detached-input walk caps depth, per-container breadth, total nodes, and
|
|
226
|
+
canonical bytes before recursively cloning untrusted children.
|
|
201
227
|
|
|
202
228
|
## Keep memory pages model-neutral
|
|
203
229
|
|
package/spec/README.md
CHANGED
|
@@ -21,7 +21,7 @@ binds these versions:
|
|
|
21
21
|
| Hosted semantic cache V1 | `oh.cloudflare.embeddinggemma.v1` |
|
|
22
22
|
| Hosted semantic cache V2 | `oh.semantic-cloud.v2` |
|
|
23
23
|
| Projection semantics | `oh.projection.positive-datalog.v1` |
|
|
24
|
-
| Composite memory | `
|
|
24
|
+
| Composite memory | `stable V1 authority over V2 query` |
|
|
25
25
|
| Memory page | `oh.memory-page.v1` |
|
|
26
26
|
|
|
27
27
|
## V1 documents
|
|
@@ -37,7 +37,7 @@ binds these versions:
|
|
|
37
37
|
- [Hosted semantic cache](v1/semantic-cloud.md)
|
|
38
38
|
- [Isolated hosted semantic cache V2](v2/semantic-cloud.md)
|
|
39
39
|
- [Derived projections](v1/projection.md)
|
|
40
|
-
- [
|
|
40
|
+
- [Composite agent memory](v1/memory.md)
|
|
41
41
|
- [Memory pages and `.oh.md` interchange](v1/memory-page.md)
|
|
42
42
|
- [Compatibility and migration](v1/migration.md)
|
|
43
43
|
|
package/spec/v1/memory.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Composite agent memory
|
|
2
2
|
|
|
3
|
-
`@hraness/oh/
|
|
4
|
-
|
|
5
|
-
ontology or a third storage authority.
|
|
3
|
+
`@hraness/oh/memory` is the stable consumer-facing composition of the store and
|
|
4
|
+
projection contracts. It is an application authority boundary, not a new
|
|
5
|
+
ontology or a third storage authority. The former
|
|
6
|
+
`@hraness/oh/experimental/memory` path remains an export-compatible alias.
|
|
6
7
|
|
|
7
8
|
## One kernel, two authorities
|
|
8
9
|
|
|
@@ -23,9 +24,13 @@ The host also binds the working actor, every named program's purpose, and every
|
|
|
23
24
|
named nomination route. None of those authority-bearing labels comes from
|
|
24
25
|
agent input.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
`createOhMemoryAuthorityV1` returns separate `agent` and `host` objects. The
|
|
28
|
+
agent exposes only `remember`, `query`, `explain`, and `nominate`. It has no
|
|
29
|
+
generic commit, store selection, path, sync, rule registration, canonical
|
|
30
|
+
write, adoption, rollover, or purge operation. The host object exposes only
|
|
31
|
+
serialized canonical-head rollover and reviewed nomination adoption. The
|
|
32
|
+
lower-level V1 and V2 agent factories remain available for hosts that already
|
|
33
|
+
own an equivalent control plane.
|
|
29
34
|
|
|
30
35
|
`remember` accepts only an expected working head, semantic puts and tombstones,
|
|
31
36
|
and an idempotency request ID. The facade supplies its host-bound actor, uses
|
|
@@ -70,9 +75,9 @@ silently canonical. Returned result, row, value, proof, source, and receipt
|
|
|
70
75
|
graphs are detached and deeply immutable, so a caller cannot mutate bytes after
|
|
71
76
|
their digest or explanation capability is issued.
|
|
72
77
|
|
|
73
|
-
##
|
|
78
|
+
## Parameterized pagination (V2)
|
|
74
79
|
|
|
75
|
-
`createOhMemoryAgentV2` is an additive
|
|
80
|
+
`createOhMemoryAgentV2` is an additive query surface. It does not
|
|
76
81
|
change a V1 request, result, digest preimage, factory, or type. Its `remember`
|
|
77
82
|
and `nominate` methods continue to use the V1 semantic-bundle and nomination
|
|
78
83
|
contracts. Only its `query` and `explain` envelopes use V2.
|
|
@@ -138,8 +143,15 @@ continuation and checks its program, binding, page-size, range, and alignment
|
|
|
138
143
|
before reading the working store, invoking extractors, evaluating rules, or
|
|
139
144
|
mapping proofs. Every valid continued call then rereads the current working
|
|
140
145
|
head and rebuilds the projection. A head, source, result, or row-count change
|
|
141
|
-
fails
|
|
142
|
-
|
|
146
|
+
fails before proof mapping rather than mixing pages from two snapshots.
|
|
147
|
+
|
|
148
|
+
`OhMemoryContinuationError` is the exact public discriminator for a supplied
|
|
149
|
+
cursor that cannot be decoded, authenticated, or rebound to the current exact
|
|
150
|
+
identity. It extends `OhIntegrityError`, carries
|
|
151
|
+
`code: "memory-continuation"`, and classifies `reason` as `encoding`,
|
|
152
|
+
`authentication`, or `identity`. Store verification, projection evaluation,
|
|
153
|
+
extractor, and other runtime failures retain their original error types; the
|
|
154
|
+
facade does not relabel them as continuation failures.
|
|
143
155
|
|
|
144
156
|
An outward result publishes `continuationSha256` beside the opaque token, or
|
|
145
157
|
`null` beside `null` on the final page. `resultSha256` commits that deterministic
|
|
@@ -169,6 +181,110 @@ canonical store, import a derived tuple, grant rights, record a review, or turn
|
|
|
169
181
|
a proposed assertion into reviewed knowledge. Destination-owned application
|
|
170
182
|
code must perform those steps under its own policy and compare-and-swap head.
|
|
171
183
|
|
|
184
|
+
## Stable host control
|
|
185
|
+
|
|
186
|
+
`createOhMemoryAuthorityV1` wraps the V2 agent and binds a separate adoption
|
|
187
|
+
actor. Both host methods accept `unknown`, require exact versioned envelopes,
|
|
188
|
+
and execute serially. A model-facing adapter MUST receive only `authority.agent`;
|
|
189
|
+
it MUST NOT receive `authority.host`, either physical store, or the authority
|
|
190
|
+
factory options.
|
|
191
|
+
|
|
192
|
+
`advanceCanonical` requires the current complete pinned head and a complete
|
|
193
|
+
next head. An equal next head returns an immutable `unchanged` receipt. A later
|
|
194
|
+
head is accepted only after the canonical change feed proves an uninterrupted
|
|
195
|
+
path from the current pin and an exact bounded snapshot reproduces the complete
|
|
196
|
+
next head. The returned `advanced` receipt binds the authority ID, binding
|
|
197
|
+
digest, prior head, and new head. A stale expected head, earlier head, missing
|
|
198
|
+
operation, fork, changed binding, malformed page, or snapshot mismatch fails
|
|
199
|
+
without changing the pin. One call proves at most 16,384 operations and at most
|
|
200
|
+
64 change-feed pages. The total operation bound is checked before the first
|
|
201
|
+
fetch; hosts MUST advance a longer reachable history in reviewed chunks.
|
|
202
|
+
|
|
203
|
+
Each query captures the current agent instance before its first asynchronous
|
|
204
|
+
read. A rollover therefore cannot mix canonical snapshots into an in-flight
|
|
205
|
+
query. All reconstructed agent generations share one explanation registry,
|
|
206
|
+
byte accounting, wall-clock guard, and monotonic-clock guard. An existing
|
|
207
|
+
explanation capability therefore continues to refer to its original result,
|
|
208
|
+
while the 256-entry and 64 MiB eviction limits and expiry order remain global
|
|
209
|
+
across rollover. Every reconstructed instance also uses the same private
|
|
210
|
+
continuation key. A continuation issued before rollover still authenticates,
|
|
211
|
+
then fails its exact memory-identity check if either source head changed.
|
|
212
|
+
|
|
213
|
+
`adoptNomination` requires an exact expected canonical head and a parsed
|
|
214
|
+
`OhMemoryNominationV1`. It verifies the host-registered nomination route,
|
|
215
|
+
destination purpose, working authority ID, and binding digest. It then asks the
|
|
216
|
+
actual bound working store to re-export the dependency closure at the
|
|
217
|
+
nomination's exact source head and requires byte equality with the proposal.
|
|
218
|
+
This prevents a detached, substituted, or stale capsule from becoming a write
|
|
219
|
+
request merely because its own digest is valid.
|
|
220
|
+
|
|
221
|
+
At the current canonical snapshot, an absent nominated record becomes a put and
|
|
222
|
+
an equal record digest is already present. A different digest remains a strict
|
|
223
|
+
conflict unless trusted host code supplies a `replacements` claim for that exact
|
|
224
|
+
logical key and exact prior canonical record digest. A request may carry at most
|
|
225
|
+
128 claims. Every claim has the exact keys `expectedPriorRecordSha256`, `key`,
|
|
226
|
+
and `v`; keys must be unique and must name a record in the verified nomination.
|
|
227
|
+
A missing, stale, or wrong claim for a record that still needs replacement is a
|
|
228
|
+
conflict. A claim for an absent canonical key is also a conflict rather than
|
|
229
|
+
permission to insert it. Every supplied claim is validated even when that key
|
|
230
|
+
is already at its nominated digest. An exact replay checks such claims against
|
|
231
|
+
the request's exact reviewed expected head, preserving idempotence without
|
|
232
|
+
silently accepting an invented or stale extra claim. A claim conflict reported
|
|
233
|
+
against an already-equal current record therefore carries equal canonical and
|
|
234
|
+
nominated digests. With no invalid claim, the record remains eligible for the
|
|
235
|
+
existing idempotent `already-present` reconciliation.
|
|
236
|
+
|
|
237
|
+
Any blocking conflict aborts every insert and replacement. Otherwise adoption
|
|
238
|
+
uses a deterministic operation ID derived from the host-bound adoption actor,
|
|
239
|
+
canonical binding, nomination digest, and exact prior complete head. The head
|
|
240
|
+
component preserves exact replay while allowing the same nomination to be
|
|
241
|
+
reviewed and adopted again after a later canonical overwrite or tombstone.
|
|
242
|
+
Before writing, it applies every
|
|
243
|
+
changed nominated record to the pinned snapshot and rejects a result over 8,192
|
|
244
|
+
records or 32 MiB. It then performs one compare-and-swap commit with no merge
|
|
245
|
+
retry. Replacement claims are host-side admission evidence, not new operation
|
|
246
|
+
fields: the existing operation binds the exact parent head and complete changed
|
|
247
|
+
records, so the persisted V1 graph and operation bytes need no new shape. After
|
|
248
|
+
the commit returns, the authority reads and proves the current physical head.
|
|
249
|
+
It installs the returned head only when that is still the physical head; a
|
|
250
|
+
reachable duplicate operation or later write is reconciled against the later
|
|
251
|
+
exact snapshot. A stale expected head or already-ahead physical head may return
|
|
252
|
+
`already-present` only when that current snapshot contains every nominated key
|
|
253
|
+
at its exact nominated digest. This remains one compare-and-swap commit;
|
|
254
|
+
reconciliation never retries it.
|
|
255
|
+
|
|
256
|
+
A host may set `maximumCanonicalOperationBytes` when creating the authority.
|
|
257
|
+
The SQLite commit constructs and hashes the exact operation, then rejects it
|
|
258
|
+
before persistence when its canonical UTF-8 bytes exceed that bound. This lets
|
|
259
|
+
an application guarantee that every admitted canonical operation fits its
|
|
260
|
+
future encrypted transport without shrinking the separate working-memory
|
|
261
|
+
limit. Exact idempotent replays remain subject to the same bound.
|
|
262
|
+
The pre-effect refusal is an `OhOperationSizeError`, a `RangeError` subtype
|
|
263
|
+
that exposes the exact operation bytes and configured maximum so a host can
|
|
264
|
+
distinguish it from an ambiguous post-effect failure.
|
|
265
|
+
|
|
266
|
+
An adoption conflict reports the expected and actual complete heads, total
|
|
267
|
+
conflict count, whether the outward list was truncated, and at most 128 entries
|
|
268
|
+
sorted by key. Each entry carries the nominated digest and the current
|
|
269
|
+
canonical digest or `null` when the key is absent. `conflictsSha256` commits the
|
|
270
|
+
complete sorted conflict set, including entries beyond the outward bound. The
|
|
271
|
+
structured conflict is deeply immutable. Conflict handling and stale
|
|
272
|
+
idempotency never overwrite a canonical record. The optional replacement path
|
|
273
|
+
does not perform last-write-wins reconciliation: only the exact reviewed head
|
|
274
|
+
and exact per-key prior digests authorize the single compare-and-swap.
|
|
275
|
+
|
|
276
|
+
Every `unknown` request on the stable agent and host surfaces is recursively
|
|
277
|
+
detached through enumerable data-property descriptors once at method entry.
|
|
278
|
+
Accessors are never invoked; symbols, non-data properties, sparse arrays,
|
|
279
|
+
non-JSON values, and proxies fail closed. Validation, canonical byte bounds,
|
|
280
|
+
digests, and execution all consume that same frozen detached graph. Bound store
|
|
281
|
+
responses are detached under the same rule before they are parsed or compared.
|
|
282
|
+
Traversal admits at most 128 nested levels, 65,536 entries in one container,
|
|
283
|
+
and 1,048,576 value nodes overall. It counts canonical UTF-8 bytes as it
|
|
284
|
+
traverses and rejects an over-bound array before enumerating or cloning its
|
|
285
|
+
entries; final canonical serialization must reproduce the incremental byte
|
|
286
|
+
count.
|
|
287
|
+
|
|
172
288
|
## Memory pages and retrieval
|
|
173
289
|
|
|
174
290
|
A memory page is an application profile for an ordinary `edition` record. Its
|
|
@@ -200,11 +316,13 @@ API boundary.
|
|
|
200
316
|
The facade requests at most 8,192 records per lane, rejects a lane snapshot
|
|
201
317
|
over 32 MiB, rejects a remember request over 8 MiB, bounds extractor
|
|
202
318
|
invocations and emitted facts, limits the public result to 32 MiB, and retains
|
|
203
|
-
at most 64 MiB of explanation evidence
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
319
|
+
at most 64 MiB of explanation evidence across every canonical generation. The
|
|
320
|
+
same record and byte ceilings are applied to a prospective adoption snapshot
|
|
321
|
+
before its only compare-and-swap. A trusted store still constructs the snapshot
|
|
322
|
+
before returning it, and a trusted synchronous fact extractor can consume time
|
|
323
|
+
or temporary memory before it returns. Provider response limits, host storage
|
|
324
|
+
quotas, callback review, isolation, deadlines, and cancellation remain
|
|
325
|
+
application responsibilities.
|
|
208
326
|
|
|
209
327
|
Suss is an optional differential evaluator behind the separate projection
|
|
210
328
|
compatibility subpath. The memory facade uses the package-owned bounded
|
package/spec/v1/storage.md
CHANGED
|
@@ -50,7 +50,9 @@ application-profile digest, and declared capabilities. A supported runtime
|
|
|
50
50
|
MUST reject a later attempt to open the same space under different binding
|
|
51
51
|
bytes. A working profile disables operation replication and enables only
|
|
52
52
|
host-controlled whole-space purge. A canonical profile cannot be purged by
|
|
53
|
-
that API.
|
|
53
|
+
that API. Its local SQLite authority may instead expose a canonical-only
|
|
54
|
+
replication handle under host control; the promise-based agent store never
|
|
55
|
+
carries that capability.
|
|
54
56
|
|
|
55
57
|
Purge deletes the space head, complete operation history, current records,
|
|
56
58
|
dependency and operation materializations, sync state, and derived keyword
|
|
@@ -78,10 +80,11 @@ before it is returned as current.
|
|
|
78
80
|
|
|
79
81
|
## Replay verification
|
|
80
82
|
|
|
81
|
-
`oh verify` runs SQLite `integrity_check`, parses every
|
|
82
|
-
replays the operation chain from an empty graph,
|
|
83
|
-
revision digests, checks dependencies, compares
|
|
84
|
-
|
|
83
|
+
`oh verify` runs SQLite `integrity_check` and `foreign_key_check`, parses every
|
|
84
|
+
canonical operation, replays the operation chain from an empty graph,
|
|
85
|
+
recomputes record-set and graph revision digests, checks dependencies, compares
|
|
86
|
+
the materialized records, operation-record rows, search documents, and FTS rows,
|
|
87
|
+
and requires the reconstructed head to equal the stored head.
|
|
85
88
|
|
|
86
89
|
Backup and restore procedures SHOULD preserve the database and WAL atomically.
|
|
87
90
|
An application SHOULD run replay verification after an untrusted transfer or
|
package/spec/v1/store.md
CHANGED
|
@@ -21,6 +21,26 @@ A historical read MUST fail if its sequence is absent or identifies a
|
|
|
21
21
|
different operation digest. A change page MUST name its source cursor, pinned
|
|
22
22
|
through-head, returned cursor, and whether more operations remain.
|
|
23
23
|
|
|
24
|
+
A commit may declare `maximumOperationBytes`. The built-in SQLite and direct
|
|
25
|
+
libSQL authorities measure the exact canonical operation before persistence,
|
|
26
|
+
including on an exact operation-ID replay. Exceeding the host-declared bound
|
|
27
|
+
throws `OhOperationSizeError`, a `RangeError` subtype carrying
|
|
28
|
+
`operationBytes` and `maximumOperationBytes`; it never indicates an ambiguous
|
|
29
|
+
post-effect failure. Its public `code` is `oh.operation-size.v1`, and
|
|
30
|
+
`instanceof OhOperationSizeError` remains stable across separately bundled Oh
|
|
31
|
+
entrypoints. Callers validating an unknown caught value may instead use
|
|
32
|
+
`isOhOperationSizeError`; the guard requires a native error with immutable
|
|
33
|
+
branded numeric fields, so copying the public fields onto a plain object is not
|
|
34
|
+
sufficient.
|
|
35
|
+
|
|
36
|
+
The core `OhConflictError`, `OhIntegrityError`, `OhDependencyError`, and
|
|
37
|
+
`OhProfileError` classes likewise preserve `instanceof` identity across
|
|
38
|
+
separately bundled Oh entrypoints. Their corresponding `isOhConflictError`,
|
|
39
|
+
`isOhIntegrityError`, `isOhDependencyError`, and `isOhProfileError` guards
|
|
40
|
+
require a native error with the immutable brand for that exact error family.
|
|
41
|
+
A branded base class remains visible through an ordinary subclass, but a base
|
|
42
|
+
instance MUST NOT satisfy an arbitrary subclass check.
|
|
43
|
+
|
|
24
44
|
## Semantic bundle ingress
|
|
25
45
|
|
|
26
46
|
Model-facing code SHOULD use `OhSemanticBundleIngressV1` instead of generic
|
package/spec/v1/sync.md
CHANGED
|
@@ -25,11 +25,59 @@ interface OhOperationSyncTransportV1 {
|
|
|
25
25
|
|
|
26
26
|
A bundle binds `oh.sync.v1`, the contract digest, one space ID, at most 1,000
|
|
27
27
|
ordered operations, and its own digest. Operations MUST form one contiguous
|
|
28
|
-
chain. An empty bundle is valid.
|
|
28
|
+
chain. An empty bundle is valid. The complete canonical bundle is limited to
|
|
29
|
+
64 MiB plus 4 KiB of envelope and operation-separator allowance, and to the
|
|
30
|
+
equivalent finite JSON-node budget. Every operation, including its top-level
|
|
31
|
+
strings, dependencies, and record values, consumes those shared limits; compact
|
|
32
|
+
in-process graphs with repeated references do not multiply into unbounded
|
|
33
|
+
detached data. `OH_SYNC_BUNDLE_MAX_BYTES_V1` exposes the canonical byte limit.
|
|
34
|
+
|
|
35
|
+
The stock synchronizer selects the largest nonempty local prefix that fits
|
|
36
|
+
these shared limits, even when the requested operation-count page is larger.
|
|
37
|
+
The libSQL adapter applies the same cumulative canonical-byte predicate in its
|
|
38
|
+
ordered remote query. A valid history larger than one bundle therefore advances
|
|
39
|
+
in bounded pages instead of becoming permanently unsynchronizable. Host and
|
|
40
|
+
offline exporters can request the same behavior with
|
|
41
|
+
`createOhSyncBundleV1(spaceId, operations, { largestFittingPrefix: true })`.
|
|
29
42
|
|
|
30
43
|
Parsing recomputes every operation digest and the bundle digest. Import also
|
|
31
44
|
requires the bundle space to equal the selected local space and each operation
|
|
32
|
-
to extend the exact local head.
|
|
45
|
+
to extend the exact local head. The SQLite host replication handle applies a
|
|
46
|
+
complete bundle in one `BEGIN IMMEDIATE` transaction. If any later operation
|
|
47
|
+
fails semantic replay, no valid prefix is retained. An exact bundle already on
|
|
48
|
+
the current authority chain is an idempotent replay.
|
|
49
|
+
|
|
50
|
+
`parseOhSyncHeadV1` is the strict ingress for untrusted transport heads. It
|
|
51
|
+
requires only `operationSha256`, `sequence`, and `v`, and enforces that sequence
|
|
52
|
+
zero is the only null-digest head. `parseOhSyncHeadRefV1` applies the same
|
|
53
|
+
descriptor-safe rules to an exact two-field host cursor. Both reject negative
|
|
54
|
+
zero, accessors, proxies, and extra or hidden properties without invoking
|
|
55
|
+
caller code. Bundle parsing checks exact envelopes and cheap collection limits
|
|
56
|
+
before recursively detaching values; it rejects operations that parsing would
|
|
57
|
+
otherwise normalize to different canonical JSON. Record-value detachment is
|
|
58
|
+
limited to 128 nested levels, 1,048,576 traversed value nodes, and the graph
|
|
59
|
+
record's 1 MiB canonical value ceiling. The parser also applies the 64 MiB
|
|
60
|
+
canonical operation ceiling and an equivalent finite operation-node budget.
|
|
61
|
+
Canonical string and container bytes are counted during traversal, and an
|
|
62
|
+
array whose length cannot fit the remaining budget is rejected before its
|
|
63
|
+
entries are enumerated or cloned. Shared object references consume the budget
|
|
64
|
+
again for every canonical occurrence. Dependencies are preflighted under the
|
|
65
|
+
same operation budget before any nested collection is cloned.
|
|
66
|
+
|
|
67
|
+
## Host-controlled SQLite replication
|
|
68
|
+
|
|
69
|
+
`createOhSqliteStoreAuthorityV1` keeps canonical replication on
|
|
70
|
+
`authority.host.replication`; it is never a method on the agent-safe
|
|
71
|
+
`OhStoreV1`. Working profiles receive no replication handle. The canonical
|
|
72
|
+
handle exposes the exact binding, current head, pinned bundle export, and
|
|
73
|
+
atomic bundle import.
|
|
74
|
+
Import requires an exact strict head reference before parsing the bundle.
|
|
75
|
+
|
|
76
|
+
Pinned export delegates to `changesSince(from, { limit, through })` and returns
|
|
77
|
+
the page's exact `from`, `to`, `through`, and `hasMore` evidence with the
|
|
78
|
+
canonical bundle. A writer that advances after `through` therefore cannot
|
|
79
|
+
silently enlarge the exported interval. Applications SHOULD persist that
|
|
80
|
+
bounded evidence before releasing local custody for network I/O.
|
|
33
81
|
|
|
34
82
|
## Settlement
|
|
35
83
|
|
|
@@ -42,9 +90,18 @@ compares heads:
|
|
|
42
90
|
- Equal sequences with different digests, or any non-extending chain, are a
|
|
43
91
|
conflict.
|
|
44
92
|
|
|
93
|
+
A pull may not exceed the requested batch size. A page that reaches the
|
|
94
|
+
observed remote sequence must end at that exact operation digest, and is
|
|
95
|
+
rejected before import otherwise. A terminal pull or acknowledged terminal
|
|
96
|
+
push settles in that same round after fresh local- and remote-head checks;
|
|
97
|
+
callers do not need to reserve an extra observation round in `maximumRounds`.
|
|
98
|
+
|
|
45
99
|
The synchronizer never performs last-write-wins merging. Divergent append-only
|
|
46
100
|
histories remain intact for an explicit domain merge. Remote acknowledgments
|
|
47
|
-
must equal the final pushed operation.
|
|
101
|
+
must equal the final pushed operation. The stock synchronizer is convenient for
|
|
102
|
+
simple transports and keeps the concrete store open while awaiting the network.
|
|
103
|
+
Applications that journal uncertain effects SHOULD instead split capture,
|
|
104
|
+
network exchange, and atomic import into separately revalidated host phases.
|
|
48
105
|
|
|
49
106
|
## libSQL and Turso seam
|
|
50
107
|
|
|
@@ -56,12 +113,24 @@ methods compatible with `@libsql/client`. It creates only:
|
|
|
56
113
|
|
|
57
114
|
The application supplies the client, credentials, endpoint, access policy,
|
|
58
115
|
retry policy, and backups. The adapter parameterizes values and verifies
|
|
59
|
-
canonical operation JSON on pull.
|
|
60
|
-
|
|
116
|
+
canonical operation JSON on pull. Pull cursors are nonnegative, pull limits are
|
|
117
|
+
1 through 1,000, a provider response may not exceed its requested row count,
|
|
118
|
+
each raw operation JSON string is byte-bounded before parsing, and the ordered
|
|
119
|
+
query returns only the largest prefix within the shared bundle byte ceiling.
|
|
120
|
+
Semantic documents and vectors never cross this seam.
|
|
121
|
+
|
|
122
|
+
Push retries are idempotent even if another writer has advanced the remote
|
|
123
|
+
past the submitted tail. The adapter reads at most the submitted row count,
|
|
124
|
+
requires every submitted sequence, digest, and canonical operation JSON to
|
|
125
|
+
match the immutable remote rows, and acknowledges the submitted tail rather
|
|
126
|
+
than the newer remote head. A missing or changed row fails closed.
|
|
61
127
|
|
|
62
128
|
## Offline transfer
|
|
63
129
|
|
|
64
130
|
`oh sync export --after <sequence> --limit <count>` emits one canonical bundle.
|
|
65
|
-
`oh sync import --file <path>` verifies and applies
|
|
66
|
-
|
|
67
|
-
|
|
131
|
+
`oh sync import --file <path>` verifies and applies the complete bundle
|
|
132
|
+
idempotently in one atomic import; an invalid later operation cannot leave a
|
|
133
|
+
valid prefix behind. Before reading, the CLI requires a regular file no larger
|
|
134
|
+
than the canonical bundle limit plus its one exported terminal LF, and checks
|
|
135
|
+
the actual bytes again after the read. A transfer process SHOULD preserve the
|
|
136
|
+
exact bytes and SHOULD run `oh verify` after the final import.
|
package/src/cli.test.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, rm, truncate, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
|
|
7
|
+
import { canonicalJson, canonicalSha256 } from "./canonical";
|
|
8
|
+
import { createKnowledgeGraphRecordV1 } from "./graph";
|
|
9
|
+
import { createOhOperationV1 } from "./operation";
|
|
10
|
+
import { OhSqliteStore } from "./sqlite/store";
|
|
11
|
+
import { createOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_BYTES_V1 } from "./sync";
|
|
12
|
+
|
|
7
13
|
const roots: string[] = [];
|
|
8
14
|
const CLI_PATH = join(import.meta.dir, "cli.ts");
|
|
9
15
|
const REPOSITORY_ROOT = join(import.meta.dir, "..");
|
|
@@ -100,4 +106,50 @@ describe("oh CLI", () => {
|
|
|
100
106
|
expect((await run(["contract", "--db", database, "--space", "contract.test"], root)).code).toBe(0);
|
|
101
107
|
expect(existsSync(database)).toBe(false);
|
|
102
108
|
});
|
|
109
|
+
|
|
110
|
+
test("imports a sync bundle atomically when a later operation is invalid", async () => {
|
|
111
|
+
const root = await mkdtemp(join(tmpdir(), "oh-cli-atomic-import-test-"));
|
|
112
|
+
roots.push(root);
|
|
113
|
+
const database = join(root, "target.sqlite");
|
|
114
|
+
const bundlePath = join(root, "hostile-bundle.json");
|
|
115
|
+
const source = new OhSqliteStore({ path: ":memory:" });
|
|
116
|
+
const entity = (key: string, name: string) => createKnowledgeGraphRecordV1({
|
|
117
|
+
dependencies: [], key, kind: "entity", v: 1, value: { name },
|
|
118
|
+
});
|
|
119
|
+
source.commit({ actorId: "agent.test", changes: [{ kind: "put",
|
|
120
|
+
record: entity("entity:first", "First"), v: 1 }], expectedHead: source.head(),
|
|
121
|
+
operationId: "op_first" });
|
|
122
|
+
source.commit({ actorId: "agent.test", changes: [{ kind: "put",
|
|
123
|
+
record: entity("entity:second", "Second"), v: 1 }], expectedHead: source.head(),
|
|
124
|
+
operationId: "op_second" });
|
|
125
|
+
const [first, second] = source.exportOperations();
|
|
126
|
+
if (first === undefined || second === undefined) throw new Error("Expected two operations.");
|
|
127
|
+
const { operationSha256: _operationSha256, ...payload } = second;
|
|
128
|
+
const hostileSecond = createOhOperationV1({ ...payload,
|
|
129
|
+
graphRevisionSha256: canonicalSha256("hostile graph revision"),
|
|
130
|
+
recordsSha256: canonicalSha256("hostile records") });
|
|
131
|
+
await writeFile(bundlePath,
|
|
132
|
+
canonicalJson(createOhSyncBundleV1(source.spaceId, [first, hostileSecond])), "utf8");
|
|
133
|
+
source.close();
|
|
134
|
+
|
|
135
|
+
const imported = await run(["sync", "import", "--db", database, "--file", bundlePath]);
|
|
136
|
+
expect(imported.code).toBe(1);
|
|
137
|
+
expect(imported.stderr).toContain("does not reproduce");
|
|
138
|
+
const verified = await run(["verify", "--db", database]);
|
|
139
|
+
expect(verified.code).toBe(0);
|
|
140
|
+
expect(JSON.parse(verified.stdout)).toMatchObject({ operations: 0, records: 0 });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("rejects an oversized sparse sync bundle before opening the database", async () => {
|
|
144
|
+
const root = await mkdtemp(join(tmpdir(), "oh-cli-bounded-import-test-"));
|
|
145
|
+
roots.push(root);
|
|
146
|
+
const database = join(root, "target.sqlite");
|
|
147
|
+
const bundlePath = join(root, "oversized-bundle.json");
|
|
148
|
+
await writeFile(bundlePath, "", "utf8");
|
|
149
|
+
await truncate(bundlePath, OH_SYNC_BUNDLE_MAX_BYTES_V1 + 2);
|
|
150
|
+
const imported = await run(["sync", "import", "--db", database, "--file", bundlePath]);
|
|
151
|
+
expect(imported.code).toBe(1);
|
|
152
|
+
expect(imported.stderr).toContain("regular file of at most");
|
|
153
|
+
expect(existsSync(database)).toBe(false);
|
|
154
|
+
});
|
|
103
155
|
});
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { canonicalJson, opaqueId, safeCode, type JsonValue } from "./canonical";
|
|
5
5
|
import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
|
|
@@ -7,9 +7,9 @@ import { OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, createKnowledgeGraphRecordV1,
|
|
|
7
7
|
type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "./graph";
|
|
8
8
|
import { Oh } from "./sdk";
|
|
9
9
|
import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations";
|
|
10
|
-
import { createOhSyncBundleV1, parseOhSyncBundleV1 } from "./sync";
|
|
10
|
+
import { createOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_BYTES_V1, parseOhSyncBundleV1 } from "./sync";
|
|
11
11
|
|
|
12
|
-
export const OH_PACKAGE_VERSION = "0.
|
|
12
|
+
export const OH_PACKAGE_VERSION = "0.4.1" as const;
|
|
13
13
|
|
|
14
14
|
type ParsedArguments = { options: Map<string, string[]>; positionals: string[] };
|
|
15
15
|
type ValidatedInvocation = Readonly<{
|
|
@@ -174,7 +174,7 @@ async function validateInvocation(command: string, parsed: ParsedArguments): Pro
|
|
|
174
174
|
assertAllowedOptions(parsed, [...GLOBAL_OPTIONS, "file"]);
|
|
175
175
|
const file = one(parsed, "file");
|
|
176
176
|
if (file === undefined) throw new TypeError("sync import needs --file.");
|
|
177
|
-
syncBundle = parseOhSyncBundleV1(
|
|
177
|
+
syncBundle = parseOhSyncBundleV1(await readSyncBundleFile(file));
|
|
178
178
|
if (syncBundle === null) throw new TypeError("Invalid sync bundle.");
|
|
179
179
|
} else {
|
|
180
180
|
throw new TypeError("sync needs export or import.");
|
|
@@ -191,6 +191,21 @@ async function validateInvocation(command: string, parsed: ParsedArguments): Pro
|
|
|
191
191
|
|
|
192
192
|
function print(value: unknown): void { process.stdout.write(`${canonicalJson(value)}\n`); }
|
|
193
193
|
|
|
194
|
+
async function readSyncBundleFile(path: string): Promise<unknown> {
|
|
195
|
+
// `sync export` writes one terminal LF after the bounded canonical bundle.
|
|
196
|
+
const maximumFileBytes = OH_SYNC_BUNDLE_MAX_BYTES_V1 + 1;
|
|
197
|
+
const metadata = await lstat(path);
|
|
198
|
+
if (!metadata.isFile() || !Number.isSafeInteger(metadata.size)
|
|
199
|
+
|| metadata.size > maximumFileBytes) {
|
|
200
|
+
throw new RangeError(`Sync bundle file must be a regular file of at most ${maximumFileBytes} bytes.`);
|
|
201
|
+
}
|
|
202
|
+
const contents = await readFile(path);
|
|
203
|
+
if (contents.byteLength > maximumFileBytes) {
|
|
204
|
+
throw new RangeError(`Sync bundle file must be at most ${maximumFileBytes} bytes.`);
|
|
205
|
+
}
|
|
206
|
+
return JSON.parse(contents.toString("utf8")) as unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
194
209
|
const HELP = `oh ${OH_PACKAGE_VERSION}
|
|
195
210
|
|
|
196
211
|
Usage:
|
|
@@ -283,14 +298,25 @@ export async function runOhCli(arguments_: readonly string[]): Promise<number> {
|
|
|
283
298
|
if (action === "export") {
|
|
284
299
|
const after = integer(one(parsed, "after"), "after") ?? 0;
|
|
285
300
|
const limit = integer(one(parsed, "limit"), "limit") ?? 1000;
|
|
286
|
-
print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit)
|
|
301
|
+
print(createOhSyncBundleV1(oh.store.spaceId, oh.store.exportOperations(after, limit), {
|
|
302
|
+
largestFittingPrefix: true,
|
|
303
|
+
})); return 0;
|
|
287
304
|
}
|
|
288
305
|
if (action === "import") {
|
|
289
306
|
const bundle = validated.syncBundle;
|
|
290
307
|
if (bundle === null) throw new TypeError("Invalid prepared sync import command.");
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
308
|
+
const first = bundle.operations[0];
|
|
309
|
+
if (first === undefined) {
|
|
310
|
+
print({ head: oh.head(), imported: 0, v: 1 }); return 0;
|
|
311
|
+
}
|
|
312
|
+
const imported = oh.store.importOperations({
|
|
313
|
+
expectedHead: {
|
|
314
|
+
operationSha256: first.parentOperationSha256,
|
|
315
|
+
sequence: first.sequence - 1,
|
|
316
|
+
},
|
|
317
|
+
operations: bundle.operations,
|
|
318
|
+
});
|
|
319
|
+
print({ head: imported.head, imported: imported.imported, v: 1 }); return 0;
|
|
294
320
|
}
|
|
295
321
|
throw new TypeError("sync needs export or import.");
|
|
296
322
|
}
|