@tpsdev-ai/flair-client 0.17.0 → 0.19.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/dist/client.d.ts CHANGED
@@ -39,20 +39,56 @@ export declare class FlairClient {
39
39
  declare class MemoryApi {
40
40
  private client;
41
41
  constructor(client: FlairClient);
42
- /** Write a memory. Optionally checks for near-duplicates before writing. */
42
+ /**
43
+ * Write a memory. NEVER suppresses the write — the record is always
44
+ * created. `dedup`/`dedupThreshold` are passthrough HINTS forwarded to the
45
+ * server, which runs a conservative (cosine + lexical) near-duplicate check
46
+ * and, when a match is found, attaches a collision signal to the response
47
+ * (`deduplicated`, `matchedId`, `matchConfidence`) instead of dropping the
48
+ * new content. (Historical note: a near-duplicate previously short-circuited
49
+ * this method to return the EXISTING record without writing — that silently
50
+ * dropped distinct-but-similar content, e.g. flair#526. The check now lives
51
+ * server-side in Memory.put()/Memory.post() and never suppresses a write.)
52
+ */
43
53
  write(content: string, opts?: {
44
54
  id?: string;
45
55
  type?: MemoryType;
46
56
  durability?: Durability;
47
57
  tags?: string[];
48
58
  subject?: string;
49
- /** Check for similar existing memories before writing. If a near-duplicate
50
- * is found (score >= threshold), returns it instead of creating a new one.
51
- * Default: false (no dedup check). */
59
+ /** Ask the server to run its conservative near-duplicate check for this
60
+ * write and report a collision signal if found. Does NOT suppress the
61
+ * write either way. Default: false (server still applies its own
62
+ * default gate — this only requests the signal be computed/reported;
63
+ * see dedupThreshold to tune it). */
52
64
  dedup?: boolean;
53
- /** Similarity threshold for dedup. Default: 0.95 */
65
+ /** Cosine-similarity threshold hint for the server's dedup gate.
66
+ * Default (server-side): 0.95 */
54
67
  dedupThreshold?: number;
55
68
  }): Promise<Memory>;
69
+ /**
70
+ * Update an existing memory by id. Dedup-BYPASSED (this IS the intentional
71
+ * overwrite/version path, not an ambiguous new write) — always writes.
72
+ * Auth: the caller must own the memory (enforced server-side by the SAME
73
+ * ownership check Memory.put()/Memory.post() already run — no parallel
74
+ * check here).
75
+ *
76
+ * Default (`preserveHistory` unset/false): same-id overwrite via a
77
+ * full-record PUT. Harper's PUT is FULL RECORD REPLACEMENT, so we read the
78
+ * existing record first and merge the new content on top (never a bare
79
+ * partial). The stale embedding is cleared so the server's existing
80
+ * "generate embedding if missing" step recomputes it for the new content.
81
+ *
82
+ * `opts.preserveHistory: true`: write a NEW id with `supersedes: id`; the
83
+ * server closes the old record's `validTo` afterward, write-new BEFORE
84
+ * close-old (safe failure = two active records, never a lost write). If the
85
+ * old record is owned by a DIFFERENT agent, the server requires a "write"
86
+ * MemoryGrant from that owner — otherwise it denies the request (cross-agent
87
+ * write).
88
+ */
89
+ update(id: string, content: string, opts?: {
90
+ preserveHistory?: boolean;
91
+ }): Promise<Memory>;
56
92
  /** Search memories by meaning. Optionally filter to facts valid at a specific point in time. */
57
93
  search(query: string, opts?: {
58
94
  limit?: number;
package/dist/client.js CHANGED
@@ -106,23 +106,18 @@ class MemoryApi {
106
106
  constructor(client) {
107
107
  this.client = client;
108
108
  }
109
- /** Write a memory. Optionally checks for near-duplicates before writing. */
109
+ /**
110
+ * Write a memory. NEVER suppresses the write — the record is always
111
+ * created. `dedup`/`dedupThreshold` are passthrough HINTS forwarded to the
112
+ * server, which runs a conservative (cosine + lexical) near-duplicate check
113
+ * and, when a match is found, attaches a collision signal to the response
114
+ * (`deduplicated`, `matchedId`, `matchConfidence`) instead of dropping the
115
+ * new content. (Historical note: a near-duplicate previously short-circuited
116
+ * this method to return the EXISTING record without writing — that silently
117
+ * dropped distinct-but-similar content, e.g. flair#526. The check now lives
118
+ * server-side in Memory.put()/Memory.post() and never suppresses a write.)
119
+ */
110
120
  async write(content, opts = {}) {
111
- // Near-duplicate check — skip for very short content where similarity
112
- // is unreliable (e.g., "ok", "thanks" would match each other)
113
- if (opts.dedup && content.length >= 20) {
114
- const threshold = opts.dedupThreshold ?? 0.95;
115
- // Use raw scoring to avoid retrieval-boost feedback loop where repeated
116
- // dedup checks inflate scores above the threshold.
117
- const existing = await this.search(content, { limit: 1, minScore: threshold, scoring: "raw" });
118
- if (existing.length > 0) {
119
- // Return the existing memory instead of creating a duplicate.
120
- // Flag deduped so callers know this write was suppressed.
121
- const match = await this.get(existing[0].id);
122
- if (match)
123
- return { ...match, deduped: true };
124
- }
125
- }
126
121
  const id = opts.id ?? `${this.client.agentId}-${crypto.randomUUID()}`;
127
122
  const record = {
128
123
  id,
@@ -134,9 +129,79 @@ class MemoryApi {
134
129
  subject: opts.subject,
135
130
  createdAt: new Date().toISOString(),
136
131
  };
137
- await this.client.request("PUT", `/Memory/${id}`, record);
138
- // Harper PUT returns {} — return the record we constructed
139
- return record;
132
+ // Passthrough hints — the server strips these before persisting; they are
133
+ // never stored on the record itself.
134
+ if (opts.dedup !== undefined)
135
+ record.dedup = opts.dedup;
136
+ if (opts.dedupThreshold !== undefined)
137
+ record.dedupThreshold = opts.dedupThreshold;
138
+ const response = await this.client.request("PUT", `/Memory/${id}`, record);
139
+ // Merge the server response (deduplicated/matchedId/matchConfidence/
140
+ // written, plus any echoed fields) over the locally-constructed record —
141
+ // the write always happens, so `record` always reflects what was sent,
142
+ // and the server's signal fields (if any) always come through.
143
+ return { ...record, ...(response ?? {}) };
144
+ }
145
+ /**
146
+ * Update an existing memory by id. Dedup-BYPASSED (this IS the intentional
147
+ * overwrite/version path, not an ambiguous new write) — always writes.
148
+ * Auth: the caller must own the memory (enforced server-side by the SAME
149
+ * ownership check Memory.put()/Memory.post() already run — no parallel
150
+ * check here).
151
+ *
152
+ * Default (`preserveHistory` unset/false): same-id overwrite via a
153
+ * full-record PUT. Harper's PUT is FULL RECORD REPLACEMENT, so we read the
154
+ * existing record first and merge the new content on top (never a bare
155
+ * partial). The stale embedding is cleared so the server's existing
156
+ * "generate embedding if missing" step recomputes it for the new content.
157
+ *
158
+ * `opts.preserveHistory: true`: write a NEW id with `supersedes: id`; the
159
+ * server closes the old record's `validTo` afterward, write-new BEFORE
160
+ * close-old (safe failure = two active records, never a lost write). If the
161
+ * old record is owned by a DIFFERENT agent, the server requires a "write"
162
+ * MemoryGrant from that owner — otherwise it denies the request (cross-agent
163
+ * write).
164
+ */
165
+ async update(id, content, opts = {}) {
166
+ const existing = await this.get(id);
167
+ if (!existing) {
168
+ throw new FlairError("PUT", `/Memory/${id}`, 404, `memory ${id} not found`);
169
+ }
170
+ if (opts.preserveHistory) {
171
+ const newId = `${this.client.agentId}-${crypto.randomUUID()}`;
172
+ const record = {
173
+ ...existing,
174
+ id: newId,
175
+ content,
176
+ supersedes: id,
177
+ createdAt: new Date().toISOString(),
178
+ };
179
+ delete record.updatedAt;
180
+ delete record.embedding;
181
+ delete record.embeddingModel;
182
+ delete record.validFrom;
183
+ delete record.validTo;
184
+ delete record.archivedAt;
185
+ delete record.deduped;
186
+ // The Memory schema does not expose a working HTTP POST route (see
187
+ // resources/Memory.ts) — Memory.post() is only reachable in-process
188
+ // (resources/mcp-tools.ts). So the supersede-link write goes through
189
+ // the same PUT-with-explicit-(new)-id path as a normal create; the
190
+ // server's Memory.put() validates/authorizes `supersedes`, writes this
191
+ // new record, then closes the old one transactionally (write-new
192
+ // BEFORE close-old — see resources/Memory.ts's closeSupersededIfNeeded).
193
+ // `supersedes` being set also makes the server bypass the dedup gate
194
+ // for this write (it's an intentional version link, not an ambiguous
195
+ // new write).
196
+ const response = await this.client.request("PUT", `/Memory/${newId}`, record);
197
+ return { ...record, ...(response ?? {}) };
198
+ }
199
+ const merged = { ...existing, content, updatedAt: new Date().toISOString() };
200
+ delete merged.embedding;
201
+ delete merged.embeddingModel;
202
+ delete merged.deduped;
203
+ const response = await this.client.request("PUT", `/Memory/${id}`, merged);
204
+ return { ...merged, id, ...(response ?? {}) };
140
205
  }
141
206
  /** Search memories by meaning. Optionally filter to facts valid at a specific point in time. */
142
207
  async search(query, opts = {}) {
package/dist/types.d.ts CHANGED
@@ -15,8 +15,29 @@ export interface Memory {
15
15
  subject?: string;
16
16
  createdAt: string;
17
17
  updatedAt?: string;
18
- /** Set to true when write() returned an existing near-duplicate instead of
19
- * creating a new entry. Omitted/undefined for new writes. */
18
+ /** Always true after a successful write()/update() the server never
19
+ * suppresses a write, so this is never false/absent on a real response. */
20
+ written?: boolean;
21
+ /** True when the server's conservative dedup gate found a near-duplicate.
22
+ * The new content was ALWAYS written regardless — this is a signal, not a
23
+ * suppression flag. See `matchedId` / `matchConfidence`. */
24
+ deduplicated?: boolean;
25
+ /** The id of the existing memory the server's dedup gate matched against,
26
+ * when `deduplicated` is true. */
27
+ matchedId?: string;
28
+ /** Confidence pair for the `matchedId` collision: raw cosine similarity and
29
+ * Jaccard token-overlap against the new content, both in [0, 1]. */
30
+ matchConfidence?: {
31
+ cosine: number;
32
+ lexical: number;
33
+ };
34
+ /**
35
+ * @deprecated Historical field from the pre-fix client-side dedup gate,
36
+ * which suppressed the write and returned the EXISTING record instead
37
+ * (silently dropping distinct-but-similar content — flair#526). The gate is
38
+ * now server-side and NEVER suppresses a write; use `deduplicated` instead.
39
+ * No longer set by write()/update().
40
+ */
20
41
  deduped?: boolean;
21
42
  }
22
43
  /** A soul entry (persistent personality/values). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-client",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Lightweight client for Flair — identity, memory, and soul for AI agents. Zero heavy dependencies.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",