@omgbase/sync 0.4.11 → 0.5.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
CHANGED
|
@@ -39,14 +39,14 @@ What it does, in order: spawn `--server` (split on whitespace) as a stdio MCP se
|
|
|
39
39
|
|
|
40
40
|
All registered in `packages/core/src/mcp/server.ts`; every one takes an optional `repo` slug.
|
|
41
41
|
|
|
42
|
-
- **`observe_many({files:[{path,content}]})`** → one result per file: `{
|
|
43
|
-
- **`observe_delete({path})`** → `{
|
|
44
|
-
- **`changes_since({cursor?, origin?, limit?})`** → `{digests, cursor, truncated, head}`. The repo-wide commit feed after a commit `seq`; each digest is `{commit, seq, ts, origin, actor, summary, revisions:[{
|
|
42
|
+
- **`observe_many({files:[{path,content}]})`** → one result per file: `{id, path, rev, commitId, converged, echo, conflicted, dispositions}` (surface 3.0; `docId`/`path` before). Bytes are committed as an **observed**-origin revision (a write *around* the engine, as a human edit is recorded) with block identity threaded by the reconciler. If `sha256(content)` already equals the doc's stored `file_hash`, it is an **echo**: `echo:true`, `rev:null`, no commit. Bytes containing git conflict markers are still ingested and flagged `conflicted:true`. The batch shares one timestamp and one resurrection-pool sweep. (`observe` is the single-file form; the coordinator uses the batch.)
|
|
43
|
+
- **`observe_delete({path})`** → `{id, path, deleted}`. Tombstones the live doc as an observed deletion (blocks pooled for resurrection, no file removed). Idempotent.
|
|
44
|
+
- **`changes_since({cursor?, origin?, limit?})`** → `{digests, cursor, truncated, head}`. The repo-wide commit feed after a commit `seq`; each digest is `{commit, seq, ts, origin, actor, summary, revisions:[{id, path, hash}]}` (sync 2.0; `{doc, path, contentHash}` before) where `hash` is the hex of that revision's rendered file hash — enough for a client to tell "changed vs echo" without a follow-up read. `head` is the repo's current max seq (cursor > head means the cursor came from another repo).
|
|
45
45
|
- **`docs_read({path})`** — used by `readDoc()` in the export direction to get a doc's current byte-exact content.
|
|
46
46
|
|
|
47
47
|
### Loop safety and echo suppression
|
|
48
48
|
|
|
49
|
-
A round-trip source → engine → source → engine terminates because the engine's echo gate makes ingest idempotent: re-observing bytes the engine already holds produces no commit. `syncOut()` adds a coarse filter on top — it **skips every `observed`-origin digest**, since those came from a source and there is nothing to push back — leaving only `api`/`import` (engine-authored) changes to write out. The `
|
|
49
|
+
A round-trip source → engine → source → engine terminates because the engine's echo gate makes ingest idempotent: re-observing bytes the engine already holds produces no commit. `syncOut()` adds a coarse filter on top — it **skips every `observed`-origin digest**, since those came from a source and there is nothing to push back — leaving only `api`/`import` (engine-authored) changes to write out. The `hash` in the feed (and `contentHash` in `readDoc()`'s own `EngineClient` type) is carried for finer per-path suppression but the v1 coordinator does not yet compare it; the durable per-path hash store (`sync_state`) is the deferred piece below.
|
|
50
50
|
|
|
51
51
|
## The identity rule
|
|
52
52
|
|
package/dist/src/coordinator.js
CHANGED
|
@@ -65,7 +65,8 @@ export class Coordinator {
|
|
|
65
65
|
if (digest.origin === "observed")
|
|
66
66
|
continue; // came from a source; don't echo back
|
|
67
67
|
for (const rev of digest.revisions) {
|
|
68
|
-
// `changes_since` reports the reference form
|
|
68
|
+
// `changes_since` reports the reference form (`path`, spec/surface
|
|
69
|
+
// §1.2); the source takes storage paths.
|
|
69
70
|
const path = storagePath(rev.path);
|
|
70
71
|
const doc = await this.engine.readDoc(path);
|
|
71
72
|
if (doc) {
|
|
@@ -8,7 +8,7 @@ export interface ChangesPage {
|
|
|
8
8
|
}
|
|
9
9
|
export interface DocBytes {
|
|
10
10
|
content: string;
|
|
11
|
-
/** hex of the doc's current rendered/file hash (matches changes_since
|
|
11
|
+
/** hex of the doc's current rendered/file hash (matches a changes_since revision's `hash`). */
|
|
12
12
|
contentHash: string;
|
|
13
13
|
}
|
|
14
14
|
export interface EngineClient {
|
|
@@ -30,7 +30,7 @@ export class InProcessEngineClient {
|
|
|
30
30
|
const res = docsRead(this.store, row.doc_id);
|
|
31
31
|
if (!res)
|
|
32
32
|
return Promise.resolve(null);
|
|
33
|
-
return Promise.resolve({ content: res.
|
|
33
|
+
return Promise.resolve({ content: res.raw, contentHash: row.file_hash?.toString("hex") ?? "" });
|
|
34
34
|
}
|
|
35
35
|
close() {
|
|
36
36
|
// The store's lifetime is owned by the caller (it may serve other work).
|
|
@@ -28,11 +28,15 @@ export class McpEngineClient {
|
|
|
28
28
|
}
|
|
29
29
|
return body;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// The wire spells a document's identity `id`/`path` (spec/surface §1.2,
|
|
32
|
+
// 3.0); the engine-client contract keeps the library's `docId`/`path`
|
|
33
|
+
// (spec/store's observe outcome), so the two are mapped back here.
|
|
34
|
+
async observeMany(files) {
|
|
35
|
+
const out = await this.callTool("observe_many", { files });
|
|
36
|
+
return out.map(fromWireObserve);
|
|
33
37
|
}
|
|
34
|
-
observeDelete(path) {
|
|
35
|
-
return this.callTool("observe_delete", { path });
|
|
38
|
+
async observeDelete(path) {
|
|
39
|
+
return fromWireObserve(await this.callTool("observe_delete", { path }));
|
|
36
40
|
}
|
|
37
41
|
changesSince(cursor, opts) {
|
|
38
42
|
return this.callTool("changes_since", {
|
|
@@ -44,7 +48,7 @@ export class McpEngineClient {
|
|
|
44
48
|
async readDoc(path) {
|
|
45
49
|
try {
|
|
46
50
|
const res = await this.callTool("docs_read", { path });
|
|
47
|
-
return { content: res.
|
|
51
|
+
return { content: res.raw, contentHash: createHash("sha256").update(res.raw, "utf8").digest("hex") };
|
|
48
52
|
}
|
|
49
53
|
catch {
|
|
50
54
|
// doc_missing (or any read failure) → treat as absent.
|
|
@@ -55,6 +59,11 @@ export class McpEngineClient {
|
|
|
55
59
|
await this.client.close();
|
|
56
60
|
}
|
|
57
61
|
}
|
|
62
|
+
/** A wire observe outcome (`{ id, path, … }`) as the library's `{ docId, path, … }`. */
|
|
63
|
+
function fromWireObserve(w) {
|
|
64
|
+
const { id, path, ...rest } = w;
|
|
65
|
+
return { docId: id, path: path, ...rest };
|
|
66
|
+
}
|
|
58
67
|
/** Connect to an omgbase MCP server over stdio by spawning `command args`
|
|
59
68
|
* (e.g. `omg mcp -C /vault`). Returns a ready EngineClient. */
|
|
60
69
|
export async function connectStdioEngine(spec) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omgbase/sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Open Markdown Graph Base — standalone store-to-store synchronizer: a coordinator over an EngineClient seam (in-process or MCP-client) and source adapters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
56
|
-
"@omgbase/core": "^0.
|
|
56
|
+
"@omgbase/core": "^0.11.1",
|
|
57
57
|
"@omgbase/fs-adapter": "^0.2.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|