@tangleai/store 0.20.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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/package.json +84 -0
- package/src/db.d.ts +27 -0
- package/src/db.js +28 -0
- package/src/document-store.d.ts +3 -0
- package/src/document-store.js +124 -0
- package/src/identities.d.ts +27 -0
- package/src/identities.js +43 -0
- package/src/index.d.ts +16 -0
- package/src/index.js +10 -0
- package/src/ledger-storage.d.ts +3 -0
- package/src/ledger-storage.js +46 -0
- package/src/mas-jobs.d.ts +121 -0
- package/src/mas-jobs.js +194 -0
- package/src/mas-store.d.ts +28 -0
- package/src/mas-store.js +524 -0
- package/src/memory-store.d.ts +28 -0
- package/src/memory-store.js +52 -0
- package/src/model.d.ts +446 -0
- package/src/model.js +242 -0
- package/src/runs.d.ts +74 -0
- package/src/runs.js +104 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @tangleai/store
|
|
2
|
+
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Establish the coordinated 0.20.0 release with JavaScript and TypeScript declaration distributions, preserved public subpaths and JSON schemas, and verified Node and Bun consumers. Use published JarenJS 0.83.3 fixes without a consumer installation patch. Prepare versions before release commits, verify locally, push directly to main and publish CI-verified tarballs. Deploy the website independently from local Tangle workspace source with JarenJS packages from npm, verifying dependency sources and the live commit.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @tangleai/core@0.20.0
|
|
13
|
+
- @tangleai/config@0.20.0
|
|
14
|
+
- @tangleai/mas@0.20.0
|
|
15
|
+
- @tangleai/documents@0.20.0
|
|
16
|
+
- @tangleai/memory@0.20.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joham (jklarenbeek@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @tangleai/store
|
|
2
|
+
|
|
3
|
+
Tangle AI persistence — the MemoryStore contract over SQLite via @jarenjs/db, plus the run/event log the DAG surface reads
|
|
4
|
+
|
|
5
|
+
Install with `npm install @tangleai/store`. The npm distribution provides ESM JavaScript, TypeScript declarations, and the documented package subpaths for Node 24 and Bun 1.4 or newer.
|
|
6
|
+
|
|
7
|
+
See the [Tangle documentation](https://github.com/jklarenbeek/tangleai#readme) for architecture, examples, and runtime requirements. All public Tangle packages use one coordinated version.
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tangleai/store",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "Tangle AI persistence — the MemoryStore contract over SQLite via @jarenjs/db, plus the run/event log the DAG surface reads",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./model": {
|
|
15
|
+
"types": "./src/model.d.ts",
|
|
16
|
+
"import": "./src/model.js",
|
|
17
|
+
"default": "./src/model.js"
|
|
18
|
+
},
|
|
19
|
+
"./db": {
|
|
20
|
+
"types": "./src/db.d.ts",
|
|
21
|
+
"import": "./src/db.js",
|
|
22
|
+
"default": "./src/db.js"
|
|
23
|
+
},
|
|
24
|
+
"./memory-store": {
|
|
25
|
+
"types": "./src/memory-store.d.ts",
|
|
26
|
+
"import": "./src/memory-store.js",
|
|
27
|
+
"default": "./src/memory-store.js"
|
|
28
|
+
},
|
|
29
|
+
"./runs": {
|
|
30
|
+
"types": "./src/runs.d.ts",
|
|
31
|
+
"import": "./src/runs.js",
|
|
32
|
+
"default": "./src/runs.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json",
|
|
35
|
+
"./mas-store": {
|
|
36
|
+
"types": "./src/mas-store.d.ts",
|
|
37
|
+
"import": "./src/mas-store.js",
|
|
38
|
+
"default": "./src/mas-store.js"
|
|
39
|
+
},
|
|
40
|
+
"./mas-jobs": {
|
|
41
|
+
"types": "./src/mas-jobs.d.ts",
|
|
42
|
+
"import": "./src/mas-jobs.js",
|
|
43
|
+
"default": "./src/mas-jobs.js"
|
|
44
|
+
},
|
|
45
|
+
"./ledger-storage": {
|
|
46
|
+
"types": "./src/ledger-storage.d.ts",
|
|
47
|
+
"import": "./src/ledger-storage.js",
|
|
48
|
+
"default": "./src/ledger-storage.js"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=24"
|
|
53
|
+
},
|
|
54
|
+
"sideEffects": false,
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@tangleai/documents": "^0.20.0",
|
|
57
|
+
"@tangleai/core": "^0.20.0",
|
|
58
|
+
"@tangleai/config": "^0.20.0",
|
|
59
|
+
"@jarenjs/db": "0.83.3",
|
|
60
|
+
"@jarenjs/validate": "0.83.3",
|
|
61
|
+
"@tangleai/mas": "^0.20.0",
|
|
62
|
+
"@jarenjs/core": "0.83.3",
|
|
63
|
+
"@tangleai/memory": "^0.20.0"
|
|
64
|
+
},
|
|
65
|
+
"private": false,
|
|
66
|
+
"types": "./src/index.d.ts",
|
|
67
|
+
"files": [
|
|
68
|
+
"src/**/*.js",
|
|
69
|
+
"src/**/*.d.ts",
|
|
70
|
+
"schemas/**/*.json",
|
|
71
|
+
"README.md",
|
|
72
|
+
"LICENSE",
|
|
73
|
+
"CHANGELOG.md"
|
|
74
|
+
],
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public",
|
|
77
|
+
"registry": "https://registry.npmjs.org/"
|
|
78
|
+
},
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "git+https://github.com/jklarenbeek/tangleai.git",
|
|
82
|
+
"directory": "packages/store"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/db.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening the Tangle database — one call, either runtime.
|
|
3
|
+
*
|
|
4
|
+
* @jarenjs/db ships BOTH drivers and both import their SQLite binding
|
|
5
|
+
* lazily inside `open()` (`lazyOpen`), so importing the two modules at
|
|
6
|
+
* top level is safe everywhere: under Node the `bun:` specifier is never
|
|
7
|
+
* resolved, under Bun `node:sqlite` is never resolved. The pick is one
|
|
8
|
+
* runtime probe: `process.versions.bun`.
|
|
9
|
+
*
|
|
10
|
+
* The handles are @jarenjs/db's own published types — `Store` from
|
|
11
|
+
* `openStore`, `Collection<T>` from `store.collection<T>(name)` with the
|
|
12
|
+
* document shape stated where the handle is taken — so nothing about
|
|
13
|
+
* the store's surface is restated here; the two aliases only keep
|
|
14
|
+
* Tangle's names.
|
|
15
|
+
*/
|
|
16
|
+
import { type Collection, type OpenStoreOptions, type Store } from '@jarenjs/db';
|
|
17
|
+
/** A @jarenjs/db collection handle over documents of shape `T`. */
|
|
18
|
+
export type DbCollection<T = unknown> = Collection<T>;
|
|
19
|
+
/** The @jarenjs/db store handle. */
|
|
20
|
+
export type TangleDb = Store;
|
|
21
|
+
/** The suite's host options, including transaction policy, runtime, native
|
|
22
|
+
* pragmas, validation, live bounds and read-only profiles. */
|
|
23
|
+
export interface OpenTangleDbOptions extends Omit<OpenStoreOptions, 'driver'> {
|
|
24
|
+
driver?: OpenStoreOptions['driver'];
|
|
25
|
+
}
|
|
26
|
+
export declare function pickDriver(): OpenStoreOptions['driver'];
|
|
27
|
+
export declare function openTangleDb(options?: OpenTangleDbOptions): Promise<TangleDb>;
|
package/src/db.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening the Tangle database — one call, either runtime.
|
|
3
|
+
*
|
|
4
|
+
* @jarenjs/db ships BOTH drivers and both import their SQLite binding
|
|
5
|
+
* lazily inside `open()` (`lazyOpen`), so importing the two modules at
|
|
6
|
+
* top level is safe everywhere: under Node the `bun:` specifier is never
|
|
7
|
+
* resolved, under Bun `node:sqlite` is never resolved. The pick is one
|
|
8
|
+
* runtime probe: `process.versions.bun`.
|
|
9
|
+
*
|
|
10
|
+
* The handles are @jarenjs/db's own published types — `Store` from
|
|
11
|
+
* `openStore`, `Collection<T>` from `store.collection<T>(name)` with the
|
|
12
|
+
* document shape stated where the handle is taken — so nothing about
|
|
13
|
+
* the store's surface is restated here; the two aliases only keep
|
|
14
|
+
* Tangle's names.
|
|
15
|
+
*/
|
|
16
|
+
import { openStore } from '@jarenjs/db';
|
|
17
|
+
import { nodeDriver } from '@jarenjs/db/node';
|
|
18
|
+
import { bunDriver } from '@jarenjs/db/bun';
|
|
19
|
+
import { TANGLE_DB_MODEL } from "./model.js";
|
|
20
|
+
export function pickDriver() {
|
|
21
|
+
return typeof process !== 'undefined' && process.versions?.bun !== undefined
|
|
22
|
+
? bunDriver()
|
|
23
|
+
: nodeDriver();
|
|
24
|
+
}
|
|
25
|
+
export function openTangleDb(options = {}) {
|
|
26
|
+
const driver = options.driver ?? pickDriver();
|
|
27
|
+
return openStore(TANGLE_DB_MODEL, { ...options, driver, path: options.path ?? ':memory:' });
|
|
28
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { assertDocumentChunk, assertDocumentElement, assertDocumentSource, assertDocumentVersion, } from '@tangleai/documents/contracts';
|
|
2
|
+
import { asRows } from "./memory-store.js";
|
|
3
|
+
async function rows(db, collection, fields = {}) {
|
|
4
|
+
return asRows(await db.collection(collection).execute({
|
|
5
|
+
$for: { row: '$[*]' },
|
|
6
|
+
$where: Object.keys(fields).length === 0 ? true : { $and: Object.entries(fields).map(([field, value]) => ({ $eq: [`$row.${field}`, { $const: value }] })) },
|
|
7
|
+
$return: '$row',
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
export function createDocumentStore(db) {
|
|
11
|
+
const sources = db.collection('sources');
|
|
12
|
+
const versions = db.collection('document_versions');
|
|
13
|
+
const elements = db.collection('document_elements');
|
|
14
|
+
const chunks = db.collection('document_chunks');
|
|
15
|
+
const locks = new Map();
|
|
16
|
+
async function serial(sourceId, operation) {
|
|
17
|
+
while (locks.has(sourceId))
|
|
18
|
+
await locks.get(sourceId);
|
|
19
|
+
let release = () => { };
|
|
20
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
21
|
+
locks.set(sourceId, held);
|
|
22
|
+
try {
|
|
23
|
+
return await operation();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
locks.delete(sourceId);
|
|
27
|
+
release();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function cleanupVersion(scope, versionId) {
|
|
31
|
+
const scopedElements = scope.collection('document_elements');
|
|
32
|
+
const scopedChunks = scope.collection('document_chunks');
|
|
33
|
+
const oldElements = await rows(scope, 'document_elements', { versionId });
|
|
34
|
+
const oldChunks = await rows(scope, 'document_chunks', { versionId });
|
|
35
|
+
for (const chunk of oldChunks)
|
|
36
|
+
await scopedChunks.delete(chunk.id);
|
|
37
|
+
for (const element of oldElements)
|
|
38
|
+
await scopedElements.delete(element.id);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
getSource: (id) => sources.get(id),
|
|
42
|
+
async listSources() {
|
|
43
|
+
const result = await rows(db, 'sources');
|
|
44
|
+
result.sort((a, b) => b.fetchedAt.localeCompare(a.fetchedAt));
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
getVersion: (id) => versions.get(id),
|
|
48
|
+
async listVersions(sourceId) {
|
|
49
|
+
const result = await rows(db, 'document_versions', sourceId === undefined ? {} : { sourceId });
|
|
50
|
+
return result
|
|
51
|
+
.sort((a, b) => b.fetchedAt.localeCompare(a.fetchedAt));
|
|
52
|
+
},
|
|
53
|
+
async listElements(versionId) {
|
|
54
|
+
return (await rows(db, 'document_elements', { versionId }))
|
|
55
|
+
.sort((a, b) => a.order - b.order);
|
|
56
|
+
},
|
|
57
|
+
async listChunks(versionId) {
|
|
58
|
+
return (await rows(db, 'document_chunks', versionId === undefined ? {} : { versionId }))
|
|
59
|
+
.sort((a, b) => a.sourceId.localeCompare(b.sourceId) || a.order - b.order);
|
|
60
|
+
},
|
|
61
|
+
async putSource(source) {
|
|
62
|
+
assertDocumentSource(source);
|
|
63
|
+
await sources.put(source);
|
|
64
|
+
},
|
|
65
|
+
async activate(bundle) {
|
|
66
|
+
await serial(bundle.source.id, async () => {
|
|
67
|
+
assertDocumentSource(bundle.source);
|
|
68
|
+
assertDocumentVersion(bundle.version);
|
|
69
|
+
for (const element of bundle.elements)
|
|
70
|
+
assertDocumentElement(element);
|
|
71
|
+
for (const chunk of bundle.chunks)
|
|
72
|
+
assertDocumentChunk(chunk);
|
|
73
|
+
if (bundle.source.activeVersionId !== bundle.version.id)
|
|
74
|
+
throw new Error('Source activeVersionId must name the staged version');
|
|
75
|
+
if (bundle.version.sourceId !== bundle.source.id
|
|
76
|
+
|| bundle.elements.some((item) => item.sourceId !== bundle.source.id || item.versionId !== bundle.version.id)
|
|
77
|
+
|| bundle.chunks.some((item) => item.sourceId !== bundle.source.id || item.versionId !== bundle.version.id)) {
|
|
78
|
+
throw new Error('Document bundle identifiers do not agree');
|
|
79
|
+
}
|
|
80
|
+
const staging = { ...bundle.version, status: 'staging', activatedAt: undefined, error: undefined };
|
|
81
|
+
await db.transaction(async (transaction) => {
|
|
82
|
+
const scopedSources = transaction.collection('sources');
|
|
83
|
+
const scopedVersions = transaction.collection('document_versions');
|
|
84
|
+
const scopedElements = transaction.collection('document_elements');
|
|
85
|
+
const scopedChunks = transaction.collection('document_chunks');
|
|
86
|
+
const previous = await scopedSources.get(bundle.source.id);
|
|
87
|
+
await scopedVersions.put(staging);
|
|
88
|
+
for (const element of bundle.elements)
|
|
89
|
+
await scopedElements.put(element);
|
|
90
|
+
for (const chunk of bundle.chunks)
|
|
91
|
+
await scopedChunks.put(chunk);
|
|
92
|
+
const active = { ...staging, status: 'active', activatedAt: bundle.source.fetchedAt };
|
|
93
|
+
await scopedVersions.put(active);
|
|
94
|
+
await scopedSources.put({ ...bundle.source, status: 'ready', error: undefined });
|
|
95
|
+
const previousId = previous?.activeVersionId;
|
|
96
|
+
if (previousId !== undefined && previousId !== bundle.version.id) {
|
|
97
|
+
const previousVersion = await scopedVersions.get(previousId);
|
|
98
|
+
if (previousVersion !== undefined) {
|
|
99
|
+
await scopedVersions.put({ ...previousVersion, status: 'superseded', supersededAt: bundle.source.fetchedAt });
|
|
100
|
+
}
|
|
101
|
+
await cleanupVersion(transaction, previousId);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async recordFailure(source, version) {
|
|
107
|
+
await serial(source.id, async () => {
|
|
108
|
+
assertDocumentSource(source);
|
|
109
|
+
const current = await sources.get(source.id);
|
|
110
|
+
const activeVersionId = current?.activeVersionId ?? source.activeVersionId;
|
|
111
|
+
const retained = activeVersionId === undefined
|
|
112
|
+
? source
|
|
113
|
+
: { ...(current ?? source), activeVersionId, status: 'ready', error: source.error, fetchedAt: source.fetchedAt };
|
|
114
|
+
await sources.put(retained);
|
|
115
|
+
if (version !== undefined) {
|
|
116
|
+
assertDocumentVersion(version);
|
|
117
|
+
const known = await versions.get(version.id);
|
|
118
|
+
if (known?.status !== 'active')
|
|
119
|
+
await versions.put({ ...version, status: 'failed' });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity repository — content-addressed, credential-free run
|
|
3
|
+
* identities, stored once and referenced by id from runs and chats.
|
|
4
|
+
*
|
|
5
|
+
* A stored identity is immutable by construction: its id is the
|
|
6
|
+
* canonical SHA-256 of its own payload, recomputed HERE before every
|
|
7
|
+
* write, so a caller cannot store a mutated identity under a stale id
|
|
8
|
+
* or repeat mutable settings snapshots into the log. Validation is the
|
|
9
|
+
* config package's — the same schema every report envelope uses — and a
|
|
10
|
+
* document that does not validate or does not hash to its claimed id is
|
|
11
|
+
* refused as a value.
|
|
12
|
+
*/
|
|
13
|
+
import { type Issue, type RunIdentity } from '@tangleai/config';
|
|
14
|
+
import type { TangleDb } from './db.ts';
|
|
15
|
+
export interface IdentityRepository {
|
|
16
|
+
/** Validate, verify the content address, and store (idempotent). */
|
|
17
|
+
put(identity: RunIdentity): Promise<{
|
|
18
|
+
ok: true;
|
|
19
|
+
id: string;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
issues: Issue[];
|
|
23
|
+
}>;
|
|
24
|
+
get(id: string): Promise<RunIdentity | undefined>;
|
|
25
|
+
list(): Promise<RunIdentity[]>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createIdentityRepository(db: TangleDb): IdentityRepository;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity repository — content-addressed, credential-free run
|
|
3
|
+
* identities, stored once and referenced by id from runs and chats.
|
|
4
|
+
*
|
|
5
|
+
* A stored identity is immutable by construction: its id is the
|
|
6
|
+
* canonical SHA-256 of its own payload, recomputed HERE before every
|
|
7
|
+
* write, so a caller cannot store a mutated identity under a stale id
|
|
8
|
+
* or repeat mutable settings snapshots into the log. Validation is the
|
|
9
|
+
* config package's — the same schema every report envelope uses — and a
|
|
10
|
+
* document that does not validate or does not hash to its claimed id is
|
|
11
|
+
* refused as a value.
|
|
12
|
+
*/
|
|
13
|
+
import { identityIdOf, validateRunIdentity } from '@tangleai/config';
|
|
14
|
+
import { asRows } from "./memory-store.js";
|
|
15
|
+
export function createIdentityRepository(db) {
|
|
16
|
+
const collection = db.collection('config_identities');
|
|
17
|
+
return {
|
|
18
|
+
async put(identity) {
|
|
19
|
+
const outcome = validateRunIdentity(identity);
|
|
20
|
+
if (!outcome.ok)
|
|
21
|
+
return outcome;
|
|
22
|
+
const { identityId, ...body } = outcome.value;
|
|
23
|
+
const recomputed = await identityIdOf(body);
|
|
24
|
+
if (recomputed !== identityId) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
issues: [{ code: 'TCFG1007', path: '/identityId', detail: 'the identity does not hash to its claimed id; a mutated identity cannot be stored under a stale address' }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
await collection.put({ id: identityId, value: outcome.value });
|
|
31
|
+
return { ok: true, id: identityId };
|
|
32
|
+
},
|
|
33
|
+
async get(id) {
|
|
34
|
+
const row = await collection.get(id);
|
|
35
|
+
return row?.value;
|
|
36
|
+
},
|
|
37
|
+
async list() {
|
|
38
|
+
const rows = asRows(await collection.execute({ $for: { r: '$[*]' }, $return: '$r' }));
|
|
39
|
+
rows.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
40
|
+
return rows.map((row) => row.value);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @tangleai/store barrel. */
|
|
2
|
+
export { TANGLE_DB_MODEL } from './model.ts';
|
|
3
|
+
export { openTangleDb, pickDriver } from './db.ts';
|
|
4
|
+
export type { TangleDb, DbCollection, OpenTangleDbOptions } from './db.ts';
|
|
5
|
+
export { createDbMemoryStore, asRows } from './memory-store.ts';
|
|
6
|
+
export type { DbMemoryStoreOptions } from './memory-store.ts';
|
|
7
|
+
export { createRunLog } from './runs.ts';
|
|
8
|
+
export type { RunLog, RunRecord, RunEvent, RunLogOptions, RunIdentityStatus, RunView } from './runs.ts';
|
|
9
|
+
export { createIdentityRepository } from './identities.ts';
|
|
10
|
+
export type { IdentityRepository } from './identities.ts';
|
|
11
|
+
export { createDocumentStore } from './document-store.ts';
|
|
12
|
+
export { createMasStore } from './mas-store.ts';
|
|
13
|
+
export { createDbLedgerStorage } from './ledger-storage.ts';
|
|
14
|
+
export type { MasStoreOptions } from './mas-store.ts';
|
|
15
|
+
export { enqueueMasSegment, ensurePendingMasSegments, createMasSegmentWorker, createMasSegmentHandlers, namespacedRegionCheckpoints, MasSegmentRefusal, } from './mas-jobs.ts';
|
|
16
|
+
export type { MasSegmentPayload, EnqueueMasSegmentPlan, MasSegmentContext, MasSegmentExecutor, MasWorkerOptions, MasWorker, RegionCheckpointStore, } from './mas-jobs.ts';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @tangleai/store barrel. */
|
|
2
|
+
export { TANGLE_DB_MODEL } from "./model.js";
|
|
3
|
+
export { openTangleDb, pickDriver } from "./db.js";
|
|
4
|
+
export { createDbMemoryStore, asRows } from "./memory-store.js";
|
|
5
|
+
export { createRunLog } from "./runs.js";
|
|
6
|
+
export { createIdentityRepository } from "./identities.js";
|
|
7
|
+
export { createDocumentStore } from "./document-store.js";
|
|
8
|
+
export { createMasStore } from "./mas-store.js";
|
|
9
|
+
export { createDbLedgerStorage } from "./ledger-storage.js";
|
|
10
|
+
export { enqueueMasSegment, ensurePendingMasSegments, createMasSegmentWorker, createMasSegmentHandlers, namespacedRegionCheckpoints, MasSegmentRefusal, } from "./mas-jobs.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Atomic AI ledger storage over the existing key/value collection. The suite
|
|
2
|
+
* owns ledger staging, conflict retries and retention; this adapter publishes
|
|
3
|
+
* each synchronous mutation through the database's transaction owner. */
|
|
4
|
+
import { cloneJson, equalsJson } from '@jarenjs/core/object';
|
|
5
|
+
import { asRows } from "./memory-store.js";
|
|
6
|
+
export function createDbLedgerStorage(db, namespace) {
|
|
7
|
+
if (typeof namespace !== 'string' || namespace.length === 0)
|
|
8
|
+
throw new TypeError('ledger storage needs a namespace');
|
|
9
|
+
const prefix = `ai-ledger/${JSON.stringify(namespace)}/`;
|
|
10
|
+
const slots = db.collection('settings');
|
|
11
|
+
const query = (part) => ({
|
|
12
|
+
$for: { s: '$[*]' }, $where: { '$starts-with': ['$s.key', part] },
|
|
13
|
+
$orderby: ['$s.key'], $return: '$s',
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
get: async (key) => (await slots.get(prefix + key))?.value,
|
|
17
|
+
set: async (key, value) => { await slots.put({ key: prefix + key, value: cloneJson(value) }); },
|
|
18
|
+
delete: async (key) => { await slots.delete(prefix + key); },
|
|
19
|
+
keys: async (part = '') => asRows(await slots.execute(query(prefix + part))).map((slot) => slot.key.slice(prefix.length)),
|
|
20
|
+
mutate: async (scope, transform) => db.transaction(async (tx) => {
|
|
21
|
+
const handle = tx.collection('settings');
|
|
22
|
+
const prefixes = typeof scope === 'string' ? [scope] : scope.prefixes ?? [];
|
|
23
|
+
const keys = typeof scope === 'string' ? [] : scope.keys ?? [];
|
|
24
|
+
const matches = (key) => keys.includes(key) || prefixes.some((part) => key.startsWith(part));
|
|
25
|
+
const rows = asRows(await handle.execute(query(prefix))).filter((slot) => matches(slot.key.slice(prefix.length)));
|
|
26
|
+
const current = Object.fromEntries(rows.map((slot) => [slot.key.slice(prefix.length), slot.value]));
|
|
27
|
+
const outcome = transform(cloneJson(current));
|
|
28
|
+
if (!outcome || typeof outcome.then === 'function')
|
|
29
|
+
throw new TypeError('ledger mutations must be synchronous');
|
|
30
|
+
if (outcome.next !== undefined) {
|
|
31
|
+
const next = cloneJson(outcome.next);
|
|
32
|
+
if (next === null || typeof next !== 'object' || Array.isArray(next) || Object.keys(next).some((key) => !matches(key))) {
|
|
33
|
+
throw new TypeError('ledger mutation escaped its scope');
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(current))
|
|
36
|
+
if (!Object.hasOwn(next, key))
|
|
37
|
+
await handle.delete(prefix + key);
|
|
38
|
+
for (const [key, value] of Object.entries(next)) {
|
|
39
|
+
if (!Object.hasOwn(current, key) || !equalsJson(current[key], value))
|
|
40
|
+
await handle.put({ key: prefix + key, value });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return outcome.result;
|
|
44
|
+
}, { mode: 'immediate' }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAS durable segments over the suite queue — composition, not a queue.
|
|
3
|
+
*
|
|
4
|
+
* Enqueue is the suite's own idempotent caller-supplied-id enqueue with
|
|
5
|
+
* the derived segment id `<masRunId>:<segment>` and the versioned kind
|
|
6
|
+
* `mas:<executableRevision>`; re-enqueueing one segment is the suite's
|
|
7
|
+
* no-op, and an unknown versioned kind is simply never claimed by a
|
|
8
|
+
* worker that does not register it. The worker is
|
|
9
|
+
* `store.jobs.createWorker`; the checkpoint store every region hands
|
|
10
|
+
* `compileDag` is a thin NAMESPACE adaptation over the suite's
|
|
11
|
+
* `context.checkpoints` — load filters by
|
|
12
|
+
* `<region>/<branch>/<iteration>/`, save prefixes the lowered node id
|
|
13
|
+
* and delegates (keeping the suite's lease guard and JSON validation),
|
|
14
|
+
* a region result is a prefixed delegated save, and only a terminal
|
|
15
|
+
* outcome for the whole runnable segment calls the suite store's
|
|
16
|
+
* `complete`, which atomically records the segment result, marks the
|
|
17
|
+
* job done and prunes that segment's checkpoint rows. It grows no map,
|
|
18
|
+
* table or serialization of its own.
|
|
19
|
+
*
|
|
20
|
+
* Before any checkpoint loads, the handler reads the MAS run and
|
|
21
|
+
* refuses `TMAS2002` unless the job id, payload, workflow version,
|
|
22
|
+
* registry snapshot and executable revision all agree — Jaren DAG
|
|
23
|
+
* resume under different document bytes is undefined, so a mismatch
|
|
24
|
+
* never reaches a checkpoint.
|
|
25
|
+
*/
|
|
26
|
+
import { type MasIssue, type MasRun, type MasStore } from '@tangleai/mas';
|
|
27
|
+
import type { TangleDb } from './db.ts';
|
|
28
|
+
import type { JobWorker, JobWorkerOptions } from '@jarenjs/db';
|
|
29
|
+
export interface MasSegmentPayload {
|
|
30
|
+
runId: string;
|
|
31
|
+
segment: number;
|
|
32
|
+
workflowVersionId: string;
|
|
33
|
+
registryRevision: string;
|
|
34
|
+
executableRevision: string;
|
|
35
|
+
/** The typed resume event for a post-interaction segment, or null for segment 0. */
|
|
36
|
+
resume: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface EnqueueMasSegmentPlan {
|
|
39
|
+
runId: string;
|
|
40
|
+
segment: number;
|
|
41
|
+
workflowVersionId: string;
|
|
42
|
+
registryRevision: string;
|
|
43
|
+
executableRevision: string;
|
|
44
|
+
resume?: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** Idempotent per segment: the suite keeps one row per caller-supplied id. */
|
|
47
|
+
export declare function enqueueMasSegment(db: TangleDb, plan: EnqueueMasSegmentPlan): Promise<string>;
|
|
48
|
+
/** The suite checkpoint store contract compileDag consumes. */
|
|
49
|
+
export interface RegionCheckpointStore {
|
|
50
|
+
load(runId: string): unknown;
|
|
51
|
+
save(runId: string, nodeId: string, value: unknown): unknown;
|
|
52
|
+
complete(runId: string, result: unknown): unknown;
|
|
53
|
+
}
|
|
54
|
+
interface SuiteCheckpoints {
|
|
55
|
+
load(runId: string): unknown;
|
|
56
|
+
save(runId: string, nodeId: string, value: unknown): unknown;
|
|
57
|
+
complete(runId: string, result: unknown): unknown;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The namespace adaptation: one region incarnation's view of the
|
|
61
|
+
* segment's suite checkpoint rows. Region-level `complete` records a
|
|
62
|
+
* prefixed result via delegated save and deliberately does NOT mark the
|
|
63
|
+
* job done.
|
|
64
|
+
*/
|
|
65
|
+
export declare function namespacedRegionCheckpoints(suite: SuiteCheckpoints, segmentJobId: string, namespace: string): RegionCheckpointStore;
|
|
66
|
+
export interface MasSegmentContext {
|
|
67
|
+
run: MasRun;
|
|
68
|
+
payload: MasSegmentPayload;
|
|
69
|
+
segmentJobId: string;
|
|
70
|
+
claimSeq: number;
|
|
71
|
+
signal: AbortSignal;
|
|
72
|
+
/** One region incarnation's namespaced checkpoint view. */
|
|
73
|
+
checkpointsFor(namespace: string): RegionCheckpointStore;
|
|
74
|
+
/**
|
|
75
|
+
* The segment's terminal outcome: atomically records the result, marks
|
|
76
|
+
* the suite job done and prunes this segment's checkpoint rows. Called
|
|
77
|
+
* exactly once per runnable segment — whole-run completion/failure or a
|
|
78
|
+
* durable interaction wait.
|
|
79
|
+
*/
|
|
80
|
+
completeSegment(result: unknown): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
export type MasSegmentExecutor = (context: MasSegmentContext) => Promise<void>;
|
|
83
|
+
export interface MasWorkerOptions extends Omit<JobWorkerOptions, 'handlers'> {
|
|
84
|
+
/** The executable revisions this worker can run — its registered kinds. */
|
|
85
|
+
executableRevisions: string[];
|
|
86
|
+
execute: MasSegmentExecutor;
|
|
87
|
+
}
|
|
88
|
+
export type MasWorker = JobWorker;
|
|
89
|
+
/** A refusal the queue records as the job's failure value. */
|
|
90
|
+
export declare class MasSegmentRefusal extends Error {
|
|
91
|
+
issue: MasIssue;
|
|
92
|
+
constructor(issue: MasIssue);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The semantic resume reconciler over the published queue — not another
|
|
96
|
+
* queue. `respondInteraction` accepts exactly one typed response and
|
|
97
|
+
* moves the run to `resume_pending` with a reserved zero-padded segment
|
|
98
|
+
* id; the enqueue of that derived id happens OUTSIDE the semantic
|
|
99
|
+
* transaction (the suite queue cannot be assumed to join it), so this
|
|
100
|
+
* scan makes the seam idempotent: it enqueues every reserved segment
|
|
101
|
+
* (the suite's caller-supplied-id no-op absorbs a crash after enqueue)
|
|
102
|
+
* and CAS-advances `resume_pending -> queued`. The second identical
|
|
103
|
+
* reconciliation changes zero rows and reports zero; every enqueue,
|
|
104
|
+
* skip and conflict is a counted value.
|
|
105
|
+
*/
|
|
106
|
+
export declare function ensurePendingMasSegments(db: TangleDb, masStore: MasStore): Promise<{
|
|
107
|
+
examined: number;
|
|
108
|
+
enqueued: number;
|
|
109
|
+
queued: number;
|
|
110
|
+
skipped: number;
|
|
111
|
+
}>;
|
|
112
|
+
export type MasHandlerContext = Pick<Parameters<JobWorkerOptions['handlers'][string]>[1], 'job' | 'checkpoints' | 'signal'>;
|
|
113
|
+
export type MasSegmentHandlers = Record<string, (payload: unknown, context: MasHandlerContext) => Promise<unknown>>;
|
|
114
|
+
/**
|
|
115
|
+
* The versioned segment handlers a worker registers — exported so the
|
|
116
|
+
* crash matrix can drive one claim at a time deterministically through
|
|
117
|
+
* `store.jobs.claim` with an injected clock, without a polling loop.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createMasSegmentHandlers(masStore: MasStore, options: Pick<MasWorkerOptions, 'executableRevisions' | 'execute' | 'owner'>): MasSegmentHandlers;
|
|
120
|
+
export declare function createMasSegmentWorker(db: TangleDb, masStore: MasStore, options: MasWorkerOptions): MasWorker;
|
|
121
|
+
export {};
|