@arkimedehq/openclaw-recordare 0.2.0 → 0.4.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/README.md +13 -2
- package/README_it.md +14 -3
- package/dist/index.js +230 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ own [Recordare](../../README.md) service — the **full** client level:
|
|
|
24
24
|
It does not take OpenClaw's memory slot: `memory-core` (`MEMORY.md`, `memory_search`) keeps working beside it. Recordare
|
|
25
25
|
stores every turn the plugin sends (it has no consent flag, D50). The plugin never blocks or breaks a turn: every call
|
|
26
26
|
is time-boxed (3 s before the turn), failures are logged without content, captured messages that could not be sent are
|
|
27
|
-
retried in the background for
|
|
27
|
+
retried in the background for a few hours, from a queue on disk that survives a Gateway restart
|
|
28
|
+
(`<OpenClaw state dir>/recordare/outbox`, normally `~/.openclaw/recordare/outbox`, owner-only files; only unsent
|
|
29
|
+
messages are written there, and each file is deleted once Recordare has it).
|
|
28
30
|
|
|
29
31
|
Requires OpenClaw ≥ 2026.9.9 (Node ≥ 24, as OpenClaw itself).
|
|
30
32
|
|
|
@@ -45,7 +47,7 @@ Requires OpenClaw ≥ 2026.9.9 (Node ≥ 24, as OpenClaw itself).
|
|
|
45
47
|
Recordare or auto-provisioned if the client allows it); a client key is also what one memory per person needs.
|
|
46
48
|
|
|
47
49
|
The memory's **mode** and **gender** are set by the admin (`PATCH api/v1/admin/memories/{id}` `{mode, gender}`), or
|
|
48
|
-
with a client key by `PATCH api/v1/me`: `personal` (your own assistant: you are "I", what arrives undeclared is
|
|
50
|
+
with a client key or the personal token by `PATCH api/v1/me`: `personal` (your own assistant: you are "I", what arrives undeclared is
|
|
49
51
|
yours) or `entity` (an agent shared by a family, a team, a place: what arrives undeclared is "someone"'s); `gender`
|
|
50
52
|
`masculine` (default) | `feminine` | `neutral` for the first person in gendered languages. The plugin has no setting
|
|
51
53
|
for them.
|
|
@@ -93,6 +95,15 @@ list your ids there (or have the admin bind them to the memory's self, `POST api
|
|
|
93
95
|
people's words are kept as theirs (role `other`, attributed to their contact), so what they say about themselves never
|
|
94
96
|
becomes a fact about you.
|
|
95
97
|
|
|
98
|
+
**Or link your sender id with a code** (D51, personal memories). Make a one-time code — the admin console's "Link code"
|
|
99
|
+
on the memory, or `POST api/v1/me/link-codes` with the memory's personal token (`→ {"code": "RCD-…", "expiresAt": …}`,
|
|
100
|
+
ten minutes) — and send it alone to the agent in a **direct chat** from your channel account. The plugin sends it to
|
|
101
|
+
Recordare at once (never stored as a message) and tells the model only to say that the code was received; then confirm
|
|
102
|
+
the claim in the console ("To confirm" → "It is the holder") or with `GET api/v1/me/identities` and `POST
|
|
103
|
+
api/v1/me/identities/pending/{id}/confirm`, within seven days. From then on that sender is you; earlier messages keep
|
|
104
|
+
their attribution. The plugin marks group, channel and room sessions (by their session key) as groups, where a code
|
|
105
|
+
claims nothing.
|
|
106
|
+
|
|
96
107
|
**Upgrading from a `users` map.** The earlier behaviour (each mapped sender a separate Recordare user, unmapped senders
|
|
97
108
|
not remembered) is `memoryPer: "user"`: add it to keep your memories split per person.
|
|
98
109
|
|
package/README_it.md
CHANGED
|
@@ -26,8 +26,10 @@ tenuta dal tuo servizio [Recordare](../../README_it.md) — il livello client **
|
|
|
26
26
|
Non occupa lo slot memoria di OpenClaw: `memory-core` (`MEMORY.md`, `memory_search`) continua a funzionare accanto.
|
|
27
27
|
Recordare conserva ogni turno che il plugin invia (non ha un flag di consenso, D50). Il plugin non blocca né rompe mai
|
|
28
28
|
un turno: ogni chiamata ha un limite di tempo (3 s prima del turno), gli errori finiscono nel log senza contenuti, i
|
|
29
|
-
messaggi catturati non inviati vengono ritentati in background per una
|
|
30
|
-
Gateway
|
|
29
|
+
messaggi catturati non inviati vengono ritentati in background per qualche ora, da una coda su disco che sopravvive a un
|
|
30
|
+
riavvio del Gateway (`<cartella di stato di OpenClaw>/recordare/outbox`, di solito `~/.openclaw/recordare/outbox`, file
|
|
31
|
+
leggibili solo dal proprietario; ci finiscono solo i messaggi non inviati, e ogni file viene cancellato appena Recordare
|
|
32
|
+
lo ha ricevuto).
|
|
31
33
|
|
|
32
34
|
Richiede OpenClaw ≥ 2026.9.9 (Node ≥ 24, come OpenClaw stesso).
|
|
33
35
|
|
|
@@ -49,7 +51,7 @@ Richiede OpenClaw ≥ 2026.9.9 (Node ≥ 24, come OpenClaw stesso).
|
|
|
49
51
|
per persona.
|
|
50
52
|
|
|
51
53
|
**Modalità** e **genere** della memoria li imposta l'amministratore (`PATCH api/v1/admin/memories/{id}`
|
|
52
|
-
`{mode, gender}`), oppure, con una chiave client, `PATCH api/v1/me`: `personal` (il tuo assistente: tu sei l'"io",
|
|
54
|
+
`{mode, gender}`), oppure, con una chiave client o il token personale, `PATCH api/v1/me`: `personal` (il tuo assistente: tu sei l'"io",
|
|
53
55
|
ciò che arriva senza identità dichiarata è tuo) o `entity` (un agente condiviso da una famiglia, un team, un luogo:
|
|
54
56
|
ciò che arriva senza identità è di "qualcuno"); `gender` `masculine` (default) | `feminine` | `neutral` per la prima
|
|
55
57
|
persona nelle lingue con il genere. Il plugin non ha impostazioni per questi valori.
|
|
@@ -97,6 +99,15 @@ non tu: elenca lì i tuoi id (oppure chiedi all'amministratore di legarli al sé
|
|
|
97
99
|
di scrivere la prima volta). Le parole degli altri restano loro (ruolo `other`, attribuite al loro contatto), così ciò
|
|
98
100
|
che dicono di sé non diventa mai un fatto su di te.
|
|
99
101
|
|
|
102
|
+
**Oppure collega il tuo sender id con un codice** (D51, memorie personali). Crea un codice monouso — "Codice di collegamento" sulla
|
|
103
|
+
memoria nella console di amministrazione, oppure `POST api/v1/me/link-codes` con il token personale della memoria
|
|
104
|
+
(`→ {"code": "RCD-…", "expiresAt": …}`, dieci minuti) — e invialo da solo all'agente in una **chat diretta** dal tuo
|
|
105
|
+
account del canale. Il plugin lo invia subito a Recordare (non viene mai salvato come messaggio) e dice al modello solo di
|
|
106
|
+
rispondere che il codice è arrivato; poi conferma la richiesta nella console ("Da confermare" → "È il titolare") oppure
|
|
107
|
+
con `GET api/v1/me/identities` e `POST api/v1/me/identities/pending/{id}/confirm`, entro sette giorni. Da allora quel
|
|
108
|
+
mittente sei tu; i messaggi precedenti mantengono la loro attribuzione. Il plugin considera gruppi le sessioni di gruppo,
|
|
109
|
+
canale e stanza (dalla chiave di sessione), dove un codice non chiede nulla.
|
|
110
|
+
|
|
100
111
|
**Aggiornare da una mappa `users`.** Il comportamento precedente (ogni mittente mappato un utente Recordare separato, i
|
|
101
112
|
non mappati non ricordati) è `memoryPer: "user"`: aggiungilo per tenere le memorie divise per persona.
|
|
102
113
|
|
package/dist/index.js
CHANGED
|
@@ -7300,6 +7300,8 @@ var require_content_type = __commonJS({
|
|
|
7300
7300
|
});
|
|
7301
7301
|
|
|
7302
7302
|
// src/index.ts
|
|
7303
|
+
import { homedir } from "node:os";
|
|
7304
|
+
import { join as join2 } from "node:path";
|
|
7303
7305
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
7304
7306
|
import { isCronSessionKey, isIncognitoSessionKey, isSubagentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
7305
7307
|
|
|
@@ -17504,8 +17506,8 @@ async function random(size) {
|
|
|
17504
17506
|
const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
|
|
17505
17507
|
let result = "";
|
|
17506
17508
|
while (result.length < size) {
|
|
17507
|
-
const
|
|
17508
|
-
for (const randomByte of
|
|
17509
|
+
const randomBytes2 = await getRandomValues(size - result.length);
|
|
17510
|
+
for (const randomByte of randomBytes2) {
|
|
17509
17511
|
if (randomByte < evenDistCutoff) {
|
|
17510
17512
|
result += mask[randomByte % mask.length];
|
|
17511
17513
|
}
|
|
@@ -19070,7 +19072,7 @@ var RecordareMcp = class {
|
|
|
19070
19072
|
},
|
|
19071
19073
|
requestInit: { headers: { ...this.http.headers(user), [CONVERSATION_HEADER]: conversation } }
|
|
19072
19074
|
});
|
|
19073
|
-
const client = new Client(this.options.clientInfo ?? { name: "recordare-client", version: "0.
|
|
19075
|
+
const client = new Client(this.options.clientInfo ?? { name: "recordare-client", version: "0.4.0" });
|
|
19074
19076
|
await client.connect(transport);
|
|
19075
19077
|
return client;
|
|
19076
19078
|
}
|
|
@@ -19109,6 +19111,33 @@ var RecordareClient = class {
|
|
|
19109
19111
|
throw err;
|
|
19110
19112
|
}
|
|
19111
19113
|
}
|
|
19114
|
+
/**
|
|
19115
|
+
* Erases the whole memory behind `user`, at once and for good (needs the `erase` scope): everything it holds goes.
|
|
19116
|
+
* `memoryId` (from `me`) confirms which memory; a user without a memory answers 404, none is created.
|
|
19117
|
+
*/
|
|
19118
|
+
eraseMemory(user, memoryId) {
|
|
19119
|
+
return this.http.request("DELETE", `api/v1/me${qs({ confirm: memoryId })}`, { user });
|
|
19120
|
+
}
|
|
19121
|
+
/** A one-time code for the holder to send from a channel (D51; personal memories only, else 409). */
|
|
19122
|
+
linkCode(user) {
|
|
19123
|
+
return this.http.request("POST", "api/v1/me/link-codes", { user });
|
|
19124
|
+
}
|
|
19125
|
+
/** The identities that speak as the memory's "I" and the claims waiting for confirmation (D51). */
|
|
19126
|
+
holderIdentities(user) {
|
|
19127
|
+
return this.http.request("GET", "api/v1/me/identities", { user });
|
|
19128
|
+
}
|
|
19129
|
+
/** Confirms a claim: that channel identity is the holder from now on. */
|
|
19130
|
+
async confirmHolderClaim(user, id) {
|
|
19131
|
+
await this.http.request("POST", `api/v1/me/identities/pending/${encodeURIComponent(id)}/confirm`, { user });
|
|
19132
|
+
}
|
|
19133
|
+
/** Rejects a claim (not the holder). */
|
|
19134
|
+
async rejectHolderClaim(user, id) {
|
|
19135
|
+
await this.http.request("DELETE", `api/v1/me/identities/pending/${encodeURIComponent(id)}`, { user });
|
|
19136
|
+
}
|
|
19137
|
+
/** Unlinks one of the holder's identities. */
|
|
19138
|
+
async unlinkHolderIdentity(user, id) {
|
|
19139
|
+
await this.http.request("DELETE", `api/v1/me/identities/${encodeURIComponent(id)}`, { user });
|
|
19140
|
+
}
|
|
19112
19141
|
/**
|
|
19113
19142
|
* Sends messages of one conversation, split into requests of at most MAX_MESSAGES_PER_REQUEST. Idempotent: Recordare
|
|
19114
19143
|
* deduplicates on each message's externalId. Recordare always stores (D50): the on/off switch is the platform's.
|
|
@@ -19133,20 +19162,44 @@ var RecordareClient = class {
|
|
|
19133
19162
|
}
|
|
19134
19163
|
return total;
|
|
19135
19164
|
}
|
|
19165
|
+
/**
|
|
19166
|
+
* Sends a room transcript to the memories of the people present (continuous listening), split into requests of at
|
|
19167
|
+
* most MAX_MESSAGES_PER_REQUEST; needs a client key (not a personal token). Idempotent like `ingest`.
|
|
19168
|
+
*/
|
|
19169
|
+
async ingestRoom(req) {
|
|
19170
|
+
const total = /* @__PURE__ */ new Map();
|
|
19171
|
+
for (let i = 0; i < req.messages.length; i += MAX_MESSAGES_PER_REQUEST) {
|
|
19172
|
+
const last = i + MAX_MESSAGES_PER_REQUEST >= req.messages.length;
|
|
19173
|
+
const part = await this.http.request("POST", "api/v1/ingest/room", {
|
|
19174
|
+
body: { ...req, messages: req.messages.slice(i, i + MAX_MESSAGES_PER_REQUEST), hints: req.hints && last ? req.hints : void 0 }
|
|
19175
|
+
});
|
|
19176
|
+
for (const r of part.recipients) {
|
|
19177
|
+
const t = total.get(r.user);
|
|
19178
|
+
if (!t) total.set(r.user, { ...r, conflicts: [...r.conflicts] });
|
|
19179
|
+
else {
|
|
19180
|
+
t.conversationId = r.conversationId ?? t.conversationId;
|
|
19181
|
+
t.accepted += r.accepted;
|
|
19182
|
+
t.duplicates += r.duplicates;
|
|
19183
|
+
t.conflicts.push(...r.conflicts);
|
|
19184
|
+
}
|
|
19185
|
+
}
|
|
19186
|
+
}
|
|
19187
|
+
return { recipients: [...total.values()] };
|
|
19188
|
+
}
|
|
19136
19189
|
/**
|
|
19137
19190
|
* The memories relevant to `query` (the message about to be answered) as a fenced block to append to the prompt — or
|
|
19138
19191
|
* null. Append it at the end of the system prompt (it changes every turn: keep the stable part first for prompt
|
|
19139
|
-
* caching); never store it as a chat message.
|
|
19192
|
+
* caching); never store it as a chat message. `card`: open it with the self card (who I am), off by default.
|
|
19140
19193
|
*/
|
|
19141
|
-
context(user, conversation, query) {
|
|
19142
|
-
return this.http.request("POST", "api/v1/context", { user, body: { query }, headers: { [CONVERSATION_HEADER]: conversation } });
|
|
19194
|
+
context(user, conversation, query, opts = {}) {
|
|
19195
|
+
return this.http.request("POST", "api/v1/context", { user, body: { query, ...opts }, headers: { [CONVERSATION_HEADER]: conversation } });
|
|
19143
19196
|
}
|
|
19144
19197
|
/**
|
|
19145
19198
|
* Stores the turn and returns the memory context for it in one round trip (needs the `ingest` and `read` scopes):
|
|
19146
19199
|
* the context is computed for the ingested conversation, from `query` or else the turn's last user message.
|
|
19147
19200
|
*/
|
|
19148
|
-
contextWithTurn(user, turn, query) {
|
|
19149
|
-
return this.http.request("POST", "api/v1/context", { user, body: { ingest: turn, ...query ? { query } : {} } });
|
|
19201
|
+
contextWithTurn(user, turn, query, opts = {}) {
|
|
19202
|
+
return this.http.request("POST", "api/v1/context", { user, body: { ingest: turn, ...query ? { query } : {}, ...opts } });
|
|
19150
19203
|
}
|
|
19151
19204
|
/** The conversation ended (session closed, /new): extraction runs now. A conversation Recordare never had is done. */
|
|
19152
19205
|
async endConversation(user, conversation) {
|
|
@@ -19206,12 +19259,23 @@ var RecordareClient = class {
|
|
|
19206
19259
|
async forgetEpisode(user, id) {
|
|
19207
19260
|
await this.http.request("DELETE", `api/v1/episodes/${enc(id)}`, { user });
|
|
19208
19261
|
}
|
|
19262
|
+
/**
|
|
19263
|
+
* The person forgets a period (days or months, inclusive): its episodes and raw messages (`keepRaw`: kept but hidden
|
|
19264
|
+
* and never extracted again), with what only they supported. Returns how much went.
|
|
19265
|
+
*/
|
|
19266
|
+
forgetPeriod(user, period) {
|
|
19267
|
+
return this.http.request("POST", "api/v1/forget", { user, body: period });
|
|
19268
|
+
}
|
|
19209
19269
|
digests(user, query = {}) {
|
|
19210
19270
|
return this.http.request("GET", `api/v1/digests${qs(query)}`, { user });
|
|
19211
19271
|
}
|
|
19212
19272
|
facts(user, query = {}) {
|
|
19213
19273
|
return this.http.request("GET", `api/v1/facts${qs(query)}`, { user });
|
|
19214
19274
|
}
|
|
19275
|
+
/** Who I am and who my contacts are: current facts and notes of the self and of each contact (no LLM). */
|
|
19276
|
+
cards(user) {
|
|
19277
|
+
return this.http.request("GET", "api/v1/cards", { user });
|
|
19278
|
+
}
|
|
19215
19279
|
notes(user, query = {}) {
|
|
19216
19280
|
return this.http.request("GET", `api/v1/notes${qs(query)}`, { user });
|
|
19217
19281
|
}
|
|
@@ -19230,6 +19294,54 @@ var RecordareClient = class {
|
|
|
19230
19294
|
async decide(user, what, id, decision) {
|
|
19231
19295
|
await this.http.request("POST", `api/v1/${what}/${enc(id)}/${decision}`, { user });
|
|
19232
19296
|
}
|
|
19297
|
+
/** The person writes an episode in the platform's UI (stated, theirs). Returns its id. */
|
|
19298
|
+
async addEpisode(user, episode) {
|
|
19299
|
+
return (await this.http.request("POST", "api/v1/episodes", { user, body: episode })).id;
|
|
19300
|
+
}
|
|
19301
|
+
/** The person writes a note in the platform's UI (stated, theirs). Returns its id. */
|
|
19302
|
+
async addNote(user, note) {
|
|
19303
|
+
return (await this.http.request("POST", "api/v1/notes", { user, body: note })).id;
|
|
19304
|
+
}
|
|
19305
|
+
fact(user, id) {
|
|
19306
|
+
return this.http.request("GET", `api/v1/facts/${enc(id)}`, { user });
|
|
19307
|
+
}
|
|
19308
|
+
note(user, id) {
|
|
19309
|
+
return this.http.request("GET", `api/v1/notes/${enc(id)}`, { user });
|
|
19310
|
+
}
|
|
19311
|
+
/** The person corrects a current fact (`value` null: not known any more); the wrong one stays in its history. */
|
|
19312
|
+
async correctFact(user, id, fix) {
|
|
19313
|
+
return (await this.http.request("POST", `api/v1/facts/${enc(id)}/corrections`, { user, body: fix })).id;
|
|
19314
|
+
}
|
|
19315
|
+
/** The person corrects a current note; the wrong one stays as `corrected`. */
|
|
19316
|
+
async correctNote(user, id, fix) {
|
|
19317
|
+
return (await this.http.request("POST", `api/v1/notes/${enc(id)}/corrections`, { user, body: fix })).id;
|
|
19318
|
+
}
|
|
19319
|
+
/** The notes change feed, for a platform keeping a copy: pass `next` back as `since`; `more` when the page was full. */
|
|
19320
|
+
noteChanges(user, query = {}) {
|
|
19321
|
+
return this.http.request("GET", `api/v1/notes/changes${qs(query)}`, { user });
|
|
19322
|
+
}
|
|
19323
|
+
settings(user) {
|
|
19324
|
+
return this.http.request("GET", "api/v1/settings", { user });
|
|
19325
|
+
}
|
|
19326
|
+
/** Needs `memory_settings`: the memory's personal token (never a client key); pass `user` empty with it. */
|
|
19327
|
+
updateSettings(user, patch) {
|
|
19328
|
+
return this.http.request("PATCH", "api/v1/settings", { user, body: patch });
|
|
19329
|
+
}
|
|
19330
|
+
/** LLM calls and tokens spent on the memory between two local days (default: the last 30). */
|
|
19331
|
+
usage(user, query = {}) {
|
|
19332
|
+
return this.http.request("GET", `api/v1/usage${qs(query)}`, { user });
|
|
19333
|
+
}
|
|
19334
|
+
/** Starts a full export (needs `export`: the memory's personal token); poll `exportJob`, then `exportArchive`. */
|
|
19335
|
+
startExport(user) {
|
|
19336
|
+
return this.http.request("POST", "api/v1/exports", { user });
|
|
19337
|
+
}
|
|
19338
|
+
exportJob(user, id) {
|
|
19339
|
+
return this.http.request("GET", `api/v1/exports/${enc(id)}`, { user });
|
|
19340
|
+
}
|
|
19341
|
+
/** The finished export: one JSON archive (`format: "recordare-export/1"`). */
|
|
19342
|
+
exportArchive(user, id) {
|
|
19343
|
+
return this.http.request("GET", `api/v1/exports/${enc(id)}/archive`, { user });
|
|
19344
|
+
}
|
|
19233
19345
|
/** Closes the MCP sessions (on shutdown). */
|
|
19234
19346
|
close() {
|
|
19235
19347
|
return this.mcp.close();
|
|
@@ -19371,7 +19483,7 @@ var TOOLS = [
|
|
|
19371
19483
|
{
|
|
19372
19484
|
"name": "log_episode",
|
|
19373
19485
|
"title": "Note something that happened or is planned",
|
|
19374
|
-
"description": 'Explicit capture ("note that today I serviced the car"). Resolve the date yourself (ISO).',
|
|
19486
|
+
"description": 'Explicit capture ("note that today I serviced the car"). Resolve the date yourself (ISO). Conversations are remembered automatically: use it only when explicitly asked, and never ask whether to remember or save something.',
|
|
19375
19487
|
"inputSchema": {
|
|
19376
19488
|
"type": "object",
|
|
19377
19489
|
"properties": {
|
|
@@ -19421,7 +19533,7 @@ var TOOLS = [
|
|
|
19421
19533
|
{
|
|
19422
19534
|
"name": "remember",
|
|
19423
19535
|
"title": "Remember something",
|
|
19424
|
-
"description": 'Explicit "remember that\u2026" about preferences, habits, values, knowledge.',
|
|
19536
|
+
"description": 'Explicit "remember that\u2026" about preferences, habits, values, knowledge. Conversations are remembered automatically: use it only when explicitly asked, and never ask whether to remember or save something.',
|
|
19425
19537
|
"inputSchema": {
|
|
19426
19538
|
"type": "object",
|
|
19427
19539
|
"properties": {
|
|
@@ -19467,7 +19579,7 @@ var TOOLS = [
|
|
|
19467
19579
|
{
|
|
19468
19580
|
"name": "search_episodes",
|
|
19469
19581
|
"title": "Search what happened",
|
|
19470
|
-
"description": 'Search your memory of what happened: what you lived, did, planned or learned, and what happened to the people you know (events, plans, changes), with dates and status. Each item has a subject: you (written in the first person), a person by name, someone, or undecided. Use `from`/`to` (ISO dates, inclusive; see resolve_period) for questions about a period. mode: "search" = most relevant, "list" = chronological in the period (overviews, counting), "latest" = most recent first ("when did I last\u2026").',
|
|
19582
|
+
"description": 'Search your memory of what happened: what you lived, did, planned or learned, and what happened to the people you know (events, plans, changes), with dates and status. Each item has a subject: you (written in the first person), a person by name, someone, or undecided. Use `from`/`to` (ISO dates, inclusive; see resolve_period) for questions about a period; a vague span ("lately", "these weeks") is not a period: leave them out. mode: "search" = most relevant, "list" = chronological in the period (overviews, counting), "latest" = most recent first ("when did I last\u2026"). An item or chat excerpt marked `heard` was overheard in the room by a listening device, not said to you: tell it as heard at home, and its speaker may have been misrecognised \u2014 what it says of you may be someone else\'s. A chat excerpt by `assistant` is your own earlier reply: what you told them then (advice, a recipe, a name), not something they lived.',
|
|
19471
19583
|
"inputSchema": {
|
|
19472
19584
|
"type": "object",
|
|
19473
19585
|
"properties": {
|
|
@@ -19531,7 +19643,7 @@ var TOOLS = [
|
|
|
19531
19643
|
{
|
|
19532
19644
|
"name": "search_memory",
|
|
19533
19645
|
"title": "Search what you know about yourself and others",
|
|
19534
|
-
"description": 'Search your memory of preferences, habits, values, relationships, knowledge and current state (car, home, job\u2026) \u2014 yours and of the people you know; each item has its subject. Use `as_of` (ISO date) for "what was it on that date" questions; each fact comes with its history.',
|
|
19646
|
+
"description": 'Search your memory of preferences, habits, values, relationships, knowledge and current state (car, home, job\u2026) \u2014 yours and of the people you know; each item has its subject. Use `as_of` (ISO date) for "what was it on that date" questions; each fact comes with its history. A note with `pattern` is a habit you noticed yourself in repeated episodes, not something said: tell it as such.',
|
|
19535
19647
|
"inputSchema": {
|
|
19536
19648
|
"type": "object",
|
|
19537
19649
|
"properties": {
|
|
@@ -19556,6 +19668,13 @@ var TOOLS = [
|
|
|
19556
19668
|
}
|
|
19557
19669
|
];
|
|
19558
19670
|
|
|
19671
|
+
// ../../packages/client/src/links.ts
|
|
19672
|
+
var LINK_CODE = /^\s*RCD-?[0-9A-Z]{4}-?[0-9A-Z]{4}\.?\s*$/i;
|
|
19673
|
+
function isLinkCode(text) {
|
|
19674
|
+
return LINK_CODE.test(text);
|
|
19675
|
+
}
|
|
19676
|
+
var LINK_CODE_INSTRUCTION = "The user's last message is a one-time code that links their account on this channel to their memory (Recordare); it is handled automatically. Reply only, briefly and in the user's language, that the code was received and that they now confirm the link where they created the code. Do not repeat the code.";
|
|
19677
|
+
|
|
19559
19678
|
// ../../packages/client/src/delivery.ts
|
|
19560
19679
|
var DEFAULT_DELIVERY = { maxAttempts: 12, baseDelayMs: 5e3, maxDelayMs: 60 * 60 * 1e3 };
|
|
19561
19680
|
function backoffMs(attempts, policy = DEFAULT_DELIVERY) {
|
|
@@ -19606,6 +19725,80 @@ function isSystemRun(ctx) {
|
|
|
19606
19725
|
return ctx.trigger === "cron" || ctx.trigger === "heartbeat" || ctx.inputProvenance?.kind === "internal_system" || ctx.inputProvenance?.kind === "inter_session";
|
|
19607
19726
|
}
|
|
19608
19727
|
|
|
19728
|
+
// src/outbox.ts
|
|
19729
|
+
import { randomBytes } from "node:crypto";
|
|
19730
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19731
|
+
import { join } from "node:path";
|
|
19732
|
+
var FILE = /^\d{15}-[0-9a-f]{12}\.json$/;
|
|
19733
|
+
function pendingId(now = Date.now()) {
|
|
19734
|
+
return `${String(now).padStart(15, "0")}-${randomBytes(6).toString("hex")}`;
|
|
19735
|
+
}
|
|
19736
|
+
var Outbox = class {
|
|
19737
|
+
constructor(dir, warn) {
|
|
19738
|
+
this.warn = warn;
|
|
19739
|
+
this.dir = dir;
|
|
19740
|
+
if (!dir) return;
|
|
19741
|
+
try {
|
|
19742
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
19743
|
+
} catch (err) {
|
|
19744
|
+
this.disable(err);
|
|
19745
|
+
}
|
|
19746
|
+
}
|
|
19747
|
+
dir;
|
|
19748
|
+
get durable() {
|
|
19749
|
+
return this.dir !== null;
|
|
19750
|
+
}
|
|
19751
|
+
/** The batches left by a previous run, oldest first; unreadable files are dropped (logged). */
|
|
19752
|
+
load() {
|
|
19753
|
+
if (!this.dir) return [];
|
|
19754
|
+
let names;
|
|
19755
|
+
try {
|
|
19756
|
+
names = readdirSync(this.dir).filter((n) => FILE.test(n)).sort();
|
|
19757
|
+
} catch (err) {
|
|
19758
|
+
this.disable(err);
|
|
19759
|
+
return [];
|
|
19760
|
+
}
|
|
19761
|
+
const out = [];
|
|
19762
|
+
for (const name of names) {
|
|
19763
|
+
const path = join(this.dir, name);
|
|
19764
|
+
try {
|
|
19765
|
+
const p = JSON.parse(readFileSync(path, "utf8"));
|
|
19766
|
+
if (typeof p.user !== "string" || !Array.isArray(p.req?.messages)) throw new Error("not a batch");
|
|
19767
|
+
out.push({ ...p, id: name.slice(0, -".json".length) });
|
|
19768
|
+
} catch {
|
|
19769
|
+
this.warn(`recordare: dropped an unreadable queued batch (${name})`);
|
|
19770
|
+
try {
|
|
19771
|
+
unlinkSync(path);
|
|
19772
|
+
} catch {
|
|
19773
|
+
}
|
|
19774
|
+
}
|
|
19775
|
+
}
|
|
19776
|
+
return out;
|
|
19777
|
+
}
|
|
19778
|
+
/** Writes (or rewrites) a batch atomically: a temporary file renamed over the old one. */
|
|
19779
|
+
save(p) {
|
|
19780
|
+
if (!this.dir) return;
|
|
19781
|
+
const path = join(this.dir, `${p.id}.json`);
|
|
19782
|
+
try {
|
|
19783
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(p), { mode: 384 });
|
|
19784
|
+
renameSync(`${path}.tmp`, path);
|
|
19785
|
+
} catch (err) {
|
|
19786
|
+
this.disable(err);
|
|
19787
|
+
}
|
|
19788
|
+
}
|
|
19789
|
+
remove(p) {
|
|
19790
|
+
if (!this.dir) return;
|
|
19791
|
+
try {
|
|
19792
|
+
unlinkSync(join(this.dir, `${p.id}.json`));
|
|
19793
|
+
} catch {
|
|
19794
|
+
}
|
|
19795
|
+
}
|
|
19796
|
+
disable(err) {
|
|
19797
|
+
this.warn(`recordare: retry queue kept in memory only, its folder is not usable (${err instanceof Error ? err.message : "error"})`);
|
|
19798
|
+
this.dir = null;
|
|
19799
|
+
}
|
|
19800
|
+
};
|
|
19801
|
+
|
|
19609
19802
|
// src/tools.ts
|
|
19610
19803
|
var OFFERED = TOOLS.filter((t) => t.name !== "log_episode");
|
|
19611
19804
|
var NAMES = new RegExp(`\\b(${OFFERED.map((t) => t.name).join("|")})\\b`, "g");
|
|
@@ -19660,9 +19853,10 @@ function lastTurn(messages) {
|
|
|
19660
19853
|
}
|
|
19661
19854
|
|
|
19662
19855
|
// src/plugin.ts
|
|
19663
|
-
var VERSION = "0.
|
|
19856
|
+
var VERSION = "0.4.0";
|
|
19664
19857
|
var TOOL_TIMEOUT_MS = 2e4;
|
|
19665
|
-
var RETRY = { maxAttempts:
|
|
19858
|
+
var RETRY = { maxAttempts: 16, baseDelayMs: 2e3, maxDelayMs: 60 * 6e4 };
|
|
19859
|
+
var RESUME_MS = 5e3;
|
|
19666
19860
|
var MAX_QUEUE = 500;
|
|
19667
19861
|
var MAX_TRACKED = 500;
|
|
19668
19862
|
var MAX_GROUP_BUFFER = 50;
|
|
@@ -19684,7 +19878,7 @@ function withTimeout(p, ms) {
|
|
|
19684
19878
|
})
|
|
19685
19879
|
]).finally(() => clearTimeout(timer));
|
|
19686
19880
|
}
|
|
19687
|
-
function registerRecordare(api, cfg, kinds) {
|
|
19881
|
+
function registerRecordare(api, cfg, kinds, outboxDir = null) {
|
|
19688
19882
|
const log = api.logger;
|
|
19689
19883
|
const clientInfo = { name: "openclaw-recordare", version: VERSION };
|
|
19690
19884
|
const fast = new RecordareClient({ baseUrl: cfg.url, apiKey: cfg.apiKey, timeoutMs: cfg.timeoutMs, mcp: { clientInfo } });
|
|
@@ -19695,7 +19889,8 @@ function registerRecordare(api, cfg, kinds) {
|
|
|
19695
19889
|
const groupBuffer = /* @__PURE__ */ new Map();
|
|
19696
19890
|
const groupParticipants = /* @__PURE__ */ new Map();
|
|
19697
19891
|
const names = /* @__PURE__ */ new Map();
|
|
19698
|
-
const
|
|
19892
|
+
const outbox = new Outbox(outboxDir, (m) => log.warn(m));
|
|
19893
|
+
const queue = outbox.load();
|
|
19699
19894
|
let timer;
|
|
19700
19895
|
let stopped = false;
|
|
19701
19896
|
if (cfg.memoryPer === "agent" && !cfg.defaultUser) {
|
|
@@ -19708,18 +19903,21 @@ function registerRecordare(api, cfg, kinds) {
|
|
|
19708
19903
|
try {
|
|
19709
19904
|
const res = await slow.ingest(headerUser(p.user), p.req);
|
|
19710
19905
|
if (res.conflicts.length) log.warn(`recordare: ${res.conflicts.length} message id(s) conflicted (kept the stored version)`);
|
|
19906
|
+
outbox.remove(p);
|
|
19711
19907
|
return true;
|
|
19712
19908
|
} catch (err) {
|
|
19713
19909
|
p.attempts++;
|
|
19714
19910
|
const next = afterFailure(err, p.attempts, RETRY);
|
|
19715
19911
|
if (next.action === "park") {
|
|
19716
19912
|
log.warn(`recordare: dropped ${p.req.messages.length} message(s) after ${p.attempts} attempt(s) (${next.reason}: ${errName(err)})`);
|
|
19913
|
+
outbox.remove(p);
|
|
19717
19914
|
return true;
|
|
19718
19915
|
}
|
|
19719
19916
|
if (queue.length >= MAX_QUEUE) {
|
|
19720
|
-
queue.shift();
|
|
19917
|
+
outbox.remove(queue.shift());
|
|
19721
19918
|
log.warn("recordare: retry queue full, dropped the oldest batch");
|
|
19722
19919
|
}
|
|
19920
|
+
outbox.save(p);
|
|
19723
19921
|
queue.push(p);
|
|
19724
19922
|
schedule(next.delayMs);
|
|
19725
19923
|
if (p.attempts === 1) log.warn(`recordare: ingest failed (${errName(err)}), retrying in the background`);
|
|
@@ -19739,7 +19937,7 @@ function registerRecordare(api, cfg, kinds) {
|
|
|
19739
19937
|
for (const p of batch) await deliver(p);
|
|
19740
19938
|
}
|
|
19741
19939
|
function send(user, req) {
|
|
19742
|
-
return deliver({ user, req, attempts: 0 });
|
|
19940
|
+
return deliver({ id: pendingId(), user, req, attempts: 0 });
|
|
19743
19941
|
}
|
|
19744
19942
|
function who(ctx) {
|
|
19745
19943
|
const key = ctx.sessionKey;
|
|
@@ -19766,6 +19964,8 @@ function registerRecordare(api, cfg, kinds) {
|
|
|
19766
19964
|
source: "chat",
|
|
19767
19965
|
channel: w.channel ? `openclaw:${w.channel}` : "openclaw",
|
|
19768
19966
|
title: ctx.sessionKey,
|
|
19967
|
+
// Group sessions are known by their key; every other session of a person is a direct chat (D51: link codes).
|
|
19968
|
+
...ctx.sessionKey ? { direct: !isGroupSession(ctx.sessionKey) } : {},
|
|
19769
19969
|
participants
|
|
19770
19970
|
};
|
|
19771
19971
|
}
|
|
@@ -19787,6 +19987,10 @@ function registerRecordare(api, cfg, kinds) {
|
|
|
19787
19987
|
const conversation = conversationMeta(w, ctx);
|
|
19788
19988
|
const message = { externalId: `${id}:u`, role: w.speaker.role, authorRef: w.speaker.ref, content: clipUtf8(text), sentAt: now };
|
|
19789
19989
|
remember(turns, ctx.runId ?? w.conversation, { user: w.user, conversation, message });
|
|
19990
|
+
if (cfg.capture && isLinkCode(text)) {
|
|
19991
|
+
void send(w.user, { conversation, messages: [message] });
|
|
19992
|
+
return { prependSystemContext: LINK_CODE_INSTRUCTION };
|
|
19993
|
+
}
|
|
19790
19994
|
if (!cfg.capture) {
|
|
19791
19995
|
const ctxRes = await fast.context(headerUser(w.user), w.conversation, text.slice(0, 4e3));
|
|
19792
19996
|
return ctxRes.block ? { prependContext: ctxRes.block } : void 0;
|
|
@@ -19897,12 +20101,17 @@ function registerRecordare(api, cfg, kinds) {
|
|
|
19897
20101
|
}, { name });
|
|
19898
20102
|
}
|
|
19899
20103
|
}
|
|
20104
|
+
if (queue.length) {
|
|
20105
|
+
log.info(`recordare: ${queue.length} undelivered batch(es) from the last run, retrying`);
|
|
20106
|
+
schedule(RESUME_MS);
|
|
20107
|
+
}
|
|
19900
20108
|
return {
|
|
19901
20109
|
async stop() {
|
|
19902
20110
|
stopped = true;
|
|
19903
20111
|
if (timer) clearTimeout(timer);
|
|
19904
20112
|
timer = void 0;
|
|
19905
|
-
|
|
20113
|
+
const last = queue.splice(0);
|
|
20114
|
+
await withTimeout(Promise.all(last.map((p) => slow.ingest(headerUser(p.user), p.req).then(() => outbox.remove(p), () => void 0))), 1500).catch(() => void 0);
|
|
19906
20115
|
await Promise.all([fast.close(), slow.close()]).catch(() => void 0);
|
|
19907
20116
|
}
|
|
19908
20117
|
};
|
|
@@ -19919,11 +20128,12 @@ var index_default = definePluginEntry({
|
|
|
19919
20128
|
api.logger.warn("recordare: not configured (set plugins.entries.recordare.config.url and apiKey); memory is off");
|
|
19920
20129
|
return;
|
|
19921
20130
|
}
|
|
20131
|
+
const stateDir = api.runtime?.state?.resolveStateDir?.(process.env) || process.env.OPENCLAW_STATE_DIR?.trim() || join2(homedir(), ".openclaw");
|
|
19922
20132
|
const plugin = registerRecordare(api, cfg, {
|
|
19923
20133
|
isIncognito: (k) => !!k && isIncognitoSessionKey(k),
|
|
19924
20134
|
isCron: (k) => !!k && isCronSessionKey(k),
|
|
19925
20135
|
isSubagent: (k) => !!k && isSubagentSessionKey(k)
|
|
19926
|
-
});
|
|
20136
|
+
}, join2(stateDir, "recordare", "outbox"));
|
|
19927
20137
|
api.registerService({ id: "recordare", start: () => void 0, stop: () => plugin.stop() });
|
|
19928
20138
|
}
|
|
19929
20139
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkimedehq/openclaw-recordare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Recordare for OpenClaw: long-term episodic memory kept by your own Recordare service (capture, recall before each turn, memory tools).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Andrea Genovese",
|