@arkimedehq/recordare-client 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 CHANGED
@@ -19,9 +19,9 @@ import { PersonDirectory, RecordareClient, afterFailure } from '@arkimedehq/reco
19
19
  const rc = new RecordareClient({ baseUrl: 'http://recordare:8080', apiKey: process.env.RECORDARE_API_KEY!,
20
20
  headers: () => traceHeaders() }); // e.g. OpenTelemetry propagation.inject
21
21
 
22
- // Which memory the user's account opens, its mode (personal | entity) and Atlas address; the name follows the platform's profile. `enabled` is the
22
+ // Which memory the user's account opens, its mode (personal | entity) and Atlas address; the name and gender follow the platform's profile. `enabled` is the
23
23
  // platform's own memory switch: while false, Recordare is not contacted (no consent flag in Recordare, D50).
24
- const people = new PersonDirectory(rc, { user: async (id) => ({ enabled: true, name: 'Andrea' }) });
24
+ const people = new PersonDirectory(rc, { user: async (id) => ({ enabled: true, name: 'Andrea', gender: 'masculine' }) });
25
25
  const person = await people.refresh('user-42'); // { memoryId, mode, atlasUrl }; peek() reads the cache
26
26
 
27
27
  // Ingest (split into requests of 500, idempotent on each externalId): → { conversationId, accepted, duplicates, conflicts }.
@@ -32,8 +32,14 @@ await rc.ingest('user-42', { conversation: { externalId: 'chat-1' }, messages: [
32
32
  await rc.ingest('user-42', { conversation: { externalId: 'doc-1', source: 'document' }, messages: [
33
33
  { externalId: 'd1', role: 'user', own: true, content: 'La caldaia va revisionata ogni due anni.', sentAt: new Date().toISOString() },
34
34
  ] });
35
+ // A room transcript (continuous listening): Recordare fans it out to the memories of the people present.
36
+ await rc.ingestRoom({ conversation: { externalId: 'kitchen-1' }, recipients: ['user-42', 'user-7'],
37
+ speakers: [{ ref: 'a', user: 'user-42' }, { ref: 'b', user: 'user-7' }, { ref: 'c', displayName: 'Leo', identity: { channel: 'voice', externalId: 'leo' } }],
38
+ messages: [{ externalId: 'k1', speakerRef: 'c', content: 'Mi hanno preso in squadra!', sentAt: new Date().toISOString() }] });
35
39
  // Mode (only while the memory is empty, else MemoryNotEmptyError) and the first person's gender, from the profile.
36
40
  await rc.updateMe('user-42', { gender: 'feminine' });
41
+ // The account is deleted: erase the whole memory (a key with the erase scope; the memory id confirms which).
42
+ await rc.eraseMemory('user-42', (await rc.me('user-42')).memoryId);
37
43
 
38
44
  // Recall: one MCP session per user AND conversation (evidence of writes, the current turn), both bound in code.
39
45
  const tools = await rc.mcp.listTools('user-42', 'chat-1'); // build the agent's tools from these schemas
@@ -68,7 +74,28 @@ await rc.learnSource('user-42', { externalId: 'boiler-manual', title: 'Manuale d
68
74
 
69
75
  ## The diary
70
76
  `episodes`, `episode`, `digests`, `facts`, `notes`, `plans` read what Recordare remembers for the person's own view in
71
- the platform's UI; `correctEpisode`, `forgetEpisode`, `pinNote`, `delete`, `decide` are the person's edits (API.md §4).
77
+ the platform's UI; `correctEpisode`, `forgetEpisode`, `forgetPeriod`, `pinNote`, `delete`, `decide` are the person's edits (API.md §4).
78
+ `fact`, `note` give one item with its evidence; `addEpisode`, `addNote` are written by the person (stated, theirs);
79
+ `correctFact`, `correctNote` keep the wrong version as history. `noteChanges` is the change feed for a platform keeping a
80
+ copy of the notes (pass `next` back as `since`). `settings`, `usage` read the memory's settings and the LLM calls spent on
81
+ it; `updateSettings` and the export (`startExport` → `exportJob` → `exportArchive`) need the memory's personal token
82
+ (scopes `memory_settings`, `export`; pass `user` empty).
83
+
84
+ ## Linking the holder's channel identity (D51)
85
+ In a personal memory a channel sender speaks as "I" only once the holder links it: `linkCode(user)` → `{code,
86
+ expiresAt}` (`RCD-XXXX-XXXX`, ten minutes, one use) — the holder sends the code alone to the agent from that channel, in
87
+ a direct chat (set `conversation.direct` on the ingest when you know it; `false` = a group, where a code claims nothing);
88
+ then `holderIdentities(user)` → `{linked, pending}`, and `confirmHolderClaim(user, id)` / `rejectHolderClaim(user, id)`
89
+ within seven days; `unlinkHolderIdentity(user, id)` undoes a link. Recordare never stores the code's message, but your
90
+ model sees it: when `isLinkCode(text)` (the service's own `LINK_CODE` pattern), send the turn at once and give the model
91
+ `LINK_CODE_INSTRUCTION` instead of a memory context.
92
+
93
+ ```ts
94
+ const { code } = await rc.linkCode('user-42'); // show it to the holder
95
+ if (isLinkCode(text)) systemExtra = LINK_CODE_INSTRUCTION; // the model says the code was received, without repeating it
96
+ const { pending } = await rc.holderIdentities('user-42');
97
+ await rc.confirmHolderClaim('user-42', pending[0].id);
98
+ ```
72
99
 
73
100
  ## What stays with the host
74
101
  How it stores its outbox (its own database, its own transaction) and how it maps its chats to the ingest contract.
@@ -79,10 +106,9 @@ Everything else is here, the same for every client.
79
106
  deletions propagate, the name, mode and gender follow the platform, own content is marked, a source is learned in
80
107
  parts, listed and forgotten, recall over MCP carries the user and the conversation. Type checks there fail the build when this contract drifts from the service's schemas.
81
108
 
82
- ## Synced copy (Arkimede)
83
- Arkimede still uses a copy of the sources instead of the npm package: `scripts/sync-to.sh <dir>` copies them into a
84
- host repository with a header naming the source commit (Arkimede: `backend/src/recordare/client/`). The host never
85
- edits them; change the library here and sync again. Switching to the npm package only changes the imports.
109
+ ## CommonJS hosts
110
+ The package is ESM only. A CommonJS host (Arkimede's NestJS backend) loads it with `require`, which Node supports for
111
+ ES modules from 20.19 / 22.12; with TypeScript, `"module": "nodenext"` (5.8 or later) type-checks that import.
86
112
 
87
113
  ## Development
88
114
  `npm ci`, `npm run typecheck`, `npm test`, `npm run build` (Node ≥ 20).
package/README_it.md CHANGED
@@ -19,9 +19,9 @@ import { PersonDirectory, RecordareClient, afterFailure } from '@arkimedehq/reco
19
19
  const rc = new RecordareClient({ baseUrl: 'http://recordare:8080', apiKey: process.env.RECORDARE_API_KEY!,
20
20
  headers: () => traceHeaders() }); // es. propagation.inject di OpenTelemetry
21
21
 
22
- // Quale memoria apre l'account dell'utente, il suo modo (personal | entity) e l'indirizzo di Atlas; il nome segue il profilo sulla piattaforma. `enabled` è
22
+ // Quale memoria apre l'account dell'utente, il suo modo (personal | entity) e l'indirizzo di Atlas; il nome e il genere seguono il profilo sulla piattaforma. `enabled` è
23
23
  // l'interruttore della memoria della piattaforma: finché è false Recordare non viene contattato (niente consenso, D50).
24
- const people = new PersonDirectory(rc, { user: async (id) => ({ enabled: true, name: 'Andrea' }) });
24
+ const people = new PersonDirectory(rc, { user: async (id) => ({ enabled: true, name: 'Andrea', gender: 'masculine' }) });
25
25
  const person = await people.refresh('user-42'); // { memoryId, mode, atlasUrl }; peek() legge la cache
26
26
 
27
27
  // Ingest (diviso in richieste da 500, idempotente su ogni externalId): → { conversationId, accepted, duplicates, conflicts }.
@@ -32,8 +32,14 @@ await rc.ingest('user-42', { conversation: { externalId: 'chat-1' }, messages: [
32
32
  await rc.ingest('user-42', { conversation: { externalId: 'doc-1', source: 'document' }, messages: [
33
33
  { externalId: 'd1', role: 'user', own: true, content: 'La caldaia va revisionata ogni due anni.', sentAt: new Date().toISOString() },
34
34
  ] });
35
+ // Una trascrizione di stanza (ascolto continuo): Recordare la distribuisce alle memorie delle persone presenti.
36
+ await rc.ingestRoom({ conversation: { externalId: 'kitchen-1' }, recipients: ['user-42', 'user-7'],
37
+ speakers: [{ ref: 'a', user: 'user-42' }, { ref: 'b', user: 'user-7' }, { ref: 'c', displayName: 'Leo', identity: { channel: 'voice', externalId: 'leo' } }],
38
+ messages: [{ externalId: 'k1', speakerRef: 'c', content: 'Mi hanno preso in squadra!', sentAt: new Date().toISOString() }] });
35
39
  // Modo (solo finché la memoria è vuota, altrimenti MemoryNotEmptyError) e genere della prima persona, dal profilo.
36
40
  await rc.updateMe('user-42', { gender: 'feminine' });
41
+ // L'account viene eliminato: si cancella l'intera memoria (una chiave con lo scope erase; l'id della memoria conferma quale).
42
+ await rc.eraseMemory('user-42', (await rc.me('user-42')).memoryId);
37
43
 
38
44
  // Richiamo: una sessione MCP per utente E conversazione (prove delle scritture, il turno corrente), entrambi fissati nel codice.
39
45
  const tools = await rc.mcp.listTools('user-42', 'chat-1'); // gli strumenti dell'agente nascono da questi schemi
@@ -69,8 +75,30 @@ await rc.learnSource('user-42', { externalId: 'boiler-manual', title: 'Manuale d
69
75
 
70
76
  ## Il diario
71
77
  `episodes`, `episode`, `digests`, `facts`, `notes`, `plans` leggono ciò che Recordare ricorda, per la vista della persona
72
- nell'interfaccia della piattaforma; `correctEpisode`, `forgetEpisode`, `pinNote`, `delete`, `decide` sono le modifiche
73
- della persona (API.md §4).
78
+ nell'interfaccia della piattaforma; `correctEpisode`, `forgetEpisode`, `forgetPeriod`, `pinNote`, `delete`, `decide` sono le modifiche
79
+ della persona (API.md §4). `fact`, `note` danno un elemento con le sue prove; `addEpisode`, `addNote` sono scritti dalla
80
+ persona (dichiarati, suoi); `correctFact`, `correctNote` tengono la versione sbagliata come cronologia. `noteChanges` è il
81
+ feed delle modifiche per una piattaforma che tiene una copia delle note (si ripassa `next` come `since`). `settings`,
82
+ `usage` leggono le impostazioni della memoria e le chiamate LLM spese per essa; `updateSettings` e l'export
83
+ (`startExport` → `exportJob` → `exportArchive`) richiedono il token personale della memoria (scope `memory_settings`,
84
+ `export`; `user` vuoto).
85
+
86
+ ## Collegare l'identità di canale del titolare (D51)
87
+ In una memoria personale un mittente di un canale parla come "io" solo dopo che il titolare l'ha collegato:
88
+ `linkCode(user)` → `{code, expiresAt}` (`RCD-XXXX-XXXX`, dieci minuti, un solo uso) — il titolare invia il codice da
89
+ solo all'agente da quel canale, in una chat diretta (impostare `conversation.direct` nell'ingest quando lo si sa;
90
+ `false` = un gruppo, dove un codice non chiede nulla); poi `holderIdentities(user)` → `{linked, pending}`, e
91
+ `confirmHolderClaim(user, id)` / `rejectHolderClaim(user, id)` entro sette giorni; `unlinkHolderIdentity(user, id)`
92
+ annulla un collegamento. Recordare non salva mai il messaggio del codice, ma il modello lo vede: quando `isLinkCode(text)`
93
+ (lo stesso pattern `LINK_CODE` del servizio), inviare subito il turno e dare al modello `LINK_CODE_INSTRUCTION` al posto
94
+ del contesto di memoria.
95
+
96
+ ```ts
97
+ const { code } = await rc.linkCode('user-42'); // da mostrare al titolare
98
+ if (isLinkCode(text)) systemExtra = LINK_CODE_INSTRUCTION; // il modello dice che il codice è arrivato, senza ripeterlo
99
+ const { pending } = await rc.holderIdentities('user-42');
100
+ await rc.confirmHolderClaim('user-42', pending[0].id);
101
+ ```
74
102
 
75
103
  ## Cosa resta all'host
76
104
  Come conserva la sua outbox (il suo database, la sua transazione) e come trasforma le sue chat nel contratto di
@@ -82,11 +110,10 @@ volta sola, le cancellazioni si propagano, nome, modo e genere seguono la piatta
82
110
  fonte viene imparata a parti, elencata e dimenticata, il richiamo via MCP porta l'utente e la conversazione. I controlli di tipo lì fanno fallire la build se questo contratto si
83
111
  allontana dagli schemi del servizio.
84
112
 
85
- ## Copia sincronizzata (Arkimede)
86
- Arkimede usa ancora una copia dei sorgenti invece del pacchetto npm: `scripts/sync-to.sh <dir>` copia i sorgenti nel
87
- repository dell'host con un'intestazione che indica il commit di origine (Arkimede: `backend/src/recordare/client/`).
88
- L'host non li modifica mai; si cambia la libreria qui e si sincronizza di nuovo. Passare al pacchetto npm cambia solo
89
- gli import.
113
+ ## Host CommonJS
114
+ Il pacchetto è solo ESM. Un host CommonJS (il backend NestJS di Arkimede) lo carica con `require`, che Node supporta per
115
+ i moduli ES dalla 20.19 / 22.12; con TypeScript, `"module": "nodenext"` (5.8 o successivo) controlla i tipi di
116
+ quell'import.
90
117
 
91
118
  ## Sviluppo
92
119
  `npm ci`, `npm run typecheck`, `npm test`, `npm run build` (Node ≥ 20).
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Digest, type Episode, type EpisodeDetail, type EpisodeQuery, type Fact, type IngestRequest, type IngestResult, type LearnSource, type LearnSourceResult, type Me, type MeSettings, type MemoryContext, type Note, type PlanStatus, type Precision, type Source } from './contract.js';
1
+ import { type Cards, type ContextOptions, type Digest, type Episode, type Erased, type EpisodeDetail, type EpisodeQuery, type ExportJob, type Fact, type FactDetail, type HolderIdentities, type IngestRequest, type IngestResult, type RoomRequest, type RoomResult, type LearnSource, type LinkCode, type LearnSourceResult, type Me, type MemorySettings, type MeSettings, type MemoryContext, type Note, type NoteChange, type NoteDetail, type PlanStatus, type Precision, type Source, type Usage } from './contract.js';
2
2
  import { type ClientOptions, Http } from './http.js';
3
3
  import { RecordareMcp, type McpOptions } from './mcp.js';
4
4
  /**
@@ -16,22 +16,42 @@ export declare class RecordareClient {
16
16
  me(user: string): Promise<Me>;
17
17
  /** The memory's settings from the platform: the name (keep it in sync on every rename), the mode and the gender. */
18
18
  updateMe(user: string, settings: MeSettings): Promise<void>;
19
+ /**
20
+ * Erases the whole memory behind `user`, at once and for good (needs the `erase` scope): everything it holds goes.
21
+ * `memoryId` (from `me`) confirms which memory; a user without a memory answers 404, none is created.
22
+ */
23
+ eraseMemory(user: string, memoryId: string): Promise<Erased>;
24
+ /** A one-time code for the holder to send from a channel (D51; personal memories only, else 409). */
25
+ linkCode(user: string): Promise<LinkCode>;
26
+ /** The identities that speak as the memory's "I" and the claims waiting for confirmation (D51). */
27
+ holderIdentities(user: string): Promise<HolderIdentities>;
28
+ /** Confirms a claim: that channel identity is the holder from now on. */
29
+ confirmHolderClaim(user: string, id: string): Promise<void>;
30
+ /** Rejects a claim (not the holder). */
31
+ rejectHolderClaim(user: string, id: string): Promise<void>;
32
+ /** Unlinks one of the holder's identities. */
33
+ unlinkHolderIdentity(user: string, id: string): Promise<void>;
19
34
  /**
20
35
  * Sends messages of one conversation, split into requests of at most MAX_MESSAGES_PER_REQUEST. Idempotent: Recordare
21
36
  * deduplicates on each message's externalId. Recordare always stores (D50): the on/off switch is the platform's.
22
37
  */
23
38
  ingest(user: string, req: IngestRequest): Promise<IngestResult>;
39
+ /**
40
+ * Sends a room transcript to the memories of the people present (continuous listening), split into requests of at
41
+ * most MAX_MESSAGES_PER_REQUEST; needs a client key (not a personal token). Idempotent like `ingest`.
42
+ */
43
+ ingestRoom(req: RoomRequest): Promise<RoomResult>;
24
44
  /**
25
45
  * The memories relevant to `query` (the message about to be answered) as a fenced block to append to the prompt — or
26
46
  * null. Append it at the end of the system prompt (it changes every turn: keep the stable part first for prompt
27
- * caching); never store it as a chat message.
47
+ * caching); never store it as a chat message. `card`: open it with the self card (who I am), off by default.
28
48
  */
29
- context(user: string, conversation: string, query: string): Promise<MemoryContext>;
49
+ context(user: string, conversation: string, query: string, opts?: ContextOptions): Promise<MemoryContext>;
30
50
  /**
31
51
  * Stores the turn and returns the memory context for it in one round trip (needs the `ingest` and `read` scopes):
32
52
  * the context is computed for the ingested conversation, from `query` or else the turn's last user message.
33
53
  */
34
- contextWithTurn(user: string, turn: IngestRequest, query?: string): Promise<MemoryContext>;
54
+ contextWithTurn(user: string, turn: IngestRequest, query?: string, opts?: ContextOptions): Promise<MemoryContext>;
35
55
  /** The conversation ended (session closed, /new): extraction runs now. A conversation Recordare never had is done. */
36
56
  endConversation(user: string, conversation: string): Promise<void>;
37
57
  /** An edited message: Recordare re-extracts what depended on it. */
@@ -63,6 +83,20 @@ export declare class RecordareClient {
63
83
  }): Promise<string>;
64
84
  /** The person forgets an episode: it is removed and never recreated. */
65
85
  forgetEpisode(user: string, id: string): Promise<void>;
86
+ /**
87
+ * The person forgets a period (days or months, inclusive): its episodes and raw messages (`keepRaw`: kept but hidden
88
+ * and never extracted again), with what only they supported. Returns how much went.
89
+ */
90
+ forgetPeriod(user: string, period: {
91
+ from: string;
92
+ to: string;
93
+ keepRaw?: boolean;
94
+ }): Promise<{
95
+ episodes: number;
96
+ messages: number;
97
+ facts: number;
98
+ notes: number;
99
+ }>;
66
100
  digests(user: string, query?: {
67
101
  level?: 'day' | 'month';
68
102
  from?: string;
@@ -73,6 +107,8 @@ export declare class RecordareClient {
73
107
  asOf?: string;
74
108
  includePending?: boolean;
75
109
  }): Promise<Fact[]>;
110
+ /** Who I am and who my contacts are: current facts and notes of the self and of each contact (no LLM). */
111
+ cards(user: string): Promise<Cards>;
76
112
  notes(user: string, query?: {
77
113
  category?: Note['category'];
78
114
  pinned?: boolean;
@@ -87,6 +123,60 @@ export declare class RecordareClient {
87
123
  delete(user: string, what: 'notes' | 'facts', id: string): Promise<void>;
88
124
  /** A pending (inferred) note or fact: confirmed it becomes the person's, rejected it is removed. */
89
125
  decide(user: string, what: 'notes' | 'facts', id: string, decision: 'confirm' | 'reject'): Promise<void>;
126
+ /** The person writes an episode in the platform's UI (stated, theirs). Returns its id. */
127
+ addEpisode(user: string, episode: {
128
+ content: string;
129
+ kind?: 'event' | 'plan';
130
+ occurredAt?: string;
131
+ occurredUntil?: string;
132
+ datePrecision?: Exclude<Precision, 'unknown' | 'minute'>;
133
+ place?: string;
134
+ people?: string[];
135
+ }): Promise<string>;
136
+ /** The person writes a note in the platform's UI (stated, theirs). Returns its id. */
137
+ addNote(user: string, note: {
138
+ content: string;
139
+ category?: Note['category'];
140
+ }): Promise<string>;
141
+ fact(user: string, id: string): Promise<FactDetail>;
142
+ note(user: string, id: string): Promise<NoteDetail>;
143
+ /** The person corrects a current fact (`value` null: not known any more); the wrong one stays in its history. */
144
+ correctFact(user: string, id: string, fix: {
145
+ value: string | null;
146
+ validFrom?: string;
147
+ datePrecision?: Exclude<Precision, 'unknown' | 'minute'>;
148
+ }): Promise<string>;
149
+ /** The person corrects a current note; the wrong one stays as `corrected`. */
150
+ correctNote(user: string, id: string, fix: {
151
+ content: string;
152
+ category?: Note['category'];
153
+ }): Promise<string>;
154
+ /** The notes change feed, for a platform keeping a copy: pass `next` back as `since`; `more` when the page was full. */
155
+ noteChanges(user: string, query?: {
156
+ since?: number;
157
+ limit?: number;
158
+ }): Promise<{
159
+ items: NoteChange[];
160
+ next: number;
161
+ more: boolean;
162
+ }>;
163
+ settings(user: string): Promise<MemorySettings>;
164
+ /** Needs `memory_settings`: the memory's personal token (never a client key); pass `user` empty with it. */
165
+ updateSettings(user: string, patch: {
166
+ locale?: string;
167
+ timezone?: string;
168
+ qualityProfile?: MemorySettings['qualityProfile'];
169
+ }): Promise<MemorySettings>;
170
+ /** LLM calls and tokens spent on the memory between two local days (default: the last 30). */
171
+ usage(user: string, query?: {
172
+ from?: string;
173
+ to?: string;
174
+ }): Promise<Usage>;
175
+ /** Starts a full export (needs `export`: the memory's personal token); poll `exportJob`, then `exportArchive`. */
176
+ startExport(user: string): Promise<ExportJob>;
177
+ exportJob(user: string, id: string): Promise<ExportJob>;
178
+ /** The finished export: one JSON archive (`format: "recordare-export/1"`). */
179
+ exportArchive(user: string, id: string): Promise<Record<string, unknown>>;
90
180
  /** Closes the MCP sessions (on shutdown). */
91
181
  close(): Promise<void>;
92
182
  private gone;
package/dist/client.js CHANGED
@@ -41,6 +41,33 @@ export class RecordareClient {
41
41
  throw err;
42
42
  }
43
43
  }
44
+ /**
45
+ * Erases the whole memory behind `user`, at once and for good (needs the `erase` scope): everything it holds goes.
46
+ * `memoryId` (from `me`) confirms which memory; a user without a memory answers 404, none is created.
47
+ */
48
+ eraseMemory(user, memoryId) {
49
+ return this.http.request('DELETE', `api/v1/me${qs({ confirm: memoryId })}`, { user });
50
+ }
51
+ /** A one-time code for the holder to send from a channel (D51; personal memories only, else 409). */
52
+ linkCode(user) {
53
+ return this.http.request('POST', 'api/v1/me/link-codes', { user });
54
+ }
55
+ /** The identities that speak as the memory's "I" and the claims waiting for confirmation (D51). */
56
+ holderIdentities(user) {
57
+ return this.http.request('GET', 'api/v1/me/identities', { user });
58
+ }
59
+ /** Confirms a claim: that channel identity is the holder from now on. */
60
+ async confirmHolderClaim(user, id) {
61
+ await this.http.request('POST', `api/v1/me/identities/pending/${encodeURIComponent(id)}/confirm`, { user });
62
+ }
63
+ /** Rejects a claim (not the holder). */
64
+ async rejectHolderClaim(user, id) {
65
+ await this.http.request('DELETE', `api/v1/me/identities/pending/${encodeURIComponent(id)}`, { user });
66
+ }
67
+ /** Unlinks one of the holder's identities. */
68
+ async unlinkHolderIdentity(user, id) {
69
+ await this.http.request('DELETE', `api/v1/me/identities/${encodeURIComponent(id)}`, { user });
70
+ }
44
71
  /**
45
72
  * Sends messages of one conversation, split into requests of at most MAX_MESSAGES_PER_REQUEST. Idempotent: Recordare
46
73
  * deduplicates on each message's externalId. Recordare always stores (D50): the on/off switch is the platform's.
@@ -65,20 +92,45 @@ export class RecordareClient {
65
92
  }
66
93
  return total;
67
94
  }
95
+ /**
96
+ * Sends a room transcript to the memories of the people present (continuous listening), split into requests of at
97
+ * most MAX_MESSAGES_PER_REQUEST; needs a client key (not a personal token). Idempotent like `ingest`.
98
+ */
99
+ async ingestRoom(req) {
100
+ const total = new Map();
101
+ for (let i = 0; i < req.messages.length; i += MAX_MESSAGES_PER_REQUEST) {
102
+ const last = i + MAX_MESSAGES_PER_REQUEST >= req.messages.length;
103
+ const part = await this.http.request('POST', 'api/v1/ingest/room', {
104
+ body: { ...req, messages: req.messages.slice(i, i + MAX_MESSAGES_PER_REQUEST), hints: req.hints && last ? req.hints : undefined },
105
+ });
106
+ for (const r of part.recipients) {
107
+ const t = total.get(r.user);
108
+ if (!t)
109
+ total.set(r.user, { ...r, conflicts: [...r.conflicts] });
110
+ else {
111
+ t.conversationId = r.conversationId ?? t.conversationId;
112
+ t.accepted += r.accepted;
113
+ t.duplicates += r.duplicates;
114
+ t.conflicts.push(...r.conflicts);
115
+ }
116
+ }
117
+ }
118
+ return { recipients: [...total.values()] };
119
+ }
68
120
  /**
69
121
  * The memories relevant to `query` (the message about to be answered) as a fenced block to append to the prompt — or
70
122
  * null. Append it at the end of the system prompt (it changes every turn: keep the stable part first for prompt
71
- * caching); never store it as a chat message.
123
+ * caching); never store it as a chat message. `card`: open it with the self card (who I am), off by default.
72
124
  */
73
- context(user, conversation, query) {
74
- return this.http.request('POST', 'api/v1/context', { user, body: { query }, headers: { [CONVERSATION_HEADER]: conversation } });
125
+ context(user, conversation, query, opts = {}) {
126
+ return this.http.request('POST', 'api/v1/context', { user, body: { query, ...opts }, headers: { [CONVERSATION_HEADER]: conversation } });
75
127
  }
76
128
  /**
77
129
  * Stores the turn and returns the memory context for it in one round trip (needs the `ingest` and `read` scopes):
78
130
  * the context is computed for the ingested conversation, from `query` or else the turn's last user message.
79
131
  */
80
- contextWithTurn(user, turn, query) {
81
- return this.http.request('POST', 'api/v1/context', { user, body: { ingest: turn, ...(query ? { query } : {}) } });
132
+ contextWithTurn(user, turn, query, opts = {}) {
133
+ return this.http.request('POST', 'api/v1/context', { user, body: { ingest: turn, ...(query ? { query } : {}), ...opts } });
82
134
  }
83
135
  /** The conversation ended (session closed, /new): extraction runs now. A conversation Recordare never had is done. */
84
136
  async endConversation(user, conversation) {
@@ -135,12 +187,23 @@ export class RecordareClient {
135
187
  async forgetEpisode(user, id) {
136
188
  await this.http.request('DELETE', `api/v1/episodes/${enc(id)}`, { user });
137
189
  }
190
+ /**
191
+ * The person forgets a period (days or months, inclusive): its episodes and raw messages (`keepRaw`: kept but hidden
192
+ * and never extracted again), with what only they supported. Returns how much went.
193
+ */
194
+ forgetPeriod(user, period) {
195
+ return this.http.request('POST', 'api/v1/forget', { user, body: period });
196
+ }
138
197
  digests(user, query = {}) {
139
198
  return this.http.request('GET', `api/v1/digests${qs(query)}`, { user });
140
199
  }
141
200
  facts(user, query = {}) {
142
201
  return this.http.request('GET', `api/v1/facts${qs(query)}`, { user });
143
202
  }
203
+ /** Who I am and who my contacts are: current facts and notes of the self and of each contact (no LLM). */
204
+ cards(user) {
205
+ return this.http.request('GET', 'api/v1/cards', { user });
206
+ }
144
207
  notes(user, query = {}) {
145
208
  return this.http.request('GET', `api/v1/notes${qs(query)}`, { user });
146
209
  }
@@ -159,6 +222,54 @@ export class RecordareClient {
159
222
  async decide(user, what, id, decision) {
160
223
  await this.http.request('POST', `api/v1/${what}/${enc(id)}/${decision}`, { user });
161
224
  }
225
+ /** The person writes an episode in the platform's UI (stated, theirs). Returns its id. */
226
+ async addEpisode(user, episode) {
227
+ return (await this.http.request('POST', 'api/v1/episodes', { user, body: episode })).id;
228
+ }
229
+ /** The person writes a note in the platform's UI (stated, theirs). Returns its id. */
230
+ async addNote(user, note) {
231
+ return (await this.http.request('POST', 'api/v1/notes', { user, body: note })).id;
232
+ }
233
+ fact(user, id) {
234
+ return this.http.request('GET', `api/v1/facts/${enc(id)}`, { user });
235
+ }
236
+ note(user, id) {
237
+ return this.http.request('GET', `api/v1/notes/${enc(id)}`, { user });
238
+ }
239
+ /** The person corrects a current fact (`value` null: not known any more); the wrong one stays in its history. */
240
+ async correctFact(user, id, fix) {
241
+ return (await this.http.request('POST', `api/v1/facts/${enc(id)}/corrections`, { user, body: fix })).id;
242
+ }
243
+ /** The person corrects a current note; the wrong one stays as `corrected`. */
244
+ async correctNote(user, id, fix) {
245
+ return (await this.http.request('POST', `api/v1/notes/${enc(id)}/corrections`, { user, body: fix })).id;
246
+ }
247
+ /** The notes change feed, for a platform keeping a copy: pass `next` back as `since`; `more` when the page was full. */
248
+ noteChanges(user, query = {}) {
249
+ return this.http.request('GET', `api/v1/notes/changes${qs(query)}`, { user });
250
+ }
251
+ settings(user) {
252
+ return this.http.request('GET', 'api/v1/settings', { user });
253
+ }
254
+ /** Needs `memory_settings`: the memory's personal token (never a client key); pass `user` empty with it. */
255
+ updateSettings(user, patch) {
256
+ return this.http.request('PATCH', 'api/v1/settings', { user, body: patch });
257
+ }
258
+ /** LLM calls and tokens spent on the memory between two local days (default: the last 30). */
259
+ usage(user, query = {}) {
260
+ return this.http.request('GET', `api/v1/usage${qs(query)}`, { user });
261
+ }
262
+ /** Starts a full export (needs `export`: the memory's personal token); poll `exportJob`, then `exportArchive`. */
263
+ startExport(user) {
264
+ return this.http.request('POST', 'api/v1/exports', { user });
265
+ }
266
+ exportJob(user, id) {
267
+ return this.http.request('GET', `api/v1/exports/${enc(id)}`, { user });
268
+ }
269
+ /** The finished export: one JSON archive (`format: "recordare-export/1"`). */
270
+ exportArchive(user, id) {
271
+ return this.http.request('GET', `api/v1/exports/${enc(id)}/archive`, { user });
272
+ }
162
273
  /** Closes the MCP sessions (on shutdown). */
163
274
  close() {
164
275
  return this.mcp.close();