linksee-memory 0.16.1 → 0.17.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.
@@ -0,0 +1,234 @@
1
+ // share_draft — build a link to KanseiLINK's reviewed draft page for an agent field note.
2
+ //
3
+ // What this does: checks a fixed set of fields, and returns a URL. Nothing else.
4
+ // - Linksee itself makes NO network call here. The human opens the link, reads the
5
+ // filled form, and only their click on that page sends anything.
6
+ // - No free text beyond the fixed fields. Each field has a length cap, and any field that
7
+ // looks like it carries a secret stops the build — we return which field and why,
8
+ // never a link. (The page masks again on display; we refuse earlier because the URL
9
+ // itself passes through the page host's request logs.)
10
+ // - Fields are filled by the agent from what the user wants to share. This module never
11
+ // reads memory to fill them.
12
+ //
13
+ // Target page: https://kansei-link.com/agents/note/new.html — it reads exactly these query
14
+ // keys: service, task, symptom, tried, result, conditions, kind, runtime (and trims the query
15
+ // to 2,000 characters).
16
+ export const DRAFT_PAGE = 'https://kansei-link.com/agents/note/new.html';
17
+ export const URL_MAX = 2000;
18
+ export const KINDS = ['execution', 'documentation', 'hypothesis'];
19
+ export const RUNTIMES = ['claude', 'codex', 'gpt', 'gemini', 'other'];
20
+ /** Text fields in URL order, with the page's caps (same numbers as the page's maxlength). */
21
+ export const TEXT_FIELDS = [
22
+ { key: 'task', max: 300, required: true },
23
+ { key: 'symptom', max: 600, required: true },
24
+ { key: 'tried', max: 2000, required: true },
25
+ { key: 'result', max: 1000, required: true },
26
+ { key: 'conditions', max: 600, required: false },
27
+ ];
28
+ export const ALLOWED_KEYS = ['service', ...TEXT_FIELDS.map((f) => f.key), 'kind', 'runtime'];
29
+ export const SERVICE_RE = /^[a-z0-9_.-]{1,160}$/;
30
+ export const NOTICE = [
31
+ 'この内容は、開いた画面で確認してから送られます。送るまでどこにも出ていません。',
32
+ 'URL の中身は KanseiLINK の通信の記録(Cloudflare)を通ります。秘密は入れないでください。',
33
+ ];
34
+ export const NOTICE_EN = [
35
+ 'Nothing is sent yet: the page opens with these fields filled, and only your click there sends them.',
36
+ 'The URL passes through KanseiLINK\'s request logs (Cloudflare). Do not put secrets in it.',
37
+ ];
38
+ // ── Secret shapes ──────────────────────────────────────────────────────────────
39
+ // Ported from KanseiLINK `src/utils/secret-mask.ts` (kansei-link/kansei-internal @ f39362a,
40
+ // file last changed in 5cdd578), copied 2026-10-30. Same TENANT_HOSTS list as the draft page
41
+ // (public/agents/note/new.html, which is itself a port of secret-mask.ts). Keep in sync.
42
+ //
43
+ // Differences, because here a match BLOCKS instead of masking:
44
+ // - Added: IP addresses, local paths (C:\, /Users/, /home/), bare tenant hosts without a scheme.
45
+ // - Long base64-ish blobs from 32 chars (the mask uses 40), but only when the run mixes letters
46
+ // and digits — so long error codes like PERMISSION_DENIED_INSUFFICIENT_SCOPES pass.
47
+ // - Bearer/Basic/Token needs a digit in the value (or 20+ chars), so "token authentication"
48
+ // in prose passes; a real credential nearly always has one.
49
+ // - A URL is blocked only when its host is a tenant of a TENANT_HOSTS service; other URLs
50
+ // (public docs) pass.
51
+ const TENANT_HOSTS = [
52
+ 'slack.com', 'atlassian.net', 'kintone.com', 'cybozu.com', 'zendesk.com', 'freshdesk.com',
53
+ 'myshopify.com', 'salesforce.com', 'my.salesforce.com', 'lightning.force.com', 'okta.com',
54
+ 'auth0.com', 'hubspot.com', 'notion.site', 'sharepoint.com', 'onmicrosoft.com', 'zoom.us',
55
+ 'monday.com', 'asana.com', 'intercom.io', 'chatwork.com', 'smarthr.jp', 'secure.freee.co.jp',
56
+ 'backlog.com', 'backlog.jp', 'box.com', 'github.io', 'supabase.co', 'vercel.app', 'railway.app',
57
+ 'herokuapp.com', 'workers.dev', 'pages.dev',
58
+ ];
59
+ const GENERIC_LABELS = new Set(['www', 'api', 'app', 'docs', 'developer', 'developers', 'accounts', 'login', 'auth', 'secure', 'status', 'help', 'support', 'cdn', 'mail']);
60
+ function tenantOf(host) {
61
+ const h = host.toLowerCase().replace(/:\d+$/, '').replace(/\.$/, '');
62
+ for (const base of TENANT_HOSTS) {
63
+ if (h.endsWith('.' + base) && h.length > base.length + 1) {
64
+ const first = h.slice(0, h.length - base.length - 1).split('.').pop() ?? '';
65
+ if (GENERIC_LABELS.has(first))
66
+ continue; // api.slack.com is the vendor, not a tenant
67
+ return base;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ const has = (re) => (s) => { re.lastIndex = 0; return re.test(s); };
73
+ const mixed = (t) => /[A-Za-z]/.test(t) && /\d/.test(t);
74
+ const FINDERS = [
75
+ {
76
+ kind: 'BEARER', ja: '認証ヘッダーの値(Bearer/Basic/Token …)',
77
+ find: (s) => {
78
+ const re = /\b(?:Bearer|Basic|Token)\s+([A-Za-z0-9\-._~+/=]{8,})/gi;
79
+ for (const m of s.matchAll(re))
80
+ if (/\d/.test(m[1]) || m[1].length >= 20)
81
+ return true;
82
+ return false;
83
+ },
84
+ },
85
+ {
86
+ kind: 'API_KEY', ja: 'API キー・トークンの形(sk-・ghp_・github_pat_・xox?-・AKIA・AIza など)',
87
+ find: has(/\b(?:sk-ant-[A-Za-z0-9\-_]{8,}|sk-[A-Za-z0-9\-_]{8,}|(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|xox[abprs]-[A-Za-z0-9\-]{8,}|(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{8,}|AKIA[A-Z0-9]{12,}|AIza[A-Za-z0-9\-_]{20,})/g),
88
+ },
89
+ {
90
+ kind: 'KV_SECRET', ja: '秘密の値の書き方(token=…・password: … など)',
91
+ find: has(/\b(access_token|refresh_token|id_token|api[_-]?key|apikey|client_secret|secret|password|passwd|token|signature|sig)(["']?\s*[=:]\s*)(?:"([^"\r\n]+)"|'([^'\r\n]+)'|[A-Za-z0-9\-._~+/%]{6,})/gi),
92
+ },
93
+ {
94
+ kind: 'JWT', ja: 'JWT の形(3 つの塊を . でつないだもの)',
95
+ find: has(/\b[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{10,}\b/g),
96
+ },
97
+ { kind: 'EMAIL', ja: 'メールアドレス', find: has(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) },
98
+ {
99
+ kind: 'TENANT_URL', ja: '会社ごとの URL・ホスト名(acme.slack.com など)',
100
+ find: (s) => {
101
+ for (const m of s.matchAll(/(?:\b[a-z][a-z0-9+.-]*:\/\/)?((?:[a-z0-9-]+\.)+[a-z]{2,})(?::\d+)?/gi)) {
102
+ if (tenantOf(m[1]))
103
+ return true;
104
+ }
105
+ return false;
106
+ },
107
+ },
108
+ {
109
+ kind: 'IP', ja: 'IP アドレス(localhost と書いてください)',
110
+ find: (s) => {
111
+ for (const m of s.matchAll(/(?<![\d.])(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?![\d.]*\d)/g)) {
112
+ if (m.slice(1, 5).every((o) => Number(o) <= 255))
113
+ return true;
114
+ }
115
+ return has(/(?<![0-9a-f:])(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,6}:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4}|::1)(?![0-9a-f:])/gi)(s);
116
+ },
117
+ },
118
+ {
119
+ kind: 'LOCAL_PATH', ja: 'PC の中の道(C:\\・/Users/・/home/)',
120
+ find: has(/(?<![A-Za-z])[A-Za-z]:[\\/]|\/(?:Users|home)\/|\\Users\\/g),
121
+ },
122
+ { kind: 'UUID', ja: 'UUID(要求・テナントの id)', find: has(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi) },
123
+ { kind: 'HEX', ja: '長い 16 進の塊(32 字以上)', find: has(/\b[0-9a-f]{32,}\b/gi) },
124
+ {
125
+ kind: 'BLOB', ja: '長い base64 などの塊(32 字以上・英字と数字が混ざったもの)',
126
+ find: (s) => {
127
+ for (const m of s.matchAll(/[A-Za-z0-9+=_\-]{32,}/g))
128
+ if (mixed(m[0]))
129
+ return true;
130
+ return false;
131
+ },
132
+ },
133
+ ];
134
+ /** Which secret shapes a text contains (kinds, in check order). Empty = clean. */
135
+ export function findSecretShapes(text) {
136
+ const out = [];
137
+ for (const f of FINDERS)
138
+ if (f.find(text))
139
+ out.push({ kind: f.kind, ja: f.ja });
140
+ return out;
141
+ }
142
+ /**
143
+ * Check the fields and build the draft URL. Pure: no I/O, no network, no DB.
144
+ * Returns problems (field + reason) instead of a URL when anything is off.
145
+ */
146
+ export function buildShareDraft(input) {
147
+ const problems = [];
148
+ const args = (input && typeof input === 'object' && !Array.isArray(input)) ? input : {};
149
+ for (const k of Object.keys(args)) {
150
+ if (!ALLOWED_KEYS.includes(k)) {
151
+ problems.push({ field: k, reason: `この欄は受けません。使える欄: ${ALLOWED_KEYS.join(', ')}` });
152
+ }
153
+ }
154
+ const str = (k) => {
155
+ const v = args[k];
156
+ if (v === undefined || v === null)
157
+ return undefined;
158
+ if (typeof v !== 'string') {
159
+ problems.push({ field: k, reason: '文字列で渡してください' });
160
+ return undefined;
161
+ }
162
+ return v.trim();
163
+ };
164
+ const service = str('service');
165
+ if (!service)
166
+ problems.push({ field: 'service', reason: '必須です(KanseiLINK の service_id、例: freee, slack)' });
167
+ else if (!SERVICE_RE.test(service))
168
+ problems.push({ field: 'service', reason: '形が違います。小文字・数字・_ . - だけで 160 字まで(例: freee, google-drive)' });
169
+ const values = {};
170
+ for (const f of TEXT_FIELDS) {
171
+ const v = str(f.key);
172
+ if (!v) {
173
+ if (f.required)
174
+ problems.push({ field: f.key, reason: '必須です' });
175
+ continue;
176
+ }
177
+ if (v.length > f.max) {
178
+ problems.push({ field: f.key, reason: `長すぎます(${v.length} 字・上限 ${f.max} 字)` });
179
+ continue;
180
+ }
181
+ values[f.key] = v;
182
+ }
183
+ const kind = str('kind');
184
+ if (!kind)
185
+ problems.push({ field: 'kind', reason: `必須です: ${KINDS.join(' / ')}(実際に動かした/文書で読んだ/試していない推測)` });
186
+ else if (!KINDS.includes(kind))
187
+ problems.push({ field: 'kind', reason: `次のどれかにしてください: ${KINDS.join(' / ')}` });
188
+ const runtime = str('runtime');
189
+ if (runtime !== undefined && runtime !== '' && !RUNTIMES.includes(runtime)) {
190
+ problems.push({ field: 'runtime', reason: `次のどれかにしてください: ${RUNTIMES.join(' / ')}` });
191
+ }
192
+ // Secret shapes — every field, including service.
193
+ const scan = [['service', service], ...Object.entries(values)];
194
+ for (const [field, v] of scan) {
195
+ if (!v)
196
+ continue;
197
+ const hits = findSecretShapes(v);
198
+ if (hits.length) {
199
+ problems.push({
200
+ field,
201
+ reason: `秘密に見える形があります: ${hits.map((h) => h.ja).join('・')}。値は書かず、種類だけを書いてください(例: 「401 invalid_token」「テナントの URL」)`,
202
+ kinds: hits.map((h) => h.kind),
203
+ });
204
+ }
205
+ }
206
+ if (problems.length) {
207
+ return { ok: false, problems, hint: 'リンクは作っていません。上の欄を直して、もう一度呼んでください。' };
208
+ }
209
+ const q = new URLSearchParams();
210
+ q.set('service', service);
211
+ for (const f of TEXT_FIELDS)
212
+ if (values[f.key])
213
+ q.set(f.key, values[f.key]);
214
+ q.set('kind', kind);
215
+ if (runtime)
216
+ q.set('runtime', runtime);
217
+ const url = `${DRAFT_PAGE}?${q.toString()}`;
218
+ if (url.length > URL_MAX) {
219
+ const sizes = TEXT_FIELDS
220
+ .filter((f) => values[f.key])
221
+ .map((f) => ({ field: f.key, encoded: encodeURIComponent(values[f.key]).length }))
222
+ .sort((a, b) => b.encoded - a.encoded);
223
+ return {
224
+ ok: false,
225
+ problems: [{
226
+ field: sizes[0]?.field ?? 'tried',
227
+ reason: `リンクが長すぎます(${url.length} 字・上限 ${URL_MAX} 字)。短くしてください。日本語は 1 字が URL で 9 字になります。長い順: ${sizes.map((s) => `${s.field} ${s.encoded}`).join('・')}`,
228
+ }],
229
+ hint: 'リンクは作っていません。こちらでは欄を削りません。短くして、もう一度呼んでください。',
230
+ };
231
+ }
232
+ return { ok: true, url, length: url.length, notice: NOTICE, notice_en: NOTICE_EN };
233
+ }
234
+ //# sourceMappingURL=share-draft.js.map
@@ -63,23 +63,23 @@ export function getPrompt(name, args) {
63
63
  role: 'user',
64
64
  content: {
65
65
  type: 'text',
66
- text: `You are an agent-memory writer. Read the session transcript below and propose memories to save. Output a JSON array; each item has {entity_name, entity_kind, layer, content, importance}.
67
-
68
- Layers (use exactly one per memory):
69
- - goal: WHY this work exists, target outcome
70
- - context: WHY THIS NOW, situation, timing
71
- - emotion: USER tone, feelings expressed
72
- - implementation: HOW it was done, what worked, what failed
73
- - caveat: PAIN lesson, "never X" / "always Y" — these are protected from forgetting
74
- - learning: GROWTH, decisions made, insights
75
-
76
- Rules:
77
- - Max 6 entries (one per layer). Skip layers with nothing worth saving.
78
- - Importance 0-1. Set 0.9+ to pin (use sparingly).
79
- - Caveats must be 1 sentence and start with a verb.
80
- - Quote nothing verbatim; summarize.
81
-
82
- Transcript:
66
+ text: `You are an agent-memory writer. Read the session transcript below and propose memories to save. Output a JSON array; each item has {entity_name, entity_kind, layer, content, importance}.
67
+
68
+ Layers (use exactly one per memory):
69
+ - goal: WHY this work exists, target outcome
70
+ - context: WHY THIS NOW, situation, timing
71
+ - emotion: USER tone, feelings expressed
72
+ - implementation: HOW it was done, what worked, what failed
73
+ - caveat: PAIN lesson, "never X" / "always Y" — these are protected from forgetting
74
+ - learning: GROWTH, decisions made, insights
75
+
76
+ Rules:
77
+ - Max 6 entries (one per layer). Skip layers with nothing worth saving.
78
+ - Importance 0-1. Set 0.9+ to pin (use sparingly).
79
+ - Caveats must be 1 sentence and start with a verb.
80
+ - Quote nothing verbatim; summarize.
81
+
82
+ Transcript:
83
83
  ${transcript}${entityHint}`,
84
84
  },
85
85
  },
@@ -96,15 +96,15 @@ ${transcript}${entityHint}`,
96
96
  role: 'user',
97
97
  content: {
98
98
  type: 'text',
99
- text: `You are a caveat extractor. Read the source text below and output a JSON list of caveats. Each caveat:
100
- - Starts with a verb ("Never", "Always", "Watch out", "Reject", "Confirm")
101
- - Is one sentence
102
- - Is concrete (a specific failure mode, not abstract advice)
103
- - Captures something the reader does NOT want to relearn the hard way
104
-
105
- Output format: [{"content": "Never X when Y, because Z", "importance": 0.7-1.0}]
106
-
107
- Source text:
99
+ text: `You are a caveat extractor. Read the source text below and output a JSON list of caveats. Each caveat:
100
+ - Starts with a verb ("Never", "Always", "Watch out", "Reject", "Confirm")
101
+ - Is one sentence
102
+ - Is concrete (a specific failure mode, not abstract advice)
103
+ - Captures something the reader does NOT want to relearn the hard way
104
+
105
+ Output format: [{"content": "Never X when Y, because Z", "importance": 0.7-1.0}]
106
+
107
+ Source text:
108
108
  ${text}${entityHint}`,
109
109
  },
110
110
  },
@@ -121,17 +121,17 @@ ${text}${entityHint}`,
121
121
  role: 'user',
122
122
  content: {
123
123
  type: 'text',
124
- text: `Consolidate the past week's memories for entity "${entityName}" (week_offset=${weekOffset}).
125
-
126
- Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=4000) to retrieve recent memories.
127
- Step 2: Read the returned memories and produce ONE learning-layer summary that captures:
128
- - What we set out to do (goal trajectory)
129
- - What actually happened (implementation summary)
130
- - What we learned (1-3 insights)
131
- - Any caveats we should never forget (preserve these — do NOT consolidate them away)
132
-
133
- Step 3: Output a JSON object {entity_name, layer:"learning", content, importance:0.7}.
134
-
124
+ text: `Consolidate the past week's memories for entity "${entityName}" (week_offset=${weekOffset}).
125
+
126
+ Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=4000) to retrieve recent memories.
127
+ Step 2: Read the returned memories and produce ONE learning-layer summary that captures:
128
+ - What we set out to do (goal trajectory)
129
+ - What actually happened (implementation summary)
130
+ - What we learned (1-3 insights)
131
+ - Any caveats we should never forget (preserve these — do NOT consolidate them away)
132
+
133
+ Step 3: Output a JSON object {entity_name, layer:"learning", content, importance:0.7}.
134
+
135
135
  Do not write to memory directly — just return the JSON. The user will choose whether to save.`,
136
136
  },
137
137
  },
@@ -151,15 +151,15 @@ Do not write to memory directly — just return the JSON. The user will choose w
151
151
  role: 'user',
152
152
  content: {
153
153
  type: 'text',
154
- text: `Before doing this task, recall first.
155
-
156
- Task: ${task}
157
-
158
- Step 1: Call ${recallCmd}.
159
- Step 2: Skim the returned memories. Identify any caveats that apply.
160
- Step 3: Produce your output with INLINE citations to relevant memory_ids: e.g. "Use better-sqlite3 v12+ [memory:1234] because v11 breaks on Node 24 [memory:5678]."
161
- Step 4: If you found NO relevant memories, say so explicitly: "No prior memories on this — proceeding from first principles."
162
-
154
+ text: `Before doing this task, recall first.
155
+
156
+ Task: ${task}
157
+
158
+ Step 1: Call ${recallCmd}.
159
+ Step 2: Skim the returned memories. Identify any caveats that apply.
160
+ Step 3: Produce your output with INLINE citations to relevant memory_ids: e.g. "Use better-sqlite3 v12+ [memory:1234] because v11 breaks on Node 24 [memory:5678]."
161
+ Step 4: If you found NO relevant memories, say so explicitly: "No prior memories on this — proceeding from first principles."
162
+
163
163
  Goal: never solve a problem twice without checking.`,
164
164
  },
165
165
  },
@@ -176,18 +176,18 @@ Goal: never solve a problem twice without checking.`,
176
176
  role: 'user',
177
177
  content: {
178
178
  type: 'text',
179
- text: `Produce a handoff document for entity "${entityName}", aimed at: ${audience}.
180
-
181
- Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=6000).
182
- Step 2: Skim memories grouped by layer.
183
- Step 3: Output a markdown doc with these sections:
184
- - **Identity**: name, kind, canonical key (if any)
185
- - **Goal** (from goal-layer memories): what this entity is for
186
- - **State** (from latest implementation memories): where things stand right now
187
- - **Caveats** (from caveat-layer memories): what NEVER to do, with memory_id citations
188
- - **Open questions**: things the prior session left unresolved
189
- - **Suggested next steps**: 3 concrete actions for the audience
190
-
179
+ text: `Produce a handoff document for entity "${entityName}", aimed at: ${audience}.
180
+
181
+ Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=6000).
182
+ Step 2: Skim memories grouped by layer.
183
+ Step 3: Output a markdown doc with these sections:
184
+ - **Identity**: name, kind, canonical key (if any)
185
+ - **Goal** (from goal-layer memories): what this entity is for
186
+ - **State** (from latest implementation memories): where things stand right now
187
+ - **Caveats** (from caveat-layer memories): what NEVER to do, with memory_id citations
188
+ - **Open questions**: things the prior session left unresolved
189
+ - **Suggested next steps**: 3 concrete actions for the audience
190
+
191
191
  Keep it under 1 page. Cite memory_ids inline like [memory:1234].`,
192
192
  },
193
193
  },
@@ -71,10 +71,10 @@ function fmtMemory(row) {
71
71
  access_count: row.access_count,
72
72
  };
73
73
  }
74
- const SELECT_MEMORY_BASE = `
75
- SELECT m.id, m.layer, m.content, m.importance, m.protected, m.created_at, m.last_accessed_at, m.access_count,
76
- e.name as entity_name, e.kind as entity_kind
77
- FROM memories m JOIN entities e ON e.id = m.entity_id
74
+ const SELECT_MEMORY_BASE = `
75
+ SELECT m.id, m.layer, m.content, m.importance, m.protected, m.created_at, m.last_accessed_at, m.access_count,
76
+ e.name as entity_name, e.kind as entity_kind
77
+ FROM memories m JOIN entities e ON e.id = m.entity_id
78
78
  `;
79
79
  export function readResource(db, uri) {
80
80
  // Static endpoints
@@ -101,9 +101,9 @@ export function readResource(db, uri) {
101
101
  // heat_band is dynamic; approximate "hot" via access_count + recent access desc.
102
102
  // For exact heat scoring, use the recall tool with band='hot'.
103
103
  const rows = db
104
- .prepare(`${SELECT_MEMORY_BASE}
105
- WHERE m.last_accessed_at IS NOT NULL
106
- ORDER BY m.access_count DESC, m.last_accessed_at DESC
104
+ .prepare(`${SELECT_MEMORY_BASE}
105
+ WHERE m.last_accessed_at IS NOT NULL
106
+ ORDER BY m.access_count DESC, m.last_accessed_at DESC
107
107
  LIMIT 50`)
108
108
  .all();
109
109
  return {
@@ -38,15 +38,15 @@ export async function sampleConsolidation(server, memoryTexts, entityName) {
38
38
  if (memoryTexts.length === 0)
39
39
  return { ok: false, reason: 'no input memories' };
40
40
  const numbered = memoryTexts.map((t, i) => `[${i + 1}] ${t}`).join('\n\n');
41
- const userPrompt = `Consolidate these ${memoryTexts.length} memories about "${entityName}" into a single learning-layer summary. The summary must:
42
- - Preserve the trajectory (what was attempted, what worked, what changed)
43
- - Be 2-4 sentences max
44
- - Not invent facts not in the originals
45
- - Not include verbatim quotes
46
-
47
- Memories:
48
- ${numbered}
49
-
41
+ const userPrompt = `Consolidate these ${memoryTexts.length} memories about "${entityName}" into a single learning-layer summary. The summary must:
42
+ - Preserve the trajectory (what was attempted, what worked, what changed)
43
+ - Be 2-4 sentences max
44
+ - Not invent facts not in the originals
45
+ - Not include verbatim quotes
46
+
47
+ Memories:
48
+ ${numbered}
49
+
50
50
  Output the summary text only — no preamble, no JSON wrapper.`;
51
51
  return sample(server, {
52
52
  systemPrompt: 'You are a memory consolidator. Compress without distorting.',
@@ -56,17 +56,17 @@ Output the summary text only — no preamble, no JSON wrapper.`;
56
56
  });
57
57
  }
58
58
  export async function sampleLayerClassification(server, content) {
59
- const userPrompt = `Classify this memory content into exactly one layer:
60
- - goal: WHY this work exists, target outcome
61
- - context: WHY THIS NOW, situation, timing
62
- - emotion: USER tone, feelings expressed
63
- - implementation: HOW it was done, what worked, what failed
64
- - caveat: PAIN lesson, "never X" / "always Y"
65
- - learning: GROWTH, decisions made, insights
66
-
67
- Content:
68
- ${content}
69
-
59
+ const userPrompt = `Classify this memory content into exactly one layer:
60
+ - goal: WHY this work exists, target outcome
61
+ - context: WHY THIS NOW, situation, timing
62
+ - emotion: USER tone, feelings expressed
63
+ - implementation: HOW it was done, what worked, what failed
64
+ - caveat: PAIN lesson, "never X" / "always Y"
65
+ - learning: GROWTH, decisions made, insights
66
+
67
+ Content:
68
+ ${content}
69
+
70
70
  Output just the layer name — no explanation, no JSON.`;
71
71
  return sample(server, {
72
72
  systemPrompt: 'You are a layer classifier. Output one word.',