dsh-memory-recall 0.1.0 → 0.2.1
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 +15 -10
- package/dsh.plugin.json +2 -2
- package/lib/client.js +1490 -1090
- package/package.json +2 -2
- package/src/index.js +217 -156
- package/src/inject.js +87 -0
- package/src/memory-tool.js +167 -136
- package/src/routes.js +205 -153
- package/src/snapshot.js +71 -63
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-recall",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Persistent memory for dsh in the style of Hermes Agent: a memory tool that keeps a user profile and the model's own notes
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Persistent memory for dsh in the style of Hermes Agent: a memory tool that keeps a user profile and the model's own notes, delivered to every chat as a logged context message — at the start, after a change and after compaction — and a session_search tool that reads earlier chats only after the user approves, with a Memory panel to see and edit what is kept.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
package/src/index.js
CHANGED
|
@@ -1,156 +1,217 @@
|
|
|
1
|
-
// dsh-memory-recall — Hermes Agent-style memory for dsh.
|
|
2
|
-
//
|
|
3
|
-
// memory the model saves, corrects and forgets facts in USER.md and
|
|
4
|
-
// MEMORY.md; every new chat starts with them in its prompt
|
|
5
|
-
// session_search the model looks things up in earlier chats — after the
|
|
6
|
-
// user allows it, through dsh's own approval prompt
|
|
7
|
-
//
|
|
8
|
-
// Data lives in ~/.dsh/dsh-memory-recall/. Nothing leaves the machine.
|
|
9
|
-
//
|
|
10
|
-
// Config (all optional, set on this plugin's row in cordis.patch.yml):
|
|
11
|
-
// dir where USER.md and MEMORY.md live
|
|
12
|
-
// limits: {memory, user} character budgets (default 2200 / 1375)
|
|
13
|
-
// recall: false drop session_search, keep memory
|
|
14
|
-
// askBeforeRecall: false read earlier chats without asking (default: ask)
|
|
15
|
-
|
|
16
|
-
import { homedir } from 'node:os';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { MemoryStore } from './store.js';
|
|
19
|
-
import { SECTION_NAME, SECTION_ORDER, renderSection } from './snapshot.js';
|
|
20
|
-
import { createMemoryTool } from './memory-tool.js';
|
|
21
|
-
import { createRecall } from './recall.js';
|
|
22
|
-
import { createRecallTool } from './recall-tool.js';
|
|
23
|
-
import { createRecallLog, registerRoutes } from './routes.js';
|
|
24
|
-
import { createChatIndex } from './chats.js';
|
|
25
|
-
import { loadDefineTool } from './dsh-tools.js';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* @param {object}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
tooling.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
1
|
+
// dsh-memory-recall — Hermes Agent-style memory for dsh.
|
|
2
|
+
//
|
|
3
|
+
// memory the model saves, corrects and forgets facts in USER.md and
|
|
4
|
+
// MEMORY.md; every new chat starts with them in its prompt
|
|
5
|
+
// session_search the model looks things up in earlier chats — after the
|
|
6
|
+
// user allows it, through dsh's own approval prompt
|
|
7
|
+
//
|
|
8
|
+
// Data lives in ~/.dsh/dsh-memory-recall/. Nothing leaves the machine.
|
|
9
|
+
//
|
|
10
|
+
// Config (all optional, set on this plugin's row in cordis.patch.yml):
|
|
11
|
+
// dir where USER.md and MEMORY.md live
|
|
12
|
+
// limits: {memory, user} character budgets (default 2200 / 1375)
|
|
13
|
+
// recall: false drop session_search, keep memory
|
|
14
|
+
// askBeforeRecall: false read earlier chats without asking (default: ask)
|
|
15
|
+
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { MemoryStore } from './store.js';
|
|
19
|
+
import { SECTION_NAME, SECTION_ORDER, renderSection } from './snapshot.js';
|
|
20
|
+
import { createMemoryTool } from './memory-tool.js';
|
|
21
|
+
import { createRecall } from './recall.js';
|
|
22
|
+
import { createRecallTool } from './recall-tool.js';
|
|
23
|
+
import { createBriefings, createRecallLog, registerRoutes } from './routes.js';
|
|
24
|
+
import { createChatIndex } from './chats.js';
|
|
25
|
+
import { loadDefineTool } from './dsh-tools.js';
|
|
26
|
+
import { injectionReason, memoryMessage, pendingMemory } from './inject.js';
|
|
27
|
+
import { digestOf } from './routes.js';
|
|
28
|
+
|
|
29
|
+
export const name = 'dsh-memory-recall';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Required services only. The session query, approval and system-prompt
|
|
33
|
+
* services are resolved per use with `get`, so a composition without one
|
|
34
|
+
* loses that feature rather than failing to load the plugin.
|
|
35
|
+
*/
|
|
36
|
+
export const inject = ['webServer', 'tools'];
|
|
37
|
+
|
|
38
|
+
/** Look a service up without throwing when it is absent. */
|
|
39
|
+
function service(ctx, id) {
|
|
40
|
+
try {
|
|
41
|
+
return ctx?.get?.(id);
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A user's own chat — not a sub-agent the model started. */
|
|
48
|
+
function isTopLevel(agent) {
|
|
49
|
+
const header = agent?.session?.header;
|
|
50
|
+
return !header || (!header.parentSession && header.origin !== 'subagent');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {object} ctx host plugin context
|
|
55
|
+
* @param {object} [config]
|
|
56
|
+
* @param {object} [deps] test hooks: defineTool, loadDefineTool
|
|
57
|
+
*/
|
|
58
|
+
export async function apply(ctx, config = {}, deps = {}) {
|
|
59
|
+
const dir = config.dir || join(homedir(), '.dsh', 'dsh-memory-recall');
|
|
60
|
+
const store = new MemoryStore({ dir, limits: config.limits });
|
|
61
|
+
await store.load();
|
|
62
|
+
|
|
63
|
+
const log = createRecallLog();
|
|
64
|
+
const grants = new Map();
|
|
65
|
+
// Every chat, grouped by workspace: the @ menu's list, and what a
|
|
66
|
+
// `workspace` given to session_search resolves against.
|
|
67
|
+
const chatIndex = createChatIndex({
|
|
68
|
+
getQuery: () => service(ctx, 'sessionQuery'),
|
|
69
|
+
getWorkspaces: () => service(ctx, 'workspaceRegistry'),
|
|
70
|
+
});
|
|
71
|
+
const cwdOf = async (sessionId) => {
|
|
72
|
+
try {
|
|
73
|
+
const rows = await service(ctx, 'sessionQuery')?.filterSessions?.([{ kind: 'id', values: [sessionId] }]);
|
|
74
|
+
return rows?.[0]?.header?.cwd ?? null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const disposers = [];
|
|
80
|
+
// Whether the model actually got its tools. The prompt and the Memory
|
|
81
|
+
// panel both read this, so neither can promise a tool that is not there.
|
|
82
|
+
const tooling = { ok: false, error: null, tools: [] };
|
|
83
|
+
|
|
84
|
+
// ---- the model's tools ----------------------------------------------
|
|
85
|
+
if (ctx.tools?.register) {
|
|
86
|
+
try {
|
|
87
|
+
const defineTool = deps.defineTool ?? (await (deps.loadDefineTool ?? loadDefineTool)());
|
|
88
|
+
const tools = [createMemoryTool({ store, defineTool })];
|
|
89
|
+
if (config.recall !== false) {
|
|
90
|
+
const recall = createRecall({ getQuery: () => service(ctx, 'sessionQuery'), options: config.recallOptions });
|
|
91
|
+
tools.push(createRecallTool({
|
|
92
|
+
recall,
|
|
93
|
+
defineTool,
|
|
94
|
+
getApproval: () => service(ctx, 'approval'),
|
|
95
|
+
grants,
|
|
96
|
+
log,
|
|
97
|
+
askFirst: config.askBeforeRecall !== false,
|
|
98
|
+
resolveWorkspace: (name) => chatIndex.resolveWorkspace(name),
|
|
99
|
+
listWorkspaces: async () => (await chatIndex.list()).workspaces,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
for (const tool of tools) {
|
|
103
|
+
const dispose = ctx.tools.register(tool);
|
|
104
|
+
if (typeof dispose === 'function') disposers.push(dispose);
|
|
105
|
+
tooling.tools.push(tool.name);
|
|
106
|
+
}
|
|
107
|
+
tooling.ok = true;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
tooling.error = String(error?.message ?? error);
|
|
110
|
+
ctx.logger?.warn?.(`[dsh-memory-recall] tools not registered: ${tooling.error}`);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
tooling.error = 'this dsh has no tool registry';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- each new chat: memory guidance in its prompt, frozen for its
|
|
117
|
+
// lifetime. dsh 0.1.7 removed `agent/session-start`; `agent/created` and
|
|
118
|
+
// `agent/disposed` exist on every dsh from 0.1.0-rc.7 through 0.2.x, so
|
|
119
|
+
// they are the seam — session-start stays wired as a back-compat path,
|
|
120
|
+
// deduped by the same map.
|
|
121
|
+
/** Agent contexts already carrying the section -> the registering fiber. */
|
|
122
|
+
const briefed = new WeakMap();
|
|
123
|
+
// sessionId -> what that chat's prompt was given, for the composer chip.
|
|
124
|
+
const briefings = createBriefings();
|
|
125
|
+
|
|
126
|
+
const installGuidance = (agent) => {
|
|
127
|
+
const agentCtx = agent?.ctx;
|
|
128
|
+
if (!agentCtx || briefed.has(agentCtx) || !isTopLevel(agent)) return;
|
|
129
|
+
try {
|
|
130
|
+
// Read now, once: this text is what the chat keeps, so a save made later
|
|
131
|
+
// in it never rewrites its prompt (which would cost the cached prefix).
|
|
132
|
+
// The guidance only: fixed text, so the cached prompt prefix holds.
|
|
133
|
+
const text = renderSection(null, store.limits, { tools: tooling.ok });
|
|
134
|
+
// `inject` rather than `get`: at agent/created the prompt service may not
|
|
135
|
+
// be resolvable yet; the callback runs once it is, and the fiber's
|
|
136
|
+
// dispose() takes the section back out.
|
|
137
|
+
const fiber = agentCtx.inject?.(['systemPrompt'], (scope) => {
|
|
138
|
+
if (!isTopLevel(agent)) return; // the header may have attached since created
|
|
139
|
+
try {
|
|
140
|
+
scope.systemPrompt?.section?.({ name: SECTION_NAME, order: SECTION_ORDER, text });
|
|
141
|
+
} catch {
|
|
142
|
+
// Already registered (e.g. after a plugin reload): a duplicate name throws.
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
if (fiber) briefed.set(agentCtx, fiber);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Losing the guidance costs this chat its memory, never the chat.
|
|
148
|
+
ctx.logger?.warn?.(`[dsh-memory-recall] no memory for this chat: ${error?.message ?? error}`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const uninstallGuidance = (agent) => {
|
|
153
|
+
const agentCtx = agent?.ctx;
|
|
154
|
+
const fiber = agentCtx && briefed.get(agentCtx);
|
|
155
|
+
if (!fiber) return;
|
|
156
|
+
briefed.delete(agentCtx);
|
|
157
|
+
try {
|
|
158
|
+
fiber.dispose?.();
|
|
159
|
+
} catch {
|
|
160
|
+
// Teardown must not throw.
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Agents that already exist when this plugin mounts (a reload).
|
|
165
|
+
ctx.inject?.(['agents'], (scope) => {
|
|
166
|
+
for (const agent of scope.agents?.list?.() ?? []) installGuidance(agent);
|
|
167
|
+
});
|
|
168
|
+
ctx.on?.('agent/created', (payload) => installGuidance(payload?.agent));
|
|
169
|
+
ctx.on?.('agent/disposed', (payload) => uninstallGuidance(payload?.agent));
|
|
170
|
+
// Older hosts fire this instead of agent/created; the map dedupes.
|
|
171
|
+
ctx.on?.('agent/session-start', (payload) => installGuidance(payload?.agent));
|
|
172
|
+
|
|
173
|
+
// ---- the memory itself: a logged context message, whenever the chat
|
|
174
|
+
// cannot see the current version (start, after compaction, after a change).
|
|
175
|
+
ctx.on?.('agent/pre-step', async (payload, next) => {
|
|
176
|
+
const decision = await next();
|
|
177
|
+
const agent = payload?.agent;
|
|
178
|
+
if (!decision || decision.kind === 'reject' || !agent?.session || !isTopLevel(agent)) return decision;
|
|
179
|
+
try {
|
|
180
|
+
const stores = await store.load();
|
|
181
|
+
const reason = injectionReason(agent.session, digestOf(stores));
|
|
182
|
+
const pending = pendingMemory(decision.messages);
|
|
183
|
+
if (!reason) {
|
|
184
|
+
return pending ? { ...decision, messages: decision.messages.filter((m) => m !== pending) } : decision;
|
|
185
|
+
}
|
|
186
|
+
const message = memoryMessage(stores, store.limits, reason);
|
|
187
|
+
if (agent.id) briefings.record(agent.id, stores, reason);
|
|
188
|
+
const messages = decision.messages ?? [];
|
|
189
|
+
return { ...decision, messages: pending ? messages.map((m) => (m === pending ? message : m)) : [...messages, message] };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// Memory that cannot be read costs this step its memory, never the step.
|
|
192
|
+
ctx.logger?.warn?.(`[dsh-memory-recall] memory not delivered: ${error?.message ?? error}`);
|
|
193
|
+
return decision;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// One "allow" lasts until the reply it was given in ends.
|
|
198
|
+
ctx.on?.('agent/status', (payload) => {
|
|
199
|
+
if (payload?.status === 'idle' && payload.agent?.id) grants.delete(payload.agent.id);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ---- the browser half ----------------------------------------------
|
|
203
|
+
if (ctx.webServer?.register) {
|
|
204
|
+
disposers.push(registerRoutes(ctx.webServer, { store, log, chatIndex, cwdOf, tooling, briefings }));
|
|
205
|
+
} else {
|
|
206
|
+
ctx.logger?.warn?.('[dsh-memory-recall] webServer unavailable; the Memory panel and cards will be empty');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
ctx.on?.('dispose', () => {
|
|
210
|
+
for (const dispose of disposers) {
|
|
211
|
+
try { dispose(); } catch { /* keep disposing the rest */ }
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// cordis treats a returned value as an effect disposer; return nothing.
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default { apply, inject, name };
|
package/src/inject.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Memory as a logged context message, the way dsh delivers its skill catalog.
|
|
2
|
+
//
|
|
3
|
+
// Before each model step (`agent/pre-step`), memory is appended to the step's
|
|
4
|
+
// new messages when the chat does not already SEE the current version:
|
|
5
|
+
//
|
|
6
|
+
// start the chat has never been given memory
|
|
7
|
+
// compaction it was given memory, but that message is no longer on the
|
|
8
|
+
// visible surface — the conversation was compacted past it
|
|
9
|
+
// update the memory it can see is older than what is on disk
|
|
10
|
+
//
|
|
11
|
+
// dsh logs each one as a "Context injection · memory · <summary>" row the user
|
|
12
|
+
// can expand, and the model gets the current memory in the conversation itself,
|
|
13
|
+
// next to the work — no tool call, nothing the model has to remember to do.
|
|
14
|
+
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { digestOf } from './routes.js';
|
|
17
|
+
import { renderBlock } from './snapshot.js';
|
|
18
|
+
|
|
19
|
+
/** The producer label dsh shows on the row ("Context injection · memory"). */
|
|
20
|
+
export const INJECT_LABEL = 'memory';
|
|
21
|
+
|
|
22
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
23
|
+
|
|
24
|
+
/** "2 profile · 5 notes", the counts part of the row's summary. */
|
|
25
|
+
export function countsText(stores) {
|
|
26
|
+
return `${(stores.user ?? []).length} profile · ${plural((stores.memory ?? []).length, 'note', 'notes')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Is this logged message one of ours? */
|
|
30
|
+
function isMemorySource(source) {
|
|
31
|
+
return source?.kind === 'plugin' && source?.plugin === INJECT_LABEL;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Why the chat needs memory now, or null when it already sees the current one.
|
|
36
|
+
* @param {object} session agent.session: seq, eventAt(i), surface.nodes
|
|
37
|
+
* @param {string} digest digestOf(the stores on disk)
|
|
38
|
+
*/
|
|
39
|
+
export function injectionReason(session, digest) {
|
|
40
|
+
const visible = new Set(session?.surface?.nodes ?? []);
|
|
41
|
+
let seen = false;
|
|
42
|
+
for (let index = (session?.seq ?? 0) - 1; index >= 0; index -= 1) {
|
|
43
|
+
const event = session.eventAt(index);
|
|
44
|
+
if (event?.type !== 'user/message' || !isMemorySource(event.data?.source)) continue;
|
|
45
|
+
seen = true;
|
|
46
|
+
if (!visible.has(event.seq)) continue; // compacted away; an older one may still be visible
|
|
47
|
+
return event.data.source.digest === digest ? null : 'update';
|
|
48
|
+
}
|
|
49
|
+
return seen ? 'compaction' : 'start';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Our message among a step's pending messages, if one is already there. */
|
|
53
|
+
export function pendingMemory(messages = []) {
|
|
54
|
+
return messages.find((m) => isMemorySource(m?.source)) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const LEAD = {
|
|
58
|
+
start: 'Your persistent memory, carried over from earlier chats.',
|
|
59
|
+
compaction: 'Your persistent memory again — the conversation was compacted, so here it is in full.',
|
|
60
|
+
update: 'Your persistent memory changed. This is the current version; it replaces any earlier memory message.',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The context message: what the model reads, and the source dsh logs and labels.
|
|
65
|
+
* @param {{user: string[], memory: string[]}} stores
|
|
66
|
+
* @param {{user: number, memory: number}} limits
|
|
67
|
+
* @param {'start'|'compaction'|'update'} reason
|
|
68
|
+
*/
|
|
69
|
+
export function memoryMessage(stores, limits, reason, id = randomUUID()) {
|
|
70
|
+
const blocks = [renderBlock('user', stores.user ?? [], limits.user), renderBlock('memory', stores.memory ?? [], limits.memory)].filter(Boolean);
|
|
71
|
+
const text = [
|
|
72
|
+
'<system-reminder>',
|
|
73
|
+
LEAD[reason] ?? LEAD.start,
|
|
74
|
+
'Use it without being asked: follow the preferences, rely on the facts and conventions instead of rediscovering them or asking',
|
|
75
|
+
'again, and apply the lessons. Do not announce that you read it.',
|
|
76
|
+
'',
|
|
77
|
+
blocks.length ? blocks.join('\n\n') : '(Nothing is saved yet.)',
|
|
78
|
+
'</system-reminder>',
|
|
79
|
+
].join('\n');
|
|
80
|
+
const summary = (reason === 'compaction' ? 'after compaction · ' : reason === 'update' ? 'updated · ' : '') + countsText(stores);
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
id,
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: Object.freeze([Object.freeze({ type: 'text', text })]),
|
|
85
|
+
source: Object.freeze({ kind: 'plugin', plugin: INJECT_LABEL, form: 'notice', summary, reason, digest: digestOf(stores) }),
|
|
86
|
+
});
|
|
87
|
+
}
|