@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.
Files changed (66) hide show
  1. package/README.md +12 -4
  2. package/dist/agent/channel-runner.d.ts +3 -0
  3. package/dist/agent/channel-runner.js +51 -0
  4. package/dist/agent/commands.js +3 -1
  5. package/dist/agent/prompt-builder.js +4 -0
  6. package/dist/agent/session-events.d.ts +1 -0
  7. package/dist/agent/session-events.js +13 -1
  8. package/dist/agent/types.d.ts +2 -0
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +2 -2
  11. package/dist/memory/channel-maintenance-queue.d.ts +5 -0
  12. package/dist/memory/channel-maintenance-queue.js +8 -0
  13. package/dist/memory/consolidation.d.ts +12 -4
  14. package/dist/memory/consolidation.js +54 -23
  15. package/dist/memory/files.js +8 -14
  16. package/dist/memory/lifecycle.d.ts +8 -14
  17. package/dist/memory/lifecycle.js +66 -111
  18. package/dist/memory/maintenance-gates.d.ts +56 -0
  19. package/dist/memory/maintenance-gates.js +161 -0
  20. package/dist/memory/maintenance-jobs.d.ts +52 -0
  21. package/dist/memory/maintenance-jobs.js +310 -0
  22. package/dist/memory/maintenance-state.d.ts +33 -0
  23. package/dist/memory/maintenance-state.js +113 -0
  24. package/dist/memory/post-turn-review.d.ts +32 -0
  25. package/dist/memory/post-turn-review.js +244 -0
  26. package/dist/memory/promotion-signals.d.ts +5 -0
  27. package/dist/memory/promotion-signals.js +34 -0
  28. package/dist/memory/promotion.d.ts +32 -0
  29. package/dist/memory/promotion.js +11 -0
  30. package/dist/memory/recall.d.ts +1 -1
  31. package/dist/memory/recall.js +33 -1
  32. package/dist/memory/review-log.d.ts +13 -0
  33. package/dist/memory/review-log.js +38 -0
  34. package/dist/memory/scheduler.d.ts +52 -0
  35. package/dist/memory/scheduler.js +152 -0
  36. package/dist/memory/session-corpus.d.ts +18 -0
  37. package/dist/memory/session-corpus.js +257 -0
  38. package/dist/memory/session-search.d.ts +30 -0
  39. package/dist/memory/session-search.js +151 -0
  40. package/dist/runtime/bootstrap.d.ts +5 -0
  41. package/dist/runtime/bootstrap.js +39 -2
  42. package/dist/runtime/delivery.js +52 -3
  43. package/dist/runtime/dingtalk.d.ts +11 -1
  44. package/dist/runtime/dingtalk.js +40 -3
  45. package/dist/runtime/events.js +5 -0
  46. package/dist/settings.d.ts +35 -1
  47. package/dist/settings.js +55 -1
  48. package/dist/shared/atomic-file.d.ts +2 -0
  49. package/dist/shared/atomic-file.js +17 -0
  50. package/dist/shared/serial-queue.d.ts +4 -0
  51. package/dist/shared/serial-queue.js +17 -0
  52. package/dist/tools/config.d.ts +10 -0
  53. package/dist/tools/config.js +28 -0
  54. package/dist/tools/index.d.ts +2 -1
  55. package/dist/tools/index.js +32 -0
  56. package/dist/tools/session-search.d.ts +17 -0
  57. package/dist/tools/session-search.js +56 -0
  58. package/dist/tools/skill-list.d.ts +17 -0
  59. package/dist/tools/skill-list.js +86 -0
  60. package/dist/tools/skill-manage.d.ts +34 -0
  61. package/dist/tools/skill-manage.js +138 -0
  62. package/dist/tools/skill-security.d.ts +10 -0
  63. package/dist/tools/skill-security.js +111 -0
  64. package/dist/tools/skill-view.d.ts +12 -0
  65. package/dist/tools/skill-view.js +43 -0
  66. package/package.json +1 -1
@@ -1,23 +1,17 @@
1
1
  import * as log from "../log.js";
2
- import { runBackgroundMaintenance, runInlineConsolidation, } from "./consolidation.js";
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.idleConsolidationTimer = null;
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.clearIdleConsolidationTimer();
42
+ this.recordActivity("user-turn-started");
49
43
  }
50
44
  noteToolCall() {
51
45
  this.durableDirty = true;
52
46
  this.durableRevision++;
53
- this.toolCallsSinceSessionUpdate++;
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
- const settings = this.options.getSessionMemorySettings();
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
- const resultPromise = this.durableMemoryQueue.then(job, job);
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
- scheduleIdleConsolidation() {
182
- this.clearIdleConsolidationTimer();
183
- if (!this.hasPendingAssistantSnapshot() || this.idleConsolidationQueued) {
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
- this.idleConsolidationTimer = setTimeout(() => {
187
- this.idleConsolidationTimer = null;
188
- if (!this.hasPendingAssistantSnapshot() || this.idleConsolidationQueued) {
189
- return;
190
- }
191
- this.idleConsolidationQueued = true;
192
- const messageSnapshot = [...this.options.getMessages()];
193
- const sessionEntrySnapshot = [...this.options.getSessionEntries()];
194
- const revisionSnapshot = this.durableRevision;
195
- this.enqueueDurableMemoryJob(async () => {
196
- try {
197
- log.logInfo(`[${this.options.channelId}] Memory consolidation starting (idle)`);
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(this.buildRunOptions(messageSnapshot, sessionEntrySnapshot));
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.enqueueBackgroundMaintenance();
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.enqueueBackgroundMaintenance();
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
- logBackgroundResult(result) {
266
- if (!result.cleanedMemory && !result.foldedHistory) {
267
- return;
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 {};