@oyasmi/pipiclaw 0.6.3 → 0.6.5
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 +12 -4
- package/dist/agent/channel-runner.d.ts +3 -0
- package/dist/agent/channel-runner.js +51 -0
- package/dist/agent/commands.js +3 -1
- package/dist/agent/prompt-builder.js +4 -0
- package/dist/agent/session-events.d.ts +1 -0
- package/dist/agent/session-events.js +13 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/memory/channel-maintenance-queue.d.ts +5 -0
- package/dist/memory/channel-maintenance-queue.js +8 -0
- package/dist/memory/consolidation.d.ts +12 -4
- package/dist/memory/consolidation.js +54 -23
- package/dist/memory/files.js +8 -14
- package/dist/memory/lifecycle.d.ts +8 -14
- package/dist/memory/lifecycle.js +66 -111
- package/dist/memory/maintenance-gates.d.ts +56 -0
- package/dist/memory/maintenance-gates.js +161 -0
- package/dist/memory/maintenance-jobs.d.ts +52 -0
- package/dist/memory/maintenance-jobs.js +310 -0
- package/dist/memory/maintenance-state.d.ts +33 -0
- package/dist/memory/maintenance-state.js +113 -0
- package/dist/memory/post-turn-review.d.ts +32 -0
- package/dist/memory/post-turn-review.js +244 -0
- package/dist/memory/promotion-signals.d.ts +5 -0
- package/dist/memory/promotion-signals.js +34 -0
- package/dist/memory/promotion.d.ts +32 -0
- package/dist/memory/promotion.js +11 -0
- package/dist/memory/recall.d.ts +1 -1
- package/dist/memory/recall.js +33 -1
- package/dist/memory/review-log.d.ts +13 -0
- package/dist/memory/review-log.js +38 -0
- package/dist/memory/scheduler.d.ts +52 -0
- package/dist/memory/scheduler.js +152 -0
- package/dist/memory/session-corpus.d.ts +18 -0
- package/dist/memory/session-corpus.js +257 -0
- package/dist/memory/session-search.d.ts +30 -0
- package/dist/memory/session-search.js +151 -0
- package/dist/runtime/bootstrap.d.ts +5 -0
- package/dist/runtime/bootstrap.js +39 -2
- package/dist/runtime/delivery.js +52 -3
- package/dist/runtime/dingtalk.d.ts +11 -1
- package/dist/runtime/dingtalk.js +40 -3
- package/dist/runtime/events.js +5 -0
- package/dist/settings.d.ts +35 -1
- package/dist/settings.js +55 -1
- package/dist/shared/atomic-file.d.ts +2 -0
- package/dist/shared/atomic-file.js +17 -0
- package/dist/shared/serial-queue.d.ts +4 -0
- package/dist/shared/serial-queue.js +17 -0
- package/dist/tools/config.d.ts +10 -0
- package/dist/tools/config.js +28 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +32 -0
- package/dist/tools/session-search.d.ts +17 -0
- package/dist/tools/session-search.js +56 -0
- package/dist/tools/skill-list.d.ts +17 -0
- package/dist/tools/skill-list.js +86 -0
- package/dist/tools/skill-manage.d.ts +34 -0
- package/dist/tools/skill-manage.js +138 -0
- package/dist/tools/skill-security.d.ts +10 -0
- package/dist/tools/skill-security.js +111 -0
- package/dist/tools/skill-view.d.ts +12 -0
- package/dist/tools/skill-view.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { buildStandardMessages } from "../shared/type-guards.js";
|
|
2
|
+
import { getDefaultChannelMemoryQueue } from "./channel-maintenance-queue.js";
|
|
3
|
+
import { cleanupChannelMemory, foldChannelHistory, getStructuralMaintenanceStats, runInlineConsolidation, } from "./consolidation.js";
|
|
4
|
+
import { readChannelHistory, readChannelMemory } from "./files.js";
|
|
5
|
+
import { shouldRunDurableConsolidation, shouldRunGrowthReview, shouldRunSessionRefresh, shouldRunStructuralMaintenance, } from "./maintenance-gates.js";
|
|
6
|
+
import { readMemoryMaintenanceState, updateMemoryMaintenanceState } from "./maintenance-state.js";
|
|
7
|
+
import { runPostTurnReview } from "./post-turn-review.js";
|
|
8
|
+
import { scanPromotionSignals } from "./promotion-signals.js";
|
|
9
|
+
import { appendMemoryReviewLog } from "./review-log.js";
|
|
10
|
+
import { updateChannelSessionMemory } from "./session.js";
|
|
11
|
+
function latestEntryId(entries) {
|
|
12
|
+
return entries.at(-1)?.id;
|
|
13
|
+
}
|
|
14
|
+
function entriesSince(entries, lastEntryId) {
|
|
15
|
+
if (!lastEntryId) {
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
18
|
+
const index = entries.findIndex((entry) => entry.id === lastEntryId);
|
|
19
|
+
return index >= 0 ? entries.slice(index + 1) : entries;
|
|
20
|
+
}
|
|
21
|
+
function messageToText(message) {
|
|
22
|
+
if (message.role === "user") {
|
|
23
|
+
return typeof message.content === "string"
|
|
24
|
+
? message.content
|
|
25
|
+
: message.content.map((part) => (part.type === "text" ? part.text : "[image]")).join("\n");
|
|
26
|
+
}
|
|
27
|
+
if (message.role === "assistant") {
|
|
28
|
+
return message.content
|
|
29
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join("\n");
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
function hasMeaningfulMessages(messages) {
|
|
36
|
+
const standardMessages = buildStandardMessages(messages);
|
|
37
|
+
let userSeen = false;
|
|
38
|
+
let assistantSeen = false;
|
|
39
|
+
for (const message of standardMessages) {
|
|
40
|
+
const text = messageToText(message).trim();
|
|
41
|
+
if (!text) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (message.role === "user") {
|
|
45
|
+
userSeen = true;
|
|
46
|
+
}
|
|
47
|
+
if (message.role === "assistant") {
|
|
48
|
+
assistantSeen = true;
|
|
49
|
+
}
|
|
50
|
+
if (userSeen && assistantSeen) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function renderMessagesForSignalScan(messages) {
|
|
57
|
+
return buildStandardMessages(messages).map(messageToText).join("\n");
|
|
58
|
+
}
|
|
59
|
+
function makeRunOptions(input) {
|
|
60
|
+
return {
|
|
61
|
+
channelDir: input.channelDir,
|
|
62
|
+
model: input.model,
|
|
63
|
+
resolveApiKey: input.resolveApiKey,
|
|
64
|
+
messages: input.messages,
|
|
65
|
+
sessionEntries: input.sessionEntries,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function backoffUntil(now, settings) {
|
|
69
|
+
return new Date(now.getTime() + Math.max(0, settings.failureBackoffMinutes) * 60_000).toISOString();
|
|
70
|
+
}
|
|
71
|
+
async function appendJobReviewLog(channelDir, channelId, reason, entry, now) {
|
|
72
|
+
await appendMemoryReviewLog(channelDir, {
|
|
73
|
+
timestamp: now.toISOString(),
|
|
74
|
+
channelId,
|
|
75
|
+
reason,
|
|
76
|
+
...entry,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function skipped(jobKind, skipReason) {
|
|
80
|
+
return { jobKind, ran: false, skipped: true, skipReason };
|
|
81
|
+
}
|
|
82
|
+
function ran(jobKind) {
|
|
83
|
+
return { jobKind, ran: true, skipped: false };
|
|
84
|
+
}
|
|
85
|
+
function failed(jobKind, error) {
|
|
86
|
+
return {
|
|
87
|
+
jobKind,
|
|
88
|
+
ran: false,
|
|
89
|
+
skipped: false,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function runQueued(input, job) {
|
|
94
|
+
return (input.queue ?? getDefaultChannelMemoryQueue()).run(input.channelId, job);
|
|
95
|
+
}
|
|
96
|
+
export async function runSessionRefreshJob(input) {
|
|
97
|
+
return runQueued(input, async () => {
|
|
98
|
+
const now = input.now ?? new Date();
|
|
99
|
+
const state = await readMemoryMaintenanceState(input.appHomeDir, input.channelId);
|
|
100
|
+
const latestId = latestEntryId(input.sessionEntries);
|
|
101
|
+
const decision = shouldRunSessionRefresh({
|
|
102
|
+
now,
|
|
103
|
+
state,
|
|
104
|
+
sessionMemory: input.settings.sessionMemory,
|
|
105
|
+
maintenance: input.settings.memoryMaintenance,
|
|
106
|
+
channelActive: input.channelActive,
|
|
107
|
+
hasNewSessionEntry: latestId !== undefined && latestId !== state.lastSessionRefreshedEntryId,
|
|
108
|
+
hasMeaningfulMaterial: hasMeaningfulMessages(input.messages),
|
|
109
|
+
});
|
|
110
|
+
if (!decision.allowed) {
|
|
111
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "session-refresh-job", { skipped: [{ target: "SESSION.md", reason: decision.skipReason }] }, now);
|
|
112
|
+
return skipped(decision.jobKind, decision.skipReason ?? "skipped");
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await updateChannelSessionMemory({
|
|
116
|
+
channelDir: input.channelDir,
|
|
117
|
+
messages: input.messages,
|
|
118
|
+
model: input.model,
|
|
119
|
+
resolveApiKey: input.resolveApiKey,
|
|
120
|
+
timeoutMs: input.settings.sessionMemory.timeoutMs,
|
|
121
|
+
});
|
|
122
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
123
|
+
...current,
|
|
124
|
+
lastSessionRefreshAt: now.toISOString(),
|
|
125
|
+
turnsSinceSessionRefresh: 0,
|
|
126
|
+
toolCallsSinceSessionRefresh: 0,
|
|
127
|
+
lastSessionRefreshedEntryId: latestId ?? current.lastSessionRefreshedEntryId,
|
|
128
|
+
lastSessionEntryId: latestId ?? current.lastSessionEntryId,
|
|
129
|
+
failureBackoffUntil: null,
|
|
130
|
+
}));
|
|
131
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "session-refresh-job", { actions: [{ target: "SESSION.md", action: "rewrite" }] }, now);
|
|
132
|
+
return ran("session-refresh");
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
136
|
+
...current,
|
|
137
|
+
failureBackoffUntil: backoffUntil(now, input.settings.memoryMaintenance),
|
|
138
|
+
}));
|
|
139
|
+
const result = failed("session-refresh", error);
|
|
140
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "session-refresh-job", { error: result.error, skipped: [{ target: "SESSION.md", reason: "failed" }] }, now);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export async function runDurableConsolidationJob(input) {
|
|
146
|
+
return runQueued(input, async () => {
|
|
147
|
+
const now = input.now ?? new Date();
|
|
148
|
+
const state = await readMemoryMaintenanceState(input.appHomeDir, input.channelId);
|
|
149
|
+
const newEntries = entriesSince(input.sessionEntries, state.lastConsolidatedEntryId);
|
|
150
|
+
const latestId = latestEntryId(input.sessionEntries);
|
|
151
|
+
const decision = shouldRunDurableConsolidation({
|
|
152
|
+
now,
|
|
153
|
+
state,
|
|
154
|
+
maintenance: input.settings.memoryMaintenance,
|
|
155
|
+
channelActive: input.channelActive,
|
|
156
|
+
hasNewEntry: newEntries.length > 0,
|
|
157
|
+
hasMeaningfulExchange: hasMeaningfulMessages(input.messages),
|
|
158
|
+
batchSize: newEntries.length,
|
|
159
|
+
coveredByGrowthReview: Boolean(latestId && state.lastReviewedEntryId === latestId),
|
|
160
|
+
});
|
|
161
|
+
if (!decision.allowed) {
|
|
162
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "durable-consolidation-job", { skipped: [{ target: "consolidation", reason: decision.skipReason }] }, now);
|
|
163
|
+
return skipped(decision.jobKind, decision.skipReason ?? "skipped");
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const result = await runInlineConsolidation({
|
|
167
|
+
...makeRunOptions(input),
|
|
168
|
+
mode: "idle",
|
|
169
|
+
});
|
|
170
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
171
|
+
...current,
|
|
172
|
+
lastDurableConsolidationAt: now.toISOString(),
|
|
173
|
+
lastConsolidatedEntryId: latestId ?? current.lastConsolidatedEntryId,
|
|
174
|
+
failureBackoffUntil: null,
|
|
175
|
+
}));
|
|
176
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "durable-consolidation-job", result.skipped
|
|
177
|
+
? { skipped: [{ target: "consolidation", reason: "no meaningful snapshot" }] }
|
|
178
|
+
: { actions: [{ target: "MEMORY.md", action: "append", entries: result.appendedMemoryEntries }] }, now);
|
|
179
|
+
return ran("durable-consolidation");
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
183
|
+
...current,
|
|
184
|
+
failureBackoffUntil: backoffUntil(now, input.settings.memoryMaintenance),
|
|
185
|
+
}));
|
|
186
|
+
const result = failed("durable-consolidation", error);
|
|
187
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "durable-consolidation-job", { error: result.error, skipped: [{ target: "consolidation", reason: "failed" }] }, now);
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export async function runGrowthReviewJob(input) {
|
|
193
|
+
return runQueued(input, async () => {
|
|
194
|
+
const now = input.now ?? new Date();
|
|
195
|
+
const state = await readMemoryMaintenanceState(input.appHomeDir, input.channelId);
|
|
196
|
+
const newEntries = entriesSince(input.sessionEntries, state.lastReviewedEntryId);
|
|
197
|
+
const latestId = latestEntryId(input.sessionEntries);
|
|
198
|
+
const signalScan = scanPromotionSignals(renderMessagesForSignalScan(input.messages));
|
|
199
|
+
const decision = shouldRunGrowthReview({
|
|
200
|
+
now,
|
|
201
|
+
state,
|
|
202
|
+
memoryGrowth: input.settings.memoryGrowth,
|
|
203
|
+
maintenance: input.settings.memoryMaintenance,
|
|
204
|
+
channelActive: input.channelActive,
|
|
205
|
+
hasNewEntry: newEntries.length > 0,
|
|
206
|
+
hasMeaningfulMaterial: hasMeaningfulMessages(input.messages),
|
|
207
|
+
hasPromotionSignal: signalScan.hasSignal,
|
|
208
|
+
});
|
|
209
|
+
if (!decision.allowed) {
|
|
210
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "growth-review-job", { skipped: [{ target: "post-turn-review", reason: decision.skipReason }] }, now);
|
|
211
|
+
return skipped(decision.jobKind, decision.skipReason ?? "skipped");
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const notices = [];
|
|
215
|
+
const result = await runPostTurnReview({
|
|
216
|
+
channelId: input.channelId,
|
|
217
|
+
channelDir: input.channelDir,
|
|
218
|
+
workspaceDir: input.workspaceDir,
|
|
219
|
+
workspacePath: input.workspacePath,
|
|
220
|
+
messages: input.messages,
|
|
221
|
+
model: input.model,
|
|
222
|
+
resolveApiKey: input.resolveApiKey,
|
|
223
|
+
timeoutMs: input.settings.sessionMemory.timeoutMs,
|
|
224
|
+
autoWriteChannelMemory: input.settings.memoryGrowth.autoWriteChannelMemory,
|
|
225
|
+
autoWriteWorkspaceSkills: input.settings.memoryGrowth.autoWriteWorkspaceSkills,
|
|
226
|
+
minMemoryAutoWriteConfidence: input.settings.memoryGrowth.minMemoryAutoWriteConfidence,
|
|
227
|
+
minSkillAutoWriteConfidence: input.settings.memoryGrowth.minSkillAutoWriteConfidence,
|
|
228
|
+
loadedSkills: input.loadedSkills,
|
|
229
|
+
emitNotice: async (notice) => {
|
|
230
|
+
notices.push(notice);
|
|
231
|
+
},
|
|
232
|
+
refreshWorkspaceResources: input.refreshWorkspaceResources,
|
|
233
|
+
});
|
|
234
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
235
|
+
...current,
|
|
236
|
+
lastGrowthReviewAt: now.toISOString(),
|
|
237
|
+
turnsSinceGrowthReview: 0,
|
|
238
|
+
toolCallsSinceGrowthReview: 0,
|
|
239
|
+
lastReviewedEntryId: latestId ?? current.lastReviewedEntryId,
|
|
240
|
+
failureBackoffUntil: null,
|
|
241
|
+
}));
|
|
242
|
+
if (notices.length > 0) {
|
|
243
|
+
const uniqueNotices = Array.from(new Set(notices));
|
|
244
|
+
await input.emitNotice?.(uniqueNotices.join("\n"));
|
|
245
|
+
}
|
|
246
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "growth-review-job", {
|
|
247
|
+
actions: result.actions,
|
|
248
|
+
skipped: result.skipped,
|
|
249
|
+
}, now);
|
|
250
|
+
return ran("growth-review");
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
254
|
+
...current,
|
|
255
|
+
failureBackoffUntil: backoffUntil(now, input.settings.memoryMaintenance),
|
|
256
|
+
}));
|
|
257
|
+
const result = failed("growth-review", error);
|
|
258
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "growth-review-job", { error: result.error, skipped: [{ target: "post-turn-review", reason: "failed" }] }, now);
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
export async function runStructuralMaintenanceJob(input) {
|
|
264
|
+
return runQueued(input, async () => {
|
|
265
|
+
const now = input.now ?? new Date();
|
|
266
|
+
const state = await readMemoryMaintenanceState(input.appHomeDir, input.channelId);
|
|
267
|
+
const [currentMemory, currentHistory] = await Promise.all([
|
|
268
|
+
readChannelMemory(input.channelDir),
|
|
269
|
+
readChannelHistory(input.channelDir),
|
|
270
|
+
]);
|
|
271
|
+
const stats = getStructuralMaintenanceStats(currentMemory, currentHistory);
|
|
272
|
+
const decision = shouldRunStructuralMaintenance({
|
|
273
|
+
now,
|
|
274
|
+
state,
|
|
275
|
+
maintenance: input.settings.memoryMaintenance,
|
|
276
|
+
channelActive: input.channelActive,
|
|
277
|
+
...stats,
|
|
278
|
+
});
|
|
279
|
+
if (!decision.allowed) {
|
|
280
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "structural-maintenance-job", { skipped: [{ target: "structural-maintenance", reason: decision.skipReason }] }, now);
|
|
281
|
+
return skipped(decision.jobKind, decision.skipReason ?? "skipped");
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const options = makeRunOptions(input);
|
|
285
|
+
const cleanedMemory = decision.runMemoryCleanup ? await cleanupChannelMemory(options, currentMemory) : false;
|
|
286
|
+
const foldedHistory = decision.runHistoryFolding ? await foldChannelHistory(options, currentHistory) : false;
|
|
287
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
288
|
+
...current,
|
|
289
|
+
lastStructuralMaintenanceAt: now.toISOString(),
|
|
290
|
+
failureBackoffUntil: null,
|
|
291
|
+
}));
|
|
292
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "structural-maintenance-job", {
|
|
293
|
+
actions: [
|
|
294
|
+
...(cleanedMemory ? [{ target: "MEMORY.md", action: "rewrite" }] : []),
|
|
295
|
+
...(foldedHistory ? [{ target: "HISTORY.md", action: "rewrite" }] : []),
|
|
296
|
+
],
|
|
297
|
+
}, now);
|
|
298
|
+
return ran("structural-maintenance");
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
await updateMemoryMaintenanceState(input.appHomeDir, input.channelId, (current) => ({
|
|
302
|
+
...current,
|
|
303
|
+
failureBackoffUntil: backoffUntil(now, input.settings.memoryMaintenance),
|
|
304
|
+
}));
|
|
305
|
+
const result = failed("structural-maintenance", error);
|
|
306
|
+
await appendJobReviewLog(input.channelDir, input.channelId, "structural-maintenance-job", { error: result.error, skipped: [{ target: "structural-maintenance", reason: "failed" }] }, now);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface MemoryMaintenanceState {
|
|
2
|
+
channelId: string;
|
|
3
|
+
dirty: boolean;
|
|
4
|
+
lastActivityAt?: string;
|
|
5
|
+
eligibleAfter?: string;
|
|
6
|
+
lastSessionRefreshAt?: string;
|
|
7
|
+
lastDurableConsolidationAt?: string;
|
|
8
|
+
lastGrowthReviewAt?: string;
|
|
9
|
+
lastStructuralMaintenanceAt?: string;
|
|
10
|
+
turnsSinceSessionRefresh: number;
|
|
11
|
+
toolCallsSinceSessionRefresh: number;
|
|
12
|
+
turnsSinceGrowthReview: number;
|
|
13
|
+
toolCallsSinceGrowthReview: number;
|
|
14
|
+
lastSessionEntryId?: string;
|
|
15
|
+
lastSessionRefreshedEntryId?: string;
|
|
16
|
+
lastConsolidatedEntryId?: string;
|
|
17
|
+
lastReviewedEntryId?: string;
|
|
18
|
+
failureBackoffUntil?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export type MemoryActivityKind = "user-turn-started" | "tool-call" | "assistant-turn-completed" | "boundary";
|
|
21
|
+
export interface MemoryActivityEvent {
|
|
22
|
+
kind: MemoryActivityKind;
|
|
23
|
+
channelId: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
eligibleAfter?: string;
|
|
26
|
+
latestSessionEntryId?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function getMemoryMaintenanceStateDir(appHomeDir: string): string;
|
|
29
|
+
export declare function getMemoryMaintenanceStatePath(appHomeDir: string, channelId: string): string;
|
|
30
|
+
export declare function readMemoryMaintenanceState(appHomeDir: string, channelId: string): Promise<MemoryMaintenanceState>;
|
|
31
|
+
export declare function writeMemoryMaintenanceState(appHomeDir: string, state: MemoryMaintenanceState): Promise<void>;
|
|
32
|
+
export declare function updateMemoryMaintenanceState(appHomeDir: string, channelId: string, update: (state: MemoryMaintenanceState) => MemoryMaintenanceState): Promise<MemoryMaintenanceState>;
|
|
33
|
+
export declare function applyMemoryActivityToState(state: MemoryMaintenanceState, event: MemoryActivityEvent): MemoryMaintenanceState;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as log from "../log.js";
|
|
4
|
+
import { writeFileAtomically } from "../shared/atomic-file.js";
|
|
5
|
+
import { createSerialQueue } from "../shared/serial-queue.js";
|
|
6
|
+
const stateUpdateQueue = createSerialQueue();
|
|
7
|
+
export function getMemoryMaintenanceStateDir(appHomeDir) {
|
|
8
|
+
return join(appHomeDir, "state", "memory");
|
|
9
|
+
}
|
|
10
|
+
export function getMemoryMaintenanceStatePath(appHomeDir, channelId) {
|
|
11
|
+
return join(getMemoryMaintenanceStateDir(appHomeDir), `${channelId}.json`);
|
|
12
|
+
}
|
|
13
|
+
function createDefaultState(channelId) {
|
|
14
|
+
return {
|
|
15
|
+
channelId,
|
|
16
|
+
dirty: false,
|
|
17
|
+
turnsSinceSessionRefresh: 0,
|
|
18
|
+
toolCallsSinceSessionRefresh: 0,
|
|
19
|
+
turnsSinceGrowthReview: 0,
|
|
20
|
+
toolCallsSinceGrowthReview: 0,
|
|
21
|
+
failureBackoffUntil: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeOptionalString(value) {
|
|
25
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
function normalizeOptionalNullableString(value) {
|
|
28
|
+
if (value === null) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return normalizeOptionalString(value);
|
|
32
|
+
}
|
|
33
|
+
function normalizeCounter(value) {
|
|
34
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
35
|
+
}
|
|
36
|
+
function normalizeState(channelId, value) {
|
|
37
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
38
|
+
return createDefaultState(channelId);
|
|
39
|
+
}
|
|
40
|
+
const record = value;
|
|
41
|
+
return {
|
|
42
|
+
channelId,
|
|
43
|
+
dirty: typeof record.dirty === "boolean" ? record.dirty : false,
|
|
44
|
+
lastActivityAt: normalizeOptionalString(record.lastActivityAt),
|
|
45
|
+
eligibleAfter: normalizeOptionalString(record.eligibleAfter),
|
|
46
|
+
lastSessionRefreshAt: normalizeOptionalString(record.lastSessionRefreshAt),
|
|
47
|
+
lastDurableConsolidationAt: normalizeOptionalString(record.lastDurableConsolidationAt),
|
|
48
|
+
lastGrowthReviewAt: normalizeOptionalString(record.lastGrowthReviewAt),
|
|
49
|
+
lastStructuralMaintenanceAt: normalizeOptionalString(record.lastStructuralMaintenanceAt),
|
|
50
|
+
turnsSinceSessionRefresh: normalizeCounter(record.turnsSinceSessionRefresh),
|
|
51
|
+
toolCallsSinceSessionRefresh: normalizeCounter(record.toolCallsSinceSessionRefresh),
|
|
52
|
+
turnsSinceGrowthReview: normalizeCounter(record.turnsSinceGrowthReview),
|
|
53
|
+
toolCallsSinceGrowthReview: normalizeCounter(record.toolCallsSinceGrowthReview),
|
|
54
|
+
lastSessionEntryId: normalizeOptionalString(record.lastSessionEntryId),
|
|
55
|
+
lastSessionRefreshedEntryId: normalizeOptionalString(record.lastSessionRefreshedEntryId),
|
|
56
|
+
lastConsolidatedEntryId: normalizeOptionalString(record.lastConsolidatedEntryId),
|
|
57
|
+
lastReviewedEntryId: normalizeOptionalString(record.lastReviewedEntryId),
|
|
58
|
+
failureBackoffUntil: normalizeOptionalNullableString(record.failureBackoffUntil) ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function readMemoryMaintenanceState(appHomeDir, channelId) {
|
|
62
|
+
const path = getMemoryMaintenanceStatePath(appHomeDir, channelId);
|
|
63
|
+
try {
|
|
64
|
+
const raw = await readFile(path, "utf-8");
|
|
65
|
+
return normalizeState(channelId, JSON.parse(raw));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
69
|
+
return createDefaultState(channelId);
|
|
70
|
+
}
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
log.logWarning(`[${channelId}] Failed to read memory maintenance state; rebuilding defaults`, message);
|
|
73
|
+
return createDefaultState(channelId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function writeMemoryMaintenanceState(appHomeDir, state) {
|
|
77
|
+
const path = getMemoryMaintenanceStatePath(appHomeDir, state.channelId);
|
|
78
|
+
await stateUpdateQueue.run(path, async () => {
|
|
79
|
+
await writeFileAtomically(path, `${JSON.stringify(normalizeState(state.channelId, state), null, 2)}\n`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function updateMemoryMaintenanceState(appHomeDir, channelId, update) {
|
|
83
|
+
const path = getMemoryMaintenanceStatePath(appHomeDir, channelId);
|
|
84
|
+
return stateUpdateQueue.run(path, async () => {
|
|
85
|
+
const current = await readMemoryMaintenanceState(appHomeDir, channelId);
|
|
86
|
+
const next = normalizeState(channelId, update(current));
|
|
87
|
+
await writeFileAtomically(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
88
|
+
return next;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function applyMemoryActivityToState(state, event) {
|
|
92
|
+
const next = {
|
|
93
|
+
...state,
|
|
94
|
+
channelId: event.channelId,
|
|
95
|
+
lastActivityAt: event.timestamp,
|
|
96
|
+
eligibleAfter: event.eligibleAfter ?? state.eligibleAfter,
|
|
97
|
+
lastSessionEntryId: event.latestSessionEntryId ?? state.lastSessionEntryId,
|
|
98
|
+
};
|
|
99
|
+
if (event.kind === "tool-call") {
|
|
100
|
+
next.dirty = true;
|
|
101
|
+
next.toolCallsSinceSessionRefresh += 1;
|
|
102
|
+
next.toolCallsSinceGrowthReview += 1;
|
|
103
|
+
}
|
|
104
|
+
if (event.kind === "assistant-turn-completed") {
|
|
105
|
+
next.dirty = true;
|
|
106
|
+
next.turnsSinceSessionRefresh += 1;
|
|
107
|
+
next.turnsSinceGrowthReview += 1;
|
|
108
|
+
}
|
|
109
|
+
if (event.kind === "boundary") {
|
|
110
|
+
next.dirty = true;
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import { type PostTurnReviewResult } from "./promotion.js";
|
|
4
|
+
export interface PostTurnReviewOptions {
|
|
5
|
+
channelId: string;
|
|
6
|
+
channelDir: string;
|
|
7
|
+
workspaceDir: string;
|
|
8
|
+
workspacePath: string;
|
|
9
|
+
messages: AgentMessage[];
|
|
10
|
+
model: Model<Api>;
|
|
11
|
+
resolveApiKey: (model: Model<Api>) => Promise<string>;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
autoWriteChannelMemory: boolean;
|
|
14
|
+
autoWriteWorkspaceSkills: boolean;
|
|
15
|
+
minMemoryAutoWriteConfidence: number;
|
|
16
|
+
minSkillAutoWriteConfidence: number;
|
|
17
|
+
loadedSkills: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
emitNotice?: (notice: string) => Promise<void>;
|
|
22
|
+
refreshWorkspaceResources?: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface PostTurnReviewApplyResult {
|
|
25
|
+
actions: unknown[];
|
|
26
|
+
suggestions: unknown[];
|
|
27
|
+
skipped: unknown[];
|
|
28
|
+
notices: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function parsePostTurnReviewResult(value: unknown): PostTurnReviewResult;
|
|
31
|
+
export declare function applyPostTurnReviewResult(options: PostTurnReviewOptions, review: PostTurnReviewResult): Promise<PostTurnReviewApplyResult>;
|
|
32
|
+
export declare function runPostTurnReview(options: PostTurnReviewOptions): Promise<PostTurnReviewApplyResult>;
|