@4fu/arbor-notes-sqlite 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +187 -2
- package/diff.d.ts +12 -0
- package/diff.js +106 -0
- package/index.d.ts +13 -0
- package/index.js +10 -0
- package/notes.d.ts +126 -0
- package/notes.js +867 -0
- package/package.json +44 -4
- package/refs.d.ts +24 -0
- package/refs.js +49 -0
- package/schema.d.ts +34 -0
- package/schema.js +141 -0
- package/score.d.ts +67 -0
- package/score.js +203 -0
- package/search.d.ts +27 -0
- package/search.js +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 4fuu
|
|
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
CHANGED
|
@@ -1,3 +1,188 @@
|
|
|
1
|
-
#
|
|
1
|
+
# arbor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
arbor is a framework for an agent without sessions: many context windows run
|
|
4
|
+
at once over one shared record, so work moves in parallel, replies come fast,
|
|
5
|
+
and every window can find everything that was ever done.
|
|
6
|
+
|
|
7
|
+
A deployment composes the pieces it wants and registers plugins for the domains
|
|
8
|
+
it serves. The framework guarantees what an agent cannot guarantee for itself -
|
|
9
|
+
the record, isolation, exactly-once effects, the checks, dispatch - and leaves
|
|
10
|
+
everything that takes judgment, such as git, merges and conflicts, to the agent.
|
|
11
|
+
|
|
12
|
+
## Core ideas
|
|
13
|
+
|
|
14
|
+
- **No sessions to manage.** Work runs in disposable context windows. Any
|
|
15
|
+
number run at the same time; none of them owns anything, and a window that
|
|
16
|
+
ends loses nothing. The model looks up what it needs instead of carrying it.
|
|
17
|
+
- **One record, shared live.** Every input, tool call, result, send and note
|
|
18
|
+
revision goes into one journal and is visible to every window as
|
|
19
|
+
soon as it is written. Records never merge and are never lost.
|
|
20
|
+
- **Dispatch by a judge.** A judge (Jev by default, replaceable) decides which
|
|
21
|
+
live window takes a new input, or opens a new one. There is no router session
|
|
22
|
+
and no main line.
|
|
23
|
+
- **Current without locks.** Windows run in parallel. New events on a
|
|
24
|
+
conversation a window is in are steered into it as they arrive; only a note
|
|
25
|
+
write is checked against the revision the window read.
|
|
26
|
+
- **Mechanisms in the framework, policy in the agent.** The framework
|
|
27
|
+
guarantees what an agent cannot guarantee for itself - the record, isolation,
|
|
28
|
+
exactly-once effects, the checks, dispatch. Everything that takes judgment,
|
|
29
|
+
such as git, merges and conflicts, is the agent's.
|
|
30
|
+
- **A directory per window.** Each window gets its own directory, and a
|
|
31
|
+
continuation keeps working in its predecessor's directory in place. It is not
|
|
32
|
+
bound to a repository; the agent clones and pushes what it needs.
|
|
33
|
+
- **A self-changing environment.** The model adds and changes skills, MCP
|
|
34
|
+
servers and resident services itself; a `reload` validates the change and
|
|
35
|
+
broadcasts it to every live window as a delta of names and descriptions.
|
|
36
|
+
- **Minimal limits.** The framework sets no policy. Permissions, privacy
|
|
37
|
+
review, budgets and approvals belong to deployments built on it, through
|
|
38
|
+
hooks.
|
|
39
|
+
- **Direct tools plus code mode.** Decisions are direct tools; everything else
|
|
40
|
+
is a module called from `code_exec`. There is no CLI.
|
|
41
|
+
- **Every module is replaceable.** The agent backend (pi by default), the
|
|
42
|
+
journal, notes, workspaces, the judge, channels and the rest are interfaces
|
|
43
|
+
([docs/architecture.md](docs/architecture.md#interfaces)).
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Node 24 or newer. Every package is published separately, so a deployment
|
|
48
|
+
installs the core and the defaults it wants:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
|
|
52
|
+
@4fu/arbor-environment-dir @4fu/arbor-supervisor-process @4fu/arbor-agent-pi \
|
|
53
|
+
@4fu/arbor-workspace-dir
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`@4fu/arbor-plugin-http` is optional; it is the example plugin below.
|
|
57
|
+
`@4fu/arbor-segmenter-jieba` is an optional peer of the journal.
|
|
58
|
+
|
|
59
|
+
## A minimal host
|
|
60
|
+
|
|
61
|
+
The smallest host needs a journal and a backend. Adding the defaults and one
|
|
62
|
+
plugin is what a deployment usually starts from:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { mkdtemp } from "node:fs/promises";
|
|
66
|
+
import { tmpdir } from "node:os";
|
|
67
|
+
import { join } from "node:path";
|
|
68
|
+
import { Arbor } from "@4fu/arbor-core";
|
|
69
|
+
import { DirectoryEnvironment } from "@4fu/arbor-environment-dir";
|
|
70
|
+
import { SqliteJournal } from "@4fu/arbor-journal-sqlite";
|
|
71
|
+
import { SqliteNoteStore } from "@4fu/arbor-notes-sqlite";
|
|
72
|
+
import { ProcessSupervisor } from "@4fu/arbor-supervisor-process";
|
|
73
|
+
import { DirectoryWorkspace } from "@4fu/arbor-workspace-dir";
|
|
74
|
+
import { PiAgentBackend } from "@4fu/arbor-agent-pi";
|
|
75
|
+
import { HttpChannel, httpPlugin } from "@4fu/arbor-plugin-http";
|
|
76
|
+
|
|
77
|
+
const root = await mkdtemp(join(tmpdir(), "arbor-"));
|
|
78
|
+
|
|
79
|
+
// Every module below is a default implementation of an interface. Replace any
|
|
80
|
+
// one by passing your own; the host does not change.
|
|
81
|
+
const journal = new SqliteJournal({ path: join(root, "journal.sqlite") });
|
|
82
|
+
const notes = new SqliteNoteStore({ path: join(root, "notes.sqlite"), journal });
|
|
83
|
+
const supervisor = new ProcessSupervisor({ root: join(root, "services") });
|
|
84
|
+
const environment = new DirectoryEnvironment({ root: join(root, "env"), supervisor });
|
|
85
|
+
const workspace = new DirectoryWorkspace({ root: join(root, "workspaces") });
|
|
86
|
+
|
|
87
|
+
// A plugin adds; it never patches the core. This one is a chat channel: a
|
|
88
|
+
// webhook in, a window's sends out, and a page that drives the whole host.
|
|
89
|
+
let arbor;
|
|
90
|
+
const channel = new HttpChannel({ host: "127.0.0.1", port: 8787, board: () => arbor.boardLines() });
|
|
91
|
+
arbor = new Arbor({
|
|
92
|
+
journal,
|
|
93
|
+
notes,
|
|
94
|
+
backend: new PiAgentBackend(),
|
|
95
|
+
workspace,
|
|
96
|
+
environment,
|
|
97
|
+
supervisor,
|
|
98
|
+
plugins: [httpPlugin({ channel })],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await arbor.start();
|
|
102
|
+
await channel.listen();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`arbor.open(task)` dispatches one task, and the journal is where everything that
|
|
106
|
+
happened can be read back ([docs/journal.md](docs/journal.md)). The workspace a
|
|
107
|
+
window gets, and how a continuation keeps working in it, are in
|
|
108
|
+
[docs/workspaces.md](docs/workspaces.md).
|
|
109
|
+
|
|
110
|
+
## Packages
|
|
111
|
+
|
|
112
|
+
| Package | What it is |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| [`@4fu/arbor-interfaces`](packages/interfaces) | every replaceable interface, plus the core's own shapes. Types only. |
|
|
115
|
+
| [`@4fu/arbor-errors`](packages/errors) | the error shape every refusal uses: the rule and the next action |
|
|
116
|
+
| [`@4fu/arbor-prompt`](packages/prompt) | typed blocks, the one deterministic renderer and the replaceable text table |
|
|
117
|
+
| [`@4fu/arbor-tools`](packages/tools) | the entry points: direct tools, code mode modules, the host API |
|
|
118
|
+
| [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
|
|
119
|
+
| [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
|
|
120
|
+
| [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
|
|
121
|
+
| [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
|
|
122
|
+
| [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
123
|
+
| [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
124
|
+
| [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
125
|
+
| [`@4fu/arbor-agent-pi`](packages/agent-pi) | the default `AgentBackend`: pi-agent-core in process |
|
|
126
|
+
| [`@4fu/arbor-plugin-http`](packages/plugin-http) | the HTTP test channel: a webhook in, sends out, a page that drives the whole host |
|
|
127
|
+
| [`@4fu/arbor-core`](packages/core) | the service layer: one implementation of every operation - the window lifecycle, dispatch, coordination, the board, notes access, workspaces, effects and schedules |
|
|
128
|
+
|
|
129
|
+
### Defaults are examples
|
|
130
|
+
|
|
131
|
+
Every default above is a reference implementation of its interface, written to
|
|
132
|
+
be read and replaced. A real deployment is expected to swap out the ones that do
|
|
133
|
+
not fit: the SQLite journal and notes, the directory workspace, the process
|
|
134
|
+
supervisor, the environment directory, the HTTP test plugin and the Jev judge
|
|
135
|
+
are all examples of what an interface can be satisfied by, not commitments.
|
|
136
|
+
What a deployment keeps is its own choice, and the interfaces are the contract
|
|
137
|
+
([docs/architecture.md](docs/architecture.md#interfaces)).
|
|
138
|
+
|
|
139
|
+
Each package's own documentation holds the limits of that default, so a
|
|
140
|
+
deployment can see what it would be taking on before it adopts one.
|
|
141
|
+
|
|
142
|
+
## Documents
|
|
143
|
+
|
|
144
|
+
| Page | Contents |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| [docs/overview.md](docs/overview.md) | goals, concepts, principles, routes considered |
|
|
147
|
+
| [docs/architecture.md](docs/architecture.md) | layers, interfaces, stack, models, plugins, hooks, data placement |
|
|
148
|
+
| [docs/journal.md](docs/journal.md) | events and their order, targets, watermarks, external ids, refs, tracing, media, redaction |
|
|
149
|
+
| [docs/windows.md](docs/windows.md) | window lifecycle, prompt layout, reminders, continuation |
|
|
150
|
+
| [docs/dispatch.md](docs/dispatch.md) | task sources, judge decisions, insertion timing, backpressure, scheduled tasks |
|
|
151
|
+
| [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
|
|
152
|
+
| [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
|
|
153
|
+
| [docs/memory.md](docs/memory.md) | notes, the active set, anchors, links |
|
|
154
|
+
| [docs/workspaces.md](docs/workspaces.md) | per-window directories: isolation, continuity, what the agent does |
|
|
155
|
+
| [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
|
|
156
|
+
| [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
|
|
157
|
+
| [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
|
|
158
|
+
|
|
159
|
+
## Repository layout
|
|
160
|
+
|
|
161
|
+
`pnpm check` typechecks and tests every package on its TypeScript sources.
|
|
162
|
+
`pnpm build` emits the `dist` publishing needs, and `pnpm pack` shows what each
|
|
163
|
+
tarball would contain ([docs/release.md](docs/release.md)).
|
|
164
|
+
|
|
165
|
+
| Path | Layer |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `packages/interfaces` | the interfaces layer: every replaceable interface, plus the core's own shapes. Types only. |
|
|
168
|
+
| `packages/errors` | the error shape every refusal uses: the rule and the next action |
|
|
169
|
+
| `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
|
|
170
|
+
| `packages/segmenter-jieba` | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
|
|
171
|
+
| `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
|
|
172
|
+
| `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
173
|
+
| `packages/workspace-dir` | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place, and the lookup of another window's directory it contributes to the catalog |
|
|
174
|
+
| `packages/environment-dir` | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
175
|
+
| `packages/supervisor-process` | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
176
|
+
| `packages/plugin-http` | the HTTP test channel: a webhook in, sends out, a page that drives the whole host |
|
|
177
|
+
| `packages/prompt` | typed blocks, the one deterministic renderer and the replaceable text table |
|
|
178
|
+
| `packages/agent-pi` | the default `AgentBackend`: pi-agent-core in process |
|
|
179
|
+
| `packages/tools` | the entry points: direct tools, code mode modules, the host API |
|
|
180
|
+
| `packages/core` | the service layer: one implementation of every operation - the window lifecycle, dispatch, coordination, the board, notes access, workspaces, effects, schedules and the snapshot epochs |
|
|
181
|
+
|
|
182
|
+
`mise.toml` pins the toolchain - Node and pnpm - installed under
|
|
183
|
+
`~/.local/share/mise`, and `.agents/setup` prepares a machine from it. A machine
|
|
184
|
+
without mise is expected to carry Node 24 or newer and pnpm 12 already.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT, see [LICENSE](LICENSE).
|
package/diff.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The difference between two bodies, for a note conflict (docs/memory.md).
|
|
3
|
+
*
|
|
4
|
+
* A refused write carries what changed since the revision it read: the
|
|
5
|
+
* revisions in between, their windows and the differences. The differences are
|
|
6
|
+
* a unified diff of the body the writer read against the body that is there
|
|
7
|
+
* now, so the model merges them and writes again without reading the whole note
|
|
8
|
+
* twice. A body too large to diff is reported as such; the revisions it has to
|
|
9
|
+
* read are named in the same refusal.
|
|
10
|
+
*/
|
|
11
|
+
/** The unified diff of `from` against `to`, or `null` when nothing changed. */
|
|
12
|
+
export declare function bodyDiff(from: string, to: string): string | null;
|
package/diff.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The difference between two bodies, for a note conflict (docs/memory.md).
|
|
3
|
+
*
|
|
4
|
+
* A refused write carries what changed since the revision it read: the
|
|
5
|
+
* revisions in between, their windows and the differences. The differences are
|
|
6
|
+
* a unified diff of the body the writer read against the body that is there
|
|
7
|
+
* now, so the model merges them and writes again without reading the whole note
|
|
8
|
+
* twice. A body too large to diff is reported as such; the revisions it has to
|
|
9
|
+
* read are named in the same refusal.
|
|
10
|
+
*/
|
|
11
|
+
/** Lines of unchanged context each hunk keeps. */
|
|
12
|
+
const CONTEXT = 3;
|
|
13
|
+
/** Beyond this many cells the diff is dropped for a line that says so. */
|
|
14
|
+
const CELL_LIMIT = 4_000_000;
|
|
15
|
+
/** The unified diff of `from` against `to`, or `null` when nothing changed. */
|
|
16
|
+
export function bodyDiff(from, to) {
|
|
17
|
+
const before = from === "" ? [] : from.split("\n");
|
|
18
|
+
const after = to === "" ? [] : to.split("\n");
|
|
19
|
+
if (before.length * after.length > CELL_LIMIT) {
|
|
20
|
+
return "the body is too large to diff; read both revisions";
|
|
21
|
+
}
|
|
22
|
+
const edits = diffLines(before, after);
|
|
23
|
+
if (edits.every((edit) => edit.kind === " "))
|
|
24
|
+
return null;
|
|
25
|
+
return render(edits);
|
|
26
|
+
}
|
|
27
|
+
function diffLines(before, after) {
|
|
28
|
+
const rows = before.length;
|
|
29
|
+
const columns = after.length;
|
|
30
|
+
// The longest common subsequence, from the end backwards.
|
|
31
|
+
const table = Array.from({ length: rows + 1 }, () => new Array(columns + 1).fill(0));
|
|
32
|
+
for (let i = rows - 1; i >= 0; i -= 1) {
|
|
33
|
+
for (let j = columns - 1; j >= 0; j -= 1) {
|
|
34
|
+
const same = before[i] === after[j];
|
|
35
|
+
const down = table[i + 1]?.[j] ?? 0;
|
|
36
|
+
const right = table[i]?.[j + 1] ?? 0;
|
|
37
|
+
table[i][j] = same ? (table[i + 1]?.[j + 1] ?? 0) + 1 : Math.max(down, right);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const edits = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
let j = 0;
|
|
43
|
+
while (i < rows && j < columns) {
|
|
44
|
+
if (before[i] === after[j]) {
|
|
45
|
+
edits.push({ kind: " ", line: i + 1, text: before[i] });
|
|
46
|
+
i += 1;
|
|
47
|
+
j += 1;
|
|
48
|
+
}
|
|
49
|
+
else if ((table[i + 1]?.[j] ?? 0) >= (table[i]?.[j + 1] ?? 0)) {
|
|
50
|
+
edits.push({ kind: "-", line: i + 1, text: before[i] });
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
edits.push({ kind: "+", line: j + 1, text: after[j] });
|
|
55
|
+
j += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
while (i < rows) {
|
|
59
|
+
edits.push({ kind: "-", line: i + 1, text: before[i] });
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
while (j < columns) {
|
|
63
|
+
edits.push({ kind: "+", line: j + 1, text: after[j] });
|
|
64
|
+
j += 1;
|
|
65
|
+
}
|
|
66
|
+
return edits;
|
|
67
|
+
}
|
|
68
|
+
/** The edits worth showing: every change with its context, as unified hunks. */
|
|
69
|
+
function render(edits) {
|
|
70
|
+
const keep = new Set();
|
|
71
|
+
edits.forEach((edit, index) => {
|
|
72
|
+
if (edit.kind === " ")
|
|
73
|
+
return;
|
|
74
|
+
for (let near = Math.max(0, index - CONTEXT); near <= Math.min(edits.length - 1, index + CONTEXT); near += 1) {
|
|
75
|
+
keep.add(near);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const lines = [];
|
|
79
|
+
let index = 0;
|
|
80
|
+
while (index < edits.length) {
|
|
81
|
+
if (!keep.has(index)) {
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let end = index;
|
|
86
|
+
while (end < edits.length && keep.has(end))
|
|
87
|
+
end += 1;
|
|
88
|
+
lines.push(...hunk(edits.slice(index, end)));
|
|
89
|
+
index = end;
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
function hunk(edits) {
|
|
94
|
+
const from = edits.filter((edit) => edit.kind !== "+");
|
|
95
|
+
const to = edits.filter((edit) => edit.kind !== "-");
|
|
96
|
+
// A hunk that only adds lines starts where the first added line is, one line
|
|
97
|
+
// earlier on the side it was added to; a hunk that only removes lines the
|
|
98
|
+
// same way the other way round.
|
|
99
|
+
const firstFrom = from[0]?.line ?? (to[0].line ?? 1) - 1;
|
|
100
|
+
const firstTo = to[0]?.line ?? (from[0].line ?? 1) - 1;
|
|
101
|
+
const lines = [`@@ -${firstFrom},${from.length} +${firstTo},${to.length} @@`];
|
|
102
|
+
for (const edit of edits) {
|
|
103
|
+
lines.push(`${edit.kind}${edit.text}`);
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @4fu/arbor-notes-sqlite - the default `NoteStore`.
|
|
3
|
+
*/
|
|
4
|
+
export { SqliteNoteStore, SUMMARY_LIMIT, DEFAULT_BODY_LIMIT, MAX_BODY_LIMIT } from "./notes.ts";
|
|
5
|
+
export { DEFAULT_MIN_INTERVAL_MS, DEFAULT_ACTIVE_BUDGET, DEFAULT_JUDGE_SHARE, DEFAULT_HALF_LIFE_MS, deriveSummary, } from "./notes.ts";
|
|
6
|
+
export type { SqliteNoteStoreOptions } from "./notes.ts";
|
|
7
|
+
export { SCHEMA_VERSION, FTS_TABLE, createSchema } from "./schema.ts";
|
|
8
|
+
export { noteIdOf, noteRefOf, parseNoteId, parseNoteRef, noteRefForms, storedWindowRef, storedEventRef, } from "./refs.ts";
|
|
9
|
+
export { USE_WEIGHTS, MAX_CREDIT_HOPS, decay, estimateTokens, linkedCredit, loadCredit, loadEdges, pickByBudget, applyJudgeShare, scoreNotes, } from "./score.ts";
|
|
10
|
+
export type { ScoredNote } from "./score.ts";
|
|
11
|
+
export { MIN_TRIGRAM_CHARS, substringClause, holdingIds, escapeLike, likePattern, quoteFts, } from "./search.ts";
|
|
12
|
+
export type { SubstringClause } from "./search.ts";
|
|
13
|
+
export { bodyDiff } from "./diff.ts";
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @4fu/arbor-notes-sqlite - the default `NoteStore`.
|
|
3
|
+
*/
|
|
4
|
+
export { SqliteNoteStore, SUMMARY_LIMIT, DEFAULT_BODY_LIMIT, MAX_BODY_LIMIT } from "./notes.js";
|
|
5
|
+
export { DEFAULT_MIN_INTERVAL_MS, DEFAULT_ACTIVE_BUDGET, DEFAULT_JUDGE_SHARE, DEFAULT_HALF_LIFE_MS, deriveSummary, } from "./notes.js";
|
|
6
|
+
export { SCHEMA_VERSION, FTS_TABLE, createSchema } from "./schema.js";
|
|
7
|
+
export { noteIdOf, noteRefOf, parseNoteId, parseNoteRef, noteRefForms, storedWindowRef, storedEventRef, } from "./refs.js";
|
|
8
|
+
export { USE_WEIGHTS, MAX_CREDIT_HOPS, decay, estimateTokens, linkedCredit, loadCredit, loadEdges, pickByBudget, applyJudgeShare, scoreNotes, } from "./score.js";
|
|
9
|
+
export { MIN_TRIGRAM_CHARS, substringClause, holdingIds, escapeLike, likePattern, quoteFts, } from "./search.js";
|
|
10
|
+
export { bodyDiff } from "./diff.js";
|
package/notes.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default `NoteStore`: SQLite beside the journal (docs/memory.md).
|
|
3
|
+
*
|
|
4
|
+
* Notes are the long-term memory: one copy of each, shared by every window,
|
|
5
|
+
* written only through the note tools. What this implementation owns:
|
|
6
|
+
*
|
|
7
|
+
* - the one copy, and the checked write: a write, replace or delete names the
|
|
8
|
+
* revision it read, and a stale one is refused with what changed since -
|
|
9
|
+
* the revisions in between, their windows and the differences - so the model
|
|
10
|
+
* merges and writes again. A refused call changes nothing.
|
|
11
|
+
* - revisions, append-only, with the anchor the host recorded and the refs the
|
|
12
|
+
* write named, so an anchor keeps working after the note is edited, archived
|
|
13
|
+
* or deleted;
|
|
14
|
+
* - the active set, picked at each snapshot epoch by usage score within a
|
|
15
|
+
* count and token budget, and frozen with the epoch's version, so every
|
|
16
|
+
* window opened in one epoch has the same prefix and later windows see the
|
|
17
|
+
* changes as a delta;
|
|
18
|
+
* - every write recorded in the journal as a `note_revision` event.
|
|
19
|
+
*
|
|
20
|
+
* It is synchronous inside: one `DatabaseSync`, one transaction per write, so
|
|
21
|
+
* the revision a write checks and the revision it writes are one atomic step.
|
|
22
|
+
* Writes are serialised in process as well, because a write spans the notes
|
|
23
|
+
* database and the journal. The interface is asynchronous because a `NoteStore`
|
|
24
|
+
* may live behind a process boundary.
|
|
25
|
+
*/
|
|
26
|
+
import type { ActiveCandidate, ActiveSetBudget, Journal, Note, NoteBody, NoteContext, NoteDelta, NoteQuery, NoteRef, NoteRevision, NoteSummary, NoteUseInput, NoteWriteInput, NotesSnapshot, NoteStore, Page, SnapshotOptions } from "@4fu/arbor-interfaces";
|
|
27
|
+
/** A summary is at most this many characters (docs/memory.md). */
|
|
28
|
+
export declare const SUMMARY_LIMIT = 200;
|
|
29
|
+
/** How many characters of a body one read returns. */
|
|
30
|
+
export declare const DEFAULT_BODY_LIMIT = 7000;
|
|
31
|
+
export declare const MAX_BODY_LIMIT = 20000;
|
|
32
|
+
/** The system prompt and memory snapshot rebuild at most this often. 10 min. */
|
|
33
|
+
export declare const DEFAULT_MIN_INTERVAL_MS = 600000;
|
|
34
|
+
/** The active set's budget, which bounds every prefix. */
|
|
35
|
+
export declare const DEFAULT_ACTIVE_BUDGET: ActiveSetBudget;
|
|
36
|
+
/** The share of the budget the judge may add, which never outweighs the model's own use. */
|
|
37
|
+
export declare const DEFAULT_JUDGE_SHARE = 0.15;
|
|
38
|
+
/** Usage loses half of its weight this often. */
|
|
39
|
+
export declare const DEFAULT_HALF_LIFE_MS = 86400000;
|
|
40
|
+
export interface SqliteNoteStoreOptions {
|
|
41
|
+
/** A file path, or `:memory:`. */
|
|
42
|
+
path?: string;
|
|
43
|
+
/** The journal every note revision is recorded in, as a `note_revision` event. */
|
|
44
|
+
journal: Journal;
|
|
45
|
+
/** The clock. Tests pass a fixed one. */
|
|
46
|
+
now?: () => number;
|
|
47
|
+
/** Usage loses half of its weight this often. 24 hours. */
|
|
48
|
+
usageHalfLifeMs?: number;
|
|
49
|
+
}
|
|
50
|
+
/** The opening of a body, when a write carries no summary of its own. */
|
|
51
|
+
export declare function deriveSummary(body: string): string;
|
|
52
|
+
export declare class SqliteNoteStore implements NoteStore {
|
|
53
|
+
readonly path: string;
|
|
54
|
+
private readonly db;
|
|
55
|
+
private readonly journal;
|
|
56
|
+
private readonly clock;
|
|
57
|
+
private readonly halfLifeMs;
|
|
58
|
+
/** One write at a time: a write spans the notes database and the journal. */
|
|
59
|
+
private writeQueue;
|
|
60
|
+
private closed;
|
|
61
|
+
constructor(options: SqliteNoteStoreOptions);
|
|
62
|
+
write(input: NoteWriteInput): Promise<Note>;
|
|
63
|
+
private writeOne;
|
|
64
|
+
/** The rules a write has to meet before it touches the one copy. */
|
|
65
|
+
private checkWrite;
|
|
66
|
+
/** The one atomic step: check the revision, then place the revision. */
|
|
67
|
+
private place;
|
|
68
|
+
/** Undo a placed revision whose journal event was refused. */
|
|
69
|
+
private takeBack;
|
|
70
|
+
/**
|
|
71
|
+
* The refusal a stale write gets: what changed since the revision it read -
|
|
72
|
+
* the revisions in between, their windows and the differences - so the model
|
|
73
|
+
* merges them and writes again against the current revision.
|
|
74
|
+
*/
|
|
75
|
+
private conflict;
|
|
76
|
+
read(ref: NoteRef, options?: {
|
|
77
|
+
offset?: number;
|
|
78
|
+
limit?: number;
|
|
79
|
+
}): Promise<NoteBody | null>;
|
|
80
|
+
revisions(ref: NoteRef): Promise<Page<NoteRevision>>;
|
|
81
|
+
search(query?: NoteQuery): Promise<Page<NoteSummary>>;
|
|
82
|
+
active(): Promise<Page<NoteSummary>>;
|
|
83
|
+
context(ref: NoteRef): Promise<NoteContext | null>;
|
|
84
|
+
snapshot(options?: SnapshotOptions): Promise<NotesSnapshot>;
|
|
85
|
+
delta(since: number, options?: {
|
|
86
|
+
limit?: number;
|
|
87
|
+
offset?: number;
|
|
88
|
+
}): Promise<NoteDelta>;
|
|
89
|
+
score(options?: {
|
|
90
|
+
limit?: number;
|
|
91
|
+
offset?: number;
|
|
92
|
+
}): Promise<Page<ActiveCandidate>>;
|
|
93
|
+
recordUse(input: NoteUseInput): Promise<void>;
|
|
94
|
+
close(): Promise<void>;
|
|
95
|
+
private ensureOpen;
|
|
96
|
+
private noteRow;
|
|
97
|
+
private revisionRow;
|
|
98
|
+
private latestRevisionRow;
|
|
99
|
+
private recordRead;
|
|
100
|
+
/** The current summary and revision of several notes, in one read. */
|
|
101
|
+
private noteStatesFor;
|
|
102
|
+
private summaryPageBySql;
|
|
103
|
+
private summaryPageByIds;
|
|
104
|
+
/**
|
|
105
|
+
* The notes about a target, or linked to a ref: the notes the events on that
|
|
106
|
+
* target name, and the notes whose revisions name that ref. A note the store
|
|
107
|
+
* has never heard of is skipped, so a hallucinated ref costs nothing.
|
|
108
|
+
*/
|
|
109
|
+
private linkedNoteIds;
|
|
110
|
+
/** The notes the events on a target name, strongest link first. */
|
|
111
|
+
private notesAbout;
|
|
112
|
+
/** The notes whose revisions name a ref, whichever revision named it. */
|
|
113
|
+
private notesLinkedTo;
|
|
114
|
+
private latestEpoch;
|
|
115
|
+
private epochRow;
|
|
116
|
+
private newestWriteSeq;
|
|
117
|
+
/**
|
|
118
|
+
* The epoch a window freezes its prefix at. A new one is built at most every
|
|
119
|
+
* `minIntervalMs`, and only when a note was written since the last one or the
|
|
120
|
+
* budget changed, so every window opened in one epoch has the same prefix and
|
|
121
|
+
* the changes since reach it as a delta.
|
|
122
|
+
*/
|
|
123
|
+
private epochAt;
|
|
124
|
+
/** The active notes one epoch froze, by summary, sorted by id. */
|
|
125
|
+
private epochSummaries;
|
|
126
|
+
}
|