@osmosis-ai/sync 0.1.0

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.
@@ -0,0 +1,10 @@
1
+ export interface SyncConfig {
2
+ /** URL of the Osmosis mesh server */
3
+ meshUrl: string;
4
+ /** Auto-sync interval in ms (default: 5 min) */
5
+ syncIntervalMs: number;
6
+ /** Enable periodic sync */
7
+ autoSync: boolean;
8
+ }
9
+ export declare const DEFAULT_SYNC_CONFIG: SyncConfig;
10
+ export declare function resolveSyncConfig(partial?: Partial<SyncConfig>): SyncConfig;
package/dist/config.js ADDED
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_SYNC_CONFIG = {
2
+ meshUrl: 'https://mesh.osmosis.dev',
3
+ syncIntervalMs: 5 * 60 * 1000,
4
+ autoSync: false,
5
+ };
6
+ export function resolveSyncConfig(partial) {
7
+ return { ...DEFAULT_SYNC_CONFIG, ...partial };
8
+ }
9
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,mBAAmB,GAAe;IAC7C,OAAO,EAAE,0BAA0B;IACnC,cAAc,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI;IAC7B,QAAQ,EAAE,KAAK;CAChB,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,OAA6B;IAC7D,OAAO,EAAE,GAAG,mBAAmB,EAAE,GAAG,OAAO,EAAE,CAAC;AAChD,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { contributeTo, pushAtoms } from './push.js';
2
+ export { learnFrom, pullAtoms } from './pull.js';
3
+ export { syncWithMesh, syncWithPeer } from './sync.js';
4
+ export { createSyncServer } from './server.js';
5
+ export { startAutoSync } from './scheduler.js';
6
+ export type { AutoSyncHandle } from './scheduler.js';
7
+ export { resolveSyncConfig, DEFAULT_SYNC_CONFIG } from './config.js';
8
+ export type { SyncConfig } from './config.js';
9
+ export type { SyncResult } from './types.js';
10
+ export { getAllPeers } from './meta.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { contributeTo, pushAtoms } from './push.js';
2
+ export { learnFrom, pullAtoms } from './pull.js';
3
+ export { syncWithMesh, syncWithPeer } from './sync.js';
4
+ export { createSyncServer } from './server.js';
5
+ export { startAutoSync } from './scheduler.js';
6
+ export { resolveSyncConfig, DEFAULT_SYNC_CONFIG } from './config.js';
7
+ export { getAllPeers } from './meta.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
package/dist/meta.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ export declare function getLastPushAt(store: AtomStore, peerUrl: string): string | null;
3
+ export declare function getLastPullAt(store: AtomStore, peerUrl: string): string | null;
4
+ export declare function setLastPushAt(store: AtomStore, peerUrl: string, ts: string): void;
5
+ export declare function setLastPullAt(store: AtomStore, peerUrl: string, ts: string): void;
6
+ export declare function getAllPeers(store: AtomStore): Array<{
7
+ peer_url: string;
8
+ last_push_at: string | null;
9
+ last_pull_at: string | null;
10
+ }>;
package/dist/meta.js ADDED
@@ -0,0 +1,45 @@
1
+ const SYNC_META_SCHEMA = `
2
+ CREATE TABLE IF NOT EXISTS sync_meta (
3
+ peer_url TEXT PRIMARY KEY,
4
+ last_push_at TEXT,
5
+ last_pull_at TEXT
6
+ );
7
+ `;
8
+ function ensureTable(store) {
9
+ const db = store.db;
10
+ db.exec(SYNC_META_SCHEMA);
11
+ }
12
+ export function getLastPushAt(store, peerUrl) {
13
+ ensureTable(store);
14
+ const db = store.db;
15
+ const row = db.prepare('SELECT last_push_at FROM sync_meta WHERE peer_url = ?').get(peerUrl);
16
+ return row?.last_push_at ?? null;
17
+ }
18
+ export function getLastPullAt(store, peerUrl) {
19
+ ensureTable(store);
20
+ const db = store.db;
21
+ const row = db.prepare('SELECT last_pull_at FROM sync_meta WHERE peer_url = ?').get(peerUrl);
22
+ return row?.last_pull_at ?? null;
23
+ }
24
+ export function setLastPushAt(store, peerUrl, ts) {
25
+ ensureTable(store);
26
+ const db = store.db;
27
+ db.prepare(`
28
+ INSERT INTO sync_meta (peer_url, last_push_at) VALUES (?, ?)
29
+ ON CONFLICT(peer_url) DO UPDATE SET last_push_at = excluded.last_push_at
30
+ `).run(peerUrl, ts);
31
+ }
32
+ export function setLastPullAt(store, peerUrl, ts) {
33
+ ensureTable(store);
34
+ const db = store.db;
35
+ db.prepare(`
36
+ INSERT INTO sync_meta (peer_url, last_pull_at) VALUES (?, ?)
37
+ ON CONFLICT(peer_url) DO UPDATE SET last_pull_at = excluded.last_pull_at
38
+ `).run(peerUrl, ts);
39
+ }
40
+ export function getAllPeers(store) {
41
+ ensureTable(store);
42
+ const db = store.db;
43
+ return db.prepare('SELECT * FROM sync_meta').all();
44
+ }
45
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAAG;;;;;;CAMxB,CAAC;AAEF,SAAS,WAAW,CAAC,KAAgB;IACnC,MAAM,EAAE,GAAI,KAAa,CAAC,EAAE,CAAC;IAC7B,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,OAAe;IAC7D,WAAW,CAAC,KAAK,CAAC,CAAC;IACnB,MAAM,EAAE,GAAI,KAAa,CAAC,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,uDAAuD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAQ,CAAC;IACpG,OAAO,GAAG,EAAE,YAAY,IAAI,IAAI,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,OAAe;IAC7D,WAAW,CAAC,KAAK,CAAC,CAAC;IACnB,MAAM,EAAE,GAAI,KAAa,CAAC,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,uDAAuD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAQ,CAAC;IACpG,OAAO,GAAG,EAAE,YAAY,IAAI,IAAI,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,OAAe,EAAE,EAAU;IACzE,WAAW,CAAC,KAAK,CAAC,CAAC;IACnB,MAAM,EAAE,GAAI,KAAa,CAAC,EAAE,CAAC;IAC7B,EAAE,CAAC,OAAO,CAAC;;;GAGV,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,OAAe,EAAE,EAAU;IACzE,WAAW,CAAC,KAAK,CAAC,CAAC;IACnB,MAAM,EAAE,GAAI,KAAa,CAAC,EAAE,CAAC;IAC7B,EAAE,CAAC,OAAO,CAAC;;;GAGV,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAgB;IAC1C,WAAW,CAAC,KAAK,CAAC,CAAC;IACnB,MAAM,EAAE,GAAI,KAAa,CAAC,EAAE,CAAC;IAC7B,OAAO,EAAE,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,GAAG,EAAW,CAAC;AAC9D,CAAC"}
package/dist/pull.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ import type { SyncResult } from './types.js';
3
+ /**
4
+ * Learn from the mesh server — pull new/updated atoms.
5
+ */
6
+ export declare function learnFrom(localStore: AtomStore, meshUrl: string): Promise<SyncResult>;
7
+ export declare const pullAtoms: typeof learnFrom;
package/dist/pull.js ADDED
@@ -0,0 +1,56 @@
1
+ import { getLastPullAt, setLastPullAt } from './meta.js';
2
+ /**
3
+ * Learn from the mesh server — pull new/updated atoms.
4
+ */
5
+ export async function learnFrom(localStore, meshUrl) {
6
+ const errors = [];
7
+ const since = getLastPullAt(localStore, meshUrl);
8
+ const now = new Date().toISOString();
9
+ let remoteAtoms = [];
10
+ try {
11
+ const url = since
12
+ ? `${meshUrl}/mesh/atoms?since=${encodeURIComponent(since)}`
13
+ : `${meshUrl}/mesh/atoms`;
14
+ const res = await fetch(url);
15
+ if (!res.ok) {
16
+ const errBody = await res.text();
17
+ return { pushed: 0, pulled: 0, deduped: 0, errors: [`Learn failed: ${res.status} ${errBody}`], timestamp: now };
18
+ }
19
+ remoteAtoms = await res.json();
20
+ }
21
+ catch (err) {
22
+ return { pushed: 0, pulled: 0, deduped: 0, errors: [`Learn error: ${err.message}`], timestamp: now };
23
+ }
24
+ let pulled = 0;
25
+ let deduped = 0;
26
+ for (const atom of remoteAtoms) {
27
+ try {
28
+ const { id, created_at, updated_at, ...data } = atom;
29
+ let result;
30
+ if (data.type === 'tool') {
31
+ result = localStore.createToolAtom(data);
32
+ }
33
+ else if (data.type === 'negative') {
34
+ result = localStore.createNegativeAtom(data);
35
+ }
36
+ else {
37
+ result = localStore.createAtom(data);
38
+ }
39
+ const ec = result.evidence_count;
40
+ if (ec && ec > 1) {
41
+ deduped++;
42
+ }
43
+ else {
44
+ pulled++;
45
+ }
46
+ }
47
+ catch (err) {
48
+ errors.push(`Learn insert error: ${err.message}`);
49
+ }
50
+ }
51
+ setLastPullAt(localStore, meshUrl, now);
52
+ return { pushed: 0, pulled, deduped, errors, timestamp: now };
53
+ }
54
+ // Keep backward compat alias
55
+ export const pullAtoms = learnFrom;
56
+ //# sourceMappingURL=pull.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull.js","sourceRoot":"","sources":["../src/pull.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAEzD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAqB,EAAE,OAAe;IACpE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,IAAI,WAAW,GAAoB,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,KAAK;YACf,CAAC,CAAC,GAAG,OAAO,qBAAqB,kBAAkB,CAAC,KAAK,CAAC,EAAE;YAC5D,CAAC,CAAC,GAAG,OAAO,aAAa,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YACjC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,iBAAiB,GAAG,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;QAClH,CAAC;QACD,WAAW,GAAG,MAAM,GAAG,CAAC,IAAI,EAAqB,CAAC;IACpD,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,gBAAgB,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;IACvG,CAAC;IAED,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,IAAW,CAAC;YAC5D,IAAI,MAAqB,CAAC;YAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACzB,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YAC3C,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBACpC,MAAM,GAAG,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAC/C,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;YAED,MAAM,EAAE,GAAI,MAAc,CAAC,cAAc,CAAC;YAC1C,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;gBACjB,OAAO,EAAE,CAAC;YACZ,CAAC;iBAAM,CAAC;gBACN,MAAM,EAAE,CAAC;YACX,CAAC;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,uBAAuB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,aAAa,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAChE,CAAC;AAED,6BAA6B;AAC7B,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAC"}
package/dist/push.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ import type { SyncResult } from './types.js';
3
+ /**
4
+ * Contribute local atoms to the mesh server.
5
+ */
6
+ export declare function contributeTo(localStore: AtomStore, meshUrl: string): Promise<SyncResult>;
7
+ export declare const pushAtoms: typeof contributeTo;
package/dist/push.js ADDED
@@ -0,0 +1,52 @@
1
+ import { getLastPushAt, setLastPushAt } from './meta.js';
2
+ /**
3
+ * Contribute local atoms to the mesh server.
4
+ */
5
+ export async function contributeTo(localStore, meshUrl) {
6
+ const errors = [];
7
+ const since = getLastPushAt(localStore, meshUrl);
8
+ const now = new Date().toISOString();
9
+ const allAtoms = localStore.getAll();
10
+ const toSync = since
11
+ ? allAtoms.filter(a => a.updated_at > since)
12
+ : allAtoms;
13
+ if (toSync.length === 0) {
14
+ setLastPushAt(localStore, meshUrl, now);
15
+ return { pushed: 0, pulled: 0, deduped: 0, errors: [], timestamp: now };
16
+ }
17
+ let pushed = 0;
18
+ let deduped = 0;
19
+ try {
20
+ const payload = toSync.map(stripAutoFields);
21
+ const res = await fetch(`${meshUrl}/mesh/contribute`, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify(payload),
25
+ });
26
+ if (res.ok) {
27
+ const body = await res.json();
28
+ for (const r of body.results) {
29
+ if (r.status === 'deduped')
30
+ deduped++;
31
+ else
32
+ pushed++;
33
+ }
34
+ }
35
+ else {
36
+ const errBody = await res.text();
37
+ errors.push(`Contribute failed: ${res.status} ${errBody}`);
38
+ }
39
+ }
40
+ catch (err) {
41
+ errors.push(`Contribute error: ${err.message}`);
42
+ }
43
+ setLastPushAt(localStore, meshUrl, now);
44
+ return { pushed, pulled: 0, deduped, errors, timestamp: now };
45
+ }
46
+ function stripAutoFields(atom) {
47
+ const { id, created_at, updated_at, ...rest } = atom;
48
+ return rest;
49
+ }
50
+ // Keep backward compat alias
51
+ export const pushAtoms = contributeTo;
52
+ //# sourceMappingURL=push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.js","sourceRoot":"","sources":["../src/push.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAEzD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAqB,EAAE,OAAe;IACvE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,KAAK;QAClB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,KAAK,CAAC;QAC5C,CAAC,CAAC,QAAQ,CAAC;IAEb,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,aAAa,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QACxC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;IAC1E,CAAC;IAED,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,kBAAkB,EAAE;YACpD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAC9B,CAAC,CAAC;QAEH,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YACX,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAA0E,CAAC;YACtG,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS;oBAAE,OAAO,EAAE,CAAC;;oBACjC,MAAM,EAAE,CAAC;YAChB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,sBAAsB,GAAG,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,MAAM,CAAC,IAAI,CAAC,qBAAqB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,aAAa,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAChE,CAAC;AAED,SAAS,eAAe,CAAC,IAAmB;IAC1C,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,IAAW,CAAC;IAC5D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6BAA6B;AAC7B,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ import type { SyncConfig } from './config.js';
3
+ export interface AutoSyncHandle {
4
+ stop(): void;
5
+ }
6
+ export declare function startAutoSync(store: AtomStore, config: SyncConfig): AutoSyncHandle;
@@ -0,0 +1,20 @@
1
+ import { syncWithMesh } from './sync.js';
2
+ export function startAutoSync(store, config) {
3
+ if (!config.autoSync || !config.meshUrl) {
4
+ return { stop() { } };
5
+ }
6
+ const timer = setInterval(async () => {
7
+ try {
8
+ await syncWithMesh(store, config.meshUrl);
9
+ }
10
+ catch {
11
+ // Silent failure — sync will retry next interval
12
+ }
13
+ }, config.syncIntervalMs);
14
+ return {
15
+ stop() {
16
+ clearInterval(timer);
17
+ },
18
+ };
19
+ }
20
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAMzC,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,MAAkB;IAChE,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACxC,OAAO,EAAE,IAAI,KAAI,CAAC,EAAE,CAAC;IACvB,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,IAAI,CAAC;YACH,MAAM,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,iDAAiD;QACnD,CAAC;IACH,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;IAE1B,OAAO;QACL,IAAI;YACF,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type Server } from 'node:http';
2
+ import { AtomStore } from '@osmosis-ai/core';
3
+ import type { SyncConfig } from './config.js';
4
+ /**
5
+ * Create a local API server for querying the local atom store.
6
+ * This is the "osmosis serve" server — local queries only, no peer sync routes.
7
+ */
8
+ export declare function createSyncServer(store: AtomStore, port: number, _config: SyncConfig): Server;
package/dist/server.js ADDED
@@ -0,0 +1,88 @@
1
+ import { createServer as createHttpServer } from 'node:http';
2
+ /**
3
+ * Create a local API server for querying the local atom store.
4
+ * This is the "osmosis serve" server — local queries only, no peer sync routes.
5
+ */
6
+ export function createSyncServer(store, port, _config) {
7
+ const server = createHttpServer(async (req, res) => {
8
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
9
+ const path = url.pathname;
10
+ const method = req.method ?? 'GET';
11
+ const json = (status, data) => {
12
+ res.writeHead(status, { 'Content-Type': 'application/json' });
13
+ res.end(JSON.stringify(data));
14
+ };
15
+ try {
16
+ const readBody = () => new Promise((resolve, reject) => {
17
+ const chunks = [];
18
+ req.on('data', (c) => chunks.push(c));
19
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()));
20
+ req.on('error', reject);
21
+ });
22
+ // POST /atoms
23
+ if (method === 'POST' && path === '/atoms') {
24
+ const body = JSON.parse(await readBody());
25
+ let atom;
26
+ if (body.type === 'tool') {
27
+ atom = store.createToolAtom(body);
28
+ }
29
+ else if (body.type === 'negative') {
30
+ atom = store.createNegativeAtom(body);
31
+ }
32
+ else {
33
+ atom = store.createAtom(body);
34
+ }
35
+ return json(201, atom);
36
+ }
37
+ // GET /atoms/search?q=...
38
+ if (method === 'GET' && path === '/atoms/search') {
39
+ const q = url.searchParams.get('q') ?? '';
40
+ return json(200, store.search(q));
41
+ }
42
+ // GET /atoms/:id
43
+ const idMatch = path.match(/^\/atoms\/([^/]+)$/);
44
+ if (method === 'GET' && idMatch) {
45
+ const atom = store.getById(idMatch[1]);
46
+ if (!atom)
47
+ return json(404, { error: 'Not found' });
48
+ return json(200, atom);
49
+ }
50
+ // GET /atoms?type=&tool_name=&since=
51
+ if (method === 'GET' && path === '/atoms') {
52
+ const type = url.searchParams.get('type');
53
+ const toolName = url.searchParams.get('tool_name');
54
+ const since = url.searchParams.get('since');
55
+ let atoms;
56
+ if (toolName) {
57
+ atoms = store.queryByToolName(toolName);
58
+ }
59
+ else if (type) {
60
+ atoms = store.queryByType(type);
61
+ }
62
+ else {
63
+ atoms = store.getAll();
64
+ }
65
+ if (since) {
66
+ atoms = atoms.filter(a => a.updated_at > since);
67
+ }
68
+ return json(200, atoms);
69
+ }
70
+ // GET /sync/status — kept for backward compat
71
+ if (method === 'GET' && path === '/sync/status') {
72
+ const allAtoms = store.getAll();
73
+ return json(200, {
74
+ atomCount: allAtoms.length,
75
+ meshUrl: _config.meshUrl,
76
+ });
77
+ }
78
+ json(404, { error: 'Not found' });
79
+ }
80
+ catch (err) {
81
+ const status = err.name === 'ZodError' ? 400 : 500;
82
+ json(status, { error: err.message ?? 'Internal error' });
83
+ }
84
+ });
85
+ server.listen(port);
86
+ return server;
87
+ }
88
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/sync.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ import type { SyncResult } from './types.js';
3
+ /**
4
+ * Full sync with mesh: contribute local atoms, then learn from mesh.
5
+ */
6
+ export declare function syncWithMesh(localStore: AtomStore, meshUrl: string): Promise<SyncResult>;
7
+ export declare const syncWithPeer: typeof syncWithMesh;
package/dist/sync.js ADDED
@@ -0,0 +1,19 @@
1
+ import { contributeTo } from './push.js';
2
+ import { learnFrom } from './pull.js';
3
+ /**
4
+ * Full sync with mesh: contribute local atoms, then learn from mesh.
5
+ */
6
+ export async function syncWithMesh(localStore, meshUrl) {
7
+ const pushResult = await contributeTo(localStore, meshUrl);
8
+ const pullResult = await learnFrom(localStore, meshUrl);
9
+ return {
10
+ pushed: pushResult.pushed + pushResult.deduped,
11
+ pulled: pullResult.pulled,
12
+ deduped: pushResult.deduped + pullResult.deduped,
13
+ errors: [...pushResult.errors, ...pullResult.errors],
14
+ timestamp: pullResult.timestamp,
15
+ };
16
+ }
17
+ // Keep backward compat alias
18
+ export const syncWithPeer = syncWithMesh;
19
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAAqB,EAAE,OAAe;IACvE,MAAM,UAAU,GAAG,MAAM,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAExD,OAAO;QACL,MAAM,EAAE,UAAU,CAAC,MAAM,GAAG,UAAU,CAAC,OAAO;QAC9C,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,OAAO,EAAE,UAAU,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO;QAChD,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC;QACpD,SAAS,EAAE,UAAU,CAAC,SAAS;KAChC,CAAC;AACJ,CAAC;AAED,6BAA6B;AAC7B,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface SyncResult {
2
+ pushed: number;
3
+ pulled: number;
4
+ deduped: number;
5
+ errors: string[];
6
+ timestamp: string;
7
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@osmosis-ai/sync",
3
+ "version": "0.1.0",
4
+ "description": "Sync engine for Osmosis — HTTP-based instance-to-instance sync",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "dependencies": {
14
+ "@osmosis-ai/core": "*",
15
+ "@osmosis-ai/mesh-server": "*"
16
+ },
17
+ "devDependencies": {
18
+ "better-sqlite3": "^11.7.0",
19
+ "@types/better-sqlite3": "^7.6.12",
20
+ "typescript": "^5.7.0",
21
+ "vitest": "^3.0.0"
22
+ },
23
+ "license": "MIT"
24
+ }
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { AtomStore } from '@osmosis-ai/core';
3
+ import type { Server } from 'node:http';
4
+ import { startMeshServer, type MeshServerHandle } from '@osmosis-ai/mesh-server';
5
+ import { contributeTo } from '../push.js';
6
+ import { learnFrom } from '../pull.js';
7
+ import { syncWithMesh } from '../sync.js';
8
+
9
+ let nextPort = 19500;
10
+ function getPort() { return nextPort++; }
11
+
12
+ function makeToolAtom(toolName: string, observation: string) {
13
+ return {
14
+ type: 'tool' as const,
15
+ observation,
16
+ context: `testing ${toolName}`,
17
+ confidence: 0.8,
18
+ fitness_score: 0.7,
19
+ trust_tier: 'local' as const,
20
+ source_agent_hash: 'test-agent-' + toolName,
21
+ decay_rate: 0.99,
22
+ tool_name: toolName,
23
+ params_hash: 'abc123',
24
+ outcome: 'success' as const,
25
+ error_signature: null,
26
+ latency_ms: 100,
27
+ reliability_score: 0.9,
28
+ };
29
+ }
30
+
31
+ describe('mesh sync: two clients + one mesh', () => {
32
+ let clientA: AtomStore;
33
+ let clientB: AtomStore;
34
+ let mesh: MeshServerHandle;
35
+ let meshUrl: string;
36
+
37
+ function setup() {
38
+ const port = getPort();
39
+ clientA = new AtomStore(':memory:');
40
+ clientB = new AtomStore(':memory:');
41
+ mesh = startMeshServer({ port, dbPath: ':memory:' });
42
+ meshUrl = `http://localhost:${port}`;
43
+ }
44
+
45
+ afterEach(() => {
46
+ try { mesh?.stop(); } catch {}
47
+ try { clientA?.close(); } catch {}
48
+ try { clientB?.close(); } catch {}
49
+ });
50
+
51
+ it('client A contributes atoms to mesh', async () => {
52
+ setup();
53
+ clientA.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
54
+
55
+ const result = await contributeTo(clientA, meshUrl);
56
+ expect(result.errors).toEqual([]);
57
+ expect(result.pushed).toBe(1);
58
+
59
+ // Mesh should have the atom
60
+ const meshAtoms = mesh.store.getAll();
61
+ expect(meshAtoms.length).toBe(1);
62
+ expect(meshAtoms[0]!.observation).toBe('browser.click fails on hidden elements');
63
+ // Mesh atoms are quarantined
64
+ expect(meshAtoms[0]!.trust_tier).toBe('quarantine');
65
+ });
66
+
67
+ it('client B learns from mesh', async () => {
68
+ setup();
69
+ // Seed the mesh directly
70
+ mesh.store.createToolAtom(makeToolAtom('fetch', 'fetch times out on large payloads'));
71
+
72
+ const result = await learnFrom(clientB, meshUrl);
73
+ expect(result.errors).toEqual([]);
74
+ expect(result.pulled).toBe(1);
75
+
76
+ const bAtoms = clientB.getAll();
77
+ expect(bAtoms.length).toBe(1);
78
+ expect(bAtoms[0]!.observation).toBe('fetch times out on large payloads');
79
+ });
80
+
81
+ it('full flow: A captures → mesh → B learns → B captures → mesh → A learns', async () => {
82
+ setup();
83
+
84
+ // Client A captures tool calls
85
+ clientA.createToolAtom(makeToolAtom('screenshot', 'screenshot fails on lazy-loaded pages'));
86
+ clientA.createToolAtom(makeToolAtom('exec', 'exec needs pty for interactive commands'));
87
+
88
+ // A contributes to mesh
89
+ await contributeTo(clientA, meshUrl);
90
+ expect(mesh.store.getAll().length).toBe(2);
91
+
92
+ // B learns from mesh
93
+ await learnFrom(clientB, meshUrl);
94
+ expect(clientB.getAll().length).toBe(2);
95
+
96
+ // B captures different tool calls
97
+ clientB.createToolAtom(makeToolAtom('file.write', 'file.write creates parent dirs automatically'));
98
+
99
+ // B contributes to mesh
100
+ await contributeTo(clientB, meshUrl);
101
+ expect(mesh.store.getAll().length).toBe(3);
102
+
103
+ // A learns from mesh
104
+ await learnFrom(clientA, meshUrl);
105
+
106
+ // Both should have all 3 atoms
107
+ expect(clientA.getAll().length).toBe(3);
108
+ expect(clientB.getAll().length).toBe(3);
109
+ expect(mesh.store.getAll().length).toBe(3);
110
+ });
111
+
112
+ it('syncWithMesh does contribute + learn in one call', async () => {
113
+ setup();
114
+ clientA.createToolAtom(makeToolAtom('browser.type', 'browser.type needs focus first'));
115
+
116
+ const result = await syncWithMesh(clientA, meshUrl);
117
+ expect(result.errors).toEqual([]);
118
+ expect(result.pushed).toBeGreaterThanOrEqual(1);
119
+
120
+ // Mesh has the atom
121
+ expect(mesh.store.getAll().length).toBe(1);
122
+ });
123
+
124
+ it('dedup: same observation does not create duplicates on mesh', async () => {
125
+ setup();
126
+
127
+ // Both clients have same observation
128
+ clientA.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
129
+ clientB.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
130
+
131
+ // Both contribute
132
+ await contributeTo(clientA, meshUrl);
133
+ await contributeTo(clientB, meshUrl);
134
+
135
+ // Mesh should have 1 atom (deduped)
136
+ expect(mesh.store.getAll().length).toBe(1);
137
+ });
138
+
139
+ it('mesh stats endpoint works', async () => {
140
+ setup();
141
+ clientA.createToolAtom(makeToolAtom('tool1', 'observation one'));
142
+ clientA.createToolAtom(makeToolAtom('tool2', 'observation two completely different'));
143
+ await contributeTo(clientA, meshUrl);
144
+
145
+ const res = await fetch(`${meshUrl}/mesh/stats`);
146
+ const stats = await res.json() as any;
147
+ expect(stats.totalAtoms).toBe(2);
148
+ expect(stats.contributors).toBeGreaterThanOrEqual(1);
149
+ });
150
+
151
+ it('mesh query endpoint works', async () => {
152
+ setup();
153
+ mesh.store.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
154
+ mesh.store.createToolAtom(makeToolAtom('fetch', 'fetch times out on large payloads'));
155
+
156
+ const res = await fetch(`${meshUrl}/mesh/query?q=browser`);
157
+ const atoms = await res.json() as any[];
158
+ expect(atoms.length).toBe(1);
159
+ expect(atoms[0].tool_name).toBe('browser.click');
160
+ });
161
+
162
+ it('incremental sync with since parameter', async () => {
163
+ setup();
164
+
165
+ // First sync: A has one atom
166
+ clientA.createToolAtom(makeToolAtom('old-tool', 'old observation'));
167
+ await syncWithMesh(clientA, meshUrl);
168
+
169
+ // B syncs — gets 1 atom
170
+ await syncWithMesh(clientB, meshUrl);
171
+ expect(clientB.getAll().length).toBe(1);
172
+
173
+ // A adds another atom
174
+ await new Promise(r => setTimeout(r, 50));
175
+ clientA.createToolAtom(makeToolAtom('new-tool', 'completely new and different observation'));
176
+ await contributeTo(clientA, meshUrl);
177
+
178
+ // B syncs again — should get only the new one (incremental)
179
+ const result = await learnFrom(clientB, meshUrl);
180
+ expect(result.pulled).toBe(1);
181
+ expect(clientB.getAll().length).toBe(2);
182
+ });
183
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { AtomStore } from '@osmosis-ai/core';
3
+ import { startMeshServer, type MeshServerHandle } from '@osmosis-ai/mesh-server';
4
+ import { createSyncServer } from '../server.js';
5
+ import { syncWithMesh } from '../sync.js';
6
+ import { contributeTo } from '../push.js';
7
+ import { learnFrom } from '../pull.js';
8
+ import { resolveSyncConfig } from '../config.js';
9
+ import type { Server } from 'node:http';
10
+
11
+ let nextPort = 19432;
12
+ function getPort() { return nextPort++; }
13
+
14
+ function makeToolAtom(toolName: string, observation: string) {
15
+ return {
16
+ type: 'tool' as const,
17
+ observation,
18
+ context: `testing ${toolName}`,
19
+ confidence: 0.8,
20
+ fitness_score: 0.7,
21
+ trust_tier: 'quarantine' as const,
22
+ source_agent_hash: 'test-agent-' + toolName,
23
+ decay_rate: 0.99,
24
+ tool_name: toolName,
25
+ params_hash: 'abc123',
26
+ outcome: 'success' as const,
27
+ error_signature: null,
28
+ latency_ms: 100,
29
+ reliability_score: 0.9,
30
+ };
31
+ }
32
+
33
+ describe('two-instance sync via mesh', () => {
34
+ let storeA: AtomStore;
35
+ let storeB: AtomStore;
36
+ let serverA: Server;
37
+ let mesh: MeshServerHandle;
38
+ let meshUrl: string;
39
+ let portA: number;
40
+
41
+ function setup() {
42
+ portA = getPort();
43
+ const meshPort = getPort();
44
+ storeA = new AtomStore(':memory:');
45
+ storeB = new AtomStore(':memory:');
46
+ meshUrl = `http://localhost:${meshPort}`;
47
+ const config = resolveSyncConfig({ meshUrl, autoSync: false });
48
+ serverA = createSyncServer(storeA, portA, config);
49
+ mesh = startMeshServer({ port: meshPort, dbPath: ':memory:' });
50
+ }
51
+
52
+ afterEach(() => {
53
+ try { serverA?.close(); } catch {}
54
+ try { mesh?.stop(); } catch {}
55
+ try { storeA?.close(); } catch {}
56
+ try { storeB?.close(); } catch {}
57
+ });
58
+
59
+ it('should push atoms from A to mesh', async () => {
60
+ setup();
61
+ storeA.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
62
+
63
+ const result = await contributeTo(storeA, meshUrl);
64
+ expect(result.errors).toEqual([]);
65
+ expect(result.pushed + result.deduped).toBeGreaterThanOrEqual(1);
66
+
67
+ const meshAtoms = mesh.store.getAll();
68
+ expect(meshAtoms.length).toBe(1);
69
+ expect(meshAtoms[0]!.observation).toBe('browser.click fails on hidden elements');
70
+ });
71
+
72
+ it('should pull atoms from mesh to B', async () => {
73
+ setup();
74
+ mesh.store.createToolAtom(makeToolAtom('fetch', 'fetch times out on large payloads'));
75
+
76
+ const result = await learnFrom(storeB, meshUrl);
77
+ expect(result.errors).toEqual([]);
78
+ expect(result.pulled).toBe(1);
79
+
80
+ const bAtoms = storeB.getAll();
81
+ expect(bAtoms.length).toBe(1);
82
+ expect(bAtoms[0]!.observation).toBe('fetch times out on large payloads');
83
+ });
84
+
85
+ it('should full sync: A captures, sync to mesh, B syncs from mesh, B captures, sync to mesh, A syncs', async () => {
86
+ setup();
87
+
88
+ // Instance A captures some tool calls
89
+ storeA.createToolAtom(makeToolAtom('screenshot', 'screenshot fails on lazy-loaded pages'));
90
+ storeA.createToolAtom(makeToolAtom('exec', 'exec needs pty for interactive commands'));
91
+
92
+ // Sync A → mesh
93
+ await syncWithMesh(storeA, meshUrl);
94
+ expect(mesh.store.getAll().length).toBe(2);
95
+
96
+ // Sync mesh → B
97
+ await syncWithMesh(storeB, meshUrl);
98
+ expect(storeB.getAll().length).toBe(2);
99
+
100
+ // Instance B captures different tool calls
101
+ storeB.createToolAtom(makeToolAtom('file.write', 'file.write creates parent dirs automatically'));
102
+
103
+ // Sync B → mesh → A
104
+ await syncWithMesh(storeB, meshUrl);
105
+ await syncWithMesh(storeA, meshUrl);
106
+
107
+ // Both should have everything
108
+ const aAtoms = storeA.getAll();
109
+ const bAtoms = storeB.getAll();
110
+ expect(aAtoms.length).toBe(3);
111
+ expect(bAtoms.length).toBe(3);
112
+ });
113
+
114
+ it('should dedup: same observation does not create duplicates', async () => {
115
+ setup();
116
+
117
+ // Both instances have the same observation
118
+ storeA.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
119
+ storeB.createToolAtom(makeToolAtom('browser.click', 'browser.click fails on hidden elements'));
120
+
121
+ // Both sync to mesh
122
+ await syncWithMesh(storeA, meshUrl);
123
+ await syncWithMesh(storeB, meshUrl);
124
+
125
+ // Mesh should have just 1 atom (deduped)
126
+ expect(mesh.store.getAll().length).toBe(1);
127
+ // Both should still have 1 atom
128
+ expect(storeA.getAll().length).toBe(1);
129
+ expect(storeB.getAll().length).toBe(1);
130
+ });
131
+
132
+ it('should filter atoms with ?since= parameter on local server', async () => {
133
+ setup();
134
+
135
+ // Create atom at known time
136
+ storeA.createToolAtom(makeToolAtom('old-tool', 'this is an old observation'));
137
+
138
+ const sinceTime = new Date().toISOString();
139
+
140
+ // Small delay to ensure timestamp difference
141
+ await new Promise(r => setTimeout(r, 50));
142
+
143
+ storeA.createToolAtom(makeToolAtom('new-tool', 'this is a totally new and different observation'));
144
+
145
+ // Fetch with since filter from local server
146
+ const res = await fetch(`http://localhost:${portA}/atoms?since=${encodeURIComponent(sinceTime)}`);
147
+ const atoms = await res.json() as any[];
148
+ expect(atoms.length).toBe(1);
149
+ expect(atoms[0].tool_name).toBe('new-tool');
150
+ });
151
+
152
+ it('should report sync status', async () => {
153
+ setup();
154
+ storeA.createToolAtom(makeToolAtom('test', 'test observation for status'));
155
+
156
+ const res = await fetch(`http://localhost:${portA}/sync/status`);
157
+ const status = await res.json() as any;
158
+ expect(status.atomCount).toBe(1);
159
+ });
160
+ });
package/src/config.ts ADDED
@@ -0,0 +1,18 @@
1
+ export interface SyncConfig {
2
+ /** URL of the Osmosis mesh server */
3
+ meshUrl: string;
4
+ /** Auto-sync interval in ms (default: 5 min) */
5
+ syncIntervalMs: number;
6
+ /** Enable periodic sync */
7
+ autoSync: boolean;
8
+ }
9
+
10
+ export const DEFAULT_SYNC_CONFIG: SyncConfig = {
11
+ meshUrl: 'https://mesh.osmosis.dev',
12
+ syncIntervalMs: 5 * 60 * 1000,
13
+ autoSync: false,
14
+ };
15
+
16
+ export function resolveSyncConfig(partial?: Partial<SyncConfig>): SyncConfig {
17
+ return { ...DEFAULT_SYNC_CONFIG, ...partial };
18
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { contributeTo, pushAtoms } from './push.js';
2
+ export { learnFrom, pullAtoms } from './pull.js';
3
+ export { syncWithMesh, syncWithPeer } from './sync.js';
4
+ export { createSyncServer } from './server.js';
5
+ export { startAutoSync } from './scheduler.js';
6
+ export type { AutoSyncHandle } from './scheduler.js';
7
+ export { resolveSyncConfig, DEFAULT_SYNC_CONFIG } from './config.js';
8
+ export type { SyncConfig } from './config.js';
9
+ export type { SyncResult } from './types.js';
10
+ export { getAllPeers } from './meta.js';
package/src/meta.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+
3
+ const SYNC_META_SCHEMA = `
4
+ CREATE TABLE IF NOT EXISTS sync_meta (
5
+ peer_url TEXT PRIMARY KEY,
6
+ last_push_at TEXT,
7
+ last_pull_at TEXT
8
+ );
9
+ `;
10
+
11
+ function ensureTable(store: AtomStore): void {
12
+ const db = (store as any).db;
13
+ db.exec(SYNC_META_SCHEMA);
14
+ }
15
+
16
+ export function getLastPushAt(store: AtomStore, peerUrl: string): string | null {
17
+ ensureTable(store);
18
+ const db = (store as any).db;
19
+ const row = db.prepare('SELECT last_push_at FROM sync_meta WHERE peer_url = ?').get(peerUrl) as any;
20
+ return row?.last_push_at ?? null;
21
+ }
22
+
23
+ export function getLastPullAt(store: AtomStore, peerUrl: string): string | null {
24
+ ensureTable(store);
25
+ const db = (store as any).db;
26
+ const row = db.prepare('SELECT last_pull_at FROM sync_meta WHERE peer_url = ?').get(peerUrl) as any;
27
+ return row?.last_pull_at ?? null;
28
+ }
29
+
30
+ export function setLastPushAt(store: AtomStore, peerUrl: string, ts: string): void {
31
+ ensureTable(store);
32
+ const db = (store as any).db;
33
+ db.prepare(`
34
+ INSERT INTO sync_meta (peer_url, last_push_at) VALUES (?, ?)
35
+ ON CONFLICT(peer_url) DO UPDATE SET last_push_at = excluded.last_push_at
36
+ `).run(peerUrl, ts);
37
+ }
38
+
39
+ export function setLastPullAt(store: AtomStore, peerUrl: string, ts: string): void {
40
+ ensureTable(store);
41
+ const db = (store as any).db;
42
+ db.prepare(`
43
+ INSERT INTO sync_meta (peer_url, last_pull_at) VALUES (?, ?)
44
+ ON CONFLICT(peer_url) DO UPDATE SET last_pull_at = excluded.last_pull_at
45
+ `).run(peerUrl, ts);
46
+ }
47
+
48
+ export function getAllPeers(store: AtomStore): Array<{ peer_url: string; last_push_at: string | null; last_pull_at: string | null }> {
49
+ ensureTable(store);
50
+ const db = (store as any).db;
51
+ return db.prepare('SELECT * FROM sync_meta').all() as any[];
52
+ }
package/src/pull.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { AtomStore, KnowledgeAtom } from '@osmosis-ai/core';
2
+ import type { SyncResult } from './types.js';
3
+ import { getLastPullAt, setLastPullAt } from './meta.js';
4
+
5
+ /**
6
+ * Learn from the mesh server — pull new/updated atoms.
7
+ */
8
+ export async function learnFrom(localStore: AtomStore, meshUrl: string): Promise<SyncResult> {
9
+ const errors: string[] = [];
10
+ const since = getLastPullAt(localStore, meshUrl);
11
+ const now = new Date().toISOString();
12
+
13
+ let remoteAtoms: KnowledgeAtom[] = [];
14
+ try {
15
+ const url = since
16
+ ? `${meshUrl}/mesh/atoms?since=${encodeURIComponent(since)}`
17
+ : `${meshUrl}/mesh/atoms`;
18
+ const res = await fetch(url);
19
+ if (!res.ok) {
20
+ const errBody = await res.text();
21
+ return { pushed: 0, pulled: 0, deduped: 0, errors: [`Learn failed: ${res.status} ${errBody}`], timestamp: now };
22
+ }
23
+ remoteAtoms = await res.json() as KnowledgeAtom[];
24
+ } catch (err: any) {
25
+ return { pushed: 0, pulled: 0, deduped: 0, errors: [`Learn error: ${err.message}`], timestamp: now };
26
+ }
27
+
28
+ let pulled = 0;
29
+ let deduped = 0;
30
+
31
+ for (const atom of remoteAtoms) {
32
+ try {
33
+ const { id, created_at, updated_at, ...data } = atom as any;
34
+ let result: KnowledgeAtom;
35
+ if (data.type === 'tool') {
36
+ result = localStore.createToolAtom(data);
37
+ } else if (data.type === 'negative') {
38
+ result = localStore.createNegativeAtom(data);
39
+ } else {
40
+ result = localStore.createAtom(data);
41
+ }
42
+
43
+ const ec = (result as any).evidence_count;
44
+ if (ec && ec > 1) {
45
+ deduped++;
46
+ } else {
47
+ pulled++;
48
+ }
49
+ } catch (err: any) {
50
+ errors.push(`Learn insert error: ${err.message}`);
51
+ }
52
+ }
53
+
54
+ setLastPullAt(localStore, meshUrl, now);
55
+ return { pushed: 0, pulled, deduped, errors, timestamp: now };
56
+ }
57
+
58
+ // Keep backward compat alias
59
+ export const pullAtoms = learnFrom;
package/src/push.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { AtomStore, KnowledgeAtom } from '@osmosis-ai/core';
2
+ import type { SyncResult } from './types.js';
3
+ import { getLastPushAt, setLastPushAt } from './meta.js';
4
+
5
+ /**
6
+ * Contribute local atoms to the mesh server.
7
+ */
8
+ export async function contributeTo(localStore: AtomStore, meshUrl: string): Promise<SyncResult> {
9
+ const errors: string[] = [];
10
+ const since = getLastPushAt(localStore, meshUrl);
11
+ const now = new Date().toISOString();
12
+
13
+ const allAtoms = localStore.getAll();
14
+ const toSync = since
15
+ ? allAtoms.filter(a => a.updated_at > since)
16
+ : allAtoms;
17
+
18
+ if (toSync.length === 0) {
19
+ setLastPushAt(localStore, meshUrl, now);
20
+ return { pushed: 0, pulled: 0, deduped: 0, errors: [], timestamp: now };
21
+ }
22
+
23
+ let pushed = 0;
24
+ let deduped = 0;
25
+
26
+ try {
27
+ const payload = toSync.map(stripAutoFields);
28
+ const res = await fetch(`${meshUrl}/mesh/contribute`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify(payload),
32
+ });
33
+
34
+ if (res.ok) {
35
+ const body = await res.json() as { accepted: number; results: Array<{ id: string; status: string }> };
36
+ for (const r of body.results) {
37
+ if (r.status === 'deduped') deduped++;
38
+ else pushed++;
39
+ }
40
+ } else {
41
+ const errBody = await res.text();
42
+ errors.push(`Contribute failed: ${res.status} ${errBody}`);
43
+ }
44
+ } catch (err: any) {
45
+ errors.push(`Contribute error: ${err.message}`);
46
+ }
47
+
48
+ setLastPushAt(localStore, meshUrl, now);
49
+ return { pushed, pulled: 0, deduped, errors, timestamp: now };
50
+ }
51
+
52
+ function stripAutoFields(atom: KnowledgeAtom): Record<string, unknown> {
53
+ const { id, created_at, updated_at, ...rest } = atom as any;
54
+ return rest;
55
+ }
56
+
57
+ // Keep backward compat alias
58
+ export const pushAtoms = contributeTo;
@@ -0,0 +1,27 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ import type { SyncConfig } from './config.js';
3
+ import { syncWithMesh } from './sync.js';
4
+
5
+ export interface AutoSyncHandle {
6
+ stop(): void;
7
+ }
8
+
9
+ export function startAutoSync(store: AtomStore, config: SyncConfig): AutoSyncHandle {
10
+ if (!config.autoSync || !config.meshUrl) {
11
+ return { stop() {} };
12
+ }
13
+
14
+ const timer = setInterval(async () => {
15
+ try {
16
+ await syncWithMesh(store, config.meshUrl);
17
+ } catch {
18
+ // Silent failure — sync will retry next interval
19
+ }
20
+ }, config.syncIntervalMs);
21
+
22
+ return {
23
+ stop() {
24
+ clearInterval(timer);
25
+ },
26
+ };
27
+ }
package/src/server.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { createServer as createHttpServer, type Server } from 'node:http';
2
+ import { AtomStore } from '@osmosis-ai/core';
3
+ import type { SyncConfig } from './config.js';
4
+
5
+ /**
6
+ * Create a local API server for querying the local atom store.
7
+ * This is the "osmosis serve" server — local queries only, no peer sync routes.
8
+ */
9
+ export function createSyncServer(
10
+ store: AtomStore,
11
+ port: number,
12
+ _config: SyncConfig,
13
+ ): Server {
14
+ const server = createHttpServer(async (req, res) => {
15
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
16
+ const path = url.pathname;
17
+ const method = req.method ?? 'GET';
18
+
19
+ const json = (status: number, data: unknown) => {
20
+ res.writeHead(status, { 'Content-Type': 'application/json' });
21
+ res.end(JSON.stringify(data));
22
+ };
23
+
24
+ try {
25
+ const readBody = (): Promise<string> => new Promise((resolve, reject) => {
26
+ const chunks: Buffer[] = [];
27
+ req.on('data', (c: Buffer) => chunks.push(c));
28
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()));
29
+ req.on('error', reject);
30
+ });
31
+
32
+ // POST /atoms
33
+ if (method === 'POST' && path === '/atoms') {
34
+ const body = JSON.parse(await readBody());
35
+ let atom;
36
+ if (body.type === 'tool') {
37
+ atom = store.createToolAtom(body);
38
+ } else if (body.type === 'negative') {
39
+ atom = store.createNegativeAtom(body);
40
+ } else {
41
+ atom = store.createAtom(body);
42
+ }
43
+ return json(201, atom);
44
+ }
45
+
46
+ // GET /atoms/search?q=...
47
+ if (method === 'GET' && path === '/atoms/search') {
48
+ const q = url.searchParams.get('q') ?? '';
49
+ return json(200, store.search(q));
50
+ }
51
+
52
+ // GET /atoms/:id
53
+ const idMatch = path.match(/^\/atoms\/([^/]+)$/);
54
+ if (method === 'GET' && idMatch) {
55
+ const atom = store.getById(idMatch[1]!);
56
+ if (!atom) return json(404, { error: 'Not found' });
57
+ return json(200, atom);
58
+ }
59
+
60
+ // GET /atoms?type=&tool_name=&since=
61
+ if (method === 'GET' && path === '/atoms') {
62
+ const type = url.searchParams.get('type');
63
+ const toolName = url.searchParams.get('tool_name');
64
+ const since = url.searchParams.get('since');
65
+
66
+ let atoms;
67
+ if (toolName) {
68
+ atoms = store.queryByToolName(toolName);
69
+ } else if (type) {
70
+ atoms = store.queryByType(type as any);
71
+ } else {
72
+ atoms = store.getAll();
73
+ }
74
+
75
+ if (since) {
76
+ atoms = atoms.filter(a => a.updated_at > since);
77
+ }
78
+
79
+ return json(200, atoms);
80
+ }
81
+
82
+ // GET /sync/status — kept for backward compat
83
+ if (method === 'GET' && path === '/sync/status') {
84
+ const allAtoms = store.getAll();
85
+ return json(200, {
86
+ atomCount: allAtoms.length,
87
+ meshUrl: _config.meshUrl,
88
+ });
89
+ }
90
+
91
+ json(404, { error: 'Not found' });
92
+ } catch (err: any) {
93
+ const status = err.name === 'ZodError' ? 400 : 500;
94
+ json(status, { error: err.message ?? 'Internal error' });
95
+ }
96
+ });
97
+
98
+ server.listen(port);
99
+ return server;
100
+ }
package/src/sync.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { AtomStore } from '@osmosis-ai/core';
2
+ import type { SyncResult } from './types.js';
3
+ import { contributeTo } from './push.js';
4
+ import { learnFrom } from './pull.js';
5
+
6
+ /**
7
+ * Full sync with mesh: contribute local atoms, then learn from mesh.
8
+ */
9
+ export async function syncWithMesh(localStore: AtomStore, meshUrl: string): Promise<SyncResult> {
10
+ const pushResult = await contributeTo(localStore, meshUrl);
11
+ const pullResult = await learnFrom(localStore, meshUrl);
12
+
13
+ return {
14
+ pushed: pushResult.pushed + pushResult.deduped,
15
+ pulled: pullResult.pulled,
16
+ deduped: pushResult.deduped + pullResult.deduped,
17
+ errors: [...pushResult.errors, ...pullResult.errors],
18
+ timestamp: pullResult.timestamp,
19
+ };
20
+ }
21
+
22
+ // Keep backward compat alias
23
+ export const syncWithPeer = syncWithMesh;
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface SyncResult {
2
+ pushed: number;
3
+ pulled: number;
4
+ deduped: number;
5
+ errors: string[];
6
+ timestamp: string;
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "declaration": true,
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "sourceMap": true,
16
+ "composite": true
17
+ },
18
+ "include": ["src"],
19
+ "exclude": ["node_modules", "dist", "**/*.test.ts"],
20
+ "references": [
21
+ { "path": "../core" },
22
+ { "path": "../mesh-server" }
23
+ ]
24
+ }