auggy 0.3.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 +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: layered-memory
|
|
3
|
+
description: When and how to use memory_write, memory_search, memory_list, memory_forget for peer-scoped episodic memory. Read this before saving or recalling anything about a peer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Layered memory
|
|
7
|
+
|
|
8
|
+
You have access to **peer-scoped episodic memory** — entries you save are bound to the peer who is talking with you in the current turn, and your search results return only entries that peer wrote (or that were written about them). Different peers do not see each other's memory.
|
|
9
|
+
|
|
10
|
+
This memory is for things you learn turn-by-turn — preferences, names, commitments, recurring topics — not for facts about the agent itself or the operator's organization (those live elsewhere in your context).
|
|
11
|
+
|
|
12
|
+
## Tools
|
|
13
|
+
|
|
14
|
+
| Tool | What it does | When to call |
|
|
15
|
+
|------|--------------|--------------|
|
|
16
|
+
| `memory_search(query)` | Full-text search over the current peer's memory entries | At the start of a turn with a returning peer; when the peer references something they said before |
|
|
17
|
+
| `memory_write(label, content)` | Persist content under a peer-scoped label | When the peer introduces themselves, states a preference, makes a commitment, or asks to be remembered |
|
|
18
|
+
| `memory_list()` | List available labels and namespaces visible to this peer | When you want to confirm what's already stored before writing or searching |
|
|
19
|
+
| `memory_forget(peerId)` | Wipe ALL episodic memory for a specific peer | Right-to-erasure requests. Requires elevated trust (operator or creator); a regular peer cannot trigger it |
|
|
20
|
+
|
|
21
|
+
`memory_read(label)` exists but **does not work on episodic labels**. Episodic memory is peer-scoped, and reading by label would bypass that scoping. Use `memory_search` instead. (`memory_read` still works for non-episodic static labels like `self` — the agent's identity preamble — when those are configured.)
|
|
22
|
+
|
|
23
|
+
## How labels are structured
|
|
24
|
+
|
|
25
|
+
Labels in this layer must start with the configured **namespace prefix** (set by the operator — typically the agent's name with a colon). When a peer is talking with you, their entries must additionally be scoped to their peer ID.
|
|
26
|
+
|
|
27
|
+
The shape:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
<prefix><peerId> ← write a single entry for this peer
|
|
31
|
+
<prefix><peerId>:<topic> ← write multiple entries grouped by topic
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Examples** (assuming the namespace prefix is `concierge:` and the peer id is `vis_a1b2c3`):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
concierge:vis_a1b2c3 ← one umbrella entry
|
|
38
|
+
concierge:vis_a1b2c3:preferences ← topic-grouped
|
|
39
|
+
concierge:vis_a1b2c3:commitments ← topic-grouped
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If you try to write a label that doesn't include the current peer's ID in the right shape, the write fails with a clear error. Don't try to outsmart this — it is a structural protection against one peer seeding entries against another.
|
|
43
|
+
|
|
44
|
+
## When to call `memory_write`
|
|
45
|
+
|
|
46
|
+
Save when the peer:
|
|
47
|
+
|
|
48
|
+
- Introduces themselves by name (`memory_write("concierge:vis_a1b2c3", "Sam from Acme — interested in research tooling.")`)
|
|
49
|
+
- States a preference you should honor in future turns ("I prefer concise responses.")
|
|
50
|
+
- Makes a commitment ("I'll get back to you Tuesday with the budget.")
|
|
51
|
+
- Explicitly asks to be remembered ("Remember that I'm working on X.")
|
|
52
|
+
- Tells you a piece of context that will matter later in the conversation or in a future one
|
|
53
|
+
|
|
54
|
+
**Don't save** every conversational turn. The peer's full message history is already part of your context. Memory is for things you'd want to recall in a *future* conversation, not the current one.
|
|
55
|
+
|
|
56
|
+
## When to call `memory_search`
|
|
57
|
+
|
|
58
|
+
Call once at the **start of a turn with a returning peer** — that gives you the lightweight context they've built with you over time. Treat the results as background to inform tone and recall, not as a script to recite.
|
|
59
|
+
|
|
60
|
+
Call again later in the turn if the peer references something specific they said before ("like I mentioned earlier…", "going back to that thing about…"). A targeted search beats trying to scroll mental context.
|
|
61
|
+
|
|
62
|
+
Keep query terms short — search uses keyword matching against entry content. Two or three meaningful words beats a sentence.
|
|
63
|
+
|
|
64
|
+
## When to call `memory_list`
|
|
65
|
+
|
|
66
|
+
Use when you want to know what labels already exist for the current peer before writing a new one — helps you decide between writing a fresh label vs. updating an existing topic. Inexpensive; safe to call.
|
|
67
|
+
|
|
68
|
+
## What you cannot do
|
|
69
|
+
|
|
70
|
+
- You cannot read another peer's entries. The system filters search and list results by the peer in the current turn.
|
|
71
|
+
- You cannot write a label that's missing the current peer's ID in the right shape.
|
|
72
|
+
- You cannot use `memory_read` on an episodic label — only `memory_search`.
|
|
73
|
+
- You cannot bulk-delete entries. `memory_forget` wipes everything for one peer (and only with elevated trust); fine-grained deletion isn't a model-callable operation.
|
|
74
|
+
|
|
75
|
+
## Common mistakes
|
|
76
|
+
|
|
77
|
+
| Wrong | Correct |
|
|
78
|
+
|-------|---------|
|
|
79
|
+
| `memory_read("ep:...")` on an episodic label | `memory_search("relevant query")` |
|
|
80
|
+
| Writing a label with the wrong namespace prefix | Use the prefix from the operator's config (typically the agent name with a colon) |
|
|
81
|
+
| Writing a peer-scoped label without the peer's ID | Always shape labels as `<prefix><peerId>` or `<prefix><peerId>:<topic>` |
|
|
82
|
+
| Writing every turn as a "remember this" | Save only what you'd want to recall in a *future* conversation |
|
|
83
|
+
| Searching with a long natural-language query | Keep queries to a few keywords |
|
|
84
|
+
| Calling `memory_forget` on your own initiative | Only call it when the peer (or operator) explicitly requests erasure |
|
|
85
|
+
|
|
86
|
+
## Workflow
|
|
87
|
+
|
|
88
|
+
### Returning peer says hello
|
|
89
|
+
|
|
90
|
+
1. `memory_search("recent")` or a query relevant to the conversation
|
|
91
|
+
2. Read the entries returned; let them inform your reply naturally — don't dump them back at the peer
|
|
92
|
+
3. Continue the conversation; save NEW things you learn via `memory_write`
|
|
93
|
+
|
|
94
|
+
### Peer states a preference or commitment
|
|
95
|
+
|
|
96
|
+
1. (Optional) `memory_list()` to see if a related label already exists
|
|
97
|
+
2. `memory_write("<prefix><peerId>:<topic>", "<concise description>")`
|
|
98
|
+
3. Acknowledge briefly ("Got it, I'll remember that.") — don't over-explain
|
|
99
|
+
|
|
100
|
+
### Peer asks to be forgotten
|
|
101
|
+
|
|
102
|
+
1. Confirm once that they want all their stored memory deleted
|
|
103
|
+
2. If you have the trust level for it, call `memory_forget(peerId)` and confirm the count returned
|
|
104
|
+
3. If you don't have the trust level, tell them you're flagging the request for the operator
|
|
105
|
+
|
|
106
|
+
## Auto-saved entries — `[AGENT-DERIVED]` provenance
|
|
107
|
+
|
|
108
|
+
### What auto-save does
|
|
109
|
+
|
|
110
|
+
A background process extracts facts after each turn (or per the operator's configured cadence) and writes them to your peer-scoped memory. **You never invoke this process directly — it runs on its own.** The only observable effect is that `memory_search` results sometimes include entries carrying the `[AGENT-DERIVED]` marker.
|
|
111
|
+
|
|
112
|
+
When you call `memory_search` and see an entry like:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
[AGENT-DERIVED] Sam prefers concise replies and works at Acme Corp.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
that entry came from the background process, not from a `memory_write` call you made.
|
|
119
|
+
|
|
120
|
+
### `[AGENT-DERIVED]` entries are paraphrases, not verbatim
|
|
121
|
+
|
|
122
|
+
When you see an entry with that marker, treat the content as an extracted paraphrase — a best-effort summary of what the peer said — **not** the peer's exact words. If precision matters (operator agreements, technical specifications, contact details, verbatim commitments), prefer entries marked `[PEER-DERIVED]` or entries you wrote explicitly with `memory_write` when the peer stated something exactly.
|
|
123
|
+
|
|
124
|
+
### Trust hierarchy on conflict
|
|
125
|
+
|
|
126
|
+
If two entries about the same fact conflict, trust them in this order:
|
|
127
|
+
|
|
128
|
+
1. `[PEER-DERIVED]` entries (or operator-set verbatim entries) — authoritative; the peer's own words or an operator-confirmed record
|
|
129
|
+
2. `[AGENT-DERIVED]` entries — useful background; defer to the above when they contradict
|
|
130
|
+
|
|
131
|
+
**Never overwrite a `[PEER-DERIVED]` entry with an extracted paraphrase.** If a peer corrects something that the background process extracted incorrectly, write the correction via `memory_write` — the new entry coexists with the old one and the trust hierarchy ensures the peer's explicit statement is preferred. If the conflict is meaningful, surface it to the peer briefly.
|
|
132
|
+
|
|
133
|
+
### When to call `memory_write` directly anyway
|
|
134
|
+
|
|
135
|
+
The background process runs after each turn. You do not need to wait for it. Call `memory_write` mid-turn when:
|
|
136
|
+
|
|
137
|
+
- The peer explicitly asks to be remembered ("save my email as foo@example.com")
|
|
138
|
+
- You want to capture the peer's exact phrasing verbatim (commitments, technical specs, contact details)
|
|
139
|
+
- You are correcting a fact you know to be wrong from a prior extraction
|
|
140
|
+
- The signal is high enough that you do not want to risk it being missed
|
|
141
|
+
|
|
142
|
+
Both writes coexist in memory. The background process does not overwrite your explicit `memory_write` calls, and your calls do not overwrite background-extracted entries — they accumulate and retrieval ranks them by the trust hierarchy above.
|
|
143
|
+
|
|
144
|
+
### Privacy boundaries
|
|
145
|
+
|
|
146
|
+
Some content should never reach memory, regardless of who writes it:
|
|
147
|
+
|
|
148
|
+
- Secrets and credentials (API keys, passwords, tokens)
|
|
149
|
+
- Content the peer explicitly marked as confidential
|
|
150
|
+
- Sensitive personal information outside what the agent's purpose warrants
|
|
151
|
+
- Anything the peer asked to be forgotten
|
|
152
|
+
|
|
153
|
+
The background extraction process embeds a privacy guard in its prompt template, but you are also responsible for respecting the same boundary in your own `memory_write` calls. If a peer shares a secret mid-turn, do not save it.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Layered-memory storage migrations
|
|
2
|
+
|
|
3
|
+
## SQLite
|
|
4
|
+
|
|
5
|
+
Migrations run automatically at augment boot via PRAGMA-checked ALTERs (idempotent). No operator action needed.
|
|
6
|
+
|
|
7
|
+
## Supabase
|
|
8
|
+
|
|
9
|
+
Manual application required:
|
|
10
|
+
|
|
11
|
+
1. Open Supabase SQL editor for the project hosting this agent's memory.
|
|
12
|
+
2. Run `supabase-add-fact-fields.sql`.
|
|
13
|
+
3. Verify via the table editor that new columns are present.
|
|
14
|
+
4. Deploy the new layered-memory version.
|
|
15
|
+
|
|
16
|
+
Layered-memory's runtime startup verifies the schema on first connect; if columns are missing, it logs a structured warning and falls back to writing without the new fields (auto-save still works; structured-fact retrieval is degraded).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Adds Phase 2 fact-fields. Apply via Supabase SQL editor or `supabase db push`.
|
|
2
|
+
|
|
3
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS subject TEXT;
|
|
4
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS predicate TEXT;
|
|
5
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS object TEXT;
|
|
6
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS source_turn_id TEXT;
|
|
7
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS origin TEXT;
|
|
8
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS is_verbatim BOOLEAN;
|
|
9
|
+
ALTER TABLE memory_entries ADD COLUMN IF NOT EXISTS retention_class TEXT;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type {
|
|
4
|
+
MemoryStore,
|
|
5
|
+
RetentionClass,
|
|
6
|
+
SqliteStoreConfig,
|
|
7
|
+
StoreEntry,
|
|
8
|
+
WriteAutoSavedArgs,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import type { TrustLevel } from "../../../types";
|
|
11
|
+
|
|
12
|
+
// Each statement run individually to keep the SQL surface explicit.
|
|
13
|
+
const SCHEMA_STATEMENTS = [
|
|
14
|
+
`CREATE TABLE IF NOT EXISTS entries (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
label TEXT NOT NULL,
|
|
17
|
+
content TEXT NOT NULL,
|
|
18
|
+
peer_id TEXT,
|
|
19
|
+
trust_level TEXT,
|
|
20
|
+
created_at INTEGER NOT NULL,
|
|
21
|
+
superseded_by TEXT,
|
|
22
|
+
retention_class TEXT NOT NULL DEFAULT 'operational',
|
|
23
|
+
is_verbatim INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
provenance_model TEXT,
|
|
25
|
+
confidence REAL,
|
|
26
|
+
embedding_model TEXT,
|
|
27
|
+
scope TEXT NOT NULL DEFAULT 'peer',
|
|
28
|
+
expires_at INTEGER
|
|
29
|
+
)`,
|
|
30
|
+
`CREATE INDEX IF NOT EXISTS idx_entries_peer ON entries(peer_id)`,
|
|
31
|
+
`CREATE INDEX IF NOT EXISTS idx_entries_label ON entries(label)`,
|
|
32
|
+
`CREATE INDEX IF NOT EXISTS idx_entries_created ON entries(created_at DESC)`,
|
|
33
|
+
`CREATE INDEX IF NOT EXISTS idx_entries_expires ON entries(expires_at) WHERE expires_at IS NOT NULL`,
|
|
34
|
+
`CREATE TABLE IF NOT EXISTS event_log (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
entry_id TEXT NOT NULL,
|
|
37
|
+
action TEXT NOT NULL,
|
|
38
|
+
peer_id TEXT,
|
|
39
|
+
timestamp INTEGER NOT NULL,
|
|
40
|
+
detail TEXT
|
|
41
|
+
)`,
|
|
42
|
+
`CREATE INDEX IF NOT EXISTS idx_events_entry ON event_log(entry_id)`,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
interface Row {
|
|
46
|
+
id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
content: string;
|
|
49
|
+
peer_id: string | null;
|
|
50
|
+
trust_level: string | null;
|
|
51
|
+
created_at: number;
|
|
52
|
+
superseded_by: string | null;
|
|
53
|
+
retention_class: string;
|
|
54
|
+
is_verbatim: number;
|
|
55
|
+
expires_at: number | null;
|
|
56
|
+
// Phase 2 fact-fields (nullable; populated by writeAutoSavedEntry)
|
|
57
|
+
subject: string | null;
|
|
58
|
+
predicate: string | null;
|
|
59
|
+
object: string | null;
|
|
60
|
+
source_turn_id: string | null;
|
|
61
|
+
origin: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rowToEntry(row: Row): StoreEntry {
|
|
65
|
+
const entry: StoreEntry = {
|
|
66
|
+
id: row.id,
|
|
67
|
+
label: row.label,
|
|
68
|
+
content: row.content,
|
|
69
|
+
peerId: row.peer_id,
|
|
70
|
+
trustLevel: row.trust_level as TrustLevel | null,
|
|
71
|
+
createdAt: row.created_at,
|
|
72
|
+
supersededBy: row.superseded_by,
|
|
73
|
+
retentionClass: row.retention_class as RetentionClass,
|
|
74
|
+
isVerbatim: row.is_verbatim === 1,
|
|
75
|
+
expiresAt: row.expires_at,
|
|
76
|
+
};
|
|
77
|
+
// Read path: populate fact-fields only when present in storage. Legacy
|
|
78
|
+
// rows (pre-Phase-2) carry NULLs and stay clean on the way out.
|
|
79
|
+
if (row.subject != null) entry.subject = row.subject;
|
|
80
|
+
if (row.predicate != null) entry.predicate = row.predicate;
|
|
81
|
+
if (row.object != null) entry.object = row.object;
|
|
82
|
+
if (row.source_turn_id != null) entry.sourceTurnId = row.source_turn_id;
|
|
83
|
+
if (row.origin != null) entry.origin = row.origin as StoreEntry["origin"];
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Sampled cleanup: every ~Nth write triggers a bounded DELETE so write
|
|
88
|
+
// latency doesn't depend on stale-data backlog. Capped via LIMIT so even
|
|
89
|
+
// a huge backlog drains in roughly constant time per write.
|
|
90
|
+
const CLEANUP_SAMPLE_RATE = 50;
|
|
91
|
+
const CLEANUP_BATCH_SIZE = 100;
|
|
92
|
+
|
|
93
|
+
export function createSqliteStore(config: SqliteStoreConfig): MemoryStore {
|
|
94
|
+
const db = new Database(config.dbPath, { create: true });
|
|
95
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
96
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
97
|
+
|
|
98
|
+
// Schema must exist before we prepare statements that reference it.
|
|
99
|
+
for (const stmt of SCHEMA_STATEMENTS) {
|
|
100
|
+
db.run(stmt);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Phase 2 migration: add structured-fact + provenance columns idempotently.
|
|
104
|
+
// PRAGMA table_info detects which columns already exist; ALTER TABLE adds
|
|
105
|
+
// only the absent ones. Existing rows survive with NULLs in the new columns.
|
|
106
|
+
// is_verbatim + retention_class are already in SCHEMA_STATEMENTS above;
|
|
107
|
+
// they appear in the list so the migration is self-documenting and safe to
|
|
108
|
+
// re-run if applied to a DB that predates them (legacy schema path).
|
|
109
|
+
function ensureMigrations(): void {
|
|
110
|
+
const cols = db.prepare("PRAGMA table_info(entries)").all() as { name: string }[];
|
|
111
|
+
const colNames = new Set(cols.map((c) => c.name));
|
|
112
|
+
|
|
113
|
+
const additions: Array<{ name: string; ddl: string }> = [
|
|
114
|
+
{ name: "subject", ddl: "ALTER TABLE entries ADD COLUMN subject TEXT" },
|
|
115
|
+
{ name: "predicate", ddl: "ALTER TABLE entries ADD COLUMN predicate TEXT" },
|
|
116
|
+
{ name: "object", ddl: "ALTER TABLE entries ADD COLUMN object TEXT" },
|
|
117
|
+
{ name: "source_turn_id", ddl: "ALTER TABLE entries ADD COLUMN source_turn_id TEXT" },
|
|
118
|
+
{ name: "origin", ddl: "ALTER TABLE entries ADD COLUMN origin TEXT" },
|
|
119
|
+
{ name: "is_verbatim", ddl: "ALTER TABLE entries ADD COLUMN is_verbatim INTEGER" },
|
|
120
|
+
{ name: "retention_class", ddl: "ALTER TABLE entries ADD COLUMN retention_class TEXT" },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
for (const { name, ddl } of additions) {
|
|
124
|
+
if (!colNames.has(name)) db.run(ddl);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ensureMigrations();
|
|
129
|
+
|
|
130
|
+
const retentionMs = config.retentionDays * 24 * 60 * 60 * 1000;
|
|
131
|
+
|
|
132
|
+
// Pre-compiled statements live as long as the connection.
|
|
133
|
+
const insertEntryStmt = db.prepare(
|
|
134
|
+
`INSERT INTO entries (id, label, content, peer_id, trust_level, created_at, superseded_by, retention_class, is_verbatim, expires_at)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
136
|
+
);
|
|
137
|
+
const insertEventStmt = db.prepare(
|
|
138
|
+
"INSERT INTO event_log (id, entry_id, action, peer_id, timestamp, detail) VALUES (?, ?, ?, ?, ?, ?)",
|
|
139
|
+
);
|
|
140
|
+
const cleanupStmt = db.prepare(
|
|
141
|
+
"DELETE FROM entries WHERE id IN (SELECT id FROM entries WHERE expires_at IS NOT NULL AND expires_at < ? LIMIT ?)",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
type SqlBinding = string | number | bigint | boolean | null | Uint8Array;
|
|
145
|
+
|
|
146
|
+
// writeAndLog runs the entry insert and audit insert atomically. If
|
|
147
|
+
// either fails, both roll back — callers either see a successful
|
|
148
|
+
// write that's fully recorded, or an error and zero side effects.
|
|
149
|
+
const writeAndLog = db.transaction((entryParams: SqlBinding[], eventParams: SqlBinding[]) => {
|
|
150
|
+
insertEntryStmt.run(...entryParams);
|
|
151
|
+
insertEventStmt.run(...eventParams);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
async function initialize(): Promise<void> {
|
|
155
|
+
// Schema is now created at construction time. Kept as a no-op for
|
|
156
|
+
// contract symmetry with SupabaseStore (whose schema lives in
|
|
157
|
+
// migrations).
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function logEvent(entryId: string, action: string, peerId: string | null, detail?: object) {
|
|
161
|
+
insertEventStmt.run(
|
|
162
|
+
randomUUID(),
|
|
163
|
+
entryId,
|
|
164
|
+
action,
|
|
165
|
+
peerId,
|
|
166
|
+
Date.now(),
|
|
167
|
+
detail ? JSON.stringify(detail) : null,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function write(input: Omit<StoreEntry, "id"> & { id?: string }): Promise<StoreEntry> {
|
|
172
|
+
const id = input.id ?? randomUUID();
|
|
173
|
+
const expiresAt = input.expiresAt ?? input.createdAt + retentionMs;
|
|
174
|
+
|
|
175
|
+
writeAndLog(
|
|
176
|
+
[
|
|
177
|
+
id,
|
|
178
|
+
input.label,
|
|
179
|
+
input.content,
|
|
180
|
+
input.peerId,
|
|
181
|
+
input.trustLevel,
|
|
182
|
+
input.createdAt,
|
|
183
|
+
input.supersededBy,
|
|
184
|
+
input.retentionClass,
|
|
185
|
+
input.isVerbatim ? 1 : 0,
|
|
186
|
+
expiresAt,
|
|
187
|
+
],
|
|
188
|
+
[randomUUID(), id, "write", input.peerId, Date.now(), null],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Sampled, bounded cleanup. Outside the transaction so a partial
|
|
192
|
+
// sweep can never roll back the user's write. ~1-in-50 writes pay
|
|
193
|
+
// the small constant DELETE cost; 49-in-50 writes pay nothing.
|
|
194
|
+
if (Math.random() * CLEANUP_SAMPLE_RATE < 1) {
|
|
195
|
+
const result = cleanupStmt.run(Date.now(), CLEANUP_BATCH_SIZE);
|
|
196
|
+
if (result.changes > 0) {
|
|
197
|
+
logEvent("(batch)", "expire-sweep", null, { swept: result.changes });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { ...input, id, expiresAt };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function search(query: string, peerId?: string, limit = 10): Promise<StoreEntry[]> {
|
|
205
|
+
const escaped = query.replace(/[%_\\]/g, (c) => `\\${c}`);
|
|
206
|
+
const pattern = `%${escaped}%`;
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
|
|
209
|
+
if (peerId) {
|
|
210
|
+
const rows = db
|
|
211
|
+
.prepare<Row, [string, string, number, number]>(
|
|
212
|
+
`SELECT * FROM entries
|
|
213
|
+
WHERE peer_id = ?
|
|
214
|
+
AND content LIKE ? ESCAPE '\\'
|
|
215
|
+
AND superseded_by IS NULL
|
|
216
|
+
AND (expires_at IS NULL OR expires_at >= ?)
|
|
217
|
+
ORDER BY created_at DESC
|
|
218
|
+
LIMIT ?`,
|
|
219
|
+
)
|
|
220
|
+
.all(peerId, pattern, now, limit);
|
|
221
|
+
return rows.map(rowToEntry);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const rows = db
|
|
225
|
+
.prepare<Row, [string, number, number]>(
|
|
226
|
+
`SELECT * FROM entries
|
|
227
|
+
WHERE content LIKE ? ESCAPE '\\'
|
|
228
|
+
AND superseded_by IS NULL
|
|
229
|
+
AND (expires_at IS NULL OR expires_at >= ?)
|
|
230
|
+
ORDER BY created_at DESC
|
|
231
|
+
LIMIT ?`,
|
|
232
|
+
)
|
|
233
|
+
.all(pattern, now, limit);
|
|
234
|
+
return rows.map(rowToEntry);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function read(label: string): Promise<StoreEntry | null> {
|
|
238
|
+
const row = db
|
|
239
|
+
.prepare<Row, [string]>(
|
|
240
|
+
"SELECT * FROM entries WHERE label = ? AND superseded_by IS NULL ORDER BY created_at DESC LIMIT 1",
|
|
241
|
+
)
|
|
242
|
+
.get(label);
|
|
243
|
+
return row ? rowToEntry(row) : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function list(peerId?: string): Promise<string[]> {
|
|
247
|
+
if (peerId) {
|
|
248
|
+
const rows = db
|
|
249
|
+
.prepare<{ label: string }, [string]>(
|
|
250
|
+
"SELECT DISTINCT label FROM entries WHERE peer_id = ? AND superseded_by IS NULL ORDER BY label",
|
|
251
|
+
)
|
|
252
|
+
.all(peerId);
|
|
253
|
+
return rows.map((r) => r.label);
|
|
254
|
+
}
|
|
255
|
+
const rows = db
|
|
256
|
+
.prepare<{ label: string }, []>(
|
|
257
|
+
"SELECT DISTINCT label FROM entries WHERE superseded_by IS NULL ORDER BY label",
|
|
258
|
+
)
|
|
259
|
+
.all();
|
|
260
|
+
return rows.map((r) => r.label);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function forget(peerId: string): Promise<number> {
|
|
264
|
+
const result = db.prepare("DELETE FROM entries WHERE peer_id = ?").run(peerId);
|
|
265
|
+
logEvent("(batch)", "forget", peerId, { deleted: result.changes });
|
|
266
|
+
return result.changes;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function supersede(entryId: string, newEntryId: string): Promise<void> {
|
|
270
|
+
db.prepare("UPDATE entries SET superseded_by = ? WHERE id = ?").run(newEntryId, entryId);
|
|
271
|
+
logEvent(entryId, "supersede", null, { supersededBy: newEntryId });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function cleanup(): Promise<number> {
|
|
275
|
+
const result = db
|
|
276
|
+
.prepare("DELETE FROM entries WHERE expires_at IS NOT NULL AND expires_at < ?")
|
|
277
|
+
.run(Date.now());
|
|
278
|
+
return result.changes;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Internal-to-layered-memory write path used by the extractor. NOT
|
|
282
|
+
// exposed on any augment-public surface — Phase 2 of ADR-018,
|
|
283
|
+
// Decision 4 of the memorist design. `origin` is hardcoded to
|
|
284
|
+
// `"agent-derived"` here rather than accepted as an argument so a
|
|
285
|
+
// misbehaving extraction prompt cannot forge `operator` or
|
|
286
|
+
// `peer-derived`. Namespace prefix is enforced when configured;
|
|
287
|
+
// when absent, this function refuses entirely (the augment factory
|
|
288
|
+
// must always pass a namespace).
|
|
289
|
+
async function writeAutoSavedEntry(args: WriteAutoSavedArgs): Promise<void> {
|
|
290
|
+
if (!config.namespace) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"writeAutoSavedEntry: store has no namespace configured; auto-save requires namespace-prefix discipline",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const prefix = config.namespace.endsWith(":") ? config.namespace : `${config.namespace}:`;
|
|
296
|
+
if (!args.label.startsWith(prefix)) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`writeAutoSavedEntry: label "${args.label}" does not start with namespace prefix "${prefix}"`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const id = randomUUID();
|
|
302
|
+
const createdAt = Date.now();
|
|
303
|
+
const expiresAt = createdAt + retentionMs;
|
|
304
|
+
db.prepare(
|
|
305
|
+
`INSERT INTO entries
|
|
306
|
+
(id, label, content, peer_id, trust_level, created_at, superseded_by,
|
|
307
|
+
retention_class, is_verbatim, expires_at,
|
|
308
|
+
subject, predicate, object, source_turn_id, origin,
|
|
309
|
+
provenance_model, confidence)
|
|
310
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'agent-derived', ?, ?)`,
|
|
311
|
+
).run(
|
|
312
|
+
id,
|
|
313
|
+
args.label,
|
|
314
|
+
args.content,
|
|
315
|
+
args.peerId,
|
|
316
|
+
null,
|
|
317
|
+
createdAt,
|
|
318
|
+
null,
|
|
319
|
+
args.retentionClass,
|
|
320
|
+
args.isVerbatim ? 1 : 0,
|
|
321
|
+
expiresAt,
|
|
322
|
+
args.subject ?? null,
|
|
323
|
+
args.predicate ?? null,
|
|
324
|
+
args.object ?? null,
|
|
325
|
+
args.sourceTurnId,
|
|
326
|
+
args.model,
|
|
327
|
+
args.confidence,
|
|
328
|
+
);
|
|
329
|
+
logEvent(id, "auto-save", args.peerId, {
|
|
330
|
+
sourceTurnId: args.sourceTurnId,
|
|
331
|
+
model: args.model,
|
|
332
|
+
confidence: args.confidence,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function close(): Promise<void> {
|
|
337
|
+
db.close();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
initialize,
|
|
342
|
+
write,
|
|
343
|
+
writeAutoSavedEntry,
|
|
344
|
+
search,
|
|
345
|
+
read,
|
|
346
|
+
list,
|
|
347
|
+
forget,
|
|
348
|
+
supersede,
|
|
349
|
+
cleanup,
|
|
350
|
+
close,
|
|
351
|
+
};
|
|
352
|
+
}
|