@oyasmi/pipiclaw 0.6.3 → 0.6.4
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 +5 -3
- package/dist/agent/channel-runner.d.ts +3 -0
- package/dist/agent/channel-runner.js +51 -0
- 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 +2 -2
- package/dist/index.js +1 -1
- 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 +23 -0
- package/dist/runtime/delivery.js +7 -1
- 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
package/dist/memory/lifecycle.js
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import * as log from "../log.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getDefaultChannelMemoryQueue } from "./channel-maintenance-queue.js";
|
|
3
|
+
import { runInlineConsolidation, } from "./consolidation.js";
|
|
4
|
+
import { appendMemoryReviewLog } from "./review-log.js";
|
|
3
5
|
import { updateChannelSessionMemory } from "./session.js";
|
|
4
|
-
const IDLE_CONSOLIDATION_DELAY_MS = 60_000;
|
|
5
6
|
export class MemoryLifecycle {
|
|
6
7
|
constructor(options) {
|
|
7
8
|
this.options = options;
|
|
8
|
-
this.durableMemoryQueue = Promise.resolve();
|
|
9
9
|
this.sessionRefreshQueue = Promise.resolve();
|
|
10
|
-
this.turnsSinceSessionUpdate = 0;
|
|
11
|
-
this.toolCallsSinceSessionUpdate = 0;
|
|
12
|
-
this.thresholdFailureBackoffTurnsRemaining = 0;
|
|
13
|
-
this.thresholdRefreshQueued = false;
|
|
14
|
-
this.sessionRefreshRunning = false;
|
|
15
10
|
this.durableDirty = false;
|
|
16
11
|
this.durableRevision = 0;
|
|
17
12
|
this.lastAssistantTurnRevision = 0;
|
|
18
13
|
this.lastDurableConsolidationRevision = 0;
|
|
19
|
-
this.
|
|
20
|
-
this.idleConsolidationQueued = false;
|
|
14
|
+
this.channelMemoryQueue = options.channelMemoryQueue ?? getDefaultChannelMemoryQueue();
|
|
21
15
|
}
|
|
22
16
|
buildRunOptions(messages, sessionEntries) {
|
|
23
17
|
return {
|
|
@@ -45,36 +39,20 @@ export class MemoryLifecycle {
|
|
|
45
39
|
};
|
|
46
40
|
}
|
|
47
41
|
noteUserTurnStarted() {
|
|
48
|
-
this.
|
|
42
|
+
this.recordActivity("user-turn-started");
|
|
49
43
|
}
|
|
50
44
|
noteToolCall() {
|
|
51
45
|
this.durableDirty = true;
|
|
52
46
|
this.durableRevision++;
|
|
53
|
-
this.
|
|
54
|
-
this.clearIdleConsolidationTimer();
|
|
47
|
+
this.recordActivity("tool-call");
|
|
55
48
|
}
|
|
56
49
|
noteCompletedAssistantTurn() {
|
|
57
50
|
this.durableDirty = true;
|
|
58
51
|
this.durableRevision++;
|
|
59
52
|
this.lastAssistantTurnRevision = this.durableRevision;
|
|
60
|
-
|
|
61
|
-
if (settings.enabled) {
|
|
62
|
-
this.turnsSinceSessionUpdate++;
|
|
63
|
-
let canTriggerThresholdRefresh = true;
|
|
64
|
-
if (this.thresholdFailureBackoffTurnsRemaining > 0) {
|
|
65
|
-
this.thresholdFailureBackoffTurnsRemaining--;
|
|
66
|
-
canTriggerThresholdRefresh = this.thresholdFailureBackoffTurnsRemaining === 0;
|
|
67
|
-
}
|
|
68
|
-
if (canTriggerThresholdRefresh &&
|
|
69
|
-
(this.turnsSinceSessionUpdate >= settings.minTurnsBetweenUpdate ||
|
|
70
|
-
this.toolCallsSinceSessionUpdate >= settings.minToolCallsBetweenUpdate)) {
|
|
71
|
-
this.requestThresholdSessionRefresh();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
this.scheduleIdleConsolidation();
|
|
53
|
+
this.recordActivity("assistant-turn-completed");
|
|
75
54
|
}
|
|
76
55
|
async flushForShutdown() {
|
|
77
|
-
this.clearIdleConsolidationTimer();
|
|
78
56
|
await this.runDurableMemoryJobSerial(async () => {
|
|
79
57
|
if (!this.hasPendingAssistantSnapshot()) {
|
|
80
58
|
return;
|
|
@@ -86,13 +64,6 @@ export class MemoryLifecycle {
|
|
|
86
64
|
await this.runPreflightConsolidationNow("shutdown", messageSnapshot, sessionEntrySnapshot, revisionSnapshot, settings);
|
|
87
65
|
});
|
|
88
66
|
}
|
|
89
|
-
clearIdleConsolidationTimer() {
|
|
90
|
-
if (!this.idleConsolidationTimer) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
clearTimeout(this.idleConsolidationTimer);
|
|
94
|
-
this.idleConsolidationTimer = null;
|
|
95
|
-
}
|
|
96
67
|
shouldForceRefreshFor(reason, settings) {
|
|
97
68
|
if (!settings.enabled) {
|
|
98
69
|
return false;
|
|
@@ -111,7 +82,6 @@ export class MemoryLifecycle {
|
|
|
111
82
|
return false;
|
|
112
83
|
}
|
|
113
84
|
const { reason } = request;
|
|
114
|
-
this.sessionRefreshRunning = true;
|
|
115
85
|
try {
|
|
116
86
|
await updateChannelSessionMemory({
|
|
117
87
|
channelDir: this.options.channelDir,
|
|
@@ -120,23 +90,14 @@ export class MemoryLifecycle {
|
|
|
120
90
|
resolveApiKey: this.options.resolveApiKey,
|
|
121
91
|
timeoutMs: settings.timeoutMs,
|
|
122
92
|
});
|
|
123
|
-
this.turnsSinceSessionUpdate = 0;
|
|
124
|
-
this.toolCallsSinceSessionUpdate = 0;
|
|
125
|
-
this.thresholdFailureBackoffTurnsRemaining = 0;
|
|
126
93
|
log.logInfo(`[${this.options.channelId}] Session memory updated (${reason})`);
|
|
127
94
|
return true;
|
|
128
95
|
}
|
|
129
96
|
catch (error) {
|
|
130
97
|
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
-
if (reason === "threshold") {
|
|
132
|
-
this.thresholdFailureBackoffTurnsRemaining = Math.max(0, settings.failureBackoffTurns);
|
|
133
|
-
}
|
|
134
98
|
log.logWarning(`[${this.options.channelId}] Session memory update failed (${reason})`, message);
|
|
135
99
|
return false;
|
|
136
100
|
}
|
|
137
|
-
finally {
|
|
138
|
-
this.sessionRefreshRunning = false;
|
|
139
|
-
}
|
|
140
101
|
}
|
|
141
102
|
runSessionRefreshSerial(request) {
|
|
142
103
|
const run = async () => this.refreshSessionMemory(request);
|
|
@@ -144,25 +105,8 @@ export class MemoryLifecycle {
|
|
|
144
105
|
this.sessionRefreshQueue = resultPromise.then(() => undefined, () => undefined);
|
|
145
106
|
return resultPromise;
|
|
146
107
|
}
|
|
147
|
-
requestThresholdSessionRefresh() {
|
|
148
|
-
if (this.thresholdRefreshQueued || this.sessionRefreshRunning) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
this.thresholdRefreshQueued = true;
|
|
152
|
-
void this.runSessionRefreshSerial({ reason: "threshold" }).finally(() => {
|
|
153
|
-
this.thresholdRefreshQueued = false;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
108
|
runDurableMemoryJobSerial(job) {
|
|
157
|
-
|
|
158
|
-
this.durableMemoryQueue = resultPromise.then(() => undefined, () => undefined);
|
|
159
|
-
return resultPromise;
|
|
160
|
-
}
|
|
161
|
-
enqueueDurableMemoryJob(job, failureMessage) {
|
|
162
|
-
void this.runDurableMemoryJobSerial(job).catch((error) => {
|
|
163
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
log.logWarning(failureMessage, message);
|
|
165
|
-
});
|
|
109
|
+
return this.channelMemoryQueue.run(this.options.channelId, job);
|
|
166
110
|
}
|
|
167
111
|
hasPendingAssistantSnapshot() {
|
|
168
112
|
return this.durableDirty && this.lastAssistantTurnRevision > this.lastDurableConsolidationRevision;
|
|
@@ -178,40 +122,41 @@ export class MemoryLifecycle {
|
|
|
178
122
|
}
|
|
179
123
|
log.logInfo(`[${this.options.channelId}] Memory consolidation finished (${reason}): memory entries=${result.appendedMemoryEntries}, history=${result.appendedHistoryBlock ? "yes" : "no"}`);
|
|
180
124
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
125
|
+
async appendReviewLog(entry) {
|
|
126
|
+
try {
|
|
127
|
+
await appendMemoryReviewLog(this.options.channelDir, {
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
channelId: this.options.channelId,
|
|
130
|
+
...entry,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
log.logWarning(`[${this.options.channelId}] Failed to write memory review log`, message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async recordConsolidationReview(reason, result) {
|
|
139
|
+
if (result.skipped) {
|
|
140
|
+
await this.appendReviewLog({
|
|
141
|
+
reason,
|
|
142
|
+
skipped: [{ target: "consolidation", reason: "no meaningful snapshot" }],
|
|
143
|
+
});
|
|
184
144
|
return;
|
|
185
145
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const result = await runInlineConsolidation(this.buildRunOptions(messageSnapshot, sessionEntrySnapshot));
|
|
199
|
-
this.markDurableConsolidationCheckpoint(revisionSnapshot);
|
|
200
|
-
this.logConsolidationResult("idle", result);
|
|
201
|
-
const maintenance = await runBackgroundMaintenance(this.buildRunOptions(messageSnapshot, sessionEntrySnapshot));
|
|
202
|
-
this.logBackgroundResult(maintenance);
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
this.idleConsolidationQueued = false;
|
|
206
|
-
if (this.durableDirty) {
|
|
207
|
-
this.scheduleIdleConsolidation();
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}, `[${this.options.channelId}] Memory consolidation failed (idle)`);
|
|
211
|
-
}, IDLE_CONSOLIDATION_DELAY_MS);
|
|
146
|
+
const actions = [];
|
|
147
|
+
const skipped = [];
|
|
148
|
+
if (result.appendedMemoryEntries > 0) {
|
|
149
|
+
actions.push({ target: "MEMORY.md", action: "append", entries: result.appendedMemoryEntries });
|
|
150
|
+
}
|
|
151
|
+
if (result.appendedHistoryBlock) {
|
|
152
|
+
actions.push({ target: "HISTORY.md", action: "append" });
|
|
153
|
+
}
|
|
154
|
+
else if (reason === "idle") {
|
|
155
|
+
skipped.push({ target: "HISTORY.md", reason: "idle does not write HISTORY.md" });
|
|
156
|
+
}
|
|
157
|
+
await this.appendReviewLog({ reason, actions, skipped });
|
|
212
158
|
}
|
|
213
159
|
async runPreflightConsolidation(reason, messages, sessionEntries) {
|
|
214
|
-
this.clearIdleConsolidationTimer();
|
|
215
160
|
const messageSnapshot = [...(messages ?? this.options.getMessages())];
|
|
216
161
|
const sessionEntrySnapshot = sessionEntries ? [...sessionEntries] : [...this.options.getSessionEntries()];
|
|
217
162
|
const revisionSnapshot = this.durableRevision;
|
|
@@ -229,20 +174,29 @@ export class MemoryLifecycle {
|
|
|
229
174
|
}
|
|
230
175
|
try {
|
|
231
176
|
log.logInfo(`[${this.options.channelId}] Memory consolidation starting (${reason})`);
|
|
232
|
-
const result = await runInlineConsolidation(
|
|
177
|
+
const result = await runInlineConsolidation({
|
|
178
|
+
...this.buildRunOptions(messageSnapshot, sessionEntrySnapshot),
|
|
179
|
+
mode: "boundary",
|
|
180
|
+
});
|
|
233
181
|
this.markDurableConsolidationCheckpoint(revisionSnapshot);
|
|
234
182
|
this.logConsolidationResult(reason, result);
|
|
183
|
+
await this.recordConsolidationReview(reason, result);
|
|
235
184
|
}
|
|
236
185
|
catch (error) {
|
|
237
186
|
const message = error instanceof Error ? error.message : String(error);
|
|
238
187
|
log.logWarning(`[${this.options.channelId}] Memory consolidation failed (${reason})`, message);
|
|
188
|
+
await this.appendReviewLog({
|
|
189
|
+
reason,
|
|
190
|
+
error: message,
|
|
191
|
+
skipped: [{ target: "consolidation", reason: "failed" }],
|
|
192
|
+
});
|
|
239
193
|
}
|
|
240
194
|
}
|
|
241
195
|
async handleSessionBeforeCompact(event) {
|
|
242
196
|
await this.runPreflightConsolidation("compaction", event.preparation.messagesToSummarize);
|
|
243
197
|
}
|
|
244
198
|
handleSessionCompact(_event) {
|
|
245
|
-
this.
|
|
199
|
+
this.recordActivity("boundary");
|
|
246
200
|
}
|
|
247
201
|
async handleSessionBeforeSwitch(event) {
|
|
248
202
|
if (event.reason !== "new") {
|
|
@@ -254,22 +208,23 @@ export class MemoryLifecycle {
|
|
|
254
208
|
if (event.reason !== "new") {
|
|
255
209
|
return;
|
|
256
210
|
}
|
|
257
|
-
this.
|
|
258
|
-
}
|
|
259
|
-
enqueueBackgroundMaintenance() {
|
|
260
|
-
this.enqueueDurableMemoryJob(async () => {
|
|
261
|
-
const result = await runBackgroundMaintenance(this.buildRunOptions([], []));
|
|
262
|
-
this.logBackgroundResult(result);
|
|
263
|
-
}, `[${this.options.channelId}] Background memory maintenance failed`);
|
|
211
|
+
this.recordActivity("boundary");
|
|
264
212
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
213
|
+
recordActivity(kind) {
|
|
214
|
+
const now = new Date();
|
|
215
|
+
const latestSessionEntryId = this.options.getSessionEntries().at(-1)?.id;
|
|
216
|
+
const event = {
|
|
217
|
+
kind,
|
|
218
|
+
channelId: this.options.channelId,
|
|
219
|
+
timestamp: now.toISOString(),
|
|
220
|
+
latestSessionEntryId,
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
void this.options.recordMemoryActivity?.(event);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
log.logWarning(`[${this.options.channelId}] Failed to record memory activity`, message);
|
|
268
228
|
}
|
|
269
|
-
const details = [
|
|
270
|
-
`memory cleanup=${result.cleanedMemory ? "yes" : "no"}`,
|
|
271
|
-
`history fold=${result.foldedHistory ? "yes" : "no"}`,
|
|
272
|
-
].join(", ");
|
|
273
|
-
log.logInfo(`[${this.options.channelId}] Background memory maintenance complete: ${details}`);
|
|
274
229
|
}
|
|
275
230
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PipiclawMemoryGrowthSettings, PipiclawMemoryMaintenanceSettings, PipiclawSessionMemorySettings } from "../settings.js";
|
|
2
|
+
import type { MemoryMaintenanceState } from "./maintenance-state.js";
|
|
3
|
+
export type MaintenanceJobKind = "session-refresh" | "durable-consolidation" | "growth-review" | "structural-maintenance";
|
|
4
|
+
export interface MaintenanceGateDecision {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
skipReason?: string;
|
|
7
|
+
jobKind: MaintenanceJobKind;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionRefreshGateInput {
|
|
10
|
+
now: Date;
|
|
11
|
+
state: MemoryMaintenanceState;
|
|
12
|
+
sessionMemory: PipiclawSessionMemorySettings;
|
|
13
|
+
maintenance: PipiclawMemoryMaintenanceSettings;
|
|
14
|
+
channelActive: boolean;
|
|
15
|
+
hasNewSessionEntry: boolean;
|
|
16
|
+
hasMeaningfulMaterial: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DurableConsolidationGateInput {
|
|
19
|
+
now: Date;
|
|
20
|
+
state: MemoryMaintenanceState;
|
|
21
|
+
maintenance: PipiclawMemoryMaintenanceSettings;
|
|
22
|
+
channelActive: boolean;
|
|
23
|
+
hasNewEntry: boolean;
|
|
24
|
+
hasMeaningfulExchange: boolean;
|
|
25
|
+
batchSize: number;
|
|
26
|
+
minBatchSize?: number;
|
|
27
|
+
coveredByGrowthReview?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface GrowthReviewGateInput {
|
|
30
|
+
now: Date;
|
|
31
|
+
state: MemoryMaintenanceState;
|
|
32
|
+
memoryGrowth: PipiclawMemoryGrowthSettings;
|
|
33
|
+
maintenance: PipiclawMemoryMaintenanceSettings;
|
|
34
|
+
channelActive: boolean;
|
|
35
|
+
hasNewEntry: boolean;
|
|
36
|
+
hasMeaningfulMaterial: boolean;
|
|
37
|
+
hasPromotionSignal: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface StructuralMaintenanceGateInput {
|
|
40
|
+
now: Date;
|
|
41
|
+
state: MemoryMaintenanceState;
|
|
42
|
+
maintenance: PipiclawMemoryMaintenanceSettings;
|
|
43
|
+
channelActive: boolean;
|
|
44
|
+
memoryCleanupNeeded: boolean;
|
|
45
|
+
historyFoldingNeeded: boolean;
|
|
46
|
+
hasMemoryContent: boolean;
|
|
47
|
+
hasHistoryContent: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface StructuralMaintenanceGateDecision extends MaintenanceGateDecision {
|
|
50
|
+
runMemoryCleanup: boolean;
|
|
51
|
+
runHistoryFolding: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function shouldRunSessionRefresh(input: SessionRefreshGateInput): MaintenanceGateDecision;
|
|
54
|
+
export declare function shouldRunDurableConsolidation(input: DurableConsolidationGateInput): MaintenanceGateDecision;
|
|
55
|
+
export declare function shouldRunGrowthReview(input: GrowthReviewGateInput): MaintenanceGateDecision;
|
|
56
|
+
export declare function shouldRunStructuralMaintenance(input: StructuralMaintenanceGateInput): StructuralMaintenanceGateDecision;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
function deny(jobKind, skipReason) {
|
|
2
|
+
return { allowed: false, jobKind, skipReason };
|
|
3
|
+
}
|
|
4
|
+
function allow(jobKind) {
|
|
5
|
+
return { allowed: true, jobKind };
|
|
6
|
+
}
|
|
7
|
+
function parseTime(value) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const time = Date.parse(value);
|
|
12
|
+
return Number.isFinite(time) ? time : null;
|
|
13
|
+
}
|
|
14
|
+
function isBeforeOptional(now, value) {
|
|
15
|
+
const time = parseTime(value ?? undefined);
|
|
16
|
+
return time !== null && now.getTime() < time;
|
|
17
|
+
}
|
|
18
|
+
function hasIntervalElapsed(now, lastRunAt, intervalMs) {
|
|
19
|
+
const lastRunTime = parseTime(lastRunAt);
|
|
20
|
+
return lastRunTime === null || now.getTime() - lastRunTime >= intervalMs;
|
|
21
|
+
}
|
|
22
|
+
function minutesToMs(minutes) {
|
|
23
|
+
return Math.max(0, minutes) * 60_000;
|
|
24
|
+
}
|
|
25
|
+
function hoursToMs(hours) {
|
|
26
|
+
return Math.max(0, hours) * 3_600_000;
|
|
27
|
+
}
|
|
28
|
+
function sessionRefreshThresholdMet(state, settings) {
|
|
29
|
+
return (state.turnsSinceSessionRefresh >= settings.minTurnsBetweenUpdate ||
|
|
30
|
+
state.toolCallsSinceSessionRefresh >= settings.minToolCallsBetweenUpdate);
|
|
31
|
+
}
|
|
32
|
+
function growthReviewThresholdMet(state, settings) {
|
|
33
|
+
return (state.turnsSinceGrowthReview >= settings.minTurnsBetweenReview ||
|
|
34
|
+
state.toolCallsSinceGrowthReview >= settings.minToolCallsBetweenReview);
|
|
35
|
+
}
|
|
36
|
+
export function shouldRunSessionRefresh(input) {
|
|
37
|
+
if (!input.sessionMemory.enabled) {
|
|
38
|
+
return deny("session-refresh", "disabled");
|
|
39
|
+
}
|
|
40
|
+
if (!input.state.dirty) {
|
|
41
|
+
return deny("session-refresh", "clean");
|
|
42
|
+
}
|
|
43
|
+
if (isBeforeOptional(input.now, input.state.eligibleAfter)) {
|
|
44
|
+
return deny("session-refresh", "not-idle-yet");
|
|
45
|
+
}
|
|
46
|
+
if (input.channelActive) {
|
|
47
|
+
return deny("session-refresh", "channel-active");
|
|
48
|
+
}
|
|
49
|
+
if (isBeforeOptional(input.now, input.state.failureBackoffUntil)) {
|
|
50
|
+
return deny("session-refresh", "backoff-active");
|
|
51
|
+
}
|
|
52
|
+
if (!hasIntervalElapsed(input.now, input.state.lastSessionRefreshAt, minutesToMs(input.maintenance.sessionRefreshIntervalMinutes))) {
|
|
53
|
+
return deny("session-refresh", "interval-not-elapsed");
|
|
54
|
+
}
|
|
55
|
+
if (!sessionRefreshThresholdMet(input.state, input.sessionMemory)) {
|
|
56
|
+
return deny("session-refresh", "threshold-not-met");
|
|
57
|
+
}
|
|
58
|
+
if (!input.hasNewSessionEntry) {
|
|
59
|
+
return deny("session-refresh", "no-new-session-entry");
|
|
60
|
+
}
|
|
61
|
+
if (!input.hasMeaningfulMaterial) {
|
|
62
|
+
return deny("session-refresh", "no-meaningful-material");
|
|
63
|
+
}
|
|
64
|
+
return allow("session-refresh");
|
|
65
|
+
}
|
|
66
|
+
export function shouldRunDurableConsolidation(input) {
|
|
67
|
+
if (!input.state.dirty) {
|
|
68
|
+
return deny("durable-consolidation", "clean");
|
|
69
|
+
}
|
|
70
|
+
if (isBeforeOptional(input.now, input.state.eligibleAfter)) {
|
|
71
|
+
return deny("durable-consolidation", "not-idle-yet");
|
|
72
|
+
}
|
|
73
|
+
if (input.channelActive) {
|
|
74
|
+
return deny("durable-consolidation", "channel-active");
|
|
75
|
+
}
|
|
76
|
+
if (!hasIntervalElapsed(input.now, input.state.lastDurableConsolidationAt, minutesToMs(input.maintenance.durableConsolidationIntervalMinutes))) {
|
|
77
|
+
return deny("durable-consolidation", "interval-not-elapsed");
|
|
78
|
+
}
|
|
79
|
+
if (isBeforeOptional(input.now, input.state.failureBackoffUntil)) {
|
|
80
|
+
return deny("durable-consolidation", "backoff-active");
|
|
81
|
+
}
|
|
82
|
+
if (!input.hasNewEntry) {
|
|
83
|
+
return deny("durable-consolidation", "no-new-entry");
|
|
84
|
+
}
|
|
85
|
+
if (!input.hasMeaningfulExchange) {
|
|
86
|
+
return deny("durable-consolidation", "no-meaningful-exchange");
|
|
87
|
+
}
|
|
88
|
+
if (input.coveredByGrowthReview) {
|
|
89
|
+
return deny("durable-consolidation", "covered-by-growth-review");
|
|
90
|
+
}
|
|
91
|
+
if (input.batchSize < (input.minBatchSize ?? 2)) {
|
|
92
|
+
return deny("durable-consolidation", "batch-threshold-not-met");
|
|
93
|
+
}
|
|
94
|
+
return allow("durable-consolidation");
|
|
95
|
+
}
|
|
96
|
+
export function shouldRunGrowthReview(input) {
|
|
97
|
+
if (!input.memoryGrowth.postTurnReviewEnabled) {
|
|
98
|
+
return deny("growth-review", "disabled");
|
|
99
|
+
}
|
|
100
|
+
if (!input.state.dirty) {
|
|
101
|
+
return deny("growth-review", "clean");
|
|
102
|
+
}
|
|
103
|
+
if (isBeforeOptional(input.now, input.state.eligibleAfter)) {
|
|
104
|
+
return deny("growth-review", "not-idle-yet");
|
|
105
|
+
}
|
|
106
|
+
if (input.channelActive) {
|
|
107
|
+
return deny("growth-review", "channel-active");
|
|
108
|
+
}
|
|
109
|
+
if (!hasIntervalElapsed(input.now, input.state.lastGrowthReviewAt, minutesToMs(input.maintenance.growthReviewIntervalMinutes))) {
|
|
110
|
+
return deny("growth-review", "interval-not-elapsed");
|
|
111
|
+
}
|
|
112
|
+
if (isBeforeOptional(input.now, input.state.failureBackoffUntil)) {
|
|
113
|
+
return deny("growth-review", "backoff-active");
|
|
114
|
+
}
|
|
115
|
+
if (!growthReviewThresholdMet(input.state, input.memoryGrowth)) {
|
|
116
|
+
return deny("growth-review", "threshold-not-met");
|
|
117
|
+
}
|
|
118
|
+
if (!input.hasNewEntry) {
|
|
119
|
+
return deny("growth-review", "no-new-entry");
|
|
120
|
+
}
|
|
121
|
+
if (!input.hasMeaningfulMaterial) {
|
|
122
|
+
return deny("growth-review", "no-meaningful-material");
|
|
123
|
+
}
|
|
124
|
+
if (!input.hasPromotionSignal) {
|
|
125
|
+
return deny("growth-review", "no-promotion-signal");
|
|
126
|
+
}
|
|
127
|
+
return allow("growth-review");
|
|
128
|
+
}
|
|
129
|
+
export function shouldRunStructuralMaintenance(input) {
|
|
130
|
+
const jobKind = "structural-maintenance";
|
|
131
|
+
const denyStructural = (skipReason) => ({
|
|
132
|
+
allowed: false,
|
|
133
|
+
jobKind,
|
|
134
|
+
skipReason,
|
|
135
|
+
runMemoryCleanup: false,
|
|
136
|
+
runHistoryFolding: false,
|
|
137
|
+
});
|
|
138
|
+
if (input.channelActive) {
|
|
139
|
+
return denyStructural("channel-active");
|
|
140
|
+
}
|
|
141
|
+
if (!hasIntervalElapsed(input.now, input.state.lastStructuralMaintenanceAt, hoursToMs(input.maintenance.structuralMaintenanceIntervalHours))) {
|
|
142
|
+
return denyStructural("interval-not-elapsed");
|
|
143
|
+
}
|
|
144
|
+
if (isBeforeOptional(input.now, input.state.failureBackoffUntil)) {
|
|
145
|
+
return denyStructural("backoff-active");
|
|
146
|
+
}
|
|
147
|
+
if (!input.hasMemoryContent && !input.hasHistoryContent) {
|
|
148
|
+
return denyStructural("empty-template-files");
|
|
149
|
+
}
|
|
150
|
+
const runMemoryCleanup = input.memoryCleanupNeeded;
|
|
151
|
+
const runHistoryFolding = input.historyFoldingNeeded;
|
|
152
|
+
if (!runMemoryCleanup && !runHistoryFolding) {
|
|
153
|
+
return denyStructural("nothing-to-maintain");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
allowed: true,
|
|
157
|
+
jobKind,
|
|
158
|
+
runMemoryCleanup,
|
|
159
|
+
runHistoryFolding,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import type { PipiclawMemoryGrowthSettings, PipiclawMemoryMaintenanceSettings, PipiclawSessionMemorySettings } from "../settings.js";
|
|
5
|
+
import { type ChannelMemoryQueue } from "./channel-maintenance-queue.js";
|
|
6
|
+
import { type MaintenanceJobKind } from "./maintenance-gates.js";
|
|
7
|
+
export interface MaintenanceJobSettings {
|
|
8
|
+
sessionMemory: PipiclawSessionMemorySettings;
|
|
9
|
+
memoryGrowth: PipiclawMemoryGrowthSettings;
|
|
10
|
+
memoryMaintenance: PipiclawMemoryMaintenanceSettings;
|
|
11
|
+
}
|
|
12
|
+
interface BaseMaintenanceJobInput {
|
|
13
|
+
appHomeDir: string;
|
|
14
|
+
channelId: string;
|
|
15
|
+
channelDir: string;
|
|
16
|
+
channelActive: boolean;
|
|
17
|
+
now?: Date;
|
|
18
|
+
settings: MaintenanceJobSettings;
|
|
19
|
+
model: Model<Api>;
|
|
20
|
+
resolveApiKey: (model: Model<Api>) => Promise<string>;
|
|
21
|
+
messages: AgentMessage[];
|
|
22
|
+
sessionEntries: SessionEntry[];
|
|
23
|
+
queue?: ChannelMemoryQueue;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionRefreshJobInput extends BaseMaintenanceJobInput {
|
|
26
|
+
}
|
|
27
|
+
export interface DurableConsolidationJobInput extends BaseMaintenanceJobInput {
|
|
28
|
+
}
|
|
29
|
+
export interface GrowthReviewJobInput extends BaseMaintenanceJobInput {
|
|
30
|
+
workspaceDir: string;
|
|
31
|
+
workspacePath: string;
|
|
32
|
+
loadedSkills: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
}>;
|
|
36
|
+
emitNotice?: (notice: string) => Promise<void>;
|
|
37
|
+
refreshWorkspaceResources?: () => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export interface StructuralMaintenanceJobInput extends BaseMaintenanceJobInput {
|
|
40
|
+
}
|
|
41
|
+
export interface MaintenanceJobResult {
|
|
42
|
+
jobKind: MaintenanceJobKind;
|
|
43
|
+
ran: boolean;
|
|
44
|
+
skipped: boolean;
|
|
45
|
+
skipReason?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function runSessionRefreshJob(input: SessionRefreshJobInput): Promise<MaintenanceJobResult>;
|
|
49
|
+
export declare function runDurableConsolidationJob(input: DurableConsolidationJobInput): Promise<MaintenanceJobResult>;
|
|
50
|
+
export declare function runGrowthReviewJob(input: GrowthReviewJobInput): Promise<MaintenanceJobResult>;
|
|
51
|
+
export declare function runStructuralMaintenanceJob(input: StructuralMaintenanceJobInput): Promise<MaintenanceJobResult>;
|
|
52
|
+
export {};
|