@chatpanel/events 0.102.0 → 0.104.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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/memory.js
CHANGED
|
@@ -1,638 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* ChatPanel remembers conversations, meetings and notes, and can search all of them. What it
|
|
5
|
-
* could not do is KNOW anything. Told "call me Alex, and never open with a preamble", the next
|
|
6
|
-
* turn — let alone the next agent, or Codex over MCP — started from zero, because the only way
|
|
7
|
-
* a fact survived was for someone to search for it, and nobody searches for their own name.
|
|
8
|
-
*
|
|
9
|
-
* Memory is the opposite of history by design. History is large, retrieved, and about events.
|
|
10
|
-
* Memory is SMALL, ambient, and about standing truths — so it can be carried into every turn
|
|
11
|
-
* instead of looked up, and so a person can read the whole of it in one screen and correct it.
|
|
12
|
-
* That size limit is the feature: a memory that grows without bound becomes a second corpus,
|
|
13
|
-
* and a second corpus needs search, and then nothing is ambient any more.
|
|
14
|
-
*
|
|
15
|
-
* Four properties make it work the same everywhere:
|
|
16
|
-
*
|
|
17
|
-
* ONE VOCABULARY. Five kinds, closed set. `identity` and `preference` are AMBIENT — they
|
|
18
|
-
* apply to every turn, so they are carried unconditionally. `project`, `fact` and
|
|
19
|
-
* `reference` are RETRIEVED, because "the staging cluster is in Frankfurt" is only worth
|
|
20
|
-
* tokens on a turn that mentions it. One rule decides which, in `recall`, so the extension,
|
|
21
|
-
* the gateway and a future mobile client cannot disagree about what the model was told.
|
|
22
|
-
*
|
|
23
|
-
* CAPTURE IS DETERMINISTIC. `candidatesFrom` reads a string. No model call, no network, no
|
|
24
|
-
* latency, on every user message — which is the only reason it can run on every user
|
|
25
|
-
* message. It separates what the user COMMANDED ("remember that…") from what they merely
|
|
26
|
-
* REVEALED ("I prefer…"), because those deserve different answers: the first is consent,
|
|
27
|
-
* the second is a guess, and a guess must be offered rather than acted on.
|
|
28
|
-
*
|
|
29
|
-
* WRITES RECONCILE, THEY DO NOT ACCUMULATE. Saying "call me Alex" twice must leave one
|
|
30
|
-
* memory, and saying "actually, call me Sam" must leave one memory with the new value and
|
|
31
|
-
* the old one recoverable. `reconcile` decides create/update/duplicate against what is
|
|
32
|
-
* already stored; a client that just pushes rows produces a list nobody can read by week
|
|
33
|
-
* two.
|
|
34
|
-
*
|
|
35
|
-
* THE PROMPT FORM IS SHARED. `memoryBlock` is the one rendering. If the extension wrote its
|
|
36
|
-
* own the model would be told something subtly different depending on which surface the
|
|
37
|
-
* user typed into, and the bug would be invisible.
|
|
38
|
-
*
|
|
39
|
-
* Class R: pure, dependency-free, no I/O. Persistence is the host's (chrome.storage in the
|
|
40
|
-
* extension, an encrypted file in the gateway) — the same split as `store.js`.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
export const MEMORY_VERSION = 1;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The closed vocabulary. Deliberately five: enough that a memory's kind tells you how to
|
|
47
|
-
* treat it, few enough that a person choosing one does not have to think.
|
|
48
|
-
*
|
|
49
|
-
* `ambient` is the load-bearing bit — it is not a label, it is the retrieval rule.
|
|
50
|
-
*/
|
|
51
|
-
export const MEMORY_KINDS = Object.freeze({
|
|
52
|
-
identity: { label: 'Identity', ambient: true, hint: 'Who the user is — name, role, pronouns, language, timezone.' },
|
|
53
|
-
preference: { label: 'Preference', ambient: true, hint: 'How they want things done — tone, format, defaults, things never to do.' },
|
|
54
|
-
project: { label: 'Project', ambient: false, hint: 'Ongoing work, goals and constraints not derivable from the material itself.' },
|
|
55
|
-
fact: { label: 'Fact', ambient: false, hint: 'A durable fact about their world — systems, teams, environments, conventions.' },
|
|
56
|
-
reference: { label: 'Reference', ambient: false, hint: 'A pointer to something external — a URL, a dashboard, a ticket, a doc.' },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
export const MEMORY_KIND_NAMES = Object.freeze(Object.keys(MEMORY_KINDS));
|
|
60
|
-
|
|
61
|
-
/** Kinds carried on every turn regardless of what was said. */
|
|
62
|
-
export const AMBIENT_KINDS = Object.freeze(MEMORY_KIND_NAMES.filter((k) => MEMORY_KINDS[k].ambient));
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Bounds. A memory longer than this is a note, and there is already a notes feature; a store
|
|
66
|
-
* larger than this is a corpus, and there is already a history feature. Both limits exist to
|
|
67
|
-
* stop memory turning into the thing next to it.
|
|
68
|
-
*/
|
|
69
|
-
export const MAX_MEMORY_CHARS = 280;
|
|
70
|
-
export const MIN_MEMORY_CHARS = 3;
|
|
71
|
-
export const DEFAULT_MAX_MEMORIES = 200;
|
|
72
|
-
/** Prompt budget for the injected block. ~100 tokens; enough for a readable standing brief. */
|
|
73
|
-
export const DEFAULT_BLOCK_CHARS = 1200;
|
|
74
|
-
|
|
75
|
-
export class MemoryError extends Error {
|
|
76
|
-
constructor(message) { super(message); this.name = 'MemoryError'; }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// --------------------------------------------------------------------------
|
|
80
|
-
// The record
|
|
81
|
-
// --------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Normalize anything memory-shaped into the canonical record. Throws MemoryError on input
|
|
85
|
-
* that cannot be a memory — callers get one validation path rather than each inventing
|
|
86
|
-
* their own defaults.
|
|
87
|
-
*
|
|
88
|
-
* `now` and `newId` are injected for the same reason they are in `event.js`: a pure module
|
|
89
|
-
* that reads the clock cannot be replayed or tested.
|
|
90
|
-
*
|
|
91
|
-
* @returns {{
|
|
92
|
-
* id: string, v: number, text: string, kind: string, scope: string, key: string,
|
|
93
|
-
* tags: string[], pinned: boolean, source: object, confidence: number,
|
|
94
|
-
* createdAt: number, updatedAt: number, usedAt: number, useCount: number,
|
|
95
|
-
* expiresAt: number|null, history: {text: string, at: number}[]
|
|
96
|
-
* }}
|
|
97
|
-
*/
|
|
98
|
-
export function normalizeMemory(input = {}, { now = 0, newId = null } = {}) {
|
|
99
|
-
const text = collapse(input.text);
|
|
100
|
-
if (text.length < MIN_MEMORY_CHARS) throw new MemoryError('a memory needs text');
|
|
101
|
-
if (text.length > MAX_MEMORY_CHARS) {
|
|
102
|
-
throw new MemoryError(`a memory must be at most ${MAX_MEMORY_CHARS} characters — save longer material as a note`);
|
|
103
|
-
}
|
|
104
|
-
const kind = MEMORY_KINDS[input.kind] ? input.kind : 'fact';
|
|
105
|
-
const at = Number(input.createdAt) || Number(now) || 0;
|
|
106
|
-
return {
|
|
107
|
-
id: String(input.id || (newId ? newId() : '') || ''),
|
|
108
|
-
v: MEMORY_VERSION,
|
|
109
|
-
text,
|
|
110
|
-
kind,
|
|
111
|
-
// Free-form so a client can scope to an agent, a workspace or a site without this module
|
|
112
|
-
// enumerating surfaces it cannot know about. 'global' means every turn everywhere.
|
|
113
|
-
scope: String(input.scope || 'global'),
|
|
114
|
-
key: memoryKey(text),
|
|
115
|
-
// Derived, not asked for — see slotOf. An explicit slot still wins.
|
|
116
|
-
slot: collapse(input.slot).toLowerCase() || slotOf(text),
|
|
117
|
-
tags: [...new Set((input.tags || []).map((t) => collapse(t).toLowerCase()).filter(Boolean))].slice(0, 8),
|
|
118
|
-
pinned: !!input.pinned,
|
|
119
|
-
// WHERE IT CAME FROM, always. A memory the user cannot trace is a memory they cannot
|
|
120
|
-
// trust, and the first thing anyone asks of a wrong one is "when did I say that".
|
|
121
|
-
source: {
|
|
122
|
-
via: String(input.source?.via || 'user'), // user | agent | import | mcp
|
|
123
|
-
surface: String(input.source?.surface || ''), // chat | notes | meeting | mcp | settings
|
|
124
|
-
ref: String(input.source?.ref || ''), // conversation/meeting/note id
|
|
125
|
-
agent: String(input.source?.agent || ''), // which model or CLI proposed it
|
|
126
|
-
},
|
|
127
|
-
// How sure the CAPTURE was, not how true the fact is. An explicit command is 1.
|
|
128
|
-
confidence: clamp01(input.confidence == null ? 1 : Number(input.confidence)),
|
|
129
|
-
createdAt: at,
|
|
130
|
-
updatedAt: Number(input.updatedAt) || at,
|
|
131
|
-
usedAt: Number(input.usedAt) || 0,
|
|
132
|
-
useCount: Math.max(0, Math.floor(Number(input.useCount) || 0)),
|
|
133
|
-
expiresAt: input.expiresAt ? Number(input.expiresAt) : null,
|
|
134
|
-
// Bounded supersession ledger — "you used to say X" without an unbounded audit log.
|
|
135
|
-
history: (input.history || []).slice(-5).map((h) => ({ text: collapse(h.text), at: Number(h.at) || 0 })),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** True when `rec` is a well-formed memory. Never throws — for filtering a loaded store. */
|
|
140
|
-
export function isValidMemory(rec) {
|
|
141
|
-
try { normalizeMemory(rec, { now: rec?.createdAt || 1 }); return !!rec?.id; } catch { return false; }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* The identity of a FACT rather than of a record — two phrasings of the same standing truth
|
|
146
|
-
* should collide here so `reconcile` can supersede rather than accumulate.
|
|
147
|
-
*
|
|
148
|
-
* Lowercased, stripped of punctuation and of the framing words people vary freely ("I always
|
|
149
|
-
* prefer" / "prefer"), then the remaining words sorted and deduped. Sorting is safe precisely
|
|
150
|
-
* because this is a dedup key and never a display value: "deploy on fridays" and "on fridays,
|
|
151
|
-
* deploy" are the same standing fact, and treating them as two is the failure this prevents.
|
|
152
|
-
*/
|
|
153
|
-
export function memoryKey(text) {
|
|
154
|
-
const words = collapse(text)
|
|
155
|
-
.toLowerCase()
|
|
156
|
-
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
157
|
-
.split(/\s+/)
|
|
158
|
-
.filter((w) => w && !KEY_STOPWORDS.has(w));
|
|
159
|
-
return [...new Set(words)].sort().join(' ');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const KEY_STOPWORDS = new Set([
|
|
163
|
-
'a', 'an', 'the', 'i', 'im', 'me', 'my', 'mine', 'we', 'our', 'us', 'you', 'your',
|
|
164
|
-
'is', 'am', 'are', 'was', 'were', 'be', 'been', 'being', 'do', 'does', 'did',
|
|
165
|
-
'to', 'of', 'in', 'on', 'at', 'for', 'with', 'and', 'or', 'that', 'this', 'it',
|
|
166
|
-
'please', 'always', 'usually', 'generally', 'really', 'just', 'very', 'so',
|
|
167
|
-
'remember', 'note', 'noting', 'user', 'prefer', 'prefers', 'preferred', 'like', 'likes',
|
|
168
|
-
]);
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* THE SLOT A MEMORY FILLS, when it fills one — derived from the text itself.
|
|
172
|
-
*
|
|
173
|
-
* Token overlap cannot see that "Goes by Alex" and "Goes by Sam" are the same fact with a new
|
|
174
|
-
* value: they share two words out of four, which is exactly what two unrelated memories look
|
|
175
|
-
* like. So changing your name produced a SECOND memory and the model was told both.
|
|
176
|
-
*
|
|
177
|
-
* A slot is the subject a statement is about. Two memories with the same slot are one memory,
|
|
178
|
-
* whatever their words, so the later one supersedes. Deriving it from the text rather than
|
|
179
|
-
* asking the caller means it works identically for a captured phrase and for a `memory` tool
|
|
180
|
-
* call from some agent that has never heard of slots.
|
|
181
|
-
*
|
|
182
|
-
* Returns '' when a statement is not slot-shaped ("Deploys on Fridays"), which is most of
|
|
183
|
-
* them — those fall back to key and similarity matching.
|
|
184
|
-
*/
|
|
185
|
-
export function slotOf(text) {
|
|
186
|
-
const t = collapse(text).toLowerCase().replace(/^the\s+/, '');
|
|
187
|
-
// Identity phrasings all name the same slot, or the user's name lives in three places.
|
|
188
|
-
if (/^(?:goes by|name is|is called|called)\b/.test(t)) return 'name';
|
|
189
|
-
if (/^pronouns\b/.test(t)) return 'pronouns';
|
|
190
|
-
// "<subject> is/are <value>" — the general form. Bounded to a short subject so a whole
|
|
191
|
-
// sentence containing "is" somewhere does not become a slot that swallows its neighbours.
|
|
192
|
-
const m = /^(?:my\s+|their\s+)?([\p{L}\p{N}][\p{L}\p{N} '-]{0,28}?)\s+(?:is|are|was|were)\b/u.exec(t);
|
|
193
|
-
if (!m) return '';
|
|
194
|
-
const subject = m[1].split(/\s+/).filter((w) => !KEY_STOPWORDS.has(w)).join(' ');
|
|
195
|
-
return subject.length >= 3 ? subject : '';
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Token-set overlap, 0..1 — "prefers terse answers" vs "prefers terse replies". */
|
|
199
|
-
export function similarity(a, b) {
|
|
200
|
-
const A = new Set(memoryKey(a).split(' ').filter(Boolean));
|
|
201
|
-
const B = new Set(memoryKey(b).split(' ').filter(Boolean));
|
|
202
|
-
if (!A.size || !B.size) return 0;
|
|
203
|
-
let hit = 0;
|
|
204
|
-
for (const w of A) if (B.has(w)) hit += 1;
|
|
205
|
-
return hit / (A.size + B.size - hit);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* How much of the SHORTER phrase the longer one contains, 0..1. Different question from
|
|
210
|
-
* `similarity`, and the right one for "forget the Frankfurt thing": a person names a memory by
|
|
211
|
-
* one distinctive word, not by restating it, so a symmetric measure scores that near zero.
|
|
212
|
-
*/
|
|
213
|
-
export function containment(a, b) {
|
|
214
|
-
const A = new Set(memoryKey(a).split(' ').filter(Boolean));
|
|
215
|
-
const B = new Set(memoryKey(b).split(' ').filter(Boolean));
|
|
216
|
-
if (!A.size || !B.size) return 0;
|
|
217
|
-
let hit = 0;
|
|
218
|
-
for (const w of A) if (B.has(w)) hit += 1;
|
|
219
|
-
return hit / Math.min(A.size, B.size);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Above this, two memories are the same standing fact stated differently. */
|
|
223
|
-
export const SAME_FACT = 0.8;
|
|
224
|
-
|
|
225
|
-
// --------------------------------------------------------------------------
|
|
226
|
-
// Capture — reading a message for things worth keeping
|
|
227
|
-
// --------------------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* COMMANDS. The user is addressing the assistant and telling it to keep something. The capture
|
|
231
|
-
* group is the fact itself, so "remember that I deploy on Fridays" stores "I deploy on
|
|
232
|
-
* Fridays" and not the instruction wrapping it.
|
|
233
|
-
*
|
|
234
|
-
* Each carries the kind it implies, because "call me Alex" is an identity and "from now on,
|
|
235
|
-
* be terse" is a preference, and making the user pick afterwards is a step they should not
|
|
236
|
-
* have to take.
|
|
237
|
-
*/
|
|
238
|
-
const COMMANDS = [
|
|
239
|
-
{ re: /^(?:please\s+)?(?:remember|memorize|memorise)(?:\s+that|\s+this)?[:,]?\s+(.+)$/i, kind: 'fact' },
|
|
240
|
-
{ re: /^(?:please\s+)?(?:keep in mind|bear in mind|don'?t forget|do not forget)(?:\s+that)?[:,]?\s+(.+)$/i, kind: 'fact' },
|
|
241
|
-
{ re: /^(?:please\s+)?(?:make a note|note)(?:\s+that|\s+of)[:,]?\s+(.+)$/i, kind: 'fact' },
|
|
242
|
-
{ re: /^(?:from now on|going forward|in future|in the future|henceforth)[:,]?\s+(.+)$/i, kind: 'preference' },
|
|
243
|
-
{ re: /^(?:call me|i go by|refer to me as)\s+(.+)$/i, kind: 'identity', rebuild: (m) => `Goes by ${trimEnd(m[1])}` },
|
|
244
|
-
{ re: /^my name(?:'s| is)\s+(.+)$/i, kind: 'identity', rebuild: (m) => `Name is ${trimEnd(m[1])}` },
|
|
245
|
-
{ re: /^(?:always|never)\s+(.+)$/i, kind: 'preference', rebuild: (m, raw) => sentence(raw) },
|
|
246
|
-
];
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* REVEALS. Not addressed to the assistant at all — the user simply said something durable
|
|
250
|
-
* about themselves in passing. These are OFFERED, never saved, because the inference is
|
|
251
|
-
* exactly the kind that is right often enough to be useful and wrong often enough to be
|
|
252
|
-
* insulting if acted on silently.
|
|
253
|
-
*/
|
|
254
|
-
const REVEALS = [
|
|
255
|
-
{ re: /^i (?:prefer|like|want|need)\s+(.+)$/i, kind: 'preference', confidence: 0.7 },
|
|
256
|
-
{ re: /^i (?:hate|dislike|don'?t like|do not like|can'?t stand)\s+(.+)$/i, kind: 'preference', confidence: 0.7 },
|
|
257
|
-
{ re: /^i(?:'m| am)(?: a| an| the)?\s+(.+)$/i, kind: 'identity', confidence: 0.6 },
|
|
258
|
-
{ re: /^i (?:work|working) (?:on|at|with)\s+(.+)$/i, kind: 'project', confidence: 0.6 },
|
|
259
|
-
{ re: /^i(?:'m| am) (?:working|building|writing) (?:on\s+)?(.+)$/i, kind: 'project', confidence: 0.6 },
|
|
260
|
-
{ re: /^(?:we|our team|my team) (?:use|uses|are using|run|runs|deploy|deploys)\s+(.+)$/i, kind: 'fact', confidence: 0.6 },
|
|
261
|
-
{ re: /^my (?:\w+\s){0,2}?(?:is|are)\s+(.+)$/i, kind: 'fact', confidence: 0.55 },
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
/** Removal is a command too, and it must be recognised or "forget that" gets stored as a fact. */
|
|
265
|
-
const FORGETS = [
|
|
266
|
-
/^(?:please\s+)?forget(?:\s+that|\s+about)?[:,]?\s+(.+)$/i,
|
|
267
|
-
/^(?:please\s+)?(?:stop remembering|no longer remember|un-?remember)[:,]?\s+(.+)$/i,
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* A trigger word inside a QUESTION is not an instruction — "do you remember what we decided?"
|
|
272
|
-
* asks for recall, and storing it as a fact is the single most obvious way to make this
|
|
273
|
-
* feature look broken. Likewise "I can't remember" is a complaint, not a command.
|
|
274
|
-
*/
|
|
275
|
-
const NOT_A_COMMAND = /(?:^|\s)(?:do|does|did|can|could|would|will|what|when|where|who|why|how|are|is)\s+(?:you|i|we|it)\b/i;
|
|
276
|
-
const NEGATED = /\b(?:can'?t|cannot|don'?t|do not|couldn'?t|never|didn'?t)\s+(?:seem to\s+)?(?:remember|recall|forget)\b/i;
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Read one user message for things worth keeping.
|
|
280
|
-
*
|
|
281
|
-
* Returns candidates in the order found. `explicit: true` means the user issued a command and
|
|
282
|
-
* the host may save it without asking; `explicit: false` means offer it and let them tap.
|
|
283
|
-
* That distinction is the whole capture policy, and it lives here rather than in a client so
|
|
284
|
-
* the panel, the gateway and a mobile app cannot each pick a different one.
|
|
285
|
-
*
|
|
286
|
-
* @param text one user message.
|
|
287
|
-
* @param opts.maxCandidates cap per message (default 3) — a wall of chips is not a prompt.
|
|
288
|
-
* @param opts.includeReveals set false for surfaces with nowhere to show an offer.
|
|
289
|
-
* @returns {{op: 'remember'|'forget', text: string, kind: string, confidence: number,
|
|
290
|
-
* explicit: boolean, trigger: string}[]}
|
|
291
|
-
*/
|
|
292
|
-
export function candidatesFrom(text, { maxCandidates = 3, includeReveals = true } = {}) {
|
|
293
|
-
const out = [];
|
|
294
|
-
const raw = String(text || '');
|
|
295
|
-
// Fenced code is material, not speech. A README line that happens to start "I prefer" is
|
|
296
|
-
// not the user telling us anything.
|
|
297
|
-
const speech = raw.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`]*`/g, ' ');
|
|
298
|
-
|
|
299
|
-
for (const line of splitStatements(speech)) {
|
|
300
|
-
if (out.length >= maxCandidates) break;
|
|
301
|
-
if (line.length < MIN_MEMORY_CHARS) continue;
|
|
302
|
-
if (NEGATED.test(line)) continue;
|
|
303
|
-
|
|
304
|
-
let matched = false;
|
|
305
|
-
for (const re of FORGETS) {
|
|
306
|
-
const m = re.exec(line);
|
|
307
|
-
if (!m) continue;
|
|
308
|
-
const body = clip(trimEnd(m[1]));
|
|
309
|
-
if (body.length >= MIN_MEMORY_CHARS) {
|
|
310
|
-
out.push({ op: 'forget', text: body, kind: 'fact', confidence: 1, explicit: true, trigger: 'forget' });
|
|
311
|
-
matched = true;
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
if (matched) continue;
|
|
316
|
-
|
|
317
|
-
// A question mark alone is not disqualifying — "remember that the demo is Friday?" is a
|
|
318
|
-
// command with a tag. It is the interrogative SHAPE that rules a command out.
|
|
319
|
-
if (NOT_A_COMMAND.test(line)) continue;
|
|
320
|
-
|
|
321
|
-
for (const { re, kind, rebuild } of COMMANDS) {
|
|
322
|
-
const m = re.exec(line);
|
|
323
|
-
if (!m) continue;
|
|
324
|
-
const body = clip(rebuild ? rebuild(m, line) : sentence(trimEnd(m[1])));
|
|
325
|
-
if (body.length >= MIN_MEMORY_CHARS) {
|
|
326
|
-
out.push({ op: 'remember', text: body, kind, confidence: 1, explicit: true, trigger: 'command' });
|
|
327
|
-
matched = true;
|
|
328
|
-
}
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
if (matched || !includeReveals) continue;
|
|
332
|
-
|
|
333
|
-
for (const { re, kind, confidence } of REVEALS) {
|
|
334
|
-
if (!re.test(line)) continue;
|
|
335
|
-
const body = clip(sentence(line));
|
|
336
|
-
// Two words minimum: "I am tired" is durable-looking and worthless. The floor is crude
|
|
337
|
-
// on purpose — the cost of a bad offer is one ignored chip, and the cost of a clever
|
|
338
|
-
// filter is a fact the user watched get dropped.
|
|
339
|
-
if (body.split(/\s+/).length >= 3) {
|
|
340
|
-
out.push({ op: 'remember', text: body, kind, confidence, explicit: false, trigger: 'reveal' });
|
|
341
|
-
}
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return out.slice(0, maxCandidates);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// --------------------------------------------------------------------------
|
|
349
|
-
// Reconcile — what a write does to what is already there
|
|
350
|
-
// --------------------------------------------------------------------------
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Decide what saving `incoming` means against the memories already held.
|
|
354
|
-
*
|
|
355
|
-
* duplicate — the same fact, said the same way. Nothing to do but touch it.
|
|
356
|
-
* update — the same fact, restated or changed. Supersede in place, keep the old text.
|
|
357
|
-
* create — genuinely new.
|
|
358
|
-
*
|
|
359
|
-
* There is deliberately no `conflict`. Two contradictory statements cannot be told apart from
|
|
360
|
-
* a correction without understanding the sentence, and a memory system that asks "did you
|
|
361
|
-
* mean to change your mind?" is a memory system people turn off. The later statement wins and
|
|
362
|
-
* the earlier one stays visible in `history`.
|
|
363
|
-
*
|
|
364
|
-
* @returns {{action: 'create'|'update'|'duplicate', record: object, replaces: object|null}}
|
|
365
|
-
*/
|
|
366
|
-
export function reconcile(memories, incoming, { now = 0, newId = null } = {}) {
|
|
367
|
-
const next = normalizeMemory(incoming, { now, newId });
|
|
368
|
-
const pool = (memories || []).filter((m) => m && m.scope === next.scope);
|
|
369
|
-
|
|
370
|
-
// Slot first: it is the only one of the three that can recognise a CHANGED VALUE, which is
|
|
371
|
-
// what a correction is. Then exact key, then near-identical wording.
|
|
372
|
-
const match = (next.slot && pool.find((m) => m.slot === next.slot))
|
|
373
|
-
|| pool.find((m) => m.key === next.key)
|
|
374
|
-
|| pool.find((m) => m.kind === next.kind && similarity(m.text, next.text) >= SAME_FACT);
|
|
375
|
-
|
|
376
|
-
if (!match) return { action: 'create', record: next, replaces: null };
|
|
377
|
-
|
|
378
|
-
if (collapse(match.text).toLowerCase() === next.text.toLowerCase()) {
|
|
379
|
-
// Restating a memory is a signal about it: it is still true, and it is on the user's mind.
|
|
380
|
-
return {
|
|
381
|
-
action: 'duplicate',
|
|
382
|
-
record: { ...match, updatedAt: Number(now) || match.updatedAt, useCount: match.useCount + 1 },
|
|
383
|
-
replaces: match,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return {
|
|
388
|
-
action: 'update',
|
|
389
|
-
record: {
|
|
390
|
-
...match,
|
|
391
|
-
text: next.text,
|
|
392
|
-
key: next.key,
|
|
393
|
-
slot: next.slot,
|
|
394
|
-
kind: next.kind,
|
|
395
|
-
tags: next.tags.length ? next.tags : match.tags,
|
|
396
|
-
confidence: next.confidence,
|
|
397
|
-
source: next.source,
|
|
398
|
-
updatedAt: Number(now) || match.updatedAt,
|
|
399
|
-
history: [...match.history, { text: match.text, at: match.updatedAt }].slice(-5),
|
|
400
|
-
},
|
|
401
|
-
replaces: match,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Which stored memories a "forget X" refers to. Matches by id, then by exact key, then by
|
|
407
|
-
* similarity — a person says "forget the Frankfurt thing", not a uuid.
|
|
408
|
-
*/
|
|
409
|
-
export function matchForForget(memories, query, { limit = 5 } = {}) {
|
|
410
|
-
const q = collapse(query);
|
|
411
|
-
if (!q) return [];
|
|
412
|
-
const byId = (memories || []).filter((m) => m.id === q);
|
|
413
|
-
if (byId.length) return byId;
|
|
414
|
-
const key = memoryKey(q);
|
|
415
|
-
return (memories || [])
|
|
416
|
-
.map((m) => ({ m, s: m.key === key ? 1 : Math.max(similarity(m.text, q), containment(q, m.text)) }))
|
|
417
|
-
.filter((x) => x.s >= 0.5)
|
|
418
|
-
.sort((a, b) => b.s - a.s)
|
|
419
|
-
.slice(0, limit)
|
|
420
|
-
.map((x) => x.m);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// --------------------------------------------------------------------------
|
|
424
|
-
// Recall — what the model is told, this turn
|
|
425
|
-
// --------------------------------------------------------------------------
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Choose the memories for one turn, within a character budget.
|
|
429
|
-
*
|
|
430
|
-
* Ambient kinds (identity, preference) come first and unconditionally: they are how the user
|
|
431
|
-
* wants to be spoken to, and a turn that fails to mention their name is still a turn where
|
|
432
|
-
* their name applies. Everything else is scored against the turn's text — a project memory
|
|
433
|
-
* earns its tokens only when the turn is about it.
|
|
434
|
-
*
|
|
435
|
-
* Pinned always wins, of any kind. That is the user's explicit override of this whole ranking.
|
|
436
|
-
*
|
|
437
|
-
* @param memories all stored memories.
|
|
438
|
-
* @param opts.text the turn's text (user message, or the question for a tool call).
|
|
439
|
-
* @param opts.scopes which scopes apply — 'global' plus, say, `agent:claude-code`.
|
|
440
|
-
* @param opts.maxChars budget for the rendered block.
|
|
441
|
-
* @returns the chosen memories, most important first.
|
|
442
|
-
*/
|
|
443
|
-
export function recall(memories, {
|
|
444
|
-
text = '', scopes = ['global'], kinds = null, now = 0,
|
|
445
|
-
limit = 24, maxChars = DEFAULT_BLOCK_CHARS, includeAmbient = true,
|
|
446
|
-
} = {}) {
|
|
447
|
-
const scopeSet = new Set(scopes.length ? scopes : ['global']);
|
|
448
|
-
const terms = new Set(memoryKey(text).split(' ').filter(Boolean));
|
|
449
|
-
|
|
450
|
-
const live = (memories || [])
|
|
451
|
-
.filter((m) => m && m.text)
|
|
452
|
-
.filter((m) => scopeSet.has(m.scope))
|
|
453
|
-
.filter((m) => !kinds || kinds.includes(m.kind))
|
|
454
|
-
.filter((m) => !m.expiresAt || !now || m.expiresAt > now);
|
|
455
|
-
|
|
456
|
-
const scored = live.map((m) => {
|
|
457
|
-
const words = new Set(m.key.split(' ').filter(Boolean));
|
|
458
|
-
let overlap = 0;
|
|
459
|
-
for (const w of words) if (terms.has(w)) overlap += 1;
|
|
460
|
-
const relevance = words.size ? overlap / words.size : 0;
|
|
461
|
-
const ambient = includeAmbient && MEMORY_KINDS[m.kind]?.ambient;
|
|
462
|
-
// Recency and use are TIE-BREAKS, not drivers. A fact does not become truer because it
|
|
463
|
-
// was mentioned recently, but between two equally relevant ones the live one is the
|
|
464
|
-
// better guess.
|
|
465
|
-
const freshness = now && m.updatedAt ? Math.max(0, 1 - (now - m.updatedAt) / YEAR) : 0;
|
|
466
|
-
const used = Math.min(1, m.useCount / 10);
|
|
467
|
-
return {
|
|
468
|
-
m,
|
|
469
|
-
keep: m.pinned || ambient || relevance > 0,
|
|
470
|
-
score: (m.pinned ? 100 : 0) + (ambient ? 10 : 0) + relevance * 8 + freshness + used,
|
|
471
|
-
};
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
const out = [];
|
|
475
|
-
let chars = 0;
|
|
476
|
-
for (const { m, keep, score } of scored.filter((s) => s.keep).sort((a, b) => b.score - a.score)) {
|
|
477
|
-
if (out.length >= limit) break;
|
|
478
|
-
const cost = m.text.length + m.kind.length + 6;
|
|
479
|
-
if (chars + cost > maxChars && out.length) break;
|
|
480
|
-
chars += cost;
|
|
481
|
-
out.push(m);
|
|
482
|
-
void score;
|
|
483
|
-
}
|
|
484
|
-
return out;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const YEAR = 365 * 24 * 60 * 60 * 1000;
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* The ONE prompt rendering. Every surface that puts memory in front of a model uses this, so
|
|
491
|
-
* "what the model was told" is a single, reviewable string rather than per-client prose.
|
|
492
|
-
*
|
|
493
|
-
* Returns '' for an empty set — callers can concatenate unconditionally.
|
|
494
|
-
*/
|
|
495
|
-
export function memoryBlock(memories, { heading = 'What you already know about this user', maxChars = DEFAULT_BLOCK_CHARS } = {}) {
|
|
496
|
-
const list = (memories || []).filter((m) => m && m.text);
|
|
497
|
-
if (!list.length) return '';
|
|
498
|
-
const lines = [];
|
|
499
|
-
let chars = 0;
|
|
500
|
-
for (const m of list) {
|
|
501
|
-
const line = `- (${m.kind}) ${m.text}`;
|
|
502
|
-
if (chars + line.length > maxChars && lines.length) break;
|
|
503
|
-
chars += line.length + 1;
|
|
504
|
-
lines.push(line);
|
|
505
|
-
}
|
|
506
|
-
return [
|
|
507
|
-
`## ${heading}`,
|
|
508
|
-
'Saved by the user in ChatPanel and true across conversations, agents and devices.',
|
|
509
|
-
'Apply them without being asked and without announcing them.',
|
|
510
|
-
...lines,
|
|
511
|
-
'If the user states something durable about themselves, their preferences or their work,'
|
|
512
|
-
+ ' save it with the `memory` tool. If they correct one of these, update it — do not just agree.',
|
|
513
|
-
].join('\n');
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/** Mark memories as used this turn. Recall quality depends on it, so it is not optional. */
|
|
517
|
-
export function markUsed(memories, ids, { now = 0 } = {}) {
|
|
518
|
-
const set = new Set(ids || []);
|
|
519
|
-
return (memories || []).map((m) => (set.has(m.id)
|
|
520
|
-
? { ...m, usedAt: Number(now) || m.usedAt, useCount: m.useCount + 1 }
|
|
521
|
-
: m));
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Drop what has expired, then hold the store to `max` by evicting the least valuable.
|
|
526
|
-
*
|
|
527
|
-
* Eviction order is the inverse of value: never pinned, never ambient, then least used and
|
|
528
|
-
* least recently touched. Returns both halves so a client can TELL the user what went rather
|
|
529
|
-
* than silently shrinking their memory.
|
|
530
|
-
*/
|
|
531
|
-
export function pruneMemories(memories, { now = 0, max = DEFAULT_MAX_MEMORIES } = {}) {
|
|
532
|
-
const all = (memories || []).filter(Boolean);
|
|
533
|
-
const expired = now ? all.filter((m) => m.expiresAt && m.expiresAt <= now) : [];
|
|
534
|
-
let kept = expired.length ? all.filter((m) => !expired.includes(m)) : all;
|
|
535
|
-
const dropped = [...expired];
|
|
536
|
-
|
|
537
|
-
if (kept.length > max) {
|
|
538
|
-
const value = (m) => (m.pinned ? 3 : 0) + (MEMORY_KINDS[m.kind]?.ambient ? 1 : 0);
|
|
539
|
-
const ranked = [...kept].sort((a, b) => value(b) - value(a)
|
|
540
|
-
|| b.useCount - a.useCount
|
|
541
|
-
|| (b.usedAt || b.updatedAt) - (a.usedAt || a.updatedAt));
|
|
542
|
-
dropped.push(...ranked.slice(max));
|
|
543
|
-
kept = ranked.slice(0, max);
|
|
544
|
-
}
|
|
545
|
-
return { kept, dropped };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// --------------------------------------------------------------------------
|
|
549
|
-
// The tool contract — one definition, every client
|
|
550
|
-
// --------------------------------------------------------------------------
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* The `memory` tool as the model sees it, shared by the extension's turn toolset and the
|
|
554
|
-
* gateway's MCP server. A second copy would drift, and the drift would be a model that
|
|
555
|
-
* behaves differently in Claude Code than in the side panel for no reason a user could see.
|
|
556
|
-
*
|
|
557
|
-
* One tool with an `action`, not four tools: memory is a small feature and four schemas
|
|
558
|
-
* resident on every turn would cost more than the memories themselves.
|
|
559
|
-
*/
|
|
560
|
-
export const MEMORY_TOOL_SPEC = Object.freeze({
|
|
561
|
-
name: 'memory',
|
|
562
|
-
description:
|
|
563
|
-
"Save, update or remove a durable fact about the USER in ChatPanel — carried into every "
|
|
564
|
-
+ 'future conversation, on every model and agent. Use it the moment they state a standing '
|
|
565
|
-
+ 'preference ("always be terse"), an identity fact ("call me Alex"), or a constraint about '
|
|
566
|
-
+ 'their work that will still be true next week. Do NOT use it for one-off task details, '
|
|
567
|
-
+ 'anything already obvious from the conversation, or content that belongs in a note. '
|
|
568
|
-
+ 'Keep each memory one short self-contained sentence. Use `forget` when the user says '
|
|
569
|
-
+ 'something is no longer true, and `list` to see what is already stored before adding.',
|
|
570
|
-
parameters: {
|
|
571
|
-
type: 'object',
|
|
572
|
-
properties: {
|
|
573
|
-
action: { type: 'string', enum: ['remember', 'forget', 'list'], description: 'What to do.' },
|
|
574
|
-
text: { type: 'string', description: `The fact, as one short sentence written in the third person ("Prefers terse answers"). Max ${MAX_MEMORY_CHARS} characters. Required for remember; for forget, the memory to drop (its text or id).` },
|
|
575
|
-
kind: { type: 'string', enum: MEMORY_KIND_NAMES, description: MEMORY_KIND_NAMES.map((k) => `${k}: ${MEMORY_KINDS[k].hint}`).join(' ') },
|
|
576
|
-
tags: { type: 'array', items: { type: 'string' }, description: 'Optional short tags for grouping.' },
|
|
577
|
-
},
|
|
578
|
-
required: ['action'],
|
|
579
|
-
},
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
/** The system text that goes with the tool — why it exists, when NOT to reach for it. */
|
|
583
|
-
export function memoryToolSystem() {
|
|
584
|
-
return [
|
|
585
|
-
'You can give the user a memory that survives this conversation, this model and this device'
|
|
586
|
-
+ ' — call the `memory` tool.',
|
|
587
|
-
'Save when they say something durable about themselves, how they want to be helped, or their'
|
|
588
|
-
+ ' ongoing work. Do not save task details, transient state, or anything they would not want'
|
|
589
|
-
+ ' repeated back to them in a month.',
|
|
590
|
-
'Saving is a change to the user\'s own data: say in one short clause what you saved.',
|
|
591
|
-
].join(' ');
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// --------------------------------------------------------------------------
|
|
595
|
-
// Versioning
|
|
596
|
-
// --------------------------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Upcasters, empty at v1 — present so the first schema change is a one-line addition rather
|
|
600
|
-
* than a migration nobody planned for. Same machinery as `upcast.js`.
|
|
601
|
-
*/
|
|
602
|
-
export const MEMORY_UPCASTERS = Object.freeze({});
|
|
603
|
-
|
|
604
|
-
export function upcastMemory(rec) {
|
|
605
|
-
let out = rec;
|
|
606
|
-
for (let v = Number(out?.v) || 1; v < MEMORY_VERSION; v += 1) {
|
|
607
|
-
const up = MEMORY_UPCASTERS[v];
|
|
608
|
-
if (!up) throw new MemoryError(`no upcaster from memory v${v}`);
|
|
609
|
-
out = up(out);
|
|
610
|
-
}
|
|
611
|
-
return out;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// --------------------------------------------------------------------------
|
|
615
|
-
// Text helpers
|
|
616
|
-
// --------------------------------------------------------------------------
|
|
617
|
-
|
|
618
|
-
const collapse = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
619
|
-
const clamp01 = (n) => (Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 1);
|
|
620
|
-
const trimEnd = (s) => collapse(s).replace(/[.,;:!]+$/, '');
|
|
621
|
-
const clip = (s) => (s.length > MAX_MEMORY_CHARS ? `${s.slice(0, MAX_MEMORY_CHARS - 1).trimEnd()}…` : s);
|
|
622
|
-
const sentence = (s) => {
|
|
623
|
-
const t = trimEnd(s);
|
|
624
|
-
return t ? t[0].toUpperCase() + t.slice(1) : t;
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Split a message into the statements a trigger could apply to. Newlines and list bullets are
|
|
629
|
-
* hard boundaries; sentence-ending punctuation is a soft one. Deliberately does NOT split on
|
|
630
|
-
* commas — "remember that we ship on Friday, not Thursday" is one fact.
|
|
631
|
-
*/
|
|
632
|
-
function splitStatements(text) {
|
|
633
|
-
return String(text || '')
|
|
634
|
-
.split(/\n+/)
|
|
635
|
-
.flatMap((line) => collapse(line).replace(/^[-*+•]\s*|^\d+[.)]\s*/, '').split(/(?<=[.!?])\s+(?=[A-Z"'])/))
|
|
636
|
-
.map(collapse)
|
|
637
|
-
.filter(Boolean);
|
|
638
|
-
}
|
|
1
|
+
const G=1,b=Object.freeze({identity:{label:"Identity",ambient:!0,hint:"Who the user is — name, role, pronouns, language, timezone."},preference:{label:"Preference",ambient:!0,hint:"How they want things done — tone, format, defaults, things never to do."},project:{label:"Project",ambient:!1,hint:"Ongoing work, goals and constraints not derivable from the material itself."},fact:{label:"Fact",ambient:!1,hint:"A durable fact about their world — systems, teams, environments, conventions."},reference:{label:"Reference",ambient:!1,hint:"A pointer to something external — a URL, a dashboard, a ticket, a doc."}}),M=Object.freeze(Object.keys(b)),X=Object.freeze(M.filter(e=>b[e].ambient)),y=280,S=3,B=200,$=1200;class v extends Error{constructor(o){super(o),this.name="MemoryError"}}function C(e={},{now:o=0,newId:s=null}={}){const r=u(e.text);if(r.length<S)throw new v("a memory needs text");if(r.length>y)throw new v(`a memory must be at most ${y} characters — save longer material as a note`);const n=b[e.kind]?e.kind:"fact",a=Number(e.createdAt)||Number(o)||0;return{id:String(e.id||(s?s():"")||""),v:1,text:r,kind:n,scope:String(e.scope||"global"),key:g(r),slot:u(e.slot).toLowerCase()||T(r),tags:[...new Set((e.tags||[]).map(t=>u(t).toLowerCase()).filter(Boolean))].slice(0,8),pinned:!!e.pinned,source:{via:String(e.source?.via||"user"),surface:String(e.source?.surface||""),ref:String(e.source?.ref||""),agent:String(e.source?.agent||"")},confidence:W(e.confidence==null?1:Number(e.confidence)),createdAt:a,updatedAt:Number(e.updatedAt)||a,usedAt:Number(e.usedAt)||0,useCount:Math.max(0,Math.floor(Number(e.useCount)||0)),expiresAt:e.expiresAt?Number(e.expiresAt):null,history:(e.history||[]).slice(-5).map(t=>({text:u(t.text),at:Number(t.at)||0}))}}function Z(e){try{return C(e,{now:e?.createdAt||1}),!!e?.id}catch{return!1}}function g(e){const o=u(e).toLowerCase().replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(s=>s&&!R.has(s));return[...new Set(o)].sort().join(" ")}const R=new Set(["a","an","the","i","im","me","my","mine","we","our","us","you","your","is","am","are","was","were","be","been","being","do","does","did","to","of","in","on","at","for","with","and","or","that","this","it","please","always","usually","generally","really","just","very","so","remember","note","noting","user","prefer","prefers","preferred","like","likes"]);function T(e){const o=u(e).toLowerCase().replace(/^the\s+/,"");if(/^(?:goes by|name is|is called|called)\b/.test(o))return"name";if(/^pronouns\b/.test(o))return"pronouns";const s=/^(?:my\s+|their\s+)?([\p{L}\p{N}][\p{L}\p{N} '-]{0,28}?)\s+(?:is|are|was|were)\b/u.exec(o);if(!s)return"";const r=s[1].split(/\s+/).filter(n=>!R.has(n)).join(" ");return r.length>=3?r:""}function _(e,o){const s=new Set(g(e).split(" ").filter(Boolean)),r=new Set(g(o).split(" ").filter(Boolean));if(!s.size||!r.size)return 0;let n=0;for(const a of s)r.has(a)&&(n+=1);return n/(s.size+r.size-n)}function I(e,o){const s=new Set(g(e).split(" ").filter(Boolean)),r=new Set(g(o).split(" ").filter(Boolean));if(!s.size||!r.size)return 0;let n=0;for(const a of s)r.has(a)&&(n+=1);return n/Math.min(s.size,r.size)}const Y=.8,D=[{re:/^(?:please\s+)?(?:remember|memorize|memorise)(?:\s+that|\s+this)?[:,]?\s+(.+)$/i,kind:"fact"},{re:/^(?:please\s+)?(?:keep in mind|bear in mind|don'?t forget|do not forget)(?:\s+that)?[:,]?\s+(.+)$/i,kind:"fact"},{re:/^(?:please\s+)?(?:make a note|note)(?:\s+that|\s+of)[:,]?\s+(.+)$/i,kind:"fact"},{re:/^(?:from now on|going forward|in future|in the future|henceforth)[:,]?\s+(.+)$/i,kind:"preference"},{re:/^(?:call me|i go by|refer to me as)\s+(.+)$/i,kind:"identity",rebuild:e=>`Goes by ${w(e[1])}`},{re:/^my name(?:'s| is)\s+(.+)$/i,kind:"identity",rebuild:e=>`Name is ${w(e[1])}`},{re:/^(?:always|never)\s+(.+)$/i,kind:"preference",rebuild:(e,o)=>O(o)}],F=[{re:/^i (?:prefer|like|want|need)\s+(.+)$/i,kind:"preference",confidence:.7},{re:/^i (?:hate|dislike|don'?t like|do not like|can'?t stand)\s+(.+)$/i,kind:"preference",confidence:.7},{re:/^i(?:'m| am)(?: a| an| the)?\s+(.+)$/i,kind:"identity",confidence:.6},{re:/^i (?:work|working) (?:on|at|with)\s+(.+)$/i,kind:"project",confidence:.6},{re:/^i(?:'m| am) (?:working|building|writing) (?:on\s+)?(.+)$/i,kind:"project",confidence:.6},{re:/^(?:we|our team|my team) (?:use|uses|are using|run|runs|deploy|deploys)\s+(.+)$/i,kind:"fact",confidence:.6},{re:/^my (?:\w+\s){0,2}?(?:is|are)\s+(.+)$/i,kind:"fact",confidence:.55}],U=[/^(?:please\s+)?forget(?:\s+that|\s+about)?[:,]?\s+(.+)$/i,/^(?:please\s+)?(?:stop remembering|no longer remember|un-?remember)[:,]?\s+(.+)$/i],P=/(?:^|\s)(?:do|does|did|can|could|would|will|what|when|where|who|why|how|are|is)\s+(?:you|i|we|it)\b/i,K=/\b(?:can'?t|cannot|don'?t|do not|couldn'?t|never|didn'?t)\s+(?:seem to\s+)?(?:remember|recall|forget)\b/i;function J(e,{maxCandidates:o=3,includeReveals:s=!0}={}){const r=[],a=String(e||"").replace(/```[\s\S]*?```/g," ").replace(/`[^`]*`/g," ");for(const t of q(a)){if(r.length>=o)break;if(t.length<S||K.test(t))continue;let i=!1;for(const f of U){const l=f.exec(t);if(!l)continue;const d=E(w(l[1]));d.length>=S&&(r.push({op:"forget",text:d,kind:"fact",confidence:1,explicit:!0,trigger:"forget"}),i=!0);break}if(!i&&!P.test(t)){for(const{re:f,kind:l,rebuild:d}of D){const p=f.exec(t);if(!p)continue;const m=E(d?d(p,t):O(w(p[1])));m.length>=S&&(r.push({op:"remember",text:m,kind:l,confidence:1,explicit:!0,trigger:"command"}),i=!0);break}if(!(i||!s))for(const{re:f,kind:l,confidence:d}of F){if(!f.test(t))continue;const p=E(O(t));p.split(/\s+/).length>=3&&r.push({op:"remember",text:p,kind:l,confidence:d,explicit:!1,trigger:"reveal"});break}}}return r.slice(0,o)}function Q(e,o,{now:s=0,newId:r=null}={}){const n=C(o,{now:s,newId:r}),a=(e||[]).filter(i=>i&&i.scope===n.scope),t=n.slot&&a.find(i=>i.slot===n.slot)||a.find(i=>i.key===n.key)||a.find(i=>i.kind===n.kind&&_(i.text,n.text)>=Y);return t?u(t.text).toLowerCase()===n.text.toLowerCase()?{action:"duplicate",record:{...t,updatedAt:Number(s)||t.updatedAt,useCount:t.useCount+1},replaces:t}:{action:"update",record:{...t,text:n.text,key:n.key,slot:n.slot,kind:n.kind,tags:n.tags.length?n.tags:t.tags,confidence:n.confidence,source:n.source,updatedAt:Number(s)||t.updatedAt,history:[...t.history,{text:t.text,at:t.updatedAt}].slice(-5)},replaces:t}:{action:"create",record:n,replaces:null}}function ee(e,o,{limit:s=5}={}){const r=u(o);if(!r)return[];const n=(e||[]).filter(t=>t.id===r);if(n.length)return n;const a=g(r);return(e||[]).map(t=>({m:t,s:t.key===a?1:Math.max(_(t.text,r),I(r,t.text))})).filter(t=>t.s>=.5).sort((t,i)=>i.s-t.s).slice(0,s).map(t=>t.m)}function te(e,{text:o="",scopes:s=["global"],kinds:r=null,now:n=0,limit:a=24,maxChars:t=$,includeAmbient:i=!0}={}){const f=new Set(s.length?s:["global"]),l=new Set(g(o).split(" ").filter(Boolean)),p=(e||[]).filter(c=>c&&c.text).filter(c=>f.has(c.scope)).filter(c=>!r||r.includes(c.kind)).filter(c=>!c.expiresAt||!n||c.expiresAt>n).map(c=>{const k=new Set(c.key.split(" ").filter(Boolean));let x=0;for(const L of k)l.has(L)&&(x+=1);const h=k.size?x/k.size:0,A=i&&b[c.kind]?.ambient,j=n&&c.updatedAt?Math.max(0,1-(n-c.updatedAt)/V):0,z=Math.min(1,c.useCount/10);return{m:c,keep:c.pinned||A||h>0,score:(c.pinned?100:0)+(A?10:0)+h*8+j+z}}),m=[];let N=0;for(const{m:c,keep:k,score:x}of p.filter(h=>h.keep).sort((h,A)=>A.score-h.score)){if(m.length>=a)break;const h=c.text.length+c.kind.length+6;if(N+h>t&&m.length)break;N+=h,m.push(c)}return m}const V=365*24*60*60*1e3;function ne(e,{heading:o="What you already know about this user",maxChars:s=$}={}){const r=(e||[]).filter(t=>t&&t.text);if(!r.length)return"";const n=[];let a=0;for(const t of r){const i=`- (${t.kind}) ${t.text}`;if(a+i.length>s&&n.length)break;a+=i.length+1,n.push(i)}return[`## ${o}`,"Saved by the user in ChatPanel and true across conversations, agents and devices.","Apply them without being asked and without announcing them.",...n,"If the user states something durable about themselves, their preferences or their work, save it with the `memory` tool. If they correct one of these, update it — do not just agree."].join(`
|
|
2
|
+
`)}function re(e,o,{now:s=0}={}){const r=new Set(o||[]);return(e||[]).map(n=>r.has(n.id)?{...n,usedAt:Number(s)||n.usedAt,useCount:n.useCount+1}:n)}function oe(e,{now:o=0,max:s=B}={}){const r=(e||[]).filter(Boolean),n=o?r.filter(i=>i.expiresAt&&i.expiresAt<=o):[];let a=n.length?r.filter(i=>!n.includes(i)):r;const t=[...n];if(a.length>s){const i=l=>(l.pinned?3:0)+(b[l.kind]?.ambient?1:0),f=[...a].sort((l,d)=>i(d)-i(l)||d.useCount-l.useCount||(d.usedAt||d.updatedAt)-(l.usedAt||l.updatedAt));t.push(...f.slice(s)),a=f.slice(0,s)}return{kept:a,dropped:t}}const se=Object.freeze({name:"memory",description:'Save, update or remove a durable fact about the USER in ChatPanel — carried into every future conversation, on every model and agent. Use it the moment they state a standing preference ("always be terse"), an identity fact ("call me Alex"), or a constraint about their work that will still be true next week. Do NOT use it for one-off task details, anything already obvious from the conversation, or content that belongs in a note. Keep each memory one short self-contained sentence. Use `forget` when the user says something is no longer true, and `list` to see what is already stored before adding.',parameters:{type:"object",properties:{action:{type:"string",enum:["remember","forget","list"],description:"What to do."},text:{type:"string",description:`The fact, as one short sentence written in the third person ("Prefers terse answers"). Max ${y} characters. Required for remember; for forget, the memory to drop (its text or id).`},kind:{type:"string",enum:M,description:M.map(e=>`${e}: ${b[e].hint}`).join(" ")},tags:{type:"array",items:{type:"string"},description:"Optional short tags for grouping."}},required:["action"]}});function ie(){return["You can give the user a memory that survives this conversation, this model and this device — call the `memory` tool.","Save when they say something durable about themselves, how they want to be helped, or their ongoing work. Do not save task details, transient state, or anything they would not want repeated back to them in a month.","Saving is a change to the user's own data: say in one short clause what you saved."].join(" ")}const H=Object.freeze({});function ae(e){let o=e;for(let s=Number(o?.v)||1;s<1;s+=1){const r=H[s];if(!r)throw new v(`no upcaster from memory v${s}`);o=r(o)}return o}const u=e=>String(e??"").replace(/\s+/g," ").trim(),W=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):1,w=e=>u(e).replace(/[.,;:!]+$/,""),E=e=>e.length>y?`${e.slice(0,y-1).trimEnd()}…`:e,O=e=>{const o=w(e);return o&&o[0].toUpperCase()+o.slice(1)};function q(e){return String(e||"").split(/\n+/).flatMap(o=>u(o).replace(/^[-*+•]\s*|^\d+[.)]\s*/,"").split(/(?<=[.!?])\s+(?=[A-Z"'])/)).map(u).filter(Boolean)}export{X as AMBIENT_KINDS,$ as DEFAULT_BLOCK_CHARS,B as DEFAULT_MAX_MEMORIES,y as MAX_MEMORY_CHARS,b as MEMORY_KINDS,M as MEMORY_KIND_NAMES,se as MEMORY_TOOL_SPEC,H as MEMORY_UPCASTERS,G as MEMORY_VERSION,S as MIN_MEMORY_CHARS,v as MemoryError,Y as SAME_FACT,J as candidatesFrom,I as containment,Z as isValidMemory,re as markUsed,ee as matchForForget,ne as memoryBlock,g as memoryKey,ie as memoryToolSystem,C as normalizeMemory,oe as pruneMemories,te as recall,Q as reconcile,_ as similarity,T as slotOf,ae as upcastMemory};
|